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
@@ -0,0 +1,230 @@
1
+ export const EMPTY_FOLDER_MARKS = Object.freeze({
2
+ hidden: Object.freeze([]),
3
+ tags: Object.freeze({}),
4
+ });
5
+ export const EMPTY_FOLDER_VIEW = Object.freeze({
6
+ showHidden: false,
7
+ only: Object.freeze([]),
8
+ hide: Object.freeze([]),
9
+ });
10
+ /** Longest tag kept. A tag is a word or two; anything longer is a sentence in the wrong field. */
11
+ export const MAX_TAG_CHARS = 32;
12
+ /**
13
+ * The one spelling of a tag: trimmed, inner whitespace collapsed, lower-cased, capped.
14
+ * `null` for nothing left — the caller treats that as "no tag", never as `""`.
15
+ */
16
+ export function normalizeTag(raw) {
17
+ const tag = raw.trim().replace(/\s+/g, ' ').toLowerCase().slice(0, MAX_TAG_CHARS).trim();
18
+ return tag.length > 0 ? tag : null;
19
+ }
20
+ function uniqueTags(list) {
21
+ const out = [];
22
+ for (const raw of list) {
23
+ const tag = typeof raw === 'string' ? normalizeTag(raw) : null;
24
+ if (tag && !out.includes(tag))
25
+ out.push(tag);
26
+ }
27
+ return out;
28
+ }
29
+ /**
30
+ * Read stored marks. Anything malformed reads as nothing rather than throwing — a corrupt
31
+ * value must cost the person their marks on that one value, never the tree.
32
+ */
33
+ export function parseFolderMarks(raw) {
34
+ if (!raw)
35
+ return EMPTY_FOLDER_MARKS;
36
+ try {
37
+ const value = JSON.parse(raw);
38
+ const hidden = Array.isArray(value.hidden)
39
+ ? [...new Set(value.hidden.filter((id) => typeof id === 'string' && id !== ''))]
40
+ : [];
41
+ const tags = {};
42
+ if (value.tags && typeof value.tags === 'object' && !Array.isArray(value.tags)) {
43
+ for (const [id, list] of Object.entries(value.tags)) {
44
+ if (!Array.isArray(list))
45
+ continue;
46
+ const clean = uniqueTags(list);
47
+ if (clean.length > 0)
48
+ tags[id] = clean;
49
+ }
50
+ }
51
+ return { hidden, tags };
52
+ }
53
+ catch {
54
+ return EMPTY_FOLDER_MARKS;
55
+ }
56
+ }
57
+ /** The stored form. `null` when there is nothing to keep, so an app can forget the key. */
58
+ export function serializeFolderMarks(marks) {
59
+ const tags = Object.fromEntries(Object.entries(marks.tags).filter(([, list]) => list.length > 0));
60
+ if (marks.hidden.length === 0 && Object.keys(tags).length === 0)
61
+ return null;
62
+ return JSON.stringify({ v: 1, hidden: marks.hidden, tags });
63
+ }
64
+ /** Read a device's view. Same forgiving rule as {@link parseFolderMarks}. */
65
+ export function parseFolderViewLocal(raw) {
66
+ if (!raw)
67
+ return EMPTY_FOLDER_VIEW;
68
+ try {
69
+ const value = JSON.parse(raw);
70
+ return {
71
+ showHidden: value.showHidden === true,
72
+ only: Array.isArray(value.only) ? uniqueTags(value.only) : [],
73
+ hide: Array.isArray(value.hide) ? uniqueTags(value.hide) : [],
74
+ };
75
+ }
76
+ catch {
77
+ return EMPTY_FOLDER_VIEW;
78
+ }
79
+ }
80
+ /** Hide or unhide these ids — a new value; the input is never touched. */
81
+ export function setHidden(marks, ids, hidden) {
82
+ const next = hidden
83
+ ? [...marks.hidden, ...ids.filter((id) => !marks.hidden.includes(id))]
84
+ : marks.hidden.filter((id) => !ids.includes(id));
85
+ return next.length === marks.hidden.length && next.every((id, i) => id === marks.hidden[i])
86
+ ? marks
87
+ : { ...marks, hidden: next };
88
+ }
89
+ /** Replace one folder's tags. An empty list forgets the folder entirely. */
90
+ export function setFolderTags(marks, id, tags) {
91
+ const clean = uniqueTags(tags);
92
+ const { [id]: _old, ...rest } = marks.tags;
93
+ return { ...marks, tags: clean.length > 0 ? { ...rest, [id]: clean } : rest };
94
+ }
95
+ /** Add or take away ONE tag on one folder — the tag editor's checkbox. */
96
+ export function toggleFolderTag(marks, id, raw) {
97
+ const tag = normalizeTag(raw);
98
+ if (!tag)
99
+ return marks;
100
+ const current = marks.tags[id] ?? [];
101
+ return setFolderTags(marks, id, current.includes(tag) ? current.filter((t) => t !== tag) : [...current, tag]);
102
+ }
103
+ /**
104
+ * Every tag in use. Derived, never stored: a tag no folder carries any more simply stops
105
+ * existing, so there is no tag list to tidy and no orphan to delete.
106
+ */
107
+ export function tagCounts(marks) {
108
+ const counts = new Map();
109
+ for (const list of Object.values(marks.tags))
110
+ for (const tag of list)
111
+ counts.set(tag, (counts.get(tag) ?? 0) + 1);
112
+ return [...counts]
113
+ .map(([tag, count]) => ({ tag, count }))
114
+ .sort((a, b) => b.count - a.count || a.tag.localeCompare(b.tag));
115
+ }
116
+ /** How many colours a tag dot can be — `--cbft-tag-0` … `--cbft-tag-7` in fileTree.css. */
117
+ export const TAG_HUES = 8;
118
+ /**
119
+ * A tag's colour slot, from its name — the same tag is the same colour in every app, on every
120
+ * device, without a colour having to be chosen or stored. FNV-1a, because it is four lines and
121
+ * spreads short words well.
122
+ */
123
+ export function tagHue(tag) {
124
+ let hash = 0x811c9dc5;
125
+ for (let i = 0; i < tag.length; i++) {
126
+ hash ^= tag.charCodeAt(i);
127
+ hash = Math.imul(hash, 0x01000193);
128
+ }
129
+ return (hash >>> 0) % TAG_HUES;
130
+ }
131
+ /** Whether a device's view narrows the tree by tag at all. */
132
+ export function tagFilterActive(local) {
133
+ return local.only.length > 0 || local.hide.length > 0;
134
+ }
135
+ /** Cycle one tag in the device's filter: `'only'`, `'hide'`, or `null` for neither. */
136
+ export function setTagFilter(local, raw, mode) {
137
+ const tag = normalizeTag(raw);
138
+ if (!tag)
139
+ return local;
140
+ const only = local.only.filter((t) => t !== tag);
141
+ const hide = local.hide.filter((t) => t !== tag);
142
+ if (mode === 'only')
143
+ only.push(tag);
144
+ if (mode === 'hide')
145
+ hide.push(tag);
146
+ return { ...local, only, hide };
147
+ }
148
+ /**
149
+ * The tree this device should draw. See the header for the five rules.
150
+ *
151
+ * Returns the input array itself when nothing is marked and nothing is filtered, so a tree
152
+ * with no marks pays one check and no copy.
153
+ */
154
+ export function applyFolderView(nodes, marks, local) {
155
+ const hiddenMarks = new Map();
156
+ const filtering = tagFilterActive(local);
157
+ if (marks.hidden.length === 0 && !filtering)
158
+ return { nodes, hiddenMarks, hiddenCount: 0 };
159
+ const hiddenSet = new Set(marks.hidden);
160
+ const only = new Set(local.only);
161
+ const hide = new Set(local.hide);
162
+ let hiddenCount = 0;
163
+ const carries = (id, set) => set.size > 0 && (marks.tags[id] ?? []).some((tag) => set.has(tag));
164
+ /**
165
+ * `null` = drop the node. `inHidden`: an ancestor is hidden. `matched`: an ancestor
166
+ * carries an "only" tag (or there is no "only" filter at all).
167
+ */
168
+ const visit = (node, depth, inHidden, matched) => {
169
+ const selfHidden = hiddenSet.has(node.id);
170
+ if (selfHidden)
171
+ hiddenCount++;
172
+ const hiddenState = selfHidden ? 'self' : inHidden ? 'inherited' : null;
173
+ if (hiddenState && !local.showHidden) {
174
+ // Still counted below it — "Show hidden folders (3)" counts nested ones too.
175
+ if (node.children)
176
+ countHidden(node.children);
177
+ return null;
178
+ }
179
+ if (carries(node.id, hide)) {
180
+ if (node.children)
181
+ countHidden(node.children);
182
+ return null;
183
+ }
184
+ const isMatch = matched || carries(node.id, only);
185
+ if (node.kind !== 'folder') {
186
+ // Rule 4: a top-level file is in no folder; a nested one follows its folder.
187
+ if (!isMatch && depth > 0)
188
+ return null;
189
+ if (hiddenState)
190
+ hiddenMarks.set(node.id, hiddenState);
191
+ return node;
192
+ }
193
+ let children = node.children;
194
+ if (node.children) {
195
+ const kept = [];
196
+ let changed = false;
197
+ for (const child of node.children) {
198
+ const out = visit(child, depth + 1, hiddenState !== null, isMatch);
199
+ if (out)
200
+ kept.push(out);
201
+ if (out !== child)
202
+ changed = true;
203
+ }
204
+ if (changed)
205
+ children = kept;
206
+ }
207
+ // Rule 3: an unmatched folder survives only as the way to a matched one.
208
+ if (!isMatch && (!children || children.length === 0))
209
+ return null;
210
+ if (hiddenState)
211
+ hiddenMarks.set(node.id, hiddenState);
212
+ return children === node.children ? node : { ...node, children: children ?? [] };
213
+ };
214
+ const countHidden = (list) => {
215
+ for (const node of list) {
216
+ if (hiddenSet.has(node.id))
217
+ hiddenCount++;
218
+ if (node.children)
219
+ countHidden(node.children);
220
+ }
221
+ };
222
+ const out = [];
223
+ for (const node of nodes) {
224
+ const kept = visit(node, 0, false, only.size === 0);
225
+ if (kept)
226
+ out.push(kept);
227
+ }
228
+ return { nodes: out, hiddenMarks, hiddenCount };
229
+ }
230
+ //# sourceMappingURL=folderView.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"folderView.js","sourceRoot":"","sources":["../../../src/react/file-tree/folderView.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,217 @@
1
+ /*
2
+ * The file tree's right-click panels — the menus' own pieces, the tag filter and the tag editor.
3
+ *
4
+ * Its own stylesheet because its own CHUNK: `FolderViewPanels.tsx` is loaded on demand (see its
5
+ * header), and a bundler ships a lazily imported module's CSS with it. Plain `cbft-` CSS for the
6
+ * reason `fileTree.css` gives in its header.
7
+ */
8
+
9
+ /* ── The menus' own pieces (the panel and its items are the belt's `Popover` and `Menu`) ── */
10
+
11
+ .cbft-menu-panel {
12
+ min-inline-size: 12rem;
13
+ max-inline-size: min(20rem, calc(100vw - 16px));
14
+ }
15
+
16
+ .cbft-fv-head {
17
+ display: flex;
18
+ align-items: baseline;
19
+ justify-content: space-between;
20
+ gap: 0.5rem;
21
+ padding-inline-end: 0.5rem;
22
+ }
23
+
24
+ .cbft-fv-scope {
25
+ font-size: 0.75rem;
26
+ color: var(--muted-foreground);
27
+ cursor: help;
28
+ }
29
+
30
+ .cbft-fv-empty {
31
+ margin: 0;
32
+ padding: 0.25rem 0.5rem 0.5rem;
33
+ font-size: 0.75rem;
34
+ line-height: 1.4;
35
+ color: var(--muted-foreground);
36
+ }
37
+
38
+ /* The belt's ScrollArea does the scrolling (and fades the cut edge); this only bounds it. */
39
+ .cbft-fv-scroll {
40
+ max-block-size: 15rem;
41
+ }
42
+
43
+ .cbft-fv-list {
44
+ list-style: none;
45
+ margin: 0;
46
+ padding: 0;
47
+ }
48
+
49
+ .cbft-fv-row {
50
+ display: flex;
51
+ align-items: center;
52
+ gap: 0.5rem;
53
+ min-block-size: 2rem;
54
+ padding: 0.125rem 0.25rem 0.125rem 0.5rem;
55
+ border-radius: var(--radius-sm, 0.375rem);
56
+ font-size: 0.875rem;
57
+ }
58
+
59
+ .cbft-fv-row:hover {
60
+ background: color-mix(in srgb, var(--muted-foreground) 8%, transparent);
61
+ }
62
+
63
+ .cbft-fv-name {
64
+ flex: 1 1 auto;
65
+ min-inline-size: 0;
66
+ overflow: hidden;
67
+ text-overflow: ellipsis;
68
+ white-space: nowrap;
69
+ }
70
+
71
+ .cbft-fv-count {
72
+ flex: none;
73
+ font-size: 0.75rem;
74
+ color: var(--muted-foreground);
75
+ font-variant-numeric: tabular-nums;
76
+ }
77
+
78
+ /* Two toggles, joined — a segmented control in miniature. Off is quiet; on is the accent. */
79
+ .cbft-fv-seg {
80
+ flex: none;
81
+ display: inline-flex;
82
+ /* A <fieldset>, so the browser's own frame comes off first. */
83
+ margin: 0;
84
+ padding: 0;
85
+ min-inline-size: 0;
86
+ border: 1px solid var(--border);
87
+ border-radius: var(--radius-sm, 0.375rem);
88
+ overflow: hidden;
89
+ }
90
+
91
+ .cbft-fv-seg button {
92
+ border: 0;
93
+ background: transparent;
94
+ color: var(--muted-foreground);
95
+ font: inherit;
96
+ font-size: 0.75rem;
97
+ padding: 0.1875rem 0.5rem;
98
+ min-block-size: 1.5rem;
99
+ cursor: pointer;
100
+ }
101
+
102
+ .cbft-fv-seg button + button {
103
+ border-inline-start: 1px solid var(--border);
104
+ }
105
+
106
+ .cbft-fv-seg button:hover {
107
+ background: color-mix(in srgb, var(--muted-foreground) 12%, transparent);
108
+ color: var(--foreground);
109
+ }
110
+
111
+ .cbft-fv-seg button:focus-visible {
112
+ outline: 2px solid var(--ring, var(--accent-solid));
113
+ outline-offset: -2px;
114
+ }
115
+
116
+ .cbft-fv-seg button[aria-pressed='true'] {
117
+ background: var(--accent-solid);
118
+ color: var(--accent-solid-foreground);
119
+ }
120
+
121
+ .cbft-fv-row[data-mode='hide'] .cbft-fv-name {
122
+ text-decoration: line-through;
123
+ color: var(--muted-foreground);
124
+ }
125
+
126
+ .cbft-fv-clear {
127
+ display: block;
128
+ inline-size: 100%;
129
+ margin-block-start: 0.25rem;
130
+ padding: 0.375rem 0.5rem;
131
+ border: 0;
132
+ border-radius: var(--radius-sm, 0.375rem);
133
+ background: transparent;
134
+ color: var(--accent);
135
+ font: inherit;
136
+ font-size: 0.8125rem;
137
+ text-align: start;
138
+ cursor: pointer;
139
+ }
140
+
141
+ .cbft-fv-clear:hover,
142
+ .cbft-fv-clear:focus-visible {
143
+ background: color-mix(in srgb, var(--muted-foreground) 10%, transparent);
144
+ outline: none;
145
+ }
146
+
147
+ /* ── The tag editor — Gmail's "Label as" ─────────────────────────────────────────────── */
148
+
149
+ .cbft-te {
150
+ display: flex;
151
+ flex-direction: column;
152
+ gap: 0.25rem;
153
+ }
154
+
155
+ .cbft-te-head {
156
+ padding: 0.25rem 0.5rem 0;
157
+ font-size: 0.8125rem;
158
+ font-weight: 600;
159
+ color: var(--foreground);
160
+ overflow: hidden;
161
+ text-overflow: ellipsis;
162
+ white-space: nowrap;
163
+ }
164
+
165
+ .cbft-te-input {
166
+ /* The belt's Input draws itself; this only spaces it inside the panel. */
167
+ margin: 0.25rem;
168
+ inline-size: auto;
169
+ }
170
+
171
+ .cbft-te-scroll {
172
+ max-block-size: 14rem;
173
+ }
174
+
175
+ .cbft-te-option {
176
+ display: flex;
177
+ align-items: center;
178
+ gap: 0.5rem;
179
+ min-block-size: 2rem;
180
+ padding: 0.25rem 0.5rem;
181
+ border-radius: var(--radius-sm, 0.375rem);
182
+ font-size: 0.875rem;
183
+ cursor: pointer;
184
+ }
185
+
186
+ .cbft-te-option[data-active] {
187
+ background: color-mix(in srgb, var(--muted-foreground) 14%, transparent);
188
+ }
189
+
190
+ .cbft-te-check {
191
+ flex: none;
192
+ inline-size: 1rem;
193
+ text-align: center;
194
+ color: var(--accent);
195
+ font-weight: 700;
196
+ }
197
+
198
+ .cbft-te-foot {
199
+ margin: 0;
200
+ padding: 0.25rem 0.5rem 0.375rem;
201
+ border-block-start: 1px solid color-mix(in srgb, var(--border) 70%, transparent);
202
+ font-size: 0.75rem;
203
+ color: var(--muted-foreground);
204
+ }
205
+
206
+ /* A coarse pointer gets the 44px hit targets every other control in this tree gets. */
207
+ @media (pointer: coarse) {
208
+ .cbft-fv-row,
209
+ .cbft-te-option {
210
+ min-block-size: 44px;
211
+ }
212
+
213
+ .cbft-fv-seg button {
214
+ min-block-size: 36px;
215
+ padding-inline: 0.75rem;
216
+ }
217
+ }
@@ -1,4 +1,6 @@
1
1
  export { type AccessMarkCopy, accessMarkCopy, descendantIdsOf, type DropOffer, type FileTreeAccess, type FileTreeDrop, type FileTreeNode, type FileTreeNodeKind, type FileTreeRow, filterFileTree, findFileTreeNode, flattenFileTree, matchTypeahead, parentIdOf, resolveDrop, walkFileTree, } from 'cursedbelt-core/file-tree/file-tree-model';
