@dowel-ui/react 0.2.0 → 0.3.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.
Files changed (79) hide show
  1. package/README.md +10 -4
  2. package/dist/components/ai-agent-plan/ai-agent-plan.d.ts +50 -0
  3. package/dist/components/ai-agent-plan/ai-agent-plan.d.ts.map +1 -0
  4. package/dist/components/ai-agent-plan/ai-agent-plan.js +156 -0
  5. package/dist/components/ai-agent-plan/ai-agent-plan.js.map +1 -0
  6. package/dist/components/ai-agent-plan/index.d.ts +2 -0
  7. package/dist/components/ai-agent-plan/index.js +2 -0
  8. package/dist/components/ai-agent-plan/meta.js +17 -0
  9. package/dist/components/ai-agent-plan/meta.js.map +1 -0
  10. package/dist/components/ai-agent-status/ai-agent-status.d.ts +1 -1
  11. package/dist/components/ai-approval-request/ai-approval-request.d.ts +67 -0
  12. package/dist/components/ai-approval-request/ai-approval-request.d.ts.map +1 -0
  13. package/dist/components/ai-approval-request/ai-approval-request.js +218 -0
  14. package/dist/components/ai-approval-request/ai-approval-request.js.map +1 -0
  15. package/dist/components/ai-approval-request/index.d.ts +2 -0
  16. package/dist/components/ai-approval-request/index.js +2 -0
  17. package/dist/components/ai-approval-request/meta.js +17 -0
  18. package/dist/components/ai-approval-request/meta.js.map +1 -0
  19. package/dist/components/ai-response/ai-response.d.ts.map +1 -1
  20. package/dist/components/ai-response/ai-response.js +1 -0
  21. package/dist/components/ai-response/ai-response.js.map +1 -1
  22. package/dist/components/ai-structured-output/ai-structured-output.d.ts.map +1 -1
  23. package/dist/components/ai-structured-output/ai-structured-output.js +13 -2
  24. package/dist/components/ai-structured-output/ai-structured-output.js.map +1 -1
  25. package/dist/components/ai-tool/ai-tool.d.ts +1 -1
  26. package/dist/components/alert/alert.d.ts +1 -1
  27. package/dist/components/badge/badge.d.ts +1 -1
  28. package/dist/components/button/button.d.ts +1 -1
  29. package/dist/components/file-upload/file-upload.d.ts +46 -0
  30. package/dist/components/file-upload/file-upload.d.ts.map +1 -0
  31. package/dist/components/file-upload/file-upload.js +204 -0
  32. package/dist/components/file-upload/file-upload.js.map +1 -0
  33. package/dist/components/file-upload/index.d.ts +3 -0
  34. package/dist/components/file-upload/index.js +3 -0
  35. package/dist/components/file-upload/meta.js +17 -0
  36. package/dist/components/file-upload/meta.js.map +1 -0
  37. package/dist/components/file-upload/upload-queue.d.ts +87 -0
  38. package/dist/components/file-upload/upload-queue.d.ts.map +1 -0
  39. package/dist/components/file-upload/upload-queue.js +249 -0
  40. package/dist/components/file-upload/upload-queue.js.map +1 -0
  41. package/dist/components/progress/progress.d.ts +1 -1
  42. package/dist/components/progress/progress.js +1 -0
  43. package/dist/components/progress/progress.js.map +1 -1
  44. package/dist/components/spinner/spinner.js +1 -0
  45. package/dist/components/spinner/spinner.js.map +1 -1
  46. package/dist/components/tags-input/index.d.ts +2 -0
  47. package/dist/components/tags-input/index.js +2 -0
  48. package/dist/components/tags-input/meta.js +17 -0
  49. package/dist/components/tags-input/meta.js.map +1 -0
  50. package/dist/components/tags-input/tags-input.d.ts +47 -0
  51. package/dist/components/tags-input/tags-input.d.ts.map +1 -0
  52. package/dist/components/tags-input/tags-input.js +182 -0
  53. package/dist/components/tags-input/tags-input.js.map +1 -0
  54. package/dist/components/toast/toast.d.ts +1 -1
  55. package/dist/index.d.ts +10 -1
  56. package/dist/index.js +6 -1
  57. package/dist/registry/components.d.ts.map +1 -1
  58. package/dist/registry/components.js +58 -50
  59. package/dist/registry/components.js.map +1 -1
  60. package/package.json +4 -4
  61. package/src/components/ai-agent-plan/ai-agent-plan.tsx +0 -0
  62. package/src/components/ai-agent-plan/index.ts +10 -0
  63. package/src/components/ai-agent-plan/meta.ts +19 -0
  64. package/src/components/ai-approval-request/ai-approval-request.tsx +368 -0
  65. package/src/components/ai-approval-request/index.ts +7 -0
  66. package/src/components/ai-approval-request/meta.ts +21 -0
  67. package/src/components/ai-response/ai-response.tsx +1 -0
  68. package/src/components/ai-structured-output/ai-structured-output.tsx +25 -1
  69. package/src/components/file-upload/file-upload.tsx +322 -0
  70. package/src/components/file-upload/index.ts +20 -0
  71. package/src/components/file-upload/meta.ts +21 -0
  72. package/src/components/file-upload/upload-queue.ts +337 -0
  73. package/src/components/progress/progress.tsx +3 -0
  74. package/src/components/spinner/spinner.tsx +5 -0
  75. package/src/components/tags-input/index.ts +1 -0
  76. package/src/components/tags-input/meta.ts +23 -0
  77. package/src/components/tags-input/tags-input.tsx +300 -0
  78. package/src/index.ts +4 -0
  79. package/src/registry/components.ts +8 -0
