@vryzel/file-next-ui 0.4.2 → 0.4.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -2,6 +2,8 @@
2
2
 
3
3
  Ready-made file explorer for Next.js. Import `FileExplorer`, pass server callbacks, done. Or compose the pieces.
4
4
 
5
+ [Live demo](https://file-next-test-production.up.railway.app)
6
+
5
7
  ```bash
6
8
  pnpm add @vryzel/file-next-ui
7
9
  ```
package/dist/index.cjs CHANGED
@@ -149,6 +149,62 @@ function useExplorerLabels() {
149
149
  function formatLabel(template, vars) {
150
150
  return template.replace(/\{(\w+)\}/g, (_, key) => String(vars[key] ?? ""));
151
151
  }
152
+ function RenameInput({
153
+ name,
154
+ kind,
155
+ ariaLabel,
156
+ onCommit,
157
+ onCancel,
158
+ className
159
+ }) {
160
+ const ref = react.useRef(null);
161
+ const done = react.useRef(false);
162
+ react.useEffect(() => {
163
+ const el = ref.current;
164
+ if (!el) return;
165
+ el.focus();
166
+ const dot = name.lastIndexOf(".");
167
+ if (kind === "file" && dot > 0) el.setSelectionRange(0, dot);
168
+ else el.select();
169
+ }, [kind, name]);
170
+ const finish = (next) => {
171
+ if (done.current) return;
172
+ done.current = true;
173
+ const trimmed = next?.trim() ?? "";
174
+ if (!trimmed) {
175
+ onCancel();
176
+ return;
177
+ }
178
+ onCommit(trimmed);
179
+ };
180
+ return /* @__PURE__ */ jsxRuntime.jsx(
181
+ "input",
182
+ {
183
+ ref,
184
+ defaultValue: name,
185
+ "aria-label": ariaLabel ?? "Rename",
186
+ className: cn(
187
+ "h-7 min-w-0 flex-1 rounded-[6px] border border-ring bg-background px-1.5 text-sm outline-none",
188
+ className
189
+ ),
190
+ onClick: (event) => event.stopPropagation(),
191
+ onDoubleClick: (event) => event.stopPropagation(),
192
+ onPointerDown: (event) => event.stopPropagation(),
193
+ onKeyDown: (event) => {
194
+ event.stopPropagation();
195
+ if (event.key === "Enter") {
196
+ event.preventDefault();
197
+ finish(event.currentTarget.value);
198
+ }
199
+ if (event.key === "Escape") {
200
+ event.preventDefault();
201
+ finish(null);
202
+ }
203
+ },
204
+ onBlur: (event) => finish(event.currentTarget.value)
205
+ }
206
+ );
207
+ }
152
208
  var LONG_PRESS_MS = 450;
153
209
  var MOVE_CANCEL_PX = 10;
154
210
  function useCoarsePointer() {
@@ -246,6 +302,9 @@ function ExplorerListView(props) {
246
302
  sortKey,
247
303
  sortDirection,
248
304
  onSortChange,
305
+ renamingId,
306
+ onRenameCommit,
307
+ onRenameCancel,
249
308
  className
250
309
  } = props;
251
310
  const labels = useExplorerLabels();
@@ -286,7 +345,10 @@ function ExplorerListView(props) {
286
345
  onDragStart,
287
346
  onDragEnd,
288
347
  onDrop,
289
- onDragOverRow
348
+ onDragOverRow,
349
+ renaming: file.id === renamingId,
350
+ onRenameCommit,
351
+ onRenameCancel
290
352
  },
291
353
  file.id
292
354
  )) })
