@janga/norna 0.7.16 → 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 (342) hide show
  1. package/README.md +18 -9
  2. package/astro.config.mjs +35 -3
  3. package/bin/norna-cli.mjs +1 -1
  4. package/docs/README.md +16 -14
  5. package/docs/commands.md +2 -2
  6. package/docs/configuration.md +25 -2
  7. package/docs/content.md +92 -103
  8. package/docs/design/navigation-and-theme-plan.md +186 -0
  9. package/docs/design/norna-diagram-design.md +9 -9
  10. package/docs/design/site-examples-structure.md +15 -11
  11. package/docs/engine-development.md +19 -22
  12. package/docs/getting-started.md +21 -16
  13. package/docs/images-and-metadata.md +12 -9
  14. package/docs/pages.md +190 -0
  15. package/docs/public-files.md +49 -36
  16. package/docs/publishing.md +19 -3
  17. package/docs/site-files.md +46 -38
  18. package/docs/sitewide-content.md +20 -36
  19. package/docs/theme.md +109 -143
  20. package/docs/typography.md +19 -31
  21. package/examples/README.md +10 -4
  22. package/examples/complete-sites/dog-shelter-multi-page/.astro/collections/site.schema.json +17 -41
  23. package/examples/complete-sites/dog-shelter-multi-page/.astro/collections/sitewide.schema.json +26 -67
  24. package/examples/complete-sites/dog-shelter-multi-page/.astro/collections/theme.schema.json +185 -88
  25. package/examples/complete-sites/dog-shelter-multi-page/.astro/data-store.json +1 -1
  26. package/examples/complete-sites/dog-shelter-multi-page/.astro/dev-local.json +4 -4
  27. package/examples/complete-sites/dog-shelter-multi-page/.astro/dev.json +5 -5
  28. package/examples/complete-sites/dog-shelter-multi-page/.astro/dev.log +33 -23
  29. package/examples/complete-sites/dog-shelter-multi-page/README.md +1 -1
  30. package/examples/complete-sites/dog-shelter-multi-page/node_modules/.vite/deps/_metadata.json +8 -8
  31. package/examples/complete-sites/dog-shelter-multi-page/site/.norna/generated-images.json +20 -20
  32. package/examples/complete-sites/dog-shelter-multi-page/site/{content.md → pages/000-home/content.md} +3 -4
  33. package/examples/complete-sites/dog-shelter-multi-page/site/{routes → pages}/010-dogs/content.md +3 -4
  34. package/examples/complete-sites/dog-shelter-multi-page/site/{routes → pages}/020-adopt/content.md +3 -4
  35. package/examples/complete-sites/dog-shelter-multi-page/site/sitewide-content.yaml +2 -4
  36. package/examples/complete-sites/dog-shelter-single-page/.astro/collections/site.schema.json +17 -41
  37. package/examples/complete-sites/dog-shelter-single-page/.astro/collections/sitewide.schema.json +26 -67
  38. package/examples/complete-sites/dog-shelter-single-page/.astro/collections/theme.schema.json +185 -88
  39. package/examples/complete-sites/dog-shelter-single-page/.astro/data-store.json +1 -1
  40. package/examples/complete-sites/dog-shelter-single-page/.astro/dev-local.json +8 -0
  41. package/examples/complete-sites/dog-shelter-single-page/.astro/dev.json +5 -5
  42. package/examples/complete-sites/dog-shelter-single-page/.astro/dev.log +16 -12
  43. package/examples/complete-sites/dog-shelter-single-page/README.md +1 -1
  44. package/examples/complete-sites/dog-shelter-single-page/node_modules/.vite/deps/_metadata.json +8 -8
  45. package/examples/complete-sites/dog-shelter-single-page/site/.norna/generated-images.json +12 -12
  46. package/examples/complete-sites/dog-shelter-single-page/site/{content.md → pages/000-home/content.md} +5 -2
  47. package/examples/complete-sites/dog-shelter-single-page/site/sitewide-content.yaml +2 -4
  48. package/examples/feature-demos/media-and-surfaces/.astro/collections/site.schema.json +17 -41
  49. package/examples/feature-demos/media-and-surfaces/.astro/collections/sitewide.schema.json +26 -67
  50. package/examples/feature-demos/media-and-surfaces/.astro/collections/theme.schema.json +180 -91
  51. package/examples/feature-demos/media-and-surfaces/.astro/data-store.json +1 -1
  52. package/examples/feature-demos/media-and-surfaces/.astro/dev.log +4 -14
  53. package/examples/feature-demos/media-and-surfaces/README.md +2 -2
  54. package/examples/feature-demos/media-and-surfaces/node_modules/.vite/deps/_metadata.json +9 -9
  55. package/examples/feature-demos/media-and-surfaces/site/.norna/generated-images.json +16 -16
  56. package/examples/feature-demos/media-and-surfaces/site/{content.md → pages/000-home/content.md} +4 -7
  57. package/examples/feature-demos/media-and-surfaces/site/{routes → pages}/010-media/content.md +6 -5
  58. package/examples/feature-demos/media-and-surfaces/site/{routes → pages}/020-surfaces/content.md +9 -7
  59. package/examples/feature-demos/media-and-surfaces/site/pages/020-surfaces/theme.yaml +2 -0
  60. package/examples/feature-demos/media-and-surfaces/site/sitewide-content.yaml +0 -2
  61. package/examples/feature-demos/sitewide-content/README.md +3 -3
  62. package/examples/feature-demos/sitewide-content/site/pages/000-home/content.md +17 -0
  63. package/examples/feature-demos/sitewide-content/site/pages/010-logo-and-navigation/content.md +17 -0
  64. package/examples/feature-demos/sitewide-content/site/{routes → pages}/020-notices/content.md +7 -6
  65. package/examples/feature-demos/sitewide-content/site/{routes → pages}/030-footer/content.md +6 -5
  66. package/examples/feature-demos/sitewide-content/site/sitewide-content.yaml +4 -13
  67. package/examples/feature-demos/theme-preset-documentation/README.md +16 -0
  68. package/examples/feature-demos/theme-preset-documentation/site/.norna/generated-images.json +74 -0
  69. package/examples/feature-demos/{theme-presets/site/routes/020-documentation → theme-preset-documentation/site/pages/000-home}/content.md +4 -7
  70. package/examples/feature-demos/theme-preset-documentation/site/sitewide-content.yaml +3 -0
  71. package/examples/feature-demos/theme-preset-portfolio/README.md +16 -0
  72. package/examples/feature-demos/theme-preset-portfolio/site/.norna/generated-images.json +74 -0
  73. package/examples/feature-demos/theme-preset-portfolio/site/config.yaml +1 -0
  74. package/examples/feature-demos/{theme-presets/site/routes/010-portfolio → theme-preset-portfolio/site/pages/000-home}/content.md +4 -7
  75. package/examples/feature-demos/theme-preset-portfolio/site/public/robots.txt +2 -0
  76. package/examples/feature-demos/theme-preset-portfolio/site/sitewide-content.yaml +3 -0
  77. package/examples/feature-demos/theme-preset-project/README.md +16 -0
  78. package/examples/feature-demos/theme-preset-project/site/.norna/generated-images.json +74 -0
  79. package/examples/feature-demos/theme-preset-project/site/config.yaml +1 -0
  80. package/examples/feature-demos/{theme-presets/site/routes/030-project → theme-preset-project/site/pages/000-home}/content.md +4 -7
  81. package/examples/feature-demos/theme-preset-project/site/public/robots.txt +2 -0
  82. package/examples/feature-demos/theme-preset-project/site/sitewide-content.yaml +3 -0
  83. package/examples/feature-demos/theme-preset-statement/README.md +16 -0
  84. package/examples/feature-demos/theme-preset-statement/site/.norna/generated-images.json +74 -0
  85. package/examples/feature-demos/theme-preset-statement/site/config.yaml +1 -0
  86. package/examples/feature-demos/{theme-presets/site/routes/040-statement → theme-preset-statement/site/pages/000-home}/content.md +4 -7
  87. package/examples/feature-demos/theme-preset-statement/site/public/robots.txt +2 -0
  88. package/examples/feature-demos/theme-preset-statement/site/sitewide-content.yaml +3 -0
  89. package/fixtures/basic/site/{content.md → pages/000-home/content.md} +5 -6
  90. package/fixtures/basic/site/sitewide-content.yaml +1 -8
  91. package/fixtures/basic/site/theme.yaml +1 -8
  92. package/fixtures/content-model-v2/site/{content.md → pages/000-home/content.md} +11 -13
  93. package/fixtures/content-model-v2/site/pages/010-guide/content.md +18 -0
  94. package/fixtures/nested-pages/.astro/collections/site.schema.json +35 -0
  95. package/fixtures/nested-pages/.astro/collections/sitewide.schema.json +97 -0
  96. package/fixtures/nested-pages/.astro/collections/theme.schema.json +844 -0
  97. package/fixtures/nested-pages/.astro/content-assets.mjs +4 -0
  98. package/fixtures/nested-pages/.astro/content-modules.mjs +1 -0
  99. package/fixtures/nested-pages/.astro/content.d.ts +183 -0
  100. package/fixtures/nested-pages/.astro/data-store.json +1 -0
  101. package/fixtures/nested-pages/.astro/dev-local.json +8 -0
  102. package/fixtures/nested-pages/.astro/dev.log +1128 -0
  103. package/fixtures/nested-pages/.astro/settings.json +5 -0
  104. package/fixtures/nested-pages/.astro/types.d.ts +2 -0
  105. package/fixtures/nested-pages/node_modules/.vite/deps/_metadata.json +74 -0
  106. package/fixtures/nested-pages/node_modules/.vite/deps/astro-D_GIpJIE.js +434 -0
  107. package/fixtures/nested-pages/node_modules/.vite/deps/astro-D_GIpJIE.js.map +1 -0
  108. package/fixtures/nested-pages/node_modules/.vite/deps/astro_n_aria-query.js +6390 -0
  109. package/fixtures/nested-pages/node_modules/.vite/deps/astro_n_aria-query.js.map +1 -0
  110. package/fixtures/nested-pages/node_modules/.vite/deps/astro_n_axobject-query.js +2702 -0
  111. package/fixtures/nested-pages/node_modules/.vite/deps/astro_n_axobject-query.js.map +1 -0
  112. package/fixtures/nested-pages/node_modules/.vite/deps/astro_n_html-escaper.js +65 -0
  113. package/fixtures/nested-pages/node_modules/.vite/deps/astro_n_html-escaper.js.map +1 -0
  114. package/fixtures/nested-pages/node_modules/.vite/deps/astro_runtime_client_dev-toolbar_entrypoint__js.js +455 -0
  115. package/fixtures/nested-pages/node_modules/.vite/deps/astro_runtime_client_dev-toolbar_entrypoint__js.js.map +1 -0
  116. package/fixtures/nested-pages/node_modules/.vite/deps/audit-DM9Hkl5B.js +1534 -0
  117. package/fixtures/nested-pages/node_modules/.vite/deps/audit-DM9Hkl5B.js.map +1 -0
  118. package/fixtures/nested-pages/node_modules/.vite/deps/highlight-DnZ1ULOL.js +48 -0
  119. package/fixtures/nested-pages/node_modules/.vite/deps/highlight-DnZ1ULOL.js.map +1 -0
  120. package/fixtures/nested-pages/node_modules/.vite/deps/icons-RyebKi45.js +42 -0
  121. package/fixtures/nested-pages/node_modules/.vite/deps/icons-RyebKi45.js.map +1 -0
  122. package/fixtures/nested-pages/node_modules/.vite/deps/package.json +3 -0
  123. package/fixtures/nested-pages/node_modules/.vite/deps/rolldown-runtime-BvCyGRYZ.js +4 -0
  124. package/fixtures/nested-pages/node_modules/.vite/deps/settings-DaU-gNnK.js +190 -0
  125. package/fixtures/nested-pages/node_modules/.vite/deps/settings-DaU-gNnK.js.map +1 -0
  126. package/fixtures/nested-pages/node_modules/.vite/deps/toolbar-63PA619H.js +458 -0
  127. package/fixtures/nested-pages/node_modules/.vite/deps/toolbar-63PA619H.js.map +1 -0
  128. package/fixtures/nested-pages/node_modules/.vite/deps/ui-library--bkcNSYz.js +1016 -0
  129. package/fixtures/nested-pages/node_modules/.vite/deps/ui-library--bkcNSYz.js.map +1 -0
  130. package/fixtures/nested-pages/node_modules/.vite/deps/window-jPkpKVcy.js +32 -0
  131. package/fixtures/nested-pages/node_modules/.vite/deps/window-jPkpKVcy.js.map +1 -0
  132. package/fixtures/nested-pages/node_modules/.vite/deps/xray-BXCHpEJP.js +111 -0
  133. package/fixtures/nested-pages/node_modules/.vite/deps/xray-BXCHpEJP.js.map +1 -0
  134. package/fixtures/nested-pages/site/.norna/generated-images.json +10 -0
  135. package/fixtures/nested-pages/site/config.yaml +3 -0
  136. package/fixtures/nested-pages/site/pages/000-home/content.md +15 -0
  137. package/fixtures/nested-pages/site/pages/010-guides/content.md +15 -0
  138. package/fixtures/nested-pages/site/pages/010-guides/pages/010-installation/content.md +21 -0
  139. package/fixtures/nested-pages/site/pages/010-guides/pages/010-installation/images/diagram.svg +21 -0
  140. package/fixtures/nested-pages/site/pages/010-guides/pages/010-installation/pages/010-macos/content.md +18 -0
  141. package/fixtures/nested-pages/site/pages/010-guides/pages/010-installation/theme.yaml +2 -0
  142. package/fixtures/nested-pages/site/pages/010-guides/pages/020-workflows/content.md +11 -0
  143. package/fixtures/nested-pages/site/pages/010-guides/pages/030-release-notes/content.md +9 -0
  144. package/fixtures/nested-pages/site/pages/010-guides/pages/040-configuration/content.md +3 -0
  145. package/fixtures/nested-pages/site/pages/010-guides/pages/050-content/content.md +3 -0
  146. package/fixtures/nested-pages/site/pages/010-guides/pages/060-themes/content.md +3 -0
  147. package/fixtures/nested-pages/site/pages/010-guides/pages/070-images/content.md +3 -0
  148. package/fixtures/nested-pages/site/pages/010-guides/pages/080-cards/content.md +3 -0
  149. package/fixtures/nested-pages/site/pages/010-guides/pages/090-notes/content.md +3 -0
  150. package/fixtures/nested-pages/site/pages/010-guides/pages/100-validation/content.md +3 -0
  151. package/fixtures/nested-pages/site/pages/010-guides/pages/110-synchronization/content.md +3 -0
  152. package/fixtures/nested-pages/site/pages/010-guides/pages/120-building/content.md +3 -0
  153. package/fixtures/nested-pages/site/pages/010-guides/pages/130-publishing/content.md +3 -0
  154. package/fixtures/nested-pages/site/pages/010-guides/pages/140-troubleshooting/content.md +3 -0
  155. package/fixtures/nested-pages/site/pages/010-guides/pages/150-migration/content.md +3 -0
  156. package/fixtures/nested-pages/site/pages/010-guides/pages/160-accessibility/content.md +3 -0
  157. package/fixtures/nested-pages/site/pages/010-guides/pages/170-performance/content.md +3 -0
  158. package/fixtures/nested-pages/site/pages/010-guides/pages/180-security/content.md +3 -0
  159. package/fixtures/nested-pages/site/pages/010-guides/pages/190-localization/content.md +3 -0
  160. package/fixtures/nested-pages/site/pages/010-guides/pages/200-upgrading/content.md +3 -0
  161. package/fixtures/nested-pages/site/pages/010-guides/pages/210-faq/content.md +3 -0
  162. package/fixtures/nested-pages/site/pages/010-guides/theme.yaml +7 -0
  163. package/fixtures/nested-pages/site/pages/020-reference/content.md +10 -0
  164. package/fixtures/nested-pages/site/pages/020-reference/pages/010-installation/content.md +11 -0
  165. package/fixtures/nested-pages/site/theme.yaml +1 -0
  166. package/package.json +8 -4
  167. package/schemas/config.schema.json +46 -5
  168. package/schemas/content-frontmatter.schema.json +28 -63
  169. package/schemas/manifest.json +4 -2
  170. package/schemas/page-theme.schema.json +192 -0
  171. package/schemas/sitewide-content.schema.json +58 -132
  172. package/schemas/theme.schema.json +434 -152
  173. package/scripts/check-config.mjs +8 -6
  174. package/scripts/deploy-site.mjs +6 -8
  175. package/scripts/doctor.mjs +2 -2
  176. package/scripts/generate-images.mjs +15 -29
  177. package/scripts/generate-schemas.mjs +7 -218
  178. package/scripts/lib/base-path-redirect.mjs +9 -0
  179. package/scripts/lib/editor-language-service.mjs +125 -79
  180. package/scripts/lib/example-sites.mjs +15 -3
  181. package/scripts/lib/heading-ids.mjs +161 -0
  182. package/scripts/lib/navigation-model.mjs +51 -0
  183. package/scripts/lib/norna-markdown-blocks.mjs +3 -4
  184. package/scripts/lib/page-model.mjs +102 -0
  185. package/scripts/lib/presentation.mjs +25 -17
  186. package/scripts/lib/project-config.mjs +46 -8
  187. package/scripts/lib/schema-definitions.mjs +78 -55
  188. package/scripts/lib/schema-editor-metadata.mjs +519 -0
  189. package/scripts/lib/schema-value-definitions.mjs +20 -6
  190. package/scripts/lib/site-content.mjs +159 -97
  191. package/scripts/lib/site-conventions.mjs +1 -0
  192. package/scripts/lib/site-paths.mjs +11 -5
  193. package/scripts/lib/theme-config.mjs +12 -18
  194. package/scripts/lib/theme-presets.mjs +55 -14
  195. package/scripts/release.mjs +1 -1
  196. package/scripts/show-typography.mjs +4 -30
  197. package/scripts/sync-content-sections.mjs +107 -68
  198. package/scripts/{test-temporary-visibility.mjs → test-banner-visibility.mjs} +8 -25
  199. package/scripts/test-client-javascript.mjs +50 -46
  200. package/scripts/test-content-check.mjs +153 -69
  201. package/scripts/test-content-model-v2.mjs +566 -285
  202. package/scripts/test-documentation.mjs +10 -3
  203. package/scripts/test-editor-language-service.mjs +172 -25
  204. package/scripts/test-engine-commands.mjs +6 -5
  205. package/scripts/test-examples.mjs +30 -1
  206. package/scripts/test-heading-ids.mjs +68 -0
  207. package/scripts/test-navigation-model.mjs +63 -0
  208. package/scripts/test-navigation.mjs +11 -2
  209. package/scripts/test-nested-pages.mjs +125 -0
  210. package/scripts/test-package-check.mjs +55 -95
  211. package/scripts/test-page-model.mjs +108 -0
  212. package/scripts/test-project-config.mjs +22 -3
  213. package/scripts/test-schemas.mjs +82 -15
  214. package/scripts/test-theme-presets.mjs +57 -42
  215. package/src/components/NavigationPageTree.astro +157 -0
  216. package/src/components/PageContentsLinks.astro +40 -0
  217. package/src/components/SectionNavigationScript.astro +117 -5
  218. package/src/components/SiteBreadcrumbs.astro +32 -0
  219. package/src/components/SiteNavigation.astro +97 -68
  220. package/src/components/SitePage.astro +93 -55
  221. package/src/components/SiteSection.astro +29 -27
  222. package/src/components/SiteTreeNavigation.astro +35 -0
  223. package/src/components/TreeNavigationScript.astro +83 -0
  224. package/src/content.config.ts +8 -10
  225. package/src/layouts/BaseLayout.astro +21 -20
  226. package/src/lib/pageThemes.ts +44 -0
  227. package/src/lib/sectionContent.ts +106 -107
  228. package/src/lib/siteNavigation.ts +49 -0
  229. package/src/lib/sitePages.ts +96 -38
  230. package/src/pages/{[slug].astro → [...slug].astro} +2 -2
  231. package/src/pages/index.astro +2 -1
  232. package/src/styles/global.css +743 -57
  233. package/starters/basic/README.md +9 -11
  234. package/starters/basic/site/{content.md → pages/000-home/content.md} +7 -4
  235. package/starters/basic/site/sitewide-content.yaml +1 -10
  236. package/starters/basic/site/theme.yaml +1 -105
  237. package/starters/project/README.md +14 -14
  238. package/starters/project/site/{content.md → pages/000-home/content.md} +5 -4
  239. package/starters/project/site/{routes → pages}/010-guide/content.md +5 -4
  240. package/starters/project/site/sitewide-content.yaml +1 -10
  241. package/starters/project/site/theme.yaml +1 -31
  242. package/docs/routes.md +0 -121
  243. package/examples/feature-demos/media-and-surfaces/site/routes/020-surfaces/theme.yaml +0 -3
  244. package/examples/feature-demos/sitewide-content/site/content.md +0 -16
  245. package/examples/feature-demos/sitewide-content/site/routes/010-identity/content.md +0 -20
  246. package/examples/feature-demos/theme-presets/README.md +0 -18
  247. package/examples/feature-demos/theme-presets/site/.norna/generated-images.json +0 -290
  248. package/examples/feature-demos/theme-presets/site/content.md +0 -17
  249. package/examples/feature-demos/theme-presets/site/sitewide-content.yaml +0 -12
  250. package/examples/feature-demos/theme-presets/site/theme.yaml +0 -1
  251. package/fixtures/content-model-v2/site/routes/010-guide/content.md +0 -17
  252. package/scripts/lib/route-model.mjs +0 -17
  253. package/scripts/test-route-model.mjs +0 -32
  254. package/src/lib/routeThemes.ts +0 -41
  255. /package/examples/complete-sites/dog-shelter-multi-page/site/{images/intro → pages/000-home/images}/shelter-dog.jpg +0 -0
  256. /package/examples/complete-sites/dog-shelter-multi-page/site/{routes/010-dogs/images/dogs → pages/010-dogs/images}/rex.jpg +0 -0
  257. /package/examples/complete-sites/dog-shelter-multi-page/site/{routes/010-dogs/images/dogs → pages/010-dogs/images}/rover.jpg +0 -0
  258. /package/examples/complete-sites/dog-shelter-multi-page/site/{routes/020-adopt/images/adopt → pages/020-adopt/images}/adoption-dog.jpg +0 -0
  259. /package/examples/complete-sites/dog-shelter-single-page/site/{images/you-can-help → pages/000-home/images}/foster-dog.jpg +0 -0
  260. /package/examples/complete-sites/dog-shelter-single-page/site/{images/what-we-do → pages/000-home/images}/rescue-dog.jpg +0 -0
  261. /package/examples/feature-demos/media-and-surfaces/site/{routes/010-media/images/cards → pages/010-media/images}/card-carousel.svg +0 -0
  262. /package/examples/feature-demos/media-and-surfaces/site/{routes/010-media/images/cards → pages/010-media/images}/card-stack.svg +0 -0
  263. /package/examples/feature-demos/media-and-surfaces/site/{routes/010-media/images/cards → pages/010-media/images}/card-surfaces.svg +0 -0
  264. /package/examples/feature-demos/media-and-surfaces/site/{routes/010-media/images/carousel → pages/010-media/images}/carousel-one.svg +0 -0
  265. /package/examples/feature-demos/media-and-surfaces/site/{routes/010-media/images/carousel → pages/010-media/images}/carousel-three.svg +0 -0
  266. /package/examples/feature-demos/media-and-surfaces/site/{routes/010-media/images/carousel → pages/010-media/images}/carousel-two.svg +0 -0
  267. /package/examples/feature-demos/media-and-surfaces/site/{routes/010-media/images/stack → pages/010-media/images}/stack-one.svg +0 -0
  268. /package/examples/feature-demos/media-and-surfaces/site/{routes/010-media/images/stack → pages/010-media/images}/stack-two.svg +0 -0
  269. /package/examples/feature-demos/{theme-presets → theme-preset-documentation}/site/config.yaml +0 -0
  270. /package/examples/feature-demos/{theme-presets/site/routes/020-documentation/images/documentation → theme-preset-documentation/site/pages/000-home/images}/black-schnauzer.jpg +0 -0
  271. /package/examples/feature-demos/{theme-presets/site/routes/020-documentation/images/documentation → theme-preset-documentation/site/pages/000-home/images}/retriever-portrait.jpg +0 -0
  272. /package/examples/feature-demos/{theme-presets/site/routes/020-documentation/images/documentation → theme-preset-documentation/site/pages/000-home/images}/schnauzer-portrait.jpg +0 -0
  273. /package/examples/feature-demos/{theme-presets → theme-preset-documentation}/site/public/robots.txt +0 -0
  274. /package/examples/feature-demos/{theme-presets/site/routes/020-documentation → theme-preset-documentation/site}/theme.yaml +0 -0
  275. /package/examples/feature-demos/{theme-presets/site/routes/010-portfolio/images/portfolio → theme-preset-portfolio/site/pages/000-home/images}/puppy-portrait.jpg +0 -0
  276. /package/examples/feature-demos/{theme-presets/site/routes/010-portfolio/images/portfolio → theme-preset-portfolio/site/pages/000-home/images}/shepherd-portrait.jpg +0 -0
  277. /package/examples/feature-demos/{theme-presets/site/routes/010-portfolio/images/portfolio → theme-preset-portfolio/site/pages/000-home/images}/white-puppy.jpg +0 -0
  278. /package/examples/feature-demos/{theme-presets/site/routes/010-portfolio → theme-preset-portfolio/site}/theme.yaml +0 -0
  279. /package/examples/feature-demos/{theme-presets → theme-preset-project}/.astro/collections/routeTheme.schema.json +0 -0
  280. /package/examples/feature-demos/{theme-presets → theme-preset-project}/.astro/collections/site.schema.json +0 -0
  281. /package/examples/feature-demos/{theme-presets → theme-preset-project}/.astro/collections/sitewide.schema.json +0 -0
  282. /package/examples/feature-demos/{theme-presets → theme-preset-project}/.astro/collections/theme.schema.json +0 -0
  283. /package/examples/feature-demos/{theme-presets → theme-preset-project}/.astro/content-assets.mjs +0 -0
  284. /package/examples/feature-demos/{theme-presets → theme-preset-project}/.astro/content-modules.mjs +0 -0
  285. /package/examples/feature-demos/{theme-presets → theme-preset-project}/.astro/content.d.ts +0 -0
  286. /package/examples/feature-demos/{theme-presets → theme-preset-project}/.astro/data-store.json +0 -0
  287. /package/examples/feature-demos/{theme-presets → theme-preset-project}/.astro/dev-local.json +0 -0
  288. /package/examples/feature-demos/{theme-presets → theme-preset-project}/.astro/dev.json +0 -0
  289. /package/examples/feature-demos/{theme-presets → theme-preset-project}/.astro/dev.log +0 -0
  290. /package/examples/feature-demos/{theme-presets → theme-preset-project}/.astro/settings.json +0 -0
  291. /package/examples/feature-demos/{theme-presets → theme-preset-project}/.astro/types.d.ts +0 -0
  292. /package/examples/feature-demos/{theme-presets → theme-preset-project}/node_modules/.vite/deps/_metadata.json +0 -0
  293. /package/examples/feature-demos/{theme-presets → theme-preset-project}/node_modules/.vite/deps/astro-D_GIpJIE.js +0 -0
  294. /package/examples/feature-demos/{theme-presets → theme-preset-project}/node_modules/.vite/deps/astro-D_GIpJIE.js.map +0 -0
  295. /package/examples/feature-demos/{theme-presets → theme-preset-project}/node_modules/.vite/deps/astro_n_aria-query.js +0 -0
  296. /package/examples/feature-demos/{theme-presets → theme-preset-project}/node_modules/.vite/deps/astro_n_aria-query.js.map +0 -0
  297. /package/examples/feature-demos/{theme-presets → theme-preset-project}/node_modules/.vite/deps/astro_n_axobject-query.js +0 -0
  298. /package/examples/feature-demos/{theme-presets → theme-preset-project}/node_modules/.vite/deps/astro_n_axobject-query.js.map +0 -0
  299. /package/examples/feature-demos/{theme-presets → theme-preset-project}/node_modules/.vite/deps/astro_n_html-escaper.js +0 -0
  300. /package/examples/feature-demos/{theme-presets → theme-preset-project}/node_modules/.vite/deps/astro_n_html-escaper.js.map +0 -0
  301. /package/examples/feature-demos/{theme-presets → theme-preset-project}/node_modules/.vite/deps/astro_runtime_client_dev-toolbar_entrypoint__js.js +0 -0
  302. /package/examples/feature-demos/{theme-presets → theme-preset-project}/node_modules/.vite/deps/astro_runtime_client_dev-toolbar_entrypoint__js.js.map +0 -0
  303. /package/examples/feature-demos/{theme-presets → theme-preset-project}/node_modules/.vite/deps/audit-DM9Hkl5B.js +0 -0
  304. /package/examples/feature-demos/{theme-presets → theme-preset-project}/node_modules/.vite/deps/audit-DM9Hkl5B.js.map +0 -0
  305. /package/examples/feature-demos/{theme-presets → theme-preset-project}/node_modules/.vite/deps/embla-carousel.js +0 -0
  306. /package/examples/feature-demos/{theme-presets → theme-preset-project}/node_modules/.vite/deps/embla-carousel.js.map +0 -0
  307. /package/examples/feature-demos/{theme-presets → theme-preset-project}/node_modules/.vite/deps/highlight-DnZ1ULOL.js +0 -0
  308. /package/examples/feature-demos/{theme-presets → theme-preset-project}/node_modules/.vite/deps/highlight-DnZ1ULOL.js.map +0 -0
  309. /package/examples/feature-demos/{theme-presets → theme-preset-project}/node_modules/.vite/deps/icons-RyebKi45.js +0 -0
  310. /package/examples/feature-demos/{theme-presets → theme-preset-project}/node_modules/.vite/deps/icons-RyebKi45.js.map +0 -0
  311. /package/examples/feature-demos/{theme-presets → theme-preset-project}/node_modules/.vite/deps/package.json +0 -0
  312. /package/examples/feature-demos/{theme-presets → theme-preset-project}/node_modules/.vite/deps/rolldown-runtime-BvCyGRYZ.js +0 -0
  313. /package/examples/feature-demos/{theme-presets → theme-preset-project}/node_modules/.vite/deps/settings-DaU-gNnK.js +0 -0
  314. /package/examples/feature-demos/{theme-presets → theme-preset-project}/node_modules/.vite/deps/settings-DaU-gNnK.js.map +0 -0
  315. /package/examples/feature-demos/{theme-presets → theme-preset-project}/node_modules/.vite/deps/toolbar-63PA619H.js +0 -0
  316. /package/examples/feature-demos/{theme-presets → theme-preset-project}/node_modules/.vite/deps/toolbar-63PA619H.js.map +0 -0
  317. /package/examples/feature-demos/{theme-presets → theme-preset-project}/node_modules/.vite/deps/ui-library--bkcNSYz.js +0 -0
  318. /package/examples/feature-demos/{theme-presets → theme-preset-project}/node_modules/.vite/deps/ui-library--bkcNSYz.js.map +0 -0
  319. /package/examples/feature-demos/{theme-presets → theme-preset-project}/node_modules/.vite/deps/window-jPkpKVcy.js +0 -0
  320. /package/examples/feature-demos/{theme-presets → theme-preset-project}/node_modules/.vite/deps/window-jPkpKVcy.js.map +0 -0
  321. /package/examples/feature-demos/{theme-presets → theme-preset-project}/node_modules/.vite/deps/xray-BXCHpEJP.js +0 -0
  322. /package/examples/feature-demos/{theme-presets → theme-preset-project}/node_modules/.vite/deps/xray-BXCHpEJP.js.map +0 -0
  323. /package/examples/feature-demos/{theme-presets/site/routes/030-project/images/project → theme-preset-project/site/pages/000-home/images}/boxer-portrait.jpg +0 -0
  324. /package/examples/feature-demos/{theme-presets/site/routes/030-project/images/project → theme-preset-project/site/pages/000-home/images}/grass-puppy.jpg +0 -0
  325. /package/examples/feature-demos/{theme-presets/site/routes/030-project/images/project → theme-preset-project/site/pages/000-home/images}/grey-street-dog.jpg +0 -0
  326. /package/examples/feature-demos/{theme-presets/site/routes/030-project → theme-preset-project/site}/theme.yaml +0 -0
  327. /package/examples/feature-demos/{theme-presets/site/routes/040-statement/images/statement → theme-preset-statement/site/pages/000-home/images}/black-pedigree.jpg +0 -0
  328. /package/examples/feature-demos/{theme-presets/site/routes/040-statement/images/statement → theme-preset-statement/site/pages/000-home/images}/sheepdog-grass.jpg +0 -0
  329. /package/examples/feature-demos/{theme-presets/site/routes/040-statement/images/statement → theme-preset-statement/site/pages/000-home/images}/sheepdog-wide.jpg +0 -0
  330. /package/examples/feature-demos/{theme-presets/site/routes/040-statement → theme-preset-statement/site}/theme.yaml +0 -0
  331. /package/fixtures/basic/site/{images/work → pages/000-home/images}/.gitkeep +0 -0
  332. /package/fixtures/content-model-v2/site/{images/card-section → pages/000-home/images}/adopt.svg +0 -0
  333. /package/fixtures/content-model-v2/site/{images/intro → pages/000-home/images}/detail.jpg +0 -0
  334. /package/fixtures/content-model-v2/site/{images/intro → pages/000-home/images}/duplicate.jpg +0 -0
  335. /package/fixtures/content-model-v2/site/{images/card-section → pages/000-home/images}/foster.svg +0 -0
  336. /package/fixtures/content-model-v2/site/{images/intro → pages/000-home/images}/hero.jpg +0 -0
  337. /package/fixtures/content-model-v2/site/{images/plain → pages/000-home/images}/image.jpg +0 -0
  338. /package/fixtures/content-model-v2/site/{images/timed → pages/000-home/images}/slide-one.jpg +0 -0
  339. /package/fixtures/content-model-v2/site/{images/timed → pages/000-home/images}/slide-three.jpg +0 -0
  340. /package/fixtures/content-model-v2/site/{images/timed → pages/000-home/images}/slide-two.jpg +0 -0
  341. /package/fixtures/content-model-v2/site/{routes/010-guide/images/intro → pages/010-guide/images}/duplicate.jpg +0 -0
  342. /package/starters/basic/site/{images/work → pages/000-home/images}/.gitkeep +0 -0
