@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
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import { getSiteNodePathname } from './site-page-urls.mjs';
|
|
2
|
+
import { sitemapFilename } from './sitemap.mjs';
|
|
3
|
+
|
|
4
|
+
export const generatedSiteRoutes = Object.freeze([
|
|
5
|
+
Object.freeze({
|
|
6
|
+
kind: 'generated-route',
|
|
7
|
+
label: `Norna generated ${sitemapFilename}`,
|
|
8
|
+
pathname: `/${sitemapFilename}`,
|
|
9
|
+
}),
|
|
10
|
+
]);
|
|
11
|
+
|
|
12
|
+
const getPagePathname = (page) => page.pathname ?? getSiteNodePathname(page.contentFile ?? page);
|
|
13
|
+
const getPageLabel = (page) => page.contentLabel ?? page.contentFile?.contentLabel ?? page.entry?.id ?? getPagePathname(page);
|
|
14
|
+
const getPageAliases = (page) => page.aliases ?? page.data?.page?.aliases ?? [];
|
|
15
|
+
|
|
16
|
+
const createIdentity = ({ kind, label, pathname, source }) => ({
|
|
17
|
+
kind,
|
|
18
|
+
label,
|
|
19
|
+
pathname,
|
|
20
|
+
source,
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
const getPublicFileIdentities = (file) => {
|
|
24
|
+
const identities = [createIdentity({
|
|
25
|
+
kind: 'public-file',
|
|
26
|
+
label: file.label,
|
|
27
|
+
pathname: file.pathname,
|
|
28
|
+
source: file,
|
|
29
|
+
})];
|
|
30
|
+
|
|
31
|
+
if (file.pathname === '/index.html') {
|
|
32
|
+
identities.push(createIdentity({
|
|
33
|
+
kind: 'public-file',
|
|
34
|
+
label: file.label,
|
|
35
|
+
pathname: '/',
|
|
36
|
+
source: file,
|
|
37
|
+
}));
|
|
38
|
+
} else if (file.pathname.endsWith('/index.html')) {
|
|
39
|
+
identities.push(createIdentity({
|
|
40
|
+
kind: 'public-file',
|
|
41
|
+
label: file.label,
|
|
42
|
+
pathname: file.pathname.slice(0, -'index.html'.length),
|
|
43
|
+
source: file,
|
|
44
|
+
}));
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return identities;
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const describeIdentity = (identity) => {
|
|
51
|
+
if (identity.kind === 'page') return `page URL from ${identity.label}`;
|
|
52
|
+
if (identity.kind === 'category') return `navigation category path from ${identity.label}`;
|
|
53
|
+
if (identity.kind === 'public-file') return `public file ${identity.label}`;
|
|
54
|
+
if (identity.kind === 'generated-route') return `generated route ${identity.label}`;
|
|
55
|
+
if (identity.kind === 'page-alias') return `page alias declared in ${identity.label}`;
|
|
56
|
+
return `${identity.kind} from ${identity.label}`;
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
const compareAliases = (left, right) => (
|
|
60
|
+
left.pathname.localeCompare(right.pathname, 'en')
|
|
61
|
+
|| left.targetPathname.localeCompare(right.targetPathname, 'en')
|
|
62
|
+
|| left.contentLabel.localeCompare(right.contentLabel, 'en')
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
export const createPageAliasModel = ({
|
|
66
|
+
pages,
|
|
67
|
+
categories = [],
|
|
68
|
+
publicFiles = [],
|
|
69
|
+
generatedRoutes = generatedSiteRoutes,
|
|
70
|
+
}) => {
|
|
71
|
+
const identitiesByPathname = new Map();
|
|
72
|
+
const aliases = [];
|
|
73
|
+
const aliasesByPathname = new Map();
|
|
74
|
+
const diagnostics = [];
|
|
75
|
+
|
|
76
|
+
const addIdentity = (identity) => {
|
|
77
|
+
if (!identitiesByPathname.has(identity.pathname)) {
|
|
78
|
+
identitiesByPathname.set(identity.pathname, identity);
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
for (const page of pages) {
|
|
83
|
+
addIdentity(createIdentity({
|
|
84
|
+
kind: 'page',
|
|
85
|
+
label: getPageLabel(page),
|
|
86
|
+
pathname: getPagePathname(page),
|
|
87
|
+
source: page,
|
|
88
|
+
}));
|
|
89
|
+
}
|
|
90
|
+
for (const category of categories) {
|
|
91
|
+
addIdentity(createIdentity({
|
|
92
|
+
kind: 'category',
|
|
93
|
+
label: category.categorySourceLabel ?? category.nodeLabel ?? getSiteNodePathname(category),
|
|
94
|
+
pathname: getSiteNodePathname(category),
|
|
95
|
+
source: category,
|
|
96
|
+
}));
|
|
97
|
+
}
|
|
98
|
+
for (const file of publicFiles) {
|
|
99
|
+
for (const identity of getPublicFileIdentities(file)) addIdentity(identity);
|
|
100
|
+
}
|
|
101
|
+
for (const route of generatedRoutes) {
|
|
102
|
+
addIdentity(createIdentity({ ...route, source: route }));
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
for (const page of pages) {
|
|
106
|
+
const contentFile = page.contentFile ?? page;
|
|
107
|
+
const contentLabel = getPageLabel(page);
|
|
108
|
+
const targetPathname = getPagePathname(page);
|
|
109
|
+
|
|
110
|
+
for (const pathname of getPageAliases(page)) {
|
|
111
|
+
const alias = {
|
|
112
|
+
contentFile,
|
|
113
|
+
contentLabel,
|
|
114
|
+
page,
|
|
115
|
+
pathname,
|
|
116
|
+
targetPathname,
|
|
117
|
+
};
|
|
118
|
+
const conflict = identitiesByPathname.get(pathname);
|
|
119
|
+
if (conflict) {
|
|
120
|
+
diagnostics.push({
|
|
121
|
+
alias,
|
|
122
|
+
code: 'page-alias-collision',
|
|
123
|
+
contentFile,
|
|
124
|
+
fix: conflict.kind === 'page-alias'
|
|
125
|
+
? 'Keep the old URL on exactly one current page.'
|
|
126
|
+
: 'Remove or change the alias. A public URL can identify only one site resource.',
|
|
127
|
+
message: `Page alias "${pathname}" in ${contentLabel} conflicts with ${describeIdentity(conflict)}.`,
|
|
128
|
+
severity: 'error',
|
|
129
|
+
});
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
aliases.push(alias);
|
|
134
|
+
aliasesByPathname.set(pathname, alias);
|
|
135
|
+
addIdentity(createIdentity({
|
|
136
|
+
kind: 'page-alias',
|
|
137
|
+
label: contentLabel,
|
|
138
|
+
pathname,
|
|
139
|
+
source: alias,
|
|
140
|
+
}));
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
aliases.sort(compareAliases);
|
|
145
|
+
|
|
146
|
+
return {
|
|
147
|
+
aliases,
|
|
148
|
+
aliasesByPathname,
|
|
149
|
+
diagnostics,
|
|
150
|
+
identitiesByPathname,
|
|
151
|
+
};
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
export const assertPageAliasModel = (model) => {
|
|
155
|
+
if (model.diagnostics.length === 0) return model;
|
|
156
|
+
|
|
157
|
+
throw new Error([
|
|
158
|
+
'Page alias validation failed.',
|
|
159
|
+
...model.diagnostics.flatMap((diagnostic) => [
|
|
160
|
+
`- ${diagnostic.message}`,
|
|
161
|
+
` Fix: ${diagnostic.fix}`,
|
|
162
|
+
]),
|
|
163
|
+
].join('\n'));
|
|
164
|
+
};
|
|
@@ -2,6 +2,10 @@ import {
|
|
|
2
2
|
getHeadingIdentifierIssues,
|
|
3
3
|
getMarkdownHeadings,
|
|
4
4
|
} from './heading-ids.mjs';
|
|
5
|
+
import {
|
|
6
|
+
extractMarkdownLinks,
|
|
7
|
+
extractNornaBlockLinks,
|
|
8
|
+
} from './markdown-links.mjs';
|
|
5
9
|
import {
|
|
6
10
|
extractInlineNoteDiagnostics,
|
|
7
11
|
extractMarkdownImageReferences,
|
|
@@ -9,7 +13,28 @@ import {
|
|
|
9
13
|
getNornaBlockImageReferences,
|
|
10
14
|
} from './norna-markdown-blocks.mjs';
|
|
11
15
|
|
|
12
|
-
const
|
|
16
|
+
const normalizeMarkdownWithOffsets = (source) => {
|
|
17
|
+
const input = String(source);
|
|
18
|
+
let normalized = '';
|
|
19
|
+
const originalOffsets = [0];
|
|
20
|
+
|
|
21
|
+
for (let index = 0; index < input.length;) {
|
|
22
|
+
if (input[index] === '\r') {
|
|
23
|
+
index += input[index + 1] === '\n' ? 2 : 1;
|
|
24
|
+
normalized += '\n';
|
|
25
|
+
originalOffsets.push(index);
|
|
26
|
+
continue;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
normalized += input[index];
|
|
30
|
+
index += 1;
|
|
31
|
+
originalOffsets.push(index);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return { normalized, originalOffsets };
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const normalizeMarkdown = (source) => normalizeMarkdownWithOffsets(source).normalized;
|
|
13
38
|
|
|
14
39
|
const hasContent = (source) => source.trim().length > 0;
|
|
15
40
|
|
|
@@ -194,7 +219,7 @@ export const parsePageMarkdown = async (markdown, options = {}) => {
|
|
|
194
219
|
const source = normalizeMarkdown(markdown);
|
|
195
220
|
const label = options.label ?? 'Markdown';
|
|
196
221
|
const lineOffset = options.lineOffset ?? 0;
|
|
197
|
-
const { headings } = await getMarkdownHeadings(source);
|
|
222
|
+
const { headings, tree } = await getMarkdownHeadings(source);
|
|
198
223
|
const structuralHeadings = headings.filter((heading) => heading.depth <= 2);
|
|
199
224
|
const prelude = structuralHeadings.length > 0
|
|
200
225
|
? source.slice(0, structuralHeadings[0].index)
|
|
@@ -225,6 +250,14 @@ export const parsePageMarkdown = async (markdown, options = {}) => {
|
|
|
225
250
|
regionId: region.id,
|
|
226
251
|
severity: 'error',
|
|
227
252
|
})));
|
|
253
|
+
const blocks = regions.flatMap((region) => region.blocks);
|
|
254
|
+
const links = [
|
|
255
|
+
...extractMarkdownLinks({ source, tree, lineOffset }),
|
|
256
|
+
...extractNornaBlockLinks({ source, blocks, lineOffset }),
|
|
257
|
+
].sort((left, right) => (
|
|
258
|
+
left.line - right.line
|
|
259
|
+
|| (left.range?.start ?? 0) - (right.range?.start ?? 0)
|
|
260
|
+
));
|
|
228
261
|
const navigationHeadings = [];
|
|
229
262
|
let parentId = null;
|
|
230
263
|
for (const heading of headings) {
|
|
@@ -240,7 +273,7 @@ export const parsePageMarkdown = async (markdown, options = {}) => {
|
|
|
240
273
|
}
|
|
241
274
|
|
|
242
275
|
return {
|
|
243
|
-
blocks
|
|
276
|
+
blocks,
|
|
244
277
|
diagnostics: [
|
|
245
278
|
...structureDiagnostics,
|
|
246
279
|
...headingDiagnostics,
|
|
@@ -252,6 +285,7 @@ export const parsePageMarkdown = async (markdown, options = {}) => {
|
|
|
252
285
|
intro: regions.find((region) => region.kind === 'page-intro') ?? null,
|
|
253
286
|
label,
|
|
254
287
|
lineOffset,
|
|
288
|
+
links,
|
|
255
289
|
managedImages: regions.flatMap((region) => region.managedImages),
|
|
256
290
|
markdownImages: regions.flatMap((region) => region.markdownImages),
|
|
257
291
|
navigationHeadings,
|
|
@@ -266,17 +300,33 @@ export const parsePageMarkdown = async (markdown, options = {}) => {
|
|
|
266
300
|
};
|
|
267
301
|
|
|
268
302
|
export const parsePageMarkdownSource = async (source, options = {}) => {
|
|
269
|
-
const
|
|
303
|
+
const normalization = normalizeMarkdownWithOffsets(source);
|
|
304
|
+
const split = splitPageMarkdownSource(normalization.normalized);
|
|
270
305
|
const model = await parsePageMarkdown(split.body, {
|
|
271
306
|
...options,
|
|
272
307
|
lineOffset: (options.lineOffset ?? 0) + split.lineOffset,
|
|
273
308
|
});
|
|
274
309
|
|
|
310
|
+
const toOriginalRange = (range) => {
|
|
311
|
+
if (!range) return null;
|
|
312
|
+
const normalizedStart = split.bodyOffset + range.start;
|
|
313
|
+
const normalizedEnd = split.bodyOffset + range.end;
|
|
314
|
+
return {
|
|
315
|
+
start: normalization.originalOffsets[normalizedStart] ?? normalizedStart,
|
|
316
|
+
end: normalization.originalOffsets[normalizedEnd] ?? normalizedEnd,
|
|
317
|
+
};
|
|
318
|
+
};
|
|
319
|
+
|
|
275
320
|
return {
|
|
276
321
|
...model,
|
|
277
|
-
bodyOffset: split.bodyOffset,
|
|
322
|
+
bodyOffset: normalization.originalOffsets[split.bodyOffset] ?? split.bodyOffset,
|
|
278
323
|
frontmatter: split.frontmatter,
|
|
279
324
|
frontmatterUnclosed: split.frontmatterUnclosed,
|
|
280
|
-
fullSource:
|
|
325
|
+
fullSource: String(source),
|
|
326
|
+
links: model.links.map((link) => ({
|
|
327
|
+
...link,
|
|
328
|
+
range: toOriginalRange(link.range),
|
|
329
|
+
targetRange: toOriginalRange(link.targetRange),
|
|
330
|
+
})),
|
|
281
331
|
};
|
|
282
332
|
};
|
|
@@ -12,7 +12,7 @@ import { presentationPaletteNames } from './presentation-palette-metadata.mjs';
|
|
|
12
12
|
import { resolveThemeConfig } from './theme-presets.mjs';
|
|
13
13
|
|
|
14
14
|
export { presentationPaletteNames };
|
|
15
|
-
export const
|
|
15
|
+
export const appearanceNames = ['system', 'light', 'dark'];
|
|
16
16
|
const sectionBackgroundPatterns = Object.freeze({
|
|
17
17
|
uniform: ['base'],
|
|
18
18
|
alternating: ['base', 'soft'],
|
|
@@ -44,7 +44,7 @@ const createPaletteMode = ({ appearance, page, surfaces, frame, css }) => {
|
|
|
44
44
|
|
|
45
45
|
const presentationPalettes = Object.freeze({
|
|
46
46
|
'near-monochrome': Object.freeze({
|
|
47
|
-
|
|
47
|
+
defaultAppearance: 'dark',
|
|
48
48
|
modes: Object.freeze({
|
|
49
49
|
light: createPaletteMode({
|
|
50
50
|
appearance: 'light',
|
|
@@ -61,7 +61,6 @@ const presentationPalettes = Object.freeze({
|
|
|
61
61
|
soft: '#d8d8d3',
|
|
62
62
|
accent: '#4b4b46',
|
|
63
63
|
border: 'rgb(23 23 23 / 16%)',
|
|
64
|
-
navBackground: 'rgb(247 247 245 / 92%)',
|
|
65
64
|
navSeparator: 'rgb(23 23 23 / 24%)',
|
|
66
65
|
},
|
|
67
66
|
}),
|
|
@@ -80,14 +79,13 @@ const presentationPalettes = Object.freeze({
|
|
|
80
79
|
soft: '#302f2c',
|
|
81
80
|
accent: '#d8d2c8',
|
|
82
81
|
border: 'rgb(255 255 255 / 14%)',
|
|
83
|
-
navBackground: 'rgb(0 0 0 / 90%)',
|
|
84
82
|
navSeparator: 'rgb(255 255 255 / 28%)',
|
|
85
83
|
},
|
|
86
84
|
}),
|
|
87
85
|
}),
|
|
88
86
|
}),
|
|
89
87
|
'arctic-blue': Object.freeze({
|
|
90
|
-
|
|
88
|
+
defaultAppearance: 'light',
|
|
91
89
|
modes: Object.freeze({
|
|
92
90
|
light: createPaletteMode({
|
|
93
91
|
appearance: 'light',
|
|
@@ -104,7 +102,6 @@ const presentationPalettes = Object.freeze({
|
|
|
104
102
|
soft: '#d5dfe7',
|
|
105
103
|
accent: '#2a5877',
|
|
106
104
|
border: 'rgb(32 41 50 / 16%)',
|
|
107
|
-
navBackground: 'rgb(243 246 248 / 92%)',
|
|
108
105
|
navSeparator: 'rgb(32 41 50 / 24%)',
|
|
109
106
|
},
|
|
110
107
|
}),
|
|
@@ -123,14 +120,13 @@ const presentationPalettes = Object.freeze({
|
|
|
123
120
|
soft: '#354655',
|
|
124
121
|
accent: '#9bc5de',
|
|
125
122
|
border: 'rgb(237 243 247 / 16%)',
|
|
126
|
-
navBackground: 'rgb(23 32 42 / 92%)',
|
|
127
123
|
navSeparator: 'rgb(237 243 247 / 28%)',
|
|
128
124
|
},
|
|
129
125
|
}),
|
|
130
126
|
}),
|
|
131
127
|
}),
|
|
132
128
|
'mineral-teal': Object.freeze({
|
|
133
|
-
|
|
129
|
+
defaultAppearance: 'light',
|
|
134
130
|
modes: Object.freeze({
|
|
135
131
|
light: createPaletteMode({
|
|
136
132
|
appearance: 'light',
|
|
@@ -147,7 +143,6 @@ const presentationPalettes = Object.freeze({
|
|
|
147
143
|
soft: '#cfdfd9',
|
|
148
144
|
accent: '#155f5a',
|
|
149
145
|
border: 'rgb(32 54 49 / 17%)',
|
|
150
|
-
navBackground: 'rgb(238 244 241 / 92%)',
|
|
151
146
|
navSeparator: 'rgb(32 54 49 / 24%)',
|
|
152
147
|
},
|
|
153
148
|
}),
|
|
@@ -166,14 +161,13 @@ const presentationPalettes = Object.freeze({
|
|
|
166
161
|
soft: '#2c554f',
|
|
167
162
|
accent: '#8bd5ca',
|
|
168
163
|
border: 'rgb(237 245 241 / 16%)',
|
|
169
|
-
navBackground: 'rgb(16 45 44 / 92%)',
|
|
170
164
|
navSeparator: 'rgb(237 245 241 / 28%)',
|
|
171
165
|
},
|
|
172
166
|
}),
|
|
173
167
|
}),
|
|
174
168
|
}),
|
|
175
169
|
'soft-lavender': Object.freeze({
|
|
176
|
-
|
|
170
|
+
defaultAppearance: 'light',
|
|
177
171
|
modes: Object.freeze({
|
|
178
172
|
light: createPaletteMode({
|
|
179
173
|
appearance: 'light',
|
|
@@ -190,7 +184,6 @@ const presentationPalettes = Object.freeze({
|
|
|
190
184
|
soft: '#ded5e7',
|
|
191
185
|
accent: '#604678',
|
|
192
186
|
border: 'rgb(46 39 54 / 16%)',
|
|
193
|
-
navBackground: 'rgb(247 243 250 / 92%)',
|
|
194
187
|
navSeparator: 'rgb(46 39 54 / 24%)',
|
|
195
188
|
},
|
|
196
189
|
}),
|
|
@@ -209,14 +202,13 @@ const presentationPalettes = Object.freeze({
|
|
|
209
202
|
soft: '#4b3d57',
|
|
210
203
|
accent: '#d4b7e3',
|
|
211
204
|
border: 'rgb(244 237 248 / 16%)',
|
|
212
|
-
navBackground: 'rgb(37 31 46 / 92%)',
|
|
213
205
|
navSeparator: 'rgb(244 237 248 / 28%)',
|
|
214
206
|
},
|
|
215
207
|
}),
|
|
216
208
|
}),
|
|
217
209
|
}),
|
|
218
210
|
'warm-paper': Object.freeze({
|
|
219
|
-
|
|
211
|
+
defaultAppearance: 'light',
|
|
220
212
|
modes: Object.freeze({
|
|
221
213
|
light: createPaletteMode({
|
|
222
214
|
appearance: 'light',
|
|
@@ -233,7 +225,6 @@ const presentationPalettes = Object.freeze({
|
|
|
233
225
|
soft: '#d8d0c4',
|
|
234
226
|
accent: '#5f513c',
|
|
235
227
|
border: 'rgb(39 37 34 / 18%)',
|
|
236
|
-
navBackground: 'rgb(248 245 238 / 92%)',
|
|
237
228
|
navSeparator: 'rgb(39 37 34 / 24%)',
|
|
238
229
|
},
|
|
239
230
|
}),
|
|
@@ -252,14 +243,13 @@ const presentationPalettes = Object.freeze({
|
|
|
252
243
|
soft: '#3b342b',
|
|
253
244
|
accent: '#d5bea0',
|
|
254
245
|
border: 'rgb(243 237 226 / 16%)',
|
|
255
|
-
navBackground: 'rgb(27 25 22 / 92%)',
|
|
256
246
|
navSeparator: 'rgb(243 237 226 / 26%)',
|
|
257
247
|
},
|
|
258
248
|
}),
|
|
259
249
|
}),
|
|
260
250
|
}),
|
|
261
251
|
'retro-earth': Object.freeze({
|
|
262
|
-
|
|
252
|
+
defaultAppearance: 'light',
|
|
263
253
|
modes: Object.freeze({
|
|
264
254
|
light: createPaletteMode({
|
|
265
255
|
appearance: 'light',
|
|
@@ -276,7 +266,6 @@ const presentationPalettes = Object.freeze({
|
|
|
276
266
|
soft: '#deca9c',
|
|
277
267
|
accent: '#5e4514',
|
|
278
268
|
border: 'rgb(52 41 28 / 18%)',
|
|
279
|
-
navBackground: 'rgb(247 239 217 / 92%)',
|
|
280
269
|
navSeparator: 'rgb(52 41 28 / 25%)',
|
|
281
270
|
},
|
|
282
271
|
}),
|
|
@@ -295,14 +284,13 @@ const presentationPalettes = Object.freeze({
|
|
|
295
284
|
soft: '#54422a',
|
|
296
285
|
accent: '#e5c36c',
|
|
297
286
|
border: 'rgb(245 232 201 / 16%)',
|
|
298
|
-
navBackground: 'rgb(40 33 23 / 92%)',
|
|
299
287
|
navSeparator: 'rgb(245 232 201 / 28%)',
|
|
300
288
|
},
|
|
301
289
|
}),
|
|
302
290
|
}),
|
|
303
291
|
}),
|
|
304
292
|
'clay-rose': Object.freeze({
|
|
305
|
-
|
|
293
|
+
defaultAppearance: 'light',
|
|
306
294
|
modes: Object.freeze({
|
|
307
295
|
light: createPaletteMode({
|
|
308
296
|
appearance: 'light',
|
|
@@ -319,7 +307,6 @@ const presentationPalettes = Object.freeze({
|
|
|
319
307
|
soft: '#e2cfca',
|
|
320
308
|
accent: '#71313d',
|
|
321
309
|
border: 'rgb(60 41 41 / 17%)',
|
|
322
|
-
navBackground: 'rgb(247 239 237 / 92%)',
|
|
323
310
|
navSeparator: 'rgb(60 41 41 / 25%)',
|
|
324
311
|
},
|
|
325
312
|
}),
|
|
@@ -338,14 +325,13 @@ const presentationPalettes = Object.freeze({
|
|
|
338
325
|
soft: '#58404a',
|
|
339
326
|
accent: '#e6a7b0',
|
|
340
327
|
border: 'rgb(247 235 235 / 16%)',
|
|
341
|
-
navBackground: 'rgb(45 30 34 / 92%)',
|
|
342
328
|
navSeparator: 'rgb(247 235 235 / 28%)',
|
|
343
329
|
},
|
|
344
330
|
}),
|
|
345
331
|
}),
|
|
346
332
|
}),
|
|
347
333
|
'forest-moss': Object.freeze({
|
|
348
|
-
|
|
334
|
+
defaultAppearance: 'light',
|
|
349
335
|
modes: Object.freeze({
|
|
350
336
|
light: createPaletteMode({
|
|
351
337
|
appearance: 'light',
|
|
@@ -362,7 +348,6 @@ const presentationPalettes = Object.freeze({
|
|
|
362
348
|
soft: '#d5ddc5',
|
|
363
349
|
accent: '#324e25',
|
|
364
350
|
border: 'rgb(38 50 37 / 17%)',
|
|
365
|
-
navBackground: 'rgb(241 243 232 / 92%)',
|
|
366
351
|
navSeparator: 'rgb(38 50 37 / 24%)',
|
|
367
352
|
},
|
|
368
353
|
}),
|
|
@@ -381,14 +366,13 @@ const presentationPalettes = Object.freeze({
|
|
|
381
366
|
soft: '#394c37',
|
|
382
367
|
accent: '#add493',
|
|
383
368
|
border: 'rgb(238 242 230 / 16%)',
|
|
384
|
-
navBackground: 'rgb(23 34 24 / 92%)',
|
|
385
369
|
navSeparator: 'rgb(238 242 230 / 28%)',
|
|
386
370
|
},
|
|
387
371
|
}),
|
|
388
372
|
}),
|
|
389
373
|
}),
|
|
390
374
|
'vivid-night': Object.freeze({
|
|
391
|
-
|
|
375
|
+
defaultAppearance: 'dark',
|
|
392
376
|
modes: Object.freeze({
|
|
393
377
|
light: createPaletteMode({
|
|
394
378
|
appearance: 'light',
|
|
@@ -405,7 +389,6 @@ const presentationPalettes = Object.freeze({
|
|
|
405
389
|
soft: '#d5daec',
|
|
406
390
|
accent: '#3b568f',
|
|
407
391
|
border: 'rgb(37 36 58 / 16%)',
|
|
408
|
-
navBackground: 'rgb(242 244 251 / 92%)',
|
|
409
392
|
navSeparator: 'rgb(37 36 58 / 24%)',
|
|
410
393
|
},
|
|
411
394
|
}),
|
|
@@ -424,7 +407,6 @@ const presentationPalettes = Object.freeze({
|
|
|
424
407
|
soft: '#403a58',
|
|
425
408
|
accent: '#9fe4e8',
|
|
426
409
|
border: 'rgb(244 241 255 / 16%)',
|
|
427
|
-
navBackground: 'rgb(29 27 45 / 92%)',
|
|
428
410
|
navSeparator: 'rgb(244 241 255 / 28%)',
|
|
429
411
|
},
|
|
430
412
|
}),
|
|
@@ -493,11 +475,11 @@ export const resolveThemePresentation = (theme, sourceLabel = 'theme.yaml') => {
|
|
|
493
475
|
const normalizedTheme = resolveThemeConfig(theme, sourceLabel);
|
|
494
476
|
const paletteName = normalizedTheme.palette ?? 'near-monochrome';
|
|
495
477
|
const palette = getPresentationPalette(paletteName);
|
|
496
|
-
const
|
|
478
|
+
const appearance = normalizedTheme.appearance ?? {};
|
|
497
479
|
const readerControls = normalizedTheme.readerControls ?? {};
|
|
498
|
-
const
|
|
499
|
-
if (!
|
|
500
|
-
throw new Error(`
|
|
480
|
+
const defaultAppearance = appearance.default ?? palette.defaultAppearance;
|
|
481
|
+
if (!appearanceNames.includes(defaultAppearance)) {
|
|
482
|
+
throw new Error(`appearance.default must be one of ${appearanceNames.join(', ')} in ${sourceLabel}.`);
|
|
501
483
|
}
|
|
502
484
|
const typography = resolveTypographyConfig(normalizedTheme.typography ?? defaultTypography, sourceLabel);
|
|
503
485
|
const textWidth = normalizedTheme.layout?.textWidth;
|
|
@@ -508,19 +490,19 @@ export const resolveThemePresentation = (theme, sourceLabel = 'theme.yaml') => {
|
|
|
508
490
|
|
|
509
491
|
return {
|
|
510
492
|
paletteName,
|
|
511
|
-
palette: palette.modes[
|
|
493
|
+
palette: palette.modes[defaultAppearance === 'dark' ? 'dark' : 'light'],
|
|
512
494
|
paletteModes: palette.modes,
|
|
513
|
-
|
|
514
|
-
default:
|
|
495
|
+
appearance: {
|
|
496
|
+
default: defaultAppearance,
|
|
515
497
|
},
|
|
516
498
|
readerPreferences: {
|
|
517
499
|
controls: {
|
|
518
|
-
appearance: readerControls.
|
|
500
|
+
appearance: readerControls.appearance === true,
|
|
519
501
|
readingWidth: true,
|
|
520
502
|
focusReading: readerControls.focusReading === true,
|
|
521
503
|
},
|
|
522
504
|
defaults: {
|
|
523
|
-
appearance:
|
|
505
|
+
appearance: defaultAppearance,
|
|
524
506
|
readingWidth: textWidth === 'narrow' ? 'narrow' : textWidth === 'wide' ? 'wide' : 'standard',
|
|
525
507
|
focusReading: 'off',
|
|
526
508
|
},
|
|
@@ -2,6 +2,7 @@ import { readFile } from 'node:fs/promises';
|
|
|
2
2
|
import {
|
|
3
3
|
validateConfigYamlStructure,
|
|
4
4
|
} from './site-content.mjs';
|
|
5
|
+
import { defaultImagePresentation, imagePresentationNames } from './image-presentation.mjs';
|
|
5
6
|
import { navigationModeNames } from './navigation-model.mjs';
|
|
6
7
|
import {
|
|
7
8
|
siteConfigLabel,
|
|
@@ -51,16 +52,6 @@ const readEnum = (object, key, path, allowedValues, fallback, sourceLabel = site
|
|
|
51
52
|
return value;
|
|
52
53
|
};
|
|
53
54
|
|
|
54
|
-
const readBoolean = (object, key, path, fallback, sourceLabel = siteConfigLabel) => {
|
|
55
|
-
const value = object[key] ?? fallback;
|
|
56
|
-
|
|
57
|
-
if (typeof value !== 'boolean') {
|
|
58
|
-
throw new Error(`${path}.${key} must be true or false in ${sourceLabel}.`);
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
return value;
|
|
62
|
-
};
|
|
63
|
-
|
|
64
55
|
const readFontFamily = (object, key, path, fallback, sourceLabel = siteConfigLabel) => {
|
|
65
56
|
const value = object[key] ?? fallback;
|
|
66
57
|
|
|
@@ -194,10 +185,13 @@ const readSiteUrl = (config) => {
|
|
|
194
185
|
const localeLabels = Object.freeze({
|
|
195
186
|
en: Object.freeze({
|
|
196
187
|
breadcrumb: 'Breadcrumb',
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
188
|
+
appearance: 'Appearance',
|
|
189
|
+
appearanceDark: 'Dark',
|
|
190
|
+
appearanceLight: 'Light',
|
|
191
|
+
appearanceSystem: 'System',
|
|
192
|
+
codeCopied: 'Copied',
|
|
193
|
+
codeCopyFailed: 'Could not copy code',
|
|
194
|
+
copyCode: 'Copy code',
|
|
201
195
|
displaySettings: 'Display',
|
|
202
196
|
focusReading: 'Focus reading',
|
|
203
197
|
readingWidth: 'Reading width',
|
|
@@ -213,6 +207,8 @@ const localeLabels = Object.freeze({
|
|
|
213
207
|
navigationMenu: 'Menu',
|
|
214
208
|
nextImage: 'Next image',
|
|
215
209
|
note: 'Note',
|
|
210
|
+
pageMoved: 'Page moved',
|
|
211
|
+
pageMovedText: 'This address now identifies',
|
|
216
212
|
pageNavigation: 'Page contents',
|
|
217
213
|
previousImage: 'Previous image',
|
|
218
214
|
siteBanners: 'Site notices',
|
|
@@ -221,10 +217,13 @@ const localeLabels = Object.freeze({
|
|
|
221
217
|
}),
|
|
222
218
|
sv: Object.freeze({
|
|
223
219
|
breadcrumb: 'Brödsmulor',
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
220
|
+
appearance: 'Utseende',
|
|
221
|
+
appearanceDark: 'Mörkt',
|
|
222
|
+
appearanceLight: 'Ljust',
|
|
223
|
+
appearanceSystem: 'System',
|
|
224
|
+
codeCopied: 'Kopierat',
|
|
225
|
+
codeCopyFailed: 'Kunde inte kopiera koden',
|
|
226
|
+
copyCode: 'Kopiera kod',
|
|
228
227
|
displaySettings: 'Visning',
|
|
229
228
|
focusReading: 'Fokuserad läsning',
|
|
230
229
|
readingWidth: 'Textbredd',
|
|
@@ -240,6 +239,8 @@ const localeLabels = Object.freeze({
|
|
|
240
239
|
navigationMenu: 'Meny',
|
|
241
240
|
nextImage: 'Nästa bild',
|
|
242
241
|
note: 'Not',
|
|
242
|
+
pageMoved: 'Sidan har flyttats',
|
|
243
|
+
pageMovedText: 'Den här adressen identifierar nu',
|
|
243
244
|
pageNavigation: 'Sidinnehåll',
|
|
244
245
|
previousImage: 'Föregående bild',
|
|
245
246
|
siteBanners: 'Meddelanden',
|
|
@@ -365,6 +366,21 @@ export const resolveThemeVisualConfig = (theme, sourceLabel = siteThemeLabel) =>
|
|
|
365
366
|
const resolvedContentSpacing = readEnum(rawLayoutConfig, 'contentSpacing', 'layout', contentSpacingNames, 'normal', sourceLabel);
|
|
366
367
|
const resolvedLayoutSpacingDefaults = contentSpacingProfiles[resolvedContentSpacing];
|
|
367
368
|
const corners = readEnum(rawThemeConfig, 'corners', 'theme', ['square', 'rounded'], 'rounded', sourceLabel);
|
|
369
|
+
const imagePresentation = readEnum(
|
|
370
|
+
rawImagesConfig,
|
|
371
|
+
'presentation',
|
|
372
|
+
'images',
|
|
373
|
+
imagePresentationNames,
|
|
374
|
+
defaultImagePresentation,
|
|
375
|
+
sourceLabel,
|
|
376
|
+
);
|
|
377
|
+
if (imagePresentation === 'prose-aligned' && rawImagesConfig.maxAvailableHeightPercent !== undefined) {
|
|
378
|
+
throw new Error([
|
|
379
|
+
`images.maxAvailableHeightPercent cannot be used with images.presentation "prose-aligned" in ${sourceLabel}.`,
|
|
380
|
+
'Prose-aligned images follow available horizontal space instead of a viewport-height limit.',
|
|
381
|
+
'Remove images.maxAvailableHeightPercent or set images.presentation to "centered-fit".',
|
|
382
|
+
].join('\n'));
|
|
383
|
+
}
|
|
368
384
|
|
|
369
385
|
return Object.freeze({
|
|
370
386
|
layout: Object.freeze({
|
|
@@ -392,10 +408,15 @@ export const resolveThemeVisualConfig = (theme, sourceLabel = siteThemeLabel) =>
|
|
|
392
408
|
}),
|
|
393
409
|
}),
|
|
394
410
|
images: Object.freeze({
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
411
|
+
presentation: imagePresentation,
|
|
412
|
+
...(imagePresentation === 'centered-fit'
|
|
413
|
+
? {
|
|
414
|
+
maxAvailableHeightPercent: readResponsivePercent(rawImagesConfig, 'maxAvailableHeightPercent', 'images', Object.freeze({
|
|
415
|
+
desktop: 74,
|
|
416
|
+
mobile: 68,
|
|
417
|
+
}), sourceLabel),
|
|
418
|
+
}
|
|
419
|
+
: {}),
|
|
399
420
|
maxAvailableWidthPercent: readResponsivePercent(rawImagesConfig, 'maxAvailableWidthPercent', 'images', Object.freeze({
|
|
400
421
|
desktop: 100,
|
|
401
422
|
mobile: 100,
|
|
@@ -416,6 +437,13 @@ export const resolveThemeVisualConfig = (theme, sourceLabel = siteThemeLabel) =>
|
|
|
416
437
|
|
|
417
438
|
export const resolveNavigationConfig = (config, sourceLabel = siteConfigLabel) => {
|
|
418
439
|
const rawNavigation = assertObject(config.navigation ?? {}, 'navigation', sourceLabel);
|
|
440
|
+
if (Object.hasOwn(rawNavigation, 'sectionTracking')) {
|
|
441
|
+
throw new Error([
|
|
442
|
+
`navigation.sectionTracking is no longer supported in ${sourceLabel}.`,
|
|
443
|
+
'Section tracking is automatic on pages with a contents rail.',
|
|
444
|
+
'Remove navigation.sectionTracking.',
|
|
445
|
+
].join('\n'));
|
|
446
|
+
}
|
|
419
447
|
|
|
420
448
|
return Object.freeze({
|
|
421
449
|
mode: readEnum(
|
|
@@ -426,13 +454,6 @@ export const resolveNavigationConfig = (config, sourceLabel = siteConfigLabel) =
|
|
|
426
454
|
'automatic',
|
|
427
455
|
sourceLabel,
|
|
428
456
|
),
|
|
429
|
-
sectionTracking: readBoolean(
|
|
430
|
-
rawNavigation,
|
|
431
|
-
'sectionTracking',
|
|
432
|
-
'navigation',
|
|
433
|
-
false,
|
|
434
|
-
sourceLabel,
|
|
435
|
-
),
|
|
436
457
|
});
|
|
437
458
|
};
|
|
438
459
|
|