@janga/norna 0.7.17 → 0.7.18

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 (262) hide show
  1. package/README.md +15 -6
  2. package/bin/norna-cli.mjs +1 -1
  3. package/docs/README.md +11 -9
  4. package/docs/configuration.md +25 -2
  5. package/docs/content.md +21 -12
  6. package/docs/design/navigation-and-theme-plan.md +90 -19
  7. package/docs/design/site-examples-structure.md +7 -3
  8. package/docs/engine-development.md +19 -22
  9. package/docs/getting-started.md +3 -3
  10. package/docs/images-and-metadata.md +6 -4
  11. package/docs/pages.md +120 -64
  12. package/docs/public-files.md +1 -1
  13. package/docs/site-files.md +36 -29
  14. package/docs/sitewide-content.md +1 -1
  15. package/docs/theme.md +107 -141
  16. package/docs/typography.md +15 -27
  17. package/examples/README.md +8 -2
  18. package/examples/complete-sites/dog-shelter-multi-page/site/.norna/generated-images.json +5 -5
  19. package/examples/complete-sites/dog-shelter-single-page/site/.norna/generated-images.json +10 -10
  20. package/examples/feature-demos/media-and-surfaces/.astro/collections/theme.schema.json +35 -43
  21. package/examples/feature-demos/media-and-surfaces/.astro/data-store.json +1 -1
  22. package/examples/feature-demos/media-and-surfaces/site/pages/020-surfaces/content.md +2 -1
  23. package/examples/feature-demos/media-and-surfaces/site/pages/020-surfaces/theme.yaml +2 -3
  24. package/examples/feature-demos/theme-preset-documentation/README.md +16 -0
  25. package/examples/feature-demos/theme-preset-documentation/site/.norna/generated-images.json +74 -0
  26. package/examples/feature-demos/{theme-presets/site/pages/020-documentation → theme-preset-documentation/site/pages/000-home}/content.md +1 -1
  27. package/examples/feature-demos/theme-preset-portfolio/README.md +16 -0
  28. package/examples/feature-demos/theme-preset-portfolio/site/.norna/generated-images.json +74 -0
  29. package/examples/feature-demos/theme-preset-portfolio/site/config.yaml +1 -0
  30. package/examples/feature-demos/{theme-presets/site/pages/010-portfolio → theme-preset-portfolio/site/pages/000-home}/content.md +1 -1
  31. package/examples/feature-demos/theme-preset-portfolio/site/public/robots.txt +2 -0
  32. package/examples/feature-demos/theme-preset-portfolio/site/sitewide-content.yaml +3 -0
  33. package/examples/feature-demos/theme-preset-project/README.md +16 -0
  34. package/examples/feature-demos/theme-preset-project/site/.norna/generated-images.json +74 -0
  35. package/examples/feature-demos/theme-preset-project/site/config.yaml +1 -0
  36. package/examples/feature-demos/{theme-presets/site/pages/030-project → theme-preset-project/site/pages/000-home}/content.md +1 -1
  37. package/examples/feature-demos/theme-preset-project/site/public/robots.txt +2 -0
  38. package/examples/feature-demos/theme-preset-project/site/sitewide-content.yaml +3 -0
  39. package/examples/feature-demos/theme-preset-statement/README.md +16 -0
  40. package/examples/feature-demos/theme-preset-statement/site/.norna/generated-images.json +74 -0
  41. package/examples/feature-demos/theme-preset-statement/site/config.yaml +1 -0
  42. package/examples/feature-demos/{theme-presets/site/pages/040-statement → theme-preset-statement/site/pages/000-home}/content.md +1 -1
  43. package/examples/feature-demos/theme-preset-statement/site/public/robots.txt +2 -0
  44. package/examples/feature-demos/theme-preset-statement/site/sitewide-content.yaml +3 -0
  45. package/fixtures/basic/site/theme.yaml +1 -8
  46. package/fixtures/nested-pages/.astro/collections/site.schema.json +35 -0
  47. package/fixtures/nested-pages/.astro/collections/sitewide.schema.json +97 -0
  48. package/fixtures/nested-pages/.astro/collections/theme.schema.json +844 -0
  49. package/fixtures/nested-pages/.astro/content-assets.mjs +4 -0
  50. package/fixtures/nested-pages/.astro/content-modules.mjs +1 -0
  51. package/fixtures/nested-pages/.astro/content.d.ts +183 -0
  52. package/fixtures/nested-pages/.astro/data-store.json +1 -0
  53. package/fixtures/nested-pages/.astro/dev-local.json +8 -0
  54. package/fixtures/nested-pages/.astro/dev.log +1128 -0
  55. package/fixtures/nested-pages/.astro/settings.json +5 -0
  56. package/fixtures/nested-pages/.astro/types.d.ts +2 -0
  57. package/fixtures/nested-pages/node_modules/.vite/deps/_metadata.json +74 -0
  58. package/fixtures/nested-pages/node_modules/.vite/deps/astro-D_GIpJIE.js +434 -0
  59. package/fixtures/nested-pages/node_modules/.vite/deps/astro-D_GIpJIE.js.map +1 -0
  60. package/fixtures/nested-pages/node_modules/.vite/deps/astro_n_aria-query.js +6390 -0
  61. package/fixtures/nested-pages/node_modules/.vite/deps/astro_n_aria-query.js.map +1 -0
  62. package/fixtures/nested-pages/node_modules/.vite/deps/astro_n_axobject-query.js +2702 -0
  63. package/fixtures/nested-pages/node_modules/.vite/deps/astro_n_axobject-query.js.map +1 -0
  64. package/fixtures/nested-pages/node_modules/.vite/deps/astro_n_html-escaper.js +65 -0
  65. package/fixtures/nested-pages/node_modules/.vite/deps/astro_n_html-escaper.js.map +1 -0
  66. package/fixtures/nested-pages/node_modules/.vite/deps/astro_runtime_client_dev-toolbar_entrypoint__js.js +455 -0
  67. package/fixtures/nested-pages/node_modules/.vite/deps/astro_runtime_client_dev-toolbar_entrypoint__js.js.map +1 -0
  68. package/fixtures/nested-pages/node_modules/.vite/deps/audit-DM9Hkl5B.js +1534 -0
  69. package/fixtures/nested-pages/node_modules/.vite/deps/audit-DM9Hkl5B.js.map +1 -0
  70. package/fixtures/nested-pages/node_modules/.vite/deps/highlight-DnZ1ULOL.js +48 -0
  71. package/fixtures/nested-pages/node_modules/.vite/deps/highlight-DnZ1ULOL.js.map +1 -0
  72. package/fixtures/nested-pages/node_modules/.vite/deps/icons-RyebKi45.js +42 -0
  73. package/fixtures/nested-pages/node_modules/.vite/deps/icons-RyebKi45.js.map +1 -0
  74. package/fixtures/nested-pages/node_modules/.vite/deps/package.json +3 -0
  75. package/fixtures/nested-pages/node_modules/.vite/deps/rolldown-runtime-BvCyGRYZ.js +4 -0
  76. package/fixtures/nested-pages/node_modules/.vite/deps/settings-DaU-gNnK.js +190 -0
  77. package/fixtures/nested-pages/node_modules/.vite/deps/settings-DaU-gNnK.js.map +1 -0
  78. package/fixtures/nested-pages/node_modules/.vite/deps/toolbar-63PA619H.js +458 -0
  79. package/fixtures/nested-pages/node_modules/.vite/deps/toolbar-63PA619H.js.map +1 -0
  80. package/fixtures/nested-pages/node_modules/.vite/deps/ui-library--bkcNSYz.js +1016 -0
  81. package/fixtures/nested-pages/node_modules/.vite/deps/ui-library--bkcNSYz.js.map +1 -0
  82. package/fixtures/nested-pages/node_modules/.vite/deps/window-jPkpKVcy.js +32 -0
  83. package/fixtures/nested-pages/node_modules/.vite/deps/window-jPkpKVcy.js.map +1 -0
  84. package/fixtures/nested-pages/node_modules/.vite/deps/xray-BXCHpEJP.js +111 -0
  85. package/fixtures/nested-pages/node_modules/.vite/deps/xray-BXCHpEJP.js.map +1 -0
  86. package/fixtures/nested-pages/site/config.yaml +2 -0
  87. package/fixtures/nested-pages/site/pages/010-guides/pages/010-installation/pages/010-macos/content.md +4 -0
  88. package/fixtures/nested-pages/site/pages/010-guides/pages/010-installation/theme.yaml +2 -0
  89. package/fixtures/nested-pages/site/pages/010-guides/pages/030-release-notes/content.md +9 -0
  90. package/fixtures/nested-pages/site/pages/010-guides/pages/040-configuration/content.md +3 -0
  91. package/fixtures/nested-pages/site/pages/010-guides/pages/050-content/content.md +3 -0
  92. package/fixtures/nested-pages/site/pages/010-guides/pages/060-themes/content.md +3 -0
  93. package/fixtures/nested-pages/site/pages/010-guides/pages/070-images/content.md +3 -0
  94. package/fixtures/nested-pages/site/pages/010-guides/pages/080-cards/content.md +3 -0
  95. package/fixtures/nested-pages/site/pages/010-guides/pages/090-notes/content.md +3 -0
  96. package/fixtures/nested-pages/site/pages/010-guides/pages/100-validation/content.md +3 -0
  97. package/fixtures/nested-pages/site/pages/010-guides/pages/110-synchronization/content.md +3 -0
  98. package/fixtures/nested-pages/site/pages/010-guides/pages/120-building/content.md +3 -0
  99. package/fixtures/nested-pages/site/pages/010-guides/pages/130-publishing/content.md +3 -0
  100. package/fixtures/nested-pages/site/pages/010-guides/pages/140-troubleshooting/content.md +3 -0
  101. package/fixtures/nested-pages/site/pages/010-guides/pages/150-migration/content.md +3 -0
  102. package/fixtures/nested-pages/site/pages/010-guides/pages/160-accessibility/content.md +3 -0
  103. package/fixtures/nested-pages/site/pages/010-guides/pages/170-performance/content.md +3 -0
  104. package/fixtures/nested-pages/site/pages/010-guides/pages/180-security/content.md +3 -0
  105. package/fixtures/nested-pages/site/pages/010-guides/pages/190-localization/content.md +3 -0
  106. package/fixtures/nested-pages/site/pages/010-guides/pages/200-upgrading/content.md +3 -0
  107. package/fixtures/nested-pages/site/pages/010-guides/pages/210-faq/content.md +3 -0
  108. package/fixtures/nested-pages/site/pages/010-guides/theme.yaml +7 -3
  109. package/fixtures/nested-pages/site/theme.yaml +1 -3
  110. package/package.json +2 -1
  111. package/schemas/config.schema.json +45 -4
  112. package/schemas/content-frontmatter.schema.json +5 -5
  113. package/schemas/manifest.json +3 -2
  114. package/schemas/page-theme.schema.json +192 -0
  115. package/schemas/sitewide-content.schema.json +17 -17
  116. package/schemas/theme.schema.json +188 -216
  117. package/scripts/check-config.mjs +2 -1
  118. package/scripts/deploy-site.mjs +1 -1
  119. package/scripts/generate-images.mjs +7 -14
  120. package/scripts/generate-schemas.mjs +4 -1
  121. package/scripts/lib/editor-language-service.mjs +0 -14
  122. package/scripts/lib/example-sites.mjs +15 -3
  123. package/scripts/lib/page-model.mjs +26 -7
  124. package/scripts/lib/presentation.mjs +25 -17
  125. package/scripts/lib/project-config.mjs +26 -16
  126. package/scripts/lib/schema-definitions.mjs +42 -14
  127. package/scripts/lib/schema-editor-metadata.mjs +70 -26
  128. package/scripts/lib/schema-value-definitions.mjs +14 -0
  129. package/scripts/lib/site-content.mjs +50 -25
  130. package/scripts/lib/site-conventions.mjs +1 -0
  131. package/scripts/lib/site-paths.mjs +9 -3
  132. package/scripts/lib/theme-config.mjs +3 -2
  133. package/scripts/lib/theme-presets.mjs +54 -21
  134. package/scripts/show-typography.mjs +1 -33
  135. package/scripts/sync-content-sections.mjs +1 -3
  136. package/scripts/test-banner-visibility.mjs +2 -2
  137. package/scripts/test-client-javascript.mjs +11 -4
  138. package/scripts/test-content-check.mjs +25 -25
  139. package/scripts/test-content-model-v2.mjs +165 -101
  140. package/scripts/test-documentation.mjs +4 -2
  141. package/scripts/test-editor-language-service.mjs +11 -8
  142. package/scripts/test-engine-commands.mjs +5 -5
  143. package/scripts/test-examples.mjs +30 -1
  144. package/scripts/test-heading-ids.mjs +2 -2
  145. package/scripts/test-navigation.mjs +11 -2
  146. package/scripts/test-nested-pages.mjs +25 -10
  147. package/scripts/test-package-check.mjs +29 -60
  148. package/scripts/test-page-model.mjs +30 -2
  149. package/scripts/test-project-config.mjs +5 -10
  150. package/scripts/test-schemas.mjs +18 -3
  151. package/scripts/test-theme-presets.mjs +33 -27
  152. package/src/components/NavigationPageTree.astro +114 -25
  153. package/src/components/PageContentsLinks.astro +40 -0
  154. package/src/components/SectionNavigationScript.astro +12 -5
  155. package/src/components/SiteBreadcrumbs.astro +17 -14
  156. package/src/components/SiteNavigation.astro +45 -12
  157. package/src/components/SitePage.astro +37 -23
  158. package/src/components/SiteSection.astro +2 -7
  159. package/src/components/SiteTreeNavigation.astro +35 -0
  160. package/src/components/TreeNavigationScript.astro +83 -0
  161. package/src/content.config.ts +2 -8
  162. package/src/layouts/BaseLayout.astro +10 -0
  163. package/src/lib/pageThemes.ts +11 -9
  164. package/src/lib/sectionContent.ts +1 -5
  165. package/src/lib/siteNavigation.ts +2 -1
  166. package/src/lib/sitePages.ts +25 -14
  167. package/src/pages/index.astro +2 -1
  168. package/src/styles/global.css +411 -43
  169. package/starters/basic/README.md +7 -7
  170. package/starters/basic/site/theme.yaml +1 -105
  171. package/starters/project/README.md +8 -8
  172. package/starters/project/site/theme.yaml +1 -31
  173. package/examples/feature-demos/theme-presets/README.md +0 -18
  174. package/examples/feature-demos/theme-presets/site/.norna/generated-images.json +0 -290
  175. package/examples/feature-demos/theme-presets/site/content.md +0 -14
  176. package/examples/feature-demos/theme-presets/site/theme.yaml +0 -1
  177. /package/examples/complete-sites/dog-shelter-multi-page/site/{content.md → pages/000-home/content.md} +0 -0
  178. /package/examples/complete-sites/dog-shelter-multi-page/site/{images → pages/000-home/images}/shelter-dog.jpg +0 -0
  179. /package/examples/complete-sites/dog-shelter-single-page/site/{content.md → pages/000-home/content.md} +0 -0
  180. /package/examples/complete-sites/dog-shelter-single-page/site/{images → pages/000-home/images}/foster-dog.jpg +0 -0
  181. /package/examples/complete-sites/dog-shelter-single-page/site/{images → pages/000-home/images}/rescue-dog.jpg +0 -0
  182. /package/examples/feature-demos/media-and-surfaces/site/{content.md → pages/000-home/content.md} +0 -0
  183. /package/examples/feature-demos/sitewide-content/site/{content.md → pages/000-home/content.md} +0 -0
  184. /package/examples/feature-demos/{theme-presets → theme-preset-documentation}/site/config.yaml +0 -0
  185. /package/examples/feature-demos/{theme-presets/site/pages/020-documentation → theme-preset-documentation/site/pages/000-home}/images/black-schnauzer.jpg +0 -0
  186. /package/examples/feature-demos/{theme-presets/site/pages/020-documentation → theme-preset-documentation/site/pages/000-home}/images/retriever-portrait.jpg +0 -0
  187. /package/examples/feature-demos/{theme-presets/site/pages/020-documentation → theme-preset-documentation/site/pages/000-home}/images/schnauzer-portrait.jpg +0 -0
  188. /package/examples/feature-demos/{theme-presets → theme-preset-documentation}/site/public/robots.txt +0 -0
  189. /package/examples/feature-demos/{theme-presets → theme-preset-documentation}/site/sitewide-content.yaml +0 -0
  190. /package/examples/feature-demos/{theme-presets/site/pages/020-documentation → theme-preset-documentation/site}/theme.yaml +0 -0
  191. /package/examples/feature-demos/{theme-presets/site/pages/010-portfolio → theme-preset-portfolio/site/pages/000-home}/images/puppy-portrait.jpg +0 -0
  192. /package/examples/feature-demos/{theme-presets/site/pages/010-portfolio → theme-preset-portfolio/site/pages/000-home}/images/shepherd-portrait.jpg +0 -0
  193. /package/examples/feature-demos/{theme-presets/site/pages/010-portfolio → theme-preset-portfolio/site/pages/000-home}/images/white-puppy.jpg +0 -0
  194. /package/examples/feature-demos/{theme-presets/site/pages/010-portfolio → theme-preset-portfolio/site}/theme.yaml +0 -0
  195. /package/examples/feature-demos/{theme-presets → theme-preset-project}/.astro/collections/routeTheme.schema.json +0 -0
  196. /package/examples/feature-demos/{theme-presets → theme-preset-project}/.astro/collections/site.schema.json +0 -0
  197. /package/examples/feature-demos/{theme-presets → theme-preset-project}/.astro/collections/sitewide.schema.json +0 -0
  198. /package/examples/feature-demos/{theme-presets → theme-preset-project}/.astro/collections/theme.schema.json +0 -0
  199. /package/examples/feature-demos/{theme-presets → theme-preset-project}/.astro/content-assets.mjs +0 -0
  200. /package/examples/feature-demos/{theme-presets → theme-preset-project}/.astro/content-modules.mjs +0 -0
  201. /package/examples/feature-demos/{theme-presets → theme-preset-project}/.astro/content.d.ts +0 -0
  202. /package/examples/feature-demos/{theme-presets → theme-preset-project}/.astro/data-store.json +0 -0
  203. /package/examples/feature-demos/{theme-presets → theme-preset-project}/.astro/dev-local.json +0 -0
  204. /package/examples/feature-demos/{theme-presets → theme-preset-project}/.astro/dev.json +0 -0
  205. /package/examples/feature-demos/{theme-presets → theme-preset-project}/.astro/dev.log +0 -0
  206. /package/examples/feature-demos/{theme-presets → theme-preset-project}/.astro/settings.json +0 -0
  207. /package/examples/feature-demos/{theme-presets → theme-preset-project}/.astro/types.d.ts +0 -0
  208. /package/examples/feature-demos/{theme-presets → theme-preset-project}/node_modules/.vite/deps/_metadata.json +0 -0
  209. /package/examples/feature-demos/{theme-presets → theme-preset-project}/node_modules/.vite/deps/astro-D_GIpJIE.js +0 -0
  210. /package/examples/feature-demos/{theme-presets → theme-preset-project}/node_modules/.vite/deps/astro-D_GIpJIE.js.map +0 -0
  211. /package/examples/feature-demos/{theme-presets → theme-preset-project}/node_modules/.vite/deps/astro_n_aria-query.js +0 -0
  212. /package/examples/feature-demos/{theme-presets → theme-preset-project}/node_modules/.vite/deps/astro_n_aria-query.js.map +0 -0
  213. /package/examples/feature-demos/{theme-presets → theme-preset-project}/node_modules/.vite/deps/astro_n_axobject-query.js +0 -0
  214. /package/examples/feature-demos/{theme-presets → theme-preset-project}/node_modules/.vite/deps/astro_n_axobject-query.js.map +0 -0
  215. /package/examples/feature-demos/{theme-presets → theme-preset-project}/node_modules/.vite/deps/astro_n_html-escaper.js +0 -0
  216. /package/examples/feature-demos/{theme-presets → theme-preset-project}/node_modules/.vite/deps/astro_n_html-escaper.js.map +0 -0
  217. /package/examples/feature-demos/{theme-presets → theme-preset-project}/node_modules/.vite/deps/astro_runtime_client_dev-toolbar_entrypoint__js.js +0 -0
  218. /package/examples/feature-demos/{theme-presets → theme-preset-project}/node_modules/.vite/deps/astro_runtime_client_dev-toolbar_entrypoint__js.js.map +0 -0
  219. /package/examples/feature-demos/{theme-presets → theme-preset-project}/node_modules/.vite/deps/audit-DM9Hkl5B.js +0 -0
  220. /package/examples/feature-demos/{theme-presets → theme-preset-project}/node_modules/.vite/deps/audit-DM9Hkl5B.js.map +0 -0
  221. /package/examples/feature-demos/{theme-presets → theme-preset-project}/node_modules/.vite/deps/embla-carousel.js +0 -0
  222. /package/examples/feature-demos/{theme-presets → theme-preset-project}/node_modules/.vite/deps/embla-carousel.js.map +0 -0
  223. /package/examples/feature-demos/{theme-presets → theme-preset-project}/node_modules/.vite/deps/highlight-DnZ1ULOL.js +0 -0
  224. /package/examples/feature-demos/{theme-presets → theme-preset-project}/node_modules/.vite/deps/highlight-DnZ1ULOL.js.map +0 -0
  225. /package/examples/feature-demos/{theme-presets → theme-preset-project}/node_modules/.vite/deps/icons-RyebKi45.js +0 -0
  226. /package/examples/feature-demos/{theme-presets → theme-preset-project}/node_modules/.vite/deps/icons-RyebKi45.js.map +0 -0
  227. /package/examples/feature-demos/{theme-presets → theme-preset-project}/node_modules/.vite/deps/package.json +0 -0
  228. /package/examples/feature-demos/{theme-presets → theme-preset-project}/node_modules/.vite/deps/rolldown-runtime-BvCyGRYZ.js +0 -0
  229. /package/examples/feature-demos/{theme-presets → theme-preset-project}/node_modules/.vite/deps/settings-DaU-gNnK.js +0 -0
  230. /package/examples/feature-demos/{theme-presets → theme-preset-project}/node_modules/.vite/deps/settings-DaU-gNnK.js.map +0 -0
  231. /package/examples/feature-demos/{theme-presets → theme-preset-project}/node_modules/.vite/deps/toolbar-63PA619H.js +0 -0
  232. /package/examples/feature-demos/{theme-presets → theme-preset-project}/node_modules/.vite/deps/toolbar-63PA619H.js.map +0 -0
  233. /package/examples/feature-demos/{theme-presets → theme-preset-project}/node_modules/.vite/deps/ui-library--bkcNSYz.js +0 -0
  234. /package/examples/feature-demos/{theme-presets → theme-preset-project}/node_modules/.vite/deps/ui-library--bkcNSYz.js.map +0 -0
  235. /package/examples/feature-demos/{theme-presets → theme-preset-project}/node_modules/.vite/deps/window-jPkpKVcy.js +0 -0
  236. /package/examples/feature-demos/{theme-presets → theme-preset-project}/node_modules/.vite/deps/window-jPkpKVcy.js.map +0 -0
  237. /package/examples/feature-demos/{theme-presets → theme-preset-project}/node_modules/.vite/deps/xray-BXCHpEJP.js +0 -0
  238. /package/examples/feature-demos/{theme-presets → theme-preset-project}/node_modules/.vite/deps/xray-BXCHpEJP.js.map +0 -0
  239. /package/examples/feature-demos/{theme-presets/site/pages/030-project → theme-preset-project/site/pages/000-home}/images/boxer-portrait.jpg +0 -0
  240. /package/examples/feature-demos/{theme-presets/site/pages/030-project → theme-preset-project/site/pages/000-home}/images/grass-puppy.jpg +0 -0
  241. /package/examples/feature-demos/{theme-presets/site/pages/030-project → theme-preset-project/site/pages/000-home}/images/grey-street-dog.jpg +0 -0
  242. /package/examples/feature-demos/{theme-presets/site/pages/030-project → theme-preset-project/site}/theme.yaml +0 -0
  243. /package/examples/feature-demos/{theme-presets/site/pages/040-statement → theme-preset-statement/site/pages/000-home}/images/black-pedigree.jpg +0 -0
  244. /package/examples/feature-demos/{theme-presets/site/pages/040-statement → theme-preset-statement/site/pages/000-home}/images/sheepdog-grass.jpg +0 -0
  245. /package/examples/feature-demos/{theme-presets/site/pages/040-statement → theme-preset-statement/site/pages/000-home}/images/sheepdog-wide.jpg +0 -0
  246. /package/examples/feature-demos/{theme-presets/site/pages/040-statement → theme-preset-statement/site}/theme.yaml +0 -0
  247. /package/fixtures/basic/site/{content.md → pages/000-home/content.md} +0 -0
  248. /package/fixtures/basic/site/{images → pages/000-home/images}/.gitkeep +0 -0
  249. /package/fixtures/content-model-v2/site/{content.md → pages/000-home/content.md} +0 -0
  250. /package/fixtures/content-model-v2/site/{images → pages/000-home/images}/adopt.svg +0 -0
  251. /package/fixtures/content-model-v2/site/{images → pages/000-home/images}/detail.jpg +0 -0
  252. /package/fixtures/content-model-v2/site/{images → pages/000-home/images}/duplicate.jpg +0 -0
  253. /package/fixtures/content-model-v2/site/{images → pages/000-home/images}/foster.svg +0 -0
  254. /package/fixtures/content-model-v2/site/{images → pages/000-home/images}/hero.jpg +0 -0
  255. /package/fixtures/content-model-v2/site/{images → pages/000-home/images}/image.jpg +0 -0
  256. /package/fixtures/content-model-v2/site/{images → pages/000-home/images}/slide-one.jpg +0 -0
  257. /package/fixtures/content-model-v2/site/{images → pages/000-home/images}/slide-three.jpg +0 -0
  258. /package/fixtures/content-model-v2/site/{images → pages/000-home/images}/slide-two.jpg +0 -0
  259. /package/fixtures/nested-pages/site/{content.md → pages/000-home/content.md} +0 -0
  260. /package/starters/basic/site/{content.md → pages/000-home/content.md} +0 -0
  261. /package/starters/basic/site/{images → pages/000-home/images}/.gitkeep +0 -0
  262. /package/starters/project/site/{content.md → pages/000-home/content.md} +0 -0
