cursedbelt 5.34.0 → 5.35.0

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.
Files changed (61) hide show
  1. package/dist/react/file-tree/FileTree.d.ts +27 -1
  2. package/dist/react/file-tree/FileTree.d.ts.map +1 -1
  3. package/dist/react/file-tree/FileTree.js +183 -9
  4. package/dist/react/file-tree/FileTree.js.map +1 -1
  5. package/dist/react/file-tree/FolderViewMenus.d.ts +88 -0
  6. package/dist/react/file-tree/FolderViewMenus.d.ts.map +1 -0
  7. package/dist/react/file-tree/FolderViewMenus.js +121 -0
  8. package/dist/react/file-tree/FolderViewMenus.js.map +1 -0
  9. package/dist/react/file-tree/FolderViewPanels.d.ts +78 -0
  10. package/dist/react/file-tree/FolderViewPanels.d.ts.map +1 -0
  11. package/dist/react/file-tree/FolderViewPanels.js +155 -0
  12. package/dist/react/file-tree/FolderViewPanels.js.map +1 -0
  13. package/dist/react/file-tree/fileTree.css +146 -0
  14. package/dist/react/file-tree/folderView.d.ts +117 -0
  15. package/dist/react/file-tree/folderView.d.ts.map +1 -0
  16. package/dist/react/file-tree/folderView.js +230 -0
  17. package/dist/react/file-tree/folderView.js.map +1 -0
  18. package/dist/react/file-tree/folderViewPanels.css +217 -0
  19. package/dist/react/file-tree/index.d.ts +2 -0
  20. package/dist/react/file-tree/index.d.ts.map +1 -1
  21. package/dist/react/file-tree/index.js +9 -0
  22. package/dist/react/file-tree/index.js.map +1 -1
  23. package/dist/react/file-tree/useFolderView.d.ts +56 -0
  24. package/dist/react/file-tree/useFolderView.d.ts.map +1 -0
  25. package/dist/react/file-tree/useFolderView.js +79 -0
  26. package/dist/react/file-tree/useFolderView.js.map +1 -0
  27. package/dist/react/filter-rail/FilterRail.d.ts +7 -0
  28. package/dist/react/filter-rail/FilterRail.d.ts.map +1 -1
  29. package/dist/react/filter-rail/FilterRail.js +8 -1
  30. package/dist/react/filter-rail/FilterRail.js.map +1 -1
  31. package/dist/react/filter-rail/RailSection.d.ts +7 -1
  32. package/dist/react/filter-rail/RailSection.d.ts.map +1 -1
  33. package/dist/react/filter-rail/RailSection.js +5 -3
  34. package/dist/react/filter-rail/RailSection.js.map +1 -1
  35. package/dist/react/filter-rail/headerContextMenu.d.ts +8 -0
  36. package/dist/react/filter-rail/headerContextMenu.d.ts.map +1 -0
  37. package/dist/react/filter-rail/headerContextMenu.js +31 -0
  38. package/dist/react/filter-rail/headerContextMenu.js.map +1 -0
  39. package/dist/react/media/denseGallery.css +46 -0
  40. package/dist/react/media-gallery/rememberedPreference.d.ts +10 -1
  41. package/dist/react/media-gallery/rememberedPreference.d.ts.map +1 -1
  42. package/dist/react/media-gallery/rememberedPreference.js +12 -3
  43. package/dist/react/media-gallery/rememberedPreference.js.map +1 -1
  44. package/package.json +1 -1
  45. package/scripts/guardrails-baseline.json +1 -1
  46. package/src/react/file-tree/FileTree.tsx +323 -4
  47. package/src/react/file-tree/FolderViewMenus.tsx +242 -0
  48. package/src/react/file-tree/FolderViewPanels.tsx +402 -0
  49. package/src/react/file-tree/fileTree.css +146 -0
  50. package/src/react/file-tree/folderView.spec.ts +174 -0
  51. package/src/react/file-tree/folderView.ts +316 -0
  52. package/src/react/file-tree/folderViewPanels.css +217 -0
  53. package/src/react/file-tree/folderViewTree.spec.tsx +252 -0
  54. package/src/react/file-tree/index.ts +14 -0
  55. package/src/react/file-tree/useFolderView.ts +152 -0
  56. package/src/react/filter-rail/FilterRail.tsx +35 -6
  57. package/src/react/filter-rail/RailSection.tsx +10 -2
  58. package/src/react/filter-rail/headerContextMenu.spec.tsx +62 -0
  59. package/src/react/filter-rail/headerContextMenu.ts +37 -0
  60. package/src/react/media/denseGallery.css +46 -0
  61. package/src/react/media-gallery/rememberedPreference.ts +10 -2
@@ -161,7 +161,11 @@
161
161
  */
162
162
  import type { DragEvent as ReactDragEvent, JSX, ReactNode } from 'react';
163
163
  import { type AccessMarkCopy, type FileTreeDrop, type FileTreeNode } from 'cursedbelt-core/file-tree/file-tree-model';
164
+ import { type FileTreeMenuItem } from './FolderViewMenus.js';
165
+ import type { FolderView } from './useFolderView.js';
164
166
  import './fileTree.css';
167
+ /** Fetch the right-click panels now — for a spec, which must not race the chunk. */
168
+ export declare const preloadFileTreePanels: () => Promise<void>;
165
169
  export interface FileTreeProps {
166
170
  nodes: readonly FileTreeNode[];
167
171
  /** The row drawn as selected — usually "what the main pane is showing". */
@@ -467,6 +471,28 @@ export interface FileTreeProps {
467
471
  x: number;
468
472
  y: number;
469
473
  }) => void;
