blume 2.2.0 → 2.2.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 +15 -0
- package/dist/cli/{chunk-xgq4eytj.js → chunk-080mfch6.js} +15 -15
- package/dist/cli/{chunk-br0n8n3e.js → chunk-1ch3ct3r.js} +14 -14
- package/dist/cli/{chunk-49r370mb.js → chunk-2ssh60qh.js} +6 -6
- package/dist/cli/{chunk-1cm0h819.js → chunk-34wap3ed.js} +3 -3
- package/dist/cli/{chunk-fqy4n3q2.js → chunk-40vk8kgf.js} +7 -7
- package/dist/cli/{chunk-96snx8yy.js → chunk-487b95qy.js} +3 -3
- package/dist/cli/{chunk-kkrteb15.js → chunk-4zxh25hc.js} +2 -2
- package/dist/cli/{chunk-cn3dy3hz.js → chunk-69rjf786.js} +5 -5
- package/dist/cli/{chunk-dmx6cezx.js → chunk-9h8mh2tk.js} +3 -3
- package/dist/cli/{chunk-dmx6cezx.js.map → chunk-9h8mh2tk.js.map} +2 -2
- package/dist/cli/{chunk-t5xx74rd.js → chunk-9jqmq5hv.js} +6 -6
- package/dist/cli/{chunk-8b489v1b.js → chunk-aafhwx74.js} +1 -1
- package/dist/cli/{chunk-2nr29y95.js → chunk-b680sv30.js} +3 -3
- package/dist/cli/{chunk-rkqae7kh.js → chunk-beb0z983.js} +16 -16
- package/dist/cli/{chunk-th0pgaj5.js → chunk-c5bjxq8v.js} +2 -2
- package/dist/cli/{chunk-wehv26ry.js → chunk-e1n41w9m.js} +3 -3
- package/dist/cli/{chunk-frk2wdq1.js → chunk-etckvbrh.js} +14 -14
- package/dist/cli/{chunk-ahc5gc9h.js → chunk-f260jdrn.js} +5 -5
- package/dist/cli/{chunk-05f21r8h.js → chunk-fnxgtjeq.js} +1 -1
- package/dist/cli/{chunk-4rftmttv.js → chunk-gwy55cx3.js} +2 -2
- package/dist/cli/{chunk-4rftmttv.js.map → chunk-gwy55cx3.js.map} +2 -2
- package/dist/cli/{chunk-vnbvhgmd.js → chunk-gxhcf2hs.js} +2 -2
- package/dist/cli/{chunk-bjk964fp.js → chunk-h0qwdbem.js} +2 -2
- package/dist/cli/{chunk-1rkxzg3n.js → chunk-hktq4j5r.js} +15 -12
- package/dist/cli/{chunk-1rkxzg3n.js.map → chunk-hktq4j5r.js.map} +3 -3
- package/dist/cli/{chunk-enk76qd5.js → chunk-hn3g2b89.js} +9 -9
- package/dist/cli/{chunk-w6e455pz.js → chunk-j4nfgn2j.js} +1 -1
- package/dist/cli/{chunk-as09pafp.js → chunk-k45sq16q.js} +4 -4
- package/dist/cli/{chunk-as09pafp.js.map → chunk-k45sq16q.js.map} +3 -3
- package/dist/cli/{chunk-p8zg3a2h.js → chunk-kmswjxj9.js} +3 -3
- package/dist/cli/{chunk-xt0qvz1t.js → chunk-nqna8dzp.js} +17 -17
- package/dist/cli/{chunk-dyk3r7jw.js → chunk-p834rkcn.js} +5 -5
- package/dist/cli/{chunk-dpcahcrb.js → chunk-ppa6mg4d.js} +6 -6
- package/dist/cli/{chunk-79bzzkx4.js → chunk-q1wwrxzw.js} +2 -2
- package/dist/cli/{chunk-m9bp2hc1.js → chunk-qcyxkj44.js} +1 -1
- package/dist/cli/{chunk-dm4mg9bj.js → chunk-r5nm9w0c.js} +740 -685
- package/dist/cli/{chunk-dm4mg9bj.js.map → chunk-r5nm9w0c.js.map} +8 -6
- package/dist/cli/{chunk-a3ydrqny.js → chunk-rtvgmg2j.js} +2 -2
- package/dist/cli/{chunk-c69tw29j.js → chunk-t2h8pwy1.js} +4 -4
- package/dist/cli/{chunk-vzy9rh6y.js → chunk-x60e15st.js} +17 -17
- package/dist/cli/{chunk-mrvh4c88.js → chunk-x7bh6d9j.js} +4 -4
- package/dist/cli/{chunk-7tekknev.js → chunk-ydqzkhjn.js} +6 -6
- package/dist/cli/{chunk-gprwej7h.js → chunk-yg8ckc3g.js} +2 -2
- package/dist/cli/{chunk-8f8xw4x2.js → chunk-zxd5jrgx.js} +14 -4
- package/dist/cli/{chunk-8f8xw4x2.js.map → chunk-zxd5jrgx.js.map} +3 -3
- package/dist/cli/index.js +18 -18
- package/dist/types/core/config-input.d.ts +37 -5
- package/dist/types/core/data.d.ts +6 -2
- package/dist/types/core/mcp-clients.d.ts +47 -0
- package/dist/types/core/nav-diagnostics.d.ts +7 -0
- package/dist/types/core/schema.d.ts +55 -4
- package/dist/types/ratelimit/index.d.ts +2 -0
- package/dist/types/ratelimit/memory.d.ts +1 -1
- package/dist/types/ratelimit/schema.d.ts +40 -0
- package/dist/types/ratelimit/unkey.d.ts +48 -0
- package/docs/02-deployment.mdx +1 -0
- package/docs/configuration/index.mdx +5 -4
- package/docs/configuration/rate-limiting.mdx +20 -1
- package/docs/content/i18n.mdx +1 -1
- package/docs/discoverability/mcp.mdx +40 -8
- package/package.json +1 -1
- package/skills/blume-migrate/SKILL.md +2 -2
- package/skills/blume-migrate/references/docusaurus.md +2 -2
- package/skills/blume-migrate/references/gitbook.md +2 -2
- package/skills/blume-migrate/references/github-wiki.md +2 -2
- package/skills/blume-migrate/references/jekyll.md +2 -2
- package/skills/blume-migrate/references/mkdocs.md +2 -2
- package/skills/blume-migrate/references/nextra.md +3 -3
- package/skills/blume-migrate/references/readme.md +1 -1
- package/skills/blume-migrate/references/redocly.md +2 -2
- package/skills/blume-migrate/references/vitepress.md +2 -2
- package/skills/blume-migrate/references/vuepress.md +1 -1
- package/skills/blume-migrate/scripts/github-wiki-codemod.mjs +1 -1
- package/src/astro/generate.ts +15 -5
- package/src/astro/templates.ts +4 -4
- package/src/cli/required-secrets.ts +0 -1
- package/src/components/layout/PageActions.astro +119 -34
- package/src/components/layout/RootLayout.astro +18 -2
- package/src/components/layout/SiteFooter.astro +31 -8
- package/src/core/config-input.ts +38 -4
- package/src/core/data.ts +10 -3
- package/src/core/mcp-clients.ts +87 -0
- package/src/core/nav-diagnostics.ts +25 -0
- package/src/core/schema.ts +93 -3
- package/src/openapi/proxy.ts +5 -1
- package/src/ratelimit/index.ts +2 -0
- package/src/ratelimit/memory.ts +1 -1
- package/src/ratelimit/runtime.ts +81 -0
- package/src/ratelimit/schema.ts +2 -0
- package/src/ratelimit/unkey.ts +61 -0
- package/src/registry/registry.ts +1 -1
- /package/dist/cli/{chunk-xgq4eytj.js.map → chunk-080mfch6.js.map} +0 -0
- /package/dist/cli/{chunk-br0n8n3e.js.map → chunk-1ch3ct3r.js.map} +0 -0
- /package/dist/cli/{chunk-49r370mb.js.map → chunk-2ssh60qh.js.map} +0 -0
- /package/dist/cli/{chunk-1cm0h819.js.map → chunk-34wap3ed.js.map} +0 -0
- /package/dist/cli/{chunk-fqy4n3q2.js.map → chunk-40vk8kgf.js.map} +0 -0
- /package/dist/cli/{chunk-96snx8yy.js.map → chunk-487b95qy.js.map} +0 -0
- /package/dist/cli/{chunk-kkrteb15.js.map → chunk-4zxh25hc.js.map} +0 -0
- /package/dist/cli/{chunk-cn3dy3hz.js.map → chunk-69rjf786.js.map} +0 -0
- /package/dist/cli/{chunk-t5xx74rd.js.map → chunk-9jqmq5hv.js.map} +0 -0
- /package/dist/cli/{chunk-8b489v1b.js.map → chunk-aafhwx74.js.map} +0 -0
- /package/dist/cli/{chunk-2nr29y95.js.map → chunk-b680sv30.js.map} +0 -0
- /package/dist/cli/{chunk-rkqae7kh.js.map → chunk-beb0z983.js.map} +0 -0
- /package/dist/cli/{chunk-th0pgaj5.js.map → chunk-c5bjxq8v.js.map} +0 -0
- /package/dist/cli/{chunk-wehv26ry.js.map → chunk-e1n41w9m.js.map} +0 -0
- /package/dist/cli/{chunk-frk2wdq1.js.map → chunk-etckvbrh.js.map} +0 -0
- /package/dist/cli/{chunk-ahc5gc9h.js.map → chunk-f260jdrn.js.map} +0 -0
- /package/dist/cli/{chunk-05f21r8h.js.map → chunk-fnxgtjeq.js.map} +0 -0
- /package/dist/cli/{chunk-vnbvhgmd.js.map → chunk-gxhcf2hs.js.map} +0 -0
- /package/dist/cli/{chunk-bjk964fp.js.map → chunk-h0qwdbem.js.map} +0 -0
- /package/dist/cli/{chunk-enk76qd5.js.map → chunk-hn3g2b89.js.map} +0 -0
- /package/dist/cli/{chunk-w6e455pz.js.map → chunk-j4nfgn2j.js.map} +0 -0
- /package/dist/cli/{chunk-p8zg3a2h.js.map → chunk-kmswjxj9.js.map} +0 -0
- /package/dist/cli/{chunk-xt0qvz1t.js.map → chunk-nqna8dzp.js.map} +0 -0
- /package/dist/cli/{chunk-dyk3r7jw.js.map → chunk-p834rkcn.js.map} +0 -0
- /package/dist/cli/{chunk-dpcahcrb.js.map → chunk-ppa6mg4d.js.map} +0 -0
- /package/dist/cli/{chunk-79bzzkx4.js.map → chunk-q1wwrxzw.js.map} +0 -0
- /package/dist/cli/{chunk-m9bp2hc1.js.map → chunk-qcyxkj44.js.map} +0 -0
- /package/dist/cli/{chunk-a3ydrqny.js.map → chunk-rtvgmg2j.js.map} +0 -0
- /package/dist/cli/{chunk-c69tw29j.js.map → chunk-t2h8pwy1.js.map} +0 -0
- /package/dist/cli/{chunk-vzy9rh6y.js.map → chunk-x60e15st.js.map} +0 -0
- /package/dist/cli/{chunk-mrvh4c88.js.map → chunk-x7bh6d9j.js.map} +0 -0
- /package/dist/cli/{chunk-7tekknev.js.map → chunk-ydqzkhjn.js.map} +0 -0
- /package/dist/cli/{chunk-gprwej7h.js.map → chunk-yg8ckc3g.js.map} +0 -0
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
---
|
|
2
2
|
import { EN_UI } from "../../core/i18n-ui.ts";
|
|
3
3
|
import type { UIStrings } from "../../core/i18n-ui.ts";
|
|
4
|
+
import {
|
|
5
|
+
isBuiltInMcpClient,
|
|
6
|
+
mcpClients,
|
|
7
|
+
mcpMenuRows,
|
|
8
|
+
} from "../../core/mcp-clients.ts";
|
|
9
|
+
import type { McpMenuEntry } from "../../core/mcp-clients.ts";
|
|
4
10
|
import { NEW_TAB_ATTRS } from "../../core/new-tab.ts";
|
|
5
11
|
import { openInChatProviders } from "../../core/open-in-chat.ts";
|
|
6
12
|
import type { OpenInChatProvider } from "../../core/open-in-chat.ts";
|
|
@@ -18,6 +24,11 @@ interface Props {
|
|
|
18
24
|
mcpUrl?: string | null;
|
|
19
25
|
/** Display name of the MCP server (defaults to the site title). */
|
|
20
26
|
mcpName?: string | null;
|
|
27
|
+
/**
|
|
28
|
+
* "Connect to MCP" clients to list after "Copy server URL", in order, with
|
|
29
|
+
* custom labels resolved; omitting the prop lists every built-in client.
|
|
30
|
+
*/
|
|
31
|
+
mcpMenu?: readonly McpMenuEntry[];
|
|
21
32
|
/**
|
|
22
33
|
* "Open in chat" providers to list, in order; an empty list hides the
|
|
23
34
|
* action, and omitting the prop shows every provider.
|
|
@@ -40,6 +51,7 @@ const {
|
|
|
40
51
|
exportEpub,
|
|
41
52
|
mcpUrl,
|
|
42
53
|
mcpName,
|
|
54
|
+
mcpMenu,
|
|
43
55
|
openInChat,
|
|
44
56
|
strings,
|
|
45
57
|
divider = true,
|
|
@@ -89,6 +101,10 @@ const providers = (openInChat ?? openInChatProviders).map((key) => ({
|
|
|
89
101
|
// keep that separator only while a configured list preserves v0 first.
|
|
90
102
|
const chatSeparator = providers.length > 1 && providers[0]?.key === "v0";
|
|
91
103
|
|
|
104
|
+
// `agents.mcp.clients` picks and orders the clients after "Copy server URL",
|
|
105
|
+
// with a rule between the commands to copy and the install links.
|
|
106
|
+
const mcpRows = mcpMenuRows(mcpMenu ?? mcpClients);
|
|
107
|
+
|
|
92
108
|
// The block renders once, for the right rail. Below `xl` the script moves it
|
|
93
109
|
// into the mobile "On this page" dropdown, and the global styles below the
|
|
94
110
|
// markup restyle it there from that container.
|
|
@@ -99,11 +115,17 @@ const rowClass =
|
|
|
99
115
|
"flex w-full items-center gap-2.5 py-1.5 text-start text-muted-foreground text-sm transition-colors hover:text-foreground";
|
|
100
116
|
|
|
101
117
|
// Items inside the floating popovers keep menu affordances (padding + hover
|
|
102
|
-
// background).
|
|
118
|
+
// background). Their icons sit in a box one line tall, so a label that wraps
|
|
119
|
+
// keeps them beside its first line.
|
|
103
120
|
const menuRowClass =
|
|
104
|
-
"flex w-full items-
|
|
121
|
+
"flex w-full items-start gap-2.5 rounded-md px-2 py-1.5 text-start text-muted-foreground text-sm transition-colors hover:bg-muted hover:text-foreground";
|
|
122
|
+
const menuIconClass = "flex h-lh shrink-0 items-center";
|
|
123
|
+
// At most out to the rail's edge, past its padding (`--blume-rail-inset`, set
|
|
124
|
+
// by the layout), less 2px so the border never lands on the edge: the rail
|
|
125
|
+
// scrolls, so it clips anything wider. Longer labels wrap instead. The minimum
|
|
126
|
+
// width gives way to that cap in a narrower rail.
|
|
105
127
|
const panelClass =
|
|
106
|
-
"absolute top-full
|
|
128
|
+
"absolute top-full end-0 z-50 mt-1 w-max [--blume-menu-room:calc(100%+var(--blume-rail-inset,1rem)-2px)] min-w-[min(14rem,var(--blume-menu-room))] max-w-(--blume-menu-room) wrap-anywhere rounded-blume border border-border bg-background p-1 shadow-xl";
|
|
107
129
|
---
|
|
108
130
|
|
|
109
131
|
<div
|
|
@@ -156,13 +178,17 @@ const panelClass =
|
|
|
156
178
|
<div class={panelClass} data-blume-menu>
|
|
157
179
|
{exportPdf && (
|
|
158
180
|
<button class={menuRowClass} data-blume-export-pdf type="button">
|
|
159
|
-
<
|
|
181
|
+
<span class={menuIconClass}>
|
|
182
|
+
<Icon name="file" size={16} />
|
|
183
|
+
</span>
|
|
160
184
|
<span class="flex-1">{a.exportPdf}</span>
|
|
161
185
|
</button>
|
|
162
186
|
)}
|
|
163
187
|
{exportEpub && (
|
|
164
188
|
<button class={menuRowClass} data-blume-export-epub type="button">
|
|
165
|
-
<
|
|
189
|
+
<span class={menuIconClass}>
|
|
190
|
+
<Icon name="book-open" size={16} />
|
|
191
|
+
</span>
|
|
166
192
|
<span class="flex-1" data-blume-epub-label>
|
|
167
193
|
{a.exportEpub}
|
|
168
194
|
</span>
|
|
@@ -198,9 +224,11 @@ const panelClass =
|
|
|
198
224
|
data-open-in={provider.key}
|
|
199
225
|
{...NEW_TAB_ATTRS}
|
|
200
226
|
>
|
|
201
|
-
<span set:html={LOGOS[provider.key]} />
|
|
227
|
+
<span class={menuIconClass} set:html={LOGOS[provider.key]} />
|
|
202
228
|
<span class="flex-1">{provider.label}</span>
|
|
203
|
-
<
|
|
229
|
+
<span class={menuIconClass}>
|
|
230
|
+
<Icon name="external-link" size={13} />
|
|
231
|
+
</span>
|
|
204
232
|
</a>
|
|
205
233
|
</>
|
|
206
234
|
))}
|
|
@@ -225,34 +253,72 @@ const panelClass =
|
|
|
225
253
|
</summary>
|
|
226
254
|
<div class={panelClass} data-blume-menu>
|
|
227
255
|
<button class={menuRowClass} data-mcp-copy-url type="button">
|
|
228
|
-
<
|
|
256
|
+
<span class={menuIconClass}>
|
|
257
|
+
<Icon name="link" size={16} />
|
|
258
|
+
</span>
|
|
229
259
|
<span class="flex-1" data-mcp-url-label>
|
|
230
260
|
{a.copyServerUrl}
|
|
231
261
|
</span>
|
|
232
262
|
</button>
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
{
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
263
|
+
{mcpRows.map(({ client, rule }) => (
|
|
264
|
+
<>
|
|
265
|
+
{rule && <hr class="my-1 border-border border-t" />}
|
|
266
|
+
{client === "claude-code" && (
|
|
267
|
+
<button
|
|
268
|
+
class={menuRowClass}
|
|
269
|
+
data-mcp-copy-claude
|
|
270
|
+
type="button"
|
|
271
|
+
>
|
|
272
|
+
<span class={menuIconClass} set:html={LOGOS.claude} />
|
|
273
|
+
<span class="flex-1" data-mcp-claude-label>
|
|
274
|
+
{a.copyClaudeCode}
|
|
275
|
+
</span>
|
|
276
|
+
</button>
|
|
277
|
+
)}
|
|
278
|
+
{client === "codex" && (
|
|
279
|
+
<button class={menuRowClass} data-mcp-copy-codex type="button">
|
|
280
|
+
<span class={menuIconClass} set:html={LOGOS.codex} />
|
|
281
|
+
<span class="flex-1" data-mcp-codex-label>
|
|
282
|
+
{a.copyCodex}
|
|
283
|
+
</span>
|
|
284
|
+
</button>
|
|
285
|
+
)}
|
|
286
|
+
{client === "cursor" && (
|
|
287
|
+
<a class={menuRowClass} data-mcp-cursor rel="noreferrer">
|
|
288
|
+
<span class={menuIconClass} set:html={LOGOS.cursor} />
|
|
289
|
+
<span class="flex-1">{a.addToCursor}</span>
|
|
290
|
+
<span class={menuIconClass}>
|
|
291
|
+
<Icon name="external-link" size={13} />
|
|
292
|
+
</span>
|
|
293
|
+
</a>
|
|
294
|
+
)}
|
|
295
|
+
{client === "vscode" && (
|
|
296
|
+
<a class={menuRowClass} data-mcp-vscode rel="noreferrer">
|
|
297
|
+
<span class={menuIconClass}>
|
|
298
|
+
<Icon name="code" size={16} />
|
|
299
|
+
</span>
|
|
300
|
+
<span class="flex-1">{a.addToVscode}</span>
|
|
301
|
+
<span class={menuIconClass}>
|
|
302
|
+
<Icon name="external-link" size={13} />
|
|
303
|
+
</span>
|
|
304
|
+
</a>
|
|
305
|
+
)}
|
|
306
|
+
{!isBuiltInMcpClient(client) && (
|
|
307
|
+
<button
|
|
308
|
+
class={menuRowClass}
|
|
309
|
+
data-mcp-copy-command={client.command}
|
|
310
|
+
type="button"
|
|
311
|
+
>
|
|
312
|
+
<span class={menuIconClass}>
|
|
313
|
+
<Icon name={client.icon ?? "terminal"} size={16} />
|
|
314
|
+
</span>
|
|
315
|
+
<span class="flex-1" data-mcp-command-label>
|
|
316
|
+
{client.label}
|
|
317
|
+
</span>
|
|
318
|
+
</button>
|
|
319
|
+
)}
|
|
320
|
+
</>
|
|
321
|
+
))}
|
|
256
322
|
</div>
|
|
257
323
|
</details>
|
|
258
324
|
)
|
|
@@ -299,6 +365,7 @@ const panelClass =
|
|
|
299
365
|
</style>
|
|
300
366
|
|
|
301
367
|
<script>
|
|
368
|
+
import { mcpClientCommand } from "../../core/mcp-clients.ts";
|
|
302
369
|
import { copyDeferredText, copyText, flashLabel } from "../copy-feedback.ts";
|
|
303
370
|
import { installDropdownDismiss } from "../dropdown-dismiss.ts";
|
|
304
371
|
import { mountBase } from "../islands/base-path.ts";
|
|
@@ -397,10 +464,15 @@ hr { border: 0; border-top: 1px solid #ddd; margin: 2em 0; }`;
|
|
|
397
464
|
return;
|
|
398
465
|
}
|
|
399
466
|
const rect = summary.getBoundingClientRect();
|
|
467
|
+
// The rail scrolls, so it clips the menu at its own edges: its top sits
|
|
468
|
+
// under the sticky header, below the viewport's.
|
|
469
|
+
const room = details
|
|
470
|
+
.closest("[data-blume-toc]")
|
|
471
|
+
?.getBoundingClientRect() ?? { bottom: window.innerHeight, top: 0 };
|
|
400
472
|
const margin = 8;
|
|
401
473
|
const flipUp =
|
|
402
|
-
rect.bottom + menu.offsetHeight + margin >
|
|
403
|
-
rect.top - menu.offsetHeight - margin >
|
|
474
|
+
rect.bottom + menu.offsetHeight + margin > room.bottom &&
|
|
475
|
+
rect.top - menu.offsetHeight - margin > room.top;
|
|
404
476
|
menu.classList.toggle("top-full", !flipUp);
|
|
405
477
|
menu.classList.toggle("mt-1", !flipUp);
|
|
406
478
|
menu.classList.toggle("bottom-full", flipUp);
|
|
@@ -468,7 +540,7 @@ hr { border: 0; border-top: 1px solid #ddd; margin: 2em 0; }`;
|
|
|
468
540
|
}
|
|
469
541
|
// The action dropdowns are native <details>. Make them behave like a proper
|
|
470
542
|
// menu: only one open at a time, and flip above the trigger when opening
|
|
471
|
-
// downward would run past the
|
|
543
|
+
// downward would run past the rail's bottom and there's room above.
|
|
472
544
|
const dropdowns = [...root.querySelectorAll<HTMLDetailsElement>("details")];
|
|
473
545
|
for (const details of dropdowns) {
|
|
474
546
|
details.addEventListener("toggle", () => {
|
|
@@ -572,6 +644,19 @@ hr { border: 0; border-top: 1px solid #ddd; margin: 2em 0; }`;
|
|
|
572
644
|
root.querySelector("[data-mcp-codex-label]")
|
|
573
645
|
)
|
|
574
646
|
);
|
|
647
|
+
for (const button of root.querySelectorAll<HTMLElement>(
|
|
648
|
+
"[data-mcp-copy-command]"
|
|
649
|
+
)) {
|
|
650
|
+
button.addEventListener("click", () =>
|
|
651
|
+
copy(
|
|
652
|
+
mcpClientCommand(button.dataset.mcpCopyCommand ?? "", {
|
|
653
|
+
name: id,
|
|
654
|
+
url: mcpUrl,
|
|
655
|
+
}),
|
|
656
|
+
button.querySelector("[data-mcp-command-label]")
|
|
657
|
+
)
|
|
658
|
+
);
|
|
659
|
+
}
|
|
575
660
|
}
|
|
576
661
|
|
|
577
662
|
const label = root.querySelector("[data-blume-copy-label]");
|
|
@@ -9,6 +9,8 @@ import type { UIStrings } from "../../core/i18n-ui.ts";
|
|
|
9
9
|
import { resolveDateFormatOptions } from "../../core/date-format.ts";
|
|
10
10
|
import type { ResolvedDateFormat } from "../../core/schema.ts";
|
|
11
11
|
import type { OpenInChatProvider } from "../../core/open-in-chat.ts";
|
|
12
|
+
import { mcpMenuEntries } from "../../core/mcp-clients.ts";
|
|
13
|
+
import type { McpClientEntry } from "../../core/mcp-clients.ts";
|
|
12
14
|
import type {
|
|
13
15
|
BlumeBanner,
|
|
14
16
|
BlumeClientData,
|
|
@@ -114,7 +116,15 @@ interface Props {
|
|
|
114
116
|
banner?: BlumeBanner | null;
|
|
115
117
|
analytics?: BlumeDataConfig["analytics"] | null;
|
|
116
118
|
navigation: Navigation;
|
|
117
|
-
|
|
119
|
+
/**
|
|
120
|
+
* The hosted MCP server. `clients` lists the "Connect to MCP" menu's
|
|
121
|
+
* clients in order, and defaults to every built-in one.
|
|
122
|
+
*/
|
|
123
|
+
mcp?: {
|
|
124
|
+
clients?: readonly McpClientEntry[];
|
|
125
|
+
name: string;
|
|
126
|
+
route: string;
|
|
127
|
+
} | null;
|
|
118
128
|
page: { title: string; description?: string; route: string };
|
|
119
129
|
headings: Heading[];
|
|
120
130
|
imageZoom?: boolean;
|
|
@@ -471,6 +481,11 @@ const markdownMirror = isBare
|
|
|
471
481
|
// Needs a configured site to be useful, so the menu is hidden without one.
|
|
472
482
|
const mcpUrl =
|
|
473
483
|
mcp && siteUrl ? new URL(withMountedBase(mcp.route), siteUrl).href : null;
|
|
484
|
+
// Custom clients' labels resolve to the page's locale. Without a list,
|
|
485
|
+
// PageActions shows every built-in client.
|
|
486
|
+
const mcpMenu = mcp?.clients
|
|
487
|
+
? mcpMenuEntries(mcp.clients, locale, data.config.i18n?.defaultLocale)
|
|
488
|
+
: undefined;
|
|
474
489
|
|
|
475
490
|
// Scope the sidebar (and the breadcrumbs/pagination derived from it) to the
|
|
476
491
|
// active tab's section, so a multi-section site drills each tab into its own
|
|
@@ -953,7 +968,7 @@ const localeRedirect = localeTargets
|
|
|
953
968
|
showToc && (
|
|
954
969
|
<aside
|
|
955
970
|
aria-label={strings.toc.title}
|
|
956
|
-
class="sticky top-16 hidden h-[calc(100dvh-4rem)] scrollbar-thin scrollbar-thumb-border scrollbar-track-transparent overflow-y-auto px-
|
|
971
|
+
class="sticky top-16 hidden h-[calc(100dvh-4rem)] scrollbar-thin scrollbar-thumb-border scrollbar-track-transparent overflow-y-auto [--blume-rail-inset:1rem] px-(--blume-rail-inset) pt-6 pb-10 text-sm xl:block"
|
|
957
972
|
data-blume-toc
|
|
958
973
|
>
|
|
959
974
|
{hasToc && (
|
|
@@ -968,6 +983,7 @@ const localeRedirect = localeTargets
|
|
|
968
983
|
editUrl={editUrl}
|
|
969
984
|
exportEpub={exportEpub}
|
|
970
985
|
exportPdf={exportPdf}
|
|
986
|
+
mcpMenu={mcpMenu}
|
|
971
987
|
mcpName={mcp?.name}
|
|
972
988
|
mcpUrl={mcpUrl}
|
|
973
989
|
openInChat={openInChat}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
---
|
|
2
2
|
// The site footer, below the content on every page: one row, as tall as the
|
|
3
|
-
// header, with `footer.links` on one side
|
|
4
|
-
// other. The site's repository
|
|
5
|
-
// is the first icon; the footer is the only place it's linked. With `consent` configured the links end with the
|
|
3
|
+
// header, with `footer.links` on one side, the social profile icons on the
|
|
4
|
+
// other, and `footer.copyright` centered between them. The site's repository
|
|
5
|
+
// (`github`, unless `navigation.repo` hides it) is the first icon; the footer is the only place it's linked. With `consent` configured the links end with the
|
|
6
6
|
// Cookie settings link, which reopens the reader's consent choices. Renders
|
|
7
7
|
// nothing when there's none of these; a `components.ts` `Footer` replaces it.
|
|
8
8
|
import data from "blume:data";
|
|
@@ -12,6 +12,7 @@ import type { Navigation } from "../../core/types.ts";
|
|
|
12
12
|
import { EN_UI } from "../../core/i18n-ui.ts";
|
|
13
13
|
import type { UIStrings } from "../../core/i18n-ui.ts";
|
|
14
14
|
import { routeSetFor } from "../../core/locale-links.ts";
|
|
15
|
+
import { resolveLocalizable } from "../../core/localizable.ts";
|
|
15
16
|
import { newTabAttrs } from "../../core/new-tab.ts";
|
|
16
17
|
import Icon from "../Icon.astro";
|
|
17
18
|
import { withBase } from "../islands/base-path.ts";
|
|
@@ -21,8 +22,9 @@ import { footerSocialLinks } from "./footer-socials.ts";
|
|
|
21
22
|
interface Props {
|
|
22
23
|
footer?: BlumeDataConfig["footer"];
|
|
23
24
|
/**
|
|
24
|
-
* Active locale of the page: labels written as
|
|
25
|
-
* entry, and internal links move into it (see
|
|
25
|
+
* Active locale of the page: the copyright and labels written as
|
|
26
|
+
* per-locale maps show its entry, and internal links move into it (see
|
|
27
|
+
* `footer-links.ts`).
|
|
26
28
|
*/
|
|
27
29
|
locale?: string;
|
|
28
30
|
/** The page's navigation: its `repoUrl` is the repository link. */
|
|
@@ -40,6 +42,14 @@ const socials = footerSocialLinks(
|
|
|
40
42
|
? { href: repoUrl, label: { ...EN_UI.nav, ...ui?.nav }.githubRepository }
|
|
41
43
|
: null
|
|
42
44
|
);
|
|
45
|
+
const copyright =
|
|
46
|
+
footer?.copyright === undefined
|
|
47
|
+
? null
|
|
48
|
+
: resolveLocalizable(
|
|
49
|
+
footer.copyright,
|
|
50
|
+
locale,
|
|
51
|
+
data.config.i18n?.defaultLocale
|
|
52
|
+
);
|
|
43
53
|
const links = footerLinks(footer?.links ?? [], {
|
|
44
54
|
basePath: data.config.basePath,
|
|
45
55
|
i18n: data.config.i18n,
|
|
@@ -58,9 +68,17 @@ const hasLinks = links.length > 0 || consentSettings !== null;
|
|
|
58
68
|
---
|
|
59
69
|
|
|
60
70
|
{
|
|
61
|
-
(hasLinks || socials.length > 0) && (
|
|
71
|
+
(copyright !== null || hasLinks || socials.length > 0) && (
|
|
62
72
|
<footer class="border-border border-t px-4 md:px-6" data-blume-footer>
|
|
63
|
-
|
|
73
|
+
{/* With a copyright, three columns from `md`, the outer two sharing
|
|
74
|
+
what it leaves so it sits at the true center; without one, the
|
|
75
|
+
links keep the row's full width. Narrower, the row wraps. */}
|
|
76
|
+
<div
|
|
77
|
+
class:list={[
|
|
78
|
+
"flex min-h-16 flex-wrap items-center gap-x-6 gap-y-3 py-3",
|
|
79
|
+
copyright !== null && "md:grid md:grid-cols-[1fr_auto_1fr]",
|
|
80
|
+
]}
|
|
81
|
+
>
|
|
64
82
|
{hasLinks && (
|
|
65
83
|
<ul class="flex flex-wrap items-center gap-x-5 gap-y-2 text-sm">
|
|
66
84
|
{links.map((link) => (
|
|
@@ -87,10 +105,15 @@ const hasLinks = links.length > 0 || consentSettings !== null;
|
|
|
87
105
|
)}
|
|
88
106
|
</ul>
|
|
89
107
|
)}
|
|
108
|
+
{copyright !== null && (
|
|
109
|
+
<p class="text-muted-foreground text-sm md:col-start-2">
|
|
110
|
+
{copyright}
|
|
111
|
+
</p>
|
|
112
|
+
)}
|
|
90
113
|
{/* Pulled out by the buttons' padding, so the glyphs line up with the
|
|
91
114
|
footer's edge, and pushed to the far side even with no links. */}
|
|
92
115
|
{socials.length > 0 && (
|
|
93
|
-
<ul class="-me-[9px] -my-[9px] ms-auto flex flex-wrap items-center gap-1">
|
|
116
|
+
<ul class="-me-[9px] -my-[9px] ms-auto flex flex-wrap items-center gap-1 md:col-start-3">
|
|
94
117
|
{socials.map((social) => (
|
|
95
118
|
<li>
|
|
96
119
|
<a
|
package/src/core/config-input.ts
CHANGED
|
@@ -22,6 +22,8 @@ import type { FontSlug } from "../theme/fonts.ts";
|
|
|
22
22
|
import type { FooterSocial } from "./footer.ts";
|
|
23
23
|
import type {
|
|
24
24
|
blumeConfigSchema,
|
|
25
|
+
McpClient,
|
|
26
|
+
McpCustomClient,
|
|
25
27
|
OpenInChatProvider,
|
|
26
28
|
SidebarDisplay,
|
|
27
29
|
SidebarItemConfig,
|
|
@@ -147,9 +149,15 @@ export interface FooterLink {
|
|
|
147
149
|
|
|
148
150
|
/**
|
|
149
151
|
* The site footer, below the content on every page: a row of links on one
|
|
150
|
-
* side,
|
|
152
|
+
* side, social profile icons on the other, and a copyright line centered
|
|
153
|
+
* between them.
|
|
151
154
|
*/
|
|
152
155
|
export interface FooterConfig {
|
|
156
|
+
/**
|
|
157
|
+
* A line of plain text centered between the links and the icons, like
|
|
158
|
+
* `© 2026 Acme, Inc.`, or a map of locale code to text.
|
|
159
|
+
*/
|
|
160
|
+
copyright?: LocalizableLabel;
|
|
153
161
|
/** Links in one row, left to right. */
|
|
154
162
|
links?: FooterLink[];
|
|
155
163
|
/**
|
|
@@ -719,6 +727,32 @@ export interface LlmsTxtConfig {
|
|
|
719
727
|
|
|
720
728
|
/** Expose the docs as an MCP server for connecting agents. */
|
|
721
729
|
export interface McpConfig {
|
|
730
|
+
/**
|
|
731
|
+
* The clients the "Connect to MCP" page action installs the server into,
|
|
732
|
+
* beside its "Copy server URL" row. Defaults to `true` (every built-in
|
|
733
|
+
* client). Set `false` to list none, or list built-in keys and your own
|
|
734
|
+
* clients, in order. A custom client's `command` is copied with `{name}`
|
|
735
|
+
* (the server name as an id) and `{url}` (the server's URL) filled in; its
|
|
736
|
+
* `label` localizes like a header link's, and `icon` defaults to `terminal`.
|
|
737
|
+
*
|
|
738
|
+
* ```ts
|
|
739
|
+
* agents: {
|
|
740
|
+
* mcp: {
|
|
741
|
+
* enabled: true,
|
|
742
|
+
* clients: [
|
|
743
|
+
* "claude-code",
|
|
744
|
+
* {
|
|
745
|
+
* label: "Copy Copilot CLI command",
|
|
746
|
+
* command: "copilot mcp add --transport http {name} {url}",
|
|
747
|
+
* icon: "terminal",
|
|
748
|
+
* },
|
|
749
|
+
* "vscode",
|
|
750
|
+
* ],
|
|
751
|
+
* },
|
|
752
|
+
* }
|
|
753
|
+
* ```
|
|
754
|
+
*/
|
|
755
|
+
clients?: boolean | (McpClient | McpCustomClient)[];
|
|
722
756
|
/** Turn the MCP server on. Defaults to `false`. */
|
|
723
757
|
enabled?: boolean;
|
|
724
758
|
/** Optional system hint passed to connecting agents. */
|
|
@@ -1567,8 +1601,8 @@ export interface BlumeConfig {
|
|
|
1567
1601
|
*/
|
|
1568
1602
|
feedback?: boolean | FeedbackConfig;
|
|
1569
1603
|
/**
|
|
1570
|
-
* The site footer:
|
|
1571
|
-
* the site has no footer.
|
|
1604
|
+
* The site footer: a copyright line, a row of links, and social profile
|
|
1605
|
+
* icons. Unset, the site has no footer.
|
|
1572
1606
|
*/
|
|
1573
1607
|
footer?: FooterConfig;
|
|
1574
1608
|
/** Opt-in custom frontmatter keys, validated by schemas you supply. */
|
|
@@ -1597,7 +1631,7 @@ export interface BlumeConfig {
|
|
|
1597
1631
|
* Limit how often one reader (by IP address) can call the server routes —
|
|
1598
1632
|
* the assistant, the API playground proxy, server-side search — with an
|
|
1599
1633
|
* adapter from `blume/ratelimit`: `memory()` (the default, 30 requests per
|
|
1600
|
-
* 10 minutes), `upstash()`, or `
|
|
1634
|
+
* 10 minutes), `upstash()`, `cloudflare()`, or `unkey()`. `false` turns it off.
|
|
1601
1635
|
*/
|
|
1602
1636
|
rateLimit?: false | RateLimitAdapter;
|
|
1603
1637
|
/** React island behavior (compiler auto-memoization). */
|
package/src/core/data.ts
CHANGED
|
@@ -187,7 +187,7 @@ export interface BlumeDataConfig {
|
|
|
187
187
|
feedback: boolean;
|
|
188
188
|
/** Whether the rating asks for a written comment after it (`feedback.comments`). */
|
|
189
189
|
feedbackComments: boolean;
|
|
190
|
-
/** `footer`:
|
|
190
|
+
/** `footer`: the copyright line, links, and social profile icons, or `null` when unset. */
|
|
191
191
|
footer: ResolvedConfig["footer"] | null;
|
|
192
192
|
/**
|
|
193
193
|
* Repo coordinates for content components that address the API or build
|
|
@@ -206,8 +206,15 @@ export interface BlumeDataConfig {
|
|
|
206
206
|
/** `markdown.imageZoom`: click-to-zoom content images. */
|
|
207
207
|
imageZoom: boolean;
|
|
208
208
|
logo: BlumeLogo | null;
|
|
209
|
-
/**
|
|
210
|
-
|
|
209
|
+
/**
|
|
210
|
+
* Hosted MCP server, or `null` when MCP is off, with the clients its
|
|
211
|
+
* "Connect to MCP" page action lists (`agents.mcp.clients`), in order.
|
|
212
|
+
*/
|
|
213
|
+
mcp: {
|
|
214
|
+
clients: ResolvedConfig["agents"]["mcp"]["clients"];
|
|
215
|
+
name: string;
|
|
216
|
+
route: string;
|
|
217
|
+
} | null;
|
|
211
218
|
/** `seo.metatags`: the site's own tags for every page's head, or `null`. */
|
|
212
219
|
metatags: Record<string, string> | null;
|
|
213
220
|
/**
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { resolveLocalizable } from "./localizable.ts";
|
|
2
|
+
import type { LocalizableLabel } from "./schema.ts";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Clients the "Connect to MCP" page action knows how to install the server
|
|
6
|
+
* into, in display order. Shared by the config schema (`agents.mcp.clients`
|
|
7
|
+
* entries validate against this list) and the PageActions menu (which renders
|
|
8
|
+
* the full list when the config is `true`). Lives outside `schema.ts` so the
|
|
9
|
+
* component can import the list without pulling the whole config schema into
|
|
10
|
+
* the layout module graph.
|
|
11
|
+
*/
|
|
12
|
+
export const mcpClients = ["claude-code", "codex", "cursor", "vscode"] as const;
|
|
13
|
+
|
|
14
|
+
export type McpClient = (typeof mcpClients)[number];
|
|
15
|
+
|
|
16
|
+
/** A client Blume doesn't ship: a menu row that copies its `command`. */
|
|
17
|
+
export interface McpCustomClient {
|
|
18
|
+
/**
|
|
19
|
+
* Command the row copies, with `{name}` (the server name as an id, like
|
|
20
|
+
* `acme-docs`) and `{url}` (the server's absolute URL) filled in.
|
|
21
|
+
*/
|
|
22
|
+
command: string;
|
|
23
|
+
/** Icon beside the label: a Lucide name, inline SVG, or image. Defaults to `terminal`. */
|
|
24
|
+
icon?: string;
|
|
25
|
+
/** Row label, optionally per locale (`{ en: "…", de: "…" }`). */
|
|
26
|
+
label: LocalizableLabel;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export type McpClientEntry = McpClient | McpCustomClient;
|
|
30
|
+
|
|
31
|
+
/** A menu entry as a page renders it: a custom label resolved to its locale. */
|
|
32
|
+
export type McpMenuEntry =
|
|
33
|
+
| McpClient
|
|
34
|
+
| (Omit<McpCustomClient, "label"> & { label: string });
|
|
35
|
+
|
|
36
|
+
/** Whether a `clients` entry names a built-in client rather than a custom one. */
|
|
37
|
+
export const isBuiltInMcpClient = (
|
|
38
|
+
client: McpClientEntry | McpMenuEntry
|
|
39
|
+
): client is McpClient => typeof client === "string";
|
|
40
|
+
|
|
41
|
+
/** Resolve the custom clients' labels for one page's locale. */
|
|
42
|
+
export const mcpMenuEntries = (
|
|
43
|
+
clients: readonly McpClientEntry[],
|
|
44
|
+
locale?: string,
|
|
45
|
+
defaultLocale?: string
|
|
46
|
+
): McpMenuEntry[] =>
|
|
47
|
+
clients.map((client) =>
|
|
48
|
+
isBuiltInMcpClient(client)
|
|
49
|
+
? client
|
|
50
|
+
: {
|
|
51
|
+
...client,
|
|
52
|
+
label: resolveLocalizable(client.label, locale, defaultLocale),
|
|
53
|
+
}
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
/** Whether a row opens an install link rather than copying a command. */
|
|
57
|
+
const opensLink = (client: McpMenuEntry): boolean =>
|
|
58
|
+
client === "cursor" || client === "vscode";
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* The menu's rows after "Copy server URL", each with whether a rule goes
|
|
62
|
+
* above it: wherever the list switches between commands to copy and install
|
|
63
|
+
* links ("Copy server URL" copies too), so the default menu keeps its one
|
|
64
|
+
* rule before Cursor.
|
|
65
|
+
*/
|
|
66
|
+
export const mcpMenuRows = (
|
|
67
|
+
entries: readonly McpMenuEntry[]
|
|
68
|
+
): { client: McpMenuEntry; rule: boolean }[] =>
|
|
69
|
+
entries.map((client, index) => {
|
|
70
|
+
const previous = entries[index - 1];
|
|
71
|
+
return {
|
|
72
|
+
client,
|
|
73
|
+
rule:
|
|
74
|
+
opensLink(client) !== (previous !== undefined && opensLink(previous)),
|
|
75
|
+
};
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
/** Fill a custom client's command for the server it installs. */
|
|
79
|
+
export const mcpClientCommand = (
|
|
80
|
+
command: string,
|
|
81
|
+
server: { name: string; url: string }
|
|
82
|
+
): string =>
|
|
83
|
+
// Replacer functions, not strings: a URL may contain `$`, which a string
|
|
84
|
+
// replacement would interpret as a substitution pattern.
|
|
85
|
+
command
|
|
86
|
+
.replaceAll("{name}", () => server.name)
|
|
87
|
+
.replaceAll("{url}", () => server.url);
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import { isAssetIcon } from "../theme/icon-kind.ts";
|
|
2
2
|
import { hasIcon } from "../theme/icons.ts";
|
|
3
3
|
import { isInternalPath } from "./base-path.ts";
|
|
4
|
+
import { resolveLocalizable } from "./localizable.ts";
|
|
5
|
+
import { isBuiltInMcpClient } from "./mcp-clients.ts";
|
|
6
|
+
import type { McpClientEntry } from "./mcp-clients.ts";
|
|
4
7
|
import { isPageRef, normalizeRef } from "./navigation.ts";
|
|
5
8
|
import type { SidebarItemConfig } from "./schema.ts";
|
|
6
9
|
import type { Diagnostic, NavNode, Navigation, PageRecord } from "./types.ts";
|
|
@@ -100,6 +103,28 @@ export const validateSearchPopularIcons = (
|
|
|
100
103
|
return unknownIconDiagnostics(icons, ICON_FORMAT_HINT);
|
|
101
104
|
};
|
|
102
105
|
|
|
106
|
+
/**
|
|
107
|
+
* Warn about unknown icons on custom "Connect to MCP" clients
|
|
108
|
+
* (`agents.mcp.clients`), which live outside the navigation model too. A
|
|
109
|
+
* per-locale label is named in the default locale.
|
|
110
|
+
*/
|
|
111
|
+
export const validateMcpClientIcons = (
|
|
112
|
+
clients: readonly McpClientEntry[],
|
|
113
|
+
defaultLocale?: string
|
|
114
|
+
): Diagnostic[] => {
|
|
115
|
+
const icons = clients.flatMap((client) =>
|
|
116
|
+
!isBuiltInMcpClient(client) && client.icon
|
|
117
|
+
? [
|
|
118
|
+
{
|
|
119
|
+
icon: client.icon,
|
|
120
|
+
where: `MCP client "${resolveLocalizable(client.label, defaultLocale, defaultLocale)}"`,
|
|
121
|
+
},
|
|
122
|
+
]
|
|
123
|
+
: []
|
|
124
|
+
);
|
|
125
|
+
return unknownIconDiagnostics(icons, ICON_FORMAT_HINT);
|
|
126
|
+
};
|
|
127
|
+
|
|
103
128
|
/** Whether an internal path resolves to a page or a section that has pages. */
|
|
104
129
|
const resolvesToPages = (routes: Set<string>, path: string): boolean =>
|
|
105
130
|
routes.has(path) || [...routes].some((route) => route.startsWith(`${path}/`));
|