blume 2.0.2 → 2.1.0

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 (601) hide show
  1. package/AGENTS.md +1 -0
  2. package/CHANGELOG.md +113 -0
  3. package/README.md +1 -1
  4. package/dist/cli/{chunk-6k4ftwze.js → chunk-0gz36akr.js} +18 -2
  5. package/dist/cli/{chunk-6k4ftwze.js.map → chunk-0gz36akr.js.map} +3 -3
  6. package/dist/cli/{chunk-zp79m0ts.js → chunk-1n228xqb.js} +2 -2
  7. package/dist/cli/{chunk-ddndchfr.js → chunk-2f3ndkpw.js} +57 -29
  8. package/dist/cli/chunk-2f3ndkpw.js.map +14 -0
  9. package/dist/cli/{chunk-fsmrqk8a.js → chunk-2f7n4qzt.js} +8 -2
  10. package/dist/cli/{chunk-fsmrqk8a.js.map → chunk-2f7n4qzt.js.map} +3 -3
  11. package/dist/cli/{chunk-ppzjqwx2.js → chunk-2n4ps1b3.js} +17 -18
  12. package/dist/cli/{chunk-ppzjqwx2.js.map → chunk-2n4ps1b3.js.map} +3 -3
  13. package/dist/cli/{chunk-5r8g91qn.js → chunk-43zcf4gz.js} +398 -159
  14. package/dist/cli/chunk-43zcf4gz.js.map +36 -0
  15. package/dist/cli/chunk-4ejxbzee.js +39 -0
  16. package/dist/cli/chunk-4ejxbzee.js.map +10 -0
  17. package/dist/cli/{chunk-hr8ne106.js → chunk-4h8z9vv5.js} +296 -77
  18. package/dist/cli/chunk-4h8z9vv5.js.map +16 -0
  19. package/dist/cli/{chunk-k7pj68a8.js → chunk-5cg2rzgf.js} +18 -16
  20. package/dist/cli/{chunk-k7pj68a8.js.map → chunk-5cg2rzgf.js.map} +2 -2
  21. package/dist/cli/{chunk-h2ez8dzb.js → chunk-5ffywsmb.js} +4 -4
  22. package/dist/cli/{chunk-g698a744.js → chunk-5wj2qwn2.js} +5 -5
  23. package/dist/cli/chunk-6bm0xpgd.js +18 -0
  24. package/dist/cli/chunk-6bm0xpgd.js.map +10 -0
  25. package/dist/cli/{chunk-3yce002v.js → chunk-713mngdq.js} +15 -8
  26. package/dist/cli/chunk-713mngdq.js.map +14 -0
  27. package/dist/cli/{chunk-91ws1n6j.js → chunk-9geymqns.js} +17 -15
  28. package/dist/cli/{chunk-91ws1n6j.js.map → chunk-9geymqns.js.map} +2 -2
  29. package/dist/cli/{chunk-6dtt0zfn.js → chunk-9xr4dgee.js} +27 -18
  30. package/dist/cli/chunk-9xr4dgee.js.map +10 -0
  31. package/dist/cli/{chunk-sqw4ekg1.js → chunk-amrb6hgh.js} +2 -2
  32. package/dist/cli/chunk-cm89scdv.js +443 -0
  33. package/dist/cli/chunk-cm89scdv.js.map +11 -0
  34. package/dist/cli/{chunk-wdrt2k2v.js → chunk-cmtcnh9f.js} +90 -26
  35. package/dist/cli/chunk-cmtcnh9f.js.map +10 -0
  36. package/dist/cli/chunk-cy5bm0j0.js +104 -0
  37. package/dist/cli/chunk-cy5bm0j0.js.map +11 -0
  38. package/dist/cli/{chunk-7mbqtmgb.js → chunk-e7vvk5ya.js} +21 -12
  39. package/dist/cli/chunk-e7vvk5ya.js.map +10 -0
  40. package/dist/cli/{chunk-qkb5a8sa.js → chunk-g2z9x01v.js} +6 -98
  41. package/dist/cli/chunk-g2z9x01v.js.map +10 -0
  42. package/dist/cli/{chunk-273ygyr4.js → chunk-ga0pf4aj.js} +7 -7
  43. package/dist/cli/{chunk-273ygyr4.js.map → chunk-ga0pf4aj.js.map} +3 -3
  44. package/dist/cli/{chunk-h7k3nq3v.js → chunk-gp84za0h.js} +2 -2
  45. package/dist/cli/{chunk-hqp2ajnh.js → chunk-h2a48yhd.js} +195 -73
  46. package/dist/cli/chunk-h2a48yhd.js.map +36 -0
  47. package/dist/cli/{chunk-pbg5a4s3.js → chunk-jnwj5kjj.js} +71 -47
  48. package/dist/cli/chunk-jnwj5kjj.js.map +19 -0
  49. package/dist/cli/chunk-kwrg63tm.js +31 -0
  50. package/dist/cli/chunk-kwrg63tm.js.map +10 -0
  51. package/dist/cli/{chunk-8g8ytmgx.js → chunk-nd1821sk.js} +21 -19
  52. package/dist/cli/{chunk-8g8ytmgx.js.map → chunk-nd1821sk.js.map} +2 -2
  53. package/dist/cli/{chunk-w4bxdvsa.js → chunk-ntv4qrt7.js} +17 -15
  54. package/dist/cli/{chunk-w4bxdvsa.js.map → chunk-ntv4qrt7.js.map} +2 -2
  55. package/dist/cli/{chunk-bfwp9vp6.js → chunk-p804f3pe.js} +7 -7
  56. package/dist/cli/{chunk-4e9b9ra6.js → chunk-py62fewh.js} +6 -6
  57. package/dist/cli/{chunk-4e9b9ra6.js.map → chunk-py62fewh.js.map} +1 -1
  58. package/dist/cli/chunk-q3wxxq39.js +126 -0
  59. package/dist/cli/chunk-q3wxxq39.js.map +11 -0
  60. package/dist/cli/{chunk-j85scx15.js → chunk-rekqz4qg.js} +142 -46
  61. package/dist/cli/chunk-rekqz4qg.js.map +10 -0
  62. package/dist/cli/{chunk-00gs3wqs.js → chunk-ta27feev.js} +1 -1
  63. package/dist/cli/{chunk-v6ya5kcb.js → chunk-xkz5xbtv.js} +4031 -1132
  64. package/dist/cli/chunk-xkz5xbtv.js.map +213 -0
  65. package/dist/cli/{chunk-bbnwccaz.js → chunk-y4ykbark.js} +2 -2
  66. package/dist/cli/{chunk-n1yg3tj3.js → chunk-y5h4r2y0.js} +6 -6
  67. package/dist/cli/{chunk-n1yg3tj3.js.map → chunk-y5h4r2y0.js.map} +3 -3
  68. package/dist/cli/{chunk-nfcyttvj.js → chunk-yj17vs39.js} +9 -9
  69. package/dist/cli/{chunk-nfcyttvj.js.map → chunk-yj17vs39.js.map} +1 -1
  70. package/dist/cli/{chunk-7vtckvaw.js → chunk-yxmk4pec.js} +16 -14
  71. package/dist/cli/{chunk-7vtckvaw.js.map → chunk-yxmk4pec.js.map} +2 -2
  72. package/dist/cli/{chunk-gs7r695n.js → chunk-zccbjcj0.js} +22 -8
  73. package/dist/cli/{chunk-gs7r695n.js.map → chunk-zccbjcj0.js.map} +3 -3
  74. package/dist/cli/{chunk-xh43dwgw.js → chunk-zf748avv.js} +415 -225
  75. package/dist/cli/chunk-zf748avv.js.map +37 -0
  76. package/dist/cli/{chunk-d1v5rhy0.js → chunk-zhvd6adm.js} +16 -14
  77. package/dist/cli/{chunk-d1v5rhy0.js.map → chunk-zhvd6adm.js.map} +2 -2
  78. package/dist/cli/index.js +28 -20
  79. package/dist/cli/index.js.map +5 -5
  80. package/dist/types/ai/agent-surface.d.ts +8 -7
  81. package/dist/types/ai/ai-catalog.d.ts +7 -0
  82. package/dist/types/ai/ask-context.d.ts +22 -0
  83. package/dist/types/ai/ask.d.ts +271 -58
  84. package/dist/types/ai/index.d.ts +2 -2
  85. package/dist/types/ai/link-headers.d.ts +8 -1
  86. package/dist/types/ai/openapi-components.d.ts +5 -2
  87. package/dist/types/ai/relative-links.d.ts +9 -7
  88. package/dist/types/ai/site-skill.d.ts +26 -0
  89. package/dist/types/ai/skills.d.ts +20 -1
  90. package/dist/types/ai/tar.d.ts +1 -3
  91. package/dist/types/analytics/index.d.ts +2 -0
  92. package/dist/types/analytics/one-dollar-stats.d.ts +48 -0
  93. package/dist/types/analytics/schema.d.ts +14 -0
  94. package/dist/types/astro/integration.d.ts +11 -2
  95. package/dist/types/captcha/hcaptcha.d.ts +27 -0
  96. package/dist/types/captcha/index.d.ts +28 -0
  97. package/dist/types/captcha/schema.d.ts +29 -0
  98. package/dist/types/captcha/turnstile.d.ts +28 -0
  99. package/dist/types/components/content/api-field.d.ts +59 -0
  100. package/dist/types/components/content/api-page.d.ts +95 -0
  101. package/dist/types/components/layout/nav-utils.d.ts +26 -0
  102. package/dist/types/components/openapi/helpers.d.ts +137 -0
  103. package/dist/types/components/openapi/security.d.ts +85 -0
  104. package/dist/types/consent/ethyca.d.ts +48 -0
  105. package/dist/types/consent/index.d.ts +37 -0
  106. package/dist/types/consent/native.d.ts +28 -0
  107. package/dist/types/consent/osano.d.ts +42 -0
  108. package/dist/types/consent/schema.d.ts +110 -0
  109. package/dist/types/core/base-path.d.ts +21 -7
  110. package/dist/types/core/config-input.d.ts +196 -27
  111. package/dist/types/core/data.d.ts +49 -4
  112. package/dist/types/core/directive-diagnostics.d.ts +12 -0
  113. package/dist/types/core/footer.d.ts +8 -0
  114. package/dist/types/core/heading-markers.d.ts +5 -7
  115. package/dist/types/core/i18n-ui.d.ts +86 -0
  116. package/dist/types/core/i18n.d.ts +2 -0
  117. package/dist/types/core/includes.d.ts +8 -2
  118. package/dist/types/core/last-modified.d.ts +10 -0
  119. package/dist/types/core/localizable.d.ts +7 -0
  120. package/dist/types/core/meta.d.ts +8 -0
  121. package/dist/types/core/page-modes.d.ts +29 -0
  122. package/dist/types/core/redirect-patterns.d.ts +144 -0
  123. package/dist/types/core/related.d.ts +40 -0
  124. package/dist/types/core/schema.d.ts +558 -56
  125. package/dist/types/core/sources/lower.d.ts +29 -16
  126. package/dist/types/core/sources/normalize.d.ts +13 -1
  127. package/dist/types/core/sources/read.d.ts +5 -0
  128. package/dist/types/core/sources/watch.d.ts +12 -5
  129. package/dist/types/core/standard-schema.d.ts +5 -0
  130. package/dist/types/core/types.d.ts +1 -1
  131. package/dist/types/core/variables.d.ts +42 -0
  132. package/dist/types/deploy/artifacts.d.ts +6 -4
  133. package/dist/types/deploy/headers.d.ts +5 -0
  134. package/dist/types/deploy/node-headers.d.ts +13 -2
  135. package/dist/types/deploy/platforms/cloudflare.d.ts +5 -0
  136. package/dist/types/deploy/platforms/netlify.d.ts +7 -0
  137. package/dist/types/deploy/platforms/types.d.ts +8 -0
  138. package/dist/types/deploy/platforms/vercel.d.ts +6 -0
  139. package/dist/types/deploy/redirects.d.ts +61 -22
  140. package/dist/types/deploy/vercel-negotiation.d.ts +9 -0
  141. package/dist/types/markdown/code-title.d.ts +17 -0
  142. package/dist/types/markdown/directives.d.ts +62 -0
  143. package/dist/types/markdown/features.d.ts +24 -2
  144. package/dist/types/markdown/mdast.d.ts +63 -0
  145. package/dist/types/narration/build.d.ts +70 -0
  146. package/dist/types/narration/provider.d.ts +26 -0
  147. package/dist/types/narration/script.d.ts +154 -0
  148. package/dist/types/openapi/asyncapi.d.ts +4 -2
  149. package/dist/types/openapi/model.d.ts +12 -6
  150. package/dist/types/openapi/overlay.d.ts +54 -0
  151. package/dist/types/openapi/parse.d.ts +13 -0
  152. package/dist/types/openapi/references.d.ts +14 -4
  153. package/dist/types/ratelimit/cloudflare.d.ts +39 -0
  154. package/dist/types/ratelimit/index.d.ts +27 -0
  155. package/dist/types/ratelimit/memory.d.ts +36 -0
  156. package/dist/types/ratelimit/schema.d.ts +111 -0
  157. package/dist/types/ratelimit/upstash.d.ts +21 -0
  158. package/dist/types/ratelimit/wrangler.d.ts +23 -0
  159. package/dist/types/reference/asyncapi.d.ts +10 -10
  160. package/dist/types/reference/graphql.d.ts +13 -10
  161. package/dist/types/reference/openapi.d.ts +33 -12
  162. package/dist/types/reference/options.d.ts +45 -5
  163. package/dist/types/reference/scalar.d.ts +13 -0
  164. package/dist/types/reference/schema.d.ts +60 -36
  165. package/dist/types/search/documents.d.ts +12 -1
  166. package/dist/types/search/orama-index.d.ts +4 -0
  167. package/dist/types/seo/metatags.d.ts +20 -0
  168. package/docs/02-deployment.mdx +44 -6
  169. package/docs/03-upgrading.mdx +4 -4
  170. package/docs/04-migrating.mdx +1 -1
  171. package/docs/08-faq.mdx +1 -1
  172. package/docs/advanced/changelog.mdx +2 -2
  173. package/docs/advanced/custom-pages.mdx +3 -3
  174. package/docs/advanced/skills.mdx +14 -0
  175. package/docs/cli/audit.mdx +38 -2
  176. package/docs/cli/evals.mdx +3 -3
  177. package/docs/cli/index.mdx +1 -0
  178. package/docs/cli/translate.mdx +3 -3
  179. package/docs/cli/version.mdx +1 -1
  180. package/docs/configuration/analytics.mdx +22 -1
  181. package/docs/configuration/assistant.mdx +188 -36
  182. package/docs/configuration/consent.mdx +100 -0
  183. package/docs/configuration/customization.mdx +6 -4
  184. package/docs/configuration/index.mdx +63 -5
  185. package/docs/configuration/meta.ts +3 -0
  186. package/docs/configuration/narration.mdx +119 -0
  187. package/docs/configuration/rate-limiting.mdx +69 -0
  188. package/docs/configuration/search.mdx +30 -2
  189. package/docs/configuration/theming.mdx +3 -1
  190. package/docs/content/components.mdx +107 -1
  191. package/docs/content/frontmatter.mdx +77 -1
  192. package/docs/content/i18n.mdx +40 -3
  193. package/docs/content/includes.mdx +16 -0
  194. package/docs/content/index.mdx +4 -2
  195. package/docs/content/islands.mdx +1 -1
  196. package/docs/content/meta.mdx +4 -1
  197. package/docs/content/meta.ts +1 -0
  198. package/docs/content/navigation.mdx +28 -6
  199. package/docs/content/sources/contentful.mdx +32 -0
  200. package/docs/content/sources/custom.mdx +39 -0
  201. package/docs/content/sources/github-releases.mdx +31 -0
  202. package/docs/content/sources/index.mdx +91 -0
  203. package/docs/content/sources/meta.ts +16 -0
  204. package/docs/content/sources/notion.mdx +32 -0
  205. package/docs/content/sources/obsidian.mdx +37 -0
  206. package/docs/content/sources/payload.mdx +30 -0
  207. package/docs/content/sources/remote-mdx.mdx +40 -0
  208. package/docs/content/sources/sanity.mdx +29 -0
  209. package/docs/content/sources/strapi.mdx +31 -0
  210. package/docs/content/syntax.mdx +81 -2
  211. package/docs/content/variables.mdx +41 -0
  212. package/docs/content/versioning.mdx +1 -1
  213. package/docs/discoverability/agent-discovery.mdx +30 -3
  214. package/docs/discoverability/index.mdx +3 -2
  215. package/docs/discoverability/llms-txt.mdx +1 -1
  216. package/docs/discoverability/markdown.mdx +2 -2
  217. package/docs/discoverability/metadata.mdx +17 -0
  218. package/docs/discoverability/open-graph.mdx +5 -3
  219. package/docs/discoverability/rss.mdx +1 -1
  220. package/docs/references/api-pages.mdx +74 -0
  221. package/docs/references/asyncapi.mdx +2 -2
  222. package/docs/references/graphql.mdx +3 -3
  223. package/docs/references/meta.ts +1 -1
  224. package/docs/references/openapi.mdx +106 -5
  225. package/docs/references/scalar.mdx +3 -1
  226. package/package.json +36 -1
  227. package/skills/blume/SKILL.md +3 -0
  228. package/skills/blume-migrate/SKILL.md +5 -5
  229. package/skills/blume-migrate/references/docusaurus.md +3 -3
  230. package/skills/blume-migrate/references/fumadocs.md +1 -1
  231. package/skills/blume-migrate/references/mintlify.md +37 -19
  232. package/skills/blume-migrate/references/nextra.md +3 -3
  233. package/skills/blume-migrate/references/starlight.md +2 -1
  234. package/skills/blume-migrate/scripts/mintlify-codemod.mjs +67 -16
  235. package/skills/blume-write-skill/SKILL.md +82 -0
  236. package/src/ai/agent-readability.ts +4 -3
  237. package/src/ai/agent-surface.ts +9 -7
  238. package/src/ai/ai-catalog.ts +12 -5
  239. package/src/ai/api/handlers.ts +2 -2
  240. package/src/ai/api/spec.ts +2 -2
  241. package/src/ai/api-catalog.ts +6 -2
  242. package/src/ai/ask-context.ts +73 -20
  243. package/src/ai/ask-data.ts +6 -0
  244. package/src/ai/ask-tools.ts +142 -0
  245. package/src/ai/ask.ts +354 -76
  246. package/src/ai/changelog-markdown.ts +2 -2
  247. package/src/ai/component-markdown.ts +75 -0
  248. package/src/ai/cors.ts +2 -0
  249. package/src/ai/index.ts +12 -0
  250. package/src/ai/link-headers.ts +14 -5
  251. package/src/ai/llms.ts +24 -9
  252. package/src/ai/markdown.ts +2 -1
  253. package/src/ai/mcp/data.ts +6 -0
  254. package/src/ai/mcp/discovery.ts +2 -2
  255. package/src/ai/mcp/query.ts +2 -2
  256. package/src/ai/mcp/server.ts +2 -2
  257. package/src/ai/openapi-components.ts +31 -9
  258. package/src/ai/relative-links.ts +42 -26
  259. package/src/ai/serializers.ts +2 -1
  260. package/src/ai/site-skill.ts +274 -0
  261. package/src/ai/skills.ts +49 -3
  262. package/src/ai/tar.ts +139 -12
  263. package/src/ai/write-skill.ts +97 -0
  264. package/src/analytics/head.ts +4 -0
  265. package/src/analytics/index.ts +5 -0
  266. package/src/analytics/one-dollar-stats.ts +86 -0
  267. package/src/analytics/schema.ts +2 -0
  268. package/src/astro/generate.ts +133 -19
  269. package/src/astro/include-hmr.ts +31 -12
  270. package/src/astro/include-refresh.ts +19 -8
  271. package/src/astro/index.ts +1 -0
  272. package/src/astro/integration.ts +143 -14
  273. package/src/astro/module-types.ts +6 -0
  274. package/src/astro/runtime-modules.ts +28 -13
  275. package/src/astro/templates.ts +364 -67
  276. package/src/astro/variables.ts +31 -0
  277. package/src/audit/catalog.ts +19 -0
  278. package/src/audit/checks/contrast.ts +183 -0
  279. package/src/audit/checks/i18n.ts +2 -3
  280. package/src/audit/checks/indexability.ts +2 -3
  281. package/src/audit/checks/links.ts +13 -13
  282. package/src/audit/checks/sitemap.ts +2 -3
  283. package/src/audit/graph.ts +3 -1
  284. package/src/audit/redirects.ts +46 -15
  285. package/src/audit/run.ts +14 -9
  286. package/src/audit/snapshot.ts +5 -0
  287. package/src/audit/types.ts +2 -1
  288. package/src/blume-modules.d.ts +7 -0
  289. package/src/captcha/client.ts +152 -0
  290. package/src/captcha/hcaptcha.ts +36 -0
  291. package/src/captcha/index.ts +28 -0
  292. package/src/captcha/schema.ts +32 -0
  293. package/src/captcha/turnstile.ts +38 -0
  294. package/src/captcha/verify.ts +71 -0
  295. package/src/cli/args.ts +32 -0
  296. package/src/cli/command-meta.ts +5 -0
  297. package/src/cli/commands/audit.ts +4 -1
  298. package/src/cli/commands/doctor.ts +25 -17
  299. package/src/cli/commands/eval.ts +19 -11
  300. package/src/cli/commands/skill.ts +96 -0
  301. package/src/cli/commands/translate.ts +9 -12
  302. package/src/cli/commands/validate.ts +3 -1
  303. package/src/cli/dev-lock.ts +157 -37
  304. package/src/cli/index.ts +5 -0
  305. package/src/cli/report-format.ts +11 -6
  306. package/src/cli/required-secrets.ts +71 -29
  307. package/src/components/BlumePage.astro +2 -0
  308. package/src/components/colors.ts +19 -0
  309. package/src/components/content/ApiEndpoint.astro +18 -0
  310. package/src/components/content/ApiField.astro +79 -0
  311. package/src/components/content/ApiPlayground.astro +62 -0
  312. package/src/components/content/ApiRail.astro +19 -0
  313. package/src/components/content/Badge.astro +5 -3
  314. package/src/components/content/Color.astro +1 -0
  315. package/src/components/content/Component.astro +2 -2
  316. package/src/components/content/FileTree.astro +1 -0
  317. package/src/components/content/GithubInfo.astro +1 -0
  318. package/src/components/content/Panel.astro +1 -0
  319. package/src/components/content/ParamField.astro +25 -0
  320. package/src/components/content/RequestExample.astro +17 -0
  321. package/src/components/content/ResponseExample.astro +17 -0
  322. package/src/components/content/ResponseField.astro +32 -0
  323. package/src/components/content/Step.astro +1 -0
  324. package/src/components/content/Tab.astro +0 -1
  325. package/src/components/content/Tabs.astro +4 -0
  326. package/src/components/content/Tile.astro +1 -0
  327. package/src/components/content/Tooltip.astro +1 -0
  328. package/src/components/content/Tree.astro +1 -0
  329. package/src/components/content/TypeTable.astro +4 -1
  330. package/src/components/content/View.astro +21 -0
  331. package/src/components/content/ViewSwitcher.astro +168 -0
  332. package/src/components/content/api-field.ts +100 -0
  333. package/src/components/content/api-page.ts +224 -0
  334. package/src/components/content/badge-color.ts +5 -2
  335. package/src/components/content/base-href.ts +13 -36
  336. package/src/components/content/view-switcher.ts +71 -0
  337. package/src/components/index.ts +2 -0
  338. package/src/components/islands/Assistant.astro +10 -1
  339. package/src/components/islands/assistant.tsx +125 -18
  340. package/src/components/islands/base-path.ts +47 -10
  341. package/src/components/islands/code-question.ts +62 -0
  342. package/src/components/islands/hooks.ts +145 -40
  343. package/src/components/islands/support-link.ts +68 -0
  344. package/src/components/islands/webmcp.ts +4 -2
  345. package/src/components/layout/Analytics.astro +11 -2
  346. package/src/components/layout/Banner.astro +14 -10
  347. package/src/components/layout/Breadcrumbs.astro +11 -4
  348. package/src/components/layout/ConsentBanner.astro +62 -0
  349. package/src/components/layout/ConsentHead.astro +38 -0
  350. package/src/components/layout/Directory.astro +94 -0
  351. package/src/components/layout/DiscoveryLinks.astro +9 -5
  352. package/src/components/layout/DrawerSwitchers.astro +51 -0
  353. package/src/components/layout/Empty.astro +2 -2
  354. package/src/components/layout/Header.astro +124 -82
  355. package/src/components/layout/LanguageSwitcher.astro +50 -17
  356. package/src/components/layout/NarrationPlayer.astro +1085 -0
  357. package/src/components/layout/NavSelector.astro +35 -9
  358. package/src/components/layout/NavTabMenu.astro +3 -3
  359. package/src/components/layout/NavTree.astro +16 -6
  360. package/src/components/layout/PageActions.astro +4 -3
  361. package/src/components/layout/PageFeedback.astro +54 -49
  362. package/src/components/layout/PageLayout.astro +78 -9
  363. package/src/components/layout/Pagination.astro +3 -3
  364. package/src/components/layout/ReferenceLayout.astro +17 -6
  365. package/src/components/layout/RelatedPages.astro +42 -0
  366. package/src/components/layout/RootLayout.astro +313 -50
  367. package/src/components/layout/Search.astro +64 -30
  368. package/src/components/layout/SiteFooter.astro +124 -0
  369. package/src/components/layout/SiteMetatags.astro +16 -0
  370. package/src/components/layout/VersionBanner.astro +2 -2
  371. package/src/components/layout/analytics-client.ts +12 -0
  372. package/src/components/layout/consent/native.ts +79 -0
  373. package/src/components/layout/consent/types.ts +37 -0
  374. package/src/components/layout/footer-links.ts +53 -0
  375. package/src/components/layout/footer-socials.ts +87 -0
  376. package/src/components/layout/locale-redirect.ts +107 -0
  377. package/src/components/layout/nav-utils.ts +83 -0
  378. package/src/components/layout/page-feedback.ts +96 -0
  379. package/src/components/layout/search/analytics.ts +98 -0
  380. package/src/components/layout/search/flexsearch.ts +17 -5
  381. package/src/components/layout/search/orama-cloud.ts +26 -14
  382. package/src/components/layout/search/types.ts +20 -0
  383. package/src/components/layout/search/typesense.ts +5 -1
  384. package/src/components/layout/toc-active.ts +41 -0
  385. package/src/components/layout/toc-element.ts +8 -14
  386. package/src/components/openapi/ApiTagOperations.astro +2 -2
  387. package/src/components/openapi/AsyncApiOperation.astro +7 -4
  388. package/src/components/openapi/GraphqlChip.astro +2 -2
  389. package/src/components/openapi/Operation.astro +163 -13
  390. package/src/components/openapi/Playground.astro +4 -4
  391. package/src/components/openapi/RequestBody.astro +6 -4
  392. package/src/components/openapi/RequestPanel.astro +63 -8
  393. package/src/components/openapi/Responses.astro +6 -4
  394. package/src/components/openapi/SchemaTable.astro +7 -0
  395. package/src/components/openapi/async-snippets.ts +4 -4
  396. package/src/components/openapi/async.ts +38 -6
  397. package/src/components/openapi/code-samples.ts +97 -0
  398. package/src/components/openapi/helpers.ts +19 -9
  399. package/src/components/openapi/message-composer.ts +6 -1
  400. package/src/components/openapi/message.ts +17 -2
  401. package/src/components/openapi/operation-model.ts +81 -11
  402. package/src/components/openapi/panel.ts +4 -2
  403. package/src/components/openapi/param-style.ts +181 -0
  404. package/src/components/openapi/playground-client.ts +73 -16
  405. package/src/components/openapi/request.ts +190 -26
  406. package/src/components/openapi/schema-tree.ts +30 -24
  407. package/src/components/openapi/security.ts +6 -3
  408. package/src/components/openapi/snippets.ts +953 -13
  409. package/src/components/openapi/ws-client.ts +18 -2
  410. package/src/components/props.ts +6 -0
  411. package/src/consent/client.ts +95 -0
  412. package/src/consent/clients.ts +19 -0
  413. package/src/consent/ethyca.ts +71 -0
  414. package/src/consent/head.ts +24 -0
  415. package/src/consent/index.ts +37 -0
  416. package/src/consent/init.ts +29 -0
  417. package/src/consent/native.ts +36 -0
  418. package/src/consent/osano.ts +59 -0
  419. package/src/consent/schema.ts +30 -0
  420. package/src/core/base-path.ts +62 -16
  421. package/src/core/builtin-tags.ts +5 -0
  422. package/src/core/client-address.ts +19 -0
  423. package/src/core/config-input.ts +203 -27
  424. package/src/core/data.ts +44 -4
  425. package/src/core/diagnostics.ts +5 -2
  426. package/src/core/directive-diagnostics.ts +99 -0
  427. package/src/core/footer.ts +26 -0
  428. package/src/core/frontmatter.ts +21 -18
  429. package/src/core/graph.ts +39 -36
  430. package/src/core/heading-markers.ts +5 -18
  431. package/src/core/i18n-ui.ts +82 -2
  432. package/src/core/i18n.ts +31 -3
  433. package/src/core/includes.ts +20 -3
  434. package/src/core/last-modified.ts +25 -3
  435. package/src/core/links.ts +16 -2
  436. package/src/core/locale-links.ts +5 -1
  437. package/src/core/localizable.ts +26 -0
  438. package/src/core/meta.ts +33 -19
  439. package/src/core/navigation.ts +71 -20
  440. package/src/core/page-modes.ts +56 -0
  441. package/src/core/project-graph.ts +138 -10
  442. package/src/core/redirect-patterns.ts +491 -0
  443. package/src/core/related.ts +131 -0
  444. package/src/core/schema.ts +342 -68
  445. package/src/core/sources/contentful-rich-text.ts +27 -20
  446. package/src/core/sources/filesystem.ts +20 -2
  447. package/src/core/sources/github-releases.ts +25 -6
  448. package/src/core/sources/lexical.ts +23 -18
  449. package/src/core/sources/lower.ts +201 -34
  450. package/src/core/sources/mdx-remote.ts +51 -16
  451. package/src/core/sources/normalize.ts +327 -91
  452. package/src/core/sources/notion.ts +52 -25
  453. package/src/core/sources/obsidian.ts +23 -6
  454. package/src/core/sources/portable-text.ts +38 -21
  455. package/src/core/sources/read.ts +8 -3
  456. package/src/core/sources/strapi-blocks.ts +20 -16
  457. package/src/core/sources/watch.ts +20 -7
  458. package/src/core/standard-schema.ts +10 -6
  459. package/src/core/types.ts +1 -1
  460. package/src/core/ui-packs/ar.ts +43 -0
  461. package/src/core/ui-packs/bg.ts +45 -0
  462. package/src/core/ui-packs/bn.ts +43 -0
  463. package/src/core/ui-packs/ca.ts +46 -0
  464. package/src/core/ui-packs/cs.ts +44 -0
  465. package/src/core/ui-packs/da.ts +45 -0
  466. package/src/core/ui-packs/de.ts +46 -0
  467. package/src/core/ui-packs/el.ts +45 -0
  468. package/src/core/ui-packs/es.ts +46 -0
  469. package/src/core/ui-packs/fa.ts +45 -0
  470. package/src/core/ui-packs/fi.ts +45 -0
  471. package/src/core/ui-packs/fr.ts +46 -0
  472. package/src/core/ui-packs/he.ts +43 -0
  473. package/src/core/ui-packs/hi.ts +44 -0
  474. package/src/core/ui-packs/hr.ts +45 -0
  475. package/src/core/ui-packs/hu.ts +44 -0
  476. package/src/core/ui-packs/id.ts +45 -0
  477. package/src/core/ui-packs/it.ts +45 -0
  478. package/src/core/ui-packs/ja.ts +45 -0
  479. package/src/core/ui-packs/ko.ts +44 -0
  480. package/src/core/ui-packs/nl.ts +46 -0
  481. package/src/core/ui-packs/no.ts +44 -0
  482. package/src/core/ui-packs/pl.ts +45 -0
  483. package/src/core/ui-packs/pt-br.ts +46 -0
  484. package/src/core/ui-packs/pt.ts +45 -0
  485. package/src/core/ui-packs/ro.ts +45 -0
  486. package/src/core/ui-packs/ru.ts +45 -0
  487. package/src/core/ui-packs/sk.ts +44 -0
  488. package/src/core/ui-packs/sr.ts +45 -0
  489. package/src/core/ui-packs/sv.ts +44 -0
  490. package/src/core/ui-packs/th.ts +43 -0
  491. package/src/core/ui-packs/tr.ts +45 -0
  492. package/src/core/ui-packs/uk.ts +45 -0
  493. package/src/core/ui-packs/vi.ts +45 -0
  494. package/src/core/ui-packs/zh-tw.ts +43 -0
  495. package/src/core/ui-packs/zh.ts +43 -0
  496. package/src/core/variables.ts +103 -0
  497. package/src/core/version-cut.ts +52 -15
  498. package/src/deploy/artifacts.ts +104 -14
  499. package/src/deploy/cloudflare-negotiation.ts +39 -17
  500. package/src/deploy/headers.ts +10 -5
  501. package/src/deploy/node-headers.ts +64 -10
  502. package/src/deploy/platforms/cloudflare.ts +44 -3
  503. package/src/deploy/platforms/netlify.ts +34 -1
  504. package/src/deploy/platforms/node.ts +1 -0
  505. package/src/deploy/platforms/static.ts +1 -0
  506. package/src/deploy/platforms/types.ts +8 -0
  507. package/src/deploy/platforms/vercel.ts +38 -2
  508. package/src/deploy/redirects.ts +139 -36
  509. package/src/deploy/robots.ts +2 -2
  510. package/src/deploy/rss.ts +4 -3
  511. package/src/deploy/sitemap.ts +7 -5
  512. package/src/deploy/vercel-negotiation.ts +36 -0
  513. package/src/markdown/api-rail.ts +259 -0
  514. package/src/markdown/base-links.ts +34 -36
  515. package/src/markdown/code-title.ts +35 -9
  516. package/src/markdown/directives.ts +242 -36
  517. package/src/markdown/features.ts +20 -2
  518. package/src/markdown/fence-meta.ts +2 -0
  519. package/src/markdown/include.ts +11 -1
  520. package/src/markdown/index.ts +35 -11
  521. package/src/markdown/math.ts +4 -4
  522. package/src/markdown/mdast.ts +5 -2
  523. package/src/markdown/relative-links.ts +12 -4
  524. package/src/markdown/variables.ts +56 -0
  525. package/src/markdown/views.ts +82 -0
  526. package/src/narration/build.ts +412 -0
  527. package/src/narration/player.ts +249 -0
  528. package/src/narration/provider.ts +29 -0
  529. package/src/narration/script.ts +592 -0
  530. package/src/og/card.ts +136 -11
  531. package/src/og/derive.ts +41 -31
  532. package/src/og/index.ts +1 -0
  533. package/src/openapi/asyncapi.ts +4 -2
  534. package/src/openapi/model.ts +77 -36
  535. package/src/openapi/overlay.ts +256 -0
  536. package/src/openapi/parse.ts +55 -2
  537. package/src/openapi/proxy.ts +63 -10
  538. package/src/openapi/references.ts +21 -5
  539. package/src/openapi/render-mdx.ts +46 -3
  540. package/src/openapi/scalar.ts +40 -6
  541. package/src/openapi/source.ts +1 -1
  542. package/src/ratelimit/cloudflare.ts +54 -0
  543. package/src/ratelimit/index.ts +30 -0
  544. package/src/ratelimit/memory.ts +45 -0
  545. package/src/ratelimit/runtime.ts +229 -0
  546. package/src/ratelimit/schema.ts +28 -0
  547. package/src/ratelimit/upstash.ts +30 -0
  548. package/src/ratelimit/wrangler.ts +70 -0
  549. package/src/reference/asyncapi.ts +2 -2
  550. package/src/reference/graphql.ts +5 -2
  551. package/src/reference/openapi.ts +21 -7
  552. package/src/reference/options.ts +51 -7
  553. package/src/reference/scalar.ts +33 -8
  554. package/src/registry/eject.ts +201 -28
  555. package/src/registry/registry.ts +7 -0
  556. package/src/search/adapters/version-scope.ts +30 -0
  557. package/src/search/documents.ts +93 -28
  558. package/src/search/orama-index.ts +28 -3
  559. package/src/search/pagefind-ranking.ts +39 -0
  560. package/src/search/popular.ts +2 -1
  561. package/src/search/sync/algolia.ts +15 -4
  562. package/src/search/sync/orama-cloud.ts +3 -0
  563. package/src/search/sync/typesense.ts +5 -0
  564. package/src/seo/jsonld.ts +7 -3
  565. package/src/seo/metatags.ts +67 -0
  566. package/src/theme/chrome-icons.ts +1 -0
  567. package/src/theme/color.ts +500 -0
  568. package/src/theme/entry.ts +56 -3
  569. package/src/theme/palette.ts +121 -45
  570. package/src/translate/meta.ts +68 -24
  571. package/src/translate/prompts.ts +1 -0
  572. package/src/translate/run.ts +3 -3
  573. package/src/translate/validate.ts +12 -3
  574. package/src/translate/work-list.ts +56 -17
  575. package/src/upgrade/upgrade.ts +4 -171
  576. package/dist/cli/chunk-3yce002v.js.map +0 -14
  577. package/dist/cli/chunk-5r8g91qn.js.map +0 -34
  578. package/dist/cli/chunk-6dtt0zfn.js.map +0 -10
  579. package/dist/cli/chunk-7mbqtmgb.js.map +0 -10
  580. package/dist/cli/chunk-ddndchfr.js.map +0 -14
  581. package/dist/cli/chunk-esh98wmb.js +0 -23
  582. package/dist/cli/chunk-esh98wmb.js.map +0 -10
  583. package/dist/cli/chunk-hqp2ajnh.js.map +0 -35
  584. package/dist/cli/chunk-hr8ne106.js.map +0 -13
  585. package/dist/cli/chunk-j85scx15.js.map +0 -10
  586. package/dist/cli/chunk-mqc662a6.js +0 -75
  587. package/dist/cli/chunk-mqc662a6.js.map +0 -11
  588. package/dist/cli/chunk-pbg5a4s3.js.map +0 -19
  589. package/dist/cli/chunk-qkb5a8sa.js.map +0 -10
  590. package/dist/cli/chunk-v6ya5kcb.js.map +0 -189
  591. package/dist/cli/chunk-wdrt2k2v.js.map +0 -10
  592. package/dist/cli/chunk-xh43dwgw.js.map +0 -36
  593. package/docs/content/sources.mdx +0 -338
  594. /package/dist/cli/{chunk-zp79m0ts.js.map → chunk-1n228xqb.js.map} +0 -0
  595. /package/dist/cli/{chunk-h2ez8dzb.js.map → chunk-5ffywsmb.js.map} +0 -0
  596. /package/dist/cli/{chunk-g698a744.js.map → chunk-5wj2qwn2.js.map} +0 -0
  597. /package/dist/cli/{chunk-sqw4ekg1.js.map → chunk-amrb6hgh.js.map} +0 -0
  598. /package/dist/cli/{chunk-h7k3nq3v.js.map → chunk-gp84za0h.js.map} +0 -0
  599. /package/dist/cli/{chunk-bfwp9vp6.js.map → chunk-p804f3pe.js.map} +0 -0
  600. /package/dist/cli/{chunk-00gs3wqs.js.map → chunk-ta27feev.js.map} +0 -0
  601. /package/dist/cli/{chunk-bbnwccaz.js.map → chunk-y4ykbark.js.map} +0 -0
