@janga/norna 0.7.23 → 0.7.25

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (82) hide show
  1. package/README.md +14 -18
  2. package/astro.config.mjs +14 -0
  3. package/bin/norna-cli.mjs +6 -0
  4. package/package.json +25 -2
  5. package/schemas/category.schema.json +2 -2
  6. package/schemas/config.schema.json +80 -15
  7. package/schemas/content-frontmatter.schema.json +7 -7
  8. package/schemas/page-theme.schema.json +26 -26
  9. package/schemas/sitewide-content.schema.json +18 -18
  10. package/schemas/theme.schema.json +246 -246
  11. package/scripts/build-site.mjs +1 -0
  12. package/scripts/check-config.mjs +16 -1
  13. package/scripts/dev-local.mjs +106 -11
  14. package/scripts/generate-search-index.mjs +57 -0
  15. package/scripts/init-site.mjs +3 -0
  16. package/scripts/lib/code-fence-metadata.mjs +228 -0
  17. package/scripts/lib/edit-source-link.mjs +102 -0
  18. package/scripts/lib/editor-language-service.mjs +32 -12
  19. package/scripts/lib/image-presentation.mjs +4 -0
  20. package/scripts/lib/navigation-model.mjs +34 -13
  21. package/scripts/lib/navigation-review.mjs +396 -0
  22. package/scripts/lib/norna-markdown-blocks.mjs +164 -26
  23. package/scripts/lib/norna-markdown-render-plugin.mjs +64 -1
  24. package/scripts/lib/page-aliases.mjs +21 -1
  25. package/scripts/lib/page-markdown.mjs +32 -1
  26. package/scripts/lib/page-move-plan.mjs +659 -0
  27. package/scripts/lib/presentation-palette-metadata.mjs +1 -1
  28. package/scripts/lib/presentation.mjs +1 -10
  29. package/scripts/lib/project-config.mjs +141 -5
  30. package/scripts/lib/public-asset-conventions.mjs +36 -2
  31. package/scripts/lib/schema-definitions.mjs +30 -5
  32. package/scripts/lib/schema-editor-metadata.mjs +36 -4
  33. package/scripts/lib/schema-value-definitions.mjs +4 -1
  34. package/scripts/lib/semantic-callouts.mjs +128 -0
  35. package/scripts/lib/site-content.mjs +2 -1
  36. package/scripts/lib/site-link-graph.mjs +33 -2
  37. package/scripts/lib/site-navigation-tree.mjs +85 -0
  38. package/scripts/lib/site-paths.mjs +14 -2
  39. package/scripts/lib/social-image-assets.mjs +30 -0
  40. package/scripts/lib/table-render-plugin.mjs +33 -0
  41. package/scripts/lib/theme-presets.mjs +2 -2
  42. package/scripts/lib/theme-profiles.mjs +0 -4
  43. package/scripts/move-site-page.mjs +256 -0
  44. package/scripts/review-navigation.mjs +32 -0
  45. package/scripts/sync-content-sections.mjs +67 -4
  46. package/scripts/sync-site-public.mjs +24 -5
  47. package/src/components/CardList.astro +1 -0
  48. package/src/components/CodeBlockCopyScript.astro +11 -5
  49. package/src/components/EditSourceLink.astro +17 -0
  50. package/src/components/ImageCarousel.astro +9 -7
  51. package/src/components/ImageStack.astro +34 -9
  52. package/src/components/ImageStackEnhancement.astro +331 -0
  53. package/src/components/NavigationPageTree.astro +66 -55
  54. package/src/components/NavigationTreeControls.astro +77 -0
  55. package/src/components/PageAliasRedirect.astro +1 -1
  56. package/src/components/PageContentsNavigation.astro +2 -3
  57. package/src/components/PageList.astro +33 -0
  58. package/src/components/PageSequenceNavigation.astro +37 -0
  59. package/src/components/SearchPage.astro +151 -0
  60. package/src/components/SectionNavigationScript.astro +45 -7
  61. package/src/components/SiteNavigation.astro +102 -51
  62. package/src/components/SitePage.astro +107 -39
  63. package/src/components/SiteSection.astro +11 -3
  64. package/src/components/SiteTreeNavigation.astro +11 -1
  65. package/src/components/TableOverflowScript.astro +251 -0
  66. package/src/components/TreeNavigationScript.astro +271 -34
  67. package/src/layouts/BaseLayout.astro +83 -2
  68. package/src/lib/generatedImages.ts +18 -0
  69. package/src/lib/sectionContent.ts +36 -30
  70. package/src/lib/siteNavigation.ts +31 -52
  71. package/src/lib/sitePages.ts +6 -3
  72. package/src/lib/sitePublicAssets.ts +1 -0
  73. package/src/pages/404.astro +110 -0
  74. package/src/pages/[...slug].astro +15 -5
  75. package/src/styles/content.css +538 -39
  76. package/src/styles/media.css +187 -8
  77. package/src/styles/navigation.css +199 -3
  78. package/src/styles/page-layout.css +481 -100
  79. package/src/styles/responsive.css +144 -28
  80. package/starters/basic/README.md +6 -19
  81. package/starters/basic/package.json +1 -0
  82. package/starters/basic/site/pages/000-home/content.md +13 -50
