@janga/norna 0.7.9 → 0.7.11

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 (287) hide show
  1. package/README.md +4 -2
  2. package/bin/norna-cli.mjs +3 -0
  3. package/docs/README.md +16 -12
  4. package/docs/commands.md +6 -0
  5. package/docs/configuration.md +10 -54
  6. package/docs/content.md +20 -21
  7. package/docs/design/norna-diagram-design.md +2 -0
  8. package/docs/design/site-examples-structure.md +35 -24
  9. package/docs/engine-development.md +13 -7
  10. package/docs/getting-started.md +56 -19
  11. package/docs/routes.md +8 -6
  12. package/docs/site-structure.md +11 -6
  13. package/docs/sitewide-content.md +74 -0
  14. package/docs/theme.md +125 -140
  15. package/docs/typography.md +72 -45
  16. package/examples/README.md +41 -0
  17. package/examples/complete-sites/dog-shelter-multi-page/.astro/collections/site.schema.json +59 -0
  18. package/examples/complete-sites/dog-shelter-multi-page/.astro/collections/sitewide.schema.json +138 -0
  19. package/examples/complete-sites/dog-shelter-multi-page/.astro/collections/theme.schema.json +755 -0
  20. package/examples/complete-sites/dog-shelter-multi-page/.astro/content-modules.mjs +1 -0
  21. package/examples/{typography-presets → complete-sites/dog-shelter-multi-page}/.astro/content.d.ts +8 -0
  22. package/examples/complete-sites/dog-shelter-multi-page/.astro/data-store.json +1 -0
  23. package/examples/{typography-presets → complete-sites/dog-shelter-multi-page}/.astro/dev-local.json +2 -2
  24. package/examples/{typography-presets → complete-sites/dog-shelter-multi-page}/.astro/dev.json +2 -2
  25. package/examples/complete-sites/dog-shelter-multi-page/.astro/dev.log +53 -0
  26. package/examples/complete-sites/dog-shelter-multi-page/.astro/settings.json +5 -0
  27. package/examples/complete-sites/dog-shelter-multi-page/README.md +9 -0
  28. package/examples/{dog-gallery → complete-sites/dog-shelter-multi-page}/node_modules/.vite/deps/_metadata.json +13 -7
  29. package/examples/complete-sites/dog-shelter-multi-page/site/.norna/generated-images.json +18 -0
  30. package/examples/complete-sites/dog-shelter-multi-page/site/config.mjs +30 -0
  31. package/examples/complete-sites/dog-shelter-multi-page/site/content.md +12 -0
  32. package/examples/complete-sites/dog-shelter-multi-page/site/public/favicon.svg +10 -0
  33. package/examples/complete-sites/dog-shelter-multi-page/site/routes/010-dogs/images/dogs/rex.svg +19 -0
  34. package/examples/complete-sites/dog-shelter-multi-page/site/routes/010-dogs/images/dogs/rover.svg +19 -0
  35. package/examples/complete-sites/dog-shelter-multi-page/site/routes/010-dogs/route-content.md +30 -0
  36. package/examples/complete-sites/dog-shelter-multi-page/site/routes/020-adopt/route-content.md +12 -0
  37. package/examples/complete-sites/dog-shelter-multi-page/site/sitewide-content.md +6 -0
  38. package/examples/complete-sites/dog-shelter-multi-page/site/theme.md +25 -0
  39. package/examples/complete-sites/dog-shelter-single-page/README.md +9 -0
  40. package/examples/complete-sites/dog-shelter-single-page/site/.norna/generated-images.json +18 -0
  41. package/examples/complete-sites/dog-shelter-single-page/site/config.mjs +30 -0
  42. package/examples/complete-sites/dog-shelter-single-page/site/content.md +38 -0
  43. package/examples/complete-sites/dog-shelter-single-page/site/images/what-we-do/dog-house.svg +25 -0
  44. package/examples/complete-sites/dog-shelter-single-page/site/images/you-can-help/heart.svg +13 -0
  45. package/examples/complete-sites/dog-shelter-single-page/site/public/favicon.svg +10 -0
  46. package/examples/complete-sites/dog-shelter-single-page/site/public/robots.txt +2 -0
  47. package/examples/complete-sites/dog-shelter-single-page/site/sitewide-content.md +6 -0
  48. package/examples/complete-sites/dog-shelter-single-page/site/theme.md +25 -0
  49. package/examples/feature-demos/media-and-surfaces/.astro/collections/site.schema.json +59 -0
  50. package/examples/feature-demos/media-and-surfaces/.astro/collections/sitewide.schema.json +138 -0
  51. package/examples/feature-demos/media-and-surfaces/.astro/collections/theme.schema.json +755 -0
  52. package/examples/feature-demos/media-and-surfaces/.astro/content-modules.mjs +1 -0
  53. package/examples/feature-demos/media-and-surfaces/.astro/content.d.ts +183 -0
  54. package/examples/feature-demos/media-and-surfaces/.astro/data-store.json +1 -0
  55. package/examples/feature-demos/media-and-surfaces/.astro/dev.log +11 -0
  56. package/examples/feature-demos/media-and-surfaces/.astro/settings.json +5 -0
  57. package/examples/feature-demos/media-and-surfaces/.astro/types.d.ts +2 -0
  58. package/examples/feature-demos/media-and-surfaces/README.md +15 -0
  59. package/examples/feature-demos/media-and-surfaces/node_modules/.vite/deps/_metadata.json +74 -0
  60. package/examples/feature-demos/media-and-surfaces/node_modules/.vite/deps/astro-B3JHJyiv.js +434 -0
  61. package/examples/feature-demos/media-and-surfaces/node_modules/.vite/deps/astro-B3JHJyiv.js.map +1 -0
  62. package/examples/feature-demos/media-and-surfaces/node_modules/.vite/deps/astro_n_aria-query.js +6390 -0
  63. package/examples/feature-demos/media-and-surfaces/node_modules/.vite/deps/astro_n_aria-query.js.map +1 -0
  64. package/examples/feature-demos/media-and-surfaces/node_modules/.vite/deps/astro_n_axobject-query.js +2702 -0
  65. package/examples/feature-demos/media-and-surfaces/node_modules/.vite/deps/astro_n_axobject-query.js.map +1 -0
  66. package/examples/feature-demos/media-and-surfaces/node_modules/.vite/deps/astro_n_html-escaper.js +65 -0
  67. package/examples/feature-demos/media-and-surfaces/node_modules/.vite/deps/astro_n_html-escaper.js.map +1 -0
  68. package/examples/feature-demos/media-and-surfaces/node_modules/.vite/deps/astro_runtime_client_dev-toolbar_entrypoint__js.js +455 -0
  69. package/examples/feature-demos/media-and-surfaces/node_modules/.vite/deps/astro_runtime_client_dev-toolbar_entrypoint__js.js.map +1 -0
  70. package/examples/feature-demos/media-and-surfaces/node_modules/.vite/deps/audit-C3SVr7dT.js +1534 -0
  71. package/examples/feature-demos/media-and-surfaces/node_modules/.vite/deps/audit-C3SVr7dT.js.map +1 -0
  72. package/examples/feature-demos/media-and-surfaces/node_modules/.vite/deps/highlight-BLtvqtzO.js +48 -0
  73. package/examples/feature-demos/media-and-surfaces/node_modules/.vite/deps/highlight-BLtvqtzO.js.map +1 -0
  74. package/examples/feature-demos/media-and-surfaces/node_modules/.vite/deps/icons-C92i61-2.js +42 -0
  75. package/examples/feature-demos/media-and-surfaces/node_modules/.vite/deps/icons-C92i61-2.js.map +1 -0
  76. package/examples/feature-demos/media-and-surfaces/node_modules/.vite/deps/settings-v5GgKks_.js +190 -0
  77. package/examples/feature-demos/media-and-surfaces/node_modules/.vite/deps/settings-v5GgKks_.js.map +1 -0
  78. package/examples/feature-demos/media-and-surfaces/node_modules/.vite/deps/toolbar-BFvJVp26.js +458 -0
  79. package/examples/feature-demos/media-and-surfaces/node_modules/.vite/deps/toolbar-BFvJVp26.js.map +1 -0
  80. package/examples/feature-demos/media-and-surfaces/node_modules/.vite/deps/ui-library-Cd35oAvo.js +1016 -0
  81. package/examples/feature-demos/media-and-surfaces/node_modules/.vite/deps/ui-library-Cd35oAvo.js.map +1 -0
  82. package/examples/feature-demos/media-and-surfaces/node_modules/.vite/deps/window-vo3A6TMn.js +32 -0
  83. package/examples/feature-demos/media-and-surfaces/node_modules/.vite/deps/window-vo3A6TMn.js.map +1 -0
  84. package/examples/feature-demos/media-and-surfaces/node_modules/.vite/deps/xray-DwhzGckb.js +111 -0
  85. package/examples/feature-demos/media-and-surfaces/node_modules/.vite/deps/xray-DwhzGckb.js.map +1 -0
  86. package/examples/feature-demos/media-and-surfaces/site/.norna/generated-images.json +66 -0
  87. package/examples/feature-demos/media-and-surfaces/site/config.mjs +30 -0
  88. package/examples/feature-demos/media-and-surfaces/site/content.md +64 -0
  89. package/examples/feature-demos/media-and-surfaces/site/public/robots.txt +2 -0
  90. package/examples/feature-demos/media-and-surfaces/site/routes/010-media/images/cards/card-carousel.svg +11 -0
  91. package/examples/feature-demos/media-and-surfaces/site/routes/010-media/images/cards/card-stack.svg +9 -0
  92. package/examples/feature-demos/media-and-surfaces/site/routes/010-media/images/cards/card-surfaces.svg +8 -0
  93. package/examples/feature-demos/media-and-surfaces/site/routes/010-media/images/carousel/carousel-one.svg +8 -0
  94. package/examples/feature-demos/media-and-surfaces/site/routes/010-media/images/carousel/carousel-three.svg +13 -0
  95. package/examples/feature-demos/media-and-surfaces/site/routes/010-media/images/carousel/carousel-two.svg +8 -0
  96. package/examples/feature-demos/media-and-surfaces/site/routes/010-media/images/stack/stack-one.svg +8 -0
  97. package/examples/feature-demos/media-and-surfaces/site/routes/010-media/images/stack/stack-two.svg +9 -0
  98. package/examples/feature-demos/media-and-surfaces/site/routes/010-media/route-content.md +85 -0
  99. package/examples/feature-demos/media-and-surfaces/site/routes/010-media/theme.md +3 -0
  100. package/examples/feature-demos/media-and-surfaces/site/routes/020-surfaces/route-content.md +44 -0
  101. package/examples/feature-demos/media-and-surfaces/site/routes/020-surfaces/theme.md +8 -0
  102. package/examples/feature-demos/media-and-surfaces/site/sitewide-content.md +6 -0
  103. package/examples/feature-demos/media-and-surfaces/site/theme.md +3 -0
  104. package/examples/{typography-presets/.astro/collections/theme.schema.json → feature-demos/theme-presets/.astro/collections/routeTheme.schema.json} +143 -65
  105. package/examples/feature-demos/theme-presets/.astro/collections/site.schema.json +59 -0
  106. package/examples/feature-demos/theme-presets/.astro/collections/sitewide.schema.json +138 -0
  107. package/examples/feature-demos/theme-presets/.astro/collections/theme.schema.json +755 -0
  108. package/examples/feature-demos/theme-presets/.astro/content-assets.mjs +4 -0
  109. package/examples/feature-demos/theme-presets/.astro/content-modules.mjs +1 -0
  110. package/examples/feature-demos/theme-presets/.astro/content.d.ts +183 -0
  111. package/examples/feature-demos/theme-presets/.astro/data-store.json +1 -0
  112. package/examples/feature-demos/theme-presets/.astro/dev-local.json +8 -0
  113. package/examples/feature-demos/theme-presets/.astro/dev.json +15 -0
  114. package/examples/feature-demos/theme-presets/.astro/dev.log +127 -0
  115. package/examples/feature-demos/theme-presets/.astro/types.d.ts +2 -0
  116. package/examples/feature-demos/theme-presets/README.md +14 -0
  117. package/examples/feature-demos/theme-presets/node_modules/.vite/deps/embla-carousel.js +1389 -0
  118. package/examples/feature-demos/theme-presets/node_modules/.vite/deps/embla-carousel.js.map +1 -0
  119. package/examples/feature-demos/theme-presets/node_modules/.vite/deps/package.json +3 -0
  120. package/examples/feature-demos/theme-presets/node_modules/.vite/deps/rolldown-runtime-BvCyGRYZ.js +4 -0
  121. package/examples/feature-demos/theme-presets/site/.norna/generated-images.json +290 -0
  122. package/examples/{typography-presets → feature-demos/theme-presets}/site/config.mjs +4 -16
  123. package/examples/feature-demos/theme-presets/site/content.md +17 -0
  124. package/examples/feature-demos/theme-presets/site/routes/010-portfolio/route-content.md +34 -0
  125. package/examples/feature-demos/theme-presets/site/routes/010-portfolio/theme.md +3 -0
  126. package/examples/feature-demos/theme-presets/site/routes/020-documentation/route-content.md +33 -0
  127. package/examples/feature-demos/theme-presets/site/routes/020-documentation/theme.md +3 -0
  128. package/examples/feature-demos/theme-presets/site/routes/030-project/route-content.md +34 -0
  129. package/examples/feature-demos/theme-presets/site/routes/030-project/theme.md +3 -0
  130. package/examples/feature-demos/theme-presets/site/routes/040-statement/route-content.md +34 -0
  131. package/examples/feature-demos/theme-presets/site/routes/040-statement/theme.md +3 -0
  132. package/examples/feature-demos/theme-presets/site/sitewide-content.md +14 -0
  133. package/examples/feature-demos/theme-presets/site/theme.md +3 -0
  134. package/fixtures/basic/site/config.mjs +0 -13
  135. package/fixtures/basic/site/sitewide-content.md +12 -0
  136. package/fixtures/basic/site/theme.md +2 -2
  137. package/fixtures/content-model-v2/site/content.md +2 -5
  138. package/fixtures/content-model-v2/site/theme.md +1 -3
  139. package/package.json +9 -4
  140. package/scripts/build-pages.mjs +71 -0
  141. package/scripts/check-config.mjs +23 -1
  142. package/scripts/export-theme-preset.mjs +31 -0
  143. package/scripts/init-site.mjs +1 -0
  144. package/scripts/lib/example-sites.mjs +34 -0
  145. package/scripts/lib/frontmatter-yaml.mjs +28 -0
  146. package/scripts/lib/logo-assets.mjs +35 -0
  147. package/scripts/lib/norna-markdown-blocks.mjs +166 -0
  148. package/scripts/lib/presentation.mjs +106 -47
  149. package/scripts/lib/project-config.mjs +68 -105
  150. package/scripts/lib/site-content.mjs +26 -118
  151. package/scripts/lib/site-paths.mjs +2 -0
  152. package/scripts/lib/sitewide-config.mjs +34 -0
  153. package/scripts/lib/theme-config.mjs +75 -8
  154. package/scripts/lib/theme-presets.mjs +209 -0
  155. package/scripts/lib/typography.mjs +20 -26
  156. package/scripts/show-typography.mjs +43 -149
  157. package/scripts/sync-content-sections.mjs +19 -17
  158. package/scripts/test-content-check.mjs +59 -27
  159. package/scripts/test-content-model-v2.mjs +154 -0
  160. package/scripts/test-engine-commands.mjs +4 -1
  161. package/scripts/test-examples.mjs +15 -0
  162. package/scripts/test-navigation.mjs +73 -45
  163. package/scripts/test-package-check.mjs +66 -24
  164. package/scripts/test-temporary-visibility.mjs +19 -2
  165. package/scripts/test-theme-presets.mjs +151 -0
  166. package/src/components/CardList.astro +5 -2
  167. package/src/components/GalleryGrid.astro +37 -16
  168. package/src/components/SiteBanners.astro +100 -0
  169. package/src/components/SiteNavigation.astro +48 -31
  170. package/src/components/SitePage.astro +55 -13
  171. package/src/components/SiteSection.astro +193 -35
  172. package/src/content.config.ts +80 -48
  173. package/src/layouts/BaseLayout.astro +64 -14
  174. package/src/lib/routeThemes.ts +61 -0
  175. package/src/lib/sectionContent.ts +235 -51
  176. package/src/lib/sitePublicAssets.ts +1 -0
  177. package/src/styles/global.css +284 -93
  178. package/starters/basic/.github/workflows/deploy.yml +1 -1
  179. package/starters/basic/README.md +7 -6
  180. package/starters/basic/package.json +1 -0
  181. package/starters/basic/site/config.mjs +0 -13
  182. package/starters/basic/site/content.md +1 -1
  183. package/starters/basic/site/sitewide-content.md +14 -0
  184. package/starters/basic/site/theme.md +13 -24
  185. package/starters/project/.github/workflows/deploy.yml +1 -1
  186. package/starters/project/README.md +10 -8
  187. package/starters/project/package.json +1 -0
  188. package/starters/project/site/config.mjs +0 -13
  189. package/starters/project/site/sitewide-content.md +14 -0
  190. package/starters/project/site/theme.md +3 -11
  191. package/examples/dog-gallery/site/.norna/generated-images.json +0 -226
  192. package/examples/dog-gallery/site/config.mjs +0 -67
  193. package/examples/dog-gallery/site/content.md +0 -128
  194. package/examples/dog-gallery/site/images/black-dogs/black-puppy-meadow.png +0 -0
  195. package/examples/dog-gallery/site/images/black-dogs/photo-of-a-black-dog.jpg +0 -0
  196. package/examples/dog-gallery/site/images/brown-dogs/brown-dog.jpg +0 -0
  197. package/examples/dog-gallery/site/images/brown-dogs/dog-accompanies-master.jpg +0 -0
  198. package/examples/dog-gallery/site/images/golden-dogs/golden-retriever.jpg +0 -0
  199. package/examples/dog-gallery/site/images/golden-dogs/toller-puppy.jpg +0 -0
  200. package/examples/dog-gallery/site/images/white-dogs/white-cute-dog.jpg +0 -0
  201. package/examples/dog-gallery/site/images/white-dogs/white-puppy-garden.png +0 -0
  202. package/examples/dog-gallery/site/public/favicon.svg +0 -7
  203. package/examples/dog-gallery/site/routes/010-dog-care/route-content.md +0 -38
  204. package/examples/dog-gallery/site/theme.md +0 -112
  205. package/examples/typography-presets/.astro/collections/site.schema.json +0 -776
  206. package/examples/typography-presets/.astro/data-store.json +0 -1
  207. package/examples/typography-presets/.astro/dev.log +0 -24
  208. package/examples/typography-presets/site/.norna/generated-images.json +0 -290
  209. package/examples/typography-presets/site/content.md +0 -134
  210. package/examples/typography-presets/site/theme.md +0 -43
  211. /package/examples/{typography-presets/.astro/content-modules.mjs → complete-sites/dog-shelter-multi-page/.astro/content-assets.mjs} +0 -0
  212. /package/examples/{typography-presets → complete-sites/dog-shelter-multi-page}/.astro/types.d.ts +0 -0
  213. /package/examples/{dog-gallery → complete-sites/dog-shelter-multi-page}/node_modules/.vite/deps/astro-D_GIpJIE.js +0 -0
  214. /package/examples/{dog-gallery → complete-sites/dog-shelter-multi-page}/node_modules/.vite/deps/astro-D_GIpJIE.js.map +0 -0
  215. /package/examples/{dog-gallery → complete-sites/dog-shelter-multi-page}/node_modules/.vite/deps/astro_n_aria-query.js +0 -0
  216. /package/examples/{dog-gallery → complete-sites/dog-shelter-multi-page}/node_modules/.vite/deps/astro_n_aria-query.js.map +0 -0
  217. /package/examples/{dog-gallery → complete-sites/dog-shelter-multi-page}/node_modules/.vite/deps/astro_n_axobject-query.js +0 -0
  218. /package/examples/{dog-gallery → complete-sites/dog-shelter-multi-page}/node_modules/.vite/deps/astro_n_axobject-query.js.map +0 -0
  219. /package/examples/{dog-gallery → complete-sites/dog-shelter-multi-page}/node_modules/.vite/deps/astro_n_html-escaper.js +0 -0
  220. /package/examples/{dog-gallery → complete-sites/dog-shelter-multi-page}/node_modules/.vite/deps/astro_n_html-escaper.js.map +0 -0
  221. /package/examples/{dog-gallery → complete-sites/dog-shelter-multi-page}/node_modules/.vite/deps/astro_runtime_client_dev-toolbar_entrypoint__js.js +0 -0
  222. /package/examples/{dog-gallery → complete-sites/dog-shelter-multi-page}/node_modules/.vite/deps/astro_runtime_client_dev-toolbar_entrypoint__js.js.map +0 -0
  223. /package/examples/{dog-gallery → complete-sites/dog-shelter-multi-page}/node_modules/.vite/deps/audit-DM9Hkl5B.js +0 -0
  224. /package/examples/{dog-gallery → complete-sites/dog-shelter-multi-page}/node_modules/.vite/deps/audit-DM9Hkl5B.js.map +0 -0
  225. /package/examples/{typography-presets → complete-sites/dog-shelter-multi-page}/node_modules/.vite/deps/embla-carousel.js +0 -0
  226. /package/examples/{typography-presets → complete-sites/dog-shelter-multi-page}/node_modules/.vite/deps/embla-carousel.js.map +0 -0
  227. /package/examples/{dog-gallery → complete-sites/dog-shelter-multi-page}/node_modules/.vite/deps/highlight-DnZ1ULOL.js +0 -0
  228. /package/examples/{dog-gallery → complete-sites/dog-shelter-multi-page}/node_modules/.vite/deps/highlight-DnZ1ULOL.js.map +0 -0
  229. /package/examples/{dog-gallery → complete-sites/dog-shelter-multi-page}/node_modules/.vite/deps/icons-RyebKi45.js +0 -0
  230. /package/examples/{dog-gallery → complete-sites/dog-shelter-multi-page}/node_modules/.vite/deps/icons-RyebKi45.js.map +0 -0
  231. /package/examples/{dog-gallery → complete-sites/dog-shelter-multi-page}/node_modules/.vite/deps/package.json +0 -0
  232. /package/examples/{dog-gallery → complete-sites/dog-shelter-multi-page}/node_modules/.vite/deps/rolldown-runtime-BvCyGRYZ.js +0 -0
  233. /package/examples/{dog-gallery → complete-sites/dog-shelter-multi-page}/node_modules/.vite/deps/settings-DaU-gNnK.js +0 -0
  234. /package/examples/{dog-gallery → complete-sites/dog-shelter-multi-page}/node_modules/.vite/deps/settings-DaU-gNnK.js.map +0 -0
  235. /package/examples/{dog-gallery → complete-sites/dog-shelter-multi-page}/node_modules/.vite/deps/toolbar-63PA619H.js +0 -0
  236. /package/examples/{dog-gallery → complete-sites/dog-shelter-multi-page}/node_modules/.vite/deps/toolbar-63PA619H.js.map +0 -0
  237. /package/examples/{dog-gallery → complete-sites/dog-shelter-multi-page}/node_modules/.vite/deps/ui-library--bkcNSYz.js +0 -0
  238. /package/examples/{dog-gallery → complete-sites/dog-shelter-multi-page}/node_modules/.vite/deps/ui-library--bkcNSYz.js.map +0 -0
  239. /package/examples/{dog-gallery → complete-sites/dog-shelter-multi-page}/node_modules/.vite/deps/window-jPkpKVcy.js +0 -0
  240. /package/examples/{dog-gallery → complete-sites/dog-shelter-multi-page}/node_modules/.vite/deps/window-jPkpKVcy.js.map +0 -0
  241. /package/examples/{dog-gallery → complete-sites/dog-shelter-multi-page}/node_modules/.vite/deps/xray-BXCHpEJP.js +0 -0
  242. /package/examples/{dog-gallery → complete-sites/dog-shelter-multi-page}/node_modules/.vite/deps/xray-BXCHpEJP.js.map +0 -0
  243. /package/examples/{dog-gallery → complete-sites/dog-shelter-multi-page}/site/public/robots.txt +0 -0
  244. /package/examples/{typography-presets → feature-demos/media-and-surfaces}/.astro/content-assets.mjs +0 -0
  245. /package/examples/{typography-presets → feature-demos/media-and-surfaces}/node_modules/.vite/deps/package.json +0 -0
  246. /package/examples/{typography-presets → feature-demos/media-and-surfaces}/node_modules/.vite/deps/rolldown-runtime-BvCyGRYZ.js +0 -0
  247. /package/examples/{typography-presets → feature-demos/theme-presets}/.astro/settings.json +0 -0
  248. /package/examples/{typography-presets → feature-demos/theme-presets}/node_modules/.vite/deps/_metadata.json +0 -0
  249. /package/examples/{typography-presets → feature-demos/theme-presets}/node_modules/.vite/deps/astro-D_GIpJIE.js +0 -0
  250. /package/examples/{typography-presets → feature-demos/theme-presets}/node_modules/.vite/deps/astro-D_GIpJIE.js.map +0 -0
  251. /package/examples/{typography-presets → feature-demos/theme-presets}/node_modules/.vite/deps/astro_n_aria-query.js +0 -0
  252. /package/examples/{typography-presets → feature-demos/theme-presets}/node_modules/.vite/deps/astro_n_aria-query.js.map +0 -0
  253. /package/examples/{typography-presets → feature-demos/theme-presets}/node_modules/.vite/deps/astro_n_axobject-query.js +0 -0
  254. /package/examples/{typography-presets → feature-demos/theme-presets}/node_modules/.vite/deps/astro_n_axobject-query.js.map +0 -0
  255. /package/examples/{typography-presets → feature-demos/theme-presets}/node_modules/.vite/deps/astro_n_html-escaper.js +0 -0
  256. /package/examples/{typography-presets → feature-demos/theme-presets}/node_modules/.vite/deps/astro_n_html-escaper.js.map +0 -0
  257. /package/examples/{typography-presets → feature-demos/theme-presets}/node_modules/.vite/deps/astro_runtime_client_dev-toolbar_entrypoint__js.js +0 -0
  258. /package/examples/{typography-presets → feature-demos/theme-presets}/node_modules/.vite/deps/astro_runtime_client_dev-toolbar_entrypoint__js.js.map +0 -0
  259. /package/examples/{typography-presets → feature-demos/theme-presets}/node_modules/.vite/deps/audit-DM9Hkl5B.js +0 -0
  260. /package/examples/{typography-presets → feature-demos/theme-presets}/node_modules/.vite/deps/audit-DM9Hkl5B.js.map +0 -0
  261. /package/examples/{typography-presets → feature-demos/theme-presets}/node_modules/.vite/deps/highlight-DnZ1ULOL.js +0 -0
  262. /package/examples/{typography-presets → feature-demos/theme-presets}/node_modules/.vite/deps/highlight-DnZ1ULOL.js.map +0 -0
  263. /package/examples/{typography-presets → feature-demos/theme-presets}/node_modules/.vite/deps/icons-RyebKi45.js +0 -0
  264. /package/examples/{typography-presets → feature-demos/theme-presets}/node_modules/.vite/deps/icons-RyebKi45.js.map +0 -0
  265. /package/examples/{typography-presets → feature-demos/theme-presets}/node_modules/.vite/deps/settings-DaU-gNnK.js +0 -0
  266. /package/examples/{typography-presets → feature-demos/theme-presets}/node_modules/.vite/deps/settings-DaU-gNnK.js.map +0 -0
  267. /package/examples/{typography-presets → feature-demos/theme-presets}/node_modules/.vite/deps/toolbar-63PA619H.js +0 -0
  268. /package/examples/{typography-presets → feature-demos/theme-presets}/node_modules/.vite/deps/toolbar-63PA619H.js.map +0 -0
  269. /package/examples/{typography-presets → feature-demos/theme-presets}/node_modules/.vite/deps/ui-library--bkcNSYz.js +0 -0
  270. /package/examples/{typography-presets → feature-demos/theme-presets}/node_modules/.vite/deps/ui-library--bkcNSYz.js.map +0 -0
  271. /package/examples/{typography-presets → feature-demos/theme-presets}/node_modules/.vite/deps/window-jPkpKVcy.js +0 -0
  272. /package/examples/{typography-presets → feature-demos/theme-presets}/node_modules/.vite/deps/window-jPkpKVcy.js.map +0 -0
  273. /package/examples/{typography-presets → feature-demos/theme-presets}/node_modules/.vite/deps/xray-BXCHpEJP.js +0 -0
  274. /package/examples/{typography-presets → feature-demos/theme-presets}/node_modules/.vite/deps/xray-BXCHpEJP.js.map +0 -0
  275. /package/examples/{typography-presets → feature-demos/theme-presets}/site/public/robots.txt +0 -0
  276. /package/examples/{typography-presets/site/images/quiet-gallery → feature-demos/theme-presets/site/routes/010-portfolio/images/portfolio}/puppy-portrait.jpg +0 -0
  277. /package/examples/{typography-presets/site/images/quiet-gallery → feature-demos/theme-presets/site/routes/010-portfolio/images/portfolio}/shepherd-portrait.jpg +0 -0
  278. /package/examples/{typography-presets/site/images/quiet-gallery → feature-demos/theme-presets/site/routes/010-portfolio/images/portfolio}/white-puppy.jpg +0 -0
  279. /package/examples/{typography-presets/site/images/compact-gallery → feature-demos/theme-presets/site/routes/020-documentation/images/documentation}/black-schnauzer.jpg +0 -0
  280. /package/examples/{typography-presets/site/images/compact-gallery → feature-demos/theme-presets/site/routes/020-documentation/images/documentation}/retriever-portrait.jpg +0 -0
  281. /package/examples/{typography-presets/site/images/compact-gallery → feature-demos/theme-presets/site/routes/020-documentation/images/documentation}/schnauzer-portrait.jpg +0 -0
  282. /package/examples/{typography-presets/site/images/text-forward → feature-demos/theme-presets/site/routes/030-project/images/project}/boxer-portrait.jpg +0 -0
  283. /package/examples/{typography-presets/site/images/text-forward → feature-demos/theme-presets/site/routes/030-project/images/project}/grass-puppy.jpg +0 -0
  284. /package/examples/{typography-presets/site/images/text-forward → feature-demos/theme-presets/site/routes/030-project/images/project}/grey-street-dog.jpg +0 -0
  285. /package/examples/{typography-presets/site → feature-demos/theme-presets/site/routes/040-statement}/images/statement/black-pedigree.jpg +0 -0
  286. /package/examples/{typography-presets/site → feature-demos/theme-presets/site/routes/040-statement}/images/statement/sheepdog-grass.jpg +0 -0
  287. /package/examples/{typography-presets/site → feature-demos/theme-presets/site/routes/040-statement}/images/statement/sheepdog-wide.jpg +0 -0
