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