@janga/norna 0.7.8 → 0.7.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (234) hide show
  1. package/README.md +1 -1
  2. package/docs/README.md +18 -11
  3. package/docs/configuration.md +9 -54
  4. package/docs/content.md +6 -7
  5. package/docs/design/norna-diagram-design.md +158 -0
  6. package/docs/getting-started.md +18 -16
  7. package/docs/routes.md +7 -6
  8. package/docs/site-structure.md +4 -2
  9. package/docs/sitewide-content.md +74 -0
  10. package/docs/theme.md +61 -128
  11. package/docs/typography.md +45 -33
  12. package/examples/001-dog-shelter-single-page/README.md +9 -0
  13. package/examples/001-dog-shelter-single-page/site/config.mjs +27 -0
  14. package/examples/001-dog-shelter-single-page/site/content.md +38 -0
  15. package/examples/001-dog-shelter-single-page/site/images/what-we-do/dog-house.svg +25 -0
  16. package/examples/001-dog-shelter-single-page/site/images/you-can-help/heart.svg +13 -0
  17. package/examples/001-dog-shelter-single-page/site/public/favicon.svg +10 -0
  18. package/examples/001-dog-shelter-single-page/site/public/robots.txt +2 -0
  19. package/examples/001-dog-shelter-single-page/site/sitewide-content.md +6 -0
  20. package/examples/001-dog-shelter-single-page/site/theme.md +25 -0
  21. package/examples/002-dog-shelter-multi-page/README.md +9 -0
  22. package/examples/002-dog-shelter-multi-page/site/config.mjs +27 -0
  23. package/examples/002-dog-shelter-multi-page/site/content.md +12 -0
  24. package/examples/002-dog-shelter-multi-page/site/public/favicon.svg +10 -0
  25. package/examples/002-dog-shelter-multi-page/site/public/robots.txt +2 -0
  26. package/examples/002-dog-shelter-multi-page/site/routes/010-dogs/images/dogs/rex.svg +19 -0
  27. package/examples/002-dog-shelter-multi-page/site/routes/010-dogs/images/dogs/rover.svg +19 -0
  28. package/examples/002-dog-shelter-multi-page/site/routes/010-dogs/route-content.md +30 -0
  29. package/examples/002-dog-shelter-multi-page/site/routes/020-adopt/route-content.md +12 -0
  30. package/examples/002-dog-shelter-multi-page/site/sitewide-content.md +6 -0
  31. package/examples/002-dog-shelter-multi-page/site/theme.md +25 -0
  32. package/examples/dog-gallery/.astro/collections/site.schema.json +776 -0
  33. package/examples/dog-gallery/.astro/collections/theme.schema.json +672 -0
  34. package/examples/dog-gallery/.astro/content-assets.mjs +4 -0
  35. package/examples/dog-gallery/.astro/content-modules.mjs +1 -0
  36. package/examples/dog-gallery/.astro/content.d.ts +175 -0
  37. package/examples/dog-gallery/.astro/data-store.json +1 -0
  38. package/examples/dog-gallery/.astro/dev-local.json +8 -0
  39. package/examples/dog-gallery/.astro/dev.json +13 -0
  40. package/examples/dog-gallery/.astro/dev.log +95 -0
  41. package/examples/dog-gallery/.astro/settings.json +5 -0
  42. package/examples/dog-gallery/.astro/types.d.ts +2 -0
  43. package/examples/dog-gallery/dist/_astro/GalleryGrid.astro_astro_type_script_index_0_lang.B0kxDyGz.js +1 -0
  44. package/examples/dog-gallery/dist/_astro/SitePage.DicGh_5N.css +1 -0
  45. package/examples/dog-gallery/dist/dog-care/index.html +15 -0
  46. package/examples/dog-gallery/dist/dog-shelter-logo.svg +14 -0
  47. package/examples/dog-gallery/dist/favicon.svg +7 -0
  48. package/examples/dog-gallery/dist/images/generated/black-dogs/black-puppy-meadow-7255e3d9-1080.webp +0 -0
  49. package/examples/dog-gallery/dist/images/generated/black-dogs/black-puppy-meadow-7255e3d9-1440.webp +0 -0
  50. package/examples/dog-gallery/dist/images/generated/black-dogs/black-puppy-meadow-7255e3d9-1536.webp +0 -0
  51. package/examples/dog-gallery/dist/images/generated/black-dogs/black-puppy-meadow-7255e3d9-480.webp +0 -0
  52. package/examples/dog-gallery/dist/images/generated/black-dogs/black-puppy-meadow-7255e3d9-768.webp +0 -0
  53. package/examples/dog-gallery/dist/images/generated/black-dogs/photo-of-a-black-dog-5cf614d2-480.webp +0 -0
  54. package/examples/dog-gallery/dist/images/generated/black-dogs/photo-of-a-black-dog-5cf614d2-768.webp +0 -0
  55. package/examples/dog-gallery/dist/images/generated/black-dogs/photo-of-a-black-dog-5cf614d2-960.webp +0 -0
  56. package/examples/dog-gallery/dist/images/generated/brown-dogs/brown-dog-50ef114e-1080.webp +0 -0
  57. package/examples/dog-gallery/dist/images/generated/brown-dogs/brown-dog-50ef114e-1440.webp +0 -0
  58. package/examples/dog-gallery/dist/images/generated/brown-dogs/brown-dog-50ef114e-1920.webp +0 -0
  59. package/examples/dog-gallery/dist/images/generated/brown-dogs/brown-dog-50ef114e-3939.webp +0 -0
  60. package/examples/dog-gallery/dist/images/generated/brown-dogs/brown-dog-50ef114e-480.webp +0 -0
  61. package/examples/dog-gallery/dist/images/generated/brown-dogs/brown-dog-50ef114e-768.webp +0 -0
  62. package/examples/dog-gallery/dist/images/generated/brown-dogs/dog-accompanies-master-7ef804d5-1080.webp +0 -0
  63. package/examples/dog-gallery/dist/images/generated/brown-dogs/dog-accompanies-master-7ef804d5-1440.webp +0 -0
  64. package/examples/dog-gallery/dist/images/generated/brown-dogs/dog-accompanies-master-7ef804d5-1920.webp +0 -0
  65. package/examples/dog-gallery/dist/images/generated/brown-dogs/dog-accompanies-master-7ef804d5-2400.webp +0 -0
  66. package/examples/dog-gallery/dist/images/generated/brown-dogs/dog-accompanies-master-7ef804d5-480.webp +0 -0
  67. package/examples/dog-gallery/dist/images/generated/brown-dogs/dog-accompanies-master-7ef804d5-768.webp +0 -0
  68. package/examples/dog-gallery/dist/images/generated/golden-dogs/golden-retriever-f797fd88-1080.webp +0 -0
  69. package/examples/dog-gallery/dist/images/generated/golden-dogs/golden-retriever-f797fd88-1440.webp +0 -0
  70. package/examples/dog-gallery/dist/images/generated/golden-dogs/golden-retriever-f797fd88-1600.webp +0 -0
  71. package/examples/dog-gallery/dist/images/generated/golden-dogs/golden-retriever-f797fd88-480.webp +0 -0
  72. package/examples/dog-gallery/dist/images/generated/golden-dogs/golden-retriever-f797fd88-768.webp +0 -0
  73. package/examples/dog-gallery/dist/images/generated/golden-dogs/toller-puppy-a2a8fbc0-1080.webp +0 -0
  74. package/examples/dog-gallery/dist/images/generated/golden-dogs/toller-puppy-a2a8fbc0-1440.webp +0 -0
  75. package/examples/dog-gallery/dist/images/generated/golden-dogs/toller-puppy-a2a8fbc0-1920.webp +0 -0
  76. package/examples/dog-gallery/dist/images/generated/golden-dogs/toller-puppy-a2a8fbc0-2576.webp +0 -0
  77. package/examples/dog-gallery/dist/images/generated/golden-dogs/toller-puppy-a2a8fbc0-480.webp +0 -0
  78. package/examples/dog-gallery/dist/images/generated/golden-dogs/toller-puppy-a2a8fbc0-768.webp +0 -0
  79. package/examples/dog-gallery/dist/images/generated/white-dogs/white-cute-dog-077a0689-1080.webp +0 -0
  80. package/examples/dog-gallery/dist/images/generated/white-dogs/white-cute-dog-077a0689-1277.webp +0 -0
  81. package/examples/dog-gallery/dist/images/generated/white-dogs/white-cute-dog-077a0689-480.webp +0 -0
  82. package/examples/dog-gallery/dist/images/generated/white-dogs/white-cute-dog-077a0689-768.webp +0 -0
  83. package/examples/dog-gallery/dist/images/generated/white-dogs/white-puppy-garden-d3ab2ca0-1080.webp +0 -0
  84. package/examples/dog-gallery/dist/images/generated/white-dogs/white-puppy-garden-d3ab2ca0-1440.webp +0 -0
  85. package/examples/dog-gallery/dist/images/generated/white-dogs/white-puppy-garden-d3ab2ca0-1536.webp +0 -0
  86. package/examples/dog-gallery/dist/images/generated/white-dogs/white-puppy-garden-d3ab2ca0-480.webp +0 -0
  87. package/examples/dog-gallery/dist/images/generated/white-dogs/white-puppy-garden-d3ab2ca0-768.webp +0 -0
  88. package/examples/dog-gallery/dist/index.html +45 -0
  89. package/examples/dog-gallery/dist/robots.txt +2 -0
  90. package/examples/dog-gallery/node_modules/.vite/deps/_metadata.json +7 -7
  91. package/examples/dog-gallery/site/.norna/generated-images.json +8 -8
  92. package/examples/dog-gallery/site/config.mjs +0 -20
  93. package/examples/dog-gallery/site/content.md +4 -5
  94. package/examples/dog-gallery/site/public/logo.svg +14 -0
  95. package/examples/dog-gallery/site/sitewide-content.md +16 -0
  96. package/examples/dog-gallery/site/theme.md +13 -24
  97. package/examples/presentation/README.md +15 -0
  98. package/examples/presentation/site/.norna/generated-images.json +66 -0
  99. package/examples/presentation/site/config.mjs +29 -0
  100. package/examples/presentation/site/content.md +67 -0
  101. package/examples/presentation/site/public/robots.txt +2 -0
  102. package/examples/presentation/site/routes/010-presets/route-content.md +63 -0
  103. package/examples/presentation/site/routes/010-presets/theme.md +4 -0
  104. package/examples/presentation/site/routes/020-media/images/cards/card-carousel.svg +11 -0
  105. package/examples/presentation/site/routes/020-media/images/cards/card-stack.svg +9 -0
  106. package/examples/presentation/site/routes/020-media/images/cards/card-surfaces.svg +8 -0
  107. package/examples/presentation/site/routes/020-media/images/carousel/carousel-one.svg +8 -0
  108. package/examples/presentation/site/routes/020-media/images/carousel/carousel-three.svg +13 -0
  109. package/examples/presentation/site/routes/020-media/images/carousel/carousel-two.svg +8 -0
  110. package/examples/presentation/site/routes/020-media/images/stack/stack-one.svg +8 -0
  111. package/examples/presentation/site/routes/020-media/images/stack/stack-two.svg +9 -0
  112. package/examples/presentation/site/routes/020-media/route-content.md +77 -0
  113. package/examples/presentation/site/routes/020-media/theme.md +4 -0
  114. package/examples/presentation/site/routes/030-surfaces/route-content.md +44 -0
  115. package/examples/presentation/site/routes/030-surfaces/theme.md +7 -0
  116. package/examples/presentation/site/sitewide-content.md +6 -0
  117. package/examples/presentation/site/theme.md +43 -0
  118. package/examples/typography-presets/.astro/collections/routeTheme.schema.json +746 -0
  119. package/examples/typography-presets/.astro/collections/site.schema.json +59 -0
  120. package/examples/typography-presets/.astro/collections/sitewide.schema.json +114 -0
  121. package/examples/typography-presets/.astro/collections/theme.schema.json +770 -0
  122. package/examples/typography-presets/.astro/content-assets.mjs +4 -0
  123. package/examples/typography-presets/.astro/content-modules.mjs +1 -0
  124. package/examples/typography-presets/.astro/content.d.ts +183 -0
  125. package/examples/typography-presets/.astro/data-store.json +1 -0
  126. package/examples/typography-presets/.astro/dev-local.json +8 -0
  127. package/examples/typography-presets/.astro/dev.json +15 -0
  128. package/examples/typography-presets/.astro/dev.log +45 -0
  129. package/examples/typography-presets/.astro/settings.json +5 -0
  130. package/examples/typography-presets/.astro/types.d.ts +2 -0
  131. package/examples/typography-presets/node_modules/.vite/deps/_metadata.json +80 -0
  132. package/examples/typography-presets/node_modules/.vite/deps/astro-D_GIpJIE.js +434 -0
  133. package/examples/typography-presets/node_modules/.vite/deps/astro-D_GIpJIE.js.map +1 -0
  134. package/examples/typography-presets/node_modules/.vite/deps/astro_n_aria-query.js +6390 -0
  135. package/examples/typography-presets/node_modules/.vite/deps/astro_n_aria-query.js.map +1 -0
  136. package/examples/typography-presets/node_modules/.vite/deps/astro_n_axobject-query.js +2702 -0
  137. package/examples/typography-presets/node_modules/.vite/deps/astro_n_axobject-query.js.map +1 -0
  138. package/examples/typography-presets/node_modules/.vite/deps/astro_n_html-escaper.js +65 -0
  139. package/examples/typography-presets/node_modules/.vite/deps/astro_n_html-escaper.js.map +1 -0
  140. package/examples/typography-presets/node_modules/.vite/deps/astro_runtime_client_dev-toolbar_entrypoint__js.js +455 -0
  141. package/examples/typography-presets/node_modules/.vite/deps/astro_runtime_client_dev-toolbar_entrypoint__js.js.map +1 -0
  142. package/examples/typography-presets/node_modules/.vite/deps/audit-DM9Hkl5B.js +1534 -0
  143. package/examples/typography-presets/node_modules/.vite/deps/audit-DM9Hkl5B.js.map +1 -0
  144. package/examples/typography-presets/node_modules/.vite/deps/embla-carousel.js +1389 -0
  145. package/examples/typography-presets/node_modules/.vite/deps/embla-carousel.js.map +1 -0
  146. package/examples/typography-presets/node_modules/.vite/deps/highlight-DnZ1ULOL.js +48 -0
  147. package/examples/typography-presets/node_modules/.vite/deps/highlight-DnZ1ULOL.js.map +1 -0
  148. package/examples/typography-presets/node_modules/.vite/deps/icons-RyebKi45.js +42 -0
  149. package/examples/typography-presets/node_modules/.vite/deps/icons-RyebKi45.js.map +1 -0
  150. package/examples/typography-presets/node_modules/.vite/deps/package.json +3 -0
  151. package/examples/typography-presets/node_modules/.vite/deps/rolldown-runtime-BvCyGRYZ.js +4 -0
  152. package/examples/typography-presets/node_modules/.vite/deps/settings-DaU-gNnK.js +190 -0
  153. package/examples/typography-presets/node_modules/.vite/deps/settings-DaU-gNnK.js.map +1 -0
  154. package/examples/typography-presets/node_modules/.vite/deps/toolbar-63PA619H.js +458 -0
  155. package/examples/typography-presets/node_modules/.vite/deps/toolbar-63PA619H.js.map +1 -0
  156. package/examples/typography-presets/node_modules/.vite/deps/ui-library--bkcNSYz.js +1016 -0
  157. package/examples/typography-presets/node_modules/.vite/deps/ui-library--bkcNSYz.js.map +1 -0
  158. package/examples/typography-presets/node_modules/.vite/deps/window-jPkpKVcy.js +32 -0
  159. package/examples/typography-presets/node_modules/.vite/deps/window-jPkpKVcy.js.map +1 -0
  160. package/examples/typography-presets/node_modules/.vite/deps/xray-BXCHpEJP.js +111 -0
  161. package/examples/typography-presets/node_modules/.vite/deps/xray-BXCHpEJP.js.map +1 -0
  162. package/examples/typography-presets/site/.norna/generated-images.json +290 -0
  163. package/examples/typography-presets/site/config.mjs +29 -0
  164. package/examples/typography-presets/site/content.md +16 -0
  165. package/examples/typography-presets/site/public/robots.txt +2 -0
  166. package/examples/typography-presets/site/routes/010-quiet-gallery/images/quiet-gallery/puppy-portrait.jpg +0 -0
  167. package/examples/typography-presets/site/routes/010-quiet-gallery/images/quiet-gallery/shepherd-portrait.jpg +0 -0
  168. package/examples/typography-presets/site/routes/010-quiet-gallery/images/quiet-gallery/white-puppy.jpg +0 -0
  169. package/examples/typography-presets/site/routes/010-quiet-gallery/route-content.md +34 -0
  170. package/examples/typography-presets/site/routes/010-quiet-gallery/theme.md +4 -0
  171. package/examples/typography-presets/site/routes/020-compact-gallery/images/compact-gallery/black-schnauzer.jpg +0 -0
  172. package/examples/typography-presets/site/routes/020-compact-gallery/images/compact-gallery/retriever-portrait.jpg +0 -0
  173. package/examples/typography-presets/site/routes/020-compact-gallery/images/compact-gallery/schnauzer-portrait.jpg +0 -0
  174. package/examples/typography-presets/site/routes/020-compact-gallery/route-content.md +33 -0
  175. package/examples/typography-presets/site/routes/020-compact-gallery/theme.md +4 -0
  176. package/examples/typography-presets/site/routes/030-text-forward/images/text-forward/boxer-portrait.jpg +0 -0
  177. package/examples/typography-presets/site/routes/030-text-forward/images/text-forward/grass-puppy.jpg +0 -0
  178. package/examples/typography-presets/site/routes/030-text-forward/images/text-forward/grey-street-dog.jpg +0 -0
  179. package/examples/typography-presets/site/routes/030-text-forward/route-content.md +34 -0
  180. package/examples/typography-presets/site/routes/030-text-forward/theme.md +4 -0
  181. package/examples/typography-presets/site/routes/040-statement/images/statement/black-pedigree.jpg +0 -0
  182. package/examples/typography-presets/site/routes/040-statement/images/statement/sheepdog-grass.jpg +0 -0
  183. package/examples/typography-presets/site/routes/040-statement/images/statement/sheepdog-wide.jpg +0 -0
  184. package/examples/typography-presets/site/routes/040-statement/route-content.md +34 -0
  185. package/examples/typography-presets/site/routes/040-statement/theme.md +4 -0
  186. package/examples/typography-presets/site/sitewide-content.md +14 -0
  187. package/examples/typography-presets/site/theme.md +43 -0
  188. package/fixtures/basic/site/config.mjs +0 -13
  189. package/fixtures/basic/site/sitewide-content.md +12 -0
  190. package/fixtures/basic/site/theme.md +2 -2
  191. package/fixtures/content-model-v2/site/content.md +2 -5
  192. package/fixtures/content-model-v2/site/theme.md +1 -3
  193. package/package.json +3 -2
  194. package/scripts/check-config.mjs +22 -1
  195. package/scripts/lib/frontmatter-yaml.mjs +28 -0
  196. package/scripts/lib/logo-assets.mjs +35 -0
  197. package/scripts/lib/norna-markdown-blocks.mjs +166 -0
  198. package/scripts/lib/presentation.mjs +104 -46
  199. package/scripts/lib/project-config.mjs +67 -105
  200. package/scripts/lib/site-content.mjs +26 -42
  201. package/scripts/lib/site-paths.mjs +2 -0
  202. package/scripts/lib/sitewide-config.mjs +34 -0
  203. package/scripts/lib/theme-config.mjs +72 -6
  204. package/scripts/lib/typography.mjs +20 -26
  205. package/scripts/show-typography.mjs +40 -147
  206. package/scripts/sync-content-sections.mjs +19 -14
  207. package/scripts/test-content-check.mjs +76 -14
  208. package/scripts/test-content-model-v2.mjs +154 -0
  209. package/scripts/test-engine-commands.mjs +3 -1
  210. package/scripts/test-package-check.mjs +78 -19
  211. package/scripts/test-temporary-visibility.mjs +19 -2
  212. package/src/components/CardList.astro +5 -2
  213. package/src/components/GalleryGrid.astro +37 -16
  214. package/src/components/SiteBanners.astro +100 -0
  215. package/src/components/SiteNavigation.astro +48 -31
  216. package/src/components/SitePage.astro +55 -13
  217. package/src/components/SiteSection.astro +194 -36
  218. package/src/content.config.ts +78 -48
  219. package/src/layouts/BaseLayout.astro +64 -14
  220. package/src/lib/routeThemes.ts +61 -0
  221. package/src/lib/sectionContent.ts +236 -44
  222. package/src/lib/sitePublicAssets.ts +1 -0
  223. package/src/styles/global.css +282 -93
  224. package/starters/basic/.github/workflows/deploy.yml +1 -1
  225. package/starters/basic/README.md +7 -6
  226. package/starters/basic/site/config.mjs +0 -13
  227. package/starters/basic/site/content.md +1 -1
  228. package/starters/basic/site/sitewide-content.md +14 -0
  229. package/starters/basic/site/theme.md +13 -24
  230. package/starters/project/.github/workflows/deploy.yml +1 -1
  231. package/starters/project/README.md +10 -8
  232. package/starters/project/site/config.mjs +0 -13
  233. package/starters/project/site/sitewide-content.md +14 -0
  234. package/starters/project/site/theme.md +3 -11
