@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 +2 -0
- package/dist/index.cjs +300 -156
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +7 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +300 -156
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
package/README.md
CHANGED
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(
|
|
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.
|
|
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:
|
|
561
|
-
|
|
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
|
-
(
|
|
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 [
|
|
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 ?
|
|
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") &&
|
|
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:
|
|
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:
|
|
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 ?
|
|
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)
|
|
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;
|