blume 0.6.7 → 0.8.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 (211) hide show
  1. package/CHANGELOG.md +618 -0
  2. package/LICENSE +21 -0
  3. package/README.md +107 -0
  4. package/dist/cli/index.js +2609 -1041
  5. package/dist/cli/index.js.map +110 -103
  6. package/dist/types/ai/component-markdown.d.ts +34 -0
  7. package/dist/types/components/content/youtube.d.ts +18 -0
  8. package/dist/types/core/base-path.d.ts +47 -0
  9. package/dist/types/core/config-input.d.ts +110 -12
  10. package/dist/types/core/config.d.ts +6 -4
  11. package/dist/types/core/data.d.ts +4 -0
  12. package/dist/types/core/i18n-ui.d.ts +477 -135
  13. package/dist/types/core/schema.d.ts +309 -195
  14. package/dist/types/core/sources/types.d.ts +2 -0
  15. package/dist/types/core/types.d.ts +6 -1
  16. package/dist/types/index.d.ts +1 -0
  17. package/dist/types/openapi/references.d.ts +60 -0
  18. package/docs/01-quickstart.mdx +5 -2
  19. package/docs/02-deployment.mdx +24 -9
  20. package/docs/03-faq.mdx +46 -16
  21. package/docs/advanced/custom-pages.mdx +1 -1
  22. package/docs/advanced/skills.mdx +1 -1
  23. package/docs/configuration/ai.mdx +49 -10
  24. package/docs/configuration/customization.mdx +11 -0
  25. package/docs/configuration/index.mdx +33 -3
  26. package/docs/configuration/seo.mdx +2 -2
  27. package/docs/content/components.mdx +30 -3
  28. package/docs/content/i18n.mdx +1 -1
  29. package/docs/content/islands.mdx +8 -0
  30. package/docs/content/navigation.mdx +3 -3
  31. package/docs/content/sources.mdx +1 -1
  32. package/docs/content/syntax.mdx +17 -2
  33. package/docs/index.mdx +2 -2
  34. package/docs/reference/cli.mdx +8 -6
  35. package/package.json +15 -4
  36. package/skills/blume/SKILL.md +5 -3
  37. package/skills/blume-update-docs/SKILL.md +3 -2
  38. package/src/ai/agent-readability.ts +11 -5
  39. package/src/ai/ask-context.ts +7 -2
  40. package/src/ai/ask-data.ts +3 -0
  41. package/src/ai/ask.ts +12 -7
  42. package/src/ai/component-markdown.ts +461 -0
  43. package/src/ai/llms.ts +143 -23
  44. package/src/ai/markdown.ts +35 -6
  45. package/src/ai/mcp/data.ts +33 -8
  46. package/src/ai/mcp/discovery.ts +10 -3
  47. package/src/ai/mcp/server.ts +24 -7
  48. package/src/ai/visibility.ts +74 -0
  49. package/src/astro/component-slots.ts +16 -4
  50. package/src/astro/examples.ts +12 -7
  51. package/src/astro/generate.ts +393 -189
  52. package/src/astro/index.ts +5 -1
  53. package/src/astro/integration.ts +9 -5
  54. package/src/astro/islands.ts +11 -5
  55. package/src/astro/markdown-negotiation.ts +2 -2
  56. package/src/astro/pages.ts +89 -22
  57. package/src/astro/templates.ts +259 -25
  58. package/src/blume-modules.d.ts +8 -0
  59. package/src/cli/commands/build.ts +131 -38
  60. package/src/cli/commands/check.ts +1 -1
  61. package/src/cli/commands/dev.ts +71 -17
  62. package/src/cli/commands/doctor.ts +2 -2
  63. package/src/cli/commands/eject.ts +47 -19
  64. package/src/cli/commands/init.ts +120 -180
  65. package/src/cli/commands/preview.ts +4 -1
  66. package/src/cli/commands/validate.ts +44 -2
  67. package/src/cli/dev-lock.ts +34 -19
  68. package/src/cli/eject-scripts.ts +72 -0
  69. package/src/cli/env.ts +15 -5
  70. package/src/cli/init/questions.ts +158 -0
  71. package/src/cli/init/scaffold.ts +380 -0
  72. package/src/cli/required-secrets.ts +2 -1
  73. package/src/components/content/AccordionItem.astro +23 -4
  74. package/src/components/content/Badge.astro +3 -1
  75. package/src/components/content/Card.astro +4 -2
  76. package/src/components/content/CodeBlock.astro +3 -0
  77. package/src/components/content/Component.astro +30 -16
  78. package/src/components/content/Diff.astro +3 -1
  79. package/src/components/content/Step.astro +10 -1
  80. package/src/components/content/Tabs.astro +15 -3
  81. package/src/components/content/Tile.astro +2 -1
  82. package/src/components/content/Tooltip.astro +3 -1
  83. package/src/components/content/Update.astro +9 -2
  84. package/src/components/content/auto-type-table.ts +25 -9
  85. package/src/components/content/base-href.ts +33 -0
  86. package/src/components/content/changelog-element.ts +9 -2
  87. package/src/components/content/diff.ts +12 -6
  88. package/src/components/content/mermaid-element.ts +10 -2
  89. package/src/components/index.ts +23 -1
  90. package/src/components/islands/AskAI.astro +5 -2
  91. package/src/components/islands/ask-ai.tsx +68 -12
  92. package/src/components/islands/base-path.ts +28 -0
  93. package/src/components/islands/hooks.ts +44 -9
  94. package/src/components/layout/Banner.astro +12 -3
  95. package/src/components/layout/Breadcrumbs.astro +2 -1
  96. package/src/components/layout/Favicon.astro +3 -2
  97. package/src/components/layout/Header.astro +15 -5
  98. package/src/components/layout/LanguageSwitcher.astro +2 -1
  99. package/src/components/layout/Logo.astro +13 -4
  100. package/src/components/layout/NavSelector.astro +2 -1
  101. package/src/components/layout/NavTree.astro +22 -7
  102. package/src/components/layout/PageActions.astro +25 -10
  103. package/src/components/layout/PageFeedback.astro +4 -1
  104. package/src/components/layout/PageLayout.astro +51 -9
  105. package/src/components/layout/Pagination.astro +3 -2
  106. package/src/components/layout/ReferenceLayout.astro +8 -1
  107. package/src/components/layout/RootLayout.astro +74 -13
  108. package/src/components/layout/Search.astro +107 -27
  109. package/src/components/layout/nav-utils.ts +18 -10
  110. package/src/components/layout/search/algolia.ts +11 -2
  111. package/src/components/layout/search/endpoint.ts +11 -5
  112. package/src/components/layout/search/orama-cloud.ts +8 -2
  113. package/src/components/layout/search/pagefind.ts +3 -0
  114. package/src/components/layout/search/types.ts +5 -1
  115. package/src/components/layout/search/typesense.ts +4 -1
  116. package/src/components/layout/toc-element.ts +8 -2
  117. package/src/components/openapi/ApiTagOperations.astro +2 -1
  118. package/src/components/openapi/Operation.astro +47 -40
  119. package/src/components/openapi/RequestPanel.astro +8 -2
  120. package/src/components/openapi/helpers.ts +71 -3
  121. package/src/components/openapi/panel.ts +1 -1
  122. package/src/components/openapi/snippets.ts +25 -11
  123. package/src/core/base-path.ts +94 -0
  124. package/src/core/builtin-tags.ts +2 -0
  125. package/src/core/component-overrides.ts +103 -74
  126. package/src/core/config-input.ts +118 -17
  127. package/src/core/config.ts +8 -5
  128. package/src/core/content.ts +2 -0
  129. package/src/core/data.ts +4 -0
  130. package/src/core/diagnostics.ts +54 -34
  131. package/src/core/gitignore.ts +4 -1
  132. package/src/core/graph.ts +166 -88
  133. package/src/core/i18n-ui.ts +63 -3
  134. package/src/core/last-modified.ts +15 -6
  135. package/src/core/links.ts +69 -25
  136. package/src/core/manifest.ts +62 -45
  137. package/src/core/nav-diagnostics.ts +1 -1
  138. package/src/core/navigation.ts +144 -58
  139. package/src/core/package-json.ts +17 -2
  140. package/src/core/project-graph.ts +25 -15
  141. package/src/core/schema.ts +605 -620
  142. package/src/core/sources/assets.ts +6 -1
  143. package/src/core/sources/filesystem.ts +4 -0
  144. package/src/core/sources/github-releases.ts +2 -1
  145. package/src/core/sources/mdx-remote.ts +76 -63
  146. package/src/core/sources/normalize.ts +236 -91
  147. package/src/core/sources/notion.ts +27 -18
  148. package/src/core/sources/types.ts +2 -0
  149. package/src/core/tsconfig-aliases.ts +59 -30
  150. package/src/core/types.ts +6 -1
  151. package/src/core/ui-packs/ar.ts +1 -0
  152. package/src/core/ui-packs/bg.ts +1 -0
  153. package/src/core/ui-packs/bn.ts +1 -0
  154. package/src/core/ui-packs/ca.ts +1 -0
  155. package/src/core/ui-packs/cs.ts +1 -0
  156. package/src/core/ui-packs/da.ts +1 -0
  157. package/src/core/ui-packs/de.ts +1 -0
  158. package/src/core/ui-packs/el.ts +1 -0
  159. package/src/core/ui-packs/es.ts +1 -0
  160. package/src/core/ui-packs/fa.ts +1 -0
  161. package/src/core/ui-packs/fi.ts +1 -0
  162. package/src/core/ui-packs/fr.ts +2 -1
  163. package/src/core/ui-packs/he.ts +1 -0
  164. package/src/core/ui-packs/hi.ts +1 -0
  165. package/src/core/ui-packs/hr.ts +1 -0
  166. package/src/core/ui-packs/hu.ts +1 -0
  167. package/src/core/ui-packs/id.ts +1 -0
  168. package/src/core/ui-packs/it.ts +1 -0
  169. package/src/core/ui-packs/ja.ts +1 -0
  170. package/src/core/ui-packs/ko.ts +1 -0
  171. package/src/core/ui-packs/nl.ts +1 -0
  172. package/src/core/ui-packs/no.ts +1 -0
  173. package/src/core/ui-packs/pl.ts +1 -0
  174. package/src/core/ui-packs/pt-br.ts +1 -0
  175. package/src/core/ui-packs/pt.ts +1 -0
  176. package/src/core/ui-packs/ro.ts +1 -0
  177. package/src/core/ui-packs/ru.ts +1 -0
  178. package/src/core/ui-packs/sk.ts +1 -0
  179. package/src/core/ui-packs/sr.ts +1 -0
  180. package/src/core/ui-packs/sv.ts +1 -0
  181. package/src/core/ui-packs/th.ts +1 -0
  182. package/src/core/ui-packs/tr.ts +1 -0
  183. package/src/core/ui-packs/uk.ts +1 -0
  184. package/src/core/ui-packs/vi.ts +1 -0
  185. package/src/core/ui-packs/zh-tw.ts +1 -0
  186. package/src/core/ui-packs/zh.ts +1 -0
  187. package/src/deploy/adapter-output.ts +18 -8
  188. package/src/deploy/redirects.ts +25 -2
  189. package/src/deploy/robots.ts +6 -1
  190. package/src/deploy/rss.ts +10 -3
  191. package/src/deploy/sitemap.ts +59 -13
  192. package/src/index.ts +5 -0
  193. package/src/markdown/base-links.ts +60 -0
  194. package/src/markdown/code-title.ts +11 -14
  195. package/src/markdown/index.ts +46 -9
  196. package/src/markdown/inline-code.ts +14 -4
  197. package/src/markdown/package-commands.ts +10 -4
  198. package/src/markdown/themes.ts +24 -0
  199. package/src/openapi/model.ts +15 -5
  200. package/src/openapi/parse.ts +21 -0
  201. package/src/openapi/references.ts +75 -21
  202. package/src/openapi/render-mdx.ts +11 -6
  203. package/src/openapi/scalar.ts +32 -16
  204. package/src/openapi/source.ts +59 -10
  205. package/src/registry/eject.ts +247 -19
  206. package/src/registry/registry.ts +0 -3
  207. package/src/search/build.ts +3 -0
  208. package/src/search/documents.ts +36 -4
  209. package/src/search/sync/typesense.ts +6 -4
  210. package/src/seo/jsonld.ts +28 -17
  211. package/src/theme/entry.ts +85 -20