@@ -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,43 +301,150 @@ 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
  >
257
314
  <div class="section-card">
258
315
  <header class="section-header">
259
- <HeadingTag>{section.title}</HeadingTag>
316
+ <HeadingTag set:html={section.titleHtml} />
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,14 @@ 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 presentationPalette = z.enum(['dark', 'light', 'paper']);
19
+ const sectionSurfaceMode = z.enum(['none', 'cycle']);
20
+ const sectionSurface = z.enum(['base', 'soft', 'emphasis']);
20
21
  const spacingDensity = z.enum(['compact', 'normal', 'airy']);
21
22
  const lineHeight = z.number()
22
23
  .min(1, 'Use a unitless line height of at least 1.')
@@ -31,7 +32,6 @@ const visualCssLength = z.string().regex(
31
32
  new RegExp(`^(?:0|${visualCssLengthValue}|clamp\\(\\s*${visualCssLengthValue}\\s*,\\s*${visualCssLengthValue}\\s*,\\s*${visualCssLengthValue}\\s*\\))$`),
32
33
  'Use a CSS length such as "0", "900px", "56rem", "90%", or "clamp(1rem, 4vw, 3rem)".',
33
34
  );
34
- const inlineStyleName = z.string().regex(/^[a-z][a-z0-9-]*$/);
35
35
  const dateOnly = z.string()
