@janga/norna 0.7.9 → 0.7.10
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 +1 -1
- package/docs/README.md +14 -11
- package/docs/configuration.md +9 -54
- package/docs/content.md +6 -7
- package/docs/design/norna-diagram-design.md +2 -0
- package/docs/getting-started.md +18 -16
- package/docs/routes.md +7 -6
- package/docs/site-structure.md +4 -2
- package/docs/sitewide-content.md +74 -0
- package/docs/theme.md +61 -128
- package/docs/typography.md +45 -33
- package/examples/001-dog-shelter-single-page/README.md +9 -0
- package/examples/001-dog-shelter-single-page/site/config.mjs +27 -0
- package/examples/001-dog-shelter-single-page/site/content.md +38 -0
- package/examples/001-dog-shelter-single-page/site/images/what-we-do/dog-house.svg +25 -0
- package/examples/001-dog-shelter-single-page/site/images/you-can-help/heart.svg +13 -0
- package/examples/001-dog-shelter-single-page/site/public/favicon.svg +10 -0
- package/examples/001-dog-shelter-single-page/site/public/robots.txt +2 -0
- package/examples/001-dog-shelter-single-page/site/sitewide-content.md +6 -0
- package/examples/001-dog-shelter-single-page/site/theme.md +25 -0
- package/examples/002-dog-shelter-multi-page/README.md +9 -0
- package/examples/002-dog-shelter-multi-page/site/config.mjs +27 -0
- package/examples/002-dog-shelter-multi-page/site/content.md +12 -0
- package/examples/002-dog-shelter-multi-page/site/public/favicon.svg +10 -0
- package/examples/002-dog-shelter-multi-page/site/public/robots.txt +2 -0
- package/examples/002-dog-shelter-multi-page/site/routes/010-dogs/images/dogs/rex.svg +19 -0
- package/examples/002-dog-shelter-multi-page/site/routes/010-dogs/images/dogs/rover.svg +19 -0
- package/examples/002-dog-shelter-multi-page/site/routes/010-dogs/route-content.md +30 -0
- package/examples/002-dog-shelter-multi-page/site/routes/020-adopt/route-content.md +12 -0
- package/examples/002-dog-shelter-multi-page/site/sitewide-content.md +6 -0
- package/examples/002-dog-shelter-multi-page/site/theme.md +25 -0
- package/examples/dog-gallery/.astro/collections/site.schema.json +776 -0
- package/examples/dog-gallery/.astro/collections/theme.schema.json +672 -0
- package/examples/dog-gallery/.astro/content-assets.mjs +4 -0
- package/examples/dog-gallery/.astro/content-modules.mjs +1 -0
- package/examples/dog-gallery/.astro/content.d.ts +175 -0
- package/examples/dog-gallery/.astro/data-store.json +1 -0
- package/examples/dog-gallery/.astro/dev-local.json +8 -0
- package/examples/dog-gallery/.astro/dev.json +13 -0
- package/examples/dog-gallery/.astro/dev.log +95 -0
- package/examples/dog-gallery/.astro/settings.json +5 -0
- package/examples/dog-gallery/.astro/types.d.ts +2 -0
- package/examples/dog-gallery/dist/_astro/GalleryGrid.astro_astro_type_script_index_0_lang.B0kxDyGz.js +1 -0
- package/examples/dog-gallery/dist/_astro/SitePage.DicGh_5N.css +1 -0
- package/examples/dog-gallery/dist/dog-care/index.html +15 -0
- package/examples/dog-gallery/dist/dog-shelter-logo.svg +14 -0
- package/examples/dog-gallery/dist/favicon.svg +7 -0
- package/examples/dog-gallery/dist/images/generated/black-dogs/black-puppy-meadow-7255e3d9-1080.webp +0 -0
- package/examples/dog-gallery/dist/images/generated/black-dogs/black-puppy-meadow-7255e3d9-1440.webp +0 -0
- package/examples/dog-gallery/dist/images/generated/black-dogs/black-puppy-meadow-7255e3d9-1536.webp +0 -0
- package/examples/dog-gallery/dist/images/generated/black-dogs/black-puppy-meadow-7255e3d9-480.webp +0 -0
- package/examples/dog-gallery/dist/images/generated/black-dogs/black-puppy-meadow-7255e3d9-768.webp +0 -0
- package/examples/dog-gallery/dist/images/generated/black-dogs/photo-of-a-black-dog-5cf614d2-480.webp +0 -0
- package/examples/dog-gallery/dist/images/generated/black-dogs/photo-of-a-black-dog-5cf614d2-768.webp +0 -0
- package/examples/dog-gallery/dist/images/generated/black-dogs/photo-of-a-black-dog-5cf614d2-960.webp +0 -0
- package/examples/dog-gallery/dist/images/generated/brown-dogs/brown-dog-50ef114e-1080.webp +0 -0
- package/examples/dog-gallery/dist/images/generated/brown-dogs/brown-dog-50ef114e-1440.webp +0 -0
- package/examples/dog-gallery/dist/images/generated/brown-dogs/brown-dog-50ef114e-1920.webp +0 -0
- package/examples/dog-gallery/dist/images/generated/brown-dogs/brown-dog-50ef114e-3939.webp +0 -0
- package/examples/dog-gallery/dist/images/generated/brown-dogs/brown-dog-50ef114e-480.webp +0 -0
- package/examples/dog-gallery/dist/images/generated/brown-dogs/brown-dog-50ef114e-768.webp +0 -0
- package/examples/dog-gallery/dist/images/generated/brown-dogs/dog-accompanies-master-7ef804d5-1080.webp +0 -0
- package/examples/dog-gallery/dist/images/generated/brown-dogs/dog-accompanies-master-7ef804d5-1440.webp +0 -0
- package/examples/dog-gallery/dist/images/generated/brown-dogs/dog-accompanies-master-7ef804d5-1920.webp +0 -0
- package/examples/dog-gallery/dist/images/generated/brown-dogs/dog-accompanies-master-7ef804d5-2400.webp +0 -0
- package/examples/dog-gallery/dist/images/generated/brown-dogs/dog-accompanies-master-7ef804d5-480.webp +0 -0
- package/examples/dog-gallery/dist/images/generated/brown-dogs/dog-accompanies-master-7ef804d5-768.webp +0 -0
- package/examples/dog-gallery/dist/images/generated/golden-dogs/golden-retriever-f797fd88-1080.webp +0 -0
- package/examples/dog-gallery/dist/images/generated/golden-dogs/golden-retriever-f797fd88-1440.webp +0 -0
- package/examples/dog-gallery/dist/images/generated/golden-dogs/golden-retriever-f797fd88-1600.webp +0 -0
- package/examples/dog-gallery/dist/images/generated/golden-dogs/golden-retriever-f797fd88-480.webp +0 -0
- package/examples/dog-gallery/dist/images/generated/golden-dogs/golden-retriever-f797fd88-768.webp +0 -0
- package/examples/dog-gallery/dist/images/generated/golden-dogs/toller-puppy-a2a8fbc0-1080.webp +0 -0
- package/examples/dog-gallery/dist/images/generated/golden-dogs/toller-puppy-a2a8fbc0-1440.webp +0 -0
- package/examples/dog-gallery/dist/images/generated/golden-dogs/toller-puppy-a2a8fbc0-1920.webp +0 -0
- package/examples/dog-gallery/dist/images/generated/golden-dogs/toller-puppy-a2a8fbc0-2576.webp +0 -0
- package/examples/dog-gallery/dist/images/generated/golden-dogs/toller-puppy-a2a8fbc0-480.webp +0 -0
- package/examples/dog-gallery/dist/images/generated/golden-dogs/toller-puppy-a2a8fbc0-768.webp +0 -0
- package/examples/dog-gallery/dist/images/generated/white-dogs/white-cute-dog-077a0689-1080.webp +0 -0
- package/examples/dog-gallery/dist/images/generated/white-dogs/white-cute-dog-077a0689-1277.webp +0 -0
- package/examples/dog-gallery/dist/images/generated/white-dogs/white-cute-dog-077a0689-480.webp +0 -0
- package/examples/dog-gallery/dist/images/generated/white-dogs/white-cute-dog-077a0689-768.webp +0 -0
- package/examples/dog-gallery/dist/images/generated/white-dogs/white-puppy-garden-d3ab2ca0-1080.webp +0 -0
- package/examples/dog-gallery/dist/images/generated/white-dogs/white-puppy-garden-d3ab2ca0-1440.webp +0 -0
- package/examples/dog-gallery/dist/images/generated/white-dogs/white-puppy-garden-d3ab2ca0-1536.webp +0 -0
- package/examples/dog-gallery/dist/images/generated/white-dogs/white-puppy-garden-d3ab2ca0-480.webp +0 -0
- package/examples/dog-gallery/dist/images/generated/white-dogs/white-puppy-garden-d3ab2ca0-768.webp +0 -0
- package/examples/dog-gallery/dist/index.html +45 -0
- package/examples/dog-gallery/dist/robots.txt +2 -0
- package/examples/dog-gallery/node_modules/.vite/deps/_metadata.json +7 -7
- package/examples/dog-gallery/site/.norna/generated-images.json +8 -8
- package/examples/dog-gallery/site/config.mjs +0 -20
- package/examples/dog-gallery/site/content.md +4 -5
- package/examples/dog-gallery/site/public/logo.svg +14 -0
- package/examples/dog-gallery/site/sitewide-content.md +16 -0
- package/examples/dog-gallery/site/theme.md +13 -24
- package/examples/presentation/README.md +15 -0
- package/examples/presentation/site/.norna/generated-images.json +66 -0
- package/examples/presentation/site/config.mjs +29 -0
- package/examples/presentation/site/content.md +67 -0
- package/examples/presentation/site/public/robots.txt +2 -0
- package/examples/presentation/site/routes/010-presets/route-content.md +63 -0
- package/examples/presentation/site/routes/010-presets/theme.md +4 -0
- package/examples/presentation/site/routes/020-media/images/cards/card-carousel.svg +11 -0
- package/examples/presentation/site/routes/020-media/images/cards/card-stack.svg +9 -0
- package/examples/presentation/site/routes/020-media/images/cards/card-surfaces.svg +8 -0
- package/examples/presentation/site/routes/020-media/images/carousel/carousel-one.svg +8 -0
- package/examples/presentation/site/routes/020-media/images/carousel/carousel-three.svg +13 -0
- package/examples/presentation/site/routes/020-media/images/carousel/carousel-two.svg +8 -0
- package/examples/presentation/site/routes/020-media/images/stack/stack-one.svg +8 -0
- package/examples/presentation/site/routes/020-media/images/stack/stack-two.svg +9 -0
- package/examples/presentation/site/routes/020-media/route-content.md +77 -0
- package/examples/presentation/site/routes/020-media/theme.md +4 -0
- package/examples/presentation/site/routes/030-surfaces/route-content.md +44 -0
- package/examples/presentation/site/routes/030-surfaces/theme.md +7 -0
- package/examples/presentation/site/sitewide-content.md +6 -0
- package/examples/presentation/site/theme.md +43 -0
- package/examples/typography-presets/.astro/collections/routeTheme.schema.json +746 -0
- package/examples/typography-presets/.astro/collections/site.schema.json +0 -717
- package/examples/typography-presets/.astro/collections/sitewide.schema.json +114 -0
- package/examples/typography-presets/.astro/collections/theme.schema.json +161 -59
- package/examples/typography-presets/.astro/content.d.ts +8 -0
- package/examples/typography-presets/.astro/data-store.json +1 -1
- package/examples/typography-presets/.astro/dev-local.json +2 -2
- package/examples/typography-presets/.astro/dev.json +2 -2
- package/examples/typography-presets/.astro/dev.log +37 -16
- package/examples/typography-presets/site/.norna/generated-images.json +76 -76
- package/examples/typography-presets/site/config.mjs +0 -13
- package/examples/typography-presets/site/content.md +8 -126
- package/examples/typography-presets/site/routes/010-quiet-gallery/route-content.md +34 -0
- package/examples/typography-presets/site/routes/010-quiet-gallery/theme.md +4 -0
- package/examples/typography-presets/site/routes/020-compact-gallery/route-content.md +33 -0
- package/examples/typography-presets/site/routes/020-compact-gallery/theme.md +4 -0
- package/examples/typography-presets/site/routes/030-text-forward/route-content.md +34 -0
- package/examples/typography-presets/site/routes/030-text-forward/theme.md +4 -0
- package/examples/typography-presets/site/routes/040-statement/route-content.md +34 -0
- package/examples/typography-presets/site/routes/040-statement/theme.md +4 -0
- package/examples/typography-presets/site/sitewide-content.md +14 -0
- package/examples/typography-presets/site/theme.md +9 -9
- package/fixtures/basic/site/config.mjs +0 -13
- package/fixtures/basic/site/sitewide-content.md +12 -0
- package/fixtures/basic/site/theme.md +2 -2
- package/fixtures/content-model-v2/site/content.md +2 -5
- package/fixtures/content-model-v2/site/theme.md +1 -3
- package/package.json +3 -2
- package/scripts/check-config.mjs +22 -1
- package/scripts/lib/frontmatter-yaml.mjs +28 -0
- package/scripts/lib/logo-assets.mjs +35 -0
- package/scripts/lib/norna-markdown-blocks.mjs +166 -0
- package/scripts/lib/presentation.mjs +104 -46
- package/scripts/lib/project-config.mjs +67 -105
- package/scripts/lib/site-content.mjs +26 -118
- package/scripts/lib/site-paths.mjs +2 -0
- package/scripts/lib/sitewide-config.mjs +34 -0
- package/scripts/lib/theme-config.mjs +72 -8
- package/scripts/lib/typography.mjs +20 -26
- package/scripts/show-typography.mjs +40 -147
- package/scripts/sync-content-sections.mjs +19 -17
- package/scripts/test-content-check.mjs +59 -27
- package/scripts/test-content-model-v2.mjs +154 -0
- package/scripts/test-engine-commands.mjs +3 -1
- package/scripts/test-package-check.mjs +66 -24
- package/scripts/test-temporary-visibility.mjs +19 -2
- package/src/components/CardList.astro +5 -2
- package/src/components/GalleryGrid.astro +37 -16
- package/src/components/SiteBanners.astro +100 -0
- package/src/components/SiteNavigation.astro +48 -31
- package/src/components/SitePage.astro +55 -13
- package/src/components/SiteSection.astro +193 -35
- package/src/content.config.ts +78 -48
- package/src/layouts/BaseLayout.astro +64 -14
- package/src/lib/routeThemes.ts +61 -0
- package/src/lib/sectionContent.ts +235 -51
- package/src/lib/sitePublicAssets.ts +1 -0
- package/src/styles/global.css +282 -93
- package/starters/basic/.github/workflows/deploy.yml +1 -1
- package/starters/basic/README.md +7 -6
- package/starters/basic/site/config.mjs +0 -13
- package/starters/basic/site/content.md +1 -1
- package/starters/basic/site/sitewide-content.md +14 -0
- package/starters/basic/site/theme.md +13 -24
- package/starters/project/.github/workflows/deploy.yml +1 -1
- package/starters/project/README.md +10 -8
- package/starters/project/site/config.mjs +0 -13
- package/starters/project/site/sitewide-content.md +14 -0
- package/starters/project/site/theme.md +3 -11
- /package/examples/typography-presets/site/{images → routes/010-quiet-gallery/images}/quiet-gallery/puppy-portrait.jpg +0 -0
- /package/examples/typography-presets/site/{images → routes/010-quiet-gallery/images}/quiet-gallery/shepherd-portrait.jpg +0 -0
- /package/examples/typography-presets/site/{images → routes/010-quiet-gallery/images}/quiet-gallery/white-puppy.jpg +0 -0
- /package/examples/typography-presets/site/{images → routes/020-compact-gallery/images}/compact-gallery/black-schnauzer.jpg +0 -0
- /package/examples/typography-presets/site/{images → routes/020-compact-gallery/images}/compact-gallery/retriever-portrait.jpg +0 -0
- /package/examples/typography-presets/site/{images → routes/020-compact-gallery/images}/compact-gallery/schnauzer-portrait.jpg +0 -0
- /package/examples/typography-presets/site/{images → routes/030-text-forward/images}/text-forward/boxer-portrait.jpg +0 -0
- /package/examples/typography-presets/site/{images → routes/030-text-forward/images}/text-forward/grass-puppy.jpg +0 -0
- /package/examples/typography-presets/site/{images → routes/030-text-forward/images}/text-forward/grey-street-dog.jpg +0 -0
- /package/examples/typography-presets/site/{images → routes/040-statement/images}/statement/black-pedigree.jpg +0 -0
- /package/examples/typography-presets/site/{images → routes/040-statement/images}/statement/sheepdog-grass.jpg +0 -0
- /package/examples/typography-presets/site/{images → routes/040-statement/images}/statement/sheepdog-wide.jpg +0 -0
|
@@ -129,6 +129,99 @@ test('content model v2 fixture checks and builds', async () => {
|
|
|
129
129
|
}
|
|
130
130
|
});
|
|
131
131
|
|
|
132
|
+
test('route theme replaces visual theme while site identity stays at the root', async () => {
|
|
133
|
+
const { root, siteDir } = await createTempSite({ underRepoCache: true });
|
|
134
|
+
try {
|
|
135
|
+
await writeFile(path.join(siteDir, 'theme.md'), `---
|
|
136
|
+
typography:
|
|
137
|
+
preset: text-forward
|
|
138
|
+
---
|
|
139
|
+
`);
|
|
140
|
+
await writeFile(path.join(siteDir, 'sitewide-content.md'), `---
|
|
141
|
+
navigation:
|
|
142
|
+
brand: Root Brand
|
|
143
|
+
---
|
|
144
|
+
`);
|
|
145
|
+
await mkdir(path.join(siteDir, 'routes', '010-guide'), { recursive: true });
|
|
146
|
+
await writeFile(path.join(siteDir, 'content.md'), `---
|
|
147
|
+
title: Root
|
|
148
|
+
description: Root page
|
|
149
|
+
---
|
|
150
|
+
|
|
151
|
+
## Home {#home}
|
|
152
|
+
|
|
153
|
+
Root content.
|
|
154
|
+
`);
|
|
155
|
+
await writeFile(path.join(siteDir, 'routes', '010-guide', 'route-content.md'), `---
|
|
156
|
+
title: Guide
|
|
157
|
+
description: Guide page
|
|
158
|
+
navigation:
|
|
159
|
+
label: Guide
|
|
160
|
+
---
|
|
161
|
+
|
|
162
|
+
## Guide {#guide}
|
|
163
|
+
|
|
164
|
+
Route content.
|
|
165
|
+
`);
|
|
166
|
+
await writeFile(path.join(siteDir, 'routes', '010-guide', 'theme.md'), `---
|
|
167
|
+
typography:
|
|
168
|
+
preset: statement
|
|
169
|
+
presentation:
|
|
170
|
+
palette: light
|
|
171
|
+
---
|
|
172
|
+
`);
|
|
173
|
+
|
|
174
|
+
await runNorna(['--site-dir', siteDir, 'build']);
|
|
175
|
+
const rootHtml = await readFile(path.join(path.dirname(siteDir), 'dist', 'index.html'), 'utf8');
|
|
176
|
+
const routeHtml = await readFile(path.join(path.dirname(siteDir), 'dist', 'guide', 'index.html'), 'utf8');
|
|
177
|
+
assert.match(rootHtml, /--color-page: #000000/);
|
|
178
|
+
assert.match(routeHtml, /--color-page: #ffffff/);
|
|
179
|
+
assert.match(routeHtml, /<a class="site-brand" href="\/">Root Brand<\/a>/);
|
|
180
|
+
} finally {
|
|
181
|
+
await rm(root, { recursive: true, force: true });
|
|
182
|
+
}
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
test('route theme cannot define site identity', async () => {
|
|
186
|
+
const { root, siteDir } = await createTempSite({ underRepoCache: true });
|
|
187
|
+
try {
|
|
188
|
+
await mkdir(path.join(siteDir, 'routes', '010-guide'), { recursive: true });
|
|
189
|
+
await writeFile(path.join(siteDir, 'content.md'), `---
|
|
190
|
+
title: Root
|
|
191
|
+
description: Root page
|
|
192
|
+
---
|
|
193
|
+
|
|
194
|
+
## Home {#home}
|
|
195
|
+
|
|
196
|
+
Root content.
|
|
197
|
+
`);
|
|
198
|
+
await writeFile(path.join(siteDir, 'routes', '010-guide', 'route-content.md'), `---
|
|
199
|
+
title: Guide
|
|
200
|
+
description: Guide page
|
|
201
|
+
---
|
|
202
|
+
|
|
203
|
+
## Guide {#guide}
|
|
204
|
+
|
|
205
|
+
Route content.
|
|
206
|
+
`);
|
|
207
|
+
await writeFile(path.join(siteDir, 'routes', '010-guide', 'theme.md'), `---
|
|
208
|
+
navigation:
|
|
209
|
+
brand: Route Brand
|
|
210
|
+
---
|
|
211
|
+
`);
|
|
212
|
+
|
|
213
|
+
await assert.rejects(
|
|
214
|
+
runNorna(['--site-dir', siteDir, 'config:check']),
|
|
215
|
+
(error) => {
|
|
216
|
+
assert.match(error.output, /may not define navigation\. Brand and logo belong in site\/sitewide-content\.md/);
|
|
217
|
+
return true;
|
|
218
|
+
},
|
|
219
|
+
);
|
|
220
|
+
} finally {
|
|
221
|
+
await rm(root, { recursive: true, force: true });
|
|
222
|
+
}
|
|
223
|
+
});
|
|
224
|
+
|
|
132
225
|
test('content:check warns for local Markdown images but not external Markdown images', async () => {
|
|
133
226
|
const { root, siteDir } = await createTempSite();
|
|
134
227
|
try {
|
|
@@ -189,6 +282,40 @@ description: Fixture
|
|
|
189
282
|
}
|
|
190
283
|
});
|
|
191
284
|
|
|
285
|
+
test('inline notes render as linked numbered margin notes', async () => {
|
|
286
|
+
const { root, siteDir } = await createTempSite({ underRepoCache: true });
|
|
287
|
+
try {
|
|
288
|
+
await writeFile(path.join(siteDir, 'content.md'), `---
|
|
289
|
+
title: Inline notes
|
|
290
|
+
description: Fixture
|
|
291
|
+
---
|
|
292
|
+
|
|
293
|
+
## Intro {#intro}
|
|
294
|
+
|
|
295
|
+
The first paragraph points to an explanation.{note-ref}
|
|
296
|
+
|
|
297
|
+
{note: This explanation names \`sitewide-content.md\` and stays beside the paragraph on wide screens.}
|
|
298
|
+
|
|
299
|
+
The second paragraph has its own explanation.{note-ref}
|
|
300
|
+
|
|
301
|
+
{note: This is the second explanation.}
|
|
302
|
+
`);
|
|
303
|
+
|
|
304
|
+
await runNorna(['--site-dir', siteDir, 'build']);
|
|
305
|
+
const html = await readFile(path.join(path.dirname(siteDir), 'dist', 'index.html'), 'utf8');
|
|
306
|
+
assert.match(html, /<sup class="section-note-ref"><a id="note-ref-home-intro-1" href="#note-home-intro-1" aria-label="Note 1" aria-describedby="note-home-intro-1" data-note-id="note-home-intro-1">1<\/a><\/sup>/);
|
|
307
|
+
assert.match(html, /<aside class="section-note section-note-margin" id="note-home-intro-1" aria-label="Note 1" role="note" data-note-id="note-home-intro-1" style="grid-row: 1;">/);
|
|
308
|
+
assert.match(html, /<a class="section-note-number" href="#note-ref-home-intro-1" aria-label="Note 1">\s*1\s*<\/a>/);
|
|
309
|
+
assert.match(html, /This explanation names <code>sitewide-content\.md<\/code> and stays beside the paragraph on wide screens\./);
|
|
310
|
+
assert.match(html, /<sup class="section-note-ref"><a id="note-ref-home-intro-2" href="#note-home-intro-2" aria-label="Note 2" aria-describedby="note-home-intro-2" data-note-id="note-home-intro-2">2<\/a><\/sup>/);
|
|
311
|
+
assert.match(html, /<aside class="section-note section-note-margin" id="note-home-intro-2" aria-label="Note 2" role="note" data-note-id="note-home-intro-2" style="grid-row: 2;">/);
|
|
312
|
+
assert.match(html, /This is the second explanation\./);
|
|
313
|
+
assert.doesNotMatch(html, /\{note:/);
|
|
314
|
+
} finally {
|
|
315
|
+
await rm(root, { recursive: true, force: true });
|
|
316
|
+
}
|
|
317
|
+
});
|
|
318
|
+
|
|
192
319
|
test('updated managed SVG images get updated static output', async () => {
|
|
193
320
|
const { root, siteDir } = await createTempSite();
|
|
194
321
|
try {
|
|
@@ -842,6 +969,33 @@ description: Fixture
|
|
|
842
969
|
}
|
|
843
970
|
});
|
|
844
971
|
|
|
972
|
+
test('content:check rejects the removed norna-note block', async () => {
|
|
973
|
+
const { root, siteDir } = await createTempSite();
|
|
974
|
+
try {
|
|
975
|
+
await writeFile(path.join(siteDir, 'content.md'), `---
|
|
976
|
+
title: Removed note block
|
|
977
|
+
description: Fixture
|
|
978
|
+
---
|
|
979
|
+
|
|
980
|
+
## Intro {#intro}
|
|
981
|
+
|
|
982
|
+
\`\`\`norna-note
|
|
983
|
+
This block is no longer supported.
|
|
984
|
+
\`\`\`
|
|
985
|
+
`);
|
|
986
|
+
|
|
987
|
+
await assert.rejects(
|
|
988
|
+
() => runContentScript(siteDir, ['--check']),
|
|
989
|
+
(error) => {
|
|
990
|
+
assert.match(error.output, /Unknown Norna block "norna-note"\. Use one of: norna-image-stack, norna-image-carousel, norna-card-list\./);
|
|
991
|
+
return true;
|
|
992
|
+
},
|
|
993
|
+
);
|
|
994
|
+
} finally {
|
|
995
|
+
await rm(root, { recursive: true, force: true });
|
|
996
|
+
}
|
|
997
|
+
});
|
|
998
|
+
|
|
845
999
|
test('content:check supports tilde fenced Norna blocks', async () => {
|
|
846
1000
|
const { root, siteDir } = await createTempSite();
|
|
847
1001
|
try {
|
|
@@ -57,8 +57,10 @@ try {
|
|
|
57
57
|
for (const [presetName, preset] of Object.entries(typographyPresets)) {
|
|
58
58
|
for (const [level, heading] of Object.entries(preset.headings)) {
|
|
59
59
|
assert.equal(heading.size, 'medium', `${presetName} ${level} should use medium size`);
|
|
60
|
+
assert.ok([400, 500, 600, 700].includes(heading.weight), `${presetName} ${level} should use a supported weight`);
|
|
60
61
|
}
|
|
61
62
|
assert.equal(preset.body.size, 'medium', `${presetName} body should use medium size`);
|
|
63
|
+
assert.ok(['narrow', 'normal', 'wide'].includes(preset.body.width), `${presetName} body should use a supported width`);
|
|
62
64
|
assert.equal(preset.caption.size, 'medium', `${presetName} caption should use medium size`);
|
|
63
65
|
}
|
|
64
66
|
for (const [rhythmName, rhythm] of Object.entries(typographyRhythms)) {
|
|
@@ -76,7 +78,7 @@ try {
|
|
|
76
78
|
assert.match(showResult.stdout, /pages:/);
|
|
77
79
|
assert.match(showResult.stdout, /\s+\/:/);
|
|
78
80
|
assert.match(showResult.stdout, /value: quiet-gallery/);
|
|
79
|
-
assert.match(showResult.stdout, /
|
|
81
|
+
assert.match(showResult.stdout, /source: "site\/theme\.md"/);
|
|
80
82
|
assert.match(showResult.stdout, /intro:/);
|
|
81
83
|
|
|
82
84
|
const initAgainResult = runCli(['init', initializedSiteRoot]);
|
|
@@ -160,6 +160,7 @@ try {
|
|
|
160
160
|
assertFileExists(path.join(packagedStarterRoot, 'README.md')),
|
|
161
161
|
assertFileExists(path.join(packagedStarterRoot, 'site', 'config.mjs')),
|
|
162
162
|
assertFileExists(path.join(packagedStarterRoot, 'site', 'theme.md')),
|
|
163
|
+
assertFileExists(path.join(packagedStarterRoot, 'site', 'sitewide-content.md')),
|
|
163
164
|
assertFileExists(path.join(packagedStarterRoot, 'site', 'content.md')),
|
|
164
165
|
assertFileExists(path.join(packagedStarterRoot, 'site', 'images', 'work', '.gitkeep')),
|
|
165
166
|
assertFileExists(path.join(packagedStarterRoot, 'site', 'public', 'robots.txt')),
|
|
@@ -184,11 +185,15 @@ try {
|
|
|
184
185
|
.replace("url: 'https://example.com/'", "url: 'https://example.com/site/'")
|
|
185
186
|
.replace("basePath: '/'", "basePath: '/site/'"),
|
|
186
187
|
);
|
|
187
|
-
const
|
|
188
|
-
const
|
|
188
|
+
const packageCheckSitewidePath = path.join(siteProjectRoot, 'site', 'sitewide-content.md');
|
|
189
|
+
const packageCheckSitewide = await readFile(packageCheckSitewidePath, 'utf8');
|
|
189
190
|
await writeFile(
|
|
190
|
-
|
|
191
|
-
|
|
191
|
+
packageCheckSitewidePath,
|
|
192
|
+
packageCheckSitewide.replace('brand: Example Site', 'brand: Package Check Brand\n logo:\n alt: Package Check Logo\n height: 2.6rem'),
|
|
193
|
+
);
|
|
194
|
+
await writeFile(
|
|
195
|
+
path.join(siteProjectRoot, 'site', 'public', 'logo.svg'),
|
|
196
|
+
'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32"><rect width="32" height="32" fill="black"/></svg>\n',
|
|
192
197
|
);
|
|
193
198
|
await writeFile(path.join(siteProjectRoot, 'site', 'content.md'), `---
|
|
194
199
|
title: Package Check Site
|
|
@@ -201,14 +206,14 @@ sections:
|
|
|
201
206
|
|
|
202
207
|
## Intro {#intro}
|
|
203
208
|
|
|
204
|
-
This page verifies that
|
|
209
|
+
This page verifies that **packaged norna sites** can build with the current content model.
|
|
205
210
|
|
|
206
211
|
<!-- norna-image-provenance:
|
|
207
212
|
image: package-check.svg
|
|
208
213
|
source: package check fixture
|
|
209
214
|
-->
|
|
210
215
|
|
|
211
|
-
##
|
|
216
|
+
## **Warning** {#warning}
|
|
212
217
|
|
|
213
218
|
This section verifies styled section headings.
|
|
214
219
|
|
|
@@ -262,6 +267,10 @@ This route verifies that packaged norna sites can build route pages.
|
|
|
262
267
|
path.join(siteProjectRoot, 'dist', 'index.html'),
|
|
263
268
|
'--section-heading-font-size-desktop: clamp(1.35rem, 1.1rem + 0.9vw, 2rem)',
|
|
264
269
|
);
|
|
270
|
+
await assertFileIncludes(
|
|
271
|
+
path.join(siteProjectRoot, 'dist', 'index.html'),
|
|
272
|
+
'--section-heading-font-weight: 400',
|
|
273
|
+
);
|
|
265
274
|
await assertFileIncludes(
|
|
266
275
|
path.join(siteProjectRoot, 'dist', 'index.html'),
|
|
267
276
|
'--section-heading-spacing-after: 0.55em',
|
|
@@ -288,7 +297,15 @@ This route verifies that packaged norna sites can build route pages.
|
|
|
288
297
|
);
|
|
289
298
|
await assertFileIncludes(
|
|
290
299
|
path.join(siteProjectRoot, 'dist', 'index.html'),
|
|
291
|
-
'<a class="site-brand" href="/site/"
|
|
300
|
+
'<a class="site-brand" href="/site/"><img class="site-brand-logo" src="/site/logo.svg" alt="Package Check Logo">',
|
|
301
|
+
);
|
|
302
|
+
await assertFileIncludes(
|
|
303
|
+
path.join(siteProjectRoot, 'dist', 'index.html'),
|
|
304
|
+
'<div class="site-nav-submenu">',
|
|
305
|
+
);
|
|
306
|
+
await assertFileIncludes(
|
|
307
|
+
path.join(siteProjectRoot, 'dist', 'index.html'),
|
|
308
|
+
'href="/site/#intro">Intro</a>',
|
|
292
309
|
);
|
|
293
310
|
await assertFileIncludes(
|
|
294
311
|
path.join(siteProjectRoot, 'dist', 'index.html'),
|
|
@@ -298,6 +315,10 @@ This route verifies that packaged norna sites can build route pages.
|
|
|
298
315
|
path.join(siteProjectRoot, 'dist', 'index.html'),
|
|
299
316
|
'--section-body-line-height: 1.5',
|
|
300
317
|
);
|
|
318
|
+
await assertFileIncludes(
|
|
319
|
+
path.join(siteProjectRoot, 'dist', 'index.html'),
|
|
320
|
+
'--section-body-width-desktop: min(72ch, var(--text-width))',
|
|
321
|
+
);
|
|
301
322
|
await assertFileExcludes(
|
|
302
323
|
path.join(siteProjectRoot, 'dist', 'index.html'),
|
|
303
324
|
'norna-image-provenance',
|
|
@@ -318,25 +339,29 @@ This route verifies that packaged norna sites can build route pages.
|
|
|
318
339
|
path.join(siteProjectRoot, 'dist', 'index.html'),
|
|
319
340
|
'--section-caption-font-size-desktop: clamp(0.86rem, 0.84rem + 0.08vw, 0.92rem)',
|
|
320
341
|
);
|
|
342
|
+
await assertFileExcludes(
|
|
343
|
+
path.join(siteProjectRoot, 'dist', 'index.html'),
|
|
344
|
+
'class="inline-style',
|
|
345
|
+
);
|
|
321
346
|
await assertFileIncludes(
|
|
322
347
|
path.join(siteProjectRoot, 'dist', 'index.html'),
|
|
323
|
-
'
|
|
348
|
+
'data-section-title="Warning"',
|
|
324
349
|
);
|
|
325
350
|
await assertFileIncludes(
|
|
326
351
|
path.join(siteProjectRoot, 'dist', 'index.html'),
|
|
327
|
-
'
|
|
352
|
+
'href="/site/#warning">Warning</a>',
|
|
328
353
|
);
|
|
329
354
|
await assertFileIncludes(
|
|
330
355
|
path.join(siteProjectRoot, 'dist', 'index.html'),
|
|
331
|
-
'
|
|
356
|
+
'--section-background-color: #000000',
|
|
332
357
|
);
|
|
333
358
|
await assertFileIncludes(
|
|
334
359
|
path.join(siteProjectRoot, 'dist', 'index.html'),
|
|
335
|
-
'
|
|
360
|
+
'--section-background-color: #171717',
|
|
336
361
|
);
|
|
337
362
|
await assertFileIncludes(
|
|
338
363
|
path.join(siteProjectRoot, 'dist', 'index.html'),
|
|
339
|
-
'--section-background-color: #
|
|
364
|
+
'--section-background-color: #252525',
|
|
340
365
|
);
|
|
341
366
|
await assertFileIncludes(
|
|
342
367
|
path.join(siteProjectRoot, 'dist', 'index.html'),
|
|
@@ -344,7 +369,7 @@ This route verifies that packaged norna sites can build route pages.
|
|
|
344
369
|
);
|
|
345
370
|
await assertFileIncludes(
|
|
346
371
|
path.join(siteProjectRoot, 'dist', 'index.html'),
|
|
347
|
-
'--site-top-text-color: #
|
|
372
|
+
'--site-top-text-color: #f2eee6',
|
|
348
373
|
);
|
|
349
374
|
await assertFileIncludes(
|
|
350
375
|
path.join(siteProjectRoot, 'dist', 'index.html'),
|
|
@@ -352,11 +377,11 @@ This route verifies that packaged norna sites can build route pages.
|
|
|
352
377
|
);
|
|
353
378
|
await assertFileIncludes(
|
|
354
379
|
path.join(siteProjectRoot, 'dist', 'index.html'),
|
|
355
|
-
'--site-footer-text-color: #
|
|
380
|
+
'--site-footer-text-color: #f2eee6',
|
|
356
381
|
);
|
|
357
382
|
await assertFileIncludes(
|
|
358
383
|
path.join(siteProjectRoot, 'dist', 'index.html'),
|
|
359
|
-
'--section-text-color: #
|
|
384
|
+
'--section-text-color: #f2eee6',
|
|
360
385
|
);
|
|
361
386
|
await writeFile(path.join(siteProjectRoot, 'site', 'public', 'favicon.ico'), 'fake icon');
|
|
362
387
|
await runInherit(npxBin, ['norna', 'build'], { cwd: siteProjectRoot, env: npmEnv });
|
|
@@ -382,35 +407,52 @@ This route verifies that packaged norna sites can build route pages.
|
|
|
382
407
|
);
|
|
383
408
|
await writeFile(
|
|
384
409
|
siteThemePath,
|
|
385
|
-
siteTheme.replace('
|
|
410
|
+
siteTheme.replace('\n palette: dark', '\n palette: neon'),
|
|
386
411
|
);
|
|
387
412
|
await runExpectFailure(
|
|
388
413
|
npxBin,
|
|
389
414
|
['norna', 'build'],
|
|
390
415
|
{ cwd: siteProjectRoot, env: npmEnv },
|
|
391
|
-
'presentation.
|
|
416
|
+
'presentation.palette',
|
|
392
417
|
);
|
|
393
418
|
await writeFile(
|
|
394
419
|
siteThemePath,
|
|
395
|
-
siteTheme.replace('
|
|
420
|
+
siteTheme.replace(' sectionSurfaces:\n mode: cycle', ' sectionSurfaces:\n mode: glowing'),
|
|
396
421
|
);
|
|
397
422
|
await runExpectFailure(
|
|
398
423
|
npxBin,
|
|
399
424
|
['norna', 'build'],
|
|
400
425
|
{ cwd: siteProjectRoot, env: npmEnv },
|
|
401
|
-
'presentation.
|
|
426
|
+
'presentation.sectionSurfaces.mode',
|
|
402
427
|
);
|
|
403
428
|
await writeFile(
|
|
404
429
|
siteThemePath,
|
|
405
|
-
siteTheme.replace(
|
|
430
|
+
siteTheme.replace(
|
|
431
|
+
' sectionSurfaces:\n mode: cycle',
|
|
432
|
+
' sectionSurfaces:\n mode: cycle\n sequence: [base, base]',
|
|
433
|
+
),
|
|
406
434
|
);
|
|
407
435
|
await runExpectFailure(
|
|
408
436
|
npxBin,
|
|
409
437
|
['norna', 'build'],
|
|
410
438
|
{ cwd: siteProjectRoot, env: npmEnv },
|
|
411
|
-
'presentation.
|
|
439
|
+
'presentation.sectionSurfaces',
|
|
412
440
|
);
|
|
413
441
|
await writeFile(siteThemePath, siteTheme);
|
|
442
|
+
await writeFile(
|
|
443
|
+
siteContentPath,
|
|
444
|
+
siteContent.replace(
|
|
445
|
+
'This page verifies that **packaged norna sites** can build with the current content model.',
|
|
446
|
+
'This page verifies that [packaged norna sites]{.highlight} can build with the current content model.',
|
|
447
|
+
),
|
|
448
|
+
);
|
|
449
|
+
await runExpectFailure(
|
|
450
|
+
npxBin,
|
|
451
|
+
['norna', 'build'],
|
|
452
|
+
{ cwd: siteProjectRoot, env: npmEnv },
|
|
453
|
+
'Inline color style ".highlight" is no longer supported',
|
|
454
|
+
);
|
|
455
|
+
await writeFile(siteContentPath, siteContent);
|
|
414
456
|
await writeFile(
|
|
415
457
|
siteContentPath,
|
|
416
458
|
siteContent.replace(
|
|
@@ -422,7 +464,7 @@ This route verifies that packaged norna sites can build route pages.
|
|
|
422
464
|
npxBin,
|
|
423
465
|
['norna', 'build'],
|
|
424
466
|
{ cwd: siteProjectRoot, env: npmEnv },
|
|
425
|
-
'presentation
|
|
467
|
+
'presentation',
|
|
426
468
|
);
|
|
427
469
|
await writeFile(
|
|
428
470
|
siteContentPath,
|
|
@@ -435,7 +477,7 @@ This route verifies that packaged norna sites can build route pages.
|
|
|
435
477
|
npxBin,
|
|
436
478
|
['norna', 'build'],
|
|
437
479
|
{ cwd: siteProjectRoot, env: npmEnv },
|
|
438
|
-
'sections.intro
|
|
480
|
+
'sections.intro: Unrecognized key: "presentation"',
|
|
439
481
|
);
|
|
440
482
|
await writeFile(
|
|
441
483
|
siteContentPath,
|
|
@@ -448,7 +490,7 @@ This route verifies that packaged norna sites can build route pages.
|
|
|
448
490
|
npxBin,
|
|
449
491
|
['norna', 'build'],
|
|
450
492
|
{ cwd: siteProjectRoot, env: npmEnv },
|
|
451
|
-
'sections.intro
|
|
493
|
+
'sections.intro: Unrecognized key: "presentation"',
|
|
452
494
|
);
|
|
453
495
|
await writeFile(
|
|
454
496
|
siteThemePath,
|
|
@@ -51,8 +51,23 @@ try {
|
|
|
51
51
|
await writeFile(path.join(siteDir, 'theme.md'), `---
|
|
52
52
|
typography:
|
|
53
53
|
preset: quiet-gallery
|
|
54
|
-
|
|
55
|
-
|
|
54
|
+
presentation:
|
|
55
|
+
palette: dark
|
|
56
|
+
---
|
|
57
|
+
`);
|
|
58
|
+
await writeFile(path.join(siteDir, 'sitewide-content.md'), `---
|
|
59
|
+
banners:
|
|
60
|
+
- id: expired-banner
|
|
61
|
+
visible:
|
|
62
|
+
until: "2026-01-01"
|
|
63
|
+
title: Expired banner
|
|
64
|
+
text: Expired banner text.
|
|
65
|
+
- id: active-banner
|
|
66
|
+
visible:
|
|
67
|
+
from: "2026-01-01"
|
|
68
|
+
until: "2026-12-31"
|
|
69
|
+
title: Active banner
|
|
70
|
+
text: Active banner text.
|
|
56
71
|
---
|
|
57
72
|
`);
|
|
58
73
|
await writeFile(path.join(siteDir, 'content.md'), `---
|
|
@@ -88,6 +103,8 @@ Always visible section text.
|
|
|
88
103
|
assert.match(html, /Active section text/);
|
|
89
104
|
assert.match(html, /Always visible section text/);
|
|
90
105
|
assert.doesNotMatch(html, /Expired section text/);
|
|
106
|
+
assert.match(html, /Active banner text/);
|
|
107
|
+
assert.doesNotMatch(html, /Expired banner text/);
|
|
91
108
|
|
|
92
109
|
console.log('Temporary visibility test passed.');
|
|
93
110
|
} finally {
|
|
@@ -18,16 +18,19 @@ type CardListWidth = 'text' | 'narrow' | 'normal' | 'wide';
|
|
|
18
18
|
|
|
19
19
|
interface Props {
|
|
20
20
|
cards: Card[];
|
|
21
|
+
maxAvailableWidthPercent: {
|
|
22
|
+
desktop: number;
|
|
23
|
+
};
|
|
21
24
|
layout?: CardListLayout;
|
|
22
25
|
flow?: CardListFlow;
|
|
23
26
|
size?: CardListSize;
|
|
24
27
|
width?: CardListWidth;
|
|
25
28
|
}
|
|
26
29
|
|
|
27
|
-
const { cards, layout = 'image-top', flow = 'grid', size = 'm', width = 'normal' } = Astro.props;
|
|
30
|
+
const { cards, maxAvailableWidthPercent, layout = 'image-top', flow = 'grid', size = 'm', width = 'normal' } = Astro.props;
|
|
28
31
|
const cardImageSizes = [
|
|
29
32
|
'(max-width: 700px) 100vw',
|
|
30
|
-
`min(20rem, ${
|
|
33
|
+
`min(20rem, ${maxAvailableWidthPercent.desktop}vw)`,
|
|
31
34
|
].join(', ');
|
|
32
35
|
const displayLink = (link: string) => withBasePath(projectConfig.site.basePath, link);
|
|
33
36
|
const cardListClasses = [
|
|
@@ -17,13 +17,25 @@ type GeneratedImageWithDimensions = NonNullable<ReturnType<typeof getGeneratedIm
|
|
|
17
17
|
interface Props {
|
|
18
18
|
title: string;
|
|
19
19
|
items: GalleryItem[];
|
|
20
|
+
maxAvailableWidthPercent: {
|
|
21
|
+
desktop: number;
|
|
22
|
+
mobile: number;
|
|
23
|
+
};
|
|
20
24
|
priorityFirstImage?: boolean;
|
|
21
25
|
}
|
|
22
26
|
|
|
23
|
-
const { title, items, priorityFirstImage = false } = Astro.props;
|
|
27
|
+
const { title, items, maxAvailableWidthPercent, priorityFirstImage = false } = Astro.props;
|
|
28
|
+
const getCaptionId = (image: GalleryImage, itemIndex: number, imageIndex = 0) => {
|
|
29
|
+
const source = (image.src ?? image.image)
|
|
30
|
+
.toLowerCase()
|
|
31
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
32
|
+
.replace(/^-|-$/g, '');
|
|
33
|
+
|
|
34
|
+
return `gallery-caption-${source}-${itemIndex}-${imageIndex}`;
|
|
35
|
+
};
|
|
24
36
|
const galleryImageSizes = [
|
|
25
|
-
`(max-width: 700px) ${
|
|
26
|
-
`${
|
|
37
|
+
`(max-width: 700px) ${maxAvailableWidthPercent.mobile}vw`,
|
|
38
|
+
`${maxAvailableWidthPercent.desktop}vw`,
|
|
27
39
|
].join(', ');
|
|
28
40
|
const hasDimensions = (image: ReturnType<typeof getGeneratedImage>): image is GeneratedImageWithDimensions => (
|
|
29
41
|
Boolean(image) && Number.isFinite(image.width) && Number.isFinite(image.height)
|
|
@@ -54,6 +66,8 @@ const getCarouselAspectRatio = (images: GalleryImage[]) => {
|
|
|
54
66
|
const isPriorityItem = priorityFirstImage && itemIndex === 0;
|
|
55
67
|
|
|
56
68
|
if ('image' in item) {
|
|
69
|
+
const captionId = item.caption ? getCaptionId(item, itemIndex) : undefined;
|
|
70
|
+
|
|
57
71
|
return (
|
|
58
72
|
<figure class="gallery-item">
|
|
59
73
|
<div class="gallery-image-frame">
|
|
@@ -61,11 +75,12 @@ const getCarouselAspectRatio = (images: GalleryImage[]) => {
|
|
|
61
75
|
{...getImageAttributes(item.src ?? item.image, galleryImageSizes)}
|
|
62
76
|
class:list={['gallery-image', getImageClass(item)]}
|
|
63
77
|
alt={item.alt ?? ''}
|
|
78
|
+
aria-describedby={captionId}
|
|
64
79
|
{...getImageLoadingProps(isPriorityItem)}
|
|
65
80
|
/>
|
|
66
81
|
</div>
|
|
67
82
|
{item.caption && (
|
|
68
|
-
<figcaption class="gallery-meta">
|
|
83
|
+
<figcaption class="gallery-meta" id={captionId}>
|
|
69
84
|
<div class="gallery-details">
|
|
70
85
|
<span>{item.caption}</span>
|
|
71
86
|
</div>
|
|
@@ -93,18 +108,23 @@ const getCarouselAspectRatio = (images: GalleryImage[]) => {
|
|
|
93
108
|
<div class="gallery-carousel-stage">
|
|
94
109
|
<div class="gallery-carousel-viewport" data-carousel-viewport>
|
|
95
110
|
<div class="gallery-carousel-slides">
|
|
96
|
-
{item.carousel.map((image, imageIndex) =>
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
111
|
+
{item.carousel.map((image, imageIndex) => {
|
|
112
|
+
const captionId = image.caption ? getCaptionId(image, itemIndex, imageIndex) : undefined;
|
|
113
|
+
|
|
114
|
+
return (
|
|
115
|
+
<figure class="gallery-carousel-slide" data-carousel-slide>
|
|
116
|
+
<div class="gallery-carousel-image-frame">
|
|
117
|
+
<img
|
|
118
|
+
{...getImageAttributes(image.src ?? image.image, galleryImageSizes)}
|
|
119
|
+
class:list={['gallery-image', getImageClass(image)]}
|
|
120
|
+
alt={image.alt ?? ''}
|
|
121
|
+
aria-describedby={captionId}
|
|
122
|
+
{...getImageLoadingProps(isPriorityItem && imageIndex === 0)}
|
|
123
|
+
/>
|
|
124
|
+
</div>
|
|
125
|
+
</figure>
|
|
126
|
+
);
|
|
127
|
+
})}
|
|
108
128
|
</div>
|
|
109
129
|
</div>
|
|
110
130
|
<button
|
|
@@ -134,6 +154,7 @@ const getCarouselAspectRatio = (images: GalleryImage[]) => {
|
|
|
134
154
|
image.caption && (
|
|
135
155
|
<p
|
|
136
156
|
class="gallery-meta gallery-carousel-caption"
|
|
157
|
+
id={getCaptionId(image, itemIndex, imageIndex)}
|
|
137
158
|
data-carousel-caption
|
|
138
159
|
data-carousel-caption-index={imageIndex}
|
|
139
160
|
>
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
---
|
|
2
|
+
import projectConfig from '../../scripts/lib/project-config.mjs';
|
|
3
|
+
|
|
4
|
+
type Banner = {
|
|
5
|
+
id: string;
|
|
6
|
+
tone: 'warning';
|
|
7
|
+
title: string;
|
|
8
|
+
text: string;
|
|
9
|
+
visible?: {
|
|
10
|
+
from?: string;
|
|
11
|
+
until?: string;
|
|
12
|
+
};
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
interface Props {
|
|
16
|
+
banners: Banner[];
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const { banners } = Astro.props;
|
|
20
|
+
const hashBanner = (banner: Banner) => {
|
|
21
|
+
const value = [
|
|
22
|
+
banner.id,
|
|
23
|
+
banner.tone,
|
|
24
|
+
banner.title,
|
|
25
|
+
banner.text,
|
|
26
|
+
banner.visible?.from ?? '',
|
|
27
|
+
banner.visible?.until ?? '',
|
|
28
|
+
].join('\u001f');
|
|
29
|
+
let hash = 0;
|
|
30
|
+
|
|
31
|
+
for (const character of value) {
|
|
32
|
+
hash = ((hash << 5) - hash + character.charCodeAt(0)) | 0;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return (hash >>> 0).toString(36);
|
|
36
|
+
};
|
|
37
|
+
const getStorageKey = (banner: Banner) => `norna-banner:${banner.id}:${hashBanner(banner)}`;
|
|
38
|
+
const dismissBannerLabel = projectConfig.locale.labels.dismissBanner;
|
|
39
|
+
const siteBannersLabel = projectConfig.locale.labels.siteBanners;
|
|
40
|
+
---
|
|
41
|
+
|
|
42
|
+
{banners.length > 0 && (
|
|
43
|
+
<aside class="site-banners" aria-label={siteBannersLabel}>
|
|
44
|
+
{banners.map((banner) => {
|
|
45
|
+
const storageKey = getStorageKey(banner);
|
|
46
|
+
const dismissLabel = `${dismissBannerLabel}: ${banner.title}`;
|
|
47
|
+
|
|
48
|
+
return (
|
|
49
|
+
<article
|
|
50
|
+
class:list={['site-banner', `site-banner-${banner.tone}`]}
|
|
51
|
+
data-norna-banner
|
|
52
|
+
data-banner-key={storageKey}
|
|
53
|
+
role="status"
|
|
54
|
+
>
|
|
55
|
+
<div class="site-banner-copy">
|
|
56
|
+
<p class="site-banner-title">{banner.title}</p>
|
|
57
|
+
<p class="site-banner-text">{banner.text}</p>
|
|
58
|
+
</div>
|
|
59
|
+
<button class="site-banner-dismiss" type="button" data-dismiss-banner aria-label={dismissLabel} title={dismissLabel}>
|
|
60
|
+
<span aria-hidden="true">×</span>
|
|
61
|
+
</button>
|
|
62
|
+
</article>
|
|
63
|
+
);
|
|
64
|
+
})}
|
|
65
|
+
</aside>
|
|
66
|
+
)}
|
|
67
|
+
|
|
68
|
+
<script is:inline>
|
|
69
|
+
(() => {
|
|
70
|
+
const storagePrefix = 'norna-banner:';
|
|
71
|
+
const banners = Array.from(document.querySelectorAll('[data-norna-banner]'));
|
|
72
|
+
|
|
73
|
+
banners.forEach((banner) => {
|
|
74
|
+
const storageKey = banner.getAttribute('data-banner-key');
|
|
75
|
+
const dismissButton = banner.querySelector('[data-dismiss-banner]');
|
|
76
|
+
|
|
77
|
+
if (!storageKey || !(dismissButton instanceof HTMLButtonElement)) return;
|
|
78
|
+
|
|
79
|
+
try {
|
|
80
|
+
if (localStorage.getItem(storageKey) === 'dismissed') {
|
|
81
|
+
banner.hidden = true;
|
|
82
|
+
}
|
|
83
|
+
} catch {
|
|
84
|
+
// Private browsing modes may deny storage; the banner remains usable.
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
dismissButton.addEventListener('click', () => {
|
|
88
|
+
try {
|
|
89
|
+
if (storageKey.startsWith(storagePrefix)) {
|
|
90
|
+
localStorage.setItem(storageKey, 'dismissed');
|
|
91
|
+
}
|
|
92
|
+
} catch {
|
|
93
|
+
// Dismissing still works for the current page when storage is unavailable.
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
banner.hidden = true;
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
})();
|
|
100
|
+
</script>
|