@janga/norna 0.7.26 → 0.7.28
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 +51 -35
- package/astro.config.mjs +19 -1
- package/bin/norna-cli.mjs +6 -4
- package/package.json +9 -5
- package/schemas/card-list.schema.json +1 -1
- package/schemas/config.schema.json +66 -66
- package/schemas/content-frontmatter.schema.json +17 -8
- package/schemas/manifest.json +49 -3
- package/schemas/page-theme.schema.json +1585 -41
- package/schemas/root-theme.schema.json +1767 -0
- package/schemas/sitewide-content.schema.json +18 -18
- package/schemas/theme.schema.json +243 -243
- package/scripts/add-site-node.mjs +13 -161
- package/scripts/build-site.mjs +11 -6
- package/scripts/check-config.mjs +7 -23
- package/scripts/dev-local.mjs +17 -10
- package/scripts/export-theme-preset.mjs +2 -2
- package/scripts/generate-images.mjs +7 -6
- package/scripts/generate-schemas.mjs +9 -6
- package/scripts/lib/category-destinations.mjs +13 -12
- package/scripts/lib/documentation-links.mjs +33 -2
- package/scripts/lib/documentation-routes.json +229 -0
- package/scripts/lib/docusaurus-migration-audit.mjs +9 -6
- package/scripts/lib/editor-file-policy.mjs +18 -0
- package/scripts/lib/editor-language-service.mjs +53 -55
- package/scripts/lib/editor-page-addresses.mjs +55 -0
- package/scripts/lib/editor-page-placement.mjs +172 -0
- package/scripts/lib/editor-site-editing-tree.mjs +146 -0
- package/scripts/lib/editor-site-files.mjs +153 -0
- package/scripts/lib/editor-site-links.mjs +79 -0
- package/scripts/lib/editor-site-tree.mjs +151 -0
- package/scripts/lib/editor-source-files.mjs +87 -0
- package/scripts/lib/editor-theme-help.mjs +43 -0
- package/scripts/lib/locale-registry.mjs +1 -1
- package/scripts/lib/locales/en.mjs +1 -1
- package/scripts/lib/navigation-model.mjs +2 -44
- package/scripts/lib/navigation-review.mjs +14 -50
- package/scripts/lib/norna-markdown-blocks.mjs +1 -1
- package/scripts/lib/page-aliases.mjs +3 -1
- package/scripts/lib/page-model.mjs +39 -20
- package/scripts/lib/page-move-apply.mjs +117 -0
- package/scripts/lib/page-move-plan.mjs +27 -59
- package/scripts/lib/presentation.mjs +3 -3
- package/scripts/lib/project-config.mjs +7 -8
- package/scripts/lib/schema-definitions.mjs +12 -39
- package/scripts/lib/schema-editor-metadata.mjs +12 -59
- package/scripts/lib/schema-value-definitions.mjs +1 -1
- package/scripts/lib/site-content.mjs +7 -24
- package/scripts/lib/site-conventions.mjs +19 -1
- package/scripts/lib/site-navigation-tree.mjs +2 -7
- package/scripts/lib/site-node-create.mjs +169 -0
- package/scripts/lib/site-paths.mjs +22 -10
- package/scripts/lib/site-structure.mjs +72 -82
- package/scripts/lib/site-upgrade.mjs +30 -0
- package/scripts/lib/source-files.mjs +34 -0
- package/scripts/lib/theme-config.mjs +26 -68
- package/scripts/lib/theme-packages.mjs +47 -0
- package/scripts/lib/theme-presets.mjs +20 -53
- package/scripts/lib/typography.mjs +2 -2
- package/scripts/lib/yaml-config.mjs +1 -1
- package/scripts/lib/yaml-source-edit.mjs +94 -0
- package/scripts/move-site-page.mjs +3 -124
- package/scripts/show-typography.mjs +32 -17
- package/scripts/sync-content-sections.mjs +9 -7
- package/scripts/sync-site-public.mjs +1 -1
- package/scripts/upgrade-site.mjs +17 -0
- package/src/components/AreaNavigationMenu.astro +24 -0
- package/src/components/AreaNavigationPanel.astro +55 -0
- package/src/components/AreaNavigationSwitcher.astro +30 -0
- package/src/components/ImageStackEnhancement.astro +0 -7
- package/src/components/NavigationContinuity.astro +26 -0
- package/src/components/NavigationPageTree.astro +13 -4
- package/src/components/NavigationState.astro +11 -0
- package/src/components/NavigationTreeControls.astro +3 -2
- package/src/components/PageAliasRedirect.astro +44 -3
- package/src/components/SearchPage.astro +9 -1
- package/src/components/SectionNavigationScript.astro +106 -26
- package/src/components/SiteNavigation.astro +45 -20
- package/src/components/SitePage.astro +37 -61
- package/src/components/SiteTreeNavigation.astro +38 -18
- package/src/components/TopPageMenu.astro +4 -3
- package/src/components/TreeNavigationScript.astro +50 -57
- package/src/content.config.ts +15 -16
- package/src/layouts/BaseLayout.astro +15 -39
- package/src/lib/areaNavigation.ts +42 -0
- package/src/lib/navigationContinuity.ts +212 -0
- package/src/lib/navigationFollowing.ts +28 -18
- package/src/lib/navigationState.mjs +88 -0
- package/src/lib/navigationStateScript.mjs +76 -0
- package/src/lib/navigationVisibility.ts +9 -0
- package/src/lib/pageThemes.ts +5 -42
- package/src/lib/sectionContent.ts +3 -2
- package/src/lib/siteNavigation.ts +6 -0
- package/src/lib/sitePages.ts +12 -23
- package/src/pages/404.astro +1 -1
- package/src/pages/[...slug].astro +5 -15
- package/src/pages/index.astro +1 -3
- package/src/styles/area-navigation.css +399 -0
- package/src/styles/content.css +5 -31
- package/src/styles/navigation-continuity.css +163 -0
- package/src/styles/page-layout.css +57 -68
- package/src/styles/responsive.css +0 -4
- package/starters/basic/README.md +10 -9
- package/starters/basic/site/{pages/000-home → root}/content.md +2 -2
- package/schemas/category.schema.json +0 -20
- package/src/components/PageContentsNavigation.astro +0 -20
- /package/starters/basic/site/{pages/000-home → root}/images/.gitkeep +0 -0
- /package/starters/basic/site/{theme.yaml → root/tree-theme.yaml} +0 -0
- /package/starters/basic/site/{config.yaml → site-config/settings.yaml} +0 -0
- /package/starters/basic/site/{sitewide-content.yaml → site-config/shared-content.yaml} +0 -0
|
@@ -6,15 +6,16 @@ import type { SiteNavigationNode } from '../lib/siteNavigation';
|
|
|
6
6
|
interface Props {
|
|
7
7
|
idPrefix: string;
|
|
8
8
|
nodes: SiteNavigationNode[];
|
|
9
|
+
forceVisible?: boolean;
|
|
9
10
|
}
|
|
10
11
|
|
|
11
|
-
const { idPrefix, nodes } = Astro.props;
|
|
12
|
+
const { idPrefix, nodes, forceVisible = false } = Astro.props;
|
|
12
13
|
const countNodes = (tree: SiteNavigationNode[]): number => tree.reduce(
|
|
13
14
|
(total, node) => total + 1 + countNodes(node.children),
|
|
14
15
|
0,
|
|
15
16
|
);
|
|
16
17
|
const minimumUsefulNodeCount = 12;
|
|
17
|
-
const showControls = countNodes(nodes) >= minimumUsefulNodeCount;
|
|
18
|
+
const showControls = forceVisible || countNodes(nodes) >= minimumUsefulNodeCount;
|
|
18
19
|
const labels = projectConfig.locale.labels;
|
|
19
20
|
const filterId = `${idPrefix}-filter`;
|
|
20
21
|
const statusId = `${idPrefix}-status`;
|
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
---
|
|
2
|
+
import { getCollection } from 'astro:content';
|
|
3
|
+
import { getPresentationCssVariables, resolveThemePresentation } from '../../scripts/lib/presentation.mjs';
|
|
2
4
|
import projectConfig from '../../scripts/lib/project-config.mjs';
|
|
3
5
|
import { getAbsolutePageUrl } from '../../scripts/lib/site-page-urls.mjs';
|
|
6
|
+
import { siteThemeLabel } from '../../scripts/lib/site-paths.mjs';
|
|
4
7
|
import { withBasePath } from '../lib/basePath';
|
|
8
|
+
import { getReaderPreferencesScript } from '../lib/readerPreferencesScript.mjs';
|
|
5
9
|
|
|
6
10
|
interface Props {
|
|
7
11
|
targetPathname: string;
|
|
@@ -13,23 +17,54 @@ const { targetPathname, targetTitle, categoryTitle } = Astro.props;
|
|
|
13
17
|
const { labels, lang } = projectConfig.locale;
|
|
14
18
|
const targetHref = withBasePath(projectConfig.site.basePath, targetPathname);
|
|
15
19
|
const canonicalUrl = getAbsolutePageUrl(projectConfig.site.url, targetPathname);
|
|
20
|
+
const [siteTheme] = await getCollection('theme');
|
|
21
|
+
if (!siteTheme) throw new Error(`${siteThemeLabel} is required. Add a root theme before building the site.`);
|
|
22
|
+
const presentation = resolveThemePresentation(siteTheme.data, siteThemeLabel);
|
|
23
|
+
const documentStyle = Object.entries(getPresentationCssVariables(presentation))
|
|
24
|
+
.map(([name, value]) => `${name}: ${value}`).join('; ');
|
|
25
|
+
const readerPreferencesScript = getReaderPreferencesScript({
|
|
26
|
+
...presentation.readerPreferences,
|
|
27
|
+
cookiePath: projectConfig.site.basePath,
|
|
28
|
+
});
|
|
16
29
|
---
|
|
17
30
|
|
|
18
31
|
<!doctype html>
|
|
19
|
-
<html lang={lang} data-pagefind-ignore="all">
|
|
32
|
+
<html lang={lang} data-pagefind-ignore="all" data-appearance={presentation.appearance.default} style={documentStyle}>
|
|
20
33
|
<head>
|
|
21
34
|
<meta charset="utf-8" />
|
|
22
35
|
<meta name="viewport" content="width=device-width" />
|
|
23
36
|
<meta name="robots" content="noindex, follow" />
|
|
24
|
-
<meta http-equiv="refresh" content={`0; url=${targetHref}`} />
|
|
25
37
|
<link rel="canonical" href={canonicalUrl} />
|
|
26
38
|
<title>{categoryTitle ?? labels.pageMoved}: {targetTitle}</title>
|
|
39
|
+
<meta name="theme-color" content={presentation.palette.page.backgroundColor} />
|
|
27
40
|
<style is:inline>
|
|
28
41
|
:root {
|
|
29
|
-
color-
|
|
42
|
+
--color-page: var(--palette-light-page-background);
|
|
43
|
+
--color-primary-text: var(--palette-light-primary-text);
|
|
44
|
+
--color-link-text: var(--palette-light-link-text);
|
|
45
|
+
color-scheme: light;
|
|
46
|
+
min-height: 100%;
|
|
47
|
+
background: var(--color-page);
|
|
48
|
+
color: var(--color-primary-text);
|
|
30
49
|
font-family: system-ui, sans-serif;
|
|
31
50
|
}
|
|
32
51
|
|
|
52
|
+
:root[data-appearance='dark'] {
|
|
53
|
+
--color-page: var(--palette-dark-page-background);
|
|
54
|
+
--color-primary-text: var(--palette-dark-primary-text);
|
|
55
|
+
--color-link-text: var(--palette-dark-link-text);
|
|
56
|
+
color-scheme: dark;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
@media (prefers-color-scheme: dark) {
|
|
60
|
+
:root[data-appearance='system'] {
|
|
61
|
+
--color-page: var(--palette-dark-page-background);
|
|
62
|
+
--color-primary-text: var(--palette-dark-primary-text);
|
|
63
|
+
--color-link-text: var(--palette-dark-link-text);
|
|
64
|
+
color-scheme: dark;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
33
68
|
body {
|
|
34
69
|
margin: 0;
|
|
35
70
|
padding: 2rem;
|
|
@@ -39,7 +74,13 @@ const canonicalUrl = getAbsolutePageUrl(projectConfig.site.url, targetPathname);
|
|
|
39
74
|
max-width: 42rem;
|
|
40
75
|
margin-inline: auto;
|
|
41
76
|
}
|
|
77
|
+
|
|
78
|
+
a {
|
|
79
|
+
color: var(--color-link-text);
|
|
80
|
+
}
|
|
42
81
|
</style>
|
|
82
|
+
<script is:inline set:html={readerPreferencesScript} />
|
|
83
|
+
<meta http-equiv="refresh" content={`0; url=${targetHref}`} />
|
|
43
84
|
</head>
|
|
44
85
|
<body>
|
|
45
86
|
<main>
|
|
@@ -17,12 +17,13 @@ import { withBasePath } from '../lib/basePath';
|
|
|
17
17
|
|
|
18
18
|
const siteDirectory = (process.env.NORNA_SITE_DIR ?? 'site').trim();
|
|
19
19
|
const siteModel = await getSiteModel(await getCollection('site'));
|
|
20
|
+
const categoryDestinations = siteModel.categoryDestinations.byPathname;
|
|
20
21
|
const { nodes: siteNodes, pages: sitePages } = siteModel;
|
|
21
22
|
const homePage = sitePages.find((page) => page.isHome);
|
|
22
23
|
if (!homePage) throw new Error('The required homepage is missing from the site page model.');
|
|
23
24
|
|
|
24
25
|
const [siteTheme] = await getCollection('theme');
|
|
25
|
-
if (!siteTheme) throw new Error('site/theme.yaml is required. Add
|
|
26
|
+
if (!siteTheme) throw new Error('site/root/tree-theme.yaml is required. Add the shared site theme before building the site.');
|
|
26
27
|
const [sitewide] = await getCollection('sitewide');
|
|
27
28
|
const sitewideData = sitewide?.data ?? {};
|
|
28
29
|
const siteThemeData = siteTheme.data;
|
|
@@ -55,6 +56,7 @@ const pagePresentation = resolvePagePresentation(siteThemeData, themeLabel, {
|
|
|
55
56
|
navigationMode: navigationModel.mode,
|
|
56
57
|
});
|
|
57
58
|
const pageTextWidth = getTextWidthCssValue(pagePresentation.typography.values.body.width);
|
|
59
|
+
const areaReadingFrame = navigationModel.mode === 'tree';
|
|
58
60
|
const frameColors = resolveFrameColors({ theme: siteThemeData, sourceLabel: themeLabel });
|
|
59
61
|
const logoAsset = getLogoAsset();
|
|
60
62
|
const logo = logoAsset
|
|
@@ -130,19 +132,24 @@ const searchScript = `(() => {
|
|
|
130
132
|
pathname="/search/"
|
|
131
133
|
footer={sitewideData.footer}
|
|
132
134
|
indexable={false}
|
|
135
|
+
areaNavigation={navigationModel.mode === 'tree'}
|
|
136
|
+
areaReadingFrame={areaReadingFrame}
|
|
133
137
|
>
|
|
134
138
|
<link slot="head" rel="stylesheet" href={`${bundlePath}pagefind-ui.css`} />
|
|
135
139
|
<SiteNavigation
|
|
136
140
|
logo={logo}
|
|
137
141
|
navigationEntries={siteNavigation}
|
|
142
|
+
categoryDestinations={categoryDestinations}
|
|
138
143
|
navigationMode={navigationModel.mode}
|
|
139
144
|
showTopNavigationSubmenus={navigationModel.requestedMode !== 'automatic' && navigationModel.mode !== 'tree'}
|
|
140
145
|
hasNavigationRailFrame={hasNavigationRailFrame}
|
|
141
146
|
frameColors={frameColors}
|
|
142
147
|
readerPreferences={pagePresentation.readerPreferences}
|
|
143
148
|
searchPage
|
|
149
|
+
areaReadingFrame={areaReadingFrame}
|
|
144
150
|
/>
|
|
145
151
|
{siteBanners.length > 0 && <SiteBanners banners={siteBanners} />}
|
|
152
|
+
<div class:list={['site-page-layout', { 'site-page-layout-tree': areaReadingFrame }]}>
|
|
146
153
|
<main class="site-content search-page-content" id="main-content" style={`--page-text-width: ${pageTextWidth}`}>
|
|
147
154
|
<SiteSection
|
|
148
155
|
section={searchSection}
|
|
@@ -158,6 +165,7 @@ const searchScript = `(() => {
|
|
|
158
165
|
</div>
|
|
159
166
|
</SiteSection>
|
|
160
167
|
</main>
|
|
168
|
+
</div>
|
|
161
169
|
<script is:inline set:html={searchScript} />
|
|
162
170
|
</BaseLayout>
|
|
163
171
|
|
|
@@ -66,17 +66,6 @@
|
|
|
66
66
|
document.documentElement.style.setProperty('--site-top-anchor-offset', `${offset}px`);
|
|
67
67
|
};
|
|
68
68
|
|
|
69
|
-
const alignInitialHashTarget = () => {
|
|
70
|
-
if (!window.location.hash) return;
|
|
71
|
-
|
|
72
|
-
try {
|
|
73
|
-
const target = document.getElementById(decodeURIComponent(window.location.hash.slice(1)));
|
|
74
|
-
target?.scrollIntoView({ block: 'start' });
|
|
75
|
-
} catch {
|
|
76
|
-
// Invalid URL encoding is left to the browser's native hash handling.
|
|
77
|
-
}
|
|
78
|
-
};
|
|
79
|
-
|
|
80
69
|
const getSamePageTarget = (link: HTMLAnchorElement) => {
|
|
81
70
|
const linkUrl = new URL(link.href, window.location.href);
|
|
82
71
|
if (linkUrl.pathname !== window.location.pathname || !linkUrl.hash) return undefined;
|
|
@@ -158,35 +147,118 @@
|
|
|
158
147
|
sectionTrackingFrame = window.requestAnimationFrame(updateReadingPosition);
|
|
159
148
|
};
|
|
160
149
|
|
|
150
|
+
const positionTopPageMenu = (menu: HTMLDetailsElement) => {
|
|
151
|
+
const panel = menu.querySelector<HTMLElement>('.top-page-panel');
|
|
152
|
+
if (!panel) return;
|
|
153
|
+
panel.style.removeProperty('--top-page-panel-shift');
|
|
154
|
+
const rectangle = panel.getBoundingClientRect();
|
|
155
|
+
let shift = Math.max(0, 16 - rectangle.left)
|
|
156
|
+
- Math.max(0, rectangle.right - document.documentElement.clientWidth + 16);
|
|
157
|
+
if (menu.hasAttribute('data-area-menu')) {
|
|
158
|
+
const trigger = menu.querySelector('summary')!.getBoundingClientRect();
|
|
159
|
+
const row = menu.closest('.site-nav-row')?.getBoundingClientRect();
|
|
160
|
+
const rightEdge = Math.min(row?.right ?? Infinity, document.documentElement.clientWidth - 16);
|
|
161
|
+
const left = Math.max(16, Math.min(trigger.left, rightEdge - rectangle.width));
|
|
162
|
+
shift = left - rectangle.left;
|
|
163
|
+
}
|
|
164
|
+
panel.style.setProperty('--top-page-panel-shift', `${shift}px`);
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
const closeTopPageMenu = new Map<HTMLDetailsElement, () => void>();
|
|
161
168
|
topPageMenus.forEach((menu) => {
|
|
169
|
+
let openTimer: number | undefined;
|
|
170
|
+
let closeTimer: number | undefined;
|
|
171
|
+
const clearTimers = () => {
|
|
172
|
+
window.clearTimeout(openTimer);
|
|
173
|
+
window.clearTimeout(closeTimer);
|
|
174
|
+
};
|
|
175
|
+
const close = () => {
|
|
176
|
+
clearTimers();
|
|
177
|
+
menu.open = false;
|
|
178
|
+
};
|
|
179
|
+
closeTopPageMenu.set(menu, close);
|
|
162
180
|
menu.addEventListener('toggle', () => {
|
|
163
181
|
if (!menu.open) return;
|
|
164
|
-
topPageMenus.forEach((other) => { if (other !== menu) other
|
|
165
|
-
|
|
166
|
-
if (!panel) return;
|
|
167
|
-
panel.style.removeProperty('--top-page-panel-shift');
|
|
168
|
-
const rectangle = panel.getBoundingClientRect();
|
|
169
|
-
const shift = Math.max(0, 16 - rectangle.left)
|
|
170
|
-
- Math.max(0, rectangle.right - document.documentElement.clientWidth + 16);
|
|
171
|
-
panel.style.setProperty('--top-page-panel-shift', `${shift}px`);
|
|
182
|
+
topPageMenus.forEach((other) => { if (other !== menu) closeTopPageMenu.get(other)?.(); });
|
|
183
|
+
positionTopPageMenu(menu);
|
|
172
184
|
});
|
|
173
185
|
menu.addEventListener('keydown', (event) => {
|
|
174
186
|
if (event.key !== 'Escape' || !menu.open) return;
|
|
175
187
|
event.preventDefault();
|
|
176
|
-
|
|
188
|
+
close();
|
|
177
189
|
menu.querySelector('summary')?.focus();
|
|
178
190
|
});
|
|
179
191
|
menu.addEventListener('focusout', (event) => {
|
|
180
|
-
if (event.relatedTarget instanceof Node && !menu.contains(event.relatedTarget))
|
|
192
|
+
if (event.relatedTarget instanceof Node && !menu.contains(event.relatedTarget)) close();
|
|
181
193
|
});
|
|
182
194
|
menu.querySelectorAll('a').forEach((link) => {
|
|
183
|
-
link.addEventListener('click',
|
|
195
|
+
link.addEventListener('click', close);
|
|
184
196
|
});
|
|
197
|
+
if (menu.hasAttribute('data-area-switcher')) {
|
|
198
|
+
menu.addEventListener('keydown', (event) => {
|
|
199
|
+
if (event.key !== 'ArrowDown' || event.target !== menu.querySelector('summary')) return;
|
|
200
|
+
event.preventDefault();
|
|
201
|
+
menu.open = true;
|
|
202
|
+
menu.querySelector('a')?.focus();
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
if (menu.hasAttribute('data-area-menu')) {
|
|
206
|
+
const canHover = window.matchMedia('(any-hover: hover)');
|
|
207
|
+
const hoverRegion = menu.closest<HTMLElement>('.site-nav-row') ?? menu;
|
|
208
|
+
const isHoverPointer = (event: PointerEvent) => event.pointerType !== 'touch' && canHover.matches;
|
|
209
|
+
menu.addEventListener('pointerenter', (event) => {
|
|
210
|
+
if (!isHoverPointer(event)) return;
|
|
211
|
+
clearTimers();
|
|
212
|
+
openTimer = window.setTimeout(() => { menu.open = true; }, 120);
|
|
213
|
+
});
|
|
214
|
+
menu.addEventListener('pointerleave', (event) => {
|
|
215
|
+
if (isHoverPointer(event)) window.clearTimeout(openTimer);
|
|
216
|
+
});
|
|
217
|
+
// The row and its descendant panel form one continuous hover region.
|
|
218
|
+
// Crossing or pausing in its padding must not start a closing timer.
|
|
219
|
+
// Other titles remain real hit targets; no invisible overlay is needed.
|
|
220
|
+
hoverRegion.addEventListener('pointerenter', (event) => {
|
|
221
|
+
if (isHoverPointer(event)) window.clearTimeout(closeTimer);
|
|
222
|
+
});
|
|
223
|
+
hoverRegion.addEventListener('pointerleave', (event) => {
|
|
224
|
+
if (!isHoverPointer(event)) return;
|
|
225
|
+
clearTimers();
|
|
226
|
+
closeTimer = window.setTimeout(() => {
|
|
227
|
+
const focus = document.activeElement;
|
|
228
|
+
if (focus instanceof Element && menu.contains(focus) && focus.matches(':focus-visible')) return;
|
|
229
|
+
close();
|
|
230
|
+
}, 180);
|
|
231
|
+
});
|
|
232
|
+
menu.querySelector('summary')?.addEventListener('click', (event) => {
|
|
233
|
+
// Mouse users open on hover; a click must not immediately fold it
|
|
234
|
+
// away. Native activation remains available for touch and keyboard.
|
|
235
|
+
if (event.detail === 0 || event.pointerType === 'touch' || !canHover.matches) return;
|
|
236
|
+
event.preventDefault();
|
|
237
|
+
clearTimers();
|
|
238
|
+
menu.open = true;
|
|
239
|
+
});
|
|
240
|
+
menu.addEventListener('keydown', (event) => {
|
|
241
|
+
if (event.key !== 'ArrowDown' || event.target !== menu.querySelector('summary')) return;
|
|
242
|
+
event.preventDefault();
|
|
243
|
+
clearTimers();
|
|
244
|
+
menu.open = true;
|
|
245
|
+
menu.querySelector('a')?.focus();
|
|
246
|
+
});
|
|
247
|
+
}
|
|
185
248
|
});
|
|
186
249
|
if (topPageMenus.length > 0) {
|
|
250
|
+
// Hover does not move focus into the panel, so Escape must also work
|
|
251
|
+
// while focus remains in the article or elsewhere on the page.
|
|
252
|
+
document.addEventListener('keydown', (event) => {
|
|
253
|
+
if (event.key !== 'Escape') return;
|
|
254
|
+
topPageMenus.filter((menu) => menu.hasAttribute('data-area-menu') || menu.hasAttribute('data-area-switcher')).forEach((menu) => {
|
|
255
|
+
if (menu.open) event.preventDefault();
|
|
256
|
+
closeTopPageMenu.get(menu)?.();
|
|
257
|
+
});
|
|
258
|
+
});
|
|
187
259
|
document.addEventListener('pointerdown', (event) => {
|
|
188
260
|
topPageMenus.forEach((menu) => {
|
|
189
|
-
if (event.target instanceof Node && !menu.contains(event.target)) menu
|
|
261
|
+
if (event.target instanceof Node && !menu.contains(event.target)) closeTopPageMenu.get(menu)?.();
|
|
190
262
|
});
|
|
191
263
|
});
|
|
192
264
|
}
|
|
@@ -309,12 +381,20 @@
|
|
|
309
381
|
}
|
|
310
382
|
|
|
311
383
|
setAnchorOffset();
|
|
312
|
-
if (window.location.hash) window.requestAnimationFrame(alignInitialHashTarget);
|
|
313
384
|
|
|
314
385
|
if (siteTop && 'ResizeObserver' in window) {
|
|
315
|
-
new ResizeObserver(() => {
|
|
386
|
+
const navigationResizeObserver = new ResizeObserver(() => {
|
|
316
387
|
setAnchorOffset();
|
|
317
388
|
scheduleReadingPositionUpdate();
|
|
318
|
-
|
|
389
|
+
topPageMenus.filter((menu) => menu.open).forEach((menu) => {
|
|
390
|
+
if (!menu.getClientRects().length) closeTopPageMenu.get(menu)?.();
|
|
391
|
+
else positionTopPageMenu(menu);
|
|
392
|
+
});
|
|
393
|
+
});
|
|
394
|
+
navigationResizeObserver.observe(siteTop);
|
|
395
|
+
if (topPageMenus.some((menu) => menu.hasAttribute('data-area-menu') || menu.hasAttribute('data-area-switcher'))) {
|
|
396
|
+
const row = siteTop.querySelector('.site-nav-row');
|
|
397
|
+
if (row) navigationResizeObserver.observe(row);
|
|
398
|
+
}
|
|
319
399
|
}
|
|
320
400
|
</script>
|
|
@@ -1,18 +1,24 @@
|
|
|
1
1
|
---
|
|
2
|
+
import NavigationState from './NavigationState.astro';
|
|
2
3
|
import { Menu, Search, X } from '@lucide/astro';
|
|
3
4
|
import projectConfig from '../../scripts/lib/project-config.mjs';
|
|
4
5
|
import { withBasePath } from '../lib/basePath';
|
|
5
6
|
import type { SectionNavigation } from '../lib/sectionContent';
|
|
6
7
|
import {
|
|
7
8
|
getListedSiteNavigationTree,
|
|
9
|
+
getNavigationDestination,
|
|
10
|
+
type CategoryDestinationMap,
|
|
8
11
|
type SiteNavigationEntry,
|
|
9
|
-
type SiteNavigationNode,
|
|
10
12
|
} from '../lib/siteNavigation';
|
|
11
13
|
import type { SiteNode } from '../lib/sitePages';
|
|
12
14
|
import DisplaySettings from './DisplaySettings.astro';
|
|
13
15
|
import NavigationPageTree from './NavigationPageTree.astro';
|
|
14
16
|
import NavigationTreeControls from './NavigationTreeControls.astro';
|
|
15
17
|
import TopPageMenu from './TopPageMenu.astro';
|
|
18
|
+
import AreaNavigationMenu from './AreaNavigationMenu.astro';
|
|
19
|
+
import AreaNavigationSwitcher from './AreaNavigationSwitcher.astro';
|
|
20
|
+
import SiteBreadcrumbs from './SiteBreadcrumbs.astro';
|
|
21
|
+
import { hasLargeAreaMenu, withH2Outlines } from '../lib/areaNavigation';
|
|
16
22
|
import SectionNavigationScript from './SectionNavigationScript.astro';
|
|
17
23
|
import SearchNavigationScript from './SearchNavigationScript.astro';
|
|
18
24
|
|
|
@@ -29,6 +35,7 @@ interface Props {
|
|
|
29
35
|
};
|
|
30
36
|
currentPage?: SiteNode;
|
|
31
37
|
navigationEntries: SiteNavigationEntry[];
|
|
38
|
+
categoryDestinations?: CategoryDestinationMap;
|
|
32
39
|
navigationMode: 'sections' | 'top' | 'tree';
|
|
33
40
|
showTopNavigationSubmenus: boolean;
|
|
34
41
|
hasNavigationRailFrame: boolean;
|
|
@@ -46,18 +53,21 @@ interface Props {
|
|
|
46
53
|
};
|
|
47
54
|
};
|
|
48
55
|
searchPage?: boolean;
|
|
56
|
+
areaReadingFrame?: boolean;
|
|
49
57
|
}
|
|
50
58
|
|
|
51
59
|
const {
|
|
52
60
|
logo,
|
|
53
61
|
currentPage,
|
|
54
62
|
navigationEntries,
|
|
63
|
+
categoryDestinations,
|
|
55
64
|
navigationMode,
|
|
56
65
|
showTopNavigationSubmenus,
|
|
57
66
|
hasNavigationRailFrame,
|
|
58
67
|
frameColors,
|
|
59
68
|
readerPreferences,
|
|
60
69
|
searchPage = false,
|
|
70
|
+
areaReadingFrame = false,
|
|
61
71
|
} = Astro.props;
|
|
62
72
|
const siteTopBackgroundColor = frameColors.backgroundColor === 'transparent'
|
|
63
73
|
? 'var(--color-page)'
|
|
@@ -68,12 +78,7 @@ const navigationStyle = [
|
|
|
68
78
|
...(logo?.height ? [`--site-brand-logo-height: ${logo.height}`] : []),
|
|
69
79
|
].join('; ');
|
|
70
80
|
const siteNavigationTree = getListedSiteNavigationTree(navigationEntries);
|
|
71
|
-
const mobileNavigationTree =
|
|
72
|
-
? getListedSiteNavigationTree(navigationEntries.map((entry) => ({
|
|
73
|
-
...entry,
|
|
74
|
-
headings: entry.headings.filter((heading) => heading.depth === 2),
|
|
75
|
-
})))
|
|
76
|
-
: siteNavigationTree;
|
|
81
|
+
const mobileNavigationTree = withH2Outlines(siteNavigationTree);
|
|
77
82
|
const topLevelNavigation = siteNavigationTree;
|
|
78
83
|
const showSiteNavigation = topLevelNavigation.some(({ node }) => !node.isHome);
|
|
79
84
|
const homeNode = navigationEntries.find(({ node }) => node.isHome)?.node;
|
|
@@ -88,7 +93,6 @@ const isCurrentBranch = (node: SiteNode) => Boolean(currentPage && (
|
|
|
88
93
|
|| currentPage.pageDirectory.startsWith(`${node.pageDirectory}/pages/`)
|
|
89
94
|
));
|
|
90
95
|
const pageHref = (page: SiteNode) => withBasePath(projectConfig.site.basePath, page.pathname);
|
|
91
|
-
const getNavigationPage = (node: SiteNavigationNode) => node.node;
|
|
92
96
|
const hasSectionMenu = (pageSections: SectionNavigation[]) => pageSections.length > 1;
|
|
93
97
|
const showSinglePageTopLink = !showSiteNavigation;
|
|
94
98
|
const currentPageSectionCount = currentPageNavigation?.sections.length ?? 0;
|
|
@@ -131,35 +135,41 @@ const needsNavigationScript = showMobileNavigation || hasCurrentPageSectionMenu
|
|
|
131
135
|
>
|
|
132
136
|
{showNavigationRow && (
|
|
133
137
|
<div class:list={['site-nav-row', { 'site-nav-row-mobile-only': !showDesktopNavigationRow }]}>
|
|
134
|
-
{logo && (
|
|
138
|
+
{(logo || areaReadingFrame) && (
|
|
135
139
|
<div class="site-nav-leading">
|
|
136
|
-
<a class="site-brand" href={pageHref(homePage)}>
|
|
137
|
-
<img class="site-brand-logo" src={withBasePath(projectConfig.site.basePath, logo.src)} alt={logo.alt} />
|
|
140
|
+
<a class="site-brand" href={pageHref(homePage)} aria-label={projectConfig.locale.labels.returnHome} title={projectConfig.locale.labels.returnHome}>
|
|
141
|
+
{logo ? <img class="site-brand-logo" src={withBasePath(projectConfig.site.basePath, logo.src)} alt={logo.alt} /> : homePage.title}
|
|
138
142
|
</a>
|
|
139
143
|
</div>
|
|
140
144
|
)}
|
|
141
145
|
|
|
142
146
|
<div class="site-nav-actions">
|
|
143
|
-
{showSiteNavigation && (
|
|
147
|
+
{showSiteNavigation && !areaReadingFrame && (
|
|
144
148
|
<nav class="site-nav" aria-label={projectConfig.locale.labels.siteNavigation}>
|
|
145
149
|
<ul>
|
|
146
150
|
{topLevelNavigation.map((navigationNode) => {
|
|
147
|
-
const destination =
|
|
151
|
+
const destination = getNavigationDestination(navigationNode.node, categoryDestinations);
|
|
148
152
|
if (!destination) return null;
|
|
149
153
|
const nodeIsCurrentPage = isCurrentPage(navigationNode.node);
|
|
154
|
+
const useAreaMenu = navigationMode === 'tree';
|
|
150
155
|
return (
|
|
151
156
|
<li class:list={['site-nav-item', {
|
|
152
|
-
'site-nav-item-current-branch': !nodeIsCurrentPage && isCurrentBranch(navigationNode.node),
|
|
157
|
+
'site-nav-item-current-branch': (!nodeIsCurrentPage || useAreaMenu) && isCurrentBranch(navigationNode.node),
|
|
153
158
|
}]}>
|
|
154
|
-
|
|
159
|
+
{useAreaMenu && navigationNode.children.length > 0 && !hasLargeAreaMenu(navigationNode) ? <AreaNavigationMenu
|
|
160
|
+
node={navigationNode}
|
|
161
|
+
currentPage={currentPage}
|
|
162
|
+
categoryDestinations={categoryDestinations}
|
|
163
|
+
/> : <a href={pageHref(destination)} aria-current={nodeIsCurrentPage ? 'page' : undefined}>
|
|
155
164
|
{navigationNode.node.title}
|
|
156
|
-
</a>
|
|
157
|
-
<TopPageMenu
|
|
165
|
+
</a>}
|
|
166
|
+
{!useAreaMenu && <TopPageMenu
|
|
158
167
|
node={navigationNode}
|
|
159
168
|
currentPage={currentPage}
|
|
169
|
+
categoryDestinations={categoryDestinations}
|
|
160
170
|
showSections={navigationMode === 'top'}
|
|
161
171
|
showChildren={showTopNavigationSubmenus}
|
|
162
|
-
/>
|
|
172
|
+
/>}
|
|
163
173
|
</li>
|
|
164
174
|
);
|
|
165
175
|
})}
|
|
@@ -211,9 +221,10 @@ const needsNavigationScript = showMobileNavigation || hasCurrentPageSectionMenu
|
|
|
211
221
|
</div>
|
|
212
222
|
<div
|
|
213
223
|
class="mobile-site-nav"
|
|
214
|
-
data-navigation-root={pageHref(homePage)}
|
|
215
|
-
data-navigation-state-root={pageHref(homePage)}
|
|
224
|
+
data-navigation-root={`area:${pageHref(homePage)}`}
|
|
225
|
+
data-navigation-state-root={`area:${pageHref(homePage)}`}
|
|
216
226
|
>
|
|
227
|
+
<NavigationState scope="mobile" />
|
|
217
228
|
{showSiteNavigation && (
|
|
218
229
|
<nav class="mobile-nav-view" aria-label={projectConfig.locale.labels.siteNavigation}>
|
|
219
230
|
<h2 tabindex="-1">{projectConfig.locale.labels.siteNavigation}</h2>
|
|
@@ -224,6 +235,7 @@ const needsNavigationScript = showMobileNavigation || hasCurrentPageSectionMenu
|
|
|
224
235
|
<NavigationPageTree
|
|
225
236
|
nodes={mobileNavigationTree}
|
|
226
237
|
currentPage={currentPage}
|
|
238
|
+
categoryDestinations={categoryDestinations}
|
|
227
239
|
variant="mobile"
|
|
228
240
|
integratePageSections={navigationMode === 'tree' || navigationMode === 'top'}
|
|
229
241
|
/>
|
|
@@ -260,6 +272,19 @@ const needsNavigationScript = showMobileNavigation || hasCurrentPageSectionMenu
|
|
|
260
272
|
</details>
|
|
261
273
|
)}
|
|
262
274
|
</div>
|
|
275
|
+
{areaReadingFrame && (
|
|
276
|
+
<div class="site-reading-location">
|
|
277
|
+
<AreaNavigationSwitcher
|
|
278
|
+
nodes={siteNavigationTree}
|
|
279
|
+
currentPage={currentPage}
|
|
280
|
+
categoryDestinations={categoryDestinations}
|
|
281
|
+
label={searchPage ? projectConfig.locale.labels.search : undefined}
|
|
282
|
+
/>
|
|
283
|
+
{currentPage
|
|
284
|
+
? <SiteBreadcrumbs currentPage={currentPage} nodes={navigationEntries.map(({ node }) => node)} />
|
|
285
|
+
: <span class="site-reading-location-label">{projectConfig.locale.labels.search}</span>}
|
|
286
|
+
</div>
|
|
287
|
+
)}
|
|
263
288
|
</div>
|
|
264
289
|
)}
|
|
265
290
|
|