@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
@@ -151,6 +151,19 @@ const addConfigHelp = (jsonSchema) => {
151
151
  'Sets the page language and selects Norna\'s built-in English or Swedish interface text. The default is `en`.',
152
152
  documentationLink('Language reference', 'configuration.md', 'language'),
153
153
  ], ['en', 'sv', 'en-GB', 'sv-SE']);
154
+ addHelp(jsonSchema, 'navigation', [
155
+ yamlExample('navigation:\n mode: automatic'),
156
+ 'Sets one navigation model for the complete site. `automatic` selects from the page hierarchy.',
157
+ documentationLink('Navigation reference', 'pages.md', 'navigation'),
158
+ ]);
159
+ addFieldHelp(
160
+ jsonSchema,
161
+ 'navigation.mode',
162
+ 'navigation:\n mode: automatic',
163
+ 'pages.md',
164
+ 'navigation',
165
+ ['automatic', 'sections', 'top', 'tree'],
166
+ );
154
167
  addHelp(jsonSchema, 'scrollBehavior', [
155
168
  yamlExample('scrollBehavior: smooth'),
156
169
  'Controls same-page anchor movement. `instant` is the default; `smooth` uses the browser\'s native smooth scrolling.',
@@ -166,25 +179,17 @@ const addThemeHelp = (jsonSchema) => {
166
179
  ].join('\n\n');
167
180
  addHelp(jsonSchema, 'preset', [
168
181
  yamlExample('preset: documentation'),
169
- 'Selects coordinated navigation, layout, image sizing, typography, palette and section surfaces. This is the normal starting point for a theme.',
182
+ 'Selects coordinated shapes, layout, image sizing, typography, palette and section backgrounds. This is the normal starting point for a theme.',
170
183
  documentationLink('Compare theme presets', 'theme.md', 'theme-presets'),
171
184
  ]);
172
- addHelp(jsonSchema, 'navigation', [
173
- yamlExample('navigation:\n mode: automatic'),
174
- 'Sets the site-wide navigation model. Only the root theme may override this preset value.',
185
+ addHelp(jsonSchema, 'shape', [
186
+ yamlExample('shape: soft'),
187
+ 'Chooses one consistent corner treatment for navigation, cards and framed content throughout the site.',
175
188
  documentationLink('Theme reference', 'theme.md'),
176
- ]);
177
- addFieldHelp(
178
- jsonSchema,
179
- 'navigation.mode',
180
- 'navigation:\n mode: automatic',
181
- 'theme.md',
182
- undefined,
183
- ['automatic', 'sections', 'top', 'tree'],
184
- );
189
+ ], ['square', 'soft']);
185
190
  addHelp(jsonSchema, 'layout', [
186
- yamlExample('layout:\n density: compact\n pageWidth: 72rem'),
187
- 'Overrides page width, gutters and structural spacing after the selected preset.',
191
+ yamlExample('layout:\n contentSpacing: compact\n textWidth: narrow'),
192
+ 'Overrides text width, content spacing, page width and gutters after the selected preset.',
188
193
  documentationLink('Layout reference', 'theme.md', 'layout'),
189
194
  ]);
190
195
  addHelp(jsonSchema, 'images', [
@@ -202,14 +207,16 @@ const addThemeHelp = (jsonSchema) => {
202
207
  'Chooses a coordinated color system for the page frame, navigation, footer and section surfaces.',
203
208
  documentationLink('Palette and section surfaces', 'theme.md', 'palette-and-section-surfaces'),
204
209
  ], ['dark', 'light', 'paper']);
205
- addHelp(jsonSchema, 'sectionSurfaces', [
206
- yamlExample('sectionSurfaces: [base, soft, emphasis]'),
207
- 'Lists one to three semantic surfaces. Norna cycles through them in section order; omit this field to keep the preset sequence.',
210
+ addHelp(jsonSchema, 'sections', [
211
+ yamlExample('sections:\n backgroundPattern: alternating'),
212
+ 'Chooses whether section backgrounds stay uniform, alternate between two surfaces or cycle through three.',
208
213
  documentationLink('Palette and section surfaces', 'theme.md', 'palette-and-section-surfaces'),
209
- ], [['base'], ['base', 'soft'], ['base', 'soft', 'emphasis']]);
214
+ ]);
215
+ addFieldHelp(jsonSchema, 'sections.backgroundPattern', 'sections:\n backgroundPattern: alternating', 'theme.md', 'palette-and-section-surfaces', ['uniform', 'alternating', 'cycling']);
210
216
 
211
217
  const layoutFields = [
212
- ['layout.density', 'layout:\n density: compact'],
218
+ ['layout.contentSpacing', 'layout:\n contentSpacing: compact'],
219
+ ['layout.textWidth', 'layout:\n textWidth: narrow'],
213
220
  ['layout.pageWidth', 'layout:\n pageWidth: 72rem'],
214
221
  ['layout.gutter', 'layout:\n gutter:\n desktop: clamp(1.25rem, 4vw, 3rem)\n mobile: 1rem'],
215
222
  ['layout.gutter.desktop', 'layout:\n gutter:\n desktop: clamp(1.25rem, 4vw, 3rem)'],
@@ -301,9 +308,9 @@ const addThemeHelp = (jsonSchema) => {
301
308
  ['typography.fontFamily', 'typography:\n fontFamily: "Arial, sans-serif"'],
302
309
  ['typography.profile', 'typography:\n profile: reading'],
303
310
  ['typography.rhythm', 'typography:\n rhythm: normal'],
304
- ['typography.overrides', 'typography:\n overrides:\n body:\n width: narrow'],
311
+ ['typography.overrides', 'typography:\n overrides:\n body:\n lineHeight: 1.55'],
305
312
  ['typography.overrides.headings', 'typography:\n overrides:\n headings:\n h2:\n size: medium'],
306
- ['typography.overrides.body', 'typography:\n overrides:\n body:\n width: narrow'],
313
+ ['typography.overrides.body', 'typography:\n overrides:\n body:\n lineHeight: 1.55'],
307
314
  ['typography.overrides.caption', 'typography:\n overrides:\n caption:\n size: small'],
308
315
  ];
309
316
  for (const [propertyPath, example] of typographyFields) {
@@ -346,7 +353,6 @@ const addThemeHelp = (jsonSchema) => {
346
353
  textOverrideHelp(propertyPath, `${prefix}\n `, true);
347
354
  }
348
355
  textOverrideHelp('typography.overrides.body', 'typography:\n overrides:\n body:');
349
- addFieldHelp(jsonSchema, 'typography.overrides.body.width', 'typography:\n overrides:\n body:\n width: narrow', 'typography.md', 'configuration-shape');
350
356
  addFieldHelp(jsonSchema, 'typography.overrides.body.paragraphSpacing', 'typography:\n overrides:\n body:\n paragraphSpacing: 1em', 'typography.md', 'configuration-shape');
351
357
  textOverrideHelp('typography.overrides.caption', 'typography:\n overrides:\n caption:');
352
358
  addFieldHelp(jsonSchema, 'typography.overrides.caption.spacingBefore', 'typography:\n overrides:\n caption:\n spacingBefore: 0.5em', 'typography.md', 'configuration-shape');
@@ -359,9 +365,8 @@ const addThemeHelp = (jsonSchema) => {
359
365
  weight: '${2:600}',
360
366
  },
361
367
  },
362
- body: {
363
- width: '${3:narrow}',
364
- lineHeight: '${4:1.55}',
368
+ body: {
369
+ lineHeight: '${3:1.55}',
365
370
  },
366
371
  },
367
372
  description: 'Start a focused set of heading and body-text overrides.',
@@ -370,6 +375,44 @@ const addThemeHelp = (jsonSchema) => {
370
375
  })]);
371
376
  };
