@burtson-labs/ui 0.14.1 → 0.15.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.
package/README.md CHANGED
@@ -58,7 +58,7 @@ npx shadcn@latest add https://ui.burtson.ai/r/button.json
58
58
 
59
59
  ## <picture><source media="(prefers-color-scheme: dark)" srcset="https://icons.burtson.ai/svg-white/panel-grid.svg"/><img src="https://icons.burtson.ai/svg-black/panel-grid.svg" align="center" alt=""/></picture> Components
60
60
 
61
- Accordion · Alert · Alert Dialog · App Shell · Attachment · Audio Player · Avatar · Badge · Breadcrumb · Button · Card · Chat History · Chat Layout · Checkbox · Checkbox Card · Collapsible · Combobox · Command · Composer · Connection Status · Context Menu · Conversation · Copy Button · Data Table · Dialog · Dropdown Menu · Editor Tabs · Empty State · Error Summary · Field · Form Actions · Icon Button · Input · Kbd · Label · Markdown · Menubar · Message · Message Actions · Mobile Nav · Native Select · Navigation Menu · Number Input · Onboarding Checklist · Page Header · Pagination · Popover · Progress · Radio Group · Reasoning · Resizable · Scroll Area · Secret Input · Select · Separator · Sheet · Skeleton · Slider · Source · Spinner · Stat Card · Status · Steps · Switch · Table · Tabs · Textarea · Toast · Toaster · Tool Call · Toolbar · Tooltip · Tour · Tree View · Voice Recorder
61
+ Accordion · Agent Run · Alert · Alert Dialog · App Shell · Attachment · Audio Player · Avatar · Avatar Upload · Badge · Breadcrumb · Button · Card · Chat History · Chat Layout · Checkbox · Checkbox Card · Collapsible · Combobox · Command · Composer · Connection Status · Context Menu · Conversation · Copy Button · Data Table · Dialog · Dropdown Menu · Editor Tabs · Empty State · Error Summary · Field · Form Actions · Icon Button · Input · Kbd · Label · Markdown · Menubar · Message · Message Actions · Mobile Nav · Native Select · Navigation Menu · Number Input · Onboarding Checklist · Page Header · Pagination · Popover · Progress · Radio Group · Reasoning · Resizable · Scroll Area · Secret Input · Select · Separator · Sheet · Skeleton · Slider · Source · Spinner · Stat Card · Status · Steps · Switch · Table · Tabs · Textarea · Toast · Toaster · Tool Call · Toolbar · Tooltip · Tour · Tree View · Voice Recorder
62
62
 