@@ -1,6 +1,6 @@
1
1
  ---
2
- import type { CollectionEntry } from 'astro:content';
3
- import { resolveTypographyOverride } from '../../scripts/lib/typography.mjs';
2
+ import projectConfig from '../../scripts/lib/project-config.mjs';
3
+ import { resolveSectionSurface } from '../../scripts/lib/presentation.mjs';
4
4
  import CardList from './CardList.astro';
5
5
  import GalleryGrid from './GalleryGrid.astro';
6
6
  import type { ResolvedSection } from '../lib/sectionContent';
@@ -9,31 +9,44 @@ type ResponsiveValue<T> = { desktop: T; mobile: T };
9
9
  type ResponsiveOverride<T> = { desktop?: T; mobile?: T };
10
10
  type TextAlign = 'left' | 'center' | 'right';
11
11
  type TextSize = 'small' | 'medium' | 'large' | 'xlarge';
12
- type SectionPresentationOverride = {
13
- backgroundColor?: string;
14
- textColor?: string;
15
- typography?: NonNullable<ResolvedSection['presentation']>['typography'];
16
- };
12
+ type TextWidth = 'narrow' | 'normal' | 'wide';
17
13
  type ResolvedPagePresentation = {
18
- backgroundColor: string;
19
- textColor: string;
14
+ palette: {
15
+ surfaces: Record<'base' | 'soft' | 'emphasis', { backgroundColor: string; textColor: string }>;
16
+ };
17
+ sectionSurfaces: {
18
+ mode: 'none' | 'cycle';
19
+ sequence: Array<'base' | 'soft' | 'emphasis'>;
20
+ };
20
21
  typography: {
21
22
  preset: string;
22
23
  values: any;
23
24
  };
24
25
  };