474
+ /**
475
+ * The row's menu on **right-click** (and ⌃-click, and Shift+F10 when {@link onRowMenu} is not
476
+ * wired) — drawn by the tree at the pointer, VS Code's and Finder's gesture.
477
+ *
478
+ * Owner, 2026-09-30: *"add an option to the right click menu to hide a folder"*. Hand it the
479
+ * SAME list your ⋯ menu draws (the shape is `DropdownMenu`'s), so the two can never offer
480
+ * different things; {@link folderView}'s "Hide folder" and "Tags…" are appended to it on a
481
+ * folder. Omit both and a right-click is the browser's, as it always was.
482
+ */
483
+ rowMenu?: (node: FileTreeNode) => readonly FileTreeMenuItem[];
484
+ /**
485
+ * Items for the right-click on the BLANK space under the rows — "New folder", say. Drawn above
486
+ * {@link folderView}'s own view items in the same menu.
487
+ */
488
+ backgroundMenu?: () => readonly FileTreeMenuItem[];
489
+ /**
490
+ * Hidden folders and folder tags — `useFolderView({ key })`. Wiring it is what puts "Hide
491
+ * folder" and "Tags…" on a folder's right-click, "Show hidden folders" and "Filter by tag" on
492
+ * the blank space's, ⇧⌘. on the keyboard, the tag dots on tagged rows and the strip that says
493
+ * when this device is narrowing the tree. See `folderView.ts` for the rules.
494
+ */
495
+ folderView?: FolderView | null;
470
496
  /** Trailing controls for a row — the tool shelf. Quiet at rest, never invisible. */
471
497
  renderTools?: (node: FileTreeNode) => ReactNode;
472
498
  /** Leading glyph. Defaults to a folder/file mark. */
@@ -548,5 +574,5 @@ export interface FileTreeProps {
548
574
  */
549
575
  scroll?: 'own' | 'host';
550
576
  }
