blume 2.2.1 → 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 CHANGED
@@ -1,5 +1,12 @@
1
1
  # blume
2
2
 
3
+ ## 2.2.2
4
+
5
+ ### Patch Changes
6
+
7
+ - 6143000: A page actions menu (Export, Open in chat, Connect to MCP) now wraps a label too long for the right rail, as in many of the built-in translations (French, Portuguese, Russian, Swedish, and others) or with a long custom MCP client label. Before, the menu could reach past the rail's edge, and the rail, which scrolls, cut it off. A wrapped label keeps its icons beside its first line, and in a short window a menu opens upward only when it fits below the header, where the rail can show it, instead of tucking its top rows under the header.
8
+ - ffbb7c0: The API reference playground's proxy (`playground: { proxy: true }`) now works on Cloudflare Workers. Before, every Try it request it forwarded to one of the spec's servers failed with a 502 "Illegal invocation" error, because the proxy called the runtime's `fetch` in a way Workers rejects and Node allows.
9
+
3
10
  ## 2.2.1
4
11
 
5
12
  ### Patch Changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "blume",
3
- "version": "2.2.1",
3
+ "version": "2.2.2",
4
4
  "description": "The open-source docs framework for humans and agents.",
5
5
  "keywords": [
6
6
  "agents",
@@ -115,11 +115,17 @@ const rowClass =
115
115
  "flex w-full items-center gap-2.5 py-1.5 text-start text-muted-foreground text-sm transition-colors hover:text-foreground";
116
116
 
117
117
  // Items inside the floating popovers keep menu affordances (padding + hover
118
- // background).
118
+ // background). Their icons sit in a box one line tall, so a label that wraps
119
+ // keeps them beside its first line.
119
120
  const menuRowClass =
120
- "flex w-full items-center 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";
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.
121
127
  const panelClass =
122
- "absolute top-full right-2 z-50 mt-1 w-max min-w-[14rem] max-w-[22rem] rounded-blume border border-border bg-background p-1 shadow-xl";
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";
123
129
  ---
124
130
 
125
131
  <div
@@ -172,13 +178,17 @@ const panelClass =
172
178
  <div class={panelClass} data-blume-menu>
173
179
  {exportPdf && (
174
180
  <button class={menuRowClass} data-blume-export-pdf type="button">
175
- <Icon name="file" size={16} />
181
+ <span class={menuIconClass}>
182
+ <Icon name="file" size={16} />
183
+ </span>
176
184
  <span class="flex-1">{a.exportPdf}</span>
177
185
  </button>
178
186
  )}
179
187
  {exportEpub && (
180
188
  <button class={menuRowClass} data-blume-export-epub type="button">
181
- <Icon name="book-open" size={16} />
189
+ <span class={menuIconClass}>
190
+ <Icon name="book-open" size={16} />
191
+ </span>
182
192
  <span class="flex-1" data-blume-epub-label>
183
193
  {a.exportEpub}
184
194
  </span>
@@ -214,9 +224,11 @@ const panelClass =
214
224
  data-open-in={provider.key}
215
225
  {...NEW_TAB_ATTRS}
216
226
  >
217
- <span set:html={LOGOS[provider.key]} />
227
+ <span class={menuIconClass} set:html={LOGOS[provider.key]} />
218
228
  <span class="flex-1">{provider.label}</span>
219
- <Icon name="external-link" size={13} />
229
+ <span class={menuIconClass}>
230
+ <Icon name="external-link" size={13} />
231
+ </span>
220
232
  </a>
221
233
  </>
222
234
  ))}
@@ -241,7 +253,9 @@ const panelClass =
241
253
  </summary>
242
254
  <div class={panelClass} data-blume-menu>
243
255
  <button class={menuRowClass} data-mcp-copy-url type="button">
244
- <Icon name="link" size={16} />
256
+ <span class={menuIconClass}>
257
+ <Icon name="link" size={16} />
258
+ </span>
245
259
  <span class="flex-1" data-mcp-url-label>
246
260
  {a.copyServerUrl}
247
261
  </span>
@@ -255,7 +269,7 @@ const panelClass =
255
269
  data-mcp-copy-claude
256
270
  type="button"
257
271
  >
258
- <span set:html={LOGOS.claude} />
272
+ <span class={menuIconClass} set:html={LOGOS.claude} />
259
273
  <span class="flex-1" data-mcp-claude-label>
260
274
  {a.copyClaudeCode}
261
275
  </span>
@@ -263,7 +277,7 @@ const panelClass =
263
277
  )}