@@ -308,7 +370,10 @@ function ExplorerListRow({
308
370
  onDragStart,
309
371
  onDragEnd,
310
372
  onDrop,
311
- onDragOverRow
373
+ onDragOverRow,
374
+ renaming,
375
+ onRenameCommit,
376
+ onRenameCancel
312
377
  }) {
313
378
  const labels = useExplorerLabels();
314
379
  const isFolder = file.kind === "folder";
@@ -319,17 +384,18 @@ function ExplorerListRow({
319
384
  role: "row",
320
385
  "data-file-id": file.id,
321
386
  "aria-selected": selected,
322
- draggable: !coarse,
387
+ draggable: !coarse && !renaming,
323
388
  onDragStart: (e) => {
324
389
  e.dataTransfer.setData(DRAG_MIME, file.id);
325
390
  e.dataTransfer.effectAllowed = "move";
326
391
  onDragStart(file.id);
327
392
  },
328
393
  onDragEnd,
329
- onPointerDown: pointer.onPointerDown,
330
- onClick: pointer.onClick,
331
- onDoubleClick: pointer.onDoubleClick,
394
+ onPointerDown: renaming ? void 0 : pointer.onPointerDown,
395
+ onClick: renaming ? void 0 : pointer.onClick,
396
+ onDoubleClick: renaming ? void 0 : pointer.onDoubleClick,
332
397
  onKeyDown: (e) => {
398
+ if (renaming) return;
333
399
  if (e.key === "Enter") {
334
400
  e.preventDefault();
335
401
  onActivate(file);
@@ -371,7 +437,16 @@ function ExplorerListRow({
371
437
  /* @__PURE__ */ jsxRuntime.jsx("span", { className: "font-mono text-[10px] uppercase tracking-[0.12em] text-muted-foreground tabular-nums", children: String(idx + 1).padStart(2, "0") }),
372
438
  /* @__PURE__ */ jsxRuntime.jsx("span", { className: isFolder ? "text-primary" : "text-muted-foreground", children: /* @__PURE__ */ jsxRuntime.jsx(FileIcon, { kind: file.kind, mimeType: file.mimeType, className: "size-5 text-current sm:size-4" }) }),
373
439
  /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "flex min-w-0 items-center gap-1.5", children: [
374
- /* @__PURE__ */ jsxRuntime.jsx("span", { className: cn("truncate", isFolder && "text-primary"), children: file.name }),
440
+ renaming && onRenameCommit && onRenameCancel ? /* @__PURE__ */ jsxRuntime.jsx(
441
+ RenameInput,
442
+ {
443
+ name: file.name,
444
+ kind: file.kind,
445
+ ariaLabel: labels.rename,
446
+ onCommit: onRenameCommit,
447
+ onCancel: onRenameCancel
448
+ }
449
+ ) : /* @__PURE__ */ jsxRuntime.jsx("span", { className: cn("truncate", isFolder && "text-primary"), children: file.name }),
375
450
  isProtected ? /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "flex shrink-0 items-center gap-0.5 rounded-[10px] bg-primary/10 px-1.5 text-[10px] text-primary", children: [
376
451
  /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Link2, { "aria-hidden": "true", className: "size-2.5" }),
377
452
  labels.inUse
@@ -448,6 +523,9 @@ function ExplorerGridView(props) {
448
523
  draggingIds,
449
524
  dropTargetId,
450
525
  onDragOverRow,
526
+ renamingId,
527
+ onRenameCommit,
528
+ onRenameCancel,
451
529
  className
452
530
  } = props;
453
531
  const labels = useExplorerLabels();
@@ -473,7 +551,10 @@ function ExplorerGridView(props) {
473
551
  onDragStart,
474
552
  onDragEnd,
475
553
  onDrop,
476
- onDragOverRow
554
+ onDragOverRow,
555
+ renaming: file.id === renamingId,
556
+ onRenameCommit,
557
+ onRenameCancel
477
558
  },
478
559
  file.id
479
560
  )) });
@@ -497,9 +578,42 @@ function ExplorerGridItem({
497
578
  onDragStart,
498
579
  onDragEnd,
499
580
  onDrop,
500
- onDragOverRow
581
+ onDragOverRow,
582
+ renaming,
583
+ onRenameCommit,
584
+ onRenameCancel
501
585
  }) {
586
+ const labels = useExplorerLabels();
502
587
  const pointer = useExplorerItemPointer({ file, coarse, onSelect, onActivate });
588
+ const bodyClass = cn(
589
+ "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",
590
+ "touch-manipulation select-none hover:bg-muted focus-visible:ring-2 focus-visible:ring-ring",
591
+ selected && "border-primary bg-muted",
592
+ dragging && "opacity-50",
593
+ dropTarget && "ring-2 ring-primary ring-inset"
594
+ );
595
+ const inner = /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
596
+ /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "flex items-center gap-1 pr-8", children: [
597
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "font-mono text-[9px] uppercase tracking-[0.12em] text-muted-foreground tabular-nums", children: String(idx + 1).padStart(2, "0") }),
598
+ isProtected ? /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "flex items-center gap-0.5 rounded-[10px] bg-primary/10 px-1.5 py-0.5 text-[10px] text-primary", children: [
599
+ /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Link2, { "aria-hidden": "true", className: "size-2.5" }),
600
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "sr-only", children: inUseLabel })
601
+ ] }) : null
602
+ ] }),
603
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: cn("flex items-center justify-center py-1", file.kind === "folder" ? "text-primary" : "text-muted-foreground"), children: /* @__PURE__ */ jsxRuntime.jsx(FileIcon, { kind: file.kind, mimeType: file.mimeType, className: "size-12 text-current" }) }),
604
+ renaming && onRenameCommit && onRenameCancel ? /* @__PURE__ */ jsxRuntime.jsx(
605
+ RenameInput,
606
+ {
607
+ name: file.name,
608
+ kind: file.kind,
609
+ ariaLabel: labels.rename,
610
+ className: "text-[11px]",
611
+ onCommit: onRenameCommit,
612
+ onCancel: onRenameCancel
613
+ }
614
+ ) : /* @__PURE__ */ jsxRuntime.jsx("span", { className: cn("truncate text-[11px]", file.kind === "folder" && "text-primary"), children: file.name }),
615
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "font-mono text-[9px] uppercase tracking-[0.12em] text-muted-foreground", children: file.kind === "folder" ? folderLabel : formatSizeShort(file.size) })
616
+ ] });
503
617
  return /* @__PURE__ */ jsxRuntime.jsxs("li", { className: "relative", children: [
504
618
  coarse && onContextMenu ? /* @__PURE__ */ jsxRuntime.jsx(
505
619
  "button",
@@ -515,7 +629,7 @@ function ExplorerGridItem({
515
629
  children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.MoreHorizontal, { "aria-hidden": "true", className: "size-5" })
516
630
  }
517
631
  ) : null,
518
- /* @__PURE__ */ jsxRuntime.jsxs(
632
+ renaming ? /* @__PURE__ */ jsxRuntime.jsx("div", { "data-file-id": file.id, className: bodyClass, children: inner }) : /* @__PURE__ */ jsxRuntime.jsx(
519
633
  "button",
520
634
  {
521
635
  type: "button",
@@ -557,25 +671,8 @@ function ExplorerGridItem({
557
671
  e.preventDefault();
558
672
  onDrop(file.id);
559
673
  },
560
- className: cn(
561
- "group relative flex w-full flex-col items-stretch gap-1 overflow-hidden rounded-[10px] border border-border bg-background p-2 text-left outline-none",
562
- "touch-manipulation select-none hover:bg-muted focus-visible:ring-2 focus-visible:ring-ring",
563
- selected && "border-primary bg-muted",
564
- dragging && "opacity-50",
565
- dropTarget && "ring-2 ring-primary ring-inset"
566
- ),
567
- children: [
568
- /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "flex items-center gap-1 pr-8", children: [
569
- /* @__PURE__ */ jsxRuntime.jsx("span", { className: "font-mono text-[9px] uppercase tracking-[0.12em] text-muted-foreground tabular-nums", children: String(idx + 1).padStart(2, "0") }),
570
- isProtected ? /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "flex items-center gap-0.5 rounded-[10px] bg-primary/10 px-1.5 py-0.5 text-[10px] text-primary", children: [
571
- /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Link2, { "aria-hidden": "true", className: "size-2.5" }),
572
- /* @__PURE__ */ jsxRuntime.jsx("span", { className: "sr-only", children: inUseLabel })
573
- ] }) : null
574
- ] }),
575
- /* @__PURE__ */ jsxRuntime.jsx("span", { className: cn("flex items-center justify-center py-1", file.kind === "folder" ? "text-primary" : "text-muted-foreground"), children: /* @__PURE__ */ jsxRuntime.jsx(FileIcon, { kind: file.kind, mimeType: file.mimeType, className: "size-12 text-current" }) }),
576
- /* @__PURE__ */ jsxRuntime.jsx("span", { className: cn("truncate text-[11px]", file.kind === "folder" && "text-primary"), children: file.name }),
577
- /* @__PURE__ */ jsxRuntime.jsx("span", { className: "font-mono text-[9px] uppercase tracking-[0.12em] text-muted-foreground", children: file.kind === "folder" ? folderLabel : formatSizeShort(file.size) })
578
- ]
674
+ className: bodyClass,
675
+ children: inner
579
676
  }
580
677
  )
