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,608 @@
1
+ ---
2
+ import { EN_UI } from "../../core/i18n-ui.ts";
3
+ import type { UIStrings } from "../../core/i18n-ui.ts";
4
+ import type { Navigation } from "../../core/types.ts";
5
+ import Icon from "../Icon.astro";
6
+ import { flattenPages } from "./nav-utils.ts";
7
+
8
+ // The provider-specific loader is generated per project into `blume:search-client`
9
+ // (an alias to `.blume/src/generated/search-client.ts`), so this component is
10
+ // provider-agnostic: it lazy-imports that module on first open and uses the
11
+ // `SearchFn` it returns. Only the configured provider's SDK is bundled.
12
+
13
+ interface Props {
14
+ askEnabled?: boolean;
15
+ navigation?: Navigation;
16
+ strings?: UIStrings["search"];
17
+ /** Active locale to filter results to; omitted disables locale filtering. */
18
+ locale?: string;
19
+ }
20
+
21
+ const { askEnabled = false, navigation, strings, locale } = Astro.props;
22
+ const s = strings ?? EN_UI.search;
23
+
24
+ // Pages shown in the empty state, before the user has typed anything.
25
+ const popular = navigation ? flattenPages(navigation.sidebar).slice(0, 6) : [];
26
+
27
+ const kbd = "rounded border border-border bg-muted px-1 py-0.5 font-mono";
28
+ ---
29
+
30
+ <blume-search
31
+ class="contents"
32
+ data-ask={askEnabled ? "" : undefined}
33
+ data-i18n-dev={s.devOnly}
34
+ data-i18n-empty={s.noResults}
35
+ data-locale={locale || undefined}
36
+ >
37
+ <button
38
+ aria-label={s.button}
39
+ class="inline-flex h-9 min-w-0 cursor-pointer items-center gap-2 rounded-full border border-border bg-background px-3 text-muted-foreground text-sm transition-colors hover:border-foreground hover:text-foreground sm:min-w-48"
40
+ data-blume-search-open
41
+ type="button"
42
+ >
43
+ <Icon name="search" size={16} />
44
+ <span class="min-w-0 flex-1 text-start max-sm:hidden">{s.button}</span>
45
+ <kbd class="font-mono text-[0.7rem] max-sm:hidden" data-blume-search-kbd
46
+ >⌘K</kbd
47
+ >
48
+ </button>
49
+
50
+ <dialog
51
+ aria-label={s.label}
52
+ class="m-auto h-[min(480px,90vh)] w-[min(62.5rem,94vw)] flex-col overflow-hidden rounded-blume border border-border bg-background/80 p-0 text-foreground shadow-2xl outline-none backdrop-blur-xl backdrop:bg-black/30 open:flex"
53
+ data-blume-search-dialog
54
+ >
55
+ <div
56
+ class="flex items-center gap-2.5 border-border border-b px-4 py-3 text-muted-foreground"
57
+ >
58
+ <Icon name="search" size={18} />
59
+ <input
60
+ aria-label={s.label}
61
+ autocomplete="off"
62
+ class="flex-1 border-0 bg-transparent text-foreground text-sm focus:outline-none [&::-webkit-search-cancel-button]:appearance-none"
63
+ data-blume-search-input
64
+ placeholder={s.placeholder}
65
+ type="search"
66
+ />
67
+ <kbd class={`${kbd} text-[0.7rem]`}>Esc</kbd>
68
+ </div>
69
+ <div
70
+ class="grid min-h-0 flex-1 md:grid-cols-[22rem_minmax(0,1fr)]"
71
+ data-blume-search-grid
72
+ >
73
+ <div class="flex min-h-0 flex-col md:border-border md:border-e">
74
+ <div
75
+ class="flex flex-wrap gap-1.5 border-border border-b px-3 py-2"
76
+ data-blume-search-filters
77
+ hidden
78
+ >
79
+ </div>
80
+ <div
81
+ class="min-h-0 flex-1 overflow-y-auto p-2"
82
+ data-blume-search-results
83
+ >
84
+ </div>
85
+ <p
86
+ class="m-0 px-4 py-6 text-center text-muted-foreground text-sm"
87
+ data-blume-search-message
88
+ hidden
89
+ >
90
+ </p>
91
+ </div>
92
+ <div
93
+ class="hidden min-h-0 overflow-y-auto p-5 md:block"
94
+ data-blume-search-preview
95
+ >
96
+ </div>
97
+ </div>
98
+ <div
99
+ class="flex items-center justify-between gap-3 border-border border-t px-3 py-2 text-muted-foreground text-xs"
100
+ >
101
+ {
102
+ locale ? (
103
+ <label class="flex cursor-pointer select-none items-center gap-1.5">
104
+ <input
105
+ class="size-3.5 accent-accent"
106
+ data-blume-search-all-locales
107
+ type="checkbox"
108
+ />
109
+ {s.allLanguages}
110
+ </label>
111
+ ) : (
112
+ <span />
113
+ )
114
+ }
115
+ <div class="flex items-center gap-3">
116
+ <span class="flex items-center gap-1">
117
+ <kbd class={`${kbd} text-[0.65rem]`}>↑</kbd>
118
+ <kbd class={`${kbd} text-[0.65rem]`}>↓</kbd>
119
+ navigate
120
+ </span>
121
+ <span class="flex items-center gap-1">
122
+ <kbd class={`${kbd} text-[0.65rem]`}>↵</kbd>
123
+ open
124
+ </span>
125
+ <span class="flex items-center gap-1 max-md:hidden">
126
+ <kbd class={`${kbd} text-[0.65rem]`}>⌘J</kbd>
127
+ preview
128
+ </span>
129
+ </div>
130
+ </div>
131
+ </dialog>
132
+
133
+ <script
134
+ data-blume-search-popular
135
+ is:inline
136
+ set:html={JSON.stringify(popular)}
137
+ type="application/json"
138
+ />
139
+
140
+ <script>
141
+ import { icons } from "../../theme/icons.ts";
142
+ import {
143
+ escapeHtml,
144
+ highlight,
145
+ matchSnippet,
146
+ } from "./search/types.ts";
147
+ import type { SearchFn, SearchHit } from "./search/types.ts";
148
+
149
+ interface Selectable {
150
+ el: HTMLElement;
151
+ hit?: SearchHit;
152
+ kind: "ask" | "link";
153
+ url?: string;
154
+ }
155
+
156
+ interface PopularPage {
157
+ label: string;
158
+ route: string;
159
+ }
160
+
161
+ const GRID_COLS = "md:grid-cols-[22rem_minmax(0,1fr)]";
162
+ const ROW_CLASS =
163
+ "flex w-full cursor-pointer items-start gap-2.5 rounded-blume border border-transparent px-2.5 py-2 text-start text-inherit transition-colors hover:no-underline";
164
+ // A transparent border sits on every row so selecting one only recolors it
165
+ // (to the theme border) — no 1px layout shift as the cursor moves.
166
+ const ROW_ON = ["bg-muted", "border-border"];
167
+ const ROW_OFF = ["border-transparent"];
168
+ const MARK =
169
+ "[&_mark]:rounded-sm [&_mark]:bg-accent/25 [&_mark]:text-inherit";
170
+ const PILL_BASE =
171
+ "inline-flex cursor-pointer items-center gap-1.5 rounded-full border px-2.5 py-1 font-medium text-xs transition-colors";
172
+ const PILL_ON = "border-transparent bg-foreground text-background";
173
+ const PILL_OFF = "border-border text-muted-foreground hover:text-foreground";
174
+
175
+ const svg = (name: string, size = 16): string =>
176
+ `<svg xmlns="http://www.w3.org/2000/svg" width="${size}" height="${size}" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">${icons[name] ?? ""}</svg>`;
177
+
178
+ class BlumeSearch extends HTMLElement {
179
+ dialog!: HTMLDialogElement;
180
+ input!: HTMLInputElement;
181
+ grid!: HTMLElement;
182
+ filters!: HTMLElement;
183
+ results!: HTMLElement;
184
+ message!: HTMLElement;
185
+ preview!: HTMLElement;
186
+ searchFn: SearchFn | null = null;
187
+ loaded = false;
188
+ askEnabled = false;
189
+ popular: PopularPage[] = [];
190
+ selectables: Selectable[] = [];
191
+ selectedIndex = -1;
192
+ activeSection: string | null = null;
193
+ previewOn = true;
194
+ devOnlyMsg = "Search is available in the production build.";
195
+ noResultsMsg = "No results found.";
196
+ // The active locale to filter to (null when i18n is off), and whether the
197
+ // reader has opted to search across every language instead.
198
+ locale: string | null = null;
199
+ allLocales = false;
200
+
201
+ connectedCallback() {
202
+ this.devOnlyMsg =
203
+ this.getAttribute("data-i18n-dev") || this.devOnlyMsg;
204
+ this.noResultsMsg =
205
+ this.getAttribute("data-i18n-empty") || this.noResultsMsg;
206
+ this.locale = this.getAttribute("data-locale");
207
+ this.dialog = this.querySelector("[data-blume-search-dialog]")!;
208
+ this.input = this.querySelector("[data-blume-search-input]")!;
209
+ this.grid = this.querySelector("[data-blume-search-grid]")!;
210
+ this.filters = this.querySelector("[data-blume-search-filters]")!;
211
+ this.results = this.querySelector("[data-blume-search-results]")!;
212
+ this.message = this.querySelector("[data-blume-search-message]")!;
213
+ this.preview = this.querySelector("[data-blume-search-preview]")!;
214
+ this.askEnabled = this.hasAttribute("data-ask");
215
+
216
+ const popularScript = this.querySelector(
217
+ "[data-blume-search-popular]"
218
+ );
219
+ try {
220
+ this.popular = JSON.parse(popularScript?.textContent || "[]");
221
+ } catch {
222
+ this.popular = [];
223
+ }
224
+
225
+ this.previewOn =
226
+ localStorage.getItem("blume-search-preview") !== "0";
227
+ this.applyPreviewState();
228
+
229
+ // Per-language filtering: default to the active locale, with an opt-in
230
+ // toggle to search every language. The choice is remembered.
231
+ const allToggle = this.querySelector<HTMLInputElement>(
232
+ "[data-blume-search-all-locales]"
233
+ );
234
+ if (allToggle) {
235
+ this.allLocales =
236
+ localStorage.getItem("blume-search-all-locales") === "1";
237
+ allToggle.checked = this.allLocales;
238
+ allToggle.addEventListener("change", () => {
239
+ this.allLocales = allToggle.checked;
240
+ localStorage.setItem(
241
+ "blume-search-all-locales",
242
+ this.allLocales ? "1" : "0"
243
+ );
244
+ this.render();
245
+ });
246
+ }
247
+
248
+ // The handler accepts ⌘K and Ctrl+K; show the right one per platform.
249
+ const hint = this.querySelector("[data-blume-search-kbd]");
250
+ if (hint) {
251
+ hint.textContent = /mac|iphone|ipad|ipod/iu.test(navigator.platform)
252
+ ? "⌘K"
253
+ : "Ctrl K";
254
+ }
255
+
256
+ this.querySelector("[data-blume-search-open]")?.addEventListener(
257
+ "click",
258
+ () => this.open()
259
+ );
260
+
261
+ document.addEventListener("keydown", (event) => {
262
+ const inField = this.isField(event.target);
263
+ if (
264
+ ((event.key === "k" || event.key === "K") &&
265
+ (event.metaKey || event.ctrlKey)) ||
266
+ (event.key === "/" && !inField)
267
+ ) {
268
+ event.preventDefault();
269
+ this.open();
270
+ }
271
+ });
272
+
273
+ this.input.addEventListener("input", () => this.render());
274
+ this.dialog.addEventListener("keydown", (event) =>
275
+ this.onKeydown(event)
276
+ );
277
+ this.dialog.addEventListener("click", (event) => {
278
+ if (event.target === this.dialog) {
279
+ this.dialog.close();
280
+ }
281
+ });
282
+ }
283
+
284
+ isField(target: EventTarget | null): boolean {
285
+ const el = target as HTMLElement | null;
286
+ return Boolean(
287
+ el &&
288
+ (el.tagName === "INPUT" ||
289
+ el.tagName === "TEXTAREA" ||
290
+ el.isContentEditable)
291
+ );
292
+ }
293
+
294
+ async open() {
295
+ this.dialog.showModal();
296
+ this.input.focus();
297
+ this.input.select();
298
+ if (!this.loaded) {
299
+ this.loaded = true;
300
+ try {
301
+ const { createSearch } = await import("blume:search-client");
302
+ this.searchFn = await createSearch();
303
+ } catch {
304
+ this.searchFn = null;
305
+ }
306
+ }
307
+ this.render();
308
+ }
309
+
310
+ onKeydown(event: KeyboardEvent) {
311
+ if (event.key === "ArrowDown") {
312
+ event.preventDefault();
313
+ this.move(1);
314
+ } else if (event.key === "ArrowUp") {
315
+ event.preventDefault();
316
+ this.move(-1);
317
+ } else if (event.key === "Enter") {
318
+ const item = this.selectables[this.selectedIndex];
319
+ if (item) {
320
+ event.preventDefault();
321
+ this.activate(item);
322
+ }
323
+ } else if (
324
+ (event.key === "j" || event.key === "J") &&
325
+ (event.metaKey || event.ctrlKey)
326
+ ) {
327
+ event.preventDefault();
328
+ this.togglePreview();
329
+ }
330
+ }
331
+
332
+ setMessage(text: string) {
333
+ this.message.textContent = text;
334
+ this.message.hidden = !text;
335
+ }
336
+
337
+ async render() {
338
+ const query = this.input.value.trim();
339
+ this.selectables = [];
340
+ this.selectedIndex = -1;
341
+ this.results.replaceChildren();
342
+ this.setMessage("");
343
+
344
+ if (!query) {
345
+ this.renderFilters([]);
346
+ this.renderEmpty();
347
+ this.finishRender();
348
+ return;
349
+ }
350
+
351
+ if (!this.searchFn) {
352
+ this.renderFilters([]);
353
+ this.clearPreview();
354
+ this.setMessage(this.devOnlyMsg);
355
+ return;
356
+ }
357
+
358
+ const localeFilter =
359
+ this.locale && !this.allLocales ? this.locale : undefined;
360
+ const result = await this.searchFn(query, {
361
+ locale: localeFilter,
362
+ section: this.activeSection ?? undefined,
363
+ });
364
+ // A later keystroke may have superseded this query mid-await.
365
+ if (this.input.value.trim() !== query) {
366
+ return;
367
+ }
368
+
369
+ this.renderFilters(result.sections);
370
+
371
+ if (this.askEnabled) {
372
+ const group = this.addGroup("Ask AI");
373
+ const ask = this.createAskRow(query);
374
+ group.appendChild(ask.el);
375
+ this.selectables.push(ask);
376
+ }
377
+
378
+ if (result.hits.length > 0) {
379
+ const group = this.addGroup("Results");
380
+ for (const hit of result.hits) {
381
+ const item = this.createHitRow(hit, query);
382
+ group.appendChild(item.el);
383
+ this.selectables.push(item);
384
+ }
385
+ } else if (!this.askEnabled) {
386
+ this.setMessage(this.noResultsMsg);
387
+ }
388
+
389
+ this.finishRender();
390
+ }
391
+
392
+ renderEmpty() {
393
+ if (this.askEnabled) {
394
+ const group = this.addGroup("Ask AI");
395
+ const ask = this.createAskRow("");
396
+ group.appendChild(ask.el);
397
+ this.selectables.push(ask);
398
+ }
399
+ if (this.popular.length > 0) {
400
+ const group = this.addGroup("Popular");
401
+ for (const page of this.popular) {
402
+ const item = this.createLinkRow(page.route, page.label);
403
+ group.appendChild(item.el);
404
+ this.selectables.push(item);
405
+ }
406
+ }
407
+ }
408
+
409
+ renderFilters(sections: { count: number; label: string }[]) {
410
+ if (sections.length < 2) {
411
+ this.filters.hidden = true;
412
+ this.filters.replaceChildren();
413
+ return;
414
+ }
415
+ this.filters.hidden = false;
416
+ this.filters.replaceChildren();
417
+ const total = sections.reduce((sum, s) => sum + s.count, 0);
418
+ this.filters.appendChild(this.createPill("All", total, null));
419
+ for (const section of sections) {
420
+ this.filters.appendChild(
421
+ this.createPill(section.label, section.count, section.label)
422
+ );
423
+ }
424
+ }
425
+
426
+ createPill(label: string, count: number, value: string | null) {
427
+ const button = document.createElement("button");
428
+ button.type = "button";
429
+ const active = this.activeSection === value;
430
+ button.className = `${PILL_BASE} ${active ? PILL_ON : PILL_OFF}`;
431
+ button.innerHTML = `${escapeHtml(label)} <span class="opacity-60">${count}</span>`;
432
+ button.addEventListener("click", () => {
433
+ this.activeSection = value;
434
+ this.render();
435
+ this.input.focus();
436
+ });
437
+ return button;
438
+ }
439
+
440
+ addGroup(label: string): HTMLElement {
441
+ const header = document.createElement("p");
442
+ header.className =
443
+ "m-0 px-2.5 pt-3 pb-1 font-normal text-muted-foreground text-xs";
444
+ header.textContent = label;
445
+ const container = document.createElement("div");
446
+ this.results.append(header, container);
447
+ return container;
448
+ }
449
+
450
+ createAskRow(query: string): Selectable {
451
+ const el = document.createElement("button");
452
+ el.type = "button";
453
+ el.className = `${ROW_CLASS} ${MARK}`;
454
+ const title = query
455
+ ? `Ask AI: <span class="text-muted-foreground">“${escapeHtml(query)}”</span>`
456
+ : "Ask AI";
457
+ el.innerHTML = `
458
+ <span class="mt-0.5 shrink-0 text-accent">${svg("sparkles")}</span>
459
+ <span class="min-w-0 flex-1">
460
+ <span class="block truncate font-normal text-foreground text-sm">${title}</span>
461
+ <span class="block truncate text-muted-foreground text-sm">Get an instant answer from AI</span>
462
+ </span>`;
463
+ const item: Selectable = { el, kind: "ask" };
464
+ this.bindRow(item);
465
+ return item;
466
+ }
467
+
468
+ createHitRow(hit: SearchHit, query: string): Selectable {
469
+ const el = document.createElement("a");
470
+ el.href = hit.url;
471
+ el.className = `${ROW_CLASS} ${MARK}`;
472
+ // `line-clamp-2` already sets `display`, so no `block` here (it would
473
+ // override the clamp and let the excerpt run to full height).
474
+ const excerpt = hit.excerpt
475
+ ? `<span class="mt-0.5 line-clamp-2 text-muted-foreground text-xs">${hit.excerpt}</span>`
476
+ : "";
477
+ el.innerHTML = `
478
+ <span class="mt-0.5 shrink-0 text-muted-foreground">${svg("file")}</span>
479
+ <span class="min-w-0 flex-1">
480
+ <span class="block truncate font-normal text-foreground text-sm">${hit.title}</span>
481
+ ${excerpt}
482
+ </span>`;
483
+ const item: Selectable = { el, hit, kind: "link", url: hit.url };
484
+ this.bindRow(item);
485
+ return item;
486
+ }
487
+
488
+ createLinkRow(url: string, label: string): Selectable {
489
+ const el = document.createElement("a");
490
+ el.href = url;
491
+ el.className = ROW_CLASS;
492
+ el.innerHTML = `
493
+ <span class="mt-0.5 shrink-0 text-muted-foreground">${svg("file")}</span>
494
+ <span class="min-w-0 flex-1">
495
+ <span class="block truncate font-normal text-foreground text-sm">${escapeHtml(label)}</span>
496
+ </span>`;
497
+ const item: Selectable = { el, kind: "link", url };
498
+ this.bindRow(item);
499
+ return item;
500
+ }
501
+
502
+ bindRow(item: Selectable) {
503
+ item.el.addEventListener("mouseenter", () => {
504
+ this.selectIndex(this.selectables.indexOf(item));
505
+ });
506
+ if (item.kind === "ask") {
507
+ item.el.addEventListener("click", (event) => {
508
+ event.preventDefault();
509
+ this.activate(item);
510
+ });
511
+ }
512
+ }
513
+
514
+ finishRender() {
515
+ if (this.selectables.length > 0) {
516
+ this.selectIndex(0);
517
+ } else {
518
+ this.clearPreview();
519
+ }
520
+ }
521
+
522
+ selectIndex(index: number) {
523
+ if (index < 0 || index >= this.selectables.length) {
524
+ return;
525
+ }
526
+ const current = this.selectables[this.selectedIndex];
527
+ if (current) {
528
+ current.el.classList.remove(...ROW_ON);
529
+ current.el.classList.add(...ROW_OFF);
530
+ }
531
+ this.selectedIndex = index;
532
+ const next = this.selectables[index];
533
+ next.el.classList.remove(...ROW_OFF);
534
+ next.el.classList.add(...ROW_ON);
535
+ next.el.scrollIntoView({ block: "nearest" });
536
+ this.updatePreview();
537
+ }
538
+
539
+ move(delta: number) {
540
+ if (this.selectables.length === 0) {
541
+ return;
542
+ }
543
+ const max = this.selectables.length - 1;
544
+ const next = Math.min(max, Math.max(0, this.selectedIndex + delta));
545
+ this.selectIndex(next);
546
+ }
547
+
548
+ activate(item: Selectable) {
549
+ if (item.kind === "ask") {
550
+ const query = this.input.value.trim();
551
+ this.dialog.close();
552
+ window.dispatchEvent(
553
+ new CustomEvent("blume:open-ask-ai", { detail: { query } })
554
+ );
555
+ } else if (item.url) {
556
+ window.location.href = item.url;
557
+ }
558
+ }
559
+
560
+ clearPreview() {
561
+ this.preview.replaceChildren();
562
+ }
563
+
564
+ updatePreview() {
565
+ if (!this.previewOn) {
566
+ return;
567
+ }
568
+ const item = this.selectables[this.selectedIndex];
569
+ if (!item || item.kind !== "link" || !item.hit) {
570
+ this.clearPreview();
571
+ return;
572
+ }
573
+ const hit = item.hit;
574
+ const query = this.input.value.trim();
575
+ const body = hit.content
576
+ ? highlight(matchSnippet(hit.content, query, 600), query)
577
+ : (hit.excerpt ?? "");
578
+ this.preview.innerHTML = `
579
+ <h3 class="m-0 mb-3 font-medium text-foreground text-lg ${MARK}">${hit.title}</h3>
580
+ <div class="text-muted-foreground text-sm leading-relaxed ${MARK}">${body}</div>`;
581
+ }
582
+
583
+ togglePreview() {
584
+ this.previewOn = !this.previewOn;
585
+ localStorage.setItem(
586
+ "blume-search-preview",
587
+ this.previewOn ? "1" : "0"
588
+ );
589
+ this.applyPreviewState();
590
+ this.updatePreview();
591
+ }
592
+
593
+ applyPreviewState() {
594
+ if (this.previewOn) {
595
+ this.grid.classList.add(GRID_COLS);
596
+ this.preview.classList.add("md:block");
597
+ } else {
598
+ this.grid.classList.remove(GRID_COLS);
599
+ this.preview.classList.remove("md:block");
600
+ }
601
+ }
602
+ }
603
+
604
+ if (!customElements.get("blume-search")) {
605
+ customElements.define("blume-search", BlumeSearch);
606
+ }
607
+ </script>
608
+ </blume-search>
@@ -0,0 +1,68 @@
1
+ ---
2
+ import type { Heading } from "../../core/types.ts";
3
+ import Icon from "../Icon.astro";
4
+
5
+ interface Props {
6
+ /** Headings to list (already filtered to the depths shown in the TOC). */
7
+ headings: Heading[];
8
+ /** Localized "On this page" heading. */
9
+ title: string;
10
+ /**
11
+ * `mobile` renders the collapsible `<details>` shown above content on narrow
12
+ * viewports; `desktop` renders the sticky aside list shown on wide ones.
13
+ */
14
+ variant: "mobile" | "desktop";
15
+ }
16
+
17
+ const { headings, title, variant } = Astro.props;
18
+ ---
19
+
20
+ {
21
+ headings.length > 0 && variant === "mobile" && (
22
+ <details class="group mx-auto mb-6 max-w-[42rem] rounded-blume border border-border xl:hidden">
23
+ <summary class="flex cursor-pointer list-none items-center justify-between gap-2 px-4 py-3 font-medium text-foreground text-sm [&::-webkit-details-marker]:hidden">
24
+ <span>{title}</span>
25
+ <Icon
26
+ class="text-muted-foreground transition-transform group-open:rotate-180"
27
+ name="chevron-down"
28
+ size={16}
29
+ />
30
+ </summary>
31
+ <blume-toc class="block">
32
+ <ul class="m-0 list-none border-border border-t p-2">
33
+ {headings.map((heading) => (
34
+ <li style={`padding-inline-start:${(heading.depth - 2) * 0.75}rem`}>
35
+ <a
36
+ class="block rounded-md px-2 py-1.5 text-muted-foreground text-sm transition-colors hover:text-foreground aria-[current=location]:font-medium aria-[current=location]:text-foreground"
37
+ href={`#${heading.slug}`}
38
+ >
39
+ {heading.text}
40
+ </a>
41
+ </li>
42
+ ))}
43
+ </ul>
44
+ </blume-toc>
45
+ </details>
46
+ )
47
+ }
48
+ {
49
+ headings.length > 0 && variant === "desktop" && (
50
+ <>
51
+ <p class="mb-3 font-semibold text-foreground text-sm">{title}</p>
52
+ <blume-toc class="block">
53
+ <ul class="m-0 list-none p-0">
54
+ {headings.map((heading) => (
55
+ <li style={`padding-inline-start:${(heading.depth - 2) * 0.75}rem`}>
56
+ <a
57
+ class="block py-1.5 text-muted-foreground transition-colors hover:text-foreground aria-[current=location]:font-medium aria-[current=location]:text-foreground"
58
+ href={`#${heading.slug}`}
59
+ >
60
+ {heading.text}
61
+ </a>
62
+ </li>
63
+ ))}
64
+ </ul>
65
+ </blume-toc>
66
+ </>
67
+ )
68
+ }