@vryzel/file-next-ui 0.4.1 → 0.4.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs 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,
@@ -836,24 +940,26 @@ function ExplorerContextMenu(props) {
836
940
  /* @__PURE__ */ jsxRuntime.jsx(lucideReact.RotateCcw, { "aria-hidden": "true", className: "size-4" }),
837
941
  labels.restore
838
942
  ] }),
839
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: "my-1 h-px bg-border" }),
840
- /* @__PURE__ */ jsxRuntime.jsxs(
841
- "button",
842
- {
843
- type: "button",
844
- role: "menuitem",
845
- className: cn(itemClass, "text-destructive"),
846
- onClick: () => {
847
- close();
848
- if (onRequestPurge) onRequestPurge(target.file);
849
- else void actions.purgeNode?.({ id: target.file.id });
850
- },
851
- children: [
852
- /* @__PURE__ */ jsxRuntime.jsx(lucideReact.X, { "aria-hidden": "true", className: "size-4" }),
853
- labels.deleteForever
854
- ]
855
- }
856
- )
943
+ onRequestPurge || actions.purgeNode ? /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
944
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "my-1 h-px bg-border" }),
945
+ /* @__PURE__ */ jsxRuntime.jsxs(
946
+ "button",
947
+ {
948
+ type: "button",
949
+ role: "menuitem",
950
+ className: cn(itemClass, "text-destructive"),
951
+ onClick: () => {
952
+ close();
953
+ if (onRequestPurge) onRequestPurge(target.file);
954
+ else void actions.purgeNode?.({ id: target.file.id });
955
+ },
956
+ children: [
957
+ /* @__PURE__ */ jsxRuntime.jsx(lucideReact.X, { "aria-hidden": "true", className: "size-4" }),
958
+ labels.deleteForever
959
+ ]
960
+ }
961
+ )
962
+ ] }) : null
857
963
  ] }) : /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
858
964
  target.file.kind === "file" && onPreview ? /* @__PURE__ */ jsxRuntime.jsxs("button", { type: "button", role: "menuitem", className: itemClass, onClick: () => {
859
965
  close();
@@ -894,7 +1000,7 @@ function ExplorerContextMenu(props) {
894
1000
  /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Copy, { "aria-hidden": "true", className: "size-4" }),
895
1001
  labels.paste
896
1002
  ] }) : null,
