@elabs-ai/components-ai 5.3.1 → 5.4.0

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.
@@ -61,8 +61,8 @@ var ToolHeader = ({
61
61
  children: [
62
62
  /* @__PURE__ */ jsxs("div", { className: "flex min-w-0 items-center gap-2", children: [
63
63
  /* @__PURE__ */ jsx(WrenchIcon, { className: "size-4 shrink-0 text-muted-foreground" }),
64
- /* @__PURE__ */ jsx("span", { className: "shrink-0 text-body font-medium", children: title ?? derivedName }),
65
- getStatusBadge(state),
64
+ /* @__PURE__ */ jsx("span", { className: "min-w-0 truncate text-body font-medium", children: title ?? derivedName }),
65
+ /* @__PURE__ */ jsx(StatusBadge, { status: statusFromToolState(state), className: "shrink-0" }),
66
66
  summary ? /* @__PURE__ */ jsx("span", { className: "truncate text-meta text-muted-foreground", children: summary }) : null
67
67
  ] }),
68
68
  /* @__PURE__ */ jsx(ChevronDownIcon, { className: "size-4 shrink-0 text-muted-foreground transition-transform group-data-[state=open]:rotate-180" })
@@ -170,4 +170,4 @@ export {
170
170
  ToolInput,
171
171
  ToolOutput
172
172
  };
173
- //# sourceMappingURL=chunk-PTWCTSZL.js.map
173
+ //# sourceMappingURL=chunk-AOOIAFDT.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/tool.tsx"],"sourcesContent":["\"use client\";\n\nimport { Skeleton, StatusBadge, useLocale, type Status } from \"@elabs-ai/components-ui\";\nimport { Collapsible, CollapsibleContent, CollapsibleTrigger } from \"@elabs-ai/components-ui\";\nimport { cn } from \"@elabs-ai/components-ui/lib/cn\";\nimport type { DynamicToolUIPart, ToolUIPart } from \"ai\";\nimport { ChevronDownIcon, WrenchIcon } from \"lucide-react\";\nimport type { ComponentProps, ReactNode } from \"react\";\nimport { isValidElement } from \"react\";\nimport type { BundledLanguage } from \"shiki\";\n\nimport { CodeBlock } from \"./code-block\";\n\nexport type ToolProps = ComponentProps<typeof Collapsible>;\n\nexport const Tool = ({ className, ...props }: ToolProps) => (\n <Collapsible\n className={cn(\"group not-prose mb-4 w-full rounded-md border\", className)}\n {...props}\n />\n);\n\nexport type ToolPart = ToolUIPart | DynamicToolUIPart;\n\n/**\n * `JSON.stringify` throws on a circular reference or a `BigInt` — both\n * realistic shapes for tool input/output payloads a model produced. Falls\n * back to a readable placeholder instead of crashing the message render.\n */\nfunction safeJsonStringify(value: unknown): string {\n try {\n return JSON.stringify(value, (_key, v) => (typeof v === \"bigint\" ? `${v.toString()}n` : v), 2);\n } catch {\n return String(value);\n }\n}\n\nexport type ToolHeaderProps = {\n title?: string;\n /**\n * The business summary line (\"3 documents found\", \"8 rows reconciled\"),\n * shown beside the name + StatusBadge (#192, research 10 §B.5). Falls back\n * to nothing — the derived tool name still labels the row.\n */\n summary?: ReactNode;\n className?: string;\n} & (\n | { type: ToolUIPart[\"type\"]; state: ToolUIPart[\"state\"]; toolName?: never }\n | {\n type: DynamicToolUIPart[\"type\"];\n state: DynamicToolUIPart[\"state\"];\n toolName: string;\n }\n);\n\n/**\n * Map the AI-SDK `ToolUIPart` 7-state machine onto the canonical `Status`\n * enum (#189, research 10 §B.1 mapping a). Lives here — not in `@elabs-ai/components-ui` —\n * because it is typed against the SDK union; the `ai` import stays TYPES-ONLY\n * (D6, gate-enforced by `pnpm ai:types-only`).\n */\nexport const statusFromToolState = (state: ToolPart[\"state\"]): Status => {\n switch (state) {\n case \"input-streaming\":\n return \"pending\";\n case \"input-available\":\n return \"running\";\n case \"approval-requested\":\n return \"awaiting-approval\";\n case \"approval-responded\":\n return \"running\";\n case \"output-available\":\n return \"complete\";\n case \"output-denied\":\n return \"denied\";\n case \"output-error\":\n return \"failed\";\n }\n};\n\n/** Same signature as before #189; renders the canonical StatusBadge. */\nexport const getStatusBadge = (status: ToolPart[\"state\"]) => (\n <StatusBadge status={statusFromToolState(status)} />\n);\n\nexport const ToolHeader = ({\n className,\n title,\n summary,\n type,\n state,\n toolName,\n ...props\n}: ToolHeaderProps) => {\n const derivedName = type === \"dynamic-tool\" ? toolName : type.split(\"-\").slice(1).join(\"-\");\n\n return (\n <CollapsibleTrigger\n className={cn(\"flex w-full items-center justify-between gap-4 p-3\", className)}\n {...props}\n >\n <div className=\"flex min-w-0 items-center gap-2\">\n <WrenchIcon className=\"size-4 shrink-0 text-muted-foreground\" />\n <span className=\"min-w-0 truncate text-body font-medium\">{title ?? derivedName}</span>\n <StatusBadge status={statusFromToolState(state)} className=\"shrink-0\" />\n {summary ? (\n <span className=\"truncate text-meta text-muted-foreground\">{summary}</span>\n ) : null}\n </div>\n <ChevronDownIcon className=\"size-4 shrink-0 text-muted-foreground transition-transform group-data-[state=open]:rotate-180\" />\n </CollapsibleTrigger>\n );\n};\n\nexport type ToolContentProps = ComponentProps<typeof CollapsibleContent>;\n\nexport const ToolContent = ({ className, ...props }: ToolContentProps) => (\n <CollapsibleContent\n className={cn(\n \"data-[state=closed]:fade-out-0 data-[state=closed]:slide-out-to-top-2 data-[state=open]:slide-in-from-top-2 space-y-4 p-4 text-popover-foreground outline-none data-[state=closed]:animate-out data-[state=open]:animate-in data-[state=open]:[--tw-ease:var(--ease-entrance)] data-[state=closed]:[--tw-ease:var(--ease-exit)]\",\n className,\n )}\n {...props}\n />\n);\n\nexport type ToolDetailsProps = ComponentProps<typeof Collapsible> & {\n /** The disclosure label. */\n label?: ReactNode;\n};\n\n/**\n * The technical view, behind disclosure — the PACKAGE DEFAULT for tool JSON\n * (#192, research 10 §B.5): a nested collapsible, COLLAPSED by default,\n * holding `ToolInput`/`ToolOutput`. The header carries the business `summary`;\n * the raw payload is one expand away, never the headline.\n */\nexport const ToolDetails = ({\n className,\n label,\n defaultOpen = false,\n children,\n ...props\n}: ToolDetailsProps) => {\n const { t } = useLocale();\n return (\n <Collapsible\n className={cn(\"group/tool-details not-prose\", className)}\n defaultOpen={defaultOpen}\n {...props}\n >\n <CollapsibleTrigger className=\"flex items-center gap-1 rounded-sm text-meta text-muted-foreground transition-colors hover:text-foreground focus-ring\">\n <ChevronDownIcon className=\"size-3.5 transition-transform group-data-[state=open]/tool-details:rotate-180\" />\n {label ?? t(\"ai.tool.showTechnicalDetails\")}\n </CollapsibleTrigger>\n <CollapsibleContent className=\"mt-3 space-y-4\">{children}</CollapsibleContent>\n </Collapsible>\n );\n};\n\nexport type ToolInputProps = ComponentProps<\"div\"> & {\n input: ToolPart[\"input\"];\n};\n\nexport const ToolInput = ({ className, input, ...props }: ToolInputProps) => {\n const { t } = useLocale();\n return (\n <div className={cn(\"space-y-2 overflow-hidden\", className)} {...props}>\n <h4 className=\"text-eyebrow uppercase text-muted-foreground\">\n {t(\"ai.schemaDisplay.parameters\")}\n </h4>\n <div className=\"rounded-md bg-muted/50\">\n <CodeBlock code={safeJsonStringify(input)} language=\"json\" />\n </div>\n </div>\n );\n};\n\nexport type ToolOutputProps = ComponentProps<\"div\"> & {\n /**\n * The tool result. Objects/strings render as JSON inside the technical view.\n *\n * @deprecated Passing a pre-rendered **React element** here (the \"rich\n * output under a muted Result heading\" path) is deprecated since #192\n * (research 10 §B.5): a produced artifact is the HEADLINE, not a technical\n * detail — host it in a `<ToolResultCard>` and keep `ToolOutput` for the\n * JSON payload behind `<ToolDetails>`. The element path still renders for\n * existing consumers (e.g. the copy-owned `ai-chart` registry block) but\n * will be removed in a future release.\n */\n output: ToolPart[\"output\"];\n errorText: ToolPart[\"errorText\"];\n /**\n * The call has not produced output yet (loading-states.md `isStreaming`) —\n * derive it from the existing `statusFromToolState(state)` mapping\n * (`\"input-streaming\"`/`\"input-available\"` → not yet `\"complete\"`/`\"failed\"`)\n * rather than a second source of truth. While true and no `output`/\n * `errorText` has arrived, renders a layout-shaped skeleton in the Result\n * slot instead of `null`, so the technical view reserves its space. A\n * still-running call is never a terminal failure — the error branch is\n * suppressed while `isStreaming` per the loading-states.md error rule, even\n * if a stale `errorText` is still set from a previous render.\n * @default false\n */\n isStreaming?: boolean;\n};\n\nexport const ToolOutput = ({\n className,\n output,\n errorText,\n isStreaming = false,\n ...props\n}: ToolOutputProps) => {\n const { t } = useLocale();\n\n // `output` is a defined-but-falsy result (`0`, `false`, `\"\"`) for plenty of\n // real tools (a count, a boolean check, an empty-string field) — only\n // `undefined` means \"no output (yet)\".\n const hasOutput = output !== undefined;\n\n if (!(hasOutput || errorText || isStreaming)) {\n return null;\n }\n\n const showError = !isStreaming && Boolean(errorText);\n const pending = isStreaming && !hasOutput && !errorText;\n\n let Output: ReactNode = null;\n\n if (hasOutput) {\n if (isValidElement(output)) {\n Output = output;\n } else if (typeof output === \"string\") {\n // A tool result is arbitrary text, not guaranteed JSON — forcing the\n // JSON highlighter on it mis-colours ordinary strings. Shiki's own\n // `BundledLanguage` union (grammar-backed languages) omits its\n // hard-coded plain-text pseudo-languages (`isPlainLang`:\n // \"plaintext\" | \"txt\" | \"text\" | \"plain\") — `createHighlighter`/\n // `codeToTokens` accept and special-case them with no grammar load\n // (verified: `getLoadedLanguages()` stays empty, no throw), so this\n // is a type-only gap, not a runtime one.\n Output = <CodeBlock code={output} language={\"text\" as BundledLanguage} />;\n } else {\n // Objects, arrays, numbers, booleans, null, bigint — all safe to\n // stringify for display.\n Output = <CodeBlock code={safeJsonStringify(output)} language=\"json\" />;\n }\n }\n\n return (\n <div className={cn(\"space-y-2\", className)} {...props}>\n <h4 className=\"text-eyebrow uppercase text-muted-foreground\">\n {showError ? t(\"ai.tool.error\") : t(\"ai.tool.result\")}\n </h4>\n {pending ? (\n <div className=\"space-y-2 rounded-md bg-muted/50 p-3\" role=\"status\" aria-live=\"polite\">\n <span className=\"sr-only\">{t(\"loading\")}</span>\n <Skeleton className=\"h-4 w-3/4\" />\n <Skeleton className=\"h-4 w-full\" />\n <Skeleton className=\"h-4 w-1/2\" />\n </div>\n ) : (\n <div\n className={cn(\n \"overflow-x-auto rounded-md text-caption [&_table]:w-full\",\n // #124: this colours the ambient text of the error/result body —\n // running text, so the error branch takes the ink rung.\n showError ? \"bg-destructive/10 text-destructive-text\" : \"bg-muted/50 text-foreground\",\n )}\n >\n {showError && <div>{errorText}</div>}\n {Output}\n </div>\n )}\n </div>\n );\n};\n"],"mappings":"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","names":[]}
@@ -1,7 +1,7 @@
1
1
  "use client";
2
2
  import {
3
3
  getStatusBadge
4
- } from "./chunk-PTWCTSZL.js";
4
+ } from "./chunk-AOOIAFDT.js";
5
5
 
6
6
  // src/sandbox.tsx
7
7
  import {
@@ -105,4 +105,4 @@ export {
105
105
  SandboxTabsTrigger,
106
106
  SandboxTabContent
107
107
  };
108
- //# sourceMappingURL=chunk-FERYDFZA.js.map
108
+ //# sourceMappingURL=chunk-UHLVPAHG.js.map
@@ -11,7 +11,7 @@ import {
11
11
  ToolInput,
12
12
  ToolOutput,
13
13
  statusFromToolState
14
- } from "./chunk-PTWCTSZL.js";
14
+ } from "./chunk-AOOIAFDT.js";
15
15
  import {
16
16
  buildPartGroups,
17
17
  defaultPartStatus,
@@ -146,4 +146,4 @@ function GroupedParts({
146
146
  export {
147
147
  GroupedParts
148
148
  };
149
- //# sourceMappingURL=chunk-WU5BLGTF.js.map
149
+ //# sourceMappingURL=chunk-VJZJWNEC.js.map
@@ -2,9 +2,9 @@
2
2
  "use client";
3
3
  import {
4
4
  GroupedParts
5
- } from "./chunk-WU5BLGTF.js";
5
+ } from "./chunk-VJZJWNEC.js";
6
6
  import "./chunk-X47QMM6K.js";
7
- import "./chunk-PTWCTSZL.js";
7
+ import "./chunk-AOOIAFDT.js";
8
8
  import "./chunk-YSKD7UXX.js";
9
9
  import "./chunk-TWPHAJNM.js";
10
10
  import "./chunk-3YPT3V7L.js";
package/dist/index.js CHANGED
@@ -115,7 +115,7 @@ import {
115
115
  SandboxTabsBar,
116
116
  SandboxTabsList,
117
117
  SandboxTabsTrigger
118
- } from "./chunk-FERYDFZA.js";
118
+ } from "./chunk-UHLVPAHG.js";
119
119
  import {
120
120
  SchemaDisplay,
121
121
  SchemaDisplayBody,
@@ -337,7 +337,7 @@ import {
337
337
  } from "./chunk-3XNPMDZY.js";
338
338
  import {
339
339
  GroupedParts
340
- } from "./chunk-WU5BLGTF.js";
340
+ } from "./chunk-VJZJWNEC.js";
341
341
  import {
342
342
  Reasoning,
343
343
  ReasoningContent,
@@ -353,7 +353,7 @@ import {
353
353
  ToolOutput,
354
354
  getStatusBadge,
355
355
  statusFromToolState
356
- } from "./chunk-PTWCTSZL.js";
356
+ } from "./chunk-AOOIAFDT.js";
357
357
  import {
358
358
  buildPartGroups,
359
359
  defaultPartStatus,
package/dist/sandbox.js CHANGED
@@ -9,8 +9,8 @@ import {
9
9
  SandboxTabsBar,
10
10
  SandboxTabsList,
11
11
  SandboxTabsTrigger
12
- } from "./chunk-FERYDFZA.js";
13
- import "./chunk-PTWCTSZL.js";
12
+ } from "./chunk-UHLVPAHG.js";
13
+ import "./chunk-AOOIAFDT.js";
14
14
  import "./chunk-TWPHAJNM.js";
15
15
  import "./chunk-3YPT3V7L.js";
16
16
  import "./chunk-3TDHPXZZ.js";
package/dist/tool.js CHANGED
@@ -9,7 +9,7 @@ import {
9
9
  ToolOutput,
10
10
  getStatusBadge,
11
11
  statusFromToolState
12
- } from "./chunk-PTWCTSZL.js";
12
+ } from "./chunk-AOOIAFDT.js";
13
13
  import "./chunk-TWPHAJNM.js";
14
14
  import "./chunk-3YPT3V7L.js";
15
15
  import "./chunk-3TDHPXZZ.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@elabs-ai/components-ai",
3
- "version": "5.3.1",
3
+ "version": "5.4.0",
4
4
  "license": "MIT",
5
5
  "repository": {
6
6
  "type": "git",
@@ -50,9 +50,9 @@
50
50
  "mermaid": "^11.12.2",
51
51
  "react": "^18.2.0 || ^19.0.0",
52
52
  "react-dom": "^18.2.0 || ^19.0.0",
53
- "@elabs-ai/components-icons": "^5.3.1",
54
- "@elabs-ai/components-tokens": "^5.3.1",
55
- "@elabs-ai/components-ui": "^5.3.1"
53
+ "@elabs-ai/components-icons": "^5.4.0",
54
+ "@elabs-ai/components-tokens": "^5.4.0",
55
+ "@elabs-ai/components-ui": "^5.4.0"
56
56
  },
57
57
  "peerDependenciesMeta": {
58
58
  "@rive-app/react-webgl2": {
@@ -87,12 +87,12 @@
87
87
  "tsup": "^8.3.5",
88
88
  "typescript": "^5.7.3",
89
89
  "vitest": "^3.0.2",
90
- "@elabs-ai/components-charts": "5.3.1",
90
+ "@elabs-ai/components-charts": "5.4.0",
91
91
  "@elabs-ai/components-eslint-config": "0.1.0",
92
- "@elabs-ai/components-icons": "5.3.1",
93
- "@elabs-ai/components-tokens": "5.3.1",
92
+ "@elabs-ai/components-icons": "5.4.0",
93
+ "@elabs-ai/components-tokens": "5.4.0",
94
94
  "@elabs-ai/components-typescript-config": "0.1.0",
95
- "@elabs-ai/components-ui": "5.3.1"
95
+ "@elabs-ai/components-ui": "5.4.0"
96
96
  },
97
97
  "scripts": {
98
98
  "build": "tsup",
@@ -762,7 +762,7 @@
762
762
  "type": "boolean"
763
763
  },
764
764
  "spec": {
765
- "description": "{ data: row[], x: string, series: string[] | { key, label?, axis?: left|right, mark?: line|area|column }[] (type: \"dual-axis\" only: default axis left, mark line; needs ≥1 line series, no right-axis columns), type?: line|area|bar|pie|scatter|radar|funnel|candlestick|heatmap|calendar|waterfall|dumbbell|unit|treemap|histogram|box|strip|bump|stream|diverging-bar|dual-axis, xType?: time|category|number, y2?, axes?: { y2?: { align?: independent|ticks, proportional?: boolean, zero?: both|auto } } (type: \"dual-axis\" only, right axis vs left; default align ticks, zero auto), group?, title?, description?, stacked?: boolean|percent|diverging, orientation?: vertical|horizontal, donut?, legend?: boolean (an object form exists; AutoChart reads it as boolean truthy today), valueFormat?: number|compact|currency|percent, currency?, palette?: mono|sequential|categorical, emphasis?: analytical|editorial, kind?: steps|records|ranking|change (change: a two-measure spec reads as a before/after move, for dumbbell), nulls?: gap|zero|connect (line/area/stream non-numeric sample, default gap), curve?: linear|monotone|natural|step|step-before|step-after (line/area/stream, default monotone), symbols?: { placement?: all|ends|first|last, shape?: circle|square|triangle|diamond|cross|star|plus|hexagon, style?: filled|hollow, size? } (line/area/stream point markers), size?: { key, range?: [lo,hi] } (scatter bubble size), shapeBy?: { key, shapes?: marker-shape[] } (scatter shape by category), trend?: linear|log (scatter trend line), shapes?: [{ kind: line|path, … }] (scatter reference lines/areas), variant?: dumbbell|slope|arrow|dots (dumbbell only, default dumbbell), delta?: { show, mode: absolute|percent } (dumbbell delta label), groupSmall?: { threshold?, max?, label? } (pie: fold small slices into an Other slice), half?: boolean (pie half-donut, default false), labels?: { series?: end|key|none, values?: { placement: first|last|all|peaks, count?, minGap?, outline?, matchColor?, format? }, points?: { key, mode?: auto|all, priorityKey? }, slices?: { placement?: inside|outside|none, show: (label|value|percent)[], matchColor?, minAngle? }, comparison?: value|difference|none } (label engine), annotations?: [{ kind: text|range|line|row, … }] (notes, bands, reference lines, row notes in data units), divergingCenter?: string (neutral series when stacked is diverging), sort?: asc|desc|none|{by,dir} (bar) | start|end|delta|deltaPercent|data|label|none (dumbbell) | desc|none (pie) | data|increasesFirst|decreasesFirst (waterfall, default data), groupBy?: string (bar/dumbbell row grouping; waterfall: a subtotal after each group), colorBy?: { key, scale?: categorical|sequential|diverging, steps? } (bar per-bar / scatter per-point colour), overlays?: [{ kind: value|range, … }] (bar value markers, range spans), comparison?: { key, label? } (bar muted prior-period column), notes?: string (italic notes under an enclosing ChartFrame), byline?: { kind?: chart|map|table, author } (ChartFrame footer: kind + author), source?: string | { name, href? } (ChartFrame footer attribution), altText?: string (image text alternative, default: description), tooltip?: { variant?: rows|table|inline, focus?: boolean, pin?: boolean } (forwarded to ChartTooltip, default rows), facet?: { by: string | { series: true }, columns?, scales?: { y?: shared|independent, rangeRounding? }, sort?: start|end|delta|deltaPercent|range|title|data, baseline?: { key } | { series }, panelHeight? } (line/area/bar/pie small multiples), dataFormat?: differences|runningTotals (waterfall only, default differences), zoomToDifferences?: boolean (waterfall only, default false) }",
765
+ "description": "{ data: row[], x: string, series: string[] | { key, label?, axis?: left|right, mark?: line|area|column }[] (type: \"dual-axis\" only: default axis left, mark line; needs ≥1 line series, no right-axis columns), type?: line|area|bar|pie|scatter|radar|funnel|candlestick|heatmap|calendar|waterfall|dumbbell|unit|treemap|histogram|box|strip|bump|stream|diverging-bar|dual-axis, xType?: time|category|number, y2?, axes?: { y2?: { align?: independent|ticks, proportional?: boolean, zero?: both|auto } } (type: \"dual-axis\" only, right axis vs left; default align ticks, zero auto), group?, title?, description?, stacked?: boolean|percent|diverging, orientation?: vertical|horizontal, donut?, legend?: boolean (an object form exists; AutoChart reads it as boolean truthy today), valueFormat?: number|compact|currency|percent, currency?, palette?: mono|sequential|categorical, emphasis?: analytical|editorial, kind?: steps|records|ranking|change (change: a two-measure spec reads as a before/after move, for dumbbell), nulls?: gap|zero|connect (line/area/stream non-numeric sample, default gap), curve?: linear|monotone|natural|step|step-before|step-after (line/area/stream, default monotone), symbols?: { placement?: all|ends|first|last, shape?: circle|square|triangle|diamond|cross|star|plus|hexagon, style?: filled|hollow, size? } (line/area/stream point markers), size?: { key, range?: [lo,hi] } (scatter bubble size), shapeBy?: { key, shapes?: marker-shape[] } (scatter shape by category), trend?: linear|log (scatter trend line), shapes?: [{ kind: line|path, … }] (scatter reference lines/areas), variant?: dumbbell|slope|arrow|dots (dumbbell only, default dumbbell), delta?: { show, mode: absolute|percent } (dumbbell delta label), groupSmall?: { threshold?, max?, label? } (pie: fold small slices into an Other slice), half?: boolean (pie half-donut, default false), labels?: { series?: end|key|none, values?: { placement: first|last|all|peaks, count?, minGap?, outline?, matchColor?, format? }, points?: { key, mode?: auto|all, priorityKey? }, slices?: { placement?: inside|outside|none, show: (label|value|percent)[], matchColor?, minAngle? }, comparison?: value|difference|none } (label engine), annotations?: [{ kind: text|range|line|row, … }] (notes, bands, reference lines, row notes in data units), analytics?: [{ kind: line|band|trend|window|forecast|errorBars, of?: series|all, … }] (computed overlays — line: { value: mean|median|min|max|sum|number|{ percentile }|{ stddev, around? }, axis?: x|y, label?: none|value|computation|text, ifOverflow?: clip|extend }; band: { from, to } | { spread: { percentiles: [lo,hi] }|{ stddev }|{ ci } }; trend: { model?: linear|log|exp|pow|{ poly: 2..6 }|{ loess }, ci?, extent?: data|domain }; window: { k, reduce?: mean|median|sum|min|max|ewm, replace? }; forecast: { horizon, season?, interval? }; errorBars: { low: field|{ percent }, high?, band? }), selection?: { gestures: (range|rect|lasso|radial)[], confirm?: immediate|explicit, field? } (bar/line/area/scatter/heatmap/histogram/box/strip selection gestures with a toolbar — a bar chart with range and lasso selection is { gestures: [range, lasso] }; explicit previews until the reader confirms; intents arrive as the selectionIntent event), divergingCenter?: string (neutral series when stacked is diverging), sort?: asc|desc|none|{by,dir} (bar) | start|end|delta|deltaPercent|data|label|none (dumbbell) | desc|none (pie) | data|increasesFirst|decreasesFirst (waterfall, default data), groupBy?: string (bar/dumbbell row grouping; waterfall: a subtotal after each group), colorBy?: { key, scale?: categorical|sequential|diverging, steps? } (bar per-bar / scatter per-point colour), overlays?: [{ kind: value|range, … }] (bar value markers, range spans), comparison?: { key, label? } (bar muted prior-period column), notes?: string (italic notes under an enclosing ChartFrame), byline?: { kind?: chart|map|table, author } (ChartFrame footer: kind + author), source?: string | { name, href? } (ChartFrame footer attribution), altText?: string (image text alternative, default: description), tooltip?: { variant?: rows|table|inline, focus?: boolean, pin?: boolean } (forwarded to ChartTooltip, default rows), facet?: { by: string | { series: true }, columns?, scales?: { y?: shared|independent, rangeRounding? }, sort?: start|end|delta|deltaPercent|range|title|data, baseline?: { key } | { series }, panelHeight? } (line/area/bar/pie small multiples), dataFormat?: differences|runningTotals (waterfall only, default differences), zoomToDifferences?: boolean (waterfall only, default false), scrollbar?: miniChart|bar|auto|none (bar/diverging-bar/heatmap/calendar and line/area on a category x: an overview strip that scrolls the categories; default none, or auto when maxVisibleItems is set), maxVisibleItems?: number (categories shown at once, the rest scroll behind the strip and the value axis keeps the full domain; for > 30 categories set maxVisibleItems) }",
766
766
  "type": "object"
767
767
  }
768
768
  },
@@ -774,6 +774,9 @@
774
774
  "properties": {
775
775
  "datapointClick": {
776
776
  "$ref": "#/$defs/action"
777
+ },
778
+ "selectionIntent": {
779
+ "$ref": "#/$defs/action"
777
780
  }
778
781
  },
779
782
  "additionalProperties": false
@@ -3195,7 +3198,7 @@
3195
3198
  "type": "boolean"
3196
3199
  },
3197
3200
  "columns": {
3198
- "description": "Target columns at the largest breakpoint. Defaults to 4.",
3201
+ "description": "Target columns once the grid's own container is wide enough (container queries, not the viewport — see `colsMap`). Defaults to 4.",
3199
3202
  "enum": [2, 3, 4]
3200
3203
  },
3201
3204
  "featured": {
@@ -574,7 +574,7 @@
574
574
  "spec": {
575
575
  "type": "object",
576
576
  "required": true,
577
- "description": "{ data: row[], x: string, series: string[] | { key, label?, axis?: left|right, mark?: line|area|column }[] (type: \"dual-axis\" only: default axis left, mark line; needs ≥1 line series, no right-axis columns), type?: line|area|bar|pie|scatter|radar|funnel|candlestick|heatmap|calendar|waterfall|dumbbell|unit|treemap|histogram|box|strip|bump|stream|diverging-bar|dual-axis, xType?: time|category|number, y2?, axes?: { y2?: { align?: independent|ticks, proportional?: boolean, zero?: both|auto } } (type: \"dual-axis\" only, right axis vs left; default align ticks, zero auto), group?, title?, description?, stacked?: boolean|percent|diverging, orientation?: vertical|horizontal, donut?, legend?: boolean (an object form exists; AutoChart reads it as boolean truthy today), valueFormat?: number|compact|currency|percent, currency?, palette?: mono|sequential|categorical, emphasis?: analytical|editorial, kind?: steps|records|ranking|change (change: a two-measure spec reads as a before/after move, for dumbbell), nulls?: gap|zero|connect (line/area/stream non-numeric sample, default gap), curve?: linear|monotone|natural|step|step-before|step-after (line/area/stream, default monotone), symbols?: { placement?: all|ends|first|last, shape?: circle|square|triangle|diamond|cross|star|plus|hexagon, style?: filled|hollow, size? } (line/area/stream point markers), size?: { key, range?: [lo,hi] } (scatter bubble size), shapeBy?: { key, shapes?: marker-shape[] } (scatter shape by category), trend?: linear|log (scatter trend line), shapes?: [{ kind: line|path, … }] (scatter reference lines/areas), variant?: dumbbell|slope|arrow|dots (dumbbell only, default dumbbell), delta?: { show, mode: absolute|percent } (dumbbell delta label), groupSmall?: { threshold?, max?, label? } (pie: fold small slices into an Other slice), half?: boolean (pie half-donut, default false), labels?: { series?: end|key|none, values?: { placement: first|last|all|peaks, count?, minGap?, outline?, matchColor?, format? }, points?: { key, mode?: auto|all, priorityKey? }, slices?: { placement?: inside|outside|none, show: (label|value|percent)[], matchColor?, minAngle? }, comparison?: value|difference|none } (label engine), annotations?: [{ kind: text|range|line|row, … }] (notes, bands, reference lines, row notes in data units), divergingCenter?: string (neutral series when stacked is diverging), sort?: asc|desc|none|{by,dir} (bar) | start|end|delta|deltaPercent|data|label|none (dumbbell) | desc|none (pie) | data|increasesFirst|decreasesFirst (waterfall, default data), groupBy?: string (bar/dumbbell row grouping; waterfall: a subtotal after each group), colorBy?: { key, scale?: categorical|sequential|diverging, steps? } (bar per-bar / scatter per-point colour), overlays?: [{ kind: value|range, … }] (bar value markers, range spans), comparison?: { key, label? } (bar muted prior-period column), notes?: string (italic notes under an enclosing ChartFrame), byline?: { kind?: chart|map|table, author } (ChartFrame footer: kind + author), source?: string | { name, href? } (ChartFrame footer attribution), altText?: string (image text alternative, default: description), tooltip?: { variant?: rows|table|inline, focus?: boolean, pin?: boolean } (forwarded to ChartTooltip, default rows), facet?: { by: string | { series: true }, columns?, scales?: { y?: shared|independent, rangeRounding? }, sort?: start|end|delta|deltaPercent|range|title|data, baseline?: { key } | { series }, panelHeight? } (line/area/bar/pie small multiples), dataFormat?: differences|runningTotals (waterfall only, default differences), zoomToDifferences?: boolean (waterfall only, default false) }"
577
+ "description": "{ data: row[], x: string, series: string[] | { key, label?, axis?: left|right, mark?: line|area|column }[] (type: \"dual-axis\" only: default axis left, mark line; needs ≥1 line series, no right-axis columns), type?: line|area|bar|pie|scatter|radar|funnel|candlestick|heatmap|calendar|waterfall|dumbbell|unit|treemap|histogram|box|strip|bump|stream|diverging-bar|dual-axis, xType?: time|category|number, y2?, axes?: { y2?: { align?: independent|ticks, proportional?: boolean, zero?: both|auto } } (type: \"dual-axis\" only, right axis vs left; default align ticks, zero auto), group?, title?, description?, stacked?: boolean|percent|diverging, orientation?: vertical|horizontal, donut?, legend?: boolean (an object form exists; AutoChart reads it as boolean truthy today), valueFormat?: number|compact|currency|percent, currency?, palette?: mono|sequential|categorical, emphasis?: analytical|editorial, kind?: steps|records|ranking|change (change: a two-measure spec reads as a before/after move, for dumbbell), nulls?: gap|zero|connect (line/area/stream non-numeric sample, default gap), curve?: linear|monotone|natural|step|step-before|step-after (line/area/stream, default monotone), symbols?: { placement?: all|ends|first|last, shape?: circle|square|triangle|diamond|cross|star|plus|hexagon, style?: filled|hollow, size? } (line/area/stream point markers), size?: { key, range?: [lo,hi] } (scatter bubble size), shapeBy?: { key, shapes?: marker-shape[] } (scatter shape by category), trend?: linear|log (scatter trend line), shapes?: [{ kind: line|path, … }] (scatter reference lines/areas), variant?: dumbbell|slope|arrow|dots (dumbbell only, default dumbbell), delta?: { show, mode: absolute|percent } (dumbbell delta label), groupSmall?: { threshold?, max?, label? } (pie: fold small slices into an Other slice), half?: boolean (pie half-donut, default false), labels?: { series?: end|key|none, values?: { placement: first|last|all|peaks, count?, minGap?, outline?, matchColor?, format? }, points?: { key, mode?: auto|all, priorityKey? }, slices?: { placement?: inside|outside|none, show: (label|value|percent)[], matchColor?, minAngle? }, comparison?: value|difference|none } (label engine), annotations?: [{ kind: text|range|line|row, … }] (notes, bands, reference lines, row notes in data units), analytics?: [{ kind: line|band|trend|window|forecast|errorBars, of?: series|all, … }] (computed overlays — line: { value: mean|median|min|max|sum|number|{ percentile }|{ stddev, around? }, axis?: x|y, label?: none|value|computation|text, ifOverflow?: clip|extend }; band: { from, to } | { spread: { percentiles: [lo,hi] }|{ stddev }|{ ci } }; trend: { model?: linear|log|exp|pow|{ poly: 2..6 }|{ loess }, ci?, extent?: data|domain }; window: { k, reduce?: mean|median|sum|min|max|ewm, replace? }; forecast: { horizon, season?, interval? }; errorBars: { low: field|{ percent }, high?, band? }), selection?: { gestures: (range|rect|lasso|radial)[], confirm?: immediate|explicit, field? } (bar/line/area/scatter/heatmap/histogram/box/strip selection gestures with a toolbar — a bar chart with range and lasso selection is { gestures: [range, lasso] }; explicit previews until the reader confirms; intents arrive as the selectionIntent event), divergingCenter?: string (neutral series when stacked is diverging), sort?: asc|desc|none|{by,dir} (bar) | start|end|delta|deltaPercent|data|label|none (dumbbell) | desc|none (pie) | data|increasesFirst|decreasesFirst (waterfall, default data), groupBy?: string (bar/dumbbell row grouping; waterfall: a subtotal after each group), colorBy?: { key, scale?: categorical|sequential|diverging, steps? } (bar per-bar / scatter per-point colour), overlays?: [{ kind: value|range, … }] (bar value markers, range spans), comparison?: { key, label? } (bar muted prior-period column), notes?: string (italic notes under an enclosing ChartFrame), byline?: { kind?: chart|map|table, author } (ChartFrame footer: kind + author), source?: string | { name, href? } (ChartFrame footer attribution), altText?: string (image text alternative, default: description), tooltip?: { variant?: rows|table|inline, focus?: boolean, pin?: boolean } (forwarded to ChartTooltip, default rows), facet?: { by: string | { series: true }, columns?, scales?: { y?: shared|independent, rangeRounding? }, sort?: start|end|delta|deltaPercent|range|title|data, baseline?: { key } | { series }, panelHeight? } (line/area/bar/pie small multiples), dataFormat?: differences|runningTotals (waterfall only, default differences), zoomToDifferences?: boolean (waterfall only, default false), scrollbar?: miniChart|bar|auto|none (bar/diverging-bar/heatmap/calendar and line/area on a category x: an overview strip that scrolls the categories; default none, or auto when maxVisibleItems is set), maxVisibleItems?: number (categories shown at once, the rest scroll behind the strip and the value axis keeps the full domain; for > 30 categories set maxVisibleItems) }"
578
578
  },
579
579
  "height": {
580
580
  "type": "number",
@@ -585,7 +585,8 @@
585
585
  }
586
586
  },
587
587
  "events": {
588
- "datapointClick": "onDatapointClick"
588
+ "datapointClick": "onDatapointClick",
589
+ "selectionIntent": "onSelectionIntent"
589
590
  },
590
591
  "omit": ["copyValueOnActivate", "dimExcluded"]
591
592
  },
