Skip to content

Commit d565ff4

Browse files
schultekparlough
andauthored
Add optional page navigation to top toc dropdown (#12715)
This extends the top floating table-of-contents dropdown by an optional page navigation list, for use inside FWE tutorials. <img width="369" height="786" alt="Bildschirmfoto 2025-11-18 um 14 04 38" src="https://github.com/user-attachments/assets/b85d3a07-cb01-4d39-9bce-f5f6f9828fca" /> --- Differences to the current toc: - Instead of "On this page" shows the current page title - When open, shows a list of pages (including chapters). - The current page's toc is nested under the entry of the current page (design & behavior stays the same) To test this, I also created a new `TutorialLayout` that hides the sidebar and forces the floating toc bar on all screen sizes. This should be improved in the future. --------- Co-authored-by: Parker Lougheed <[email protected]>
1 parent a8fd072 commit d565ff4

32 files changed

+552
-199
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(#site-subheader.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: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,10 @@
77
border-bottom: 0.1rem solid var(--site-outline-variant);
88

99
@media (min-width: 1200px) {
10-
box-shadow: 0 2px 4px rgba(0, 0, 0, .05);
11-
border-bottom: none;
10+
&:not(:has(~* #site-subheader.show-always)) {
11+
box-shadow: 0 2px 4px rgba(0, 0, 0, .05);
12+
border-bottom: none;
13+
}
1214
}
1315

1416
.navbar {
@@ -186,7 +188,9 @@ body.open_menu #menu-toggle span.material-symbols {
186188
border-bottom: 0.1rem solid var(--site-outline-variant);
187189
box-shadow: 0 2px 4px rgba(0, 0, 0, .05);
188190

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

site/lib/_sass/components/_pagenav.scss

Lines changed: 100 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
#pagenav {
22
flex-grow: 1;
33
min-width: 0;
4+
max-width: 100%;
45

56
>button.dropdown-button {
67
display: flex;
@@ -23,29 +24,56 @@
2324
color: var(--site-base-fgColor-alt);
2425
font-size: 20px;
2526
}
27+
28+
>.material-symbols:first-child {
29+
margin-right: 0.25rem;
30+
}
2631
}
2732

28-
.toc-intro {
33+
.toc-breadcrumb {
34+
flex-shrink: 2;
2935
white-space: nowrap;
36+
overflow: hidden;
3037

31-
.material-symbols {
32-
margin-right: 0.25rem;
38+
&.toc-hide-medium {
39+
@media (width < 576px) {
40+
display: none;
41+
}
3342
}
34-
}
3543

36-
.toc-current {
37-
display: none;
44+
&.toc-hide-small {
45+
@media (width < 420px) {
46+
display: none;
47+
}
48+
}
3849

39-
@media (min-width: 320px) {
40-
display: flex;
50+
span:not(.material-symbols) {
51+
overflow: hidden;
52+
text-overflow: ellipsis;
4153
}
4254

43-
flex-wrap: nowrap;
55+
.page-number {
56+
flex-shrink: 0;
57+
height: 1.3rem;
58+
width: 1.3rem;
59+
60+
margin-right: 0.4rem;
61+
background-color: var(--site-primary-color);
62+
color: var(--site-onPrimary-color-lightest);
63+
}
64+
}
65+
66+
.toc-current {
67+
flex-shrink: 1;
4468
white-space: nowrap;
4569
overflow: hidden;
46-
text-overflow: ellipsis;
4770

4871
color: var(--site-base-fgColor-alt);
72+
73+
span:last-child {
74+
overflow: hidden;
75+
text-overflow: ellipsis;
76+
}
4977
}
5078

5179
#pagenav-content {
@@ -64,7 +92,13 @@
6492
scrollbar-width: thin;
6593
overscroll-behavior: contain;
6694

67-
padding: 0.2rem 0.4rem;
95+
padding: 0.5rem;
96+
97+
>div {
98+
display: flex;
99+
flex-direction: column;
100+
gap: 0.25rem;
101+
}
68102

69103
@media (min-width: 420px) {
70104
border: none;
@@ -85,6 +119,7 @@
85119
text-decoration: none;
86120
display: flex;
87121
align-items: center;
122+
gap: 4px;
88123
color: var(--site-base-fgColor-alt);
89124
font-weight: 500;
90125

@@ -93,10 +128,6 @@
93128
user-select: none;
94129
}
95130

96-
span:last-child {
97-
margin-left: 3px;
98-
}
99-
100131
&:hover {
101132
color: var(--site-link-fgColor);
102133
}
@@ -109,5 +140,58 @@
109140
nav {
110141
padding: 0.6rem 0 0.8rem;
111142
}
143+
144+
.page-link {
145+
display: flex;
146+
align-items: center;
147+
gap: 0.5rem;
148+
149+
padding: 0;
150+
font-weight: 400;
151+
text-decoration: none;
152+
color: var(--site-base-fgColor);
153+
154+
&:hover {
155+
color: var(--site-link-fgColor);
156+
}
157+
158+
&.active .page-number {
159+
background-color: var(--site-primary-color);
160+
color: var(--site-onPrimary-color-lightest);
161+
}
162+
163+
&:not(.active):has(~.page-link.active) .page-number {
164+
background-color: var(--site-onPrimary-color-light);
165+
color: var(--site-primary-color);
166+
}
167+
168+
~nav {
169+
padding: 0;
170+
}
171+
}
172+
173+
.page-divider {
174+
padding: 0.25rem;
175+
font-weight: 600;
176+
color: var(--site-base-fgColor-alt);
177+
}
178+
179+
.dropdown-divider:has(~.page-link) {
180+
margin-top: 0.6rem;
181+
}
182+
}
183+
184+
.page-number {
185+
width: 25px;
186+
height: 25px;
187+
border-radius: 50%;
188+
background: var(--site-inset-borderColor);
189+
color: var(--site-base-fgColor);
190+
display: inline-flex;
191+
align-items: center;
192+
justify-content: center;
193+
font-weight: 500;
194+
195+
transition: background-color 300ms ease, color 300ms ease;
112196
}
113-
}
197+
}

site/lib/_sass/components/_tooltip.scss

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,4 +60,4 @@
6060
visibility: visible;
6161
}
6262
}
63-
}
63+
}

