blume 0.2.0 → 0.3.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 (71) hide show
  1. package/dist/cli/index.js +1921 -560
  2. package/dist/cli/index.js.map +36 -24
  3. package/dist/types/core/data.d.ts +16 -0
  4. package/dist/types/core/define-components.d.ts +9 -2
  5. package/dist/types/core/diagnostics.d.ts +5 -0
  6. package/dist/types/core/schema.d.ts +26 -502
  7. package/dist/types/core/types.d.ts +2 -2
  8. package/docs/02-deployment.mdx +21 -2
  9. package/docs/advanced/custom-pages.mdx +63 -1
  10. package/docs/configuration/ai.mdx +20 -3
  11. package/docs/configuration/customization.mdx +103 -5
  12. package/docs/configuration/index.mdx +13 -0
  13. package/docs/configuration/seo.mdx +5 -0
  14. package/docs/content/islands.mdx +73 -0
  15. package/docs/content/navigation.mdx +25 -0
  16. package/docs/index.mdx +3 -12
  17. package/docs/reference/cli.mdx +42 -0
  18. package/package.json +3 -1
  19. package/src/ai/ask-context.ts +131 -0
  20. package/src/ai/ask-data.ts +25 -0
  21. package/src/astro/component-slots.ts +165 -0
  22. package/src/astro/generate.ts +132 -13
  23. package/src/astro/integration.ts +59 -0
  24. package/src/astro/pages.ts +5 -12
  25. package/src/astro/templates.ts +92 -44
  26. package/src/blume-modules.d.ts +25 -0
  27. package/src/cli/commands/build.ts +186 -1
  28. package/src/cli/commands/check.ts +62 -0
  29. package/src/cli/commands/dev.ts +21 -1
  30. package/src/cli/commands/doctor.ts +23 -6
  31. package/src/cli/commands/init.ts +163 -15
  32. package/src/cli/commands/validate.ts +16 -2
  33. package/src/cli/index.ts +15 -0
  34. package/src/cli/internal-error.ts +63 -0
  35. package/src/cli/log.ts +30 -1
  36. package/src/cli/prepare.ts +17 -3
  37. package/src/cli/required-secrets.ts +44 -0
  38. package/src/components/BlumePage.astro +107 -0
  39. package/src/components/index.ts +3 -3
  40. package/src/components/islands/ask-ai.tsx +15 -1
  41. package/src/components/islands/hooks.ts +188 -0
  42. package/src/components/layout/Empty.astro +6 -0
  43. package/src/components/layout/Header.astro +24 -39
  44. package/src/components/layout/Logo.astro +50 -0
  45. package/src/components/layout/NavSelector.astro +75 -0
  46. package/src/components/layout/PageLayout.astro +38 -2
  47. package/src/components/layout/RootLayout.astro +70 -4
  48. package/src/components/layout/hydration-hint.ts +30 -0
  49. package/src/components/layout/overrides.ts +6 -4
  50. package/src/components/props.ts +68 -0
  51. package/src/core/builtin-tags.ts +39 -0
  52. package/src/core/component-diagnostics.ts +44 -0
  53. package/src/core/component-overrides.ts +478 -0
  54. package/src/core/config.ts +8 -0
  55. package/src/core/data.ts +14 -0
  56. package/src/core/define-components.ts +9 -2
  57. package/src/core/diagnostics.ts +90 -1
  58. package/src/core/graph.ts +7 -0
  59. package/src/core/nav-diagnostics.ts +205 -0
  60. package/src/core/project-graph.ts +40 -1
  61. package/src/core/schema.ts +28 -96
  62. package/src/core/sources/normalize.ts +51 -0
  63. package/src/core/types.ts +2 -2
  64. package/src/deploy/redirects.ts +43 -0
  65. package/src/migrate/mintlify/config.ts +1 -176
  66. package/src/migrate/starlight/config.ts +0 -4
  67. package/src/og/card.ts +163 -38
  68. package/src/registry/eject.ts +39 -9
  69. package/src/registry/registry.ts +166 -0
  70. package/src/runtime/index.ts +61 -0
  71. package/src/vite-env.d.ts +14 -0
@@ -1,6 +1,7 @@
1
1
  ---
2
2
  import { EN_UI } from "../../core/i18n-ui.ts";
3
3
  import type { UIStrings } from "../../core/i18n-ui.ts";
