blume 0.0.0 → 0.1.1

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 (330) hide show
  1. package/bin/blume.mjs +18 -0
  2. package/dist/cli/index.js +12696 -0
  3. package/dist/cli/index.js.map +145 -0
  4. package/dist/types/core/bridge.d.ts +24 -0
  5. package/dist/types/core/config.d.ts +35 -0
  6. package/dist/types/core/data.d.ts +129 -0
  7. package/dist/types/core/define-components.d.ts +27 -0
  8. package/dist/types/core/define-meta.d.ts +16 -0
  9. package/dist/types/core/deployment-env.d.ts +9 -0
  10. package/dist/types/core/diagnostics.d.ts +17 -0
  11. package/dist/types/core/i18n-ui.d.ts +500 -0
  12. package/dist/types/core/load-module.d.ts +7 -0
  13. package/dist/types/core/package-root.d.ts +17 -0
  14. package/dist/types/core/project.d.ts +9 -0
  15. package/dist/types/core/schema.d.ts +3451 -0
  16. package/dist/types/core/sources/types.d.ts +107 -0
  17. package/dist/types/core/types.d.ts +245 -0
  18. package/dist/types/core/ui-packs/ar.d.ts +3 -0
  19. package/dist/types/core/ui-packs/bg.d.ts +3 -0
  20. package/dist/types/core/ui-packs/bn.d.ts +3 -0
  21. package/dist/types/core/ui-packs/ca.d.ts +3 -0
  22. package/dist/types/core/ui-packs/cs.d.ts +3 -0
  23. package/dist/types/core/ui-packs/da.d.ts +3 -0
  24. package/dist/types/core/ui-packs/de.d.ts +3 -0
  25. package/dist/types/core/ui-packs/el.d.ts +3 -0
  26. package/dist/types/core/ui-packs/es.d.ts +3 -0
  27. package/dist/types/core/ui-packs/fa.d.ts +3 -0
  28. package/dist/types/core/ui-packs/fi.d.ts +3 -0
  29. package/dist/types/core/ui-packs/fr.d.ts +3 -0
  30. package/dist/types/core/ui-packs/he.d.ts +3 -0
  31. package/dist/types/core/ui-packs/hi.d.ts +3 -0
  32. package/dist/types/core/ui-packs/hr.d.ts +3 -0
  33. package/dist/types/core/ui-packs/hu.d.ts +3 -0
  34. package/dist/types/core/ui-packs/id.d.ts +3 -0
  35. package/dist/types/core/ui-packs/index.d.ts +13 -0
  36. package/dist/types/core/ui-packs/it.d.ts +3 -0
  37. package/dist/types/core/ui-packs/ja.d.ts +3 -0
  38. package/dist/types/core/ui-packs/ko.d.ts +3 -0
  39. package/dist/types/core/ui-packs/nl.d.ts +3 -0
  40. package/dist/types/core/ui-packs/no.d.ts +3 -0
  41. package/dist/types/core/ui-packs/pl.d.ts +3 -0
  42. package/dist/types/core/ui-packs/pt-br.d.ts +3 -0
  43. package/dist/types/core/ui-packs/pt.d.ts +3 -0
  44. package/dist/types/core/ui-packs/ro.d.ts +3 -0
  45. package/dist/types/core/ui-packs/ru.d.ts +3 -0
  46. package/dist/types/core/ui-packs/sk.d.ts +3 -0
  47. package/dist/types/core/ui-packs/sr.d.ts +3 -0
  48. package/dist/types/core/ui-packs/sv.d.ts +3 -0
  49. package/dist/types/core/ui-packs/th.d.ts +3 -0
  50. package/dist/types/core/ui-packs/tr.d.ts +3 -0
  51. package/dist/types/core/ui-packs/uk.d.ts +3 -0
  52. package/dist/types/core/ui-packs/vi.d.ts +3 -0
  53. package/dist/types/core/ui-packs/zh-tw.d.ts +3 -0
  54. package/dist/types/core/ui-packs/zh.d.ts +3 -0
  55. package/dist/types/core/version.d.ts +8 -0
  56. package/dist/types/index.d.ts +10 -0
  57. package/dist/types/migrate/mintlify/config.d.ts +2 -0
  58. package/dist/types/migrate/mintlify/i18n.d.ts +7 -0
  59. package/dist/types/theme/fonts.d.ts +163 -0
  60. package/docs/01-quickstart.mdx +99 -0
  61. package/docs/02-deployment.mdx +129 -0
  62. package/docs/advanced/api-reference.mdx +114 -0
  63. package/docs/advanced/blog.mdx +121 -0
  64. package/docs/advanced/changelog.mdx +113 -0
  65. package/docs/advanced/custom-pages.mdx +268 -0
  66. package/docs/advanced/meta.ts +7 -0
  67. package/docs/changelog/v0-1-0.mdx +12 -0
  68. package/docs/changelog/v0-2-0.mdx +16 -0
  69. package/docs/configuration/ai.mdx +228 -0
  70. package/docs/configuration/analytics.mdx +98 -0
  71. package/docs/configuration/customization.mdx +91 -0
  72. package/docs/configuration/export.mdx +70 -0
  73. package/docs/configuration/index.mdx +290 -0
  74. package/docs/configuration/meta.ts +15 -0
  75. package/docs/configuration/search.mdx +172 -0
  76. package/docs/configuration/seo.mdx +196 -0
  77. package/docs/configuration/theming.mdx +178 -0
  78. package/docs/content/components.mdx +651 -0
  79. package/docs/content/i18n.mdx +205 -0
  80. package/docs/content/index.mdx +161 -0
  81. package/docs/content/islands.mdx +94 -0
  82. package/docs/content/meta.mdx +119 -0
  83. package/docs/content/meta.ts +15 -0
  84. package/docs/content/navigation.mdx +168 -0
  85. package/docs/content/sources.mdx +216 -0
  86. package/docs/content/syntax.mdx +445 -0
  87. package/docs/index.mdx +112 -0
  88. package/docs/reference/cli.mdx +43 -0
  89. package/docs/reference/frontmatter.mdx +74 -0
  90. package/docs/reference/meta.ts +7 -0
  91. package/package.json +150 -6
  92. package/src/ai/ask.ts +93 -0
  93. package/src/ai/llms.ts +64 -0
  94. package/src/ai/markdown.ts +31 -0
  95. package/src/ai/mcp/data.ts +74 -0
  96. package/src/ai/mcp/discovery.ts +49 -0
  97. package/src/ai/mcp/server.ts +225 -0
  98. package/src/ai/mcp/tools.ts +47 -0
  99. package/src/assets/icon.png +0 -0
  100. package/src/astro/examples.ts +104 -0
  101. package/src/astro/generate.ts +1006 -0
  102. package/src/astro/index.ts +4 -0
  103. package/src/astro/integration.ts +74 -0
  104. package/src/astro/islands.ts +131 -0
  105. package/src/astro/markdown-negotiation.ts +68 -0
  106. package/src/astro/pages.ts +87 -0
  107. package/src/astro/templates.ts +1326 -0
  108. package/src/cli/commands/add.ts +81 -0
  109. package/src/cli/commands/build.ts +103 -0
  110. package/src/cli/commands/dev.ts +108 -0
  111. package/src/cli/commands/doctor.ts +74 -0
  112. package/src/cli/commands/eject.ts +57 -0
  113. package/src/cli/commands/init.ts +98 -0
  114. package/src/cli/commands/migrate.ts +39 -0
  115. package/src/cli/commands/preview.ts +39 -0
  116. package/src/cli/commands/sync.ts +52 -0
  117. package/src/cli/commands/validate.ts +61 -0
  118. package/src/cli/index.ts +35 -0
  119. package/src/cli/log.ts +37 -0
  120. package/src/cli/prepare.ts +80 -0
  121. package/src/components/Icon.astro +99 -0
  122. package/src/components/content/Accordion.astro +8 -0
  123. package/src/components/content/AccordionItem.astro +121 -0
  124. package/src/components/content/AutoTypeTable.astro +51 -0
  125. package/src/components/content/Badge.astro +124 -0
  126. package/src/components/content/Callout.astro +73 -0
  127. package/src/components/content/Card.astro +104 -0
  128. package/src/components/content/CardGroup.astro +14 -0
  129. package/src/components/content/CodeBlock.astro +28 -0
  130. package/src/components/content/CodeGroup.astro +13 -0
  131. package/src/components/content/Color.astro +15 -0
  132. package/src/components/content/ColorItem.astro +87 -0
  133. package/src/components/content/ColorRow.astro +10 -0
  134. package/src/components/content/Column.astro +6 -0
  135. package/src/components/content/Columns.astro +9 -0
  136. package/src/components/content/Component.astro +65 -0
  137. package/src/components/content/Diff.astro +44 -0
  138. package/src/components/content/Expandable.astro +11 -0
  139. package/src/components/content/FileTree.astro +8 -0
  140. package/src/components/content/Frame.astro +70 -0
  141. package/src/components/content/GithubInfo.astro +110 -0
  142. package/src/components/content/Math.astro +24 -0
  143. package/src/components/content/Panel.astro +20 -0
  144. package/src/components/content/Prompt.astro +129 -0
  145. package/src/components/content/Step.astro +34 -0
  146. package/src/components/content/Steps.astro +20 -0
  147. package/src/components/content/Tab.astro +46 -0
  148. package/src/components/content/Tabs.astro +273 -0
  149. package/src/components/content/Tile.astro +42 -0
  150. package/src/components/content/Tooltip.astro +68 -0
  151. package/src/components/content/Tree.astro +300 -0
  152. package/src/components/content/TreeFile.astro +15 -0
  153. package/src/components/content/TreeFolder.astro +62 -0
  154. package/src/components/content/TypeTable.astro +106 -0
  155. package/src/components/content/Update.astro +66 -0
  156. package/src/components/content/Visibility.astro +12 -0
  157. package/src/components/content/Warning.astro +9 -0
  158. package/src/components/content/auto-type-table.ts +141 -0
  159. package/src/components/content/diff.ts +95 -0
  160. package/src/components/content/github-info.ts +79 -0
  161. package/src/components/content/mermaid-element.ts +68 -0
  162. package/src/components/github-mark.ts +9 -0
  163. package/src/components/index.ts +14 -0
  164. package/src/components/islands/AskAI.astro +12 -0
  165. package/src/components/islands/ask-ai.tsx +156 -0
  166. package/src/components/layout/Analytics.astro +63 -0
  167. package/src/components/layout/Banner.astro +50 -0
  168. package/src/components/layout/Breadcrumbs.astro +31 -0
  169. package/src/components/layout/Favicon.astro +22 -0
  170. package/src/components/layout/Fonts.astro +14 -0
  171. package/src/components/layout/Header.astro +188 -0
  172. package/src/components/layout/LanguageSwitcher.astro +56 -0
  173. package/src/components/layout/NavTree.astro +462 -0
  174. package/src/components/layout/PageActions.astro +438 -0
  175. package/src/components/layout/PageFeedback.astro +58 -0
  176. package/src/components/layout/PageLayout.astro +173 -0
  177. package/src/components/layout/Pagination.astro +56 -0
  178. package/src/components/layout/ReferenceLayout.astro +107 -0
  179. package/src/components/layout/RootLayout.astro +537 -0
  180. package/src/components/layout/Search.astro +608 -0
  181. package/src/components/layout/TableOfContents.astro +68 -0
  182. package/src/components/layout/analytics-client.ts +38 -0
  183. package/src/components/layout/head-scripts.ts +19 -0
  184. package/src/components/layout/nav-utils.ts +87 -0
  185. package/src/components/layout/overrides.ts +32 -0
  186. package/src/components/layout/search/algolia.ts +43 -0
  187. package/src/components/layout/search/endpoint.ts +22 -0
  188. package/src/components/layout/search/flexsearch.ts +52 -0
  189. package/src/components/layout/search/orama-cloud.ts +41 -0
  190. package/src/components/layout/search/orama.ts +26 -0
  191. package/src/components/layout/search/pagefind.ts +43 -0
  192. package/src/components/layout/search/types.ts +163 -0
  193. package/src/components/layout/search/typesense.ts +60 -0
  194. package/src/components/layout/toc-element.ts +108 -0
  195. package/src/core/bridge.ts +92 -0
  196. package/src/core/config.ts +112 -0
  197. package/src/core/content.ts +50 -0
  198. package/src/core/data.ts +128 -0
  199. package/src/core/define-components.ts +34 -0
  200. package/src/core/define-meta.ts +20 -0
  201. package/src/core/deployment-env.ts +73 -0
  202. package/src/core/diagnostics.ts +104 -0
  203. package/src/core/frontmatter.ts +43 -0
  204. package/src/core/graph.ts +128 -0
  205. package/src/core/i18n-ui.ts +171 -0
  206. package/src/core/i18n.ts +169 -0
  207. package/src/core/last-modified.ts +88 -0
  208. package/src/core/links.ts +352 -0
  209. package/src/core/load-module.ts +15 -0
  210. package/src/core/manifest.ts +126 -0
  211. package/src/core/meta.ts +97 -0
  212. package/src/core/navigation.ts +392 -0
  213. package/src/core/package-root.ts +37 -0
  214. package/src/core/project-graph.ts +153 -0
  215. package/src/core/project.ts +56 -0
  216. package/src/core/schema.ts +1057 -0
  217. package/src/core/server-features.ts +23 -0
  218. package/src/core/sources/assets.ts +77 -0
  219. package/src/core/sources/cache.ts +122 -0
  220. package/src/core/sources/filesystem.ts +99 -0
  221. package/src/core/sources/mdx-remote.ts +215 -0
  222. package/src/core/sources/mintlify.ts +161 -0
  223. package/src/core/sources/normalize.ts +243 -0
  224. package/src/core/sources/notion.ts +440 -0
  225. package/src/core/sources/portable-text.ts +143 -0
  226. package/src/core/sources/read.ts +36 -0
  227. package/src/core/sources/resolve.ts +158 -0
  228. package/src/core/sources/sanity.ts +218 -0
  229. package/src/core/sources/types.ts +105 -0
  230. package/src/core/tsconfig-aliases.ts +201 -0
  231. package/src/core/types.ts +261 -0
  232. package/src/core/ui-packs/ar.ts +47 -0
  233. package/src/core/ui-packs/bg.ts +47 -0
  234. package/src/core/ui-packs/bn.ts +47 -0
  235. package/src/core/ui-packs/ca.ts +47 -0
  236. package/src/core/ui-packs/cs.ts +47 -0
  237. package/src/core/ui-packs/da.ts +47 -0
  238. package/src/core/ui-packs/de.ts +47 -0
  239. package/src/core/ui-packs/el.ts +47 -0
  240. package/src/core/ui-packs/es.ts +47 -0
  241. package/src/core/ui-packs/fa.ts +47 -0
  242. package/src/core/ui-packs/fi.ts +47 -0
  243. package/src/core/ui-packs/fr.ts +47 -0
  244. package/src/core/ui-packs/he.ts +47 -0
  245. package/src/core/ui-packs/hi.ts +47 -0
  246. package/src/core/ui-packs/hr.ts +47 -0
  247. package/src/core/ui-packs/hu.ts +47 -0
  248. package/src/core/ui-packs/id.ts +47 -0
  249. package/src/core/ui-packs/index.ts +87 -0
  250. package/src/core/ui-packs/it.ts +47 -0
  251. package/src/core/ui-packs/ja.ts +47 -0
  252. package/src/core/ui-packs/ko.ts +47 -0
  253. package/src/core/ui-packs/nl.ts +47 -0
  254. package/src/core/ui-packs/no.ts +47 -0
  255. package/src/core/ui-packs/pl.ts +47 -0
  256. package/src/core/ui-packs/pt-br.ts +47 -0
  257. package/src/core/ui-packs/pt.ts +47 -0
  258. package/src/core/ui-packs/ro.ts +47 -0
  259. package/src/core/ui-packs/ru.ts +47 -0
  260. package/src/core/ui-packs/sk.ts +47 -0
  261. package/src/core/ui-packs/sr.ts +47 -0
  262. package/src/core/ui-packs/sv.ts +47 -0
  263. package/src/core/ui-packs/th.ts +47 -0
  264. package/src/core/ui-packs/tr.ts +47 -0
  265. package/src/core/ui-packs/uk.ts +47 -0
  266. package/src/core/ui-packs/vi.ts +47 -0
  267. package/src/core/ui-packs/zh-tw.ts +47 -0
  268. package/src/core/ui-packs/zh.ts +47 -0
  269. package/src/core/version.ts +23 -0
  270. package/src/deploy/robots.ts +20 -0
  271. package/src/deploy/rss.ts +128 -0
  272. package/src/deploy/sitemap.ts +28 -0
  273. package/src/index.ts +39 -0
  274. package/src/markdown/code-title.ts +71 -0
  275. package/src/markdown/directives.ts +83 -0
  276. package/src/markdown/heading-anchors.ts +137 -0
  277. package/src/markdown/index.ts +228 -0
  278. package/src/markdown/inline-code.ts +108 -0
  279. package/src/markdown/language-icon.ts +172 -0
  280. package/src/markdown/math.ts +32 -0
  281. package/src/markdown/mdast.ts +48 -0
  282. package/src/markdown/mermaid.ts +37 -0
  283. package/src/markdown/package-commands.ts +159 -0
  284. package/src/markdown/package-install.ts +40 -0
  285. package/src/migrate/fumadocs/config.ts +155 -0
  286. package/src/migrate/fumadocs/content.ts +365 -0
  287. package/src/migrate/fumadocs/frontmatter.ts +18 -0
  288. package/src/migrate/fumadocs/groups.ts +230 -0
  289. package/src/migrate/fumadocs/index.ts +337 -0
  290. package/src/migrate/fumadocs/meta.ts +244 -0
  291. package/src/migrate/migrate.ts +53 -0
  292. package/src/migrate/mintlify/config.ts +1040 -0
  293. package/src/migrate/mintlify/content.ts +98 -0
  294. package/src/migrate/mintlify/frontmatter.ts +126 -0
  295. package/src/migrate/mintlify/i18n.ts +51 -0
  296. package/src/migrate/mintlify/icons.ts +128 -0
  297. package/src/migrate/mintlify/index.ts +266 -0
  298. package/src/migrate/mintlify/snippets.ts +306 -0
  299. package/src/migrate/mintlify/transform.ts +80 -0
  300. package/src/migrate/nextra/content.ts +46 -0
  301. package/src/migrate/nextra/frontmatter.ts +40 -0
  302. package/src/migrate/nextra/index.ts +374 -0
  303. package/src/migrate/nextra/meta.ts +266 -0
  304. package/src/migrate/shared.ts +720 -0
  305. package/src/migrate/starlight/config.ts +459 -0
  306. package/src/migrate/starlight/content.ts +78 -0
  307. package/src/migrate/starlight/frontmatter.ts +111 -0
  308. package/src/migrate/starlight/i18n.ts +54 -0
  309. package/src/migrate/starlight/index.ts +131 -0
  310. package/src/og/card.ts +92 -0
  311. package/src/og/index.ts +2 -0
  312. package/src/openapi/scalar.ts +246 -0
  313. package/src/registry/eject.ts +310 -0
  314. package/src/registry/registry.ts +100 -0
  315. package/src/registry/rewrite-imports.ts +39 -0
  316. package/src/runtime/index.ts +14 -0
  317. package/src/search/build.ts +23 -0
  318. package/src/search/documents.ts +164 -0
  319. package/src/search/orama-index.ts +66 -0
  320. package/src/search/providers.ts +91 -0
  321. package/src/search/sync/algolia.ts +30 -0
  322. package/src/search/sync/index.ts +50 -0
  323. package/src/search/sync/orama-cloud.ts +40 -0
  324. package/src/search/sync/typesense.ts +65 -0
  325. package/src/seo/jsonld.ts +113 -0
  326. package/src/theme/entry.ts +637 -0
  327. package/src/theme/fonts.ts +198 -0
  328. package/src/theme/icons.ts +184 -0
  329. package/src/theme/palette.ts +143 -0
  330. 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,173 @@