63
63
  Live previews, when-to-use notes, accessibility notes and code for each one are at [ui.burtson.ai](https://ui.burtson.ai/docs/components/button). Building a chat app? The [chat recipe](https://ui.burtson.ai/docs/recipes/chat) puts history, attachments, streaming, voice notes and full screen together, with the complete source. Building a form? `Field` wires label, help and error onto any control, `FieldGrid` lines fields up and `FormActions` places the buttons, on phones too.
64
64
 
@@ -0,0 +1,22 @@
1
+ import * as React from 'react';
2
+ export type AgentRunState = 'queued' | 'running' | 'waiting' | 'paused' | 'completed' | 'failed' | 'canceled';
3
+ export interface AgentRunStep {
4
+ id: string;
5
+ label: string;
6
+ state: AgentRunState;
7
+ detail?: React.ReactNode;
8
+ }
9
+ export interface AgentRunProps extends Omit<React.ComponentProps<'section'>, 'title'> {
10
+ title: string;
11
+ state: AgentRunState;
12
+ steps: AgentRunStep[];
13
+ /** Explain the current state, especially what a person needs to do next. */
14
+ summary?: React.ReactNode;
15
+ busy?: boolean;
16
+ onPause?: () => void;
17
+ onResume?: () => void;
18
+ onCancel?: () => void;
19
+ onRetry?: () => void;
20
+ }
21
+ /** A controlled run timeline. The application owns execution, cancellation and retry. */
22
+ export declare function AgentRun({ title, state, steps, summary, busy, onPause, onResume, onCancel, onRetry, children, className, ...props }: AgentRunProps): React.JSX.Element;
@@ -0,0 +1,131 @@
1
+ "use client";
2
+ import { cn } from "../lib/utils.js";
3
+ import { Button } from "./button.js";
4
+ import { Spinner } from "./spinner.js";
5
+ import * as React from "react";
6
+ import { jsx, jsxs } from "react/jsx-runtime";
7
+ import AlertCircle from "@burtson-labs/icons/react/alert-circle";
8
+ import Pause from "@burtson-labs/icons/react/pause";
9
+ import Check from "@burtson-labs/icons/react/check";
10
+ import Circle from "@burtson-labs/icons/react/circle";
11
+ //#region src/components/agent-run.tsx
12
+ var labels = {
13
+ queued: "Queued",
14
+ running: "Running",
15
+ waiting: "Needs your input",
16
+ paused: "Paused",
17
+ completed: "Completed",
18
+ failed: "Failed",
19
+ canceled: "Canceled"
20
+ };
21
+ function RunIcon({ state }) {
22
+ if (state === "running") return /* @__PURE__ */ jsx(Spinner, {
23
+ className: "size-4",
24
+ label: "Running"
25
+ });
26
+ return /* @__PURE__ */ jsx(state === "completed" ? Check : state === "failed" || state === "waiting" ? AlertCircle : state === "paused" ? Pause : Circle, {
27
+ "aria-hidden": true,
28
+ className: cn("size-4", state === "completed" && "text-success", state === "failed" && "text-destructive", state === "waiting" && "text-warning")
29
+ });
30
+ }
31
+ /** A controlled run timeline. The application owns execution, cancellation and retry. */
32
+ function AgentRun({ title, state, steps, summary, busy = false, onPause, onResume, onCancel, onRetry, children, className, ...props }) {
33
+ const id = React.useId();
34
+ const active = [
35
+ "queued",
36
+ "running",
37
+ "waiting",
38
+ "paused"
39
+ ].includes(state);
40
+ return /* @__PURE__ */ jsxs("section", {
41
+ "data-slot": "agent-run",
42
+ "data-state": state,
43
+ "aria-labelledby": id,
44
+ className: cn("min-w-0 overflow-hidden rounded-lg border border-border bg-surface", className),
45
+ ...props,
46
+ children: [
47
+ /* @__PURE__ */ jsxs("header", {
48
+ className: "grid gap-2 border-b border-border p-4",
49
+ children: [/* @__PURE__ */ jsxs("div", {
50
+ className: "flex flex-wrap items-center justify-between gap-2",
51
+ children: [/* @__PURE__ */ jsx("h3", {
52
+ id,
53
+ className: "min-w-0 text-sm font-semibold break-words",
54
+ children: title
55
+ }), /* @__PURE__ */ jsxs("span", {
56
+ role: "status",
57
+ className: "inline-flex items-center gap-1.5 text-xs text-muted-foreground",
58
+ children: [/* @__PURE__ */ jsx(RunIcon, { state }), labels[state]]
59
+ })]
60
+ }), summary && /* @__PURE__ */ jsx("p", {
61
+ className: "text-sm leading-relaxed text-muted-foreground",
62
+ children: summary
63
+ })]
64
+ }),
65
+ /* @__PURE__ */ jsx("ol", {
66
+ className: "grid gap-4 p-4",
67
+ "aria-label": "Run steps",
68
+ children: steps.map((step) => /* @__PURE__ */ jsxs("li", {
69
+ className: "flex min-w-0 gap-3",
70
+ "aria-current": step.state === "running" || step.state === "waiting" ? "step" : void 0,
71
+ children: [/* @__PURE__ */ jsx("span", {
72
+ className: "mt-0.5 shrink-0",
73
+ children: /* @__PURE__ */ jsx(RunIcon, { state: step.state })
74
+ }), /* @__PURE__ */ jsxs("div", {
75
+ className: "grid min-w-0 flex-1 gap-1",
76
+ children: [/* @__PURE__ */ jsxs("span", {
77
+ className: "text-sm font-medium break-words",
78
+ children: [step.label, /* @__PURE__ */ jsxs("span", {
79
+ className: "sr-only",
80
+ children: [" — ", labels[step.state]]
81
+ })]
82
+ }), step.detail && /* @__PURE__ */ jsx("div", {
83
+ className: "text-xs leading-relaxed break-words text-muted-foreground",
84
+ children: step.detail
85
+ })]
86
+ })]
87
+ }, step.id))
88
+ }),
89
+ children && /* @__PURE__ */ jsx("div", {
90
+ className: "grid min-w-0 gap-3 border-t border-border p-4",
91
+ children
92
+ }),
93
+ (state === "running" && onPause || state === "paused" && onResume || state === "failed" && onRetry || active && onCancel) && /* @__PURE__ */ jsxs("footer", {
94
+ className: "flex flex-wrap justify-end gap-2 border-t border-border p-3",
95
+ "aria-busy": busy || void 0,
96
+ children: [
97
+ active && onCancel && /* @__PURE__ */ jsx(Button, {
98
+ variant: "ghost",
99
+ size: "sm",
100
+ disabled: busy,
101
+ onClick: onCancel,
102
+ children: "Cancel run"
103
+ }),
104
+ state === "running" && onPause && /* @__PURE__ */ jsx(Button, {
105
+ variant: "outline",
106
+ size: "sm",
107
+ disabled: busy,
108
+ onClick: onPause,
109
+ children: "Pause run"
110
+ }),
111
+ state === "paused" && onResume && /* @__PURE__ */ jsx(Button, {
112
+ size: "sm",
113
+ disabled: busy,
114
+ onClick: onResume,
115
+ children: "Resume run"
116
+ }),
117
+ state === "failed" && onRetry && /* @__PURE__ */ jsx(Button, {
118
+ size: "sm",
119
+ disabled: busy,
120
+ onClick: onRetry,
121
+ children: "Retry run"
122
+ })
123
+ ]
124
+ })
125
+ ]
126
+ });
127
+ }
128
+ //#endregion
129
+ export { AgentRun };
130
+
131
+ //# sourceMappingURL=agent-run.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"agent-run.js","names":[],"sources":["../../src/components/agent-run.tsx"],"sourcesContent":["import AlertCircle from '@burtson-labs/icons/react/alert-circle';\nimport Check from '@burtson-labs/icons/react/check';\nimport Circle from '@burtson-labs/icons/react/circle';\nimport Pause from '@burtson-labs/icons/react/pause';\nimport * as React from 'react';\n\nimport { cn } from '../lib/utils';\n\nimport { Button } from './button';\nimport { Spinner } from './spinner';\n\nexport type AgentRunState =\n 'queued' | 'running' | 'waiting' | 'paused' | 'completed' | 'failed' | 'canceled';\nexport interface AgentRunStep {\n id: string;\n label: string;\n state: AgentRunState;\n detail?: React.ReactNode;\n}\nexport interface AgentRunProps extends Omit<React.ComponentProps<'section'>, 'title'> {\n title: string;\n state: AgentRunState;\n steps: AgentRunStep[];\n /** Explain the current state, especially what a person needs to do next. */\n summary?: React.ReactNode;\n busy?: boolean;\n onPause?: () => void;\n onResume?: () => void;\n onCancel?: () => void;\n onRetry?: () => void;\n}\nconst labels: Record<AgentRunState, string> = {\n queued: 'Queued',\n running: 'Running',\n waiting: 'Needs your input',\n paused: 'Paused',\n completed: 'Completed',\n failed: 'Failed',\n canceled: 'Canceled',\n};\nfunction RunIcon({ state }: { state: AgentRunState }) {\n if (state === 'running') return <Spinner className=\"size-4\" label=\"Running\" />;\n const Icon =\n state === 'completed'\n ? Check\n : state === 'failed' || state === 'waiting'\n ? AlertCircle\n : state === 'paused'\n ? Pause\n : Circle;\n return (\n <Icon\n aria-hidden\n className={cn(\n 'size-4',\n state === 'completed' && 'text-success',\n state === 'failed' && 'text-destructive',\n state === 'waiting' && 'text-warning',\n )}\n />\n );\n}\n\n/** A controlled run timeline. The application owns execution, cancellation and retry. */\nexport function AgentRun({\n title,\n state,\n steps,\n summary,\n busy = false,\n onPause,\n onResume,\n onCancel,\n onRetry,\n children,\n className,\n ...props\n}: AgentRunProps) {\n const id = React.useId();\n const active = ['queued', 'running', 'waiting', 'paused'].includes(state);\n return (\n <section\n data-slot=\"agent-run\"\n data-state={state}\n aria-labelledby={id}\n className={cn(\n 'min-w-0 overflow-hidden rounded-lg border border-border bg-surface',\n className,\n )}\n {...props}\n >\n <header className=\"grid gap-2 border-b border-border p-4\">\n <div className=\"flex flex-wrap items-center justify-between gap-2\">\n <h3 id={id} className=\"min-w-0 text-sm font-semibold break-words\">\n {title}\n </h3>\n <span\n role=\"status\"\n className=\"inline-flex items-center gap-1.5 text-xs text-muted-foreground\"\n >\n <RunIcon state={state} />\n {labels[state]}\n </span>\n </div>\n {summary && <p className=\"text-sm leading-relaxed text-muted-foreground\">{summary}</p>}\n </header>\n <ol className=\"grid gap-4 p-4\" aria-label=\"Run steps\">\n {steps.map((step) => (\n <li\n key={step.id}\n className=\"flex min-w-0 gap-3\"\n aria-current={step.state === 'running' || step.state === 'waiting' ? 'step' : undefined}\n >\n <span className=\"mt-0.5 shrink-0\">\n <RunIcon state={step.state} />\n </span>\n <div className=\"grid min-w-0 flex-1 gap-1\">\n <span className=\"text-sm font-medium break-words\">\n {step.label}\n <span className=\"sr-only\"> — {labels[step.state]}</span>\n </span>\n {step.detail && (\n <div className=\"text-xs leading-relaxed break-words text-muted-foreground\">\n {step.detail}\n </div>\n )}\n </div>\n </li>\n ))}\n </ol>\n {children && <div className=\"grid min-w-0 gap-3 border-t border-border p-4\">{children}</div>}\n {((state === 'running' && onPause) ||\n (state === 'paused' && onResume) ||\n (state === 'failed' && onRetry) ||\n (active && onCancel)) && (\n <footer\n className=\"flex flex-wrap justify-end gap-2 border-t border-border p-3\"\n aria-busy={busy || undefined}\n >\n {active && onCancel && (\n <Button variant=\"ghost\" size=\"sm\" disabled={busy} onClick={onCancel}>\n Cancel run\n </Button>\n )}\n {state === 'running' && onPause && (\n <Button variant=\"outline\" size=\"sm\" disabled={busy} onClick={onPause}>\n Pause run\n </Button>\n )}\n {state === 'paused' && onResume && (\n <Button size=\"sm\" disabled={busy} onClick={onResume}>\n Resume run\n </Button>\n )}\n {state === 'failed' && onRetry && (\n <Button size=\"sm\" disabled={busy} onClick={onRetry}>\n Retry run\n </Button>\n )}\n </footer>\n )}\n </section>\n );\n}\n"],"mappings":";;;;;;;;;;;AA+BA,IAAM,SAAwC;CAC5C,QAAQ;CACR,SAAS;CACT,SAAS;CACT,QAAQ;CACR,WAAW;CACX,QAAQ;CACR,UAAU;AACZ;AACA,SAAS,QAAQ,EAAE,SAAmC;CACpD,IAAI,UAAU,WAAW,OAAO,oBAAC,SAAD;EAAS,WAAU;EAAS,OAAM;CAAW,CAAA;CAS7E,OACE,oBARA,UAAU,cACN,QACA,UAAU,YAAY,UAAU,YAC9B,cACA,UAAU,WACR,QACA,QAER;EACE,eAAA;EACA,WAAW,GACT,UACA,UAAU,eAAe,gBACzB,UAAU,YAAY,oBACtB,UAAU,aAAa,cACzB;CACD,CAAA;AAEL;;AAGA,SAAgB,SAAS,EACvB,OACA,OACA,OACA,SACA,OAAO,OACP,SACA,UACA,UACA,SACA,UACA,WACA,GAAG,SACa;CAChB,MAAM,KAAK,MAAM,MAAM;CACvB,MAAM,SAAS;EAAC;EAAU;EAAW;EAAW;CAAQ,CAAC,CAAC,SAAS,KAAK;CACxE,OACE,qBAAC,WAAD;EACE,aAAU;EACV,cAAY;EACZ,mBAAiB;EACjB,WAAW,GACT,sEACA,SACF;EACA,GAAI;EARN,UAAA;GAUE,qBAAC,UAAD;IAAQ,WAAU;IAAlB,UAAA,CACE,qBAAC,OAAD;KAAK,WAAU;KAAf,UAAA,CACE,oBAAC,MAAD;MAAQ;MAAI,WAAU;MACnB,UAAA;KACC,CAAA,GACJ,qBAAC,QAAD;MACE,MAAK;MACL,WAAU;MAFZ,UAAA,CAIE,oBAAC,SAAD,EAAgB,MAAQ,CAAA,GACvB,OAAO,MACJ;KACH,CAAA,CAAA;IACJ,CAAA,GAAA,WAAW,oBAAC,KAAD;KAAG,WAAU;KAAiD,UAAA;IAAW,CAAA,CAC/E;;GACR,oBAAC,MAAD;IAAI,WAAU;IAAiB,cAAW;IACvC,UAAA,MAAM,KAAK,SACV,qBAAC,MAAD;KAEE,WAAU;KACV,gBAAc,KAAK,UAAU,aAAa,KAAK,UAAU,YAAY,SAAS,KAAA;KAHhF,UAAA,CAKE,oBAAC,QAAD;MAAM,WAAU;MACd,UAAA,oBAAC,SAAD,EAAS,OAAO,KAAK,MAAQ,CAAA;KACzB,CAAA,GACN,qBAAC,OAAD;MAAK,WAAU;MAAf,UAAA,CACE,qBAAC,QAAD;OAAM,WAAU;OAAhB,UAAA,CACG,KAAK,OACN,qBAAC,QAAD;QAAM,WAAU;QAAhB,UAAA,CAA0B,OAAI,OAAO,KAAK,MAAa;OACnD,CAAA,CAAA;MACL,CAAA,GAAA,KAAK,UACJ,oBAAC,OAAD;OAAK,WAAU;OACZ,UAAA,KAAK;MACH,CAAA,CAEJ;KACH,CAAA,CAAA;IAlBG,GAAA,KAAK,EAkBR,CACL;GACC,CAAA;GACH,YAAY,oBAAC,OAAD;IAAK,WAAU;IAAiD;GAAc,CAAA;IACxF,UAAU,aAAa,WACvB,UAAU,YAAY,YACtB,UAAU,YAAY,WACtB,UAAU,aACX,qBAAC,UAAD;IACE,WAAU;IACV,aAAW,QAAQ,KAAA;IAFrB,UAAA;KAIG,UAAU,YACT,oBAAC,QAAD;MAAQ,SAAQ;MAAQ,MAAK;MAAK,UAAU;MAAM,SAAS;MAAU,UAAA;KAE7D,CAAA;KAET,UAAU,aAAa,WACtB,oBAAC,QAAD;MAAQ,SAAQ;MAAU,MAAK;MAAK,UAAU;MAAM,SAAS;MAAS,UAAA;KAE9D,CAAA;KAET,UAAU,YAAY,YACrB,oBAAC,QAAD;MAAQ,MAAK;MAAK,UAAU;MAAM,SAAS;MAAU,UAAA;KAE7C,CAAA;KAET,UAAU,YAAY,WACrB,oBAAC,QAAD;MAAQ,MAAK;MAAK,UAAU;MAAM,SAAS;MAAS,UAAA;KAE5C,CAAA;IAEJ;;EAEH;;AAEb"}
@@ -0,0 +1,19 @@
1
+ import * as React from 'react';
2
+ export interface AvatarUploadProps extends Omit<React.ComponentProps<'div'>, 'onError'> {
3
+ src?: string;
4
+ /** A stored photo may exist even when its preview cannot be loaded. */
5
+ hasPhoto?: boolean;
6
+ /** The person's name, used to label the current photo. */
7
+ name: string;
8
+ fallback?: React.ReactNode;
9
+ /** Receives a square JPEG. Resolve only after it has been saved; reject to offer retry. */
10
+ onUpload: (file: File) => Promise<void>;
11
+ onRemove?: () => Promise<void>;
12
+ disabled?: boolean;
13
+ /** Maximum size of the original file. Default: 10 MiB. */
14
+ maxBytes?: number;
15
+ /** Exported square dimension. Default: 512px. */
16
+ outputSize?: number;
17
+ }
18
+ /** Choose, position and save a profile photo. No storage or networking is built in. */
19
+ export declare function AvatarUpload({ src, hasPhoto, name, fallback, onUpload, onRemove, disabled, maxBytes, outputSize, className, ...props }: AvatarUploadProps): React.JSX.Element;
@@ -0,0 +1,359 @@
1
+ "use client";
2
+ import { cn } from "../lib/utils.js";
3
+ import { Button } from "./button.js";
4
+ import { Avatar, AvatarFallback, AvatarImage } from "./avatar.js";
5
+ import { Slider } from "./slider.js";
6
+ import * as React from "react";
7
+ import { jsx, jsxs } from "react/jsx-runtime";
8
+ import Camera from "@burtson-labs/icons/react/camera";
9
+ //#region src/components/avatar-upload.tsx
10
+ var clamp = (n) => Math.min(1, Math.max(0, n));
11
+ /** Choose, position and save a profile photo. No storage or networking is built in. */
12
+ function AvatarUpload({ src, hasPhoto = Boolean(src), name, fallback, onUpload, onRemove, disabled = false, maxBytes = 10485760, outputSize = 512, className, ...props }) {
13
+ const input = React.useRef(null);
14
+ const trigger = React.useRef(null);
15
+ const crop = React.useRef(null);
16
+ const request = React.useRef(0);
17
+ const saving = React.useRef(false);
18
+ const returnFocus = React.useRef(false);
19
+ const ownedUrl = React.useRef(null);
20
+ const drag = React.useRef(null);
21
+ const [photo, setPhoto] = React.useState(null);
22
+ const [position, setPosition] = React.useState({
23
+ x: .5,
24
+ y: .5
25
+ });
26
+ const [zoom, setZoom] = React.useState(1);
27
+ const [busy, setBusy] = React.useState(false);
28
+ const [reading, setReading] = React.useState(false);
29
+ const [error, setError] = React.useState("");
30
+ const [message, setMessage] = React.useState("");
31
+ const helpId = React.useId();
32
+ const errorId = React.useId();
33
+ const blocked = disabled || busy || reading;
34
+ React.useEffect(() => () => {
35
+ request.current++;
36
+ if (ownedUrl.current) URL.revokeObjectURL(ownedUrl.current);
37
+ }, []);
38
+ React.useEffect(() => {
39
+ if (photo) crop.current?.focus();
40
+ }, [photo]);
41
+ React.useEffect(() => {
42
+ if (returnFocus.current && !photo && !blocked) {
43
+ returnFocus.current = false;
44
+ trigger.current?.focus();
45
+ }
46
+ }, [photo, blocked]);
47
+ const discard = () => {
48
+ request.current++;
49
+ returnFocus.current = true;
50
+ if (ownedUrl.current) URL.revokeObjectURL(ownedUrl.current);
51
+ ownedUrl.current = null;
52
+ setPhoto(null);
53
+ setReading(false);
54
+ };
55
+ const choose = (file) => {
56
+ if (!file || blocked || saving.current) return;
57
+ setError("");
58
+ setMessage("");
59
+ if (![
60
+ "image/jpeg",
61
+ "image/png",
62
+ "image/webp"
63
+ ].includes(file.type)) {
64
+ setError("Choose a JPEG, PNG or WebP photo. Export HEIC photos as JPEG first.");
65
+ return;
66
+ }
67
+ if (!file.size || file.size > maxBytes) {
68
+ setError(`Choose a photo under ${Math.round(maxBytes / 1024 / 1024)} MB that is not empty.`);
69
+ return;
70
+ }
71
+ const id = ++request.current;
72
+ const url = URL.createObjectURL(file);
73
+ if (ownedUrl.current) URL.revokeObjectURL(ownedUrl.current);
74
+ ownedUrl.current = url;
75
+ setPhoto(null);
76
+ setReading(true);
77
+ const image = new Image();
78
+ image.onload = () => {
79
+ if (id !== request.current) return;
80
+ setReading(false);
81
+ if (!image.naturalWidth || !image.naturalHeight) {
82
+ setError("This photo could not be opened. Try another image.");
83
+ URL.revokeObjectURL(url);
84
+ ownedUrl.current = null;
85
+ return;
86
+ }
87
+ setPosition({
88
+ x: .5,
89
+ y: .5
90
+ });
91
+ setZoom(1);
92
+ setPhoto({
93
+ url,
94
+ image
95
+ });
96
+ };
97
+ image.onerror = () => {
98
+ if (id !== request.current) return;
99
+ setReading(false);
100
+ setError("This photo could not be opened. Try another image.");
101
+ URL.revokeObjectURL(url);
102
+ ownedUrl.current = null;
103
+ };
104
+ image.src = url;
105
+ };
106
+ const side = photo ? Math.min(photo.image.naturalWidth, photo.image.naturalHeight) / zoom : 1;
107
+ const act = async (remove = false) => {
108
+ if (blocked || saving.current || !remove && !photo) return;
109
+ saving.current = true;
110
+ setBusy(true);
111
+ setError("");
112
+ setMessage("");
113
+ try {
114
+ if (remove) await onRemove?.();
115
+ else if (photo) {
116
+ const canvas = document.createElement("canvas");
117
+ canvas.width = canvas.height = Math.min(2048, Math.max(64, Math.round(outputSize) || 512));
118
+ const ctx = canvas.getContext("2d");
119
+ if (!ctx) throw new Error("Your browser could not prepare the photo. Please try again.");
120
+ ctx.fillStyle = "#ffffff";
121
+ ctx.fillRect(0, 0, canvas.width, canvas.height);
122
+ ctx.drawImage(photo.image, (photo.image.naturalWidth - side) * position.x, (photo.image.naturalHeight - side) * position.y, side, side, 0, 0, canvas.width, canvas.height);
123
+ const blob = await new Promise((resolve, reject) => canvas.toBlob((value) => value ? resolve(value) : reject(/* @__PURE__ */ new Error("Could not prepare this photo. Try another image.")), "image/jpeg", .9));
124
+ await onUpload(new File([blob], "profile-photo.jpg", { type: "image/jpeg" }));
125
+ }
126
+ discard();
127
+ setMessage(remove ? "Profile photo removed." : "Profile photo updated.");
128
+ } catch (err) {
129
+ setError(err instanceof Error ? err.message : "Could not save your photo. Please try again.");
130
+ } finally {
131
+ saving.current = false;
132
+ setBusy(false);
133
+ }
134
+ };
135
+ return /* @__PURE__ */ jsxs("div", {
136
+ "data-slot": "avatar-upload",
137
+ "aria-busy": busy || reading || void 0,
138
+ className: cn("grid min-w-0 gap-4", className),
139
+ ...props,
140
+ onDragOver: (event) => {
141
+ props.onDragOver?.(event);
142
+ event.preventDefault();
143
+ },
144
+ onDrop: (event) => {
145
+ props.onDrop?.(event);
146
+ event.preventDefault();
147
+ choose(event.dataTransfer.files[0]);
148
+ },
149
+ children: [
150
+ /* @__PURE__ */ jsxs("div", {
151
+ className: "flex flex-wrap items-center gap-4",
152
+ children: [
153
+ /* @__PURE__ */ jsxs(Avatar, {
154
+ className: "size-16",
155
+ children: [src && /* @__PURE__ */ jsx(AvatarImage, {
156
+ src,
157
+ alt: `${name}'s profile photo`,
158
+ referrerPolicy: "no-referrer"
159
+ }), /* @__PURE__ */ jsx(AvatarFallback, {
160
+ className: "text-lg",
161
+ children: fallback ?? name.slice(0, 2).toUpperCase()
162
+ })]
163
+ }),
164
+ /* @__PURE__ */ jsxs("div", {
165
+ className: "grid min-w-0 flex-1 gap-2",
166
+ children: [/* @__PURE__ */ jsxs("div", {
167
+ className: "flex flex-wrap gap-2",
168
+ children: [/* @__PURE__ */ jsxs(Button, {
169
+ ref: trigger,
170
+ variant: "outline",
171
+ disabled: blocked,
172
+ onClick: () => input.current?.click(),
173
+ "aria-describedby": `${helpId}${error ? ` ${errorId}` : ""}`,
174
+ children: [
175
+ /* @__PURE__ */ jsx(Camera, {}),
176
+ " ",
177
+ hasPhoto ? "Change photo" : "Choose photo"
178
+ ]
179
+ }), onRemove && hasPhoto && !photo && /* @__PURE__ */ jsx(Button, {
180
+ variant: "ghost",
181
+ disabled: blocked,
182
+ onClick: () => void act(true),
183
+ children: "Remove photo"
184
+ })]
185
+ }), /* @__PURE__ */ jsxs("p", {
186
+ id: helpId,
187
+ className: "text-xs text-muted-foreground",
188
+ children: [
189
+ "JPEG, PNG or WebP · up to ",
190
+ Math.round(maxBytes / 1024 / 1024),
191
+ " MB. You can also drop a photo here."
192
+ ]
193
+ })]
194
+ }),
195
+ /* @__PURE__ */ jsx("input", {
196
+ ref: input,
197
+ type: "file",
198
+ accept: "image/jpeg,image/png,image/webp",
199
+ "aria-label": "Choose profile photo",
200
+ className: "hidden",
201
+ disabled: blocked,
202
+ onChange: (event) => {
203
+ const file = event.target.files?.[0];
204
+ event.target.value = "";
205
+ choose(file);
206
+ }
207
+ })
208
+ ]
209
+ }),
210
+ photo && /* @__PURE__ */ jsxs("div", {
211
+ className: "grid gap-4 rounded-lg border border-border bg-surface p-4",
212
+ children: [
213
+ /* @__PURE__ */ jsx("div", {
214
+ ref: crop,
215
+ tabIndex: -1,
216
+ role: "group",
217
+ "aria-label": "Photo crop preview. Use the zoom and position controls to adjust it.",
218
+ className: cn("relative mx-auto aspect-square w-full max-w-64 touch-none overflow-hidden rounded-full bg-muted", "outline-none focus-visible:outline-solid focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring", !blocked && "cursor-move"),
219
+ onPointerDown: (event) => {
220
+ if (blocked) return;
221
+ event.currentTarget.setPointerCapture(event.pointerId);
222
+ drag.current = {
223
+ x: event.clientX,
224
+ y: event.clientY,
225
+ px: position.x,
226
+ py: position.y
227
+ };
228
+ },
229
+ onPointerMove: (event) => {
230
+ if (!drag.current || blocked) return;
231
+ const scale = event.currentTarget.clientWidth / side;
232
+ const dx = (photo.image.naturalWidth - side) * scale;
233
+ const dy = (photo.image.naturalHeight - side) * scale;
234
+ setPosition({
235
+ x: dx ? clamp(drag.current.px - (event.clientX - drag.current.x) / dx) : .5,
236
+ y: dy ? clamp(drag.current.py - (event.clientY - drag.current.y) / dy) : .5
237
+ });
238
+ },
239
+ onPointerUp: () => {
240
+ drag.current = null;
241
+ },
242
+ onPointerCancel: () => {
243
+ drag.current = null;
244
+ },
245
+ onLostPointerCapture: () => {
246
+ drag.current = null;
247
+ },
248
+ children: /* @__PURE__ */ jsx("img", {
249
+ src: photo.url,
250
+ alt: "Crop preview",
251
+ draggable: false,
252
+ className: "pointer-events-none absolute max-w-none select-none",
253
+ style: {
254
+ width: `${photo.image.naturalWidth / side * 100}%`,
255
+ height: `${photo.image.naturalHeight / side * 100}%`,
256
+ left: `${-(photo.image.naturalWidth - side) * position.x / side * 100}%`,
257
+ top: `${-(photo.image.naturalHeight - side) * position.y / side * 100}%`
258
+ }
259
+ })
260
+ }),
261
+ /* @__PURE__ */ jsx("p", {
262
+ className: "text-center text-xs text-muted-foreground",
263
+ children: "Drag to reposition. Use position controls for precise adjustments."
264
+ }),
265
+ /* @__PURE__ */ jsxs("label", {
266
+ className: "grid gap-2 text-sm",
267
+ children: [
268
+ "Zoom",
269
+ " ",
270
+ /* @__PURE__ */ jsx(Slider, {
271
+ min: 1,
272
+ max: 3,
273
+ step: .05,
274
+ value: zoom,
275
+ onValueChange: setZoom,
276
+ disabled: blocked,
277
+ "aria-valuetext": `${Math.round(zoom * 100)}%`
278
+ })
279
+ ]
280
+ }),
281
+ /* @__PURE__ */ jsxs("details", {
282
+ className: "rounded-md border border-border px-3 text-sm",
283
+ children: [/* @__PURE__ */ jsx("summary", {
284
+ className: "flex min-h-11 cursor-pointer items-center font-medium",
285
+ children: "Position controls"
286
+ }), /* @__PURE__ */ jsxs("div", {
287
+ className: "grid gap-3 pb-3 sm:grid-cols-2",
288
+ children: [/* @__PURE__ */ jsxs("label", {
289
+ className: "grid gap-2",
290
+ htmlFor: `${helpId}-x`,
291
+ children: ["Horizontal position", /* @__PURE__ */ jsx(Slider, {
292
+ min: 0,
293
+ max: 100,
294
+ step: 1,
295
+ id: `${helpId}-x`,
296
+ value: Math.round(position.x * 100),
297
+ onValueChange: (value) => setPosition((p) => ({
298
+ ...p,
299
+ x: value / 100
300
+ })),
301
+ disabled: blocked || photo.image.naturalWidth <= side,
302
+ "aria-valuetext": `${Math.round(position.x * 100)}%`
303
+ })]
304
+ }), /* @__PURE__ */ jsxs("label", {
305
+ className: "grid gap-2",
306
+ htmlFor: `${helpId}-y`,
307
+ children: ["Vertical position", /* @__PURE__ */ jsx(Slider, {
308
+ min: 0,
309
+ max: 100,
310
+ step: 1,
311
+ id: `${helpId}-y`,
312
+ value: Math.round(position.y * 100),
313
+ onValueChange: (value) => setPosition((p) => ({
314
+ ...p,
315
+ y: value / 100
316
+ })),
317
+ disabled: blocked || photo.image.naturalHeight <= side,
318
+ "aria-valuetext": `${Math.round(position.y * 100)}%`
319
+ })]
320
+ })]
321
+ })]
322
+ }),
323
+ /* @__PURE__ */ jsxs("div", {
324
+ className: "flex flex-wrap justify-end gap-2",
325
+ children: [/* @__PURE__ */ jsx(Button, {
326
+ variant: "outline",
327
+ disabled: busy,
328
+ onClick: () => {
329
+ discard();
330
+ setError("");
331
+ },
332
+ children: "Cancel"
333
+ }), /* @__PURE__ */ jsx(Button, {
334
+ disabled: blocked,
335
+ onClick: () => void act(),
336
+ children: busy ? "Saving photo…" : "Save photo"
337
+ })]
338
+ })
339
+ ]
340
+ }),
341
+ error && /* @__PURE__ */ jsx("p", {
342
+ id: errorId,
343
+ role: "alert",
344
+ className: "text-sm text-destructive",
345
+ children: error
346
+ }),
347
+ /* @__PURE__ */ jsx("p", {
348
+ role: "status",
349
+ "aria-live": "polite",
350
+ className: cn("text-sm text-muted-foreground", !reading && !busy && !message && "sr-only"),
351
+ children: reading ? "Opening photo…" : busy ? "Saving your profile photo…" : message
352
+ })
353
+ ]
354
+ });
355
+ }
356
+ //#endregion
357
+ export { AvatarUpload };
358
+
359
+ //# sourceMappingURL=avatar-upload.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"avatar-upload.js","names":[],"sources":["../../src/components/avatar-upload.tsx"],"sourcesContent":["import Camera from '@burtson-labs/icons/react/camera';\nimport * as React from 'react';\n\nimport { cn, focusRingClasses } from '../lib/utils';\n\nimport { Avatar, AvatarFallback, AvatarImage } from './avatar';\nimport { Button } from './button';\nimport { Slider } from './slider';\n\nexport interface AvatarUploadProps extends Omit<React.ComponentProps<'div'>, 'onError'> {\n src?: string;\n /** A stored photo may exist even when its preview cannot be loaded. */\n hasPhoto?: boolean;\n /** The person's name, used to label the current photo. */\n name: string;\n fallback?: React.ReactNode;\n /** Receives a square JPEG. Resolve only after it has been saved; reject to offer retry. */\n onUpload: (file: File) => Promise<void>;\n onRemove?: () => Promise<void>;\n disabled?: boolean;\n /** Maximum size of the original file. Default: 10 MiB. */\n maxBytes?: number;\n /** Exported square dimension. Default: 512px. */\n outputSize?: number;\n}\n\ntype Photo = { url: string; image: HTMLImageElement };\nconst clamp = (n: number) => Math.min(1, Math.max(0, n));\n\n/** Choose, position and save a profile photo. No storage or networking is built in. */\nexport function AvatarUpload({\n src,\n hasPhoto = Boolean(src),\n name,\n fallback,\n onUpload,\n onRemove,\n disabled = false,\n maxBytes = 10 * 1024 * 1024,\n outputSize = 512,\n className,\n ...props\n}: AvatarUploadProps) {\n const input = React.useRef<HTMLInputElement>(null);\n const trigger = React.useRef<HTMLButtonElement>(null);\n const crop = React.useRef<HTMLDivElement>(null);\n const request = React.useRef(0);\n const saving = React.useRef(false);\n const returnFocus = React.useRef(false);\n const ownedUrl = React.useRef<string | null>(null);\n const drag = React.useRef<{ x: number; y: number; px: number; py: number } | null>(null);\n const [photo, setPhoto] = React.useState<Photo | null>(null);\n const [position, setPosition] = React.useState({ x: 0.5, y: 0.5 });\n const [zoom, setZoom] = React.useState(1);\n const [busy, setBusy] = React.useState(false);\n const [reading, setReading] = React.useState(false);\n const [error, setError] = React.useState('');\n const [message, setMessage] = React.useState('');\n const helpId = React.useId();\n const errorId = React.useId();\n const blocked = disabled || busy || reading;\n\n React.useEffect(\n () => () => {\n request.current++;\n if (ownedUrl.current) URL.revokeObjectURL(ownedUrl.current);\n },\n [],\n );\n React.useEffect(() => {\n if (photo) crop.current?.focus();\n }, [photo]);\n React.useEffect(() => {\n if (returnFocus.current && !photo && !blocked) {\n returnFocus.current = false;\n trigger.current?.focus();\n }\n }, [photo, blocked]);\n\n const discard = () => {\n request.current++;\n returnFocus.current = true;\n if (ownedUrl.current) URL.revokeObjectURL(ownedUrl.current);\n ownedUrl.current = null;\n setPhoto(null);\n setReading(false);\n };\n\n const choose = (file?: File) => {\n if (!file || blocked || saving.current) return;\n setError('');\n setMessage('');\n if (!['image/jpeg', 'image/png', 'image/webp'].includes(file.type)) {\n setError('Choose a JPEG, PNG or WebP photo. Export HEIC photos as JPEG first.');\n return;\n }\n if (!file.size || file.size > maxBytes) {\n setError(`Choose a photo under ${Math.round(maxBytes / 1024 / 1024)} MB that is not empty.`);\n return;\n }\n const id = ++request.current;\n const url = URL.createObjectURL(file);\n if (ownedUrl.current) URL.revokeObjectURL(ownedUrl.current);\n ownedUrl.current = url;\n setPhoto(null);\n setReading(true);\n const image = new Image();\n image.onload = () => {\n if (id !== request.current) return;\n setReading(false);\n if (!image.naturalWidth || !image.naturalHeight) {\n setError('This photo could not be opened. Try another image.');\n URL.revokeObjectURL(url);\n ownedUrl.current = null;\n return;\n }\n setPosition({ x: 0.5, y: 0.5 });\n setZoom(1);\n setPhoto({ url, image });\n };\n image.onerror = () => {\n if (id !== request.current) return;\n setReading(false);\n setError('This photo could not be opened. Try another image.');\n URL.revokeObjectURL(url);\n ownedUrl.current = null;\n };\n image.src = url;\n };\n\n const side = photo ? Math.min(photo.image.naturalWidth, photo.image.naturalHeight) / zoom : 1;\n const act = async (remove = false) => {\n if (blocked || saving.current || (!remove && !photo)) return;\n saving.current = true;\n setBusy(true);\n setError('');\n setMessage('');\n try {\n if (remove) await onRemove?.();\n else if (photo) {\n const canvas = document.createElement('canvas');\n canvas.width = canvas.height = Math.min(2048, Math.max(64, Math.round(outputSize) || 512));\n const ctx = canvas.getContext('2d');\n if (!ctx) throw new Error('Your browser could not prepare the photo. Please try again.');\n ctx.fillStyle = '#ffffff';\n ctx.fillRect(0, 0, canvas.width, canvas.height);\n ctx.drawImage(\n photo.image,\n (photo.image.naturalWidth - side) * position.x,\n (photo.image.naturalHeight - side) * position.y,\n side,\n side,\n 0,\n 0,\n canvas.width,\n canvas.height,\n );\n const blob = await new Promise<Blob>((resolve, reject) =>\n canvas.toBlob(\n (value) =>\n value\n ? resolve(value)\n : reject(new Error('Could not prepare this photo. Try another image.')),\n 'image/jpeg',\n 0.9,\n ),\n );\n await onUpload(new File([blob], 'profile-photo.jpg', { type: 'image/jpeg' }));\n }\n discard();\n setMessage(remove ? 'Profile photo removed.' : 'Profile photo updated.');\n } catch (err) {\n setError(err instanceof Error ? err.message : 'Could not save your photo. Please try again.');\n } finally {\n saving.current = false;\n setBusy(false);\n }\n };\n\n return (\n <div\n data-slot=\"avatar-upload\"\n aria-busy={busy || reading || undefined}\n className={cn('grid min-w-0 gap-4', className)}\n {...props}\n onDragOver={(event) => {\n props.onDragOver?.(event);\n event.preventDefault();\n }}\n onDrop={(event) => {\n props.onDrop?.(event);\n event.preventDefault();\n choose(event.dataTransfer.files[0]);\n }}\n >\n <div className=\"flex flex-wrap items-center gap-4\">\n <Avatar className=\"size-16\">\n {src && (\n <AvatarImage src={src} alt={`${name}'s profile photo`} referrerPolicy=\"no-referrer\" />\n )}\n <AvatarFallback className=\"text-lg\">\n {fallback ?? name.slice(0, 2).toUpperCase()}\n </AvatarFallback>\n </Avatar>\n <div className=\"grid min-w-0 flex-1 gap-2\">\n <div className=\"flex flex-wrap gap-2\">\n <Button\n ref={trigger}\n variant=\"outline\"\n disabled={blocked}\n onClick={() => input.current?.click()}\n aria-describedby={`${helpId}${error ? ` ${errorId}` : ''}`}\n >\n <Camera /> {hasPhoto ? 'Change photo' : 'Choose photo'}\n </Button>\n {onRemove && hasPhoto && !photo && (\n <Button variant=\"ghost\" disabled={blocked} onClick={() => void act(true)}>\n Remove photo\n </Button>\n )}\n </div>\n <p id={helpId} className=\"text-xs text-muted-foreground\">\n JPEG, PNG or WebP · up to {Math.round(maxBytes / 1024 / 1024)} MB. You can also drop a\n photo here.\n </p>\n </div>\n <input\n ref={input}\n type=\"file\"\n accept=\"image/jpeg,image/png,image/webp\"\n aria-label=\"Choose profile photo\"\n className=\"hidden\"\n disabled={blocked}\n onChange={(event) => {\n const file = event.target.files?.[0];\n event.target.value = '';\n choose(file);\n }}\n />\n </div>\n {photo && (\n <div className=\"grid gap-4 rounded-lg border border-border bg-surface p-4\">\n <div\n ref={crop}\n tabIndex={-1}\n role=\"group\"\n aria-label=\"Photo crop preview. Use the zoom and position controls to adjust it.\"\n className={cn(\n 'relative mx-auto aspect-square w-full max-w-64 touch-none overflow-hidden rounded-full bg-muted',\n focusRingClasses,\n !blocked && 'cursor-move',\n )}\n onPointerDown={(event) => {\n if (blocked) return;\n event.currentTarget.setPointerCapture(event.pointerId);\n drag.current = { x: event.clientX, y: event.clientY, px: position.x, py: position.y };\n }}\n onPointerMove={(event) => {\n if (!drag.current || blocked) return;\n const scale = event.currentTarget.clientWidth / side;\n const dx = (photo.image.naturalWidth - side) * scale;\n const dy = (photo.image.naturalHeight - side) * scale;\n setPosition({\n x: dx ? clamp(drag.current.px - (event.clientX - drag.current.x) / dx) : 0.5,\n y: dy ? clamp(drag.current.py - (event.clientY - drag.current.y) / dy) : 0.5,\n });\n }}\n onPointerUp={() => {\n drag.current = null;\n }}\n onPointerCancel={() => {\n drag.current = null;\n }}\n onLostPointerCapture={() => {\n drag.current = null;\n }}\n >\n <img\n src={photo.url}\n alt=\"Crop preview\"\n draggable={false}\n className=\"pointer-events-none absolute max-w-none select-none\"\n style={{\n width: `${(photo.image.naturalWidth / side) * 100}%`,\n height: `${(photo.image.naturalHeight / side) * 100}%`,\n left: `${((-(photo.image.naturalWidth - side) * position.x) / side) * 100}%`,\n top: `${((-(photo.image.naturalHeight - side) * position.y) / side) * 100}%`,\n }}\n />\n </div>\n <p className=\"text-center text-xs text-muted-foreground\">\n Drag to reposition. Use position controls for precise adjustments.\n </p>\n <label className=\"grid gap-2 text-sm\">\n Zoom{' '}\n <Slider\n min={1}\n max={3}\n step={0.05}\n value={zoom}\n onValueChange={setZoom}\n disabled={blocked}\n aria-valuetext={`${Math.round(zoom * 100)}%`}\n />\n </label>\n <details className=\"rounded-md border border-border px-3 text-sm\">\n <summary className=\"flex min-h-11 cursor-pointer items-center font-medium\">\n Position controls\n </summary>\n <div className=\"grid gap-3 pb-3 sm:grid-cols-2\">\n <label className=\"grid gap-2\" htmlFor={`${helpId}-x`}>\n Horizontal position\n <Slider\n min={0}\n max={100}\n step={1}\n id={`${helpId}-x`}\n value={Math.round(position.x * 100)}\n onValueChange={(value) => setPosition((p) => ({ ...p, x: value / 100 }))}\n disabled={blocked || photo.image.naturalWidth <= side}\n aria-valuetext={`${Math.round(position.x * 100)}%`}\n />\n </label>\n <label className=\"grid gap-2\" htmlFor={`${helpId}-y`}>\n Vertical position\n <Slider\n min={0}\n max={100}\n step={1}\n id={`${helpId}-y`}\n value={Math.round(position.y * 100)}\n onValueChange={(value) => setPosition((p) => ({ ...p, y: value / 100 }))}\n disabled={blocked || photo.image.naturalHeight <= side}\n aria-valuetext={`${Math.round(position.y * 100)}%`}\n />\n </label>\n </div>\n </details>\n <div className=\"flex flex-wrap justify-end gap-2\">\n <Button\n variant=\"outline\"\n disabled={busy}\n onClick={() => {\n discard();\n setError('');\n }}\n >\n Cancel\n </Button>\n <Button disabled={blocked} onClick={() => void act()}>\n {busy ? 'Saving photo…' : 'Save photo'}\n </Button>\n </div>\n </div>\n )}\n {error && (\n <p id={errorId} role=\"alert\" className=\"text-sm text-destructive\">\n {error}\n </p>\n )}\n <p\n role=\"status\"\n aria-live=\"polite\"\n className={cn('text-sm text-muted-foreground', !reading && !busy && !message && 'sr-only')}\n >\n {reading ? 'Opening photo…' : busy ? 'Saving your profile photo…' : message}\n </p>\n </div>\n );\n}\n"],"mappings":"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"}
@@ -1,5 +1,5 @@
1
1
  import * as React from 'react';
2
- export type ToolCallStatus = 'pending' | 'running' | 'success' | 'error';
2
+ export type ToolCallStatus = 'pending' | 'running' | 'success' | 'error' | 'canceled';
3
3
  export interface ToolCallProps extends Omit<React.ComponentProps<'div'>, 'title'> {
4
4
  /** The tool's name, e.g. `get_load`. */
5
5
  name: string;
@@ -12,13 +12,15 @@ var statusText = {
12
12
  pending: "Queued",
13
13
  running: "Running",
14
14
  success: "Done",
15
- error: "Failed"
15
+ error: "Failed",
16
+ canceled: "Canceled"
16
17
  };
17
18
  var statusDot = {
18
19
  pending: "neutral",
19
20
  running: "brand",
20
21
  success: "success",
21
- error: "destructive"
22
+ error: "destructive",
23
+ canceled: "neutral"
22
24
  };
23
25
  var json = (value) => {
24
26
  if (typeof value === "string") return value;
@@ -121,18 +123,18 @@ var ToolApproval = React.forwardRef(function ToolApproval({ name, description, a
121
123
  "data-slot": "tool-approval",
122
124
  "aria-busy": busy || void 0,
123
125
  "data-state": state,
124
- className: cn("grid animate-in gap-3 rounded-md border p-3 text-[13px]", state === "pending" ? "border-warning/40 bg-warning/5" : "bg-surface", className),
126
+ className: cn("grid min-w-0 animate-in gap-3 rounded-md border p-3 text-[13px]", state === "pending" ? "border-warning/40 bg-warning/5" : "bg-surface", className),
125
127
  ...props,
126
128
  children: [
127
129
  /* @__PURE__ */ jsxs("div", {
128
- className: "flex items-center gap-2",
130
+ className: "flex flex-wrap items-center gap-2",
129
131
  children: [
130
132
  /* @__PURE__ */ jsx(Wrench, {
131
133
  className: "size-3.5 text-muted-foreground",
132
134
  "aria-hidden": true
133
135
  }),
134
136
  /* @__PURE__ */ jsx("span", {
135
- className: "font-mono text-[12.5px]",
137
+ className: "min-w-0 break-all font-mono text-[12.5px]",
136
138
  children: name
137
139
  }),
138
140
  /* @__PURE__ */ jsx("span", {