@janga/norna 0.7.23 → 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 +12 -0
- package/bin/norna-cli.mjs +6 -0
- package/package.json +13 -2
- package/schemas/category.schema.json +2 -2
- package/schemas/config.schema.json +45 -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 +12 -1
- 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/edit-source-link.mjs +49 -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 +73 -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 +111 -5
- package/scripts/lib/public-asset-conventions.mjs +36 -2
- package/scripts/lib/schema-definitions.mjs +22 -5
- package/scripts/lib/schema-editor-metadata.mjs +19 -4
- package/scripts/lib/schema-value-definitions.mjs +1 -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/social-image-assets.mjs +30 -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/CodeBlockCopyScript.astro +9 -4
- package/src/components/EditSourceLink.astro +17 -0
- package/src/components/ImageCarousel.astro +5 -6
- package/src/components/NavigationPageTree.astro +66 -55
- package/src/components/NavigationTreeControls.astro +77 -0
- package/src/components/PageAliasRedirect.astro +1 -1
- package/src/components/PageList.astro +33 -0
- package/src/components/PageSequenceNavigation.astro +37 -0
- package/src/components/SearchPage.astro +151 -0
- package/src/components/SiteNavigation.astro +35 -8
- package/src/components/SitePage.astro +79 -26
- package/src/components/SiteSection.astro +11 -3
- package/src/components/SiteTreeNavigation.astro +8 -1
- package/src/components/TreeNavigationScript.astro +207 -26
- package/src/layouts/BaseLayout.astro +57 -2
- package/src/lib/sectionContent.ts +32 -3
- package/src/lib/siteNavigation.ts +31 -52
- 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 +242 -18
- package/src/styles/media.css +0 -5
- package/src/styles/navigation.css +31 -0
- package/src/styles/page-layout.css +438 -89
- package/src/styles/responsive.css +77 -8
- 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
|
@@ -2,21 +2,32 @@
|
|
|
2
2
|
import { getCollection, type CollectionEntry } from 'astro:content';
|
|
3
3
|
import BaseLayout from '../layouts/BaseLayout.astro';
|
|
4
4
|
import CodeBlockCopyScript from './CodeBlockCopyScript.astro';
|
|
5
|
+
import EditSourceLink from './EditSourceLink.astro';
|
|
5
6
|
import SiteBreadcrumbs from './SiteBreadcrumbs.astro';
|
|
6
7
|
import SiteNavigation from './SiteNavigation.astro';
|
|
7
8
|
import SiteBanners from './SiteBanners.astro';
|
|
8
9
|
import PageContentsNavigation from './PageContentsNavigation.astro';
|
|
10
|
+
import PageSequenceNavigation from './PageSequenceNavigation.astro';
|
|
9
11
|
import SiteSection from './SiteSection.astro';
|
|
10
12
|
import SiteTreeNavigation from './SiteTreeNavigation.astro';
|
|
11
13
|
import TreeNavigationScript from './TreeNavigationScript.astro';
|
|
12
14
|
import { getSectionsContent, type ResolvedSection } from '../lib/sectionContent';
|
|
13
15
|
import { getPageTheme } from '../lib/pageThemes';
|
|
14
16
|
import { getSiteModel, type SitePage } from '../lib/sitePages';
|
|
15
|
-
import {
|
|
17
|
+
import {
|
|
18
|
+
flattenSiteNavigationTree,
|
|
19
|
+
getDirectChildPages,
|
|
20
|
+
getListedSiteNavigationTree,
|
|
21
|
+
getSequentialPageNavigation,
|
|
22
|
+
} from '../lib/siteNavigation';
|
|
16
23
|
import { getToday, isVisibleToday } from '../lib/visibility';
|
|
17
24
|
import { getLogoAsset } from '../lib/sitePublicAssets';
|
|
18
|
-
import {
|
|
25
|
+
import {
|
|
26
|
+
resolveNavigationModel,
|
|
27
|
+
resolvePageContentsPlacement,
|
|
28
|
+
} from '../../scripts/lib/navigation-model.mjs';
|
|
19
29
|
import { projectConfig, resolveThemeVisualConfig } from '../../scripts/lib/project-config.mjs';
|
|
30
|
+
import { getEditSourceUrl } from '../../scripts/lib/edit-source-link.mjs';
|
|
20
31
|
import { getTextWidthCssValue, resolveFrameColors, resolvePagePresentation } from '../../scripts/lib/presentation.mjs';
|
|
21
32
|
import { mergePageThemeConfig } from '../../scripts/lib/theme-presets.mjs';
|
|
22
33
|
|
|
@@ -55,24 +66,8 @@ const logo = logoAsset
|
|
|
55
66
|
height: logoSettings?.height,
|
|
56
67
|
}
|
|
57
68
|
: undefined;
|
|
58
|
-
const siteHtml = entry.rendered?.html ?? '';
|
|
59
|
-
const resolvedSections = await getSectionsContent(siteHtml, currentPage);
|
|
60
|
-
const hasCodeBlocks = resolvedSections.some((section) => section.contentBlocks.some((block) => (
|
|
61
|
-
block.type === 'html' && /<pre\b[^>]*>[\s\S]*?<code\b/i.test(block.html)
|
|
62
|
-
)));
|
|
63
69
|
const today = getToday();
|
|
64
70
|
const siteBanners = (sitewideData.banners ?? []).filter((banner) => isVisibleToday(banner.visible, today));
|
|
65
|
-
const hasPrioritizableImage = (section: ResolvedSection) => section.contentBlocks.some((block) => (
|
|
66
|
-
block.type === 'image-stack'
|
|
67
|
-
|| block.type === 'image-carousel'
|
|
68
|
-
));
|
|
69
|
-
const firstImageSectionIndex = resolvedSections.findIndex(hasPrioritizableImage);
|
|
70
|
-
let contentSectionIndex = 0;
|
|
71
|
-
const renderedSections = resolvedSections.map((section, index) => ({
|
|
72
|
-
section,
|
|
73
|
-
sectionIndex: section.headingLevel === 1 ? 0 : contentSectionIndex++,
|
|
74
|
-
prioritizeFirstImage: index === firstImageSectionIndex,
|
|
75
|
-
}));
|
|
76
71
|
|
|
77
72
|
const getPageContent = (page: SitePage) =>
|
|
78
73
|
page.markdownDocument.navigationHeadings.map(({ depth, id, parentId, title }) => ({
|
|
@@ -92,7 +87,33 @@ const siteNavigation = siteNodes.map((node) => {
|
|
|
92
87
|
.map(({ id, title }) => ({ id, title })),
|
|
93
88
|
};
|
|
94
89
|
});
|
|
95
|
-
const
|
|
90
|
+
const listedNavigationTree = getListedSiteNavigationTree(siteNavigation);
|
|
91
|
+
const renderedNavigationEntries = flattenSiteNavigationTree(listedNavigationTree);
|
|
92
|
+
const pageSequence = getSequentialPageNavigation(listedNavigationTree, currentPage.pagePath);
|
|
93
|
+
const childPages = getDirectChildPages(listedNavigationTree, currentPage.pagePath);
|
|
94
|
+
const siteHtml = entry.rendered?.html ?? '';
|
|
95
|
+
const resolvedSections = await getSectionsContent(siteHtml, currentPage, childPages);
|
|
96
|
+
const hasCodeBlocks = resolvedSections.some((section) => section.contentBlocks.some((block) => (
|
|
97
|
+
block.type === 'html' && /<pre\b[^>]*>[\s\S]*?<code\b/i.test(block.html)
|
|
98
|
+
)));
|
|
99
|
+
const hasPrioritizableImage = (section: ResolvedSection) => section.contentBlocks.some((block) => (
|
|
100
|
+
block.type === 'image-stack'
|
|
101
|
+
|| block.type === 'image-carousel'
|
|
102
|
+
));
|
|
103
|
+
const firstImageSectionIndex = resolvedSections.findIndex(hasPrioritizableImage);
|
|
104
|
+
let contentSectionIndex = 0;
|
|
105
|
+
const renderedSections = resolvedSections.map((section, index) => ({
|
|
106
|
+
section,
|
|
107
|
+
sectionIndex: section.headingLevel === 1 ? 0 : contentSectionIndex++,
|
|
108
|
+
prioritizeFirstImage: index === firstImageSectionIndex,
|
|
109
|
+
}));
|
|
110
|
+
const hasPageSequence = Boolean(pageSequence.previous || pageSequence.next);
|
|
111
|
+
const editSourceUrl = projectConfig.editLink
|
|
112
|
+
? getEditSourceUrl({
|
|
113
|
+
baseUrl: projectConfig.editLink.baseUrl,
|
|
114
|
+
sourcePath: currentPage.contentLabel,
|
|
115
|
+
})
|
|
116
|
+
: null;
|
|
96
117
|
const navigationModel = resolveNavigationModel({
|
|
97
118
|
mode: projectConfig.navigation.mode,
|
|
98
119
|
nodes: renderedNavigationEntries.map(({ node, headings }) => ({
|
|
@@ -118,7 +139,23 @@ const currentPageNavigation = siteNavigation.find(({ node }) => (
|
|
|
118
139
|
node.kind === 'page' && node.pathname === currentPage.pathname
|
|
119
140
|
));
|
|
120
141
|
const pageContentsHeadings = currentPageNavigation?.headings.filter(({ depth }) => depth === 2 || depth === 3) ?? [];
|
|
121
|
-
const
|
|
142
|
+
const pageContentsPlacement = resolvePageContentsPlacement({
|
|
143
|
+
navigationMode: navigationModel.mode,
|
|
144
|
+
nodes: renderedNavigationEntries.map(({ node }) => ({
|
|
145
|
+
depth: Math.max(1, node.depth),
|
|
146
|
+
isHome: node.isHome,
|
|
147
|
+
listed: node.navigation.listed,
|
|
148
|
+
pagePath: node.pagePath,
|
|
149
|
+
})),
|
|
150
|
+
currentPage: {
|
|
151
|
+
isHome: currentPage.isHome,
|
|
152
|
+
pagePath: currentPage.pagePath,
|
|
153
|
+
},
|
|
154
|
+
headingCount: pageContentsHeadings.length,
|
|
155
|
+
});
|
|
156
|
+
const hasContentsNavigation = pageContentsPlacement.placement !== 'none';
|
|
157
|
+
const integratePageContentsInTree = pageContentsPlacement.placement === 'page-tree';
|
|
158
|
+
const showContentsRail = pageContentsPlacement.placement === 'contents-rail';
|
|
122
159
|
const hasNavigationRailFrame = navigationModel.requestedMode === 'tree'
|
|
123
160
|
|| (
|
|
124
161
|
navigationModel.requestedMode === 'automatic'
|
|
@@ -138,6 +175,7 @@ const reserveBreadcrumbSpace = hasTreeNavigation || showBreadcrumbs;
|
|
|
138
175
|
pathname={currentPage.pathname}
|
|
139
176
|
footer={sitewideData.footer}
|
|
140
177
|
contentsNavigation={hasContentsNavigation}
|
|
178
|
+
searchable
|
|
141
179
|
>
|
|
142
180
|
<SiteNavigation
|
|
143
181
|
logo={logo}
|
|
@@ -153,20 +191,31 @@ const reserveBreadcrumbSpace = hasTreeNavigation || showBreadcrumbs;
|
|
|
153
191
|
|
|
154
192
|
<div class:list={['site-page-layout', {
|
|
155
193
|
'site-page-layout-tree': hasTreeNavigation,
|
|
156
|
-
'site-page-layout-contents':
|
|
157
|
-
}]}
|
|
194
|
+
'site-page-layout-contents': showContentsRail,
|
|
195
|
+
}]}
|
|
196
|
+
data-page-contents-placement={hasContentsNavigation ? pageContentsPlacement.placement : undefined}
|
|
197
|
+
>
|
|
158
198
|
{hasTreeNavigation && (
|
|
159
|
-
<SiteTreeNavigation
|
|
199
|
+
<SiteTreeNavigation
|
|
200
|
+
currentPage={currentPage}
|
|
201
|
+
navigationEntries={siteNavigation}
|
|
202
|
+
integratePageSections={integratePageContentsInTree}
|
|
203
|
+
/>
|
|
160
204
|
)}
|
|
161
205
|
<main
|
|
162
|
-
class:list={['site-content', {
|
|
206
|
+
class:list={['site-content', {
|
|
207
|
+
'site-content-has-breadcrumbs': reserveBreadcrumbSpace,
|
|
208
|
+
'site-content-has-edit-source': Boolean(editSourceUrl),
|
|
209
|
+
'site-content-has-page-sequence': hasPageSequence,
|
|
210
|
+
}]}
|
|
163
211
|
id="main-content"
|
|
212
|
+
data-pagefind-body
|
|
164
213
|
style={`--page-text-width: ${pageTextWidth}`}
|
|
165
214
|
>
|
|
166
215
|
{showBreadcrumbs
|
|
167
216
|
? <SiteBreadcrumbs currentPage={currentPage} nodes={siteNodes} />
|
|
168
217
|
: reserveBreadcrumbSpace && <div class="site-breadcrumbs" aria-hidden="true"></div>}
|
|
169
|
-
{
|
|
218
|
+
{showContentsRail && (
|
|
170
219
|
<PageContentsNavigation
|
|
171
220
|
headings={pageContentsHeadings}
|
|
172
221
|
pageTitle={currentPage.title}
|
|
@@ -182,8 +231,12 @@ const reserveBreadcrumbSpace = hasTreeNavigation || showBreadcrumbs;
|
|
|
182
231
|
prioritizeFirstImage={prioritizeFirstImage}
|
|
183
232
|
/>
|
|
184
233
|
))}
|
|
234
|
+
{editSourceUrl && (
|
|
235
|
+
<EditSourceLink href={editSourceUrl} label={projectConfig.locale.labels.editSource} />
|
|
236
|
+
)}
|
|
237
|
+
<PageSequenceNavigation previous={pageSequence.previous} next={pageSequence.next} />
|
|
185
238
|
</main>
|
|
186
|
-
{
|
|
239
|
+
{showContentsRail && (
|
|
187
240
|
<PageContentsNavigation
|
|
188
241
|
headings={pageContentsHeadings}
|
|
189
242
|
pageTitle={currentPage.title}
|
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
import CardList from './CardList.astro';
|
|
10
10
|
import ImageCarousel from './ImageCarousel.astro';
|
|
11
11
|
import ImageStack from './ImageStack.astro';
|
|
12
|
+
import PageList from './PageList.astro';
|
|
12
13
|
import type { ResolvedSection } from '../lib/sectionContent';
|
|
13
14
|
|
|
14
15
|
type ResponsiveValue<T> = { desktop: T; mobile: T };
|
|
@@ -113,6 +114,7 @@ const isStructuredBlock = (block: ResolvedSection['contentBlocks'][number]) => (
|
|
|
113
114
|
block.type === 'image-stack'
|
|
114
115
|
|| block.type === 'image-carousel'
|
|
115
116
|
|| block.type === 'card-list'
|
|
117
|
+
|| block.type === 'page-list'
|
|
116
118
|
);
|
|
117
119
|
const hasMedia = section.contentBlocks.some((block) => (
|
|
118
120
|
block.type === 'image-stack'
|
|
@@ -121,7 +123,7 @@ const hasMedia = section.contentBlocks.some((block) => (
|
|
|
121
123
|
));
|
|
122
124
|
const contentGroups: Array<
|
|
123
125
|
| { type: 'reading'; blocks: ResolvedSection['contentBlocks'] }
|
|
124
|
-
| { type: '
|
|
126
|
+
| { type: 'structured'; block: Extract<ResolvedSection['contentBlocks'][number], { type: 'image-stack' | 'image-carousel' | 'card-list' | 'page-list' }> }
|
|
125
127
|
> = [];
|
|
126
128
|
let readingBlocks: ResolvedSection['contentBlocks'] = [];
|
|
127
129
|
const flushReadingBlocks = () => {
|
|
@@ -133,14 +135,17 @@ const flushReadingBlocks = () => {
|
|
|
133
135
|
for (const block of section.contentBlocks) {
|
|
134
136
|
if (isStructuredBlock(block)) {
|
|
135
137
|
flushReadingBlocks();
|
|
136
|
-
contentGroups.push({ type: '
|
|
138
|
+
contentGroups.push({ type: 'structured', block });
|
|
137
139
|
} else {
|
|
138
140
|
readingBlocks = [...readingBlocks, block];
|
|
139
141
|
}
|
|
140
142
|
}
|
|
141
143
|
|
|
142
144
|
flushReadingBlocks();
|
|
143
|
-
const firstMediaGroupIndex = contentGroups.findIndex((group) =>
|
|
145
|
+
const firstMediaGroupIndex = contentGroups.findIndex((group) => (
|
|
146
|
+
group.type === 'structured'
|
|
147
|
+
&& (group.block.type === 'image-stack' || group.block.type === 'image-carousel')
|
|
148
|
+
));
|
|
144
149
|
const markdownHeadingStyle = (level: 'h3' | 'h4') => {
|
|
145
150
|
const headingConfig = markdownHeadings[level];
|
|
146
151
|
const fontSize = getHeadingTypographySizeValue(level, headingConfig.size);
|
|
@@ -240,6 +245,9 @@ const sectionStyle = [
|
|
|
240
245
|
/>
|
|
241
246
|
);
|
|
242
247
|
}
|
|
248
|
+
if (block.type === 'page-list') {
|
|
249
|
+
return <PageList pages={block.pages} />;
|
|
250
|
+
}
|
|
243
251
|
|
|
244
252
|
return (
|
|
245
253
|
<ImageStack
|
|
@@ -3,13 +3,15 @@ import projectConfig from '../../scripts/lib/project-config.mjs';
|
|
|
3
3
|
import { getListedSiteNavigationTree, type SiteNavigationEntry } from '../lib/siteNavigation';
|
|
4
4
|
import type { SitePage } from '../lib/sitePages';
|
|
5
5
|
import NavigationPageTree from './NavigationPageTree.astro';
|
|
6
|
+
import NavigationTreeControls from './NavigationTreeControls.astro';
|
|
6
7
|
|
|
7
8
|
interface Props {
|
|
8
9
|
currentPage: SitePage;
|
|
9
10
|
navigationEntries: SiteNavigationEntry[];
|
|
11
|
+
integratePageSections?: boolean;
|
|
10
12
|
}
|
|
11
13
|
|
|
12
|
-
const { currentPage, navigationEntries } = Astro.props;
|
|
14
|
+
const { currentPage, navigationEntries, integratePageSections = false } = Astro.props;
|
|
13
15
|
const roots = getListedSiteNavigationTree(navigationEntries);
|
|
14
16
|
const activeRoot = roots.find((node) => (
|
|
15
17
|
currentPage.pageDirectory === node.node.pageDirectory
|
|
@@ -24,11 +26,16 @@ const navigationRoot = activeRoot?.node.pagePath ? `/${activeRoot.node.pagePath}
|
|
|
24
26
|
class="tree-local-navigation"
|
|
25
27
|
data-navigation-root={navigationRoot}
|
|
26
28
|
>
|
|
29
|
+
<NavigationTreeControls
|
|
30
|
+
idPrefix="desktop-tree-navigation"
|
|
31
|
+
nodes={[activeRoot]}
|
|
32
|
+
/>
|
|
27
33
|
<nav aria-label={`${activeRoot.node.title}: ${projectConfig.locale.labels.siteNavigation}`}>
|
|
28
34
|
<NavigationPageTree
|
|
29
35
|
nodes={[activeRoot]}
|
|
30
36
|
currentPage={currentPage}
|
|
31
37
|
variant="sidebar"
|
|
38
|
+
integratePageSections={integratePageSections}
|
|
32
39
|
/>
|
|
33
40
|
</nav>
|
|
34
41
|
</aside>
|
|
@@ -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
|
|
|
@@ -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,39 @@ 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 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');
|
|
135
176
|
const { buildInfo, copyrightMessage } = footer ?? {};
|
|
136
177
|
const showBuildInfo = buildInfo === true;
|
|
137
178
|
const buildTime = showBuildInfo
|
|
@@ -164,16 +205,29 @@ const readerPreferencesScript = readerPreferencesEnabled
|
|
|
164
205
|
data-reading-width={presentation.readerPreferences.defaults.readingWidth}
|
|
165
206
|
data-focus-reading={presentation.readerPreferences.defaults.focusReading}
|
|
166
207
|
data-section-tracking={contentsNavigation ? 'enabled' : undefined}
|
|
208
|
+
data-pagefind-ignore={searchable ? undefined : 'all'}
|
|
167
209
|
style={documentStyle}
|
|
168
210
|
>
|
|
169
211
|
<head>
|
|
170
212
|
<meta charset="utf-8" />
|
|
171
213
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
172
214
|
<meta name="format-detection" content="telephone=no" />
|
|
215
|
+
{!indexable && <meta name="robots" content="noindex" />}
|
|
173
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} />}
|
|
174
227
|
<meta name="theme-color" content={presentation.palette.page.backgroundColor} />
|
|
175
228
|
{readerPreferencesScript && <script is:inline set:html={readerPreferencesScript} />}
|
|
176
|
-
<
|
|
229
|
+
<style is:inline set:html={marginNoteLayoutCss} />
|
|
230
|
+
{indexable && <link rel="canonical" href={canonicalUrl} />}
|
|
177
231
|
<meta name="generator" content={Astro.generator} />
|
|
178
232
|
{iconLinks.map((iconLink) => (
|
|
179
233
|
<link
|
|
@@ -183,6 +237,7 @@ const readerPreferencesScript = readerPreferencesEnabled
|
|
|
183
237
|
href={iconLink.href}
|
|
184
238
|
/>
|
|
185
239
|
))}
|
|
240
|
+
<slot name="head" />
|
|
186
241
|
<title>{documentTitle}</title>
|
|
187
242
|
</head>
|
|
188
243
|
<body>
|