@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
|
@@ -3,10 +3,12 @@ import { spawnSync } from 'node:child_process';
|
|
|
3
3
|
import { mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises';
|
|
4
4
|
import path from 'node:path';
|
|
5
5
|
import { fileURLToPath } from 'node:url';
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
6
|
+
import { load } from 'js-yaml';
|
|
7
|
+
import { resolveThemePresentation } from './lib/presentation.mjs';
|
|
8
8
|
import {
|
|
9
|
+
getThemePresetMetadata,
|
|
9
10
|
resolveThemeConfig,
|
|
11
|
+
themePresetDefinitions,
|
|
10
12
|
themePresetNames,
|
|
11
13
|
themePresets,
|
|
12
14
|
} from './lib/theme-presets.mjs';
|
|
@@ -25,36 +27,68 @@ const runCli = (args) => spawnSync(process.execPath, [cliPath, '--site-dir', sit
|
|
|
25
27
|
|
|
26
28
|
try {
|
|
27
29
|
assert.deepEqual(Object.keys(themePresets), themePresetNames);
|
|
30
|
+
assert.deepEqual(Object.keys(themePresetDefinitions), themePresetNames);
|
|
28
31
|
for (const presetName of themePresetNames) {
|
|
32
|
+
const metadata = getThemePresetMetadata(presetName);
|
|
33
|
+
assert.equal(metadata.name, presetName);
|
|
34
|
+
assert.ok(metadata.title);
|
|
35
|
+
assert.ok(metadata.description);
|
|
29
36
|
const resolved = resolveThemeConfig({ preset: presetName }, 'test theme');
|
|
30
37
|
assert.equal(resolved.preset, presetName);
|
|
31
38
|
assert.ok(resolved.layout?.density);
|
|
32
|
-
assert.ok(resolved.
|
|
39
|
+
assert.ok(resolved.images?.width);
|
|
33
40
|
assert.ok(resolved.typography?.fontFamily);
|
|
34
|
-
assert.ok(resolved.typography?.
|
|
35
|
-
assert.ok(resolved.
|
|
41
|
+
assert.ok(resolved.typography?.profile);
|
|
42
|
+
assert.ok(resolved.palette);
|
|
43
|
+
assert.ok(resolved.sectionSurfaces);
|
|
36
44
|
}
|
|
37
45
|
|
|
38
46
|
const overridden = resolveThemeConfig({
|
|
39
47
|
preset: 'documentation',
|
|
40
48
|
layout: { pageWidth: '1300px' },
|
|
41
|
-
|
|
49
|
+
palette: 'dark',
|
|
42
50
|
}, 'test theme');
|
|
43
51
|
assert.equal(overridden.layout.density, 'compact');
|
|
44
52
|
assert.equal(overridden.layout.pageWidth, '1300px');
|
|
45
|
-
assert.equal(overridden.
|
|
46
|
-
assert.equal(overridden.
|
|
47
|
-
assert.deepEqual(overridden.
|
|
53
|
+
assert.equal(overridden.images.width, '920px');
|
|
54
|
+
assert.equal(overridden.palette, 'dark');
|
|
55
|
+
assert.deepEqual(overridden.sectionSurfaces, ['base', 'soft']);
|
|
48
56
|
assert.throws(
|
|
49
|
-
() => resolveThemeConfig({ preset: 'unknown' }, 'test/theme.
|
|
50
|
-
/Unknown theme preset "unknown" in test\/theme\.
|
|
57
|
+
() => resolveThemeConfig({ preset: 'unknown' }, 'test/theme.yaml'),
|
|
58
|
+
/Unknown theme preset "unknown" in test\/theme\.yaml.*portfolio, documentation, project, statement/,
|
|
59
|
+
);
|
|
60
|
+
assert.throws(
|
|
61
|
+
() => resolveThemePresentation({ sectionSurfaces: ['base', 'glowing'] }, 'test/theme.yaml'),
|
|
62
|
+
/Unknown section surface "glowing" in test\/theme\.yaml.*base, soft, emphasis/,
|
|
63
|
+
);
|
|
64
|
+
assert.throws(
|
|
65
|
+
() => resolveThemePresentation({ sectionSurfaces: ['base', 'base'] }, 'test/theme.yaml'),
|
|
66
|
+
/Each section surface may appear only once in test\/theme\.yaml/,
|
|
51
67
|
);
|
|
52
68
|
|
|
69
|
+
const listResult = runCli(['theme:presets']);
|
|
70
|
+
assert.equal(listResult.status, 0, listResult.stderr || listResult.stdout);
|
|
71
|
+
for (const presetName of themePresetNames) {
|
|
72
|
+
const metadata = getThemePresetMetadata(presetName);
|
|
73
|
+
assert.ok(listResult.stdout.includes(`${presetName}\n ${metadata.description}`));
|
|
74
|
+
}
|
|
75
|
+
const invalidListResult = runCli(['theme:presets', 'documentation']);
|
|
76
|
+
assert.notEqual(invalidListResult.status, 0);
|
|
77
|
+
assert.match(invalidListResult.stderr, /Usage: norna theme:presets/);
|
|
78
|
+
|
|
79
|
+
const themeSchema = JSON.parse(await readFile(path.join(repoRoot, 'schemas', 'theme.schema.json'), 'utf8'));
|
|
80
|
+
assert.deepEqual(
|
|
81
|
+
themeSchema.properties.preset.oneOf.map((entry) => entry.const),
|
|
82
|
+
themePresetNames,
|
|
83
|
+
);
|
|
84
|
+
for (const entry of themeSchema.properties.preset.oneOf) {
|
|
85
|
+
const metadata = getThemePresetMetadata(entry.const);
|
|
86
|
+
assert.equal(entry.title, metadata.title);
|
|
87
|
+
assert.equal(entry.description, metadata.description);
|
|
88
|
+
}
|
|
89
|
+
|
|
53
90
|
await mkdir(path.join(siteDir, 'routes', '010-guide'), { recursive: true });
|
|
54
|
-
await writeFile(path.join(siteDir, 'config.
|
|
55
|
-
url: https://example.com/
|
|
56
|
-
---
|
|
57
|
-
`);
|
|
91
|
+
await writeFile(path.join(siteDir, 'config.yaml'), 'url: https://example.com/\n');
|
|
58
92
|
await writeFile(path.join(siteDir, 'content.md'), `---
|
|
59
93
|
title: Theme preset test
|
|
60
94
|
description: Root page
|
|
@@ -64,15 +98,12 @@ description: Root page
|
|
|
64
98
|
|
|
65
99
|
Root content.
|
|
66
100
|
`);
|
|
67
|
-
await writeFile(path.join(siteDir, 'theme.
|
|
68
|
-
preset: documentation
|
|
101
|
+
await writeFile(path.join(siteDir, 'theme.yaml'), `preset: documentation
|
|
69
102
|
layout:
|
|
70
103
|
pageWidth: 1300px
|
|
71
|
-
|
|
72
|
-
palette: dark
|
|
73
|
-
---
|
|
104
|
+
palette: dark
|
|
74
105
|
`);
|
|
75
|
-
await writeFile(path.join(siteDir, 'routes', '010-guide', '
|
|
106
|
+
await writeFile(path.join(siteDir, 'routes', '010-guide', 'content.md'), `---
|
|
76
107
|
title: Guide
|
|
77
108
|
description: Route page
|
|
78
109
|
navigation:
|
|
@@ -83,13 +114,10 @@ navigation:
|
|
|
83
114
|
|
|
84
115
|
Route content.
|
|
85
116
|
`);
|
|
86
|
-
await writeFile(path.join(siteDir, 'routes', '010-guide', 'theme.
|
|
87
|
-
preset: portfolio
|
|
117
|
+
await writeFile(path.join(siteDir, 'routes', '010-guide', 'theme.yaml'), `preset: portfolio
|
|
88
118
|
layout:
|
|
89
119
|
pageWidth: 1010px
|
|
90
|
-
|
|
91
|
-
palette: light
|
|
92
|
-
---
|
|
120
|
+
palette: light
|
|
93
121
|
`);
|
|
94
122
|
|
|
95
123
|
const buildResult = runCli(['build']);
|
|
@@ -105,31 +133,30 @@ presentation:
|
|
|
105
133
|
|
|
106
134
|
const typographyResult = runCli(['typography', 'show']);
|
|
107
135
|
assert.equal(typographyResult.status, 0, typographyResult.stderr || typographyResult.stdout);
|
|
108
|
-
assert.match(typographyResult.stdout, /value:
|
|
109
|
-
assert.match(typographyResult.stdout, /value:
|
|
136
|
+
assert.match(typographyResult.stdout, /value: reading/);
|
|
137
|
+
assert.match(typographyResult.stdout, /value: restrained/);
|
|
110
138
|
|
|
111
|
-
const routeThemePath = path.join(siteDir, 'routes', '010-guide', 'theme.
|
|
139
|
+
const routeThemePath = path.join(siteDir, 'routes', '010-guide', 'theme.yaml');
|
|
112
140
|
const routeThemeSource = await readFile(routeThemePath, 'utf8');
|
|
113
|
-
await writeFile(routeThemePath,
|
|
141
|
+
await writeFile(routeThemePath, 'preset: unknown\n');
|
|
114
142
|
const invalidRoutePresetResult = runCli(['config:check']);
|
|
115
143
|
assert.notEqual(invalidRoutePresetResult.status, 0);
|
|
116
|
-
assert.match(invalidRoutePresetResult.stderr, /Unknown theme preset "unknown" in .*routes\/010-guide\/theme\.
|
|
144
|
+
assert.match(invalidRoutePresetResult.stderr, /Unknown theme preset "unknown" in .*routes\/010-guide\/theme\.yaml/);
|
|
117
145
|
await writeFile(routeThemePath, routeThemeSource);
|
|
118
146
|
|
|
119
147
|
const exportResult = runCli(['theme:export', 'documentation']);
|
|
120
148
|
assert.equal(exportResult.status, 0, exportResult.stderr || exportResult.stdout);
|
|
121
|
-
assert.match(exportResult.stdout, /orig-documentation-theme\.
|
|
122
|
-
const exportedPath = path.join(siteDir, 'orig-documentation-theme.
|
|
149
|
+
assert.match(exportResult.stdout, /orig-documentation-theme\.yaml/);
|
|
150
|
+
const exportedPath = path.join(siteDir, 'orig-documentation-theme.yaml');
|
|
123
151
|
const exportedSource = await readFile(exportedPath, 'utf8');
|
|
124
|
-
assert.match(exportedSource, /This is a reference file\. Norna only loads theme\.
|
|
152
|
+
assert.match(exportedSource, /This is a reference file\. Norna only loads theme\.yaml\./);
|
|
125
153
|
assert.match(exportedSource, /Available theme presets: portfolio, documentation, project, statement\./);
|
|
126
154
|
assert.match(exportedSource, /# Alternatives: dark, light, paper\./);
|
|
127
|
-
const
|
|
128
|
-
const exportedConfig = parseYamlMapping(frontmatterBody);
|
|
155
|
+
const exportedConfig = load(exportedSource);
|
|
129
156
|
assert.equal(exportedConfig.preset, 'documentation');
|
|
130
157
|
assert.equal(exportedConfig.layout.pageWidth, themePresets.documentation.layout.pageWidth);
|
|
131
158
|
assert.equal(exportedConfig.typography.fontFamily, themePresets.documentation.typography.fontFamily);
|
|
132
|
-
assert.deepEqual(exportedConfig.
|
|
159
|
+
assert.deepEqual(exportedConfig.sectionSurfaces, ['base', 'soft']);
|
|
133
160
|
|
|
134
161
|
const exportAgainResult = runCli(['theme:export', 'documentation']);
|
|
135
162
|
assert.notEqual(exportAgainResult.status, 0);
|
|
@@ -18,19 +18,16 @@ type CardListWidth = 'text' | 'narrow' | 'normal' | 'wide';
|
|
|
18
18
|
|
|
19
19
|
interface Props {
|
|
20
20
|
cards: Card[];
|
|
21
|
-
maxAvailableWidthPercent: {
|
|
22
|
-
desktop: number;
|
|
23
|
-
};
|
|
24
21
|
layout?: CardListLayout;
|
|
25
22
|
flow?: CardListFlow;
|
|
26
23
|
size?: CardListSize;
|
|
27
24
|
width?: CardListWidth;
|
|
28
25
|
}
|
|
29
26
|
|
|
30
|
-
const { cards,
|
|
27
|
+
const { cards, layout = 'image-top', flow = 'grid', size = 'm', width = 'normal' } = Astro.props;
|
|
31
28
|
const cardImageSizes = [
|
|
32
29
|
'(max-width: 700px) 100vw',
|
|
33
|
-
|
|
30
|
+
'min(20rem, 100vw)',
|
|
34
31
|
].join(', ');
|
|
35
32
|
const displayLink = (link: string) => withBasePath(projectConfig.site.basePath, link);
|
|
36
33
|
const cardListClasses = [
|
|
@@ -4,15 +4,15 @@ import { getImageAttributes } from '../lib/generatedImages';
|
|
|
4
4
|
import {
|
|
5
5
|
getCaptionId,
|
|
6
6
|
getCarouselAspectRatio,
|
|
7
|
-
|
|
7
|
+
getManagedImageSizes,
|
|
8
8
|
getImageClass,
|
|
9
9
|
getImageLoadingProps,
|
|
10
|
-
type
|
|
11
|
-
} from '../lib/
|
|
10
|
+
type ManagedImage,
|
|
11
|
+
} from '../lib/imagePresentation';
|
|
12
12
|
|
|
13
13
|
interface Props {
|
|
14
14
|
title: string;
|
|
15
|
-
images:
|
|
15
|
+
images: ManagedImage[];
|
|
16
16
|
maxAvailableWidthPercent: {
|
|
17
17
|
desktop: number;
|
|
18
18
|
mobile: number;
|
|
@@ -21,16 +21,16 @@ interface Props {
|
|
|
21
21
|
}
|
|
22
22
|
|
|
23
23
|
const { title, images, maxAvailableWidthPercent, priorityFirstImage = false } = Astro.props;
|
|
24
|
-
const
|
|
24
|
+
const managedImageSizes = getManagedImageSizes(maxAvailableWidthPercent);
|
|
25
25
|
const carouselAspectRatio = getCarouselAspectRatio(images);
|
|
26
26
|
const carouselStyle = carouselAspectRatio
|
|
27
|
-
? `--
|
|
27
|
+
? `--image-carousel-aspect-ratio: ${carouselAspectRatio}`
|
|
28
28
|
: undefined;
|
|
29
29
|
---
|
|
30
30
|
|
|
31
|
-
<div class="
|
|
31
|
+
<div class="managed-images" aria-label={`${projectConfig.locale.labels.images}: ${title}`}>
|
|
32
32
|
<section
|
|
33
|
-
class="
|
|
33
|
+
class="managed-image-item image-carousel"
|
|
34
34
|
data-carousel
|
|
35
35
|
tabindex="0"
|
|
36
36
|
role="group"
|
|
@@ -38,18 +38,18 @@ const carouselStyle = carouselAspectRatio
|
|
|
38
38
|
aria-label={`${title}, bildkarusell`}
|
|
39
39
|
style={carouselStyle}
|
|
40
40
|
>
|
|
41
|
-
<div class="
|
|
42
|
-
<div class="
|
|
43
|
-
<div class="
|
|
41
|
+
<div class="image-carousel-stage">
|
|
42
|
+
<div class="image-carousel-viewport" data-carousel-viewport>
|
|
43
|
+
<div class="image-carousel-slides">
|
|
44
44
|
{images.map((image, imageIndex) => {
|
|
45
45
|
const captionId = image.caption ? getCaptionId(image, 0, imageIndex) : undefined;
|
|
46
46
|
|
|
47
47
|
return (
|
|
48
|
-
<figure class="
|
|
49
|
-
<div class="
|
|
48
|
+
<figure class="image-carousel-slide" data-carousel-slide>
|
|
49
|
+
<div class="image-carousel-frame">
|
|
50
50
|
<img
|
|
51
|
-
{...getImageAttributes(image.src ?? image.image,
|
|
52
|
-
class:list={['
|
|
51
|
+
{...getImageAttributes(image.src ?? image.image, managedImageSizes)}
|
|
52
|
+
class:list={['managed-image', getImageClass(image)]}
|
|
53
53
|
alt={image.alt ?? ''}
|
|
54
54
|
aria-describedby={captionId}
|
|
55
55
|
{...getImageLoadingProps(priorityFirstImage && imageIndex === 0)}
|
|
@@ -61,7 +61,7 @@ const carouselStyle = carouselAspectRatio
|
|
|
61
61
|
</div>
|
|
62
62
|
</div>
|
|
63
63
|
<button
|
|
64
|
-
class="
|
|
64
|
+
class="image-carousel-button image-carousel-button-previous"
|
|
65
65
|
type="button"
|
|
66
66
|
data-carousel-previous
|
|
67
67
|
aria-label="Föregående bild"
|
|
@@ -70,7 +70,7 @@ const carouselStyle = carouselAspectRatio
|
|
|
70
70
|
<span aria-hidden="true">‹</span>
|
|
71
71
|
</button>
|
|
72
72
|
<button
|
|
73
|
-
class="
|
|
73
|
+
class="image-carousel-button image-carousel-button-next"
|
|
74
74
|
type="button"
|
|
75
75
|
data-carousel-next
|
|
76
76
|
aria-label="Nästa bild"
|
|
@@ -78,20 +78,20 @@ const carouselStyle = carouselAspectRatio
|
|
|
78
78
|
>
|
|
79
79
|
<span aria-hidden="true">›</span>
|
|
80
80
|
</button>
|
|
81
|
-
<output class="
|
|
81
|
+
<output class="image-carousel-position" data-carousel-position aria-live="polite">
|
|
82
82
|
1 / {images.length}
|
|
83
83
|
</output>
|
|
84
84
|
</div>
|
|
85
|
-
<div class="
|
|
85
|
+
<div class="image-carousel-captions" data-carousel-captions>
|
|
86
86
|
{images.map((image, imageIndex) => (
|
|
87
87
|
image.caption && (
|
|
88
88
|
<p
|
|
89
|
-
class="
|
|
89
|
+
class="image-meta image-carousel-caption"
|
|
90
90
|
id={getCaptionId(image, 0, imageIndex)}
|
|
91
91
|
data-carousel-caption
|
|
92
92
|
data-carousel-caption-index={imageIndex}
|
|
93
93
|
>
|
|
94
|
-
<span class="
|
|
94
|
+
<span class="image-details">
|
|
95
95
|
<span>{image.caption}</span>
|
|
96
96
|
</span>
|
|
97
97
|
</p>
|
|
@@ -3,15 +3,15 @@ import projectConfig from '../../scripts/lib/project-config.mjs';
|
|
|
3
3
|
import { getImageAttributes } from '../lib/generatedImages';
|
|
4
4
|
import {
|
|
5
5
|
getCaptionId,
|
|
6
|
-
|
|
6
|
+
getManagedImageSizes,
|
|
7
7
|
getImageClass,
|
|
8
8
|
getImageLoadingProps,
|
|
9
|
-
type
|
|
10
|
-
} from '../lib/
|
|
9
|
+
type ManagedImage,
|
|
10
|
+
} from '../lib/imagePresentation';
|
|
11
11
|
|
|
12
12
|
interface Props {
|
|
13
13
|
title: string;
|
|
14
|
-
images:
|
|
14
|
+
images: ManagedImage[];
|
|
15
15
|
maxAvailableWidthPercent: {
|
|
16
16
|
desktop: number;
|
|
17
17
|
mobile: number;
|
|
@@ -20,28 +20,28 @@ interface Props {
|
|
|
20
20
|
}
|
|
21
21
|
|
|
22
22
|
const { title, images, maxAvailableWidthPercent, priorityFirstImage = false } = Astro.props;
|
|
23
|
-
const
|
|
23
|
+
const managedImageSizes = getManagedImageSizes(maxAvailableWidthPercent);
|
|
24
24
|
---
|
|
25
25
|
|
|
26
|
-
<div class="
|
|
26
|
+
<div class="image-stack" aria-label={`${projectConfig.locale.labels.images}: ${title}`}>
|
|
27
27
|
{images.map((image, itemIndex) => {
|
|
28
28
|
const isPriorityItem = priorityFirstImage && itemIndex === 0;
|
|
29
29
|
const captionId = image.caption ? getCaptionId(image, itemIndex) : undefined;
|
|
30
30
|
|
|
31
31
|
return (
|
|
32
|
-
<figure class="
|
|
33
|
-
<div class="
|
|
32
|
+
<figure class="managed-image-item">
|
|
33
|
+
<div class="managed-image-frame">
|
|
34
34
|
<img
|
|
35
|
-
{...getImageAttributes(image.src ?? image.image,
|
|
36
|
-
class:list={['
|
|
35
|
+
{...getImageAttributes(image.src ?? image.image, managedImageSizes)}
|
|
36
|
+
class:list={['managed-image', getImageClass(image)]}
|
|
37
37
|
alt={image.alt ?? ''}
|
|
38
38
|
aria-describedby={captionId}
|
|
39
39
|
{...getImageLoadingProps(isPriorityItem)}
|
|
40
40
|
/>
|
|
41
41
|
</div>
|
|
42
42
|
{image.caption && (
|
|
43
|
-
<figcaption class="
|
|
44
|
-
<div class="
|
|
43
|
+
<figcaption class="image-meta" id={captionId}>
|
|
44
|
+
<div class="image-details">
|
|
45
45
|
<span>{image.caption}</span>
|
|
46
46
|
</div>
|
|
47
47
|
</figcaption>
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
<script>
|
|
2
|
+
const siteTop = document.querySelector<HTMLElement>('.site-top');
|
|
3
|
+
const mobileMenu = document.querySelector<HTMLDetailsElement>('.mobile-nav-menu');
|
|
4
|
+
const mobileSectionLinks = Array.from(document.querySelectorAll<HTMLAnchorElement>('.mobile-page-nav a'));
|
|
5
|
+
|
|
6
|
+
const setAnchorOffset = () => {
|
|
7
|
+
if (!siteTop) return;
|
|
8
|
+
|
|
9
|
+
const offset = Math.ceil(siteTop.getBoundingClientRect().height);
|
|
10
|
+
document.documentElement.style.setProperty('--site-top-anchor-offset', `${offset}px`);
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
mobileSectionLinks.forEach((link) => {
|
|
14
|
+
link.addEventListener('click', (event) => {
|
|
15
|
+
const linkUrl = new URL(link.href, window.location.href);
|
|
16
|
+
if (linkUrl.pathname !== window.location.pathname) return;
|
|
17
|
+
|
|
18
|
+
if (mobileMenu) mobileMenu.open = false;
|
|
19
|
+
if (event.detail > 0) link.blur();
|
|
20
|
+
});
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
setAnchorOffset();
|
|
24
|
+
|
|
25
|
+
if (siteTop && 'ResizeObserver' in window) {
|
|
26
|
+
new ResizeObserver(setAnchorOffset).observe(siteTop);
|
|
27
|
+
}
|
|
28
|
+
</script>
|
|
@@ -3,7 +3,7 @@ import projectConfig from '../../scripts/lib/project-config.mjs';
|
|
|
3
3
|
import { withBasePath } from '../lib/basePath';
|
|
4
4
|
import type { SectionNavigation } from '../lib/sectionContent';
|
|
5
5
|
import type { SitePage } from '../lib/sitePages';
|
|
6
|
-
import
|
|
6
|
+
import SectionNavigationScript from './SectionNavigationScript.astro';
|
|
7
7
|
|
|
8
8
|
type FrameColors = {
|
|
9
9
|
backgroundColor: string;
|
|
@@ -11,7 +11,7 @@ type FrameColors = {
|
|
|
11
11
|
};
|
|
12
12
|
|
|
13
13
|
interface Props {
|
|
14
|
-
|
|
14
|
+
navigationLabel: string;
|
|
15
15
|
logo?: {
|
|
16
16
|
src: string;
|
|
17
17
|
alt: string;
|
|
@@ -25,7 +25,7 @@ interface Props {
|
|
|
25
25
|
frameColors: FrameColors;
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
-
const {
|
|
28
|
+
const { navigationLabel, logo, currentPage, pageNavigation, frameColors } = Astro.props;
|
|
29
29
|
const siteTopBackgroundColor = frameColors.backgroundColor === 'transparent'
|
|
30
30
|
? 'var(--color-page)'
|
|
31
31
|
: frameColors.backgroundColor;
|
|
@@ -48,6 +48,7 @@ const showSinglePageSectionNavigation = Boolean(
|
|
|
48
48
|
const hasCurrentPageSectionMenu = Boolean(
|
|
49
49
|
showSiteNavigation && currentPageNavigation && hasSectionMenu(currentPageNavigation.sections),
|
|
50
50
|
);
|
|
51
|
+
const hasSectionNavigation = showSinglePageSectionNavigation || hasCurrentPageSectionMenu;
|
|
51
52
|
---
|
|
52
53
|
|
|
53
54
|
<a class="skip-link" href="#main-content">{projectConfig.locale.labels.skipToContent}</a>
|
|
@@ -59,7 +60,7 @@ const hasCurrentPageSectionMenu = Boolean(
|
|
|
59
60
|
{showNavigationRow && (
|
|
60
61
|
<div class="site-nav-row">
|
|
61
62
|
<a class="site-brand" href={pageHref(homePage)}>
|
|
62
|
-
{logo ? <img class="site-brand-logo" src={withBasePath(projectConfig.site.basePath, logo.src)} alt={logo.alt} /> :
|
|
63
|
+
{logo ? <img class="site-brand-logo" src={withBasePath(projectConfig.site.basePath, logo.src)} alt={logo.alt} /> : navigationLabel}
|
|
63
64
|
</a>
|
|
64
65
|
|
|
65
66
|
{showSiteNavigation && (
|
|
@@ -140,4 +141,4 @@ const hasCurrentPageSectionMenu = Boolean(
|
|
|
140
141
|
|
|
141
142
|
</header>
|
|
142
143
|
|
|
143
|
-
{
|
|
144
|
+
{hasSectionNavigation && <SectionNavigationScript />}
|
|
@@ -27,15 +27,15 @@ const siteDirectory = (process.env.NORNA_SITE_DIR ?? 'site').trim();
|
|
|
27
27
|
const sitePages = getSitePages(await getCollection('site'));
|
|
28
28
|
const currentPage = getSitePage(entry);
|
|
29
29
|
const siteContentLabel = entry.id.includes('-route-')
|
|
30
|
-
? `${siteDirectory || 'site'}/routes/${currentPage.routeDirectory}/
|
|
30
|
+
? `${siteDirectory || 'site'}/routes/${currentPage.routeDirectory}/content.md`
|
|
31
31
|
: `${siteDirectory || 'site'}/content.md`;
|
|
32
32
|
const pageContentPath = currentPage.routeDirectory
|
|
33
|
-
? path.join(siteRoutesDir, currentPage.routeDirectory, '
|
|
33
|
+
? path.join(siteRoutesDir, currentPage.routeDirectory, 'content.md')
|
|
34
34
|
: siteContentPath;
|
|
35
35
|
const rawMarkdown = (await readSiteFile(pageContentPath, siteContentLabel)).body;
|
|
36
36
|
const [siteTheme] = await getCollection('theme');
|
|
37
37
|
if (!siteTheme) {
|
|
38
|
-
throw new Error('site/theme.
|
|
38
|
+
throw new Error('site/theme.yaml is required. Add a root theme before building the site.');
|
|
39
39
|
}
|
|
40
40
|
const [sitewide] = await getCollection('sitewide');
|
|
41
41
|
const sitewideData = sitewide?.data ?? {};
|
|
@@ -46,15 +46,15 @@ const routeTheme = await getRouteTheme(currentPage.routeDirectory);
|
|
|
46
46
|
const themeData = routeTheme?.data ?? siteThemeData;
|
|
47
47
|
const themeLabel = routeTheme
|
|
48
48
|
? `${siteDirectory || 'site'}/${routeTheme.id}`
|
|
49
|
-
: `${siteDirectory || 'site'}
|
|
49
|
+
: `${siteDirectory || 'site'}/${siteTheme.id}`;
|
|
50
50
|
const visualTheme = resolveThemeVisualConfig(themeData, themeLabel);
|
|
51
|
-
const
|
|
51
|
+
const navigationLabel = sitewideData.navigation?.label ?? homePage.title;
|
|
52
52
|
const logoAsset = getLogoAsset();
|
|
53
53
|
const logoSettings = sitewideData.navigation?.logo;
|
|
54
54
|
const logo = logoAsset
|
|
55
55
|
? {
|
|
56
56
|
src: logoAsset.href,
|
|
57
|
-
alt:
|
|
57
|
+
alt: navigationLabel,
|
|
58
58
|
height: logoSettings?.height,
|
|
59
59
|
}
|
|
60
60
|
: undefined;
|
|
@@ -98,7 +98,7 @@ if (visibleSections.length === 0) {
|
|
|
98
98
|
footer={sitewideData.footer}
|
|
99
99
|
>
|
|
100
100
|
<SiteNavigation
|
|
101
|
-
|
|
101
|
+
navigationLabel={navigationLabel}
|
|
102
102
|
logo={logo}
|
|
103
103
|
currentPage={currentPage}
|
|
104
104
|
pageNavigation={pageNavigation}
|
|
@@ -114,7 +114,7 @@ if (visibleSections.length === 0) {
|
|
|
114
114
|
visualTheme={visualTheme}
|
|
115
115
|
sectionIndex={index}
|
|
116
116
|
headingLevel={index === 0 ? 1 : 2}
|
|
117
|
-
|
|
117
|
+
prioritizeFirstImage={index === 0}
|
|
118
118
|
/>
|
|
119
119
|
))}
|
|
120
120
|
</main>
|
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
import projectConfig from '../../scripts/lib/project-config.mjs';
|
|
3
3
|
import { resolveSectionSurface } from '../../scripts/lib/presentation.mjs';
|
|
4
4
|
import CardList from './CardList.astro';
|
|
5
|
-
import GalleryGrid from './GalleryGrid.astro';
|
|
6
5
|
import ImageCarousel from './ImageCarousel.astro';
|
|
6
|
+
import ImageStack from './ImageStack.astro';
|
|
7
7
|
import type { ResolvedSection } from '../lib/sectionContent';
|
|
8
8
|
|
|
9
9
|
type ResponsiveValue<T> = { desktop: T; mobile: T };
|
|
@@ -15,17 +15,14 @@ type ResolvedPagePresentation = {
|
|
|
15
15
|
palette: {
|
|
16
16
|
surfaces: Record<'base' | 'soft' | 'emphasis', { backgroundColor: string; textColor: string }>;
|
|
17
17
|
};
|
|
18
|
-
sectionSurfaces:
|
|
19
|
-
mode: 'none' | 'cycle';
|
|
20
|
-
sequence: Array<'base' | 'soft' | 'emphasis'>;
|
|
21
|
-
};
|
|
18
|
+
sectionSurfaces: Array<'base' | 'soft' | 'emphasis'>;
|
|
22
19
|
typography: {
|
|
23
|
-
|
|
20
|
+
profile: string;
|
|
24
21
|
values: any;
|
|
25
22
|
};
|
|
26
23
|
};
|
|
27
24
|
type VisualTheme = {
|
|
28
|
-
|
|
25
|
+
images: {
|
|
29
26
|
maxAvailableWidthPercent: {
|
|
30
27
|
desktop: number;
|
|
31
28
|
mobile: number;
|
|
@@ -39,10 +36,10 @@ interface Props {
|
|
|
39
36
|
visualTheme: VisualTheme;
|
|
40
37
|
sectionIndex: number;
|
|
41
38
|
headingLevel: 1 | 2;
|
|
42
|
-
|
|
39
|
+
prioritizeFirstImage?: boolean;
|
|
43
40
|
}
|
|
44
41
|
|
|
45
|
-
const { section, pagePresentation, visualTheme, sectionIndex, headingLevel,
|
|
42
|
+
const { section, pagePresentation, visualTheme, sectionIndex, headingLevel, prioritizeFirstImage = false } = Astro.props;
|
|
46
43
|
const HeadingTag = headingLevel === 1 ? 'h1' : 'h2';
|
|
47
44
|
// Sections are authored with Markdown ## headings even when the first one renders as HTML h1.
|
|
48
45
|
const sectionHeadingVisualLevel = 'h2';
|
|
@@ -169,7 +166,7 @@ const getHeadingSizeValue = (level: HeadingLevel, size: TextSize) => (
|
|
|
169
166
|
const textWidthValues: Record<TextWidth, string> = {
|
|
170
167
|
narrow: 'min(60ch, var(--text-width))',
|
|
171
168
|
normal: 'min(72ch, var(--text-width))',
|
|
172
|
-
wide: 'min(72ch, var(--
|
|
169
|
+
wide: 'min(72ch, var(--image-layout-width))',
|
|
173
170
|
};
|
|
174
171
|
const surface = resolveSectionSurface(pagePresentation, sectionIndex);
|
|
175
172
|
const typography = pagePresentation.typography.values;
|
|
@@ -213,7 +210,7 @@ const bodyWidth = {
|
|
|
213
210
|
desktop: bodyWidthValue,
|
|
214
211
|
mobile: bodyWidthValue,
|
|
215
212
|
};
|
|
216
|
-
const
|
|
213
|
+
const isStructuredBlock = (block: ResolvedSection['contentBlocks'][number]) => (
|
|
217
214
|
block.type === 'image-stack'
|
|
218
215
|
|| block.type === 'image-carousel'
|
|
219
216
|
|| block.type === 'card-list'
|
|
@@ -243,7 +240,7 @@ const flushReadingBlocks = () => {
|
|
|
243
240
|
};
|
|
244
241
|
|
|
245
242
|
for (const block of section.contentBlocks) {
|
|
246
|
-
if (
|
|
243
|
+
if (isStructuredBlock(block)) {
|
|
247
244
|
flushReadingBlocks();
|
|
248
245
|
contentGroups.push({ type: 'media', block });
|
|
249
246
|
} else {
|
|
@@ -301,7 +298,7 @@ const sectionStyle = [
|
|
|
301
298
|
---
|
|
302
299
|
|
|
303
300
|
<section
|
|
304
|
-
|
|
301
|
+
aria-labelledby={section.id}
|
|
305
302
|
class:list={[
|
|
306
303
|
'site-section',
|
|
307
304
|
{
|
|
@@ -314,10 +311,10 @@ const sectionStyle = [
|
|
|
314
311
|
>
|
|
315
312
|
<div class="section-card">
|
|
316
313
|
<header class="section-header">
|
|
317
|
-
<HeadingTag set:html={section.titleHtml} />
|
|
314
|
+
<HeadingTag id={section.id} set:html={section.titleHtml} />
|
|
318
315
|
</header>
|
|
319
316
|
|
|
320
|
-
<div class:list={['section-body', { 'section-body-has-
|
|
317
|
+
<div class:list={['section-body', { 'section-body-has-media': hasMedia, 'section-body-text-only': !hasMedia }]}>
|
|
321
318
|
{contentGroups.map((group, groupIndex) => {
|
|
322
319
|
if (group.type === 'reading') {
|
|
323
320
|
if (hasMarginNotes && group.blocks.some((block) => block.type === 'note')) {
|
|
@@ -383,22 +380,22 @@ const sectionStyle = [
|
|
|
383
380
|
<ImageCarousel
|
|
384
381
|
title={section.title}
|
|
385
382
|
images={block.images}
|
|
386
|
-
maxAvailableWidthPercent={visualTheme.
|
|
387
|
-
priorityFirstImage={
|
|
383
|
+
maxAvailableWidthPercent={visualTheme.images.maxAvailableWidthPercent}
|
|
384
|
+
priorityFirstImage={prioritizeFirstImage && groupIndex === 0}
|
|
388
385
|
/>
|
|
389
386
|
);
|
|
390
387
|
}
|
|
391
388
|
|
|
392
389
|
if (block.type === 'card-list') {
|
|
393
|
-
return <CardList cards={block.cards}
|
|
390
|
+
return <CardList cards={block.cards} layout={block.layout} flow={block.flow} size={block.size} width={block.width} />;
|
|
394
391
|
}
|
|
395
392
|
|
|
396
393
|
return (
|
|
397
|
-
<
|
|
394
|
+
<ImageStack
|
|
398
395
|
title={section.title}
|
|
399
396
|
images={block.images}
|
|
400
|
-
maxAvailableWidthPercent={visualTheme.
|
|
401
|
-
priorityFirstImage={
|
|
397
|
+
maxAvailableWidthPercent={visualTheme.images.maxAvailableWidthPercent}
|
|
398
|
+
priorityFirstImage={prioritizeFirstImage && groupIndex === 0}
|
|
402
399
|
/>
|
|
403
400
|
);
|
|
404
401
|
})}
|