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.
- package/bin/blume.mjs +18 -0
- package/dist/cli/index.js +12696 -0
- package/dist/cli/index.js.map +145 -0
- package/dist/types/core/bridge.d.ts +24 -0
- package/dist/types/core/config.d.ts +35 -0
- package/dist/types/core/data.d.ts +129 -0
- package/dist/types/core/define-components.d.ts +27 -0
- package/dist/types/core/define-meta.d.ts +16 -0
- package/dist/types/core/deployment-env.d.ts +9 -0
- package/dist/types/core/diagnostics.d.ts +17 -0
- package/dist/types/core/i18n-ui.d.ts +500 -0
- package/dist/types/core/load-module.d.ts +7 -0
- package/dist/types/core/package-root.d.ts +17 -0
- package/dist/types/core/project.d.ts +9 -0
- package/dist/types/core/schema.d.ts +3451 -0
- package/dist/types/core/sources/types.d.ts +107 -0
- package/dist/types/core/types.d.ts +245 -0
- package/dist/types/core/ui-packs/ar.d.ts +3 -0
- package/dist/types/core/ui-packs/bg.d.ts +3 -0
- package/dist/types/core/ui-packs/bn.d.ts +3 -0
- package/dist/types/core/ui-packs/ca.d.ts +3 -0
- package/dist/types/core/ui-packs/cs.d.ts +3 -0
- package/dist/types/core/ui-packs/da.d.ts +3 -0
- package/dist/types/core/ui-packs/de.d.ts +3 -0
- package/dist/types/core/ui-packs/el.d.ts +3 -0
- package/dist/types/core/ui-packs/es.d.ts +3 -0
- package/dist/types/core/ui-packs/fa.d.ts +3 -0
- package/dist/types/core/ui-packs/fi.d.ts +3 -0
- package/dist/types/core/ui-packs/fr.d.ts +3 -0
- package/dist/types/core/ui-packs/he.d.ts +3 -0
- package/dist/types/core/ui-packs/hi.d.ts +3 -0
- package/dist/types/core/ui-packs/hr.d.ts +3 -0
- package/dist/types/core/ui-packs/hu.d.ts +3 -0
- package/dist/types/core/ui-packs/id.d.ts +3 -0
- package/dist/types/core/ui-packs/index.d.ts +13 -0
- package/dist/types/core/ui-packs/it.d.ts +3 -0
- package/dist/types/core/ui-packs/ja.d.ts +3 -0
- package/dist/types/core/ui-packs/ko.d.ts +3 -0
- package/dist/types/core/ui-packs/nl.d.ts +3 -0
- package/dist/types/core/ui-packs/no.d.ts +3 -0
- package/dist/types/core/ui-packs/pl.d.ts +3 -0
- package/dist/types/core/ui-packs/pt-br.d.ts +3 -0
- package/dist/types/core/ui-packs/pt.d.ts +3 -0
- package/dist/types/core/ui-packs/ro.d.ts +3 -0
- package/dist/types/core/ui-packs/ru.d.ts +3 -0
- package/dist/types/core/ui-packs/sk.d.ts +3 -0
- package/dist/types/core/ui-packs/sr.d.ts +3 -0
- package/dist/types/core/ui-packs/sv.d.ts +3 -0
- package/dist/types/core/ui-packs/th.d.ts +3 -0
- package/dist/types/core/ui-packs/tr.d.ts +3 -0
- package/dist/types/core/ui-packs/uk.d.ts +3 -0
- package/dist/types/core/ui-packs/vi.d.ts +3 -0
- package/dist/types/core/ui-packs/zh-tw.d.ts +3 -0
- package/dist/types/core/ui-packs/zh.d.ts +3 -0
- package/dist/types/core/version.d.ts +8 -0
- package/dist/types/index.d.ts +10 -0
- package/dist/types/migrate/mintlify/config.d.ts +2 -0
- package/dist/types/migrate/mintlify/i18n.d.ts +7 -0
- package/dist/types/theme/fonts.d.ts +163 -0
- package/docs/01-quickstart.mdx +99 -0
- package/docs/02-deployment.mdx +129 -0
- package/docs/advanced/api-reference.mdx +114 -0
- package/docs/advanced/blog.mdx +121 -0
- package/docs/advanced/changelog.mdx +113 -0
- package/docs/advanced/custom-pages.mdx +268 -0
- package/docs/advanced/meta.ts +7 -0
- package/docs/changelog/v0-1-0.mdx +12 -0
- package/docs/changelog/v0-2-0.mdx +16 -0
- package/docs/configuration/ai.mdx +228 -0
- package/docs/configuration/analytics.mdx +98 -0
- package/docs/configuration/customization.mdx +91 -0
- package/docs/configuration/export.mdx +70 -0
- package/docs/configuration/index.mdx +290 -0
- package/docs/configuration/meta.ts +15 -0
- package/docs/configuration/search.mdx +172 -0
- package/docs/configuration/seo.mdx +196 -0
- package/docs/configuration/theming.mdx +178 -0
- package/docs/content/components.mdx +651 -0
- package/docs/content/i18n.mdx +205 -0
- package/docs/content/index.mdx +161 -0
- package/docs/content/islands.mdx +94 -0
- package/docs/content/meta.mdx +119 -0
- package/docs/content/meta.ts +15 -0
- package/docs/content/navigation.mdx +168 -0
- package/docs/content/sources.mdx +216 -0
- package/docs/content/syntax.mdx +445 -0
- package/docs/index.mdx +112 -0
- package/docs/reference/cli.mdx +43 -0
- package/docs/reference/frontmatter.mdx +74 -0
- package/docs/reference/meta.ts +7 -0
- package/package.json +150 -6
- package/src/ai/ask.ts +93 -0
- package/src/ai/llms.ts +64 -0
- package/src/ai/markdown.ts +31 -0
- package/src/ai/mcp/data.ts +74 -0
- package/src/ai/mcp/discovery.ts +49 -0
- package/src/ai/mcp/server.ts +225 -0
- package/src/ai/mcp/tools.ts +47 -0
- package/src/assets/icon.png +0 -0
- package/src/astro/examples.ts +104 -0
- package/src/astro/generate.ts +1006 -0
- package/src/astro/index.ts +4 -0
- package/src/astro/integration.ts +74 -0
- package/src/astro/islands.ts +131 -0
- package/src/astro/markdown-negotiation.ts +68 -0
- package/src/astro/pages.ts +87 -0
- package/src/astro/templates.ts +1326 -0
- package/src/cli/commands/add.ts +81 -0
- package/src/cli/commands/build.ts +103 -0
- package/src/cli/commands/dev.ts +108 -0
- package/src/cli/commands/doctor.ts +74 -0
- package/src/cli/commands/eject.ts +57 -0
- package/src/cli/commands/init.ts +98 -0
- package/src/cli/commands/migrate.ts +39 -0
- package/src/cli/commands/preview.ts +39 -0
- package/src/cli/commands/sync.ts +52 -0
- package/src/cli/commands/validate.ts +61 -0
- package/src/cli/index.ts +35 -0
- package/src/cli/log.ts +37 -0
- package/src/cli/prepare.ts +80 -0
- package/src/components/Icon.astro +99 -0
- package/src/components/content/Accordion.astro +8 -0
- package/src/components/content/AccordionItem.astro +121 -0
- package/src/components/content/AutoTypeTable.astro +51 -0
- package/src/components/content/Badge.astro +124 -0
- package/src/components/content/Callout.astro +73 -0
- package/src/components/content/Card.astro +104 -0
- package/src/components/content/CardGroup.astro +14 -0
- package/src/components/content/CodeBlock.astro +28 -0
- package/src/components/content/CodeGroup.astro +13 -0
- package/src/components/content/Color.astro +15 -0
- package/src/components/content/ColorItem.astro +87 -0
- package/src/components/content/ColorRow.astro +10 -0
- package/src/components/content/Column.astro +6 -0
- package/src/components/content/Columns.astro +9 -0
- package/src/components/content/Component.astro +65 -0
- package/src/components/content/Diff.astro +44 -0
- package/src/components/content/Expandable.astro +11 -0
- package/src/components/content/FileTree.astro +8 -0
- package/src/components/content/Frame.astro +70 -0
- package/src/components/content/GithubInfo.astro +110 -0
- package/src/components/content/Math.astro +24 -0
- package/src/components/content/Panel.astro +20 -0
- package/src/components/content/Prompt.astro +129 -0
- package/src/components/content/Step.astro +34 -0
- package/src/components/content/Steps.astro +20 -0
- package/src/components/content/Tab.astro +46 -0
- package/src/components/content/Tabs.astro +273 -0
- package/src/components/content/Tile.astro +42 -0
- package/src/components/content/Tooltip.astro +68 -0
- package/src/components/content/Tree.astro +300 -0
- package/src/components/content/TreeFile.astro +15 -0
- package/src/components/content/TreeFolder.astro +62 -0
- package/src/components/content/TypeTable.astro +106 -0
- package/src/components/content/Update.astro +66 -0
- package/src/components/content/Visibility.astro +12 -0
- package/src/components/content/Warning.astro +9 -0
- package/src/components/content/auto-type-table.ts +141 -0
- package/src/components/content/diff.ts +95 -0
- package/src/components/content/github-info.ts +79 -0
- package/src/components/content/mermaid-element.ts +68 -0
- package/src/components/github-mark.ts +9 -0
- package/src/components/index.ts +14 -0
- package/src/components/islands/AskAI.astro +12 -0
- package/src/components/islands/ask-ai.tsx +156 -0
- package/src/components/layout/Analytics.astro +63 -0
- package/src/components/layout/Banner.astro +50 -0
- package/src/components/layout/Breadcrumbs.astro +31 -0
- package/src/components/layout/Favicon.astro +22 -0
- package/src/components/layout/Fonts.astro +14 -0
- package/src/components/layout/Header.astro +188 -0
- package/src/components/layout/LanguageSwitcher.astro +56 -0
- package/src/components/layout/NavTree.astro +462 -0
- package/src/components/layout/PageActions.astro +438 -0
- package/src/components/layout/PageFeedback.astro +58 -0
- package/src/components/layout/PageLayout.astro +173 -0
- package/src/components/layout/Pagination.astro +56 -0
- package/src/components/layout/ReferenceLayout.astro +107 -0
- package/src/components/layout/RootLayout.astro +537 -0
- package/src/components/layout/Search.astro +608 -0
- package/src/components/layout/TableOfContents.astro +68 -0
- package/src/components/layout/analytics-client.ts +38 -0
- package/src/components/layout/head-scripts.ts +19 -0
- package/src/components/layout/nav-utils.ts +87 -0
- package/src/components/layout/overrides.ts +32 -0
- package/src/components/layout/search/algolia.ts +43 -0
- package/src/components/layout/search/endpoint.ts +22 -0
- package/src/components/layout/search/flexsearch.ts +52 -0
- package/src/components/layout/search/orama-cloud.ts +41 -0
- package/src/components/layout/search/orama.ts +26 -0
- package/src/components/layout/search/pagefind.ts +43 -0
- package/src/components/layout/search/types.ts +163 -0
- package/src/components/layout/search/typesense.ts +60 -0
- package/src/components/layout/toc-element.ts +108 -0
- package/src/core/bridge.ts +92 -0
- package/src/core/config.ts +112 -0
- package/src/core/content.ts +50 -0
- package/src/core/data.ts +128 -0
- package/src/core/define-components.ts +34 -0
- package/src/core/define-meta.ts +20 -0
- package/src/core/deployment-env.ts +73 -0
- package/src/core/diagnostics.ts +104 -0
- package/src/core/frontmatter.ts +43 -0
- package/src/core/graph.ts +128 -0
- package/src/core/i18n-ui.ts +171 -0
- package/src/core/i18n.ts +169 -0
- package/src/core/last-modified.ts +88 -0
- package/src/core/links.ts +352 -0
- package/src/core/load-module.ts +15 -0
- package/src/core/manifest.ts +126 -0
- package/src/core/meta.ts +97 -0
- package/src/core/navigation.ts +392 -0
- package/src/core/package-root.ts +37 -0
- package/src/core/project-graph.ts +153 -0
- package/src/core/project.ts +56 -0
- package/src/core/schema.ts +1057 -0
- package/src/core/server-features.ts +23 -0
- package/src/core/sources/assets.ts +77 -0
- package/src/core/sources/cache.ts +122 -0
- package/src/core/sources/filesystem.ts +99 -0
- package/src/core/sources/mdx-remote.ts +215 -0
- package/src/core/sources/mintlify.ts +161 -0
- package/src/core/sources/normalize.ts +243 -0
- package/src/core/sources/notion.ts +440 -0
- package/src/core/sources/portable-text.ts +143 -0
- package/src/core/sources/read.ts +36 -0
- package/src/core/sources/resolve.ts +158 -0
- package/src/core/sources/sanity.ts +218 -0
- package/src/core/sources/types.ts +105 -0
- package/src/core/tsconfig-aliases.ts +201 -0
- package/src/core/types.ts +261 -0
- package/src/core/ui-packs/ar.ts +47 -0
- package/src/core/ui-packs/bg.ts +47 -0
- package/src/core/ui-packs/bn.ts +47 -0
- package/src/core/ui-packs/ca.ts +47 -0
- package/src/core/ui-packs/cs.ts +47 -0
- package/src/core/ui-packs/da.ts +47 -0
- package/src/core/ui-packs/de.ts +47 -0
- package/src/core/ui-packs/el.ts +47 -0
- package/src/core/ui-packs/es.ts +47 -0
- package/src/core/ui-packs/fa.ts +47 -0
- package/src/core/ui-packs/fi.ts +47 -0
- package/src/core/ui-packs/fr.ts +47 -0
- package/src/core/ui-packs/he.ts +47 -0
- package/src/core/ui-packs/hi.ts +47 -0
- package/src/core/ui-packs/hr.ts +47 -0
- package/src/core/ui-packs/hu.ts +47 -0
- package/src/core/ui-packs/id.ts +47 -0
- package/src/core/ui-packs/index.ts +87 -0
- package/src/core/ui-packs/it.ts +47 -0
- package/src/core/ui-packs/ja.ts +47 -0
- package/src/core/ui-packs/ko.ts +47 -0
- package/src/core/ui-packs/nl.ts +47 -0
- package/src/core/ui-packs/no.ts +47 -0
- package/src/core/ui-packs/pl.ts +47 -0
- package/src/core/ui-packs/pt-br.ts +47 -0
- package/src/core/ui-packs/pt.ts +47 -0
- package/src/core/ui-packs/ro.ts +47 -0
- package/src/core/ui-packs/ru.ts +47 -0
- package/src/core/ui-packs/sk.ts +47 -0
- package/src/core/ui-packs/sr.ts +47 -0
- package/src/core/ui-packs/sv.ts +47 -0
- package/src/core/ui-packs/th.ts +47 -0
- package/src/core/ui-packs/tr.ts +47 -0
- package/src/core/ui-packs/uk.ts +47 -0
- package/src/core/ui-packs/vi.ts +47 -0
- package/src/core/ui-packs/zh-tw.ts +47 -0
- package/src/core/ui-packs/zh.ts +47 -0
- package/src/core/version.ts +23 -0
- package/src/deploy/robots.ts +20 -0
- package/src/deploy/rss.ts +128 -0
- package/src/deploy/sitemap.ts +28 -0
- package/src/index.ts +39 -0
- package/src/markdown/code-title.ts +71 -0
- package/src/markdown/directives.ts +83 -0
- package/src/markdown/heading-anchors.ts +137 -0
- package/src/markdown/index.ts +228 -0
- package/src/markdown/inline-code.ts +108 -0
- package/src/markdown/language-icon.ts +172 -0
- package/src/markdown/math.ts +32 -0
- package/src/markdown/mdast.ts +48 -0
- package/src/markdown/mermaid.ts +37 -0
- package/src/markdown/package-commands.ts +159 -0
- package/src/markdown/package-install.ts +40 -0
- package/src/migrate/fumadocs/config.ts +155 -0
- package/src/migrate/fumadocs/content.ts +365 -0
- package/src/migrate/fumadocs/frontmatter.ts +18 -0
- package/src/migrate/fumadocs/groups.ts +230 -0
- package/src/migrate/fumadocs/index.ts +337 -0
- package/src/migrate/fumadocs/meta.ts +244 -0
- package/src/migrate/migrate.ts +53 -0
- package/src/migrate/mintlify/config.ts +1040 -0
- package/src/migrate/mintlify/content.ts +98 -0
- package/src/migrate/mintlify/frontmatter.ts +126 -0
- package/src/migrate/mintlify/i18n.ts +51 -0
- package/src/migrate/mintlify/icons.ts +128 -0
- package/src/migrate/mintlify/index.ts +266 -0
- package/src/migrate/mintlify/snippets.ts +306 -0
- package/src/migrate/mintlify/transform.ts +80 -0
- package/src/migrate/nextra/content.ts +46 -0
- package/src/migrate/nextra/frontmatter.ts +40 -0
- package/src/migrate/nextra/index.ts +374 -0
- package/src/migrate/nextra/meta.ts +266 -0
- package/src/migrate/shared.ts +720 -0
- package/src/migrate/starlight/config.ts +459 -0
- package/src/migrate/starlight/content.ts +78 -0
- package/src/migrate/starlight/frontmatter.ts +111 -0
- package/src/migrate/starlight/i18n.ts +54 -0
- package/src/migrate/starlight/index.ts +131 -0
- package/src/og/card.ts +92 -0
- package/src/og/index.ts +2 -0
- package/src/openapi/scalar.ts +246 -0
- package/src/registry/eject.ts +310 -0
- package/src/registry/registry.ts +100 -0
- package/src/registry/rewrite-imports.ts +39 -0
- package/src/runtime/index.ts +14 -0
- package/src/search/build.ts +23 -0
- package/src/search/documents.ts +164 -0
- package/src/search/orama-index.ts +66 -0
- package/src/search/providers.ts +91 -0
- package/src/search/sync/algolia.ts +30 -0
- package/src/search/sync/index.ts +50 -0
- package/src/search/sync/orama-cloud.ts +40 -0
- package/src/search/sync/typesense.ts +65 -0
- package/src/seo/jsonld.ts +113 -0
- package/src/theme/entry.ts +637 -0
- package/src/theme/fonts.ts +198 -0
- package/src/theme/icons.ts +184 -0
- package/src/theme/palette.ts +143 -0
- 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
|
+
}
|