@@ -0,0 +1,37 @@
1
+ ---
2
+ import { ArrowLeft, ArrowRight } from '@lucide/astro';
3
+ import projectConfig from '../../scripts/lib/project-config.mjs';
4
+ import { withBasePath } from '../lib/basePath';
5
+ import type { SitePage } from '../lib/sitePages';
6
+
7
+ interface Props {
8
+ previous: SitePage | null;
9
+ next: SitePage | null;
10
+ }
11
+
12
+ const { previous, next } = Astro.props;
13
+ const pageHref = (page: SitePage) => withBasePath(projectConfig.site.basePath, page.pathname);
14
+ ---
15
+
16
+ {(previous || next) && (
17
+ <nav class="page-sequence-navigation" aria-label={projectConfig.locale.labels.pageSequence}>
18
+ {previous ? (
19
+ <a class="page-sequence-link page-sequence-previous" href={pageHref(previous)} rel="prev">
20
+ <ArrowLeft aria-hidden="true" />
21
+ <span>
22
+ <span class="page-sequence-direction">{projectConfig.locale.labels.previousPage}</span>
23
+ <span class="page-sequence-title">{previous.title}</span>
24
+ </span>
25
+ </a>
26
+ ) : <span class="page-sequence-empty" aria-hidden="true"></span>}
27
+ {next ? (
28
+ <a class="page-sequence-link page-sequence-next" href={pageHref(next)} rel="next">
29
+ <span>
30
+ <span class="page-sequence-direction">{projectConfig.locale.labels.nextPage}</span>
31
+ <span class="page-sequence-title">{next.title}</span>
32
+ </span>
33
+ <ArrowRight aria-hidden="true" />
34
+ </a>
35
+ ) : <span class="page-sequence-empty" aria-hidden="true"></span>}
36
+ </nav>
37
+ )}
@@ -0,0 +1,151 @@
1
+ ---
2
+ import { getCollection } from 'astro:content';
3
+ import { resolveNavigationModel } from '../../scripts/lib/navigation-model.mjs';
4
+ import { getTextWidthCssValue, resolveFrameColors, resolvePagePresentation } from '../../scripts/lib/presentation.mjs';
5
+ import { projectConfig, resolveThemeVisualConfig } from '../../scripts/lib/project-config.mjs';
6
+ import SiteBanners from './SiteBanners.astro';
7
+ import SiteNavigation from './SiteNavigation.astro';
8
+ import SiteSection from './SiteSection.astro';
9
+ import BaseLayout from '../layouts/BaseLayout.astro';
10
+ import type { ResolvedSection } from '../lib/sectionContent';
11
+ import { flattenSiteNavigationTree, getListedSiteNavigationTree } from '../lib/siteNavigation';
12
+ import { getSiteModel, type SitePage } from '../lib/sitePages';
13
+ import { getLogoAsset } from '../lib/sitePublicAssets';
14
+ import { getToday, isVisibleToday } from '../lib/visibility';
15
+ import { withBasePath } from '../lib/basePath';
16
+
17
+ const siteDirectory = (process.env.NORNA_SITE_DIR ?? 'site').trim();
18
+ const siteModel = await getSiteModel(await getCollection('site'));
19
+ const { nodes: siteNodes, pages: sitePages } = siteModel;
20
+ const homePage = sitePages.find((page) => page.isHome);
21
+ if (!homePage) throw new Error('The required homepage is missing from the site page model.');
22
+
23
+ const [siteTheme] = await getCollection('theme');
24
+ if (!siteTheme) throw new Error('site/theme.yaml is required. Add a root theme before building the site.');
25
+ const [sitewide] = await getCollection('sitewide');
26
+ const sitewideData = sitewide?.data ?? {};
27
+ const siteThemeData = siteTheme.data;
28
+ const themeLabel = `${siteDirectory || 'site'}/${siteTheme.id}`;
29
+ const visualTheme = resolveThemeVisualConfig(siteThemeData, themeLabel);
30
+ const getPageContent = (page: SitePage) => page.markdownDocument.navigationHeadings.map((heading) => ({ ...heading }));
31
+ const siteNavigation = siteNodes.map((node) => {
32
+ const headings = node.kind === 'page' ? getPageContent(node) : [];
33
+ return {
34
+ node,
35
+ headings,
36
+ sections: headings
37
+ .filter((heading) => heading.depth === 2)
38
+ .map(({ id, title }) => ({ id, title })),
39
+ };
40
+ });
41
+ const renderedNavigationEntries = flattenSiteNavigationTree(getListedSiteNavigationTree(siteNavigation));
42
+ const navigationModel = resolveNavigationModel({
43
+ mode: projectConfig.navigation.mode,
44
+ nodes: renderedNavigationEntries.map(({ node, headings }) => ({
45
+ depth: Math.max(1, node.depth),
46
+ headings,
47
+ isHome: node.isHome,
48
+ kind: node.kind,
49
+ listed: node.navigation.listed,
50
+ pagePath: node.pagePath,
51
+ })),
52
+ });
53
+ const pagePresentation = resolvePagePresentation(siteThemeData, themeLabel, {
54
+ navigationMode: navigationModel.mode,
55
+ });
56
+ const pageTextWidth = getTextWidthCssValue(pagePresentation.typography.values.body.width);
57
+ const frameColors = resolveFrameColors({ theme: siteThemeData, sourceLabel: themeLabel });
58
+ const logoAsset = getLogoAsset();
59
+ const logo = logoAsset
60
+ ? {
61
+ src: logoAsset.href,
62
+ alt: homePage.title,
63
+ height: sitewideData.logo?.height,
64
+ }
65
+ : undefined;
66
+ const today = getToday();
67
+ const siteBanners = (sitewideData.banners ?? []).filter((banner) => isVisibleToday(banner.visible, today));
68
+ const labels = projectConfig.locale.labels;
69
+ const searchSection: ResolvedSection = {
70
+ id: null,
71
+ title: labels.search,
72
+ titleHtml: labels.search,
73
+ headingLevel: 1,
74
+ contentBlocks: [{
75
+ type: 'html',
76
+ html: `<p>${labels.searchDescription}</p><div class="norna-search" id="norna-search"><p data-search-status>${labels.searchLoading}</p><noscript><p>${labels.searchNoScript}</p></noscript></div>`,
77
+ }],
78
+ };
79
+ const hasNavigationRailFrame = navigationModel.requestedMode === 'tree'
80
+ || (
81
+ navigationModel.requestedMode === 'automatic'
82
+ && (navigationModel.hasNestedPages || navigationModel.hasCategories)
83
+ );
84
+ const bundlePath = withBasePath(projectConfig.site.basePath, '/pagefind/');
85
+ const searchScript = `(() => {
86
+ const root = document.querySelector('#norna-search');
87
+ const status = root?.querySelector('[data-search-status]');
88
+ if (!root) return;
89
+ const script = document.createElement('script');
90
+ script.src = ${JSON.stringify(`${bundlePath}pagefind-ui.js`)};
91
+ script.onload = () => {
92
+ status?.remove();
93
+ new window.PagefindUI({
94
+ element: '#norna-search',
95
+ bundlePath: ${JSON.stringify(bundlePath)},
96
+ baseUrl: ${JSON.stringify(projectConfig.site.basePath)},
97
+ showImages: false,
98
+ showSubResults: true
99
+ });
100
+ const labelInput = () => {
101
+ const input = root.querySelector('.pagefind-ui__search-input');
102
+ if (!input) return false;
103
+ input.setAttribute('aria-label', ${JSON.stringify(labels.search)});
104
+ return true;
105
+ };
106
+ if (!labelInput()) {
107
+ const observer = new MutationObserver(() => {
108
+ if (labelInput()) observer.disconnect();
109
+ });
110
+ observer.observe(root, { childList: true, subtree: true });
111
+ }
112
+ };
113
+ script.onerror = () => {
114
+ if (status) status.textContent = ${JSON.stringify(labels.searchUnavailable)};
115
+ };
116
+ document.head.append(script);
117
+ })();`;
118
+ ---
119
+
120
+ <BaseLayout
121
+ title={labels.search}
122
+ description={labels.searchDescription}
123
+ presentation={pagePresentation}
124
+ visualTheme={visualTheme}
125
+ frameColors={frameColors}
126
+ pathname="/search/"
127
+ footer={sitewideData.footer}
128
+ indexable={false}
129
+ >
130
+ <link slot="head" rel="stylesheet" href={`${bundlePath}pagefind-ui.css`} />
131
+ <SiteNavigation
132
+ logo={logo}
133
+ navigationEntries={siteNavigation}
134
+ navigationMode={navigationModel.mode}
135
+ showTopNavigationSubmenus={navigationModel.requestedMode !== 'automatic' && navigationModel.mode !== 'tree'}
136
+ hasNavigationRailFrame={hasNavigationRailFrame}
137
+ frameColors={frameColors}
138
+ readerPreferences={pagePresentation.readerPreferences}
139
+ searchPage
140
+ />
141
+ {siteBanners.length > 0 && <SiteBanners banners={siteBanners} />}
142
+ <main class="site-content search-page-content" id="main-content" style={`--page-text-width: ${pageTextWidth}`}>
143
+ <SiteSection
144
+ section={searchSection}
145
+ pagePresentation={pagePresentation}
146
+ visualTheme={visualTheme}
147
+ sectionIndex={0}
148
+ />
149
+ </main>
150
+ <script is:inline set:html={searchScript} />
151
+ </BaseLayout>
@@ -7,6 +7,8 @@
7
7
  const submenuItems = Array.from(document.querySelectorAll<HTMLElement>('.site-nav-item-has-submenu'));
