@dbx-tools/ui-mastra 0.3.6 → 0.3.7

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/package.json CHANGED
@@ -26,19 +26,19 @@
26
26
  "shiki": "^3.0.0",
27
27
  "sql-formatter": "^15.6.9",
28
28
  "streamdown": "^2.5.0",
29
- "@dbx-tools/shared-core": "0.3.6",
30
- "@dbx-tools/shared-genie": "0.3.6",
31
- "@dbx-tools/shared-mastra": "0.3.6",
32
- "@dbx-tools/ui-appkit": "0.3.6",
33
- "@dbx-tools/shared-model": "0.3.6",
34
- "@dbx-tools/ui-branding": "0.3.6"
29
+ "@dbx-tools/shared-core": "0.3.7",
30
+ "@dbx-tools/shared-genie": "0.3.7",
31
+ "@dbx-tools/shared-mastra": "0.3.7",
32
+ "@dbx-tools/ui-appkit": "0.3.7",
33
+ "@dbx-tools/shared-model": "0.3.7",
34
+ "@dbx-tools/ui-branding": "0.3.7"
35
35
  },
36
36
  "main": "index.ts",
37
37
  "license": "UNLICENSED",
38
38
  "publishConfig": {
39
39
  "access": "public"
40
40
  },
41
- "version": "0.3.6",
41
+ "version": "0.3.7",
42
42
  "types": "index.ts",
43
43
  "type": "module",
