@janga/norna 0.7.8 → 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 +18 -11
- package/docs/configuration.md +9 -54
- package/docs/content.md +6 -7
- package/docs/design/norna-diagram-design.md +158 -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 +59 -0
- package/examples/typography-presets/.astro/collections/sitewide.schema.json +114 -0
- package/examples/typography-presets/.astro/collections/theme.schema.json +770 -0
- package/examples/typography-presets/.astro/content-assets.mjs +4 -0
- package/examples/typography-presets/.astro/content-modules.mjs +1 -0
- package/examples/typography-presets/.astro/content.d.ts +183 -0
- package/examples/typography-presets/.astro/data-store.json +1 -0
- package/examples/typography-presets/.astro/dev-local.json +8 -0
- package/examples/typography-presets/.astro/dev.json +15 -0
- package/examples/typography-presets/.astro/dev.log +45 -0
- package/examples/typography-presets/.astro/settings.json +5 -0
- package/examples/typography-presets/.astro/types.d.ts +2 -0
- package/examples/typography-presets/node_modules/.vite/deps/_metadata.json +80 -0
- package/examples/typography-presets/node_modules/.vite/deps/astro-D_GIpJIE.js +434 -0
- package/examples/typography-presets/node_modules/.vite/deps/astro-D_GIpJIE.js.map +1 -0
- package/examples/typography-presets/node_modules/.vite/deps/astro_n_aria-query.js +6390 -0
- package/examples/typography-presets/node_modules/.vite/deps/astro_n_aria-query.js.map +1 -0
- package/examples/typography-presets/node_modules/.vite/deps/astro_n_axobject-query.js +2702 -0
- package/examples/typography-presets/node_modules/.vite/deps/astro_n_axobject-query.js.map +1 -0
- package/examples/typography-presets/node_modules/.vite/deps/astro_n_html-escaper.js +65 -0
- package/examples/typography-presets/node_modules/.vite/deps/astro_n_html-escaper.js.map +1 -0
- package/examples/typography-presets/node_modules/.vite/deps/astro_runtime_client_dev-toolbar_entrypoint__js.js +455 -0
- package/examples/typography-presets/node_modules/.vite/deps/astro_runtime_client_dev-toolbar_entrypoint__js.js.map +1 -0
- package/examples/typography-presets/node_modules/.vite/deps/audit-DM9Hkl5B.js +1534 -0
- package/examples/typography-presets/node_modules/.vite/deps/audit-DM9Hkl5B.js.map +1 -0
- package/examples/typography-presets/node_modules/.vite/deps/embla-carousel.js +1389 -0
- package/examples/typography-presets/node_modules/.vite/deps/embla-carousel.js.map +1 -0
- package/examples/typography-presets/node_modules/.vite/deps/highlight-DnZ1ULOL.js +48 -0
- package/examples/typography-presets/node_modules/.vite/deps/highlight-DnZ1ULOL.js.map +1 -0
- package/examples/typography-presets/node_modules/.vite/deps/icons-RyebKi45.js +42 -0
- package/examples/typography-presets/node_modules/.vite/deps/icons-RyebKi45.js.map +1 -0
- package/examples/typography-presets/node_modules/.vite/deps/package.json +3 -0
- package/examples/typography-presets/node_modules/.vite/deps/rolldown-runtime-BvCyGRYZ.js +4 -0
- package/examples/typography-presets/node_modules/.vite/deps/settings-DaU-gNnK.js +190 -0
- package/examples/typography-presets/node_modules/.vite/deps/settings-DaU-gNnK.js.map +1 -0
- package/examples/typography-presets/node_modules/.vite/deps/toolbar-63PA619H.js +458 -0
- package/examples/typography-presets/node_modules/.vite/deps/toolbar-63PA619H.js.map +1 -0
- package/examples/typography-presets/node_modules/.vite/deps/ui-library--bkcNSYz.js +1016 -0
- package/examples/typography-presets/node_modules/.vite/deps/ui-library--bkcNSYz.js.map +1 -0
- package/examples/typography-presets/node_modules/.vite/deps/window-jPkpKVcy.js +32 -0
- package/examples/typography-presets/node_modules/.vite/deps/window-jPkpKVcy.js.map +1 -0
- package/examples/typography-presets/node_modules/.vite/deps/xray-BXCHpEJP.js +111 -0
- package/examples/typography-presets/node_modules/.vite/deps/xray-BXCHpEJP.js.map +1 -0
- package/examples/typography-presets/site/.norna/generated-images.json +290 -0
- package/examples/typography-presets/site/config.mjs +29 -0
- package/examples/typography-presets/site/content.md +16 -0
- package/examples/typography-presets/site/public/robots.txt +2 -0
- package/examples/typography-presets/site/routes/010-quiet-gallery/images/quiet-gallery/puppy-portrait.jpg +0 -0
- package/examples/typography-presets/site/routes/010-quiet-gallery/images/quiet-gallery/shepherd-portrait.jpg +0 -0
- package/examples/typography-presets/site/routes/010-quiet-gallery/images/quiet-gallery/white-puppy.jpg +0 -0
- 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/images/compact-gallery/black-schnauzer.jpg +0 -0
- package/examples/typography-presets/site/routes/020-compact-gallery/images/compact-gallery/retriever-portrait.jpg +0 -0
- package/examples/typography-presets/site/routes/020-compact-gallery/images/compact-gallery/schnauzer-portrait.jpg +0 -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/images/text-forward/boxer-portrait.jpg +0 -0
- package/examples/typography-presets/site/routes/030-text-forward/images/text-forward/grass-puppy.jpg +0 -0
- package/examples/typography-presets/site/routes/030-text-forward/images/text-forward/grey-street-dog.jpg +0 -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/images/statement/black-pedigree.jpg +0 -0
- package/examples/typography-presets/site/routes/040-statement/images/statement/sheepdog-grass.jpg +0 -0
- package/examples/typography-presets/site/routes/040-statement/images/statement/sheepdog-wide.jpg +0 -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 +43 -0
- 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 -42
- package/scripts/lib/site-paths.mjs +2 -0
- package/scripts/lib/sitewide-config.mjs +34 -0
- package/scripts/lib/theme-config.mjs +72 -6
- package/scripts/lib/typography.mjs +20 -26
- package/scripts/show-typography.mjs +40 -147
- package/scripts/sync-content-sections.mjs +19 -14
- package/scripts/test-content-check.mjs +76 -14
- package/scripts/test-content-model-v2.mjs +154 -0
- package/scripts/test-engine-commands.mjs +3 -1
- package/scripts/test-package-check.mjs +78 -19
- 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 +194 -36
- 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 +236 -44
- 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
|
@@ -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>
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
import projectConfig from '../../scripts/lib/project-config.mjs';
|
|
3
3
|
import { withBasePath } from '../lib/basePath';
|
|
4
|
-
import type {
|
|
4
|
+
import type { SectionNavigation } from '../lib/sectionContent';
|
|
5
5
|
import type { SitePage } from '../lib/sitePages';
|
|
6
6
|
|
|
7
7
|
type FrameColors = {
|
|
@@ -11,13 +11,20 @@ type FrameColors = {
|
|
|
11
11
|
|
|
12
12
|
interface Props {
|
|
13
13
|
brandLabel: string;
|
|
14
|
+
logo?: {
|
|
15
|
+
src: string;
|
|
16
|
+
alt: string;
|
|
17
|
+
height?: string;
|
|
18
|
+
};
|
|
14
19
|
currentPage: SitePage;
|
|
15
|
-
|
|
16
|
-
|
|
20
|
+
pageNavigation: Array<{
|
|
21
|
+
page: SitePage;
|
|
22
|
+
sections: SectionNavigation[];
|
|
23
|
+
}>;
|
|
17
24
|
frameColors: FrameColors;
|
|
18
25
|
}
|
|
19
26
|
|
|
20
|
-
const { brandLabel,
|
|
27
|
+
const { brandLabel, logo, currentPage, pageNavigation, frameColors } = Astro.props;
|
|
21
28
|
const smoothScrollConfig = projectConfig.navigation.smoothScroll;
|
|
22
29
|
const siteTopBackgroundColor = frameColors.backgroundColor === 'transparent'
|
|
23
30
|
? 'var(--color-page)'
|
|
@@ -25,12 +32,15 @@ const siteTopBackgroundColor = frameColors.backgroundColor === 'transparent'
|
|
|
25
32
|
const navigationStyle = [
|
|
26
33
|
`--site-top-background-color: ${siteTopBackgroundColor}`,
|
|
27
34
|
`--site-top-text-color: ${frameColors.textColor}`,
|
|
35
|
+
...(logo?.height ? [`--site-brand-logo-height: ${logo.height}`] : []),
|
|
28
36
|
].join('; ');
|
|
29
|
-
const showSiteNavigation =
|
|
30
|
-
const
|
|
31
|
-
const
|
|
37
|
+
const showSiteNavigation = pageNavigation.length > 1;
|
|
38
|
+
const homePage = pageNavigation.find(({ page }) => page.isHome)?.page ?? currentPage;
|
|
39
|
+
const currentPageNavigation = pageNavigation.find(({ page }) => page.pathname === currentPage.pathname);
|
|
32
40
|
const isCurrentPage = (page: SitePage) => page.pathname === currentPage.pathname;
|
|
33
41
|
const pageHref = (page: SitePage) => withBasePath(projectConfig.site.basePath, page.pathname);
|
|
42
|
+
const sectionHref = (page: SitePage, section: SectionNavigation) => `${pageHref(page)}#${section.id}`;
|
|
43
|
+
const hasSectionMenu = (pageSections: SectionNavigation[]) => pageSections.length > 1;
|
|
34
44
|
---
|
|
35
45
|
|
|
36
46
|
<a class="skip-link" href="#main-content">{projectConfig.locale.labels.skipToContent}</a>
|
|
@@ -42,15 +52,30 @@ const pageHref = (page: SitePage) => withBasePath(projectConfig.site.basePath, p
|
|
|
42
52
|
>
|
|
43
53
|
{showSiteNavigation && (
|
|
44
54
|
<div class="site-nav-row">
|
|
45
|
-
<a class="site-brand" href={pageHref(homePage)}>
|
|
55
|
+
<a class="site-brand" href={pageHref(homePage)}>
|
|
56
|
+
{logo ? <img class="site-brand-logo" src={withBasePath(projectConfig.site.basePath, logo.src)} alt={logo.alt} /> : brandLabel}
|
|
57
|
+
</a>
|
|
46
58
|
|
|
47
59
|
<nav class="site-nav" aria-label={projectConfig.locale.labels.siteNavigation}>
|
|
48
60
|
<ul>
|
|
49
|
-
{
|
|
50
|
-
<li>
|
|
61
|
+
{pageNavigation.map(({ page, sections: pageSections }) => (
|
|
62
|
+
<li class:list={['site-nav-item', { 'site-nav-item-has-sections': hasSectionMenu(pageSections) }]}>
|
|
51
63
|
<a href={pageHref(page)} aria-current={isCurrentPage(page) ? 'page' : undefined}>
|
|
52
64
|
{page.navigation.label}
|
|
53
65
|
</a>
|
|
66
|
+
{hasSectionMenu(pageSections) && (
|
|
67
|
+
<div class="site-nav-submenu">
|
|
68
|
+
<nav aria-label={`${page.navigation.label} ${projectConfig.locale.labels.pageNavigation}`}>
|
|
69
|
+
<ul>
|
|
70
|
+
{pageSections.map((section) => (
|
|
71
|
+
<li>
|
|
72
|
+
<a href={sectionHref(page, section)}>{section.title}</a>
|
|
73
|
+
</li>
|
|
74
|
+
))}
|
|
75
|
+
</ul>
|
|
76
|
+
</nav>
|
|
77
|
+
</div>
|
|
78
|
+
)}
|
|
54
79
|
</li>
|
|
55
80
|
))}
|
|
56
81
|
</ul>
|
|
@@ -64,7 +89,7 @@ const pageHref = (page: SitePage) => withBasePath(projectConfig.site.basePath, p
|
|
|
64
89
|
<nav class="mobile-site-nav" aria-label={projectConfig.locale.labels.siteNavigation}>
|
|
65
90
|
<h2>{projectConfig.locale.labels.siteNavigation}</h2>
|
|
66
91
|
<ul>
|
|
67
|
-
{
|
|
92
|
+
{pageNavigation.map(({ page }) => (
|
|
68
93
|
<li>
|
|
69
94
|
<a href={pageHref(page)} aria-current={isCurrentPage(page) ? 'page' : undefined}>
|
|
70
95
|
{page.navigation.label}
|
|
@@ -74,13 +99,13 @@ const pageHref = (page: SitePage) => withBasePath(projectConfig.site.basePath, p
|
|
|
74
99
|
</ul>
|
|
75
100
|
</nav>
|
|
76
101
|
|
|
77
|
-
{
|
|
102
|
+
{currentPageNavigation && hasSectionMenu(currentPageNavigation.sections) && (
|
|
78
103
|
<nav class="mobile-page-nav" aria-label={projectConfig.locale.labels.pageNavigation}>
|
|
79
104
|
<h2>{projectConfig.locale.labels.pageNavigation}</h2>
|
|
80
105
|
<ul>
|
|
81
|
-
{sections.map((section, index) => (
|
|
106
|
+
{currentPageNavigation.sections.map((section, index) => (
|
|
82
107
|
<li>
|
|
83
|
-
<a href={
|
|
108
|
+
<a href={sectionHref(currentPage, section)} aria-current={index === 0 ? 'true' : undefined}>
|
|
84
109
|
{section.title}
|
|
85
110
|
</a>
|
|
86
111
|
</li>
|
|
@@ -93,25 +118,12 @@ const pageHref = (page: SitePage) => withBasePath(projectConfig.site.basePath, p
|
|
|
93
118
|
</div>
|
|
94
119
|
)}
|
|
95
120
|
|
|
96
|
-
{showPageNavigation && (
|
|
97
|
-
<nav class="page-nav" aria-label={projectConfig.locale.labels.pageNavigation}>
|
|
98
|
-
<ul>
|
|
99
|
-
{sections.map((section, index) => (
|
|
100
|
-
<li>
|
|
101
|
-
<a href={`#${section.id}`} aria-current={index === 0 ? 'true' : undefined}>
|
|
102
|
-
{section.title}
|
|
103
|
-
</a>
|
|
104
|
-
</li>
|
|
105
|
-
))}
|
|
106
|
-
</ul>
|
|
107
|
-
</nav>
|
|
108
|
-
)}
|
|
109
121
|
</header>
|
|
110
122
|
|
|
111
123
|
<script>
|
|
112
124
|
const siteTop = document.querySelector('.site-top');
|
|
113
125
|
const sections = Array.from(document.querySelectorAll('.site-section'));
|
|
114
|
-
const links = Array.from(document.querySelectorAll('.
|
|
126
|
+
const links = Array.from(document.querySelectorAll('.site-nav-submenu a, .mobile-page-nav a'));
|
|
115
127
|
const mobileMenu = document.querySelector('.mobile-nav-menu');
|
|
116
128
|
const smoothScrollConfig = JSON.parse(siteTop?.getAttribute('data-smooth-scroll') ?? '{}');
|
|
117
129
|
const anchorPadding = 0;
|
|
@@ -159,7 +171,9 @@ const pageHref = (page: SitePage) => withBasePath(projectConfig.site.basePath, p
|
|
|
159
171
|
activeSectionId = section.id;
|
|
160
172
|
|
|
161
173
|
links.forEach((link) => {
|
|
162
|
-
const
|
|
174
|
+
const linkUrl = new URL(link.href, window.location.href);
|
|
175
|
+
const isCurrentPageLink = linkUrl.pathname === window.location.pathname;
|
|
176
|
+
const isActive = isCurrentPageLink && linkUrl.hash === `#${section.id}`;
|
|
163
177
|
if (isActive) {
|
|
164
178
|
link.setAttribute('aria-current', 'true');
|
|
165
179
|
} else {
|
|
@@ -372,7 +386,10 @@ const pageHref = (page: SitePage) => withBasePath(projectConfig.site.basePath, p
|
|
|
372
386
|
|
|
373
387
|
links.forEach((link) => {
|
|
374
388
|
link.addEventListener('click', (event) => {
|
|
375
|
-
const
|
|
389
|
+
const linkUrl = new URL(link.href, window.location.href);
|
|
390
|
+
if (linkUrl.pathname !== window.location.pathname) return;
|
|
391
|
+
|
|
392
|
+
const section = getSectionFromHash(linkUrl.hash);
|
|
376
393
|
if (!(section instanceof HTMLElement)) return;
|
|
377
394
|
|
|
378
395
|
const top = getSectionScrollTop(section);
|
|
@@ -380,7 +397,7 @@ const pageHref = (page: SitePage) => withBasePath(projectConfig.site.basePath, p
|
|
|
380
397
|
|
|
381
398
|
event.preventDefault();
|
|
382
399
|
|
|
383
|
-
const targetHash =
|
|
400
|
+
const targetHash = linkUrl.hash;
|
|
384
401
|
const duration = getScrollDuration(top - window.scrollY);
|
|
385
402
|
|
|
386
403
|
setHash(targetHash);
|
|
@@ -3,12 +3,16 @@ import { getCollection, type CollectionEntry } from 'astro:content';
|
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import BaseLayout from '../layouts/BaseLayout.astro';
|
|
5
5
|
import SiteNavigation from './SiteNavigation.astro';
|
|
6
|
+
import SiteBanners from './SiteBanners.astro';
|
|
6
7
|
import SiteSection from './SiteSection.astro';
|
|
7
|
-
import { getSectionsContent } from '../lib/sectionContent';
|
|
8
|
+
import { getSectionNavigation, getSectionsContent } from '../lib/sectionContent';
|
|
9
|
+
import { getRouteTheme } from '../lib/routeThemes';
|
|
8
10
|
import { getNavigationPages, getSitePage, getSitePages } from '../lib/sitePages';
|
|
9
11
|
import { getToday, isVisibleToday } from '../lib/visibility';
|
|
12
|
+
import { getLogoAsset } from '../lib/sitePublicAssets';
|
|
10
13
|
import { readSiteFile } from '../../scripts/lib/site-content.mjs';
|
|
11
14
|
import { siteContentPath, siteRoutesDir } from '../../scripts/lib/site-paths.mjs';
|
|
15
|
+
import { resolveThemeVisualConfig } from '../../scripts/lib/project-config.mjs';
|
|
12
16
|
import { resolveFrameColors, resolvePagePresentation } from '../../scripts/lib/presentation.mjs';
|
|
13
17
|
|
|
14
18
|
type SiteEntry = CollectionEntry<'site'>;
|
|
@@ -28,23 +32,55 @@ const pageContentPath = currentPage.routeDirectory
|
|
|
28
32
|
? path.join(siteRoutesDir, currentPage.routeDirectory, 'route-content.md')
|
|
29
33
|
: siteContentPath;
|
|
30
34
|
const rawMarkdown = (await readSiteFile(pageContentPath, siteContentLabel)).body;
|
|
31
|
-
const [
|
|
35
|
+
const [siteTheme] = await getCollection('theme');
|
|
36
|
+
if (!siteTheme) {
|
|
37
|
+
throw new Error('site/theme.md is required. Add a root theme before building the site.');
|
|
38
|
+
}
|
|
39
|
+
const [sitewide] = await getCollection('sitewide');
|
|
40
|
+
const sitewideData = sitewide?.data ?? {};
|
|
32
41
|
const navigationPages = getNavigationPages(sitePages);
|
|
33
42
|
const homePage = sitePages.find((page) => page.isHome) ?? currentPage;
|
|
34
|
-
const
|
|
35
|
-
const
|
|
36
|
-
const
|
|
37
|
-
const
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
43
|
+
const siteThemeData = siteTheme.data;
|
|
44
|
+
const routeTheme = await getRouteTheme(currentPage.routeDirectory);
|
|
45
|
+
const themeData = routeTheme?.data ?? siteThemeData;
|
|
46
|
+
const themeLabel = routeTheme
|
|
47
|
+
? `${siteDirectory || 'site'}/${routeTheme.id}`
|
|
48
|
+
: `${siteDirectory || 'site'}/theme.md`;
|
|
49
|
+
const visualTheme = resolveThemeVisualConfig(themeData, themeLabel);
|
|
50
|
+
const brandLabel = sitewideData.navigation?.brand ?? homePage.title;
|
|
51
|
+
const logoAsset = getLogoAsset();
|
|
52
|
+
const logoSettings = sitewideData.navigation?.logo;
|
|
53
|
+
const logo = logoAsset
|
|
54
|
+
? {
|
|
55
|
+
src: logoAsset.href,
|
|
56
|
+
alt: logoSettings?.alt ?? brandLabel,
|
|
57
|
+
height: logoSettings?.height,
|
|
58
|
+
}
|
|
59
|
+
: undefined;
|
|
60
|
+
const pagePresentation = resolvePagePresentation(themeData);
|
|
61
|
+
const frameColors = resolveFrameColors({ theme: themeData });
|
|
42
62
|
const sections = entry.data.sections;
|
|
43
63
|
const siteHtml = entry.rendered?.html ?? '';
|
|
44
|
-
const resolvedSections = getSectionsContent(siteHtml, rawMarkdown, sections, currentPage
|
|
64
|
+
const resolvedSections = await getSectionsContent(siteHtml, rawMarkdown, sections, currentPage);
|
|
45
65
|
const today = getToday();
|
|
66
|
+
const siteBanners = (sitewideData.banners ?? []).filter((banner) => isVisibleToday(banner.visible, today));
|
|
46
67
|
const visibleSections = resolvedSections.filter((section) => isVisibleToday(section.visible, today));
|
|
47
68
|
|
|
69
|
+
const getPageContent = async (page: SitePage) => {
|
|
70
|
+
if (page.pathname === currentPage.pathname) {
|
|
71
|
+
return visibleSections.map(({ id, title }) => ({ id, title }));
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const pageSections = getSectionNavigation(page.entry.rendered?.html ?? '', page.entry.data.sections);
|
|
75
|
+
|
|
76
|
+
return pageSections.filter((section) => isVisibleToday(page.entry.data.sections[section.id]?.visible, today));
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
const pageNavigation = await Promise.all(navigationPages.map(async (page) => ({
|
|
80
|
+
page,
|
|
81
|
+
sections: await getPageContent(page),
|
|
82
|
+
})));
|
|
83
|
+
|
|
48
84
|
if (visibleSections.length === 0) {
|
|
49
85
|
throw new Error(`No sections are visible for ${today}. Check sections.<section-id>.visible in ${siteContentLabel}.`);
|
|
50
86
|
}
|
|
@@ -53,22 +89,28 @@ if (visibleSections.length === 0) {
|
|
|
53
89
|
<BaseLayout
|
|
54
90
|
title={entry.data.title}
|
|
55
91
|
description={entry.data.description}
|
|
92
|
+
presentation={pagePresentation}
|
|
93
|
+
visualTheme={visualTheme}
|
|
56
94
|
frameColors={frameColors}
|
|
57
95
|
pathname={currentPage.pathname}
|
|
96
|
+
footer={sitewideData.footer}
|
|
58
97
|
>
|
|
59
98
|
<SiteNavigation
|
|
60
99
|
brandLabel={brandLabel}
|
|
100
|
+
logo={logo}
|
|
61
101
|
currentPage={currentPage}
|
|
62
|
-
|
|
63
|
-
sections={visibleSections}
|
|
102
|
+
pageNavigation={pageNavigation}
|
|
64
103
|
frameColors={frameColors}
|
|
65
104
|
/>
|
|
105
|
+
<SiteBanners banners={siteBanners} />
|
|
66
106
|
|
|
67
107
|
<main id="main-content" class="site-content">
|
|
68
108
|
{visibleSections.map((section, index) => (
|
|
69
109
|
<SiteSection
|
|
70
110
|
section={section}
|
|
71
111
|
pagePresentation={pagePresentation}
|
|
112
|
+
visualTheme={visualTheme}
|
|
113
|
+
sectionIndex={index}
|
|
72
114
|
headingLevel={index === 0 ? 1 : 2}
|
|
73
115
|
priorityGalleryImage={index === 0}
|
|
74
116
|
/>
|