@janga/norna 0.7.11 → 0.7.13

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 (239) hide show
  1. package/README.md +120 -108
  2. package/bin/norna-cli.mjs +1 -1
  3. package/docs/README.md +107 -75
  4. package/docs/assets/norna-dog-shelter.jpg +0 -0
  5. package/docs/commands.md +7 -4
  6. package/docs/configuration.md +86 -185
  7. package/docs/design/command-organization.md +1 -1
  8. package/docs/design/norna-diagram-design.md +2 -1
  9. package/docs/design/site-examples-structure.md +4 -1
  10. package/docs/engine-development.md +15 -8
  11. package/docs/getting-started.md +126 -144
  12. package/docs/how-to/embedded-site.md +35 -0
  13. package/docs/publishing.md +47 -46
  14. package/docs/requirements.md +54 -0
  15. package/docs/routes.md +1 -1
  16. package/docs/site-structure.md +4 -4
  17. package/docs/sitewide-content.md +2 -1
  18. package/docs/theme.md +18 -0
  19. package/examples/README.md +26 -24
  20. package/examples/complete-sites/dog-shelter-multi-page/.astro/content-assets.mjs +4 -1
  21. package/examples/complete-sites/dog-shelter-multi-page/.astro/content.d.ts +1 -1
  22. package/examples/complete-sites/dog-shelter-multi-page/.astro/data-store.json +1 -1
  23. package/examples/complete-sites/dog-shelter-multi-page/.astro/dev.json +6 -8
  24. package/examples/complete-sites/dog-shelter-multi-page/.astro/dev.log +26 -53
  25. package/examples/complete-sites/dog-shelter-multi-page/README.md +7 -0
  26. package/examples/complete-sites/dog-shelter-multi-page/node_modules/.vite/deps/_metadata.json +29 -35
  27. package/examples/complete-sites/dog-shelter-multi-page/node_modules/.vite/deps/{astro-D_GIpJIE.js → astro-B3JHJyiv.js} +4 -4
  28. package/examples/complete-sites/dog-shelter-multi-page/node_modules/.vite/deps/astro-B3JHJyiv.js.map +1 -0
  29. package/examples/complete-sites/dog-shelter-multi-page/node_modules/.vite/deps/astro_n_aria-query.js +151 -151
  30. package/examples/complete-sites/dog-shelter-multi-page/node_modules/.vite/deps/astro_n_aria-query.js.map +1 -1
  31. package/examples/complete-sites/dog-shelter-multi-page/node_modules/.vite/deps/astro_n_axobject-query.js +131 -131
  32. package/examples/complete-sites/dog-shelter-multi-page/node_modules/.vite/deps/astro_n_axobject-query.js.map +1 -1
  33. package/examples/complete-sites/dog-shelter-multi-page/node_modules/.vite/deps/astro_n_html-escaper.js +1 -1
  34. package/examples/complete-sites/dog-shelter-multi-page/node_modules/.vite/deps/astro_n_html-escaper.js.map +1 -1
  35. package/examples/complete-sites/dog-shelter-multi-page/node_modules/.vite/deps/astro_runtime_client_dev-toolbar_entrypoint__js.js +10 -10
  36. package/examples/complete-sites/dog-shelter-multi-page/node_modules/.vite/deps/astro_runtime_client_dev-toolbar_entrypoint__js.js.map +1 -1
  37. package/examples/complete-sites/dog-shelter-multi-page/node_modules/.vite/deps/{audit-DM9Hkl5B.js → audit-C3SVr7dT.js} +10 -10
  38. package/examples/complete-sites/dog-shelter-multi-page/node_modules/.vite/deps/audit-C3SVr7dT.js.map +1 -0
  39. package/examples/complete-sites/dog-shelter-multi-page/node_modules/.vite/deps/{highlight-DnZ1ULOL.js → highlight-BLtvqtzO.js} +1 -1
  40. package/examples/complete-sites/dog-shelter-multi-page/node_modules/.vite/deps/highlight-BLtvqtzO.js.map +1 -0
  41. package/examples/complete-sites/dog-shelter-multi-page/node_modules/.vite/deps/{icons-RyebKi45.js → icons-C92i61-2.js} +1 -1
  42. package/examples/complete-sites/dog-shelter-multi-page/node_modules/.vite/deps/icons-C92i61-2.js.map +1 -0
  43. package/examples/complete-sites/dog-shelter-multi-page/node_modules/.vite/deps/{settings-DaU-gNnK.js → settings-v5GgKks_.js} +2 -2
  44. package/examples/complete-sites/dog-shelter-multi-page/node_modules/.vite/deps/settings-v5GgKks_.js.map +1 -0
  45. package/examples/complete-sites/dog-shelter-multi-page/node_modules/.vite/deps/{toolbar-63PA619H.js → toolbar-BFvJVp26.js} +2 -2
  46. package/examples/complete-sites/dog-shelter-multi-page/node_modules/.vite/deps/toolbar-BFvJVp26.js.map +1 -0
  47. package/examples/complete-sites/dog-shelter-multi-page/node_modules/.vite/deps/{ui-library--bkcNSYz.js → ui-library-Cd35oAvo.js} +10 -10
  48. package/examples/complete-sites/dog-shelter-multi-page/node_modules/.vite/deps/ui-library-Cd35oAvo.js.map +1 -0
  49. package/examples/complete-sites/dog-shelter-multi-page/node_modules/.vite/deps/{window-jPkpKVcy.js → window-vo3A6TMn.js} +1 -1
  50. package/examples/complete-sites/dog-shelter-multi-page/node_modules/.vite/deps/window-vo3A6TMn.js.map +1 -0
  51. package/examples/complete-sites/dog-shelter-multi-page/node_modules/.vite/deps/{xray-BXCHpEJP.js → xray-DwhzGckb.js} +3 -3
  52. package/examples/complete-sites/dog-shelter-multi-page/node_modules/.vite/deps/xray-DwhzGckb.js.map +1 -0
  53. package/examples/complete-sites/dog-shelter-multi-page/site/.norna/generated-images.json +92 -12
  54. package/examples/complete-sites/dog-shelter-multi-page/site/config.md +3 -0
  55. package/examples/complete-sites/dog-shelter-multi-page/site/content.md +12 -0
  56. package/examples/complete-sites/dog-shelter-multi-page/site/images/intro/shelter-dog.jpg +0 -0
  57. package/examples/complete-sites/dog-shelter-multi-page/site/public/logo.svg +9 -0
  58. package/examples/complete-sites/dog-shelter-multi-page/site/routes/010-dogs/images/dogs/rex.jpg +0 -0
  59. package/examples/complete-sites/dog-shelter-multi-page/site/routes/010-dogs/images/dogs/rover.jpg +0 -0
  60. package/examples/complete-sites/dog-shelter-multi-page/site/routes/010-dogs/route-content.md +11 -19
  61. package/examples/complete-sites/dog-shelter-multi-page/site/routes/020-adopt/images/adopt/adoption-dog.jpg +0 -0
  62. package/examples/complete-sites/dog-shelter-multi-page/site/routes/020-adopt/route-content.md +11 -0
  63. package/examples/complete-sites/dog-shelter-multi-page/site/sitewide-content.md +12 -2
  64. package/examples/complete-sites/dog-shelter-multi-page/site/theme.md +1 -23
  65. package/examples/complete-sites/dog-shelter-single-page/.astro/collections/site.schema.json +59 -0
  66. package/examples/complete-sites/dog-shelter-single-page/.astro/collections/sitewide.schema.json +138 -0
  67. package/examples/complete-sites/dog-shelter-single-page/.astro/collections/theme.schema.json +755 -0
  68. package/examples/complete-sites/dog-shelter-single-page/.astro/content-assets.mjs +4 -0
  69. package/examples/complete-sites/dog-shelter-single-page/.astro/content-modules.mjs +1 -0
  70. package/examples/complete-sites/dog-shelter-single-page/.astro/content.d.ts +183 -0
  71. package/examples/complete-sites/dog-shelter-single-page/.astro/data-store.json +1 -0
  72. package/examples/complete-sites/dog-shelter-single-page/.astro/dev.json +13 -0
  73. package/examples/complete-sites/dog-shelter-single-page/.astro/dev.log +14 -0
  74. package/examples/complete-sites/dog-shelter-single-page/.astro/settings.json +5 -0
  75. package/examples/complete-sites/dog-shelter-single-page/.astro/types.d.ts +2 -0
  76. package/examples/complete-sites/dog-shelter-single-page/README.md +7 -0
  77. package/examples/complete-sites/dog-shelter-single-page/node_modules/.vite/deps/_metadata.json +74 -0
  78. package/examples/complete-sites/dog-shelter-single-page/node_modules/.vite/deps/astro-B3JHJyiv.js +434 -0
  79. package/examples/complete-sites/dog-shelter-single-page/node_modules/.vite/deps/astro-B3JHJyiv.js.map +1 -0
  80. package/examples/complete-sites/dog-shelter-single-page/node_modules/.vite/deps/astro_n_aria-query.js +6390 -0
  81. package/examples/complete-sites/dog-shelter-single-page/node_modules/.vite/deps/astro_n_aria-query.js.map +1 -0
  82. package/examples/complete-sites/dog-shelter-single-page/node_modules/.vite/deps/astro_n_axobject-query.js +2702 -0
  83. package/examples/complete-sites/dog-shelter-single-page/node_modules/.vite/deps/astro_n_axobject-query.js.map +1 -0
  84. package/examples/complete-sites/dog-shelter-single-page/node_modules/.vite/deps/astro_n_html-escaper.js +65 -0
  85. package/examples/complete-sites/dog-shelter-single-page/node_modules/.vite/deps/astro_n_html-escaper.js.map +1 -0
  86. package/examples/complete-sites/dog-shelter-single-page/node_modules/.vite/deps/astro_runtime_client_dev-toolbar_entrypoint__js.js +455 -0
  87. package/examples/complete-sites/dog-shelter-single-page/node_modules/.vite/deps/astro_runtime_client_dev-toolbar_entrypoint__js.js.map +1 -0
  88. package/examples/complete-sites/dog-shelter-single-page/node_modules/.vite/deps/audit-C3SVr7dT.js +1534 -0
  89. package/examples/complete-sites/dog-shelter-single-page/node_modules/.vite/deps/audit-C3SVr7dT.js.map +1 -0
  90. package/examples/complete-sites/dog-shelter-single-page/node_modules/.vite/deps/highlight-BLtvqtzO.js +48 -0
  91. package/examples/complete-sites/dog-shelter-single-page/node_modules/.vite/deps/highlight-BLtvqtzO.js.map +1 -0
  92. package/examples/complete-sites/dog-shelter-single-page/node_modules/.vite/deps/icons-C92i61-2.js +42 -0
  93. package/examples/complete-sites/dog-shelter-single-page/node_modules/.vite/deps/icons-C92i61-2.js.map +1 -0
  94. package/examples/complete-sites/dog-shelter-single-page/node_modules/.vite/deps/package.json +3 -0
  95. package/examples/complete-sites/dog-shelter-single-page/node_modules/.vite/deps/rolldown-runtime-BvCyGRYZ.js +4 -0
  96. package/examples/complete-sites/dog-shelter-single-page/node_modules/.vite/deps/settings-v5GgKks_.js +190 -0
  97. package/examples/complete-sites/dog-shelter-single-page/node_modules/.vite/deps/settings-v5GgKks_.js.map +1 -0
  98. package/examples/complete-sites/dog-shelter-single-page/node_modules/.vite/deps/toolbar-BFvJVp26.js +458 -0
  99. package/examples/complete-sites/dog-shelter-single-page/node_modules/.vite/deps/toolbar-BFvJVp26.js.map +1 -0
  100. package/examples/complete-sites/dog-shelter-single-page/node_modules/.vite/deps/ui-library-Cd35oAvo.js +1016 -0
  101. package/examples/complete-sites/dog-shelter-single-page/node_modules/.vite/deps/ui-library-Cd35oAvo.js.map +1 -0
  102. package/examples/complete-sites/dog-shelter-single-page/node_modules/.vite/deps/window-vo3A6TMn.js +32 -0
  103. package/examples/complete-sites/dog-shelter-single-page/node_modules/.vite/deps/window-vo3A6TMn.js.map +1 -0
  104. package/examples/complete-sites/dog-shelter-single-page/node_modules/.vite/deps/xray-DwhzGckb.js +111 -0
  105. package/examples/complete-sites/dog-shelter-single-page/node_modules/.vite/deps/xray-DwhzGckb.js.map +1 -0
  106. package/examples/complete-sites/dog-shelter-single-page/site/.norna/generated-images.json +44 -12
  107. package/examples/complete-sites/dog-shelter-single-page/site/config.md +3 -0
  108. package/examples/complete-sites/dog-shelter-single-page/site/content.md +20 -24
  109. package/examples/complete-sites/dog-shelter-single-page/site/images/what-we-do/rescue-dog.jpg +0 -0
  110. package/examples/complete-sites/dog-shelter-single-page/site/images/you-can-help/foster-dog.jpg +0 -0
  111. package/examples/complete-sites/dog-shelter-single-page/site/public/logo.svg +9 -0
  112. package/examples/complete-sites/dog-shelter-single-page/site/sitewide-content.md +12 -2
  113. package/examples/complete-sites/dog-shelter-single-page/site/theme.md +1 -23
  114. package/examples/feature-demos/media-and-surfaces/.astro/data-store.json +1 -1
  115. package/examples/feature-demos/media-and-surfaces/.astro/dev.log +15 -2
  116. package/examples/feature-demos/media-and-surfaces/README.md +7 -1
  117. package/examples/feature-demos/media-and-surfaces/node_modules/.vite/deps/_metadata.json +7 -1
  118. package/examples/{complete-sites/dog-shelter-multi-page → feature-demos/media-and-surfaces}/node_modules/.vite/deps/embla-carousel.js +1 -1
  119. package/examples/feature-demos/media-and-surfaces/node_modules/.vite/deps/embla-carousel.js.map +1 -0
  120. package/examples/feature-demos/media-and-surfaces/site/config.md +3 -0
  121. package/examples/feature-demos/media-and-surfaces/site/content.md +5 -51
  122. package/examples/feature-demos/media-and-surfaces/site/routes/010-media/route-content.md +10 -25
  123. package/examples/feature-demos/media-and-surfaces/site/routes/020-surfaces/route-content.md +1 -1
  124. package/examples/feature-demos/sitewide-content/.astro/collections/site.schema.json +59 -0
  125. package/examples/feature-demos/sitewide-content/.astro/collections/sitewide.schema.json +138 -0
  126. package/examples/feature-demos/sitewide-content/.astro/collections/theme.schema.json +755 -0
  127. package/examples/feature-demos/sitewide-content/.astro/content-assets.mjs +1 -0
  128. package/examples/feature-demos/sitewide-content/.astro/content-modules.mjs +1 -0
  129. package/examples/feature-demos/sitewide-content/.astro/content.d.ts +183 -0
  130. package/examples/feature-demos/sitewide-content/.astro/data-store.json +1 -0
  131. package/examples/feature-demos/sitewide-content/.astro/dev.json +13 -0
  132. package/examples/feature-demos/sitewide-content/.astro/dev.log +18 -0
  133. package/examples/feature-demos/sitewide-content/.astro/settings.json +5 -0
  134. package/examples/feature-demos/sitewide-content/.astro/types.d.ts +2 -0
  135. package/examples/feature-demos/sitewide-content/README.md +18 -0
  136. package/examples/feature-demos/sitewide-content/node_modules/.vite/deps/_metadata.json +74 -0
  137. package/examples/feature-demos/sitewide-content/node_modules/.vite/deps/astro-B3JHJyiv.js +434 -0
  138. package/examples/feature-demos/sitewide-content/node_modules/.vite/deps/astro-B3JHJyiv.js.map +1 -0
  139. package/examples/feature-demos/sitewide-content/node_modules/.vite/deps/astro_n_aria-query.js +6390 -0
  140. package/examples/feature-demos/sitewide-content/node_modules/.vite/deps/astro_n_aria-query.js.map +1 -0
  141. package/examples/feature-demos/sitewide-content/node_modules/.vite/deps/astro_n_axobject-query.js +2702 -0
  142. package/examples/feature-demos/sitewide-content/node_modules/.vite/deps/astro_n_axobject-query.js.map +1 -0
  143. package/examples/feature-demos/sitewide-content/node_modules/.vite/deps/astro_n_html-escaper.js +65 -0
  144. package/examples/feature-demos/sitewide-content/node_modules/.vite/deps/astro_n_html-escaper.js.map +1 -0
  145. package/examples/feature-demos/sitewide-content/node_modules/.vite/deps/astro_runtime_client_dev-toolbar_entrypoint__js.js +455 -0
  146. package/examples/feature-demos/sitewide-content/node_modules/.vite/deps/astro_runtime_client_dev-toolbar_entrypoint__js.js.map +1 -0
  147. package/examples/feature-demos/sitewide-content/node_modules/.vite/deps/audit-C3SVr7dT.js +1534 -0
  148. package/examples/feature-demos/sitewide-content/node_modules/.vite/deps/audit-C3SVr7dT.js.map +1 -0
  149. package/examples/feature-demos/sitewide-content/node_modules/.vite/deps/highlight-BLtvqtzO.js +48 -0
  150. package/examples/feature-demos/sitewide-content/node_modules/.vite/deps/highlight-BLtvqtzO.js.map +1 -0
  151. package/examples/feature-demos/sitewide-content/node_modules/.vite/deps/icons-C92i61-2.js +42 -0
  152. package/examples/feature-demos/sitewide-content/node_modules/.vite/deps/icons-C92i61-2.js.map +1 -0
  153. package/examples/feature-demos/sitewide-content/node_modules/.vite/deps/package.json +3 -0
  154. package/examples/feature-demos/sitewide-content/node_modules/.vite/deps/rolldown-runtime-BvCyGRYZ.js +4 -0
  155. package/examples/feature-demos/sitewide-content/node_modules/.vite/deps/settings-v5GgKks_.js +190 -0
  156. package/examples/feature-demos/sitewide-content/node_modules/.vite/deps/settings-v5GgKks_.js.map +1 -0
  157. package/examples/feature-demos/sitewide-content/node_modules/.vite/deps/toolbar-BFvJVp26.js +458 -0
  158. package/examples/feature-demos/sitewide-content/node_modules/.vite/deps/toolbar-BFvJVp26.js.map +1 -0
  159. package/examples/feature-demos/sitewide-content/node_modules/.vite/deps/ui-library-Cd35oAvo.js +1016 -0
  160. package/examples/feature-demos/sitewide-content/node_modules/.vite/deps/ui-library-Cd35oAvo.js.map +1 -0
  161. package/examples/feature-demos/sitewide-content/node_modules/.vite/deps/window-vo3A6TMn.js +32 -0
  162. package/examples/feature-demos/sitewide-content/node_modules/.vite/deps/window-vo3A6TMn.js.map +1 -0
  163. package/examples/feature-demos/sitewide-content/node_modules/.vite/deps/xray-DwhzGckb.js +111 -0
  164. package/examples/feature-demos/sitewide-content/node_modules/.vite/deps/xray-DwhzGckb.js.map +1 -0
  165. package/examples/feature-demos/sitewide-content/site/.norna/generated-images.json +1 -0
  166. package/examples/feature-demos/sitewide-content/site/config.md +3 -0
  167. package/examples/feature-demos/sitewide-content/site/content.md +16 -0
  168. package/examples/feature-demos/sitewide-content/site/public/logo.svg +6 -0
  169. package/examples/feature-demos/sitewide-content/site/public/robots.txt +2 -0
  170. package/examples/feature-demos/sitewide-content/site/routes/010-identity/route-content.md +20 -0
  171. package/examples/feature-demos/sitewide-content/site/routes/020-notices/route-content.md +22 -0
  172. package/examples/feature-demos/sitewide-content/site/routes/030-footer/route-content.md +17 -0
  173. package/examples/feature-demos/sitewide-content/site/sitewide-content.md +33 -0
  174. package/examples/feature-demos/sitewide-content/site/theme.md +3 -0
  175. package/examples/feature-demos/theme-presets/.astro/dev.log +37 -0
  176. package/examples/feature-demos/theme-presets/README.md +5 -1
  177. package/examples/feature-demos/theme-presets/site/config.md +3 -0
  178. package/fixtures/basic/site/config.md +3 -0
  179. package/fixtures/content-model-v2/site/config.md +3 -0
  180. package/package.json +12 -7
  181. package/scripts/build-pages.mjs +4 -6
  182. package/scripts/check-config.mjs +1 -10
  183. package/scripts/deploy-site.mjs +12 -8
  184. package/scripts/lib/example-sites.mjs +3 -0
  185. package/scripts/lib/github-repository.mjs +44 -0
  186. package/scripts/lib/norna-markdown-blocks.mjs +97 -10
  187. package/scripts/lib/project-config.mjs +112 -123
  188. package/scripts/lib/site-content.mjs +7 -0
  189. package/scripts/lib/site-paths.mjs +6 -2
  190. package/scripts/test-client-javascript.mjs +221 -0
  191. package/scripts/test-content-check.mjs +103 -0
  192. package/scripts/test-content-model-v2.mjs +9 -13
  193. package/scripts/test-documentation.mjs +116 -0
  194. package/scripts/test-engine-commands.mjs +12 -3
  195. package/scripts/test-examples.mjs +9 -0
  196. package/scripts/test-package-check.mjs +17 -9
  197. package/scripts/test-project-config.mjs +125 -0
  198. package/scripts/test-temporary-visibility.mjs +3 -10
  199. package/scripts/test-theme-presets.mjs +3 -8
  200. package/scripts/watch-pages-deploy.mjs +15 -11
  201. package/src/components/GalleryGrid.astro +30 -230
  202. package/src/components/ImageCarousel.astro +184 -0
  203. package/src/components/MobileSectionNavigationScript.astro +14 -0
  204. package/src/components/SiteNavigation.astro +54 -341
  205. package/src/components/SiteNotesScript.astro +43 -0
  206. package/src/components/SitePage.astro +4 -1
  207. package/src/components/SiteSection.astro +4 -47
  208. package/src/layouts/BaseLayout.astro +1 -1
  209. package/src/lib/galleryImages.ts +53 -0
  210. package/src/styles/global.css +81 -1
  211. package/starters/basic/.github/workflows/deploy.yml +1 -2
  212. package/starters/basic/README.md +10 -8
  213. package/starters/basic/site/config.md +3 -0
  214. package/starters/project/.github/workflows/deploy.yml +1 -2
  215. package/starters/project/README.md +3 -4
  216. package/starters/project/site/config.md +3 -0
  217. package/examples/complete-sites/dog-shelter-multi-page/node_modules/.vite/deps/astro-D_GIpJIE.js.map +0 -1
  218. package/examples/complete-sites/dog-shelter-multi-page/node_modules/.vite/deps/audit-DM9Hkl5B.js.map +0 -1
  219. package/examples/complete-sites/dog-shelter-multi-page/node_modules/.vite/deps/embla-carousel.js.map +0 -1
  220. package/examples/complete-sites/dog-shelter-multi-page/node_modules/.vite/deps/highlight-DnZ1ULOL.js.map +0 -1
  221. package/examples/complete-sites/dog-shelter-multi-page/node_modules/.vite/deps/icons-RyebKi45.js.map +0 -1
  222. package/examples/complete-sites/dog-shelter-multi-page/node_modules/.vite/deps/settings-DaU-gNnK.js.map +0 -1
  223. package/examples/complete-sites/dog-shelter-multi-page/node_modules/.vite/deps/toolbar-63PA619H.js.map +0 -1
  224. package/examples/complete-sites/dog-shelter-multi-page/node_modules/.vite/deps/ui-library--bkcNSYz.js.map +0 -1
  225. package/examples/complete-sites/dog-shelter-multi-page/node_modules/.vite/deps/window-jPkpKVcy.js.map +0 -1
  226. package/examples/complete-sites/dog-shelter-multi-page/node_modules/.vite/deps/xray-BXCHpEJP.js.map +0 -1
  227. package/examples/complete-sites/dog-shelter-multi-page/site/config.mjs +0 -30
  228. package/examples/complete-sites/dog-shelter-multi-page/site/routes/010-dogs/images/dogs/rex.svg +0 -19
  229. package/examples/complete-sites/dog-shelter-multi-page/site/routes/010-dogs/images/dogs/rover.svg +0 -19
  230. package/examples/complete-sites/dog-shelter-single-page/site/config.mjs +0 -30
  231. package/examples/complete-sites/dog-shelter-single-page/site/images/what-we-do/dog-house.svg +0 -25
  232. package/examples/complete-sites/dog-shelter-single-page/site/images/you-can-help/heart.svg +0 -13
  233. package/examples/feature-demos/media-and-surfaces/site/config.mjs +0 -30
  234. package/examples/feature-demos/media-and-surfaces/site/routes/010-media/theme.md +0 -3
  235. package/examples/feature-demos/theme-presets/site/config.mjs +0 -30
  236. package/fixtures/basic/site/config.mjs +0 -27
  237. package/fixtures/content-model-v2/site/config.mjs +0 -11
  238. package/starters/basic/site/config.mjs +0 -27
  239. package/starters/project/site/config.mjs +0 -30
