@geminilight/mindos 0.6.14 → 0.6.16

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.
@@ -78,7 +78,7 @@ body {
78
78
  --amber-text: #9a6a2b;
79
79
  --amber-dim: rgba(200, 135, 58, 0.18);
80
80
  --amber-subtle: rgba(200, 135, 30, 0.08);
81
- --amber-foreground: #131210;
81
+ --amber-foreground: #ffffff;
82
82
  --success: #7aad80;
83
83
  --error: #c85050;
84
84
  --sidebar: #ede9e1;
@@ -114,7 +114,7 @@ body {
114
114
  --amber-text: #e0a85e;
115
115
  --amber-dim: rgba(212, 149, 74, 0.20);
116
116
  --amber-subtle: rgba(212, 149, 74, 0.10);
117
- --amber-foreground: #131210;
117
+ --amber-foreground: #ffffff;
118
118
  --success: #7aad80;
119
119
  --error: #c85050;
120
120
  --sidebar: #1c1a17;
@@ -434,6 +434,53 @@ a:focus-visible,
434
434
  outline: none;
435
435
  }
436
436
 
437
+ /* ── Styled form controls ──────────────────────────────────── */
438
+
439
+ input[type="checkbox"].form-check {
440
+ appearance: none;
441
+ width: 14px; height: 14px;
442
+ flex-shrink: 0;
443
+ border-radius: 3px;
444
+ border: 1px solid var(--border);
445
+ background: var(--background);
446
+ cursor: pointer;
447
+ transition: background-color 0.15s, border-color 0.15s;
448
+ background-repeat: no-repeat;
449
+ background-position: center;
450
+ background-size: 100% 100%;
451
+ }
452
+ input[type="checkbox"].form-check:checked {
453
+ background-color: var(--amber);
454
+ border-color: var(--amber);
455
+ 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");
456
+ }
457
+ input[type="checkbox"].form-check:focus-visible {
458
+ outline: none;
459
+ box-shadow: 0 0 0 2px var(--ring);
460
+ }
461
+ input[type="checkbox"].form-check:disabled {
462
+ opacity: 0.5;
463
+ cursor: not-allowed;
464
+ }
465
+
466
+ input[type="radio"].form-radio {
467
+ appearance: none;
468
+ width: 14px; height: 14px;
469
+ flex-shrink: 0;
470
+ border-radius: 9999px;
471
+ border: 1px solid var(--border);
472
+ background: var(--background);
473
+ cursor: pointer;
474
+ transition: border-color 0.15s;
475
+ }
476
+ input[type="radio"].form-radio:checked {
477
+ border: 4px solid var(--amber);
478
+ }
479
+ input[type="radio"].form-radio:focus-visible {
480
+ outline: none;
481
+ box-shadow: 0 0 0 2px var(--ring);
482
+ }
483
+
437
484
  /* macOS Electron: traffic-light safe zone. Actual CSS is injected by Electron
438
485
  main process via webContents.insertCSS(); this is a no-op fallback. */
439
486
  .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,228 @@
1
+ 'use client';
2
+
3
+ import { useState, useRef, useEffect, useCallback, useMemo } from 'react';
4
+ import { ChevronDown, Check } from 'lucide-react';
5
+
6
+ export interface SelectOption {
7
+ value: string;
8
+ label: string;
9
+ icon?: React.ReactNode;
10
+ suffix?: React.ReactNode;
11
+ }
12
+
13
+ export interface SelectOptionGroup {
14
+ label: string;
15
+ options: SelectOption[];
16
+ }
17
+
18
+ export type SelectItem = SelectOption | SelectOptionGroup;
19
+
20
+ function isGroup(item: SelectItem): item is SelectOptionGroup {
21
+ return 'options' in item;
22
+ }
23
+
24
+ function flatOptions(items: SelectItem[]): SelectOption[] {
25
+ const result: SelectOption[] = [];
26
+ for (const item of items) {
27
+ if (isGroup(item)) result.push(...item.options);
28
+ else result.push(item);
29
+ }
30
+ return result;
31
+ }
32
+
33
+ interface CustomSelectProps {
34
+ value: string;
35
+ onChange: (value: string) => void;
36
+ options: SelectItem[];
37
+ className?: string;
38
+ /** 'sm' for inline/compact, 'md' (default) for form fields */
39
+ size?: 'sm' | 'md';
40
+ placeholder?: string;
41
+ }
42
+
43
+ export default function CustomSelect({
44
+ value,
45
+ onChange,
46
+ options,
47
+ className = '',
48
+ size = 'md',
49
+ placeholder,
50
+ }: CustomSelectProps) {
51
+ const [open, setOpen] = useState(false);
52
+ const [highlightIdx, setHighlightIdx] = useState(-1);
53
+ const [flipUp, setFlipUp] = useState(false);
54
+ const btnRef = useRef<HTMLButtonElement>(null);
55
+ const listRef = useRef<HTMLDivElement>(null);
56
+
57
+ const allOptions = useMemo(() => flatOptions(options), [options]);
58
+
59
+ const selectedLabel = useMemo(() => {
60
+ return allOptions.find(o => o.value === value) ?? null;
61
+ }, [allOptions, value]);
62
+
63
+ const close = useCallback(() => {
64
+ setOpen(false);
65
+ setHighlightIdx(-1);
66
+ }, []);
67
+
68
+ const select = useCallback((val: string) => {
69
+ onChange(val);
70
+ close();
71
+ }, [onChange, close]);
72
+
73
+ useEffect(() => {
74
+ if (!open) return;
75
+ const handleClick = (e: MouseEvent) => {
76
+ if (
77
+ btnRef.current && !btnRef.current.contains(e.target as Node) &&
78
+ listRef.current && !listRef.current.contains(e.target as Node)
79
+ ) {
80
+ close();
81
+ }
82
+ };
83
+ document.addEventListener('mousedown', handleClick);
84
+ return () => document.removeEventListener('mousedown', handleClick);
85
+ }, [open, close]);
86
+
87
+ // Keyboard navigation
88
+ useEffect(() => {
89
+ if (!open) return;
90
+ const handleKey = (e: KeyboardEvent) => {
91
+ switch (e.key) {
92
+ case 'Escape':
93
+ e.preventDefault();
94
+ close();
95
+ btnRef.current?.focus();
96
+ break;
97
+ case 'ArrowDown':
98
+ e.preventDefault();
99
+ setHighlightIdx(prev => Math.min(prev + 1, allOptions.length - 1));
100
+ break;
101
+ case 'ArrowUp':
102
+ e.preventDefault();
103
+ setHighlightIdx(prev => Math.max(prev - 1, 0));
104
+ break;
105
+ case 'Enter':
106
+ e.preventDefault();
107
+ if (highlightIdx >= 0 && highlightIdx < allOptions.length) {
108
+ select(allOptions[highlightIdx].value);
109
+ btnRef.current?.focus();
110
+ }
111
+ break;
112
+ }
113
+ };
114
+ document.addEventListener('keydown', handleKey);
115
+ return () => document.removeEventListener('keydown', handleKey);
116
+ }, [open, close, highlightIdx, allOptions, select]);
117
+
118
+ // Scroll highlighted option into view
119
+ useEffect(() => {
120
+ if (!open || !listRef.current || highlightIdx < 0) return;
121
+ const el = listRef.current.querySelector(`[data-idx="${highlightIdx}"]`);
122
+ if (el) el.scrollIntoView({ block: 'nearest' });
123
+ }, [open, highlightIdx]);
124
+
125
+ // Initialize highlight + flip direction when opening
126
+ useEffect(() => {
127
+ if (open) {
128
+ const idx = allOptions.findIndex(o => o.value === value);
129
+ setHighlightIdx(idx >= 0 ? idx : 0);
130
+ if (btnRef.current) {
131
+ const rect = btnRef.current.getBoundingClientRect();
132
+ const maxH = size === 'sm' ? 200 : 260;
133
+ const spaceBelow = window.innerHeight - rect.bottom;
134
+ const spaceAbove = rect.top;
135
+ setFlipUp(spaceBelow < maxH + 8 && spaceAbove > spaceBelow);
136
+ }
137
+ }
138
+ }, [open, allOptions, value, size]);
139
+
140
+ const isSm = size === 'sm';
141
+
142
+ const triggerCls = isSm
143
+ ? `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}`
144
+ : `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}`;
145
+
146
+ const chevronCls = isSm
147
+ ? 'absolute right-1 top-1/2 -translate-y-1/2 text-muted-foreground pointer-events-none'
148
+ : 'absolute right-2.5 top-1/2 -translate-y-1/2 text-muted-foreground pointer-events-none';
149
+
150
+ const listCls = isSm
151
+ ? `absolute z-50 min-w-full max-h-[200px] overflow-y-auto rounded-md border border-border bg-card shadow-lg py-0.5 ${flipUp ? 'bottom-full mb-1' : 'top-full mt-1'}`
152
+ : `absolute z-50 min-w-full max-h-[260px] overflow-y-auto rounded-lg border border-border bg-card shadow-lg py-1 ${flipUp ? 'bottom-full mb-1' : 'top-full mt-1'}`;
153
+
154
+ const itemBaseCls = isSm
155
+ ? 'w-full flex items-center gap-1.5 px-2 py-1 text-2xs text-left transition-colors cursor-pointer'
156
+ : 'w-full flex items-center gap-2 px-3 py-1.5 text-sm text-left transition-colors cursor-pointer';
157
+
158
+ // Track flat index across groups for keyboard highlight
159
+ let flatIdx = -1;
160
+
161
+ function renderOption(opt: SelectOption) {
162
+ flatIdx++;
163
+ const idx = flatIdx;
164
+ const isSelected = opt.value === value;
165
+ const isHighlighted = idx === highlightIdx;
166
+ return (
167
+ <button
168
+ key={opt.value}
169
+ type="button"
170
+ role="option"
171
+ aria-selected={isSelected}
172
+ data-idx={idx}
173
+ onClick={() => select(opt.value)}
174
+ onMouseEnter={() => setHighlightIdx(idx)}
175
+ className={`${itemBaseCls} ${
176
+ isHighlighted ? 'bg-muted text-foreground' : 'text-foreground'
177
+ }`}
178
+ >
179
+ {opt.icon}
180
+ <span className="flex-1 truncate">{opt.label}</span>
181
+ {opt.suffix}
182
+ {isSelected && <Check size={isSm ? 10 : 12} className="shrink-0 text-[var(--amber)]" />}
183
+ </button>
184
+ );
185
+ }
186
+
187
+ return (
188
+ <div className="relative">
189
+ <button
190
+ ref={btnRef}
191
+ type="button"
192
+ onClick={() => setOpen(v => !v)}
193
+ aria-haspopup="listbox"
194
+ aria-expanded={open}
195
+ className={triggerCls}
196
+ >
197
+ {selectedLabel?.icon}
198
+ <span className="flex-1 truncate text-left">
199
+ {selectedLabel?.label ?? placeholder ?? '—'}
200
+ </span>
201
+ {selectedLabel?.suffix}
202
+ <ChevronDown
203
+ size={isSm ? 12 : 14}
204
+ className={`${chevronCls} transition-transform duration-150 ${open ? 'rotate-180' : ''}`}
205
+ />
206
+ </button>
207
+
208
+ {open && (
209
+ <div ref={listRef} className={listCls} role="listbox">
210
+ {options.map((item, idx) => {
211
+ if (isGroup(item)) {
212
+ return (
213
+ <div key={item.label}>
214
+ {idx > 0 && <div className="my-0.5 border-t border-border/50" />}
215
+ <div className={`py-1 text-2xs font-medium text-muted-foreground uppercase tracking-wider ${isSm ? 'px-2' : 'px-3'}`}>
216
+ {item.label}
217
+ </div>
218
+ {item.options.map(renderOption)}
219
+ </div>
220
+ );
221
+ }
222
+ return renderOption(item);
223
+ })}
224
+ </div>
225
+ )}
226
+ </div>
227
+ );
228
+ }
@@ -1,7 +1,7 @@
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 { Folder, ChevronDown, ChevronRight, Check } from 'lucide-react';
5
5
  import { stripEmoji } from '@/lib/utils';
6
6
 
7
7
  interface DirPickerProps {
@@ -15,16 +15,51 @@ interface DirPickerProps {
15
15
  rootLabel?: string;
16
16
  }
17
17
 
18
+ const PANEL_MAX_H = 200;
19
+
18
20
  /**
19
- * Hierarchical directory picker — collapsed by default as a single-line button,
20
- * expands into a mini file browser with breadcrumb navigation.
21
+ * Hierarchical directory picker — always renders as a single-line trigger button.
22
+ * When expanded, the tree browser floats as an overlay (absolute) so it never
23
+ * pushes sibling content down.
21
24
  */
22
25
  export default function DirPicker({ dirPaths, value, onChange, rootLabel = 'Root' }: DirPickerProps) {
23
26
  const [expanded, setExpanded] = useState(false);
24
27
  const [browsing, setBrowsing] = useState(value);
28
+ const [flipUp, setFlipUp] = useState(false);
29
+ const containerRef = useRef<HTMLDivElement>(null);
30
+ const btnRef = useRef<HTMLButtonElement>(null);
25
31
 
26
32
  useEffect(() => { setBrowsing(value); }, [value]);
27
33
 
34
+ // Decide flip direction when opening
35
+ useEffect(() => {
36
+ if (!expanded || !btnRef.current) return;
37
+ const rect = btnRef.current.getBoundingClientRect();
38
+ const spaceBelow = window.innerHeight - rect.bottom;
39
+ const spaceAbove = rect.top;
40
+ setFlipUp(spaceBelow < PANEL_MAX_H + 8 && spaceAbove > spaceBelow);
41
+ }, [expanded]);
42
+
43
+ const collapse = useCallback(() => setExpanded(false), []);
44
+
45
+ useEffect(() => {
46
+ if (!expanded) return;
47
+ const handleKey = (e: KeyboardEvent) => {
48
+ if (e.key === 'Escape') { e.preventDefault(); collapse(); }
49
+ };
50
+ const handleClick = (e: MouseEvent) => {
51
+ if (containerRef.current && !containerRef.current.contains(e.target as Node)) {
52
+ collapse();
53
+ }
54
+ };
55
+ document.addEventListener('keydown', handleKey);
56
+ document.addEventListener('mousedown', handleClick);
57
+ return () => {
58
+ document.removeEventListener('keydown', handleKey);
59
+ document.removeEventListener('mousedown', handleClick);
60
+ };
61
+ }, [expanded, collapse]);
62
+
28
63
  const children = useMemo(() => {
29
64
  const prefix = browsing ? browsing + '/' : '';
30
65
  return dirPaths
@@ -52,79 +87,91 @@ export default function DirPicker({ dirPaths, value, onChange, rootLabel = 'Root
52
87
  ? value.split('/').map(s => stripEmoji(s)).join(' / ')
53
88
  : '/ ' + rootLabel;
54
89
 
55
- if (!expanded) {
56
- return (
90
+ return (
91
+ <div ref={containerRef} className="relative">
92
+ {/* Trigger — always in document flow */}
57
93
  <button
94
+ ref={btnRef}
58
95
  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"
96
+ onClick={() => setExpanded(v => !v)}
97
+ aria-haspopup="true"
98
+ aria-expanded={expanded}
99
+ 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 ${
100
+ expanded ? 'border-[var(--amber)]' : 'border-border hover:border-[var(--amber)]/40'
101
+ }`}
61
102
  >
62
103
  <Folder size={14} className="shrink-0 text-[var(--amber)]" />
63
104
  <span className="flex-1 truncate">{displayLabel}</span>
64
- <ChevronDown size={14} className="shrink-0 text-muted-foreground" />
105
+ <ChevronDown
106
+ size={14}
107
+ className={`shrink-0 text-muted-foreground transition-transform duration-150 ${expanded ? 'rotate-180' : ''}`}
108
+ />
65
109
  </button>
66
- );
67
- }
68
110
 
69
- return (
70
- <div className="rounded-lg border border-[var(--amber)] bg-background overflow-hidden max-h-[200px] flex flex-col">
71
- {/* Breadcrumb */}
72
- <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
- <button
74
- type="button"
75
- onClick={() => navigateTo(-1)}
76
- className={`shrink-0 px-1.5 py-0.5 rounded transition-colors ${
77
- browsing === '' ? 'text-[var(--amber)] font-medium' : 'text-muted-foreground hover:text-foreground'
78
- }`}
79
- >
80
- / {rootLabel}
81
- </button>
82
- {segments.map((seg, i) => (
83
- <span key={i} className="flex items-center gap-0.5 shrink-0">
84
- <ChevronRight size={10} className="text-muted-foreground/50" />
111
+ {/* Floating panel — absolute, never pushes content */}
112
+ {expanded && (
113
+ <div className={`absolute z-50 left-0 right-0 rounded-lg border border-[var(--amber)] bg-card shadow-lg overflow-hidden max-h-[200px] flex flex-col ${
114
+ flipUp ? 'bottom-full mb-1' : 'top-full mt-1'
115
+ }`}>
116
+ {/* Breadcrumb */}
117
+ <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">
85
118
  <button
86
119
  type="button"
87
- onClick={() => navigateTo(i)}
88
- className={`px-1.5 py-0.5 rounded transition-colors truncate max-w-[100px] ${
89
- i === segments.length - 1 ? 'text-[var(--amber)] font-medium' : 'text-muted-foreground hover:text-foreground'
120
+ onClick={() => navigateTo(-1)}
121
+ className={`shrink-0 px-1.5 py-0.5 rounded transition-colors ${
122
+ browsing === '' ? 'text-[var(--amber)] font-medium' : 'text-muted-foreground hover:text-foreground'
90
123
  }`}
91
124
  >
92
- {seg}
125
+ / {rootLabel}
93
126
  </button>
94
- </span>
95
- ))}
96
- </div>
97
- {/* Child directories */}
98
- {children.length > 0 ? (
99
- <div className="flex-1 min-h-0 overflow-y-auto">
100
- {children.map(childPath => {
101
- const childName = childPath.split('/').pop() || childPath;
102
- const hasChildren = dirPaths.some(p => p.startsWith(childPath + '/'));
103
- return (
104
- <button
105
- key={childPath}
106
- type="button"
107
- onClick={() => drillInto(childPath)}
108
- className="w-full flex items-center gap-2 px-3 py-1.5 text-xs text-foreground hover:bg-muted/60 transition-colors"
109
- >
110
- <Folder size={12} className="shrink-0 text-[var(--amber)]" />
111
- <span className="flex-1 text-left truncate">{childName}</span>
112
- {hasChildren && <ChevronRight size={11} className="shrink-0 text-muted-foreground/40" />}
113
- </button>
114
- );
115
- })}
127
+ {segments.map((seg, i) => (
128
+ <span key={i} className="flex items-center gap-0.5 shrink-0">
129
+ <ChevronRight size={10} className="text-muted-foreground/50" />
130
+ <button
131
+ type="button"
132
+ onClick={() => navigateTo(i)}
133
+ className={`px-1.5 py-0.5 rounded transition-colors truncate max-w-[100px] ${
134
+ i === segments.length - 1 ? 'text-[var(--amber)] font-medium' : 'text-muted-foreground hover:text-foreground'
135
+ }`}
136
+ >
137
+ {seg}
138
+ </button>
139
+ </span>
140
+ ))}
141
+ </div>
142
+ {/* Child directories */}
143
+ {children.length > 0 ? (
144
+ <div className="flex-1 min-h-0 overflow-y-auto">
145
+ {children.map(childPath => {
146
+ const childName = childPath.split('/').pop() || childPath;
147
+ const hasChildren = dirPaths.some(p => p.startsWith(childPath + '/'));
148
+ return (
149
+ <button
150
+ key={childPath}
151
+ type="button"
152
+ onClick={() => drillInto(childPath)}
153
+ className="w-full flex items-center gap-2 px-3 py-1.5 text-xs text-foreground hover:bg-muted/60 transition-colors"
154
+ >
155
+ <Folder size={12} className="shrink-0 text-[var(--amber)]" />
156
+ <span className="flex-1 text-left truncate">{childName}</span>
157
+ {hasChildren && <ChevronRight size={11} className="shrink-0 text-muted-foreground/40" />}
158
+ </button>
159
+ );
160
+ })}
161
+ </div>
162
+ ) : (
163
+ <div className="px-3 py-2 text-xs text-muted-foreground/50 text-center">—</div>
164
+ )}
165
+ {/* Confirm & collapse */}
166
+ <button
167
+ type="button"
168
+ onClick={collapse}
169
+ 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"
170
+ >
171
+ <Check size={12} />
172
+ </button>
116
173
  </div>
117
- ) : (
118
- <div className="px-3 py-2 text-xs text-muted-foreground/50 text-center">—</div>
119
174
  )}
120
- {/* Collapse */}
121
- <button
122
- 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"
125
- >
126
-
127
- </button>
128
175
  </div>
129
176
  );
130
177
  }