@vryzel/file-next-ui 0.4.4 → 0.5.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 CHANGED
@@ -1,9 +1,10 @@
1
- import { createContext, createElement, useContext, useRef, useLayoutEffect, useEffect, useState, useCallback, useMemo } from 'react';
1
+ import { createContext, createElement, useContext, useRef, useState, useLayoutEffect, useEffect, useCallback, useMemo } from 'react';
2
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';
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, Minus, Plus } from 'lucide-react';
4
4
  import { clsx } from 'clsx';
5
5
  import { twMerge } from 'tailwind-merge';
6
6
  import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
7
+ import { useVirtualizer } from '@tanstack/react-virtual';
7
8
 
8
9
  // src/file-explorer.tsx
9
10
  function cn(...inputs) {
@@ -127,11 +128,17 @@ var defaultLabels = {
127
128
  clearClipboard: "Clear clipboard",
128
129
  uploadStatus: "uploading",
129
130
  dismissUploads: "Dismiss uploads",
131
+ cancelQueue: "Cancel queue",
132
+ removeUpload: "Remove from queue",
130
133
  uploadQueued: "Queued",
131
134
  uploadDone: "Done",
132
135
  uploadFailed: "Failed",
133
136
  unknownType: "Unknown type",
134
- previewUnavailable: "Preview is not available for this file."
137
+ previewUnavailable: "Preview is not available for this file.",
138
+ closePreview: "Close preview",
139
+ zoomIn: "Zoom in",
140
+ zoomOut: "Zoom out",
141
+ zoomFit: "Fit"
135
142
  };
136
143
  var LabelsContext = createContext(defaultLabels);
137
144
  function ExplorerLabelsProvider({
@@ -280,6 +287,39 @@ function useExplorerItemPointer(opts) {
280
287
  }, [coarse, file, onActivate]);
281
288
  return { onPointerDown, onClick, onDoubleClick };
282
289
  }
290
+ function shouldLoadMore(input) {
291
+ const threshold = input.threshold ?? 8;
292
+ if (!input.hasMore || input.loading || input.loaded === 0) return false;
293
+ if (input.lastIndex === void 0) return false;
294
+ return input.lastIndex >= input.loaded - 1 - threshold;
295
+ }
296
+ function gridColumnCount(width) {
297
+ if (width < 640) return 3;
298
+ if (width < 768) return 5;
299
+ if (width < 1024) return 6;
300
+ return 8;
301
+ }
302
+ function selectionEdgeIndex(files, selectedIds, previous) {
303
+ let minPrev = Number.POSITIVE_INFINITY;
304
+ const added = [];
305
+ for (let i = 0; i < files.length; i++) {
306
+ const id = files[i]?.id;
307
+ if (!id) continue;
308
+ if (previous.has(id) && i < minPrev) minPrev = i;
309
+ if (selectedIds.has(id) && !previous.has(id)) added.push(i);
310
+ }
311
+ const minAdded = added[0];
312
+ const maxAdded = added[added.length - 1];
313
+ if (minAdded === void 0 || maxAdded === void 0) return -1;
314
+ return minAdded < minPrev ? minAdded : maxAdded;
315
+ }
316
+ function useLoadMoreOnEnd(input) {
317
+ const { lastIndex, loaded, hasMore, loading, onLoadMore } = input;
318
+ useEffect(() => {
319
+ if (!onLoadMore) return;
320
+ if (shouldLoadMore({ lastIndex, loaded, hasMore, loading })) onLoadMore();
321
+ }, [hasMore, lastIndex, loaded, loading, onLoadMore]);
322
+ }
283
323
  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]";
284
324
  var DRAG_MIME = "application/x-file-next-id";
