@arcfusionz/arc-primitive-ui 0.2.12 → 0.2.14

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.
@@ -0,0 +1,515 @@
1
+ import { cn } from "../../lib/cn.js";
2
+ import { Button } from "../Button/Button.js";
3
+ import { FieldContext } from "../Field/FieldContext.js";
4
+ import { Progress } from "../Progress/Progress.js";
5
+ import { SystemState } from "../SystemState/SystemState.js";
6
+ import { Fragment, createContext, forwardRef, useContext, useEffect, useId, useRef, useState } from "react";
7
+ import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
8
+ import { useRender } from "@base-ui/react/use-render";
9
+ //#region src/components/FileUpload/FileUpload.tsx
10
+ /** `1023 B`, `1.5 KB`, `12 MB` — a compact, locale-neutral size readout. */
11
+ function formatFileSize(bytes) {
12
+ if (!Number.isFinite(bytes) || bytes < 0) return "";
13
+ if (bytes < 1024) return `${bytes} B`;
14
+ const units = [
15
+ "KB",
16
+ "MB",
17
+ "GB",
18
+ "TB"
19
+ ];
20
+ let value = bytes / 1024;
21
+ let unit = 0;
22
+ while (value >= 1024 && unit < units.length - 1) {
23
+ value /= 1024;
24
+ unit += 1;
25
+ }
26
+ return `${value < 10 ? value.toFixed(1) : Math.round(value)} ${units[unit]}`;
27
+ }
28
+ function matchesAccept(file, accept) {
29
+ if (!accept) return true;
30
+ const tokens = accept.split(",").map((token) => token.trim().toLowerCase()).filter(Boolean);
31
+ if (tokens.length === 0) return true;
32
+ const name = file.name.toLowerCase();
33
+ const mime = file.type.toLowerCase();
34
+ return tokens.some((token) => {
35
+ if (token.startsWith(".")) return name.endsWith(token);
36
+ if (token.endsWith("/*")) return mime.startsWith(token.slice(0, -1));
37
+ return mime === token;
38
+ });
39
+ }
40
+ function fileKey(file) {
41
+ return `${file.name}:${file.size}:${file.lastModified}`;
42
+ }
43
+ const FileUploadContext = createContext(null);
44
+ function useFileUploadContext(part) {
45
+ const context = useContext(FileUploadContext);
46
+ if (context == null) throw new Error(`${part} must be rendered inside <FileUpload>.`);
47
+ return context;
48
+ }
49
+ /**
50
+ * Read the enclosing `FileUpload`'s state and controls — for building custom
51
+ * pieces the flat parts don't cover (a file counter, an "Upload all" button, a
52
+ * dropzone that hides once a file is chosen). Must be called under `FileUpload`.
53
+ */
54
+ function useFileUpload() {
55
+ const context = useFileUploadContext("useFileUpload");
56
+ return {
57
+ files: context.files,
58
+ isDragging: context.isDragging,
59
+ disabled: context.disabled,
60
+ invalid: context.invalid,
61
+ open: context.openPicker,
62
+ remove: context.removeFile,
63
+ clear: context.clear
64
+ };
65
+ }
66
+ const rootClasses = "flex w-full flex-col gap-3";
67
+ const stateAttributesMapping = {
68
+ dragging: (value) => value ? { "data-dragging": "" } : null,
69
+ disabled: (value) => value ? { "data-disabled": "" } : null,
70
+ invalid: (value) => value ? { "data-invalid": "" } : null,
71
+ empty: (value) => value ? { "data-empty": "" } : null
72
+ };
73
+ /**
74
+ * Drag-and-drop / browse file selection built on a native input. Owns the
75
+ * selected-file list, drag interaction, and client-side validation (`accept`,
76
+ * `maxSize`/`minSize`, `maxFiles`, custom `validate`); the actual upload stays
77
+ * with the consumer, reflected per file through `FileUploadItem`. The empty
78
+ * dropzone is a `SystemState` (`upload` / `upload-error`).
79
+ *
80
+ * Accessibility: the input is visually hidden and the `FileUploadTrigger`
81
+ * button is the focusable, keyboard-operable control (drag-and-drop is a
82
+ * pointer enhancement). Announce added or rejected files yourself — the
83
+ * component is not a live region. Inside a `Field` it inherits
84
+ * `disabled`/`invalid`/`required`.
85
+ */
86
+ const FileUpload = forwardRef(({ accept, multiple = false, maxFiles, maxSize, minSize, validate, allowDrop = true, preventDocumentDrop = true, directory = false, capture, value, defaultValue, onValueChange, onAccept, onReject, disabled, invalid, required, name, inputLabel, render, className, children, ...rest }, ref) => {
87
+ const fieldContext = useContext(FieldContext);
88
+ const resolvedDisabled = disabled ?? fieldContext?.control?.disabled ?? false;
89
+ const resolvedInvalid = invalid ?? fieldContext?.invalid ?? false;
90
+ const resolvedRequired = required ?? fieldContext?.required ?? false;
91
+ const inputId = `${useId()}-file-input`;
92
+ const inputRef = useRef(null);
93
+ const rootRef = useRef(null);
94
+ const dragDepth = useRef(0);
95
+ const isControlled = value !== void 0;
96
+ const [internalFiles, setInternalFiles] = useState(defaultValue ?? []);
97
+ const files = isControlled ? value : internalFiles;
98
+ const [isDragging, setIsDragging] = useState(false);
99
+ const syncInputFiles = (list) => {
100
+ const input = inputRef.current;
101
+ if (!input || typeof DataTransfer === "undefined") return;
102
+ try {
103
+ const transfer = new DataTransfer();
104
+ for (const file of list) transfer.items.add(file);
105
+ input.files = transfer.files;
106
+ } catch {}
107
+ };
108
+ useEffect(() => {
109
+ syncInputFiles(files);
110
+ }, [files]);
111
+ const commit = (next) => {
112
+ if (!isControlled) setInternalFiles(next);
113
+ onValueChange?.(next);
114
+ };
115
+ const getRejectionCodes = (file, projectedCount) => {
116
+ const codes = [];
117
+ if (!matchesAccept(file, accept)) codes.push("file-invalid-type");
118
+ if (typeof minSize === "number" && file.size < minSize) codes.push("file-too-small");
119
+ if (typeof maxSize === "number" && file.size > maxSize) codes.push("file-too-large");
120
+ if (projectedCount >= (multiple ? maxFiles ?? Infinity : 1)) codes.push("too-many-files");
121
+ const custom = validate?.(file);
122
+ if (custom) {
123
+ for (const code of custom) if (!codes.includes(code)) codes.push(code);
124
+ }
125
+ return codes;
126
+ };
127
+ const processIncoming = (incoming) => {
128
+ const kept = [...multiple ? files : []];
129
+ const accepted = [];
130
+ const rejections = [];
131
+ for (const file of incoming) {
132
+ if (kept.some((existing) => fileKey(existing) === fileKey(file))) continue;
133
+ const codes = getRejectionCodes(file, kept.length);
134
+ if (codes.length > 0) rejections.push({
135
+ file,
136
+ errors: codes
137
+ });
138
+ else {
139
+ kept.push(file);
140
+ accepted.push(file);
141
+ }
142
+ }
143
+ const next = multiple ? kept : accepted.length > 0 ? [accepted[0]] : files;
144
+ if (accepted.length > 0) onAccept?.(accepted);
145
+ if (rejections.length > 0) onReject?.(rejections);
146
+ if (next.length !== files.length || next.some((file, index) => file !== files[index])) commit(next);
147
+ syncInputFiles(next);
148
+ };
149
+ const openPicker = () => {
150
+ if (resolvedDisabled) return;
151
+ inputRef.current?.click();
152
+ };
153
+ const removeFile = (file) => {
154
+ commit(files.filter((existing) => fileKey(existing) !== fileKey(file)));
155
+ };
156
+ const clear = () => commit([]);
157
+ const dropHasFiles = (event) => Array.from(event.dataTransfer?.types ?? []).includes("Files");
158
+ const onDragEnter = (event) => {
159
+ if (resolvedDisabled || !allowDrop || !dropHasFiles(event)) return;
160
+ event.preventDefault();
161
+ dragDepth.current += 1;
162
+ setIsDragging(true);
163
+ };
164
+ const onDragOver = (event) => {
165
+ if (resolvedDisabled || !allowDrop || !dropHasFiles(event)) return;
166
+ event.preventDefault();
167
+ event.dataTransfer.dropEffect = "copy";
168
+ };
169
+ const onDragLeave = (event) => {
170
+ if (resolvedDisabled || !allowDrop) return;
171
+ event.preventDefault();
172
+ dragDepth.current = Math.max(0, dragDepth.current - 1);
173
+ if (dragDepth.current === 0) setIsDragging(false);
174
+ };
175
+ const onDrop = (event) => {
176
+ if (resolvedDisabled || !allowDrop) return;
177
+ event.preventDefault();
178
+ dragDepth.current = 0;
179
+ setIsDragging(false);
180
+ const dropped = Array.from(event.dataTransfer?.files ?? []);
181
+ if (dropped.length > 0) processIncoming(dropped);
182
+ };
183
+ useEffect(() => {
184
+ if (!preventDocumentDrop || !allowDrop || typeof document === "undefined") return;
185
+ const isOutside = (event) => {
186
+ const root = rootRef.current;
187
+ return !root || !(event.target instanceof Node) || !root.contains(event.target);
188
+ };
189
+ const prevent = (event) => {
190
+ if (isOutside(event)) event.preventDefault();
191
+ };
192
+ document.addEventListener("dragover", prevent);
193
+ document.addEventListener("drop", prevent);
194
+ return () => {
195
+ document.removeEventListener("dragover", prevent);
196
+ document.removeEventListener("drop", prevent);
197
+ };
198
+ }, [preventDocumentDrop, allowDrop]);
199
+ const contextValue = {
200
+ files,
201
+ disabled: resolvedDisabled,
202
+ invalid: resolvedInvalid,
203
+ multiple,
204
+ isDragging,
205
+ inputId,
206
+ openPicker,
207
+ removeFile,
208
+ clear,
209
+ dropzoneListeners: {
210
+ onDragEnter,
211
+ onDragOver,
212
+ onDragLeave,
213
+ onDrop
214
+ }
215
+ };
216
+ const state = {
217
+ dragging: isDragging,
218
+ disabled: resolvedDisabled,
219
+ invalid: resolvedInvalid,
220
+ empty: files.length === 0
221
+ };
222
+ return useRender({
223
+ defaultTagName: "div",
224
+ render,
225
+ ref: [ref, rootRef],
226
+ state,
227
+ stateAttributesMapping,
228
+ props: {
229
+ className: cn(rootClasses, className),
230
+ children: /* @__PURE__ */ jsxs(FileUploadContext.Provider, {
231
+ value: contextValue,
232
+ children: [/* @__PURE__ */ jsx("input", {
233
+ ref: inputRef,
234
+ id: inputId,
235
+ type: "file",
236
+ accept,
237
+ multiple,
238
+ capture,
239
+ disabled: resolvedDisabled || void 0,
240
+ required: resolvedRequired || void 0,
241
+ name,
242
+ "aria-label": inputLabel ?? (multiple ? "Upload files" : "Upload file"),
243
+ "aria-invalid": resolvedInvalid || void 0,
244
+ tabIndex: -1,
245
+ className: "sr-only",
246
+ onChange: (event) => {
247
+ const list = event.target.files;
248
+ if (list && list.length > 0) processIncoming(Array.from(list));
249
+ },
250
+ ...directory ? { webkitdirectory: "" } : null
251
+ }), children]
252
+ }),
253
+ ...rest
254
+ }
255
+ });
256
+ });
257
+ FileUpload.displayName = "FileUpload";
258
+ const dropzoneClasses = "relative flex w-full flex-col items-center justify-center rounded-md border-2 border-dashed border-border bg-transparent cursor-pointer transition-colors duration-150 hover:border-slate-300 hover:bg-surface has-[:focus-visible]:border-primary has-[:focus-visible]:bg-primary-50 data-dragging:border-primary data-dragging:bg-primary-50 data-invalid:border-destructive data-disabled:cursor-default data-disabled:opacity-50 data-disabled:hover:border-border data-disabled:hover:bg-transparent";
259
+ /**
260
+ * Class list for an element styled as the dashed drop area — for building a
261
+ * bespoke dropzone while keeping the brand's border, hover, and drag/invalid
262
+ * looks. The drag state hooks are `data-dragging` / `data-invalid` /
263
+ * `data-disabled`.
264
+ */
265
+ function fileUploadDropzoneVariants({ className } = {}) {
266
+ return cn(dropzoneClasses, className);
267
+ }
268
+ /**
269
+ * The dashed drop area. Clicking anywhere in it opens the picker (a pointer
270
+ * convenience); the `FileUploadTrigger` inside is the focusable control for
271
+ * keyboard and assistive tech. With no `children` it renders a `SystemState`
272
+ * (`upload`, or `upload-error` when invalid) with a browse button; pass
273
+ * `children` to supply your own prompt. Highlights via `data-dragging` while a
274
+ * file is dragged over it.
275
+ */
276
+ const FileUploadDropzone = forwardRef(({ children, className, onClick, ...rest }, ref) => {
277
+ const context = useFileUploadContext("FileUploadDropzone");
278
+ return /* @__PURE__ */ jsx("div", {
279
+ ref,
280
+ "data-dragging": context.isDragging ? "" : void 0,
281
+ "data-invalid": context.invalid ? "" : void 0,
282
+ "data-disabled": context.disabled ? "" : void 0,
283
+ className: fileUploadDropzoneVariants({ className }),
284
+ onClick: (event) => {
285
+ onClick?.(event);
286
+ if (!event.defaultPrevented) context.openPicker();
287
+ },
288
+ ...context.dropzoneListeners,
289
+ ...rest,
290
+ children: children ?? /* @__PURE__ */ jsx(SystemState, {
291
+ variant: context.invalid ? "upload-error" : "upload",
292
+ bg: "transparent",
293
+ action: /* @__PURE__ */ jsx(FileUploadTrigger, {})
294
+ })
295
+ });
296
+ });
297
+ FileUploadDropzone.displayName = "FileUploadDropzone";
298
+ /**
299
+ * Button that opens the file picker. Rendered inside `FileUploadDropzone` by
300
+ * default; use it on its own for a compact "attach a file" control with no drop
301
+ * area. Defaults to the `outline` variant and an upload-icon + "Browse files"
302
+ * label. A custom label opts into an icon via `startIcon`; pass
303
+ * `startIcon={null}` to remove the default one. Inherits the upload's
304
+ * `disabled` state.
305
+ */
306
+ const FileUploadTrigger = forwardRef(({ variant = "outline", disabled, onClick, startIcon, children, ...rest }, ref) => {
307
+ const context = useFileUploadContext("FileUploadTrigger");
308
+ const resolvedStartIcon = startIcon !== void 0 ? startIcon : children == null ? /* @__PURE__ */ jsx(UploadIcon, {}) : void 0;
309
+ return /* @__PURE__ */ jsx(Button, {
310
+ ref,
311
+ type: "button",
312
+ variant,
313
+ disabled: disabled ?? context.disabled,
314
+ startIcon: resolvedStartIcon,
315
+ onClick: (event) => {
316
+ event.stopPropagation();
317
+ onClick?.(event);
318
+ if (!event.defaultPrevented) context.openPicker();
319
+ },
320
+ ...rest,
321
+ children: children ?? "Browse files"
322
+ });
323
+ });
324
+ FileUploadTrigger.displayName = "FileUploadTrigger";
325
+ const listClasses = "flex w-full flex-col gap-2";
326
+ /**
327
+ * Container for the selected files. `role="list"` is set explicitly so the
328
+ * list semantics survive the `flex` reset in Safari/VoiceOver. Renders nothing
329
+ * while empty in the default (auto or render-prop) mode.
330
+ */
331
+ const FileUploadList = forwardRef(({ children, className, ...rest }, ref) => {
332
+ const context = useFileUploadContext("FileUploadList");
333
+ const isRenderProp = typeof children === "function";
334
+ if ((children == null || isRenderProp) && context.files.length === 0) return null;
335
+ return /* @__PURE__ */ jsx("ul", {
336
+ ref,
337
+ role: "list",
338
+ className: cn(listClasses, className),
339
+ ...rest,
340
+ children: isRenderProp ? context.files.map((file, index) => /* @__PURE__ */ jsx(Fragment, { children: children(file, index) }, `${fileKey(file)}-${index}`)) : children ?? context.files.map((file, index) => /* @__PURE__ */ jsx(FileUploadItem, { file }, `${fileKey(file)}-${index}`))
341
+ });
342
+ });
343
+ FileUploadList.displayName = "FileUploadList";
344
+ const itemClasses = "flex items-center gap-3 rounded-md border border-border bg-background px-3 py-2.5 data-[status=error]:border-destructive";
345
+ const itemNameClasses = "truncate font-sans text-sm font-medium text-foreground";
346
+ const itemMetaClasses = "font-sans text-xs text-muted-foreground";
347
+ const itemErrorClasses = "font-sans text-xs font-medium text-error-700";
348
+ /**
349
+ * One selected file: a thumbnail (auto-generated for images) or file icon, the
350
+ * name, and — depending on `status` — its size, an upload progress bar, or an
351
+ * error message, plus a remove button. `status`/`progress`/`error` are
352
+ * consumer-driven so the row reflects real upload state.
353
+ */
354
+ const FileUploadItem = forwardRef(({ file, status = "idle", progress, error, removable = true, onRemove, removeLabel, preview, children, className, ...rest }, ref) => {
355
+ const context = useFileUploadContext("FileUploadItem");
356
+ const handleRemove = onRemove ?? (() => context.removeFile(file));
357
+ return /* @__PURE__ */ jsx("li", {
358
+ ref,
359
+ "data-status": status,
360
+ className: cn(itemClasses, className),
361
+ ...rest,
362
+ children: children ?? /* @__PURE__ */ jsxs(Fragment$1, { children: [
363
+ /* @__PURE__ */ jsx("span", {
364
+ className: "shrink-0",
365
+ children: preview ?? /* @__PURE__ */ jsx(FileUploadThumbnail, { file })
366
+ }),
367
+ /* @__PURE__ */ jsxs("div", {
368
+ className: "flex min-w-0 flex-1 flex-col gap-1",
369
+ children: [/* @__PURE__ */ jsxs("div", {
370
+ className: "flex items-center gap-2",
371
+ children: [
372
+ /* @__PURE__ */ jsx("span", {
373
+ className: itemNameClasses,
374
+ children: file.name
375
+ }),
376
+ status === "success" && /* @__PURE__ */ jsx(CheckCircleIcon, {
377
+ className: "ms-auto size-4 shrink-0 text-success-700",
378
+ "aria-hidden": "true"
379
+ }),
380
+ status === "error" && /* @__PURE__ */ jsx(AlertCircleIcon, {
381
+ className: "ms-auto size-4 shrink-0 text-error-700",
382
+ "aria-hidden": "true"
383
+ })
384
+ ]
385
+ }), status === "uploading" ? /* @__PURE__ */ jsx(Progress, {
386
+ size: "sm",
387
+ value: progress ?? null,
388
+ "aria-label": `Uploading ${file.name}`
389
+ }) : status === "error" ? /* @__PURE__ */ jsx("span", {
390
+ className: itemErrorClasses,
391
+ children: error ?? "Upload failed"
392
+ }) : /* @__PURE__ */ jsx("span", {
393
+ className: itemMetaClasses,
394
+ children: formatFileSize(file.size)
395
+ })]
396
+ }),
397
+ removable && /* @__PURE__ */ jsx(Button, {
398
+ iconOnly: true,
399
+ size: "sm",
400
+ variant: "ghost",
401
+ type: "button",
402
+ "aria-label": removeLabel ?? `Remove ${file.name}`,
403
+ disabled: context.disabled,
404
+ onClick: handleRemove,
405
+ className: "ms-auto shrink-0 self-start",
406
+ children: /* @__PURE__ */ jsx(XIcon, {})
407
+ })
408
+ ] })
409
+ });
410
+ });
411
+ FileUploadItem.displayName = "FileUploadItem";
412
+ function FileUploadThumbnail({ file }) {
413
+ const [url, setUrl] = useState(null);
414
+ useEffect(() => {
415
+ if (typeof URL === "undefined" || !file.type.startsWith("image/")) {
416
+ setUrl(null);
417
+ return;
418
+ }
419
+ const objectUrl = URL.createObjectURL(file);
420
+ setUrl(objectUrl);
421
+ return () => URL.revokeObjectURL(objectUrl);
422
+ }, [file]);
423
+ if (url != null) return /* @__PURE__ */ jsx("img", {
424
+ src: url,
425
+ alt: "",
426
+ "aria-hidden": "true",
427
+ className: "size-10 rounded-md object-cover",
428
+ draggable: false
429
+ });
430
+ return /* @__PURE__ */ jsx("span", {
431
+ className: "flex size-10 items-center justify-center rounded-md bg-muted text-muted-foreground",
432
+ children: /* @__PURE__ */ jsx(FileIcon, {
433
+ className: "size-5",
434
+ "aria-hidden": "true"
435
+ })
436
+ });
437
+ }
438
+ function UploadIcon(props) {
439
+ return /* @__PURE__ */ jsxs("svg", {
440
+ viewBox: "0 0 24 24",
441
+ fill: "none",
442
+ stroke: "currentColor",
443
+ strokeWidth: 2,
444
+ strokeLinecap: "round",
445
+ strokeLinejoin: "round",
446
+ ...props,
447
+ children: [
448
+ /* @__PURE__ */ jsx("path", { d: "M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" }),
449
+ /* @__PURE__ */ jsx("path", { d: "m17 8-5-5-5 5" }),
450
+ /* @__PURE__ */ jsx("path", { d: "M12 3v12" })
451
+ ]
452
+ });
453
+ }
454
+ function FileIcon(props) {
455
+ return /* @__PURE__ */ jsxs("svg", {
456
+ viewBox: "0 0 24 24",
457
+ fill: "none",
458
+ stroke: "currentColor",
459
+ strokeWidth: 2,
460
+ strokeLinecap: "round",
461
+ strokeLinejoin: "round",
462
+ ...props,
463
+ children: [/* @__PURE__ */ jsx("path", { d: "M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7z" }), /* @__PURE__ */ jsx("path", { d: "M14 2v4a2 2 0 0 0 2 2h4" })]
464
+ });
465
+ }
466
+ function XIcon(props) {
467
+ return /* @__PURE__ */ jsx("svg", {
468
+ viewBox: "0 0 24 24",
469
+ fill: "none",
470
+ stroke: "currentColor",
471
+ strokeWidth: 2,
472
+ strokeLinecap: "round",
473
+ strokeLinejoin: "round",
474
+ ...props,
475
+ children: /* @__PURE__ */ jsx("path", { d: "M18 6 6 18M6 6l12 12" })
476
+ });
477
+ }
478
+ function CheckCircleIcon(props) {
479
+ return /* @__PURE__ */ jsxs("svg", {
480
+ viewBox: "0 0 24 24",
481
+ fill: "none",
482
+ stroke: "currentColor",
483
+ strokeWidth: 2,
484
+ strokeLinecap: "round",
485
+ strokeLinejoin: "round",
486
+ ...props,
487
+ children: [/* @__PURE__ */ jsx("circle", {
488
+ cx: "12",
489
+ cy: "12",
490
+ r: "10"
491
+ }), /* @__PURE__ */ jsx("path", { d: "m9 12 2 2 4-4" })]
492
+ });
493
+ }
494
+ function AlertCircleIcon(props) {
495
+ return /* @__PURE__ */ jsxs("svg", {
496
+ viewBox: "0 0 24 24",
497
+ fill: "none",
498
+ stroke: "currentColor",
499
+ strokeWidth: 2,
500
+ strokeLinecap: "round",
501
+ strokeLinejoin: "round",
502
+ ...props,
503
+ children: [
504
+ /* @__PURE__ */ jsx("circle", {
505
+ cx: "12",
506
+ cy: "12",
507
+ r: "10"
508
+ }),
509
+ /* @__PURE__ */ jsx("path", { d: "M12 8v4" }),
510
+ /* @__PURE__ */ jsx("path", { d: "M12 16h.01" })
511
+ ]
512
+ });
513
+ }
514
+ //#endregion
515
+ export { FileUpload, FileUploadDropzone, FileUploadItem, FileUploadList, FileUploadTrigger, fileUploadDropzoneVariants, formatFileSize, useFileUpload };
@@ -0,0 +1,2 @@
1
+ import { FileUpload, FileUploadDropzone, FileUploadDropzoneProps, FileUploadDropzoneVariantsOptions, FileUploadHandle, FileUploadItem, FileUploadItemProps, FileUploadItemStatus, FileUploadList, FileUploadListProps, FileUploadProps, FileUploadRejection, FileUploadRejectionCode, FileUploadRenderState, FileUploadTrigger, FileUploadTriggerProps, fileUploadDropzoneVariants, formatFileSize, useFileUpload } from "./FileUpload.js";
2
+ export { FileUpload, FileUploadDropzone, type FileUploadDropzoneProps, type FileUploadDropzoneVariantsOptions, type FileUploadHandle, FileUploadItem, type FileUploadItemProps, type FileUploadItemStatus, FileUploadList, type FileUploadListProps, type FileUploadProps, type FileUploadRejection, type FileUploadRejectionCode, type FileUploadRenderState, FileUploadTrigger, type FileUploadTriggerProps, fileUploadDropzoneVariants, formatFileSize, useFileUpload };
@@ -0,0 +1,2 @@
1
+ import { FileUpload, FileUploadDropzone, FileUploadItem, FileUploadList, FileUploadTrigger, fileUploadDropzoneVariants, formatFileSize, useFileUpload } from "./FileUpload.js";
2
+ export { FileUpload, FileUploadDropzone, FileUploadItem, FileUploadList, FileUploadTrigger, fileUploadDropzoneVariants, formatFileSize, useFileUpload };
@@ -1,6 +1,6 @@
1
1
  import { cn } from "../../lib/cn.js";