581
678
  ] });
@@ -744,6 +841,13 @@ function swallowClickThrough() {
744
841
  window.removeEventListener("pointerup", swallow, true);
745
842
  }, 400);
746
843
  }
844
+
845
+ // src/share-url.ts
846
+ function absoluteShareUrl(link) {
847
+ if (link.startsWith("http://") || link.startsWith("https://")) return link;
848
+ const origin = typeof window === "undefined" ? "" : window.location.origin;
849
+ return `${origin}${link.startsWith("/") ? link : `/${link}`}`;
850
+ }
747
851
  function ExplorerContextMenu(props) {
748
852
  const {
749
853
  target,
@@ -896,7 +1000,7 @@ function ExplorerContextMenu(props) {
896
1000
  /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Copy, { "aria-hidden": "true", className: "size-4" }),
897
1001
  labels.paste
898
1002
  ] }) : null,
899
- actions.createShare ? /* @__PURE__ */ jsxRuntime.jsxs(
1003
+ target.file.kind === "file" && actions.createShare ? /* @__PURE__ */ jsxRuntime.jsxs(
900
1004
  "button",
901
1005
  {
902
1006
  type: "button",
@@ -905,7 +1009,7 @@ function ExplorerContextMenu(props) {
905
1009
  onClick: () => {
906
1010
  close();
907
1011
  void actions.createShare({ id: target.file.id }).then(
908
- (token) => navigator.clipboard.writeText(token)
1012
+ (link) => navigator.clipboard.writeText(absoluteShareUrl(link))
909
1013
  );
910
1014
  },
911
1015
  children: [
@@ -965,92 +1069,6 @@ function Overlay({
965
1069
  }
966
1070
  );
967
1071
  }
968
- function CreateFolderDialog({
969
- open,
970
- onOpenChange,
971
- onCreate,
972
- title,
973
- description,
974
- confirmLabel,
975
- initialName = ""
976
- }) {
977
- const labels = useExplorerLabels();
978
- const [name, setName] = react.useState(initialName);
979
- const [saving, setSaving] = react.useState(false);
980
- const [error, setError] = react.useState(null);
981
- const inputRef = react.useRef(null);
982
- react.useEffect(() => {
983
- if (!open) {
984
- setSaving(false);
985
- setError(null);
986
- return;
987
- }
988
- setName(initialName);
989
- const id = window.setTimeout(() => {
990
- inputRef.current?.focus();
991
- inputRef.current?.select();
992
- }, 0);
993
- return () => window.clearTimeout(id);
994
- }, [initialName, open]);
995
- async function submit() {
996
- const trimmed = name.trim();
997
- if (!trimmed || saving) return;
998
- setSaving(true);
999
- setError(null);
1000
- try {
1001
- await onCreate(trimmed);
1002
- onOpenChange(false);
1003
- } catch (cause) {
1004
- const code = cause && typeof cause === "object" && "code" in cause ? String(cause.code) : "";
1005
- setError(code === "Conflict" ? labels.folderExists : labels.folderCreateFailed);
1006
- } finally {
1007
- setSaving(false);
1008
- }
1009
- }
1010
- return /* @__PURE__ */ jsxRuntime.jsxs(Overlay, { open, onClose: () => onOpenChange(false), children: [
1011
- /* @__PURE__ */ jsxRuntime.jsx("h2", { className: "text-base font-semibold", children: title ?? labels.newFolder }),
1012
- /* @__PURE__ */ jsxRuntime.jsx("p", { className: "mt-1 text-sm text-muted-foreground", children: description ?? labels.folderNameHint }),
1013
- /* @__PURE__ */ jsxRuntime.jsx(
1014
- "input",
1015
- {
1016
- ref: inputRef,
1017
- value: name,
1018
- onChange: (event) => setName(event.target.value),
1019
- onKeyDown: (event) => {
1020
- if (event.key === "Enter") {
1021
- event.preventDefault();
1022
- void submit();
1023
- }
1024
- },
1025
- placeholder: labels.folderName,
1026
- "aria-invalid": Boolean(error),
1027
- 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"
1028
- }
1029
- ),
1030
- error ? /* @__PURE__ */ jsxRuntime.jsx("p", { className: "mt-2 text-sm text-destructive", children: error }) : null,
1031
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "mt-4 flex justify-end gap-2", children: [
1032
- /* @__PURE__ */ jsxRuntime.jsx(
1033
- "button",
1034
- {
1035
- type: "button",
1036
- className: "inline-flex h-8 items-center rounded-[10px] border border-border px-3 text-sm",
1037
- onClick: () => onOpenChange(false),
1038
- children: labels.cancel
1039
- }
1040
- ),
1041
- /* @__PURE__ */ jsxRuntime.jsx(
1042
- "button",
1043
- {
1044
- type: "button",
1045
- disabled: !name.trim() || saving,
1046
- className: "inline-flex h-8 items-center rounded-[10px] bg-primary px-3 text-sm text-primary-foreground disabled:opacity-50",
1047
- onClick: () => void submit(),
1048
- children: confirmLabel ?? labels.createFolder
1049
- }
1050
- )
1051
- ] })
1052
- ] });
1053
- }
1054
1072
  function ConfirmDeleteDialog({
1055
1073
  open,
1056
1074
  names,
@@ -1516,6 +1534,32 @@ function UploadQueueProvider({
1516
1534
  }
1517
1535
  var PAGE_SIZE = 50;
1518
1536
  var DEFAULT_PREFS = { view: "list", sortKey: "name", sortDir: "asc" };
1537
+ var DRAFT_FOLDER_ID = "__draft-folder__";
1538
+ function unusedFolderName(files, base) {
1539
+ const taken = new Set(files.map((file) => file.name.toLowerCase()));
1540
+ if (!taken.has(base.toLowerCase())) return base;
1541
+ let n = 2;
1542
+ while (taken.has(`${base} ${n}`.toLowerCase())) n += 1;
1543
+ return `${base} ${n}`;
1544
+ }
1545
+ function draftFolder(name, tenantId, parentId) {
1546
+ return {
1547
+ id: DRAFT_FOLDER_ID,
1548
+ tenantId,
1549
+ parentId,
1550
+ name,
1551
+ path: name,
1552
+ kind: "folder",
1553
+ size: 0,
1554
+ mimeType: "",
1555
+ s3Key: "",
1556
+ ownerId: "",
1557
+ metadata: {},
1558
+ createdAt: /* @__PURE__ */ new Date(0),
1559
+ updatedAt: /* @__PURE__ */ new Date(0),
1560
+ deletedAt: null
1561
+ };
1562
+ }
1519
1563
  function reviveFile(node) {
1520
1564
  const asDate = (value) => value instanceof Date ? value : new Date(value);
1521
1565
  return {
@@ -1639,7 +1683,7 @@ function FileExplorerInner(props) {
1639
1683
  const [prefsReady, setPrefsReady] = react.useState(!persistViewKey);
1640
1684
  const skipPrefsWrite = react.useRef(!persistViewKey);
1641
1685
  const [menu, setMenu] = react.useState(null);
1642
- const [folderOpen, setFolderOpen] = react.useState(false);
1686
+ const [draftFolderName, setDraftFolderName] = react.useState(null);
1643
1687
  const [renameTarget, setRenameTarget] = react.useState(null);
1644
1688
  const [deleteTargets, setDeleteTargets] = react.useState([]);
1645
1689
  const [purgeTargets, setPurgeTargets] = react.useState([]);
@@ -1821,6 +1865,7 @@ function FileExplorerInner(props) {
1821
1865
  }, [refreshKey]);
1822
1866
  const handleActivate = react.useCallback(
1823
1867
  (file) => {
1868
+ if (file.id === DRAFT_FOLDER_ID) return;
1824
1869
  if (file.kind === "folder") {
1825
1870
  onOpenFolder?.(file);
1826
1871
  return;
@@ -1832,6 +1877,7 @@ function FileExplorerInner(props) {
1832
1877
  [onActivate, onDownload, onOpenFolder, onPreview]
1833
1878
  );
1834
1879
  const handleContextMenu = react.useCallback((file, event) => {
1880
+ if (file.id === DRAFT_FOLDER_ID) return;
1835
1881
  setMenu({ kind: "item", file, x: event.clientX, y: event.clientY });
1836
1882
  }, []);
1837
1883
  const clearSelection = react.useCallback(() => {
@@ -1843,6 +1889,7 @@ function FileExplorerInner(props) {
1843
1889
  }, [clearSelection, parentId, trashOpen]);
1844
1890
  const handleSelect = react.useCallback(
1845
1891
  (file, event) => {
1892
+ if (file.id === DRAFT_FOLDER_ID) return;
1846
1893
  const id = file.id;
1847
1894
  if (event.shiftKey && anchorId.current) {
1848
1895
  const from = sortedFiles.findIndex((item) => item.id === anchorId.current);
@@ -1981,6 +2028,37 @@ function FileExplorerInner(props) {
1981
2028
  trashOpen
1982
2029
  ]
1983
2030
  );
2031
+ const commitRename = (name) => {
2032
+ if (draftFolderName != null) {
2033
+ setDraftFolderName(null);
2034
+ void actions.createFolder?.({ name, parentId }).then(() => reload());
2035
+ return;
2036
+ }
2037
+ if (!renameTarget) return;
2038
+ const target = renameTarget;
2039
+ setRenameTarget(null);
2040
+ if (name === target.name) return;
2041
+ void actions.renameFile(target.id, name).then(() => reload());
2042
+ };
2043
+ const cancelRename = () => {
2044
+ setDraftFolderName(null);
2045
+ setRenameTarget(null);
2046
+ };
2047
+ const startCreateFolder = () => {
2048
+ if (!actions.createFolder || trashOpen) return;
2049
+ setMenu(null);
2050
+ setRenameTarget(null);
2051
+ setQuery("");
2052
+ setDraftFolderName(unusedFolderName(pageItems, labels.newFolder));
2053
+ };
2054
+ const displayFiles = react.useMemo(() => {
2055
+ if (draftFolderName == null) return sortedFiles;
2056
+ return [
2057
+ draftFolder(draftFolderName, tenantId, parentId),
2058
+ ...sortedFiles
2059
+ ];
2060
+ }, [draftFolderName, parentId, sortedFiles, tenantId]);
2061
+ const renamingId = draftFolderName != null ? DRAFT_FOLDER_ID : renameTarget?.id ?? null;
1984
2062
  const openUpload = () => uploadInputRef.current?.click();
1985
2063
  return /* @__PURE__ */ jsxRuntime.jsxs(
1986
2064
  "div",
@@ -2029,7 +2107,7 @@ function FileExplorerInner(props) {
2029
2107
  view: explorer.view,
2030
2108
  onViewChange: explorer.setView,
2031
2109
  onUpload: requestUpload && !trashOpen ? openUpload : void 0,
2032
- onNewFolder: actions.createFolder && !trashOpen ? () => setFolderOpen(true) : void 0,
2110
+ onNewFolder: actions.createFolder && !trashOpen ? startCreateFolder : void 0,
2033
2111
  query,
2034
2112
  onQueryChange: searchFiles ? setQuery : void 0,
2035
2113
  trashOpen,
@@ -2044,7 +2122,7 @@ function FileExplorerInner(props) {
2044
2122
  }
2045
2123
  )
2046
2124
  ] }),
2047
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: "min-h-0 flex-1 overflow-y-auto", children: (overlayLoading || pageStatus === "loading") && sortedFiles.length === 0 ? /* @__PURE__ */ jsxRuntime.jsx("p", { className: "px-4 py-12 text-center font-mono text-[10px] uppercase tracking-[0.12em] text-muted-foreground", children: labels.loading }) : overlayError || pageStatus === "error" && pageError ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: "border-b border-destructive/30 bg-destructive/5 px-4 py-3 text-sm text-destructive", children: (overlayError ?? pageError)?.message ?? labels.folderCreateFailed }) : sortedFiles.length === 0 ? /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex min-h-full flex-col justify-center px-4 py-16 text-center", children: [
2125
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "min-h-0 flex-1 overflow-y-auto", children: (overlayLoading || pageStatus === "loading") && displayFiles.length === 0 ? /* @__PURE__ */ jsxRuntime.jsx("p", { className: "px-4 py-12 text-center font-mono text-[10px] uppercase tracking-[0.12em] text-muted-foreground", children: labels.loading }) : overlayError || pageStatus === "error" && pageError ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: "border-b border-destructive/30 bg-destructive/5 px-4 py-3 text-sm text-destructive", children: (overlayError ?? pageError)?.message ?? labels.folderCreateFailed }) : displayFiles.length === 0 ? /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex min-h-full flex-col justify-center px-4 py-16 text-center", children: [
2048
2126
  /* @__PURE__ */ jsxRuntime.jsx("p", { className: "font-mono text-[10px] uppercase tracking-[0.12em] text-muted-foreground", children: trashOpen ? labels.emptyTrash : query.trim() ? labels.noMatches : labels.emptyFolder }),
2049
2127
  trashOpen ? /* @__PURE__ */ jsxRuntime.jsx("p", { className: "mt-2 text-sm text-muted-foreground", children: labels.trashRetention }) : !query.trim() ? /* @__PURE__ */ jsxRuntime.jsx("p", { className: "mt-2 text-sm text-muted-foreground", children: labels.dropHint }) : null
2050
2128
  ] }) : explorer.view === "list" ? /* @__PURE__ */ jsxRuntime.jsx(
@@ -2057,7 +2135,7 @@ function FileExplorerInner(props) {
2057
2135
  children: /* @__PURE__ */ jsxRuntime.jsx(
2058
2136
  ExplorerListView,
2059
2137
  {
2060
- files: sortedFiles,
2138
+ files: displayFiles,
2061
2139
  protectedIds: protectedSet,
2062
2140
  selectedIds,
2063
2141
  onSelect: handleSelect,
@@ -2072,7 +2150,10 @@ function FileExplorerInner(props) {
2072
2150
  onDragOverRow: explorer.setDropTarget,
2073
2151
  sortKey,
2074
2152
  sortDirection: sortDir,
2075
- onSortChange: handleSortChange
2153
+ onSortChange: handleSortChange,
2154
+ renamingId,
2155
+ onRenameCommit: commitRename,
2156
+ onRenameCancel: cancelRename
2076
2157
  }
2077
2158
  )
2078
2159
  }
@@ -2086,7 +2167,7 @@ function FileExplorerInner(props) {
2086
2167
  children: /* @__PURE__ */ jsxRuntime.jsx(
2087
2168
  ExplorerGridView,
2088
2169
  {
2089
- files: sortedFiles,
2170
+ files: displayFiles,
2090
2171
  protectedIds: protectedSet,
2091
2172
  selectedIds,
2092
2173
  onSelect: handleSelect,
@@ -2098,7 +2179,10 @@ function FileExplorerInner(props) {
2098
2179
  draggingId: explorer.draggingId,
2099
2180
  draggingIds,
2100
2181
  dropTargetId: explorer.dropTargetId,
2101
- onDragOverRow: explorer.setDropTarget
2182
+ onDragOverRow: explorer.setDropTarget,
2183
+ renamingId,
2184
+ onRenameCommit: commitRename,
2185
+ onRenameCancel: cancelRename
2102
2186
  }
2103
2187
  )
2104
2188
  }
@@ -2162,7 +2246,7 @@ function FileExplorerInner(props) {
2162
2246
  onRequestPurge: actions.purgeNode ? (file) => setPurgeTargets([file]) : void 0,
2163
2247
  onCopy: (file) => copyToClipboard([file]),
2164
2248
  onPaste: clipboard.length > 0 && !trashOpen ? (folder) => void pasteClipboard(folder ? folder.id : parentId) : void 0,
2165
- onNewFolder: actions.createFolder ? () => setFolderOpen(true) : void 0,
2249
+ onNewFolder: actions.createFolder ? startCreateFolder : void 0,
2166
2250
  onUpload: requestUpload ? openUpload : void 0
2167
2251
  }
2168
2252
  ),
@@ -2180,7 +2264,11 @@ function FileExplorerInner(props) {
2180
2264
  if (file.kind !== "file") continue;
2181
2265
  tokens.push(await actions.createShare({ id: file.id }));
2182
2266
  }
2183
- if (tokens.length > 0) await navigator.clipboard.writeText(tokens.join("\n"));
2267
+ if (tokens.length > 0) {
2268
+ await navigator.clipboard.writeText(
2269
+ tokens.map(absoluteShareUrl).join("\n")
2270
+ );
2271
+ }
2184
2272
  } : void 0,
2185
2273
  onDelete: () => setDeleteTargets(selectedFiles),
2186
2274
  onRestore: actions.restoreNode ? async () => {
@@ -2200,36 +2288,6 @@ function FileExplorerInner(props) {
2200
2288
  onClear: () => setClipboard([])
2201
2289
  }
2202
2290
  ) }),
2203
- /* @__PURE__ */ jsxRuntime.jsx(
2204
- CreateFolderDialog,
2205
- {
2206
- open: folderOpen,
2207
- onOpenChange: setFolderOpen,
2208
- onCreate: async (name) => {
2209
- await actions.createFolder?.({ name, parentId });
2210
- await reload();
2211
- }
2212
- }
2213
- ),
2214
- /* @__PURE__ */ jsxRuntime.jsx(
2215
- CreateFolderDialog,
2216
- {
2217
- open: Boolean(renameTarget),
2218
- onOpenChange: (open) => {
2219
- if (!open) setRenameTarget(null);
2220
- },
2221
- title: labels.rename,
2222
- description: labels.renameHint,
2223
- confirmLabel: labels.rename,
2224
- initialName: renameTarget?.name ?? "",
2225
- onCreate: async (name) => {
2226
- if (!renameTarget) return;
2227
- await actions.renameFile(renameTarget.id, name);
2228
- setRenameTarget(null);
2229
- await reload();
2230
- }
2231
- }
2232
- ),
2233
2291
  /* @__PURE__ */ jsxRuntime.jsx(
2234
2292
  ConfirmDeleteDialog,
2235
2293
  {
@@ -2384,6 +2442,92 @@ function FilePreviewDialog({
2384
2442
  ) })
2385
2443
  ] });
2386
2444
  }
