Skip to content

Commit 56e6529

Browse files
committed
Add optional page navigation to top toc dropdown
1 parent 97017b3 commit 56e6529

File tree

13 files changed

+380
-166
lines changed

13 files changed

+380
-166
lines changed

site/lib/_sass/base/_base.scss

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,10 @@ body {
88
color: var(--site-base-fgColor);
99

1010
// The top TOC is not shown on narrow screens.
11-
@media (min-width: 1200px) {
12-
--site-subheader-height: 0rem;
11+
&:not(:has(#toc-top.show-always)) {
12+
@media (min-width: 1200px) {
13+
--site-subheader-height: 0rem;
14+
}
1315
}
1416

1517
// If the TOC is disabled, reduce the subheader height to

site/lib/_sass/components/_header.scss

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -186,7 +186,9 @@ body.open_menu #menu-toggle span.material-symbols {
186186
border-bottom: 0.1rem solid var(--site-outline-variant);
187187
box-shadow: 0 2px 4px rgba(0, 0, 0, .05);
188188

189-
@media (width < 240px), (width >= 1200px) {
190-
display: none;
189+
&:not(.show-always) {
190+
@media (width < 240px), (width >= 1200px) {
191+
display: none;
192+
}
191193
}
192194
}

site/lib/_sass/components/_pagenav.scss

Lines changed: 47 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,7 @@
8585
text-decoration: none;
8686
display: flex;
8787
align-items: center;
88+
gap: 4px;
8889
color: var(--site-base-fgColor-alt);
8990
font-weight: 500;
9091

@@ -93,10 +94,6 @@
9394
user-select: none;
9495
}
9596

96-
span:last-child {
97-
margin-left: 3px;
98-
}
99-
10097
&:hover {
10198
color: var(--site-link-fgColor);
10299
}
@@ -109,5 +106,51 @@
109106
nav {
110107
padding: 0.6rem 0 0.8rem;
111108
}
109+
110+
.page-link {
111+
padding: 0;
112+
font-weight: 400;
113+
color: var(--site-base-fgColor);
114+
margin-top: 0.6rem;
115+
116+
.page-number {
117+
width: 25px;
118+
height: 25px;
119+
border-radius: 50%;
120+
background: var(--site-raised-bgColor);
121+
color: var(--site-base-fgColor);
122+
display: flex;
123+
align-items: center;
124+
justify-content: center;
125+
font-weight: 500;
126+
127+
transition: background-color 300ms ease, color 300ms ease;
128+
}
129+
130+
&.active .page-number {
131+
background-color: var(--site-primary-color);
132+
color: var(--site-onPrimary-color-lightest);
133+
}
134+
135+
&:not(.active):has(~.page-link.active) .page-number {
136+
background-color: var(--site-onPrimary-color-light);
137+
color: var(--site-primary-color);
138+
}
139+
140+
~ nav {
141+
padding: 0;
142+
}
143+
}
144+
145+
.page-divider {
146+
padding-left: 0.25rem;
147+
padding-top: 0.25rem;
148+
font-weight: 600;
149+
color: var(--site-base-fgColor-alt);
150+
}
151+
152+
.dropdown-divider:has(~.page-link) {
153+
margin-top: 0.6rem;
154+
}
112155
}
113156
}