1
+ ---
2
+ // A full-width page layout for landing pages, marketing pages, dashboards — any
3
+ // page that wants Blume's chrome (document shell, header, theme, fonts) without
4
+ // the docs sidebar + prose + TOC grid that RootLayout hard-codes. It renders the
5
+ // `<head>`, theme/banner pre-paint scripts, banner, and header, then a single
6
+ // full-width `<slot />` for the body (plus an optional `footer` slot rendered
7
+ // after `<main>`). Import it into a custom page and pull props from `blume:data`:
8
+ //
9
+ // import PageLayout from "blume/components/layout/PageLayout.astro";
10
+ // import data from "blume:data";
11
+ // ---
12
+ // <PageLayout site={{ title: data.config.title }} logo={data.config.logo}
13
+ // navigation={data.navigation} themeMode={data.config.theme.mode}
14
+ // fontCssVars={data.fontCssVars} favicon={data.config.favicon}
15
+ // searchEnabled={data.config.search.enabled} page={{ title: "Home" }}>
16
+ // <!-- your full-width content -->
17
+ // </PageLayout>
18
+ import type {
19
+ BlumeBanner,
20
+ BlumeDataConfig,
21
+ BlumeFavicon,
22
+ BlumeLogo,
23
+ } from "../../core/data.ts";
24
+ import { EN_UI } from "../../core/i18n-ui.ts";
25
+ import type { UIStrings } from "../../core/i18n-ui.ts";
26
+ import type { LocaleSwitchOption, Navigation } from "../../core/types.ts";
27
+ import "blume:theme";
28
+ import Analytics from "./Analytics.astro";
29
+ import Banner from "./Banner.astro";
30
+ import Favicon from "./Favicon.astro";
31
+ import Fonts from "./Fonts.astro";
32
+ import { bannerInitScript, themeInitScript } from "./head-scripts.ts";
33
+ import Header from "./Header.astro";
34
+
35
+ interface Props {
36
+ site: { title: string; description?: string };
37
+ logo?: BlumeLogo | null;
38
+ favicon?: BlumeFavicon | null;
39
+ appleIcon?: BlumeFavicon | null;
40
+ banner?: BlumeBanner | null;
41
+ analytics?: BlumeDataConfig["analytics"];
42
+ navigation: Navigation;
43
+ /**
44
+ * Title/description/route for `<title>`, `og:*`, and the header active link.
45
+ * Unlike RootLayout, `title` is used verbatim (no `- siteTitle` suffix), since
46
+ * marketing pages usually set their own complete document title.
47
+ */
48
+ page?: { title?: string; description?: string; route?: string };
49
+ themeMode: "system" | "light" | "dark";
50
+ fontCssVars?: string[];
51
+ searchEnabled: boolean;
52
+ askEnabled?: boolean;
53
+ /**
54
+ * Absolute site URL (`data.config.site`). When set, `canonical` and the
55
+ * generated `og:image` for this route are derived from it automatically;
56
+ * `canonical`/`ogImage` below still override.
57
+ */
58
+ siteUrl?: string | null;
59
+ /** Whether OG image generation is on (`data.config.og.enabled`). */
60
+ ogEnabled?: boolean;
61
+ /** SEO overrides; a marketing page often sets its own canonical/og image. */
62
+ ogImage?: string | null;
63
+ canonical?: string | null;
64
+ noindex?: boolean;
65
+ /** Active locale + direction for `<html lang>`/`<html dir>`. */
66
+ locale?: string;
67
+ dir?: "ltr" | "rtl";
68
+ /** Resolved UI dictionary; English baseline when omitted. */
69
+ ui?: UIStrings;
70
+ /** Language-switcher entries for the header. */
71
+ localeSwitch?: LocaleSwitchOption[];
72
+ }
73
+
74
+ const {
75
+ site,
76
+ logo,
77
+ favicon,
78
+ appleIcon,
79
+ banner,
80
+ analytics,
81
+ navigation,
82
+ page,
83
+ themeMode,
84
+ fontCssVars,
85
+ searchEnabled,
86
+ askEnabled,
87
+ siteUrl,
88
+ ogEnabled,
89
+ ogImage,
90
+ canonical,
91
+ noindex,
92
+ locale = "en",
93
+ dir = "ltr",
94
+ ui,
95
+ localeSwitch,
96
+ } = Astro.props;
97
+
98
+ const strings = ui ?? EN_UI;
99
+ // Filter search to the active language only when the site is multi-locale.
100
+ const searchLocale =
101
+ localeSwitch && localeSwitch.length > 1 ? locale : undefined;
102
+ const pageTitle = page?.title ?? site.title;
103
+ const description = page?.description ?? site.description;
104
+ const route = page?.route ?? "/";
105
+
106
+ // Derive canonical + og:image from the site URL the same way the catch-all does
107
+ // for content pages, so a custom page gets both for free. The matching OG card
108
+ // (`/og/<slug>.png`, `index` for the home) is generated for static custom pages
109
+ // when `og.enabled`. Explicit props still win.
110
+ const ogSlug = route === "/" ? "index" : route.slice(1);
111
+ const resolvedCanonical =
112
+ canonical ?? (siteUrl ? `${siteUrl}${route === "/" ? "" : route}` : null);
113
+ const resolvedOgImage =
114
+ ogImage ?? (ogEnabled && siteUrl ? `${siteUrl}/og/${ogSlug}.png` : null);
115
+
116
+ const initialThemeScript = themeInitScript(themeMode);
117
+ const bannerScript = banner?.dismissible
118
+ ? bannerInitScript(banner.key)
119
+ : null;
120
+ ---
121
+
122
+ <!doctype html>
123
+ <html dir={dir} lang={locale}>
124
+ <head>
125
+ <meta charset="utf-8" />
126
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
127
+ <title>{pageTitle}</title>
128
+ <Favicon appleIcon={appleIcon} favicon={favicon} />
129
+ <Fonts cssVars={fontCssVars ?? []} />
130
+ {resolvedCanonical && <link href={resolvedCanonical} rel="canonical" />}
131
+ {noindex && <meta content="noindex" name="robots" />}
132
+ {description && <meta content={description} name="description" />}
133
+ <meta content="website" property="og:type" />
134
+ <meta content={pageTitle} property="og:title" />
135
+ {description && <meta content={description} property="og:description" />}
136
+ {
137
+ resolvedOgImage && (
138
+ <>
139
+ <meta content={resolvedOgImage} property="og:image" />
140
+ <meta content={resolvedOgImage} name="twitter:image" />
141
+ <meta content="summary_large_image" name="twitter:card" />
142
+ </>
143
+ )
144
+ }
145
+ {bannerScript && <script is:inline set:html={bannerScript} />}
146
+ <script is:inline set:html={initialThemeScript} />
147
+ <Analytics analytics={analytics} />
148
+ </head>
149
+ <body class="bg-background font-sans text-foreground antialiased">
150
+ <a
151
+ class="absolute start-[-999px] top-0 z-[100] bg-accent px-4 py-2 text-accent-foreground focus:start-0"
152
+ href="#blume-content">{strings.page.skipToContent}</a
153
+ >
154
+ <Banner banner={banner} />
155
+ <Header
156
+ askEnabled={askEnabled}
157
+ hasSidebar={false}
158
+ localeSwitch={localeSwitch}
159
+ logo={logo}
160
+ navigation={navigation}
161
+ route={route}
162
+ searchEnabled={searchEnabled}
163
+ searchLocale={searchLocale}
164
+ searchStrings={strings.search}
165
+ site={site}
166
+ switcherStrings={strings.languageSwitcher}
167
+ >
168
+ <slot name="ask" slot="ask" />
169
+ </Header>
170
+ <main id="blume-content"><slot /></main>
171
+ <slot name="footer" />
172
+ </body>
173
+ </html>