2
2
  export { FileTree, type FileTreeProps } from './FileTree.js';
3
3
  export { draggedIdsOf, nodesByIds, planWrap, refuseCopy, refuseWrap, type ResolveMultiDropOptions, resolveMultiDrop, selectionRange, toggleSelected, topmostSelected, type WrapPlan, wrapFolderName, } from './fileTreeSelection.js';
4
+ export { type FileTreeMenuItem, folderTargets, folderViewMenuItems, withFolderItems, } from './FolderViewMenus.js';
5
+ export { type FolderView, type UseFolderViewOptions, useFolderView } from './useFolderView.js';
4
6
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/react/file-tree/index.ts"],"names":[],"mappings":"AAcA,OAAO,EACL,KAAK,cAAc,EACnB,cAAc,EACd,eAAe,EACf,KAAK,SAAS,EACd,KAAK,cAAc,EACnB,KAAK,YAAY,EACjB,KAAK,YAAY,EACjB,KAAK,gBAAgB,EACrB,KAAK,WAAW,EAChB,cAAc,EACd,gBAAgB,EAChB,eAAe,EACf,cAAc,EACd,UAAU,EACV,WAAW,EACX,YAAY,GACb,MAAM,2CAA2C,CAAC;AACnD,OAAO,EAAE,QAAQ,EAAE,KAAK,aAAa,EAAE,MAAM,eAAe,CAAC;AAQ7D,OAAO,EACL,YAAY,EACZ,UAAU,EACV,QAAQ,EACR,UAAU,EACV,UAAU,EACV,KAAK,uBAAuB,EAC5B,gBAAgB,EAChB,cAAc,EACd,cAAc,EACd,eAAe,EACf,KAAK,QAAQ,EACb,cAAc,GACf,MAAM,wBAAwB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/react/file-tree/index.ts"],"names":[],"mappings":"AAcA,OAAO,EACL,KAAK,cAAc,EACnB,cAAc,EACd,eAAe,EACf,KAAK,SAAS,EACd,KAAK,cAAc,EACnB,KAAK,YAAY,EACjB,KAAK,YAAY,EACjB,KAAK,gBAAgB,EACrB,KAAK,WAAW,EAChB,cAAc,EACd,gBAAgB,EAChB,eAAe,EACf,cAAc,EACd,UAAU,EACV,WAAW,EACX,YAAY,GACb,MAAM,2CAA2C,CAAC;AACnD,OAAO,EAAE,QAAQ,EAAE,KAAK,aAAa,EAAE,MAAM,eAAe,CAAC;AAQ7D,OAAO,EACL,YAAY,EACZ,UAAU,EACV,QAAQ,EACR,UAAU,EACV,UAAU,EACV,KAAK,uBAAuB,EAC5B,gBAAgB,EAChB,cAAc,EACd,cAAc,EACd,eAAe,EACf,KAAK,QAAQ,EACb,cAAc,GACf,MAAM,wBAAwB,CAAC;AAQhC,OAAO,EACL,KAAK,gBAAgB,EACrB,aAAa,EACb,mBAAmB,EACnB,eAAe,GAChB,MAAM,sBAAsB,CAAC;AAC9B,OAAO,EAAE,KAAK,UAAU,EAAE,KAAK,oBAAoB,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC"}
@@ -22,4 +22,13 @@ export { FileTree } from './FileTree.js';
22
22
  * header of `fileTreeSelection.ts`.