372
377
 
378
+ const addPageThemeHelp = (jsonSchema) => {
379
+ jsonSchema.markdownDescription = [
380
+ yamlExample('layout:\n textWidth: narrow\n contentSpacing: compact'),
381
+ 'A page theme may adjust only page layout, managed-image sizing and the section background pattern. Descendant pages inherit these values. Site colors, shapes and typography stay consistent.',
382
+ documentationLink('Page theme reference', 'theme.md', 'page-themes'),
383
+ ].join('\n\n');
384
+ addHelp(jsonSchema, 'layout', [
385
+ yamlExample('layout:\n textWidth: narrow\n contentSpacing: compact'),
386
+ 'Adjusts body-text line length and vertical content spacing for this page and its descendants.',
387
+ documentationLink('Page theme reference', 'theme.md', 'page-themes'),
388
+ ]);
389
+ addFieldHelp(jsonSchema, 'layout.textWidth', 'layout:\n textWidth: narrow', 'theme.md', 'page-themes', ['narrow', 'normal', 'wide']);
390
+ addFieldHelp(jsonSchema, 'layout.contentSpacing', 'layout:\n contentSpacing: compact', 'theme.md', 'page-themes', ['compact', 'normal', 'spacious']);
391
+ addHelp(jsonSchema, 'images', [
392
+ yamlExample('images:\n width: 900px'),
393
+ 'Adjusts managed-image sizing for this page and its descendants.',
394
+ documentationLink('Image sizing reference', 'theme.md', 'image-sizing'),
395
+ ]);
396
+ const imageFields = [
397
+ ['images.width', 'images:\n width: 900px'],
398
+ ['images.maxAvailableWidthPercent', 'images:\n maxAvailableWidthPercent:\n desktop: 100\n mobile: 100'],
399
+ ['images.maxAvailableWidthPercent.desktop', 'images:\n maxAvailableWidthPercent:\n desktop: 100'],
400
+ ['images.maxAvailableWidthPercent.mobile', 'images:\n maxAvailableWidthPercent:\n mobile: 100'],
401
+ ['images.maxAvailableHeightPercent', 'images:\n maxAvailableHeightPercent:\n desktop: 74\n mobile: 68'],
402
+ ['images.maxAvailableHeightPercent.desktop', 'images:\n maxAvailableHeightPercent:\n desktop: 74'],
403
+ ['images.maxAvailableHeightPercent.mobile', 'images:\n maxAvailableHeightPercent:\n mobile: 68'],
404
+ ];
405
+ for (const [propertyPath, example] of imageFields) {
406
+ addFieldHelp(jsonSchema, propertyPath, example, 'theme.md', 'image-sizing');
407
+ }
408
+ addHelp(jsonSchema, 'sections', [
409
+ yamlExample('sections:\n backgroundPattern: alternating'),
410
+ 'Adjusts the section background sequence for this page and its descendants.',
411
+ documentationLink('Page theme reference', 'theme.md', 'page-themes'),
412
+ ]);
413
+ addFieldHelp(jsonSchema, 'sections.backgroundPattern', 'sections:\n backgroundPattern: alternating', 'theme.md', 'page-themes', ['uniform', 'alternating', 'cycling']);
414
+ };
415
+
373
416
  const addSitewideHelp = (jsonSchema) => {
374
417
  jsonSchema.markdownDescription = [
375
418
  yamlExample('footer:\n copyrightMessage: Copyright Example Owner.'),
@@ -469,6 +512,7 @@ export const applySchemaEditorMetadata = (filename, jsonSchema) => {
469
512
  addConfigHelp(jsonSchema);
470
513
  }
471
514
  if (filename === 'theme.schema.json') addThemeHelp(jsonSchema);
515
+ if (filename === 'page-theme.schema.json') addPageThemeHelp(jsonSchema);
472
516
  if (filename === 'sitewide-content.schema.json') addSitewideHelp(jsonSchema);
473
517
  if (filename === 'content-frontmatter.schema.json') addContentHelp(jsonSchema);
474
518
  return jsonSchema;
@@ -61,6 +61,20 @@ export const schemaValueDefinitions = Object.freeze([
61
61
  light: option('Light', 'White and cool light surfaces with dark text.'),
62
62
  paper: option('Paper', 'Warm off-white surfaces with dark text.'),
63
63
  }),
64
+ definition(['square', 'soft'], {
65
+ square: option('Square', 'Use square corners for navigation, cards and framed content.'),
66
+ soft: option('Soft', 'Use restrained rounded corners consistently across the site.'),
67
+ }),
68
+ definition(['uniform', 'alternating', 'cycling'], {
69
+ uniform: option('Uniform', 'Use the palette base surface for every section.'),
70
+ alternating: option('Alternating', 'Alternate between the palette base and soft surfaces.'),
71
+ cycling: option('Cycling', 'Cycle through the palette base, soft and emphasis surfaces.'),
72
+ }),
73
+ definition(['compact', 'normal', 'spacious'], {
74
+ compact: option('Compact', 'Use restrained spacing between sections and structured blocks.'),
75
+ normal: option('Normal', 'Use balanced content spacing.'),
76
+ spacious: option('Spacious', 'Use more expansive spacing between sections and structured blocks.'),
77
+ }),
64
78
  definition(['base', 'soft', 'emphasis'], {
65
79
  base: option('Base', 'Use the palette\'s normal page surface.'),
66
80
  soft: option('Soft', 'Use the palette\'s subtle contrasting surface.'),
@@ -2,10 +2,9 @@ import { access, readdir, readFile } from 'node:fs/promises';
2
2
  import path from 'node:path';
3
3
  import {
4
4
  siteContentLabel,
5
- siteContentPath,
6
5
  siteDir,
7
6
  siteDirLabel,
8
- siteImagesDir,
7
+ homePageDirectory,
9
8
  siteImagesLabel,
10
9
  sitePagesDir,
11
10
  sitePagesLabel,
@@ -25,7 +24,7 @@ const knownConfigTopLevelFrontmatterKeys = new Set(schemaTopLevelKeys.config);
25
24
  const knownContentTopLevelFrontmatterKeys = new Set(schemaTopLevelKeys.content);
26
25
  const knownThemeTopLevelFrontmatterKeys = new Set(schemaTopLevelKeys.theme);
27
26
  const knownPageThemeTopLevelFrontmatterKeys = new Set(
28
- [...knownThemeTopLevelFrontmatterKeys].filter((key) => key !== 'navigation'),
27
+ schemaTopLevelKeys.pageTheme,
29
28
  );
30
29
  const knownSitewideTopLevelFrontmatterKeys = new Set(schemaTopLevelKeys.sitewide);
31
30
  const knownNestedFrontmatterKeys = new Set([
@@ -35,7 +34,7 @@ const knownNestedFrontmatterKeys = new Set([
35
34
  'blockGap',
36
35
  'caption',
37
36
  'carousel',
38
- 'density',
37
+ 'contentSpacing',
39
38
  'desktop',
40
39
  'fontFamily',
41
40
  'from',
@@ -67,11 +66,14 @@ const knownNestedFrontmatterKeys = new Set([
67
66
  'profile',
68
67
  'rhythm',
69
68
  'sectionGap',
70
- 'sectionSurfaces',
69
+ 'backgroundPattern',
70
+ 'sections',
71
+ 'shape',
71
72
  'size',
72
73
  'spacingAfter',
73
74
  'spacingBefore',
74
75
  'theme',
76
+ 'textWidth',
75
77
  'typography',
76
78
  'until',
77
79
  'visible',
@@ -83,6 +85,18 @@ export const toPosixPath = (filePath) => filePath.split(path.sep).join('/');
83
85
  const fileExists = async (filePath) => access(filePath).then(() => true, () => false);
84
86
 
85
87
  export const getContentFiles = async () => {
88
+ const legacyContentPath = path.join(siteDir, 'content.md');
89
+ const legacyImagesPath = path.join(siteDir, 'images');
90
+ const legacyPaths = [];
91
+ if (await fileExists(legacyContentPath)) legacyPaths.push(`${siteDirLabel}/content.md`);
92
+ if (await fileExists(legacyImagesPath)) legacyPaths.push(`${siteDirLabel}/images`);
93
+ if (legacyPaths.length > 0) {
94
+ throw new Error([
95
+ `The old root-page structure is no longer supported: ${legacyPaths.join(', ')}.`,
96
+ `Move the homepage content to ${sitePagesLabel}/${homePageDirectory}/content.md and its images to ${sitePagesLabel}/${homePageDirectory}/images/.`,
97
+ ].join('\n'));
98
+ }
99
+
86
100
  const legacyRoutesDir = path.join(siteDir, 'routes');
87
101
  const legacyRoutes = await readdir(legacyRoutesDir).catch((error) => {
88
102
  if (error?.code === 'ENOENT') return null;
@@ -92,22 +106,7 @@ export const getContentFiles = async () => {
92
106
  throw new Error(`${siteDirLabel}/routes is no longer supported. Rename it to ${sitePagesLabel} and use NNN-page-id directory names.`);
93
107
  }
94
108
 
95
- const contentFiles = [{
96
- contentLabel: siteContentLabel,
97
- contentPath: siteContentPath,
98
- imagesDir: siteImagesDir,
99
- imagesLabel: siteImagesLabel,
100
- isHome: true,
101
- pageDirectory: null,
102
- pageDirectories: [],
103
- pageId: null,
104
- pageIds: [],
105
- pageOrder: 0,
106
- pageOrders: [],
107
- pagePath: '',
108
- parentPagePath: null,
109
- depth: 0,
110
- }];
109
+ const contentFiles = [];
111
110
  const collectPageContentFiles = async (pagesDir, pagesLabel, parentPageDirectory = '') => {
112
111
  const pageEntries = await readdir(pagesDir, { withFileTypes: true }).catch((error) => {
113
112
  if (error?.code === 'ENOENT') {
@@ -138,16 +137,36 @@ export const getContentFiles = async () => {
138
137
  continue;
139
138
  }
140
139
  const pageMetadata = parsePageDirectoryPath(pageDirectory, pageLabel);
140
+ const isHome = pageDirectory === homePageDirectory;
141
141
 
142
142
  contentFiles.push({
143
143
  contentLabel: `${pageLabel}/content.md`,
144
144
  contentPath: pageContentPath,
145
145
  imagesDir: path.join(pageDir, 'images'),
146
146
  imagesLabel: `${pageLabel}/images`,
147
- isHome: false,
147
+ isHome,
148
148
  ...pageMetadata,
149
+ pagePath: isHome ? '' : pageMetadata.pagePath,
150
+ parentPagePath: isHome ? null : pageMetadata.parentPagePath,
149
151
  });
150
152
 
153
+ if (isHome) {
154
+ const homePagesDir = path.join(pageDir, 'pages');
155
+ const homeChildEntries = await readdir(homePagesDir, { withFileTypes: true }).catch((error) => {
156
+ if (error?.code === 'ENOENT') return [];
157
+ throw error;
158
+ });
159
+ const homeChildDirectories = homeChildEntries.filter((childEntry) => childEntry.isDirectory());
160
+ if (homeChildDirectories.length > 0) {
161
+ throw new Error([
162
+ `${pageLabel} is the homepage and cannot contain child pages.`,
163
+ `Move these page directories beside ${homePageDirectory} under ${sitePagesLabel}/, or below another non-home page:`,
164
+ ...homeChildDirectories.map(({ name }) => `- ${pageLabel}/pages/${name}`),
165
+ ].join('\n'));
166
+ }
167
+ continue;
168
+ }
169
+
151
170
  await collectPageContentFiles(
152
171
  path.join(pageDir, 'pages'),
153
172
  `${pageLabel}/pages`,
@@ -157,6 +176,9 @@ export const getContentFiles = async () => {
157
176
  };
158
177
 
159
178
  await collectPageContentFiles(sitePagesDir, sitePagesLabel);
179
+ if (!contentFiles.some(({ isHome }) => isHome)) {
180
+ throw new Error(`Homepage content is missing. Create ${siteContentLabel}.`);
181
+ }
160
182
 
161
183
  return contentFiles;
162
184
  };
@@ -303,9 +325,12 @@ export const validateFrontmatterStructure = (frontmatter, addIssue, {
303
325
  fix = 'Put local image references in norna-image-stack or norna-image-carousel blocks in the Markdown body.';
304
326
  } else if (fileKind === 'content' && knownThemeTopLevelFrontmatterKeys.has(key)) {
305
327
  fix = `Move "${key}:" to theme.yaml. Visual settings do not belong in content frontmatter.`;
306
- } else if (fileKind === 'page theme' && key === 'navigation') {
307
- message = `Frontmatter line ${lineNumber}: page themes may not define navigation.`;
308
- fix = 'Set the site-wide navigation mode under "navigation:" in the root theme.yaml.';
328
+ } else if ((fileKind === 'theme' || fileKind === 'page theme') && key === 'navigation') {
329
+ message = `Frontmatter line ${lineNumber}: navigation is technical, site-wide configuration and does not belong in ${fileKind}. Set it in config.yaml.`;
330
+ fix = 'Set the navigation mode under "navigation:" in config.yaml.';
331
+ } else if (fileKind === 'page theme' && ['preset', 'shape', 'palette', 'typography'].includes(key)) {
332
+ message = `Frontmatter line ${lineNumber}: page themes may not define site-wide visual identity through "${key}".`;
333
+ fix = `Move "${key}:" to the root theme.yaml. Page themes may set only layout.textWidth, layout.contentSpacing, images, and sections.backgroundPattern.`;
309
334
  } else if ((fileKind === 'theme' || fileKind === 'page theme') && ['logo', 'site'].includes(key)) {
310
335
  message = `Frontmatter line ${lineNumber}: ${fileKind} may not define navigation logo settings. Optional logo display settings belong under "logo:" in ${sitewideContentLabel}.`;
311
336
  fix = `Move only the logo display settings under "logo:" in ${sitewideContentLabel}; the homepage Markdown H1 supplies navigation text and logo alternative text.`;
@@ -0,0 +1 @@
1
+ export const homePageDirectory = '000-home';
@@ -1,10 +1,12 @@
1
1
  import { existsSync } from 'node:fs';
2
2
  import path from 'node:path';
3
3
  import { fileURLToPath } from 'node:url';
4
+ import { homePageDirectory } from './site-conventions.mjs';
4
5
 
5
6
  const siteDirectoryEnvName = 'NORNA_SITE_DIR';
6
7
  const invocationRootEnvName = 'NORNA_INVOCATION_ROOT';
7
8
  const defaultSiteDirectory = 'site';
9
+ export { homePageDirectory };
8
10
 
9
11
  const currentFile = fileURLToPath(import.meta.url);
10
12
  const currentDirectory = path.dirname(currentFile);
@@ -23,7 +25,10 @@ export const invocationRoot = configuredInvocationRoot
23
25
  const hasSiteFilesInDirectory = (siteDir) => {
24
26
  return (
25
27
  existsSync(path.join(siteDir, 'config.yaml'))
26
- && existsSync(path.join(siteDir, 'content.md'))
28
+ && (
29
+ existsSync(path.join(siteDir, 'pages', homePageDirectory, 'content.md'))
30
+ || existsSync(path.join(siteDir, 'content.md'))
31
+ )
27
32
  );
28
33
  };
29
34
 
@@ -116,10 +121,11 @@ const getPathLabel = (filePath) => {
116
121
  export const siteDir = resolvedSitePaths.siteDir;
117
122
  export const siteConfigPath = path.join(siteDir, 'config.yaml');
118
123
  export const siteThemePath = path.join(siteDir, 'theme.yaml');
119
- export const siteContentPath = path.join(siteDir, 'content.md');
120
124
  export const sitewideContentPath = path.join(siteDir, 'sitewide-content.yaml');
121
- export const siteImagesDir = path.join(siteDir, 'images');
122
125
  export const sitePagesDir = path.join(siteDir, 'pages');
126
+ export const siteHomePageDir = path.join(sitePagesDir, homePageDirectory);
127
+ export const siteContentPath = path.join(siteHomePageDir, 'content.md');
128
+ export const siteImagesDir = path.join(siteHomePageDir, 'images');
123
129
  export const sitePublicDir = path.join(siteDir, 'public');
124
130
  export const siteStateDir = path.join(siteDir, '.norna');
125
131
  export const astroPublicDir = path.join(siteStateDir, 'public');
@@ -12,6 +12,7 @@ import {
12
12
  } from './site-paths.mjs';
13
13
  import { resolveThemePresentation } from './presentation.mjs';
14
14
  import { resolveThemeConfig } from './theme-presets.mjs';
15
+ import { pageThemeSchema, themeVisualSchema } from './schema-definitions.mjs';
15
16
  import { parseYamlConfig } from './yaml-config.mjs';
16
17
 
17
18
  export const readThemeConfig = async () => {
@@ -23,6 +24,7 @@ export const readThemeConfig = async () => {
23
24
  throw error;
24
25
  });
25
26
  const config = parseYamlConfig(themeFile, siteThemeLabel, {
27
+ schema: themeVisualSchema,
26
28
  validateStructure: validateThemeYamlStructure,
27
29
  });
28
30
  resolveThemeConfig(config, siteThemeLabel);
@@ -64,10 +66,9 @@ export const validatePageThemeFiles = async () => {
64
66
  for (const file of files) {
65
67
  const source = await readFile(file.path, 'utf8');
66
68
  const config = parseYamlConfig(source, file.label, {
69
+ schema: pageThemeSchema,
67
70
  validateStructure: validatePageThemeYamlStructure,
68
71
  });
69
- resolveThemeConfig(config, file.label);
70
- resolveThemePresentation(config, file.label);
71
72
 
72
73
  configs.push({ ...file, config });
73
74
  }
@@ -2,10 +2,12 @@ import { mergeDeep } from './typography.mjs';
2
2
 
3
3
  const themePresetConfigurations = Object.freeze({
4
4
  portfolio: Object.freeze({
5
- navigation: { mode: 'automatic' },
5
+ shape: 'square',
6
6
  layout: {
7
- density: 'normal',
7
+ contentSpacing: 'normal',
8
+ textWidth: 'wide',
8
9
  pageWidth: '1240px',
10
+ localNavigationGap: 'clamp(1.5rem, 3vw, 3rem)',
9
11
  gutter: {
10
12
  desktop: 'clamp(1.25rem, 4vw, 3rem)',
11
13
  mobile: '1rem',
@@ -22,13 +24,15 @@ const themePresetConfigurations = Object.freeze({
22
24
  rhythm: 'normal',
23
25
  },
24
26
  palette: 'dark',
25
- sectionSurfaces: ['base'],
27
+ sections: { backgroundPattern: 'uniform' },
26
28
  }),
27
29
  documentation: Object.freeze({
28
- navigation: { mode: 'automatic' },
30
+ shape: 'soft',
29
31
  layout: {
30
- density: 'compact',
32
+ contentSpacing: 'compact',
33
+ textWidth: 'narrow',
31
34
  pageWidth: '1240px',
35
+ localNavigationGap: 'clamp(1.5rem, 3vw, 3rem)',
32
36
  gutter: {
33
37
  desktop: 'clamp(1.25rem, 4vw, 3rem)',
34
38
  mobile: '1rem',
@@ -45,13 +49,15 @@ const themePresetConfigurations = Object.freeze({
45
49
  rhythm: 'compact',
46
50
  },
47
51
  palette: 'paper',
48
- sectionSurfaces: ['base', 'soft'],
52
+ sections: { backgroundPattern: 'alternating' },
49
53
  }),
50
54
  project: Object.freeze({
51
- navigation: { mode: 'automatic' },
55
+ shape: 'soft',
52
56
  layout: {
53
- density: 'compact',
57
+ contentSpacing: 'compact',
58
+ textWidth: 'normal',
54
59
  pageWidth: '1120px',
60
+ localNavigationGap: 'clamp(1.5rem, 3vw, 3rem)',
55
61
  gutter: {
56
62
  desktop: 'clamp(1.25rem, 4vw, 3rem)',
57
63
  mobile: '1rem',
@@ -68,13 +74,15 @@ const themePresetConfigurations = Object.freeze({
68
74
  rhythm: 'compact',
69
75
  },
70
76
  palette: 'light',
71
- sectionSurfaces: ['base', 'soft'],
77
+ sections: { backgroundPattern: 'alternating' },
72
78
  }),
73
79
  statement: Object.freeze({
74
- navigation: { mode: 'automatic' },
80
+ shape: 'square',
75
81
  layout: {
76
- density: 'airy',
82
+ contentSpacing: 'spacious',
83
+ textWidth: 'normal',
77
84
  pageWidth: '1280px',
85
+ localNavigationGap: 'clamp(1.5rem, 3vw, 3rem)',
78
86
  gutter: {
79
87
  desktop: 'clamp(1.5rem, 5vw, 4rem)',
80
88
  mobile: '1rem',
@@ -91,7 +99,7 @@ const themePresetConfigurations = Object.freeze({
91
99
  rhythm: 'airy',
92
100
  },
93
101
  palette: 'paper',
94
- sectionSurfaces: ['base', 'emphasis'],
102
+ sections: { backgroundPattern: 'cycling' },
95
103
  }),
96
104
  });
97
105
 
@@ -158,6 +166,30 @@ export const resolveThemeConfig = (theme = {}, sourceLabel = 'theme.yaml') => {
158
166
  };
159
167
  };
160
168
 
169
+ const mergePageThemePart = (base, override, keys) => Object.fromEntries(keys
170
+ .filter((key) => override?.[key] !== undefined || base?.[key] !== undefined)
171
+ .map((key) => [key, override?.[key] ?? base?.[key]]));
172
+
173
+ export const mergePageThemeConfig = (base = {}, override = {}) => ({
174
+ ...base,
175
+ layout: {
176
+ ...(base.layout ?? {}),
177
+ ...mergePageThemePart(base.layout, override.layout, ['contentSpacing', 'textWidth']),
178
+ },
179
+ images: {
180
+ ...(base.images ?? {}),
181
+ ...mergePageThemePart(base.images, override.images, [
182
+ 'width',
183
+ 'maxAvailableWidthPercent',
184
+ 'maxAvailableHeightPercent',
185
+ ]),
186
+ },
187
+ sections: {
188
+ ...(base.sections ?? {}),
189
+ ...mergePageThemePart(base.sections, override.sections, ['backgroundPattern']),
190
+ },
191
+ });
192
+
161
193
  const quote = (value) => JSON.stringify(value);
162
194
  const responsiveValueLines = (label, value, indent = 2) => {
163
195
  const prefix = ' '.repeat(indent);
@@ -171,7 +203,7 @@ const responsiveValueLines = (label, value, indent = 2) => {
171
203
  export const renderThemePresetReference = (presetName, sourceLabel = 'theme.yaml') => {
172
204
  const preset = getThemePreset(presetName, sourceLabel);
173
205
  const metadata = getThemePresetMetadata(presetName);
174
- const { navigation, layout, images, typography, palette, sectionSurfaces } = preset;
206
+ const { shape, layout, images, typography, palette, sections } = preset;
175
207
 
176
208
  return [
177
209
  `# Original values for Norna's "${presetName}" theme preset.`,
@@ -181,13 +213,14 @@ export const renderThemePresetReference = (presetName, sourceLabel = 'theme.yaml
181
213
  `# Available theme presets: ${themePresetNames.join(', ')}.`,
182
214
  `preset: ${presetName}`,
183
215
  '',
184
- 'navigation:',
185
- ' # Alternatives: automatic, sections, top, tree.',
186
- ` mode: ${navigation.mode}`,
216
+ '# Alternatives: square, soft.',
217
+ `shape: ${shape}`,
187
218
  '',
188
219
  'layout:',
189
- ' # Alternatives: compact, normal, airy.',
190
- ` density: ${layout.density}`,
220
+ ' # Alternatives: compact, normal, spacious.',
221
+ ` contentSpacing: ${layout.contentSpacing}`,
222
+ ' # Alternatives: narrow, normal, wide.',
223
+ ` textWidth: ${layout.textWidth}`,
191
224
  ' # Any positive CSS length, including px, rem, em, %, or clamp(...).',
192
225
  ` pageWidth: ${layout.pageWidth}`,
193
226
  ' gutter:',
@@ -219,9 +252,9 @@ export const renderThemePresetReference = (presetName, sourceLabel = 'theme.yaml
219
252
  '',
220
253
  '# Alternatives: dark, light, paper.',
221
254
  `palette: ${palette}`,
222
- '# One value keeps every section on the same surface; multiple values cycle.',
223
- '# Use one to three unique values from: base, soft, emphasis.',
224
- `sectionSurfaces: [${sectionSurfaces.join(', ')}]`,
255
+ 'sections:',
256
+ ' # Alternatives: uniform, alternating, cycling.',
257
+ ` backgroundPattern: ${sections.backgroundPattern}`,
225
258
  '',
226
259
  ].join('\n');
227
260
  };
@@ -1,5 +1,3 @@
1
- import { readFile } from 'node:fs/promises';
2
- import path from 'node:path';
3
1
  import {
4
2
  defaultTypography,
5
3
  resolveTypographyConfig,
@@ -13,20 +11,12 @@ import {
13
11
  readSiteFile,
14
12
  validateContentFrontmatterStructure,
15
13
  validateFrontmatterIndentation,
16
- validatePageThemeYamlStructure,
17
14
  } from './lib/site-content.mjs';
18
15
  import {
19
- siteContentLabel,
20
- siteContentPath,
21
- sitePagesDir,
22
- sitePagesLabel,
23
16
  siteThemeLabel,
24
- siteThemePath,
25
17
  } from './lib/site-paths.mjs';
26
- import { getPageDirectoryAncestors } from './lib/page-model.mjs';
27
18
  import { readThemeConfig } from './lib/theme-config.mjs';
28
19
  import { resolveThemeConfig } from './lib/theme-presets.mjs';
29
- import { parseYamlConfig } from './lib/yaml-config.mjs';
30
20
 
31
21
  const mode = process.argv[2] ?? 'show';
32
22
 
@@ -199,28 +189,6 @@ const readThemeTypography = async () => {
199
189
  return resolveAnnotatedTypographyConfig(themeTypographyConfig ?? defaultTypography, siteThemeLabel);
200
190
  };
201
191
 
202
- const readPageThemeTypography = async (contentFile, themeTypography) => {
203
- if (contentFile.isHome) return themeTypography;
204
-
205
- for (const pageDirectory of getPageDirectoryAncestors(contentFile.pageDirectory).reverse()) {
206
- const pageThemePath = path.join(sitePagesDir, pageDirectory, 'theme.yaml');
207
- const pageThemeFile = await readFile(pageThemePath, 'utf8').catch((error) => {
208
- if (error?.code === 'ENOENT') return null;
209
- throw error;
210
- });
211
- if (!pageThemeFile) continue;
212
- const pageThemeLabel = `${sitePagesLabel}/${pageDirectory}/theme.yaml`;
213
-
214
- const pageThemeConfig = resolveThemeConfig(parseYamlConfig(pageThemeFile, pageThemeLabel, {
215
- validateStructure: validatePageThemeYamlStructure,
216
- }), pageThemeLabel);
217
- const typographyConfig = pageThemeConfig.typography;
218
- return resolveAnnotatedTypographyConfig(typographyConfig ?? defaultTypography, pageThemeLabel);
219
- }
220
-
221
- return themeTypography;
222
- };
223
-
224
192
  const readPageTypography = async (contentFile, siteThemeTypography) => {
225
193
  const { frontmatter, body } = await readSiteFile(contentFile.contentPath, contentFile.contentLabel);
226
194
  const indentationIssues = [];
@@ -233,7 +201,7 @@ const readPageTypography = async (contentFile, siteThemeTypography) => {
233
201
  ].join('\n'));
234
202
  }
235
203
 
236
- const pageTypography = await readPageThemeTypography(contentFile, siteThemeTypography);
204
+ const pageTypography = siteThemeTypography;
237
205
  const sections = (await getBodySections(body)).sections
238
206
  .filter((section) => section.id)
239
207
  .map((section) => {
@@ -182,9 +182,7 @@ const getGlobalImageCandidates = (globalImageCandidatesByName, imageName, expect
182
182
 
183
183
  const getReferenceLabel = (contentFile, section) => `${contentFile.contentLabel} [${section.id ?? 'page title'}]`;
184
184
 
185
- const getRootLabel = (contentFile) => contentFile.isHome
186
- ? contentFile.imagesLabel
187
- : contentFile.imagesLabel.replace(/\/images$/, '');
185
+ const getRootLabel = (contentFile) => contentFile.imagesLabel.replace(/\/images$/, '');
188
186
 
189
187
  const getCandidateLabel = ({ contentFile, imagePath }) =>
190
188
  `${contentFile.imagesLabel}/${toPosixPath(path.relative(contentFile.imagesDir, imagePath))}`;
@@ -34,7 +34,7 @@ const runCli = (args, env = {}) => {
34
34
  };
35
35
 
36
36
  try {
37
- await mkdir(path.join(siteDir, 'images'), { recursive: true });
37
+ await mkdir(path.join(siteDir, 'pages', '000-home'), { recursive: true });
38
38
  await mkdir(path.join(siteDir, 'public'), { recursive: true });
39
39
  await writeFile(path.join(siteDir, 'public', 'robots.txt'), 'User-agent: *\nAllow: /\n');
40
40
  await writeFile(path.join(siteDir, 'config.yaml'), 'url: https://example.com/\n');
@@ -55,7 +55,7 @@ palette: dark
55
55
  title: Active banner
56
56
  text: Active banner text.
57
57
  `);
58
- await writeFile(path.join(siteDir, 'content.md'), `---
58
+ await writeFile(path.join(siteDir, 'pages', '000-home', 'content.md'), `---
59
59
  page:
60
60
  description: Test site for temporary banners.
61
61
  ---