diff --git a/site/lib/_sass/base/_base.scss b/site/lib/_sass/base/_base.scss index 40ae9db1c6c..ce855aca2bb 100644 --- a/site/lib/_sass/base/_base.scss +++ b/site/lib/_sass/base/_base.scss @@ -8,8 +8,10 @@ body { color: var(--site-base-fgColor); // The top TOC is not shown on narrow screens. - @media (min-width: 1200px) { - --site-subheader-height: 0rem; + &:not(:has(#site-subheader.show-always)) { + @media (min-width: 1200px) { + --site-subheader-height: 0rem; + } } // If the TOC is disabled, reduce the subheader height to diff --git a/site/lib/_sass/components/_header.scss b/site/lib/_sass/components/_header.scss index d2926018f9d..57528e26ad4 100644 --- a/site/lib/_sass/components/_header.scss +++ b/site/lib/_sass/components/_header.scss @@ -7,8 +7,10 @@ border-bottom: 0.1rem solid var(--site-outline-variant); @media (min-width: 1200px) { - box-shadow: 0 2px 4px rgba(0, 0, 0, .05); - border-bottom: none; + &:not(:has(~* #site-subheader.show-always)) { + box-shadow: 0 2px 4px rgba(0, 0, 0, .05); + border-bottom: none; + } } .navbar { @@ -186,7 +188,9 @@ body.open_menu #menu-toggle span.material-symbols { border-bottom: 0.1rem solid var(--site-outline-variant); box-shadow: 0 2px 4px rgba(0, 0, 0, .05); - @media (width < 240px), (width >= 1200px) { - display: none; + &:not(.show-always) { + @media (width < 240px), (width >= 1200px) { + display: none; + } } } diff --git a/site/lib/_sass/components/_pagenav.scss b/site/lib/_sass/components/_pagenav.scss index c541f41ca65..8d3adb183dc 100644 --- a/site/lib/_sass/components/_pagenav.scss +++ b/site/lib/_sass/components/_pagenav.scss @@ -1,6 +1,7 @@ #pagenav { flex-grow: 1; min-width: 0; + max-width: 100%; >button.dropdown-button { display: flex; @@ -25,27 +26,36 @@ } } - .toc-intro { + .toc-breadcrumb { + flex-shrink: 2; white-space: nowrap; + overflow: hidden; - .material-symbols { + &:first-child .material-symbols { margin-right: 0.25rem; } - } - .toc-current { - display: none; - - @media (min-width: 320px) { - display: flex; + span:last-child { + overflow: hidden; + text-overflow: ellipsis; } + } - flex-wrap: nowrap; + .toc-current { + flex-shrink: 1; white-space: nowrap; overflow: hidden; - text-overflow: ellipsis; color: var(--site-base-fgColor-alt); + + @media (max-width: 320px) { + display: none !important; + } + + span:last-child { + overflow: hidden; + text-overflow: ellipsis; + } } #pagenav-content { @@ -85,6 +95,7 @@ text-decoration: none; display: flex; align-items: center; + gap: 4px; color: var(--site-base-fgColor-alt); font-weight: 500; @@ -93,10 +104,6 @@ user-select: none; } - span:last-child { - margin-left: 3px; - } - &:hover { color: var(--site-link-fgColor); } @@ -109,5 +116,51 @@ nav { padding: 0.6rem 0 0.8rem; } + + .page-link { + padding: 0; + font-weight: 400; + color: var(--site-base-fgColor); + margin-top: 0.6rem; + + .page-number { + width: 25px; + height: 25px; + border-radius: 50%; + background: var(--site-raised-bgColor); + color: var(--site-base-fgColor); + display: flex; + align-items: center; + justify-content: center; + font-weight: 500; + + transition: background-color 300ms ease, color 300ms ease; + } + + &.active .page-number { + background-color: var(--site-primary-color); + color: var(--site-onPrimary-color-lightest); + } + + &:not(.active):has(~.page-link.active) .page-number { + background-color: var(--site-onPrimary-color-light); + color: var(--site-primary-color); + } + + ~nav { + padding: 0; + } + } + + .page-divider { + padding-left: 0.25rem; + padding-top: 0.25rem; + font-weight: 600; + color: var(--site-base-fgColor-alt); + } + + .dropdown-divider:has(~.page-link) { + margin-top: 0.6rem; + } } } diff --git a/site/lib/_sass/components/_tooltip.scss b/site/lib/_sass/components/_tooltip.scss index cc71c232248..5584613fb15 100644 --- a/site/lib/_sass/components/_tooltip.scss +++ b/site/lib/_sass/components/_tooltip.scss @@ -60,4 +60,4 @@ visibility: visible; } } -} +} \ No newline at end of file diff --git a/site/lib/jaspr_options.dart b/site/lib/jaspr_options.dart index 5ef15399edc..a0d7a68a07f 100644 --- a/site/lib/jaspr_options.dart +++ b/site/lib/jaspr_options.dart @@ -170,7 +170,8 @@ Map _prefix8DartPadInjector(prefix8.DartPadInjector c) => { 'runAutomatically': c.runAutomatically, }; Map _prefix9PageNav(prefix9.PageNav c) => { - 'title': c.title, + 'breadcrumbs': c.breadcrumbs, + 'initialHeading': c.initialHeading, 'content': c.content.toId(), }; Map _prefix13ArchiveTable(prefix13.ArchiveTable c) => { diff --git a/site/lib/main.dart b/site/lib/main.dart index 2b41724cdb7..fffc7e2185e 100644 --- a/site/lib/main.dart +++ b/site/lib/main.dart @@ -26,6 +26,7 @@ import 'src/extensions/registry.dart'; import 'src/layouts/catalog_page_layout.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'; @@ -66,7 +67,12 @@ Component get _docsFlutterDevSite => ContentApp.custom( rawOutputPattern: _passThroughPattern, extensions: allNodeProcessingExtensions, components: _embeddableComponents, - layouts: const [DocLayout(), TocLayout(), CatalogPageLayout()], + layouts: const [ + DocLayout(), + TocLayout(), + CatalogPageLayout(), + TutorialLayout(), + ], theme: const ContentTheme.none(), secondaryOutputs: [ const RobotsTxtOutput(), diff --git a/site/lib/src/components/common/prev_next.dart b/site/lib/src/components/common/prev_next.dart index 3f207450bcf..600e899a636 100644 --- a/site/lib/src/components/common/prev_next.dart +++ b/site/lib/src/components/common/prev_next.dart @@ -4,6 +4,7 @@ import 'package:jaspr/jaspr.dart'; +import '../../models/page_navigation_model.dart'; import 'material_icon.dart'; /// Previous and next page buttons to display at the end of a page @@ -11,8 +12,8 @@ import 'material_icon.dart'; class PrevNext extends StatelessComponent { const PrevNext({super.key, this.previousPage, this.nextPage}); - final ({String url, String title})? previousPage; - final ({String url, String title})? nextPage; + final PageNavigationEntry? previousPage; + final PageNavigationEntry? nextPage; @override Component build(BuildContext context) { @@ -32,7 +33,7 @@ class PrevNext extends StatelessComponent { class _PrevNextCard extends StatelessComponent { const _PrevNextCard({required this.page, required this.isPrevious}); - final ({String url, String title}) page; + final PageNavigationEntry page; final bool isPrevious; @override diff --git a/site/lib/src/components/layout/client/pagenav.dart b/site/lib/src/components/layout/client/pagenav.dart index 18737c4b68a..b661d02a133 100644 --- a/site/lib/src/components/layout/client/pagenav.dart +++ b/site/lib/src/components/layout/client/pagenav.dart @@ -14,12 +14,14 @@ import '../../util/component_ref.dart'; @client class PageNav extends StatefulComponent { const PageNav({ - required this.title, + this.breadcrumbs = const [], + required this.initialHeading, required this.content, super.key, }); - final String title; + final List breadcrumbs; + final String initialHeading; final ComponentRef content; @override @@ -63,21 +65,34 @@ class _PageNavState extends State { 'aria-label': 'Toggle the table of contents dropdown', }, [ - span(classes: 'toc-intro', [ - const MaterialIcon('list'), - span( - attributes: {'aria-label': 'On this page'}, - [ - text('On this page'), - ], - ), - ]), + if (component.breadcrumbs.isEmpty) + span(classes: 'toc-intro', [ + const MaterialIcon('list'), + span( + attributes: {'aria-label': 'On this page'}, + [text('On this page')], + ), + ]) + else ...[ + for (final (index, crumb) in component.breadcrumbs.indexed) ...[ + span(classes: 'toc-breadcrumb', [ + if (index == 0) + const MaterialIcon('list') + else + const MaterialIcon('chevron_right'), + span([ + text(crumb), + ]), + ]), + ], + ], + span(classes: 'toc-current', [ const MaterialIcon('chevron_right'), ValueListenableBuilder( listenable: currentPageHeading, builder: (context, value) { - return span([text(value ?? component.title)]); + return span([text(value ?? component.initialHeading)]); }, ), ]), diff --git a/site/lib/src/components/layout/header.dart b/site/lib/src/components/layout/header.dart index 14a0d962f6e..2daabbb885d 100644 --- a/site/lib/src/components/layout/header.dart +++ b/site/lib/src/components/layout/header.dart @@ -3,6 +3,7 @@ // found in the LICENSE file. import 'package:jaspr/jaspr.dart'; +import 'package:jaspr_content/jaspr_content.dart'; import '../common/button.dart'; import '../common/material_icon.dart'; @@ -84,7 +85,7 @@ class DashHeader extends StatelessComponent { content: 'Get started', href: '/get-started/quick', ), - const MenuToggle(), + if (context.page.data['sidenav'] != null) const MenuToggle(), ], ), ]), diff --git a/site/lib/src/components/layout/toc.dart b/site/lib/src/components/layout/toc.dart index 4b0d5c50634..30d798a29cc 100644 --- a/site/lib/src/components/layout/toc.dart +++ b/site/lib/src/components/layout/toc.dart @@ -3,8 +3,10 @@ // found in the LICENSE file. import 'package:jaspr/jaspr.dart'; +import 'package:jaspr_content/jaspr_content.dart'; -import '../../models/on_this_page_model.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'; @@ -13,7 +15,7 @@ import 'client/pagenav.dart'; final class DashTableOfContents extends StatelessComponent { const DashTableOfContents(this.data); - final OnThisPageData data; + final TocNavigationData data; @override Component build(BuildContext _) { @@ -22,38 +24,95 @@ final class DashTableOfContents extends StatelessComponent { _TocContents(data), ]); } +} - static Component asDropdown( - OnThisPageData data, { - required String currentTitle, - }) { - return Builder( - builder: (context) { - return PageNav( - title: currentTitle, - content: context.ref( - div([ - a( - href: '#site-content-title', - id: 'return-to-top', - [ - const MaterialIcon('vertical_align_top'), - span([text(currentTitle)]), - ], - ), - div( - classes: 'dropdown-divider', - attributes: {'aria-hidden': 'true', 'role': 'separator'}, - [], - ), +final class PageNavBar extends StatelessComponent { + const PageNavBar(this.data); + + final PageNavigationData data; + + @override + Component build(BuildContext context) { + PageNavigationEntry? currentLinkedPage; + String? currentDivider; + + for (final page in data.pageEntries) { + if (page.url == context.page.url) { + currentLinkedPage = page; + break; + } + if (page.isDivider) { + currentDivider = page.title; + } + } + + final linkedPageTitle = currentLinkedPage?.title; + final currentTitle = context.page.data.page['title'] as String; + + var pageEntryNumber = 1; + + return PageNav( + breadcrumbs: [?data.parentTitle, ?currentDivider, ?linkedPageTitle], + initialHeading: currentTitle, + content: context.ref( + div([ + if (data.pageEntries.isEmpty) ...[ + a( + href: '#site-content-title', + id: 'return-to-top', + [ + const MaterialIcon('vertical_align_top'), + span([text(currentTitle)]), + ], + ), + div( + classes: 'dropdown-divider', + attributes: {'aria-hidden': 'true', 'role': 'separator'}, + [], + ), + if (data.toc != null) nav( attributes: {'role': 'menu'}, - [_TocContents(data)], + [_TocContents(data.toc!)], ), - ]), - ), - ); - }, + ] else ...[ + for (final page in data.pageEntries) ...[ + if (!page.isDivider) ...[ + a( + classes: [ + 'page-link', + if (page == currentLinkedPage) 'active', + ].toClasses, + href: page.url, + attributes: {'role': 'menuitem'}, + [ + span(classes: 'page-number', [ + text('${pageEntryNumber++}'), + ]), + text(page.title), + ], + ), + if (currentLinkedPage == page && data.toc != null) + nav( + attributes: {'role': 'menu'}, + [_TocContents(data.toc!)], + ), + ] else ...[ + if (page != data.pageEntries.first) + div( + classes: 'dropdown-divider', + attributes: {'aria-hidden': 'true', 'role': 'separator'}, + [], + ), + div( + classes: 'page-divider', + [text(page.title)], + ), + ], + ], + ], + ]), + ), ); } } @@ -61,7 +120,7 @@ final class DashTableOfContents extends StatelessComponent { final class _TocContents extends StatelessComponent { const _TocContents(this.data); - final OnThisPageData data; + final TocNavigationData data; @override Component build(BuildContext _) => ul( @@ -69,7 +128,7 @@ final class _TocContents extends StatelessComponent { _buildEntries(data.topLevelEntries, 0), ); - List _buildEntries(List entries, int depth) { + List _buildEntries(List entries, int depth) { final nextDepth = depth + 1; return [ diff --git a/site/lib/src/layouts/doc_layout.dart b/site/lib/src/layouts/doc_layout.dart index a15a3a0b038..3b00c9379b3 100644 --- a/site/lib/src/layouts/doc_layout.dart +++ b/site/lib/src/layouts/doc_layout.dart @@ -10,8 +10,7 @@ import '../components/common/prev_next.dart'; import '../components/layout/banner.dart'; import '../components/layout/toc.dart'; import '../components/layout/trailing_content.dart'; -import '../extensions/header_extractor.dart'; -import '../models/on_this_page_model.dart'; +import '../models/page_navigation_model.dart'; import '../util.dart'; import 'dash_layout.dart'; @@ -33,29 +32,37 @@ class DocLayout extends FlutterDocsLayout { (pageData['showBanner'] as bool?) ?? (siteData['showBanner'] as bool?) ?? false; - final tocData = _tocForPage(page); + final navigationData = page.navigationData; return super.buildBody( page, Component.fragment( [ - if (tocData == null) + if (navigationData == null) const Document.body(attributes: {'data-toc': 'false'}) else - div(id: 'site-subheader', [ - DashTableOfContents.asDropdown( - tocData, - currentTitle: pageTitle, - ), - ]), + div( + id: 'site-subheader', + classes: navigationData.pageEntries.isNotEmpty + ? 'show-always' + : null, + [ + PageNavBar( + navigationData, + ), + ], + ), if (showBanner) if (siteData['bannerHtml'] case final String bannerHtml when bannerHtml.trim().isNotEmpty) DashBanner(bannerHtml), div(classes: 'after-leading-content', [ - if (tocData != null) + if (navigationData case PageNavigationData( + toc: final toc?, + pageEntries: [], + )) aside(id: 'side-menu', [ - DashTableOfContents(tocData), + DashTableOfContents(toc), ]), article([ div(id: 'site-content-title', [ @@ -72,8 +79,8 @@ class DocLayout extends FlutterDocsLayout { child, PrevNext( - previousPage: _pageInfoFromObject(pageData['prev']), - nextPage: _pageInfoFromObject(pageData['next']), + previousPage: PageNavigationEntry.fromData(pageData['prev']), + nextPage: PageNavigationEntry.fromData(pageData['next']), ), const TrailingContent(), ]), @@ -82,34 +89,4 @@ class DocLayout extends FlutterDocsLayout { ), ); } - - OnThisPageData? _tocForPage(Page page) { - final pageData = page.data.page; - final showToc = pageData['showToc'] as bool? ?? true; - - // If 'showToc' was explicitly set to false, hide the toc. - if (!showToc) return null; - - final onThisPageData = OnThisPageData.fromContentHeaders( - page.data['contentHeaders'] as List? ?? const [], - minLevel: pageData['minTocDepth'] as int? ?? 2, - maxLevel: pageData['maxTocDepth'] as int? ?? 3, - ); - - // If there are less than 2 top-level entries, hide the toc. - if (onThisPageData.topLevelEntries.length < 2) return null; - - return onThisPageData; - } -} - -({String url, String title})? _pageInfoFromObject(Object? data) { - if (data case { - 'path': final String pageUrl, - 'title': final String pageTitle, - }) { - return (url: pageUrl, title: pageTitle); - } - - return null; } diff --git a/site/lib/src/layouts/tutorial_layout.dart b/site/lib/src/layouts/tutorial_layout.dart new file mode 100644 index 00000000000..6126e9e9ec5 --- /dev/null +++ b/site/lib/src/layouts/tutorial_layout.dart @@ -0,0 +1,53 @@ +// 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 'doc_layout.dart'; + +class TutorialLayout extends DocLayout { + const TutorialLayout(); + + @override + String get name => 'tutorial'; + + @override + Component buildBody(Page page, Component child) { + //TODO(schultek): Extract the real pages in some way. + const navigationEntries = [ + {'type': 'divider', 'title': 'Introdution to Flutter UI'}, + {'title': 'Create a Flutter app', 'path': '/fwe0'}, + {'title': 'Widget fundamentals', 'path': '/fwe1'}, + {'title': 'Layout widgets on a screen', 'path': '/fwe2'}, + {'title': 'FWE Testing Page', 'path': '/fwe'}, + {'title': 'Devtools', 'path': '/fwe3'}, + {'title': 'Handle user input', 'path': '/fwe4'}, + {'type': 'divider', 'title': 'State in Flutter apps'}, + {'title': 'Set up a new project', 'path': '/fwe5'}, + {'title': 'Make Http Requests', 'path': '/fwe6'}, + {'title': 'Use ChangeNotifier to update app state', 'path': '/fwe7'}, + {'title': 'Use ListenableBuilder to update app UI', 'path': '/fwe8'}, + {'type': 'divider', 'title': 'Flutter UI 102'}, + {'title': 'Set up your project', 'path': '/fwe9'}, + {'title': 'LayoutBuilder and adaptive layouts', 'path': '/fwe10'}, + {'title': 'Scrolling and slivers', 'path': '/fwe11'}, + {'title': 'Stack based navigation', 'path': '/fwe12'}, + ]; + + return super.buildBody( + page..apply( + data: { + 'page': { + 'showBanner': false, + 'navigationCollectionTitle': 'Flutter Fundamentals', + 'navigationEntries': navigationEntries, + }, + 'sidenav': null, + }, + ), + child, + ); + } +} diff --git a/site/lib/src/models/on_this_page_model.dart b/site/lib/src/models/on_this_page_model.dart deleted file mode 100644 index 2efd3eb7a4c..00000000000 --- a/site/lib/src/models/on_this_page_model.dart +++ /dev/null @@ -1,75 +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 '../extensions/header_extractor.dart'; - -class OnThisPageData { - final List topLevelEntries; - - OnThisPageData(this.topLevelEntries); - - factory OnThisPageData.fromContentHeaders( - List headers, { - required int minLevel, - required int maxLevel, - }) { - final rootEntries = []; - final levelMap = {}; - - for (final header in headers) { - // Clear entries at this level and below - // so that they aren't tracked any more. - for ( - var removeLevel = header.level; - removeLevel <= maxLevel; - removeLevel += 1 - ) { - levelMap.remove(removeLevel); - } - - final id = header.attributes['id']; - final classes = header.attributes['class']?.split(' ') ?? []; - - // Check if header should be skipped. - if (id == null || - classes.contains('no_toc') || - header.level < minLevel || - header.level > maxLevel) { - continue; - } - - final entry = OnThisPageEntry( - id: id, - text: header.text, - children: [], - ); - - // Check if this is a root level entry. - if (header.level == minLevel) { - rootEntries.add(entry); - levelMap[header.level] = entry; - } else { - // Look for parent at exactly one level above. - if (levelMap[header.level - 1] case final parent?) { - parent.children.add(entry); - levelMap[header.level] = entry; - } - } - } - - return OnThisPageData(rootEntries); - } -} - -final class OnThisPageEntry { - final String id; - final String text; - final List children; - - const OnThisPageEntry({ - required this.id, - required this.text, - this.children = const [], - }); -} diff --git a/site/lib/src/models/page_navigation_model.dart b/site/lib/src/models/page_navigation_model.dart new file mode 100644 index 00000000000..54c8728f76a --- /dev/null +++ b/site/lib/src/models/page_navigation_model.dart @@ -0,0 +1,153 @@ +// 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_content/jaspr_content.dart'; + +import '../extensions/header_extractor.dart'; + +extension GetPageNavigationData on Page { + PageNavigationData? get navigationData { + final pageData = data.page; + final showToc = pageData['showToc'] as bool? ?? true; + + // If 'showToc' was explicitly set to false, hide the toc. + if (!showToc) return null; + + final tocData = _getTocData( + data['contentHeaders'] as List? ?? const [], + minLevel: pageData['minTocDepth'] as int? ?? 2, + maxLevel: pageData['maxTocDepth'] as int? ?? 3, + ); + + final parentTitle = pageData['navigationCollectionTitle'] as String?; + + final pageEntries = []; + if (pageData['navigationEntries'] case final List entries) { + for (final entry in entries) { + if (PageNavigationEntry.fromData(entry) case final entry?) { + pageEntries.add(entry); + } + } + } + + // If there are less than 2 top-level entries, hide the toc. + if (tocData.topLevelEntries.length < 2) { + return PageNavigationData(null, pageEntries, parentTitle); + } + + return PageNavigationData(tocData, pageEntries, parentTitle); + } + + TocNavigationData _getTocData( + List headers, { + required int minLevel, + required int maxLevel, + }) { + final rootEntries = []; + final levelMap = {}; + + for (final header in headers) { + // Clear entries at this level and below + // so that they aren't tracked any more. + for ( + var removeLevel = header.level; + removeLevel <= maxLevel; + removeLevel += 1 + ) { + levelMap.remove(removeLevel); + } + + final id = header.attributes['id']; + final classes = header.attributes['class']?.split(' ') ?? []; + + // Check if header should be skipped. + if (id == null || + classes.contains('no_toc') || + header.level < minLevel || + header.level > maxLevel) { + continue; + } + + final entry = TocNavigationEntry( + id: id, + text: header.text, + children: [], + ); + + // Check if this is a root level entry. + if (header.level == minLevel) { + rootEntries.add(entry); + levelMap[header.level] = entry; + } else { + // Look for parent at exactly one level above. + if (levelMap[header.level - 1] case final parent?) { + parent.children.add(entry); + levelMap[header.level] = entry; + } + } + } + + return TocNavigationData(rootEntries); + } +} + +final class PageNavigationData { + PageNavigationData(this.toc, this.pageEntries, this.parentTitle); + + final TocNavigationData? toc; + final List pageEntries; + final String? parentTitle; +} + +final class TocNavigationData { + TocNavigationData(this.topLevelEntries); + + final List topLevelEntries; +} + +final class TocNavigationEntry { + const TocNavigationEntry({ + required this.id, + required this.text, + this.children = const [], + }); + + final String id; + final String text; + final List children; +} + +final class PageNavigationEntry { + const PageNavigationEntry({ + required this.title, + required this.url, + }) : isDivider = false; + + const PageNavigationEntry.divider({ + required this.title, + }) : url = '', + isDivider = true; + + static PageNavigationEntry? fromData(Object? data) { + if (data case { + 'type': 'divider', + 'title': final String title, + }) { + return PageNavigationEntry.divider(title: title); + } + + if (data case { + 'title': final String title, + 'path': final String path, + }) { + return PageNavigationEntry(title: title, url: path); + } + + return null; + } + + final String title; + final String url; + final bool isDivider; +} diff --git a/site/lib/src/pages/custom_pages.dart b/site/lib/src/pages/custom_pages.dart index 2cffefadcad..b6703d3eada 100644 --- a/site/lib/src/pages/custom_pages.dart +++ b/site/lib/src/pages/custom_pages.dart @@ -76,6 +76,7 @@ MemoryPage get _fweTestingPage => const MemoryPage( title: FWE Testing Page description: This is a test page for experimenting with First Week Experience (FWE) features. sitemap: false +layout: tutorial --- ## Quiz diff --git a/site/lib/src/style_hash.dart b/site/lib/src/style_hash.dart index 306c4ee4e65..5bfa059bfd2 100644 --- a/site/lib/src/style_hash.dart +++ b/site/lib/src/style_hash.dart @@ -2,4 +2,4 @@ // dart format off /// The generated hash of the `main.css` file. -const generatedStylesHash = 'VAQsUT7crZAw'; +const generatedStylesHash = 'udYDN8P9KB1z';