package/src/tool.test.tsx CHANGED
@@ -32,6 +32,32 @@ describe("Tool JSON-behind-disclosure (#192, research 10 §B.5)", () => {
32
32
  });
33
33
  });
34
34
 
35
+ describe("ToolHeader layout in narrow containers (#598)", () => {
36
+ it("StatusBadge element has shrink-0 class to prevent clipping in narrow flex containers", () => {
37
+ const { container } = render(
38
+ <Tool>
39
+ <ToolHeader type="tool-search_web" state="output-available" summary="3 results found" />
40
+ </Tool>,
41
+ );
42
+ // StatusBadge renders as a span with data-slot="status-badge"
43
+ const statusBadgeSpan = container.querySelector('span[data-slot="status-badge"]');
44
+ expect(statusBadgeSpan).not.toBeNull();
45
+ expect(statusBadgeSpan).toHaveClass("shrink-0");
46
+ });
47
+
48
+ it("title span has min-w-0 and truncate classes to yield space to fixed-width siblings", () => {
49
+ render(
50
+ <Tool>
51
+ <ToolHeader type="tool-search_web" state="output-available" title="Search Tool" />
52
+ </Tool>,
53
+ );
54
+ // Find the title span (it contains the title text and comes after the wrench icon)
55
+ const titleSpan = screen.getByText("Search Tool");
56
+ expect(titleSpan).toHaveClass("min-w-0");
57
+ expect(titleSpan).toHaveClass("truncate");
58
+ });
59
+ });
60
+
35
61
  describe("ToolOutput isStreaming (#269, loading-states.md)", () => {
36
62
  it("renders a skeleton (not null) while streaming with no output yet", () => {
37
63
  const { container } = render(
package/src/tool.tsx CHANGED
@@ -101,8 +101,8 @@ export const ToolHeader = ({
101
101
  >
102
102
  <div className="flex min-w-0 items-center gap-2">
103
103
  <WrenchIcon className="size-4 shrink-0 text-muted-foreground" />
104
- <span className="shrink-0 text-body font-medium">{title ?? derivedName}</span>
105
- {getStatusBadge(state)}
104
+ <span className="min-w-0 truncate text-body font-medium">{title ?? derivedName}</span>
105
+ <StatusBadge status={statusFromToolState(state)} className="shrink-0" />
106
106
  {summary ? (
107
107
  <span className="truncate text-meta text-muted-foreground">{summary}</span>
108
108
  ) : null}
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/tool.tsx"],"sourcesContent":["\"use client\";\n\nimport { Skeleton, StatusBadge, useLocale, type Status } from \"@elabs-ai/components-ui\";\nimport { Collapsible, CollapsibleContent, CollapsibleTrigger } from \"@elabs-ai/components-ui\";\nimport { cn } from \"@elabs-ai/components-ui/lib/cn\";\nimport type { DynamicToolUIPart, ToolUIPart } from \"ai\";\nimport { ChevronDownIcon, WrenchIcon } from \"lucide-react\";\nimport type { ComponentProps, ReactNode } from \"react\";\nimport { isValidElement } from \"react\";\nimport type { BundledLanguage } from \"shiki\";\n\nimport { CodeBlock } from \"./code-block\";\n\nexport type ToolProps = ComponentProps<typeof Collapsible>;\n\nexport const Tool = ({ className, ...props }: ToolProps) => (\n <Collapsible\n className={cn(\"group not-prose mb-4 w-full rounded-md border\", className)}\n {...props}\n />\n);\n\nexport type ToolPart = ToolUIPart | DynamicToolUIPart;\n\n/**\n * `JSON.stringify` throws on a circular reference or a `BigInt` — both\n * realistic shapes for tool input/output payloads a model produced. Falls\n * back to a readable placeholder instead of crashing the message render.\n */\nfunction safeJsonStringify(value: unknown): string {\n try {\n return JSON.stringify(value, (_key, v) => (typeof v === \"bigint\" ? `${v.toString()}n` : v), 2);\n } catch {\n return String(value);\n }\n}\n\nexport type ToolHeaderProps = {\n title?: string;\n /**\n * The business summary line (\"3 documents found\", \"8 rows reconciled\"),\n * shown beside the name + StatusBadge (#192, research 10 §B.5). Falls back\n * to nothing — the derived tool name still labels the row.\n */\n summary?: ReactNode;\n className?: string;\n} & (\n | { type: ToolUIPart[\"type\"]; state: ToolUIPart[\"state\"]; toolName?: never }\n | {\n type: DynamicToolUIPart[\"type\"];\n state: DynamicToolUIPart[\"state\"];\n toolName: string;\n }\n);\n\n/**\n * Map the AI-SDK `ToolUIPart` 7-state machine onto the canonical `Status`\n * enum (#189, research 10 §B.1 mapping a). Lives here — not in `@elabs-ai/components-ui` —\n * because it is typed against the SDK union; the `ai` import stays TYPES-ONLY\n * (D6, gate-enforced by `pnpm ai:types-only`).\n */\nexport const statusFromToolState = (state: ToolPart[\"state\"]): Status => {\n switch (state) {\n case \"input-streaming\":\n return \"pending\";\n case \"input-available\":\n return \"running\";\n case \"approval-requested\":\n return \"awaiting-approval\";\n case \"approval-responded\":\n return \"running\";\n case \"output-available\":\n return \"complete\";\n case \"output-denied\":\n return \"denied\";\n case \"output-error\":\n return \"failed\";\n }\n};\n\n/** Same signature as before #189; renders the canonical StatusBadge. */\nexport const getStatusBadge = (status: ToolPart[\"state\"]) => (\n <StatusBadge status={statusFromToolState(status)} />\n);\n\nexport const ToolHeader = ({\n className,\n title,\n summary,\n type,\n state,\n toolName,\n ...props\n}: ToolHeaderProps) => {\n const derivedName = type === \"dynamic-tool\" ? toolName : type.split(\"-\").slice(1).join(\"-\");\n\n return (\n <CollapsibleTrigger\n className={cn(\"flex w-full items-center justify-between gap-4 p-3\", className)}\n {...props}\n >\n <div className=\"flex min-w-0 items-center gap-2\">\n <WrenchIcon className=\"size-4 shrink-0 text-muted-foreground\" />\n <span className=\"shrink-0 text-body font-medium\">{title ?? derivedName}</span>\n {getStatusBadge(state)}\n {summary ? (\n <span className=\"truncate text-meta text-muted-foreground\">{summary}</span>\n ) : null}\n </div>\n <ChevronDownIcon className=\"size-4 shrink-0 text-muted-foreground transition-transform group-data-[state=open]:rotate-180\" />\n </CollapsibleTrigger>\n );\n};\n\nexport type ToolContentProps = ComponentProps<typeof CollapsibleContent>;\n\nexport const ToolContent = ({ className, ...props }: ToolContentProps) => (\n <CollapsibleContent\n className={cn(\n \"data-[state=closed]:fade-out-0 data-[state=closed]:slide-out-to-top-2 data-[state=open]:slide-in-from-top-2 space-y-4 p-4 text-popover-foreground outline-none data-[state=closed]:animate-out data-[state=open]:animate-in data-[state=open]:[--tw-ease:var(--ease-entrance)] data-[state=closed]:[--tw-ease:var(--ease-exit)]\",\n className,\n )}\n {...props}\n />\n);\n\nexport type ToolDetailsProps = ComponentProps<typeof Collapsible> & {\n /** The disclosure label. */\n label?: ReactNode;\n};\n\n/**\n * The technical view, behind disclosure — the PACKAGE DEFAULT for tool JSON\n * (#192, research 10 §B.5): a nested collapsible, COLLAPSED by default,\n * holding `ToolInput`/`ToolOutput`. The header carries the business `summary`;\n * the raw payload is one expand away, never the headline.\n */\nexport const ToolDetails = ({\n className,\n label,\n defaultOpen = false,\n children,\n ...props\n}: ToolDetailsProps) => {\n const { t } = useLocale();\n return (\n <Collapsible\n className={cn(\"group/tool-details not-prose\", className)}\n defaultOpen={defaultOpen}\n {...props}\n >\n <CollapsibleTrigger className=\"flex items-center gap-1 rounded-sm text-meta text-muted-foreground transition-colors hover:text-foreground focus-ring\">\n <ChevronDownIcon className=\"size-3.5 transition-transform group-data-[state=open]/tool-details:rotate-180\" />\n {label ?? t(\"ai.tool.showTechnicalDetails\")}\n </CollapsibleTrigger>\n <CollapsibleContent className=\"mt-3 space-y-4\">{children}</CollapsibleContent>\n </Collapsible>\n );\n};\n\nexport type ToolInputProps = ComponentProps<\"div\"> & {\n input: ToolPart[\"input\"];\n};\n\nexport const ToolInput = ({ className, input, ...props }: ToolInputProps) => {\n const { t } = useLocale();\n return (\n <div className={cn(\"space-y-2 overflow-hidden\", className)} {...props}>\n <h4 className=\"text-eyebrow uppercase text-muted-foreground\">\n {t(\"ai.schemaDisplay.parameters\")}\n </h4>\n <div className=\"rounded-md bg-muted/50\">\n <CodeBlock code={safeJsonStringify(input)} language=\"json\" />\n </div>\n </div>\n );\n};\n\nexport type ToolOutputProps = ComponentProps<\"div\"> & {\n /**\n * The tool result. Objects/strings render as JSON inside the technical view.\n *\n * @deprecated Passing a pre-rendered **React element** here (the \"rich\n * output under a muted Result heading\" path) is deprecated since #192\n * (research 10 §B.5): a produced artifact is the HEADLINE, not a technical\n * detail — host it in a `<ToolResultCard>` and keep `ToolOutput` for the\n * JSON payload behind `<ToolDetails>`. The element path still renders for\n * existing consumers (e.g. the copy-owned `ai-chart` registry block) but\n * will be removed in a future release.\n */\n output: ToolPart[\"output\"];\n errorText: ToolPart[\"errorText\"];\n /**\n * The call has not produced output yet (loading-states.md `isStreaming`) —\n * derive it from the existing `statusFromToolState(state)` mapping\n * (`\"input-streaming\"`/`\"input-available\"` → not yet `\"complete\"`/`\"failed\"`)\n * rather than a second source of truth. While true and no `output`/\n * `errorText` has arrived, renders a layout-shaped skeleton in the Result\n * slot instead of `null`, so the technical view reserves its space. A\n * still-running call is never a terminal failure — the error branch is\n * suppressed while `isStreaming` per the loading-states.md error rule, even\n * if a stale `errorText` is still set from a previous render.\n * @default false\n */\n isStreaming?: boolean;\n};\n\nexport const ToolOutput = ({\n className,\n output,\n errorText,\n isStreaming = false,\n ...props\n}: ToolOutputProps) => {\n const { t } = useLocale();\n\n // `output` is a defined-but-falsy result (`0`, `false`, `\"\"`) for plenty of\n // real tools (a count, a boolean check, an empty-string field) — only\n // `undefined` means \"no output (yet)\".\n const hasOutput = output !== undefined;\n\n if (!(hasOutput || errorText || isStreaming)) {\n return null;\n }\n\n const showError = !isStreaming && Boolean(errorText);\n const pending = isStreaming && !hasOutput && !errorText;\n\n let Output: ReactNode = null;\n\n if (hasOutput) {\n if (isValidElement(output)) {\n Output = output;\n } else if (typeof output === \"string\") {\n // A tool result is arbitrary text, not guaranteed JSON — forcing the\n // JSON highlighter on it mis-colours ordinary strings. Shiki's own\n // `BundledLanguage` union (grammar-backed languages) omits its\n // hard-coded plain-text pseudo-languages (`isPlainLang`:\n // \"plaintext\" | \"txt\" | \"text\" | \"plain\") — `createHighlighter`/\n // `codeToTokens` accept and special-case them with no grammar load\n // (verified: `getLoadedLanguages()` stays empty, no throw), so this\n // is a type-only gap, not a runtime one.\n Output = <CodeBlock code={output} language={\"text\" as BundledLanguage} />;\n } else {\n // Objects, arrays, numbers, booleans, null, bigint — all safe to\n // stringify for display.\n Output = <CodeBlock code={safeJsonStringify(output)} language=\"json\" />;\n }\n }\n\n return (\n <div className={cn(\"space-y-2\", className)} {...props}>\n <h4 className=\"text-eyebrow uppercase text-muted-foreground\">\n {showError ? t(\"ai.tool.error\") : t(\"ai.tool.result\")}\n </h4>\n {pending ? (\n <div className=\"space-y-2 rounded-md bg-muted/50 p-3\" role=\"status\" aria-live=\"polite\">\n <span className=\"sr-only\">{t(\"loading\")}</span>\n <Skeleton className=\"h-4 w-3/4\" />\n <Skeleton className=\"h-4 w-full\" />\n <Skeleton className=\"h-4 w-1/2\" />\n </div>\n ) : (\n <div\n className={cn(\n \"overflow-x-auto rounded-md text-caption [&_table]:w-full\",\n // #124: this colours the ambient text of the error/result body —\n // running text, so the error branch takes the ink rung.\n showError ? \"bg-destructive/10 text-destructive-text\" : \"bg-muted/50 text-foreground\",\n )}\n >\n {showError && <div>{errorText}</div>}\n {Output}\n </div>\n )}\n </div>\n );\n};\n"],"mappings":"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","names":[]}