@vryzel/file-next-ui 0.5.1 → 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));
@@ -2192,6 +2212,8 @@ function FileExplorerInner(props) {
2192
2212
  (file) => {
2193
2213
  if (file.id === DRAFT_FOLDER_ID) return;
2194
2214
  if (file.kind === "folder") {
2215
+ if (openingFolderIdRef.current === file.id) return;
2216
+ openingFolderIdRef.current = file.id;
2195
2217
  onOpenFolder?.(file);
2196
2218
  return;
2197
2219
  }
@@ -2322,6 +2344,7 @@ function FileExplorerInner(props) {
2322
2344
  }
2323
2345
  if (event.key === "F2" && selected && !trashOpen) {
2324
2346
  event.preventDefault();
2347
+ if (locksFor(selected).rename) return;
2325
2348
  setRenameTarget(selected);
2326
2349
  return;
2327
2350
  }
@@ -2331,7 +2354,8 @@ function FileExplorerInner(props) {
2331
2354
  if (actions.purgeNode) setPurgeTargets(selectedFiles);
2332
2355
  return;
2333
2356
  }
2334
- if (selectedFiles.some((file) => protectedSet.has(file.id))) return;
2357
+ if (selectedFiles.some((file) => protectedSet.has(file.id) || locksFor(file).delete))
2358
+ return;
2335
2359
  setDeleteTargets(selectedFiles);
2336
2360
  return;
2337
2361
  }
@@ -2345,6 +2369,7 @@ function FileExplorerInner(props) {
2345
2369
  clearSelection,
2346
2370
  copyToClipboard,
2347
2371
  handleActivate,
2372
+ locksFor,
2348
2373
  pasteClipboard,
2349
2374
  protectedSet,
2350
2375
  selected,
@@ -2383,6 +2408,7 @@ function FileExplorerInner(props) {
2383
2408
  ...sortedFiles
2384
2409
  ];
2385
2410
  }, [draftFolderName, parentId, sortedFiles, tenantId]);
2411
+ const listing = overlayLoading || pageStatus === "loading";
2386
2412
  const renamingId = draftFolderName != null ? DRAFT_FOLDER_ID : renameTarget?.id ?? null;
2387
2413
  const openUpload = () => uploadInputRef.current?.click();
2388
2414
  return /* @__PURE__ */ jsxRuntime.jsxs(
@@ -2447,79 +2473,95 @@ function FileExplorerInner(props) {
2447
2473
  }
2448
2474
  )
2449
2475
  ] }),
2450
- /* @__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: [
2451
- /* @__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 }),
2452
- 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
2453
- ] }) : explorer.view === "list" ? /* @__PURE__ */ jsxRuntime.jsx(
2454
- "div",
2455
- {
2456
- onClick: (event) => {
2457
- if (event.target.closest("[data-file-id]")) return;
2458
- clearSelection();
2459
- },
2460
- children: /* @__PURE__ */ jsxRuntime.jsx(
2461
- ExplorerListView,
2462
- {
2463
- files: displayFiles,
2464
- protectedIds: protectedSet,
2465
- selectedIds,
2466
- onSelect: handleSelect,
2467
- onActivate: handleActivate,
2468
- onContextMenu: handleContextMenu,
2469
- onDragStart: explorer.beginDrag,
2470
- onDragEnd: explorer.endDrag,
2471
- onDrop: handleDropOnFolder,
2472
- draggingId: explorer.draggingId,
2473
- draggingIds,
2474
- dropTargetId: explorer.dropTargetId,
2475
- onDragOverRow: explorer.setDropTarget,
2476
- sortKey,
2477
- sortDirection: sortDir,
2478
- onSortChange: handleSortChange,
2479
- renamingId,
2480
- onRenameCommit: commitRename,
2481
- onRenameCancel: cancelRename,
2482
- scrollRef,
2483
- hasMore,
2484
- loadingMore: loadingMore || overlayMore,
2485
- onLoadMore: loadNext
2486
- }
2487
- )
2488
- }
2489
- ) : /* @__PURE__ */ jsxRuntime.jsx(
2476
+ /* @__PURE__ */ jsxRuntime.jsxs(
2490
2477
  "div",
2491
2478
  {
2492
- onClick: (event) => {
2493
- if (event.target.closest("[data-file-id]")) return;
2494
- clearSelection();
2495
- },
2496
- children: /* @__PURE__ */ jsxRuntime.jsx(
2497
- ExplorerGridView,
2498
- {
2499
- files: displayFiles,
2500
- protectedIds: protectedSet,
2501
- selectedIds,
2502
- onSelect: handleSelect,
2503
- onActivate: handleActivate,
2504
- onContextMenu: handleContextMenu,
2505
- onDragStart: explorer.beginDrag,
2506
- onDragEnd: explorer.endDrag,
2507
- onDrop: handleDropOnFolder,
2508
- draggingId: explorer.draggingId,
2509
- draggingIds,
2510
- dropTargetId: explorer.dropTargetId,
2511
- onDragOverRow: explorer.setDropTarget,
2512
- renamingId,
2513
- onRenameCommit: commitRename,
2514
- onRenameCancel: cancelRename,
2515
- scrollRef,
2516
- hasMore,
2517
- loadingMore: loadingMore || overlayMore,
2518
- onLoadMore: loadNext
2519
- }
2520
- )
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
+ ]
2521
2563
  }
2522
- ) }),
2564
+ ),
2523
2565
  /* @__PURE__ */ jsxRuntime.jsxs("footer", { className: "flex shrink-0 items-center justify-between gap-3 border-t border-border px-4 py-2", children: [
2524
2566
  /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "font-mono text-[10px] uppercase tracking-[0.12em] text-muted-foreground", children: [
2525
2567
  /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "text-primary tabular-nums", children: [
@@ -2540,6 +2582,8 @@ function FileExplorerInner(props) {
2540
2582
  target: menu,
2541
2583
  mode: trashOpen ? "trash" : "browse",
2542
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,
2543
2587
  extraFileAction,
2544
2588
  actions: {
2545
2589
  deleteFile: async (input) => {
@@ -2574,7 +2618,10 @@ function FileExplorerInner(props) {
2574
2618
  ExplorerSelectionToast,
2575
2619
  {
2576
2620
  files: selectedFiles,
2577
- protectedIds: protectedSet,
2621
+ protectedIds: /* @__PURE__ */ new Set([
2622
+ ...protectedSet,
2623
+ ...selectedFiles.filter((file) => locksFor(file).delete).map((file) => file.id)
2624
+ ]),
2578
2625
  trashOpen,
2579
2626
  onDownload,
2580
2627
  onCopy: trashOpen ? void 0 : () => copyToClipboard(selectedFiles),