26
+ type VisualTheme = {
27
+ gallery: {
28
+ maxAvailableWidthPercent: {
29
+ desktop: number;
30
+ mobile: number;
31
+ };
32
+ };
33
+ };
25
34
 
26
35
  interface Props {
27
36
  section: ResolvedSection;
28
37
  pagePresentation: ResolvedPagePresentation;
38
+ visualTheme: VisualTheme;
39
+ sectionIndex: number;
29
40
  headingLevel: 1 | 2;
30
41
  priorityGalleryImage?: boolean;
31
42
  }
32
43
 
33
- const { section, pagePresentation, headingLevel, priorityGalleryImage = false } = Astro.props;
44
+ const { section, pagePresentation, visualTheme, sectionIndex, headingLevel, priorityGalleryImage = false } = Astro.props;
34
45
  const HeadingTag = headingLevel === 1 ? 'h1' : 'h2';
35
46
  // Sections are authored with Markdown ## headings even when the first one renders as HTML h1.
36
47
  const sectionHeadingVisualLevel = 'h2';
48
+ const firstContentBlockIsVisual = section.contentBlocks[0]?.type !== 'html'
49
+ && section.contentBlocks[0]?.type !== 'note';
37
50
  type HeadingLevel = 'h1' | 'h2' | 'h3' | 'h4';