@@ -45,7 +45,8 @@ try {
45
45
  console.log(`Theme preset: ${themeConfig.preset ?? '(none)'}`);
46
46
  console.log(`Page width: ${projectConfig.layout.pageWidth}`);
47
47
  console.log(`Gutter: desktop ${projectConfig.layout.gutter.desktop}, mobile ${projectConfig.layout.gutter.mobile}`);
48
- console.log(`Layout density: ${projectConfig.layout.density}`);
48
+ console.log(`Content spacing: ${projectConfig.layout.contentSpacing}`);
49
+ console.log(`Text width: ${projectConfig.layout.textWidth}`);
49
50
  console.log(`Image area width: ${projectConfig.images.width}`);
50
51
  console.log(`Image max available width: desktop ${projectConfig.images.maxAvailableWidthPercent.desktop}%, mobile ${projectConfig.images.maxAvailableWidthPercent.mobile}%`);
51
52
  console.log(`Image max available height: desktop ${projectConfig.images.maxAvailableHeightPercent.desktop}%, mobile ${projectConfig.images.maxAvailableHeightPercent.mobile}%`);
@@ -211,7 +211,7 @@ const assertDeployableStatus = async (entries, expectedImagePaths) => {
211
211
  if (unexpectedUntracked.length > 0) {
212
212
  fail([
213
213
  'Refusing to deploy: unexpected untracked files are present.',
214
- `Only new referenced images directly under ${siteImagesLabel}/ or ${sitePagesLabel}/<NNN-page-id>/images/ are staged automatically.`,
214
+ `Only new referenced images directly under a page's images/ directory in ${sitePagesLabel}/ are staged automatically.`,
215
215
  ...unexpectedUntracked.map((entry) => `- ${formatEntry(entry)}`),
216
216
  ].join('\n'));
217
217
  }
@@ -23,7 +23,6 @@ import {
23
23
  generatedImagesManifestPath,
24
24
  originalImagesDir,
25
25
  siteDir,
26
- siteImagesDir,
27
26
  } from './lib/site-paths.mjs';
28
27
 
29
28
  const execFileAsync = promisify(execFile);
@@ -88,10 +87,8 @@ const getFilePathFromPublicPath = (publicPath) => path.join(astroPublicDir, publ
88
87
 
89
88
  const getSourceHashSlug = (sourceHash) => sourceHash.slice(0, sourceHashSlugLength);
90
89
 
91
- const getGeneratedPath = (sourcePath, sourceHash, width) => {
92
- const parsed = path.parse(path.relative(siteImagesDir, sourcePath).startsWith('..')
93
- ? path.relative(path.dirname(siteImagesDir), sourcePath)
94
- : path.relative(siteImagesDir, sourcePath));
90
+ const getGeneratedPath = (sourceKey, sourceHash, width) => {
91
+ const parsed = path.parse(sourceKey);
95
92
  return path.join(generatedImagesDir, parsed.dir, `${parsed.name}-${getSourceHashSlug(sourceHash)}-${width}.webp`);
96
93
  };
97
94
 
@@ -104,11 +101,7 @@ const fail = (message) => {
104
101
  throw new Error(message);
105
102
  };
106
103
 
107
- const getImageSourceKey = (contentFile, image) => (
108
- contentFile.isHome
109
- ? `images/${image}`
110
- : `pages/${contentFile.pageDirectory}/images/${image}`
111
- );
104
+ const getImageSourceKey = (contentFile, image) => `pages/${contentFile.pageDirectory}/images/${image}`;
112
105
 
113
106
  const getReferencedImages = async (contentFile) => {
114
107
  const { body } = await readSiteFile(contentFile.contentPath, contentFile.contentLabel);
@@ -241,8 +234,8 @@ const getVariantWidths = ({ width }) => {
241
234
  return variantWidths;
242
235
  };
243
236
 
244
- const getVariants = (sourcePath, sourceHash, variantWidths) => variantWidths.map((width) => ({
245
- src: getPublicPath(getGeneratedPath(sourcePath, sourceHash, width)),
237
+ const getVariants = (sourceKey, sourceHash, variantWidths) => variantWidths.map((width) => ({
238
+ src: getPublicPath(getGeneratedPath(sourceKey, sourceHash, width)),
246
239
  width,
247
240
  }));
248
241
 
@@ -417,9 +410,9 @@ for (const { sourcePath, sourceKey } of sources) {
417
410
 
418
411
  const dimensions = await identify(sourcePath);
419
412
  const variantWidths = getVariantWidths(dimensions);
420
- const variants = getVariants(sourcePath, sourceHash, variantWidths);
413
+ const variants = getVariants(sourceKey, sourceHash, variantWidths);
421
414
  for (const width of variantWidths) {
422
- const outputPath = getGeneratedPath(sourcePath, sourceHash, width);
415
+ const outputPath = getGeneratedPath(sourceKey, sourceHash, width);
423
416
  await convert(sourcePath, outputPath, width);
424
417
  }
425
418
 
@@ -4,6 +4,7 @@ import { fileURLToPath } from 'node:url';
4
4
  import { z } from 'astro/zod';
5
5
  import {
6
6
  configSchema,
7
+ pageThemeSchema,
7
8
  siteSchema,
8
9
  sitewideSchema,
9
10
  themeVisualSchema,
@@ -15,6 +16,7 @@ const schemaDirectory = path.join(root, 'schemas');
15
16
  const definitions = [
16
17
  ['config.schema.json', 'Norna site configuration', configSchema],
17
18
  ['theme.schema.json', 'Norna visual theme', themeVisualSchema],
19
+ ['page-theme.schema.json', 'Norna page theme', pageThemeSchema],
18
20
  ['sitewide-content.schema.json', 'Norna site-wide content', sitewideSchema],
19
21
  ['content-frontmatter.schema.json', 'Norna page frontmatter', siteSchema],
20
22
  ];
@@ -30,12 +32,13 @@ for (const [filename, title, schema] of definitions) {
30
32
 
31
33
  schemaFiles.set('manifest.json', `${JSON.stringify({
32
34
  editorApiVersion: 1,
33
- schemaVersion: 1,
35
+ schemaVersion: 2,
34
36
  files: {
35
37
  config: 'config.schema.json',
36
38
  contentFrontmatter: 'content-frontmatter.schema.json',
37
39
  sitewideContent: 'sitewide-content.schema.json',
38
40
  theme: 'theme.schema.json',
41
+ pageTheme: 'page-theme.schema.json',
39
42
  },
40
43
  }, null, 2)}\n`);
41
44
 
@@ -116,15 +116,6 @@ export const getSitePublicAssetStatus = async (documentPath) => {
116
116
  const getPageContext = (siteRoot, documentPath) => {
117
117
  const absoluteDocumentPath = path.resolve(documentPath);
118
118
  const relativePath = toPosixPath(path.relative(siteRoot, absoluteDocumentPath));
119
- if (relativePath === 'content.md') {
120
- return {
121
- contentPath: absoluteDocumentPath,
122
- imagesRoot: path.join(siteRoot, 'images'),
123
- pageLabel: 'Home',
124
- pageDirectory: null,
125
- };
126
- }
127
-
128
119
  const match = relativePath.match(/^pages\/(.+)\/content\.md$/);
129
120
  if (!match) return null;
130
121
 
@@ -316,8 +307,6 @@ const collectPageImageRoots = async (directory, siteRoot, roots) => {
316
307
 
317
308
  const getContentFiles = async (siteRoot) => {
318
309
  const files = [];
319
- const homePath = path.join(siteRoot, 'content.md');
320
- if (await fileExists(homePath)) files.push(homePath);
321
310
 
322
311
  const visit = async (directory) => {
323
312
  const entries = await readdir(directory, { withFileTypes: true }).catch((error) => {
@@ -350,9 +339,6 @@ const getReferencesByFilename = async (siteRoot) => {
350
339
 
351
340
  export const createSiteImageIndex = async (siteRoot) => {
352
341
  const files = [];
353
- const homeImageRoot = path.join(siteRoot, 'images');
354
- await collectImageFiles(homeImageRoot, homeImageRoot, 'Home', files);
355
-
356
342
  const pageRoots = [];
357
343
  await collectPageImageRoots(path.join(siteRoot, 'pages'), siteRoot, pageRoots);
358
344
  for (const root of pageRoots) {
@@ -1,9 +1,19 @@
1
1
  import { existsSync } from 'node:fs';
2
2
  import { readdir } from 'node:fs/promises';
3
3
  import path from 'node:path';
4
+ import { homePageDirectory } from './site-conventions.mjs';
4
5
 
5
6
  export const exampleCategories = ['complete-sites', 'feature-demos'];
6
7
 
8
+ const isGeneratedOnlyCacheDirectory = async (exampleDirectory, siteDirectory) => {
9
+ if (!existsSync(path.join(siteDirectory, '.norna'))) return false;
10
+
11
+ const exampleEntries = await readdir(exampleDirectory);
12
+ const siteEntries = await readdir(siteDirectory);
13
+ return exampleEntries.every((name) => name === 'site' || name === '.DS_Store')
14
+ && siteEntries.every((name) => name === '.norna' || name === '.DS_Store');
15
+ };
16
+
7
17
  export const getExampleSites = async (root) => {
8
18
  const examples = [];
9
19
 
@@ -12,12 +22,14 @@ export const getExampleSites = async (root) => {
12
22
  const entries = await readdir(categoryDirectory, { withFileTypes: true });
13
23
 
14
24
  for (const entry of entries.filter((candidate) => candidate.isDirectory()).sort((a, b) => a.name.localeCompare(b.name))) {
15
- const siteDirectory = path.join(categoryDirectory, entry.name, 'site');
25
+ const exampleDirectory = path.join(categoryDirectory, entry.name);
26
+ const siteDirectory = path.join(exampleDirectory, 'site');
16
27
  if (!existsSync(path.join(siteDirectory, 'config.yaml'))) {
28
+ if (await isGeneratedOnlyCacheDirectory(exampleDirectory, siteDirectory)) continue;
17
29
  throw new Error(`Example ${path.relative(root, siteDirectory)} is missing config.yaml.`);
18
30
  }
19
- if (!existsSync(path.join(siteDirectory, 'content.md'))) {
20
- throw new Error(`Example ${path.relative(root, siteDirectory)} is missing content.md.`);
31
+ if (!existsSync(path.join(siteDirectory, 'pages', homePageDirectory, 'content.md'))) {
32
+ throw new Error(`Example ${path.relative(root, siteDirectory)} is missing pages/${homePageDirectory}/content.md.`);
21
33
  }
22
34
 
23
35
  examples.push({
@@ -1,4 +1,6 @@
1
- export const pageDirectoryPattern = /^(?!000)(\d{3})-([a-z0-9]+(?:-[a-z0-9]+)*)$/;
1
+ import { homePageDirectory } from './site-conventions.mjs';
2
+
3
+ export const pageDirectoryPattern = /^(\d{3})-([a-z0-9]+(?:-[a-z0-9]+)*)$/;
2
4
 
3
5
  const pageEntryPathSeparator = '--';
4
6
 
@@ -7,9 +9,12 @@ export const parsePageDirectory = (pageDirectory, label = 'page directory') => {
7
9
 
8
10
  if (!match) {
9
11
  throw new Error(
10
- `Invalid ${label} "${pageDirectory}". Page directories must use the form NNN-page-id, for example 010-getting-started. The prefix must be 001-999 and page-id may contain only lowercase letters, numbers, and single hyphens.`,
12
+ `Invalid ${label} "${pageDirectory}". Page directories must use the form NNN-page-id, for example 010-getting-started. Page ids may contain only lowercase letters, numbers, and single hyphens.`,
11
13
  );
12
14
  }
15
+ if (match[1] === '000' && pageDirectory !== homePageDirectory) {
16
+ throw new Error(`Invalid ${label} "${pageDirectory}". The 000 prefix is reserved for ${homePageDirectory}.`);
17
+ }
13
18
 
14
19
  return {
15
20
  pageDirectory,
@@ -47,17 +52,31 @@ export const parsePageDirectoryPath = (pageDirectoryPath, label = 'page director
47
52
  const pageIds = pageDirectories.map(({ pageId }) => pageId);
48
53
  const pageOrders = pageDirectories.map(({ pageOrder }) => pageOrder);
49
54
  const currentPage = pageDirectories.at(-1);
55
+ const homeIndex = pageDirectories.findIndex(({ pageDirectory }) => pageDirectory === homePageDirectory);
56
+ if (homeIndex > 0) {
57
+ throw new Error(`Invalid ${label} "${pageDirectoryPath}". ${homePageDirectory} is allowed only as a top-level page.`);
58
+ }
59
+ if (homeIndex === 0 && pageDirectories.length > 1) {
60
+ throw new Error(
61
+ `Invalid ${label} "${pageDirectoryPath}". ${homePageDirectory} is the homepage and cannot contain child pages. Place the page beside it under site/pages/, or below another non-home page.`,
62
+ );
63
+ }
64
+ const isHome = homeIndex === 0;
65
+ const logicalPageIds = isHome ? [] : pageIds;
66
+ const logicalPageOrders = isHome ? [] : pageOrders;
50
67
 
51
68
  return {
52
69
  pageDirectory: normalizedPath,
53
70
  pageDirectories: pageDirectories.map(({ pageDirectory }) => pageDirectory),
54
71
  pageId: currentPage.pageId,
55
- pageIds,
72
+ pageIds: logicalPageIds,
56
73
  pageOrder: currentPage.pageOrder,
57
- pageOrders,
58
- pagePath: pageIds.join('/'),
59
- parentPagePath: pageIds.length > 1 ? pageIds.slice(0, -1).join('/') : null,
60
- depth: pageIds.length,
74
+ pageOrders: logicalPageOrders,
75
+ pagePath: logicalPageIds.join('/'),
76
+ parentPagePath: logicalPageIds.length > 1
77
+ ? logicalPageIds.slice(0, -1).join('/')
78
+ : null,
79
+ depth: pageDirectories.length,
61
80
  };
62
81
  };
63
82
 
@@ -5,7 +5,11 @@ import {
5
5
  import { resolveThemeConfig } from './theme-presets.mjs';
6
6
 
7
7
  export const presentationPaletteNames = ['dark', 'light', 'paper'];
8
- const sectionSurfaceNames = ['base', 'soft', 'emphasis'];
8
+ const sectionBackgroundPatterns = Object.freeze({
9
+ uniform: ['base'],
10
+ alternating: ['base', 'soft'],
11
+ cycling: ['base', 'soft', 'emphasis'],
12
+ });
9
13
 
10
14
  const presentationPalettes = Object.freeze({
11
15
  dark: {
@@ -92,34 +96,30 @@ export const getPresentationCssVariables = (presentation) => {
92
96
  };
93
97
  };
94
98
 
95
- const getSectionSurfaces = (value = ['base'], sourceLabel = 'theme.yaml') => {
96
- if (!Array.isArray(value) || value.length < 1 || value.length > 3) {
97
- throw new Error(`sectionSurfaces must contain one to three values in ${sourceLabel}.`);
98
- }
99
-
100
- for (const surface of value) {
101
- if (!sectionSurfaceNames.includes(surface)) {
102
- throw new Error(`Unknown section surface "${surface}" in ${sourceLabel}. Use one of: ${sectionSurfaceNames.join(', ')}.`);
103
- }
104
- }
105
-
106
- if (new Set(value).size !== value.length) {
107
- throw new Error(`Each section surface may appear only once in ${sourceLabel}.`);
99
+ const getSectionSurfaces = (pattern = 'uniform', sourceLabel = 'theme.yaml') => {
100
+ const surfaces = sectionBackgroundPatterns[pattern];
101
+ if (!surfaces) {
102
+ throw new Error(`sections.backgroundPattern must be one of ${Object.keys(sectionBackgroundPatterns).join(', ')} in ${sourceLabel}.`);
108
103
  }
109
104
 
110
- return value;
105
+ return surfaces;
111
106
  };
112
107
 
113
108
  export const resolveThemePresentation = (theme, sourceLabel = 'theme.yaml') => {
114
109
  const normalizedTheme = resolveThemeConfig(theme, sourceLabel);
115
110
  const paletteName = normalizedTheme.palette ?? 'dark';
116
111
  const palette = getPresentationPalette(paletteName);
112
+ const typography = resolveTypographyConfig(normalizedTheme.typography ?? defaultTypography);
113
+ const textWidth = normalizedTheme.layout?.textWidth;
114
+ if (textWidth !== undefined) {
115
+ typography.values.body.width = textWidth;
116
+ }
117
117
 
118
118
  return {
119
119
  paletteName,
120
120
  palette,
121
- sectionSurfaces: getSectionSurfaces(normalizedTheme.sectionSurfaces, sourceLabel),
122
- typography: resolveTypographyConfig(normalizedTheme.typography ?? defaultTypography),
121
+ sectionSurfaces: getSectionSurfaces(normalizedTheme.sections?.backgroundPattern, sourceLabel),
122
+ typography,
123
123
  };
124
124
  };
125
125
 
@@ -129,6 +129,14 @@ export const resolvePagePresentation = (theme, sourceLabel) => {
129
129
  return resolvedThemePresentation;
130
130
  };
131
131
 
132
+ const textWidthCssValues = Object.freeze({
133
+ narrow: 'min(60ch, var(--text-width))',
134
+ normal: 'min(72ch, var(--text-width))',
135
+ wide: 'min(72ch, var(--image-layout-width))',
136
+ });
137
+
138
+ export const getTextWidthCssValue = (textWidth) => textWidthCssValues[textWidth];
139
+
132
140
  export const resolveSectionSurface = (pagePresentation, sectionIndex) => {
133
141
  const surfaceName = pagePresentation.sectionSurfaces[sectionIndex % pagePresentation.sectionSurfaces.length];
134
142
 
@@ -187,11 +187,10 @@ const localeLabels = Object.freeze({
187
187
  dismissBanner: 'Dismiss notice',
188
188
  built: 'Built',
189
189
  images: 'Images',
190
- navigationBack: 'Back to pages',
190
+ navigationChildren: 'Child pages',
191
191
  navigationMenu: 'Menu',
192
- navigationOverview: 'Overview',
193
192
  note: 'Note',
194
- pageNavigation: 'On this page',
193
+ pageNavigation: 'Page contents',
195
194
  siteBanners: 'Site notices',
196
195
  siteNavigation: 'Pages',
197
196
  skipToContent: 'Skip to content',
@@ -201,11 +200,10 @@ const localeLabels = Object.freeze({
201
200
  dismissBanner: 'Stäng meddelande',
202
201
  built: 'Byggd',
203
202
  images: 'Bilder',
204
- navigationBack: 'Tillbaka till sidor',
203
+ navigationChildren: 'Undersidor',
205
204
  navigationMenu: 'Meny',
206
- navigationOverview: 'Översikt',
207
205
  note: 'Not',
208
- pageNavigation: 'På den här sidan',
206
+ pageNavigation: 'Sidinnehåll',
209
207
  siteBanners: 'Meddelanden',
210
208
  siteNavigation: 'Sidor',
211
209
  skipToContent: 'Hoppa till innehållet',
@@ -239,8 +237,8 @@ const siteUrl = readSiteUrl(rawConfig);
239
237
  const scrollBehaviorNames = ['instant', 'smooth'];
240
238
 
241
239
  const defaultFontFamily = "Arial, 'Helvetica Neue', Helvetica, sans-serif";
242
- const layoutDensityNames = ['compact', 'normal', 'airy'];
243
- const layoutDensityProfiles = Object.freeze({
240
+ const contentSpacingNames = ['compact', 'normal', 'spacious'];
241
+ const contentSpacingProfiles = Object.freeze({
244
242
  compact: Object.freeze({
245
243
  blockGap: Object.freeze({
246
244
  desktop: '1.25em',
@@ -293,7 +291,7 @@ const layoutDensityProfiles = Object.freeze({
293
291
  mobile: '1.5rem',
294
292
  }),
295
293
  }),
296
- airy: Object.freeze({
294
+ spacious: Object.freeze({
297
295
  blockGap: Object.freeze({
298
296
  desktop: '1.75em',
299
297
  mobile: '1.5em',
@@ -326,12 +324,19 @@ export const resolveThemeVisualConfig = (theme, sourceLabel = siteThemeLabel) =>
326
324
  const rawLayoutSpacingConfig = assertObject(rawLayoutConfig.spacing ?? {}, 'layout.spacing', sourceLabel);
327
325
  const rawImagesConfig = assertObject(rawThemeConfig.images ?? {}, 'images', sourceLabel);
328
326
  const rawTypographyConfig = assertObject(rawThemeConfig.typography ?? {}, 'typography', sourceLabel);
329
- const resolvedLayoutDensity = readEnum(rawLayoutConfig, 'density', 'layout', layoutDensityNames, 'normal', sourceLabel);
330
- const resolvedLayoutSpacingDefaults = layoutDensityProfiles[resolvedLayoutDensity];
327
+ const resolvedContentSpacing = readEnum(rawLayoutConfig, 'contentSpacing', 'layout', contentSpacingNames, 'normal', sourceLabel);
328
+ const resolvedLayoutSpacingDefaults = contentSpacingProfiles[resolvedContentSpacing];
329
+ const shape = readEnum(rawThemeConfig, 'shape', 'theme', ['square', 'soft'], 'soft', sourceLabel);
331
330
 
332
331
  return Object.freeze({
333
332
  layout: Object.freeze({
334
- density: resolvedLayoutDensity,
333
+ contentSpacing: resolvedContentSpacing,
334
+ textWidth: readEnum(rawLayoutConfig, 'textWidth', 'layout', ['narrow', 'normal', 'wide'], 'normal', sourceLabel),
335
+ localNavigationGap: readCssLengthValue(
336
+ rawLayoutConfig.localNavigationGap ?? 'clamp(1.5rem, 3vw, 3rem)',
337
+ 'layout.localNavigationGap',
338
+ sourceLabel,
339
+ ),
335
340
  gutter: readResponsiveCssLength(rawLayoutConfig, 'gutter', 'layout', Object.freeze({
336
341
  desktop: 'clamp(1.25rem, 4vw, 3rem)',
337
342
  mobile: '1rem',
@@ -360,12 +365,17 @@ export const resolveThemeVisualConfig = (theme, sourceLabel = siteThemeLabel) =>
360
365
  typography: Object.freeze({
361
366
  fontFamily: readFontFamily(rawTypographyConfig, 'fontFamily', 'typography', defaultFontFamily, sourceLabel),
362
367
  }),
368
+ shape: Object.freeze({
369
+ name: shape,
370
+ radiusSmall: shape === 'square' ? '0' : '2px',
371
+ radiusMedium: shape === 'square' ? '0' : '6px',
372
+ radiusLarge: shape === 'square' ? '0' : '8px',
373
+ }),
363
374
  });
364
375
  };
365
376
 
366
- export const resolveThemeNavigationConfig = (theme, sourceLabel = siteThemeLabel) => {
367
- const resolvedTheme = assertObject(resolveThemeConfig(theme, sourceLabel), 'theme YAML', sourceLabel);
368
- const rawNavigation = assertObject(resolvedTheme.navigation ?? {}, 'navigation', sourceLabel);
377
+ export const resolveNavigationConfig = (config, sourceLabel = siteConfigLabel) => {
378
+ const rawNavigation = assertObject(config.navigation ?? {}, 'navigation', sourceLabel);
369
379
 
370
380
  return Object.freeze({
371
381
  mode: readEnum(
@@ -386,7 +396,7 @@ export const projectConfig = Object.freeze({
386
396
  }),
387
397
  ...resolveThemeVisualConfig(rawTheme, siteThemeLabel),
388
398
  navigation: Object.freeze({
389
- ...resolveThemeNavigationConfig(rawTheme, siteThemeLabel),
399
+ ...resolveNavigationConfig(rawConfig, siteConfigLabel),
390
400
  scrollBehavior: readEnum(
391
401
  rawConfig,
392
402
  'scrollBehavior',
@@ -11,13 +11,15 @@ const isDateOnly = (value) => {
11
11
 
12
12
  const textAlign = z.enum(['left', 'center', 'right']).describe('Text alignment. Omit an override to keep the active typography profile.');
13
13
  const textSize = z.enum(['small', 'medium', 'large', 'xlarge']).describe('Text size from the active typography system. Omit an override to keep the profile value.');
14
- const textWidth = z.enum(['narrow', 'normal', 'wide']).describe('Maximum width of body text. Omit an override to keep the profile value.');
14
+ const textWidth = z.enum(['narrow', 'normal', 'wide']).describe('Maximum width of body text. Omit this to keep the inherited page setting.');
15
15
  const headingWeight = z.union([z.literal(400), z.literal(500), z.literal(600), z.literal(700)]).describe('CSS font weight.');
16
16
  const typographyProfile = z.enum(['restrained', 'dense', 'reading', 'statement']).describe('Coordinated typography defaults. Omit this to use the selected preset.');
17
17
  const themePreset = z.enum(themePresetNames).describe('Complete Norna visual preset. Start here and add overrides only when needed.');
18
18
  const presentationPalette = z.enum(['dark', 'light', 'paper']).describe('Coordinated site color palette. Omit this to use the selected preset.');
19
- const sectionSurface = z.enum(['base', 'soft', 'emphasis']).describe('Semantic section surface from the active palette.');
20
19
  const spacingDensity = z.enum(['compact', 'normal', 'airy']).describe('Coordinated spacing density. Omit this to use the selected preset.');
20
+ const contentSpacing = z.enum(['compact', 'normal', 'spacious']).describe('Vertical spacing between page sections and structured content blocks.');
21
+ const backgroundPattern = z.enum(['uniform', 'alternating', 'cycling']).describe('How sections cycle through the active palette surfaces.');
22
+ const shapeProfile = z.enum(['square', 'soft']).describe('Site-wide corner treatment for interface and content surfaces.');
21
23
  const navigationMode = z.enum(navigationModeNames).describe('Site-wide navigation model. Automatic selects from the site structure.');
22
24
  const lineHeight = z.number()
23
25
  .min(1, 'Use a unitless line height of at least 1.')
@@ -73,7 +75,6 @@ const headingLevelsPresentationOverride = z.object({
73
75
  }).strict().describe('Heading overrides by Markdown level.');
74
76
  const bodyPresentationOverride = z.object({
75
77
  ...commonTextPresentationOverride,
76
- width: textWidth.optional().describe('Maximum body-text line length.'),
77
78
  paragraphSpacing: cssLength.optional().describe('Vertical space between body paragraphs.'),
78
79
  }).strict().describe('Focused overrides for body text.');
79
80
  const captionPresentationOverride = z.object({
@@ -121,23 +122,36 @@ const themeLayoutSpacing = z.object({
121
122
  sectionGap: responsiveCssLength.optional().describe('Vertical separation between page sections.'),
122
123
  }).strict().describe('Fine-grained spacing overrides applied after layout density.');
123
124
  const themeLayout = z.object({
124
- density: spacingDensity.optional().describe('Overall spacing density. Omit this to keep the selected preset.'),
125
+ contentSpacing: contentSpacing.optional().describe('Page content spacing. Omit this to keep the selected preset or inherited page setting.'),
126
+ textWidth: textWidth.optional().describe('Body-text line length. Omit this to keep the selected preset or inherited page setting.'),
125
127
  pageWidth: visualCssLength.optional().describe('Maximum width of the site layout. Omit this to keep the selected preset.'),
126
128
  gutter: responsiveCssLength.optional().describe('Horizontal page gutter as one value or separate desktop and mobile values.'),
127
129
  spacing: themeLayoutSpacing.optional().describe('Fine-grained spacing overrides.'),
128
130
  }).strict().describe('Optional layout overrides applied after the preset.');
131
+ const pageThemeLayout = z.object({
132
+ contentSpacing: contentSpacing.optional().describe('Page content spacing. Descendant pages inherit this value.'),
133
+ textWidth: textWidth.optional().describe('Body-text line length. Descendant pages inherit this value.'),
134
+ }).strict().refine(
135
+ (value) => value.contentSpacing !== undefined || value.textWidth !== undefined,
136
+ 'Specify contentSpacing, textWidth, or both.',
137
+ ).describe('Page-local layout settings inherited by descendant pages.');
129
138
  const themeImages = z.object({
130
139
  width: visualCssLength.optional().describe('Maximum managed-image width. Omit this to keep the selected preset.'),
131
140
  maxAvailableWidthPercent: responsivePercent.optional().describe('Maximum percentage of available horizontal space used by managed images.'),
132
141
  maxAvailableHeightPercent: responsivePercent.optional().describe('Maximum percentage of viewport height used by managed images.'),
133
142
  }).strict().describe('Optional defaults for managed image presentation.');
134
- const themeNavigation = z.object({
135
- mode: navigationMode.optional().describe('Navigation model. Omit this to keep the selected preset.'),
136
- }).strict().describe('Site-wide navigation behavior. This may be set only in the root theme.');
137
- const sectionSurfaces = z.array(sectionSurface).min(1).max(3).refine(
138
- (value) => new Set(value).size === value.length,
139
- 'Each section surface may appear only once.',
140
- ).describe('Surface sequence cycled across page sections.');
143
+ const configNavigation = z.object({
144
+ mode: navigationMode.optional().describe('Navigation model. Omit this to let Norna choose from the site structure.'),
145
+ }).strict().describe('Site-wide navigation behavior.');
146
+ const themeSections = z.object({
147
+ backgroundPattern: backgroundPattern.optional().describe('Section background sequence. Omit this to keep the selected preset or inherited page setting.'),
148
+ }).strict().describe('Defaults for page section presentation.');
149
+ const pageThemeSections = z.object({
150
+ backgroundPattern: backgroundPattern.optional().describe('Section background sequence inherited by descendant pages.'),
151
+ }).strict().refine(
152
+ (value) => value.backgroundPattern !== undefined,
153
+ 'Specify backgroundPattern.',
154
+ ).describe('Page-local section presentation inherited by descendant pages.');
141
155
  const pageNavigation = z.object({
142
156
  listed: z.boolean().optional().default(true).describe('List this page in site navigation. The page remains public when false.'),
143
157
  }).strict();
@@ -167,6 +181,7 @@ const sitewideFooter = z.object({
167
181
  const configShape = {
168
182
  url: z.string().url().describe('Absolute public URL for the built site.'),
169
183
  language: z.string().regex(/^(?:en|sv)(?:-[a-zA-Z0-9]+)*$/).optional().describe('Site language tag using Norna\'s English or Swedish interface text; the default is en.'),
184
+ navigation: configNavigation.optional(),
170
185
  scrollBehavior: z.enum(['instant', 'smooth']).optional().default('instant').describe('Use instant anchors by default or the browser\'s native smooth scrolling.'),
171
186
  };
172
187
 
@@ -177,12 +192,18 @@ const siteShape = {
177
192
 
178
193
  const themeVisualShape = {
179
194
  preset: themePreset.optional().describe('Complete visual starting point. Add only the overrides the site actually needs.'),
180
- navigation: themeNavigation.optional(),
195
+ shape: shapeProfile.optional().describe('Site-wide shape profile. Omit this to use the selected preset.'),
181
196
  layout: themeLayout.optional(),
182
197
  images: themeImages.optional(),
183
198
  typography: themeTypography.optional(),
184
199
  palette: presentationPalette.optional(),
185
- sectionSurfaces: sectionSurfaces.optional().describe('Surface sequence cycled through page sections. Omit this to keep the selected preset.'),
200
+ sections: themeSections.optional(),
201
+ };
202
+
203
+ const pageThemeShape = {
204
+ layout: pageThemeLayout.optional(),
205
+ images: themeImages.optional(),
206
+ sections: pageThemeSections.optional(),
186
207
  };
187
208
 
188
209
  const sitewideShape = {
@@ -196,6 +217,7 @@ export const schemaTopLevelKeys = Object.freeze({
196
217
  content: Object.freeze(Object.keys(siteShape)),
197
218
  sitewide: Object.freeze(Object.keys(sitewideShape)),
198
219
  theme: Object.freeze(Object.keys(themeVisualShape)),
220
+ pageTheme: Object.freeze(Object.keys(pageThemeShape)),
199
221
  });
200
222
 
201
223
  export const configSchema = z.object(configShape).strict()
@@ -203,6 +225,12 @@ export const configSchema = z.object(configShape).strict()
203
225
  export const siteSchema = z.object(siteShape).strict()
204
226
  .describe('Frontmatter for a homepage or additional page content.md file.');
205
227
  export const themeVisualSchema = z.object(themeVisualShape).strict()
206
- .describe('Visual settings for a site or page. A page theme replaces the inherited visual theme; navigation logo settings remain site-wide.');
228
+ .describe('Site-wide visual identity and default page presentation. Presets may be selected only in the root theme.');
229
+ export const pageThemeSchema = z.object(pageThemeShape).strict()
230
+ .refine(
231
+ (value) => value.layout !== undefined || value.images !== undefined || value.sections !== undefined,
232
+ 'Specify layout, images, sections, or a combination of them.',
233
+ )
234
+ .describe('Limited page presentation overrides inherited by descendant pages. Site colors, shapes, typography and navigation remain global.');
207
235
  export const sitewideSchema = z.object(sitewideShape).strict()
208
236
  .describe('Editorial content and optional navigation logo display settings shared by every page.');