site/lib/main.dart

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import 'src/extensions/registry.dart';
2626
import 'src/layouts/catalog_page_layout.dart';
2727
import 'src/layouts/doc_layout.dart';
2828
import 'src/layouts/toc_layout.dart';
29+
import 'src/layouts/tutorial_layout.dart';
2930
import 'src/loaders/data_processor.dart';
3031
import 'src/markdown/markdown_parser.dart';
3132
import 'src/pages/custom_pages.dart';
@@ -66,7 +67,12 @@ Component get _docsFlutterDevSite => ContentApp.custom(
6667
rawOutputPattern: _passThroughPattern,
6768
extensions: allNodeProcessingExtensions,
6869
components: _embeddableComponents,
69-
layouts: const [DocLayout(), TocLayout(), CatalogPageLayout()],
70+
layouts: const [
71+
DocLayout(),
72+
TocLayout(),
73+
CatalogPageLayout(),
74+
TutorialLayout(),
75+
],
7076
theme: const ContentTheme.none(),
7177
secondaryOutputs: [
7278
const RobotsTxtOutput(),

site/lib/src/components/common/prev_next.dart

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,16 @@
44

55
import 'package:jaspr/jaspr.dart';
66

7+
import '../../models/page_navigation_model.dart';
78
import 'material_icon.dart';
89

910
/// Previous and next page buttons to display at the end of a page
1011
/// in a connected series of pages, such as the language docs.
1112
class PrevNext extends StatelessComponent {
1213
const PrevNext({super.key, this.previousPage, this.nextPage});
1314

14-
final ({String url, String title})? previousPage;
15-
final ({String url, String title})? nextPage;
15+
final PageNavigationEntry? previousPage;
16+
final PageNavigationEntry? nextPage;
1617

1718
@override
1819
Component build(BuildContext context) {
@@ -32,7 +33,7 @@ class PrevNext extends StatelessComponent {
3233
class _PrevNextCard extends StatelessComponent {
3334
const _PrevNextCard({required this.page, required this.isPrevious});
3435

35-
final ({String url, String title}) page;
36+
final PageNavigationEntry page;
3637
final bool isPrevious;
3738

3839
@override

site/lib/src/components/layout/client/pagenav.dart

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,13 @@ import '../../util/component_ref.dart';
1414
@client
1515
class PageNav extends StatefulComponent {
1616
const PageNav({
17+
this.label,
1718
required this.title,
1819
required this.content,
1920
super.key,
2021
});
2122

23+
final String? label;
2224
final String title;
2325
final ComponentRef content;
2426

@@ -66,9 +68,9 @@ class _PageNavState extends State<PageNav> {
6668
span(classes: 'toc-intro', [
6769
const MaterialIcon('list'),
6870
span(
69-
attributes: {'aria-label': 'On this page'},
71+
attributes: {'aria-label': component.label ?? 'On this page'},
7072
[
71-
text('On this page'),
73+
text(component.label ?? 'On this page'),
7274
],
7375
),
7476
]),

site/lib/src/components/layout/header.dart

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
// found in the LICENSE file.
44

55
import 'package:jaspr/jaspr.dart';
6+
import 'package:jaspr_content/jaspr_content.dart';
67

78
import '../common/button.dart';
89
import '../common/material_icon.dart';
@@ -84,7 +85,7 @@ class DashHeader extends StatelessComponent {
8485
content: 'Get started',
8586
href: '/get-started/quick',
8687
),
87-
const MenuToggle(),
88+
if (context.page.data['sidenav'] != null) const MenuToggle(),
8889
],
8990
),
9091
]),

site/lib/src/components/layout/toc.dart

Lines changed: 82 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,10 @@
33
// found in the LICENSE file.
44

55
import 'package:jaspr/jaspr.dart';
6+
import 'package:jaspr_content/jaspr_content.dart';
67

7-
import '../../models/on_this_page_model.dart';
8+
import '../../models/page_navigation_model.dart';
9+
import '../../util.dart';
810
import '../common/client/on_this_page_button.dart';
911
import '../common/material_icon.dart';
1012
import '../util/component_ref.dart';
@@ -13,7 +15,7 @@ import 'client/pagenav.dart';
1315
final class DashTableOfContents extends StatelessComponent {
1416
const DashTableOfContents(this.data);
1517

16-
final OnThisPageData data;
18+
final TocNavigationData data;
1719

1820
@override
1921
Component build(BuildContext _) {
@@ -22,54 +24,102 @@ final class DashTableOfContents extends StatelessComponent {
2224
_TocContents(data),
2325
]);
2426
}
27+
}
2528

26-
static Component asDropdown(
27-
OnThisPageData data, {
28-
required String currentTitle,
29-
}) {
30-
return Builder(
31-
builder: (context) {
32-
return PageNav(
33-
title: currentTitle,
34-
content: context.ref(
35-
div([
36-
a(
37-
href: '#site-content-title',
38-
id: 'return-to-top',
39-
[
40-
const MaterialIcon('vertical_align_top'),
41-
span([text(currentTitle)]),
42-
],
43-
),
44-
div(
45-
classes: 'dropdown-divider',
46-
attributes: {'aria-hidden': 'true', 'role': 'separator'},
47-
[],
48-
),
29+
final class PageNavBar extends StatelessComponent {
30+
const PageNavBar(this.data);
31+
32+
final PageNavigationData data;
33+
34+
@override
35+
Component build(BuildContext context) {
36+
final currentLinkedPage = data.pageEntries
37+
.where((page) => page.url == context.page.url)
38+
.firstOrNull;
39+
40+
final linkedPageTitle = currentLinkedPage?.title;
41+
final currentTitle = context.page.data.page['title'] as String;
42+
43+
var pageEntryNumber = 1;
44+
45+
return PageNav(
46+
label: linkedPageTitle,
47+
title: currentTitle,
48+
content: context.ref(
49+
div([
50+
if (data.pageEntries.isEmpty) ...[
51+
a(
52+
href: '#site-content-title',
53+
id: 'return-to-top',
54+
[
55+
const MaterialIcon('vertical_align_top'),
56+
span([text(currentTitle)]),
57+
],
58+
),
59+
div(
60+
classes: 'dropdown-divider',
61+
attributes: {'aria-hidden': 'true', 'role': 'separator'},
62+
[],
63+
),
64+
if (data.toc != null)
4965
nav(
5066
attributes: {'role': 'menu'},
51-
[_TocContents(data)],
67+
[_TocContents(data.toc!)],
5268
),
53-
]),
54-
),
55-
);
56-
},
69+
] else ...[
70+
for (final page in data.pageEntries) ...[
71+
if (!page.isDivider) ...[
72+
a(
73+
classes: [
74+
'page-link',
75+
if (page == currentLinkedPage) 'active',
76+
].toClasses,
77+
href: page.url,
78+
attributes: {'role': 'menuitem'},
79+
[
80+
span(classes: 'page-number', [
81+
text('${pageEntryNumber++}'),
82+
]),
83+
text(page.title),
84+
],
85+
),
86+
if (currentLinkedPage == page && data.toc != null)
87+
nav(
88+
attributes: {'role': 'menu'},
89+
[_TocContents(data.toc!)],
90+
),
91+
] else ...[
92+
if (page != data.pageEntries.first)
93+
div(
94+
classes: 'dropdown-divider',
95+
attributes: {'aria-hidden': 'true', 'role': 'separator'},
96+
[],
97+
),
98+
div(
99+
classes: 'page-divider',
100+
[text(page.title)],
101+
),
102+
],
103+
],
104+
],
105+
]),
106+
),
57107
);
58108
}
59109
}
60110

61111
final class _TocContents extends StatelessComponent {
62112
const _TocContents(this.data);
63113

64-
final OnThisPageData data;
114+
final TocNavigationData data;
65115

66116
@override
67117
Component build(BuildContext _) => ul(
68118
classes: 'toc-list',
69119
_buildEntries(data.topLevelEntries, 0),
70120
);
71121

72-
List<Component> _buildEntries(List<OnThisPageEntry> entries, int depth) {
122+
List<Component> _buildEntries(List<TocNavigationEntry> entries, int depth) {
73123
final nextDepth = depth + 1;
74124

75125
return [

0 commit comments

Comments
 (0)