23
23
  */
24
24
  export { draggedIdsOf, nodesByIds, planWrap, refuseCopy, refuseWrap, resolveMultiDrop, selectionRange, toggleSelected, topmostSelected, wrapFolderName, } from './fileTreeSelection.js';
25
+ /*
26
+ * Hidden folders and folder tags (owner, 2026-09-30) — `useFolderView({ key })` handed to the
27
+ * tree's `folderView` prop, and its `onContextMenu` on the app's header over the tree. The rules
28
+ * are DOM-free in `folderView.ts`; the menus, tag editor and strip are `FolderViewMenus.tsx`.
29
+ * `folderViewMenuItems` is for an app's own ⋯ menu, so it offers exactly what the right-click
30
+ * does; `FileTreeMenuItem` is `DropdownMenuItem`'s shape, for `rowMenu`/`backgroundMenu`.
31
+ */
32
+ export { folderTargets, folderViewMenuItems, withFolderItems, } from './FolderViewMenus.js';
33
+ export { useFolderView } from './useFolderView.js';
25
34
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/react/file-tree/index.ts"],"names":[],"mappings":"AAAA,0FAA0F;AAC1F,uFAAuF;AACvF,0FAA0F;AAC1F,2CAA2C;AAC3C,EAAE;AACF,8EAA8E;AAC9E,EAAE;AACF,oFAAoF;AACpF,0FAA0F;AAC1F,yFAAyF;AACzF,0FAA0F;AAC1F,0BAA0B;AAC1B,EAAE;AACF,yEAAyE;AACzE,OAAO,EAEL,cAAc,EACd,eAAe,EAOf,cAAc,EACd,gBAAgB,EAChB,eAAe,EACf,cAAc,EACd,UAAU,EACV,WAAW,EACX,YAAY,GACb,MAAM,2CAA2C,CAAC;AACnD,OAAO,EAAE,QAAQ,EAAsB,MAAM,eAAe,CAAC;AAC7D;;;;;;GAMG;AACH,OAAO,EACL,YAAY,EACZ,UAAU,EACV,QAAQ,EACR,UAAU,EACV,UAAU,EAEV,gBAAgB,EAChB,cAAc,EACd,cAAc,EACd,eAAe,EAEf,cAAc,GACf,MAAM,wBAAwB,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/react/file-tree/index.ts"],"names":[],"mappings":"AAAA,0FAA0F;AAC1F,uFAAuF;AACvF,0FAA0F;AAC1F,2CAA2C;AAC3C,EAAE;AACF,8EAA8E;AAC9E,EAAE;AACF,oFAAoF;AACpF,0FAA0F;AAC1F,yFAAyF;AACzF,0FAA0F;AAC1F,0BAA0B;AAC1B,EAAE;AACF,yEAAyE;AACzE,OAAO,EAEL,cAAc,EACd,eAAe,EAOf,cAAc,EACd,gBAAgB,EAChB,eAAe,EACf,cAAc,EACd,UAAU,EACV,WAAW,EACX,YAAY,GACb,MAAM,2CAA2C,CAAC;AACnD,OAAO,EAAE,QAAQ,EAAsB,MAAM,eAAe,CAAC;AAC7D;;;;;;GAMG;AACH,OAAO,EACL,YAAY,EACZ,UAAU,EACV,QAAQ,EACR,UAAU,EACV,UAAU,EAEV,gBAAgB,EAChB,cAAc,EACd,cAAc,EACd,eAAe,EAEf,cAAc,GACf,MAAM,wBAAwB,CAAC;AAChC;;;;;;GAMG;AACH,OAAO,EAEL,aAAa,EACb,mBAAmB,EACnB,eAAe,GAChB,MAAM,sBAAsB,CAAC;AAC9B,OAAO,EAA8C,aAAa,EAAE,MAAM,oBAAoB,CAAC"}
@@ -0,0 +1,56 @@
1
+ import { type FolderMarks, type FolderViewLocal, type TagCount } from './folderView.js';
2
+ /** A point in viewport coordinates — what a context menu anchors to. */
3
+ export interface FolderViewPoint {
4
+ x: number;
5
+ y: number;
6
+ }
7
+ /** Which of the tree's folder-view panels is open, if one is. */
8
+ export type FolderViewPanel = {
9
+ kind: 'view';
10
+ at: FolderViewPoint;
11
+ } | {
12
+ kind: 'tags';
13
+ at: FolderViewPoint | null;
14
+ ids: readonly string[];
15
+ };
16
+ export interface FolderView {
17
+ marks: FolderMarks;
18
+ local: FolderViewLocal;
19
+ /** Every tag in use, most-used first. */
20
+ tags: readonly TagCount[];
21
+ hide: (ids: readonly string[]) => void;
22
+ unhide: (ids: readonly string[]) => void;
23
+ /** Add or take away one tag on each folder named. */
24
+ toggleTag: (ids: readonly string[], tag: string) => void;
25
+ /** Replace one folder's tags. */
26
+ setTags: (id: string, tags: readonly string[]) => void;
27
+ setShowHidden: (show: boolean) => void;
28
+ /** Put one tag in this device's filter: only folders carrying it, none carrying it, or neither. */
29
+ filterTag: (tag: string, mode: 'only' | 'hide' | null) => void;
30
+ clearTagFilter: () => void;
31
+ /** The open panel — read by the tree, which draws it. */
32
+ panel: FolderViewPanel | null;
33
+ openViewMenu: (at: FolderViewPoint) => void;
34
+ /** Open the tag editor for these folders; `at` omitted anchors it to the first one's row. */
35
+ openTagEditor: (ids: readonly string[], at?: FolderViewPoint) => void;
36
+ closePanel: () => void;
37
+ /**
38
+ * For the APP's header over the tree: right-click (and ⌃-click, which macOS turns into a
39
+ * `contextmenu` by itself) opens the view menu at the pointer instead of the browser's.
40
+ */
41
+ onContextMenu: (event: {
42
+ clientX: number;
43
+ clientY: number;
44
+ preventDefault: () => void;
45
+ }) => void;
46
+ }
47
+ export interface UseFolderViewOptions {
48
+ /**
49
+ * Where the marks are remembered, and the name they are announced under. Name it per tree
50
+ * (`'collections:folder-marks'`) for the reason `FilterRail.resizeKey` is: two trees on one
51
+ * origin must not hide each other's folders.
52
+ */
53
+ key: string;
54
+ }
55
+ export declare function useFolderView({ key }: UseFolderViewOptions): FolderView;
56
+ //# sourceMappingURL=useFolderView.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useFolderView.d.ts","sourceRoot":"","sources":["../../../src/react/file-tree/useFolderView.ts"],"names":[],"mappings":"AA0BA,OAAO,EACL,KAAK,WAAW,EAChB,KAAK,eAAe,EAQpB,KAAK,QAAQ,EAGd,MAAM,iBAAiB,CAAC;AAEzB,wEAAwE;AACxE,MAAM,WAAW,eAAe;IAC9B,CAAC,EAAE,MAAM,CAAC;IACV,CAAC,EAAE,MAAM,CAAC;CACX;AAED,iEAAiE;AACjE,MAAM,MAAM,eAAe,GACvB;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,EAAE,EAAE,eAAe,CAAA;CAAE,GACrC;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,EAAE,EAAE,eAAe,GAAG,IAAI,CAAC;IAAC,GAAG,EAAE,SAAS,MAAM,EAAE,CAAA;CAAE,CAAC;AAEzE,MAAM,WAAW,UAAU;IACzB,KAAK,EAAE,WAAW,CAAC;IACnB,KAAK,EAAE,eAAe,CAAC;IACvB,yCAAyC;IACzC,IAAI,EAAE,SAAS,QAAQ,EAAE,CAAC;IAE1B,IAAI,EAAE,CAAC,GAAG,EAAE,SAAS,MAAM,EAAE,KAAK,IAAI,CAAC;IACvC,MAAM,EAAE,CAAC,GAAG,EAAE,SAAS,MAAM,EAAE,KAAK,IAAI,CAAC;IACzC,qDAAqD;IACrD,SAAS,EAAE,CAAC,GAAG,EAAE,SAAS,MAAM,EAAE,EAAE,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC;IACzD,iCAAiC;IACjC,OAAO,EAAE,CAAC,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,SAAS,MAAM,EAAE,KAAK,IAAI,CAAC;IAEvD,aAAa,EAAE,CAAC,IAAI,EAAE,OAAO,KAAK,IAAI,CAAC;IACvC,mGAAmG;IACnG,SAAS,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,KAAK,IAAI,CAAC;IAC/D,cAAc,EAAE,MAAM,IAAI,CAAC;IAE3B,yDAAyD;IACzD,KAAK,EAAE,eAAe,GAAG,IAAI,CAAC;IAC9B,YAAY,EAAE,CAAC,EAAE,EAAE,eAAe,KAAK,IAAI,CAAC;IAC5C,6FAA6F;IAC7F,aAAa,EAAE,CAAC,GAAG,EAAE,SAAS,MAAM,EAAE,EAAE,EAAE,CAAC,EAAE,eAAe,KAAK,IAAI,CAAC;IACtE,UAAU,EAAE,MAAM,IAAI,CAAC;IACvB;;;OAGG;IACH,aAAa,EAAE,CAAC,KAAK,EAAE;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAC;QAAC,cAAc,EAAE,MAAM,IAAI,CAAA;KAAE,KAAK,IAAI,CAAC;CAClG;AAED,MAAM,WAAW,oBAAoB;IACnC;;;;OAIG;IACH,GAAG,EAAE,MAAM,CAAC;CACb;AAED,wBAAgB,aAAa,CAAC,EAAE,GAAG,EAAE,EAAE,oBAAoB,GAAG,UAAU,CA2DvE"}
@@ -0,0 +1,79 @@
1
+ /**
2
+ * `useFolderView` — hidden folders and folder tags for one {@link FileTree}, remembered.
3
+ *
4
+ * const folderView = useFolderView({ key: 'collections:folder-marks' });
5
+ * <FileTree folderView={folderView} … />
6
+ * <header onContextMenu={folderView.onContextMenu}>Files</header>
7
+ *
8
+ * The arithmetic is `folderView.ts`; this is where the two halves are KEPT, and the reason it
9
+ * is a hook rather than two more controlled props on the tree:
10
+ *
11
+ * · the MARKS (hidden, tags) are stored under `key` and announced on the preference bus, the
12
+ * seam every synced setting in this package uses (`preferenceSync.ts`). An app that keeps
13
+ * preferences on its server lists `key` once and the marks follow the person to every
14
+ * device — *"The general hiding is persisted for all machines."* An app that does not still
15
+ * remembers them in this browser, so the feature works before anyone wires a server.
16
+ * · the VIEW (show hidden, the tag filter) is stored under `${key}:device` and is NEVER
17
+ * announced — *"Hiding or showing by tag can be stored per machine, so viewing the 'home'
18
+ * tag at home doesn't change my work computer."*
19
+ *
20
+ * And because the tree's header belongs to the APP (collections' `FilterRail` section, desk's
21
+ * explorer bar), the view menu has to be openable from outside the tree: the hook holds which
22
+ * menu is open, and the tree draws it. `onContextMenu` is the one handler an app puts on its
23
+ * header so right-click and ⌃-click there open the same menu as the tree's blank space.
24
+ */
25
+ import { useCallback, useMemo, useState } from 'react';
26
+ import { useRememberedPreference } from '../media-gallery/rememberedPreference.js';
27
+ import { normalizeTag, parseFolderMarks, parseFolderViewLocal, serializeFolderMarks, setFolderTags, setHidden, setTagFilter, tagCounts, toggleFolderTag, } from './folderView.js';
28
+ export function useFolderView({ key }) {
29
+ const [rawMarks, writeMarks] = useRememberedPreference(key);
30
+ const [rawLocal, writeLocal] = useRememberedPreference(`${key}:device`, { announce: false });
31
+ const marks = useMemo(() => parseFolderMarks(rawMarks), [rawMarks]);
32
+ const local = useMemo(() => parseFolderViewLocal(rawLocal), [rawLocal]);
33
+ const tags = useMemo(() => tagCounts(marks), [marks]);
34
+ const [panel, setPanel] = useState(null);
35
+ const saveMarks = useCallback((next) => {
36
+ if (next !== marks)
37
+ writeMarks(serializeFolderMarks(next));
38
+ }, [marks, writeMarks]);
39
+ const saveLocal = useCallback((next) => {
40
+ const empty = !next.showHidden && next.only.length === 0 && next.hide.length === 0;
41
+ writeLocal(empty ? null : JSON.stringify(next));
42
+ }, [writeLocal]);
43
+ const onContextMenu = useCallback((event) => {
44
+ event.preventDefault();
45
+ setPanel({ kind: 'view', at: { x: event.clientX, y: event.clientY } });
46
+ }, []);
47
+ return useMemo(() => ({
48
+ marks,
49
+ local,
50
+ tags,
51
+ hide: (ids) => saveMarks(setHidden(marks, ids, true)),
52
+ unhide: (ids) => saveMarks(setHidden(marks, ids, false)),
53
+ toggleTag: (ids, raw) => {
54
+ const tag = normalizeTag(raw);
55
+ if (!tag || ids.length === 0)
56
+ return;
57
+ // Several folders take the tag together: if every one already has it, it comes off
58
+ // all of them; otherwise it goes on the ones missing it — Finder's mixed-state rule.
59
+ const all = ids.every((id) => (marks.tags[id] ?? []).includes(tag));
60
+ let next = marks;
61
+ for (const id of ids) {
62
+ const has = (next.tags[id] ?? []).includes(tag);
63
+ if (all || !has)
64
+ next = toggleFolderTag(next, id, tag);
65
+ }
66
+ saveMarks(next);
67
+ },
68
+ setTags: (id, list) => saveMarks(setFolderTags(marks, id, list)),
69
+ setShowHidden: (show) => saveLocal({ ...local, showHidden: show }),
70
+ filterTag: (tag, mode) => saveLocal(setTagFilter(local, tag, mode)),
71
+ clearTagFilter: () => saveLocal({ ...local, only: [], hide: [] }),
72
+ panel,
73
+ openViewMenu: (at) => setPanel({ kind: 'view', at }),
74
+ openTagEditor: (ids, at) => setPanel({ kind: 'tags', at: at ?? null, ids }),
75
+ closePanel: () => setPanel(null),
76
+ onContextMenu,
77
+ }), [marks, local, tags, panel, saveMarks, saveLocal, onContextMenu]);
78
+ }
79
+ //# sourceMappingURL=useFolderView.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useFolderView.js","sourceRoot":"","sources":["../../../src/react/file-tree/useFolderView.ts"],"names":[],"mappings":"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"}
@@ -1,4 +1,5 @@
1
1
  import type { CSSProperties, DragEvent, ReactNode } from 'react';
2
+ import { type HeaderContextMenuHandler } from './headerContextMenu.js';
2
3
  export interface FilterRailItem {
3
4
  /** Stable key, and the value handed to `onSelect`. */
4
5
  id: string;
@@ -115,6 +116,12 @@ export interface FilterRailSection {
115
116
  children?: ReactNode;
116
117
  /** A heading-level control, right-aligned (a "+" that opens a create dialog). */
117
118
  action?: ReactNode;
119
+ /**
120
+ * Right-click (and ⌃-click) on the section's heading — for a section holding a file tree,
121
+ * the tree's view menu: `useFolderView().onContextMenu`. Owner, 2026-09-30: *"a new menu that
122
+ * appears when I right click (or control + click) the top bar of the file tree"*.
123
+ */
124
+ onHeaderContextMenu?: HeaderContextMenuHandler;
118
125
  /**
119
126
  * A selection axis of this section's own, overriding {@link FilterRailProps.selected}.
120
127
  *