@janga/norna 0.7.25 → 0.7.26
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 +3 -2
- package/astro.config.mjs +26 -12
- package/bin/norna-cli.mjs +12 -0
- package/package.json +14 -5
- package/schemas/card-list.schema.json +121 -0
- package/schemas/category.schema.json +2 -2
- package/schemas/config.schema.json +306 -32
- package/schemas/content-frontmatter.schema.json +7 -7
- package/schemas/image-carousel.schema.json +43 -0
- package/schemas/image-stack.schema.json +43 -0
- package/schemas/manifest.json +6 -1
- package/schemas/page-theme.schema.json +26 -26
- package/schemas/sitewide-content.schema.json +18 -18
- package/schemas/theme.schema.json +237 -262
- package/scripts/dev-local.mjs +24 -2
- package/scripts/generate-schemas.mjs +15 -3
- package/scripts/lib/category-destinations.mjs +48 -0
- package/scripts/lib/code-fence-metadata.mjs +14 -37
- package/scripts/lib/content-tabs.mjs +155 -0
- package/scripts/lib/details-headings.mjs +58 -0
- package/scripts/lib/docusaurus-migration-audit.mjs +603 -0
- package/scripts/lib/editor-language-service.mjs +238 -116
- package/scripts/lib/locale-registry.mjs +597 -0
- package/scripts/lib/locales/az-Latn.mjs +78 -0
- package/scripts/lib/locales/be.mjs +78 -0
- package/scripts/lib/locales/bg.mjs +78 -0
- package/scripts/lib/locales/bs.mjs +78 -0
- package/scripts/lib/locales/ca.mjs +78 -0
- package/scripts/lib/locales/cnr.mjs +78 -0
- package/scripts/lib/locales/cs.mjs +78 -0
- package/scripts/lib/locales/da.mjs +78 -0
- package/scripts/lib/locales/de.mjs +78 -0
- package/scripts/lib/locales/el.mjs +78 -0
- package/scripts/lib/locales/en.mjs +78 -0
- package/scripts/lib/locales/es.mjs +78 -0
- package/scripts/lib/locales/et.mjs +78 -0
- package/scripts/lib/locales/fi.mjs +78 -0
- package/scripts/lib/locales/fil.mjs +78 -0
- package/scripts/lib/locales/fr.mjs +78 -0
- package/scripts/lib/locales/ga.mjs +78 -0
- package/scripts/lib/locales/ha.mjs +78 -0
- package/scripts/lib/locales/hr.mjs +78 -0
- package/scripts/lib/locales/hu.mjs +78 -0
- package/scripts/lib/locales/id.mjs +78 -0
- package/scripts/lib/locales/is.mjs +78 -0
- package/scripts/lib/locales/it.mjs +78 -0
- package/scripts/lib/locales/jv.mjs +78 -0
- package/scripts/lib/locales/lb.mjs +78 -0
- package/scripts/lib/locales/lt.mjs +78 -0
- package/scripts/lib/locales/lv.mjs +78 -0
- package/scripts/lib/locales/mk.mjs +78 -0
- package/scripts/lib/locales/mt.mjs +78 -0
- package/scripts/lib/locales/nb.mjs +78 -0
- package/scripts/lib/locales/nl.mjs +78 -0
- package/scripts/lib/locales/nn.mjs +78 -0
- package/scripts/lib/locales/pcm.mjs +78 -0
- package/scripts/lib/locales/pl.mjs +78 -0
- package/scripts/lib/locales/pt.mjs +78 -0
- package/scripts/lib/locales/ro.mjs +78 -0
- package/scripts/lib/locales/ru.mjs +78 -0
- package/scripts/lib/locales/sk.mjs +78 -0
- package/scripts/lib/locales/sl.mjs +78 -0
- package/scripts/lib/locales/sq.mjs +78 -0
- package/scripts/lib/locales/sr-Cyrl.mjs +78 -0
- package/scripts/lib/locales/sr-Latn.mjs +78 -0
- package/scripts/lib/locales/sv.mjs +78 -0
- package/scripts/lib/locales/sw.mjs +78 -0
- package/scripts/lib/locales/tr.mjs +78 -0
- package/scripts/lib/locales/uk.mjs +78 -0
- package/scripts/lib/locales/vi.mjs +78 -0
- package/scripts/lib/markdown-links.mjs +4 -25
- package/scripts/lib/markdown-notes-render-plugin.mjs +75 -0
- package/scripts/lib/markdown-notes.mjs +165 -0
- package/scripts/lib/norna-markdown-blocks.mjs +32 -447
- package/scripts/lib/norna-markdown-render-plugin.mjs +4 -0
- package/scripts/lib/page-aliases.mjs +12 -1
- package/scripts/lib/page-markdown.mjs +63 -16
- package/scripts/lib/page-move-plan.mjs +22 -5
- package/scripts/lib/pagefind-translations.mjs +620 -0
- package/scripts/lib/presentation-contract.mjs +6 -1
- package/scripts/lib/presentation.mjs +29 -30
- package/scripts/lib/project-config.mjs +2 -173
- package/scripts/lib/schema-definitions.mjs +3 -10
- package/scripts/lib/schema-editor-metadata.mjs +22 -43
- package/scripts/lib/schema-value-definitions.mjs +4 -8
- package/scripts/lib/semantic-callouts.mjs +4 -2
- package/scripts/lib/site-content.mjs +4 -2
- package/scripts/lib/site-link-graph.mjs +16 -6
- package/scripts/lib/site-navigation-tree.mjs +0 -11
- package/scripts/lib/sitemap.mjs +5 -3
- package/scripts/lib/structured-blocks.mjs +108 -0
- package/scripts/lib/table-render-plugin.mjs +46 -3
- package/scripts/lib/table-row-headers.mjs +218 -0
- package/scripts/lib/theme-presets.mjs +3 -10
- package/scripts/lib/yaml-config.mjs +2 -6
- package/scripts/migrate-check.mjs +19 -0
- package/scripts/sync-content-sections.mjs +39 -10
- package/scripts/sync-site-public.mjs +7 -1
- package/src/components/CodeBlockCopyScript.astro +47 -5
- package/src/components/ContentBlocks.astro +39 -0
- package/src/components/ContentTabsScript.astro +60 -0
- package/src/components/DisplaySettings.astro +17 -19
- package/src/components/NavigationPageTree.astro +1 -1
- package/src/components/NoteNavigationScript.astro +4 -0
- package/src/components/PageAliasRedirect.astro +5 -4
- package/src/components/SearchNavigationScript.astro +4 -0
- package/src/components/SearchPage.astro +43 -2
- package/src/components/SectionNavigationScript.astro +43 -11
- package/src/components/SiteBreadcrumbs.astro +1 -1
- package/src/components/SiteNavigation.astro +29 -20
- package/src/components/SitePage.astro +40 -15
- package/src/components/SiteSection.astro +5 -81
- package/src/components/SiteTreeNavigation.astro +2 -2
- package/src/components/TableOverflowScript.astro +386 -58
- package/src/components/TopPageMenu.astro +47 -0
- package/src/components/TreeNavigationScript.astro +10 -3
- package/src/layouts/BaseLayout.astro +8 -12
- package/src/lib/navigationFollowing.ts +142 -0
- package/src/lib/noteNavigation.ts +50 -0
- package/src/lib/searchNavigation.ts +107 -0
- package/src/lib/sectionContent.ts +20 -90
- package/src/lib/siteNavigation.ts +0 -5
- package/src/lib/sitePages.ts +13 -2
- package/src/lib/table-sort-tooltip.ts +109 -0
- package/src/pages/[...slug].astro +16 -6
- package/src/styles/content.css +394 -132
- package/src/styles/foundations.css +9 -0
- package/src/styles/media.css +5 -0
- package/src/styles/navigation.css +63 -74
- package/src/styles/page-layout.css +7 -0
- package/src/styles/responsive.css +2 -7
|
@@ -9,6 +9,8 @@ import {
|
|
|
9
9
|
sitePublicLabel,
|
|
10
10
|
} from './lib/site-paths.mjs';
|
|
11
11
|
import { getSiteStructure } from './lib/site-structure.mjs';
|
|
12
|
+
import { getSiteLinkGraph } from './lib/site-link-graph.mjs';
|
|
13
|
+
import { assertCategoryDestinationModel } from './lib/category-destinations.mjs';
|
|
12
14
|
|
|
13
15
|
const keepAstroPublicEntries = new Set([
|
|
14
16
|
'images',
|
|
@@ -53,9 +55,12 @@ for (const generatedFile of generatedPublicFiles) {
|
|
|
53
55
|
}
|
|
54
56
|
|
|
55
57
|
const siteStructure = await getSiteStructure();
|
|
58
|
+
const { categoryModel } = await getSiteLinkGraph({ siteStructure });
|
|
59
|
+
const { destinations: categoryDestinations } = assertCategoryDestinationModel(categoryModel);
|
|
56
60
|
const sitemapXml = createSitemapXml({
|
|
57
61
|
siteStructure,
|
|
58
62
|
siteUrl: projectConfig.site.url,
|
|
63
|
+
categoryDestinations,
|
|
59
64
|
});
|
|
60
65
|
|
|
61
66
|
await mkdir(astroPublicDir, { recursive: true });
|
|
@@ -79,4 +84,5 @@ for (const entry of sourceEntries) {
|
|
|
79
84
|
await writeFile(path.join(astroPublicDir, sitemapFilename), sitemapXml);
|
|
80
85
|
|
|
81
86
|
console.log(`Synced ${sitePublicLabel}/ to ${astroPublicLabel}/.`);
|
|
82
|
-
|
|
87
|
+
const pageCount = siteStructure.contentFiles.length + categoryDestinations.filter(({ kind }) => kind === 'listing').length;
|
|
88
|
+
console.log(`Generated ${astroPublicLabel}/${sitemapFilename} for ${pageCount} public page${pageCount === 1 ? '' : 's'}.`);
|
|
@@ -14,7 +14,7 @@ const labels = {
|
|
|
14
14
|
<Copy aria-hidden="true" data-code-copy-icon="copy" size={18} stroke-width={2} />
|
|
15
15
|
<Check aria-hidden="true" data-code-copy-icon="copied" hidden size={18} stroke-width={2} />
|
|
16
16
|
<X aria-hidden="true" data-code-copy-icon="failed" hidden size={18} stroke-width={2} />
|
|
17
|
-
<span class="
|
|
17
|
+
<span class="code-block-copy-status" aria-live="polite" data-code-copy-status></span>
|
|
18
18
|
</button>
|
|
19
19
|
</template>
|
|
20
20
|
|
|
@@ -24,6 +24,39 @@ const labels = {
|
|
|
24
24
|
if (!(template instanceof HTMLTemplateElement)) return;
|
|
25
25
|
|
|
26
26
|
const resetTimers = new WeakMap();
|
|
27
|
+
const layoutUpdaters = [];
|
|
28
|
+
const prepareLayout = (wrapper, pre) => {
|
|
29
|
+
const topLevel = wrapper.parentElement?.classList.contains('section-markdown');
|
|
30
|
+
let scheduled = false;
|
|
31
|
+
const update = () => {
|
|
32
|
+
scheduled = false;
|
|
33
|
+
if (topLevel) {
|
|
34
|
+
const layout = ['prose', 'end', 'canvas'].find((candidate) => {
|
|
35
|
+
wrapper.dataset.codeLayout = candidate;
|
|
36
|
+
return pre.scrollWidth <= pre.clientWidth + 1;
|
|
37
|
+
}) ?? 'canvas';
|
|
38
|
+
wrapper.dataset.codeLayout = layout;
|
|
39
|
+
}
|
|
40
|
+
const overflow = pre.scrollWidth > pre.clientWidth + 1;
|
|
41
|
+
wrapper.dataset.codeOverflow = String(overflow);
|
|
42
|
+
if (overflow) pre.setAttribute('tabindex', '0');
|
|
43
|
+
else pre.removeAttribute('tabindex');
|
|
44
|
+
};
|
|
45
|
+
const schedule = () => {
|
|
46
|
+
if (scheduled) return;
|
|
47
|
+
scheduled = true;
|
|
48
|
+
requestAnimationFrame(update);
|
|
49
|
+
};
|
|
50
|
+
layoutUpdaters.push(schedule);
|
|
51
|
+
if ('ResizeObserver' in window) {
|
|
52
|
+
const observer = new ResizeObserver(schedule);
|
|
53
|
+
observer.observe(wrapper.parentElement);
|
|
54
|
+
const body = wrapper.closest('.section-body');
|
|
55
|
+
if (body) observer.observe(body);
|
|
56
|
+
}
|
|
57
|
+
window.addEventListener('resize', schedule, { passive: true });
|
|
58
|
+
update();
|
|
59
|
+
};
|
|
27
60
|
const copyText = async (text) => {
|
|
28
61
|
if (navigator.clipboard?.writeText) {
|
|
29
62
|
try {
|
|
@@ -45,7 +78,7 @@ const labels = {
|
|
|
45
78
|
|
|
46
79
|
const showState = (button, state) => {
|
|
47
80
|
button.querySelectorAll('[data-code-copy-icon]').forEach((icon) => {
|
|
48
|
-
icon.hidden
|
|
81
|
+
icon.toggleAttribute('hidden', icon.getAttribute('data-code-copy-icon') !== state);
|
|
49
82
|
});
|
|
50
83
|
const status = button.querySelector('[data-code-copy-status]');
|
|
51
84
|
if (status) status.textContent = state === 'copied' ? labels.copied : labels.failed;
|
|
@@ -54,11 +87,11 @@ const labels = {
|
|
|
54
87
|
clearTimeout(resetTimers.get(button));
|
|
55
88
|
resetTimers.set(button, setTimeout(() => {
|
|
56
89
|
button.querySelectorAll('[data-code-copy-icon]').forEach((icon) => {
|
|
57
|
-
icon.hidden
|
|
90
|
+
icon.toggleAttribute('hidden', icon.getAttribute('data-code-copy-icon') !== 'copy');
|
|
58
91
|
});
|
|
59
|
-
|
|
92
|
+
// Keep the live announcement available after the visual feedback ends.
|
|
60
93
|
delete button.dataset.copyState;
|
|
61
|
-
}, 2000));
|
|
94
|
+
}, state === 'copied' ? 1000 : 2000));
|
|
62
95
|
};
|
|
63
96
|
|
|
64
97
|
document.querySelectorAll('#main-content .section-markdown pre > code').forEach((code) => {
|
|
@@ -79,9 +112,12 @@ const labels = {
|
|
|
79
112
|
if (!(button instanceof HTMLButtonElement)) return;
|
|
80
113
|
const title = codeExample?.querySelector(':scope > .norna-code-title');
|
|
81
114
|
(title ?? wrapper).append(button);
|
|
115
|
+
prepareLayout(wrapper, pre);
|
|
82
116
|
button.addEventListener('click', async () => {
|
|
83
117
|
if (button.dataset.copyBusy === 'true') return;
|
|
84
118
|
button.dataset.copyBusy = 'true';
|
|
119
|
+
const status = button.querySelector('[data-code-copy-status]');
|
|
120
|
+
if (status) status.textContent = '';
|
|
85
121
|
try {
|
|
86
122
|
await copyText(code.textContent ?? '');
|
|
87
123
|
showState(button, 'copied');
|
|
@@ -92,6 +128,12 @@ const labels = {
|
|
|
92
128
|
}
|
|
93
129
|
});
|
|
94
130
|
});
|
|
131
|
+
const preferences = new MutationObserver(() => layoutUpdaters.forEach((update) => update()));
|
|
132
|
+
preferences.observe(document.documentElement, {
|
|
133
|
+
attributes: true,
|
|
134
|
+
attributeFilter: ['data-reading-width', 'data-focus-reading', 'dir'],
|
|
135
|
+
});
|
|
136
|
+
document.fonts?.ready.then(() => layoutUpdaters.forEach((update) => update()));
|
|
95
137
|
|
|
96
138
|
template.remove();
|
|
97
139
|
})();
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
---
|
|
2
|
+
import CardList from './CardList.astro';
|
|
3
|
+
import ImageCarousel from './ImageCarousel.astro';
|
|
4
|
+
import ImageStack from './ImageStack.astro';
|
|
5
|
+
import PageList from './PageList.astro';
|
|
6
|
+
import type { SectionContentBlock } from '../lib/sectionContent';
|
|
7
|
+
|
|
8
|
+
interface Props {
|
|
9
|
+
blocks: SectionContentBlock[];
|
|
10
|
+
title: string;
|
|
11
|
+
images: {
|
|
12
|
+
maxAvailableWidthPercent: { desktop: number; mobile: number };
|
|
13
|
+
maxAvailableHeightPercent?: { desktop: number; mobile: number };
|
|
14
|
+
};
|
|
15
|
+
cardWidth: 'text' | 'narrow' | 'normal' | 'wide';
|
|
16
|
+
prioritizeFirstImage?: boolean;
|
|
17
|
+
}
|
|
18
|
+
const { blocks, title, images, cardWidth, prioritizeFirstImage = false } = Astro.props;
|
|
19
|
+
const firstMediaIndex = blocks.findIndex((block) => block.type === 'image-stack' || block.type === 'image-carousel');
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
{blocks.map((block, index) => {
|
|
23
|
+
if (block.type === 'html') return <div class="section-markdown" set:html={block.html} />;
|
|
24
|
+
if (block.type === 'tabs') return (
|
|
25
|
+
<div class="content-tabs content-block-note-lane-boundary" data-content-tabs>
|
|
26
|
+
<div class="content-tabs-controls" data-tabs-controls hidden></div>
|
|
27
|
+
{block.panels.map((panel, panelIndex) => (
|
|
28
|
+
<div class="content-tab-panel" data-tab-panel role="group" aria-labelledby={`norna-tabs-label-${block.index}-${panelIndex}`}>
|
|
29
|
+
<div class="content-tab-label" data-tab-label id={`norna-tabs-label-${block.index}-${panelIndex}`}>{panel.label}</div>
|
|
30
|
+
<Astro.self blocks={panel.contentBlocks} title={title} images={images} cardWidth={cardWidth} />
|
|
31
|
+
</div>
|
|
32
|
+
))}
|
|
33
|
+
</div>
|
|
34
|
+
);
|
|
35
|
+
if (block.type === 'page-list') return <PageList pages={block.pages} />;
|
|
36
|
+
if (block.type === 'card-list') return <CardList cards={block.cards} layout={block.layout} flow={block.flow} size={block.size} width={block.width ?? cardWidth} />;
|
|
37
|
+
if (block.type === 'image-carousel') return <ImageCarousel title={title} images={block.images} maxAvailableWidthPercent={images.maxAvailableWidthPercent} maxAvailableHeightPercent={images.maxAvailableHeightPercent} priorityFirstImage={prioritizeFirstImage && index === firstMediaIndex} />;
|
|
38
|
+
return <ImageStack title={title} images={block.images} maxAvailableWidthPercent={images.maxAvailableWidthPercent} priorityFirstImage={prioritizeFirstImage && index === firstMediaIndex} />;
|
|
39
|
+
})}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
<script>
|
|
2
|
+
document.querySelectorAll<HTMLElement>('[data-content-tabs]').forEach((group, groupIndex) => {
|
|
3
|
+
const controls = group.querySelector<HTMLElement>('[data-tabs-controls]')!;
|
|
4
|
+
const panels = Array.from(group.querySelectorAll<HTMLElement>(':scope > [data-tab-panel]'));
|
|
5
|
+
const labels = panels.map((panel) => panel.querySelector<HTMLElement>('[data-tab-label]')!);
|
|
6
|
+
const buttons = panels.map((panel, index) => {
|
|
7
|
+
const button = document.createElement('button');
|
|
8
|
+
const id = `norna-tabs-${groupIndex}-${index}`;
|
|
9
|
+
button.type = 'button';
|
|
10
|
+
button.id = `${id}-tab`;
|
|
11
|
+
button.textContent = labels[index].textContent;
|
|
12
|
+
button.setAttribute('role', 'tab');
|
|
13
|
+
button.setAttribute('aria-controls', id);
|
|
14
|
+
panel.id = id;
|
|
15
|
+
panel.setAttribute('role', 'tabpanel');
|
|
16
|
+
panel.setAttribute('aria-labelledby', button.id);
|
|
17
|
+
panel.tabIndex = 0;
|
|
18
|
+
controls.append(button);
|
|
19
|
+
return button;
|
|
20
|
+
});
|
|
21
|
+
const heading = group.closest('section')?.querySelector<HTMLElement>('h1, h2');
|
|
22
|
+
controls.setAttribute('role', 'tablist');
|
|
23
|
+
if (heading?.id) controls.setAttribute('aria-labelledby', heading.id);
|
|
24
|
+
const select = (selected: number, focus = false) => {
|
|
25
|
+
buttons.forEach((button, index) => {
|
|
26
|
+
button.setAttribute('aria-selected', String(index === selected));
|
|
27
|
+
button.tabIndex = index === selected ? 0 : -1;
|
|
28
|
+
panels[index].hidden = index !== selected;
|
|
29
|
+
});
|
|
30
|
+
if (focus) buttons[selected].focus({ preventScroll: true });
|
|
31
|
+
group.dispatchEvent(new CustomEvent('norna:tab-change', { bubbles: true }));
|
|
32
|
+
window.dispatchEvent(new Event('resize'));
|
|
33
|
+
};
|
|
34
|
+
buttons.forEach((button, index) => {
|
|
35
|
+
button.addEventListener('click', () => select(index));
|
|
36
|
+
button.addEventListener('keydown', (event) => {
|
|
37
|
+
let selected = index;
|
|
38
|
+
if (event.key === 'ArrowRight') selected = (index + 1) % buttons.length;
|
|
39
|
+
else if (event.key === 'ArrowLeft') selected = (index - 1 + buttons.length) % buttons.length;
|
|
40
|
+
else if (event.key === 'Home') selected = 0;
|
|
41
|
+
else if (event.key === 'End') selected = buttons.length - 1;
|
|
42
|
+
else return;
|
|
43
|
+
event.preventDefault();
|
|
44
|
+
select(selected, true);
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
controls.hidden = false;
|
|
48
|
+
group.dataset.tabsReady = 'true';
|
|
49
|
+
select(0);
|
|
50
|
+
const revealTarget = () => {
|
|
51
|
+
let id: string;
|
|
52
|
+
try { id = decodeURIComponent(location.hash.slice(1)); } catch { return; }
|
|
53
|
+
const target = document.getElementById(id);
|
|
54
|
+
const index = panels.findIndex((panel) => target && panel.contains(target));
|
|
55
|
+
if (index >= 0) { select(index); target?.scrollIntoView(); }
|
|
56
|
+
};
|
|
57
|
+
window.addEventListener('hashchange', revealTarget);
|
|
58
|
+
revealTarget();
|
|
59
|
+
});
|
|
60
|
+
</script>
|
|
@@ -49,25 +49,23 @@ const widthOptions = [
|
|
|
49
49
|
<form class="display-settings-panel">
|
|
50
50
|
<p class="display-settings-title">{labels.displaySettings}</p>
|
|
51
51
|
|
|
52
|
-
|
|
53
|
-
<
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
<
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
</fieldset>
|
|
70
|
-
)}
|
|
52
|
+
<fieldset>
|
|
53
|
+
<legend>{labels.appearance}</legend>
|
|
54
|
+
<div class="display-settings-segmented">
|
|
55
|
+
{appearanceOptions.map((option) => (
|
|
56
|
+
<label>
|
|
57
|
+
<input
|
|
58
|
+
type="radio"
|
|
59
|
+
name="appearance"
|
|
60
|
+
value={option.value}
|
|
61
|
+
checked={defaults.appearance === option.value}
|
|
62
|
+
data-reader-appearance
|
|
63
|
+
/>
|
|
64
|
+
<span>{option.label}</span>
|
|
65
|
+
</label>
|
|
66
|
+
))}
|
|
67
|
+
</div>
|
|
68
|
+
</fieldset>
|
|
71
69
|
|
|
72
70
|
{controls.readingWidth && (
|
|
73
71
|
<fieldset>
|
|
@@ -6,9 +6,10 @@ import { withBasePath } from '../lib/basePath';
|
|
|
6
6
|
interface Props {
|
|
7
7
|
targetPathname: string;
|
|
8
8
|
targetTitle: string;
|
|
9
|
+
categoryTitle?: string;
|
|
9
10
|
}
|
|
10
11
|
|
|
11
|
-
const { targetPathname, targetTitle } = Astro.props;
|
|
12
|
+
const { targetPathname, targetTitle, categoryTitle } = Astro.props;
|
|
12
13
|
const { labels, lang } = projectConfig.locale;
|
|
13
14
|
const targetHref = withBasePath(projectConfig.site.basePath, targetPathname);
|
|
14
15
|
const canonicalUrl = getAbsolutePageUrl(projectConfig.site.url, targetPathname);
|
|
@@ -22,7 +23,7 @@ const canonicalUrl = getAbsolutePageUrl(projectConfig.site.url, targetPathname);
|
|
|
22
23
|
<meta name="robots" content="noindex, follow" />
|
|
23
24
|
<meta http-equiv="refresh" content={`0; url=${targetHref}`} />
|
|
24
25
|
<link rel="canonical" href={canonicalUrl} />
|
|
25
|
-
<title>{labels.pageMoved}: {targetTitle}</title>
|
|
26
|
+
<title>{categoryTitle ?? labels.pageMoved}: {targetTitle}</title>
|
|
26
27
|
<style is:inline>
|
|
27
28
|
:root {
|
|
28
29
|
color-scheme: light dark;
|
|
@@ -42,8 +43,8 @@ const canonicalUrl = getAbsolutePageUrl(projectConfig.site.url, targetPathname);
|
|
|
42
43
|
</head>
|
|
43
44
|
<body>
|
|
44
45
|
<main>
|
|
45
|
-
<h1>{labels.pageMoved}</h1>
|
|
46
|
-
<p>{labels.pageMovedText} <a href={targetHref}>{targetTitle}</a>.</p>
|
|
46
|
+
<h1>{categoryTitle ?? labels.pageMoved}</h1>
|
|
47
|
+
<p>{!categoryTitle && labels.pageMovedText} <a href={targetHref}>{targetTitle}</a>.</p>
|
|
47
48
|
</main>
|
|
48
49
|
</body>
|
|
49
50
|
</html>
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
import { getCollection } from 'astro:content';
|
|
3
|
+
import { ArrowLeft } from '@lucide/astro';
|
|
3
4
|
import { resolveNavigationModel } from '../../scripts/lib/navigation-model.mjs';
|
|
4
5
|
import { getTextWidthCssValue, resolveFrameColors, resolvePagePresentation } from '../../scripts/lib/presentation.mjs';
|
|
5
6
|
import { projectConfig, resolveThemeVisualConfig } from '../../scripts/lib/project-config.mjs';
|
|
@@ -82,6 +83,9 @@ const hasNavigationRailFrame = navigationModel.requestedMode === 'tree'
|
|
|
82
83
|
&& (navigationModel.hasNestedPages || navigationModel.hasCategories)
|
|
83
84
|
);
|
|
84
85
|
const bundlePath = withBasePath(projectConfig.site.basePath, '/pagefind/');
|
|
86
|
+
const pagefindTranslationOption = projectConfig.locale.pagefindTranslations
|
|
87
|
+
? `,\n\t\t\ttranslations: ${JSON.stringify(projectConfig.locale.pagefindTranslations)}`
|
|
88
|
+
: '';
|
|
85
89
|
const searchScript = `(() => {
|
|
86
90
|
const root = document.querySelector('#norna-search');
|
|
87
91
|
const status = root?.querySelector('[data-search-status]');
|
|
@@ -95,7 +99,7 @@ const searchScript = `(() => {
|
|
|
95
99
|
bundlePath: ${JSON.stringify(bundlePath)},
|
|
96
100
|
baseUrl: ${JSON.stringify(projectConfig.site.basePath)},
|
|
97
101
|
showImages: false,
|
|
98
|
-
showSubResults: true
|
|
102
|
+
showSubResults: true${pagefindTranslationOption}
|
|
99
103
|
});
|
|
100
104
|
const labelInput = () => {
|
|
101
105
|
const input = root.querySelector('.pagefind-ui__search-input');
|
|
@@ -145,7 +149,44 @@ const searchScript = `(() => {
|
|
|
145
149
|
pagePresentation={pagePresentation}
|
|
146
150
|
visualTheme={visualTheme}
|
|
147
151
|
sectionIndex={0}
|
|
148
|
-
|
|
152
|
+
>
|
|
153
|
+
<div slot="before-heading" class="search-return">
|
|
154
|
+
<a href={projectConfig.site.basePath} data-search-return data-search-return-label={labels.searchReturn}>
|
|
155
|
+
<ArrowLeft aria-hidden="true" />
|
|
156
|
+
<span>{labels.returnHome}</span>
|
|
157
|
+
</a>
|
|
158
|
+
</div>
|
|
159
|
+
</SiteSection>
|
|
149
160
|
</main>
|
|
150
161
|
<script is:inline set:html={searchScript} />
|
|
151
162
|
</BaseLayout>
|
|
163
|
+
|
|
164
|
+
<style>
|
|
165
|
+
.search-return {
|
|
166
|
+
width: var(--layout-heading-width);
|
|
167
|
+
margin-inline: var(--layout-inline-start-margin) var(--layout-inline-end-margin);
|
|
168
|
+
margin-bottom: 1rem;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
.search-return a {
|
|
172
|
+
display: inline-flex;
|
|
173
|
+
align-items: center;
|
|
174
|
+
gap: 0.5rem;
|
|
175
|
+
min-height: 2.75rem;
|
|
176
|
+
max-width: 100%;
|
|
177
|
+
color: var(--color-link-text);
|
|
178
|
+
font-size: 0.9375rem;
|
|
179
|
+
line-height: 1.5;
|
|
180
|
+
text-underline-offset: 0.2em;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
.search-return svg {
|
|
184
|
+
width: 1.125rem;
|
|
185
|
+
height: 1.125rem;
|
|
186
|
+
flex-shrink: 0;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
.search-return span {
|
|
190
|
+
overflow-wrap: anywhere;
|
|
191
|
+
}
|
|
192
|
+
</style>
|
|
@@ -1,15 +1,18 @@
|
|
|
1
1
|
<script>
|
|
2
|
+
import { setupNavigationFollowing } from '../lib/navigationFollowing';
|
|
3
|
+
|
|
2
4
|
const siteTop = document.querySelector<HTMLElement>('.site-top');
|
|
3
5
|
const mobileMenu = document.querySelector<HTMLDetailsElement>('.mobile-nav-menu');
|
|
4
6
|
const sectionLinks = Array.from(document.querySelectorAll<HTMLAnchorElement>(
|
|
5
|
-
'.mobile-nav-sections a, .page-contents-links a, .page-nav a',
|
|
7
|
+
'.mobile-nav-sections a, .page-contents-links a, .page-nav a, .top-page-sections a',
|
|
6
8
|
));
|
|
7
|
-
const
|
|
9
|
+
const topPageMenus = Array.from(document.querySelectorAll<HTMLDetailsElement>('.top-page-menu'));
|
|
8
10
|
const mobileDestinationLinks = Array.from(mobileMenu?.querySelectorAll<HTMLAnchorElement>('.mobile-nav-destination') ?? []);
|
|
9
11
|
const mobileMenuSummary = mobileMenu?.querySelector<HTMLElement>(':scope > summary');
|
|
10
12
|
const mobileMenuPanel = mobileMenu?.querySelector<HTMLElement>('[data-compact-navigation-panel]');
|
|
11
13
|
const mobileMenuClose = mobileMenu?.querySelector<HTMLButtonElement>('[data-compact-navigation-close]');
|
|
12
14
|
const sectionTrackingEnabled = document.documentElement.dataset.sectionTracking === 'enabled';
|
|
15
|
+
const navigationFollowing = sectionTrackingEnabled ? setupNavigationFollowing() : undefined;
|
|
13
16
|
const trackedHeadingIds = new Set(sectionLinks.flatMap((link) => {
|
|
14
17
|
const linkUrl = new URL(link.href, window.location.href);
|
|
15
18
|
if (linkUrl.pathname !== window.location.pathname || !linkUrl.hash) return [];
|
|
@@ -101,6 +104,7 @@
|
|
|
101
104
|
link.removeAttribute('aria-current');
|
|
102
105
|
}
|
|
103
106
|
});
|
|
107
|
+
navigationFollowing?.update();
|
|
104
108
|
};
|
|
105
109
|
|
|
106
110
|
const updateReadingPosition = () => {
|
|
@@ -146,6 +150,7 @@
|
|
|
146
150
|
link.removeAttribute('aria-current');
|
|
147
151
|
}
|
|
148
152
|
});
|
|
153
|
+
navigationFollowing?.update();
|
|
149
154
|
};
|
|
150
155
|
|
|
151
156
|
const scheduleReadingPositionUpdate = () => {
|
|
@@ -153,11 +158,38 @@
|
|
|
153
158
|
sectionTrackingFrame = window.requestAnimationFrame(updateReadingPosition);
|
|
154
159
|
};
|
|
155
160
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
161
|
+
topPageMenus.forEach((menu) => {
|
|
162
|
+
menu.addEventListener('toggle', () => {
|
|
163
|
+
if (!menu.open) return;
|
|
164
|
+
topPageMenus.forEach((other) => { if (other !== menu) other.open = false; });
|
|
165
|
+
const panel = menu.querySelector<HTMLElement>('.top-page-panel');
|
|
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`);
|
|
172
|
+
});
|
|
173
|
+
menu.addEventListener('keydown', (event) => {
|
|
174
|
+
if (event.key !== 'Escape' || !menu.open) return;
|
|
175
|
+
event.preventDefault();
|
|
176
|
+
menu.open = false;
|
|
177
|
+
menu.querySelector('summary')?.focus();
|
|
178
|
+
});
|
|
179
|
+
menu.addEventListener('focusout', (event) => {
|
|
180
|
+
if (event.relatedTarget instanceof Node && !menu.contains(event.relatedTarget)) menu.open = false;
|
|
181
|
+
});
|
|
182
|
+
menu.querySelectorAll('a').forEach((link) => {
|
|
183
|
+
link.addEventListener('click', () => { menu.open = false; });
|
|
184
|
+
});
|
|
160
185
|
});
|
|
186
|
+
if (topPageMenus.length > 0) {
|
|
187
|
+
document.addEventListener('pointerdown', (event) => {
|
|
188
|
+
topPageMenus.forEach((menu) => {
|
|
189
|
+
if (event.target instanceof Node && !menu.contains(event.target)) menu.open = false;
|
|
190
|
+
});
|
|
191
|
+
});
|
|
192
|
+
}
|
|
161
193
|
|
|
162
194
|
if (mobileMenu) {
|
|
163
195
|
const siteHeader = mobileMenu.closest<HTMLElement>('.site-top');
|
|
@@ -192,7 +224,7 @@
|
|
|
192
224
|
};
|
|
193
225
|
const closeMenu = (restoreFocus: boolean) => {
|
|
194
226
|
mobileMenu.open = false;
|
|
195
|
-
if (restoreFocus) mobileMenuSummary?.focus();
|
|
227
|
+
if (restoreFocus) mobileMenuSummary?.focus({ preventScroll: true });
|
|
196
228
|
};
|
|
197
229
|
|
|
198
230
|
mobileDestinationLinks.forEach((link) => {
|
|
@@ -206,9 +238,10 @@
|
|
|
206
238
|
document.documentElement.classList.toggle('mobile-navigation-open', mobileMenu.open);
|
|
207
239
|
setBackgroundInert(mobileMenu.open);
|
|
208
240
|
if (mobileMenu.open) {
|
|
209
|
-
window.requestAnimationFrame(() =>
|
|
210
|
-
mobileMenuClose?.focus()
|
|
211
|
-
|
|
241
|
+
window.requestAnimationFrame(() => {
|
|
242
|
+
mobileMenuClose?.focus({ preventScroll: true });
|
|
243
|
+
navigationFollowing?.revealCompact();
|
|
244
|
+
});
|
|
212
245
|
}
|
|
213
246
|
});
|
|
214
247
|
|
|
@@ -247,7 +280,6 @@
|
|
|
247
280
|
|
|
248
281
|
pauseSectionTrackingForAnchorNavigation();
|
|
249
282
|
if (mobileMenu) mobileMenu.open = false;
|
|
250
|
-
link.closest<HTMLElement>('.site-nav-item')?.setAttribute('data-submenu-dismissed', 'true');
|
|
251
283
|
|
|
252
284
|
window.requestAnimationFrame(() => {
|
|
253
285
|
updateCurrentSection();
|
|
@@ -4,16 +4,17 @@ import projectConfig from '../../scripts/lib/project-config.mjs';
|
|
|
4
4
|
import { withBasePath } from '../lib/basePath';
|
|
5
5
|
import type { SectionNavigation } from '../lib/sectionContent';
|
|
6
6
|
import {
|
|
7
|
-
getFirstPageInNavigationNode,
|
|
8
7
|
getListedSiteNavigationTree,
|
|
9
8
|
type SiteNavigationEntry,
|
|
10
9
|
type SiteNavigationNode,
|
|
11
10
|
} from '../lib/siteNavigation';
|
|
12
|
-
import type { SiteNode
|
|
11
|
+
import type { SiteNode } from '../lib/sitePages';
|
|
13
12
|
import DisplaySettings from './DisplaySettings.astro';
|
|
14
13
|
import NavigationPageTree from './NavigationPageTree.astro';
|
|
15
14
|
import NavigationTreeControls from './NavigationTreeControls.astro';
|
|
15
|
+
import TopPageMenu from './TopPageMenu.astro';
|
|
16
16
|
import SectionNavigationScript from './SectionNavigationScript.astro';
|
|
17
|
+
import SearchNavigationScript from './SearchNavigationScript.astro';
|
|
17
18
|
|
|
18
19
|
type FrameColors = {
|
|
19
20
|
backgroundColor: string;
|
|
@@ -26,7 +27,7 @@ interface Props {
|
|
|
26
27
|
alt: string;
|
|
27
28
|
height?: string;
|
|
28
29
|
};
|
|
29
|
-
currentPage?:
|
|
30
|
+
currentPage?: SiteNode;
|
|
30
31
|
navigationEntries: SiteNavigationEntry[];
|
|
31
32
|
navigationMode: 'sections' | 'top' | 'tree';
|
|
32
33
|
showTopNavigationSubmenus: boolean;
|
|
@@ -67,6 +68,12 @@ const navigationStyle = [
|
|
|
67
68
|
...(logo?.height ? [`--site-brand-logo-height: ${logo.height}`] : []),
|
|
68
69
|
].join('; ');
|
|
69
70
|
const siteNavigationTree = getListedSiteNavigationTree(navigationEntries);
|
|
71
|
+
const mobileNavigationTree = navigationMode === 'top'
|
|
72
|
+
? getListedSiteNavigationTree(navigationEntries.map((entry) => ({
|
|
73
|
+
...entry,
|
|
74
|
+
headings: entry.headings.filter((heading) => heading.depth === 2),
|
|
75
|
+
})))
|
|
76
|
+
: siteNavigationTree;
|
|
70
77
|
const topLevelNavigation = siteNavigationTree;
|
|
71
78
|
const showSiteNavigation = topLevelNavigation.some(({ node }) => !node.isHome);
|
|
72
79
|
const homeNode = navigationEntries.find(({ node }) => node.isHome)?.node;
|
|
@@ -75,13 +82,13 @@ if (!homePage) throw new Error('Site navigation requires a homepage.');
|
|
|
75
82
|
const currentPageNavigation = currentPage
|
|
76
83
|
? navigationEntries.find(({ node }) => node.kind === 'page' && node.pathname === currentPage.pathname)
|
|
77
84
|
: undefined;
|
|
78
|
-
const isCurrentPage = (page:
|
|
85
|
+
const isCurrentPage = (page: SiteNode) => currentPage?.pathname === page.pathname;
|
|
79
86
|
const isCurrentBranch = (node: SiteNode) => Boolean(currentPage && (
|
|
80
87
|
currentPage.pageDirectory === node.pageDirectory
|
|
81
88
|
|| currentPage.pageDirectory.startsWith(`${node.pageDirectory}/pages/`)
|
|
82
89
|
));
|
|
83
|
-
const pageHref = (page:
|
|
84
|
-
const getNavigationPage = (node: SiteNavigationNode) =>
|
|
90
|
+
const pageHref = (page: SiteNode) => withBasePath(projectConfig.site.basePath, page.pathname);
|
|
91
|
+
const getNavigationPage = (node: SiteNavigationNode) => node.node;
|
|
85
92
|
const hasSectionMenu = (pageSections: SectionNavigation[]) => pageSections.length > 1;
|
|
86
93
|
const showSinglePageTopLink = !showSiteNavigation;
|
|
87
94
|
const currentPageSectionCount = currentPageNavigation?.sections.length ?? 0;
|
|
@@ -97,6 +104,7 @@ const hasTreePageContents = Boolean(
|
|
|
97
104
|
);
|
|
98
105
|
const showPageNavigation = Boolean(
|
|
99
106
|
navigationMode !== 'tree'
|
|
107
|
+
&& (navigationMode !== 'top' || showSinglePageTopLink)
|
|
100
108
|
&& currentPageNavigation
|
|
101
109
|
&& (showSinglePageTopLink || hasCurrentPageSectionMenu),
|
|
102
110
|
);
|
|
@@ -107,7 +115,7 @@ const showSearch = projectConfig.search.enabled;
|
|
|
107
115
|
const searchHref = withBasePath(projectConfig.site.basePath, '/search/');
|
|
108
116
|
const showDesktopNavigationRow = showSiteNavigation || Boolean(logo) || showReaderControls || showSearch;
|
|
109
117
|
const showNavigationRow = showMobileNavigation || showDesktopNavigationRow;
|
|
110
|
-
const needsNavigationScript =
|
|
118
|
+
const needsNavigationScript = showMobileNavigation || hasCurrentPageSectionMenu || hasTreePageContents;
|
|
111
119
|
---
|
|
112
120
|
|
|
113
121
|
<a class="skip-link" href="#main-content">{projectConfig.locale.labels.skipToContent}</a>
|
|
@@ -138,22 +146,20 @@ const needsNavigationScript = hasCurrentPageSectionMenu || hasTreePageContents |
|
|
|
138
146
|
{topLevelNavigation.map((navigationNode) => {
|
|
139
147
|
const destination = getNavigationPage(navigationNode);
|
|
140
148
|
if (!destination) return null;
|
|
141
|
-
const nodeIsCurrentPage =
|
|
149
|
+
const nodeIsCurrentPage = isCurrentPage(navigationNode.node);
|
|
142
150
|
return (
|
|
143
151
|
<li class:list={['site-nav-item', {
|
|
144
|
-
'site-nav-item-has-submenu': showTopNavigationSubmenus && navigationNode.children.length > 0,
|
|
145
152
|
'site-nav-item-current-branch': !nodeIsCurrentPage && isCurrentBranch(navigationNode.node),
|
|
146
153
|
}]}>
|
|
147
154
|
<a href={pageHref(destination)} aria-current={nodeIsCurrentPage ? 'page' : undefined}>
|
|
148
155
|
{navigationNode.node.title}
|
|
149
156
|
</a>
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
)}
|
|
157
|
+
<TopPageMenu
|
|
158
|
+
node={navigationNode}
|
|
159
|
+
currentPage={currentPage}
|
|
160
|
+
showSections={navigationMode === 'top'}
|
|
161
|
+
showChildren={showTopNavigationSubmenus}
|
|
162
|
+
/>
|
|
157
163
|
</li>
|
|
158
164
|
);
|
|
159
165
|
})}
|
|
@@ -165,6 +171,8 @@ const needsNavigationScript = hasCurrentPageSectionMenu || hasTreePageContents |
|
|
|
165
171
|
<a
|
|
166
172
|
class="site-search-link"
|
|
167
173
|
href={searchHref}
|
|
174
|
+
data-search-base={projectConfig.site.basePath}
|
|
175
|
+
data-search-source-title={currentPage?.title}
|
|
168
176
|
aria-current={searchPage ? 'page' : undefined}
|
|
169
177
|
aria-label={projectConfig.locale.labels.search}
|
|
170
178
|
title={projectConfig.locale.labels.search}
|
|
@@ -211,18 +219,18 @@ const needsNavigationScript = hasCurrentPageSectionMenu || hasTreePageContents |
|
|
|
211
219
|
<h2 tabindex="-1">{projectConfig.locale.labels.siteNavigation}</h2>
|
|
212
220
|
<NavigationTreeControls
|
|
213
221
|
idPrefix="mobile-tree-navigation"
|
|
214
|
-
nodes={
|
|
222
|
+
nodes={mobileNavigationTree}
|
|
215
223
|
/>
|
|
216
224
|
<NavigationPageTree
|
|
217
|
-
nodes={
|
|
225
|
+
nodes={mobileNavigationTree}
|
|
218
226
|
currentPage={currentPage}
|
|
219
227
|
variant="mobile"
|
|
220
|
-
integratePageSections={navigationMode === 'tree'}
|
|
228
|
+
integratePageSections={navigationMode === 'tree' || navigationMode === 'top'}
|
|
221
229
|
/>
|
|
222
230
|
</nav>
|
|
223
231
|
)}
|
|
224
232
|
|
|
225
|
-
{navigationMode
|
|
233
|
+
{(navigationMode === 'sections' || !showSiteNavigation) && hasCurrentPageSectionMenu && currentPageNavigation && currentPage && (
|
|
226
234
|
<nav class="mobile-nav-view" aria-label={projectConfig.locale.labels.pageNavigation}>
|
|
227
235
|
{!showSinglePageTopLink && (
|
|
228
236
|
<h2 tabindex="-1">{projectConfig.locale.labels.pageNavigation}</h2>
|
|
@@ -285,3 +293,4 @@ const needsNavigationScript = hasCurrentPageSectionMenu || hasTreePageContents |
|
|
|
285
293
|
</header>
|
|
286
294
|
|
|
287
295
|
{needsNavigationScript && <SectionNavigationScript />}
|
|
296
|
+
{showSearch && <SearchNavigationScript />}
|