897
- actions.createShare ? /* @__PURE__ */ jsxRuntime.jsxs(
1003
+ target.file.kind === "file" && actions.createShare ? /* @__PURE__ */ jsxRuntime.jsxs(
898
1004
  "button",
899
1005
  {
900
1006
  type: "button",
@@ -903,7 +1009,7 @@ function ExplorerContextMenu(props) {
903
1009
  onClick: () => {
904
1010
  close();
905
1011
  void actions.createShare({ id: target.file.id }).then(
906
- (token) => navigator.clipboard.writeText(token)
1012
+ (link) => navigator.clipboard.writeText(absoluteShareUrl(link))
907
1013
  );
908
1014
  },
909
1015
  children: [
@@ -963,92 +1069,6 @@ function Overlay({
963
1069
  }
964
1070
  );
965
1071
  }
966
- function CreateFolderDialog({
967
- open,
968
- onOpenChange,
969
- onCreate,
970
- title,
971
- description,
972
- confirmLabel,
973
- initialName = ""
974
- }) {
975
- const labels = useExplorerLabels();
976
- const [name, setName] = react.useState(initialName);
977
- const [saving, setSaving] = react.useState(false);
978
- const [error, setError] = react.useState(null);
979
- const inputRef = react.useRef(null);
980
- react.useEffect(() => {
981
- if (!open) {
982
- setSaving(false);
983
- setError(null);
984
- return;
985
- }
986
- setName(initialName);
987
- const id = window.setTimeout(() => {
988
- inputRef.current?.focus();
989
- inputRef.current?.select();
990
- }, 0);
991
- return () => window.clearTimeout(id);
992
- }, [initialName, open]);
993
- async function submit() {
994
- const trimmed = name.trim();
995
- if (!trimmed || saving) return;
996
- setSaving(true);
997
- setError(null);
998
- try {
999
- await onCreate(trimmed);
1000
- onOpenChange(false);
1001
- } catch (cause) {
1002
- const code = cause && typeof cause === "object" && "code" in cause ? String(cause.code) : "";
1003
- setError(code === "Conflict" ? labels.folderExists : labels.folderCreateFailed);
1004
- } finally {
1005
- setSaving(false);
1006
- }
1007
- }
1008
- return /* @__PURE__ */ jsxRuntime.jsxs(Overlay, { open, onClose: () => onOpenChange(false), children: [
1009
- /* @__PURE__ */ jsxRuntime.jsx("h2", { className: "text-base font-semibold", children: title ?? labels.newFolder }),
1010
- /* @__PURE__ */ jsxRuntime.jsx("p", { className: "mt-1 text-sm text-muted-foreground", children: description ?? labels.folderNameHint }),
1011
- /* @__PURE__ */ jsxRuntime.jsx(
1012
- "input",
1013
- {
1014
- ref: inputRef,
1015
- value: name,
1016
- onChange: (event) => setName(event.target.value),
1017
- onKeyDown: (event) => {
1018
- if (event.key === "Enter") {
1019
- event.preventDefault();
1020
- void submit();
1021
- }
1022
- },
1023
- placeholder: labels.folderName,
1024
- "aria-invalid": Boolean(error),
1025
- className: "mt-3 h-9 w-full rounded-[10px] border border-border bg-background px-3 text-sm outline-none focus-visible:ring-2 focus-visible:ring-ring"
1026
- }
1027
- ),
1028
- error ? /* @__PURE__ */ jsxRuntime.jsx("p", { className: "mt-2 text-sm text-destructive", children: error }) : null,
1029
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "mt-4 flex justify-end gap-2", children: [
1030
- /* @__PURE__ */ jsxRuntime.jsx(
1031
- "button",
1032
- {
1033
- type: "button",
1034
- className: "inline-flex h-8 items-center rounded-[10px] border border-border px-3 text-sm",
1035
- onClick: () => onOpenChange(false),
1036
- children: labels.cancel
1037
- }
1038
- ),
1039
- /* @__PURE__ */ jsxRuntime.jsx(
1040
- "button",
1041
- {
1042
- type: "button",
1043
- disabled: !name.trim() || saving,
1044
- className: "inline-flex h-8 items-center rounded-[10px] bg-primary px-3 text-sm text-primary-foreground disabled:opacity-50",
1045
- onClick: () => void submit(),
1046
- children: confirmLabel ?? labels.createFolder
1047
- }
1048
- )
1049
- ] })
1050
- ] });
1051
- }
1052
1072
  function ConfirmDeleteDialog({
1053
1073
  open,
1054
1074
  names,
@@ -1514,6 +1534,32 @@ function UploadQueueProvider({
1514
1534
  }
1515
1535
  var PAGE_SIZE = 50;
1516
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
+ }
1517
1563
  function reviveFile(node) {
1518
1564
  const asDate = (value) => value instanceof Date ? value : new Date(value);
1519
1565
  return {
@@ -1637,7 +1683,7 @@ function FileExplorerInner(props) {
1637
1683
  const [prefsReady, setPrefsReady] = react.useState(!persistViewKey);
1638
1684
  const skipPrefsWrite = react.useRef(!persistViewKey);
1639
1685
  const [menu, setMenu] = react.useState(null);
1640
- const [folderOpen, setFolderOpen] = react.useState(false);
1686
+ const [draftFolderName, setDraftFolderName] = react.useState(null);
1641
1687
  const [renameTarget, setRenameTarget] = react.useState(null);
1642
1688
  const [deleteTargets, setDeleteTargets] = react.useState([]);
1643
1689
  const [purgeTargets, setPurgeTargets] = react.useState([]);
@@ -1819,6 +1865,7 @@ function FileExplorerInner(props) {
1819
1865
  }, [refreshKey]);
1820
1866
  const handleActivate = react.useCallback(
1821
1867
  (file) => {
1868
+ if (file.id === DRAFT_FOLDER_ID) return;
1822
1869
  if (file.kind === "folder") {
1823
1870
  onOpenFolder?.(file);
1824
1871
  return;
@@ -1830,6 +1877,7 @@ function FileExplorerInner(props) {
1830
1877
  [onActivate, onDownload, onOpenFolder, onPreview]
1831
1878
  );
1832
1879
  const handleContextMenu = react.useCallback((file, event) => {
1880
+ if (file.id === DRAFT_FOLDER_ID) return;
1833
1881
  setMenu({ kind: "item", file, x: event.clientX, y: event.clientY });
1834
1882
  }, []);
1835
1883
  const clearSelection = react.useCallback(() => {
@@ -1841,6 +1889,7 @@ function FileExplorerInner(props) {
1841
1889
  }, [clearSelection, parentId, trashOpen]);
1842
1890
  const handleSelect = react.useCallback(
1843
1891
  (file, event) => {
1892
+ if (file.id === DRAFT_FOLDER_ID) return;
1844
1893
  const id = file.id;
1845
1894
  if (event.shiftKey && anchorId.current) {
1846
1895
  const from = sortedFiles.findIndex((item) => item.id === anchorId.current);
@@ -1954,7 +2003,7 @@ function FileExplorerInner(props) {
1954
2003
  if ((event.key === "Delete" || event.key === "Backspace") && selectedFiles.length > 0) {
1955
2004
  event.preventDefault();
1956
2005
  if (trashOpen) {
1957
- setPurgeTargets(selectedFiles);
2006
+ if (actions.purgeNode) setPurgeTargets(selectedFiles);
1958
2007
  return;
1959
2008
  }
1960
2009
  if (selectedFiles.some((file) => protectedSet.has(file.id))) return;
@@ -1979,6 +2028,37 @@ function FileExplorerInner(props) {
1979
2028
  trashOpen
1980
2029
  ]
1981
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;
1982
2062
  const openUpload = () => uploadInputRef.current?.click();
1983
2063
  return /* @__PURE__ */ jsxRuntime.jsxs(
1984
2064
  "div",
@@ -2027,7 +2107,7 @@ function FileExplorerInner(props) {
2027
2107
  view: explorer.view,
2028
2108
  onViewChange: explorer.setView,
2029
2109
  onUpload: requestUpload && !trashOpen ? openUpload : void 0,
2030
- onNewFolder: actions.createFolder && !trashOpen ? () => setFolderOpen(true) : void 0,
2110
+ onNewFolder: actions.createFolder && !trashOpen ? startCreateFolder : void 0,
2031
2111
  query,
2032
2112
  onQueryChange: searchFiles ? setQuery : void 0,
2033
2113
  trashOpen,
@@ -2042,7 +2122,7 @@ function FileExplorerInner(props) {
2042
2122
  }
2043
2123
  )
2044
2124
  ] }),
2045
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: "min-h-0 flex-1 overflow-y-auto", children: (overlayLoading || pageStatus === "loading") && sortedFiles.length === 0 ? /* @__PURE__ */ jsxRuntime.jsx("p", { className: "px-4 py-12 text-center font-mono text-[10px] uppercase tracking-[0.12em] text-muted-foreground", children: labels.loading }) : overlayError || pageStatus === "error" && pageError ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: "border-b border-destructive/30 bg-destructive/5 px-4 py-3 text-sm text-destructive", children: (overlayError ?? pageError)?.message ?? labels.folderCreateFailed }) : sortedFiles.length === 0 ? /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex min-h-full flex-col justify-center px-4 py-16 text-center", children: [
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: [
2046
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 }),
2047
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
2048
2128
  ] }) : explorer.view === "list" ? /* @__PURE__ */ jsxRuntime.jsx(
@@ -2055,7 +2135,7 @@ function FileExplorerInner(props) {
2055
2135
  children: /* @__PURE__ */ jsxRuntime.jsx(
2056
2136
  ExplorerListView,
2057
2137
  {
2058
- files: sortedFiles,
2138
+ files: displayFiles,
2059
2139
  protectedIds: protectedSet,
2060
2140
  selectedIds,
2061
2141
  onSelect: handleSelect,
@@ -2070,7 +2150,10 @@ function FileExplorerInner(props) {
2070
2150
  onDragOverRow: explorer.setDropTarget,
2071
2151
  sortKey,
2072
2152
  sortDirection: sortDir,
2073
- onSortChange: handleSortChange
2153
+ onSortChange: handleSortChange,
2154
+ renamingId,
2155
+ onRenameCommit: commitRename,
2156
+ onRenameCancel: cancelRename
2074
2157
  }
2075
2158
  )
2076
2159
  }
@@ -2084,7 +2167,7 @@ function FileExplorerInner(props) {
2084
2167
  children: /* @__PURE__ */ jsxRuntime.jsx(
2085
2168
  ExplorerGridView,
2086
2169
  {
2087
- files: sortedFiles,
2170
+ files: displayFiles,
2088
2171
  protectedIds: protectedSet,
2089
2172
  selectedIds,
2090
2173
  onSelect: handleSelect,
@@ -2096,7 +2179,10 @@ function FileExplorerInner(props) {
2096
2179
  draggingId: explorer.draggingId,
2097
2180
  draggingIds,
2098
2181
  dropTargetId: explorer.dropTargetId,
2099
- onDragOverRow: explorer.setDropTarget
2182
+ onDragOverRow: explorer.setDropTarget,
2183
+ renamingId,
2184
+ onRenameCommit: commitRename,
2185
+ onRenameCancel: cancelRename
2100
2186
  }
2101
2187
  )
2102
2188
  }
@@ -2157,10 +2243,10 @@ function FileExplorerInner(props) {
2157
2243
  onDownload,
2158
2244
  onRename: setRenameTarget,
2159
2245
  onRequestDelete: (file) => setDeleteTargets([file]),
2160
- onRequestPurge: (file) => setPurgeTargets([file]),
2246
+ onRequestPurge: actions.purgeNode ? (file) => setPurgeTargets([file]) : void 0,
2161
2247
  onCopy: (file) => copyToClipboard([file]),
2162
2248
  onPaste: clipboard.length > 0 && !trashOpen ? (folder) => void pasteClipboard(folder ? folder.id : parentId) : void 0,
2163
- onNewFolder: actions.createFolder ? () => setFolderOpen(true) : void 0,
2249
+ onNewFolder: actions.createFolder ? startCreateFolder : void 0,
2164
2250
  onUpload: requestUpload ? openUpload : void 0
2165
2251
  }
2166
2252
  ),
@@ -2178,7 +2264,11 @@ function FileExplorerInner(props) {
2178
2264
  if (file.kind !== "file") continue;
2179
2265
  tokens.push(await actions.createShare({ id: file.id }));
2180
2266
  }
2181
- 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
+ }
2182
2272
  } : void 0,
2183
2273
  onDelete: () => setDeleteTargets(selectedFiles),
2184
2274
  onRestore: actions.restoreNode ? async () => {
@@ -2186,7 +2276,7 @@ function FileExplorerInner(props) {
2186
2276
  clearSelection();
2187
2277
  await reload();
2188
2278
  } : void 0,
2189
- onPurge: () => setPurgeTargets(selectedFiles),
2279
+ onPurge: actions.purgeNode ? () => setPurgeTargets(selectedFiles) : void 0,
2190
2280
  onClear: clearSelection
2191
2281
  }
2192
2282
  ),
@@ -2198,36 +2288,6 @@ function FileExplorerInner(props) {
2198
2288
  onClear: () => setClipboard([])
2199
2289
  }
2200
2290
  ) }),
2201
- /* @__PURE__ */ jsxRuntime.jsx(
2202
- CreateFolderDialog,
2203
- {
2204
- open: folderOpen,
2205
- onOpenChange: setFolderOpen,
2206
- onCreate: async (name) => {
2207
- await actions.createFolder?.({ name, parentId });
2208
- await reload();
2209
- }
2210
- }
2211
- ),
2212
- /* @__PURE__ */ jsxRuntime.jsx(
2213
- CreateFolderDialog,
2214
- {
2215
- open: Boolean(renameTarget),
2216
- onOpenChange: (open) => {
2217
- if (!open) setRenameTarget(null);
2218
- },
2219
- title: labels.rename,
2220
- description: labels.renameHint,
2221
- confirmLabel: labels.rename,
2222
- initialName: renameTarget?.name ?? "",
2223
- onCreate: async (name) => {
2224
- if (!renameTarget) return;
2225
- await actions.renameFile(renameTarget.id, name);
2226
- setRenameTarget(null);
2227
- await reload();
2228
- }
2229
- }
2230
- ),
2231
2291
  /* @__PURE__ */ jsxRuntime.jsx(
2232
2292
  ConfirmDeleteDialog,
2233
2293
  {
@@ -2382,6 +2442,92 @@ function FilePreviewDialog({
2382
2442
  ) })
2383
2443
  ] });
2384
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
+ }
2385
2531
 
2386
2532
  exports.Breadcrumbs = Breadcrumbs;
2387
2533
  exports.ConfirmDeleteDialog = ConfirmDeleteDialog;