@geminilight/mindos 0.6.15 → 0.6.17

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.
@@ -26,6 +26,7 @@
26
26
  --color-input: var(--input);
27
27
  --color-border: var(--border);
28
28
  --color-destructive: var(--destructive);
29
+ --color-destructive-foreground: var(--destructive-foreground);
29
30
  --color-success: var(--success);
30
31
  --color-error: var(--error);
31
32
  --color-amber-foreground: var(--amber-foreground);
@@ -69,7 +70,8 @@ body {
69
70
  --muted-foreground: #685f52;
70
71
  --accent: #d9d3c6;
71
72
  --accent-foreground: #1c1a17;
72
- --destructive: oklch(0.58 0.22 27);
73
+ --destructive: oklch(0.56 0.14 24);
74
+ --destructive-foreground: #ffffff;
73
75
  --border: rgba(28, 26, 23, 0.1);
74
76
  --input: rgba(28, 26, 23, 0.12);
75
77
  --ring: var(--amber);
@@ -78,7 +80,7 @@ body {
78
80
  --amber-text: #9a6a2b;
79
81
  --amber-dim: rgba(200, 135, 58, 0.18);
80
82
  --amber-subtle: rgba(200, 135, 30, 0.08);
81
- --amber-foreground: #131210;
83
+ --amber-foreground: #ffffff;
82
84
  --success: #7aad80;
83
85
  --error: #c85050;
84
86
  --sidebar: #ede9e1;
@@ -106,7 +108,8 @@ body {
106
108
  --muted-foreground: #8a8275;
107
109
  --accent: #2e2b22;
108
110
  --accent-foreground: #e8e4dc;
109
- --destructive: oklch(0.704 0.191 22.216);
111
+ --destructive: oklch(0.56 0.14 22);
112
+ --destructive-foreground: #ffffff;
110
113
  --border: rgba(232, 228, 220, 0.08);
111
114
  --input: rgba(232, 228, 220, 0.1);
112
115
  --ring: var(--amber);
@@ -114,7 +117,7 @@ body {
114
117
  --amber-text: #e0a85e;
115
118
  --amber-dim: rgba(212, 149, 74, 0.20);
116
119
  --amber-subtle: rgba(212, 149, 74, 0.10);
117
- --amber-foreground: #131210;
120
+ --amber-foreground: #ffffff;
118
121
  --success: #7aad80;
119
122
  --error: #c85050;
120
123
  --sidebar: #1c1a17;
@@ -434,6 +437,53 @@ a:focus-visible,
434
437
  outline: none;
435
438
  }
436
439
 
440
+ /* ── Styled form controls ──────────────────────────────────── */
441
+
442
+ input[type="checkbox"].form-check {
443
+ appearance: none;
444
+ width: 14px; height: 14px;
445
+ flex-shrink: 0;
446
+ border-radius: 3px;
447
+ border: 1px solid var(--border);
448
+ background: var(--background);
449
+ cursor: pointer;
450
+ transition: background-color 0.15s, border-color 0.15s;
451
+ background-repeat: no-repeat;
452
+ background-position: center;
453
+ background-size: 100% 100%;
454
+ }
455
+ input[type="checkbox"].form-check:checked {
456
+ background-color: var(--amber);
457
+ border-color: var(--amber);
458
+ background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 16 16' fill='white' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M12.207 4.793a1 1 0 010 1.414l-5 5a1 1 0 01-1.414 0l-2-2a1 1 0 011.414-1.414L6.5 9.086l4.293-4.293a1 1 0 011.414 0z'/%3E%3C/svg%3E");
459
+ }
460
+ input[type="checkbox"].form-check:focus-visible {
461
+ outline: none;
462
+ box-shadow: 0 0 0 2px var(--ring);
463
+ }
464
+ input[type="checkbox"].form-check:disabled {
465
+ opacity: 0.5;
466
+ cursor: not-allowed;
467
+ }
468
+
469
+ input[type="radio"].form-radio {
470
+ appearance: none;
471
+ width: 14px; height: 14px;
472
+ flex-shrink: 0;
473
+ border-radius: 9999px;
474
+ border: 1px solid var(--border);
475
+ background: var(--background);
476
+ cursor: pointer;
477
+ transition: border-color 0.15s;
478
+ }
479
+ input[type="radio"].form-radio:checked {
480
+ border: 4px solid var(--amber);
481
+ }
482
+ input[type="radio"].form-radio:focus-visible {
483
+ outline: none;
484
+ box-shadow: 0 0 0 2px var(--ring);
485
+ }
486
+
437
487
  /* macOS Electron: traffic-light safe zone. Actual CSS is injected by Electron
438
488
  main process via webContents.insertCSS(); this is a no-op fallback. */
439
489
  .electron-mac-titlebar-pad { display: none; }
package/app/app/page.tsx CHANGED
@@ -34,20 +34,6 @@ function extractDescription(spacePath: string): string {
34
34
  return '';
35
35
  }
36
36
 
37
- /** Recursively collect all directory paths from the file tree */
38
- function collectDirPaths(nodes: FileNode[], prefix = ''): string[] {
39
- const result: string[] = [];
40
- for (const n of nodes) {
41
- if (n.type === 'directory' && !n.name.startsWith('.')) {
42
- const path = prefix ? `${prefix}/${n.name}` : n.name;
43
- result.push(path);
44
- if (n.children) {
45
- result.push(...collectDirPaths(n.children, path));
46
- }
47
- }
48
- }
49
- return result;
50
- }
51
37
 
52
38
  function getTopLevelDirs(): SpaceInfo[] {
53
39
  try {
@@ -90,9 +76,5 @@ export default function HomePage() {
90
76
 
91
77
  const spaces = getTopLevelDirs();
92
78
 
93
- // Collect all directory paths for hierarchical space creation
94
- let dirPaths: string[] = [];
95
- try { dirPaths = collectDirPaths(getFileTree()); } catch { /* ignore */ }
96
-
97
- return <HomeContent recent={recent} existingFiles={existingFiles} spaces={spaces} dirPaths={dirPaths} />;
79
+ return <HomeContent recent={recent} existingFiles={existingFiles} spaces={spaces} />;
98
80
  }
@@ -168,7 +168,7 @@ export default function CreateSpaceModal({ t, dirPaths }: { t: ReturnType<typeof
168
168
  >
169
169
  {/* Header */}
170
170
  <div className="flex items-center justify-between px-5 pt-5 pb-3">
171
- <h3 className="text-sm font-semibold font-display text-foreground">{h.newSpace}</h3>
171
+ <h3 className="text-base font-semibold font-display text-foreground">{h.newSpace}</h3>
172
172
  <button onClick={close} className="p-1 rounded-md text-muted-foreground hover:bg-muted transition-colors">
173
173
  <X size={14} />
174
174
  </button>
@@ -214,7 +214,7 @@ export default function CreateSpaceModal({ t, dirPaths }: { t: ReturnType<typeof
214
214
  onChange={e => setDescription(e.target.value)}
215
215
  placeholder={h.spaceDescPlaceholder ?? 'Describe the purpose of this space'}
216
216
  maxLength={200}
217
- className="w-full px-3 py-2 text-sm rounded-lg border border-border bg-background text-muted-foreground outline-none focus-visible:ring-1 focus-visible:ring-ring transition-colors"
217
+ className="w-full px-3 py-2 text-sm rounded-lg border border-border bg-background text-foreground placeholder:text-muted-foreground outline-none focus-visible:ring-1 focus-visible:ring-ring transition-colors"
218
218
  />
219
219
  </div>
220
220
  {/* AI initialization toggle */}
@@ -268,7 +268,7 @@ export default function CreateSpaceModal({ t, dirPaths }: { t: ReturnType<typeof
268
268
  <button
269
269
  onClick={handleCreate}
270
270
  disabled={!name.trim() || loading || !!nameHint}
271
- className="flex items-center gap-1.5 px-4 py-2 rounded-lg text-sm font-medium bg-[var(--amber)] text-white transition-colors hover:opacity-90 disabled:opacity-40 disabled:cursor-not-allowed"
271
+ className="flex items-center gap-1.5 px-4 py-2 rounded-lg text-sm font-medium bg-[var(--amber)] text-[var(--amber-foreground)] transition-colors hover:opacity-90 disabled:opacity-50 disabled:cursor-not-allowed"
272
272
  >
273
273
  {loading && <Loader2 size={14} className="animate-spin" />}
274
274
  {h.createSpace}
@@ -0,0 +1,256 @@
1
+ 'use client';
2
+
3
+ import { useState, useRef, useEffect, useCallback, useMemo } from 'react';
4
+ import { createPortal } from 'react-dom';
5
+ import { ChevronDown, Check } from 'lucide-react';
6
+
7
+ export interface SelectOption {
8
+ value: string;
9
+ label: string;
10
+ icon?: React.ReactNode;
11
+ suffix?: React.ReactNode;
12
+ }
13
+
14
+ export interface SelectOptionGroup {
15
+ label: string;
16
+ options: SelectOption[];
17
+ }
18
+
19
+ export type SelectItem = SelectOption | SelectOptionGroup;
20
+
21
+ function isGroup(item: SelectItem): item is SelectOptionGroup {
22
+ return 'options' in item;
23
+ }
24
+
25
+ function flatOptions(items: SelectItem[]): SelectOption[] {
26
+ const result: SelectOption[] = [];
27
+ for (const item of items) {
28
+ if (isGroup(item)) result.push(...item.options);
29
+ else result.push(item);
30
+ }
31
+ return result;
32
+ }
33
+
34
+ interface CustomSelectProps {
35
+ value: string;
36
+ onChange: (value: string) => void;
37
+ options: SelectItem[];
38
+ className?: string;
39
+ /** 'sm' for inline/compact, 'md' (default) for form fields */
40
+ size?: 'sm' | 'md';
41
+ placeholder?: string;
42
+ }
43
+
44
+ export default function CustomSelect({
45
+ value,
46
+ onChange,
47
+ options,
48
+ className = '',
49
+ size = 'md',
50
+ placeholder,
51
+ }: CustomSelectProps) {
52
+ const [open, setOpen] = useState(false);
53
+ const [highlightIdx, setHighlightIdx] = useState(-1);
54
+ const [panelPos, setPanelPos] = useState<{ top: number; left: number; width: number; flipUp: boolean } | null>(null);
55
+ const btnRef = useRef<HTMLButtonElement>(null);
56
+ const listRef = useRef<HTMLDivElement>(null);
57
+
58
+ const allOptions = useMemo(() => flatOptions(options), [options]);
59
+
60
+ const selectedLabel = useMemo(() => {
61
+ return allOptions.find(o => o.value === value) ?? null;
62
+ }, [allOptions, value]);
63
+
64
+ const close = useCallback(() => {
65
+ setOpen(false);
66
+ setHighlightIdx(-1);
67
+ }, []);
68
+
69
+ const select = useCallback((val: string) => {
70
+ onChange(val);
71
+ close();
72
+ }, [onChange, close]);
73
+
74
+ useEffect(() => {
75
+ if (!open) return;
76
+ const handleClick = (e: MouseEvent) => {
77
+ if (
78
+ btnRef.current && !btnRef.current.contains(e.target as Node) &&
79
+ listRef.current && !listRef.current.contains(e.target as Node)
80
+ ) {
81
+ close();
82
+ }
83
+ };
84
+ document.addEventListener('mousedown', handleClick);
85
+ return () => document.removeEventListener('mousedown', handleClick);
86
+ }, [open, close]);
87
+
88
+ // Keyboard navigation
89
+ useEffect(() => {
90
+ if (!open) return;
91
+ const handleKey = (e: KeyboardEvent) => {
92
+ switch (e.key) {
93
+ case 'Escape':
94
+ e.preventDefault();
95
+ close();
96
+ btnRef.current?.focus();
97
+ break;
98
+ case 'ArrowDown':
99
+ e.preventDefault();
100
+ setHighlightIdx(prev => Math.min(prev + 1, allOptions.length - 1));
101
+ break;
102
+ case 'ArrowUp':
103
+ e.preventDefault();
104
+ setHighlightIdx(prev => Math.max(prev - 1, 0));
105
+ break;
106
+ case 'Enter':
107
+ e.preventDefault();
108
+ if (highlightIdx >= 0 && highlightIdx < allOptions.length) {
109
+ select(allOptions[highlightIdx].value);
110
+ btnRef.current?.focus();
111
+ }
112
+ break;
113
+ }
114
+ };
115
+ document.addEventListener('keydown', handleKey);
116
+ return () => document.removeEventListener('keydown', handleKey);
117
+ }, [open, close, highlightIdx, allOptions, select]);
118
+
119
+ // Scroll highlighted option into view
120
+ useEffect(() => {
121
+ if (!open || !listRef.current || highlightIdx < 0) return;
122
+ const el = listRef.current.querySelector(`[data-idx="${highlightIdx}"]`);
123
+ if (el) el.scrollIntoView({ block: 'nearest' });
124
+ }, [open, highlightIdx]);
125
+
126
+ const calcPosition = useCallback(() => {
127
+ if (!btnRef.current) return;
128
+ const rect = btnRef.current.getBoundingClientRect();
129
+ const maxH = size === 'sm' ? 200 : 260;
130
+ const spaceBelow = window.innerHeight - rect.bottom;
131
+ const spaceAbove = rect.top;
132
+ setPanelPos({
133
+ top: spaceBelow < maxH + 8 && spaceAbove > spaceBelow ? rect.top : rect.bottom,
134
+ left: rect.left,
135
+ width: rect.width,
136
+ flipUp: spaceBelow < maxH + 8 && spaceAbove > spaceBelow,
137
+ });
138
+ }, [size]);
139
+
140
+ // Initialize highlight + position when opening; reposition on scroll/resize
141
+ useEffect(() => {
142
+ if (!open) { setPanelPos(null); return; }
143
+ const idx = allOptions.findIndex(o => o.value === value);
144
+ setHighlightIdx(idx >= 0 ? idx : 0);
145
+ calcPosition();
146
+ window.addEventListener('scroll', calcPosition, true);
147
+ window.addEventListener('resize', calcPosition);
148
+ return () => {
149
+ window.removeEventListener('scroll', calcPosition, true);
150
+ window.removeEventListener('resize', calcPosition);
151
+ };
152
+ }, [open, allOptions, value, calcPosition]);
153
+
154
+ const isSm = size === 'sm';
155
+
156
+ const triggerCls = isSm
157
+ ? `inline-flex items-center gap-1 appearance-none rounded-md border border-border bg-background text-foreground outline-none focus-visible:ring-1 focus-visible:ring-ring transition-colors text-2xs px-1.5 py-0.5 pr-5 ${className}`
158
+ : `w-full flex items-center gap-2 appearance-none rounded-lg border border-border bg-background text-foreground outline-none focus-visible:ring-1 focus-visible:ring-ring transition-colors text-sm px-3 py-2 pr-8 ${className}`;
159
+
160
+ const chevronCls = isSm
161
+ ? 'absolute right-1 top-1/2 -translate-y-1/2 text-muted-foreground pointer-events-none'
162
+ : 'absolute right-2.5 top-1/2 -translate-y-1/2 text-muted-foreground pointer-events-none';
163
+
164
+ const listBaseCls = isSm
165
+ ? 'fixed z-[9999] overflow-y-auto rounded-md border border-border bg-card shadow-lg py-0.5'
166
+ : 'fixed z-[9999] overflow-y-auto rounded-lg border border-border bg-card shadow-lg py-1';
167
+
168
+ const itemBaseCls = isSm
169
+ ? 'w-full flex items-center gap-1.5 px-2 py-1 text-2xs text-left transition-colors cursor-pointer'
170
+ : 'w-full flex items-center gap-2 px-3 py-1.5 text-sm text-left transition-colors cursor-pointer';
171
+
172
+ // Track flat index across groups for keyboard highlight
173
+ let flatIdx = -1;
174
+
175
+ function renderOption(opt: SelectOption) {
176
+ flatIdx++;
177
+ const idx = flatIdx;
178
+ const isSelected = opt.value === value;
179
+ const isHighlighted = idx === highlightIdx;
180
+ return (
181
+ <button
182
+ key={opt.value}
183
+ type="button"
184
+ role="option"
185
+ aria-selected={isSelected}
186
+ data-idx={idx}
187
+ onClick={() => select(opt.value)}
188
+ onMouseEnter={() => setHighlightIdx(idx)}
189
+ className={`${itemBaseCls} ${
190
+ isHighlighted ? 'bg-muted text-foreground' : 'text-foreground'
191
+ }`}
192
+ >
193
+ {opt.icon}
194
+ <span className="flex-1 truncate">{opt.label}</span>
195
+ {opt.suffix}
196
+ {isSelected && <Check size={isSm ? 10 : 12} className="shrink-0 text-[var(--amber)]" />}
197
+ </button>
198
+ );
199
+ }
200
+
201
+ const listPortal = open && panelPos && createPortal(
202
+ <div
203
+ ref={listRef}
204
+ className={listBaseCls}
205
+ role="listbox"
206
+ style={{
207
+ left: panelPos.left,
208
+ minWidth: panelPos.width,
209
+ maxHeight: isSm ? 200 : 260,
210
+ ...(panelPos.flipUp
211
+ ? { bottom: window.innerHeight - panelPos.top + 4 }
212
+ : { top: panelPos.top + 4 }),
213
+ }}
214
+ >
215
+ {options.map((item, idx) => {
216
+ if (isGroup(item)) {
217
+ return (
218
+ <div key={item.label}>
219
+ {idx > 0 && <div className="my-0.5 border-t border-border/50" />}
220
+ <div className={`py-1 text-2xs font-medium text-muted-foreground uppercase tracking-wider ${isSm ? 'px-2' : 'px-3'}`}>
221
+ {item.label}
222
+ </div>
223
+ {item.options.map(renderOption)}
224
+ </div>
225
+ );
226
+ }
227
+ return renderOption(item);
228
+ })}
229
+ </div>,
230
+ document.body,
231
+ );
232
+
233
+ return (
234
+ <div className="relative">
235
+ <button
236
+ ref={btnRef}
237
+ type="button"
238
+ onClick={() => setOpen(v => !v)}
239
+ aria-haspopup="listbox"
240
+ aria-expanded={open}
241
+ className={triggerCls}
242
+ >
243
+ {selectedLabel?.icon}
244
+ <span className="flex-1 truncate text-left">
245
+ {selectedLabel?.label ?? placeholder ?? '—'}
246
+ </span>
247
+ {selectedLabel?.suffix}
248
+ <ChevronDown
249
+ size={isSm ? 12 : 14}
250
+ className={`${chevronCls} transition-transform duration-150 ${open ? 'rotate-180' : ''}`}
251
+ />
252
+ </button>
253
+ {listPortal}
254
+ </div>
255
+ );
256
+ }
@@ -1,7 +1,8 @@
1
1
  'use client';
2
2
 
3
- import { useState, useEffect, useMemo } from 'react';
4
- import { Folder, ChevronDown, ChevronRight } from 'lucide-react';
3
+ import { useState, useEffect, useMemo, useRef, useCallback } from 'react';
4
+ import { createPortal } from 'react-dom';
5
+ import { Folder, ChevronDown, ChevronRight, Check } from 'lucide-react';
5
6
  import { stripEmoji } from '@/lib/utils';
6
7
 
7
8
  interface DirPickerProps {
@@ -15,16 +16,71 @@ interface DirPickerProps {
15
16
  rootLabel?: string;
16
17
  }
17
18
 
19
+ const PANEL_MAX_H = 200;
20
+
18
21
  /**
19
- * Hierarchical directory picker — collapsed by default as a single-line button,
20
- * expands into a mini file browser with breadcrumb navigation.
22
+ * Hierarchical directory picker — trigger button stays in layout flow;
23
+ * the expanded panel renders via portal with position:fixed so it escapes
24
+ * any ancestor overflow:hidden / overflow:auto containers.
21
25
  */
22
26
  export default function DirPicker({ dirPaths, value, onChange, rootLabel = 'Root' }: DirPickerProps) {
23
27
  const [expanded, setExpanded] = useState(false);
24
28
  const [browsing, setBrowsing] = useState(value);
29
+ const [panelPos, setPanelPos] = useState<{ top: number; left: number; width: number; flipUp: boolean } | null>(null);
30
+ const btnRef = useRef<HTMLButtonElement>(null);
31
+ const panelRef = useRef<HTMLDivElement>(null);
25
32
 
26
33
  useEffect(() => { setBrowsing(value); }, [value]);
27
34
 
35
+ const calcPosition = useCallback(() => {
36
+ if (!btnRef.current) return;
37
+ const rect = btnRef.current.getBoundingClientRect();
38
+ const spaceBelow = window.innerHeight - rect.bottom;
39
+ const spaceAbove = rect.top;
40
+ const flip = spaceBelow < PANEL_MAX_H + 8 && spaceAbove > spaceBelow;
41
+ setPanelPos({
42
+ top: flip ? rect.top : rect.bottom,
43
+ left: rect.left,
44
+ width: rect.width,
45
+ flipUp: flip,
46
+ });
47
+ }, []);
48
+
49
+ // Recalculate position on open, scroll, and resize
50
+ useEffect(() => {
51
+ if (!expanded) { setPanelPos(null); return; }
52
+ calcPosition();
53
+ window.addEventListener('scroll', calcPosition, true);
54
+ window.addEventListener('resize', calcPosition);
55
+ return () => {
56
+ window.removeEventListener('scroll', calcPosition, true);
57
+ window.removeEventListener('resize', calcPosition);
58
+ };
59
+ }, [expanded, calcPosition]);
60
+
61
+ const collapse = useCallback(() => setExpanded(false), []);
62
+
63
+ useEffect(() => {
64
+ if (!expanded) return;
65
+ const handleKey = (e: KeyboardEvent) => {
66
+ if (e.key === 'Escape') { e.preventDefault(); collapse(); }
67
+ };
68
+ const handleClick = (e: MouseEvent) => {
69
+ const target = e.target as Node;
70
+ if (
71
+ btnRef.current?.contains(target) ||
72
+ panelRef.current?.contains(target)
73
+ ) return;
74
+ collapse();
75
+ };
76
+ document.addEventListener('keydown', handleKey);
77
+ document.addEventListener('mousedown', handleClick);
78
+ return () => {
79
+ document.removeEventListener('keydown', handleKey);
80
+ document.removeEventListener('mousedown', handleClick);
81
+ };
82
+ }, [expanded, collapse]);
83
+
28
84
  const children = useMemo(() => {
29
85
  const prefix = browsing ? browsing + '/' : '';
30
86
  return dirPaths
@@ -52,22 +108,19 @@ export default function DirPicker({ dirPaths, value, onChange, rootLabel = 'Root
52
108
  ? value.split('/').map(s => stripEmoji(s)).join(' / ')
53
109
  : '/ ' + rootLabel;
54
110
 
55
- if (!expanded) {
56
- return (
57
- <button
58
- type="button"
59
- onClick={() => setExpanded(true)}
60
- className="w-full flex items-center gap-2 px-3 py-2 text-sm rounded-lg border border-border bg-background text-foreground hover:border-[var(--amber)]/40 transition-colors text-left"
61
- >
62
- <Folder size={14} className="shrink-0 text-[var(--amber)]" />
63
- <span className="flex-1 truncate">{displayLabel}</span>
64
- <ChevronDown size={14} className="shrink-0 text-muted-foreground" />
65
- </button>
66
- );
67
- }
68
-
69
- return (
70
- <div className="rounded-lg border border-[var(--amber)] bg-background overflow-hidden max-h-[200px] flex flex-col">
111
+ const panel = expanded && panelPos && createPortal(
112
+ <div
113
+ ref={panelRef}
114
+ className="fixed z-[9999] rounded-lg border border-[var(--amber)] bg-card shadow-lg overflow-hidden flex flex-col"
115
+ style={{
116
+ left: panelPos.left,
117
+ width: panelPos.width,
118
+ maxHeight: PANEL_MAX_H,
119
+ ...(panelPos.flipUp
120
+ ? { bottom: window.innerHeight - panelPos.top + 4 }
121
+ : { top: panelPos.top + 4 }),
122
+ }}
123
+ >
71
124
  {/* Breadcrumb */}
72
125
  <div className="flex items-center gap-0.5 px-3 py-1.5 bg-muted/30 border-b border-border overflow-x-auto text-xs shrink-0">
73
126
  <button
@@ -117,14 +170,38 @@ export default function DirPicker({ dirPaths, value, onChange, rootLabel = 'Root
117
170
  ) : (
118
171
  <div className="px-3 py-2 text-xs text-muted-foreground/50 text-center">—</div>
119
172
  )}
120
- {/* Collapse */}
173
+ {/* Confirm & collapse */}
121
174
  <button
122
175
  type="button"
123
- onClick={() => setExpanded(false)}
124
- className="w-full py-1.5 text-xs font-medium text-[var(--amber)] border-t border-border hover:bg-muted/30 transition-colors shrink-0"
176
+ onClick={collapse}
177
+ className="w-full py-1.5 flex items-center justify-center gap-1 text-xs font-medium text-[var(--amber)] border-t border-border hover:bg-muted/30 transition-colors shrink-0"
125
178
  >
126
-
179
+ <Check size={12} />
180
+ </button>
181
+ </div>,
182
+ document.body,
183
+ );
184
+
185
+ return (
186
+ <>
187
+ <button
188
+ ref={btnRef}
189
+ type="button"
190
+ onClick={() => setExpanded(v => !v)}
191
+ aria-haspopup="true"
192
+ aria-expanded={expanded}
193
+ className={`w-full flex items-center gap-2 px-3 py-2 text-sm rounded-lg border bg-background text-foreground transition-colors text-left focus-visible:ring-1 focus-visible:ring-ring outline-none ${
194
+ expanded ? 'border-[var(--amber)]' : 'border-border hover:border-[var(--amber)]/40'
195
+ }`}
196
+ >
197
+ <Folder size={14} className="shrink-0 text-[var(--amber)]" />
198
+ <span className="flex-1 truncate">{displayLabel}</span>
199
+ <ChevronDown
200
+ size={14}
201
+ className={`shrink-0 text-muted-foreground transition-transform duration-150 ${expanded ? 'rotate-180' : ''}`}
202
+ />
127
203
  </button>
128
- </div>
204
+ {panel}
205
+ </>
129
206
  );
130
207
  }