285
325
  function ExplorerListView(props) {
@@ -303,58 +343,123 @@ function ExplorerListView(props) {
303
343
  renamingId,
304
344
  onRenameCommit,
305
345
  onRenameCancel,
306
- className
346
+ className,
347
+ scrollRef: scrollRefProp,
348
+ hasMore = false,
349
+ loadingMore = false,
350
+ onLoadMore
307
351
  } = props;
308
352
  const labels = useExplorerLabels();
309
353
  const coarse = useCoarsePointer();
310
- 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: [
311
- /* @__PURE__ */ jsxs(
312
- "div",
313
- {
314
- role: "row",
315
- className: cn(
316
- "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",
317
- ROW_GRID
354
+ const ownScrollRef = useRef(null);
355
+ const scrollRef = scrollRefProp ?? ownScrollRef;
356
+ const headerRef = useRef(null);
357
+ const [scrollMargin, setScrollMargin] = useState(0);
358
+ useLayoutEffect(() => {
359
+ const header = headerRef.current;
360
+ if (!header) return;
361
+ const measure = () => setScrollMargin(header.offsetHeight);
362
+ measure();
363
+ if (typeof ResizeObserver === "undefined") return;
364
+ const observer = new ResizeObserver(measure);
365
+ observer.observe(header);
366
+ return () => observer.disconnect();
367
+ }, []);
368
+ const virtualizer = useVirtualizer({
369
+ count: files.length,
370
+ getScrollElement: () => scrollRef.current,
371
+ estimateSize: () => coarse ? 64 : 44,
372
+ overscan: 8,
373
+ scrollMargin,
374
+ scrollPaddingStart: scrollMargin,
375
+ getItemKey: (index) => files[index]?.id ?? index,
376
+ initialRect: { width: 800, height: 600 }
377
+ });
378
+ const virtualItems = virtualizer.getVirtualItems();
379
+ useLoadMoreOnEnd({
380
+ lastIndex: virtualItems.at(-1)?.index,
381
+ loaded: files.length,
382
+ hasMore,
383
+ loading: loadingMore,
384
+ onLoadMore
385
+ });
386
+ const prevSelected = useRef(selectedIds);
387
+ useEffect(() => {
388
+ const index = renamingId ? files.findIndex((file) => file.id === renamingId) : selectionEdgeIndex(files, selectedIds, prevSelected.current);
389
+ prevSelected.current = selectedIds;
390
+ if (index < 0) return;
391
+ virtualizer.scrollToIndex(index, { align: "auto" });
392
+ }, [renamingId, selectedIds]);
393
+ return /* @__PURE__ */ jsx(
394
+ "div",
395
+ {
396
+ ref: scrollRefProp ? void 0 : ownScrollRef,
397
+ className: cn(
398
+ !scrollRefProp && "min-h-0 flex-1 overflow-y-auto overflow-x-auto overscroll-x-contain",
399
+ className
400
+ ),
401
+ children: /* @__PURE__ */ jsxs("div", { className: "min-w-[32rem] sm:min-w-0", children: [
402
+ /* @__PURE__ */ jsxs(
403
+ "div",
404
+ {
405
+ ref: headerRef,
406
+ role: "row",
407
+ className: cn(
408
+ "sticky top-0 z-10 grid items-center gap-3 border-b border-border bg-background px-3 py-2 font-mono text-[10px] font-medium uppercase tracking-[0.12em] text-muted-foreground",
409
+ ROW_GRID
410
+ ),
411
+ children: [
412
+ /* @__PURE__ */ jsx("span", { "aria-hidden": "true" }),
413
+ /* @__PURE__ */ jsx("span", { "aria-hidden": "true" }),
414
+ /* @__PURE__ */ jsx(SortHeader, { col: "name", label: labels.sortName, sortKey, sortDirection, onSort: onSortChange }),
415
+ /* @__PURE__ */ jsx(SortHeader, { col: "size", label: labels.sortSize, sortKey, sortDirection, onSort: onSortChange, className: "justify-end text-right" }),
416
+ coarse ? /* @__PURE__ */ jsx("span", { "aria-hidden": "true" }) : /* @__PURE__ */ jsx(SortHeader, { col: "modified", label: labels.sortModified, sortKey, sortDirection, onSort: onSortChange, className: "hidden sm:flex" })
417
+ ]
418
+ }
318
419
  ),
319
- children: [
320
- /* @__PURE__ */ jsx("span", { "aria-hidden": "true" }),
321
- /* @__PURE__ */ jsx("span", { "aria-hidden": "true" }),
322
- /* @__PURE__ */ jsx(SortHeader, { col: "name", label: labels.sortName, sortKey, sortDirection, onSort: onSortChange }),
323
- /* @__PURE__ */ jsx(SortHeader, { col: "size", label: labels.sortSize, sortKey, sortDirection, onSort: onSortChange, className: "justify-end text-right" }),
324
- coarse ? /* @__PURE__ */ jsx("span", { "aria-hidden": "true" }) : /* @__PURE__ */ jsx(SortHeader, { col: "modified", label: labels.sortModified, sortKey, sortDirection, onSort: onSortChange, className: "hidden sm:flex" })
325
- ]
326
- }
327
- ),
328
- /* @__PURE__ */ jsx("ul", { role: "list", children: files.map((file, idx) => /* @__PURE__ */ jsx(
329
- ExplorerListRow,
330
- {
331
- file,
332
- idx,
333
- selected: selectedIds.has(file.id),
334
- dragging: draggingIds?.has(file.id) ?? file.id === draggingId,
335
- dropTarget: file.id === dropTargetId && file.kind === "folder",
336
- isProtected: protectedIds?.has(file.id) ?? false,
337
- draggingId,
338
- dropTargetId,
339
- coarse,
340
- onSelect,
341
- onActivate,
342
- onContextMenu,
343
- onDragStart,
344
- onDragEnd,
345
- onDrop,
346
- onDragOverRow,
347
- renaming: file.id === renamingId,
348
- onRenameCommit,
349
- onRenameCancel
350
- },
351
- file.id
352
- )) })
353
- ] }) });
420
+ /* @__PURE__ */ jsx("ul", { role: "list", className: "relative z-0 w-full", style: { height: virtualizer.getTotalSize() }, children: virtualItems.map((virtualRow) => {
421
+ const file = files[virtualRow.index];
422
+ if (!file) return null;
423
+ return /* @__PURE__ */ jsx(
424
+ ExplorerListRow,
425
+ {
426
+ file,
427
+ idx: virtualRow.index,
428
+ dataIndex: virtualRow.index,
429
+ measureRef: virtualizer.measureElement,
430
+ translateY: virtualRow.start - virtualizer.options.scrollMargin,
431
+ selected: selectedIds.has(file.id),
432
+ dragging: draggingIds?.has(file.id) ?? file.id === draggingId,
433
+ dropTarget: file.id === dropTargetId && file.kind === "folder",
434
+ isProtected: protectedIds?.has(file.id) ?? false,
435
+ draggingId,
436
+ dropTargetId,
437
+ coarse,
438
+ onSelect,
439
+ onActivate,
440
+ onContextMenu,
441
+ onDragStart,
442
+ onDragEnd,
443
+ onDrop,
444
+ onDragOverRow,
445
+ renaming: file.id === renamingId,
446
+ onRenameCommit,
447
+ onRenameCancel
448
+ },
449
+ virtualRow.key
450
+ );
451
+ }) }),
452
+ loadingMore ? /* @__PURE__ */ jsx("p", { className: "px-4 py-3 text-center font-mono text-[10px] uppercase tracking-[0.12em] text-muted-foreground", children: labels.loading }) : null
453
+ ] })
454
+ }
455
+ );
354
456
  }