@@ -1,21 +1,90 @@
1
1
  import type { ResolvedConfig } from "../core/schema.ts";
2
+ import { composite, contrastRatio, parseColor } from "./color.ts";
3
+ import type { Rgba } from "./color.ts";
2
4
 
3
- const FALLBACK_ACCENT = "oklch(0.62 0.16 250)";
5
+ /** Per-mode CSS colors. */
6
+ export interface AccentColors {
7
+ dark: string;
8
+ light: string;
9
+ }
10
+
11
+ /** A color mode. */
12
+ export type ColorMode = keyof AccentColors;
4
13
 
5
14
  /**
6
- * Named accent presets mapped to OKLCH values. The single source of truth for
7
- * preset colors: the theme CSS and the OG card (og/card.ts) both resolve from
8
- * this table, so a site and its social cards can't disagree about "blue".
15
+ * Named accent presets mapped to OKLCH values, per mode. The single source of
16
+ * truth for preset colors: the theme CSS and the OG card (og/card.ts) both
17
+ * resolve from this table, so a site and its social cards can't disagree
18
+ * about "blue". Each clears WCAG AA (4.5:1) as text on its mode's page and
19
+ * under its button label ({@link readableOn}), with room to spare on the
20
+ * tinted surfaces accent text also sits on: the light shades are dark enough
21
+ * for white labels, the dark shades light enough to read on the dark page.
9
22
  */