@@ -0,0 +1,87 @@
1
+ //#region src/components/file-upload/upload-queue.d.ts
2
+ /**
3
+ * The upload queue.
4
+ *
5
+ * The dropzone is the most duplicated component in the React ecosystem and the
6
+ * least valuable half: it is a styled rectangle over `<input type="file">`.
7
+ * What almost nobody ships is this — progress, cancel, retry with backoff, and
8
+ * a concurrency limit — so every team writes it again, usually twice, because
9
+ * the first version has no cancel and no retry.
10
+ *
11
+ * Transport is injected. This never calls `fetch` or constructs a request,
12
+ * because the request is the part that differs everywhere: presigned S3 PUT,
13
+ * multipart POST, tus, an internal gateway with its own auth. `upload` receives
14
+ * the file, a progress callback and an AbortSignal, and returns a promise. That
15
+ * is the whole contract.
16
+ *
17
+ * Progress needs XHR, not fetch. `fetch` still cannot report upload progress in
18
+ * any shipping browser — there is no readable stream for the request body — so
19
+ * a transport that wants a real progress bar has to use XMLHttpRequest. That is
20
+ * the consumer's choice to make, and `xhrUpload` below is a working example
21
+ * rather than a dependency.
22
+ */
23
+ type UploadStatus = "queued" | "uploading" | "done" | "failed" | "cancelled";
24
+ interface QueuedFile {
25
+ /** Stable across retries, so React keys and announcements do not jump. */
26
+ id: string;
27
+ file: File;
28
+ status: UploadStatus;
29
+ /** 0–1, or null when the transport cannot report it. */
30
+ progress: number | null;
31
+ /** Why it failed or was refused. Kept so it can be read and acted on. */
32
+ error?: string;
33
+ attempts: number;
34
+ }
35
+ interface UploadContext {
36
+ onProgress: (fraction: number) => void;
37
+ signal: AbortSignal;
38
+ }
39
+ type UploadFn = (file: File, context: UploadContext) => Promise<void>;
40
+ interface UploadQueueOptions {
41
+ upload: UploadFn;
42
+ /** Uploads running at once. More is not faster once the link is saturated. */
43
+ concurrency?: number;
44
+ /** Automatic retries per file before it is left failed. */
45
+ maxAttempts?: number;
46
+ /** Largest accepted file, in bytes. */
47
+ maxSize?: number;
48
+ /** Accepted types, as an `accept` attribute value: ".pdf,image/*". */
49
+ accept?: string;
50
+ /** Total files the queue will hold. */
51
+ maxFiles?: number;
52
+ onComplete?: (file: QueuedFile) => void;
53
+ }
54
+ /** Matches a file against an `accept` string the same way the browser does. */
55
+ declare function matchesAccept(file: File, accept: string | undefined): boolean;
56
+ declare function formatBytes(bytes: number): string;
57
+ declare function useUploadQueue(options: UploadQueueOptions): {
58
+ files: QueuedFile[];
59
+ stats: {
60
+ total: number;
61
+ queued: number;
62
+ uploading: number;
63
+ done: number;
64
+ failed: number;
65
+ cancelled: number;
66
+ active: number;
67
+ };
68
+ add: (incoming: File[]) => void;
69
+ cancel: (id: string) => void;
70
+ retry: (id: string) => void;
71
+ remove: (id: string) => void;
72
+ clearCompleted: () => void;
73
+ };
74
+ /**
75
+ * A working XHR transport, as an example rather than a dependency.
76
+ *
77
+ * XMLHttpRequest and not fetch, because fetch still cannot report upload
78
+ * progress: there is no readable stream for a request body in any shipping
79
+ * browser. Copy this and change the request to match your backend.
80
+ */
81
+ declare function xhrUpload(url: string, init?: {
82
+ method?: string;
83
+ headers?: Record<string, string>;
84
+ }): UploadFn;
85
+ //#endregion
86
+ export { QueuedFile, UploadContext, UploadFn, UploadQueueOptions, UploadStatus, formatBytes, matchesAccept, useUploadQueue, xhrUpload };
87
+ //# sourceMappingURL=upload-queue.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"upload-queue.d.ts","names":[],"sources":["../../../src/components/file-upload/upload-queue.ts"],"mappings":";;;;;;;;;;;;;;;;;;;;;;KA0BY;UAEK;;EAEf;EACA,MAAM;EACN,QAAQ;;EAER;;EAEA;EACA;;UAGe;EACf,aAAa;EACb,QAAQ;;KAGE,YAAY,MAAM,MAAM,SAAS,kBAAkB;UAE9C;EACf,QAAQ;;EAER;;EAEA;;EAEA;;EAEA;;EAEA;EACA,cAAc,MAAM;;;iBASN,cAAc,MAAM,MAAM;iBAkB1B,YAAY;iBAcZ,eAAe,SAAS;;;;;;;;;;;EA2GzB,MAAA,UAAA;EA4CmB,SAAA;EAID,QAAA;EAUC,SAAA;;;;;;;;;;iBAmClB,UACd,aACA;EAAQ;EAAiB,UAAU;IAClC"}
@@ -0,0 +1,249 @@
1
+ "use client";
2
+ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
3
+ //#region src/components/file-upload/upload-queue.ts
4
+ /** Backoff between attempts. Bounded, because a person is waiting. */
5
+ function backoffMs(attempt) {
6
+ return Math.min(8e3, 500 * 2 ** (attempt - 1));
7
+ }
8
+ /** Matches a file against an `accept` string the same way the browser does. */
9
+ function matchesAccept(file, accept) {
10
+ if (!accept) return true;
11
+ const patterns = accept.split(",").map((part) => part.trim().toLowerCase()).filter(Boolean);
12
+ if (patterns.length === 0) return true;
13
+ const type = file.type.toLowerCase();
14
+ const name = file.name.toLowerCase();
15
+ return patterns.some((pattern) => {
16
+ if (pattern.startsWith(".")) return name.endsWith(pattern);
17
+ if (pattern.endsWith("/*")) return type.startsWith(pattern.slice(0, -1));
18
+ return type === pattern;
19
+ });
20
+ }
21
+ function formatBytes(bytes) {
22
+ if (bytes < 1e3) return `${String(bytes)} B`;
23
+ const units = [
24
+ "kB",
25
+ "MB",
26
+ "GB",
27
+ "TB"
28
+ ];
29
+ let value = bytes / 1e3;
30
+ let unit = 0;
31
+ while (value >= 1e3 && unit < units.length - 1) {
32
+ value /= 1e3;
33
+ unit += 1;
34
+ }
35
+ return `${value.toFixed(value < 10 ? 1 : 0)} ${units[unit] ?? "B"}`;
36
+ }
37
+ let sequence = 0;
38
+ function useUploadQueue(options) {
39
+ const { upload, concurrency = 3, maxAttempts = 3, maxSize, accept, maxFiles, onComplete } = options;
40
+ const [files, setFiles] = useState([]);
41
+ const controllers = useRef(/* @__PURE__ */ new Map());
42
+ const running = useRef(/* @__PURE__ */ new Set());
43
+ const latest = useRef({
44
+ upload,
45
+ concurrency,
46
+ maxAttempts,
47
+ onComplete
48
+ });
49
+ useEffect(() => {
50
+ latest.current = {
51
+ upload,
52
+ concurrency,
53
+ maxAttempts,
54
+ onComplete
55
+ };
56
+ });
57
+ const patch = useCallback((id, changes) => {
58
+ setFiles((current) => current.map((entry) => entry.id === id ? {
59
+ ...entry,
60
+ ...changes
61
+ } : entry));
62
+ }, []);
63
+ const run = useCallback(async (entry) => {
64
+ const controller = new AbortController();
65
+ controllers.current.set(entry.id, controller);
66
+ const attempt = entry.attempts + 1;
67
+ patch(entry.id, {
68
+ status: "uploading",
69
+ progress: 0,
70
+ attempts: attempt,
71
+ error: void 0
72
+ });
73
+ try {
74
+ await latest.current.upload(entry.file, {
75
+ signal: controller.signal,
76
+ onProgress: (fraction) => {
77
+ patch(entry.id, { progress: Math.min(1, Math.max(0, fraction)) });
78
+ }
79
+ });
80
+ patch(entry.id, {
81
+ status: "done",
82
+ progress: 1
83
+ });
84
+ latest.current.onComplete?.({
85
+ ...entry,
86
+ status: "done",
87
+ progress: 1
88
+ });
89
+ } catch (error) {
90
+ if (controller.signal.aborted) patch(entry.id, {
91
+ status: "cancelled",
92
+ progress: null
93
+ });
94
+ else if (attempt < latest.current.maxAttempts) {
95
+ setTimeout(() => {
96
+ running.current.delete(entry.id);
97
+ controllers.current.delete(entry.id);
98
+ patch(entry.id, {
99
+ status: "queued",
100
+ progress: null
101
+ });
102
+ }, backoffMs(attempt));
103
+ return;
104
+ } else patch(entry.id, {
105
+ status: "failed",
106
+ progress: null,
107
+ error: error instanceof Error ? error.message : "Upload failed"
108
+ });
109
+ }
110
+ controllers.current.delete(entry.id);
111
+ running.current.delete(entry.id);
112
+ setFiles((current) => [...current]);
113
+ }, [patch]);
114
+ useEffect(() => {
115
+ const free = concurrency - running.current.size;
116
+ if (free <= 0) return;
117
+ const next = files.filter((entry) => entry.status === "queued" && !running.current.has(entry.id)).slice(0, free);
118
+ for (const entry of next) {
119
+ running.current.add(entry.id);
120
+ run(entry);
121
+ }
122
+ }, [
123
+ files,
124
+ concurrency,
125
+ run
126
+ ]);
127
+ /** Validates and enqueues. A rejected file is kept and told why. */
128
+ const add = useCallback((incoming) => {
129
+ setFiles((current) => {
130
+ const room = maxFiles === void 0 ? incoming.length : maxFiles - current.length;
131
+ const admitted = [];
132
+ for (const file of incoming.slice(0, Math.max(0, room))) {
133
+ sequence += 1;
134
+ const id = `upload-${String(sequence)}`;
135
+ if (maxSize !== void 0 && file.size > maxSize) {
136
+ admitted.push({
137
+ id,
138
+ file,
139
+ status: "failed",
140
+ progress: null,
141
+ attempts: 0,
142
+ error: `Larger than ${formatBytes(maxSize)}`
143
+ });
144
+ continue;
145
+ }
146
+ if (!matchesAccept(file, accept)) {
147
+ admitted.push({
148
+ id,
149
+ file,
150
+ status: "failed",
151
+ progress: null,
152
+ attempts: 0,
153
+ error: "Type not accepted"
154
+ });
155
+ continue;
156
+ }
157
+ admitted.push({
158
+ id,
159
+ file,
160
+ status: "queued",
161
+ progress: null,
162
+ attempts: 0
163
+ });
164
+ }
165
+ return [...current, ...admitted];
166
+ });
167
+ }, [
168
+ accept,
169
+ maxFiles,
170
+ maxSize
171
+ ]);
172
+ const cancel = useCallback((id) => {
173
+ controllers.current.get(id)?.abort();
174
+ }, []);
175
+ const retry = useCallback((id) => {
176
+ setFiles((current) => current.map((entry) => entry.id === id ? {
177
+ ...entry,
178
+ status: "queued",
179
+ error: void 0,
180
+ attempts: 0
181
+ } : entry));
182
+ }, []);
183
+ const remove = useCallback((id) => {
184
+ controllers.current.get(id)?.abort();
185
+ controllers.current.delete(id);
186
+ running.current.delete(id);
187
+ setFiles((current) => current.filter((entry) => entry.id !== id));
188
+ }, []);
189
+ const clearCompleted = useCallback(() => {
190
+ setFiles((current) => current.filter((entry) => entry.status !== "done"));
191
+ }, []);
192
+ return {
193
+ files,
194
+ stats: useMemo(() => {
195
+ const by = (status) => files.filter((entry) => entry.status === status).length;
196
+ return {
197
+ total: files.length,
198
+ queued: by("queued"),
199
+ uploading: by("uploading"),
200
+ done: by("done"),
201
+ failed: by("failed"),
202
+ cancelled: by("cancelled"),
203
+ active: by("queued") + by("uploading")
204
+ };
205
+ }, [files]),
206
+ add,
207
+ cancel,
208
+ retry,
209
+ remove,
210
+ clearCompleted
211
+ };
212
+ }
213
+ /**
214
+ * A working XHR transport, as an example rather than a dependency.
215
+ *
216
+ * XMLHttpRequest and not fetch, because fetch still cannot report upload
217
+ * progress: there is no readable stream for a request body in any shipping
218
+ * browser. Copy this and change the request to match your backend.
219
+ */
220
+ function xhrUpload(url, init = {}) {
221
+ return (file, { onProgress, signal }) => new Promise((resolve, reject) => {
222
+ const request = new XMLHttpRequest();
223
+ request.open(init.method ?? "POST", url);
224
+ for (const [header, value] of Object.entries(init.headers ?? {})) request.setRequestHeader(header, value);
225
+ request.upload.addEventListener("progress", (event) => {
226
+ if (event.lengthComputable) onProgress(event.loaded / event.total);
227
+ });
228
+ request.addEventListener("load", () => {
229
+ if (request.status >= 200 && request.status < 300) resolve();
230
+ else reject(/* @__PURE__ */ new Error(`Upload failed with status ${String(request.status)}`));
231
+ });
232
+ request.addEventListener("error", () => {
233
+ reject(/* @__PURE__ */ new Error("Network error"));
234
+ });
235
+ request.addEventListener("abort", () => {
236
+ reject(/* @__PURE__ */ new Error("Cancelled"));
237
+ });
238
+ signal.addEventListener("abort", () => {
239
+ request.abort();
240
+ });
241
+ const body = new FormData();
242
+ body.append("file", file);
243
+ request.send(body);
244
+ });
245
+ }
246
+ //#endregion
247
+ export { formatBytes, matchesAccept, useUploadQueue, xhrUpload };
248
+
249
+ //# sourceMappingURL=upload-queue.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"upload-queue.js","names":[],"sources":["../../../src/components/file-upload/upload-queue.ts"],"sourcesContent":["\"use client\";\n\nimport { useCallback, useEffect, useMemo, useRef, useState } from \"react\";\n\n/**\n * The upload queue.\n *\n * The dropzone is the most duplicated component in the React ecosystem and the\n * least valuable half: it is a styled rectangle over `<input type=\"file\">`.\n * What almost nobody ships is this — progress, cancel, retry with backoff, and\n * a concurrency limit — so every team writes it again, usually twice, because\n * the first version has no cancel and no retry.\n *\n * Transport is injected. This never calls `fetch` or constructs a request,\n * because the request is the part that differs everywhere: presigned S3 PUT,\n * multipart POST, tus, an internal gateway with its own auth. `upload` receives\n * the file, a progress callback and an AbortSignal, and returns a promise. That\n * is the whole contract.\n *\n * Progress needs XHR, not fetch. `fetch` still cannot report upload progress in\n * any shipping browser — there is no readable stream for the request body — so\n * a transport that wants a real progress bar has to use XMLHttpRequest. That is\n * the consumer's choice to make, and `xhrUpload` below is a working example\n * rather than a dependency.\n */\n\nexport type UploadStatus = \"queued\" | \"uploading\" | \"done\" | \"failed\" | \"cancelled\";\n\nexport interface QueuedFile {\n /** Stable across retries, so React keys and announcements do not jump. */\n id: string;\n file: File;\n status: UploadStatus;\n /** 0–1, or null when the transport cannot report it. */\n progress: number | null;\n /** Why it failed or was refused. Kept so it can be read and acted on. */\n error?: string;\n attempts: number;\n}\n\nexport interface UploadContext {\n onProgress: (fraction: number) => void;\n signal: AbortSignal;\n}\n\nexport type UploadFn = (file: File, context: UploadContext) => Promise<void>;\n\nexport interface UploadQueueOptions {\n upload: UploadFn;\n /** Uploads running at once. More is not faster once the link is saturated. */\n concurrency?: number;\n /** Automatic retries per file before it is left failed. */\n maxAttempts?: number;\n /** Largest accepted file, in bytes. */\n maxSize?: number;\n /** Accepted types, as an `accept` attribute value: \".pdf,image/*\". */\n accept?: string;\n /** Total files the queue will hold. */\n maxFiles?: number;\n onComplete?: (file: QueuedFile) => void;\n}\n\n/** Backoff between attempts. Bounded, because a person is waiting. */\nfunction backoffMs(attempt: number): number {\n return Math.min(8000, 500 * 2 ** (attempt - 1));\n}\n\n/** Matches a file against an `accept` string the same way the browser does. */\nexport function matchesAccept(file: File, accept: string | undefined): boolean {\n if (!accept) return true;\n const patterns = accept\n .split(\",\")\n .map((part) => part.trim().toLowerCase())\n .filter(Boolean);\n if (patterns.length === 0) return true;\n\n const type = file.type.toLowerCase();\n const name = file.name.toLowerCase();\n\n return patterns.some((pattern) => {\n if (pattern.startsWith(\".\")) return name.endsWith(pattern);\n if (pattern.endsWith(\"/*\")) return type.startsWith(pattern.slice(0, -1));\n return type === pattern;\n });\n}\n\nexport function formatBytes(bytes: number): string {\n if (bytes < 1000) return `${String(bytes)} B`;\n const units = [\"kB\", \"MB\", \"GB\", \"TB\"];\n let value = bytes / 1000;\n let unit = 0;\n while (value >= 1000 && unit < units.length - 1) {\n value /= 1000;\n unit += 1;\n }\n return `${value.toFixed(value < 10 ? 1 : 0)} ${units[unit] ?? \"B\"}`;\n}\n\nlet sequence = 0;\n\nexport function useUploadQueue(options: UploadQueueOptions) {\n const {\n upload,\n concurrency = 3,\n maxAttempts = 3,\n maxSize,\n accept,\n maxFiles,\n onComplete,\n } = options;\n\n const [files, setFiles] = useState<QueuedFile[]>([]);\n\n const controllers = useRef(new Map<string, AbortController>());\n const running = useRef(new Set<string>());\n\n // The live option values, so an in-flight upload reads the current transport\n // without `run` being re-created — which would churn the scheduling effect\n // every time a consumer passes an inline `upload`. Written after commit\n // rather than during render, because a ref write during render is unsafe\n // under concurrent rendering; `run` only reads this once it is executing,\n // which is always after the effect has run.\n const latest = useRef({ upload, concurrency, maxAttempts, onComplete });\n useEffect(() => {\n latest.current = { upload, concurrency, maxAttempts, onComplete };\n });\n\n const patch = useCallback((id: string, changes: Partial<QueuedFile>) => {\n setFiles((current) =>\n current.map((entry) => (entry.id === id ? { ...entry, ...changes } : entry)),\n );\n }, []);\n\n const run = useCallback(\n async (entry: QueuedFile) => {\n const controller = new AbortController();\n controllers.current.set(entry.id, controller);\n\n const attempt = entry.attempts + 1;\n patch(entry.id, {\n status: \"uploading\",\n progress: 0,\n attempts: attempt,\n error: undefined,\n });\n\n try {\n await latest.current.upload(entry.file, {\n signal: controller.signal,\n onProgress: (fraction) => {\n patch(entry.id, { progress: Math.min(1, Math.max(0, fraction)) });\n },\n });\n\n patch(entry.id, { status: \"done\", progress: 1 });\n latest.current.onComplete?.({ ...entry, status: \"done\", progress: 1 });\n } catch (error) {\n if (controller.signal.aborted) {\n patch(entry.id, { status: \"cancelled\", progress: null });\n } else if (attempt < latest.current.maxAttempts) {\n // Back off, then requeue. Releasing the slot only after the delay is\n // what makes the wait real rather than a busy retry, and the file\n // re-enters the queue like any other so retries obey concurrency too.\n setTimeout(() => {\n running.current.delete(entry.id);\n controllers.current.delete(entry.id);\n patch(entry.id, { status: \"queued\", progress: null });\n }, backoffMs(attempt));\n return;\n } else {\n patch(entry.id, {\n status: \"failed\",\n progress: null,\n error: error instanceof Error ? error.message : \"Upload failed\",\n });\n }\n }\n\n controllers.current.delete(entry.id);\n running.current.delete(entry.id);\n // No explicit pump: the scheduling effect below reacts to the state\n // change and starts whatever can start next.\n setFiles((current) => [...current]);\n },\n [patch],\n );\n\n // Starts whatever can start, once the state that made it startable has been\n // committed. Doing this inside a setState updater — the obvious shortcut —\n // means React may run it twice and upload the same file twice; the `running`\n // set guards that, but the effect is the honest place for it.\n useEffect(() => {\n const free = concurrency - running.current.size;\n if (free <= 0) return;\n\n const next = files\n .filter((entry) => entry.status === \"queued\" && !running.current.has(entry.id))\n .slice(0, free);\n\n for (const entry of next) {\n running.current.add(entry.id);\n void run(entry);\n }\n }, [files, concurrency, run]);\n\n /** Validates and enqueues. A rejected file is kept and told why. */\n const add = useCallback(\n (incoming: File[]) => {\n setFiles((current) => {\n const room = maxFiles === undefined ? incoming.length : maxFiles - current.length;\n const admitted: QueuedFile[] = [];\n\n for (const file of incoming.slice(0, Math.max(0, room))) {\n sequence += 1;\n const id = `upload-${String(sequence)}`;\n\n // A rejected file becomes a failed entry rather than disappearing.\n // Silently dropping a file the reader chose is how these components\n // lose work without anybody noticing.\n if (maxSize !== undefined && file.size > maxSize) {\n admitted.push({\n id,\n file,\n status: \"failed\",\n progress: null,\n attempts: 0,\n error: `Larger than ${formatBytes(maxSize)}`,\n });\n continue;\n }\n if (!matchesAccept(file, accept)) {\n admitted.push({\n id,\n file,\n status: \"failed\",\n progress: null,\n attempts: 0,\n error: \"Type not accepted\",\n });\n continue;\n }\n\n admitted.push({ id, file, status: \"queued\", progress: null, attempts: 0 });\n }\n\n return [...current, ...admitted];\n });\n },\n [accept, maxFiles, maxSize],\n );\n\n const cancel = useCallback((id: string) => {\n controllers.current.get(id)?.abort();\n }, []);\n\n const retry = useCallback((id: string) => {\n setFiles((current) =>\n current.map((entry) =>\n entry.id === id\n ? { ...entry, status: \"queued\" as const, error: undefined, attempts: 0 }\n : entry,\n ),\n );\n }, []);\n\n const remove = useCallback((id: string) => {\n controllers.current.get(id)?.abort();\n controllers.current.delete(id);\n running.current.delete(id);\n setFiles((current) => current.filter((entry) => entry.id !== id));\n }, []);\n\n const clearCompleted = useCallback(() => {\n setFiles((current) => current.filter((entry) => entry.status !== \"done\"));\n }, []);\n\n const stats = useMemo(() => {\n const by = (status: UploadStatus) =>\n files.filter((entry) => entry.status === status).length;\n return {\n total: files.length,\n queued: by(\"queued\"),\n uploading: by(\"uploading\"),\n done: by(\"done\"),\n failed: by(\"failed\"),\n cancelled: by(\"cancelled\"),\n active: by(\"queued\") + by(\"uploading\"),\n };\n }, [files]);\n\n return { files, stats, add, cancel, retry, remove, clearCompleted };\n}\n\n/**\n * A working XHR transport, as an example rather than a dependency.\n *\n * XMLHttpRequest and not fetch, because fetch still cannot report upload\n * progress: there is no readable stream for a request body in any shipping\n * browser. Copy this and change the request to match your backend.\n */\nexport function xhrUpload(\n url: string,\n init: { method?: string; headers?: Record<string, string> } = {},\n): UploadFn {\n return (file, { onProgress, signal }) =>\n new Promise<void>((resolve, reject) => {\n const request = new XMLHttpRequest();\n request.open(init.method ?? \"POST\", url);\n\n for (const [header, value] of Object.entries(init.headers ?? {})) {\n request.setRequestHeader(header, value);\n }\n\n request.upload.addEventListener(\"progress\", (event) => {\n if (event.lengthComputable) onProgress(event.loaded / event.total);\n });\n\n request.addEventListener(\"load\", () => {\n if (request.status >= 200 && request.status < 300) resolve();\n else reject(new Error(`Upload failed with status ${String(request.status)}`));\n });\n request.addEventListener(\"error\", () => {\n reject(new Error(\"Network error\"));\n });\n request.addEventListener(\"abort\", () => {\n reject(new Error(\"Cancelled\"));\n });\n\n signal.addEventListener(\"abort\", () => {\n request.abort();\n });\n\n const body = new FormData();\n body.append(\"file\", file);\n request.send(body);\n });\n}\n"],"mappings":"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"}
@@ -4,7 +4,7 @@ import { ComponentPropsWithRef } from "react";
4
4
  //#region src/components/progress/progress.d.ts
