blume 0.0.0 → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (263) hide show
  1. package/bin/blume.mjs +18 -0
  2. package/dist/cli/index.js +11989 -0
  3. package/dist/cli/index.js.map +141 -0
  4. package/docs/01-quickstart.mdx +99 -0
  5. package/docs/02-deployment.mdx +129 -0
  6. package/docs/advanced/api-reference.mdx +114 -0
  7. package/docs/advanced/blog.mdx +121 -0
  8. package/docs/advanced/changelog.mdx +113 -0
  9. package/docs/advanced/custom-pages.mdx +187 -0
  10. package/docs/advanced/meta.ts +7 -0
  11. package/docs/changelog/v0-1-0.mdx +12 -0
  12. package/docs/changelog/v0-2-0.mdx +16 -0
  13. package/docs/configuration/ai.mdx +228 -0
  14. package/docs/configuration/analytics.mdx +98 -0
  15. package/docs/configuration/customization.mdx +91 -0
  16. package/docs/configuration/export.mdx +70 -0
  17. package/docs/configuration/index.mdx +271 -0
  18. package/docs/configuration/meta.ts +15 -0
  19. package/docs/configuration/search.mdx +172 -0
  20. package/docs/configuration/seo.mdx +196 -0
  21. package/docs/configuration/theming.mdx +178 -0
  22. package/docs/content/components.mdx +565 -0
  23. package/docs/content/i18n.mdx +205 -0
  24. package/docs/content/index.mdx +161 -0
  25. package/docs/content/islands.mdx +94 -0
  26. package/docs/content/meta.mdx +119 -0
  27. package/docs/content/meta.ts +15 -0
  28. package/docs/content/navigation.mdx +168 -0
  29. package/docs/content/sources.mdx +216 -0
  30. package/docs/content/syntax.mdx +445 -0
  31. package/docs/index.mdx +112 -0
  32. package/docs/reference/cli.mdx +43 -0
  33. package/docs/reference/frontmatter.mdx +74 -0
  34. package/docs/reference/meta.ts +7 -0
  35. package/package.json +140 -6
  36. package/src/ai/ask.ts +93 -0
  37. package/src/ai/llms.ts +65 -0
  38. package/src/ai/markdown.ts +31 -0
  39. package/src/ai/mcp/data.ts +74 -0
  40. package/src/ai/mcp/discovery.ts +49 -0
  41. package/src/ai/mcp/server.ts +225 -0
  42. package/src/ai/mcp/tools.ts +47 -0
  43. package/src/assets/icon.png +0 -0
  44. package/src/astro/generate.ts +878 -0
  45. package/src/astro/index.ts +4 -0
  46. package/src/astro/integration.ts +74 -0
  47. package/src/astro/islands.ts +131 -0
  48. package/src/astro/markdown-negotiation.ts +68 -0
  49. package/src/astro/pages.ts +28 -0
  50. package/src/astro/templates.ts +1199 -0
  51. package/src/cli/commands/add.ts +81 -0
  52. package/src/cli/commands/build.ts +103 -0
  53. package/src/cli/commands/dev.ts +108 -0
  54. package/src/cli/commands/doctor.ts +74 -0
  55. package/src/cli/commands/eject.ts +57 -0
  56. package/src/cli/commands/init.ts +98 -0
  57. package/src/cli/commands/migrate.ts +39 -0
  58. package/src/cli/commands/preview.ts +39 -0
  59. package/src/cli/commands/sync.ts +52 -0
  60. package/src/cli/commands/validate.ts +60 -0
  61. package/src/cli/index.ts +35 -0
  62. package/src/cli/log.ts +37 -0
  63. package/src/cli/prepare.ts +80 -0
  64. package/src/components/Icon.astro +99 -0
  65. package/src/components/content/Accordion.astro +8 -0
  66. package/src/components/content/AccordionItem.astro +121 -0
  67. package/src/components/content/AutoTypeTable.astro +51 -0
  68. package/src/components/content/Badge.astro +124 -0
  69. package/src/components/content/Callout.astro +73 -0
  70. package/src/components/content/Card.astro +104 -0
  71. package/src/components/content/CardGroup.astro +14 -0
  72. package/src/components/content/CodeGroup.astro +13 -0
  73. package/src/components/content/Color.astro +15 -0
  74. package/src/components/content/ColorItem.astro +87 -0
  75. package/src/components/content/ColorRow.astro +10 -0
  76. package/src/components/content/Column.astro +6 -0
  77. package/src/components/content/Columns.astro +9 -0
  78. package/src/components/content/Expandable.astro +11 -0
  79. package/src/components/content/FileTree.astro +8 -0
  80. package/src/components/content/Frame.astro +70 -0
  81. package/src/components/content/GithubInfo.astro +110 -0
  82. package/src/components/content/Math.astro +24 -0
  83. package/src/components/content/Panel.astro +20 -0
  84. package/src/components/content/Prompt.astro +129 -0
  85. package/src/components/content/Step.astro +34 -0
  86. package/src/components/content/Steps.astro +20 -0
  87. package/src/components/content/Tab.astro +40 -0
  88. package/src/components/content/Tabs.astro +273 -0
  89. package/src/components/content/Tile.astro +42 -0
  90. package/src/components/content/Tooltip.astro +68 -0
  91. package/src/components/content/Tree.astro +300 -0
  92. package/src/components/content/TreeFile.astro +15 -0
  93. package/src/components/content/TreeFolder.astro +62 -0
  94. package/src/components/content/TypeTable.astro +106 -0
  95. package/src/components/content/Update.astro +66 -0
  96. package/src/components/content/Visibility.astro +12 -0
  97. package/src/components/content/Warning.astro +9 -0
  98. package/src/components/content/auto-type-table.ts +141 -0
  99. package/src/components/content/github-info.ts +79 -0
  100. package/src/components/content/mermaid-element.ts +68 -0
  101. package/src/components/github-mark.ts +9 -0
  102. package/src/components/index.ts +14 -0
  103. package/src/components/islands/AskAI.astro +12 -0
  104. package/src/components/islands/ask-ai.tsx +156 -0
  105. package/src/components/layout/Analytics.astro +63 -0
  106. package/src/components/layout/Banner.astro +50 -0
  107. package/src/components/layout/Breadcrumbs.astro +31 -0
  108. package/src/components/layout/Favicon.astro +15 -0
  109. package/src/components/layout/Fonts.astro +14 -0
  110. package/src/components/layout/Header.astro +188 -0
  111. package/src/components/layout/LanguageSwitcher.astro +56 -0
  112. package/src/components/layout/NavTree.astro +462 -0
  113. package/src/components/layout/PageActions.astro +438 -0
  114. package/src/components/layout/PageFeedback.astro +58 -0
  115. package/src/components/layout/Pagination.astro +56 -0
  116. package/src/components/layout/ReferenceLayout.astro +102 -0
  117. package/src/components/layout/RootLayout.astro +533 -0
  118. package/src/components/layout/Search.astro +608 -0
  119. package/src/components/layout/TableOfContents.astro +68 -0
  120. package/src/components/layout/analytics-client.ts +38 -0
  121. package/src/components/layout/nav-utils.ts +87 -0
  122. package/src/components/layout/overrides.ts +32 -0
  123. package/src/components/layout/search/algolia.ts +43 -0
  124. package/src/components/layout/search/endpoint.ts +22 -0
  125. package/src/components/layout/search/flexsearch.ts +52 -0
  126. package/src/components/layout/search/orama-cloud.ts +41 -0
  127. package/src/components/layout/search/orama.ts +26 -0
  128. package/src/components/layout/search/pagefind.ts +43 -0
  129. package/src/components/layout/search/types.ts +163 -0
  130. package/src/components/layout/search/typesense.ts +60 -0
  131. package/src/components/layout/toc-element.ts +108 -0
  132. package/src/core/bridge.ts +92 -0
  133. package/src/core/config.ts +112 -0
  134. package/src/core/content.ts +50 -0
  135. package/src/core/define-components.ts +34 -0
  136. package/src/core/define-meta.ts +20 -0
  137. package/src/core/deployment-env.ts +73 -0
  138. package/src/core/diagnostics.ts +104 -0
  139. package/src/core/graph.ts +128 -0
  140. package/src/core/i18n-ui.ts +171 -0
  141. package/src/core/i18n.ts +169 -0
  142. package/src/core/last-modified.ts +88 -0
  143. package/src/core/links.ts +336 -0
  144. package/src/core/load-module.ts +15 -0
  145. package/src/core/manifest.ts +126 -0
  146. package/src/core/meta.ts +97 -0
  147. package/src/core/navigation.ts +392 -0
  148. package/src/core/package-root.ts +37 -0
  149. package/src/core/project-graph.ts +153 -0
  150. package/src/core/project.ts +56 -0
  151. package/src/core/schema.ts +1057 -0
  152. package/src/core/server-features.ts +23 -0
  153. package/src/core/sources/assets.ts +77 -0
  154. package/src/core/sources/cache.ts +122 -0
  155. package/src/core/sources/filesystem.ts +99 -0
  156. package/src/core/sources/mdx-remote.ts +216 -0
  157. package/src/core/sources/mintlify.ts +161 -0
  158. package/src/core/sources/normalize.ts +227 -0
  159. package/src/core/sources/notion.ts +440 -0
  160. package/src/core/sources/portable-text.ts +143 -0
  161. package/src/core/sources/read.ts +36 -0
  162. package/src/core/sources/resolve.ts +158 -0
  163. package/src/core/sources/sanity.ts +218 -0
  164. package/src/core/sources/types.ts +105 -0
  165. package/src/core/types.ts +261 -0
  166. package/src/core/ui-packs/ar.ts +47 -0
  167. package/src/core/ui-packs/bg.ts +47 -0
  168. package/src/core/ui-packs/bn.ts +47 -0
  169. package/src/core/ui-packs/ca.ts +47 -0
  170. package/src/core/ui-packs/cs.ts +47 -0
  171. package/src/core/ui-packs/da.ts +47 -0
  172. package/src/core/ui-packs/de.ts +47 -0
  173. package/src/core/ui-packs/el.ts +47 -0
  174. package/src/core/ui-packs/es.ts +47 -0
  175. package/src/core/ui-packs/fa.ts +47 -0
  176. package/src/core/ui-packs/fi.ts +47 -0
  177. package/src/core/ui-packs/fr.ts +47 -0
  178. package/src/core/ui-packs/he.ts +47 -0
  179. package/src/core/ui-packs/hi.ts +47 -0
  180. package/src/core/ui-packs/hr.ts +47 -0
  181. package/src/core/ui-packs/hu.ts +47 -0
  182. package/src/core/ui-packs/id.ts +47 -0
  183. package/src/core/ui-packs/index.ts +87 -0
  184. package/src/core/ui-packs/it.ts +47 -0
  185. package/src/core/ui-packs/ja.ts +47 -0
  186. package/src/core/ui-packs/ko.ts +47 -0
  187. package/src/core/ui-packs/nl.ts +47 -0
  188. package/src/core/ui-packs/no.ts +47 -0
  189. package/src/core/ui-packs/pl.ts +47 -0
  190. package/src/core/ui-packs/pt-br.ts +47 -0
  191. package/src/core/ui-packs/pt.ts +47 -0
  192. package/src/core/ui-packs/ro.ts +47 -0
  193. package/src/core/ui-packs/ru.ts +47 -0
  194. package/src/core/ui-packs/sk.ts +47 -0
  195. package/src/core/ui-packs/sr.ts +47 -0
  196. package/src/core/ui-packs/sv.ts +47 -0
  197. package/src/core/ui-packs/th.ts +47 -0
  198. package/src/core/ui-packs/tr.ts +47 -0
  199. package/src/core/ui-packs/uk.ts +47 -0
  200. package/src/core/ui-packs/vi.ts +47 -0
  201. package/src/core/ui-packs/zh-tw.ts +47 -0
  202. package/src/core/ui-packs/zh.ts +47 -0
  203. package/src/core/version.ts +23 -0
  204. package/src/deploy/robots.ts +20 -0
  205. package/src/deploy/rss.ts +128 -0
  206. package/src/deploy/sitemap.ts +28 -0
  207. package/src/index.ts +27 -0
  208. package/src/markdown/code-title.ts +71 -0
  209. package/src/markdown/directives.ts +83 -0
  210. package/src/markdown/heading-anchors.ts +137 -0
  211. package/src/markdown/index.ts +159 -0
  212. package/src/markdown/inline-code.ts +108 -0
  213. package/src/markdown/language-icon.ts +172 -0
  214. package/src/markdown/math.ts +32 -0
  215. package/src/markdown/mdast.ts +48 -0
  216. package/src/markdown/mermaid.ts +37 -0
  217. package/src/markdown/package-commands.ts +159 -0
  218. package/src/markdown/package-install.ts +40 -0
  219. package/src/migrate/fumadocs/config.ts +106 -0
  220. package/src/migrate/fumadocs/content.ts +365 -0
  221. package/src/migrate/fumadocs/frontmatter.ts +18 -0
  222. package/src/migrate/fumadocs/index.ts +252 -0
  223. package/src/migrate/fumadocs/meta.ts +114 -0
  224. package/src/migrate/migrate.ts +53 -0
  225. package/src/migrate/mintlify/config.ts +1040 -0
  226. package/src/migrate/mintlify/content.ts +98 -0
  227. package/src/migrate/mintlify/frontmatter.ts +126 -0
  228. package/src/migrate/mintlify/i18n.ts +51 -0
  229. package/src/migrate/mintlify/icons.ts +128 -0
  230. package/src/migrate/mintlify/index.ts +266 -0
  231. package/src/migrate/mintlify/snippets.ts +305 -0
  232. package/src/migrate/mintlify/transform.ts +81 -0
  233. package/src/migrate/nextra/content.ts +46 -0
  234. package/src/migrate/nextra/frontmatter.ts +40 -0
  235. package/src/migrate/nextra/index.ts +374 -0
  236. package/src/migrate/nextra/meta.ts +266 -0
  237. package/src/migrate/shared.ts +623 -0
  238. package/src/migrate/starlight/config.ts +459 -0
  239. package/src/migrate/starlight/content.ts +78 -0
  240. package/src/migrate/starlight/frontmatter.ts +111 -0
  241. package/src/migrate/starlight/i18n.ts +54 -0
  242. package/src/migrate/starlight/index.ts +131 -0
  243. package/src/og/card.ts +92 -0
  244. package/src/og/index.ts +2 -0
  245. package/src/openapi/scalar.ts +246 -0
  246. package/src/registry/eject.ts +263 -0
  247. package/src/registry/registry.ts +100 -0
  248. package/src/registry/rewrite-imports.ts +39 -0
  249. package/src/runtime/index.ts +14 -0
  250. package/src/search/build.ts +23 -0
  251. package/src/search/documents.ts +165 -0
  252. package/src/search/orama-index.ts +66 -0
  253. package/src/search/providers.ts +91 -0
  254. package/src/search/sync/algolia.ts +30 -0
  255. package/src/search/sync/index.ts +50 -0
  256. package/src/search/sync/orama-cloud.ts +40 -0
  257. package/src/search/sync/typesense.ts +65 -0
  258. package/src/seo/jsonld.ts +113 -0
  259. package/src/theme/entry.ts +608 -0
  260. package/src/theme/fonts.ts +198 -0
  261. package/src/theme/icons.ts +184 -0
  262. package/src/theme/palette.ts +143 -0
  263. package/src/theme/twoslash.ts +81 -0
