blume 0.6.0 → 0.6.2

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 (65) hide show
  1. package/dist/cli/index.js +6070 -5718
  2. package/dist/cli/index.js.map +41 -40
  3. package/dist/types/core/config-input.d.ts +749 -0
  4. package/dist/types/core/config.d.ts +126 -3
  5. package/dist/types/core/schema.d.ts +10 -27
  6. package/dist/types/core/sources/types.d.ts +6 -0
  7. package/dist/types/index.d.ts +2 -1
  8. package/docs/advanced/changelog.mdx +10 -2
  9. package/docs/configuration/index.mdx +0 -2
  10. package/docs/content/syntax.mdx +4 -8
  11. package/package.json +1 -1
  12. package/src/astro/generate.ts +59 -24
  13. package/src/astro/markdown-negotiation.ts +12 -3
  14. package/src/astro/templates.ts +94 -12
  15. package/src/cli/commands/build.ts +26 -1
  16. package/src/cli/commands/dev.ts +30 -14
  17. package/src/cli/commands/doctor.ts +35 -7
  18. package/src/cli/commands/sync.ts +14 -2
  19. package/src/cli/dev-lock.ts +40 -10
  20. package/src/cli/env.ts +5 -1
  21. package/src/components/content/Update.astro +12 -2
  22. package/src/components/content/changelog-element.ts +62 -0
  23. package/src/components/islands/ask-ai.tsx +3 -1
  24. package/src/components/islands/hooks.ts +5 -1
  25. package/src/components/layout/Header.astro +10 -2
  26. package/src/components/layout/NavSelector.astro +5 -3
  27. package/src/components/layout/PageLayout.astro +2 -1
  28. package/src/components/layout/ReferenceLayout.astro +1 -0
  29. package/src/components/layout/RootLayout.astro +47 -10
  30. package/src/components/layout/Search.astro +8 -3
  31. package/src/components/layout/nav-utils.ts +7 -3
  32. package/src/core/config-input.ts +923 -0
  33. package/src/core/config.ts +126 -3
  34. package/src/core/i18n.ts +6 -5
  35. package/src/core/links.ts +16 -1
  36. package/src/core/meta.ts +112 -52
  37. package/src/core/navigation.ts +15 -5
  38. package/src/core/project-graph.ts +68 -2
  39. package/src/core/schema.ts +8 -14
  40. package/src/core/sources/assets.ts +21 -5
  41. package/src/core/sources/cache.ts +19 -1
  42. package/src/core/sources/github-releases.ts +9 -3
  43. package/src/core/sources/mdx-remote.ts +14 -4
  44. package/src/core/sources/normalize.ts +13 -1
  45. package/src/core/sources/notion.ts +43 -7
  46. package/src/core/sources/resolve.ts +44 -1
  47. package/src/core/sources/sanity.ts +9 -3
  48. package/src/core/sources/types.ts +6 -0
  49. package/src/deploy/adapter-output.ts +82 -0
  50. package/src/deploy/rss.ts +3 -1
  51. package/src/index.ts +1 -1
  52. package/src/markdown/code-title.ts +11 -4
  53. package/src/markdown/index.ts +28 -30
  54. package/src/markdown/math.ts +3 -2
  55. package/src/markdown/package-commands.ts +13 -0
  56. package/src/og/card.ts +3 -1
  57. package/src/openapi/model.ts +2 -1
  58. package/src/openapi/parse.ts +9 -1
  59. package/src/openapi/references.ts +11 -1
  60. package/src/openapi/render-mdx.ts +30 -3
  61. package/src/openapi/source.ts +3 -1
  62. package/src/registry/eject.ts +21 -14
  63. package/src/search/documents.ts +4 -1
  64. package/src/theme/entry.ts +10 -3
  65. package/src/theme/icons.ts +7 -11
@@ -5,6 +5,7 @@
5
5
  // <details>/<summary> like the language switcher.
6
6
  import type { NavSelector } from "../../core/types.ts";
7
7
  import Icon from "../Icon.astro";
8
+ import { isUnderPath } from "./nav-utils.ts";
8
9
 