8
8
  const mobileDestinationLinks = Array.from(mobileMenu?.querySelectorAll<HTMLAnchorElement>('.mobile-nav-destination') ?? []);
9
9
  const mobileMenuSummary = mobileMenu?.querySelector<HTMLElement>(':scope > summary');
10
+ const mobileMenuPanel = mobileMenu?.querySelector<HTMLElement>('[data-compact-navigation-panel]');
11
+ const mobileMenuClose = mobileMenu?.querySelector<HTMLButtonElement>('[data-compact-navigation-close]');
10
12
  const sectionTrackingEnabled = document.documentElement.dataset.sectionTracking === 'enabled';
11
13
  const trackedHeadingIds = new Set(sectionLinks.flatMap((link) => {
12
14
  const linkUrl = new URL(link.href, window.location.href);
@@ -158,17 +160,54 @@
158
160
  });
159
161
 
160
162
  if (mobileMenu) {
163
+ const siteHeader = mobileMenu.closest<HTMLElement>('.site-top');
164
+ const navigationRow = mobileMenu.closest<HTMLElement>('.site-nav-row');
165
+ const navigationActions = mobileMenu.closest<HTMLElement>('.site-nav-actions');
166
+ const backgroundTargets = new Set<HTMLElement>([
167
+ ...Array.from(document.body.children)
168
+ .filter((element): element is HTMLElement => element instanceof HTMLElement && element !== siteHeader),
169
+ ...Array.from(siteHeader?.children ?? [])
170
+ .filter((element): element is HTMLElement => element instanceof HTMLElement && element !== navigationRow),
171
+ ...Array.from(navigationRow?.children ?? [])
172
+ .filter((element): element is HTMLElement => element instanceof HTMLElement && element !== navigationActions),
173
+ ...Array.from(navigationActions?.children ?? [])
174
+ .filter((element): element is HTMLElement => element instanceof HTMLElement && element !== mobileMenu),
175
+ ]);
176
+ const previousInertState = new Map<HTMLElement, boolean>();
177
+ const setBackgroundInert = (inert: boolean) => {
178
+ if (inert) {
179
+ backgroundTargets.forEach((element) => {
180
+ if (!previousInertState.has(element)) {
181
+ previousInertState.set(element, element.hasAttribute('inert'));
182
+ }
183
+ element.setAttribute('inert', '');
184
+ });
185
+ return;
186
+ }
187
+
188
+ previousInertState.forEach((wasInert, element) => {
189
+ if (!wasInert) element.removeAttribute('inert');
190
+ });
191
+ previousInertState.clear();
192
+ };
193
+ const closeMenu = (restoreFocus: boolean) => {
194
+ mobileMenu.open = false;
195
+ if (restoreFocus) mobileMenuSummary?.focus();
196
+ };
197
+
161
198
  mobileDestinationLinks.forEach((link) => {
162
199
  link.addEventListener('click', () => {
163
- mobileMenu.open = false;
200
+ closeMenu(false);
164
201
  });
165
202
  });
203
+ mobileMenuClose?.addEventListener('click', () => closeMenu(true));
166
204
 
167
205
  mobileMenu.addEventListener('toggle', () => {
168
206
  document.documentElement.classList.toggle('mobile-navigation-open', mobileMenu.open);
207
+ setBackgroundInert(mobileMenu.open);
169
208
  if (mobileMenu.open) {
170
209
  window.requestAnimationFrame(() => (
171
- mobileMenu.querySelector<HTMLElement>('.mobile-nav-view h2, .mobile-nav-view a')?.focus()
210
+ mobileMenuClose?.focus()
172
211
  ));
173
212
  }
174
213
  });
@@ -178,16 +217,15 @@
178
217
 
179
218
  if (event.key === 'Escape') {
180
219
  event.preventDefault();
181
- mobileMenu.open = false;
182
- mobileMenuSummary?.focus();
220
+ closeMenu(true);
183
221
  return;
184
222
  }
185
223
 
186
224
  if (event.key !== 'Tab') return;
187
225
 
188
- const focusableElements = Array.from(mobileMenu.querySelectorAll<HTMLElement>(
189
- 'summary, a[href]:not([hidden]), [tabindex]:not([tabindex="-1"]):not([hidden])',
190
- )).filter((element) => element.getClientRects().length > 0);
226
+ const focusableElements = Array.from(mobileMenuPanel?.querySelectorAll<HTMLElement>(
227
+ 'a[href]:not([hidden]), button:not([disabled]):not([hidden]), input:not([disabled]):not([hidden]), summary, [tabindex]:not([tabindex="-1"]):not([hidden])',
228
+ ) ?? []).filter((element) => element.getClientRects().length > 0);
191
229
  const firstFocusable = focusableElements[0];
192
230
  const lastFocusable = focusableElements.at(-1);
193
231
  if (!firstFocusable || !lastFocusable) return;
@@ -1,4 +1,5 @@
1
1
  ---
2
+ import { Menu, Search, X } from '@lucide/astro';
2
3
  import projectConfig from '../../scripts/lib/project-config.mjs';
3
4
  import { withBasePath } from '../lib/basePath';
4
5
  import type { SectionNavigation } from '../lib/sectionContent';
@@ -11,6 +12,7 @@ import {
11
12
  import type { SiteNode, SitePage } from '../lib/sitePages';
12
13
  import DisplaySettings from './DisplaySettings.astro';
13
14
  import NavigationPageTree from './NavigationPageTree.astro';
15
+ import NavigationTreeControls from './NavigationTreeControls.astro';
14
16
  import SectionNavigationScript from './SectionNavigationScript.astro';
15
17
 
16
18
  type FrameColors = {
@@ -24,7 +26,7 @@ interface Props {
24
26
  alt: string;
25
27
  height?: string;
26
28
  };
27
- currentPage: SitePage;
29
+ currentPage?: SitePage;
28
30
  navigationEntries: SiteNavigationEntry[];
29
31
  navigationMode: 'sections' | 'top' | 'tree';
30
32
  showTopNavigationSubmenus: boolean;
@@ -42,6 +44,7 @@ interface Props {
42
44
  focusReading: 'off' | 'on';
43
45
  };
44
46
  };
47
+ searchPage?: boolean;
45
48
  }
46
49
 
47
50
  const {
@@ -53,6 +56,7 @@ const {
53
56
  hasNavigationRailFrame,
54
57
  frameColors,
55
58
  readerPreferences,
59
+ searchPage = false,
56
60
  } = Astro.props;
57
61
  const siteTopBackgroundColor = frameColors.backgroundColor === 'transparent'
58
62
  ? 'var(--color-page)'
@@ -67,10 +71,15 @@ const topLevelNavigation = siteNavigationTree;
67
71
  const showSiteNavigation = topLevelNavigation.some(({ node }) => !node.isHome);
68
72
  const homeNode = navigationEntries.find(({ node }) => node.isHome)?.node;
69
73
  const homePage = homeNode?.kind === 'page' ? homeNode : currentPage;
70
- const currentPageNavigation = navigationEntries.find(({ node }) => node.kind === 'page' && node.pathname === currentPage.pathname);
71
- const isCurrentPage = (page: SitePage) => page.pathname === currentPage.pathname;
72
- const isCurrentBranch = (node: SiteNode) => currentPage.pageDirectory === node.pageDirectory
73
- || currentPage.pageDirectory.startsWith(`${node.pageDirectory}/pages/`);
74
+ if (!homePage) throw new Error('Site navigation requires a homepage.');
75
+ const currentPageNavigation = currentPage
76
+ ? navigationEntries.find(({ node }) => node.kind === 'page' && node.pathname === currentPage.pathname)
77
+ : undefined;
78
+ const isCurrentPage = (page: SitePage) => currentPage?.pathname === page.pathname;
79
+ const isCurrentBranch = (node: SiteNode) => Boolean(currentPage && (
80
+ currentPage.pageDirectory === node.pageDirectory
81
+ || currentPage.pageDirectory.startsWith(`${node.pageDirectory}/pages/`)
82
+ ));
74
83
  const pageHref = (page: SitePage) => withBasePath(projectConfig.site.basePath, page.pathname);
75
84
  const getNavigationPage = (node: SiteNavigationNode) => getFirstPageInNavigationNode(node);
76
85
  const hasSectionMenu = (pageSections: SectionNavigation[]) => pageSections.length > 1;
@@ -94,7 +103,9 @@ const showPageNavigation = Boolean(
94
103
  const showSinglePageTitleOnly = showSinglePageTopLink && currentPageSectionCount === 0;
95
104
  const showMobileNavigation = showSiteNavigation || hasCurrentPageSectionMenu || hasTreePageContents;
96
105
  const showReaderControls = Object.values(readerPreferences.controls).some(Boolean);
97
- const showDesktopNavigationRow = showSiteNavigation || Boolean(logo) || showReaderControls;
106
+ const showSearch = projectConfig.search.enabled;
107
+ const searchHref = withBasePath(projectConfig.site.basePath, '/search/');
108
+ const showDesktopNavigationRow = showSiteNavigation || Boolean(logo) || showReaderControls || showSearch;
98
109
  const showNavigationRow = showMobileNavigation || showDesktopNavigationRow;
99
110
  const needsNavigationScript = hasCurrentPageSectionMenu || hasTreePageContents || navigationMode === 'tree';
100
111
  ---
@@ -150,61 +161,101 @@ const needsNavigationScript = hasCurrentPageSectionMenu || hasTreePageContents |
150
161
  </nav>
151
162
  )}
152
163
 
164
+ {showSearch && (
165
+ <a
166
+ class="site-search-link"
167
+ href={searchHref}
168
+ aria-current={searchPage ? 'page' : undefined}
169
+ aria-label={projectConfig.locale.labels.search}
170
+ title={projectConfig.locale.labels.search}
171
+ >
172
+ <Search aria-hidden="true" />
173
+ </a>
174
+ )}
175
+
153
176
  {showReaderControls && (
154
177
  <DisplaySettings controls={readerPreferences.controls} defaults={readerPreferences.defaults} />
155
178
  )}
156
179
 
157
- {showMobileNavigation && <details class="mobile-nav-menu">
158
- <summary>
159
- <span>{projectConfig.locale.labels.navigationMenu}</span>
160
- </summary>
161
- <div class="mobile-nav-panel">
162
- <div class="mobile-site-nav" data-navigation-root={pageHref(homePage)}>
163
- {showSiteNavigation && (
164
- <nav class="mobile-nav-view" aria-label={projectConfig.locale.labels.siteNavigation}>
165
- <h2 tabindex="-1">{projectConfig.locale.labels.siteNavigation}</h2>
166
- <NavigationPageTree
167
- nodes={siteNavigationTree}
168
- currentPage={currentPage}
169
- variant="mobile"
170
- integratePageSections={navigationMode === 'tree'}
171
- />
172
- </nav>
173
- )}
180
+ {showMobileNavigation && (
181
+ <details class="mobile-nav-menu" data-compact-navigation>
182
+ <summary>
183
+ <span>{projectConfig.locale.labels.navigationMenu}</span>
184
+ <Menu class="mobile-nav-trigger-icon mobile-nav-trigger-open" aria-hidden="true" />
185
+ <X class="mobile-nav-trigger-icon mobile-nav-trigger-close" aria-hidden="true" />
186
+ </summary>
187
+ <div
188
+ class="mobile-nav-panel"
189
+ data-compact-navigation-panel
190
+ role="dialog"
191
+ aria-modal="true"
192
+ aria-label={projectConfig.locale.labels.navigationMenu}
193
+ >
194
+ <div class="mobile-nav-panel-actions">
195
+ <button
196
+ type="button"
197
+ class="mobile-nav-close"
198
+ data-compact-navigation-close
199
+ aria-label={projectConfig.locale.labels.closeNavigation}
200
+ >
201
+ <X aria-hidden="true" />
202
+ </button>
203
+ </div>
204
+ <div
205
+ class="mobile-site-nav"
206
+ data-navigation-root={pageHref(homePage)}
207
+ data-navigation-state-root={pageHref(homePage)}
208
+ >
209
+ {showSiteNavigation && (
210
+ <nav class="mobile-nav-view" aria-label={projectConfig.locale.labels.siteNavigation}>
211
+ <h2 tabindex="-1">{projectConfig.locale.labels.siteNavigation}</h2>
212
+ <NavigationTreeControls
213
+ idPrefix="mobile-tree-navigation"
214
+ nodes={siteNavigationTree}
215
+ />
216
+ <NavigationPageTree
217
+ nodes={siteNavigationTree}
218
+ currentPage={currentPage}
219
+ variant="mobile"
220
+ integratePageSections={navigationMode === 'tree'}
221
+ />
222
+ </nav>
223
+ )}
174
224
 
175
- {navigationMode !== 'tree' && hasCurrentPageSectionMenu && currentPageNavigation && (
176
- <nav class="mobile-nav-view" aria-label={projectConfig.locale.labels.pageNavigation}>
177
- {!showSinglePageTopLink && (
178
- <h2 tabindex="-1">{projectConfig.locale.labels.pageNavigation}</h2>
179
- )}
180
- <ul class="mobile-nav-sections">
181
- {showSinglePageTopLink && (
182
- <li class="mobile-nav-page-top-item">
183
- <a
184
- class="mobile-nav-destination mobile-nav-page-top"
185
- data-page-top
186
- href={pageHref(currentPage)}
187
- >
188
- {currentPage.title}
189
- </a>
190
- </li>
191
- )}
192
- {currentPageNavigation.sections.map((section) => (
193
- <li>
194
- <a class="mobile-nav-destination" href={`#${section.id}`}>{section.title}</a>
195
- </li>
196
- ))}
197
- </ul>
198
- </nav>
225
+ {navigationMode !== 'tree' && hasCurrentPageSectionMenu && currentPageNavigation && currentPage && (
226
+ <nav class="mobile-nav-view" aria-label={projectConfig.locale.labels.pageNavigation}>
227
+ {!showSinglePageTopLink && (
228
+ <h2 tabindex="-1">{projectConfig.locale.labels.pageNavigation}</h2>
229
+ )}
230
+ <ul class="mobile-nav-sections">
231
+ {showSinglePageTopLink && (
232
+ <li class="mobile-nav-page-top-item">
233
+ <a
234
+ class="mobile-nav-destination mobile-nav-page-top"
235
+ data-page-top
236
+ href={pageHref(currentPage)}
237
+ >
238
+ {currentPage.title}
239
+ </a>
240
+ </li>
241
+ )}
242
+ {currentPageNavigation.sections.map((section) => (
243
+ <li>
244
+ <a class="mobile-nav-destination" href={`#${section.id}`}>{section.title}</a>
245
+ </li>
246
+ ))}
247
+ </ul>
248
+ </nav>
249
+ )}
250
+ </div>
251
+ </div>
252
+ </details>
199
253
  )}
200
- </div>
201
- </div>
202
- </details>}
203
254
  </div>
204
255
  </div>
205
256
  )}
206
257
 
207
- {showPageNavigation && currentPageNavigation && (
258
+ {showPageNavigation && currentPageNavigation && currentPage && (
208
259
  <nav
209
260
  class:list={['page-nav', { 'page-nav-title-only': showSinglePageTitleOnly }]}
210
261
  aria-label={projectConfig.locale.labels.pageNavigation}