2445
+ function CreateFolderDialog({
2446
+ open,
2447
+ onOpenChange,
2448
+ onCreate,
2449
+ title,
2450
+ description,
2451
+ confirmLabel,
2452
+ initialName = ""
2453
+ }) {
2454
+ const labels = useExplorerLabels();
2455
+ const [name, setName] = react.useState(initialName);
2456
+ const [saving, setSaving] = react.useState(false);
2457
+ const [error, setError] = react.useState(null);
2458
+ const inputRef = react.useRef(null);
2459
+ react.useEffect(() => {
2460
+ if (!open) {
2461
+ setSaving(false);
2462
+ setError(null);
2463
+ return;
2464
+ }
2465
+ setName(initialName);
2466
+ const id = window.setTimeout(() => {
2467
+ inputRef.current?.focus();
2468
+ inputRef.current?.select();
2469
+ }, 0);
2470
+ return () => window.clearTimeout(id);
2471
+ }, [initialName, open]);
2472
+ async function submit() {
2473
+ const trimmed = name.trim();
2474
+ if (!trimmed || saving) return;
2475
+ setSaving(true);
2476
+ setError(null);
2477
+ try {
2478
+ await onCreate(trimmed);
2479
+ onOpenChange(false);
2480
+ } catch (cause) {
2481
+ const code = cause && typeof cause === "object" && "code" in cause ? String(cause.code) : "";
2482
+ setError(code === "Conflict" ? labels.folderExists : labels.folderCreateFailed);
2483
+ } finally {
2484
+ setSaving(false);
2485
+ }
2486
+ }
2487
+ return /* @__PURE__ */ jsxRuntime.jsxs(Overlay, { open, onClose: () => onOpenChange(false), children: [
2488
+ /* @__PURE__ */ jsxRuntime.jsx("h2", { className: "text-base font-semibold", children: title ?? labels.newFolder }),
2489
+ /* @__PURE__ */ jsxRuntime.jsx("p", { className: "mt-1 text-sm text-muted-foreground", children: description ?? labels.folderNameHint }),
2490
+ /* @__PURE__ */ jsxRuntime.jsx(
2491
+ "input",
2492
+ {
2493
+ ref: inputRef,
2494
+ value: name,
2495
+ onChange: (event) => setName(event.target.value),
2496
+ onKeyDown: (event) => {
2497
+ if (event.key === "Enter") {
2498
+ event.preventDefault();
2499
+ void submit();
2500
+ }
2501
+ },
2502
+ placeholder: labels.folderName,
2503
+ "aria-invalid": Boolean(error),
2504
+ 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"
2505
+ }
2506
+ ),
2507
+ error ? /* @__PURE__ */ jsxRuntime.jsx("p", { className: "mt-2 text-sm text-destructive", children: error }) : null,
2508
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "mt-4 flex justify-end gap-2", children: [
2509
+ /* @__PURE__ */ jsxRuntime.jsx(
2510
+ "button",
2511
+ {
2512
+ type: "button",
2513
+ className: "inline-flex h-8 items-center rounded-[10px] border border-border px-3 text-sm",
2514
+ onClick: () => onOpenChange(false),
2515
+ children: labels.cancel
2516
+ }
2517
+ ),
2518
+ /* @__PURE__ */ jsxRuntime.jsx(
2519
+ "button",
2520
+ {
2521
+ type: "button",
2522
+ disabled: !name.trim() || saving,
2523
+ className: "inline-flex h-8 items-center rounded-[10px] bg-primary px-3 text-sm text-primary-foreground disabled:opacity-50",
2524
+ onClick: () => void submit(),
2525
+ children: confirmLabel ?? labels.createFolder
2526
+ }
2527
+ )
2528
+ ] })
2529
+ ] });
2530
+ }
2387
2531
 
2388
2532
  exports.Breadcrumbs = Breadcrumbs;
2389
2533
  exports.ConfirmDeleteDialog = ConfirmDeleteDialog;