@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
@@ -45,8 +45,8 @@ const makePngHeader = ({ width, height }) => {
45
45
  const defaultTheme = `---
46
46
  typography:
47
47
  preset: quiet-gallery
48
- frame:
49
- colors: presentation
48
+ presentation:
49
+ palette: dark
50
50
  ---
51
51
  `;
52
52
 
@@ -173,14 +173,6 @@ test('content:check warns when carousel images use different aspect ratios', asy
173
173
  });
174
174
  });
175
175
 
176
- const inlineStyleTheme = `---
177
- presentation:
178
- inlineStyles:
179
- highlight:
180
- color: "#ffd84d"
181
- ---
182
- `;
183
-
184
176
  const inlineStyleSite = `---
185
177
  title: Inline Style
186
178
  ---
@@ -188,10 +180,9 @@ title: Inline Style
188
180
  This has [known text]{.highlight} and [unknown text]{.missing}.
189
181
  `;
190
182
 
191
- test('content:check fails when Markdown uses undefined inline styles', async () => {
183
+ test('content:check fails when Markdown uses removed inline color styles', async () => {
192
184
  await withTempProject({
193
185
  site: inlineStyleSite,
194
- theme: inlineStyleTheme,
195
186
  files: [],
196
187
  }, async (root) => {
197
188
  const result = runContentScript(root, ['--check']);
@@ -199,8 +190,79 @@ test('content:check fails when Markdown uses undefined inline styles', async ()
199
190
 
200
191
  assert.equal(result.status, 1, output);
201
192
  assert.match(output, /^Content check failed\./m);
202
- assert.match(output, /Inline style "\.missing" is used in site\/content\.md but is not defined in site\/theme\.md presentation\.inlineStyles\./);
203
- assert.doesNotMatch(output, /"\.highlight" is used in Markdown/);
193
+ assert.match(output, /Inline color style "\.highlight" is no longer supported in site\/content\.md\./);
194
+ assert.match(output, /Inline color style "\.missing" is no longer supported in site\/content\.md\./);
195
+ });
196
+ });
197
+
198
+ test('content:check suggests semantic Markdown instead of inline color styles', async () => {
199
+ await withTempProject({
200
+ site: `---
201
+ title: Invalid Inline Color
202
+ ---
203
+ ## Intro {#intro}
204
+ This has [yellow text]{.yellow}.
205
+ `,
206
+ files: [],
207
+ }, async (root) => {
208
+ const result = runContentScript(root, ['--check']);
209
+ const output = getOutput(result);
210
+
211
+ assert.equal(result.status, 1, output);
212
+ assert.match(output, /^Content check failed\./m);
213
+ assert.match(output, /Use standard Markdown emphasis, a blockquote, or a semantic Norna block instead of color syntax\./);
214
+ });
215
+ });
216
+
217
+ const inlineNoteSite = `---
218
+ title: Inline notes
219
+ description: Fixture
220
+ ---
221
+ ## Intro {#intro}
222
+ This paragraph has a note reference.{note-ref}
223
+
224
+ {note: Additional context.}
225
+
226
+ \`\`\`text
227
+ {note-ref}
228
+ {note: This is code, not a note.
229
+ \`\`\`
230
+ `;
231
+
232
+ test('content:check accepts valid inline notes and ignores note syntax in code fences', async () => {
233
+ await withTempProject({ site: inlineNoteSite, files: [] }, async (root) => {
234
+ const result = runContentScript(root, ['--check']);
235
+ const output = getOutput(result);
236
+
237
+ assert.equal(result.status, 0, output);
238
+ assert.match(output, /Content check passed\./);
239
+ });
240
+ });
241
+
242
+ test('content:check rejects unpaired and repeated inline notes', async () => {
243
+ await withTempProject({
244
+ site: `---
245
+ title: Invalid inline notes
246
+ description: Fixture
247
+ ---
248
+ ## Orphan {#orphan}
249
+ {note: This note has no reference.}
250
+
251
+ ## Missing {#missing}
252
+ This reference has no note.{note-ref}
253
+
254
+ ## Repeated {#repeated}
255
+ This paragraph has {note-ref} two references {note-ref}.
256
+ `,
257
+ files: [],
258
+ }, async (root) => {
259
+ const result = runContentScript(root, ['--check']);
260
+ const output = getOutput(result);
261
+
262
+ assert.equal(result.status, 1, output);
263
+ assert.match(output, /A "\{note: \.\.\.\}" requires a preceding paragraph containing "\{note-ref\}"\./);
264
+ assert.match(output, /contains "\{note-ref\}" but has no following "\{note: \.\.\.\}"/);
265
+ assert.match(output, /A paragraph may contain only one "\{note-ref\}"\./);
204
266
  });
205
267
  });
206
268
 
@@ -129,6 +129,99 @@ test('content model v2 fixture checks and builds', async () => {
129
129
  }
130
130
  });
131
131
 
132
+ test('route theme replaces visual theme while site identity stays at the root', async () => {
133
+ const { root, siteDir } = await createTempSite({ underRepoCache: true });
134
+ try {
135
+ await writeFile(path.join(siteDir, 'theme.md'), `---
136
+ typography:
137
+ preset: text-forward
138
+ ---
139
+ `);
140
+ await writeFile(path.join(siteDir, 'sitewide-content.md'), `---
141
+ navigation:
142
+ brand: Root Brand
143
+ ---
144
+ `);
145
+ await mkdir(path.join(siteDir, 'routes', '010-guide'), { recursive: true });
146
+ await writeFile(path.join(siteDir, 'content.md'), `---
147
+ title: Root
148
+ description: Root page
149
+ ---
150
+
151
+ ## Home {#home}
152
+
153
+ Root content.
154
+ `);
155
+ await writeFile(path.join(siteDir, 'routes', '010-guide', 'route-content.md'), `---
156
+ title: Guide
157
+ description: Guide page
158
+ navigation:
159
+ label: Guide
160
+ ---
161
+
162
+ ## Guide {#guide}
163
+
164
+ Route content.
165
+ `);
166
+ await writeFile(path.join(siteDir, 'routes', '010-guide', 'theme.md'), `---
167
+ typography:
168
+ preset: statement
169
+ presentation:
170
+ palette: light
171
+ ---
172
+ `);
173
+
174
+ await runNorna(['--site-dir', siteDir, 'build']);
175
+ const rootHtml = await readFile(path.join(path.dirname(siteDir), 'dist', 'index.html'), 'utf8');
176
+ const routeHtml = await readFile(path.join(path.dirname(siteDir), 'dist', 'guide', 'index.html'), 'utf8');
177
+ assert.match(rootHtml, /--color-page: #000000/);
178
+ assert.match(routeHtml, /--color-page: #ffffff/);
179
+ assert.match(routeHtml, /<a class="site-brand" href="\/">Root Brand<\/a>/);
180
+ } finally {
181
+ await rm(root, { recursive: true, force: true });
182
+ }
183
+ });
184
+
185
+ test('route theme cannot define site identity', async () => {
186
+ const { root, siteDir } = await createTempSite({ underRepoCache: true });
187
+ try {
188
+ await mkdir(path.join(siteDir, 'routes', '010-guide'), { recursive: true });
189
+ await writeFile(path.join(siteDir, 'content.md'), `---
190
+ title: Root
191
+ description: Root page
192
+ ---
193
+
194
+ ## Home {#home}
195
+
196
+ Root content.
197
+ `);
198
+ await writeFile(path.join(siteDir, 'routes', '010-guide', 'route-content.md'), `---
199
+ title: Guide
200
+ description: Guide page
201
+ ---
202
+
203
+ ## Guide {#guide}
204
+
205
+ Route content.
206
+ `);
207
+ await writeFile(path.join(siteDir, 'routes', '010-guide', 'theme.md'), `---
208
+ navigation:
209
+ brand: Route Brand
210
+ ---
211
+ `);
212
+
213
+ await assert.rejects(
214
+ runNorna(['--site-dir', siteDir, 'config:check']),
215
+ (error) => {
216
+ assert.match(error.output, /may not define navigation\. Brand and logo belong in site\/sitewide-content\.md/);
217
+ return true;
218
+ },
219
+ );
220
+ } finally {
221
+ await rm(root, { recursive: true, force: true });
222
+ }
223
+ });
224
+
132
225
  test('content:check warns for local Markdown images but not external Markdown images', async () => {
133
226
  const { root, siteDir } = await createTempSite();
134
227
  try {
@@ -189,6 +282,40 @@ description: Fixture
189
282
  }
190
283
  });
191
284
 
285
+ test('inline notes render as linked numbered margin notes', async () => {
286
+ const { root, siteDir } = await createTempSite({ underRepoCache: true });
287
+ try {
288
+ await writeFile(path.join(siteDir, 'content.md'), `---
289
+ title: Inline notes
290
+ description: Fixture
291
+ ---
292
+
293
+ ## Intro {#intro}
294
+
295
+ The first paragraph points to an explanation.{note-ref}
296
+
297
+ {note: This explanation names \`sitewide-content.md\` and stays beside the paragraph on wide screens.}
298
+
299
+ The second paragraph has its own explanation.{note-ref}
300
+
301
+ {note: This is the second explanation.}
302
+ `);
303
+
304
+ await runNorna(['--site-dir', siteDir, 'build']);
305
+ const html = await readFile(path.join(path.dirname(siteDir), 'dist', 'index.html'), 'utf8');
306
+ assert.match(html, /<sup class="section-note-ref"><a id="note-ref-home-intro-1" href="#note-home-intro-1" aria-label="Note 1" aria-describedby="note-home-intro-1" data-note-id="note-home-intro-1">1<\/a><\/sup>/);
307
+ assert.match(html, /<aside class="section-note section-note-margin" id="note-home-intro-1" aria-label="Note 1" role="note" data-note-id="note-home-intro-1" style="grid-row: 1;">/);
308
+ assert.match(html, /<a class="section-note-number" href="#note-ref-home-intro-1" aria-label="Note 1">\s*1\s*<\/a>/);
309
+ assert.match(html, /This explanation names <code>sitewide-content\.md<\/code> and stays beside the paragraph on wide screens\./);
310
+ assert.match(html, /<sup class="section-note-ref"><a id="note-ref-home-intro-2" href="#note-home-intro-2" aria-label="Note 2" aria-describedby="note-home-intro-2" data-note-id="note-home-intro-2">2<\/a><\/sup>/);
311
+ assert.match(html, /<aside class="section-note section-note-margin" id="note-home-intro-2" aria-label="Note 2" role="note" data-note-id="note-home-intro-2" style="grid-row: 2;">/);
312
+ assert.match(html, /This is the second explanation\./);
313
+ assert.doesNotMatch(html, /\{note:/);
314
+ } finally {
315
+ await rm(root, { recursive: true, force: true });
316
+ }
317
+ });
318
+
192
319
  test('updated managed SVG images get updated static output', async () => {
193
320
  const { root, siteDir } = await createTempSite();
194
321
  try {
@@ -842,6 +969,33 @@ description: Fixture
842
969
  }
843
970
  });
844
971
 
972
+ test('content:check rejects the removed norna-note block', async () => {
973
+ const { root, siteDir } = await createTempSite();
974
+ try {
975
+ await writeFile(path.join(siteDir, 'content.md'), `---
976
+ title: Removed note block
977
+ description: Fixture
978
+ ---
979
+
980
+ ## Intro {#intro}
981
+
982
+ \`\`\`norna-note
983
+ This block is no longer supported.
984
+ \`\`\`
985
+ `);
986
+
987
+ await assert.rejects(
988
+ () => runContentScript(siteDir, ['--check']),
989
+ (error) => {
990
+ assert.match(error.output, /Unknown Norna block "norna-note"\. Use one of: norna-image-stack, norna-image-carousel, norna-card-list\./);
991
+ return true;
992
+ },
993
+ );
994
+ } finally {
995
+ await rm(root, { recursive: true, force: true });
996
+ }
997
+ });
998
+
845
999
  test('content:check supports tilde fenced Norna blocks', async () => {
846
1000
  const { root, siteDir } = await createTempSite();
847
1001
  try {
@@ -57,8 +57,10 @@ try {
57
57
  for (const [presetName, preset] of Object.entries(typographyPresets)) {
58
58
  for (const [level, heading] of Object.entries(preset.headings)) {
59
59
  assert.equal(heading.size, 'medium', `${presetName} ${level} should use medium size`);
60
+ assert.ok([400, 500, 600, 700].includes(heading.weight), `${presetName} ${level} should use a supported weight`);
60
61
  }
61
62
  assert.equal(preset.body.size, 'medium', `${presetName} body should use medium size`);
63
+ assert.ok(['narrow', 'normal', 'wide'].includes(preset.body.width), `${presetName} body should use a supported width`);
62
64
  assert.equal(preset.caption.size, 'medium', `${presetName} caption should use medium size`);
63
65
  }
64
66
  for (const [rhythmName, rhythm] of Object.entries(typographyRhythms)) {
@@ -76,7 +78,7 @@ try {
76
78
  assert.match(showResult.stdout, /pages:/);
77
79
  assert.match(showResult.stdout, /\s+\/:/);
78
80
  assert.match(showResult.stdout, /value: quiet-gallery/);
79
- assert.match(showResult.stdout, /inherited: true/);
81
+ assert.match(showResult.stdout, /source: "site\/theme\.md"/);
80
82
  assert.match(showResult.stdout, /intro:/);
81
83
 
82
84
  const initAgainResult = runCli(['init', initializedSiteRoot]);
@@ -160,6 +160,7 @@ try {
160
160
  assertFileExists(path.join(packagedStarterRoot, 'README.md')),
161
161
  assertFileExists(path.join(packagedStarterRoot, 'site', 'config.mjs')),
162
162
  assertFileExists(path.join(packagedStarterRoot, 'site', 'theme.md')),
163
+ assertFileExists(path.join(packagedStarterRoot, 'site', 'sitewide-content.md')),
163
164
  assertFileExists(path.join(packagedStarterRoot, 'site', 'content.md')),
164
165
  assertFileExists(path.join(packagedStarterRoot, 'site', 'images', 'work', '.gitkeep')),
165
166
  assertFileExists(path.join(packagedStarterRoot, 'site', 'public', 'robots.txt')),
@@ -184,29 +185,38 @@ try {
184
185
  .replace("url: 'https://example.com/'", "url: 'https://example.com/site/'")
185
186
  .replace("basePath: '/'", "basePath: '/site/'"),
186
187
  );
187
- const packageCheckThemePath = path.join(siteProjectRoot, 'site', 'theme.md');
188
- const packageCheckTheme = await readFile(packageCheckThemePath, 'utf8');
188
+ const packageCheckSitewidePath = path.join(siteProjectRoot, 'site', 'sitewide-content.md');
189
+ const packageCheckSitewide = await readFile(packageCheckSitewidePath, 'utf8');
189
190
  await writeFile(
190
- packageCheckThemePath,
191
- packageCheckTheme.replace('brand: Example Site', 'brand: Package Check Brand'),
191
+ packageCheckSitewidePath,
192
+ packageCheckSitewide.replace('brand: Example Site', 'brand: Package Check Brand\n logo:\n alt: Package Check Logo\n height: 2.6rem'),
193
+ );
194
+ await writeFile(
195
+ path.join(siteProjectRoot, 'site', 'public', 'logo.svg'),
196
+ '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32"><rect width="32" height="32" fill="black"/></svg>\n',
192
197
  );
193
198
  await writeFile(path.join(siteProjectRoot, 'site', 'content.md'), `---
194
199
  title: Package Check Site
195
200
  description: Site used by package checks.
196
201
  sections:
197
202
  intro: {}
203
+ warning: {}
198
204
  work: {}
199
205
  ---
200
206
 
201
207
  ## Intro {#intro}
202
208
 
203
- This page verifies that [packaged norna sites]{.highlight} can build with the current content model.
209
+ This page verifies that **packaged norna sites** can build with the current content model.
204
210
 
205
211
  <!-- norna-image-provenance:
206
212
  image: package-check.svg
207
213
  source: package check fixture
208
214
  -->
209
215
 
216
+ ## **Warning** {#warning}
217
+
218
+ This section verifies styled section headings.
219
+
210
220
  ## Work {#work}
211
221
 
212
222
  This section keeps the package check independent from user-facing starter copy.
@@ -257,6 +267,10 @@ This route verifies that packaged norna sites can build route pages.
257
267
  path.join(siteProjectRoot, 'dist', 'index.html'),
258
268
  '--section-heading-font-size-desktop: clamp(1.35rem, 1.1rem + 0.9vw, 2rem)',
259
269
  );
270
+ await assertFileIncludes(
271
+ path.join(siteProjectRoot, 'dist', 'index.html'),
272
+ '--section-heading-font-weight: 400',
273
+ );
260
274
  await assertFileIncludes(
261
275
  path.join(siteProjectRoot, 'dist', 'index.html'),
262
276
  '--section-heading-spacing-after: 0.55em',
@@ -283,7 +297,15 @@ This route verifies that packaged norna sites can build route pages.
283
297
  );
284
298
  await assertFileIncludes(
285
299
  path.join(siteProjectRoot, 'dist', 'index.html'),
286
- '<a class="site-brand" href="/site/">Package Check Brand</a>',
300
+ '<a class="site-brand" href="/site/"><img class="site-brand-logo" src="/site/logo.svg" alt="Package Check Logo">',
301
+ );
302
+ await assertFileIncludes(
303
+ path.join(siteProjectRoot, 'dist', 'index.html'),
304
+ '<div class="site-nav-submenu">',
305
+ );
306
+ await assertFileIncludes(
307
+ path.join(siteProjectRoot, 'dist', 'index.html'),
308
+ 'href="/site/#intro">Intro</a>',
287
309
  );
288
310
  await assertFileIncludes(
289
311
  path.join(siteProjectRoot, 'dist', 'index.html'),
@@ -293,6 +315,10 @@ This route verifies that packaged norna sites can build route pages.
293
315
  path.join(siteProjectRoot, 'dist', 'index.html'),
294
316
  '--section-body-line-height: 1.5',
295
317
  );
318
+ await assertFileIncludes(
319
+ path.join(siteProjectRoot, 'dist', 'index.html'),
320
+ '--section-body-width-desktop: min(72ch, var(--text-width))',
321
+ );
296
322
  await assertFileExcludes(
297
323
  path.join(siteProjectRoot, 'dist', 'index.html'),
298
324
  'norna-image-provenance',
@@ -313,21 +339,37 @@ This route verifies that packaged norna sites can build route pages.
313
339
  path.join(siteProjectRoot, 'dist', 'index.html'),
314
340
  '--section-caption-font-size-desktop: clamp(0.86rem, 0.84rem + 0.08vw, 0.92rem)',
315
341
  );
342
+ await assertFileExcludes(
343
+ path.join(siteProjectRoot, 'dist', 'index.html'),
344
+ 'class="inline-style',
345
+ );
346
+ await assertFileIncludes(
347
+ path.join(siteProjectRoot, 'dist', 'index.html'),
348
+ 'data-section-title="Warning"',
349
+ );
316
350
  await assertFileIncludes(
317
351
  path.join(siteProjectRoot, 'dist', 'index.html'),
318
- 'class="inline-style inline-style-highlight" style="--inline-style-color: #ffd84d"',
352
+ 'href="/site/#warning">Warning</a>',
319
353
  );
320
354
  await assertFileIncludes(
321
355
  path.join(siteProjectRoot, 'dist', 'index.html'),
322
356
  '--section-background-color: #000000',
323
357
  );
358
+ await assertFileIncludes(
359
+ path.join(siteProjectRoot, 'dist', 'index.html'),
360
+ '--section-background-color: #171717',
361
+ );
362
+ await assertFileIncludes(
363
+ path.join(siteProjectRoot, 'dist', 'index.html'),
364
+ '--section-background-color: #252525',
365
+ );
324
366
  await assertFileIncludes(
325
367
  path.join(siteProjectRoot, 'dist', 'index.html'),
326
368
  '--site-top-background-color: #000000',
327
369
  );
328
370
  await assertFileIncludes(
329
371
  path.join(siteProjectRoot, 'dist', 'index.html'),
330
- '--site-top-text-color: #f7f4ee',
372
+ '--site-top-text-color: #f2eee6',
331
373
  );
332
374
  await assertFileIncludes(
333
375
  path.join(siteProjectRoot, 'dist', 'index.html'),
@@ -335,11 +377,11 @@ This route verifies that packaged norna sites can build route pages.
335
377
  );
336
378
  await assertFileIncludes(
337
379
  path.join(siteProjectRoot, 'dist', 'index.html'),
338
- '--site-footer-text-color: #f7f4ee',
380
+ '--site-footer-text-color: #f2eee6',
339
381
  );
340
382
  await assertFileIncludes(
341
383
  path.join(siteProjectRoot, 'dist', 'index.html'),
342
- '--section-text-color: #f7f4ee',
384
+ '--section-text-color: #f2eee6',
343
385
  );
344
386
  await writeFile(path.join(siteProjectRoot, 'site', 'public', 'favicon.ico'), 'fake icon');
345
387
  await runInherit(npxBin, ['norna', 'build'], { cwd: siteProjectRoot, env: npmEnv });
@@ -365,35 +407,52 @@ This route verifies that packaged norna sites can build route pages.
365
407
  );
366
408
  await writeFile(
367
409
  siteThemePath,
368
- siteTheme.replace('backgroundColor: "#000000"', 'backgroundColor: blue'),
410
+ siteTheme.replace('\n palette: dark', '\n palette: neon'),
369
411
  );
370
412
  await runExpectFailure(
371
413
  npxBin,
372
414
  ['norna', 'build'],
373
415
  { cwd: siteProjectRoot, env: npmEnv },
374
- 'presentation.backgroundColor',
416
+ 'presentation.palette',
375
417
  );
376
418
  await writeFile(
377
419
  siteThemePath,
378
- siteTheme.replace('textColor: "#f7f4ee"', 'textColor: white'),
420
+ siteTheme.replace(' sectionSurfaces:\n mode: cycle', ' sectionSurfaces:\n mode: glowing'),
379
421
  );
380
422
  await runExpectFailure(
381
423
  npxBin,
382
424
  ['norna', 'build'],
383
425
  { cwd: siteProjectRoot, env: npmEnv },
384
- 'presentation.textColor',
426
+ 'presentation.sectionSurfaces.mode',
385
427
  );
386
428
  await writeFile(
387
429
  siteThemePath,
388
- siteTheme.replace(' color: "#ffd84d"', ' color: yellow'),
430
+ siteTheme.replace(
431
+ ' sectionSurfaces:\n mode: cycle',
432
+ ' sectionSurfaces:\n mode: cycle\n sequence: [base, base]',
433
+ ),
389
434
  );
390
435
  await runExpectFailure(
391
436
  npxBin,
392
437
  ['norna', 'build'],
393
438
  { cwd: siteProjectRoot, env: npmEnv },
394
- 'presentation.inlineStyles.highlight.color',
439
+ 'presentation.sectionSurfaces',
395
440
  );
396
441
  await writeFile(siteThemePath, siteTheme);
442
+ await writeFile(
443
+ siteContentPath,
444
+ siteContent.replace(
445
+ 'This page verifies that **packaged norna sites** can build with the current content model.',
446
+ 'This page verifies that [packaged norna sites]{.highlight} can build with the current content model.',
447
+ ),
448
+ );
449
+ await runExpectFailure(
450
+ npxBin,
451
+ ['norna', 'build'],
452
+ { cwd: siteProjectRoot, env: npmEnv },
453
+ 'Inline color style ".highlight" is no longer supported',
454
+ );
455
+ await writeFile(siteContentPath, siteContent);
397
456
  await writeFile(
398
457
  siteContentPath,
399
458
  siteContent.replace(
@@ -405,7 +464,7 @@ This route verifies that packaged norna sites can build route pages.
405
464
  npxBin,
406
465
  ['norna', 'build'],
407
466
  { cwd: siteProjectRoot, env: npmEnv },
408
- 'presentation.typography.overrides.body.lineHeight',
467
+ 'presentation',
409
468
  );
410
469
  await writeFile(
411
470
  siteContentPath,
@@ -418,7 +477,7 @@ This route verifies that packaged norna sites can build route pages.
418
477
  npxBin,
419
478
  ['norna', 'build'],
420
479
  { cwd: siteProjectRoot, env: npmEnv },
421
- 'sections.intro.presentation.typography.overrides.caption.spacingBefore',
480
+ 'sections.intro: Unrecognized key: "presentation"',
422
481
  );
423
482
  await writeFile(
424
483
  siteContentPath,
@@ -431,7 +490,7 @@ This route verifies that packaged norna sites can build route pages.
431
490
  npxBin,
432
491
  ['norna', 'build'],
433
492
  { cwd: siteProjectRoot, env: npmEnv },
434
- 'sections.intro.presentation.typography.preset',
493
+ 'sections.intro: Unrecognized key: "presentation"',
435
494
  );
436
495
  await writeFile(
437
496
  siteThemePath,
@@ -51,8 +51,23 @@ try {
51
51
  await writeFile(path.join(siteDir, 'theme.md'), `---
52
52
  typography:
53
53
  preset: quiet-gallery
54
- frame:
55
- colors: presentation
54
+ presentation:
55
+ palette: dark
56
+ ---
57
+ `);
58
+ await writeFile(path.join(siteDir, 'sitewide-content.md'), `---
59
+ banners:
60
+ - id: expired-banner
61
+ visible:
62
+ until: "2026-01-01"
63
+ title: Expired banner
64
+ text: Expired banner text.
65
+ - id: active-banner
66
+ visible:
67
+ from: "2026-01-01"
68
+ until: "2026-12-31"
69
+ title: Active banner
70
+ text: Active banner text.
56
71
  ---
57
72
  `);
58
73
  await writeFile(path.join(siteDir, 'content.md'), `---
@@ -88,6 +103,8 @@ Always visible section text.
88
103
  assert.match(html, /Active section text/);
89
104
  assert.match(html, /Always visible section text/);
90
105
  assert.doesNotMatch(html, /Expired section text/);
106
+ assert.match(html, /Active banner text/);
107
+ assert.doesNotMatch(html, /Expired banner text/);
91
108
 
92
109
  console.log('Temporary visibility test passed.');
93
110
  } finally {
@@ -18,16 +18,19 @@ type CardListWidth = 'text' | 'narrow' | 'normal' | 'wide';
18
18
 
19
19
  interface Props {
20
20
  cards: Card[];
21
+ maxAvailableWidthPercent: {
22
+ desktop: number;
23
+ };
21
24
  layout?: CardListLayout;
22
25
  flow?: CardListFlow;
23
26
  size?: CardListSize;
24
27
  width?: CardListWidth;
25
28
  }
26
29
 
27
- const { cards, layout = 'image-top', flow = 'grid', size = 'm', width = 'normal' } = Astro.props;
30
+ const { cards, maxAvailableWidthPercent, layout = 'image-top', flow = 'grid', size = 'm', width = 'normal' } = Astro.props;
28
31
  const cardImageSizes = [
29
32
  '(max-width: 700px) 100vw',
30
- `min(20rem, ${projectConfig.gallery.maxAvailableWidthPercent.desktop}vw)`,
33
+ `min(20rem, ${maxAvailableWidthPercent.desktop}vw)`,
31
34
  ].join(', ');
32
35
  const displayLink = (link: string) => withBasePath(projectConfig.site.basePath, link);
33
36
  const cardListClasses = [