@@ -0,0 +1,20 @@
1
+ ---
2
+ interface Props {
3
+ titleSize?: "h2" | "h3" | "h4" | "p";
4
+ }
5
+
6
+ const { titleSize = "p" } = Astro.props;
7
+ const titleSizeStyle = {
8
+ h2: "--blume-step-title-size:1.5rem;--blume-step-title-line-height:2rem;",
9
+ h3: "--blume-step-title-size:1.25rem;--blume-step-title-line-height:1.75rem;",
10
+ h4: "--blume-step-title-size:1.125rem;--blume-step-title-line-height:1.75rem;",
11
+ p: "--blume-step-title-size:1rem;--blume-step-title-line-height:1.75rem;",
12
+ }[titleSize];
13
+ ---
14
+
15
+ <div
16
+ class="not-prose my-6 ms-3 space-y-6 border-border border-s ps-8 [counter-reset:blume-step]"
17
+ style={titleSizeStyle}
18
+ >
19
+ <slot />
20
+ </div>
@@ -0,0 +1,40 @@
1
+ ---
2
+ import Icon from "../Icon.astro";
3
+
4
+ interface Props {
5
+ icon?: string;
6
+ iconType?: string;
7
+ id?: string;
8
+ title: string;
9
+ }
10
+
11
+ const { icon, iconType, id, title } = Astro.props;
12
+ const slugify = (value: string): string =>
13
+ value
14
+ .toLowerCase()
15
+ .trim()
16
+ .replaceAll(/[^\w\s-]/gu, "")
17
+ .replaceAll(/[\s_]+/gu, "-")
18
+ .replaceAll(/-+/gu, "-")
19
+ .replaceAll(/^-|-$/gu, "");
20
+ const tabId = id ?? slugify(title);
21
+ ---
22
+
23
+ <div
24
+ class="px-4 py-3 [&:has(>pre)]:p-0 [&>:first-child]:mt-0! [&>:last-child]:mb-0!"
25
+ data-blume-tab-panel
26
+ data-icon={icon}
27
+ data-tab-id={tabId}
28
+ data-title={title}
29
+ id={id}
30
+ role="tabpanel"
31
+ >
32
+ <slot />
33
+ {
34
+ icon && (
35
+ <template data-blume-tab-icon>
36
+ <Icon class="shrink-0" iconType={iconType} name={icon} size={14} />
37
+ </template>
38
+ )
39
+ }
40
+ </div>
@@ -0,0 +1,273 @@
1
+ ---
2
+ interface Props {
3
+ borderBottom?: boolean;
4
+ defaultTabIndex?: number;
5
+ dropdown?: boolean;
6
+ hash?: boolean;
7
+ sync?: boolean;
8
+ }
9
+
10
+ const {
11
+ borderBottom = true,
12
+ defaultTabIndex = 0,
13
+ dropdown = false,
14
+ hash = true,
15
+ sync = true,
16
+ } = Astro.props;
17
+ ---
18
+
19
+ <blume-tabs
20
+ class="not-prose my-6 block overflow-hidden rounded-blume border border-border bg-background"
21
+ data-border-bottom={borderBottom ? "true" : "false"}
22
+ data-default-tab-index={defaultTabIndex}
23
+ data-dropdown={dropdown ? "true" : "false"}
24
+ data-hash={hash ? "true" : "false"}
25
+ data-sync={sync ? "true" : "false"}
26
+ >
27
+ <div class:list={[dropdown ? "" : "overflow-x-auto"]}>
28
+ <div
29
+ class:list={[
30
+ dropdown || borderBottom ? "border-border border-b" : "",
31
+ dropdown ? "p-3" : "flex gap-4 px-3",
32
+ ]}
33
+ data-blume-tablist
34
+ role={dropdown ? undefined : "tablist"}
35
+ >
36
+ </div>
37
+ </div>
38
+ <div data-blume-tab-content>
39
+ <slot />
40
+ </div>
41
+ </blume-tabs>
42
+
43
+ <script>
44
+ const TRIGGER_CLASS =
45
+ "-mb-px inline-flex shrink-0 cursor-pointer items-center gap-1.5 border-transparent border-b-2 bg-transparent px-1 py-2.5 font-medium text-muted-foreground text-xs transition-colors hover:text-foreground aria-[selected=true]:border-accent aria-[selected=true]:text-foreground";
46
+ const SELECT_CLASS =
47
+ "w-full rounded-blume border border-border bg-background px-3 py-2 font-medium text-foreground text-sm";
48
+ const SYNC_EVENT = "blume-tabs-change";
49
+
50
+ const panelTitle = (panel: HTMLElement, index: number) =>
51
+ panel.dataset.title ?? `Tab ${index + 1}`;
52
+ const panelId = (panel: HTMLElement) => panel.dataset.tabId ?? panel.id;
53
+ const boundedIndex = (value: number, length: number) =>
54
+ Math.min(Math.max(value, 0), Math.max(length - 1, 0));
55
+ const appendTriggerContent = (
56
+ trigger: HTMLButtonElement,
57
+ panel: HTMLElement,
58
+ index: number
59
+ ) => {
60
+ const icon = panel.querySelector<HTMLTemplateElement>(
61
+ ":scope > template[data-blume-tab-icon]"
62
+ );
63
+ if (icon) {
64
+ trigger.appendChild(icon.content.cloneNode(true));
65
+ }
66
+ const label = document.createElement("span");
67
+ label.textContent = panelTitle(panel, index);
68
+ trigger.appendChild(label);
69
+ };
70
+
71
+ class BlumeTabs extends HTMLElement {
72
+ #panels: HTMLElement[] = [];
73
+ #triggers: HTMLButtonElement[] = [];
74
+ #select: HTMLSelectElement | null = null;
75
+
76
+ connectedCallback() {
77
+ const list = this.querySelector<HTMLElement>("[data-blume-tablist]");
78
+ let panels = Array.from(
79
+ this.querySelectorAll<HTMLElement>("[data-blume-tab-panel]")
80
+ );
81
+ // CodeGroup passes raw code fences instead of <Tab> elements, so adopt
82
+ // the content wrapper's direct code blocks as panels. The tab label comes
83
+ // from each block's title (the text after the language in the fence).
84
+ if (panels.length === 0) {
85
+ const wrapper = this.querySelector<HTMLElement>(
86
+ ":scope > [data-blume-tab-content]"
87
+ );
88
+ const blocks = wrapper
89
+ ? Array.from(wrapper.children).filter(
90
+ (child): child is HTMLElement =>
91
+ child instanceof HTMLElement && child.matches("pre, figure")
92
+ )
93
+ : [];
94
+ for (const block of blocks) {
95
+ block.setAttribute("data-blume-tab-panel", "");
96
+ }
97
+ panels = blocks;
98
+ }
99
+ if (!list || panels.length === 0) {
100
+ return;
101
+ }
102
+ if (this.dataset.blumeTabsReady === "true") {
103
+ return;
104
+ }
105
+ this.dataset.blumeTabsReady = "true";
106
+ this.#panels = panels;
107
+
108
+ if (this.dataset.dropdown === "true") {
109
+ this.#select = document.createElement("select");
110
+ this.#select.className = SELECT_CLASS;
111
+ this.#select.setAttribute("aria-label", "Select code example");
112
+ panels.forEach((panel, index) => {
113
+ const option = document.createElement("option");
114
+ option.value = String(index);
115
+ option.textContent = panelTitle(panel, index);
116
+ this.#select?.appendChild(option);
117
+ });
118
+ this.#select.addEventListener("change", () => {
119
+ this.activate(Number(this.#select?.value ?? "0"), true, true);
120
+ });
121
+ list.appendChild(this.#select);
122
+ } else {
123
+ panels.forEach((panel, index) => {
124
+ const trigger = document.createElement("button");
125
+ trigger.type = "button";
126
+ trigger.role = "tab";
127
+ trigger.className = TRIGGER_CLASS;
128
+ appendTriggerContent(trigger, panel, index);
129
+ trigger.tabIndex = index === 0 ? 0 : -1;
130
+ if (panel.id) {
131
+ trigger.setAttribute("aria-controls", panel.id);
132
+ }
133
+ trigger.setAttribute(
134
+ "aria-selected",
135
+ index === 0 ? "true" : "false"
136
+ );
137
+ trigger.addEventListener("click", () => {
138
+ this.activate(index, true, true);
139
+ });
140
+ trigger.addEventListener("keydown", (event) => {
141
+ this.#onTriggerKeydown(event, index);
142
+ });
143
+ list.appendChild(trigger);
144
+ this.#triggers.push(trigger);
145
+ panel.classList.toggle("hidden", index !== 0);
146
+ });
147
+ }
148
+
149
+ this.activate(this.#initialIndex(), false, false);
150
+ if (this.#syncEnabled()) {
151
+ document.addEventListener(SYNC_EVENT, this.#sync);
152
+ }
153
+ window.addEventListener("hashchange", this.#hashChange);
154
+ }
155
+
156
+ disconnectedCallback() {
157
+ document.removeEventListener(SYNC_EVENT, this.#sync);
158
+ window.removeEventListener("hashchange", this.#hashChange);
159
+ }
160
+
161
+ activate(index: number, sync: boolean, updateHash: boolean) {
162
+ const activePanel = this.#panels[index];
163
+ if (!activePanel) {
164
+ return;
165
+ }
166
+
167
+ this.#panels.forEach((panel, panelIndex) => {
168
+ panel.classList.toggle("hidden", panelIndex !== index);
169
+ });
170
+ this.#triggers.forEach((trigger, triggerIndex) => {
171
+ trigger.setAttribute(
172
+ "aria-selected",
173
+ triggerIndex === index ? "true" : "false"
174
+ );
175
+ trigger.tabIndex = triggerIndex === index ? 0 : -1;
176
+ });
177
+ if (this.#select) {
178
+ this.#select.value = String(index);
179
+ }
180
+ const id = panelId(activePanel);
181
+ if (updateHash && id && this.#hashEnabled()) {
182
+ history.replaceState(null, "", `#${encodeURIComponent(id)}`);
183
+ }
184
+ if (sync && this.#syncEnabled()) {
185
+ document.dispatchEvent(
186
+ new CustomEvent(SYNC_EVENT, {
187
+ detail: { title: panelTitle(activePanel, index) },
188
+ })
189
+ );
190
+ }
191
+ }
192
+
193
+ activateByTitle(title: string, sync: boolean) {
194
+ const index = this.#panels.findIndex((panel, panelIndex) => {
195
+ return panelTitle(panel, panelIndex) === title;
196
+ });
197
+ if (index !== -1) {
198
+ this.activate(index, sync, false);
199
+ }
200
+ }
201
+
202
+ #activateByHash() {
203
+ if (!this.#hashEnabled()) {
204
+ return false;
205
+ }
206
+ const id = decodeURIComponent(window.location.hash.slice(1));
207
+ if (!id) {
208
+ return false;
209
+ }
210
+ const index = this.#panels.findIndex((panel) => panelId(panel) === id);
211
+ if (index === -1) {
212
+ return false;
213
+ }
214
+
215
+ this.activate(index, false, false);
216
+ return true;
217
+ }
218
+
219
+ #initialIndex() {
220
+ if (this.#activateByHash()) {
221
+ return this.#panels.findIndex((panel) => !panel.classList.contains("hidden"));
222
+ }
223
+ return boundedIndex(
224
+ Number(this.dataset.defaultTabIndex ?? "0"),
225
+ this.#panels.length
226
+ );
227
+ }
228
+
229
+ #onTriggerKeydown(event: KeyboardEvent, index: number) {
230
+ const lastIndex = this.#triggers.length - 1;
231
+ let nextIndex: number | undefined;
232
+ if (event.key === "ArrowRight") {
233
+ nextIndex = index === lastIndex ? 0 : index + 1;
234
+ } else if (event.key === "ArrowLeft") {
235
+ nextIndex = index === 0 ? lastIndex : index - 1;
236
+ } else if (event.key === "Home") {
237
+ nextIndex = 0;
238
+ } else if (event.key === "End") {
239
+ nextIndex = lastIndex;
240
+ }
241
+ if (nextIndex === undefined) {
242
+ return;
243
+ }
244
+
245
+ event.preventDefault();
246
+ this.activate(nextIndex, true, true);
247
+ this.#triggers[nextIndex]?.focus();
248
+ }
249
+
250
+ #syncEnabled() {
251
+ return this.dataset.sync !== "false";
252
+ }
253
+
254
+ #hashEnabled() {
255
+ return this.dataset.hash !== "false";
256
+ }
257
+
258
+ #sync = (event: Event) => {
259
+ const title = (event as CustomEvent<{ title?: string }>).detail?.title;
260
+ if (title) {
261
+ this.activateByTitle(title, false);
262
+ }
263
+ };
264
+
265
+ #hashChange = () => {
266
+ this.#activateByHash();
267
+ };
268
+ }
269
+
270
+ if (!customElements.get("blume-tabs")) {
271
+ customElements.define("blume-tabs", BlumeTabs);
272
+ }
273
+ </script>
@@ -0,0 +1,42 @@
1
+ ---
2
+ import Icon from "../Icon.astro";
3
+
4
+ const { description, href, title } = Astro.props;
5
+ const Tag = href ? "a" : "div";
6
+ const external = href?.startsWith("http");
7
+ const previewPattern =
8
+ "background-image:radial-gradient(circle at 1px 1px,color-mix(in oklab,currentColor 18%,transparent) 1px,transparent 0);background-size:18px 18px;";
9
+ ---
10
+
11
+ <Tag
12
+ class="not-prose block overflow-hidden rounded-blume border border-border bg-background text-inherit no-underline! transition-colors hover:no-underline! hover:[&[href]]:border-accent"
13
+ data-blume-tile
14
+ href={href}
15
+ rel={external ? "noreferrer" : undefined}
16
+ target={external ? "_blank" : undefined}
17
+ >
18
+ <div
19
+ class="flex aspect-[16/9] min-h-32 items-center justify-center bg-muted/40 p-4 text-muted-foreground [&_img]:max-h-full [&_img]:max-w-full [&_svg]:max-h-full [&_svg]:max-w-full"
20
+ data-blume-tile-preview
21
+ style={previewPattern}
22
+ >
23
+ <slot />
24
+ </div>
25
+ <div class="p-4">
26
+ {
27
+ title && (
28
+ <p class="m-0! flex items-center gap-1.5 font-semibold">
29
+ {title}
30
+ {href && (
31
+ <Icon class="text-muted-foreground" name="arrow-right" size={14} />
32
+ )}
33
+ </p>
34
+ )
35
+ }
36
+ {
37
+ description && (
38
+ <p class="mt-1! mb-0! text-muted-foreground text-sm">{description}</p>
39
+ )
40
+ }
41
+ </div>
42
+ </Tag>
@@ -0,0 +1,68 @@
1
+ ---
2
+ import { randomUUID } from "node:crypto";
3
+
4
+ interface Props {
5
+ cta?: string;
6
+ headline?: string;
7
+ href?: string;
8
+ tip: string;
9
+ }
10
+
11
+ const { cta, headline, href, tip } = Astro.props;
12
+ const tooltipId = `blume-tooltip-${randomUUID()}`;
13
+ const external = href?.startsWith("http");
14
+
15
+ const unwrapParagraph = (html: string): string => {
16
+ const trimmed = html.trim();
17
+ const match = trimmed.match(/^<p>(?<content>[\s\S]*)<\/p>$/u);
18
+ return match?.groups?.content ?? trimmed;
19
+ };
20
+
21
+ const labelHtml = Astro.slots.has("default")
22
+ ? unwrapParagraph(await Astro.slots.render("default"))
23
+ : "";
24
+ ---
25
+
26
+ <span class="not-prose group relative inline-flex cursor-help items-baseline">
27
+ <span
28
+ aria-describedby={tooltipId}
29
+ class="underline decoration-dotted underline-offset-4"
30
+ data-blume-tooltip-trigger
31
+ tabindex="0"
32
+ set:html={labelHtml}
33
+ />
34
+ <span
35
+ class="absolute bottom-full left-1/2 z-20 mb-2 w-max max-w-xs -translate-x-1/2 rounded-blume border border-border bg-background p-3 text-start font-sans text-foreground text-sm shadow-lg"
36
+ data-blume-tooltip-panel
37
+ id={tooltipId}
38
+ role="tooltip"
39
+ >
40
+ {headline && <span class="mb-1 block font-semibold">{headline}</span>}
41
+ <span class="block text-muted-foreground leading-5">{tip}</span>
42
+ {
43
+ cta && href && (
44
+ <a
45
+ class="mt-2 inline-flex font-medium text-accent text-xs hover:underline"
46
+ href={href}
47
+ rel={external ? "noreferrer" : undefined}
48
+ target={external ? "_blank" : undefined}
49
+ >
50
+ {cta}
51
+ </a>
52
+ )
53
+ }
54
+ </span>
55
+ </span>
56
+
57
+ <style>
58
+ [data-blume-tooltip-panel] {
59
+ pointer-events: none;
60
+ visibility: hidden;
61
+ }
62
+
63
+ [data-blume-tooltip-trigger]:focus + [data-blume-tooltip-panel],
64
+ .group:hover [data-blume-tooltip-panel] {
65
+ pointer-events: auto;
66
+ visibility: visible;
67
+ }
68
+ </style>