@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
|
@@ -2,21 +2,34 @@
|
|
|
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';
|
|
6
|
+
import ImageStackEnhancement from './ImageStackEnhancement.astro';
|
|
5
7
|
import SiteBreadcrumbs from './SiteBreadcrumbs.astro';
|
|
6
8
|
import SiteNavigation from './SiteNavigation.astro';
|
|
7
9
|
import SiteBanners from './SiteBanners.astro';
|
|
8
10
|
import PageContentsNavigation from './PageContentsNavigation.astro';
|
|
11
|
+
import PageSequenceNavigation from './PageSequenceNavigation.astro';
|
|
9
12
|
import SiteSection from './SiteSection.astro';
|
|
10
13
|
import SiteTreeNavigation from './SiteTreeNavigation.astro';
|
|
14
|
+
import TableOverflowScript from './TableOverflowScript.astro';
|
|
11
15
|
import TreeNavigationScript from './TreeNavigationScript.astro';
|
|
12
16
|
import { getSectionsContent, type ResolvedSection } from '../lib/sectionContent';
|
|
13
17
|
import { getPageTheme } from '../lib/pageThemes';
|
|
14
18
|
import { getSiteModel, type SitePage } from '../lib/sitePages';
|
|
15
|
-
import {
|
|
19
|
+
import {
|
|
20
|
+
flattenSiteNavigationTree,
|
|
21
|
+
getDirectChildPages,
|
|
22
|
+
getListedSiteNavigationTree,
|
|
23
|
+
getSequentialPageNavigation,
|
|
24
|
+
} from '../lib/siteNavigation';
|
|
16
25
|
import { getToday, isVisibleToday } from '../lib/visibility';
|
|
17
26
|
import { getLogoAsset } from '../lib/sitePublicAssets';
|
|
18
|
-
import {
|
|
27
|
+
import {
|
|
28
|
+
resolveNavigationModel,
|
|
29
|
+
resolvePageContentsPlacement,
|
|
30
|
+
} from '../../scripts/lib/navigation-model.mjs';
|
|
19
31
|
import { projectConfig, resolveThemeVisualConfig } from '../../scripts/lib/project-config.mjs';
|
|
32
|
+
import { resolveEditSourceTarget } from '../../scripts/lib/edit-source-link.mjs';
|
|
20
33
|
import { getTextWidthCssValue, resolveFrameColors, resolvePagePresentation } from '../../scripts/lib/presentation.mjs';
|
|
21
34
|
import { mergePageThemeConfig } from '../../scripts/lib/theme-presets.mjs';
|
|
22
35
|
|
|
@@ -55,24 +68,8 @@ const logo = logoAsset
|
|
|
55
68
|
height: logoSettings?.height,
|
|
56
69
|
}
|
|
57
70
|
: 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
71
|
const today = getToday();
|
|
64
72
|
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
73
|
|
|
77
74
|
const getPageContent = (page: SitePage) =>
|
|
78
75
|
page.markdownDocument.navigationHeadings.map(({ depth, id, parentId, title }) => ({
|
|
@@ -92,7 +89,42 @@ const siteNavigation = siteNodes.map((node) => {
|
|
|
92
89
|
.map(({ id, title }) => ({ id, title })),
|
|
93
90
|
};
|
|
94
91
|
});
|
|
95
|
-
const
|
|
92
|
+
const listedNavigationTree = getListedSiteNavigationTree(siteNavigation);
|
|
93
|
+
const renderedNavigationEntries = flattenSiteNavigationTree(listedNavigationTree);
|
|
94
|
+
const pageSequence = getSequentialPageNavigation(listedNavigationTree, currentPage.pagePath);
|
|
95
|
+
const childPages = getDirectChildPages(listedNavigationTree, currentPage.pagePath);
|
|
96
|
+
const siteHtml = entry.rendered?.html ?? '';
|
|
97
|
+
const resolvedSections = await getSectionsContent(siteHtml, currentPage, childPages);
|
|
98
|
+
const hasCodeBlocks = resolvedSections.some((section) => section.contentBlocks.some((block) => (
|
|
99
|
+
block.type === 'html' && /<pre\b[^>]*>[\s\S]*?<code\b/i.test(block.html)
|
|
100
|
+
)));
|
|
101
|
+
const hasTables = resolvedSections.some((section) => section.contentBlocks.some((block) => (
|
|
102
|
+
block.type === 'html' && /\bdata-table-frame(?:="")?/i.test(block.html)
|
|
103
|
+
)));
|
|
104
|
+
const hasImageStack = resolvedSections.some((section) => section.contentBlocks.some((block) => (
|
|
105
|
+
block.type === 'image-stack'
|
|
106
|
+
)));
|
|
107
|
+
const hasPrioritizableImage = (section: ResolvedSection) => section.contentBlocks.some((block) => (
|
|
108
|
+
block.type === 'image-stack'
|
|
109
|
+
|| block.type === 'image-carousel'
|
|
110
|
+
));
|
|
111
|
+
const firstImageSectionIndex = resolvedSections.findIndex(hasPrioritizableImage);
|
|
112
|
+
let contentSectionIndex = 0;
|
|
113
|
+
const renderedSections = resolvedSections.map((section, index) => ({
|
|
114
|
+
section,
|
|
115
|
+
sectionIndex: section.headingLevel === 1 ? 0 : contentSectionIndex++,
|
|
116
|
+
prioritizeFirstImage: index === firstImageSectionIndex,
|
|
117
|
+
}));
|
|
118
|
+
const hasPageSequence = Boolean(pageSequence.previous || pageSequence.next);
|
|
119
|
+
const editSourceTarget = projectConfig.editLink
|
|
120
|
+
? resolveEditSourceTarget({
|
|
121
|
+
...projectConfig.editLink,
|
|
122
|
+
development: import.meta.env.DEV,
|
|
123
|
+
hostname: Astro.url.hostname,
|
|
124
|
+
sourceLabel: currentPage.contentLabel,
|
|
125
|
+
sourcePath: currentPage.contentPath,
|
|
126
|
+
})
|
|
127
|
+
: null;
|
|
96
128
|
const navigationModel = resolveNavigationModel({
|
|
97
129
|
mode: projectConfig.navigation.mode,
|
|
98
130
|
nodes: renderedNavigationEntries.map(({ node, headings }) => ({
|
|
@@ -114,18 +146,39 @@ const pagePresentation = resolvePagePresentation(themeData, themeLabel, {
|
|
|
114
146
|
const pageTextWidth = getTextWidthCssValue(pagePresentation.typography.values.body.width);
|
|
115
147
|
const frameColors = resolveFrameColors({ theme: siteThemeData, sourceLabel: `${siteDirectory || 'site'}/${siteTheme.id}` });
|
|
116
148
|
const hasTreeNavigation = navigationModel.mode === 'tree';
|
|
149
|
+
const showTreeNavigation = hasTreeNavigation && !currentPage.isHome;
|
|
117
150
|
const currentPageNavigation = siteNavigation.find(({ node }) => (
|
|
118
151
|
node.kind === 'page' && node.pathname === currentPage.pathname
|
|
119
152
|
));
|
|
120
153
|
const pageContentsHeadings = currentPageNavigation?.headings.filter(({ depth }) => depth === 2 || depth === 3) ?? [];
|
|
121
|
-
const
|
|
122
|
-
|
|
154
|
+
const pageContentsPlacement = resolvePageContentsPlacement({
|
|
155
|
+
navigationMode: navigationModel.mode,
|
|
156
|
+
nodes: renderedNavigationEntries.map(({ node }) => ({
|
|
157
|
+
depth: Math.max(1, node.depth),
|
|
158
|
+
isHome: node.isHome,
|
|
159
|
+
listed: node.navigation.listed,
|
|
160
|
+
pagePath: node.pagePath,
|
|
161
|
+
})),
|
|
162
|
+
currentPage: {
|
|
163
|
+
isHome: currentPage.isHome,
|
|
164
|
+
pagePath: currentPage.pagePath,
|
|
165
|
+
},
|
|
166
|
+
headingCount: pageContentsHeadings.length,
|
|
167
|
+
});
|
|
168
|
+
const hasContentsNavigation = pageContentsPlacement.placement !== 'none';
|
|
169
|
+
const hasTrackedSectionNavigation = hasContentsNavigation
|
|
170
|
+
|| (hasTreeNavigation && currentPage.isHome && pageContentsHeadings.length >= 2);
|
|
171
|
+
const integratePageContentsInTree = hasTreeNavigation;
|
|
172
|
+
const showContentsRail = pageContentsPlacement.placement === 'contents-rail';
|
|
173
|
+
const hasNavigationRailFrame = !currentPage.isHome && (
|
|
174
|
+
navigationModel.requestedMode === 'tree'
|
|
123
175
|
|| (
|
|
124
176
|
navigationModel.requestedMode === 'automatic'
|
|
125
177
|
&& (navigationModel.hasNestedPages || navigationModel.hasCategories)
|
|
126
|
-
)
|
|
178
|
+
)
|
|
179
|
+
);
|
|
127
180
|
const showBreadcrumbs = !currentPage.isHome && currentPage.parentPagePath !== null;
|
|
128
|
-
const reserveBreadcrumbSpace =
|
|
181
|
+
const reserveBreadcrumbSpace = showTreeNavigation || showBreadcrumbs;
|
|
129
182
|
|
|
130
183
|
---
|
|
131
184
|
|
|
@@ -137,7 +190,8 @@ const reserveBreadcrumbSpace = hasTreeNavigation || showBreadcrumbs;
|
|
|
137
190
|
frameColors={frameColors}
|
|
138
191
|
pathname={currentPage.pathname}
|
|
139
192
|
footer={sitewideData.footer}
|
|
140
|
-
contentsNavigation={
|
|
193
|
+
contentsNavigation={hasTrackedSectionNavigation}
|
|
194
|
+
searchable
|
|
141
195
|
>
|
|
142
196
|
<SiteNavigation
|
|
143
197
|
logo={logo}
|
|
@@ -152,27 +206,31 @@ const reserveBreadcrumbSpace = hasTreeNavigation || showBreadcrumbs;
|
|
|
152
206
|
{siteBanners.length > 0 && <SiteBanners banners={siteBanners} />}
|
|
153
207
|
|
|
154
208
|
<div class:list={['site-page-layout', {
|
|
155
|
-
'site-page-layout-tree':
|
|
156
|
-
'site-page-layout-contents':
|
|
157
|
-
}]}
|
|
158
|
-
{
|
|
159
|
-
|
|
209
|
+
'site-page-layout-tree': showTreeNavigation,
|
|
210
|
+
'site-page-layout-contents': showContentsRail,
|
|
211
|
+
}]}
|
|
212
|
+
data-page-contents-placement={hasContentsNavigation ? pageContentsPlacement.placement : undefined}
|
|
213
|
+
>
|
|
214
|
+
{showTreeNavigation && (
|
|
215
|
+
<SiteTreeNavigation
|
|
216
|
+
currentPage={currentPage}
|
|
217
|
+
navigationEntries={siteNavigation}
|
|
218
|
+
integratePageSections={integratePageContentsInTree}
|
|
219
|
+
/>
|
|
160
220
|
)}
|
|
161
221
|
<main
|
|
162
|
-
class:list={['site-content', {
|
|
222
|
+
class:list={['site-content', {
|
|
223
|
+
'site-content-has-breadcrumbs': reserveBreadcrumbSpace,
|
|
224
|
+
'site-content-has-edit-source': Boolean(editSourceTarget),
|
|
225
|
+
'site-content-has-page-sequence': hasPageSequence,
|
|
226
|
+
}]}
|
|
163
227
|
id="main-content"
|
|
228
|
+
data-pagefind-body
|
|
164
229
|
style={`--page-text-width: ${pageTextWidth}`}
|
|
165
230
|
>
|
|
166
231
|
{showBreadcrumbs
|
|
167
232
|
? <SiteBreadcrumbs currentPage={currentPage} nodes={siteNodes} />
|
|
168
233
|
: reserveBreadcrumbSpace && <div class="site-breadcrumbs" aria-hidden="true"></div>}
|
|
169
|
-
{hasContentsNavigation && (
|
|
170
|
-
<PageContentsNavigation
|
|
171
|
-
headings={pageContentsHeadings}
|
|
172
|
-
pageTitle={currentPage.title}
|
|
173
|
-
variant="inline"
|
|
174
|
-
/>
|
|
175
|
-
)}
|
|
176
234
|
{renderedSections.map(({ section, sectionIndex, prioritizeFirstImage }) => (
|
|
177
235
|
<SiteSection
|
|
178
236
|
section={section}
|
|
@@ -182,15 +240,25 @@ const reserveBreadcrumbSpace = hasTreeNavigation || showBreadcrumbs;
|
|
|
182
240
|
prioritizeFirstImage={prioritizeFirstImage}
|
|
183
241
|
/>
|
|
184
242
|
))}
|
|
243
|
+
{editSourceTarget && (
|
|
244
|
+
<EditSourceLink
|
|
245
|
+
href={editSourceTarget.href}
|
|
246
|
+
label={editSourceTarget.kind === 'local'
|
|
247
|
+
? projectConfig.locale.labels.openInVsCode
|
|
248
|
+
: projectConfig.locale.labels.editSource}
|
|
249
|
+
/>
|
|
250
|
+
)}
|
|
251
|
+
<PageSequenceNavigation previous={pageSequence.previous} next={pageSequence.next} />
|
|
185
252
|
</main>
|
|
186
|
-
{
|
|
253
|
+
{showContentsRail && (
|
|
187
254
|
<PageContentsNavigation
|
|
188
255
|
headings={pageContentsHeadings}
|
|
189
256
|
pageTitle={currentPage.title}
|
|
190
|
-
variant="rail"
|
|
191
257
|
/>
|
|
192
258
|
)}
|
|
193
259
|
</div>
|
|
194
260
|
{hasTreeNavigation && <TreeNavigationScript />}
|
|
195
261
|
{hasCodeBlocks && <CodeBlockCopyScript />}
|
|
262
|
+
{hasTables && <TableOverflowScript />}
|
|
263
|
+
{hasImageStack && <ImageStackEnhancement />}
|
|
196
264
|
</BaseLayout>
|
|
@@ -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
|
|
@@ -1,21 +1,25 @@
|
|
|
1
1
|
---
|
|
2
2
|
import projectConfig from '../../scripts/lib/project-config.mjs';
|
|
3
|
+
import { withBasePath } from '../lib/basePath';
|
|
3
4
|
import { getListedSiteNavigationTree, type SiteNavigationEntry } from '../lib/siteNavigation';
|
|
4
5
|
import type { SitePage } from '../lib/sitePages';
|
|
5
6
|
import NavigationPageTree from './NavigationPageTree.astro';
|
|
7
|
+
import NavigationTreeControls from './NavigationTreeControls.astro';
|
|
6
8
|
|
|
7
9
|
interface Props {
|
|
8
10
|
currentPage: SitePage;
|
|
9
11
|
navigationEntries: SiteNavigationEntry[];
|
|
12
|
+
integratePageSections?: boolean;
|
|
10
13
|
}
|
|
11
14
|
|
|
12
|
-
const { currentPage, navigationEntries } = Astro.props;
|
|
15
|
+
const { currentPage, navigationEntries, integratePageSections = false } = Astro.props;
|
|
13
16
|
const roots = getListedSiteNavigationTree(navigationEntries);
|
|
14
17
|
const activeRoot = roots.find((node) => (
|
|
15
18
|
currentPage.pageDirectory === node.node.pageDirectory
|
|
16
19
|
|| currentPage.pageDirectory.startsWith(`${node.node.pageDirectory}/pages/`)
|
|
17
20
|
));
|
|
18
21
|
const navigationRoot = activeRoot?.node.pagePath ? `/${activeRoot.node.pagePath}/` : '/';
|
|
22
|
+
const navigationStateRoot = withBasePath(projectConfig.site.basePath, '/');
|
|
19
23
|
---
|
|
20
24
|
|
|
21
25
|
{activeRoot && (
|
|
@@ -23,12 +27,18 @@ const navigationRoot = activeRoot?.node.pagePath ? `/${activeRoot.node.pagePath}
|
|
|
23
27
|
id="tree-local-navigation"
|
|
24
28
|
class="tree-local-navigation"
|
|
25
29
|
data-navigation-root={navigationRoot}
|
|
30
|
+
data-navigation-state-root={navigationStateRoot}
|
|
26
31
|
>
|
|
32
|
+
<NavigationTreeControls
|
|
33
|
+
idPrefix="desktop-tree-navigation"
|
|
34
|
+
nodes={[activeRoot]}
|
|
35
|
+
/>
|
|
27
36
|
<nav aria-label={`${activeRoot.node.title}: ${projectConfig.locale.labels.siteNavigation}`}>
|
|
28
37
|
<NavigationPageTree
|
|
29
38
|
nodes={[activeRoot]}
|
|
30
39
|
currentPage={currentPage}
|
|
31
40
|
variant="sidebar"
|
|
41
|
+
integratePageSections={integratePageSections}
|
|
32
42
|
/>
|
|
33
43
|
</nav>
|
|
34
44
|
</aside>
|
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
---
|
|
2
|
+
import { ChevronLeft, ChevronRight } from '@lucide/astro';
|
|
3
|
+
import projectConfig from '../../scripts/lib/project-config.mjs';
|
|
4
|
+
|
|
5
|
+
const labels = projectConfig.locale.labels;
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
<template data-table-navigation-template>
|
|
9
|
+
<div class="norna-table-navigation" data-table-navigation hidden>
|
|
10
|
+
<span class="visually-hidden" data-table-overflow-description>{labels.tableOverflowDescription}</span>
|
|
11
|
+
<div class="norna-table-navigation-buttons" role="group" aria-label={labels.tableColumns}>
|
|
12
|
+
<button
|
|
13
|
+
type="button"
|
|
14
|
+
data-table-scroll-previous
|
|
15
|
+
aria-label={labels.tablePreviousColumns}
|
|
16
|
+
title={labels.tablePreviousColumns}
|
|
17
|
+
>
|
|
18
|
+
<ChevronLeft aria-hidden="true" size={18} stroke-width={1.8} />
|
|
19
|
+
</button>
|
|
20
|
+
<button
|
|
21
|
+
type="button"
|
|
22
|
+
data-table-scroll-next
|
|
23
|
+
aria-label={labels.tableNextColumns}
|
|
24
|
+
title={labels.tableNextColumns}
|
|
25
|
+
>
|
|
26
|
+
<ChevronRight aria-hidden="true" size={18} stroke-width={1.8} />
|
|
27
|
+
</button>
|
|
28
|
+
</div>
|
|
29
|
+
</div>
|
|
30
|
+
</template>
|
|
31
|
+
|
|
32
|
+
<script>
|
|
33
|
+
type TableLayout = 'contained' | 'prose' | 'end' | 'canvas';
|
|
34
|
+
const layoutUpdaters = new Set<() => void>();
|
|
35
|
+
const navigationTemplate = document.querySelector<HTMLTemplateElement>('[data-table-navigation-template]');
|
|
36
|
+
const synchronizedAttributes = [
|
|
37
|
+
'data-appearance',
|
|
38
|
+
'data-focus-reading',
|
|
39
|
+
'data-reading-width',
|
|
40
|
+
'dir',
|
|
41
|
+
];
|
|
42
|
+
|
|
43
|
+
const initializeTableFrame = (frame: HTMLElement, frameIndex: number) => {
|
|
44
|
+
const scrollRegion = frame.querySelector<HTMLElement>('[data-table-scroll]');
|
|
45
|
+
const table = scrollRegion?.querySelector<HTMLTableElement>('table');
|
|
46
|
+
if (!scrollRegion || frame.dataset.tableReady === 'true') return;
|
|
47
|
+
const isTopLevel = frame.parentElement?.classList.contains('section-markdown') === true;
|
|
48
|
+
const tableHead = table?.tHead;
|
|
49
|
+
const headingCells = Array.from(tableHead?.querySelectorAll<HTMLTableCellElement>('th') ?? []);
|
|
50
|
+
let scheduledFrame: number | null = null;
|
|
51
|
+
let stickyHeading: HTMLElement | null = null;
|
|
52
|
+
let stickyHeadingTrack: HTMLElement | null = null;
|
|
53
|
+
let tableNavigation: HTMLElement | null = null;
|
|
54
|
+
let previousButton: HTMLButtonElement | null = null;
|
|
55
|
+
let nextButton: HTMLButtonElement | null = null;
|
|
56
|
+
let overflowDescriptionId: string | null = null;
|
|
57
|
+
|
|
58
|
+
const removeDuplicateIdentity = (element: HTMLElement) => {
|
|
59
|
+
element.removeAttribute('id');
|
|
60
|
+
element.querySelectorAll<HTMLElement>('[id]').forEach((descendant) => descendant.removeAttribute('id'));
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
const createStickyHeading = () => {
|
|
64
|
+
if (!isTopLevel || !tableHead || headingCells.length === 0) return;
|
|
65
|
+
|
|
66
|
+
stickyHeading = document.createElement('div');
|
|
67
|
+
stickyHeading.className = 'norna-table-sticky-heading';
|
|
68
|
+
stickyHeading.dataset.tableStickyHeading = '';
|
|
69
|
+
stickyHeading.setAttribute('aria-hidden', 'true');
|
|
70
|
+
stickyHeading.setAttribute('inert', '');
|
|
71
|
+
|
|
72
|
+
stickyHeadingTrack = document.createElement('div');
|
|
73
|
+
stickyHeadingTrack.className = 'norna-table-sticky-heading-track';
|
|
74
|
+
stickyHeading.append(stickyHeadingTrack);
|
|
75
|
+
frame.insertBefore(stickyHeading, scrollRegion);
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
const createTableNavigation = () => {
|
|
79
|
+
if (!navigationTemplate) return;
|
|
80
|
+
const fragment = navigationTemplate.content.cloneNode(true) as DocumentFragment;
|
|
81
|
+
tableNavigation = fragment.querySelector<HTMLElement>('[data-table-navigation]');
|
|
82
|
+
if (!tableNavigation) return;
|
|
83
|
+
|
|
84
|
+
const description = tableNavigation.querySelector<HTMLElement>('[data-table-overflow-description]');
|
|
85
|
+
previousButton = tableNavigation.querySelector<HTMLButtonElement>('[data-table-scroll-previous]');
|
|
86
|
+
nextButton = tableNavigation.querySelector<HTMLButtonElement>('[data-table-scroll-next]');
|
|
87
|
+
overflowDescriptionId = `norna-table-overflow-${frameIndex + 1}`;
|
|
88
|
+
if (description) description.id = overflowDescriptionId;
|
|
89
|
+
frame.insertBefore(fragment, scrollRegion);
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
const setOverflowDescription = (enabled: boolean) => {
|
|
93
|
+
if (!overflowDescriptionId) return;
|
|
94
|
+
const descriptionIds = new Set((scrollRegion.getAttribute('aria-describedby') ?? '')
|
|
95
|
+
.split(/\s+/u)
|
|
96
|
+
.filter(Boolean));
|
|
97
|
+
if (enabled) descriptionIds.add(overflowDescriptionId);
|
|
98
|
+
else descriptionIds.delete(overflowDescriptionId);
|
|
99
|
+
if (descriptionIds.size > 0) scrollRegion.setAttribute('aria-describedby', [...descriptionIds].join(' '));
|
|
100
|
+
else scrollRegion.removeAttribute('aria-describedby');
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
const scrollTable = (direction: -1 | 1) => {
|
|
104
|
+
const isRightToLeft = getComputedStyle(scrollRegion).direction === 'rtl';
|
|
105
|
+
const distance = Math.max(1, Math.round(scrollRegion.clientWidth * 0.8));
|
|
106
|
+
const reducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
|
|
107
|
+
scrollRegion.scrollBy({
|
|
108
|
+
behavior: reducedMotion ? 'auto' : 'smooth',
|
|
109
|
+
left: direction * distance * (isRightToLeft ? -1 : 1),
|
|
110
|
+
});
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
const synchronizeStickyHeadingPosition = () => {
|
|
114
|
+
if (!stickyHeading || !stickyHeadingTrack || !table) return;
|
|
115
|
+
const layerBounds = stickyHeading.getBoundingClientRect();
|
|
116
|
+
const tableBounds = table.getBoundingClientRect();
|
|
117
|
+
stickyHeadingTrack.style.transform = `translateX(${tableBounds.left - layerBounds.left}px)`;
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
const synchronizeStickyHeadingGeometry = () => {
|
|
121
|
+
if (!stickyHeading || !stickyHeadingTrack || !table || !tableHead) return false;
|
|
122
|
+
const tableBounds = table.getBoundingClientRect();
|
|
123
|
+
const headingBounds = tableHead.getBoundingClientRect();
|
|
124
|
+
if (tableBounds.width <= 0 || headingBounds.height <= 0) {
|
|
125
|
+
return false;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
frame.style.setProperty('--table-sticky-heading-height', `${headingBounds.height}px`);
|
|
129
|
+
stickyHeadingTrack.style.width = `${tableBounds.width}px`;
|
|
130
|
+
stickyHeadingTrack.style.height = `${headingBounds.height}px`;
|
|
131
|
+
stickyHeadingTrack.replaceChildren(...headingCells.map((headingCell) => {
|
|
132
|
+
const cellBounds = headingCell.getBoundingClientRect();
|
|
133
|
+
const computedStyle = getComputedStyle(headingCell);
|
|
134
|
+
const visualCell = document.createElement('div');
|
|
135
|
+
visualCell.className = 'norna-table-sticky-heading-cell';
|
|
136
|
+
visualCell.style.left = `${cellBounds.left - tableBounds.left}px`;
|
|
137
|
+
visualCell.style.top = `${cellBounds.top - headingBounds.top}px`;
|
|
138
|
+
visualCell.style.width = `${cellBounds.width}px`;
|
|
139
|
+
visualCell.style.height = `${cellBounds.height}px`;
|
|
140
|
+
visualCell.style.font = computedStyle.font;
|
|
141
|
+
visualCell.style.fontWeight = computedStyle.fontWeight;
|
|
142
|
+
visualCell.style.letterSpacing = computedStyle.letterSpacing;
|
|
143
|
+
visualCell.style.lineHeight = computedStyle.lineHeight;
|
|
144
|
+
visualCell.style.padding = computedStyle.padding;
|
|
145
|
+
visualCell.style.textAlign = computedStyle.textAlign;
|
|
146
|
+
visualCell.style.textTransform = computedStyle.textTransform;
|
|
147
|
+
visualCell.style.whiteSpace = computedStyle.whiteSpace;
|
|
148
|
+
visualCell.style.wordBreak = computedStyle.wordBreak;
|
|
149
|
+
visualCell.style.overflowWrap = computedStyle.overflowWrap;
|
|
150
|
+
visualCell.append(...Array.from(headingCell.childNodes, (node) => node.cloneNode(true)));
|
|
151
|
+
removeDuplicateIdentity(visualCell);
|
|
152
|
+
return visualCell;
|
|
153
|
+
}));
|
|
154
|
+
synchronizeStickyHeadingPosition();
|
|
155
|
+
return true;
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
createTableNavigation();
|
|
159
|
+
createStickyHeading();
|
|
160
|
+
previousButton?.addEventListener('click', () => scrollTable(-1));
|
|
161
|
+
nextButton?.addEventListener('click', () => scrollTable(1));
|
|
162
|
+
|
|
163
|
+
const updateOverflow = ({ refreshStickyHeading = false } = {}) => {
|
|
164
|
+
const maximumScroll = Math.max(0, scrollRegion.scrollWidth - scrollRegion.clientWidth);
|
|
165
|
+
const hasOverflow = maximumScroll > 1;
|
|
166
|
+
const scrollBounds = scrollRegion.getBoundingClientRect();
|
|
167
|
+
const tableBounds = table?.getBoundingClientRect();
|
|
168
|
+
const isRightToLeft = getComputedStyle(scrollRegion).direction === 'rtl';
|
|
169
|
+
const isAtStart = !hasOverflow || !tableBounds || (isRightToLeft
|
|
170
|
+
? tableBounds.right <= scrollBounds.right + 2
|
|
171
|
+
: tableBounds.left >= scrollBounds.left - 2);
|
|
172
|
+
const isAtEnd = !hasOverflow || !tableBounds || (isRightToLeft
|
|
173
|
+
? tableBounds.left >= scrollBounds.left - 2
|
|
174
|
+
: tableBounds.right <= scrollBounds.right + 2);
|
|
175
|
+
|
|
176
|
+
if (tableNavigation) {
|
|
177
|
+
tableNavigation.hidden = !hasOverflow;
|
|
178
|
+
frame.style.setProperty(
|
|
179
|
+
'--table-navigation-height',
|
|
180
|
+
hasOverflow ? `${tableNavigation.getBoundingClientRect().height}px` : '0px',
|
|
181
|
+
);
|
|
182
|
+
}
|
|
183
|
+
const stickyHeadingReady = hasOverflow && stickyHeading
|
|
184
|
+
? (refreshStickyHeading
|
|
185
|
+
? synchronizeStickyHeadingGeometry()
|
|
186
|
+
: (stickyHeadingTrack?.childElementCount ?? 0) > 0)
|
|
187
|
+
: false;
|
|
188
|
+
|
|
189
|
+
frame.dataset.tableOverflow = hasOverflow ? 'true' : 'false';
|
|
190
|
+
frame.dataset.tableAtStart = isAtStart ? 'true' : 'false';
|
|
191
|
+
frame.dataset.tableAtEnd = isAtEnd ? 'true' : 'false';
|
|
192
|
+
frame.dataset.tableStickyHeading = stickyHeadingReady ? 'true' : 'false';
|
|
193
|
+
if (previousButton) previousButton.disabled = isAtStart;
|
|
194
|
+
if (nextButton) nextButton.disabled = isAtEnd;
|
|
195
|
+
setOverflowDescription(hasOverflow);
|
|
196
|
+
if (hasOverflow) scrollRegion.setAttribute('tabindex', '0');
|
|
197
|
+
else scrollRegion.removeAttribute('tabindex');
|
|
198
|
+
if (stickyHeadingReady && !refreshStickyHeading) synchronizeStickyHeadingPosition();
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
const tableFitsLayout = (layout: TableLayout) => {
|
|
202
|
+
frame.dataset.tableLayout = layout;
|
|
203
|
+
return scrollRegion.scrollWidth - scrollRegion.clientWidth <= 1;
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
const updateLayout = () => {
|
|
207
|
+
scheduledFrame = null;
|
|
208
|
+
if (!isTopLevel || !table) {
|
|
209
|
+
frame.dataset.tableLayout = 'contained';
|
|
210
|
+
updateOverflow();
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const candidates: TableLayout[] = ['prose', 'end', 'canvas'];
|
|
215
|
+
const selected = candidates.find(tableFitsLayout) ?? 'canvas';
|
|
216
|
+
frame.dataset.tableLayout = selected;
|
|
217
|
+
updateOverflow({ refreshStickyHeading: true });
|
|
218
|
+
};
|
|
219
|
+
|
|
220
|
+
const scheduleLayoutUpdate = () => {
|
|
221
|
+
if (scheduledFrame !== null) return;
|
|
222
|
+
scheduledFrame = requestAnimationFrame(updateLayout);
|
|
223
|
+
};
|
|
224
|
+
|
|
225
|
+
frame.dataset.tableReady = 'true';
|
|
226
|
+
scrollRegion.addEventListener('scroll', () => updateOverflow(), { passive: true });
|
|
227
|
+
layoutUpdaters.add(scheduleLayoutUpdate);
|
|
228
|
+
if ('ResizeObserver' in window) {
|
|
229
|
+
const observer = new ResizeObserver(scheduleLayoutUpdate);
|
|
230
|
+
observer.observe(frame);
|
|
231
|
+
observer.observe(scrollRegion);
|
|
232
|
+
if (table) observer.observe(table);
|
|
233
|
+
if (frame.parentElement) observer.observe(frame.parentElement);
|
|
234
|
+
} else {
|
|
235
|
+
window.addEventListener('resize', scheduleLayoutUpdate, { passive: true });
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
updateLayout();
|
|
239
|
+
document.fonts?.ready.then(scheduleLayoutUpdate).catch(() => {});
|
|
240
|
+
};
|
|
241
|
+
|
|
242
|
+
const frames = Array.from(document.querySelectorAll<HTMLElement>('[data-table-frame]'));
|
|
243
|
+
frames.forEach(initializeTableFrame);
|
|
244
|
+
if (frames.length > 0) {
|
|
245
|
+
const preferenceObserver = new MutationObserver(() => layoutUpdaters.forEach((update) => update()));
|
|
246
|
+
preferenceObserver.observe(document.documentElement, {
|
|
247
|
+
attributeFilter: synchronizedAttributes,
|
|
248
|
+
attributes: true,
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
</script>
|