@vryzel/file-next-ui 0.5.0 → 0.5.2

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
@@ -1041,6 +1041,8 @@ function ExplorerContextMenu(props) {
1041
1041
  mode = "browse",
1042
1042
  actions,
1043
1043
  protectedItem = false,
1044
+ renameLocked = false,
1045
+ deleteLocked = false,
1044
1046
  onClose,
1045
1047
  onPreview,
1046
1048
  onDownload,
@@ -1089,7 +1091,7 @@ function ExplorerContextMenu(props) {
1089
1091
  swallowClickThrough();
1090
1092
  onClose();
1091
1093
  };
1092
- const itemClass = "flex w-full min-h-11 cursor-pointer items-center gap-2 rounded-[10px] px-3 py-2.5 text-left text-sm outline-none hover:bg-accent disabled:opacity-40 sm:min-h-0 sm:px-2.5 sm:py-1.5";
1094
+ const itemClass = "flex w-full min-h-11 cursor-pointer items-center gap-2 rounded-[10px] px-3 py-2.5 text-left text-sm outline-none hover:bg-accent disabled:cursor-not-allowed disabled:opacity-40 sm:min-h-0 sm:px-2.5 sm:py-1.5";
1093
1095
  return /* @__PURE__ */ jsxRuntime.jsx(
1094
1096
  "div",
1095
1097
  {
@@ -1166,13 +1168,24 @@ function ExplorerContextMenu(props) {
1166
1168
  close();
1167
1169
  extraFileAction.onSelect(target.file);
1168
1170
  }, children: extraFileAction.label }) : null,
1169
- onRename ? /* @__PURE__ */ jsxRuntime.jsxs("button", { type: "button", role: "menuitem", className: itemClass, onClick: () => {
1170
- close();
1171
- onRename(target.file);
1172
- }, children: [
1173
- /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Pencil, { "aria-hidden": "true", className: "size-4" }),
1174
- labels.rename
1175
- ] }) : null,
1171
+ onRename ? /* @__PURE__ */ jsxRuntime.jsxs(
1172
+ "button",
1173
+ {
1174
+ type: "button",
1175
+ role: "menuitem",
1176
+ className: itemClass,
1177
+ disabled: renameLocked,
1178
+ onClick: () => {
1179
+ if (renameLocked) return;
1180
+ close();
1181
+ onRename(target.file);
1182
+ },
1183
+ children: [
1184
+ /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Pencil, { "aria-hidden": "true", className: "size-4" }),
1185
+ labels.rename
1186
+ ]
1187
+ }
1188
+ ) : null,
1176
1189
  onCopy ? /* @__PURE__ */ jsxRuntime.jsxs("button", { type: "button", role: "menuitem", className: itemClass, onClick: () => {
1177
1190
  close();
1178
1191
  onCopy(target.file);
@@ -1211,10 +1224,10 @@ function ExplorerContextMenu(props) {
1211
1224
  {
1212
1225
  type: "button",
1213
1226
  role: "menuitem",
1214
- disabled: protectedItem,
1227
+ disabled: protectedItem || deleteLocked,
1215
1228
  className: cn(itemClass, "text-destructive"),
1216
1229
  onClick: () => {
1217
- if (protectedItem) return;
1230
+ if (protectedItem || deleteLocked) return;
1218
1231
  close();
1219
1232
  if (onRequestDelete) onRequestDelete(target.file);
1220
1233
  else void actions.deleteFile({ id: target.file.id });
@@ -1966,10 +1979,15 @@ function FileExplorerInner(props) {
1966
1979
  onPreview,
1967
1980
  onDownload,
1968
1981
  extraFileAction,
1969
- protectedIds
1982
+ protectedIds,
1983
+ itemLocks
1970
1984
  } = props;
1971
1985
  const labels = useExplorerLabels();
1972
1986
  const protectedSet = react.useMemo(() => new Set(protectedIds ?? []), [protectedIds]);
1987
+ const locksFor = react.useCallback(
1988
+ (file) => itemLocks?.(file) ?? {},
1989
+ [itemLocks]
1990
+ );
1973
1991
  const explorer = fileNextHeadless.useFileExplorer({
1974
1992
  listFiles: ((input) => listFiles({ parentId: input.parentId, limit: PAGE_SIZE })),
1975
1993
  tenantId,
@@ -1991,6 +2009,7 @@ function FileExplorerInner(props) {
1991
2009
  const browseCursor = react.useRef(void 0);
1992
2010
  const overlayCursor = react.useRef(void 0);
1993
2011
  const loadingMoreRef = react.useRef(false);
2012
+ const openingFolderIdRef = react.useRef(null);
1994
2013
  const overlayMoreRef = react.useRef(false);
1995
2014
  const browseGen = react.useRef(0);
1996
2015
  const [overlayFiles, setOverlayFiles] = react.useState(null);
@@ -2063,6 +2082,7 @@ function FileExplorerInner(props) {
2063
2082
  setLoadingMore(false);
2064
2083
  return;
2065
2084
  }
2085
+ if (mode === "reset") openingFolderIdRef.current = null;
2066
2086
  if (result.ok) {
2067
2087
  browseCursor.current = result.value.nextCursor;
2068
2088
  setBrowseHasMore(Boolean(result.value.nextCursor));
@@ -2182,14 +2202,18 @@ function FileExplorerInner(props) {
2182
2202
  () => [...sourceFiles].sort((a, b) => compareFiles(a, b, sortKey, sortDir)),
2183
2203
  [sourceFiles, sortKey, sortDir]
2184
2204
  );
2205
+ const refreshSeen = react.useRef(refreshKey);
2185
2206
  react.useEffect(() => {
2186
- if (refreshKey === void 0) return;
2207
+ if (refreshKey === void 0 || refreshSeen.current === refreshKey) return;
2208
+ refreshSeen.current = refreshKey;
2187
2209
  void reload();
2188
- }, [refreshKey]);
2210
+ }, [refreshKey, reload]);
2189
2211
  const handleActivate = react.useCallback(
2190
2212
  (file) => {
2191
2213
  if (file.id === DRAFT_FOLDER_ID) return;
2192
2214
  if (file.kind === "folder") {
2215
+ if (openingFolderIdRef.current === file.id) return;
2216
+ openingFolderIdRef.current = file.id;
2193
2217
  onOpenFolder?.(file);
2194
2218
  return;
2195
2219
  }
@@ -2320,6 +2344,7 @@ function FileExplorerInner(props) {
2320
2344
  }
2321
2345
  if (event.key === "F2" && selected && !trashOpen) {
2322
2346
  event.preventDefault();
2347
+ if (locksFor(selected).rename) return;
2323
2348
  setRenameTarget(selected);
2324
2349
  return;
2325
2350
  }
@@ -2329,7 +2354,8 @@ function FileExplorerInner(props) {
2329
2354
  if (actions.purgeNode) setPurgeTargets(selectedFiles);
2330
2355
  return;
2331
2356
  }
2332
- if (selectedFiles.some((file) => protectedSet.has(file.id))) return;
2357
+ if (selectedFiles.some((file) => protectedSet.has(file.id) || locksFor(file).delete))
2358
+ return;
2333
2359
  setDeleteTargets(selectedFiles);
2334
2360
  return;
2335
2361
  }
@@ -2343,6 +2369,7 @@ function FileExplorerInner(props) {
2343
2369
  clearSelection,
2344
2370
  copyToClipboard,
2345
2371
  handleActivate,
2372
+ locksFor,
2346
2373
  pasteClipboard,
2347
2374
  protectedSet,
2348
2375
  selected,
@@ -2381,6 +2408,7 @@ function FileExplorerInner(props) {
2381
2408
  ...sortedFiles
2382
2409
  ];
2383
2410
  }, [draftFolderName, parentId, sortedFiles, tenantId]);
2411
+ const listing = overlayLoading || pageStatus === "loading";
2384
2412
  const renamingId = draftFolderName != null ? DRAFT_FOLDER_ID : renameTarget?.id ?? null;
2385
2413
  const openUpload = () => uploadInputRef.current?.click();
2386
2414
  return /* @__PURE__ */ jsxRuntime.jsxs(
@@ -2445,79 +2473,95 @@ function FileExplorerInner(props) {
2445
2473
  }
2446
2474
  )
2447
2475
  ] }),
2448
- /* @__PURE__ */ jsxRuntime.jsx("div", { ref: scrollRef, className: "min-h-0 flex-1 overflow-y-auto overflow-x-auto overscroll-x-contain", children: (overlayLoading || pageStatus === "loading") && displayFiles.length === 0 ? /* @__PURE__ */ 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: [
2449
- /* @__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 }),
2450
- 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
2451
- ] }) : explorer.view === "list" ? /* @__PURE__ */ jsxRuntime.jsx(
2452
- "div",
2453
- {
2454
- onClick: (event) => {
2455
- if (event.target.closest("[data-file-id]")) return;
2456
- clearSelection();
2457
- },
2458
- children: /* @__PURE__ */ jsxRuntime.jsx(
2459
- ExplorerListView,
2460
- {
2461
- files: displayFiles,
2462
- protectedIds: protectedSet,
2463
- selectedIds,
2464
- onSelect: handleSelect,
2465
- onActivate: handleActivate,
2466
- onContextMenu: handleContextMenu,
2467
- onDragStart: explorer.beginDrag,
2468
- onDragEnd: explorer.endDrag,
2469
- onDrop: handleDropOnFolder,
2470
- draggingId: explorer.draggingId,
2471
- draggingIds,
2472
- dropTargetId: explorer.dropTargetId,
2473
- onDragOverRow: explorer.setDropTarget,
2474
- sortKey,
2475
- sortDirection: sortDir,
2476
- onSortChange: handleSortChange,
2477
- renamingId,
2478
- onRenameCommit: commitRename,
2479
- onRenameCancel: cancelRename,
2480
- scrollRef,
2481
- hasMore,
2482
- loadingMore: loadingMore || overlayMore,
2483
- onLoadMore: loadNext
2484
- }
2485
- )
2486
- }
2487
- ) : /* @__PURE__ */ jsxRuntime.jsx(
2476
+ /* @__PURE__ */ jsxRuntime.jsxs(
2488
2477
  "div",
2489
2478
  {
2490
- onClick: (event) => {
2491
- if (event.target.closest("[data-file-id]")) return;
2492
- clearSelection();
2493
- },
2494
- children: /* @__PURE__ */ jsxRuntime.jsx(
2495
- ExplorerGridView,
2496
- {
2497
- files: displayFiles,
2498
- protectedIds: protectedSet,
2499
- selectedIds,
2500
- onSelect: handleSelect,
2501
- onActivate: handleActivate,
2502
- onContextMenu: handleContextMenu,
2503
- onDragStart: explorer.beginDrag,
2504
- onDragEnd: explorer.endDrag,
2505
- onDrop: handleDropOnFolder,
2506
- draggingId: explorer.draggingId,
2507
- draggingIds,
2508
- dropTargetId: explorer.dropTargetId,
2509
- onDragOverRow: explorer.setDropTarget,
2510
- renamingId,
2511
- onRenameCommit: commitRename,
2512
- onRenameCancel: cancelRename,
2513
- scrollRef,
2514
- hasMore,
2515
- loadingMore: loadingMore || overlayMore,
2516
- onLoadMore: loadNext
2517
- }
2518
- )
2479
+ ref: scrollRef,
2480
+ "aria-busy": listing,
2481
+ className: cn(
2482
+ "min-h-0 flex-1 overflow-y-auto overflow-x-auto overscroll-x-contain",
2483
+ // Stale rows stay mounted while the next listing loads, so dim them and
2484
+ // swallow clicks instead of letting the user re-open the same folder.
2485
+ listing && displayFiles.length > 0 && "pointer-events-none opacity-60"
2486
+ ),
2487
+ children: [
2488
+ listing && displayFiles.length > 0 ? /* @__PURE__ */ jsxRuntime.jsx("div", { "aria-hidden": true, className: "h-0.5 w-full overflow-hidden bg-muted", children: /* @__PURE__ */ jsxRuntime.jsx("div", { className: "h-full w-1/3 animate-pulse bg-primary" }) }) : null,
2489
+ listing && 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: [
2490
+ /* @__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 }),
2491
+ 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
2492
+ ] }) : explorer.view === "list" ? /* @__PURE__ */ jsxRuntime.jsx(
2493
+ "div",
2494
+ {
2495
+ onClick: (event) => {
2496
+ if (event.target.closest("[data-file-id]")) return;
2497
+ clearSelection();
2498
+ },
2499
+ children: /* @__PURE__ */ jsxRuntime.jsx(
2500
+ ExplorerListView,
2501
+ {
2502
+ files: displayFiles,
2503
+ protectedIds: protectedSet,
2504
+ selectedIds,
2505
+ onSelect: handleSelect,
2506
+ onActivate: handleActivate,
2507
+ onContextMenu: handleContextMenu,
2508
+ onDragStart: explorer.beginDrag,
2509
+ onDragEnd: explorer.endDrag,
2510
+ onDrop: handleDropOnFolder,
2511
+ draggingId: explorer.draggingId,
2512
+ draggingIds,
2513
+ dropTargetId: explorer.dropTargetId,
2514
+ onDragOverRow: explorer.setDropTarget,
2515
+ sortKey,
2516
+ sortDirection: sortDir,
2517
+ onSortChange: handleSortChange,
2518
+ renamingId,
2519
+ onRenameCommit: commitRename,
2520
+ onRenameCancel: cancelRename,
2521
+ scrollRef,
2522
+ hasMore,
2523
+ loadingMore: loadingMore || overlayMore,
2524
+ onLoadMore: loadNext
2525
+ }
2526
+ )
2527
+ }
2528
+ ) : /* @__PURE__ */ jsxRuntime.jsx(
2529
+ "div",
2530
+ {
2531
+ onClick: (event) => {
2532
+ if (event.target.closest("[data-file-id]")) return;
2533
+ clearSelection();
2534
+ },
2535
+ children: /* @__PURE__ */ jsxRuntime.jsx(
2536
+ ExplorerGridView,
2537
+ {
2538
+ files: displayFiles,
2539
+ protectedIds: protectedSet,
2540
+ selectedIds,
2541
+ onSelect: handleSelect,
2542
+ onActivate: handleActivate,
2543
+ onContextMenu: handleContextMenu,
2544
+ onDragStart: explorer.beginDrag,
2545
+ onDragEnd: explorer.endDrag,
2546
+ onDrop: handleDropOnFolder,
2547
+ draggingId: explorer.draggingId,
2548
+ draggingIds,
2549
+ dropTargetId: explorer.dropTargetId,
2550
+ onDragOverRow: explorer.setDropTarget,
2551
+ renamingId,
2552
+ onRenameCommit: commitRename,
2553
+ onRenameCancel: cancelRename,
2554
+ scrollRef,
2555
+ hasMore,
2556
+ loadingMore: loadingMore || overlayMore,
2557
+ onLoadMore: loadNext
2558
+ }
2559
+ )
2560
+ }
2561
+ )
2562
+ ]
2519
2563
  }
2520
- ) }),
2564
+ ),
2521
2565
  /* @__PURE__ */ jsxRuntime.jsxs("footer", { className: "flex shrink-0 items-center justify-between gap-3 border-t border-border px-4 py-2", children: [
2522
2566
  /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "font-mono text-[10px] uppercase tracking-[0.12em] text-muted-foreground", children: [
2523
2567
  /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "text-primary tabular-nums", children: [
@@ -2538,6 +2582,8 @@ function FileExplorerInner(props) {
2538
2582
  target: menu,
2539
2583
  mode: trashOpen ? "trash" : "browse",
2540
2584
  protectedItem: menu?.kind === "item" ? protectedSet.has(menu.file.id) : false,
2585
+ renameLocked: menu?.kind === "item" ? Boolean(locksFor(menu.file).rename) : false,
2586
+ deleteLocked: menu?.kind === "item" ? Boolean(locksFor(menu.file).delete) : false,
2541
2587
  extraFileAction,
2542
2588
  actions: {
2543
2589
  deleteFile: async (input) => {
@@ -2572,7 +2618,10 @@ function FileExplorerInner(props) {
2572
2618
  ExplorerSelectionToast,
2573
2619
  {
2574
2620
  files: selectedFiles,
2575
- protectedIds: protectedSet,
2621
+ protectedIds: /* @__PURE__ */ new Set([
2622
+ ...protectedSet,
2623
+ ...selectedFiles.filter((file) => locksFor(file).delete).map((file) => file.id)
2624
+ ]),
2576
2625
  trashOpen,
2577
2626
  onDownload,
2578
2627
  onCopy: trashOpen ? void 0 : () => copyToClipboard(selectedFiles),