@janga/norna 0.7.23 → 0.7.25
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +14 -18
- package/astro.config.mjs +14 -0
- package/bin/norna-cli.mjs +6 -0
- package/package.json +25 -2
- package/schemas/category.schema.json +2 -2
- package/schemas/config.schema.json +80 -15
- package/schemas/content-frontmatter.schema.json +7 -7
- package/schemas/page-theme.schema.json +26 -26
- package/schemas/sitewide-content.schema.json +18 -18
- package/schemas/theme.schema.json +246 -246
- package/scripts/build-site.mjs +1 -0
- package/scripts/check-config.mjs +16 -1
- package/scripts/dev-local.mjs +106 -11
- package/scripts/generate-search-index.mjs +57 -0
- package/scripts/init-site.mjs +3 -0
- package/scripts/lib/code-fence-metadata.mjs +228 -0
- package/scripts/lib/edit-source-link.mjs +102 -0
- package/scripts/lib/editor-language-service.mjs +32 -12
- package/scripts/lib/image-presentation.mjs +4 -0
- package/scripts/lib/navigation-model.mjs +34 -13
- package/scripts/lib/navigation-review.mjs +396 -0
- package/scripts/lib/norna-markdown-blocks.mjs +164 -26
- package/scripts/lib/norna-markdown-render-plugin.mjs +64 -1
- package/scripts/lib/page-aliases.mjs +21 -1
- package/scripts/lib/page-markdown.mjs +32 -1
- package/scripts/lib/page-move-plan.mjs +659 -0
- package/scripts/lib/presentation-palette-metadata.mjs +1 -1
- package/scripts/lib/presentation.mjs +1 -10
- package/scripts/lib/project-config.mjs +141 -5
- package/scripts/lib/public-asset-conventions.mjs +36 -2
- package/scripts/lib/schema-definitions.mjs +30 -5
- package/scripts/lib/schema-editor-metadata.mjs +36 -4
- package/scripts/lib/schema-value-definitions.mjs +4 -1
- package/scripts/lib/semantic-callouts.mjs +128 -0
- package/scripts/lib/site-content.mjs +2 -1
- package/scripts/lib/site-link-graph.mjs +33 -2
- package/scripts/lib/site-navigation-tree.mjs +85 -0
- package/scripts/lib/site-paths.mjs +14 -2
- package/scripts/lib/social-image-assets.mjs +30 -0
- package/scripts/lib/table-render-plugin.mjs +33 -0
- package/scripts/lib/theme-presets.mjs +2 -2
- package/scripts/lib/theme-profiles.mjs +0 -4
- package/scripts/move-site-page.mjs +256 -0
- package/scripts/review-navigation.mjs +32 -0
- package/scripts/sync-content-sections.mjs +67 -4
- package/scripts/sync-site-public.mjs +24 -5
- package/src/components/CardList.astro +1 -0
- package/src/components/CodeBlockCopyScript.astro +11 -5
- package/src/components/EditSourceLink.astro +17 -0
- package/src/components/ImageCarousel.astro +9 -7
- package/src/components/ImageStack.astro +34 -9
- package/src/components/ImageStackEnhancement.astro +331 -0
- package/src/components/NavigationPageTree.astro +66 -55
- package/src/components/NavigationTreeControls.astro +77 -0
- package/src/components/PageAliasRedirect.astro +1 -1
- package/src/components/PageContentsNavigation.astro +2 -3
- package/src/components/PageList.astro +33 -0
- package/src/components/PageSequenceNavigation.astro +37 -0
- package/src/components/SearchPage.astro +151 -0
- package/src/components/SectionNavigationScript.astro +45 -7
- package/src/components/SiteNavigation.astro +102 -51
- package/src/components/SitePage.astro +107 -39
- package/src/components/SiteSection.astro +11 -3
- package/src/components/SiteTreeNavigation.astro +11 -1
- package/src/components/TableOverflowScript.astro +251 -0
- package/src/components/TreeNavigationScript.astro +271 -34
- package/src/layouts/BaseLayout.astro +83 -2
- package/src/lib/generatedImages.ts +18 -0
- package/src/lib/sectionContent.ts +36 -30
- package/src/lib/siteNavigation.ts +31 -52
- package/src/lib/sitePages.ts +6 -3
- package/src/lib/sitePublicAssets.ts +1 -0
- package/src/pages/404.astro +110 -0
- package/src/pages/[...slug].astro +15 -5
- package/src/styles/content.css +538 -39
- package/src/styles/media.css +187 -8
- package/src/styles/navigation.css +199 -3
- package/src/styles/page-layout.css +481 -100
- package/src/styles/responsive.css +144 -28
- package/starters/basic/README.md +6 -19
- package/starters/basic/package.json +1 -0
- package/starters/basic/site/pages/000-home/content.md +13 -50
|
@@ -1,39 +1,109 @@
|
|
|
1
1
|
<script>
|
|
2
2
|
type NavigationState = {
|
|
3
3
|
openPaths: string[];
|
|
4
|
-
|
|
4
|
+
sectionOpenByPath: Record<string, boolean>;
|
|
5
|
+
scrollTop: number;
|
|
6
|
+
};
|
|
7
|
+
type SharedNavigationState = Pick<NavigationState, 'openPaths' | 'sectionOpenByPath'>;
|
|
8
|
+
type NavigationSnapshot = {
|
|
9
|
+
branchOpen: boolean[];
|
|
10
|
+
sectionOpen: boolean[];
|
|
5
11
|
scrollTop: number;
|
|
6
12
|
};
|
|
7
13
|
|
|
8
14
|
const setupNavigationState = (container: HTMLElement, scope: 'desktop' | 'mobile') => {
|
|
9
15
|
const root = container.dataset.navigationRoot ?? '/';
|
|
16
|
+
const sharedRoot = container.dataset.navigationStateRoot ?? root;
|
|
10
17
|
const stateKey = `norna:tree-navigation:${scope}:${root}`;
|
|
18
|
+
const sharedStateKey = `norna:tree-navigation:shared:${sharedRoot}`;
|
|
11
19
|
const branches = Array.from(container.querySelectorAll<HTMLDetailsElement>(
|
|
12
20
|
'.navigation-page-disclosure[data-page-path]',
|
|
13
21
|
));
|
|
14
|
-
|
|
22
|
+
const sectionBranches = Array.from(container.querySelectorAll<HTMLDetailsElement>(
|
|
23
|
+
'.navigation-page-sections-disclosure[data-page-path]',
|
|
24
|
+
));
|
|
25
|
+
const navigationNodes = Array.from(container.querySelectorAll<HTMLElement>(
|
|
26
|
+
'.navigation-page-node[data-navigation-title]',
|
|
27
|
+
));
|
|
28
|
+
const getScrollContainer = () => {
|
|
29
|
+
let candidate: HTMLElement | null = container;
|
|
15
30
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
sectionPaths: Array.isArray(parsedState.sectionPaths) ? parsedState.sectionPaths : [],
|
|
21
|
-
scrollTop: Number.isFinite(Number(parsedState.scrollTop)) ? Number(parsedState.scrollTop) : 0,
|
|
22
|
-
};
|
|
31
|
+
while (candidate) {
|
|
32
|
+
if (/auto|scroll/.test(getComputedStyle(candidate).overflowY)) return candidate;
|
|
33
|
+
candidate = candidate.parentElement;
|
|
34
|
+
}
|
|
23
35
|
|
|
36
|
+
return container;
|
|
37
|
+
};
|
|
38
|
+
const scrollContainer = getScrollContainer();
|
|
39
|
+
let filterSnapshot: NavigationSnapshot | null = null;
|
|
40
|
+
let lastSharedState = '';
|
|
41
|
+
const parseState = (value: string | null): NavigationState => {
|
|
42
|
+
try {
|
|
43
|
+
const parsedState = JSON.parse(value ?? '{}');
|
|
44
|
+
return {
|
|
45
|
+
openPaths: Array.isArray(parsedState.openPaths) ? parsedState.openPaths : [],
|
|
46
|
+
sectionOpenByPath: parsedState.sectionOpenByPath
|
|
47
|
+
&& typeof parsedState.sectionOpenByPath === 'object'
|
|
48
|
+
? Object.fromEntries(Object.entries(parsedState.sectionOpenByPath)
|
|
49
|
+
.filter((entry): entry is [string, boolean] => typeof entry[1] === 'boolean'))
|
|
50
|
+
: {},
|
|
51
|
+
scrollTop: Number.isFinite(Number(parsedState.scrollTop)) ? Number(parsedState.scrollTop) : 0,
|
|
52
|
+
};
|
|
53
|
+
} catch {
|
|
54
|
+
return { openPaths: [], sectionOpenByPath: {}, scrollTop: 0 };
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
const getDisclosureState = (): SharedNavigationState => ({
|
|
58
|
+
openPaths: branches.filter((branch) => branch.open)
|
|
59
|
+
.map((branch) => branch.dataset.pagePath ?? ''),
|
|
60
|
+
sectionOpenByPath: Object.fromEntries(sectionBranches.map((branch) => [
|
|
61
|
+
branch.dataset.pagePath ?? '',
|
|
62
|
+
branch.open,
|
|
63
|
+
])),
|
|
64
|
+
});
|
|
65
|
+
const applyDisclosureState = (state: SharedNavigationState) => {
|
|
24
66
|
branches.forEach((branch) => {
|
|
25
67
|
const pagePath = branch.dataset.pagePath ?? '';
|
|
26
68
|
branch.open = branch.dataset.currentBranch === 'true'
|
|
27
|
-
||
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
69
|
+
|| state.openPaths.includes(pagePath);
|
|
70
|
+
});
|
|
71
|
+
sectionBranches.forEach((branch) => {
|
|
72
|
+
const pagePath = branch.dataset.pagePath ?? '';
|
|
73
|
+
const storedOpen = state.sectionOpenByPath[pagePath];
|
|
74
|
+
branch.open = typeof storedOpen === 'boolean'
|
|
75
|
+
? storedOpen
|
|
76
|
+
: branch.dataset.currentPage === 'true';
|
|
32
77
|
});
|
|
78
|
+
};
|
|
79
|
+
const mergeSharedState = (state: SharedNavigationState): SharedNavigationState => {
|
|
80
|
+
const existingState = parseState(sessionStorage.getItem(sharedStateKey));
|
|
81
|
+
const knownBranchPaths = new Set(branches.map((branch) => branch.dataset.pagePath ?? ''));
|
|
82
|
+
return {
|
|
83
|
+
openPaths: [
|
|
84
|
+
...existingState.openPaths.filter((pagePath) => !knownBranchPaths.has(pagePath)),
|
|
85
|
+
...state.openPaths,
|
|
86
|
+
],
|
|
87
|
+
sectionOpenByPath: {
|
|
88
|
+
...existingState.sectionOpenByPath,
|
|
89
|
+
...state.sectionOpenByPath,
|
|
90
|
+
},
|
|
91
|
+
};
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
try {
|
|
95
|
+
const storedState = parseState(sessionStorage.getItem(stateKey));
|
|
96
|
+
const storedSharedState = sessionStorage.getItem(sharedStateKey);
|
|
97
|
+
applyDisclosureState(storedSharedState === null
|
|
98
|
+
? storedState
|
|
99
|
+
: parseState(storedSharedState));
|
|
100
|
+
const initialSharedState = mergeSharedState(getDisclosureState());
|
|
101
|
+
lastSharedState = JSON.stringify(initialSharedState);
|
|
102
|
+
sessionStorage.setItem(sharedStateKey, lastSharedState);
|
|
33
103
|
|
|
34
104
|
if (storedState.scrollTop > 0) {
|
|
35
105
|
requestAnimationFrame(() => {
|
|
36
|
-
|
|
106
|
+
scrollContainer.scrollTop = storedState.scrollTop;
|
|
37
107
|
});
|
|
38
108
|
}
|
|
39
109
|
} catch {
|
|
@@ -41,37 +111,204 @@
|
|
|
41
111
|
}
|
|
42
112
|
|
|
43
113
|
const saveState = () => {
|
|
114
|
+
if (filterSnapshot) return;
|
|
115
|
+
|
|
44
116
|
try {
|
|
45
|
-
const
|
|
117
|
+
const disclosureState = getDisclosureState();
|
|
46
118
|
const state: NavigationState = {
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
.filter((branch) => (
|
|
50
|
-
branch.dataset.sectionsOpen === 'true'
|
|
51
|
-
|| branch.dataset.currentPage === 'true'
|
|
52
|
-
))
|
|
53
|
-
.map((branch) => branch.dataset.pagePath ?? ''),
|
|
54
|
-
scrollTop: container.scrollTop,
|
|
119
|
+
...disclosureState,
|
|
120
|
+
scrollTop: scrollContainer.scrollTop,
|
|
55
121
|
};
|
|
56
122
|
sessionStorage.setItem(stateKey, JSON.stringify(state));
|
|
123
|
+
const sharedState = mergeSharedState(disclosureState);
|
|
124
|
+
const serializedSharedState = JSON.stringify(sharedState);
|
|
125
|
+
sessionStorage.setItem(sharedStateKey, serializedSharedState);
|
|
126
|
+
if (serializedSharedState !== lastSharedState) {
|
|
127
|
+
lastSharedState = serializedSharedState;
|
|
128
|
+
window.dispatchEvent(new CustomEvent('norna:tree-navigation-state', {
|
|
129
|
+
detail: { root: sharedRoot, state: sharedState },
|
|
130
|
+
}));
|
|
131
|
+
}
|
|
57
132
|
} catch {
|
|
58
133
|
// Navigation state persistence is an optional enhancement.
|
|
59
134
|
}
|
|
60
135
|
};
|
|
136
|
+
window.addEventListener('norna:tree-navigation-state', (event) => {
|
|
137
|
+
const detail = (event as CustomEvent<{
|
|
138
|
+
root?: string;
|
|
139
|
+
state?: SharedNavigationState;
|
|
140
|
+
}>).detail;
|
|
141
|
+
if (detail?.root !== sharedRoot || !detail.state) return;
|
|
142
|
+
const serializedSharedState = JSON.stringify(detail.state);
|
|
143
|
+
if (serializedSharedState === lastSharedState) return;
|
|
144
|
+
lastSharedState = serializedSharedState;
|
|
145
|
+
applyDisclosureState(detail.state);
|
|
146
|
+
});
|
|
147
|
+
const controls = container.querySelector<HTMLElement>('[data-tree-controls]');
|
|
148
|
+
const filterInput = controls?.querySelector<HTMLInputElement>('[data-tree-filter]') ?? null;
|
|
149
|
+
const expandButton = controls?.querySelector<HTMLButtonElement>('[data-tree-expand-all]') ?? null;
|
|
150
|
+
const collapseButton = controls?.querySelector<HTMLButtonElement>('[data-tree-collapse-all]') ?? null;
|
|
151
|
+
const locateButton = controls?.querySelector<HTMLButtonElement>('[data-tree-locate-current]') ?? null;
|
|
152
|
+
const emptyMessage = controls?.querySelector<HTMLElement>('[data-tree-filter-empty]') ?? null;
|
|
153
|
+
const status = controls?.querySelector<HTMLElement>('[data-tree-status]') ?? null;
|
|
154
|
+
const currentPageNode = container.querySelector<HTMLElement>(
|
|
155
|
+
'.navigation-page-node[data-navigation-current-page="true"]',
|
|
156
|
+
);
|
|
157
|
+
const labels = {
|
|
158
|
+
collapsed: controls?.dataset.treeLabelCollapsed ?? '',
|
|
159
|
+
expanded: controls?.dataset.treeLabelExpanded ?? '',
|
|
160
|
+
filterMatches: controls?.dataset.treeLabelFilterMatches ?? '{count}',
|
|
161
|
+
located: controls?.dataset.treeLabelLocated ?? '',
|
|
162
|
+
};
|
|
163
|
+
const normalizeTitle = (value: string) => value
|
|
164
|
+
.normalize('NFKD')
|
|
165
|
+
.replace(/\p{Mark}+/gu, '')
|
|
166
|
+
.toLocaleLowerCase(document.documentElement.lang || undefined)
|
|
167
|
+
.trim();
|
|
168
|
+
const setStatus = (message: string) => {
|
|
169
|
+
if (status) status.textContent = message;
|
|
170
|
+
};
|
|
171
|
+
const getSnapshot = (): NavigationSnapshot => ({
|
|
172
|
+
branchOpen: branches.map((branch) => branch.open),
|
|
173
|
+
sectionOpen: sectionBranches.map((branch) => branch.open),
|
|
174
|
+
scrollTop: scrollContainer.scrollTop,
|
|
175
|
+
});
|
|
176
|
+
const revealNodeAndAncestors = (node: HTMLElement) => {
|
|
177
|
+
let currentNode: HTMLElement | null = node;
|
|
61
178
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
179
|
+
while (currentNode && container.contains(currentNode)) {
|
|
180
|
+
currentNode.hidden = false;
|
|
181
|
+
const parentNode = currentNode.parentElement?.closest<HTMLElement>(
|
|
182
|
+
'.navigation-page-node[data-navigation-title]',
|
|
183
|
+
) ?? null;
|
|
184
|
+
const parentBranch = parentNode?.querySelector<HTMLDetailsElement>(
|
|
185
|
+
':scope > .navigation-page-disclosure[data-page-path]',
|
|
186
|
+
);
|
|
187
|
+
if (parentBranch) parentBranch.open = true;
|
|
188
|
+
currentNode = parentNode;
|
|
189
|
+
}
|
|
190
|
+
};
|
|
191
|
+
const restoreFilterSnapshot = () => {
|
|
192
|
+
if (!filterSnapshot) return;
|
|
193
|
+
|
|
194
|
+
branches.forEach((branch, index) => { branch.open = filterSnapshot?.branchOpen[index] ?? false; });
|
|
195
|
+
sectionBranches.forEach((branch, index) => { branch.open = filterSnapshot?.sectionOpen[index] ?? false; });
|
|
196
|
+
scrollContainer.scrollTop = filterSnapshot.scrollTop;
|
|
197
|
+
filterSnapshot = null;
|
|
198
|
+
requestAnimationFrame(saveState);
|
|
199
|
+
};
|
|
200
|
+
const clearFilter = () => {
|
|
201
|
+
navigationNodes.forEach((node) => { node.hidden = false; });
|
|
202
|
+
delete container.dataset.treeFilterActive;
|
|
203
|
+
if (emptyMessage) emptyMessage.hidden = true;
|
|
204
|
+
if (expandButton) expandButton.disabled = false;
|
|
205
|
+
if (collapseButton) collapseButton.disabled = false;
|
|
206
|
+
restoreFilterSnapshot();
|
|
207
|
+
setStatus('');
|
|
208
|
+
};
|
|
209
|
+
const applyFilter = () => {
|
|
210
|
+
const query = normalizeTitle(filterInput?.value ?? '');
|
|
211
|
+
|
|
212
|
+
if (!query) {
|
|
213
|
+
clearFilter();
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
if (!filterSnapshot) filterSnapshot = getSnapshot();
|
|
218
|
+
container.dataset.treeFilterActive = 'true';
|
|
219
|
+
if (expandButton) expandButton.disabled = true;
|
|
220
|
+
if (collapseButton) collapseButton.disabled = true;
|
|
221
|
+
navigationNodes.forEach((node) => { node.hidden = true; });
|
|
222
|
+
|
|
223
|
+
const matches = navigationNodes.filter((node) => normalizeTitle(
|
|
224
|
+
node.dataset.navigationTitle ?? '',
|
|
225
|
+
).includes(query));
|
|
226
|
+
matches.forEach((node) => {
|
|
227
|
+
revealNodeAndAncestors(node);
|
|
228
|
+
|
|
229
|
+
if (node.dataset.navigationKind === 'category') {
|
|
230
|
+
node.querySelectorAll<HTMLElement>('.navigation-page-node[data-navigation-title]')
|
|
231
|
+
.forEach((descendant) => { descendant.hidden = false; });
|
|
232
|
+
const categoryBranch = node.querySelector<HTMLDetailsElement>(
|
|
233
|
+
':scope > .navigation-category-disclosure',
|
|
234
|
+
);
|
|
235
|
+
if (categoryBranch) categoryBranch.open = true;
|
|
68
236
|
}
|
|
69
|
-
|
|
70
|
-
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
if (matches.length === 0 && currentPageNode) revealNodeAndAncestors(currentPageNode);
|
|
240
|
+
if (emptyMessage) emptyMessage.hidden = matches.length > 0;
|
|
241
|
+
setStatus(labels.filterMatches.replace('{count}', String(matches.length)));
|
|
242
|
+
};
|
|
243
|
+
const setAllOpen = (open: boolean) => {
|
|
244
|
+
branches.forEach((branch) => { branch.open = open; });
|
|
245
|
+
sectionBranches.forEach((branch) => { branch.open = open; });
|
|
246
|
+
saveState();
|
|
247
|
+
setStatus(open ? labels.expanded : labels.collapsed);
|
|
248
|
+
};
|
|
249
|
+
const locateCurrentPage = () => {
|
|
250
|
+
if (!currentPageNode) return;
|
|
251
|
+
|
|
252
|
+
if (filterInput?.value) {
|
|
253
|
+
filterInput.value = '';
|
|
254
|
+
clearFilter();
|
|
255
|
+
}
|
|
256
|
+
revealNodeAndAncestors(currentPageNode);
|
|
257
|
+
currentPageNode.querySelectorAll<HTMLDetailsElement>(
|
|
258
|
+
':scope > .navigation-page-disclosure, :scope > .navigation-page-sections-disclosure',
|
|
259
|
+
).forEach((branch) => { branch.open = true; });
|
|
260
|
+
saveState();
|
|
261
|
+
|
|
262
|
+
const currentPageLink = currentPageNode.querySelector<HTMLAnchorElement>(
|
|
263
|
+
':scope > .navigation-page-link, :scope > .navigation-page-open-link',
|
|
264
|
+
);
|
|
265
|
+
if (!currentPageLink) return;
|
|
266
|
+
|
|
267
|
+
currentPageLink.focus({ preventScroll: true });
|
|
268
|
+
const scrollBox = scrollContainer.getBoundingClientRect();
|
|
269
|
+
const linkBox = currentPageLink.getBoundingClientRect();
|
|
270
|
+
scrollContainer.scrollTop += linkBox.top - scrollBox.top
|
|
271
|
+
- Math.max(0, (scrollContainer.clientHeight - linkBox.height) / 2);
|
|
272
|
+
setStatus(labels.located);
|
|
273
|
+
};
|
|
274
|
+
const openLinkedPage = (link: HTMLAnchorElement) => {
|
|
275
|
+
if (filterInput?.value) {
|
|
276
|
+
filterInput.value = '';
|
|
277
|
+
clearFilter();
|
|
278
|
+
}
|
|
279
|
+
const pageNode = link.closest<HTMLElement>('.navigation-page-node');
|
|
280
|
+
const pageDisclosure = pageNode?.querySelector<HTMLDetailsElement>(
|
|
281
|
+
':scope > .navigation-page-disclosure:not(.navigation-category-disclosure)',
|
|
282
|
+
);
|
|
283
|
+
const sectionDisclosure = pageNode?.querySelector<HTMLDetailsElement>(
|
|
284
|
+
':scope > .navigation-page-sections-disclosure[data-page-path]',
|
|
285
|
+
);
|
|
286
|
+
|
|
287
|
+
if (pageDisclosure) pageDisclosure.open = true;
|
|
288
|
+
if (sectionDisclosure) sectionDisclosure.open = true;
|
|
289
|
+
saveState();
|
|
290
|
+
};
|
|
291
|
+
|
|
292
|
+
requestAnimationFrame(() => {
|
|
293
|
+
branches.forEach((branch) => branch.addEventListener('toggle', saveState));
|
|
294
|
+
sectionBranches.forEach((branch) => branch.addEventListener('toggle', saveState));
|
|
71
295
|
});
|
|
72
296
|
|
|
73
|
-
|
|
74
|
-
|
|
297
|
+
if (controls) controls.hidden = false;
|
|
298
|
+
if (locateButton && !currentPageNode) locateButton.disabled = true;
|
|
299
|
+
filterInput?.addEventListener('input', applyFilter);
|
|
300
|
+
filterInput?.addEventListener('keydown', (event) => {
|
|
301
|
+
if (event.key !== 'Escape' || !filterInput.value) return;
|
|
302
|
+
event.preventDefault();
|
|
303
|
+
filterInput.value = '';
|
|
304
|
+
clearFilter();
|
|
305
|
+
});
|
|
306
|
+
expandButton?.addEventListener('click', () => setAllOpen(true));
|
|
307
|
+
collapseButton?.addEventListener('click', () => setAllOpen(false));
|
|
308
|
+
locateButton?.addEventListener('click', locateCurrentPage);
|
|
309
|
+
scrollContainer.addEventListener('scroll', saveState, { passive: true });
|
|
310
|
+
container.querySelectorAll<HTMLAnchorElement>('.navigation-page-link, .navigation-page-open-link')
|
|
311
|
+
.forEach((link) => link.addEventListener('click', () => openLinkedPage(link)));
|
|
75
312
|
window.addEventListener('pagehide', saveState);
|
|
76
313
|
};
|
|
77
314
|
|
|
@@ -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';
|
|
@@ -77,6 +77,8 @@ interface Props {
|
|
|
77
77
|
buildInfo?: boolean;
|
|
78
78
|
};
|
|
79
79
|
contentsNavigation?: boolean;
|
|
80
|
+
indexable?: boolean;
|
|
81
|
+
searchable?: boolean;
|
|
80
82
|
}
|
|
81
83
|
|
|
82
84
|
const {
|
|
@@ -88,11 +90,17 @@ const {
|
|
|
88
90
|
pathname = '/',
|
|
89
91
|
footer,
|
|
90
92
|
contentsNavigation = false,
|
|
93
|
+
indexable = true,
|
|
94
|
+
searchable = false,
|
|
91
95
|
} = Astro.props;
|
|
92
96
|
|
|
93
97
|
const documentTitle = title;
|
|
94
98
|
const canonicalUrl = new URL(withBasePath(projectConfig.site.basePath, pathname), projectConfig.site.url).href;
|
|
95
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;
|
|
96
104
|
const scrollBehavior = projectConfig.navigation.scrollBehavior === 'smooth' ? 'smooth' : 'auto';
|
|
97
105
|
const formatPercent = (value: number) => `${value}%`;
|
|
98
106
|
const formatViewportHeightPercent = (value: number) => `${value}svh`;
|
|
@@ -132,6 +140,65 @@ const documentStyle = [
|
|
|
132
140
|
]
|
|
133
141
|
: []),
|
|
134
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 marginNoteOuterReserve = '0.5rem';
|
|
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}"] .site-page-layout:not(.site-page-layout-tree) .section-note {`,
|
|
161
|
+
marginNoteDeclarations,
|
|
162
|
+
'}',
|
|
163
|
+
'}',
|
|
164
|
+
].join('\n'),
|
|
165
|
+
[
|
|
166
|
+
`@container (min-width: calc(${textWidth} + ${visualTheme.layout.noteWidth} + ${visualTheme.layout.noteGap} + ${marginNoteOuterReserve} - 11rem - ${visualTheme.layout.localNavigationGap})) {`,
|
|
167
|
+
`:root[data-reader-preferences-ready="true"][data-reading-width="${readingWidth}"][data-focus-reading="on"] .site-page-layout-tree .section-note {`,
|
|
168
|
+
marginNoteDeclarations,
|
|
169
|
+
'}',
|
|
170
|
+
'}',
|
|
171
|
+
].join('\n'),
|
|
172
|
+
]),
|
|
173
|
+
'}',
|
|
174
|
+
'@media (min-width: 1101px) and (max-width: 1280px) {',
|
|
175
|
+
...marginNoteReadingWidths.map(([readingWidth, textWidth]) => [
|
|
176
|
+
`@container (min-width: calc(${textWidth} + ${visualTheme.layout.noteWidth} + ${visualTheme.layout.noteGap} + ${marginNoteOuterReserve})) {`,
|
|
177
|
+
`:root[data-reading-width="${readingWidth}"] .site-page-layout-tree:not(.site-page-layout-contents) .section-note {`,
|
|
178
|
+
marginNoteDeclarations,
|
|
179
|
+
'}',
|
|
180
|
+
'}',
|
|
181
|
+
].join('\n')),
|
|
182
|
+
'}',
|
|
183
|
+
'@media (min-width: 1281px) {',
|
|
184
|
+
...marginNoteReadingWidths.flatMap(([readingWidth, textWidth]) => [
|
|
185
|
+
[
|
|
186
|
+
`@container (min-width: calc(${textWidth} + ${visualTheme.layout.noteWidth} + ${visualTheme.layout.noteGap})) {`,
|
|
187
|
+
`:root[data-reading-width="${readingWidth}"] .site-page-layout-tree.site-page-layout-contents .section-note {`,
|
|
188
|
+
marginNoteDeclarations,
|
|
189
|
+
'}',
|
|
190
|
+
'}',
|
|
191
|
+
].join('\n'),
|
|
192
|
+
[
|
|
193
|
+
`@container (min-width: calc(${textWidth} + ${visualTheme.layout.noteWidth} + ${visualTheme.layout.noteGap} + ${marginNoteOuterReserve} - 11rem - ${visualTheme.layout.localNavigationGap})) {`,
|
|
194
|
+
`:root[data-reading-width="${readingWidth}"] .site-page-layout-tree:not(.site-page-layout-contents) .section-note {`,
|
|
195
|
+
marginNoteDeclarations,
|
|
196
|
+
'}',
|
|
197
|
+
'}',
|
|
198
|
+
].join('\n'),
|
|
199
|
+
]),
|
|
200
|
+
'}',
|
|
201
|
+
].join('\n');
|
|
135
202
|
const { buildInfo, copyrightMessage } = footer ?? {};
|
|
136
203
|
const showBuildInfo = buildInfo === true;
|
|
137
204
|
const buildTime = showBuildInfo
|
|
@@ -164,16 +231,29 @@ const readerPreferencesScript = readerPreferencesEnabled
|
|
|
164
231
|
data-reading-width={presentation.readerPreferences.defaults.readingWidth}
|
|
165
232
|
data-focus-reading={presentation.readerPreferences.defaults.focusReading}
|
|
166
233
|
data-section-tracking={contentsNavigation ? 'enabled' : undefined}
|
|
234
|
+
data-pagefind-ignore={searchable ? undefined : 'all'}
|
|
167
235
|
style={documentStyle}
|
|
168
236
|
>
|
|
169
237
|
<head>
|
|
170
238
|
<meta charset="utf-8" />
|
|
171
239
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
172
240
|
<meta name="format-detection" content="telephone=no" />
|
|
241
|
+
{!indexable && <meta name="robots" content="noindex" />}
|
|
173
242
|
{description && <meta name="description" content={description} />}
|
|
243
|
+
{indexable && <meta property="og:type" content="website" />}
|
|
244
|
+
{indexable && <meta property="og:title" content={documentTitle} />}
|
|
245
|
+
{indexable && <meta property="og:url" content={canonicalUrl} />}
|
|
246
|
+
{indexable && description && <meta property="og:description" content={description} />}
|
|
247
|
+
{indexable && socialImageUrl && <meta property="og:image" content={socialImageUrl} />}
|
|
248
|
+
{indexable && socialImageAsset && <meta property="og:image:type" content={socialImageAsset.type} />}
|
|
249
|
+
{indexable && <meta name="twitter:card" content={socialImageUrl ? 'summary_large_image' : 'summary'} />}
|
|
250
|
+
{indexable && <meta name="twitter:title" content={documentTitle} />}
|
|
251
|
+
{indexable && description && <meta name="twitter:description" content={description} />}
|
|
252
|
+
{indexable && socialImageUrl && <meta name="twitter:image" content={socialImageUrl} />}
|
|
174
253
|
<meta name="theme-color" content={presentation.palette.page.backgroundColor} />
|
|
175
254
|
{readerPreferencesScript && <script is:inline set:html={readerPreferencesScript} />}
|
|
176
|
-
<
|
|
255
|
+
<style is:inline set:html={marginNoteLayoutCss} />
|
|
256
|
+
{indexable && <link rel="canonical" href={canonicalUrl} />}
|
|
177
257
|
<meta name="generator" content={Astro.generator} />
|
|
178
258
|
{iconLinks.map((iconLink) => (
|
|
179
259
|
<link
|
|
@@ -183,6 +263,7 @@ const readerPreferencesScript = readerPreferencesEnabled
|
|
|
183
263
|
href={iconLink.href}
|
|
184
264
|
/>
|
|
185
265
|
))}
|
|
266
|
+
<slot name="head" />
|
|
186
267
|
<title>{documentTitle}</title>
|
|
187
268
|
</head>
|
|
188
269
|
<body>
|
|
@@ -45,6 +45,24 @@ export const getGeneratedImage = (src: string) => readGeneratedImages()[src];
|
|
|
45
45
|
|
|
46
46
|
const displaySrc = (src: string) => withBasePath(projectConfig.site.basePath, src);
|
|
47
47
|
|
|
48
|
+
export const getImageInspectionAttributes = (src: string) => {
|
|
49
|
+
const image = getGeneratedImage(src);
|
|
50
|
+
const largestVariant = image?.variants
|
|
51
|
+
? [...image.variants].sort((left, right) => left.width - right.width).at(-1)
|
|
52
|
+
: undefined;
|
|
53
|
+
const publishedSource = image?.kind === 'static'
|
|
54
|
+
? image.src
|
|
55
|
+
: largestVariant?.src;
|
|
56
|
+
|
|
57
|
+
return {
|
|
58
|
+
href: displaySrc(publishedSource ?? src),
|
|
59
|
+
scalable: image?.kind === 'static' || /\.svg(?:[?#]|$)/iu.test(publishedSource ?? src),
|
|
60
|
+
...(Number.isFinite(image?.width) && Number.isFinite(image?.height)
|
|
61
|
+
? { width: image?.width, height: image?.height }
|
|
62
|
+
: {}),
|
|
63
|
+
};
|
|
64
|
+
};
|
|
65
|
+
|
|
48
66
|
const getDisplayVariants = (variants: NonNullable<GeneratedImage['variants']>) => {
|
|
49
67
|
const sortedVariants = [...variants].sort((a, b) => a.width - b.width);
|
|
50
68
|
const displayVariants = sortedVariants.filter((variant) => variant.width <= maxDisplayImageWidth);
|
|
@@ -3,6 +3,7 @@ import projectConfig from '../../scripts/lib/project-config.mjs';
|
|
|
3
3
|
import {
|
|
4
4
|
formatHeadingIdentifierIssue,
|
|
5
5
|
} from '../../scripts/lib/heading-ids.mjs';
|
|
6
|
+
import { splitNornaRenderedBlocks } from '../../scripts/lib/norna-markdown-blocks.mjs';
|
|
6
7
|
import { applyBasePathToHtml } from './basePath';
|
|
7
8
|
import type { SitePage } from './sitePages';
|
|
8
9
|
|
|
@@ -36,7 +37,16 @@ type ParsedCardListBlock = {
|
|
|
36
37
|
width?: CardListWidth;
|
|
37
38
|
cards: CardListItem[];
|
|
38
39
|
};
|
|
39
|
-
type
|
|
40
|
+
type ParsedPageListBlock = {
|
|
41
|
+
type: 'page-list';
|
|
42
|
+
line: number;
|
|
43
|
+
};
|
|
44
|
+
type ParsedNornaBlock = ParsedImageBlock | ParsedCardListBlock | ParsedPageListBlock;
|
|
45
|
+
type PageListItem = {
|
|
46
|
+
title: string;
|
|
47
|
+
pathname: string;
|
|
48
|
+
description?: string;
|
|
49
|
+
};
|
|
40
50
|
type InlineNote = {
|
|
41
51
|
markdown: string;
|
|
42
52
|
number: number;
|
|
@@ -48,7 +58,8 @@ type SectionContentBlock =
|
|
|
48
58
|
| { type: 'html'; html: string }
|
|
49
59
|
| { type: 'image-stack'; images: ManagedImage[] }
|
|
50
60
|
| { type: 'image-carousel'; images: ManagedImage[] }
|
|
51
|
-
| { type: 'card-list'; layout: CardListLayout; flow: CardListFlow; size: CardListSize; width?: CardListWidth; cards: CardListItem[] }
|
|
61
|
+
| { type: 'card-list'; layout: CardListLayout; flow: CardListFlow; size: CardListSize; width?: CardListWidth; cards: CardListItem[] }
|
|
62
|
+
| { type: 'page-list'; pages: PageListItem[] };
|
|
52
63
|
export type ResolvedSection = {
|
|
53
64
|
id: string | null;
|
|
54
65
|
title: string;
|
|
@@ -192,36 +203,11 @@ const splitRenderedRegions = (html: string, regionCount: number) => {
|
|
|
192
203
|
});
|
|
193
204
|
};
|
|
194
205
|
|
|
195
|
-
const splitNornaBlockMarkers = (html: string, blocks: ParsedNornaBlock[]) => {
|
|
196
|
-
const result: Array<{ type: 'html'; html: string } | ParsedNornaBlock> = [];
|
|
197
|
-
const markerRegex = /<norna-block\s+data-index="(\d+)"\s*><\/norna-block>/g;
|
|
198
|
-
const seen = new Set<number>();
|
|
199
|
-
let cursor = 0;
|
|
200
|
-
|
|
201
|
-
for (const match of html.matchAll(markerRegex)) {
|
|
202
|
-
const start = match.index ?? 0;
|
|
203
|
-
if (start > cursor) result.push({ type: 'html', html: html.slice(cursor, start) });
|
|
204
|
-
|
|
205
|
-
const index = Number.parseInt(match[1] ?? '', 10);
|
|
206
|
-
const block = blocks[index];
|
|
207
|
-
if (!block) throw new Error(`Rendered Norna block ${index + 1} has no matching parsed block.`);
|
|
208
|
-
seen.add(index);
|
|
209
|
-
result.push(block);
|
|
210
|
-
cursor = start + match[0].length;
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
if (cursor < html.length) result.push({ type: 'html', html: html.slice(cursor) });
|
|
214
|
-
if (seen.size !== blocks.length) {
|
|
215
|
-
throw new Error(`Rendered Markdown contains ${seen.size} Norna block markers, but ${blocks.length} blocks were parsed.`);
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
return result.filter((block) => block.type !== 'html' || block.html.trim());
|
|
219
|
-
};
|
|
220
|
-
|
|
221
206
|
const resolveContentBlocks = async (
|
|
222
207
|
html: string,
|
|
223
208
|
blocks: ParsedNornaBlock[],
|
|
224
209
|
page: SitePage,
|
|
210
|
+
childPages: SitePage[],
|
|
225
211
|
inlineNotes: InlineNote[] = [],
|
|
226
212
|
) => {
|
|
227
213
|
const renderedNotes = await Promise.all(inlineNotes.map(async (note) => ({
|
|
@@ -229,7 +215,9 @@ const resolveContentBlocks = async (
|
|
|
229
215
|
html: await renderInlineNoteMarkdown(note.markdown),
|
|
230
216
|
})));
|
|
231
217
|
const renderedHtml = applyInlineNoteMarkup(html, renderedNotes);
|
|
232
|
-
const splitBlocks =
|
|
218
|
+
const splitBlocks = splitNornaRenderedBlocks(renderedHtml, blocks) as Array<
|
|
219
|
+
{ type: 'html'; html: string } | ParsedNornaBlock
|
|
220
|
+
>;
|
|
233
221
|
const resolvedBlocks: SectionContentBlock[] = [];
|
|
234
222
|
|
|
235
223
|
for (const block of splitBlocks) {
|
|
@@ -252,6 +240,20 @@ const resolveContentBlocks = async (
|
|
|
252
240
|
});
|
|
253
241
|
continue;
|
|
254
242
|
}
|
|
243
|
+
if (block.type === 'page-list') {
|
|
244
|
+
if (childPages.length === 0) {
|
|
245
|
+
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.`);
|
|
246
|
+
}
|
|
247
|
+
resolvedBlocks.push({
|
|
248
|
+
type: 'page-list',
|
|
249
|
+
pages: childPages.map((childPage) => ({
|
|
250
|
+
title: childPage.title,
|
|
251
|
+
pathname: childPage.pathname,
|
|
252
|
+
description: childPage.entry.data.page?.description,
|
|
253
|
+
})),
|
|
254
|
+
});
|
|
255
|
+
continue;
|
|
256
|
+
}
|
|
255
257
|
|
|
256
258
|
resolvedBlocks.push({
|
|
257
259
|
type: block.type,
|
|
@@ -268,6 +270,7 @@ const resolveContentBlocks = async (
|
|
|
268
270
|
export const getSectionsContent = async (
|
|
269
271
|
html: string,
|
|
270
272
|
page: SitePage,
|
|
273
|
+
childPages: SitePage[] = [],
|
|
271
274
|
) => {
|
|
272
275
|
const pageDocument = page.markdownDocument;
|
|
273
276
|
const sourceLabel = page.contentLabel;
|
|
@@ -307,6 +310,9 @@ export const getSectionsContent = async (
|
|
|
307
310
|
if (bodySection.noteErrors.length > 0) {
|
|
308
311
|
throw new Error(bodySection.noteErrors[0].message);
|
|
309
312
|
}
|
|
313
|
+
if (bodySection.calloutErrors.length > 0) {
|
|
314
|
+
throw new Error(bodySection.calloutErrors[0].message);
|
|
315
|
+
}
|
|
310
316
|
|
|
311
317
|
const inlineNotes = bodySection.notes.map((note) => {
|
|
312
318
|
const number = nextNoteNumber;
|
|
@@ -326,7 +332,7 @@ export const getSectionsContent = async (
|
|
|
326
332
|
title,
|
|
327
333
|
titleHtml: await renderHeadingTitleHtml(bodySection.heading.source),
|
|
328
334
|
headingLevel,
|
|
329
|
-
contentBlocks: await resolveContentBlocks(content, bodySection.blocks, page, inlineNotes),
|
|
335
|
+
contentBlocks: await resolveContentBlocks(content, bodySection.blocks, page, childPages, inlineNotes),
|
|
330
336
|
});
|
|
331
337
|
}
|
|
332
338
|
|