551
- export declare function FileTree({ nodes, selectedId, onSelect, selectedIds, onSelectionChange, onOpen, collapsed: collapsedProp, onCollapsedChange, expandOnReveal, onRevealExpand, pinnedIds, onPinnedChange, pinnedLabel, onMove, onMoveMany, onDropOutside, describeDropOutside, allowReorder, lockedReason, canDrop, takesDrop, unavailableReason, renamingId, onRenameStart, onRename, onRenameCancel, onDelete, onDeleteMany, onCopy, canCopy, draft, onDraftCommit, onDraftCancel, draftPlaceholder, onWrapStart, canWrap, onRowMenu, renderTools, renderIcon, thumbnailTransform, renderAccessory, onDropExternal, acceptsExternalDrag, accessCopy, nameTooltips, markPath, filter, empty, ariaLabel, className, scroll, }: FileTreeProps): JSX.Element;
577
+ export declare function FileTree({ nodes, selectedId, onSelect, selectedIds, onSelectionChange, onOpen, collapsed: collapsedProp, onCollapsedChange, expandOnReveal, onRevealExpand, pinnedIds, onPinnedChange, pinnedLabel, onMove, onMoveMany, onDropOutside, describeDropOutside, allowReorder, lockedReason, canDrop, takesDrop, unavailableReason, renamingId, onRenameStart, onRename, onRenameCancel, onDelete, onDeleteMany, onCopy, canCopy, draft, onDraftCommit, onDraftCancel, draftPlaceholder, onWrapStart, canWrap, onRowMenu, rowMenu, backgroundMenu, folderView, renderTools, renderIcon, thumbnailTransform, renderAccessory, onDropExternal, acceptsExternalDrag, accessCopy, nameTooltips, markPath, filter, empty, ariaLabel, className, scroll, }: FileTreeProps): JSX.Element;
552
578
  //# sourceMappingURL=FileTree.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"FileTree.d.ts","sourceRoot":"","sources":["../../../src/react/file-tree/FileTree.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgKG;AACH,OAAO,KAAK,EACV,SAAS,IAAI,cAAc,EAC3B,GAAG,EAEH,SAAS,EACV,MAAM,OAAO,CAAC;AAEf,OAAO,EACL,KAAK,cAAc,EAKnB,KAAK,YAAY,EACjB,KAAK,YAAY,EAOlB,MAAM,2CAA2C,CAAC;AAenD,OAAO,gBAAgB,CAAC;AAwExB,MAAM,WAAW,aAAa;IAC5B,KAAK,EAAE,SAAS,YAAY,EAAE,CAAC;IAC/B,2EAA2E;IAC3E,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,QAAQ,EAAE,CAAC,IAAI,EAAE,YAAY,KAAK,IAAI,CAAC;IAEvC;;;;;;;;;;;;;OAaG;IACH,WAAW,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAChC;;;;;;OAMG;IACH,iBAAiB,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,IAAI,CAAC;IAC5C;;;;OAIG;IACH,MAAM,CAAC,EAAE,CAAC,IAAI,EAAE,YAAY,KAAK,IAAI,CAAC;IAEtC,gFAAgF;IAChF,SAAS,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAC9B,iBAAiB,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,IAAI,CAAC;IAC7C;;;;;;;;;;;;;;;;;;;OAmBG;IACH,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB;;;;;;;;;;;OAWG;IACH,cAAc,CAAC,EAAE,CAAC,GAAG,EAAE,SAAS,MAAM,EAAE,KAAK,IAAI,CAAC;IAElD;;;;;;;;;;;;;;;;;;OAkBG;IACH,SAAS,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAC9B;;;;;;;OAOG;IACH,cAAc,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,IAAI,CAAC;IAC1C;;;;;OAKG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;IAErB;;;OAGG;IACH,MAAM,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,YAAY,KAAK,IAAI,CAAC;IACtD;;;;;;;;;;;;OAYG;IACH,UAAU,CAAC,EAAE,CAAC,OAAO,EAAE,SAAS,MAAM,EAAE,EAAE,IAAI,EAAE,YAAY,KAAK,IAAI,CAAC;IACtE;;;;;;;;;;OAUG;IACH,aAAa,CAAC,EAAE,CACd,OAAO,EAAE,SAAS,MAAM,EAAE,EAC1B,KAAK,EAAE;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,KACxC,IAAI,CAAC;IACV;;;;;OAKG;IACH,mBAAmB,CAAC,EAAE,CAAC,KAAK,EAAE;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,KAAK,MAAM,GAAG,IAAI,CAAC;IACrF,sFAAsF;IACtF,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,0EAA0E;IAC1E,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,0FAA0F;IAC1F,OAAO,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,EAAE,MAAM,EAAE,YAAY,KAAK,MAAM,GAAG,IAAI,CAAC;IACzE;;;OAGG;IACH,SAAS,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,EAAE,IAAI,EAAE,YAAY,KAAK,OAAO,CAAC;IACnE,kFAAkF;IAClF,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAE3B,2FAA2F;IAC3F,UAAU,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B;;;;;;;;;OASG;IACH,aAAa,CAAC,EAAE,CAAC,IAAI,EAAE,YAAY,KAAK,IAAI,CAAC;IAC7C,sCAAsC;IACtC,QAAQ,CAAC,EAAE,CAAC,IAAI,EAAE,YAAY,EAAE,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IACtD,6DAA6D;IAC7D,cAAc,CAAC,EAAE,MAAM,IAAI,CAAC;IAC5B;;;;;;OAMG;IACH,QAAQ,CAAC,EAAE,CAAC,IAAI,EAAE,YAAY,KAAK,IAAI,CAAC;IACxC;;;;;;;OAOG;IACH,YAAY,CAAC,EAAE,CAAC,KAAK,EAAE,SAAS,YAAY,EAAE,KAAK,IAAI,CAAC;IAExD;;;;;;OAMG;IACH,MAAM,CAAC,EAAE,CAAC,OAAO,EAAE,SAAS,MAAM,EAAE,EAAE,IAAI,EAAE,YAAY,KAAK,IAAI,CAAC;IAClE;;;;;;;;OAQG;IACH,OAAO,CAAC,EAAE,CAAC,IAAI,EAAE,YAAY,KAAK,MAAM,GAAG,IAAI,CAAC;IAEhD;;;;;;;;;;;;OAYG;IACH,KAAK,CAAC,EAAE;QAAE,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;QAAC,IAAI,CAAC,EAAE,SAAS,MAAM,EAAE,CAAA;KAAE,GAAG,IAAI,CAAC;IACrE;;;;;;;;;;;;;OAaG;IACH,aAAa,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,SAAS,MAAM,EAAE,KAAK,IAAI,CAAC;IACjE,aAAa,CAAC,EAAE,MAAM,IAAI,CAAC;IAC3B;;;;;;;OAOG;IACH,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B;;;;;;;;;;;;;;OAcG;IACH,WAAW,CAAC,EAAE,CAAC,KAAK,EAAE,SAAS,YAAY,EAAE,EAAE,QAAQ,EAAE,MAAM,GAAG,IAAI,KAAK,IAAI,CAAC;IAChF;;;;;;;;;;;OAWG;IACH,OAAO,CAAC,EAAE,CAAC,IAAI,EAAE,YAAY,KAAK,MAAM,GAAG,IAAI,CAAC;IAEhD;;;;;;;;;;;;;OAaG;IACH,SAAS,CAAC,EAAE,CAAC,IAAI,EAAE,YAAY,EAAE,EAAE,EAAE;QAAE,CAAC,EAAE,MAAM,CAAC;QAAC,CAAC,EAAE,MAAM,CAAA;KAAE,KAAK,IAAI,CAAC;IAEvE,oFAAoF;IACpF,WAAW,CAAC,EAAE,CAAC,IAAI,EAAE,YAAY,KAAK,SAAS,CAAC;IAChD,qDAAqD;IACrD,UAAU,CAAC,EAAE,CAAC,IAAI,EAAE,YAAY,KAAK,SAAS,CAAC;IAC/C;;;;;;;;OAQG;IACH,kBAAkB,CAAC,EAAE,CAAC,IAAI,EAAE,YAAY,KAAK,MAAM,GAAG,SAAS,CAAC;IAChE,2DAA2D;IAC3D,eAAe,CAAC,EAAE,CAAC,IAAI,EAAE,YAAY,KAAK,SAAS,CAAC;IAEpD;;;;;OAKG;IACH,cAAc,CAAC,EAAE,CAAC,IAAI,EAAE,YAAY,GAAG,IAAI,EAAE,KAAK,EAAE,cAAc,KAAK,IAAI,CAAC;IAC5E,oFAAoF;IACpF,mBAAmB,CAAC,EAAE,CAAC,KAAK,EAAE,cAAc,KAAK,OAAO,CAAC;IAEzD;;;;;;;;;;OAUG;IACH,UAAU,CAAC,EAAE,cAAc,CAAC;IAE5B;;;;;;;;;;OAUG;IACH,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB;;;;;;;;;;OAUG;IACH,QAAQ,CAAC,EAAE,OAAO,CAAC;IAEnB,gFAAgF;IAChF,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,2FAA2F;IAC3F,KAAK,CAAC,EAAE,SAAS,CAAC;IAClB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;;;;;;;;;;;OAWG;IACH,MAAM,CAAC,EAAE,KAAK,GAAG,MAAM,CAAC;CACzB;AA8FD,wBAAgB,QAAQ,CAAC,EACvB,KAAK,EACL,UAAU,EACV,QAAQ,EACR,WAAW,EACX,iBAAiB,EACjB,MAAM,EACN,SAAS,EAAE,aAAa,EACxB,iBAAiB,EACjB,cAAsB,EACtB,cAAc,EACd,SAAS,EACT,cAAc,EACd,WAAsB,EACtB,MAAM,EACN,UAAU,EACV,aAAa,EACb,mBAAmB,EACnB,YAAoB,EACpB,YAAY,EACZ,OAAO,EACP,SAAS,EACT,iBAAiB,EACjB,UAAU,EACV,aAAa,EACb,QAAQ,EACR,cAAc,EACd,QAAQ,EACR,YAAY,EACZ,MAAM,EACN,OAAO,EACP,KAAK,EACL,aAAa,EACb,aAAa,EACb,gBAAgB,EAChB,WAAW,EACX,OAAO,EACP,SAAS,EACT,WAAW,EACX,UAAU,EACV,kBAAkB,EAClB,eAAe,EACf,cAAc,EACd,mBAAmB,EACnB,UAAU,EACV,YAAmB,EACnB,QAAe,EACf,MAAM,EACN,KAA2B,EAC3B,SAAmB,EACnB,SAAS,EACT,MAAc,GACf,EAAE,aAAa,GAAG,GAAG,CAAC,OAAO,CA4hE7B"}
