@geminilight/mindos 0.6.16 → 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);
@@ -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);
@@ -1,6 +1,7 @@
1
1
  'use client';
2
2
 
3
3
  import { useState, useRef, useEffect, useCallback, useMemo } from 'react';
4
+ import { createPortal } from 'react-dom';
4
5
  import { ChevronDown, Check } from 'lucide-react';
5
6
 
6
7
  export interface SelectOption {
@@ -50,7 +51,7 @@ export default function CustomSelect({
50
51
  }: CustomSelectProps) {
51
52
  const [open, setOpen] = useState(false);
52
53
  const [highlightIdx, setHighlightIdx] = useState(-1);
53
- const [flipUp, setFlipUp] = useState(false);
54
+ const [panelPos, setPanelPos] = useState<{ top: number; left: number; width: number; flipUp: boolean } | null>(null);
54
55
  const btnRef = useRef<HTMLButtonElement>(null);
55
56
  const listRef = useRef<HTMLDivElement>(null);
56
57
 
@@ -122,20 +123,33 @@ export default function CustomSelect({
122
123
  if (el) el.scrollIntoView({ block: 'nearest' });
123
124
  }, [open, highlightIdx]);
124
125
 
125
- // Initialize highlight + flip direction when opening
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
126
141
  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]);
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]);
139
153
 
140
154
  const isSm = size === 'sm';
141
155
 