10
23
  export const ACCENTS = {
11
- blue: FALLBACK_ACCENT,
12
- green: "oklch(0.6 0.16 150)",
13
- orange: "oklch(0.68 0.17 50)",
14
- pink: "oklch(0.65 0.2 350)",
15
- purple: "oklch(0.58 0.2 290)",
16
- red: "oklch(0.58 0.22 25)",
17
- teal: "oklch(0.6 0.12 195)",
18
- } satisfies Record<string, string>;
24
+ blue: { dark: "oklch(0.62 0.16 250)", light: "oklch(0.55 0.16 250)" },
25
+ green: { dark: "oklch(0.6 0.16 150)", light: "oklch(0.53 0.16 150)" },
26
+ orange: { dark: "oklch(0.68 0.17 50)", light: "oklch(0.56 0.17 50)" },
27
+ pink: { dark: "oklch(0.65 0.2 350)", light: "oklch(0.58 0.2 350)" },
28
+ purple: { dark: "oklch(0.6 0.2 290)", light: "oklch(0.57 0.2 290)" },
29
+ red: { dark: "oklch(0.61 0.22 25)", light: "oklch(0.58 0.22 25)" },
30
+ teal: { dark: "oklch(0.6 0.12 195)", light: "oklch(0.53 0.12 195)" },
31
+ } satisfies Record<string, AccentColors>;
32
+
33
+ /** The contrast WCAG AA asks of body-size text. */
34
+ export const MIN_TEXT_CONTRAST = 4.5;
35
+
36
+ /**
37
+ * The theme's own colors a configured accent is drawn against: each mode's
38
+ * page background and text, and the dark ink a filled accent can carry.
39
+ * Mirrors the defaults in `entry.ts`.
40
+ */
41
+ export const THEME_INKS = {
42
+ dark: {
43
+ background: "oklch(0.085 0 0)",
44
+ foreground: "oklch(0.96 0 0)",
45
+ ink: "oklch(0.085 0 0)",
46
+ mutedForeground: "oklch(0.68 0 0)",
47
+ },
48
+ light: {
49
+ background: "oklch(1 0 0)",
50
+ foreground: "oklch(0.145 0 0)",
51
+ ink: "oklch(0.145 0 0)",
52
+ mutedForeground: "oklch(0.53 0 0)",
53
+ },
54
+ } satisfies Record<ColorMode, Record<string, string>>;
55
+
56
+ const WHITE = "oklch(1 0 0)";
57
+
58
+ /** A color this module writes itself, parsed (they all parse). */
59
+ const own = (value: string): Rgba =>
60
+ parseColor(value) ?? { alpha: 1, blue: 0, green: 0, red: 0 };
61
+
62
+ /** {@link THEME_INKS}, parsed, for contrast math. */
63
+ export const themeInk = (
64
+ mode: ColorMode,
65
+ name: keyof (typeof THEME_INKS)[ColorMode]
66
+ ): Rgba => own(THEME_INKS[mode][name]);
67
+
68
+ /**
69
+ * The label color for text on a filled `fill` (a button, a step number): white
70
+ * while it reaches AA, else the mode's dark ink when that reads better, so a
71
+ * light accent (every dark-mode preset) gets dark text instead of white text
72
+ * nobody can read.
73
+ */
74
+ export const labelOn = (fill: Rgba, mode: ColorMode): string => {
75
+ const opaque = composite(fill, themeInk(mode, "background"));
76
+ const onWhite = contrastRatio(own(WHITE), opaque);
77
+ return onWhite >= MIN_TEXT_CONTRAST ||
78
+ onWhite >= contrastRatio(themeInk(mode, "ink"), opaque)
79
+ ? WHITE
80
+ : THEME_INKS[mode].ink;
81
+ };
82
+
83
+ /** {@link labelOn} for a CSS color; one this module can't parse keeps white. */
84
+ export const readableOn = (fill: string, mode: ColorMode): string => {
85
+ const color = parseColor(fill);
86
+ return color ? labelOn(color, mode) : WHITE;
87
+ };
19
88
 
20
89
  /**
21
90
  * Whether a raw config value names an accent preset. `hasOwn` keeps a value
@@ -35,9 +104,11 @@ const CSS_COLOR = /^[\w\s#%.,()/+-]+$/u;
35
104
  const safeColor = (value: string, fallback: string): string =>
36
105
  CSS_COLOR.test(value.trim()) ? value.trim() : fallback;
37
106
 
38
- /** Resolve a named preset or fall back to {@link safeColor}. */
39
- const presetOrColor = (value: string): string =>
40
- isAccentPreset(value) ? ACCENTS[value] : safeColor(value, FALLBACK_ACCENT);
107
+ /** Resolve a named preset in `mode`, or fall back to {@link safeColor}. */
108
+ const presetOrColor = (value: string, mode: ColorMode): string =>
109
+ isAccentPreset(value)
110
+ ? ACCENTS[value][mode]
111
+ : safeColor(value, ACCENTS.blue[mode]);
41
112
 
42
113
  /** Like {@link safeColor} but drops an unsafe/absent value to `null`. */
43
114
  const safeColorOrNull = (value: string | undefined): string | null =>
@@ -58,20 +129,23 @@ const backgroundImageCss = (image: string): string =>
58
129
  const cssToken = (name: string, value?: string | null): string[] =>
59
130
  value ? [` ${name}: ${value};`] : [];
60
131
 
132
+ /** One mode's resolved accent and action fills. */
133
+ interface ModeFills {
134
+ accent: string;
135
+ action: string | null;
136
+ }
137
+
61
138
  const themeRootCss = (
62
139
  theme: ResolvedConfig["theme"],
63
- options: {
64
- accent: string;
65
- action: string | null;
66
- radius: string;
67
- }
140
+ options: ModeFills & { radius: string }
68
141
  ): string =>
69
142
  [
70
143
  ` --blume-accent: ${options.accent};`,
144
+ ` --blume-accent-foreground: ${readableOn(options.accent, "light")};`,
71
145
  ...cssToken("--blume-action", options.action),
72
146
  ...cssToken(
73
147
  "--blume-action-foreground",
74
- options.action ? "oklch(1 0 0)" : null
148
+ options.action ? readableOn(options.action, "light") : null
75
149
  ),
76
150
  ...cssToken("--blume-background", safeColorOrNull(theme.background?.light)),
77
151
  ...cssToken(
@@ -87,10 +161,7 @@ const themeRootCss = (
87
161
 
88
162
  const themeDarkCss = (
89
163
  theme: ResolvedConfig["theme"],
90
- options: {
91
- accent: string;
92
- action: string | null;
93
- }
164
+ options: ModeFills
94
165
  ): string => {
95
166
  // Mode-shared tokens (accent, action) must be re-declared here: the base
96
167
  // stylesheet's own `:root[data-theme="dark"]` block outranks the `:root`
@@ -98,11 +169,11 @@ const themeDarkCss = (
98
169
  // silently keep its neutral defaults and ignore the config.
99
170
  const tokens = [
100
171
  ` --blume-accent: ${options.accent};`,
101
- " --blume-accent-foreground: oklch(1 0 0);",
172
+ ` --blume-accent-foreground: ${readableOn(options.accent, "dark")};`,
102
173
  ...cssToken("--blume-action", options.action),
103
174
  ...cssToken(
104
175
  "--blume-action-foreground",
105
- options.action ? "oklch(1 0 0)" : null
176
+ options.action ? readableOn(options.action, "dark") : null
106
177
  ),
107
178
  ...cssToken("--blume-background", safeColorOrNull(theme.background?.dark)),
108
179
  ...cssToken(
@@ -118,26 +189,31 @@ ${tokens.join("\n")}
118
189
  `;
119
190
  };
120
191
 
121
- /** Per-mode accent CSS colors. */
122
- export interface AccentColors {
123
- dark: string;
124
- light: string;
125
- }
126
-
127
192
  /**
128
193
  * Resolve the configured accent to per-mode CSS colors. A named accent
129
- * resolves to its preset; any other value is treated as a raw CSS color so
130
- * users can pass arbitrary colors without a config change. A string accent
131
- * has already been normalized by the config schema to the same color for
132
- * both modes.
194
+ * resolves to its preset's shade for each mode; any other value is treated as
195
+ * a raw CSS color so users can pass arbitrary colors without a config change.
196
+ * A string accent has already been normalized by the config schema to the
197
+ * same value for both modes.
133
198
  */
134
199
  export const resolveAccent = (
135
200
  theme: ResolvedConfig["theme"]
136
201
  ): AccentColors => ({
137
- dark: presetOrColor(theme.accent.dark),
138
- light: presetOrColor(theme.accent.light),
202
+ dark: presetOrColor(theme.accent.dark, "dark"),
203
+ light: presetOrColor(theme.accent.light, "light"),
139
204
  });
140
205
 
206
+ /** Resolve the configured `action` fill per mode, or `null` when unset. */
207
+ export const resolveAction = (
208
+ theme: ResolvedConfig["theme"]
209
+ ): AccentColors | null =>
210
+ theme.action
211
+ ? {
212
+ dark: presetOrColor(theme.action, "dark"),
213
+ light: presetOrColor(theme.action, "light"),
214
+ }
215
+ : null;
216
+
141
217
  /** Resolve the configured radius preset to a CSS length. */
142
218
  export const resolveRadius = (theme: ResolvedConfig["theme"]): string =>
143
219
  RADII[theme.radius];
@@ -145,20 +221,20 @@ export const resolveRadius = (theme: ResolvedConfig["theme"]): string =>
145
221
  /**
146
222
  * Compile theme config into CSS custom properties. A named accent resolves to
147
223
  * its preset; any other value is treated as a raw CSS color so users can pass
148
- * arbitrary colors without a config change.
224
+ * arbitrary colors without a config change. Labels on accent and action fills
225
+ * take whichever of white or dark ink reads ({@link readableOn}).
149
226
  */
150
227
  export const buildThemeCss = (theme: ResolvedConfig["theme"]): string => {
151
228
  const accent = resolveAccent(theme);
152
- const action = theme.action ? presetOrColor(theme.action) : null;
153
- const radius = RADII[theme.radius];
229
+ const action = resolveAction(theme);
154
230
  const root = themeRootCss(theme, {
155
231
  accent: accent.light,
156
- action,
157
- radius,
232
+ action: action?.light ?? null,
233
+ radius: RADII[theme.radius],
158
234
  });
159
235
  const dark = themeDarkCss(theme, {
160
236
  accent: accent.dark,
161
- action,
237
+ action: action?.dark ?? null,
162
238
  });
163
239
 
164
240
  return `/* Generated by Blume from theme config. */
@@ -1,12 +1,14 @@
1
+ import { existsSync } from "node:fs";
1
2
  import { readFile } from "node:fs/promises";
2
3
 
3
4
  import { dirname, join, relative } from "pathe";
4
- import { glob } from "tinyglobby";
5
5
 
6
+ import { localeTargetPath } from "../core/i18n.ts";
6
7
  import { createModuleLoader } from "../core/load-module.ts";
8
+ import { findFolderMetaFiles } from "../core/meta.ts";
7
9
  import type { BlumeProject } from "../core/project-graph.ts";
8
10
  import { folderMetaSchema } from "../core/schema.ts";
9
- import type { FolderMeta } from "../core/schema.ts";
11
+ import type { FolderMeta, ResolvedI18nConfig } from "../core/schema.ts";
10
12
  import type { Diagnostic } from "../core/types.ts";
11
13
 
12
14
  /**
@@ -37,7 +39,10 @@ export interface TranslatableMeta {
37
39
  title: string;
38
40
  }
39
41
 
40
- const META_FILES = ["**/meta.ts", "**/meta.js", "**/meta.mjs"];
42
+ /** A folder's meta module names, in the order a lookup prefers them. */
43
+ const META_NAMES = ["meta.ts", "meta.js", "meta.mjs"];
44
+
45
+ const META_FILES = META_NAMES.map((name) => `**/${name}`);
41
46
 
42
47
  /**
43
48
  * Whether a loaded meta module default-exports a factory function. Generic so
@@ -48,21 +53,44 @@ const isFactoryModule = <T>(
48
53
  ): value is T & ((...args: never[]) => FolderMeta) =>
49
54
  typeof value === "function";
50
55
 
51
- /** Where a locale's generated meta module lives (always written as `meta.ts`). */
56
+ /**
57
+ * Where a locale's meta module lives. The locale folder is resolved the way
58
+ * pages resolve it (`localeTargetPath`): `folders` are the content root's
59
+ * existing top-level folders, and one naming the locale in another casing
60
+ * (`pt-br/` for a configured `pt-BR`) is the locale's folder already. In that
61
+ * folder, a `meta.js`/`meta.mjs` that already exists is the target — the
62
+ * generated module is plain JavaScript, and writing a `meta.ts` beside it
63
+ * would leave the folder with two meta files. Otherwise it's a new `meta.ts`.
64
+ */
52
65
  export const metaTargetPath = (
53
66
  meta: TranslatableMeta,
54
- locale: string
55
- ): string => join(meta.contentRoot, locale, meta.dir, "meta.ts");
67
+ locale: string,
68
+ i18n: ResolvedI18nConfig,
69
+ folders: readonly string[]
70
+ ): string => {
71
+ const dir = join(
72
+ meta.contentRoot,
73
+ dirname(
74
+ localeTargetPath(join(meta.dir, "meta.ts"), ".ts", locale, i18n, folders)
75
+ )
76
+ );
77
+ const existing = META_NAMES.find((name) => existsSync(join(dir, name)));
78
+ return join(dir, existing ?? "meta.ts");
79
+ };
56
80
 
57
81
  /**
58
82
  * Discover the default-locale `meta.{ts,js,mjs}` files whose titles a
59
83
  * translation run covers. Skips (in order): non-`dir` i18n projects entirely,
60
- * files inside a configured locale directory (those ARE translations), files
61
- * inside an archived-version snapshot (frozen, with their own translations —
62
- * the same folders the navigation's meta lookup hoists as versions),
63
- * factory-form modules (a warning — the generator can't re-emit a function),
64
- * modules that fail meta validation (the scan already errors on those), and
65
- * modules with no `title` (nothing to translate).
84
+ * files the scan's own folder-meta discovery never reads either — those
85
+ * matching the source's `exclude` globs (application code) or in folders its
86
+ * `include` globs don't reach (see `findFolderMetaFiles`) — files inside a
87
+ * configured locale directory (those ARE translations), files inside an
88
+ * archived-version snapshot (frozen, with their own translations — the same
89
+ * folders the navigation's meta lookup hoists as versions), modules that fail
90
+ * to load (the scan already errors on those), factory-form modules (a warning
91
+ * — the generator can't re-emit a function), modules that fail meta
92
+ * validation (the scan already errors on those too), and modules with no
93
+ * `title` (nothing to translate).
66
94
  */
67
95
  export const discoverTranslatableMeta = async (
68
96
  project: BlumeProject
@@ -85,16 +113,26 @@ export const discoverTranslatableMeta = async (
85
113
  const diagnostics: Diagnostic[] = [];
86
114
 
87
115
  const roots = project.sources.flatMap((source) =>
88
- source.staged || !source.contentRoot ? [] : [source.contentRoot]
116
+ source.staged || !source.contentRoot
117
+ ? []
118
+ : [
119
+ {
120
+ contentRoot: source.contentRoot,
121
+ exclude: source.exclude,
122
+ include: source.include,
123
+ },
124
+ ]
89
125
  );
90
- for (const contentRoot of roots) {
126
+ for (const { contentRoot, exclude, include } of roots) {
127
+ // The scan's own file set (`discoverFolderMeta`): a `meta.ts` under an
128
+ // excluded folder (`src/lib/meta.ts` beside `exclude: ["src/**"]`) is
129
+ // application code, and one in a folder no `include` glob reaches
130
+ // configures no group the build renders.
91
131
  // oxlint-disable-next-line no-await-in-loop -- a project has O(1) sources
92
- const files = await glob(META_FILES, {
93
- absolute: true,
94
- cwd: contentRoot,
95
- ignore: ["**/node_modules/**", "**/.blume/**", "**/dist/**"],
96
- onlyFiles: true,
97
- });
132
+ const files = await findFolderMetaFiles(
133
+ { exclude, include, root: contentRoot },
134
+ META_FILES
135
+ );
98
136
  for (const file of files.toSorted()) {
99
137
  const dir = relative(contentRoot, dirname(file));
100
138
  const [head] = dir.split("/");
@@ -104,11 +142,17 @@ export const discoverTranslatableMeta = async (
104
142
  ) {
105
143
  continue;
106
144
  }
145
+ let mod: Awaited<ReturnType<typeof load>>;
146
+ try {
147
+ // oxlint-disable-next-line no-await-in-loop -- sequential, ordered discovery
148
+ mod = await load(file);
149
+ } catch {
150
+ // Folder meta that fails to load is the scan's BLUME_META_LOAD_FAILED
151
+ // error, not a crash here; either way there's no title to read.
152
+ continue;
153
+ }
107
154
  // oxlint-disable-next-line no-await-in-loop -- sequential, ordered discovery
108
- const [raw, mod] = await Promise.all([
109
- readFile(file, "utf-8"),
110
- load(file),
111
- ]);
155
+ const raw = await readFile(file, "utf-8");
112
156
  if (isFactoryModule(mod)) {
113
157
  diagnostics.push({
114
158
  code: "BLUME_TRANSLATE_META_FACTORY",
@@ -64,6 +64,7 @@ ${previousTranslation}`;
64
64
  Rules:
65
65
  - Translate the prose: headings, paragraphs, list items, table cells, admonitions, and image alt text.
66
66
  - In the YAML frontmatter, translate ONLY the values of these keys: ${KEY_LIST}. Copy every other frontmatter key and value exactly as written.
67
+ - Keep the frontmatter valid YAML: if a translated value contains a colon followed by a space (\`: \`) or a space followed by \`#\`, or starts with a quote, a backtick, or another YAML indicator character, write it as a double-quoted string and escape any double quotes inside it.
67
68
  - Never translate or alter: code blocks, inline code, import/export statements, JSX/MDX component names and their attributes, URLs, link targets, HTML tags, or frontmatter keys.
68
69
  - A heading may end with anchor markers: \`[#some-id]\`, \`{#some-id}\` (also written \`\\{#some-id\\}\`), \`[!toc]\`, or \`[toc]\`. Copy every marker exactly as written, in the same position at the end of the translated heading — never translate, reorder, or drop a marker, and never change the id inside \`[#...]\` or \`{#...}\` (anchors must stay identical across languages).
69
70
 
@@ -146,9 +146,9 @@ const runPageItem = async (
146
146
  // SAFETY: `targets` maps every configured locale, and work items only carry
147
147
  // configured locale codes.
148
148
  const target = context.targets.get(item.locale) as LocaleConfig;
149
- // A hand-authored translation can live at a non-canonical name (see
150
- // WorkStatus); the disk probe finds only canonical targets, and a miss just
151
- // means the prompt goes out without a style precedent.
149
+ // A stale item's target is the existing translation's own file, even at a
150
+ // non-canonical name (see `PageWorkItem.targetPath`), so it's the style
151
+ // precedent and the file rewritten; a missing item has nothing to read.
152
152
  const previousTranslation = existsSync(item.targetPath)
153
153
  ? await readFile(item.targetPath, "utf-8")
154
154
  : undefined;
@@ -32,7 +32,10 @@ interface FrontmatterData {
32
32
  [key: string]: FrontmatterValue;
33
33
  }
34
34
 
35
- const FRONTMATTER_OPEN = /^---\r?\n/u;
35
+ // gray-matter strips a leading byte order mark before it looks for the fence,
36
+ // so a BOM-prefixed file still has frontmatter — this test must agree, or the
37
+ // reconstruction below writes the body alone and the metadata is lost.
38
+ const FRONTMATTER_OPEN = /^?---\r?\n/u;
36
39
  const FENCE_LINE = /^\s*(?:```|~~~)/u;
37
40
 
38
41
  /**
@@ -129,8 +132,14 @@ export const validateTranslation = (
129
132
  let parsed: { content: string; data: FrontmatterData };
130
133
  try {
131
134
  parsed = matter(candidate);
132
- } catch {
133
- return { ok: false, reason: "frontmatter does not parse as YAML" };
135
+ } catch (error) {
136
+ // js-yaml's message leads with the problem and its line:column in the
137
+ // file, then a multi-line code frame; the first line is enough to act on.
138
+ const message = error instanceof Error ? error.message : String(error);
139
+ return {
140
+ ok: false,
141
+ reason: `frontmatter does not parse as YAML: ${message.replace(/\n[\s\S]*$/u, "")}`,
142
+ };
134
143
  }
135
144
 
136
145
  const rawBody = ensureTrailingNewline(parsed.content.replace(/^\r?\n/u, ""));
@@ -1,7 +1,7 @@
1
1
  import { existsSync, readdirSync } from "node:fs";
2
2
  import { readFile } from "node:fs/promises";
3
3
 
4
- import { dirname, extname, join, relative } from "pathe";
4
+ import { extname, join, relative } from "pathe";
5
5
 
6
6
  import { localeCodes, localeTargetPath } from "../core/i18n.ts";
7
7
  import { scanProject } from "../core/project-graph.ts";
@@ -35,7 +35,11 @@ export interface PageWorkItem {
35
35
  /** POSIX root-relative source path — the ledger key. */
36
36
  sourceRel: string;
37
37
  status: WorkStatus;
38
- /** Absolute canonical target path (`localeTargetPath`). */
38
+ /**
39
+ * Absolute target path: an existing translation's own file (which can sit
40
+ * at a non-canonical name, like a hand-written `fr/guide.md` for
41
+ * `guide.mdx`), otherwise the canonical `localeTargetPath`.
42
+ */
39
43
  targetPath: string;
40
44
  /** POSIX root-relative target path, for display. */
41
45
  targetRel: string;
@@ -49,7 +53,7 @@ export interface PageWorkItem {
49
53
  export interface MetaWorkEntry {
50
54
  meta: TranslatableMeta;
51
55
  status: WorkStatus;
52
- /** Absolute path of the generated per-locale `meta.ts`. */
56
+ /** Absolute path of the per-locale meta module (see `metaTargetPath`). */
53
57
  targetPath: string;
54
58
  }
55
59
 
@@ -283,6 +287,37 @@ const partialWorkItems = async (
283
287
  return work;
284
288
  };
285
289
 
290
+ /**
291
+ * Every existing translation, keyed by `translationKey` and locale, with the
292
+ * file it was read from when that file is on disk in a filesystem source. A
293
+ * retranslation writes there: an adopted hand-written `fr/guide.md` for a
294
+ * `guide.mdx` source is replaced in place, never joined by a canonical
295
+ * `fr/guide.mdx` that would publish the route twice.
296
+ */
297
+ const existingTranslations = (
298
+ project: BlumeProject
299
+ ): Map<string, string | undefined> => {
300
+ const writableSources = new Set(
301
+ project.sources.flatMap((source) =>
302
+ source.staged || !source.contentRoot ? [] : [source.name]
303
+ )
304
+ );
305
+ const translated = new Map<string, string | undefined>();
306
+ for (const page of project.graph.pages) {
307
+ const key = `${page.translationKey}\0${page.locale}`;
308
+ if (page.fallback || translated.has(key)) {
309
+ continue;
310
+ }
311
+ translated.set(
312
+ key,
313
+ page.sourcePath && writableSources.has(page.source.name)
314
+ ? page.sourcePath
315
+ : undefined
316
+ );
317
+ }
318
+ return translated;
319
+ };
320
+
286
321
  /**
287
322
  * Compute the run's work: which (source, locale) pairs are missing or stale,
288
323
  * which existing translations to adopt, and what's already up to date.
@@ -310,11 +345,7 @@ export const computeWorkList = async (
310
345
  (options.locales === undefined || options.locales.includes(code))
311
346
  );
312
347
 
313
- const translated = new Set(
314
- project.graph.pages.flatMap((page) =>
315
- page.fallback ? [] : [`${page.translationKey}${page.locale}`]
316
- )
317
- );
348
+ const translated = existingTranslations(project);
318
349
 
319
350
  const { root } = project.context;
320
351
  const knownSources = new Set<string>();
@@ -333,10 +364,13 @@ export const computeWorkList = async (
333
364
  const contentRel = relative(contentRoot, sourcePath);
334
365
 
335
366
  for (const locale of targetLocales) {
336
- const targetPath = join(
337
- contentRoot,
338
- localeTargetPath(contentRel, ext, locale, i18n, folders(contentRoot))
339
- );
367
+ const key = `${page.translationKey}\0${locale}`;
368
+ const targetPath =
369
+ translated.get(key) ??
370
+ join(
371
+ contentRoot,
372
+ localeTargetPath(contentRel, ext, locale, i18n, folders(contentRoot))
373
+ );
340
374
  const item = (status: WorkStatus): PageWorkItem => ({
341
375
  kind: "page",
342
376
  locale,
@@ -346,7 +380,7 @@ export const computeWorkList = async (
346
380
  targetPath,
347
381
  targetRel: relative(root, targetPath),
348
382
  });
349
- const exists = translated.has(`${page.translationKey}${locale}`);
383
+ const exists = translated.has(key);
350
384
  const stamp = ledger.files[sourceRel]?.[locale];
351
385
  const outcome = classifyPair(exists, stamp, hash, options.force === true);
352
386
  if (outcome === "untracked") {
@@ -380,16 +414,21 @@ export const computeWorkList = async (
380
414
  const entries: MetaWorkEntry[] = [];
381
415
  for (const source of meta.metas) {
382
416
  knownSources.add(source.sourceRel);
383
- const targetDir = dirname(metaTargetPath(source, locale));
384
- const exists = ["meta.ts", "meta.js", "meta.mjs"].some((name) =>
385
- existsSync(join(targetDir, name))
417
+ // The locale folder's existing meta module when there is one, so a
418
+ // retranslation rewrites it instead of adding a `meta.ts` beside it.
419
+ const targetPath = metaTargetPath(
420
+ source,
421
+ locale,
422
+ i18n,
423
+ folders(source.contentRoot)
386
424
  );
425
+ const exists = existsSync(targetPath);
387
426
  const hash = hashSource(source.raw);
388
427
  const stamp = ledger.files[source.sourceRel]?.[locale];
389
428
  const entry = (status: WorkStatus): MetaWorkEntry => ({
390
429
  meta: source,
391
430
  status,
392
- targetPath: metaTargetPath(source, locale),
431
+ targetPath,
393
432
  });
394
433
  const outcome = classifyPair(exists, stamp, hash, options.force === true);
395
434
  if (outcome === "untracked") {