4
+ import type { BlumeClientData } from "../../core/data.ts";
4
5
  import type {
5
6
  Heading,
6
7
  LocaleSwitchOption,
@@ -12,6 +13,7 @@ import { buildStructuredData } from "../../seo/jsonld.ts";
12
13
  import Analytics from "./Analytics.astro";
13
14
  import Banner from "./Banner.astro";
14
15
  import Breadcrumbs from "./Breadcrumbs.astro";
16
+ import Empty from "./Empty.astro";
15
17
  import Favicon from "./Favicon.astro";
16
18
  import Fonts from "./Fonts.astro";
17
19
  import { bannerInitScript, themeInitScript } from "./head-scripts.ts";
@@ -108,9 +110,20 @@ interface Props {
108
110
  /**
109
111
  * User layout-slot overrides from `components.ts` (`defineComponents`). Each
110
112
  * key replaces the matching built-in; unknown keys are ignored. Wired slots:
111
- * `Header`, `Sidebar`, `Breadcrumbs`, `TableOfContents`, `Pagination`.
113
+ * `Header`, `Logo`, `Search`, `Sidebar`, `MobileNav`, `Breadcrumbs`,
114
+ * `TableOfContents`, `Pagination`, `Feedback`, plus the content-injection slots
115
+ * `PageHeader`, `PageFooter`, and `Footer` (which have no built-in and render
116
+ * only when overridden). `Layout` (replacing this whole component) is wired in
117
+ * the generated page templates, not here.
112
118
  */
113
119
  layout?: Record<string, ComponentOverride>;
120
+ /**
121
+ * Snapshot serialized for React island hooks (`blume/hooks`). Rendered into a
122
+ * JSON `<script>` when present; omitted for zero-JS pages that need no islands.
123
+ */
124
+ clientData?: BlumeClientData | null;
125
+ /** Table-of-contents settings (`toc` config): visibility + heading range. */
126
+ toc?: { enabled: boolean; maxLevel: number; minLevel: number };
114
127
  }
115
128
 
116
129
  const {
@@ -152,21 +165,44 @@ const {
152
165
  xDefault,
153
166
  localeSwitch,
154
167
  layout = {},
168
+ clientData,
169
+ toc = { enabled: true, maxLevel: 3, minLevel: 2 },
155
170
  } = Astro.props;
156
171
 
172
+ // Serialized once for island hooks; `<` escaped so content can't break the tag.
173
+ const clientDataJson = clientData
174
+ ? JSON.stringify(clientData).replaceAll("<", "\\u003c")
175
+ : null;
176
+
157
177
  // Layout-slot overrides: render the user's component when configured, else the
158
178
  // built-in. Each override receives the same props as the component it replaces.
179
+ // `Logo` and `Search` are resolved inside the header (forwarded via `layout`).
159
180
  const HeaderSlot = resolveSlot(layout.Header, Header);
160
181
  const SidebarSlot = resolveSlot(layout.Sidebar, NavTree);
161
182
  const BreadcrumbsSlot = resolveSlot(layout.Breadcrumbs, Breadcrumbs);
162
183
  const TableOfContentsSlot = resolveSlot(layout.TableOfContents, TableOfContents);
163
184
  const PaginationSlot = resolveSlot(layout.Pagination, Pagination);
185
+ // The mobile drawer reuses the sidebar unless a distinct `MobileNav` is given.
186
+ const MobileNavSlot = layout.MobileNav
187
+ ? resolveSlot(layout.MobileNav, NavTree)
188
+ : null;
189
+ // Content-injection slots with no built-in: the `Empty` fallback renders nothing
190
+ // when unconfigured, so these are safe to render unconditionally.
191
+ const FooterSlot = resolveSlot(layout.Footer, Empty);
192
+ const PageHeaderSlot = resolveSlot(layout.PageHeader, Empty);
193
+ const PageFooterSlot = resolveSlot(layout.PageFooter, Empty);
194
+ // The "Was this page helpful?" rating (rendered only when `feedback` is on).
195
+ const FeedbackSlot = resolveSlot(layout.Feedback, PageFeedback);
164
196
 
165
197
  const strings = ui ?? EN_UI;
166
198
  // Filter search to the active language only when the site is multi-locale.
167
199
  const searchLocale =
168
200
  localeSwitch && localeSwitch.length > 1 ? locale : undefined;
169
- const tocHeadings = headings.filter((h) => h.depth >= 2 && h.depth <= 3);
201
+ // TOC entries: the configured heading range, or none when the TOC is disabled
202
+ // (an empty list makes TableOfContents render nothing).
203
+ const tocHeadings = toc.enabled
204
+ ? headings.filter((h) => h.depth >= toc.minLevel && h.depth <= toc.maxLevel)
205
+ : [];
170
206
  const pageTitle = page.title ? `${page.title} - ${site.title}` : site.title;
171
207
  const description = page.description ?? site.description;
172
208
 
@@ -282,6 +318,7 @@ const bannerScript = banner?.dismissible
282
318
  <Banner banner={banner} />
283
319
  <HeaderSlot
284
320
  askEnabled={askEnabled}
321
+ layout={layout}
285
322
  localeSwitch={localeSwitch}
286
323
  logo={logo}
287
324
  navigation={navigation}
@@ -300,7 +337,20 @@ const bannerScript = banner?.dismissible
300
337
  class="fixed top-16 start-0 z-[35] h-[calc(100dvh-4rem)] w-64 max-w-[80vw] -translate-x-[105%] overflow-y-auto border-border border-e bg-background px-5 pt-4 pb-6 transition-transform rtl:translate-x-[105%] [:where([data-blume-nav-open])_&]:translate-x-0! lg:sticky lg:z-auto lg:w-auto lg:max-w-none lg:translate-x-0! lg:border-e-0 lg:bg-transparent lg:px-4"
301
338
  >
302
339
  <nav>
303
- <SidebarSlot currentRoute={page.route} items={sidebar} />
340
+ {
341
+ MobileNavSlot ? (
342
+ <>
343
+ <div class="lg:hidden">
344
+ <MobileNavSlot currentRoute={page.route} items={sidebar} />
345
+ </div>
346
+ <div class="hidden lg:block">
347
+ <SidebarSlot currentRoute={page.route} items={sidebar} />
348
+ </div>
349
+ </>
350
+ ) : (
351
+ <SidebarSlot currentRoute={page.route} items={sidebar} />
352
+ )
353
+ }
304
354
  </nav>
305
355
  </aside>
306
356
  <main class="min-w-0 px-6 pt-6 pb-10 lg:px-8 xl:px-10" id="blume-content">
@@ -310,9 +360,11 @@ const bannerScript = banner?.dismissible
310
360
  title={strings.toc.title}
311
361
  variant="mobile"
312
362
  />
363
+ <PageHeaderSlot headings={tocHeadings} page={page} route={page.route} />
313
364
  <article class="prose mx-auto max-w-[42rem]" dir={contentDir}>
314
365
  <slot />
315
366
  </article>
367
+ <PageFooterSlot headings={tocHeadings} page={page} route={page.route} />
316
368
  {
317
369
  formattedLastModified && (
318
370
  <p class="mx-auto mt-10 max-w-[42rem] text-muted-foreground text-sm">
@@ -320,7 +372,7 @@ const bannerScript = banner?.dismissible
320
372
  </p>
321
373
  )
322
374
  }
323
- {feedback && <PageFeedback strings={strings.feedback} />}
375
+ {feedback && <FeedbackSlot strings={strings.feedback} />}
324
376
  <PaginationSlot next={next} prev={prev} strings={strings.page} />
325
377
  </main>
326
378
  <aside
@@ -344,6 +396,17 @@ const bannerScript = banner?.dismissible
344
396
  />
345
397
  </aside>
346
398
  </div>
399
+ <FooterSlot navigation={navigation} site={site} ui={strings} />
400
+ {
401
+ clientDataJson && (
402
+ <script
403
+ id="blume-client-data"
404
+ is:inline
405
+ set:html={clientDataJson}
406
+ type="application/json"
407
+ />
408
+ )
409
+ }
347
410
  <button
348
411
  aria-label="Close navigation"
349
412
  class="fixed inset-0 z-[30] hidden cursor-pointer border-0 bg-black/40 [:where([data-blume-nav-open])_&]:block lg:hidden"
@@ -358,6 +421,9 @@ const bannerScript = banner?.dismissible
358
421
  // Registers the <blume-toc> custom element: scrollspy for the table of
359
422
  // contents, highlighting the section currently in view as you scroll.
360
423
  import "./toc-element.ts";
424
+ // Dev-only: adds a friendly hint after a React island hydration mismatch.
425
+ // Tree-shaken out of production builds.
426
+ import "./hydration-hint.ts";
361
427
 
362
428
  const svg = (name: string) =>
363
429
  `<svg xmlns="http://www.w3.org/2000/svg" width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">${icons[name]}</svg>`;
@@ -0,0 +1,30 @@
1
+ // Dev-only: when React reports a hydration mismatch for an island, follow it
2
+ // with a friendly Blume hint pointing at the islands guide. Guarded by
3
+ // `import.meta.env.DEV`, so the whole block is tree-shaken out of production
4
+ // builds (the module ships no runtime code there).
5
+
6
+ const HINT =
7
+ "[blume] A hydration mismatch was detected in an island. Make sure props passed from `.astro`/MDX are serializable and the component renders the same output on the server and client (avoid `Date.now()`, `Math.random()`, `window` at render time). See https://useblume.dev/docs/content/islands";
8
+
9
+ const PATTERNS = [
10
+ /hydrat/iu,
11
+ /did not match/iu,
12
+ /server-rendered html/iu,
13
+ /server rendered html/iu,
14
+ ];
15
+
16
+ if (import.meta.env.DEV && typeof window !== "undefined") {
17
+ const original = console.error.bind(console);
18
+ let shown = false;
19
+ console.error = (...args: unknown[]) => {
20
+ original(...args);
21
+ if (shown) {
22
+ return;
23
+ }
24
+ const text = args.map(String).join(" ");
25
+ if (PATTERNS.some((pattern) => pattern.test(text))) {
26
+ shown = true;
27
+ original(HINT);
28
+ }
29
+ };
30
+ }
@@ -4,10 +4,12 @@ import type { ComponentOverride } from "../../core/define-components.ts";
4
4
  * Resolve a layout-slot override to the component Astro should render, falling
5
5
  * back to Blume's built-in when no usable override is configured.
6
6
  *
7
- * An override may be a bare component reference or an `IslandDescriptor`
8
- * (`{ component, client }`); only the component is used here. String-path
9
- * overrides can't be imported at render time, so they fall back to the built-in
10
- * for now (imported components are the recommended, type-safe form).
7
+ * By the time values reach here, the generated `components.ts` has already turned
8
+ * path strings and hydrated (`client:*`) overrides into imported components /
9
+ * wrappers, so the runtime map holds real components. This handles the remaining
10
+ * cases: a bare component reference, an `IslandDescriptor` (`{ component }`,
11
+ * unwrapped to its component), and — as a safety net for overrides that couldn't
12
+ * be resolved at build time — a leftover string, which falls back to the built-in.
11
13
  */
12
14
  export const resolveSlot = <T>(
13
15
  override: ComponentOverride | undefined,
@@ -0,0 +1,68 @@
1
+ // Public prop types for Blume's built-in components, so users can type their
2
+ // overrides and wrappers: `import type { CalloutProps } from "blume/components"`.
3
+ //
4
+ // Each type is derived straight from the component with Astro's `ComponentProps`,
5
+ // so it can never drift from the real props. `typeof import(...)` keeps these
6
+ // type-only (no runtime import); the file-level disable is because that syntax is
7
+ // the only way to reference an `.astro` component's type without a value import.
8
+ // oxlint-disable typescript/consistent-type-imports
9
+ import type { ComponentProps } from "astro/types";
10
+
11
+ export type AccordionProps = ComponentProps<
12
+ typeof import("./content/Accordion.astro").default
13
+ >;
14
+ export type AccordionItemProps = ComponentProps<
15
+ typeof import("./content/AccordionItem.astro").default
16
+ >;
17
+ export type BadgeProps = ComponentProps<
18
+ typeof import("./content/Badge.astro").default
19
+ >;
20
+ export type CalloutProps = ComponentProps<
21
+ typeof import("./content/Callout.astro").default
22
+ >;
23
+ export type CardProps = ComponentProps<
24
+ typeof import("./content/Card.astro").default
25
+ >;
26
+ export type CardGroupProps = ComponentProps<
27
+ typeof import("./content/CardGroup.astro").default
28
+ >;
29
+ export type CodeGroupProps = ComponentProps<
30
+ typeof import("./content/CodeGroup.astro").default
31
+ >;
32
+ export type ColumnProps = ComponentProps<
33
+ typeof import("./content/Column.astro").default
34
+ >;
35
+ export type ColumnsProps = ComponentProps<
36
+ typeof import("./content/Columns.astro").default
37
+ >;
38
+ export type ExpandableProps = ComponentProps<
39
+ typeof import("./content/Expandable.astro").default
40
+ >;
41
+ export type FrameProps = ComponentProps<
42
+ typeof import("./content/Frame.astro").default
43
+ >;
44
+ export type PanelProps = ComponentProps<
45
+ typeof import("./content/Panel.astro").default
46
+ >;
47
+ export type PromptProps = ComponentProps<
48
+ typeof import("./content/Prompt.astro").default
49
+ >;
50
+ export type StepProps = ComponentProps<
51
+ typeof import("./content/Step.astro").default
52
+ >;
53
+ export type StepsProps = ComponentProps<
54
+ typeof import("./content/Steps.astro").default
55
+ >;
56
+ export type TabProps = ComponentProps<
57
+ typeof import("./content/Tab.astro").default
58
+ >;
59
+ export type TabsProps = ComponentProps<
60
+ typeof import("./content/Tabs.astro").default
61
+ >;
62
+ export type TileProps = ComponentProps<
63
+ typeof import("./content/Tile.astro").default
64
+ >;
65
+ export type TooltipProps = ComponentProps<
66
+ typeof import("./content/Tooltip.astro").default
67
+ >;
68
+ export type IconProps = ComponentProps<typeof import("./Icon.astro").default>;
@@ -0,0 +1,39 @@
1
+ /**
2
+ * The MDX component tags Blume ships (the keys of the catch-all's component map,
3
+ * plus composed sub-parts like `Color.Item`/`Tree.File`). Used by the
4
+ * missing-component diagnostic to tell an unknown `<Tag>` from a built-in. Keep
5
+ * in sync with the component map in `astro/templates.ts`.
6
+ */
7
+ export const BUILTIN_MDX_TAGS = new Set<string>([
8
+ "Accordion",
9
+ "AccordionItem",
10
+ "AutoTypeTable",
11
+ "Badge",
12
+ "Callout",
13
+ "Card",
14
+ "CardGroup",
15
+ "CodeBlock",
16
+ "CodeGroup",
17
+ "Color",
18
+ "Column",
19
+ "Columns",
20
+ "Component",
21
+ "Diff",
22
+ "Expandable",
23
+ "FileTree",
24
+ "Frame",
25
+ "GithubInfo",
26
+ "Icon",
27
+ "Math",
28
+ "Panel",
29
+ "Prompt",
30
+ "Step",
31
+ "Steps",
32
+ "Tab",
33
+ "Tabs",
34
+ "Tile",
35
+ "Tooltip",
36
+ "Tree",
37
+ "TypeTable",
38
+ "Visibility",
39
+ ]);
@@ -0,0 +1,44 @@
1
+ import { BUILTIN_MDX_TAGS } from "./builtin-tags.ts";
2
+ import type { Diagnostic, PageRecord } from "./types.ts";
3
+
4
+ /** `CardGroup` → `card-group`, matching registry item names. */
5
+ const toKebab = (tag: string): string =>
6
+ tag
7
+ .replaceAll(/(?<lower>[a-z0-9])(?<upper>[A-Z])/gu, "$<lower>-$<upper>")
8
+ .toLowerCase();
9
+
10
+ /**
11
+ * Warn when an `.mdx` page uses a `<Component>` tag that resolves to nothing —
12
+ * a built-in, an island, or a `components.ts` override — so a typo surfaces as a
13
+ * friendly diagnostic (with a `blume add` hint where one exists) instead of a raw
14
+ * MDX "X is not defined" build error. `extraTags` are the project's own known
15
+ * components (islands + overrides); `registryNames` gates the install hint.
16
+ */
17
+ export const validateUsedComponents = (
18
+ pages: PageRecord[],
19
+ extraTags: Set<string>,
20
+ registryNames: Set<string>
21
+ ): Diagnostic[] => {
22
+ const diagnostics: Diagnostic[] = [];
23
+ const seen = new Set<string>();
24
+ for (const page of pages) {
25
+ for (const tag of page.componentsUsed ?? []) {
26
+ if (BUILTIN_MDX_TAGS.has(tag) || extraTags.has(tag) || seen.has(tag)) {
27
+ continue;
28
+ }
29
+ seen.add(tag);
30
+ const name = toKebab(tag);
31
+ const suggestion = registryNames.has(name)
32
+ ? `Run \`blume add ${name}\` to install it, or register <${tag}> in components.ts (mdx).`
33
+ : `Register <${tag}> in components.ts (mdx), or add an islands/${tag}.tsx component.`;
34
+ diagnostics.push({
35
+ code: "BLUME_UNKNOWN_COMPONENT",
36
+ file: page.sourcePath ?? page.id,
37
+ message: `<${tag}> is used in ${page.route} but isn't a known component.`,
38
+ severity: "warning",
39
+ suggestion,
40
+ });
41
+ }
42
+ }
43
+ return diagnostics;
44
+ };