@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
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  import projectConfig from '../../scripts/lib/project-config.mjs';
3
- import { resolveSectionSurface } from '../../scripts/lib/presentation.mjs';
3
+ import { getTextWidthCssValue, resolveSectionSurface } from '../../scripts/lib/presentation.mjs';
4
4
  import CardList from './CardList.astro';
5
5
  import ImageCarousel from './ImageCarousel.astro';
6
6
  import ImageStack from './ImageStack.astro';
@@ -165,11 +165,6 @@ const getSizeValue = (
165
165
  const getHeadingSizeValue = (level: HeadingLevel, size: TextSize) => (
166
166
  headingSizeValues[level][size]
167
167
  );
168
- const textWidthValues: Record<TextWidth, string> = {
169
- narrow: 'min(60ch, var(--text-width))',
170
- normal: 'min(72ch, var(--text-width))',
171
- wide: 'min(72ch, var(--image-layout-width))',
172
- };
173
168
  const surface = isPageTitle
174
169
  ? pagePresentation.palette.surfaces.base
175
170
  : resolveSectionSurface(pagePresentation, sectionIndex);
@@ -205,7 +200,7 @@ const justifyByAlign: Record<TextAlign, string> = {
205
200
  center: 'center',
206
201
  right: 'flex-end',
207
202
  };
208
- const bodyWidthValue = textWidthValues[typography.body.width as TextWidth];
203
+ const bodyWidthValue = getTextWidthCssValue(typography.body.width as TextWidth);
209
204
  const headingWidth = {
210
205
  desktop: bodyWidthValue,
211
206
  mobile: bodyWidthValue,
@@ -0,0 +1,35 @@
1
+ ---
2
+ import projectConfig from '../../scripts/lib/project-config.mjs';
3
+ import { withBasePath } from '../lib/basePath';
4
+ import { getListedPageNavigationTree, type PageNavigationEntry } from '../lib/siteNavigation';
5
+ import type { SitePage } from '../lib/sitePages';
6
+ import NavigationPageTree from './NavigationPageTree.astro';
7
+
8
+ interface Props {
9
+ currentPage: SitePage;
10
+ pageNavigation: PageNavigationEntry[];
11
+ }
12
+
13
+ const { currentPage, pageNavigation } = Astro.props;
14
+ const roots = getListedPageNavigationTree(pageNavigation);
15
+ const activeRoot = roots.find((node) => (
16
+ currentPage.pageDirectory === node.page.pageDirectory
17
+ || currentPage.pageDirectory.startsWith(`${node.page.pageDirectory}/pages/`)
18
+ ));
19
+ ---
20
+
21
+ {activeRoot && (
22
+ <aside
23
+ class="tree-local-navigation"
24
+ data-navigation-root={withBasePath(projectConfig.site.basePath, activeRoot.page.pathname)}
25
+ >
26
+ <nav aria-label={`${activeRoot.page.title}: ${projectConfig.locale.labels.siteNavigation}`}>
27
+ <NavigationPageTree
28
+ nodes={[activeRoot]}
29
+ currentPage={currentPage}
30
+ variant="sidebar"
31
+ integratePageSections
32
+ />
33
+ </nav>
34
+ </aside>
35
+ )}
@@ -0,0 +1,83 @@
1
+ <script>
2
+ type NavigationState = {
3
+ openPaths: string[];
4
+ sectionPaths: string[];
5
+ scrollTop: number;
6
+ };
7
+
8
+ const setupNavigationState = (container: HTMLElement, scope: 'desktop' | 'mobile') => {
9
+ const root = container.dataset.navigationRoot ?? '/';
10
+ const stateKey = `norna:tree-navigation:${scope}:${root}`;
11
+ const branches = Array.from(container.querySelectorAll<HTMLDetailsElement>(
12
+ '.navigation-page-disclosure[data-page-path]',
13
+ ));
14
+ let storedState: NavigationState = { openPaths: [], sectionPaths: [], scrollTop: 0 };
15
+
16
+ try {
17
+ const parsedState = JSON.parse(sessionStorage.getItem(stateKey) ?? '{}');
18
+ storedState = {
19
+ openPaths: Array.isArray(parsedState.openPaths) ? parsedState.openPaths : [],
20
+ sectionPaths: Array.isArray(parsedState.sectionPaths) ? parsedState.sectionPaths : [],
21
+ scrollTop: Number.isFinite(Number(parsedState.scrollTop)) ? Number(parsedState.scrollTop) : 0,
22
+ };
23
+
24
+ branches.forEach((branch) => {
25
+ const pagePath = branch.dataset.pagePath ?? '';
26
+ branch.open = branch.dataset.currentBranch === 'true'
27
+ || storedState.openPaths.includes(pagePath);
28
+
29
+ if (storedState.sectionPaths.includes(pagePath)) {
30
+ branch.dataset.sectionsOpen = 'true';
31
+ }
32
+ });
33
+
34
+ if (storedState.scrollTop > 0) {
35
+ requestAnimationFrame(() => {
36
+ container.scrollTop = storedState.scrollTop;
37
+ });
38
+ }
39
+ } catch {
40
+ // Navigation remains fully usable when browser storage is unavailable.
41
+ }
42
+
43
+ const saveState = () => {
44
+ try {
45
+ const openBranches = branches.filter((branch) => branch.open);
46
+ const state: NavigationState = {
47
+ openPaths: openBranches.map((branch) => branch.dataset.pagePath ?? ''),
48
+ sectionPaths: openBranches
49
+ .filter((branch) => (
50
+ branch.dataset.sectionsOpen === 'true'
51
+ || branch.dataset.currentPage === 'true'
52
+ ))
53
+ .map((branch) => branch.dataset.pagePath ?? ''),
54
+ scrollTop: container.scrollTop,
55
+ };
56
+ sessionStorage.setItem(stateKey, JSON.stringify(state));
57
+ } catch {
58
+ // Navigation state persistence is an optional enhancement.
59
+ }
60
+ };
61
+
62
+ requestAnimationFrame(() => {
63
+ branches.forEach((branch) => branch.addEventListener('toggle', () => {
64
+ if (branch.open && branch.dataset.hasSections === 'true') {
65
+ branch.dataset.sectionsOpen = 'true';
66
+ } else if (!branch.open) {
67
+ delete branch.dataset.sectionsOpen;
68
+ }
69
+ saveState();
70
+ }));
71
+ });
72
+
73
+ container.addEventListener('scroll', saveState, { passive: true });
74
+ container.querySelectorAll('a').forEach((link) => link.addEventListener('click', saveState));
75
+ window.addEventListener('pagehide', saveState);
76
+ };
77
+
78
+ const desktopNavigation = document.querySelector<HTMLElement>('.tree-local-navigation');
79
+ const mobileNavigation = document.querySelector<HTMLElement>('.mobile-site-nav[data-navigation-root]');
80
+
81
+ if (desktopNavigation) setupNavigationState(desktopNavigation, 'desktop');
82
+ if (mobileNavigation) setupNavigationState(mobileNavigation, 'mobile');
83
+ </script>
@@ -23,21 +23,15 @@ const siteThemeSchema = themeVisualSchema;
23
23
 
24
24
  const site = defineCollection({
25
25
  loader: glob({
26
- pattern: ['content.md', 'pages/**/content.md'],
26
+ pattern: 'pages/**/content.md',
27
27
  base: pathToFileURL(siteDir),
28
28
  generateId: ({ entry }) => {
29
- if (entry === 'content.md') {
30
- return siteEntryId;
31
- }
32
-
33
29
  const pageEntryDirectory = entry.split('/').slice(1, -1).join('/');
34
30
  const pageDirectory = pageEntryDirectory
35
31
  .split('/')
36
32
  .filter((segment) => segment !== 'pages')
37
33
  .join('/pages/');
38
- return pageDirectory
39
- ? `${siteEntryId.replace(/-content$/, '')}-page-${encodePageDirectoryPath(parsePageDirectoryPath(pageDirectory, `page directory pages/${pageEntryDirectory}`).pageDirectory)}`
40
- : entry.replace(/[^a-zA-Z0-9-]+/g, '-').replace(/^-+|-+$/g, '');
34
+ return `${siteEntryId.replace(/-content$/, '')}-page-${encodePageDirectoryPath(parsePageDirectoryPath(pageDirectory, `page directory pages/${pageEntryDirectory}`).pageDirectory)}`;
41
35
  },
42
36
  }),
43
37
  schema: siteSchema,
@@ -20,6 +20,7 @@ type Presentation = {
20
20
  type VisualTheme = {
21
21
  layout: {
22
22
  gutter: { desktop: string; mobile: string };
23
+ localNavigationGap: string;
23
24
  pageWidth: string;
24
25
  spacing: {
25
26
  firstSectionTop: { desktop: string; mobile: string };
@@ -36,6 +37,11 @@ type VisualTheme = {
36
37
  maxAvailableHeightPercent: { desktop: number; mobile: number };
37
38
  };
38
39
  typography: { fontFamily: string };
40
+ shape: {
41
+ radiusSmall: string;
42
+ radiusMedium: string;
43
+ radiusLarge: string;
44
+ };
39
45
  };
40
46
 
41
47
  interface Props {
@@ -72,7 +78,11 @@ const documentStyle = [
72
78
  `scroll-behavior: ${scrollBehavior}`,
73
79
  ...Object.entries(getPresentationCssVariables(presentation)).map(([name, value]) => `${name}: ${value}`),
74
80
  `--font-sans: ${visualTheme.typography.fontFamily}`,
81
+ `--shape-radius-small: ${visualTheme.shape.radiusSmall}`,
82
+ `--shape-radius-medium: ${visualTheme.shape.radiusMedium}`,
83
+ `--shape-radius-large: ${visualTheme.shape.radiusLarge}`,
75
84
  `--page-width: ${visualTheme.layout.pageWidth}`,
85
+ `--local-navigation-gap: ${visualTheme.layout.localNavigationGap}`,
76
86
  `--content-gutter-desktop: ${visualTheme.layout.gutter.desktop}`,
77
87
  `--content-gutter-mobile: ${visualTheme.layout.gutter.mobile}`,
78
88
  `--space-nav-to-first-section-desktop: ${layoutSpacing.firstSectionTop.desktop}`,
@@ -3,7 +3,9 @@ import path from 'node:path';
3
3
  import {
4
4
  validatePageThemeYamlStructure,
5
5
  } from '../../scripts/lib/site-content.mjs';
6
+ import { pageThemeSchema } from '../../scripts/lib/schema-definitions.mjs';
6
7
  import { sitePagesDir, sitePagesLabel } from '../../scripts/lib/site-paths.mjs';
8
+ import { mergePageThemeConfig } from '../../scripts/lib/theme-presets.mjs';
7
9
  import { parseYamlConfig } from '../../scripts/lib/yaml-config.mjs';
8
10
  import { getPageDirectoryAncestors } from '../../scripts/lib/page-model.mjs';
9
11
 
@@ -15,7 +17,9 @@ type PageTheme = {
15
17
  export const getPageTheme = async (pageDirectory: string | null): Promise<PageTheme | null> => {
16
18
  if (!pageDirectory) return null;
17
19
 
18
- const pageAncestors = getPageDirectoryAncestors(pageDirectory).reverse();
20
+ const pageAncestors = getPageDirectoryAncestors(pageDirectory);
21
+ let inheritedData: Record<string, unknown> = {};
22
+ const inheritedIds: string[] = [];
19
23
  for (const pageAncestor of pageAncestors) {
20
24
  const themeSegments = pageAncestor.split('/');
21
25
  const themePath = path.join(sitePagesDir, ...themeSegments, 'theme.yaml');
@@ -27,16 +31,14 @@ export const getPageTheme = async (pageDirectory: string | null): Promise<PageTh
27
31
  const themeLabel = `${sitePagesLabel}/${themeSegments.join('/')}/theme.yaml`;
28
32
 
29
33
  const data = parseYamlConfig(source, themeLabel, {
34
+ schema: pageThemeSchema,
30
35
  validateStructure: validatePageThemeYamlStructure,
31
36
  });
32
- if (Object.hasOwn(data, 'navigation')) {
33
- throw new Error(`${themeLabel} may not define navigation. Set the site-wide navigation mode in the root theme.yaml.`);
34
- }
35
- return {
36
- id: `pages/${themeSegments.join('/')}/theme`,
37
- data,
38
- };
37
+ inheritedData = mergePageThemeConfig(inheritedData, data);
38
+ inheritedIds.push(`pages/${themeSegments.join('/')}/theme`);
39
39
  }
40
40
 
41
- return null;
41
+ return inheritedIds.length > 0
42
+ ? { id: inheritedIds.join(' + '), data: inheritedData }
43
+ : null;
42
44
  };
@@ -246,11 +246,7 @@ const applyH3HeadingIds = (
246
246
  return result;
247
247
  };
248
248
 
249
- const getImageSourceKey = (page: SitePage, image: string) => (
250
- page.pageDirectory
251
- ? `pages/${page.pageDirectory}/images/${image}`
252
- : `images/${image}`
253
- );
249
+ const getImageSourceKey = (page: SitePage, image: string) => `pages/${page.pageDirectory}/images/${image}`;
254
250
 
255
251
  const renderInlineNoteMarkdown = async (markdown: string) => {
256
252
  const result = await markdownToHtml(markdown, {
@@ -1,8 +1,9 @@
1
- import type { SectionNavigation } from './sectionContent';
1
+ import type { HeadingNavigation, SectionNavigation } from './sectionContent';
2
2
  import type { SitePage } from './sitePages';
3
3
 
4
4
  export type PageNavigationEntry = {
5
5
  page: SitePage;
6
+ headings: HeadingNavigation[];
6
7
  sections: SectionNavigation[];
7
8
  };
8
9
 
@@ -1,5 +1,6 @@
1
1
  import type { CollectionEntry } from 'astro:content';
2
2
  import { decodePageDirectoryPath, parsePageDirectoryPath } from '../../scripts/lib/page-model.mjs';
3
+ import { homePageDirectory } from '../../scripts/lib/site-conventions.mjs';
3
4
 
4
5
  type SiteEntry = CollectionEntry<'site'>;
5
6
 
@@ -14,7 +15,7 @@ export type SitePage = {
14
15
  pageDirectories: string[];
15
16
  pathSegment: string;
16
17
  pathname: string;
17
- pageDirectory: string | null;
18
+ pageDirectory: string;
18
19
  pageId: string;
19
20
  pageIds: string[];
20
21
  pageOrders: number[];
@@ -37,13 +38,17 @@ const stripPageIdPrefix = (id: string) => {
37
38
  }
38
39
  };
39
40
 
40
- export const isHomePageEntry = (entry: SiteEntry) => stripPageIdPrefix(entry.id) === null;
41
+ export const isHomePageEntry = (entry: SiteEntry) => stripPageIdPrefix(entry.id) === homePageDirectory;
41
42
 
42
- const getPageDirectory = (entry: SiteEntry) => isHomePageEntry(entry) ? null : stripPageIdPrefix(entry.id);
43
+ const getPageDirectory = (entry: SiteEntry) => {
44
+ const pageDirectory = stripPageIdPrefix(entry.id);
45
+ if (!pageDirectory) throw new Error(`Page entry "${entry.id}" has no valid page directory.`);
46
+ return pageDirectory;
47
+ };
43
48
 
44
49
  const getPageMetadata = (entry: SiteEntry) => {
45
50
  const pageDirectory = getPageDirectory(entry);
46
- return pageDirectory ? parsePageDirectoryPath(pageDirectory, `page directory ${pageDirectory}`) : null;
51
+ return parsePageDirectoryPath(pageDirectory, `page directory ${pageDirectory}`);
47
52
  };
48
53
 
49
54
  const getPagePathname = (pathSegment: string) => pathSegment ? `/${pathSegment}/` : '/';
@@ -78,29 +83,29 @@ const getPageTitle = (entry: SiteEntry) => {
78
83
  export const getSitePage = (entry: SiteEntry): SitePage => {
79
84
  const isHome = isHomePageEntry(entry);
80
85
  const pageMetadata = getPageMetadata(entry);
81
- const pageDirectory = pageMetadata?.pageDirectory ?? null;
82
- const pageId = pageMetadata?.pageId ?? '';
83
- const pagePath = pageMetadata?.pagePath ?? '';
86
+ const pageDirectory = pageMetadata.pageDirectory;
87
+ const pageId = pageMetadata.pageId;
88
+ const pagePath = pageMetadata.pagePath;
84
89
  const pathSegment = isHome ? '' : pagePath;
85
90
  const navigation = entry.data.navigation ?? {};
86
91
 
87
92
  return {
88
93
  entry,
89
94
  isHome,
90
- depth: pageMetadata?.depth ?? 0,
95
+ depth: pageMetadata.depth,
91
96
  navigation: {
92
97
  listed: navigation.listed ?? true,
93
- order: isHome ? 0 : pageMetadata?.pageOrder ?? 100,
98
+ order: isHome ? 0 : pageMetadata.pageOrder,
94
99
  },
95
100
  pathSegment,
96
101
  pathname: getPagePathname(pathSegment),
97
102
  pageDirectory,
98
- pageDirectories: pageMetadata?.pageDirectories ?? [],
103
+ pageDirectories: pageMetadata.pageDirectories,
99
104
  pageId,
100
- pageIds: pageMetadata?.pageIds ?? [],
101
- pageOrders: pageMetadata?.pageOrders ?? [],
105
+ pageIds: pageMetadata.pageIds,
106
+ pageOrders: pageMetadata.pageOrders,
102
107
  pagePath,
103
- parentPagePath: pageMetadata?.parentPagePath ?? null,
108
+ parentPagePath: isHome ? null : pageMetadata.parentPagePath,
104
109
  title: getPageTitle(entry),
105
110
  };
106
111
  };
@@ -112,6 +117,9 @@ export const getSitePages = (entries: SiteEntry[]) => {
112
117
  const siblingPageOrders = new Map<string, SitePage>();
113
118
 
114
119
  for (const page of pages) {
120
+ if (page.isHome && page.navigation.listed === false) {
121
+ throw new Error(`${homePageDirectory} is the required homepage and cannot set navigation.listed to false.`);
122
+ }
115
123
  const existing = pathnames.get(page.pathname);
116
124
  if (existing) {
117
125
  throw new Error(`Duplicate page path "${page.pathname}" from "${existing.entry.id}" and "${page.entry.id}".`);
@@ -121,7 +129,7 @@ export const getSitePages = (entries: SiteEntry[]) => {
121
129
 
122
130
  if (page.isHome) continue;
123
131
 
124
- const siblingScope = page.parentPagePath ?? '';
132
+ const siblingScope = page.pageDirectories.slice(0, -1).join('/pages/');
125
133
  const pageIdKey = `${siblingScope}\0${page.pageId}`;
126
134
  const existingPageId = siblingPageIds.get(pageIdKey);
127
135
  if (existingPageId) {
@@ -138,6 +146,9 @@ export const getSitePages = (entries: SiteEntry[]) => {
138
146
 
139
147
  siblingPageOrders.set(pageOrderKey, page);
140
148
  }
149
+ if (!pages.some(({ isHome }) => isHome)) {
150
+ throw new Error(`Homepage page entry ${homePageDirectory} is missing.`);
151
+ }
141
152
 
142
153
  return pages.sort((left, right) => (
143
154
  left.isHome ? -1 : right.isHome ? 1 :
@@ -2,9 +2,10 @@
2
2
  import { getCollection } from 'astro:content';
3
3
  import SitePage from '../components/SitePage.astro';
4
4
  import { isHomePageEntry } from '../lib/sitePages';
5
+ import { homePageDirectory } from '../../scripts/lib/site-conventions.mjs';
5
6
 
6
7
  const siteDirectory = (process.env.NORNA_SITE_DIR ?? 'site').trim();
7
- const siteContentLabel = `${siteDirectory || 'site'}/content.md`;
8
+ const siteContentLabel = `${siteDirectory || 'site'}/pages/${homePageDirectory}/content.md`;
8
9
  const entries = await getCollection('site');
9
10
  const site = entries.find(isHomePageEntry);
10
11