@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
@@ -9,6 +9,8 @@ const cliPath = path.join(repoRoot, 'bin', 'norna.mjs');
9
9
  const tutorialPath = path.join(repoRoot, 'docs', 'getting-started.md');
10
10
  const scratchRoot = path.join(repoRoot, 'todos');
11
11
  const obsoleteSourceReferences = [
12
+ 'site/content.md',
13
+ 'site/images/',
12
14
  'site/config.md',
13
15
  'site/theme.md',
14
16
  'site/sitewide-content.md',
@@ -149,8 +151,8 @@ try {
149
151
 
150
152
  await Promise.all([
151
153
  writeFile(
152
- path.join(tutorialSiteRoot, 'site', 'content.md'),
153
- extractTutorialFile(tutorial, 'Replace `site/content.md` with:', 'md'),
154
+ path.join(tutorialSiteRoot, 'site', 'pages', '000-home', 'content.md'),
155
+ extractTutorialFile(tutorial, 'Replace `site/pages/000-home/content.md` with:', 'md'),
154
156
  ),
155
157
  writeFile(
156
158
  path.join(tutorialSiteRoot, 'site', 'sitewide-content.yaml'),
@@ -18,7 +18,7 @@ import {
18
18
 
19
19
  const root = await mkdtemp(path.join(os.tmpdir(), 'norna-editor-language-'));
20
20
  const siteRoot = path.join(root, 'site');
21
- const homeContentPath = path.join(siteRoot, 'content.md');
21
+ const homeContentPath = path.join(siteRoot, 'pages', '000-home', 'content.md');
22
22
  const pageContentPath = path.join(siteRoot, 'pages', '010-about', 'content.md');
23
23
  const pageThemePath = path.join(siteRoot, 'pages', '010-about', 'theme.yaml');
24
24
  const nestedPageContentPath = path.join(siteRoot, 'pages', '010-about', 'pages', '020-team', 'content.md');
@@ -63,7 +63,7 @@ Page content.
63
63
 
64
64
  try {
65
65
  await mkdir(path.join(installedNornaRoot, 'schemas'), { recursive: true });
66
- await mkdir(path.join(siteRoot, 'images'), { recursive: true });
66
+ await mkdir(path.join(siteRoot, 'pages', '000-home', 'images'), { recursive: true });
67
67
  await mkdir(path.join(siteRoot, 'pages', '010-about', 'images'), { recursive: true });
68
68
  await mkdir(path.dirname(nestedPageContentPath), { recursive: true });
69
69
  await mkdir(path.join(siteRoot, 'public'), { recursive: true });
@@ -74,6 +74,7 @@ try {
74
74
  files: {
75
75
  config: 'config.schema.json',
76
76
  contentFrontmatter: 'content-frontmatter.schema.json',
77
+ pageTheme: 'page-theme.schema.json',
77
78
  sitewideContent: 'sitewide-content.schema.json',
78
79
  theme: 'theme.schema.json',
79
80
  },
@@ -84,7 +85,7 @@ try {
84
85
  await writeFile(path.join(siteRoot, 'sitewide-content.yaml'), `logo:
85
86
  height: 2rem
86
87
  `);
87
- await writeFile(path.join(siteRoot, 'images', 'local.jpg'), 'local');
88
+ await writeFile(path.join(siteRoot, 'pages', '000-home', 'images', 'local.jpg'), 'local');
88
89
  await writeFile(path.join(siteRoot, 'pages', '010-about', 'images', 'portrait.jpg'), 'portrait');
89
90
  await writeFile(path.join(siteRoot, 'public', 'logo.svg'), '<svg/>');
90
91
  await writeFile(path.join(siteRoot, 'public', 'logo.png'), 'logo');
@@ -94,21 +95,22 @@ try {
94
95
  await writeFile(path.join(siteRoot, 'public', 'favicon-32x32.png'), 'unrecognized');
95
96
  await writeFile(homeContentPath, homeSource);
96
97
  await writeFile(pageContentPath, pageSource);
97
- await writeFile(pageThemePath, 'preset: portfolio\n');
98
+ await writeFile(pageThemePath, 'layout:\n textWidth: wide\n');
98
99
  await writeFile(nestedPageContentPath, pageSource.replace('# About', '# Team'));
99
- await writeFile(nestedPageThemePath, 'preset: documentation\n');
100
+ await writeFile(nestedPageThemePath, 'layout:\n contentSpacing: compact\n');
100
101
 
101
102
  assert.equal(await findNornaSiteRoot(homeContentPath), siteRoot);
102
103
  assert.equal(getNornaDocumentContext(homeContentPath).schemaKind, 'contentFrontmatter');
104
+ assert.equal(getNornaDocumentContext(homeContentPath).pageDirectory, '000-home');
103
105
  assert.equal(getNornaDocumentContext(homeContentPath).nornaPackage.root, installedNornaRoot);
104
106
  assert.equal(getNornaDocumentContext(path.join(siteRoot, 'config.yaml')).schemaKind, 'config');
105
107
  assert.equal(getNornaDocumentContext(path.join(siteRoot, 'theme.yaml')).schemaKind, 'theme');
106
108
  assert.equal(getNornaDocumentContext(path.join(siteRoot, 'sitewide-content.yaml')).schemaKind, 'sitewideContent');
107
109
  assert.equal(getNornaDocumentContext(pageContentPath).pageDirectory, '010-about');
108
- assert.equal(getNornaDocumentContext(pageThemePath).schemaKind, 'theme');
110
+ assert.equal(getNornaDocumentContext(pageThemePath).schemaKind, 'pageTheme');
109
111
  assert.equal(getNornaDocumentContext(nestedPageContentPath).pageDirectory, '010-about/pages/020-team');
110
112
  assert.equal(getNornaDocumentContext(nestedPageContentPath).schemaKind, 'contentFrontmatter');
111
- assert.equal(getNornaDocumentContext(nestedPageThemePath).schemaKind, 'theme');
113
+ assert.equal(getNornaDocumentContext(nestedPageThemePath).schemaKind, 'pageTheme');
112
114
  assert.equal(getNornaProjectContext(path.join(siteRoot, 'public', 'logo.svg')).siteRoot, siteRoot);
113
115
  assert.equal(getNornaDocumentContext(path.join(siteRoot, 'public', 'logo.svg')), null);
114
116
  assert.equal(getNornaDocumentContext(path.join(root, 'README.md')), null);
@@ -116,6 +118,7 @@ try {
116
118
  await mkdir(path.join(siteRoot, 'pages', 'about'), { recursive: true });
117
119
  await writeFile(path.join(siteRoot, 'pages', 'about', 'content.md'), pageSource);
118
120
  assert.equal(getNornaDocumentContext(path.join(siteRoot, 'pages', 'about', 'content.md')), null);
121
+ assert.equal(getNornaDocumentContext(path.join(siteRoot, 'pages', '000-home', 'pages', '010-news', 'content.md')), null);
119
122
  await mkdir(path.join(siteRoot, 'pages', '010-about', 'nested'), { recursive: true });
120
123
  await writeFile(path.join(siteRoot, 'pages', '010-about', 'nested', 'content.md'), pageSource);
121
124
  assert.equal(getNornaDocumentContext(path.join(siteRoot, 'pages', '010-about', 'nested', 'content.md')), null);
@@ -244,7 +247,7 @@ try {
244
247
  line: localDefinitionLine,
245
248
  source: homeSource,
246
249
  });
247
- assert.deepEqual(localDefinition.files, [path.join(siteRoot, 'images', 'local.jpg')]);
250
+ assert.deepEqual(localDefinition.files, [path.join(siteRoot, 'pages', '000-home', 'images', 'local.jpg')]);
248
251
 
249
252
  const diagnostics = await getMarkdownDiagnostics({ documentPath: homeContentPath, source: homeSource });
250
253
  assert.ok(diagnostics.some(({ code, message }) => code === 'image-needs-sync' && message.includes('Run "norna content:sync"')));
@@ -39,9 +39,9 @@ try {
39
39
  assert.match(deployWatchHelpResult.stdout, /Default: deploy\.yml/);
40
40
 
41
41
  const directSiteRoot = path.join(tempRoot, 'current-directory-site');
42
- await mkdir(directSiteRoot, { recursive: true });
42
+ await mkdir(path.join(directSiteRoot, 'pages', '000-home'), { recursive: true });
43
43
  await writeFile(path.join(directSiteRoot, 'config.yaml'), 'url: https://example.com/\n');
44
- await writeFile(path.join(directSiteRoot, 'content.md'), '# Direct Site\n\n## Intro {#intro}\n\nText.\n');
44
+ await writeFile(path.join(directSiteRoot, 'pages', '000-home', 'content.md'), '# Direct Site\n\n## Intro {#intro}\n\nText.\n');
45
45
  const directSiteProjectRoot = await realpath(tempRoot);
46
46
  const directSiteDoctorResult = runCli(['doctor'], {
47
47
  cwd: directSiteRoot,
@@ -99,7 +99,7 @@ try {
99
99
  assert.match(showResult.stdout, /theme:/);
100
100
  assert.match(showResult.stdout, /pages:/);
101
101
  assert.match(showResult.stdout, /\s+\/:/);
102
- assert.match(showResult.stdout, /value: restrained/);
102
+ assert.match(showResult.stdout, /value: reading/);
103
103
  assert.match(showResult.stdout, /source: "site\/theme\.yaml"/);
104
104
  assert.match(showResult.stdout, /intro:/);
105
105
 
@@ -116,7 +116,7 @@ try {
116
116
  assert.equal(customStandalonePackageJson.scripts['norna:dev'], 'norna --site-dir presentation dev:local');
117
117
  assert.equal(customStandalonePackageJson.scripts['norna:check'], 'norna --site-dir presentation check');
118
118
  assert.equal(customStandalonePackageJson.scripts['norna:build'], 'norna --site-dir presentation build');
119
- await readFile(path.join(customStandaloneSiteRoot, 'presentation', 'content.md'));
119
+ await readFile(path.join(customStandaloneSiteRoot, 'presentation', 'pages', '000-home', 'content.md'));
120
120
  await readFile(path.join(customStandaloneSiteRoot, 'presentation', 'config.yaml'));
121
121
  await readFile(path.join(customStandaloneSiteRoot, 'presentation', 'theme.yaml'));
122
122
 
@@ -143,7 +143,7 @@ try {
143
143
  assert.equal(mixedPackageJson.scripts['norna:check'], 'norna --site-dir presentation check');
144
144
  assert.equal(mixedPackageJson.scripts['norna:engine:update'], 'norna --site-dir presentation engine:update');
145
145
  assert.equal(mixedPackageJson.scripts['norna:engine:version'], 'norna --site-dir presentation engine:version');
146
- await readFile(path.join(mixedProjectRoot, 'presentation', 'content.md'));
146
+ await readFile(path.join(mixedProjectRoot, 'presentation', 'pages', '000-home', 'content.md'));
147
147
  await readFile(path.join(mixedProjectRoot, 'presentation', 'config.yaml'));
148
148
  await readFile(path.join(mixedProjectRoot, 'presentation', 'theme.yaml'));
149
149
 
@@ -1,5 +1,7 @@
1
+ import assert from 'node:assert/strict';
1
2
  import { existsSync } from 'node:fs';
2
- import { readFile } from 'node:fs/promises';
3
+ import { mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises';
4
+ import { tmpdir } from 'node:os';
3
5
  import path from 'node:path';
4
6
  import { fileURLToPath } from 'node:url';
5
7
  import { getExampleSites } from './lib/example-sites.mjs';
@@ -9,6 +11,33 @@ const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
9
11
  const cliPath = path.join(root, 'bin', 'norna.mjs');
10
12
  const examples = await getExampleSites(root);
11
13
 
14
+ const discoveryRoot = await mkdtemp(path.join(tmpdir(), 'norna-example-discovery-'));
15
+ try {
16
+ for (const category of ['complete-sites', 'feature-demos']) {
17
+ await mkdir(path.join(discoveryRoot, 'examples', category), { recursive: true });
18
+ }
19
+ const validSite = path.join(discoveryRoot, 'examples', 'complete-sites', 'valid', 'site');
20
+ await mkdir(path.join(validSite, 'pages', '000-home'), { recursive: true });
21
+ await writeFile(path.join(validSite, 'config.yaml'), 'url: https://example.com/\n');
22
+ await writeFile(path.join(validSite, 'pages', '000-home', 'content.md'), '# Example\n');
23
+ await mkdir(path.join(
24
+ discoveryRoot,
25
+ 'examples',
26
+ 'feature-demos',
27
+ 'removed-example',
28
+ 'site',
29
+ '.norna',
30
+ 'public',
31
+ 'images',
32
+ 'generated',
33
+ ), { recursive: true });
34
+
35
+ const discovered = await getExampleSites(discoveryRoot);
36
+ assert.deepEqual(discovered.map(({ name }) => name), ['valid']);
37
+ } finally {
38
+ await rm(discoveryRoot, { recursive: true, force: true });
39
+ }
40
+
12
41
  for (const example of examples) {
13
42
  console.log(`\nBuilding ${example.siteLabel}`);
14
43
  await runInherit(process.execPath, [cliPath, '--site-dir', example.siteLabel, 'build'], { cwd: root });
@@ -43,9 +43,9 @@ const collision = await getMarkdownHeadings(`# Page
43
43
  `);
44
44
  assert.equal(getHeadingIdentifierIssues(collision.headings)[0]?.code, 'duplicate-heading-id');
45
45
  assert.equal(
46
- formatHeadingIdentifierIssue(getHeadingIdentifierIssues(collision.headings)[0], 'site/content.md'),
46
+ formatHeadingIdentifierIssue(getHeadingIdentifierIssues(collision.headings)[0], 'site/pages/000-home/content.md'),
47
47
  [
48
- 'site/content.md: Two headings resolve to id "forsta".',
48
+ 'site/pages/000-home/content.md: Two headings resolve to id "forsta".',
49
49
  '- line 3: "Förstå"',
50
50
  '- line 5: "Forsta"',
51
51
  'Add a unique explicit id to at least one heading, for example {#forsta-details}.',
@@ -5,10 +5,19 @@ import { fileURLToPath } from 'node:url';
5
5
 
6
6
  const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
7
7
  const cliPath = path.join(root, 'bin', 'norna.mjs');
8
- const navigationDemoSiteDir = path.join(root, 'examples', 'feature-demos', 'media-and-surfaces', 'site');
9
8
  const host = '127.0.0.1';
10
9
  const npmBin = process.platform === 'win32' ? 'npm.cmd' : 'npm';
11
- const testTargets = process.argv.slice(2);
10
+ const cliArguments = process.argv.slice(2);
11
+ const siteDirOptionIndex = cliArguments.indexOf('--site-dir');
12
+ const configuredSiteDir = siteDirOptionIndex === -1
13
+ ? path.join('examples', 'feature-demos', 'media-and-surfaces', 'site')
14
+ : cliArguments[siteDirOptionIndex + 1];
15
+ if (!configuredSiteDir || configuredSiteDir.startsWith('--')) {
16
+ throw new Error('Use --site-dir <path> to select a navigation test site.');
17
+ }
18
+ if (siteDirOptionIndex !== -1) cliArguments.splice(siteDirOptionIndex, 2);
19
+ const navigationDemoSiteDir = path.resolve(root, configuredSiteDir);
20
+ const testTargets = cliArguments;
12
21
  const playwrightTargets = testTargets.length > 0 ? testTargets : ['tests/navigation.spec.ts'];
13
22
 
14
23
  const sleep = (milliseconds) => new Promise((resolve) => {
@@ -46,6 +46,7 @@ try {
46
46
  'guides/installation/index.html',
47
47
  'guides/installation/macos/index.html',
48
48
  'guides/workflows/index.html',
49
+ 'guides/release-notes/index.html',
49
50
  'reference/index.html',
50
51
  'reference/installation/index.html',
51
52
  ];
@@ -57,20 +58,34 @@ try {
57
58
  const guidesHtml = await readFile(path.join(distDir, 'guides', 'index.html'), 'utf8');
58
59
  const installationHtml = await readFile(path.join(distDir, 'guides', 'installation', 'index.html'), 'utf8');
59
60
  const macosHtml = await readFile(path.join(distDir, 'guides', 'installation', 'macos', 'index.html'), 'utf8');
60
- assert.match(rootHtml, /--color-page: #000000/);
61
- assert.match(guidesHtml, /--color-page: #ffffff/);
62
- assert.match(installationHtml, /--color-page: #ffffff/);
63
- assert.match(macosHtml, /--color-page: #ffffff/);
61
+ for (const html of [rootHtml, guidesHtml, installationHtml, macosHtml]) {
62
+ assert.match(html, /--color-page: #f8f5ee/);
63
+ assert.match(html, /--font-sans: Georgia, 'Times New Roman', serif/);
64
+ }
65
+ assert.match(rootHtml, /--image-width: 920px/);
66
+ assert.match(rootHtml, /--space-section-to-section-desktop: clamp\(1\.2rem, 2\.4vw, 2\.25rem\)/);
67
+ assert.match(guidesHtml, /--image-width: 760px/);
68
+ assert.match(guidesHtml, /--space-section-to-section-desktop: clamp\(2\.25rem, 5vw, 4\.5rem\)/);
69
+ assert.match(guidesHtml, /--section-body-width-desktop: min\(72ch,/);
70
+ assert.match(installationHtml, /--image-width: 760px/);
71
+ assert.match(installationHtml, /--space-section-to-section-desktop: clamp\(2\.25rem, 5vw, 4\.5rem\)/);
72
+ assert.match(installationHtml, /--section-body-width-desktop: min\(60ch,/);
73
+ assert.match(macosHtml, /--section-body-width-desktop: min\(60ch,/);
64
74
  assert.match(installationHtml, /Three nested page levels connected in sequence/);
65
75
  assert.match(installationHtml, /\/original\/pages\/010-guides\/pages\/010-installation\/images\/diagram-[a-f0-9]+\.svg/);
66
- assert.match(macosHtml, /class="site-nav-item site-nav-item-has-submenu site-nav-item-current-branch"/);
76
+ assert.match(macosHtml, /data-navigation-mode="tree"/);
77
+ assert.match(macosHtml, /class="site-nav-item site-nav-item-current-branch"/);
78
+ assert.doesNotMatch(macosHtml, /class="site-nav-submenu"/);
67
79
  assert.match(macosHtml, /href="\/guides\/installation\/macos\/" aria-current="page"/);
68
- assert.match(macosHtml, /<details class="mobile-navigation-branch" open>/);
80
+ assert.match(macosHtml, /<aside class="tree-local-navigation" data-navigation-root="\/guides\/">/);
81
+ assert.match(macosHtml, /<ul class="navigation-page-tree navigation-page-tree-sidebar">/);
82
+ assert.match(macosHtml, /<details class="navigation-page-branch navigation-page-disclosure navigation-page-disclosure-sidebar" data-page-path="guides" data-current-branch="true"/);
83
+ assert.match(macosHtml, /<summary class="navigation-page-summary"[^>]*><span class="navigation-page-summary-title">Guides<\/span>/);
69
84
  assert.match(macosHtml, /<nav class="site-breadcrumbs" aria-label="Breadcrumb"><ol><li><a href="\/guides\/">Guides<\/a><\/li><li><a href="\/guides\/installation\/">Installation<\/a><\/li><li><span aria-current="page">macOS<\/span>/);
70
- for (const submenuMatch of macosHtml.matchAll(/<div class="site-nav-submenu">([\s\S]*?)<\/div>/g)) {
71
- assert.doesNotMatch(submenuMatch[1], /href="[^"#]*#[^"]+"/);
72
- }
73
- assert.match(macosHtml, /<nav class="mobile-nav-view" aria-label="On this page">/);
85
+ assert.match(macosHtml, /<nav class="navigation-page-sections" aria-label="Page contents: macOS">/);
86
+ assert.doesNotMatch(macosHtml, /class="tree-page-contents"/);
87
+ assert.match(macosHtml, /href="#install">Install<\/a><ol><li><a href="#prerequisites">Prerequisites<\/a>/);
88
+ assert.doesNotMatch(macosHtml, /<nav class="page-nav"/);
74
89
  assert.ok(
75
90
  macosHtml.indexOf('href="/guides/installation/"') < macosHtml.indexOf('href="/guides/workflows/"'),
76
91
  'Nested sibling navigation should follow page directory order.',
@@ -167,8 +167,8 @@ try {
167
167
  assertFileExists(path.join(packagedStarterRoot, 'site', 'config.yaml')),
168
168
  assertFileExists(path.join(packagedStarterRoot, 'site', 'theme.yaml')),
169
169
  assertFileExists(path.join(packagedStarterRoot, 'site', 'sitewide-content.yaml')),
170
- assertFileExists(path.join(packagedStarterRoot, 'site', 'content.md')),
171
- assertFileExists(path.join(packagedStarterRoot, 'site', 'images', '.gitkeep')),
170
+ assertFileExists(path.join(packagedStarterRoot, 'site', 'pages', '000-home', 'content.md')),
171
+ assertFileExists(path.join(packagedStarterRoot, 'site', 'pages', '000-home', 'images', '.gitkeep')),
172
172
  assertFileExists(path.join(packagedStarterRoot, 'site', 'public', 'robots.txt')),
173
173
  ]);
174
174
  await assertFileIncludes(
@@ -204,7 +204,7 @@ try {
204
204
  path.join(siteProjectRoot, 'site', 'public', 'logo.svg'),
205
205
  '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32"><rect width="32" height="32" fill="black"/></svg>\n',
206
206
  );
207
- await writeFile(path.join(siteProjectRoot, 'site', 'content.md'), `---
207
+ await writeFile(path.join(siteProjectRoot, 'site', 'pages', '000-home', 'content.md'), `---
208
208
  page:
209
209
  description: Site used by package checks.
210
210
  ---
@@ -248,12 +248,14 @@ This page verifies that packaged norna sites can build additional pages.
248
248
  assertFileExists(path.join(initializedSiteRoot, 'package.json')),
249
249
  assertFileExists(path.join(initializedSiteRoot, 'site', 'config.yaml')),
250
250
  assertFileExists(path.join(initializedSiteRoot, 'site', 'theme.yaml')),
251
+ assertFileExists(path.join(initializedSiteRoot, 'site', 'pages', '000-home', 'content.md')),
251
252
  assertFileMissing(path.join(initializedSiteRoot, '.DS_Store')),
252
253
  assertFileMissing(path.join(initializedSiteRoot, 'site', '.DS_Store')),
253
254
  ]);
254
- await runInherit(npxBin, ['norna', 'engine:version'], { cwd: path.join(siteProjectRoot, 'site', 'images'), env: npmEnv });
255
- await runInherit(npxBin, ['norna', 'doctor'], { cwd: path.join(siteProjectRoot, 'site', 'images'), env: npmEnv });
256
- await runInherit(npxBin, ['norna', 'config:check'], { cwd: path.join(siteProjectRoot, 'site', 'images'), env: npmEnv });
255
+ const homeImagesDir = path.join(siteProjectRoot, 'site', 'pages', '000-home', 'images');
256
+ await runInherit(npxBin, ['norna', 'engine:version'], { cwd: homeImagesDir, env: npmEnv });
257
+ await runInherit(npxBin, ['norna', 'doctor'], { cwd: homeImagesDir, env: npmEnv });
258
+ await runInherit(npxBin, ['norna', 'config:check'], { cwd: homeImagesDir, env: npmEnv });
257
259
  await runInherit(npxBin, ['norna', 'content:check'], { cwd: siteProjectRoot, env: npmEnv });
258
260
  await runInherit(npxBin, ['norna', 'check'], { cwd: siteProjectRoot, env: npmEnv });
259
261
  await runInherit(npxBin, ['norna', 'build'], { cwd: siteProjectRoot, env: npmEnv });
@@ -275,19 +277,19 @@ This page verifies that packaged norna sites can build additional pages.
275
277
  );
276
278
  await assertFileIncludes(
277
279
  path.join(siteProjectRoot, 'dist', 'index.html'),
278
- '--section-heading-font-weight: 400',
280
+ '--section-heading-font-weight: 500',
279
281
  );
280
282
  await assertFileIncludes(
281
283
  path.join(siteProjectRoot, 'dist', 'index.html'),
282
- '--section-heading-spacing-after: 0.55em',
284
+ '--section-heading-spacing-after: 0.45em',
283
285
  );
284
286
  await assertFileIncludes(
285
287
  path.join(siteProjectRoot, 'dist', 'index.html'),
286
- '--section-markdown-h3-spacing-before: 1.5em',
288
+ '--section-markdown-h3-spacing-before: 1.2em',
287
289
  );
288
290
  await assertFileIncludes(
289
291
  path.join(siteProjectRoot, 'dist', 'index.html'),
290
- "--font-sans: Arial, 'Helvetica Neue', Helvetica, sans-serif",
292
+ "--font-sans: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif",
291
293
  );
292
294
  await assertFileIncludes(
293
295
  path.join(siteProjectRoot, 'dist', 'index.html'),
@@ -323,7 +325,7 @@ This page verifies that packaged norna sites can build additional pages.
323
325
  );
324
326
  await assertFileIncludes(
325
327
  path.join(siteProjectRoot, 'dist', 'index.html'),
326
- '<nav class="page-nav" aria-label="On this page">',
328
+ '<nav class="page-nav" aria-label="Page contents">',
327
329
  );
328
330
  await assertFileIncludes(
329
331
  path.join(siteProjectRoot, 'dist', 'index.html'),
@@ -335,7 +337,7 @@ This page verifies that packaged norna sites can build additional pages.
335
337
  );
336
338
  await assertFileIncludes(
337
339
  path.join(siteProjectRoot, 'dist', 'index.html'),
338
- '--section-body-line-height: 1.5',
340
+ '--section-body-line-height: 1.62',
339
341
  );
340
342
  await assertFileIncludes(
341
343
  path.join(siteProjectRoot, 'dist', 'index.html'),
@@ -347,15 +349,15 @@ This page verifies that packaged norna sites can build additional pages.
347
349
  );
348
350
  await assertFileIncludes(
349
351
  path.join(siteProjectRoot, 'dist', 'index.html'),
350
- '--section-body-paragraph-spacing: 0.85em',
352
+ '--section-body-paragraph-spacing: 0.65em',
351
353
  );
352
354
  await assertFileIncludes(
353
355
  path.join(siteProjectRoot, 'dist', 'index.html'),
354
- '--section-caption-line-height: 1.35',
356
+ '--section-caption-line-height: 1.4',
355
357
  );
356
358
  await assertFileIncludes(
357
359
  path.join(siteProjectRoot, 'dist', 'index.html'),
358
- '--section-caption-spacing-before: 0.5em',
360
+ '--section-caption-spacing-before: 0.35em',
359
361
  );
360
362
  await assertFileIncludes(
361
363
  path.join(siteProjectRoot, 'dist', 'index.html'),
@@ -375,35 +377,31 @@ This page verifies that packaged norna sites can build additional pages.
375
377
  );
376
378
  await assertFileIncludes(
377
379
  path.join(siteProjectRoot, 'dist', 'index.html'),
378
- '--section-background-color: #000000',
380
+ '--section-background-color: #ffffff',
379
381
  );
380
382
  await assertFileIncludes(
381
383
  path.join(siteProjectRoot, 'dist', 'index.html'),
382
- '--section-background-color: #171717',
384
+ '--section-background-color: #f1f4f2',
383
385
  );
384
386
  await assertFileIncludes(
385
387
  path.join(siteProjectRoot, 'dist', 'index.html'),
386
- '--section-background-color: #252525',
388
+ '--site-top-background-color: #ffffff',
387
389
  );
388
390
  await assertFileIncludes(
389
391
  path.join(siteProjectRoot, 'dist', 'index.html'),
390
- '--site-top-background-color: #000000',
392
+ '--site-top-text-color: #17201d',
391
393
  );
392
394
  await assertFileIncludes(
393
395
  path.join(siteProjectRoot, 'dist', 'index.html'),
394
- '--site-top-text-color: #f2eee6',
396
+ '--site-footer-background-color: #ffffff',
395
397
  );
396
398
  await assertFileIncludes(
397
399
  path.join(siteProjectRoot, 'dist', 'index.html'),
398
- '--site-footer-background-color: #000000',
400
+ '--site-footer-text-color: #17201d',
399
401
  );
400
402
  await assertFileIncludes(
401
403
  path.join(siteProjectRoot, 'dist', 'index.html'),
402
- '--site-footer-text-color: #f2eee6',
403
- );
404
- await assertFileIncludes(
405
- path.join(siteProjectRoot, 'dist', 'index.html'),
406
- '--section-text-color: #f2eee6',
404
+ '--section-text-color: #17201d',
407
405
  );
408
406
  await writeFile(path.join(siteProjectRoot, 'site', 'public', 'favicon.ico'), 'fake icon');
409
407
  await runInherit(npxBin, ['norna', 'build'], { cwd: siteProjectRoot, env: npmEnv });
@@ -416,47 +414,24 @@ This page verifies that packaged norna sites can build additional pages.
416
414
  path.join(siteProjectRoot, 'dist', 'index.html'),
417
415
  'href="/favicon.svg"',
418
416
  );
419
- const siteContentPath = path.join(siteProjectRoot, 'site', 'content.md');
417
+ const siteContentPath = path.join(siteProjectRoot, 'site', 'pages', '000-home', 'content.md');
420
418
  const siteContent = await readFile(siteContentPath, 'utf8');
421
419
  const siteThemePath = path.join(siteProjectRoot, 'site', 'theme.yaml');
422
420
  const siteTheme = await readFile(siteThemePath, 'utf8');
423
- await writeFile(siteThemePath, siteTheme.replace('\n profile: restrained', '\n profile: noisy'));
421
+ await writeFile(siteThemePath, `${siteTheme.trim()}\ntypography:\n profile: noisy\n`);
424
422
  await runExpectFailure(
425
423
  npxBin,
426
424
  ['norna', 'build'],
427
425
  { cwd: siteProjectRoot, env: npmEnv },
428
- 'Unknown typography profile: noisy',
429
- );
430
- await writeFile(
431
- siteThemePath,
432
- siteTheme.replace('\npalette: dark', '\npalette: neon'),
426
+ 'typography.profile',
433
427
  );
428
+ await writeFile(siteThemePath, `${siteTheme.trim()}\npalette: neon\n`);
434
429
  await runExpectFailure(
435
430
  npxBin,
436
431
  ['norna', 'build'],
437
432
  { cwd: siteProjectRoot, env: npmEnv },
438
433
  'palette',
439
434
  );
440
- await writeFile(
441
- siteThemePath,
442
- siteTheme.replace('\nsectionSurfaces: [base, soft, emphasis]', '\nsectionSurfaces: [base, glowing]'),
443
- );
444
- await runExpectFailure(
445
- npxBin,
446
- ['norna', 'build'],
447
- { cwd: siteProjectRoot, env: npmEnv },
448
- 'Unknown section surface "glowing"',
449
- );
450
- await writeFile(
451
- siteThemePath,
452
- siteTheme.replace('\nsectionSurfaces: [base, soft, emphasis]', '\nsectionSurfaces: [base, base]'),
453
- );
454
- await runExpectFailure(
455
- npxBin,
456
- ['norna', 'build'],
457
- { cwd: siteProjectRoot, env: npmEnv },
458
- 'Each section surface may appear only once',
459
- );
460
435
  await writeFile(siteThemePath, siteTheme);
461
436
  await writeFile(
462
437
  siteContentPath,
@@ -498,13 +473,7 @@ This page verifies that packaged norna sites can build additional pages.
498
473
  { cwd: siteProjectRoot, env: npmEnv },
499
474
  'defines "sections" at the top level, but it is not a valid top-level content field',
500
475
  );
501
- await writeFile(
502
- siteThemePath,
503
- siteTheme.replace(
504
- 'fontFamily: "Arial, \'Helvetica Neue\', Helvetica, sans-serif"',
505
- 'fontFamily: "Arial; color: red"',
506
- ),
507
- );
476
+ await writeFile(siteThemePath, `${siteTheme.trim()}\ntypography:\n fontFamily: "Arial; color: red"\n`);
508
477
  await runExpectFailure(
509
478
  npxBin,
510
479
  ['norna', 'config:check'],
@@ -19,6 +19,29 @@ assert.deepEqual(parsePageDirectory('120-api-reference'), {
19
19
  pageId: 'api-reference',
20
20
  });
21
21
 
22
+ assert.deepEqual(parsePageDirectory('000-home'), {
23
+ pageDirectory: '000-home',
24
+ pageOrder: 0,
25
+ pageId: 'home',
26
+ });
27
+
28
+ assert.deepEqual(parsePageDirectoryPath('000-home'), {
29
+ pageDirectory: '000-home',
30
+ pageDirectories: ['000-home'],
31
+ pageId: 'home',
32
+ pageIds: [],
33
+ pageOrder: 0,
34
+ pageOrders: [],
35
+ pagePath: '',
36
+ parentPagePath: null,
37
+ depth: 1,
38
+ });
39
+
40
+ assert.throws(
41
+ () => parsePageDirectoryPath('000-home/pages/010-about'),
42
+ /000-home is the homepage and cannot contain child pages.*beside it under site\/pages\/.*below another non-home page/,
43
+ );
44
+
22
45
  assert.deepEqual(parsePageDirectoryPath('010-guides/pages/020-installation'), {
23
46
  pageDirectory: '010-guides/pages/020-installation',
24
47
  pageDirectories: ['010-guides', '020-installation'],
@@ -62,7 +85,7 @@ for (const pageDirectoryPath of [
62
85
  }
63
86
 
64
87
  for (const pageDirectory of [
65
- '000-home',
88
+ '000-about',
66
89
  '10-about',
67
90
  '010_About',
68
91
  '010-About',
@@ -72,9 +95,14 @@ for (const pageDirectory of [
72
95
  ]) {
73
96
  assert.throws(
74
97
  () => parsePageDirectory(pageDirectory),
75
- /Page directories must use the form NNN-page-id/,
98
+ /(Page directories must use the form NNN-page-id|000 prefix is reserved)/,
76
99
  `${pageDirectory} should be rejected`,
77
100
  );
78
101
  }
79
102
 
103
+ assert.throws(
104
+ () => parsePageDirectoryPath('010-guides/pages/000-home'),
105
+ /000-home is allowed only as a top-level page/,
106
+ );
107
+
80
108
  console.log('Page model test passed.');
@@ -24,9 +24,9 @@ const importScript = `
24
24
  const createSite = async (name, config) => {
25
25
  const projectRoot = path.join(tempRoot, name);
26
26
  const siteDir = path.join(projectRoot, 'site');
27
- await mkdir(siteDir, { recursive: true });
27
+ await mkdir(path.join(siteDir, 'pages', '000-home'), { recursive: true });
28
28
  await writeFile(path.join(siteDir, 'config.yaml'), config);
29
- await writeFile(path.join(siteDir, 'content.md'), '# Config test\n\n## Intro {#intro}\n\nText.\n');
29
+ await writeFile(path.join(siteDir, 'pages', '000-home', 'content.md'), '# Config test\n\n## Intro {#intro}\n\nText.\n');
30
30
  await writeFile(path.join(siteDir, 'theme.yaml'), 'preset: documentation\n');
31
31
  return { projectRoot, siteDir };
32
32
  };
@@ -69,11 +69,10 @@ try {
69
69
  dismissBanner: 'Dismiss notice',
70
70
  built: 'Built',
71
71
  images: 'Images',
72
- navigationBack: 'Back to pages',
72
+ navigationChildren: 'Child pages',
73
73
  navigationMenu: 'Menu',
74
- navigationOverview: 'Overview',
75
74
  note: 'Note',
76
- pageNavigation: 'On this page',
75
+ pageNavigation: 'Page contents',
77
76
  siteBanners: 'Site notices',
78
77
  siteNavigation: 'Pages',
79
78
  skipToContent: 'Skip to content',
@@ -92,11 +91,7 @@ try {
92
91
  assert.equal(localizedConfig.labels.skipToContent, 'Hoppa till innehållet');
93
92
  assert.equal(localizedConfig.scrollBehavior, 'smooth');
94
93
 
95
- const treeNavigationSite = await createSite('tree-navigation', 'url: https://example.com/\n');
96
- await writeFile(path.join(treeNavigationSite.siteDir, 'theme.yaml'), `preset: documentation
97
- navigation:
98
- mode: tree
99
- `);
94
+ const treeNavigationSite = await createSite('tree-navigation', 'url: https://example.com/\nnavigation:\n mode: tree\n');
100
95
  const treeNavigationResult = loadConfig(treeNavigationSite);
101
96
  assert.equal(treeNavigationResult.status, 0, treeNavigationResult.stderr);
102
97
  assert.equal(JSON.parse(treeNavigationResult.stdout).navigationMode, 'tree');
@@ -10,18 +10,21 @@ const expectedDocumentationRef = `v${packageJson.version}`;
10
10
  const filenames = [
11
11
  'config.schema.json',
12
12
  'theme.schema.json',
13
+ 'page-theme.schema.json',
13
14
  'sitewide-content.schema.json',
14
15
  'content-frontmatter.schema.json',
15
16
  ];
16
17
  const requiredRichHelp = {
17
- 'config.schema.json': ['url', 'language', 'scrollBehavior'],
18
- 'theme.schema.json': ['preset', 'navigation', 'layout', 'images', 'typography', 'palette', 'sectionSurfaces'],
18
+ 'config.schema.json': ['url', 'language', 'navigation', 'scrollBehavior'],
19
+ 'theme.schema.json': ['preset', 'shape', 'layout', 'images', 'typography', 'palette', 'sections'],
20
+ 'page-theme.schema.json': ['layout', 'images', 'sections'],
19
21
  'sitewide-content.schema.json': ['logo', 'banners', 'footer'],
20
22
  'content-frontmatter.schema.json': ['page', 'navigation'],
21
23
  };
22
24
  const manifest = JSON.parse(await readFile(path.join(root, 'schemas', 'manifest.json'), 'utf8'));
23
25
  assert.equal(manifest.editorApiVersion, 1);
24
- assert.equal(manifest.schemaVersion, 1);
26
+ assert.equal(manifest.schemaVersion, 2);
27
+ assert.equal(manifest.files.pageTheme, 'page-theme.schema.json');
25
28
 
26
29
  const githubHeadingAnchor = (heading) => heading
27
30
  .toLowerCase()
@@ -143,6 +146,18 @@ assert.equal(
143
146
  theme.properties.images.properties.maxAvailableWidthPercent.defaultSnippets[0].label,
144
147
  'Responsive image limit',
145
148
  );
149
+ assert.equal(theme.properties.navigation, undefined);
150
+ assert.equal(theme.properties.layout.properties.density, undefined);
151
+ assert.ok(theme.properties.layout.properties.contentSpacing);
152
+ assert.ok(theme.properties.layout.properties.textWidth);
153
+ assert.ok(theme.properties.sections.properties.backgroundPattern);
154
+
155
+ const pageTheme = JSON.parse(await readFile(path.join(root, 'schemas', 'page-theme.schema.json'), 'utf8'));
156
+ assert.deepEqual(Object.keys(pageTheme.properties), ['layout', 'images', 'sections']);
157
+ assert.deepEqual(Object.keys(pageTheme.properties.layout.properties), ['contentSpacing', 'textWidth']);
158
+ assert.equal(pageTheme.properties.preset, undefined);
159
+ assert.equal(pageTheme.properties.palette, undefined);
160
+ assert.equal(pageTheme.properties.typography, undefined);
146
161
 
147
162
  const content = JSON.parse(await readFile(path.join(root, 'schemas', 'content-frontmatter.schema.json'), 'utf8'));
148
163
  assert.deepEqual(Object.keys(content.properties), ['page', 'navigation']);