You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@beam.apache.org by da...@apache.org on 2022/09/02 16:08:35 UTC

[beam] branch master updated: [Tour of Beam]: Welcome Screen frontend layout (#22794)

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

damccorm 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 9e475f63013 [Tour of Beam]: Welcome Screen frontend layout (#22794)
9e475f63013 is described below

commit 9e475f63013d17cae7c894e47eeb17b67f71b34d
Author: Darkhan Nausharipov <31...@users.noreply.github.com>
AuthorDate: Fri Sep 2 22:08:25 2022 +0600

    [Tour of Beam]: Welcome Screen frontend layout (#22794)
    
    * Tour of Beam frontend blank project
    
    * TOBF (ToB frontend): welcome screen (#226)
    
    * 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
    
    Co-authored-by: darkhan.nausharipov <da...@kzn.akvelon.com>
    
    * removed whitespace from readme (issue-22583)
    
    * renamed "content" to "child" to mimic widgets
    
    * README in tour-of-beam
    
    * translation path rename,
    grey dot with opacity,
    footer link text style
    
    * report issue in github, grey dot color
    
    * table instead of row to clip the laptop image
    
    * horizontalHalves & verticalHalves
    
    * cropped laptop image
    
    * row in an expensive intrinsic height
    
    * laptop image in the bottom
    
    * ScreenBreakpoints
    
    * intrinsic height is not needed!
    
    * _WideWelcome and _NarrowWelcome
    
    * draft readme
    
    * blank line in readme
    
    * removed irrelevant info from readme
    
    * removed whitespace
    
    Co-authored-by: Alexey Inkin <al...@akvelon.com>
    Co-authored-by: darkhan.nausharipov <da...@kzn.akvelon.com>
    Co-authored-by: alexeyinkin <le...@inkin.ru>
---
 build.gradle.kts                                   |   8 +
 learning/tour-of-beam/README.md                    |  28 +
 learning/tour-of-beam/frontend/.metadata           |  30 ++
 learning/tour-of-beam/frontend/README.md           |  47 ++
 .../tour-of-beam/frontend/analysis_options.yaml    |  18 +
 .../tour-of-beam/frontend/assets/png/beam-logo.png | Bin 0 -> 1752 bytes
 .../frontend/assets/png/laptop-dark.png            | Bin 0 -> 129276 bytes
 .../frontend/assets/png/laptop-light.png           | Bin 0 -> 126440 bytes
 .../frontend/assets/svg/theme-mode.svg             |  20 +
 .../frontend/assets/svg/welcome-progress-0.svg     |  19 +
 .../frontend/assets/translations/en.yaml           |  39 ++
 .../frontend/integration_test/app_test.dart        |  43 ++
 .../frontend/lib/components/complexity.dart        |  65 +++
 .../frontend/lib/components/footer.dart            | 105 ++++
 .../tour-of-beam/frontend/lib/components/logo.dart |  65 +++
 .../frontend/lib/components/page_container.dart    |  54 ++
 .../lib/components/sign_in/sign_in_button.dart     |  58 +++
 .../sign_in/sign_in_overlay_content.dart           |  89 ++++
 .../lib/components/toggle_theme_button.dart        |  54 ++
 .../frontend/lib/config/theme/colors_provider.dart |  86 +++
 .../frontend/lib/config/theme/switch_notifier.dart |  84 +++
 .../frontend/lib/config/theme/theme.dart           | 160 ++++++
 .../frontend/lib/constants/assets.dart             |  34 ++
 .../frontend/lib/constants/colors.dart             |  48 ++
 .../tour-of-beam/frontend/lib/constants/links.dart |  22 +
 .../tour-of-beam/frontend/lib/constants/sizes.dart |  59 +++
 learning/tour-of-beam/frontend/lib/locator.dart    |  24 +
 learning/tour-of-beam/frontend/lib/main.dart       |  70 +++
 .../frontend/lib/pages/welcome/screen.dart         | 373 +++++++++++++
 learning/tour-of-beam/frontend/pubspec.lock        | 578 +++++++++++++++++++++
 learning/tour-of-beam/frontend/pubspec.yaml        |  53 ++
 .../test/config/theme/switch_notifier_test.dart    |  28 +
 .../frontend/test_driver/integration_test.dart     |  21 +
 learning/tour-of-beam/frontend/web/favicon.ico     | Bin 0 -> 370070 bytes
 learning/tour-of-beam/frontend/web/index.html      |  74 +++
 learning/tour-of-beam/frontend/web/manifest.json   |  18 +
 .../frontend/playground_components/.metadata       |  10 +
 .../frontend/playground_components/CHANGELOG.md    |  22 +
 playground/frontend/playground_components/LICENSE  | 407 +++++++++++++++
 .../frontend/playground_components/README.md       |  45 ++
 .../playground_components/analysis_options.yaml    |  21 +
 .../lib/dismissible_overlay.dart                   |  43 ++
 .../frontend/playground_components/pubspec.yaml    |  34 ++
 43 files changed, 3056 insertions(+)

diff --git a/build.gradle.kts b/build.gradle.kts
index e6d36b3743c..88ded595c5b 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -139,6 +139,14 @@ tasks.rat {
     "playground/frontend/.metadata",
     "playground/frontend/pubspec.lock",
 
+    // Ignore Flutter autogenerated files for Playground Components
+    "playground/frontend/playground_components/.metadata",
+    "playground/frontend/playground_components/pubspec.lock",
+
+    // Ignore Flutter autogenerated files for Tour of Beam
+    "learning/tour-of-beam/frontend/.metadata",
+    "learning/tour-of-beam/frontend/pubspec.lock",
+
     // Ignore .gitkeep file
     "**/.gitkeep",
 
diff --git a/learning/tour-of-beam/README.md b/learning/tour-of-beam/README.md
new file mode 100644
index 00000000000..e09f6ca27e4
--- /dev/null
+++ b/learning/tour-of-beam/README.md
@@ -0,0 +1,28 @@
+<!--
+    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.
+-->
+
+# Overview
+
+This folder holds code that supports the user interface for [Tour of Beam](../).
+
+# Requirements
+
+To develop, build and test code in this folder requires the following:
+
+- https://flutter.dev/
diff --git a/learning/tour-of-beam/frontend/.metadata b/learning/tour-of-beam/frontend/.metadata
new file mode 100644
index 00000000000..2112298c229
--- /dev/null
+++ b/learning/tour-of-beam/frontend/.metadata
@@ -0,0 +1,30 @@
+# This file tracks properties of this Flutter project.
+# Used by Flutter tool to assess capabilities and perform upgrades etc.
+#
+# This file should be version controlled.
+
+version:
+  revision: f1875d570e39de09040c8f79aa13cc56baab8db1
+  channel: stable
+
+project_type: app
+
+# Tracks metadata for the flutter migrate command
+migration:
+  platforms:
+    - platform: root
+      create_revision: f1875d570e39de09040c8f79aa13cc56baab8db1
+      base_revision: f1875d570e39de09040c8f79aa13cc56baab8db1
+    - platform: web
+      create_revision: f1875d570e39de09040c8f79aa13cc56baab8db1
+      base_revision: f1875d570e39de09040c8f79aa13cc56baab8db1
+
+  # User provided section
+
+  # List of Local paths (relative to this file) that should be
+  # ignored by the migrate tool.
+  #
+  # Files that are not part of the templates will be ignored by default.
+  unmanaged_files:
+    - 'lib/main.dart'
+    - 'ios/Runner.xcodeproj/project.pbxproj'
diff --git a/learning/tour-of-beam/frontend/README.md b/learning/tour-of-beam/frontend/README.md
new file mode 100644
index 00000000000..afd892b3f8e
--- /dev/null
+++ b/learning/tour-of-beam/frontend/README.md
@@ -0,0 +1,47 @@
+<!--
+     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.
+ -->
+
+ # Tour of Beam
+ These are the main sources of the Tour of Beam website.
+
+ # About
+
+ # Getting started
+ Flutter installation guide: https://docs.flutter.dev/get-started/install
+ Run the app: `flutter run --web-renderer html`
+
+ # Deployment
+
+ # Tests
+ Install ChromeDriver to run integration tests in a browser: https://docs.flutter.dev/testing/integration-tests#running-in-a-browser
+ Run integration tests:
+ flutter drive \
+   --driver=test_driver/integration_test.dart \
+   --target=integration_test/counter_test.dart \
+   -d web-server
+
+ # Packages
+ `flutter pub get`
+
+ # Contribution guide
+ For checks: `./gradlew rat`
+
+ # Additional resources
+
+ # Troubleshooting
diff --git a/learning/tour-of-beam/frontend/analysis_options.yaml b/learning/tour-of-beam/frontend/analysis_options.yaml
new file mode 100644
index 00000000000..fe2e0e8eb95
--- /dev/null
+++ b/learning/tour-of-beam/frontend/analysis_options.yaml
@@ -0,0 +1,18 @@
+#  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.
+
+include: package:total_lints/app.yaml
diff --git a/learning/tour-of-beam/frontend/assets/png/beam-logo.png b/learning/tour-of-beam/frontend/assets/png/beam-logo.png
new file mode 100644
index 00000000000..cb196949a8b
Binary files /dev/null and b/learning/tour-of-beam/frontend/assets/png/beam-logo.png differ
diff --git a/learning/tour-of-beam/frontend/assets/png/laptop-dark.png b/learning/tour-of-beam/frontend/assets/png/laptop-dark.png
new file mode 100644
index 00000000000..9c1bda80d48
Binary files /dev/null and b/learning/tour-of-beam/frontend/assets/png/laptop-dark.png differ
diff --git a/learning/tour-of-beam/frontend/assets/png/laptop-light.png b/learning/tour-of-beam/frontend/assets/png/laptop-light.png
new file mode 100644
index 00000000000..49224ed3767
Binary files /dev/null and b/learning/tour-of-beam/frontend/assets/png/laptop-light.png differ
diff --git a/learning/tour-of-beam/frontend/assets/svg/theme-mode.svg b/learning/tour-of-beam/frontend/assets/svg/theme-mode.svg
new file mode 100644
index 00000000000..fc1438aecf3
--- /dev/null
+++ b/learning/tour-of-beam/frontend/assets/svg/theme-mode.svg
@@ -0,0 +1,20 @@
+<!-- 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="M10 19C14.9706 19 19 14.9706 19 10C19 8.81443 18.7708 7.68239 18.3542 6.64581C18.3067 6.52774 18.2569 6.41091 18.2046 6.29539C17.754 5.29897 17.1272 4.39928 16.364 3.63604C16.1972 3.46923 16.0238 3.30895 15.8444 3.15559C15.2028 2.60724 14.4833 2.14752 13.7046 1.79539C12.5748 1.28444 11.3206 1 10 1V9V15V19Z" fill="#A0A4AB" />
+    <path d="M10 19C14.9706 19 19 14.9706 19 10C19 8.81443 18.7708 7.68239 18.3542 6.64581C18.3067 6.52774 18.2569 6.41091 18.2046 6.29539C17.754 5.29897 17.1272 4.39928 16.364 3.63604C16.1972 3.46923 16.0238 3.30895 15.8444 3.15559C15.2028 2.60724 14.4833 2.14752 13.7046 1.79539C12.5748 1.28444 11.3206 1 10 1M10 19C5.02944 19 1 14.9706 1 10C1 5.02944 5.02944 1 10 1M10 19V15V9V1" 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/welcome-progress-0.svg b/learning/tour-of-beam/frontend/assets/svg/welcome-progress-0.svg
new file mode 100644
index 00000000000..d80426bf789
--- /dev/null
+++ b/learning/tour-of-beam/frontend/assets/svg/welcome-progress-0.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="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>
diff --git a/learning/tour-of-beam/frontend/assets/translations/en.yaml b/learning/tour-of-beam/frontend/assets/translations/en.yaml
new file mode 100644
index 00000000000..b40cbfee09d
--- /dev/null
+++ b/learning/tour-of-beam/frontend/assets/translations/en.yaml
@@ -0,0 +1,39 @@
+#  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.
+
+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'
+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.'
+    selectLanguage: 'Please select the default language (you may change the language at any time):'
+    startLearning: 'Start learning'
+dialogs:
+  signInIf: If you would like to save your progress and track completed modules
+complexity:
+  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
new file mode 100644
index 00000000000..232234bb056
--- /dev/null
+++ b/learning/tour-of-beam/frontend/integration_test/app_test.dart
@@ -0,0 +1,43 @@
+/*
+ * 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_test/flutter_test.dart';
+import 'package:integration_test/integration_test.dart';
+import 'package:tour_of_beam/components/toggle_theme_button.dart';
+import 'package:tour_of_beam/main.dart' as app;
+
+void main() {
+  IntegrationTestWidgetsFlutterBinding.ensureInitialized();
+
+  group('theme', () {
+    testWidgets('mode toggle', (tester) async {
+      app.main();
+      await tester.pumpAndSettle();
+      final Finder switchToDarkModeButton =
+          find.widgetWithText(ToggleThemeButton, 'ui.darkMode'.tr());
+      expect(switchToDarkModeButton, findsOneWidget);
+      await tester.tap(switchToDarkModeButton);
+      await tester.pumpAndSettle();
+      expect(
+        find.widgetWithText(ToggleThemeButton, 'ui.lightMode'.tr()),
+        findsOneWidget,
+      );
+    });
+  });
+}
diff --git a/learning/tour-of-beam/frontend/lib/components/complexity.dart b/learning/tour-of-beam/frontend/lib/components/complexity.dart
new file mode 100644
index 00000000000..f8c28c1c034
--- /dev/null
+++ b/learning/tour-of-beam/frontend/lib/components/complexity.dart
@@ -0,0 +1,65 @@
+/*
+ * 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 '../constants/colors.dart';
+import '../constants/sizes.dart';
+
+enum Complexity { basic, medium, advanced }
+
+class ComplexityWidget extends StatelessWidget {
+  final Complexity complexity;
+
+  const ComplexityWidget({required this.complexity});
+
+  @override
+  Widget build(BuildContext context) {
+    return Row(children: _dots[complexity]!);
+  }
+
+  static const Map<Complexity, List<Widget>> _dots = {
+    Complexity.basic: [_Dot.green, _Dot.grey, _Dot.grey],
+    Complexity.medium: [_Dot.orange, _Dot.orange, _Dot.grey],
+    Complexity.advanced: [_Dot.red, _Dot.red, _Dot.red],
+  };
+}
+
+class _Dot extends StatelessWidget {
+  final Color color;
+
+  const _Dot({required this.color});
+
+  @override
+  Widget build(BuildContext context) {
+    return Container(
+      margin: const EdgeInsets.only(left: 1),
+      width: TobSizes.size4,
+      height: TobSizes.size4,
+      decoration: BoxDecoration(
+        shape: BoxShape.circle,
+        color: color,
+      ),
+    );
+  }
+
+  static const grey = _Dot(color: TobColors.grey4);
+  static const green = _Dot(color: TobColors.green);
+  static const orange = _Dot(color: TobColors.orange);
+  static const red = _Dot(color: TobColors.red);
+}
diff --git a/learning/tour-of-beam/frontend/lib/components/footer.dart b/learning/tour-of-beam/frontend/lib/components/footer.dart
new file mode 100644
index 00000000000..b42fbca162c
--- /dev/null
+++ b/learning/tour-of-beam/frontend/lib/components/footer.dart
@@ -0,0 +1,105 @@
+/*
+ * 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:url_launcher/url_launcher.dart';
+
+import '../config/theme/colors_provider.dart';
+import '../constants/links.dart';
+import '../constants/sizes.dart';
+
+class Footer extends StatelessWidget {
+  const Footer();
+
+  @override
+  Widget build(BuildContext context) {
+    return _Body(
+      child: Wrap(
+        spacing: TobSizes.size16,
+        crossAxisAlignment: WrapCrossAlignment.center,
+        children: [
+          const _ReportIssueButton(),
+          const _PrivacyPolicyButton(),
+          const Text('ui.copyright').tr(),
+        ],
+      ),
+    );
+  }
+}
+
+class _Body extends StatelessWidget {
+  final Widget child;
+  const _Body({required this.child});
+
+  @override
+  Widget build(BuildContext context) {
+    return Container(
+      padding: const EdgeInsets.symmetric(
+        vertical: TobSizes.size4,
+        horizontal: TobSizes.size16,
+      ),
+      decoration: BoxDecoration(
+        color: ThemeColors.of(context).secondaryBackground,
+        border: Border(
+          top: BorderSide(color: ThemeColors.of(context).divider),
+        ),
+      ),
+      height: TobSizes.footerHeight,
+      width: double.infinity,
+      child: child,
+    );
+  }
+}
+
+class _ReportIssueButton extends StatelessWidget {
+  const _ReportIssueButton();
+
+  @override
+  Widget build(BuildContext context) {
+    return TextButton(
+      style: _linkButtonStyle,
+      onPressed: () {
+        launchUrl(Uri.parse(TobLinks.reportIssue));
+      },
+      child: const Text('ui.reportIssue').tr(),
+    );
+  }
+}
+
+class _PrivacyPolicyButton extends StatelessWidget {
+  const _PrivacyPolicyButton();
+
+  @override
+  Widget build(BuildContext context) {
+    return TextButton(
+      style: _linkButtonStyle,
+      onPressed: () {
+        launchUrl(Uri.parse(TobLinks.privacyPolicy));
+      },
+      child: const Text('ui.privacyPolicy').tr(),
+    );
+  }
+}
+
+final _linkButtonStyle = TextButton.styleFrom(
+  textStyle: const TextStyle(
+    fontSize: 12,
+    fontWeight: FontWeight.w400,
+  ),
+);
diff --git a/learning/tour-of-beam/frontend/lib/components/logo.dart b/learning/tour-of-beam/frontend/lib/components/logo.dart
new file mode 100644
index 00000000000..e48b3985bf6
--- /dev/null
+++ b/learning/tour-of-beam/frontend/lib/components/logo.dart
@@ -0,0 +1,65 @@
+/*
+ * 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 '../constants/assets.dart';
+import '../constants/sizes.dart';
+
+class Logo extends StatelessWidget {
+  const Logo();
+
+  @override
+  Widget build(BuildContext context) {
+    return Row(
+      mainAxisSize: MainAxisSize.min,
+      children: [
+        Image.asset(
+          TobAssets.beamLogo,
+          height: TobIconSizes.large,
+        ),
+        const _Text(),
+      ],
+    );
+  }
+}
+
+class _Text extends StatelessWidget {
+  const _Text();
+
+  @override
+  Widget build(BuildContext context) {
+    return RichText(
+      text: TextSpan(
+        style: Theme.of(context).textTheme.displaySmall,
+        children: [
+          TextSpan(
+            text: 'Tour of',
+            style: TextStyle(
+              color: Theme.of(context).textTheme.labelLarge?.color,
+            ),
+          ),
+          TextSpan(
+            text: ' Beam',
+            style: TextStyle(color: Theme.of(context).primaryColor),
+          ),
+        ],
+      ),
+    );
+  }
+}
diff --git a/learning/tour-of-beam/frontend/lib/components/page_container.dart b/learning/tour-of-beam/frontend/lib/components/page_container.dart
new file mode 100644
index 00000000000..3500e173927
--- /dev/null
+++ b/learning/tour-of-beam/frontend/lib/components/page_container.dart
@@ -0,0 +1,54 @@
+/*
+ * 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 '../constants/sizes.dart';
+import 'footer.dart';
+import 'logo.dart';
+import 'sign_in/sign_in_button.dart';
+import 'toggle_theme_button.dart';
+
+class PageContainer extends StatelessWidget {
+  final Widget child;
+
+  const PageContainer({
+    super.key,
+    required this.child,
+  });
+
+  @override
+  Widget build(BuildContext context) {
+    return Scaffold(
+      appBar: AppBar(
+        title: const Logo(),
+        actions: const [
+          ToggleThemeButton(),
+          SignInButton(),
+          SizedBox(width: TobSizes.size16),
+        ],
+      ),
+      body: Column(
+        children: [
+          Expanded(child: child),
+          const Footer(),
+        ],
+      ),
+    );
+  }
+}
diff --git a/learning/tour-of-beam/frontend/lib/components/sign_in/sign_in_button.dart b/learning/tour-of-beam/frontend/lib/components/sign_in/sign_in_button.dart
new file mode 100644
index 00000000000..b823ea71c1b
--- /dev/null
+++ b/learning/tour-of-beam/frontend/lib/components/sign_in/sign_in_button.dart
@@ -0,0 +1,58 @@
+/*
+ * 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:playground_components/dismissible_overlay.dart';
+
+import '../../constants/sizes.dart';
+import 'sign_in_overlay_content.dart';
+
+class SignInButton extends StatefulWidget {
+  const SignInButton();
+
+  @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(),
+    );
+  }
+
+  void _openOverlay() {
+    OverlayEntry? overlay;
+    overlay = OverlayEntry(
+      builder: (context) => DismissibleOverlay(
+        close: () {
+          overlay?.remove();
+        },
+        child: const Positioned(
+          right: TobSizes.size10,
+          top: TobSizes.appBarHeight,
+          child: SignInOverlayContent(),
+        ),
+      ),
+    );
+    Overlay.of(context)?.insert(overlay);
+  }
+}
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
new file mode 100644
index 00000000000..80fb00805e2
--- /dev/null
+++ b/learning/tour-of-beam/frontend/lib/components/sign_in/sign_in_overlay_content.dart
@@ -0,0 +1,89 @@
+/*
+ * 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/components/toggle_theme_button.dart b/learning/tour-of-beam/frontend/lib/components/toggle_theme_button.dart
new file mode 100644
index 00000000000..2e0c84b68bf
--- /dev/null
+++ b/learning/tour-of-beam/frontend/lib/components/toggle_theme_button.dart
@@ -0,0 +1,54 @@
+/*
+ * 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:provider/provider.dart';
+
+import '../config/theme/switch_notifier.dart';
+import '../constants/assets.dart';
+import '../constants/sizes.dart';
+
+class ToggleThemeButton extends StatelessWidget {
+  const ToggleThemeButton();
+
+  @override
+  Widget build(BuildContext context) {
+    return Consumer<ThemeSwitchNotifier>(
+      builder: (context, notifier, child) {
+        final text =
+            notifier.isDarkMode ? 'ui.lightMode'.tr() : 'ui.darkMode'.tr();
+
+        return Padding(
+          padding: const EdgeInsets.symmetric(
+            vertical: TobSizes.size4,
+            horizontal: TobSizes.size8,
+          ),
+          child: TextButton.icon(
+            icon: SvgPicture.asset(TobAssets.themeMode),
+            label: Text(text),
+            onPressed: () {
+              notifier.toggleTheme();
+            },
+          ),
+        );
+      },
+    );
+  }
+}
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
new file mode 100644
index 00000000000..3aab5a1703c
--- /dev/null
+++ b/learning/tour-of-beam/frontend/lib/config/theme/colors_provider.dart
@@ -0,0 +1,86 @@
+/*
+ * 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/switch_notifier.dart b/learning/tour-of-beam/frontend/lib/config/theme/switch_notifier.dart
new file mode 100644
index 00000000000..09851c200a2
--- /dev/null
+++ b/learning/tour-of-beam/frontend/lib/config/theme/switch_notifier.dart
@@ -0,0 +1,84 @@
+/*
+ * 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 'package:shared_preferences/shared_preferences.dart';
+
+import 'colors_provider.dart';
+
+const kThemeMode = 'theme_mode';
+
+class ThemeSwitchNotifier extends ChangeNotifier {
+  ThemeMode themeMode = ThemeMode.light;
+
+  static const _darkThemeColors = ThemeColors.fromBrightness(isDark: true);
+  static const _lightThemeColors = ThemeColors.fromBrightness(isDark: false);
+
+  ThemeColors get themeColors {
+    if (themeMode == ThemeMode.dark) {
+      return _darkThemeColors;
+    }
+    return _lightThemeColors;
+  }
+
+  void init() {
+    _setPreferences();
+  }
+
+  Future<void> _setPreferences() async {
+    final preferences = await SharedPreferences.getInstance();
+    themeMode = preferences.getString(kThemeMode) == ThemeMode.dark.toString()
+        ? ThemeMode.dark
+        : ThemeMode.light;
+    notifyListeners();
+  }
+
+  bool get isDarkMode {
+    return themeMode == ThemeMode.dark;
+  }
+
+  Future<void> toggleTheme() async {
+    themeMode = themeMode == ThemeMode.light ? ThemeMode.dark : ThemeMode.light;
+    final preferences = await SharedPreferences.getInstance();
+    await 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,
+        ),
+      ),
+    );
+  }
+}
diff --git a/learning/tour-of-beam/frontend/lib/config/theme/theme.dart b/learning/tour-of-beam/frontend/lib/config/theme/theme.dart
new file mode 100644
index 00000000000..d8cf2c1c208
--- /dev/null
+++ b/learning/tour-of-beam/frontend/lib/config/theme/theme.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: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
new file mode 100644
index 00000000000..1af152cc402
--- /dev/null
+++ b/learning/tour-of-beam/frontend/lib/constants/assets.dart
@@ -0,0 +1,34 @@
+/*
+ * 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/colors.dart b/learning/tour-of-beam/frontend/lib/constants/colors.dart
new file mode 100644
index 00000000000..b595f8a46a6
--- /dev/null
+++ b/learning/tour-of-beam/frontend/lib/constants/colors.dart
@@ -0,0 +1,48 @@
+/*
+ * 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';
+
+class TobColors {
+  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 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);
+  static const grey = Color(0xffE5E5E5);
+  static const text = Color(0xff242639);
+  static const primary = Color(0xffE74D1A);
+}
+
+class TobDarkThemeColors {
+  static const primaryBackground = Color(0xff18181B);
+  static const secondaryBackground = Color(0xff2E2E34);
+  static const grey = Color(0xff3F3F46);
+  static const text = Color(0xffFFFFFF);
+  static const primary = Color(0xffF26628);
+}
diff --git a/learning/tour-of-beam/frontend/lib/constants/links.dart b/learning/tour-of-beam/frontend/lib/constants/links.dart
new file mode 100644
index 00000000000..1d15c5c3656
--- /dev/null
+++ b/learning/tour-of-beam/frontend/lib/constants/links.dart
@@ -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.
+ */
+
+class TobLinks {
+  static const reportIssue = 'https://github.com/apache/beam/issues';
+  static const privacyPolicy = 'https://beam.apache.org/privacy_policy/';
+}
diff --git a/learning/tour-of-beam/frontend/lib/constants/sizes.dart b/learning/tour-of-beam/frontend/lib/constants/sizes.dart
new file mode 100644
index 00000000000..187a0f60f95
--- /dev/null
+++ b/learning/tour-of-beam/frontend/lib/constants/sizes.dart
@@ -0,0 +1,59 @@
+/*
+ * 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.
+ */
+
+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;
+}
+
+class ScreenSizes {
+  // TODO(nausharipov): name better
+  static const medium = 1024;
+}
+
+class ScreenBreakpoints {
+  static const twoColumns = ScreenSizes.medium;
+}
diff --git a/learning/tour-of-beam/frontend/lib/locator.dart b/learning/tour-of-beam/frontend/lib/locator.dart
new file mode 100644
index 00000000000..8ab88a830d0
--- /dev/null
+++ b/learning/tour-of-beam/frontend/lib/locator.dart
@@ -0,0 +1,24 @@
+/*
+ * 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:get_it/get_it.dart';
+
+Future<void> initializeServiceLocator() async {
+  // See https://github.com/alexeyinkin/mefolio-standalone/blob/main/flutter/lib/locator.dart
+  // as an example.
+}
diff --git a/learning/tour-of-beam/frontend/lib/main.dart b/learning/tour-of-beam/frontend/lib/main.dart
new file mode 100644
index 00000000000..4c81d88a4f5
--- /dev/null
+++ b/learning/tour-of-beam/frontend/lib/main.dart
@@ -0,0 +1,70 @@
+/*
+ * 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:easy_localization_loader/easy_localization_loader.dart';
+import 'package:flutter/material.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';
+
+void main() async {
+  setPathUrlStrategy();
+  await EasyLocalization.ensureInitialized();
+  await initializeServiceLocator();
+  const englishLocale = Locale('en');
+
+  runApp(
+    EasyLocalization(
+      supportedLocales: const [englishLocale],
+      startLocale: englishLocale,
+      fallbackLocale: englishLocale,
+      path: 'assets/translations',
+      assetLoader: YamlAssetLoader(),
+      child: const TourOfBeamApp(),
+    ),
+  );
+}
+
+class TourOfBeamApp extends StatelessWidget {
+  const TourOfBeamApp();
+
+  @override
+  Widget build(BuildContext context) {
+    return ThemeSwitchNotifierProvider(
+      child: Consumer<ThemeSwitchNotifier>(
+        builder: (context, themeSwitchNotifier, _) {
+          return MaterialApp(
+            debugShowCheckedModeBanner: false,
+            themeMode: themeSwitchNotifier.themeMode,
+            theme: kLightTheme,
+            darkTheme: kDarkTheme,
+            localizationsDelegates: context.localizationDelegates,
+            supportedLocales: context.supportedLocales,
+            locale: context.locale,
+            home: const WelcomeScreen(),
+          );
+        },
+      ),
+    );
+  }
+}
diff --git a/learning/tour-of-beam/frontend/lib/pages/welcome/screen.dart b/learning/tour-of-beam/frontend/lib/pages/welcome/screen.dart
new file mode 100644
index 00000000000..2898e158465
--- /dev/null
+++ b/learning/tour-of-beam/frontend/lib/pages/welcome/screen.dart
@@ -0,0 +1,373 @@
+/*
+ * 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/gestures.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter_svg/svg.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 '../../constants/sizes.dart';
+
+class WelcomeScreen extends StatelessWidget {
+  const WelcomeScreen();
+
+  @override
+  Widget build(BuildContext context) {
+    return PageContainer(
+      child: SingleChildScrollView(
+        child: MediaQuery.of(context).size.width > ScreenBreakpoints.twoColumns
+            ? const _WideWelcome()
+            : const _NarrowWelcome(),
+      ),
+    );
+  }
+}
+
+class _WideWelcome extends StatelessWidget {
+  const _WideWelcome();
+
+  @override
+  Widget build(BuildContext context) {
+    return Row(
+      crossAxisAlignment: CrossAxisAlignment.start,
+      children: const [
+        Expanded(
+          child: _SdkSelection(),
+        ),
+        Expanded(
+          child: _TourSummary(),
+        ),
+      ],
+    );
+  }
+}
+
+class _NarrowWelcome extends StatelessWidget {
+  const _NarrowWelcome();
+
+  @override
+  Widget build(BuildContext context) {
+    return Column(
+      children: const [
+        _SdkSelection(),
+        _TourSummary(),
+      ],
+    );
+  }
+}
+
+class _SdkSelection extends StatelessWidget {
+  const _SdkSelection();
+
+  @override
+  Widget build(BuildContext context) {
+    return Container(
+      constraints: BoxConstraints(
+        minHeight: MediaQuery.of(context).size.height -
+            TobSizes.appBarHeight -
+            TobSizes.footerHeight,
+      ),
+      color: ThemeColors.of(context).background,
+      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),
+          ),
+          const SizedBox(height: 900),
+          Padding(
+            padding: const EdgeInsets.fromLTRB(50, 60, 50, 20),
+            child: Column(
+              crossAxisAlignment: CrossAxisAlignment.start,
+              children: const [
+                _IntroText(),
+                SizedBox(height: TobSizes.size32),
+                _Buttons(),
+              ],
+            ),
+          ),
+        ],
+      ),
+    );
+  }
+}
+
+class _TourSummary extends StatelessWidget {
+  const _TourSummary();
+
+  @override
+  Widget build(BuildContext context) {
+    return Padding(
+      padding: const EdgeInsets.symmetric(
+        vertical: TobSizes.size20,
+        horizontal: 27,
+      ),
+      child: Column(
+        children: _modules
+            .map(
+              (module) => _Module(
+                title: module,
+                isLast: module == _modules.last,
+              ),
+            )
+            .toList(),
+      ),
+    );
+  }
+
+  static const List<String> _modules = [
+    'Core Transforms',
+    'Common Transforms',
+    'IO',
+    'Windowing',
+    'Triggers',
+  ];
+}
+
+class _IntroText extends StatelessWidget {
+  const _IntroText();
+
+  @override
+  Widget build(BuildContext context) {
+    return Column(
+      crossAxisAlignment: CrossAxisAlignment.start,
+      children: [
+        Text(
+          'pages.welcome.title',
+          style: Theme.of(context).textTheme.displayMedium,
+        ).tr(),
+        Container(
+          margin: const EdgeInsets.symmetric(vertical: 32),
+          height: 2,
+          color: TobColors.grey2,
+          constraints: const BoxConstraints(maxWidth: 150),
+        ),
+        RichText(
+          text: TextSpan(
+            style: Theme.of(context).textTheme.bodyLarge,
+            children: [
+              TextSpan(
+                text: 'pages.welcome.ifSaveProgress'.tr(),
+              ),
+              TextSpan(
+                text: 'pages.welcome.signIn'.tr(),
+                style: Theme.of(context)
+                    .textTheme
+                    .bodyLarge!
+                    .copyWith(color: ThemeColors.of(context).primary),
+                recognizer: TapGestureRecognizer()
+                  ..onTap = () {
+                    // TODO(nausharipov): sign in
+                  },
+              ),
+              TextSpan(text: '\n\n${'pages.welcome.selectLanguage'.tr()}'),
+            ],
+          ),
+        ),
+      ],
+    );
+  }
+}
+
+class _Buttons extends StatelessWidget {
+  const _Buttons();
+
+  void _onSdkChanged(String value) {
+    // TODO(nausharipov): select the language
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    return Wrap(
+      children: [
+        Wrap(
+          children: ['Java', 'Python', 'Go']
+              .map(
+                (e) => _SdkButton(
+                  value: e,
+                  groupValue: _sdk,
+                  onChanged: _onSdkChanged,
+                ),
+              )
+              .toList(),
+        ),
+        ElevatedButton(
+          onPressed: () {
+            // TODO(nausharipov): redirect
+          },
+          child: const Text('pages.welcome.startLearning').tr(),
+        ),
+      ],
+    );
+  }
+
+  static const String _sdk = 'Java';
+}
+
+class _SdkButton extends StatelessWidget {
+  final String value;
+  final String groupValue;
+  final ValueChanged<String> onChanged;
+
+  const _SdkButton({
+    required this.value,
+    required this.groupValue,
+    required this.onChanged,
+  });
+
+  @override
+  Widget build(BuildContext context) {
+    return Padding(
+      padding: const EdgeInsets.only(right: 15, bottom: 10),
+      child: OutlinedButton(
+        style: OutlinedButton.styleFrom(
+          backgroundColor: ThemeColors.of(context).background,
+          side: groupValue == value
+              ? null
+              : const BorderSide(color: TobColors.grey1),
+        ),
+        onPressed: () {
+          onChanged(value);
+        },
+        child: Text(value),
+      ),
+    );
+  }
+}
+
+class _Module extends StatelessWidget {
+  final String title;
+  final bool isLast;
+
+  const _Module({
+    required this.title,
+    required this.isLast,
+  });
+
+  @override
+  Widget build(BuildContext context) {
+    return Column(
+      children: [
+        _ModuleHeader(title: title),
+        if (isLast) const _LastModuleBody() else const _ModuleBody(),
+      ],
+    );
+  }
+}
+
+class _ModuleHeader extends StatelessWidget {
+  final String title;
+  const _ModuleHeader({required this.title});
+
+  @override
+  Widget build(BuildContext context) {
+    return Row(
+      mainAxisAlignment: MainAxisAlignment.spaceBetween,
+      children: [
+        Expanded(
+          child: Row(
+            children: [
+              Padding(
+                padding: const EdgeInsets.all(TobSizes.size4),
+                child: SvgPicture.asset(
+                  TobAssets.welcomeProgress0,
+                  color: ThemeColors.of(context).progressBackgroundColor,
+                ),
+              ),
+              const SizedBox(width: TobSizes.size16),
+              Expanded(
+                child: Text(
+                  title,
+                  style: Theme.of(context).textTheme.titleLarge,
+                ),
+              ),
+            ],
+          ),
+        ),
+        Row(
+          children: [
+            Text(
+              'complexity.medium',
+              style: Theme.of(context).textTheme.headlineSmall,
+            ).tr(),
+            const SizedBox(width: TobSizes.size6),
+            const ComplexityWidget(complexity: Complexity.medium),
+          ],
+        ),
+      ],
+    );
+  }
+}
+
+const EdgeInsets _moduleLeftMargin = EdgeInsets.only(left: 21);
+const EdgeInsets _modulePadding = EdgeInsets.only(left: 39, top: 10);
+
+class _ModuleBody extends StatelessWidget {
+  const _ModuleBody();
+
+  @override
+  Widget build(BuildContext context) {
+    return Container(
+      margin: _moduleLeftMargin,
+      decoration: BoxDecoration(
+        border: Border(
+          left: BorderSide(
+            color: ThemeColors.of(context).divider,
+          ),
+        ),
+      ),
+      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),
+          Divider(
+            color: ThemeColors.of(context).divider,
+          ),
+        ],
+      ),
+    );
+  }
+}
+
+class _LastModuleBody extends StatelessWidget {
+  const _LastModuleBody();
+
+  @override
+  Widget build(BuildContext context) {
+    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.',
+      ),
+    );
+  }
+}
diff --git a/learning/tour-of-beam/frontend/pubspec.lock b/learning/tour-of-beam/frontend/pubspec.lock
new file mode 100644
index 00000000000..b0a898316f4
--- /dev/null
+++ b/learning/tour-of-beam/frontend/pubspec.lock
@@ -0,0 +1,578 @@
+# Generated by pub
+# See https://dart.dev/tools/pub/glossary#lockfile
+packages:
+  archive:
+    dependency: transitive
+    description:
+      name: archive
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "3.1.11"
+  args:
+    dependency: transitive
+    description:
+      name: args
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "2.3.1"
+  async:
+    dependency: transitive
+    description:
+      name: async
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "2.8.2"
+  boolean_selector:
+    dependency: transitive
+    description:
+      name: boolean_selector
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "2.1.0"
+  characters:
+    dependency: transitive
+    description:
+      name: characters
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "1.2.0"
+  charcode:
+    dependency: transitive
+    description:
+      name: charcode
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "1.3.1"
+  clock:
+    dependency: transitive
+    description:
+      name: clock
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "1.1.0"
+  collection:
+    dependency: transitive
+    description:
+      name: collection
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "1.16.0"
+  crypto:
+    dependency: transitive
+    description:
+      name: crypto
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "3.0.1"
+  csv:
+    dependency: transitive
+    description:
+      name: csv
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "5.0.1"
+  easy_localization:
+    dependency: "direct main"
+    description:
+      name: easy_localization
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "3.0.1"
+  easy_localization_loader:
+    dependency: "direct main"
+    description:
+      name: easy_localization_loader
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "1.0.0"
+  easy_logger:
+    dependency: transitive
+    description:
+      name: easy_logger
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "0.0.2"
+  fake_async:
+    dependency: transitive
+    description:
+      name: fake_async
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "1.3.0"
+  ffi:
+    dependency: transitive
+    description:
+      name: ffi
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "2.0.1"
+  file:
+    dependency: transitive
+    description:
+      name: file
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "6.1.2"
+  flutter:
+    dependency: "direct main"
+    description: flutter
+    source: sdk
+    version: "0.0.0"
+  flutter_driver:
+    dependency: transitive
+    description: flutter
+    source: sdk
+    version: "0.0.0"
+  flutter_localizations:
+    dependency: transitive
+    description: flutter
+    source: sdk
+    version: "0.0.0"
+  flutter_svg:
+    dependency: "direct main"
+    description:
+      name: flutter_svg
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "1.0.3"
+  flutter_test:
+    dependency: "direct dev"
+    description: flutter
+    source: sdk
+    version: "0.0.0"
+  flutter_web_plugins:
+    dependency: transitive
+    description: flutter
+    source: sdk
+    version: "0.0.0"
+  fuchsia_remote_debug_protocol:
+    dependency: transitive
+    description: flutter
+    source: sdk
+    version: "0.0.0"
+  get_it:
+    dependency: "direct main"
+    description:
+      name: get_it
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "7.2.0"
+  google_fonts:
+    dependency: "direct main"
+    description:
+      name: google_fonts
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "3.0.1"
+  http:
+    dependency: transitive
+    description:
+      name: http
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "0.13.4"
+  http_parser:
+    dependency: transitive
+    description:
+      name: http_parser
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "4.0.1"
+  integration_test:
+    dependency: "direct dev"
+    description: flutter
+    source: sdk
+    version: "0.0.0"
+  intl:
+    dependency: transitive
+    description:
+      name: intl
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "0.17.0"
+  js:
+    dependency: transitive
+    description:
+      name: js
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "0.6.4"
+  matcher:
+    dependency: transitive
+    description:
+      name: matcher
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "0.12.11"
+  material_color_utilities:
+    dependency: transitive
+    description:
+      name: material_color_utilities
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "0.1.4"
+  meta:
+    dependency: transitive
+    description:
+      name: meta
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "1.7.0"
+  nested:
+    dependency: transitive
+    description:
+      name: nested
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "1.0.0"
+  path:
+    dependency: transitive
+    description:
+      name: path
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "1.8.1"
+  path_drawing:
+    dependency: transitive
+    description:
+      name: path_drawing
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "1.0.0"
+  path_parsing:
+    dependency: transitive
+    description:
+      name: path_parsing
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "1.0.0"
+  path_provider:
+    dependency: transitive
+    description:
+      name: path_provider
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "2.0.11"
+  path_provider_android:
+    dependency: transitive
+    description:
+      name: path_provider_android
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "2.0.17"
+  path_provider_ios:
+    dependency: transitive
+    description:
+      name: path_provider_ios
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "2.0.11"
+  path_provider_linux:
+    dependency: transitive
+    description:
+      name: path_provider_linux
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "2.1.7"
+  path_provider_macos:
+    dependency: transitive
+    description:
+      name: path_provider_macos
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "2.0.6"
+  path_provider_platform_interface:
+    dependency: transitive
+    description:
+      name: path_provider_platform_interface
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "2.0.4"
+  path_provider_windows:
+    dependency: transitive
+    description:
+      name: path_provider_windows
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "2.1.0"
+  petitparser:
+    dependency: transitive
+    description:
+      name: petitparser
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "5.0.0"
+  platform:
+    dependency: transitive
+    description:
+      name: platform
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "3.1.0"
+  playground_components:
+    dependency: "direct main"
+    description:
+      path: "../../../playground/frontend/playground_components"
+      relative: true
+    source: path
+    version: "0.0.1"
+  plugin_platform_interface:
+    dependency: transitive
+    description:
+      name: plugin_platform_interface
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "2.1.2"
+  process:
+    dependency: transitive
+    description:
+      name: process
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "4.2.4"
+  provider:
+    dependency: "direct main"
+    description:
+      name: provider
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "6.0.3"
+  shared_preferences:
+    dependency: "direct main"
+    description:
+      name: shared_preferences
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "2.0.15"
+  shared_preferences_android:
+    dependency: transitive
+    description:
+      name: shared_preferences_android
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "2.0.12"
+  shared_preferences_ios:
+    dependency: transitive
+    description:
+      name: shared_preferences_ios
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "2.1.1"
+  shared_preferences_linux:
+    dependency: transitive
+    description:
+      name: shared_preferences_linux
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "2.1.1"
+  shared_preferences_macos:
+    dependency: transitive
+    description:
+      name: shared_preferences_macos
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "2.0.4"
+  shared_preferences_platform_interface:
+    dependency: transitive
+    description:
+      name: shared_preferences_platform_interface
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "2.0.0"
+  shared_preferences_web:
+    dependency: transitive
+    description:
+      name: shared_preferences_web
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "2.0.4"
+  shared_preferences_windows:
+    dependency: transitive
+    description:
+      name: shared_preferences_windows
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "2.1.1"
+  sky_engine:
+    dependency: transitive
+    description: flutter
+    source: sdk
+    version: "0.0.99"
+  source_span:
+    dependency: transitive
+    description:
+      name: source_span
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "1.8.2"
+  stack_trace:
+    dependency: transitive
+    description:
+      name: stack_trace
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "1.10.0"
+  stream_channel:
+    dependency: transitive
+    description:
+      name: stream_channel
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "2.1.0"
+  string_scanner:
+    dependency: transitive
+    description:
+      name: string_scanner
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "1.1.0"
+  sync_http:
+    dependency: transitive
+    description:
+      name: sync_http
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "0.3.0"
+  term_glyph:
+    dependency: transitive
+    description:
+      name: term_glyph
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "1.2.0"
+  test_api:
+    dependency: transitive
+    description:
+      name: test_api
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "0.4.9"
+  total_lints:
+    dependency: "direct dev"
+    description:
+      name: total_lints
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "2.17.4"
+  typed_data:
+    dependency: transitive
+    description:
+      name: typed_data
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "1.3.0"
+  url_launcher:
+    dependency: "direct main"
+    description:
+      name: url_launcher
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "6.1.5"
+  url_launcher_android:
+    dependency: transitive
+    description:
+      name: url_launcher_android
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "6.0.17"
+  url_launcher_ios:
+    dependency: transitive
+    description:
+      name: url_launcher_ios
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "6.0.17"
+  url_launcher_linux:
+    dependency: transitive
+    description:
+      name: url_launcher_linux
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "3.0.1"
+  url_launcher_macos:
+    dependency: transitive
+    description:
+      name: url_launcher_macos
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "3.0.1"
+  url_launcher_platform_interface:
+    dependency: transitive
+    description:
+      name: url_launcher_platform_interface
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "2.1.0"
+  url_launcher_web:
+    dependency: transitive
+    description:
+      name: url_launcher_web
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "2.0.12"
+  url_launcher_windows:
+    dependency: transitive
+    description:
+      name: url_launcher_windows
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "3.0.1"
+  url_strategy:
+    dependency: "direct main"
+    description:
+      name: url_strategy
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "0.2.0"
+  vector_math:
+    dependency: transitive
+    description:
+      name: vector_math
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "2.1.2"
+  vm_service:
+    dependency: transitive
+    description:
+      name: vm_service
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "8.2.2"
+  webdriver:
+    dependency: transitive
+    description:
+      name: webdriver
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "3.0.0"
+  win32:
+    dependency: transitive
+    description:
+      name: win32
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "2.7.0"
+  xdg_directories:
+    dependency: transitive
+    description:
+      name: xdg_directories
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "0.2.0+1"
+  xml:
+    dependency: transitive
+    description:
+      name: xml
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "5.4.1"
+  yaml:
+    dependency: transitive
+    description:
+      name: yaml
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "3.1.1"
+sdks:
+  dart: ">=2.17.6 <3.0.0"
+  flutter: ">=3.0.0"
diff --git a/learning/tour-of-beam/frontend/pubspec.yaml b/learning/tour-of-beam/frontend/pubspec.yaml
new file mode 100644
index 00000000000..220bfd5e5bc
--- /dev/null
+++ b/learning/tour-of-beam/frontend/pubspec.yaml
@@ -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.
+
+name: tour_of_beam
+description: Tour of Beam
+
+publish_to: 'none' # Remove this line if you wish to publish to pub.dev
+
+version: 0.1.0
+
+environment:
+  sdk: ">=2.17.6 <3.0.0"
+  flutter: ">=3.0.0 <4.0.0"
+
+dependencies:
+  easy_localization: ^3.0.1
+  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
+  provider: ^6.0.3
+  shared_preferences: ^2.0.15
+  url_launcher: ^6.1.5
+  url_strategy: ^0.2.0
+
+dev_dependencies:
+  flutter_test: { sdk: flutter }
+  integration_test: { sdk: flutter }
+  total_lints: ^2.17.0
+
+flutter:
+  uses-material-design: true
+  assets:
+    - assets/translations/en.yaml
+    - assets/png/
+    - assets/svg/
diff --git a/learning/tour-of-beam/frontend/test/config/theme/switch_notifier_test.dart b/learning/tour-of-beam/frontend/test/config/theme/switch_notifier_test.dart
new file mode 100644
index 00000000000..2565c073d2a
--- /dev/null
+++ b/learning/tour-of-beam/frontend/test/config/theme/switch_notifier_test.dart
@@ -0,0 +1,28 @@
+/*
+ * 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_test/flutter_test.dart';
+import 'package:tour_of_beam/config/theme/switch_notifier.dart';
+
+void main() {
+  group('theme mode', () {
+    test('light mode is default', () {
+      expect(ThemeSwitchNotifier().isDarkMode, false);
+    });
+  });
+}
diff --git a/learning/tour-of-beam/frontend/test_driver/integration_test.dart b/learning/tour-of-beam/frontend/test_driver/integration_test.dart
new file mode 100644
index 00000000000..6b59b37dd12
--- /dev/null
+++ b/learning/tour-of-beam/frontend/test_driver/integration_test.dart
@@ -0,0 +1,21 @@
+/*
+ * 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:integration_test/integration_test_driver.dart';
+
+Future<void> main() => integrationDriver();
diff --git a/learning/tour-of-beam/frontend/web/favicon.ico b/learning/tour-of-beam/frontend/web/favicon.ico
new file mode 100644
index 00000000000..47e6fdb5853
Binary files /dev/null and b/learning/tour-of-beam/frontend/web/favicon.ico differ
diff --git a/learning/tour-of-beam/frontend/web/index.html b/learning/tour-of-beam/frontend/web/index.html
new file mode 100644
index 00000000000..59b029d0363
--- /dev/null
+++ b/learning/tour-of-beam/frontend/web/index.html
@@ -0,0 +1,74 @@
+<!--
+    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.
+-->
+
+<!DOCTYPE html>
+<html lang="en">
+<head>
+  <meta charset="UTF-8">
+  <!--
+    If you are serving your web app in a path other than the root, change the
+    href value below to reflect the base path you are serving from.
+
+    The path provided below has to start and end with a slash "/" in order for
+    it to work correctly.
+
+    For more details:
+    * https://developer.mozilla.org/en-US/docs/Web/HTML/Element/base
+
+    This is a placeholder for base href that will be replaced by the value of
+    the `--base-href` argument provided to `flutter build`.
+  -->
+  <base href="/">
+
+  <meta content="IE=Edge" http-equiv="X-UA-Compatible">
+  <meta name="description" content="Tour of Beam">
+
+  <!-- iOS meta tags & icons -->
+  <meta name="apple-mobile-web-app-capable" content="yes">
+  <meta name="apple-mobile-web-app-status-bar-style" content="black">
+  <meta name="apple-mobile-web-app-title" content="Tour of Beam">
+  <link rel="shortcut icon" type="image/x-icon" href="/favicon.ico">
+
+  <title>Tour of Beam</title>
+  <link rel="manifest" href="manifest.json">
+
+  <script>
+    // The value below is injected by flutter build, do not touch.
+    var serviceWorkerVersion = null;
+  </script>
+  <!-- This script adds the flutter initialization JS code -->
+  <script src="flutter.js" defer></script>
+</head>
+<body>
+  <script>
+    window.addEventListener('load', function(ev) {
+      // Download main.dart.js
+      _flutter.loader.loadEntrypoint({
+        serviceWorker: {
+          serviceWorkerVersion: serviceWorkerVersion,
+        }
+      }).then(function(engineInitializer) {
+        return engineInitializer.initializeEngine();
+      }).then(function(appRunner) {
+        return appRunner.runApp();
+      });
+    });
+  </script>
+</body>
+</html>
diff --git a/learning/tour-of-beam/frontend/web/manifest.json b/learning/tour-of-beam/frontend/web/manifest.json
new file mode 100644
index 00000000000..c0d00f9b8e4
--- /dev/null
+++ b/learning/tour-of-beam/frontend/web/manifest.json
@@ -0,0 +1,18 @@
+{
+    "name": "frontend",
+    "short_name": "frontend",
+    "start_url": ".",
+    "display": "standalone",
+    "background_color": "#0175C2",
+    "theme_color": "#0175C2",
+    "description": "Tour of Beam",
+    "orientation": "portrait-primary",
+    "prefer_related_applications": false,
+    "icons": [
+        {
+            "src": "favicon.ico",
+            "sizes": "256x266",
+            "type": "image/vnd.microsoft.icon"
+        }
+    ]
+}
diff --git a/playground/frontend/playground_components/.metadata b/playground/frontend/playground_components/.metadata
new file mode 100644
index 00000000000..e7011f64f39
--- /dev/null
+++ b/playground/frontend/playground_components/.metadata
@@ -0,0 +1,10 @@
+# This file tracks properties of this Flutter project.
+# Used by Flutter tool to assess capabilities and perform upgrades etc.
+#
+# This file should be version controlled and should not be manually edited.
+
+version:
+  revision: f1875d570e39de09040c8f79aa13cc56baab8db1
+  channel: stable
+
+project_type: package
diff --git a/playground/frontend/playground_components/CHANGELOG.md b/playground/frontend/playground_components/CHANGELOG.md
new file mode 100644
index 00000000000..504fa05fe23
--- /dev/null
+++ b/playground/frontend/playground_components/CHANGELOG.md
@@ -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.
+-->
+
+## 0.0.1
+
+* TODO: Describe initial release.
diff --git a/playground/frontend/playground_components/LICENSE b/playground/frontend/playground_components/LICENSE
new file mode 100644
index 00000000000..8c048c96fb5
--- /dev/null
+++ b/playground/frontend/playground_components/LICENSE
@@ -0,0 +1,407 @@
+
+                                 Apache License
+                           Version 2.0, January 2004
+                        http://www.apache.org/licenses/
+
+   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+   1. Definitions.
+
+      "License" shall mean the terms and conditions for use, reproduction,
+      and distribution as defined by Sections 1 through 9 of this document.
+
+      "Licensor" shall mean the copyright owner or entity authorized by
+      the copyright owner that is granting the License.
+
+      "Legal Entity" shall mean the union of the acting entity and all
+      other entities that control, are controlled by, or are under common
+      control with that entity. For the purposes of this definition,
+      "control" means (i) the power, direct or indirect, to cause the
+      direction or management of such entity, whether by contract or
+      otherwise, or (ii) ownership of fifty percent (50%) or more of the
+      outstanding shares, or (iii) beneficial ownership of such entity.
+
+      "You" (or "Your") shall mean an individual or Legal Entity
+      exercising permissions granted by this License.
+
+      "Source" form shall mean the preferred form for making modifications,
+      including but not limited to software source code, documentation
+      source, and configuration files.
+
+      "Object" form shall mean any form resulting from mechanical
+      transformation or translation of a Source form, including but
+      not limited to compiled object code, generated documentation,
+      and conversions to other media types.
+
+      "Work" shall mean the work of authorship, whether in Source or
+      Object form, made available under the License, as indicated by a
+      copyright notice that is included in or attached to the work
+      (an example is provided in the Appendix below).
+
+      "Derivative Works" shall mean any work, whether in Source or Object
+      form, that is based on (or derived from) the Work and for which the
+      editorial revisions, annotations, elaborations, or other modifications
+      represent, as a whole, an original work of authorship. For the purposes
+      of this License, Derivative Works shall not include works that remain
+      separable from, or merely link (or bind by name) to the interfaces of,
+      the Work and Derivative Works thereof.
+
+      "Contribution" shall mean any work of authorship, including
+      the original version of the Work and any modifications or additions
+      to that Work or Derivative Works thereof, that is intentionally
+      submitted to Licensor for inclusion in the Work by the copyright owner
+      or by an individual or Legal Entity authorized to submit on behalf of
+      the copyright owner. For the purposes of this definition, "submitted"
+      means any form of electronic, verbal, or written communication sent
+      to the Licensor or its representatives, including but not limited to
+      communication on electronic mailing lists, source code control systems,
+      and issue tracking systems that are managed by, or on behalf of, the
+      Licensor for the purpose of discussing and improving the Work, but
+      excluding communication that is conspicuously marked or otherwise
+      designated in writing by the copyright owner as "Not a Contribution."
+
+      "Contributor" shall mean Licensor and any individual or Legal Entity
+      on behalf of whom a Contribution has been received by Licensor and
+      subsequently incorporated within the Work.
+
+   2. Grant of Copyright License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      copyright license to reproduce, prepare Derivative Works of,
+      publicly display, publicly perform, sublicense, and distribute the
+      Work and such Derivative Works in Source or Object form.
+
+   3. Grant of Patent License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      (except as stated in this section) patent license to make, have made,
+      use, offer to sell, sell, import, and otherwise transfer the Work,
+      where such license applies only to those patent claims licensable
+      by such Contributor that are necessarily infringed by their
+      Contribution(s) alone or by combination of their Contribution(s)
+      with the Work to which such Contribution(s) was submitted. If You
+      institute patent litigation against any entity (including a
+      cross-claim or counterclaim in a lawsuit) alleging that the Work
+      or a Contribution incorporated within the Work constitutes direct
+      or contributory patent infringement, then any patent licenses
+      granted to You under this License for that Work shall terminate
+      as of the date such litigation is filed.
+
+   4. Redistribution. You may reproduce and distribute copies of the
+      Work or Derivative Works thereof in any medium, with or without
+      modifications, and in Source or Object form, provided that You
+      meet the following conditions:
+
+      (a) You must give any other recipients of the Work or
+          Derivative Works a copy of this License; and
+
+      (b) You must cause any modified files to carry prominent notices
+          stating that You changed the files; and
+
+      (c) You must retain, in the Source form of any Derivative Works
+          that You distribute, all copyright, patent, trademark, and
+          attribution notices from the Source form of the Work,
+          excluding those notices that do not pertain to any part of
+          the Derivative Works; and
+
+      (d) If the Work includes a "NOTICE" text file as part of its
+          distribution, then any Derivative Works that You distribute must
+          include a readable copy of the attribution notices contained
+          within such NOTICE file, excluding those notices that do not
+          pertain to any part of the Derivative Works, in at least one
+          of the following places: within a NOTICE text file distributed
+          as part of the Derivative Works; within the Source form or
+          documentation, if provided along with the Derivative Works; or,
+          within a display generated by the Derivative Works, if and
+          wherever such third-party notices normally appear. The contents
+          of the NOTICE file are for informational purposes only and
+          do not modify the License. You may add Your own attribution
+          notices within Derivative Works that You distribute, alongside
+          or as an addendum to the NOTICE text from the Work, provided
+          that such additional attribution notices cannot be construed
+          as modifying the License.
+
+      You may add Your own copyright statement to Your modifications and
+      may provide additional or different license terms and conditions
+      for use, reproduction, or distribution of Your modifications, or
+      for any such Derivative Works as a whole, provided Your use,
+      reproduction, and distribution of the Work otherwise complies with
+      the conditions stated in this License.
+
+   5. Submission of Contributions. Unless You explicitly state otherwise,
+      any Contribution intentionally submitted for inclusion in the Work
+      by You to the Licensor shall be under the terms and conditions of
+      this License, without any additional terms or conditions.
+      Notwithstanding the above, nothing herein shall supersede or modify
+      the terms of any separate license agreement you may have executed
+      with Licensor regarding such Contributions.
+
+   6. Trademarks. This License does not grant permission to use the trade
+      names, trademarks, service marks, or product names of the Licensor,
+      except as required for reasonable and customary use in describing the
+      origin of the Work and reproducing the content of the NOTICE file.
+
+   7. Disclaimer of Warranty. Unless required by applicable law or
+      agreed to in writing, Licensor provides the Work (and each
+      Contributor provides its Contributions) on an "AS IS" BASIS,
+      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+      implied, including, without limitation, any warranties or conditions
+      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+      PARTICULAR PURPOSE. You are solely responsible for determining the
+      appropriateness of using or redistributing the Work and assume any
+      risks associated with Your exercise of permissions under this License.
+
+   8. Limitation of Liability. In no event and under no legal theory,
+      whether in tort (including negligence), contract, or otherwise,
+      unless required by applicable law (such as deliberate and grossly
+      negligent acts) or agreed to in writing, shall any Contributor be
+      liable to You for damages, including any direct, indirect, special,
+      incidental, or consequential damages of any character arising as a
+      result of this License or out of the use or inability to use the
+      Work (including but not limited to damages for loss of goodwill,
+      work stoppage, computer failure or malfunction, or any and all
+      other commercial damages or losses), even if such Contributor
+      has been advised of the possibility of such damages.
+
+   9. Accepting Warranty or Additional Liability. While redistributing
+      the Work or Derivative Works thereof, You may choose to offer,
+      and charge a fee for, acceptance of support, warranty, indemnity,
+      or other liability obligations and/or rights consistent with this
+      License. However, in accepting such obligations, You may act only
+      on Your own behalf and on Your sole responsibility, not on behalf
+      of any other Contributor, and only if You agree to indemnify,
+      defend, and hold each Contributor harmless for any liability
+      incurred by, or claims asserted against, such Contributor by reason
+      of your accepting any such warranty or additional liability.
+
+   END OF TERMS AND CONDITIONS
+
+   APPENDIX: How to apply the Apache License to your work.
+
+      To apply the Apache License to your work, attach the following
+      boilerplate notice, with the fields enclosed by brackets "[]"
+      replaced with your own identifying information. (Don't include
+      the brackets!)  The text should be enclosed in the appropriate
+      comment syntax for the file format. We also recommend that a
+      file or class name and description of purpose be included on the
+      same "printed page" as the copyright notice for easier
+      identification within third-party archives.
+
+   Copyright [yyyy] [name of copyright owner]
+
+   Licensed 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.
+
+   A part of several convenience binary distributions of this software is licensed as follows:
+
+   Google Protobuf:
+     Copyright 2008 Google Inc.  All rights reserved.
+
+     Redistribution and use in source and binary forms, with or without
+     modification, are permitted provided that the following conditions are
+     met:
+
+         * Redistributions of source code must retain the above copyright
+     notice, this list of conditions and the following disclaimer.
+         * Redistributions in binary form must reproduce the above
+     copyright notice, this list of conditions and the following disclaimer
+     in the documentation and/or other materials provided with the
+     distribution.
+         * Neither the name of Google Inc. nor the names of its
+     contributors may be used to endorse or promote products derived from
+     this software without specific prior written permission.
+
+     THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+     "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+     LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+     A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+     OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+     SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+     LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+     DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+     THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+     (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+     OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+     Code generated by the Protocol Buffer compiler is owned by the owner
+     of the input file used when generating it.  This code is not
+     standalone and requires a support library to be linked with it.  This
+     support library is itself covered by the above license.
+
+   jsr-305:
+    Copyright (c) 2007-2009, JSR305 expert group
+    All rights reserved.
+
+    https://opensource.org/licenses/BSD-3-Clause
+
+    Redistribution and use in source and binary forms, with or without
+    modification, are permitted provided that the following conditions are met:
+
+        * Redistributions of source code must retain the above copyright notice,
+          this list of conditions and the following disclaimer.
+        * Redistributions in binary form must reproduce the above copyright notice,
+          this list of conditions and the following disclaimer in the documentation
+          and/or other materials provided with the distribution.
+        * Neither the name of the JSR305 expert group nor the names of its
+          contributors may be used to endorse or promote products derived from
+          this software without specific prior written permission.
+
+    THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+    AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
+    THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+    ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
+    LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+    CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+    SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+    INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+    CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+    ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+    POSSIBILITY OF SUCH DAMAGE.
+
+   janino-compiler:
+    Janino - An embedded Java[TM] compiler
+
+    Copyright (c) 2001-2016, Arno Unkrig
+    Copyright (c) 2015-2016  TIBCO Software Inc.
+    All rights reserved.
+
+    Redistribution and use in source and binary forms, with or without
+    modification, are permitted provided that the following conditions
+    are met:
+
+       1. Redistributions of source code must retain the above copyright
+          notice, this list of conditions and the following disclaimer.
+       2. Redistributions in binary form must reproduce the above
+          copyright notice, this list of conditions and the following
+          disclaimer in the documentation and/or other materials
+          provided with the distribution.
+       3. Neither the name of JANINO nor the names of its contributors
+          may be used to endorse or promote products derived from this
+          software without specific prior written permission.
+
+    THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+    AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+    IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+    ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE
+    LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+    CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+    SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+    INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER
+    IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
+    OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN
+    IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+   jline:
+    Copyright (c) 2002-2016, the original author or authors.
+    All rights reserved.
+
+    http://www.opensource.org/licenses/bsd-license.php
+
+    Redistribution and use in source and binary forms, with or
+    without modification, are permitted provided that the following
+    conditions are met:
+
+    Redistributions of source code must retain the above copyright
+    notice, this list of conditions and the following disclaimer.
+
+    Redistributions in binary form must reproduce the above copyright
+    notice, this list of conditions and the following disclaimer
+    in the documentation and/or other materials provided with
+    the distribution.
+
+    Neither the name of JLine nor the names of its contributors
+    may be used to endorse or promote products derived from this
+    software without specific prior written permission.
+
+    THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+    "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING,
+    BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY
+    AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO
+    EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
+    FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY,
+    OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+    PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+    DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
+    AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+    LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING
+    IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
+    OF THE POSSIBILITY OF SUCH DAMAGE.
+
+   sqlline:
+    SQLLine - Shell for issuing SQL to relational databases via JDBC
+
+    Copyright (c) 2002,2003,2004,2005,2006,2007 Marc Prud'hommeaux
+    Copyright (c) 2004-2010 The Eigenbase Project
+    Copyright (c) 2013-2017 Julian Hyde
+    All rights reserved.
+
+    ===============================================================================
+
+    Licensed under the Modified BSD License (the "License"); you may not
+    use this file except in compliance with the License. You may obtain a
+    copy of the License at:
+
+    http://opensource.org/licenses/BSD-3-Clause
+
+    Redistribution and use in source and binary forms,
+    with or without modification, are permitted provided
+    that the following conditions are met:
+
+    (1) Redistributions of source code must retain the above copyright
+        notice, this list of conditions and the following disclaimer.
+
+    (2) Redistributions in binary form must reproduce the above copyright
+        notice, this list of conditions and the following disclaimer in the
+        documentation and/or other materials provided with the
+        distribution.
+
+    (3) The name of the author may not be used to endorse or promote
+        products derived from this software without specific prior written
+        permission.
+
+    THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+    "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+    LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+    A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+    OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+    SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+    LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+    DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+    THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+    (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+    OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+   slf4j:
+    Copyright (c) 2004-2017 QOS.ch
+    All rights reserved.
+
+    Permission is hereby granted, free  of charge, to any person obtaining
+    a  copy  of this  software  and  associated  documentation files  (the
+    "Software"), to  deal in  the Software without  restriction, including
+    without limitation  the rights to  use, copy, modify,  merge, publish,
+    distribute,  sublicense, and/or sell  copies of  the Software,  and to
+    permit persons to whom the Software  is furnished to do so, subject to
+    the following conditions:
+
+    The  above  copyright  notice  and  this permission  notice  shall  be
+    included in all copies or substantial portions of the Software.
+
+    THE  SOFTWARE IS  PROVIDED  "AS  IS", WITHOUT  WARRANTY  OF ANY  KIND,
+    EXPRESS OR  IMPLIED, INCLUDING  BUT NOT LIMITED  TO THE  WARRANTIES OF
+    MERCHANTABILITY,    FITNESS    FOR    A   PARTICULAR    PURPOSE    AND
+    NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+    LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+    OF CONTRACT, TORT OR OTHERWISE,  ARISING FROM, OUT OF OR IN CONNECTION
+    WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE
+
+See the adjacent LICENSE.python file, if present, for additional licenses that
+apply to parts of Apache Beam Python.
diff --git a/playground/frontend/playground_components/README.md b/playground/frontend/playground_components/README.md
new file mode 100644
index 00000000000..9c4ef73d25d
--- /dev/null
+++ b/playground/frontend/playground_components/README.md
@@ -0,0 +1,45 @@
+<!--
+    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.
+-->
+
+TODO: Put a short description of the package here that helps potential users
+know whether this package might be useful for them.
+
+## Features
+
+TODO: List what your package can do. Maybe include images, gifs, or videos.
+
+## Getting started
+
+TODO: List prerequisites and provide or point to information on how to
+start using the package.
+
+## Usage
+
+TODO: Include short and useful examples for package users. Add longer examples
+to `/example` folder.
+
+```dart
+const like = 'sample';
+```
+
+## Additional information
+
+TODO: Tell users more about the package: where to find more information, how to
+contribute to the package, how to file issues, what response they can expect
+from the package authors, and more.
diff --git a/playground/frontend/playground_components/analysis_options.yaml b/playground/frontend/playground_components/analysis_options.yaml
new file mode 100644
index 00000000000..318f01bfa2f
--- /dev/null
+++ b/playground/frontend/playground_components/analysis_options.yaml
@@ -0,0 +1,21 @@
+#  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.
+
+include: package:total_lints/app.yaml
+
+# Additional information about this file can be found at
+# https://dart.dev/guides/language/analysis-options
diff --git a/playground/frontend/playground_components/lib/dismissible_overlay.dart b/playground/frontend/playground_components/lib/dismissible_overlay.dart
new file mode 100644
index 00000000000..2119c5314c7
--- /dev/null
+++ b/playground/frontend/playground_components/lib/dismissible_overlay.dart
@@ -0,0 +1,43 @@
+/*
+ * 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';
+
+class DismissibleOverlay extends StatelessWidget {
+  final void Function() close;
+  final Positioned child;
+
+  const DismissibleOverlay({
+    required this.close,
+    required this.child,
+  });
+
+  @override
+  Widget build(BuildContext context) {
+    return Stack(
+      children: [
+        Positioned.fill(
+          child: GestureDetector(
+            onTap: close,
+          ),
+        ),
+        child,
+      ],
+    );
+  }
+}
diff --git a/playground/frontend/playground_components/pubspec.yaml b/playground/frontend/playground_components/pubspec.yaml
new file mode 100644
index 00000000000..56037dea769
--- /dev/null
+++ b/playground/frontend/playground_components/pubspec.yaml
@@ -0,0 +1,34 @@
+#  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.
+
+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: