blume 0.0.0 → 0.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 (263) hide show
  1. package/bin/blume.mjs +18 -0
  2. package/dist/cli/index.js +11989 -0
  3. package/dist/cli/index.js.map +141 -0
  4. package/docs/01-quickstart.mdx +99 -0
  5. package/docs/02-deployment.mdx +129 -0
  6. package/docs/advanced/api-reference.mdx +114 -0
  7. package/docs/advanced/blog.mdx +121 -0
  8. package/docs/advanced/changelog.mdx +113 -0
  9. package/docs/advanced/custom-pages.mdx +187 -0
  10. package/docs/advanced/meta.ts +7 -0
  11. package/docs/changelog/v0-1-0.mdx +12 -0
  12. package/docs/changelog/v0-2-0.mdx +16 -0
  13. package/docs/configuration/ai.mdx +228 -0
  14. package/docs/configuration/analytics.mdx +98 -0
  15. package/docs/configuration/customization.mdx +91 -0
  16. package/docs/configuration/export.mdx +70 -0
  17. package/docs/configuration/index.mdx +271 -0
  18. package/docs/configuration/meta.ts +15 -0
  19. package/docs/configuration/search.mdx +172 -0
  20. package/docs/configuration/seo.mdx +196 -0
  21. package/docs/configuration/theming.mdx +178 -0
  22. package/docs/content/components.mdx +565 -0
  23. package/docs/content/i18n.mdx +205 -0
  24. package/docs/content/index.mdx +161 -0
  25. package/docs/content/islands.mdx +94 -0
  26. package/docs/content/meta.mdx +119 -0
  27. package/docs/content/meta.ts +15 -0
  28. package/docs/content/navigation.mdx +168 -0
  29. package/docs/content/sources.mdx +216 -0
  30. package/docs/content/syntax.mdx +445 -0
  31. package/docs/index.mdx +112 -0
  32. package/docs/reference/cli.mdx +43 -0
  33. package/docs/reference/frontmatter.mdx +74 -0
  34. package/docs/reference/meta.ts +7 -0
  35. package/package.json +140 -6
  36. package/src/ai/ask.ts +93 -0
  37. package/src/ai/llms.ts +65 -0
  38. package/src/ai/markdown.ts +31 -0
  39. package/src/ai/mcp/data.ts +74 -0
  40. package/src/ai/mcp/discovery.ts +49 -0
  41. package/src/ai/mcp/server.ts +225 -0
  42. package/src/ai/mcp/tools.ts +47 -0
  43. package/src/assets/icon.png +0 -0
  44. package/src/astro/generate.ts +878 -0
  45. package/src/astro/index.ts +4 -0
  46. package/src/astro/integration.ts +74 -0
  47. package/src/astro/islands.ts +131 -0
  48. package/src/astro/markdown-negotiation.ts +68 -0
  49. package/src/astro/pages.ts +28 -0
  50. package/src/astro/templates.ts +1199 -0
  51. package/src/cli/commands/add.ts +81 -0
  52. package/src/cli/commands/build.ts +103 -0
  53. package/src/cli/commands/dev.ts +108 -0
  54. package/src/cli/commands/doctor.ts +74 -0
  55. package/src/cli/commands/eject.ts +57 -0
  56. package/src/cli/commands/init.ts +98 -0
  57. package/src/cli/commands/migrate.ts +39 -0
  58. package/src/cli/commands/preview.ts +39 -0
  59. package/src/cli/commands/sync.ts +52 -0
  60. package/src/cli/commands/validate.ts +60 -0
  61. package/src/cli/index.ts +35 -0
  62. package/src/cli/log.ts +37 -0
  63. package/src/cli/prepare.ts +80 -0
  64. package/src/components/Icon.astro +99 -0
  65. package/src/components/content/Accordion.astro +8 -0
  66. package/src/components/content/AccordionItem.astro +121 -0
  67. package/src/components/content/AutoTypeTable.astro +51 -0
  68. package/src/components/content/Badge.astro +124 -0
  69. package/src/components/content/Callout.astro +73 -0
  70. package/src/components/content/Card.astro +104 -0
  71. package/src/components/content/CardGroup.astro +14 -0
  72. package/src/components/content/CodeGroup.astro +13 -0
  73. package/src/components/content/Color.astro +15 -0
  74. package/src/components/content/ColorItem.astro +87 -0
  75. package/src/components/content/ColorRow.astro +10 -0
  76. package/src/components/content/Column.astro +6 -0
  77. package/src/components/content/Columns.astro +9 -0
  78. package/src/components/content/Expandable.astro +11 -0
  79. package/src/components/content/FileTree.astro +8 -0
  80. package/src/components/content/Frame.astro +70 -0
  81. package/src/components/content/GithubInfo.astro +110 -0
  82. package/src/components/content/Math.astro +24 -0
  83. package/src/components/content/Panel.astro +20 -0
  84. package/src/components/content/Prompt.astro +129 -0
  85. package/src/components/content/Step.astro +34 -0
  86. package/src/components/content/Steps.astro +20 -0
  87. package/src/components/content/Tab.astro +40 -0
  88. package/src/components/content/Tabs.astro +273 -0
  89. package/src/components/content/Tile.astro +42 -0
  90. package/src/components/content/Tooltip.astro +68 -0
  91. package/src/components/content/Tree.astro +300 -0
  92. package/src/components/content/TreeFile.astro +15 -0
  93. package/src/components/content/TreeFolder.astro +62 -0
  94. package/src/components/content/TypeTable.astro +106 -0
  95. package/src/components/content/Update.astro +66 -0
  96. package/src/components/content/Visibility.astro +12 -0
  97. package/src/components/content/Warning.astro +9 -0
  98. package/src/components/content/auto-type-table.ts +141 -0
  99. package/src/components/content/github-info.ts +79 -0
  100. package/src/components/content/mermaid-element.ts +68 -0
  101. package/src/components/github-mark.ts +9 -0
  102. package/src/components/index.ts +14 -0
  103. package/src/components/islands/AskAI.astro +12 -0
  104. package/src/components/islands/ask-ai.tsx +156 -0
  105. package/src/components/layout/Analytics.astro +63 -0
  106. package/src/components/layout/Banner.astro +50 -0
  107. package/src/components/layout/Breadcrumbs.astro +31 -0
  108. package/src/components/layout/Favicon.astro +15 -0
  109. package/src/components/layout/Fonts.astro +14 -0
  110. package/src/components/layout/Header.astro +188 -0
  111. package/src/components/layout/LanguageSwitcher.astro +56 -0
  112. package/src/components/layout/NavTree.astro +462 -0
  113. package/src/components/layout/PageActions.astro +438 -0
  114. package/src/components/layout/PageFeedback.astro +58 -0
  115. package/src/components/layout/Pagination.astro +56 -0
  116. package/src/components/layout/ReferenceLayout.astro +102 -0
  117. package/src/components/layout/RootLayout.astro +533 -0
  118. package/src/components/layout/Search.astro +608 -0
  119. package/src/components/layout/TableOfContents.astro +68 -0
  120. package/src/components/layout/analytics-client.ts +38 -0
  121. package/src/components/layout/nav-utils.ts +87 -0
  122. package/src/components/layout/overrides.ts +32 -0
  123. package/src/components/layout/search/algolia.ts +43 -0
  124. package/src/components/layout/search/endpoint.ts +22 -0
  125. package/src/components/layout/search/flexsearch.ts +52 -0
  126. package/src/components/layout/search/orama-cloud.ts +41 -0
  127. package/src/components/layout/search/orama.ts +26 -0
  128. package/src/components/layout/search/pagefind.ts +43 -0
  129. package/src/components/layout/search/types.ts +163 -0
  130. package/src/components/layout/search/typesense.ts +60 -0
  131. package/src/components/layout/toc-element.ts +108 -0
  132. package/src/core/bridge.ts +92 -0
  133. package/src/core/config.ts +112 -0
  134. package/src/core/content.ts +50 -0
  135. package/src/core/define-components.ts +34 -0
  136. package/src/core/define-meta.ts +20 -0
  137. package/src/core/deployment-env.ts +73 -0
  138. package/src/core/diagnostics.ts +104 -0
  139. package/src/core/graph.ts +128 -0
  140. package/src/core/i18n-ui.ts +171 -0
  141. package/src/core/i18n.ts +169 -0
  142. package/src/core/last-modified.ts +88 -0
  143. package/src/core/links.ts +336 -0
  144. package/src/core/load-module.ts +15 -0
  145. package/src/core/manifest.ts +126 -0
  146. package/src/core/meta.ts +97 -0
  147. package/src/core/navigation.ts +392 -0
  148. package/src/core/package-root.ts +37 -0
  149. package/src/core/project-graph.ts +153 -0
  150. package/src/core/project.ts +56 -0
  151. package/src/core/schema.ts +1057 -0
  152. package/src/core/server-features.ts +23 -0
  153. package/src/core/sources/assets.ts +77 -0
  154. package/src/core/sources/cache.ts +122 -0
  155. package/src/core/sources/filesystem.ts +99 -0
  156. package/src/core/sources/mdx-remote.ts +216 -0
  157. package/src/core/sources/mintlify.ts +161 -0
  158. package/src/core/sources/normalize.ts +227 -0
  159. package/src/core/sources/notion.ts +440 -0
  160. package/src/core/sources/portable-text.ts +143 -0
  161. package/src/core/sources/read.ts +36 -0
  162. package/src/core/sources/resolve.ts +158 -0
  163. package/src/core/sources/sanity.ts +218 -0
  164. package/src/core/sources/types.ts +105 -0
  165. package/src/core/types.ts +261 -0
  166. package/src/core/ui-packs/ar.ts +47 -0
  167. package/src/core/ui-packs/bg.ts +47 -0
  168. package/src/core/ui-packs/bn.ts +47 -0
  169. package/src/core/ui-packs/ca.ts +47 -0
  170. package/src/core/ui-packs/cs.ts +47 -0
  171. package/src/core/ui-packs/da.ts +47 -0
  172. package/src/core/ui-packs/de.ts +47 -0
  173. package/src/core/ui-packs/el.ts +47 -0
  174. package/src/core/ui-packs/es.ts +47 -0
  175. package/src/core/ui-packs/fa.ts +47 -0
  176. package/src/core/ui-packs/fi.ts +47 -0
  177. package/src/core/ui-packs/fr.ts +47 -0
  178. package/src/core/ui-packs/he.ts +47 -0
  179. package/src/core/ui-packs/hi.ts +47 -0
  180. package/src/core/ui-packs/hr.ts +47 -0
  181. package/src/core/ui-packs/hu.ts +47 -0
  182. package/src/core/ui-packs/id.ts +47 -0
  183. package/src/core/ui-packs/index.ts +87 -0
  184. package/src/core/ui-packs/it.ts +47 -0
  185. package/src/core/ui-packs/ja.ts +47 -0
  186. package/src/core/ui-packs/ko.ts +47 -0
  187. package/src/core/ui-packs/nl.ts +47 -0
  188. package/src/core/ui-packs/no.ts +47 -0
  189. package/src/core/ui-packs/pl.ts +47 -0
  190. package/src/core/ui-packs/pt-br.ts +47 -0
  191. package/src/core/ui-packs/pt.ts +47 -0
  192. package/src/core/ui-packs/ro.ts +47 -0
  193. package/src/core/ui-packs/ru.ts +47 -0
  194. package/src/core/ui-packs/sk.ts +47 -0
  195. package/src/core/ui-packs/sr.ts +47 -0
  196. package/src/core/ui-packs/sv.ts +47 -0
  197. package/src/core/ui-packs/th.ts +47 -0
  198. package/src/core/ui-packs/tr.ts +47 -0
  199. package/src/core/ui-packs/uk.ts +47 -0
  200. package/src/core/ui-packs/vi.ts +47 -0
  201. package/src/core/ui-packs/zh-tw.ts +47 -0
  202. package/src/core/ui-packs/zh.ts +47 -0
  203. package/src/core/version.ts +23 -0
  204. package/src/deploy/robots.ts +20 -0
  205. package/src/deploy/rss.ts +128 -0
  206. package/src/deploy/sitemap.ts +28 -0
  207. package/src/index.ts +27 -0
  208. package/src/markdown/code-title.ts +71 -0
  209. package/src/markdown/directives.ts +83 -0
  210. package/src/markdown/heading-anchors.ts +137 -0
  211. package/src/markdown/index.ts +159 -0
  212. package/src/markdown/inline-code.ts +108 -0
  213. package/src/markdown/language-icon.ts +172 -0
  214. package/src/markdown/math.ts +32 -0
  215. package/src/markdown/mdast.ts +48 -0
  216. package/src/markdown/mermaid.ts +37 -0
  217. package/src/markdown/package-commands.ts +159 -0
  218. package/src/markdown/package-install.ts +40 -0
  219. package/src/migrate/fumadocs/config.ts +106 -0
  220. package/src/migrate/fumadocs/content.ts +365 -0
  221. package/src/migrate/fumadocs/frontmatter.ts +18 -0
  222. package/src/migrate/fumadocs/index.ts +252 -0
  223. package/src/migrate/fumadocs/meta.ts +114 -0
  224. package/src/migrate/migrate.ts +53 -0
  225. package/src/migrate/mintlify/config.ts +1040 -0
  226. package/src/migrate/mintlify/content.ts +98 -0
  227. package/src/migrate/mintlify/frontmatter.ts +126 -0
  228. package/src/migrate/mintlify/i18n.ts +51 -0
  229. package/src/migrate/mintlify/icons.ts +128 -0
  230. package/src/migrate/mintlify/index.ts +266 -0
  231. package/src/migrate/mintlify/snippets.ts +305 -0
  232. package/src/migrate/mintlify/transform.ts +81 -0
  233. package/src/migrate/nextra/content.ts +46 -0
  234. package/src/migrate/nextra/frontmatter.ts +40 -0
  235. package/src/migrate/nextra/index.ts +374 -0
  236. package/src/migrate/nextra/meta.ts +266 -0
  237. package/src/migrate/shared.ts +623 -0
  238. package/src/migrate/starlight/config.ts +459 -0
  239. package/src/migrate/starlight/content.ts +78 -0
  240. package/src/migrate/starlight/frontmatter.ts +111 -0
  241. package/src/migrate/starlight/i18n.ts +54 -0
  242. package/src/migrate/starlight/index.ts +131 -0
  243. package/src/og/card.ts +92 -0
  244. package/src/og/index.ts +2 -0
  245. package/src/openapi/scalar.ts +246 -0
  246. package/src/registry/eject.ts +263 -0
  247. package/src/registry/registry.ts +100 -0
  248. package/src/registry/rewrite-imports.ts +39 -0
  249. package/src/runtime/index.ts +14 -0
  250. package/src/search/build.ts +23 -0
  251. package/src/search/documents.ts +165 -0
  252. package/src/search/orama-index.ts +66 -0
  253. package/src/search/providers.ts +91 -0
  254. package/src/search/sync/algolia.ts +30 -0
  255. package/src/search/sync/index.ts +50 -0
  256. package/src/search/sync/orama-cloud.ts +40 -0
  257. package/src/search/sync/typesense.ts +65 -0
  258. package/src/seo/jsonld.ts +113 -0
  259. package/src/theme/entry.ts +608 -0
  260. package/src/theme/fonts.ts +198 -0
  261. package/src/theme/icons.ts +184 -0
  262. package/src/theme/palette.ts +143 -0
  263. package/src/theme/twoslash.ts +81 -0
@@ -0,0 +1,438 @@
1
+ ---
2
+ import { EN_UI } from "../../core/i18n-ui.ts";
3
+ import type { UIStrings } from "../../core/i18n-ui.ts";
4
+ import { GITHUB_MARK } from "../github-mark.ts";
5
+ import Icon from "../Icon.astro";
6
+
7
+ interface Props {
8
+ route: string;
9
+ editUrl?: string | null;
10
+ askEnabled?: boolean;
11
+ /** Show the "Export to PDF" action (native browser print). */
12
+ exportPdf?: boolean;
13
+ /** Show the "Export to EPUB" action (client-side, current page). */
14
+ exportEpub?: boolean;
15
+ /** Absolute URL of the hosted MCP server, when enabled and a site is set. */
16
+ mcpUrl?: string | null;
17
+ /** Display name of the MCP server (defaults to the site title). */
18
+ mcpName?: string | null;
19
+ /** Localized action labels. */
20
+ strings?: UIStrings["actions"];
21
+ }
22
+
23
+ const {
24
+ route,
25
+ editUrl,
26
+ askEnabled,
27
+ exportPdf,
28
+ exportEpub,
29
+ mcpUrl,
30
+ mcpName,
31
+ strings,
32
+ } = Astro.props;
33
+ const a = strings ?? EN_UI.actions;
34
+ const mdPath = route === "/" ? "/index.md" : `${route}.md`;
35
+
36
+ const GITHUB_LOGO = `<svg class="size-4 shrink-0" fill="currentColor" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg">${GITHUB_MARK}</svg>`;
37
+
38
+ /** Brand logos for the "Open in chat" providers (monochrome, currentColor). */
39
+ const LOGOS: Record<string, string> = {
40
+ chatgpt:
41
+ '<svg class="size-4 shrink-0" fill="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="M22.2819 9.8211a5.9847 5.9847 0 0 0-.5157-4.9108 6.0462 6.0462 0 0 0-6.5098-2.9A6.0651 6.0651 0 0 0 4.9807 4.1818a5.9847 5.9847 0 0 0-3.9977 2.9 6.0462 6.0462 0 0 0 .7427 7.0966 5.98 5.98 0 0 0 .511 4.9107 6.051 6.051 0 0 0 6.5146 2.9001A5.9847 5.9847 0 0 0 13.2599 24a6.0557 6.0557 0 0 0 5.7718-4.2058 5.9894 5.9894 0 0 0 3.9977-2.9001 6.0557 6.0557 0 0 0-.7475-7.0729zm-9.022 12.6081a4.4755 4.4755 0 0 1-2.8764-1.0408l.1419-.0804 4.7783-2.7582a.7948.7948 0 0 0 .3927-.6813v-6.7369l2.02 1.1686a.071.071 0 0 1 .038.052v5.5826a4.504 4.504 0 0 1-4.4945 4.4944zm-9.6607-4.1254a4.4708 4.4708 0 0 1-.5346-3.0137l.142.0852 4.783 2.7582a.7712.7712 0 0 0 .7806 0l5.8428-3.3685v2.3324a.0804.0804 0 0 1-.0332.0615L9.74 19.9502a4.4992 4.4992 0 0 1-6.1408-1.6464zM2.3408 7.8956a4.485 4.485 0 0 1 2.3655-1.9728V11.6a.7664.7664 0 0 0 .3879.6765l5.8144 3.3543-2.0201 1.1685a.0757.0757 0 0 1-.071 0l-4.8303-2.7865A4.504 4.504 0 0 1 2.3408 7.872zm16.5963 3.8558L13.1038 8.364 15.1192 7.2a.0757.0757 0 0 1 .071 0l4.8303 2.7913a4.4944 4.4944 0 0 1-.6765 8.1042v-5.6772a.79.79 0 0 0-.407-.667zm2.0107-3.0231l-.142-.0852-4.7735-2.7818a.7759.7759 0 0 0-.7854 0L9.409 9.2297V6.8974a.0662.0662 0 0 1 .0284-.0615l4.8303-2.7866a4.4992 4.4992 0 0 1 6.6802 4.66zM8.3065 12.863l-2.02-1.1638a.0804.0804 0 0 1-.038-.0567V6.0742a4.4992 4.4992 0 0 1 7.3757-3.4537l-.142.0805L8.704 5.459a.7948.7948 0 0 0-.3927.6813zm1.0976-2.3654l2.602-1.4998 2.6069 1.4998v2.9994l-2.5974 1.4997-2.6067-1.4997Z"/></svg>',
42
+ claude:
43
+ '<svg class="size-4 shrink-0" fill="currentColor" viewBox="0 0 12 12" xmlns="http://www.w3.org/2000/svg"><path clip-rule="evenodd" fill-rule="evenodd" d="M2.3545 7.9775L4.7145 6.654L4.7545 6.539L4.7145 6.475H4.6L4.205 6.451L2.856 6.4145L1.6865 6.366L0.5535 6.305L0.268 6.2445L0 5.892L0.0275 5.716L0.2675 5.5555L0.6105 5.5855L1.3705 5.637L2.5095 5.716L3.3355 5.7645L4.56 5.892H4.7545L4.782 5.8135L4.715 5.7645L4.6635 5.716L3.4845 4.918L2.2085 4.074L1.5405 3.588L1.1785 3.3425L0.9965 3.1115L0.9175 2.6075L1.2455 2.2465L1.686 2.2765L1.7985 2.307L2.245 2.65L3.199 3.388L4.4445 4.3045L4.627 4.4565L4.6995 4.405L4.709 4.3685L4.627 4.2315L3.9495 3.0085L3.2265 1.7635L2.9045 1.2475L2.8195 0.938C2.78711 0.819128 2.76965 0.696687 2.7675 0.5735L3.1415 0.067L3.348 0L3.846 0.067L4.056 0.249L4.366 0.956L4.867 2.0705L5.6445 3.5855L5.8725 4.0345L5.994 4.4505L6.0395 4.578H6.1185V4.505L6.1825 3.652L6.301 2.6045L6.416 1.257L6.456 0.877L6.644 0.422L7.0175 0.176L7.3095 0.316L7.5495 0.6585L7.516 0.8805L7.373 1.806L7.0935 3.2575L6.9115 4.2285H7.0175L7.139 4.1075L7.6315 3.4545L8.4575 2.4225L8.8225 2.0125L9.2475 1.5605L9.521 1.345H10.0375L10.4175 1.9095L10.2475 2.4925L9.7155 3.166L9.275 3.737L8.643 4.587L8.248 5.267L8.2845 5.322L8.3785 5.312L9.8065 5.009L10.578 4.869L11.4985 4.7115L11.915 4.9055L11.9605 5.103L11.7965 5.5065L10.812 5.7495L9.6575 5.9805L7.938 6.387L7.917 6.402L7.9415 6.4325L8.716 6.5055L9.047 6.5235H9.858L11.368 6.636L11.763 6.897L12 7.216L11.9605 7.4585L11.353 7.7685L10.533 7.574L8.6185 7.119L7.9625 6.9545H7.8715V7.0095L8.418 7.5435L9.421 8.4485L10.6755 9.6135L10.739 9.9025L10.578 10.13L10.408 10.1055L9.3055 9.277L8.88 8.9035L7.917 8.0935H7.853V8.1785L8.075 8.503L9.2475 10.2635L9.3085 10.8035L9.2235 10.98L8.9195 11.0865L8.5855 11.0255L7.8985 10.063L7.191 8.9795L6.6195 8.008L6.5495 8.048L6.2125 11.675L6.0545 11.86L5.69 12L5.3865 11.7695L5.2255 11.396L5.3865 10.658L5.581 9.696L5.7385 8.931L5.8815 7.981L5.9665 7.665L5.9605 7.644L5.8905 7.653L5.1735 8.6365L4.0835 10.109L3.2205 11.0315L3.0135 11.1135L2.655 10.9285L2.6885 10.5975L2.889 10.303L4.083 8.785L4.803 7.844L5.268 7.301L5.265 7.222H5.2375L2.066 9.28L1.501 9.353L1.2575 9.125L1.288 8.752L1.4035 8.6305L2.3575 7.9745L2.3545 7.9775Z"/></svg>',
44
+ cursor:
45
+ '<svg class="size-4 shrink-0" viewBox="0 0 466.73 532.09" xmlns="http://www.w3.org/2000/svg"><path d="M457.43,125.94L244.42,2.96c-6.84-3.95-15.28-3.95-22.12,0L9.3,125.94c-5.75,3.32-9.3,9.46-9.3,16.11v247.99c0,6.65,3.55,12.79,9.3,16.11l213.01,122.98c6.84,3.95,15.28,3.95,22.12,0l213.01-122.98c5.75-3.32,9.3-9.46,9.3-16.11v-247.99c0-6.65-3.55-12.79-9.3-16.11h-.01ZM444.05,151.99l-205.63,356.16c-1.39,2.4-5.06,1.42-5.06-1.36v-233.21c0-4.66-2.49-8.97-6.53-11.31L24.87,145.67c-2.4-1.39-1.42-5.06,1.36-5.06h411.26c5.84,0,9.49,6.33,6.57,11.39h-.01Z" fill="currentColor"/></svg>',
46
+ scira:
47
+ '<svg class="size-4 shrink-0" fill="none" viewBox="0 0 910 934" xmlns="http://www.w3.org/2000/svg"><path d="M647.664 197.775C569.13 189.049 525.5 145.419 516.774 66.8849C508.048 145.419 464.418 189.049 385.884 197.775C464.418 206.501 508.048 250.131 516.774 328.665C525.5 250.131 569.13 206.501 647.664 197.775Z" fill="currentColor" stroke="currentColor" stroke-linejoin="round" stroke-width="8"/><path d="M857.5 508.116C763.259 497.644 710.903 445.288 700.432 351.047C689.961 445.288 637.605 497.644 543.364 508.116C637.605 518.587 689.961 570.943 700.432 665.184C710.903 570.943 763.259 518.587 857.5 508.116Z" stroke="currentColor" stroke-linejoin="round" stroke-width="20"/><path d="M889.949 121.237C831.049 114.692 798.326 81.9698 791.782 23.0692C785.237 81.9698 752.515 114.692 693.614 121.237C752.515 127.781 785.237 160.504 791.782 219.404C798.326 160.504 831.049 127.781 889.949 121.237Z" fill="currentColor" stroke="currentColor" stroke-linejoin="round" stroke-width="8"/><path d="M760.632 764.337C720.719 814.616 669.835 855.1 611.872 882.692C553.91 910.285 490.404 924.255 426.213 923.533C362.022 922.812 298.846 907.419 241.518 878.531C184.19 849.643 134.228 808.026 95.4548 756.863C56.6815 705.7 30.1238 646.346 17.8129 583.343C5.50207 520.339 7.76433 455.354 24.4266 393.359C41.089 331.364 71.7099 274.001 113.947 225.658C156.184 177.315 208.919 139.273 268.117 114.442" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="30"/></svg>',
48
+ t3: '<svg class="size-4 shrink-0" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="M7.9 20A9 9 0 1 0 4 16.1L2 22Z"/></svg>',
49
+ v0: '<svg class="size-4 shrink-0" fill="currentColor" viewBox="0 0 147 70" xmlns="http://www.w3.org/2000/svg"><path d="M56 50.2031V14H70V60.1562C70 65.5928 65.5928 70 60.1562 70C57.5605 70 54.9982 68.9992 53.1562 67.1573L0 14H19.7969L56 50.2031Z"/><path d="M147 56H133V23.9531L100.953 56H133V70H96.6875C85.8144 70 77 61.1856 77 50.3125V14H91V46.1562L123.156 14H91V0H127.312C138.186 0 147 8.81439 147 19.6875V56Z"/></svg>',
50
+ };
51
+
52
+ const PROVIDERS = [
53
+ { key: "v0", label: "Open in v0" },
54
+ { key: "chatgpt", label: "Open in ChatGPT" },
55
+ { key: "claude", label: "Open in Claude" },
56
+ { key: "t3", label: "Open in T3 Chat" },
57
+ { key: "scira", label: "Open in Scira" },
58
+ { key: "cursor", label: "Open in Cursor" },
59
+ ];
60
+
61
+ // Top-level actions match the ToC links: flat, no background/padding, color
62
+ // shift on hover only.
63
+ const rowClass =
64
+ "flex w-full items-center gap-2.5 py-1.5 text-start text-muted-foreground text-sm transition-colors hover:text-foreground";
65
+
66
+ // Items inside the floating popovers keep menu affordances (padding + hover
67
+ // background).
68
+ const menuRowClass =
69
+ "flex w-full items-center gap-2.5 rounded-md px-2 py-1.5 text-start text-muted-foreground text-sm transition-colors hover:bg-muted hover:text-foreground";
70
+ ---
71
+
72
+ <div
73
+ class="mt-8 space-y-0.5 border-border border-t pt-4"
74
+ data-blume-page-actions
75
+ data-i18n-copied={a.copied}
76
+ data-mcp-name={mcpName ?? undefined}
77
+ data-mcp-url={mcpUrl ?? undefined}
78
+ data-md={mdPath}
79
+ >
80
+ {
81
+ editUrl && (
82
+ <a class={rowClass} href={editUrl} rel="noreferrer" target="_blank">
83
+ <span set:html={GITHUB_LOGO} />
84
+ {a.edit}
85
+ </a>
86
+ )
87
+ }
88
+
89
+ <button class={rowClass} data-blume-scroll-top type="button">
90
+ <Icon name="arrow-up" size={16} />
91
+ {a.scrollToTop}
92
+ </button>
93
+
94
+ <button class={rowClass} data-blume-copy-page type="button">
95
+ <Icon name="copy" size={16} />
96
+ <span data-blume-copy-label>{a.copyMarkdown}</span>
97
+ </button>
98
+
99
+ {
100
+ (exportPdf || exportEpub) && (
101
+ <details class="group relative">
102
+ <summary
103
+ class={`${rowClass} cursor-pointer list-none [&::-webkit-details-marker]:hidden`}
104
+ >
105
+ <Icon name="download" size={16} />
106
+ Export
107
+ <Icon
108
+ class="ms-auto transition-transform group-open:rotate-180"
109
+ name="chevron-down"
110
+ size={14}
111
+ />
112
+ </summary>
113
+ <div class="absolute right-0 left-0 z-50 mt-1 rounded-blume border border-border bg-background p-1 shadow-xl">
114
+ {exportPdf && (
115
+ <button class={menuRowClass} data-blume-export-pdf type="button">
116
+ <Icon name="file" size={16} />
117
+ <span class="flex-1">Export to PDF</span>
118
+ </button>
119
+ )}
120
+ {exportEpub && (
121
+ <button class={menuRowClass} data-blume-export-epub type="button">
122
+ <Icon name="book-open" size={16} />
123
+ <span class="flex-1" data-blume-epub-label>
124
+ Export to EPUB
125
+ </span>
126
+ </button>
127
+ )}
128
+ </div>
129
+ </details>
130
+ )
131
+ }
132
+
133
+ {
134
+ askEnabled && (
135
+ <button class={rowClass} data-blume-ask-ai type="button">
136
+ <Icon name="message-circle" size={16} />
137
+ {a.askAI}
138
+ </button>
139
+ )
140
+ }
141
+
142
+ <details class="group relative">
143
+ <summary
144
+ class={`${rowClass} cursor-pointer list-none [&::-webkit-details-marker]:hidden`}
145
+ >
146
+ <Icon name="external-link" size={16} />
147
+ {a.openInChat}
148
+ <Icon
149
+ class="ms-auto transition-transform group-open:rotate-180"
150
+ name="chevron-down"
151
+ size={14}
152
+ />
153
+ </summary>
154
+ <div class="absolute right-0 left-0 z-50 mt-1 rounded-blume border border-border bg-background p-1 shadow-xl">
155
+ {
156
+ PROVIDERS.map((provider, index) => (
157
+ <>
158
+ {index === 1 && <hr class="my-1 border-border border-t" />}
159
+ <a
160
+ class={menuRowClass}
161
+ data-open-in={provider.key}
162
+ rel="noreferrer"
163
+ target="_blank"
164
+ >
165
+ <span set:html={LOGOS[provider.key]} />
166
+ <span class="flex-1">{provider.label}</span>
167
+ <Icon name="external-link" size={13} />
168
+ </a>
169
+ </>
170
+ ))
171
+ }
172
+ </div>
173
+ </details>
174
+
175
+ {
176
+ mcpUrl && (
177
+ <details class="group relative">
178
+ <summary
179
+ class={`${rowClass} cursor-pointer list-none [&::-webkit-details-marker]:hidden`}
180
+ >
181
+ <Icon name="plug" size={16} />
182
+ {a.connectMcp}
183
+ <Icon
184
+ class="ms-auto transition-transform group-open:rotate-180"
185
+ name="chevron-down"
186
+ size={14}
187
+ />
188
+ </summary>
189
+ <div class="absolute right-0 left-0 z-50 mt-1 rounded-blume border border-border bg-background p-1 shadow-xl">
190
+ <button class={menuRowClass} data-mcp-copy-url type="button">
191
+ <Icon name="link" size={16} />
192
+ <span class="flex-1" data-mcp-url-label>
193
+ {a.copyServerUrl}
194
+ </span>
195
+ </button>
196
+ <button class={menuRowClass} data-mcp-copy-claude type="button">
197
+ <span set:html={LOGOS.claude} />
198
+ <span class="flex-1" data-mcp-claude-label>
199
+ {a.copyClaudeCode}
200
+ </span>
201
+ </button>
202
+ <hr class="my-1 border-border border-t" />
203
+ <a class={menuRowClass} data-mcp-cursor rel="noreferrer">
204
+ <span set:html={LOGOS.cursor} />
205
+ <span class="flex-1">{a.addToCursor}</span>
206
+ <Icon name="external-link" size={13} />
207
+ </a>
208
+ <a class={menuRowClass} data-mcp-vscode rel="noreferrer">
209
+ <Icon name="code" size={16} />
210
+ <span class="flex-1">{a.addToVscode}</span>
211
+ <Icon name="external-link" size={13} />
212
+ </a>
213
+ </div>
214
+ </details>
215
+ )
216
+ }
217
+ </div>
218
+
219
+ <script>
220
+ const CHAT_URLS: Record<string, (q: string) => string> = {
221
+ chatgpt: (q) => `https://chatgpt.com/?hints=search&prompt=${q}`,
222
+ claude: (q) => `https://claude.ai/new?q=${q}`,
223
+ cursor: (q) => `https://cursor.com/link/prompt?text=${q}`,
224
+ scira: (q) => `https://scira.ai/?q=${q}`,
225
+ t3: (q) => `https://t3.chat/new?q=${q}`,
226
+ v0: (q) => `https://v0.app?q=${q}`,
227
+ };
228
+
229
+ // Self-contained e-reader stylesheet for the EPUB. None of Blume's theme CSS
230
+ // ships inside the .epub, so the chapter HTML must stand on its own.
231
+ const EPUB_STYLES = `
232
+ body { line-height: 1.6; }
233
+ h1, h2, h3, h4, h5, h6 { line-height: 1.25; margin: 1.4em 0 0.6em; }
234
+ h1 { font-size: 1.8em; } h2 { font-size: 1.4em; } h3 { font-size: 1.15em; }
235
+ p, ul, ol { margin: 0.8em 0; }
236
+ a { color: #0b63c4; }
237
+ img { max-width: 100%; height: auto; }
238
+ pre {
239
+ background: #f4f4f5; padding: 0.85em 1em; border-radius: 6px;
240
+ overflow-x: auto; white-space: pre-wrap; word-wrap: break-word;
241
+ font-size: 0.85em; line-height: 1.45;
242
+ }
243
+ pre, code { font-family: "SF Mono", Menlo, Consolas, monospace; }
244
+ :not(pre) > code {
245
+ background: #f0f0f1; padding: 0.1em 0.3em; border-radius: 3px; font-size: 0.9em;
246
+ }
247
+ blockquote { margin: 1em 0; padding-left: 1em; border-left: 3px solid #ddd; color: #555; }
248
+ table { border-collapse: collapse; width: 100%; }
249
+ th, td { border: 1px solid #ddd; padding: 0.4em 0.6em; text-align: left; }
250
+ hr { border: 0; border-top: 1px solid #ddd; margin: 2em 0; }`;
251
+
252
+ // Turn the live, JS/CSS-dependent article DOM into clean, self-contained HTML
253
+ // for the EPUB: drop decorative SVGs and interactive controls (which render as
254
+ // broken boxes in a reader), flatten Shiki's variable-colored code to plain
255
+ // monospace text, and reveal panels the tab scripts would otherwise hide.
256
+ const buildEpubChapterHtml = (article: Element): string => {
257
+ const clone = article.cloneNode(true) as Element;
258
+ for (const el of clone.querySelectorAll(
259
+ "svg, button, select, script, style, link, noscript, template"
260
+ )) {
261
+ el.remove();
262
+ }
263
+ for (const pre of clone.querySelectorAll("pre")) {
264
+ const code = document.createElement("code");
265
+ code.textContent = pre.textContent ?? "";
266
+ pre.replaceChildren(code);
267
+ pre.removeAttribute("style");
268
+ pre.removeAttribute("class");
269
+ pre.removeAttribute("tabindex");
270
+ }
271
+ for (const el of clone.querySelectorAll(".hidden")) {
272
+ el.classList.remove("hidden");
273
+ }
274
+ return clone.innerHTML;
275
+ };
276
+
277
+ const root = document.querySelector("[data-blume-page-actions]");
278
+ if (root) {
279
+ const md = root.getAttribute("data-md") ?? "";
280
+ const copiedLabel = root.getAttribute("data-i18n-copied") || "Copied!";
281
+ const absolute = new URL(md, location.origin).href;
282
+ const query = encodeURIComponent(
283
+ `Read ${absolute} so I can ask you questions about this page.`
284
+ );
285
+
286
+ for (const link of root.querySelectorAll("[data-open-in]")) {
287
+ const build = CHAT_URLS[link.getAttribute("data-open-in") ?? ""];
288
+ if (build) {
289
+ link.setAttribute("href", build(query));
290
+ }
291
+ }
292
+
293
+ root
294
+ .querySelector("[data-blume-scroll-top]")
295
+ ?.addEventListener("click", () =>
296
+ window.scrollTo({ behavior: "smooth", top: 0 })
297
+ );
298
+
299
+ root
300
+ .querySelector("[data-blume-ask-ai]")
301
+ ?.addEventListener("click", () =>
302
+ window.dispatchEvent(new CustomEvent("blume:open-ask-ai"))
303
+ );
304
+
305
+ const mcpUrl = root.getAttribute("data-mcp-url");
306
+ if (mcpUrl) {
307
+ const rawName = root.getAttribute("data-mcp-name") || "docs";
308
+ const id =
309
+ rawName
310
+ .toLowerCase()
311
+ .replace(/[^a-z0-9]+/gu, "-")
312
+ .replace(/^-+|-+$/gu, "") || "docs";
313
+
314
+ const cursorConfig = btoa(JSON.stringify({ url: mcpUrl }));
315
+ root
316
+ .querySelector("[data-mcp-cursor]")
317
+ ?.setAttribute(
318
+ "href",
319
+ `cursor://anysphere.cursor-deeplink/mcp/install?name=${encodeURIComponent(id)}&config=${cursorConfig}`
320
+ );
321
+ root
322
+ .querySelector("[data-mcp-vscode]")
323
+ ?.setAttribute(
324
+ "href",
325
+ `vscode:mcp/install?${encodeURIComponent(JSON.stringify({ name: id, type: "http", url: mcpUrl }))}`
326
+ );
327
+
328
+ const flash = (el: Element | null, text: string, reset: string) => {
329
+ if (el) {
330
+ el.textContent = text;
331
+ setTimeout(() => {
332
+ el.textContent = reset;
333
+ }, 1500);
334
+ }
335
+ };
336
+ const copy = async (value: string, el: Element | null) => {
337
+ // Restore the element's own label, so localized text round-trips.
338
+ const reset = el?.textContent ?? "";
339
+ try {
340
+ await navigator.clipboard.writeText(value);
341
+ flash(el, copiedLabel, reset);
342
+ } catch {
343
+ // Clipboard unavailable; nothing to do.
344
+ }
345
+ };
346
+ root
347
+ .querySelector("[data-mcp-copy-url]")
348
+ ?.addEventListener("click", () =>
349
+ copy(mcpUrl, root.querySelector("[data-mcp-url-label]"))
350
+ );
351
+ root
352
+ .querySelector("[data-mcp-copy-claude]")
353
+ ?.addEventListener("click", () =>
354
+ copy(
355
+ `claude mcp add --transport http ${id} ${mcpUrl}`,
356
+ root.querySelector("[data-mcp-claude-label]")
357
+ )
358
+ );
359
+ }
360
+
361
+ const label = root.querySelector("[data-blume-copy-label]");
362
+ root
363
+ .querySelector("[data-blume-copy-page]")
364
+ ?.addEventListener("click", async () => {
365
+ const original = label?.textContent ?? "";
366
+ try {
367
+ const response = await fetch(md);
368
+ await navigator.clipboard.writeText(await response.text());
369
+ if (label) {
370
+ label.textContent = copiedLabel;
371
+ setTimeout(() => {
372
+ label.textContent = original;
373
+ }, 1500);
374
+ }
375
+ } catch {
376
+ // Clipboard or fetch unavailable; nothing to do.
377
+ }
378
+ });
379
+
380
+ root
381
+ .querySelector("[data-blume-export-pdf]")
382
+ ?.addEventListener("click", () => window.print());
383
+
384
+ const epubButton = root.querySelector("[data-blume-export-epub]");
385
+ const epubLabel = root.querySelector("[data-blume-epub-label]");
386
+ epubButton?.addEventListener("click", async () => {
387
+ const article = document.querySelector("#blume-content article");
388
+ if (!article) {
389
+ return;
390
+ }
391
+ const heading = document.querySelector("#blume-content h1");
392
+ const title = heading?.textContent?.trim() || document.title;
393
+ const reset = epubLabel?.textContent ?? "Export to EPUB";
394
+ if (epubLabel) {
395
+ epubLabel.textContent = "Generating…";
396
+ }
397
+ try {
398
+ // Browser bundle: avoids Node built-ins and returns a Blob. Lazy-loaded
399
+ // so it never ships in the main bundle. The browserified UMD nests its
400
+ // callable under `.default`, which Vite's dev (esbuild) and build
401
+ // (rollup) interops expose at slightly different depths — unwrap both.
402
+ const epubModule = await import("epub-gen-memory/bundle");
403
+ const epub = epubModule.default?.default ?? epubModule.default;
404
+ // The article HTML already opens with the page's <h1>, so don't let the
405
+ // generator prepend a second chapter-title heading.
406
+ const result = await epub(
407
+ { css: EPUB_STYLES, prependChapterTitles: false, title },
408
+ [{ content: buildEpubChapterHtml(article), title }]
409
+ );
410
+ const blob =
411
+ result instanceof Blob
412
+ ? result
413
+ : new Blob([result], { type: "application/epub+zip" });
414
+ const slug =
415
+ md
416
+ .replace(/\.md$/u, "")
417
+ .replace(/^\//u, "")
418
+ .replace(/\//gu, "-") || "index";
419
+ const url = URL.createObjectURL(blob);
420
+ const link = document.createElement("a");
421
+ link.download = `${slug}.epub`;
422
+ link.href = url;
423
+ document.body.append(link);
424
+ link.click();
425
+ link.remove();
426
+ // Revoke later: revoking synchronously after click can cancel the
427
+ // download before the browser has finished reading the blob.
428
+ setTimeout(() => URL.revokeObjectURL(url), 10_000);
429
+ } catch (error) {
430
+ console.error("[blume] EPUB export failed", error);
431
+ } finally {
432
+ if (epubLabel) {
433
+ epubLabel.textContent = reset;
434
+ }
435
+ }
436
+ });
437
+ }
438
+ </script>
@@ -0,0 +1,58 @@
1
+ ---
2
+ import { EN_UI } from "../../core/i18n-ui.ts";
3
+ import type { UIStrings } from "../../core/i18n-ui.ts";
4
+ import Icon from "../Icon.astro";
5
+
6
+ interface Props {
7
+ /** Localized feedback labels. */
8
+ strings?: UIStrings["feedback"];
9
+ }
10
+
11
+ const { strings } = Astro.props;
12
+ const f = strings ?? EN_UI.feedback;
13
+
14
+ const buttonClass =
15
+ "inline-flex items-center gap-2 rounded-full border border-border px-4 py-2 text-foreground text-sm transition-colors hover:border-foreground";
16
+ ---
17
+
18
+ <section
19
+ aria-label={f.question}
20
+ class="mx-auto mt-12 flex max-w-[42rem] items-center justify-between gap-4 border-border border-t pt-6"
21
+ data-blume-page-feedback
22
+ >
23
+ <p class="text-muted-foreground text-sm">{f.question}</p>
24
+ <div class="flex items-center gap-2" data-feedback-actions>
25
+ <button class={buttonClass} data-feedback-value="yes" type="button">
26
+ <Icon name="thumbs-up" size={16} />
27
+ {f.yes}
28
+ </button>
29
+ <button class={buttonClass} data-feedback-value="no" type="button">
30
+ <Icon name="thumbs-down" size={16} />
31
+ {f.no}
32
+ </button>
33
+ </div>
34
+ <p class="hidden text-muted-foreground text-sm" data-feedback-thanks>
35
+ {f.thanks}
36
+ </p>
37
+ </section>
38
+
39
+ <script>
40
+ import { track } from "./analytics-client.ts";
41
+
42
+ const root = document.querySelector("[data-blume-page-feedback]");
43
+ if (root) {
44
+ const actions = root.querySelector("[data-feedback-actions]");
45
+ const thanks = root.querySelector("[data-feedback-thanks]");
46
+ for (const button of root.querySelectorAll("[data-feedback-value]")) {
47
+ button.addEventListener("click", () => {
48
+ track("feedback", {
49
+ helpful: button.getAttribute("data-feedback-value") ?? "",
50
+ path: location.pathname,
51
+ title: document.title,
52
+ });
53
+ actions?.classList.add("hidden");
54
+ thanks?.classList.remove("hidden");
55
+ });
56
+ }
57
+ }
58
+ </script>
@@ -0,0 +1,56 @@
1
+ ---
2
+ import type { UIStrings } from "../../core/i18n-ui.ts";
3
+ import type { FlatPage } from "./nav-utils.ts";
4
+ import Icon from "../Icon.astro";
5
+
6
+ interface Props {
7
+ /** Previous page in reading order, or `null` at the start. */
8
+ prev: FlatPage | null;
9
+ /** Next page in reading order, or `null` at the end. */
10
+ next: FlatPage | null;
11
+ /** Localized page-level strings (`previous`, `next`). */
12
+ strings: UIStrings["page"];
13
+ }
14
+
15
+ const { prev, next, strings } = Astro.props;
16
+ ---
17
+
18
+ {
19
+ (prev || next) && (
20
+ <nav
21
+ aria-label="Pagination"
22
+ class="mx-auto mt-12 flex max-w-[42rem] justify-between gap-4 border-border border-t pt-6 max-md:flex-col"
23
+ >
24
+ {prev ? (
25
+ <a
26
+ class="flex max-w-[48%] flex-1 items-center gap-2 rounded-full border border-border px-4 py-3 text-foreground transition-colors hover:border-foreground max-md:max-w-full"
27
+ href={prev.route}
28
+ >
29
+ <Icon class="rtl:-scale-x-100" name="arrow-left" size={16} />
30
+ <span>
31
+ <span class="block text-muted-foreground text-xs">
32
+ {strings.previous}
33
+ </span>
34
+ <span class="block font-medium">{prev.label}</span>
35
+ </span>
36
+ </a>
37
+ ) : (
38
+ <span />
39
+ )}
40
+ {next && (
41
+ <a
42
+ class="ms-auto flex max-w-[48%] flex-1 items-center justify-end gap-2 rounded-full border border-border px-4 py-3 text-end text-foreground transition-colors hover:border-foreground max-md:max-w-full"
43
+ href={next.route}
44
+ >
45
+ <span>
46
+ <span class="block text-muted-foreground text-xs">
47
+ {strings.next}
48
+ </span>
49
+ <span class="block font-medium">{next.label}</span>
50
+ </span>
51
+ <Icon class="rtl:-scale-x-100" name="arrow-right" size={16} />
52
+ </a>
53
+ )}
54
+ </nav>
55
+ )
56
+ }
@@ -0,0 +1,102 @@
1
+ ---
2
+ import "blume:theme";
3
+ import type { Navigation } from "../../core/types.ts";
4
+ import Analytics from "./Analytics.astro";
5
+ import Banner from "./Banner.astro";
6
+ import Favicon from "./Favicon.astro";
7
+ import Fonts from "./Fonts.astro";
8
+ import Header from "./Header.astro";
9
+
10
+ // A minimal shell for the Scalar API/AsyncAPI reference: Blume's banner + navbar
11
+ // on top, then a full-height region the reference mounts into. Unlike RootLayout
12
+ // it has no docs sidebar/TOC/prose column, so the reference renders edge to edge.
13
+ interface Props {
14
+ site: { title: string; description?: string };
15
+ logo?: {
16
+ svg?: string;
17
+ light?: string;
18
+ dark?: string;
19
+ alt: string;
20
+ href: string;
21
+ } | null;
22
+ favicon?: {
23
+ href: string;
24
+ type?: string;
25
+ } | null;
26
+ banner?: {
27
+ content: string;
28
+ link?: { text: string; href: string };
29
+ dismissible: boolean;
30
+ key: string;
31
+ } | null;
32
+ analytics?: {
33
+ posthog?: { host?: string; key: string };
34
+ scripts?: {
35
+ attributes?: Record<string, string>;
36
+ content?: string;
37
+ src?: string;
38
+ strategy?: "async" | "defer";
39
+ }[];
40
+ vercel?: boolean;
41
+ } | null;
42
+ navigation: Navigation;
43
+ route: string;
44
+ themeMode: "system" | "light" | "dark";
45
+ fontCssVars?: string[];
46
+ searchEnabled: boolean;
47
+ pageTitle: string;
48
+ }
49
+
50
+ const {
51
+ site,
52
+ logo,
53
+ favicon,
54
+ banner,
55
+ analytics,
56
+ navigation,
57
+ route,
58
+ themeMode,
59
+ fontCssVars,
60
+ searchEnabled,
61
+ pageTitle,
62
+ } = Astro.props;
63
+
64
+ // Set the theme before paint so the navbar never flashes the wrong colors
65
+ // (mirrors RootLayout's pre-paint script).
66
+ const initialThemeScript = `(()=>{const m=${JSON.stringify(themeMode)};const s=localStorage.getItem("blume-theme");const sys=matchMedia("(prefers-color-scheme: dark)").matches?"dark":"light";document.documentElement.dataset.theme=s??(m==="system"?sys:m);})();`;
67
+
68
+ // Pre-paint: hide a previously-dismissed banner before it flashes in.
69
+ const bannerScript = banner?.dismissible
70
+ ? `(()=>{if(localStorage.getItem("blume-banner:"+${JSON.stringify(banner.key)}))document.documentElement.setAttribute("data-blume-banner-hidden","");})();`
71
+ : null;
72
+ ---
73
+
74
+ <!doctype html>
75
+ <html lang="en">
76
+ <head>
77
+ <meta charset="utf-8" />
78
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
79
+ <title>{pageTitle}</title>
80
+ <Favicon favicon={favicon} />
81
+ <Fonts cssVars={fontCssVars ?? []} />
82
+ {bannerScript && <script is:inline set:html={bannerScript} />}
83
+ <script is:inline set:html={initialThemeScript} />
84
+ <Analytics analytics={analytics} />
85
+ </head>
86
+ <body class="bg-background font-sans text-foreground antialiased">
87
+ <Banner banner={banner} />
88
+ <Header
89
+ hasSidebar={false}
90
+ logo={logo}
91
+ navigation={navigation}
92
+ route={route}
93
+ searchEnabled={searchEnabled}
94
+ site={site}
95
+ />
96
+ <div
97
+ class="h-[calc(100dvh-4rem)] overflow-auto border-border border-t [&>[data-scalar-client]]:h-full"
98
+ >
99
+ <slot />
100
+ </div>
101
+ </body>
102
+ </html>