@@ -10,15 +10,21 @@ const expectedDocumentationRef = `v${packageJson.version}`;
10
10
  const filenames = [
11
11
  'config.schema.json',
12
12
  'theme.schema.json',
13
+ 'page-theme.schema.json',
13
14
  'sitewide-content.schema.json',
14
15
  'content-frontmatter.schema.json',
15
16
  ];
16
17
  const requiredRichHelp = {
17
- 'config.schema.json': ['url', 'language', 'scrollBehavior'],
18
- 'theme.schema.json': ['preset', 'layout', 'images', 'typography', 'palette', 'sectionSurfaces'],
19
- 'sitewide-content.schema.json': ['navigation', 'banners', 'footer'],
20
- 'content-frontmatter.schema.json': ['title', 'description', 'navigation', 'sections'],
18
+ 'config.schema.json': ['url', 'language', 'navigation', 'scrollBehavior'],
19
+ 'theme.schema.json': ['preset', 'shape', 'layout', 'images', 'typography', 'palette', 'sections'],
20
+ 'page-theme.schema.json': ['layout', 'images', 'sections'],
21
+ 'sitewide-content.schema.json': ['logo', 'banners', 'footer'],
22
+ 'content-frontmatter.schema.json': ['page', 'navigation'],
21
23
  };
