@janga/norna 0.7.27 → 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 +37 -28
- package/astro.config.mjs +19 -1
- package/bin/norna-cli.mjs +5 -3
- 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 +16 -7
- 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 +1 -1
- package/scripts/lib/documentation-routes.json +67 -60
- 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 +17 -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 +11 -38
- 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/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 +10 -8
- 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 +9 -6
- 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 +2 -29
- 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 +9 -8
- 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
|
@@ -233,7 +233,6 @@ try {
|
|
|
233
233
|
const siteStructure = await getSiteStructure();
|
|
234
234
|
const contentFiles = siteStructure.contentFiles;
|
|
235
235
|
for (const warning of siteStructure.warnings) {
|
|
236
|
-
if (warning.code === 'empty-category') continue;
|
|
237
236
|
addIssue({
|
|
238
237
|
severity: 'warning',
|
|
239
238
|
message: warning.message,
|
|
@@ -424,17 +423,20 @@ const listedNavigationTree = getListedSiteNavigationTree(siteStructure.nodes.map
|
|
|
424
423
|
return {
|
|
425
424
|
node: {
|
|
426
425
|
...node,
|
|
427
|
-
navigation: {
|
|
428
|
-
listed: node.kind === 'category'
|
|
429
|
-
? true
|
|
430
|
-
: context?.frontmatterData.navigation?.listed ?? true,
|
|
431
|
-
},
|
|
426
|
+
navigation: { listed: context?.frontmatterData.navigation?.listed ?? true },
|
|
432
427
|
},
|
|
433
428
|
};
|
|
434
429
|
}));
|
|
435
430
|
|
|
436
431
|
for (const context of contentFileContexts) {
|
|
437
432
|
const childPages = getDirectChildPages(listedNavigationTree, context.contentFile.pagePath);
|
|
433
|
+
if (context.frontmatterData.page?.listChildren && childPages.length === 0) {
|
|
434
|
+
addContentIssue(context.contentFile, {
|
|
435
|
+
severity: 'warning',
|
|
436
|
+
message: 'page.listChildren has no listed direct child pages to display.',
|
|
437
|
+
fix: 'Add a listed direct child page, or remove page.listChildren while this page has no children.',
|
|
438
|
+
});
|
|
439
|
+
}
|
|
438
440
|
const warnedChildren = new Set();
|
|
439
441
|
|
|
440
442
|
for (const section of context.sections) {
|
|
@@ -444,7 +446,7 @@ for (const context of contentFileContexts) {
|
|
|
444
446
|
if (childPages.length === 0) {
|
|
445
447
|
addSectionIssue(context.contentFile, section, {
|
|
446
448
|
severity: 'error',
|
|
447
|
-
message: `page-list on line ${block.line} has no listed direct child pages to display
|
|
449
|
+
message: `page-list on line ${block.line} has no listed direct child pages to display.`,
|
|
448
450
|
fix: 'Add a listed direct child page or remove the block.',
|
|
449
451
|
});
|
|
450
452
|
continue;
|
|
@@ -33,7 +33,7 @@ const sourceEntries = await readDirectory(sitePublicDir);
|
|
|
33
33
|
const generatedPublicFiles = [
|
|
34
34
|
{
|
|
35
35
|
filename: sitemapFilename,
|
|
36
|
-
explanation: `Norna generates ${sitemapFilename} from the public page tree and the URL in site/config.yaml.`,
|
|
36
|
+
explanation: `Norna generates ${sitemapFilename} from the public page tree and the URL in site/site-config/settings.yaml.`,
|
|
37
37
|
},
|
|
38
38
|
{
|
|
39
39
|
filename: '404.html',
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { siteDir } from './lib/site-paths.mjs';
|
|
2
|
+
import { planSiteUpgrade } from './lib/site-upgrade.mjs';
|
|
3
|
+
|
|
4
|
+
const usage = 'Usage: norna site:upgrade [--apply]\nCheck the root/ source layout. Former layouts require manual conversion; --apply is retained but does not convert old sources.';
|
|
5
|
+
const args = process.argv.slice(2);
|
|
6
|
+
try {
|
|
7
|
+
if (args.includes('--help') || args.includes('-h')) {
|
|
8
|
+
console.log(usage);
|
|
9
|
+
} else {
|
|
10
|
+
if (args.length > 1 || args.some((arg) => arg !== '--apply')) throw new Error(usage);
|
|
11
|
+
await planSiteUpgrade(siteDir);
|
|
12
|
+
console.log('The site already uses the current configuration and homepage layout. No files changed.');
|
|
13
|
+
}
|
|
14
|
+
} catch (error) {
|
|
15
|
+
console.error(error.message);
|
|
16
|
+
process.exitCode = 1;
|
|
17
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
---
|
|
2
|
+
import projectConfig from '../../scripts/lib/project-config.mjs';
|
|
3
|
+
import type { CategoryDestinationMap, SiteNavigationNode } from '../lib/siteNavigation';
|
|
4
|
+
import type { SiteNode } from '../lib/sitePages';
|
|
5
|
+
import AreaNavigationPanel from './AreaNavigationPanel.astro';
|
|
6
|
+
|
|
7
|
+
interface Props {
|
|
8
|
+
node: SiteNavigationNode;
|
|
9
|
+
currentPage?: SiteNode;
|
|
10
|
+
categoryDestinations?: CategoryDestinationMap;
|
|
11
|
+
}
|
|
12
|
+
const { node, currentPage, categoryDestinations } = Astro.props;
|
|
13
|
+
const label = `${projectConfig.locale.labels.navigationMenu}: ${node.node.title}`;
|
|
14
|
+
const panelId = `area-menu-${encodeURIComponent(node.node.pagePath)}`;
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
{node.children.length > 0 && (
|
|
18
|
+
<details class="top-page-menu area-navigation-menu" data-area-menu={node.node.pagePath}>
|
|
19
|
+
<summary aria-label={label} aria-controls={panelId}>
|
|
20
|
+
<span class="top-page-menu-title">{node.node.title}</span>
|
|
21
|
+
</summary>
|
|
22
|
+
<AreaNavigationPanel id={panelId} node={node} currentPage={currentPage} categoryDestinations={categoryDestinations} />
|
|
23
|
+
</details>
|
|
24
|
+
)}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
---
|
|
2
|
+
import { Check } from '@lucide/astro';
|
|
3
|
+
import projectConfig from '../../scripts/lib/project-config.mjs';
|
|
4
|
+
import { withBasePath } from '../lib/basePath';
|
|
5
|
+
import { getNavigationDestination, type CategoryDestinationMap, type SiteNavigationNode } from '../lib/siteNavigation';
|
|
6
|
+
import { getAreaMenuGroups, resolveAreaNavigation } from '../lib/areaNavigation';
|
|
7
|
+
import type { SiteNode } from '../lib/sitePages';
|
|
8
|
+
|
|
9
|
+
interface Props {
|
|
10
|
+
id: string;
|
|
11
|
+
node?: SiteNavigationNode;
|
|
12
|
+
currentPage?: SiteNode;
|
|
13
|
+
categoryDestinations?: CategoryDestinationMap;
|
|
14
|
+
otherRoots?: SiteNavigationNode[];
|
|
15
|
+
}
|
|
16
|
+
const { id, node, currentPage, categoryDestinations, otherRoots = [] } = Astro.props;
|
|
17
|
+
const { localRoot } = resolveAreaNavigation(node ? [node] : [], currentPage);
|
|
18
|
+
const { branches, pages } = node ? getAreaMenuGroups(node) : { branches: [], pages: [] };
|
|
19
|
+
const columnsFor = (count: number, maximum: number) => count === 0 ? 0 : Math.ceil(count / Math.ceil(count / maximum));
|
|
20
|
+
const branchColumns = columnsFor(branches.length, pages.length > 0 ? 2 : 3);
|
|
21
|
+
const pageColumns = branches.length === 0 ? columnsFor(pages.length, 3) : pages.length > 0 ? 1 : 0;
|
|
22
|
+
const groups = [
|
|
23
|
+
{ kind: 'branches', choices: branches, columns: branchColumns },
|
|
24
|
+
{ kind: 'pages', choices: pages, columns: pageColumns },
|
|
25
|
+
].filter(({ choices }) => choices.length > 0);
|
|
26
|
+
---
|
|
27
|
+
|
|
28
|
+
<nav id={id} class="top-page-panel area-navigation-panel"
|
|
29
|
+
style={`--area-menu-columns: ${branchColumns + pageColumns || 2}`}
|
|
30
|
+
aria-label={node ? `${node.node.title}: ${projectConfig.locale.labels.navigationChildren}` : projectConfig.locale.labels.siteNavigation}>
|
|
31
|
+
{groups.map((group) => <ul class="area-navigation-choices" data-area-choice-group={group.kind}
|
|
32
|
+
style={`--area-group-columns: ${group.columns}; --area-group-rows: ${Math.ceil(group.choices.length / group.columns)}`}>
|
|
33
|
+
{group.choices.map((child) => {
|
|
34
|
+
const destination = getNavigationDestination(child.node, categoryDestinations);
|
|
35
|
+
const selected = localRoot === child || currentPage?.pagePath === child.node.pagePath;
|
|
36
|
+
const description = child.node.kind === 'page'
|
|
37
|
+
? child.node.entry.data.page?.description
|
|
38
|
+
: child.node.description;
|
|
39
|
+
return <li>
|
|
40
|
+
<a href={withBasePath(projectConfig.site.basePath, destination.pathname)}
|
|
41
|
+
aria-current={selected ? 'true' : undefined} data-area-choice={child.node.pagePath}>
|
|
42
|
+
<span class="area-navigation-title">{child.node.title}{selected && <Check aria-hidden="true" />}</span>
|
|
43
|
+
{group.kind === 'branches' && description && <span class="area-navigation-description">{description}</span>}
|
|
44
|
+
</a>
|
|
45
|
+
</li>;
|
|
46
|
+
})}
|
|
47
|
+
</ul>)}
|
|
48
|
+
{otherRoots.length > 0 && <ul class="area-navigation-other-roots" aria-label={projectConfig.locale.labels.siteNavigation}>
|
|
49
|
+
{otherRoots.map(({ node: root }) => <li>
|
|
50
|
+
<a href={withBasePath(projectConfig.site.basePath, getNavigationDestination(root, categoryDestinations).pathname)}>
|
|
51
|
+
{root.isHome ? projectConfig.locale.labels.returnHome : root.title}
|
|
52
|
+
</a>
|
|
53
|
+
</li>)}
|
|
54
|
+
</ul>}
|
|
55
|
+
</nav>
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
---
|
|
2
|
+
import { Menu } from '@lucide/astro';
|
|
3
|
+
import projectConfig from '../../scripts/lib/project-config.mjs';
|
|
4
|
+
import { hasLargeAreaMenu, resolveAreaNavigation } from '../lib/areaNavigation';
|
|
5
|
+
import type { CategoryDestinationMap, SiteNavigationNode } from '../lib/siteNavigation';
|
|
6
|
+
import type { SiteNode } from '../lib/sitePages';
|
|
7
|
+
import AreaNavigationPanel from './AreaNavigationPanel.astro';
|
|
8
|
+
|
|
9
|
+
interface Props {
|
|
10
|
+
nodes: SiteNavigationNode[];
|
|
11
|
+
currentPage?: SiteNode;
|
|
12
|
+
categoryDestinations?: CategoryDestinationMap;
|
|
13
|
+
label?: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const { nodes, currentPage, categoryDestinations, label } = Astro.props;
|
|
17
|
+
const { globalRoot } = resolveAreaNavigation(nodes, currentPage);
|
|
18
|
+
const currentLabel = label ?? globalRoot?.node.title ?? projectConfig.locale.labels.navigationMenu;
|
|
19
|
+
const menuRoot = globalRoot && globalRoot.children.length > 0 && !hasLargeAreaMenu(globalRoot) ? globalRoot : undefined;
|
|
20
|
+
const otherRoots = nodes.filter((root) => root !== menuRoot);
|
|
21
|
+
const menuLabel = `${projectConfig.locale.labels.navigationMenu}: ${currentLabel}`;
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
<details class="top-page-menu area-navigation-switcher" data-area-switcher>
|
|
25
|
+
<summary aria-label={menuLabel} title={menuLabel} aria-controls="area-switcher-panel">
|
|
26
|
+
<Menu aria-hidden="true" />
|
|
27
|
+
</summary>
|
|
28
|
+
<AreaNavigationPanel id="area-switcher-panel" node={menuRoot} currentPage={currentPage}
|
|
29
|
+
categoryDestinations={categoryDestinations} otherRoots={otherRoots} />
|
|
30
|
+
</details>
|
|
@@ -104,13 +104,6 @@ const labels = projectConfig.locale.labels;
|
|
|
104
104
|
const imageBounds = image.getBoundingClientRect();
|
|
105
105
|
if (imageBounds.height < usableViewportHeight * persistentCaptionHeightRatio) return;
|
|
106
106
|
|
|
107
|
-
const visibleContentsRail = Array.from(
|
|
108
|
-
layout.querySelectorAll<HTMLElement>('.page-contents-navigation-rail'),
|
|
109
|
-
).some(isVisible);
|
|
110
|
-
const reservesContentsLane = layout.dataset.pageContentsPlacement === 'contents-rail'
|
|
111
|
-
&& document.documentElement.dataset.focusReading !== 'on';
|
|
112
|
-
if (visibleContentsRail || reservesContentsLane) return;
|
|
113
|
-
|
|
114
107
|
const frameBounds = frame.getBoundingClientRect();
|
|
115
108
|
const figureBounds = figure.getBoundingClientRect();
|
|
116
109
|
const captionBounds = caption.getBoundingClientRect();
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
---
|
|
2
|
+
import styles from '../styles/navigation-continuity.css?inline';
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
<style is:inline set:html={styles} />
|
|
6
|
+
<script is:inline>
|
|
7
|
+
// A cold history arrival can restore its position at pagereveal, then lose
|
|
8
|
+
// it to native fragment scrolling when deferred modules finish parsing.
|
|
9
|
+
// Keep the browser's restored position until that initial load completes.
|
|
10
|
+
window.addEventListener('pagereveal', () => {
|
|
11
|
+
if (!location.hash || document.readyState === 'complete'
|
|
12
|
+
|| performance.getEntriesByType('navigation')[0]?.type !== 'back_forward'
|
|
13
|
+
|| history.scrollRestoration !== 'auto') return;
|
|
14
|
+
|
|
15
|
+
history.scrollRestoration = 'manual';
|
|
16
|
+
const release = () => {
|
|
17
|
+
history.scrollRestoration = 'auto';
|
|
18
|
+
window.removeEventListener('load', release);
|
|
19
|
+
window.removeEventListener('pagehide', release);
|
|
20
|
+
};
|
|
21
|
+
window.addEventListener('load', release);
|
|
22
|
+
window.addEventListener('pagehide', release);
|
|
23
|
+
}, { once: true });
|
|
24
|
+
</script>
|
|
25
|
+
<!-- A streamed response must finish the page before its transition snapshot. -->
|
|
26
|
+
<link rel="expect" blocking="render" href="#norna-navigation-ready" />
|
|
@@ -1,19 +1,23 @@
|
|
|
1
1
|
---
|
|
2
2
|
import projectConfig from '../../scripts/lib/project-config.mjs';
|
|
3
3
|
import { withBasePath } from '../lib/basePath';
|
|
4
|
-
import type
|
|
4
|
+
import { getNavigationDestination, type CategoryDestinationMap, type SiteNavigationNode } from '../lib/siteNavigation';
|
|
5
5
|
import type { SiteNode, SitePage } from '../lib/sitePages';
|
|
6
6
|
import PageContentsLinks from './PageContentsLinks.astro';
|
|
7
7
|
|
|
8
8
|
interface Props {
|
|
9
9
|
nodes: SiteNavigationNode[];
|
|
10
10
|
currentPage?: SiteNode;
|
|
11
|
+
categoryDestinations?: CategoryDestinationMap;
|
|
11
12
|
variant: 'mobile' | 'sidebar' | 'submenu';
|
|
12
13
|
integratePageSections?: boolean;
|
|
13
14
|
}
|
|
14
15
|
|
|
15
|
-
const { nodes, currentPage, variant, integratePageSections = false } = Astro.props;
|
|
16
|
+
const { nodes, currentPage, categoryDestinations, variant, integratePageSections = false } = Astro.props;
|
|
16
17
|
const pageHref = (page: SitePage) => withBasePath(projectConfig.site.basePath, page.pathname);
|
|
18
|
+
const categoryHref = (node: SiteNavigationNode) => withBasePath(
|
|
19
|
+
projectConfig.site.basePath, getNavigationDestination(node.node, categoryDestinations).pathname,
|
|
20
|
+
);
|
|
17
21
|
const hasBranch = (node: SiteNavigationNode) => node.children.length > 0;
|
|
18
22
|
const isCurrentPage = (page: SitePage) => currentPage?.pathname === page.pathname;
|
|
19
23
|
const isCurrentBranch = (node: SiteNode) => Boolean(currentPage && (
|
|
@@ -71,13 +75,17 @@ const usesPageDisclosure = (node: SiteNavigationNode) => variant !== 'submenu' &
|
|
|
71
75
|
class="navigation-page-summary"
|
|
72
76
|
aria-controls={getBranchId(node.node)}
|
|
73
77
|
>
|
|
74
|
-
|
|
78
|
+
<a class:list={['navigation-page-summary-title', 'navigation-category-link', {
|
|
79
|
+
'mobile-nav-destination': variant === 'mobile',
|
|
80
|
+
}]}
|
|
81
|
+
href={categoryHref(node)}>{node.node.title}</a>
|
|
75
82
|
<span class="navigation-page-chevron" aria-hidden="true"></span>
|
|
76
83
|
</summary>
|
|
77
84
|
<div id={getBranchId(node.node)} class="navigation-page-branch-content">
|
|
78
85
|
<Astro.self
|
|
79
86
|
nodes={node.children}
|
|
80
87
|
currentPage={currentPage}
|
|
88
|
+
categoryDestinations={categoryDestinations}
|
|
81
89
|
variant={variant}
|
|
82
90
|
integratePageSections={integratePageSections}
|
|
83
91
|
/>
|
|
@@ -123,6 +131,7 @@ const usesPageDisclosure = (node: SiteNavigationNode) => variant !== 'submenu' &
|
|
|
123
131
|
<Astro.self
|
|
124
132
|
nodes={node.children}
|
|
125
133
|
currentPage={currentPage}
|
|
134
|
+
categoryDestinations={categoryDestinations}
|
|
126
135
|
variant={variant}
|
|
127
136
|
integratePageSections={integratePageSections}
|
|
128
137
|
/>
|
|
@@ -174,7 +183,7 @@ const usesPageDisclosure = (node: SiteNavigationNode) => variant !== 'submenu' &
|
|
|
174
183
|
{node.node.title}
|
|
175
184
|
</a>
|
|
176
185
|
{node.children.length > 0 && (
|
|
177
|
-
<Astro.self nodes={node.children} currentPage={currentPage} variant={variant} />
|
|
186
|
+
<Astro.self nodes={node.children} currentPage={currentPage} categoryDestinations={categoryDestinations} variant={variant} />
|
|
178
187
|
)}
|
|
179
188
|
</>
|
|
180
189
|
)}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
---
|
|
2
|
+
import { getNavigationStateScript } from '../lib/navigationStateScript.mjs';
|
|
3
|
+
|
|
4
|
+
interface Props {
|
|
5
|
+
scope: 'desktop' | 'mobile';
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
const { scope } = Astro.props;
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
<script is:inline data-navigation-state-scope={scope} set:html={getNavigationStateScript()} />
|
|
@@ -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`;
|
|
@@ -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>
|