blume 1.7.0 → 1.7.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.
- package/CHANGELOG.md +18 -0
- package/dist/cli/{chunk-9qs6acpw.js → chunk-12dxjqk7.js} +32 -43
- package/dist/cli/{chunk-9qs6acpw.js.map → chunk-12dxjqk7.js.map} +2 -2
- package/dist/cli/{chunk-s5dsk8bj.js → chunk-196vjxp9.js} +65 -64
- package/dist/cli/chunk-196vjxp9.js.map +13 -0
- package/dist/cli/{chunk-27gtm2ym.js → chunk-2mzebbbz.js} +1 -1
- package/dist/cli/{chunk-s5e5jt53.js → chunk-2z47ypj8.js} +1 -1
- package/dist/cli/{chunk-tnskyrej.js → chunk-30e87n55.js} +15 -24
- package/dist/cli/{chunk-tnskyrej.js.map → chunk-30e87n55.js.map} +2 -2
- package/dist/cli/{chunk-4trphnvy.js → chunk-3w7b2vcx.js} +10 -13
- package/dist/cli/{chunk-4trphnvy.js.map → chunk-3w7b2vcx.js.map} +2 -2
- package/dist/cli/{chunk-v5mm027v.js → chunk-450a7rcr.js} +8 -8
- package/dist/cli/{chunk-v5mm027v.js.map → chunk-450a7rcr.js.map} +1 -1
- package/dist/cli/{chunk-5d4q7121.js → chunk-5n7t497w.js} +142 -134
- package/dist/cli/{chunk-5d4q7121.js.map → chunk-5n7t497w.js.map} +5 -5
- package/dist/cli/{chunk-xv91q4nm.js → chunk-61j18dwk.js} +37 -9
- package/dist/cli/{chunk-xv91q4nm.js.map → chunk-61j18dwk.js.map} +3 -3
- package/dist/cli/{chunk-3r94j3tc.js → chunk-688e0dde.js} +2 -2
- package/dist/cli/{chunk-vxv4x1n8.js → chunk-88by27n5.js} +2 -2
- package/dist/cli/{chunk-cfw6x4rm.js → chunk-8cd8tj54.js} +28 -35
- package/dist/cli/{chunk-cfw6x4rm.js.map → chunk-8cd8tj54.js.map} +2 -2
- package/dist/cli/{chunk-8gnpdsn1.js → chunk-9bkjd11x.js} +2 -2
- package/dist/cli/{chunk-qq9nm3qd.js → chunk-9he6crym.js} +21 -28
- package/dist/cli/{chunk-qq9nm3qd.js.map → chunk-9he6crym.js.map} +2 -2
- package/dist/cli/{chunk-jk1zwka1.js → chunk-aztttvb3.js} +27 -33
- package/dist/cli/{chunk-jk1zwka1.js.map → chunk-aztttvb3.js.map} +2 -2
- package/dist/cli/{chunk-ckh3a410.js → chunk-cvky9gb2.js} +18 -24
- package/dist/cli/{chunk-ckh3a410.js.map → chunk-cvky9gb2.js.map} +2 -2
- package/dist/cli/{chunk-kwx90v78.js → chunk-eevwt1sc.js} +23 -32
- package/dist/cli/{chunk-kwx90v78.js.map → chunk-eevwt1sc.js.map} +2 -2
- package/dist/cli/{chunk-agy5rzxy.js → chunk-ejjx8znq.js} +126 -77
- package/dist/cli/chunk-ejjx8znq.js.map +15 -0
- package/dist/cli/{chunk-ye9zdkgv.js → chunk-exeeb35e.js} +3 -3
- package/dist/cli/{chunk-v2ymm99c.js → chunk-fmceyezb.js} +41 -50
- package/dist/cli/{chunk-v2ymm99c.js.map → chunk-fmceyezb.js.map} +2 -2
- package/dist/cli/{chunk-s102bysw.js → chunk-hdm2dkd2.js} +209 -97
- package/dist/cli/{chunk-s102bysw.js.map → chunk-hdm2dkd2.js.map} +6 -5
- package/dist/cli/{chunk-zr3ygrq3.js → chunk-hs3gbh8p.js} +10 -15
- package/dist/cli/{chunk-zr3ygrq3.js.map → chunk-hs3gbh8p.js.map} +2 -2
- package/dist/cli/{chunk-n0nyat6g.js → chunk-jbj4qhfw.js} +3 -3
- package/dist/cli/{chunk-ev67ycx0.js → chunk-jq5n4avg.js} +1 -1
- package/dist/cli/{chunk-jxkxjsc1.js → chunk-mqb2ka8m.js} +22 -30
- package/dist/cli/{chunk-jxkxjsc1.js.map → chunk-mqb2ka8m.js.map} +2 -2
- package/dist/cli/{chunk-drke6t0h.js → chunk-mt76t7dj.js} +26 -34
- package/dist/cli/{chunk-drke6t0h.js.map → chunk-mt76t7dj.js.map} +2 -2
- package/dist/cli/{chunk-jtb45atp.js → chunk-n9sra6sy.js} +13 -13
- package/dist/cli/{chunk-jtb45atp.js.map → chunk-n9sra6sy.js.map} +1 -1
- package/dist/cli/{chunk-x66c5yjn.js → chunk-ppfvdcd4.js} +2 -2
- package/dist/cli/{chunk-wd27zjcz.js → chunk-q4rae3bg.js} +1 -1
- package/dist/cli/{chunk-pxj10x8y.js → chunk-ra1v2nc2.js} +1 -1
- package/dist/cli/{chunk-y3g15rvv.js → chunk-t3tj0dgr.js} +26 -33
- package/dist/cli/{chunk-y3g15rvv.js.map → chunk-t3tj0dgr.js.map} +2 -2
- package/dist/cli/{chunk-j6pxe0dt.js → chunk-tqa1s0k8.js} +4 -4
- package/dist/cli/{chunk-cbjnx4s8.js → chunk-vh9w1sgp.js} +1 -1
- package/dist/cli/{chunk-0qhq7b8q.js → chunk-vkrsvbr5.js} +13 -17
- package/dist/cli/{chunk-0qhq7b8q.js.map → chunk-vkrsvbr5.js.map} +2 -2
- package/dist/cli/{chunk-sbdqrjbb.js → chunk-vrfp10qk.js} +1 -1
- package/dist/cli/{chunk-ynacq3ev.js → chunk-wjt80jps.js} +27 -40
- package/dist/cli/{chunk-ynacq3ev.js.map → chunk-wjt80jps.js.map} +3 -4
- package/dist/cli/{chunk-18tjv4f7.js → chunk-xhtpx3ff.js} +21 -30
- package/dist/cli/{chunk-18tjv4f7.js.map → chunk-xhtpx3ff.js.map} +2 -2
- package/dist/cli/{chunk-5hs6gb7n.js → chunk-yzhm0j9q.js} +1 -1
- package/dist/cli/index.js +397 -34
- package/dist/cli/index.js.map +12 -4
- package/dist/types/components/layout/nav-utils.d.ts +15 -2
- package/dist/types/core/config-input.d.ts +33 -0
- package/dist/types/core/schema.d.ts +29 -3
- package/docs/08-faq.mdx +21 -0
- package/docs/configuration/ask-ai.mdx +63 -0
- package/docs/content/sources.mdx +2 -2
- package/package.json +1 -1
- package/src/ai/ask.ts +31 -4
- package/src/ai/cors.ts +87 -0
- package/src/astro/generate.ts +3 -0
- package/src/astro/templates.ts +192 -83
- package/src/components/content/YouTube.astro +1 -1
- package/src/components/layout/Header.astro +1 -1
- package/src/components/layout/NavTree.astro +75 -66
- package/src/components/layout/RootLayout.astro +4 -1
- package/src/components/layout/Search.astro +11 -0
- package/src/components/layout/nav-utils.ts +47 -15
- package/src/core/adapter.ts +61 -0
- package/src/core/config-input.ts +33 -0
- package/src/core/schema.ts +61 -0
- package/src/core/sources/assets.ts +162 -26
- package/src/core/sources/notion.ts +60 -1
- package/src/registry/eject.ts +3 -0
- package/src/theme/entry.ts +9 -0
- package/dist/cli/chunk-2aj8ddew.js +0 -72
- package/dist/cli/chunk-2aj8ddew.js.map +0 -10
- package/dist/cli/chunk-4xyggvgf.js +0 -21
- package/dist/cli/chunk-4xyggvgf.js.map +0 -10
- package/dist/cli/chunk-6kzzpsx8.js +0 -26
- package/dist/cli/chunk-6kzzpsx8.js.map +0 -10
- package/dist/cli/chunk-agy5rzxy.js.map +0 -15
- package/dist/cli/chunk-bcy492zc.js +0 -16
- package/dist/cli/chunk-bcy492zc.js.map +0 -10
- package/dist/cli/chunk-btfr9yvw.js +0 -41
- package/dist/cli/chunk-btfr9yvw.js.map +0 -10
- package/dist/cli/chunk-ey89bjj1.js +0 -209
- package/dist/cli/chunk-ey89bjj1.js.map +0 -11
- package/dist/cli/chunk-s5dsk8bj.js.map +0 -13
- package/dist/cli/chunk-vt8fgygt.js +0 -23
- package/dist/cli/chunk-vt8fgygt.js.map +0 -10
- /package/dist/cli/{chunk-27gtm2ym.js.map → chunk-2mzebbbz.js.map} +0 -0
- /package/dist/cli/{chunk-s5e5jt53.js.map → chunk-2z47ypj8.js.map} +0 -0
- /package/dist/cli/{chunk-3r94j3tc.js.map → chunk-688e0dde.js.map} +0 -0
- /package/dist/cli/{chunk-vxv4x1n8.js.map → chunk-88by27n5.js.map} +0 -0
- /package/dist/cli/{chunk-8gnpdsn1.js.map → chunk-9bkjd11x.js.map} +0 -0
- /package/dist/cli/{chunk-ye9zdkgv.js.map → chunk-exeeb35e.js.map} +0 -0
- /package/dist/cli/{chunk-n0nyat6g.js.map → chunk-jbj4qhfw.js.map} +0 -0
- /package/dist/cli/{chunk-ev67ycx0.js.map → chunk-jq5n4avg.js.map} +0 -0
- /package/dist/cli/{chunk-x66c5yjn.js.map → chunk-ppfvdcd4.js.map} +0 -0
- /package/dist/cli/{chunk-wd27zjcz.js.map → chunk-q4rae3bg.js.map} +0 -0
- /package/dist/cli/{chunk-pxj10x8y.js.map → chunk-ra1v2nc2.js.map} +0 -0
- /package/dist/cli/{chunk-j6pxe0dt.js.map → chunk-tqa1s0k8.js.map} +0 -0
- /package/dist/cli/{chunk-cbjnx4s8.js.map → chunk-vh9w1sgp.js.map} +0 -0
- /package/dist/cli/{chunk-sbdqrjbb.js.map → chunk-vrfp10qk.js.map} +0 -0
- /package/dist/cli/{chunk-5hs6gb7n.js.map → chunk-yzhm0j9q.js.map} +0 -0
|
@@ -35,7 +35,10 @@ interface Props {
|
|
|
35
35
|
/**
|
|
36
36
|
* Stable ids for the full sidebar's groups (`navGroupIds`), so panel and
|
|
37
37
|
* fragment ids match across the scoped views the layout renders. Absent,
|
|
38
|
-
* ids fall back to positions within this render.
|
|
38
|
+
* ids fall back to positions within this render. A group missing from the
|
|
39
|
+
* map exists only in this render — a container the tab scoping rebuilt
|
|
40
|
+
* without a nested tab section — so no fragment can serve it: it renders
|
|
41
|
+
* in full instead of being deferred.
|
|
39
42
|
*/
|
|
40
43
|
ids?: Map<NavNode, string>;
|
|
41
44
|
/**
|
|
@@ -63,9 +66,12 @@ const {
|
|
|
63
66
|
const idOf = (node: NavNode, positional: string): string =>
|
|
64
67
|
ids?.get(node) ?? positional;
|
|
65
68
|
|
|
66
|
-
/**
|
|
67
|
-
|
|
68
|
-
|
|
69
|
+
/**
|
|
70
|
+
* The fragment a group's section loads from, when sections are deferred and
|
|
71
|
+
* the group is one the fragments know (in `ids`, or `ids` is absent).
|
|
72
|
+
*/
|
|
73
|
+
const fragmentFor = (node: NavNode, id: string): string | undefined =>
|
|
74
|
+
fragmentBase && (ids?.has(node) ?? true) ? `${fragmentBase}/${id}` : undefined;
|
|
69
75
|
|
|
70
76
|
// Merge over the English defaults so a label missing from a translation (or
|
|
71
77
|
// from a not-yet-regenerated snapshot) still renders instead of coming out
|
|
@@ -150,66 +156,56 @@ const initialId =
|
|
|
150
156
|
strings={n}
|
|
151
157
|
/>
|
|
152
158
|
</div>
|
|
153
|
-
{panels.map((panel) =>
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
159
|
+
{panels.map((panel) => {
|
|
160
|
+
const src = panel.active ? undefined : fragmentFor(panel.node, panel.id);
|
|
161
|
+
return (
|
|
162
|
+
<div
|
|
163
|
+
data-nav-depth={panel.depth}
|
|
164
|
+
data-nav-panel={panel.id}
|
|
165
|
+
data-nav-src={src}
|
|
166
|
+
hidden={panel.id !== initialId}
|
|
167
|
+
>
|
|
168
|
+
{/* The title is the only sidebar link to the section's own page, so
|
|
169
|
+
a routed panel keeps it as a link; without a route the whole row
|
|
170
|
+
becomes the back button. Either way every part of the row is
|
|
171
|
+
interactive. */}
|
|
172
|
+
{panel.route ? (
|
|
173
|
+
<div class="mb-3 flex items-center gap-0.5">
|
|
174
|
+
<button
|
|
175
|
+
aria-label={n.back}
|
|
176
|
+
class="-ml-1 flex shrink-0 items-center justify-center self-stretch rounded-[0.65rem] px-1 text-muted-foreground transition-colors hover:bg-muted hover:text-foreground"
|
|
177
|
+
data-nav-back={panel.parentId}
|
|
178
|
+
type="button"
|
|
179
|
+
>
|
|
180
|
+
<Icon class="rtl:-scale-x-100" name="arrow-left" size={16} />
|
|
181
|
+
</button>
|
|
182
|
+
<a
|
|
183
|
+
aria-current={panel.route === currentRoute ? "page" : undefined}
|
|
184
|
+
class="flex-1 truncate rounded-[0.65rem] px-1 py-1 font-semibold text-foreground text-sm transition-colors hover:bg-muted"
|
|
185
|
+
href={withBase(panel.route)}
|
|
186
|
+
>
|
|
187
|
+
{panel.label}
|
|
188
|
+
</a>
|
|
189
|
+
</div>
|
|
190
|
+
) : (
|
|
166
191
|
<button
|
|
167
|
-
aria-label={n.back}
|
|
168
|
-
class="-ml-1 flex
|
|
192
|
+
aria-label={`${n.back}: ${panel.label}`}
|
|
193
|
+
class="-ml-1 mb-3 flex w-full items-center gap-1.5 rounded-[0.65rem] p-1 text-left font-semibold text-foreground text-sm transition-colors hover:bg-muted"
|
|
169
194
|
data-nav-back={panel.parentId}
|
|
170
195
|
type="button"
|
|
171
196
|
>
|
|
172
|
-
<Icon
|
|
197
|
+
<Icon
|
|
198
|
+
class="shrink-0 text-muted-foreground rtl:-scale-x-100"
|
|
199
|
+
name="arrow-left"
|
|
200
|
+
size={16}
|
|
201
|
+
/>
|
|
202
|
+
<span class="flex-1 truncate">{panel.label}</span>
|
|
173
203
|
</button>
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
{panel.label}
|
|
180
|
-
</a>
|
|
181
|
-
</div>
|
|
182
|
-
) : (
|
|
183
|
-
<button
|
|
184
|
-
aria-label={`${n.back}: ${panel.label}`}
|
|
185
|
-
class="-ml-1 mb-3 flex w-full items-center gap-1.5 rounded-[0.65rem] p-1 text-left font-semibold text-foreground text-sm transition-colors hover:bg-muted"
|
|
186
|
-
data-nav-back={panel.parentId}
|
|
187
|
-
type="button"
|
|
188
|
-
>
|
|
189
|
-
<Icon
|
|
190
|
-
class="shrink-0 text-muted-foreground rtl:-scale-x-100"
|
|
191
|
-
name="arrow-left"
|
|
192
|
-
size={16}
|
|
193
|
-
/>
|
|
194
|
-
<span class="flex-1 truncate">{panel.label}</span>
|
|
195
|
-
</button>
|
|
196
|
-
)}
|
|
197
|
-
{/* An inactive panel's contents are deferred (fetched into this
|
|
198
|
-
element on drill-in) when fragments are on; otherwise the
|
|
199
|
-
build-time cache renders them once. */}
|
|
200
|
-
{panel.active ? (
|
|
201
|
-
<Self
|
|
202
|
-
currentRoute={currentRoute}
|
|
203
|
-
depth={1}
|
|
204
|
-
fragmentBase={fragmentBase}
|
|
205
|
-
idPrefix={panel.id}
|
|
206
|
-
ids={ids}
|
|
207
|
-
items={panel.children}
|
|
208
|
-
root={false}
|
|
209
|
-
strings={n}
|
|
210
|
-
/>
|
|
211
|
-
) : fragmentBase ? null : (
|
|
212
|
-
<NavTreeCache node={panel.node} variant={`${panel.id}|${n.back}|${n.deprecated}`}>
|
|
204
|
+
)}
|
|
205
|
+
{/* An inactive panel's contents are deferred (fetched into this
|
|
206
|
+
element on drill-in) when it has a fragment; otherwise the
|
|
207
|
+
build-time cache renders them once. */}
|
|
208
|
+
{panel.active ? (
|
|
213
209
|
<Self
|
|
214
210
|
currentRoute={currentRoute}
|
|
215
211
|
depth={1}
|
|
@@ -220,10 +216,23 @@ const initialId =
|
|
|
220
216
|
root={false}
|
|
221
217
|
strings={n}
|
|
222
218
|
/>
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
219
|
+
) : src ? null : (
|
|
220
|
+
<NavTreeCache node={panel.node} variant={`${panel.id}|${n.back}|${n.deprecated}`}>
|
|
221
|
+
<Self
|
|
222
|
+
currentRoute={currentRoute}
|
|
223
|
+
depth={1}
|
|
224
|
+
fragmentBase={fragmentBase}
|
|
225
|
+
idPrefix={panel.id}
|
|
226
|
+
ids={ids}
|
|
227
|
+
items={panel.children}
|
|
228
|
+
root={false}
|
|
229
|
+
strings={n}
|
|
230
|
+
/>
|
|
231
|
+
</NavTreeCache>
|
|
232
|
+
)}
|
|
233
|
+
</div>
|
|
234
|
+
);
|
|
235
|
+
})}
|
|
227
236
|
</blume-nav>
|
|
228
237
|
) : (
|
|
229
238
|
<ul class="m-0 list-none space-y-px p-0">
|
|
@@ -362,11 +371,11 @@ const initialId =
|
|
|
362
371
|
</summary>
|
|
363
372
|
<div
|
|
364
373
|
class="space-y-0.5 border-border border-l pl-3"
|
|
365
|
-
data-nav-src={open ? undefined : fragmentFor(id)}
|
|
374
|
+
data-nav-src={open ? undefined : fragmentFor(item, id)}
|
|
366
375
|
>
|
|
367
376
|
{/* A closed group's children are deferred (fetched into this
|
|
368
|
-
element on first open) when
|
|
369
|
-
{!open &&
|
|
377
|
+
element on first open) when it has a fragment. */}
|
|
378
|
+
{!open && fragmentFor(item, id) ? null : active ? (
|
|
370
379
|
<Self
|
|
371
380
|
currentRoute={currentRoute}
|
|
372
381
|
depth={depth + 1}
|
|
@@ -407,7 +407,10 @@ const sidebar = sidebarForRoute(
|
|
|
407
407
|
navigation.root
|
|
408
408
|
);
|
|
409
409
|
// Stable group ids over the full tree, so the scoped view above names its
|
|
410
|
-
// panels and deferred fragments the same way every other page does.
|
|
410
|
+
// panels and deferred fragments the same way every other page does. A
|
|
411
|
+
// container the scoping rebuilt is not in the map, and NavTree renders it in
|
|
412
|
+
// full rather than deferring it to a fragment that would render the full
|
|
413
|
+
// tree's version.
|
|
411
414
|
const navIds = navGroupIds(navigation.sidebar);
|
|
412
415
|
const activeTab = currentTabForRoute(
|
|
413
416
|
navigation.tabs,
|
|
@@ -445,6 +445,7 @@ const kbd = "rounded border border-border bg-muted px-1 py-0.5 font-mono";
|
|
|
445
445
|
this.dialog.addEventListener("keydown", (event) =>
|
|
446
446
|
this.onKeydown(event)
|
|
447
447
|
);
|
|
448
|
+
this.dialog.addEventListener("close", () => this.unlockPageScroll());
|
|
448
449
|
this.dialog.addEventListener("click", (event) => {
|
|
449
450
|
if (event.target === this.dialog) {
|
|
450
451
|
this.dialog.close();
|
|
@@ -454,6 +455,7 @@ const kbd = "rounded border border-border bg-muted px-1 py-0.5 font-mono";
|
|
|
454
455
|
|
|
455
456
|
disconnectedCallback() {
|
|
456
457
|
document.removeEventListener("keydown", this.#onDocumentKeydown);
|
|
458
|
+
this.unlockPageScroll();
|
|
457
459
|
}
|
|
458
460
|
|
|
459
461
|
isField(target: EventTarget | null): boolean {
|
|
@@ -473,6 +475,7 @@ const kbd = "rounded border border-border bg-muted px-1 py-0.5 font-mono";
|
|
|
473
475
|
return;
|
|
474
476
|
}
|
|
475
477
|
this.dialog.showModal();
|
|
478
|
+
this.lockPageScroll();
|
|
476
479
|
this.input.focus();
|
|
477
480
|
this.input.select();
|
|
478
481
|
if (!this.loaded) {
|
|
@@ -498,6 +501,14 @@ const kbd = "rounded border border-border bg-muted px-1 py-0.5 font-mono";
|
|
|
498
501
|
this.render();
|
|
499
502
|
}
|
|
500
503
|
|
|
504
|
+
lockPageScroll() {
|
|
505
|
+
document.documentElement.setAttribute("data-blume-search-dialog-open", "");
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
unlockPageScroll() {
|
|
509
|
+
document.documentElement.removeAttribute("data-blume-search-dialog-open");
|
|
510
|
+
}
|
|
511
|
+
|
|
501
512
|
onKeydown(event: KeyboardEvent) {
|
|
502
513
|
if (event.key === "ArrowDown") {
|
|
503
514
|
event.preventDefault();
|
|
@@ -151,6 +151,15 @@ const isTabSection = (node: NavNode, tabPaths: Set<string>): boolean => {
|
|
|
151
151
|
* instead of duplicating each tab as a sidebar group. A container left empty by
|
|
152
152
|
* this pruning is dropped too, so no bare heading is stranded. The root tab
|
|
153
153
|
* spans everything, so it never removes anything.
|
|
154
|
+
*
|
|
155
|
+
* A group with no tab section anywhere beneath it is kept as the same object:
|
|
156
|
+
* the stable group ids (`navGroupIds`) and the build-time subtree cache are
|
|
157
|
+
* both keyed by node identity, so a copy would lose its id — its collapsed
|
|
158
|
+
* fragment was then requested by a positional name no route serves — and
|
|
159
|
+
* re-render on every page. A container that did lose a section is rebuilt,
|
|
160
|
+
* so it has no id: the deferred fragments render from the full tree, which
|
|
161
|
+
* would put the section back, so `NavTree` renders such a container in full
|
|
162
|
+
* instead (its untouched children still defer by their own ids).
|
|
154
163
|
*/
|
|
155
164
|
const withoutTabSections = (
|
|
156
165
|
nodes: NavNode[],
|
|
@@ -173,10 +182,15 @@ const withoutTabSections = (
|
|
|
173
182
|
continue;
|
|
174
183
|
}
|
|
175
184
|
if (item.kind === "group") {
|
|
176
|
-
// A container left empty by pruning is dropped, so no bare heading is
|
|
177
|
-
// stranded.
|
|
178
185
|
const children = prune(item.children);
|
|
179
|
-
if (
|
|
186
|
+
if (
|
|
187
|
+
children.length === item.children.length &&
|
|
188
|
+
children.every((child, index) => child === item.children[index])
|
|
189
|
+
) {
|
|
190
|
+
kept.push(item);
|
|
191
|
+
} else if (children.length > 0) {
|
|
192
|
+
// A container left empty by pruning is dropped, so no bare heading
|
|
193
|
+
// is stranded.
|
|
180
194
|
kept.push({ ...item, children });
|
|
181
195
|
}
|
|
182
196
|
} else {
|
|
@@ -280,27 +294,45 @@ export interface NavVariant {
|
|
|
280
294
|
navigation: Navigation;
|
|
281
295
|
}
|
|
282
296
|
|
|
297
|
+
/**
|
|
298
|
+
* The locale whose code must not appear as a fragment URL segment: the
|
|
299
|
+
* default locale while its URL prefix is hidden. Astro's i18n routing 404s any
|
|
300
|
+
* page URL carrying that locale's code as a segment (it expects the default
|
|
301
|
+
* locale to be unprefixed), so its trees are keyed `default` instead, the same
|
|
302
|
+
* way its page routes drop the prefix. `null` when every locale is prefixed
|
|
303
|
+
* or the site is single-locale.
|
|
304
|
+
*/
|
|
305
|
+
export const hiddenDefaultLocale = (
|
|
306
|
+
i18n: { defaultLocale: string; hideDefaultLocalePrefix: boolean } | null
|
|
307
|
+
): string | null => (i18n?.hideDefaultLocalePrefix ? i18n.defaultLocale : null);
|
|
308
|
+
|
|
283
309
|
/**
|
|
284
310
|
* Every navigation tree the runtime data holds — the default, each locale's,
|
|
285
311
|
* and each archived version's per locale — keyed the way the deferred
|
|
286
312
|
* sidebar fragments' URLs are (`/blume-nav/<version>/<locale>/…`). An
|
|
287
313
|
* unlocalized version tree is keyed by `""` in the data; it maps to
|
|
288
|
-
* `default` here
|
|
314
|
+
* `default` here, as does the hidden-prefix default locale's (see
|
|
315
|
+
* `hiddenDefaultLocale`), whose current tree is `data.navigation` already.
|
|
289
316
|
*/
|
|
290
|
-
export const navVariants = (
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
317
|
+
export const navVariants = (
|
|
318
|
+
data: {
|
|
319
|
+
navigation: Navigation;
|
|
320
|
+
navigationByLocale: Record<string, Navigation>;
|
|
321
|
+
navigationByVersion: Record<string, Record<string, Navigation>>;
|
|
322
|
+
},
|
|
323
|
+
hiddenDefault: string | null = null
|
|
324
|
+
): NavVariant[] => [
|
|
295
325
|
{ locale: "default", navigation: data.navigation, version: "current" },
|
|
296
|
-
...Object.entries(data.navigationByLocale)
|
|
297
|
-
locale
|
|
298
|
-
navigation
|
|
299
|
-
|
|
300
|
-
|
|
326
|
+
...Object.entries(data.navigationByLocale)
|
|
327
|
+
.filter(([locale]) => locale !== hiddenDefault)
|
|
328
|
+
.map(([locale, navigation]) => ({
|
|
329
|
+
locale,
|
|
330
|
+
navigation,
|
|
331
|
+
version: "current",
|
|
332
|
+
})),
|
|
301
333
|
...Object.entries(data.navigationByVersion).flatMap(([version, byLocale]) =>
|
|
302
334
|
Object.entries(byLocale).map(([locale, navigation]) => ({
|
|
303
|
-
locale: locale
|
|
335
|
+
locale: locale && locale !== hiddenDefault ? locale : "default",
|
|
304
336
|
navigation,
|
|
305
337
|
version,
|
|
306
338
|
}))
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The serializable descriptor an integration factory returns — `posthog({ key })`,
|
|
5
|
+
* `vercel()`, `script({ src })` and their siblings. A descriptor is plain data:
|
|
6
|
+
* the CLI evaluates `blume.config.ts` once, validates what came back, and
|
|
7
|
+
* writes it into the generated project's data snapshot as a JSON literal. That
|
|
8
|
+
* is where the runtime reads it — nothing generated imports the config at
|
|
9
|
+
* request time — so a descriptor can't carry functions, class instances, or
|
|
10
|
+
* anything else JSON drops.
|
|
11
|
+
*
|
|
12
|
+
* Every consumer reads the descriptor instead of switching on a provider name:
|
|
13
|
+
* the head emitter and `blume doctor` branch on `kind`, the generated
|
|
14
|
+
* `.blume/package.json` declares `runtimeDeps`, and the secrets check warns
|
|
15
|
+
* when an entry of `requiredSecrets` is unset.
|
|
16
|
+
*
|
|
17
|
+
* An adapter's verbatim option passthrough is typed as {@link JsonValue} and
|
|
18
|
+
* validated with `z.json()`, so a function, `undefined`, a bigint, or a
|
|
19
|
+
* non-finite number fails config validation with a path instead of vanishing
|
|
20
|
+
* (or throwing) when the snapshot is serialized.
|
|
21
|
+
*/
|
|
22
|
+
export interface AdapterDescriptor<Kind extends string, Options> {
|
|
23
|
+
/** Which integration this is. */
|
|
24
|
+
kind: Kind;
|
|
25
|
+
/** The options the factory was called with, verbatim. */
|
|
26
|
+
options: Options;
|
|
27
|
+
/** Env vars the integration reads at runtime; `blume dev`/`build` warn when one is unset. */
|
|
28
|
+
requiredSecrets: string[];
|
|
29
|
+
/** Extra packages the generated `.blume/package.json` must declare. */
|
|
30
|
+
runtimeDeps: string[];
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* A value JSON can carry unchanged — what an adapter's passthrough options are
|
|
35
|
+
* typed as. Structurally identical to what `z.json()` accepts.
|
|
36
|
+
*/
|
|
37
|
+
export type JsonValue =
|
|
38
|
+
| string
|
|
39
|
+
| number
|
|
40
|
+
| boolean
|
|
41
|
+
| null
|
|
42
|
+
| JsonValue[]
|
|
43
|
+
| { [key: string]: JsonValue };
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* The schema for one descriptor `kind`, validating its `options` with the
|
|
47
|
+
* adapter's own option schema. Members of a `z.discriminatedUnion("kind", …)`.
|
|
48
|
+
*/
|
|
49
|
+
export const adapterDescriptorSchema = <
|
|
50
|
+
Kind extends string,
|
|
51
|
+
Options extends z.ZodType,
|
|
52
|
+
>(
|
|
53
|
+
kind: Kind,
|
|
54
|
+
options: Options
|
|
55
|
+
) =>
|
|
56
|
+
z.strictObject({
|
|
57
|
+
kind: z.literal(kind),
|
|
58
|
+
options,
|
|
59
|
+
requiredSecrets: z.array(z.string()),
|
|
60
|
+
runtimeDeps: z.array(z.string()),
|
|
61
|
+
});
|
package/src/core/config-input.ts
CHANGED
|
@@ -681,6 +681,8 @@ export interface AskSuggestion {
|
|
|
681
681
|
/** Backends that can route an Ask AI request. */
|
|
682
682
|
type AskProviderGateway = "gateway" | "openrouter" | "llmgateway";
|
|
683
683
|
type AskProvider = AskProviderGateway | "inkeep" | "openai-compatible";
|
|
684
|
+
/** How much the model reasons before answering (`ai.ask.reasoning`). */
|
|
685
|
+
type AskReasoning = "none" | "minimal" | "low" | "medium" | "high" | "xhigh";
|
|
684
686
|
|
|
685
687
|
/** How much retrieved documentation each Ask AI question carries. */
|
|
686
688
|
export interface AskRetrievalConfig {
|
|
@@ -715,6 +717,18 @@ export interface AskConfig {
|
|
|
715
717
|
* overrides the built-in preset.
|
|
716
718
|
*/
|
|
717
719
|
baseUrl?: string;
|
|
720
|
+
/**
|
|
721
|
+
* Origins allowed to call the generated endpoint from another site — a
|
|
722
|
+
* marketing page that embeds an ask box, for example — or `"*"` to allow
|
|
723
|
+
* every origin. The route answers preflight requests and names a listed
|
|
724
|
+
* origin on every response, errors included; every other origin stays
|
|
725
|
+
* subject to the browser's same-origin rule. Callers must send the body as
|
|
726
|
+
* JSON with a `content-type: application/json` header, or Astro's cross-site
|
|
727
|
+
* request check rejects the `POST` before the route runs. Only the generated
|
|
728
|
+
* route reads this; an external `endpoint` handles its own CORS and can't be
|
|
729
|
+
* combined with it.
|
|
730
|
+
*/
|
|
731
|
+
cors?: string[];
|
|
718
732
|
/** Turn Ask AI on. Defaults to `false`. */
|
|
719
733
|
enabled?: boolean;
|
|
720
734
|
/**
|
|
@@ -723,6 +737,13 @@ export interface AskConfig {
|
|
|
723
737
|
* limiting, and streaming. Accepts an absolute URL or root-relative path.
|
|
724
738
|
*/
|
|
725
739
|
endpoint?: string;
|
|
740
|
+
/**
|
|
741
|
+
* Static request headers sent to the provider on every call — a
|
|
742
|
+
* caller-identifying header for a shared backend, for example. Values are
|
|
743
|
+
* written into the generated route as literals, so keep secrets in
|
|
744
|
+
* `apiKeyEnv` rather than here.
|
|
745
|
+
*/
|
|
746
|
+
headers?: Record<string, string>;
|
|
726
747
|
/**
|
|
727
748
|
* Extra system-prompt text appended to the built-in instructions — use it
|
|
728
749
|
* for identity, language, or tone. The built-in grounding behavior (answer
|
|
@@ -733,6 +754,18 @@ export interface AskConfig {
|
|
|
733
754
|
model?: string;
|
|
734
755
|
/** Which backend routes the request. Defaults to `gateway`. */
|
|
735
756
|
provider?: AskProvider;
|
|
757
|
+
/**
|
|
758
|
+
* How much the model reasons before answering, from `"none"` to `"xhigh"`.
|
|
759
|
+
* Sent as the backend's own reasoning-effort control: the AI SDK's
|
|
760
|
+
* `reasoning` option on the gateway, `reasoning.effort` on OpenRouter, and
|
|
761
|
+
* `reasoning_effort` on OpenAI-compatible endpoints. The model has to
|
|
762
|
+
* support the level — OpenAI rejects one a model doesn't offer — and the
|
|
763
|
+
* endpoint has to accept the parameter; Inkeep has no reasoning control,
|
|
764
|
+
* so the field is rejected there. Omitted keeps the model's default.
|
|
765
|
+
* `"none"` is the fastest and cheapest for grounded docs Q&A, where the
|
|
766
|
+
* retrieved excerpts carry the answer.
|
|
767
|
+
*/
|
|
768
|
+
reasoning?: AskReasoning;
|
|
736
769
|
/**
|
|
737
770
|
* How much documentation each question carries into the model's prompt.
|
|
738
771
|
* Lower values cut time-to-first-token — which dominates on a self-hosted
|
package/src/core/schema.ts
CHANGED
|
@@ -767,6 +767,19 @@ const searchConfigSchema = z
|
|
|
767
767
|
}
|
|
768
768
|
});
|
|
769
769
|
|
|
770
|
+
/**
|
|
771
|
+
* The `ai.ask.reasoning` levels: the AI SDK's top-level `reasoning` values
|
|
772
|
+
* minus `provider-default`, which is what omitting the field means.
|
|
773
|
+
*/
|
|
774
|
+
export const askReasoningLevels = [
|
|
775
|
+
"none",
|
|
776
|
+
"minimal",
|
|
777
|
+
"low",
|
|
778
|
+
"medium",
|
|
779
|
+
"high",
|
|
780
|
+
"xhigh",
|
|
781
|
+
] as const;
|
|
782
|
+
|
|
770
783
|
/** Ask AI backends. `gateway` (default) routes through the Vercel AI Gateway. */
|
|
771
784
|
export const askAiProviders = [
|
|
772
785
|
"gateway",
|
|
@@ -872,17 +885,43 @@ const aiConfigSchema = z.strictObject({
|
|
|
872
885
|
// Base URL of the backend. Required for `openai-compatible` only when no
|
|
873
886
|
// external endpoint is supplied; for named providers it overrides the preset.
|
|
874
887
|
baseUrl: z.url().optional(),
|
|
888
|
+
// Origins allowed to call the generated `/api/ask` from another site (a
|
|
889
|
+
// marketing page that embeds an ask box, say), or `"*"` for every
|
|
890
|
+
// origin. Each URL is reduced to its origin so a trailing slash or path
|
|
891
|
+
// can't defeat the exact match the route performs. Read by the
|
|
892
|
+
// generated route only; an external `endpoint` owns its own CORS.
|
|
893
|
+
cors: z
|
|
894
|
+
.array(
|
|
895
|
+
z.union([
|
|
896
|
+
z.literal("*"),
|
|
897
|
+
z
|
|
898
|
+
.url({ protocol: /^https?$/u })
|
|
899
|
+
.transform((value) => new URL(value).origin),
|
|
900
|
+
])
|
|
901
|
+
)
|
|
902
|
+
.optional(),
|
|
875
903
|
enabled: z.boolean().default(false),
|
|
876
904
|
// Optional external endpoint for projects that keep their docs static
|
|
877
905
|
// and host Ask AI in an existing backend. Absolute URLs and root-relative
|
|
878
906
|
// paths are both valid; the built-in request/stream contract is unchanged.
|
|
879
907
|
endpoint: askEndpointSchema.optional(),
|
|
908
|
+
// Static request headers the generated endpoint sends the provider on
|
|
909
|
+
// every call (a caller-identifying header for a shared backend, say).
|
|
910
|
+
// Values are inlined into the generated route as literals, so the API
|
|
911
|
+
// key stays in `apiKeyEnv`; these are for non-secret metadata.
|
|
912
|
+
headers: z.record(z.string(), z.string()).optional(),
|
|
880
913
|
// Extra system-prompt text (identity, language, tone) appended to the
|
|
881
914
|
// built-in instructions, so the grounding contract — answer from the
|
|
882
915
|
// retrieved excerpts, cite pages as Markdown links — stays intact.
|
|
883
916
|
instructions: z.string().trim().min(1).optional(),
|
|
884
917
|
model: z.string().default("openai/gpt-5.5"),
|
|
885
918
|
provider: z.enum(askAiProviders).default("gateway"),
|
|
919
|
+
// How much the model reasons before answering, sent as the backend's
|
|
920
|
+
// own reasoning-effort control (see `askEndpointTemplate`). Omitted
|
|
921
|
+
// keeps the provider's default; `none` is the fastest and cheapest for
|
|
922
|
+
// grounded docs Q&A, where the excerpts carry the answer. Not for
|
|
923
|
+
// Inkeep, which has no such control (refined below).
|
|
924
|
+
reasoning: z.enum(askReasoningLevels).optional(),
|
|
886
925
|
// How much documentation each question carries. Injected characters are
|
|
887
926
|
// the dominant term in time-to-first-token on a self-hosted backend, so
|
|
888
927
|
// these trade recall for latency. No zod defaults here: only what the
|
|
@@ -921,6 +960,27 @@ const aiConfigSchema = z.strictObject({
|
|
|
921
960
|
path: ["baseUrl"],
|
|
922
961
|
});
|
|
923
962
|
}
|
|
963
|
+
// `cors` configures the generated route, which an external `endpoint`
|
|
964
|
+
// replaces; accepting both would silently do nothing.
|
|
965
|
+
if (value.cors && value.endpoint) {
|
|
966
|
+
ctx.addIssue({
|
|
967
|
+
code: z.ZodIssueCode.custom,
|
|
968
|
+
message:
|
|
969
|
+
"ai.ask.cors only applies to the generated route; with ai.ask.endpoint, CORS is that backend's job.",
|
|
970
|
+
path: ["cors"],
|
|
971
|
+
});
|
|
972
|
+
}
|
|
973
|
+
// Inkeep runs its own QA pipeline behind an OpenAI-compatible endpoint
|
|
974
|
+
// with no reasoning control; a level would only reach it as an
|
|
975
|
+
// unsupported `reasoning_effort`, so refuse it up front.
|
|
976
|
+
if (value.provider === "inkeep" && value.reasoning) {
|
|
977
|
+
ctx.addIssue({
|
|
978
|
+
code: z.ZodIssueCode.custom,
|
|
979
|
+
message:
|
|
980
|
+
'ai.ask.reasoning is not supported when provider is "inkeep".',
|
|
981
|
+
path: ["reasoning"],
|
|
982
|
+
});
|
|
983
|
+
}
|
|
924
984
|
})
|
|
925
985
|
.optional(),
|
|
926
986
|
/**
|
|
@@ -1073,6 +1133,7 @@ const navigationConfigSchema = z.strictObject({
|
|
|
1073
1133
|
});
|
|
1074
1134
|
|
|
1075
1135
|
export type AskAiProvider = (typeof askAiProviders)[number];
|
|
1136
|
+
export type AskReasoning = (typeof askReasoningLevels)[number];
|
|
1076
1137
|
export type AskAiConfig = NonNullable<z.infer<typeof aiConfigSchema>["ask"]>;
|
|
1077
1138
|
export { openInChatProviders } from "./open-in-chat.ts";
|
|
1078
1139
|
export type { OpenInChatProvider } from "./open-in-chat.ts";
|