@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.
- package/README.md +14 -18
- package/astro.config.mjs +14 -0
- package/bin/norna-cli.mjs +6 -0
- package/package.json +25 -2
- package/schemas/category.schema.json +2 -2
- package/schemas/config.schema.json +80 -15
- package/schemas/content-frontmatter.schema.json +7 -7
- package/schemas/page-theme.schema.json +26 -26
- package/schemas/sitewide-content.schema.json +18 -18
- package/schemas/theme.schema.json +246 -246
- package/scripts/build-site.mjs +1 -0
- package/scripts/check-config.mjs +16 -1
- package/scripts/dev-local.mjs +106 -11
- package/scripts/generate-search-index.mjs +57 -0
- package/scripts/init-site.mjs +3 -0
- package/scripts/lib/code-fence-metadata.mjs +228 -0
- package/scripts/lib/edit-source-link.mjs +102 -0
- package/scripts/lib/editor-language-service.mjs +32 -12
- package/scripts/lib/image-presentation.mjs +4 -0
- package/scripts/lib/navigation-model.mjs +34 -13
- package/scripts/lib/navigation-review.mjs +396 -0
- package/scripts/lib/norna-markdown-blocks.mjs +164 -26
- package/scripts/lib/norna-markdown-render-plugin.mjs +64 -1
- package/scripts/lib/page-aliases.mjs +21 -1
- package/scripts/lib/page-markdown.mjs +32 -1
- package/scripts/lib/page-move-plan.mjs +659 -0
- package/scripts/lib/presentation-palette-metadata.mjs +1 -1
- package/scripts/lib/presentation.mjs +1 -10
- package/scripts/lib/project-config.mjs +141 -5
- package/scripts/lib/public-asset-conventions.mjs +36 -2
- package/scripts/lib/schema-definitions.mjs +30 -5
- package/scripts/lib/schema-editor-metadata.mjs +36 -4
- package/scripts/lib/schema-value-definitions.mjs +4 -1
- package/scripts/lib/semantic-callouts.mjs +128 -0
- package/scripts/lib/site-content.mjs +2 -1
- package/scripts/lib/site-link-graph.mjs +33 -2
- package/scripts/lib/site-navigation-tree.mjs +85 -0
- package/scripts/lib/site-paths.mjs +14 -2
- package/scripts/lib/social-image-assets.mjs +30 -0
- package/scripts/lib/table-render-plugin.mjs +33 -0
- package/scripts/lib/theme-presets.mjs +2 -2
- package/scripts/lib/theme-profiles.mjs +0 -4
- package/scripts/move-site-page.mjs +256 -0
- package/scripts/review-navigation.mjs +32 -0
- package/scripts/sync-content-sections.mjs +67 -4
- package/scripts/sync-site-public.mjs +24 -5
- package/src/components/CardList.astro +1 -0
- package/src/components/CodeBlockCopyScript.astro +11 -5
- package/src/components/EditSourceLink.astro +17 -0
- package/src/components/ImageCarousel.astro +9 -7
- package/src/components/ImageStack.astro +34 -9
- package/src/components/ImageStackEnhancement.astro +331 -0
- package/src/components/NavigationPageTree.astro +66 -55
- package/src/components/NavigationTreeControls.astro +77 -0
- package/src/components/PageAliasRedirect.astro +1 -1
- package/src/components/PageContentsNavigation.astro +2 -3
- package/src/components/PageList.astro +33 -0
- package/src/components/PageSequenceNavigation.astro +37 -0
- package/src/components/SearchPage.astro +151 -0
- package/src/components/SectionNavigationScript.astro +45 -7
- package/src/components/SiteNavigation.astro +102 -51
- package/src/components/SitePage.astro +107 -39
- package/src/components/SiteSection.astro +11 -3
- package/src/components/SiteTreeNavigation.astro +11 -1
- package/src/components/TableOverflowScript.astro +251 -0
- package/src/components/TreeNavigationScript.astro +271 -34
- package/src/layouts/BaseLayout.astro +83 -2
- package/src/lib/generatedImages.ts +18 -0
- package/src/lib/sectionContent.ts +36 -30
- package/src/lib/siteNavigation.ts +31 -52
- package/src/lib/sitePages.ts +6 -3
- package/src/lib/sitePublicAssets.ts +1 -0
- package/src/pages/404.astro +110 -0
- package/src/pages/[...slug].astro +15 -5
- package/src/styles/content.css +538 -39
- package/src/styles/media.css +187 -8
- package/src/styles/navigation.css +199 -3
- package/src/styles/page-layout.css +481 -100
- package/src/styles/responsive.css +144 -28
- package/starters/basic/README.md +6 -19
- package/starters/basic/package.json +1 -0
- 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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
189
|
-
'
|
|
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
|
|
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
|
-
|
|
71
|
-
const
|
|
72
|
-
|
|
73
|
-
|
|
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
|
|
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
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
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
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
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}
|