@janga/norna 0.7.13 → 0.7.15
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 +15 -9
- package/bin/norna-cli.mjs +11 -4
- package/docs/README.md +71 -43
- package/docs/commands.md +34 -20
- package/docs/configuration.md +43 -62
- package/docs/content.md +34 -7
- package/docs/design/command-organization.md +15 -8
- package/docs/design/norna-diagram-design.md +6 -6
- package/docs/design/site-examples-structure.md +1 -1
- package/docs/engine-development.md +22 -9
- package/docs/getting-started.md +29 -16
- package/docs/how-to/embedded-site.md +1 -1
- package/docs/images-and-metadata.md +3 -3
- package/docs/local-development.md +11 -3
- package/docs/public-files.md +105 -0
- package/docs/publishing.md +4 -8
- package/docs/routes.md +6 -7
- package/docs/site-files.md +144 -0
- package/docs/sitewide-content.md +47 -33
- package/docs/theme.md +37 -49
- package/docs/typography.md +25 -31
- package/examples/complete-sites/dog-shelter-multi-page/README.md +8 -0
- package/examples/complete-sites/{dog-shelter-single-page/site/config.md → dog-shelter-multi-page/site/config.yaml} +0 -2
- package/examples/complete-sites/dog-shelter-multi-page/site/sitewide-content.yaml +6 -0
- package/examples/complete-sites/dog-shelter-multi-page/site/{theme.md → theme.yaml} +0 -2
- package/examples/complete-sites/dog-shelter-single-page/README.md +8 -0
- package/examples/complete-sites/{dog-shelter-multi-page/site/config.md → dog-shelter-single-page/site/config.yaml} +0 -2
- package/examples/complete-sites/dog-shelter-single-page/site/sitewide-content.yaml +6 -0
- package/examples/complete-sites/dog-shelter-single-page/site/{theme.md → theme.yaml} +0 -2
- package/examples/feature-demos/media-and-surfaces/.astro/data-store.json +1 -1
- package/examples/feature-demos/media-and-surfaces/README.md +1 -1
- package/examples/feature-demos/media-and-surfaces/site/{config.md → config.yaml} +0 -2
- package/examples/feature-demos/media-and-surfaces/site/content.md +2 -2
- package/examples/feature-demos/media-and-surfaces/site/routes/020-surfaces/{route-content.md → content.md} +3 -7
- package/examples/feature-demos/media-and-surfaces/site/routes/020-surfaces/theme.yaml +3 -0
- package/examples/feature-demos/media-and-surfaces/site/{sitewide-content.md → sitewide-content.yaml} +1 -3
- package/examples/feature-demos/media-and-surfaces/site/{theme.md → theme.yaml} +0 -2
- package/examples/feature-demos/sitewide-content/README.md +8 -0
- package/examples/feature-demos/sitewide-content/site/{config.md → config.yaml} +0 -2
- package/examples/feature-demos/sitewide-content/site/routes/010-identity/{route-content.md → content.md} +3 -3
- package/examples/feature-demos/sitewide-content/site/routes/020-notices/{route-content.md → content.md} +1 -1
- package/examples/feature-demos/sitewide-content/site/routes/030-footer/{route-content.md → content.md} +1 -1
- package/examples/feature-demos/sitewide-content/site/{sitewide-content.md → sitewide-content.yaml} +1 -11
- package/examples/feature-demos/sitewide-content/site/{theme.md → theme.yaml} +0 -2
- package/examples/feature-demos/theme-presets/site/config.yaml +1 -0
- package/examples/feature-demos/theme-presets/site/routes/010-portfolio/{theme.md → theme.yaml} +0 -2
- package/examples/feature-demos/theme-presets/site/routes/020-documentation/{theme.md → theme.yaml} +0 -2
- package/examples/feature-demos/theme-presets/site/routes/030-project/theme.yaml +1 -0
- package/examples/feature-demos/theme-presets/site/routes/040-statement/{route-content.md → content.md} +1 -1
- package/examples/feature-demos/theme-presets/site/routes/040-statement/{theme.md → theme.yaml} +0 -2
- package/examples/feature-demos/theme-presets/site/{sitewide-content.md → sitewide-content.yaml} +1 -3
- package/examples/feature-demos/theme-presets/site/theme.yaml +1 -0
- package/fixtures/basic/site/{config.md → config.yaml} +0 -2
- package/fixtures/basic/site/content.md +3 -3
- package/fixtures/basic/site/{sitewide-content.md → sitewide-content.yaml} +0 -2
- package/fixtures/basic/site/{theme.md → theme.yaml} +3 -6
- package/fixtures/content-model-v2/site/{config.md → config.yaml} +0 -2
- package/fixtures/content-model-v2/site/theme.yaml +4 -0
- package/package.json +10 -3
- package/schemas/config.schema.json +77 -0
- package/schemas/content-frontmatter.schema.json +81 -0
- package/schemas/manifest.json +9 -0
- package/schemas/sitewide-content.schema.json +215 -0
- package/schemas/theme.schema.json +1208 -0
- package/scripts/check-config.mjs +18 -11
- package/scripts/export-theme-preset.mjs +1 -1
- package/scripts/generate-schemas.mjs +283 -0
- package/scripts/init-site.mjs +22 -27
- package/scripts/lib/documentation-links.mjs +8 -0
- package/scripts/lib/editor-language-service.mjs +609 -0
- package/scripts/lib/example-sites.mjs +2 -2
- package/scripts/lib/git-status.mjs +7 -0
- package/scripts/lib/logo-assets.mjs +15 -10
- package/scripts/lib/norna-markdown-blocks.mjs +90 -11
- package/scripts/lib/presentation.mjs +23 -12
- package/scripts/lib/project-config.mjs +17 -41
- package/scripts/lib/public-asset-conventions.mjs +112 -0
- package/scripts/lib/schema-definitions.mjs +213 -0
- package/scripts/lib/schema-value-definitions.mjs +86 -0
- package/scripts/lib/site-content.mjs +24 -18
- package/scripts/lib/site-paths.mjs +4 -8
- package/scripts/lib/sitewide-content.mjs +24 -0
- package/scripts/lib/theme-config.mjs +15 -35
- package/scripts/lib/theme-presets.mjs +76 -66
- package/scripts/lib/typography.mjs +15 -15
- package/scripts/lib/yaml-config.mjs +50 -0
- package/scripts/list-theme-presets.mjs +15 -0
- package/scripts/release.mjs +84 -8
- package/scripts/show-typography.mjs +22 -46
- package/scripts/sync-content-sections.mjs +2 -2
- package/scripts/test-client-javascript.mjs +28 -28
- package/scripts/test-content-check.mjs +21 -26
- package/scripts/test-content-model-v2.mjs +72 -40
- package/scripts/test-documentation.mjs +70 -4
- package/scripts/test-editor-language-service.mjs +158 -0
- package/scripts/test-engine-commands.mjs +65 -34
- package/scripts/test-package-check.mjs +37 -30
- package/scripts/test-project-config.mjs +15 -20
- package/scripts/test-schemas.mjs +102 -0
- package/scripts/test-temporary-visibility.mjs +5 -13
- package/scripts/test-theme-presets.mjs +64 -37
- package/src/components/CardList.astro +2 -5
- package/src/components/ImageCarousel.astro +21 -21
- package/src/components/{GalleryGrid.astro → ImageStack.astro} +12 -12
- package/src/components/SectionNavigationScript.astro +28 -0
- package/src/components/SiteNavigation.astro +6 -5
- package/src/components/SitePage.astro +8 -8
- package/src/components/SiteSection.astro +18 -21
- package/src/content.config.ts +17 -209
- package/src/layouts/BaseLayout.astro +9 -9
- package/src/lib/{galleryImages.ts → imagePresentation.ts} +7 -7
- package/src/lib/routeThemes.ts +9 -29
- package/src/lib/sectionContent.ts +3 -3
- package/src/lib/sitePublicAssets.ts +5 -21
- package/src/styles/global.css +73 -73
- package/starters/basic/.github/workflows/deploy.yml +1 -1
- package/starters/basic/README.md +16 -13
- package/starters/basic/package.json +3 -2
- package/starters/basic/site/{config.md → config.yaml} +0 -2
- package/starters/basic/site/content.md +1 -1
- package/starters/basic/site/{sitewide-content.md → sitewide-content.yaml} +1 -3
- package/starters/basic/site/{theme.md → theme.yaml} +8 -15
- package/starters/project/.github/workflows/deploy.yml +1 -1
- package/starters/project/README.md +22 -15
- package/starters/project/package.json +3 -2
- package/starters/project/site/{config.md → config.yaml} +0 -2
- package/starters/project/site/{sitewide-content.md → sitewide-content.yaml} +1 -3
- package/starters/project/site/{theme.md → theme.yaml} +4 -8
- package/docs/site-structure.md +0 -98
- package/examples/complete-sites/dog-shelter-multi-page/site/sitewide-content.md +0 -16
- package/examples/complete-sites/dog-shelter-single-page/site/sitewide-content.md +0 -16
- package/examples/feature-demos/media-and-surfaces/site/routes/020-surfaces/theme.md +0 -8
- package/examples/feature-demos/theme-presets/site/config.md +0 -3
- package/examples/feature-demos/theme-presets/site/routes/030-project/theme.md +0 -3
- package/examples/feature-demos/theme-presets/site/theme.md +0 -3
- package/fixtures/content-model-v2/site/theme.md +0 -7
- package/scripts/lib/sitewide-config.mjs +0 -34
- package/src/components/MobileSectionNavigationScript.astro +0 -14
- /package/examples/complete-sites/dog-shelter-multi-page/site/routes/010-dogs/{route-content.md → content.md} +0 -0
- /package/examples/complete-sites/dog-shelter-multi-page/site/routes/020-adopt/{route-content.md → content.md} +0 -0
- /package/examples/feature-demos/media-and-surfaces/site/routes/010-media/{route-content.md → content.md} +0 -0
- /package/examples/feature-demos/theme-presets/site/routes/010-portfolio/{route-content.md → content.md} +0 -0
- /package/examples/feature-demos/theme-presets/site/routes/020-documentation/{route-content.md → content.md} +0 -0
- /package/examples/feature-demos/theme-presets/site/routes/030-project/{route-content.md → content.md} +0 -0
- /package/fixtures/content-model-v2/site/routes/010-guide/{route-content.md → content.md} +0 -0
- /package/starters/project/site/routes/010-guide/{route-content.md → content.md} +0 -0
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import {
|
|
2
|
+
getThemePresetMetadata,
|
|
3
|
+
themePresetNames,
|
|
4
|
+
} from './theme-presets.mjs';
|
|
5
|
+
|
|
6
|
+
const option = (title, description) => Object.freeze({ title, description });
|
|
7
|
+
const definition = (values, options) => Object.freeze({
|
|
8
|
+
values: Object.freeze(values),
|
|
9
|
+
options: Object.freeze(options),
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
export const schemaValueDefinitions = Object.freeze([
|
|
13
|
+
definition(themePresetNames, Object.fromEntries(themePresetNames.map((name) => {
|
|
14
|
+
const metadata = getThemePresetMetadata(name);
|
|
15
|
+
return [name, option(metadata.title, metadata.description)];
|
|
16
|
+
}))),
|
|
17
|
+
definition(['instant', 'smooth'], {
|
|
18
|
+
instant: option('Instant', 'Jump directly to anchors without animated scrolling. This is the default.'),
|
|
19
|
+
smooth: option('Browser smooth', 'Use the browser\'s native smooth scrolling for anchor navigation.'),
|
|
20
|
+
}),
|
|
21
|
+
definition(['en', 'sv'], {
|
|
22
|
+
en: option('English', 'Use Norna\'s built-in English interface text.'),
|
|
23
|
+
sv: option('Swedish', 'Use Norna\'s built-in Swedish interface text.'),
|
|
24
|
+
}),
|
|
25
|
+
definition(['left', 'center', 'right'], {
|
|
26
|
+
left: option('Left', 'Align text with the left edge of its text area.'),
|
|
27
|
+
center: option('Center', 'Center text within its text area.'),
|
|
28
|
+
right: option('Right', 'Align text with the right edge of its text area.'),
|
|
29
|
+
}),
|
|
30
|
+
definition(['small', 'medium', 'large', 'xlarge'], {
|
|
31
|
+
small: option('Small', 'Use the small type size from the active typography system.'),
|
|
32
|
+
medium: option('Medium', 'Use the standard type size from the active typography system.'),
|
|
33
|
+
large: option('Large', 'Use a larger type size for stronger emphasis.'),
|
|
34
|
+
xlarge: option('Extra large', 'Use the largest available type size for exceptional emphasis.'),
|
|
35
|
+
}),
|
|
36
|
+
definition(['narrow', 'normal', 'wide'], {
|
|
37
|
+
narrow: option('Narrow', 'Use a shorter line length suited to reading-focused text.'),
|
|
38
|
+
normal: option('Normal', 'Use the balanced default line length.'),
|
|
39
|
+
wide: option('Wide', 'Allow body text to use more horizontal space.'),
|
|
40
|
+
}),
|
|
41
|
+
definition([400, 500, 600, 700], {
|
|
42
|
+
400: option('Regular', 'Use regular font weight.'),
|
|
43
|
+
500: option('Medium', 'Use medium font weight.'),
|
|
44
|
+
600: option('Semibold', 'Use semibold font weight.'),
|
|
45
|
+
700: option('Bold', 'Use bold font weight.'),
|
|
46
|
+
}),
|
|
47
|
+
definition(['restrained', 'dense', 'reading', 'statement'], {
|
|
48
|
+
restrained: option('Restrained', 'Quiet typography with regular-weight headings and balanced line lengths.'),
|
|
49
|
+
dense: option('Dense', 'Compact typography with wider text and tighter line height.'),
|
|
50
|
+
reading: option('Reading', 'Reading-focused typography with narrower text and relaxed line height.'),
|
|
51
|
+
statement: option('Statement', 'Stronger headings and tighter body rhythm for short, expressive pages.'),
|
|
52
|
+
}),
|
|
53
|
+
definition(['dark', 'light', 'paper'], {
|
|
54
|
+
dark: option('Dark', 'Black page and section surfaces with light text.'),
|
|
55
|
+
light: option('Light', 'White and cool light surfaces with dark text.'),
|
|
56
|
+
paper: option('Paper', 'Warm off-white surfaces with dark text.'),
|
|
57
|
+
}),
|
|
58
|
+
definition(['base', 'soft', 'emphasis'], {
|
|
59
|
+
base: option('Base', 'Use the palette\'s normal page surface.'),
|
|
60
|
+
soft: option('Soft', 'Use the palette\'s subtle contrasting surface.'),
|
|
61
|
+
emphasis: option('Emphasis', 'Use the palette\'s strongest section surface.'),
|
|
62
|
+
}),
|
|
63
|
+
definition(['compact', 'normal', 'airy'], {
|
|
64
|
+
compact: option('Compact', 'Reduce vertical spacing while preserving readable separation.'),
|
|
65
|
+
normal: option('Normal', 'Use balanced vertical spacing.'),
|
|
66
|
+
airy: option('Airy', 'Increase vertical spacing for a more expansive presentation.'),
|
|
67
|
+
}),
|
|
68
|
+
definition(['warning'], {
|
|
69
|
+
warning: option('Warning', 'Present the banner as an important warning notice.'),
|
|
70
|
+
}),
|
|
71
|
+
definition(['short', 'medium', 'long', 'full'], {
|
|
72
|
+
short: option('Short', 'Use a compact localized date or time representation.'),
|
|
73
|
+
medium: option('Medium', 'Use a moderately detailed localized representation.'),
|
|
74
|
+
long: option('Long', 'Use a detailed localized representation.'),
|
|
75
|
+
full: option('Full', 'Use the most detailed localized representation.'),
|
|
76
|
+
}),
|
|
77
|
+
]);
|
|
78
|
+
|
|
79
|
+
const valuesEqual = (left, right) => (
|
|
80
|
+
left.length === right.length
|
|
81
|
+
&& left.every((value, index) => value === right[index])
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
export const getSchemaValueDefinition = (values) => (
|
|
85
|
+
schemaValueDefinitions.find((candidate) => valuesEqual(candidate.values, values))
|
|
86
|
+
);
|
|
@@ -8,7 +8,6 @@ import {
|
|
|
8
8
|
siteImagesLabel,
|
|
9
9
|
siteRoutesDir,
|
|
10
10
|
siteRoutesLabel,
|
|
11
|
-
siteThemeLabel,
|
|
12
11
|
} from './site-paths.mjs';
|
|
13
12
|
import { parseRouteDirectory } from './route-model.mjs';
|
|
14
13
|
|
|
@@ -22,15 +21,14 @@ const deprecatedInlineStyleReferenceRegex = /\[[^\]\n]+\]\{\.([a-z][a-z0-9-]*)\}
|
|
|
22
21
|
const frontmatterDelimiterRegex = /^---\s*$/;
|
|
23
22
|
const knownConfigTopLevelFrontmatterKeys = new Set(['url', 'language', 'scrollBehavior']);
|
|
24
23
|
const knownContentTopLevelFrontmatterKeys = new Set(['title', 'description', 'navigation', 'sections']);
|
|
25
|
-
const knownThemeTopLevelFrontmatterKeys = new Set(['navigation', 'preset', 'layout', '
|
|
26
|
-
const knownRouteThemeTopLevelFrontmatterKeys = new Set(['navigation', 'preset', 'layout', '
|
|
24
|
+
const knownThemeTopLevelFrontmatterKeys = new Set(['navigation', 'preset', 'layout', 'images', 'typography', 'palette', 'sectionSurfaces']);
|
|
25
|
+
const knownRouteThemeTopLevelFrontmatterKeys = new Set(['navigation', 'preset', 'layout', 'images', 'typography', 'palette', 'sectionSurfaces']);
|
|
27
26
|
const knownSitewideTopLevelFrontmatterKeys = new Set(['navigation', 'banners', 'footer']);
|
|
28
27
|
const knownNestedFrontmatterKeys = new Set([
|
|
29
28
|
'align',
|
|
30
29
|
'alt',
|
|
31
30
|
'body',
|
|
32
31
|
'blockGap',
|
|
33
|
-
'brand',
|
|
34
32
|
'caption',
|
|
35
33
|
'carousel',
|
|
36
34
|
'density',
|
|
@@ -38,7 +36,6 @@ const knownNestedFrontmatterKeys = new Set([
|
|
|
38
36
|
'fontFamily',
|
|
39
37
|
'from',
|
|
40
38
|
'finalSectionBottom',
|
|
41
|
-
'gallery',
|
|
42
39
|
'gutter',
|
|
43
40
|
'headingToBlock',
|
|
44
41
|
'h1',
|
|
@@ -51,6 +48,7 @@ const knownNestedFrontmatterKeys = new Set([
|
|
|
51
48
|
'include',
|
|
52
49
|
'image',
|
|
53
50
|
'imageGap',
|
|
51
|
+
'images',
|
|
54
52
|
'label',
|
|
55
53
|
'logo',
|
|
56
54
|
'lineHeight',
|
|
@@ -63,16 +61,14 @@ const knownNestedFrontmatterKeys = new Set([
|
|
|
63
61
|
'pageWidth',
|
|
64
62
|
'palette',
|
|
65
63
|
'preset',
|
|
66
|
-
'
|
|
64
|
+
'profile',
|
|
67
65
|
'rhythm',
|
|
68
|
-
'sequence',
|
|
69
66
|
'sections',
|
|
70
67
|
'sectionGap',
|
|
71
68
|
'sectionSurfaces',
|
|
72
69
|
'size',
|
|
73
70
|
'spacingAfter',
|
|
74
71
|
'spacingBefore',
|
|
75
|
-
'surface',
|
|
76
72
|
'theme',
|
|
77
73
|
'typography',
|
|
78
74
|
'until',
|
|
@@ -108,14 +104,14 @@ export const getContentFiles = async () => {
|
|
|
108
104
|
|
|
109
105
|
const routeDirectory = entry.name;
|
|
110
106
|
const routeDir = path.join(siteRoutesDir, routeDirectory);
|
|
111
|
-
const routeContentPath = path.join(routeDir, '
|
|
107
|
+
const routeContentPath = path.join(routeDir, 'content.md');
|
|
112
108
|
|
|
113
109
|
if (!(await fileExists(routeContentPath))) continue;
|
|
114
110
|
|
|
115
111
|
const { routeId, routeOrder } = parseRouteDirectory(routeDirectory, `${siteRoutesLabel}/${routeDirectory}`);
|
|
116
112
|
|
|
117
113
|
contentFiles.push({
|
|
118
|
-
contentLabel: `${siteRoutesLabel}/${routeDirectory}/
|
|
114
|
+
contentLabel: `${siteRoutesLabel}/${routeDirectory}/content.md`,
|
|
119
115
|
contentPath: routeContentPath,
|
|
120
116
|
imagesDir: path.join(routeDir, 'images'),
|
|
121
117
|
imagesLabel: `${siteRoutesLabel}/${routeDirectory}/images`,
|
|
@@ -253,9 +249,16 @@ export const validateFrontmatterStructure = (frontmatter, addIssue, {
|
|
|
253
249
|
const key = keyMatch[1];
|
|
254
250
|
if (knownTopLevelFrontmatterKeys.has(key)) continue;
|
|
255
251
|
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
252
|
+
let fix;
|
|
253
|
+
if (fileKind === 'content' && key === 'images') {
|
|
254
|
+
fix = 'Put local image references in norna-image-stack or norna-image-carousel blocks in the Markdown body.';
|
|
255
|
+
} else if (fileKind === 'content' && knownThemeTopLevelFrontmatterKeys.has(key)) {
|
|
256
|
+
fix = `Move "${key}:" to theme.yaml. Visual settings do not belong in content frontmatter.`;
|
|
257
|
+
} else if (knownNestedFrontmatterKeys.has(key)) {
|
|
258
|
+
fix = `Indent "${key}:" under the object it belongs to.`;
|
|
259
|
+
} else {
|
|
260
|
+
fix = `Move "${key}:" under the correct parent key, or remove it if it is not part of the ${fileKind} schema.`;
|
|
261
|
+
}
|
|
259
262
|
|
|
260
263
|
addIssue({
|
|
261
264
|
severity: 'error',
|
|
@@ -271,31 +274,34 @@ export const validateContentFrontmatterStructure = (frontmatter, addIssue) =>
|
|
|
271
274
|
fileKind: 'content',
|
|
272
275
|
});
|
|
273
276
|
|
|
274
|
-
export const
|
|
277
|
+
export const validateConfigYamlStructure = (frontmatter, addIssue) =>
|
|
275
278
|
validateFrontmatterStructure(frontmatter, addIssue, {
|
|
276
279
|
knownTopLevelFrontmatterKeys: knownConfigTopLevelFrontmatterKeys,
|
|
277
280
|
fileKind: 'config',
|
|
278
281
|
});
|
|
279
282
|
|
|
280
|
-
export const
|
|
283
|
+
export const validateThemeYamlStructure = (frontmatter, addIssue) =>
|
|
281
284
|
validateFrontmatterStructure(frontmatter, addIssue, {
|
|
282
285
|
knownTopLevelFrontmatterKeys: knownThemeTopLevelFrontmatterKeys,
|
|
283
286
|
fileKind: 'theme',
|
|
284
287
|
});
|
|
285
288
|
|
|
286
|
-
export const
|
|
289
|
+
export const validateRouteThemeYamlStructure = (frontmatter, addIssue) =>
|
|
287
290
|
validateFrontmatterStructure(frontmatter, addIssue, {
|
|
288
291
|
knownTopLevelFrontmatterKeys: knownRouteThemeTopLevelFrontmatterKeys,
|
|
289
292
|
fileKind: 'route theme',
|
|
290
293
|
});
|
|
291
294
|
|
|
292
|
-
export const
|
|
295
|
+
export const validateSitewideYamlStructure = (frontmatter, addIssue) =>
|
|
293
296
|
validateFrontmatterStructure(frontmatter, addIssue, {
|
|
294
297
|
knownTopLevelFrontmatterKeys: knownSitewideTopLevelFrontmatterKeys,
|
|
295
298
|
fileKind: 'sitewide content',
|
|
296
299
|
});
|
|
297
300
|
|
|
298
|
-
export const readThemeFile = async (sitePath) =>
|
|
301
|
+
export const readThemeFile = async (sitePath) => {
|
|
302
|
+
const source = await readFile(sitePath, 'utf8');
|
|
303
|
+
return { frontmatter: source, frontmatterBody: source, body: '' };
|
|
304
|
+
};
|
|
299
305
|
|
|
300
306
|
export const getFrontmatterSections = (frontmatter) => {
|
|
301
307
|
const sections = [];
|
|
@@ -22,10 +22,7 @@ export const invocationRoot = configuredInvocationRoot
|
|
|
22
22
|
|
|
23
23
|
const hasSiteFilesInDirectory = (siteDir) => {
|
|
24
24
|
return (
|
|
25
|
-
(
|
|
26
|
-
existsSync(path.join(siteDir, 'config.md'))
|
|
27
|
-
|| existsSync(path.join(siteDir, 'config.mjs'))
|
|
28
|
-
)
|
|
25
|
+
existsSync(path.join(siteDir, 'config.yaml'))
|
|
29
26
|
&& existsSync(path.join(siteDir, 'content.md'))
|
|
30
27
|
);
|
|
31
28
|
};
|
|
@@ -117,11 +114,10 @@ const getPathLabel = (filePath) => {
|
|
|
117
114
|
};
|
|
118
115
|
|
|
119
116
|
export const siteDir = resolvedSitePaths.siteDir;
|
|
120
|
-
export const siteConfigPath = path.join(siteDir, 'config.
|
|
121
|
-
export const
|
|
122
|
-
export const siteThemePath = path.join(siteDir, 'theme.md');
|
|
117
|
+
export const siteConfigPath = path.join(siteDir, 'config.yaml');
|
|
118
|
+
export const siteThemePath = path.join(siteDir, 'theme.yaml');
|
|
123
119
|
export const siteContentPath = path.join(siteDir, 'content.md');
|
|
124
|
-
export const sitewideContentPath = path.join(siteDir, 'sitewide-content.
|
|
120
|
+
export const sitewideContentPath = path.join(siteDir, 'sitewide-content.yaml');
|
|
125
121
|
export const siteImagesDir = path.join(siteDir, 'images');
|
|
126
122
|
export const siteRoutesDir = path.join(siteDir, 'routes');
|
|
127
123
|
export const sitePublicDir = path.join(siteDir, 'public');
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises';
|
|
2
|
+
import { sitewideSchema } from './schema-definitions.mjs';
|
|
3
|
+
import {
|
|
4
|
+
sitewideContentLabel,
|
|
5
|
+
sitewideContentPath,
|
|
6
|
+
} from './site-paths.mjs';
|
|
7
|
+
import {
|
|
8
|
+
validateSitewideYamlStructure,
|
|
9
|
+
} from './site-content.mjs';
|
|
10
|
+
import { parseYamlConfig } from './yaml-config.mjs';
|
|
11
|
+
|
|
12
|
+
export const readSitewideContent = async () => {
|
|
13
|
+
const source = await readFile(sitewideContentPath, 'utf8').catch((error) => {
|
|
14
|
+
if (error?.code === 'ENOENT') return null;
|
|
15
|
+
throw error;
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
if (!source) return {};
|
|
19
|
+
|
|
20
|
+
return parseYamlConfig(source, sitewideContentLabel, {
|
|
21
|
+
schema: sitewideSchema,
|
|
22
|
+
validateStructure: validateSitewideYamlStructure,
|
|
23
|
+
});
|
|
24
|
+
};
|
|
@@ -1,11 +1,8 @@
|
|
|
1
1
|
import { readFile, readdir } from 'node:fs/promises';
|
|
2
2
|
import path from 'node:path';
|
|
3
|
-
import { parseYamlMapping } from './frontmatter-yaml.mjs';
|
|
4
3
|
import {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
validateRouteThemeFrontmatterStructure,
|
|
8
|
-
validateThemeFrontmatterStructure,
|
|
4
|
+
validateRouteThemeYamlStructure,
|
|
5
|
+
validateThemeYamlStructure,
|
|
9
6
|
} from './site-content.mjs';
|
|
10
7
|
import {
|
|
11
8
|
siteRoutesDir,
|
|
@@ -14,7 +11,9 @@ import {
|
|
|
14
11
|
siteThemePath,
|
|
15
12
|
sitewideContentLabel,
|
|
16
13
|
} from './site-paths.mjs';
|
|
14
|
+
import { resolveThemePresentation } from './presentation.mjs';
|
|
17
15
|
import { resolveThemeConfig } from './theme-presets.mjs';
|
|
16
|
+
import { parseYamlConfig } from './yaml-config.mjs';
|
|
18
17
|
|
|
19
18
|
export const readThemeConfig = async () => {
|
|
20
19
|
const themeFile = await readFile(siteThemePath, 'utf8').catch((error) => {
|
|
@@ -24,24 +23,14 @@ export const readThemeConfig = async () => {
|
|
|
24
23
|
|
|
25
24
|
throw error;
|
|
26
25
|
});
|
|
27
|
-
const
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
validateFrontmatterIndentation(frontmatter, (issue) => issues.push(issue));
|
|
31
|
-
validateThemeFrontmatterStructure(frontmatter, (issue) => issues.push(issue));
|
|
32
|
-
|
|
33
|
-
if (issues.length > 0) {
|
|
34
|
-
throw new Error([
|
|
35
|
-
`${siteThemeLabel} has invalid frontmatter.`,
|
|
36
|
-
...issues.map((issue) => `- ${issue.message}`),
|
|
37
|
-
].join('\n'));
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
const config = parseYamlMapping(frontmatterBody);
|
|
26
|
+
const config = parseYamlConfig(themeFile, siteThemeLabel, {
|
|
27
|
+
validateStructure: validateThemeYamlStructure,
|
|
28
|
+
});
|
|
41
29
|
if (Object.hasOwn(config, 'navigation')) {
|
|
42
|
-
throw new Error(`${siteThemeLabel} may not define navigation.
|
|
30
|
+
throw new Error(`${siteThemeLabel} may not define navigation. The site label and logo settings belong in ${sitewideContentLabel}.`);
|
|
43
31
|
}
|
|
44
32
|
resolveThemeConfig(config, siteThemeLabel);
|
|
33
|
+
resolveThemePresentation(config, siteThemeLabel);
|
|
45
34
|
|
|
46
35
|
return config;
|
|
47
36
|
};
|
|
@@ -61,7 +50,7 @@ const getRouteThemeFiles = async (directory, relativeDirectory = '') => {
|
|
|
61
50
|
continue;
|
|
62
51
|
}
|
|
63
52
|
|
|
64
|
-
if (entry.name === 'theme.
|
|
53
|
+
if (entry.name === 'theme.yaml') {
|
|
65
54
|
files.push({
|
|
66
55
|
path: absolutePath,
|
|
67
56
|
label: `${siteRoutesLabel}/${relativePath.split(path.sep).join('/')}`,
|
|
@@ -78,23 +67,14 @@ export const validateRouteThemeFiles = async () => {
|
|
|
78
67
|
|
|
79
68
|
for (const file of files) {
|
|
80
69
|
const source = await readFile(file.path, 'utf8');
|
|
81
|
-
const
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
validateRouteThemeFrontmatterStructure(frontmatter, (issue) => issues.push(issue));
|
|
85
|
-
|
|
86
|
-
if (issues.length > 0) {
|
|
87
|
-
throw new Error([
|
|
88
|
-
`${file.label} has invalid frontmatter.`,
|
|
89
|
-
...issues.map((issue) => `- ${issue.message}`),
|
|
90
|
-
].join('\n'));
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
const config = parseYamlMapping(frontmatterBody);
|
|
70
|
+
const config = parseYamlConfig(source, file.label, {
|
|
71
|
+
validateStructure: validateRouteThemeYamlStructure,
|
|
72
|
+
});
|
|
94
73
|
if (Object.hasOwn(config, 'navigation')) {
|
|
95
|
-
throw new Error(`${file.label} may not define navigation.
|
|
74
|
+
throw new Error(`${file.label} may not define navigation. The site label and logo settings belong in ${sitewideContentLabel}.`);
|
|
96
75
|
}
|
|
97
76
|
resolveThemeConfig(config, file.label);
|
|
77
|
+
resolveThemePresentation(config, file.label);
|
|
98
78
|
|
|
99
79
|
configs.push({ ...file, config });
|
|
100
80
|
}
|
|
@@ -1,13 +1,6 @@
|
|
|
1
1
|
import { mergeDeep } from './typography.mjs';
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
'portfolio',
|
|
5
|
-
'documentation',
|
|
6
|
-
'project',
|
|
7
|
-
'statement',
|
|
8
|
-
];
|
|
9
|
-
|
|
10
|
-
export const themePresets = Object.freeze({
|
|
3
|
+
const themePresetConfigurations = Object.freeze({
|
|
11
4
|
portfolio: Object.freeze({
|
|
12
5
|
layout: {
|
|
13
6
|
density: 'normal',
|
|
@@ -17,23 +10,18 @@ export const themePresets = Object.freeze({
|
|
|
17
10
|
mobile: '1rem',
|
|
18
11
|
},
|
|
19
12
|
},
|
|
20
|
-
|
|
13
|
+
images: {
|
|
21
14
|
width: '1000px',
|
|
22
15
|
maxAvailableWidthPercent: { desktop: 100, mobile: 100 },
|
|
23
16
|
maxAvailableHeightPercent: { desktop: 78, mobile: 68 },
|
|
24
17
|
},
|
|
25
18
|
typography: {
|
|
26
19
|
fontFamily: "'Helvetica Neue', Arial, sans-serif",
|
|
27
|
-
|
|
20
|
+
profile: 'restrained',
|
|
28
21
|
rhythm: 'normal',
|
|
29
22
|
},
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
sectionSurfaces: {
|
|
33
|
-
mode: 'none',
|
|
34
|
-
sequence: ['base', 'soft', 'emphasis'],
|
|
35
|
-
},
|
|
36
|
-
},
|
|
23
|
+
palette: 'dark',
|
|
24
|
+
sectionSurfaces: ['base'],
|
|
37
25
|
}),
|
|
38
26
|
documentation: Object.freeze({
|
|
39
27
|
layout: {
|
|
@@ -44,23 +32,18 @@ export const themePresets = Object.freeze({
|
|
|
44
32
|
mobile: '1rem',
|
|
45
33
|
},
|
|
46
34
|
},
|
|
47
|
-
|
|
35
|
+
images: {
|
|
48
36
|
width: '920px',
|
|
49
37
|
maxAvailableWidthPercent: { desktop: 100, mobile: 100 },
|
|
50
38
|
maxAvailableHeightPercent: { desktop: 74, mobile: 68 },
|
|
51
39
|
},
|
|
52
40
|
typography: {
|
|
53
41
|
fontFamily: "Georgia, 'Times New Roman', serif",
|
|
54
|
-
|
|
42
|
+
profile: 'reading',
|
|
55
43
|
rhythm: 'compact',
|
|
56
44
|
},
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
sectionSurfaces: {
|
|
60
|
-
mode: 'cycle',
|
|
61
|
-
sequence: ['base', 'soft'],
|
|
62
|
-
},
|
|
63
|
-
},
|
|
45
|
+
palette: 'paper',
|
|
46
|
+
sectionSurfaces: ['base', 'soft'],
|
|
64
47
|
}),
|
|
65
48
|
project: Object.freeze({
|
|
66
49
|
layout: {
|
|
@@ -71,23 +54,18 @@ export const themePresets = Object.freeze({
|
|
|
71
54
|
mobile: '1rem',
|
|
72
55
|
},
|
|
73
56
|
},
|
|
74
|
-
|
|
57
|
+
images: {
|
|
75
58
|
width: '840px',
|
|
76
59
|
maxAvailableWidthPercent: { desktop: 100, mobile: 100 },
|
|
77
60
|
maxAvailableHeightPercent: { desktop: 70, mobile: 62 },
|
|
78
61
|
},
|
|
79
62
|
typography: {
|
|
80
63
|
fontFamily: "system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif",
|
|
81
|
-
|
|
64
|
+
profile: 'reading',
|
|
82
65
|
rhythm: 'compact',
|
|
83
66
|
},
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
sectionSurfaces: {
|
|
87
|
-
mode: 'cycle',
|
|
88
|
-
sequence: ['base', 'soft'],
|
|
89
|
-
},
|
|
90
|
-
},
|
|
67
|
+
palette: 'light',
|
|
68
|
+
sectionSurfaces: ['base', 'soft'],
|
|
91
69
|
}),
|
|
92
70
|
statement: Object.freeze({
|
|
93
71
|
layout: {
|
|
@@ -98,27 +76,62 @@ export const themePresets = Object.freeze({
|
|
|
98
76
|
mobile: '1rem',
|
|
99
77
|
},
|
|
100
78
|
},
|
|
101
|
-
|
|
79
|
+
images: {
|
|
102
80
|
width: '1080px',
|
|
103
81
|
maxAvailableWidthPercent: { desktop: 100, mobile: 100 },
|
|
104
82
|
maxAvailableHeightPercent: { desktop: 80, mobile: 70 },
|
|
105
83
|
},
|
|
106
84
|
typography: {
|
|
107
85
|
fontFamily: "'Trebuchet MS', 'Helvetica Neue', Arial, sans-serif",
|
|
108
|
-
|
|
86
|
+
profile: 'statement',
|
|
109
87
|
rhythm: 'airy',
|
|
110
88
|
},
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
89
|
+
palette: 'paper',
|
|
90
|
+
sectionSurfaces: ['base', 'emphasis'],
|
|
91
|
+
}),
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
export const themePresetDefinitions = Object.freeze({
|
|
95
|
+
portfolio: Object.freeze({
|
|
96
|
+
title: 'Portfolio',
|
|
97
|
+
description: 'For portfolios and image-led sites, with restrained typography and generous space for images.',
|
|
98
|
+
theme: themePresetConfigurations.portfolio,
|
|
99
|
+
}),
|
|
100
|
+
documentation: Object.freeze({
|
|
101
|
+
title: 'Documentation',
|
|
102
|
+
description: 'For guides and reference material, with reading-focused typography and compact spacing.',
|
|
103
|
+
theme: themePresetConfigurations.documentation,
|
|
104
|
+
}),
|
|
105
|
+
project: Object.freeze({
|
|
106
|
+
title: 'Project',
|
|
107
|
+
description: 'For project and product sites that balance explanation, code, cards, and images.',
|
|
108
|
+
theme: themePresetConfigurations.project,
|
|
109
|
+
}),
|
|
110
|
+
statement: Object.freeze({
|
|
111
|
+
title: 'Statement',
|
|
112
|
+
description: 'For short, expressive sites, with larger typography, airy spacing, and stronger section emphasis.',
|
|
113
|
+
theme: themePresetConfigurations.statement,
|
|
118
114
|
}),
|
|
119
115
|
});
|
|
120
116
|
|
|
121
|
-
export const
|
|
117
|
+
export const themePresetNames = Object.freeze(Object.keys(themePresetDefinitions));
|
|
118
|
+
|
|
119
|
+
export const themePresets = Object.freeze(Object.fromEntries(
|
|
120
|
+
themePresetNames.map((name) => [name, themePresetDefinitions[name].theme]),
|
|
121
|
+
));
|
|
122
|
+
|
|
123
|
+
export const getThemePresetMetadata = (presetName) => {
|
|
124
|
+
const definition = themePresetDefinitions[presetName];
|
|
125
|
+
if (!definition) return undefined;
|
|
126
|
+
|
|
127
|
+
return {
|
|
128
|
+
name: presetName,
|
|
129
|
+
title: definition.title,
|
|
130
|
+
description: definition.description,
|
|
131
|
+
};
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
export const getThemePreset = (presetName, sourceLabel = 'theme.yaml') => {
|
|
122
135
|
const preset = themePresets[presetName];
|
|
123
136
|
|
|
124
137
|
if (!preset) {
|
|
@@ -128,7 +141,7 @@ export const getThemePreset = (presetName, sourceLabel = 'theme.md') => {
|
|
|
128
141
|
return structuredClone(preset);
|
|
129
142
|
};
|
|
130
143
|
|
|
131
|
-
export const resolveThemeConfig = (theme = {}, sourceLabel = 'theme.
|
|
144
|
+
export const resolveThemeConfig = (theme = {}, sourceLabel = 'theme.yaml') => {
|
|
132
145
|
const presetName = theme?.preset;
|
|
133
146
|
if (presetName === undefined) return structuredClone(theme ?? {});
|
|
134
147
|
|
|
@@ -151,15 +164,16 @@ const responsiveValueLines = (label, value, indent = 2) => {
|
|
|
151
164
|
];
|
|
152
165
|
};
|
|
153
166
|
|
|
154
|
-
export const renderThemePresetReference = (presetName, sourceLabel = 'theme.
|
|
167
|
+
export const renderThemePresetReference = (presetName, sourceLabel = 'theme.yaml') => {
|
|
155
168
|
const preset = getThemePreset(presetName, sourceLabel);
|
|
156
|
-
const
|
|
169
|
+
const metadata = getThemePresetMetadata(presetName);
|
|
170
|
+
const { layout, images, typography, palette, sectionSurfaces } = preset;
|
|
157
171
|
|
|
158
172
|
return [
|
|
159
|
-
'---',
|
|
160
173
|
`# Original values for Norna's "${presetName}" theme preset.`,
|
|
161
|
-
|
|
162
|
-
'#
|
|
174
|
+
`# ${metadata.description}`,
|
|
175
|
+
'# This is a reference file. Norna only loads theme.yaml.',
|
|
176
|
+
'# Keep the preset in theme.yaml and copy only the values you want to override.',
|
|
163
177
|
`# Available theme presets: ${themePresetNames.join(', ')}.`,
|
|
164
178
|
`preset: ${presetName}`,
|
|
165
179
|
'',
|
|
@@ -174,18 +188,18 @@ export const renderThemePresetReference = (presetName, sourceLabel = 'theme.md')
|
|
|
174
188
|
' # Optional spacing overrides use blockGap, finalSectionBottom,',
|
|
175
189
|
' # firstSectionTop, headingToBlock, imageGap, and sectionGap.',
|
|
176
190
|
'',
|
|
177
|
-
'
|
|
191
|
+
'images:',
|
|
178
192
|
' # width accepts a positive CSS length.',
|
|
179
|
-
` width: ${
|
|
193
|
+
` width: ${images.width}`,
|
|
180
194
|
' # Percent values must be greater than 0 and at most 100.',
|
|
181
|
-
...responsiveValueLines('maxAvailableWidthPercent',
|
|
182
|
-
...responsiveValueLines('maxAvailableHeightPercent',
|
|
195
|
+
...responsiveValueLines('maxAvailableWidthPercent', images.maxAvailableWidthPercent),
|
|
196
|
+
...responsiveValueLines('maxAvailableHeightPercent', images.maxAvailableHeightPercent),
|
|
183
197
|
'',
|
|
184
198
|
'typography:',
|
|
185
199
|
' # Any valid CSS font-family stack without semicolons.',
|
|
186
200
|
` fontFamily: ${quote(typography.fontFamily)}`,
|
|
187
|
-
' # Alternatives:
|
|
188
|
-
`
|
|
201
|
+
' # Alternatives: restrained, dense, reading, statement.',
|
|
202
|
+
` profile: ${typography.profile}`,
|
|
189
203
|
' # Alternatives: compact, normal, airy.',
|
|
190
204
|
` rhythm: ${typography.rhythm}`,
|
|
191
205
|
' # Fine-grained overrides can target headings.h1-h4, body, and caption.',
|
|
@@ -195,15 +209,11 @@ export const renderThemePresetReference = (presetName, sourceLabel = 'theme.md')
|
|
|
195
209
|
' # h2:',
|
|
196
210
|
' # size: large # small, medium, large, or xlarge',
|
|
197
211
|
'',
|
|
198
|
-
'
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
'
|
|
202
|
-
|
|
203
|
-
` mode: ${presentation.sectionSurfaces.mode}`,
|
|
204
|
-
' # Use one to three unique values from: base, soft, emphasis.',
|
|
205
|
-
` sequence: [${presentation.sectionSurfaces.sequence.join(', ')}]`,
|
|
206
|
-
'---',
|
|
212
|
+
'# Alternatives: dark, light, paper.',
|
|
213
|
+
`palette: ${palette}`,
|
|
214
|
+
'# One value keeps every section on the same surface; multiple values cycle.',
|
|
215
|
+
'# Use one to three unique values from: base, soft, emphasis.',
|
|
216
|
+
`sectionSurfaces: [${sectionSurfaces.join(', ')}]`,
|
|
207
217
|
'',
|
|
208
218
|
].join('\n');
|
|
209
219
|
};
|