5
5
  declare const progressVariants: (props?: ({
6
6
  size?: "sm" | "md" | "lg" | null | undefined;
7
- tone?: "primary" | "destructive" | "success" | "warning" | null | undefined;
7
+ tone?: "destructive" | "success" | "warning" | "primary" | null | undefined;
8
8
  } & import("class-variance-authority/types").ClassProp) | undefined) => string;
9
9
  interface ProgressProps extends ComponentPropsWithRef<typeof Progress.Root>, VariantProps<typeof progressVariants> {}
10
10
  /**
@@ -44,6 +44,7 @@ function Progress$1({ className, size, tone, value, ...props }) {
44
44
  ...props,
45
45
  children: /* @__PURE__ */ jsx(Progress.Indicator, {
46
46
  "data-slot": "progress-indicator",
47
+ "data-motion": indeterminate ? "indicator" : void 0,
47
48
  className: cn("h-full w-full flex-1 rounded-full bg-[var(--progress-fill)]", "transition-transform duration-[var(--duration-slow)] ease-[var(--ease-out-quint)]", indeterminate && "animate-pulse-soft"),
48
49
  style: indeterminate ? void 0 : { transform: `translateX(-${String(100 - percent)}%)` }
49
50
  })
@@ -1 +1 @@
1
- {"version":3,"file":"progress.js","names":["Progress","ProgressPrimitive"],"sources":["../../../src/components/progress/progress.tsx"],"sourcesContent":["\"use client\";\n\nimport { cva, type VariantProps } from \"class-variance-authority\";\nimport { Progress as ProgressPrimitive } from \"radix-ui\";\nimport type { ComponentPropsWithRef } from \"react\";\n\nimport { cn } from \"@/lib/utils\";\n\nconst progressVariants = cva(\"relative w-full overflow-hidden rounded-full bg-muted\", {\n variants: {\n size: {\n sm: \"h-1\",\n md: \"h-2\",\n lg: \"h-3\",\n },\n tone: {\n primary: \"[--progress-fill:var(--color-primary)]\",\n success: \"[--progress-fill:var(--color-success)]\",\n warning: \"[--progress-fill:var(--color-warning)]\",\n destructive: \"[--progress-fill:var(--color-destructive)]\",\n },\n },\n defaultVariants: {\n size: \"md\",\n tone: \"primary\",\n },\n});\n\nexport interface ProgressProps\n extends\n ComponentPropsWithRef<typeof ProgressPrimitive.Root>,\n VariantProps<typeof progressVariants> {}\n\n/**\n * Shows how far along a task is.\n *\n * Pass `value={null}` for work whose duration is unknown — that is a genuinely\n * different state from zero percent, and it is announced as such rather than as\n * \"no progress\". For a short wait with no measurable progress, a Spinner says\n * the same thing with less furniture.\n */\nexport function Progress({ className, size, tone, value, ...props }: ProgressProps) {\n const indeterminate = value === null || value === undefined;\n const percent = indeterminate ? 0 : Math.min(100, Math.max(0, value));\n\n return (\n <ProgressPrimitive.Root\n data-slot=\"progress\"\n value={value}\n className={cn(progressVariants({ size, tone }), className)}\n {...props}\n >\n <ProgressPrimitive.Indicator\n data-slot=\"progress-indicator\"\n className={cn(\n \"h-full w-full flex-1 rounded-full bg-[var(--progress-fill)]\",\n \"transition-transform duration-[var(--duration-slow)] ease-[var(--ease-out-quint)]\",\n // With no known value the bar sweeps instead of filling, so it reads\n // as \"working\" rather than as a stalled 0%.\n indeterminate && \"animate-pulse-soft\",\n )}\n style={\n indeterminate ? undefined : { transform: `translateX(-${String(100 - percent)}%)` }\n }\n />\n </ProgressPrimitive.Root>\n );\n}\n\nexport { progressVariants };\n"],"mappings":";;;;;;AAQA,MAAM,mBAAmB,IAAI,yDAAyD;CACpF,UAAU;EACR,MAAM;GACJ,IAAI;GACJ,IAAI;GACJ,IAAI;EACN;EACA,MAAM;GACJ,SAAS;GACT,SAAS;GACT,SAAS;GACT,aAAa;EACf;CACF;CACA,iBAAiB;EACf,MAAM;EACN,MAAM;CACR;AACF,CAAC;;;;;;;;;AAeD,SAAgBA,WAAS,EAAE,WAAW,MAAM,MAAM,OAAO,GAAG,SAAwB;CAClF,MAAM,gBAAgB,UAAU,QAAQ,UAAU,KAAA;CAClD,MAAM,UAAU,gBAAgB,IAAI,KAAK,IAAI,KAAK,KAAK,IAAI,GAAG,KAAK,CAAC;CAEpE,OACE,oBAACC,SAAkB,MAAnB;EACE,aAAU;EACH;EACP,WAAW,GAAG,iBAAiB;GAAE;GAAM;EAAK,CAAC,GAAG,SAAS;EACzD,GAAI;EAEJ,UAAA,oBAACA,SAAkB,WAAnB;GACE,aAAU;GACV,WAAW,GACT,+DACA,qFAGA,iBAAiB,oBACnB;GACA,OACE,gBAAgB,KAAA,IAAY,EAAE,WAAW,eAAe,OAAO,MAAM,OAAO,EAAE,IAAI;EAErF,CAAA;CACqB,CAAA;AAE5B"}
1
+ {"version":3,"file":"progress.js","names":["Progress","ProgressPrimitive"],"sources":["../../../src/components/progress/progress.tsx"],"sourcesContent":["\"use client\";\n\nimport { cva, type VariantProps } from \"class-variance-authority\";\nimport { Progress as ProgressPrimitive } from \"radix-ui\";\nimport type { ComponentPropsWithRef } from \"react\";\n\nimport { cn } from \"@/lib/utils\";\n\nconst progressVariants = cva(\"relative w-full overflow-hidden rounded-full bg-muted\", {\n variants: {\n size: {\n sm: \"h-1\",\n md: \"h-2\",\n lg: \"h-3\",\n },\n tone: {\n primary: \"[--progress-fill:var(--color-primary)]\",\n success: \"[--progress-fill:var(--color-success)]\",\n warning: \"[--progress-fill:var(--color-warning)]\",\n destructive: \"[--progress-fill:var(--color-destructive)]\",\n },\n },\n defaultVariants: {\n size: \"md\",\n tone: \"primary\",\n },\n});\n\nexport interface ProgressProps\n extends\n ComponentPropsWithRef<typeof ProgressPrimitive.Root>,\n VariantProps<typeof progressVariants> {}\n\n/**\n * Shows how far along a task is.\n *\n * Pass `value={null}` for work whose duration is unknown — that is a genuinely\n * different state from zero percent, and it is announced as such rather than as\n * \"no progress\". For a short wait with no measurable progress, a Spinner says\n * the same thing with less furniture.\n */\nexport function Progress({ className, size, tone, value, ...props }: ProgressProps) {\n const indeterminate = value === null || value === undefined;\n const percent = indeterminate ? 0 : Math.min(100, Math.max(0, value));\n\n return (\n <ProgressPrimitive.Root\n data-slot=\"progress\"\n value={value}\n className={cn(progressVariants({ size, tone }), className)}\n {...props}\n >\n <ProgressPrimitive.Indicator\n data-slot=\"progress-indicator\"\n // Only while indeterminate: the sweep is the signal that work is\n // ongoing, and freezing it reads as a stalled zero percent.\n data-motion={indeterminate ? \"indicator\" : undefined}\n className={cn(\n \"h-full w-full flex-1 rounded-full bg-[var(--progress-fill)]\",\n \"transition-transform duration-[var(--duration-slow)] ease-[var(--ease-out-quint)]\",\n // With no known value the bar sweeps instead of filling, so it reads\n // as \"working\" rather than as a stalled 0%.\n indeterminate && \"animate-pulse-soft\",\n )}\n style={\n indeterminate ? undefined : { transform: `translateX(-${String(100 - percent)}%)` }\n }\n />\n </ProgressPrimitive.Root>\n );\n}\n\nexport { progressVariants };\n"],"mappings":";;;;;;AAQA,MAAM,mBAAmB,IAAI,yDAAyD;CACpF,UAAU;EACR,MAAM;GACJ,IAAI;GACJ,IAAI;GACJ,IAAI;EACN;EACA,MAAM;GACJ,SAAS;GACT,SAAS;GACT,SAAS;GACT,aAAa;EACf;CACF;CACA,iBAAiB;EACf,MAAM;EACN,MAAM;CACR;AACF,CAAC;;;;;;;;;AAeD,SAAgBA,WAAS,EAAE,WAAW,MAAM,MAAM,OAAO,GAAG,SAAwB;CAClF,MAAM,gBAAgB,UAAU,QAAQ,UAAU,KAAA;CAClD,MAAM,UAAU,gBAAgB,IAAI,KAAK,IAAI,KAAK,KAAK,IAAI,GAAG,KAAK,CAAC;CAEpE,OACE,oBAACC,SAAkB,MAAnB;EACE,aAAU;EACH;EACP,WAAW,GAAG,iBAAiB;GAAE;GAAM;EAAK,CAAC,GAAG,SAAS;EACzD,GAAI;EAEJ,UAAA,oBAACA,SAAkB,WAAnB;GACE,aAAU;GAGV,eAAa,gBAAgB,cAAc,KAAA;GAC3C,WAAW,GACT,+DACA,qFAGA,iBAAiB,oBACnB;GACA,OACE,gBAAgB,KAAA,IAAY,EAAE,WAAW,eAAe,OAAO,MAAM,OAAO,EAAE,IAAI;EAErF,CAAA;CACqB,CAAA;AAE5B"}
@@ -18,6 +18,7 @@ function Spinner({ className, size, label, ...props }) {
18
18
  viewBox: "0 0 24 24",
19
19
  fill: "none",
20
20
  "aria-hidden": "true",
21
+ "data-motion": "indicator",
21
22
  className: cn(spinnerVariants({ size }), className),
22
23
  ...props,
23
24
  children: [/* @__PURE__ */ jsx("circle", {
@@ -1 +1 @@
1
- {"version":3,"file":"spinner.js","names":[],"sources":["../../../src/components/spinner/spinner.tsx"],"sourcesContent":["import { cva, type VariantProps } from \"class-variance-authority\";\nimport type { ComponentPropsWithRef } from \"react\";\n\nimport { cn } from \"@/lib/utils\";\n\nconst spinnerVariants = cva(\"animate-spin text-current\", {\n variants: {\n size: {\n xs: \"size-3\",\n sm: \"size-3.5\",\n md: \"size-4\",\n lg: \"size-5\",\n xl: \"size-6\",\n },\n },\n defaultVariants: {\n size: \"md\",\n },\n});\n\nexport interface SpinnerProps\n extends Omit<ComponentPropsWithRef<\"svg\">, \"children\">, VariantProps<typeof spinnerVariants> {\n /**\n * Announced to assistive technology while the spinner is visible.\n *\n * Omit it when the spinner sits inside a control that already communicates\n * its busy state (a loading Button, for example) — announcing twice is worse\n * than not announcing at all.\n */\n label?: string;\n}\n\n/** Indeterminate loading indicator. */\nexport function Spinner({ className, size, label, ...props }: SpinnerProps) {\n return (\n <>\n <svg\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n aria-hidden=\"true\"\n className={cn(spinnerVariants({ size }), className)}\n {...props}\n >\n <circle\n cx=\"12\"\n cy=\"12\"\n r=\"9.5\"\n stroke=\"currentColor\"\n strokeWidth=\"2.5\"\n opacity=\"0.22\"\n />\n <path\n d=\"M12 2.5a9.5 9.5 0 0 1 9.5 9.5\"\n stroke=\"currentColor\"\n strokeWidth=\"2.5\"\n strokeLinecap=\"round\"\n />\n </svg>\n {label ? (\n <span role=\"status\" className=\"sr-only\">\n {label}\n </span>\n ) : null}\n </>\n );\n}\n\nexport { spinnerVariants };\n"],"mappings":";;;;AAKA,MAAM,kBAAkB,IAAI,6BAA6B;CACvD,UAAU,EACR,MAAM;EACJ,IAAI;EACJ,IAAI;EACJ,IAAI;EACJ,IAAI;EACJ,IAAI;CACN,EACF;CACA,iBAAiB,EACf,MAAM,KACR;AACF,CAAC;;AAeD,SAAgB,QAAQ,EAAE,WAAW,MAAM,OAAO,GAAG,SAAuB;CAC1E,OACE,qBAAA,UAAA,EAAA,UAAA,CACE,qBAAC,OAAD;EACE,SAAQ;EACR,MAAK;EACL,eAAY;EACZ,WAAW,GAAG,gBAAgB,EAAE,KAAK,CAAC,GAAG,SAAS;EAClD,GAAI;EALN,UAAA,CAOE,oBAAC,UAAD;GACE,IAAG;GACH,IAAG;GACH,GAAE;GACF,QAAO;GACP,aAAY;GACZ,SAAQ;EACT,CAAA,GACD,oBAAC,QAAD;GACE,GAAE;GACF,QAAO;GACP,aAAY;GACZ,eAAc;EACf,CAAA,CACE;CACJ,CAAA,GAAA,QACC,oBAAC,QAAD;EAAM,MAAK;EAAS,WAAU;EAC3B,UAAA;CACG,CAAA,IACJ,IACJ,EAAA,CAAA;AAEN"}
1
+ {"version":3,"file":"spinner.js","names":[],"sources":["../../../src/components/spinner/spinner.tsx"],"sourcesContent":["import { cva, type VariantProps } from \"class-variance-authority\";\nimport type { ComponentPropsWithRef } from \"react\";\n\nimport { cn } from \"@/lib/utils\";\n\nconst spinnerVariants = cva(\"animate-spin text-current\", {\n variants: {\n size: {\n xs: \"size-3\",\n sm: \"size-3.5\",\n md: \"size-4\",\n lg: \"size-5\",\n xl: \"size-6\",\n },\n },\n defaultVariants: {\n size: \"md\",\n },\n});\n\nexport interface SpinnerProps\n extends Omit<ComponentPropsWithRef<\"svg\">, \"children\">, VariantProps<typeof spinnerVariants> {\n /**\n * Announced to assistive technology while the spinner is visible.\n *\n * Omit it when the spinner sits inside a control that already communicates\n * its busy state (a loading Button, for example) — announcing twice is worse\n * than not announcing at all.\n */\n label?: string;\n}\n\n/** Indeterminate loading indicator. */\nexport function Spinner({ className, size, label, ...props }: SpinnerProps) {\n return (\n <>\n <svg\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n aria-hidden=\"true\"\n // Exempt from the reduced-motion blanket: this is not decoration. A\n // spinner that stops turning reports that the application has hung,\n // which is a worse experience than the motion it was avoiding. It is\n // slowed instead — see --motion-scale-indicator.\n data-motion=\"indicator\"\n className={cn(spinnerVariants({ size }), className)}\n {...props}\n >\n <circle\n cx=\"12\"\n cy=\"12\"\n r=\"9.5\"\n stroke=\"currentColor\"\n strokeWidth=\"2.5\"\n opacity=\"0.22\"\n />\n <path\n d=\"M12 2.5a9.5 9.5 0 0 1 9.5 9.5\"\n stroke=\"currentColor\"\n strokeWidth=\"2.5\"\n strokeLinecap=\"round\"\n />\n </svg>\n {label ? (\n <span role=\"status\" className=\"sr-only\">\n {label}\n </span>\n ) : null}\n </>\n );\n}\n\nexport { spinnerVariants };\n"],"mappings":";;;;AAKA,MAAM,kBAAkB,IAAI,6BAA6B;CACvD,UAAU,EACR,MAAM;EACJ,IAAI;EACJ,IAAI;EACJ,IAAI;EACJ,IAAI;EACJ,IAAI;CACN,EACF;CACA,iBAAiB,EACf,MAAM,KACR;AACF,CAAC;;AAeD,SAAgB,QAAQ,EAAE,WAAW,MAAM,OAAO,GAAG,SAAuB;CAC1E,OACE,qBAAA,UAAA,EAAA,UAAA,CACE,qBAAC,OAAD;EACE,SAAQ;EACR,MAAK;EACL,eAAY;EAKZ,eAAY;EACZ,WAAW,GAAG,gBAAgB,EAAE,KAAK,CAAC,GAAG,SAAS;EAClD,GAAI;EAVN,UAAA,CAYE,oBAAC,UAAD;GACE,IAAG;GACH,IAAG;GACH,GAAE;GACF,QAAO;GACP,aAAY;GACZ,SAAQ;EACT,CAAA,GACD,oBAAC,QAAD;GACE,GAAE;GACF,QAAO;GACP,aAAY;GACZ,eAAc;EACf,CAAA,CACE;CACJ,CAAA,GAAA,QACC,oBAAC,QAAD;EAAM,MAAK;EAAS,WAAU;EAC3B,UAAA;CACG,CAAA,IACJ,IACJ,EAAA,CAAA;AAEN"}
@@ -0,0 +1,2 @@
1
+ import { TagValidator, TagsInput, TagsInputProps } from "./tags-input.js";
2
+ export { type TagValidator, TagsInput, type TagsInputProps };
@@ -0,0 +1,2 @@
1
+ import { TagsInput } from "./tags-input.js";
2
+ export { TagsInput };
@@ -0,0 +1,17 @@
1
+ import { defineMeta } from "../../registry/schema.js";
2
+ //#region src/components/tags-input/meta.ts
3
+ const meta = defineMeta({
4
+ name: "tags-input",
5
+ title: "Tags Input",
6
+ description: "A list of short values, where invalid entries stay visible instead of vanishing.",
7
+ category: "form",
8
+ status: "stable",
9
+ dependencies: [],
10
+ registryDependencies: [],
11
+ files: ["tags-input.tsx"],
12
+ a11y: "No WAI-ARIA pattern covers a token field, and the near misses misdescribe it — these are committed values, not options being chosen, so calling the container a listbox would promise navigation that does not exist. It is a labelled group holding a list, with a remove button per token carrying the tag in its accessible name. An invalid token's reason is inside the token, so it is announced with it rather than as a detached error. Additions, removals and refusals all reach a polite live region, because a refusal that looks like nothing happening is the failure this component exists to prevent. Remove buttons sit in the natural tab order rather than behind a roving tabindex: it costs a tab stop per token, and buys a pattern every assistive technology already understands."
13
+ });
14
+ //#endregion
15
+ export { meta };
16
+
17
+ //# sourceMappingURL=meta.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"meta.js","names":[],"sources":["../../../src/components/tags-input/meta.ts"],"sourcesContent":["import { defineMeta } from \"@/registry/schema\";\n\nexport const meta = defineMeta({\n name: \"tags-input\",\n title: \"Tags Input\",\n description:\n \"A list of short values, where invalid entries stay visible instead of vanishing.\",\n category: \"form\",\n status: \"stable\",\n dependencies: [],\n registryDependencies: [],\n files: [\"tags-input.tsx\"],\n a11y:\n \"No WAI-ARIA pattern covers a token field, and the near misses misdescribe it — these are \" +\n \"committed values, not options being chosen, so calling the container a listbox would promise \" +\n \"navigation that does not exist. It is a labelled group holding a list, with a remove button \" +\n \"per token carrying the tag in its accessible name. An invalid token's reason is inside the \" +\n \"token, so it is announced with it rather than as a detached error. Additions, removals and \" +\n \"refusals all reach a polite live region, because a refusal that looks like nothing happening \" +\n \"is the failure this component exists to prevent. Remove buttons sit in the natural tab order \" +\n \"rather than behind a roving tabindex: it costs a tab stop per token, and buys a pattern every \" +\n \"assistive technology already understands.\",\n});\n"],"mappings":";;AAEA,MAAa,OAAO,WAAW;CAC7B,MAAM;CACN,OAAO;CACP,aACE;CACF,UAAU;CACV,QAAQ;CACR,cAAc,CAAC;CACf,sBAAsB,CAAC;CACvB,OAAO,CAAC,gBAAgB;CACxB,MACE;AASJ,CAAC"}
@@ -0,0 +1,47 @@
1
+ import { ComponentPropsWithRef } from "react";
2
+ //#region src/components/tags-input/tags-input.d.ts
3
+ /**
4
+ * A field whose value is a list of short strings: invited emails, allowed
5
+ * domains, labels, stop sequences.
6
+ *
7
+ * The behaviour that makes this worth shipping is what happens to input that
8
+ * fails validation. Every implementation surveyed either refuses to create the
9
+ * token, or creates it and silently throws it away — and both leave the reader
10
+ * staring at a field that did not do what they asked, with nothing to correct.
11
+ * Here an invalid entry becomes a token like any other, marked invalid and
12
+ * carrying its reason, so it can be seen, read out, and fixed by removing it.
13
+ * `value` holds every token including the invalid ones; the consumer runs the
14
+ * same `validate` to decide whether the field is submittable.
15
+ *
16
+ * Deliberately no suggestion list. A token field with an anchored listbox is a
17
+ * multi-select combobox, and this library already has 544 lines of hand-rolled
18
+ * combobox ARIA. A second copy of that would be the expensive kind of
19
+ * duplication — the kind that drifts. When multi-select is wanted, it belongs in
20
+ * Combobox.
21
+ */
22
+ /** Returns `true` for a valid tag, or a reason it is not. */
23
+ type TagValidator = (tag: string, existing: string[]) => true | string;
24
+ interface TagsInputProps extends Omit<ComponentPropsWithRef<"div">, "onChange" | "children"> {
25
+ value: string[];
26
+ onValueChange: (tags: string[]) => void;
27
+ /** Names the field. Required — an unlabelled list of tokens is unreadable. */
28
+ label: string;
29
+ /** Characters that commit the token being typed, besides Enter. */
30
+ delimiters?: string[];
31
+ /**
32
+ * Decides whether a tag is acceptable. An invalid tag is still added, marked,
33
+ * and given its reason, rather than being refused or dropped.
34
+ */
35
+ validate?: TagValidator;
36
+ /** Rejects a tag already present. Rejections are announced, never silent. */
37
+ allowDuplicates?: boolean;
38
+ /** Refuses further tags once reached, and says so. */
39
+ max?: number;
40
+ placeholder?: string;
41
+ disabled?: boolean;
42
+ inputProps?: Omit<ComponentPropsWithRef<"input">, "value" | "onChange" | "disabled">;
43
+ }
44
+ declare function TagsInput({ className, value, onValueChange, label, delimiters, validate, allowDuplicates, max, placeholder, disabled, inputProps, ...props }: TagsInputProps): import("react").JSX.Element;
45
+ //#endregion
46
+ export { TagValidator, TagsInput, TagsInputProps };
47
+ //# sourceMappingURL=tags-input.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tags-input.d.ts","names":[],"sources":["../../../src/components/tags-input/tags-input.tsx"],"mappings":";;;;;;;;;;;;;;;;;;;;;;KAmCY,gBAAgB,aAAa;UAExB,uBAAuB,KACtC;EAGA;EACA,gBAAgB;;EAEhB;;EAEA;;;;;EAKA,WAAW;;EAEX;;EAEA;EACA;EACA;EACA,aAAa,KAAK;;iBAaJ,YACd,WACA,OACA,eACA,OACA,YACA,UACA,iBACA,KACA,aACA,UACA,eACG,SACF,iCAAc,IAAA"}