36
36
  .regex(/^\d{4}-\d{2}-\d{2}$/, 'Use YYYY-MM-DD format.')
37
37
  .refine(isDateOnly, 'Use a real calendar date.');
@@ -59,6 +59,7 @@ const commonTextPresentationOverride = {
59
59
  };
60
60
  const headingPresentationOverride = z.object({
61
61
  ...commonTextPresentationOverride,
62
+ weight: headingWeight.optional(),
62
63
  spacingBefore: cssLength.optional(),
63
64
  spacingAfter: cssLength.optional(),
64
65
  }).strict();
@@ -70,6 +71,7 @@ const headingLevelsPresentationOverride = z.object({
70
71
  }).strict();
71
72
  const bodyPresentationOverride = z.object({
72
73
  ...commonTextPresentationOverride,
74
+ width: textWidth.optional(),
73
75
  paragraphSpacing: cssLength.optional(),
74
76
  }).strict();
75
77
  const captionPresentationOverride = z.object({
@@ -81,14 +83,6 @@ const typographyOverrides = z.object({
81
83
  body: bodyPresentationOverride.optional(),
82
84
  caption: captionPresentationOverride.optional(),
83
85
  }).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
86
  const themeTypography = z.object({
93
87
  fontFamily: z.string()
94
88
  .min(1)
@@ -115,9 +109,10 @@ const responsivePercent = z.union([
115
109
  }).strict(),
116
110
  ]);
117
111
  const themeLayoutSpacing = z.object({
118
- bodyToImages: responsiveCssLength.optional(),
112
+ blockGap: responsiveCssLength.optional(),
119
113
  finalSectionBottom: responsiveCssLength.optional(),
120
114
  firstSectionTop: responsiveCssLength.optional(),
115
+ headingToBlock: responsiveCssLength.optional(),
121
116
  imageGap: responsiveCssLength.optional(),
122
117
  sectionGap: responsiveCssLength.optional(),
123
118
  }).strict();
@@ -132,32 +127,15 @@ const themeGallery = z.object({
132
127
  maxAvailableWidthPercent: responsivePercent.optional(),
133
128
  maxAvailableHeightPercent: responsivePercent.optional(),
134
129
  }).strict();
135
- const sectionPresentationOverride = z.object({
136
- backgroundColor: colorValue.optional(),
137
- textColor: colorValue.optional(),
138
- typography: typography.optional(),
139
- }).strict();
140
130
  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(),
131
+ palette: presentationPalette.optional(),
132
+ sectionSurfaces: z.object({
133
+ mode: sectionSurfaceMode,
134
+ sequence: z.array(sectionSurface).min(1).max(3).optional(),
135
+ }).strict().refine(
136
+ (value) => !value.sequence || new Set(value.sequence).size === value.sequence.length,
137
+ 'Each section surface may appear only once in a sequence.',
138
+ ).optional(),
161
139
  }).strict();
