@vryzel/file-next-ui 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/dist/index.cjs ADDED
@@ -0,0 +1,2412 @@
1
+ 'use strict';
2
+
3
+ var react = require('react');
4
+ var fileNextHeadless = require('@vryzel/file-next-headless');
5
+ var lucideReact = require('lucide-react');
6
+ var clsx = require('clsx');
7
+ var tailwindMerge = require('tailwind-merge');
8
+ var jsxRuntime = require('react/jsx-runtime');
9
+
10
+ // src/file-explorer.tsx
11
+ function cn(...inputs) {
12
+ return tailwindMerge.twMerge(clsx.clsx(inputs));
13
+ }
14
+ function Breadcrumbs(props) {
15
+ const { segments, onNavigate, className } = props;
16
+ return /* @__PURE__ */ jsxRuntime.jsx("nav", { "aria-label": "Breadcrumb", className: cn("flex items-center gap-1 text-sm", className), children: /* @__PURE__ */ jsxRuntime.jsx("ol", { className: "flex flex-wrap items-center gap-1", children: segments.map((segment, index) => {
17
+ const isLast = index === segments.length - 1;
18
+ return /* @__PURE__ */ jsxRuntime.jsx("li", { className: "flex items-center gap-1", children: isLast ? /* @__PURE__ */ jsxRuntime.jsx("span", { "aria-current": "page", className: "font-medium text-foreground", children: segment.name }) : /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
19
+ /* @__PURE__ */ jsxRuntime.jsx(
20
+ "button",
21
+ {
22
+ type: "button",
23
+ onClick: () => onNavigate?.(segment),
24
+ className: "rounded-sm text-muted-foreground hover:text-foreground focus:outline-none focus-visible:ring-2 focus-visible:ring-ring",
25
+ children: segment.name
26
+ }
27
+ ),
28
+ /* @__PURE__ */ jsxRuntime.jsx(lucideReact.ChevronRight, { "aria-hidden": "true", className: "size-3.5 text-muted-foreground" })
29
+ ] }) }, segment.id);
30
+ }) }) });
31
+ }
32
+ var FILE_ICON_MAP = {
33
+ "text/plain": lucideReact.FileText,
34
+ "text/csv": lucideReact.FileText,
35
+ "text/html": lucideReact.FileCode,
36
+ "application/pdf": lucideReact.FileText,
37
+ "application/json": lucideReact.FileCode,
38
+ "application/xml": lucideReact.FileCode,
39
+ "application/zip": lucideReact.Archive,
40
+ "application/gzip": lucideReact.Archive,
41
+ "application/x-tar": lucideReact.Archive,
42
+ "application/x-7z-compressed": lucideReact.Archive,
43
+ "image/png": lucideReact.FileImage,
44
+ "image/jpeg": lucideReact.FileImage,
45
+ "image/gif": lucideReact.FileImage,
46
+ "image/webp": lucideReact.FileImage,
47
+ "image/svg+xml": lucideReact.FileImage,
48
+ "audio/mpeg": lucideReact.FileAudio,
49
+ "audio/wav": lucideReact.FileAudio,
50
+ "video/mp4": lucideReact.FileVideo,
51
+ "video/webm": lucideReact.FileVideo,
52
+ "text/javascript": lucideReact.FileCode,
53
+ "audio/mp4": lucideReact.Music
54
+ };
55
+ function resolveFileIcon(mimeType) {
56
+ if (!mimeType) return lucideReact.File;
57
+ if (FILE_ICON_MAP[mimeType]) return FILE_ICON_MAP[mimeType];
58
+ const prefix = mimeType.split("/")[0];
59
+ if (prefix === "image") return lucideReact.FileImage;
60
+ if (prefix === "audio") return lucideReact.FileAudio;
61
+ if (prefix === "video") return lucideReact.FileVideo;
62
+ if (prefix === "text") return lucideReact.FileText;
63
+ return lucideReact.File;
64
+ }
65
+ function FileIcon(props) {
66
+ const { kind, mimeType, className } = props;
67
+ const Icon = kind === "folder" ? lucideReact.Folder : resolveFileIcon(mimeType);
68
+ return /* @__PURE__ */ jsxRuntime.jsx(
69
+ Icon,
70
+ {
71
+ "aria-hidden": "true",
72
+ className: cn(
73
+ kind === "folder" ? "text-primary" : "text-muted-foreground",
74
+ "size-5 shrink-0",
75
+ className
76
+ )
77
+ }
78
+ );
79
+ }
80
+ var defaultLabels = {
81
+ loading: "Loading\u2026",
82
+ emptyFolder: "This folder is empty",
83
+ emptyTrash: "Trash is empty",
84
+ noMatches: "No matches",
85
+ dropHint: "Drop files here or use Upload.",
86
+ trashRetention: "Deleted files stay here until you purge them.",
87
+ items: "items",
88
+ inTrash: "in trash",
89
+ loadMore: "Load more",
90
+ search: "Search files",
91
+ upload: "Upload",
92
+ newFolder: "New folder",
93
+ folderName: "Folder name",
94
+ folderNameHint: "Name the folder.",
95
+ folderExists: "A folder with that name already exists.",
96
+ folderCreateFailed: "Could not create the folder.",
97
+ createFolder: "Create",
98
+ cancel: "Cancel",
99
+ rename: "Rename",
100
+ renameHint: "Enter a new name.",
101
+ viewOptions: "View options",
102
+ listView: "List view",
103
+ gridView: "Grid view",
104
+ sortName: "Name",
105
+ sortSize: "Size",
106
+ sortModified: "Modified",
107
+ sortKind: "Kind",
108
+ trash: "Trash",
109
+ folder: "Folder",
110
+ inUse: "In use",
111
+ itemActions: "Item actions",
112
+ preview: "Preview",
113
+ download: "Download",
114
+ copy: "Copy",
115
+ paste: "Paste",
116
+ copied: "copied",
117
+ copyShare: "Copy share link",
118
+ share: "Share",
119
+ restore: "Restore",
120
+ moveToTrash: "Move to trash",
121
+ deleteForever: "Delete forever",
122
+ confirmDelete: "Move \u201C{name}\u201D to trash?",
123
+ confirmDeleteMany: "Move {count} items to trash?",
124
+ confirmPurge: "Delete \u201C{name}\u201D forever? This cannot be undone.",
125
+ confirmPurgeMany: "Delete {count} items forever? This cannot be undone.",
126
+ deleting: "Deleting\u2026",
127
+ selectedCount: "selected",
128
+ clearSelection: "Clear selection",
129
+ clearClipboard: "Clear clipboard",
130
+ uploadStatus: "uploading",
131
+ dismissUploads: "Dismiss uploads",
132
+ uploadQueued: "Queued",
133
+ uploadDone: "Done",
134
+ uploadFailed: "Failed",
135
+ unknownType: "Unknown type",
136
+ previewUnavailable: "Preview is not available for this file."
137
+ };
138
+ var LabelsContext = react.createContext(defaultLabels);
139
+ function ExplorerLabelsProvider({
140
+ labels,
141
+ children
142
+ }) {
143
+ const value = { ...defaultLabels, ...labels };
144
+ return react.createElement(LabelsContext.Provider, { value }, children);
145
+ }
146
+ function useExplorerLabels() {
147
+ return react.useContext(LabelsContext);
148
+ }
149
+ function formatLabel(template, vars) {
150
+ return template.replace(/\{(\w+)\}/g, (_, key) => String(vars[key] ?? ""));
151
+ }
152
+ var LONG_PRESS_MS = 450;
153
+ var MOVE_CANCEL_PX = 10;
154
+ function useCoarsePointer() {
155
+ const [coarse, setCoarse] = react.useState(false);
156
+ react.useEffect(() => {
157
+ if (typeof window.matchMedia !== "function") return;
158
+ const mql = window.matchMedia("(pointer: coarse)");
159
+ const sync = () => setCoarse(mql.matches);
160
+ sync();
161
+ mql.addEventListener("change", sync);
162
+ return () => mql.removeEventListener("change", sync);
163
+ }, []);
164
+ return coarse;
165
+ }
166
+ function useExplorerItemPointer(opts) {
167
+ const timer = react.useRef(0);
168
+ const start = react.useRef({ x: 0, y: 0 });
169
+ const didLongPress = react.useRef(false);
170
+ const detach = react.useRef(null);
171
+ const { file, coarse, onSelect, onActivate } = opts;
172
+ const clear = react.useCallback(() => {
173
+ if (timer.current) {
174
+ window.clearTimeout(timer.current);
175
+ timer.current = 0;
176
+ }
177
+ detach.current?.();
178
+ detach.current = null;
179
+ }, []);
180
+ react.useEffect(() => () => clear(), [clear]);
181
+ const onPointerDown = react.useCallback(
182
+ (event) => {
183
+ if (!coarse || event.pointerType !== "touch") return;
184
+ didLongPress.current = false;
185
+ start.current = { x: event.clientX, y: event.clientY };
186
+ clear();
187
+ timer.current = window.setTimeout(() => {
188
+ didLongPress.current = true;
189
+ onSelect(file, { shiftKey: false });
190
+ }, LONG_PRESS_MS);
191
+ const onMove = (move) => {
192
+ const dx = move.clientX - start.current.x;
193
+ const dy = move.clientY - start.current.y;
194
+ if (dx * dx + dy * dy > MOVE_CANCEL_PX * MOVE_CANCEL_PX) clear();
195
+ };
196
+ const onEnd = () => clear();
197
+ window.addEventListener("pointermove", onMove);
198
+ window.addEventListener("pointerup", onEnd);
199
+ window.addEventListener("pointercancel", onEnd);
200
+ detach.current = () => {
201
+ window.removeEventListener("pointermove", onMove);
202
+ window.removeEventListener("pointerup", onEnd);
203
+ window.removeEventListener("pointercancel", onEnd);
204
+ };
205
+ },
206
+ [clear, coarse, file, onSelect]
207
+ );
208
+ const onClick = react.useCallback(
209
+ (event) => {
210
+ if (event.detail > 1) return;
211
+ event.stopPropagation();
212
+ if (didLongPress.current) {
213
+ didLongPress.current = false;
214
+ return;
215
+ }
216
+ if (coarse) {
217
+ onActivate(file);
218
+ return;
219
+ }
220
+ onSelect(file, event);
221
+ },
222
+ [coarse, file, onActivate, onSelect]
223
+ );
224
+ const onDoubleClick = react.useCallback(() => {
225
+ if (!coarse) onActivate(file);
226
+ }, [coarse, file, onActivate]);
227
+ return { onPointerDown, onClick, onDoubleClick };
228
+ }
229
+ var ROW_GRID = "grid-cols-[2.25rem_1.5rem_minmax(12rem,1fr)_5rem_2.75rem] sm:grid-cols-[2.25rem_1.25rem_minmax(0,1fr)_5rem_7.5rem]";
230
+ var DRAG_MIME = "application/x-file-next-id";
231
+ function ExplorerListView(props) {
232
+ const {
233
+ files,
234
+ protectedIds,
235
+ selectedIds,
236
+ onSelect,
237
+ onActivate,
238
+ onContextMenu,
239
+ onDragStart,
240
+ onDragEnd,
241
+ onDrop,
242
+ draggingId,
243
+ draggingIds,
244
+ dropTargetId,
245
+ onDragOverRow,
246
+ sortKey,
247
+ sortDirection,
248
+ onSortChange,
249
+ className
250
+ } = props;
251
+ const labels = useExplorerLabels();
252
+ const coarse = useCoarsePointer();
253
+ return /* @__PURE__ */ jsxRuntime.jsx("div", { className: cn("overflow-x-auto overscroll-x-contain", className), children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "min-w-[32rem] sm:min-w-0", children: [
254
+ /* @__PURE__ */ jsxRuntime.jsxs(
255
+ "div",
256
+ {
257
+ role: "row",
258
+ className: cn(
259
+ "grid items-center gap-3 border-b border-border px-3 py-2 font-mono text-[10px] font-medium uppercase tracking-[0.12em] text-muted-foreground",
260
+ ROW_GRID
261
+ ),
262
+ children: [
263
+ /* @__PURE__ */ jsxRuntime.jsx("span", { "aria-hidden": "true" }),
264
+ /* @__PURE__ */ jsxRuntime.jsx("span", { "aria-hidden": "true" }),
265
+ /* @__PURE__ */ jsxRuntime.jsx(SortHeader, { col: "name", label: labels.sortName, sortKey, sortDirection, onSort: onSortChange }),
266
+ /* @__PURE__ */ jsxRuntime.jsx(SortHeader, { col: "size", label: labels.sortSize, sortKey, sortDirection, onSort: onSortChange, className: "justify-end text-right" }),
267
+ coarse ? /* @__PURE__ */ jsxRuntime.jsx("span", { "aria-hidden": "true" }) : /* @__PURE__ */ jsxRuntime.jsx(SortHeader, { col: "modified", label: labels.sortModified, sortKey, sortDirection, onSort: onSortChange, className: "hidden sm:flex" })
268
+ ]
269
+ }
270
+ ),
271
+ /* @__PURE__ */ jsxRuntime.jsx("ul", { role: "list", children: files.map((file, idx) => /* @__PURE__ */ jsxRuntime.jsx(
272
+ ExplorerListRow,
273
+ {
274
+ file,
275
+ idx,
276
+ selected: selectedIds.has(file.id),
277
+ dragging: draggingIds?.has(file.id) ?? file.id === draggingId,
278
+ dropTarget: file.id === dropTargetId && file.kind === "folder",
279
+ isProtected: protectedIds?.has(file.id) ?? false,
280
+ draggingId,
281
+ dropTargetId,
282
+ coarse,
283
+ onSelect,
284
+ onActivate,
285
+ onContextMenu,
286
+ onDragStart,
287
+ onDragEnd,
288
+ onDrop,
289
+ onDragOverRow
290
+ },
291
+ file.id
292
+ )) })
293
+ ] }) });
294
+ }
295
+ function ExplorerListRow({
296
+ file,
297
+ idx,
298
+ selected,
299
+ dragging,
300
+ dropTarget,
301
+ isProtected,
302
+ draggingId,
303
+ dropTargetId,
304
+ coarse,
305
+ onSelect,
306
+ onActivate,
307
+ onContextMenu,
308
+ onDragStart,
309
+ onDragEnd,
310
+ onDrop,
311
+ onDragOverRow
312
+ }) {
313
+ const labels = useExplorerLabels();
314
+ const isFolder = file.kind === "folder";
315
+ const pointer = useExplorerItemPointer({ file, coarse, onSelect, onActivate });
316
+ return /* @__PURE__ */ jsxRuntime.jsxs(
317
+ "li",
318
+ {
319
+ role: "row",
320
+ "data-file-id": file.id,
321
+ "aria-selected": selected,
322
+ draggable: !coarse,
323
+ onDragStart: (e) => {
324
+ e.dataTransfer.setData(DRAG_MIME, file.id);
325
+ e.dataTransfer.effectAllowed = "move";
326
+ onDragStart(file.id);
327
+ },
328
+ onDragEnd,
329
+ onPointerDown: pointer.onPointerDown,
330
+ onClick: pointer.onClick,
331
+ onDoubleClick: pointer.onDoubleClick,
332
+ onKeyDown: (e) => {
333
+ if (e.key === "Enter") {
334
+ e.preventDefault();
335
+ onActivate(file);
336
+ }
337
+ },
338
+ onContextMenu: onContextMenu ? (e) => {
339
+ e.preventDefault();
340
+ e.stopPropagation();
341
+ if (coarse) return;
342
+ onContextMenu(file, e);
343
+ } : void 0,
344
+ onDragOver: (e) => {
345
+ if (!isFolder || !draggingId) return;
346
+ e.preventDefault();
347
+ e.dataTransfer.dropEffect = "move";
348
+ if (dropTargetId !== file.id) onDragOverRow(file.id);
349
+ },
350
+ onDragLeave: () => {
351
+ if (dropTargetId === file.id) onDragOverRow(null);
352
+ },
353
+ onDrop: (e) => {
354
+ if (!isFolder) return;
355
+ e.preventDefault();
356
+ const draggedId = e.dataTransfer.getData(DRAG_MIME);
357
+ if (draggedId && draggedId !== file.id) onDrop(file.id);
358
+ else onDragEnd();
359
+ },
360
+ tabIndex: 0,
361
+ className: cn(
362
+ "group grid items-center gap-3 border-b border-border/60 px-3 py-3.5 text-sm outline-none last:border-b-0 sm:py-2",
363
+ "touch-manipulation select-none [-webkit-touch-callout:none]",
364
+ ROW_GRID,
365
+ "hover:bg-muted focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-inset",
366
+ selected && "bg-muted",
367
+ dragging && "opacity-50",
368
+ dropTarget && "ring-2 ring-primary ring-inset"
369
+ ),
370
+ children: [
371
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "font-mono text-[10px] uppercase tracking-[0.12em] text-muted-foreground tabular-nums", children: String(idx + 1).padStart(2, "0") }),
372
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: isFolder ? "text-primary" : "text-muted-foreground", children: /* @__PURE__ */ jsxRuntime.jsx(FileIcon, { kind: file.kind, mimeType: file.mimeType, className: "size-5 text-current sm:size-4" }) }),
373
+ /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "flex min-w-0 items-center gap-1.5", children: [
374
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: cn("truncate", isFolder && "text-primary"), children: file.name }),
375
+ isProtected ? /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "flex shrink-0 items-center gap-0.5 rounded-[10px] bg-primary/10 px-1.5 text-[10px] text-primary", children: [
376
+ /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Link2, { "aria-hidden": "true", className: "size-2.5" }),
377
+ labels.inUse
378
+ ] }) : null
379
+ ] }),
380
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "truncate text-right font-mono text-[10px] uppercase tracking-[0.12em] text-muted-foreground tabular-nums", children: formatSize(file.size) }),
381
+ coarse ? /* @__PURE__ */ jsxRuntime.jsx(
382
+ "button",
383
+ {
384
+ type: "button",
385
+ "aria-label": labels.itemActions,
386
+ className: "inline-flex size-11 items-center justify-center rounded-[10px] text-muted-foreground hover:bg-muted",
387
+ onPointerDown: (event) => event.stopPropagation(),
388
+ onClick: (event) => {
389
+ event.stopPropagation();
390
+ onContextMenu?.(file, event);
391
+ },
392
+ children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.MoreHorizontal, { "aria-hidden": "true", className: "size-5" })
393
+ }
394
+ ) : /* @__PURE__ */ jsxRuntime.jsx("span", { className: "hidden truncate font-mono text-[10px] uppercase tracking-[0.12em] text-muted-foreground tabular-nums sm:block", children: formatDate(file.updatedAt) })
395
+ ]
396
+ }
397
+ );
398
+ }
399
+ function SortHeader({
400
+ col,
401
+ label,
402
+ sortKey,
403
+ sortDirection,
404
+ onSort,
405
+ className
406
+ }) {
407
+ const active = sortKey === col;
408
+ return /* @__PURE__ */ jsxRuntime.jsxs(
409
+ "button",
410
+ {
411
+ type: "button",
412
+ role: "columnheader",
413
+ "aria-sort": active ? sortDirection === "asc" ? "ascending" : "descending" : "none",
414
+ onClick: onSort ? () => onSort(col) : void 0,
415
+ className: cn(
416
+ "flex items-center gap-1 text-left outline-none",
417
+ onSort ? "cursor-pointer hover:text-foreground" : "cursor-default",
418
+ className
419
+ ),
420
+ children: [
421
+ /* @__PURE__ */ jsxRuntime.jsx("span", { children: label }),
422
+ active ? sortDirection === "asc" ? /* @__PURE__ */ jsxRuntime.jsx(lucideReact.ChevronUp, { "aria-hidden": "true", className: "size-3" }) : /* @__PURE__ */ jsxRuntime.jsx(lucideReact.ChevronDown, { "aria-hidden": "true", className: "size-3" }) : null
423
+ ]
424
+ }
425
+ );
426
+ }
427
+ var formatSize = (bytes) => {
428
+ if (bytes === 0) return "\u2014";
429
+ const units = ["B", "KB", "MB", "GB", "TB"];
430
+ const exp = Math.min(Math.floor(Math.log(bytes) / Math.log(1024)), units.length - 1);
431
+ const value = bytes / Math.pow(1024, exp);
432
+ return `${value.toFixed(value >= 100 || exp === 0 ? 0 : 1)} ${units[exp]}`;
433
+ };
434
+ var formatDate = (d) => d.toLocaleDateString(void 0, { year: "numeric", month: "short", day: "numeric" });
435
+ var EXPLORER_DRAG_MIME = DRAG_MIME;
436
+ function ExplorerGridView(props) {
437
+ const {
438
+ files,
439
+ protectedIds,
440
+ selectedIds,
441
+ onSelect,
442
+ onActivate,
443
+ onContextMenu,
444
+ onDragStart,
445
+ onDragEnd,
446
+ onDrop,
447
+ draggingId,
448
+ draggingIds,
449
+ dropTargetId,
450
+ onDragOverRow,
451
+ className
452
+ } = props;
453
+ const labels = useExplorerLabels();
454
+ const coarse = useCoarsePointer();
455
+ return /* @__PURE__ */ jsxRuntime.jsx("ul", { role: "list", className: cn("grid grid-cols-3 gap-1.5 p-3 sm:grid-cols-5 md:grid-cols-6 lg:grid-cols-8", className), children: files.map((file, idx) => /* @__PURE__ */ jsxRuntime.jsx(
456
+ ExplorerGridItem,
457
+ {
458
+ file,
459
+ idx,
460
+ selected: selectedIds.has(file.id),
461
+ dragging: draggingIds?.has(file.id) ?? file.id === draggingId,
462
+ dropTarget: file.id === dropTargetId && file.kind === "folder",
463
+ isProtected: protectedIds?.has(file.id) ?? false,
464
+ draggingId,
465
+ dropTargetId,
466
+ coarse,
467
+ folderLabel: labels.folder,
468
+ inUseLabel: labels.inUse,
469
+ actionsLabel: labels.itemActions,
470
+ onSelect,
471
+ onActivate,
472
+ onContextMenu,
473
+ onDragStart,
474
+ onDragEnd,
475
+ onDrop,
476
+ onDragOverRow
477
+ },
478
+ file.id
479
+ )) });
480
+ }
481
+ function ExplorerGridItem({
482
+ file,
483
+ idx,
484
+ selected,
485
+ dragging,
486
+ dropTarget,
487
+ isProtected,
488
+ draggingId,
489
+ dropTargetId,
490
+ coarse,
491
+ folderLabel,
492
+ inUseLabel,
493
+ actionsLabel,
494
+ onSelect,
495
+ onActivate,
496
+ onContextMenu,
497
+ onDragStart,
498
+ onDragEnd,
499
+ onDrop,
500
+ onDragOverRow
501
+ }) {
502
+ const pointer = useExplorerItemPointer({ file, coarse, onSelect, onActivate });
503
+ return /* @__PURE__ */ jsxRuntime.jsxs("li", { className: "relative", children: [
504
+ coarse && onContextMenu ? /* @__PURE__ */ jsxRuntime.jsx(
505
+ "button",
506
+ {
507
+ type: "button",
508
+ "aria-label": actionsLabel,
509
+ className: "absolute right-0.5 top-0.5 z-10 inline-flex size-10 items-center justify-center rounded-[10px] text-muted-foreground",
510
+ onPointerDown: (event) => event.stopPropagation(),
511
+ onClick: (event) => {
512
+ event.stopPropagation();
513
+ onContextMenu(file, event);
514
+ },
515
+ children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.MoreHorizontal, { "aria-hidden": "true", className: "size-5" })
516
+ }
517
+ ) : null,
518
+ /* @__PURE__ */ jsxRuntime.jsxs(
519
+ "button",
520
+ {
521
+ type: "button",
522
+ "data-file-id": file.id,
523
+ draggable: !coarse,
524
+ "aria-selected": selected,
525
+ onDragStart: (e) => {
526
+ e.dataTransfer.setData(EXPLORER_DRAG_MIME, file.id);
527
+ e.dataTransfer.effectAllowed = "move";
528
+ onDragStart(file.id);
529
+ },
530
+ onDragEnd,
531
+ onPointerDown: pointer.onPointerDown,
532
+ onClick: pointer.onClick,
533
+ onDoubleClick: pointer.onDoubleClick,
534
+ onKeyDown: (e) => {
535
+ if (e.key === "Enter") {
536
+ e.preventDefault();
537
+ onActivate(file);
538
+ }
539
+ },
540
+ onContextMenu: onContextMenu ? (e) => {
541
+ e.preventDefault();
542
+ e.stopPropagation();
543
+ if (coarse) return;
544
+ onContextMenu(file, e);
545
+ } : void 0,
546
+ onDragOver: (e) => {
547
+ if (file.kind !== "folder" || !draggingId) return;
548
+ e.preventDefault();
549
+ e.dataTransfer.dropEffect = "move";
550
+ if (dropTargetId !== file.id) onDragOverRow(file.id);
551
+ },
552
+ onDragLeave: () => {
553
+ if (dropTargetId === file.id) onDragOverRow(null);
554
+ },
555
+ onDrop: (e) => {
556
+ if (file.kind !== "folder") return;
557
+ e.preventDefault();
558
+ onDrop(file.id);
559
+ },
560
+ className: cn(
561
+ "group relative flex w-full flex-col items-stretch gap-1 overflow-hidden rounded-[10px] border border-border bg-background p-2 text-left outline-none",
562
+ "touch-manipulation select-none hover:bg-muted focus-visible:ring-2 focus-visible:ring-ring",
563
+ selected && "border-primary bg-muted",
564
+ dragging && "opacity-50",
565
+ dropTarget && "ring-2 ring-primary ring-inset"
566
+ ),
567
+ children: [
568
+ /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "flex items-center gap-1 pr-8", children: [
569
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "font-mono text-[9px] uppercase tracking-[0.12em] text-muted-foreground tabular-nums", children: String(idx + 1).padStart(2, "0") }),
570
+ isProtected ? /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "flex items-center gap-0.5 rounded-[10px] bg-primary/10 px-1.5 py-0.5 text-[10px] text-primary", children: [
571
+ /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Link2, { "aria-hidden": "true", className: "size-2.5" }),
572
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "sr-only", children: inUseLabel })
573
+ ] }) : null
574
+ ] }),
575
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: cn("flex items-center justify-center py-1", file.kind === "folder" ? "text-primary" : "text-muted-foreground"), children: /* @__PURE__ */ jsxRuntime.jsx(FileIcon, { kind: file.kind, mimeType: file.mimeType, className: "size-12 text-current" }) }),
576
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: cn("truncate text-[11px]", file.kind === "folder" && "text-primary"), children: file.name }),
577
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "font-mono text-[9px] uppercase tracking-[0.12em] text-muted-foreground", children: file.kind === "folder" ? folderLabel : formatSizeShort(file.size) })
578
+ ]
579
+ }
580
+ )
581
+ ] });
582
+ }
583
+ var formatSizeShort = (bytes) => {
584
+ if (bytes === 0) return "0 B";
585
+ const units = ["B", "KB", "MB", "GB", "TB"];
586
+ const exp = Math.min(Math.floor(Math.log(bytes) / Math.log(1024)), units.length - 1);
587
+ const value = bytes / Math.pow(1024, exp);
588
+ return `${value.toFixed(value >= 10 || exp === 0 ? 0 : 1)} ${units[exp]}`;
589
+ };
590
+ var SORT_KEYS = ["name", "size", "modified", "kind"];
591
+ var btn = "inline-flex h-10 shrink-0 items-center justify-center gap-1.5 rounded-[10px] border border-border bg-background px-2.5 text-xs font-medium text-foreground outline-none hover:bg-muted focus-visible:ring-2 focus-visible:ring-ring disabled:opacity-50 sm:h-8 sm:px-3";
592
+ function ExplorerToolbar(props) {
593
+ const {
594
+ view,
595
+ onViewChange,
596
+ query,
597
+ onQueryChange,
598
+ trashOpen,
599
+ onTrashToggle,
600
+ trashHasItems = false,
601
+ onNewFolder,
602
+ onUpload,
603
+ uploading = false,
604
+ uploadLabel,
605
+ sortKey,
606
+ sortDirection,
607
+ onSortChange,
608
+ className
609
+ } = props;
610
+ const labels = useExplorerLabels();
611
+ const sortLabel = {
612
+ name: labels.sortName,
613
+ size: labels.sortSize,
614
+ modified: labels.sortModified,
615
+ kind: labels.sortKind
616
+ };
617
+ const viewToggle = /* @__PURE__ */ jsxRuntime.jsxs(
618
+ "div",
619
+ {
620
+ role: "toolbar",
621
+ "aria-label": labels.viewOptions,
622
+ className: "inline-flex shrink-0 items-center gap-0.5 rounded-[10px] border border-border bg-background p-0.5",
623
+ children: [
624
+ /* @__PURE__ */ jsxRuntime.jsx(
625
+ "button",
626
+ {
627
+ type: "button",
628
+ "aria-label": labels.listView,
629
+ "aria-pressed": view === "list",
630
+ onClick: () => onViewChange("list"),
631
+ className: cn(
632
+ "inline-flex size-9 items-center justify-center rounded-[8px] outline-none focus-visible:ring-2 focus-visible:ring-ring sm:size-7",
633
+ view === "list" ? "bg-accent text-accent-foreground" : "text-muted-foreground hover:bg-muted"
634
+ ),
635
+ children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.List, { "aria-hidden": "true", className: "size-4" })
636
+ }
637
+ ),
638
+ /* @__PURE__ */ jsxRuntime.jsx(
639
+ "button",
640
+ {
641
+ type: "button",
642
+ "aria-label": labels.gridView,
643
+ "aria-pressed": view === "grid",
644
+ onClick: () => onViewChange("grid"),
645
+ className: cn(
646
+ "inline-flex size-9 items-center justify-center rounded-[8px] outline-none focus-visible:ring-2 focus-visible:ring-ring sm:size-7",
647
+ view === "grid" ? "bg-accent text-accent-foreground" : "text-muted-foreground hover:bg-muted"
648
+ ),
649
+ children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.LayoutGrid, { "aria-hidden": "true", className: "size-4" })
650
+ }
651
+ )
652
+ ]
653
+ }
654
+ );
655
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: cn("flex min-w-0 flex-col gap-2 sm:flex-row sm:items-center sm:justify-between", className), children: [
656
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex min-w-0 items-center gap-2 sm:max-w-xs sm:flex-1", children: [
657
+ onQueryChange ? /* @__PURE__ */ jsxRuntime.jsxs("label", { className: "relative min-w-0 flex-1 sm:min-w-40", children: [
658
+ /* @__PURE__ */ jsxRuntime.jsx(
659
+ lucideReact.Search,
660
+ {
661
+ "aria-hidden": "true",
662
+ className: "pointer-events-none absolute left-2.5 top-1/2 size-4 -translate-y-1/2 text-muted-foreground"
663
+ }
664
+ ),
665
+ /* @__PURE__ */ jsxRuntime.jsx(
666
+ "input",
667
+ {
668
+ type: "search",
669
+ value: query ?? "",
670
+ onChange: (event) => onQueryChange(event.target.value),
671
+ placeholder: labels.search,
672
+ "aria-label": labels.search,
673
+ className: "h-10 w-full rounded-[10px] border border-border bg-background pl-9 pr-2 text-sm outline-none focus-visible:ring-2 focus-visible:ring-ring sm:h-8"
674
+ }
675
+ )
676
+ ] }) : null,
677
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "sm:hidden", children: viewToggle })
678
+ ] }),
679
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex min-w-0 items-center gap-2 sm:ml-auto", children: [
680
+ onUpload ? /* @__PURE__ */ jsxRuntime.jsxs("button", { type: "button", onClick: onUpload, disabled: uploading, "aria-label": uploadLabel ?? labels.upload, className: btn, children: [
681
+ /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Upload, { "aria-hidden": "true", className: "size-4 sm:size-3.5" }),
682
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "hidden sm:inline", children: uploadLabel ?? labels.upload })
683
+ ] }) : null,
684
+ onNewFolder ? /* @__PURE__ */ jsxRuntime.jsxs("button", { type: "button", onClick: onNewFolder, "aria-label": labels.newFolder, className: btn, children: [
685
+ /* @__PURE__ */ jsxRuntime.jsx(lucideReact.FolderPlus, { "aria-hidden": "true", className: "size-4 sm:size-3.5" }),
686
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "hidden sm:inline", children: labels.newFolder })
687
+ ] }) : null,
688
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "inline-flex h-10 min-w-0 flex-1 items-center rounded-[10px] border border-border sm:h-8 sm:max-w-48 sm:flex-none", children: [
689
+ /* @__PURE__ */ jsxRuntime.jsx(
690
+ "select",
691
+ {
692
+ "aria-label": sortLabel[sortKey],
693
+ value: sortKey,
694
+ onChange: (event) => onSortChange(event.target.value),
695
+ className: "h-10 min-w-0 flex-1 appearance-none bg-transparent pl-2.5 pr-1 font-mono text-[10px] uppercase tracking-[0.12em] text-muted-foreground outline-none sm:h-8",
696
+ children: SORT_KEYS.map((key) => /* @__PURE__ */ jsxRuntime.jsx("option", { value: key, children: sortLabel[key] }, key))
697
+ }
698
+ ),
699
+ /* @__PURE__ */ jsxRuntime.jsx(
700
+ "button",
701
+ {
702
+ type: "button",
703
+ "aria-label": `${sortLabel[sortKey]} ${sortDirection}`,
704
+ onClick: () => onSortChange(sortKey),
705
+ className: "inline-flex size-10 shrink-0 items-center justify-center font-mono text-[10px] text-muted-foreground hover:bg-muted sm:size-8",
706
+ children: sortDirection === "asc" ? "\u2191" : "\u2193"
707
+ }
708
+ )
709
+ ] }),
710
+ onTrashToggle ? /* @__PURE__ */ jsxRuntime.jsxs(
711
+ "button",
712
+ {
713
+ type: "button",
714
+ "aria-label": labels.trash,
715
+ "aria-pressed": trashOpen === true,
716
+ onClick: onTrashToggle,
717
+ className: cn(
718
+ "relative inline-flex size-10 shrink-0 items-center justify-center rounded-[10px] border border-border outline-none focus-visible:ring-2 focus-visible:ring-ring sm:size-8",
719
+ trashOpen ? "bg-accent text-accent-foreground" : "text-muted-foreground hover:bg-muted"
720
+ ),
721
+ children: [
722
+ /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Trash, { "aria-hidden": "true", className: "size-4" }),
723
+ trashHasItems ? /* @__PURE__ */ jsxRuntime.jsx("span", { "aria-hidden": "true", className: "absolute top-1 right-1 size-1.5 rounded-full bg-primary" }) : null
724
+ ]
725
+ }
726
+ ) : null,
727
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "hidden sm:block", children: viewToggle })
728
+ ] })
729
+ ] });
730
+ }
731
+
732
+ // src/swallow-click-through.ts
733
+ function swallowClickThrough() {
734
+ const swallow = (event) => {
735
+ event.preventDefault();
736
+ event.stopPropagation();
737
+ window.removeEventListener("click", swallow, true);
738
+ window.removeEventListener("pointerup", swallow, true);
739
+ };
740
+ window.addEventListener("click", swallow, true);
741
+ window.addEventListener("pointerup", swallow, true);
742
+ window.setTimeout(() => {
743
+ window.removeEventListener("click", swallow, true);
744
+ window.removeEventListener("pointerup", swallow, true);
745
+ }, 400);
746
+ }
747
+ function ExplorerContextMenu(props) {
748
+ const {
749
+ target,
750
+ mode = "browse",
751
+ actions,
752
+ protectedItem = false,
753
+ onClose,
754
+ onPreview,
755
+ onDownload,
756
+ onRename,
757
+ onRequestDelete,
758
+ onRequestPurge,
759
+ onCopy,
760
+ onPaste,
761
+ onNewFolder,
762
+ onUpload,
763
+ extraFileAction
764
+ } = props;
765
+ const labels = useExplorerLabels();
766
+ const menuRef = react.useRef(null);
767
+ react.useLayoutEffect(() => {
768
+ const el = menuRef.current;
769
+ if (!el || !target) return;
770
+ const rect = el.getBoundingClientRect();
771
+ const pad = 8;
772
+ const left = Math.min(Math.max(pad, target.x), window.innerWidth - rect.width - pad);
773
+ const top = Math.min(Math.max(pad, target.y), window.innerHeight - rect.height - pad);
774
+ el.style.left = `${left}px`;
775
+ el.style.top = `${top}px`;
776
+ }, [target]);
777
+ react.useEffect(() => {
778
+ if (!target) return;
779
+ const onKey = (event) => {
780
+ if (event.key === "Escape") onClose();
781
+ };
782
+ const onPointer = (event) => {
783
+ if (menuRef.current?.contains(event.target)) return;
784
+ event.preventDefault();
785
+ event.stopPropagation();
786
+ swallowClickThrough();
787
+ onClose();
788
+ };
789
+ window.addEventListener("keydown", onKey);
790
+ window.addEventListener("pointerdown", onPointer, true);
791
+ return () => {
792
+ window.removeEventListener("keydown", onKey);
793
+ window.removeEventListener("pointerdown", onPointer, true);
794
+ };
795
+ }, [onClose, target]);
796
+ if (!target) return null;
797
+ const close = () => {
798
+ swallowClickThrough();
799
+ onClose();
800
+ };
801
+ const itemClass = "flex w-full min-h-11 cursor-pointer items-center gap-2 rounded-[10px] px-3 py-2.5 text-left text-sm outline-none hover:bg-accent disabled:opacity-40 sm:min-h-0 sm:px-2.5 sm:py-1.5";
802
+ return /* @__PURE__ */ jsxRuntime.jsx(
803
+ "div",
804
+ {
805
+ ref: menuRef,
806
+ role: "menu",
807
+ className: "fixed z-50 min-w-48 rounded-[10px] border border-border bg-card p-1 text-card-foreground shadow-xl",
808
+ style: { left: target.x, top: target.y },
809
+ children: target.kind === "blank" ? /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
810
+ onNewFolder ? /* @__PURE__ */ jsxRuntime.jsxs("button", { type: "button", role: "menuitem", className: itemClass, onClick: () => {
811
+ close();
812
+ onNewFolder();
813
+ }, children: [
814
+ /* @__PURE__ */ jsxRuntime.jsx(lucideReact.FolderPlus, { "aria-hidden": "true", className: "size-4" }),
815
+ labels.newFolder
816
+ ] }) : null,
817
+ onUpload ? /* @__PURE__ */ jsxRuntime.jsxs("button", { type: "button", role: "menuitem", className: itemClass, onClick: () => {
818
+ close();
819
+ onUpload();
820
+ }, children: [
821
+ /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Upload, { "aria-hidden": "true", className: "size-4" }),
822
+ labels.upload
823
+ ] }) : null,
824
+ onPaste ? /* @__PURE__ */ jsxRuntime.jsxs("button", { type: "button", role: "menuitem", className: itemClass, onClick: () => {
825
+ close();
826
+ onPaste();
827
+ }, children: [
828
+ /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Copy, { "aria-hidden": "true", className: "size-4" }),
829
+ labels.paste
830
+ ] }) : null
831
+ ] }) : mode === "trash" ? /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
832
+ /* @__PURE__ */ jsxRuntime.jsxs("button", { type: "button", role: "menuitem", className: itemClass, onClick: () => {
833
+ close();
834
+ void actions.restoreNode?.({ id: target.file.id });
835
+ }, children: [
836
+ /* @__PURE__ */ jsxRuntime.jsx(lucideReact.RotateCcw, { "aria-hidden": "true", className: "size-4" }),
837
+ labels.restore
838
+ ] }),
839
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "my-1 h-px bg-border" }),
840
+ /* @__PURE__ */ jsxRuntime.jsxs(
841
+ "button",
842
+ {
843
+ type: "button",
844
+ role: "menuitem",
845
+ className: cn(itemClass, "text-destructive"),
846
+ onClick: () => {
847
+ close();
848
+ if (onRequestPurge) onRequestPurge(target.file);
849
+ else void actions.purgeNode?.({ id: target.file.id });
850
+ },
851
+ children: [
852
+ /* @__PURE__ */ jsxRuntime.jsx(lucideReact.X, { "aria-hidden": "true", className: "size-4" }),
853
+ labels.deleteForever
854
+ ]
855
+ }
856
+ )
857
+ ] }) : /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
858
+ target.file.kind === "file" && onPreview ? /* @__PURE__ */ jsxRuntime.jsxs("button", { type: "button", role: "menuitem", className: itemClass, onClick: () => {
859
+ close();
860
+ onPreview(target.file);
861
+ }, children: [
862
+ /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Eye, { "aria-hidden": "true", className: "size-4" }),
863
+ labels.preview
864
+ ] }) : null,
865
+ target.file.kind === "file" && onDownload ? /* @__PURE__ */ jsxRuntime.jsxs("button", { type: "button", role: "menuitem", className: itemClass, onClick: () => {
866
+ close();
867
+ onDownload(target.file);
868
+ }, children: [
869
+ /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Download, { "aria-hidden": "true", className: "size-4" }),
870
+ labels.download
871
+ ] }) : null,
872
+ target.file.kind === "file" && extraFileAction ? /* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", role: "menuitem", className: itemClass, onClick: () => {
873
+ close();
874
+ extraFileAction.onSelect(target.file);
875
+ }, children: extraFileAction.label }) : null,
876
+ onRename ? /* @__PURE__ */ jsxRuntime.jsxs("button", { type: "button", role: "menuitem", className: itemClass, onClick: () => {
877
+ close();
878
+ onRename(target.file);
879
+ }, children: [
880
+ /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Pencil, { "aria-hidden": "true", className: "size-4" }),
881
+ labels.rename
882
+ ] }) : null,
883
+ onCopy ? /* @__PURE__ */ jsxRuntime.jsxs("button", { type: "button", role: "menuitem", className: itemClass, onClick: () => {
884
+ close();
885
+ onCopy(target.file);
886
+ }, children: [
887
+ /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Copy, { "aria-hidden": "true", className: "size-4" }),
888
+ labels.copy
889
+ ] }) : null,
890
+ onPaste && target.file.kind === "folder" ? /* @__PURE__ */ jsxRuntime.jsxs("button", { type: "button", role: "menuitem", className: itemClass, onClick: () => {
891
+ close();
892
+ onPaste(target.file);
893
+ }, children: [
894
+ /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Copy, { "aria-hidden": "true", className: "size-4" }),
895
+ labels.paste
896
+ ] }) : null,
897
+ actions.createShare ? /* @__PURE__ */ jsxRuntime.jsxs(
898
+ "button",
899
+ {
900
+ type: "button",
901
+ role: "menuitem",
902
+ className: itemClass,
903
+ onClick: () => {
904
+ close();
905
+ void actions.createShare({ id: target.file.id }).then(
906
+ (token) => navigator.clipboard.writeText(token)
907
+ );
908
+ },
909
+ children: [
910
+ /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Link2, { "aria-hidden": "true", className: "size-4" }),
911
+ labels.copyShare
912
+ ]
913
+ }
914
+ ) : null,
915
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "my-1 h-px bg-border" }),
916
+ /* @__PURE__ */ jsxRuntime.jsxs(
917
+ "button",
918
+ {
919
+ type: "button",
920
+ role: "menuitem",
921
+ disabled: protectedItem,
922
+ className: cn(itemClass, "text-destructive"),
923
+ onClick: () => {
924
+ if (protectedItem) return;
925
+ close();
926
+ if (onRequestDelete) onRequestDelete(target.file);
927
+ else void actions.deleteFile({ id: target.file.id });
928
+ },
929
+ children: [
930
+ /* @__PURE__ */ jsxRuntime.jsx(lucideReact.X, { "aria-hidden": "true", className: "size-4" }),
931
+ labels.moveToTrash
932
+ ]
933
+ }
934
+ )
935
+ ] })
936
+ }
937
+ );
938
+ }
939
+ function Overlay({
940
+ open,
941
+ onClose,
942
+ className,
943
+ children
944
+ }) {
945
+ if (!open) return null;
946
+ return /* @__PURE__ */ jsxRuntime.jsx(
947
+ "div",
948
+ {
949
+ className: "fixed inset-0 z-50 flex items-center justify-center bg-black/40 p-4",
950
+ onClick: onClose,
951
+ children: /* @__PURE__ */ jsxRuntime.jsx(
952
+ "div",
953
+ {
954
+ role: "dialog",
955
+ className: cn(
956
+ "w-full max-w-sm rounded-[10px] border border-border bg-card p-4 text-card-foreground shadow-xl",
957
+ className
958
+ ),
959
+ onClick: (event) => event.stopPropagation(),
960
+ children
961
+ }
962
+ )
963
+ }
964
+ );
965
+ }
966
+ function CreateFolderDialog({
967
+ open,
968
+ onOpenChange,
969
+ onCreate,
970
+ title,
971
+ description,
972
+ confirmLabel,
973
+ initialName = ""
974
+ }) {
975
+ const labels = useExplorerLabels();
976
+ const [name, setName] = react.useState(initialName);
977
+ const [saving, setSaving] = react.useState(false);
978
+ const [error, setError] = react.useState(null);
979
+ const inputRef = react.useRef(null);
980
+ react.useEffect(() => {
981
+ if (!open) {
982
+ setSaving(false);
983
+ setError(null);
984
+ return;
985
+ }
986
+ setName(initialName);
987
+ const id = window.setTimeout(() => {
988
+ inputRef.current?.focus();
989
+ inputRef.current?.select();
990
+ }, 0);
991
+ return () => window.clearTimeout(id);
992
+ }, [initialName, open]);
993
+ async function submit() {
994
+ const trimmed = name.trim();
995
+ if (!trimmed || saving) return;
996
+ setSaving(true);
997
+ setError(null);
998
+ try {
999
+ await onCreate(trimmed);
1000
+ onOpenChange(false);
1001
+ } catch (cause) {
1002
+ const code = cause && typeof cause === "object" && "code" in cause ? String(cause.code) : "";
1003
+ setError(code === "Conflict" ? labels.folderExists : labels.folderCreateFailed);
1004
+ } finally {
1005
+ setSaving(false);
1006
+ }
1007
+ }
1008
+ return /* @__PURE__ */ jsxRuntime.jsxs(Overlay, { open, onClose: () => onOpenChange(false), children: [
1009
+ /* @__PURE__ */ jsxRuntime.jsx("h2", { className: "text-base font-semibold", children: title ?? labels.newFolder }),
1010
+ /* @__PURE__ */ jsxRuntime.jsx("p", { className: "mt-1 text-sm text-muted-foreground", children: description ?? labels.folderNameHint }),
1011
+ /* @__PURE__ */ jsxRuntime.jsx(
1012
+ "input",
1013
+ {
1014
+ ref: inputRef,
1015
+ value: name,
1016
+ onChange: (event) => setName(event.target.value),
1017
+ onKeyDown: (event) => {
1018
+ if (event.key === "Enter") {
1019
+ event.preventDefault();
1020
+ void submit();
1021
+ }
1022
+ },
1023
+ placeholder: labels.folderName,
1024
+ "aria-invalid": Boolean(error),
1025
+ className: "mt-3 h-9 w-full rounded-[10px] border border-border bg-background px-3 text-sm outline-none focus-visible:ring-2 focus-visible:ring-ring"
1026
+ }
1027
+ ),
1028
+ error ? /* @__PURE__ */ jsxRuntime.jsx("p", { className: "mt-2 text-sm text-destructive", children: error }) : null,
1029
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "mt-4 flex justify-end gap-2", children: [
1030
+ /* @__PURE__ */ jsxRuntime.jsx(
1031
+ "button",
1032
+ {
1033
+ type: "button",
1034
+ className: "inline-flex h-8 items-center rounded-[10px] border border-border px-3 text-sm",
1035
+ onClick: () => onOpenChange(false),
1036
+ children: labels.cancel
1037
+ }
1038
+ ),
1039
+ /* @__PURE__ */ jsxRuntime.jsx(
1040
+ "button",
1041
+ {
1042
+ type: "button",
1043
+ disabled: !name.trim() || saving,
1044
+ className: "inline-flex h-8 items-center rounded-[10px] bg-primary px-3 text-sm text-primary-foreground disabled:opacity-50",
1045
+ onClick: () => void submit(),
1046
+ children: confirmLabel ?? labels.createFolder
1047
+ }
1048
+ )
1049
+ ] })
1050
+ ] });
1051
+ }
1052
+ function ConfirmDeleteDialog({
1053
+ open,
1054
+ names,
1055
+ permanent = false,
1056
+ onOpenChange,
1057
+ onConfirm
1058
+ }) {
1059
+ const labels = useExplorerLabels();
1060
+ const [saving, setSaving] = react.useState(false);
1061
+ const description = names.length === 1 ? formatLabel(permanent ? labels.confirmPurge : labels.confirmDelete, {
1062
+ name: names[0] ?? ""
1063
+ }) : formatLabel(permanent ? labels.confirmPurgeMany : labels.confirmDeleteMany, {
1064
+ count: names.length
1065
+ });
1066
+ async function submit() {
1067
+ if (saving) return;
1068
+ setSaving(true);
1069
+ try {
1070
+ await onConfirm();
1071
+ onOpenChange(false);
1072
+ } finally {
1073
+ setSaving(false);
1074
+ }
1075
+ }
1076
+ return /* @__PURE__ */ jsxRuntime.jsxs(Overlay, { open, onClose: () => !saving && onOpenChange(false), children: [
1077
+ /* @__PURE__ */ jsxRuntime.jsx("h2", { className: "text-base font-semibold", children: permanent ? labels.deleteForever : labels.moveToTrash }),
1078
+ /* @__PURE__ */ jsxRuntime.jsx("p", { className: "mt-1 text-sm text-muted-foreground", children: description }),
1079
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "mt-4 flex justify-end gap-2", children: [
1080
+ /* @__PURE__ */ jsxRuntime.jsx(
1081
+ "button",
1082
+ {
1083
+ type: "button",
1084
+ disabled: saving,
1085
+ className: "inline-flex h-8 items-center rounded-[10px] border border-border px-3 text-sm",
1086
+ onClick: () => onOpenChange(false),
1087
+ children: labels.cancel
1088
+ }
1089
+ ),
1090
+ /* @__PURE__ */ jsxRuntime.jsx(
1091
+ "button",
1092
+ {
1093
+ type: "button",
1094
+ disabled: saving,
1095
+ className: permanent ? "inline-flex h-8 items-center rounded-[10px] bg-destructive px-3 text-sm text-destructive-foreground" : "inline-flex h-8 items-center rounded-[10px] bg-primary px-3 text-sm text-primary-foreground",
1096
+ onClick: () => void submit(),
1097
+ children: saving ? labels.deleting : permanent ? labels.deleteForever : labels.moveToTrash
1098
+ }
1099
+ )
1100
+ ] })
1101
+ ] });
1102
+ }
1103
+ function ExplorerSelectionToast({
1104
+ files,
1105
+ protectedIds,
1106
+ trashOpen,
1107
+ onDownload,
1108
+ onCopy,
1109
+ onShare,
1110
+ onDelete,
1111
+ onRestore,
1112
+ onPurge,
1113
+ onClear
1114
+ }) {
1115
+ const labels = useExplorerLabels();
1116
+ const [open, setOpen] = react.useState(false);
1117
+ const [compact, setCompact] = react.useState(true);
1118
+ const rootRef = react.useRef(null);
1119
+ react.useEffect(() => {
1120
+ if (typeof window.matchMedia !== "function") return;
1121
+ const mq = window.matchMedia("(max-width: 767px), (pointer: coarse)");
1122
+ const sync = () => setCompact(mq.matches);
1123
+ sync();
1124
+ mq.addEventListener("change", sync);
1125
+ return () => mq.removeEventListener("change", sync);
1126
+ }, []);
1127
+ react.useEffect(() => {
1128
+ if (files.length < 2) setOpen(false);
1129
+ }, [files.length]);
1130
+ react.useEffect(() => {
1131
+ if (!open) return;
1132
+ const onPointer = (event) => {
1133
+ if (rootRef.current?.contains(event.target)) return;
1134
+ event.preventDefault();
1135
+ event.stopPropagation();
1136
+ swallowClickThrough();
1137
+ setOpen(false);
1138
+ };
1139
+ const timer = window.setTimeout(() => {
1140
+ window.addEventListener("pointerdown", onPointer, true);
1141
+ }, 0);
1142
+ return () => {
1143
+ window.clearTimeout(timer);
1144
+ window.removeEventListener("pointerdown", onPointer, true);
1145
+ };
1146
+ }, [open]);
1147
+ if (files.length < 2) return null;
1148
+ return /* @__PURE__ */ jsxRuntime.jsxs(
1149
+ "div",
1150
+ {
1151
+ ref: rootRef,
1152
+ className: cn(
1153
+ "z-50 flex flex-col items-start gap-2",
1154
+ compact ? "fixed bottom-4 left-4" : "fixed right-4 bottom-4 w-[min(22rem,calc(100vw-2rem))]"
1155
+ ),
1156
+ children: [
1157
+ !compact || open ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: "w-[min(18rem,calc(100vw-2rem))] md:w-full", children: /* @__PURE__ */ jsxRuntime.jsx(
1158
+ SelectionActions,
1159
+ {
1160
+ files,
1161
+ protectedIds,
1162
+ trashOpen,
1163
+ onDownload,
1164
+ onCopy,
1165
+ onShare,
1166
+ onDelete,
1167
+ onRestore,
1168
+ onPurge,
1169
+ onClear
1170
+ }
1171
+ ) }) : null,
1172
+ compact ? /* @__PURE__ */ jsxRuntime.jsxs(
1173
+ "button",
1174
+ {
1175
+ type: "button",
1176
+ "aria-expanded": open,
1177
+ "aria-label": `${files.length} ${labels.selectedCount}`,
1178
+ className: "relative inline-flex size-12 items-center justify-center rounded-[10px] border border-border bg-card shadow-xl",
1179
+ onClick: (event) => {
1180
+ event.stopPropagation();
1181
+ setOpen((value) => !value);
1182
+ },
1183
+ children: [
1184
+ /* @__PURE__ */ jsxRuntime.jsx(lucideReact.ListChecks, { "aria-hidden": "true", className: "size-5" }),
1185
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "absolute -right-1 -top-1 min-w-5 rounded-full bg-primary px-1 text-center font-mono text-[10px] leading-5 text-primary-foreground", children: files.length })
1186
+ ]
1187
+ }
1188
+ ) : null
1189
+ ]
1190
+ }
1191
+ );
1192
+ }
1193
+ function SelectionActions({
1194
+ files,
1195
+ protectedIds,
1196
+ trashOpen,
1197
+ onDownload,
1198
+ onCopy,
1199
+ onShare,
1200
+ onDelete,
1201
+ onRestore,
1202
+ onPurge,
1203
+ onClear
1204
+ }) {
1205
+ const labels = useExplorerLabels();
1206
+ const downloadable = files.filter((file) => file.kind === "file");
1207
+ const hasProtected = files.some((file) => protectedIds.has(file.id));
1208
+ const btn2 = "inline-flex h-10 items-center gap-1.5 rounded-[10px] border border-border bg-background px-3 text-xs font-medium hover:bg-muted disabled:opacity-40 sm:h-8 sm:px-2.5";
1209
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { role: "status", className: "w-full rounded-[10px] border border-border bg-card p-3 shadow-xl", children: [
1210
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "mb-2 flex items-center gap-2", children: [
1211
+ /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "font-mono text-[10px] uppercase tracking-[0.12em] text-muted-foreground", children: [
1212
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-primary", children: String(files.length).padStart(2, "0") }),
1213
+ " ",
1214
+ labels.selectedCount
1215
+ ] }),
1216
+ /* @__PURE__ */ jsxRuntime.jsx(
1217
+ "button",
1218
+ {
1219
+ type: "button",
1220
+ "aria-label": labels.clearSelection,
1221
+ className: "ml-auto inline-flex size-7 items-center justify-center rounded-[10px] text-muted-foreground hover:bg-muted",
1222
+ onClick: (event) => {
1223
+ event.stopPropagation();
1224
+ swallowClickThrough();
1225
+ onClear();
1226
+ },
1227
+ children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.X, { "aria-hidden": "true", className: "size-4" })
1228
+ }
1229
+ )
1230
+ ] }),
1231
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex flex-wrap gap-1.5", children: trashOpen ? /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
1232
+ /* @__PURE__ */ jsxRuntime.jsxs("button", { type: "button", className: btn2, onClick: onRestore, children: [
1233
+ /* @__PURE__ */ jsxRuntime.jsx(lucideReact.RotateCcw, { "aria-hidden": "true", className: "size-3.5" }),
1234
+ labels.restore
1235
+ ] }),
1236
+ onPurge ? /* @__PURE__ */ jsxRuntime.jsxs("button", { type: "button", className: cn(btn2, "text-destructive"), onClick: onPurge, children: [
1237
+ /* @__PURE__ */ jsxRuntime.jsx(lucideReact.X, { "aria-hidden": "true", className: "size-3.5" }),
1238
+ labels.deleteForever
1239
+ ] }) : null
1240
+ ] }) : /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
1241
+ onDownload && downloadable.length > 0 ? /* @__PURE__ */ jsxRuntime.jsxs("button", { type: "button", className: btn2, onClick: () => downloadable.forEach(onDownload), children: [
1242
+ /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Download, { "aria-hidden": "true", className: "size-3.5" }),
1243
+ labels.download
1244
+ ] }) : null,
1245
+ onCopy ? /* @__PURE__ */ jsxRuntime.jsxs("button", { type: "button", className: btn2, onClick: onCopy, children: [
1246
+ /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Copy, { "aria-hidden": "true", className: "size-3.5" }),
1247
+ labels.copy
1248
+ ] }) : null,
1249
+ onShare && downloadable.length > 0 ? /* @__PURE__ */ jsxRuntime.jsxs("button", { type: "button", className: btn2, onClick: onShare, children: [
1250
+ /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Link2, { "aria-hidden": "true", className: "size-3.5" }),
1251
+ labels.share
1252
+ ] }) : null,
1253
+ /* @__PURE__ */ jsxRuntime.jsxs("button", { type: "button", disabled: hasProtected, className: cn(btn2, "text-destructive"), onClick: onDelete, children: [
1254
+ /* @__PURE__ */ jsxRuntime.jsx(lucideReact.X, { "aria-hidden": "true", className: "size-3.5" }),
1255
+ labels.moveToTrash
1256
+ ] })
1257
+ ] }) })
1258
+ ] });
1259
+ }
1260
+ function ExplorerClipboardToast({
1261
+ count,
1262
+ onPaste,
1263
+ onClear
1264
+ }) {
1265
+ const labels = useExplorerLabels();
1266
+ if (count < 1) return null;
1267
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { role: "status", className: "pointer-events-auto w-full rounded-[10px] border border-border bg-card p-3 shadow-xl", children: [
1268
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "mb-2 flex items-center gap-2", children: [
1269
+ /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "font-mono text-[10px] uppercase tracking-[0.12em] text-muted-foreground", children: [
1270
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-primary", children: String(count).padStart(2, "0") }),
1271
+ " ",
1272
+ labels.copied
1273
+ ] }),
1274
+ /* @__PURE__ */ jsxRuntime.jsx(
1275
+ "button",
1276
+ {
1277
+ type: "button",
1278
+ "aria-label": labels.clearClipboard,
1279
+ className: "ml-auto inline-flex size-7 items-center justify-center rounded-[10px] text-muted-foreground hover:bg-muted",
1280
+ onClick: onClear,
1281
+ children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.X, { "aria-hidden": "true", className: "size-4" })
1282
+ }
1283
+ )
1284
+ ] }),
1285
+ /* @__PURE__ */ jsxRuntime.jsxs(
1286
+ "button",
1287
+ {
1288
+ type: "button",
1289
+ className: "inline-flex h-8 items-center gap-1.5 rounded-[10px] border border-border px-2.5 text-xs font-medium hover:bg-muted",
1290
+ onClick: onPaste,
1291
+ children: [
1292
+ /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Copy, { "aria-hidden": "true", className: "size-3.5" }),
1293
+ labels.paste
1294
+ ]
1295
+ }
1296
+ )
1297
+ ] });
1298
+ }
1299
+ function ExplorerUploadStatus({
1300
+ items,
1301
+ onDismiss,
1302
+ className
1303
+ }) {
1304
+ const labels = useExplorerLabels();
1305
+ if (items.length === 0) return null;
1306
+ const done = items.filter((item) => item.status === "success").length;
1307
+ const failed = items.filter((item) => item.status === "error").length;
1308
+ const active = items.some(
1309
+ (item) => item.status === "queued" || item.status === "uploading"
1310
+ );
1311
+ const visible = items.slice(-6);
1312
+ return /* @__PURE__ */ jsxRuntime.jsxs(
1313
+ "div",
1314
+ {
1315
+ role: "status",
1316
+ className: cn(
1317
+ "w-full rounded-[10px] border border-border bg-card p-3 shadow-xl",
1318
+ className
1319
+ ),
1320
+ children: [
1321
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "mb-2 flex items-center gap-2", children: [
1322
+ /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "font-mono text-[10px] uppercase tracking-[0.12em] text-muted-foreground", children: [
1323
+ /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "text-primary", children: [
1324
+ String(done).padStart(2, "0"),
1325
+ "/",
1326
+ String(items.length).padStart(2, "0")
1327
+ ] }),
1328
+ " ",
1329
+ labels.uploadStatus,
1330
+ failed > 0 ? /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "text-destructive", children: [
1331
+ " \xB7 ",
1332
+ failed
1333
+ ] }) : null
1334
+ ] }),
1335
+ !active ? /* @__PURE__ */ jsxRuntime.jsx(
1336
+ "button",
1337
+ {
1338
+ type: "button",
1339
+ "aria-label": labels.dismissUploads,
1340
+ className: "ml-auto inline-flex size-7 items-center justify-center rounded-[10px] text-muted-foreground hover:bg-muted",
1341
+ onClick: onDismiss,
1342
+ children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.X, { "aria-hidden": "true", className: "size-4" })
1343
+ }
1344
+ ) : /* @__PURE__ */ jsxRuntime.jsx("span", { className: "ml-auto" })
1345
+ ] }),
1346
+ /* @__PURE__ */ jsxRuntime.jsx("ul", { className: "flex max-h-48 flex-col gap-1.5 overflow-y-auto", children: visible.map((item) => /* @__PURE__ */ jsxRuntime.jsxs("li", { className: "min-w-0", children: [
1347
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-2", children: [
1348
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "min-w-0 flex-1 truncate text-xs", children: item.name }),
1349
+ /* @__PURE__ */ jsxRuntime.jsx(
1350
+ "span",
1351
+ {
1352
+ className: cn(
1353
+ "shrink-0 font-mono text-[10px] uppercase tracking-[0.12em] text-muted-foreground",
1354
+ item.status === "error" && "text-destructive",
1355
+ item.status === "success" && "text-primary"
1356
+ ),
1357
+ children: item.status === "uploading" ? `${item.progress}%` : item.status === "queued" ? labels.uploadQueued : item.status === "success" ? labels.uploadDone : labels.uploadFailed
1358
+ }
1359
+ )
1360
+ ] }),
1361
+ item.status === "uploading" ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: "mt-1 h-1 overflow-hidden rounded-[10px] bg-muted", children: /* @__PURE__ */ jsxRuntime.jsx("div", { className: "h-full bg-primary", style: { width: `${item.progress}%` } }) }) : null,
1362
+ item.error ? /* @__PURE__ */ jsxRuntime.jsx("p", { className: "mt-0.5 truncate text-[10px] text-destructive", children: item.error }) : null
1363
+ ] }, item.id)) })
1364
+ ]
1365
+ }
1366
+ );
1367
+ }
1368
+ function useUploadQueue(options) {
1369
+ const requestUploadRef = react.useRef(options.requestUpload);
1370
+ requestUploadRef.current = options.requestUpload;
1371
+ const confirmUploadRef = react.useRef(options.confirmUpload);
1372
+ confirmUploadRef.current = options.confirmUpload;
1373
+ const queueRef = react.useRef([]);
1374
+ const running = react.useRef(false);
1375
+ const [items, setItems] = react.useState([]);
1376
+ const publish = react.useCallback(() => {
1377
+ setItems(queueRef.current.map(({ content: _content, ...item }) => item));
1378
+ }, []);
1379
+ const pump = react.useCallback(async () => {
1380
+ if (running.current) return;
1381
+ const next = queueRef.current.find((item) => item.status === "queued");
1382
+ if (!next) return;
1383
+ running.current = true;
1384
+ next.status = "uploading";
1385
+ next.progress = 0;
1386
+ publish();
1387
+ try {
1388
+ const target = await requestUploadRef.current({
1389
+ name: next.name,
1390
+ size: next.size,
1391
+ type: next.type,
1392
+ content: next.content,
1393
+ parentId: next.parentId
1394
+ });
1395
+ let lastPublish = 0;
1396
+ await putFile(target, next, (progress) => {
1397
+ next.progress = progress;
1398
+ const now = Date.now();
1399
+ if (progress === 100 || now - lastPublish > 200) {
1400
+ lastPublish = now;
1401
+ publish();
1402
+ }
1403
+ });
1404
+ await confirmUploadRef.current?.({
1405
+ name: next.name,
1406
+ size: next.size,
1407
+ type: next.type,
1408
+ content: next.content
1409
+ });
1410
+ next.status = "success";
1411
+ next.progress = 100;
1412
+ if (typeof window !== "undefined") {
1413
+ window.dispatchEvent(new Event("file-next-uploaded"));
1414
+ }
1415
+ } catch (error) {
1416
+ next.status = "error";
1417
+ next.error = error instanceof Error ? error.message : "Upload failed";
1418
+ } finally {
1419
+ publish();
1420
+ running.current = false;
1421
+ void pump();
1422
+ }
1423
+ }, [publish]);
1424
+ const enqueue = react.useCallback(
1425
+ (files, parentId) => {
1426
+ if (files.length === 0) return;
1427
+ const added = files.map((file) => ({
1428
+ id: typeof crypto !== "undefined" && "randomUUID" in crypto ? crypto.randomUUID() : `${file.name}-${file.size}-${Date.now()}-${Math.random()}`,
1429
+ name: file.name,
1430
+ size: file.size,
1431
+ type: file.type,
1432
+ content: file,
1433
+ parentId,
1434
+ status: "queued",
1435
+ progress: 0
1436
+ }));
1437
+ queueRef.current = [...queueRef.current, ...added];
1438
+ publish();
1439
+ void pump();
1440
+ },
1441
+ [publish, pump]
1442
+ );
1443
+ const dismiss = react.useCallback(() => {
1444
+ queueRef.current = queueRef.current.filter(
1445
+ (item) => item.status === "queued" || item.status === "uploading"
1446
+ );
1447
+ publish();
1448
+ }, [publish]);
1449
+ const active = items.some(
1450
+ (item) => item.status === "queued" || item.status === "uploading"
1451
+ );
1452
+ const itemCount = items.length;
1453
+ react.useEffect(() => {
1454
+ if (active || itemCount === 0) return void 0;
1455
+ const timeout = window.setTimeout(() => {
1456
+ dismiss();
1457
+ }, 3e3);
1458
+ return () => {
1459
+ window.clearTimeout(timeout);
1460
+ };
1461
+ }, [active, dismiss, itemCount]);
1462
+ return { items, enqueue, dismiss, active };
1463
+ }
1464
+ function putFile(target, file, onProgress) {
1465
+ return new Promise((resolve, reject) => {
1466
+ const xhr = new XMLHttpRequest();
1467
+ xhr.upload.addEventListener("progress", (event) => {
1468
+ if (event.lengthComputable) {
1469
+ onProgress(Math.min(100, Math.round(event.loaded / event.total * 100)));
1470
+ }
1471
+ });
1472
+ xhr.addEventListener("load", () => {
1473
+ if (xhr.status < 200 || xhr.status >= 300) {
1474
+ let message = `Upload failed (${xhr.status})`;
1475
+ try {
1476
+ const parsed = JSON.parse(xhr.responseText);
1477
+ if (parsed.error?.message) message = parsed.error.message;
1478
+ } catch {
1479
+ }
1480
+ reject(new Error(message));
1481
+ return;
1482
+ }
1483
+ resolve();
1484
+ });
1485
+ xhr.addEventListener("error", () => {
1486
+ reject(new Error("Upload failed"));
1487
+ });
1488
+ xhr.open(target.method ?? "PUT", target.url);
1489
+ xhr.setRequestHeader("Content-Type", file.type || "application/octet-stream");
1490
+ for (const [header, value] of Object.entries(target.headers ?? {})) {
1491
+ xhr.setRequestHeader(header, value);
1492
+ }
1493
+ xhr.send(file.content);
1494
+ });
1495
+ }
1496
+ var EnqueueContext = react.createContext(null);
1497
+ function useUploadEnqueue() {
1498
+ return react.useContext(EnqueueContext);
1499
+ }
1500
+ function UploadQueueProvider({
1501
+ requestUpload,
1502
+ confirmUpload,
1503
+ children
1504
+ }) {
1505
+ const uploads = useUploadQueue({ requestUpload, confirmUpload });
1506
+ const panel = react.useMemo(
1507
+ () => /* @__PURE__ */ jsxRuntime.jsx("div", { className: "fixed right-4 bottom-4 z-50 flex w-[min(22rem,calc(100vw-2rem))] flex-col gap-2", children: /* @__PURE__ */ jsxRuntime.jsx(ExplorerUploadStatus, { items: uploads.items, onDismiss: uploads.dismiss }) }),
1508
+ [uploads.dismiss, uploads.items]
1509
+ );
1510
+ return /* @__PURE__ */ jsxRuntime.jsxs(EnqueueContext.Provider, { value: uploads.enqueue, children: [
1511
+ children,
1512
+ panel
1513
+ ] });
1514
+ }
1515
+ var PAGE_SIZE = 50;
1516
+ var DEFAULT_PREFS = { view: "list", sortKey: "name", sortDir: "asc" };
1517
+ function reviveFile(node) {
1518
+ const asDate = (value) => value instanceof Date ? value : new Date(value);
1519
+ return {
1520
+ ...node,
1521
+ createdAt: asDate(node.createdAt),
1522
+ updatedAt: asDate(node.updatedAt),
1523
+ deletedAt: node.deletedAt ? asDate(node.deletedAt) : null
1524
+ };
1525
+ }
1526
+ function readPrefs(key) {
1527
+ try {
1528
+ const raw = window.localStorage.getItem(key);
1529
+ if (!raw) return DEFAULT_PREFS;
1530
+ if (raw === "list" || raw === "grid") return { ...DEFAULT_PREFS, view: raw };
1531
+ const parsed = JSON.parse(raw);
1532
+ const view = parsed.view === "grid" ? "grid" : "list";
1533
+ const sortKey = parsed.sortKey === "size" || parsed.sortKey === "modified" || parsed.sortKey === "kind" ? parsed.sortKey : "name";
1534
+ const sortDir = parsed.sortDir === "desc" ? "desc" : "asc";
1535
+ return { view, sortKey, sortDir };
1536
+ } catch {
1537
+ return DEFAULT_PREFS;
1538
+ }
1539
+ }
1540
+ var compareFiles = (a, b, key, dir) => {
1541
+ if (a.kind !== b.kind) return a.kind === "folder" ? -1 : 1;
1542
+ let cmp = 0;
1543
+ switch (key) {
1544
+ case "name":
1545
+ cmp = a.name.localeCompare(b.name, void 0, { sensitivity: "base" });
1546
+ break;
1547
+ case "size":
1548
+ cmp = a.size - b.size;
1549
+ break;
1550
+ case "modified":
1551
+ cmp = a.updatedAt.getTime() - b.updatedAt.getTime();
1552
+ break;
1553
+ case "kind":
1554
+ cmp = a.kind.localeCompare(b.kind);
1555
+ break;
1556
+ }
1557
+ return dir === "asc" ? cmp : -cmp;
1558
+ };
1559
+ function formatExplorerBytes(bytes) {
1560
+ if (bytes < 1024) return `${bytes} B`;
1561
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
1562
+ return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
1563
+ }
1564
+ function FileExplorer(props) {
1565
+ const outerEnqueue = useUploadEnqueue();
1566
+ const tree = /* @__PURE__ */ jsxRuntime.jsx(ExplorerLabelsProvider, { labels: props.labels, children: /* @__PURE__ */ jsxRuntime.jsx(FileExplorerInner, { ...props }) });
1567
+ if (props.requestUpload && !outerEnqueue) {
1568
+ return /* @__PURE__ */ jsxRuntime.jsx(
1569
+ UploadQueueProvider,
1570
+ {
1571
+ requestUpload: props.requestUpload,
1572
+ confirmUpload: props.confirmUpload ? async () => {
1573
+ await props.confirmUpload?.();
1574
+ } : void 0,
1575
+ children: tree
1576
+ }
1577
+ );
1578
+ }
1579
+ return tree;
1580
+ }
1581
+ function FileExplorerInner(props) {
1582
+ const {
1583
+ tenantId,
1584
+ parentId,
1585
+ listFiles,
1586
+ actions,
1587
+ breadcrumbs,
1588
+ onBreadcrumbNavigate,
1589
+ initialView = "list",
1590
+ onActivate,
1591
+ onOpenFolder,
1592
+ searchFiles,
1593
+ listTrash,
1594
+ requestUpload,
1595
+ confirmUpload,
1596
+ usedBytes,
1597
+ quotaBytes,
1598
+ onMove = () => void 0,
1599
+ refreshKey,
1600
+ className,
1601
+ persistViewKey,
1602
+ onPreview,
1603
+ onDownload,
1604
+ extraFileAction,
1605
+ protectedIds
1606
+ } = props;
1607
+ const labels = useExplorerLabels();
1608
+ const protectedSet = react.useMemo(() => new Set(protectedIds ?? []), [protectedIds]);
1609
+ const explorer = fileNextHeadless.useFileExplorer({
1610
+ listFiles: ((input) => listFiles({ parentId: input.parentId, limit: PAGE_SIZE })),
1611
+ tenantId,
1612
+ parentId,
1613
+ initialView,
1614
+ autoFetch: false,
1615
+ onMove
1616
+ });
1617
+ const uploadInputRef = react.useRef(null);
1618
+ const [query, setQuery] = react.useState("");
1619
+ const [trashOpen, setTrashOpen] = react.useState(false);
1620
+ const [pageItems, setPageItems] = react.useState([]);
1621
+ const [pageStatus, setPageStatus] = react.useState("idle");
1622
+ const [pageError, setPageError] = react.useState(null);
1623
+ const [loadingMore, setLoadingMore] = react.useState(false);
1624
+ const [browseHasMore, setBrowseHasMore] = react.useState(false);
1625
+ const [overlayHasMore, setOverlayHasMore] = react.useState(false);
1626
+ const browseCursor = react.useRef(void 0);
1627
+ const overlayCursor = react.useRef(void 0);
1628
+ const loadingMoreRef = react.useRef(false);
1629
+ const overlayMoreRef = react.useRef(false);
1630
+ const browseGen = react.useRef(0);
1631
+ const [overlayFiles, setOverlayFiles] = react.useState(null);
1632
+ const [overlayError, setOverlayError] = react.useState(null);
1633
+ const [overlayLoading, setOverlayLoading] = react.useState(false);
1634
+ const [overlayMore, setOverlayMore] = react.useState(false);
1635
+ const [sortKey, setSortKey] = react.useState("name");
1636
+ const [sortDir, setSortDir] = react.useState("asc");
1637
+ const [prefsReady, setPrefsReady] = react.useState(!persistViewKey);
1638
+ const skipPrefsWrite = react.useRef(!persistViewKey);
1639
+ const [menu, setMenu] = react.useState(null);
1640
+ const [folderOpen, setFolderOpen] = react.useState(false);
1641
+ const [renameTarget, setRenameTarget] = react.useState(null);
1642
+ const [deleteTargets, setDeleteTargets] = react.useState([]);
1643
+ const [purgeTargets, setPurgeTargets] = react.useState([]);
1644
+ const [trashHasItems, setTrashHasItems] = react.useState(false);
1645
+ const [clipboard, setClipboard] = react.useState([]);
1646
+ const [selectedIds, setSelectedIds] = react.useState(() => /* @__PURE__ */ new Set());
1647
+ const anchorId = react.useRef(null);
1648
+ react.useEffect(() => {
1649
+ if (!persistViewKey) return;
1650
+ const prefs = readPrefs(persistViewKey);
1651
+ explorer.setView(prefs.view);
1652
+ setSortKey(prefs.sortKey);
1653
+ setSortDir(prefs.sortDir);
1654
+ skipPrefsWrite.current = true;
1655
+ setPrefsReady(true);
1656
+ }, [persistViewKey]);
1657
+ react.useEffect(() => {
1658
+ if (!persistViewKey || !prefsReady) return;
1659
+ if (skipPrefsWrite.current) {
1660
+ skipPrefsWrite.current = false;
1661
+ return;
1662
+ }
1663
+ window.localStorage.setItem(
1664
+ persistViewKey,
1665
+ JSON.stringify({ view: explorer.view, sortKey, sortDir })
1666
+ );
1667
+ }, [explorer.view, persistViewKey, prefsReady, sortDir, sortKey]);
1668
+ const handleSortChange = react.useCallback(
1669
+ (key) => {
1670
+ if (key === sortKey) {
1671
+ setSortDir((dir) => dir === "asc" ? "desc" : "asc");
1672
+ return;
1673
+ }
1674
+ setSortKey(key);
1675
+ setSortDir(key === "modified" ? "desc" : "asc");
1676
+ },
1677
+ [sortKey]
1678
+ );
1679
+ const loadBrowse = react.useCallback(
1680
+ async (mode) => {
1681
+ const gen = mode === "reset" ? ++browseGen.current : browseGen.current;
1682
+ if (mode === "more") {
1683
+ if (!browseCursor.current || loadingMoreRef.current) return;
1684
+ loadingMoreRef.current = true;
1685
+ setLoadingMore(true);
1686
+ } else {
1687
+ setPageStatus("loading");
1688
+ browseCursor.current = void 0;
1689
+ setBrowseHasMore(false);
1690
+ }
1691
+ const result = await listFiles({
1692
+ parentId,
1693
+ limit: PAGE_SIZE,
1694
+ cursor: mode === "more" ? browseCursor.current : void 0
1695
+ });
1696
+ if (gen !== browseGen.current) {
1697
+ loadingMoreRef.current = false;
1698
+ setLoadingMore(false);
1699
+ return;
1700
+ }
1701
+ if (result.ok) {
1702
+ browseCursor.current = result.value.nextCursor;
1703
+ setBrowseHasMore(Boolean(result.value.nextCursor));
1704
+ const items = result.value.items.map(reviveFile);
1705
+ setPageItems((current) => mode === "more" ? [...current, ...items] : items);
1706
+ setPageError(null);
1707
+ setPageStatus("success");
1708
+ } else if (mode === "reset") {
1709
+ setPageItems([]);
1710
+ setPageError(result.error);
1711
+ setPageStatus("error");
1712
+ }
1713
+ loadingMoreRef.current = false;
1714
+ setLoadingMore(false);
1715
+ },
1716
+ [listFiles, parentId]
1717
+ );
1718
+ const loadOverlay = react.useCallback(
1719
+ async (mode = "reset") => {
1720
+ if (trashOpen && listTrash) {
1721
+ const trimmed2 = query.trim();
1722
+ if (trimmed2 && searchFiles) {
1723
+ setOverlayLoading(true);
1724
+ const result2 = await searchFiles({ query: trimmed2, limit: PAGE_SIZE, trash: true });
1725
+ setOverlayLoading(false);
1726
+ if (result2.ok) {
1727
+ setOverlayFiles(result2.value.items.map(reviveFile));
1728
+ setOverlayError(null);
1729
+ } else setOverlayError(result2.error);
1730
+ return;
1731
+ }
1732
+ if (mode === "more") {
1733
+ if (!overlayCursor.current || overlayMoreRef.current) return;
1734
+ overlayMoreRef.current = true;
1735
+ setOverlayMore(true);
1736
+ } else {
1737
+ setOverlayLoading(true);
1738
+ overlayCursor.current = void 0;
1739
+ setOverlayHasMore(false);
1740
+ }
1741
+ const result = await listTrash({
1742
+ limit: PAGE_SIZE,
1743
+ cursor: mode === "more" ? overlayCursor.current : void 0
1744
+ });
1745
+ if (result.ok) {
1746
+ overlayCursor.current = result.value.nextCursor;
1747
+ setOverlayHasMore(Boolean(result.value.nextCursor));
1748
+ const items = result.value.items.map(reviveFile);
1749
+ setOverlayFiles((current) => mode === "more" && current ? [...current, ...items] : items);
1750
+ setOverlayError(null);
1751
+ } else if (mode === "reset") setOverlayError(result.error);
1752
+ overlayMoreRef.current = false;
1753
+ setOverlayLoading(false);
1754
+ setOverlayMore(false);
1755
+ return;
1756
+ }
1757
+ const trimmed = query.trim();
1758
+ if (trimmed && searchFiles) {
1759
+ setOverlayLoading(true);
1760
+ const result = await searchFiles({ query: trimmed, limit: PAGE_SIZE });
1761
+ setOverlayLoading(false);
1762
+ if (result.ok) {
1763
+ setOverlayFiles(result.value.items.map(reviveFile));
1764
+ setOverlayError(null);
1765
+ } else setOverlayError(result.error);
1766
+ return;
1767
+ }
1768
+ overlayCursor.current = void 0;
1769
+ setOverlayHasMore(false);
1770
+ setOverlayFiles(null);
1771
+ setOverlayError(null);
1772
+ setOverlayLoading(false);
1773
+ },
1774
+ [listTrash, query, searchFiles, trashOpen]
1775
+ );
1776
+ const refreshTrashFlag = react.useCallback(async () => {
1777
+ if (!listTrash) {
1778
+ setTrashHasItems(false);
1779
+ return;
1780
+ }
1781
+ const result = await listTrash({ limit: 1 });
1782
+ setTrashHasItems(result.ok && result.value.items.length > 0);
1783
+ }, [listTrash]);
1784
+ const reload = react.useCallback(async () => {
1785
+ await loadBrowse("reset");
1786
+ await loadOverlay("reset");
1787
+ await refreshTrashFlag();
1788
+ }, [loadBrowse, loadOverlay, refreshTrashFlag]);
1789
+ const enqueueUploads = useUploadEnqueue();
1790
+ react.useEffect(() => {
1791
+ const onUploaded = () => {
1792
+ void reload();
1793
+ void confirmUpload?.();
1794
+ };
1795
+ window.addEventListener("file-next-uploaded", onUploaded);
1796
+ return () => window.removeEventListener("file-next-uploaded", onUploaded);
1797
+ }, [confirmUpload, reload]);
1798
+ react.useEffect(() => {
1799
+ void loadBrowse("reset");
1800
+ }, [loadBrowse]);
1801
+ react.useEffect(() => {
1802
+ void refreshTrashFlag();
1803
+ }, [refreshTrashFlag]);
1804
+ react.useEffect(() => {
1805
+ const handle = window.setTimeout(() => {
1806
+ void loadOverlay("reset");
1807
+ }, trashOpen ? 0 : 250);
1808
+ return () => window.clearTimeout(handle);
1809
+ }, [loadOverlay, trashOpen]);
1810
+ const sourceFiles = overlayFiles ?? pageItems;
1811
+ const hasMore = overlayFiles ? overlayHasMore : browseHasMore;
1812
+ const sortedFiles = react.useMemo(
1813
+ () => [...sourceFiles].sort((a, b) => compareFiles(a, b, sortKey, sortDir)),
1814
+ [sourceFiles, sortKey, sortDir]
1815
+ );
1816
+ react.useEffect(() => {
1817
+ if (refreshKey === void 0) return;
1818
+ void reload();
1819
+ }, [refreshKey]);
1820
+ const handleActivate = react.useCallback(
1821
+ (file) => {
1822
+ if (file.kind === "folder") {
1823
+ onOpenFolder?.(file);
1824
+ return;
1825
+ }
1826
+ if (onPreview) onPreview(file);
1827
+ else if (onDownload) onDownload(file);
1828
+ else onActivate?.(file);
1829
+ },
1830
+ [onActivate, onDownload, onOpenFolder, onPreview]
1831
+ );
1832
+ const handleContextMenu = react.useCallback((file, event) => {
1833
+ setMenu({ kind: "item", file, x: event.clientX, y: event.clientY });
1834
+ }, []);
1835
+ const clearSelection = react.useCallback(() => {
1836
+ setSelectedIds(/* @__PURE__ */ new Set());
1837
+ anchorId.current = null;
1838
+ }, []);
1839
+ react.useEffect(() => {
1840
+ clearSelection();
1841
+ }, [clearSelection, parentId, trashOpen]);
1842
+ const handleSelect = react.useCallback(
1843
+ (file, event) => {
1844
+ const id = file.id;
1845
+ if (event.shiftKey && anchorId.current) {
1846
+ const from = sortedFiles.findIndex((item) => item.id === anchorId.current);
1847
+ const to = sortedFiles.findIndex((item) => item.id === id);
1848
+ if (from >= 0 && to >= 0) {
1849
+ const start = Math.min(from, to);
1850
+ const end = Math.max(from, to);
1851
+ setSelectedIds(new Set(sortedFiles.slice(start, end + 1).map((item) => item.id)));
1852
+ return;
1853
+ }
1854
+ }
1855
+ setSelectedIds((current) => {
1856
+ const next = new Set(current);
1857
+ if (next.has(id)) next.delete(id);
1858
+ else next.add(id);
1859
+ return next;
1860
+ });
1861
+ anchorId.current = id;
1862
+ },
1863
+ [sortedFiles]
1864
+ );
1865
+ const selectedFiles = react.useMemo(
1866
+ () => sortedFiles.filter((file) => selectedIds.has(file.id)),
1867
+ [selectedIds, sortedFiles]
1868
+ );
1869
+ const selected = selectedFiles.length === 1 ? selectedFiles[0] : void 0;
1870
+ const draggingIds = react.useMemo(() => {
1871
+ const id = explorer.draggingId;
1872
+ if (!id) return /* @__PURE__ */ new Set();
1873
+ if (selectedIds.has(id)) return selectedIds;
1874
+ return /* @__PURE__ */ new Set([id]);
1875
+ }, [explorer.draggingId, selectedIds]);
1876
+ const handleDropOnFolder = react.useCallback(
1877
+ (destinationFolderId) => {
1878
+ const ids = [...draggingIds].filter((id) => id !== destinationFolderId);
1879
+ explorer.endDrag();
1880
+ if (ids.length === 0) return;
1881
+ void Promise.resolve(onMove({ itemIds: ids, destinationFolderId })).then(clearSelection);
1882
+ },
1883
+ [clearSelection, draggingIds, explorer, onMove]
1884
+ );
1885
+ const copyToClipboard = react.useCallback(
1886
+ (files) => {
1887
+ if (files.length === 0) return;
1888
+ setClipboard(files);
1889
+ clearSelection();
1890
+ },
1891
+ [clearSelection]
1892
+ );
1893
+ const pasteClipboard = react.useCallback(
1894
+ async (destinationId = parentId) => {
1895
+ if (clipboard.length === 0 || trashOpen) return;
1896
+ for (const node of clipboard) {
1897
+ if (node.id === destinationId) continue;
1898
+ await actions.copyFile({ id: node.id, newParentId: destinationId, newName: node.name });
1899
+ }
1900
+ setClipboard([]);
1901
+ await reload();
1902
+ },
1903
+ [actions, clipboard, parentId, reload, trashOpen]
1904
+ );
1905
+ const handleKeyDown = react.useCallback(
1906
+ (event) => {
1907
+ const target = event.target;
1908
+ if (target.closest("input, textarea, [contenteditable=true]")) return;
1909
+ const index = sortedFiles.findIndex((file) => file.id === (anchorId.current ?? selected?.id));
1910
+ if ((event.metaKey || event.ctrlKey) && event.key.toLowerCase() === "a") {
1911
+ event.preventDefault();
1912
+ setSelectedIds(new Set(sortedFiles.map((file) => file.id)));
1913
+ return;
1914
+ }
1915
+ if ((event.metaKey || event.ctrlKey) && event.key.toLowerCase() === "c") {
1916
+ if (selectedFiles.length === 0 || trashOpen) return;
1917
+ event.preventDefault();
1918
+ copyToClipboard(selectedFiles);
1919
+ return;
1920
+ }
1921
+ if ((event.metaKey || event.ctrlKey) && event.key.toLowerCase() === "v") {
1922
+ if (clipboard.length === 0 || trashOpen) return;
1923
+ event.preventDefault();
1924
+ void pasteClipboard();
1925
+ return;
1926
+ }
1927
+ if (event.key === "ArrowDown" || event.key === "ArrowUp") {
1928
+ event.preventDefault();
1929
+ const delta = event.key === "ArrowDown" ? 1 : -1;
1930
+ const nextIndex = Math.min(sortedFiles.length - 1, Math.max(0, (index < 0 ? 0 : index) + delta));
1931
+ const next = sortedFiles[nextIndex];
1932
+ if (!next) return;
1933
+ if (event.shiftKey && anchorId.current) {
1934
+ const from = sortedFiles.findIndex((item) => item.id === anchorId.current);
1935
+ const start = Math.min(from, nextIndex);
1936
+ const end = Math.max(from, nextIndex);
1937
+ setSelectedIds(new Set(sortedFiles.slice(start, end + 1).map((item) => item.id)));
1938
+ } else {
1939
+ setSelectedIds(/* @__PURE__ */ new Set([next.id]));
1940
+ anchorId.current = next.id;
1941
+ }
1942
+ return;
1943
+ }
1944
+ if (event.key === "Enter" && selected) {
1945
+ event.preventDefault();
1946
+ handleActivate(selected);
1947
+ return;
1948
+ }
1949
+ if (event.key === "F2" && selected && !trashOpen) {
1950
+ event.preventDefault();
1951
+ setRenameTarget(selected);
1952
+ return;
1953
+ }
1954
+ if ((event.key === "Delete" || event.key === "Backspace") && selectedFiles.length > 0) {
1955
+ event.preventDefault();
1956
+ if (trashOpen) {
1957
+ setPurgeTargets(selectedFiles);
1958
+ return;
1959
+ }
1960
+ if (selectedFiles.some((file) => protectedSet.has(file.id))) return;
1961
+ setDeleteTargets(selectedFiles);
1962
+ return;
1963
+ }
1964
+ if (event.key === "Escape") {
1965
+ setMenu(null);
1966
+ clearSelection();
1967
+ }
1968
+ },
1969
+ [
1970
+ clipboard.length,
1971
+ clearSelection,
1972
+ copyToClipboard,
1973
+ handleActivate,
1974
+ pasteClipboard,
1975
+ protectedSet,
1976
+ selected,
1977
+ selectedFiles,
1978
+ sortedFiles,
1979
+ trashOpen
1980
+ ]
1981
+ );
1982
+ const openUpload = () => uploadInputRef.current?.click();
1983
+ return /* @__PURE__ */ jsxRuntime.jsxs(
1984
+ "div",
1985
+ {
1986
+ className: cn("flex min-h-0 flex-1 flex-col", className),
1987
+ onKeyDown: handleKeyDown,
1988
+ onDragOver: (event) => {
1989
+ if (!requestUpload || trashOpen) return;
1990
+ if (event.dataTransfer.types.includes("Files")) event.preventDefault();
1991
+ },
1992
+ onDrop: (event) => {
1993
+ if (!requestUpload || trashOpen) return;
1994
+ const files = event.dataTransfer.files;
1995
+ if (!files.length) return;
1996
+ event.preventDefault();
1997
+ enqueueUploads?.(Array.from(files), parentId);
1998
+ },
1999
+ onContextMenu: (event) => {
2000
+ if (trashOpen) return;
2001
+ if (event.target.closest("[data-file-id]")) return;
2002
+ event.preventDefault();
2003
+ setMenu({ kind: "blank", x: event.clientX, y: event.clientY });
2004
+ },
2005
+ children: [
2006
+ breadcrumbs && onBreadcrumbNavigate ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: "border-b border-border px-4 py-3", children: /* @__PURE__ */ jsxRuntime.jsx(Breadcrumbs, { segments: breadcrumbs, onNavigate: onBreadcrumbNavigate }) }) : null,
2007
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex shrink-0 items-center gap-2 border-b border-border px-4 py-3", children: [
2008
+ requestUpload ? /* @__PURE__ */ jsxRuntime.jsx(
2009
+ "input",
2010
+ {
2011
+ ref: uploadInputRef,
2012
+ type: "file",
2013
+ multiple: true,
2014
+ className: "sr-only",
2015
+ onChange: (event) => {
2016
+ const files = event.target.files;
2017
+ if (!files || files.length === 0) return;
2018
+ enqueueUploads?.(Array.from(files), parentId);
2019
+ event.target.value = "";
2020
+ }
2021
+ }
2022
+ ) : null,
2023
+ /* @__PURE__ */ jsxRuntime.jsx(
2024
+ ExplorerToolbar,
2025
+ {
2026
+ className: "min-w-0 flex-1",
2027
+ view: explorer.view,
2028
+ onViewChange: explorer.setView,
2029
+ onUpload: requestUpload && !trashOpen ? openUpload : void 0,
2030
+ onNewFolder: actions.createFolder && !trashOpen ? () => setFolderOpen(true) : void 0,
2031
+ query,
2032
+ onQueryChange: searchFiles ? setQuery : void 0,
2033
+ trashOpen,
2034
+ trashHasItems,
2035
+ onTrashToggle: listTrash ? () => {
2036
+ setQuery("");
2037
+ setTrashOpen((open) => !open);
2038
+ } : void 0,
2039
+ sortKey,
2040
+ sortDirection: sortDir,
2041
+ onSortChange: handleSortChange
2042
+ }
2043
+ )
2044
+ ] }),
2045
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "min-h-0 flex-1 overflow-y-auto", children: (overlayLoading || pageStatus === "loading") && sortedFiles.length === 0 ? /* @__PURE__ */ jsxRuntime.jsx("p", { className: "px-4 py-12 text-center font-mono text-[10px] uppercase tracking-[0.12em] text-muted-foreground", children: labels.loading }) : overlayError || pageStatus === "error" && pageError ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: "border-b border-destructive/30 bg-destructive/5 px-4 py-3 text-sm text-destructive", children: (overlayError ?? pageError)?.message ?? labels.folderCreateFailed }) : sortedFiles.length === 0 ? /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex min-h-full flex-col justify-center px-4 py-16 text-center", children: [
2046
+ /* @__PURE__ */ jsxRuntime.jsx("p", { className: "font-mono text-[10px] uppercase tracking-[0.12em] text-muted-foreground", children: trashOpen ? labels.emptyTrash : query.trim() ? labels.noMatches : labels.emptyFolder }),
2047
+ trashOpen ? /* @__PURE__ */ jsxRuntime.jsx("p", { className: "mt-2 text-sm text-muted-foreground", children: labels.trashRetention }) : !query.trim() ? /* @__PURE__ */ jsxRuntime.jsx("p", { className: "mt-2 text-sm text-muted-foreground", children: labels.dropHint }) : null
2048
+ ] }) : explorer.view === "list" ? /* @__PURE__ */ jsxRuntime.jsx(
2049
+ "div",
2050
+ {
2051
+ onClick: (event) => {
2052
+ if (event.target.closest("[data-file-id]")) return;
2053
+ clearSelection();
2054
+ },
2055
+ children: /* @__PURE__ */ jsxRuntime.jsx(
2056
+ ExplorerListView,
2057
+ {
2058
+ files: sortedFiles,
2059
+ protectedIds: protectedSet,
2060
+ selectedIds,
2061
+ onSelect: handleSelect,
2062
+ onActivate: handleActivate,
2063
+ onContextMenu: handleContextMenu,
2064
+ onDragStart: explorer.beginDrag,
2065
+ onDragEnd: explorer.endDrag,
2066
+ onDrop: handleDropOnFolder,
2067
+ draggingId: explorer.draggingId,
2068
+ draggingIds,
2069
+ dropTargetId: explorer.dropTargetId,
2070
+ onDragOverRow: explorer.setDropTarget,
2071
+ sortKey,
2072
+ sortDirection: sortDir,
2073
+ onSortChange: handleSortChange
2074
+ }
2075
+ )
2076
+ }
2077
+ ) : /* @__PURE__ */ jsxRuntime.jsx(
2078
+ "div",
2079
+ {
2080
+ onClick: (event) => {
2081
+ if (event.target.closest("[data-file-id]")) return;
2082
+ clearSelection();
2083
+ },
2084
+ children: /* @__PURE__ */ jsxRuntime.jsx(
2085
+ ExplorerGridView,
2086
+ {
2087
+ files: sortedFiles,
2088
+ protectedIds: protectedSet,
2089
+ selectedIds,
2090
+ onSelect: handleSelect,
2091
+ onActivate: handleActivate,
2092
+ onContextMenu: handleContextMenu,
2093
+ onDragStart: explorer.beginDrag,
2094
+ onDragEnd: explorer.endDrag,
2095
+ onDrop: handleDropOnFolder,
2096
+ draggingId: explorer.draggingId,
2097
+ draggingIds,
2098
+ dropTargetId: explorer.dropTargetId,
2099
+ onDragOverRow: explorer.setDropTarget
2100
+ }
2101
+ )
2102
+ }
2103
+ ) }),
2104
+ hasMore && sortedFiles.length > 0 ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: "shrink-0 border-t border-border p-3", children: /* @__PURE__ */ jsxRuntime.jsx(
2105
+ "button",
2106
+ {
2107
+ type: "button",
2108
+ disabled: loadingMore || overlayMore,
2109
+ onClick: () => {
2110
+ if (overlayFiles) void loadOverlay("more");
2111
+ else void loadBrowse("more");
2112
+ },
2113
+ className: "inline-flex h-8 w-full items-center justify-center rounded-[10px] border border-border text-xs font-medium hover:bg-muted disabled:opacity-50",
2114
+ children: loadingMore || overlayMore ? labels.loading : labels.loadMore
2115
+ }
2116
+ ) }) : null,
2117
+ /* @__PURE__ */ jsxRuntime.jsxs("footer", { className: "flex shrink-0 items-center justify-between gap-3 border-t border-border px-4 py-2", children: [
2118
+ /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "font-mono text-[10px] uppercase tracking-[0.12em] text-muted-foreground", children: [
2119
+ /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "text-primary tabular-nums", children: [
2120
+ sortedFiles.length,
2121
+ hasMore ? "+" : ""
2122
+ ] }),
2123
+ trashOpen ? ` ${labels.inTrash}` : ` ${labels.items}`
2124
+ ] }),
2125
+ quotaBytes != null ? /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "hidden font-mono text-[10px] uppercase tracking-[0.12em] text-muted-foreground sm:inline", children: [
2126
+ formatExplorerBytes(usedBytes ?? 0),
2127
+ " / ",
2128
+ formatExplorerBytes(quotaBytes)
2129
+ ] }) : null
2130
+ ] }),
2131
+ /* @__PURE__ */ jsxRuntime.jsx(
2132
+ ExplorerContextMenu,
2133
+ {
2134
+ target: menu,
2135
+ mode: trashOpen ? "trash" : "browse",
2136
+ protectedItem: menu?.kind === "item" ? protectedSet.has(menu.file.id) : false,
2137
+ extraFileAction,
2138
+ actions: {
2139
+ deleteFile: async (input) => {
2140
+ await actions.deleteFile(input);
2141
+ clearSelection();
2142
+ await reload();
2143
+ },
2144
+ renameFile: actions.renameFile,
2145
+ restoreNode: actions.restoreNode ? async (input) => {
2146
+ await actions.restoreNode?.(input);
2147
+ await reload();
2148
+ } : void 0,
2149
+ purgeNode: actions.purgeNode ? async (input) => {
2150
+ await actions.purgeNode?.(input);
2151
+ await reload();
2152
+ } : void 0,
2153
+ createShare: actions.createShare
2154
+ },
2155
+ onClose: () => setMenu(null),
2156
+ onPreview,
2157
+ onDownload,
2158
+ onRename: setRenameTarget,
2159
+ onRequestDelete: (file) => setDeleteTargets([file]),
2160
+ onRequestPurge: (file) => setPurgeTargets([file]),
2161
+ onCopy: (file) => copyToClipboard([file]),
2162
+ onPaste: clipboard.length > 0 && !trashOpen ? (folder) => void pasteClipboard(folder ? folder.id : parentId) : void 0,
2163
+ onNewFolder: actions.createFolder ? () => setFolderOpen(true) : void 0,
2164
+ onUpload: requestUpload ? openUpload : void 0
2165
+ }
2166
+ ),
2167
+ /* @__PURE__ */ jsxRuntime.jsx(
2168
+ ExplorerSelectionToast,
2169
+ {
2170
+ files: selectedFiles,
2171
+ protectedIds: protectedSet,
2172
+ trashOpen,
2173
+ onDownload,
2174
+ onCopy: trashOpen ? void 0 : () => copyToClipboard(selectedFiles),
2175
+ onShare: actions.createShare ? async () => {
2176
+ const tokens = [];
2177
+ for (const file of selectedFiles) {
2178
+ if (file.kind !== "file") continue;
2179
+ tokens.push(await actions.createShare({ id: file.id }));
2180
+ }
2181
+ if (tokens.length > 0) await navigator.clipboard.writeText(tokens.join("\n"));
2182
+ } : void 0,
2183
+ onDelete: () => setDeleteTargets(selectedFiles),
2184
+ onRestore: actions.restoreNode ? async () => {
2185
+ for (const file of selectedFiles) await actions.restoreNode?.({ id: file.id });
2186
+ clearSelection();
2187
+ await reload();
2188
+ } : void 0,
2189
+ onPurge: () => setPurgeTargets(selectedFiles),
2190
+ onClear: clearSelection
2191
+ }
2192
+ ),
2193
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "fixed right-4 bottom-4 z-50 w-[min(22rem,calc(100vw-2rem))]", children: /* @__PURE__ */ jsxRuntime.jsx(
2194
+ ExplorerClipboardToast,
2195
+ {
2196
+ count: clipboard.length,
2197
+ onPaste: () => void pasteClipboard(),
2198
+ onClear: () => setClipboard([])
2199
+ }
2200
+ ) }),
2201
+ /* @__PURE__ */ jsxRuntime.jsx(
2202
+ CreateFolderDialog,
2203
+ {
2204
+ open: folderOpen,
2205
+ onOpenChange: setFolderOpen,
2206
+ onCreate: async (name) => {
2207
+ await actions.createFolder?.({ name, parentId });
2208
+ await reload();
2209
+ }
2210
+ }
2211
+ ),
2212
+ /* @__PURE__ */ jsxRuntime.jsx(
2213
+ CreateFolderDialog,
2214
+ {
2215
+ open: Boolean(renameTarget),
2216
+ onOpenChange: (open) => {
2217
+ if (!open) setRenameTarget(null);
2218
+ },
2219
+ title: labels.rename,
2220
+ description: labels.renameHint,
2221
+ confirmLabel: labels.rename,
2222
+ initialName: renameTarget?.name ?? "",
2223
+ onCreate: async (name) => {
2224
+ if (!renameTarget) return;
2225
+ await actions.renameFile(renameTarget.id, name);
2226
+ setRenameTarget(null);
2227
+ await reload();
2228
+ }
2229
+ }
2230
+ ),
2231
+ /* @__PURE__ */ jsxRuntime.jsx(
2232
+ ConfirmDeleteDialog,
2233
+ {
2234
+ open: deleteTargets.length > 0,
2235
+ names: deleteTargets.map((file) => file.name),
2236
+ onOpenChange: (open) => {
2237
+ if (!open) setDeleteTargets([]);
2238
+ },
2239
+ onConfirm: async () => {
2240
+ for (const file of deleteTargets) await actions.deleteFile({ id: file.id });
2241
+ setDeleteTargets([]);
2242
+ clearSelection();
2243
+ await reload();
2244
+ }
2245
+ }
2246
+ ),
2247
+ /* @__PURE__ */ jsxRuntime.jsx(
2248
+ ConfirmDeleteDialog,
2249
+ {
2250
+ open: purgeTargets.length > 0,
2251
+ names: purgeTargets.map((file) => file.name),
2252
+ permanent: true,
2253
+ onOpenChange: (open) => {
2254
+ if (!open) setPurgeTargets([]);
2255
+ },
2256
+ onConfirm: async () => {
2257
+ for (const file of purgeTargets) await actions.purgeNode?.({ id: file.id });
2258
+ setPurgeTargets([]);
2259
+ clearSelection();
2260
+ await reload();
2261
+ }
2262
+ }
2263
+ )
2264
+ ]
2265
+ }
2266
+ );
2267
+ }
2268
+ function EmptyState(props) {
2269
+ const { title, description, icon, action, className } = props;
2270
+ return /* @__PURE__ */ jsxRuntime.jsxs(
2271
+ "div",
2272
+ {
2273
+ role: "status",
2274
+ "aria-live": "polite",
2275
+ className: cn(
2276
+ "flex flex-col items-center justify-center gap-2 rounded-lg border border-dashed border-border bg-muted/40 p-8 text-center",
2277
+ className
2278
+ ),
2279
+ children: [
2280
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "rounded-full bg-background p-3 text-muted-foreground shadow-sm", children: icon ?? /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Inbox, { "aria-hidden": "true", className: "size-6" }) }),
2281
+ /* @__PURE__ */ jsxRuntime.jsx("h3", { className: "text-base font-semibold text-foreground", children: title }),
2282
+ description ? /* @__PURE__ */ jsxRuntime.jsx("p", { className: "max-w-sm text-sm text-muted-foreground", children: description }) : null,
2283
+ action ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: "mt-2", children: action }) : null
2284
+ ]
2285
+ }
2286
+ );
2287
+ }
2288
+ function ErrorState(props) {
2289
+ const { error, onRetry, className } = props;
2290
+ return /* @__PURE__ */ jsxRuntime.jsxs(
2291
+ "div",
2292
+ {
2293
+ role: "alert",
2294
+ className: cn(
2295
+ "flex items-start gap-3 rounded-lg border border-destructive/30 bg-destructive/5 p-4 text-sm",
2296
+ className
2297
+ ),
2298
+ children: [
2299
+ /* @__PURE__ */ jsxRuntime.jsx(
2300
+ lucideReact.AlertCircleIcon,
2301
+ {
2302
+ "aria-hidden": "true",
2303
+ className: "mt-0.5 size-5 shrink-0 text-destructive"
2304
+ }
2305
+ ),
2306
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex-1", children: [
2307
+ /* @__PURE__ */ jsxRuntime.jsx("p", { className: "font-medium text-destructive", children: error.code }),
2308
+ /* @__PURE__ */ jsxRuntime.jsx("p", { className: "mt-1 text-muted-foreground", children: error.message })
2309
+ ] }),
2310
+ onRetry ? /* @__PURE__ */ jsxRuntime.jsx(
2311
+ "button",
2312
+ {
2313
+ type: "button",
2314
+ onClick: onRetry,
2315
+ className: "inline-flex h-8 items-center justify-center rounded-[10px] border border-border bg-background px-3 text-sm font-medium hover:bg-accent",
2316
+ children: "Retry"
2317
+ }
2318
+ ) : null
2319
+ ]
2320
+ }
2321
+ );
2322
+ }
2323
+ function canPreviewFile(file) {
2324
+ return previewKind(file.mimeType, file.name) !== "none";
2325
+ }
2326
+ function previewKind(mimeType, name) {
2327
+ const mime = mimeType.toLowerCase();
2328
+ const ext = name.split(".").pop()?.toLowerCase() ?? "";
2329
+ if (mime.startsWith("image/") || ["png", "jpg", "jpeg", "gif", "webp"].includes(ext)) {
2330
+ return "image";
2331
+ }
2332
+ if (mime === "application/pdf" || ext === "pdf") return "pdf";
2333
+ if (mime.startsWith("video/") || ["mp4", "webm", "mov"].includes(ext)) return "video";
2334
+ if (mime.startsWith("text/") || ["txt", "csv"].includes(ext)) return "text";
2335
+ return "none";
2336
+ }
2337
+ function FilePreviewDialog({
2338
+ file,
2339
+ src,
2340
+ onClose,
2341
+ onDownload
2342
+ }) {
2343
+ const labels = useExplorerLabels();
2344
+ if (!file) return null;
2345
+ const kind = previewKind(file.mimeType, file.name);
2346
+ return /* @__PURE__ */ jsxRuntime.jsxs(Overlay, { open: true, onClose, className: "max-w-4xl", children: [
2347
+ /* @__PURE__ */ jsxRuntime.jsx("h2", { className: "truncate pr-6 text-base font-semibold", children: file.name }),
2348
+ /* @__PURE__ */ jsxRuntime.jsx("p", { className: "mt-1 text-sm text-muted-foreground", children: file.mimeType || labels.unknownType }),
2349
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "mt-3", children: kind === "image" ? /* @__PURE__ */ jsxRuntime.jsx(
2350
+ "img",
2351
+ {
2352
+ src,
2353
+ alt: file.name,
2354
+ className: "max-h-[70vh] w-full rounded-[10px] object-contain"
2355
+ }
2356
+ ) : kind === "pdf" || kind === "text" ? /* @__PURE__ */ jsxRuntime.jsx(
2357
+ "iframe",
2358
+ {
2359
+ src,
2360
+ title: file.name,
2361
+ className: "h-[70vh] w-full rounded-[10px] border border-border bg-background"
2362
+ }
2363
+ ) : kind === "video" ? /* @__PURE__ */ jsxRuntime.jsx(
2364
+ "video",
2365
+ {
2366
+ controls: true,
2367
+ playsInline: true,
2368
+ preload: "metadata",
2369
+ src,
2370
+ className: "max-h-[70vh] w-full rounded-[10px] bg-foreground",
2371
+ children: labels.previewUnavailable
2372
+ }
2373
+ ) : /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-sm text-muted-foreground", children: labels.previewUnavailable }) }),
2374
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "mt-4 flex justify-end", children: /* @__PURE__ */ jsxRuntime.jsx(
2375
+ "button",
2376
+ {
2377
+ type: "button",
2378
+ className: "inline-flex h-8 items-center rounded-[10px] border border-border px-3 text-sm",
2379
+ onClick: () => onDownload(file),
2380
+ children: labels.download
2381
+ }
2382
+ ) })
2383
+ ] });
2384
+ }
2385
+
2386
+ exports.Breadcrumbs = Breadcrumbs;
2387
+ exports.ConfirmDeleteDialog = ConfirmDeleteDialog;
2388
+ exports.CreateFolderDialog = CreateFolderDialog;
2389
+ exports.EmptyState = EmptyState;
2390
+ exports.ErrorState = ErrorState;
2391
+ exports.ExplorerClipboardToast = ExplorerClipboardToast;
2392
+ exports.ExplorerContextMenu = ExplorerContextMenu;
2393
+ exports.ExplorerGridView = ExplorerGridView;
2394
+ exports.ExplorerLabelsProvider = ExplorerLabelsProvider;
2395
+ exports.ExplorerListView = ExplorerListView;
2396
+ exports.ExplorerSelectionToast = ExplorerSelectionToast;
2397
+ exports.ExplorerToolbar = ExplorerToolbar;
2398
+ exports.ExplorerUploadStatus = ExplorerUploadStatus;
2399
+ exports.FILE_ICON_MAP = FILE_ICON_MAP;
2400
+ exports.FileExplorer = FileExplorer;
2401
+ exports.FileIcon = FileIcon;
2402
+ exports.FilePreviewDialog = FilePreviewDialog;
2403
+ exports.UploadQueueProvider = UploadQueueProvider;
2404
+ exports.canPreviewFile = canPreviewFile;
2405
+ exports.cn = cn;
2406
+ exports.defaultLabels = defaultLabels;
2407
+ exports.resolveFileIcon = resolveFileIcon;
2408
+ exports.useExplorerLabels = useExplorerLabels;
2409
+ exports.useUploadEnqueue = useUploadEnqueue;
2410
+ exports.useUploadQueue = useUploadQueue;
2411
+ //# sourceMappingURL=index.cjs.map
2412
+ //# sourceMappingURL=index.cjs.map