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,402 @@
1
+ /**
2
+ * The panels a right-click opens on the file tree — the row menu, the view menu with its tag
3
+ * filter, and the tag editor. Loaded ON DEMAND (`FileTree.tsx` → `TreePanel`, a `lazyPart`)
4
+ * and preloaded when the browser is idle, so the first right-click draws at once and the first
5
+ * paint does not pay for it. See `FolderViewMenus.tsx` for what was copied from whom.
6
+ */
7
+ import type { ReactNode } from 'react';
8
+ import { useId, useMemo, useRef, useState } from 'react';
9
+ import { Input } from '../components/Input.js';
10
+ import { Menu, MenuItem, MenuLabel, MenuSeparator } from '../components/Menu.js';
11
+ import { Popover } from '../components/Popover.js';
12
+ import { ScrollArea } from '../components/ScrollArea.js';
13
+ import { Tooltip } from '../components/Tooltip.js';
14
+ import { UnstyledButton } from '../components/UnstyledButton.js';
15
+ import { normalizeTag, tagFilterActive } from './folderView.js';
16
+ import { type FileTreeMenuItem, TagDot } from './FolderViewMenus.js';
17
+ import type { FolderView, FolderViewPoint } from './useFolderView.js';
18
+ import './folderViewPanels.css';
19
+
20
+ /** Which panel to draw — the one component the lazy chunk exports. */
21
+ export type TreePanelProps =
22
+ | {
23
+ kind: 'row';
24
+ at: FolderViewPoint;
25
+ label: string;
26
+ items: readonly FileTreeMenuItem[];
27
+ onClose: () => void;
28
+ }
29
+ | {
30
+ kind: 'view';
31
+ at: FolderViewPoint;
32
+ view: FolderView | null;
33
+ hiddenCount: number;
34
+ extraItems: readonly FileTreeMenuItem[];
35
+ onCollapseAll: (() => void) | null;
36
+ onClose: () => void;
37
+ }
38
+ | {
39
+ kind: 'tags';
40
+ at: FolderViewPoint;
41
+ view: FolderView;
42
+ ids: readonly string[];
43
+ names: readonly string[];
44
+ onClose: () => void;
45
+ };
46
+
47
+ export function TreePanel(props: TreePanelProps): ReactNode {
48
+ if (props.kind === 'row')
49
+ return (
50
+ <TreeMenuPanel at={props.at} label={props.label} onClose={props.onClose}>
51
+ <Menu aria-label={props.label}>
52
+ <TreeMenuItems items={props.items} onDone={props.onClose} />
53
+ </Menu>
54
+ </TreeMenuPanel>
55
+ );
56
+ if (props.kind === 'view')
57
+ return (
58
+ <FolderViewMenu
59
+ view={props.view}
60
+ at={props.at}
61
+ hiddenCount={props.hiddenCount}
62
+ extraItems={props.extraItems}
63
+ onCollapseAll={props.onCollapseAll}
64
+ onClose={props.onClose}
65
+ />
66
+ );
67
+ return (
68
+ <FolderTagEditor view={props.view} ids={props.ids} names={props.names} at={props.at} onClose={props.onClose} />
69
+ );
70
+ }
71
+
72
+ /** The menu items in a tree menu — refused items stay reachable, with their reason. */
73
+ export function TreeMenuItems({
74
+ items,
75
+ onDone,
76
+ }: {
77
+ items: readonly FileTreeMenuItem[];
78
+ onDone: () => void;
79
+ }): ReactNode {
80
+ return items.map((item, index) => {
81
+ if (item.divider) {
82
+ // A divider at either end, or after another, is a line under nothing.
83
+ const prev = items[index - 1];
84
+ const next = items[index + 1];
85
+ if (!prev || !next || prev.divider || next.divider) return null;
86
+ return <MenuSeparator key={item.key} />;
87
+ }
88
+ const hint = typeof item.description === 'string' ? item.description : undefined;
89
+ return (
90
+ <MenuItem
91
+ key={item.key}
92
+ {...(item.checked !== undefined ? { selected: item.checked } : {})}
93
+ {...(item.destructive ? { danger: true } : {})}
94
+ {...(item.shortcut ? { shortcut: item.shortcut } : {})}
95
+ {...(hint ? { hint } : {})}
96
+ {...(item.disabled
97
+ ? item.title
98
+ ? { unavailableReason: item.title }
99
+ : { disabled: true }
100
+ : item.title
101
+ ? { title: item.title }
102
+ : {})}
103
+ onSelect={() => {
104
+ // Shut first, then act: an item that opens ANOTHER panel (Tags…) must not have it
105
+ // shut again by this menu's own close.
106
+ onDone();
107
+ item.onClick?.();
108
+ }}
109
+ >
110
+ {item.label}
111
+ </MenuItem>
112
+ );
113
+ });
114
+ }
115
+
116
+ /** The panel every tree menu sits in — a point-anchored popover with a menu's padding. */
117
+ export function TreeMenuPanel({
118
+ at,
119
+ label,
120
+ onClose,
121
+ children,
122
+ width,
123
+ }: {
124
+ at: FolderViewPoint;
125
+ label: string;
126
+ onClose: () => void;
127
+ children: ReactNode;
128
+ width?: string;
129
+ }): ReactNode {
130
+ // A fresh object per open, so reopening at the same point still re-anchors.
131
+ const anchor = useMemo(() => ({ x: at.x, y: at.y }), [at.x, at.y]);
132
+ return (
133
+ <Popover
134
+ anchor={anchor}
135
+ onOpenChange={(open) => {
136
+ if (!open) onClose();
137
+ }}
138
+ label={label}
139
+ arrow={false}
140
+ side='bottom'
141
+ align='start'
142
+ sideOffset={2}
143
+ {...(width ? { width } : {})}
144
+ className='cbft-menu-panel p-1'
145
+ >
146
+ {children}
147
+ </Popover>
148
+ );
149
+ }
150
+
151
+ /**
152
+ * The view menu — right-click on the blank space under the rows, or on the app's header.
153
+ *
154
+ * Two parts: COMMANDS (a menu — they act and close) and the TAG FILTER (toggles that stay
155
+ * open, because narrowing to "home and code" is two presses and a menu that shut after the
156
+ * first would make it two right-clicks).
157
+ */
158
+ export function FolderViewMenu({
159
+ view,
160
+ at,
161
+ hiddenCount,
162
+ extraItems,
163
+ onCollapseAll,
164
+ onClose,
165
+ }: {
166
+ view: FolderView | null;
167
+ at: FolderViewPoint;
168
+ hiddenCount: number;
169
+ extraItems: readonly FileTreeMenuItem[];
170
+ onCollapseAll: (() => void) | null;
171
+ onClose: () => void;
172
+ }): ReactNode {
173
+ const commands: FileTreeMenuItem[] = [...extraItems];
174
+ if (view) {
175
+ if (commands.length > 0) commands.push({ key: 'fv-sep-app', divider: true });
176
+ commands.push({
177
+ key: 'fv-show-hidden',
178
+ label: hiddenCount > 0 ? `Show hidden folders (${hiddenCount})` : 'Show hidden folders',
179
+ checked: view.local.showHidden,
180
+ shortcut: '⇧⌘.',
181
+ title: view.local.showHidden
182
+ ? 'Leave hidden folders out again. On this device only.'
183
+ : 'Draw hidden folders too, faded and in italics, so you can unhide them. On this device only.',
184
+ onClick: () => view.setShowHidden(!view.local.showHidden),
185
+ });
186
+ }
187
+ if (onCollapseAll)
188
+ commands.push({ key: 'fv-collapse', label: 'Collapse all folders', onClick: onCollapseAll });
189
+
190
+ return (
191
+ <TreeMenuPanel at={at} label='Tree view' onClose={onClose} width='17rem'>
192
+ <Menu aria-label='Tree view'>
193
+ <TreeMenuItems items={commands} onDone={onClose} />
194
+ </Menu>
195
+ {view ? <TagFilter view={view} /> : null}
196
+ </TreeMenuPanel>
197
+ );
198
+ }
199
+
200
+ /** "Filter by tag" — one row per tag, each with an Only and a Hide toggle. */
201
+ function TagFilter({ view }: { view: FolderView }): ReactNode {
202
+ const { local } = view;
203
+ // A tag still in this device's filter but carried by no folder any more must stay listed:
204
+ // it is narrowing the tree, and a filter you cannot see is one you cannot take off.
205
+ const rows = useMemo(() => {
206
+ const list = view.tags.map((t) => ({ ...t }));
207
+ for (const tag of [...local.only, ...local.hide])
208
+ if (!list.some((t) => t.tag === tag)) list.push({ tag, count: 0 });
209
+ return list;
210
+ }, [view.tags, local.only, local.hide]);
211
+ const headId = useId();
212
+
213
+ return (
214
+ <section className='cbft-fv' aria-labelledby={headId}>
215
+ <MenuSeparator />
216
+ <div className='cbft-fv-head'>
217
+ <MenuLabel className='cbft-fv-title'>
218
+ <span id={headId}>Filter by tag</span>
219
+ </MenuLabel>
220
+ <Tooltip content='The tag filter is remembered on this device only — your other computers keep their own.'>
221
+ <span className='cbft-fv-scope'>This device</span>
222
+ </Tooltip>
223
+ </div>
224
+ {rows.length === 0 ? (
225
+ <p className='cbft-fv-empty'>
226
+ No tags yet. Right-click a folder and choose <b>Tags…</b> to add one.
227
+ </p>
228
+ ) : (
229
+ <ScrollArea className='cbft-fv-scroll' space='none' safeArea={false} fade>
230
+ <ul className='cbft-fv-list'>
231
+ {rows.map(({ tag, count }) => {
232
+ const mode = local.only.includes(tag) ? 'only' : local.hide.includes(tag) ? 'hide' : null;
233
+ return (
234
+ <li key={tag} className='cbft-fv-row' data-mode={mode ?? undefined}>
235
+ <TagDot tag={tag} />
236
+ <span className='cbft-fv-name'>{tag}</span>
237
+ <span className='cbft-fv-count'>{count}</span>
238
+ {/* A 2-segment toggle in a 28px menu row, styled by `.cbft-fv-seg` — the tree
239
+ ships plain CSS (see fileTree.css's header), so the button is the belt's
240
+ UNSTYLED one: its behaviour and hint, none of a 36px control's look. */}
241
+ <fieldset className='cbft-fv-seg' aria-label={`Filter by ${tag}`}>
242
+ <UnstyledButton
243
+ tooltip={`Show only folders tagged ${tag} (and those with any other “Only” tag)`}
244
+ aria-pressed={mode === 'only'}
245
+ onClick={() => view.filterTag(tag, mode === 'only' ? null : 'only')}
246
+ >
247
+ Only
248
+ </UnstyledButton>
249
+ <UnstyledButton
250
+ tooltip={`Hide folders tagged ${tag}`}
251
+ aria-pressed={mode === 'hide'}
252
+ onClick={() => view.filterTag(tag, mode === 'hide' ? null : 'hide')}
253
+ >
254
+ Hide
255
+ </UnstyledButton>
256
+ </fieldset>
257
+ </li>
258
+ );
259
+ })}
260
+ </ul>
261
+ </ScrollArea>
262
+ )}
263
+ {tagFilterActive(local) ? (
264
+ <UnstyledButton className='cbft-fv-clear' onClick={() => view.clearTagFilter()}>
265
+ Clear tag filter
266
+ </UnstyledButton>
267
+ ) : null}
268
+ </section>
269
+ );
270
+ }
271
+
272
+ /**
273
+ * The tag editor — Gmail's *Label as*: type to find a tag, or to make one.
274
+ *
275
+ * ↑/↓ move through the list, Enter ticks or unticks the highlighted tag (or creates the typed
276
+ * one), Escape closes. A tag ticked here is on the folder at once — there is no Save, because
277
+ * every change is one small reversible write.
278
+ */
279
+ export function FolderTagEditor({
280
+ view,
281
+ ids,
282
+ names,
283
+ at,
284
+ onClose,
285
+ }: {
286
+ view: FolderView;
287
+ ids: readonly string[];
288
+ /** The folders' names, for the heading. */
289
+ names: readonly string[];
290
+ at: FolderViewPoint;
291
+ onClose: () => void;
292
+ }): ReactNode {
293
+ const [query, setQuery] = useState('');
294
+ const [active, setActive] = useState(0);
295
+ const listId = useId();
296
+ const inputRef = useRef<HTMLInputElement>(null);
297
+
298
+ const typed = normalizeTag(query);
299
+ const options = useMemo(() => {
300
+ const all = view.tags.map((t) => t.tag);
301
+ const shown = typed ? all.filter((tag) => tag.includes(typed)) : all;
302
+ const create = typed && !all.includes(typed) ? [{ tag: typed, create: true }] : [];
303
+ return [...shown.map((tag) => ({ tag, create: false })), ...create];
304
+ }, [view.tags, typed]);
305
+ const at0 = Math.min(active, Math.max(0, options.length - 1));
306
+
307
+ const stateOf = (tag: string): 'all' | 'some' | 'none' => {
308
+ const have = ids.filter((id) => (view.marks.tags[id] ?? []).includes(tag)).length;
309
+ return have === 0 ? 'none' : have === ids.length ? 'all' : 'some';
310
+ };
311
+
312
+ const pick = (tag: string, create: boolean) => {
313
+ view.toggleTag(ids, tag);
314
+ if (create) setQuery('');
315
+ inputRef.current?.focus();
316
+ };
317
+
318
+ const heading = names.length === 1 ? `Tags for “${names[0]}”` : `Tags for ${names.length} folders`;
319
+
320
+ return (
321
+ <TreeMenuPanel at={at} label={heading} onClose={onClose} width='16rem'>
322
+ <div className='cbft-te'>
323
+ <div className='cbft-te-head'>{heading}</div>
324
+ <Input
325
+ ref={inputRef}
326
+ size='sm'
327
+ className='cbft-te-input'
328
+ autoFocus
329
+ value={query}
330
+ placeholder='Find or create a tag'
331
+ aria-label='Find or create a tag'
332
+ role='combobox'
333
+ aria-expanded='true'
334
+ aria-controls={listId}
335
+ aria-activedescendant={options[at0] ? `${listId}-${at0}` : undefined}
336
+ maxLength={64}
337
+ onChange={(event) => {
338
+ setQuery(event.currentTarget.value);
339
+ setActive(0);
340
+ }}
341
+ onKeyDown={(event) => {
342
+ if (event.key === 'ArrowDown') {
343
+ event.preventDefault();
344
+ setActive((i) => Math.min(options.length - 1, i + 1));
345
+ } else if (event.key === 'ArrowUp') {
346
+ event.preventDefault();
347
+ setActive((i) => Math.max(0, i - 1));
348
+ } else if (event.key === 'Enter') {
349
+ event.preventDefault();
350
+ const option = options[at0];
351
+ if (option) pick(option.tag, option.create);
352
+ }
353
+ }}
354
+ />
355
+ {options.length === 0 ? (
356
+ <p className='cbft-fv-empty'>Type a word to make your first tag — “home”, “work”.</p>
357
+ ) : (
358
+ <ScrollArea className='cbft-te-scroll' space='none' safeArea={false} fade>
359
+ <div id={listId} role='listbox' aria-multiselectable='true' aria-label='Tags'>
360
+ {options.map((option, index) => {
361
+ const state = option.create ? 'none' : stateOf(option.tag);
362
+ return (
363
+ // biome-ignore lint/a11y/useKeyWithClickEvents: the keyboard drives this list through the field above (aria-activedescendant), the combobox pattern.
364
+ <div
365
+ key={`${option.create ? '+' : ''}${option.tag}`}
366
+ id={`${listId}-${index}`}
367
+ role='option'
368
+ // Reachable through the field's aria-activedescendant, never by Tab.
369
+ tabIndex={-1}
370
+ aria-selected={state === 'all'}
371
+ data-active={index === at0 || undefined}
372
+ data-state={state}
373
+ className='cbft-te-option'
374
+ onMouseEnter={() => setActive(index)}
375
+ onMouseDown={(event) => event.preventDefault()}
376
+ onClick={() => pick(option.tag, option.create)}
377
+ >
378
+ <span className='cbft-te-check' aria-hidden='true'>
379
+ {state === 'all' ? '✓' : state === 'some' ? '–' : ''}
380
+ </span>
381
+ <TagDot tag={option.tag} />
382
+ <span className='cbft-fv-name'>
383
+ {option.create ? (
384
+ <>
385
+ Create <b>“{option.tag}”</b>
386
+ </>
387
+ ) : (
388
+ option.tag
389
+ )}
390
+ </span>
391
+ </div>
392
+ );
393
+ })}
394
+ </div>
395
+ </ScrollArea>
396
+ )}
397
+ <p className='cbft-te-foot'>Tags follow the folder to every device.</p>
398
+ </div>
399
+ </TreeMenuPanel>
400
+ );
401
+ }
402
+
@@ -953,3 +953,149 @@
953
953
  white-space: nowrap;
