@burtson-labs/ui 0.9.0 → 0.10.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 · Avatar · Badge · Breadcrumb · Button · Card · Checkbox · Collapsible · Combobox · Command · Context Menu · Copy Button · Dialog · Dropdown Menu · Editor Tabs · Empty State · Field · Icon Button · Input · Kbd · Label · Menubar · Navigation Menu · Onboarding Checklist · Page Header · Pagination · Popover · Progress · Radio Group · Reasoning · Resizable · Scroll Area · Secret Input · Select · Separator · Sheet · Skeleton · Slider · Spinner · Stat Card · Status · Steps · Switch · Table · Tabs · Textarea · Toast · Toaster · Toolbar · Tooltip · Tour · Tree View
61
+ Accordion · Alert · Alert Dialog · App Shell · Attachment · Avatar · Badge · Breadcrumb · Button · Card · Checkbox · Collapsible · Combobox · Command · Connection Status · Context Menu · Copy Button · Data Table · Dialog · Dropdown Menu · Editor Tabs · Empty State · Field · Icon Button · Input · Kbd · Label · Menubar · Mobile Nav · Navigation Menu · 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 · Toolbar · Tooltip · Tour · Tree View
62
62
 
63
63
  Live previews and code for each one are at [ui.burtson.ai](https://ui.burtson.ai/docs/components/button).
64
64
 
@@ -0,0 +1,56 @@
1
+ import * as React from 'react';
2
+ /**
3
+ * Where a file is on its way into a message. The app owns the upload and the
4
+ * parsing; these components only show the state it reports.
5
+ */
6
+ export type AttachmentState = 'queued' | 'uploading' | 'parsing' | 'ready' | 'failed';
7
+ /** 1536 → "1.5 KB". */
8
+ export declare function formatBytes(bytes: number): string;
9
+ export interface AttachmentItemProps extends Omit<React.ComponentProps<'div'>, 'children'> {
10
+ name: string;
11
+ /** Bytes. */
12
+ size?: number;
13
+ state?: AttachmentState;
14
+ /** 0–100 while uploading or reading. Leave out for an indeterminate bar. */
15
+ progress?: number;
16
+ /** Why it failed, in words the person can act on. */
17
+ error?: string;
18
+ /** A thumbnail URL for images. */
19
+ preview?: string;
20
+ /** Replaces the file icon. */
21
+ icon?: React.ReactNode;
22
+ onRemove?: () => void;
23
+ onRetry?: () => void;
24
+ /** `chip` sits in a composer; `row` fills an upload list. */
25
+ layout?: 'chip' | 'row';
26
+ }
27
+ /**
28
+ * One attached file: name, size, state, progress, and remove/retry. State
29
+ * changes are announced politely; a failure says why.
30
+ */
31
+ declare function AttachmentItem({ name, size, state, progress, error, preview, icon, onRemove, onRetry, layout, className, ...props }: AttachmentItemProps): React.JSX.Element;
32
+ export interface AttachmentTrayProps extends React.ComponentProps<'div'> {
33
+ /** Accessible name for the list. */
34
+ label?: string;
35
+ }
36
+ /** Attachments waiting to send, in a row that scrolls sideways when full. */
37
+ declare function AttachmentTray({ label, className, children, ...props }: AttachmentTrayProps): React.JSX.Element;
38
+ export interface UploadQueueFile {
39
+ id: string;
40
+ name: string;
41
+ size?: number;
42
+ state: AttachmentState;
43
+ progress?: number;
44
+ error?: string;
45
+ }
46
+ export interface UploadQueueProps extends Omit<React.ComponentProps<'section'>, 'title'> {
47
+ files: UploadQueueFile[];
48
+ title?: React.ReactNode;
49
+ onRemove?: (id: string) => void;
50
+ onRetry?: (id: string) => void;
51
+ /** Shown when there are no files. */
52
+ empty?: React.ReactNode;
53
+ }
54
+ /** A list of uploads with a summary: "2 of 4 ready, 1 failed". */
55
+ declare function UploadQueue({ files, title, onRemove, onRetry, empty, className, ...props }: UploadQueueProps): React.JSX.Element;
56
+ export { AttachmentItem, AttachmentTray, UploadQueue };
@@ -0,0 +1,176 @@
1
+ import { cn } from "../lib/utils.js";
2
+ import { Button } from "./button.js";
3
+ import { Progress } from "./progress.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 FileText from "@burtson-labs/icons/react/file-text";
9
+ import RotateCw from "@burtson-labs/icons/react/rotate-cw";
10
+ import X from "@burtson-labs/icons/react/x";
11
+ //#region src/components/attachment.tsx
12
+ var stateText = {
13
+ queued: "Waiting",
14
+ uploading: "Uploading",
15
+ parsing: "Reading",
16
+ ready: "Ready",
17
+ failed: "Failed"
18
+ };
19
+ /** 1536 → "1.5 KB". */
20
+ function formatBytes(bytes) {
21
+ if (!Number.isFinite(bytes) || bytes < 0) return "";
22
+ if (bytes < 1024) return `${bytes} B`;
23
+ const units = [
24
+ "KB",
25
+ "MB",
26
+ "GB",
27
+ "TB"
28
+ ];
29
+ let value = bytes / 1024;
30
+ let unit = 0;
31
+ while (value >= 1024 && unit < units.length - 1) {
32
+ value /= 1024;
33
+ unit++;
34
+ }
35
+ return `${value < 10 ? value.toFixed(1) : Math.round(value)} ${units[unit]}`;
36
+ }
37
+ var InTray = React.createContext(false);
38
+ /**
39
+ * One attached file: name, size, state, progress, and remove/retry. State
40
+ * changes are announced politely; a failure says why.
41
+ */
42
+ function AttachmentItem({ name, size, state = "ready", progress, error, preview, icon, onRemove, onRetry, layout = "chip", className, ...props }) {
43
+ const inTray = React.useContext(InTray);
44
+ const busy = state === "uploading" || state === "parsing";
45
+ const failed = state === "failed";
46
+ const meta = [size !== void 0 ? formatBytes(size) : null, stateText[state]].filter(Boolean).join(" · ");
47
+ return /* @__PURE__ */ jsxs("div", {
48
+ "data-slot": "attachment-item",
49
+ "data-state": state,
50
+ "data-layout": layout,
51
+ role: inTray ? "listitem" : void 0,
52
+ "aria-busy": busy || void 0,
53
+ className: cn("group/attachment relative flex min-w-0 items-center gap-2.5 rounded-md border bg-surface text-sm", layout === "chip" ? "w-60 max-w-full shrink-0 p-1.5 pr-1" : "w-full p-2.5", failed && "border-destructive/40 bg-destructive/5", className),
54
+ ...props,
55
+ children: [
56
+ /* @__PURE__ */ jsx("div", {
57
+ className: cn("grid shrink-0 place-items-center overflow-hidden rounded-sm bg-muted text-muted-foreground [&_svg]:size-4", layout === "chip" ? "size-9" : "size-10"),
58
+ children: preview ? /* @__PURE__ */ jsx("img", {
59
+ src: preview,
60
+ alt: "",
61
+ className: "size-full object-cover"
62
+ }) : failed ? /* @__PURE__ */ jsx(AlertCircle, {
63
+ className: "text-destructive",
64
+ "aria-hidden": true
65
+ }) : busy ? /* @__PURE__ */ jsx(Spinner, {
66
+ className: "size-4",
67
+ role: "presentation",
68
+ "aria-hidden": true
69
+ }) : icon ?? /* @__PURE__ */ jsx(FileText, { "aria-hidden": true })
70
+ }),
71
+ /* @__PURE__ */ jsxs("div", {
72
+ className: "grid min-w-0 flex-1 gap-1",
73
+ children: [
74
+ /* @__PURE__ */ jsx("span", {
75
+ className: "truncate font-medium text-foreground",
76
+ title: name,
77
+ children: name
78
+ }),
79
+ /* @__PURE__ */ jsxs("span", {
80
+ "aria-live": "polite",
81
+ className: cn("truncate text-xs", failed ? "text-destructive" : "text-muted-foreground"),
82
+ children: [/* @__PURE__ */ jsxs("span", {
83
+ className: "sr-only",
84
+ children: [name, ": "]
85
+ }), failed && error ? error : meta]
86
+ }),
87
+ busy && /* @__PURE__ */ jsx(Progress, {
88
+ value: progress,
89
+ "aria-label": `${stateText[state]} ${name}`,
90
+ className: "h-1"
91
+ })
92
+ ]
93
+ }),
94
+ failed && onRetry && /* @__PURE__ */ jsx(Button, {
95
+ type: "button",
96
+ variant: "ghost",
97
+ size: "icon-sm",
98
+ "aria-label": `Retry ${name}`,
99
+ title: "Retry",
100
+ onClick: onRetry,
101
+ children: /* @__PURE__ */ jsx(RotateCw, {})
102
+ }),
103
+ onRemove && /* @__PURE__ */ jsx(Button, {
104
+ type: "button",
105
+ variant: "ghost",
106
+ size: "icon-sm",
107
+ "aria-label": `Remove ${name}`,
108
+ title: "Remove",
109
+ onClick: onRemove,
110
+ children: /* @__PURE__ */ jsx(X, {})
111
+ })
112
+ ]
113
+ });
114
+ }
115
+ /** Attachments waiting to send, in a row that scrolls sideways when full. */
116
+ function AttachmentTray({ label = "Attachments", className, children, ...props }) {
117
+ return /* @__PURE__ */ jsx(InTray.Provider, {
118
+ value: true,
119
+ children: /* @__PURE__ */ jsx("div", {
120
+ "data-slot": "attachment-tray",
121
+ role: "list",
122
+ "aria-label": label,
123
+ className: cn("flex max-w-full gap-1.5 overflow-x-auto pb-0.5", className),
124
+ ...props,
125
+ children
126
+ })
127
+ });
128
+ }
129
+ /** A list of uploads with a summary: "2 of 4 ready, 1 failed". */
130
+ function UploadQueue({ files, title = "Uploads", onRemove, onRetry, empty, className, ...props }) {
131
+ const headingId = React.useId();
132
+ const ready = files.filter((f) => f.state === "ready").length;
133
+ const failed = files.filter((f) => f.state === "failed").length;
134
+ const summary = files.length ? `${ready} of ${files.length} ready${failed ? `, ${failed} failed` : ""}` : "";
135
+ return /* @__PURE__ */ jsxs("section", {
136
+ "data-slot": "upload-queue",
137
+ "aria-labelledby": headingId,
138
+ className: cn("grid gap-2", className),
139
+ ...props,
140
+ children: [/* @__PURE__ */ jsxs("div", {
141
+ className: "flex items-baseline justify-between gap-3",
142
+ children: [/* @__PURE__ */ jsx("h3", {
143
+ id: headingId,
144
+ className: "text-sm font-semibold",
145
+ children: title
146
+ }), /* @__PURE__ */ jsx("span", {
147
+ className: "text-xs text-muted-foreground tabular-nums",
148
+ children: summary
149
+ })]
150
+ }), files.length === 0 ? empty ? /* @__PURE__ */ jsx("div", {
151
+ className: "text-sm text-muted-foreground",
152
+ children: empty
153
+ }) : null : /* @__PURE__ */ jsx(InTray.Provider, {
154
+ value: true,
155
+ children: /* @__PURE__ */ jsx("div", {
156
+ role: "list",
157
+ "aria-labelledby": headingId,
158
+ className: "grid gap-1.5",
159
+ children: files.map((f) => /* @__PURE__ */ jsx(AttachmentItem, {
160
+ layout: "row",
161
+ name: f.name,
162
+ size: f.size,
163
+ state: f.state,
164
+ progress: f.progress,
165
+ error: f.error,
166
+ onRemove: onRemove ? () => onRemove(f.id) : void 0,
167
+ onRetry: onRetry ? () => onRetry(f.id) : void 0
168
+ }, f.id))
169
+ })
170
+ })]
171
+ });
172
+ }
173
+ //#endregion
174
+ export { AttachmentItem, AttachmentTray, UploadQueue, formatBytes };
175
+
176
+ //# sourceMappingURL=attachment.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"attachment.js","names":[],"sources":["../../src/components/attachment.tsx"],"sourcesContent":["import AlertCircle from '@burtson-labs/icons/react/alert-circle';\nimport FileText from '@burtson-labs/icons/react/file-text';\nimport RotateCw from '@burtson-labs/icons/react/rotate-cw';\nimport X from '@burtson-labs/icons/react/x';\nimport * as React from 'react';\n\nimport { cn } from '../lib/utils';\n\nimport { Button } from './button';\nimport { Progress } from './progress';\nimport { Spinner } from './spinner';\n\n/**\n * Where a file is on its way into a message. The app owns the upload and the\n * parsing; these components only show the state it reports.\n */\nexport type AttachmentState = 'queued' | 'uploading' | 'parsing' | 'ready' | 'failed';\n\nconst stateText: Record<AttachmentState, string> = {\n queued: 'Waiting',\n uploading: 'Uploading',\n parsing: 'Reading',\n ready: 'Ready',\n failed: 'Failed',\n};\n\n/** 1536 → \"1.5 KB\". */\nexport function formatBytes(bytes: number): string {\n if (!Number.isFinite(bytes) || bytes < 0) return '';\n if (bytes < 1024) return `${bytes} B`;\n const units = ['KB', 'MB', 'GB', 'TB'];\n let value = bytes / 1024;\n let unit = 0;\n while (value >= 1024 && unit < units.length - 1) {\n value /= 1024;\n unit++;\n }\n return `${value < 10 ? value.toFixed(1) : Math.round(value)} ${units[unit]}`;\n}\n\nconst InTray = React.createContext(false);\n\nexport interface AttachmentItemProps extends Omit<React.ComponentProps<'div'>, 'children'> {\n name: string;\n /** Bytes. */\n size?: number;\n state?: AttachmentState;\n /** 0–100 while uploading or reading. Leave out for an indeterminate bar. */\n progress?: number;\n /** Why it failed, in words the person can act on. */\n error?: string;\n /** A thumbnail URL for images. */\n preview?: string;\n /** Replaces the file icon. */\n icon?: React.ReactNode;\n onRemove?: () => void;\n onRetry?: () => void;\n /** `chip` sits in a composer; `row` fills an upload list. */\n layout?: 'chip' | 'row';\n}\n\n/**\n * One attached file: name, size, state, progress, and remove/retry. State\n * changes are announced politely; a failure says why.\n */\nfunction AttachmentItem({\n name,\n size,\n state = 'ready',\n progress,\n error,\n preview,\n icon,\n onRemove,\n onRetry,\n layout = 'chip',\n className,\n ...props\n}: AttachmentItemProps) {\n const inTray = React.useContext(InTray);\n const busy = state === 'uploading' || state === 'parsing';\n const failed = state === 'failed';\n const meta = [size !== undefined ? formatBytes(size) : null, stateText[state]]\n .filter(Boolean)\n .join(' · ');\n return (\n <div\n data-slot=\"attachment-item\"\n data-state={state}\n data-layout={layout}\n role={inTray ? 'listitem' : undefined}\n aria-busy={busy || undefined}\n className={cn(\n 'group/attachment relative flex min-w-0 items-center gap-2.5 rounded-md border bg-surface text-sm',\n layout === 'chip' ? 'w-60 max-w-full shrink-0 p-1.5 pr-1' : 'w-full p-2.5',\n failed && 'border-destructive/40 bg-destructive/5',\n className,\n )}\n {...props}\n >\n <div\n className={cn(\n 'grid shrink-0 place-items-center overflow-hidden rounded-sm bg-muted text-muted-foreground [&_svg]:size-4',\n layout === 'chip' ? 'size-9' : 'size-10',\n )}\n >\n {preview ? (\n <img src={preview} alt=\"\" className=\"size-full object-cover\" />\n ) : failed ? (\n <AlertCircle className=\"text-destructive\" aria-hidden />\n ) : busy ? (\n <Spinner className=\"size-4\" role=\"presentation\" aria-hidden />\n ) : (\n (icon ?? <FileText aria-hidden />)\n )}\n </div>\n <div className=\"grid min-w-0 flex-1 gap-1\">\n <span className=\"truncate font-medium text-foreground\" title={name}>\n {name}\n </span>\n <span\n aria-live=\"polite\"\n className={cn('truncate text-xs', failed ? 'text-destructive' : 'text-muted-foreground')}\n >\n <span className=\"sr-only\">{name}: </span>\n {failed && error ? error : meta}\n </span>\n {busy && (\n <Progress value={progress} aria-label={`${stateText[state]} ${name}`} className=\"h-1\" />\n )}\n </div>\n {failed && onRetry && (\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"icon-sm\"\n aria-label={`Retry ${name}`}\n title=\"Retry\"\n onClick={onRetry}\n >\n <RotateCw />\n </Button>\n )}\n {onRemove && (\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"icon-sm\"\n aria-label={`Remove ${name}`}\n title=\"Remove\"\n onClick={onRemove}\n >\n <X />\n </Button>\n )}\n </div>\n );\n}\n\nexport interface AttachmentTrayProps extends React.ComponentProps<'div'> {\n /** Accessible name for the list. */\n label?: string;\n}\n\n/** Attachments waiting to send, in a row that scrolls sideways when full. */\nfunction AttachmentTray({\n label = 'Attachments',\n className,\n children,\n ...props\n}: AttachmentTrayProps) {\n return (\n <InTray.Provider value={true}>\n <div\n data-slot=\"attachment-tray\"\n role=\"list\"\n aria-label={label}\n className={cn('flex max-w-full gap-1.5 overflow-x-auto pb-0.5', className)}\n {...props}\n >\n {children}\n </div>\n </InTray.Provider>\n );\n}\n\nexport interface UploadQueueFile {\n id: string;\n name: string;\n size?: number;\n state: AttachmentState;\n progress?: number;\n error?: string;\n}\n\nexport interface UploadQueueProps extends Omit<React.ComponentProps<'section'>, 'title'> {\n files: UploadQueueFile[];\n title?: React.ReactNode;\n onRemove?: (id: string) => void;\n onRetry?: (id: string) => void;\n /** Shown when there are no files. */\n empty?: React.ReactNode;\n}\n\n/** A list of uploads with a summary: \"2 of 4 ready, 1 failed\". */\nfunction UploadQueue({\n files,\n title = 'Uploads',\n onRemove,\n onRetry,\n empty,\n className,\n ...props\n}: UploadQueueProps) {\n const headingId = React.useId();\n const ready = files.filter((f) => f.state === 'ready').length;\n const failed = files.filter((f) => f.state === 'failed').length;\n const summary = files.length\n ? `${ready} of ${files.length} ready${failed ? `, ${failed} failed` : ''}`\n : '';\n return (\n <section\n data-slot=\"upload-queue\"\n aria-labelledby={headingId}\n className={cn('grid gap-2', className)}\n {...props}\n >\n <div className=\"flex items-baseline justify-between gap-3\">\n <h3 id={headingId} className=\"text-sm font-semibold\">\n {title}\n </h3>\n <span className=\"text-xs text-muted-foreground tabular-nums\">{summary}</span>\n </div>\n {files.length === 0 ? (\n empty ? (\n <div className=\"text-sm text-muted-foreground\">{empty}</div>\n ) : null\n ) : (\n <InTray.Provider value={true}>\n <div role=\"list\" aria-labelledby={headingId} className=\"grid gap-1.5\">\n {files.map((f) => (\n <AttachmentItem\n key={f.id}\n layout=\"row\"\n name={f.name}\n size={f.size}\n state={f.state}\n progress={f.progress}\n error={f.error}\n onRemove={onRemove ? () => onRemove(f.id) : undefined}\n onRetry={onRetry ? () => onRetry(f.id) : undefined}\n />\n ))}\n </div>\n </InTray.Provider>\n )}\n </section>\n );\n}\n\nexport { AttachmentItem, AttachmentTray, UploadQueue };\n"],"mappings":"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"}
@@ -1,8 +1,8 @@
1
1
  import { cn } from "../lib/utils.js";
2
+ import { Spinner } from "./spinner.js";
2
3
  import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from "./command.js";
3
4
  import { fieldClasses } from "./input.js";
4
5
  import { Popover, PopoverContent, PopoverTrigger } from "./popover.js";
5
- import { Spinner } from "./spinner.js";
6
6
  import * as React from "react";
7
7
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
8
8
  import Check from "@burtson-labs/icons/react/check";
@@ -1,6 +1,9 @@
1
1
  import * as React from 'react';
2
2
  export interface ComposerProps extends Omit<React.ComponentProps<'form'>, 'onSubmit' | 'onChange'> {
3
- /** Clears after success. Reject a returned promise to retain the draft. */
3
+ /**
4
+ * Clears after success. Reject a returned promise to retain the draft. The
5
+ * text can be empty when `attachmentCount` allows an attachment-only send.
6
+ */
4
7
  onSubmit: (text: string) => void | Promise<void>;
5
8
  onSubmitError?: (error: unknown) => void;
6
9
  submitErrorText?: string;
@@ -14,6 +17,13 @@ export interface ComposerProps extends Omit<React.ComponentProps<'form'>, 'onSub
14
17
  placeholder?: string;
15
18
  /** Attachment chips or a file button, shown under the text. */
16
19
  attachments?: React.ReactNode;
20
+ /** Attachments ready to send. Above zero, an empty message can be sent. */
21
+ attachmentCount?: number;
22
+ /**
23
+ * Extra gate on sending, e.g. false while an attachment is still parsing.
24
+ * Unlike `disabled`, the person can keep typing.
25
+ */
26
+ canSubmit?: boolean;
17
27
  /** Extra controls left of the send button (model picker, tools). */
18
28
  actions?: React.ReactNode;
19
29
  label?: string;
@@ -22,7 +32,7 @@ export interface ComposerProps extends Omit<React.ComponentProps<'form'>, 'onSub
22
32
  * The message box: grows with its text, Enter sends, Shift+Enter adds a
23
33
  * line, and it never sends mid-composition (IME input).
24
34
  */
25
- declare function Composer({ onSubmit, onSubmitError, submitErrorText, value: controlled, onValueChange, streaming, onStop, disabled, placeholder, attachments, actions, label, className, ...props }: ComposerProps): React.JSX.Element;
35
+ declare function Composer({ onSubmit, onSubmitError, submitErrorText, value: controlled, onValueChange, streaming, onStop, disabled, placeholder, attachments, attachmentCount, canSubmit, actions, label, className, ...props }: ComposerProps): React.JSX.Element;
26
36
  export interface SuggestionsProps extends Omit<React.ComponentProps<'div'>, 'onSelect'> {
27
37
  items: string[];
28
38
  onSelect: (item: string) => void;
@@ -9,7 +9,7 @@ import Square from "@burtson-labs/icons/react/square";
9
9
  * The message box: grows with its text, Enter sends, Shift+Enter adds a
10
10
  * line, and it never sends mid-composition (IME input).
11
11
  */
12
- function Composer({ onSubmit, onSubmitError, submitErrorText = "Message could not be sent. Your draft is saved here. Try again.", value: controlled, onValueChange, streaming = false, onStop, disabled = false, placeholder = "Ask anything…", attachments, actions, label = "Message", className, ...props }) {
12
+ function Composer({ onSubmit, onSubmitError, submitErrorText = "Message could not be sent. Your draft is saved here. Try again.", value: controlled, onValueChange, streaming = false, onStop, disabled = false, placeholder = "Ask anything…", attachments, attachmentCount = 0, canSubmit = true, actions, label = "Message", className, ...props }) {
13
13
  const [own, setOwn] = React.useState("");
14
14
  const value = controlled ?? own;
15
15
  const [pending, setPending] = React.useState(false);
@@ -31,7 +31,7 @@ function Composer({ onSubmit, onSubmitError, submitErrorText = "Message could no
31
31
  if (controlled === void 0) setOwn(v);
32
32
  onValueChange?.(v);
33
33
  };
34
- const canSend = !disabled && !streaming && !pending && value.trim().length > 0;
34
+ const canSend = canSubmit && !disabled && !streaming && !pending && (value.trim().length > 0 || attachmentCount > 0);
35
35
  const send = async () => {
36
36
  if (!canSend || sending.current) return;
37
37
  const draft = value;
@@ -1 +1 @@
1
- {"version":3,"file":"composer.js","names":[],"sources":["../../src/components/composer.tsx"],"sourcesContent":["import ArrowUp from '@burtson-labs/icons/react/arrow-up';\nimport Square from '@burtson-labs/icons/react/square';\nimport * as React from 'react';\n\nimport { cn } from '../lib/utils';\n\nimport { Button } from './button';\n\nexport interface ComposerProps extends Omit<React.ComponentProps<'form'>, 'onSubmit' | 'onChange'> {\n /** Clears after success. Reject a returned promise to retain the draft. */\n onSubmit: (text: string) => void | Promise<void>;\n onSubmitError?: (error: unknown) => void;\n submitErrorText?: string;\n /** Controlled value; leave out to let the composer keep its own. */\n value?: string;\n onValueChange?: (value: string) => void;\n /** While a reply streams, the send button becomes Stop. */\n streaming?: boolean;\n onStop?: () => void;\n disabled?: boolean;\n placeholder?: string;\n /** Attachment chips or a file button, shown under the text. */\n attachments?: React.ReactNode;\n /** Extra controls left of the send button (model picker, tools). */\n actions?: React.ReactNode;\n label?: string;\n}\n\n/**\n * The message box: grows with its text, Enter sends, Shift+Enter adds a\n * line, and it never sends mid-composition (IME input).\n */\nfunction Composer({\n onSubmit,\n onSubmitError,\n submitErrorText = 'Message could not be sent. Your draft is saved here. Try again.',\n value: controlled,\n onValueChange,\n streaming = false,\n onStop,\n disabled = false,\n placeholder = 'Ask anything…',\n attachments,\n actions,\n label = 'Message',\n className,\n ...props\n}: ComposerProps) {\n const [own, setOwn] = React.useState('');\n const value = controlled ?? own;\n const [pending, setPending] = React.useState(false);\n const [failed, setFailed] = React.useState(false);\n const sending = React.useRef(false);\n const latest = React.useRef(value);\n const mounted = React.useRef(true);\n const errorId = React.useId();\n React.useEffect(() => {\n latest.current = value;\n }, [value]);\n React.useEffect(() => {\n mounted.current = true;\n return () => {\n mounted.current = false;\n };\n }, []);\n const setValue = (v: string) => {\n if (controlled === undefined) setOwn(v);\n onValueChange?.(v);\n };\n const canSend = !disabled && !streaming && !pending && value.trim().length > 0;\n const send = async () => {\n if (!canSend || sending.current) return;\n const draft = value;\n sending.current = true;\n setFailed(false);\n setPending(true);\n try {\n await onSubmit(draft.trim());\n if (mounted.current && latest.current === draft) setValue('');\n } catch (error) {\n if (mounted.current) setFailed(true);\n onSubmitError?.(error);\n } finally {\n sending.current = false;\n if (mounted.current) setPending(false);\n }\n };\n\n return (\n <form\n data-slot=\"composer\"\n onSubmit={(e) => {\n e.preventDefault();\n void send();\n }}\n className={cn(\n 'grid gap-2 rounded-lg border border-input bg-surface p-2 shadow-xs transition-[border-color,box-shadow] focus-within:border-brand focus-within:ring-[3px] focus-within:ring-ring/15 dark:bg-surface-raised',\n disabled && 'opacity-60',\n className,\n )}\n {...props}\n >\n <textarea\n aria-label={label}\n rows={1}\n value={value}\n disabled={disabled || pending}\n aria-describedby={failed ? errorId : undefined}\n aria-invalid={failed || undefined}\n placeholder={placeholder}\n onChange={(e) => setValue(e.target.value)}\n onKeyDown={(e) => {\n if (\n e.key === 'Enter' &&\n !e.shiftKey &&\n !e.nativeEvent.isComposing &&\n e.nativeEvent.keyCode !== 229\n ) {\n e.preventDefault();\n void send();\n }\n }}\n className=\"field-sizing-content max-h-48 min-h-9 w-full resize-none bg-transparent px-2 py-1.5 text-base sm:text-sm leading-6 outline-none placeholder:text-muted-foreground disabled:cursor-not-allowed\"\n />\n {failed && (\n <p id={errorId} role=\"alert\" className=\"px-2 text-sm text-destructive\">\n {submitErrorText}\n </p>\n )}\n {attachments && <div className=\"flex flex-wrap gap-1.5 px-1\">{attachments}</div>}\n <div className=\"flex items-center gap-1\">\n {actions}\n <div className=\"ml-auto\">\n {streaming ? (\n <Button\n type=\"button\"\n size=\"icon-sm\"\n variant=\"secondary\"\n aria-label=\"Stop\"\n onClick={onStop}\n disabled={!onStop || disabled}\n >\n <Square className=\"fill-current\" />\n </Button>\n ) : (\n <Button\n type=\"submit\"\n size=\"icon-sm\"\n aria-label=\"Send\"\n loading={pending}\n disabled={!canSend}\n >\n <ArrowUp />\n </Button>\n )}\n </div>\n </div>\n </form>\n );\n}\n\nexport interface SuggestionsProps extends Omit<React.ComponentProps<'div'>, 'onSelect'> {\n items: string[];\n onSelect: (item: string) => void;\n}\n\n/** Starter prompts as chips. */\nfunction Suggestions({ items, onSelect, className, ...props }: SuggestionsProps) {\n return (\n <div\n data-slot=\"suggestions\"\n role=\"group\"\n aria-label=\"Suggestions\"\n className={cn('flex flex-wrap gap-2', className)}\n {...props}\n >\n {items.map((item) => (\n <button\n key={item}\n type=\"button\"\n onClick={() => onSelect(item)}\n className=\"animate-in rounded-full border border-border-strong bg-surface px-3 py-1.5 text-[13px] text-foreground outline-none transition-colors hover:border-brand/40 hover:bg-brand-soft hover:text-brand-soft-foreground focus-visible:ring-[3px] focus-visible:ring-ring/20\"\n >\n {item}\n </button>\n ))}\n </div>\n );\n}\n\nexport { Composer, Suggestions };\n"],"mappings":"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"}
1
+ {"version":3,"file":"composer.js","names":[],"sources":["../../src/components/composer.tsx"],"sourcesContent":["import ArrowUp from '@burtson-labs/icons/react/arrow-up';\nimport Square from '@burtson-labs/icons/react/square';\nimport * as React from 'react';\n\nimport { cn } from '../lib/utils';\n\nimport { Button } from './button';\n\nexport interface ComposerProps extends Omit<React.ComponentProps<'form'>, 'onSubmit' | 'onChange'> {\n /**\n * Clears after success. Reject a returned promise to retain the draft. The\n * text can be empty when `attachmentCount` allows an attachment-only send.\n */\n onSubmit: (text: string) => void | Promise<void>;\n onSubmitError?: (error: unknown) => void;\n submitErrorText?: string;\n /** Controlled value; leave out to let the composer keep its own. */\n value?: string;\n onValueChange?: (value: string) => void;\n /** While a reply streams, the send button becomes Stop. */\n streaming?: boolean;\n onStop?: () => void;\n disabled?: boolean;\n placeholder?: string;\n /** Attachment chips or a file button, shown under the text. */\n attachments?: React.ReactNode;\n /** Attachments ready to send. Above zero, an empty message can be sent. */\n attachmentCount?: number;\n /**\n * Extra gate on sending, e.g. false while an attachment is still parsing.\n * Unlike `disabled`, the person can keep typing.\n */\n canSubmit?: boolean;\n /** Extra controls left of the send button (model picker, tools). */\n actions?: React.ReactNode;\n label?: string;\n}\n\n/**\n * The message box: grows with its text, Enter sends, Shift+Enter adds a\n * line, and it never sends mid-composition (IME input).\n */\nfunction Composer({\n onSubmit,\n onSubmitError,\n submitErrorText = 'Message could not be sent. Your draft is saved here. Try again.',\n value: controlled,\n onValueChange,\n streaming = false,\n onStop,\n disabled = false,\n placeholder = 'Ask anything…',\n attachments,\n attachmentCount = 0,\n canSubmit = true,\n actions,\n label = 'Message',\n className,\n ...props\n}: ComposerProps) {\n const [own, setOwn] = React.useState('');\n const value = controlled ?? own;\n const [pending, setPending] = React.useState(false);\n const [failed, setFailed] = React.useState(false);\n const sending = React.useRef(false);\n const latest = React.useRef(value);\n const mounted = React.useRef(true);\n const errorId = React.useId();\n React.useEffect(() => {\n latest.current = value;\n }, [value]);\n React.useEffect(() => {\n mounted.current = true;\n return () => {\n mounted.current = false;\n };\n }, []);\n const setValue = (v: string) => {\n if (controlled === undefined) setOwn(v);\n onValueChange?.(v);\n };\n const canSend =\n canSubmit &&\n !disabled &&\n !streaming &&\n !pending &&\n (value.trim().length > 0 || attachmentCount > 0);\n const send = async () => {\n if (!canSend || sending.current) return;\n const draft = value;\n sending.current = true;\n setFailed(false);\n setPending(true);\n try {\n await onSubmit(draft.trim());\n if (mounted.current && latest.current === draft) setValue('');\n } catch (error) {\n if (mounted.current) setFailed(true);\n onSubmitError?.(error);\n } finally {\n sending.current = false;\n if (mounted.current) setPending(false);\n }\n };\n\n return (\n <form\n data-slot=\"composer\"\n onSubmit={(e) => {\n e.preventDefault();\n void send();\n }}\n className={cn(\n 'grid gap-2 rounded-lg border border-input bg-surface p-2 shadow-xs transition-[border-color,box-shadow] focus-within:border-brand focus-within:ring-[3px] focus-within:ring-ring/15 dark:bg-surface-raised',\n disabled && 'opacity-60',\n className,\n )}\n {...props}\n >\n <textarea\n aria-label={label}\n rows={1}\n value={value}\n disabled={disabled || pending}\n aria-describedby={failed ? errorId : undefined}\n aria-invalid={failed || undefined}\n placeholder={placeholder}\n onChange={(e) => setValue(e.target.value)}\n onKeyDown={(e) => {\n if (\n e.key === 'Enter' &&\n !e.shiftKey &&\n !e.nativeEvent.isComposing &&\n e.nativeEvent.keyCode !== 229\n ) {\n e.preventDefault();\n void send();\n }\n }}\n className=\"field-sizing-content max-h-48 min-h-9 w-full resize-none bg-transparent px-2 py-1.5 text-base sm:text-sm leading-6 outline-none placeholder:text-muted-foreground disabled:cursor-not-allowed\"\n />\n {failed && (\n <p id={errorId} role=\"alert\" className=\"px-2 text-sm text-destructive\">\n {submitErrorText}\n </p>\n )}\n {attachments && <div className=\"flex flex-wrap gap-1.5 px-1\">{attachments}</div>}\n <div className=\"flex items-center gap-1\">\n {actions}\n <div className=\"ml-auto\">\n {streaming ? (\n <Button\n type=\"button\"\n size=\"icon-sm\"\n variant=\"secondary\"\n aria-label=\"Stop\"\n onClick={onStop}\n disabled={!onStop || disabled}\n >\n <Square className=\"fill-current\" />\n </Button>\n ) : (\n <Button\n type=\"submit\"\n size=\"icon-sm\"\n aria-label=\"Send\"\n loading={pending}\n disabled={!canSend}\n >\n <ArrowUp />\n </Button>\n )}\n </div>\n </div>\n </form>\n );\n}\n\nexport interface SuggestionsProps extends Omit<React.ComponentProps<'div'>, 'onSelect'> {\n items: string[];\n onSelect: (item: string) => void;\n}\n\n/** Starter prompts as chips. */\nfunction Suggestions({ items, onSelect, className, ...props }: SuggestionsProps) {\n return (\n <div\n data-slot=\"suggestions\"\n role=\"group\"\n aria-label=\"Suggestions\"\n className={cn('flex flex-wrap gap-2', className)}\n {...props}\n >\n {items.map((item) => (\n <button\n key={item}\n type=\"button\"\n onClick={() => onSelect(item)}\n className=\"animate-in rounded-full border border-border-strong bg-surface px-3 py-1.5 text-[13px] text-foreground outline-none transition-colors hover:border-brand/40 hover:bg-brand-soft hover:text-brand-soft-foreground focus-visible:ring-[3px] focus-visible:ring-ring/20\"\n >\n {item}\n </button>\n ))}\n </div>\n );\n}\n\nexport { Composer, Suggestions };\n"],"mappings":"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"}
@@ -0,0 +1,44 @@
1
+ import * as React from 'react';
2
+ /**
3
+ * The host reports the state; these components only present it. `unknown`
4
+ * means nobody has checked yet and never reads as connected.
5
+ */
6
+ export type ConnectionState = 'connected' | 'connecting' | 'reconnecting' | 'offline' | 'error' | 'unknown';
7
+ export type SyncState = 'synced' | 'syncing' | 'pending' | 'offline' | 'error' | 'unknown';
8
+ export interface ConnectionStatusProps extends Omit<React.ComponentProps<'div'>, 'children'> {
9
+ state: ConnectionState;
10
+ /** Overrides the default text, e.g. "Connected to Ollama". */
11
+ label?: React.ReactNode;
12
+ /** More detail in a tooltip, e.g. the endpoint or last error. */
13
+ detail?: React.ReactNode;
14
+ /** Offered when offline or failed. */
15
+ onRetry?: () => void;
16
+ }
17
+ /** A dot and a word for a live connection: model provider, gateway, socket. */
18
+ declare function ConnectionStatus({ state, label, detail, onRetry, ...props }: ConnectionStatusProps): React.JSX.Element;
19
+ export interface SyncStatusProps extends Omit<React.ComponentProps<'div'>, 'children'> {
20
+ state: SyncState;
21
+ /** When the last successful sync finished. */
22
+ lastSynced?: Date;
23
+ /** Changes waiting to go up. */
24
+ pendingCount?: number;
25
+ detail?: React.ReactNode;
26
+ /** Offered when sync failed. */
27
+ onRetry?: () => void;
28
+ /** Formats `lastSynced`; the default is the local time. */
29
+ formatTime?: (date: Date) => string;
30
+ }
31
+ /** Whether local changes have reached the server. */
32
+ declare function SyncStatus({ state, lastSynced, pendingCount, detail, onRetry, formatTime, ...props }: SyncStatusProps): React.JSX.Element;
33
+ export interface ConnectionBannerProps extends Omit<React.ComponentProps<'div'>, 'children'> {
34
+ state: ConnectionState;
35
+ /** What still works, e.g. "Messages you send will go out when you reconnect." */
36
+ description?: React.ReactNode;
37
+ onRetry?: () => void;
38
+ }
39
+ /**
40
+ * A full-width notice for connection trouble. It renders nothing while
41
+ * connected, connecting or unknown, so it can stay mounted.
42
+ */
43
+ declare function ConnectionBanner({ state, description, onRetry, className, ...props }: ConnectionBannerProps): React.JSX.Element | null;
44
+ export { ConnectionBanner, ConnectionStatus, SyncStatus };
@@ -0,0 +1,167 @@
1
+ import { cn } from "../lib/utils.js";
2
+ import { Alert, AlertDescription, AlertTitle } from "./alert.js";
3
+ import { Button } from "./button.js";
4
+ import { Status } from "./status.js";
5
+ import { Tooltip, TooltipContent, TooltipTrigger } from "./tooltip.js";
6
+ import "react";
7
+ import { jsx, jsxs } from "react/jsx-runtime";
8
+ import CloudOff from "@burtson-labs/icons/react/cloud-off";
9
+ import WifiOff from "@burtson-labs/icons/react/wifi-off";
10
+ //#region src/components/connection-status.tsx
11
+ var connection = {
12
+ connected: {
13
+ text: "Connected",
14
+ tone: "success"
15
+ },
16
+ connecting: {
17
+ text: "Connecting",
18
+ tone: "info",
19
+ pulse: true
20
+ },
21
+ reconnecting: {
22
+ text: "Reconnecting",
23
+ tone: "warning",
24
+ pulse: true
25
+ },
26
+ offline: {
27
+ text: "Offline",
28
+ tone: "neutral"
29
+ },
30
+ error: {
31
+ text: "Connection failed",
32
+ tone: "danger"
33
+ },
34
+ unknown: {
35
+ text: "Status unknown",
36
+ tone: "neutral"
37
+ }
38
+ };
39
+ var sync = {
40
+ synced: {
41
+ text: "Synced",
42
+ tone: "success"
43
+ },
44
+ syncing: {
45
+ text: "Syncing",
46
+ tone: "info",
47
+ pulse: true
48
+ },
49
+ pending: {
50
+ text: "Changes not synced yet",
51
+ tone: "warning"
52
+ },
53
+ offline: {
54
+ text: "Offline, saved on this device",
55
+ tone: "neutral"
56
+ },
57
+ error: {
58
+ text: "Sync failed",
59
+ tone: "danger"
60
+ },
61
+ unknown: {
62
+ text: "Sync status unknown",
63
+ tone: "neutral"
64
+ }
65
+ };
66
+ function WithDetail({ detail, children }) {
67
+ if (!detail) return children;
68
+ return /* @__PURE__ */ jsxs(Tooltip, { children: [/* @__PURE__ */ jsx(TooltipTrigger, {
69
+ asChild: true,
70
+ children
71
+ }), /* @__PURE__ */ jsx(TooltipContent, { children: detail })] });
72
+ }
73
+ function StatusLine({ slot, state, text, tone, pulse, detail, onRetry, className, ...props }) {
74
+ return /* @__PURE__ */ jsxs("div", {
75
+ "data-slot": slot,
76
+ "data-state": state,
77
+ role: "status",
78
+ className: cn("inline-flex items-center gap-2", className),
79
+ ...props,
80
+ children: [/* @__PURE__ */ jsx(WithDetail, {
81
+ detail,
82
+ children: /* @__PURE__ */ jsxs(Status, {
83
+ status: tone,
84
+ pulse,
85
+ tabIndex: detail ? 0 : void 0,
86
+ className: "rounded-sm outline-none focus-visible:ring-[3px] focus-visible:ring-ring/20",
87
+ children: [text, detail && typeof detail === "string" && /* @__PURE__ */ jsxs("span", {
88
+ className: "sr-only",
89
+ children: [". ", detail]
90
+ })]
91
+ })
92
+ }), onRetry && /* @__PURE__ */ jsx(Button, {
93
+ type: "button",
94
+ variant: "link",
95
+ size: "xs",
96
+ className: "h-auto px-0",
97
+ onClick: onRetry,
98
+ children: "Retry"
99
+ })]
100
+ });
101
+ }
102
+ /** A dot and a word for a live connection: model provider, gateway, socket. */
103
+ function ConnectionStatus({ state, label, detail, onRetry, ...props }) {
104
+ const c = connection[state];
105
+ return /* @__PURE__ */ jsx(StatusLine, {
106
+ slot: "connection-status",
107
+ state,
108
+ text: label ?? c.text,
109
+ tone: c.tone,
110
+ pulse: c.pulse,
111
+ detail,
112
+ onRetry: state === "offline" || state === "error" ? onRetry : void 0,
113
+ ...props
114
+ });
115
+ }
116
+ /** Whether local changes have reached the server. */
117
+ function SyncStatus({ state, lastSynced, pendingCount, detail, onRetry, formatTime = (d) => d.toLocaleTimeString([], {
118
+ hour: "numeric",
119
+ minute: "2-digit"
120
+ }), ...props }) {
121
+ const s = sync[state];
122
+ let text = s.text;
123
+ if (state === "synced" && lastSynced) text = `Synced ${formatTime(lastSynced)}`;
124
+ if (state === "pending" && pendingCount) text = `${pendingCount} ${pendingCount === 1 ? "change" : "changes"} not synced yet`;
125
+ return /* @__PURE__ */ jsx(StatusLine, {
126
+ slot: "sync-status",
127
+ state,
128
+ text,
129
+ tone: s.tone,
130
+ pulse: s.pulse,
131
+ detail,
132
+ onRetry: state === "error" ? onRetry : void 0,
133
+ ...props
134
+ });
135
+ }
136
+ /**
137
+ * A full-width notice for connection trouble. It renders nothing while
138
+ * connected, connecting or unknown, so it can stay mounted.
139
+ */
140
+ function ConnectionBanner({ state, description, onRetry, className, ...props }) {
141
+ if (state !== "offline" && state !== "error" && state !== "reconnecting") return null;
142
+ const title = state === "offline" ? "You are offline" : state === "reconnecting" ? "Reconnecting…" : "Could not connect";
143
+ return /* @__PURE__ */ jsxs(Alert, {
144
+ "data-slot": "connection-banner",
145
+ "data-state": state,
146
+ variant: state === "error" ? "destructive" : "warning",
147
+ className: cn("items-center", className),
148
+ ...props,
149
+ children: [state === "offline" ? /* @__PURE__ */ jsx(WifiOff, { "aria-hidden": true }) : /* @__PURE__ */ jsx(CloudOff, { "aria-hidden": true }), /* @__PURE__ */ jsxs("div", {
150
+ className: "flex min-w-0 flex-wrap items-center justify-between gap-x-4 gap-y-2",
151
+ children: [/* @__PURE__ */ jsxs("div", {
152
+ className: "min-w-0",
153
+ children: [/* @__PURE__ */ jsx(AlertTitle, { children: title }), description && /* @__PURE__ */ jsx(AlertDescription, { children: description })]
154
+ }), onRetry && state !== "reconnecting" && /* @__PURE__ */ jsx(Button, {
155
+ type: "button",
156
+ size: "sm",
157
+ variant: "outline",
158
+ onClick: onRetry,
159
+ children: "Retry"
160
+ })]
161
+ })]
162
+ });
163
+ }
164
+ //#endregion
165
+ export { ConnectionBanner, ConnectionStatus, SyncStatus };
166
+
167
+ //# sourceMappingURL=connection-status.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"connection-status.js","names":[],"sources":["../../src/components/connection-status.tsx"],"sourcesContent":["import CloudOff from '@burtson-labs/icons/react/cloud-off';\nimport WifiOff from '@burtson-labs/icons/react/wifi-off';\nimport * as React from 'react';\n\nimport { cn } from '../lib/utils';\n\nimport { Alert, AlertDescription, AlertTitle } from './alert';\nimport { Button } from './button';\nimport { Status } from './status';\nimport { Tooltip, TooltipContent, TooltipTrigger } from './tooltip';\n\ntype Tone = 'neutral' | 'brand' | 'success' | 'warning' | 'danger' | 'info';\n\n/**\n * The host reports the state; these components only present it. `unknown`\n * means nobody has checked yet and never reads as connected.\n */\nexport type ConnectionState =\n 'connected' | 'connecting' | 'reconnecting' | 'offline' | 'error' | 'unknown';\n\nconst connection: Record<ConnectionState, { text: string; tone: Tone; pulse?: boolean }> = {\n connected: { text: 'Connected', tone: 'success' },\n connecting: { text: 'Connecting', tone: 'info', pulse: true },\n reconnecting: { text: 'Reconnecting', tone: 'warning', pulse: true },\n offline: { text: 'Offline', tone: 'neutral' },\n error: { text: 'Connection failed', tone: 'danger' },\n unknown: { text: 'Status unknown', tone: 'neutral' },\n};\n\nexport type SyncState = 'synced' | 'syncing' | 'pending' | 'offline' | 'error' | 'unknown';\n\nconst sync: Record<SyncState, { text: string; tone: Tone; pulse?: boolean }> = {\n synced: { text: 'Synced', tone: 'success' },\n syncing: { text: 'Syncing', tone: 'info', pulse: true },\n pending: { text: 'Changes not synced yet', tone: 'warning' },\n offline: { text: 'Offline, saved on this device', tone: 'neutral' },\n error: { text: 'Sync failed', tone: 'danger' },\n unknown: { text: 'Sync status unknown', tone: 'neutral' },\n};\n\nfunction WithDetail({\n detail,\n children,\n}: {\n detail?: React.ReactNode;\n children: React.ReactElement;\n}) {\n if (!detail) return children;\n return (\n <Tooltip>\n <TooltipTrigger asChild>{children}</TooltipTrigger>\n <TooltipContent>{detail}</TooltipContent>\n </Tooltip>\n );\n}\n\nfunction StatusLine({\n slot,\n state,\n text,\n tone,\n pulse,\n detail,\n onRetry,\n className,\n ...props\n}: Omit<React.ComponentProps<'div'>, 'children'> & {\n slot: string;\n state: string;\n text: React.ReactNode;\n tone: Tone;\n pulse?: boolean;\n detail?: React.ReactNode;\n onRetry?: () => void;\n}) {\n return (\n <div\n data-slot={slot}\n data-state={state}\n role=\"status\"\n className={cn('inline-flex items-center gap-2', className)}\n {...props}\n >\n <WithDetail detail={detail}>\n <Status\n status={tone}\n pulse={pulse}\n tabIndex={detail ? 0 : undefined}\n className=\"rounded-sm outline-none focus-visible:ring-[3px] focus-visible:ring-ring/20\"\n >\n {text}\n {detail && typeof detail === 'string' && <span className=\"sr-only\">. {detail}</span>}\n </Status>\n </WithDetail>\n {onRetry && (\n <Button type=\"button\" variant=\"link\" size=\"xs\" className=\"h-auto px-0\" onClick={onRetry}>\n Retry\n </Button>\n )}\n </div>\n );\n}\n\nexport interface ConnectionStatusProps extends Omit<React.ComponentProps<'div'>, 'children'> {\n state: ConnectionState;\n /** Overrides the default text, e.g. \"Connected to Ollama\". */\n label?: React.ReactNode;\n /** More detail in a tooltip, e.g. the endpoint or last error. */\n detail?: React.ReactNode;\n /** Offered when offline or failed. */\n onRetry?: () => void;\n}\n\n/** A dot and a word for a live connection: model provider, gateway, socket. */\nfunction ConnectionStatus({ state, label, detail, onRetry, ...props }: ConnectionStatusProps) {\n const c = connection[state];\n return (\n <StatusLine\n slot=\"connection-status\"\n state={state}\n text={label ?? c.text}\n tone={c.tone}\n pulse={c.pulse}\n detail={detail}\n onRetry={state === 'offline' || state === 'error' ? onRetry : undefined}\n {...props}\n />\n );\n}\n\nexport interface SyncStatusProps extends Omit<React.ComponentProps<'div'>, 'children'> {\n state: SyncState;\n /** When the last successful sync finished. */\n lastSynced?: Date;\n /** Changes waiting to go up. */\n pendingCount?: number;\n detail?: React.ReactNode;\n /** Offered when sync failed. */\n onRetry?: () => void;\n /** Formats `lastSynced`; the default is the local time. */\n formatTime?: (date: Date) => string;\n}\n\n/** Whether local changes have reached the server. */\nfunction SyncStatus({\n state,\n lastSynced,\n pendingCount,\n detail,\n onRetry,\n formatTime = (d) => d.toLocaleTimeString([], { hour: 'numeric', minute: '2-digit' }),\n ...props\n}: SyncStatusProps) {\n const s = sync[state];\n let text = s.text;\n if (state === 'synced' && lastSynced) text = `Synced ${formatTime(lastSynced)}`;\n if (state === 'pending' && pendingCount)\n text = `${pendingCount} ${pendingCount === 1 ? 'change' : 'changes'} not synced yet`;\n return (\n <StatusLine\n slot=\"sync-status\"\n state={state}\n text={text}\n tone={s.tone}\n pulse={s.pulse}\n detail={detail}\n onRetry={state === 'error' ? onRetry : undefined}\n {...props}\n />\n );\n}\n\nexport interface ConnectionBannerProps extends Omit<React.ComponentProps<'div'>, 'children'> {\n state: ConnectionState;\n /** What still works, e.g. \"Messages you send will go out when you reconnect.\" */\n description?: React.ReactNode;\n onRetry?: () => void;\n}\n\n/**\n * A full-width notice for connection trouble. It renders nothing while\n * connected, connecting or unknown, so it can stay mounted.\n */\nfunction ConnectionBanner({\n state,\n description,\n onRetry,\n className,\n ...props\n}: ConnectionBannerProps) {\n if (state !== 'offline' && state !== 'error' && state !== 'reconnecting') return null;\n const title =\n state === 'offline'\n ? 'You are offline'\n : state === 'reconnecting'\n ? 'Reconnecting…'\n : 'Could not connect';\n return (\n <Alert\n data-slot=\"connection-banner\"\n data-state={state}\n variant={state === 'error' ? 'destructive' : 'warning'}\n className={cn('items-center', className)}\n {...props}\n >\n {state === 'offline' ? <WifiOff aria-hidden /> : <CloudOff aria-hidden />}\n <div className=\"flex min-w-0 flex-wrap items-center justify-between gap-x-4 gap-y-2\">\n <div className=\"min-w-0\">\n <AlertTitle>{title}</AlertTitle>\n {description && <AlertDescription>{description}</AlertDescription>}\n </div>\n {onRetry && state !== 'reconnecting' && (\n <Button type=\"button\" size=\"sm\" variant=\"outline\" onClick={onRetry}>\n Retry\n </Button>\n )}\n </div>\n </Alert>\n );\n}\n\nexport { ConnectionBanner, ConnectionStatus, SyncStatus };\n"],"mappings":";;;;;;;;;;AAoBA,IAAM,aAAqF;CACzF,WAAW;EAAE,MAAM;EAAa,MAAM;CAAU;CAChD,YAAY;EAAE,MAAM;EAAc,MAAM;EAAQ,OAAO;CAAK;CAC5D,cAAc;EAAE,MAAM;EAAgB,MAAM;EAAW,OAAO;CAAK;CACnE,SAAS;EAAE,MAAM;EAAW,MAAM;CAAU;CAC5C,OAAO;EAAE,MAAM;EAAqB,MAAM;CAAS;CACnD,SAAS;EAAE,MAAM;EAAkB,MAAM;CAAU;AACrD;AAIA,IAAM,OAAyE;CAC7E,QAAQ;EAAE,MAAM;EAAU,MAAM;CAAU;CAC1C,SAAS;EAAE,MAAM;EAAW,MAAM;EAAQ,OAAO;CAAK;CACtD,SAAS;EAAE,MAAM;EAA0B,MAAM;CAAU;CAC3D,SAAS;EAAE,MAAM;EAAiC,MAAM;CAAU;CAClE,OAAO;EAAE,MAAM;EAAe,MAAM;CAAS;CAC7C,SAAS;EAAE,MAAM;EAAuB,MAAM;CAAU;AAC1D;AAEA,SAAS,WAAW,EAClB,QACA,YAIC;CACD,IAAI,CAAC,QAAQ,OAAO;CACpB,OACE,qBAAC,SAAD,EAAA,UAAA,CACE,oBAAC,gBAAD;EAAgB,SAAA;EAAS;CAAyB,CAAA,GAClD,oBAAC,gBAAD,EAAA,UAAiB,OAAuB,CAAA,CACjC,EAAA,CAAA;AAEb;AAEA,SAAS,WAAW,EAClB,MACA,OACA,MACA,MACA,OACA,QACA,SACA,WACA,GAAG,SASF;CACD,OACE,qBAAC,OAAD;EACE,aAAW;EACX,cAAY;EACZ,MAAK;EACL,WAAW,GAAG,kCAAkC,SAAS;EACzD,GAAI;EALN,UAAA,CAOE,oBAAC,YAAD;GAAoB;GAClB,UAAA,qBAAC,QAAD;IACE,QAAQ;IACD;IACP,UAAU,SAAS,IAAI,KAAA;IACvB,WAAU;IAJZ,UAAA,CAMG,MACA,UAAU,OAAO,WAAW,YAAY,qBAAC,QAAD;KAAM,WAAU;KAAhB,UAAA,CAA0B,MAAG,MAAa;IAC7E,CAAA,CAAA;;EACE,CAAA,GACX,WACC,oBAAC,QAAD;GAAQ,MAAK;GAAS,SAAQ;GAAO,MAAK;GAAK,WAAU;GAAc,SAAS;GAAS,UAAA;EAEjF,CAAA,CAEP;;AAET;;AAaA,SAAS,iBAAiB,EAAE,OAAO,OAAO,QAAQ,SAAS,GAAG,SAAgC;CAC5F,MAAM,IAAI,WAAW;CACrB,OACE,oBAAC,YAAD;EACE,MAAK;EACE;EACP,MAAM,SAAS,EAAE;EACjB,MAAM,EAAE;EACR,OAAO,EAAE;EACD;EACR,SAAS,UAAU,aAAa,UAAU,UAAU,UAAU,KAAA;EAC9D,GAAI;CACL,CAAA;AAEL;;AAgBA,SAAS,WAAW,EAClB,OACA,YACA,cACA,QACA,SACA,cAAc,MAAM,EAAE,mBAAmB,CAAC,GAAG;CAAE,MAAM;CAAW,QAAQ;AAAU,CAAC,GACnF,GAAG,SACe;CAClB,MAAM,IAAI,KAAK;CACf,IAAI,OAAO,EAAE;CACb,IAAI,UAAU,YAAY,YAAY,OAAO,UAAU,WAAW,UAAU;CAC5E,IAAI,UAAU,aAAa,cACzB,OAAO,GAAG,aAAa,GAAG,iBAAiB,IAAI,WAAW,UAAU;CACtE,OACE,oBAAC,YAAD;EACE,MAAK;EACE;EACD;EACN,MAAM,EAAE;EACR,OAAO,EAAE;EACD;EACR,SAAS,UAAU,UAAU,UAAU,KAAA;EACvC,GAAI;CACL,CAAA;AAEL;;;;;AAaA,SAAS,iBAAiB,EACxB,OACA,aACA,SACA,WACA,GAAG,SACqB;CACxB,IAAI,UAAU,aAAa,UAAU,WAAW,UAAU,gBAAgB,OAAO;CACjF,MAAM,QACJ,UAAU,YACN,oBACA,UAAU,iBACR,kBACA;CACR,OACE,qBAAC,OAAD;EACE,aAAU;EACV,cAAY;EACZ,SAAS,UAAU,UAAU,gBAAgB;EAC7C,WAAW,GAAG,gBAAgB,SAAS;EACvC,GAAI;EALN,UAAA,CAOG,UAAU,YAAY,oBAAC,SAAD,EAAS,eAAA,KAAa,CAAA,IAAI,oBAAC,UAAD,EAAU,eAAA,KAAa,CAAA,GACxE,qBAAC,OAAD;GAAK,WAAU;GAAf,UAAA,CACE,qBAAC,OAAD;IAAK,WAAU;IAAf,UAAA,CACE,oBAAC,YAAD,EAAA,UAAa,MAAkB,CAAA,GAC9B,eAAe,oBAAC,kBAAD,EAAA,UAAmB,YAA8B,CAAA,CAC9D;GACJ,CAAA,GAAA,WAAW,UAAU,kBACpB,oBAAC,QAAD;IAAQ,MAAK;IAAS,MAAK;IAAK,SAAQ;IAAU,SAAS;IAAS,UAAA;GAE5D,CAAA,CAEP;EACA,CAAA,CAAA;;AAEX"}