diff --git a/packages/site_shared/README.md b/packages/site_shared/README.md new file mode 100644 index 00000000000..db75ce7d779 --- /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 00000000000..95c3595413a --- /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 00000000000..2f015507221 --- /dev/null +++ b/packages/site_shared/build.yaml @@ -0,0 +1,14 @@ +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" + 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 e4df9379615..79b5b15e8f5 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 7c9a31b48a8..56a5ec68cc3 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 00000000000..ea0d9618f75 --- /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 00000000000..44970297a98 --- /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 97% rename from sites/docs/lib/_sass/components/_site-switcher.scss rename to packages/site_shared/lib/_sass/components/_site-switcher.scss index 58b3fb0e331..07b6fad5300 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; 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 100% rename from sites/docs/lib/_sass/components/_summary-card.scss rename to packages/site_shared/lib/_sass/components/_summary-card.scss diff --git a/sites/docs/lib/_sass/components/_tabs.scss b/packages/site_shared/lib/_sass/components/_tabs.scss similarity index 100% rename from sites/docs/lib/_sass/components/_tabs.scss rename to packages/site_shared/lib/_sass/components/_tabs.scss 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 5584613fb15..b17b985a5d5 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 00000000000..c697464b75f --- /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 66d9bb4894f..9a0ef2790b0 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 740af24cf37..85feaebbd5d 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 41415e59693..080ec351b95 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 a9c5c851427..e401f1daddb 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 b4362a5aca0..a01abe85135 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 fffc9dc0aba..39f1a0f835d 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 721bee0d308..c9efaf603c8 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 869527f5de5..12e9f1fba84 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 bf177204590..e6e62ca5f03 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 932f174ff24..3cdb63782bf 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 95c0981d579..69bb328a4f1 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/sites/docs/lib/src/components/layout/banner.dart b/packages/site_shared/lib/components/layout/banner.dart similarity index 100% rename from sites/docs/lib/src/components/layout/banner.dart rename to packages/site_shared/lib/components/layout/banner.dart 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 73% rename from sites/docs/lib/src/components/layout/site_switcher.dart rename to packages/site_shared/lib/components/layout/site_switcher.dart index 811a308957e..3dc00d2e851 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.dev', ), @@ -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.dev', ), - _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.dev', + dart: true, + current: !isFlutter, + ), + if (!isFlutter) + const _SiteWordMarkListEntry( name: 'Dart', - href: 'https://dart.dev', - dart: true, - ), - _SiteWordMarkListEntry( - name: 'DartPad', - href: 'https://dartpad.dev', - dart: true, - ), - _SiteWordMarkListEntry( - name: 'pub.dev', - href: 'https://pub.dev', + subtype: 'API', + href: 'https://api.dart.dev', dart: true, ), - ], - ), + const _SiteWordMarkListEntry( + name: 'DartPad', + href: 'https://dartpad.dev', + dart: true, + ), + const _SiteWordMarkListEntry( + name: 'pub.dev', + href: 'https://pub.dev', + 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 95% rename from sites/docs/lib/src/components/tutorial/client/quiz.dart rename to packages/site_shared/lib/components/tutorial/client/quiz.dart index f04b044568f..98200df9b00 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('That\'s right!')]) else - const p(classes: 'incorrect', [.text('Not quite')]), - p([.text(option.explanation)]), + const p(classes: 'incorrect', [.text('Not quite.')]), + p([RawText(option.explanation)]), ]), ]), ], @@ -144,7 +144,7 @@ class _InteractiveQuizState extends State { currentQuestionIndex--; }); }, - content: 'Previous', + content: 'Previous question', ), Button( key: nextButtonKey, 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 b11391a25b3..ab77e8b583d 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 c4453b17f87..1658e8b92e5 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 00000000000..f576e34877b --- /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 76b8a2e7dd1..298745d318d 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 672680636f8..c4297b1a656 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 00000000000..722299368b3 --- /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 00000000000..78632c1ce83 --- /dev/null +++ b/packages/site_shared/lib/components/utils/define_component.dart @@ -0,0 +1,33 @@ +// 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) { + return CustomComponent( + pattern: RegExp(name, caseSensitive: false), + builder: (_, _, _) => child, + ); +} + +CustomComponent defineComponentWithAttrs( + String name, + Component Function(Map attributes) factory, +) { + return CustomComponent( + pattern: RegExp(name, caseSensitive: false), + builder: (_, attrs, _) => factory(attrs), + ); +} + +CustomComponent defineComponentWithChild( + String name, + Component Function(Map attributes, Component? child) factory, +) { + return 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 00000000000..3e8a0a6595f --- /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 00000000000..23917e4faa7 --- /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 00000000000..3b889b65c0b --- /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 8042bef976d..522e7a0bc51 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 568e56e3c9c..94e595af88a 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 89% rename from sites/docs/lib/src/builders/styles_hash_builder.dart rename to packages/site_shared/lib/src/builders/styles_hash_builder.dart index 1d1c0bfbf07..e86a33e07ae 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); @@ -31,7 +34,7 @@ class StylesHashBuilder implements Builder { 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. 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 64aa04e2074..6f5b8ce4208 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 53% rename from sites/docs/lib/src/layouts/dash_layout.dart rename to packages/site_shared/lib/src/layouts/dash_layout.dart index d1e5d6a676a..fe00482c80e 100644 --- a/sites/docs/lib/src/layouts/dash_layout.dart +++ b/packages/site_shared/lib/src/layouts/dash_layout.dart @@ -1,32 +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/banner.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. @@ -36,88 +44,114 @@ abstract class FlutterDocsLayout extends PageLayoutBase { ({Set prerender, Set prefetch}) speculationUrls(Page page) => const (prerender: {}, prefetch: {}); - @override - @mustCallSuper - Iterable buildHead(Page page) { + List get fonts => [ + 'https://fonts.googleapis.com/css2?family=Google+Sans:wght@400;500;700&display=swap', + 'https://fonts.googleapis.com/css2?family=Google+Sans+Mono:wght@400;500;700&display=swap', + 'https://fonts.googleapis.com/css2?family=Google+Sans+Text:wght@400;500;700&display=swap', + 'https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@24,400,0..1,0', + 'https://fonts.googleapis.com/css2?family=Noto+Serif:ital,wght@0,100..900;1,100..900&display=swap', + ]; + + 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), + 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/branding/flutter/icon/64.png', - attributes: {'sizes': '64x64'}, - ), - const link( - rel: 'apple-touch-icon', - href: '/assets/images/branding/flutter/logo/flutter-logomark-320px.png', - ), - const meta(name: 'twitter:card', content: 'summary'), - const meta(name: 'twitter:site', content: '@flutterdev'), - 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/flutter-logo-sharing.png', + 'content': pageImage ?? twitterDefaultImageUrl, }, ), + // Set site fonts and related preconnection information. const link(rel: 'preconnect', href: 'https://fonts.googleapis.com'), const link( rel: 'preconnect', href: 'https://fonts.gstatic.com', attributes: {'crossorigin': ''}, ), - 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( + for (final font in fonts) link(rel: 'stylesheet', href: font), + + // Set site styles. + link( rel: 'stylesheet', href: - 'https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@24,400,0..1,0', - ), - const link( - rel: 'stylesheet', - href: '/assets/css/main.css?hash=$generatedStylesHash', + '/assets/css/main.css?hash=${Uri.encodeQueryComponent(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}) @@ -125,6 +159,8 @@ abstract class FlutterDocsLayout extends PageLayoutBase { src: jsUrl, attributes: {if (defer == 'true' || defer == true) 'defer': ''}, ), + ...buildExtraHead(page), + const script( src: 'https://cdn.jsdelivr.net/npm/@justinribeiro/lite-youtube@1.8.2/lite-youtube.js', @@ -137,67 +173,68 @@ abstract class FlutterDocsLayout extends PageLayoutBase { ), // Set up tag manager and analytics. - if (productionBuild) - const RawText(''' - - - - - -'''), +''', + ), + ], + // 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': 'en', - 'dir': 'ltr', - }, + return Component.element( + tag: 'html', + attributes: {'lang': 'en', '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('Skip to main content')], - ), - 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) + RawText( + '', + ), + const a( + id: 'skip-to-main', + classes: 'filled-button', + href: '#site-content-title', + attributes: {'tabindex': '1'}, + [.text('Skip to main content')], ), - if (obsolete) - const div(id: 'obsolete-banner', [ - div(classes: 'text-center', [ - .text( - 'Some content on this page might be out of date.', - ), - ]), - ]), - ]), - 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), + ], + ), ], ); } diff --git a/sites/docs/lib/src/markdown/alert_syntax.dart b/packages/site_shared/lib/src/markdown/alert_syntax.dart similarity index 100% rename from sites/docs/lib/src/markdown/alert_syntax.dart rename to packages/site_shared/lib/src/markdown/alert_syntax.dart diff --git a/sites/docs/lib/src/markdown/attribute_syntax.dart b/packages/site_shared/lib/src/markdown/attribute_syntax.dart similarity index 98% rename from sites/docs/lib/src/markdown/attribute_syntax.dart rename to packages/site_shared/lib/src/markdown/attribute_syntax.dart index 9a213317fac..b54a8b35622 100644 --- a/sites/docs/lib/src/markdown/attribute_syntax.dart +++ b/packages/site_shared/lib/src/markdown/attribute_syntax.dart @@ -4,7 +4,7 @@ import 'package:markdown/markdown.dart' as md; -import '../util.dart'; +import '../../util.dart'; /// A `package:markdown` extension that adds support for /// attribute syntax as a standalone block. diff --git a/sites/docs/lib/src/markdown/fenced_code_block_syntax.dart b/packages/site_shared/lib/src/markdown/fenced_code_block_syntax.dart similarity index 100% rename from sites/docs/lib/src/markdown/fenced_code_block_syntax.dart rename to packages/site_shared/lib/src/markdown/fenced_code_block_syntax.dart diff --git a/sites/docs/lib/src/markdown/header_syntax.dart b/packages/site_shared/lib/src/markdown/header_syntax.dart similarity index 98% rename from sites/docs/lib/src/markdown/header_syntax.dart rename to packages/site_shared/lib/src/markdown/header_syntax.dart index e4be28e7e11..6da8f2d7986 100644 --- a/sites/docs/lib/src/markdown/header_syntax.dart +++ b/packages/site_shared/lib/src/markdown/header_syntax.dart @@ -4,7 +4,7 @@ import 'package:markdown/markdown.dart' as md; -import '../util.dart'; +import '../../util.dart'; /// A custom header syntax that extends the default header syntax to support: /// diff --git a/sites/docs/lib/src/markdown/markdown_parser.dart b/packages/site_shared/lib/src/markdown/markdown_parser.dart similarity index 98% rename from sites/docs/lib/src/markdown/markdown_parser.dart rename to packages/site_shared/lib/src/markdown/markdown_parser.dart index ea8bead4e26..05d44b53b51 100644 --- a/sites/docs/lib/src/markdown/markdown_parser.dart +++ b/packages/site_shared/lib/src/markdown/markdown_parser.dart @@ -14,7 +14,6 @@ import 'package:jaspr_content/jaspr_content.dart'; import 'package:markdown/markdown.dart' as md; import 'package:markdown_description_list/markdown_description_list.dart'; -import '../extensions/registry.dart'; import 'alert_syntax.dart'; import 'attribute_syntax.dart'; import 'fenced_code_block_syntax.dart'; @@ -65,7 +64,7 @@ class DashMarkdown extends AsyncStatelessComponent { ? _defaultMarkdownDocument.parseInline(content) : _defaultMarkdownDocument.parse(content); var nodes = DashMarkdownParser.buildNodes(markdownNodes); - for (final extension in allNodeProcessingExtensions) { + for (final extension in currentPage.config.extensions) { nodes = await extension.apply(currentPage, nodes); } diff --git a/sites/docs/lib/src/components/util/retake_element.dart b/packages/site_shared/lib/src/utils/retake_element.dart similarity index 100% rename from sites/docs/lib/src/components/util/retake_element.dart rename to packages/site_shared/lib/src/utils/retake_element.dart diff --git a/sites/docs/lib/src/components/util/retake_element_vm.dart b/packages/site_shared/lib/src/utils/retake_element_vm.dart similarity index 51% rename from sites/docs/lib/src/components/util/retake_element_vm.dart rename to packages/site_shared/lib/src/utils/retake_element_vm.dart index 775262eedd1..b00fc0f18f9 100644 --- a/sites/docs/lib/src/components/util/retake_element_vm.dart +++ b/packages/site_shared/lib/src/utils/retake_element_vm.dart @@ -3,15 +3,9 @@ // found in the LICENSE file. import 'package:jaspr/jaspr.dart'; -import 'package:universal_web/web.dart' as web; -web.Element? retakeElement( - BuildContext context, - bool Function(web.Element element) predicate, -) { +Component retakeRef(BuildContext context, String id) { throw UnimplementedError(); } -Component wrapNode(web.Node node) { - throw UnimplementedError(); -} +String extractContent(Element element) => ''; diff --git a/sites/docs/lib/src/components/util/retake_element_web.dart b/packages/site_shared/lib/src/utils/retake_element_web.dart similarity index 59% rename from sites/docs/lib/src/components/util/retake_element_web.dart rename to packages/site_shared/lib/src/utils/retake_element_web.dart index 36a1694baac..5631e5ac73f 100644 --- a/sites/docs/lib/src/components/util/retake_element_web.dart +++ b/packages/site_shared/lib/src/utils/retake_element_web.dart @@ -5,23 +5,54 @@ import 'package:jaspr/client.dart'; // ignore: implementation_imports import 'package:jaspr/src/dom/type_checks.dart'; +import 'package:universal_web/js_interop.dart'; import 'package:universal_web/web.dart' as web; -/// Retakes the element matching [predicate] during hydration. -web.Element? retakeElement( - BuildContext context, - bool Function(web.Element element) predicate, -) { +/// Retakes the element with the specified [id] ref during hydration. +Component retakeRef(BuildContext context, String id) { final r = (context as Element).parentRenderObjectElement?.renderObject; - if (r == null) return null; - final node = (r as DomRenderObject).retakeNode((node) { - return node.isElement && predicate(node as web.Element); + if (r == null) return const .empty(); + + var node = (r as DomRenderObject).retakeNode((node) { + return node.isComment && (node as web.Comment).data.startsWith('ref:$id'); }); - return node as web.Element?; + + if (node == null) return const .empty(); + + final nodes = [node]; + + node = node.nextSibling; + while (node != null) { + r.retakeNode((n) => n == node); + nodes.add(node); + + if (node.isComment && (node as web.Comment).data.startsWith('/ref:$id')) { + break; + } + + node = node.nextSibling; + } + + return .fragment([ + for (final node in nodes) RawNode(node), + ]); } -Component wrapNode(web.Node node) { - return RawNode(node); +/// Extracts the content of a
 block inside the given
