@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.
Files changed (234) hide show
  1. package/README.md +1 -1
  2. package/docs/README.md +18 -11
  3. package/docs/configuration.md +9 -54
  4. package/docs/content.md +6 -7
  5. package/docs/design/norna-diagram-design.md +158 -0
  6. package/docs/getting-started.md +18 -16
  7. package/docs/routes.md +7 -6
  8. package/docs/site-structure.md +4 -2
  9. package/docs/sitewide-content.md +74 -0
  10. package/docs/theme.md +61 -128
  11. package/docs/typography.md +45 -33
  12. package/examples/001-dog-shelter-single-page/README.md +9 -0
  13. package/examples/001-dog-shelter-single-page/site/config.mjs +27 -0
  14. package/examples/001-dog-shelter-single-page/site/content.md +38 -0
  15. package/examples/001-dog-shelter-single-page/site/images/what-we-do/dog-house.svg +25 -0
  16. package/examples/001-dog-shelter-single-page/site/images/you-can-help/heart.svg +13 -0
  17. package/examples/001-dog-shelter-single-page/site/public/favicon.svg +10 -0
  18. package/examples/001-dog-shelter-single-page/site/public/robots.txt +2 -0
  19. package/examples/001-dog-shelter-single-page/site/sitewide-content.md +6 -0
  20. package/examples/001-dog-shelter-single-page/site/theme.md +25 -0
  21. package/examples/002-dog-shelter-multi-page/README.md +9 -0
  22. package/examples/002-dog-shelter-multi-page/site/config.mjs +27 -0
  23. package/examples/002-dog-shelter-multi-page/site/content.md +12 -0
  24. package/examples/002-dog-shelter-multi-page/site/public/favicon.svg +10 -0
  25. package/examples/002-dog-shelter-multi-page/site/public/robots.txt +2 -0
  26. package/examples/002-dog-shelter-multi-page/site/routes/010-dogs/images/dogs/rex.svg +19 -0
  27. package/examples/002-dog-shelter-multi-page/site/routes/010-dogs/images/dogs/rover.svg +19 -0
  28. package/examples/002-dog-shelter-multi-page/site/routes/010-dogs/route-content.md +30 -0
  29. package/examples/002-dog-shelter-multi-page/site/routes/020-adopt/route-content.md +12 -0
  30. package/examples/002-dog-shelter-multi-page/site/sitewide-content.md +6 -0
  31. package/examples/002-dog-shelter-multi-page/site/theme.md +25 -0
  32. package/examples/dog-gallery/.astro/collections/site.schema.json +776 -0
  33. package/examples/dog-gallery/.astro/collections/theme.schema.json +672 -0
  34. package/examples/dog-gallery/.astro/content-assets.mjs +4 -0
  35. package/examples/dog-gallery/.astro/content-modules.mjs +1 -0
  36. package/examples/dog-gallery/.astro/content.d.ts +175 -0
  37. package/examples/dog-gallery/.astro/data-store.json +1 -0
  38. package/examples/dog-gallery/.astro/dev-local.json +8 -0
  39. package/examples/dog-gallery/.astro/dev.json +13 -0
  40. package/examples/dog-gallery/.astro/dev.log +95 -0
  41. package/examples/dog-gallery/.astro/settings.json +5 -0
  42. package/examples/dog-gallery/.astro/types.d.ts +2 -0
  43. package/examples/dog-gallery/dist/_astro/GalleryGrid.astro_astro_type_script_index_0_lang.B0kxDyGz.js +1 -0
  44. package/examples/dog-gallery/dist/_astro/SitePage.DicGh_5N.css +1 -0
  45. package/examples/dog-gallery/dist/dog-care/index.html +15 -0
  46. package/examples/dog-gallery/dist/dog-shelter-logo.svg +14 -0
  47. package/examples/dog-gallery/dist/favicon.svg +7 -0
  48. package/examples/dog-gallery/dist/images/generated/black-dogs/black-puppy-meadow-7255e3d9-1080.webp +0 -0
  49. package/examples/dog-gallery/dist/images/generated/black-dogs/black-puppy-meadow-7255e3d9-1440.webp +0 -0
  50. package/examples/dog-gallery/dist/images/generated/black-dogs/black-puppy-meadow-7255e3d9-1536.webp +0 -0
  51. package/examples/dog-gallery/dist/images/generated/black-dogs/black-puppy-meadow-7255e3d9-480.webp +0 -0
  52. package/examples/dog-gallery/dist/images/generated/black-dogs/black-puppy-meadow-7255e3d9-768.webp +0 -0
  53. package/examples/dog-gallery/dist/images/generated/black-dogs/photo-of-a-black-dog-5cf614d2-480.webp +0 -0
  54. package/examples/dog-gallery/dist/images/generated/black-dogs/photo-of-a-black-dog-5cf614d2-768.webp +0 -0
  55. package/examples/dog-gallery/dist/images/generated/black-dogs/photo-of-a-black-dog-5cf614d2-960.webp +0 -0
  56. package/examples/dog-gallery/dist/images/generated/brown-dogs/brown-dog-50ef114e-1080.webp +0 -0
  57. package/examples/dog-gallery/dist/images/generated/brown-dogs/brown-dog-50ef114e-1440.webp +0 -0
  58. package/examples/dog-gallery/dist/images/generated/brown-dogs/brown-dog-50ef114e-1920.webp +0 -0
  59. package/examples/dog-gallery/dist/images/generated/brown-dogs/brown-dog-50ef114e-3939.webp +0 -0
  60. package/examples/dog-gallery/dist/images/generated/brown-dogs/brown-dog-50ef114e-480.webp +0 -0
  61. package/examples/dog-gallery/dist/images/generated/brown-dogs/brown-dog-50ef114e-768.webp +0 -0
  62. package/examples/dog-gallery/dist/images/generated/brown-dogs/dog-accompanies-master-7ef804d5-1080.webp +0 -0
  63. package/examples/dog-gallery/dist/images/generated/brown-dogs/dog-accompanies-master-7ef804d5-1440.webp +0 -0
  64. package/examples/dog-gallery/dist/images/generated/brown-dogs/dog-accompanies-master-7ef804d5-1920.webp +0 -0
  65. package/examples/dog-gallery/dist/images/generated/brown-dogs/dog-accompanies-master-7ef804d5-2400.webp +0 -0
  66. package/examples/dog-gallery/dist/images/generated/brown-dogs/dog-accompanies-master-7ef804d5-480.webp +0 -0
  67. package/examples/dog-gallery/dist/images/generated/brown-dogs/dog-accompanies-master-7ef804d5-768.webp +0 -0
  68. package/examples/dog-gallery/dist/images/generated/golden-dogs/golden-retriever-f797fd88-1080.webp +0 -0
  69. package/examples/dog-gallery/dist/images/generated/golden-dogs/golden-retriever-f797fd88-1440.webp +0 -0
  70. package/examples/dog-gallery/dist/images/generated/golden-dogs/golden-retriever-f797fd88-1600.webp +0 -0
  71. package/examples/dog-gallery/dist/images/generated/golden-dogs/golden-retriever-f797fd88-480.webp +0 -0
  72. package/examples/dog-gallery/dist/images/generated/golden-dogs/golden-retriever-f797fd88-768.webp +0 -0
  73. package/examples/dog-gallery/dist/images/generated/golden-dogs/toller-puppy-a2a8fbc0-1080.webp +0 -0
  74. package/examples/dog-gallery/dist/images/generated/golden-dogs/toller-puppy-a2a8fbc0-1440.webp +0 -0
  75. package/examples/dog-gallery/dist/images/generated/golden-dogs/toller-puppy-a2a8fbc0-1920.webp +0 -0
  76. package/examples/dog-gallery/dist/images/generated/golden-dogs/toller-puppy-a2a8fbc0-2576.webp +0 -0
  77. package/examples/dog-gallery/dist/images/generated/golden-dogs/toller-puppy-a2a8fbc0-480.webp +0 -0
  78. package/examples/dog-gallery/dist/images/generated/golden-dogs/toller-puppy-a2a8fbc0-768.webp +0 -0
  79. package/examples/dog-gallery/dist/images/generated/white-dogs/white-cute-dog-077a0689-1080.webp +0 -0
  80. package/examples/dog-gallery/dist/images/generated/white-dogs/white-cute-dog-077a0689-1277.webp +0 -0
  81. package/examples/dog-gallery/dist/images/generated/white-dogs/white-cute-dog-077a0689-480.webp +0 -0
  82. package/examples/dog-gallery/dist/images/generated/white-dogs/white-cute-dog-077a0689-768.webp +0 -0
  83. package/examples/dog-gallery/dist/images/generated/white-dogs/white-puppy-garden-d3ab2ca0-1080.webp +0 -0
  84. package/examples/dog-gallery/dist/images/generated/white-dogs/white-puppy-garden-d3ab2ca0-1440.webp +0 -0
  85. package/examples/dog-gallery/dist/images/generated/white-dogs/white-puppy-garden-d3ab2ca0-1536.webp +0 -0
  86. package/examples/dog-gallery/dist/images/generated/white-dogs/white-puppy-garden-d3ab2ca0-480.webp +0 -0
  87. package/examples/dog-gallery/dist/images/generated/white-dogs/white-puppy-garden-d3ab2ca0-768.webp +0 -0
  88. package/examples/dog-gallery/dist/index.html +45 -0
  89. package/examples/dog-gallery/dist/robots.txt +2 -0
  90. package/examples/dog-gallery/node_modules/.vite/deps/_metadata.json +7 -7
  91. package/examples/dog-gallery/site/.norna/generated-images.json +8 -8
  92. package/examples/dog-gallery/site/config.mjs +0 -20
  93. package/examples/dog-gallery/site/content.md +4 -5
  94. package/examples/dog-gallery/site/public/logo.svg +14 -0
  95. package/examples/dog-gallery/site/sitewide-content.md +16 -0
  96. package/examples/dog-gallery/site/theme.md +13 -24
  97. package/examples/presentation/README.md +15 -0
  98. package/examples/presentation/site/.norna/generated-images.json +66 -0
  99. package/examples/presentation/site/config.mjs +29 -0
  100. package/examples/presentation/site/content.md +67 -0
  101. package/examples/presentation/site/public/robots.txt +2 -0
  102. package/examples/presentation/site/routes/010-presets/route-content.md +63 -0
  103. package/examples/presentation/site/routes/010-presets/theme.md +4 -0
  104. package/examples/presentation/site/routes/020-media/images/cards/card-carousel.svg +11 -0
  105. package/examples/presentation/site/routes/020-media/images/cards/card-stack.svg +9 -0
  106. package/examples/presentation/site/routes/020-media/images/cards/card-surfaces.svg +8 -0
  107. package/examples/presentation/site/routes/020-media/images/carousel/carousel-one.svg +8 -0
  108. package/examples/presentation/site/routes/020-media/images/carousel/carousel-three.svg +13 -0
  109. package/examples/presentation/site/routes/020-media/images/carousel/carousel-two.svg +8 -0
  110. package/examples/presentation/site/routes/020-media/images/stack/stack-one.svg +8 -0
  111. package/examples/presentation/site/routes/020-media/images/stack/stack-two.svg +9 -0
  112. package/examples/presentation/site/routes/020-media/route-content.md +77 -0
  113. package/examples/presentation/site/routes/020-media/theme.md +4 -0
  114. package/examples/presentation/site/routes/030-surfaces/route-content.md +44 -0
  115. package/examples/presentation/site/routes/030-surfaces/theme.md +7 -0
  116. package/examples/presentation/site/sitewide-content.md +6 -0
  117. package/examples/presentation/site/theme.md +43 -0
  118. package/examples/typography-presets/.astro/collections/routeTheme.schema.json +746 -0
  119. package/examples/typography-presets/.astro/collections/site.schema.json +59 -0
  120. package/examples/typography-presets/.astro/collections/sitewide.schema.json +114 -0
  121. package/examples/typography-presets/.astro/collections/theme.schema.json +770 -0
  122. package/examples/typography-presets/.astro/content-assets.mjs +4 -0
  123. package/examples/typography-presets/.astro/content-modules.mjs +1 -0
  124. package/examples/typography-presets/.astro/content.d.ts +183 -0
  125. package/examples/typography-presets/.astro/data-store.json +1 -0
  126. package/examples/typography-presets/.astro/dev-local.json +8 -0
  127. package/examples/typography-presets/.astro/dev.json +15 -0
  128. package/examples/typography-presets/.astro/dev.log +45 -0
  129. package/examples/typography-presets/.astro/settings.json +5 -0
  130. package/examples/typography-presets/.astro/types.d.ts +2 -0
  131. package/examples/typography-presets/node_modules/.vite/deps/_metadata.json +80 -0
  132. package/examples/typography-presets/node_modules/.vite/deps/astro-D_GIpJIE.js +434 -0
  133. package/examples/typography-presets/node_modules/.vite/deps/astro-D_GIpJIE.js.map +1 -0
  134. package/examples/typography-presets/node_modules/.vite/deps/astro_n_aria-query.js +6390 -0
  135. package/examples/typography-presets/node_modules/.vite/deps/astro_n_aria-query.js.map +1 -0
  136. package/examples/typography-presets/node_modules/.vite/deps/astro_n_axobject-query.js +2702 -0
  137. package/examples/typography-presets/node_modules/.vite/deps/astro_n_axobject-query.js.map +1 -0
  138. package/examples/typography-presets/node_modules/.vite/deps/astro_n_html-escaper.js +65 -0
  139. package/examples/typography-presets/node_modules/.vite/deps/astro_n_html-escaper.js.map +1 -0
  140. package/examples/typography-presets/node_modules/.vite/deps/astro_runtime_client_dev-toolbar_entrypoint__js.js +455 -0
  141. package/examples/typography-presets/node_modules/.vite/deps/astro_runtime_client_dev-toolbar_entrypoint__js.js.map +1 -0
  142. package/examples/typography-presets/node_modules/.vite/deps/audit-DM9Hkl5B.js +1534 -0
  143. package/examples/typography-presets/node_modules/.vite/deps/audit-DM9Hkl5B.js.map +1 -0
  144. package/examples/typography-presets/node_modules/.vite/deps/embla-carousel.js +1389 -0
  145. package/examples/typography-presets/node_modules/.vite/deps/embla-carousel.js.map +1 -0
  146. package/examples/typography-presets/node_modules/.vite/deps/highlight-DnZ1ULOL.js +48 -0
  147. package/examples/typography-presets/node_modules/.vite/deps/highlight-DnZ1ULOL.js.map +1 -0
  148. package/examples/typography-presets/node_modules/.vite/deps/icons-RyebKi45.js +42 -0
  149. package/examples/typography-presets/node_modules/.vite/deps/icons-RyebKi45.js.map +1 -0
  150. package/examples/typography-presets/node_modules/.vite/deps/package.json +3 -0
  151. package/examples/typography-presets/node_modules/.vite/deps/rolldown-runtime-BvCyGRYZ.js +4 -0
  152. package/examples/typography-presets/node_modules/.vite/deps/settings-DaU-gNnK.js +190 -0
  153. package/examples/typography-presets/node_modules/.vite/deps/settings-DaU-gNnK.js.map +1 -0
  154. package/examples/typography-presets/node_modules/.vite/deps/toolbar-63PA619H.js +458 -0
  155. package/examples/typography-presets/node_modules/.vite/deps/toolbar-63PA619H.js.map +1 -0
  156. package/examples/typography-presets/node_modules/.vite/deps/ui-library--bkcNSYz.js +1016 -0
  157. package/examples/typography-presets/node_modules/.vite/deps/ui-library--bkcNSYz.js.map +1 -0
  158. package/examples/typography-presets/node_modules/.vite/deps/window-jPkpKVcy.js +32 -0
  159. package/examples/typography-presets/node_modules/.vite/deps/window-jPkpKVcy.js.map +1 -0
  160. package/examples/typography-presets/node_modules/.vite/deps/xray-BXCHpEJP.js +111 -0
  161. package/examples/typography-presets/node_modules/.vite/deps/xray-BXCHpEJP.js.map +1 -0
  162. package/examples/typography-presets/site/.norna/generated-images.json +290 -0
  163. package/examples/typography-presets/site/config.mjs +29 -0
  164. package/examples/typography-presets/site/content.md +16 -0
  165. package/examples/typography-presets/site/public/robots.txt +2 -0
  166. package/examples/typography-presets/site/routes/010-quiet-gallery/images/quiet-gallery/puppy-portrait.jpg +0 -0
  167. package/examples/typography-presets/site/routes/010-quiet-gallery/images/quiet-gallery/shepherd-portrait.jpg +0 -0
  168. package/examples/typography-presets/site/routes/010-quiet-gallery/images/quiet-gallery/white-puppy.jpg +0 -0
  169. package/examples/typography-presets/site/routes/010-quiet-gallery/route-content.md +34 -0
  170. package/examples/typography-presets/site/routes/010-quiet-gallery/theme.md +4 -0
  171. package/examples/typography-presets/site/routes/020-compact-gallery/images/compact-gallery/black-schnauzer.jpg +0 -0
  172. package/examples/typography-presets/site/routes/020-compact-gallery/images/compact-gallery/retriever-portrait.jpg +0 -0
  173. package/examples/typography-presets/site/routes/020-compact-gallery/images/compact-gallery/schnauzer-portrait.jpg +0 -0
  174. package/examples/typography-presets/site/routes/020-compact-gallery/route-content.md +33 -0
  175. package/examples/typography-presets/site/routes/020-compact-gallery/theme.md +4 -0
  176. package/examples/typography-presets/site/routes/030-text-forward/images/text-forward/boxer-portrait.jpg +0 -0
  177. package/examples/typography-presets/site/routes/030-text-forward/images/text-forward/grass-puppy.jpg +0 -0
  178. package/examples/typography-presets/site/routes/030-text-forward/images/text-forward/grey-street-dog.jpg +0 -0
  179. package/examples/typography-presets/site/routes/030-text-forward/route-content.md +34 -0
  180. package/examples/typography-presets/site/routes/030-text-forward/theme.md +4 -0
  181. package/examples/typography-presets/site/routes/040-statement/images/statement/black-pedigree.jpg +0 -0
  182. package/examples/typography-presets/site/routes/040-statement/images/statement/sheepdog-grass.jpg +0 -0
  183. package/examples/typography-presets/site/routes/040-statement/images/statement/sheepdog-wide.jpg +0 -0
  184. package/examples/typography-presets/site/routes/040-statement/route-content.md +34 -0
  185. package/examples/typography-presets/site/routes/040-statement/theme.md +4 -0
  186. package/examples/typography-presets/site/sitewide-content.md +14 -0
  187. package/examples/typography-presets/site/theme.md +43 -0
  188. package/fixtures/basic/site/config.mjs +0 -13
  189. package/fixtures/basic/site/sitewide-content.md +12 -0
  190. package/fixtures/basic/site/theme.md +2 -2
  191. package/fixtures/content-model-v2/site/content.md +2 -5
  192. package/fixtures/content-model-v2/site/theme.md +1 -3
  193. package/package.json +3 -2
  194. package/scripts/check-config.mjs +22 -1
  195. package/scripts/lib/frontmatter-yaml.mjs +28 -0
  196. package/scripts/lib/logo-assets.mjs +35 -0
  197. package/scripts/lib/norna-markdown-blocks.mjs +166 -0
  198. package/scripts/lib/presentation.mjs +104 -46
  199. package/scripts/lib/project-config.mjs +67 -105
  200. package/scripts/lib/site-content.mjs +26 -42
  201. package/scripts/lib/site-paths.mjs +2 -0
  202. package/scripts/lib/sitewide-config.mjs +34 -0
  203. package/scripts/lib/theme-config.mjs +72 -6
  204. package/scripts/lib/typography.mjs +20 -26
  205. package/scripts/show-typography.mjs +40 -147
  206. package/scripts/sync-content-sections.mjs +19 -14
  207. package/scripts/test-content-check.mjs +76 -14
  208. package/scripts/test-content-model-v2.mjs +154 -0
  209. package/scripts/test-engine-commands.mjs +3 -1
  210. package/scripts/test-package-check.mjs +78 -19
  211. package/scripts/test-temporary-visibility.mjs +19 -2
  212. package/src/components/CardList.astro +5 -2
  213. package/src/components/GalleryGrid.astro +37 -16
  214. package/src/components/SiteBanners.astro +100 -0
  215. package/src/components/SiteNavigation.astro +48 -31
  216. package/src/components/SitePage.astro +55 -13
  217. package/src/components/SiteSection.astro +194 -36
  218. package/src/content.config.ts +78 -48
  219. package/src/layouts/BaseLayout.astro +64 -14
  220. package/src/lib/routeThemes.ts +61 -0
  221. package/src/lib/sectionContent.ts +236 -44
  222. package/src/lib/sitePublicAssets.ts +1 -0
  223. package/src/styles/global.css +282 -93
  224. package/starters/basic/.github/workflows/deploy.yml +1 -1
  225. package/starters/basic/README.md +7 -6
  226. package/starters/basic/site/config.mjs +0 -13
  227. package/starters/basic/site/content.md +1 -1
  228. package/starters/basic/site/sitewide-content.md +14 -0
  229. package/starters/basic/site/theme.md +13 -24
  230. package/starters/project/.github/workflows/deploy.yml +1 -1
  231. package/starters/project/README.md +10 -8
  232. package/starters/project/site/config.mjs +0 -13
  233. package/starters/project/site/sitewide-content.md +14 -0
  234. 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) ${projectConfig.gallery.maxAvailableWidthPercent.mobile}vw`,
26
- `${projectConfig.gallery.maxAvailableWidthPercent.desktop}vw`,
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
- <figure class="gallery-carousel-slide" data-carousel-slide>
98
- <div class="gallery-carousel-image-frame">
99
- <img
100
- {...getImageAttributes(image.src ?? image.image, galleryImageSizes)}
101
- class:list={['gallery-image', getImageClass(image)]}
102
- alt={image.alt ?? ''}
103
- {...getImageLoadingProps(isPriorityItem && imageIndex === 0)}
104
- />
105
- </div>
106
- </figure>
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">&times;</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 { ResolvedSection } from '../lib/sectionContent';
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
- pages: SitePage[];
16
- sections: ResolvedSection[];
20
+ pageNavigation: Array<{
21
+ page: SitePage;
22
+ sections: SectionNavigation[];
23
+ }>;
17
24
  frameColors: FrameColors;
18
25
  }
19
26
 
20
- const { brandLabel, currentPage, pages, sections, frameColors } = Astro.props;
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 = pages.length > 1;
30
- const showPageNavigation = sections.length > 1;
31
- const homePage = pages.find((page) => page.isHome) ?? currentPage;
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)}>{brandLabel}</a>
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
- {pages.map((page) => (
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
- {pages.map((page) => (
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
- {showPageNavigation && (
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={`#${section.id}`} aria-current={index === 0 ? 'true' : undefined}>
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('.page-nav a, .mobile-page-nav a'));
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 isActive = link.getAttribute('href') === `#${section.id}`;
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 section = getSectionFromHash(link.hash);
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 = `#${section.id}`;
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 [theme] = await getCollection('theme');
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 themeData = theme?.data ?? {};
35
- const brandLabel = themeData.navigation?.brand ?? homePage.title;
36
- const pagePresentation = resolvePagePresentation(themeData, entry.data.presentation);
37
- const frameColors = resolveFrameColors({
38
- theme: themeData,
39
- pagePresentation,
40
- pageFrame: entry.data.frame,
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, pagePresentation.inlineStyles);
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
- pages={navigationPages}
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
  />