355
457
  function ExplorerListRow({
356
458
  file,
357
459
  idx,
460
+ dataIndex,
461
+ measureRef,
462
+ translateY,
358
463
  selected,
359
464
  dragging,
360
465
  dropTarget,
@@ -381,6 +486,8 @@ function ExplorerListRow({
381
486
  {
382
487
  role: "row",
383
488
  "data-file-id": file.id,
489
+ "data-index": dataIndex,
490
+ ref: measureRef,
384
491
  "aria-selected": selected,
385
492
  draggable: !coarse && !renaming,
386
493
  onDragStart: (e) => {
@@ -422,8 +529,9 @@ function ExplorerListRow({
422
529
  else onDragEnd();
423
530
  },
424
531
  tabIndex: 0,
532
+ style: { transform: `translateY(${translateY}px)` },
425
533
  className: cn(
426
- "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",
534
+ "absolute left-0 top-0 grid w-full items-center gap-3 border-b border-border/60 px-3 py-3.5 text-sm outline-none sm:py-2",
427
535
  "touch-manipulation select-none [-webkit-touch-callout:none]",
428
536
  ROW_GRID,
429
537
  "hover:bg-muted focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-inset",
@@ -506,6 +614,7 @@ var formatSize = (bytes) => {
506
614
  };
507
615
  var formatDate = (d) => d.toLocaleDateString(void 0, { year: "numeric", month: "short", day: "numeric" });
508
616
  var EXPLORER_DRAG_MIME = DRAG_MIME;
617
+ var GRID_PAD = 12;
509
618
  function ExplorerGridView(props) {
510
619
  const {
511
620
  files,
@@ -524,38 +633,116 @@ function ExplorerGridView(props) {
524
633
  renamingId,
525
634
  onRenameCommit,
526
635
  onRenameCancel,
527
- className
636
+ className,
637
+ scrollRef: scrollRefProp,
638
+ hasMore = false,
639
+ loadingMore = false,
640
+ onLoadMore
528
641
  } = props;
529
642
  const labels = useExplorerLabels();
530
643
  const coarse = useCoarsePointer();
531
- 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(
532
- ExplorerGridItem,
644
+ const ownScrollRef = useRef(null);
645
+ const scrollRef = scrollRefProp ?? ownScrollRef;
646
+ const [width, setWidth] = useState(null);
647
+ useLayoutEffect(() => {
648
+ const el = scrollRef.current;
649
+ if (!el) return;
650
+ const measure = () => setWidth(el.clientWidth);
651
+ measure();
652
+ if (typeof ResizeObserver === "undefined") return;
653
+ const observer = new ResizeObserver(measure);
654
+ observer.observe(el);
655
+ return () => observer.disconnect();
656
+ }, [scrollRef]);
657
+ const columns = width != null && width > 0 ? gridColumnCount(width) : 6;
658
+ const rowCount = Math.ceil(files.length / columns);
659
+ const virtualizer = useVirtualizer({
660
+ count: rowCount,
661
+ getScrollElement: () => scrollRef.current,
662
+ estimateSize: () => 156,
663
+ overscan: 4,
664
+ gap: 6,
665
+ // ponytail: gap-1.5 at 16px root
666
+ scrollMargin: GRID_PAD,
667
+ getItemKey: (index) => files[index * columns]?.id ?? index,
668
+ initialRect: { width: 800, height: 600 }
669
+ });
670
+ const virtualRows = virtualizer.getVirtualItems();
671
+ const viewportEnd = (virtualizer.scrollOffset ?? 0) + (virtualizer.scrollRect?.height ?? 0);
672
+ const lastVisibleRow = virtualRows.filter((row) => row.start < viewportEnd).at(-1);
673
+ useLoadMoreOnEnd({
674
+ lastIndex: lastVisibleRow ? Math.min(files.length - 1, (lastVisibleRow.index + 1) * columns - 1) : void 0,
675
+ loaded: files.length,
676
+ hasMore,
677
+ loading: loadingMore,
678
+ onLoadMore
679
+ });
680
+ const prevSelected = useRef(selectedIds);
681
+ useEffect(() => {
682
+ const itemIndex = renamingId ? files.findIndex((file) => file.id === renamingId) : selectionEdgeIndex(files, selectedIds, prevSelected.current);
683
+ prevSelected.current = selectedIds;
684
+ if (itemIndex < 0) return;
685
+ virtualizer.scrollToIndex(Math.floor(itemIndex / columns), { align: "auto" });
686
+ }, [renamingId, selectedIds]);
687
+ return /* @__PURE__ */ jsxs(
688
+ "div",
533
689
  {
534
- file,
535
- idx,
536
- selected: selectedIds.has(file.id),
537
- dragging: draggingIds?.has(file.id) ?? file.id === draggingId,
538
- dropTarget: file.id === dropTargetId && file.kind === "folder",
539
- isProtected: protectedIds?.has(file.id) ?? false,
540
- draggingId,
541
- dropTargetId,
542
- coarse,
543
- folderLabel: labels.folder,
544
- inUseLabel: labels.inUse,
545
- actionsLabel: labels.itemActions,
546
- onSelect,
547
- onActivate,
548
- onContextMenu,
549
- onDragStart,
550
- onDragEnd,
551
- onDrop,
552
- onDragOverRow,
553
- renaming: file.id === renamingId,
554
- onRenameCommit,
555
- onRenameCancel
556
- },
557
- file.id
558
- )) });
690
+ ref: scrollRefProp ? void 0 : ownScrollRef,
691
+ className: cn(!scrollRefProp && "min-h-0 flex-1 overflow-y-auto", className),
692
+ children: [
693
+ /* @__PURE__ */ jsxs("div", { role: "list", className: "pt-3", children: [
694
+ /* @__PURE__ */ jsx("div", { role: "presentation", className: "relative z-0 w-full", style: { height: virtualizer.getTotalSize() }, children: virtualRows.map((virtualRow) => {
695
+ const start = virtualRow.index * columns;
696
+ const rowFiles = files.slice(start, start + columns);
697
+ return /* @__PURE__ */ jsx(
698
+ "div",
699
+ {
700
+ role: "presentation",
701
+ "data-index": virtualRow.index,
702
+ ref: virtualizer.measureElement,
703
+ className: "absolute left-0 top-0 grid w-full gap-1.5 px-3",
704
+ style: {
705
+ gridTemplateColumns: `repeat(${columns}, minmax(0, 1fr))`,
706
+ transform: `translateY(${virtualRow.start - virtualizer.options.scrollMargin}px)`
707
+ },
708
+ children: rowFiles.map((file, offset) => /* @__PURE__ */ jsx(
709
+ ExplorerGridItem,
710
+ {
711
+ file,
712
+ idx: start + offset,
713
+ selected: selectedIds.has(file.id),
714
+ dragging: draggingIds?.has(file.id) ?? file.id === draggingId,
715
+ dropTarget: file.id === dropTargetId && file.kind === "folder",
716
+ isProtected: protectedIds?.has(file.id) ?? false,
717
+ draggingId,
718
+ dropTargetId,
719
+ coarse,
720
+ folderLabel: labels.folder,
721
+ inUseLabel: labels.inUse,
722
+ actionsLabel: labels.itemActions,
723
+ onSelect,
724
+ onActivate,
725
+ onContextMenu,
726
+ onDragStart,
727
+ onDragEnd,
728
+ onDrop,
729
+ onDragOverRow,
730
+ renaming: file.id === renamingId,
731
+ onRenameCommit,
732
+ onRenameCancel
733
+ },
734
+ file.id
735
+ ))
736
+ },
737
+ virtualRow.key
738
+ );
739
+ }) }),
740
+ /* @__PURE__ */ jsx("div", { className: "h-3" })
741
+ ] }),
742
+ loadingMore ? /* @__PURE__ */ jsx("p", { className: "px-4 py-3 text-center font-mono text-[10px] uppercase tracking-[0.12em] text-muted-foreground", children: labels.loading }) : null
743
+ ]
744
+ }
745
+ );
559
746
  }
560
747
  function ExplorerGridItem({
561
748
  file,
@@ -612,7 +799,7 @@ function ExplorerGridItem({
612
799
  ) : /* @__PURE__ */ jsx("span", { className: cn("truncate text-[11px]", file.kind === "folder" && "text-primary"), children: file.name }),
613
800
  /* @__PURE__ */ jsx("span", { className: "font-mono text-[9px] uppercase tracking-[0.12em] text-muted-foreground", children: file.kind === "folder" ? folderLabel : formatSizeShort(file.size) })
614
801
  ] });
615
- return /* @__PURE__ */ jsxs("li", { className: "relative", children: [
802
+ return /* @__PURE__ */ jsxs("div", { role: "listitem", className: "relative", children: [
616
803
  coarse && onContextMenu ? /* @__PURE__ */ jsx(
617
804
  "button",
618
805
  {
@@ -1314,19 +1501,47 @@ function ExplorerClipboardToast({
1314
1501
  )
1315
1502
  ] });
1316
1503
  }
1504
+ function uploadQueuePercent(items) {
1505
+ const total = items.reduce((sum, item) => sum + item.size, 0);
1506
+ if (total <= 0) return items.length === 0 ? 0 : 100;
1507
+ const loaded = items.reduce((sum, item) => {
1508
+ if (item.status === "success") return sum + item.size;
1509
+ if (item.status === "uploading" || item.status === "error") {
1510
+ return sum + item.size * item.progress / 100;
1511
+ }
1512
+ return sum;
1513
+ }, 0);
1514
+ return Math.min(100, Math.round(loaded / total * 100));
1515
+ }
1317
1516
  function ExplorerUploadStatus({
1318
1517
  items,
1319
1518
  onDismiss,
1519
+ onCancel,
1520
+ onRemove,
1320
1521
  className
1321
1522
  }) {
1322
1523
  const labels = useExplorerLabels();
1524
+ const listRef = useRef(null);
1525
+ const activeId = items.find((item) => item.status === "uploading")?.id;
1526
+ useLayoutEffect(() => {
1527
+ const list = listRef.current;
1528
+ if (!list || !activeId) return;
1529
+ const row = list.querySelector(`[data-upload-id="${activeId}"]`);
1530
+ if (!row) return;
1531
+ const top = row.offsetTop;
1532
+ const bottom = top + row.offsetHeight;
1533
+ if (top < list.scrollTop) list.scrollTop = top;
1534
+ else if (bottom > list.scrollTop + list.clientHeight) {
1535
+ list.scrollTop = bottom - list.clientHeight;
1536
+ }
1537
+ }, [activeId]);
1323
1538
  if (items.length === 0) return null;
1324
1539
  const done = items.filter((item) => item.status === "success").length;
1325
1540
  const failed = items.filter((item) => item.status === "error").length;
1326
1541
  const active = items.some(
1327
1542
  (item) => item.status === "queued" || item.status === "uploading"
1328
1543
  );
1329
- const visible = items.slice(-6);
1544
+ const totalPercent = uploadQueuePercent(items);
1330
1545
  return /* @__PURE__ */ jsxs(
1331
1546
  "div",
1332
1547
  {
@@ -1350,20 +1565,44 @@ function ExplorerUploadStatus({
1350
1565
  failed
1351
1566
  ] }) : null
1352
1567
  ] }),
1568
+ /* @__PURE__ */ jsxs("span", { className: "ml-auto font-mono text-[10px] tabular-nums text-primary", children: [
1569
+ totalPercent,
1570
+ "%"
1571
+ ] }),
1572
+ active && onCancel ? /* @__PURE__ */ jsx(
1573
+ "button",
1574
+ {
1575
+ type: "button",
1576
+ className: "inline-flex h-7 items-center rounded-[10px] border border-border px-2 text-[10px] font-medium hover:bg-muted",
1577
+ onClick: onCancel,
1578
+ children: labels.cancelQueue
1579
+ }
1580
+ ) : null,
1353
1581
  !active ? /* @__PURE__ */ jsx(
1354
1582
  "button",
1355
1583
  {
1356
1584
  type: "button",
1357
1585
  "aria-label": labels.dismissUploads,
1358
- className: "ml-auto inline-flex size-7 items-center justify-center rounded-[10px] text-muted-foreground hover:bg-muted",
1586
+ className: "inline-flex size-7 items-center justify-center rounded-[10px] text-muted-foreground hover:bg-muted",
1359
1587
  onClick: onDismiss,
1360
1588
  children: /* @__PURE__ */ jsx(X, { "aria-hidden": "true", className: "size-4" })
1361
1589
  }
1362
- ) : /* @__PURE__ */ jsx("span", { className: "ml-auto" })
1590
+ ) : null
1363
1591
  ] }),
1364
- /* @__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: [
1592
+ /* @__PURE__ */ jsx("div", { className: "mb-2 h-1 overflow-hidden rounded-[10px] bg-muted", children: /* @__PURE__ */ jsx("div", { className: "h-full bg-primary", style: { width: `${totalPercent}%` } }) }),
1593
+ /* @__PURE__ */ jsx("ul", { ref: listRef, className: "flex max-h-48 min-h-0 flex-col gap-1.5 overflow-y-auto overscroll-contain", children: items.map((item) => /* @__PURE__ */ jsxs("li", { "data-upload-id": item.id, className: "min-w-0 shrink-0", children: [
1365
1594
  /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
1366
1595
  /* @__PURE__ */ jsx("span", { className: "min-w-0 flex-1 truncate text-xs", children: item.name }),
1596
+ onRemove ? /* @__PURE__ */ jsx(
1597
+ "button",
1598
+ {
1599
+ type: "button",
1600
+ "aria-label": `${labels.removeUpload} ${item.name}`,
1601
+ className: "inline-flex size-6 shrink-0 items-center justify-center rounded-[10px] text-muted-foreground hover:bg-muted",
1602
+ onClick: () => onRemove(item.id),
1603
+ children: /* @__PURE__ */ jsx(X, { "aria-hidden": "true", className: "size-3.5" })
1604
+ }
1605
+ ) : null,
1367
1606
  /* @__PURE__ */ jsx(
1368
1607
  "span",
1369
1608
  {
@@ -1390,6 +1629,11 @@ function useUploadQueue(options) {
1390
1629
  confirmUploadRef.current = options.confirmUpload;
1391
1630
  const queueRef = useRef([]);
1392
1631
  const running = useRef(false);
1632
+ const dropped = useRef(/* @__PURE__ */ new Set());
1633
+ const activeId = useRef(null);
1634
+ const stepAbort = useRef(null);
1635
+ const xhrRef = useRef(null);
1636
+ const xhrOwner = useRef(null);
1393
1637
  const [items, setItems] = useState([]);
1394
1638
  const publish = useCallback(() => {
1395
1639
  setItems(queueRef.current.map(({ content: _content, ...item }) => item));
@@ -1399,6 +1643,13 @@ function useUploadQueue(options) {
1399
1643
  const next = queueRef.current.find((item) => item.status === "queued");
1400
1644
  if (!next) return;
1401
1645
  running.current = true;
1646
+ activeId.current = next.id;
1647
+ const controller = new AbortController();
1648
+ stepAbort.current = controller;
1649
+ let confirming = false;
1650
+ const forget = () => {
1651
+ queueRef.current = queueRef.current.filter((item) => item.id !== next.id);
1652
+ };
1402
1653
  next.status = "uploading";
1403
1654
  next.progress = 0;
1404
1655
  publish();
@@ -1409,31 +1660,62 @@ function useUploadQueue(options) {
1409
1660
  type: next.type,
1410
1661
  content: next.content,
1411
1662
  parentId: next.parentId
1412
- });
1663
+ }, controller.signal);
1664
+ if (dropped.current.has(next.id)) {
1665
+ forget();
1666
+ return;
1667
+ }
1413
1668
  let lastPublish = 0;
1414
- await putFile(target, next, (progress) => {
1415
- next.progress = progress;
1416
- const now = Date.now();
1417
- if (progress === 100 || now - lastPublish > 200) {
1418
- lastPublish = now;
1419
- publish();
1669
+ await putFile(
1670
+ target,
1671
+ next,
1672
+ (progress) => {
1673
+ next.progress = progress;
1674
+ const now = Date.now();
1675
+ if (progress === 100 || now - lastPublish > 200) {
1676
+ lastPublish = now;
1677
+ publish();
1678
+ }
1679
+ },
1680
+ (xhr) => {
1681
+ xhrRef.current = xhr;
1682
+ xhrOwner.current = next.id;
1420
1683
  }
1421
- });
1684
+ );
1685
+ if (dropped.current.has(next.id)) {
1686
+ forget();
1687
+ return;
1688
+ }
1689
+ confirming = true;
1422
1690
  await confirmUploadRef.current?.({
1423
1691
  name: next.name,
1424
1692
  size: next.size,
1425
1693
  type: next.type,
1426
1694
  content: next.content
1427
- });
1695
+ }, controller.signal);
1696
+ dropped.current.delete(next.id);
1428
1697
  next.status = "success";
1429
1698
  next.progress = 100;
1430
1699
  if (typeof window !== "undefined") {
1431
1700
  window.dispatchEvent(new Event("file-next-uploaded"));
1432
1701
  }
1433
1702
  } catch (error) {
1703
+ const aborted = error instanceof Error && error.name === "AbortError";
1704
+ if (!confirming && dropped.current.has(next.id) || confirming && aborted) {
1705
+ forget();
1706
+ return;
1707
+ }
1434
1708
  next.status = "error";
1435
1709
  next.error = error instanceof Error ? error.message : "Upload failed";
1436
1710
  } finally {
1711
+ if (activeId.current === next.id) {
1712
+ activeId.current = null;
1713
+ stepAbort.current = null;
1714
+ }
1715
+ if (xhrOwner.current === next.id) {
1716
+ xhrRef.current = null;
1717
+ xhrOwner.current = null;
1718
+ }
1437
1719
  publish();
1438
1720
  running.current = false;
1439
1721
  void pump();
@@ -1458,6 +1740,26 @@ function useUploadQueue(options) {
1458
1740
  },
1459
1741
  [publish, pump]
1460
1742
  );
1743
+ const dropIds = useCallback(
1744
+ (ids) => {
1745
+ if (ids.length === 0) return;
1746
+ for (const id of ids) dropped.current.add(id);
1747
+ if (xhrOwner.current && ids.includes(xhrOwner.current)) xhrRef.current?.abort();
1748
+ if (activeId.current && ids.includes(activeId.current)) stepAbort.current?.abort();
1749
+ const idSet = new Set(ids);
1750
+ queueRef.current = queueRef.current.filter(
1751
+ (item) => !idSet.has(item.id) || item.id === activeId.current
1752
+ );
1753
+ publish();
1754
+ },
1755
+ [publish]
1756
+ );
1757
+ const remove = useCallback((id) => dropIds([id]), [dropIds]);
1758
+ const cancel = useCallback(() => {
1759
+ dropIds(
1760
+ queueRef.current.filter((item) => item.status === "queued" || item.status === "uploading").map((item) => item.id)
1761
+ );
1762
+ }, [dropIds]);
1461
1763
  const dismiss = useCallback(() => {
1462
1764
  queueRef.current = queueRef.current.filter(
1463
1765
  (item) => item.status === "queued" || item.status === "uploading"
@@ -1477,11 +1779,19 @@ function useUploadQueue(options) {
1477
1779
  window.clearTimeout(timeout);
1478
1780
  };
1479
1781
  }, [active, dismiss, itemCount]);
1480
- return { items, enqueue, dismiss, active };
1782
+ return { items, enqueue, dismiss, cancel, remove, active };
1481
1783
  }
1482
- function putFile(target, file, onProgress) {
1784
+ function putFile(target, file, onProgress, onStart) {
1483
1785
  return new Promise((resolve, reject) => {
1786
+ let settled = false;
1787
+ const finish = (fn) => {
1788
+ if (settled) return;
1789
+ settled = true;
1790
+ fn();
1791
+ };
1484
1792
  const xhr = new XMLHttpRequest();
1793
+ onStart(xhr);
1794
+ xhr.addEventListener("abort", () => finish(() => reject(new Error("aborted"))));
1485
1795
  xhr.upload.addEventListener("progress", (event) => {
1486
1796
  if (event.lengthComputable) {
1487
1797
  onProgress(Math.min(100, Math.round(event.loaded / event.total * 100)));
@@ -1495,13 +1805,13 @@ function putFile(target, file, onProgress) {
1495
1805
  if (parsed.error?.message) message = parsed.error.message;
1496
1806
  } catch {
1497
1807
  }
1498
- reject(new Error(message));
1808
+ finish(() => reject(new Error(message)));
1499
1809
  return;
1500
1810
  }
1501
- resolve();
1811
+ finish(resolve);
1502
1812
  });
1503
1813
  xhr.addEventListener("error", () => {
1504
- reject(new Error("Upload failed"));
1814
+ finish(() => reject(new Error("Upload failed")));
1505
1815
  });
1506
1816
  xhr.open(target.method ?? "PUT", target.url);
1507
1817
  xhr.setRequestHeader("Content-Type", file.type || "application/octet-stream");
@@ -1522,8 +1832,16 @@ function UploadQueueProvider({
1522
1832
  }) {
1523
1833
  const uploads = useUploadQueue({ requestUpload, confirmUpload });
1524
1834
  const panel = useMemo(
1525
- () => /* @__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 }) }),
1526
- [uploads.dismiss, uploads.items]
1835
+ () => /* @__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(
1836
+ ExplorerUploadStatus,
1837
+ {
1838
+ items: uploads.items,
1839
+ onDismiss: uploads.dismiss,
1840
+ onCancel: uploads.cancel,
1841
+ onRemove: uploads.remove
1842
+ }
1843
+ ) }),
1844
+ [uploads.cancel, uploads.dismiss, uploads.items, uploads.remove]
1527
1845
  );
1528
1846
  return /* @__PURE__ */ jsxs(EnqueueContext.Provider, { value: uploads.enqueue, children: [
1529
1847
  children,
@@ -1659,6 +1977,7 @@ function FileExplorerInner(props) {
1659
1977
  onMove
1660
1978
  });
1661
1979
  const uploadInputRef = useRef(null);
1980
+ const scrollRef = useRef(null);
1662
1981
  const [query, setQuery] = useState("");
1663
1982
  const [trashOpen, setTrashOpen] = useState(false);
1664
1983
  const [pageItems, setPageItems] = useState([]);
@@ -1817,6 +2136,10 @@ function FileExplorerInner(props) {
1817
2136
  },
1818
2137
  [listTrash, query, searchFiles, trashOpen]
1819
2138
  );
2139
+ const loadNext = useCallback(() => {
2140
+ if (overlayFiles) void loadOverlay("more");
2141
+ else void loadBrowse("more");
2142
+ }, [loadBrowse, loadOverlay, overlayFiles]);
1820
2143
  const refreshTrashFlag = useCallback(async () => {
1821
2144
  if (!listTrash) {
1822
2145
  setTrashHasItems(false);
@@ -2120,7 +2443,7 @@ function FileExplorerInner(props) {
2120
2443
  }
2121
2444
  )
2122
2445
  ] }),
2123
- /* @__PURE__ */ jsx("div", { className: "min-h-0 flex-1 overflow-y-auto", children: (overlayLoading || pageStatus === "loading") && displayFiles.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 }) : displayFiles.length === 0 ? /* @__PURE__ */ jsxs("div", { className: "flex min-h-full flex-col justify-center px-4 py-16 text-center", children: [
2446
+ /* @__PURE__ */ jsx("div", { ref: scrollRef, className: "min-h-0 flex-1 overflow-y-auto overflow-x-auto overscroll-x-contain", children: (overlayLoading || pageStatus === "loading") && displayFiles.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 }) : displayFiles.length === 0 ? /* @__PURE__ */ jsxs("div", { className: "flex min-h-full flex-col justify-center px-4 py-16 text-center", children: [
2124
2447
  /* @__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 }),
2125
2448
  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
2126
2449
  ] }) : explorer.view === "list" ? /* @__PURE__ */ jsx(
@@ -2151,7 +2474,11 @@ function FileExplorerInner(props) {
2151
2474
  onSortChange: handleSortChange,
2152
2475
  renamingId,
2153
2476
  onRenameCommit: commitRename,
2154
- onRenameCancel: cancelRename
2477
+ onRenameCancel: cancelRename,
2478
+ scrollRef,
2479
+ hasMore,
2480
+ loadingMore: loadingMore || overlayMore,
2481
+ onLoadMore: loadNext
2155
2482
  }
2156
2483
  )
2157
2484
  }
@@ -2180,24 +2507,15 @@ function FileExplorerInner(props) {
2180
2507
  onDragOverRow: explorer.setDropTarget,
2181
2508
  renamingId,
2182
2509
  onRenameCommit: commitRename,
2183
- onRenameCancel: cancelRename
2510
+ onRenameCancel: cancelRename,
2511
+ scrollRef,
2512
+ hasMore,
2513
+ loadingMore: loadingMore || overlayMore,
2514
+ onLoadMore: loadNext
2184
2515
  }
2185
2516
  )
2186
2517
  }
2187
2518
  ) }),
2188
- hasMore && sortedFiles.length > 0 ? /* @__PURE__ */ jsx("div", { className: "shrink-0 border-t border-border p-3", children: /* @__PURE__ */ jsx(
2189
- "button",
2190
- {
2191
- type: "button",
2192
- disabled: loadingMore || overlayMore,
2193
- onClick: () => {
2194
- if (overlayFiles) void loadOverlay("more");
2195
- else void loadBrowse("more");
2196
- },
2197
- 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",
2198
- children: loadingMore || overlayMore ? labels.loading : labels.loadMore
2199
- }
2200
- ) }) : null,
2201
2519
  /* @__PURE__ */ jsxs("footer", { className: "flex shrink-0 items-center justify-between gap-3 border-t border-border px-4 py-2", children: [
2202
2520
  /* @__PURE__ */ jsxs("span", { className: "font-mono text-[10px] uppercase tracking-[0.12em] text-muted-foreground", children: [
2203
2521
  /* @__PURE__ */ jsxs("span", { className: "text-primary tabular-nums", children: [
@@ -2378,18 +2696,27 @@ function ErrorState(props) {
2378
2696
  }
2379
2697
  );
2380
2698
  }
2699
+ var MIN_ZOOM = 1;
2700
+ var MAX_ZOOM = 4;
2701
+ function clampPreviewZoom(scale) {
2702
+ return Math.min(MAX_ZOOM, Math.max(MIN_ZOOM, Number.isFinite(scale) ? scale : 1));
2703
+ }
2381
2704
  function canPreviewFile(file) {
2382
2705
  return previewKind(file.mimeType, file.name) !== "none";
2383
2706
  }
2384
2707
  function previewKind(mimeType, name) {
2385
2708
  const mime = mimeType.toLowerCase();
2386
2709
  const ext = name.split(".").pop()?.toLowerCase() ?? "";
2387
- if (mime.startsWith("image/") || ["png", "jpg", "jpeg", "gif", "webp"].includes(ext)) {
2710
+ if (mime === "text/html" || mime === "application/xhtml+xml" || ext === "html" || ext === "htm") {
2711
+ return "none";
2712
+ }
2713
+ if (mime.startsWith("image/") || ["png", "jpg", "jpeg", "gif", "webp", "avif"].includes(ext)) {
2388
2714
  return "image";
2389
2715
  }
2390
2716
  if (mime === "application/pdf" || ext === "pdf") return "pdf";
2391
- if (mime.startsWith("video/") || ["mp4", "webm", "mov"].includes(ext)) return "video";
2392
- if (mime.startsWith("text/") || ["txt", "csv"].includes(ext)) return "text";
2717
+ if (mime.startsWith("video/") || ["mp4", "webm"].includes(ext)) return "video";
2718
+ if (mime.startsWith("audio/") || ["mp3", "wav", "ogg"].includes(ext)) return "audio";
2719
+ if (mime.startsWith("text/") || ["txt", "csv", "md"].includes(ext)) return "text";
2393
2720
  return "none";
2394
2721
  }
2395
2722
  function FilePreviewDialog({
@@ -2399,46 +2726,162 @@ function FilePreviewDialog({
2399
2726
  onDownload
2400
2727
  }) {
2401
2728
  const labels = useExplorerLabels();
2729
+ const [scale, setScale] = useState(1);
2730
+ const [pan, setPan] = useState({ x: 0, y: 0 });
2731
+ const drag = useRef(null);
2732
+ const closeRef = useRef(null);
2733
+ useEffect(() => {
2734
+ setScale(1);
2735
+ setPan({ x: 0, y: 0 });
2736
+ closeRef.current?.focus();
2737
+ }, [file?.id, src]);
2738
+ useEffect(() => {
2739
+ const onKey = (event) => {
2740
+ if (event.key === "Escape") onClose();
2741
+ };
2742
+ window.addEventListener("keydown", onKey);
2743
+ return () => window.removeEventListener("keydown", onKey);
2744
+ }, [onClose]);
2402
2745
  if (!file) return null;
2403
2746
  const kind = previewKind(file.mimeType, file.name);
2404
- return /* @__PURE__ */ jsxs(Overlay, { open: true, onClose, className: "max-w-4xl", children: [
2405
- /* @__PURE__ */ jsx("h2", { className: "truncate pr-6 text-base font-semibold", children: file.name }),
2406
- /* @__PURE__ */ jsx("p", { className: "mt-1 text-sm text-muted-foreground", children: file.mimeType || labels.unknownType }),
2407
- /* @__PURE__ */ jsx("div", { className: "mt-3", children: kind === "image" ? /* @__PURE__ */ jsx(
2408
- "img",
2409
- {
2410
- src,
2411
- alt: file.name,
2412
- className: "max-h-[70vh] w-full rounded-[10px] object-contain"
2413
- }
2414
- ) : kind === "pdf" || kind === "text" ? /* @__PURE__ */ jsx(
2415
- "iframe",
2416
- {
2417
- src,
2418
- title: file.name,
2419
- className: "h-[70vh] w-full rounded-[10px] border border-border bg-background"
2420
- }
2421
- ) : kind === "video" ? /* @__PURE__ */ jsx(
2422
- "video",
2423
- {
2424
- controls: true,
2425
- playsInline: true,
2426
- preload: "metadata",
2427
- src,
2428
- className: "max-h-[70vh] w-full rounded-[10px] bg-foreground",
2429
- children: labels.previewUnavailable
2430
- }
2431
- ) : /* @__PURE__ */ jsx("p", { className: "text-sm text-muted-foreground", children: labels.previewUnavailable }) }),
2432
- /* @__PURE__ */ jsx("div", { className: "mt-4 flex justify-end", children: /* @__PURE__ */ jsx(
2433
- "button",
2434
- {
2435
- type: "button",
2436
- className: "inline-flex h-8 items-center rounded-[10px] border border-border px-3 text-sm",
2437
- onClick: () => onDownload(file),
2438
- children: labels.download
2439
- }
2440
- ) })
2441
- ] });
2747
+ const zoom = (next) => {
2748
+ const clamped = clampPreviewZoom(next);
2749
+ setScale(clamped);
2750
+ if (clamped === 1) setPan({ x: 0, y: 0 });
2751
+ };
2752
+ const stop = (event) => event.stopPropagation();
2753
+ return /* @__PURE__ */ jsxs(
2754
+ "div",
2755
+ {
2756
+ role: "dialog",
2757
+ "aria-modal": "true",
2758
+ "aria-label": file.name,
2759
+ className: "fixed inset-0 z-50 bg-black/90 text-white",
2760
+ onClick: onClose,
2761
+ children: [
2762
+ /* @__PURE__ */ jsxs("div", { className: "absolute inset-x-0 top-0 z-10 flex items-center gap-2 px-3 py-2", onClick: stop, children: [
2763
+ /* @__PURE__ */ jsx("p", { className: "min-w-0 flex-1 truncate text-sm", children: file.name }),
2764
+ /* @__PURE__ */ jsx(
2765
+ "button",
2766
+ {
2767
+ type: "button",
2768
+ className: "inline-flex h-8 items-center rounded-full px-3 text-sm hover:bg-white/10",
2769
+ onClick: () => onDownload(file),
2770
+ children: labels.download
2771
+ }
2772
+ ),
2773
+ /* @__PURE__ */ jsx(
2774
+ "button",
2775
+ {
2776
+ ref: closeRef,
2777
+ type: "button",
2778
+ "aria-label": labels.closePreview,
2779
+ className: "inline-flex size-8 items-center justify-center rounded-full hover:bg-white/10",
2780
+ onClick: onClose,
2781
+ children: /* @__PURE__ */ jsx(X, { "aria-hidden": "true", className: "size-4" })
2782
+ }
2783
+ )
2784
+ ] }),
2785
+ /* @__PURE__ */ jsx("div", { className: "flex h-full items-center justify-center px-4 pt-12 pb-16", children: kind === "image" ? /* @__PURE__ */ jsx(
2786
+ "img",
2787
+ {
2788
+ src,
2789
+ alt: file.name,
2790
+ draggable: false,
2791
+ className: "max-h-full max-w-full cursor-zoom-in select-none object-contain",
2792
+ style: { transform: `translate(${pan.x}px, ${pan.y}px) scale(${scale})` },
2793
+ onClick: stop,
2794
+ onDoubleClick: (event) => {
2795
+ stop(event);
2796
+ zoom(scale === 1 ? 2 : 1);
2797
+ },
2798
+ onWheel: (event) => {
2799
+ event.preventDefault();
2800
+ stop(event);
2801
+ zoom(scale + (event.deltaY < 0 ? 0.25 : -0.25));
2802
+ },
2803
+ onPointerDown: (event) => {
2804
+ if (scale === 1) return;
2805
+ stop(event);
2806
+ event.currentTarget.setPointerCapture(event.pointerId);
2807
+ drag.current = { x: event.clientX, y: event.clientY, px: pan.x, py: pan.y };
2808
+ },
2809
+ onPointerMove: (event) => {
2810
+ if (!drag.current) return;
2811
+ setPan({
2812
+ x: drag.current.px + event.clientX - drag.current.x,
2813
+ y: drag.current.py + event.clientY - drag.current.y
2814
+ });
2815
+ },
2816
+ onPointerUp: () => {
2817
+ drag.current = null;
2818
+ }
2819
+ }
2820
+ ) : kind === "pdf" || kind === "text" ? /* @__PURE__ */ jsx(
2821
+ "iframe",
2822
+ {
2823
+ src,
2824
+ title: file.name,
2825
+ className: "h-full w-full max-w-5xl bg-white",
2826
+ onClick: stop
2827
+ }
2828
+ ) : kind === "video" ? /* @__PURE__ */ jsx(
2829
+ "video",
2830
+ {
2831
+ controls: true,
2832
+ playsInline: true,
2833
+ preload: "metadata",
2834
+ src,
2835
+ className: "max-h-full max-w-full",
2836
+ onClick: stop,
2837
+ children: labels.previewUnavailable
2838
+ }
2839
+ ) : kind === "audio" ? /* @__PURE__ */ jsx("audio", { controls: true, preload: "metadata", src, className: "w-full max-w-md", onClick: stop }) : /* @__PURE__ */ jsx("p", { className: "text-sm text-white/70", children: labels.previewUnavailable }) }),
2840
+ kind === "image" ? /* @__PURE__ */ jsxs(
2841
+ "div",
2842
+ {
2843
+ className: "absolute bottom-4 left-1/2 flex -translate-x-1/2 items-center gap-1 rounded-full bg-black/70 px-1 py-1",
2844
+ onClick: stop,
2845
+ children: [
2846
+ /* @__PURE__ */ jsx(
2847
+ "button",
2848
+ {
2849
+ type: "button",
2850
+ "aria-label": labels.zoomOut,
2851
+ className: "inline-flex size-8 items-center justify-center rounded-full hover:bg-white/10",
2852
+ onClick: () => zoom(scale - 0.25),
2853
+ children: /* @__PURE__ */ jsx(Minus, { "aria-hidden": "true", className: "size-4" })
2854
+ }
2855
+ ),
2856
+ /* @__PURE__ */ jsxs(
2857
+ "button",
2858
+ {
2859
+ type: "button",
2860
+ "aria-label": labels.zoomFit,
2861
+ className: "min-w-14 px-2 text-xs tabular-nums",
2862
+ onClick: () => zoom(1),
2863
+ children: [
2864
+ Math.round(scale * 100),
2865
+ "%"
2866
+ ]
2867
+ }
2868
+ ),
2869
+ /* @__PURE__ */ jsx(
2870
+ "button",
2871
+ {
2872
+ type: "button",
2873
+ "aria-label": labels.zoomIn,
2874
+ className: "inline-flex size-8 items-center justify-center rounded-full hover:bg-white/10",
2875
+ onClick: () => zoom(scale + 0.25),
2876
+ children: /* @__PURE__ */ jsx(Plus, { "aria-hidden": "true", className: "size-4" })
2877
+ }
2878
+ )
2879
+ ]
2880
+ }
2881
+ ) : null
2882
+ ]
2883
+ }
2884
+ );
2442
2885
  }
2443
2886
  function CreateFolderDialog({
2444
2887
  open,