24
+ const manifest = JSON.parse(await readFile(path.join(root, 'schemas', 'manifest.json'), 'utf8'));
25
+ assert.equal(manifest.editorApiVersion, 1);
26
+ assert.equal(manifest.schemaVersion, 2);
27
+ assert.equal(manifest.files.pageTheme, 'page-theme.schema.json');
22
28
 
23
29
  const githubHeadingAnchor = (heading) => heading
24
30
  .toLowerCase()
@@ -49,22 +55,26 @@ const assertDocumentationLinks = async (markdownDescription, location) => {
49
55
  }
50
56
  };
51
57
 
52
- const visitSchema = (schema, location) => {
58
+ const visitSchema = (schema, location, richProperties) => {
53
59
  assert.equal(schema.enum, undefined, `${location} uses an undescribed enum.`);
54
60
  if (schema.properties) {
55
61
  for (const [name, property] of Object.entries(schema.properties)) {
56
62
  assert.ok(property.description, `${location}.${name} has no description.`);
57
- visitSchema(property, `${location}.${name}`);
63
+ richProperties.push([property, `${location}.${name}`]);
64
+ visitSchema(property, `${location}.${name}`, richProperties);
58
65
  }
59
66
  }
60
- if (schema.items) visitSchema(schema.items, `${location}[]`);
67
+ if (schema.items) visitSchema(schema.items, `${location}[]`, richProperties);
68
+ if (schema.additionalProperties && typeof schema.additionalProperties === 'object') {
69
+ visitSchema(schema.additionalProperties, `${location}.*`, richProperties);
70
+ }
61
71
  for (const keyword of ['allOf', 'anyOf', 'oneOf']) {
62
72
  for (const [index, candidate] of (schema[keyword] ?? []).entries()) {
63
73
  if (Object.hasOwn(candidate, 'const')) {
64
74
  assert.ok(candidate.title, `${location}.${keyword}[${index}] has no title.`);
65
75
  assert.ok(candidate.description, `${location}.${keyword}[${index}] has no description.`);
66
76
  }
67
- visitSchema(candidate, `${location}.${keyword}[${index}]`);
77
+ visitSchema(candidate, `${location}.${keyword}[${index}]`, richProperties);
68
78
  }
69
79
  }
70
80
  };
@@ -83,7 +93,16 @@ for (const filename of filenames) {
83
93
  );
84
94
  await assertDocumentationLinks(property.markdownDescription, `${filename}.${propertyName}`);
85
95
  }
