You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@beam.apache.org by pa...@apache.org on 2022/09/24 19:45:42 UTC

[beam] branch master updated: Extract playground components (#23253)

This is an automated email from the ASF dual-hosted git repository.

pabloem pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/beam.git


The following commit(s) were added to refs/heads/master by this push:
     new c4fe823b4f9 Extract playground components (#23253)
c4fe823b4f9 is described below

commit c4fe823b4f9e8fc4711478749efb35cd143bfce2
Author: alexeyinkin <al...@akvelon.com>
AuthorDate: Sat Sep 24 23:45:33 2022 +0400

    Extract playground components (#23253)
    
    * [Tour of Beam][Frontend][#22600] TourScreen layout
    
    * theme setup
    
    * Replaced ThemeProvider with ThemeSwitchNotifier
    
    * header with theme mode switcher and logo
    
    * page container with header & footer
    
    * theme mode tests
    
    * renamed the directory to tour-of-beam
    
    * compressed beam_logo.png
    
    * added missing license comments
    
    * rudimentary layout of the first screen
    
    * review comments fixes #1
    
    * moved notifyListeners inside then
    
    * responsive todo
    
    * split into 2 simple functions
    
    * deleted redundant constants &
    replaced 2018 text theme with 2021
    
    * styling refinement
    
    * home screen layout
    
    * clickable sign in text
    
    * font weights fix
    
    * removed _getBaseFontTheme function
    
    * fixed border and bg color
    
    * color fixes
    
    * difficulty component
    
    * _LastModuleBody
    
    * todo in test
    
    * footer border
    
    * fixed overflows
    
    * replaced Project prefix with Tob
    
    * replaced then with await
    
    * inferred type
    
    * started translation of the home screen
    
    * sorted translations
    
    * Complexity comments
    
    * comment fixes
    
    * home screen translations
    
    * sign in overlay
    
    * import fix
    
    * integration test does not fail
    
    * playground_components package with
    dismissible_overlay
    
    * missing license
    
    * removed _dots from build
    
    * widgets refinement
    
    * renamed home screen to welcome screen
    
    * deleted copyWith
    
    * _SdkButton
    
    * trailing comma & pubspec formatting
    
    * license and lints
    
    * license
    
    * removed license from .metadata
    
    * pubspec formatting
    
    * total lints update
    
    * changed from tour_of_beam to
    tour-of-beam in build.gradle.kts
    
    * license check
    
    * _SdkButton mimics Radio button
    
    * renamed MyApp to TourOfBeamApp
    
    * onChanged mimics Radio button
    
    Tour of Beam frontend blank project
    
    [Tour of Beam][Frontend][#22600] TourScreen layout
    
    TourScreen layout (#22600)
    
    common theme, constants, split view
    
    missing license
    
    flutter_gen, summary layout details
    
    content layout details
    
    no functional widgets in split view
    
    main screen todos & translation
    
    main screen todos & translation
    
    comment fixes #1
    
    ExpansionTileWrapper
    
    SplitViewController
    
    lists in tour screen widgets
    
    comment fixes #1 (31.08)
    
    split view package in PGC
    
    fixed button overflow
    
    splitter theme color
    
    comment fixes #2 (31.08)
    
    gradlew check
    
    welcome screen overflow test (#22600)
    
    SDK dropdown (#22600)
    
    flexible complete unit OutlinedButton (#22600)
    
    renamed PageContainer to TobScaffold
    
    dropdown style refinement
    
    DropdownButton implicit type
    
    sdk instead of e
    
    licenses #22600
    
    renamed _ShrinkedTour to _NarrowTour #22600
    
    tour screen style refinement #22600
    
    BeamDivider in PGC #22600
    
    removed todo, added license #22600
    
    built with text #22600
    
    _WideWelcome with IntrinsicHeight (#22600)
    
    Co-Authored-By: darkhan.nausharipov <da...@kzn.akvelon.com>
    
    * addressing review comments #22600
    
    replaced magic numbers #22600
    
    comments (#22600)
    
    comments #22600
    
    comments #22600
    
    comments #22600
    
    comments #22600
    
    comments, flutter 3.3.0 upgrade #22600
    
    renamed ActionPadding to ActionVerticalPadding #22600
    
    actions formatting #22600
    
    * branded sign in buttons #22600
    
    * _BrandedSignInButtons #22600
    
    * _Divider color #22600
    
    * profile #22600
    
    * moved split_view from PGC into ToB #22600
    
    * indentation fix #22600
    
    * split ProfileContent into widgets #22600
    
    * Extract playground components to a separate package (#22600)
    
    * Minor fixes (#22600)
    
    * Address review issues (#22600)
    
    * Upgrade Flutter to v3.3.2 (#22600)
    
    * Add precommit Gradle task for playground_components, add code generation to frontend Gradle task, remove generated mocks, fix linter issues (#22600)
    
    * startTour button (#22600)
    
    * lint fixes (#22600)
    
    * Fix highlighting for Python and SCIO (#22600)
    
    Co-authored-by: darkhan.nausharipov <da...@kzn.akvelon.com>
---
 .github/workflows/build_playground_frontend.yml    |   6 +-
 .gitignore                                         |   1 +
 .../frontend/assets/png/profile-website.png        | Bin 0 -> 614 bytes
 .../frontend/assets/svg/github-logo.svg            |  19 +
 .../frontend/assets/svg/google-logo.svg            |  22 ++
 .../{welcome-progress-0.svg => profile-about.svg}  |   8 +-
 .../frontend/assets/svg/profile-delete.svg         |  19 +
 .../frontend/assets/svg/profile-logout.svg         |  19 +
 ...{welcome-progress-0.svg => unit-progress-0.svg} |   4 +-
 ...elcome-progress-0.svg => unit-progress-100.svg} |   4 +-
 .../frontend/assets/svg/welcome-progress-0.svg     |   2 +-
 .../frontend/assets/translations/en.yaml           |  39 ++-
 .../frontend/integration_test/app_test.dart        |   2 +-
 .../lib/components/expansion_tile_wrapper.dart     |  27 +-
 .../frontend/lib/components/filler_text.dart       |  12 +-
 .../frontend/lib/components/footer.dart            |  43 ++-
 .../login_button.dart}                             |  26 +-
 .../lib/components/login/login_content.dart        | 140 ++++++++
 .../tour-of-beam/frontend/lib/components/logo.dart |  13 +-
 .../sign_in_button.dart => profile/avatar.dart}    |  35 +-
 .../lib/components/profile/profile_content.dart    | 160 +++++++++
 .../{page_container.dart => scaffold.dart}         |  39 ++-
 .../frontend/lib/components/sdk_dropdown.dart      |  64 ++++
 .../sign_in/sign_in_overlay_content.dart           |  89 -----
 .../frontend/lib/config/theme/colors_provider.dart |  86 -----
 .../frontend/lib/config/theme/theme.dart           | 160 ---------
 .../frontend/lib/constants/assets.dart             |  34 --
 .../tour-of-beam/frontend/lib/constants/sizes.dart |  32 +-
 learning/tour-of-beam/frontend/lib/main.dart       |  13 +-
 .../frontend/lib/pages/tour/playground_demo.dart   | 126 +++++++
 .../frontend/lib/pages/tour/screen.dart            | 359 +++++++++++++++++++
 .../frontend/lib/pages/welcome/screen.dart         |  98 +++---
 learning/tour-of-beam/frontend/pubspec.lock        | 382 +++++++++++++++++++--
 learning/tour-of-beam/frontend/pubspec.yaml        |  19 +-
 .../frontend/test/common/test_screen_wrapper.dart  |  27 +-
 ...witch_notifier_test.dart => overflow_test.dart} |  16 +-
 playground/buf.gen.yaml                            |   4 +-
 playground/frontend/Dockerfile                     |  19 +-
 playground/frontend/README.md                      |  12 +
 playground/frontend/assets/theme.svg               |  27 --
 .../pubspec.yaml => assets/translations/en.yaml}   |  21 +-
 playground/frontend/build.gradle                   |  63 ++--
 .../lib/components/banner/banner_description.dart  |   7 +-
 .../dropdown_button/dropdown_button.dart           |   8 +-
 .../playground_run_or_cancel_button.dart           |  57 +++
 .../toggle_theme_button/toggle_theme_button.dart   |  55 ---
 playground/frontend/lib/config/theme.dart          | 305 ----------------
 playground/frontend/lib/configure_web.dart         |  23 --
 playground/frontend/lib/constants/sizes.dart       |  14 -
 playground/frontend/lib/l10n/app_en.arb            |  16 -
 playground/frontend/lib/l10n/l10n.dart             |   6 +-
 playground/frontend/lib/main.dart                  |  31 +-
 .../actions/components/new_example_action.dart     |  10 +-
 .../modules/actions/components/reset_action.dart   |  30 +-
 .../lib/modules/analytics/analytics_service.dart   |   7 +-
 .../analytics/google_analytics_service.dart        |   9 +-
 .../modules/editor/components/editor_themes.dart   |  64 ----
 .../pipeline_options_dropdown_body.dart            |   2 +-
 .../pipeline_options_dropdown_separator.dart       |   4 +-
 .../pipeline_options_form.dart                     |   5 +-
 .../pipeline_options_text_field.dart               |  12 +-
 .../components/share_dropdown/link_text_field.dart |   8 +-
 .../components/share_dropdown/share_button.dart    |  18 +-
 .../share_dropdown/share_dropdown_body.dart        |   2 +-
 .../share_dropdown/share_tabs/share_tabs.dart      |  12 +-
 .../share_tabs/snippet_save_and_share_tabs.dart    |  12 +-
 .../share_dropdown/share_tabs_headers.dart         |  28 +-
 .../code_client/grpc_code_client.dart              | 234 -------------
 .../description_popover/description_popover.dart   |   4 +-
 .../description_popover_button.dart                |   9 +-
 .../example_list/category_expansion_panel.dart     |   4 +-
 .../example_list/example_item_actions.dart         |   7 +-
 .../components/example_list/example_list.dart      |   6 +-
 .../example_list/expansion_panel_item.dart         |  25 +-
 .../components/filter/category_bubble.dart         |  55 +--
 .../examples/components/filter/type_filter.dart    |   2 +-
 .../multifile_popover/multifile_popover.dart       |   4 +-
 .../multifile_popover_button.dart                  |   6 +-
 .../components/search_field/search_field.dart      |  11 +-
 .../lib/modules/examples/example_selector.dart     |  46 ++-
 ...catalog_default_example_loading_descriptor.dart |  44 ---
 .../examples_loading_descriptor_factory.dart       |  29 +-
 .../standard_example_loading_descriptor.dart       |  46 ---
 .../user_shared_example_loading_descriptor.dart    |  47 ---
 .../examples/models/example_token_type.dart        |   4 +-
 .../example_client/example_client.dart             |  66 ----
 .../example_client/grpc_example_client.dart        | 366 --------------------
 .../examples/repositories/example_repository.dart  |  98 ------
 .../repositories/models/get_example_request.dart   |  37 --
 .../models/get_list_of_examples_request.dart       |  37 --
 .../models/get_list_of_examples_response.dart      |  26 --
 .../messages/handlers/messages_handler.dart        |   8 +-
 .../handlers/set_content_message_handler.dart      |   8 +-
 .../messages/handlers/set_sdk_message_handler.dart |   8 +-
 .../messages/models/set_content_message.dart       |   2 +-
 .../modules/messages/models/set_sdk_message.dart   |   8 +-
 .../lib/modules/output/components/output_area.dart |  74 ----
 .../output_header/result_filter_bubble.dart        |  80 -----
 .../output_header/result_filter_popover.dart       |  75 ----
 .../modules/output/models/output_placement.dart    |   7 +
 .../lib/modules/sdk/components/sdk_selector.dart   |  41 ++-
 .../modules/sdk/components/sdk_selector_row.dart   |   6 +-
 .../frontend/lib/modules/sdk/models/sdk.dart       | 117 -------
 .../modules/shortcuts/components/shortcut_row.dart |   7 +-
 .../shortcuts/components/shortcuts_manager.dart    |   4 +-
 .../shortcuts/components/shortcuts_modal.dart      |  32 +-
 .../shortcuts/constants/global_shortcuts.dart      |  67 +---
 .../shortcuts/utils/shortcuts_display_name.dart    |  36 --
 .../components/embedded_actions.dart               |  14 +-
 .../components/embedded_appbar_title.dart          |  48 +--
 .../components/embedded_editor.dart                |  23 +-
 .../embedded_playground_page.dart                  |  13 +-
 .../playground/components/close_listener.dart      |   4 +-
 .../components/editor_textarea_wrapper.dart        |  77 +----
 .../feedback/feedback_dropdown_content.dart        |  13 +-
 .../pages/playground/components/more_actions.dart  |  18 +-
 .../components/playground_page_body.dart           |  35 +-
 .../components/playground_page_footer.dart         |   6 +-
 .../components/playground_page_providers.dart      |  54 ++-
 .../lib/pages/playground/playground_page.dart      |  76 ++--
 .../states/example_loaders/examples_loader.dart    | 110 ------
 .../playground/states/example_selector_state.dart  |  32 +-
 .../pages/playground/states/examples_state.dart    | 218 ------------
 playground/frontend/lib/playground_app.dart        |  54 ++-
 playground/frontend/lib/utils/analytics_utils.dart |  11 +-
 .../assets/buttons}/reset.svg                      |   0
 .../assets/buttons}/theme-mode.svg                 |   0
 .../assets/notification_icons/error.svg}           |   0
 .../assets/notification_icons/info.svg}            |   0
 .../assets/notification_icons/success.svg}         |   0
 .../assets/notification_icons/warning.svg}         |   0
 .../assets/png/beam-logo.png                       | Bin
 .../assets/svg/drag-horizontal.svg}                |   7 +-
 .../assets/svg/drag-vertical.svg}                  |   7 +-
 .../{pubspec.yaml => assets/translations/en.yaml}  |  44 ++-
 .../playground_components/build.gradle.kts         |  90 +++++
 .../lib/playground_components.dart                 |  73 ++++
 .../lib/src/api/iis_workaround_channel.dart}       |   5 +-
 .../src/api/iis_workaround_channel_non_web.dart}   |  14 +-
 .../lib/src/api/iis_workaround_channel_web.dart}   |   9 +-
 .../lib/src}/api/v1/api.pb.dart                    |   0
 .../lib/src}/api/v1/api.pbenum.dart                |   0
 .../lib/src}/api/v1/api.pbgrpc.dart                |   0
 .../lib/src}/api/v1/api.pbjson.dart                |   0
 .../lib/src/cache/example_cache.dart               | 238 +++++++++++++
 .../lib/src}/constants/colors.dart                 |  44 ++-
 .../lib/src}/constants/links.dart                  |   2 +-
 .../lib/src/constants/playground_components.dart}  |  18 +-
 .../lib/src}/constants/sizes.dart                  |  40 +--
 .../catalog_default_example_loader.dart            |  20 +-
 .../example_loaders/content_example_loader.dart    |  13 +-
 .../example_loaders/empty_example_loader.dart      |  14 +-
 .../example_loaders/example_loader.dart            |   4 +-
 .../example_loaders/example_loader_factory.dart    |  53 +++
 .../example_loaders/examples_loader.dart           | 103 ++++++
 .../example_loaders/standard_example_loader.dart   |  35 +-
 .../user_shared_example_loader.dart                |  16 +-
 .../src/controllers/playground_controller.dart}    | 143 ++++----
 .../controllers/snippet_editing_controller.dart    |  22 +-
 .../lib/src/enums/complexity.dart}                 |   8 +-
 .../lib/src/models/category_with_examples.dart}    |  28 +-
 .../lib/src/models/example.dart                    |  61 ++++
 .../lib/src/models/example_base.dart}              |  65 +---
 ...atalog_default_example_loading_descriptor.dart} |  16 +-
 .../content_example_loading_descriptor.dart        |  33 +-
 .../empty_example_loading_descriptor.dart}         |  16 +-
 .../example_loading_descriptor.dart}               |   8 +-
 .../examples_loading_descriptor.dart               |  13 +-
 .../standard_example_loading_descriptor.dart}      |  14 +-
 .../user_shared_example_loading_descriptor.dart}   |  14 +-
 .../lib/src/models/intents.dart}                   |  24 +-
 .../lib/src/models/outputs.dart}                   |   8 +-
 .../playground_components/lib/src/models/sdk.dart  |  96 ++++++
 .../lib/src}/models/shortcut.dart                  |  27 +-
 .../lib/src/notifications}/base_notification.dart  |  21 +-
 .../lib/src/notifications}/notification.dart       |  23 +-
 .../src/repositories}/code_client/code_client.dart |  18 +-
 .../repositories/code_client/grpc_code_client.dart | 256 ++++++++++++++
 .../lib/src/repositories}/code_repository.dart     |  74 ++--
 .../example_client/example_client.dart             |  67 ++++
 .../example_client/grpc_example_client.dart        | 376 ++++++++++++++++++++
 .../lib/src/repositories/example_repository.dart   |  99 ++++++
 .../models}/check_status_response.dart             |   6 +-
 .../get_default_precompiled_object_request.dart}   |  19 +-
 .../get_precompiled_object_code_response.dart}     |   6 +-
 .../models/get_precompiled_object_request.dart}    |  24 +-
 .../models/get_precompiled_object_response.dart}   |  10 +-
 .../models/get_precompiled_objects_request.dart}   |  22 +-
 .../models/get_precompiled_objects_response.dart}  |  11 +-
 .../repositories/models/get_snippet_request.dart   |   4 +-
 .../repositories/models/get_snippet_response.dart  |   6 +-
 .../src/repositories/models}/output_response.dart  |   5 +-
 .../src/repositories/models}/run_code_error.dart   |   4 +-
 .../src/repositories/models}/run_code_request.dart |   8 +-
 .../repositories/models}/run_code_response.dart    |   4 +-
 .../src/repositories/models}/run_code_result.dart  |  28 +-
 .../repositories/models/save_snippet_request.dart  |  10 +-
 .../repositories/models/save_snippet_response.dart |   0
 .../lib/src/repositories/models/shared_file.dart}  |   0
 .../lib/src/repositories/sdk_grpc_extension.dart}  |  42 ++-
 .../lib/src}/theme/switch_notifier.dart            |  19 +-
 .../playground_components/lib/src/theme/theme.dart | 367 ++++++++++++++++++++
 .../lib/src/util/pipeline_options.dart}            |   0
 .../lib/src/util}/replace_incorrect_symbols.dart   |   6 +-
 .../lib/src/util}/run_with_retry.dart              |   0
 .../lib/src/widgets/bubble.dart                    |  69 ++++
 .../lib/src/widgets}/complexity.dart               |  15 +-
 .../lib/{ => src/widgets}/dismissible_overlay.dart |   0
 .../lib/src/widgets/divider.dart}                  |  17 +-
 .../lib/src/widgets/drag_handle.dart}              |  28 +-
 .../lib/src/widgets}/editor_textarea.dart          |  54 ++-
 .../lib/src/widgets}/header_icon_button.dart       |   9 +-
 .../lib/src/widgets}/loading_indicator.dart        |  12 +-
 .../widgets/logo.dart}                             |  25 +-
 .../lib/src/widgets/output/graph}/graph.dart       |  21 +-
 .../output}/graph/graph_builder/canvas_drawer.dart |  22 +-
 .../graph_builder/extractors/edge_extractor.dart   |   4 +-
 .../extractors/element_extractor.dart              |   6 +-
 .../graph_builder/extractors/extractor_utils.dart  |   0
 .../graph/graph_builder/extractors/extractors.dart |   0
 .../graph_builder/extractors/label_extractor.dart  |   2 +-
 .../output}/graph/graph_builder/graph_builder.dart |  51 +--
 .../graph/graph_builder/painters/edge_painter.dart |  21 +-
 .../graph_builder/painters/graph_painter.dart      |  21 +-
 .../graph/graph_builder/painters/node_painter.dart |  18 +-
 .../src/widgets/output}/graph/models/graph.dart    |   0
 .../widgets/output}/graph/models/table_cell.dart   |   0
 .../lib/src/widgets/output}/output.dart            |  45 +--
 .../lib/src/widgets/output/output_area.dart}       |  53 +--
 .../lib/src/widgets/output}/output_result.dart     |  13 +-
 .../lib/src/widgets/output}/output_tab.dart        |  31 +-
 .../lib/src/widgets/output}/output_tabs.dart       |  57 ++-
 .../src/widgets/output/result_filter_bubble.dart   |  46 ++-
 .../src/widgets/output/result_filter_popover.dart  |  84 +++++
 .../lib/src/widgets/reset_button.dart}             |  38 +-
 .../lib/src/widgets}/run_button.dart               |  39 ++-
 .../lib/src/widgets/run_or_cancel_button.dart      |  63 ++++
 .../lib/src/widgets}/shortcut_tooltip.dart         |  12 +-
 .../lib/src/widgets/snippet_editor.dart            |  44 +--
 .../lib/src/widgets}/split_view.dart               |  47 ++-
 .../lib/src/widgets}/tab_header.dart               |   6 +-
 .../lib/src/widgets}/toggle_theme_button.dart      |  25 +-
 .../lib/src/widgets}/toggle_theme_icon_button.dart |  19 +-
 .../frontend/playground_components/pubspec.yaml    |  39 ++-
 .../test/src/cache/example_cache_test.dart}        | 124 ++++---
 .../test/src/common/categories.dart}               |  42 +--
 .../test/src/common}/example_repository_mock.dart  |  14 +-
 .../test/src/common/examples.dart}                 |  30 +-
 .../test/src/common/requests.dart                  |  66 ++++
 .../controllers/playground_controller_test.dart}   |  41 ++-
 .../src/repositories}/code_repository_test.dart    | 126 +++----
 .../src/repositories/example_repository_test.dart  | 130 +++++++
 .../test/src/util/pipeline_options_test.dart}      |   2 +-
 .../test/src/util}/run_with_retry_test.dart        |   2 +-
 .../test}/theme/switch_notifier_test.dart          |   2 +-
 playground/frontend/pubspec.lock                   | 102 ++++--
 playground/frontend/pubspec.yaml                   |  19 +-
 .../code_repository_test.mocks.dart                | 138 --------
 .../example_repository_test.dart                   | 130 -------
 .../example_repository_test.mocks.dart             | 147 --------
 .../messages/handlers/messages_debouncer_test.dart |  10 +-
 .../messages/models/set_content_message_test.dart  |  17 +-
 .../messages/models/set_sdk_message_test.dart      |   6 +-
 .../messages/parsers/message_parser_test.dart      |  10 +-
 .../states/example_selector_state_test.dart        |  38 +-
 .../states/example_selector_state_test.mocks.dart  |  69 ----
 .../playground/states/mocks/categories_mock.dart   |  45 ---
 .../mocks/example_repository_mock.mocks.dart       | 113 ------
 .../playground/states/mocks/request_mock.dart      |  45 ---
 .../states/playground_state_test.mocks.dart        | 216 ------------
 settings.gradle.kts                                |   1 +
 271 files changed, 5813 insertions(+), 5543 deletions(-)

diff --git a/.github/workflows/build_playground_frontend.yml b/.github/workflows/build_playground_frontend.yml
index f71fc314b7a..a27ce08d07a 100644
--- a/.github/workflows/build_playground_frontend.yml
+++ b/.github/workflows/build_playground_frontend.yml
@@ -37,7 +37,7 @@ jobs:
       GO_VERSION: 1.18.0
       BEAM_VERSION: 2.40.0
       TERRAFORM_VERSION: 1.0.9
-      FLUTTER_VERSION: 3.0.1-stable
+      FLUTTER_VERSION: 3.3.2
       STAND_SUFFIX: ''
       GOOGLE_DOMAIN: '-dot-apache-beam-testing.appspot.com'
     steps:
@@ -49,8 +49,8 @@ jobs:
             java-version: '8'
       - name: install flutter
         run: |
-             wget https://storage.googleapis.com/flutter_infra_release/releases/stable/linux/flutter_linux_$FLUTTER_VERSION.tar.xz &&\
-             tar -xf flutter_linux_$FLUTTER_VERSION.tar.xz &&\
+             wget https://storage.googleapis.com/flutter_infra_release/releases/stable/linux/flutter_linux_$FLUTTER_VERSION-stable.tar.xz &&\
+             tar -xf flutter_linux_$FLUTTER_VERSION-stable.tar.xz &&\
              mv flutter /opt/ &&\
              ln -s /opt/flutter/bin/flutter /usr/local/bin/flutter &&\
              ln -s /opt/flutter/bin/dart /usr/local/bin/dart &&\
diff --git a/.gitignore b/.gitignore
index 443ced0aaee..ec600f50f83 100644
--- a/.gitignore
+++ b/.gitignore
@@ -125,6 +125,7 @@ website/www/yarn-error.log
 **/.flutter-plugins
 **/.flutter-plugins-dependencies
 **/generated_plugin_registrant.dart
+**/*.mocks.dart
 
 # Ignore Beam Playground Terraform
 **/.terraform
diff --git a/learning/tour-of-beam/frontend/assets/png/profile-website.png b/learning/tour-of-beam/frontend/assets/png/profile-website.png
new file mode 100644
index 00000000000..e0b25a42aaf
Binary files /dev/null and b/learning/tour-of-beam/frontend/assets/png/profile-website.png differ
diff --git a/learning/tour-of-beam/frontend/assets/svg/github-logo.svg b/learning/tour-of-beam/frontend/assets/svg/github-logo.svg
new file mode 100644
index 00000000000..495172be937
--- /dev/null
+++ b/learning/tour-of-beam/frontend/assets/svg/github-logo.svg
@@ -0,0 +1,19 @@
+<!-- Licensed to the Apache Software Foundation (ASF) under one
+or more contributor license agreements.  See the NOTICE file
+distributed with this work for additional information
+regarding copyright ownership.  The ASF licenses this file
+to you under the Apache License, Version 2.0 (the
+"License"); you may not use this file except in compliance
+with the License.  You may obtain a copy of the License at
+ 
+      http://www.apache.org/licenses/LICENSE-2.0
+ 
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License. -->
+
+<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
+    <path fill-rule="evenodd" clip-rule="evenodd" d="M10 0C4.475 0 0 4.475 0 10C0 14.425 2.8625 18.1625 6.8375 19.4875C7.3375 19.575 7.525 19.275 7.525 19.0125C7.525 18.775 7.5125 17.9875 7.5125 17.15C5 17.6125 4.35 16.5375 4.15 15.975C4.0375 15.6875 3.55 14.8 3.125 14.5625C2.775 14.375 2.275 13.9125 3.1125 13.9C3.9 13.8875 4.4625 14.625 4.65 14.925C5.55 16.4375 6.9875 16.0125 7.5625 15.75C7.65 15.1 7.9125 14.6625 8.2 14.4125C5.975 14.1625 3.65 13.3 3.65 9.475C3.65 8.3875 4.0375 7.4875 4 [...]
+</svg>
\ No newline at end of file
diff --git a/learning/tour-of-beam/frontend/assets/svg/google-logo.svg b/learning/tour-of-beam/frontend/assets/svg/google-logo.svg
new file mode 100644
index 00000000000..b704ef2cc7c
--- /dev/null
+++ b/learning/tour-of-beam/frontend/assets/svg/google-logo.svg
@@ -0,0 +1,22 @@
+<!-- Licensed to the Apache Software Foundation (ASF) under one
+or more contributor license agreements.  See the NOTICE file
+distributed with this work for additional information
+regarding copyright ownership.  The ASF licenses this file
+to you under the Apache License, Version 2.0 (the
+"License"); you may not use this file except in compliance
+with the License.  You may obtain a copy of the License at
+ 
+      http://www.apache.org/licenses/LICENSE-2.0
+ 
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License. -->
+
+<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
+    <path d="M15.8299 8.18002C15.8299 7.65335 15.7833 7.15335 15.7033 6.66669H8.16992V9.67336H12.4833C12.2899 10.66 11.7233 11.4934 10.8833 12.06V14.06H13.4566C14.9633 12.6667 15.8299 10.6134 15.8299 8.18002Z" fill="#3E67F6" />
+    <path d="M8.17003 16C10.33 16 12.1367 15.28 13.4567 14.06L10.8834 12.06C10.1634 12.54 9.25003 12.8333 8.17003 12.8333C6.08336 12.8333 4.3167 11.4267 3.68336 9.52667H1.03003V11.5867C2.34336 14.2 5.04336 16 8.17003 16Z" fill="#37AC66" />
+    <path d="M3.68326 9.52666C3.51659 9.04666 3.42992 8.53333 3.42992 7.99999C3.42992 7.46666 3.52326 6.95333 3.68326 6.47333V4.41333H1.02992C0.483255 5.49333 0.169922 6.70666 0.169922 7.99999C0.169922 9.29333 0.483255 10.5067 1.02992 11.5867L3.68326 9.52666Z" fill="#EEAB00" />
+    <path d="M8.17003 3.16667C9.35003 3.16667 10.4034 3.57334 11.2367 4.36667L13.5167 2.08667C12.1367 0.793334 10.33 0 8.17003 0C5.04336 0 2.34336 1.8 1.03003 4.41334L3.68336 6.47334C4.3167 4.57334 6.08336 3.16667 8.17003 3.16667Z" fill="#E54545" />
+</svg>
\ No newline at end of file
diff --git a/learning/tour-of-beam/frontend/assets/svg/welcome-progress-0.svg b/learning/tour-of-beam/frontend/assets/svg/profile-about.svg
similarity index 58%
copy from learning/tour-of-beam/frontend/assets/svg/welcome-progress-0.svg
copy to learning/tour-of-beam/frontend/assets/svg/profile-about.svg
index d80426bf789..9096d879ce6 100644
--- a/learning/tour-of-beam/frontend/assets/svg/welcome-progress-0.svg
+++ b/learning/tour-of-beam/frontend/assets/svg/profile-about.svg
@@ -14,6 +14,8 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 See the License for the specific language governing permissions and
 limitations under the License. -->
 
-<svg width="36" height="36" viewBox="0 0 36 36" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M35.5 18C35.5 27.665 27.665 35.5 18 35.5C8.33502 35.5 0.5 27.665 0.5 18C0.5 8.33502 8.33502 0.5 18 0.5C27.665 0.5 35.5 8.33502 35.5 18ZM11 18C11 21.866 14.134 25 18 25C21.866 25 25 21.866 25 18C25 14.134 21.866 11 18 11C14.134 11 11 14.134 11 18Z" fill="#242639" fill-opacity="0.1"/>
-</svg>
+<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
+    <path d="M10 17.5C14.1421 17.5 17.5 14.1421 17.5 10C17.5 5.85786 14.1421 2.5 10 2.5C5.85786 2.5 2.5 5.85786 2.5 10C2.5 14.1421 5.85786 17.5 10 17.5Z" stroke="#A0A4AB" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" />
+    <path d="M10 6.6665H10.0083" stroke="#A0A4AB" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" />
+    <path d="M9.16675 10H10.0001V13.3333H10.8334" stroke="#A0A4AB" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" />
+</svg>
\ No newline at end of file
diff --git a/learning/tour-of-beam/frontend/assets/svg/profile-delete.svg b/learning/tour-of-beam/frontend/assets/svg/profile-delete.svg
new file mode 100644
index 00000000000..2d801aba6e2
--- /dev/null
+++ b/learning/tour-of-beam/frontend/assets/svg/profile-delete.svg
@@ -0,0 +1,19 @@
+<!-- Licensed to the Apache Software Foundation (ASF) under one
+or more contributor license agreements.  See the NOTICE file
+distributed with this work for additional information
+regarding copyright ownership.  The ASF licenses this file
+to you under the Apache License, Version 2.0 (the
+"License"); you may not use this file except in compliance
+with the License.  You may obtain a copy of the License at
+ 
+      http://www.apache.org/licenses/LICENSE-2.0
+ 
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License. -->
+
+<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
+    <path d="M14.5833 5.41683L15.2041 5.48847C15.2246 5.31141 15.1684 5.13405 15.0498 5.00099C14.9312 4.86793 14.7615 4.79183 14.5833 4.79183V5.41683ZM5.41659 5.41683V4.79183C5.23835 4.79183 5.06859 4.86793 4.95 5.00099C4.83141 5.13405 4.77527 5.31141 4.7957 5.48847L5.41659 5.41683ZM4.58325 4.79183C4.23807 4.79183 3.95825 5.07165 3.95825 5.41683C3.95825 5.76201 4.23807 6.04183 4.58325 6.04183V4.79183ZM15.4166 6.04183C15.7618 6.04183 16.0416 5.76201 16.0416 5.41683C16.0416 5.07165 15.7618 [...]
+</svg>
\ No newline at end of file
diff --git a/learning/tour-of-beam/frontend/assets/svg/profile-logout.svg b/learning/tour-of-beam/frontend/assets/svg/profile-logout.svg
new file mode 100644
index 00000000000..9f4b5de09be
--- /dev/null
+++ b/learning/tour-of-beam/frontend/assets/svg/profile-logout.svg
@@ -0,0 +1,19 @@
+<!-- Licensed to the Apache Software Foundation (ASF) under one
+or more contributor license agreements.  See the NOTICE file
+distributed with this work for additional information
+regarding copyright ownership.  The ASF licenses this file
+to you under the Apache License, Version 2.0 (the
+"License"); you may not use this file except in compliance
+with the License.  You may obtain a copy of the License at
+ 
+      http://www.apache.org/licenses/LICENSE-2.0
+ 
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License. -->
+
+<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
+    <path d="M8.65075 15.9969C8.65075 16.3875 8.3461 16.6665 7.98606 16.6665H5.992C4.49645 16.6665 3.33325 15.4946 3.33325 13.9879V6.84508C3.33325 5.36628 4.49645 4.1665 5.992 4.1665H7.98606C8.3461 4.1665 8.65075 4.47342 8.65075 4.83615C8.65075 5.22677 8.3461 5.50579 7.98606 5.50579H5.992C5.24423 5.50579 4.66263 6.11963 4.66263 6.84508V13.9879C4.66263 14.7413 5.24423 15.3272 5.992 15.3272H7.98606C8.3461 15.3272 8.65075 15.6341 8.65075 15.9969ZM17.3194 9.97008L13.8021 6.17543C13.5528 5.89 [...]
+</svg>
\ No newline at end of file
diff --git a/learning/tour-of-beam/frontend/assets/svg/welcome-progress-0.svg b/learning/tour-of-beam/frontend/assets/svg/unit-progress-0.svg
similarity index 61%
copy from learning/tour-of-beam/frontend/assets/svg/welcome-progress-0.svg
copy to learning/tour-of-beam/frontend/assets/svg/unit-progress-0.svg
index d80426bf789..37d5945870d 100644
--- a/learning/tour-of-beam/frontend/assets/svg/welcome-progress-0.svg
+++ b/learning/tour-of-beam/frontend/assets/svg/unit-progress-0.svg
@@ -14,6 +14,6 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 See the License for the specific language governing permissions and
 limitations under the License. -->
 
-<svg width="36" height="36" viewBox="0 0 36 36" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M35.5 18C35.5 27.665 27.665 35.5 18 35.5C8.33502 35.5 0.5 27.665 0.5 18C0.5 8.33502 8.33502 0.5 18 0.5C27.665 0.5 35.5 8.33502 35.5 18ZM11 18C11 21.866 14.134 25 18 25C21.866 25 25 21.866 25 18C25 14.134 21.866 11 18 11C14.134 11 11 14.134 11 18Z" fill="#242639" fill-opacity="0.1"/>
+<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M13.2223 7.00005C13.2223 10.4365 10.4365 13.2223 7.00005 13.2223C3.56362 13.2223 0.777832 10.4365 0.777832 7.00005C0.777832 3.56362 3.56362 0.777832 7.00005 0.777832C10.4365 0.777832 13.2223 3.56362 13.2223 7.00005ZM4.51117 7.00005C4.51117 8.37463 5.62548 9.48894 7.00005 9.48894C8.37463 9.48894 9.48894 8.37463 9.48894 7.00005C9.48894 5.62548 8.37463 4.51117 7.00005 4.51117C5.62548 4.51117 4.51117 5.62548 4.51117 7.00005Z" fill="#E6E7E9"/>
 </svg>
diff --git a/learning/tour-of-beam/frontend/assets/svg/welcome-progress-0.svg b/learning/tour-of-beam/frontend/assets/svg/unit-progress-100.svg
similarity index 60%
copy from learning/tour-of-beam/frontend/assets/svg/welcome-progress-0.svg
copy to learning/tour-of-beam/frontend/assets/svg/unit-progress-100.svg
index d80426bf789..8eb0cf172d2 100644
--- a/learning/tour-of-beam/frontend/assets/svg/welcome-progress-0.svg
+++ b/learning/tour-of-beam/frontend/assets/svg/unit-progress-100.svg
@@ -14,6 +14,6 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 See the License for the specific language governing permissions and
 limitations under the License. -->
 
-<svg width="36" height="36" viewBox="0 0 36 36" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M35.5 18C35.5 27.665 27.665 35.5 18 35.5C8.33502 35.5 0.5 27.665 0.5 18C0.5 8.33502 8.33502 0.5 18 0.5C27.665 0.5 35.5 8.33502 35.5 18ZM11 18C11 21.866 14.134 25 18 25C21.866 25 25 21.866 25 18C25 14.134 21.866 11 18 11C14.134 11 11 14.134 11 18Z" fill="#242639" fill-opacity="0.1"/>
+<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path fill-rule="evenodd" clip-rule="evenodd" d="M6 0C2.68629 0 0 2.68629 0 6C0 9.31371 2.68629 12 6 12C9.31371 12 12 9.31371 12 6C12 2.68629 9.31371 0 6 0ZM9.41421 5C9.80474 4.60948 9.80474 3.97631 9.41421 3.58579C9.02369 3.19526 8.39052 3.19526 8 3.58579L5 6.58579L4 5.58583C3.60947 5.19531 2.97631 5.19531 2.58579 5.58583C2.19526 5.97636 2.19526 6.60952 2.58579 7.00005L4.29289 8.70711C4.68342 9.09763 5.31658 9.09763 5.70711 8.70711L9.41421 5Z" fill="#37AC66"/>
 </svg>
diff --git a/learning/tour-of-beam/frontend/assets/svg/welcome-progress-0.svg b/learning/tour-of-beam/frontend/assets/svg/welcome-progress-0.svg
index d80426bf789..1b8d4d67d5e 100644
--- a/learning/tour-of-beam/frontend/assets/svg/welcome-progress-0.svg
+++ b/learning/tour-of-beam/frontend/assets/svg/welcome-progress-0.svg
@@ -15,5 +15,5 @@ See the License for the specific language governing permissions and
 limitations under the License. -->
 
 <svg width="36" height="36" viewBox="0 0 36 36" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M35.5 18C35.5 27.665 27.665 35.5 18 35.5C8.33502 35.5 0.5 27.665 0.5 18C0.5 8.33502 8.33502 0.5 18 0.5C27.665 0.5 35.5 8.33502 35.5 18ZM11 18C11 21.866 14.134 25 18 25C21.866 25 25 21.866 25 18C25 14.134 21.866 11 18 11C14.134 11 11 14.134 11 18Z" fill="#242639" fill-opacity="0.1"/>
+<path d="M35.5 18C35.5 27.665 27.665 35.5 18 35.5C8.33502 35.5 0.5 27.665 0.5 18C0.5 8.33502 8.33502 0.5 18 0.5C27.665 0.5 35.5 8.33502 35.5 18ZM11 18C11 21.866 14.134 25 18 25C21.866 25 25 21.866 25 18C25 14.134 21.866 11 18 11C14.134 11 11 14.134 11 18Z" fill="#E0E0E0"/>
 </svg>
diff --git a/learning/tour-of-beam/frontend/assets/translations/en.yaml b/learning/tour-of-beam/frontend/assets/translations/en.yaml
index b40cbfee09d..8dff25f4f91 100644
--- a/learning/tour-of-beam/frontend/assets/translations/en.yaml
+++ b/learning/tour-of-beam/frontend/assets/translations/en.yaml
@@ -16,24 +16,33 @@
 #  under the License.
 
 ui:
-  copyright: '© The Apache Software Foundation'
-  darkMode: 'Dark Mode'
-  lightMode: 'Light Mode'
-  privacyPolicy: 'Privacy Policy'
-  reportIssue: 'Report Issue in GitHub'
-  signIn: 'Sign in'
-  continueGitHub: 'Continue with GitHub'
-  continueGoogle: 'Continue with Google'
+  about: About Tour of Beam
+  builtWith: Built with Apache Beam
+  continueGitHub: Continue with GitHub
+  continueGoogle: Continue with Google
+  copyright: © The Apache Software Foundation
+  deleteAccount: Delete my account
+  privacyPolicy: Privacy Policy
+  reportIssue: Report Issue in GitHub
+  signIn: Sign in
+  signOut: Sign out
+  toWebsite: To Apache Beam website
+
 pages:
   welcome:
-    title: 'Welcome to the Tour of Beam!'
-    ifSaveProgress: 'Your journey is broken down into learning modules. If you would like to save your progress and track completed modules, please'
-    signIn: ' sign in.'
+    ifSaveProgress: Your journey is broken down into learning modules. If you would like to save your progress and track completed modules, please
     selectLanguage: 'Please select the default language (you may change the language at any time):'
-    startLearning: 'Start learning'
+    signIn: ' sign in.'
+    startTour: Start your tour
+    title: Welcome to the Tour of Beam!
+  tour:
+    completeUnit: Complete Unit
+    summaryTitle: Table of Contents
+
 dialogs:
   signInIf: If you would like to save your progress and track completed modules
+
 complexity:
-  basic: 'Basic level'
-  medium: 'Medium level'
-  advanced: 'Advanced level'
+  basic: Basic level
+  medium: Medium level
+  advanced: Advanced level
diff --git a/learning/tour-of-beam/frontend/integration_test/app_test.dart b/learning/tour-of-beam/frontend/integration_test/app_test.dart
index 232234bb056..e74b4d35624 100644
--- a/learning/tour-of-beam/frontend/integration_test/app_test.dart
+++ b/learning/tour-of-beam/frontend/integration_test/app_test.dart
@@ -19,7 +19,7 @@
 import 'package:easy_localization/easy_localization.dart';
 import 'package:flutter_test/flutter_test.dart';
 import 'package:integration_test/integration_test.dart';
-import 'package:tour_of_beam/components/toggle_theme_button.dart';
+import 'package:playground_components/playground_components.dart';
 import 'package:tour_of_beam/main.dart' as app;
 
 void main() {
diff --git a/playground/frontend/lib/components/loading_indicator/loading_indicator.dart b/learning/tour-of-beam/frontend/lib/components/expansion_tile_wrapper.dart
similarity index 56%
copy from playground/frontend/lib/components/loading_indicator/loading_indicator.dart
copy to learning/tour-of-beam/frontend/lib/components/expansion_tile_wrapper.dart
index 8d7fb142a28..b09e6177ef2 100644
--- a/playground/frontend/lib/components/loading_indicator/loading_indicator.dart
+++ b/learning/tour-of-beam/frontend/lib/components/expansion_tile_wrapper.dart
@@ -17,23 +17,26 @@
  */
 
 import 'package:flutter/material.dart';
-import 'package:playground/constants/colors.dart';
+import 'package:playground_components/playground_components.dart';
 
-class LoadingIndicator extends StatelessWidget {
-  final double size;
-
-  const LoadingIndicator({Key? key, required this.size}) : super(key: key);
+class ExpansionTileWrapper extends StatelessWidget {
+  final ExpansionTile expansionTile;
+  const ExpansionTileWrapper(this.expansionTile);
 
   @override
   Widget build(BuildContext context) {
-    return Center(
-      child: SizedBox(
-        height: size,
-        width: size,
-        child: const CircularProgressIndicator(
-          color: kLightPrimary,
-        ),
+    return Theme(
+      data: Theme.of(context).copyWith(
+        hoverColor: BeamColors.transparent,
+        splashColor: BeamColors.transparent,
+        highlightColor: BeamColors.transparent,
+        dividerColor: BeamColors.transparent,
+        unselectedWidgetColor: Colors.grey,
+        colorScheme: ColorScheme.fromSwatch(primarySwatch: Colors.grey),
+        visualDensity: const VisualDensity(vertical: -4),
+        listTileTheme: const ListTileThemeData(dense: true),
       ),
+      child: expansionTile,
     );
   }
 }
diff --git a/playground/frontend/lib/l10n/l10n.dart b/learning/tour-of-beam/frontend/lib/components/filler_text.dart
similarity index 79%
copy from playground/frontend/lib/l10n/l10n.dart
copy to learning/tour-of-beam/frontend/lib/components/filler_text.dart
index cd63ec77990..ca6099e6d9d 100644
--- a/playground/frontend/lib/l10n/l10n.dart
+++ b/learning/tour-of-beam/frontend/lib/components/filler_text.dart
@@ -18,8 +18,12 @@
 
 import 'package:flutter/material.dart';
 
-class L10n {
-  static const locales = [
-    Locale('en'),
-  ];
+class FillerText extends StatelessWidget {
+  final int width;
+  const FillerText({required this.width});
+
+  @override
+  Widget build(BuildContext context) {
+    return Text(''.padRight(width, 'Just a filler text. '));
+  }
 }
diff --git a/learning/tour-of-beam/frontend/lib/components/footer.dart b/learning/tour-of-beam/frontend/lib/components/footer.dart
index b42fbca162c..e801836bb89 100644
--- a/learning/tour-of-beam/frontend/lib/components/footer.dart
+++ b/learning/tour-of-beam/frontend/lib/components/footer.dart
@@ -18,10 +18,9 @@
 
 import 'package:easy_localization/easy_localization.dart';
 import 'package:flutter/material.dart';
+import 'package:playground_components/playground_components.dart';
 import 'package:url_launcher/url_launcher.dart';
 
-import '../config/theme/colors_provider.dart';
-import '../constants/links.dart';
 import '../constants/sizes.dart';
 
 class Footer extends StatelessWidget {
@@ -31,12 +30,25 @@ class Footer extends StatelessWidget {
   Widget build(BuildContext context) {
     return _Body(
       child: Wrap(
-        spacing: TobSizes.size16,
+        alignment: WrapAlignment.spaceBetween,
         crossAxisAlignment: WrapCrossAlignment.center,
         children: [
-          const _ReportIssueButton(),
-          const _PrivacyPolicyButton(),
-          const Text('ui.copyright').tr(),
+          Wrap(
+            spacing: BeamSizes.size16,
+            crossAxisAlignment: WrapCrossAlignment.center,
+            children: [
+              const _ReportIssueButton(),
+              const _PrivacyPolicyButton(),
+              const Text('ui.copyright').tr(),
+            ],
+          ),
+          // TODO(nausharipov): get version, https://github.com/apache/beam/issues/23038
+          Text(
+            '${'ui.builtWith'.tr()} (TODO: Version)',
+            style: const TextStyle(
+              color: BeamColors.grey3,
+            ),
+          ),
         ],
       ),
     );
@@ -49,19 +61,22 @@ class _Body extends StatelessWidget {
 
   @override
   Widget build(BuildContext context) {
+    final themeData = Theme.of(context);
+    final ext = themeData.extension<BeamThemeExtension>()!;
+
     return Container(
+      width: double.infinity,
+      height: TobSizes.footerHeight,
       padding: const EdgeInsets.symmetric(
-        vertical: TobSizes.size4,
-        horizontal: TobSizes.size16,
+        vertical: BeamSizes.size4,
+        horizontal: BeamSizes.size16,
       ),
       decoration: BoxDecoration(
-        color: ThemeColors.of(context).secondaryBackground,
+        color: ext.secondaryBackgroundColor,
         border: Border(
-          top: BorderSide(color: ThemeColors.of(context).divider),
+          top: BorderSide(color: themeData.dividerColor),
         ),
       ),
-      height: TobSizes.footerHeight,
-      width: double.infinity,
       child: child,
     );
   }
@@ -75,7 +90,7 @@ class _ReportIssueButton extends StatelessWidget {
     return TextButton(
       style: _linkButtonStyle,
       onPressed: () {
-        launchUrl(Uri.parse(TobLinks.reportIssue));
+        launchUrl(Uri.parse(BeamLinks.reportIssue));
       },
       child: const Text('ui.reportIssue').tr(),
     );
@@ -90,7 +105,7 @@ class _PrivacyPolicyButton extends StatelessWidget {
     return TextButton(
       style: _linkButtonStyle,
       onPressed: () {
-        launchUrl(Uri.parse(TobLinks.privacyPolicy));
+        launchUrl(Uri.parse(BeamLinks.privacyPolicy));
       },
       child: const Text('ui.privacyPolicy').tr(),
     );
diff --git a/learning/tour-of-beam/frontend/lib/components/sign_in/sign_in_button.dart b/learning/tour-of-beam/frontend/lib/components/login/login_button.dart
similarity index 72%
copy from learning/tour-of-beam/frontend/lib/components/sign_in/sign_in_button.dart
copy to learning/tour-of-beam/frontend/lib/components/login/login_button.dart
index b823ea71c1b..36d96faffe5 100644
--- a/learning/tour-of-beam/frontend/lib/components/sign_in/sign_in_button.dart
+++ b/learning/tour-of-beam/frontend/lib/components/login/login_button.dart
@@ -18,28 +18,24 @@
 
 import 'package:easy_localization/easy_localization.dart';
 import 'package:flutter/material.dart';
-import 'package:playground_components/dismissible_overlay.dart';
+import 'package:playground_components/playground_components.dart';
 
-import '../../constants/sizes.dart';
-import 'sign_in_overlay_content.dart';
+import 'login_content.dart';
 
-class SignInButton extends StatefulWidget {
-  const SignInButton();
+class LoginButton extends StatelessWidget {
+  const LoginButton();
 
-  @override
-  State<SignInButton> createState() => _SignInButtonState();
-}
-
-class _SignInButtonState extends State<SignInButton> {
   @override
   Widget build(BuildContext context) {
     return TextButton(
-      onPressed: _openOverlay,
+      onPressed: () {
+        _openOverlay(context);
+      },
       child: const Text('ui.signIn').tr(),
     );
   }
 
-  void _openOverlay() {
+  void _openOverlay(BuildContext context) {
     OverlayEntry? overlay;
     overlay = OverlayEntry(
       builder: (context) => DismissibleOverlay(
@@ -47,9 +43,9 @@ class _SignInButtonState extends State<SignInButton> {
           overlay?.remove();
         },
         child: const Positioned(
-          right: TobSizes.size10,
-          top: TobSizes.appBarHeight,
-          child: SignInOverlayContent(),
+          right: BeamSizes.size10,
+          top: BeamSizes.appBarHeight,
+          child: LoginContent(),
         ),
       ),
     );
diff --git a/learning/tour-of-beam/frontend/lib/components/login/login_content.dart b/learning/tour-of-beam/frontend/lib/components/login/login_content.dart
new file mode 100644
index 00000000000..d4d0d8873cc
--- /dev/null
+++ b/learning/tour-of-beam/frontend/lib/components/login/login_content.dart
@@ -0,0 +1,140 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import 'package:easy_localization/easy_localization.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter_svg/svg.dart';
+import 'package:playground_components/playground_components.dart';
+
+import '../../constants/sizes.dart';
+import '../../generated/assets.gen.dart';
+
+class LoginContent extends StatelessWidget {
+  const LoginContent();
+
+  @override
+  Widget build(BuildContext context) {
+    return _Body(
+      child: Column(
+        children: [
+          Text(
+            'ui.signIn',
+            style: Theme.of(context).textTheme.titleLarge,
+          ).tr(),
+          const SizedBox(height: BeamSizes.size10),
+          const Text(
+            'dialogs.signInIf',
+            textAlign: TextAlign.center,
+          ).tr(),
+          const _Divider(),
+          const _BrandedLoginButtons(),
+        ],
+      ),
+    );
+  }
+}
+
+class _Body extends StatelessWidget {
+  final Widget child;
+  const _Body({required this.child});
+
+  @override
+  Widget build(BuildContext context) {
+    return Material(
+      elevation: BeamSizes.size10,
+      borderRadius: BorderRadius.circular(10),
+      child: Container(
+        width: TobSizes.authOverlayWidth,
+        padding: const EdgeInsets.all(BeamSizes.size24),
+        child: child,
+      ),
+    );
+  }
+}
+
+class _Divider extends StatelessWidget {
+  const _Divider();
+
+  @override
+  Widget build(BuildContext context) {
+    return Container(
+      color: BeamColors.grey3,
+      margin: const EdgeInsets.symmetric(vertical: 20),
+      width: BeamSizes.size32,
+      height: BeamSizes.size1,
+    );
+  }
+}
+
+class _BrandedLoginButtons extends StatelessWidget {
+  const _BrandedLoginButtons();
+
+  @override
+  Widget build(BuildContext context) {
+    final isLightTheme = Theme.of(context).brightness == Brightness.light;
+    final textStyle =
+        MaterialStatePropertyAll(Theme.of(context).textTheme.bodyMedium);
+    const padding = MaterialStatePropertyAll(
+      EdgeInsets.symmetric(
+        vertical: BeamSizes.size20,
+        horizontal: BeamSizes.size24,
+      ),
+    );
+    const minimumSize = MaterialStatePropertyAll(Size(double.infinity, 0));
+
+    final darkButtonStyle = ButtonStyle(
+      backgroundColor: const MaterialStatePropertyAll(BeamColors.darkGrey),
+      minimumSize: minimumSize,
+      padding: padding,
+      textStyle: textStyle,
+    );
+    final githubLightButtonStyle = ButtonStyle(
+      backgroundColor: const MaterialStatePropertyAll(BeamColors.darkBlue),
+      minimumSize: minimumSize,
+      padding: padding,
+      textStyle: textStyle,
+    );
+    final googleLightButtonStyle = ButtonStyle(
+      backgroundColor: const MaterialStatePropertyAll(BeamColors.white),
+      elevation: const MaterialStatePropertyAll(BeamSizes.size4),
+      foregroundColor: const MaterialStatePropertyAll(BeamColors.black),
+      minimumSize: minimumSize,
+      overlayColor: MaterialStatePropertyAll(Theme.of(context).hoverColor),
+      padding: padding,
+      textStyle: textStyle,
+    );
+
+    return Column(
+      children: [
+        ElevatedButton.icon(
+          onPressed: () {},
+          style: isLightTheme ? githubLightButtonStyle : darkButtonStyle,
+          icon: SvgPicture.asset(Assets.svg.githubLogo),
+          label: const Text('ui.continueGitHub').tr(),
+        ),
+        const SizedBox(height: BeamSizes.size16),
+        ElevatedButton.icon(
+          onPressed: () {},
+          style: isLightTheme ? googleLightButtonStyle : darkButtonStyle,
+          icon: SvgPicture.asset(Assets.svg.googleLogo),
+          label: const Text('ui.continueGoogle').tr(),
+        ),
+      ],
+    );
+  }
+}
diff --git a/learning/tour-of-beam/frontend/lib/components/logo.dart b/learning/tour-of-beam/frontend/lib/components/logo.dart
index e48b3985bf6..913678c76bd 100644
--- a/learning/tour-of-beam/frontend/lib/components/logo.dart
+++ b/learning/tour-of-beam/frontend/lib/components/logo.dart
@@ -17,9 +17,7 @@
  */
 
 import 'package:flutter/material.dart';
-
-import '../constants/assets.dart';
-import '../constants/sizes.dart';
+import 'package:playground_components/playground_components.dart';
 
 class Logo extends StatelessWidget {
   const Logo();
@@ -28,12 +26,9 @@ class Logo extends StatelessWidget {
   Widget build(BuildContext context) {
     return Row(
       mainAxisSize: MainAxisSize.min,
-      children: [
-        Image.asset(
-          TobAssets.beamLogo,
-          height: TobIconSizes.large,
-        ),
-        const _Text(),
+      children: const [
+        BeamLogo(),
+        _Text(),
       ],
     );
   }
diff --git a/learning/tour-of-beam/frontend/lib/components/sign_in/sign_in_button.dart b/learning/tour-of-beam/frontend/lib/components/profile/avatar.dart
similarity index 65%
rename from learning/tour-of-beam/frontend/lib/components/sign_in/sign_in_button.dart
rename to learning/tour-of-beam/frontend/lib/components/profile/avatar.dart
index b823ea71c1b..959c83876e3 100644
--- a/learning/tour-of-beam/frontend/lib/components/sign_in/sign_in_button.dart
+++ b/learning/tour-of-beam/frontend/lib/components/profile/avatar.dart
@@ -16,30 +16,29 @@
  * limitations under the License.
  */
 
-import 'package:easy_localization/easy_localization.dart';
 import 'package:flutter/material.dart';
-import 'package:playground_components/dismissible_overlay.dart';
+import 'package:playground_components/playground_components.dart';
 
-import '../../constants/sizes.dart';
-import 'sign_in_overlay_content.dart';
+import '../../generated/assets.gen.dart';
+import 'profile_content.dart';
 
-class SignInButton extends StatefulWidget {
-  const SignInButton();
+class Avatar extends StatelessWidget {
+  const Avatar();
 
-  @override
-  State<SignInButton> createState() => _SignInButtonState();
-}
-
-class _SignInButtonState extends State<SignInButton> {
   @override
   Widget build(BuildContext context) {
-    return TextButton(
-      onPressed: _openOverlay,
-      child: const Text('ui.signIn').tr(),
+    return GestureDetector(
+      onTap: () {
+        _openOverlay(context);
+      },
+      child: CircleAvatar(
+        backgroundColor: BeamColors.white,
+        foregroundImage: AssetImage(Assets.png.laptopLight.path),
+      ),
     );
   }
 
-  void _openOverlay() {
+  void _openOverlay(BuildContext context) {
     OverlayEntry? overlay;
     overlay = OverlayEntry(
       builder: (context) => DismissibleOverlay(
@@ -47,9 +46,9 @@ class _SignInButtonState extends State<SignInButton> {
           overlay?.remove();
         },
         child: const Positioned(
-          right: TobSizes.size10,
-          top: TobSizes.appBarHeight,
-          child: SignInOverlayContent(),
+          right: BeamSizes.size10,
+          top: BeamSizes.appBarHeight,
+          child: ProfileContent(),
         ),
       ),
     );
diff --git a/learning/tour-of-beam/frontend/lib/components/profile/profile_content.dart b/learning/tour-of-beam/frontend/lib/components/profile/profile_content.dart
new file mode 100644
index 00000000000..65f039dad32
--- /dev/null
+++ b/learning/tour-of-beam/frontend/lib/components/profile/profile_content.dart
@@ -0,0 +1,160 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import 'package:easy_localization/easy_localization.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter_svg/svg.dart';
+import 'package:playground_components/playground_components.dart';
+
+import '../../constants/sizes.dart';
+import '../../generated/assets.gen.dart';
+
+class ProfileContent extends StatelessWidget {
+  const ProfileContent();
+
+  @override
+  Widget build(BuildContext context) {
+    return _Body(
+      child: Column(
+        crossAxisAlignment: CrossAxisAlignment.start,
+        children: const [
+          _Info(),
+          BeamDivider(),
+          _Buttons(),
+        ],
+      ),
+    );
+  }
+}
+
+class _Body extends StatelessWidget {
+  final Widget child;
+
+  const _Body({required this.child});
+
+  @override
+  Widget build(BuildContext context) {
+    return Material(
+      elevation: BeamSizes.size10,
+      borderRadius: BorderRadius.circular(10),
+      child: SizedBox(
+        width: TobSizes.authOverlayWidth,
+        child: child,
+      ),
+    );
+  }
+}
+
+class _Info extends StatelessWidget {
+  const _Info();
+
+  @override
+  Widget build(BuildContext context) {
+    return Padding(
+      padding: const EdgeInsets.all(BeamSizes.size16),
+      child: Column(
+        crossAxisAlignment: CrossAxisAlignment.start,
+        children: [
+          Text(
+            'Name Surname',
+            style: Theme.of(context).textTheme.titleLarge,
+          ),
+          Text(
+            'email@mail.com',
+            style: Theme.of(context).textTheme.bodySmall,
+          ),
+        ],
+      ),
+    );
+  }
+}
+
+class _Buttons extends StatelessWidget {
+  const _Buttons();
+
+  @override
+  Widget build(BuildContext context) {
+    return Column(
+      children: [
+        _IconLabel(
+          isSvg: false,
+          onTap: () {},
+          iconPath: Assets.png.profileWebsite.path,
+          label: 'ui.toWebsite'.tr(),
+        ),
+        const BeamDivider(),
+        _IconLabel(
+          onTap: () {},
+          iconPath: Assets.svg.profileAbout,
+          label: 'ui.about'.tr(),
+        ),
+        const BeamDivider(),
+        _IconLabel(
+          onTap: () {},
+          iconPath: Assets.svg.profileLogout,
+          label: 'ui.signOut'.tr(),
+        ),
+        const BeamDivider(),
+        _IconLabel(
+          onTap: () {},
+          iconPath: Assets.svg.profileDelete,
+          label: 'ui.deleteAccount'.tr(),
+        ),
+      ],
+    );
+  }
+}
+
+class _IconLabel extends StatelessWidget {
+  final String iconPath;
+  final String label;
+  final void Function()? onTap;
+
+  // TODO(nausharipov): Auto-determine.
+  final bool isSvg;
+
+  const _IconLabel({
+    required this.iconPath,
+    required this.label,
+    required this.onTap,
+    this.isSvg = true,
+  });
+
+  @override
+  Widget build(BuildContext context) {
+    return InkWell(
+      onTap: onTap,
+      child: Padding(
+        padding: const EdgeInsets.all(BeamSizes.size12),
+        child: Row(
+          children: [
+            if (isSvg)
+              SvgPicture.asset(iconPath)
+            else
+              Image.asset(
+                iconPath,
+                height: 20,
+              ),
+            const SizedBox(width: BeamSizes.size10),
+            Text(label),
+          ],
+        ),
+      ),
+    );
+  }
+}
diff --git a/learning/tour-of-beam/frontend/lib/components/page_container.dart b/learning/tour-of-beam/frontend/lib/components/scaffold.dart
similarity index 56%
copy from learning/tour-of-beam/frontend/lib/components/page_container.dart
copy to learning/tour-of-beam/frontend/lib/components/scaffold.dart
index 3500e173927..f8352140436 100644
--- a/learning/tour-of-beam/frontend/lib/components/page_container.dart
+++ b/learning/tour-of-beam/frontend/lib/components/scaffold.dart
@@ -17,30 +17,39 @@
  */
 
 import 'package:flutter/material.dart';
+import 'package:playground_components/playground_components.dart';
 
-import '../constants/sizes.dart';
 import 'footer.dart';
+import 'login/login_button.dart';
 import 'logo.dart';
-import 'sign_in/sign_in_button.dart';
-import 'toggle_theme_button.dart';
+import 'profile/avatar.dart';
+import 'sdk_dropdown.dart';
 
-class PageContainer extends StatelessWidget {
+class TobScaffold extends StatelessWidget {
   final Widget child;
 
-  const PageContainer({
+  const TobScaffold({
     super.key,
     required this.child,
   });
 
+  // TODO(nausharipov): get state
+  static const _isAuthorized = true;
+
   @override
   Widget build(BuildContext context) {
     return Scaffold(
       appBar: AppBar(
         title: const Logo(),
         actions: const [
-          ToggleThemeButton(),
-          SignInButton(),
-          SizedBox(width: TobSizes.size16),
+          _ActionVerticalPadding(child: SdkDropdown()),
+          SizedBox(width: BeamSizes.size12),
+          _ActionVerticalPadding(child: ToggleThemeButton()),
+          SizedBox(width: BeamSizes.size6),
+          _ActionVerticalPadding(
+            child: _isAuthorized ? Avatar() : LoginButton(),
+          ),
+          SizedBox(width: BeamSizes.size16),
         ],
       ),
       body: Column(
@@ -52,3 +61,17 @@ class PageContainer extends StatelessWidget {
     );
   }
 }
+
+class _ActionVerticalPadding extends StatelessWidget {
+  final Widget child;
+
+  const _ActionVerticalPadding({required this.child});
+
+  @override
+  Widget build(BuildContext context) {
+    return Padding(
+      padding: const EdgeInsets.symmetric(vertical: BeamSizes.size10),
+      child: child,
+    );
+  }
+}
diff --git a/learning/tour-of-beam/frontend/lib/components/sdk_dropdown.dart b/learning/tour-of-beam/frontend/lib/components/sdk_dropdown.dart
new file mode 100644
index 00000000000..47f1a728b8e
--- /dev/null
+++ b/learning/tour-of-beam/frontend/lib/components/sdk_dropdown.dart
@@ -0,0 +1,64 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import 'package:flutter/material.dart';
+import 'package:playground_components/playground_components.dart';
+
+class SdkDropdown extends StatelessWidget {
+  const SdkDropdown();
+
+  @override
+  Widget build(BuildContext context) {
+    return _DropdownWrapper(
+      child: DropdownButton(
+        value: 'Java',
+        onChanged: (sdk) {
+          // TODO(nausharipov): change SDK
+        },
+        items: const ['Java', 'Python', 'Go']
+            .map(
+              (sdk) => DropdownMenuItem(
+                value: sdk,
+                child: Text(sdk),
+              ),
+            )
+            .toList(growable: false),
+        isDense: true,
+        alignment: Alignment.center,
+        focusColor: BeamColors.transparent,
+        borderRadius: BorderRadius.circular(BeamSizes.size6),
+      ),
+    );
+  }
+}
+
+class _DropdownWrapper extends StatelessWidget {
+  final Widget child;
+  const _DropdownWrapper({required this.child});
+
+  @override
+  Widget build(BuildContext context) {
+    return DecoratedBox(
+      decoration: BoxDecoration(
+        color: Theme.of(context).hoverColor,
+        borderRadius: BorderRadius.circular(BeamSizes.size6),
+      ),
+      child: DropdownButtonHideUnderline(child: child),
+    );
+  }
+}
diff --git a/learning/tour-of-beam/frontend/lib/components/sign_in/sign_in_overlay_content.dart b/learning/tour-of-beam/frontend/lib/components/sign_in/sign_in_overlay_content.dart
deleted file mode 100644
index 80fb00805e2..00000000000
--- a/learning/tour-of-beam/frontend/lib/components/sign_in/sign_in_overlay_content.dart
+++ /dev/null
@@ -1,89 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License.  You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-import 'package:easy_localization/easy_localization.dart';
-import 'package:flutter/material.dart';
-
-import '../../../constants/colors.dart';
-import '../../../constants/sizes.dart';
-
-class SignInOverlayContent extends StatelessWidget {
-  const SignInOverlayContent();
-
-  @override
-  Widget build(BuildContext context) {
-    return _Body(
-      child: Column(
-        children: [
-          Text(
-            'ui.signIn',
-            style: Theme.of(context).textTheme.titleLarge,
-          ).tr(),
-          const SizedBox(height: TobSizes.size10),
-          const Text(
-            'dialogs.signInIf',
-            textAlign: TextAlign.center,
-          ).tr(),
-          const _Divider(),
-          // TODO(nausharipov): check branded buttons in firebase_auth
-          ElevatedButton(
-            onPressed: () {},
-            child: const Text('ui.continueGitHub').tr(),
-          ),
-          const SizedBox(height: TobSizes.size16),
-          ElevatedButton(
-            onPressed: () {},
-            child: const Text('ui.continueGoogle').tr(),
-          ),
-        ],
-      ),
-    );
-  }
-}
-
-class _Body extends StatelessWidget {
-  final Widget child;
-  const _Body({required this.child});
-
-  @override
-  Widget build(BuildContext context) {
-    return Material(
-      elevation: TobSizes.size10,
-      borderRadius: BorderRadius.circular(10),
-      child: Container(
-        width: TobSizes.authOverlayWidth,
-        padding: const EdgeInsets.all(TobSizes.size24),
-        child: child,
-      ),
-    );
-  }
-}
-
-class _Divider extends StatelessWidget {
-  const _Divider();
-
-  @override
-  Widget build(BuildContext context) {
-    return Container(
-      margin: const EdgeInsets.symmetric(vertical: 20),
-      width: TobSizes.size32,
-      height: TobSizes.size1,
-      color: TobColors.grey3,
-    );
-  }
-}
diff --git a/learning/tour-of-beam/frontend/lib/config/theme/colors_provider.dart b/learning/tour-of-beam/frontend/lib/config/theme/colors_provider.dart
deleted file mode 100644
index 3aab5a1703c..00000000000
--- a/learning/tour-of-beam/frontend/lib/config/theme/colors_provider.dart
+++ /dev/null
@@ -1,86 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License.  You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-import 'package:flutter/material.dart';
-import 'package:provider/provider.dart';
-
-import '../../constants/colors.dart';
-
-class ThemeColorsProvider extends StatelessWidget {
-  final ThemeColors data;
-  final Widget child;
-
-  const ThemeColorsProvider({
-    super.key,
-    required this.data,
-    required this.child,
-  });
-
-  @override
-  Widget build(BuildContext context) {
-    return Provider<ThemeColors>.value(
-      value: data,
-      child: child,
-    );
-  }
-}
-
-class ThemeColors {
-  final Color? _background;
-  final bool isDark;
-
-  ThemeColors({
-    required this.isDark,
-    Color? background,
-  }) : _background = background;
-
-  static ThemeColors of(BuildContext context, {bool listen = true}) {
-    return Provider.of<ThemeColors>(context, listen: listen);
-  }
-
-  const ThemeColors.fromBrightness({
-    required this.isDark,
-  }) : _background = null;
-
-  Color get divider =>
-      isDark ? TobDarkThemeColors.grey : TobLightThemeColors.grey;
-
-  Color get primary =>
-      isDark ? TobDarkThemeColors.primary : TobLightThemeColors.primary;
-
-  Color get primaryBackgroundTextColor => TobColors.white;
-
-  Color get lightGreyBackgroundTextColor => TobColors.black;
-
-  Color get secondaryBackground => isDark
-      ? TobDarkThemeColors.secondaryBackground
-      : TobLightThemeColors.secondaryBackground;
-
-  Color get background =>
-      _background ??
-      (isDark
-          ? TobDarkThemeColors.primaryBackground
-          : TobLightThemeColors.primaryBackground);
-
-  Color get textColor =>
-      isDark ? TobDarkThemeColors.text : TobLightThemeColors.text;
-
-  Color get progressBackgroundColor =>
-      // TODO(nausharipov): reuse these colors after discussion with Anna
-      isDark ? const Color(0xffFFFFFF) : const Color(0xff242639);
-}
diff --git a/learning/tour-of-beam/frontend/lib/config/theme/theme.dart b/learning/tour-of-beam/frontend/lib/config/theme/theme.dart
deleted file mode 100644
index d8cf2c1c208..00000000000
--- a/learning/tour-of-beam/frontend/lib/config/theme/theme.dart
+++ /dev/null
@@ -1,160 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License.  You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-import 'package:flutter/material.dart';
-import 'package:google_fonts/google_fonts.dart';
-
-import '../../constants/colors.dart';
-import '../../constants/sizes.dart';
-
-final kLightTheme = ThemeData(
-  brightness: Brightness.light,
-  primaryColor: TobLightThemeColors.primary,
-  canvasColor: TobLightThemeColors.primaryBackground,
-  scaffoldBackgroundColor: TobLightThemeColors.secondaryBackground,
-  backgroundColor: TobLightThemeColors.primaryBackground,
-  textTheme: _getTextTheme(TobLightThemeColors.text),
-  textButtonTheme: _getTextButtonTheme(TobLightThemeColors.text),
-  outlinedButtonTheme: _getOutlineButtonTheme(
-    TobLightThemeColors.text,
-    TobLightThemeColors.primary,
-  ),
-  elevatedButtonTheme: _getElevatedButtonTheme(TobLightThemeColors.primary),
-  appBarTheme: _getAppBarTheme(TobLightThemeColors.secondaryBackground),
-);
-
-final kDarkTheme = ThemeData(
-  brightness: Brightness.dark,
-  primaryColor: TobDarkThemeColors.primary,
-  canvasColor: TobDarkThemeColors.primaryBackground,
-  scaffoldBackgroundColor: TobDarkThemeColors.secondaryBackground,
-  backgroundColor: TobDarkThemeColors.primaryBackground,
-  textTheme: _getTextTheme(TobDarkThemeColors.text),
-  textButtonTheme: _getTextButtonTheme(TobDarkThemeColors.text),
-  outlinedButtonTheme: _getOutlineButtonTheme(
-    TobDarkThemeColors.text,
-    TobDarkThemeColors.primary,
-  ),
-  elevatedButtonTheme: _getElevatedButtonTheme(TobDarkThemeColors.primary),
-  appBarTheme: _getAppBarTheme(TobDarkThemeColors.secondaryBackground),
-);
-
-TextTheme _getTextTheme(Color textColor) {
-  return GoogleFonts.sourceSansProTextTheme(
-    const TextTheme(
-      displayLarge: _emptyTextStyle,
-      displayMedium: TextStyle(
-        fontSize: 48,
-        fontWeight: FontWeight.w900,
-      ),
-      displaySmall: TextStyle(
-        fontFamily: 'Roboto_regular',
-        fontSize: 18,
-        fontWeight: FontWeight.w400,
-      ),
-      headlineLarge: _emptyTextStyle,
-      headlineMedium: _emptyTextStyle,
-      headlineSmall: TextStyle(
-        fontSize: 12,
-        fontWeight: FontWeight.w600,
-      ),
-      titleLarge: TextStyle(
-        fontSize: 24,
-        fontWeight: FontWeight.w600,
-      ),
-      titleMedium: _emptyTextStyle,
-      titleSmall: _emptyTextStyle,
-      labelLarge: TextStyle(
-        fontSize: 16,
-        fontWeight: FontWeight.w600,
-      ),
-      labelMedium: _emptyTextStyle,
-      labelSmall: _emptyTextStyle,
-      bodyLarge: TextStyle(
-        fontSize: 24,
-        fontWeight: FontWeight.w400,
-      ),
-      bodyMedium: TextStyle(
-        fontSize: 13,
-        fontWeight: FontWeight.w400,
-      ),
-      bodySmall: _emptyTextStyle,
-    ).apply(
-      bodyColor: textColor,
-      displayColor: textColor,
-    ),
-  );
-}
-
-TextButtonThemeData _getTextButtonTheme(Color textColor) {
-  return TextButtonThemeData(
-    style: TextButton.styleFrom(
-      primary: textColor,
-      shape: _getButtonBorder(TobBorderRadius.large),
-    ),
-  );
-}
-
-OutlinedButtonThemeData _getOutlineButtonTheme(
-  Color textColor,
-  Color outlineColor,
-) {
-  return OutlinedButtonThemeData(
-    style: OutlinedButton.styleFrom(
-      primary: textColor,
-      side: BorderSide(color: outlineColor, width: 3),
-      padding: _buttonPadding,
-      shape: _getButtonBorder(TobBorderRadius.small),
-    ),
-  );
-}
-
-ElevatedButtonThemeData _getElevatedButtonTheme(Color color) {
-  return ElevatedButtonThemeData(
-    style: ElevatedButton.styleFrom(
-      onPrimary: TobColors.white,
-      primary: color,
-      padding: _buttonPadding,
-      elevation: TobSizes.size0,
-    ),
-  );
-}
-
-AppBarTheme _getAppBarTheme(Color backgroundColor) {
-  return AppBarTheme(
-    color: backgroundColor,
-    elevation: TobSizes.size1,
-    centerTitle: false,
-    toolbarHeight: TobSizes.appBarHeight,
-  );
-}
-
-const EdgeInsets _buttonPadding = EdgeInsets.symmetric(
-  vertical: TobSizes.size20,
-  horizontal: TobSizes.size40,
-);
-
-RoundedRectangleBorder _getButtonBorder(double radius) {
-  return RoundedRectangleBorder(
-    borderRadius: BorderRadius.all(
-      Radius.circular(radius),
-    ),
-  );
-}
-
-const TextStyle _emptyTextStyle = TextStyle();
diff --git a/learning/tour-of-beam/frontend/lib/constants/assets.dart b/learning/tour-of-beam/frontend/lib/constants/assets.dart
deleted file mode 100644
index 1af152cc402..00000000000
--- a/learning/tour-of-beam/frontend/lib/constants/assets.dart
+++ /dev/null
@@ -1,34 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License.  You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-String _getPngPath(String fileName) {
-  return 'png/$fileName.png';
-}
-
-String _getSvgPath(String fileName) {
-  return 'svg/$fileName.svg';
-}
-
-class TobAssets {
-  static final beamLogo = _getPngPath('beam-logo');
-  static final themeMode = _getSvgPath('theme-mode');
-  static final welcomeLaptop = _getPngPath('welcome-laptop');
-  static final laptopDark = _getPngPath('laptop-dark');
-  static final laptopLight = _getPngPath('laptop-light');
-  static final welcomeProgress0 = _getSvgPath('welcome-progress-0');
-}
diff --git a/learning/tour-of-beam/frontend/lib/constants/sizes.dart b/learning/tour-of-beam/frontend/lib/constants/sizes.dart
index 187a0f60f95..bb9a665c8a3 100644
--- a/learning/tour-of-beam/frontend/lib/constants/sizes.dart
+++ b/learning/tour-of-beam/frontend/lib/constants/sizes.dart
@@ -17,36 +17,8 @@
  */
 
 class TobSizes {
-  static const double size0 = 0;
-  static const double size1 = 1;
-  static const double size4 = 4;
-  static const double size6 = 6;
-  static const double size8 = 8;
-  static const double size10 = 10;
-  static const double size12 = 12;
-  static const double size16 = 16;
-  static const double size20 = 20;
-  static const double size24 = 24;
-  static const double size32 = 32;
-  static const double size36 = 36;
-  static const double size40 = 40;
-  static const double appBarHeight = 55;
-  static const double footerHeight = 30;
-  static const double authOverlayWidth = 300;
-}
-
-class TobBorderRadius {
-  static const double small = 4;
-  static const double medium = 6;
-  static const double large = 8;
-  static const double xl = 28;
-}
-
-class TobIconSizes {
-  static const double xs = 8;
-  static const double small = 16;
-  static const double medium = 24;
-  static const double large = 32;
+  static const double footerHeight = 35;
+  static const double authOverlayWidth = 260;
 }
 
 class ScreenSizes {
diff --git a/learning/tour-of-beam/frontend/lib/main.dart b/learning/tour-of-beam/frontend/lib/main.dart
index 4c81d88a4f5..c7eb698c378 100644
--- a/learning/tour-of-beam/frontend/lib/main.dart
+++ b/learning/tour-of-beam/frontend/lib/main.dart
@@ -17,15 +17,15 @@
  */
 
 import 'package:easy_localization/easy_localization.dart';
+import 'package:easy_localization_ext/easy_localization_ext.dart';
 import 'package:easy_localization_loader/easy_localization_loader.dart';
 import 'package:flutter/material.dart';
+import 'package:playground_components/playground_components.dart';
 import 'package:provider/provider.dart';
 import 'package:url_strategy/url_strategy.dart';
 
-import 'config/theme/switch_notifier.dart';
-import 'config/theme/theme.dart';
 import 'locator.dart';
-import 'pages/welcome/screen.dart';
+import 'pages/tour/screen.dart';
 
 void main() async {
   setPathUrlStrategy();
@@ -39,7 +39,10 @@ void main() async {
       startLocale: englishLocale,
       fallbackLocale: englishLocale,
       path: 'assets/translations',
-      assetLoader: YamlAssetLoader(),
+      assetLoader: MultiAssetLoader([
+        PlaygroundComponents.translationLoader,
+        YamlAssetLoader(),
+      ]),
       child: const TourOfBeamApp(),
     ),
   );
@@ -61,7 +64,7 @@ class TourOfBeamApp extends StatelessWidget {
             localizationsDelegates: context.localizationDelegates,
             supportedLocales: context.supportedLocales,
             locale: context.locale,
-            home: const WelcomeScreen(),
+            home: const TourScreen(),
           );
         },
       ),
diff --git a/learning/tour-of-beam/frontend/lib/pages/tour/playground_demo.dart b/learning/tour-of-beam/frontend/lib/pages/tour/playground_demo.dart
new file mode 100644
index 00000000000..384d46b0fc2
--- /dev/null
+++ b/learning/tour-of-beam/frontend/lib/pages/tour/playground_demo.dart
@@ -0,0 +1,126 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import 'package:flutter/material.dart';
+import 'package:flutter/widgets.dart';
+import 'package:playground_components/playground_components.dart';
+
+// This is for demo only. Need a thought-through import in production.
+
+const String kApiClientURL =
+    'https://backend-router-beta-dot-apache-beam-testing.appspot.com';
+const String kApiJavaClientURL =
+    'https://backend-java-beta-dot-apache-beam-testing.appspot.com';
+const String kApiGoClientURL =
+    'https://backend-go-beta-dot-apache-beam-testing.appspot.com';
+const String kApiPythonClientURL =
+    'https://backend-python-beta-dot-apache-beam-testing.appspot.com';
+const String kApiScioClientURL =
+    'https://backend-scio-beta-dot-apache-beam-testing.appspot.com';
+
+class PlaygroundDemoWidget extends StatefulWidget {
+  const PlaygroundDemoWidget({super.key});
+
+  @override
+  State<PlaygroundDemoWidget> createState() => _PlaygroundDemoWidgetState();
+}
+
+class _PlaygroundDemoWidgetState extends State<PlaygroundDemoWidget> {
+  late final PlaygroundController playgroundController;
+
+  @override
+  void initState() {
+    super.initState();
+
+    final exampleRepository = ExampleRepository(
+      client: GrpcExampleClient(url: kApiClientURL),
+    );
+
+    final codeRepository = CodeRepository(
+      client: GrpcCodeClient(
+        url: kApiClientURL,
+        runnerUrlsById: {
+          Sdk.java.id: kApiJavaClientURL,
+          Sdk.go.id: kApiGoClientURL,
+          Sdk.python.id: kApiPythonClientURL,
+          Sdk.scio.id: kApiScioClientURL,
+        },
+      ),
+    );
+
+    final exampleCache = ExampleCache(
+      exampleRepository: exampleRepository,
+      hasCatalog: true,
+    );
+
+    playgroundController = PlaygroundController(
+      codeRepository: codeRepository,
+      exampleCache: exampleCache,
+      examplesLoader: ExamplesLoader(),
+    );
+
+    playgroundController.examplesLoader.load(
+      const ExamplesLoadingDescriptor(
+        descriptors: [
+          CatalogDefaultExampleLoadingDescriptor(sdk: Sdk.java),
+        ],
+      ),
+    );
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    return AnimatedBuilder(
+      animation: playgroundController,
+      builder: _buildOnChange,
+    );
+  }
+
+  Widget _buildOnChange(BuildContext context, Widget? child) {
+    final snippetController = playgroundController.snippetEditingController;
+    if (snippetController == null) {
+      return const LoadingIndicator();
+    }
+
+    return Stack(
+      children: [
+        SplitView(
+          direction: Axis.vertical,
+          first: SnippetEditor(
+            controller: snippetController,
+            isEditable: true,
+            goToContextLine: false,
+          ),
+          second: OutputWidget(
+            playgroundController: playgroundController,
+            graphDirection: Axis.horizontal,
+          ),
+        ),
+        Positioned(
+          top: 30,
+          right: 30,
+          child: Row(
+            children: [
+              RunOrCancelButton(playgroundController: playgroundController),
+            ],
+          ),
+        ),
+      ],
+    );
+  }
+}
diff --git a/learning/tour-of-beam/frontend/lib/pages/tour/screen.dart b/learning/tour-of-beam/frontend/lib/pages/tour/screen.dart
new file mode 100644
index 00000000000..7d708ff2508
--- /dev/null
+++ b/learning/tour-of-beam/frontend/lib/pages/tour/screen.dart
@@ -0,0 +1,359 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import 'package:easy_localization/easy_localization.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter_svg/svg.dart';
+import 'package:playground_components/playground_components.dart';
+
+import '../../components/expansion_tile_wrapper.dart';
+import '../../components/filler_text.dart';
+import '../../components/scaffold.dart';
+import '../../constants/sizes.dart';
+import '../../generated/assets.gen.dart';
+import 'playground_demo.dart';
+
+class TourScreen extends StatelessWidget {
+  const TourScreen();
+
+  @override
+  Widget build(BuildContext context) {
+    return TobScaffold(
+      child: MediaQuery.of(context).size.width > ScreenBreakpoints.twoColumns
+          ? const _WideTour()
+          : const _NarrowTour(),
+    );
+  }
+}
+
+class _WideTour extends StatelessWidget {
+  const _WideTour();
+
+  @override
+  Widget build(BuildContext context) {
+    return Row(
+      crossAxisAlignment: CrossAxisAlignment.start,
+      children: const [
+        _ContentTree(),
+        Expanded(
+          child: SplitView(
+            direction: Axis.horizontal,
+            first: _Content(),
+            second: PlaygroundDemoWidget(),
+          ),
+        ),
+      ],
+    );
+  }
+}
+
+class _NarrowTour extends StatelessWidget {
+  const _NarrowTour();
+
+  @override
+  Widget build(BuildContext context) {
+    return SingleChildScrollView(
+      child: Column(
+        children: [
+          Row(
+            crossAxisAlignment: CrossAxisAlignment.start,
+            children: const [
+              _ContentTree(),
+              Expanded(child: _Content()),
+            ],
+          ),
+          DecoratedBox(
+            decoration: BoxDecoration(
+              border: Border(
+                top: BorderSide(color: Theme.of(context).dividerColor),
+              ),
+            ),
+            child: const _NarrowScreenPlayground(),
+          ),
+        ],
+      ),
+    );
+  }
+}
+
+class _ContentTree extends StatelessWidget {
+  const _ContentTree();
+
+  @override
+  Widget build(BuildContext context) {
+    return Container(
+      width: 250,
+      padding: const EdgeInsets.symmetric(horizontal: BeamSizes.size12),
+      child: SingleChildScrollView(
+        child: Column(
+          children: [
+            const _ContentTreeTitle(),
+            ...[
+              'Core Transforms',
+              'Common Transforms',
+            ].map((e) => _Module(module: e)).toList(growable: false),
+            const SizedBox(height: BeamSizes.size12),
+          ],
+        ),
+      ),
+    );
+  }
+}
+
+class _Module extends StatelessWidget {
+  final String module;
+  const _Module({required this.module});
+
+  @override
+  Widget build(BuildContext context) {
+    return Column(
+      children: [
+        _ModuleTitle(title: module),
+        ...['Map', 'Combine']
+            .map((group) => _Group(group: group))
+            .toList(growable: false),
+        const BeamDivider(
+          margin: EdgeInsets.symmetric(vertical: BeamSizes.size10),
+        ),
+      ],
+    );
+  }
+}
+
+class _ContentTreeTitle extends StatelessWidget {
+  const _ContentTreeTitle();
+
+  @override
+  Widget build(BuildContext context) {
+    return Padding(
+      padding: const EdgeInsets.symmetric(vertical: BeamSizes.size12),
+      child: Row(
+        mainAxisAlignment: MainAxisAlignment.spaceBetween,
+        children: [
+          Text(
+            'pages.tour.summaryTitle',
+            style: Theme.of(context).textTheme.headlineLarge,
+          ).tr(),
+        ],
+      ),
+    );
+  }
+}
+
+class _ModuleTitle extends StatelessWidget {
+  final String title;
+  const _ModuleTitle({required this.title});
+
+  @override
+  Widget build(BuildContext context) {
+    return Padding(
+      padding: const EdgeInsets.only(bottom: BeamSizes.size6),
+      child: Row(
+        mainAxisAlignment: MainAxisAlignment.spaceBetween,
+        children: [
+          Text(
+            title,
+            style: Theme.of(context).textTheme.headlineMedium,
+          ),
+          const Padding(
+            padding: EdgeInsets.only(right: BeamSizes.size4),
+            child: ComplexityWidget(complexity: Complexity.basic),
+          ),
+        ],
+      ),
+    );
+  }
+}
+
+class _Group extends StatelessWidget {
+  final String group;
+  const _Group({required this.group});
+
+  @override
+  Widget build(BuildContext context) {
+    return ExpansionTileWrapper(
+      ExpansionTile(
+        tilePadding: EdgeInsets.zero,
+        title: _GroupTitle(title: group),
+        childrenPadding: const EdgeInsets.only(
+          left: BeamSizes.size24,
+          top: BeamSizes.size10,
+        ),
+        children: const [_Units()],
+      ),
+    );
+  }
+}
+
+class _Units extends StatelessWidget {
+  const _Units();
+
+  @override
+  Widget build(BuildContext context) {
+    return Column(
+      children: ['ParDo one-to-one', 'ParDo one-to-many']
+          .map((e) => _Unit(title: e))
+          .toList(growable: false),
+    );
+  }
+}
+
+class _Unit extends StatelessWidget {
+  final String title;
+  const _Unit({required this.title});
+
+  @override
+  Widget build(BuildContext context) {
+    return Padding(
+      padding: const EdgeInsets.only(bottom: BeamSizes.size18),
+      child: Row(
+        children: [
+          _ProgressIndicator(
+            assetPath: Assets.svg.unitProgress100,
+          ),
+          Text(title),
+        ],
+      ),
+    );
+  }
+}
+
+class _GroupTitle extends StatelessWidget {
+  final String title;
+  const _GroupTitle({required this.title});
+
+  @override
+  Widget build(BuildContext context) {
+    return Row(
+      children: [
+        _ProgressIndicator(
+          assetPath: Assets.svg.unitProgress100,
+        ),
+        Text(
+          title,
+          style: Theme.of(context).textTheme.headlineMedium,
+        ),
+      ],
+    );
+  }
+}
+
+class _ProgressIndicator extends StatelessWidget {
+  final String assetPath;
+  const _ProgressIndicator({required this.assetPath});
+
+  @override
+  Widget build(BuildContext context) {
+    return Padding(
+      padding: const EdgeInsets.only(
+        left: BeamSizes.size4,
+        right: BeamSizes.size8,
+      ),
+      child: SvgPicture.asset(assetPath),
+    );
+  }
+}
+
+class _Content extends StatelessWidget {
+  const _Content();
+
+  @override
+  Widget build(BuildContext context) {
+    final themeData = Theme.of(context);
+
+    return Container(
+      height: MediaQuery.of(context).size.height -
+          BeamSizes.appBarHeight -
+          TobSizes.footerHeight,
+      decoration: BoxDecoration(
+        color: themeData.backgroundColor,
+        border: Border(
+          left: BorderSide(color: themeData.dividerColor),
+        ),
+      ),
+      child: Column(
+        mainAxisAlignment: MainAxisAlignment.spaceBetween,
+        children: [
+          Expanded(
+            child: SingleChildScrollView(
+              controller: ScrollController(),
+              child: const FillerText(width: 1000),
+            ),
+          ),
+          const _ContentFooter(),
+        ],
+      ),
+    );
+  }
+}
+
+class _ContentFooter extends StatelessWidget {
+  const _ContentFooter();
+
+  @override
+  Widget build(BuildContext context) {
+    final themeData = Theme.of(context);
+
+    return Container(
+      decoration: BoxDecoration(
+        border: Border(
+          top: BorderSide(color: themeData.dividerColor),
+        ),
+        color:
+            themeData.extension<BeamThemeExtension>()?.secondaryBackgroundColor,
+      ),
+      width: double.infinity,
+      padding: const EdgeInsets.all(BeamSizes.size20),
+      child: Row(
+        mainAxisAlignment: MainAxisAlignment.end,
+        children: [
+          Flexible(
+            child: OutlinedButton(
+              style: OutlinedButton.styleFrom(
+                foregroundColor: themeData.primaryColor,
+                side: BorderSide(color: themeData.primaryColor),
+                shape: const RoundedRectangleBorder(
+                  borderRadius: BorderRadius.all(
+                    Radius.circular(BeamSizes.size4),
+                  ),
+                ),
+              ),
+              child: const Text(
+                'pages.tour.completeUnit',
+                overflow: TextOverflow.ellipsis,
+              ).tr(),
+              onPressed: () {
+                // TODO(nausharipov): complete unit
+              },
+            ),
+          ),
+        ],
+      ),
+    );
+  }
+}
+
+class _NarrowScreenPlayground extends StatelessWidget {
+  const _NarrowScreenPlayground();
+
+  @override
+  Widget build(BuildContext context) {
+    // TODO(alexeyinkin): Even this way the narrow layout breaks, https://github.com/apache/beam/issues/23244
+    return const Center(child: Text('TODO: Playground for narrow screen'));
+  }
+}
diff --git a/learning/tour-of-beam/frontend/lib/pages/welcome/screen.dart b/learning/tour-of-beam/frontend/lib/pages/welcome/screen.dart
index 2898e158465..ae799b7e77c 100644
--- a/learning/tour-of-beam/frontend/lib/pages/welcome/screen.dart
+++ b/learning/tour-of-beam/frontend/lib/pages/welcome/screen.dart
@@ -20,20 +20,19 @@ import 'package:easy_localization/easy_localization.dart';
 import 'package:flutter/gestures.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter_svg/svg.dart';
+import 'package:playground_components/playground_components.dart';
 
-import '../../components/complexity.dart';
-import '../../components/page_container.dart';
-import '../../config/theme/colors_provider.dart';
-import '../../constants/assets.dart';
-import '../../constants/colors.dart';
+import '../../components/filler_text.dart';
+import '../../components/scaffold.dart';
 import '../../constants/sizes.dart';
+import '../../generated/assets.gen.dart';
 
 class WelcomeScreen extends StatelessWidget {
   const WelcomeScreen();
 
   @override
   Widget build(BuildContext context) {
-    return PageContainer(
+    return TobScaffold(
       child: SingleChildScrollView(
         child: MediaQuery.of(context).size.width > ScreenBreakpoints.twoColumns
             ? const _WideWelcome()
@@ -48,16 +47,18 @@ class _WideWelcome extends StatelessWidget {
 
   @override
   Widget build(BuildContext context) {
-    return Row(
-      crossAxisAlignment: CrossAxisAlignment.start,
-      children: const [
-        Expanded(
-          child: _SdkSelection(),
-        ),
-        Expanded(
-          child: _TourSummary(),
-        ),
-      ],
+    return IntrinsicHeight(
+      child: Row(
+        crossAxisAlignment: CrossAxisAlignment.start,
+        children: const [
+          Expanded(
+            child: _SdkSelection(),
+          ),
+          Expanded(
+            child: _TourSummary(),
+          ),
+        ],
+      ),
     );
   }
 }
@@ -79,34 +80,35 @@ class _NarrowWelcome extends StatelessWidget {
 class _SdkSelection extends StatelessWidget {
   const _SdkSelection();
 
+  static const double _minimalHeight = 900;
+
   @override
   Widget build(BuildContext context) {
     return Container(
       constraints: BoxConstraints(
         minHeight: MediaQuery.of(context).size.height -
-            TobSizes.appBarHeight -
+            BeamSizes.appBarHeight -
             TobSizes.footerHeight,
       ),
-      color: ThemeColors.of(context).background,
+      color: Theme.of(context).backgroundColor,
       child: Stack(
         children: [
           Positioned(
             bottom: 0,
             left: 0,
             right: 0,
-            // TODO(nausharipov): use flutter_gen after merging
             child: Theme.of(context).brightness == Brightness.dark
-                ? Image.asset(TobAssets.laptopDark)
-                : Image.asset(TobAssets.laptopLight),
+                ? Image.asset(Assets.png.laptopDark.path)
+                : Image.asset(Assets.png.laptopLight.path),
           ),
-          const SizedBox(height: 900),
+          const SizedBox(height: _minimalHeight),
           Padding(
             padding: const EdgeInsets.fromLTRB(50, 60, 50, 20),
             child: Column(
               crossAxisAlignment: CrossAxisAlignment.start,
               children: const [
                 _IntroText(),
-                SizedBox(height: TobSizes.size32),
+                SizedBox(height: BeamSizes.size32),
                 _Buttons(),
               ],
             ),
@@ -124,7 +126,7 @@ class _TourSummary extends StatelessWidget {
   Widget build(BuildContext context) {
     return Padding(
       padding: const EdgeInsets.symmetric(
-        vertical: TobSizes.size20,
+        vertical: BeamSizes.size20,
         horizontal: 27,
       ),
       child: Column(
@@ -135,7 +137,7 @@ class _TourSummary extends StatelessWidget {
                 isLast: module == _modules.last,
               ),
             )
-            .toList(),
+            .toList(growable: false),
       ),
     );
   }
@@ -152,6 +154,8 @@ class _TourSummary extends StatelessWidget {
 class _IntroText extends StatelessWidget {
   const _IntroText();
 
+  static const double _dividerMaxWidth = 150;
+
   @override
   Widget build(BuildContext context) {
     return Column(
@@ -163,9 +167,9 @@ class _IntroText extends StatelessWidget {
         ).tr(),
         Container(
           margin: const EdgeInsets.symmetric(vertical: 32),
-          height: 2,
-          color: TobColors.grey2,
-          constraints: const BoxConstraints(maxWidth: 150),
+          height: BeamSizes.size2,
+          color: BeamColors.grey2,
+          constraints: const BoxConstraints(maxWidth: _dividerMaxWidth),
         ),
         RichText(
           text: TextSpan(
@@ -179,7 +183,7 @@ class _IntroText extends StatelessWidget {
                 style: Theme.of(context)
                     .textTheme
                     .bodyLarge!
-                    .copyWith(color: ThemeColors.of(context).primary),
+                    .copyWith(color: Theme.of(context).primaryColor),
                 recognizer: TapGestureRecognizer()
                   ..onTap = () {
                     // TODO(nausharipov): sign in
@@ -198,7 +202,7 @@ class _Buttons extends StatelessWidget {
   const _Buttons();
 
   void _onSdkChanged(String value) {
-    // TODO(nausharipov): select the language
+    // TODO(nausharipov): change sdk
   }
 
   @override
@@ -214,13 +218,13 @@ class _Buttons extends StatelessWidget {
                   onChanged: _onSdkChanged,
                 ),
               )
-              .toList(),
+              .toList(growable: false),
         ),
         ElevatedButton(
           onPressed: () {
             // TODO(nausharipov): redirect
           },
-          child: const Text('pages.welcome.startLearning').tr(),
+          child: const Text('pages.welcome.startTour').tr(),
         ),
       ],
     );
@@ -246,10 +250,10 @@ class _SdkButton extends StatelessWidget {
       padding: const EdgeInsets.only(right: 15, bottom: 10),
       child: OutlinedButton(
         style: OutlinedButton.styleFrom(
-          backgroundColor: ThemeColors.of(context).background,
+          backgroundColor: Theme.of(context).backgroundColor,
           side: groupValue == value
               ? null
-              : const BorderSide(color: TobColors.grey1),
+              : const BorderSide(color: BeamColors.grey1),
         ),
         onPressed: () {
           onChanged(value);
@@ -293,13 +297,13 @@ class _ModuleHeader extends StatelessWidget {
           child: Row(
             children: [
               Padding(
-                padding: const EdgeInsets.all(TobSizes.size4),
+                padding: const EdgeInsets.all(BeamSizes.size4),
                 child: SvgPicture.asset(
-                  TobAssets.welcomeProgress0,
-                  color: ThemeColors.of(context).progressBackgroundColor,
+                  Assets.svg.welcomeProgress0,
+                  color: BeamColors.grey4,
                 ),
               ),
-              const SizedBox(width: TobSizes.size16),
+              const SizedBox(width: BeamSizes.size16),
               Expanded(
                 child: Text(
                   title,
@@ -315,7 +319,7 @@ class _ModuleHeader extends StatelessWidget {
               'complexity.medium',
               style: Theme.of(context).textTheme.headlineSmall,
             ).tr(),
-            const SizedBox(width: TobSizes.size6),
+            const SizedBox(width: BeamSizes.size6),
             const ComplexityWidget(complexity: Complexity.medium),
           ],
         ),
@@ -332,24 +336,24 @@ class _ModuleBody extends StatelessWidget {
 
   @override
   Widget build(BuildContext context) {
+    final themeData = Theme.of(context);
+
     return Container(
       margin: _moduleLeftMargin,
       decoration: BoxDecoration(
         border: Border(
           left: BorderSide(
-            color: ThemeColors.of(context).divider,
+            color: themeData.dividerColor,
           ),
         ),
       ),
       padding: _modulePadding,
       child: Column(
         children: [
-          const Text(
-            'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aliquam velit purus, tincidunt id velit vitae, mattis dictum velit. Nunc sit amet nunc at turpis eleifend commodo ac ut libero. Aenean rutrum rutrum nulla ut efficitur. Vestibulum pulvinar eros dictum lectus volutpat dignissim vitae quis nisi. Maecenas sem erat, elementum in euismod ut, interdum ac massa.',
-          ),
-          const SizedBox(height: TobSizes.size16),
+          const FillerText(width: 20),
+          const SizedBox(height: BeamSizes.size16),
           Divider(
-            color: ThemeColors.of(context).divider,
+            color: themeData.dividerColor,
           ),
         ],
       ),
@@ -365,9 +369,7 @@ class _LastModuleBody extends StatelessWidget {
     return Container(
       margin: _moduleLeftMargin,
       padding: _modulePadding,
-      child: const Text(
-        'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aliquam velit purus, tincidunt id velit vitae, mattis dictum velit. Nunc sit amet nunc at turpis eleifend commodo ac ut libero. Aenean rutrum rutrum nulla ut efficitur. Vestibulum pulvinar eros dictum lectus volutpat dignissim vitae quis nisi. Maecenas sem erat, elementum in euismod ut, interdum ac massa.',
-      ),
+      child: const FillerText(width: 20),
     );
   }
 }
diff --git a/learning/tour-of-beam/frontend/pubspec.lock b/learning/tour-of-beam/frontend/pubspec.lock
index b0a898316f4..c59bb0affd6 100644
--- a/learning/tour-of-beam/frontend/pubspec.lock
+++ b/learning/tour-of-beam/frontend/pubspec.lock
@@ -1,13 +1,34 @@
 # Generated by pub
 # See https://dart.dev/tools/pub/glossary#lockfile
 packages:
+  _fe_analyzer_shared:
+    dependency: transitive
+    description:
+      name: _fe_analyzer_shared
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "46.0.0"
+  aligned_dialog:
+    dependency: transitive
+    description:
+      name: aligned_dialog
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "0.0.6"
+  analyzer:
+    dependency: transitive
+    description:
+      name: analyzer
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "4.6.0"
   archive:
     dependency: transitive
     description:
       name: archive
       url: "https://pub.dartlang.org"
     source: hosted
-    version: "3.1.11"
+    version: "3.3.0"
   args:
     dependency: transitive
     description:
@@ -21,7 +42,7 @@ packages:
       name: async
       url: "https://pub.dartlang.org"
     source: hosted
-    version: "2.8.2"
+    version: "2.9.0"
   boolean_selector:
     dependency: transitive
     description:
@@ -29,27 +50,99 @@ packages:
       url: "https://pub.dartlang.org"
     source: hosted
     version: "2.1.0"
+  build:
+    dependency: transitive
+    description:
+      name: build
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "2.3.0"
+  build_config:
+    dependency: transitive
+    description:
+      name: build_config
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "1.1.0"
+  build_daemon:
+    dependency: transitive
+    description:
+      name: build_daemon
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "3.1.0"
+  build_resolvers:
+    dependency: transitive
+    description:
+      name: build_resolvers
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "2.0.9"
+  build_runner:
+    dependency: "direct dev"
+    description:
+      name: build_runner
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "2.2.0"
+  build_runner_core:
+    dependency: transitive
+    description:
+      name: build_runner_core
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "7.2.3"
+  built_collection:
+    dependency: transitive
+    description:
+      name: built_collection
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "5.1.1"
+  built_value:
+    dependency: transitive
+    description:
+      name: built_value
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "8.4.1"
   characters:
     dependency: transitive
     description:
       name: characters
       url: "https://pub.dartlang.org"
     source: hosted
-    version: "1.2.0"
-  charcode:
+    version: "1.2.1"
+  checked_yaml:
     dependency: transitive
     description:
-      name: charcode
+      name: checked_yaml
       url: "https://pub.dartlang.org"
     source: hosted
-    version: "1.3.1"
+    version: "2.0.1"
   clock:
     dependency: transitive
     description:
       name: clock
       url: "https://pub.dartlang.org"
     source: hosted
-    version: "1.1.0"
+    version: "1.1.1"
+  code_builder:
+    dependency: transitive
+    description:
+      name: code_builder
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "4.2.0"
+  code_text_field:
+    dependency: "direct main"
+    description:
+      path: "."
+      ref: "9e2c9fe52a69481f038f4b6609e8a0a776429437"
+      resolved-ref: "9e2c9fe52a69481f038f4b6609e8a0a776429437"
+      url: "https://github.com/BertrandBev/code_field.git"
+    source: git
+    version: "1.0.3"
   collection:
     dependency: transitive
     description:
@@ -57,13 +150,27 @@ packages:
       url: "https://pub.dartlang.org"
     source: hosted
     version: "1.16.0"
+  color:
+    dependency: transitive
+    description:
+      name: color
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "3.0.0"
+  convert:
+    dependency: transitive
+    description:
+      name: convert
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "3.0.2"
   crypto:
     dependency: transitive
     description:
       name: crypto
       url: "https://pub.dartlang.org"
     source: hosted
-    version: "3.0.1"
+    version: "3.0.2"
   csv:
     dependency: transitive
     description:
@@ -71,6 +178,20 @@ packages:
       url: "https://pub.dartlang.org"
     source: hosted
     version: "5.0.1"
+  dart_style:
+    dependency: transitive
+    description:
+      name: dart_style
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "2.2.3"
+  dartx:
+    dependency: transitive
+    description:
+      name: dartx
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "1.1.0"
   easy_localization:
     dependency: "direct main"
     description:
@@ -78,6 +199,13 @@ packages:
       url: "https://pub.dartlang.org"
     source: hosted
     version: "3.0.1"
+  easy_localization_ext:
+    dependency: "direct main"
+    description:
+      name: easy_localization_ext
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "0.1.1"
   easy_localization_loader:
     dependency: "direct main"
     description:
@@ -92,13 +220,20 @@ packages:
       url: "https://pub.dartlang.org"
     source: hosted
     version: "0.0.2"
+  equatable:
+    dependency: transitive
+    description:
+      name: equatable
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "2.0.5"
   fake_async:
     dependency: transitive
     description:
       name: fake_async
       url: "https://pub.dartlang.org"
     source: hosted
-    version: "1.3.0"
+    version: "1.3.1"
   ffi:
     dependency: transitive
     description:
@@ -113,6 +248,13 @@ packages:
       url: "https://pub.dartlang.org"
     source: hosted
     version: "6.1.2"
+  fixnum:
+    dependency: transitive
+    description:
+      name: fixnum
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "1.0.1"
   flutter:
     dependency: "direct main"
     description: flutter
@@ -123,6 +265,27 @@ packages:
     description: flutter
     source: sdk
     version: "0.0.0"
+  flutter_gen_core:
+    dependency: transitive
+    description:
+      name: flutter_gen_core
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "4.3.0"
+  flutter_gen_runner:
+    dependency: "direct dev"
+    description:
+      name: flutter_gen_runner
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "4.3.0"
+  flutter_highlight:
+    dependency: transitive
+    description:
+      name: flutter_highlight
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "0.7.0"
   flutter_localizations:
     dependency: transitive
     description: flutter
@@ -145,6 +308,13 @@ packages:
     description: flutter
     source: sdk
     version: "0.0.0"
+  frontend_server_client:
+    dependency: transitive
+    description:
+      name: frontend_server_client
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "2.1.3"
   fuchsia_remote_debug_protocol:
     dependency: transitive
     description: flutter
@@ -157,6 +327,13 @@ packages:
       url: "https://pub.dartlang.org"
     source: hosted
     version: "7.2.0"
+  glob:
+    dependency: transitive
+    description:
+      name: glob
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "2.1.0"
   google_fonts:
     dependency: "direct main"
     description:
@@ -164,6 +341,34 @@ packages:
       url: "https://pub.dartlang.org"
     source: hosted
     version: "3.0.1"
+  googleapis_auth:
+    dependency: transitive
+    description:
+      name: googleapis_auth
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "1.3.1"
+  graphs:
+    dependency: transitive
+    description:
+      name: graphs
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "2.1.0"
+  grpc:
+    dependency: transitive
+    description:
+      name: grpc
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "3.0.2"
+  highlight:
+    dependency: transitive
+    description:
+      name: highlight
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "0.7.0"
   http:
     dependency: transitive
     description:
@@ -171,6 +376,20 @@ packages:
       url: "https://pub.dartlang.org"
     source: hosted
     version: "0.13.4"
+  http2:
+    dependency: transitive
+    description:
+      name: http2
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "2.0.0"
+  http_multi_server:
+    dependency: transitive
+    description:
+      name: http_multi_server
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "3.2.1"
   http_parser:
     dependency: transitive
     description:
@@ -190,6 +409,13 @@ packages:
       url: "https://pub.dartlang.org"
     source: hosted
     version: "0.17.0"
+  io:
+    dependency: transitive
+    description:
+      name: io
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "1.0.3"
   js:
     dependency: transitive
     description:
@@ -197,27 +423,55 @@ packages:
       url: "https://pub.dartlang.org"
     source: hosted
     version: "0.6.4"
+  json_annotation:
+    dependency: transitive
+    description:
+      name: json_annotation
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "4.6.0"
+  linked_scroll_controller:
+    dependency: transitive
+    description:
+      name: linked_scroll_controller
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "0.2.0"
+  logging:
+    dependency: transitive
+    description:
+      name: logging
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "1.0.2"
   matcher:
     dependency: transitive
     description:
       name: matcher
       url: "https://pub.dartlang.org"
     source: hosted
-    version: "0.12.11"
+    version: "0.12.12"
   material_color_utilities:
     dependency: transitive
     description:
       name: material_color_utilities
       url: "https://pub.dartlang.org"
     source: hosted
-    version: "0.1.4"
+    version: "0.1.5"
   meta:
     dependency: transitive
     description:
       name: meta
       url: "https://pub.dartlang.org"
     source: hosted
-    version: "1.7.0"
+    version: "1.8.0"
+  mime:
+    dependency: transitive
+    description:
+      name: mime
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "1.0.2"
   nested:
     dependency: transitive
     description:
@@ -225,13 +479,20 @@ packages:
       url: "https://pub.dartlang.org"
     source: hosted
     version: "1.0.0"
+  package_config:
+    dependency: transitive
+    description:
+      name: package_config
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "2.1.0"
   path:
     dependency: transitive
     description:
       name: path
       url: "https://pub.dartlang.org"
     source: hosted
-    version: "1.8.1"
+    version: "1.8.2"
   path_drawing:
     dependency: transitive
     description:
@@ -323,6 +584,13 @@ packages:
       url: "https://pub.dartlang.org"
     source: hosted
     version: "2.1.2"
+  pool:
+    dependency: transitive
+    description:
+      name: pool
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "1.5.1"
   process:
     dependency: transitive
     description:
@@ -330,6 +598,13 @@ packages:
       url: "https://pub.dartlang.org"
     source: hosted
     version: "4.2.4"
+  protobuf:
+    dependency: transitive
+    description:
+      name: protobuf
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "2.1.0"
   provider:
     dependency: "direct main"
     description:
@@ -337,6 +612,20 @@ packages:
       url: "https://pub.dartlang.org"
     source: hosted
     version: "6.0.3"
+  pub_semver:
+    dependency: transitive
+    description:
+      name: pub_semver
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "2.1.1"
+  pubspec_parse:
+    dependency: transitive
+    description:
+      name: pubspec_parse
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "1.2.1"
   shared_preferences:
     dependency: "direct main"
     description:
@@ -393,6 +682,20 @@ packages:
       url: "https://pub.dartlang.org"
     source: hosted
     version: "2.1.1"
+  shelf:
+    dependency: transitive
+    description:
+      name: shelf
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "1.3.2"
+  shelf_web_socket:
+    dependency: transitive
+    description:
+      name: shelf_web_socket
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "1.0.2"
   sky_engine:
     dependency: transitive
     description: flutter
@@ -404,7 +707,7 @@ packages:
       name: source_span
       url: "https://pub.dartlang.org"
     source: hosted
-    version: "1.8.2"
+    version: "1.9.0"
   stack_trace:
     dependency: transitive
     description:
@@ -419,34 +722,55 @@ packages:
       url: "https://pub.dartlang.org"
     source: hosted
     version: "2.1.0"
+  stream_transform:
+    dependency: transitive
+    description:
+      name: stream_transform
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "2.0.0"
   string_scanner:
     dependency: transitive
     description:
       name: string_scanner
       url: "https://pub.dartlang.org"
     source: hosted
-    version: "1.1.0"
+    version: "1.1.1"
   sync_http:
     dependency: transitive
     description:
       name: sync_http
       url: "https://pub.dartlang.org"
     source: hosted
-    version: "0.3.0"
+    version: "0.3.1"
   term_glyph:
     dependency: transitive
     description:
       name: term_glyph
       url: "https://pub.dartlang.org"
     source: hosted
-    version: "1.2.0"
+    version: "1.2.1"
   test_api:
     dependency: transitive
     description:
       name: test_api
       url: "https://pub.dartlang.org"
     source: hosted
-    version: "0.4.9"
+    version: "0.4.12"
+  time:
+    dependency: transitive
+    description:
+      name: time
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "2.1.2"
+  timing:
+    dependency: transitive
+    description:
+      name: timing
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "1.0.0"
   total_lints:
     dependency: "direct dev"
     description:
@@ -460,7 +784,7 @@ packages:
       name: typed_data
       url: "https://pub.dartlang.org"
     source: hosted
-    version: "1.3.0"
+    version: "1.3.1"
   url_launcher:
     dependency: "direct main"
     description:
@@ -537,7 +861,21 @@ packages:
       name: vm_service
       url: "https://pub.dartlang.org"
     source: hosted
-    version: "8.2.2"
+    version: "9.0.0"
+  watcher:
+    dependency: transitive
+    description:
+      name: watcher
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "1.0.1"
+  web_socket_channel:
+    dependency: transitive
+    description:
+      name: web_socket_channel
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "2.2.0"
   webdriver:
     dependency: transitive
     description:
@@ -574,5 +912,5 @@ packages:
     source: hosted
     version: "3.1.1"
 sdks:
-  dart: ">=2.17.6 <3.0.0"
-  flutter: ">=3.0.0"
+  dart: ">=2.18.1 <3.0.0"
+  flutter: ">=3.3.2"
diff --git a/learning/tour-of-beam/frontend/pubspec.yaml b/learning/tour-of-beam/frontend/pubspec.yaml
index 220bfd5e5bc..fb9ee46307d 100644
--- a/learning/tour-of-beam/frontend/pubspec.yaml
+++ b/learning/tour-of-beam/frontend/pubspec.yaml
@@ -18,29 +18,35 @@
 name: tour_of_beam
 description: Tour of Beam
 
-publish_to: 'none' # Remove this line if you wish to publish to pub.dev
+publish_to: 'none'
 
 version: 0.1.0
 
 environment:
-  sdk: ">=2.17.6 <3.0.0"
-  flutter: ">=3.0.0 <4.0.0"
+  sdk: ">=2.18.1 <3.0.0"
+  flutter: ">=3.3.2"
 
 dependencies:
+  code_text_field:
+    git:
+      url: https://github.com/BertrandBev/code_field.git
+      ref: 9e2c9fe52a69481f038f4b6609e8a0a776429437
   easy_localization: ^3.0.1
+  easy_localization_ext: ^0.1.0
   easy_localization_loader: ^1.0.0
   flutter: { sdk: flutter }
   flutter_svg: ^1.0.3
   get_it: ^7.2.0
   google_fonts: ^3.0.1
-  playground_components:
-    path: ../../../playground/frontend/playground_components
+  playground_components: { path: ../../../playground/frontend/playground_components }
   provider: ^6.0.3
   shared_preferences: ^2.0.15
   url_launcher: ^6.1.5
   url_strategy: ^0.2.0
 
 dev_dependencies:
+  build_runner: ^2.2.0
+  flutter_gen_runner: ^4.3.0
   flutter_test: { sdk: flutter }
   integration_test: { sdk: flutter }
   total_lints: ^2.17.0
@@ -51,3 +57,6 @@ flutter:
     - assets/translations/en.yaml
     - assets/png/
     - assets/svg/
+
+flutter_gen:
+  output: lib/generated/
diff --git a/playground/frontend/lib/modules/shortcuts/components/shortcut_tooltip.dart b/learning/tour-of-beam/frontend/test/common/test_screen_wrapper.dart
similarity index 66%
copy from playground/frontend/lib/modules/shortcuts/components/shortcut_tooltip.dart
copy to learning/tour-of-beam/frontend/test/common/test_screen_wrapper.dart
index 94f14677c14..8146e336c3d 100644
--- a/playground/frontend/lib/modules/shortcuts/components/shortcut_tooltip.dart
+++ b/learning/tour-of-beam/frontend/test/common/test_screen_wrapper.dart
@@ -17,25 +17,24 @@
  */
 
 import 'package:flutter/material.dart';
-import 'package:playground/modules/shortcuts/models/shortcut.dart';
-import 'package:playground/modules/shortcuts/utils/shortcuts_display_name.dart';
+import 'package:playground_components/playground_components.dart';
+import 'package:provider/provider.dart';
 
-class ShortcutTooltip extends StatelessWidget {
-  final Shortcut shortcut;
+class TestScreenWrapper extends StatelessWidget {
   final Widget child;
-
-  const ShortcutTooltip({
-    Key? key,
-    required this.shortcut,
-    required this.child,
-  }) : super(key: key);
+  const TestScreenWrapper({required this.child});
 
   @override
   Widget build(BuildContext context) {
-    return Tooltip(
-      excludeFromSemantics: true,
-      message: getShortcutDisplayName(shortcut),
-      child: child,
+    return ThemeSwitchNotifierProvider(
+      child: Consumer<ThemeSwitchNotifier>(
+        builder: (context, themeSwitchNotifier, _) {
+          return MaterialApp(
+            theme: kLightTheme,
+            home: child,
+          );
+        },
+      ),
     );
   }
 }
diff --git a/learning/tour-of-beam/frontend/test/config/theme/switch_notifier_test.dart b/learning/tour-of-beam/frontend/test/overflow_test.dart
similarity index 68%
copy from learning/tour-of-beam/frontend/test/config/theme/switch_notifier_test.dart
copy to learning/tour-of-beam/frontend/test/overflow_test.dart
index 2565c073d2a..0596b63a61c 100644
--- a/learning/tour-of-beam/frontend/test/config/theme/switch_notifier_test.dart
+++ b/learning/tour-of-beam/frontend/test/overflow_test.dart
@@ -16,13 +16,19 @@
  * limitations under the License.
  */
 
+import 'package:flutter/material.dart';
 import 'package:flutter_test/flutter_test.dart';
-import 'package:tour_of_beam/config/theme/switch_notifier.dart';
+import 'package:tour_of_beam/pages/tour/screen.dart';
+import 'common/test_screen_wrapper.dart';
 
 void main() {
-  group('theme mode', () {
-    test('light mode is default', () {
-      expect(ThemeSwitchNotifier().isDarkMode, false);
-    });
+  testWidgets('WelcomeScreen overflow', (tester) async {
+    tester.binding.window.physicalSizeTestValue = const Size(500, 296);
+    // TODO(nausharipov): fix the failure
+    await tester.pumpWidget(
+      const TestScreenWrapper(
+        child: TourScreen(),
+      ),
+    );
   });
 }
diff --git a/playground/buf.gen.yaml b/playground/buf.gen.yaml
index bd9b9f3d830..d04b54a6c5d 100644
--- a/playground/buf.gen.yaml
+++ b/playground/buf.gen.yaml
@@ -28,5 +28,5 @@ plugins:
   - paths=source_relative
   - require_unimplemented_servers=false
 - name: dart
-  out: frontend/lib
-  opt: grpc
\ No newline at end of file
+  out: frontend/playground_components/lib/src
+  opt: grpc
diff --git a/playground/frontend/Dockerfile b/playground/frontend/Dockerfile
index 42651869307..2b342adfca1 100644
--- a/playground/frontend/Dockerfile
+++ b/playground/frontend/Dockerfile
@@ -17,11 +17,11 @@
 ###############################################################################
 
 FROM debian:11 as build
-ARG FLUTTER_VERSION=3.0.1-stable
+ARG FLUTTER_VERSION=3.3.2
 
 RUN apt-get update && apt-get install -y wget xz-utils git
-RUN wget https://storage.googleapis.com/flutter_infra_release/releases/stable/linux/flutter_linux_$FLUTTER_VERSION.tar.xz &&\
-    tar -xf flutter_linux_$FLUTTER_VERSION.tar.xz &&\
+RUN wget https://storage.googleapis.com/flutter_infra_release/releases/stable/linux/flutter_linux_$FLUTTER_VERSION-stable.tar.xz &&\
+    tar -xf flutter_linux_$FLUTTER_VERSION-stable.tar.xz &&\
     mv flutter /opt/ &&\
     ln -s /opt/flutter/bin/flutter /usr/bin/flutter &&\
     ln -s /opt/flutter/bin/dart /usr/bin/dart &&\
@@ -31,9 +31,18 @@ RUN wget https://storage.googleapis.com/flutter_infra_release/releases/stable/li
 
 COPY . /app
 WORKDIR /app
-RUN cd /app && flutter build web -v
+
+RUN cd /app/playground_components &&\
+    flutter pub get -v &&\
+    flutter pub run build_runner build -v
+
+RUN cd /app &&\
+    flutter pub get -v &&\
+    flutter pub run build_runner build -v &&\
+    flutter build web -v
+
 FROM nginx:1.21.3
 COPY --from=build /app/nginx_default.conf /etc/nginx/conf.d/default.conf
 COPY --from=build /app/build/web/ /usr/share/nginx/html
-RUN cp /usr/share/nginx/html/assets/assets/* /usr/share/nginx/html/assets/
+RUN cp -r /usr/share/nginx/html/assets/assets/* /usr/share/nginx/html/assets/
 RUN gzip -kr /usr/share/nginx/html/assets/*
diff --git a/playground/frontend/README.md b/playground/frontend/README.md
index 92eb33628ab..eb015b48b9e 100644
--- a/playground/frontend/README.md
+++ b/playground/frontend/README.md
@@ -25,6 +25,18 @@ Beam Playground is an interactive environment to try out Beam transforms and exa
 
 ## Getting Started
 
+Running, debugging, and testing all require this first step that fetches
+dependencies and generates code:
+
+```bash
+cd playground_components
+flutter pub get
+flutter pub run build_runner build
+cd ..
+flutter pub get
+flutter pub run build_runner build
+```
+
 ### Run
 
 See [playground/README.md](../README.md) for details on requirements and setup.
diff --git a/playground/frontend/assets/theme.svg b/playground/frontend/assets/theme.svg
deleted file mode 100644
index 10d8b3d5c9b..00000000000
--- a/playground/frontend/assets/theme.svg
+++ /dev/null
@@ -1,27 +0,0 @@
-<!--
-    Licensed to the Apache Software Foundation (ASF) under one
-    or more contributor license agreements.  See the NOTICE file
-    distributed with this work for additional information
-    regarding copyright ownership.  The ASF licenses this file
-    to you under the Apache License, Version 2.0 (the
-    "License"); you may not use this file except in compliance
-    with the License.  You may obtain a copy of the License at
-
-      http://www.apache.org/licenses/LICENSE-2.0
-
-    Unless required by applicable law or agreed to in writing,
-    software distributed under the License is distributed on an
-    "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
-    KIND, either express or implied.  See the License for the
-    specific language governing permissions and limitations
-    under the License.
--->
-
-<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
-    <path
-        d="M12 21C16.9706 21 21 16.9706 21 12C21 10.8144 20.7708 9.68239 20.3542 8.64581C20.3067 8.52774 20.2569 8.41091 20.2046 8.29539C19.754 7.29897 19.1272 6.39928 18.364 5.63604C18.1972 5.46923 18.0238 5.30895 17.8444 5.15559C17.2028 4.60724 16.4833 4.14752 15.7046 3.79539C14.5748 3.28444 13.3206 3 12 3V11V17V21Z"
-        fill="#A0A4AB" />
-    <path
-        d="M12 21C16.9706 21 21 16.9706 21 12C21 10.8144 20.7708 9.68239 20.3542 8.64581C20.3067 8.52774 20.2569 8.41091 20.2046 8.29539C19.754 7.29897 19.1272 6.39928 18.364 5.63604C18.1972 5.46923 18.0238 5.30895 17.8444 5.15559C17.2028 4.60724 16.4833 4.14752 15.7046 3.79539C14.5748 3.28444 13.3206 3 12 3M12 21C7.02944 21 3 16.9706 3 12C3 7.02944 7.02944 3 12 3M12 21V17V11V3"
-        stroke="#A0A4AB" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" />
-</svg>
diff --git a/playground/frontend/playground_components/pubspec.yaml b/playground/frontend/assets/translations/en.yaml
similarity index 72%
copy from playground/frontend/playground_components/pubspec.yaml
copy to playground/frontend/assets/translations/en.yaml
index 56037dea769..c7d74f96d44 100644
--- a/playground/frontend/playground_components/pubspec.yaml
+++ b/playground/frontend/assets/translations/en.yaml
@@ -15,20 +15,7 @@
 #  specific language governing permissions and limitations
 #  under the License.
 
-name: playground_components
-description: Reusable Playground components
-version: 0.0.1
-
-environment:
-  sdk: '>=2.17.6 <3.0.0'
-  flutter: '>=1.17.0'
-
-dependencies:
-  flutter: { sdk: flutter }
-  total_lints: ^2.17.4
-
-dev_dependencies:
-  flutter_lints: ^2.0.0
-  flutter_test: { sdk: flutter }
-
-flutter:
+intents:
+  playground:
+    clearOutput: 'Clear Output'
+    newExample: 'New Example'
diff --git a/playground/frontend/build.gradle b/playground/frontend/build.gradle
index fe8aa85bdf1..5843927fc77 100644
--- a/playground/frontend/build.gradle
+++ b/playground/frontend/build.gradle
@@ -40,26 +40,18 @@ dependencies {
   dockerDependency project(path: playgroundJobServerProject, configuration: "shadow")
 }
 
-task removeBuild  {
-  group = "verification"
-  description = "remove build artifacts"
-  doLast {
-    exec {
-      executable("rm")
-      args("-r", "build")
-    }
-  }
-}
-
 task analyze  {
-  dependsOn("pubGet")
-  dependsOn("removeBuild")
+  dependsOn("playground_components:generate")
+  dependsOn("generate")
+
   group = "verification"
   description = "Analyze dart code"
+
   doLast {
     exec {
+      // Exact paths instead of '.' so it does not go into playground_components
       executable("dart")
-      args("analyze", ".")
+      args("analyze", "lib", "test")
     }
   }
 }
@@ -80,8 +72,9 @@ task format {
   description = "Idiomatically format Dart source code"
   doLast {
     exec {
+      // Exact paths instead of '.' so it does not go into playground_components
       executable("dart")
-      args("format", ".")
+      args("format", "lib", "test")
     }
   }
 }
@@ -98,9 +91,12 @@ task run {
 }
 
 task test {
-  dependsOn("pubGet")
+  dependsOn("playground_components:generate")
+  dependsOn("generate")
+
   group = "verification"
   description = "flutter test"
+
   doLast {
     exec {
       executable("flutter")
@@ -110,10 +106,37 @@ task test {
 }
 
 task precommit {
-  dependsOn(":playground:frontend:removeBuild")
-  dependsOn(":playground:frontend:pubGet")
-  dependsOn(":playground:frontend:analyze")
-  dependsOn(":playground:frontend:test")
+  dependsOn("playground_components:precommit")
+
+  dependsOn("analyze")
+  dependsOn("test")
+}
+
+task generate {
+  dependsOn("flutterClean")
+  dependsOn("pubGet")
+
+  group = "build"
+  description = "Generate code"
+
+  doLast {
+    exec {
+      executable("flutter")
+      args("pub", "run", "build_runner", "build", "--delete-conflicting-outputs")
+    }
+  }
+}
+
+task flutterClean {
+  group = "build"
+  description = "Remove build artifacts"
+
+  doLast {
+    exec {
+      executable("flutter")
+      args("clean")
+    }
+  }
 }
 
 task copyDockerfileDependencies(type: Copy) {
diff --git a/playground/frontend/lib/components/banner/banner_description.dart b/playground/frontend/lib/components/banner/banner_description.dart
index ab9ccfd5179..34ff3b51cef 100644
--- a/playground/frontend/lib/components/banner/banner_description.dart
+++ b/playground/frontend/lib/components/banner/banner_description.dart
@@ -18,7 +18,6 @@
 
 import 'package:flutter/gestures.dart';
 import 'package:flutter/material.dart';
-import 'package:playground/config/theme.dart';
 import 'package:playground/constants/font_weight.dart';
 import 'package:playground/constants/sizes.dart';
 import 'package:playground/modules/examples/components/description_popover/description_popover.dart';
@@ -61,10 +60,9 @@ class BannerDescription extends StatelessWidget {
                     RichText(
                       text: TextSpan(
                         children: [
-                          TextSpan(
+                          const TextSpan(
                             text: kBannerDescription1,
                             style: TextStyle(
-                              color: ThemeColors.of(context).textColor,
                               height: kDescriptionLineHeight,
                             ),
                           ),
@@ -76,10 +74,9 @@ class BannerDescription extends StatelessWidget {
                                 ..onTap = () async {
                                   launchUrl(Uri.parse(kBannerUrl));
                                 }),
-                          TextSpan(
+                          const TextSpan(
                             text: kHyperlinkText,
                             style: TextStyle(
-                              color: ThemeColors.of(context).textColor,
                               height: kDescriptionLineHeight,
                             ),
                           ),
diff --git a/playground/frontend/lib/components/dropdown_button/dropdown_button.dart b/playground/frontend/lib/components/dropdown_button/dropdown_button.dart
index fa89fce72ff..c2aff6f2f95 100644
--- a/playground/frontend/lib/components/dropdown_button/dropdown_button.dart
+++ b/playground/frontend/lib/components/dropdown_button/dropdown_button.dart
@@ -17,9 +17,9 @@
  */
 
 import 'package:flutter/material.dart';
-import 'package:playground/config/theme.dart';
 import 'package:playground/constants/sizes.dart';
 import 'package:playground/utils/dropdown_utils.dart';
+import 'package:playground_components/playground_components.dart';
 
 const int kAnimationDurationInMilliseconds = 80;
 const Offset kAnimationBeginOffset = Offset(0.0, -0.02);
@@ -87,10 +87,12 @@ class _AppDropdownButtonState extends State<AppDropdownButton>
 
   @override
   Widget build(BuildContext context) {
+    final ext = Theme.of(context).extension<BeamThemeExtension>()!;
+
     return Container(
       height: kContainerHeight,
       decoration: BoxDecoration(
-        color: ThemeColors.of(context).dropdownButton,
+        color: ext.fieldBackgroundColor,
         borderRadius: BorderRadius.circular(kSmBorderRadius),
       ),
       child: TextButton(
@@ -149,7 +151,7 @@ class _AppDropdownButtonState extends State<AppDropdownButton>
                     height: widget.height,
                     width: widget.width,
                     decoration: BoxDecoration(
-                      color: ThemeColors.of(context).background,
+                      color: Theme.of(context).backgroundColor,
                       borderRadius: BorderRadius.circular(kMdBorderRadius),
                     ),
                     child: widget.createDropdown(_close),
diff --git a/playground/frontend/lib/components/playground_run_or_cancel_button.dart b/playground/frontend/lib/components/playground_run_or_cancel_button.dart
new file mode 100644
index 00000000000..b555a71a882
--- /dev/null
+++ b/playground/frontend/lib/components/playground_run_or_cancel_button.dart
@@ -0,0 +1,57 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import 'package:flutter/widgets.dart';
+import 'package:playground_components/playground_components.dart';
+import 'package:provider/provider.dart';
+
+import '../modules/analytics/analytics_service.dart';
+import '../utils/analytics_utils.dart';
+
+class PlaygroundRunOrCancelButton extends StatelessWidget {
+  const PlaygroundRunOrCancelButton();
+
+  @override
+  Widget build(BuildContext context) {
+    return Consumer<PlaygroundController>(
+      builder: (context, playgroundController, child) {
+        final analyticsService = AnalyticsService.get(context);
+        final stopwatch = Stopwatch();
+        final exampleName = getAnalyticsExampleName(playgroundController);
+
+        return RunOrCancelButton(
+          playgroundController: playgroundController,
+          beforeCancel: () {
+            final exampleName = getAnalyticsExampleName(playgroundController);
+            analyticsService.trackClickCancelRunEvent(exampleName);
+          },
+          beforeRun: () {
+            stopwatch.start();
+            analyticsService.trackClickRunEvent(exampleName);
+          },
+          onComplete: () {
+            analyticsService.trackRunTimeEvent(
+              exampleName,
+              stopwatch.elapsedMilliseconds,
+            );
+          },
+        );
+      }
+    );
+  }
+}
diff --git a/playground/frontend/lib/components/toggle_theme_button/toggle_theme_button.dart b/playground/frontend/lib/components/toggle_theme_button/toggle_theme_button.dart
deleted file mode 100644
index 730b5b240b9..00000000000
--- a/playground/frontend/lib/components/toggle_theme_button/toggle_theme_button.dart
+++ /dev/null
@@ -1,55 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License.  You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-import 'package:flutter/material.dart';
-import 'package:flutter_gen/gen_l10n/app_localizations.dart';
-import 'package:flutter_svg/flutter_svg.dart';
-import 'package:playground/config/theme.dart';
-import 'package:playground/constants/assets.dart';
-import 'package:playground/constants/sizes.dart';
-import 'package:playground/modules/analytics/analytics_service.dart';
-import 'package:provider/provider.dart';
-
-class ToggleThemeButton extends StatelessWidget {
-  const ToggleThemeButton({Key? key}) : super(key: key);
-
-  @override
-  Widget build(BuildContext context) {
-    final appLocale = AppLocalizations.of(context)!;
-
-    return Consumer<ThemeSwitchNotifier>(builder: (context, notifier, child) {
-      final text = notifier.isDarkMode ? appLocale.lightMode : appLocale.darkMode;
-
-      return Padding(
-        padding: const EdgeInsets.symmetric(
-          vertical: kSmSpacing,
-          horizontal: kMdSpacing,
-        ),
-        child: TextButton.icon(
-          icon: SvgPicture.asset(kThemeIconAsset),
-          label: Text(text),
-          onPressed: () {
-            notifier.toggleTheme();
-            AnalyticsService.get(context)
-                .trackClickToggleTheme(!notifier.isDarkMode);
-          },
-        ),
-      );
-    });
-  }
-}
diff --git a/playground/frontend/lib/config/theme.dart b/playground/frontend/lib/config/theme.dart
deleted file mode 100644
index 62a30f0082b..00000000000
--- a/playground/frontend/lib/config/theme.dart
+++ /dev/null
@@ -1,305 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License.  You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-import 'package:code_text_field/code_text_field.dart';
-import 'package:flutter/material.dart';
-import 'package:playground/constants/colors.dart';
-import 'package:playground/constants/font_weight.dart';
-import 'package:playground/constants/fonts.dart';
-import 'package:playground/constants/sizes.dart';
-import 'package:playground/modules/editor/components/editor_themes.dart';
-import 'package:provider/provider.dart';
-import 'package:shared_preferences/shared_preferences.dart';
-
-const kThemeMode = 'theme_mode';
-
-class ThemeSwitchNotifier extends ChangeNotifier {
-  late SharedPreferences _preferences;
-  ThemeMode themeMode = ThemeMode.light;
-
-  static const _darkThemeColors = ThemeColors.fromBrightness(isDark: true);
-  static const _lightThemeColors = ThemeColors.fromBrightness(isDark: false);
-
-  ThemeColors get themeColors {
-    switch (themeMode) {
-      case ThemeMode.dark:
-        return _darkThemeColors;
-      default:
-        return _lightThemeColors;
-    }
-  }
-
-  final _darkCodeTheme = createTheme(_darkThemeColors);
-  final _lightCodeTheme = createTheme(_lightThemeColors);
-
-  CodeThemeData get codeTheme {
-    switch (themeMode) {
-      case ThemeMode.dark:
-        return _darkCodeTheme;
-      default:
-        return _lightCodeTheme;
-    }
-  }
-
-  init() {
-    _setPreferences();
-  }
-
-  _setPreferences() async {
-    _preferences = await SharedPreferences.getInstance();
-    themeMode = _preferences.getString(kThemeMode) == ThemeMode.dark.toString()
-        ? ThemeMode.dark
-        : ThemeMode.light;
-    notifyListeners();
-  }
-
-  bool get isDarkMode {
-    return themeMode == ThemeMode.dark;
-  }
-
-  void toggleTheme() {
-    themeMode = themeMode == ThemeMode.light ? ThemeMode.dark : ThemeMode.light;
-    _preferences.setString(kThemeMode, themeMode.toString());
-    notifyListeners();
-  }
-}
-
-class ThemeSwitchNotifierProvider extends StatelessWidget {
-  final Widget child;
-
-  const ThemeSwitchNotifierProvider({
-    super.key,
-    required this.child,
-  });
-
-  @override
-  Widget build(BuildContext context) {
-    return ChangeNotifierProvider<ThemeSwitchNotifier>(
-      create: (context) => ThemeSwitchNotifier()..init(),
-      child: Consumer<ThemeSwitchNotifier>(
-        builder: (context, themeSwitchNotifier, _) => ThemeColorsProvider(
-          data: themeSwitchNotifier.themeColors,
-          child: child,
-        ),
-      ),
-    );
-  }
-}
-
-class ThemeColorsProvider extends StatelessWidget {
-  final ThemeColors data;
-  final Widget child;
-
-  const ThemeColorsProvider({
-    super.key,
-    required this.data,
-    required this.child,
-  });
-
-  @override
-  Widget build(BuildContext context) {
-    return Provider<ThemeColors>.value(
-      value: data,
-      child: child,
-    );
-  }
-}
-
-TextTheme createTextTheme(Color textColor) {
-  return getBaseFontTheme(
-    const TextTheme(
-      headline1: TextStyle(),
-      headline2: TextStyle(),
-      headline3: TextStyle(),
-      headline4: TextStyle(),
-      headline5: TextStyle(),
-      headline6: TextStyle(),
-      subtitle1: TextStyle(),
-      subtitle2: TextStyle(),
-      bodyText1: TextStyle(),
-      bodyText2: TextStyle(),
-      caption: TextStyle(),
-      overline: TextStyle(),
-      button: TextStyle(fontWeight: kBoldWeight),
-    ).apply(
-      bodyColor: textColor,
-      displayColor: textColor,
-    ),
-  );
-}
-
-TextButtonThemeData createTextButtonTheme(Color textColor) {
-  return TextButtonThemeData(
-    style: TextButton.styleFrom(
-      primary: textColor,
-      shape: const RoundedRectangleBorder(
-        borderRadius: BorderRadius.all(Radius.circular(kLgBorderRadius)),
-      ),
-    ),
-  );
-}
-
-OutlinedButtonThemeData createOutlineButtonTheme(Color textColor) {
-  return OutlinedButtonThemeData(
-    style: OutlinedButton.styleFrom(
-      primary: textColor,
-      shape: const RoundedRectangleBorder(
-        borderRadius: BorderRadius.all(Radius.circular(kSmBorderRadius)),
-      ),
-    ),
-  );
-}
-
-ElevatedButtonThemeData createElevatedButtonTheme(Color primaryColor) {
-  return ElevatedButtonThemeData(
-    style: ElevatedButton.styleFrom(primary: primaryColor),
-  );
-}
-
-PopupMenuThemeData createPopupMenuTheme() {
-  return const PopupMenuThemeData(
-    shape: RoundedRectangleBorder(
-      borderRadius: BorderRadius.all(
-        Radius.circular(kLgBorderRadius),
-      ),
-    ),
-  );
-}
-
-AppBarTheme createAppBarTheme(Color backgroundColor) {
-  return AppBarTheme(
-    color: backgroundColor,
-    elevation: 1,
-    centerTitle: false,
-  );
-}
-
-TabBarTheme createTabBarTheme(Color textColor, Color indicatorColor) {
-  const labelStyle = TextStyle(fontWeight: kMediumWeight);
-  return TabBarTheme(
-    unselectedLabelColor: textColor,
-    labelColor: textColor,
-    labelStyle: labelStyle,
-    unselectedLabelStyle: labelStyle,
-    indicator: UnderlineTabIndicator(
-      borderSide: BorderSide(width: 2.0, color: indicatorColor),
-    ),
-  );
-}
-
-DialogTheme createDialogTheme(Color textColor) {
-  return DialogTheme(
-    titleTextStyle: TextStyle(
-      color: textColor,
-      fontSize: 32.0,
-      fontWeight: kBoldWeight,
-    ),
-  );
-}
-
-final kLightTheme = ThemeData(
-  brightness: Brightness.light,
-  primaryColor: kLightPrimary,
-  backgroundColor: kLightPrimaryBackground,
-  appBarTheme: createAppBarTheme(kLightSecondaryBackground),
-  textTheme: createTextTheme(kLightText),
-  popupMenuTheme: createPopupMenuTheme(),
-  textButtonTheme: createTextButtonTheme(kLightText),
-  outlinedButtonTheme: createOutlineButtonTheme(kLightText),
-  elevatedButtonTheme: createElevatedButtonTheme(kLightPrimary),
-  tabBarTheme: createTabBarTheme(kLightText, kLightPrimary),
-  dialogTheme: createDialogTheme(kLightText),
-);
-
-final kDarkTheme = ThemeData(
-  brightness: Brightness.dark,
-  primaryColor: kDarkPrimary,
-  backgroundColor: kDarkPrimaryBackground,
-  appBarTheme: createAppBarTheme(kDarkSecondaryBackground),
-  textTheme: createTextTheme(kDarkText),
-  popupMenuTheme: createPopupMenuTheme(),
-  textButtonTheme: createTextButtonTheme(kDarkText),
-  outlinedButtonTheme: createOutlineButtonTheme(kDarkText),
-  elevatedButtonTheme: createElevatedButtonTheme(kDarkPrimary),
-  tabBarTheme: createTabBarTheme(kDarkText, kDarkPrimary),
-  dialogTheme: createDialogTheme(kDarkText),
-);
-
-class ThemeColors {
-  final Color? _background;
-  final Color? _dropdownButton;
-
-  final bool isDark;
-
-  static ThemeColors of(BuildContext context, {bool listen = true}) {
-    return Provider.of<ThemeColors>(context, listen: listen);
-  }
-
-  ThemeColors({
-    required this.isDark,
-    Color? background,
-    Color? dropdownButtonColor,
-  })  : _background = background,
-        _dropdownButton = dropdownButtonColor;
-
-  const ThemeColors.fromBrightness({
-    required this.isDark,
-  })  : _background = null,
-        _dropdownButton = null;
-
-  ThemeColors copyWith({
-    Color? background,
-    Color? dropdownButton,
-  }) {
-    return ThemeColors(
-      isDark: isDark,
-      background: background ?? this.background,
-      dropdownButtonColor: dropdownButton ?? this.dropdownButton,
-    );
-  }
-
-  Color get dropdownButton =>
-      _dropdownButton ?? (isDark ? kDarkGrey : kLightGrey);
-
-  Color get divider => isDark ? kDarkGrey : kLightGrey;
-
-  Color get lightGreyColor => isDark ? kLightGrey1 : kLightGrey;
-
-  Color get primary => isDark ? kLightPrimary : kDarkPrimary;
-
-  Color get primaryBackgroundTextColor => Colors.white;
-
-  Color get lightGreyBackgroundTextColor => Colors.black;
-
-  Color get grey1Color => isDark ? kDarkGrey1 : kLightGrey1;
-
-  Color get secondaryBackground =>
-      isDark ? kDarkSecondaryBackground : kLightSecondaryBackground;
-
-  Color get background =>
-      _background ??
-      (isDark ? kDarkPrimaryBackground : kLightPrimaryBackground);
-
-  Color get code1 => isDark ? kDarkCode2 : kLightCode2;
-
-  Color get code2 => isDark ? kDarkCode1 : kLightCode1;
-
-  Color get codeComment => isDark ? kDarkCodeComment : kLightCodeComment;
-
-  Color get textColor => isDark ? kDarkText : kLightText;
-}
diff --git a/playground/frontend/lib/configure_web.dart b/playground/frontend/lib/configure_web.dart
deleted file mode 100644
index 5fa7eb6de54..00000000000
--- a/playground/frontend/lib/configure_web.dart
+++ /dev/null
@@ -1,23 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License.  You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-import 'package:flutter_web_plugins/flutter_web_plugins.dart';
-
-void configureApp() {
-  setUrlStrategy(PathUrlStrategy());
-}
diff --git a/playground/frontend/lib/constants/sizes.dart b/playground/frontend/lib/constants/sizes.dart
index 3d895aae285..c4515a5e3dc 100644
--- a/playground/frontend/lib/constants/sizes.dart
+++ b/playground/frontend/lib/constants/sizes.dart
@@ -25,23 +25,17 @@ const double kXlSpacing = 16.0;
 const double kXxlSpacing = 36.0;
 
 // sizes
-const kHeaderButtonHeight = 46.0;
-const kRunButtonWidth = 150.0;
 const kButtonHeight = 40.0;
 const kIconButtonSplashRadius = 24.0;
-const kFooterHeight = 32.0;
 
 // border radius
 const double kSmBorderRadius = 4.0;
 const double kMdBorderRadius = 6.0;
-const double kLgBorderRadius = 8.0;
-const double kXlBorderRadius = 28.0;
 
 // elevation
 const double kElevation = 2;
 
 // icon sizes
-const double kIconSizeXs = 8.0;
 const double kIconSizeSm = 16.0;
 const double kIconSizeMd = 24.0;
 const double kIconSizeLg = 32.0;
@@ -52,16 +46,8 @@ const double kCursorSize = 1.0;
 // container size
 const double kContainerHeight = 40.0;
 
-const double kCaptionFontSize = 10.0;
-const double kCodeFontSize = 14.0;
 const double kLabelFontSize = 16.0;
-const double kHintFontSize = 16.0;
 const double kTitleFontSize = 18.0;
 
 //divider size
 const double kDividerHeight = 1.0;
-const double kLgDividerHeight = 2.0;
-
-//loading indicator size
-const double kMdLoadingIndicatorSize = 40.0;
-const double kLgLoadingIndicatorSize = 50.0;
diff --git a/playground/frontend/lib/l10n/app_en.arb b/playground/frontend/lib/l10n/app_en.arb
index 48a3a8675b8..538df437994 100644
--- a/playground/frontend/lib/l10n/app_en.arb
+++ b/playground/frontend/lib/l10n/app_en.arb
@@ -7,14 +7,6 @@
   "@darkMode": {
     "description": "Title for a theme switch"
   },
-  "newExample": "New Example",
-  "@newExample": {
-    "description": "Title for the New Example button"
-  },
-  "reset": "Reset",
-  "@reset": {
-    "description": "Title for the reset button"
-  },
   "run": "Run",
   "@run": {
     "description": "Title for the run button"
@@ -143,10 +135,6 @@
   "@reportIssue": {
     "description": "Title for the Report issue in GitHub button"
   },
-  "codeTextArea": "Code Text Area",
-  "@codeTextArea": {
-    "description": "Title for the Code text area semantics"
-  },
   "bottom": "Bottom",
   "@bottom": {
     "description": "Part of the output placements semantics label"
@@ -159,10 +147,6 @@
   "@left": {
     "description": "Part of the output placements semantics label"
   },
-  "clearOutput": "Clear Output",
-  "@clearOutput": {
-    "description": "Title for the Clear Output shortcut row"
-  },
   "pipelineOptions": "Pipeline Options",
   "@pipelineOptions": {
     "description": "Title for the Pipeline Options"
diff --git a/playground/frontend/lib/l10n/l10n.dart b/playground/frontend/lib/l10n/l10n.dart
index cd63ec77990..9f7e197317e 100644
--- a/playground/frontend/lib/l10n/l10n.dart
+++ b/playground/frontend/lib/l10n/l10n.dart
@@ -16,10 +16,12 @@
  * limitations under the License.
  */
 
-import 'package:flutter/material.dart';
+import 'dart:ui';
 
 class L10n {
+  static const en = Locale('en');
+
   static const locales = [
-    Locale('en'),
+    en,
   ];
 }
diff --git a/playground/frontend/lib/main.dart b/playground/frontend/lib/main.dart
index 2e57fc49a12..0c36af3e7bc 100644
--- a/playground/frontend/lib/main.dart
+++ b/playground/frontend/lib/main.dart
@@ -17,15 +17,34 @@
  */
 
 import 'package:akvelon_flutter_issue_106664_workaround/akvelon_flutter_issue_106664_workaround.dart';
+import 'package:easy_localization/easy_localization.dart';
+import 'package:easy_localization_ext/easy_localization_ext.dart';
+import 'package:easy_localization_loader/easy_localization_loader.dart';
 import 'package:flutter/material.dart';
 import 'package:intl/intl_browser.dart';
-import 'package:playground/configure_nonweb.dart'
-if (dart.library.html) 'package:playground/configure_web.dart';
 import 'package:playground/playground_app.dart';
+import 'package:playground_components/playground_components.dart';
+import 'package:url_strategy/url_strategy.dart';
 
-void main() {
+import 'l10n/l10n.dart';
+
+void main() async {
   FlutterIssue106664Workaround.instance.apply();
-  findSystemLocale();
-  configureApp();
-  runApp(const PlaygroundApp());
+  setPathUrlStrategy();
+  await EasyLocalization.ensureInitialized();
+
+  await findSystemLocale();
+  runApp(
+    EasyLocalization(
+      supportedLocales: L10n.locales,
+      startLocale: L10n.en,
+      fallbackLocale: L10n.en,
+      path: 'assets/translations',
+      assetLoader: MultiAssetLoader([
+        PlaygroundComponents.translationLoader,
+        YamlAssetLoader(),
+      ]),
+      child: const PlaygroundApp(),
+    ),
+  );
 }
diff --git a/playground/frontend/lib/modules/actions/components/new_example_action.dart b/playground/frontend/lib/modules/actions/components/new_example_action.dart
index 332b4beba39..594492987c1 100644
--- a/playground/frontend/lib/modules/actions/components/new_example_action.dart
+++ b/playground/frontend/lib/modules/actions/components/new_example_action.dart
@@ -16,13 +16,11 @@
  * limitations under the License.
  */
 
+import 'package:easy_localization/easy_localization.dart';
 import 'package:flutter/material.dart';
-import 'package:flutter_gen/gen_l10n/app_localizations.dart';
-import 'package:playground/config/theme.dart';
-import 'package:playground/modules/actions/components/header_icon_button.dart';
 import 'package:playground/modules/analytics/analytics_service.dart';
-import 'package:playground/modules/shortcuts/components/shortcut_tooltip.dart';
 import 'package:playground/modules/shortcuts/constants/global_shortcuts.dart';
+import 'package:playground_components/playground_components.dart';
 import 'package:url_launcher/url_launcher.dart';
 
 class NewExampleAction extends StatelessWidget {
@@ -35,9 +33,9 @@ class NewExampleAction extends StatelessWidget {
       child: HeaderIconButton(
         icon: Icon(
           Icons.add_circle_outline,
-          color: ThemeColors.of(context).grey1Color,
+          color: Theme.of(context).extension<BeamThemeExtension>()?.iconColor,
         ),
-        label: AppLocalizations.of(context)!.newExample,
+        label: 'intents.playground.newExample'.tr(),
         onPressed: () {
           launchUrl(Uri.parse('/'));
           AnalyticsService.get(context).trackClickNewExample();
diff --git a/playground/frontend/lib/modules/actions/components/reset_action.dart b/playground/frontend/lib/modules/actions/components/reset_action.dart
index 533bdec2d1c..a0e11199632 100644
--- a/playground/frontend/lib/modules/actions/components/reset_action.dart
+++ b/playground/frontend/lib/modules/actions/components/reset_action.dart
@@ -17,33 +17,21 @@
  */
 
 import 'package:flutter/material.dart';
-import 'package:flutter_svg/flutter_svg.dart';
-import 'package:playground/config/theme.dart';
-import 'package:playground/constants/assets.dart';
-import 'package:playground/modules/actions/components/header_icon_button.dart';
 import 'package:playground/modules/analytics/analytics_service.dart';
-import 'package:playground/modules/shortcuts/components/shortcut_tooltip.dart';
-import 'package:playground/modules/shortcuts/constants/global_shortcuts.dart';
-import 'package:flutter_gen/gen_l10n/app_localizations.dart';
+import 'package:playground_components/playground_components.dart';
+import 'package:provider/provider.dart';
 
 class ResetAction extends StatelessWidget {
-  final VoidCallback reset;
-
-  const ResetAction({Key? key, required this.reset}) : super(key: key);
+  const ResetAction();
 
   @override
   Widget build(BuildContext context) {
-    return ShortcutTooltip(
-      shortcut: kResetShortcut,
-      child: HeaderIconButton(
-        icon: SvgPicture.asset(
-          kResetIconAsset,
-          color: ThemeColors.of(context).grey1Color,
-        ),
-        label: AppLocalizations.of(context)!.reset,
-        onPressed: () {
-          reset();
-          AnalyticsService.get(context).trackReset();
+    final analyticsService = AnalyticsService.get(context);
+    return Consumer<PlaygroundController>(
+      builder: (context, playgroundController, child) => ResetButton(
+        playgroundController: playgroundController,
+        beforeReset: () {
+          analyticsService.trackReset();
         },
       ),
     );
diff --git a/playground/frontend/lib/modules/analytics/analytics_service.dart b/playground/frontend/lib/modules/analytics/analytics_service.dart
index 6571a4898d4..0416e7dcc52 100644
--- a/playground/frontend/lib/modules/analytics/analytics_service.dart
+++ b/playground/frontend/lib/modules/analytics/analytics_service.dart
@@ -17,8 +17,7 @@
  */
 
 import 'package:flutter/widgets.dart';
-import 'package:playground/modules/examples/models/example_model.dart';
-import 'package:playground/modules/sdk/models/sdk.dart';
+import 'package:playground_components/playground_components.dart';
 import 'package:provider/provider.dart';
 
 abstract class AnalyticsService {
@@ -26,8 +25,8 @@ abstract class AnalyticsService {
     return Provider.of<AnalyticsService>(context, listen: false);
   }
 
-  void trackSelectSdk(SDK? oldSdk, SDK newSdk);
-  void trackSelectExample(ExampleModel newExample);
+  void trackSelectSdk(Sdk? oldSdk, Sdk newSdk);
+  void trackSelectExample(ExampleBase newExample);
   void trackClickNewExample();
   void trackReset();
   void trackClickToggleTheme(bool isDark);
diff --git a/playground/frontend/lib/modules/analytics/google_analytics_service.dart b/playground/frontend/lib/modules/analytics/google_analytics_service.dart
index d9eaf2e6f12..7b083b3bc25 100644
--- a/playground/frontend/lib/modules/analytics/google_analytics_service.dart
+++ b/playground/frontend/lib/modules/analytics/google_analytics_service.dart
@@ -19,24 +19,23 @@
 import 'package:playground/config.g.dart';
 import 'package:playground/modules/analytics/analytics_events.dart';
 import 'package:playground/modules/analytics/analytics_service.dart';
-import 'package:playground/modules/examples/models/example_model.dart';
-import 'package:playground/modules/sdk/models/sdk.dart';
+import 'package:playground_components/playground_components.dart';
 import 'package:usage/usage_html.dart';
 
 class GoogleAnalyticsService implements AnalyticsService {
   final _analytics = AnalyticsHtml(kAnalyticsUA, 'beam', '1.0');
 
   @override
-  void trackSelectSdk(SDK? oldSdk, SDK newSdk) {
+  void trackSelectSdk(Sdk? oldSdk, Sdk newSdk) {
     safeSendEvent(
       kSdkCategory,
       kSelectSdkEvent,
-      label: '${oldSdk?.displayName}_${newSdk.displayName}',
+      label: '${oldSdk?.title}_${newSdk.title}',
     );
   }
 
   @override
-  void trackSelectExample(ExampleModel newExample) {
+  void trackSelectExample(ExampleBase newExample) {
     safeSendEvent(
       kExampleCategory,
       kSelectExampleEvent,
diff --git a/playground/frontend/lib/modules/editor/components/editor_themes.dart b/playground/frontend/lib/modules/editor/components/editor_themes.dart
deleted file mode 100644
index 42c68905f96..00000000000
--- a/playground/frontend/lib/modules/editor/components/editor_themes.dart
+++ /dev/null
@@ -1,64 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License.  You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-import 'package:code_text_field/code_text_field.dart';
-import 'package:flutter/material.dart';
-import 'package:playground/config/theme.dart';
-
-CodeThemeData createTheme(ThemeColors colors) {
-  return CodeThemeData(
-    styles: _createThemeStyles(colors),
-  );
-}
-
-Map<String, TextStyle> _createThemeStyles(ThemeColors colors) {
-  return {
-    'root': TextStyle(
-      backgroundColor: colors.background,
-      color: colors.textColor,
-    ),
-    'comment': TextStyle(color: colors.codeComment),
-    'quote': TextStyle(color: colors.code2),
-    'variable': TextStyle(color: colors.code2),
-    'keyword': TextStyle(color: colors.code2),
-    'selector-tag': TextStyle(color: colors.code2),
-    'built_in': TextStyle(color: colors.code2),
-    'name': TextStyle(color: colors.code2),
-    'tag': TextStyle(color: colors.code2),
-    'string': TextStyle(color: colors.code1),
-    'title': TextStyle(color: colors.code1),
-    'section': TextStyle(color: colors.code1),
-    'attribute': TextStyle(color: colors.code1),
-    'literal': TextStyle(color: colors.code1),
-    'template-tag': TextStyle(color: colors.code1),
-    'template-variable': TextStyle(color: colors.code1),
-    'type': TextStyle(color: colors.code1),
-    'addition': TextStyle(color: colors.code1),
-    'deletion': TextStyle(color: colors.code2),
-    'selector-attr': TextStyle(color: colors.code2),
-    'selector-pseudo': TextStyle(color: colors.code2),
-    'meta': TextStyle(color: colors.code2),
-    'doctag': TextStyle(color: colors.codeComment),
-    'attr': TextStyle(color: colors.primary),
-    'symbol': TextStyle(color: colors.code2),
-    'bullet': TextStyle(color: colors.code2),
-    'link': TextStyle(color: colors.code2),
-    'emphasis': const TextStyle(fontStyle: FontStyle.italic),
-    'strong': const TextStyle(fontWeight: FontWeight.bold),
-  };
-}
diff --git a/playground/frontend/lib/modules/editor/components/pipeline_options_dropdown/pipeline_options_dropdown_body.dart b/playground/frontend/lib/modules/editor/components/pipeline_options_dropdown/pipeline_options_dropdown_body.dart
index fd1d6c9eeda..0b8cd5836b4 100644
--- a/playground/frontend/lib/modules/editor/components/pipeline_options_dropdown/pipeline_options_dropdown_body.dart
+++ b/playground/frontend/lib/modules/editor/components/pipeline_options_dropdown/pipeline_options_dropdown_body.dart
@@ -24,7 +24,7 @@ import 'package:playground/modules/editor/components/pipeline_options_dropdown/p
 import 'package:playground/modules/editor/components/pipeline_options_dropdown/pipeline_options_dropdown_input.dart';
 import 'package:playground/modules/editor/components/pipeline_options_dropdown/pipeline_options_dropdown_separator.dart';
 import 'package:playground/modules/editor/components/pipeline_options_dropdown/pipeline_options_form.dart';
-import 'package:playground/modules/editor/parsers/run_options_parser.dart';
+import 'package:playground_components/playground_components.dart';
 
 const kOptionsTabIndex = 0;
 const kRawTabIndex = 1;
diff --git a/playground/frontend/lib/modules/editor/components/pipeline_options_dropdown/pipeline_options_dropdown_separator.dart b/playground/frontend/lib/modules/editor/components/pipeline_options_dropdown/pipeline_options_dropdown_separator.dart
index 17070dd84f1..78f8b0d7c34 100644
--- a/playground/frontend/lib/modules/editor/components/pipeline_options_dropdown/pipeline_options_dropdown_separator.dart
+++ b/playground/frontend/lib/modules/editor/components/pipeline_options_dropdown/pipeline_options_dropdown_separator.dart
@@ -17,8 +17,8 @@
  */
 
 import 'package:flutter/material.dart';
-import 'package:playground/config/theme.dart';
 import 'package:playground/constants/sizes.dart';
+import 'package:playground_components/playground_components.dart';
 
 class PipelineOptionsDropdownSeparator extends StatelessWidget {
   const PipelineOptionsDropdownSeparator({Key? key}) : super(key: key);
@@ -28,7 +28,7 @@ class PipelineOptionsDropdownSeparator extends StatelessWidget {
     return Container(
       height: kDividerHeight,
       decoration: BoxDecoration(
-        color: ThemeColors.of(context).lightGreyColor,
+        color: Theme.of(context).extension<BeamThemeExtension>()?.borderColor,
       ),
     );
   }
diff --git a/playground/frontend/lib/modules/editor/components/pipeline_options_dropdown/pipeline_options_form.dart b/playground/frontend/lib/modules/editor/components/pipeline_options_dropdown/pipeline_options_form.dart
index a7f2eabf87e..6aa41c0c3cd 100644
--- a/playground/frontend/lib/modules/editor/components/pipeline_options_dropdown/pipeline_options_form.dart
+++ b/playground/frontend/lib/modules/editor/components/pipeline_options_dropdown/pipeline_options_form.dart
@@ -16,10 +16,9 @@
  * limitations under the License.
  */
 
+import 'package:collection/collection.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter_gen/gen_l10n/app_localizations.dart';
-import 'package:collection/collection.dart';
-import 'package:playground/config/theme.dart';
 import 'package:playground/constants/colors.dart';
 import 'package:playground/constants/sizes.dart';
 import 'package:playground/modules/editor/components/pipeline_options_dropdown/pipeline_option_controller.dart';
@@ -81,7 +80,7 @@ class PipelineOptionsForm extends StatelessWidget {
                     Icons.delete_outlined,
                     color: kLightPrimary,
                   ),
-                  color: ThemeColors.of(context).grey1Color,
+                  color: Theme.of(context).dividerColor,
                   onPressed: () => onDelete(index),
                 ),
               ),
diff --git a/playground/frontend/lib/modules/editor/components/pipeline_options_dropdown/pipeline_options_text_field.dart b/playground/frontend/lib/modules/editor/components/pipeline_options_dropdown/pipeline_options_text_field.dart
index 2c118736528..d4202b12569 100644
--- a/playground/frontend/lib/modules/editor/components/pipeline_options_dropdown/pipeline_options_text_field.dart
+++ b/playground/frontend/lib/modules/editor/components/pipeline_options_dropdown/pipeline_options_text_field.dart
@@ -17,8 +17,8 @@
  */
 
 import 'package:flutter/material.dart';
-import 'package:playground/config/theme.dart';
 import 'package:playground/constants/sizes.dart';
+import 'package:playground_components/playground_components.dart';
 
 class PipelineOptionsTextField extends StatelessWidget {
   final TextEditingController controller;
@@ -32,6 +32,9 @@ class PipelineOptionsTextField extends StatelessWidget {
 
   @override
   Widget build(BuildContext context) {
+    final themeData = Theme.of(context);
+    final ext = themeData.extension<BeamThemeExtension>()!;
+
     return Container(
       margin: const EdgeInsets.only(
         top: kMdSpacing,
@@ -48,16 +51,15 @@ class PipelineOptionsTextField extends StatelessWidget {
           controller: controller,
           decoration: InputDecoration(
             contentPadding: const EdgeInsets.all(kMdSpacing),
-            border: _getInputBorder(ThemeColors.of(context).lightGreyColor),
-            focusedBorder: _getInputBorder(ThemeColors.of(context).primary),
+            border: _getInputBorder(ext.borderColor),
+            focusedBorder: _getInputBorder(themeData.primaryColor),
           ),
-          cursorColor: ThemeColors.of(context).textColor,
         ),
       ),
     );
   }
 
-  _getInputBorder(Color color) {
+  OutlineInputBorder _getInputBorder(Color color) {
     return OutlineInputBorder(
       borderSide: BorderSide(color: color),
       borderRadius: BorderRadius.circular(kMdBorderRadius),
diff --git a/playground/frontend/lib/modules/editor/components/share_dropdown/link_text_field.dart b/playground/frontend/lib/modules/editor/components/share_dropdown/link_text_field.dart
index 9708a88208a..67430514e5a 100644
--- a/playground/frontend/lib/modules/editor/components/share_dropdown/link_text_field.dart
+++ b/playground/frontend/lib/modules/editor/components/share_dropdown/link_text_field.dart
@@ -18,9 +18,9 @@
 
 import 'package:flutter/material.dart';
 import 'package:flutter/services.dart';
-import 'package:playground/config/theme.dart';
 import 'package:playground/constants/font_weight.dart';
 import 'package:playground/constants/sizes.dart';
+import 'package:playground_components/playground_components.dart';
 
 const _kTextFieldMaxHeight = 45.0;
 
@@ -45,9 +45,11 @@ class _LinkTextFieldState extends State<LinkTextField> {
 
   @override
   Widget build(BuildContext context) {
+    final themeData = Theme.of(context);
+
     return Container(
       decoration: BoxDecoration(
-        color: ThemeColors.of(context).dropdownButton,
+        color: themeData.extension<BeamThemeExtension>()?.borderColor,
         borderRadius: BorderRadius.circular(kSmBorderRadius),
       ),
       child: Container(
@@ -66,7 +68,7 @@ class _LinkTextFieldState extends State<LinkTextField> {
             style: TextStyle(
               fontSize: kLabelFontSize,
               fontWeight: kNormalWeight,
-              color: ThemeColors.of(context).primary,
+              color: themeData.primaryColor,
             ),
           ),
         ),
diff --git a/playground/frontend/lib/modules/editor/components/share_dropdown/share_button.dart b/playground/frontend/lib/modules/editor/components/share_dropdown/share_button.dart
index 9355934e718..f0f6026ea69 100644
--- a/playground/frontend/lib/modules/editor/components/share_dropdown/share_button.dart
+++ b/playground/frontend/lib/modules/editor/components/share_dropdown/share_button.dart
@@ -19,8 +19,8 @@
 import 'package:flutter/material.dart';
 import 'package:flutter_gen/gen_l10n/app_localizations.dart';
 import 'package:playground/components/dropdown_button/dropdown_button.dart';
-import 'package:playground/config/theme.dart';
 import 'package:playground/modules/editor/components/share_dropdown/share_dropdown_body.dart';
+import 'package:playground_components/playground_components.dart';
 
 const _kShareDropdownHeight = 140.0;
 const _kShareDropdownWidth = 460.0;
@@ -31,22 +31,28 @@ class ShareButton extends StatelessWidget {
 
   @override
   Widget build(BuildContext context) {
+    final parentThemeData = Theme.of(context);
+    final ext = parentThemeData.extension<BeamThemeExtension>()!;
     final appLocale = AppLocalizations.of(context)!;
-    final parentThemeData = ThemeColors.of(context);
 
     final themeData = parentThemeData.copyWith(
-      background: parentThemeData.secondaryBackground,
-      dropdownButton: parentThemeData.primary.withOpacity(_kButtonColorOpacity),
+      backgroundColor: ext.secondaryBackgroundColor,
+      extensions: {
+        ext.copyWith(
+          fieldBackgroundColor:
+              parentThemeData.primaryColor.withOpacity(_kButtonColorOpacity),
+        ),
+      },
     );
 
-    return ThemeColorsProvider(
+    return Theme(
       data: themeData,
       child: AppDropdownButton(
         buttonText: Text(appLocale.shareMyCode),
         showArrow: false,
         leading: Icon(
           Icons.share_outlined,
-          color: ThemeColors.of(context).primary,
+          color: themeData.primaryColor,
         ),
         height: _kShareDropdownHeight,
         width: _kShareDropdownWidth,
diff --git a/playground/frontend/lib/modules/editor/components/share_dropdown/share_dropdown_body.dart b/playground/frontend/lib/modules/editor/components/share_dropdown/share_dropdown_body.dart
index 68a20837367..18366335fee 100644
--- a/playground/frontend/lib/modules/editor/components/share_dropdown/share_dropdown_body.dart
+++ b/playground/frontend/lib/modules/editor/components/share_dropdown/share_dropdown_body.dart
@@ -19,7 +19,7 @@
 import 'package:flutter/material.dart';
 import 'package:playground/modules/editor/components/share_dropdown/share_tabs/share_tabs.dart';
 import 'package:playground/modules/editor/components/share_dropdown/share_tabs_headers.dart';
-import 'package:playground/modules/output/components/output_header/tab_header.dart';
+import 'package:playground_components/playground_components.dart';
 
 const _kTabsCount = 2;
 
diff --git a/playground/frontend/lib/modules/editor/components/share_dropdown/share_tabs/share_tabs.dart b/playground/frontend/lib/modules/editor/components/share_dropdown/share_tabs/share_tabs.dart
index 8c2e72948ae..45d5b82cd00 100644
--- a/playground/frontend/lib/modules/editor/components/share_dropdown/share_tabs/share_tabs.dart
+++ b/playground/frontend/lib/modules/editor/components/share_dropdown/share_tabs/share_tabs.dart
@@ -19,7 +19,7 @@
 import 'package:flutter/material.dart';
 import 'package:playground/modules/editor/components/share_dropdown/share_tabs/example_share_tabs.dart';
 import 'package:playground/modules/editor/components/share_dropdown/share_tabs/snippet_save_and_share_tabs.dart';
-import 'package:playground/pages/playground/states/playground_state.dart';
+import 'package:playground_components/playground_components.dart';
 import 'package:provider/provider.dart';
 
 class ShareTabs extends StatelessWidget {
@@ -34,17 +34,17 @@ class ShareTabs extends StatelessWidget {
   Widget build(BuildContext context) {
     return Container(
       color: Theme.of(context).backgroundColor,
-      child: Consumer<PlaygroundState>(
-        builder: (context, playgroundState, _) {
-          if (playgroundState.isExampleChanged) {
+      child: Consumer<PlaygroundController>(
+        builder: (context, playgroundController, _) {
+          if (playgroundController.isExampleChanged) {
             return SnippetSaveAndShareTabs(
-              playgroundState: playgroundState,
+              playgroundController: playgroundController,
               tabController: tabController,
             );
           }
 
           return ExampleShareTabs(
-            examplePath: playgroundState.selectedExample!.path,
+            examplePath: playgroundController.selectedExample!.path,
             tabController: tabController,
           );
         },
diff --git a/playground/frontend/lib/modules/editor/components/share_dropdown/share_tabs/snippet_save_and_share_tabs.dart b/playground/frontend/lib/modules/editor/components/share_dropdown/share_tabs/snippet_save_and_share_tabs.dart
index ddd1bbb0f53..050725de8d9 100644
--- a/playground/frontend/lib/modules/editor/components/share_dropdown/share_tabs/snippet_save_and_share_tabs.dart
+++ b/playground/frontend/lib/modules/editor/components/share_dropdown/share_tabs/snippet_save_and_share_tabs.dart
@@ -17,28 +17,26 @@
  */
 
 import 'package:flutter/material.dart';
-import 'package:playground/components/loading_indicator/loading_indicator.dart';
-import 'package:playground/constants/sizes.dart';
 import 'package:playground/modules/editor/components/share_dropdown/share_tabs/example_share_tabs.dart';
-import 'package:playground/pages/playground/states/playground_state.dart';
+import 'package:playground_components/playground_components.dart';
 
 class SnippetSaveAndShareTabs extends StatelessWidget {
-  final PlaygroundState playgroundState;
+  final PlaygroundController playgroundController;
   final TabController tabController;
 
   const SnippetSaveAndShareTabs({
     super.key,
-    required this.playgroundState,
+    required this.playgroundController,
     required this.tabController,
   });
 
   @override
   Widget build(BuildContext context) {
     return FutureBuilder(
-      future: playgroundState.getSnippetId(),
+      future: playgroundController.getSnippetId(),
       builder: (context, snapshot) {
         if (!snapshot.hasData) {
-          return const LoadingIndicator(size: kLgLoadingIndicatorSize);
+          return const LoadingIndicator();
         }
 
         return ExampleShareTabs(
diff --git a/playground/frontend/lib/modules/editor/components/share_dropdown/share_tabs_headers.dart b/playground/frontend/lib/modules/editor/components/share_dropdown/share_tabs_headers.dart
index 4d80b4309bd..1d528a0b228 100644
--- a/playground/frontend/lib/modules/editor/components/share_dropdown/share_tabs_headers.dart
+++ b/playground/frontend/lib/modules/editor/components/share_dropdown/share_tabs_headers.dart
@@ -17,7 +17,7 @@
  */
 
 import 'package:flutter/material.dart';
-import 'package:playground/pages/playground/states/playground_state.dart';
+import 'package:playground_components/playground_components.dart';
 import 'package:provider/provider.dart';
 import 'package:flutter_gen/gen_l10n/app_localizations.dart';
 
@@ -35,17 +35,19 @@ class ShareTabsHeaders extends StatelessWidget {
   Widget build(BuildContext context) {
     final appLocale = AppLocalizations.of(context)!;
 
-    return Consumer<PlaygroundState>(builder: (context, state, child) {
-      return SizedBox(
-        width: _width,
-        child: TabBar(
-          controller: tabController,
-          tabs: [
-            Text(appLocale.link),
-            Text(appLocale.embed),
-          ],
-        ),
-      );
-    });
+    return Consumer<PlaygroundController>(
+      builder: (context, controller, child) {
+        return SizedBox(
+          width: _width,
+          child: TabBar(
+            controller: tabController,
+            tabs: [
+              Text(appLocale.link),
+              Text(appLocale.embed),
+            ],
+          ),
+        );
+      },
+    );
   }
 }
diff --git a/playground/frontend/lib/modules/editor/repository/code_repository/code_client/grpc_code_client.dart b/playground/frontend/lib/modules/editor/repository/code_repository/code_client/grpc_code_client.dart
deleted file mode 100644
index 6ec86f1826b..00000000000
--- a/playground/frontend/lib/modules/editor/repository/code_repository/code_client/grpc_code_client.dart
+++ /dev/null
@@ -1,234 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License.  You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-import 'package:grpc/grpc_web.dart';
-import 'package:playground/api/iis_workaround_channel.dart';
-import 'package:playground/api/v1/api.pbgrpc.dart' as grpc;
-import 'package:playground/config.g.dart';
-import 'package:playground/modules/editor/parsers/run_options_parser.dart';
-import 'package:playground/modules/editor/repository/code_repository/code_client/check_status_response.dart';
-import 'package:playground/modules/editor/repository/code_repository/code_client/code_client.dart';
-import 'package:playground/modules/editor/repository/code_repository/code_client/output_response.dart';
-import 'package:playground/modules/editor/repository/code_repository/code_client/run_code_response.dart';
-import 'package:playground/modules/editor/repository/code_repository/run_code_error.dart';
-import 'package:playground/modules/editor/repository/code_repository/run_code_request.dart';
-import 'package:playground/modules/editor/repository/code_repository/run_code_result.dart';
-import 'package:playground/modules/sdk/models/sdk.dart';
-import 'package:playground/utils/replace_incorrect_symbols.dart';
-
-const kGeneralError = 'Failed to execute code';
-
-class GrpcCodeClient implements CodeClient {
-  late final grpc.PlaygroundServiceClient _defaultClient;
-
-  GrpcCodeClient() {
-    final channel = IisWorkaroundChannel.xhr(
-      Uri.parse(kApiClientURL),
-    );
-    _defaultClient = grpc.PlaygroundServiceClient(channel);
-  }
-
-  @override
-  Future<RunCodeResponse> runCode(RunCodeRequestWrapper request) {
-    return _runSafely(() => _createRunCodeClient(request.sdk)
-        .runCode(_toGrpcRequest(request))
-        .then((response) => RunCodeResponse(response.pipelineUuid)));
-  }
-
-  @override
-  Future<void> cancelExecution(String pipelineUuid) {
-    return _runSafely(() =>
-        _defaultClient.cancel(grpc.CancelRequest(pipelineUuid: pipelineUuid)));
-  }
-
-  @override
-  Future<CheckStatusResponse> checkStatus(
-    String pipelineUuid,
-    RunCodeRequestWrapper request,
-  ) {
-    return _runSafely(() => _defaultClient
-        .checkStatus(grpc.CheckStatusRequest(pipelineUuid: pipelineUuid))
-        .then(
-          (response) => CheckStatusResponse(_toClientStatus(response.status)),
-        ));
-  }
-
-  @override
-  Future<OutputResponse> getCompileOutput(
-    String pipelineUuid,
-    RunCodeRequestWrapper request,
-  ) {
-    return _runSafely(() => _defaultClient
-        .getCompileOutput(
-          grpc.GetCompileOutputRequest(pipelineUuid: pipelineUuid),
-        )
-        .then((response) => _toOutputResponse(response.output)));
-  }
-
-  @override
-  Future<OutputResponse> getRunOutput(
-    String pipelineUuid,
-    RunCodeRequestWrapper request,
-  ) {
-    return _runSafely(() => _defaultClient
-            .getRunOutput(grpc.GetRunOutputRequest(pipelineUuid: pipelineUuid))
-            .then((response) => _toOutputResponse(response.output))
-            .catchError((err) {
-          print(err);
-          return _toOutputResponse('');
-        }));
-  }
-
-  @override
-  Future<OutputResponse> getLogOutput(
-    String pipelineUuid,
-    RunCodeRequestWrapper request,
-  ) {
-    return _runSafely(() => _defaultClient
-            .getLogs(grpc.GetLogsRequest(pipelineUuid: pipelineUuid))
-            .then((response) => _toOutputResponse(response.output))
-            .catchError((err) {
-          print(err);
-          return _toOutputResponse('');
-        }));
-  }
-
-  @override
-  Future<OutputResponse> getRunErrorOutput(
-    String pipelineUuid,
-    RunCodeRequestWrapper request,
-  ) {
-    return _runSafely(() => _defaultClient
-        .getRunError(grpc.GetRunErrorRequest(pipelineUuid: pipelineUuid))
-        .then((response) => _toOutputResponse(response.output)));
-  }
-
-  @override
-  Future<OutputResponse> getValidationErrorOutput(
-    String pipelineUuid,
-    RunCodeRequestWrapper request,
-  ) {
-    return _runSafely(() => _defaultClient
-        .getValidationOutput(
-            grpc.GetValidationOutputRequest(pipelineUuid: pipelineUuid))
-        .then((response) => _toOutputResponse(response.output)));
-  }
-
-  @override
-  Future<OutputResponse> getPreparationErrorOutput(
-    String pipelineUuid,
-    RunCodeRequestWrapper request,
-  ) {
-    return _runSafely(() => _defaultClient
-        .getPreparationOutput(
-            grpc.GetPreparationOutputRequest(pipelineUuid: pipelineUuid))
-        .then((response) => _toOutputResponse(response.output)));
-  }
-
-  @override
-  Future<OutputResponse> getGraphOutput(
-    String pipelineUuid,
-    RunCodeRequestWrapper request,
-  ) {
-    return _runSafely(() => _defaultClient
-            .getGraph(grpc.GetGraphRequest(pipelineUuid: pipelineUuid))
-            .then((response) => OutputResponse(response.graph))
-            .catchError((err) {
-          print(err);
-          return _toOutputResponse('');
-        }));
-  }
-
-  Future<T> _runSafely<T>(Future<T> Function() invoke) async {
-    try {
-      return await invoke();
-    } on GrpcError catch (error) {
-      throw RunCodeError(error.message);
-    } on Exception catch (_) {
-      throw RunCodeError(null);
-    }
-  }
-
-  /// Run Code request should use different urls for each sdk
-  /// instead of the default one, because we need to code
-  /// sdk services for it
-  grpc.PlaygroundServiceClient _createRunCodeClient(SDK? sdk) {
-    String apiClientURL = kApiClientURL;
-    if (sdk != null) {
-      apiClientURL = sdk.getRoute;
-    }
-    IisWorkaroundChannel channel = IisWorkaroundChannel.xhr(
-      Uri.parse(apiClientURL),
-    );
-    return grpc.PlaygroundServiceClient(channel);
-  }
-
-  grpc.RunCodeRequest _toGrpcRequest(RunCodeRequestWrapper request) {
-    return grpc.RunCodeRequest()
-      ..code = request.code
-      ..sdk = _getGrpcSdk(request.sdk)
-      ..pipelineOptions = pipelineOptionsToString(request.pipelineOptions);
-  }
-
-  grpc.Sdk _getGrpcSdk(SDK sdk) {
-    switch (sdk) {
-      case SDK.java:
-        return grpc.Sdk.SDK_JAVA;
-      case SDK.go:
-        return grpc.Sdk.SDK_GO;
-      case SDK.python:
-        return grpc.Sdk.SDK_PYTHON;
-      case SDK.scio:
-        return grpc.Sdk.SDK_SCIO;
-    }
-  }
-
-  RunCodeStatus _toClientStatus(grpc.Status status) {
-    switch (status) {
-      case grpc.Status.STATUS_UNSPECIFIED:
-        return RunCodeStatus.unspecified;
-      case grpc.Status.STATUS_VALIDATING:
-      case grpc.Status.STATUS_PREPARING:
-        return RunCodeStatus.preparation;
-      case grpc.Status.STATUS_COMPILING:
-        return RunCodeStatus.compiling;
-      case grpc.Status.STATUS_EXECUTING:
-        return RunCodeStatus.executing;
-      case grpc.Status.STATUS_CANCELED:
-      case grpc.Status.STATUS_FINISHED:
-        return RunCodeStatus.finished;
-      case grpc.Status.STATUS_COMPILE_ERROR:
-        return RunCodeStatus.compileError;
-      case grpc.Status.STATUS_RUN_TIMEOUT:
-        return RunCodeStatus.timeout;
-      case grpc.Status.STATUS_RUN_ERROR:
-        return RunCodeStatus.runError;
-      case grpc.Status.STATUS_VALIDATION_ERROR:
-        return RunCodeStatus.validationError;
-      case grpc.Status.STATUS_PREPARATION_ERROR:
-        return RunCodeStatus.preparationError;
-      case grpc.Status.STATUS_ERROR:
-        return RunCodeStatus.unknownError;
-    }
-    return RunCodeStatus.unspecified;
-  }
-
-  OutputResponse _toOutputResponse(String response) {
-    return OutputResponse(replaceIncorrectSymbols(response));
-  }
-}
diff --git a/playground/frontend/lib/modules/examples/components/description_popover/description_popover.dart b/playground/frontend/lib/modules/examples/components/description_popover/description_popover.dart
index a438aac5bae..e368973cb64 100644
--- a/playground/frontend/lib/modules/examples/components/description_popover/description_popover.dart
+++ b/playground/frontend/lib/modules/examples/components/description_popover/description_popover.dart
@@ -22,13 +22,13 @@ import 'package:flutter_gen/gen_l10n/app_localizations.dart';
 import 'package:playground/constants/assets.dart';
 import 'package:playground/constants/font_weight.dart';
 import 'package:playground/constants/sizes.dart';
-import 'package:playground/modules/examples/models/example_model.dart';
+import 'package:playground_components/playground_components.dart';
 import 'package:url_launcher/url_launcher.dart';
 
 const kDescriptionWidth = 300.0;
 
 class DescriptionPopover extends StatelessWidget {
-  final ExampleModel example;
+  final ExampleBase example;
 
   const DescriptionPopover({Key? key, required this.example}) : super(key: key);
 
diff --git a/playground/frontend/lib/modules/examples/components/description_popover/description_popover_button.dart b/playground/frontend/lib/modules/examples/components/description_popover/description_popover_button.dart
index 5e935eb0328..8df888ed76f 100644
--- a/playground/frontend/lib/modules/examples/components/description_popover/description_popover_button.dart
+++ b/playground/frontend/lib/modules/examples/components/description_popover/description_popover_button.dart
@@ -19,14 +19,13 @@
 import 'package:aligned_dialog/aligned_dialog.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter_gen/gen_l10n/app_localizations.dart';
-import 'package:playground/config/theme.dart';
 import 'package:playground/constants/sizes.dart';
 import 'package:playground/modules/examples/components/description_popover/description_popover.dart';
-import 'package:playground/modules/examples/models/example_model.dart';
+import 'package:playground_components/playground_components.dart';
 
 class DescriptionPopoverButton extends StatelessWidget {
   final BuildContext? parentContext;
-  final ExampleModel example;
+  final ExampleBase example;
   final Alignment followerAnchor;
   final Alignment targetAnchor;
   final void Function()? onOpen;
@@ -52,7 +51,7 @@ class DescriptionPopoverButton extends StatelessWidget {
         splashRadius: kIconButtonSplashRadius,
         icon: Icon(
           Icons.info_outline_rounded,
-          color: ThemeColors.of(context).grey1Color,
+          color: Theme.of(context).extension<BeamThemeExtension>()?.iconColor,
         ),
         tooltip: appLocale.exampleDescription,
         onPressed: () {
@@ -69,7 +68,7 @@ class DescriptionPopoverButton extends StatelessWidget {
 
   void _showDescriptionPopover(
     BuildContext context,
-    ExampleModel example,
+    ExampleBase example,
     Alignment followerAnchor,
     Alignment targetAnchor,
   ) async {
diff --git a/playground/frontend/lib/modules/examples/components/example_list/category_expansion_panel.dart b/playground/frontend/lib/modules/examples/components/example_list/category_expansion_panel.dart
index c6dcf47271e..049db9e5968 100644
--- a/playground/frontend/lib/modules/examples/components/example_list/category_expansion_panel.dart
+++ b/playground/frontend/lib/modules/examples/components/example_list/category_expansion_panel.dart
@@ -22,12 +22,12 @@ import 'package:expansion_widget/expansion_widget.dart';
 import 'package:flutter/material.dart';
 import 'package:playground/constants/sizes.dart';
 import 'package:playground/modules/examples/components/example_list/expansion_panel_item.dart';
-import 'package:playground/modules/examples/models/example_model.dart';
+import 'package:playground_components/playground_components.dart';
 
 class CategoryExpansionPanel extends StatelessWidget {
   final String categoryName;
   final List examples;
-  final ExampleModel selectedExample;
+  final ExampleBase selectedExample;
   final AnimationController animationController;
   final OverlayEntry? dropdown;
 
diff --git a/playground/frontend/lib/modules/examples/components/example_list/example_item_actions.dart b/playground/frontend/lib/modules/examples/components/example_list/example_item_actions.dart
index 9cae77f6073..584b4c15731 100644
--- a/playground/frontend/lib/modules/examples/components/example_list/example_item_actions.dart
+++ b/playground/frontend/lib/modules/examples/components/example_list/example_item_actions.dart
@@ -18,14 +18,13 @@
 
 import 'package:flutter/material.dart';
 import 'package:playground/modules/examples/components/description_popover/description_popover_button.dart';
-import 'package:playground/modules/examples/models/example_model.dart';
+import 'package:playground/modules/examples/components/multifile_popover/multifile_popover_button.dart';
 import 'package:playground/modules/examples/models/popover_state.dart';
+import 'package:playground_components/playground_components.dart';
 import 'package:provider/provider.dart';
 
-import '../multifile_popover/multifile_popover_button.dart';
-
 class ExampleItemActions extends StatelessWidget {
-  final ExampleModel example;
+  final ExampleBase example;
   final BuildContext parentContext;
 
   const ExampleItemActions(
diff --git a/playground/frontend/lib/modules/examples/components/example_list/example_list.dart b/playground/frontend/lib/modules/examples/components/example_list/example_list.dart
index 6b175e2692a..1c9227d2f59 100644
--- a/playground/frontend/lib/modules/examples/components/example_list/example_list.dart
+++ b/playground/frontend/lib/modules/examples/components/example_list/example_list.dart
@@ -18,15 +18,15 @@
 
 import 'package:flutter/material.dart';
 import 'package:playground/modules/examples/components/examples_components.dart';
-import 'package:playground/modules/examples/models/example_model.dart';
 import 'package:playground/pages/playground/states/example_selector_state.dart';
+import 'package:playground_components/playground_components.dart';
 import 'package:provider/provider.dart';
 
 class ExampleList extends StatelessWidget {
   final ScrollController controller;
   final AnimationController animationController;
   final OverlayEntry? dropdown;
-  final ExampleModel selectedExample;
+  final ExampleBase selectedExample;
 
   const ExampleList({
     Key? key,
@@ -50,7 +50,7 @@ class ExampleList extends StatelessWidget {
               itemCount: state.categories.length,
               itemBuilder: (context, index) => CategoryExpansionPanel(
                 selectedExample: selectedExample,
-                categoryName: state.categories[index].name,
+                categoryName: state.categories[index].title,
                 examples: state.categories[index].examples,
                 animationController: animationController,
                 dropdown: dropdown,
diff --git a/playground/frontend/lib/modules/examples/components/example_list/expansion_panel_item.dart b/playground/frontend/lib/modules/examples/components/example_list/expansion_panel_item.dart
index f212f3c07e4..b035e35d2d9 100644
--- a/playground/frontend/lib/modules/examples/components/example_list/expansion_panel_item.dart
+++ b/playground/frontend/lib/modules/examples/components/example_list/expansion_panel_item.dart
@@ -20,14 +20,12 @@ import 'package:flutter/material.dart';
 import 'package:playground/constants/sizes.dart';
 import 'package:playground/modules/analytics/analytics_service.dart';
 import 'package:playground/modules/examples/components/example_list/example_item_actions.dart';
-import 'package:playground/modules/examples/models/example_model.dart';
-import 'package:playground/pages/playground/states/examples_state.dart';
-import 'package:playground/pages/playground/states/playground_state.dart';
+import 'package:playground_components/playground_components.dart';
 import 'package:provider/provider.dart';
 
 class ExpansionPanelItem extends StatelessWidget {
-  final ExampleModel example;
-  final ExampleModel selectedExample;
+  final ExampleBase example;
+  final ExampleBase selectedExample;
   final AnimationController animationController;
   final OverlayEntry? dropdown;
 
@@ -41,20 +39,21 @@ class ExpansionPanelItem extends StatelessWidget {
 
   @override
   Widget build(BuildContext context) {
-    return Consumer<PlaygroundState>(
-      builder: (context, playgroundState, child) => MouseRegion(
+    return Consumer<PlaygroundController>(
+      builder: (context, controller, child) => MouseRegion(
         cursor: SystemMouseCursors.click,
         child: GestureDetector(
           onTap: () async {
-            if (playgroundState.selectedExample != example) {
-              _closeDropdown(playgroundState.exampleState);
+            if (controller.selectedExample != example) {
+              _closeDropdown(controller.exampleCache);
               AnalyticsService.get(context).trackSelectExample(example);
               final exampleWithInfo =
-                  await playgroundState.exampleState.loadExampleInfo(example);
+                  await controller.exampleCache.loadExampleInfo(example);
               // TODO: setCurrentSdk = false when we do
               //  per-SDK output and run status.
               //  Now using true to reset the output and run status.
-              playgroundState.setExample(exampleWithInfo, setCurrentSdk: true);
+              //  https://github.com/apache/beam/issues/23248
+              controller.setExample(exampleWithInfo, setCurrentSdk: true);
             }
           },
           child: Container(
@@ -83,9 +82,9 @@ class ExpansionPanelItem extends StatelessWidget {
     );
   }
 
-  void _closeDropdown(ExampleState exampleState) {
+  void _closeDropdown(ExampleCache exampleCache) {
     animationController.reverse();
     dropdown?.remove();
-    exampleState.changeSelectorVisibility();
+    exampleCache.changeSelectorVisibility();
   }
 }
diff --git a/playground/frontend/lib/modules/examples/components/filter/category_bubble.dart b/playground/frontend/lib/modules/examples/components/filter/category_bubble.dart
index aabe00735ee..849f19db877 100644
--- a/playground/frontend/lib/modules/examples/components/filter/category_bubble.dart
+++ b/playground/frontend/lib/modules/examples/components/filter/category_bubble.dart
@@ -17,10 +17,8 @@
  */
 
 import 'package:flutter/material.dart';
-import 'package:playground/config/theme.dart';
-import 'package:playground/constants/sizes.dart';
-import 'package:playground/modules/examples/models/example_model.dart';
 import 'package:playground/pages/playground/states/example_selector_state.dart';
+import 'package:playground_components/playground_components.dart';
 import 'package:provider/provider.dart';
 
 class CategoryBubble extends StatelessWidget {
@@ -35,46 +33,21 @@ class CategoryBubble extends StatelessWidget {
 
   @override
   Widget build(BuildContext context) {
-    return MouseRegion(
-      cursor: SystemMouseCursors.click,
-      child: Padding(
-        padding: const EdgeInsets.only(right: kMdSpacing),
-        child: Consumer<ExampleSelectorState>(
-          builder: (context, state, child) {
-            final isSelected = type == state.selectedFilterType;
+    return Consumer<ExampleSelectorState>(
+      builder: (context, state, child) {
+        final isSelected = type == state.selectedFilterType;
 
-            return GestureDetector(
-              onTap: () {
-                if (!isSelected) {
-                  state.setSelectedFilterType(type);
-                  state.sortCategories();
-                }
-              },
-              child: Container(
-                height: kContainerHeight,
-                padding: const EdgeInsets.symmetric(horizontal: kXlSpacing),
-                decoration: BoxDecoration(
-                  color: isSelected
-                      ? ThemeColors.of(context).primary
-                      : ThemeColors.of(context).lightGreyColor,
-                  borderRadius: BorderRadius.circular(kXlBorderRadius),
-                ),
-                child: Center(
-                  child: Text(
-                    name,
-                    style: TextStyle(
-                      color: isSelected
-                          ? ThemeColors.of(context).primaryBackgroundTextColor
-                          : ThemeColors.of(context)
-                              .lightGreyBackgroundTextColor,
-                    ),
-                  ),
-                ),
-              ),
-            );
+        return BubbleWidget(
+          isSelected: isSelected,
+          title: name,
+          onTap: () {
+            if (!isSelected) {
+              state.setSelectedFilterType(type);
+              state.sortCategories();
+            }
           },
-        ),
-      ),
+        );
+      },
     );
   }
 }
diff --git a/playground/frontend/lib/modules/examples/components/filter/type_filter.dart b/playground/frontend/lib/modules/examples/components/filter/type_filter.dart
index c56f6fcde41..a221999f3ce 100644
--- a/playground/frontend/lib/modules/examples/components/filter/type_filter.dart
+++ b/playground/frontend/lib/modules/examples/components/filter/type_filter.dart
@@ -20,7 +20,7 @@ import 'package:flutter/material.dart';
 import 'package:flutter_gen/gen_l10n/app_localizations.dart';
 import 'package:playground/constants/sizes.dart';
 import 'package:playground/modules/examples/components/examples_components.dart';
-import 'package:playground/modules/examples/models/example_model.dart';
+import 'package:playground_components/playground_components.dart';
 
 class TypeFilter extends StatelessWidget {
   const TypeFilter({Key? key}) : super(key: key);
diff --git a/playground/frontend/lib/modules/examples/components/multifile_popover/multifile_popover.dart b/playground/frontend/lib/modules/examples/components/multifile_popover/multifile_popover.dart
index 6d90bc7bfd5..c4fb93fa470 100644
--- a/playground/frontend/lib/modules/examples/components/multifile_popover/multifile_popover.dart
+++ b/playground/frontend/lib/modules/examples/components/multifile_popover/multifile_popover.dart
@@ -22,13 +22,13 @@ import 'package:flutter_gen/gen_l10n/app_localizations.dart';
 import 'package:playground/constants/assets.dart';
 import 'package:playground/constants/font_weight.dart';
 import 'package:playground/constants/sizes.dart';
-import 'package:playground/modules/examples/models/example_model.dart';
+import 'package:playground_components/playground_components.dart';
 import 'package:url_launcher/url_launcher.dart';
 
 const kMultifileWidth = 300.0;
 
 class MultifilePopover extends StatelessWidget {
-  final ExampleModel example;
+  final ExampleBase example;
 
   const MultifilePopover({Key? key, required this.example}) : super(key: key);
 
diff --git a/playground/frontend/lib/modules/examples/components/multifile_popover/multifile_popover_button.dart b/playground/frontend/lib/modules/examples/components/multifile_popover/multifile_popover_button.dart
index 069830c65c7..d530aa645eb 100644
--- a/playground/frontend/lib/modules/examples/components/multifile_popover/multifile_popover_button.dart
+++ b/playground/frontend/lib/modules/examples/components/multifile_popover/multifile_popover_button.dart
@@ -23,11 +23,11 @@ import 'package:flutter_gen/gen_l10n/app_localizations.dart';
 import 'package:playground/constants/assets.dart';
 import 'package:playground/constants/sizes.dart';
 import 'package:playground/modules/examples/components/multifile_popover/multifile_popover.dart';
-import 'package:playground/modules/examples/models/example_model.dart';
+import 'package:playground_components/playground_components.dart';
 
 class MultifilePopoverButton extends StatelessWidget {
   final BuildContext? parentContext;
-  final ExampleModel example;
+  final ExampleBase example;
   final Alignment followerAnchor;
   final Alignment targetAnchor;
   final void Function()? onOpen;
@@ -67,7 +67,7 @@ class MultifilePopoverButton extends StatelessWidget {
 
   void _showMultifilePopover(
     BuildContext context,
-    ExampleModel example,
+    ExampleBase example,
     Alignment followerAnchor,
     Alignment targetAnchor,
   ) async {
diff --git a/playground/frontend/lib/modules/examples/components/search_field/search_field.dart b/playground/frontend/lib/modules/examples/components/search_field/search_field.dart
index 80939ac1555..2095dfac1d2 100644
--- a/playground/frontend/lib/modules/examples/components/search_field/search_field.dart
+++ b/playground/frontend/lib/modules/examples/components/search_field/search_field.dart
@@ -18,9 +18,9 @@
 
 import 'package:flutter/material.dart';
 import 'package:flutter_gen/gen_l10n/app_localizations.dart';
-import 'package:playground/config/theme.dart';
 import 'package:playground/constants/sizes.dart';
 import 'package:playground/pages/playground/states/example_selector_state.dart';
+import 'package:playground_components/playground_components.dart';
 import 'package:provider/provider.dart';
 
 const double kContainerWidth = 376.0;
@@ -34,8 +34,11 @@ class SearchField extends StatelessWidget {
 
   @override
   Widget build(BuildContext context) {
+    final borderColor =
+        Theme.of(context).extension<BeamThemeExtension>()!.borderColor;
+
     final OutlineInputBorder border = OutlineInputBorder(
-      borderSide: BorderSide(color: ThemeColors.of(context).lightGreyColor),
+      borderSide: BorderSide(color: borderColor),
       borderRadius: BorderRadius.circular(kMdBorderRadius),
     );
 
@@ -61,7 +64,7 @@ class SearchField extends StatelessWidget {
                 ),
                 child: Icon(
                   Icons.search,
-                  color: ThemeColors.of(context).lightGreyColor,
+                  color: borderColor,
                   size: kIconSizeMd,
                 ),
               ),
@@ -72,7 +75,7 @@ class SearchField extends StatelessWidget {
               hintText: AppLocalizations.of(context)!.search,
               contentPadding: const EdgeInsets.only(left: kLgSpacing),
             ),
-            cursorColor: ThemeColors.of(context).lightGreyColor,
+            cursorColor: borderColor,
             cursorWidth: kCursorSize,
             textAlignVertical: TextAlignVertical.center,
             onFieldSubmitted: (String filterText) =>
diff --git a/playground/frontend/lib/modules/examples/example_selector.dart b/playground/frontend/lib/modules/examples/example_selector.dart
index f65c45bfacf..c08fdb3cea2 100644
--- a/playground/frontend/lib/modules/examples/example_selector.dart
+++ b/playground/frontend/lib/modules/examples/example_selector.dart
@@ -18,18 +18,14 @@
 
 import 'package:flutter/material.dart';
 import 'package:flutter_gen/gen_l10n/app_localizations.dart';
-import 'package:playground/components/horizontal_divider/horizontal_divider.dart';
-import 'package:playground/components/loading_indicator/loading_indicator.dart';
-import 'package:playground/config/theme.dart';
 import 'package:playground/constants/links.dart';
 import 'package:playground/constants/sizes.dart';
 import 'package:playground/modules/examples/components/examples_components.dart';
 import 'package:playground/modules/examples/components/outside_click_handler.dart';
 import 'package:playground/modules/examples/models/popover_state.dart';
 import 'package:playground/pages/playground/states/example_selector_state.dart';
-import 'package:playground/pages/playground/states/examples_state.dart';
-import 'package:playground/pages/playground/states/playground_state.dart';
 import 'package:playground/utils/dropdown_utils.dart';
+import 'package:playground_components/playground_components.dart';
 import 'package:provider/provider.dart';
 import 'package:url_launcher/url_launcher.dart';
 
@@ -90,10 +86,10 @@ class _ExampleSelectorState extends State<ExampleSelector>
     return Container(
       height: kContainerHeight,
       decoration: BoxDecoration(
-        color: ThemeColors.of(context).dropdownButton,
+        color: Theme.of(context).dividerColor,
         borderRadius: BorderRadius.circular(kSmBorderRadius),
       ),
-      child: Consumer<PlaygroundState>(
+      child: Consumer<PlaygroundController>(
         builder: (context, state, child) => TextButton(
           key: selectorKey,
           onPressed: () {
@@ -111,7 +107,7 @@ class _ExampleSelectorState extends State<ExampleSelector>
             alignment: WrapAlignment.center,
             crossAxisAlignment: WrapCrossAlignment.center,
             children: [
-              Consumer<PlaygroundState>(
+              Consumer<PlaygroundController>(
                 builder: (context, state, child) => Text(state.examplesTitle),
               ),
               const Icon(Icons.keyboard_arrow_down),
@@ -130,12 +126,12 @@ class _ExampleSelectorState extends State<ExampleSelector>
         return ChangeNotifierProvider<PopoverState>(
           create: (context) => PopoverState(false),
           builder: (context, state) {
-            return Consumer<PlaygroundState>(
-              builder: (context, playgroundState, child) => Stack(
+            return Consumer<PlaygroundController>(
+              builder: (context, playgroundController, child) => Stack(
                 children: [
                   OutsideClickHandler(
                     onTap: () {
-                      _closeDropdown(playgroundState.exampleState);
+                      _closeDropdown(playgroundController.exampleCache);
                       // handle description dialogs
                       Navigator.of(context, rootNavigator: true)
                           .popUntil((route) {
@@ -145,9 +141,9 @@ class _ExampleSelectorState extends State<ExampleSelector>
                   ),
                   ChangeNotifierProvider(
                     create: (context) => ExampleSelectorState(
-                      playgroundState,
-                      playgroundState.exampleState
-                          .getCategories(playgroundState.sdk),
+                      playgroundController,
+                      playgroundController.exampleCache
+                          .getCategories(playgroundController.sdk),
                     ),
                     builder: (context, _) => Positioned(
                       left: dropdownOffset.dx,
@@ -166,7 +162,7 @@ class _ExampleSelectorState extends State<ExampleSelector>
                             ),
                             child: _buildDropdownContent(
                               context,
-                              playgroundState,
+                              playgroundController,
                             ),
                           ),
                         ),
@@ -184,13 +180,11 @@ class _ExampleSelectorState extends State<ExampleSelector>
 
   Widget _buildDropdownContent(
     BuildContext context,
-    PlaygroundState playgroundState,
+    PlaygroundController playgroundController,
   ) {
-    if (playgroundState.exampleState.sdkCategories == null ||
-        playgroundState.selectedExample == null) {
-      return const LoadingIndicator(
-        size: kMdLoadingIndicatorSize,
-      );
+    if (playgroundController.exampleCache.categoryListsBySdk.isEmpty ||
+        playgroundController.selectedExample == null) {
+      return const LoadingIndicator();
     }
 
     return Column(
@@ -199,11 +193,11 @@ class _ExampleSelectorState extends State<ExampleSelector>
         const TypeFilter(),
         ExampleList(
           controller: scrollController,
-          selectedExample: playgroundState.selectedExample!,
+          selectedExample: playgroundController.selectedExample!,
           animationController: animationController,
           dropdown: examplesDropdown,
         ),
-        const HorizontalDivider(indent: kLgSpacing),
+        const BeamDivider(),
         SizedBox(
           width: double.infinity,
           child: TextButton(
@@ -213,7 +207,7 @@ class _ExampleSelectorState extends State<ExampleSelector>
                 alignment: Alignment.centerLeft,
                 child: Text(
                   AppLocalizations.of(context)!.addExample,
-                  style: TextStyle(color: ThemeColors.of(context).primary),
+                  style: TextStyle(color: Theme.of(context).primaryColor),
                 ),
               ),
             ),
@@ -224,9 +218,9 @@ class _ExampleSelectorState extends State<ExampleSelector>
     );
   }
 
-  void _closeDropdown(ExampleState exampleState) {
+  void _closeDropdown(ExampleCache exampleCache) {
     animationController.reverse();
     examplesDropdown?.remove();
-    exampleState.changeSelectorVisibility();
+    exampleCache.changeSelectorVisibility();
   }
 }
diff --git a/playground/frontend/lib/modules/examples/models/example_loading_descriptors/catalog_default_example_loading_descriptor.dart b/playground/frontend/lib/modules/examples/models/example_loading_descriptors/catalog_default_example_loading_descriptor.dart
deleted file mode 100644
index 5841e0e294c..00000000000
--- a/playground/frontend/lib/modules/examples/models/example_loading_descriptors/catalog_default_example_loading_descriptor.dart
+++ /dev/null
@@ -1,44 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License.  You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-import 'package:playground/modules/sdk/models/sdk.dart';
-import 'package:playground/modules/examples/models/example_loading_descriptors/example_loading_descriptor.dart';
-import 'package:playground/modules/examples/models/example_origin.dart';
-
-class CatalogDefaultExampleLoadingDescriptor extends ExampleLoadingDescriptor {
-  final SDK sdk;
-
-  const CatalogDefaultExampleLoadingDescriptor({
-    required this.sdk,
-  });
-
-  @override
-  ExampleOrigin get origin => ExampleOrigin.catalogDefault;
-
-  @override
-  int get hashCode => sdk.hashCode;
-
-  @override
-  bool operator ==(Object other) {
-    return other is CatalogDefaultExampleLoadingDescriptor && sdk == other.sdk;
-  }
-
-  // Only ContentExampleLoadingDescriptor is serialized now.
-  @override
-  Map<String, dynamic> toJson() => throw UnimplementedError();
-}
diff --git a/playground/frontend/lib/modules/examples/models/example_loading_descriptors/examples_loading_descriptor_factory.dart b/playground/frontend/lib/modules/examples/models/example_loading_descriptors/examples_loading_descriptor_factory.dart
index ca9199f1285..7b3854d6f31 100644
--- a/playground/frontend/lib/modules/examples/models/example_loading_descriptors/examples_loading_descriptor_factory.dart
+++ b/playground/frontend/lib/modules/examples/models/example_loading_descriptors/examples_loading_descriptor_factory.dart
@@ -19,18 +19,11 @@
 import 'dart:convert';
 
 import 'package:playground/constants/params.dart';
-import 'package:playground/modules/examples/models/example_loading_descriptors/catalog_default_example_loading_descriptor.dart';
-import 'package:playground/modules/examples/models/example_loading_descriptors/content_example_loading_descriptor.dart';
-import 'package:playground/modules/examples/models/example_loading_descriptors/empty_example_loading_descriptor.dart';
-import 'package:playground/modules/examples/models/example_loading_descriptors/example_loading_descriptor.dart';
-import 'package:playground/modules/examples/models/example_loading_descriptors/examples_loading_descriptor.dart';
-import 'package:playground/modules/examples/models/example_loading_descriptors/standard_example_loading_descriptor.dart';
-import 'package:playground/modules/examples/models/example_loading_descriptors/user_shared_example_loading_descriptor.dart';
 import 'package:playground/modules/examples/models/example_token_type.dart';
-import 'package:playground/modules/sdk/models/sdk.dart';
+import 'package:playground_components/playground_components.dart';
 
 class ExamplesLoadingDescriptorFactory {
-  static const _defaultSdk = SDK.java;
+  static const _defaultSdk = Sdk.java;
 
   static ExamplesLoadingDescriptor fromUriParts({
     required String path,
@@ -61,7 +54,7 @@ class ExamplesLoadingDescriptorFactory {
         return null;
       }
 
-      final sdk = SDK.tryParse(params[kSdkParam]);
+      final sdk = Sdk.tryParse(params[kSdkParam]);
 
       return ExamplesLoadingDescriptor(
         descriptors: _parseMultipleInstantExamples(list, sdk),
@@ -75,7 +68,7 @@ class ExamplesLoadingDescriptorFactory {
 
   static List<ExampleLoadingDescriptor> _parseMultipleInstantExamples(
     List<dynamic> list,
-    SDK? sdk,
+    Sdk? sdk,
   ) {
     final result = <ExampleLoadingDescriptor>[];
 
@@ -132,7 +125,7 @@ class ExamplesLoadingDescriptorFactory {
       return null;
     }
 
-    final sdk = SDK.tryParse(params[kSdkParam]) ?? _defaultSdk;
+    final sdk = Sdk.tryParse(params[kSdkParam]) ?? _defaultSdk;
 
     return ExamplesLoadingDescriptor(
       descriptors: [
@@ -161,14 +154,14 @@ class ExamplesLoadingDescriptorFactory {
     return ExamplesLoadingDescriptor(
       descriptors: [
         EmptyExampleLoadingDescriptor(
-          sdk: SDK.tryParse(params[kSdkParam]) ?? _defaultSdk,
+          sdk: Sdk.tryParse(params[kSdkParam]) ?? _defaultSdk,
         ),
       ],
       lazyLoadDescriptors: _emptyLazyLoadDescriptors,
     );
   }
 
-  static Map<SDK, List<ExampleLoadingDescriptor>> _getLazyLoadDescriptors() {
+  static Map<Sdk, List<ExampleLoadingDescriptor>> _getLazyLoadDescriptors() {
     if (isEmbedded()) {
       return _emptyLazyLoadDescriptors;
     }
@@ -176,18 +169,18 @@ class ExamplesLoadingDescriptorFactory {
     return _defaultLazyLoadDescriptors;
   }
 
-  static Map<SDK, List<ExampleLoadingDescriptor>>
+  static Map<Sdk, List<ExampleLoadingDescriptor>>
       get _emptyLazyLoadDescriptors {
     return {
-      for (final sdk in SDK.values)
+      for (final sdk in Sdk.known)
         sdk: [EmptyExampleLoadingDescriptor(sdk: sdk)]
     };
   }
 
-  static Map<SDK, List<ExampleLoadingDescriptor>>
+  static Map<Sdk, List<ExampleLoadingDescriptor>>
       get _defaultLazyLoadDescriptors {
     return {
-      for (final sdk in SDK.values)
+      for (final sdk in Sdk.known)
         sdk: [CatalogDefaultExampleLoadingDescriptor(sdk: sdk)]
     };
   }
diff --git a/playground/frontend/lib/modules/examples/models/example_loading_descriptors/standard_example_loading_descriptor.dart b/playground/frontend/lib/modules/examples/models/example_loading_descriptors/standard_example_loading_descriptor.dart
deleted file mode 100644
index a5adbff51ac..00000000000
--- a/playground/frontend/lib/modules/examples/models/example_loading_descriptors/standard_example_loading_descriptor.dart
+++ /dev/null
@@ -1,46 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License.  You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-import 'package:playground/modules/examples/models/example_loading_descriptors/example_loading_descriptor.dart';
-import 'package:playground/modules/examples/models/example_origin.dart';
-
-class StandardExampleLoadingDescriptor extends ExampleLoadingDescriptor {
-  final String path;
-
-  const StandardExampleLoadingDescriptor({
-    required this.path,
-  });
-
-  @override
-  ExampleOrigin get origin => ExampleOrigin.standard;
-
-  @override
-  String toString() => '$origin-$path';
-
-  @override
-  int get hashCode => path.hashCode;
-
-  @override
-  bool operator ==(Object other) {
-    return other is StandardExampleLoadingDescriptor && path == other.path;
-  }
-
-  // Only ContentExampleLoadingDescriptor is serialized now.
-  @override
-  Map<String, dynamic> toJson() => throw UnimplementedError();
-}
diff --git a/playground/frontend/lib/modules/examples/models/example_loading_descriptors/user_shared_example_loading_descriptor.dart b/playground/frontend/lib/modules/examples/models/example_loading_descriptors/user_shared_example_loading_descriptor.dart
deleted file mode 100644
index 26e58a8453f..00000000000
--- a/playground/frontend/lib/modules/examples/models/example_loading_descriptors/user_shared_example_loading_descriptor.dart
+++ /dev/null
@@ -1,47 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License.  You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-import 'package:playground/modules/examples/models/example_loading_descriptors/example_loading_descriptor.dart';
-import 'package:playground/modules/examples/models/example_origin.dart';
-
-class UserSharedExampleLoadingDescriptor extends ExampleLoadingDescriptor {
-  final String snippetId;
-
-  const UserSharedExampleLoadingDescriptor({
-    required this.snippetId,
-  });
-
-  @override
-  ExampleOrigin get origin => ExampleOrigin.userShared;
-
-  @override
-  String toString() => '$origin-$snippetId';
-
-  @override
-  int get hashCode => snippetId.hashCode;
-
-  @override
-  bool operator ==(Object other) {
-    return other is UserSharedExampleLoadingDescriptor &&
-        snippetId == other.snippetId;
-  }
-
-  // Only ContentExampleLoadingDescriptor is serialized now.
-  @override
-  Map<String, dynamic> toJson() => throw UnimplementedError();
-}
diff --git a/playground/frontend/lib/modules/examples/models/example_token_type.dart b/playground/frontend/lib/modules/examples/models/example_token_type.dart
index 789b1c3a179..48c7c580635 100644
--- a/playground/frontend/lib/modules/examples/models/example_token_type.dart
+++ b/playground/frontend/lib/modules/examples/models/example_token_type.dart
@@ -16,7 +16,7 @@
  * limitations under the License.
  */
 
-import 'package:playground/modules/sdk/models/sdk.dart';
+import 'package:playground_components/playground_components.dart';
 
 enum ExampleTokenType {
   standard,
@@ -24,7 +24,7 @@ enum ExampleTokenType {
   ;
 
   static ExampleTokenType fromToken(String token) {
-    final sdk = SDK.tryParseExamplePath(token);
+    final sdk = Sdk.tryParseExamplePath(token);
     if (sdk != null) {
       return standard;
     }
diff --git a/playground/frontend/lib/modules/examples/repositories/example_client/example_client.dart b/playground/frontend/lib/modules/examples/repositories/example_client/example_client.dart
deleted file mode 100644
index 039a567c5ef..00000000000
--- a/playground/frontend/lib/modules/examples/repositories/example_client/example_client.dart
+++ /dev/null
@@ -1,66 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License.  You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-import 'package:playground/modules/editor/repository/code_repository/code_client/output_response.dart';
-import 'package:playground/modules/examples/repositories/models/get_snippet_request.dart';
-import 'package:playground/modules/examples/repositories/models/get_snippet_response.dart';
-import 'package:playground/modules/examples/repositories/models/get_example_code_response.dart';
-import 'package:playground/modules/examples/repositories/models/get_example_request.dart';
-import 'package:playground/modules/examples/repositories/models/get_example_response.dart';
-import 'package:playground/modules/examples/repositories/models/get_list_of_examples_request.dart';
-import 'package:playground/modules/examples/repositories/models/get_list_of_examples_response.dart';
-import 'package:playground/modules/examples/repositories/models/save_snippet_request.dart';
-import 'package:playground/modules/examples/repositories/models/save_snippet_response.dart';
-
-abstract class ExampleClient {
-  Future<GetListOfExampleResponse> getListOfExamples(
-    GetListOfExamplesRequestWrapper request,
-  );
-
-  Future<GetExampleCodeResponse> getExampleSource(
-    GetExampleRequestWrapper request,
-  );
-
-  Future<GetExampleResponse> getDefaultExample(
-    GetExampleRequestWrapper request,
-  );
-
-  Future<GetExampleResponse> getExample(
-    GetExampleRequestWrapper request,
-  );
-
-  Future<OutputResponse> getExampleOutput(
-    GetExampleRequestWrapper request,
-  );
-
-  Future<OutputResponse> getExampleLogs(
-    GetExampleRequestWrapper request,
-  );
-
-  Future<OutputResponse> getExampleGraph(
-    GetExampleRequestWrapper request,
-  );
-
-  Future<GetSnippetResponse> getSnippet(
-    GetSnippetRequestWrapper request,
-  );
-
-  Future<SaveSnippetResponse> saveSnippet(
-    SaveSnippetRequestWrapper request,
-  );
-}
diff --git a/playground/frontend/lib/modules/examples/repositories/example_client/grpc_example_client.dart b/playground/frontend/lib/modules/examples/repositories/example_client/grpc_example_client.dart
deleted file mode 100644
index f4a11be58aa..00000000000
--- a/playground/frontend/lib/modules/examples/repositories/example_client/grpc_example_client.dart
+++ /dev/null
@@ -1,366 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License.  You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-import 'package:grpc/grpc_web.dart';
-import 'package:playground/api/iis_workaround_channel.dart';
-import 'package:playground/api/v1/api.pbgrpc.dart' as grpc;
-import 'package:playground/config.g.dart';
-import 'package:playground/modules/editor/repository/code_repository/code_client/output_response.dart';
-import 'package:playground/modules/examples/models/category_model.dart';
-import 'package:playground/modules/examples/models/example_model.dart';
-import 'package:playground/modules/examples/repositories/example_client/example_client.dart';
-import 'package:playground/modules/examples/repositories/models/get_example_code_response.dart';
-import 'package:playground/modules/examples/repositories/models/get_example_request.dart';
-import 'package:playground/modules/examples/repositories/models/get_example_response.dart';
-import 'package:playground/modules/examples/repositories/models/get_list_of_examples_request.dart';
-import 'package:playground/modules/examples/repositories/models/get_list_of_examples_response.dart';
-import 'package:playground/modules/examples/repositories/models/get_snippet_request.dart';
-import 'package:playground/modules/examples/repositories/models/get_snippet_response.dart';
-import 'package:playground/modules/examples/repositories/models/save_snippet_request.dart';
-import 'package:playground/modules/examples/repositories/models/save_snippet_response.dart';
-import 'package:playground/modules/examples/repositories/models/shared_file_model.dart';
-import 'package:playground/modules/sdk/models/sdk.dart';
-import 'package:playground/utils/replace_incorrect_symbols.dart';
-
-class GrpcExampleClient implements ExampleClient {
-  late final grpc.PlaygroundServiceClient _defaultClient;
-
-  GrpcExampleClient() {
-    final channel = IisWorkaroundChannel.xhr(
-      Uri.parse(kApiClientURL),
-    );
-    _defaultClient = grpc.PlaygroundServiceClient(channel);
-  }
-
-  @override
-  Future<GetListOfExampleResponse> getListOfExamples(
-    GetListOfExamplesRequestWrapper request,
-  ) {
-    return _runSafely(
-      () => _defaultClient
-          .getPrecompiledObjects(
-              _getListOfExamplesRequestToGrpcRequest(request))
-          .then((response) => GetListOfExampleResponse(
-              _toClientCategories(response.sdkCategories))),
-    );
-  }
-
-  @override
-  Future<GetExampleResponse> getDefaultExample(
-    GetExampleRequestWrapper request,
-  ) {
-    return _runSafely(
-      () => _defaultClient
-          .getDefaultPrecompiledObject(
-              _getDefaultExampleRequestToGrpcRequest(request))
-          .then(
-            (response) => GetExampleResponse(
-              _toExampleModel(
-                request.sdk,
-                response.precompiledObject,
-              ),
-            ),
-          ),
-    );
-  }
-
-  @override
-  Future<GetExampleResponse> getExample(
-    GetExampleRequestWrapper request,
-  ) {
-    return _runSafely(
-      () => _defaultClient
-          .getPrecompiledObject(
-              grpc.GetPrecompiledObjectRequest()..cloudPath = request.path)
-          .then(
-            (response) => GetExampleResponse(
-              _toExampleModel(
-                request.sdk,
-                response.precompiledObject,
-              ),
-            ),
-          ),
-    );
-  }
-
-  @override
-  Future<GetExampleCodeResponse> getExampleSource(
-      GetExampleRequestWrapper request) {
-    return _runSafely(
-      () => _defaultClient
-          .getPrecompiledObjectCode(
-              _getExampleCodeRequestToGrpcRequest(request))
-          .then((response) =>
-              GetExampleCodeResponse(replaceIncorrectSymbols(response.code))),
-    );
-  }
-
-  @override
-  Future<OutputResponse> getExampleOutput(GetExampleRequestWrapper request) {
-    return _runSafely(
-      () => _defaultClient
-          .getPrecompiledObjectOutput(
-              _getExampleOutputRequestToGrpcRequest(request))
-          .then((response) =>
-              OutputResponse(replaceIncorrectSymbols(response.output)))
-          .catchError((err) {
-        print(err);
-        return OutputResponse('');
-      }),
-    );
-  }
-
-  @override
-  Future<OutputResponse> getExampleLogs(GetExampleRequestWrapper request) {
-    return _runSafely(
-      () => _defaultClient
-          .getPrecompiledObjectLogs(_getExampleLogRequestToGrpcRequest(request))
-          .then((response) =>
-              OutputResponse(replaceIncorrectSymbols(response.output)))
-          .catchError((err) {
-        print(err);
-        return OutputResponse('');
-      }),
-    );
-  }
-
-  @override
-  Future<OutputResponse> getExampleGraph(GetExampleRequestWrapper request) {
-    return _runSafely(
-      () => _defaultClient
-          .getPrecompiledObjectGraph(
-              _getExampleGraphRequestToGrpcRequest(request))
-          .then((response) => OutputResponse(response.graph))
-          .catchError((err) {
-        print(err);
-        return OutputResponse('');
-      }),
-    );
-  }
-
-  @override
-  Future<GetSnippetResponse> getSnippet(
-    GetSnippetRequestWrapper request,
-  ) {
-    return _runSafely(
-      () => _defaultClient
-          .getSnippet(_getSnippetRequestToGrpcRequest(request))
-          .then(
-            (response) => GetSnippetResponse(
-              files: _convertToSharedFileList(response.files),
-              sdk: _getAppSdk(response.sdk),
-              pipelineOptions: response.pipelineOptions,
-            ),
-          ),
-    );
-  }
-
-  @override
-  Future<SaveSnippetResponse> saveSnippet(
-    SaveSnippetRequestWrapper request,
-  ) {
-    return _runSafely(
-      () => _defaultClient
-          .saveSnippet(_saveSnippetRequestToGrpcRequest(request))
-          .then(
-            (response) => SaveSnippetResponse(
-              id: response.id,
-            ),
-          ),
-    );
-  }
-
-  Future<T> _runSafely<T>(Future<T> Function() invoke) {
-    try {
-      return invoke();
-    } on GrpcError catch (error) {
-      throw Exception(error.message);
-    }
-  }
-
-  grpc.GetPrecompiledObjectsRequest _getListOfExamplesRequestToGrpcRequest(
-    GetListOfExamplesRequestWrapper request,
-  ) {
-    return grpc.GetPrecompiledObjectsRequest()
-      ..category = request.category ?? ''
-      ..sdk = request.sdk == null
-          ? grpc.Sdk.SDK_UNSPECIFIED
-          : _getGrpcSdk(request.sdk!);
-  }
-
-  grpc.GetDefaultPrecompiledObjectRequest
-      _getDefaultExampleRequestToGrpcRequest(
-    GetExampleRequestWrapper request,
-  ) {
-    return grpc.GetDefaultPrecompiledObjectRequest()
-      ..sdk = _getGrpcSdk(request.sdk);
-  }
-
-  grpc.GetPrecompiledObjectCodeRequest _getExampleCodeRequestToGrpcRequest(
-    GetExampleRequestWrapper request,
-  ) {
-    return grpc.GetPrecompiledObjectCodeRequest()..cloudPath = request.path;
-  }
-
-  grpc.GetPrecompiledObjectOutputRequest _getExampleOutputRequestToGrpcRequest(
-    GetExampleRequestWrapper request,
-  ) {
-    return grpc.GetPrecompiledObjectOutputRequest()..cloudPath = request.path;
-  }
-
-  grpc.GetPrecompiledObjectLogsRequest _getExampleLogRequestToGrpcRequest(
-    GetExampleRequestWrapper request,
-  ) {
-    return grpc.GetPrecompiledObjectLogsRequest()..cloudPath = request.path;
-  }
-
-  grpc.GetPrecompiledObjectGraphRequest _getExampleGraphRequestToGrpcRequest(
-    GetExampleRequestWrapper request,
-  ) {
-    return grpc.GetPrecompiledObjectGraphRequest()..cloudPath = request.path;
-  }
-
-  grpc.GetSnippetRequest _getSnippetRequestToGrpcRequest(
-    GetSnippetRequestWrapper request,
-  ) {
-    return grpc.GetSnippetRequest()..id = request.id;
-  }
-
-  grpc.SaveSnippetRequest _saveSnippetRequestToGrpcRequest(
-    SaveSnippetRequestWrapper request,
-  ) {
-    return grpc.SaveSnippetRequest()
-      ..sdk = _getGrpcSdk(request.sdk)
-      ..pipelineOptions = request.pipelineOptions
-      ..files.addAll(_convertToSnippetFileList(request.files));
-  }
-
-  grpc.Sdk _getGrpcSdk(SDK sdk) {
-    switch (sdk) {
-      case SDK.java:
-        return grpc.Sdk.SDK_JAVA;
-      case SDK.go:
-        return grpc.Sdk.SDK_GO;
-      case SDK.python:
-        return grpc.Sdk.SDK_PYTHON;
-      case SDK.scio:
-        return grpc.Sdk.SDK_SCIO;
-    }
-  }
-
-  SDK _getAppSdk(grpc.Sdk sdk) {
-    switch (sdk) {
-      case grpc.Sdk.SDK_JAVA:
-        return SDK.java;
-      case grpc.Sdk.SDK_GO:
-        return SDK.go;
-      case grpc.Sdk.SDK_PYTHON:
-        return SDK.python;
-      case grpc.Sdk.SDK_SCIO:
-        return SDK.scio;
-      default:
-        return SDK.java;
-    }
-  }
-
-  ExampleType _exampleTypeFromString(grpc.PrecompiledObjectType type) {
-    switch (type) {
-      case grpc.PrecompiledObjectType.PRECOMPILED_OBJECT_TYPE_EXAMPLE:
-        return ExampleType.example;
-      case grpc.PrecompiledObjectType.PRECOMPILED_OBJECT_TYPE_KATA:
-        return ExampleType.kata;
-      case grpc.PrecompiledObjectType.PRECOMPILED_OBJECT_TYPE_UNIT_TEST:
-        return ExampleType.test;
-      case grpc.PrecompiledObjectType.PRECOMPILED_OBJECT_TYPE_UNSPECIFIED:
-        return ExampleType.all;
-      default:
-        return ExampleType.example;
-    }
-  }
-
-  Map<SDK, List<CategoryModel>> _toClientCategories(
-    List<grpc.Categories> response,
-  ) {
-    Map<SDK, List<CategoryModel>> sdkCategoriesMap = {};
-    List<MapEntry<SDK, List<CategoryModel>>> entries = [];
-    for (var sdkMap in response) {
-      SDK sdk = _getAppSdk(sdkMap.sdk);
-      List<CategoryModel> categoriesForSdk = [];
-      for (var category in sdkMap.categories) {
-        List<ExampleModel> examples = category.precompiledObjects
-            .map((example) => _toExampleModel(sdk, example))
-            .toList()
-          ..sort();
-        categoriesForSdk.add(CategoryModel(
-          name: category.categoryName,
-          examples: examples,
-        ));
-      }
-      entries.add(MapEntry(sdk, categoriesForSdk..sort()));
-    }
-    sdkCategoriesMap.addEntries(entries);
-    return sdkCategoriesMap;
-  }
-
-  ExampleModel _toExampleModel(SDK sdk, grpc.PrecompiledObject example) {
-    return ExampleModel(
-      sdk: sdk,
-      name: example.name,
-      description: example.description,
-      type: _exampleTypeFromString(example.type),
-      path: example.cloudPath,
-      contextLine: example.contextLine,
-      pipelineOptions: example.pipelineOptions,
-      isMultiFile: example.multifile,
-      link: example.link,
-    );
-  }
-
-  List<SharedFile> _convertToSharedFileList(
-    List<grpc.SnippetFile> snippetFileList,
-  ) {
-    final sharedFilesList = <SharedFile>[];
-
-    for (grpc.SnippetFile item in snippetFileList) {
-      sharedFilesList.add(SharedFile(
-        code: item.content,
-        isMain: item.isMain,
-        name: item.name,
-      ));
-    }
-
-    return sharedFilesList;
-  }
-
-  List<grpc.SnippetFile> _convertToSnippetFileList(
-    List<SharedFile> sharedFilesList,
-  ) {
-    final snippetFileList = <grpc.SnippetFile>[];
-
-    for (SharedFile item in sharedFilesList) {
-      snippetFileList.add(
-        grpc.SnippetFile()
-          ..name = item.name
-          ..isMain = true
-          ..content = item.code,
-      );
-    }
-
-    return snippetFileList;
-  }
-}
diff --git a/playground/frontend/lib/modules/examples/repositories/example_repository.dart b/playground/frontend/lib/modules/examples/repositories/example_repository.dart
deleted file mode 100644
index 65c09f885fb..00000000000
--- a/playground/frontend/lib/modules/examples/repositories/example_repository.dart
+++ /dev/null
@@ -1,98 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License.  You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-import 'package:playground/modules/examples/models/category_model.dart';
-import 'package:playground/modules/examples/models/example_model.dart';
-import 'package:playground/modules/examples/repositories/example_client/example_client.dart';
-import 'package:playground/modules/examples/repositories/models/get_snippet_request.dart';
-import 'package:playground/modules/examples/repositories/models/get_snippet_response.dart';
-import 'package:playground/modules/examples/repositories/models/get_example_request.dart';
-import 'package:playground/modules/examples/repositories/models/get_list_of_examples_request.dart';
-import 'package:playground/modules/examples/repositories/models/save_snippet_request.dart';
-import 'package:playground/modules/sdk/models/sdk.dart';
-
-class ExampleRepository {
-  late final ExampleClient _client;
-
-  ExampleRepository(ExampleClient client) {
-    _client = client;
-  }
-
-  Future<Map<SDK, List<CategoryModel>>> getListOfExamples(
-    GetListOfExamplesRequestWrapper request,
-  ) async {
-    final result = await _client.getListOfExamples(request);
-    return result.categories;
-  }
-
-  Future<ExampleModel> getDefaultExample(
-    GetExampleRequestWrapper request,
-  ) async {
-    final result = await _client.getDefaultExample(request);
-    return result.example;
-  }
-
-  Future<String> getExampleSource(
-    GetExampleRequestWrapper request,
-  ) async {
-    final result = await _client.getExampleSource(request);
-    return result.code;
-  }
-
-  Future<String> getExampleOutput(
-    GetExampleRequestWrapper request,
-  ) async {
-    final result = await _client.getExampleOutput(request);
-    return result.output;
-  }
-
-  Future<String> getExampleLogs(
-    GetExampleRequestWrapper request,
-  ) async {
-    final result = await _client.getExampleLogs(request);
-    return result.output;
-  }
-
-  Future<String> getExampleGraph(
-    GetExampleRequestWrapper request,
-  ) async {
-    final result = await _client.getExampleGraph(request);
-    return result.output;
-  }
-
-  Future<ExampleModel> getExample(
-    GetExampleRequestWrapper request,
-  ) async {
-    final result = await _client.getExample(request);
-    return result.example;
-  }
-
-  Future<GetSnippetResponse> getSnippet(
-    GetSnippetRequestWrapper request,
-  ) async {
-    final result = await _client.getSnippet(request);
-    return result;
-  }
-
-  Future<String> saveSnippet(
-    SaveSnippetRequestWrapper request,
-  ) async {
-    final result = await _client.saveSnippet(request);
-    return result.id;
-  }
-}
diff --git a/playground/frontend/lib/modules/examples/repositories/models/get_example_request.dart b/playground/frontend/lib/modules/examples/repositories/models/get_example_request.dart
deleted file mode 100644
index da87f09b288..00000000000
--- a/playground/frontend/lib/modules/examples/repositories/models/get_example_request.dart
+++ /dev/null
@@ -1,37 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License.  You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-import 'package:flutter/cupertino.dart';
-import 'package:playground/modules/sdk/models/sdk.dart';
-
-class GetExampleRequestWrapper {
-  final String path;
-  final SDK sdk;
-
-  GetExampleRequestWrapper(this.path, this.sdk);
-
-  @override
-  bool operator ==(Object other) =>
-      identical(this, other) ||
-      other is GetExampleRequestWrapper &&
-          path == other.path &&
-          sdk == other.sdk;
-
-  @override
-  int get hashCode => hashValues(path.hashCode, sdk.hashCode);
-}
diff --git a/playground/frontend/lib/modules/examples/repositories/models/get_list_of_examples_request.dart b/playground/frontend/lib/modules/examples/repositories/models/get_list_of_examples_request.dart
deleted file mode 100644
index 0d39df522d5..00000000000
--- a/playground/frontend/lib/modules/examples/repositories/models/get_list_of_examples_request.dart
+++ /dev/null
@@ -1,37 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License.  You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-import 'package:flutter/cupertino.dart';
-import 'package:playground/modules/sdk/models/sdk.dart';
-
-class GetListOfExamplesRequestWrapper {
-  final SDK? sdk;
-  final String? category;
-
-  GetListOfExamplesRequestWrapper({required this.sdk, required this.category});
-
-  @override
-  bool operator ==(Object other) =>
-      identical(this, other) ||
-          other is GetListOfExamplesRequestWrapper &&
-              category == other.category &&
-              sdk == other.sdk;
-
-  @override
-  int get hashCode => hashValues(category.hashCode, sdk.hashCode);
-}
diff --git a/playground/frontend/lib/modules/examples/repositories/models/get_list_of_examples_response.dart b/playground/frontend/lib/modules/examples/repositories/models/get_list_of_examples_response.dart
deleted file mode 100644
index 609c61df3ad..00000000000
--- a/playground/frontend/lib/modules/examples/repositories/models/get_list_of_examples_response.dart
+++ /dev/null
@@ -1,26 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License.  You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-import 'package:playground/modules/examples/models/category_model.dart';
-import 'package:playground/modules/sdk/models/sdk.dart';
-
-class GetListOfExampleResponse {
-  final Map<SDK, List<CategoryModel>> categories;
-
-  GetListOfExampleResponse(this.categories);
-}
diff --git a/playground/frontend/lib/modules/messages/handlers/messages_handler.dart b/playground/frontend/lib/modules/messages/handlers/messages_handler.dart
index 3a4047afbb5..12538d7daec 100644
--- a/playground/frontend/lib/modules/messages/handlers/messages_handler.dart
+++ b/playground/frontend/lib/modules/messages/handlers/messages_handler.dart
@@ -20,16 +20,16 @@ import 'package:playground/modules/messages/handlers/abstract_message_handler.da
 import 'package:playground/modules/messages/handlers/set_content_message_handler.dart';
 import 'package:playground/modules/messages/handlers/set_sdk_message_handler.dart';
 import 'package:playground/modules/messages/models/abstract_message.dart';
-import 'package:playground/pages/playground/states/playground_state.dart';
+import 'package:playground_components/playground_components.dart';
 
 class MessagesHandler extends AbstractMessageHandler {
   final List<AbstractMessageHandler> handlers;
 
   MessagesHandler({
-    required PlaygroundState playgroundState,
+    required PlaygroundController playgroundController,
   }) : handlers = [
-    SetContentMessageHandler(playgroundState: playgroundState),
-    SetSdkMessageHandler(playgroundState: playgroundState),
+    SetContentMessageHandler(playgroundController: playgroundController),
+    SetSdkMessageHandler(playgroundController: playgroundController),
   ];
 
   @override
diff --git a/playground/frontend/lib/modules/messages/handlers/set_content_message_handler.dart b/playground/frontend/lib/modules/messages/handlers/set_content_message_handler.dart
index 1e16e397f6e..c2424b38af6 100644
--- a/playground/frontend/lib/modules/messages/handlers/set_content_message_handler.dart
+++ b/playground/frontend/lib/modules/messages/handlers/set_content_message_handler.dart
@@ -19,13 +19,13 @@
 import 'package:playground/modules/messages/handlers/abstract_message_handler.dart';
 import 'package:playground/modules/messages/models/abstract_message.dart';
 import 'package:playground/modules/messages/models/set_content_message.dart';
-import 'package:playground/pages/playground/states/playground_state.dart';
+import 'package:playground_components/playground_components.dart';
 
 class SetContentMessageHandler extends AbstractMessageHandler {
-  final PlaygroundState playgroundState;
+  final PlaygroundController playgroundController;
 
   const SetContentMessageHandler({
-    required this.playgroundState,
+    required this.playgroundController,
   });
 
   @override
@@ -39,6 +39,6 @@ class SetContentMessageHandler extends AbstractMessageHandler {
   }
 
   void _handle(SetContentMessage message) {
-    playgroundState.examplesLoader.load(message.descriptor);
+    playgroundController.examplesLoader.load(message.descriptor);
   }
 }
diff --git a/playground/frontend/lib/modules/messages/handlers/set_sdk_message_handler.dart b/playground/frontend/lib/modules/messages/handlers/set_sdk_message_handler.dart
index 9a2595f9215..2d066a9e846 100644
--- a/playground/frontend/lib/modules/messages/handlers/set_sdk_message_handler.dart
+++ b/playground/frontend/lib/modules/messages/handlers/set_sdk_message_handler.dart
@@ -19,13 +19,13 @@
 import 'package:playground/modules/messages/handlers/abstract_message_handler.dart';
 import 'package:playground/modules/messages/models/abstract_message.dart';
 import 'package:playground/modules/messages/models/set_sdk_message.dart';
-import 'package:playground/pages/playground/states/playground_state.dart';
+import 'package:playground_components/playground_components.dart';
 
 class SetSdkMessageHandler extends AbstractMessageHandler {
-  final PlaygroundState playgroundState;
+  final PlaygroundController playgroundController;
 
   const SetSdkMessageHandler({
-    required this.playgroundState,
+    required this.playgroundController,
   });
 
   @override
@@ -39,6 +39,6 @@ class SetSdkMessageHandler extends AbstractMessageHandler {
   }
 
   void _handle(SetSdkMessage message) {
-    playgroundState.setSdk(message.sdk);
+    playgroundController.setSdk(message.sdk);
   }
 }
diff --git a/playground/frontend/lib/modules/messages/models/set_content_message.dart b/playground/frontend/lib/modules/messages/models/set_content_message.dart
index 707bf5deb68..fd8e1a04ba8 100644
--- a/playground/frontend/lib/modules/messages/models/set_content_message.dart
+++ b/playground/frontend/lib/modules/messages/models/set_content_message.dart
@@ -16,9 +16,9 @@
  * limitations under the License.
  */
 
-import 'package:playground/modules/examples/models/example_loading_descriptors/examples_loading_descriptor.dart';
 import 'package:playground/modules/examples/models/example_loading_descriptors/examples_loading_descriptor_factory.dart';
 import 'package:playground/modules/messages/models/abstract_message.dart';
+import 'package:playground_components/playground_components.dart';
 
 /// A message that sets content for multiple snippets.
 ///
diff --git a/playground/frontend/lib/modules/messages/models/set_sdk_message.dart b/playground/frontend/lib/modules/messages/models/set_sdk_message.dart
index ed195fd8013..af3841afadf 100644
--- a/playground/frontend/lib/modules/messages/models/set_sdk_message.dart
+++ b/playground/frontend/lib/modules/messages/models/set_sdk_message.dart
@@ -17,13 +17,13 @@
  */
 
 import 'package:playground/modules/messages/models/abstract_message.dart';
-import 'package:playground/modules/sdk/models/sdk.dart';
+import 'package:playground_components/playground_components.dart';
 
 /// A message that switches the SDK.
 ///
 /// Sent to iframes by Beam documentation HTML when the language is switched.
 class SetSdkMessage extends AbstractMessage {
-  final SDK sdk;
+  final Sdk sdk;
 
   static const type = 'SetSdk';
 
@@ -36,7 +36,7 @@ class SetSdkMessage extends AbstractMessage {
       return null;
     }
 
-    final sdk = SDK.tryParse(map['sdk']);
+    final sdk = Sdk.tryParse(map['sdk']);
     if (sdk == null) {
       return null;
     }
@@ -63,7 +63,7 @@ class SetSdkMessage extends AbstractMessage {
   @override
   Map<String, dynamic> toJson() {
     return {
-      'sdk': sdk.name,
+      'sdk': sdk.id,
     };
   }
 }
diff --git a/playground/frontend/lib/modules/output/components/output_area.dart b/playground/frontend/lib/modules/output/components/output_area.dart
deleted file mode 100644
index 1492124ef35..00000000000
--- a/playground/frontend/lib/modules/output/components/output_area.dart
+++ /dev/null
@@ -1,74 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License.  You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-import 'package:flutter/material.dart';
-import 'package:playground/modules/graph/graph_builder/painters/graph_painter.dart';
-import 'package:playground/modules/output/components/graph.dart';
-import 'package:playground/modules/output/components/output_result.dart';
-import 'package:playground/modules/output/models/output_placement.dart';
-import 'package:playground/modules/output/models/output_placement_state.dart';
-import 'package:playground/pages/playground/states/playground_state.dart';
-import 'package:provider/provider.dart';
-
-class OutputArea extends StatelessWidget {
-  final TabController tabController;
-  final bool showGraph;
-
-  const OutputArea({
-    Key? key,
-    required this.tabController,
-    required this.showGraph,
-  }) : super(key: key);
-
-  @override
-  Widget build(BuildContext context) {
-    return Container(
-      color: Theme.of(context).backgroundColor,
-      child: Consumer2<PlaygroundState, OutputPlacementState>(
-        builder: (context, playgroundState, placementState, child) {
-          final sdk = playgroundState.sdk;
-
-          return TabBarView(
-            controller: tabController,
-            physics: const NeverScrollableScrollPhysics(),
-            children: <Widget>[
-              OutputResult(
-                text: playgroundState.outputResult,
-                isSelected: tabController.index == 0,
-              ),
-              if (showGraph)
-                sdk == null
-                    ? Container()
-                    : GraphTab(
-                        graph: playgroundState.result?.graph ?? '',
-                        sdk: sdk,
-                        direction: _getGraphDirection(placementState.placement),
-                      ),
-            ],
-          );
-        },
-      ),
-    );
-  }
-
-  GraphDirection _getGraphDirection(OutputPlacement placement) {
-    return placement == OutputPlacement.bottom
-        ? GraphDirection.horizontal
-        : GraphDirection.vertical;
-  }
-}
diff --git a/playground/frontend/lib/modules/output/components/output_header/result_filter_bubble.dart b/playground/frontend/lib/modules/output/components/output_header/result_filter_bubble.dart
deleted file mode 100644
index d4401e77561..00000000000
--- a/playground/frontend/lib/modules/output/components/output_header/result_filter_bubble.dart
+++ /dev/null
@@ -1,80 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License.  You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-import 'package:flutter/material.dart';
-import 'package:playground/config/theme.dart';
-import 'package:playground/constants/sizes.dart';
-import 'package:playground/modules/examples/models/outputs_model.dart';
-import 'package:playground/pages/playground/states/playground_state.dart';
-import 'package:provider/provider.dart';
-
-class ResultFilterBubble extends StatelessWidget {
-  final OutputType type;
-  final String name;
-
-  const ResultFilterBubble({
-    Key? key,
-    required this.type,
-    required this.name,
-  }) : super(key: key);
-
-  @override
-  Widget build(BuildContext context) {
-    return MouseRegion(
-      cursor: SystemMouseCursors.click,
-      child: Padding(
-        padding: const EdgeInsets.only(right: kMdSpacing),
-        child: Consumer<PlaygroundState>(
-          builder: (context, state, child) {
-            final isSelected = type == state.selectedOutputFilterType;
-
-            return GestureDetector(
-              onTap: () {
-                if (!isSelected) {
-                  state.setSelectedOutputFilterType(type);
-                  state.filterOutput(type);
-                }
-              },
-              child: Container(
-                height: kContainerHeight,
-                padding: const EdgeInsets.symmetric(horizontal: kXlSpacing),
-                decoration: BoxDecoration(
-                  color: isSelected
-                      ? ThemeColors.of(context).primary
-                      : ThemeColors.of(context).lightGreyColor,
-                  borderRadius: BorderRadius.circular(kXlBorderRadius),
-                ),
-                child: Center(
-                  child: Text(
-                    name,
-                    style: TextStyle(
-                      color: isSelected
-                          ? ThemeColors.of(context).primaryBackgroundTextColor
-                          : ThemeColors.of(context)
-                          .lightGreyBackgroundTextColor,
-                    ),
-                  ),
-                ),
-              ),
-            );
-          },
-        ),
-      ),
-    );
-  }
-}
diff --git a/playground/frontend/lib/modules/output/components/output_header/result_filter_popover.dart b/playground/frontend/lib/modules/output/components/output_header/result_filter_popover.dart
deleted file mode 100644
index d93d79e5b1f..00000000000
--- a/playground/frontend/lib/modules/output/components/output_header/result_filter_popover.dart
+++ /dev/null
@@ -1,75 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License.  You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-import 'package:flutter/material.dart';
-import 'package:flutter_gen/gen_l10n/app_localizations.dart';
-import 'package:playground/constants/sizes.dart';
-import 'package:playground/modules/examples/models/outputs_model.dart';
-import 'package:playground/modules/output/components/output_header/result_filter_bubble.dart';
-
-const kPopoverWidth = 240.0;
-const kPopoverPadding = 50.0;
-
-class ResultFilterPopover extends StatelessWidget {
-  const ResultFilterPopover({Key? key}) : super(key: key);
-
-  @override
-  Widget build(BuildContext context) {
-    AppLocalizations appLocale = AppLocalizations.of(context)!;
-
-    return Padding(
-      padding: const EdgeInsets.only(top: kPopoverPadding),
-      child: SizedBox(
-        width: kPopoverWidth,
-        child: Card(
-          child: Padding(
-            padding: const EdgeInsets.all(kMdSpacing),
-            child: Wrap(
-              runSpacing: kMdSpacing,
-              children: [
-                Text(appLocale.displayAtThisTab),
-                Padding(
-                  padding: const EdgeInsets.symmetric(
-                    horizontal: kSmSpacing,
-                    vertical: kSmSpacing,
-                  ),
-                  child: Row(
-                    children: [
-                      ResultFilterBubble(
-                        type: OutputType.all,
-                        name: appLocale.all,
-                      ),
-                      ResultFilterBubble(
-                        type: OutputType.log,
-                        name: appLocale.log,
-                      ),
-                      ResultFilterBubble(
-                        type: OutputType.output,
-                        name: appLocale.output,
-                      ),
-                    ],
-                  ),
-                ),
-              ],
-            ),
-          ),
-        ),
-      ),
-    );
-  }
-}
diff --git a/playground/frontend/lib/modules/output/models/output_placement.dart b/playground/frontend/lib/modules/output/models/output_placement.dart
index d15c9d36c38..642553751cc 100644
--- a/playground/frontend/lib/modules/output/models/output_placement.dart
+++ b/playground/frontend/lib/modules/output/models/output_placement.dart
@@ -24,6 +24,13 @@ enum OutputPlacement {
   right,
   left,
   bottom,
+  ;
+
+  Axis get graphDirection {
+    return this == OutputPlacement.bottom
+        ? Axis.horizontal
+        : Axis.vertical;
+  }
 }
 
 extension OutputPlacementToIcon on OutputPlacement {
diff --git a/playground/frontend/lib/modules/sdk/components/sdk_selector.dart b/playground/frontend/lib/modules/sdk/components/sdk_selector.dart
index c8197e8acbc..c4c1656a174 100644
--- a/playground/frontend/lib/modules/sdk/components/sdk_selector.dart
+++ b/playground/frontend/lib/modules/sdk/components/sdk_selector.dart
@@ -21,8 +21,7 @@ import 'package:flutter_gen/gen_l10n/app_localizations.dart';
 import 'package:playground/components/dropdown_button/dropdown_button.dart';
 import 'package:playground/constants/sizes.dart';
 import 'package:playground/modules/sdk/components/sdk_selector_row.dart';
-import 'package:playground/modules/sdk/models/sdk.dart';
-import 'package:playground/pages/playground/states/playground_state.dart';
+import 'package:playground_components/playground_components.dart';
 import 'package:provider/provider.dart';
 
 const kEmptyExampleName = 'Catalog';
@@ -31,8 +30,8 @@ const double kWidth = 150;
 const double kHeight = 172;
 
 class SDKSelector extends StatelessWidget {
-  final SDK? value;
-  final ValueChanged<SDK> onChanged;
+  final Sdk? value;
+  final ValueChanged<Sdk> onChanged;
 
   const SDKSelector({
     Key? key,
@@ -45,35 +44,35 @@ class SDKSelector extends StatelessWidget {
     final localizations = AppLocalizations.of(context)!;
     final text = value == null
         ? localizations.selectSdkPlaceholder
-        : 'SDK: ${value?.displayName}';
+        : 'SDK: ${value?.title}';
 
     return Semantics(
       container: true,
       button: true,
       label: localizations.selectSdkDropdownSemantics,
-      child: AppDropdownButton(
-        buttonText: Text(text),
-        createDropdown: (close) => Column(
-          children: [
-            const SizedBox(height: kMdSpacing),
-            ...SDK.values.map((SDK value) {
-              return SizedBox(
-                width: double.infinity,
-                child: Consumer<PlaygroundState>(
-                  builder: (context, state, child) => SdkSelectorRow(
+      child: Consumer<PlaygroundController>(
+        builder: (context, controller, child) => AppDropdownButton(
+          buttonText: Text(text),
+          createDropdown: (close) => Column(
+            children: [
+              const SizedBox(height: kMdSpacing),
+              ...Sdk.known.map((Sdk value) {
+                return SizedBox(
+                  width: double.infinity,
+                  child: SdkSelectorRow(
                     sdk: value,
                     onSelect: () {
                       close();
                       onChanged(value);
                     },
                   ),
-                ),
-              );
-            }),
-          ],
+                );
+              }),
+            ],
+          ),
+          width: kWidth,
+          height: kHeight,
         ),
-        width: kWidth,
-        height: kHeight,
       ),
     );
   }
diff --git a/playground/frontend/lib/modules/sdk/components/sdk_selector_row.dart b/playground/frontend/lib/modules/sdk/components/sdk_selector_row.dart
index d66dda2dee7..7993723bf25 100644
--- a/playground/frontend/lib/modules/sdk/components/sdk_selector_row.dart
+++ b/playground/frontend/lib/modules/sdk/components/sdk_selector_row.dart
@@ -19,10 +19,10 @@
 import 'package:flutter/material.dart';
 import 'package:playground/constants/font_weight.dart';
 import 'package:playground/constants/sizes.dart';
-import 'package:playground/modules/sdk/models/sdk.dart';
+import 'package:playground_components/playground_components.dart';
 
 class SdkSelectorRow extends StatelessWidget {
-  final SDK sdk;
+  final Sdk sdk;
   final VoidCallback onSelect;
 
   const SdkSelectorRow({
@@ -48,7 +48,7 @@ class SdkSelectorRow extends StatelessWidget {
       onPressed: onSelect,
       child: Padding(
         padding: const EdgeInsets.all(kLgSpacing),
-        child: Text(sdk.displayName),
+        child: Text(sdk.title),
       ),
     );
   }
diff --git a/playground/frontend/lib/modules/sdk/models/sdk.dart b/playground/frontend/lib/modules/sdk/models/sdk.dart
deleted file mode 100644
index ef1fd6f5728..00000000000
--- a/playground/frontend/lib/modules/sdk/models/sdk.dart
+++ /dev/null
@@ -1,117 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License.  You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-import 'package:highlight/highlight.dart';
-import 'package:highlight/languages/go.dart';
-import 'package:highlight/languages/java.dart';
-import 'package:highlight/languages/python.dart';
-import 'package:highlight/languages/scala.dart';
-import 'package:playground/config.g.dart';
-
-enum SDK {
-  java,
-  go,
-  python,
-  scio,
-  ;
-
-  /// A temporary solution while we wait for the backend to add
-  /// sdk in example responses.
-  static SDK? tryParseExamplePath(String? path) {
-    if (path == null) {
-      return null;
-    }
-
-    if (path.startsWith('SDK_JAVA')) {
-      return java;
-    }
-
-    if (path.startsWith('SDK_GO')) {
-      return go;
-    }
-
-    if (path.startsWith('SDK_PYTHON')) {
-      return python;
-    }
-
-    if (path.startsWith('SDK_SCIO')) {
-      return scio;
-    }
-
-    return null;
-  }
-
-  static SDK? tryParse(Object? value) {
-    if (value is! String) {
-      return null;
-    }
-
-    try {
-      return values.byName(value);
-    } catch (ex) {
-      return null;
-    }
-  }
-}
-
-extension SDKToString on SDK {
-  String get displayName {
-    switch (this) {
-      case SDK.go:
-        return 'Go';
-      case SDK.java:
-        return 'Java';
-      case SDK.python:
-        return 'Python';
-      case SDK.scio:
-        return 'SCIO';
-    }
-  }
-}
-
-extension SdkToRoute on SDK {
-  String get getRoute {
-    switch (this) {
-      case SDK.java:
-        return kApiJavaClientURL;
-      case SDK.go:
-        return kApiGoClientURL;
-      case SDK.python:
-        return kApiPythonClientURL;
-      case SDK.scio:
-        return kApiScioClientURL;
-      default:
-        return '';
-    }
-  }
-}
-
-extension SdkToHighlightMode on SDK {
-  Mode get highlightMode {
-    switch (this) {
-      case SDK.java:
-        return java;
-      case SDK.go:
-        return go;
-      case SDK.python:
-        return python;
-      case SDK.scio:
-        return scala;
-    }
-  }
-}
diff --git a/playground/frontend/lib/modules/shortcuts/components/shortcut_row.dart b/playground/frontend/lib/modules/shortcuts/components/shortcut_row.dart
index 538c20cdecf..27cd3435703 100644
--- a/playground/frontend/lib/modules/shortcuts/components/shortcut_row.dart
+++ b/playground/frontend/lib/modules/shortcuts/components/shortcut_row.dart
@@ -18,11 +18,10 @@
 
 import 'package:flutter/material.dart';
 import 'package:playground/constants/sizes.dart';
-import 'package:playground/modules/shortcuts/models/shortcut.dart';
-import 'package:playground/modules/shortcuts/utils/shortcuts_display_name.dart';
+import 'package:playground_components/playground_components.dart';
 
 class ShortcutRow extends StatelessWidget {
-  final Shortcut shortcut;
+  final BeamShortcut shortcut;
 
   const ShortcutRow({Key? key, required this.shortcut}) : super(key: key);
 
@@ -39,7 +38,7 @@ class ShortcutRow extends StatelessWidget {
           ),
           padding: const EdgeInsets.all(kMdSpacing),
           child: Text(
-            getShortcutDisplayName(shortcut),
+            shortcut.title,
             style: TextStyle(color: primaryColor),
           ),
         ),
diff --git a/playground/frontend/lib/modules/shortcuts/components/shortcuts_manager.dart b/playground/frontend/lib/modules/shortcuts/components/shortcuts_manager.dart
index 32d3ce26151..5a9f1bc1845 100644
--- a/playground/frontend/lib/modules/shortcuts/components/shortcuts_manager.dart
+++ b/playground/frontend/lib/modules/shortcuts/components/shortcuts_manager.dart
@@ -17,11 +17,11 @@
  */
 
 import 'package:flutter/material.dart';
-import 'package:playground/modules/shortcuts/models/shortcut.dart';
+import 'package:playground_components/playground_components.dart';
 
 class ShortcutsManager extends StatelessWidget {
   final Widget child;
-  final List<Shortcut> shortcuts;
+  final List<BeamShortcut> shortcuts;
 
   const ShortcutsManager({
     Key? key,
diff --git a/playground/frontend/lib/modules/shortcuts/components/shortcuts_modal.dart b/playground/frontend/lib/modules/shortcuts/components/shortcuts_modal.dart
index c92c2748756..1f334327f6b 100644
--- a/playground/frontend/lib/modules/shortcuts/components/shortcuts_modal.dart
+++ b/playground/frontend/lib/modules/shortcuts/components/shortcuts_modal.dart
@@ -16,12 +16,14 @@
  * limitations under the License.
  */
 
+import 'package:easy_localization/easy_localization.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter_gen/gen_l10n/app_localizations.dart';
 import 'package:playground/constants/font_weight.dart';
 import 'package:playground/constants/sizes.dart';
 import 'package:playground/modules/shortcuts/components/shortcut_row.dart';
 import 'package:playground/modules/shortcuts/constants/global_shortcuts.dart';
+import 'package:playground_components/playground_components.dart';
 
 const kButtonBorderRadius = 24.0;
 const kButtonWidth = 120.0;
@@ -29,7 +31,11 @@ const kButtonHeight = 40.0;
 const kDialogPadding = 40.0;
 
 class ShortcutsModal extends StatelessWidget {
-  const ShortcutsModal({Key? key}) : super(key: key);
+  final PlaygroundController playgroundController;
+
+  const ShortcutsModal({
+    required this.playgroundController,
+  });
 
   @override
   Widget build(BuildContext context) {
@@ -50,7 +56,10 @@ class ShortcutsModal extends StatelessWidget {
         crossAxisAlignment: WrapCrossAlignment.start,
         runSpacing: kXlSpacing,
         children: [
-          ...globalShortcuts.map(
+          ...[
+            ...playgroundController.shortcuts,
+            ...globalShortcuts,
+          ].map(
             (shortcut) => Row(
               crossAxisAlignment: CrossAxisAlignment.center,
               children: [
@@ -58,7 +67,7 @@ class ShortcutsModal extends StatelessWidget {
                 Expanded(
                   flex: 3,
                   child: Text(
-                    localize(context, shortcut.name),
+                    shortcut.actionIntent.slug.tr(),
                     style: const TextStyle(fontWeight: kBoldWeight),
                   ),
                 ),
@@ -84,21 +93,4 @@ class ShortcutsModal extends StatelessWidget {
       ],
     );
   }
-
-  String localize(BuildContext context, String shortcutName) {
-    AppLocalizations appLocale = AppLocalizations.of(context)!;
-
-    switch(shortcutName) {
-      case 'Run':
-        return appLocale.run;
-      case 'Reset':
-        return appLocale.reset;
-      case 'Clear Output':
-        return appLocale.clearOutput;
-      case 'New Example':
-        return appLocale.newExample;
-      default:
-        return shortcutName;
-    }
-  }
 }
diff --git a/playground/frontend/lib/modules/shortcuts/constants/global_shortcuts.dart b/playground/frontend/lib/modules/shortcuts/constants/global_shortcuts.dart
index bf3b14e7a73..03797eaff3a 100644
--- a/playground/frontend/lib/modules/shortcuts/constants/global_shortcuts.dart
+++ b/playground/frontend/lib/modules/shortcuts/constants/global_shortcuts.dart
@@ -18,93 +18,44 @@
 
 import 'package:flutter/material.dart';
 import 'package:flutter/services.dart';
-import 'package:playground/modules/shortcuts/models/shortcut.dart';
-import 'package:playground/pages/playground/states/playground_state.dart';
+import 'package:playground_components/playground_components.dart';
 import 'package:provider/provider.dart';
 import 'package:url_launcher/url_launcher.dart';
 
-const kRunText = 'Run';
-const kResetText = 'Reset';
-const kClearOutputText = 'Clear Output';
-const kNewExampleText = 'New Example';
-
-class ResetIntent extends Intent {
-  const ResetIntent();
-}
-
-class RunIntent extends Intent {
-  const RunIntent();
+class ClearOutputIntent extends BeamIntent {
+  const ClearOutputIntent() : super(slug: 'intents.playground.clearOutput');
 }
 
-class ClearOutputIntent extends Intent {
-  const ClearOutputIntent();
+class NewExampleIntent extends BeamIntent {
+  const NewExampleIntent() : super(slug: 'intents.playground.newExample');
 }
 
-class NewExampleIntent extends Intent {
-  const NewExampleIntent();
-}
-
-final kRunShortcut = Shortcut(
-  shortcuts: LogicalKeySet(
-    LogicalKeyboardKey.meta,
-    LogicalKeyboardKey.enter,
-  ),
-  actionIntent: const RunIntent(),
-  name: kRunText,
-  createAction: (BuildContext context) => CallbackAction(
-    onInvoke: (_) => Provider.of<PlaygroundState>(
-      context,
-      listen: false,
-    ).runCode(),
-  ),
-);
-
-final kResetShortcut = Shortcut(
-  shortcuts: LogicalKeySet(
-    LogicalKeyboardKey.meta,
-    LogicalKeyboardKey.shift,
-    LogicalKeyboardKey.keyE,
-  ),
-  actionIntent: const ResetIntent(),
-  name: kResetText,
-  createAction: (BuildContext context) => CallbackAction(
-    onInvoke: (_) => Provider.of<PlaygroundState>(
-      context,
-      listen: false,
-    ).reset(),
-  ),
-);
-
-final kClearOutputShortcut = Shortcut(
+final kClearOutputShortcut = BeamShortcut(
   shortcuts: LogicalKeySet(
     LogicalKeyboardKey.meta,
     LogicalKeyboardKey.keyB,
   ),
   actionIntent: const ClearOutputIntent(),
-  name: kClearOutputText,
   createAction: (BuildContext context) => CallbackAction(
-    onInvoke: (_) => Provider.of<PlaygroundState>(
+    onInvoke: (_) => Provider.of<PlaygroundController>(
       context,
       listen: false,
     ).clearOutput(),
   ),
 );
 
-final kNewExampleShortcut = Shortcut(
+final kNewExampleShortcut = BeamShortcut(
   shortcuts: LogicalKeySet(
     LogicalKeyboardKey.meta,
     LogicalKeyboardKey.keyM,
   ),
   actionIntent: const NewExampleIntent(),
-  name: kNewExampleText,
   createAction: (_) => CallbackAction(
     onInvoke: (_) => launchUrl(Uri.parse('/')),
   ),
 );
 
-final List<Shortcut> globalShortcuts = [
-  kRunShortcut,
-  kResetShortcut,
+final List<BeamShortcut> globalShortcuts = [
   kClearOutputShortcut,
   kNewExampleShortcut,
 ];
diff --git a/playground/frontend/lib/modules/shortcuts/utils/shortcuts_display_name.dart b/playground/frontend/lib/modules/shortcuts/utils/shortcuts_display_name.dart
deleted file mode 100644
index eff27c1c446..00000000000
--- a/playground/frontend/lib/modules/shortcuts/utils/shortcuts_display_name.dart
+++ /dev/null
@@ -1,36 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License.  You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-import 'package:flutter/services.dart';
-import 'package:playground/modules/shortcuts/models/shortcut.dart';
-
-const kMetaKeyName = 'CMD/CTRL';
-const kShortcutKeyJoinSymbol = ' + ';
-
-String getShortcutDisplayName(Shortcut shortcut) {
-  return shortcut.shortcuts.keys
-      .map((e) => getKeyDisplayName(e))
-      .join(kShortcutKeyJoinSymbol);
-}
-
-String getKeyDisplayName(LogicalKeyboardKey e) {
-  if (e.keyId == LogicalKeyboardKey.meta.keyId) {
-    return kMetaKeyName;
-  }
-  return e.keyLabel;
-}
diff --git a/playground/frontend/lib/pages/embedded_playground/components/embedded_actions.dart b/playground/frontend/lib/pages/embedded_playground/components/embedded_actions.dart
index 5e8e56bf4fa..10ccf96ba4f 100644
--- a/playground/frontend/lib/pages/embedded_playground/components/embedded_actions.dart
+++ b/playground/frontend/lib/pages/embedded_playground/components/embedded_actions.dart
@@ -27,8 +27,8 @@ import 'package:playground/constants/assets.dart';
 import 'package:playground/constants/params.dart';
 import 'package:playground/constants/sizes.dart';
 import 'package:playground/modules/messages/models/set_content_message.dart';
-import 'package:playground/pages/playground/states/playground_state.dart';
 import 'package:playground/utils/javascript_post_message.dart';
+import 'package:playground_components/playground_components.dart';
 import 'package:provider/provider.dart';
 
 const kTryPlaygroundButtonWidth = 200.0;
@@ -44,29 +44,29 @@ class EmbeddedActions extends StatelessWidget {
       child: SizedBox(
         width: kTryPlaygroundButtonWidth,
         height: kTryPlaygroundButtonHeight,
-        child: Consumer<PlaygroundState>(
-          builder: (context, state, child) => ElevatedButton.icon(
+        child: Consumer<PlaygroundController>(
+          builder: (context, controller, child) => ElevatedButton.icon(
             icon: SvgPicture.asset(kLinkIconAsset),
             label: Text(AppLocalizations.of(context)!.tryInPlayground),
-            onPressed: () => _openStandalonePlayground(state),
+            onPressed: () => _openStandalonePlayground(controller),
           ),
         ),
       ),
     );
   }
 
-  void _openStandalonePlayground(PlaygroundState state) {
+  void _openStandalonePlayground(PlaygroundController controller) {
     // The empty list forces the parsing of EmptyExampleLoadingDescriptor
     // and prevents the glimpse of the default catalog example.
     final window = html.window.open(
-      '/?$kExamplesParam=[]&$kSdkParam=${state.sdk?.name}',
+      '/?$kExamplesParam=[]&$kSdkParam=${controller.sdk?.id}',
       '',
     );
 
     javaScriptPostMessageRepeated(
       window,
       SetContentMessage(
-        descriptor: state.getLoadingDescriptor(),
+        descriptor: controller.getLoadingDescriptor(),
       ),
     );
   }
diff --git a/playground/frontend/lib/pages/embedded_playground/components/embedded_appbar_title.dart b/playground/frontend/lib/pages/embedded_playground/components/embedded_appbar_title.dart
index fd981f59875..112bea826dd 100644
--- a/playground/frontend/lib/pages/embedded_playground/components/embedded_appbar_title.dart
+++ b/playground/frontend/lib/pages/embedded_playground/components/embedded_appbar_title.dart
@@ -18,16 +18,11 @@
 
 import 'package:flutter/material.dart';
 import 'package:flutter/services.dart';
-import 'package:flutter_gen/gen_l10n/app_localizations.dart';
 import 'package:flutter_svg/flutter_svg.dart';
-import 'package:playground/components/toggle_theme_button/toggle_theme_icon_button.dart';
+import 'package:playground/components/playground_run_or_cancel_button.dart';
 import 'package:playground/constants/assets.dart';
 import 'package:playground/constants/sizes.dart';
-import 'package:playground/modules/analytics/analytics_service.dart';
-import 'package:playground/modules/editor/components/run_button.dart';
-import 'package:playground/modules/notifications/components/notification.dart';
-import 'package:playground/pages/playground/states/playground_state.dart';
-import 'package:playground/utils/analytics_utils.dart';
+import 'package:playground_components/playground_components.dart';
 import 'package:provider/provider.dart';
 
 class EmbeddedAppBarTitle extends StatelessWidget {
@@ -35,50 +30,19 @@ class EmbeddedAppBarTitle extends StatelessWidget {
 
   @override
   Widget build(BuildContext context) {
-    return Consumer<PlaygroundState>(
-      builder: (context, state, child) => Wrap(
+    return Consumer<PlaygroundController>(
+      builder: (context, controller, child) => Wrap(
         crossAxisAlignment: WrapCrossAlignment.center,
         spacing: kXlSpacing,
         children: [
-          RunButton(
-            isRunning: state.isCodeRunning,
-            cancelRun: () {
-              final exampleName = getAnalyticsExampleName(state);
-              final analyticsService = AnalyticsService.get(context);
-              analyticsService.trackClickCancelRunEvent(exampleName);
-
-              state.cancelRun().catchError(
-                    (_) => NotificationManager.showError(
-                      context,
-                      AppLocalizations.of(context)!.runCode,
-                      AppLocalizations.of(context)!.cancelExecution,
-                    ),
-                  );
-            },
-            runCode: () {
-              final stopwatch = Stopwatch()..start();
-              final exampleName = getAnalyticsExampleName(state);
-              final analyticsService = AnalyticsService.get(context);
-
-              state.runCode(
-                onFinish: () {
-                  analyticsService.trackRunTimeEvent(
-                    exampleName,
-                    stopwatch.elapsedMilliseconds,
-                  );
-                },
-              );
-              analyticsService.trackClickRunEvent(exampleName);
-            },
-          ),
+          const PlaygroundRunOrCancelButton(),
           const ToggleThemeIconButton(),
           IconButton(
             iconSize: kIconSizeLg,
             splashRadius: kIconButtonSplashRadius,
             icon: SvgPicture.asset(kCopyIconAsset),
             onPressed: () {
-              final source =
-                  Provider.of<PlaygroundState>(context, listen: false).source;
+              final source = controller.source;
               Clipboard.setData(ClipboardData(text: source));
             },
           ),
diff --git a/playground/frontend/lib/pages/embedded_playground/components/embedded_editor.dart b/playground/frontend/lib/pages/embedded_playground/components/embedded_editor.dart
index 9728bdb1f43..94000034f29 100644
--- a/playground/frontend/lib/pages/embedded_playground/components/embedded_editor.dart
+++ b/playground/frontend/lib/pages/embedded_playground/components/embedded_editor.dart
@@ -17,10 +17,7 @@
  */
 
 import 'package:flutter/material.dart';
-import 'package:playground/components/loading_indicator/loading_indicator.dart';
-import 'package:playground/constants/sizes.dart';
-import 'package:playground/modules/editor/components/editor_textarea.dart';
-import 'package:playground/pages/playground/states/playground_state.dart';
+import 'package:playground_components/playground_components.dart';
 import 'package:provider/provider.dart';
 
 class EmbeddedEditor extends StatelessWidget {
@@ -30,21 +27,17 @@ class EmbeddedEditor extends StatelessWidget {
 
   @override
   Widget build(BuildContext context) {
-    final state = Provider.of<PlaygroundState>(context);
-    final controller = state.snippetEditingController;
+    final controller = Provider.of<PlaygroundController>(context);
+    final snippetController = controller.snippetEditingController;
 
-    if (controller == null) {
-      return const LoadingIndicator(size: kLgLoadingIndicatorSize);
+    if (snippetController == null) {
+      return const LoadingIndicator();
     }
 
-    return EditorTextArea(
-      codeController: controller.codeController,
-      key: ValueKey(state.selectedExample),
-      enabled: true,
-      sdk: controller.sdk,
-      example: state.selectedExample,
+    return SnippetEditor(
+      controller: snippetController,
       isEditable: isEditable,
-      isEmbedded: true,
+      goToContextLine: false,
     );
   }
 }
diff --git a/playground/frontend/lib/pages/embedded_playground/embedded_playground_page.dart b/playground/frontend/lib/pages/embedded_playground/embedded_playground_page.dart
index 8d4c41454f3..396c31a8264 100644
--- a/playground/frontend/lib/pages/embedded_playground/embedded_playground_page.dart
+++ b/playground/frontend/lib/pages/embedded_playground/embedded_playground_page.dart
@@ -17,12 +17,11 @@
  */
 
 import 'package:flutter/material.dart';
-import 'package:playground/modules/output/components/output.dart';
 import 'package:playground/pages/embedded_playground/components/embedded_actions.dart';
 import 'package:playground/pages/embedded_playground/components/embedded_appbar_title.dart';
 import 'package:playground/pages/embedded_playground/components/embedded_editor.dart';
 import 'package:playground/pages/embedded_playground/components/embedded_split_view.dart';
-import 'package:playground/pages/playground/states/playground_state.dart';
+import 'package:playground_components/playground_components.dart';
 import 'package:provider/provider.dart';
 
 const kActionsWidth = 300.0;
@@ -38,8 +37,8 @@ class EmbeddedPlaygroundPage extends StatelessWidget {
 
   @override
   Widget build(BuildContext context) {
-    return Consumer<PlaygroundState>(
-      builder: (context, state, child) => Scaffold(
+    return Consumer<PlaygroundController>(
+      builder: (context, controller, child) => Scaffold(
         appBar: AppBar(
           automaticallyImplyLeading: false,
           title: const EmbeddedAppBarTitle(),
@@ -49,9 +48,9 @@ class EmbeddedPlaygroundPage extends StatelessWidget {
           first: EmbeddedEditor(isEditable: isEditable),
           second: Container(
             color: Theme.of(context).backgroundColor,
-            child: Output(
-              isEmbedded: true,
-              playgroundState: state,
+            child: OutputWidget(
+              playgroundController: controller,
+              graphDirection: Axis.horizontal,
             ),
           ),
         ),
diff --git a/playground/frontend/lib/pages/playground/components/close_listener.dart b/playground/frontend/lib/pages/playground/components/close_listener.dart
index 682355585b0..ba2a19a7ae2 100644
--- a/playground/frontend/lib/pages/playground/components/close_listener.dart
+++ b/playground/frontend/lib/pages/playground/components/close_listener.dart
@@ -17,7 +17,7 @@
  */
 
 import 'package:flutter/material.dart';
-import 'package:playground/pages/playground/states/playground_state.dart';
+import 'package:playground_components/playground_components.dart';
 import 'dart:html' as html;
 
 import 'package:provider/provider.dart';
@@ -36,7 +36,7 @@ class _CloseListenerState extends State<CloseListener> {
   void initState() {
     WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
       html.window.onBeforeUnload.listen((event) async {
-        Provider.of<PlaygroundState>(context, listen: false).cancelRun();
+        Provider.of<PlaygroundController>(context, listen: false).cancelRun();
       });
     });
     super.initState();
diff --git a/playground/frontend/lib/pages/playground/components/editor_textarea_wrapper.dart b/playground/frontend/lib/pages/playground/components/editor_textarea_wrapper.dart
index be58a603d18..5d427f41d98 100644
--- a/playground/frontend/lib/pages/playground/components/editor_textarea_wrapper.dart
+++ b/playground/frontend/lib/pages/playground/components/editor_textarea_wrapper.dart
@@ -18,17 +18,12 @@
 
 import 'package:flutter/material.dart';
 import 'package:flutter_gen/gen_l10n/app_localizations.dart';
-import 'package:playground/components/loading_indicator/loading_indicator.dart';
+import 'package:playground/components/playground_run_or_cancel_button.dart';
 import 'package:playground/constants/sizes.dart';
-import 'package:playground/modules/analytics/analytics_service.dart';
-import 'package:playground/modules/editor/components/editor_textarea.dart';
-import 'package:playground/modules/editor/components/run_button.dart';
 import 'package:playground/modules/editor/components/share_dropdown/share_button.dart';
 import 'package:playground/modules/examples/components/description_popover/description_popover_button.dart';
 import 'package:playground/modules/examples/components/multifile_popover/multifile_popover_button.dart';
-import 'package:playground/modules/notifications/components/notification.dart';
-import 'package:playground/pages/playground/states/playground_state.dart';
-import 'package:playground/utils/analytics_utils.dart';
+import 'package:playground_components/playground_components.dart';
 import 'package:provider/provider.dart';
 
 class CodeTextAreaWrapper extends StatelessWidget {
@@ -36,17 +31,17 @@ class CodeTextAreaWrapper extends StatelessWidget {
 
   @override
   Widget build(BuildContext context) {
-    return Consumer<PlaygroundState>(builder: (context, state, child) {
-      if (state.result?.errorMessage?.isNotEmpty ?? false) {
+    return Consumer<PlaygroundController>(builder: (context, controller, child) {
+      if (controller.result?.errorMessage?.isNotEmpty ?? false) {
         WidgetsBinding.instance.addPostFrameCallback((_) {
-          _handleError(context, state);
+          _handleError(context, controller);
         });
       }
 
-      final controller = state.snippetEditingController;
+      final snippetController = controller.snippetEditingController;
 
-      if (controller == null) {
-        return const LoadingIndicator(size: kLgLoadingIndicatorSize);
+      if (snippetController == null) {
+        return const LoadingIndicator();
       }
 
       return Column(
@@ -55,12 +50,10 @@ class CodeTextAreaWrapper extends StatelessWidget {
             child: Stack(
               children: [
                 Positioned.fill(
-                  child: EditorTextArea(
-                    codeController: controller.codeController,
-                    enabled: !(state.selectedExample?.isMultiFile ?? false),
-                    example: state.selectedExample,
-                    sdk: controller.sdk,
+                  child: SnippetEditor(
+                    controller: snippetController,
                     isEditable: true,
+                    goToContextLine: true,
                   ),
                 ),
                 Positioned(
@@ -69,12 +62,12 @@ class CodeTextAreaWrapper extends StatelessWidget {
                   height: kButtonHeight,
                   child: Row(
                     children: [
-                      if (state.selectedExample != null) ...[
-                        if (state.selectedExample?.isMultiFile ?? false)
+                      if (controller.selectedExample != null) ...[
+                        if (controller.selectedExample?.isMultiFile ?? false)
                           Semantics(
                             container: true,
                             child: MultifilePopoverButton(
-                              example: state.selectedExample!,
+                              example: controller.selectedExample!,
                               followerAnchor: Alignment.topRight,
                               targetAnchor: Alignment.bottomRight,
                             ),
@@ -82,7 +75,7 @@ class CodeTextAreaWrapper extends StatelessWidget {
                         Semantics(
                           container: true,
                           child: DescriptionPopoverButton(
-                            example: state.selectedExample!,
+                            example: controller.selectedExample!,
                             followerAnchor: Alignment.topRight,
                             targetAnchor: Alignment.bottomRight,
                           ),
@@ -95,39 +88,7 @@ class CodeTextAreaWrapper extends StatelessWidget {
                       const SizedBox(width: kLgSpacing),
                       Semantics(
                         container: true,
-                        child: RunButton(
-                          disabled: state.selectedExample?.isMultiFile ?? false,
-                          isRunning: state.isCodeRunning,
-                          cancelRun: () {
-                            final exampleName = getAnalyticsExampleName(state);
-                            AnalyticsService.get(context)
-                                .trackClickCancelRunEvent(exampleName);
-                            state.cancelRun().catchError(
-                                  (_) => NotificationManager.showError(
-                                    context,
-                                    AppLocalizations.of(context)!.runCode,
-                                    AppLocalizations.of(context)!
-                                        .cancelExecution,
-                                  ),
-                                );
-                          },
-                          runCode: () {
-                            AnalyticsService analyticsService =
-                                AnalyticsService.get(context);
-                            final stopwatch = Stopwatch()..start();
-                            final exampleName = getAnalyticsExampleName(state);
-
-                            state.runCode(
-                              onFinish: () {
-                                analyticsService.trackRunTimeEvent(
-                                  exampleName,
-                                  stopwatch.elapsedMilliseconds,
-                                );
-                              },
-                            );
-                            analyticsService.trackClickRunEvent(exampleName);
-                          },
-                        ),
+                        child: const PlaygroundRunOrCancelButton(),
                       ),
                     ],
                   ),
@@ -140,12 +101,12 @@ class CodeTextAreaWrapper extends StatelessWidget {
     });
   }
 
-  _handleError(BuildContext context, PlaygroundState state) {
+  void _handleError(BuildContext context, PlaygroundController controller) {
     NotificationManager.showError(
       context,
       AppLocalizations.of(context)!.runCode,
-      state.result?.errorMessage ?? '',
+      controller.result?.errorMessage ?? '',
     );
-    state.resetError();
+    controller.resetError();
   }
 }
diff --git a/playground/frontend/lib/pages/playground/components/feedback/feedback_dropdown_content.dart b/playground/frontend/lib/pages/playground/components/feedback/feedback_dropdown_content.dart
index 38f17396cbf..278d973d84e 100644
--- a/playground/frontend/lib/pages/playground/components/feedback/feedback_dropdown_content.dart
+++ b/playground/frontend/lib/pages/playground/components/feedback/feedback_dropdown_content.dart
@@ -17,13 +17,12 @@
  */
 
 import 'package:flutter/material.dart';
-import 'package:playground/components/horizontal_divider/horizontal_divider.dart';
-import 'package:playground/config/theme.dart';
 import 'package:playground/constants/font_weight.dart';
 import 'package:playground/constants/fonts.dart';
 import 'package:playground/constants/sizes.dart';
 import 'package:playground/modules/analytics/analytics_service.dart';
 import 'package:playground/pages/playground/components/feedback/feedback_dropdown_icon_button.dart';
+import 'package:playground_components/playground_components.dart';
 
 const double kTextFieldWidth = 365.0;
 const double kTextFieldHeight = 68.0;
@@ -46,8 +45,10 @@ class FeedbackDropdownContent extends StatelessWidget {
 
   @override
   Widget build(BuildContext context) {
+    final borderColor = Theme.of(context).extension<BeamThemeExtension>()!.borderColor;
+
     final OutlineInputBorder border = OutlineInputBorder(
-      borderSide: BorderSide(color: ThemeColors.of(context).lightGreyColor),
+      borderSide: BorderSide(color: borderColor),
       borderRadius: BorderRadius.circular(kMdBorderRadius),
     );
 
@@ -114,7 +115,7 @@ class FeedbackDropdownContent extends StatelessWidget {
                         enabledBorder: border,
                         contentPadding: const EdgeInsets.all(kMdSpacing),
                       ),
-                      cursorColor: ThemeColors.of(context).lightGreyColor,
+                      cursorColor: borderColor,
                       cursorWidth: kCursorSize,
                       onFieldSubmitted: (String filterText) {},
                       maxLines: 3,
@@ -124,7 +125,7 @@ class FeedbackDropdownContent extends StatelessWidget {
               ],
             ),
           ),
-          const HorizontalDivider(),
+          const BeamDivider(),
           Padding(
             padding: const EdgeInsets.only(
               top: kXlSpacing,
@@ -141,7 +142,7 @@ class FeedbackDropdownContent extends StatelessWidget {
                     color: Theme.of(context).backgroundColor,
                     borderRadius: BorderRadius.circular(kSmBorderRadius),
                     border: Border.all(
-                      color: ThemeColors.of(context).lightGreyColor,
+                      color: borderColor,
                     ),
                   ),
                   child: TextButton(
diff --git a/playground/frontend/lib/pages/playground/components/more_actions.dart b/playground/frontend/lib/pages/playground/components/more_actions.dart
index 4fe7c9aa121..60f4e0ff9b3 100644
--- a/playground/frontend/lib/pages/playground/components/more_actions.dart
+++ b/playground/frontend/lib/pages/playground/components/more_actions.dart
@@ -19,11 +19,11 @@
 import 'package:flutter/material.dart';
 import 'package:flutter_gen/gen_l10n/app_localizations.dart';
 import 'package:flutter_svg/flutter_svg.dart';
-import 'package:playground/config/theme.dart';
 import 'package:playground/constants/assets.dart';
 import 'package:playground/constants/links.dart';
 import 'package:playground/modules/analytics/analytics_service.dart';
 import 'package:playground/modules/shortcuts/components/shortcuts_modal.dart';
+import 'package:playground_components/playground_components.dart';
 import 'package:url_launcher/url_launcher.dart';
 
 enum HeaderAction {
@@ -36,7 +36,11 @@ enum HeaderAction {
 }
 
 class MoreActions extends StatefulWidget {
-  const MoreActions({Key? key}) : super(key: key);
+  final PlaygroundController playgroundController;
+
+  const MoreActions({
+    required this.playgroundController,
+  });
 
   @override
   State<MoreActions> createState() => _MoreActionsState();
@@ -52,7 +56,7 @@ class _MoreActionsState extends State<MoreActions> {
       child: PopupMenuButton<HeaderAction>(
         icon: Icon(
           Icons.more_horiz_outlined,
-          color: ThemeColors.of(context).grey1Color,
+          color: Theme.of(context).extension<BeamThemeExtension>()?.iconColor,
         ),
         itemBuilder: (BuildContext context) => <PopupMenuEntry<HeaderAction>>[
           PopupMenuItem<HeaderAction>(
@@ -62,10 +66,12 @@ class _MoreActionsState extends State<MoreActions> {
               leading: SvgPicture.asset(kShortcutsIconAsset),
               title: Text(appLocale.shortcuts),
               onTap: () {
-              AnalyticsService.get(context).trackOpenShortcutsModal();
-              showDialog<void>(
+                AnalyticsService.get(context).trackOpenShortcutsModal();
+                showDialog<void>(
                   context: context,
-                  builder: (BuildContext context) => const ShortcutsModal(),
+                  builder: (BuildContext context) => ShortcutsModal(
+                    playgroundController: widget.playgroundController,
+                  ),
                 );
               },
             ),
diff --git a/playground/frontend/lib/pages/playground/components/playground_page_body.dart b/playground/frontend/lib/pages/playground/components/playground_page_body.dart
index 8a723982ed4..63d918ae292 100644
--- a/playground/frontend/lib/pages/playground/components/playground_page_body.dart
+++ b/playground/frontend/lib/pages/playground/components/playground_page_body.dart
@@ -17,14 +17,12 @@
  */
 
 import 'package:flutter/material.dart';
-import 'package:playground/components/split_view/split_view.dart';
-import 'package:playground/config/theme.dart';
 import 'package:playground/constants/sizes.dart';
-import 'package:playground/modules/output/components/output.dart';
+import 'package:playground/modules/output/components/output_header/output_placements.dart';
 import 'package:playground/modules/output/models/output_placement.dart';
 import 'package:playground/modules/output/models/output_placement_state.dart';
 import 'package:playground/pages/playground/components/editor_textarea_wrapper.dart';
-import 'package:playground/pages/playground/states/playground_state.dart';
+import 'package:playground_components/playground_components.dart';
 import 'package:provider/provider.dart';
 
 class PlaygroundPageBody extends StatelessWidget {
@@ -32,30 +30,34 @@ class PlaygroundPageBody extends StatelessWidget {
 
   @override
   Widget build(BuildContext context) {
-    return Consumer2<OutputPlacementState, PlaygroundState>(
+    return Consumer2<OutputPlacementState, PlaygroundController>(
         builder: (context, outputState, playgroundState, child) {
-      final output = createOutput(playgroundState);
+      final output = OutputWidget(
+        graphDirection: outputState.placement.graphDirection,
+        playgroundController: playgroundState,
+        trailing: const OutputPlacements(),
+      );
+
       switch (outputState.placement) {
         case OutputPlacement.bottom:
           return SplitView(
-            direction: SplitViewDirection.vertical,
+            direction: Axis.vertical,
             first: codeTextArea,
             second: output,
-            dividerSize: kMdSpacing,
           );
+
         case OutputPlacement.left:
           return SplitView(
-            direction: SplitViewDirection.horizontal,
+            direction: Axis.horizontal,
             first: output,
             second: codeTextArea,
-            dividerSize: kMdSpacing,
           );
+
         case OutputPlacement.right:
           return SplitView(
-            direction: SplitViewDirection.horizontal,
+            direction: Axis.horizontal,
             first: codeTextArea,
             second: output,
-            dividerSize: kMdSpacing,
           );
       }
     });
@@ -63,18 +65,13 @@ class PlaygroundPageBody extends StatelessWidget {
 
   Widget get codeTextArea => const CodeTextAreaWrapper();
 
-  Widget createOutput(PlaygroundState state) => Output(
-        isEmbedded: false,
-        playgroundState: state,
-      );
-
   Widget getVerticalSeparator(BuildContext context) => Container(
         width: kMdSpacing,
-        color: ThemeColors.of(context).divider,
+        color: Theme.of(context).dividerColor,
       );
 
   Widget getHorizontalSeparator(BuildContext context) => Container(
         height: kMdSpacing,
-        color: ThemeColors.of(context).divider,
+        color: Theme.of(context).dividerColor,
       );
 }
diff --git a/playground/frontend/lib/pages/playground/components/playground_page_footer.dart b/playground/frontend/lib/pages/playground/components/playground_page_footer.dart
index 7e6b6bcf799..b97bafad949 100644
--- a/playground/frontend/lib/pages/playground/components/playground_page_footer.dart
+++ b/playground/frontend/lib/pages/playground/components/playground_page_footer.dart
@@ -18,12 +18,12 @@
 
 import 'package:flutter/material.dart';
 import 'package:flutter_gen/gen_l10n/app_localizations.dart';
-import 'package:playground/config/theme.dart';
 import 'package:playground/constants/font_weight.dart';
 import 'package:playground/constants/links.dart';
 import 'package:playground/constants/sizes.dart';
 import 'package:playground/modules/analytics/analytics_service.dart';
 import 'package:playground/pages/playground/components/feedback/playground_feedback.dart';
+import 'package:playground_components/playground_components.dart';
 import 'package:url_launcher/url_launcher.dart';
 
 class PlaygroundPageFooter extends StatelessWidget {
@@ -34,7 +34,9 @@ class PlaygroundPageFooter extends StatelessWidget {
     AppLocalizations appLocale = AppLocalizations.of(context)!;
 
     return Container(
-      color: ThemeColors.of(context).secondaryBackground,
+      color: Theme.of(context)
+          .extension<BeamThemeExtension>()
+          ?.secondaryBackgroundColor,
       width: double.infinity,
       child: Padding(
         padding: const EdgeInsets.symmetric(
diff --git a/playground/frontend/lib/pages/playground/components/playground_page_providers.dart b/playground/frontend/lib/pages/playground/components/playground_page_providers.dart
index 9b809c9c262..a5e89130b05 100644
--- a/playground/frontend/lib/pages/playground/components/playground_page_providers.dart
+++ b/playground/frontend/lib/pages/playground/components/playground_page_providers.dart
@@ -17,26 +17,19 @@
  */
 
 import 'package:flutter/material.dart';
+import 'package:playground/config.g.dart';
+import 'package:playground/constants/params.dart';
 import 'package:playground/modules/analytics/analytics_service.dart';
 import 'package:playground/modules/analytics/google_analytics_service.dart';
-import 'package:playground/modules/editor/repository/code_repository/code_client/grpc_code_client.dart';
-import 'package:playground/modules/editor/repository/code_repository/code_repository.dart';
-import 'package:playground/modules/examples/repositories/example_client/grpc_example_client.dart';
-import 'package:playground/modules/examples/repositories/example_repository.dart';
+import 'package:playground/modules/examples/models/example_loading_descriptors/examples_loading_descriptor_factory.dart';
 import 'package:playground/modules/messages/handlers/messages_debouncer.dart';
 import 'package:playground/modules/messages/handlers/messages_handler.dart';
 import 'package:playground/modules/messages/listeners/messages_listener.dart';
 import 'package:playground/modules/output/models/output_placement_state.dart';
-import 'package:playground/pages/playground/states/example_loaders/examples_loader.dart';
-import 'package:playground/pages/playground/states/examples_state.dart';
 import 'package:playground/pages/playground/states/feedback_state.dart';
-import 'package:playground/pages/playground/states/playground_state.dart';
+import 'package:playground_components/playground_components.dart';
 import 'package:provider/provider.dart';
 
-final CodeRepository kCodeRepository = CodeRepository(GrpcCodeClient());
-final ExampleRepository kExampleRepository =
-    ExampleRepository(GrpcExampleClient());
-
 class PlaygroundPageProviders extends StatelessWidget {
   final Widget child;
 
@@ -52,20 +45,47 @@ class PlaygroundPageProviders extends StatelessWidget {
         Provider<AnalyticsService>(
           create: (context) => GoogleAnalyticsService(),
         ),
-        ChangeNotifierProvider<PlaygroundState>(
+        ChangeNotifierProvider<PlaygroundController>(
           create: (context) {
-            final state = PlaygroundState(
+            final codeRepository = CodeRepository(
+              client: GrpcCodeClient(
+                url: kApiClientURL,
+                runnerUrlsById: {
+                  Sdk.java.id: kApiJavaClientURL,
+                  Sdk.go.id: kApiGoClientURL,
+                  Sdk.python.id: kApiPythonClientURL,
+                  Sdk.scio.id: kApiScioClientURL,
+                },
+              ),
+            );
+
+            final exampleRepository = ExampleRepository(
+              client: GrpcExampleClient(url: kApiClientURL),
+            );
+
+            final exampleCache = ExampleCache(
+              exampleRepository: exampleRepository,
+              hasCatalog: !isEmbedded(),
+            )..init();
+
+            final controller = PlaygroundController(
               examplesLoader: ExamplesLoader(),
-              exampleState: ExampleState(kExampleRepository)..init(),
-              codeRepository: kCodeRepository,
+              exampleCache: exampleCache,
+              codeRepository: codeRepository,
+            );
+
+            final descriptor = ExamplesLoadingDescriptorFactory.fromUriParts(
+              path: Uri.base.path,
+              params: Uri.base.queryParameters,
             );
+            controller.examplesLoader.load(descriptor);
 
             final handler = MessagesDebouncer(
-              handler: MessagesHandler(playgroundState: state),
+              handler: MessagesHandler(playgroundController: controller),
             );
             MessagesListener(handler: handler);
 
-            return state;
+            return controller;
           },
         ),
         ChangeNotifierProvider<OutputPlacementState>(
diff --git a/playground/frontend/lib/pages/playground/playground_page.dart b/playground/frontend/lib/pages/playground/playground_page.dart
index 7f68817f3e4..a7a66f02d5a 100644
--- a/playground/frontend/lib/pages/playground/playground_page.dart
+++ b/playground/frontend/lib/pages/playground/playground_page.dart
@@ -18,7 +18,6 @@
 
 import 'package:flutter/material.dart';
 import 'package:playground/components/logo/logo_component.dart';
-import 'package:playground/components/toggle_theme_button/toggle_theme_button.dart';
 import 'package:playground/constants/sizes.dart';
 import 'package:playground/modules/actions/components/new_example_action.dart';
 import 'package:playground/modules/actions/components/reset_action.dart';
@@ -33,7 +32,7 @@ import 'package:playground/pages/playground/components/close_listener_nonweb.dar
 import 'package:playground/pages/playground/components/more_actions.dart';
 import 'package:playground/pages/playground/components/playground_page_body.dart';
 import 'package:playground/pages/playground/components/playground_page_footer.dart';
-import 'package:playground/pages/playground/states/playground_state.dart';
+import 'package:playground_components/playground_components.dart';
 import 'package:provider/provider.dart';
 
 class PlaygroundPage extends StatelessWidget {
@@ -42,56 +41,65 @@ class PlaygroundPage extends StatelessWidget {
   @override
   Widget build(BuildContext context) {
     return CloseListener(
-      child: ShortcutsManager(
-        shortcuts: globalShortcuts,
-        child: Scaffold(
-          appBar: AppBar(
-            automaticallyImplyLeading: false,
-            title: Consumer<PlaygroundState>(
-              builder: (context, state, child) {
-                final controller = state.snippetEditingController;
+      child: Consumer<PlaygroundController>(
+        builder: (context, controller, child) {
+          final snippetController = controller.snippetEditingController;
 
-                return Wrap(
+          return ShortcutsManager(
+            shortcuts: [
+              ...controller.shortcuts,
+              ...globalShortcuts,
+            ],
+            child: Scaffold(
+              appBar: AppBar(
+                automaticallyImplyLeading: false,
+                title: Wrap(
                   crossAxisAlignment: WrapCrossAlignment.center,
                   spacing: kXlSpacing,
                   children: [
                     const Logo(),
                     ExampleSelector(
                       changeSelectorVisibility:
-                          state.exampleState.changeSelectorVisibility,
-                      isSelectorOpened: state.exampleState.isSelectorOpened,
+                          controller.exampleCache.changeSelectorVisibility,
+                      isSelectorOpened:
+                          controller.exampleCache.isSelectorOpened,
                     ),
                     SDKSelector(
-                      value: state.sdk,
+                      value: controller.sdk,
                       onChanged: (newSdk) {
                         AnalyticsService.get(context)
-                            .trackSelectSdk(state.sdk, newSdk);
-                        state.setSdk(newSdk);
+                            .trackSelectSdk(controller.sdk, newSdk);
+                        controller.setSdk(newSdk);
                       },
                     ),
-                    if (controller != null)
+                    if (snippetController != null)
                       PipelineOptionsDropdown(
-                        pipelineOptions: controller.pipelineOptions,
-                        setPipelineOptions: state.setPipelineOptions,
+                        pipelineOptions: snippetController.pipelineOptions,
+                        setPipelineOptions: controller.setPipelineOptions,
                       ),
                     const NewExampleAction(),
-                    ResetAction(reset: state.reset),
+                    const ResetAction(),
                   ],
-                );
-              },
-            ),
-            actions: const [ToggleThemeButton(), MoreActions()],
-          ),
-          body: Column(
-            children: [
-              const Expanded(child: PlaygroundPageBody()),
-              Semantics(
-                container: true,
-                child: const PlaygroundPageFooter(),
+                ),
+                actions: [
+                  const ToggleThemeButton(),
+                  MoreActions(
+                    playgroundController: controller,
+                  ),
+                ],
               ),
-            ],
-          ),
-        ),
+              body: Column(
+                children: [
+                  const Expanded(child: PlaygroundPageBody()),
+                  Semantics(
+                    container: true,
+                    child: const PlaygroundPageFooter(),
+                  ),
+                ],
+              ),
+            ),
+          );
+        },
       ),
     );
   }
diff --git a/playground/frontend/lib/pages/playground/states/example_loaders/examples_loader.dart b/playground/frontend/lib/pages/playground/states/example_loaders/examples_loader.dart
deleted file mode 100644
index ad4efacd130..00000000000
--- a/playground/frontend/lib/pages/playground/states/example_loaders/examples_loader.dart
+++ /dev/null
@@ -1,110 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License.  You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-import 'package:playground/modules/examples/models/example_loading_descriptors/catalog_default_example_loading_descriptor.dart';
-import 'package:playground/modules/examples/models/example_loading_descriptors/content_example_loading_descriptor.dart';
-import 'package:playground/modules/examples/models/example_loading_descriptors/empty_example_loading_descriptor.dart';
-import 'package:playground/modules/examples/models/example_loading_descriptors/example_loading_descriptor.dart';
-import 'package:playground/modules/examples/models/example_loading_descriptors/examples_loading_descriptor.dart';
-import 'package:playground/modules/examples/models/example_loading_descriptors/standard_example_loading_descriptor.dart';
-import 'package:playground/modules/examples/models/example_loading_descriptors/user_shared_example_loading_descriptor.dart';
-import 'package:playground/pages/playground/states/example_loaders/catalog_default_example_loader.dart';
-import 'package:playground/pages/playground/states/example_loaders/content_example_loader.dart';
-import 'package:playground/pages/playground/states/example_loaders/empty_example_loader.dart';
-import 'package:playground/pages/playground/states/example_loaders/example_loader.dart';
-import 'package:playground/pages/playground/states/example_loaders/standard_example_loader.dart';
-import 'package:playground/pages/playground/states/example_loaders/user_shared_example_loader.dart';
-import 'package:playground/pages/playground/states/playground_state.dart';
-
-class ExamplesLoader {
-  PlaygroundState? _playgroundState;
-  ExamplesLoadingDescriptor? _descriptor;
-
-  void setPlaygroundState(PlaygroundState value) {
-    _playgroundState = value;
-  }
-
-  Future<void> load(ExamplesLoadingDescriptor descriptor) async {
-    if (_descriptor == descriptor) {
-      return;
-    }
-
-    _descriptor = descriptor;
-    await Future.wait(
-      descriptor.descriptors.map(
-        (one) => loadOne(group: descriptor, one: one),
-      ),
-    );
-
-    final sdk = descriptor.initialSdk;
-    if (sdk != null) {
-      _playgroundState!.setSdk(sdk);
-    }
-  }
-
-  Future<void> loadOne({
-    required ExamplesLoadingDescriptor group,
-    required ExampleLoadingDescriptor one,
-  }) async {
-    final example = await _getOneLoader(one).future;
-    _playgroundState!.setExample(
-      example,
-      setCurrentSdk:
-          example.sdk == group.initialSdk || group.initialSdk == null,
-    );
-  }
-
-  ExampleLoader _getOneLoader(ExampleLoadingDescriptor descriptor) {
-    final exampleState = _playgroundState!.exampleState;
-
-    if (descriptor is CatalogDefaultExampleLoadingDescriptor) {
-      return CatalogDefaultExampleLoader(
-        descriptor: descriptor,
-        exampleState: exampleState,
-      );
-    }
-
-    if (descriptor is ContentExampleLoadingDescriptor) {
-      return ContentExampleLoader(
-        descriptor: descriptor,
-      );
-    }
-
-    if (descriptor is EmptyExampleLoadingDescriptor) {
-      return EmptyExampleLoader(
-        descriptor: descriptor,
-      );
-    }
-
-    if (descriptor is StandardExampleLoadingDescriptor) {
-      return StandardExampleLoader(
-        descriptor: descriptor,
-        exampleState: exampleState,
-      );
-    }
-
-    if (descriptor is UserSharedExampleLoadingDescriptor) {
-      return UserSharedExampleLoader(
-        descriptor: descriptor,
-        exampleState: exampleState,
-      );
-    }
-
-    throw Exception('Unknown example loading descriptor: $descriptor');
-  }
-}
diff --git a/playground/frontend/lib/pages/playground/states/example_selector_state.dart b/playground/frontend/lib/pages/playground/states/example_selector_state.dart
index 2f6f462f7f0..f62a1773012 100644
--- a/playground/frontend/lib/pages/playground/states/example_selector_state.dart
+++ b/playground/frontend/lib/pages/playground/states/example_selector_state.dart
@@ -17,18 +17,16 @@
  */
 
 import 'package:flutter/material.dart';
-import 'package:playground/modules/examples/models/category_model.dart';
-import 'package:playground/modules/examples/models/example_model.dart';
-import 'package:playground/pages/playground/states/playground_state.dart';
+import 'package:playground_components/playground_components.dart';
 
 class ExampleSelectorState with ChangeNotifier {
-  final PlaygroundState _playgroundState;
+  final PlaygroundController _playgroundController;
   ExampleType _selectedFilterType;
   String _filterText;
-  List<CategoryModel> categories;
+  List<CategoryWithExamples> categories;
 
   ExampleSelectorState(
-    this._playgroundState,
+    this._playgroundController,
     this.categories, [
     this._selectedFilterType = ExampleType.all,
     this._filterText = '',
@@ -48,26 +46,26 @@ class ExampleSelectorState with ChangeNotifier {
     notifyListeners();
   }
 
-  void setCategories(List<CategoryModel>? categories) {
-    this.categories = categories ?? [];
+  void setCategories(List<CategoryWithExamples> categories) {
+    this.categories = categories;
     notifyListeners();
   }
 
   void sortCategories() {
-    final categories = _playgroundState.exampleState.getCategories(
-      _playgroundState.sdk,
+    final categories = _playgroundController.exampleCache.getCategories(
+      _playgroundController.sdk,
     );
 
     final sortedCategories = categories
-        .map((category) => CategoryModel(
-            name: category.name,
+        .map((category) => CategoryWithExamples(
+            title: category.title,
             examples: _sortCategoryExamples(category.examples)))
         .where((category) => category.examples.isNotEmpty)
         .toList();
     setCategories(sortedCategories);
   }
 
-  List<ExampleModel> _sortCategoryExamples(List<ExampleModel> examples) {
+  List<ExampleBase> _sortCategoryExamples(List<ExampleBase> examples) {
     final isAllFilterType = selectedFilterType == ExampleType.all;
     final isFilterTextEmpty = filterText.isEmpty;
     if (isAllFilterType && isFilterTextEmpty) {
@@ -89,15 +87,15 @@ class ExampleSelectorState with ChangeNotifier {
     return sortExamplesByName(sorted, filterText);
   }
 
-  List<ExampleModel> sortExamplesByType(
-    List<ExampleModel> examples,
+  List<ExampleBase> sortExamplesByType(
+    List<ExampleBase> examples,
     ExampleType type,
   ) {
     return examples.where((element) => element.type == type).toList();
   }
 
-  List<ExampleModel> sortExamplesByName(
-    List<ExampleModel> examples,
+  List<ExampleBase> sortExamplesByName(
+    List<ExampleBase> examples,
     String name,
   ) {
     return examples
diff --git a/playground/frontend/lib/pages/playground/states/examples_state.dart b/playground/frontend/lib/pages/playground/states/examples_state.dart
deleted file mode 100644
index 594e74a7a1b..00000000000
--- a/playground/frontend/lib/pages/playground/states/examples_state.dart
+++ /dev/null
@@ -1,218 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License.  You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-import 'dart:async';
-
-import 'package:collection/collection.dart';
-import 'package:flutter/material.dart';
-import 'package:playground/constants/params.dart';
-import 'package:playground/modules/examples/models/category_model.dart';
-import 'package:playground/modules/examples/models/example_model.dart';
-import 'package:playground/modules/examples/repositories/example_repository.dart';
-import 'package:playground/modules/examples/repositories/models/get_snippet_request.dart';
-import 'package:playground/modules/examples/repositories/models/get_snippet_response.dart';
-import 'package:playground/modules/examples/repositories/models/get_example_request.dart';
-import 'package:playground/modules/examples/repositories/models/get_list_of_examples_request.dart';
-import 'package:playground/modules/examples/repositories/models/save_snippet_request.dart';
-import 'package:playground/modules/examples/repositories/models/shared_file_model.dart';
-import 'package:playground/modules/sdk/models/sdk.dart';
-
-class ExampleState with ChangeNotifier {
-  final ExampleRepository _exampleRepository;
-  Map<SDK, List<CategoryModel>>? sdkCategories;
-  Map<SDK, ExampleModel> defaultExamplesMap = {};
-  ExampleModel? defaultExample;
-  bool isSelectorOpened = false;
-
-  final _allExamplesCompleter = Completer<void>();
-
-  Future<void> get allExamplesFuture => _allExamplesCompleter.future;
-
-  bool get hasExampleCatalog => !isEmbedded();
-
-  ExampleState(this._exampleRepository);
-
-  Future<void> init() async {
-    if (hasExampleCatalog) {
-      await Future.wait([
-        _loadCategories(),
-        loadDefaultExamplesIfNot(),
-      ]);
-    }
-  }
-
-  void setSdkCategories(Map<SDK, List<CategoryModel>> map) {
-    sdkCategories = map;
-    _allExamplesCompleter.complete();
-  }
-
-  List<CategoryModel> getCategories(SDK? sdk) {
-    return sdkCategories?[sdk] ?? [];
-  }
-
-  Future<String> getExampleOutput(String id, SDK sdk) async {
-    return _exampleRepository.getExampleOutput(
-      GetExampleRequestWrapper(id, sdk),
-    );
-  }
-
-  Future<String> getExampleSource(String id, SDK sdk) async {
-    return _exampleRepository.getExampleSource(
-      GetExampleRequestWrapper(id, sdk),
-    );
-  }
-
-  Future<ExampleModel> getExample(String path, SDK sdk) async {
-    return _exampleRepository.getExample(
-      GetExampleRequestWrapper(path, sdk),
-    );
-  }
-
-  Future<String> getExampleLogs(String id, SDK sdk) async {
-    return _exampleRepository.getExampleLogs(
-      GetExampleRequestWrapper(id, sdk),
-    );
-  }
-
-  Future<String> getExampleGraph(String id, SDK sdk) async {
-    return _exampleRepository.getExampleGraph(
-      GetExampleRequestWrapper(id, sdk),
-    );
-  }
-
-  Future<ExampleModel> loadSharedExample(String id) async {
-    GetSnippetResponse result = await _exampleRepository.getSnippet(
-      GetSnippetRequestWrapper(id: id),
-    );
-    return ExampleModel(
-      sdk: result.sdk,
-      name: result.files.first.name,
-      path: id,
-      description: '',
-      type: ExampleType.example,
-      source: result.files.first.code,
-      pipelineOptions: result.pipelineOptions,
-    );
-  }
-
-  Future<String> getSnippetId({
-    required List<SharedFile> files,
-    required SDK sdk,
-    required String pipelineOptions,
-  }) async {
-    String id = await _exampleRepository.saveSnippet(SaveSnippetRequestWrapper(
-      files: files,
-      sdk: sdk,
-      pipelineOptions: pipelineOptions,
-    ));
-    return id;
-  }
-
-  Future<ExampleModel> loadExampleInfo(ExampleModel example) async {
-    if (example.isInfoFetched()) {
-      return example;
-    }
-
-    //GRPC GetPrecompiledGraph errors hotfix
-    if (example.name == 'MinimalWordCount' &&
-        (example.sdk == SDK.go || example.sdk == SDK.scio)) {
-      final exampleData = await Future.wait([
-        getExampleSource(example.path, example.sdk),
-        getExampleOutput(example.path, example.sdk),
-        getExampleLogs(example.path, example.sdk),
-      ]);
-      example.setSource(exampleData[0]);
-      example.setOutputs(exampleData[1]);
-      example.setLogs(exampleData[2]);
-      return example;
-    }
-
-    final exampleData = await Future.wait([
-      getExampleSource(example.path, example.sdk),
-      getExampleOutput(example.path, example.sdk),
-      getExampleLogs(example.path, example.sdk),
-      getExampleGraph(example.path, example.sdk)
-    ]);
-    example.setSource(exampleData[0]);
-    example.setOutputs(exampleData[1]);
-    example.setLogs(exampleData[2]);
-    example.setGraph(exampleData[3]);
-    return example;
-  }
-
-  Future<void> _loadCategories() {
-    return _exampleRepository
-        .getListOfExamples(
-          GetListOfExamplesRequestWrapper(sdk: null, category: null),
-        )
-        .then((map) => setSdkCategories(map));
-  }
-
-  void changeSelectorVisibility() {
-    isSelectorOpened = !isSelectorOpened;
-    notifyListeners();
-  }
-
-  Future<void> loadDefaultExamples() async {
-    if (defaultExamplesMap.isNotEmpty) {
-      return;
-    }
-
-    try {
-      await Future.wait(SDK.values.map(_loadDefaultExample));
-    } catch (ex) {
-      if (defaultExamplesMap.isEmpty) {
-        rethrow;
-      }
-      // As long as any of the examples is loaded, continue.
-      print(ex);
-      // TODO: Log.
-    }
-
-    notifyListeners();
-  }
-
-  Future<void> _loadDefaultExample(SDK sdk) async {
-    final exampleWithoutInfo = await _exampleRepository.getDefaultExample(
-      // First parameter is an empty string, because we don't need path to get the default example.
-      GetExampleRequestWrapper('', sdk),
-    );
-
-    defaultExamplesMap[sdk] = await loadExampleInfo(exampleWithoutInfo);
-  }
-
-  Future<void> loadDefaultExamplesIfNot() async {
-    if (defaultExamplesMap.isNotEmpty) {
-      return;
-    }
-
-    await loadDefaultExamples();
-  }
-
-  Future<ExampleModel?> getCatalogExampleByPath(String path) async {
-    await allExamplesFuture;
-
-    final allExamples = sdkCategories?.values
-        .expand((sdkCategory) => sdkCategory.map((e) => e.examples))
-        .expand((element) => element);
-
-    return allExamples?.firstWhereOrNull(
-      (e) => e.path == path,
-    );
-  }
-}
diff --git a/playground/frontend/lib/playground_app.dart b/playground/frontend/lib/playground_app.dart
index 8c5f2a489e6..867bbad7f4b 100644
--- a/playground/frontend/lib/playground_app.dart
+++ b/playground/frontend/lib/playground_app.dart
@@ -16,16 +16,16 @@
  * limitations under the License.
  */
 
-import 'package:code_text_field/code_text_field.dart';
+import 'package:easy_localization/easy_localization.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter_gen/gen_l10n/app_localizations.dart';
 import 'package:flutter_localizations/flutter_localizations.dart';
 import 'package:playground/config/locale.dart';
-import 'package:playground/config/theme.dart';
 import 'package:playground/l10n/l10n.dart';
 import 'package:playground/pages/playground/components/playground_page_providers.dart';
 import 'package:playground/pages/playground/playground_page.dart';
 import 'package:playground/pages/routes.dart';
+import 'package:playground_components/playground_components.dart';
 import 'package:provider/provider.dart';
 
 class PlaygroundApp extends StatelessWidget {
@@ -36,32 +36,30 @@ class PlaygroundApp extends StatelessWidget {
     return ThemeSwitchNotifierProvider(
       child: Consumer<ThemeSwitchNotifier>(
         builder: (context, themeSwitchNotifier, _) {
-          return CodeTheme(
-            data: themeSwitchNotifier.codeTheme,
-            child: ChangeNotifierProvider<LocaleProvider>(
-              create: (context) => LocaleProvider(),
-              builder: (context, state) {
-                final localeProvider = Provider.of<LocaleProvider>(context);
-                return PlaygroundPageProviders(
-                  child: MaterialApp(
-                    title: 'Apache Beam Playground',
-                    themeMode: themeSwitchNotifier.themeMode,
-                    theme: kLightTheme,
-                    darkTheme: kDarkTheme,
-                    onGenerateRoute: Routes.generateRoute,
-                    home: const PlaygroundPage(),
-                    debugShowCheckedModeBanner: false,
-                    locale: localeProvider.locale,
-                    supportedLocales: L10n.locales,
-                    localizationsDelegates: const [
-                      AppLocalizations.delegate,
-                      GlobalMaterialLocalizations.delegate,
-                      GlobalWidgetsLocalizations.delegate,
-                    ],
-                  ),
-                );
-              },
-            ),
+          return ChangeNotifierProvider<LocaleProvider>(
+            create: (context) => LocaleProvider(),
+            builder: (context, state) {
+              final localeProvider = Provider.of<LocaleProvider>(context);
+              return PlaygroundPageProviders(
+                child: MaterialApp(
+                  title: 'Apache Beam Playground',
+                  themeMode: themeSwitchNotifier.themeMode,
+                  theme: kLightTheme,
+                  darkTheme: kDarkTheme,
+                  onGenerateRoute: Routes.generateRoute,
+                  home: const PlaygroundPage(),
+                  debugShowCheckedModeBanner: false,
+                  locale: localeProvider.locale,
+                  supportedLocales: L10n.locales,
+                  localizationsDelegates: [
+                    ...context.localizationDelegates,
+                    AppLocalizations.delegate,
+                    GlobalMaterialLocalizations.delegate,
+                    GlobalWidgetsLocalizations.delegate,
+                  ],
+                ),
+              );
+            },
           );
         },
       ),
diff --git a/playground/frontend/lib/utils/analytics_utils.dart b/playground/frontend/lib/utils/analytics_utils.dart
index ab77fc23d96..786b54ff382 100644
--- a/playground/frontend/lib/utils/analytics_utils.dart
+++ b/playground/frontend/lib/utils/analytics_utils.dart
@@ -16,13 +16,12 @@
  * limitations under the License.
  */
 
-import 'package:playground/modules/sdk/models/sdk.dart';
-import 'package:playground/pages/playground/states/playground_state.dart';
+import 'package:playground_components/playground_components.dart';
 
-String getAnalyticsExampleName(PlaygroundState state) {
-  final customCodeName = 'Custom code, sdk ${state.sdk?.displayName}';
-  if (state.isExampleChanged) {
+String getAnalyticsExampleName(PlaygroundController controller) {
+  final customCodeName = 'Custom code, sdk ${controller.sdk?.title}';
+  if (controller.isExampleChanged) {
     return customCodeName;
   }
-  return state.selectedExample?.path ?? customCodeName;
+  return controller.selectedExample?.path ?? customCodeName;
 }
diff --git a/playground/frontend/assets/reset.svg b/playground/frontend/playground_components/assets/buttons/reset.svg
similarity index 100%
copy from playground/frontend/assets/reset.svg
copy to playground/frontend/playground_components/assets/buttons/reset.svg
diff --git a/learning/tour-of-beam/frontend/assets/svg/theme-mode.svg b/playground/frontend/playground_components/assets/buttons/theme-mode.svg
similarity index 100%
rename from learning/tour-of-beam/frontend/assets/svg/theme-mode.svg
rename to playground/frontend/playground_components/assets/buttons/theme-mode.svg
diff --git a/playground/frontend/assets/error_notification.svg b/playground/frontend/playground_components/assets/notification_icons/error.svg
similarity index 100%
rename from playground/frontend/assets/error_notification.svg
rename to playground/frontend/playground_components/assets/notification_icons/error.svg
diff --git a/playground/frontend/assets/info_notification.svg b/playground/frontend/playground_components/assets/notification_icons/info.svg
similarity index 100%
rename from playground/frontend/assets/info_notification.svg
rename to playground/frontend/playground_components/assets/notification_icons/info.svg
diff --git a/playground/frontend/assets/success_notification.svg b/playground/frontend/playground_components/assets/notification_icons/success.svg
similarity index 100%
rename from playground/frontend/assets/success_notification.svg
rename to playground/frontend/playground_components/assets/notification_icons/success.svg
diff --git a/playground/frontend/assets/warning_notification.svg b/playground/frontend/playground_components/assets/notification_icons/warning.svg
similarity index 100%
rename from playground/frontend/assets/warning_notification.svg
rename to playground/frontend/playground_components/assets/notification_icons/warning.svg
diff --git a/learning/tour-of-beam/frontend/assets/png/beam-logo.png b/playground/frontend/playground_components/assets/png/beam-logo.png
similarity index 100%
rename from learning/tour-of-beam/frontend/assets/png/beam-logo.png
rename to playground/frontend/playground_components/assets/png/beam-logo.png
diff --git a/playground/frontend/assets/reset.svg b/playground/frontend/playground_components/assets/svg/drag-horizontal.svg
similarity index 57%
copy from playground/frontend/assets/reset.svg
copy to playground/frontend/playground_components/assets/svg/drag-horizontal.svg
index 5b355a07d37..f5e8dcda558 100644
--- a/playground/frontend/assets/reset.svg
+++ b/playground/frontend/playground_components/assets/svg/drag-horizontal.svg
@@ -17,8 +17,7 @@
     under the License.
 -->
 
-<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
-    <path
-        d="M18.4386 16.8504C19.7186 15.1478 20.2697 13.0064 19.9708 10.8975C19.672 8.7885 18.5475 6.88464 16.8449 5.60471C15.1423 4.32478 13.001 3.77363 10.892 4.07251C8.78306 4.37139 6.87919 5.49581 5.59926 7.19842M9.89763 8.35016L5.068 7.05607L6.3621 2.22644M15.0453 19.4301L15.0357 19.4276M10.9299 19.9526L10.9202 19.95M7.11712 18.3408L7.10746 18.3382M4.61334 15.0509L4.60368 15.0483M4.09071 10.9353L4.08105 10.9327"
-        stroke="#A0A4AB" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" />
+<svg width="4" height="30" viewBox="0 0 4 30" fill="none" xmlns="http://www.w3.org/2000/svg">
+<rect width="30" height="1" rx="0.5" transform="matrix(-4.37114e-08 -1 -1 4.37114e-08 4 30)" fill="#A0A4AB"/>
+<rect width="30" height="1" rx="0.5" transform="matrix(-4.37114e-08 -1 -1 4.37114e-08 1 30)" fill="#A0A4AB"/>
 </svg>
diff --git a/playground/frontend/assets/reset.svg b/playground/frontend/playground_components/assets/svg/drag-vertical.svg
similarity index 57%
rename from playground/frontend/assets/reset.svg
rename to playground/frontend/playground_components/assets/svg/drag-vertical.svg
index 5b355a07d37..fea5377776e 100644
--- a/playground/frontend/assets/reset.svg
+++ b/playground/frontend/playground_components/assets/svg/drag-vertical.svg
@@ -17,8 +17,7 @@
     under the License.
 -->
 
-<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
-    <path
-        d="M18.4386 16.8504C19.7186 15.1478 20.2697 13.0064 19.9708 10.8975C19.672 8.7885 18.5475 6.88464 16.8449 5.60471C15.1423 4.32478 13.001 3.77363 10.892 4.07251C8.78306 4.37139 6.87919 5.49581 5.59926 7.19842M9.89763 8.35016L5.068 7.05607L6.3621 2.22644M15.0453 19.4301L15.0357 19.4276M10.9299 19.9526L10.9202 19.95M7.11712 18.3408L7.10746 18.3382M4.61334 15.0509L4.60368 15.0483M4.09071 10.9353L4.08105 10.9327"
-        stroke="#A0A4AB" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" />
+<svg width="30" height="4" viewBox="0 0 30 4" fill="none" xmlns="http://www.w3.org/2000/svg">
+<rect width="30" height="1" rx="0.5" transform="matrix(1 0 0 -1 0 4)" fill="#A0A4AB"/>
+<rect width="30" height="1" rx="0.5" transform="matrix(1 0 0 -1 0 1)" fill="#A0A4AB"/>
 </svg>
diff --git a/playground/frontend/playground_components/pubspec.yaml b/playground/frontend/playground_components/assets/translations/en.yaml
similarity index 60%
copy from playground/frontend/playground_components/pubspec.yaml
copy to playground/frontend/playground_components/assets/translations/en.yaml
index 56037dea769..f80677c598a 100644
--- a/playground/frontend/playground_components/pubspec.yaml
+++ b/playground/frontend/playground_components/assets/translations/en.yaml
@@ -15,20 +15,38 @@
 #  specific language governing permissions and limitations
 #  under the License.
 
-name: playground_components
-description: Reusable Playground components
-version: 0.0.1
+ui:
+  darkMode: 'Dark Mode'
+  lightMode: 'Light Mode'
 
-environment:
-  sdk: '>=2.17.6 <3.0.0'
-  flutter: '>=1.17.0'
+intents:
+  playground:
+    run: 'Run Code'
+    reset: 'Reset Code'
 
-dependencies:
-  flutter: { sdk: flutter }
-  total_lints: ^2.17.4
+widgets:
 
-dev_dependencies:
-  flutter_lints: ^2.0.0
-  flutter_test: { sdk: flutter }
+  codeEditor:
+    label: 'Code Text Area'
+
+  output:
+    filter:
+      all: 'All'
+      log: 'Log'
+      output: 'Output'
+    filterTitle: 'Display at this tab'
+    graph: 'Graph'
+    result: 'Result'
+
+
+  resetButton:
+    label: 'Reset'
+
+  runOrCancelButton:
+    titles:
+      run: 'Run'
+      cancel: 'Cancel'
+    notificationTitles:
+      run: 'Run Code'
+      cancelExecution: 'Cancel Execution'
 
-flutter:
diff --git a/playground/frontend/playground_components/build.gradle.kts b/playground/frontend/playground_components/build.gradle.kts
new file mode 100644
index 00000000000..bacc8daddec
--- /dev/null
+++ b/playground/frontend/playground_components/build.gradle.kts
@@ -0,0 +1,90 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * License); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an AS IS BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+description = "Apache Beam :: Playground :: playground_components Flutter Package"
+
+tasks.register("precommit") {
+  dependsOn("analyze")
+  dependsOn("test")
+}
+
+tasks.register("analyze") {
+  dependsOn("generate")
+
+  group = "verification"
+  description = "Run dart analyzer"
+
+  doLast {
+    exec {
+      executable("dart")
+      args("analyze", ".")
+    }
+  }
+}
+
+tasks.register("test") {
+  dependsOn("generate")
+
+  group = "verification"
+  description = "Run tests"
+
+  doLast {
+    exec {
+      executable("flutter")
+      args("test")
+    }
+  }
+}
+
+tasks.register("clean") {
+  group = "build"
+  description = "Remove build artifacts"
+  doLast {
+    exec {
+      executable("flutter")
+      args("clean")
+    }
+  }
+}
+
+tasks.register("pubGet") {
+  group = "build"
+  description = "Install dependencies"
+
+  doLast {
+    exec {
+      executable("flutter")
+      args("pub", "get")
+    }
+  }
+}
+
+tasks.register("generate") {
+  dependsOn("clean")
+  dependsOn("pubGet")
+
+  group = "build"
+  description = "Generate code"
+
+  doLast {
+    exec {
+      executable("flutter")
+      args("pub", "run", "build_runner", "build", "--delete-conflicting-outputs")
+    }
+  }
+}
diff --git a/playground/frontend/playground_components/lib/playground_components.dart b/playground/frontend/playground_components/lib/playground_components.dart
new file mode 100644
index 00000000000..a7201940a84
--- /dev/null
+++ b/playground/frontend/playground_components/lib/playground_components.dart
@@ -0,0 +1,73 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+export 'src/cache/example_cache.dart';
+
+export 'src/constants/colors.dart';
+export 'src/constants/links.dart';
+export 'src/constants/playground_components.dart';
+export 'src/constants/sizes.dart';
+
+export 'src/controllers/example_loaders/examples_loader.dart';
+export 'src/controllers/playground_controller.dart';
+
+export 'src/enums/complexity.dart';
+
+export 'src/models/category_with_examples.dart';
+export 'src/models/example.dart';
+export 'src/models/example_base.dart';
+export 'src/models/example_loading_descriptors/catalog_default_example_loading_descriptor.dart';
+export 'src/models/example_loading_descriptors/content_example_loading_descriptor.dart';
+export 'src/models/example_loading_descriptors/empty_example_loading_descriptor.dart';
+export 'src/models/example_loading_descriptors/example_loading_descriptor.dart';
+export 'src/models/example_loading_descriptors/examples_loading_descriptor.dart';
+export 'src/models/example_loading_descriptors/standard_example_loading_descriptor.dart';
+export 'src/models/example_loading_descriptors/user_shared_example_loading_descriptor.dart';
+export 'src/models/intents.dart';
+export 'src/models/outputs.dart';
+export 'src/models/sdk.dart';
+export 'src/models/shortcut.dart';
+
+export 'src/notifications/notification.dart';
+
+export 'src/repositories/code_client/grpc_code_client.dart';
+export 'src/repositories/code_repository.dart';
+export 'src/repositories/example_client/grpc_example_client.dart';
+export 'src/repositories/example_repository.dart';
+
+export 'src/theme/switch_notifier.dart';
+export 'src/theme/theme.dart';
+
+export 'src/util/pipeline_options.dart';
+
+export 'src/widgets/bubble.dart';
+export 'src/widgets/complexity.dart';
+export 'src/widgets/dismissible_overlay.dart';
+export 'src/widgets/divider.dart';
+export 'src/widgets/header_icon_button.dart';
+export 'src/widgets/loading_indicator.dart';
+export 'src/widgets/logo.dart';
+export 'src/widgets/output/output.dart';
+export 'src/widgets/reset_button.dart';
+export 'src/widgets/run_or_cancel_button.dart';
+export 'src/widgets/shortcut_tooltip.dart';
+export 'src/widgets/snippet_editor.dart';
+export 'src/widgets/split_view.dart';
+export 'src/widgets/tab_header.dart';
+export 'src/widgets/toggle_theme_button.dart';
+export 'src/widgets/toggle_theme_icon_button.dart';
diff --git a/playground/frontend/lib/configure_nonweb.dart b/playground/frontend/playground_components/lib/src/api/iis_workaround_channel.dart
similarity index 88%
rename from playground/frontend/lib/configure_nonweb.dart
rename to playground/frontend/playground_components/lib/src/api/iis_workaround_channel.dart
index 4568ded62f9..5c41c00d327 100644
--- a/playground/frontend/lib/configure_nonweb.dart
+++ b/playground/frontend/playground_components/lib/src/api/iis_workaround_channel.dart
@@ -16,6 +16,5 @@
  * limitations under the License.
  */
 
-void configureApp() {
-  // see https://flutter.dev/docs/development/ui/navigation/url-strategies
-}
+export 'iis_workaround_channel_non_web.dart'
+    if (dart.library.html) 'iis_workaround_channel_web.dart';
diff --git a/playground/frontend/lib/modules/examples/models/example_loading_descriptors/example_loading_descriptor.dart b/playground/frontend/playground_components/lib/src/api/iis_workaround_channel_non_web.dart
similarity index 75%
copy from playground/frontend/lib/modules/examples/models/example_loading_descriptors/example_loading_descriptor.dart
copy to playground/frontend/playground_components/lib/src/api/iis_workaround_channel_non_web.dart
index 4867544e505..39fb9d0ba38 100644
--- a/playground/frontend/lib/modules/examples/models/example_loading_descriptors/example_loading_descriptor.dart
+++ b/playground/frontend/playground_components/lib/src/api/iis_workaround_channel_non_web.dart
@@ -16,15 +16,15 @@
  * limitations under the License.
  */
 
-import 'package:playground/modules/examples/models/example_origin.dart';
+import 'package:grpc/grpc_connection_interface.dart';
 
-abstract class ExampleLoadingDescriptor {
-  const ExampleLoadingDescriptor();
+class IisWorkaroundChannel extends ClientChannelBase {
+  final Uri uri;
 
-  ExampleOrigin get origin;
+  IisWorkaroundChannel.xhr(this.uri) : super();
 
   @override
-  String toString() => '$origin';
-
-  Map<String, dynamic> toJson();
+  ClientConnection createConnection() {
+    throw UnimplementedError('This only works in web');
+  }
 }
diff --git a/playground/frontend/lib/api/iis_workaround_channel.dart b/playground/frontend/playground_components/lib/src/api/iis_workaround_channel_web.dart
similarity index 88%
rename from playground/frontend/lib/api/iis_workaround_channel.dart
rename to playground/frontend/playground_components/lib/src/api/iis_workaround_channel_web.dart
index 2232c59e503..b666677a643 100644
--- a/playground/frontend/lib/api/iis_workaround_channel.dart
+++ b/playground/frontend/playground_components/lib/src/api/iis_workaround_channel_web.dart
@@ -28,12 +28,12 @@ class IisWorkaroundChannel extends ClientChannelBase {
 
   @override
   ClientConnection createConnection() {
-    return IisClientConnection(uri);
+    return _IisClientConnection(uri);
   }
 }
 
-class IisClientConnection extends XhrClientConnection {
-  IisClientConnection(Uri uri) : super(uri);
+class _IisClientConnection extends XhrClientConnection {
+  _IisClientConnection(super.uri);
 
   @override
   GrpcTransportStream makeRequest(
@@ -43,7 +43,8 @@ class IisClientConnection extends XhrClientConnection {
     ErrorHandler onError, {
     CallOptions? callOptions,
   }) {
-    var pathWithoutFirstSlash = path.substring(1);
+    final pathWithoutFirstSlash = path.substring(1);
+
     return super.makeRequest(
       pathWithoutFirstSlash,
       timeout,
diff --git a/playground/frontend/lib/api/v1/api.pb.dart b/playground/frontend/playground_components/lib/src/api/v1/api.pb.dart
similarity index 100%
rename from playground/frontend/lib/api/v1/api.pb.dart
rename to playground/frontend/playground_components/lib/src/api/v1/api.pb.dart
diff --git a/playground/frontend/lib/api/v1/api.pbenum.dart b/playground/frontend/playground_components/lib/src/api/v1/api.pbenum.dart
similarity index 100%
rename from playground/frontend/lib/api/v1/api.pbenum.dart
rename to playground/frontend/playground_components/lib/src/api/v1/api.pbenum.dart
diff --git a/playground/frontend/lib/api/v1/api.pbgrpc.dart b/playground/frontend/playground_components/lib/src/api/v1/api.pbgrpc.dart
similarity index 100%
rename from playground/frontend/lib/api/v1/api.pbgrpc.dart
rename to playground/frontend/playground_components/lib/src/api/v1/api.pbgrpc.dart
diff --git a/playground/frontend/lib/api/v1/api.pbjson.dart b/playground/frontend/playground_components/lib/src/api/v1/api.pbjson.dart
similarity index 100%
rename from playground/frontend/lib/api/v1/api.pbjson.dart
rename to playground/frontend/playground_components/lib/src/api/v1/api.pbjson.dart
diff --git a/playground/frontend/playground_components/lib/src/cache/example_cache.dart b/playground/frontend/playground_components/lib/src/cache/example_cache.dart
new file mode 100644
index 00000000000..04d1425ec80
--- /dev/null
+++ b/playground/frontend/playground_components/lib/src/cache/example_cache.dart
@@ -0,0 +1,238 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import 'dart:async';
+
+import 'package:collection/collection.dart';
+import 'package:flutter/material.dart';
+
+import '../models/category_with_examples.dart';
+import '../models/example.dart';
+import '../models/example_base.dart';
+import '../models/sdk.dart';
+import '../repositories/example_repository.dart';
+import '../repositories/models/get_default_precompiled_object_request.dart';
+import '../repositories/models/get_precompiled_object_request.dart';
+import '../repositories/models/get_precompiled_objects_request.dart';
+import '../repositories/models/shared_file.dart';
+import '../repositories/models/get_snippet_request.dart';
+import '../repositories/models/save_snippet_request.dart';
+
+/// A runtime cache for examples fetched from a repository.
+class ExampleCache extends ChangeNotifier {
+  /// If set, then categories and default examples are enabled.
+  /// Otherwise examples can only be queried by paths.
+  final bool hasCatalog;
+
+  final ExampleRepository _exampleRepository;
+  final categoryListsBySdk = <Sdk, List<CategoryWithExamples>>{};
+
+  final Map<Sdk, Example> defaultExamplesBySdk = {};
+
+  // TODO(alexeyinkin): Extract, https://github.com/apache/beam/issues/23249
+  bool isSelectorOpened = false;
+
+  final _allExamplesCompleter = Completer<void>();
+
+  Future<void> get allExamplesFuture => _allExamplesCompleter.future;
+
+  ExampleCache({
+    required ExampleRepository exampleRepository,
+    required this.hasCatalog,
+  }) : _exampleRepository = exampleRepository;
+
+  Future<void> init() async {
+    if (hasCatalog) {
+      await Future.wait([
+        _loadCategories(),
+        loadDefaultExamplesIfNot(),
+      ]);
+    }
+  }
+
+  void setSdkCategories(Map<Sdk, List<CategoryWithExamples>> map) {
+    categoryListsBySdk.addAll(map);
+    _allExamplesCompleter.complete();
+  }
+
+  List<CategoryWithExamples> getCategories(Sdk? sdk) {
+    return categoryListsBySdk[sdk] ?? [];
+  }
+
+  Future<String> getExampleOutput(String path, Sdk sdk) async {
+    return _exampleRepository.getExampleOutput(
+      GetPrecompiledObjectRequest(path: path, sdk: sdk),
+    );
+  }
+
+  Future<String> getExampleSource(String path, Sdk sdk) async {
+    return _exampleRepository.getExampleSource(
+      GetPrecompiledObjectRequest(path: path, sdk: sdk),
+    );
+  }
+
+  Future<ExampleBase> getExample(String path, Sdk sdk) async {
+    return _exampleRepository.getExample(
+      GetPrecompiledObjectRequest(path: path, sdk: sdk),
+    );
+  }
+
+  Future<String> getExampleLogs(String path, Sdk sdk) async {
+    return _exampleRepository.getExampleLogs(
+      GetPrecompiledObjectRequest(path: path, sdk: sdk),
+    );
+  }
+
+  Future<String> getExampleGraph(String id, Sdk sdk) async {
+    return _exampleRepository.getExampleGraph(
+      GetPrecompiledObjectRequest(path: id, sdk: sdk),
+    );
+  }
+
+  Future<Example> loadSharedExample(String id) async {
+    final result = await _exampleRepository.getSnippet(
+      GetSnippetRequest(id: id),
+    );
+
+    return Example(
+      sdk: result.sdk,
+      name: result.files.first.name,
+      path: id,
+      description: '',
+      type: ExampleType.example,
+      source: result.files.first.code,
+      pipelineOptions: result.pipelineOptions,
+    );
+  }
+
+  Future<String> getSnippetId({
+    required List<SharedFile> files,
+    required Sdk sdk,
+    required String pipelineOptions,
+  }) async {
+    final id = await _exampleRepository.saveSnippet(
+      SaveSnippetRequest(
+        files: files,
+        sdk: sdk,
+        pipelineOptions: pipelineOptions,
+      ),
+    );
+    return id;
+  }
+
+  Future<Example> loadExampleInfo(ExampleBase example) async {
+    if (example is Example) {
+      return example;
+    }
+
+    //GRPC GetPrecompiledGraph errors hotfix
+    if (example.name == 'MinimalWordCount' &&
+        (example.sdk == Sdk.go || example.sdk == Sdk.scio)) {
+      final exampleData = await Future.wait([
+        getExampleSource(example.path, example.sdk),
+        getExampleOutput(example.path, example.sdk),
+        getExampleLogs(example.path, example.sdk),
+      ]);
+
+      return Example.fromBase(
+        example,
+        source: exampleData[0],
+        outputs: exampleData[1],
+        logs: exampleData[2],
+      );
+    }
+
+    final exampleData = await Future.wait([
+      getExampleSource(example.path, example.sdk),
+      getExampleOutput(example.path, example.sdk),
+      getExampleLogs(example.path, example.sdk),
+      getExampleGraph(example.path, example.sdk)
+    ]);
+
+    return Example.fromBase(
+      example,
+      source: exampleData[0],
+      outputs: exampleData[1],
+      logs: exampleData[2],
+      graph: exampleData[3],
+    );
+  }
+
+  Future<void> _loadCategories() async {
+    final result = await _exampleRepository.getListOfExamples(
+      const GetPrecompiledObjectsRequest(
+        sdk: null,
+        category: null,
+      ),
+    );
+
+    setSdkCategories(result);
+  }
+
+  void changeSelectorVisibility() {
+    isSelectorOpened = !isSelectorOpened;
+    notifyListeners();
+  }
+
+  Future<void> loadDefaultExamples() async {
+    if (defaultExamplesBySdk.isNotEmpty) {
+      return;
+    }
+
+    try {
+      await Future.wait(Sdk.known.map(_loadDefaultExample));
+    } catch (ex) {
+      if (defaultExamplesBySdk.isEmpty) {
+        rethrow;
+      }
+      // As long as any of the examples is loaded, continue.
+      print(ex);
+      // TODO: Log.
+    }
+
+    notifyListeners();
+  }
+
+  Future<void> _loadDefaultExample(Sdk sdk) async {
+    final exampleWithoutInfo = await _exampleRepository.getDefaultExample(
+      GetDefaultPrecompiledObjectRequest(sdk: sdk),
+    );
+
+    defaultExamplesBySdk[sdk] = await loadExampleInfo(exampleWithoutInfo);
+  }
+
+  Future<void> loadDefaultExamplesIfNot() async {
+    if (defaultExamplesBySdk.isNotEmpty) {
+      return;
+    }
+
+    await loadDefaultExamples();
+  }
+
+  Future<ExampleBase?> getCatalogExampleByPath(String path) async {
+    await allExamplesFuture;
+
+    final allExamples = categoryListsBySdk.values
+        .expand((categories) => categories.map((c) => c.examples))
+        .expand((examples) => examples);
+
+    return allExamples.firstWhereOrNull(
+      (e) => e.path == path,
+    );
+  }
+}
diff --git a/learning/tour-of-beam/frontend/lib/constants/colors.dart b/playground/frontend/playground_components/lib/src/constants/colors.dart
similarity index 52%
rename from learning/tour-of-beam/frontend/lib/constants/colors.dart
rename to playground/frontend/playground_components/lib/src/constants/colors.dart
index b595f8a46a6..43be2453329 100644
--- a/learning/tour-of-beam/frontend/lib/constants/colors.dart
+++ b/playground/frontend/playground_components/lib/src/constants/colors.dart
@@ -18,31 +18,61 @@
 
 import 'package:flutter/material.dart';
 
-class TobColors {
+class BeamColors {
+  static const transparent = Colors.transparent;
   static const white = Colors.white;
   static const black = Colors.black;
   static const grey1 = Color(0xffDFE1E3);
   static const grey2 = Color(0xffCBCBCB);
   static const grey3 = Color(0xffA0A4AB);
   static const grey4 = Color(0x30808080);
+  static const darkGrey = Color(0xff2E2E34);
+  static const darkBlue = Color(0xff242639);
 
   static const green = Color(0xff37AC66);
   static const orange = Color(0xffEEAB00);
   static const red = Color(0xffE54545);
 }
 
-class TobLightThemeColors {
-  static const primaryBackground = Colors.white;
-  static const secondaryBackground = Color(0xffFEFDFD);
+class BeamGraphColors {
+  static const node = BeamColors.grey3;
+  static const border = Color(0xFF45454E);
+  static const edge = BeamLightThemeColors.primary;
+}
+
+class BeamNotificationColors {
+  static const error = Color(0xFFE54545);
+  static const info = Color(0xFF3E67F6);
+  static const success = Color(0xFF37AC66);
+  static const warning = Color(0xFFEEAB00);
+}
+
+class BeamLightThemeColors {
+  static const border = Color(0xFFE5E5E5);
+  static const primaryBackground = BeamColors.white;
+  static const secondaryBackground = Color(0xffFCFCFC);
   static const grey = Color(0xffE5E5E5);
-  static const text = Color(0xff242639);
+  static const listBackground = Color(0xFFA0A4AB);
+  static const text = BeamColors.darkBlue;
   static const primary = Color(0xffE74D1A);
+  static const icon = Color(0xFFA0A4AB);
+
+  static const code1 = Color(0xFFDA2833);
+  static const code2 = Color(0xFF5929B4);
+  static const codeComment = Color(0xFF4C6B60);
 }
 
-class TobDarkThemeColors {
+class BeamDarkThemeColors {
+  static const border = Color(0xFFA0A4AB);
   static const primaryBackground = Color(0xff18181B);
-  static const secondaryBackground = Color(0xff2E2E34);
+  static const secondaryBackground = BeamColors.darkGrey;
   static const grey = Color(0xff3F3F46);
+  static const listBackground = Color(0xFF606772);
   static const text = Color(0xffFFFFFF);
   static const primary = Color(0xffF26628);
+  static const icon = Color(0xFF606772);
+
+  static const code1 = Color(0xFFDA2833);
+  static const code2 = Color(0xFF5929B4);
+  static const codeComment = Color(0xFF4C6B60);
 }
diff --git a/learning/tour-of-beam/frontend/lib/constants/links.dart b/playground/frontend/playground_components/lib/src/constants/links.dart
similarity index 98%
rename from learning/tour-of-beam/frontend/lib/constants/links.dart
rename to playground/frontend/playground_components/lib/src/constants/links.dart
index 1d15c5c3656..d55de92c802 100644
--- a/learning/tour-of-beam/frontend/lib/constants/links.dart
+++ b/playground/frontend/playground_components/lib/src/constants/links.dart
@@ -16,7 +16,7 @@
  * limitations under the License.
  */
 
-class TobLinks {
+class BeamLinks {
   static const reportIssue = 'https://github.com/apache/beam/issues';
   static const privacyPolicy = 'https://beam.apache.org/privacy_policy/';
 }
diff --git a/playground/frontend/lib/modules/editor/repository/code_repository/run_code_request.dart b/playground/frontend/playground_components/lib/src/constants/playground_components.dart
similarity index 68%
copy from playground/frontend/lib/modules/editor/repository/code_repository/run_code_request.dart
copy to playground/frontend/playground_components/lib/src/constants/playground_components.dart
index aa09fcd0bff..89909e112a8 100644
--- a/playground/frontend/lib/modules/editor/repository/code_repository/run_code_request.dart
+++ b/playground/frontend/playground_components/lib/src/constants/playground_components.dart
@@ -16,16 +16,14 @@
  * limitations under the License.
  */
 
-import 'package:playground/modules/sdk/models/sdk.dart';
+import 'package:easy_localization_ext/easy_localization_ext.dart';
 
-class RunCodeRequestWrapper {
-  final String code;
-  final SDK sdk;
-  final Map<String, String> pipelineOptions;
+class PlaygroundComponents {
+  static const packageName = 'playground_components';
 
-  RunCodeRequestWrapper({
-    required this.code,
-    required this.sdk,
-    required this.pipelineOptions,
-  });
+  // TODO(alexeyinkin): Make const when this is fixed: https://github.com/aissat/easy_localization_loader/issues/39
+  static final translationLoader = YamlPackageAssetLoader(
+    packageName,
+    path: 'assets/translations',
+  );
 }
diff --git a/learning/tour-of-beam/frontend/lib/constants/sizes.dart b/playground/frontend/playground_components/lib/src/constants/sizes.dart
similarity index 63%
copy from learning/tour-of-beam/frontend/lib/constants/sizes.dart
copy to playground/frontend/playground_components/lib/src/constants/sizes.dart
index 187a0f60f95..916ae6f7243 100644
--- a/learning/tour-of-beam/frontend/lib/constants/sizes.dart
+++ b/playground/frontend/playground_components/lib/src/constants/sizes.dart
@@ -16,44 +16,42 @@
  * limitations under the License.
  */
 
-class TobSizes {
+class BeamSizes {
   static const double size0 = 0;
   static const double size1 = 1;
+  static const double size2 = 2;
   static const double size4 = 4;
   static const double size6 = 6;
   static const double size8 = 8;
   static const double size10 = 10;
   static const double size12 = 12;
   static const double size16 = 16;
+  static const double size18 = 18;
   static const double size20 = 20;
   static const double size24 = 24;
+  static const double size30 = 30;
   static const double size32 = 32;
   static const double size36 = 36;
   static const double size40 = 40;
-  static const double appBarHeight = 55;
-  static const double footerHeight = 30;
-  static const double authOverlayWidth = 300;
-}
+  static const double size64 = 64;
 
-class TobBorderRadius {
-  static const double small = 4;
-  static const double medium = 6;
-  static const double large = 8;
-  static const double xl = 28;
+  static const double appBarHeight = 55;
+  static const double buttonHeight = 40;
+  static const double headerButtonHeight = 46;
+  static const double loadingIndicator = 40;
+  static const double splitViewSeparator = BeamSizes.size8;
 }
 
-class TobIconSizes {
-  static const double xs = 8;
-  static const double small = 16;
-  static const double medium = 24;
-  static const double large = 32;
+class BeamBorderRadius {
+  static const double small = BeamSizes.size4;
+  static const double large = BeamSizes.size8;
+  static const double infinite = 1000; // TODO: Use StadiumBorder
 }
 
-class ScreenSizes {
-  // TODO(nausharipov): name better
-  static const medium = 1024;
-}
+class BeamIconSizes {
+  static const double xs = BeamSizes.size8;
+  static const double small = BeamSizes.size16;
+  static const double large = BeamSizes.size32;
 
-class ScreenBreakpoints {
-  static const twoColumns = ScreenSizes.medium;
+  static const double largeSplashRadius = 24;
 }
diff --git a/playground/frontend/lib/pages/playground/states/example_loaders/catalog_default_example_loader.dart b/playground/frontend/playground_components/lib/src/controllers/example_loaders/catalog_default_example_loader.dart
similarity index 65%
rename from playground/frontend/lib/pages/playground/states/example_loaders/catalog_default_example_loader.dart
rename to playground/frontend/playground_components/lib/src/controllers/example_loaders/catalog_default_example_loader.dart
index e07d4cb67d6..d07d0897026 100644
--- a/playground/frontend/lib/pages/playground/states/example_loaders/catalog_default_example_loader.dart
+++ b/playground/frontend/playground_components/lib/src/controllers/example_loaders/catalog_default_example_loader.dart
@@ -16,28 +16,28 @@
  * limitations under the License.
  */
 
-import 'package:playground/modules/examples/models/example_loading_descriptors/catalog_default_example_loading_descriptor.dart';
-import 'package:playground/modules/examples/models/example_model.dart';
-import 'package:playground/pages/playground/states/example_loaders/example_loader.dart';
-import 'package:playground/pages/playground/states/examples_state.dart';
+import '../../cache/example_cache.dart';
+import '../../models/example.dart';
+import '../../models/example_loading_descriptors/catalog_default_example_loading_descriptor.dart';
+import 'example_loader.dart';
 
 class CatalogDefaultExampleLoader extends ExampleLoader {
   final CatalogDefaultExampleLoadingDescriptor descriptor;
-  final ExampleState exampleState;
+  final ExampleCache exampleCache;
 
   const CatalogDefaultExampleLoader({
     required this.descriptor,
-    required this.exampleState,
+    required this.exampleCache,
   });
 
   @override
-  Future<ExampleModel> get future async {
-    if (!exampleState.hasExampleCatalog) {
+  Future<Example> get future async {
+    if (!exampleCache.hasCatalog) {
       throw Exception('Default example requires a catalog in ExampleState');
     }
 
-    await exampleState.loadDefaultExamplesIfNot();
-    final result = exampleState.defaultExamplesMap[descriptor.sdk];
+    await exampleCache.loadDefaultExamplesIfNot();
+    final result = exampleCache.defaultExamplesBySdk[descriptor.sdk];
 
     if (result == null) {
       throw Exception('Default example not found for $descriptor');
diff --git a/playground/frontend/lib/pages/playground/states/example_loaders/content_example_loader.dart b/playground/frontend/playground_components/lib/src/controllers/example_loaders/content_example_loader.dart
similarity index 72%
rename from playground/frontend/lib/pages/playground/states/example_loaders/content_example_loader.dart
rename to playground/frontend/playground_components/lib/src/controllers/example_loaders/content_example_loader.dart
index 117bf139f52..a196d7122a7 100644
--- a/playground/frontend/lib/pages/playground/states/example_loaders/content_example_loader.dart
+++ b/playground/frontend/playground_components/lib/src/controllers/example_loaders/content_example_loader.dart
@@ -16,24 +16,29 @@
  * limitations under the License.
  */
 
-import 'package:playground/modules/examples/models/example_loading_descriptors/content_example_loading_descriptor.dart';
-import 'package:playground/modules/examples/models/example_model.dart';
-import 'package:playground/pages/playground/states/example_loaders/example_loader.dart';
+import '../../cache/example_cache.dart';
+import '../../models/example.dart';
+import '../../models/example_base.dart';
+import '../../models/example_loading_descriptors/content_example_loading_descriptor.dart';
+import 'example_loader.dart';
 
 class ContentExampleLoader extends ExampleLoader {
   final ContentExampleLoadingDescriptor descriptor;
 
   const ContentExampleLoader({
     required this.descriptor,
+    // TODO(alexeyinkin): Remove when this lands: https://github.com/dart-lang/language/issues/1813
+    required ExampleCache exampleCache,
   });
 
   @override
-  Future<ExampleModel> get future async => ExampleModel(
+  Future<Example> get future async => Example(
     sdk: descriptor.sdk,
     name: descriptor.name ?? 'Embedded_Example',
     path: '',
     description: '',
     type: ExampleType.example,
     source: descriptor.content,
+    pipelineOptions: '',
   );
 }
diff --git a/playground/frontend/lib/pages/playground/states/example_loaders/empty_example_loader.dart b/playground/frontend/playground_components/lib/src/controllers/example_loaders/empty_example_loader.dart
similarity index 70%
rename from playground/frontend/lib/pages/playground/states/example_loaders/empty_example_loader.dart
rename to playground/frontend/playground_components/lib/src/controllers/example_loaders/empty_example_loader.dart
index 3f1acdc87bd..8f3b3d07e4c 100644
--- a/playground/frontend/lib/pages/playground/states/example_loaders/empty_example_loader.dart
+++ b/playground/frontend/playground_components/lib/src/controllers/example_loaders/empty_example_loader.dart
@@ -16,23 +16,29 @@
  * limitations under the License.
  */
 
-import 'package:playground/modules/examples/models/example_loading_descriptors/empty_example_loading_descriptor.dart';
-import 'package:playground/modules/examples/models/example_model.dart';
-import 'package:playground/pages/playground/states/example_loaders/example_loader.dart';
+import '../../cache/example_cache.dart';
+import '../../models/example.dart';
+import '../../models/example_base.dart';
+import '../../models/example_loading_descriptors/empty_example_loading_descriptor.dart';
+import 'example_loader.dart';
 
 class EmptyExampleLoader extends ExampleLoader {
   final EmptyExampleLoadingDescriptor descriptor;
 
   const EmptyExampleLoader({
     required this.descriptor,
+    // TODO(alexeyinkin): Remove when this lands: https://github.com/dart-lang/language/issues/1813
+    required ExampleCache exampleCache,
   });
 
   @override
-  Future<ExampleModel> get future async => ExampleModel(
+  Future<Example> get future async => Example(
         sdk: descriptor.sdk,
         name: 'Embedded_Example',
         path: '',
         description: '',
         type: ExampleType.example,
+        source: '',
+        pipelineOptions: '',
       );
 }
diff --git a/playground/frontend/lib/pages/playground/states/example_loaders/example_loader.dart b/playground/frontend/playground_components/lib/src/controllers/example_loaders/example_loader.dart
similarity index 89%
rename from playground/frontend/lib/pages/playground/states/example_loaders/example_loader.dart
rename to playground/frontend/playground_components/lib/src/controllers/example_loaders/example_loader.dart
index 9c6b256683c..c90f03745da 100644
--- a/playground/frontend/lib/pages/playground/states/example_loaders/example_loader.dart
+++ b/playground/frontend/playground_components/lib/src/controllers/example_loaders/example_loader.dart
@@ -16,10 +16,10 @@
  * limitations under the License.
  */
 
-import 'package:playground/modules/examples/models/example_model.dart';
+import '../../models/example.dart';
 
 abstract class ExampleLoader {
   const ExampleLoader();
 
-  Future<ExampleModel> get future;
+  Future<Example> get future;
 }
diff --git a/playground/frontend/playground_components/lib/src/controllers/example_loaders/example_loader_factory.dart b/playground/frontend/playground_components/lib/src/controllers/example_loaders/example_loader_factory.dart
new file mode 100644
index 00000000000..9a16d7d4b99
--- /dev/null
+++ b/playground/frontend/playground_components/lib/src/controllers/example_loaders/example_loader_factory.dart
@@ -0,0 +1,53 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import '../../cache/example_cache.dart';
+import '../../models/example_loading_descriptors/example_loading_descriptor.dart';
+import 'example_loader.dart';
+
+typedef ExampleLoaderFactoryFunction<D extends ExampleLoadingDescriptor>
+    = ExampleLoader Function({
+  required D descriptor,
+  required ExampleCache exampleCache,
+});
+
+class ExampleLoaderFactory {
+  final factories = <Type, ExampleLoaderFactoryFunction>{};
+
+  void add<D extends ExampleLoadingDescriptor>(
+    ExampleLoaderFactoryFunction<D> function,
+  ) {
+    factories[D] = ({
+      required ExampleLoadingDescriptor descriptor,
+      required ExampleCache exampleCache,
+    }) {
+      return function(descriptor: descriptor as D, exampleCache: exampleCache);
+    };
+  }
+
+  ExampleLoader? create<D extends ExampleLoadingDescriptor>({
+    required D descriptor,
+    required ExampleCache exampleCache,
+  }) {
+    return factories[descriptor.runtimeType]?.call(
+      descriptor: descriptor,
+      exampleCache: exampleCache,
+    );
+  }
+}
... 8038 lines suppressed ...