1
+ {"version":3,"file":"FileTree.d.ts","sourceRoot":"","sources":["../../../src/react/file-tree/FileTree.tsx"],"names":[],"mappings":"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"}
@@ -6,7 +6,19 @@ import { Input } from '../components/Input.js';
6
6
  import { Tooltip } from '../components/Tooltip.js';
7
7
  import { draggedIdsOf, nodesByIds, planWrap, refuseCopy, refuseWrap, resolveMultiDrop, selectionRange, toggleSelected, topmostSelected, } from './fileTreeSelection.js';
8
8
  import { useInView } from '../media/previewGate.js';
9
+ import { lazyPart } from '../components/DataTable/lazyPart.js';
10
+ import { applyFolderView, tagFilterActive } from './folderView.js';
11
+ import { FolderViewStrip, folderTargets, folderViewMenuItems, RowTagDots, withFolderItems, } from './FolderViewMenus.js';
9
12
  import './fileTree.css';
13
+ /**
14
+ * The right-click panels — row menu, view menu, tag editor — fetched on demand. They reach
15
+ * Radix's Popover and the belt's ScrollArea, which a tree nobody right-clicks never needs, so
16
+ * they stay out of an app's first chunk; `preload` at idle makes the first right-click draw at
17
+ * once anyway. See `FolderViewPanels.tsx`.
18
+ */
19
+ const TreePanel = lazyPart(() => import('./FolderViewPanels.js').then((m) => m.TreePanel));
20
+ /** Fetch the right-click panels now — for a spec, which must not race the chunk. */
21
+ export const preloadFileTreePanels = () => TreePanel.preload();
10
22
  /** How far a press travels before it stops being a click and becomes a drag. */
11
23
  const DRAG_THRESHOLD = 4;
12
24
  /** How near the top or bottom of the list the pointer scrolls it. */
@@ -96,7 +108,7 @@ function RowGlyph({ node, open, renderIcon, transform, }) {
96
108
  return icon;
97
109
  return (_jsx("img", { ref: ref, className: 'cbft-thumb', src: seen || inView ? src : undefined, alt: '', "aria-hidden": 'true', loading: 'lazy', decoding: 'async', draggable: false, onError: () => setFailed(src), ...(transform ? { style: { transform } } : {}) }, src));
98
110
  }