2
2
  import { createContext, forwardRef, useContext, useEffect, useMemo, useState } from "react";
3
- import { Fragment, jsx, jsxs } from "react/jsx-runtime";
3
+ import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
4
4
  import { useRender } from "@base-ui/react/use-render";
5
5
  //#region src/components/Stepper/Stepper.tsx
6
6
  function CheckIcon(props) {
@@ -272,7 +272,7 @@ const StepperIndicator = forwardRef(({ children, render, className, ...rest }, r
272
272
  props: {
273
273
  ...stepStateAttributes(item, orientation),
274
274
  ...rest,
275
- children: /* @__PURE__ */ jsxs(Fragment, { children: [children ?? glyph, statusLabel != null && /* @__PURE__ */ jsx("span", {
275
+ children: /* @__PURE__ */ jsxs(Fragment$1, { children: [children ?? glyph, statusLabel != null && /* @__PURE__ */ jsx("span", {
276
276
  className: "sr-only",
277
277
  children: statusLabel
278
278
  })] }),
@@ -1,6 +1,6 @@
1
1
  import { cn } from "../../lib/cn.js";
2
2
  import { forwardRef, useId } from "react";
3
- import { Fragment, jsx, jsxs } from "react/jsx-runtime";
3
+ import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
4
4
  import { Switch } from "@base-ui/react/switch";
5
5
  //#region src/components/Switch/Switch.tsx
6
6
  const trackClasses = "inline-flex shrink-0 cursor-pointer select-none items-center rounded-full border border-border bg-muted p-px transition-colors duration-150 hover:border-slate-300 hover:bg-slate-200 data-checked:border-primary data-checked:bg-primary hover:data-checked:border-primary-700 hover:data-checked:bg-primary-700 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring aria-invalid:border-destructive data-invalid:border-destructive aria-invalid:focus-visible:outline-destructive data-invalid:focus-visible:outline-destructive data-readonly:cursor-default disabled:pointer-events-none disabled:opacity-50 data-disabled:pointer-events-none data-disabled:opacity-50";
@@ -74,7 +74,7 @@ const Switch$1 = forwardRef(({ size = "md", label, description, labelPosition =
74
74
  className: cn("inline-flex max-w-full flex-col gap-1 font-sans", "has-data-disabled:pointer-events-none has-data-disabled:opacity-50"),
75
75
  children: [hasLabel ? /* @__PURE__ */ jsx("label", {
76
76
  className: cn("inline-flex cursor-pointer items-start gap-2", "has-data-readonly:cursor-default", labelPosition === "start" && "justify-between"),
77
- children: labelPosition === "start" ? /* @__PURE__ */ jsxs(Fragment, { children: [labelText, control] }) : /* @__PURE__ */ jsxs(Fragment, { children: [control, labelText] })
77
+ children: labelPosition === "start" ? /* @__PURE__ */ jsxs(Fragment$1, { children: [labelText, control] }) : /* @__PURE__ */ jsxs(Fragment$1, { children: [control, labelText] })
78
78
  }) : control, description != null && /* @__PURE__ */ jsx("span", {
79
79
  id: descriptionId,
80
80
  className: cn("text-muted-foreground", descriptionTextClasses[size], labelPosition === "end" && descriptionIndentClasses[size]),
@@ -1,7 +1,7 @@
1
1
  import { ComponentPropsWithoutRef, ReactNode } from "react";
2
2
  import { useRender } from "@base-ui/react/use-render";
3
3
  //#region src/components/SystemState/SystemState.d.ts
4
- type SystemStateVariant = "first-use" | "empty" | "no-results" | "completed" | "setup-required" | "sign-in-required" | "access-denied" | "feature-locked" | "unsupported" | "not-found" | "resource-unavailable" | "conflict" | "offline" | "network-error" | "timeout" | "rate-limited" | "maintenance" | "service-unavailable" | "server-error";
4
+ type SystemStateVariant = "first-use" | "empty" | "no-results" | "completed" | "upload" | "upload-error" | "setup-required" | "sign-in-required" | "access-denied" | "feature-locked" | "unsupported" | "not-found" | "resource-unavailable" | "conflict" | "offline" | "network-error" | "timeout" | "rate-limited" | "maintenance" | "service-unavailable" | "server-error";
5
5
  type SystemStateScope = "inline" | "full";
6
6
  type SystemStateBackground = "transparent" | "background" | "surface" | "muted";
7
7
  type SystemStateTitleLevel = 1 | 2 | 3 | 4 | 5 | 6;
@@ -62,8 +62,9 @@ interface SystemStateProps extends Omit<ComponentPropsWithoutRef<"div">, "classN
62
62
  className?: string;
63
63
  }
64
64
  /**
65
- * Communicates a non-ideal or milestone state — empty, error, restricted, or
66
- * completed — with a brand illustration, copy, and optional recovery actions.
65
+ * Communicates a non-ideal or milestone state — empty, error, restricted,
66
+ * completed, or a file-upload prompt — with a brand illustration, copy, and
67
+ * optional recovery actions.
67
68
  */
68
69
  declare const SystemState: import("react").ForwardRefExoticComponent<SystemStateProps & import("react").RefAttributes<HTMLDivElement>>;
69
70
  //#endregion
@@ -1,6 +1,6 @@
1
1
  import { cn } from "../../lib/cn.js";
2
2
  import { forwardRef } from "react";
3
- import { Fragment, jsx, jsxs } from "react/jsx-runtime";
3
+ import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
4
4
  import { useRender } from "@base-ui/react/use-render";
5
5
  import accessDeniedImage from "../../assets/system-states/access-denied.png";
6
6
  import completedImage from "../../assets/system-states/completed.png";
@@ -32,6 +32,16 @@ const variantDefaults = {
32
32
  title: "All done",
33
33
  description: "Everything here is taken care of. Nice work."
34
34
  },
35
+ upload: {
36
+ image: firstUseImage,
37
+ title: "Drag and drop files here",
38
+ description: "or browse to choose files from your device."
39
+ },
40
+ "upload-error": {
41
+ image: setupRequiredImage,
42
+ title: "Upload failed",
43
+ description: "Something went wrong while uploading. Check your files and try again."
44
+ },
35
45
  "setup-required": {
36
46
  image: setupRequiredImage,
37
47
  title: "Finish setting up",
@@ -132,8 +142,9 @@ const descriptionClasses = {
132
142
  full: "font-sans text-base text-muted-foreground"
133
143
  };
134
144
  /**
135
- * Communicates a non-ideal or milestone state — empty, error, restricted, or
136
- * completed — with a brand illustration, copy, and optional recovery actions.
145
+ * Communicates a non-ideal or milestone state — empty, error, restricted,
146
+ * completed, or a file-upload prompt — with a brand illustration, copy, and
147
+ * optional recovery actions.
137
148
  */
138
149
  const SystemState = forwardRef(({ variant, title, description, action, secondaryAction, scope = "inline", bg = "transparent", titleLevel, render, className, ...rest }, ref) => {
139
150
  const defaults = variantDefaults[variant];
@@ -153,7 +164,7 @@ const SystemState = forwardRef(({ variant, title, description, action, secondary
153
164
  },
154
165
  props: {
155
166
  className: cn(rootClasses, scopeClasses[scope], backgroundClasses[bg], className),
156
- children: /* @__PURE__ */ jsxs(Fragment, { children: [
167
+ children: /* @__PURE__ */ jsxs(Fragment$1, { children: [
157
168
  /* @__PURE__ */ jsx("img", {
158
169
  src: defaults.image,
159
170
  alt: "",