+/// [element] during hydration.
+String extractContent(Element element) {
+  final r = element.parentRenderObjectElement?.renderObject as DomRenderObject?;
+  if (r == null) return '';
+
+  final code = r.retakeNode((node) {
+    return node.instanceOfString('Element') &&
+        (node as web.Element).tagName.toLowerCase() == 'pre';
+  });
+
+  if (code == null) return '';
+
+  code.parentNode?.removeChild(code);
+  return (code as web.Element).textContent ?? '';
 }
 
 class RawNode extends Component {
diff --git a/packages/site_shared/lib/src/utils/slugify.dart b/packages/site_shared/lib/src/utils/slugify.dart
new file mode 100644
index 00000000000..8bcd0b0c3e4
--- /dev/null
+++ b/packages/site_shared/lib/src/utils/slugify.dart
@@ -0,0 +1,21 @@
+// 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.
+
+/// Converts the specified [text] into a standardized URL slug
+/// that can be used as the ID for headers and other anchors in HTML.
+String slugify(String text) => text
+    .toLowerCase()
+    .trim()
+    .replaceAll(_slugifyPunctuationToReplace, '-')
+    .replaceAll(_slugifyUnsupportedToRemove, '')
+    .replaceAll(_slugifyCharsToCombine, '-')
+    .replaceAll(_slugifyHyphenTrim, '');
+
+final RegExp _slugifyPunctuationToReplace = RegExp(r'[:._]');
+final RegExp _slugifyUnsupportedToRemove = RegExp(
+  r'[^\p{L}\p{N}\s:.-]',
+  unicode: true,
+);
+final RegExp _slugifyCharsToCombine = RegExp(r'[\s-]+');
+final RegExp _slugifyHyphenTrim = RegExp(r'^-+|-+$');
diff --git a/packages/site_shared/lib/src/utils/web_current_os.dart b/packages/site_shared/lib/src/utils/web_current_os.dart
new file mode 100644
index 00000000000..87782bd39b5
--- /dev/null
+++ b/packages/site_shared/lib/src/utils/web_current_os.dart
@@ -0,0 +1,60 @@
+// 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.
+
+/// Utilities for detecting the platform a user's web browser is running on.
+library;
+
+import 'package:jaspr/jaspr.dart';
+import 'package:universal_web/web.dart' as web;
+
+/// An operating system that a user's web browser can run on.
+enum OperatingSystem {
+  windows(id: 'windows', label: 'Windows'),
+  macOS(id: 'macos', label: 'macOS'),
+  linux(id: 'linux', label: 'Linux'),
+  chromeOS(id: 'chromeos', label: 'ChromeOS');
+
+  const OperatingSystem({required this.id, required this.label});
+
+  /// All-lowercase identifier for the operating system.
+  final String id;
+
+  /// Human-readable name of the operating system.
+  final String label;
+}
+
+/// The [OperatingSystem] reported by the current web browser,
+/// determined from its user agent string.
+///
+/// Is `null` if the operating system can't be determined or
+/// when not running on the web (such as during server-side rendering).
+OperatingSystem? get browserOperatingSystem {
+  // The browser user agent is only available when running on the web.
+  if (!kIsWeb) return null;
+
+  final userAgent = web.window.navigator.userAgent;
+  if (userAgent.contains('Mac')) {
+    // macOS or iPhone.
+    return .macOS;
+  }
+
+  if (userAgent.contains('Win')) {
+    // Windows.
+    return .windows;
+  }
+
+  if ((userAgent.contains('Linux') || userAgent.contains('X11')) &&
+      !userAgent.contains('Android')) {
+    // Linux, but not Android.
+    return .linux;
+  }
+
+  if (userAgent.contains('CrOS')) {
+    // ChromeOS.
+    return .chromeOS;
+  }
+
+  // Any other, undetermined operating system.
+  return null;
+}
diff --git a/sites/docs/lib/src/util.dart b/packages/site_shared/lib/util.dart
similarity index 61%
rename from sites/docs/lib/src/util.dart
rename to packages/site_shared/lib/util.dart
index ad146314dcf..e064a2c7213 100644
--- a/sites/docs/lib/src/util.dart
+++ b/packages/site_shared/lib/util.dart
@@ -2,37 +2,15 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
-import 'package:universal_web/web.dart' as web;
+export 'src/utils/slugify.dart';
 
 /// Whether this build of the site will be deployed to production.
-const productionBuild = bool.fromEnvironment('PRODUCTION');
-
-/// Path to the `/src` directory where site content is located.
-const siteSrcDirectoryPath = 'src';
-
-/// Converts the specified [text] into a standardized URL slug
-/// that can be used as the ID for headers and other anchors in HTML.
-String slugify(String text) => text
-    .toLowerCase()
-    .trim()
-    .replaceAll(_slugifyPunctuationToReplace, '-')
-    .replaceAll(_slugifyUnsupportedToRemove, '')
-    .replaceAll(_slugifyCharsToCombine, '-')
-    .replaceAll(_slugifyHyphenTrim, '');
-
-final RegExp _slugifyPunctuationToReplace = RegExp(r'[:._]');
-final RegExp _slugifyUnsupportedToRemove = RegExp(
-  r'[^\p{L}\p{N}\s:.-]',
-  unicode: true,
-);
-final RegExp _slugifyCharsToCombine = RegExp(r'[\s-]+');
-final RegExp _slugifyHyphenTrim = RegExp(r'^-+|-+$');
-
-final RegExp _attributePattern = RegExp(r'(\w+)="([^"]*)"');
-final RegExp _whitespacePattern = RegExp(r'\s+');
-final RegExp _wordPattern = RegExp(r'\S+(?:\s*|$)');
-final RegExp _trailingMarkdownLinkPattern = RegExp(r'(\[.+\]:\s*\S+\s*)+$');
+const bool productionBuild = .fromEnvironment('PRODUCTION');
 
+/// Parses attribute tokens from [attributeString] into HTML attributes.
+///
+/// Supports IDs (`#id`), classes (`.class`), and `key="value"` pairs,
+/// joining multiple classes into a single space-separated `class` value.
 Map parseAttributes(String attributeString) {
   final attributes = {};
   final classes = [];
@@ -68,6 +46,17 @@ Map parseAttributes(String attributeString) {
   return attributes;
 }
 
+final RegExp _attributePattern = RegExp(r'(\w+)="([^"]*)"');
+final RegExp _whitespacePattern = RegExp(r'\s+');
+final RegExp _wordPattern = RegExp(r'\S+(?:\s*|$)');
+final RegExp _trailingMarkdownLinkPattern = RegExp(r'(\[.+\]:\s*\S+\s*)+$');
+
+/// Truncates the specified [text] to the specified number of words [maxWords].
+///
+/// Leaves already-short text unchanged and
+/// appends `...` after normalizing retained word spacing.
+///
+/// Returns an empty string when [maxWords] is non-positive.
 String truncateWords(String text, int maxWords) {
   if (maxWords <= 0) {
     return '';
@@ -91,11 +80,13 @@ String truncateWordsMarkdown(String text, int maxWords) {
   }
 
   final trailingLinks = _trailingMarkdownLinkPattern.firstMatch(text);
-  var endContent = '';
+  final String endContent;
 
   if (trailingLinks != null) {
     text = text.substring(0, trailingLinks.start);
     endContent = '\n${trailingLinks.group(0)!}';
+  } else {
+    endContent = '';
   }
 
   final matches = _wordPattern.allMatches(text);
@@ -108,6 +99,7 @@ String truncateWordsMarkdown(String text, int maxWords) {
 }
 
 extension StringUnCapitalize on String {
+  /// Returns a new string with the first character converted to lowercase.
   String unCapitalize() =>
       isEmpty ? this : substring(0, 1).toLowerCase() + substring(1);
 }
@@ -117,42 +109,3 @@ extension ListToClasses on List {
   /// that can be added to an HTML element.
   String get toClasses => join(' ');
 }
-
-enum OperatingSystem {
-  windows('Windows'),
-  macos('macOS'),
-  linux('Linux'),
-  chromeos('ChromeOS');
-
-  const OperatingSystem(this.label);
-  final String label;
-}
-
-/// Get the user's current operating system, or
-/// `null` if not of one "macos", "windows", "linux", or "chromeos".
-OperatingSystem? getOS() {
-  final userAgent = web.window.navigator.userAgent;
-  if (userAgent.contains('Mac')) {
-    // macOS or iPhone
-    return OperatingSystem.macos;
-  }
-
-  if (userAgent.contains('Win')) {
-    // Windows
-    return OperatingSystem.windows;
-  }
-
-  if ((userAgent.contains('Linux') || userAgent.contains('X11')) &&
-      !userAgent.contains('Android')) {
-    // Linux, but not Android
-    return OperatingSystem.linux;
-  }
-
-  if (userAgent.contains('CrOS')) {
-    // ChromeOS
-    return OperatingSystem.chromeos;
-  }
-
-  // Anything else
-  return null;
-}
diff --git a/packages/site_shared/lib/web_util.dart b/packages/site_shared/lib/web_util.dart
new file mode 100644
index 00000000000..f7818cc24d7
--- /dev/null
+++ b/packages/site_shared/lib/web_util.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.
+
+/// Utilities meant to be used when running on the web.
+library;
+
+export 'src/utils/web_current_os.dart';
diff --git a/packages/site_shared/pubspec.yaml b/packages/site_shared/pubspec.yaml
new file mode 100644
index 00000000000..acc16fe5453
--- /dev/null
+++ b/packages/site_shared/pubspec.yaml
@@ -0,0 +1,25 @@
+name: site_shared
+publish_to: none
+
+resolution: workspace
+environment:
+  sdk: ^3.12.0
+
+dependencies:
+  build: ^4.0.6
+  collection: ^1.19.1
+  crypto: ^3.0.7
+  html: ^0.15.6
+  jaspr: ^0.23.1
+  jaspr_content: ^0.5.2
+  markdown: ^7.3.1
+  markdown_description_list: ^0.1.1
+  meta: ^1.18.2
+  nanoid2: ^2.0.1
+  opal: ^0.2.2
+  path: ^1.9.1
+  universal_web: ^1.1.1+1
+  yaml: ^3.1.3
+
+dev_dependencies:
+  analysis_defaults: any
diff --git a/pubspec.yaml b/pubspec.yaml
index 38cb7599e62..51ff774796d 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -9,6 +9,7 @@ environment:
 workspace:
   - packages/analysis_defaults
   - packages/excerpter
+  - packages/site_shared
   - sites/docs
   - sites/www
   - tool/dash_site
diff --git a/sites/docs/build.yaml b/sites/docs/build.yaml
index 4787d5a2661..b30a155a0fa 100644
--- a/sites/docs/build.yaml
+++ b/sites/docs/build.yaml
@@ -1,13 +1,3 @@
-builders:
-  stylesHashBuilder:
-    import: 'package:docs_flutter_dev_site/builders.dart'
-    builder_factories: [ 'stylesHashBuilder' ]
-    build_extensions:
-      "web/assets/css/main.css":
-        - "lib/src/style_hash.dart"
-    auto_apply: root_package
-    build_to: source
-
 targets:
   $default:
     builders:
@@ -27,6 +17,3 @@ targets:
             # Disable for now.
             # dart2wasm:
             #   args: [-O2, -Djaspr.flags.release=true]
-      docs_flutter_dev_site:stylesHashBuilder:
-        dev_options:
-          fixed_hash: true
diff --git a/sites/docs/lib/_sass/_site.scss b/sites/docs/lib/_sass/_site.scss
index c400cf86d58..99092c6a676 100644
--- a/sites/docs/lib/_sass/_site.scss
+++ b/sites/docs/lib/_sass/_site.scss
@@ -1,6 +1,6 @@
 // Must be imported first to ensure that
 // the reset is applied before any other styles are applied.
-@use 'base/reset';
+@use 'package:site_shared/_sass/base/reset';
 
 // Must be imported second to ensure that our defined CSS variables are defined
 // before any other styles are applied and they are used.
@@ -11,17 +11,7 @@
 @use 'base/utils';
 
 // Styles for individual components or content types, alphabetically ordered.
-@use 'components/alert';
-@use 'components/banner';
-@use 'components/books';
-@use 'components/breadcrumbs';
-@use 'components/button';
-@use 'components/card';
-@use 'components/code';
 @use 'components/content';
-@use 'components/collapsible';
-@use 'components/cookie-notice';
-@use 'components/dropdown';
 @use 'components/expansion-list';
 @use 'components/filter-search';
 @use 'components/footer';
@@ -33,18 +23,30 @@
 @use 'components/pagenav';
 @use 'components/platform-cards';
 @use 'components/pill';
-@use 'components/quiz';
 @use 'components/sidebar';
 @use 'components/side-menu';
-@use 'components/site-switcher';
-@use 'components/summary-card';
-@use 'components/stepper';
-@use 'components/tabs';
-@use 'components/theming';
-@use 'components/tooltip';
 @use 'components/trailing';
 @use 'components/tutorial_pages';
 
+// Shared styles from site_shared package, alphabetically ordered.
+@use 'package:site_shared/_sass/components/alert';
+@use 'package:site_shared/_sass/components/banner';
+@use 'package:site_shared/_sass/components/breadcrumbs';
+@use 'package:site_shared/_sass/components/button';
+@use 'package:site_shared/_sass/components/card';
+@use 'package:site_shared/_sass/components/code';
+@use 'package:site_shared/_sass/components/cookie-notice';
+@use 'package:site_shared/_sass/components/dropdown';
+@use 'package:site_shared/_sass/components/menu-toggle';
+@use 'package:site_shared/_sass/components/progress-ring';
+@use 'package:site_shared/_sass/components/quiz';
+@use 'package:site_shared/_sass/components/site-switcher';
+@use 'package:site_shared/_sass/components/stepper';
+@use 'package:site_shared/_sass/components/summary-card';
+@use 'package:site_shared/_sass/components/tabs';
+@use 'package:site_shared/_sass/components/theming';
+@use 'package:site_shared/_sass/components/tooltip';
+
 // Styles for specific pages, alphabetically ordered.
 @use 'pages/glossary';
 @use 'pages/learning-resources-index';
diff --git a/sites/docs/lib/_sass/base/_base.scss b/sites/docs/lib/_sass/base/_base.scss
index 181c9d05408..f43758e2f28 100644
--- a/sites/docs/lib/_sass/base/_base.scss
+++ b/sites/docs/lib/_sass/base/_base.scss
@@ -411,57 +411,6 @@ p+dl {
   color: var(--site-base-fgColor-lighter);
 }
 
-.card-os-bug {
-  position: relative;
-
-  &::before {
-    content: "";
-    position: absolute;
-    top: 0.5rem;
-    left: 0.5rem;
-    width: 24px;
-    height: 24px;
-    background-position: center center;
-    background-size: contain;
-    background-color: transparent;
-    background-repeat: no-repeat;
-    z-index: var(--site-z-floating);
-    /* Place in front of card */
-  }
-}
-
-.card-macos {
-  @extend .card-os-bug;
-
-  &::before {
-    background-image: url("/assets/images/docs/brand-svg/macos-bug.svg");
-  }
-}
-
-.card-windows {
-  @extend .card-os-bug;
-
-  &::before {
-    background-image: url("/assets/images/docs/brand-svg/windows-bug.svg");
-  }
-}
-
-.card-linux {
-  @extend .card-os-bug;
-
-  &::before {
-    background-image: url("/assets/images/docs/brand-svg/linux.svg");
-  }
-}
-
-.card-chromeos {
-  @extend .card-os-bug;
-
-  &::before {
-    background-image: url("/assets/images/docs/brand-svg/chromeos.svg");
-  }
-}
-
 .video-wrapper {
   display: flex;
   flex-direction: column;
diff --git a/sites/docs/lib/_sass/base/_print-overrides.scss b/sites/docs/lib/_sass/base/_print-overrides.scss
index fab0e919a3a..87e0afcbe6a 100644
--- a/sites/docs/lib/_sass/base/_print-overrides.scss
+++ b/sites/docs/lib/_sass/base/_print-overrides.scss
@@ -1,22 +1,21 @@
 // Overrides for printing so only page content is printed
 @media print {
 
-  // Ignore navigation elements and other non-necessary interactive ones
+  // Ignore navigation elements and other non-necessary interactive ones.
   #site-header,
-  .subnav,
   .site-footer,
   #sidenav,
   .navbar,
   #site-toc--side,
   #page-github-links,
   #cookie-notice,
-  .site-banner,
-  .code-excerpt__copy-btn,
-  .breadcrumb {
+  #site-banner,
+  .code-block-wrapper .copy-button,
+  nav.breadcrumbs {
     display: none !important;
   }
 
-  // Make sure content fills up 100% of width
+  // Make sure content fills up 100% of width.
   #page-content {
     max-width: 100% !important;
     margin-left: 0;
@@ -30,12 +29,12 @@
     display: none;
   }
 
-  // Display underlines under links
+  // Display underlines under links.
   a {
     text-decoration: underline;
   }
 
-  // Show borders around notes and code blocks
+  // Show borders around notes and code blocks.
   .alert,
   pre {
     border: 1px solid black;
diff --git a/sites/docs/lib/_sass/base/_utils.scss b/sites/docs/lib/_sass/base/_utils.scss
index 1eb32e68c78..644e49ddac9 100644
--- a/sites/docs/lib/_sass/base/_utils.scss
+++ b/sites/docs/lib/_sass/base/_utils.scss
@@ -69,4 +69,3 @@ main {
 .text-center {
   text-align: center;
 }
-
diff --git a/sites/docs/lib/_sass/components/_books.scss b/sites/docs/lib/_sass/components/_books.scss
deleted file mode 100644
index b4375766358..00000000000
--- a/sites/docs/lib/_sass/components/_books.scss
+++ /dev/null
@@ -1,27 +0,0 @@
-.book-img-with-details {
-  margin-block-start: 0.5rem;
-  display: flex;
-  flex-direction: column;
-  row-gap: 1rem;
-  column-gap: 2rem;
-
-  @media (min-width: 576px) {
-    flex-direction: row;
-    margin-block-start: 1.5rem;
-  }
-
-  img {
-    min-width: 0;
-    width: 160px;
-    max-width: 240px;
-
-    @media (min-width: 768px) {
-      width: 200px;
-    }
-  }
-
-  h3.title {
-    font-size: 1.5rem;
-    margin-block: 0;
-  }
-}
diff --git a/sites/docs/lib/_sass/components/_collapsible.scss b/sites/docs/lib/_sass/components/_collapsible.scss
deleted file mode 100644
index c29cff727a1..00000000000
--- a/sites/docs/lib/_sass/components/_collapsible.scss
+++ /dev/null
@@ -1,21 +0,0 @@
-.collapsible-section {
-  .collapsible-button {
-    background-color: #eee;
-    color: #444;
-    cursor: pointer;
-    padding: 18px;
-    width: 100%;
-    border: none;
-    text-align: left;
-    outline: none;
-    font-size: 15px;
-  }
-
-  .collapsible-content {
-    display: none;
-
-    &.show {
-      display: block;
-    }
-  }
-}
diff --git a/sites/docs/lib/_sass/components/_header.scss b/sites/docs/lib/_sass/components/_header.scss
index ad24c34f596..2370c1e21e3 100644
--- a/sites/docs/lib/_sass/components/_header.scss
+++ b/sites/docs/lib/_sass/components/_header.scss
@@ -136,32 +136,6 @@
   }
 }
 
-body:not(.sidenav-closed) #menu-toggle {
-  @media (min-width: 1024px) {
-    display: none;
-  }
-}
-
-// Toggle between menu and close buttons if sidenav is open or not.
-#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;
-  }
-}
 
 #site-primary-logo {
   text-decoration: none;
diff --git a/sites/docs/lib/_sass/components/_misc.scss b/sites/docs/lib/_sass/components/_misc.scss
index 9eb5f6ce177..466af21a524 100644
--- a/sites/docs/lib/_sass/components/_misc.scss
+++ b/sites/docs/lib/_sass/components/_misc.scss
@@ -29,66 +29,3 @@
     transform: translateX(25rem);
   }
 }
-
-// A big button centered on top of an image.
-//
-// The container is typically a 
element. The div has only 2 children: -// an element (the background) and a button. -// -// The image should be very bright and slightly blurred so that the button -// stands out. -.juicy-button-container { - position: relative; - padding: 2em 0; - - img { - width: 100%; - height: auto; - margin-bottom: 0; - } - - .filled-button { - position: absolute; - top: 50%; - left: 50%; - transform: translate(-50%, -50%); - white-space: nowrap; - - // Add a white "glow" that separates the button from the background. - box-shadow: 0 0 10px 10px white; - - // Make the button extra large. - @media (min-width: 576px) { - font-size: 150%; - padding: 0.7rem; - - .material-symbols { - font-size: 120%; - } - } - - @media (min-width: 900px) { - font-size: 200%; - padding: 1rem; - - .material-symbols { - font-size: 140%; - } - } - } -} - -.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/_next-prev-nav.scss b/sites/docs/lib/_sass/components/_next-prev-nav.scss index ce2bde478b8..1897e453f13 100644 --- a/sites/docs/lib/_sass/components/_next-prev-nav.scss +++ b/sites/docs/lib/_sass/components/_next-prev-nav.scss @@ -1,4 +1,4 @@ -@use '../base/mixins'; +@use 'package:site_shared/_sass/base/mixins'; #site-prev-next { display: flex; diff --git a/sites/docs/lib/_sass/components/_pagenav.scss b/sites/docs/lib/_sass/components/_pagenav.scss index fe80f981959..ca5136e00c8 100644 --- a/sites/docs/lib/_sass/components/_pagenav.scss +++ b/sites/docs/lib/_sass/components/_pagenav.scss @@ -194,4 +194,4 @@ transition: background-color 300ms ease, color 300ms ease; } -} \ No newline at end of file +} diff --git a/sites/docs/lib/_sass/components/_sidebar.scss b/sites/docs/lib/_sass/components/_sidebar.scss index db57724e097..ffa324a545c 100644 --- a/sites/docs/lib/_sass/components/_sidebar.scss +++ b/sites/docs/lib/_sass/components/_sidebar.scss @@ -1,4 +1,4 @@ -@use '../base/mixins'; +@use 'package:site_shared/_sass/base/mixins'; #sidenav { margin: 0; diff --git a/sites/docs/lib/_sass/components/_tutorial_pages.scss b/sites/docs/lib/_sass/components/_tutorial_pages.scss index b7c93040e1d..ba84d997ae7 100644 --- a/sites/docs/lib/_sass/components/_tutorial_pages.scss +++ b/sites/docs/lib/_sass/components/_tutorial_pages.scss @@ -36,11 +36,11 @@ } } -// Images are centered and have a +// Images are centered and have a // max width of 500px within the /learn directory .learn-image-wrapper { - display: block; - margin: 0 auto; - max-width: 500px; - width: 100%; - } \ No newline at end of file + display: block; + margin: 0 auto; + max-width: 500px; + width: 100%; +} diff --git a/sites/docs/lib/_sass/pages/_glossary.scss b/sites/docs/lib/_sass/pages/_glossary.scss index 74fbe7ad353..d7ab79e0672 100644 --- a/sites/docs/lib/_sass/pages/_glossary.scss +++ b/sites/docs/lib/_sass/pages/_glossary.scss @@ -1,4 +1,4 @@ -@use '../base/mixins'; +@use 'package:site_shared/_sass/base/mixins'; body.glossary-page main { .glossary-card { diff --git a/sites/docs/lib/_sass/pages/_learning-resources-index.scss b/sites/docs/lib/_sass/pages/_learning-resources-index.scss index d19ecc8a80f..3527fa4ca80 100644 --- a/sites/docs/lib/_sass/pages/_learning-resources-index.scss +++ b/sites/docs/lib/_sass/pages/_learning-resources-index.scss @@ -1,4 +1,4 @@ -@use '../base/mixins'; +@use 'package:site_shared/_sass/base/mixins'; #resource-filter-group-wrapper { border: 1px solid var(--site-inset-borderColor); diff --git a/sites/docs/lib/_sass/pages/_search.scss b/sites/docs/lib/_sass/pages/_search.scss index 38680fdcb73..ac566d19f3d 100644 --- a/sites/docs/lib/_sass/pages/_search.scss +++ b/sites/docs/lib/_sass/pages/_search.scss @@ -1,4 +1,4 @@ -@use '../base/mixins'; +@use 'package:site_shared/_sass/base/mixins'; #search-body { margin-block-start: 1.5rem; diff --git a/sites/docs/lib/builders.dart b/sites/docs/lib/builders.dart deleted file mode 100644 index e5605d1cbae..00000000000 --- a/sites/docs/lib/builders.dart +++ /dev/null @@ -1,9 +0,0 @@ -// 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:build/build.dart'; - -import 'src/builders/styles_hash_builder.dart' show StylesHashBuilder; - -Builder stylesHashBuilder(BuilderOptions options) => StylesHashBuilder(options); diff --git a/sites/docs/lib/main.client.options.dart b/sites/docs/lib/main.client.options.dart index 6ebbe20949f..c781ee279b5 100644 --- a/sites/docs/lib/main.client.options.dart +++ b/sites/docs/lib/main.client.options.dart @@ -6,36 +6,12 @@ import 'package:jaspr/client.dart'; -import 'package:docs_flutter_dev_site/src/components/common/client/collapse_button.dart' - deferred as _collapse_button; -import 'package:docs_flutter_dev_site/src/components/common/client/cookie_notice.dart' - deferred as _cookie_notice; -import 'package:docs_flutter_dev_site/src/components/common/client/copy_button.dart' - deferred as _copy_button; -import 'package:docs_flutter_dev_site/src/components/common/client/download_button.dart' - deferred as _download_button; import 'package:docs_flutter_dev_site/src/components/common/client/download_latest_button.dart' deferred as _download_latest_button; -import 'package:docs_flutter_dev_site/src/components/common/client/feedback.dart' - deferred as _feedback; -import 'package:docs_flutter_dev_site/src/components/common/client/on_this_page_button.dart' - deferred as _on_this_page_button; import 'package:docs_flutter_dev_site/src/components/common/client/os_selector.dart' deferred as _os_selector; -import 'package:docs_flutter_dev_site/src/components/common/client/page_header_options.dart' - deferred as _page_header_options; -import 'package:docs_flutter_dev_site/src/components/common/client/simple_tooltip.dart' - deferred as _simple_tooltip; -import 'package:docs_flutter_dev_site/src/components/dartpad/dartpad_injector.dart' - deferred as _dartpad_injector; import 'package:docs_flutter_dev_site/src/components/layout/client/pagenav.dart' deferred as _pagenav; -import 'package:docs_flutter_dev_site/src/components/layout/menu_toggle.dart' - deferred as _menu_toggle; -import 'package:docs_flutter_dev_site/src/components/layout/site_switcher.dart' - deferred as _site_switcher; -import 'package:docs_flutter_dev_site/src/components/layout/theme_switcher.dart' - deferred as _theme_switcher; import 'package:docs_flutter_dev_site/src/components/pages/archive_table.dart' deferred as _archive_table; import 'package:docs_flutter_dev_site/src/components/pages/glossary_search_section.dart' @@ -44,12 +20,36 @@ import 'package:docs_flutter_dev_site/src/components/pages/learning_resource_fil deferred as _learning_resource_filters; import 'package:docs_flutter_dev_site/src/components/pages/learning_resource_filters_sidebar.dart' deferred as _learning_resource_filters_sidebar; -import 'package:docs_flutter_dev_site/src/components/tutorial/client/quiz.dart' +import 'package:site_shared/components/common/client/collapse_button.dart' + deferred as _collapse_button; +import 'package:site_shared/components/common/client/cookie_notice.dart' + deferred as _cookie_notice; +import 'package:site_shared/components/common/client/copy_button.dart' + deferred as _copy_button; +import 'package:site_shared/components/common/client/download_button.dart' + deferred as _download_button; +import 'package:site_shared/components/common/client/feedback.dart' + deferred as _feedback; +import 'package:site_shared/components/common/client/on_this_page_button.dart' + deferred as _on_this_page_button; +import 'package:site_shared/components/common/client/page_header_options.dart' + deferred as _page_header_options; +import 'package:site_shared/components/common/client/simple_tooltip.dart' + deferred as _simple_tooltip; +import 'package:site_shared/components/dartpad/dartpad_injector.dart' + deferred as _dartpad_injector; +import 'package:site_shared/components/layout/menu_toggle.dart' + deferred as _menu_toggle; +import 'package:site_shared/components/layout/site_switcher.dart' + deferred as _site_switcher; +import 'package:site_shared/components/layout/theme_switcher.dart' + deferred as _theme_switcher; +import 'package:site_shared/components/tutorial/client/quiz.dart' deferred as _quiz; -import 'package:docs_flutter_dev_site/src/components/util/component_ref.dart' - as _component_ref; -import 'package:docs_flutter_dev_site/src/models/quiz_model.dart' +import 'package:site_shared/components/tutorial/models/quiz_model.dart' as _quiz_model; +import 'package:site_shared/components/utils/component_ref.dart' + as _component_ref; /// Default [ClientOptions] for use with your Jaspr project. /// @@ -69,49 +69,82 @@ import 'package:docs_flutter_dev_site/src/models/quiz_model.dart' /// ``` ClientOptions get defaultClientOptions => ClientOptions( clients: { - 'collapse_button': ClientLoader( + 'download_latest_button': ClientLoader( + (p) => _download_latest_button.DownloadLatestButton( + os: p['os'] as String, + arch: p['arch'] as String?, + ), + loader: _download_latest_button.loadLibrary, + ), + 'os_selector': ClientLoader( + (p) => _os_selector.OsSelector(), + loader: _os_selector.loadLibrary, + ), + 'pagenav': ClientLoader( + (p) => _pagenav.PageNav( + breadcrumbs: (p['breadcrumbs'] as List).cast(), + pageNumber: p['pageNumber'] as int?, + initialHeading: p['initialHeading'] as String, + content: _component_ref.ComponentRef.fromId(p['content'] as String), + ), + loader: _pagenav.loadLibrary, + ), + 'archive_table': ClientLoader( + (p) => _archive_table.ArchiveTable( + os: p['os'] as String, + channel: p['channel'] as String, + ), + loader: _archive_table.loadLibrary, + ), + 'glossary_search_section': ClientLoader( + (p) => _glossary_search_section.GlossarySearchSection(), + loader: _glossary_search_section.loadLibrary, + ), + 'learning_resource_filters': ClientLoader( + (p) => _learning_resource_filters.LearningResourceFilters(), + loader: _learning_resource_filters.loadLibrary, + ), + 'learning_resource_filters_sidebar': ClientLoader( + (p) => + _learning_resource_filters_sidebar.LearningResourceFiltersSidebar(), + loader: _learning_resource_filters_sidebar.loadLibrary, + ), + 'site_shared:collapse_button': ClientLoader( (p) => _collapse_button.CollapseButton( classes: (p['classes'] as List).cast(), title: p['title'] as String?, ), loader: _collapse_button.loadLibrary, ), - 'cookie_notice': ClientLoader( - (p) => _cookie_notice.CookieNotice(), + 'site_shared:cookie_notice': ClientLoader( + (p) => _cookie_notice.CookieNotice( + host: p['host'] as String, + alwaysDarkMode: p['alwaysDarkMode'] as bool, + ), loader: _cookie_notice.loadLibrary, ), - 'copy_button': ClientLoader( + 'site_shared:copy_button': ClientLoader( (p) => _copy_button.CopyButton( buttonText: p['buttonText'] as String?, + toCopy: p['toCopy'] as String?, classes: (p['classes'] as List).cast(), title: p['title'] as String?, ), loader: _copy_button.loadLibrary, ), - 'download_button': ClientLoader( + 'site_shared:download_button': ClientLoader( (p) => _download_button.DownloadButton(name: p['name'] as String), loader: _download_button.loadLibrary, ), - 'download_latest_button': ClientLoader( - (p) => _download_latest_button.DownloadLatestButton( - os: p['os'] as String, - arch: p['arch'] as String?, - ), - loader: _download_latest_button.loadLibrary, - ), - 'feedback': ClientLoader( + 'site_shared:feedback': ClientLoader( (p) => _feedback.FeedbackComponent(issueUrl: p['issueUrl'] as String), loader: _feedback.loadLibrary, ), - 'on_this_page_button': ClientLoader( + 'site_shared:on_this_page_button': ClientLoader( (p) => _on_this_page_button.OnThisPageButton(), loader: _on_this_page_button.loadLibrary, ), - 'os_selector': ClientLoader( - (p) => _os_selector.OsSelector(), - loader: _os_selector.loadLibrary, - ), - 'page_header_options': ClientLoader( + 'site_shared:page_header_options': ClientLoader( (p) => _page_header_options.PageHeaderOptions( title: p['title'] as String, sourceUrl: p['sourceUrl'] as String?, @@ -119,14 +152,14 @@ ClientOptions get defaultClientOptions => ClientOptions( ), loader: _page_header_options.loadLibrary, ), - 'simple_tooltip': ClientLoader( + 'site_shared:simple_tooltip': ClientLoader( (p) => _simple_tooltip.SimpleTooltip( target: _component_ref.ComponentRef.fromId(p['target'] as String), content: _component_ref.ComponentRef.fromId(p['content'] as String), ), loader: _simple_tooltip.loadLibrary, ), - 'dartpad_injector': ClientLoader( + 'site_shared:dartpad_injector': ClientLoader( (p) => _dartpad_injector.DartPadInjector( title: p['title'] as String, theme: p['theme'] as String?, @@ -135,48 +168,19 @@ ClientOptions get defaultClientOptions => ClientOptions( ), loader: _dartpad_injector.loadLibrary, ), - 'pagenav': ClientLoader( - (p) => _pagenav.PageNav( - breadcrumbs: (p['breadcrumbs'] as List).cast(), - pageNumber: p['pageNumber'] as int?, - initialHeading: p['initialHeading'] as String, - content: _component_ref.ComponentRef.fromId(p['content'] as String), - ), - loader: _pagenav.loadLibrary, - ), - 'menu_toggle': ClientLoader( + 'site_shared:menu_toggle': ClientLoader( (p) => _menu_toggle.MenuToggle(), loader: _menu_toggle.loadLibrary, ), - 'site_switcher': ClientLoader( - (p) => _site_switcher.SiteSwitcher(), + 'site_shared:site_switcher': ClientLoader( + (p) => _site_switcher.SiteSwitcher(isFlutter: p['isFlutter'] as bool), loader: _site_switcher.loadLibrary, ), - 'theme_switcher': ClientLoader( + 'site_shared:theme_switcher': ClientLoader( (p) => _theme_switcher.ThemeSwitcher(), loader: _theme_switcher.loadLibrary, ), - 'archive_table': ClientLoader( - (p) => _archive_table.ArchiveTable( - os: p['os'] as String, - channel: p['channel'] as String, - ), - loader: _archive_table.loadLibrary, - ), - 'glossary_search_section': ClientLoader( - (p) => _glossary_search_section.GlossarySearchSection(), - loader: _glossary_search_section.loadLibrary, - ), - 'learning_resource_filters': ClientLoader( - (p) => _learning_resource_filters.LearningResourceFilters(), - loader: _learning_resource_filters.loadLibrary, - ), - 'learning_resource_filters_sidebar': ClientLoader( - (p) => - _learning_resource_filters_sidebar.LearningResourceFiltersSidebar(), - loader: _learning_resource_filters_sidebar.loadLibrary, - ), - 'quiz': ClientLoader( + 'site_shared:quiz': ClientLoader( (p) => _quiz.InteractiveQuiz( title: p['title'] as String?, questions: (p['questions'] as List) diff --git a/sites/docs/lib/main.server.dart b/sites/docs/lib/main.server.dart index 2c897e0b8d0..23e8bbff793 100644 --- a/sites/docs/lib/main.server.dart +++ b/sites/docs/lib/main.server.dart @@ -7,16 +7,24 @@ import 'package:jaspr_content/components/file_tree.dart'; import 'package:jaspr_content/jaspr_content.dart'; import 'package:jaspr_content/theme.dart'; import 'package:path/path.dart' as path; +import 'package:site_shared/components/common/card.dart'; +import 'package:site_shared/components/common/material_icon.dart'; +import 'package:site_shared/components/common/tabs.dart'; +import 'package:site_shared/components/common/youtube_embed.dart'; +import 'package:site_shared/components/tutorial/downloadable_snippet.dart'; +import 'package:site_shared/components/tutorial/progress_ring.dart'; +import 'package:site_shared/components/tutorial/quiz.dart'; +import 'package:site_shared/components/tutorial/stepper.dart'; +import 'package:site_shared/components/tutorial/summary_card.dart'; +import 'package:site_shared/components/tutorial/tutorial_outline.dart'; +import 'package:site_shared/components/utils/define_component.dart'; +import 'package:site_shared/markdown.dart'; import 'main.server.options.dart'; // Generated. Do not remove or edit. -import 'src/components/common/card.dart'; import 'src/components/common/client/download_latest_button.dart'; import 'src/components/common/client/os_selector.dart'; import 'src/components/common/code_preview.dart'; import 'src/components/common/dash_image.dart'; -import 'src/components/common/material_icon.dart'; -import 'src/components/common/tabs.dart'; -import 'src/components/common/youtube_embed.dart'; import 'src/components/pages/architecture_recommendations.dart'; import 'src/components/pages/archive_table.dart'; import 'src/components/pages/devtools_release_notes_index.dart'; @@ -24,48 +32,39 @@ import 'src/components/pages/expansion_list.dart'; import 'src/components/pages/learning_resource_index.dart'; import 'src/components/pages/platforms_grid.dart'; import 'src/components/pages/widget_catalog.dart'; -import 'src/components/tutorial/downloadable_snippet.dart'; -import 'src/components/tutorial/progress_ring.dart'; -import 'src/components/tutorial/quiz.dart'; -import 'src/components/tutorial/stepper.dart'; -import 'src/components/tutorial/summary_card.dart'; -import 'src/components/tutorial/tutorial_outline.dart'; -import 'src/components/util/component_ref.dart'; import 'src/extensions/registry.dart'; import 'src/layouts/doc_layout.dart'; import 'src/layouts/toc_layout.dart'; import 'src/layouts/tutorial_layout.dart'; import 'src/loaders/data_processor.dart'; -import 'src/markdown/markdown_parser.dart'; import 'src/pages/custom_pages.dart'; import 'src/pages/robots_txt.dart'; import 'src/templating/dash_template_engine.dart'; -import 'src/util.dart'; void main() { // Initializes the server environment with the generated default options. Jaspr.initializeApp(options: defaultServerOptions); - runApp(ComponentRefScope(child: _docsFlutterDevSite)); + runApp(_docsFlutterDevSite); } Component get _docsFlutterDevSite => ContentApp.custom( eagerlyLoadAllPages: true, loaders: [ FilesystemLoader( - path.join(siteSrcDirectoryPath, 'content'), + path.join(_siteSrcDirectoryPath, 'content'), keepSuffixPattern: _passThroughPattern, ), MemoryLoader(pages: allMemoryPages), ], configResolver: PageConfig.all( dataLoaders: [ - FilesystemDataLoader(path.join(siteSrcDirectoryPath, 'data')), + FilesystemDataLoader(path.join(_siteSrcDirectoryPath, 'data')), DataProcessor(), ], templateEngine: DashTemplateEngine( partialDirectoryPath: path.canonicalize( - path.join(siteSrcDirectoryPath, '_includes'), + path.join(_siteSrcDirectoryPath, '_includes'), ), ), parsers: const [ @@ -104,6 +103,8 @@ Component get _docsFlutterDevSite => ContentApp.custom( ), ); +/// Path to the `/src` directory where site content is located. +const String _siteSrcDirectoryPath = 'src'; final RegExp _passThroughPattern = RegExp(r'.*\.(txt|json|pdf)$'); /// Custom "components" that can be used from Markdown files. @@ -116,7 +117,9 @@ List get _embeddableComponents => [ const Quiz(), const ProgressRing(), const SummaryCard(), - const DownloadableSnippet(), + DownloadableSnippet( + snippetsDirectoryPath: path.join(_siteSrcDirectoryPath, '_snippets'), + ), const Stepper(), const WidgetCatalogCategories(), const TutorialOutline(), @@ -124,36 +127,18 @@ List get _embeddableComponents => [ const ArchitectureRecommendations(), const PlatformsGrid(), const PlatformCard(), - CustomComponent( - pattern: RegExp('Icon', caseSensitive: false), - builder: (_, attrs, _) => MaterialIcon.fromAttributes(attrs), - ), - CustomComponent( - pattern: RegExp('OSSelector', caseSensitive: false), - builder: (_, _, _) => const OsSelector(), - ), - CustomComponent( - pattern: RegExp('Card', caseSensitive: false), - builder: (_, attrs, child) => Card.fromAttributes(attrs, child), - ), - CustomComponent( - pattern: RegExp('LearningResourceIndex', caseSensitive: false), - builder: (_, _, _) => LearningResourceIndex(), - ), - CustomComponent( - pattern: RegExp('ArchiveTable'), - builder: (_, attrs, _) => ArchiveTable.fromAttributes(attrs), - ), - CustomComponent( - pattern: RegExp('DownloadLatestButton', caseSensitive: false), - builder: (_, attrs, _) => DownloadLatestButton.fromAttributes(attrs), - ), - CustomComponent( - pattern: RegExp('ExpansionList', caseSensitive: false), - builder: (_, attrs, _) => ExpansionList.fromAttributes(attrs), + defineComponentWithAttrs('Icon', MaterialIcon.fromAttributes), + defineComponent('OSSelector', const OsSelector()), + defineComponentWithChild('Card', Card.fromAttributes), + defineComponent('LearningResourceIndex', const LearningResourceIndex()), + defineComponentWithAttrs('ArchiveTable', ArchiveTable.fromAttributes), + defineComponentWithAttrs( + 'DownloadLatestButton', + DownloadLatestButton.fromAttributes, ), - CustomComponent( - pattern: RegExp('DevToolsReleaseNotesIndex', caseSensitive: false), - builder: (_, _, _) => const DevToolsReleaseNotesIndex(), + defineComponentWithAttrs('ExpansionList', ExpansionList.fromAttributes), + defineComponent( + 'DevToolsReleaseNotesIndex', + const DevToolsReleaseNotesIndex(), ), ]; diff --git a/sites/docs/lib/main.server.options.dart b/sites/docs/lib/main.server.options.dart index 6262d37d117..70381901f68 100644 --- a/sites/docs/lib/main.server.options.dart +++ b/sites/docs/lib/main.server.options.dart @@ -5,36 +5,12 @@ // Generated with jaspr_builder import 'package:jaspr/server.dart'; -import 'package:docs_flutter_dev_site/src/components/common/client/collapse_button.dart' - as _collapse_button; -import 'package:docs_flutter_dev_site/src/components/common/client/cookie_notice.dart' - as _cookie_notice; -import 'package:docs_flutter_dev_site/src/components/common/client/copy_button.dart' - as _copy_button; -import 'package:docs_flutter_dev_site/src/components/common/client/download_button.dart' - as _download_button; import 'package:docs_flutter_dev_site/src/components/common/client/download_latest_button.dart' as _download_latest_button; -import 'package:docs_flutter_dev_site/src/components/common/client/feedback.dart' - as _feedback; -import 'package:docs_flutter_dev_site/src/components/common/client/on_this_page_button.dart' - as _on_this_page_button; import 'package:docs_flutter_dev_site/src/components/common/client/os_selector.dart' as _os_selector; -import 'package:docs_flutter_dev_site/src/components/common/client/page_header_options.dart' - as _page_header_options; -import 'package:docs_flutter_dev_site/src/components/common/client/simple_tooltip.dart' - as _simple_tooltip; -import 'package:docs_flutter_dev_site/src/components/dartpad/dartpad_injector.dart' - as _dartpad_injector; import 'package:docs_flutter_dev_site/src/components/layout/client/pagenav.dart' as _pagenav; -import 'package:docs_flutter_dev_site/src/components/layout/menu_toggle.dart' - as _menu_toggle; -import 'package:docs_flutter_dev_site/src/components/layout/site_switcher.dart' - as _site_switcher; -import 'package:docs_flutter_dev_site/src/components/layout/theme_switcher.dart' - as _theme_switcher; import 'package:docs_flutter_dev_site/src/components/pages/archive_table.dart' as _archive_table; import 'package:docs_flutter_dev_site/src/components/pages/glossary_search_section.dart' @@ -43,9 +19,31 @@ import 'package:docs_flutter_dev_site/src/components/pages/learning_resource_fil as _learning_resource_filters; import 'package:docs_flutter_dev_site/src/components/pages/learning_resource_filters_sidebar.dart' as _learning_resource_filters_sidebar; -import 'package:docs_flutter_dev_site/src/components/tutorial/client/quiz.dart' - as _quiz; import 'package:jaspr_content/components/file_tree.dart' as _file_tree; +import 'package:site_shared/components/common/client/collapse_button.dart' + as _collapse_button; +import 'package:site_shared/components/common/client/cookie_notice.dart' + as _cookie_notice; +import 'package:site_shared/components/common/client/copy_button.dart' + as _copy_button; +import 'package:site_shared/components/common/client/download_button.dart' + as _download_button; +import 'package:site_shared/components/common/client/feedback.dart' + as _feedback; +import 'package:site_shared/components/common/client/on_this_page_button.dart' + as _on_this_page_button; +import 'package:site_shared/components/common/client/page_header_options.dart' + as _page_header_options; +import 'package:site_shared/components/common/client/simple_tooltip.dart' + as _simple_tooltip; +import 'package:site_shared/components/dartpad/dartpad_injector.dart' + as _dartpad_injector; +import 'package:site_shared/components/layout/menu_toggle.dart' as _menu_toggle; +import 'package:site_shared/components/layout/site_switcher.dart' + as _site_switcher; +import 'package:site_shared/components/layout/theme_switcher.dart' + as _theme_switcher; +import 'package:site_shared/components/tutorial/client/quiz.dart' as _quiz; /// Default [ServerOptions] for use with your Jaspr project. /// @@ -66,104 +64,119 @@ import 'package:jaspr_content/components/file_tree.dart' as _file_tree; ServerOptions get defaultServerOptions => ServerOptions( clientId: 'main.client.dart.js', clients: { + _download_latest_button.DownloadLatestButton: + ClientTarget<_download_latest_button.DownloadLatestButton>( + 'download_latest_button', + params: __download_latest_buttonDownloadLatestButton, + ), + _os_selector.OsSelector: ClientTarget<_os_selector.OsSelector>( + 'os_selector', + ), + _pagenav.PageNav: ClientTarget<_pagenav.PageNav>( + 'pagenav', + params: __pagenavPageNav, + ), + _archive_table.ArchiveTable: ClientTarget<_archive_table.ArchiveTable>( + 'archive_table', + params: __archive_tableArchiveTable, + ), + _glossary_search_section.GlossarySearchSection: + ClientTarget<_glossary_search_section.GlossarySearchSection>( + 'glossary_search_section', + ), + _learning_resource_filters.LearningResourceFilters: + ClientTarget<_learning_resource_filters.LearningResourceFilters>( + 'learning_resource_filters', + ), + _learning_resource_filters_sidebar.LearningResourceFiltersSidebar: + ClientTarget< + _learning_resource_filters_sidebar.LearningResourceFiltersSidebar + >('learning_resource_filters_sidebar'), _collapse_button.CollapseButton: ClientTarget<_collapse_button.CollapseButton>( - 'collapse_button', + 'site_shared:collapse_button', params: __collapse_buttonCollapseButton, ), _cookie_notice.CookieNotice: ClientTarget<_cookie_notice.CookieNotice>( - 'cookie_notice', + 'site_shared:cookie_notice', + params: __cookie_noticeCookieNotice, ), _copy_button.CopyButton: ClientTarget<_copy_button.CopyButton>( - 'copy_button', + 'site_shared:copy_button', params: __copy_buttonCopyButton, ), _download_button.DownloadButton: ClientTarget<_download_button.DownloadButton>( - 'download_button', + 'site_shared:download_button', params: __download_buttonDownloadButton, ), - _download_latest_button.DownloadLatestButton: - ClientTarget<_download_latest_button.DownloadLatestButton>( - 'download_latest_button', - params: __download_latest_buttonDownloadLatestButton, - ), _feedback.FeedbackComponent: ClientTarget<_feedback.FeedbackComponent>( - 'feedback', + 'site_shared:feedback', params: __feedbackFeedbackComponent, ), _on_this_page_button.OnThisPageButton: ClientTarget<_on_this_page_button.OnThisPageButton>( - 'on_this_page_button', + 'site_shared:on_this_page_button', ), - _os_selector.OsSelector: ClientTarget<_os_selector.OsSelector>( - 'os_selector', - ), _page_header_options.PageHeaderOptions: ClientTarget<_page_header_options.PageHeaderOptions>( - 'page_header_options', + 'site_shared:page_header_options', params: __page_header_optionsPageHeaderOptions, ), _simple_tooltip.SimpleTooltip: ClientTarget<_simple_tooltip.SimpleTooltip>( - 'simple_tooltip', + 'site_shared:simple_tooltip', params: __simple_tooltipSimpleTooltip, ), _dartpad_injector.DartPadInjector: ClientTarget<_dartpad_injector.DartPadInjector>( - 'dartpad_injector', + 'site_shared:dartpad_injector', params: __dartpad_injectorDartPadInjector, ), - _pagenav.PageNav: ClientTarget<_pagenav.PageNav>( - 'pagenav', - params: __pagenavPageNav, - ), _menu_toggle.MenuToggle: ClientTarget<_menu_toggle.MenuToggle>( - 'menu_toggle', + 'site_shared:menu_toggle', ), _site_switcher.SiteSwitcher: ClientTarget<_site_switcher.SiteSwitcher>( - 'site_switcher', + 'site_shared:site_switcher', + params: __site_switcherSiteSwitcher, ), _theme_switcher.ThemeSwitcher: ClientTarget<_theme_switcher.ThemeSwitcher>( - 'theme_switcher', - ), - _archive_table.ArchiveTable: ClientTarget<_archive_table.ArchiveTable>( - 'archive_table', - params: __archive_tableArchiveTable, + 'site_shared:theme_switcher', ), - _glossary_search_section.GlossarySearchSection: - ClientTarget<_glossary_search_section.GlossarySearchSection>( - 'glossary_search_section', - ), - _learning_resource_filters.LearningResourceFilters: - ClientTarget<_learning_resource_filters.LearningResourceFilters>( - 'learning_resource_filters', - ), - _learning_resource_filters_sidebar.LearningResourceFiltersSidebar: - ClientTarget< - _learning_resource_filters_sidebar.LearningResourceFiltersSidebar - >('learning_resource_filters_sidebar'), _quiz.InteractiveQuiz: ClientTarget<_quiz.InteractiveQuiz>( - 'quiz', + 'site_shared:quiz', params: __quizInteractiveQuiz, ), }, styles: () => [..._file_tree.FileTree.styles], ); +Map __download_latest_buttonDownloadLatestButton( + _download_latest_button.DownloadLatestButton c, +) => {'os': c.os, 'arch': c.arch}; +Map __pagenavPageNav(_pagenav.PageNav c) => { + 'breadcrumbs': c.breadcrumbs, + 'pageNumber': c.pageNumber, + 'initialHeading': c.initialHeading, + 'content': c.content.toId(), +}; +Map __archive_tableArchiveTable( + _archive_table.ArchiveTable c, +) => {'os': c.os, 'channel': c.channel}; Map __collapse_buttonCollapseButton( _collapse_button.CollapseButton c, ) => {'classes': c.classes, 'title': c.title}; +Map __cookie_noticeCookieNotice( + _cookie_notice.CookieNotice c, +) => {'host': c.host, 'alwaysDarkMode': c.alwaysDarkMode}; Map __copy_buttonCopyButton(_copy_button.CopyButton c) => { 'buttonText': c.buttonText, + 'toCopy': c.toCopy, 'classes': c.classes, 'title': c.title, }; Map __download_buttonDownloadButton( _download_button.DownloadButton c, ) => {'name': c.name}; -Map __download_latest_buttonDownloadLatestButton( - _download_latest_button.DownloadLatestButton c, -) => {'os': c.os, 'arch': c.arch}; Map __feedbackFeedbackComponent( _feedback.FeedbackComponent c, ) => {'issueUrl': c.issueUrl}; @@ -181,15 +194,9 @@ Map __dartpad_injectorDartPadInjector( 'height': c.height, 'runAutomatically': c.runAutomatically, }; -Map __pagenavPageNav(_pagenav.PageNav c) => { - 'breadcrumbs': c.breadcrumbs, - 'pageNumber': c.pageNumber, - 'initialHeading': c.initialHeading, - 'content': c.content.toId(), -}; -Map __archive_tableArchiveTable( - _archive_table.ArchiveTable c, -) => {'os': c.os, 'channel': c.channel}; +Map __site_switcherSiteSwitcher( + _site_switcher.SiteSwitcher c, +) => {'isFlutter': c.isFlutter}; Map __quizInteractiveQuiz(_quiz.InteractiveQuiz c) => { 'title': c.title, 'questions': c.questions.map((i) => i.toJson()).toList(), diff --git a/sites/docs/lib/src/client/global_scripts.dart b/sites/docs/lib/src/client/global_scripts.dart index fe15df697be..d29ebc2c9d3 100644 --- a/sites/docs/lib/src/client/global_scripts.dart +++ b/sites/docs/lib/src/client/global_scripts.dart @@ -3,11 +3,10 @@ // found in the LICENSE file. import 'package:jaspr/jaspr.dart'; +import 'package:site_shared/web_util.dart'; import 'package:universal_web/js_interop.dart'; import 'package:universal_web/web.dart' as web; -import '../util.dart'; - /// Global scripts converted from JS. /// /// These are temporary until they can be integrated with their @@ -132,12 +131,12 @@ void _setUpTabs() { // If this tab wrapper is for the archive page, // and no tab was retrieved from local storage, // switch to the tab for the current OS. - var currentOperatingSystem = getOS(); + var currentOperatingSystem = browserOperatingSystem; if (currentOperatingSystem == null) { - currentOperatingSystem = OperatingSystem.windows; - } else if (currentOperatingSystem == OperatingSystem.chromeos) { + currentOperatingSystem = .windows; + } else if (currentOperatingSystem == .chromeOS) { // ChromeOS uses the Linux tab. - currentOperatingSystem = OperatingSystem.linux; + currentOperatingSystem = .linux; } _activateTabWithSaveId(element, currentOperatingSystem.name); @@ -294,10 +293,10 @@ void _setUpExpandableCards() { } void _setUpPlatformKeys() { - final os = getOS(); + final os = browserOperatingSystem; // Use Command key for macOS, Control key for other OS. final specialKey = switch (os) { - OperatingSystem.macos => 'Command', + .macOS => 'Command', _ => 'Control', }; final keys = web.document.querySelectorAll('kbd.special-key'); diff --git a/sites/docs/lib/src/components/common/client/os_selector.dart b/sites/docs/lib/src/components/common/client/os_selector.dart index e5e6fb8c1d1..2a82e6a8eba 100644 --- a/sites/docs/lib/src/components/common/client/os_selector.dart +++ b/sites/docs/lib/src/components/common/client/os_selector.dart @@ -4,10 +4,10 @@ import 'package:jaspr/dom.dart'; import 'package:jaspr/jaspr.dart'; +import 'package:site_shared/util.dart'; +import 'package:site_shared/web_util.dart'; import 'package:universal_web/web.dart' as web; -import '../../../util.dart'; - @client class OsSelector extends StatefulComponent { const OsSelector({super.key}); @@ -27,7 +27,7 @@ class _OsSelectorState extends State { super.initState(); if (kIsWeb) { - final currentOs = getOS() ?? OperatingSystem.windows; + final currentOs = browserOperatingSystem ?? OperatingSystem.windows; setOS(currentOs); } } @@ -74,7 +74,7 @@ class _OsSelectorState extends State { [ div(classes: 'card-leading', [ img( - src: '/assets/images/docs/brand-svg/${os.name}.svg', + src: '/assets/images/docs/brand-svg/${os.id}.svg', alt: '${os.label} logo', attributes: { 'width': '72', diff --git a/sites/docs/lib/src/components/common/code_preview.dart b/sites/docs/lib/src/components/common/code_preview.dart index c4adcda0328..e3cdd26570a 100644 --- a/sites/docs/lib/src/components/common/code_preview.dart +++ b/sites/docs/lib/src/components/common/code_preview.dart @@ -5,9 +5,8 @@ import 'package:jaspr/dom.dart'; import 'package:jaspr/jaspr.dart'; import 'package:jaspr_content/jaspr_content.dart'; - -import '../../util.dart'; -import 'wrapped_code_block.dart'; +import 'package:site_shared/components/common/wrapped_code_block.dart'; +import 'package:site_shared/util.dart'; /// A component that displays a preview area alongside a code block. /// diff --git a/sites/docs/lib/src/components/common/dash_image.dart b/sites/docs/lib/src/components/common/dash_image.dart index f39aa586326..8dde006c617 100644 --- a/sites/docs/lib/src/components/common/dash_image.dart +++ b/sites/docs/lib/src/components/common/dash_image.dart @@ -5,8 +5,7 @@ import 'package:jaspr/dom.dart'; import 'package:jaspr/jaspr.dart'; import 'package:jaspr_content/jaspr_content.dart'; - -import '../../markdown/markdown_parser.dart'; +import 'package:site_shared/markdown.dart'; class DashImage with CustomComponentBase { const DashImage(); diff --git a/sites/docs/lib/src/components/common/page_header.dart b/sites/docs/lib/src/components/common/page_header.dart index 74178855586..257bc63ff14 100644 --- a/sites/docs/lib/src/components/common/page_header.dart +++ b/sites/docs/lib/src/components/common/page_header.dart @@ -5,12 +5,12 @@ import 'package:jaspr/dom.dart'; import 'package:jaspr/jaspr.dart'; import 'package:jaspr_content/jaspr_content.dart'; +import 'package:site_shared/components/common/breadcrumbs.dart'; +import 'package:site_shared/components/common/client/page_header_options.dart'; +import 'package:site_shared/markdown.dart'; +import 'package:site_shared/util.dart'; -import '../../markdown/markdown_parser.dart'; -import '../../util.dart'; import '../../utils/page_source_info.dart'; -import 'breadcrumbs.dart'; -import 'client/page_header_options.dart'; final class PageHeader extends StatelessComponent { const PageHeader({ diff --git a/sites/docs/lib/src/components/common/prev_next.dart b/sites/docs/lib/src/components/common/prev_next.dart index 5750e48a7d0..7104c7ca418 100644 --- a/sites/docs/lib/src/components/common/prev_next.dart +++ b/sites/docs/lib/src/components/common/prev_next.dart @@ -4,10 +4,10 @@ import 'package:jaspr/dom.dart'; import 'package:jaspr/jaspr.dart'; +import 'package:site_shared/components/common/material_icon.dart'; +import 'package:site_shared/markdown.dart'; -import '../../markdown/markdown_parser.dart'; import '../../models/page_navigation_model.dart'; -import 'material_icon.dart'; /// Previous and next page buttons to display at the end of a page /// in a connected series of pages, such as the language docs. diff --git a/sites/docs/lib/src/components/layout/client/pagenav.dart b/sites/docs/lib/src/components/layout/client/pagenav.dart index 49ce5b72307..5e770106a13 100644 --- a/sites/docs/lib/src/components/layout/client/pagenav.dart +++ b/sites/docs/lib/src/components/layout/client/pagenav.dart @@ -4,14 +4,14 @@ import 'package:jaspr/dom.dart'; import 'package:jaspr/jaspr.dart'; +import 'package:site_shared/components/common/dropdown.dart'; +import 'package:site_shared/components/common/material_icon.dart'; +import 'package:site_shared/components/utils/component_ref.dart'; +import 'package:site_shared/util.dart'; import 'package:universal_web/js_interop.dart'; import 'package:universal_web/web.dart' as web; import '../../../client/global_scripts.dart'; -import '../../../util.dart'; -import '../../common/dropdown.dart'; -import '../../common/material_icon.dart'; -import '../../util/component_ref.dart'; @client class PageNav extends StatefulComponent { @@ -121,7 +121,7 @@ class _PageNavState extends State { ]), ], ), - content: component.content.component, + content: component.content, ); } diff --git a/sites/docs/lib/src/components/layout/header.dart b/sites/docs/lib/src/components/layout/header.dart index 82bb08d26cf..dbe464d28e5 100644 --- a/sites/docs/lib/src/components/layout/header.dart +++ b/sites/docs/lib/src/components/layout/header.dart @@ -5,14 +5,14 @@ import 'package:jaspr/dom.dart'; import 'package:jaspr/jaspr.dart'; import 'package:jaspr_content/jaspr_content.dart'; +import 'package:site_shared/components/common/button.dart'; +import 'package:site_shared/components/common/material_icon.dart'; +import 'package:site_shared/components/layout/menu_toggle.dart'; +import 'package:site_shared/components/layout/site_switcher.dart'; +import 'package:site_shared/components/layout/theme_switcher.dart'; +import 'package:site_shared/util.dart'; -import '../../util.dart'; import '../../utils/active_nav.dart'; -import '../common/button.dart'; -import '../common/material_icon.dart'; -import 'menu_toggle.dart'; -import 'site_switcher.dart'; -import 'theme_switcher.dart'; /// The site-wide top navigation bar. class DashHeader extends StatelessComponent { diff --git a/sites/docs/lib/src/components/layout/sidenav.dart b/sites/docs/lib/src/components/layout/sidenav.dart index 17a6e3127f3..749ca93f340 100644 --- a/sites/docs/lib/src/components/layout/sidenav.dart +++ b/sites/docs/lib/src/components/layout/sidenav.dart @@ -5,11 +5,11 @@ import 'package:jaspr/dom.dart'; import 'package:jaspr/jaspr.dart'; import 'package:jaspr_content/jaspr_content.dart'; +import 'package:site_shared/components/common/material_icon.dart'; +import 'package:site_shared/util.dart'; import '../../models/sidenav_model.dart'; -import '../../util.dart'; import '../../utils/active_nav.dart'; -import '../common/material_icon.dart'; /// The site-wide side navigation menu, /// with entries loaded from files in the `src/data/sidenav` directory. diff --git a/sites/docs/lib/src/components/layout/toc.dart b/sites/docs/lib/src/components/layout/toc.dart index 07362c1b6c6..0cd4075f9d7 100644 --- a/sites/docs/lib/src/components/layout/toc.dart +++ b/sites/docs/lib/src/components/layout/toc.dart @@ -5,13 +5,13 @@ import 'package:jaspr/dom.dart'; import 'package:jaspr/jaspr.dart'; import 'package:jaspr_content/jaspr_content.dart'; +import 'package:site_shared/components/common/client/on_this_page_button.dart'; +import 'package:site_shared/components/common/material_icon.dart'; +import 'package:site_shared/components/utils/component_ref.dart'; +import 'package:site_shared/markdown.dart'; +import 'package:site_shared/util.dart'; -import '../../markdown/markdown_parser.dart'; import '../../models/page_navigation_model.dart'; -import '../../util.dart'; -import '../common/client/on_this_page_button.dart'; -import '../common/material_icon.dart'; -import '../util/component_ref.dart'; import 'client/pagenav.dart'; final class DashTableOfContents extends StatelessComponent { @@ -68,7 +68,7 @@ final class PageNavBar extends StatelessComponent { ], pageNumber: linkedPageTitle != null ? currentLinkedPageNumber : null, initialHeading: currentTitle, - content: context.ref( + content: ref( div([ if (data.pageEntries.isEmpty) ...[ a( diff --git a/sites/docs/lib/src/components/layout/trailing_content.dart b/sites/docs/lib/src/components/layout/trailing_content.dart index 39ec4ac558b..2efcfc836a8 100644 --- a/sites/docs/lib/src/components/layout/trailing_content.dart +++ b/sites/docs/lib/src/components/layout/trailing_content.dart @@ -5,9 +5,9 @@ import 'package:jaspr/dom.dart'; import 'package:jaspr/jaspr.dart'; import 'package:jaspr_content/jaspr_content.dart'; +import 'package:site_shared/components/common/client/feedback.dart'; import '../../utils/page_source_info.dart'; -import '../common/client/feedback.dart'; /// The trailing content of a content documentation page, such as /// its last updated information, report an issue links, and similar. diff --git a/sites/docs/lib/src/components/pages/architecture_recommendations.dart b/sites/docs/lib/src/components/pages/architecture_recommendations.dart index 5477bbc4a11..7ea371b7176 100644 --- a/sites/docs/lib/src/components/pages/architecture_recommendations.dart +++ b/sites/docs/lib/src/components/pages/architecture_recommendations.dart @@ -5,8 +5,7 @@ import 'package:jaspr/dom.dart'; import 'package:jaspr/jaspr.dart'; import 'package:jaspr_content/jaspr_content.dart'; - -import '../../markdown/markdown_parser.dart'; +import 'package:site_shared/markdown.dart'; class ArchitectureRecommendations extends CustomComponentBase { const ArchitectureRecommendations(); diff --git a/sites/docs/lib/src/components/pages/expansion_list.dart b/sites/docs/lib/src/components/pages/expansion_list.dart index 25aed291081..e46870ebaf7 100644 --- a/sites/docs/lib/src/components/pages/expansion_list.dart +++ b/sites/docs/lib/src/components/pages/expansion_list.dart @@ -7,9 +7,8 @@ import 'package:jaspr/dom.dart'; import 'package:jaspr/jaspr.dart'; import 'package:jaspr_content/jaspr_content.dart'; import 'package:path/path.dart' as path; - -import '../../markdown/markdown_parser.dart'; -import '../../util.dart'; +import 'package:site_shared/markdown.dart'; +import 'package:site_shared/util.dart'; class ExpansionListItem { ExpansionListItem({ diff --git a/sites/docs/lib/src/components/pages/glossary_search_section.dart b/sites/docs/lib/src/components/pages/glossary_search_section.dart index 440746f834e..07efcee01cf 100644 --- a/sites/docs/lib/src/components/pages/glossary_search_section.dart +++ b/sites/docs/lib/src/components/pages/glossary_search_section.dart @@ -4,10 +4,9 @@ import 'package:jaspr/dom.dart'; import 'package:jaspr/jaspr.dart'; +import 'package:site_shared/components/common/search.dart'; import 'package:universal_web/web.dart' as web; -import '../common/search.dart'; - @client class GlossarySearchSection extends StatefulComponent { const GlossarySearchSection({super.key}); diff --git a/sites/docs/lib/src/components/pages/learning_resource_filters.dart b/sites/docs/lib/src/components/pages/learning_resource_filters.dart index 870780c44b6..c3e90ddf9c7 100644 --- a/sites/docs/lib/src/components/pages/learning_resource_filters.dart +++ b/sites/docs/lib/src/components/pages/learning_resource_filters.dart @@ -6,14 +6,14 @@ import 'dart:math'; import 'package:jaspr/dom.dart'; import 'package:jaspr/jaspr.dart'; +import 'package:site_shared/analytics.dart'; +import 'package:site_shared/components/common/material_icon.dart'; +import 'package:site_shared/components/common/search.dart'; +import 'package:site_shared/components/utils/global_event_listener.dart'; import 'package:universal_web/js_interop.dart'; import 'package:universal_web/web.dart' as web; -import '../../analytics/analytics.dart'; import '../../models/learning_resource_model.dart'; -import '../common/material_icon.dart'; -import '../common/search.dart'; -import '../util/global_event_listener.dart'; import 'learning_resource_filters_sidebar.dart'; @client diff --git a/sites/docs/lib/src/components/pages/learning_resource_filters_sidebar.dart b/sites/docs/lib/src/components/pages/learning_resource_filters_sidebar.dart index c918d05692a..cf512193009 100644 --- a/sites/docs/lib/src/components/pages/learning_resource_filters_sidebar.dart +++ b/sites/docs/lib/src/components/pages/learning_resource_filters_sidebar.dart @@ -4,11 +4,11 @@ import 'package:jaspr/dom.dart'; import 'package:jaspr/jaspr.dart'; +import 'package:site_shared/analytics.dart'; +import 'package:site_shared/components/common/material_icon.dart'; +import 'package:site_shared/util.dart'; -import '../../analytics/analytics.dart'; import '../../models/learning_resource_model.dart'; -import '../../util.dart'; -import '../common/material_icon.dart'; import 'learning_resource_filters.dart'; @client diff --git a/sites/docs/lib/src/components/pages/learning_resource_index.dart b/sites/docs/lib/src/components/pages/learning_resource_index.dart index 139d075f92e..9293ad60f63 100644 --- a/sites/docs/lib/src/components/pages/learning_resource_index.dart +++ b/sites/docs/lib/src/components/pages/learning_resource_index.dart @@ -5,14 +5,14 @@ import 'package:jaspr/dom.dart'; import 'package:jaspr/jaspr.dart'; import 'package:jaspr_content/jaspr_content.dart'; +import 'package:site_shared/util.dart'; import '../../models/learning_resource_model.dart'; -import '../../util.dart'; import 'learning_resource_filters.dart'; import 'learning_resource_filters_sidebar.dart'; final class LearningResourceIndex extends StatelessComponent { - LearningResourceIndex({super.key}); + const LearningResourceIndex({super.key}); @override Component build(BuildContext context) { diff --git a/sites/docs/lib/src/components/pages/platforms_grid.dart b/sites/docs/lib/src/components/pages/platforms_grid.dart index 66793e7e81f..d41f065bd38 100644 --- a/sites/docs/lib/src/components/pages/platforms_grid.dart +++ b/sites/docs/lib/src/components/pages/platforms_grid.dart @@ -5,10 +5,9 @@ import 'package:jaspr/dom.dart'; import 'package:jaspr/jaspr.dart'; import 'package:jaspr_content/jaspr_content.dart'; - -import '../../markdown/markdown_parser.dart'; -import '../common/button.dart'; -import '../common/material_icon.dart'; +import 'package:site_shared/components/common/button.dart'; +import 'package:site_shared/components/common/material_icon.dart'; +import 'package:site_shared/markdown.dart'; class PlatformsGrid extends CustomComponentBase { const PlatformsGrid(); diff --git a/sites/docs/lib/src/components/pages/widget_catalog.dart b/sites/docs/lib/src/components/pages/widget_catalog.dart index 61e968225b3..fb744f58401 100644 --- a/sites/docs/lib/src/components/pages/widget_catalog.dart +++ b/sites/docs/lib/src/components/pages/widget_catalog.dart @@ -6,10 +6,10 @@ import 'package:collection/collection.dart'; import 'package:jaspr/dom.dart'; import 'package:jaspr/jaspr.dart'; import 'package:jaspr_content/jaspr_content.dart'; +import 'package:site_shared/markdown.dart'; +import 'package:site_shared/util.dart'; -import '../../markdown/markdown_parser.dart'; import '../../models/widget_catalog_model.dart'; -import '../../util.dart'; class WidgetCatalogCategories extends CustomComponentBase { const WidgetCatalogCategories(); diff --git a/sites/docs/lib/src/components/tutorial/quiz.dart b/sites/docs/lib/src/components/tutorial/quiz.dart deleted file mode 100644 index d552df4f8c3..00000000000 --- a/sites/docs/lib/src/components/tutorial/quiz.dart +++ /dev/null @@ -1,38 +0,0 @@ -// 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 '../../models/quiz_model.dart'; -import 'client/quiz.dart'; - -class Quiz extends CustomComponent { - const Quiz() : super.base(); - - @override - Component? create(Node node, NodesBuilder builder) { - if (node is ElementNode && node.tag.toLowerCase() == 'quiz') { - if (node.children?.whereType().isNotEmpty ?? false) { - throw Exception( - 'Invalid Quiz content. Remove any leading empty lines to ' - 'avoid parsing as markdown.', - ); - } - - final title = node.attributes['title']; - - 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) => Question.fromMap(n as YamlMap)) - .toList(); - assert(questions.isNotEmpty, 'Quiz must contain at least one question.'); - return InteractiveQuiz(title: title, questions: questions); - } - return null; - } -} diff --git a/sites/docs/lib/src/components/util/component_ref.dart b/sites/docs/lib/src/components/util/component_ref.dart deleted file mode 100644 index f4a0c499d03..00000000000 --- a/sites/docs/lib/src/components/util/component_ref.dart +++ /dev/null @@ -1,102 +0,0 @@ -// 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:nanoid2/nanoid2.dart'; - -import '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. -/// -/// On the server, wrap your component with `context.ref(yourComponent)`, and -/// pass the resulting [ComponentRef] to your @client component. -/// On the client, retrieve the original component by calling `myRef.component`. -class ComponentRef { - const ComponentRef._(this.id); - - final String id; - - Component get component { - return Builder( - builder: (context) { - if (!kIsWeb) { - final scope = - context - .getElementForInheritedComponentOfExactType< - ComponentRefScope - >() - as _ComponentRefScopeElement?; - return Component.wrapElement( - id: id, - child: scope!.getComponentById(id), - ); - } else { - final elem = retakeElement(context, (elem) => elem.id == id); - assert(elem != null, 'Element with id "$id" not found'); - return wrapNode(elem!); - } - }, - ); - } - - @decoder - factory ComponentRef.fromId(String id) { - return ComponentRef._(id); - } - - @encoder - String toId() => id; -} - -extension ComponentRefExtension on BuildContext { - /// Wraps a [Component] in a [ComponentRef] for use in @client components. - ComponentRef ref(Component child) { - final scope = - getElementForInheritedComponentOfExactType() - as _ComponentRefScopeElement?; - assert(scope != null, 'No ComponentRefScope found in context'); - final ref = scope!.register(child); - return ref; - } -} - -/// A scope for registering and retrieving component references. -/// -/// This should wrap your entire app, typically in `main.dart`. -class ComponentRefScope extends InheritedComponent { - const ComponentRefScope({ - required super.child, - }); - - @override - bool updateShouldNotify(ComponentRefScope oldComponent) { - return false; - } - - @override - InheritedElement createElement() => _ComponentRefScopeElement(this); -} - -class _ComponentRefScopeElement extends InheritedElement { - _ComponentRefScopeElement(ComponentRefScope super.component); - - final Map _registeredComponents = {}; - - Component getComponentById(String id) { - final component = _registeredComponents[id]; - assert(component != null, 'No component registered with id "$id"'); - return component!; - } - - ComponentRef register(Component child) { - final id = 'ref-${nanoid(length: 8)}'; - _registeredComponents[id] = child; - return ComponentRef._(id); - } -} diff --git a/sites/docs/lib/src/extensions/glossary_link_processor.dart b/sites/docs/lib/src/extensions/glossary_link_processor.dart index f1602547136..af86e036278 100644 --- a/sites/docs/lib/src/extensions/glossary_link_processor.dart +++ b/sites/docs/lib/src/extensions/glossary_link_processor.dart @@ -5,9 +5,9 @@ import 'package:jaspr/dom.dart'; import 'package:jaspr/jaspr.dart'; import 'package:jaspr_content/jaspr_content.dart'; +import 'package:site_shared/components/common/client/simple_tooltip.dart'; +import 'package:site_shared/components/utils/component_ref.dart'; -import '../components/common/client/simple_tooltip.dart'; -import '../components/util/component_ref.dart'; import '../pages/glossary.dart'; /// A node-processing, page extension for Jaspr Content that looks for links to @@ -52,8 +52,8 @@ class GlossaryLinkProcessor implements PageExtension { Builder( builder: (context) { return SimpleTooltip( - target: context.ref(target), - content: context.ref(content), + target: ref(target), + content: ref(content), ); }, ), diff --git a/sites/docs/lib/src/extensions/registry.dart b/sites/docs/lib/src/extensions/registry.dart index a2690a6a188..158f0a3a9c9 100644 --- a/sites/docs/lib/src/extensions/registry.dart +++ b/sites/docs/lib/src/extensions/registry.dart @@ -3,13 +3,9 @@ // found in the LICENSE file. import 'package:jaspr_content/jaspr_content.dart'; +import 'package:site_shared/page_extensions.dart'; -import 'attribute_processor.dart'; -import 'code_block_processor.dart'; import 'glossary_link_processor.dart'; -import 'header_extractor.dart'; -import 'header_processor.dart'; -import 'table_processor.dart'; import 'tutorial_navigation.dart'; import 'tutorial_structure_processor.dart'; @@ -20,7 +16,7 @@ const List allNodeProcessingExtensions = [ HeaderExtractorExtension(), HeaderWrapperExtension(), TableWrapperExtension(), - CodeBlockProcessor(), + CodeBlockProcessor(defaultTitle: 'Runnable Flutter example'), GlossaryLinkProcessor(), TutorialNavigationExtension(), TutorialStructureExtension(), diff --git a/sites/docs/lib/src/extensions/tutorial_navigation.dart b/sites/docs/lib/src/extensions/tutorial_navigation.dart index ed5aed6d3aa..0d47175269e 100644 --- a/sites/docs/lib/src/extensions/tutorial_navigation.dart +++ b/sites/docs/lib/src/extensions/tutorial_navigation.dart @@ -3,8 +3,7 @@ // found in the LICENSE file. import 'package:jaspr_content/jaspr_content.dart'; - -import '../models/tutorial_model.dart'; +import 'package:site_shared/components/tutorial/models/tutorial_model.dart'; /// A page extension for Jaspr Content that adds /// page navigation to the current tutorial page. diff --git a/sites/docs/lib/src/layouts/doc_layout.dart b/sites/docs/lib/src/layouts/doc_layout.dart index 4766d4c3cce..15d1821e9fd 100644 --- a/sites/docs/lib/src/layouts/doc_layout.dart +++ b/sites/docs/lib/src/layouts/doc_layout.dart @@ -11,7 +11,7 @@ import '../components/common/prev_next.dart'; import '../components/layout/toc.dart'; import '../components/layout/trailing_content.dart'; import '../models/page_navigation_model.dart'; -import 'dash_layout.dart'; +import 'flutter_layout.dart'; /// The Jaspr Content layout to use for normal docs pages, /// adding elements such as breadcrumbs, TOC, and prev/next cards. @@ -88,7 +88,6 @@ class DocLayout extends FlutterDocsLayout { ), child, - PrevNext( previousPage: PageNavigationEntry.fromData(pageData['prev']), nextPage: PageNavigationEntry.fromData(pageData['next']), diff --git a/sites/docs/lib/src/layouts/flutter_layout.dart b/sites/docs/lib/src/layouts/flutter_layout.dart new file mode 100644 index 00000000000..f20c80e836d --- /dev/null +++ b/sites/docs/lib/src/layouts/flutter_layout.dart @@ -0,0 +1,114 @@ +// 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:jaspr_content/jaspr_content.dart'; +import 'package:site_shared/layouts.dart'; +import 'package:site_shared/util.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'; + +/// The base Jaspr Content layout for wrapping site content. +abstract class FlutterDocsLayout extends DashLayout { + const FlutterDocsLayout(); + + @override + String get name; + + @override + String get siteHost => 'docs.flutter.dev'; + @override + String get iconUrl => '/assets/images/branding/flutter/icon/64.png'; + @override + String get iconUrlApple => + '/assets/images/branding/flutter/logo/flutter-logomark-320px.png'; + @override + String get twitterSiteTag => '@flutterdev'; + @override + String get twitterDefaultImageUrl => + '/assets/images/flutter-logo-sharing.png'; + + @override + String get tagManagerId => 'GTM-ND4LWWZ'; + @override + String get analyticsId => 'UA-67589403-1'; + + @override + String get stylesHash => generatedStylesHash; + + String get defaultSidenav => 'default'; + + @override + Iterable buildExtraHead(Page page) { + return const [ + if (productionBuild) + meta( + name: 'google-site-verification', + content: 'HFqxhSbf9YA_0rBglNLzDiWnrHiK_w4cqDh2YD2GEY4', + ), + ]; + } + + @override + Component buildBody(Page page, Component child) { + final pageData = page.data.page; + 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 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 (obsolete) + const div(id: 'obsolete-banner', [ + div(classes: 'text-center', [ + .text('Some content on this page might be out of date.'), + ]), + ]), + ]), + const DashFooter(), + ]), + // Scroll the sidenav to the active item before other logic + // to avoid it jumping after page load. + const script( + content: ''' +const sidenav = document.getElementById('sidenav'); +if (sidenav) { + const activeEntries = sidenav.querySelectorAll('.nav-link.active'); + if (activeEntries.length > 0) { + const activeEntry = activeEntries[activeEntries.length - 1]; + + sidenav.scrollTo({ + top: activeEntry.offsetTop - window.innerHeight / 3, + }); + } +} + ''', + ), + ]); + } +} diff --git a/sites/docs/lib/src/layouts/toc_layout.dart b/sites/docs/lib/src/layouts/toc_layout.dart index 9401e9ffd23..7dd3d864d32 100644 --- a/sites/docs/lib/src/layouts/toc_layout.dart +++ b/sites/docs/lib/src/layouts/toc_layout.dart @@ -5,8 +5,8 @@ import 'package:jaspr/dom.dart'; import 'package:jaspr/jaspr.dart'; import 'package:jaspr_content/jaspr_content.dart'; +import 'package:site_shared/components/common/card.dart'; -import '../components/common/card.dart'; import 'doc_layout.dart'; class TocLayout extends DocLayout { diff --git a/sites/docs/lib/src/layouts/tutorial_layout.dart b/sites/docs/lib/src/layouts/tutorial_layout.dart index c148b36fb4e..1a314afdddf 100644 --- a/sites/docs/lib/src/layouts/tutorial_layout.dart +++ b/sites/docs/lib/src/layouts/tutorial_layout.dart @@ -4,8 +4,8 @@ import 'package:jaspr/jaspr.dart'; import 'package:jaspr_content/jaspr_content.dart'; +import 'package:site_shared/components/tutorial/models/tutorial_model.dart'; -import '../models/tutorial_model.dart'; import 'doc_layout.dart'; class TutorialLayout extends DocLayout { diff --git a/sites/docs/lib/src/models/page_navigation_model.dart b/sites/docs/lib/src/models/page_navigation_model.dart index 671981bf295..a52011bb32f 100644 --- a/sites/docs/lib/src/models/page_navigation_model.dart +++ b/sites/docs/lib/src/models/page_navigation_model.dart @@ -3,8 +3,7 @@ // found in the LICENSE file. import 'package:jaspr_content/jaspr_content.dart'; - -import '../extensions/header_extractor.dart'; +import 'package:site_shared/page_extensions.dart'; extension GetPageNavigationData on Page { PageNavigationData? get navigationData { diff --git a/sites/docs/lib/src/models/widget_catalog_model.dart b/sites/docs/lib/src/models/widget_catalog_model.dart index a81286927e4..ab85167e843 100644 --- a/sites/docs/lib/src/models/widget_catalog_model.dart +++ b/sites/docs/lib/src/models/widget_catalog_model.dart @@ -1,3 +1,7 @@ +// 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. + extension type WidgetCatalogCategory(Map _data) { String get id => _data['id'] as String? ?? diff --git a/sites/docs/lib/src/pages/glossary.dart b/sites/docs/lib/src/pages/glossary.dart index c3a2590159d..5f317f2dc2d 100644 --- a/sites/docs/lib/src/pages/glossary.dart +++ b/sites/docs/lib/src/pages/glossary.dart @@ -5,12 +5,12 @@ import 'package:jaspr/dom.dart'; import 'package:jaspr/jaspr.dart'; import 'package:jaspr_content/jaspr_content.dart'; +import 'package:site_shared/components/common/button.dart'; +import 'package:site_shared/components/common/card.dart'; +import 'package:site_shared/markdown.dart'; +import 'package:site_shared/util.dart'; -import '../components/common/button.dart'; -import '../components/common/card.dart'; import '../components/pages/glossary_search_section.dart'; -import '../markdown/markdown_parser.dart'; -import '../util.dart'; /// Different types of resources that glossary terms might link to. enum ResourceType { diff --git a/sites/docs/lib/src/pages/robots_txt.dart b/sites/docs/lib/src/pages/robots_txt.dart index 8fbfaea15a6..3ee7bbac409 100644 --- a/sites/docs/lib/src/pages/robots_txt.dart +++ b/sites/docs/lib/src/pages/robots_txt.dart @@ -4,8 +4,7 @@ import 'package:jaspr/server.dart'; import 'package:jaspr_content/jaspr_content.dart'; - -import '../util.dart'; +import 'package:site_shared/util.dart'; /// The secondary output to configure to create /// a `robots.txt` file in the root directory. diff --git a/sites/docs/lib/src/pages/widget_catalog.dart b/sites/docs/lib/src/pages/widget_catalog.dart index 82d83955202..7b11f23c006 100644 --- a/sites/docs/lib/src/pages/widget_catalog.dart +++ b/sites/docs/lib/src/pages/widget_catalog.dart @@ -1,3 +1,7 @@ +// 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 'dart:io'; import 'package:collection/collection.dart'; @@ -5,18 +9,17 @@ import 'package:jaspr/dom.dart'; import 'package:jaspr/jaspr.dart'; import 'package:jaspr_content/jaspr_content.dart'; import 'package:path/path.dart' as path; +import 'package:site_shared/markdown.dart'; +import 'package:site_shared/util.dart'; import '../components/pages/widget_catalog.dart'; -import '../markdown/markdown_parser.dart'; import '../models/widget_catalog_model.dart'; -import '../util.dart'; - -final _widgetCatalogIndexFile = File( - path.join(siteSrcDirectoryPath, 'data', 'catalog', 'index.yml'), -); List get widgetCatalogPages { - final catalogData = _widgetCatalogIndexFile.readAsStringSync(); + final widgetCatalogIndexFile = File( + path.join('src', 'data', 'catalog', 'index.yml'), + ); + final catalogData = widgetCatalogIndexFile.readAsStringSync(); final catalog = (DataLoader.parseData('index.yml', catalogData) as List) .cast>() diff --git a/sites/docs/lib/src/style_hash.dart b/sites/docs/lib/src/style_hash.dart index ce114383cc9..bd8e030c02b 100644 --- a/sites/docs/lib/src/style_hash.dart +++ b/sites/docs/lib/src/style_hash.dart @@ -1,4 +1,4 @@ -// 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. diff --git a/sites/docs/lib/src/templating/dash_template_engine.dart b/sites/docs/lib/src/templating/dash_template_engine.dart index ed2a8e2dbe7..681410e2c41 100644 --- a/sites/docs/lib/src/templating/dash_template_engine.dart +++ b/sites/docs/lib/src/templating/dash_template_engine.dart @@ -6,8 +6,7 @@ import 'package:collection/collection.dart'; import 'package:jaspr_content/jaspr_content.dart'; import 'package:liquify/liquify.dart'; import 'package:path/path.dart' as path; - -import '../util.dart'; +import 'package:site_shared/util.dart'; /// A template engine for Jaspr Content that /// uses `package:liquify` to parse and render Liquid templates. diff --git a/sites/docs/pubspec.yaml b/sites/docs/pubspec.yaml index 36a378a3b7d..c818dbb02ee 100644 --- a/sites/docs/pubspec.yaml +++ b/sites/docs/pubspec.yaml @@ -9,7 +9,6 @@ environment: dependencies: build: ^4.0.6 collection: ^1.19.1 - crypto: ^3.0.7 html: ^0.15.6 http: ^1.6.0 jaspr: ^0.23.1 @@ -24,6 +23,8 @@ dependencies: opal: ^0.2.2 path: ^1.9.1 pub_semver: ^2.2.0 + site_shared: + path: ../../packages/site_shared universal_web: ^1.1.1+1 yaml: ^3.1.3