diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index d6c63050c0..add8c87668 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -29,11 +29,11 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@7211b7c8077ea37d8641b6271f6a365a22a5fbfa + uses: github/codeql-action/init@8aad20d150bbac5944a9f9d289da16a4b0d87c1e with: languages: ${{ matrix.language }} # If you wish to specify custom queries, you can do so here or in a config file. @@ -44,7 +44,7 @@ jobs: # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). # If this step fails, then you should remove it and run the build manually (see below) - name: Autobuild - uses: github/codeql-action/autobuild@7211b7c8077ea37d8641b6271f6a365a22a5fbfa + uses: github/codeql-action/autobuild@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # ℹ️ Command-line programs to run using the OS shell. # 📚 https://git.io/JvXDl @@ -58,4 +58,4 @@ jobs: # make release - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@7211b7c8077ea37d8641b6271f6a365a22a5fbfa + uses: github/codeql-action/analyze@8aad20d150bbac5944a9f9d289da16a4b0d87c1e diff --git a/.github/workflows/compile_host_redirect_js.yml b/.github/workflows/compile-host-redirect-js.yml similarity index 95% rename from .github/workflows/compile_host_redirect_js.yml rename to .github/workflows/compile-host-redirect-js.yml index abdbfa078e..3b1b5c6fd1 100644 --- a/.github/workflows/compile_host_redirect_js.yml +++ b/.github/workflows/compile-host-redirect-js.yml @@ -23,7 +23,7 @@ jobs: github.repository == 'cfug/flutter.cn' runs-on: ubuntu-latest steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e with: node-version: ${{ env.NODE_VERSION }} diff --git a/.github/workflows/dart.yml b/.github/workflows/dart.yml index 481158e315..1a11ef836b 100644 --- a/.github/workflows/dart.yml +++ b/.github/workflows/dart.yml @@ -52,7 +52,7 @@ jobs: experimental: true continue-on-error: ${{ matrix.experimental }} steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 - uses: subosito/flutter-action@e938fdf56512cc96ef2f93601a5a40bde3801046 with: channel: ${{ matrix.branch }} diff --git a/.github/workflows/stage.yml b/.github/workflows/docs-stage.yml similarity index 80% rename from .github/workflows/stage.yml rename to .github/workflows/docs-stage.yml index 7e4d67f0a4..309f62b8e6 100644 --- a/.github/workflows/stage.yml +++ b/.github/workflows/docs-stage.yml @@ -4,6 +4,17 @@ on: pull_request: branches: - main + paths: + - 'sites/docs/**' + - 'examples/**' + - 'packages/excerpter/**' + - 'firebase.json' + - 'tool/dash_site/**' + - 'tool/config/linkcheck-skip-list.txt' + - 'pubspec.yaml' + - 'pubspec.lock' + - 'analysis_options.yaml' + - '.github/workflows/docs.yml' # Declare default permissions as read only. permissions: read-all @@ -18,7 +29,7 @@ jobs: if: | github.event.pull_request.head.repo.full_name == 'cfug/flutter.cn' steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 with: # docs.flutter.cn | https://github.com/cfug/flutter.cn/pull/1518 fetch-depth: 0 diff --git a/.github/workflows/translator_bot.yml b/.github/workflows/docs-translator-bot.yml similarity index 96% rename from .github/workflows/translator_bot.yml rename to .github/workflows/docs-translator-bot.yml index 5dd2c499d9..39b6817f83 100644 --- a/.github/workflows/translator_bot.yml +++ b/.github/workflows/docs-translator-bot.yml @@ -1,4 +1,4 @@ -name: translator_bot +name: translator_bot (docs) on: issue_comment: diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 35425bd763..d74c7c32f9 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -1,6 +1,7 @@ name: Docs site on: + workflow_dispatch: push: branches: - main @@ -45,7 +46,7 @@ jobs: if: github.repository == 'cfug/flutter.cn' timeout-minutes: 20 steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 - uses: dart-lang/setup-dart@65eb853c7ba17dde3be364c3d2858773e7144260 with: sdk: beta @@ -60,7 +61,7 @@ jobs: if: github.repository == 'cfug/flutter.cn' timeout-minutes: 30 steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 - uses: dart-lang/setup-dart@65eb853c7ba17dde3be364c3d2858773e7144260 with: sdk: beta @@ -87,7 +88,7 @@ jobs: if: github.repository == 'cfug/flutter.cn' timeout-minutes: 10 steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 - uses: dart-lang/setup-dart@65eb853c7ba17dde3be364c3d2858773e7144260 with: sdk: beta @@ -105,7 +106,7 @@ jobs: github.ref == 'refs/heads/main' && github.repository == 'cfug/flutter.cn' steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 with: # docs.flutter.cn | https://github.com/cfug/flutter.cn/pull/1518 fetch-depth: 0 @@ -123,7 +124,7 @@ jobs: run: bash tool/translator/build.sh shell: bash - name: Deploy to production - run: bash tool/translator/deploy-cn.sh + run: bash tool/deploy/deploy-cn-docs.sh shell: bash env: DEPLOY_USER: ${{ secrets.DOCS_REPO_DEPLOY_USER }} diff --git a/.github/workflows/www.yml b/.github/workflows/www.yml new file mode 100644 index 0000000000..080b69ec35 --- /dev/null +++ b/.github/workflows/www.yml @@ -0,0 +1,97 @@ +name: Marketing site + +on: + workflow_dispatch: + push: + branches: + - main + paths: + - 'sites/www/**' + - 'tool/dash_site/**' + - 'tool/config/linkcheck-skip-list.txt' + - 'pubspec.yaml' + - 'pubspec.lock' + - 'analysis_options.yaml' + - '.github/workflows/www.yml' + pull_request: + branches: + - main + paths: + - 'sites/www/**' + - 'tool/dash_site/**' + - 'tool/config/linkcheck-skip-list.txt' + - 'pubspec.yaml' + - 'pubspec.lock' + - 'analysis_options.yaml' + - '.github/workflows/www.yml' + # schedule: + # - cron: '0 0 * * 0' + +permissions: read-all + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: ${{ github.event_name == 'pull_request' }} + +jobs: + linkcheck: + name: Build and check links + runs-on: ubuntu-latest + if: github.repository == 'cfug/flutter.cn' + timeout-minutes: 30 + steps: + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 + - uses: dart-lang/setup-dart@65eb853c7ba17dde3be364c3d2858773e7144260 + with: + sdk: beta + - name: Fetch Dart dependencies + run: dart pub get + - name: Install firebase-tools + run: curl -sL https://firebase.tools | bash + - name: Build site + run: dart run dash_site --site=www build + - name: Check for broken Markdown link references + run: dart run dash_site --site=www check-link-references + - name: Check for broken internal links + run: dart run dash_site --site=www check-links + + firebase-validate: + name: Validate Firebase configuration + runs-on: ubuntu-latest + if: github.repository == 'cfug/flutter.cn' + timeout-minutes: 10 + steps: + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 + - uses: dart-lang/setup-dart@65eb853c7ba17dde3be364c3d2858773e7144260 + with: + sdk: beta + - name: Fetch Dart dependencies + run: dart pub get + - name: Validate the firebase.json file + run: dart run dash_site --site=www verify-firebase-json + + deploy: + name: Deploy to production + needs: [linkcheck, firebase-validate] + runs-on: ubuntu-latest + if: | + github.event_name == 'push' && + github.ref == 'refs/heads/main' && + github.repository == 'cfug/flutter.cn' + steps: + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 + with: + fetch-depth: 0 + - uses: dart-lang/setup-dart@65eb853c7ba17dde3be364c3d2858773e7144260 + with: + sdk: beta + - name: Fetch Dart dependencies + run: dart pub get + - name: Build site + run: dart run dash_site --site=www build --release + - name: Deploy to production + run: bash tool/deploy/deploy-cn-www.sh + shell: bash + env: + DEPLOY_USER: ${{ secrets.DOCS_REPO_DEPLOY_USER }} + DEPLOY_TOKEN: ${{ secrets.DOCS_REPO_DEPLOY_TOKEN }} diff --git a/AGENTS.md b/AGENTS.md index 9b8a7148cb..62929dde03 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -7,14 +7,20 @@ content and code in this repository. ## Project overview This repository contains the source code and content for -the Flutter framework's documentation website. -It is hosted at `docs.flutter.dev` and shouldn't be confused with -the marketing site at `flutter.dev` or the API docs at `api.flutter.dev`. +the Flutter framework's documentation and marketing websites: -The website is statically built and implemented with +- `docs.flutter.dev`: + The Flutter framework's documentation website. +- `flutter.dev`: + Flutter's homepage and marketing website. + +The API docs at `api.flutter.dev` are embedded in the Flutter SDK source +and are maintained in the repository. + +The websites are statically built and implemented with Dart and the [Jaspr](https://jaspr.site) web framework. -For loading content, data, and some of its assets, -it uses [Jaspr Content](https://docs.jaspr.site/content). +For loading content, data, and some assets, +they use [Jaspr Content](https://docs.jaspr.site/content). ### Directory structure @@ -30,15 +36,24 @@ it uses [Jaspr Content](https://docs.jaspr.site/content). for the sidenav, glossary, and various indices. - `sites/docs/src/_includes/`: Liquid partial files written in Markdown. + - `sites/www/`: + The implementation of flutter.dev, + written in Dart using Jaspr and Jaspr Content. + - `sites/www/content/`: + Markdown-based marketing pages and structured content. + - `sites/www/lib/`: + Dart source code for the site. + - `sites/www/firebase.json`: + Firebase Hosting configuration for flutter.dev. - `examples/`: A pub workspace containing Dart and Flutter code examples, - referenced by code excerpts in the Markdown files. + referenced by code excerpts in the docs Markdown files. - `tool/dash_site/`: CLI tool for site development and maintenance. ## Common commands -While working on the site, +While working on these sites, you might need to run these commands: ```bash @@ -46,13 +61,15 @@ you might need to run these commands: dart pub get # Serve a dev server of the site locally: -dart run dash_site serve +dart run dash_site --site=docs serve +dart run dash_site --site=www serve # Build a production version of the site: -dart run dash_site build +dart run dash_site --site=docs build +dart run dash_site --site=www build -# Sync code excerpts to Markdown files: -dart run dash_site refresh-excerpts +# Sync docs code excerpts to Markdown files: +dart run dash_site --site=docs refresh-excerpts # Learn what other commands are available: dart run dash_site --help diff --git a/README.md b/README.md index a2fff356a4..28f621a256 100644 --- a/README.md +++ b/README.md @@ -148,9 +148,8 @@ following the instructions in [Get the prerequisites](#get-the-prerequisites). This command generates and serves the site on a local port that's printed to your terminal. -5. View your changes in the browser by navigating to . - - Note the port might be different if `8080` is taken. +5. View your changes in the browser by navigating to + the local URL printed to your terminal. 6. Make your changes to the local repo. diff --git a/examples/cookbook/plugins/google_mobile_ads/pubspec.yaml b/examples/cookbook/plugins/google_mobile_ads/pubspec.yaml index 7ebed256f7..6b207429d7 100644 --- a/examples/cookbook/plugins/google_mobile_ads/pubspec.yaml +++ b/examples/cookbook/plugins/google_mobile_ads/pubspec.yaml @@ -9,7 +9,7 @@ dependencies: flutter: sdk: flutter - google_mobile_ads: ^8.0.0 + google_mobile_ads: ^9.0.0 flutter: uses-material-design: true diff --git a/examples/get-started/flutter-for/android_devs/lib/animation.dart b/examples/get-started/flutter-for/android_devs/lib/animation.dart index 3c453baeb7..560b7ede30 100644 --- a/examples/get-started/flutter-for/android_devs/lib/animation.dart +++ b/examples/get-started/flutter-for/android_devs/lib/animation.dart @@ -41,6 +41,12 @@ class _MyFadeTest extends State with TickerProviderStateMixin { curve = CurvedAnimation(parent: controller, curve: Curves.easeIn); } + @override + void dispose() { + controller.dispose(); + super.dispose(); + } + @override Widget build(BuildContext context) { return Scaffold( diff --git a/examples/get-started/flutter-for/android_devs/lib/events.dart b/examples/get-started/flutter-for/android_devs/lib/events.dart index f74252bb78..9351b3bbb1 100644 --- a/examples/get-started/flutter-for/android_devs/lib/events.dart +++ b/examples/get-started/flutter-for/android_devs/lib/events.dart @@ -62,6 +62,12 @@ class _SampleAppState extends State curve = CurvedAnimation(parent: controller, curve: Curves.easeIn); } + @override + void dispose() { + controller.dispose(); + super.dispose(); + } + @override Widget build(BuildContext context) { return Scaffold( diff --git a/examples/platform_integration/pigeon/pubspec.yaml b/examples/platform_integration/pigeon/pubspec.yaml index 9259d83a51..5224f9d76f 100644 --- a/examples/platform_integration/pigeon/pubspec.yaml +++ b/examples/platform_integration/pigeon/pubspec.yaml @@ -13,7 +13,7 @@ dependencies: flutter: sdk: flutter meta: any - pigeon: ^26.3.4 + pigeon: ^27.1.0 dev_dependencies: flutter_test: diff --git a/firebase.json b/firebase.json index 25b272d1a3..68d30ebbd0 100644 --- a/firebase.json +++ b/firebase.json @@ -19,6 +19,8 @@ { "regex": "(.*)\\.$", "destination": ":1", "type": 301 }, { "source": "/ads", "destination": "https://flutter.cn/monetization", "type": 301 }, + { "source": "/blog", "destination": "https://flutter.cn/blog", "type": 301 }, + { "source": "/blog/:rest*", "destination": "https://flutter.cn/blog/:rest*", "type": 301 }, { "source": "/community", "destination": "https://flutter.cn/community", "type": 301 }, { "source": "/gallery", "destination": "https://github.com/flutter/gallery#flutter-gallery", "type": 301 }, { "source": "/showcase", "destination": "https://flutter.cn/showcase", "type": 301 }, @@ -33,9 +35,10 @@ { "source": "/ai-best-practices/:rest*", "destination": "/ai/best-practices/:rest*", "type": 301 }, { "source": "/ai-toolkit", "destination": "/ai/ai-toolkit", "type": 301 }, { "source": "/ai-toolkit/:rest*", "destination": "/ai/ai-toolkit/:rest*", "type": 301 }, - { "source": "/ai/flutter-ext-for-gemini", "destination": "/ai/gemini-cli-extension", "type": 301 }, + { "source": "/ai/flutter-ext-for-gemini", "destination": "/ai/antigravity-cli", "type": 301 }, + { "source": "/ai/gemini-cli-extension", "destination": "/ai/antigravity-cli", "type": 301 }, { "source": "/ai/best-practices/tool-calls-aka-function-calls", "destination": "/ai/best-practices/tool-calls", "type": 301 }, - { "source": "/ai/antigravity", "destination": "/ai/coding-assistants", "type": 301 }, + { "source": "/tools/antigravity", "destination": "/ai/antigravity", "type": 301 }, { "source": "/ai/gemini-code-assist", "destination": "/ai/coding-assistants", "type": 301 }, { "source": "/ai/firebase-ai-logic", "destination": "https://firebase.google.cn/docs/ai-logic/get-started?platform=flutter", "type": 301 }, { "source": "/android-release", "destination": "/deployment/android", "type": 301 }, @@ -167,20 +170,23 @@ { "source": "/learn/learning-resources", "destination": "/reference/learning-resources", "type": 301 }, { "source": "/material-3-migration", "destination": "/release/breaking-changes/material-3-migration", "type": 301 }, { "source": "/packages-and-plugins/androidx-compatibility", "destination": "/platform-integration/android/androidx-migration", "type": 301 }, - { "source": "/packages-and-plugins/c-interop", "destination": "/platform-integration/android/c-interop", "type": 301 }, + { "source": "/packages-and-plugins/c-interop", "destination": "/platform-integration/legacy-ffi-plugin", "type": 301 }, { "source": "/packages-and-plugins/plugins-in-tests", "destination": "/testing/plugins-in-tests", "type": 301 }, { "source": "/packages-and-plugins/plugin-api-migration", "destination": "/release/breaking-changes/plugin-api-migration", "type": 301 }, { "source": "/platform-integration/android/androidx-migration", "destination": "/release/breaking-changes/androidx-migration", "type": 301 }, + { "source": "/platform-integration/android/c-interop", "destination": "/platform-integration/legacy-ffi-plugin", "type": 301 }, { "source": "/platform-integration/android/install-android", "destination": "/platform-integration/android/setup", "type": 301 }, { "source": "/platform-integration/android/install-android/:rest*", "destination": "/platform-integration/android/setup", "type": 301 }, { "source": "/platform-integration/android/splash-screen-migration", "destination": "/release/breaking-changes/splash-screen-migration", "type": 301 }, - { "source": "/platform-integration/c-interop", "destination": "/platform-integration", "type": 301 }, + { "source": "/platform-integration/c-interop", "destination": "/platform-integration/legacy-ffi-plugin", "type": 301 }, { "source": "/platform-integration/ios-app-clip", "destination": "/platform-integration/ios/ios-app-clip", "type": 301 }, + { "source": "/platform-integration/ios/c-interop", "destination": "/platform-integration/legacy-ffi-plugin", "type": 301 }, { "source": "/platform-integration/ios/install-ios", "destination": "/platform-integration/ios/setup", "type": 301 }, { "source": "/platform-integration/ios/install-ios/:rest*", "destination": "/platform-integration/ios/setup", "type": 301 }, { "source": "/platform-integration/ios/splash-screen", "destination": "/platform-integration/ios/launch-screen", "type": 301 }, { "source": "/platform-integration/linux/install-linux", "destination": "/platform-integration/linux/setup", "type": 301 }, { "source": "/platform-integration/linux/install-linux/:rest*", "destination": "/platform-integration/linux/setup", "type": 301 }, + { "source": "/platform-integration/macos/c-interop", "destination": "/platform-integration/legacy-ffi-plugin", "type": 301 }, { "source": "/platform-integration/macos/install-macos", "destination": "/platform-integration/macos/setup", "type": 301 }, { "source": "/platform-integration/macos/install-macos/:rest*", "destination": "/platform-integration/macos/setup", "type": 301 }, { "source": "/platform-integration/platform-adaptations", "destination": "/ui/adaptive-responsive/platform-adaptations", "type": 301 }, @@ -809,7 +815,7 @@ { "source": "/to/add-to-app", "destination": "/add-to-app", "type": 301 }, { "source": "/to/add-swift-package-manager-manually", "destination": "/packages-and-plugins/swift-package-manager/for-app-developers#add-to-a-flutter-app-manually", "type": 301 }, { "source": "/to/add-web-support", "destination": "/platform-integration/web/building#add-web-support-to-an-existing-app", "type": 301 }, - { "source": "/to/android-ffi", "destination": "/platform-integration/android/c-interop", "type": 301 }, + { "source": "/to/android-ffi", "destination": "/platform-integration/legacy-ffi-plugin", "type": 301 }, { "source": "/to/android-setup", "destination": "/platform-integration/android/setup", "type": 301 }, { "source": "/to/android-supported-architectures", "destination": "/deployment/android#what-are-the-supported-target-architectures", "type": 301 }, { "source": "/to/asset-from-package", "destination": "/ui/assets/assets-and-images#from-packages", "type": 301 }, @@ -860,7 +866,7 @@ { "source": "/to/ios-create-flutter-engine", "destination": "/add-to-app/ios/add-flutter-screen#create-a-flutterengine", "type": 301 }, { "source": "/to/ios-deploy", "destination": "/deployment/ios", "type": 301 }, { "source": "/to/ios-development-team", "destination": "/deployment/ios#review-xcode-project-settings", "type": 301 }, - { "source": "/to/ios-ffi", "destination": "/platform-integration/ios/c-interop", "type": 301 }, + { "source": "/to/ios-ffi", "destination": "/platform-integration/legacy-ffi-plugin", "type": 301 }, { "source": "/to/ios-minimum-target", "destination": "/deployment/ios#review-xcode-project-settings", "type": 301 }, { "source": "/to/to/java-gradle-incompatibility", "destination": "/release/breaking-changes/android-java-gradle-migration-guide", "type": 301 }, { "source": "/to/java-gradle-incompatibility", "destination": "/release/breaking-changes/android-java-gradle-migration-guide", "type": 301 }, @@ -868,7 +874,7 @@ { "source": "/to/macos-android-setup", "destination": "/platform-integration/android/setup", "type": 301 }, { "source": "/to/macos-deploy", "destination": "/deployment/macos", "type": 301 }, { "source": "/to/macos-entitlements", "destination": "/platform-integration/macos/building#entitlements-and-the-app-sandbox", "type": 301 }, - { "source": "/to/macos-ffi", "destination": "/platform-integration/macos/c-interop", "type": 301 }, + { "source": "/to/macos-ffi", "destination": "/platform-integration/legacy-ffi-plugin", "type": 301 }, { "source": "/to/macos-ios-setup", "destination": "/platform-integration/ios/setup", "type": 301 }, { "source": "/to/macos-minimum-target", "destination": "/deployment/ios#review-xcode-project-settings", "type": 301 }, { "source": "/to/minimize-layout-passes", "destination": "/perf/best-practices#minimize-layout-passes-caused-by-intrinsic-operations", "type": 301 }, diff --git a/packages/site_shared/README.md b/packages/site_shared/README.md new file mode 100644 index 0000000000..db75ce7d77 --- /dev/null +++ b/packages/site_shared/README.md @@ -0,0 +1,48 @@ +# site_shared + +This package is the core library containing +shared logic, UI components, and the design system for +the Dart and Flutter documentation sites. + +It provides a centralized location for APIs, +user interface elements, and logic intended for use by +both the `dart.dev` and `docs.flutter.dev` websites. +Using a shared package ensures a consistent design language and +feature set across Dart and Flutter web documentation platforms. + +## What's included + +The `site_shared` package provides several key capabilities to +build documentation websites using Dart, Jaspr, and Jaspr Content: + +- **UI components** (`lib/components`): + Reusable, modular components built for use across documentation pages. + - **Common components** (`lib/components/common`): + Everyday UI elements such as breadcrumbs, buttons, code blocks, and more. + - **Layout components** (`lib/components/layout`): + Structural layout elements like theme switchers, + site switchers, banners, and menu toggles. + - **Interactive components**: + Integrations such as Dartpad (`lib/components/dartpad`), + tutorials, and user client-side feedback tools. +- **Markdown extensions and processors** (`lib/extensions`): + Custom processors that hook into the Dart Markdown parser to + extend its default syntax and behavior, such as `attribute_processor.dart`. +- **Core styles** (`lib/_sass`): + The shared base styles and component-specific SCSS styling. + These resources define the unified visual identity used by both websites. +- **Utilities and builders** (`lib/src`): + Reusable logic for code syntax highlighting (`lib/src/highlight`), + analytics integrations (`lib/src/analytics`), + builders (`lib/src/builders`), and various helper utilities. + +## Goals + +The primary aims of this shared package are to: + +1. Streamline styling and standardize UI component implementation + across our various websites. +1. Prevent code duplication between the + `dart-lang/site-www` and `flutter/website` repositories. +1. Establish a robust codebase that can be + updated, maintained, and improved in a unified way. diff --git a/packages/site_shared/analysis_options.yaml b/packages/site_shared/analysis_options.yaml new file mode 100644 index 0000000000..95c3595413 --- /dev/null +++ b/packages/site_shared/analysis_options.yaml @@ -0,0 +1,4 @@ +include: package:analysis_defaults/analysis.yaml + +formatter: + trailing_commas: preserve diff --git a/packages/site_shared/build.yaml b/packages/site_shared/build.yaml new file mode 100644 index 0000000000..68cf78f9cd --- /dev/null +++ b/packages/site_shared/build.yaml @@ -0,0 +1,16 @@ +builders: + stylesHashBuilder: + import: "package:site_shared/src/builders/styles_hash_builder.dart" + builder_factories: ["stylesHashBuilder"] + build_extensions: + "web/assets/css/main.css": + - "lib/src/style_hash.dart" + "web/main.css": + - "lib/src/style_hash.dart" + auto_apply: dependents + build_to: source + required_inputs: + - ".css" + defaults: + dev_options: + fixed_hash: true diff --git a/sites/docs/lib/_sass/base/_mixins.scss b/packages/site_shared/lib/_sass/base/_mixins.scss similarity index 100% rename from sites/docs/lib/_sass/base/_mixins.scss rename to packages/site_shared/lib/_sass/base/_mixins.scss diff --git a/sites/docs/lib/_sass/base/_reset.scss b/packages/site_shared/lib/_sass/base/_reset.scss similarity index 100% rename from sites/docs/lib/_sass/base/_reset.scss rename to packages/site_shared/lib/_sass/base/_reset.scss diff --git a/sites/docs/lib/_sass/components/_alert.scss b/packages/site_shared/lib/_sass/components/_alert.scss similarity index 100% rename from sites/docs/lib/_sass/components/_alert.scss rename to packages/site_shared/lib/_sass/components/_alert.scss diff --git a/sites/docs/lib/_sass/components/_banner.scss b/packages/site_shared/lib/_sass/components/_banner.scss similarity index 100% rename from sites/docs/lib/_sass/components/_banner.scss rename to packages/site_shared/lib/_sass/components/_banner.scss diff --git a/sites/docs/lib/_sass/components/_breadcrumbs.scss b/packages/site_shared/lib/_sass/components/_breadcrumbs.scss similarity index 100% rename from sites/docs/lib/_sass/components/_breadcrumbs.scss rename to packages/site_shared/lib/_sass/components/_breadcrumbs.scss diff --git a/sites/docs/lib/_sass/components/_button.scss b/packages/site_shared/lib/_sass/components/_button.scss similarity index 98% rename from sites/docs/lib/_sass/components/_button.scss rename to packages/site_shared/lib/_sass/components/_button.scss index e4df937961..79b5b15e8f 100644 --- a/sites/docs/lib/_sass/components/_button.scss +++ b/packages/site_shared/lib/_sass/components/_button.scss @@ -25,7 +25,8 @@ button { cursor: pointer; &.filled-button, - &.text-button, &.outlined-button { + &.text-button, + &.outlined-button { display: flex; align-items: center; width: fit-content; diff --git a/sites/docs/lib/_sass/components/_card.scss b/packages/site_shared/lib/_sass/components/_card.scss similarity index 99% rename from sites/docs/lib/_sass/components/_card.scss rename to packages/site_shared/lib/_sass/components/_card.scss index 7c9a31b48a..56a5ec68cc 100644 --- a/sites/docs/lib/_sass/components/_card.scss +++ b/packages/site_shared/lib/_sass/components/_card.scss @@ -173,7 +173,7 @@ } } - &.install-card { + &.install-card { gap: 0.25rem; .card-leading { diff --git a/sites/docs/lib/_sass/components/_code.scss b/packages/site_shared/lib/_sass/components/_code.scss similarity index 100% rename from sites/docs/lib/_sass/components/_code.scss rename to packages/site_shared/lib/_sass/components/_code.scss diff --git a/sites/docs/lib/_sass/components/_cookie-notice.scss b/packages/site_shared/lib/_sass/components/_cookie-notice.scss similarity index 100% rename from sites/docs/lib/_sass/components/_cookie-notice.scss rename to packages/site_shared/lib/_sass/components/_cookie-notice.scss diff --git a/sites/docs/lib/_sass/components/_dropdown.scss b/packages/site_shared/lib/_sass/components/_dropdown.scss similarity index 100% rename from sites/docs/lib/_sass/components/_dropdown.scss rename to packages/site_shared/lib/_sass/components/_dropdown.scss diff --git a/packages/site_shared/lib/_sass/components/_menu-toggle.scss b/packages/site_shared/lib/_sass/components/_menu-toggle.scss new file mode 100644 index 0000000000..ea0d9618f7 --- /dev/null +++ b/packages/site_shared/lib/_sass/components/_menu-toggle.scss @@ -0,0 +1,26 @@ +// Toggle between menu and close buttons if sidenav is open or not. +body:not(.sidenav-closed) #menu-toggle { + @media (min-width: 1024px) { + display: none; + } +} + +#menu-toggle span.material-symbols { + &:first-child { + display: inline; + } + + &:last-child { + display: none; + } +} + +body.open_menu #menu-toggle span.material-symbols { + &:first-child { + display: none; + } + + &:last-child { + display: inline; + } +} diff --git a/packages/site_shared/lib/_sass/components/_progress-ring.scss b/packages/site_shared/lib/_sass/components/_progress-ring.scss new file mode 100644 index 0000000000..44970297a9 --- /dev/null +++ b/packages/site_shared/lib/_sass/components/_progress-ring.scss @@ -0,0 +1,14 @@ +.progress-ring { + circle { + fill: none; + stroke-linecap: round; + } + + .ring-inactive { + stroke: var(--site-inset-borderColor); + } + + .ring-active { + stroke: var(--site-primary-color); + } +} diff --git a/sites/docs/lib/_sass/components/_quiz.scss b/packages/site_shared/lib/_sass/components/_quiz.scss similarity index 100% rename from sites/docs/lib/_sass/components/_quiz.scss rename to packages/site_shared/lib/_sass/components/_quiz.scss diff --git a/sites/docs/lib/_sass/components/_site-switcher.scss b/packages/site_shared/lib/_sass/components/_site-switcher.scss similarity index 94% rename from sites/docs/lib/_sass/components/_site-switcher.scss rename to packages/site_shared/lib/_sass/components/_site-switcher.scss index 58b3fb0e33..ac4f4cc9f0 100644 --- a/sites/docs/lib/_sass/components/_site-switcher.scss +++ b/packages/site_shared/lib/_sass/components/_site-switcher.scss @@ -1,5 +1,3 @@ -@use '../base/mixins'; - #site-switcher { position: relative; @@ -33,7 +31,7 @@ font-size: 1.75rem; line-height: 1.25em; letter-spacing: 0.015em; - font-family: 'Google Sans', sans-serif; + font-family: var(--site-ui-fontFamily); user-select: none; >img { diff --git a/sites/docs/lib/_sass/components/_stepper.scss b/packages/site_shared/lib/_sass/components/_stepper.scss similarity index 100% rename from sites/docs/lib/_sass/components/_stepper.scss rename to packages/site_shared/lib/_sass/components/_stepper.scss diff --git a/sites/docs/lib/_sass/components/_summary-card.scss b/packages/site_shared/lib/_sass/components/_summary-card.scss similarity index 98% rename from sites/docs/lib/_sass/components/_summary-card.scss rename to packages/site_shared/lib/_sass/components/_summary-card.scss index a49bf5b13d..17773ccc6b 100644 --- a/sites/docs/lib/_sass/components/_summary-card.scss +++ b/packages/site_shared/lib/_sass/components/_summary-card.scss @@ -17,7 +17,7 @@ h3 { margin: 0; font-size: 1.25rem; - font-weight: 700; + font-weight: 600; color: var(--site-base-fgColor); } diff --git a/sites/docs/lib/_sass/components/_tabs.scss b/packages/site_shared/lib/_sass/components/_tabs.scss similarity index 98% rename from sites/docs/lib/_sass/components/_tabs.scss rename to packages/site_shared/lib/_sass/components/_tabs.scss index 17381350e7..5b2efae51b 100644 --- a/sites/docs/lib/_sass/components/_tabs.scss +++ b/packages/site_shared/lib/_sass/components/_tabs.scss @@ -24,6 +24,7 @@ ul.nav-tabs { gap: 0.5rem; overflow-x: auto; scrollbar-width: thin; + margin-block-end: 1rem; li { margin: 0; diff --git a/sites/docs/lib/_sass/components/_theming.scss b/packages/site_shared/lib/_sass/components/_theming.scss similarity index 100% rename from sites/docs/lib/_sass/components/_theming.scss rename to packages/site_shared/lib/_sass/components/_theming.scss diff --git a/sites/docs/lib/_sass/components/_tooltip.scss b/packages/site_shared/lib/_sass/components/_tooltip.scss similarity index 96% rename from sites/docs/lib/_sass/components/_tooltip.scss rename to packages/site_shared/lib/_sass/components/_tooltip.scss index 5584613fb1..b17b985a5d 100644 --- a/sites/docs/lib/_sass/components/_tooltip.scss +++ b/packages/site_shared/lib/_sass/components/_tooltip.scss @@ -44,7 +44,7 @@ } // On non-touch devices, show tooltip on hover or focus. - @media all and not (pointer: coarse) { + @media all and (not (pointer: coarse)) { &:hover .tooltip { visibility: visible; } @@ -60,4 +60,4 @@ visibility: visible; } } -} \ No newline at end of file +} diff --git a/packages/site_shared/lib/analytics.dart b/packages/site_shared/lib/analytics.dart new file mode 100644 index 0000000000..c697464b75 --- /dev/null +++ b/packages/site_shared/lib/analytics.dart @@ -0,0 +1,5 @@ +// Copyright 2026 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +export 'src/analytics/analytics.dart'; diff --git a/sites/docs/lib/src/components/common/breadcrumbs.dart b/packages/site_shared/lib/components/common/breadcrumbs.dart similarity index 90% rename from sites/docs/lib/src/components/common/breadcrumbs.dart rename to packages/site_shared/lib/components/common/breadcrumbs.dart index 66d9bb4894..9a0ef2790b 100644 --- a/sites/docs/lib/src/components/common/breadcrumbs.dart +++ b/packages/site_shared/lib/components/common/breadcrumbs.dart @@ -18,11 +18,14 @@ import 'material_icon.dart'; /// - https://schema.org/BreadcrumbList /// - https://www.w3.org/TR/wai-aria-practices/examples/breadcrumb/index.html class PageBreadcrumbs extends StatelessComponent { - const PageBreadcrumbs({super.key}); + const PageBreadcrumbs({this.crumbs, super.key}); + + final List? crumbs; @override Component build(BuildContext context) { - final crumbs = _breadcrumbsForPage(context.pages, context.page); + final crumbs = + this.crumbs ?? _breadcrumbsForPage(context.pages, context.page); if (crumbs == null || crumbs.isEmpty) { return const Component.empty(); } @@ -54,7 +57,7 @@ class PageBreadcrumbs extends StatelessComponent { /// /// Uses page metadata to generate breadcrumb titles with fallbacks: /// `breadcrumb` > `shortTitle` > `title`. - List<_BreadcrumbItem>? _breadcrumbsForPage(List pages, Page page) { + List? _breadcrumbsForPage(List pages, Page page) { final pageUrl = page.url; // Only show breadcrumbs if the URL isn't empty. @@ -71,7 +74,7 @@ class PageBreadcrumbs extends StatelessComponent { .toList(growable: false); if (segments.isEmpty) return null; - final breadcrumbs = <_BreadcrumbItem>[]; + final breadcrumbs = []; var currentPath = ''; // Build breadcrumbs for each segment except the current page. @@ -88,7 +91,7 @@ class PageBreadcrumbs extends StatelessComponent { if (indexPage.breadcrumb case final indexBreadcrumb?) { breadcrumbs.add( - _BreadcrumbItem( + BreadcrumbItem( title: indexBreadcrumb, url: indexPage.url, ), @@ -104,7 +107,7 @@ class PageBreadcrumbs extends StatelessComponent { // Add the current page as the final breadcrumb. breadcrumbs.add( - _BreadcrumbItem( + BreadcrumbItem( title: pageBreadcrumb, url: pageUrl, ), @@ -127,8 +130,8 @@ extension on Page { } } -final class _BreadcrumbItem { - const _BreadcrumbItem({required this.title, required this.url}); +final class BreadcrumbItem { + const BreadcrumbItem({required this.title, required this.url}); final String title; final String url; @@ -142,7 +145,7 @@ final class _BreadcrumbItemComponent extends StatelessComponent { required this.isLast, }); - final _BreadcrumbItem crumb; + final BreadcrumbItem crumb; final int index; final bool isLast; diff --git a/sites/docs/lib/src/components/common/button.dart b/packages/site_shared/lib/components/common/button.dart similarity index 86% rename from sites/docs/lib/src/components/common/button.dart rename to packages/site_shared/lib/components/common/button.dart index 740af24cf3..85feaebbd5 100644 --- a/sites/docs/lib/src/components/common/button.dart +++ b/packages/site_shared/lib/components/common/button.dart @@ -14,6 +14,7 @@ class Button extends StatelessComponent { const Button({ super.key, this.icon, + this.trailingIcon, this.href, this.content, this.style = ButtonStyle.text, @@ -30,6 +31,7 @@ class Button extends StatelessComponent { final String? title; final ButtonStyle style; final String? icon; + final String? trailingIcon; final String? id; final String? href; final Map attributes; @@ -48,7 +50,8 @@ class Button extends StatelessComponent { final mergedClasses = [ style.cssClass, - if (icon != null && content == null) 'icon-button', + if ((icon != null || trailingIcon != null) && content == null) + 'icon-button', ...?classes, ].toClasses; @@ -56,6 +59,7 @@ class Button extends StatelessComponent { if (icon case final iconId?) MaterialIcon(iconId), if (content case final contentText?) asRaw ? RawText(contentText) : .text(contentText), + if (trailingIcon case final iconId?) MaterialIcon(iconId), ]; if (href case final href?) { @@ -90,17 +94,3 @@ enum ButtonStyle { ButtonStyle.text => 'text-button', }; } - -class SegmentedButton extends StatelessComponent { - const SegmentedButton({ - super.key, - required this.children, - }); - - final List children; - - @override - Component build(BuildContext context) { - return span(classes: ['segmented-button'].toClasses, children); - } -} diff --git a/sites/docs/lib/src/components/common/card.dart b/packages/site_shared/lib/components/common/card.dart similarity index 100% rename from sites/docs/lib/src/components/common/card.dart rename to packages/site_shared/lib/components/common/card.dart diff --git a/sites/docs/lib/src/components/common/chip.dart b/packages/site_shared/lib/components/common/chip.dart similarity index 99% rename from sites/docs/lib/src/components/common/chip.dart rename to packages/site_shared/lib/components/common/chip.dart index a67be256db..455bc61727 100644 --- a/sites/docs/lib/src/components/common/chip.dart +++ b/packages/site_shared/lib/components/common/chip.dart @@ -7,7 +7,7 @@ import 'package:jaspr/jaspr.dart'; import 'package:universal_web/web.dart' as web; import '../../util.dart'; -import '../util/global_event_listener.dart'; +import '../utils/global_event_listener.dart'; import 'material_icon.dart'; /// A set of Material Design-like chips for configuration. diff --git a/sites/docs/lib/src/components/common/client/collapse_button.dart b/packages/site_shared/lib/components/common/client/collapse_button.dart similarity index 99% rename from sites/docs/lib/src/components/common/client/collapse_button.dart rename to packages/site_shared/lib/components/common/client/collapse_button.dart index a9c5c85142..e401f1dadd 100644 --- a/sites/docs/lib/src/components/common/client/collapse_button.dart +++ b/packages/site_shared/lib/components/common/client/collapse_button.dart @@ -3,7 +3,6 @@ // found in the LICENSE file. import 'package:jaspr/jaspr.dart'; - import 'package:universal_web/web.dart' as web; import '../button.dart'; diff --git a/sites/docs/lib/src/components/common/client/cookie_notice.dart b/packages/site_shared/lib/components/common/client/cookie_notice.dart similarity index 88% rename from sites/docs/lib/src/components/common/client/cookie_notice.dart rename to packages/site_shared/lib/components/common/client/cookie_notice.dart index b4362a5aca..a01abe8513 100644 --- a/sites/docs/lib/src/components/common/client/cookie_notice.dart +++ b/packages/site_shared/lib/components/common/client/cookie_notice.dart @@ -12,7 +12,14 @@ import '../button.dart'; /// The cookie banner to show on a user's first time visiting the site. @client final class CookieNotice extends StatefulComponent { - const CookieNotice({super.key}); + const CookieNotice({ + super.key, + required this.host, + this.alwaysDarkMode = false, + }); + + final String host; + final bool alwaysDarkMode; @override State createState() => _CookieNoticeState(); @@ -60,13 +67,16 @@ final class _CookieNoticeState extends State { Component build(BuildContext context) { return section( id: 'cookie-notice', - classes: [if (showNotice) 'show'].toClasses, + classes: [ + if (showNotice) 'show', + if (component.alwaysDarkMode) 'always-dark-mode', + ].toClasses, attributes: {'data-nosnippet': 'true'}, [ div(classes: 'container', [ - const p([ + p([ .text( - 'docs.flutter.dev uses cookies from Google to deliver and ' + '${component.host} uses cookies from Google to deliver and ' 'enhance the quality of its services and to analyze traffic.', ), ]), diff --git a/sites/docs/lib/src/components/common/client/copy_button.dart b/packages/site_shared/lib/components/common/client/copy_button.dart similarity index 78% rename from sites/docs/lib/src/components/common/client/copy_button.dart rename to packages/site_shared/lib/components/common/client/copy_button.dart index fffc9dc0ab..39f1a0f835 100644 --- a/sites/docs/lib/src/components/common/client/copy_button.dart +++ b/packages/site_shared/lib/components/common/client/copy_button.dart @@ -11,11 +11,13 @@ import '../button.dart'; class CopyButton extends StatefulComponent { const CopyButton({ this.buttonText, + this.toCopy, this.classes = const [], this.title, }); final String? title; + final String? toCopy; final String? buttonText; final List classes; @@ -32,9 +34,11 @@ class _CopyButtonState extends State { @override void initState() { if (kIsWeb) { - // Extract the code content and unhide the copy button on the client. - context.binding.addPostFrameCallback(() { - setState(() { + if (component.toCopy case final contentToCopy?) { + content = contentToCopy; + } else { + // Extract the code content and unhide the copy button on the client. + context.binding.addPostFrameCallback(() { final codeElement = buttonKey.currentNode ?.closest('.code-block-wrapper') ?.querySelector('pre code') @@ -46,6 +50,7 @@ class _CopyButtonState extends State { codeElement, /* NodeFilter.SHOW_ELEMENT */ 1, ); + web.Node? currentNode; while ((currentNode = iterator.nextNode()) != null) { final element = currentNode as web.Element; @@ -54,16 +59,20 @@ class _CopyButtonState extends State { } } - // Remove zero-width spaces - content = codeElement.textContent?.replaceAll('\u200B', ''); - }); + // Remove zero-width spaces. + final extracted = codeElement.textContent?.replaceAll('\u200B', ''); + + assert( + extracted != null, + 'CopyButton: Unable to find code content to copy. ' + 'Is the CopyButton inside a code block?', + ); - assert( - content != null, - 'CopyButton: Unable to find code content to copy. ' - 'Is the CopyButton inside a code block?', - ); - }); + setState(() { + content = extracted; + }); + }); + } } super.initState(); diff --git a/sites/docs/lib/src/components/common/client/download_button.dart b/packages/site_shared/lib/components/common/client/download_button.dart similarity index 100% rename from sites/docs/lib/src/components/common/client/download_button.dart rename to packages/site_shared/lib/components/common/client/download_button.dart diff --git a/sites/docs/lib/src/components/common/client/feedback.dart b/packages/site_shared/lib/components/common/client/feedback.dart similarity index 98% rename from sites/docs/lib/src/components/common/client/feedback.dart rename to packages/site_shared/lib/components/common/client/feedback.dart index 71219a0038..21930d06c6 100644 --- a/sites/docs/lib/src/components/common/client/feedback.dart +++ b/packages/site_shared/lib/components/common/client/feedback.dart @@ -5,7 +5,7 @@ import 'package:jaspr/dom.dart'; import 'package:jaspr/jaspr.dart'; -import '../../../analytics/analytics.dart'; +import '../../../analytics.dart'; import '../button.dart'; /// Provides the user options to provide feedback on the specified page. diff --git a/sites/docs/lib/src/components/common/client/on_this_page_button.dart b/packages/site_shared/lib/components/common/client/on_this_page_button.dart similarity index 100% rename from sites/docs/lib/src/components/common/client/on_this_page_button.dart rename to packages/site_shared/lib/components/common/client/on_this_page_button.dart diff --git a/sites/docs/lib/src/components/common/client/page_header_options.dart b/packages/site_shared/lib/components/common/client/page_header_options.dart similarity index 100% rename from sites/docs/lib/src/components/common/client/page_header_options.dart rename to packages/site_shared/lib/components/common/client/page_header_options.dart diff --git a/sites/docs/lib/src/components/common/client/simple_tooltip.dart b/packages/site_shared/lib/components/common/client/simple_tooltip.dart similarity index 83% rename from sites/docs/lib/src/components/common/client/simple_tooltip.dart rename to packages/site_shared/lib/components/common/client/simple_tooltip.dart index 869527f5de..12e9f1fba8 100644 --- a/sites/docs/lib/src/components/common/client/simple_tooltip.dart +++ b/packages/site_shared/lib/components/common/client/simple_tooltip.dart @@ -4,7 +4,7 @@ import 'package:jaspr/jaspr.dart'; -import '../../util/component_ref.dart'; +import '../../utils/component_ref.dart'; import '../tooltip.dart'; @client @@ -21,8 +21,8 @@ class SimpleTooltip extends StatelessComponent { @override Component build(BuildContext context) { return Tooltip( - target: target.component, - content: content.component, + target: target, + content: content, ); } } diff --git a/sites/docs/lib/src/components/common/dropdown.dart b/packages/site_shared/lib/components/common/dropdown.dart similarity index 98% rename from sites/docs/lib/src/components/common/dropdown.dart rename to packages/site_shared/lib/components/common/dropdown.dart index bf17720459..e6e62ca5f0 100644 --- a/sites/docs/lib/src/components/common/dropdown.dart +++ b/packages/site_shared/lib/components/common/dropdown.dart @@ -4,10 +4,9 @@ import 'package:jaspr/dom.dart'; import 'package:jaspr/jaspr.dart'; - import 'package:universal_web/web.dart' as web; -import '../util/global_event_listener.dart'; +import '../utils/global_event_listener.dart'; /// A dropdown with a toggle button and expandable content. final class Dropdown extends StatefulComponent { diff --git a/sites/docs/lib/src/components/common/fragment_target.dart b/packages/site_shared/lib/components/common/fragment_target.dart similarity index 100% rename from sites/docs/lib/src/components/common/fragment_target.dart rename to packages/site_shared/lib/components/common/fragment_target.dart diff --git a/sites/docs/lib/src/components/common/material_icon.dart b/packages/site_shared/lib/components/common/material_icon.dart similarity index 100% rename from sites/docs/lib/src/components/common/material_icon.dart rename to packages/site_shared/lib/components/common/material_icon.dart diff --git a/sites/docs/lib/src/components/common/search.dart b/packages/site_shared/lib/components/common/search.dart similarity index 100% rename from sites/docs/lib/src/components/common/search.dart rename to packages/site_shared/lib/components/common/search.dart diff --git a/sites/docs/lib/src/components/common/tabs.dart b/packages/site_shared/lib/components/common/tabs.dart similarity index 100% rename from sites/docs/lib/src/components/common/tabs.dart rename to packages/site_shared/lib/components/common/tabs.dart diff --git a/sites/docs/lib/src/components/common/tags.dart b/packages/site_shared/lib/components/common/tags.dart similarity index 100% rename from sites/docs/lib/src/components/common/tags.dart rename to packages/site_shared/lib/components/common/tags.dart diff --git a/sites/docs/lib/src/components/common/tooltip.dart b/packages/site_shared/lib/components/common/tooltip.dart similarity index 98% rename from sites/docs/lib/src/components/common/tooltip.dart rename to packages/site_shared/lib/components/common/tooltip.dart index 932f174ff2..3cdb63782b 100644 --- a/sites/docs/lib/src/components/common/tooltip.dart +++ b/packages/site_shared/lib/components/common/tooltip.dart @@ -7,7 +7,7 @@ import 'package:jaspr/jaspr.dart'; import 'package:universal_web/web.dart' as web; import '../../util.dart'; -import '../util/global_event_listener.dart'; +import '../utils/global_event_listener.dart'; class Tooltip extends StatefulComponent { const Tooltip({ diff --git a/sites/docs/lib/src/components/common/wrapped_code_block.dart b/packages/site_shared/lib/components/common/wrapped_code_block.dart similarity index 100% rename from sites/docs/lib/src/components/common/wrapped_code_block.dart rename to packages/site_shared/lib/components/common/wrapped_code_block.dart diff --git a/sites/docs/lib/src/components/common/youtube_embed.dart b/packages/site_shared/lib/components/common/youtube_embed.dart similarity index 100% rename from sites/docs/lib/src/components/common/youtube_embed.dart rename to packages/site_shared/lib/components/common/youtube_embed.dart diff --git a/sites/docs/lib/src/components/dartpad/dartpad_injector.dart b/packages/site_shared/lib/components/dartpad/dartpad_injector.dart similarity index 90% rename from sites/docs/lib/src/components/dartpad/dartpad_injector.dart rename to packages/site_shared/lib/components/dartpad/dartpad_injector.dart index 95c0981d57..69bb328a4f 100644 --- a/sites/docs/lib/src/components/dartpad/dartpad_injector.dart +++ b/packages/site_shared/lib/components/dartpad/dartpad_injector.dart @@ -4,7 +4,8 @@ import 'package:jaspr/dom.dart'; import 'package:jaspr/jaspr.dart'; -import '../util/retake_element.dart'; + +import '../../src/utils/retake_element.dart'; import 'embedded_dartpad.dart'; /// Prepares a code block that will be replaced with an embedded @@ -79,16 +80,7 @@ class _DartPadInjectorState extends State { if (kIsWeb) { // During hydration, extract the content from the pre-rendered code block. - final elem = retakeElement(context, (elem) { - return elem.tagName.toLowerCase() == 'pre'; - }); - - if (elem == null) { - content = ''; - } else { - elem.parentNode?.removeChild(elem); - content = elem.textContent ?? ''; - } + content = extractContent(context as Element); } } diff --git a/sites/docs/lib/src/components/dartpad/embedded_dartpad.dart b/packages/site_shared/lib/components/dartpad/embedded_dartpad.dart similarity index 100% rename from sites/docs/lib/src/components/dartpad/embedded_dartpad.dart rename to packages/site_shared/lib/components/dartpad/embedded_dartpad.dart diff --git a/packages/site_shared/lib/components/layout/banner.dart b/packages/site_shared/lib/components/layout/banner.dart new file mode 100644 index 0000000000..671ad7dc6b --- /dev/null +++ b/packages/site_shared/lib/components/layout/banner.dart @@ -0,0 +1,127 @@ +// Copyright 2025 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:jaspr/dom.dart'; +import 'package:jaspr/jaspr.dart'; + +/// The information to display in the site banner, +/// as configured in `src/data/banner.yml`. +@immutable +final class BannerContent { + /// The ordered content parts to render in the banner. + final List parts; + + /// Creates banner content from the specified [parts]. + const BannerContent({required this.parts}); + + /// Creates banner content from the parsed [bannerData]. + /// + /// The [bannerData] list is expected to contain + /// `text`, `link`, or `newLine` entries from `src/data/banner.yml`. + /// + /// Throws if any entry has an unsupported structure. + factory BannerContent.fromList(List bannerData) => BannerContent( + parts: [ + for (final item in bannerData) + switch (item) { + {'text': final String text} => .text(text), + {'link': final Map link} => .link( + text: link['text'] as String, + url: link['url'] as String, + newTab: link['newTab'] as bool? ?? false, + ), + {'newLine': _} => const .newLine(), + _ => throw FormatException('Invalid banner item: $item'), + }, + ], + ); +} + +/// A single renderable piece of banner content. +@immutable +sealed class BannerPart { + /// Creates a banner content part. + const BannerPart(); + + /// Creates a text part with the specified [text]. + const factory BannerPart.text(String text) = _BannerText; + + /// Creates a link part with the specified [text] and [url]. + /// + /// Unless [newTab] is `true`, the link opens in the same tab. + const factory BannerPart.link({ + required String text, + required String url, + bool newTab, + }) = _BannerLink; + + /// Creates a new line part that renders a line break. + const factory BannerPart.newLine() = _BannerNewLine; +} + +/// Plain text within a site banner. +final class _BannerText extends BannerPart { + /// Creates a text banner part with the specified [text]. + const _BannerText(this.text); + + /// The text to render in the banner. + final String text; +} + +/// A link within a site banner. +final class _BannerLink extends BannerPart { + /// Creates a link banner part with the specified [text] and [url]. + /// + /// Unless [newTab] is `true`, the link opens in the same tab. + const _BannerLink({ + required this.text, + required this.url, + this.newTab = false, + }); + + /// The link label to render in the banner. + final String text; + + /// The destination URL for this link. + final String url; + + /// Whether this link opens in a new browser tab. + final bool newTab; +} + +/// A line break within a site banner. +final class _BannerNewLine extends BannerPart { + /// Creates a line break banner part. + const _BannerNewLine(); +} + +/// A site-wide banner rendered from structured content. +class DashBanner extends StatelessComponent { + /// Creates a site banner that displays the specified [content]. + const DashBanner(this.content, {super.key}); + + /// The structured content to render in this banner. + final BannerContent content; + + @override + Component build(BuildContext context) => div( + id: 'site-banner', + attributes: {'role': 'alert'}, + [ + p([ + for (final part in content.parts) + switch (part) { + _BannerText(:final text) => .text(text), + _BannerLink(:final text, :final url, :final newTab) => a( + href: url, + target: newTab ? Target.blank : null, + attributes: newTab ? const {'rel': 'noopener'} : null, + [.text(text)], + ), + _BannerNewLine() => const br(), + }, + ]), + ], + ); +} diff --git a/sites/docs/lib/src/components/layout/menu_toggle.dart b/packages/site_shared/lib/components/layout/menu_toggle.dart similarity index 100% rename from sites/docs/lib/src/components/layout/menu_toggle.dart rename to packages/site_shared/lib/components/layout/menu_toggle.dart diff --git a/sites/docs/lib/src/components/layout/site_switcher.dart b/packages/site_shared/lib/components/layout/site_switcher.dart similarity index 72% rename from sites/docs/lib/src/components/layout/site_switcher.dart rename to packages/site_shared/lib/components/layout/site_switcher.dart index c0b7d79589..3ee7ed56df 100644 --- a/sites/docs/lib/src/components/layout/site_switcher.dart +++ b/packages/site_shared/lib/components/layout/site_switcher.dart @@ -11,20 +11,22 @@ import '../common/dropdown.dart'; @client final class SiteSwitcher extends StatelessComponent { - const SiteSwitcher(); + const SiteSwitcher({this.isFlutter = true, super.key}); + + final bool isFlutter; @override Component build(BuildContext _) { - return const Dropdown( + return Dropdown( id: 'site-switcher', - toggle: Button(icon: 'apps', title: 'Visit related sites.'), + toggle: const Button(icon: 'apps', title: 'Visit related sites.'), content: nav( classes: 'dropdown-menu', attributes: {'role': 'menu'}, [ - ul( - [ - _SiteWordMarkListEntry( + ul([ + if (isFlutter) ...[ + const _SiteWordMarkListEntry( name: 'Flutter', href: 'https://flutter.cn', ), @@ -32,40 +34,48 @@ final class SiteSwitcher extends StatelessComponent { name: 'Flutter', subtype: 'Docs', href: '/', - current: true, + current: isFlutter, ), - _SiteWordMarkListEntry( + const _SiteWordMarkListEntry( name: 'Flutter', subtype: 'API', href: 'https://api.flutter-io.cn', ), - _SiteWordMarkListEntry( + const _SiteWordMarkListEntry( name: 'Flutter', subtype: 'Blog', href: 'https://blog.flutter.dev', ), - Component.element( + const Component.element( tag: 'li', classes: 'dropdown-divider', attributes: {'aria-hidden': 'true', 'role': 'separator'}, ), - _SiteWordMarkListEntry( + ], + _SiteWordMarkListEntry( + name: 'Dart', + href: 'https://dart.cn', + dart: true, + current: !isFlutter, + ), + if (!isFlutter) + const _SiteWordMarkListEntry( name: 'Dart', - href: 'https://dart.cn', - dart: true, - ), - _SiteWordMarkListEntry( - name: 'DartPad', - href: 'https://dartpad.cn', - dart: true, - ), - _SiteWordMarkListEntry( - name: 'pub.dev', - href: 'https://pub-web.flutter-io.cn', + subtype: 'API', + href: 'https://api.dart.cn', dart: true, ), - ], - ), + const _SiteWordMarkListEntry( + name: 'DartPad', + href: 'https://dartpad.cn', + dart: true, + ), + const _SiteWordMarkListEntry( + name: 'pub.dev', + href: 'https://pub-web.flutter-io.cn', + dart: true, + ), + ]), ], ), ); diff --git a/sites/docs/lib/src/components/layout/theme_switcher.dart b/packages/site_shared/lib/components/layout/theme_switcher.dart similarity index 100% rename from sites/docs/lib/src/components/layout/theme_switcher.dart rename to packages/site_shared/lib/components/layout/theme_switcher.dart diff --git a/sites/docs/lib/src/components/tutorial/client/progress_ring.dart b/packages/site_shared/lib/components/tutorial/client/progress_ring.dart similarity index 100% rename from sites/docs/lib/src/components/tutorial/client/progress_ring.dart rename to packages/site_shared/lib/components/tutorial/client/progress_ring.dart diff --git a/sites/docs/lib/src/components/tutorial/client/quiz.dart b/packages/site_shared/lib/components/tutorial/client/quiz.dart similarity index 96% rename from sites/docs/lib/src/components/tutorial/client/quiz.dart rename to packages/site_shared/lib/components/tutorial/client/quiz.dart index a50f40b53c..e107005f43 100644 --- a/sites/docs/lib/src/components/tutorial/client/quiz.dart +++ b/packages/site_shared/lib/components/tutorial/client/quiz.dart @@ -6,9 +6,9 @@ import 'package:jaspr/dom.dart'; import 'package:jaspr/jaspr.dart'; import 'package:universal_web/web.dart' as web; -import '../../../models/quiz_model.dart'; import '../../../util.dart'; import '../../common/button.dart'; +import '../models/quiz_model.dart'; @client class InteractiveQuiz extends StatefulComponent { @@ -89,7 +89,7 @@ class _InteractiveQuizState extends State { if (question == currentQuestion) 'active', ].toClasses, [ - strong([.text(question.question)]), + strong([RawText(question.question)]), ol([ for (final (index, option) in question.options.indexed) li( @@ -113,14 +113,14 @@ class _InteractiveQuizState extends State { [ div(classes: 'question-wrapper', [ div(classes: 'question', [ - p([.text(option.text)]), + p([RawText(option.text)]), ]), div(classes: 'solution', [ if (option.correct) const p(classes: 'correct', [.text('正确!')]) else - const p(classes: 'incorrect', [.text('不正确')]), - p([.text(option.explanation)]), + const p(classes: 'incorrect', [.text('不正确。')]), + p([RawText(option.explanation)]), ]), ]), ], diff --git a/sites/docs/lib/src/components/tutorial/downloadable_snippet.dart b/packages/site_shared/lib/components/tutorial/downloadable_snippet.dart similarity index 89% rename from sites/docs/lib/src/components/tutorial/downloadable_snippet.dart rename to packages/site_shared/lib/components/tutorial/downloadable_snippet.dart index b11391a25b..ab77e8b583 100644 --- a/sites/docs/lib/src/components/tutorial/downloadable_snippet.dart +++ b/packages/site_shared/lib/components/tutorial/downloadable_snippet.dart @@ -6,14 +6,17 @@ import 'package:jaspr/jaspr.dart'; import 'package:jaspr_content/jaspr_content.dart'; import 'package:path/path.dart' as path; -import '../../extensions/code_block_processor.dart'; -import '../../util.dart'; +import '../../src/extensions/code_block_processor.dart'; import '../common/client/copy_button.dart'; import '../common/client/download_button.dart'; import '../common/wrapped_code_block.dart'; class DownloadableSnippet extends CustomComponentBase { - const DownloadableSnippet(); + const DownloadableSnippet({ + required this.snippetsDirectoryPath, + }); + + final String snippetsDirectoryPath; @override Pattern get pattern => 'DownloadableSnippet'; @@ -33,7 +36,7 @@ class DownloadableSnippet extends CustomComponentBase { builder: (context) { final page = context.page; final snippet = page.loader.readPartialSync( - path.join(siteSrcDirectoryPath, '_snippets', src), + path.join(snippetsDirectoryPath, src), page, ); final language = src.split('.').last; diff --git a/sites/docs/lib/src/models/quiz_model.dart b/packages/site_shared/lib/components/tutorial/models/quiz_model.dart similarity index 100% rename from sites/docs/lib/src/models/quiz_model.dart rename to packages/site_shared/lib/components/tutorial/models/quiz_model.dart diff --git a/sites/docs/lib/src/models/summary_card_model.dart b/packages/site_shared/lib/components/tutorial/models/summary_card_model.dart similarity index 100% rename from sites/docs/lib/src/models/summary_card_model.dart rename to packages/site_shared/lib/components/tutorial/models/summary_card_model.dart diff --git a/sites/docs/lib/src/models/tutorial_model.dart b/packages/site_shared/lib/components/tutorial/models/tutorial_model.dart similarity index 100% rename from sites/docs/lib/src/models/tutorial_model.dart rename to packages/site_shared/lib/components/tutorial/models/tutorial_model.dart diff --git a/sites/docs/lib/src/components/tutorial/progress_ring.dart b/packages/site_shared/lib/components/tutorial/progress_ring.dart similarity index 86% rename from sites/docs/lib/src/components/tutorial/progress_ring.dart rename to packages/site_shared/lib/components/tutorial/progress_ring.dart index c4453b17f8..1658e8b92e 100644 --- a/sites/docs/lib/src/components/tutorial/progress_ring.dart +++ b/packages/site_shared/lib/components/tutorial/progress_ring.dart @@ -19,14 +19,18 @@ class ProgressRing extends CustomComponentBase { Map attributes, Component? child, ) { - final progress = double.tryParse(attributes['progress'] ?? '') ?? 0.0; + final progress = double.tryParse(attributes['progress'] ?? '') ?? 0; assert( - progress >= 0.0 && progress <= 1.0, + progress >= 0 && progress <= 1, 'ProgressRing progress must be between 0.0 and 1.0', ); final small = attributes['small'] != null; final large = attributes['large'] != null; + assert( + !small || !large, + 'ProgressRing can\'t be both small and large', + ); return InteractiveProgressRing( progress: progress, diff --git a/packages/site_shared/lib/components/tutorial/quiz.dart b/packages/site_shared/lib/components/tutorial/quiz.dart new file mode 100644 index 0000000000..f576e34877 --- /dev/null +++ b/packages/site_shared/lib/components/tutorial/quiz.dart @@ -0,0 +1,73 @@ +// Copyright 2025 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:jaspr/jaspr.dart'; +import 'package:jaspr_content/jaspr_content.dart'; +import 'package:yaml/yaml.dart'; + +import '../../src/markdown/markdown_parser.dart'; +import 'client/quiz.dart'; +import 'models/quiz_model.dart'; + +class Quiz extends CustomComponent { + const Quiz() : super.base(); + + @override + Component? create(Node node, NodesBuilder builder) { + if (node is! ElementNode || node.tag.toLowerCase() != 'quiz') { + return null; + } + + final title = node.attributes['title']; + + // If the quiz has an ID, load it from the page data. + if (node.attributes['id'] case final String quizId when quizId.isNotEmpty) { + return Builder( + builder: (context) { + final quizzes = context.page.data['quiz'] as Map?; + if (quizzes?[quizId] case final List quizData) { + return InteractiveQuiz( + title: title, + questions: quizData + .map((q) => _parseQuestion(q as Map)) + .toList(growable: false), + ); + } + + throw ArgumentError('Failed to parse quiz with ID: $quizId'); + }, + ); + } + + // If the quiz does not have an ID, parse it from the content. + if (node.children?.whereType().isNotEmpty ?? false) { + throw Exception( + 'Invalid Quiz content. Remove any leading empty lines to ' + 'avoid parsing as markdown.', + ); + } + + final content = node.children?.map((n) => n.innerText).join('\n') ?? ''; + final data = loadYamlNode(content); + assert(data is YamlList, 'Invalid Quiz content. Expected a YAML list.'); + final questions = (data as YamlList).nodes + .map((n) => _parseQuestion(n as YamlMap)) + .toList(); + assert(questions.isNotEmpty, 'Quiz must contain at least one question.'); + return InteractiveQuiz(title: title, questions: questions); + } +} + +Question _parseQuestion(Map map) => Question( + parseMarkdownToHtml(map['question'] as String, inline: true), + (map['options'] as List) + .map((e) => _parseAnswer(e as Map)) + .toList(), +); + +AnswerOption _parseAnswer(Map map) => AnswerOption( + parseMarkdownToHtml(map['text'] as String, inline: true), + map['correct'] as bool? ?? false, + parseMarkdownToHtml(map['explanation'] as String), +); diff --git a/sites/docs/lib/src/components/tutorial/stepper.dart b/packages/site_shared/lib/components/tutorial/stepper.dart similarity index 100% rename from sites/docs/lib/src/components/tutorial/stepper.dart rename to packages/site_shared/lib/components/tutorial/stepper.dart diff --git a/sites/docs/lib/src/components/tutorial/summary_card.dart b/packages/site_shared/lib/components/tutorial/summary_card.dart similarity index 97% rename from sites/docs/lib/src/components/tutorial/summary_card.dart rename to packages/site_shared/lib/components/tutorial/summary_card.dart index 76b8a2e7dd..298745d318 100644 --- a/sites/docs/lib/src/components/tutorial/summary_card.dart +++ b/packages/site_shared/lib/components/tutorial/summary_card.dart @@ -7,9 +7,9 @@ import 'package:jaspr/jaspr.dart'; import 'package:jaspr_content/jaspr_content.dart'; import 'package:yaml/yaml.dart'; -import '../../markdown/markdown_parser.dart'; -import '../../models/summary_card_model.dart'; +import '../../src/markdown/markdown_parser.dart'; import '../common/material_icon.dart'; +import 'models/summary_card_model.dart'; class SummaryCard extends CustomComponent { const SummaryCard() : super.base(); diff --git a/sites/docs/lib/src/components/tutorial/tutorial_outline.dart b/packages/site_shared/lib/components/tutorial/tutorial_outline.dart similarity index 58% rename from sites/docs/lib/src/components/tutorial/tutorial_outline.dart rename to packages/site_shared/lib/components/tutorial/tutorial_outline.dart index 672680636f..c4297b1a65 100644 --- a/sites/docs/lib/src/components/tutorial/tutorial_outline.dart +++ b/packages/site_shared/lib/components/tutorial/tutorial_outline.dart @@ -6,11 +6,13 @@ import 'package:jaspr/dom.dart'; import 'package:jaspr/jaspr.dart'; import 'package:jaspr_content/jaspr_content.dart'; -import '../../markdown/markdown_parser.dart'; -import '../../models/tutorial_model.dart'; +import '../../src/markdown/markdown_parser.dart'; +import 'models/tutorial_model.dart'; class TutorialOutline extends CustomComponentBase { - const TutorialOutline(); + const TutorialOutline({this.showUnitTitle = true}); + + final bool showUnitTitle; @override Pattern get pattern => 'TutorialOutline'; @@ -30,22 +32,31 @@ class TutorialOutline extends CustomComponentBase { }; return div(classes: 'tutorial-outline', [ - ol([ - for (final unit in model.units) - li([ - .text(unit.title), - ol([ - for (final chapter in unit.chapters) - li([ - a(href: chapter.url, [ - DashMarkdown(content: chapter.title, inline: true), - ]), - ]), - ]), - ]), - ]), + ol([for (final unit in model.units) ..._buildUnit(unit)]), ]); }, ); } + + List _buildUnit(TutorialUnit unit) { + final chapters = [ + for (final chapter in unit.chapters) + li([ + a(href: chapter.url, [ + DashMarkdown(content: chapter.title, inline: true), + ]), + ]), + ]; + + if (showUnitTitle) { + return [ + li([ + .text(unit.title), + ol(chapters), + ]), + ]; + } else { + return chapters; + } + } } diff --git a/packages/site_shared/lib/components/utils/component_ref.dart b/packages/site_shared/lib/components/utils/component_ref.dart new file mode 100644 index 0000000000..722299368b --- /dev/null +++ b/packages/site_shared/lib/components/utils/component_ref.dart @@ -0,0 +1,51 @@ +// Copyright 2025 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:jaspr/dom.dart'; +import 'package:jaspr/jaspr.dart'; +import 'package:nanoid2/nanoid2.dart'; + +import '../../src/utils/retake_element.dart'; + +/// A wrapper around [Component] to make it usable across server/client boundaries. +/// +/// This is a temporary (and limited) solution until server components have +/// landed in Jaspr. They enable passing components to @client components +/// directly, by creating a unique ID on the server and retaking the dom node +/// on the client. +/// +/// Wrap your component with the top-level [ref] function, then pass the +/// resulting [ComponentRef] directly to your `@client` component. +/// Since [ComponentRef] is itself a [Component], +/// the original component is rendered on the server and +/// retaken from the DOM on the client during hydration. +class ComponentRef extends StatelessComponent { + const ComponentRef._(this.id, [this._component = const .empty()]); + + final String id; + final Component _component; + + @override + Component build(BuildContext context) { + if (!kIsWeb) { + return Component.fragment([ + RawText(''), + _component, + RawText(''), + ]); + } + + return retakeRef(context, id); + } + + @decoder + factory ComponentRef.fromId(String id) { + return ComponentRef._(id); + } + + @encoder + String toId() => id; +} + +ComponentRef ref(Component child) => ComponentRef._(nanoid(length: 8), child); diff --git a/packages/site_shared/lib/components/utils/define_component.dart b/packages/site_shared/lib/components/utils/define_component.dart new file mode 100644 index 0000000000..92483614a1 --- /dev/null +++ b/packages/site_shared/lib/components/utils/define_component.dart @@ -0,0 +1,28 @@ +// Copyright 2026 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:jaspr/jaspr.dart'; +import 'package:jaspr_content/jaspr_content.dart'; + +CustomComponent defineComponent(String name, Component child) => + CustomComponent( + pattern: RegExp(name, caseSensitive: false), + builder: (_, _, _) => child, + ); + +CustomComponent defineComponentWithAttrs( + String name, + Component Function(Map attributes) factory, +) => CustomComponent( + pattern: RegExp(name, caseSensitive: false), + builder: (_, attrs, _) => factory(attrs), +); + +CustomComponent defineComponentWithChild( + String name, + Component Function(Map attributes, Component? child) factory, +) => CustomComponent( + pattern: RegExp(name, caseSensitive: false), + builder: (_, attrs, child) => factory(attrs, child), +); diff --git a/sites/docs/lib/src/components/util/global_event_listener.dart b/packages/site_shared/lib/components/utils/global_event_listener.dart similarity index 100% rename from sites/docs/lib/src/components/util/global_event_listener.dart rename to packages/site_shared/lib/components/utils/global_event_listener.dart diff --git a/packages/site_shared/lib/layouts.dart b/packages/site_shared/lib/layouts.dart new file mode 100644 index 0000000000..3e8a0a6595 --- /dev/null +++ b/packages/site_shared/lib/layouts.dart @@ -0,0 +1,8 @@ +// Copyright 2026 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +/// Shared Jaspr Content layouts usable as a base across sites. +library; + +export 'src/layouts/dash_layout.dart'; diff --git a/packages/site_shared/lib/markdown.dart b/packages/site_shared/lib/markdown.dart new file mode 100644 index 0000000000..23917e4faa --- /dev/null +++ b/packages/site_shared/lib/markdown.dart @@ -0,0 +1,5 @@ +// Copyright 2026 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +export 'src/markdown/markdown_parser.dart'; diff --git a/packages/site_shared/lib/page_extensions.dart b/packages/site_shared/lib/page_extensions.dart new file mode 100644 index 0000000000..3b889b65c0 --- /dev/null +++ b/packages/site_shared/lib/page_extensions.dart @@ -0,0 +1,12 @@ +// Copyright 2026 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +/// Extensions that can be applied to content loaded with Jaspr Content. +library; + +export 'src/extensions/attribute_processor.dart'; +export 'src/extensions/code_block_processor.dart'; +export 'src/extensions/header_extractor.dart'; +export 'src/extensions/header_processor.dart'; +export 'src/extensions/table_processor.dart'; diff --git a/sites/docs/lib/src/analytics/analytics.dart b/packages/site_shared/lib/src/analytics/analytics.dart similarity index 67% rename from sites/docs/lib/src/analytics/analytics.dart rename to packages/site_shared/lib/src/analytics/analytics.dart index 8042bef976..522e7a0bc5 100644 --- a/sites/docs/lib/src/analytics/analytics.dart +++ b/packages/site_shared/lib/src/analytics/analytics.dart @@ -2,19 +2,18 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import 'package:meta/meta.dart'; - import 'analytics_server.dart' if (dart.library.js_interop) 'analytics_web.dart'; /// Used to report analytic events. -final analytics = AnalyticsImplementation(); +final Analytics analytics = AnalyticsImplementation(); -/// Contains methods for reporting analytics events. +/// Used for reporting analytics events. abstract class Analytics { - @internal + /// Reports an event named [eventName], along with the specified [parameters]. void sendEvent(String eventName, Map parameters); + /// Reports whether the user found the current page [helpful]. void sendFeedback(bool helpful) { sendEvent('feedback', {'feedback_type': helpful ? 'up' : 'down'}); } diff --git a/sites/docs/lib/src/analytics/analytics_server.dart b/packages/site_shared/lib/src/analytics/analytics_server.dart similarity index 100% rename from sites/docs/lib/src/analytics/analytics_server.dart rename to packages/site_shared/lib/src/analytics/analytics_server.dart diff --git a/sites/docs/lib/src/analytics/analytics_web.dart b/packages/site_shared/lib/src/analytics/analytics_web.dart similarity index 97% rename from sites/docs/lib/src/analytics/analytics_web.dart rename to packages/site_shared/lib/src/analytics/analytics_web.dart index 568e56e3c9..94e595af88 100644 --- a/sites/docs/lib/src/analytics/analytics_web.dart +++ b/packages/site_shared/lib/src/analytics/analytics_web.dart @@ -6,7 +6,7 @@ import 'package:meta/meta.dart'; import 'package:universal_web/js_interop.dart'; import 'package:universal_web/web.dart' as web; -import '../util.dart'; +import '../../util.dart'; import 'analytics.dart'; /// Web implementation of [Analytics]. diff --git a/sites/docs/lib/src/builders/styles_hash_builder.dart b/packages/site_shared/lib/src/builders/styles_hash_builder.dart similarity index 79% rename from sites/docs/lib/src/builders/styles_hash_builder.dart rename to packages/site_shared/lib/src/builders/styles_hash_builder.dart index 1d1c0bfbf0..32480817db 100644 --- a/sites/docs/lib/src/builders/styles_hash_builder.dart +++ b/packages/site_shared/lib/src/builders/styles_hash_builder.dart @@ -3,9 +3,12 @@ // found in the LICENSE file. import 'dart:convert'; + import 'package:build/build.dart'; import 'package:crypto/crypto.dart'; +Builder stylesHashBuilder(BuilderOptions options) => StylesHashBuilder(options); + class StylesHashBuilder implements Builder { const StylesHashBuilder(this.options); @@ -14,6 +17,7 @@ class StylesHashBuilder implements Builder { @override Map> get buildExtensions => { 'web/assets/css/main.css': ['lib/src/style_hash.dart'], + 'web/main.css': ['lib/src/style_hash.dart'], }; @override @@ -26,17 +30,17 @@ class StylesHashBuilder implements Builder { final inputId = buildStep.inputId; final bytes = await buildStep.readAsBytes(inputId); final digest = sha256.convert(bytes); - hashString = base64.encode(digest.bytes).substring(0, 12); + hashString = base64Url.encode(digest.bytes).substring(0, 12); } final outputContent = - """ -// Generated by docs_flutter_dev_site|stylesHashBuilder. Do not edit. + ''' +// Generated by site_shared|stylesHashBuilder. Do not edit. // dart format off /// The generated hash of the `main.css` file. const generatedStylesHash = '$hashString'; -"""; +'''; await buildStep.writeAsString( buildStep.allowedOutputs.single, diff --git a/sites/docs/lib/src/extensions/attribute_processor.dart b/packages/site_shared/lib/src/extensions/attribute_processor.dart similarity index 100% rename from sites/docs/lib/src/extensions/attribute_processor.dart rename to packages/site_shared/lib/src/extensions/attribute_processor.dart diff --git a/sites/docs/lib/src/extensions/code_block_processor.dart b/packages/site_shared/lib/src/extensions/code_block_processor.dart similarity index 98% rename from sites/docs/lib/src/extensions/code_block_processor.dart rename to packages/site_shared/lib/src/extensions/code_block_processor.dart index 64aa04e207..6f5b8ce420 100644 --- a/sites/docs/lib/src/extensions/code_block_processor.dart +++ b/packages/site_shared/lib/src/extensions/code_block_processor.dart @@ -11,8 +11,8 @@ import 'package:jaspr_content/jaspr_content.dart'; import 'package:meta/meta.dart'; import 'package:opal/opal.dart' as opal; -import '../components/common/wrapped_code_block.dart'; -import '../components/dartpad/dartpad_injector.dart'; +import '../../components/common/wrapped_code_block.dart'; +import '../../components/dartpad/dartpad_injector.dart'; import '../highlight/theme/dark.dart'; import '../highlight/theme/light.dart'; import '../highlight/token_renderer.dart' as highlighter; @@ -21,7 +21,9 @@ final class CodeBlockProcessor implements PageExtension { static final opal.LanguageRegistry _languageRegistry = opal.LanguageRegistry.withDefaults(); - const CodeBlockProcessor(); + const CodeBlockProcessor({required this.defaultTitle}); + + final String defaultTitle; @override Future> apply(Page page, List nodes) async { @@ -55,7 +57,7 @@ final class CodeBlockProcessor implements PageExtension { return ComponentNode( DartPadWrapper( content: lines.join('\n'), - title: title ?? 'Runnable Flutter example', + title: title ?? defaultTitle, theme: metadata['theme'], height: metadata['height'], runAutomatically: metadata['run'] == 'true', diff --git a/sites/docs/lib/src/extensions/header_extractor.dart b/packages/site_shared/lib/src/extensions/header_extractor.dart similarity index 100% rename from sites/docs/lib/src/extensions/header_extractor.dart rename to packages/site_shared/lib/src/extensions/header_extractor.dart diff --git a/sites/docs/lib/src/extensions/header_processor.dart b/packages/site_shared/lib/src/extensions/header_processor.dart similarity index 100% rename from sites/docs/lib/src/extensions/header_processor.dart rename to packages/site_shared/lib/src/extensions/header_processor.dart diff --git a/sites/docs/lib/src/extensions/table_processor.dart b/packages/site_shared/lib/src/extensions/table_processor.dart similarity index 100% rename from sites/docs/lib/src/extensions/table_processor.dart rename to packages/site_shared/lib/src/extensions/table_processor.dart diff --git a/sites/docs/lib/src/highlight/theme/dark.dart b/packages/site_shared/lib/src/highlight/theme/dark.dart similarity index 100% rename from sites/docs/lib/src/highlight/theme/dark.dart rename to packages/site_shared/lib/src/highlight/theme/dark.dart diff --git a/sites/docs/lib/src/highlight/theme/light.dart b/packages/site_shared/lib/src/highlight/theme/light.dart similarity index 100% rename from sites/docs/lib/src/highlight/theme/light.dart rename to packages/site_shared/lib/src/highlight/theme/light.dart diff --git a/sites/docs/lib/src/highlight/token_renderer.dart b/packages/site_shared/lib/src/highlight/token_renderer.dart similarity index 100% rename from sites/docs/lib/src/highlight/token_renderer.dart rename to packages/site_shared/lib/src/highlight/token_renderer.dart diff --git a/sites/docs/lib/src/layouts/dash_layout.dart b/packages/site_shared/lib/src/layouts/dash_layout.dart similarity index 52% rename from sites/docs/lib/src/layouts/dash_layout.dart rename to packages/site_shared/lib/src/layouts/dash_layout.dart index ebca3453a1..854ce187b2 100644 --- a/sites/docs/lib/src/layouts/dash_layout.dart +++ b/packages/site_shared/lib/src/layouts/dash_layout.dart @@ -1,31 +1,40 @@ -// Copyright 2025 The Flutter Authors. All rights reserved. +// Copyright 2026 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. import 'dart:convert'; import 'package:jaspr/dom.dart'; -import 'package:jaspr/jaspr.dart'; +import 'package:jaspr/server.dart'; import 'package:jaspr_content/jaspr_content.dart'; -// import '../components/common/client/cookie_notice.dart'; -import '../components/layout/footer.dart'; -import '../components/layout/header.dart'; -import '../components/layout/sidenav.dart'; -import '../models/sidenav_model.dart'; -import '../style_hash.dart'; -import '../util.dart'; +// import '../../components/common/client/cookie_notice.dart'; +import '../../components/layout/banner.dart'; +import '../../util.dart'; -/// The base Jaspr Content layout for wrapping site content. -abstract class FlutterDocsLayout extends PageLayoutBase { - const FlutterDocsLayout(); +/// The base Jaspr Content layout for all sites. +abstract class DashLayout implements PageLayout { + const DashLayout(); @override String get name; List get defaultBodyClasses => []; - String get defaultSidenav => 'default'; + String? get titleBase => null; + String get siteHost; + bool get cookieNoticeDarkMode => false; + + String get iconUrl; + String get iconUrlApple; + String? get iconUrlApple152 => null; + String? get iconUrlApple167 => null; + String? get iconUrlApple180 => null; + String get twitterSiteTag; + String get twitterDefaultImageUrl; + + String get tagManagerId; + String get analyticsId; /// Returns page-specific URLs to eagerly speculate on, in addition to /// the document-level rules that match all internal links. @@ -35,60 +44,100 @@ abstract class FlutterDocsLayout extends PageLayoutBase { ({Set prerender, Set prefetch}) speculationUrls(Page page) => const (prerender: {}, prefetch: {}); - @override - @mustCallSuper - Iterable buildHead(Page page) { + /// Font stylesheet URLs to load in the document head before site styles. + /// + /// Override this to add, remove, or replace site-level font resources. + List get fontUrls => const [ + 'https://files.flutter-io.cn/fonts/flutter/fonts.css', + 'https://files.flutter-io.cn/fonts/material-icons/material-icons-symbols-outlined.css', + ]; + + String get stylesHash; + + Iterable buildExtraHead(Page page) => const []; + + Iterable _buildHead(Page page) { final pageData = page.data.page; final siteData = page.data.site; + final pageTitle = (pageData['title'] ?? siteData['title']) as String; - final pageDescription = pageData['description'] as String? ?? ''; + final pageDescription = pageData['description'] as String?; + final pageImage = pageData['image'] as String?; + + final windowTitle = titleBase != null + ? '$pageTitle | $titleBase' + : pageTitle; + + final canonicalUrl = pageData['canonical'] as String?; return [ - ...super.buildHead(page), // docs.flutter.cn const meta(httpEquiv: 'Content-Language', content: 'zh'), + Component.element(tag: 'title', children: [.text(windowTitle)]), + if (pageDescription case final String desc) + meta(name: 'description', content: desc), + + // Set indexing and canonical URL configuration. if (pageData['noindex'] case final noIndex? when noIndex == true || noIndex == 'true') const meta(name: 'robots', content: 'noindex'), - if (pageData['canonical'] case final String canonicalUrl - when canonicalUrl.isNotEmpty) + if (canonicalUrl case final canonicalUrl? when canonicalUrl.isNotEmpty) link(rel: 'canonical', href: canonicalUrl), if (pageData['redirectTo'] case final String redirectTo when redirectTo.isNotEmpty) RawText(''), - const link( - rel: 'icon', - href: '/assets/images/cn/flutter-icon.png', - attributes: {'sizes': '64x64'}, - ), - const link( - rel: 'apple-touch-icon', - href: '/assets/images/cn/flutter-320px.png', - ), - const meta(name: 'twitter:card', content: 'summary'), - // docs.flutter.cn - const meta(name: 'twitter:site', content: '@flutterchina'), - meta(name: 'twitter:title', content: pageTitle), + + // Set site icons. + link(rel: 'icon', href: iconUrl, attributes: {'sizes': '64x64'}), + link(rel: 'apple-touch-icon', href: iconUrlApple), + if (iconUrlApple152 case final url?) + link( + rel: 'apple-touch-icon', + href: url, + attributes: {'sizes': '152x152'}, + ), + if (iconUrlApple180 case final url?) + link( + rel: 'apple-touch-icon', + href: url, + attributes: {'sizes': '180x180'}, + ), + if (iconUrlApple167 case final url?) + link( + rel: 'apple-touch-icon', + href: url, + attributes: {'sizes': '167x167'}, + ), + + // Set social media metadata. meta( - name: 'twitter:description', - content: pageDescription, + name: 'twitter:card', + content: pageImage != null ? 'summary_large_image' : 'summary', ), + meta(name: 'twitter:site', content: twitterSiteTag), + meta(name: 'twitter:title', content: pageTitle), + if (pageDescription case final String desc) + meta(name: 'twitter:description', content: desc), + if (pageImage case final String img) + meta(name: 'twitter:image', content: img), meta(attributes: {'property': 'og:title', 'content': pageTitle}), + if (pageDescription case final String desc) + meta(attributes: {'property': 'og:description', 'content': desc}), meta( attributes: { - 'property': 'og:description', - 'content': pageDescription, + 'property': 'og:url', + 'content': canonicalUrl ?? page.path, }, ), - meta(attributes: {'property': 'og:url', 'content': page.path}), - const meta( + meta( attributes: { 'property': 'og:image', - 'content': '/assets/images/cn/flutter-cn-logo.png', + 'content': pageImage ?? twitterDefaultImageUrl, }, ), + // Set site fonts and related preconnection information. const link(rel: 'preconnect', href: 'https://fonts.googleapis.cn'), const link( rel: 'preconnect', @@ -97,42 +146,15 @@ abstract class FlutterDocsLayout extends PageLayoutBase { ), // docs.flutter.cn const link(rel: 'preconnect', href: 'https://files.flutter-io.cn'), - // docs.flutter.cn - const link( - rel: 'stylesheet', - href: 'https://files.flutter-io.cn/fonts/flutter/fonts.css', - ), - // docs.flutter.cn - const link( - rel: 'stylesheet', - href: - 'https://files.flutter-io.cn/fonts/material-icons/material-icons-symbols-outlined.css', - ), - // const link( - // rel: 'stylesheet', - // href: - // 'https://fonts.googleapis.com/css2?family=Google+Sans:wght@400;500;700&display=swap', - // ), - // const link( - // rel: 'stylesheet', - // href: - // 'https://fonts.googleapis.com/css2?family=Google+Sans+Mono:wght@400;500;700&display=swap', - // ), - // const link( - // rel: 'stylesheet', - // href: - // 'https://fonts.googleapis.com/css2?family=Google+Sans+Text:wght@400;500;700&display=swap', - // ), - // const link( - // rel: 'stylesheet', - // href: - // 'https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@24,400,0..1,0', - // ), - const link( + for (final font in fontUrls) link(rel: 'stylesheet', href: font), + + // Set site styles. + link( rel: 'stylesheet', - href: '/assets/css/main.css?hash=$generatedStylesHash', + href: '/assets/css/main.css?hash=$stylesHash', ), + // Set site scripts. if (pageData['js'] case final List jsList) for (final js in jsList) if (js case {'url': final String jsUrl, 'defer': final Object? defer}) @@ -140,7 +162,8 @@ abstract class FlutterDocsLayout extends PageLayoutBase { src: jsUrl, attributes: {if (defer == 'true' || defer == true) 'defer': ''}, ), - // docs.flutter.cn + ...buildExtraHead(page), + const script( src: 'https://files.flutter-io.cn/static/deps/lite-youtube/1.8.1/lite-youtube.js', @@ -153,11 +176,9 @@ abstract class FlutterDocsLayout extends PageLayoutBase { ), // Set up tag manager and analytics. - // if (productionBuild) - // raw(''' - // ... - // '''), - + // if (productionBuild) ...[ + // ...... + // ], // docs.flutter.cn // Google tag (gtag.js) if (productionBuild) @@ -179,45 +200,43 @@ abstract class FlutterDocsLayout extends PageLayoutBase { })(); '''), + // Add speculation rules and prefetch fallback links for // URLs provided by subclass overrides of speculationUrls. ..._buildSpeculationRulesHead(page), ]; } + Component buildBody(Page page, Component child); + @override - Component buildBody(Page page, Component child) { + Component buildLayout(Page page, Component child) { final pageData = page.data.page; final bodyClass = pageData['bodyClass'] as String?; - final pageUrl = page.url.startsWith('/') ? page.url : '/${page.url}'; - final sidenavs = page.data['sidenav'] as Map; - final pageSidenavKey = pageData['sidenav'] as String? ?? defaultSidenav; - final sideNavEntries = navEntriesFromData( - sidenavs[pageSidenavKey] as List, - ); - - final obsolete = pageData['obsolete'] == true; - - return .fragment( - [ - const Document.html( - attributes: { - 'lang': 'zh', - 'dir': 'ltr', - }, + return Component.element( + tag: 'html', + attributes: {'lang': 'zh', 'dir': 'ltr'}, + children: [ + Component.element( + tag: 'head', + children: [ + const meta(charset: 'utf-8'), + const meta( + name: 'viewport', + content: 'width=device-width, initial-scale=1', + ), + ..._buildHead(page), + ], ), - if ([?bodyClass, ...defaultBodyClasses] case final bodyClasses - when bodyClasses.isNotEmpty) - Document.body( - attributes: { - 'class': bodyClasses.toClasses, - }, - ), - // The theme setting logic should remain before other scripts to - // avoid a flash of the initial theme on load. - const RawText(''' - - '''), - if (productionBuild) - const RawText( - '', - ), - const a( - id: 'skip-to-main', - classes: 'filled-button', - href: '#site-content-title', - [.text('跳转至正文')], - ), - // const CookieNotice(), - const DashHeader(), - div(id: 'site-below-header', [ - div(id: 'site-main-row', [ - DashSideNav( - navEntries: sideNavEntries, - currentPageUrl: pageUrl, + ''', ), - main_( - id: 'page-content', - classes: [ - if (pageData['focusedLayout'] == true) 'focused', - ].toClasses, - [child], + if (productionBuild) + const RawText( + '', + ), + const a( + id: 'skip-to-main', + classes: 'filled-button', + href: '#site-content-title', + attributes: {'tabindex': '1'}, + [.text('跳转至正文')], ), - if (obsolete) - const div(id: 'obsolete-banner', [ - div(classes: 'text-center', [ - .text( - '本页面的部分内容可能已经过时。', - ), - ]), - ]), - ]), - const DashFooter(), - ]), - // Scroll the sidenav to the active item before other logic - // to avoid it jumping after page load. - const RawText(''' - - '''), + // CookieNotice(host: siteHost, + // alwaysDarkMode: cookieNoticeDarkMode), + buildBody(page, child), + ], + ), ], ); } + /// Builds the banner component for the given [page]. + Component? buildBanner(Page page) { + final showBanner = + (page.data.page['showBanner'] as bool?) ?? + (page.data.site['showBanner'] as bool?) ?? + false; + if (showBanner) { + if (page.data['banner'] case final List bannerData) { + return DashBanner(BannerContent.fromList(bannerData)); + } + } + + return null; + } + /// Builds the speculation rules `