@dowel-ui/react 0.2.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +14 -7
- package/dist/components/ai-agent-plan/ai-agent-plan.d.ts +50 -0
- package/dist/components/ai-agent-plan/ai-agent-plan.d.ts.map +1 -0
- package/dist/components/ai-agent-plan/ai-agent-plan.js +156 -0
- package/dist/components/ai-agent-plan/ai-agent-plan.js.map +1 -0
- package/dist/components/ai-agent-plan/index.d.ts +2 -0
- package/dist/components/ai-agent-plan/index.js +2 -0
- package/dist/components/ai-agent-plan/meta.js +17 -0
- package/dist/components/ai-agent-plan/meta.js.map +1 -0
- package/dist/components/ai-agent-status/ai-agent-status.d.ts +1 -1
- package/dist/components/ai-approval-request/ai-approval-request.d.ts +67 -0
- package/dist/components/ai-approval-request/ai-approval-request.d.ts.map +1 -0
- package/dist/components/ai-approval-request/ai-approval-request.js +218 -0
- package/dist/components/ai-approval-request/ai-approval-request.js.map +1 -0
- package/dist/components/ai-approval-request/index.d.ts +2 -0
- package/dist/components/ai-approval-request/index.js +2 -0
- package/dist/components/ai-approval-request/meta.js +17 -0
- package/dist/components/ai-approval-request/meta.js.map +1 -0
- package/dist/components/ai-disclosure/ai-disclosure.d.ts +92 -0
- package/dist/components/ai-disclosure/ai-disclosure.d.ts.map +1 -0
- package/dist/components/ai-disclosure/ai-disclosure.js +122 -0
- package/dist/components/ai-disclosure/ai-disclosure.js.map +1 -0
- package/dist/components/ai-disclosure/index.d.ts +2 -0
- package/dist/components/ai-disclosure/index.js +2 -0
- package/dist/components/ai-disclosure/meta.js +17 -0
- package/dist/components/ai-disclosure/meta.js.map +1 -0
- package/dist/components/ai-response/ai-response.d.ts.map +1 -1
- package/dist/components/ai-response/ai-response.js +1 -0
- package/dist/components/ai-response/ai-response.js.map +1 -1
- package/dist/components/ai-structured-output/ai-structured-output.d.ts.map +1 -1
- package/dist/components/ai-structured-output/ai-structured-output.js +13 -2
- package/dist/components/ai-structured-output/ai-structured-output.js.map +1 -1
- package/dist/components/ai-tool/ai-tool.d.ts +1 -1
- package/dist/components/alert/alert.d.ts +1 -1
- package/dist/components/avatar/avatar.d.ts +1 -1
- package/dist/components/badge/badge.d.ts +1 -1
- package/dist/components/button/button.d.ts +1 -1
- package/dist/components/date-picker/date-picker.js +1 -1
- package/dist/components/diff-viewer/diff-model.d.ts +69 -0
- package/dist/components/diff-viewer/diff-model.d.ts.map +1 -0
- package/dist/components/diff-viewer/diff-model.js +180 -0
- package/dist/components/diff-viewer/diff-model.js.map +1 -0
- package/dist/components/diff-viewer/diff-viewer.d.ts +42 -0
- package/dist/components/diff-viewer/diff-viewer.d.ts.map +1 -0
- package/dist/components/diff-viewer/diff-viewer.js +212 -0
- package/dist/components/diff-viewer/diff-viewer.js.map +1 -0
- package/dist/components/diff-viewer/index.d.ts +3 -0
- package/dist/components/diff-viewer/index.js +3 -0
- package/dist/components/diff-viewer/meta.js +17 -0
- package/dist/components/diff-viewer/meta.js.map +1 -0
- package/dist/components/file-upload/file-upload.d.ts +46 -0
- package/dist/components/file-upload/file-upload.d.ts.map +1 -0
- package/dist/components/file-upload/file-upload.js +204 -0
- package/dist/components/file-upload/file-upload.js.map +1 -0
- package/dist/components/file-upload/index.d.ts +3 -0
- package/dist/components/file-upload/index.js +3 -0
- package/dist/components/file-upload/meta.js +17 -0
- package/dist/components/file-upload/meta.js.map +1 -0
- package/dist/components/file-upload/upload-queue.d.ts +87 -0
- package/dist/components/file-upload/upload-queue.d.ts.map +1 -0
- package/dist/components/file-upload/upload-queue.js +249 -0
- package/dist/components/file-upload/upload-queue.js.map +1 -0
- package/dist/components/log-viewer/index.d.ts +3 -0
- package/dist/components/log-viewer/index.js +3 -0
- package/dist/components/log-viewer/log-stream.d.ts +68 -0
- package/dist/components/log-viewer/log-stream.d.ts.map +1 -0
- package/dist/components/log-viewer/log-stream.js +120 -0
- package/dist/components/log-viewer/log-stream.js.map +1 -0
- package/dist/components/log-viewer/log-viewer.d.ts +51 -0
- package/dist/components/log-viewer/log-viewer.d.ts.map +1 -0
- package/dist/components/log-viewer/log-viewer.js +284 -0
- package/dist/components/log-viewer/log-viewer.js.map +1 -0
- package/dist/components/log-viewer/meta.js +17 -0
- package/dist/components/log-viewer/meta.js.map +1 -0
- package/dist/components/progress/progress.d.ts +1 -1
- package/dist/components/progress/progress.js +1 -0
- package/dist/components/progress/progress.js.map +1 -1
- package/dist/components/spinner/spinner.d.ts +1 -1
- package/dist/components/spinner/spinner.js +1 -0
- package/dist/components/spinner/spinner.js.map +1 -1
- package/dist/components/tags-input/index.d.ts +2 -0
- package/dist/components/tags-input/index.js +2 -0
- package/dist/components/tags-input/meta.js +17 -0
- package/dist/components/tags-input/meta.js.map +1 -0
- package/dist/components/tags-input/tags-input.d.ts +47 -0
- package/dist/components/tags-input/tags-input.d.ts.map +1 -0
- package/dist/components/tags-input/tags-input.js +182 -0
- package/dist/components/tags-input/tags-input.js.map +1 -0
- package/dist/components/time-range-picker/index.d.ts +3 -0
- package/dist/components/time-range-picker/index.js +3 -0
- package/dist/components/time-range-picker/meta.js +22 -0
- package/dist/components/time-range-picker/meta.js.map +1 -0
- package/dist/components/time-range-picker/time-expression.d.ts +88 -0
- package/dist/components/time-range-picker/time-expression.d.ts.map +1 -0
- package/dist/components/time-range-picker/time-expression.js +335 -0
- package/dist/components/time-range-picker/time-expression.js.map +1 -0
- package/dist/components/time-range-picker/time-range-picker.d.ts +65 -0
- package/dist/components/time-range-picker/time-range-picker.d.ts.map +1 -0
- package/dist/components/time-range-picker/time-range-picker.js +377 -0
- package/dist/components/time-range-picker/time-range-picker.js.map +1 -0
- package/dist/components/toast/toast.d.ts +1 -1
- package/dist/index.d.ts +21 -1
- package/dist/index.js +14 -2
- package/dist/registry/components.d.ts.map +1 -1
- package/dist/registry/components.js +66 -50
- package/dist/registry/components.js.map +1 -1
- package/package.json +6 -4
- package/src/components/ai-agent-plan/ai-agent-plan.tsx +0 -0
- package/src/components/ai-agent-plan/index.ts +10 -0
- package/src/components/ai-agent-plan/meta.ts +19 -0
- package/src/components/ai-approval-request/ai-approval-request.tsx +368 -0
- package/src/components/ai-approval-request/index.ts +7 -0
- package/src/components/ai-approval-request/meta.ts +21 -0
- package/src/components/ai-disclosure/ai-disclosure.tsx +244 -0
- package/src/components/ai-disclosure/index.ts +9 -0
- package/src/components/ai-disclosure/meta.ts +23 -0
- package/src/components/ai-response/ai-response.tsx +1 -0
- package/src/components/ai-structured-output/ai-structured-output.tsx +25 -1
- package/src/components/diff-viewer/diff-model.ts +256 -0
- package/src/components/diff-viewer/diff-viewer.tsx +339 -0
- package/src/components/diff-viewer/index.ts +17 -0
- package/src/components/diff-viewer/meta.ts +20 -0
- package/src/components/file-upload/file-upload.tsx +322 -0
- package/src/components/file-upload/index.ts +20 -0
- package/src/components/file-upload/meta.ts +21 -0
- package/src/components/file-upload/upload-queue.ts +337 -0
- package/src/components/log-viewer/index.ts +21 -0
- package/src/components/log-viewer/log-stream.ts +176 -0
- package/src/components/log-viewer/log-viewer.tsx +424 -0
- package/src/components/log-viewer/meta.ts +21 -0
- package/src/components/progress/progress.tsx +3 -0
- package/src/components/spinner/spinner.tsx +5 -0
- package/src/components/tags-input/index.ts +1 -0
- package/src/components/tags-input/meta.ts +23 -0
- package/src/components/tags-input/tags-input.tsx +300 -0
- package/src/components/time-range-picker/index.ts +25 -0
- package/src/components/time-range-picker/meta.ts +22 -0
- package/src/components/time-range-picker/time-expression.ts +491 -0
- package/src/components/time-range-picker/time-range-picker.tsx +522 -0
- package/src/index.ts +8 -0
- package/src/registry/components.ts +16 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"file-upload.d.ts","names":[],"sources":["../../../src/components/file-upload/file-upload.tsx"],"mappings":";;;UAqCiB,wBAAwB,KAAK;;EAE5C;EACA,UAAU,OAAO;EACjB;EACA;EACA;;EAEA,OAAO;EACP,WAAW;;iBAGG,aACd,WACA,OACA,SACA,QACA,UACA,UACA,MACA,aACG,SACF,kCAAe,IAAA;UAsFD,4BAA4B;EAC3C,OAAO;EACP,YAAY;EACZ,WAAW;EACX,YAAY;;iBAGE,iBACd,WACA,OACA,UACA,SACA,aACG,SACF,sCAAmB,IAAA;UAsBL,4BAA4B,KAAK;EAChD,OAAO;EACP,YAAY;EACZ,WAAW;EACX,YAAY;;iBAGE,iBACd,WACA,OACA,UACA,SACA,aACG,SACF,sCAAmB,IAAA;;;;;;;iBA+FN,mBACd,WACA,UACG,SACF;EACD;IAAS;IAAe;IAAgB;IAAc;;oBACvD,IAAA"}
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { disabledStyles, focusRing } from "../../lib/styles.js";
|
|
3
|
+
import { cn } from "../../lib/utils.js";
|
|
4
|
+
import { formatBytes } from "./upload-queue.js";
|
|
5
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
6
|
+
import { useId, useRef, useState } from "react";
|
|
7
|
+
//#region src/components/file-upload/file-upload.tsx
|
|
8
|
+
/**
|
|
9
|
+
* The visible half of uploading. The queue is in `upload-queue.ts` and is the
|
|
10
|
+
* part worth owning.
|
|
11
|
+
*
|
|
12
|
+
* There is no dropzone pattern in the WAI-ARIA APG, and inventing one is the
|
|
13
|
+
* usual failure: a `div` with `role="button"`, a keydown handler, and a file
|
|
14
|
+
* picker that keyboard users can never reach. So the control here is a real
|
|
15
|
+
* `<input type="file">` with a real `<label>`. That is already operable by
|
|
16
|
+
* keyboard, already announces itself, already opens the picker on Enter and
|
|
17
|
+
* Space, and needs nothing added. Drag and drop is layered on top as a pointer
|
|
18
|
+
* convenience, and every drop can also be done from the input.
|
|
19
|
+
*/
|
|
20
|
+
const STATUS_LABEL = {
|
|
21
|
+
queued: "Waiting",
|
|
22
|
+
uploading: "Uploading",
|
|
23
|
+
done: "Uploaded",
|
|
24
|
+
failed: "Failed",
|
|
25
|
+
cancelled: "Cancelled"
|
|
26
|
+
};
|
|
27
|
+
function FileUpload({ className, label, onFiles, accept, multiple = true, disabled = false, hint, children, ...props }) {
|
|
28
|
+
const inputId = useId();
|
|
29
|
+
const hintId = useId();
|
|
30
|
+
const [dragging, setDragging] = useState(false);
|
|
31
|
+
const depth = useRef(0);
|
|
32
|
+
function handleDrop(event) {
|
|
33
|
+
event.preventDefault();
|
|
34
|
+
depth.current = 0;
|
|
35
|
+
setDragging(false);
|
|
36
|
+
if (disabled) return;
|
|
37
|
+
const dropped = [...event.dataTransfer.files];
|
|
38
|
+
if (dropped.length > 0) onFiles(multiple ? dropped : dropped.slice(0, 1));
|
|
39
|
+
}
|
|
40
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
41
|
+
"data-slot": "file-upload",
|
|
42
|
+
className: cn("flex flex-col gap-3", className),
|
|
43
|
+
...props,
|
|
44
|
+
children: [/* @__PURE__ */ jsxs("div", {
|
|
45
|
+
"data-slot": "file-upload-dropzone",
|
|
46
|
+
"data-dragging": dragging || void 0,
|
|
47
|
+
onDragEnter: (event) => {
|
|
48
|
+
event.preventDefault();
|
|
49
|
+
depth.current += 1;
|
|
50
|
+
if (!disabled) setDragging(true);
|
|
51
|
+
},
|
|
52
|
+
onDragLeave: () => {
|
|
53
|
+
depth.current -= 1;
|
|
54
|
+
if (depth.current <= 0) setDragging(false);
|
|
55
|
+
},
|
|
56
|
+
onDragOver: (event) => {
|
|
57
|
+
event.preventDefault();
|
|
58
|
+
},
|
|
59
|
+
onDrop: handleDrop,
|
|
60
|
+
className: cn("rounded-lg border border-dashed border-border-strong bg-muted/30 px-4 py-6 text-center", "transition-colors duration-[var(--duration-fast)]", dragging && "border-primary bg-primary/5", disabled && "pointer-events-none opacity-55"),
|
|
61
|
+
children: [/* @__PURE__ */ jsxs("label", {
|
|
62
|
+
htmlFor: inputId,
|
|
63
|
+
className: cn("inline-flex cursor-pointer flex-col items-center gap-1 rounded-md px-2 py-1 text-sm", "focus-within:ring-2 focus-within:ring-ring/55"),
|
|
64
|
+
children: [
|
|
65
|
+
/* @__PURE__ */ jsx("span", {
|
|
66
|
+
className: "font-medium",
|
|
67
|
+
children: label
|
|
68
|
+
}),
|
|
69
|
+
/* @__PURE__ */ jsxs("span", {
|
|
70
|
+
className: "text-xs text-muted-foreground",
|
|
71
|
+
children: [
|
|
72
|
+
"Drop ",
|
|
73
|
+
multiple ? "files" : "a file",
|
|
74
|
+
" here, or choose from your device"
|
|
75
|
+
]
|
|
76
|
+
}),
|
|
77
|
+
/* @__PURE__ */ jsx("input", {
|
|
78
|
+
id: inputId,
|
|
79
|
+
type: "file",
|
|
80
|
+
accept,
|
|
81
|
+
multiple,
|
|
82
|
+
disabled,
|
|
83
|
+
"aria-describedby": hint ? hintId : void 0,
|
|
84
|
+
onChange: (event) => {
|
|
85
|
+
const chosen = [...event.target.files ?? []];
|
|
86
|
+
if (chosen.length > 0) onFiles(chosen);
|
|
87
|
+
event.target.value = "";
|
|
88
|
+
},
|
|
89
|
+
className: "sr-only"
|
|
90
|
+
})
|
|
91
|
+
]
|
|
92
|
+
}), hint ? /* @__PURE__ */ jsx("p", {
|
|
93
|
+
id: hintId,
|
|
94
|
+
className: "mt-2 text-xs text-muted-foreground",
|
|
95
|
+
children: hint
|
|
96
|
+
}) : null]
|
|
97
|
+
}), children]
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
function FileUploadList({ className, files, onCancel, onRetry, onRemove, ...props }) {
|
|
101
|
+
if (files.length === 0) return null;
|
|
102
|
+
return /* @__PURE__ */ jsx("ul", {
|
|
103
|
+
"data-slot": "file-upload-list",
|
|
104
|
+
className: cn("flex list-none flex-col gap-2", className),
|
|
105
|
+
...props,
|
|
106
|
+
children: files.map((entry) => /* @__PURE__ */ jsx(FileUploadItem, {
|
|
107
|
+
entry,
|
|
108
|
+
onCancel,
|
|
109
|
+
onRetry,
|
|
110
|
+
onRemove
|
|
111
|
+
}, entry.id))
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
function FileUploadItem({ className, entry, onCancel, onRetry, onRemove, ...props }) {
|
|
115
|
+
const { id, file, status, progress, error } = entry;
|
|
116
|
+
const percent = progress === null ? null : Math.round(progress * 100);
|
|
117
|
+
return /* @__PURE__ */ jsxs("li", {
|
|
118
|
+
"data-slot": "file-upload-item",
|
|
119
|
+
"data-status": status,
|
|
120
|
+
className: cn("flex items-center gap-3 rounded-lg border px-3 py-2 text-sm", status === "failed" ? "border-destructive/40 bg-destructive/5" : "border-border bg-card", className),
|
|
121
|
+
...props,
|
|
122
|
+
children: [/* @__PURE__ */ jsxs("div", {
|
|
123
|
+
className: "flex min-w-0 flex-1 flex-col gap-1",
|
|
124
|
+
children: [
|
|
125
|
+
/* @__PURE__ */ jsxs("div", {
|
|
126
|
+
className: "flex items-baseline justify-between gap-2",
|
|
127
|
+
children: [/* @__PURE__ */ jsx("span", {
|
|
128
|
+
className: "truncate font-medium",
|
|
129
|
+
children: file.name
|
|
130
|
+
}), /* @__PURE__ */ jsx("span", {
|
|
131
|
+
className: "shrink-0 text-xs text-muted-foreground tabular-nums",
|
|
132
|
+
children: formatBytes(file.size)
|
|
133
|
+
})]
|
|
134
|
+
}),
|
|
135
|
+
/* @__PURE__ */ jsxs("p", {
|
|
136
|
+
className: cn("text-xs", status === "failed" ? "text-destructive" : "text-muted-foreground"),
|
|
137
|
+
children: [
|
|
138
|
+
STATUS_LABEL[status],
|
|
139
|
+
status === "uploading" && percent !== null ? ` · ${String(percent)}%` : null,
|
|
140
|
+
error ? ` · ${error}` : null
|
|
141
|
+
]
|
|
142
|
+
}),
|
|
143
|
+
status === "uploading" ? /* @__PURE__ */ jsx("div", {
|
|
144
|
+
role: "progressbar",
|
|
145
|
+
"aria-label": `Uploading ${file.name}`,
|
|
146
|
+
"aria-valuenow": percent ?? void 0,
|
|
147
|
+
"aria-valuemin": 0,
|
|
148
|
+
"aria-valuemax": 100,
|
|
149
|
+
className: "h-1 w-full overflow-hidden rounded-full bg-muted",
|
|
150
|
+
children: /* @__PURE__ */ jsx("div", {
|
|
151
|
+
"data-slot": "file-upload-progress",
|
|
152
|
+
className: "h-full rounded-full bg-primary transition-[width] duration-[var(--duration-normal)] ease-[var(--ease-out-quint)]",
|
|
153
|
+
style: { width: `${String(percent ?? 0)}%` }
|
|
154
|
+
})
|
|
155
|
+
}) : null
|
|
156
|
+
]
|
|
157
|
+
}), /* @__PURE__ */ jsxs("div", {
|
|
158
|
+
className: "flex shrink-0 items-center gap-1",
|
|
159
|
+
children: [
|
|
160
|
+
status === "uploading" && onCancel ? /* @__PURE__ */ jsx(ItemButton, {
|
|
161
|
+
onClick: () => onCancel(id),
|
|
162
|
+
children: "Cancel"
|
|
163
|
+
}) : null,
|
|
164
|
+
(status === "failed" || status === "cancelled") && onRetry ? /* @__PURE__ */ jsx(ItemButton, {
|
|
165
|
+
onClick: () => onRetry(id),
|
|
166
|
+
children: "Retry"
|
|
167
|
+
}) : null,
|
|
168
|
+
onRemove ? /* @__PURE__ */ jsx(ItemButton, {
|
|
169
|
+
onClick: () => onRemove(id),
|
|
170
|
+
"aria-label": `Remove ${file.name}`,
|
|
171
|
+
children: "Remove"
|
|
172
|
+
}) : null
|
|
173
|
+
]
|
|
174
|
+
})]
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
function ItemButton({ className, ...props }) {
|
|
178
|
+
return /* @__PURE__ */ jsx("button", {
|
|
179
|
+
type: "button",
|
|
180
|
+
className: cn("rounded-md border border-input bg-background px-2 py-0.5 text-xs font-medium", "transition-colors hover:bg-accent hover:text-accent-foreground", focusRing, disabledStyles, className),
|
|
181
|
+
...props
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
/**
|
|
185
|
+
* One sentence covering the whole queue, announced politely.
|
|
186
|
+
*
|
|
187
|
+
* Per-file live regions would talk over each other the moment two uploads run
|
|
188
|
+
* at once; one summary of the set is readable where six competing ones are not.
|
|
189
|
+
*/
|
|
190
|
+
function FileUploadStatus({ className, stats, ...props }) {
|
|
191
|
+
const { total, active, done, failed } = stats;
|
|
192
|
+
const message = total === 0 ? "" : active > 0 ? `${String(done)} of ${String(total)} uploaded` : failed > 0 ? `${String(done)} uploaded, ${String(failed)} failed` : `${String(done)} uploaded`;
|
|
193
|
+
return /* @__PURE__ */ jsx("p", {
|
|
194
|
+
"data-slot": "file-upload-status",
|
|
195
|
+
"aria-live": "polite",
|
|
196
|
+
className: cn("text-xs text-muted-foreground", className),
|
|
197
|
+
...props,
|
|
198
|
+
children: message
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
//#endregion
|
|
202
|
+
export { FileUpload, FileUploadItem, FileUploadList, FileUploadStatus };
|
|
203
|
+
|
|
204
|
+
//# sourceMappingURL=file-upload.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"file-upload.js","names":[],"sources":["../../../src/components/file-upload/file-upload.tsx"],"sourcesContent":["\"use client\";\n\nimport {\n useId,\n useRef,\n useState,\n type ComponentPropsWithRef,\n type DragEvent,\n type ReactNode,\n} from \"react\";\n\nimport { disabledStyles, focusRing } from \"@/lib/styles\";\nimport { cn } from \"@/lib/utils\";\n\nimport { formatBytes, type QueuedFile, type UploadStatus } from \"./upload-queue\";\n\n/**\n * The visible half of uploading. The queue is in `upload-queue.ts` and is the\n * part worth owning.\n *\n * There is no dropzone pattern in the WAI-ARIA APG, and inventing one is the\n * usual failure: a `div` with `role=\"button\"`, a keydown handler, and a file\n * picker that keyboard users can never reach. So the control here is a real\n * `<input type=\"file\">` with a real `<label>`. That is already operable by\n * keyboard, already announces itself, already opens the picker on Enter and\n * Space, and needs nothing added. Drag and drop is layered on top as a pointer\n * convenience, and every drop can also be done from the input.\n */\n\nconst STATUS_LABEL: Record<UploadStatus, string> = {\n queued: \"Waiting\",\n uploading: \"Uploading\",\n done: \"Uploaded\",\n failed: \"Failed\",\n cancelled: \"Cancelled\",\n};\n\nexport interface FileUploadProps extends Omit<ComponentPropsWithRef<\"div\">, \"onDrop\"> {\n /** Names the control. */\n label: string;\n onFiles: (files: File[]) => void;\n accept?: string;\n multiple?: boolean;\n disabled?: boolean;\n /** Shown under the prompt: accepted types, size limit. */\n hint?: ReactNode;\n children?: ReactNode;\n}\n\nexport function FileUpload({\n className,\n label,\n onFiles,\n accept,\n multiple = true,\n disabled = false,\n hint,\n children,\n ...props\n}: FileUploadProps) {\n const inputId = useId();\n const hintId = useId();\n const [dragging, setDragging] = useState(false);\n const depth = useRef(0);\n\n function handleDrop(event: DragEvent<HTMLDivElement>) {\n event.preventDefault();\n depth.current = 0;\n setDragging(false);\n if (disabled) return;\n\n const dropped = [...event.dataTransfer.files];\n if (dropped.length > 0) onFiles(multiple ? dropped : dropped.slice(0, 1));\n }\n\n return (\n <div data-slot=\"file-upload\" className={cn(\"flex flex-col gap-3\", className)} {...props}>\n {/* dragenter/dragleave fire for every child element, so a plain boolean\n flickers as the pointer crosses the prompt text. Counting depth is\n what makes the highlight steady. */}\n <div\n data-slot=\"file-upload-dropzone\"\n data-dragging={dragging || undefined}\n onDragEnter={(event) => {\n event.preventDefault();\n depth.current += 1;\n if (!disabled) setDragging(true);\n }}\n onDragLeave={() => {\n depth.current -= 1;\n if (depth.current <= 0) setDragging(false);\n }}\n onDragOver={(event) => {\n event.preventDefault();\n }}\n onDrop={handleDrop}\n className={cn(\n \"rounded-lg border border-dashed border-border-strong bg-muted/30 px-4 py-6 text-center\",\n \"transition-colors duration-[var(--duration-fast)]\",\n dragging && \"border-primary bg-primary/5\",\n disabled && \"pointer-events-none opacity-55\",\n )}\n >\n {/* The label is the control. Clicking it opens the picker, Enter and\n Space activate it, and assistive technology already describes it. */}\n <label\n htmlFor={inputId}\n className={cn(\n \"inline-flex cursor-pointer flex-col items-center gap-1 rounded-md px-2 py-1 text-sm\",\n \"focus-within:ring-2 focus-within:ring-ring/55\",\n )}\n >\n <span className=\"font-medium\">{label}</span>\n <span className=\"text-xs text-muted-foreground\">\n Drop {multiple ? \"files\" : \"a file\"} here, or choose from your device\n </span>\n <input\n id={inputId}\n type=\"file\"\n accept={accept}\n multiple={multiple}\n disabled={disabled}\n aria-describedby={hint ? hintId : undefined}\n onChange={(event) => {\n const chosen = [...(event.target.files ?? [])];\n if (chosen.length > 0) onFiles(chosen);\n // Reset, so choosing the same file twice fires change twice.\n event.target.value = \"\";\n }}\n className=\"sr-only\"\n />\n </label>\n\n {hint ? (\n <p id={hintId} className=\"mt-2 text-xs text-muted-foreground\">\n {hint}\n </p>\n ) : null}\n </div>\n\n {children}\n </div>\n );\n}\n\nexport interface FileUploadListProps extends ComponentPropsWithRef<\"ul\"> {\n files: QueuedFile[];\n onCancel?: (id: string) => void;\n onRetry?: (id: string) => void;\n onRemove?: (id: string) => void;\n}\n\nexport function FileUploadList({\n className,\n files,\n onCancel,\n onRetry,\n onRemove,\n ...props\n}: FileUploadListProps) {\n if (files.length === 0) return null;\n\n return (\n <ul\n data-slot=\"file-upload-list\"\n className={cn(\"flex list-none flex-col gap-2\", className)}\n {...props}\n >\n {files.map((entry) => (\n <FileUploadItem\n key={entry.id}\n entry={entry}\n onCancel={onCancel}\n onRetry={onRetry}\n onRemove={onRemove}\n />\n ))}\n </ul>\n );\n}\n\nexport interface FileUploadItemProps extends Omit<ComponentPropsWithRef<\"li\">, \"children\"> {\n entry: QueuedFile;\n onCancel?: (id: string) => void;\n onRetry?: (id: string) => void;\n onRemove?: (id: string) => void;\n}\n\nexport function FileUploadItem({\n className,\n entry,\n onCancel,\n onRetry,\n onRemove,\n ...props\n}: FileUploadItemProps) {\n const { id, file, status, progress, error } = entry;\n const percent = progress === null ? null : Math.round(progress * 100);\n\n return (\n <li\n data-slot=\"file-upload-item\"\n data-status={status}\n className={cn(\n \"flex items-center gap-3 rounded-lg border px-3 py-2 text-sm\",\n status === \"failed\"\n ? \"border-destructive/40 bg-destructive/5\"\n : \"border-border bg-card\",\n className,\n )}\n {...props}\n >\n <div className=\"flex min-w-0 flex-1 flex-col gap-1\">\n <div className=\"flex items-baseline justify-between gap-2\">\n <span className=\"truncate font-medium\">{file.name}</span>\n <span className=\"shrink-0 text-xs text-muted-foreground tabular-nums\">\n {formatBytes(file.size)}\n </span>\n </div>\n\n {/* Status in words, always. A bar at 60% with a red tint does not say\n whether it is uploading, stalled or failed. */}\n <p\n className={cn(\n \"text-xs\",\n status === \"failed\" ? \"text-destructive\" : \"text-muted-foreground\",\n )}\n >\n {STATUS_LABEL[status]}\n {status === \"uploading\" && percent !== null ? ` · ${String(percent)}%` : null}\n {error ? ` · ${error}` : null}\n </p>\n\n {status === \"uploading\" ? (\n <div\n role=\"progressbar\"\n aria-label={`Uploading ${file.name}`}\n aria-valuenow={percent ?? undefined}\n aria-valuemin={0}\n aria-valuemax={100}\n className=\"h-1 w-full overflow-hidden rounded-full bg-muted\"\n >\n <div\n data-slot=\"file-upload-progress\"\n className=\"h-full rounded-full bg-primary transition-[width] duration-[var(--duration-normal)] ease-[var(--ease-out-quint)]\"\n style={{ width: `${String(percent ?? 0)}%` }}\n />\n </div>\n ) : null}\n </div>\n\n <div className=\"flex shrink-0 items-center gap-1\">\n {status === \"uploading\" && onCancel ? (\n <ItemButton onClick={() => onCancel(id)}>Cancel</ItemButton>\n ) : null}\n {(status === \"failed\" || status === \"cancelled\") && onRetry ? (\n <ItemButton onClick={() => onRetry(id)}>Retry</ItemButton>\n ) : null}\n {onRemove ? (\n <ItemButton onClick={() => onRemove(id)} aria-label={`Remove ${file.name}`}>\n Remove\n </ItemButton>\n ) : null}\n </div>\n </li>\n );\n}\n\nfunction ItemButton({ className, ...props }: ComponentPropsWithRef<\"button\">) {\n return (\n <button\n type=\"button\"\n className={cn(\n \"rounded-md border border-input bg-background px-2 py-0.5 text-xs font-medium\",\n \"transition-colors hover:bg-accent hover:text-accent-foreground\",\n focusRing,\n disabledStyles,\n className,\n )}\n {...props}\n />\n );\n}\n\n/**\n * One sentence covering the whole queue, announced politely.\n *\n * Per-file live regions would talk over each other the moment two uploads run\n * at once; one summary of the set is readable where six competing ones are not.\n */\nexport function FileUploadStatus({\n className,\n stats,\n ...props\n}: ComponentPropsWithRef<\"p\"> & {\n stats: { total: number; active: number; done: number; failed: number };\n}) {\n const { total, active, done, failed } = stats;\n\n // Progress through the set, not a count of what happens to be in flight.\n // \"3 of 5 uploading\" is wrong the moment a concurrency limit holds two back,\n // and it never tells the reader how much of the job is left.\n const message =\n total === 0\n ? \"\"\n : active > 0\n ? `${String(done)} of ${String(total)} uploaded`\n : failed > 0\n ? `${String(done)} uploaded, ${String(failed)} failed`\n : `${String(done)} uploaded`;\n\n return (\n <p\n data-slot=\"file-upload-status\"\n aria-live=\"polite\"\n className={cn(\"text-xs text-muted-foreground\", className)}\n {...props}\n >\n {message}\n </p>\n );\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import { QueuedFile, UploadContext, UploadFn, UploadQueueOptions, UploadStatus, formatBytes, matchesAccept, useUploadQueue, xhrUpload } from "./upload-queue.js";
|
|
2
|
+
import { FileUpload, FileUploadItem, FileUploadItemProps, FileUploadList, FileUploadListProps, FileUploadProps, FileUploadStatus } from "./file-upload.js";
|
|
3
|
+
export { FileUpload, FileUploadItem, type FileUploadItemProps, FileUploadList, type FileUploadListProps, type FileUploadProps, FileUploadStatus, type QueuedFile, type UploadContext, type UploadFn, type UploadQueueOptions, type UploadStatus, formatBytes, matchesAccept, useUploadQueue, xhrUpload };
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import { formatBytes, matchesAccept, useUploadQueue, xhrUpload } from "./upload-queue.js";
|
|
2
|
+
import { FileUpload, FileUploadItem, FileUploadList, FileUploadStatus } from "./file-upload.js";
|
|
3
|
+
export { FileUpload, FileUploadItem, FileUploadList, FileUploadStatus, formatBytes, matchesAccept, useUploadQueue, xhrUpload };
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { defineMeta } from "../../registry/schema.js";
|
|
2
|
+
//#region src/components/file-upload/meta.ts
|
|
3
|
+
const meta = defineMeta({
|
|
4
|
+
name: "file-upload",
|
|
5
|
+
title: "File Upload",
|
|
6
|
+
description: "A dropzone over a real file input, plus the upload queue nobody ships.",
|
|
7
|
+
category: "form",
|
|
8
|
+
status: "stable",
|
|
9
|
+
dependencies: [],
|
|
10
|
+
registryDependencies: [],
|
|
11
|
+
files: ["upload-queue.ts", "file-upload.tsx"],
|
|
12
|
+
a11y: "The APG has no dropzone pattern, and inventing one is the usual failure — a div with role=\"button\", a keydown handler, and a picker keyboard users never reach. The control here is a real input[type=file] with a real label, which is already operable, already announced, and opens the picker on Enter and Space with nothing added. Drag and drop is a pointer convenience layered on top; every drop can also be done from the input. Each uploading file gets a progressbar named after it, status is always stated in words as well as drawn, and one polite live region summarises the whole queue rather than six per-file regions talking over each other."
|
|
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/file-upload/meta.ts"],"sourcesContent":["import { defineMeta } from \"@/registry/schema\";\n\nexport const meta = defineMeta({\n name: \"file-upload\",\n title: \"File Upload\",\n description: \"A dropzone over a real file input, plus the upload queue nobody ships.\",\n category: \"form\",\n status: \"stable\",\n dependencies: [],\n registryDependencies: [],\n files: [\"upload-queue.ts\", \"file-upload.tsx\"],\n a11y:\n \"The APG has no dropzone pattern, and inventing one is the usual failure — a div with \" +\n 'role=\"button\", a keydown handler, and a picker keyboard users never reach. The control here ' +\n \"is a real input[type=file] with a real label, which is already operable, already announced, \" +\n \"and opens the picker on Enter and Space with nothing added. Drag and drop is a pointer \" +\n \"convenience layered on top; every drop can also be done from the input. Each uploading file \" +\n \"gets a progressbar named after it, status is always stated in words as well as drawn, and \" +\n \"one polite live region summarises the whole queue rather than six per-file regions talking \" +\n \"over each other.\",\n});\n"],"mappings":";;AAEA,MAAa,OAAO,WAAW;CAC7B,MAAM;CACN,OAAO;CACP,aAAa;CACb,UAAU;CACV,QAAQ;CACR,cAAc,CAAC;CACf,sBAAsB,CAAC;CACvB,OAAO,CAAC,mBAAmB,iBAAiB;CAC5C,MACE;AAQJ,CAAC"}
|
|
@@ -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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"}
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import { FilterState, LOG_LEVELS, LogLevel, LogLine, MatchRange, UseLogStreamOptions, VisibleLine, compileQuery, findMatches, segment, useLogStream } from "./log-stream.js";
|
|
2
|
+
import { LogViewer, LogViewerProps, LogViewerRow, LogViewerRowProps, LogViewerToolbar, LogViewerToolbarProps } from "./log-viewer.js";
|
|
3
|
+
export { type FilterState, LOG_LEVELS, type LogLevel, type LogLine, LogViewer, type LogViewerProps, LogViewerRow, type LogViewerRowProps, LogViewerToolbar, type LogViewerToolbarProps, type MatchRange, type UseLogStreamOptions, type VisibleLine, compileQuery, findMatches, segment, useLogStream };
|