9
10
  interface Props {
10
11
  selector: NavSelector;
@@ -13,12 +14,13 @@ interface Props {
13
14
 
14
15
  const { selector, route } = Astro.props;
15
16
 
16
- // The active item is the deepest path that prefixes the current route, falling
17
- // back to the first item so the summary always shows something meaningful.
17
+ // The active item is the deepest path the current route sits under (on a path
18
+ // boundary, so `/api` never claims `/api-reference` routes), falling back to
19
+ // the first item so the summary always shows something meaningful.
18
20
  const active =
19
21
  selector.items.find((item) => item.path === route) ??
20
22
  selector.items
21
- .filter((item) => route.startsWith(item.path))
23
+ .filter((item) => isUnderPath(route, item.path))
22
24
  .toSorted((a, b) => b.path.length - a.path.length)[0] ??
23
25
  selector.items[0];
24
26
 
@@ -32,6 +32,7 @@ import Favicon from "./Favicon.astro";
32
32
  import Fonts from "./Fonts.astro";
33
33
  import { bannerInitScript, themeInitScript } from "./head-scripts.ts";
34
34
  import Header from "./Header.astro";
35
+ import { isUnderPath } from "./nav-utils.ts";
35
36
 
36
37
  interface Props {
37
38
  site: { title: string; description?: string };
@@ -209,7 +210,7 @@ const bannerScript = banner?.dismissible
209
210
  <a
210
211
  aria-current={
211
212
  route === tab.path ||
212
- (tab.path !== "/" && route.startsWith(tab.path))
213
+ (tab.path !== "/" && isUnderPath(route, tab.path))
213
214
  ? "page"
214
215
  : undefined
215
216
  }
@@ -92,6 +92,7 @@ const bannerScript = banner?.dismissible
92
92
  <body class="bg-background font-sans text-foreground antialiased">
93
93
  <Banner banner={banner} />
94
94
  <Header
95
+ hasDrawer={false}
95
96
  hasSidebar={false}
96
97
  logo={logo}
97
98
  navigation={navigation}
@@ -23,6 +23,7 @@ import {
23
23
  findBreadcrumbs,
24
24
  flattenPages,
25
25
  getPagination,
26
+ isUnderPath,
26
27
  sidebarForRoute,
27
28
  } from "./nav-utils.ts";
28
29
  import NavTree from "./NavTree.astro";
@@ -126,6 +127,12 @@ interface Props {
126
127
  clientData?: BlumeClientData | null;
127
128
  /** Table-of-contents settings (`toc` config): visibility + heading range. */
128
129
  toc?: { enabled: boolean; maxLevel: number; minLevel: number };
130
+ /**
131
+ * Content-column preset. `"bare"` (the generated changelog index) drops both
132
+ * the sidebar and the table of contents and centers a single wide column;
133
+ * everything else uses the standard sidebar + content + TOC grid.
134
+ */
135
+ contentLayout?: "default" | "bare";
129
136
  }
130
137
 
131
138
  const {
@@ -169,6 +176,7 @@ const {
169
176
  layout = {},
170
177
  clientData,
171
178
  toc = { enabled: true, maxLevel: 3, minLevel: 2 },
179
+ contentLayout = "default",
172
180
  } = Astro.props;
173
181
 
174
182
  // Serialized once for island hooks; `<` escaped so content can't break the tag.
@@ -205,16 +213,28 @@ const searchLocale =
205
213
  // API operation pages own a two-column body (docs + request panel) and their own
206
214
  // right rail, so they drop the table of contents and widen the content column.
207
215
  const isApiOperation = pageType === "openapi-operation";
216
+ // A "bare" page (the changelog index) is a standalone landing: no sidebar, no
217
+ // TOC, just a wider centered column of content.
218
+ const isBare = contentLayout === "bare";
219
+ const showSidebar = !isBare;
220
+ const showToc = !(isApiOperation || isBare);
208
221
  const tocHeadings =
209
- toc.enabled && !isApiOperation
222
+ toc.enabled && showToc
210
223
  ? headings.filter((h) => h.depth >= toc.minLevel && h.depth <= toc.maxLevel)
211
224
  : [];
212
- const gridClass = isApiOperation
213
- ? "lg:grid-cols-[17.5rem_minmax(0,1fr)]"
214
- : "lg:grid-cols-[17.5rem_minmax(0,1fr)] xl:grid-cols-[17.5rem_minmax(0,1fr)_17.5rem]";
215
- const articleClass = isApiOperation
216
- ? "prose max-w-none"
217
- : "prose mx-auto max-w-[42rem]";
225
+ let gridClass =
226
+ "lg:grid-cols-[17.5rem_minmax(0,1fr)] xl:grid-cols-[17.5rem_minmax(0,1fr)_17.5rem]";
227
+ if (isBare) {
228
+ gridClass = "";
229
+ } else if (isApiOperation) {
230
+ gridClass = "lg:grid-cols-[17.5rem_minmax(0,1fr)]";
231
+ }
232
+ let articleClass = "prose mx-auto max-w-[42rem]";
233
+ if (isBare) {
234
+ articleClass = "prose mx-auto max-w-[54rem]";
235
+ } else if (isApiOperation) {
236
+ articleClass = "prose max-w-none";
237
+ }
218
238
  const pageTitle = page.title ? `${page.title} - ${site.title}` : site.title;
219
239
  const description = page.description ?? site.description;
220
240
 
@@ -347,6 +367,8 @@ const bannerScript = banner?.dismissible
347
367
  class:list={["mx-auto grid grid-cols-1 items-start", gridClass]}
348
368
  data-blume-doc-grid
349
369
  >
370
+ {
371
+ showSidebar && (
350
372
  <aside
351
373
  aria-label="Primary"
352
374
  class="fixed top-[var(--blume-drawer-top,4rem)] start-0 z-[35] h-[calc(100dvh-var(--blume-drawer-top,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:top-16 lg:z-auto lg:h-[calc(100dvh-4rem)] lg:w-auto lg:max-w-none lg:translate-x-0! lg:border-e-0 lg:bg-transparent lg:px-4"
@@ -406,7 +428,7 @@ const bannerScript = banner?.dismissible
406
428
  <a
407
429
  aria-current={
408
430
  page.route === tab.path ||
409
- (tab.path !== "/" && page.route.startsWith(tab.path))
431
+ (tab.path !== "/" && isUnderPath(page.route, tab.path))
410
432
  ? "page"
411
433
  : undefined
412
434
  }
@@ -438,6 +460,8 @@ const bannerScript = banner?.dismissible
438
460
  }
439
461
  </nav>
440
462
  </aside>
463
+ )
464
+ }
441
465
  <main class="px-6 pt-6 pb-10 lg:px-8 xl:px-10" id="blume-content">
442
466
  <BreadcrumbsSlot crumbs={crumbs} wide={isApiOperation} />
443
467
  <TableOfContentsSlot
@@ -469,7 +493,7 @@ const bannerScript = banner?.dismissible
469
493
  }
470
494
  </main>
471
495
  {
472
- !isApiOperation && (
496
+ showToc && (
473
497
  <aside
474
498
  aria-label={strings.toc.title}
475
499
  class="sticky top-16 hidden h-[calc(100dvh-4rem)] overflow-y-auto px-4 pt-6 pb-10 text-sm xl:block"
@@ -578,7 +602,20 @@ const bannerScript = banner?.dismissible
578
602
  button.setAttribute("aria-label", "Copy code");
579
603
  button.innerHTML = svg("copy");
580
604
  button.addEventListener("click", async () => {
581
- const text = pre.querySelector("code")?.textContent ?? "";
605
+ const code = pre.querySelector("code");
606
+ let text = code?.textContent ?? "";
607
+ // Twoslash nests each hover popup's type signature and docs inside
608
+ // the <code>; copying textContent verbatim would interleave them
609
+ // with the source. Strip the popups from a clone first.
610
+ if (code?.querySelector(".twoslash-popup-container")) {
611
+ const clone = code.cloneNode(true) as HTMLElement;
612
+ for (const popup of clone.querySelectorAll(
613
+ ".twoslash-popup-container"
614
+ )) {
615
+ popup.remove();
616
+ }
617
+ text = clone.textContent ?? "";
618
+ }
582
619
  try {
583
620
  await navigator.clipboard.writeText(text);
584
621
  } catch {
@@ -133,7 +133,7 @@ const kbd = "rounded border border-border bg-muted px-1 py-0.5 font-mono";
133
133
  <script
134
134
  data-blume-search-popular
135
135
  is:inline
136
- set:html={JSON.stringify(popular)}
136
+ set:html={JSON.stringify(popular).replaceAll("<", "\\u003c")}
137
137
  type="application/json"
138
138
  />
139
139
 
@@ -190,6 +190,7 @@ const kbd = "rounded border border-border bg-muted px-1 py-0.5 font-mono";
190
190
  selectables: Selectable[] = [];
191
191
  selectedIndex = -1;
192
192
  activeSection: string | null = null;
193
+ renderGeneration = 0;
193
194
  previewOn = true;
194
195
  devOnlyMsg = "Search is available in the production build.";
195
196
  noResultsMsg = "No results found.";
@@ -339,6 +340,8 @@ const kbd = "rounded border border-border bg-muted px-1 py-0.5 font-mono";
339
340
 
340
341
  async render() {
341
342
  const query = this.input.value.trim();
343
+ this.renderGeneration += 1;
344
+ const generation = this.renderGeneration;
342
345
  this.selectables = [];
343
346
  this.selectedIndex = -1;
344
347
  this.results.replaceChildren();
@@ -364,8 +367,10 @@ const kbd = "rounded border border-border bg-muted px-1 py-0.5 font-mono";
364
367
  locale: localeFilter,
365
368
  section: this.activeSection ?? undefined,
366
369
  });
367
- // A later keystroke may have superseded this query mid-await.
368
- if (this.input.value.trim() !== query) {
370
+ // Any newer render — a keystroke, a section pill, a locale toggle —
371
+ // supersedes this one mid-await, even for the same query text;
372
+ // appending the stale hits would duplicate rows and desync selection.
373
+ if (generation !== this.renderGeneration) {
369
374
  return;
370
375
  }
371
376
 
@@ -71,9 +71,13 @@ export const findBreadcrumbs = (nodes: NavNode[], route: string): Crumb[] => {
71
71
  return search(nodes, []) ?? [];
72
72
  };
73
73
 
74
- /** Whether `route` is the section root `base` or nested beneath it. */
75
- const isUnderPath = (route: string, base: string): boolean =>
76
- route === base || route.startsWith(`${base}/`);
74
+ /**
75
+ * Whether `route` is the section root `base` or nested beneath it. Requires a
76
+ * path boundary, so `/api-reference` is not under `/api`. The root `/` spans
77
+ * every route.
78
+ */
79
+ export const isUnderPath = (route: string, base: string): boolean =>
80
+ base === "/" || route === base || route.startsWith(`${base}/`);
77
81
 
78
82
  /**
79
83
  * The tab whose `path` is the longest prefix of `route`, mirroring the header's