@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
@@ -35,13 +35,14 @@ try {
35
35
  assert.ok(metadata.description);
36
36
  const resolved = resolveThemeConfig({ preset: presetName }, 'test theme');
37
37
  assert.equal(resolved.preset, presetName);
38
- assert.ok(resolved.layout?.density);
39
- assert.equal(resolved.navigation?.mode, 'automatic');
38
+ assert.ok(resolved.layout?.contentSpacing);
39
+ assert.ok(resolved.layout?.textWidth);
40
+ assert.ok(resolved.shape);
40
41
  assert.ok(resolved.images?.width);
41
42
  assert.ok(resolved.typography?.fontFamily);
42
43
  assert.ok(resolved.typography?.profile);
43
44
  assert.ok(resolved.palette);
44
- assert.ok(resolved.sectionSurfaces);
45
+ assert.ok(resolved.sections?.backgroundPattern);
45
46
  }
46
47
 
47
48
  const overridden = resolveThemeConfig({
@@ -49,23 +50,20 @@ try {
49
50
  layout: { pageWidth: '1300px' },
50
51
  palette: 'dark',
51
52
  }, 'test theme');
52
- assert.equal(overridden.layout.density, 'compact');
53
- assert.equal(overridden.navigation.mode, 'automatic');
53
+ assert.equal(overridden.layout.contentSpacing, 'compact');
54
+ assert.equal(overridden.layout.textWidth, 'narrow');
54
55
  assert.equal(overridden.layout.pageWidth, '1300px');
56
+ assert.equal(overridden.layout.localNavigationGap, 'clamp(1.5rem, 3vw, 3rem)');
55
57
  assert.equal(overridden.images.width, '920px');
56
58
  assert.equal(overridden.palette, 'dark');
57
- assert.deepEqual(overridden.sectionSurfaces, ['base', 'soft']);
59
+ assert.equal(overridden.sections.backgroundPattern, 'alternating');
58
60
  assert.throws(
59
61
  () => resolveThemeConfig({ preset: 'unknown' }, 'test/theme.yaml'),
60
62
  /Unknown theme preset "unknown" in test\/theme\.yaml.*portfolio, documentation, project, statement/,
61
63
  );
62
64
  assert.throws(
63
- () => resolveThemePresentation({ sectionSurfaces: ['base', 'glowing'] }, 'test/theme.yaml'),
64
- /Unknown section surface "glowing" in test\/theme\.yaml.*base, soft, emphasis/,
65
- );
66
- assert.throws(
67
- () => resolveThemePresentation({ sectionSurfaces: ['base', 'base'] }, 'test/theme.yaml'),
68
- /Each section surface may appear only once in test\/theme\.yaml/,
65
+ () => resolveThemePresentation({ sections: { backgroundPattern: 'glowing' } }, 'test/theme.yaml'),
66
+ /sections\.backgroundPattern must be one of uniform, alternating, cycling in test\/theme\.yaml/,
69
67
  );
70
68
 
71
69
  const listResult = runCli(['theme:presets']);
@@ -89,9 +87,10 @@ try {
89
87
  assert.equal(entry.description, metadata.description);
90
88
  }
91
89
 
90
+ await mkdir(path.join(siteDir, 'pages', '000-home'), { recursive: true });
92
91
  await mkdir(path.join(siteDir, 'pages', '010-guide'), { recursive: true });
93
- await writeFile(path.join(siteDir, 'config.yaml'), 'url: https://example.com/\n');
94
- await writeFile(path.join(siteDir, 'content.md'), `---
92
+ await writeFile(path.join(siteDir, 'config.yaml'), 'url: https://example.com/\nnavigation:\n mode: top\n');
93
+ await writeFile(path.join(siteDir, 'pages', '000-home', 'content.md'), `---
95
94
  page:
96
95
  description: Root page
97
96
  ---
@@ -115,10 +114,13 @@ page:
115
114
  # Guide
116
115
  Page content.
117
116
  `);
118
- await writeFile(path.join(siteDir, 'pages', '010-guide', 'theme.yaml'), `preset: portfolio
119
- layout:
120
- pageWidth: 1010px
121
- palette: light
117
+ await writeFile(path.join(siteDir, 'pages', '010-guide', 'theme.yaml'), `layout:
118
+ contentSpacing: spacious
119
+ textWidth: wide
120
+ images:
121
+ width: 700px
122
+ sections:
123
+ backgroundPattern: cycling
122
124
  `);
123
125
 
124
126
  const buildResult = runCli(['build']);
@@ -128,28 +130,31 @@ palette: light
128
130
  assert.match(rootHtml, /--page-width: 1300px/);
129
131
  assert.match(rootHtml, /--font-sans: Georgia, 'Times New Roman', serif/);
130
132
  assert.match(rootHtml, /--color-page: #000000/);
131
- assert.match(pageHtml, /--page-width: 1010px/);
132
- assert.match(pageHtml, /--font-sans: 'Helvetica Neue', Arial, sans-serif/);
133
- assert.match(pageHtml, /--color-page: #ffffff/);
133
+ assert.match(pageHtml, /--page-width: 1300px/);
134
+ assert.match(pageHtml, /--font-sans: Georgia, 'Times New Roman', serif/);
135
+ assert.match(pageHtml, /--color-page: #000000/);
136
+ assert.match(pageHtml, /--image-width: 700px/);
137
+ assert.match(pageHtml, /--space-section-to-section-desktop: clamp\(2\.25rem, 5vw, 4\.5rem\)/);
134
138
  assert.match(rootHtml, /data-navigation-mode="top"/);
135
139
  assert.match(pageHtml, /data-navigation-mode="top"/);
136
140
 
137
141
  const typographyResult = runCli(['typography', 'show']);
138
142
  assert.equal(typographyResult.status, 0, typographyResult.stderr || typographyResult.stdout);
139
143
  assert.match(typographyResult.stdout, /value: reading/);
140
- assert.match(typographyResult.stdout, /value: restrained/);
144
+ assert.doesNotMatch(typographyResult.stdout, /value: restrained/);
141
145
 
142
146
  const pageThemePath = path.join(siteDir, 'pages', '010-guide', 'theme.yaml');
143
147
  const pageThemeSource = await readFile(pageThemePath, 'utf8');
144
148
  await writeFile(pageThemePath, 'preset: unknown\n');
145
149
  const invalidPagePresetResult = runCli(['config:check']);
146
150
  assert.notEqual(invalidPagePresetResult.status, 0);
147
- assert.match(invalidPagePresetResult.stderr, /Unknown theme preset "unknown" in .*pages\/010-guide\/theme\.yaml/);
151
+ assert.match(invalidPagePresetResult.stderr, /page themes may not define site-wide visual identity through "preset"/);
148
152
  await writeFile(pageThemePath, pageThemeSource);
149
153
  await writeFile(pageThemePath, 'navigation:\n mode: sections\n');
150
154
  const pageNavigationResult = runCli(['config:check']);
151
155
  assert.notEqual(pageNavigationResult.status, 0);
152
- assert.match(pageNavigationResult.stderr, /page themes may not define navigation/);
156
+ assert.match(pageNavigationResult.stderr, /navigation is technical, site-wide configuration/);
157
+ assert.match(pageNavigationResult.stderr, /config\.yaml/);
153
158
  await writeFile(pageThemePath, pageThemeSource);
154
159
 
155
160
  const exportResult = runCli(['theme:export', 'documentation']);
@@ -162,10 +167,11 @@ palette: light
162
167
  assert.match(exportedSource, /# Alternatives: dark, light, paper\./);
163
168
  const exportedConfig = load(exportedSource);
164
169
  assert.equal(exportedConfig.preset, 'documentation');
165
- assert.equal(exportedConfig.navigation.mode, 'automatic');
170
+ assert.equal(exportedConfig.navigation, undefined);
171
+ assert.equal(exportedConfig.shape, themePresets.documentation.shape);
166
172
  assert.equal(exportedConfig.layout.pageWidth, themePresets.documentation.layout.pageWidth);
167
173
  assert.equal(exportedConfig.typography.fontFamily, themePresets.documentation.typography.fontFamily);
168
- assert.deepEqual(exportedConfig.sectionSurfaces, ['base', 'soft']);
174
+ assert.equal(exportedConfig.sections.backgroundPattern, 'alternating');
169
175
 
170
176
  const exportAgainResult = runCli(['theme:export', 'documentation']);
171
177
  assert.notEqual(exportAgainResult.status, 0);
@@ -176,7 +182,7 @@ palette: light
176
182
  assert.notEqual(unknownExportResult.status, 0);
177
183
  assert.match(unknownExportResult.stderr, /Unknown theme preset "unknown"/);
178
184
 
179
- console.log('ok - complete theme presets resolve for root and page themes and export as protected references');
185
+ console.log('ok - complete root presets and limited inherited page themes resolve and export as protected references');
180
186
  } finally {
181
187
  await rm(tempRoot, { force: true, recursive: true });
182
188
  }
@@ -3,22 +3,31 @@ import projectConfig from '../../scripts/lib/project-config.mjs';
3
3
  import { withBasePath } from '../lib/basePath';
4
4
  import type { PageNavigationNode } from '../lib/siteNavigation';
5
5
  import type { SitePage } from '../lib/sitePages';
6
+ import PageContentsLinks from './PageContentsLinks.astro';
6
7
 
7
8
  interface Props {
8
9
  nodes: PageNavigationNode[];
9
10
  currentPage: SitePage;
10
- variant: 'mobile' | 'submenu';
11
+ variant: 'mobile' | 'sidebar' | 'submenu';
12
+ integratePageSections?: boolean;
11
13
  }
12
14
 
13
- const { nodes, currentPage, variant } = Astro.props;
15
+ const { nodes, currentPage, variant, integratePageSections = false } = Astro.props;
14
16
  const pageHref = (page: SitePage) => withBasePath(projectConfig.site.basePath, page.pathname);
15
17
  const hasBranch = (node: PageNavigationNode) => node.children.length > 0;
16
18
  const isCurrentPage = (page: SitePage) => page.pathname === currentPage.pathname;
17
19
  const isCurrentBranch = (page: SitePage) => (
18
- page.isHome
19
- ? currentPage.isHome
20
- : currentPage.pagePath === page.pagePath || currentPage.pagePath.startsWith(`${page.pagePath}/`)
20
+ currentPage.pageDirectory === page.pageDirectory
21
+ || currentPage.pageDirectory.startsWith(`${page.pageDirectory}/pages/`)
21
22
  );
23
+ const getBranchId = (page: SitePage) => [
24
+ 'navigation',
25
+ variant,
26
+ page.pagePath || 'home',
27
+ 'children',
28
+ ].join('-').replace(/[^a-z0-9-]+/gi, '-');
29
+ const hasPageSections = (node: PageNavigationNode) => node.headings.some((heading) => heading.depth === 2);
30
+ const hasPageDisclosure = (node: PageNavigationNode) => hasBranch(node) || hasPageSections(node);
22
31
  ---
23
32
 
24
33
  <ul class:list={['navigation-page-tree', `navigation-page-tree-${variant}`]}>
@@ -29,27 +38,63 @@ const isCurrentBranch = (page: SitePage) => (
29
38
  'navigation-page-node-current-branch': isCurrentBranch(node.page),
30
39
  }]}
31
40
  >
32
- {variant === 'mobile' && hasBranch(node) ? (
33
- <details class="mobile-navigation-branch" open={isCurrentBranch(node.page)}>
34
- <summary><span>{node.page.title}</span></summary>
35
- <div class="mobile-navigation-branch-content">
36
- <ul class="mobile-navigation-page-links">
37
- <li>
38
- <a
39
- class="mobile-nav-destination mobile-nav-overview"
40
- href={pageHref(node.page)}
41
- aria-current={isCurrentPage(node.page) ? 'page' : undefined}
41
+ {variant !== 'submenu' && integratePageSections ? (
42
+ hasPageDisclosure(node) ? (
43
+ <details
44
+ class:list={['navigation-page-branch', 'navigation-page-disclosure', `navigation-page-disclosure-${variant}`]}
45
+ data-page-path={node.page.pagePath || '/'}
46
+ data-current-branch={isCurrentBranch(node.page) ? 'true' : undefined}
47
+ data-current-page={isCurrentPage(node.page) ? 'true' : undefined}
48
+ data-has-sections={hasPageSections(node) ? 'true' : undefined}
49
+ open={isCurrentBranch(node.page)}
50
+ >
51
+ <summary
52
+ class="navigation-page-summary"
53
+ aria-controls={getBranchId(node.page)}
54
+ >
55
+ <span class="navigation-page-summary-title">{node.page.title}</span>
56
+ <span class="navigation-page-chevron" aria-hidden="true"></span>
57
+ </summary>
58
+ <a
59
+ class:list={['navigation-page-open-link', { 'mobile-nav-destination': variant === 'mobile' }]}
60
+ href={pageHref(node.page)}
61
+ aria-current={isCurrentPage(node.page) ? 'page' : undefined}
62
+ >
63
+ {node.page.title}
64
+ </a>
65
+ <div id={getBranchId(node.page)} class="navigation-page-branch-content">
66
+ {hasPageSections(node) && (
67
+ <nav
68
+ class="navigation-page-sections"
69
+ aria-label={`${projectConfig.locale.labels.pageNavigation}: ${node.page.title}`}
42
70
  >
43
- {projectConfig.locale.labels.navigationOverview}
44
- </a>
45
- </li>
46
- </ul>
47
- {node.children.length > 0 && (
48
- <Astro.self nodes={node.children} currentPage={currentPage} variant={variant} />
49
- )}
50
- </div>
51
- </details>
52
- ) : (
71
+ <PageContentsLinks
72
+ headings={node.headings}
73
+ variant={variant === 'mobile' ? 'mobile' : 'desktop'}
74
+ pageHref={isCurrentPage(node.page) ? undefined : pageHref(node.page)}
75
+ />
76
+ </nav>
77
+ )}
78
+ {hasBranch(node) && (
79
+ <Astro.self
80
+ nodes={node.children}
81
+ currentPage={currentPage}
82
+ variant={variant}
83
+ integratePageSections={integratePageSections}
84
+ />
85
+ )}
86
+ </div>
87
+ </details>
88
+ ) : (
89
+ <a
90
+ class:list={['navigation-page-link', { 'mobile-nav-destination': variant === 'mobile' }]}
91
+ href={pageHref(node.page)}
92
+ aria-current={isCurrentPage(node.page) ? 'page' : undefined}
93
+ >
94
+ {node.page.title}
95
+ </a>
96
+ )
97
+ ) : variant !== 'submenu' ? (
53
98
  <>
54
99
  <a
55
100
  class:list={['navigation-page-link', { 'mobile-nav-destination': variant === 'mobile' }]}
@@ -58,6 +103,50 @@ const isCurrentBranch = (page: SitePage) => (
58
103
  >
59
104
  {node.page.title}
60
105
  </a>
106
+ {variant === 'sidebar' && isCurrentPage(node.page) && hasPageSections(node) && (
107
+ <nav
108
+ class="navigation-page-sections"
109
+ aria-label={`${projectConfig.locale.labels.pageNavigation}: ${node.page.title}`}
110
+ >
111
+ <PageContentsLinks
112
+ headings={node.headings}
113
+ variant="desktop"
114
+ />
115
+ </nav>
116
+ )}
117
+ {hasBranch(node) && (
118
+ <details
119
+ class="navigation-page-branch"
120
+ data-page-path={node.page.pagePath || '/'}
121
+ data-current-branch={isCurrentBranch(node.page) ? 'true' : undefined}
122
+ open={isCurrentBranch(node.page)}
123
+ >
124
+ <summary aria-controls={getBranchId(node.page)}>
125
+ <span class="visually-hidden">
126
+ {projectConfig.locale.labels.navigationChildren}: {node.page.title}
127
+ </span>
128
+ <span aria-hidden="true"></span>
129
+ </summary>
130
+ <div id={getBranchId(node.page)} class="navigation-page-branch-content">
131
+ <Astro.self
132
+ nodes={node.children}
133
+ currentPage={currentPage}
134
+ variant={variant}
135
+ integratePageSections={integratePageSections}
136
+ />
137
+ </div>
138
+ </details>
139
+ )}
140
+ </>
141
+ ) : (
142
+ <>
143
+ <a
144
+ class="navigation-page-link"
145
+ href={pageHref(node.page)}
146
+ aria-current={isCurrentPage(node.page) ? 'page' : undefined}
147
+ >
148
+ {node.page.title}
149
+ </a>
61
150
  {node.children.length > 0 && (
62
151
  <Astro.self nodes={node.children} currentPage={currentPage} variant={variant} />
63
152
  )}
@@ -0,0 +1,40 @@
1
+ ---
2
+ import type { HeadingNavigation } from '../lib/sectionContent';
3
+
4
+ interface Props {
5
+ headings: HeadingNavigation[];
6
+ variant: 'desktop' | 'mobile';
7
+ pageHref?: string;
8
+ }
9
+
10
+ const { headings, variant, pageHref = '' } = Astro.props;
11
+ const sections = headings
12
+ .filter((heading) => heading.depth === 2)
13
+ .map((heading) => ({
14
+ ...heading,
15
+ children: headings.filter((candidate) => (
16
+ candidate.depth === 3 && candidate.parentId === heading.id
17
+ )),
18
+ }));
19
+ ---
20
+
21
+ <ol class:list={['page-contents-links', `page-contents-links-${variant}`]}>
22
+ {sections.map((section) => (
23
+ <li>
24
+ <a class={variant === 'mobile' ? 'mobile-nav-destination' : undefined} href={`${pageHref}#${section.id}`}>
25
+ {section.title}
26
+ </a>
27
+ {section.children.length > 0 && (
28
+ <ol>
29
+ {section.children.map((heading) => (
30
+ <li>
31
+ <a class={variant === 'mobile' ? 'mobile-nav-destination' : undefined} href={`${pageHref}#${heading.id}`}>
32
+ {heading.title}
33
+ </a>
34
+ </li>
35
+ ))}
36
+ </ol>
37
+ )}
38
+ </li>
39
+ ))}
40
+ </ol>
@@ -2,7 +2,7 @@
2
2
  const siteTop = document.querySelector<HTMLElement>('.site-top');
3
3
  const mobileMenu = document.querySelector<HTMLDetailsElement>('.mobile-nav-menu');
4
4
  const sectionLinks = Array.from(document.querySelectorAll<HTMLAnchorElement>(
5
- '.mobile-nav-sections a, .page-nav a',
5
+ '.mobile-nav-sections a, .page-contents-links a, .page-nav a',
6
6
  ));
7
7
  const submenuItems = Array.from(document.querySelectorAll<HTMLElement>('.site-nav-item-has-submenu'));
8
8
  const mobileDestinationLinks = Array.from(mobileMenu?.querySelectorAll<HTMLAnchorElement>('.mobile-nav-destination') ?? []);
@@ -40,9 +40,12 @@
40
40
  const updateCurrentSection = () => {
41
41
  sectionLinks.forEach((link) => {
42
42
  const linkUrl = new URL(link.href, window.location.href);
43
- const isCurrent = linkUrl.pathname === window.location.pathname
44
- && linkUrl.hash !== ''
45
- && linkUrl.hash === window.location.hash;
43
+ const isPageTop = link.hasAttribute('data-page-top');
44
+ const isCurrent = linkUrl.pathname === window.location.pathname && (
45
+ isPageTop
46
+ ? window.location.hash === ''
47
+ : linkUrl.hash !== '' && linkUrl.hash === window.location.hash
48
+ );
46
49
 
47
50
  if (isCurrent) {
48
51
  link.setAttribute('aria-current', 'location');
@@ -67,7 +70,11 @@
67
70
 
68
71
  mobileMenu.addEventListener('toggle', () => {
69
72
  document.documentElement.classList.toggle('mobile-navigation-open', mobileMenu.open);
70
- if (mobileMenu.open) window.requestAnimationFrame(() => mobileMenu.querySelector<HTMLElement>('h2')?.focus());
73
+ if (mobileMenu.open) {
74
+ window.requestAnimationFrame(() => (
75
+ mobileMenu.querySelector<HTMLElement>('.mobile-nav-view h2, .mobile-nav-view a')?.focus()
76
+ ));
77
+ }
71
78
  });
72
79
 
73
80
  mobileMenu.addEventListener('keydown', (event) => {
@@ -10,20 +10,23 @@ interface Props {
10
10
 
11
11
  const { currentPage, pages } = Astro.props;
12
12
  const pagesByPath = new Map(pages.map((page) => [page.pagePath, page]));
13
- const ancestors = currentPage.pageIds
14
- .slice(0, -1)
15
- .map((_, index) => pagesByPath.get(currentPage.pageIds.slice(0, index + 1).join('/')))
16
- .filter((page): page is SitePage => page !== undefined);
13
+ const ancestors: SitePage[] = [];
14
+ let parentPagePath = currentPage.parentPagePath;
15
+ while (parentPagePath !== null) {
16
+ const parentPage = pagesByPath.get(parentPagePath);
17
+ if (!parentPage) break;
18
+ ancestors.unshift(parentPage);
19
+ parentPagePath = parentPage.parentPagePath;
20
+ }
17
21
  const pageHref = (page: SitePage) => withBasePath(projectConfig.site.basePath, page.pathname);
22
+ const breadcrumbPages = ancestors;
18
23
  ---
19
24
 
20
- {ancestors.length > 0 && (
21
- <nav class="site-breadcrumbs" aria-label={projectConfig.locale.labels.breadcrumb}>
22
- <ol>
23
- {ancestors.map((page) => (
24
- <li><a href={pageHref(page)}>{page.title}</a></li>
25
- ))}
26
- <li><span aria-current="page">{currentPage.title}</span></li>
27
- </ol>
28
- </nav>
29
- )}
25
+ <nav class="site-breadcrumbs" aria-label={projectConfig.locale.labels.breadcrumb}>
26
+ <ol>
27
+ {breadcrumbPages.map((page) => (
28
+ <li><a href={pageHref(page)}>{page.title}</a></li>
29
+ ))}
30
+ <li><span aria-current="page">{currentPage.title}</span></li>
31
+ </ol>
32
+ </nav>
@@ -40,14 +40,20 @@ const showSiteNavigation = siteNavigationPages.some(({ page }) => !page.isHome);
40
40
  const homePage = pageNavigation.find(({ page }) => page.isHome)?.page ?? currentPage;
41
41
  const currentPageNavigation = pageNavigation.find(({ page }) => page.pathname === currentPage.pathname);
42
42
  const isCurrentPage = (page: SitePage) => page.pathname === currentPage.pathname;
43
+ const isCurrentBranch = (page: SitePage) => currentPage.pageDirectory === page.pageDirectory
44
+ || currentPage.pageDirectory.startsWith(`${page.pageDirectory}/pages/`);
43
45
  const pageHref = (page: SitePage) => withBasePath(projectConfig.site.basePath, page.pathname);
44
46
  const hasSectionMenu = (pageSections: SectionNavigation[]) => pageSections.length > 1;
45
47
  const hasCurrentPageSectionMenu = Boolean(
46
48
  currentPageNavigation && hasSectionMenu(currentPageNavigation.sections),
47
49
  );
48
- const showMobileNavigation = showSiteNavigation || hasCurrentPageSectionMenu;
50
+ const hasTreePageContents = Boolean(
51
+ navigationMode === 'tree' && currentPageNavigation?.headings.length,
52
+ );
53
+ const showSinglePageTopLink = !showSiteNavigation;
54
+ const showMobileNavigation = showSiteNavigation || hasCurrentPageSectionMenu || hasTreePageContents;
49
55
  const showNavigationRow = showMobileNavigation || Boolean(logo);
50
- const hasSectionNavigation = hasCurrentPageSectionMenu;
56
+ const needsNavigationScript = hasCurrentPageSectionMenu || hasTreePageContents || navigationMode === 'tree';
51
57
  ---
52
58
 
53
59
  <a class="skip-link" href="#main-content">{projectConfig.locale.labels.skipToContent}</a>
@@ -70,13 +76,13 @@ const hasSectionNavigation = hasCurrentPageSectionMenu;
70
76
  <ul>
71
77
  {topLevelNavigation.map((node) => (
72
78
  <li class:list={['site-nav-item', {
73
- 'site-nav-item-has-submenu': node.children.length > 0,
74
- 'site-nav-item-current-branch': !node.page.isHome && currentPage.pagePath.startsWith(`${node.page.pagePath}/`),
79
+ 'site-nav-item-has-submenu': navigationMode !== 'tree' && node.children.length > 0,
80
+ 'site-nav-item-current-branch': !isCurrentPage(node.page) && isCurrentBranch(node.page),
75
81
  }]}>
76
82
  <a href={pageHref(node.page)} aria-current={isCurrentPage(node.page) ? 'page' : undefined}>
77
83
  {node.page.title}
78
84
  </a>
79
- {node.children.length > 0 && (
85
+ {navigationMode !== 'tree' && node.children.length > 0 && (
80
86
  <div class="site-nav-submenu">
81
87
  <nav aria-label={`${node.page.title} ${projectConfig.locale.labels.siteNavigation}`}>
82
88
  <NavigationPageTree nodes={[node]} currentPage={currentPage} variant="submenu" />
@@ -94,18 +100,36 @@ const hasSectionNavigation = hasCurrentPageSectionMenu;
94
100
  <span>{projectConfig.locale.labels.navigationMenu}</span>
95
101
  </summary>
96
102
  <div class="mobile-nav-panel">
97
- <div class="mobile-site-nav">
103
+ <div class="mobile-site-nav" data-navigation-root={pageHref(homePage)}>
98
104
  {showSiteNavigation && (
99
105
  <nav class="mobile-nav-view" aria-label={projectConfig.locale.labels.siteNavigation}>
100
106
  <h2 tabindex="-1">{projectConfig.locale.labels.siteNavigation}</h2>
101
- <NavigationPageTree nodes={siteNavigationTree} currentPage={currentPage} variant="mobile" />
107
+ <NavigationPageTree
108
+ nodes={siteNavigationTree}
109
+ currentPage={currentPage}
110
+ variant="mobile"
111
+ integratePageSections={navigationMode === 'tree'}
112
+ />
102
113
  </nav>
103
114
  )}
104
115
 
105
- {hasCurrentPageSectionMenu && currentPageNavigation && (
116
+ {navigationMode !== 'tree' && hasCurrentPageSectionMenu && currentPageNavigation && (
106
117
  <nav class="mobile-nav-view" aria-label={projectConfig.locale.labels.pageNavigation}>
107
- <h2 tabindex="-1">{projectConfig.locale.labels.pageNavigation}</h2>
118
+ {!showSinglePageTopLink && (
119
+ <h2 tabindex="-1">{projectConfig.locale.labels.pageNavigation}</h2>
120
+ )}
108
121
  <ul class="mobile-nav-sections">
122
+ {showSinglePageTopLink && (
123
+ <li class="mobile-nav-page-top-item">
124
+ <a
125
+ class="mobile-nav-destination mobile-nav-page-top"
126
+ data-page-top
127
+ href={pageHref(currentPage)}
128
+ >
129
+ {currentPage.title}
130
+ </a>
131
+ </li>
132
+ )}
109
133
  {currentPageNavigation.sections.map((section) => (
110
134
  <li>
111
135
  <a class="mobile-nav-destination" href={`#${section.id}`}>{section.title}</a>
@@ -120,11 +144,20 @@ const hasSectionNavigation = hasCurrentPageSectionMenu;
120
144
  </div>
121
145
  )}
122
146
 
123
- {hasCurrentPageSectionMenu && currentPageNavigation && (
147
+ {navigationMode !== 'tree' && hasCurrentPageSectionMenu && currentPageNavigation && (
124
148
  <nav class="page-nav" aria-label={projectConfig.locale.labels.pageNavigation}>
125
149
  <div class="page-nav-inner">
126
- <span class="page-nav-label">{projectConfig.locale.labels.pageNavigation}</span>
150
+ {!showSinglePageTopLink && (
151
+ <span class="page-nav-label">{projectConfig.locale.labels.pageNavigation}</span>
152
+ )}
127
153
  <ul>
154
+ {showSinglePageTopLink && (
155
+ <li class="page-nav-page-top-item">
156
+ <a class="page-nav-page-top" data-page-top href={pageHref(currentPage)}>
157
+ {currentPage.title}
158
+ </a>
159
+ </li>
160
+ )}
128
161
  {currentPageNavigation.sections.map((section) => (
129
162
  <li>
130
163
  <a href={`#${section.id}`}>{section.title}</a>
@@ -137,4 +170,4 @@ const hasSectionNavigation = hasCurrentPageSectionMenu;
137
170
 
138
171
  </header>
139
172
 
140
- {hasSectionNavigation && <SectionNavigationScript />}
173
+ {needsNavigationScript && <SectionNavigationScript />}
@@ -7,6 +7,8 @@ import SiteNavigation from './SiteNavigation.astro';
7
7
  import SiteBanners from './SiteBanners.astro';
8
8
  import SiteNotesScript from './SiteNotesScript.astro';
9
9
  import SiteSection from './SiteSection.astro';
10
+ import SiteTreeNavigation from './SiteTreeNavigation.astro';
11
+ import TreeNavigationScript from './TreeNavigationScript.astro';
10
12
  import { getHeadingNavigation, getSectionsContent, type ResolvedSection } from '../lib/sectionContent';
11
13
  import { getPageTheme } from '../lib/pageThemes';
12
14
  import { getSitePage, getSitePages, type SitePage } from '../lib/sitePages';
@@ -14,9 +16,10 @@ import { getToday, isVisibleToday } from '../lib/visibility';
14
16
  import { getLogoAsset } from '../lib/sitePublicAssets';
15
17
  import { readSiteFile } from '../../scripts/lib/site-content.mjs';
16
18
  import { resolveNavigationModel } from '../../scripts/lib/navigation-model.mjs';
17
- import { siteContentPath, sitePagesDir } from '../../scripts/lib/site-paths.mjs';
19
+ import { sitePagesDir } from '../../scripts/lib/site-paths.mjs';
18
20
  import { projectConfig, resolveThemeVisualConfig } from '../../scripts/lib/project-config.mjs';
19
- import { resolveFrameColors, resolvePagePresentation } from '../../scripts/lib/presentation.mjs';
21
+ import { getTextWidthCssValue, resolveFrameColors, resolvePagePresentation } from '../../scripts/lib/presentation.mjs';
22
+ import { mergePageThemeConfig } from '../../scripts/lib/theme-presets.mjs';
20
23
 
21
24
  type SiteEntry = CollectionEntry<'site'>;
22
25
 
@@ -28,15 +31,10 @@ const { entry } = Astro.props;
28
31
  const siteDirectory = (process.env.NORNA_SITE_DIR ?? 'site').trim();
29
32
  const sitePages = getSitePages(await getCollection('site'));
30
33
  const currentPage = getSitePage(entry);
31
- const getPageContentFile = (page: SitePage) => page.pageDirectory
32
- ? {
34
+ const getPageContentFile = (page: SitePage) => ({
33
35
  label: `${siteDirectory || 'site'}/pages/${page.pageDirectory}/content.md`,
34
36
  path: path.join(sitePagesDir, page.pageDirectory, 'content.md'),
35
- }
36
- : {
37
- label: `${siteDirectory || 'site'}/content.md`,
38
- path: siteContentPath,
39
- };
37
+ });
40
38
  const pageContentFile = getPageContentFile(currentPage);
41
39
  const siteContentLabel = pageContentFile.label;
42
40
  const rawMarkdown = (await readSiteFile(pageContentFile.path, pageContentFile.label)).body;
@@ -49,7 +47,7 @@ const sitewideData = sitewide?.data ?? {};
49
47
  const homePage = sitePages.find((page) => page.isHome) ?? currentPage;
50
48
  const siteThemeData = siteTheme.data;
51
49
  const pageTheme = await getPageTheme(currentPage.pageDirectory);
52
- const themeData = pageTheme?.data ?? siteThemeData;
50
+ const themeData = mergePageThemeConfig(siteThemeData, pageTheme?.data);
53
51
  const themeLabel = pageTheme
54
52
  ? `${siteDirectory || 'site'}/${pageTheme.id}`
55
53
  : `${siteDirectory || 'site'}/${siteTheme.id}`;
@@ -64,7 +62,8 @@ const logo = logoAsset
64
62
  }
65
63
  : undefined;
66
64
  const pagePresentation = resolvePagePresentation(themeData, themeLabel);
67
- const frameColors = resolveFrameColors({ theme: themeData, sourceLabel: themeLabel });
65
+ const pageTextWidth = getTextWidthCssValue(pagePresentation.typography.values.body.width);
66
+ const frameColors = resolveFrameColors({ theme: siteThemeData, sourceLabel: `${siteDirectory || 'site'}/${siteTheme.id}` });
68
67
  const siteHtml = entry.rendered?.html ?? '';
69
68
  const resolvedSections = await getSectionsContent(siteHtml, rawMarkdown, currentPage, siteContentLabel);
70
69
  const today = getToday();
@@ -109,6 +108,9 @@ const navigationModel = resolveNavigationModel({
109
108
  listed: page.navigation.listed,
110
109
  })),
111
110
  });
111
+ const hasTreeNavigation = navigationModel.mode === 'tree';
112
+ const showBreadcrumbs = !currentPage.isHome && currentPage.parentPagePath !== null;
113
+ const reserveBreadcrumbSpace = hasTreeNavigation || showBreadcrumbs;
112
114
 
113
115
  ---
114
116
 
@@ -130,17 +132,29 @@ const navigationModel = resolveNavigationModel({
130
132
  />
131
133
  {siteBanners.length > 0 && <SiteBanners banners={siteBanners} />}
132
134
 
133
- <main class:list={['site-content', { 'site-content-has-breadcrumbs': currentPage.depth > 1 }]} id="main-content">
134
- {currentPage.depth > 1 && <SiteBreadcrumbs currentPage={currentPage} pages={sitePages} />}
135
- {renderedSections.map(({ section, sectionIndex, prioritizeFirstImage }) => (
136
- <SiteSection
137
- section={section}
138
- pagePresentation={pagePresentation}
139
- visualTheme={visualTheme}
140
- sectionIndex={sectionIndex}
141
- prioritizeFirstImage={prioritizeFirstImage}
142
- />
143
- ))}
144
- </main>
135
+ <div class:list={['site-page-layout', { 'site-page-layout-tree': hasTreeNavigation }]}>
136
+ {hasTreeNavigation && (
137
+ <SiteTreeNavigation currentPage={currentPage} pageNavigation={pageNavigation} />
138
+ )}
139
+ <main
140
+ class:list={['site-content', { 'site-content-has-breadcrumbs': reserveBreadcrumbSpace }]}
141
+ id="main-content"
142
+ style={`--page-text-width: ${pageTextWidth}`}
143
+ >
144
+ {showBreadcrumbs
145
+ ? <SiteBreadcrumbs currentPage={currentPage} pages={sitePages} />
146
+ : reserveBreadcrumbSpace && <div class="site-breadcrumbs" aria-hidden="true"></div>}
147
+ {renderedSections.map(({ section, sectionIndex, prioritizeFirstImage }) => (
148
+ <SiteSection
149
+ section={section}
150
+ pagePresentation={pagePresentation}
151
+ visualTheme={visualTheme}
152
+ sectionIndex={sectionIndex}
153
+ prioritizeFirstImage={prioritizeFirstImage}
154
+ />
155
+ ))}
156
+ </main>
157
+ </div>
158
+ {hasTreeNavigation && <TreeNavigationScript />}
145
159
  {hasMarginNotes && <SiteNotesScript />}
146
160
  </BaseLayout>