@janga/norna 0.7.22 → 0.7.24
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +14 -18
- package/astro.config.mjs +15 -0
- package/bin/norna-cli.mjs +8 -2
- package/package.json +28 -3
- package/schemas/category.schema.json +2 -2
- package/schemas/config.schema.json +52 -24
- package/schemas/content-frontmatter.schema.json +25 -5
- package/schemas/page-theme.schema.json +53 -25
- package/schemas/sitewide-content.schema.json +19 -18
- package/schemas/theme.schema.json +385 -252
- package/scripts/build-site.mjs +1 -0
- package/scripts/check-config.mjs +36 -5
- package/scripts/deploy-site.mjs +0 -1
- package/scripts/dev-local.mjs +175 -112
- package/scripts/generate-search-index.mjs +57 -0
- package/scripts/init-site.mjs +1 -0
- package/scripts/lib/code-fence-metadata.mjs +223 -0
- package/scripts/lib/content-sync-apply.mjs +34 -0
- package/scripts/lib/content-sync-plan.mjs +179 -0
- package/scripts/lib/edit-source-link.mjs +49 -0
- package/scripts/lib/editor-language-service.mjs +160 -12
- package/scripts/lib/heading-ids.mjs +1 -1
- package/scripts/lib/image-presentation.mjs +6 -0
- package/scripts/lib/markdown-links.mjs +182 -0
- package/scripts/lib/navigation-model.mjs +44 -12
- package/scripts/lib/navigation-review.mjs +396 -0
- package/scripts/lib/norna-markdown-blocks.mjs +74 -26
- package/scripts/lib/norna-markdown-render-plugin.mjs +64 -1
- package/scripts/lib/page-aliases.mjs +184 -0
- package/scripts/lib/page-markdown.mjs +88 -7
- package/scripts/lib/page-move-plan.mjs +659 -0
- package/scripts/lib/presentation-palette-metadata.mjs +1 -1
- package/scripts/lib/presentation.mjs +10 -37
- package/scripts/lib/project-config.mjs +146 -19
- package/scripts/lib/public-asset-conventions.mjs +36 -2
- package/scripts/lib/schema-definitions.mjs +43 -15
- package/scripts/lib/schema-editor-metadata.mjs +60 -32
- package/scripts/lib/schema-value-definitions.mjs +6 -2
- package/scripts/lib/semantic-callouts.mjs +128 -0
- package/scripts/lib/site-content.mjs +28 -5
- package/scripts/lib/site-link-graph.mjs +357 -0
- package/scripts/lib/site-navigation-tree.mjs +85 -0
- package/scripts/lib/site-page-urls.mjs +10 -0
- package/scripts/lib/sitemap.mjs +34 -0
- package/scripts/lib/social-image-assets.mjs +30 -0
- package/scripts/lib/theme-presets.mjs +53 -25
- package/scripts/lib/theme-profiles.mjs +6 -8
- package/scripts/lib/yaml-config.mjs +6 -2
- package/scripts/move-site-page.mjs +256 -0
- package/scripts/review-navigation.mjs +32 -0
- package/scripts/sync-content-sections.mjs +182 -216
- package/scripts/sync-site-public.mjs +43 -3
- package/src/components/CodeBlockCopyScript.astro +97 -0
- package/src/components/DisplaySettings.astro +4 -4
- package/src/components/EditSourceLink.astro +17 -0
- package/src/components/ImageCarousel.astro +6 -3
- package/src/components/NavigationPageTree.astro +81 -86
- package/src/components/NavigationTreeControls.astro +77 -0
- package/src/components/PageAliasRedirect.astro +49 -0
- package/src/components/PageContentsNavigation.astro +21 -0
- package/src/components/PageList.astro +33 -0
- package/src/components/PageSequenceNavigation.astro +37 -0
- package/src/components/SearchPage.astro +151 -0
- package/src/components/SectionNavigationScript.astro +62 -6
- package/src/components/SiteNavigation.astro +50 -11
- package/src/components/SitePage.astro +113 -20
- package/src/components/SiteSection.astro +16 -3
- package/src/components/SiteTreeNavigation.astro +8 -2
- package/src/components/TreeNavigationScript.astro +207 -26
- package/src/content.config.ts +5 -2
- package/src/layouts/BaseLayout.astro +71 -10
- package/src/lib/readerPreferencesScript.mjs +2 -2
- package/src/lib/sectionContent.ts +32 -3
- package/src/lib/siteNavigation.ts +31 -52
- package/src/lib/sitePages.ts +3 -2
- package/src/lib/sitePublicAssets.ts +1 -0
- package/src/pages/404.astro +110 -0
- package/src/pages/[...slug].astro +45 -8
- package/src/styles/content.css +718 -0
- package/src/styles/foundations.css +284 -0
- package/src/styles/global.css +6 -2510
- package/src/styles/media.css +518 -0
- package/src/styles/navigation.css +526 -0
- package/src/styles/page-layout.css +786 -0
- package/src/styles/responsive.css +594 -0
- package/starters/basic/README.md +6 -19
- package/starters/basic/package.json +1 -0
- package/starters/basic/site/pages/000-home/content.md +13 -50
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
<script>
|
|
2
2
|
type NavigationState = {
|
|
3
3
|
openPaths: string[];
|
|
4
|
-
|
|
4
|
+
sectionOpenByPath: Record<string, boolean>;
|
|
5
|
+
scrollTop: number;
|
|
6
|
+
};
|
|
7
|
+
type NavigationSnapshot = {
|
|
8
|
+
branchOpen: boolean[];
|
|
9
|
+
sectionOpen: boolean[];
|
|
5
10
|
scrollTop: number;
|
|
6
11
|
};
|
|
7
12
|
|
|
@@ -11,13 +16,34 @@
|
|
|
11
16
|
const branches = Array.from(container.querySelectorAll<HTMLDetailsElement>(
|
|
12
17
|
'.navigation-page-disclosure[data-page-path]',
|
|
13
18
|
));
|
|
14
|
-
|
|
19
|
+
const sectionBranches = Array.from(container.querySelectorAll<HTMLDetailsElement>(
|
|
20
|
+
'.navigation-page-sections-disclosure[data-page-path]',
|
|
21
|
+
));
|
|
22
|
+
const navigationNodes = Array.from(container.querySelectorAll<HTMLElement>(
|
|
23
|
+
'.navigation-page-node[data-navigation-title]',
|
|
24
|
+
));
|
|
25
|
+
const getScrollContainer = () => {
|
|
26
|
+
let candidate: HTMLElement | null = container;
|
|
27
|
+
|
|
28
|
+
while (candidate) {
|
|
29
|
+
if (/auto|scroll/.test(getComputedStyle(candidate).overflowY)) return candidate;
|
|
30
|
+
candidate = candidate.parentElement;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return container;
|
|
34
|
+
};
|
|
35
|
+
const scrollContainer = getScrollContainer();
|
|
36
|
+
let filterSnapshot: NavigationSnapshot | null = null;
|
|
15
37
|
|
|
16
38
|
try {
|
|
17
39
|
const parsedState = JSON.parse(sessionStorage.getItem(stateKey) ?? '{}');
|
|
18
|
-
storedState = {
|
|
40
|
+
const storedState: NavigationState = {
|
|
19
41
|
openPaths: Array.isArray(parsedState.openPaths) ? parsedState.openPaths : [],
|
|
20
|
-
|
|
42
|
+
sectionOpenByPath: parsedState.sectionOpenByPath
|
|
43
|
+
&& typeof parsedState.sectionOpenByPath === 'object'
|
|
44
|
+
? Object.fromEntries(Object.entries(parsedState.sectionOpenByPath)
|
|
45
|
+
.filter((entry): entry is [string, boolean] => typeof entry[1] === 'boolean'))
|
|
46
|
+
: {},
|
|
21
47
|
scrollTop: Number.isFinite(Number(parsedState.scrollTop)) ? Number(parsedState.scrollTop) : 0,
|
|
22
48
|
};
|
|
23
49
|
|
|
@@ -25,15 +51,18 @@
|
|
|
25
51
|
const pagePath = branch.dataset.pagePath ?? '';
|
|
26
52
|
branch.open = branch.dataset.currentBranch === 'true'
|
|
27
53
|
|| storedState.openPaths.includes(pagePath);
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
54
|
+
});
|
|
55
|
+
sectionBranches.forEach((branch) => {
|
|
56
|
+
const pagePath = branch.dataset.pagePath ?? '';
|
|
57
|
+
const storedOpen = storedState.sectionOpenByPath[pagePath];
|
|
58
|
+
branch.open = typeof storedOpen === 'boolean'
|
|
59
|
+
? storedOpen
|
|
60
|
+
: branch.dataset.currentPage === 'true';
|
|
32
61
|
});
|
|
33
62
|
|
|
34
63
|
if (storedState.scrollTop > 0) {
|
|
35
64
|
requestAnimationFrame(() => {
|
|
36
|
-
|
|
65
|
+
scrollContainer.scrollTop = storedState.scrollTop;
|
|
37
66
|
});
|
|
38
67
|
}
|
|
39
68
|
} catch {
|
|
@@ -41,37 +70,189 @@
|
|
|
41
70
|
}
|
|
42
71
|
|
|
43
72
|
const saveState = () => {
|
|
73
|
+
if (filterSnapshot) return;
|
|
74
|
+
|
|
44
75
|
try {
|
|
45
76
|
const openBranches = branches.filter((branch) => branch.open);
|
|
77
|
+
const sectionOpenByPath = Object.fromEntries(sectionBranches.map((branch) => [
|
|
78
|
+
branch.dataset.pagePath ?? '',
|
|
79
|
+
branch.open,
|
|
80
|
+
]));
|
|
46
81
|
const state: NavigationState = {
|
|
47
82
|
openPaths: openBranches.map((branch) => branch.dataset.pagePath ?? ''),
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
branch.dataset.sectionsOpen === 'true'
|
|
51
|
-
|| branch.dataset.currentPage === 'true'
|
|
52
|
-
))
|
|
53
|
-
.map((branch) => branch.dataset.pagePath ?? ''),
|
|
54
|
-
scrollTop: container.scrollTop,
|
|
83
|
+
sectionOpenByPath,
|
|
84
|
+
scrollTop: scrollContainer.scrollTop,
|
|
55
85
|
};
|
|
56
86
|
sessionStorage.setItem(stateKey, JSON.stringify(state));
|
|
57
87
|
} catch {
|
|
58
88
|
// Navigation state persistence is an optional enhancement.
|
|
59
89
|
}
|
|
60
90
|
};
|
|
91
|
+
const controls = container.querySelector<HTMLElement>('[data-tree-controls]');
|
|
92
|
+
const filterInput = controls?.querySelector<HTMLInputElement>('[data-tree-filter]') ?? null;
|
|
93
|
+
const expandButton = controls?.querySelector<HTMLButtonElement>('[data-tree-expand-all]') ?? null;
|
|
94
|
+
const collapseButton = controls?.querySelector<HTMLButtonElement>('[data-tree-collapse-all]') ?? null;
|
|
95
|
+
const locateButton = controls?.querySelector<HTMLButtonElement>('[data-tree-locate-current]') ?? null;
|
|
96
|
+
const emptyMessage = controls?.querySelector<HTMLElement>('[data-tree-filter-empty]') ?? null;
|
|
97
|
+
const status = controls?.querySelector<HTMLElement>('[data-tree-status]') ?? null;
|
|
98
|
+
const currentPageNode = container.querySelector<HTMLElement>(
|
|
99
|
+
'.navigation-page-node[data-navigation-current-page="true"]',
|
|
100
|
+
);
|
|
101
|
+
const labels = {
|
|
102
|
+
collapsed: controls?.dataset.treeLabelCollapsed ?? '',
|
|
103
|
+
expanded: controls?.dataset.treeLabelExpanded ?? '',
|
|
104
|
+
filterMatches: controls?.dataset.treeLabelFilterMatches ?? '{count}',
|
|
105
|
+
located: controls?.dataset.treeLabelLocated ?? '',
|
|
106
|
+
};
|
|
107
|
+
const normalizeTitle = (value: string) => value
|
|
108
|
+
.normalize('NFKD')
|
|
109
|
+
.replace(/\p{Mark}+/gu, '')
|
|
110
|
+
.toLocaleLowerCase(document.documentElement.lang || undefined)
|
|
111
|
+
.trim();
|
|
112
|
+
const setStatus = (message: string) => {
|
|
113
|
+
if (status) status.textContent = message;
|
|
114
|
+
};
|
|
115
|
+
const getSnapshot = (): NavigationSnapshot => ({
|
|
116
|
+
branchOpen: branches.map((branch) => branch.open),
|
|
117
|
+
sectionOpen: sectionBranches.map((branch) => branch.open),
|
|
118
|
+
scrollTop: scrollContainer.scrollTop,
|
|
119
|
+
});
|
|
120
|
+
const revealNodeAndAncestors = (node: HTMLElement) => {
|
|
121
|
+
let currentNode: HTMLElement | null = node;
|
|
122
|
+
|
|
123
|
+
while (currentNode && container.contains(currentNode)) {
|
|
124
|
+
currentNode.hidden = false;
|
|
125
|
+
const parentNode = currentNode.parentElement?.closest<HTMLElement>(
|
|
126
|
+
'.navigation-page-node[data-navigation-title]',
|
|
127
|
+
) ?? null;
|
|
128
|
+
const parentBranch = parentNode?.querySelector<HTMLDetailsElement>(
|
|
129
|
+
':scope > .navigation-page-disclosure[data-page-path]',
|
|
130
|
+
);
|
|
131
|
+
if (parentBranch) parentBranch.open = true;
|
|
132
|
+
currentNode = parentNode;
|
|
133
|
+
}
|
|
134
|
+
};
|
|
135
|
+
const restoreFilterSnapshot = () => {
|
|
136
|
+
if (!filterSnapshot) return;
|
|
61
137
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
138
|
+
branches.forEach((branch, index) => { branch.open = filterSnapshot?.branchOpen[index] ?? false; });
|
|
139
|
+
sectionBranches.forEach((branch, index) => { branch.open = filterSnapshot?.sectionOpen[index] ?? false; });
|
|
140
|
+
scrollContainer.scrollTop = filterSnapshot.scrollTop;
|
|
141
|
+
filterSnapshot = null;
|
|
142
|
+
requestAnimationFrame(saveState);
|
|
143
|
+
};
|
|
144
|
+
const clearFilter = () => {
|
|
145
|
+
navigationNodes.forEach((node) => { node.hidden = false; });
|
|
146
|
+
delete container.dataset.treeFilterActive;
|
|
147
|
+
if (emptyMessage) emptyMessage.hidden = true;
|
|
148
|
+
if (expandButton) expandButton.disabled = false;
|
|
149
|
+
if (collapseButton) collapseButton.disabled = false;
|
|
150
|
+
restoreFilterSnapshot();
|
|
151
|
+
setStatus('');
|
|
152
|
+
};
|
|
153
|
+
const applyFilter = () => {
|
|
154
|
+
const query = normalizeTitle(filterInput?.value ?? '');
|
|
155
|
+
|
|
156
|
+
if (!query) {
|
|
157
|
+
clearFilter();
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if (!filterSnapshot) filterSnapshot = getSnapshot();
|
|
162
|
+
container.dataset.treeFilterActive = 'true';
|
|
163
|
+
if (expandButton) expandButton.disabled = true;
|
|
164
|
+
if (collapseButton) collapseButton.disabled = true;
|
|
165
|
+
navigationNodes.forEach((node) => { node.hidden = true; });
|
|
166
|
+
|
|
167
|
+
const matches = navigationNodes.filter((node) => normalizeTitle(
|
|
168
|
+
node.dataset.navigationTitle ?? '',
|
|
169
|
+
).includes(query));
|
|
170
|
+
matches.forEach((node) => {
|
|
171
|
+
revealNodeAndAncestors(node);
|
|
172
|
+
|
|
173
|
+
if (node.dataset.navigationKind === 'category') {
|
|
174
|
+
node.querySelectorAll<HTMLElement>('.navigation-page-node[data-navigation-title]')
|
|
175
|
+
.forEach((descendant) => { descendant.hidden = false; });
|
|
176
|
+
const categoryBranch = node.querySelector<HTMLDetailsElement>(
|
|
177
|
+
':scope > .navigation-category-disclosure',
|
|
178
|
+
);
|
|
179
|
+
if (categoryBranch) categoryBranch.open = true;
|
|
68
180
|
}
|
|
69
|
-
|
|
70
|
-
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
if (matches.length === 0 && currentPageNode) revealNodeAndAncestors(currentPageNode);
|
|
184
|
+
if (emptyMessage) emptyMessage.hidden = matches.length > 0;
|
|
185
|
+
setStatus(labels.filterMatches.replace('{count}', String(matches.length)));
|
|
186
|
+
};
|
|
187
|
+
const setAllOpen = (open: boolean) => {
|
|
188
|
+
branches.forEach((branch) => { branch.open = open; });
|
|
189
|
+
sectionBranches.forEach((branch) => { branch.open = open; });
|
|
190
|
+
saveState();
|
|
191
|
+
setStatus(open ? labels.expanded : labels.collapsed);
|
|
192
|
+
};
|
|
193
|
+
const locateCurrentPage = () => {
|
|
194
|
+
if (!currentPageNode) return;
|
|
195
|
+
|
|
196
|
+
if (filterInput?.value) {
|
|
197
|
+
filterInput.value = '';
|
|
198
|
+
clearFilter();
|
|
199
|
+
}
|
|
200
|
+
revealNodeAndAncestors(currentPageNode);
|
|
201
|
+
currentPageNode.querySelectorAll<HTMLDetailsElement>(
|
|
202
|
+
':scope > .navigation-page-disclosure, :scope > .navigation-page-sections-disclosure',
|
|
203
|
+
).forEach((branch) => { branch.open = true; });
|
|
204
|
+
saveState();
|
|
205
|
+
|
|
206
|
+
const currentPageLink = currentPageNode.querySelector<HTMLAnchorElement>(
|
|
207
|
+
':scope > .navigation-page-link, :scope > .navigation-page-open-link',
|
|
208
|
+
);
|
|
209
|
+
if (!currentPageLink) return;
|
|
210
|
+
|
|
211
|
+
currentPageLink.focus({ preventScroll: true });
|
|
212
|
+
const scrollBox = scrollContainer.getBoundingClientRect();
|
|
213
|
+
const linkBox = currentPageLink.getBoundingClientRect();
|
|
214
|
+
scrollContainer.scrollTop += linkBox.top - scrollBox.top
|
|
215
|
+
- Math.max(0, (scrollContainer.clientHeight - linkBox.height) / 2);
|
|
216
|
+
setStatus(labels.located);
|
|
217
|
+
};
|
|
218
|
+
const openLinkedPage = (link: HTMLAnchorElement) => {
|
|
219
|
+
if (filterInput?.value) {
|
|
220
|
+
filterInput.value = '';
|
|
221
|
+
clearFilter();
|
|
222
|
+
}
|
|
223
|
+
const pageNode = link.closest<HTMLElement>('.navigation-page-node');
|
|
224
|
+
const pageDisclosure = pageNode?.querySelector<HTMLDetailsElement>(
|
|
225
|
+
':scope > .navigation-page-disclosure:not(.navigation-category-disclosure)',
|
|
226
|
+
);
|
|
227
|
+
const sectionDisclosure = pageNode?.querySelector<HTMLDetailsElement>(
|
|
228
|
+
':scope > .navigation-page-sections-disclosure[data-page-path]',
|
|
229
|
+
);
|
|
230
|
+
|
|
231
|
+
if (pageDisclosure) pageDisclosure.open = true;
|
|
232
|
+
if (sectionDisclosure) sectionDisclosure.open = true;
|
|
233
|
+
saveState();
|
|
234
|
+
};
|
|
235
|
+
|
|
236
|
+
requestAnimationFrame(() => {
|
|
237
|
+
branches.forEach((branch) => branch.addEventListener('toggle', saveState));
|
|
238
|
+
sectionBranches.forEach((branch) => branch.addEventListener('toggle', saveState));
|
|
71
239
|
});
|
|
72
240
|
|
|
73
|
-
|
|
74
|
-
|
|
241
|
+
if (controls) controls.hidden = false;
|
|
242
|
+
if (locateButton && !currentPageNode) locateButton.disabled = true;
|
|
243
|
+
filterInput?.addEventListener('input', applyFilter);
|
|
244
|
+
filterInput?.addEventListener('keydown', (event) => {
|
|
245
|
+
if (event.key !== 'Escape' || !filterInput.value) return;
|
|
246
|
+
event.preventDefault();
|
|
247
|
+
filterInput.value = '';
|
|
248
|
+
clearFilter();
|
|
249
|
+
});
|
|
250
|
+
expandButton?.addEventListener('click', () => setAllOpen(true));
|
|
251
|
+
collapseButton?.addEventListener('click', () => setAllOpen(false));
|
|
252
|
+
locateButton?.addEventListener('click', locateCurrentPage);
|
|
253
|
+
scrollContainer.addEventListener('scroll', saveState, { passive: true });
|
|
254
|
+
container.querySelectorAll<HTMLAnchorElement>('.navigation-page-link, .navigation-page-open-link')
|
|
255
|
+
.forEach((link) => link.addEventListener('click', () => openLinkedPage(link)));
|
|
75
256
|
window.addEventListener('pagehide', saveState);
|
|
76
257
|
};
|
|
77
258
|
|
package/src/content.config.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { defineCollection } from 'astro:content';
|
|
2
2
|
import { glob } from 'astro/loaders';
|
|
3
|
+
import { z } from 'astro/zod';
|
|
3
4
|
import { basename } from 'node:path';
|
|
4
5
|
import { pathToFileURL } from 'node:url';
|
|
5
6
|
import {
|
|
@@ -19,7 +20,9 @@ const siteEntryId = `${siteDirLabel
|
|
|
19
20
|
.replace(/^[./\\]+/, '')
|
|
20
21
|
.replace(/[^a-zA-Z0-9-]+/g, '-')
|
|
21
22
|
.replace(/^-+|-+$/g, '') || 'site'}-content`;
|
|
22
|
-
const
|
|
23
|
+
const emptyYamlMapping = (value: unknown) => value ?? {};
|
|
24
|
+
const siteThemeSchema = z.preprocess(emptyYamlMapping, themeVisualSchema);
|
|
25
|
+
const sitewideContentSchema = z.preprocess(emptyYamlMapping, sitewideSchema);
|
|
23
26
|
|
|
24
27
|
const site = defineCollection({
|
|
25
28
|
loader: glob({
|
|
@@ -52,7 +55,7 @@ const sitewide = defineCollection({
|
|
|
52
55
|
base: pathToFileURL(siteDir),
|
|
53
56
|
generateId: () => `${siteEntryId.replace(/-content$/, '')}-sitewide`,
|
|
54
57
|
}),
|
|
55
|
-
schema:
|
|
58
|
+
schema: sitewideContentSchema,
|
|
56
59
|
});
|
|
57
60
|
|
|
58
61
|
export const collections = { site, theme, sitewide };
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
import projectConfig from '../../scripts/lib/project-config.mjs';
|
|
3
3
|
import { getPresentationCssVariables } from '../../scripts/lib/presentation.mjs';
|
|
4
|
-
import { getIconLinks } from '../lib/sitePublicAssets';
|
|
4
|
+
import { getIconLinks, getSocialImageAsset } from '../lib/sitePublicAssets';
|
|
5
5
|
import { withBasePath } from '../lib/basePath';
|
|
6
6
|
import { getReaderPreferencesScript } from '../lib/readerPreferencesScript.mjs';
|
|
7
7
|
import '../styles/global.css';
|
|
@@ -19,7 +19,7 @@ type Presentation = {
|
|
|
19
19
|
page: FrameColors;
|
|
20
20
|
css: Record<string, string>;
|
|
21
21
|
}>;
|
|
22
|
-
|
|
22
|
+
appearance: {
|
|
23
23
|
default: 'system' | 'light' | 'dark';
|
|
24
24
|
};
|
|
25
25
|
readerPreferences: {
|
|
@@ -52,9 +52,10 @@ type VisualTheme = {
|
|
|
52
52
|
};
|
|
53
53
|
};
|
|
54
54
|
images: {
|
|
55
|
+
presentation: 'prose-aligned' | 'centered-fit';
|
|
55
56
|
width: string;
|
|
56
57
|
maxAvailableWidthPercent: { desktop: number; mobile: number };
|
|
57
|
-
maxAvailableHeightPercent
|
|
58
|
+
maxAvailableHeightPercent?: { desktop: number; mobile: number };
|
|
58
59
|
};
|
|
59
60
|
typography: { fontFamily: string };
|
|
60
61
|
corners: {
|
|
@@ -75,7 +76,9 @@ interface Props {
|
|
|
75
76
|
copyrightMessage?: string;
|
|
76
77
|
buildInfo?: boolean;
|
|
77
78
|
};
|
|
78
|
-
|
|
79
|
+
contentsNavigation?: boolean;
|
|
80
|
+
indexable?: boolean;
|
|
81
|
+
searchable?: boolean;
|
|
79
82
|
}
|
|
80
83
|
|
|
81
84
|
const {
|
|
@@ -86,12 +89,18 @@ const {
|
|
|
86
89
|
frameColors,
|
|
87
90
|
pathname = '/',
|
|
88
91
|
footer,
|
|
89
|
-
|
|
92
|
+
contentsNavigation = false,
|
|
93
|
+
indexable = true,
|
|
94
|
+
searchable = false,
|
|
90
95
|
} = Astro.props;
|
|
91
96
|
|
|
92
97
|
const documentTitle = title;
|
|
93
98
|
const canonicalUrl = new URL(withBasePath(projectConfig.site.basePath, pathname), projectConfig.site.url).href;
|
|
94
99
|
const iconLinks = getIconLinks();
|
|
100
|
+
const socialImageAsset = getSocialImageAsset();
|
|
101
|
+
const socialImageUrl = socialImageAsset
|
|
102
|
+
? new URL(withBasePath(projectConfig.site.basePath, socialImageAsset.href), projectConfig.site.url).href
|
|
103
|
+
: null;
|
|
95
104
|
const scrollBehavior = projectConfig.navigation.scrollBehavior === 'smooth' ? 'smooth' : 'auto';
|
|
96
105
|
const formatPercent = (value: number) => `${value}%`;
|
|
97
106
|
const formatViewportHeightPercent = (value: number) => `${value}svh`;
|
|
@@ -124,9 +133,46 @@ const documentStyle = [
|
|
|
124
133
|
`--image-width: ${visualTheme.images.width}`,
|
|
125
134
|
`--image-max-available-width-desktop: ${formatPercent(visualTheme.images.maxAvailableWidthPercent.desktop)}`,
|
|
126
135
|
`--image-max-available-width-mobile: ${formatPercent(visualTheme.images.maxAvailableWidthPercent.mobile)}`,
|
|
127
|
-
|
|
128
|
-
|
|
136
|
+
...(visualTheme.images.maxAvailableHeightPercent
|
|
137
|
+
? [
|
|
138
|
+
`--image-max-height-desktop: ${formatViewportHeightPercent(visualTheme.images.maxAvailableHeightPercent.desktop)}`,
|
|
139
|
+
`--image-max-height-mobile: ${formatViewportHeightPercent(visualTheme.images.maxAvailableHeightPercent.mobile)}`,
|
|
140
|
+
]
|
|
141
|
+
: []),
|
|
129
142
|
].join('; ');
|
|
143
|
+
const marginNoteReadingWidths = [
|
|
144
|
+
['narrow', 'min(60ch, 680px)'],
|
|
145
|
+
['standard', 'min(72ch, 680px)'],
|
|
146
|
+
['wide', `min(80ch, ${visualTheme.images.width})`],
|
|
147
|
+
] as const;
|
|
148
|
+
const vacantContentsTrackWidth = `11rem + ${visualTheme.layout.localNavigationGap}`;
|
|
149
|
+
const marginNoteDeclarations = [
|
|
150
|
+
'float: right;',
|
|
151
|
+
'clear: right;',
|
|
152
|
+
'width: var(--layout-note-width);',
|
|
153
|
+
'margin: 0 calc(0px - var(--layout-note-width) - var(--layout-note-gap)) 0.75rem var(--layout-note-gap);',
|
|
154
|
+
].join('\n');
|
|
155
|
+
const marginNoteLayoutCss = [
|
|
156
|
+
'@media (min-width: 1101px) {',
|
|
157
|
+
...marginNoteReadingWidths.flatMap(([readingWidth, textWidth]) => [
|
|
158
|
+
[
|
|
159
|
+
`@container (min-width: calc(${textWidth} + ${visualTheme.layout.noteWidth} + ${visualTheme.layout.noteGap})) {`,
|
|
160
|
+
`:root[data-reading-width="${readingWidth}"] .section-note {`,
|
|
161
|
+
marginNoteDeclarations,
|
|
162
|
+
'}',
|
|
163
|
+
'}',
|
|
164
|
+
].join('\n'),
|
|
165
|
+
[
|
|
166
|
+
`@container (min-width: calc(${textWidth} + ${visualTheme.layout.noteWidth} + ${visualTheme.layout.noteGap} - ${vacantContentsTrackWidth})) {`,
|
|
167
|
+
`:root[data-reading-width="${readingWidth}"] .site-page-layout-tree:not(.site-page-layout-contents) .section-note,`,
|
|
168
|
+
`:root[data-reading-width="${readingWidth}"][data-focus-reading="on"] .site-page-layout-tree .section-note {`,
|
|
169
|
+
marginNoteDeclarations,
|
|
170
|
+
'}',
|
|
171
|
+
'}',
|
|
172
|
+
].join('\n'),
|
|
173
|
+
]),
|
|
174
|
+
'}',
|
|
175
|
+
].join('\n');
|
|
130
176
|
const { buildInfo, copyrightMessage } = footer ?? {};
|
|
131
177
|
const showBuildInfo = buildInfo === true;
|
|
132
178
|
const buildTime = showBuildInfo
|
|
@@ -154,20 +200,34 @@ const readerPreferencesScript = readerPreferencesEnabled
|
|
|
154
200
|
<!doctype html>
|
|
155
201
|
<html
|
|
156
202
|
lang={projectConfig.locale.lang}
|
|
157
|
-
data-
|
|
203
|
+
data-appearance={presentation.readerPreferences.defaults.appearance}
|
|
204
|
+
data-image-presentation={visualTheme.images.presentation}
|
|
158
205
|
data-reading-width={presentation.readerPreferences.defaults.readingWidth}
|
|
159
206
|
data-focus-reading={presentation.readerPreferences.defaults.focusReading}
|
|
160
|
-
data-section-tracking={
|
|
207
|
+
data-section-tracking={contentsNavigation ? 'enabled' : undefined}
|
|
208
|
+
data-pagefind-ignore={searchable ? undefined : 'all'}
|
|
161
209
|
style={documentStyle}
|
|
162
210
|
>
|
|
163
211
|
<head>
|
|
164
212
|
<meta charset="utf-8" />
|
|
165
213
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
166
214
|
<meta name="format-detection" content="telephone=no" />
|
|
215
|
+
{!indexable && <meta name="robots" content="noindex" />}
|
|
167
216
|
{description && <meta name="description" content={description} />}
|
|
217
|
+
{indexable && <meta property="og:type" content="website" />}
|
|
218
|
+
{indexable && <meta property="og:title" content={documentTitle} />}
|
|
219
|
+
{indexable && <meta property="og:url" content={canonicalUrl} />}
|
|
220
|
+
{indexable && description && <meta property="og:description" content={description} />}
|
|
221
|
+
{indexable && socialImageUrl && <meta property="og:image" content={socialImageUrl} />}
|
|
222
|
+
{indexable && socialImageAsset && <meta property="og:image:type" content={socialImageAsset.type} />}
|
|
223
|
+
{indexable && <meta name="twitter:card" content={socialImageUrl ? 'summary_large_image' : 'summary'} />}
|
|
224
|
+
{indexable && <meta name="twitter:title" content={documentTitle} />}
|
|
225
|
+
{indexable && description && <meta name="twitter:description" content={description} />}
|
|
226
|
+
{indexable && socialImageUrl && <meta name="twitter:image" content={socialImageUrl} />}
|
|
168
227
|
<meta name="theme-color" content={presentation.palette.page.backgroundColor} />
|
|
169
228
|
{readerPreferencesScript && <script is:inline set:html={readerPreferencesScript} />}
|
|
170
|
-
<
|
|
229
|
+
<style is:inline set:html={marginNoteLayoutCss} />
|
|
230
|
+
{indexable && <link rel="canonical" href={canonicalUrl} />}
|
|
171
231
|
<meta name="generator" content={Astro.generator} />
|
|
172
232
|
{iconLinks.map((iconLink) => (
|
|
173
233
|
<link
|
|
@@ -177,6 +237,7 @@ const readerPreferencesScript = readerPreferencesEnabled
|
|
|
177
237
|
href={iconLink.href}
|
|
178
238
|
/>
|
|
179
239
|
))}
|
|
240
|
+
<slot name="head" />
|
|
180
241
|
<title>{documentTitle}</title>
|
|
181
242
|
</head>
|
|
182
243
|
<body>
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
const preferenceDefinitions = Object.freeze({
|
|
2
2
|
appearance: Object.freeze({
|
|
3
3
|
allowed: Object.freeze(['system', 'light', 'dark']),
|
|
4
|
-
cookieName: 'norna-
|
|
4
|
+
cookieName: 'norna-appearance',
|
|
5
5
|
}),
|
|
6
6
|
readingWidth: Object.freeze({
|
|
7
7
|
allowed: Object.freeze(['narrow', 'standard', 'wide']),
|
|
@@ -72,7 +72,7 @@ export const getReaderPreferencesScript = ({ controls, defaults, cookiePath }) =
|
|
|
72
72
|
const apply = (name, value, persist = false) => {
|
|
73
73
|
const nextValue = isAllowed(name, value) ? value : defaults[name];
|
|
74
74
|
values[name] = nextValue;
|
|
75
|
-
if (name === 'appearance') root.dataset.
|
|
75
|
+
if (name === 'appearance') root.dataset.appearance = nextValue;
|
|
76
76
|
if (name === 'readingWidth') root.dataset.readingWidth = nextValue;
|
|
77
77
|
if (name === 'focusReading') root.dataset.focusReading = nextValue;
|
|
78
78
|
if (persist) writeCookie(definitions[name].cookieName, nextValue);
|
|
@@ -36,7 +36,16 @@ type ParsedCardListBlock = {
|
|
|
36
36
|
width?: CardListWidth;
|
|
37
37
|
cards: CardListItem[];
|
|
38
38
|
};
|
|
39
|
-
type
|
|
39
|
+
type ParsedPageListBlock = {
|
|
40
|
+
type: 'page-list';
|
|
41
|
+
line: number;
|
|
42
|
+
};
|
|
43
|
+
type ParsedNornaBlock = ParsedImageBlock | ParsedCardListBlock | ParsedPageListBlock;
|
|
44
|
+
type PageListItem = {
|
|
45
|
+
title: string;
|
|
46
|
+
pathname: string;
|
|
47
|
+
description?: string;
|
|
48
|
+
};
|
|
40
49
|
type InlineNote = {
|
|
41
50
|
markdown: string;
|
|
42
51
|
number: number;
|
|
@@ -48,7 +57,8 @@ type SectionContentBlock =
|
|
|
48
57
|
| { type: 'html'; html: string }
|
|
49
58
|
| { type: 'image-stack'; images: ManagedImage[] }
|
|
50
59
|
| { type: 'image-carousel'; images: ManagedImage[] }
|
|
51
|
-
| { type: 'card-list'; layout: CardListLayout; flow: CardListFlow; size: CardListSize; width?: CardListWidth; cards: CardListItem[] }
|
|
60
|
+
| { type: 'card-list'; layout: CardListLayout; flow: CardListFlow; size: CardListSize; width?: CardListWidth; cards: CardListItem[] }
|
|
61
|
+
| { type: 'page-list'; pages: PageListItem[] };
|
|
52
62
|
export type ResolvedSection = {
|
|
53
63
|
id: string | null;
|
|
54
64
|
title: string;
|
|
@@ -222,6 +232,7 @@ const resolveContentBlocks = async (
|
|
|
222
232
|
html: string,
|
|
223
233
|
blocks: ParsedNornaBlock[],
|
|
224
234
|
page: SitePage,
|
|
235
|
+
childPages: SitePage[],
|
|
225
236
|
inlineNotes: InlineNote[] = [],
|
|
226
237
|
) => {
|
|
227
238
|
const renderedNotes = await Promise.all(inlineNotes.map(async (note) => ({
|
|
@@ -252,6 +263,20 @@ const resolveContentBlocks = async (
|
|
|
252
263
|
});
|
|
253
264
|
continue;
|
|
254
265
|
}
|
|
266
|
+
if (block.type === 'page-list') {
|
|
267
|
+
if (childPages.length === 0) {
|
|
268
|
+
throw new Error(`${page.contentLabel} line ${block.line}: page-list has no listed direct child pages to display. Navigation categories are not pages. Add a listed direct child page or remove the block.`);
|
|
269
|
+
}
|
|
270
|
+
resolvedBlocks.push({
|
|
271
|
+
type: 'page-list',
|
|
272
|
+
pages: childPages.map((childPage) => ({
|
|
273
|
+
title: childPage.title,
|
|
274
|
+
pathname: childPage.pathname,
|
|
275
|
+
description: childPage.entry.data.page?.description,
|
|
276
|
+
})),
|
|
277
|
+
});
|
|
278
|
+
continue;
|
|
279
|
+
}
|
|
255
280
|
|
|
256
281
|
resolvedBlocks.push({
|
|
257
282
|
type: block.type,
|
|
@@ -268,6 +293,7 @@ const resolveContentBlocks = async (
|
|
|
268
293
|
export const getSectionsContent = async (
|
|
269
294
|
html: string,
|
|
270
295
|
page: SitePage,
|
|
296
|
+
childPages: SitePage[] = [],
|
|
271
297
|
) => {
|
|
272
298
|
const pageDocument = page.markdownDocument;
|
|
273
299
|
const sourceLabel = page.contentLabel;
|
|
@@ -307,6 +333,9 @@ export const getSectionsContent = async (
|
|
|
307
333
|
if (bodySection.noteErrors.length > 0) {
|
|
308
334
|
throw new Error(bodySection.noteErrors[0].message);
|
|
309
335
|
}
|
|
336
|
+
if (bodySection.calloutErrors.length > 0) {
|
|
337
|
+
throw new Error(bodySection.calloutErrors[0].message);
|
|
338
|
+
}
|
|
310
339
|
|
|
311
340
|
const inlineNotes = bodySection.notes.map((note) => {
|
|
312
341
|
const number = nextNoteNumber;
|
|
@@ -326,7 +355,7 @@ export const getSectionsContent = async (
|
|
|
326
355
|
title,
|
|
327
356
|
titleHtml: await renderHeadingTitleHtml(bodySection.heading.source),
|
|
328
357
|
headingLevel,
|
|
329
|
-
contentBlocks: await resolveContentBlocks(content, bodySection.blocks, page, inlineNotes),
|
|
358
|
+
contentBlocks: await resolveContentBlocks(content, bodySection.blocks, page, childPages, inlineNotes),
|
|
330
359
|
});
|
|
331
360
|
}
|
|
332
361
|
|
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
import type { HeadingNavigation, SectionNavigation } from './sectionContent';
|
|
2
2
|
import type { SiteNode, SitePage } from './sitePages';
|
|
3
|
+
import {
|
|
4
|
+
flattenSiteNavigationTree as flattenSiteNavigationTreeShared,
|
|
5
|
+
getDirectChildPages as getDirectChildPagesShared,
|
|
6
|
+
getFirstPageInNavigationNode as getFirstPageInNavigationNodeShared,
|
|
7
|
+
getListedSiteNavigationTree as getListedSiteNavigationTreeShared,
|
|
8
|
+
getSequentialPageNavigation as getSequentialPageNavigationShared,
|
|
9
|
+
} from '../../scripts/lib/site-navigation-tree.mjs';
|
|
3
10
|
|
|
4
11
|
export type SiteNavigationEntry = {
|
|
5
12
|
node: SiteNode;
|
|
@@ -11,57 +18,29 @@ export type SiteNavigationNode = SiteNavigationEntry & {
|
|
|
11
18
|
children: SiteNavigationNode[];
|
|
12
19
|
};
|
|
13
20
|
|
|
14
|
-
export const
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
throw new Error(`Site node "${entry.node.pagePath}" has no parent node "${entry.node.parentPagePath}".`);
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
parent.children.push(node);
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
return roots;
|
|
40
|
-
};
|
|
41
|
-
|
|
42
|
-
export const getListedSiteNavigationTree = (entries: SiteNavigationEntry[]) => {
|
|
43
|
-
const filterListed = (nodes: SiteNavigationNode[]): SiteNavigationNode[] => nodes.flatMap((node) => {
|
|
44
|
-
if (!node.node.isHome && !node.node.navigation.listed) return [];
|
|
45
|
-
const children = filterListed(node.children);
|
|
46
|
-
if (node.node.kind === 'category' && children.length === 0) return [];
|
|
47
|
-
return [{ ...node, children }];
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
return filterListed(getSiteNavigationTree(entries));
|
|
51
|
-
};
|
|
52
|
-
|
|
53
|
-
export const getFirstPageInNavigationNode = (node: SiteNavigationNode): SitePage | null => {
|
|
54
|
-
if (node.node.kind === 'page') return node.node;
|
|
55
|
-
|
|
56
|
-
for (const child of node.children) {
|
|
57
|
-
const page = getFirstPageInNavigationNode(child);
|
|
58
|
-
if (page) return page;
|
|
21
|
+
export const getListedSiteNavigationTree = (entries: SiteNavigationEntry[]) => (
|
|
22
|
+
getListedSiteNavigationTreeShared(entries) as SiteNavigationNode[]
|
|
23
|
+
);
|
|
24
|
+
|
|
25
|
+
export const getFirstPageInNavigationNode = (node: SiteNavigationNode): SitePage | null => (
|
|
26
|
+
getFirstPageInNavigationNodeShared(node) as SitePage | null
|
|
27
|
+
);
|
|
28
|
+
|
|
29
|
+
export const flattenSiteNavigationTree = (nodes: SiteNavigationNode[]): SiteNavigationNode[] => (
|
|
30
|
+
flattenSiteNavigationTreeShared(nodes) as SiteNavigationNode[]
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
export const getSequentialPageNavigation = (
|
|
34
|
+
nodes: SiteNavigationNode[],
|
|
35
|
+
currentPagePath: string,
|
|
36
|
+
): { previous: SitePage | null; next: SitePage | null } => (
|
|
37
|
+
getSequentialPageNavigationShared(nodes, currentPagePath) as {
|
|
38
|
+
previous: SitePage | null;
|
|
39
|
+
next: SitePage | null;
|
|
59
40
|
}
|
|
41
|
+
);
|
|
60
42
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
node,
|
|
66
|
-
...flattenSiteNavigationTree(node.children),
|
|
67
|
-
]);
|
|
43
|
+
export const getDirectChildPages = (
|
|
44
|
+
nodes: SiteNavigationNode[],
|
|
45
|
+
currentPagePath: string,
|
|
46
|
+
): SitePage[] => getDirectChildPagesShared(nodes, currentPagePath) as SitePage[];
|