@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 +323 -177
- 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 +323 -177
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
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,
|
|
@@ -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.
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
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
|
-
(
|
|
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 [
|
|
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 ?
|
|
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") &&
|
|
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:
|
|
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:
|
|
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 ?
|
|
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)
|
|
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;
|