38
51
  const headingSizeValues: Record<HeadingLevel, Record<TextSize, ResponsiveValue<string>>> = {
39
52
  h1: {
@@ -145,7 +158,6 @@ const captionSizeValues: Record<TextSize, ResponsiveValue<string>> = {
145
158
  mobile: 'clamp(1.04rem, 0.98rem + 0.18vw, 1.12rem)',
146
159
  },
147
160
  };
148
- const sectionOverride = section.presentation as SectionPresentationOverride | undefined;
149
161
  const getSizeValue = (
150
162
  sizeValues: Record<TextSize, ResponsiveValue<string>>,
151
163
  size: TextSize,
@@ -153,15 +165,17 @@ const getSizeValue = (
153
165
  const getHeadingSizeValue = (level: HeadingLevel, size: TextSize) => (
154
166
  headingSizeValues[level][size]
155
167
  );
156
- const backgroundColor = sectionOverride?.backgroundColor
157
- ?? pagePresentation.backgroundColor;
158
- const textColor = sectionOverride?.textColor
159
- ?? pagePresentation.textColor;
160
- const resolvedTypography = resolveTypographyOverride(pagePresentation.typography, sectionOverride?.typography);
161
- const typography = resolvedTypography.values;
168
+ const textWidthValues: Record<TextWidth, string> = {
169
+ narrow: 'min(60ch, var(--text-width))',
170
+ normal: 'min(72ch, var(--text-width))',
171
+ wide: 'min(72ch, var(--gallery-layout-width))',
172
+ };
173
+ const surface = resolveSectionSurface(pagePresentation, sectionIndex);
174
+ const typography = pagePresentation.typography.values;
162
175
  const heading = typography.headings[sectionHeadingVisualLevel] as {
163
176
  align: ResponsiveValue<TextAlign>;
164
177
  size: TextSize;
178
+ weight: number;
165
179
  lineHeight: number;
166
180
  spacingBefore: string;
167
181
  spacingAfter: string;
@@ -172,6 +186,7 @@ const markdownHeadings = Object.fromEntries(markdownHeadingLevels.map((level) =>
172
186
  typography.headings[level] as {
173
187
  align: ResponsiveValue<TextAlign>;
174
188
  size: TextSize;
189
+ weight: number;
175
190
  lineHeight: number;
176
191
  spacingBefore: string;
177
192
  spacingAfter: string;
@@ -188,20 +203,54 @@ const justifyByAlign: Record<TextAlign, string> = {
188
203
  center: 'center',
189
204
  right: 'flex-end',
190
205
  };
191
- const getAlignedTextWidth = (
192
- align: ResponsiveValue<TextAlign>,
193
- centeredWidth: string,
194
- ): ResponsiveValue<string> => ({
195
- desktop: align.desktop === 'center' ? centeredWidth : 'var(--gallery-layout-width)',
196
- mobile: align.mobile === 'center' ? centeredWidth : 'var(--gallery-layout-width)',
197
- });
198
- const headingWidth = getAlignedTextWidth(headingAlign, '760px');
199
- const bodyWidth = getAlignedTextWidth(bodyAlign, 'var(--text-width)');
206
+ const bodyWidthValue = textWidthValues[typography.body.width as TextWidth];
207
+ const headingWidth = {
208
+ desktop: bodyWidthValue,
209
+ mobile: bodyWidthValue,
210
+ };
211
+ const bodyWidth = {
212
+ desktop: bodyWidthValue,
213
+ mobile: bodyWidthValue,
214
+ };
215
+ const isMediaBlock = (block: ResolvedSection['contentBlocks'][number]) => (
216
+ block.type === 'image-stack'
217
+ || block.type === 'image-carousel'
218
+ || block.type === 'card-list'
219
+ );
200
220
  const hasMedia = section.contentBlocks.some((block) => (
201
221
  block.type === 'image-stack'
202
222
  || block.type === 'image-carousel'
203
223
  || (block.type === 'card-list' && block.cards.some((card) => card.image))
204
224
  ));
225
+ const hasMarginNotes = section.contentBlocks.some((block) => (
226
+ block.type === 'note'
227
+ ));
228
+ const getNoteGridRow = (blocks: ResolvedSection['contentBlocks'], blockIndex: number) => Math.max(
229
+ 1,
230
+ blocks.slice(0, blockIndex).filter((block) => block.type === 'html').length,
231
+ );
232
+ const noteLabel = projectConfig.locale.labels.note;
233
+ const contentGroups: Array<
234
+ | { type: 'reading'; blocks: ResolvedSection['contentBlocks'] }
235
+ | { type: 'media'; block: Extract<ResolvedSection['contentBlocks'][number], { type: 'image-stack' | 'image-carousel' | 'card-list' }> }
236
+ > = [];
237
+ let readingBlocks: ResolvedSection['contentBlocks'] = [];
238
+ const flushReadingBlocks = () => {
239
+ if (readingBlocks.length === 0) return;
240
+ contentGroups.push({ type: 'reading', blocks: readingBlocks });
241
+ readingBlocks = [];
242
+ };
243
+
244
+ for (const block of section.contentBlocks) {
245
+ if (isMediaBlock(block)) {
246
+ flushReadingBlocks();
247
+ contentGroups.push({ type: 'media', block });
248
+ } else {
249
+ readingBlocks = [...readingBlocks, block];
250
+ }
251
+ }
252
+
253
+ flushReadingBlocks();
205
254
  const markdownHeadingStyle = (level: 'h3' | 'h4') => {
206
255
  const headingConfig = markdownHeadings[level];
207
256
  const fontSize = getHeadingSizeValue(level, headingConfig.size);
@@ -211,18 +260,20 @@ const markdownHeadingStyle = (level: 'h3' | 'h4') => {
211
260
  `--section-markdown-${level}-align-mobile: ${headingConfig.align.mobile}`,
212
261
  `--section-markdown-${level}-font-size-desktop: ${fontSize.desktop}`,
213
262
  `--section-markdown-${level}-font-size-mobile: ${fontSize.mobile}`,
263
+ `--section-markdown-${level}-font-weight: ${headingConfig.weight}`,
214
264
  `--section-markdown-${level}-line-height: ${headingConfig.lineHeight}`,
215
265
  `--section-markdown-${level}-spacing-before: ${headingConfig.spacingBefore}`,
216
266
  `--section-markdown-${level}-spacing-after: ${headingConfig.spacingAfter}`,
217
267
  ];
218
268
  };
219
269
  const sectionStyle = [
220
- `--section-background-color: ${backgroundColor}`,
221
- `--section-text-color: ${textColor}`,
270
+ `--section-background-color: ${surface.backgroundColor}`,
271
+ `--section-text-color: ${surface.textColor}`,
222
272
  `--section-heading-align-desktop: ${headingAlign.desktop}`,
223
273
  `--section-heading-align-mobile: ${headingAlign.mobile}`,
224
274
  `--section-heading-font-size-desktop: ${headingFontSize.desktop}`,
225
275
  `--section-heading-font-size-mobile: ${headingFontSize.mobile}`,
276
+ `--section-heading-font-weight: ${heading.weight}`,
226
277
  `--section-heading-line-height: ${heading.lineHeight}`,
227
278
  `--section-heading-spacing-after: ${heading.spacingAfter}`,
228
279
  `--section-heading-width-desktop: ${headingWidth.desktop}`,
@@ -250,7 +301,13 @@ const sectionStyle = [
250
301
 
251
302
  <section
252
303
  id={section.id}
253
- class="site-section"
304
+ class:list={[
305
+ 'site-section',
306
+ {
307
+ 'site-section-first-block-is-visual': firstContentBlockIsVisual,
308
+ 'site-section-has-margin-notes': hasMarginNotes,
309
+ },
310
+ ]}
254
311
  data-section-title={section.title}
255
312
  style={sectionStyle}
256
313
  >
@@ -260,33 +317,134 @@ const sectionStyle = [
260
317
  </header>
261
318
 
262
319
  <div class:list={['section-body', { 'section-body-has-gallery': hasMedia, 'section-body-text-only': !hasMedia }]}>
263
- {section.contentBlocks.map((block, blockIndex) => {
264
- if (block.type === 'html') {
265
- return block.html && <div class="section-markdown" set:html={block.html} />;
320
+ {contentGroups.map((group, groupIndex) => {
321
+ if (group.type === 'reading') {
322
+ if (hasMarginNotes && group.blocks.some((block) => block.type === 'note')) {
323
+ return (
324
+ <div class="section-reading-layout">
325
+ {group.blocks.map((block, blockIndex) => {
326
+ if (block.type === 'html') {
327
+ return block.html && <div class="section-markdown" set:html={block.html} />;
328
+ }
329
+
330
+ const noteGridRow = getNoteGridRow(group.blocks, blockIndex);
331
+ return (
332
+ <aside
333
+ class="section-note section-note-margin"
334
+ id={block.id}
335
+ aria-label={`${noteLabel}${block.number ? ` ${block.number}` : ''}`}
336
+ role="note"
337
+ data-note-id={block.id}
338
+ style={`grid-row: ${noteGridRow};`}
339
+ >
340
+ {block.number && block.referenceId && (
341
+ <a class="section-note-number" href={`#${block.referenceId}`} aria-label={`${noteLabel} ${block.number}`}>
342
+ {block.number}
343
+ </a>
344
+ )}
345
+ <div class="section-note-content" set:html={block.html} />
346
+ </aside>
347
+ );
348
+ })}
349
+ </div>
350
+ );
351
+ }
352
+
353
+ return group.blocks.map((block, blockIndex) => {
354
+ if (block.type === 'html') {
355
+ return block.html && <div class="section-markdown" set:html={block.html} />;
356
+ }
357
+
358
+ const noteGridRow = getNoteGridRow(group.blocks, blockIndex);
359
+ return (
360
+ <aside
361
+ class="section-note section-note-margin"
362
+ id={block.id}
363
+ aria-label={`${noteLabel}${block.number ? ` ${block.number}` : ''}`}
364
+ role="note"
365
+ data-note-id={block.id}
366
+ style={`grid-row: ${noteGridRow};`}
367
+ >
368
+ {block.number && block.referenceId && (
369
+ <a class="section-note-number" href={`#${block.referenceId}`} aria-label={`${noteLabel} ${block.number}`}>
370
+ {block.number}
371
+ </a>
372
+ )}
373
+ <div class="section-note-content" set:html={block.html} />
374
+ </aside>
375
+ );
376
+ });
266
377
  }
267
378
 
379
+ const block = group.block;
268
380
  if (block.type === 'image-carousel') {
269
381
  return (
270
382
  <GalleryGrid
271
383
  title={section.title}
272
384
  items={[{ carousel: block.images }]}
273
- priorityFirstImage={priorityGalleryImage && blockIndex === 0}
385
+ maxAvailableWidthPercent={visualTheme.gallery.maxAvailableWidthPercent}
386
+ priorityFirstImage={priorityGalleryImage && groupIndex === 0}
274
387
  />
275
388
  );
276
389
  }
277
390
 
278
391
  if (block.type === 'card-list') {
279
- return <CardList cards={block.cards} layout={block.layout} flow={block.flow} size={block.size} width={block.width} />;
392
+ return <CardList cards={block.cards} maxAvailableWidthPercent={visualTheme.gallery.maxAvailableWidthPercent} layout={block.layout} flow={block.flow} size={block.size} width={block.width} />;
280
393
  }
281
394
 
282
395
  return (
283
396
  <GalleryGrid
284
397
  title={section.title}
285
398
  items={block.images}
286
- priorityFirstImage={priorityGalleryImage && blockIndex === 0}
399
+ maxAvailableWidthPercent={visualTheme.gallery.maxAvailableWidthPercent}
400
+ priorityFirstImage={priorityGalleryImage && groupIndex === 0}
287
401
  />
288
402
  );
289
403
  })}
290
404
  </div>
291
405
  </div>
292
406
  </section>
407
+
408
+ <script>
409
+ const noteParts = Array.from(document.querySelectorAll<HTMLElement>('[data-note-id]'));
410
+ const noteReferences = noteParts.filter((part) => part.matches('.section-note-ref a'));
411
+ const marginNotes = noteParts.filter((part) => part.matches('.section-note'));
412
+
413
+ const setNoteState = (noteId: string, active: boolean) => {
414
+ noteParts
415
+ .filter((part) => part.dataset.noteId === noteId)
416
+ .forEach((part) => part.classList.toggle('is-referenced', active));
417
+ };
418
+
419
+ noteParts.forEach((part) => {
420
+ const noteId = part.dataset.noteId;
421
+ if (!noteId) return;
422
+
423
+ part.addEventListener('pointerenter', () => setNoteState(noteId, true));
424
+ part.addEventListener('pointerleave', () => setNoteState(noteId, false));
425
+ part.addEventListener('focusin', () => setNoteState(noteId, true));
426
+ part.addEventListener('focusout', () => setNoteState(noteId, false));
427
+ });
428
+
429
+ const alignMarginNotes = () => {
430
+ const wideLayout = window.matchMedia('(min-width: 1101px)').matches;
431
+ if (!wideLayout) {
432
+ marginNotes.forEach((note) => note.style.removeProperty('--section-note-offset'));
433
+ return;
434
+ }
435
+
436
+ marginNotes.forEach((note) => {
437
+ const noteId = note.dataset.noteId;
438
+ const reference = noteReferences.find((part) => part.dataset.noteId === noteId);
439
+ if (!noteId || !reference) return;
440
+
441
+ note.style.removeProperty('--section-note-offset');
442
+ const offset = reference.getBoundingClientRect().top - note.getBoundingClientRect().top;
443
+ note.style.setProperty('--section-note-offset', `${Math.max(0, offset)}px`);
444
+ });
445
+ };
446
+
447
+ alignMarginNotes();
448
+ window.addEventListener('resize', alignMarginNotes);
449
+ document.fonts?.ready.then(alignMarginNotes);
450
+ </script>
@@ -10,13 +10,15 @@ const siteEntryId = `${siteDirLabel
10
10
  .replace(/^[./\\]+/, '')
11
11
  .replace(/[^a-zA-Z0-9-]+/g, '-')
12
12
  .replace(/^-+|-+$/g, '') || 'site'}-content`;
13
- const colorValue = z.string().regex(
14
- /^#(?:[0-9a-fA-F]{3}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})$/,
15
- 'Use a hex color such as "#000000".',
16
- );
17
13
  const textAlign = z.enum(['left', 'center', 'right']);
18
14
  const textSize = z.enum(['small', 'medium', 'large', 'xlarge']);
15
+ const textWidth = z.enum(['narrow', 'normal', 'wide']);
16
+ const headingWeight = z.union([z.literal(400), z.literal(500), z.literal(600), z.literal(700)]);
19
17
  const typographyPreset = z.enum(['quiet-gallery', 'compact-gallery', 'text-forward', 'statement']);
18
+ const themePreset = z.enum(['portfolio', 'documentation', 'project', 'statement']);
19
+ const presentationPalette = z.enum(['dark', 'light', 'paper']);
20
+ const sectionSurfaceMode = z.enum(['none', 'cycle']);
21
+ const sectionSurface = z.enum(['base', 'soft', 'emphasis']);
20
22
  const spacingDensity = z.enum(['compact', 'normal', 'airy']);
21
23
  const lineHeight = z.number()
22
24
  .min(1, 'Use a unitless line height of at least 1.')
@@ -31,7 +33,6 @@ const visualCssLength = z.string().regex(
31
33
  new RegExp(`^(?:0|${visualCssLengthValue}|clamp\\(\\s*${visualCssLengthValue}\\s*,\\s*${visualCssLengthValue}\\s*,\\s*${visualCssLengthValue}\\s*\\))$`),
32
34
  'Use a CSS length such as "0", "900px", "56rem", "90%", or "clamp(1rem, 4vw, 3rem)".',
33
35
  );
34
- const inlineStyleName = z.string().regex(/^[a-z][a-z0-9-]*$/);
35
36
  const dateOnly = z.string()
36
37
  .regex(/^\d{4}-\d{2}-\d{2}$/, 'Use YYYY-MM-DD format.')
37
38
  .refine(isDateOnly, 'Use a real calendar date.');
@@ -59,6 +60,7 @@ const commonTextPresentationOverride = {
59
60
  };
60
61
  const headingPresentationOverride = z.object({
61
62
  ...commonTextPresentationOverride,
63
+ weight: headingWeight.optional(),
62
64
  spacingBefore: cssLength.optional(),
63
65
  spacingAfter: cssLength.optional(),
64
66
  }).strict();
@@ -70,6 +72,7 @@ const headingLevelsPresentationOverride = z.object({
70
72
  }).strict();
71
73
  const bodyPresentationOverride = z.object({
72
74
  ...commonTextPresentationOverride,
75
+ width: textWidth.optional(),
73
76
  paragraphSpacing: cssLength.optional(),
74
77
  }).strict();
75
78
  const captionPresentationOverride = z.object({
@@ -81,14 +84,6 @@ const typographyOverrides = z.object({
81
84
  body: bodyPresentationOverride.optional(),
82
85
  caption: captionPresentationOverride.optional(),
83
86
  }).strict();
84
- const typography = z.object({
85
- preset: typographyPreset.optional(),
86
- rhythm: spacingDensity.optional(),
87
- overrides: typographyOverrides.optional(),
88
- }).strict().refine(
89
- (value) => value.preset !== undefined || value.rhythm !== undefined || value.overrides !== undefined,
90
- 'Specify preset, rhythm, overrides, or both.',
91
- );
92
87
  const themeTypography = z.object({
93
88
  fontFamily: z.string()
94
89
  .min(1)
@@ -115,9 +110,10 @@ const responsivePercent = z.union([
115
110
  }).strict(),
116
111
  ]);
117
112
  const themeLayoutSpacing = z.object({
118
- bodyToImages: responsiveCssLength.optional(),
113
+ blockGap: responsiveCssLength.optional(),
119
114
  finalSectionBottom: responsiveCssLength.optional(),
120
115
  firstSectionTop: responsiveCssLength.optional(),
116
+ headingToBlock: responsiveCssLength.optional(),
121
117
  imageGap: responsiveCssLength.optional(),
122
118
  sectionGap: responsiveCssLength.optional(),
123
119
  }).strict();
@@ -132,32 +128,15 @@ const themeGallery = z.object({
132
128
  maxAvailableWidthPercent: responsivePercent.optional(),
133
129
  maxAvailableHeightPercent: responsivePercent.optional(),
134
130
  }).strict();
135
- const sectionPresentationOverride = z.object({
136
- backgroundColor: colorValue.optional(),
137
- textColor: colorValue.optional(),
138
- typography: typography.optional(),
139
- }).strict();
140
131
  const themePresentation = z.object({
141
- backgroundColor: colorValue.optional(),
142
- textColor: colorValue.optional(),
143
- inlineStyles: z.record(inlineStyleName, z.object({
144
- color: colorValue,
145
- }).strict()).optional(),
146
- }).strict();
147
- const pagePresentation = z.object({
148
- backgroundColor: colorValue.optional(),
149
- textColor: colorValue.optional(),
150
- typography: typography.optional(),
151
- }).strict();
152
- const frameColors = z.union([
153
- z.enum(['theme', 'presentation']),
154
- z.object({
155
- backgroundColor: colorValue,
156
- textColor: colorValue,
157
- }).strict(),
158
- ]);
159
- const frame = z.object({
160
- colors: frameColors.optional(),
132
+ palette: presentationPalette.optional(),
133
+ sectionSurfaces: z.object({
134
+ mode: sectionSurfaceMode,
135
+ sequence: z.array(sectionSurface).min(1).max(3).optional(),
136
+ }).strict().refine(
137
+ (value) => !value.sequence || new Set(value.sequence).size === value.sequence.length,
138
+ 'Each section surface may appear only once in a sequence.',
139
+ ).optional(),
161
140
  }).strict();
162
141
  const pageNavigation = z.object({
163
142
  include: z.boolean().optional(),
@@ -165,29 +144,73 @@ const pageNavigation = z.object({
165
144
  }).strict();
166
145
  const themeNavigation = z.object({
167
146
  brand: z.string().min(1).optional(),
147
+ logo: z.object({
148
+ alt: z.string().min(1).optional(),
149
+ height: visualCssLength.optional(),
150
+ }).strict().optional(),
168
151
  }).strict();
169
152
 
170
153
  const sectionMetadata = z.object({
171
154
  visible: visibilityWindow.optional(),
172
- presentation: sectionPresentationOverride.optional(),
155
+ }).strict();
156
+ const banner = z.object({
157
+ id: z.string().regex(/^[a-z0-9-]+$/),
158
+ tone: z.enum(['warning']).default('warning'),
159
+ visible: visibilityWindow.optional(),
160
+ title: z.string().min(1),
161
+ text: z.string().min(1),
162
+ }).strict();
163
+ const banners = z.array(banner).optional().default([]).refine(
164
+ (values) => new Set(values.map((value) => value.id)).size === values.length,
165
+ 'Banner ids must be unique.',
166
+ );
167
+ const dateTimeFormat = z.object({
168
+ locale: z.string().min(1),
169
+ timeZone: z.string().min(1),
170
+ dateStyle: z.string().min(1),
171
+ timeStyle: z.string().min(1),
172
+ }).strict().refine((value) => {
173
+ try {
174
+ new Intl.DateTimeFormat(value.locale, {
175
+ dateStyle: value.dateStyle as Intl.DateTimeFormatOptions['dateStyle'],
176
+ timeStyle: value.timeStyle as Intl.DateTimeFormatOptions['timeStyle'],
177
+ timeZone: value.timeZone,
178
+ });
179
+ return true;
180
+ } catch {
181
+ return false;
182
+ }
183
+ }, 'Use a valid Intl.DateTimeFormat configuration.');
184
+ const sitewideFooter = z.object({
185
+ copyrightMessage: z.string().min(1).optional(),
186
+ buildInfo: z.object({
187
+ enabled: z.boolean().default(true),
188
+ text: z.string().min(1),
189
+ dateTimeFormat,
190
+ }).strict().optional(),
173
191
  }).strict();
174
192
 
175
193
  const siteSchema = z.object({
176
194
  title: z.string(),
177
195
  description: z.string(),
178
196
  navigation: pageNavigation.optional(),
179
- presentation: pagePresentation.optional(),
180
- frame: frame.optional(),
181
197
  sections: z.record(z.string().regex(/^[a-z0-9-]+$/), sectionMetadata).optional().default({}),
182
198
  });
183
199
 
184
- const themeSchema = z.object({
185
- navigation: themeNavigation.optional(),
200
+ export const themeVisualSchema = z.object({
201
+ preset: themePreset.optional(),
186
202
  layout: themeLayout.optional(),
187
203
  gallery: themeGallery.optional(),
188
204
  typography: themeTypography.optional(),
189
205
  presentation: themePresentation.optional(),
190
- frame: frame.optional(),
206
+ }).strict();
207
+
208
+ const siteThemeSchema = themeVisualSchema;
209
+
210
+ const sitewideSchema = z.object({
211
+ navigation: themeNavigation.optional(),
212
+ banners,
213
+ footer: sitewideFooter.optional(),
191
214
  }).strict();
192
215
 
193
216
  const site = defineCollection({
@@ -212,9 +235,18 @@ const theme = defineCollection({
212
235
  loader: glob({
213
236
  pattern: 'theme.md',
214
237
  base: pathToFileURL(siteDir),
215
- generateId: () => `${siteEntryId.replace(/-content$/, '')}-theme`,
238
+ generateId: ({ entry }) => entry,
239
+ }),
240
+ schema: siteThemeSchema,
241
+ });
242
+
243
+ const sitewide = defineCollection({
244
+ loader: glob({
245
+ pattern: 'sitewide-content.md',
246
+ base: pathToFileURL(siteDir),
247
+ generateId: () => `${siteEntryId.replace(/-content$/, '')}-sitewide`,
216
248
  }),
217
- schema: themeSchema,
249
+ schema: sitewideSchema,
218
250
  });
219
251
 
220
- export const collections = { site, theme };
252
+ export const collections = { site, theme, sitewide };
@@ -1,5 +1,6 @@
1
1
  ---
2
2
  import projectConfig from '../../scripts/lib/project-config.mjs';
3
+ import { getPresentationCssVariables } from '../../scripts/lib/presentation.mjs';
3
4
  import { getIconLinks } from '../lib/sitePublicAssets';
4
5
  import { withBasePath } from '../lib/basePath';
5
6
  import '../styles/global.css';
@@ -9,18 +10,64 @@ type FrameColors = {
9
10
  textColor: string;
10
11
  };
11
12
 
13
+ type Presentation = {
14
+ palette: {
15
+ page: FrameColors;
16
+ css: Record<string, string>;
17
+ colorScheme: string;
18
+ };
19
+ };
20
+ type VisualTheme = {
21
+ layout: {
22
+ gutter: { desktop: string; mobile: string };
23
+ pageWidth: string;
24
+ spacing: {
25
+ firstSectionTop: { desktop: string; mobile: string };
26
+ blockGap: { desktop: string; mobile: string };
27
+ headingToBlock: { desktop: string; mobile: string };
28
+ imageGap: { desktop: string; mobile: string };
29
+ sectionGap: { desktop: string; mobile: string };
30
+ finalSectionBottom: { desktop: string; mobile: string };
31
+ };
32
+ };
33
+ gallery: {
34
+ width: string;
35
+ maxAvailableWidthPercent: { desktop: number; mobile: number };
36
+ maxAvailableHeightPercent: { desktop: number; mobile: number };
37
+ };
38
+ typography: { fontFamily: string };
39
+ };
40
+
12
41
  interface Props {
13
42
  title?: string;
14
43
  description?: string;
44
+ presentation: Presentation;
45
+ visualTheme: VisualTheme;
15
46
  frameColors: FrameColors;
16
47
  pathname?: string;
48
+ footer?: {
49
+ copyrightMessage?: string;
50
+ buildInfo?: {
51
+ enabled: boolean;
52
+ text: string;
53
+ dateTimeFormat: {
54
+ locale: string;
55
+ timeZone: string;
56
+ dateStyle: string;
57
+ timeStyle: string;
58
+ };
59
+ };
60
+ };
17
61
  }
18
62
 
19
63
  const {
20
64
  title = 'norna',
21
65
  description = 'A norna site.',
66
+ presentation,
67
+ visualTheme,
22
68
  frameColors,
23
69
  pathname = '/',
70
+ footer,
24
71
  } = Astro.props;
25
72
 
26
73
  const documentTitle = title;
@@ -29,30 +76,33 @@ const iconLinks = getIconLinks();
29
76
  const scrollBehavior = projectConfig.navigation.smoothScroll.enabled ? 'smooth' : 'auto';
30
77
  const formatPercent = (value: number) => `${value}%`;
31
78
  const formatViewportHeightPercent = (value: number) => `${value}svh`;
32
- const layoutSpacing = projectConfig.layout.spacing;
79
+ const layoutSpacing = visualTheme.layout.spacing;
33
80
  const documentStyle = [
34
81
  `scroll-behavior: ${scrollBehavior}`,
35
- `--font-sans: ${projectConfig.typography.fontFamily}`,
36
- `--page-width: ${projectConfig.layout.pageWidth}`,
37
- `--content-gutter-desktop: ${projectConfig.layout.gutter.desktop}`,
38
- `--content-gutter-mobile: ${projectConfig.layout.gutter.mobile}`,
82
+ ...Object.entries(getPresentationCssVariables(presentation)).map(([name, value]) => `${name}: ${value}`),
83
+ `--font-sans: ${visualTheme.typography.fontFamily}`,
84
+ `--page-width: ${visualTheme.layout.pageWidth}`,
85
+ `--content-gutter-desktop: ${visualTheme.layout.gutter.desktop}`,
86
+ `--content-gutter-mobile: ${visualTheme.layout.gutter.mobile}`,
39
87
  `--space-nav-to-first-section-desktop: ${layoutSpacing.firstSectionTop.desktop}`,
40
88
  `--space-nav-to-first-section-mobile: ${layoutSpacing.firstSectionTop.mobile}`,
41
- `--space-section-body-to-gallery-desktop: ${layoutSpacing.bodyToImages.desktop}`,
42
- `--space-section-body-to-gallery-mobile: ${layoutSpacing.bodyToImages.mobile}`,
89
+ `--space-content-block-gap-desktop: ${layoutSpacing.blockGap.desktop}`,
90
+ `--space-content-block-gap-mobile: ${layoutSpacing.blockGap.mobile}`,
91
+ `--space-heading-to-block-desktop: ${layoutSpacing.headingToBlock.desktop}`,
92
+ `--space-heading-to-block-mobile: ${layoutSpacing.headingToBlock.mobile}`,
43
93
  `--space-gallery-item-desktop: ${layoutSpacing.imageGap.desktop}`,
44
94
  `--space-gallery-item-mobile: ${layoutSpacing.imageGap.mobile}`,
45
95
  `--space-section-to-section-desktop: ${layoutSpacing.sectionGap.desktop}`,
46
96
  `--space-section-to-section-mobile: ${layoutSpacing.sectionGap.mobile}`,
47
97
  `--space-final-section-bottom-desktop: ${layoutSpacing.finalSectionBottom.desktop}`,
48
98
  `--space-final-section-bottom-mobile: ${layoutSpacing.finalSectionBottom.mobile}`,
49
- `--gallery-width: ${projectConfig.gallery.width}`,
50
- `--gallery-max-available-width-desktop: ${formatPercent(projectConfig.gallery.maxAvailableWidthPercent.desktop)}`,
51
- `--gallery-max-available-width-mobile: ${formatPercent(projectConfig.gallery.maxAvailableWidthPercent.mobile)}`,
52
- `--gallery-max-image-height-desktop: ${formatViewportHeightPercent(projectConfig.gallery.maxAvailableHeightPercent.desktop)}`,
53
- `--gallery-max-image-height-mobile: ${formatViewportHeightPercent(projectConfig.gallery.maxAvailableHeightPercent.mobile)}`,
99
+ `--gallery-width: ${visualTheme.gallery.width}`,
100
+ `--gallery-max-available-width-desktop: ${formatPercent(visualTheme.gallery.maxAvailableWidthPercent.desktop)}`,
101
+ `--gallery-max-available-width-mobile: ${formatPercent(visualTheme.gallery.maxAvailableWidthPercent.mobile)}`,
102
+ `--gallery-max-image-height-desktop: ${formatViewportHeightPercent(visualTheme.gallery.maxAvailableHeightPercent.desktop)}`,
103
+ `--gallery-max-image-height-mobile: ${formatViewportHeightPercent(visualTheme.gallery.maxAvailableHeightPercent.mobile)}`,
54
104
  ].join('; ');
55
- const { buildInfo, copyrightMessage } = projectConfig.footer;
105
+ const { buildInfo, copyrightMessage } = footer ?? {};
56
106
  const showBuildInfo = Boolean(buildInfo?.enabled);
57
107
  const buildTime = showBuildInfo
58
108
  ? new Intl.DateTimeFormat(buildInfo.dateTimeFormat.locale, {
@@ -75,7 +125,7 @@ const footerStyle = [
75
125
  <meta name="viewport" content="width=device-width, initial-scale=1" />
76
126
  <meta name="format-detection" content="telephone=no" />
77
127
  <meta name="description" content={description} />
78
- <meta name="theme-color" content="#000000" />
128
+ <meta name="theme-color" content={presentation.palette.page.backgroundColor} />
79
129
  <link rel="canonical" href={canonicalUrl} />
80
130
  <meta name="generator" content={Astro.generator} />
81
131
  {iconLinks.map((iconLink) => (