162
140
  const pageNavigation = z.object({
163
141
  include: z.boolean().optional(),
@@ -165,29 +143,72 @@ const pageNavigation = z.object({
165
143
  }).strict();
166
144
  const themeNavigation = z.object({
167
145
  brand: z.string().min(1).optional(),
146
+ logo: z.object({
147
+ alt: z.string().min(1).optional(),
148
+ height: visualCssLength.optional(),
149
+ }).strict().optional(),
168
150
  }).strict();
169
151
 
170
152
  const sectionMetadata = z.object({
171
153
  visible: visibilityWindow.optional(),
172
- presentation: sectionPresentationOverride.optional(),
154
+ }).strict();
155
+ const banner = z.object({
156
+ id: z.string().regex(/^[a-z0-9-]+$/),
157
+ tone: z.enum(['warning']).default('warning'),
158
+ visible: visibilityWindow.optional(),
159
+ title: z.string().min(1),
160
+ text: z.string().min(1),
161
+ }).strict();
162
+ const banners = z.array(banner).optional().default([]).refine(
163
+ (values) => new Set(values.map((value) => value.id)).size === values.length,
164
+ 'Banner ids must be unique.',
165
+ );
166
+ const dateTimeFormat = z.object({
167
+ locale: z.string().min(1),
168
+ timeZone: z.string().min(1),
169
+ dateStyle: z.string().min(1),
170
+ timeStyle: z.string().min(1),
171
+ }).strict().refine((value) => {
172
+ try {
173
+ new Intl.DateTimeFormat(value.locale, {
174
+ dateStyle: value.dateStyle as Intl.DateTimeFormatOptions['dateStyle'],
175
+ timeStyle: value.timeStyle as Intl.DateTimeFormatOptions['timeStyle'],
176
+ timeZone: value.timeZone,
177
+ });
178
+ return true;
179
+ } catch {
180
+ return false;
181
+ }
182
+ }, 'Use a valid Intl.DateTimeFormat configuration.');
183
+ const sitewideFooter = z.object({
184
+ copyrightMessage: z.string().min(1).optional(),
185
+ buildInfo: z.object({
186
+ enabled: z.boolean().default(true),
187
+ text: z.string().min(1),
188
+ dateTimeFormat,
189
+ }).strict().optional(),
173
190
  }).strict();
174
191
 
175
192
  const siteSchema = z.object({
176
193
  title: z.string(),
177
194
  description: z.string(),
178
195
  navigation: pageNavigation.optional(),
179
- presentation: pagePresentation.optional(),
180
- frame: frame.optional(),
181
196
  sections: z.record(z.string().regex(/^[a-z0-9-]+$/), sectionMetadata).optional().default({}),
182
197
  });
183
198
 
184
- const themeSchema = z.object({
185
- navigation: themeNavigation.optional(),
199
+ export const themeVisualSchema = z.object({
186
200
  layout: themeLayout.optional(),
187
201
  gallery: themeGallery.optional(),
188
202
  typography: themeTypography.optional(),
189
203
  presentation: themePresentation.optional(),
190
- frame: frame.optional(),
204
+ }).strict();
205
+
206
+ const siteThemeSchema = themeVisualSchema;
207
+
208
+ const sitewideSchema = z.object({
209
+ navigation: themeNavigation.optional(),
210
+ banners,
211
+ footer: sitewideFooter.optional(),
191
212
  }).strict();
192
213
 
193
214
  const site = defineCollection({
@@ -212,9 +233,18 @@ const theme = defineCollection({
212
233
  loader: glob({
213
234
  pattern: 'theme.md',
214
235
  base: pathToFileURL(siteDir),
215
- generateId: () => `${siteEntryId.replace(/-content$/, '')}-theme`,
236
+ generateId: ({ entry }) => entry,
237
+ }),
238
+ schema: siteThemeSchema,
239
+ });
240
+
241
+ const sitewide = defineCollection({
242
+ loader: glob({
243
+ pattern: 'sitewide-content.md',
244
+ base: pathToFileURL(siteDir),
245
+ generateId: () => `${siteEntryId.replace(/-content$/, '')}-sitewide`,
216
246
  }),
217
- schema: themeSchema,
247
+ schema: sitewideSchema,
218
248
  });
219
249
 
220
- export const collections = { site, theme };
250
+ 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) => (