44
44
  "exports": {
@@ -349,10 +349,14 @@ export const ChatView = ({
349
349
  if (isMobile) setMobileDrawerOpen((open) => !open);
350
350
  else toggleDesktopSidebar();
351
351
  };
352
- // Top bar carries only the sidebar toggle now; the model picker, export, and
353
- // clear all live in a toolbar row below the composer, closer to where the
354
- // user is typing.
355
- const showHeader = showSidebar;
352
+ // The top bar carries only the sidebar toggle; the model picker, export, and
353
+ // clear controls live in a toolbar row below the composer, closer to where
354
+ // the user is typing. The toggle is a mobile hamburger, or a desktop "show"
355
+ // affordance while the inline sidebar is collapsed - so the bar renders only
356
+ // when that toggle would actually be visible (an open desktop sidebar has its
357
+ // own hide button, leaving nothing for the bar to hold).
358
+ const showSidebarToggle = showSidebar && (isMobile || !desktopSidebarOpen);
359
+ const showHeader = showSidebarToggle;
356
360
  const showComposerToolbar = showModelDisplay || showExport || showClear;
357
361
 
358
362
  // Clear confirmation is an AppKit `AlertDialog` (a real modal), plus an
@@ -446,33 +450,31 @@ export const ChatView = ({
446
450
  ))}
447
451
  <div className="flex h-full min-w-0 flex-1 flex-col">
448
452
  {showHeader && (
449
- <div className="mx-auto flex w-full max-w-4xl items-center justify-between gap-2 px-3 pb-2 pt-1 text-xs text-muted-foreground md:gap-3 md:px-6">
450
- <div className="flex shrink-0 items-center gap-2">
451
- {/*
452
- * Sidebar toggle. On mobile it's a persistent hamburger (the
453
- * overlay drawer has no always-visible hide button). On desktop
454
- * the panel hides itself, so the header only needs a "show"
455
- * affordance while the inline sidebar is collapsed.
456
- */}
457
- {showSidebar && (isMobile || !desktopSidebarOpen) && (
458
- <Tooltip>
459
- <TooltipTrigger asChild>
460
- <Button
461
- type="button"
462
- variant="ghost"
463
- size="icon-sm"
464
- onClick={toggleSidebar}
465
- aria-label={sidebarOpen ? "Hide conversations" : "Show conversations"}
466
- >
467
- <PanelLeftIcon className="size-4" />
468
- </Button>
469
- </TooltipTrigger>
470
- <TooltipContent>
471
- {sidebarOpen ? "Hide conversations" : "Show conversations"}
472
- </TooltipContent>
473
- </Tooltip>
474
- )}
475
- </div>
453
+ /*
454
+ * Slim top bar holding the sidebar toggle. On mobile the toggle is
455
+ * a persistent hamburger (the overlay drawer has no always-visible
456
+ * hide button); on desktop it's a "show" affordance rendered only
457
+ * while the inline sidebar is collapsed (an open sidebar has its
458
+ * own hide button). `showHeader` already tracks that visibility, so
459
+ * the bar never renders empty.
460
+ */
461
+ <div className="mx-auto flex w-full max-w-4xl items-center gap-2 px-3 pb-2 pt-1 text-xs text-muted-foreground md:gap-3 md:px-6">
462
+ <Tooltip>
463
+ <TooltipTrigger asChild>
464
+ <Button
465
+ type="button"
466
+ variant="ghost"
467
+ size="icon-sm"
468
+ onClick={toggleSidebar}
469
+ aria-label={sidebarOpen ? "Hide conversations" : "Show conversations"}
470
+ >
471
+ <PanelLeftIcon className="size-4" />
472
+ </Button>
473
+ </TooltipTrigger>
474
+ <TooltipContent>
475
+ {sidebarOpen ? "Hide conversations" : "Show conversations"}
476
+ </TooltipContent>
477
+ </Tooltip>
476
478
  </div>
477
479
  )}
478
480
  <div className="relative flex flex-1 flex-col overflow-hidden">
@@ -46,10 +46,9 @@ import type {
46
46
  // `{ runId, payload: { toolCallId, toolName, args } }`. We surface that
47
47
  // as an out-of-band entry in `pendingApprovalsByMessage` and wire
48
48
  // `onResolveToolApproval` to {@link MastraPluginClient.approveToolCallStream}
49
- // / `declineToolCallStream`, which read SSE directly and avoid a stock
50
- // `@mastra/client-js` bug on resumed approval streams.
51
- // both of which return a fresh stream Response we run through the same
52
- // chunk handler.
49
+ // / `declineToolCallStream` - both of which read SSE directly (avoiding a
50
+ // stock `@mastra/client-js` bug on resumed approval streams) and return a
51
+ // fresh stream Response we run through the same chunk handler.
53
52
  //
54
53
  // On mount the transcript hydrates with the most recent page of thread
55
54
  // history from the Mastra plugin's `/history` endpoint; scrolling near
@@ -1256,9 +1255,9 @@ export const useMastraChat = (
1256
1255
  [activeKey, mastraClient, updateSession],
1257
1256
  );
1258
1257
 
1259
- // Merge optimistic rows
1260
- // the server list, newest first, dropping any optimistic entry the
1261
- // server already returns so a thread is never listed twice.
1258
+ // Merge optimistic rows over the server list, newest first, dropping any
1259
+ // optimistic entry the server already returns so a thread is never listed
1260
+ // twice.
1262
1261
  const sidebarThreads = useMemo<ThreadSummary[]>(() => {
1263
1262
  // Title overlay precedence per row: a manual rename always wins;
1264
1263
  // otherwise a provisional first-message title fills an untitled row
@@ -162,13 +162,20 @@ function printViaHiddenIframe(html: string, downloadName: string): void {
162
162
  return;
163
163
  }
164
164
  // Settle so charts / fonts lay out, then print. `afterprint` removes the
165
- // iframe once the dialog closes (a timeout backstops browsers that don't
166
- // fire it).
165
+ // iframe once the dialog closes; a 60s timer backstops browsers that don't
166
+ // fire it. The backstop is armed BEFORE `print()` so a throw (sandboxed /
167
+ // policy-restricted frames can reject `print()`) still cleans up, and the
168
+ // export falls back to a file download rather than silently vanishing.
167
169
  win.addEventListener("afterprint", cleanup);
168
170
  win.setTimeout(() => {
169
- win.focus();
170
- win.print();
171
171
  window.setTimeout(cleanup, 60000);
172
+ try {
173
+ win.focus();
174
+ win.print();
175
+ } catch {
176
+ cleanup();
177
+ downloadTextFile(downloadName, html, "text/html;charset=utf-8");
178
+ }
172
179
  }, PRINT_SETTLE_MS);
173
180
  };
174
181
 
@@ -478,6 +485,20 @@ function downloadTextFile(filename: string, content: string, mime: string): void
478
485
  window.setTimeout(() => URL.revokeObjectURL(url), 1000);
479
486
  }
480
487
 
488
+ /**
489
+ * A brand color / font value safe to interpolate into a `<style>` block.
490
+ * Brand colors are hex-validated upstream, but the font stack is only checked
491
+ * for non-blankness, so strip the characters that could close a declaration or
492
+ * rule (`{ } ; < > \` plus HTML-comment openers) to prevent CSS injection into
493
+ * the export document. Falls back to `fallback` when the value is blank.
494
+ */
495
+ function cssValue(value: string | undefined, fallback: string): string {
496
+ const trimmed = value?.trim();
497
+ if (!trimmed) return fallback;
498
+ const safe = trimmed.replace(/[{}<>;\\]/g, "");
499
+ return safe || fallback;
500
+ }
501
+
481
502
  /**
482
503
  * Build the inline stylesheet for the exported document, folding in optional
483
504
  * brand color / font overrides (falling back to the neutral slate/blue theme
@@ -497,11 +518,13 @@ function downloadTextFile(filename: string, content: string, mime: string): void
497
518
  * out; the visible page margin is re-supplied as body padding.
498
519
  */
499
520
  function buildDocumentCss(brand?: ExportBrand): string {
500
- const fg = brand?.foreground || "#0f172a";
501
- const primary = brand?.primary || "#0f172a";
502
- const link = brand?.accent || "#2563eb";
503
- const font =
504
- brand?.fontSans || 'system-ui, -apple-system, "Segoe UI", Roboto, sans-serif';
521
+ const fg = cssValue(brand?.foreground, "#0f172a");
522
+ const primary = cssValue(brand?.primary, "#0f172a");
523
+ const link = cssValue(brand?.accent, "#2563eb");
524
+ const font = cssValue(
525
+ brand?.fontSans,
526
+ 'system-ui, -apple-system, "Segoe UI", Roboto, sans-serif',
527
+ );
505
528
  return `
506
529
  :root { color-scheme: light; }
507
530
  * { box-sizing: border-box; }