@@ -5,14 +5,18 @@
5
5
  // `<Component path="forms/login" />` for `examples/forms/login.tsx`.
6
6
  //
7
7
  // Blume discovers `examples/` at build time and generates `blume:examples`: a
8
- // map of path → { Component (a static hydration wrapper), code, lang }. React,
9
- // Vue, Svelte, and Astro examples are all supported. The source is highlighted
10
- // with the same Shiki setup as ordinary code fences.
8
+ // map of path → { Component (a static hydration wrapper), code, lang } plus
9
+ // the route base preview frames live under. React, Vue, Svelte, and Astro
10
+ // examples are all supported. The source is highlighted with the same Shiki
11
+ // setup as ordinary code fences.
12
+ import data from "blume:data";
13
+
11
14
  import { highlightCode } from "../../markdown/index.ts";
15
+ import { withBase } from "../islands/base-path.ts";
12
16
  import Tab from "./Tab.astro";
13
17
  import Tabs from "./Tabs.astro";
14
18
  // Generated per project; resolves to an empty map when there are no examples.
15
- import { examples } from "blume:examples";
19
+ import { examples, examplesBase } from "blume:examples";
16
20
 
17
21
  interface Props {
18
22
  path: string;
@@ -20,14 +24,27 @@ interface Props {
20
24
 
21
25
  const { path } = Astro.props;
22
26
  const entry = examples[path];
23
- const Preview = entry?.Component;
27
+
28
+ // The live preview renders in its own generated route, embedded as an iframe:
29
+ // the frame boundary is what keeps the docs CSS (prose typography, component
30
+ // chrome) out of the example entirely, so users can preview components styled
31
+ // by their own design system (e.g. shadcn, via the configured `examples.css`).
32
+ // The frame syncs its theme with this document before first paint.
33
+ const previewSrc = entry
34
+ ? withBase(
35
+ `${examplesBase}/${path.split("/").map(encodeURIComponent).join("/")}`
36
+ )
37
+ : undefined;
24
38
 
25
39
  // Highlight the source with the shared helper so it matches Markdown fences.
26
40
  // `blume-source` makes the inner `<code>` (not the `<pre>`) the scroll/height-
27
41
  // capped box — see the theme entry — so the copy button, pinned to the `<pre>`,
28
42
  // stays put. This pane already lives inside `.prose`, so the theme styles it.
29
43
  const codeHtml = entry
30
- ? await highlightCode(entry.code, entry.lang, { className: "blume-source" })
44
+ ? await highlightCode(entry.code, entry.lang, {
45
+ className: "blume-source",
46
+ themes: data.config.codeThemes,
47
+ })
31
48
  : undefined;
32
49
 
33
50
  // Both tabs share one height so toggling them never shifts the layout. Size it
@@ -45,16 +62,13 @@ const paneStyle = `height:${paneHeight}px`;
45
62
  // `sync={false}`: each preview's Preview/Code tabs are independent — unlike
46
63
  // CodeGroup, switching one Component must not switch the others.
47
64
  <Tabs hash={false} sync={false}>
48
- <Tab
49
- // `not-prose`: the preview lives inside the page's `.prose` wrapper, so
50
- // without this the typography styles bleed into the live component
51
- // (headings, links, lists, spacing). The Code pane below keeps prose on
52
- // purpose — that's what styles the highlighted source.
53
- class="not-prose flex items-center justify-center overflow-auto"
54
- style={paneStyle}
55
- title="Preview"
56
- >
57
- <Preview />
65
+ <Tab class="overflow-hidden p-0!" style={paneStyle} title="Preview">
66
+ <iframe
67
+ class="h-full w-full"
68
+ loading="lazy"
69
+ src={previewSrc}
70
+ title={`Preview of ${path}`}
71
+ />
58
72
  </Tab>
59
73
  <Tab class="overflow-hidden" style={paneStyle} title="Code">
60
74
  <Fragment set:html={codeHtml} />
@@ -6,6 +6,8 @@
6
6
  //
7
7
  // Inputs (one of): `patch`/`src` (unified patch), `before`+`after` (file paths),
8
8
  // or `old`+`new` (inline strings, with optional `lang`).
9
+ import data from "blume:data";
10
+
9
11
  import { renderDiff } from "./diff.ts";
10
12
 
11
13
  interface Props {
@@ -22,7 +24,7 @@ let html: string | undefined;
22
24
  let error: string | undefined;
23
25
 
24
26
  try {
25
- html = await renderDiff(Astro.props);
27
+ html = await renderDiff({ ...Astro.props, theme: data.config.codeThemes });
26
28
  } catch (cause) {
27
29
  error = cause instanceof Error ? cause.message : String(cause);
28
30
  }
@@ -26,7 +26,16 @@ const canRenderIcon =
26
26
  <span class:list={[markerClass, !canRenderIcon && "before:[content:counter(blume-step)]"]}>
27
27
  {canRenderIcon && <Icon name={icon} size={14} />}
28
28
  </span>
29
- {title && <p class="mb-1.5 font-semibold">{title}</p>}
29
+ {
30
+ // Consumes the `titleSize` variables set by <Steps>; the fallbacks match
31
+ // its "p" default (and the prose-inherited sizing) so a bare <Step>
32
+ // renders identically.
33
+ title && (
34
+ <p class="mb-1.5 font-semibold text-[length:var(--blume-step-title-size,1rem)] leading-[var(--blume-step-title-line-height,1.75rem)]">
35
+ {title}
36
+ </p>
37
+ )
38
+ }
30
39
  <div class="[&>:first-child]:mt-0! [&>:last-child]:mb-0!">
31
40
  <slot />
32
41
  </div>
@@ -51,6 +51,10 @@ const hashEnabled = String(hash) !== "false";
51
51
  "w-full rounded-blume border border-border bg-background px-3 py-2 font-medium text-foreground text-sm";
52
52
  const SYNC_EVENT = "blume-tabs-change";
53
53
 
54
+ // Disambiguates generated trigger/panel ids when the same tab labels appear
55
+ // in several <Tabs> instances on one page.
56
+ let instanceCount = 0;
57
+
54
58
  const panelTitle = (panel: HTMLElement, index: number) =>
55
59
  panel.dataset.title ?? `Tab ${index + 1}`;
56
60
  const panelId = (panel: HTMLElement) => panel.dataset.tabId ?? panel.id;
@@ -119,7 +123,8 @@ const hashEnabled = String(hash) !== "false";
119
123
  if (this.dataset.dropdown === "true") {
120
124
  this.#select = document.createElement("select");
121
125
  this.#select.className = SELECT_CLASS;
122
- this.#select.setAttribute("aria-label", "Select code example");
126
+ // Neutral default: tab sets aren't necessarily code examples.
127
+ this.#select.setAttribute("aria-label", "Select tab");
123
128
  panels.forEach((panel, index) => {
124
129
  const option = document.createElement("option");
125
130
  option.value = String(index);
@@ -131,6 +136,7 @@ const hashEnabled = String(hash) !== "false";
131
136
  });
132
137
  list.appendChild(this.#select);
133
138
  } else {
139
+ const instance = ++instanceCount;
134
140
  panels.forEach((panel, index) => {
135
141
  const trigger = document.createElement("button");
136
142
  trigger.type = "button";
@@ -138,9 +144,15 @@ const hashEnabled = String(hash) !== "false";
138
144
  trigger.className = TRIGGER_CLASS;
139
145
  appendTriggerContent(trigger, panel, index);
140
146
  trigger.tabIndex = index === 0 ? 0 : -1;
141
- if (panel.id) {
142
- trigger.setAttribute("aria-controls", panel.id);
147
+ // Wire tab and panel both ways. An author-supplied panel id is kept;
148
+ // otherwise derive unique ids from the label slug per instance.
149
+ const slug = panelId(panel) || `tab-${index + 1}`;
150
+ if (!panel.id) {
151
+ panel.id = `blume-tabpanel-${slug}-${instance}`;
143
152
  }
153
+ trigger.id = `blume-tab-${slug}-${instance}`;
154
+ trigger.setAttribute("aria-controls", panel.id);
155
+ panel.setAttribute("aria-labelledby", trigger.id);
144
156
  trigger.setAttribute(
145
157
  "aria-selected",
146
158
  index === 0 ? "true" : "false"
@@ -1,5 +1,6 @@
1
1
  ---
2
2
  import Icon from "../Icon.astro";
3
+ import { contentHref } from "./base-href.ts";
3
4
 
4
5
  const { description, href, title } = Astro.props;
5
6
  const Tag = href ? "a" : "div";
@@ -11,7 +12,7 @@ const previewPattern =
11
12
  <Tag
12
13
  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
14
  data-blume-tile
14
- href={href}
15
+ href={href === undefined ? undefined : contentHref(href)}
15
16
  rel={external ? "noreferrer" : undefined}
16
17
  target={external ? "_blank" : undefined}
17
18
  >
@@ -1,6 +1,8 @@
1
1
  ---
2
2
  import { randomUUID } from "node:crypto";
3
3
 
4
+ import { contentHref } from "./base-href.ts";
5
+
4
6
  interface Props {
5
7
  cta?: string;
6
8
  headline?: string;
@@ -43,7 +45,7 @@ const labelHtml = Astro.slots.has("default")
43
45
  cta && href && (
44
46
  <a
45
47
  class="mt-2 inline-flex font-medium text-accent text-xs hover:underline"
46
- href={href}
48
+ href={contentHref(href)}
47
49
  rel={external ? "noreferrer" : undefined}
48
50
  target={external ? "_blank" : undefined}
49
51
  >
@@ -1,4 +1,6 @@
1
1
  ---
2
+ import { contentHref } from "./base-href.ts";
3
+
2
4
  interface RssMetadata {
3
5
  description?: string;
4
6
  title?: string;
@@ -6,7 +8,12 @@ interface RssMetadata {
6
8
 
7
9
  interface Props {
8
10
  description?: string;
9
- /** Link the heading to a dedicated page; falls back to the in-page anchor. */
11
+ /**
12
+ * Link the heading to a dedicated page; falls back to the in-page anchor.
13
+ * A root-relative route gains the composed `deployment.base` + `basePath`
14
+ * prefix at emit time (like a markdown link); anchors and external URLs
15
+ * pass through.
16
+ */
10
17
  href?: string;
11
18
  id?: string;
12
19
  label?: string;
@@ -47,7 +54,7 @@ const tagList = Array.isArray(tags) ? tags : tags ? [tags] : [];
47
54
  <header class="md:border-border md:border-e md:pe-4">
48
55
  <a
49
56
  class="font-semibold text-foreground text-sm no-underline hover:text-accent"
50
- href={href ?? `#${id}`}
57
+ href={contentHref(href ?? `#${id}`)}
51
58
  >
52
59
  {updateLabel}
53
60
  </a>
@@ -9,7 +9,17 @@
9
9
  */
10
10
  import nodePath from "node:path";
11
11
 
12
- import type * as TypeScriptApi from "typescript";
12
+ import type {
13
+ CompilerHost,
14
+ CompilerOptions,
15
+ DeclarationStatement,
16
+ Program,
17
+ } from "typescript";
18
+
19
+ // `typeof import(...)` is the only way to name the whole lazily-imported
20
+ // `typescript` module without a value import (see components/props.ts).
21
+ // oxlint-disable-next-line typescript/consistent-type-imports
22
+ type TypeScriptApi = typeof import("typescript");
13
23
 
14
24
  /** A single documented property — one generated row of a type table. */
15
25
  export interface TypeTableProperty {
@@ -36,10 +46,10 @@ const VIRTUAL_FILE = "__blume_auto_type_table__.ts";
36
46
 
37
47
  /** Build a compiler host that serves a single in-memory file plus the real libs. */
38
48
  const inMemoryHost = (
39
- ts: typeof TypeScriptApi,
40
- options: TypeScriptApi.CompilerOptions,
49
+ ts: TypeScriptApi,
50
+ options: CompilerOptions,
41
51
  source: string
42
- ): TypeScriptApi.CompilerHost => {
52
+ ): CompilerHost => {
43
53
  const host = ts.createCompilerHost(options, true);
44
54
  const getSourceFile = host.getSourceFile.bind(host);
45
55
  host.getSourceFile = (requested, languageVersion, onError, shouldCreate) =>
@@ -65,9 +75,9 @@ export const extractTypeTable = async (
65
75
  ): Promise<TypeTableProperty[]> => {
66
76
  const { name, path, root = process.cwd(), source } = options;
67
77
  const tsModule = await import("typescript");
68
- const ts = (tsModule.default ?? tsModule) as typeof TypeScriptApi;
78
+ const ts = (tsModule.default ?? tsModule) as TypeScriptApi;
69
79
 
70
- const compilerOptions: TypeScriptApi.CompilerOptions = {
80
+ const compilerOptions: CompilerOptions = {
71
81
  allowJs: true,
72
82
  module: ts.ModuleKind.ESNext,
73
83
  moduleResolution: ts.ModuleResolutionKind.Bundler,
@@ -78,7 +88,7 @@ export const extractTypeTable = async (
78
88
  };
79
89
 
80
90
  let fileName: string;
81
- let program: TypeScriptApi.Program;
91
+ let program: Program;
82
92
  if (source === undefined) {
83
93
  if (path === undefined) {
84
94
  throw new Error("AutoTypeTable needs a `path` or inline `type` source.");
@@ -100,7 +110,7 @@ export const extractTypeTable = async (
100
110
  }
101
111
 
102
112
  const declaration = sourceFile.statements.find(
103
- (statement): statement is TypeScriptApi.DeclarationStatement =>
113
+ (statement): statement is DeclarationStatement =>
104
114
  (ts.isInterfaceDeclaration(statement) ||
105
115
  ts.isTypeAliasDeclaration(statement)) &&
106
116
  statement.name?.text === name
@@ -134,7 +144,13 @@ export const extractTypeTable = async (
134
144
  default: defaultValue || undefined,
135
145
  description: description || undefined,
136
146
  name: symbol.getName(),
137
- required: !signature?.questionToken,
147
+ // Optionality lives on the checker's symbol, not the backing
148
+ // declaration: mapped/utility types synthesize members whose
149
+ // declaration has no question token (`Partial<Base>` would read as
150
+ // all-required), and `Required<Base>` strips optionality while the
151
+ // declaration keeps its `?` — the token is wrong in both directions.
152
+ // oxlint-disable-next-line no-bitwise -- SymbolFlags is a bitfield
153
+ required: (symbol.flags & ts.SymbolFlags.Optional) === 0,
138
154
  type: typeText,
139
155
  };
140
156
  });
@@ -0,0 +1,33 @@
1
+ import data from "blume:data";
2
+
3
+ import {
4
+ isInternalPath,
5
+ normalizeBasePath,
6
+ withComposedBasePath,
7
+ } from "../../core/base-path.ts";
8
+
9
+ // Mirrors `markdown/base-links.ts`: a path whose final segment carries a file
10
+ // extension is a `public/` asset, served at the site root and never moved
11
+ // under `basePath`.
12
+ const ASSET_PATH = /\.[a-z0-9]+$/iu;
13
+
14
+ /** Strip any `#fragment`/`?query` so only the path is extension-tested. */
15
+ const pathOf = (url: string): string => url.replace(/[#?].*$/u, "");
16
+
17
+ /**
18
+ * Rebase a component-emitted `href` the way `markdown/base-links.ts` rebases
19
+ * `[x](/guide)`: a root-relative internal page link gains the composed
20
+ * `deployment.base` + `basePath` prefix, so authors write component links
21
+ * (`<Card href="/guide">`) under the same "as if mounted at root" contract as
22
+ * markdown links. Idempotent per layer (a hand-written `/docs/x` isn't
23
+ * double-prefixed) and inert for external URLs, fragments, relative paths, and
24
+ * asset links.
25
+ */
26
+ export const contentHref = (href: string): string =>
27
+ isInternalPath(href) && !ASSET_PATH.test(pathOf(href))
28
+ ? withComposedBasePath(
29
+ normalizeBasePath(import.meta.env.BASE_URL),
30
+ data.config.basePath,
31
+ href
32
+ )
33
+ : href;
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Client behaviour for the `<blume-changelog>` custom element wrapping the
2
+ * Client behavior for the `<blume-changelog>` custom element wrapping the
3
3
  * generated changelog timeline when its releases are semver-versioned. The
4
4
  * newest major line stays visible; every older major is collapsed into a group
5
5
  * revealed one major at a time by the "Show N.x releases" button at the bottom.
@@ -24,6 +24,10 @@ class BlumeChangelog extends HTMLElement {
24
24
  return;
25
25
  }
26
26
 
27
+ // Localized button template from the generated page markup (the
28
+ // data-attribute channel); `{version}` is replaced with the major line.
29
+ const template = this.dataset.i18nMore || "Show {version} releases";
30
+
27
31
  for (const group of groups) {
28
32
  group.hidden = true;
29
33
  // Focusable only programmatically, so revealing a group can move focus to
@@ -35,7 +39,10 @@ class BlumeChangelog extends HTMLElement {
35
39
  const sync = () => {
36
40
  const next = groups[revealed];
37
41
  if (next) {
38
- button.textContent = `Show ${next.dataset.changelogLabel} releases`;
42
+ button.textContent = template.replace(
43
+ "{version}",
44
+ next.dataset.changelogLabel ?? ""
45
+ );
39
46
  button.hidden = false;
40
47
  } else {
41
48
  button.hidden = true;
@@ -13,6 +13,9 @@ import { readFile } from "node:fs/promises";
13
13
  import { preloadDiffHTML, preloadPatchDiff } from "@pierre/diffs/ssr";
14
14
  import { isAbsolute, join } from "pathe";
15
15
 
16
+ import { DEFAULT_CODE_THEMES } from "../../markdown/themes.ts";
17
+ import type { CodeThemes } from "../../markdown/themes.ts";
18
+
16
19
  export interface DiffOptions {
17
20
  /** Path to the "after" file, resolved relative to {@link DiffOptions.root}. */
18
21
  after?: string;
@@ -30,11 +33,13 @@ export interface DiffOptions {
30
33
  root?: string;
31
34
  /** Path to a `.patch`/`.diff` file, resolved relative to {@link DiffOptions.root}. */
32
35
  src?: string;
36
+ /**
37
+ * Light/dark Shiki themes (`markdown.codeBlocks.theme`). Defaults to the same
38
+ * github pair Blume's code blocks use, keeping diffs in lockstep.
39
+ */
40
+ theme?: CodeThemes;
33
41
  }
34
42
 
35
- /** Dual-theme config mirroring Blume's Shiki convention (see `templates.ts`). */
36
- const THEME = { dark: "github-dark", light: "github-light" } as const;
37
-
38
43
  const resolvePath = (path: string, root: string): string =>
39
44
  isAbsolute(path) ? path : join(root, path);
40
45
 
@@ -56,12 +61,13 @@ export const renderDiff = async (options: DiffOptions): Promise<string> => {
56
61
  patch,
57
62
  root = process.cwd(),
58
63
  src,
64
+ theme = DEFAULT_CODE_THEMES,
59
65
  } = options;
60
66
 
61
67
  if (patch !== undefined || src !== undefined) {
62
68
  const text = patch ?? (await readText(src as string, root));
63
69
  const result = await preloadPatchDiff({
64
- options: { theme: THEME },
70
+ options: { theme },
65
71
  patch: text,
66
72
  });
67
73
  return result.prerenderedHTML;
@@ -74,7 +80,7 @@ export const renderDiff = async (options: DiffOptions): Promise<string> => {
74
80
  return await preloadDiffHTML({
75
81
  newFile: { contents: await readText(after, root), name: after },
76
82
  oldFile: { contents: await readText(before, root), name: before },
77
- options: { theme: THEME },
83
+ options: { theme },
78
84
  });
79
85
  }
80
86
 
@@ -85,7 +91,7 @@ export const renderDiff = async (options: DiffOptions): Promise<string> => {
85
91
  return await preloadDiffHTML({
86
92
  newFile: { contents: newText, lang, name: "snippet" },
87
93
  oldFile: { contents: old, lang, name: "snippet" },
88
- options: { disableFileHeader: true, theme: THEME },
94
+ options: { disableFileHeader: true, theme },
89
95
  });
90
96
  }
91
97
 
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Client behaviour for the `<blume-mermaid>` custom element emitted by the
2
+ * Client behavior for the `<blume-mermaid>` custom element emitted by the
3
3
  * Mermaid markdown plugin. Mermaid is lazy-loaded — it needs a DOM and is large,
4
4
  * so the dependency only downloads on pages that actually contain a diagram —
5
5
  * and each diagram re-renders when the color theme flips so it tracks light/dark.
@@ -55,10 +55,18 @@ class BlumeMermaid extends HTMLElement {
55
55
  // A newer render (rapid theme toggles) superseded this one — dropping
56
56
  // the stale result keeps the diagram in the latest theme.
57
57
  if (token === this.#renderToken) {
58
+ // Mermaid's own render output is SVG markup; it must be injected as
59
+ // HTML, not text. securityLevel "strict" (see initialize) sanitizes it.
60
+ // oxlint-disable-next-line github/no-inner-html -- Mermaid-generated SVG must be injected as HTML
58
61
  output.innerHTML = svg;
59
62
  }
60
63
  } catch {
61
- output.textContent = "Could not render this diagram.";
64
+ // Localized message stamped on <body> by RootLayout's markup (the
65
+ // data-attribute channel); English fallback when the attribute is
66
+ // missing (a stale snapshot or a custom layout).
67
+ output.textContent =
68
+ document.body.dataset.i18nDiagramError ||
69
+ "Could not render this diagram.";
62
70
  }
63
71
  output.removeAttribute("aria-busy");
64
72
  };
@@ -11,4 +11,26 @@ export type {
11
11
  IslandDescriptor,
12
12
  } from "../core/define-components.ts";
13
13
  export type { HydrationMode } from "../core/schema.ts";
14
- export type * from "./props.ts";
14
+ export type {
15
+ AccordionItemProps,
16
+ AccordionProps,
17
+ BadgeProps,
18
+ CalloutProps,
19
+ CardGroupProps,
20
+ CardProps,
21
+ CodeGroupProps,
22
+ ColumnProps,
23
+ ColumnsProps,
24
+ ExpandableProps,
25
+ FrameProps,
26
+ IconProps,
27
+ PanelProps,
28
+ PromptProps,
29
+ StepProps,
30
+ StepsProps,
31
+ TabProps,
32
+ TabsProps,
33
+ TileProps,
34
+ TooltipProps,
35
+ YouTubeProps,
36
+ } from "./props.ts";
@@ -63,13 +63,16 @@ const icons = {
63
63
  }
64
64
 
65
65
  /* The table of contents only shows at xl; reclaim its column for the article
66
- while the panel is open so the shrunken content still has room to breathe. */
66
+ while the panel is open so the shrunken content still has room to breathe.
67
+ Scoped to grids that actually have a TOC column: a "bare" layout (the
68
+ changelog index) is single-column, and forcing the sidebar tracks onto it
69
+ would squeeze its only item — <main> — into the 17.5rem track. */
67
70
  @media (min-width: 1280px) {
68
71
  body[data-blume-ask="open"] [data-blume-toc] {
69
72
  display: none;
70
73
  }
71
74
 
72
- body[data-blume-ask="open"] [data-blume-doc-grid] {
75
+ body[data-blume-ask="open"] [data-blume-doc-grid]:has([data-blume-toc]) {
73
76
  grid-template-columns: 17.5rem minmax(0, 1fr);
74
77
  }
75
78
  }