954
954
  border: 0;
955
955
  }
956
+
957
+ /* ── Hidden folders and folder tags ─────────────────────────────────────────────────────
958
+ *
959
+ * Owner, 2026-09-30: *"Showing hidden folders still keeps them hideable and they should be
960
+ * slightly faded or italics or something like that to let me see which ones are hideable when
961
+ * all are showing. The indication should be subtle but easy for me to notice."*
962
+ *
963
+ * So a hidden folder drawn by "Show hidden folders" is FADED, ITALIC and carries a small
964
+ * eye-off; what is inside it is faded only. Faded is what says "not normally here" across the
965
+ * whole list; the eye-off is what keeps it from reading as a PRIVATE row, which is italic too
966
+ * (`data-access` above) and carries a padlock instead. The row stays a full-strength target —
967
+ * only its ink is quieter — because the point of drawing it is to act on it.
968
+ */
969
+ .cbft-row[data-hidden] .cbft-name,
970
+ .cbft-row[data-hidden] .cbft-slot,
971
+ .cbft-row[data-hidden] .cbft-twisty {
972
+ opacity: 0.5;
973
+ }
974
+
975
+ .cbft-row[data-hidden='self'] .cbft-name {
976
+ font-style: italic;
977
+ }
978
+
979
+ /* A selected or focused hidden row is the one being acted on; let it read at nearly full ink. */
980
+ .cbft-row[data-hidden][data-selected] .cbft-name,
981
+ .cbft-row[data-hidden]:focus-visible .cbft-name {
982
+ opacity: 0.8;
983
+ }
984
+
985
+ .cbft-hidden-mark {
986
+ flex: none;
987
+ display: inline-flex;
988
+ color: var(--muted-foreground);
989
+ opacity: 0.7;
990
+ pointer-events: none;
991
+ }
992
+
993
+ /* The row whose right-click menu is open — Finder's ring, so the menu says what it is for. */
994
+ .cbft-row[data-menu-open] {
995
+ outline: 1px solid var(--ring, var(--accent-solid));
996
+ outline-offset: -1px;
997
+ }
998
+
999
+ /*
1000
+ * Tag dots — Finder's: a few coloured dots after the name, one per tag, the rest in the
1001
+ * hover. The colour is the tag's NAME hashed onto the theme's chart palette (`tagHue`), so the
1002
+ * same tag is the same colour everywhere without anyone choosing or storing a colour.
1003
+ */
1004
+ .cbft-tagdots {
1005
+ flex: none;
1006
+ display: inline-flex;
1007
+ align-items: center;
1008
+ gap: 2px;
1009
+ padding-inline: 2px;
1010
+ }
1011
+
1012
+ .cbft-tagdot {
1013
+ flex: none;
1014
+ display: inline-block;
1015
+ inline-size: 7px;
1016
+ block-size: 7px;
1017
+ border-radius: 999px;
1018
+ background: var(--cbft-tag, var(--muted-foreground));
1019
+ }
1020
+
1021
+ .cbft-tagdot[data-hue='0'] { --cbft-tag: var(--chart-1); }
1022
+ .cbft-tagdot[data-hue='1'] { --cbft-tag: var(--chart-2); }
1023
+ .cbft-tagdot[data-hue='2'] { --cbft-tag: var(--chart-3); }
1024
+ .cbft-tagdot[data-hue='3'] { --cbft-tag: var(--chart-4); }
1025
+ .cbft-tagdot[data-hue='4'] { --cbft-tag: var(--chart-5); }
1026
+ .cbft-tagdot[data-hue='5'] { --cbft-tag: var(--chart-6); }
1027
+ .cbft-tagdot[data-hue='6'] { --cbft-tag: var(--chart-7); }
1028
+ .cbft-tagdot[data-hue='7'] { --cbft-tag: var(--chart-8); }
1029
+
1030
+ /*
1031
+ * The strip — what this device is narrowing the tree by, and one press back to everything.
1032
+ * One tree row tall and as quiet as the pinned heading, because it is a reminder rather than
1033
+ * a control panel; the menu behind it is the control panel.
1034
+ */
1035
+ .cbft-fv-strip {
1036
+ display: flex;
1037
+ align-items: center;
1038
+ gap: 0.25rem;
1039
+ flex: none;
1040
+ min-block-size: var(--cbft-row);
1041
+ padding: 0.125rem calc(var(--cbft-inset) - 2px) 0.125rem var(--cbft-inset);
1042
+ margin: 0 0 0.125rem;
1043
+ border-block-end: 1px solid color-mix(in srgb, var(--border) 70%, transparent);
1044
+ font-size: 0.75rem;
1045
+ color: var(--muted-foreground);
1046
+ }
1047
+
1048
+ .cbft-fv-strip-open,
1049
+ .cbft-fv-strip-clear {
1050
+ border: 0;
1051
+ background: transparent;
1052
+ color: inherit;
1053
+ font: inherit;
1054
+ cursor: pointer;
1055
+ border-radius: var(--radius-sm, 0.375rem);
1056
+ }
1057
+
1058
+ .cbft-fv-strip-open {
1059
+ display: flex;
1060
+ flex: 1 1 auto;
1061
+ min-inline-size: 0;
1062
+ align-items: center;
1063
+ gap: 0.5rem;
1064
+ padding: 0.125rem 0.25rem;
1065
+ margin-inline-start: -0.25rem;
1066
+ overflow: hidden;
1067
+ white-space: nowrap;
1068
+ text-align: start;
1069
+ }
1070
+
1071
+ .cbft-fv-strip-clear {
1072
+ flex: none;
1073
+ padding: 0.125rem 0.375rem;
1074
+ color: var(--accent);
1075
+ font-weight: 500;
1076
+ }
1077
+
1078
+ .cbft-fv-strip-open:hover,
1079
+ .cbft-fv-strip-clear:hover {
1080
+ background: color-mix(in srgb, var(--muted-foreground) 12%, transparent);
1081
+ }
1082
+
1083
+ .cbft-fv-strip-open:focus-visible,
1084
+ .cbft-fv-strip-clear:focus-visible {
1085
+ outline: 2px solid var(--ring, var(--accent-solid));
1086
+ outline-offset: -2px;
1087
+ }
1088
+
1089
+ .cbft-fv-part {
1090
+ display: inline-flex;
1091
+ align-items: center;
1092
+ gap: 0.25rem;
1093
+ min-inline-size: 0;
1094
+ }
1095
+
1096
+ .cbft-fv-chip {
1097
+ display: inline-flex;
1098
+ align-items: center;
1099
+ gap: 0.25rem;
1100
+ color: var(--cbft-fg, var(--foreground));
1101
+ }