@geminilight/mindos 0.5.34 → 0.5.35
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.
|
@@ -27,7 +27,7 @@ export default function AskFab({ onToggle, askPanelOpen }: AskFabProps) {
|
|
|
27
27
|
${askPanelOpen ? 'opacity-0 pointer-events-none translate-y-2' : 'opacity-100 translate-y-0'}
|
|
28
28
|
`}
|
|
29
29
|
style={{
|
|
30
|
-
background: 'linear-gradient(135deg,
|
|
30
|
+
background: 'linear-gradient(135deg, var(--amber), color-mix(in srgb, var(--amber) 80%, white))',
|
|
31
31
|
}}
|
|
32
32
|
title="MindOS Agent (⌘/)"
|
|
33
33
|
aria-label="MindOS Agent"
|
|
@@ -15,6 +15,10 @@ const DIR_ICONS: Record<string, string> = {
|
|
|
15
15
|
|
|
16
16
|
const EMPTY_FILES = ['INSTRUCTION.md', 'README.md', 'CONFIG.json'];
|
|
17
17
|
|
|
18
|
+
/* Shared amber-subtle background — used as inline style because Tailwind can't handle
|
|
19
|
+
CSS var() with fallback in arbitrary values: bg-[var(--amber-subtle,rgba(...))] breaks. */
|
|
20
|
+
const amberSubtleBg = 'var(--amber-subtle, rgba(200,135,30,0.08))';
|
|
21
|
+
|
|
18
22
|
interface GuideCardProps {
|
|
19
23
|
/** Called when user clicks a file/dir to open it in FileView */
|
|
20
24
|
onNavigate?: (path: string) => void;
|
|
@@ -39,7 +43,6 @@ export default function GuideCard({ onNavigate }: GuideCardProps) {
|
|
|
39
43
|
setGuideState(gs);
|
|
40
44
|
if (gs.step1Done) setBrowsedCount(1);
|
|
41
45
|
} else {
|
|
42
|
-
// Guide inactive or dismissed — clear local state
|
|
43
46
|
setGuideState(null);
|
|
44
47
|
}
|
|
45
48
|
})
|
|
@@ -49,13 +52,9 @@ export default function GuideCard({ onNavigate }: GuideCardProps) {
|
|
|
49
52
|
useEffect(() => {
|
|
50
53
|
fetchGuideState();
|
|
51
54
|
|
|
52
|
-
|
|
53
|
-
const handleFirstVisit = () => {
|
|
54
|
-
setIsFirstVisit(true);
|
|
55
|
-
};
|
|
55
|
+
const handleFirstVisit = () => { setIsFirstVisit(true); };
|
|
56
56
|
window.addEventListener('mindos:first-visit', handleFirstVisit);
|
|
57
57
|
|
|
58
|
-
// Re-fetch when guide state is updated (e.g. after AskFab patches askedAI)
|
|
59
58
|
const handleGuideUpdate = () => fetchGuideState();
|
|
60
59
|
window.addEventListener('focus', handleGuideUpdate);
|
|
61
60
|
window.addEventListener('guide-state-updated', handleGuideUpdate);
|
|
@@ -66,14 +65,12 @@ export default function GuideCard({ onNavigate }: GuideCardProps) {
|
|
|
66
65
|
};
|
|
67
66
|
}, [fetchGuideState]);
|
|
68
67
|
|
|
69
|
-
// Auto-expand KB task on first visit
|
|
70
68
|
useEffect(() => {
|
|
71
69
|
if (isFirstVisit && guideState && !guideState.step1Done) {
|
|
72
70
|
setExpanded('kb');
|
|
73
71
|
}
|
|
74
72
|
}, [isFirstVisit, guideState]);
|
|
75
73
|
|
|
76
|
-
// Patch guideState to backend
|
|
77
74
|
const patchGuide = useCallback((patch: Partial<GuideState>) => {
|
|
78
75
|
setGuideState(prev => prev ? { ...prev, ...patch } : prev);
|
|
79
76
|
fetch('/api/setup', {
|
|
@@ -93,7 +90,6 @@ export default function GuideCard({ onNavigate }: GuideCardProps) {
|
|
|
93
90
|
if (browsedCount === 0) {
|
|
94
91
|
setBrowsedCount(1);
|
|
95
92
|
patchGuide({ step1Done: true });
|
|
96
|
-
// Collapse after a beat
|
|
97
93
|
setTimeout(() => setExpanded(null), 300);
|
|
98
94
|
}
|
|
99
95
|
}, [browsedCount, patchGuide, onNavigate]);
|
|
@@ -109,8 +105,6 @@ export default function GuideCard({ onNavigate }: GuideCardProps) {
|
|
|
109
105
|
const isEmpty = gs?.template === 'empty';
|
|
110
106
|
const prompt = isEmpty ? g.ai.promptEmpty : g.ai.prompt;
|
|
111
107
|
openAskModal(prompt, 'guide');
|
|
112
|
-
// Don't optimistically set askedAI here — wait until user actually sends a message
|
|
113
|
-
// AskFab.onFirstMessage will PATCH askedAI:true
|
|
114
108
|
}, [guideState, g]);
|
|
115
109
|
|
|
116
110
|
const handleNextStepClick = useCallback(() => {
|
|
@@ -119,13 +113,11 @@ export default function GuideCard({ onNavigate }: GuideCardProps) {
|
|
|
119
113
|
const steps = g.done.steps;
|
|
120
114
|
if (idx < steps.length) {
|
|
121
115
|
openAskModal(steps[idx].prompt, 'guide-next');
|
|
122
|
-
// Optimistic local update — AskFab will persist to backend on first message
|
|
123
116
|
patchGuide({ nextStepIndex: idx + 1 });
|
|
124
117
|
}
|
|
125
118
|
}, [guideState, g, patchGuide]);
|
|
126
119
|
|
|
127
120
|
const handleSyncClick = useCallback(() => {
|
|
128
|
-
// Dispatch ⌘, to open Settings modal
|
|
129
121
|
window.dispatchEvent(new KeyboardEvent('keydown', { key: ',', metaKey: true, bubbles: true }));
|
|
130
122
|
}, []);
|
|
131
123
|
|
|
@@ -144,10 +136,8 @@ export default function GuideCard({ onNavigate }: GuideCardProps) {
|
|
|
144
136
|
return () => { if (autoDismissRef.current) clearTimeout(autoDismissRef.current); };
|
|
145
137
|
}, [allNextDone_, handleDismiss]);
|
|
146
138
|
|
|
147
|
-
// Don't render if no active guide
|
|
148
139
|
if (!guideState) return null;
|
|
149
140
|
|
|
150
|
-
// Hide GuideCard while walkthrough is active
|
|
151
141
|
const walkthroughActive = guideState.walkthroughStep !== undefined
|
|
152
142
|
&& guideState.walkthroughStep >= 0
|
|
153
143
|
&& guideState.walkthroughStep < walkthroughSteps.length
|
|
@@ -165,21 +155,19 @@ export default function GuideCard({ onNavigate }: GuideCardProps) {
|
|
|
165
155
|
// After all next-steps done → final state (auto-dismisses after 8s)
|
|
166
156
|
if (allDone && allNextDone) {
|
|
167
157
|
return (
|
|
168
|
-
<div className="mb-6 rounded-xl border px-5 py-4 flex items-center gap-3 animate-in fade-in duration-300"
|
|
169
|
-
style={{ background:
|
|
170
|
-
<Sparkles size={16} className="animate-spin-slow
|
|
171
|
-
<span className="text-sm font-semibold flex-1
|
|
158
|
+
<div className="mb-6 rounded-xl border border-[var(--amber)] px-5 py-4 flex items-center gap-3 animate-in fade-in duration-300"
|
|
159
|
+
style={{ background: amberSubtleBg }}>
|
|
160
|
+
<Sparkles size={16} className="animate-spin-slow text-[var(--amber)]" />
|
|
161
|
+
<span className="text-sm font-semibold flex-1 text-foreground">
|
|
172
162
|
✨ {g.done.titleFinal}
|
|
173
163
|
</span>
|
|
174
164
|
<Link
|
|
175
165
|
href="/explore"
|
|
176
|
-
className="text-xs font-medium transition-colors hover:opacity-80"
|
|
177
|
-
style={{ color: 'var(--amber)' }}
|
|
166
|
+
className="text-xs font-medium text-[var(--amber)] transition-colors hover:opacity-80"
|
|
178
167
|
>
|
|
179
168
|
{t.walkthrough.exploreCta}
|
|
180
169
|
</Link>
|
|
181
|
-
<button onClick={handleDismiss} className="p-1 rounded hover:bg-muted transition-colors"
|
|
182
|
-
style={{ color: 'var(--muted-foreground)' }}>
|
|
170
|
+
<button onClick={handleDismiss} className="p-1 rounded hover:bg-muted transition-colors text-muted-foreground">
|
|
183
171
|
<X size={14} />
|
|
184
172
|
</button>
|
|
185
173
|
</div>
|
|
@@ -190,23 +178,21 @@ export default function GuideCard({ onNavigate }: GuideCardProps) {
|
|
|
190
178
|
if (allDone) {
|
|
191
179
|
const step = nextSteps[nextIdx];
|
|
192
180
|
return (
|
|
193
|
-
<div className="mb-6 rounded-xl border px-5 py-4 animate-in fade-in duration-300"
|
|
194
|
-
style={{ background:
|
|
181
|
+
<div className="mb-6 rounded-xl border border-[var(--amber)] px-5 py-4 animate-in fade-in duration-300"
|
|
182
|
+
style={{ background: amberSubtleBg }}>
|
|
195
183
|
<div className="flex items-center gap-3">
|
|
196
|
-
<Sparkles size={16}
|
|
197
|
-
<span className="text-sm font-semibold flex-1
|
|
184
|
+
<Sparkles size={16} className="text-[var(--amber)]" />
|
|
185
|
+
<span className="text-sm font-semibold flex-1 text-foreground">
|
|
198
186
|
🎉 {g.done.title}
|
|
199
187
|
</span>
|
|
200
|
-
<button onClick={handleDismiss} className="p-1 rounded hover:bg-muted transition-colors"
|
|
201
|
-
style={{ color: 'var(--muted-foreground)' }}>
|
|
188
|
+
<button onClick={handleDismiss} className="p-1 rounded hover:bg-muted transition-colors text-muted-foreground">
|
|
202
189
|
<X size={14} />
|
|
203
190
|
</button>
|
|
204
191
|
</div>
|
|
205
192
|
{step && (
|
|
206
193
|
<button
|
|
207
194
|
onClick={handleNextStepClick}
|
|
208
|
-
className="mt-3 flex items-center gap-2 text-sm transition-colors hover:opacity-80 cursor-pointer animate-in fade-in slide-in-from-left-2 duration-300"
|
|
209
|
-
style={{ color: 'var(--amber)' }}
|
|
195
|
+
className="mt-3 flex items-center gap-2 text-sm text-[var(--amber)] transition-colors hover:opacity-80 cursor-pointer animate-in fade-in slide-in-from-left-2 duration-300"
|
|
210
196
|
>
|
|
211
197
|
<ChevronRight size={14} />
|
|
212
198
|
<span>{step.hint}</span>
|
|
@@ -218,60 +204,32 @@ export default function GuideCard({ onNavigate }: GuideCardProps) {
|
|
|
218
204
|
|
|
219
205
|
// Main guide card with 3 tasks
|
|
220
206
|
return (
|
|
221
|
-
<div className="mb-6 rounded-xl border overflow-hidden"
|
|
222
|
-
style={{ background:
|
|
207
|
+
<div className="mb-6 rounded-xl border border-[var(--amber)] overflow-hidden"
|
|
208
|
+
style={{ background: amberSubtleBg }}>
|
|
223
209
|
|
|
224
210
|
{/* Header */}
|
|
225
211
|
<div className="flex items-center gap-3 px-5 pt-4 pb-2">
|
|
226
|
-
<Sparkles size={16}
|
|
227
|
-
<span className="text-sm font-semibold flex-1 font-display
|
|
212
|
+
<Sparkles size={16} className="text-[var(--amber)]" />
|
|
213
|
+
<span className="text-sm font-semibold flex-1 font-display text-foreground">
|
|
228
214
|
{g.title}
|
|
229
215
|
</span>
|
|
230
|
-
<button onClick={handleDismiss} className="p-1 rounded hover:bg-muted transition-colors"
|
|
231
|
-
style={{ color: 'var(--muted-foreground)' }}>
|
|
216
|
+
<button onClick={handleDismiss} className="p-1 rounded hover:bg-muted transition-colors text-muted-foreground">
|
|
232
217
|
<X size={14} />
|
|
233
218
|
</button>
|
|
234
219
|
</div>
|
|
235
220
|
|
|
236
221
|
{/* Task cards */}
|
|
237
222
|
<div className="grid grid-cols-1 sm:grid-cols-3 gap-2 px-5 py-3">
|
|
238
|
-
{
|
|
239
|
-
<TaskCard
|
|
240
|
-
|
|
241
|
-
title={g.kb.title}
|
|
242
|
-
cta={g.kb.cta}
|
|
243
|
-
done={step1Done}
|
|
244
|
-
active={expanded === 'kb'}
|
|
245
|
-
onClick={() => step1Done ? null : setExpanded(expanded === 'kb' ? null : 'kb')}
|
|
246
|
-
/>
|
|
247
|
-
{/* ② Chat with AI */}
|
|
248
|
-
<TaskCard
|
|
249
|
-
icon={<MessageCircle size={16} />}
|
|
250
|
-
title={g.ai.title}
|
|
251
|
-
cta={g.ai.cta}
|
|
252
|
-
done={step2Done}
|
|
253
|
-
active={expanded === 'ai'}
|
|
254
|
-
onClick={() => {
|
|
255
|
-
if (!step2Done) handleStartAI();
|
|
256
|
-
}}
|
|
257
|
-
/>
|
|
258
|
-
{/* ③ Sync (optional) */}
|
|
259
|
-
<TaskCard
|
|
260
|
-
icon={<RefreshCw size={16} />}
|
|
261
|
-
title={g.sync.title}
|
|
262
|
-
cta={g.sync.cta}
|
|
263
|
-
done={false}
|
|
264
|
-
optional={g.sync.optional}
|
|
265
|
-
active={false}
|
|
266
|
-
onClick={handleSyncClick}
|
|
267
|
-
/>
|
|
223
|
+
<TaskCard icon={<FolderOpen size={16} />} title={g.kb.title} cta={g.kb.cta} done={step1Done} active={expanded === 'kb'} onClick={() => step1Done ? null : setExpanded(expanded === 'kb' ? null : 'kb')} />
|
|
224
|
+
<TaskCard icon={<MessageCircle size={16} />} title={g.ai.title} cta={g.ai.cta} done={step2Done} active={expanded === 'ai'} onClick={() => { if (!step2Done) handleStartAI(); }} />
|
|
225
|
+
<TaskCard icon={<RefreshCw size={16} />} title={g.sync.title} cta={g.sync.cta} done={false} optional={g.sync.optional} active={false} onClick={handleSyncClick} />
|
|
268
226
|
</div>
|
|
269
227
|
|
|
270
228
|
{/* Expanded content: Explore KB */}
|
|
271
229
|
{expanded === 'kb' && !step1Done && (
|
|
272
230
|
<div className="px-5 pb-4 animate-in slide-in-from-top-2 duration-200">
|
|
273
|
-
<div className="rounded-lg border p-4"
|
|
274
|
-
<p className="text-xs mb-3
|
|
231
|
+
<div className="rounded-lg border border-border bg-card p-4">
|
|
232
|
+
<p className="text-xs mb-3 text-muted-foreground">
|
|
275
233
|
{isEmptyTemplate ? g.kb.emptyDesc : g.kb.fullDesc}
|
|
276
234
|
</p>
|
|
277
235
|
|
|
@@ -279,12 +237,11 @@ export default function GuideCard({ onNavigate }: GuideCardProps) {
|
|
|
279
237
|
<div className="flex flex-col gap-1.5">
|
|
280
238
|
{EMPTY_FILES.map(file => (
|
|
281
239
|
<button key={file} onClick={() => handleFileOpen(file)}
|
|
282
|
-
className="text-left text-xs px-3 py-2 rounded-lg border transition-colors hover:border-amber-500/30 hover:bg-muted/50"
|
|
283
|
-
style={{ borderColor: 'var(--border)', color: 'var(--foreground)' }}>
|
|
240
|
+
className="text-left text-xs px-3 py-2 rounded-lg border border-border text-foreground transition-colors hover:border-amber-500/30 hover:bg-muted/50">
|
|
284
241
|
📄 {(g.kb.emptyFiles as Record<string, string>)[file.split('.')[0].toLowerCase()] || file}
|
|
285
242
|
</button>
|
|
286
243
|
))}
|
|
287
|
-
<p className="text-xs mt-2
|
|
244
|
+
<p className="text-xs mt-2 text-muted-foreground opacity-70">
|
|
288
245
|
{g.kb.emptyHint}
|
|
289
246
|
</p>
|
|
290
247
|
</div>
|
|
@@ -293,29 +250,27 @@ export default function GuideCard({ onNavigate }: GuideCardProps) {
|
|
|
293
250
|
<div className="grid grid-cols-2 sm:grid-cols-3 gap-1.5">
|
|
294
251
|
{Object.entries(DIR_ICONS).map(([key, icon]) => (
|
|
295
252
|
<button key={key} onClick={() => handleFileOpen(key.charAt(0).toUpperCase() + key.slice(1))}
|
|
296
|
-
className="text-left text-xs px-3 py-2 rounded-lg border transition-colors hover:border-amber-500/30 hover:bg-muted/50"
|
|
297
|
-
style={{ borderColor: 'var(--border)', color: 'var(--foreground)' }}>
|
|
253
|
+
className="text-left text-xs px-3 py-2 rounded-lg border border-border text-foreground transition-colors hover:border-amber-500/30 hover:bg-muted/50">
|
|
298
254
|
<span className="mr-1.5">{icon}</span>
|
|
299
255
|
<span className="capitalize">{key}</span>
|
|
300
|
-
<span className="block text-2xs mt-0.5
|
|
256
|
+
<span className="block text-2xs mt-0.5 text-muted-foreground">
|
|
301
257
|
{(g.kb.dirs as Record<string, string>)[key]}
|
|
302
258
|
</span>
|
|
303
259
|
</button>
|
|
304
260
|
))}
|
|
305
261
|
</div>
|
|
306
|
-
<p className="text-xs mt-3
|
|
262
|
+
<p className="text-xs mt-3 text-[var(--amber)]">
|
|
307
263
|
💡 {g.kb.instructionHint}
|
|
308
264
|
</p>
|
|
309
265
|
</>
|
|
310
266
|
)}
|
|
311
267
|
|
|
312
|
-
<div className="flex items-center justify-between mt-3 pt-3
|
|
313
|
-
<span className="text-xs
|
|
268
|
+
<div className="flex items-center justify-between mt-3 pt-3 border-t border-border">
|
|
269
|
+
<span className="text-xs text-muted-foreground">
|
|
314
270
|
{g.kb.progress(browsedCount)}
|
|
315
271
|
</span>
|
|
316
272
|
<button onClick={handleSkipKB}
|
|
317
|
-
className="text-xs px-3 py-1 rounded-lg transition-colors hover:bg-muted"
|
|
318
|
-
style={{ color: 'var(--muted-foreground)' }}>
|
|
273
|
+
className="text-xs px-3 py-1 rounded-lg text-muted-foreground transition-colors hover:bg-muted">
|
|
319
274
|
{g.skip}
|
|
320
275
|
</button>
|
|
321
276
|
</div>
|
|
@@ -326,10 +281,8 @@ export default function GuideCard({ onNavigate }: GuideCardProps) {
|
|
|
326
281
|
);
|
|
327
282
|
}
|
|
328
283
|
|
|
329
|
-
// Expose callback for AskModal integration
|
|
330
284
|
export { type GuideCardProps };
|
|
331
285
|
|
|
332
|
-
// Reusable sub-component
|
|
333
286
|
function TaskCard({ icon, title, cta, done, active, optional, onClick }: {
|
|
334
287
|
icon: React.ReactNode;
|
|
335
288
|
title: string;
|
|
@@ -348,25 +301,22 @@ function TaskCard({ icon, title, cta, done, active, optional, onClick }: {
|
|
|
348
301
|
transition-all duration-150
|
|
349
302
|
${done ? 'opacity-60' : 'hover:border-amber-500/30 hover:bg-muted/50 cursor-pointer'}
|
|
350
303
|
${active ? 'border-amber-500/40 bg-muted/50' : ''}
|
|
304
|
+
${done || active ? 'border-[var(--amber)]' : 'border-border'}
|
|
351
305
|
`}
|
|
352
|
-
style={{ borderColor: done || active ? 'var(--amber)' : 'var(--border)' }}
|
|
353
306
|
>
|
|
354
|
-
<span
|
|
355
|
-
className={done ? 'animate-in zoom-in-50 duration-300' : ''}
|
|
356
|
-
style={{ color: done ? 'var(--success)' : 'var(--amber)' }}
|
|
357
|
-
>
|
|
307
|
+
<span className={`${done ? 'animate-in zoom-in-50 duration-300 text-success' : 'text-[var(--amber)]'}`}>
|
|
358
308
|
{done ? <Check size={16} /> : icon}
|
|
359
309
|
</span>
|
|
360
|
-
<span className="text-xs font-medium
|
|
310
|
+
<span className="text-xs font-medium text-foreground">
|
|
361
311
|
{title}
|
|
362
312
|
</span>
|
|
363
313
|
{optional && (
|
|
364
|
-
<span className="text-2xs px-1.5 py-0.5 rounded-full
|
|
314
|
+
<span className="text-2xs px-1.5 py-0.5 rounded-full bg-muted text-muted-foreground">
|
|
365
315
|
{optional}
|
|
366
316
|
</span>
|
|
367
317
|
)}
|
|
368
318
|
{!done && !optional && (
|
|
369
|
-
<span className="text-2xs
|
|
319
|
+
<span className="text-2xs text-[var(--amber)]">
|
|
370
320
|
{cta} →
|
|
371
321
|
</span>
|
|
372
322
|
)}
|
|
@@ -52,8 +52,7 @@ export default function HomeContent({ recent, existingFiles }: { recent: RecentF
|
|
|
52
52
|
const formatTime = (mtime: number) => relativeTime(mtime, t.home.relativeTime);
|
|
53
53
|
|
|
54
54
|
// Only show renderers that are available (have entryPath + file exists) as quick-access chips
|
|
55
|
-
const
|
|
56
|
-
const availablePlugins = renderers.filter(r => r.entryPath && existingSet.has(r.entryPath));
|
|
55
|
+
const availablePlugins = getAllRenderers().filter(r => r.entryPath && existingSet.has(r.entryPath));
|
|
57
56
|
|
|
58
57
|
const lastFile = recent[0];
|
|
59
58
|
|
|
@@ -63,38 +62,29 @@ export default function HomeContent({ recent, existingFiles }: { recent: RecentF
|
|
|
63
62
|
{/* Hero */}
|
|
64
63
|
<div className="mb-10">
|
|
65
64
|
<div className="flex items-center gap-2 mb-3">
|
|
66
|
-
<div className="w-1 h-5 rounded-full
|
|
67
|
-
<h1 className="text-2xl font-semibold tracking-tight font-display
|
|
65
|
+
<div className="w-1 h-5 rounded-full bg-[var(--amber)]" />
|
|
66
|
+
<h1 className="text-2xl font-semibold tracking-tight font-display text-foreground">
|
|
68
67
|
MindOS
|
|
69
68
|
</h1>
|
|
70
69
|
</div>
|
|
71
|
-
<p className="text-sm leading-relaxed mb-5
|
|
70
|
+
<p className="text-sm leading-relaxed mb-5 text-muted-foreground pl-4">
|
|
72
71
|
{t.app.tagline}
|
|
73
72
|
</p>
|
|
74
73
|
|
|
75
74
|
{/* AI-first command bar */}
|
|
76
|
-
<div
|
|
77
|
-
className="w-full max-w-[620px] flex flex-col sm:flex-row items-stretch sm:items-center gap-2"
|
|
78
|
-
style={{
|
|
79
|
-
marginLeft: '1rem',
|
|
80
|
-
}}
|
|
81
|
-
>
|
|
75
|
+
<div className="w-full max-w-[620px] flex flex-col sm:flex-row items-stretch sm:items-center gap-2 ml-4">
|
|
82
76
|
{/* Ask AI (primary) */}
|
|
83
77
|
<button
|
|
84
78
|
onClick={triggerAsk}
|
|
85
79
|
title="⌘/"
|
|
86
80
|
data-walkthrough="ask-button"
|
|
87
|
-
className="flex-1 flex items-center gap-3 px-4 py-3 rounded-xl border transition-all duration-150 hover:border-amber-500/50 hover:bg-amber-500/8"
|
|
88
|
-
style={{ background: 'var(--card)', borderColor: 'var(--border)' }}
|
|
81
|
+
className="flex-1 flex items-center gap-3 px-4 py-3 rounded-xl border border-border bg-card transition-all duration-150 hover:border-amber-500/50 hover:bg-amber-500/8"
|
|
89
82
|
>
|
|
90
|
-
<Sparkles size={15}
|
|
91
|
-
<span className="text-sm flex-1 text-left"
|
|
83
|
+
<Sparkles size={15} className="shrink-0 text-[var(--amber)]" />
|
|
84
|
+
<span className="text-sm flex-1 text-left text-foreground" aria-live="polite" aria-atomic="true">
|
|
92
85
|
{suggestions[suggestionIdx]}
|
|
93
86
|
</span>
|
|
94
|
-
<kbd
|
|
95
|
-
className="hidden sm:inline-flex items-center gap-0.5 px-2 py-0.5 rounded text-xs font-mono font-medium"
|
|
96
|
-
style={{ background: 'var(--amber-dim)', color: 'var(--amber)' }}
|
|
97
|
-
>
|
|
87
|
+
<kbd className="hidden sm:inline-flex items-center gap-0.5 px-2 py-0.5 rounded text-xs font-mono font-medium bg-[var(--amber-dim)] text-[var(--amber)]">
|
|
98
88
|
⌘/
|
|
99
89
|
</kbd>
|
|
100
90
|
</button>
|
|
@@ -103,27 +93,22 @@ export default function HomeContent({ recent, existingFiles }: { recent: RecentF
|
|
|
103
93
|
<button
|
|
104
94
|
onClick={triggerSearch}
|
|
105
95
|
title="⌘K"
|
|
106
|
-
className="flex items-center gap-2 px-3 py-3 rounded-xl border text-sm transition-colors shrink-0 hover:bg-muted"
|
|
107
|
-
style={{ borderColor: 'var(--border)', color: 'var(--muted-foreground)' }}
|
|
96
|
+
className="flex items-center gap-2 px-3 py-3 rounded-xl border border-border text-sm text-muted-foreground transition-colors shrink-0 hover:bg-muted"
|
|
108
97
|
>
|
|
109
98
|
<Search size={14} />
|
|
110
99
|
<span className="hidden sm:inline">{t.home.shortcuts.searchFiles}</span>
|
|
111
|
-
<kbd className="hidden sm:inline-flex items-center px-1.5 py-0.5 rounded text-2xs font-mono
|
|
100
|
+
<kbd className="hidden sm:inline-flex items-center px-1.5 py-0.5 rounded text-2xs font-mono bg-muted">
|
|
112
101
|
⌘K
|
|
113
102
|
</kbd>
|
|
114
103
|
</button>
|
|
115
104
|
</div>
|
|
116
105
|
|
|
117
106
|
{/* Quick Actions */}
|
|
118
|
-
<div className="flex flex-wrap gap-2.5 mt-4"
|
|
107
|
+
<div className="flex flex-wrap gap-2.5 mt-4 pl-4">
|
|
119
108
|
{lastFile && (
|
|
120
109
|
<Link
|
|
121
110
|
href={`/view/${encodePath(lastFile.path)}`}
|
|
122
|
-
className="inline-flex items-center gap-2 px-3.5 py-2 rounded-lg text-sm font-medium transition-all duration-150 hover:translate-x-0.5"
|
|
123
|
-
style={{
|
|
124
|
-
background: 'var(--amber-dim)',
|
|
125
|
-
color: 'var(--amber)',
|
|
126
|
-
}}
|
|
111
|
+
className="inline-flex items-center gap-2 px-3.5 py-2 rounded-lg text-sm font-medium transition-all duration-150 hover:translate-x-0.5 bg-[var(--amber-dim)] text-[var(--amber)]"
|
|
127
112
|
>
|
|
128
113
|
<ArrowRight size={14} />
|
|
129
114
|
<span>{t.home.continueEditing}</span>
|
|
@@ -134,21 +119,14 @@ export default function HomeContent({ recent, existingFiles }: { recent: RecentF
|
|
|
134
119
|
)}
|
|
135
120
|
<Link
|
|
136
121
|
href="/view/Untitled.md"
|
|
137
|
-
className="inline-flex items-center gap-2 px-3.5 py-2 rounded-lg text-sm font-medium transition-colors"
|
|
138
|
-
style={{
|
|
139
|
-
background: 'var(--muted)',
|
|
140
|
-
color: 'var(--muted-foreground)',
|
|
141
|
-
}}
|
|
122
|
+
className="inline-flex items-center gap-2 px-3.5 py-2 rounded-lg text-sm font-medium transition-colors bg-muted text-muted-foreground"
|
|
142
123
|
>
|
|
143
124
|
<FilePlus size={14} />
|
|
144
125
|
<span>{t.home.newNote}</span>
|
|
145
126
|
</Link>
|
|
146
127
|
<Link
|
|
147
128
|
href="/explore"
|
|
148
|
-
className="inline-flex items-center gap-2 px-3.5 py-2 rounded-lg text-sm font-medium transition-all duration-150 hover:translate-x-0.5"
|
|
149
|
-
style={{
|
|
150
|
-
color: 'var(--amber)',
|
|
151
|
-
}}
|
|
129
|
+
className="inline-flex items-center gap-2 px-3.5 py-2 rounded-lg text-sm font-medium transition-all duration-150 hover:translate-x-0.5 text-[var(--amber)]"
|
|
152
130
|
>
|
|
153
131
|
<Compass size={14} />
|
|
154
132
|
<span>{t.explore.title}</span>
|
|
@@ -157,13 +135,12 @@ export default function HomeContent({ recent, existingFiles }: { recent: RecentF
|
|
|
157
135
|
|
|
158
136
|
{/* Plugin quick-access chips — only show available plugins */}
|
|
159
137
|
{availablePlugins.length > 0 && (
|
|
160
|
-
<div className="flex flex-wrap gap-1.5 mt-3"
|
|
138
|
+
<div className="flex flex-wrap gap-1.5 mt-3 pl-4">
|
|
161
139
|
{availablePlugins.map(r => (
|
|
162
140
|
<Link
|
|
163
141
|
key={r.id}
|
|
164
142
|
href={`/view/${encodePath(r.entryPath!)}`}
|
|
165
|
-
className="inline-flex items-center gap-1.5 px-2.5 py-1.5 rounded-lg text-xs transition-all duration-100 hover:bg-muted/60"
|
|
166
|
-
style={{ color: 'var(--muted-foreground)' }}
|
|
143
|
+
className="inline-flex items-center gap-1.5 px-2.5 py-1.5 rounded-lg text-xs text-muted-foreground transition-all duration-100 hover:bg-muted/60"
|
|
167
144
|
>
|
|
168
145
|
<span className="text-sm leading-none" suppressHydrationWarning>{r.icon}</span>
|
|
169
146
|
<span>{r.name}</span>
|
|
@@ -182,15 +159,15 @@ export default function HomeContent({ recent, existingFiles }: { recent: RecentF
|
|
|
182
159
|
return (
|
|
183
160
|
<section className="mb-12">
|
|
184
161
|
<div className="flex items-center gap-2 mb-5">
|
|
185
|
-
<Clock size={13}
|
|
186
|
-
<h2 className="text-xs font-semibold uppercase tracking-[0.08em] font-display
|
|
162
|
+
<Clock size={13} className="text-[var(--amber)]" />
|
|
163
|
+
<h2 className="text-xs font-semibold uppercase tracking-[0.08em] font-display text-muted-foreground">
|
|
187
164
|
{t.home.recentlyModified}
|
|
188
165
|
</h2>
|
|
189
166
|
</div>
|
|
190
167
|
|
|
191
168
|
<div className="relative pl-4">
|
|
192
169
|
{/* Timeline line */}
|
|
193
|
-
<div className="absolute left-0 top-1 bottom-1 w-px
|
|
170
|
+
<div className="absolute left-0 top-1 bottom-1 w-px bg-border" />
|
|
194
171
|
|
|
195
172
|
<div className="flex flex-col gap-0.5">
|
|
196
173
|
{visibleRecent.map(({ path: filePath, mtime }, idx) => {
|
|
@@ -201,6 +178,7 @@ export default function HomeContent({ recent, existingFiles }: { recent: RecentF
|
|
|
201
178
|
<div key={filePath} className="relative group">
|
|
202
179
|
{/* Timeline dot */}
|
|
203
180
|
<div
|
|
181
|
+
aria-hidden="true"
|
|
204
182
|
className={`absolute -left-4 top-1/2 -translate-y-1/2 rounded-full transition-all duration-150 group-hover:scale-150 ${idx === 0 ? 'w-2 h-2' : 'w-1.5 h-1.5'}`}
|
|
205
183
|
style={{
|
|
206
184
|
background: idx === 0 ? 'var(--amber)' : 'var(--border)',
|
|
@@ -212,14 +190,14 @@ export default function HomeContent({ recent, existingFiles }: { recent: RecentF
|
|
|
212
190
|
className="flex items-center gap-3 px-3 py-2.5 rounded-lg transition-all duration-100 group-hover:translate-x-0.5 hover:bg-muted"
|
|
213
191
|
>
|
|
214
192
|
{isCSV
|
|
215
|
-
? <Table size={13} className="shrink-0
|
|
216
|
-
: <FileText size={13} className="shrink-0
|
|
193
|
+
? <Table size={13} className="shrink-0 text-success" />
|
|
194
|
+
: <FileText size={13} className="shrink-0 text-muted-foreground" />
|
|
217
195
|
}
|
|
218
196
|
<div className="flex-1 min-w-0">
|
|
219
|
-
<span className="text-sm font-medium truncate block
|
|
220
|
-
{dir && <span className="text-xs truncate block
|
|
197
|
+
<span className="text-sm font-medium truncate block text-foreground" suppressHydrationWarning>{name}</span>
|
|
198
|
+
{dir && <span className="text-xs truncate block text-muted-foreground opacity-60" suppressHydrationWarning>{dir}</span>}
|
|
221
199
|
</div>
|
|
222
|
-
<span className="text-xs shrink-0 tabular-nums font-display
|
|
200
|
+
<span className="text-xs shrink-0 tabular-nums font-display text-muted-foreground opacity-50" suppressHydrationWarning>
|
|
223
201
|
{formatTime(mtime)}
|
|
224
202
|
</span>
|
|
225
203
|
</Link>
|
|
@@ -233,8 +211,7 @@ export default function HomeContent({ recent, existingFiles }: { recent: RecentF
|
|
|
233
211
|
<button
|
|
234
212
|
onClick={() => setShowAll(v => !v)}
|
|
235
213
|
aria-expanded={showAll}
|
|
236
|
-
|
|
237
|
-
className="flex items-center gap-1.5 mt-2 ml-3 text-xs font-medium transition-colors hover:opacity-80 cursor-pointer font-display"
|
|
214
|
+
className="flex items-center gap-1.5 mt-2 ml-3 text-xs font-medium text-[var(--amber)] transition-colors hover:opacity-80 cursor-pointer font-display"
|
|
238
215
|
>
|
|
239
216
|
<ChevronDown
|
|
240
217
|
size={12}
|
|
@@ -250,8 +227,8 @@ export default function HomeContent({ recent, existingFiles }: { recent: RecentF
|
|
|
250
227
|
})()}
|
|
251
228
|
|
|
252
229
|
{/* Footer */}
|
|
253
|
-
<div className="mt-16 flex items-center gap-1.5 text-xs font-display
|
|
254
|
-
<Sparkles size={10}
|
|
230
|
+
<div className="mt-16 flex items-center gap-1.5 text-xs font-display text-muted-foreground opacity-60">
|
|
231
|
+
<Sparkles size={10} className="text-[var(--amber)]" />
|
|
255
232
|
<span>{t.app.footer}</span>
|
|
256
233
|
</div>
|
|
257
234
|
</div>
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import { useState } from 'react';
|
|
4
4
|
import { useRouter } from 'next/navigation';
|
|
5
|
-
import { Sparkles, Globe, BookOpen, FileText, Loader2, GitBranch } from 'lucide-react';
|
|
5
|
+
import { Sparkles, Globe, BookOpen, FileText, Loader2, GitBranch, AlertCircle } from 'lucide-react';
|
|
6
6
|
import { useLocale } from '@/lib/LocaleContext';
|
|
7
7
|
|
|
8
8
|
type Template = 'en' | 'zh' | 'empty';
|
|
@@ -33,11 +33,13 @@ export default function OnboardingView() {
|
|
|
33
33
|
const { t } = useLocale();
|
|
34
34
|
const router = useRouter();
|
|
35
35
|
const [loading, setLoading] = useState<Template | null>(null);
|
|
36
|
+
const [error, setError] = useState<string | null>(null);
|
|
36
37
|
|
|
37
38
|
const ob = t.onboarding;
|
|
38
39
|
|
|
39
40
|
async function handleSelect(template: Template) {
|
|
40
41
|
setLoading(template);
|
|
42
|
+
setError(null);
|
|
41
43
|
try {
|
|
42
44
|
const res = await fetch('/api/init', {
|
|
43
45
|
method: 'POST',
|
|
@@ -51,6 +53,7 @@ export default function OnboardingView() {
|
|
|
51
53
|
router.refresh();
|
|
52
54
|
} catch (e) {
|
|
53
55
|
console.error('[Onboarding] init failed:', e);
|
|
56
|
+
setError(e instanceof Error ? e.message : 'Unknown error');
|
|
54
57
|
setLoading(null);
|
|
55
58
|
}
|
|
56
59
|
}
|
|
@@ -60,22 +63,33 @@ export default function OnboardingView() {
|
|
|
60
63
|
{/* Header */}
|
|
61
64
|
<div className="text-center mb-10">
|
|
62
65
|
<div className="inline-flex items-center gap-2 mb-4">
|
|
63
|
-
<Sparkles size={18}
|
|
64
|
-
<h1
|
|
65
|
-
className="text-2xl font-semibold tracking-tight font-display"
|
|
66
|
-
style={{ color: 'var(--foreground)' }}
|
|
67
|
-
>
|
|
66
|
+
<Sparkles size={18} className="text-[var(--amber)]" />
|
|
67
|
+
<h1 className="text-2xl font-semibold tracking-tight font-display text-foreground">
|
|
68
68
|
MindOS
|
|
69
69
|
</h1>
|
|
70
70
|
</div>
|
|
71
|
-
<p
|
|
72
|
-
className="text-sm leading-relaxed max-w-md mx-auto"
|
|
73
|
-
style={{ color: 'var(--muted-foreground)' }}
|
|
74
|
-
>
|
|
71
|
+
<p className="text-sm leading-relaxed max-w-md mx-auto text-muted-foreground">
|
|
75
72
|
{ob.subtitle}
|
|
76
73
|
</p>
|
|
77
74
|
</div>
|
|
78
75
|
|
|
76
|
+
{/* Error banner */}
|
|
77
|
+
{error && (
|
|
78
|
+
<div
|
|
79
|
+
role="alert"
|
|
80
|
+
className="max-w-2xl mx-auto mb-6 flex items-center gap-2.5 px-4 py-3 rounded-lg border border-destructive/30 bg-destructive/5 text-sm text-destructive"
|
|
81
|
+
>
|
|
82
|
+
<AlertCircle size={16} className="shrink-0" />
|
|
83
|
+
<span className="flex-1">{ob.initError ?? 'Initialization failed. Please try again.'} ({error})</span>
|
|
84
|
+
<button
|
|
85
|
+
onClick={() => setError(null)}
|
|
86
|
+
className="text-xs underline shrink-0 hover:opacity-80"
|
|
87
|
+
>
|
|
88
|
+
{ob.dismiss ?? 'Dismiss'}
|
|
89
|
+
</button>
|
|
90
|
+
</div>
|
|
91
|
+
)}
|
|
92
|
+
|
|
79
93
|
{/* Template cards */}
|
|
80
94
|
<div className="grid grid-cols-1 sm:grid-cols-3 gap-4 max-w-2xl mx-auto mb-10">
|
|
81
95
|
{TEMPLATES.map((tpl) => {
|
|
@@ -86,37 +100,26 @@ export default function OnboardingView() {
|
|
|
86
100
|
key={tpl.id}
|
|
87
101
|
disabled={isDisabled}
|
|
88
102
|
onClick={() => handleSelect(tpl.id)}
|
|
89
|
-
className="group relative flex flex-col items-start gap-3 p-5 rounded-xl border text-left transition-all duration-150 hover:border-amber-500/50 hover:bg-amber-500/5 disabled:opacity-60 disabled:cursor-not-allowed"
|
|
90
|
-
style={{ background: 'var(--card)', borderColor: 'var(--border)' }}
|
|
103
|
+
className="group relative flex flex-col items-start gap-3 p-5 rounded-xl border border-border bg-card text-left transition-all duration-150 hover:border-amber-500/50 hover:bg-amber-500/5 disabled:opacity-60 disabled:cursor-not-allowed"
|
|
91
104
|
>
|
|
92
105
|
{/* Icon + title */}
|
|
93
106
|
<div className="flex items-center gap-2.5 w-full">
|
|
94
|
-
<span
|
|
95
|
-
<span
|
|
96
|
-
className="text-sm font-semibold"
|
|
97
|
-
style={{ color: 'var(--foreground)' }}
|
|
98
|
-
>
|
|
107
|
+
<span className="text-[var(--amber)]">{tpl.icon}</span>
|
|
108
|
+
<span className="text-sm font-semibold text-foreground">
|
|
99
109
|
{ob.templates[tpl.id].title}
|
|
100
110
|
</span>
|
|
101
111
|
{isLoading && (
|
|
102
|
-
<Loader2 size={14} className="animate-spin ml-auto
|
|
112
|
+
<Loader2 size={14} className="animate-spin ml-auto text-[var(--amber)]" />
|
|
103
113
|
)}
|
|
104
114
|
</div>
|
|
105
115
|
|
|
106
116
|
{/* Description */}
|
|
107
|
-
<p className="text-xs leading-relaxed
|
|
117
|
+
<p className="text-xs leading-relaxed text-muted-foreground">
|
|
108
118
|
{ob.templates[tpl.id].desc}
|
|
109
119
|
</p>
|
|
110
120
|
|
|
111
121
|
{/* Directory preview */}
|
|
112
|
-
<div
|
|
113
|
-
className="w-full rounded-lg px-3 py-2 text-xs leading-relaxed font-display"
|
|
114
|
-
style={{
|
|
115
|
-
background: 'var(--muted)',
|
|
116
|
-
color: 'var(--muted-foreground)',
|
|
117
|
-
opacity: 0.8,
|
|
118
|
-
}}
|
|
119
|
-
>
|
|
122
|
+
<div className="w-full rounded-lg px-3 py-2 text-xs leading-relaxed font-display bg-muted text-muted-foreground opacity-80">
|
|
120
123
|
{tpl.dirs.map((d) => (
|
|
121
124
|
<div key={d}>{d}</div>
|
|
122
125
|
))}
|
|
@@ -127,27 +130,18 @@ export default function OnboardingView() {
|
|
|
127
130
|
</div>
|
|
128
131
|
|
|
129
132
|
{/* Import hint */}
|
|
130
|
-
<p
|
|
131
|
-
className="text-center text-xs leading-relaxed max-w-sm mx-auto font-display"
|
|
132
|
-
style={{ color: 'var(--muted-foreground)', opacity: 0.6 }}
|
|
133
|
-
>
|
|
133
|
+
<p className="text-center text-xs leading-relaxed max-w-sm mx-auto font-display text-muted-foreground opacity-60">
|
|
134
134
|
{ob.importHint}
|
|
135
135
|
</p>
|
|
136
136
|
|
|
137
137
|
{/* Sync hint card */}
|
|
138
|
-
<div
|
|
139
|
-
className="
|
|
140
|
-
style={{ borderColor: 'var(--border)', background: 'var(--card)' }}
|
|
141
|
-
>
|
|
142
|
-
<GitBranch size={16} style={{ color: 'var(--muted-foreground)', flexShrink: 0 }} />
|
|
138
|
+
<div className="max-w-md mx-auto mt-6 flex items-center gap-3 px-4 py-3 rounded-lg border border-border bg-card text-left">
|
|
139
|
+
<GitBranch size={16} className="text-muted-foreground shrink-0" />
|
|
143
140
|
<div className="min-w-0">
|
|
144
|
-
<p className="text-xs
|
|
141
|
+
<p className="text-xs text-muted-foreground">
|
|
145
142
|
{ob.syncHint ?? 'Want cross-device sync? Run'}
|
|
146
143
|
{' '}
|
|
147
|
-
<code
|
|
148
|
-
className="font-mono px-1 py-0.5 rounded select-all"
|
|
149
|
-
style={{ background: 'var(--muted)', fontSize: '11px' }}
|
|
150
|
-
>
|
|
144
|
+
<code className="font-mono px-1 py-0.5 rounded select-all bg-muted text-[11px]">
|
|
151
145
|
mindos sync init
|
|
152
146
|
</code>
|
|
153
147
|
{' '}
|
package/app/lib/i18n-en.ts
CHANGED
|
@@ -419,6 +419,8 @@ export const en = {
|
|
|
419
419
|
importHint: 'Already have notes? Set MIND_ROOT to your existing directory in Settings.',
|
|
420
420
|
syncHint: 'Want cross-device sync? Run',
|
|
421
421
|
syncHintSuffix: 'in the terminal after setup.',
|
|
422
|
+
initError: 'Initialization failed. Please try again.',
|
|
423
|
+
dismiss: 'Dismiss',
|
|
422
424
|
},
|
|
423
425
|
shortcuts: [
|
|
424
426
|
{ keys: ['⌘', 'K'], description: 'Search' },
|
package/app/lib/i18n-zh.ts
CHANGED
package/package.json
CHANGED