@janga/norna 0.7.22 → 0.7.23
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/astro.config.mjs +3 -0
- package/bin/norna-cli.mjs +2 -2
- package/package.json +17 -3
- package/schemas/category.schema.json +2 -2
- package/schemas/config.schema.json +22 -24
- package/schemas/content-frontmatter.schema.json +25 -5
- package/schemas/page-theme.schema.json +53 -25
- package/schemas/sitewide-content.schema.json +19 -18
- package/schemas/theme.schema.json +383 -250
- package/scripts/check-config.mjs +24 -4
- package/scripts/deploy-site.mjs +0 -1
- package/scripts/dev-local.mjs +175 -112
- package/scripts/lib/content-sync-apply.mjs +34 -0
- package/scripts/lib/content-sync-plan.mjs +179 -0
- package/scripts/lib/editor-language-service.mjs +128 -0
- package/scripts/lib/heading-ids.mjs +1 -1
- package/scripts/lib/image-presentation.mjs +2 -0
- package/scripts/lib/markdown-links.mjs +182 -0
- package/scripts/lib/navigation-model.mjs +25 -14
- package/scripts/lib/norna-markdown-blocks.mjs +1 -0
- package/scripts/lib/page-aliases.mjs +164 -0
- package/scripts/lib/page-markdown.mjs +56 -6
- package/scripts/lib/presentation.mjs +19 -37
- package/scripts/lib/project-config.mjs +50 -29
- package/scripts/lib/schema-definitions.mjs +24 -13
- package/scripts/lib/schema-editor-metadata.mjs +43 -30
- package/scripts/lib/schema-value-definitions.mjs +6 -2
- package/scripts/lib/site-content.mjs +26 -4
- package/scripts/lib/site-link-graph.mjs +326 -0
- package/scripts/lib/site-page-urls.mjs +10 -0
- package/scripts/lib/sitemap.mjs +34 -0
- package/scripts/lib/theme-presets.mjs +51 -23
- package/scripts/lib/theme-profiles.mjs +7 -5
- package/scripts/lib/yaml-config.mjs +6 -2
- package/scripts/sync-content-sections.mjs +116 -213
- package/scripts/sync-site-public.mjs +23 -2
- package/src/components/CodeBlockCopyScript.astro +92 -0
- package/src/components/DisplaySettings.astro +4 -4
- package/src/components/ImageCarousel.astro +7 -3
- package/src/components/NavigationPageTree.astro +44 -60
- package/src/components/PageAliasRedirect.astro +49 -0
- package/src/components/PageContentsNavigation.astro +21 -0
- package/src/components/SectionNavigationScript.astro +62 -6
- package/src/components/SiteNavigation.astro +15 -3
- package/src/components/SitePage.astro +42 -2
- package/src/components/SiteSection.astro +5 -0
- package/src/components/SiteTreeNavigation.astro +0 -1
- package/src/content.config.ts +5 -2
- package/src/layouts/BaseLayout.astro +14 -8
- package/src/lib/readerPreferencesScript.mjs +2 -2
- package/src/lib/sitePages.ts +3 -2
- package/src/pages/[...slug].astro +35 -8
- package/src/styles/content.css +494 -0
- package/src/styles/foundations.css +284 -0
- package/src/styles/global.css +6 -2510
- package/src/styles/media.css +523 -0
- package/src/styles/navigation.css +495 -0
- package/src/styles/page-layout.css +437 -0
- package/src/styles/responsive.css +525 -0
- package/starters/basic/README.md +1 -1
|
@@ -30,6 +30,11 @@ const hasPageSections = (node: SiteNavigationNode) => (
|
|
|
30
30
|
node.node.kind === 'page' && node.headings.some((heading) => heading.depth === 2)
|
|
31
31
|
);
|
|
32
32
|
const hasPageDisclosure = (node: SiteNavigationNode) => hasBranch(node) || hasPageSections(node);
|
|
33
|
+
const usesPageDisclosure = (node: SiteNavigationNode) => (
|
|
34
|
+
variant === 'sidebar'
|
|
35
|
+
? hasBranch(node)
|
|
36
|
+
: integratePageSections && hasPageDisclosure(node)
|
|
37
|
+
);
|
|
33
38
|
---
|
|
34
39
|
|
|
35
40
|
<ul class:list={['navigation-page-tree', `navigation-page-tree-${variant}`]}>
|
|
@@ -69,62 +74,52 @@ const hasPageDisclosure = (node: SiteNavigationNode) => hasBranch(node) || hasPa
|
|
|
69
74
|
/>
|
|
70
75
|
</div>
|
|
71
76
|
</details>
|
|
72
|
-
) : variant !== 'submenu' &&
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
77
|
+
) : variant !== 'submenu' && usesPageDisclosure(node) ? (
|
|
78
|
+
<details
|
|
79
|
+
class:list={['navigation-page-branch', 'navigation-page-disclosure', `navigation-page-disclosure-${variant}`]}
|
|
80
|
+
data-page-path={node.node.pagePath || '/'}
|
|
81
|
+
data-current-branch={isCurrentBranch(node.node) ? 'true' : undefined}
|
|
82
|
+
data-current-page={isCurrentPage(node.node) ? 'true' : undefined}
|
|
83
|
+
data-has-sections={hasPageSections(node) ? 'true' : undefined}
|
|
84
|
+
open={isCurrentBranch(node.node)}
|
|
85
|
+
>
|
|
86
|
+
<summary
|
|
87
|
+
class="navigation-page-summary"
|
|
88
|
+
aria-controls={getBranchId(node.node)}
|
|
81
89
|
>
|
|
82
|
-
<summary
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
>
|
|
86
|
-
<span class="navigation-page-summary-title">{node.node.title}</span>
|
|
87
|
-
<span class="navigation-page-chevron" aria-hidden="true"></span>
|
|
88
|
-
</summary>
|
|
89
|
-
<a
|
|
90
|
-
class:list={['navigation-page-open-link', { 'mobile-nav-destination': variant === 'mobile' }]}
|
|
91
|
-
href={pageHref(node.node)}
|
|
92
|
-
aria-current={isCurrentPage(node.node) ? 'page' : undefined}
|
|
93
|
-
>
|
|
94
|
-
{node.node.title}
|
|
95
|
-
</a>
|
|
96
|
-
<div id={getBranchId(node.node)} class="navigation-page-branch-content">
|
|
97
|
-
{hasPageSections(node) && (
|
|
98
|
-
<nav
|
|
99
|
-
class="navigation-page-sections"
|
|
100
|
-
aria-label={`${projectConfig.locale.labels.pageNavigation}: ${node.node.title}`}
|
|
101
|
-
>
|
|
102
|
-
<PageContentsLinks
|
|
103
|
-
headings={node.headings}
|
|
104
|
-
variant={variant === 'mobile' ? 'mobile' : 'desktop'}
|
|
105
|
-
pageHref={isCurrentPage(node.node) ? undefined : pageHref(node.node)}
|
|
106
|
-
/>
|
|
107
|
-
</nav>
|
|
108
|
-
)}
|
|
109
|
-
{hasBranch(node) && (
|
|
110
|
-
<Astro.self
|
|
111
|
-
nodes={node.children}
|
|
112
|
-
currentPage={currentPage}
|
|
113
|
-
variant={variant}
|
|
114
|
-
integratePageSections={integratePageSections}
|
|
115
|
-
/>
|
|
116
|
-
)}
|
|
117
|
-
</div>
|
|
118
|
-
</details>
|
|
119
|
-
) : (
|
|
90
|
+
<span class="navigation-page-summary-title">{node.node.title}</span>
|
|
91
|
+
<span class="navigation-page-chevron" aria-hidden="true"></span>
|
|
92
|
+
</summary>
|
|
120
93
|
<a
|
|
121
|
-
class:list={['navigation-page-link', { 'mobile-nav-destination': variant === 'mobile' }]}
|
|
94
|
+
class:list={['navigation-page-open-link', { 'mobile-nav-destination': variant === 'mobile' }]}
|
|
122
95
|
href={pageHref(node.node)}
|
|
123
96
|
aria-current={isCurrentPage(node.node) ? 'page' : undefined}
|
|
124
97
|
>
|
|
125
98
|
{node.node.title}
|
|
126
99
|
</a>
|
|
127
|
-
|
|
100
|
+
<div id={getBranchId(node.node)} class="navigation-page-branch-content">
|
|
101
|
+
{integratePageSections && hasPageSections(node) && (
|
|
102
|
+
<nav
|
|
103
|
+
class="navigation-page-sections"
|
|
104
|
+
aria-label={`${projectConfig.locale.labels.pageNavigation}: ${node.node.title}`}
|
|
105
|
+
>
|
|
106
|
+
<PageContentsLinks
|
|
107
|
+
headings={node.headings}
|
|
108
|
+
variant={variant === 'mobile' ? 'mobile' : 'desktop'}
|
|
109
|
+
pageHref={isCurrentPage(node.node) ? undefined : pageHref(node.node)}
|
|
110
|
+
/>
|
|
111
|
+
</nav>
|
|
112
|
+
)}
|
|
113
|
+
{hasBranch(node) && (
|
|
114
|
+
<Astro.self
|
|
115
|
+
nodes={node.children}
|
|
116
|
+
currentPage={currentPage}
|
|
117
|
+
variant={variant}
|
|
118
|
+
integratePageSections={integratePageSections}
|
|
119
|
+
/>
|
|
120
|
+
)}
|
|
121
|
+
</div>
|
|
122
|
+
</details>
|
|
128
123
|
) : variant !== 'submenu' ? (
|
|
129
124
|
<>
|
|
130
125
|
<a
|
|
@@ -134,17 +129,6 @@ const hasPageDisclosure = (node: SiteNavigationNode) => hasBranch(node) || hasPa
|
|
|
134
129
|
>
|
|
135
130
|
{node.node.title}
|
|
136
131
|
</a>
|
|
137
|
-
{variant === 'sidebar' && isCurrentPage(node.node) && hasPageSections(node) && (
|
|
138
|
-
<nav
|
|
139
|
-
class="navigation-page-sections"
|
|
140
|
-
aria-label={`${projectConfig.locale.labels.pageNavigation}: ${node.node.title}`}
|
|
141
|
-
>
|
|
142
|
-
<PageContentsLinks
|
|
143
|
-
headings={node.headings}
|
|
144
|
-
variant="desktop"
|
|
145
|
-
/>
|
|
146
|
-
</nav>
|
|
147
|
-
)}
|
|
148
132
|
{hasBranch(node) && (
|
|
149
133
|
<details
|
|
150
134
|
class="navigation-page-branch"
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
---
|
|
2
|
+
import projectConfig from '../../scripts/lib/project-config.mjs';
|
|
3
|
+
import { getAbsolutePageUrl } from '../../scripts/lib/site-page-urls.mjs';
|
|
4
|
+
import { withBasePath } from '../lib/basePath';
|
|
5
|
+
|
|
6
|
+
interface Props {
|
|
7
|
+
targetPathname: string;
|
|
8
|
+
targetTitle: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const { targetPathname, targetTitle } = Astro.props;
|
|
12
|
+
const { labels, lang } = projectConfig.locale;
|
|
13
|
+
const targetHref = withBasePath(projectConfig.site.basePath, targetPathname);
|
|
14
|
+
const canonicalUrl = getAbsolutePageUrl(projectConfig.site.url, targetPathname);
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
<!doctype html>
|
|
18
|
+
<html lang={lang}>
|
|
19
|
+
<head>
|
|
20
|
+
<meta charset="utf-8" />
|
|
21
|
+
<meta name="viewport" content="width=device-width" />
|
|
22
|
+
<meta name="robots" content="noindex, follow" />
|
|
23
|
+
<meta http-equiv="refresh" content={`0; url=${targetHref}`} />
|
|
24
|
+
<link rel="canonical" href={canonicalUrl} />
|
|
25
|
+
<title>{labels.pageMoved}: {targetTitle}</title>
|
|
26
|
+
<style is:inline>
|
|
27
|
+
:root {
|
|
28
|
+
color-scheme: light dark;
|
|
29
|
+
font-family: system-ui, sans-serif;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
body {
|
|
33
|
+
margin: 0;
|
|
34
|
+
padding: 2rem;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
main {
|
|
38
|
+
max-width: 42rem;
|
|
39
|
+
margin-inline: auto;
|
|
40
|
+
}
|
|
41
|
+
</style>
|
|
42
|
+
</head>
|
|
43
|
+
<body>
|
|
44
|
+
<main>
|
|
45
|
+
<h1>{labels.pageMoved}</h1>
|
|
46
|
+
<p>{labels.pageMovedText} <a href={targetHref}>{targetTitle}</a>.</p>
|
|
47
|
+
</main>
|
|
48
|
+
</body>
|
|
49
|
+
</html>
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
---
|
|
2
|
+
import projectConfig from '../../scripts/lib/project-config.mjs';
|
|
3
|
+
import type { HeadingNavigation } from '../lib/sectionContent';
|
|
4
|
+
import PageContentsLinks from './PageContentsLinks.astro';
|
|
5
|
+
|
|
6
|
+
interface Props {
|
|
7
|
+
headings: HeadingNavigation[];
|
|
8
|
+
pageTitle: string;
|
|
9
|
+
variant: 'inline' | 'rail';
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const { headings, pageTitle, variant } = Astro.props;
|
|
13
|
+
const label = `${projectConfig.locale.labels.pageNavigation}: ${pageTitle}`;
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
<aside class={`page-contents-navigation page-contents-navigation-${variant}`}>
|
|
17
|
+
<nav aria-label={label}>
|
|
18
|
+
<p class="page-contents-navigation-label">{projectConfig.locale.labels.pageNavigation}</p>
|
|
19
|
+
<PageContentsLinks headings={headings} variant="desktop" />
|
|
20
|
+
</nav>
|
|
21
|
+
</aside>
|
|
@@ -23,6 +23,36 @@
|
|
|
23
23
|
.filter((heading) => trackedHeadingIds.has(heading.id))
|
|
24
24
|
: [];
|
|
25
25
|
let sectionTrackingFrame: number | undefined;
|
|
26
|
+
let sectionTrackingPaused = false;
|
|
27
|
+
let sectionTrackingResumeTimer: number | undefined;
|
|
28
|
+
|
|
29
|
+
const resumeSectionTracking = () => {
|
|
30
|
+
window.clearTimeout(sectionTrackingResumeTimer);
|
|
31
|
+
window.removeEventListener('scrollend', resumeSectionTracking);
|
|
32
|
+
sectionTrackingPaused = false;
|
|
33
|
+
sectionTrackingResumeTimer = undefined;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const pauseSectionTrackingForAnchorNavigation = () => {
|
|
37
|
+
if (!sectionTrackingEnabled) return;
|
|
38
|
+
|
|
39
|
+
sectionTrackingPaused = true;
|
|
40
|
+
if (sectionTrackingFrame !== undefined) {
|
|
41
|
+
window.cancelAnimationFrame(sectionTrackingFrame);
|
|
42
|
+
sectionTrackingFrame = undefined;
|
|
43
|
+
}
|
|
44
|
+
window.clearTimeout(sectionTrackingResumeTimer);
|
|
45
|
+
window.removeEventListener('scrollend', resumeSectionTracking);
|
|
46
|
+
if ('onscrollend' in document.documentElement) {
|
|
47
|
+
window.addEventListener('scrollend', resumeSectionTracking, { once: true });
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const scrollBehavior = getComputedStyle(document.documentElement).scrollBehavior;
|
|
51
|
+
sectionTrackingResumeTimer = window.setTimeout(
|
|
52
|
+
resumeSectionTracking,
|
|
53
|
+
scrollBehavior === 'smooth' ? 2000 : 120,
|
|
54
|
+
);
|
|
55
|
+
};
|
|
26
56
|
|
|
27
57
|
const setAnchorOffset = () => {
|
|
28
58
|
if (!siteTop) return;
|
|
@@ -73,12 +103,35 @@
|
|
|
73
103
|
|
|
74
104
|
const updateReadingPosition = () => {
|
|
75
105
|
sectionTrackingFrame = undefined;
|
|
76
|
-
|
|
77
|
-
|
|
106
|
+
if (trackedHeadings.length === 0) return;
|
|
107
|
+
|
|
108
|
+
let currentHeadingId: string | undefined = trackedHeadings[0]?.id;
|
|
109
|
+
|
|
110
|
+
if (window.scrollY > 1) {
|
|
111
|
+
const viewportHeight = window.innerHeight;
|
|
112
|
+
const documentHeight = Math.max(
|
|
113
|
+
document.body.scrollHeight,
|
|
114
|
+
document.body.offsetHeight,
|
|
115
|
+
document.documentElement.clientHeight,
|
|
116
|
+
document.documentElement.scrollHeight,
|
|
117
|
+
document.documentElement.offsetHeight,
|
|
118
|
+
);
|
|
119
|
+
const maximumScroll = Math.max(0, documentHeight - viewportHeight);
|
|
120
|
+
const remainingScroll = Math.max(0, maximumScroll - window.scrollY);
|
|
121
|
+
const normalReadingLine = Math.max(
|
|
122
|
+
Math.ceil(siteTop?.getBoundingClientRect().bottom ?? 0) + 8,
|
|
123
|
+
Math.round(viewportHeight / 2),
|
|
124
|
+
);
|
|
125
|
+
const terminalReadingLine = viewportHeight - remainingScroll;
|
|
126
|
+
const readingLine = Math.min(
|
|
127
|
+
viewportHeight,
|
|
128
|
+
Math.max(normalReadingLine, terminalReadingLine),
|
|
129
|
+
);
|
|
78
130
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
131
|
+
for (const heading of trackedHeadings) {
|
|
132
|
+
if (heading.getBoundingClientRect().top > readingLine) break;
|
|
133
|
+
currentHeadingId = heading.id;
|
|
134
|
+
}
|
|
82
135
|
}
|
|
83
136
|
|
|
84
137
|
sectionLinks.forEach((link) => {
|
|
@@ -94,7 +147,7 @@
|
|
|
94
147
|
};
|
|
95
148
|
|
|
96
149
|
const scheduleReadingPositionUpdate = () => {
|
|
97
|
-
if (!sectionTrackingEnabled || sectionTrackingFrame !== undefined) return;
|
|
150
|
+
if (!sectionTrackingEnabled || sectionTrackingPaused || sectionTrackingFrame !== undefined) return;
|
|
98
151
|
sectionTrackingFrame = window.requestAnimationFrame(updateReadingPosition);
|
|
99
152
|
};
|
|
100
153
|
|
|
@@ -154,6 +207,7 @@
|
|
|
154
207
|
const target = getSamePageTarget(link);
|
|
155
208
|
if (!target) return;
|
|
156
209
|
|
|
210
|
+
pauseSectionTrackingForAnchorNavigation();
|
|
157
211
|
if (mobileMenu) mobileMenu.open = false;
|
|
158
212
|
link.closest<HTMLElement>('.site-nav-item')?.setAttribute('data-submenu-dismissed', 'true');
|
|
159
213
|
|
|
@@ -172,9 +226,11 @@
|
|
|
172
226
|
});
|
|
173
227
|
|
|
174
228
|
window.addEventListener('hashchange', () => {
|
|
229
|
+
pauseSectionTrackingForAnchorNavigation();
|
|
175
230
|
updateCurrentSection();
|
|
176
231
|
scheduleReadingPositionUpdate();
|
|
177
232
|
});
|
|
233
|
+
if (window.location.hash) pauseSectionTrackingForAnchorNavigation();
|
|
178
234
|
updateCurrentSection();
|
|
179
235
|
if (sectionTrackingEnabled) {
|
|
180
236
|
window.addEventListener('scroll', scheduleReadingPositionUpdate, { passive: true });
|
|
@@ -27,6 +27,8 @@ interface Props {
|
|
|
27
27
|
currentPage: SitePage;
|
|
28
28
|
navigationEntries: SiteNavigationEntry[];
|
|
29
29
|
navigationMode: 'sections' | 'top' | 'tree';
|
|
30
|
+
showTopNavigationSubmenus: boolean;
|
|
31
|
+
hasNavigationRailFrame: boolean;
|
|
30
32
|
frameColors: FrameColors;
|
|
31
33
|
readerPreferences: {
|
|
32
34
|
controls: {
|
|
@@ -42,7 +44,16 @@ interface Props {
|
|
|
42
44
|
};
|
|
43
45
|
}
|
|
44
46
|
|
|
45
|
-
const {
|
|
47
|
+
const {
|
|
48
|
+
logo,
|
|
49
|
+
currentPage,
|
|
50
|
+
navigationEntries,
|
|
51
|
+
navigationMode,
|
|
52
|
+
showTopNavigationSubmenus,
|
|
53
|
+
hasNavigationRailFrame,
|
|
54
|
+
frameColors,
|
|
55
|
+
readerPreferences,
|
|
56
|
+
} = Astro.props;
|
|
46
57
|
const siteTopBackgroundColor = frameColors.backgroundColor === 'transparent'
|
|
47
58
|
? 'var(--color-page)'
|
|
48
59
|
: frameColors.backgroundColor;
|
|
@@ -94,6 +105,7 @@ const needsNavigationScript = hasCurrentPageSectionMenu || hasTreePageContents |
|
|
|
94
105
|
class:list={['site-top', {
|
|
95
106
|
'site-top-has-site-nav': showSiteNavigation,
|
|
96
107
|
'site-top-has-navigation-row': showDesktopNavigationRow,
|
|
108
|
+
'site-top-has-navigation-rail-frame': hasNavigationRailFrame,
|
|
97
109
|
}]}
|
|
98
110
|
data-navigation-mode={navigationMode}
|
|
99
111
|
style={navigationStyle}
|
|
@@ -118,13 +130,13 @@ const needsNavigationScript = hasCurrentPageSectionMenu || hasTreePageContents |
|
|
|
118
130
|
const nodeIsCurrentPage = navigationNode.node.kind === 'page' && isCurrentPage(navigationNode.node);
|
|
119
131
|
return (
|
|
120
132
|
<li class:list={['site-nav-item', {
|
|
121
|
-
'site-nav-item-has-submenu':
|
|
133
|
+
'site-nav-item-has-submenu': showTopNavigationSubmenus && navigationNode.children.length > 0,
|
|
122
134
|
'site-nav-item-current-branch': !nodeIsCurrentPage && isCurrentBranch(navigationNode.node),
|
|
123
135
|
}]}>
|
|
124
136
|
<a href={pageHref(destination)} aria-current={nodeIsCurrentPage ? 'page' : undefined}>
|
|
125
137
|
{navigationNode.node.title}
|
|
126
138
|
</a>
|
|
127
|
-
{
|
|
139
|
+
{showTopNavigationSubmenus && navigationNode.children.length > 0 && (
|
|
128
140
|
<div class="site-nav-submenu">
|
|
129
141
|
<nav aria-label={`${navigationNode.node.title} ${projectConfig.locale.labels.siteNavigation}`}>
|
|
130
142
|
<NavigationPageTree nodes={[navigationNode]} currentPage={currentPage} variant="submenu" />
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
---
|
|
2
2
|
import { getCollection, type CollectionEntry } from 'astro:content';
|
|
3
3
|
import BaseLayout from '../layouts/BaseLayout.astro';
|
|
4
|
+
import CodeBlockCopyScript from './CodeBlockCopyScript.astro';
|
|
4
5
|
import SiteBreadcrumbs from './SiteBreadcrumbs.astro';
|
|
5
6
|
import SiteNavigation from './SiteNavigation.astro';
|
|
6
7
|
import SiteBanners from './SiteBanners.astro';
|
|
8
|
+
import PageContentsNavigation from './PageContentsNavigation.astro';
|
|
7
9
|
import SiteSection from './SiteSection.astro';
|
|
8
10
|
import SiteTreeNavigation from './SiteTreeNavigation.astro';
|
|
9
11
|
import TreeNavigationScript from './TreeNavigationScript.astro';
|
|
@@ -55,6 +57,9 @@ const logo = logoAsset
|
|
|
55
57
|
: undefined;
|
|
56
58
|
const siteHtml = entry.rendered?.html ?? '';
|
|
57
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
|
+
)));
|
|
58
63
|
const today = getToday();
|
|
59
64
|
const siteBanners = (sitewideData.banners ?? []).filter((banner) => isVisibleToday(banner.visible, today));
|
|
60
65
|
const hasPrioritizableImage = (section: ResolvedSection) => section.contentBlocks.some((block) => (
|
|
@@ -96,7 +101,12 @@ const navigationModel = resolveNavigationModel({
|
|
|
96
101
|
isHome: node.isHome,
|
|
97
102
|
kind: node.kind,
|
|
98
103
|
listed: node.navigation.listed,
|
|
104
|
+
pagePath: node.pagePath,
|
|
99
105
|
})),
|
|
106
|
+
currentPage: {
|
|
107
|
+
isHome: currentPage.isHome,
|
|
108
|
+
pagePath: currentPage.pagePath,
|
|
109
|
+
},
|
|
100
110
|
});
|
|
101
111
|
const pagePresentation = resolvePagePresentation(themeData, themeLabel, {
|
|
102
112
|
navigationMode: navigationModel.mode,
|
|
@@ -104,6 +114,16 @@ const pagePresentation = resolvePagePresentation(themeData, themeLabel, {
|
|
|
104
114
|
const pageTextWidth = getTextWidthCssValue(pagePresentation.typography.values.body.width);
|
|
105
115
|
const frameColors = resolveFrameColors({ theme: siteThemeData, sourceLabel: `${siteDirectory || 'site'}/${siteTheme.id}` });
|
|
106
116
|
const hasTreeNavigation = navigationModel.mode === 'tree';
|
|
117
|
+
const currentPageNavigation = siteNavigation.find(({ node }) => (
|
|
118
|
+
node.kind === 'page' && node.pathname === currentPage.pathname
|
|
119
|
+
));
|
|
120
|
+
const pageContentsHeadings = currentPageNavigation?.headings.filter(({ depth }) => depth === 2 || depth === 3) ?? [];
|
|
121
|
+
const hasContentsNavigation = hasTreeNavigation && pageContentsHeadings.length > 1;
|
|
122
|
+
const hasNavigationRailFrame = navigationModel.requestedMode === 'tree'
|
|
123
|
+
|| (
|
|
124
|
+
navigationModel.requestedMode === 'automatic'
|
|
125
|
+
&& (navigationModel.hasNestedPages || navigationModel.hasCategories)
|
|
126
|
+
);
|
|
107
127
|
const showBreadcrumbs = !currentPage.isHome && currentPage.parentPagePath !== null;
|
|
108
128
|
const reserveBreadcrumbSpace = hasTreeNavigation || showBreadcrumbs;
|
|
109
129
|
|
|
@@ -117,19 +137,24 @@ const reserveBreadcrumbSpace = hasTreeNavigation || showBreadcrumbs;
|
|
|
117
137
|
frameColors={frameColors}
|
|
118
138
|
pathname={currentPage.pathname}
|
|
119
139
|
footer={sitewideData.footer}
|
|
120
|
-
|
|
140
|
+
contentsNavigation={hasContentsNavigation}
|
|
121
141
|
>
|
|
122
142
|
<SiteNavigation
|
|
123
143
|
logo={logo}
|
|
124
144
|
currentPage={currentPage}
|
|
125
145
|
navigationEntries={siteNavigation}
|
|
126
146
|
navigationMode={navigationModel.mode}
|
|
147
|
+
showTopNavigationSubmenus={navigationModel.requestedMode !== 'automatic' && navigationModel.mode !== 'tree'}
|
|
148
|
+
hasNavigationRailFrame={hasNavigationRailFrame}
|
|
127
149
|
frameColors={frameColors}
|
|
128
150
|
readerPreferences={pagePresentation.readerPreferences}
|
|
129
151
|
/>
|
|
130
152
|
{siteBanners.length > 0 && <SiteBanners banners={siteBanners} />}
|
|
131
153
|
|
|
132
|
-
<div class:list={['site-page-layout', {
|
|
154
|
+
<div class:list={['site-page-layout', {
|
|
155
|
+
'site-page-layout-tree': hasTreeNavigation,
|
|
156
|
+
'site-page-layout-contents': hasContentsNavigation,
|
|
157
|
+
}]}>
|
|
133
158
|
{hasTreeNavigation && (
|
|
134
159
|
<SiteTreeNavigation currentPage={currentPage} navigationEntries={siteNavigation} />
|
|
135
160
|
)}
|
|
@@ -141,6 +166,13 @@ const reserveBreadcrumbSpace = hasTreeNavigation || showBreadcrumbs;
|
|
|
141
166
|
{showBreadcrumbs
|
|
142
167
|
? <SiteBreadcrumbs currentPage={currentPage} nodes={siteNodes} />
|
|
143
168
|
: 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
|
+
)}
|
|
144
176
|
{renderedSections.map(({ section, sectionIndex, prioritizeFirstImage }) => (
|
|
145
177
|
<SiteSection
|
|
146
178
|
section={section}
|
|
@@ -151,6 +183,14 @@ const reserveBreadcrumbSpace = hasTreeNavigation || showBreadcrumbs;
|
|
|
151
183
|
/>
|
|
152
184
|
))}
|
|
153
185
|
</main>
|
|
186
|
+
{hasContentsNavigation && (
|
|
187
|
+
<PageContentsNavigation
|
|
188
|
+
headings={pageContentsHeadings}
|
|
189
|
+
pageTitle={currentPage.title}
|
|
190
|
+
variant="rail"
|
|
191
|
+
/>
|
|
192
|
+
)}
|
|
154
193
|
</div>
|
|
155
194
|
{hasTreeNavigation && <TreeNavigationScript />}
|
|
195
|
+
{hasCodeBlocks && <CodeBlockCopyScript />}
|
|
156
196
|
</BaseLayout>
|
|
@@ -33,10 +33,15 @@ type ResolvedPagePresentation = {
|
|
|
33
33
|
};
|
|
34
34
|
type VisualTheme = {
|
|
35
35
|
images: {
|
|
36
|
+
presentation: 'prose-aligned' | 'centered-fit';
|
|
36
37
|
maxAvailableWidthPercent: {
|
|
37
38
|
desktop: number;
|
|
38
39
|
mobile: number;
|
|
39
40
|
};
|
|
41
|
+
maxAvailableHeightPercent?: {
|
|
42
|
+
desktop: number;
|
|
43
|
+
mobile: number;
|
|
44
|
+
};
|
|
40
45
|
};
|
|
41
46
|
};
|
|
42
47
|
|
package/src/content.config.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { defineCollection } from 'astro:content';
|
|
2
2
|
import { glob } from 'astro/loaders';
|
|
3
|
+
import { z } from 'astro/zod';
|
|
3
4
|
import { basename } from 'node:path';
|
|
4
5
|
import { pathToFileURL } from 'node:url';
|
|
5
6
|
import {
|
|
@@ -19,7 +20,9 @@ const siteEntryId = `${siteDirLabel
|
|
|
19
20
|
.replace(/^[./\\]+/, '')
|
|
20
21
|
.replace(/[^a-zA-Z0-9-]+/g, '-')
|
|
21
22
|
.replace(/^-+|-+$/g, '') || 'site'}-content`;
|
|
22
|
-
const
|
|
23
|
+
const emptyYamlMapping = (value: unknown) => value ?? {};
|
|
24
|
+
const siteThemeSchema = z.preprocess(emptyYamlMapping, themeVisualSchema);
|
|
25
|
+
const sitewideContentSchema = z.preprocess(emptyYamlMapping, sitewideSchema);
|
|
23
26
|
|
|
24
27
|
const site = defineCollection({
|
|
25
28
|
loader: glob({
|
|
@@ -52,7 +55,7 @@ const sitewide = defineCollection({
|
|
|
52
55
|
base: pathToFileURL(siteDir),
|
|
53
56
|
generateId: () => `${siteEntryId.replace(/-content$/, '')}-sitewide`,
|
|
54
57
|
}),
|
|
55
|
-
schema:
|
|
58
|
+
schema: sitewideContentSchema,
|
|
56
59
|
});
|
|
57
60
|
|
|
58
61
|
export const collections = { site, theme, sitewide };
|
|
@@ -19,7 +19,7 @@ type Presentation = {
|
|
|
19
19
|
page: FrameColors;
|
|
20
20
|
css: Record<string, string>;
|
|
21
21
|
}>;
|
|
22
|
-
|
|
22
|
+
appearance: {
|
|
23
23
|
default: 'system' | 'light' | 'dark';
|
|
24
24
|
};
|
|
25
25
|
readerPreferences: {
|
|
@@ -52,9 +52,10 @@ type VisualTheme = {
|
|
|
52
52
|
};
|
|
53
53
|
};
|
|
54
54
|
images: {
|
|
55
|
+
presentation: 'prose-aligned' | 'centered-fit';
|
|
55
56
|
width: string;
|
|
56
57
|
maxAvailableWidthPercent: { desktop: number; mobile: number };
|
|
57
|
-
maxAvailableHeightPercent
|
|
58
|
+
maxAvailableHeightPercent?: { desktop: number; mobile: number };
|
|
58
59
|
};
|
|
59
60
|
typography: { fontFamily: string };
|
|
60
61
|
corners: {
|
|
@@ -75,7 +76,7 @@ interface Props {
|
|
|
75
76
|
copyrightMessage?: string;
|
|
76
77
|
buildInfo?: boolean;
|
|
77
78
|
};
|
|
78
|
-
|
|
79
|
+
contentsNavigation?: boolean;
|
|
79
80
|
}
|
|
80
81
|
|
|
81
82
|
const {
|
|
@@ -86,7 +87,7 @@ const {
|
|
|
86
87
|
frameColors,
|
|
87
88
|
pathname = '/',
|
|
88
89
|
footer,
|
|
89
|
-
|
|
90
|
+
contentsNavigation = false,
|
|
90
91
|
} = Astro.props;
|
|
91
92
|
|
|
92
93
|
const documentTitle = title;
|
|
@@ -124,8 +125,12 @@ const documentStyle = [
|
|
|
124
125
|
`--image-width: ${visualTheme.images.width}`,
|
|
125
126
|
`--image-max-available-width-desktop: ${formatPercent(visualTheme.images.maxAvailableWidthPercent.desktop)}`,
|
|
126
127
|
`--image-max-available-width-mobile: ${formatPercent(visualTheme.images.maxAvailableWidthPercent.mobile)}`,
|
|
127
|
-
|
|
128
|
-
|
|
128
|
+
...(visualTheme.images.maxAvailableHeightPercent
|
|
129
|
+
? [
|
|
130
|
+
`--image-max-height-desktop: ${formatViewportHeightPercent(visualTheme.images.maxAvailableHeightPercent.desktop)}`,
|
|
131
|
+
`--image-max-height-mobile: ${formatViewportHeightPercent(visualTheme.images.maxAvailableHeightPercent.mobile)}`,
|
|
132
|
+
]
|
|
133
|
+
: []),
|
|
129
134
|
].join('; ');
|
|
130
135
|
const { buildInfo, copyrightMessage } = footer ?? {};
|
|
131
136
|
const showBuildInfo = buildInfo === true;
|
|
@@ -154,10 +159,11 @@ const readerPreferencesScript = readerPreferencesEnabled
|
|
|
154
159
|
<!doctype html>
|
|
155
160
|
<html
|
|
156
161
|
lang={projectConfig.locale.lang}
|
|
157
|
-
data-
|
|
162
|
+
data-appearance={presentation.readerPreferences.defaults.appearance}
|
|
163
|
+
data-image-presentation={visualTheme.images.presentation}
|
|
158
164
|
data-reading-width={presentation.readerPreferences.defaults.readingWidth}
|
|
159
165
|
data-focus-reading={presentation.readerPreferences.defaults.focusReading}
|
|
160
|
-
data-section-tracking={
|
|
166
|
+
data-section-tracking={contentsNavigation ? 'enabled' : undefined}
|
|
161
167
|
style={documentStyle}
|
|
162
168
|
>
|
|
163
169
|
<head>
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
const preferenceDefinitions = Object.freeze({
|
|
2
2
|
appearance: Object.freeze({
|
|
3
3
|
allowed: Object.freeze(['system', 'light', 'dark']),
|
|
4
|
-
cookieName: 'norna-
|
|
4
|
+
cookieName: 'norna-appearance',
|
|
5
5
|
}),
|
|
6
6
|
readingWidth: Object.freeze({
|
|
7
7
|
allowed: Object.freeze(['narrow', 'standard', 'wide']),
|
|
@@ -72,7 +72,7 @@ export const getReaderPreferencesScript = ({ controls, defaults, cookiePath }) =
|
|
|
72
72
|
const apply = (name, value, persist = false) => {
|
|
73
73
|
const nextValue = isAllowed(name, value) ? value : defaults[name];
|
|
74
74
|
values[name] = nextValue;
|
|
75
|
-
if (name === 'appearance') root.dataset.
|
|
75
|
+
if (name === 'appearance') root.dataset.appearance = nextValue;
|
|
76
76
|
if (name === 'readingWidth') root.dataset.readingWidth = nextValue;
|
|
77
77
|
if (name === 'focusReading') root.dataset.focusReading = nextValue;
|
|
78
78
|
if (persist) writeCookie(definitions[name].cookieName, nextValue);
|
package/src/lib/sitePages.ts
CHANGED
|
@@ -10,6 +10,7 @@ import { sitePagesDir, sitePagesLabel } from '../../scripts/lib/site-paths.mjs';
|
|
|
10
10
|
type SiteEntry = CollectionEntry<'site'>;
|
|
11
11
|
|
|
12
12
|
type SiteNodeBase = {
|
|
13
|
+
aliases: string[];
|
|
13
14
|
kind: 'category' | 'page';
|
|
14
15
|
isHome: boolean;
|
|
15
16
|
navigation: {
|
|
@@ -106,6 +107,7 @@ const createSitePage = async (entry: SiteEntry): Promise<SitePage> => {
|
|
|
106
107
|
const navigation = entry.data.navigation ?? {};
|
|
107
108
|
|
|
108
109
|
return {
|
|
110
|
+
aliases: entry.data.page?.aliases ?? [],
|
|
109
111
|
kind: 'page',
|
|
110
112
|
entry,
|
|
111
113
|
isHome,
|
|
@@ -131,6 +133,7 @@ const createSitePage = async (entry: SiteEntry): Promise<SitePage> => {
|
|
|
131
133
|
};
|
|
132
134
|
|
|
133
135
|
const createSiteCategory = (category: Awaited<ReturnType<typeof getSiteStructure>>['categories'][number]): SiteCategory => ({
|
|
136
|
+
aliases: [],
|
|
134
137
|
kind: 'category',
|
|
135
138
|
isHome: false,
|
|
136
139
|
depth: category.depth,
|
|
@@ -226,5 +229,3 @@ export const getSiteModel = async (entries: SiteEntry[]) => {
|
|
|
226
229
|
warnings: structure.warnings,
|
|
227
230
|
};
|
|
228
231
|
};
|
|
229
|
-
|
|
230
|
-
export const getSitePages = async (entries: SiteEntry[]) => (await getSiteModel(entries)).pages;
|