@@ -0,0 +1,125 @@
1
+ import assert from 'node:assert/strict';
2
+ import { spawnSync } from 'node:child_process';
3
+ import { mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises';
4
+ import { tmpdir } from 'node:os';
5
+ import path from 'node:path';
6
+ import { pathToFileURL } from 'node:url';
7
+
8
+ const repoRoot = path.resolve(import.meta.dirname, '..');
9
+ const projectConfigUrl = pathToFileURL(path.join(repoRoot, 'scripts', 'lib', 'project-config.mjs')).href;
10
+ const tempRoot = await mkdtemp(path.join(tmpdir(), 'norna-project-config-'));
11
+ const importScript = `
12
+ const { projectConfig } = await import(${JSON.stringify(projectConfigUrl)});
13
+ console.log(JSON.stringify({
14
+ basePath: projectConfig.site.basePath,
15
+ language: projectConfig.locale.lang,
16
+ labels: projectConfig.locale.labels,
17
+ scrollBehavior: projectConfig.navigation.scrollBehavior,
18
+ url: projectConfig.site.url,
19
+ }));
20
+ `;
21
+
22
+ const createSite = async (name, config, { legacy = false } = {}) => {
23
+ const projectRoot = path.join(tempRoot, name);
24
+ const siteDir = path.join(projectRoot, 'site');
25
+ await mkdir(siteDir, { recursive: true });
26
+ await writeFile(path.join(siteDir, legacy ? 'config.mjs' : 'config.md'), config);
27
+ await writeFile(path.join(siteDir, 'content.md'), '---\ntitle: Config test\ndescription: Config test.\n---\n\n## Intro {#intro}\n\nText.\n');
28
+ await writeFile(path.join(siteDir, 'theme.md'), '---\npreset: documentation\n---\n');
29
+ return { projectRoot, siteDir };
30
+ };
31
+
32
+ const loadConfig = ({ projectRoot, siteDir }, env = {}) => spawnSync(
33
+ process.execPath,
34
+ ['--input-type=module', '--eval', importScript],
35
+ {
36
+ cwd: projectRoot,
37
+ encoding: 'utf8',
38
+ env: {
39
+ ...process.env,
40
+ NORNA_INVOCATION_ROOT: projectRoot,
41
+ NORNA_SITE_DIR: siteDir,
42
+ ...env,
43
+ },
44
+ },
45
+ );
46
+
47
+ const assertFailure = (result, expectedPattern) => {
48
+ assert.notEqual(result.status, 0, result.stdout);
49
+ assert.match(`${result.stdout}\n${result.stderr}`, expectedPattern);
50
+ };
51
+
52
+ try {
53
+ const minimalSite = await createSite('minimal', '---\nurl: https://example.com/docs\n---\n');
54
+ const minimalResult = loadConfig(minimalSite);
55
+ assert.equal(minimalResult.status, 0, minimalResult.stderr);
56
+ assert.deepEqual(JSON.parse(minimalResult.stdout), {
57
+ basePath: '/docs/',
58
+ language: 'en',
59
+ labels: {
60
+ dismissBanner: 'Dismiss notice',
61
+ gallery: 'Images',
62
+ note: 'Note',
63
+ pageNavigation: 'On this page',
64
+ siteBanners: 'Site notices',
65
+ siteNavigation: 'Pages',
66
+ skipToContent: 'Skip to content',
67
+ },
68
+ scrollBehavior: 'instant',
69
+ url: 'https://example.com/docs/',
70
+ });
71
+
72
+ const localizedSite = await createSite('localized', '---\nurl: https://example.com/\nlanguage: sv-SE\nscrollBehavior: browser-smooth\n---\n');
73
+ const localizedResult = loadConfig(localizedSite);
74
+ assert.equal(localizedResult.status, 0, localizedResult.stderr);
75
+ const localizedConfig = JSON.parse(localizedResult.stdout);
76
+ assert.equal(localizedConfig.language, 'sv-SE');
77
+ assert.equal(localizedConfig.labels.skipToContent, 'Hoppa till innehållet');
78
+ assert.equal(localizedConfig.scrollBehavior, 'browser-smooth');
79
+
80
+ const overrideResult = loadConfig(minimalSite, {
81
+ NORNA_SITE_URL: 'http://127.0.0.1:4567/preview',
82
+ });
83
+ assert.equal(overrideResult.status, 0, overrideResult.stderr);
84
+ assert.deepEqual(
85
+ JSON.parse(overrideResult.stdout),
86
+ {
87
+ ...JSON.parse(minimalResult.stdout),
88
+ basePath: '/preview/',
89
+ url: 'http://127.0.0.1:4567/preview/',
90
+ },
91
+ );
92
+
93
+ assertFailure(
94
+ loadConfig(await createSite('unknown-field', '---\nurl: https://example.com/\nbasePath: /docs/\n---\n')),
95
+ /not a valid top-level config field/,
96
+ );
97
+ assertFailure(
98
+ loadConfig(await createSite('markdown-body', '---\nurl: https://example.com/\n---\n\nConfiguration prose.\n')),
99
+ /may contain YAML frontmatter only/,
100
+ );
101
+ assertFailure(
102
+ loadConfig(await createSite('unsupported-language', '---\nurl: https://example.com/\nlanguage: de\n---\n')),
103
+ /has no built-in Norna UI text/,
104
+ );
105
+ assertFailure(
106
+ loadConfig(await createSite('invalid-url', '---\nurl: example.com\n---\n')),
107
+ /url must be an absolute URL/,
108
+ );
109
+ assertFailure(
110
+ loadConfig(await createSite('invalid-scroll-behavior', '---\nurl: https://example.com/\nscrollBehavior: slow\n---\n')),
111
+ /scrollBehavior must be one of instant, browser-smooth/,
112
+ );
113
+ assertFailure(
114
+ loadConfig(await createSite('obsolete-smooth-scroll', '---\nurl: https://example.com/\nsmoothScroll: true\n---\n')),
115
+ /not a valid top-level config field/,
116
+ );
117
+ assertFailure(
118
+ loadConfig(await createSite('legacy', 'export default {};\n', { legacy: true })),
119
+ /Replace the obsolete config\.mjs with a frontmatter-only config\.md file/,
120
+ );
121
+
122
+ console.log('Project config test passed.');
123
+ } finally {
124
+ await rm(tempRoot, { force: true, recursive: true });
125
+ }
@@ -37,16 +37,9 @@ try {
37
37
  await mkdir(path.join(siteDir, 'images', 'work'), { recursive: true });
38
38
  await mkdir(path.join(siteDir, 'public'), { recursive: true });
39
39
  await writeFile(path.join(siteDir, 'public', 'robots.txt'), 'User-agent: *\nAllow: /\n');
40
- await writeFile(path.join(siteDir, 'config.mjs'), `export default {
41
- site: {
42
- url: 'https://example.com/',
43
- },
44
- github: {
45
- repo: 'owner/example',
46
- branch: 'main',
47
- pagesWorkflow: 'Deploy to GitHub Pages',
48
- },
49
- };
40
+ await writeFile(path.join(siteDir, 'config.md'), `---
41
+ url: https://example.com/
42
+ ---
50
43
  `);
51
44
  await writeFile(path.join(siteDir, 'theme.md'), `---
52
45
  typography:
@@ -51,14 +51,9 @@ try {
51
51
  );
52
52
 
53
53
  await mkdir(path.join(siteDir, 'routes', '010-guide'), { recursive: true });
54
- await writeFile(path.join(siteDir, 'config.mjs'), `export default {
55
- site: { url: 'https://example.com/', basePath: '/' },
56
- github: {
57
- repo: 'owner/repo',
58
- branch: 'main',
59
- pagesWorkflow: 'Deploy to GitHub Pages',
60
- },
61
- };
54
+ await writeFile(path.join(siteDir, 'config.md'), `---
55
+ url: https://example.com/
56
+ ---
62
57
  `);
63
58
  await writeFile(path.join(siteDir, 'content.md'), `---
64
59
  title: Theme preset test
@@ -1,18 +1,17 @@
1
1
  import { execFile } from 'node:child_process';
2
2
  import { promisify } from 'node:util';
3
+ import { getGitHubRepositoryContext, githubPagesWorkflow } from './lib/github-repository.mjs';
3
4
  import { projectConfig } from './lib/project-config.mjs';
4
5
  import { siteProjectRoot } from './lib/site-paths.mjs';
5
6
 
6
7
  const execFileAsync = promisify(execFile);
7
8
 
8
9
  const root = siteProjectRoot;
9
- const defaultRepo = projectConfig.github.repo;
10
- const defaultWorkflow = projectConfig.github.pagesWorkflow;
11
- const defaultBranch = projectConfig.github.branch;
10
+ const defaultWorkflow = githubPagesWorkflow;
12
11
  const defaultSiteUrl = projectConfig.site.url;
13
- const defaultPollIntervalMs = projectConfig.deploy.watch.intervalMs;
14
- const defaultTimeoutMs = projectConfig.deploy.watch.timeoutMs;
15
- const defaultRunLimit = projectConfig.deploy.watch.runLimit;
12
+ const defaultPollIntervalMs = 10_000;
13
+ const defaultTimeoutMs = 15 * 60_000;
14
+ const defaultRunLimit = 10;
16
15
  const runListFields = [
17
16
  'conclusion',
18
17
  'createdAt',
@@ -49,9 +48,9 @@ const usage = `
49
48
  Usage: norna deploy:watch [options]
50
49
 
51
50
  Options:
52
- --repo <owner/name> GitHub repository. Default: ${defaultRepo}
53
- --workflow <name> Workflow name. Default: ${defaultWorkflow}
54
- --branch <name> Branch to monitor. Default: ${defaultBranch}
51
+ --repo <owner/name> GitHub repository. Default: current repository
52
+ --workflow <name> Workflow name or file. Default: ${defaultWorkflow}
53
+ --branch <name> Branch to monitor. Default: repository default branch
55
54
  --sha <sha> Commit SHA to monitor. Default: current HEAD
56
55
  --site-url <url> Public site URL to print. Default: ${defaultSiteUrl}
57
56
  --interval <duration> Poll interval, for example 5s or 0.5m. Default: 10s
@@ -99,10 +98,10 @@ const readOptionValue = (args, index, optionName) => {
99
98
 
100
99
  const parseArgs = (args) => {
101
100
  const options = {
102
- branch: defaultBranch,
101
+ branch: null,
103
102
  intervalMs: defaultPollIntervalMs,
104
103
  limit: defaultRunLimit,
105
- repo: defaultRepo,
104
+ repo: null,
106
105
  sha: null,
107
106
  siteUrl: defaultSiteUrl,
108
107
  timeoutMs: defaultTimeoutMs,
@@ -367,6 +366,11 @@ const printFailureDetails = async (run, options, elapsedMs) => {
367
366
 
368
367
  const monitor = async () => {
369
368
  const options = parseArgs(process.argv.slice(2));
369
+ if (!options.repo || !options.branch) {
370
+ const repository = await getGitHubRepositoryContext({ cwd: root, repo: options.repo });
371
+ options.repo ??= repository.repo;
372
+ options.branch ??= repository.branch;
373
+ }
370
374
  options.sha ??= await getCurrentSha();
371
375
 
372
376
  const startedAt = Date.now();
@@ -1,22 +1,17 @@
1
1
  ---
2
2
  import projectConfig from '../../scripts/lib/project-config.mjs';
3
- import { getGeneratedImage, getImageAttributes } from '../lib/generatedImages';
4
-
5
- type GalleryImage = {
6
- image: string;
7
- src?: string;
8
- alt?: string;
9
- caption?: string;
10
- };
11
- type GalleryItem = GalleryImage | { carousel: GalleryImage[] };
12
- type GeneratedImageWithDimensions = NonNullable<ReturnType<typeof getGeneratedImage>> & {
13
- width: number;
14
- height: number;
15
- };
3
+ import { getImageAttributes } from '../lib/generatedImages';
4
+ import {
5
+ getCaptionId,
6
+ getGalleryImageSizes,
7
+ getImageClass,
8
+ getImageLoadingProps,
9
+ type GalleryImage,
10
+ } from '../lib/galleryImages';
16
11
 
17
12
  interface Props {
18
13
  title: string;
19
- items: GalleryItem[];
14
+ images: GalleryImage[];
20
15
  maxAvailableWidthPercent: {
21
16
  desktop: number;
22
17
  mobile: number;
@@ -24,229 +19,34 @@ interface Props {
24
19
  priorityFirstImage?: boolean;
25
20
  }
26
21
 
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
- };
36
- const galleryImageSizes = [
37
- `(max-width: 700px) ${maxAvailableWidthPercent.mobile}vw`,
38
- `${maxAvailableWidthPercent.desktop}vw`,
39
- ].join(', ');
40
- const hasDimensions = (image: ReturnType<typeof getGeneratedImage>): image is GeneratedImageWithDimensions => (
41
- Boolean(image) && Number.isFinite(image.width) && Number.isFinite(image.height)
42
- );
43
- const getImageClass = (image: GalleryImage) => {
44
- const generatedImage = getGeneratedImage(image.src ?? image.image);
45
- return hasDimensions(generatedImage) && generatedImage.width > generatedImage.height
46
- ? 'gallery-image-landscape'
47
- : undefined;
48
- };
49
- const getImageLoadingProps = (isPriority: boolean) => ({
50
- decoding: 'async' as const,
51
- fetchpriority: isPriority ? 'high' as const : 'low' as const,
52
- loading: isPriority ? 'eager' as const : 'lazy' as const,
53
- });
54
- const getCarouselAspectRatio = (images: GalleryImage[]) => {
55
- const ratios = images
56
- .map((image) => getGeneratedImage(image.src ?? image.image))
57
- .filter(hasDimensions)
58
- .map((image) => image.width / image.height);
59
-
60
- return ratios.length > 0 ? Math.min(...ratios) : null;
61
- };
22
+ const { title, images, maxAvailableWidthPercent, priorityFirstImage = false } = Astro.props;
23
+ const galleryImageSizes = getGalleryImageSizes(maxAvailableWidthPercent);
62
24
  ---
63
25
 
64
26
  <div class="gallery" aria-label={`${projectConfig.locale.labels.gallery}: ${title}`}>
65
- {items.map((item, itemIndex) => {
27
+ {images.map((image, itemIndex) => {
66
28
  const isPriorityItem = priorityFirstImage && itemIndex === 0;
67
-
68
- if ('image' in item) {
69
- const captionId = item.caption ? getCaptionId(item, itemIndex) : undefined;
70
-
71
- return (
72
- <figure class="gallery-item">
73
- <div class="gallery-image-frame">
74
- <img
75
- {...getImageAttributes(item.src ?? item.image, galleryImageSizes)}
76
- class:list={['gallery-image', getImageClass(item)]}
77
- alt={item.alt ?? ''}
78
- aria-describedby={captionId}
79
- {...getImageLoadingProps(isPriorityItem)}
80
- />
81
- </div>
82
- {item.caption && (
83
- <figcaption class="gallery-meta" id={captionId}>
84
- <div class="gallery-details">
85
- <span>{item.caption}</span>
86
- </div>
87
- </figcaption>
88
- )}
89
- </figure>
90
- );
91
- }
92
-
93
- const carouselAspectRatio = getCarouselAspectRatio(item.carousel);
94
- const carouselStyle = carouselAspectRatio
95
- ? `--gallery-carousel-aspect-ratio: ${carouselAspectRatio}`
96
- : undefined;
29
+ const captionId = image.caption ? getCaptionId(image, itemIndex) : undefined;
97
30
 
98
31
  return (
99
- <section
100
- class="gallery-item gallery-carousel"
101
- data-carousel
102
- tabindex="0"
103
- role="group"
104
- aria-roledescription="carousel"
105
- aria-label={`${title}, bildkarusell`}
106
- style={carouselStyle}
107
- >
108
- <div class="gallery-carousel-stage">
109
- <div class="gallery-carousel-viewport" data-carousel-viewport>
110
- <div class="gallery-carousel-slides">
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
- })}
128
- </div>
129
- </div>
130
- <button
131
- class="gallery-carousel-button gallery-carousel-button-previous"
132
- type="button"
133
- data-carousel-previous
134
- aria-label="Föregående bild"
135
- title="Föregående bild"
136
- >
137
- <span aria-hidden="true">‹</span>
138
- </button>
139
- <button
140
- class="gallery-carousel-button gallery-carousel-button-next"
141
- type="button"
142
- data-carousel-next
143
- aria-label="Nästa bild"
144
- title="Nästa bild"
145
- >
146
- <span aria-hidden="true">›</span>
147
- </button>
148
- <output class="gallery-carousel-position" data-carousel-position aria-live="polite">
149
- 1 / {item.carousel.length}
150
- </output>
32
+ <figure class="gallery-item">
33
+ <div class="gallery-image-frame">
34
+ <img
35
+ {...getImageAttributes(image.src ?? image.image, galleryImageSizes)}
36
+ class:list={['gallery-image', getImageClass(image)]}
37
+ alt={image.alt ?? ''}
38
+ aria-describedby={captionId}
39
+ {...getImageLoadingProps(isPriorityItem)}
40
+ />
151
41
  </div>
152
- <div class="gallery-carousel-captions" data-carousel-captions>
153
- {item.carousel.map((image, imageIndex) => (
154
- image.caption && (
155
- <p
156
- class="gallery-meta gallery-carousel-caption"
157
- id={getCaptionId(image, itemIndex, imageIndex)}
158
- data-carousel-caption
159
- data-carousel-caption-index={imageIndex}
160
- >
161
- <span class="gallery-details">
162
- <span>{image.caption}</span>
163
- </span>
164
- </p>
165
- )
166
- ))}
167
- </div>
168
- </section>
42
+ {image.caption && (
43
+ <figcaption class="gallery-meta" id={captionId}>
44
+ <div class="gallery-details">
45
+ <span>{image.caption}</span>
46
+ </div>
47
+ </figcaption>
48
+ )}
49
+ </figure>
169
50
  );
170
51
  })}
171
52
  </div>
172
-
173
- <script>
174
- import EmblaCarousel from 'embla-carousel';
175
-
176
- const carouselSelector = '[data-carousel]';
177
-
178
- const initializeCarousel = (carousel: HTMLElement) => {
179
- const viewport = carousel.querySelector<HTMLElement>('[data-carousel-viewport]');
180
- const previous = carousel.querySelector<HTMLButtonElement>('[data-carousel-previous]');
181
- const next = carousel.querySelector<HTMLButtonElement>('[data-carousel-next]');
182
- const position = carousel.querySelector<HTMLOutputElement>('[data-carousel-position]');
183
- const captions = Array.from(carousel.querySelectorAll<HTMLElement>('[data-carousel-caption]'));
184
- const slides = Array.from(carousel.querySelectorAll<HTMLElement>('[data-carousel-slide]'));
185
- if (!viewport || !previous || !next || !position || slides.length < 2) return;
186
-
187
- let controlsTimeout: number | undefined;
188
- const showControls = () => {
189
- carousel.classList.add('is-interacting');
190
- window.clearTimeout(controlsTimeout);
191
- controlsTimeout = window.setTimeout(() => {
192
- carousel.classList.remove('is-interacting');
193
- }, 1600);
194
- };
195
-
196
- carousel.dataset.carouselReady = 'true';
197
-
198
- const embla = EmblaCarousel(viewport, {
199
- align: 'start',
200
- containScroll: false,
201
- loop: true,
202
- });
203
-
204
- const update = () => {
205
- const activeIndex = embla.selectedScrollSnap();
206
- slides.forEach((slide, index) => {
207
- const isSelected = index === activeIndex;
208
- slide.classList.toggle('is-active', isSelected);
209
- slide.setAttribute('aria-hidden', String(!isSelected));
210
- });
211
- captions.forEach((caption) => {
212
- const isSelected = Number(caption.dataset.carouselCaptionIndex) === activeIndex;
213
- caption.classList.toggle('is-active', isSelected);
214
- caption.setAttribute('aria-hidden', String(!isSelected));
215
- });
216
- previous.disabled = false;
217
- next.disabled = false;
218
- position.textContent = `${activeIndex + 1} / ${slides.length}`;
219
- };
220
-
221
- previous.addEventListener('click', () => {
222
- showControls();
223
- embla.scrollPrev();
224
- });
225
- next.addEventListener('click', () => {
226
- showControls();
227
- embla.scrollNext();
228
- });
229
- carousel.addEventListener('focusin', showControls);
230
- carousel.addEventListener('pointerdown', showControls);
231
- carousel.addEventListener('keydown', (event) => {
232
- if (event.key === 'ArrowLeft') {
233
- event.preventDefault();
234
- showControls();
235
- embla.scrollPrev();
236
- }
237
- if (event.key === 'ArrowRight') {
238
- event.preventDefault();
239
- showControls();
240
- embla.scrollNext();
241
- }
242
- });
243
- embla.on('select', () => {
244
- showControls();
245
- update();
246
- });
247
- embla.on('reInit', update);
248
- update();
249
- };
250
-
251
- document.querySelectorAll<HTMLElement>(carouselSelector).forEach(initializeCarousel);
252
- </script>
@@ -0,0 +1,184 @@
1
+ ---
2
+ import projectConfig from '../../scripts/lib/project-config.mjs';
3
+ import { getImageAttributes } from '../lib/generatedImages';
4
+ import {
5
+ getCaptionId,
6
+ getCarouselAspectRatio,
7
+ getGalleryImageSizes,
8
+ getImageClass,
9
+ getImageLoadingProps,
10
+ type GalleryImage,
11
+ } from '../lib/galleryImages';
12
+
13
+ interface Props {
14
+ title: string;
15
+ images: GalleryImage[];
16
+ maxAvailableWidthPercent: {
17
+ desktop: number;
18
+ mobile: number;
19
+ };
20
+ priorityFirstImage?: boolean;
21
+ }
22
+
23
+ const { title, images, maxAvailableWidthPercent, priorityFirstImage = false } = Astro.props;
24
+ const galleryImageSizes = getGalleryImageSizes(maxAvailableWidthPercent);
25
+ const carouselAspectRatio = getCarouselAspectRatio(images);
26
+ const carouselStyle = carouselAspectRatio
27
+ ? `--gallery-carousel-aspect-ratio: ${carouselAspectRatio}`
28
+ : undefined;
29
+ ---
30
+
31
+ <div class="gallery" aria-label={`${projectConfig.locale.labels.gallery}: ${title}`}>
32
+ <section
33
+ class="gallery-item gallery-carousel"
34
+ data-carousel
35
+ tabindex="0"
36
+ role="group"
37
+ aria-roledescription="carousel"
38
+ aria-label={`${title}, bildkarusell`}
39
+ style={carouselStyle}
40
+ >
41
+ <div class="gallery-carousel-stage">
42
+ <div class="gallery-carousel-viewport" data-carousel-viewport>
43
+ <div class="gallery-carousel-slides">
44
+ {images.map((image, imageIndex) => {
45
+ const captionId = image.caption ? getCaptionId(image, 0, imageIndex) : undefined;
46
+
47
+ return (
48
+ <figure class="gallery-carousel-slide" data-carousel-slide>
49
+ <div class="gallery-carousel-image-frame">
50
+ <img
51
+ {...getImageAttributes(image.src ?? image.image, galleryImageSizes)}
52
+ class:list={['gallery-image', getImageClass(image)]}
53
+ alt={image.alt ?? ''}
54
+ aria-describedby={captionId}
55
+ {...getImageLoadingProps(priorityFirstImage && imageIndex === 0)}
56
+ />
57
+ </div>
58
+ </figure>
59
+ );
60
+ })}
61
+ </div>
62
+ </div>
63
+ <button
64
+ class="gallery-carousel-button gallery-carousel-button-previous"
65
+ type="button"
66
+ data-carousel-previous
67
+ aria-label="Föregående bild"
68
+ title="Föregående bild"
69
+ >
70
+ <span aria-hidden="true">‹</span>
71
+ </button>
72
+ <button
73
+ class="gallery-carousel-button gallery-carousel-button-next"
74
+ type="button"
75
+ data-carousel-next
76
+ aria-label="Nästa bild"
77
+ title="Nästa bild"
78
+ >
79
+ <span aria-hidden="true">›</span>
80
+ </button>
81
+ <output class="gallery-carousel-position" data-carousel-position aria-live="polite">
82
+ 1 / {images.length}
83
+ </output>
84
+ </div>
85
+ <div class="gallery-carousel-captions" data-carousel-captions>
86
+ {images.map((image, imageIndex) => (
87
+ image.caption && (
88
+ <p
89
+ class="gallery-meta gallery-carousel-caption"
90
+ id={getCaptionId(image, 0, imageIndex)}
91
+ data-carousel-caption
92
+ data-carousel-caption-index={imageIndex}
93
+ >
94
+ <span class="gallery-details">
95
+ <span>{image.caption}</span>
96
+ </span>
97
+ </p>
98
+ )
99
+ ))}
100
+ </div>
101
+ </section>
102
+ </div>
103
+
104
+ <script>
105
+ import EmblaCarousel from 'embla-carousel';
106
+
107
+ const initializeCarousel = (carousel: HTMLElement) => {
108
+ if (carousel.dataset.carouselInitialized === 'true') return;
109
+
110
+ const viewport = carousel.querySelector<HTMLElement>('[data-carousel-viewport]');
111
+ const previous = carousel.querySelector<HTMLButtonElement>('[data-carousel-previous]');
112
+ const next = carousel.querySelector<HTMLButtonElement>('[data-carousel-next]');
113
+ const position = carousel.querySelector<HTMLOutputElement>('[data-carousel-position]');
114
+ const captions = Array.from(carousel.querySelectorAll<HTMLElement>('[data-carousel-caption]'));
115
+ const slides = Array.from(carousel.querySelectorAll<HTMLElement>('[data-carousel-slide]'));
116
+ if (!viewport || !previous || !next || !position || slides.length < 2) return;
117
+
118
+ let controlsTimeout: number | undefined;
119
+ const showControls = () => {
120
+ carousel.classList.add('is-interacting');
121
+ window.clearTimeout(controlsTimeout);
122
+ controlsTimeout = window.setTimeout(() => {
123
+ carousel.classList.remove('is-interacting');
124
+ }, 1600);
125
+ };
126
+
127
+ carousel.dataset.carouselInitialized = 'true';
128
+ carousel.dataset.carouselReady = 'true';
129
+
130
+ const embla = EmblaCarousel(viewport, {
131
+ align: 'start',
132
+ containScroll: false,
133
+ loop: true,
134
+ });
135
+
136
+ const update = () => {
137
+ const activeIndex = embla.selectedScrollSnap();
138
+ slides.forEach((slide, index) => {
139
+ const isSelected = index === activeIndex;
140
+ slide.classList.toggle('is-active', isSelected);
141
+ slide.setAttribute('aria-hidden', String(!isSelected));
142
+ });
143
+ captions.forEach((caption) => {
144
+ const isSelected = Number(caption.dataset.carouselCaptionIndex) === activeIndex;
145
+ caption.classList.toggle('is-active', isSelected);
146
+ caption.setAttribute('aria-hidden', String(!isSelected));
147
+ });
148
+ previous.disabled = false;
149
+ next.disabled = false;
150
+ position.textContent = `${activeIndex + 1} / ${slides.length}`;
151
+ };
152
+
153
+ previous.addEventListener('click', () => {
154
+ showControls();
155
+ embla.scrollPrev();
156
+ });
157
+ next.addEventListener('click', () => {
158
+ showControls();
159
+ embla.scrollNext();
160
+ });
161
+ carousel.addEventListener('focusin', showControls);
162
+ carousel.addEventListener('pointerdown', showControls);
163
+ carousel.addEventListener('keydown', (event) => {
164
+ if (event.key === 'ArrowLeft') {
165
+ event.preventDefault();
166
+ showControls();
167
+ embla.scrollPrev();
168
+ }
169
+ if (event.key === 'ArrowRight') {
170
+ event.preventDefault();
171
+ showControls();
172
+ embla.scrollNext();
173
+ }
174
+ });
175
+ embla.on('select', () => {
176
+ showControls();
177
+ update();
178
+ });
179
+ embla.on('reInit', update);
180
+ update();
181
+ };
182
+
183
+ document.querySelectorAll<HTMLElement>('[data-carousel]').forEach(initializeCarousel);
184
+ </script>