99
- export function FileTree({ nodes, selectedId, onSelect, selectedIds, onSelectionChange, onOpen, collapsed: collapsedProp, onCollapsedChange, expandOnReveal = false, onRevealExpand, pinnedIds, onPinnedChange, pinnedLabel = 'Pinned', onMove, onMoveMany, onDropOutside, describeDropOutside, allowReorder = false, lockedReason, canDrop, takesDrop, unavailableReason, renamingId, onRenameStart, onRename, onRenameCancel, onDelete, onDeleteMany, onCopy, canCopy, draft, onDraftCommit, onDraftCancel, draftPlaceholder, onWrapStart, canWrap, onRowMenu, renderTools, renderIcon, thumbnailTransform, renderAccessory, onDropExternal, acceptsExternalDrag, accessCopy, nameTooltips = true, markPath = true, filter, empty = 'Nothing here yet.', ariaLabel = 'Files', className, scroll = 'own', }) {
111
+ export function FileTree({ nodes, selectedId, onSelect, selectedIds, onSelectionChange, onOpen, collapsed: collapsedProp, onCollapsedChange, expandOnReveal = false, onRevealExpand, pinnedIds, onPinnedChange, pinnedLabel = 'Pinned', onMove, onMoveMany, onDropOutside, describeDropOutside, allowReorder = false, lockedReason, canDrop, takesDrop, unavailableReason, renamingId, onRenameStart, onRename, onRenameCancel, onDelete, onDeleteMany, onCopy, canCopy, draft, onDraftCommit, onDraftCancel, draftPlaceholder, onWrapStart, canWrap, onRowMenu, rowMenu, backgroundMenu, folderView = null, renderTools, renderIcon, thumbnailTransform, renderAccessory, onDropExternal, acceptsExternalDrag, accessCopy, nameTooltips = true, markPath = true, filter, empty = 'Nothing here yet.', ariaLabel = 'Files', className, scroll = 'own', }) {
100
112
  const scrollRef = useRef(null);
101
113
  /** The whole tree, pinned section included — "outside" for `onDropOutside` is outside this. */
102
114
  const rootRef = useRef(null);
@@ -247,7 +259,19 @@ export function FileTree({ nodes, selectedId, onSelect, selectedIds, onSelection
247
259
  if (!clipboard)
248
260
  mirrored.current = null;
249
261
  }, [clipboard]);
250
- const shown = useMemo(() => (filter ? filterFileTree(nodes, filter) : nodes), [nodes, filter]);
262
+ /*
263
+ * Hidden folders and the tag filter come off FIRST, before the search: a search that found
264
+ * things inside a hidden folder would be the folder un-hiding itself. See `folderView.ts`.
265
+ */
266
+ const viewed = useMemo(() => (folderView ? applyFolderView(nodes, folderView.marks, folderView.local) : null), [nodes, folderView]);
267
+ const viewedNodes = viewed?.nodes ?? nodes;
268
+ const shown = useMemo(() => (filter ? filterFileTree(viewedNodes, filter) : viewedNodes), [viewedNodes, filter]);
269
+ /**
270
+ * What the pinned section is built from — the tree WITHOUT the folder view. A pin is the
271
+ * person saying *"keep it there until I unpin it"*, and hiding the folder it lives in, or
272
+ * narrowing this device to another tag, is not unpinning it.
273
+ */
274
+ const pinSource = useMemo(() => (viewed ? (filter ? filterFileTree(nodes, filter) : nodes) : shown), [viewed, filter, nodes, shown]);
251
275
  /*
252
276
  * A search shows everything it kept — a match hidden inside a collapsed folder is a match
253
277
  * the person cannot see, which reads as "not found".
@@ -273,7 +297,7 @@ export function FileTree({ nodes, selectedId, onSelect, selectedIds, onSelection
273
297
  const pinnedRows = useMemo(() => {
274
298
  if (pinned.length === 0)
275
299
  return [];
276
- const all = new Map(flattenFileTree(shown, []).map((row) => [row.node.id, row]));
300
+ const all = new Map(flattenFileTree(pinSource, []).map((row) => [row.node.id, row]));
277
301
  const out = [];
278
302
  for (const id of pinned) {
279
303
  const real = all.get(id);
@@ -281,7 +305,7 @@ export function FileTree({ nodes, selectedId, onSelect, selectedIds, onSelection
281
305
  out.push({ ...real, depth: 0, expandable: false, expanded: false });
282
306
  }
283
307
  return out;
284
- }, [pinned, shown]);
308
+ }, [pinned, pinSource]);
285
309
  /**
286
310
  * The whole selection, singular or plural — `[selectedId]` for a consumer that never
287
311
  * passed {@link FileTreeProps.selectedIds}, which is every consumer that existed before
@@ -863,6 +887,71 @@ export function FileTree({ nodes, selectedId, onSelect, selectedIds, onSelection
863
887
  if (id)
864
888
  rowEl(id, pinnedCopy)?.focus();
865
889
  }, [rowEl]);
890
+ // ── Right-click menus ─────────────────────────────────────────────────────────────────
891
+ /**
892
+ * The row menu open at the pointer, or the view menu when the tree owns it alone (no
893
+ * {@link FileTreeProps.folderView}, whose hook holds the view menu so the APP's header can
894
+ * open it too).
895
+ */
896
+ const [ctxMenu, setCtxMenu] = useState(null);
897
+ /** Whether a row has a right-click menu at all — otherwise the browser's stays. */
898
+ const hasRowMenu = useCallback((node) => Boolean(rowMenu) || (folderView !== null && node.kind === 'folder'), [rowMenu, folderView]);
899
+ const openRowMenu = useCallback((node, pinnedCopy, at) => {
900
+ setFocused({ id: node.id, pinned: pinnedCopy });
901
+ setCtxMenu({ kind: 'row', node, pinned: pinnedCopy, at });
902
+ }, []);
903
+ const openViewMenu = useCallback((at) => {
904
+ if (folderView)
905
+ folderView.openViewMenu(at);
906
+ else
907
+ setCtxMenu({ kind: 'view', at });
908
+ }, [folderView]);
909
+ /*
910
+ * The blank space under the rows answers a right-click with the VIEW menu. A native listener
911
+ * rather than a JSX one because the scroller is the tree's frame, not a control — giving it a
912
+ * role to hang a handler on would announce it as one. A row answers its own menu; a press on
913
+ * a row's tools, or on a row with no menu, keeps the browser's.
914
+ */
915
+ const wantsBlankMenu = Boolean(folderView || backgroundMenu);
916
+ useEffect(() => {
917
+ const scroller = scrollRef.current;
918
+ if (!scroller || !wantsBlankMenu)
919
+ return;
920
+ const onContextMenu = (event) => {
921
+ if (event.target instanceof Element && event.target.closest('.cbft-row'))
922
+ return;
923
+ event.preventDefault();
924
+ openViewMenu({ x: event.clientX, y: event.clientY });
925
+ };
926
+ scroller.addEventListener('contextmenu', onContextMenu);
927
+ return () => scroller.removeEventListener('contextmenu', onContextMenu);
928
+ }, [wantsBlankMenu, openViewMenu]);
929
+ /** Fetch the panels once the page is idle, if this tree has any menu to open. */
930
+ const hasMenus = wantsBlankMenu || Boolean(rowMenu);
931
+ useEffect(() => {
932
+ if (!hasMenus)
933
+ return;
934
+ const load = () => void TreePanel.preload().catch(() => { });
935
+ const idle = globalThis.requestIdleCallback;
936
+ if (idle) {
937
+ const handle = idle(load, { timeout: 4000 });
938
+ return () => globalThis.cancelIdleCallback?.(handle);
939
+ }
940
+ const timer = setTimeout(load, 1500);
941
+ return () => clearTimeout(timer);
942
+ }, [hasMenus]);
943
+ /**
944
+ * Put the keyboard back on the row a menu was opened from — but only if the menu left it on
945
+ * `<body>`. An item that opened ANOTHER panel (Tags…) has already moved focus into it, and
946
+ * taking it back would leave the person typing into nothing.
947
+ */
948
+ const refocusAfterMenu = useCallback((id, pinnedCopy) => {
949
+ requestAnimationFrame(() => {
950
+ const at = globalThis.document?.activeElement;
951
+ if (!at || at === globalThis.document.body)
952
+ rowEl(id, pinnedCopy)?.focus();
953
+ });
954
+ }, [rowEl]);
866
955
  /*
867
956
  * ── The keyboard comes back from a rename ─────────────────────────────────────────────
868
957
  *
@@ -961,6 +1050,27 @@ export function FileTree({ nodes, selectedId, onSelect, selectedIds, onSelection
961
1050
  onRowMenu(row.node, { x: box?.right ?? 0, y: box?.bottom ?? 0 });
962
1051
  return;
963
1052
  }
1053
+ // No menu of the consumer's own: the tree's right-click menu answers the same keys.
1054
+ if (hasRowMenu(row.node) &&
1055
+ (event.key === 'ContextMenu' || (event.shiftKey && event.key === 'F10'))) {
1056
+ event.preventDefault();
1057
+ const box = rowEl(row.node.id, pinnedCopy)?.getBoundingClientRect();
1058
+ openRowMenu(row.node, pinnedCopy, {
1059
+ x: (box?.left ?? 0) + 24,
1060
+ y: box?.bottom ?? 0,
1061
+ });
1062
+ return;
1063
+ }
1064
+ /*
1065
+ * ⇧⌘. — show or leave out hidden folders. Finder's own chord for hidden files, so a Mac
1066
+ * user already has it in their hands. By `code`: with Shift held, `key` is `>` on a US
1067
+ * keyboard and something else on others.
1068
+ */
1069
+ if (folderView && event.code === 'Period' && event.shiftKey && (event.metaKey || event.ctrlKey)) {
1070
+ event.preventDefault();
1071
+ folderView.setShowHidden(!folderView.local.showHidden);
1072
+ return;
1073
+ }
964
1074
  /*
965
1075
  * ── ⌃⌘T — pin, and unpin ────────────────────────────────────────────────────────
966
1076
  *
@@ -1289,6 +1399,9 @@ export function FileTree({ nodes, selectedId, onSelect, selectedIds, onSelection
1289
1399
  onPinnedChange,
1290
1400
  onRenameStart,
1291
1401
  onRowMenu,
1402
+ hasRowMenu,
1403
+ openRowMenu,
1404
+ folderView,
1292
1405
  onSelect,
1293
1406
  onWrapStart,
1294
1407
  mirror,
@@ -1547,7 +1660,8 @@ export function FileTree({ nodes, selectedId, onSelect, selectedIds, onSelection
1547
1660
  refs.current.set(node.id, el);
1548
1661
  else
1549
1662
  refs.current.delete(node.id);
1550
- }, key: node.id, "data-id": node.id, className: 'cbft-row', "data-selected": isSelected || undefined, "data-cut": cut?.has(node.id) || undefined, "data-dragged": isDragged || undefined, "data-into": isDropTarget || undefined, "data-marker": marker ?? undefined, "data-pinned": isPinned || undefined, "data-pinned-copy": pinnedCopy || undefined, "data-ancestor": openPath.path.has(node.id) || undefined, "data-holds-open": (row.expandable && !row.expanded && openPath.holder === node.id) || undefined, "data-access": node.access && node.access !== 'normal' ? node.access : undefined, "data-matched": node.matched || undefined, style: { '--cbft-depth': row.depth }, onPointerDown: (event) => {
1663
+ }, key: node.id, "data-id": node.id, className: 'cbft-row', "data-selected": isSelected || undefined, "data-cut": cut?.has(node.id) || undefined, "data-dragged": isDragged || undefined, "data-into": isDropTarget || undefined, "data-marker": marker ?? undefined, "data-pinned": isPinned || undefined, "data-pinned-copy": pinnedCopy || undefined, "data-ancestor": openPath.path.has(node.id) || undefined, "data-holds-open": (row.expandable && !row.expanded && openPath.holder === node.id) || undefined, "data-access": node.access && node.access !== 'normal' ? node.access : undefined, "data-matched": node.matched || undefined, "data-hidden": viewed?.hiddenMarks.get(node.id), "data-menu-open": (ctxMenu?.kind === 'row' && ctxMenu.node.id === node.id && ctxMenu.pinned === pinnedCopy) ||
1664
+ undefined, style: { '--cbft-depth': row.depth }, onPointerDown: (event) => {
1551
1665
  if (inTools(event.target))
1552
1666
  return;
1553
1667
  // A finger on the row is a SCROLL; only the grip picks it up. See `startsDrag`.
@@ -1569,7 +1683,13 @@ export function FileTree({ nodes, selectedId, onSelect, selectedIds, onSelection
1569
1683
  if (inTools(event.target))
1570
1684
  return;
1571
1685
  (onOpen ?? onSelect)(node);
1572
- }, onKeyDown: (event) => onKeyDown(event, row, list, pinnedCopy), onFocus: () => setFocused({ id: node.id, pinned: pinnedCopy }), ...externalHandlers(node) },
1686
+ }, onKeyDown: (event) => onKeyDown(event, row, list, pinnedCopy), onFocus: () => setFocused({ id: node.id, pinned: pinnedCopy }), onContextMenu: (event) => {
1687
+ if (inTools(event.target) || !hasRowMenu(node))
1688
+ return;
1689
+ event.preventDefault();
1690
+ event.stopPropagation();
1691
+ openRowMenu(node, pinnedCopy, { x: event.clientX, y: event.clientY });
1692
+ }, ...externalHandlers(node) },
1573
1693
  row.expandable ? (_jsx("span", { className: 'cbft-twisty', "data-open": row.expanded || undefined, "aria-hidden": 'true', onPointerDown: (event) => event.stopPropagation(), onClick: (event) => {
1574
1694
  event.stopPropagation();
1575
1695
  toggle(node.id);
@@ -1578,6 +1698,8 @@ export function FileTree({ nodes, selectedId, onSelect, selectedIds, onSelection
1578
1698
  // VS Code's field: the whole name is there, and only the part before the
1579
1699
  // suffix is selected, so typing replaces the name and keeps the type.
1580
1700
  node.suffix ? node.name.length : undefined)) : (_jsx(Tooltip, { content: nameTooltips ? node.name : undefined, children: _jsxs("span", { className: 'cbft-name', children: [node.name, node.suffix ? _jsx("span", { className: 'cbft-suffix', children: node.suffix }) : null] }) })),
1701
+ viewed?.hiddenMarks.get(node.id) === 'self' ? (_jsx("span", { className: 'cbft-hidden-mark', role: 'img', "aria-label": 'Hidden', children: _jsxs("svg", { viewBox: '0 0 24 24', width: '12', height: '12', fill: 'none', stroke: 'currentColor', strokeWidth: '2', strokeLinecap: 'round', strokeLinejoin: 'round', focusable: 'false', "aria-hidden": 'true', children: [_jsx("path", { d: 'M10.73 5.08A10.43 10.43 0 0 1 12 5c7 0 10 7 10 7a13.16 13.16 0 0 1-1.67 2.68' }), _jsx("path", { d: 'M6.61 6.61A13.53 13.53 0 0 0 2 12s3 7 10 7a9.74 9.74 0 0 0 5.39-1.61' }), _jsx("path", { d: 'M14.12 14.12a3 3 0 1 1-4.24-4.24' }), _jsx("path", { d: 'm2 2 20 20' })] }) })) : null,
1702
+ folderView && node.kind === 'folder' ? (_jsx(RowTagDots, { tags: folderView.marks.tags[node.id] ?? [] })) : null,
1581
1703
  node.access === 'private' || node.access === 'locked' ? (_jsx(Tooltip, { content: accessMarkCopy(node.access, accessCopy), children: _jsx("span", { className: 'cbft-access', role: 'img', "aria-label": 'Private', children: "\uD83D\uDD12" }) })) : null,
1582
1704
  renderAccessory ? renderAccessory(node) : null,
1583
1705
  node.count !== undefined ? _jsx("span", { className: 'cbft-count', children: node.count }) : null,
@@ -1605,14 +1727,66 @@ export function FileTree({ nodes, selectedId, onSelect, selectedIds, onSelection
1605
1727
  // being made in a place, and the place is where the folder actually lives.
1606
1728
  return !pinnedCopy && draft && draft.parentId === node.id ? (_jsxs(Fragment, { children: [body, draftRow(row.depth + 1)] }, node.id)) : (body);
1607
1729
  };
1608
- return (_jsxs("div", { ref: rootRef, onPaste: onPaste, className: `cbft${scroll === 'host' ? ' cbft--host-scroll' : ''}${className ? ` ${className}` : ''}`, "data-quiet-path": markPath ? undefined : true, children: [pinnedRows.length > 0 ? (_jsxs("div", { className: 'cbft-pinned', children: [_jsx("div", { className: 'cbft-pinned-head', children: pinnedLabel }), _jsx("div", { role: 'tree', "aria-label": pinnedLabel, ...(onSelectionChange ? { 'aria-multiselectable': true } : {}), className: `cbft-pinned-list${drag?.active ? ' cbft-list-dragging' : ''}`, children: pinnedRows.map((row) => renderRow(row, true)) })] })) : null, _jsx("div", { className: 'cbft-scroll', ref: scrollRef, "data-into-root": (drag?.active && drag.offer.target?.kind === 'root') ||
1730
+ /** The open right-click menu or tag editor, if any — see `FolderViewMenus.tsx`. */
1731
+ const treeMenus = () => {
1732
+ const out = [];
1733
+ if (ctxMenu?.kind === 'row') {
1734
+ const { node, pinned: pinnedCopy, at } = ctxMenu;
1735
+ const close = () => {
1736
+ setCtxMenu(null);
1737
+ refocusAfterMenu(node.id, pinnedCopy);
1738
+ };
1739
+ const own = rowMenu?.(node) ?? [];
1740
+ const ids = folderTargets(node, selection, (id) => findFileTreeNode(nodes, id)?.kind === 'folder');
1741
+ const fv = folderView
1742
+ ? folderViewMenuItems(folderView, node, ids, viewed?.hiddenMarks.get(node.id) ?? null)
1743
+ : [];
1744
+ const items = withFolderItems(own, fv);
1745
+ if (items.length > 0)
1746
+ out.push(_jsx(TreePanel, { kind: 'row', at: at, label: `Actions for ${node.name}`, items: items, onClose: close }, 'row-menu'));
1747
+ }
1748
+ const viewAt = folderView?.panel?.kind === 'view'
1749
+ ? folderView.panel.at
1750
+ : ctxMenu?.kind === 'view'
1751
+ ? ctxMenu.at
1752
+ : null;
1753
+ if (viewAt) {
1754
+ const close = () => {
1755
+ setCtxMenu(null);
1756
+ folderView?.closePanel();
1757
+ };
1758
+ const folderIds = flattenFileTree(shown, [])
1759
+ .filter((r) => r.expandable)
1760
+ .map((r) => r.node.id);
1761
+ out.push(_jsx(TreePanel, { kind: 'view', view: folderView, at: viewAt, hiddenCount: viewed?.hiddenCount ?? 0, extraItems: backgroundMenu?.() ?? [], onCollapseAll: folderIds.length > 0 ? () => shutFolders(folderIds) : null, onClose: close }, 'view-menu'));
1762
+ }
1763
+ if (folderView?.panel?.kind === 'tags') {
1764
+ const { ids } = folderView.panel;
1765
+ const names = ids.map((id) => findFileTreeNode(nodes, id)?.name ?? '').filter(Boolean);
1766
+ let at = folderView.panel.at;
1767
+ if (!at) {
1768
+ // Opened from a ⋯ menu, which has no pointer to give: under the first folder's row.
1769
+ const box = (rowEl(ids[0] ?? '', false) ?? rootRef.current)?.getBoundingClientRect();
1770
+ at = { x: (box?.left ?? 0) + 24, y: box?.bottom ?? 0 };
1771
+ }
1772
+ out.push(_jsx(TreePanel, { kind: 'tags', view: folderView, ids: ids, names: names.length > 0 ? names : ['this folder'], at: at, onClose: () => {
1773
+ folderView.closePanel();
1774
+ if (ids[0])
1775
+ refocusAfterMenu(ids[0], false);
1776
+ } }, `tags:${ids.join(',')}`));
1777
+ }
1778
+ return out;
1779
+ };
1780
+ return (_jsxs("div", { ref: rootRef, onPaste: onPaste, className: `cbft${scroll === 'host' ? ' cbft--host-scroll' : ''}${className ? ` ${className}` : ''}`, "data-quiet-path": markPath ? undefined : true, children: [folderView ? _jsx(FolderViewStrip, { view: folderView, onOpenMenu: openViewMenu }) : null, pinnedRows.length > 0 ? (_jsxs("div", { className: 'cbft-pinned', children: [_jsx("div", { className: 'cbft-pinned-head', children: pinnedLabel }), _jsx("div", { role: 'tree', "aria-label": pinnedLabel, ...(onSelectionChange ? { 'aria-multiselectable': true } : {}), className: `cbft-pinned-list${drag?.active ? ' cbft-list-dragging' : ''}`, children: pinnedRows.map((row) => renderRow(row, true)) })] })) : null, _jsx("div", { className: 'cbft-scroll', ref: scrollRef, "data-into-root": (drag?.active && drag.offer.target?.kind === 'root') ||
1609
1781
  externalOverId === EXTERNAL_OVER_ROOT ||
1610
1782
  undefined, ...externalHandlers(null), onPointerUp: () => {
1611
1783
  // Releasing over the blank space means the top level; the pointer capture keeps
1612
1784
  // the row's own listeners running, so this only has to not swallow the event.
1613
- }, children: _jsxs("div", { role: 'tree', "aria-label": ariaLabel, ...(onSelectionChange ? { 'aria-multiselectable': true } : {}), className: `cbft-list${drag?.active ? ' cbft-list-dragging' : ''}`, children: [rows.length === 0 && !draft ? _jsx("div", { className: 'cbft-empty', children: empty }) : null, draft && draft.parentId === null ? draftRow(0) : null, rows.map((row) => renderRow(row, false)), draft && draft.parentId !== null && !rows.some((r) => r.node.id === draft.parentId)
1785
+ }, children: _jsxs("div", { role: 'tree', "aria-label": ariaLabel, ...(onSelectionChange ? { 'aria-multiselectable': true } : {}), className: `cbft-list${drag?.active ? ' cbft-list-dragging' : ''}`, children: [rows.length === 0 && !draft ? (_jsx("div", { className: 'cbft-empty', children: folderView && tagFilterActive(folderView.local) && nodes.length > 0
1786
+ ? 'No folders carry these tags. Right-click here to change the filter.'
1787
+ : empty })) : null, draft && draft.parentId === null ? draftRow(0) : null, rows.map((row) => renderRow(row, false)), draft && draft.parentId !== null && !rows.some((r) => r.node.id === draft.parentId)
1614
1788
  ? draftRow(0)
1615
- : null] }) }), drag?.active ? (_jsxs("div", { className: 'cbft-chip', "data-refused": drag.offer.refused || undefined, style: { left: `${drag.x}px`, top: `${drag.y}px` }, "aria-hidden": 'true', children: [_jsx("span", { className: 'cbft-chip-name', children: drag.ids.length > 1 ? `${drag.ids.length} items` : nameOf(shown, drag.id) }), _jsx("span", { className: 'cbft-chip-where', children: drag.offer.reason ?? describeDrop(shown, drag.offer.target) })] })) : null, _jsx("span", { className: 'cbft-sr', role: 'status', "aria-live": 'polite', children: drag?.active ? (drag.offer.reason ?? describeDrop(shown, drag.offer.target)) : notice })] }));
1789
+ : null] }) }), drag?.active ? (_jsxs("div", { className: 'cbft-chip', "data-refused": drag.offer.refused || undefined, style: { left: `${drag.x}px`, top: `${drag.y}px` }, "aria-hidden": 'true', children: [_jsx("span", { className: 'cbft-chip-name', children: drag.ids.length > 1 ? `${drag.ids.length} items` : nameOf(shown, drag.id) }), _jsx("span", { className: 'cbft-chip-where', children: drag.offer.reason ?? describeDrop(shown, drag.offer.target) })] })) : null, treeMenus(), _jsx("span", { className: 'cbft-sr', role: 'status', "aria-live": 'polite', children: drag?.active ? (drag.offer.reason ?? describeDrop(shown, drag.offer.target)) : notice })] }));
1616
1790
  }
1617
1791
  /**
1618
1792
  * Whether an event landed on the consumer's own row controls.