@@ -147,9 +161,9 @@ export default function CustomSelect({
147
161
  ? 'absolute right-1 top-1/2 -translate-y-1/2 text-muted-foreground pointer-events-none'
148
162
  : 'absolute right-2.5 top-1/2 -translate-y-1/2 text-muted-foreground pointer-events-none';
149
163
 
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'}`;
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';
153
167
 
154
168
  const itemBaseCls = isSm
155
169
  ? 'w-full flex items-center gap-1.5 px-2 py-1 text-2xs text-left transition-colors cursor-pointer'
@@ -184,6 +198,38 @@ export default function CustomSelect({
184
198
  );
185
199
  }
186
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
+
187
233
  return (
188
234
  <div className="relative">
189
235
  <button
@@ -204,25 +250,7 @@ export default function CustomSelect({
204
250
  className={`${chevronCls} transition-transform duration-150 ${open ? 'rotate-180' : ''}`}
205
251
  />
206
252
  </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
- )}
253
+ {listPortal}
226
254
  </div>
227
255
  );
228
256
  }
@@ -1,6 +1,7 @@
1
1
  'use client';
2
2
 
3
3
  import { useState, useEffect, useMemo, useRef, useCallback } from 'react';
4
+ import { createPortal } from 'react-dom';
4
5
  import { Folder, ChevronDown, ChevronRight, Check } from 'lucide-react';
5
6
  import { stripEmoji } from '@/lib/utils';
6
7
 
@@ -18,27 +19,44 @@ interface DirPickerProps {
18
19
  const PANEL_MAX_H = 200;
19
20
 
20
21
  /**
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.
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.
24
25
  */
25
26
  export default function DirPicker({ dirPaths, value, onChange, rootLabel = 'Root' }: DirPickerProps) {
26
27
  const [expanded, setExpanded] = useState(false);
27
28
  const [browsing, setBrowsing] = useState(value);
28
- const [flipUp, setFlipUp] = useState(false);
29
- const containerRef = useRef<HTMLDivElement>(null);
29
+ const [panelPos, setPanelPos] = useState<{ top: number; left: number; width: number; flipUp: boolean } | null>(null);
30
30
  const btnRef = useRef<HTMLButtonElement>(null);
31
+ const panelRef = useRef<HTMLDivElement>(null);
31
32
 
32
33
  useEffect(() => { setBrowsing(value); }, [value]);
33
34
 
34
- // Decide flip direction when opening
35
- useEffect(() => {
36
- if (!expanded || !btnRef.current) return;
35
+ const calcPosition = useCallback(() => {
36
+ if (!btnRef.current) return;
37
37
  const rect = btnRef.current.getBoundingClientRect();
38
38
  const spaceBelow = window.innerHeight - rect.bottom;
39
39
  const spaceAbove = rect.top;
40
- setFlipUp(spaceBelow < PANEL_MAX_H + 8 && spaceAbove > spaceBelow);
41
- }, [expanded]);
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]);
42
60
 
43
61
  const collapse = useCallback(() => setExpanded(false), []);
44
62
 
@@ -48,9 +66,12 @@ export default function DirPicker({ dirPaths, value, onChange, rootLabel = 'Root
48
66
  if (e.key === 'Escape') { e.preventDefault(); collapse(); }
49
67
  };
50
68
  const handleClick = (e: MouseEvent) => {
51
- if (containerRef.current && !containerRef.current.contains(e.target as Node)) {
52
- collapse();
53
- }
69
+ const target = e.target as Node;
70
+ if (
71
+ btnRef.current?.contains(target) ||
72
+ panelRef.current?.contains(target)
73
+ ) return;
74
+ collapse();
54
75
  };
55
76
  document.addEventListener('keydown', handleKey);
56
77
  document.addEventListener('mousedown', handleClick);
@@ -87,9 +108,82 @@ export default function DirPicker({ dirPaths, value, onChange, rootLabel = 'Root
87
108
  ? value.split('/').map(s => stripEmoji(s)).join(' / ')
88
109
  : '/ ' + rootLabel;
89
110
 
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
+ >
124
+ {/* Breadcrumb */}
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">
126
+ <button
127
+ type="button"
128
+ onClick={() => navigateTo(-1)}
129
+ className={`shrink-0 px-1.5 py-0.5 rounded transition-colors ${
130
+ browsing === '' ? 'text-[var(--amber)] font-medium' : 'text-muted-foreground hover:text-foreground'
131
+ }`}
132
+ >
133
+ / {rootLabel}
134
+ </button>
135
+ {segments.map((seg, i) => (
136
+ <span key={i} className="flex items-center gap-0.5 shrink-0">
137
+ <ChevronRight size={10} className="text-muted-foreground/50" />
138
+ <button
139
+ type="button"
140
+ onClick={() => navigateTo(i)}
141
+ className={`px-1.5 py-0.5 rounded transition-colors truncate max-w-[100px] ${
142
+ i === segments.length - 1 ? 'text-[var(--amber)] font-medium' : 'text-muted-foreground hover:text-foreground'
143
+ }`}
144
+ >
145
+ {seg}
146
+ </button>
147
+ </span>
148
+ ))}
149
+ </div>
150
+ {/* Child directories */}
151
+ {children.length > 0 ? (
152
+ <div className="flex-1 min-h-0 overflow-y-auto">
153
+ {children.map(childPath => {
154
+ const childName = childPath.split('/').pop() || childPath;
155
+ const hasChildren = dirPaths.some(p => p.startsWith(childPath + '/'));
156
+ return (
157
+ <button
158
+ key={childPath}
159
+ type="button"
160
+ onClick={() => drillInto(childPath)}
161
+ className="w-full flex items-center gap-2 px-3 py-1.5 text-xs text-foreground hover:bg-muted/60 transition-colors"
162
+ >
163
+ <Folder size={12} className="shrink-0 text-[var(--amber)]" />
164
+ <span className="flex-1 text-left truncate">{childName}</span>
165
+ {hasChildren && <ChevronRight size={11} className="shrink-0 text-muted-foreground/40" />}
166
+ </button>
167
+ );
168
+ })}
169
+ </div>
170
+ ) : (
171
+ <div className="px-3 py-2 text-xs text-muted-foreground/50 text-center">—</div>
172
+ )}
173
+ {/* Confirm & collapse */}
174
+ <button
175
+ type="button"
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"
178
+ >
179
+ <Check size={12} />
180
+ </button>
181
+ </div>,
182
+ document.body,
183
+ );
184
+
90
185
  return (
91
- <div ref={containerRef} className="relative">
92
- {/* Trigger — always in document flow */}
186
+ <>
93
187
  <button
94
188
  ref={btnRef}
95
189
  type="button"
@@ -107,71 +201,7 @@ export default function DirPicker({ dirPaths, value, onChange, rootLabel = 'Root
107
201
  className={`shrink-0 text-muted-foreground transition-transform duration-150 ${expanded ? 'rotate-180' : ''}`}
108
202
  />
109
203
  </button>
110
-
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">
118
- <button
119
- type="button"
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'
123
- }`}
124
- >
125
- / {rootLabel}
126
- </button>
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>
173
- </div>
174
- )}
175
- </div>
204
+ {panel}
205
+ </>
176
206
  );
177
207
  }
@@ -73,11 +73,18 @@ export default function ImportModal({ open, onClose, defaultSpace, initialFiles,
73
73
  useEffect(() => {
74
74
  if (!open) return;
75
75
  const handler = (e: KeyboardEvent) => {
76
- if (e.key === 'Escape') { e.stopPropagation(); handleClose(); }
76
+ if (e.key !== 'Escape') return;
77
+ if (showDiscard) {
78
+ e.stopPropagation();
79
+ setShowDiscard(false);
80
+ return;
81
+ }
82
+ e.stopPropagation();
83
+ handleClose();
77
84
  };
78
85
  window.addEventListener('keydown', handler, true);
79
86
  return () => window.removeEventListener('keydown', handler, true);
80
- }, [open, handleClose]);
87
+ }, [open, handleClose, showDiscard]);
81
88
 
82
89
  const checkConflicts = useCallback(async (fileNames: string[], space: string) => {
83
90
  try {
@@ -197,15 +204,6 @@ export default function ImportModal({ open, onClose, defaultSpace, initialFiles,
197
204
 
198
205
  return (
199
206
  <>
200
- <ConfirmDialog
201
- open={showDiscard}
202
- title={t.fileImport.discardTitle}
203
- message={t.fileImport.discardMessage(im.files.length)}
204
- confirmLabel={t.fileImport.discardConfirm}
205
- cancelLabel={t.fileImport.discardCancel}
206
- onConfirm={doClose}
207
- onCancel={() => setShowDiscard(false)}
208
- />
209
207
  <div
210
208
  ref={overlayRef}
211
209
  className={`fixed inset-0 z-50 modal-backdrop flex items-center justify-center p-4 transition-opacity duration-200 ${closing ? 'opacity-0' : 'opacity-100'}`}
@@ -507,6 +505,15 @@ export default function ImportModal({ open, onClose, defaultSpace, initialFiles,
507
505
  </div>
508
506
  </div>
509
507
  </div>
508
+ <ConfirmDialog
509
+ open={showDiscard}
510
+ title={t.fileImport.discardTitle}
511
+ message={t.fileImport.discardMessage(im.files.length)}
512
+ confirmLabel={t.fileImport.discardConfirm}
513
+ cancelLabel={t.fileImport.discardCancel}
514
+ onConfirm={doClose}
515
+ onCancel={() => setShowDiscard(false)}
516
+ />
510
517
  </>
511
518
  );
512
519
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@geminilight/mindos",
3
- "version": "0.6.16",
3
+ "version": "0.6.17",
4
4
  "description": "MindOS — Human-Agent Collaborative Mind System. Local-first knowledge base that syncs your mind to all AI Agents via MCP.",
5
5
  "keywords": [
6
6
  "mindos",