site/lib/jaspr_options.dart

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -190,7 +190,9 @@ Map<String, dynamic> _prefix10DartPadInjector(prefix10.DartPadInjector c) => {
190190
'runAutomatically': c.runAutomatically,
191191
};
192192
Map<String, dynamic> _prefix11PageNav(prefix11.PageNav c) => {
193-
'title': c.title,
193+
'breadcrumbs': c.breadcrumbs,
194+
'pageNumber': c.pageNumber,
195+
'initialHeading': c.initialHeading,
194196
'content': c.content.toId(),
195197
};
196198
Map<String, dynamic> _prefix15ArchiveTable(prefix15.ArchiveTable c) => {

site/lib/main.dart

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ import 'src/components/util/component_ref.dart';
3232
import 'src/extensions/registry.dart';
3333
import 'src/layouts/doc_layout.dart';
3434
import 'src/layouts/toc_layout.dart';
35+
import 'src/layouts/tutorial_layout.dart';
3536
import 'src/loaders/data_processor.dart';
3637
import 'src/markdown/markdown_parser.dart';
3738
import 'src/pages/custom_pages.dart';
@@ -72,7 +73,11 @@ Component get _docsFlutterDevSite => ContentApp.custom(
7273
rawOutputPattern: _passThroughPattern,
7374
extensions: allNodeProcessingExtensions,
7475
components: _embeddableComponents,
75-
layouts: const [DocLayout(), TocLayout()],
76+
layouts: const [
77+
DocLayout(),
78+
TocLayout(),
79+
TutorialLayout(),
80+
],
7681
theme: const ContentTheme.none(),
7782
secondaryOutputs: [
7883
const RobotsTxtOutput(),

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

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,16 @@
55
import 'package:jaspr/jaspr.dart';
66

77
import '../../markdown/markdown_parser.dart';
8+
import '../../models/page_navigation_model.dart';
89
import 'material_icon.dart';
910

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

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

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

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

3940
@override

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

Lines changed: 71 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -7,19 +7,24 @@ import 'package:universal_web/js_interop.dart';
77
import 'package:universal_web/web.dart' as web;
88

99
import '../../../client/global_scripts.dart';
10+
import '../../../util.dart';
1011
import '../../common/dropdown.dart';
1112
import '../../common/material_icon.dart';
1213
import '../../util/component_ref.dart';
1314

1415
@client
1516
class PageNav extends StatefulComponent {
1617
const PageNav({
17-
required this.title,
18+
this.breadcrumbs = const [],
19+
this.pageNumber,
20+
required this.initialHeading,
1821
required this.content,
1922
super.key,
2023
});
2124

22-
final String title;
25+
final List<String> breadcrumbs;
26+
final int? pageNumber;
27+
final String initialHeading;
2328
final ComponentRef content;
2429

2530
@override
@@ -63,21 +68,45 @@ class _PageNavState extends State<PageNav> {
6368
'aria-label': 'Toggle the table of contents dropdown',
6469
},
6570
[
66-
span(classes: 'toc-intro', [
67-
const MaterialIcon('list'),
68-
span(
69-
attributes: {'aria-label': 'On this page'},
70-
[
71-
text('On this page'),
72-
],
73-
),
74-
]),
71+
const MaterialIcon('list'),
72+
if (component.breadcrumbs.isEmpty)
73+
span(classes: 'toc-breadcrumb', [
74+
span(
75+
attributes: {'aria-label': 'On this page'},
76+
[text('On this page')],
77+
),
78+
const MaterialIcon('chevron_right'),
79+
])
80+
else ...[
81+
for (final (index, crumb) in component.breadcrumbs.indexed) ...[
82+
span(
83+
classes: [
84+
'toc-breadcrumb',
85+
if (index < component.breadcrumbs.length - 2)
86+
'toc-hide-medium',
87+
if (index < component.breadcrumbs.length - 1)
88+
'toc-hide-small',
89+
].toClasses,
90+
[
91+
if (index == component.breadcrumbs.length - 1 &&
92+
component.pageNumber != null)
93+
span(classes: 'page-number', [
94+
text('${component.pageNumber}'),
95+
]),
96+
span([
97+
_simpleInlineMarkdown(crumb),
98+
]),
99+
const MaterialIcon('chevron_right'),
100+
],
101+
),
102+
],
103+
],
104+
75105
span(classes: 'toc-current', [
76-
const MaterialIcon('chevron_right'),
77106
ValueListenableBuilder(
78107
listenable: currentPageHeading,
79108
builder: (context, value) {
80-
return span([text(value ?? component.title)]);
109+
return span([text(value ?? component.initialHeading)]);
81110
},
82111
),
83112
]),
@@ -86,4 +115,33 @@ class _PageNavState extends State<PageNav> {
86115
content: component.content.component,
87116
);
88117
}
118+
119+
/// Simple (and incomplete) implementation of inline markdown parsing
120+
/// for use on the client.
121+
Component _simpleInlineMarkdown(String content) {
122+
final syntaxRegex = RegExp(r'`([^`]+)`|\*([^*]+)\*|\*\*([^*]+)\*\*');
123+
124+
final components = <Component>[];
125+
126+
var current = 0;
127+
final matches = syntaxRegex.allMatches(content);
128+
129+
for (final match in matches) {
130+
if (match.start > current) {
131+
components.add(text(content.substring(current, match.start)));
132+
}
133+
if (match.group(1) != null) {
134+
components.add(code([text(match.group(1)!)]));
135+
} else if (match.group(2) != null) {
136+
components.add(em([text(match.group(2)!)]));
137+
} else if (match.group(3) != null) {
138+
components.add(strong([text(match.group(3)!)]));
139+
}
140+
current = match.end;
141+
}
142+
if (current < content.length) {
143+
components.add(text(content.substring(current)));
144+
}
145+
return components.length > 1 ? fragment(components) : components.first;
146+
}
89147
}

0 commit comments

Comments
 (0)