@burtson-labs/ui 0.9.1 → 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 +1 -1
- package/dist/components/attachment.d.ts +56 -0
- package/dist/components/attachment.js +176 -0
- package/dist/components/attachment.js.map +1 -0
- package/dist/components/combobox.js +1 -1
- package/dist/components/connection-status.d.ts +44 -0
- package/dist/components/connection-status.js +167 -0
- package/dist/components/connection-status.js.map +1 -0
- package/dist/components/data-table.d.ts +62 -0
- package/dist/components/data-table.js +264 -0
- package/dist/components/data-table.js.map +1 -0
- package/dist/components/mobile-nav.d.ts +32 -0
- package/dist/components/mobile-nav.js +72 -0
- package/dist/components/mobile-nav.js.map +1 -0
- package/dist/components/sheet.d.ts +9 -3
- package/dist/components/sheet.js +18 -10
- package/dist/components/sheet.js.map +1 -1
- package/dist/components/source.d.ts +31 -0
- package/dist/components/source.js +119 -0
- package/dist/components/source.js.map +1 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +13 -8
- package/dist/styles.css +1 -1
- package/package.json +1 -1
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":";;;;;;;;;;;AAkBA,IAAM,YAA6C;CACjD,QAAQ;CACR,WAAW;CACX,SAAS;CACT,OAAO;CACP,QAAQ;AACV;;AAGA,SAAgB,YAAY,OAAuB;CACjD,IAAI,CAAC,OAAO,SAAS,KAAK,KAAK,QAAQ,GAAG,OAAO;CACjD,IAAI,QAAQ,MAAM,OAAO,GAAG,MAAM;CAClC,MAAM,QAAQ;EAAC;EAAM;EAAM;EAAM;CAAI;CACrC,IAAI,QAAQ,QAAQ;CACpB,IAAI,OAAO;CACX,OAAO,SAAS,QAAQ,OAAO,MAAM,SAAS,GAAG;EAC/C,SAAS;EACT;CACF;CACA,OAAO,GAAG,QAAQ,KAAK,MAAM,QAAQ,CAAC,IAAI,KAAK,MAAM,KAAK,EAAE,GAAG,MAAM;AACvE;AAEA,IAAM,SAAS,MAAM,cAAc,KAAK;;;;;AAyBxC,SAAS,eAAe,EACtB,MACA,MACA,QAAQ,SACR,UACA,OACA,SACA,MACA,UACA,SACA,SAAS,QACT,WACA,GAAG,SACmB;CACtB,MAAM,SAAS,MAAM,WAAW,MAAM;CACtC,MAAM,OAAO,UAAU,eAAe,UAAU;CAChD,MAAM,SAAS,UAAU;CACzB,MAAM,OAAO,CAAC,SAAS,KAAA,IAAY,YAAY,IAAI,IAAI,MAAM,UAAU,MAAM,CAAC,CAC3E,OAAO,OAAO,CAAC,CACf,KAAK,KAAK;CACb,OACE,qBAAC,OAAD;EACE,aAAU;EACV,cAAY;EACZ,eAAa;EACb,MAAM,SAAS,aAAa,KAAA;EAC5B,aAAW,QAAQ,KAAA;EACnB,WAAW,GACT,oGACA,WAAW,SAAS,wCAAwC,gBAC5D,UAAU,0CACV,SACF;EACA,GAAI;EAZN,UAAA;GAcE,oBAAC,OAAD;IACE,WAAW,GACT,6GACA,WAAW,SAAS,WAAW,SACjC;IAEC,UAAA,UACC,oBAAC,OAAD;KAAK,KAAK;KAAS,KAAI;KAAG,WAAU;IAA0B,CAAA,IAC5D,SACF,oBAAC,aAAD;KAAa,WAAU;KAAmB,eAAA;IAAa,CAAA,IACrD,OACF,oBAAC,SAAD;KAAS,WAAU;KAAS,MAAK;KAAe,eAAA;IAAa,CAAA,IAE5D,QAAQ,oBAAC,UAAD,EAAU,eAAA,KAAa,CAAA;GAE/B,CAAA;GACL,qBAAC,OAAD;IAAK,WAAU;IAAf,UAAA;KACE,oBAAC,QAAD;MAAM,WAAU;MAAuC,OAAO;MAC3D,UAAA;KACG,CAAA;KACN,qBAAC,QAAD;MACE,aAAU;MACV,WAAW,GAAG,oBAAoB,SAAS,qBAAqB,uBAAuB;MAFzF,UAAA,CAIE,qBAAC,QAAD;OAAM,WAAU;OAAhB,UAAA,CAA2B,MAAK,IAAQ;MACvC,CAAA,GAAA,UAAU,QAAQ,QAAQ,IACvB;;KACL,QACC,oBAAC,UAAD;MAAU,OAAO;MAAU,cAAY,GAAG,UAAU,OAAO,GAAG;MAAQ,WAAU;KAAO,CAAA;IAEtF;;GACJ,UAAU,WACT,oBAAC,QAAD;IACE,MAAK;IACL,SAAQ;IACR,MAAK;IACL,cAAY,SAAS;IACrB,OAAM;IACN,SAAS;IAET,UAAA,oBAAC,UAAD,CAAW,CAAA;GACL,CAAA;GAET,YACC,oBAAC,QAAD;IACE,MAAK;IACL,SAAQ;IACR,MAAK;IACL,cAAY,UAAU;IACtB,OAAM;IACN,SAAS;IAET,UAAA,oBAAC,GAAD,CAAI,CAAA;GACE,CAAA;EAEP;;AAET;;AAQA,SAAS,eAAe,EACtB,QAAQ,eACR,WACA,UACA,GAAG,SACmB;CACtB,OACE,oBAAC,OAAO,UAAR;EAAiB,OAAO;EACtB,UAAA,oBAAC,OAAD;GACE,aAAU;GACV,MAAK;GACL,cAAY;GACZ,WAAW,GAAG,kDAAkD,SAAS;GACzE,GAAI;GAEH;EACE,CAAA;CACU,CAAA;AAErB;;AAqBA,SAAS,YAAY,EACnB,OACA,QAAQ,WACR,UACA,SACA,OACA,WACA,GAAG,SACgB;CACnB,MAAM,YAAY,MAAM,MAAM;CAC9B,MAAM,QAAQ,MAAM,QAAQ,MAAM,EAAE,UAAU,OAAO,CAAC,CAAC;CACvD,MAAM,SAAS,MAAM,QAAQ,MAAM,EAAE,UAAU,QAAQ,CAAC,CAAC;CACzD,MAAM,UAAU,MAAM,SAClB,GAAG,MAAM,MAAM,MAAM,OAAO,QAAQ,SAAS,KAAK,OAAO,WAAW,OACpE;CACJ,OACE,qBAAC,WAAD;EACE,aAAU;EACV,mBAAiB;EACjB,WAAW,GAAG,cAAc,SAAS;EACrC,GAAI;EAJN,UAAA,CAME,qBAAC,OAAD;GAAK,WAAU;GAAf,UAAA,CACE,oBAAC,MAAD;IAAI,IAAI;IAAW,WAAU;IAC1B,UAAA;GACC,CAAA,GACJ,oBAAC,QAAD;IAAM,WAAU;IAA8C,UAAA;GAAc,CAAA,CACzE;EACJ,CAAA,GAAA,MAAM,WAAW,IAChB,QACE,oBAAC,OAAD;GAAK,WAAU;GAAiC,UAAA;EAAW,CAAA,IACzD,OAEJ,oBAAC,OAAO,UAAR;GAAiB,OAAO;GACtB,UAAA,oBAAC,OAAD;IAAK,MAAK;IAAO,mBAAiB;IAAW,WAAU;IACpD,UAAA,MAAM,KAAK,MACV,oBAAC,gBAAD;KAEE,QAAO;KACP,MAAM,EAAE;KACR,MAAM,EAAE;KACR,OAAO,EAAE;KACT,UAAU,EAAE;KACZ,OAAO,EAAE;KACT,UAAU,iBAAiB,SAAS,EAAE,EAAE,IAAI,KAAA;KAC5C,SAAS,gBAAgB,QAAQ,EAAE,EAAE,IAAI,KAAA;IAC1C,GATM,EAAE,EASR,CACF;GACE,CAAA;EACU,CAAA,CAEZ;;AAEb"}
|
|
@@ -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";
|
|
@@ -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":"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"}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
export interface DataTableColumn<T> {
|
|
3
|
+
id: string;
|
|
4
|
+
header: React.ReactNode;
|
|
5
|
+
cell: (row: T) => React.ReactNode;
|
|
6
|
+
sortable?: boolean;
|
|
7
|
+
/** Right-aligned tabular figures. */
|
|
8
|
+
numeric?: boolean;
|
|
9
|
+
className?: string;
|
|
10
|
+
}
|
|
11
|
+
export type SortDirection = 'asc' | 'desc';
|
|
12
|
+
export interface DataTableSort {
|
|
13
|
+
columnId: string;
|
|
14
|
+
direction: SortDirection;
|
|
15
|
+
}
|
|
16
|
+
export interface DataTableProps<T> extends Omit<React.ComponentProps<'div'>, 'children'> {
|
|
17
|
+
/** Required: what the table lists, e.g. "Workspace members". */
|
|
18
|
+
'aria-label': string;
|
|
19
|
+
columns: DataTableColumn<T>[];
|
|
20
|
+
/** The rows to show now: already sorted, filtered and paged by the app or server. */
|
|
21
|
+
rows: T[];
|
|
22
|
+
getRowId: (row: T) => string;
|
|
23
|
+
/** Names a row for screen readers ("Select Ada Lovelace"). Defaults to its id. */
|
|
24
|
+
getRowLabel?: (row: T) => string;
|
|
25
|
+
/** Controlled sort. A sortable header cycles ascending, descending, none. */
|
|
26
|
+
sort?: DataTableSort | null;
|
|
27
|
+
onSortChange?: (sort: DataTableSort | null) => void;
|
|
28
|
+
/** Controlled filter text; a search box appears when onFilterChange is set. */
|
|
29
|
+
filter?: string;
|
|
30
|
+
onFilterChange?: (value: string) => void;
|
|
31
|
+
filterPlaceholder?: string;
|
|
32
|
+
/** Controlled selection by row id; checkboxes appear when onSelectedChange is set. */
|
|
33
|
+
selected?: string[];
|
|
34
|
+
onSelectedChange?: (ids: string[]) => void;
|
|
35
|
+
/** Enter or a click on the row, e.g. open the record. */
|
|
36
|
+
onRowAction?: (row: T) => void;
|
|
37
|
+
/** Per-row controls in a trailing column, e.g. a DropdownMenu. */
|
|
38
|
+
rowActions?: (row: T) => React.ReactNode;
|
|
39
|
+
/** 1-based; Pagination appears when onPageChange is set. */
|
|
40
|
+
page?: number;
|
|
41
|
+
pageCount?: number;
|
|
42
|
+
onPageChange?: (page: number) => void;
|
|
43
|
+
/** e.g. "1–25 of 312". */
|
|
44
|
+
pageSummary?: React.ReactNode;
|
|
45
|
+
loading?: boolean;
|
|
46
|
+
/** A message shown in place of the rows. */
|
|
47
|
+
error?: React.ReactNode;
|
|
48
|
+
onRetry?: () => void;
|
|
49
|
+
/** Shown when there are no rows and nothing failed. */
|
|
50
|
+
empty?: React.ReactNode;
|
|
51
|
+
/** Extra controls beside the search box, e.g. filters or a bulk action. */
|
|
52
|
+
toolbar?: React.ReactNode;
|
|
53
|
+
density?: 'compact' | 'default';
|
|
54
|
+
stickyHeader?: boolean;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* A table recipe for records: sort, search, select, page, and row actions,
|
|
58
|
+
* with loading, empty and error states. Fully controlled, so the data can
|
|
59
|
+
* come from a server; the table never reorders rows itself.
|
|
60
|
+
*/
|
|
61
|
+
declare function DataTable<T>({ 'aria-label': ariaLabel, columns, rows, getRowId, getRowLabel, sort, onSortChange, filter, onFilterChange, filterPlaceholder, selected, onSelectedChange, onRowAction, rowActions, page, pageCount, onPageChange, pageSummary, loading, error, onRetry, empty, toolbar, density, stickyHeader, className, ...props }: DataTableProps<T>): React.JSX.Element;
|
|
62
|
+
export { DataTable };
|