264
278
  {client === "codex" && (
265
279
  <button class={menuRowClass} data-mcp-copy-codex type="button">
266
- <span set:html={LOGOS.codex} />
280
+ <span class={menuIconClass} set:html={LOGOS.codex} />
267
281
  <span class="flex-1" data-mcp-codex-label>
268
282
  {a.copyCodex}
269
283
  </span>
@@ -271,16 +285,22 @@ const panelClass =
271
285
  )}
272
286
  {client === "cursor" && (
273
287
  <a class={menuRowClass} data-mcp-cursor rel="noreferrer">
274
- <span set:html={LOGOS.cursor} />
288
+ <span class={menuIconClass} set:html={LOGOS.cursor} />
275
289
  <span class="flex-1">{a.addToCursor}</span>
276
- <Icon name="external-link" size={13} />
290
+ <span class={menuIconClass}>
291
+ <Icon name="external-link" size={13} />
292
+ </span>
277
293
  </a>
278
294
  )}
279
295
  {client === "vscode" && (
280
296
  <a class={menuRowClass} data-mcp-vscode rel="noreferrer">
281
- <Icon name="code" size={16} />
297
+ <span class={menuIconClass}>
298
+ <Icon name="code" size={16} />
299
+ </span>
282
300
  <span class="flex-1">{a.addToVscode}</span>
283
- <Icon name="external-link" size={13} />
301
+ <span class={menuIconClass}>
302
+ <Icon name="external-link" size={13} />
303
+ </span>
284
304
  </a>
285
305
  )}
286
306
  {!isBuiltInMcpClient(client) && (
@@ -289,7 +309,9 @@ const panelClass =
289
309
  data-mcp-copy-command={client.command}
290
310
  type="button"
291
311
  >
292
- <Icon name={client.icon ?? "terminal"} size={16} />
312
+ <span class={menuIconClass}>
313
+ <Icon name={client.icon ?? "terminal"} size={16} />
314
+ </span>
293
315
  <span class="flex-1" data-mcp-command-label>
294
316
  {client.label}
295
317
  </span>
@@ -442,10 +464,15 @@ hr { border: 0; border-top: 1px solid #ddd; margin: 2em 0; }`;
442
464
  return;
443
465
  }
444
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 };
445
472
  const margin = 8;
446
473
  const flipUp =
447
- rect.bottom + menu.offsetHeight + margin > window.innerHeight &&
448
- rect.top - menu.offsetHeight - margin > 0;
474
+ rect.bottom + menu.offsetHeight + margin > room.bottom &&
475
+ rect.top - menu.offsetHeight - margin > room.top;
449
476
  menu.classList.toggle("top-full", !flipUp);
450
477
  menu.classList.toggle("mt-1", !flipUp);
451
478
  menu.classList.toggle("bottom-full", flipUp);
@@ -513,7 +540,7 @@ hr { border: 0; border-top: 1px solid #ddd; margin: 2em 0; }`;
513
540
  }
514
541
  // The action dropdowns are native <details>. Make them behave like a proper
515
542
  // menu: only one open at a time, and flip above the trigger when opening
516
- // downward would run past the viewport bottom.
543
+ // downward would run past the rail's bottom and there's room above.
517
544
  const dropdowns = [...root.querySelectorAll<HTMLDetailsElement>("details")];
518
545
  for (const details of dropdowns) {
519
546
  details.addEventListener("toggle", () => {
@@ -968,7 +968,7 @@ const localeRedirect = localeTargets
968
968
  showToc && (
969
969
  <aside
970
970
  aria-label={strings.toc.title}
971
- class="sticky top-16 hidden h-[calc(100dvh-4rem)] scrollbar-thin scrollbar-thumb-border scrollbar-track-transparent overflow-y-auto px-4 pt-6 pb-10 text-sm xl:block"
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"
972
972
  data-blume-toc
973
973
  >
974
974
  {hasToc && (
@@ -196,7 +196,11 @@ const followUpstream = async (args: {
196
196
  signal: AbortSignal;
197
197
  url: URL;
198
198
  }): Promise<Response> => {
199
- const response = await args.fetchImpl(args.url, {
199
+ // Called bare, never as `args.fetchImpl(...)`: that would run the global
200
+ // `fetch` with `this` set to `args`, which Node allows and workerd rejects
201
+ // as an "Illegal invocation".
202
+ const { fetchImpl } = args;
203
+ const response = await fetchImpl(args.url, {
200
204
  body: args.body,
201
205
  headers: args.headers,
202
206
  method: args.method,