86
- visitSchema(schema, filename);
96
+ const richProperties = [];
97
+ visitSchema(schema, filename, richProperties);
98
+ for (const [property, location] of richProperties) {
99
+ assert.match(
100
+ property.markdownDescription,
101
+ /^```yaml\n/,
102
+ `${location} rich help must start with YAML syntax.`,
103
+ );
104
+ await assertDocumentationLinks(property.markdownDescription, location);
105
+ }
87
106
  }
88
107
 
89
108
  const config = JSON.parse(await readFile(path.join(root, 'schemas', 'config.schema.json'), 'utf8'));
@@ -92,11 +111,59 @@ assert.deepEqual(config.properties.language.examples, ['en', 'sv', 'en-GB', 'sv-
92
111
  assert.equal(config.properties.scrollBehavior.default, 'instant');
93
112
 
94
113
  const sitewide = JSON.parse(await readFile(path.join(root, 'schemas', 'sitewide-content.schema.json'), 'utf8'));
95
- assert.deepEqual(Object.keys(sitewide.properties.navigation.properties), ['label', 'logo']);
96
- assert.deepEqual(Object.keys(sitewide.properties.navigation.properties.logo.properties), ['height']);
97
- assert.match(sitewide.properties.navigation.properties.logo.markdownDescription, /```yaml\nnavigation:\n label: Example Site\n logo:\n height: 2rem\n```/);
98
- assert.match(sitewide.properties.navigation.properties.logo.markdownDescription, /does not enable or select the file/);
99
- assert.match(sitewide.properties.navigation.properties.logo.markdownDescription, /docs\/public-files\.md#navigation-logo/);
100
- assert.equal(sitewide.properties.navigation.properties.logo.description.length < 100, true);
114
+ assert.deepEqual(Object.keys(sitewide.properties.logo.properties), ['height']);
115
+ assert.match(sitewide.properties.logo.markdownDescription, /```yaml\nlogo:\n height: 2rem\n```/);
116
+ assert.match(sitewide.properties.logo.markdownDescription, /does not enable or select the file/);
117
+ assert.match(sitewide.properties.logo.markdownDescription, /homepage Markdown H1/);
118
+ assert.match(sitewide.properties.logo.markdownDescription, /docs\/public-files\.md#navigation-logo/);
119
+ assert.equal(sitewide.properties.logo.description.length < 100, true);
120
+ const bannerItem = sitewide.properties.banners.items;
121
+ assert.equal(bannerItem.title, 'Warning banner');
122
+ assert.match(bannerItem.description, /important temporary information/);
123
+ assert.match(bannerItem.markdownDescription, /A `warning` is a dismissible site-wide notice/);
124
+ await assertDocumentationLinks(bannerItem.markdownDescription, 'sitewide-content.schema.json.banners[]');
125
+ assert.equal(bannerItem.defaultSnippets[0].label, 'Warning banner');
126
+ assert.deepEqual(bannerItem.defaultSnippets[0].body, {
127
+ id: '${1:project-status}',
128
+ tone: 'warning',
129
+ title: '${2:Important notice}',
130
+ text: '${3:Brief explanation.}',
131
+ });
132
+ await assertDocumentationLinks(
133
+ bannerItem.defaultSnippets[0].markdownDescription,
134
+ 'sitewide-content.schema.json.banners[].defaultSnippets[0]',
135
+ );
136
+ const buildInfo = sitewide.properties.footer.properties.buildInfo;
137
+ assert.equal(buildInfo.type, 'boolean');
138
+ assert.equal(buildInfo.default, false);
139
+ assert.match(buildInfo.markdownDescription, /footer:\n buildInfo: true/);
140
+
141
+ const theme = JSON.parse(await readFile(path.join(root, 'schemas', 'theme.schema.json'), 'utf8'));
142
+ assert.equal(theme.properties.layout.properties.gutter.defaultSnippets[0].label, 'Responsive page gutter');
143
+ assert.equal(theme.properties.layout.properties.spacing.defaultSnippets[0].label, 'Layout spacing overrides');
144
+ assert.equal(theme.properties.typography.properties.overrides.defaultSnippets[0].label, 'Typography overrides');
145
+ assert.equal(
146
+ theme.properties.images.properties.maxAvailableWidthPercent.defaultSnippets[0].label,
147
+ 'Responsive image limit',
148
+ );
149
+ assert.equal(theme.properties.navigation, undefined);
150
+ assert.equal(theme.properties.layout.properties.density, undefined);
151
+ assert.ok(theme.properties.layout.properties.contentSpacing);
152
+ assert.ok(theme.properties.layout.properties.textWidth);
153
+ assert.ok(theme.properties.sections.properties.backgroundPattern);
154
+
155
+ const pageTheme = JSON.parse(await readFile(path.join(root, 'schemas', 'page-theme.schema.json'), 'utf8'));
156
+ assert.deepEqual(Object.keys(pageTheme.properties), ['layout', 'images', 'sections']);
157
+ assert.deepEqual(Object.keys(pageTheme.properties.layout.properties), ['contentSpacing', 'textWidth']);
158
+ assert.equal(pageTheme.properties.preset, undefined);
159
+ assert.equal(pageTheme.properties.palette, undefined);
160
+ assert.equal(pageTheme.properties.typography, undefined);
161
+
162
+ const content = JSON.parse(await readFile(path.join(root, 'schemas', 'content-frontmatter.schema.json'), 'utf8'));
163
+ assert.deepEqual(Object.keys(content.properties), ['page', 'navigation']);
164
+ assert.deepEqual(Object.keys(content.properties.page.properties), ['description']);
165
+ assert.equal(content.properties.page.required, undefined);
166
+ assert.equal(content.required, undefined);
167
+ assert.deepEqual(Object.keys(content.properties.navigation.properties), ['listed']);
101
168
 
102
169
  console.log('Schema metadata tests passed.');
@@ -35,12 +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);
38
+ assert.ok(resolved.layout?.contentSpacing);
39
+ assert.ok(resolved.layout?.textWidth);
40
+ assert.ok(resolved.shape);
39
41
  assert.ok(resolved.images?.width);
40
42
  assert.ok(resolved.typography?.fontFamily);
41
43
  assert.ok(resolved.typography?.profile);
42
44
  assert.ok(resolved.palette);
43
- assert.ok(resolved.sectionSurfaces);
45
+ assert.ok(resolved.sections?.backgroundPattern);
44
46
  }
45
47
 
46
48
  const overridden = resolveThemeConfig({
@@ -48,22 +50,20 @@ try {
48
50
  layout: { pageWidth: '1300px' },
49
51
  palette: 'dark',
50
52
  }, 'test theme');
51
- assert.equal(overridden.layout.density, 'compact');
53
+ assert.equal(overridden.layout.contentSpacing, 'compact');
54
+ assert.equal(overridden.layout.textWidth, 'narrow');
52
55
  assert.equal(overridden.layout.pageWidth, '1300px');
56
+ assert.equal(overridden.layout.localNavigationGap, 'clamp(1.5rem, 3vw, 3rem)');
53
57
  assert.equal(overridden.images.width, '920px');
54
58
  assert.equal(overridden.palette, 'dark');
55
- assert.deepEqual(overridden.sectionSurfaces, ['base', 'soft']);
59
+ assert.equal(overridden.sections.backgroundPattern, 'alternating');
56
60
  assert.throws(
57
61
  () => resolveThemeConfig({ preset: 'unknown' }, 'test/theme.yaml'),
58
62
  /Unknown theme preset "unknown" in test\/theme\.yaml.*portfolio, documentation, project, statement/,
59
63
  );
60
64
  assert.throws(
61
- () => resolveThemePresentation({ sectionSurfaces: ['base', 'glowing'] }, 'test/theme.yaml'),
62
- /Unknown section surface "glowing" in test\/theme\.yaml.*base, soft, emphasis/,
63
- );
64
- assert.throws(
65
- () => resolveThemePresentation({ sectionSurfaces: ['base', 'base'] }, 'test/theme.yaml'),
66
- /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/,
67
67
  );
68
68
 
69
69
  const listResult = runCli(['theme:presets']);
@@ -87,13 +87,16 @@ try {
87
87
  assert.equal(entry.description, metadata.description);
88
88
  }
89
89
 
90
- await mkdir(path.join(siteDir, 'routes', '010-guide'), { recursive: true });
91
- await writeFile(path.join(siteDir, 'config.yaml'), 'url: https://example.com/\n');
92
- await writeFile(path.join(siteDir, 'content.md'), `---
93
- title: Theme preset test
94
- description: Root page
90
+ await mkdir(path.join(siteDir, 'pages', '000-home'), { recursive: true });
91
+ await mkdir(path.join(siteDir, 'pages', '010-guide'), { recursive: true });
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'), `---
94
+ page:
95
+ description: Root page
95
96
  ---
96
97
 
98
+ # Theme preset test
99
+
97
100
  ## Home {#home}
98
101
 
99
102
  Root content.
@@ -103,46 +106,56 @@ layout:
103
106
  pageWidth: 1300px
104
107
  palette: dark
105
108
  `);
106
- await writeFile(path.join(siteDir, 'routes', '010-guide', 'content.md'), `---
107
- title: Guide
108
- description: Route page
109
- navigation:
110
- label: Guide
109
+ await writeFile(path.join(siteDir, 'pages', '010-guide', 'content.md'), `---
110
+ page:
111
+ description: Additional page
111
112
  ---
112
113
 
113
- ## Guide {#guide}
114
-
115
- Route content.
114
+ # Guide
115
+ Page content.
116
116
  `);
117
- await writeFile(path.join(siteDir, 'routes', '010-guide', 'theme.yaml'), `preset: portfolio
118
- layout:
119
- pageWidth: 1010px
120
- 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
121
124
  `);
122
125
 
123
126
  const buildResult = runCli(['build']);
124
127
  assert.equal(buildResult.status, 0, buildResult.stderr || buildResult.stdout);
125
128
  const rootHtml = await readFile(path.join(tempRoot, 'dist', 'index.html'), 'utf8');
126
- const routeHtml = await readFile(path.join(tempRoot, 'dist', 'guide', 'index.html'), 'utf8');
129
+ const pageHtml = await readFile(path.join(tempRoot, 'dist', 'guide', 'index.html'), 'utf8');
127
130
  assert.match(rootHtml, /--page-width: 1300px/);
128
131
  assert.match(rootHtml, /--font-sans: Georgia, 'Times New Roman', serif/);
129
132
  assert.match(rootHtml, /--color-page: #000000/);
130
- assert.match(routeHtml, /--page-width: 1010px/);
131
- assert.match(routeHtml, /--font-sans: 'Helvetica Neue', Arial, sans-serif/);
132
- assert.match(routeHtml, /--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\)/);
138
+ assert.match(rootHtml, /data-navigation-mode="top"/);
139
+ assert.match(pageHtml, /data-navigation-mode="top"/);
133
140
 
134
141
  const typographyResult = runCli(['typography', 'show']);
135
142
  assert.equal(typographyResult.status, 0, typographyResult.stderr || typographyResult.stdout);
136
143
  assert.match(typographyResult.stdout, /value: reading/);
137
- assert.match(typographyResult.stdout, /value: restrained/);
138
-
139
- const routeThemePath = path.join(siteDir, 'routes', '010-guide', 'theme.yaml');
140
- const routeThemeSource = await readFile(routeThemePath, 'utf8');
141
- await writeFile(routeThemePath, 'preset: unknown\n');
142
- const invalidRoutePresetResult = runCli(['config:check']);
143
- assert.notEqual(invalidRoutePresetResult.status, 0);
144
- assert.match(invalidRoutePresetResult.stderr, /Unknown theme preset "unknown" in .*routes\/010-guide\/theme\.yaml/);
145
- await writeFile(routeThemePath, routeThemeSource);
144
+ assert.doesNotMatch(typographyResult.stdout, /value: restrained/);
145
+
146
+ const pageThemePath = path.join(siteDir, 'pages', '010-guide', 'theme.yaml');
147
+ const pageThemeSource = await readFile(pageThemePath, 'utf8');
148
+ await writeFile(pageThemePath, 'preset: unknown\n');
149
+ const invalidPagePresetResult = runCli(['config:check']);
150
+ assert.notEqual(invalidPagePresetResult.status, 0);
151
+ assert.match(invalidPagePresetResult.stderr, /page themes may not define site-wide visual identity through "preset"/);
152
+ await writeFile(pageThemePath, pageThemeSource);
153
+ await writeFile(pageThemePath, 'navigation:\n mode: sections\n');
154
+ const pageNavigationResult = runCli(['config:check']);
155
+ assert.notEqual(pageNavigationResult.status, 0);
156
+ assert.match(pageNavigationResult.stderr, /navigation is technical, site-wide configuration/);
157
+ assert.match(pageNavigationResult.stderr, /config\.yaml/);
158
+ await writeFile(pageThemePath, pageThemeSource);
146
159
 
147
160
  const exportResult = runCli(['theme:export', 'documentation']);
148
161
  assert.equal(exportResult.status, 0, exportResult.stderr || exportResult.stdout);
@@ -154,9 +167,11 @@ palette: light
154
167
  assert.match(exportedSource, /# Alternatives: dark, light, paper\./);
155
168
  const exportedConfig = load(exportedSource);
156
169
  assert.equal(exportedConfig.preset, 'documentation');
170
+ assert.equal(exportedConfig.navigation, undefined);
171
+ assert.equal(exportedConfig.shape, themePresets.documentation.shape);
157
172
  assert.equal(exportedConfig.layout.pageWidth, themePresets.documentation.layout.pageWidth);
158
173
  assert.equal(exportedConfig.typography.fontFamily, themePresets.documentation.typography.fontFamily);
159
- assert.deepEqual(exportedConfig.sectionSurfaces, ['base', 'soft']);
174
+ assert.equal(exportedConfig.sections.backgroundPattern, 'alternating');
160
175
 
161
176
  const exportAgainResult = runCli(['theme:export', 'documentation']);
162
177
  assert.notEqual(exportAgainResult.status, 0);
@@ -167,7 +182,7 @@ palette: light
167
182
  assert.notEqual(unknownExportResult.status, 0);
168
183
  assert.match(unknownExportResult.stderr, /Unknown theme preset "unknown"/);
169
184
 
170
- console.log('ok - complete theme presets resolve for root and route themes and export as protected references');
185
+ console.log('ok - complete root presets and limited inherited page themes resolve and export as protected references');
171
186
  } finally {
172
187
  await rm(tempRoot, { force: true, recursive: true });
173
188
  }
@@ -0,0 +1,157 @@
1
+ ---
2
+ import projectConfig from '../../scripts/lib/project-config.mjs';
3
+ import { withBasePath } from '../lib/basePath';
4
+ import type { PageNavigationNode } from '../lib/siteNavigation';
5
+ import type { SitePage } from '../lib/sitePages';
6
+ import PageContentsLinks from './PageContentsLinks.astro';
7
+
8
+ interface Props {
9
+ nodes: PageNavigationNode[];
10
+ currentPage: SitePage;
11
+ variant: 'mobile' | 'sidebar' | 'submenu';
12
+ integratePageSections?: boolean;
13
+ }
14
+
15
+ const { nodes, currentPage, variant, integratePageSections = false } = Astro.props;
16
+ const pageHref = (page: SitePage) => withBasePath(projectConfig.site.basePath, page.pathname);
17
+ const hasBranch = (node: PageNavigationNode) => node.children.length > 0;
18
+ const isCurrentPage = (page: SitePage) => page.pathname === currentPage.pathname;
19
+ const isCurrentBranch = (page: SitePage) => (
20
+ currentPage.pageDirectory === page.pageDirectory
21
+ || currentPage.pageDirectory.startsWith(`${page.pageDirectory}/pages/`)
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);
31
+ ---
32
+
33
+ <ul class:list={['navigation-page-tree', `navigation-page-tree-${variant}`]}>
34
+ {nodes.map((node) => (
35
+ <li
36
+ class:list={['navigation-page-node', {
37
+ 'navigation-page-node-current': isCurrentPage(node.page),
38
+ 'navigation-page-node-current-branch': isCurrentBranch(node.page),
39
+ }]}
40
+ >
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}`}
70
+ >
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' ? (
98
+ <>
99
+ <a
100
+ class:list={['navigation-page-link', { 'mobile-nav-destination': variant === 'mobile' }]}
101
+ href={pageHref(node.page)}
102
+ aria-current={isCurrentPage(node.page) ? 'page' : undefined}
103
+ >
104
+ {node.page.title}
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>
150
+ {node.children.length > 0 && (
151
+ <Astro.self nodes={node.children} currentPage={currentPage} variant={variant} />
152
+ )}
153
+ </>
154
+ )}
155
+ </li>
156
+ ))}
157
+ </ul>
@@ -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>
@@ -1,7 +1,12 @@
1
1
  <script>
2
2
  const siteTop = document.querySelector<HTMLElement>('.site-top');
3
3
  const mobileMenu = document.querySelector<HTMLDetailsElement>('.mobile-nav-menu');
4
- const mobileSectionLinks = Array.from(document.querySelectorAll<HTMLAnchorElement>('.mobile-page-nav a'));
4
+ const sectionLinks = Array.from(document.querySelectorAll<HTMLAnchorElement>(
5
+ '.mobile-nav-sections a, .page-contents-links a, .page-nav a',
6
+ ));
7
+ const submenuItems = Array.from(document.querySelectorAll<HTMLElement>('.site-nav-item-has-submenu'));
8
+ const mobileDestinationLinks = Array.from(mobileMenu?.querySelectorAll<HTMLAnchorElement>('.mobile-nav-destination') ?? []);
9
+ const mobileMenuSummary = mobileMenu?.querySelector<HTMLElement>(':scope > summary');
5
10
 
6
11
  const setAnchorOffset = () => {
7
12
  if (!siteTop) return;
@@ -10,17 +15,124 @@
10
15
  document.documentElement.style.setProperty('--site-top-anchor-offset', `${offset}px`);
11
16
  };
12
17
 
13
- mobileSectionLinks.forEach((link) => {
14
- link.addEventListener('click', (event) => {
18
+ const alignInitialHashTarget = () => {
19
+ if (!window.location.hash) return;
20
+
21
+ try {
22
+ const target = document.getElementById(decodeURIComponent(window.location.hash.slice(1)));
23
+ target?.scrollIntoView({ block: 'start' });
24
+ } catch {
25
+ // Invalid URL encoding is left to the browser's native hash handling.
26
+ }
27
+ };
28
+
29
+ const getSamePageTarget = (link: HTMLAnchorElement) => {
30
+ const linkUrl = new URL(link.href, window.location.href);
31
+ if (linkUrl.pathname !== window.location.pathname || !linkUrl.hash) return undefined;
32
+
33
+ try {
34
+ return document.getElementById(decodeURIComponent(linkUrl.hash.slice(1))) ?? undefined;
35
+ } catch {
36
+ return undefined;
37
+ }
38
+ };
39
+
40
+ const updateCurrentSection = () => {
41
+ sectionLinks.forEach((link) => {
15
42
  const linkUrl = new URL(link.href, window.location.href);
16
- if (linkUrl.pathname !== window.location.pathname) return;
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
+ );
49
+
50
+ if (isCurrent) {
51
+ link.setAttribute('aria-current', 'location');
52
+ } else if (link.getAttribute('aria-current') === 'location') {
53
+ link.removeAttribute('aria-current');
54
+ }
55
+ });
56
+ };
57
+
58
+ submenuItems.forEach((item) => {
59
+ const resetDismissal = () => delete item.dataset.submenuDismissed;
60
+ item.addEventListener('pointerleave', resetDismissal);
61
+ item.querySelector<HTMLAnchorElement>(':scope > a')?.addEventListener('focus', resetDismissal);
62
+ });
63
+
64
+ if (mobileMenu) {
65
+ mobileDestinationLinks.forEach((link) => {
66
+ link.addEventListener('click', () => {
67
+ mobileMenu.open = false;
68
+ });
69
+ });
70
+
71
+ mobileMenu.addEventListener('toggle', () => {
72
+ document.documentElement.classList.toggle('mobile-navigation-open', mobileMenu.open);
73
+ if (mobileMenu.open) {
74
+ window.requestAnimationFrame(() => (
75
+ mobileMenu.querySelector<HTMLElement>('.mobile-nav-view h2, .mobile-nav-view a')?.focus()
76
+ ));
77
+ }
78
+ });
79
+
80
+ mobileMenu.addEventListener('keydown', (event) => {
81
+ if (!mobileMenu.open) return;
82
+
83
+ if (event.key === 'Escape') {
84
+ event.preventDefault();
85
+ mobileMenu.open = false;
86
+ mobileMenuSummary?.focus();
87
+ return;
88
+ }
89
+
90
+ if (event.key !== 'Tab') return;
91
+
92
+ const focusableElements = Array.from(mobileMenu.querySelectorAll<HTMLElement>(
93
+ 'summary, a[href]:not([hidden]), [tabindex]:not([tabindex="-1"]):not([hidden])',
94
+ )).filter((element) => element.getClientRects().length > 0);
95
+ const firstFocusable = focusableElements[0];
96
+ const lastFocusable = focusableElements.at(-1);
97
+ if (!firstFocusable || !lastFocusable) return;
98
+
99
+ if (event.shiftKey && document.activeElement === firstFocusable) {
100
+ event.preventDefault();
101
+ lastFocusable.focus();
102
+ } else if (!event.shiftKey && document.activeElement === lastFocusable) {
103
+ event.preventDefault();
104
+ firstFocusable.focus();
105
+ }
106
+ });
107
+ }
108
+
109
+ sectionLinks.forEach((link) => {
110
+ link.addEventListener('click', (event) => {
111
+ const target = getSamePageTarget(link);
112
+ if (!target) return;
17
113
 
18
114
  if (mobileMenu) mobileMenu.open = false;
19
- if (event.detail > 0) link.blur();
115
+ link.closest<HTMLElement>('.site-nav-item')?.setAttribute('data-submenu-dismissed', 'true');
116
+
117
+ window.requestAnimationFrame(() => {
118
+ updateCurrentSection();
119
+ if (event.detail > 0) {
120
+ link.blur();
121
+ return;
122
+ }
123
+
124
+ target.setAttribute('tabindex', '-1');
125
+ target.focus({ preventScroll: true });
126
+ target.addEventListener('blur', () => target.removeAttribute('tabindex'), { once: true });
127
+ });
20
128
  });
21
129
  });
22
130
 
131
+ window.addEventListener('hashchange', updateCurrentSection);
132
+ updateCurrentSection();
133
+
23
134
  setAnchorOffset();
135
+ if (window.location.hash) window.requestAnimationFrame(alignInitialHashTarget);
24
136
 
25
137
  if (siteTop && 'ResizeObserver' in window) {
26
138
  new ResizeObserver(setAnchorOffset).observe(siteTop);