@geminilight/mindos 0.2.1 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (82) hide show
  1. package/app/app/api/init/route.ts +7 -41
  2. package/app/app/api/mcp/agents/route.ts +72 -0
  3. package/app/app/api/mcp/install/route.ts +95 -0
  4. package/app/app/api/mcp/status/route.ts +47 -0
  5. package/app/app/api/settings/route.ts +3 -0
  6. package/app/app/api/setup/generate-token/route.ts +23 -0
  7. package/app/app/api/setup/route.ts +81 -0
  8. package/app/app/api/skills/route.ts +208 -0
  9. package/app/app/api/sync/route.ts +54 -3
  10. package/app/app/api/update-check/route.ts +52 -0
  11. package/app/app/globals.css +12 -0
  12. package/app/app/layout.tsx +4 -2
  13. package/app/app/login/page.tsx +20 -13
  14. package/app/app/page.tsx +22 -2
  15. package/app/app/setup/page.tsx +9 -0
  16. package/app/app/view/[...path]/ViewPageClient.tsx +47 -21
  17. package/app/app/view/[...path]/loading.tsx +1 -1
  18. package/app/app/view/[...path]/not-found.tsx +101 -0
  19. package/app/components/AskFab.tsx +1 -1
  20. package/app/components/AskModal.tsx +1 -1
  21. package/app/components/Backlinks.tsx +1 -1
  22. package/app/components/Breadcrumb.tsx +13 -3
  23. package/app/components/CsvView.tsx +5 -6
  24. package/app/components/DirView.tsx +42 -21
  25. package/app/components/FindInPage.tsx +211 -0
  26. package/app/components/HomeContent.tsx +97 -44
  27. package/app/components/JsonView.tsx +1 -2
  28. package/app/components/MarkdownEditor.tsx +1 -2
  29. package/app/components/OnboardingView.tsx +6 -7
  30. package/app/components/SettingsModal.tsx +5 -2
  31. package/app/components/SetupWizard.tsx +479 -0
  32. package/app/components/Sidebar.tsx +1 -1
  33. package/app/components/UpdateBanner.tsx +101 -0
  34. package/app/components/renderers/{AgentInspectorRenderer.tsx → agent-inspector/AgentInspectorRenderer.tsx} +13 -11
  35. package/app/components/renderers/agent-inspector/manifest.ts +14 -0
  36. package/app/components/renderers/{BacklinksRenderer.tsx → backlinks/BacklinksRenderer.tsx} +6 -6
  37. package/app/components/renderers/backlinks/manifest.ts +14 -0
  38. package/app/components/renderers/config/manifest.ts +14 -0
  39. package/app/components/renderers/csv/BoardView.tsx +12 -12
  40. package/app/components/renderers/csv/ConfigPanel.tsx +7 -8
  41. package/app/components/renderers/{CsvRenderer.tsx → csv/CsvRenderer.tsx} +8 -9
  42. package/app/components/renderers/csv/GalleryView.tsx +3 -3
  43. package/app/components/renderers/csv/TableView.tsx +4 -5
  44. package/app/components/renderers/csv/manifest.ts +14 -0
  45. package/app/components/renderers/{DiffRenderer.tsx → diff/DiffRenderer.tsx} +10 -9
  46. package/app/components/renderers/diff/manifest.ts +14 -0
  47. package/app/components/renderers/{GraphRenderer.tsx → graph/GraphRenderer.tsx} +4 -5
  48. package/app/components/renderers/graph/manifest.ts +14 -0
  49. package/app/components/renderers/{SummaryRenderer.tsx → summary/SummaryRenderer.tsx} +6 -6
  50. package/app/components/renderers/summary/manifest.ts +14 -0
  51. package/app/components/renderers/{TimelineRenderer.tsx → timeline/TimelineRenderer.tsx} +6 -6
  52. package/app/components/renderers/timeline/manifest.ts +14 -0
  53. package/app/components/renderers/{TodoRenderer.tsx → todo/TodoRenderer.tsx} +2 -2
  54. package/app/components/renderers/todo/manifest.ts +14 -0
  55. package/app/components/renderers/{WorkflowRenderer.tsx → workflow/WorkflowRenderer.tsx} +13 -13
  56. package/app/components/renderers/workflow/manifest.ts +14 -0
  57. package/app/components/settings/McpTab.tsx +549 -0
  58. package/app/components/settings/SyncTab.tsx +139 -50
  59. package/app/components/settings/types.ts +1 -1
  60. package/app/data/pages/home.png +0 -0
  61. package/app/lib/i18n.ts +270 -10
  62. package/app/lib/renderers/index.ts +20 -89
  63. package/app/lib/renderers/registry.ts +4 -1
  64. package/app/lib/settings.ts +15 -1
  65. package/app/lib/template.ts +45 -0
  66. package/app/package.json +1 -0
  67. package/app/types/semver.d.ts +8 -0
  68. package/bin/cli.js +137 -24
  69. package/bin/lib/build.js +53 -18
  70. package/bin/lib/colors.js +3 -1
  71. package/bin/lib/config.js +4 -0
  72. package/bin/lib/constants.js +2 -0
  73. package/bin/lib/debug.js +10 -0
  74. package/bin/lib/startup.js +21 -20
  75. package/bin/lib/stop.js +41 -3
  76. package/bin/lib/sync.js +65 -53
  77. package/bin/lib/update-check.js +94 -0
  78. package/bin/lib/utils.js +2 -2
  79. package/package.json +1 -1
  80. package/scripts/gen-renderer-index.js +57 -0
  81. package/scripts/setup.js +117 -1
  82. /package/app/components/renderers/{ConfigRenderer.tsx → config/ConfigRenderer.tsx} +0 -0
@@ -2,9 +2,9 @@
2
2
 
3
3
  import { useState, useSyncExternalStore, useMemo } from 'react';
4
4
  import Link from 'next/link';
5
- import { FileText, Table, Folder, FolderOpen, LayoutGrid, List } from 'lucide-react';
5
+ import { FileText, Table, Folder, FolderOpen, LayoutGrid, List, FilePlus } from 'lucide-react';
6
6
  import Breadcrumb from '@/components/Breadcrumb';
7
- import { encodePath } from '@/lib/utils';
7
+ import { encodePath, relativeTime } from '@/lib/utils';
8
8
  import { FileNode } from '@/lib/types';
9
9
  import { useLocale } from '@/lib/LocaleContext';
10
10
 
@@ -57,6 +57,7 @@ function useDirViewPref() {
57
57
  export default function DirView({ dirPath, entries }: DirViewProps) {
58
58
  const [view, setView] = useDirViewPref();
59
59
  const { t } = useLocale();
60
+ const formatTime = (mtime: number) => relativeTime(mtime, t.home.relativeTime);
60
61
  const fileCounts = useMemo(() => {
61
62
  const map = new Map<string, number>();
62
63
  for (const e of entries) map.set(e.path, countFiles(e));
@@ -71,22 +72,31 @@ export default function DirView({ dirPath, entries }: DirViewProps) {
71
72
  <div className="min-w-0 flex-1">
72
73
  <Breadcrumb filePath={dirPath} />
73
74
  </div>
74
- {/* View toggle */}
75
- <div className="flex items-center gap-1 p-1 bg-muted rounded-lg shrink-0">
76
- <button
77
- onClick={() => setView('grid')}
78
- className={`p-1.5 rounded transition-colors ${view === 'grid' ? 'bg-card text-foreground shadow-sm' : 'text-muted-foreground hover:text-foreground'}`}
79
- title={t.dirView.gridView}
75
+ {/* New file + View toggle */}
76
+ <div className="flex items-center gap-2 shrink-0">
77
+ <Link
78
+ href={`/view/${encodePath(dirPath ? `${dirPath}/Untitled.md` : 'Untitled.md')}`}
79
+ className="flex items-center gap-1.5 px-2.5 py-1.5 text-xs rounded-lg transition-colors text-muted-foreground hover:text-foreground hover:bg-muted"
80
80
  >
81
- <LayoutGrid size={14} />
82
- </button>
83
- <button
84
- onClick={() => setView('list')}
85
- className={`p-1.5 rounded transition-colors ${view === 'list' ? 'bg-card text-foreground shadow-sm' : 'text-muted-foreground hover:text-foreground'}`}
86
- title={t.dirView.listView}
87
- >
88
- <List size={14} />
89
- </button>
81
+ <FilePlus size={13} />
82
+ <span className="hidden sm:inline">{t.dirView.newFile}</span>
83
+ </Link>
84
+ <div className="flex items-center gap-1 p-1 bg-muted rounded-lg">
85
+ <button
86
+ onClick={() => setView('grid')}
87
+ className={`p-1.5 rounded transition-colors ${view === 'grid' ? 'bg-card text-foreground shadow-sm' : 'text-muted-foreground hover:text-foreground'}`}
88
+ title={t.dirView.gridView}
89
+ >
90
+ <LayoutGrid size={14} />
91
+ </button>
92
+ <button
93
+ onClick={() => setView('list')}
94
+ className={`p-1.5 rounded transition-colors ${view === 'list' ? 'bg-card text-foreground shadow-sm' : 'text-muted-foreground hover:text-foreground'}`}
95
+ title={t.dirView.listView}
96
+ >
97
+ <List size={14} />
98
+ </button>
99
+ </div>
90
100
  </div>
91
101
  </div>
92
102
  </div>
@@ -102,15 +112,26 @@ export default function DirView({ dirPath, entries }: DirViewProps) {
102
112
  <Link
103
113
  key={entry.path}
104
114
  href={`/view/${encodePath(entry.path)}`}
105
- className="flex flex-col items-center gap-2 p-4 rounded-xl border border-border bg-card hover:bg-accent hover:border-border/80 transition-all duration-100 text-center"
115
+ className={
116
+ entry.type === 'directory'
117
+ ? 'flex flex-col items-center gap-1.5 p-3 rounded-xl border border-border bg-card hover:bg-accent hover:border-border/80 transition-all duration-100 text-center'
118
+ : 'flex flex-col items-center gap-2 p-4 rounded-xl border border-border bg-card hover:bg-accent hover:border-border/80 transition-all duration-100 text-center'
119
+ }
106
120
  >
107
- <FileIconLarge node={entry} />
121
+ {entry.type === 'directory'
122
+ ? <FolderOpen size={22} className="text-yellow-400" />
123
+ : <FileIconLarge node={entry} />}
108
124
  <span className="text-xs text-foreground leading-snug line-clamp-2 w-full" suppressHydrationWarning>
109
125
  {entry.name}
110
126
  </span>
111
127
  {entry.type === 'directory' && (
112
128
  <span className="text-[10px] text-muted-foreground">{t.dirView.fileCount(fileCounts.get(entry.path) ?? 0)}</span>
113
129
  )}
130
+ {entry.type === 'file' && entry.mtime && (
131
+ <span className="text-[10px] text-muted-foreground font-display" suppressHydrationWarning>
132
+ {formatTime(entry.mtime)}
133
+ </span>
134
+ )}
114
135
  </Link>
115
136
  ))}
116
137
  </div>
@@ -129,8 +150,8 @@ export default function DirView({ dirPath, entries }: DirViewProps) {
129
150
  {entry.type === 'directory' ? (
130
151
  <span className="text-xs text-muted-foreground shrink-0">{t.dirView.fileCount(fileCounts.get(entry.path) ?? 0)}</span>
131
152
  ) : entry.mtime ? (
132
- <span className="text-xs text-muted-foreground shrink-0 tabular-nums" style={{ fontFamily: "'IBM Plex Mono', monospace" }} suppressHydrationWarning>
133
- {new Date(entry.mtime).toLocaleDateString()}
153
+ <span className="text-xs text-muted-foreground shrink-0 tabular-nums font-display" suppressHydrationWarning>
154
+ {formatTime(entry.mtime)}
134
155
  </span>
135
156
  ) : null}
136
157
  </Link>
@@ -0,0 +1,211 @@
1
+ 'use client';
2
+
3
+ import { useState, useEffect, useCallback, useRef } from 'react';
4
+ import { X, ChevronUp, ChevronDown } from 'lucide-react';
5
+ import { useLocale } from '@/lib/LocaleContext';
6
+
7
+ interface FindInPageProps {
8
+ containerRef: React.RefObject<HTMLElement | null>;
9
+ onClose: () => void;
10
+ }
11
+
12
+ const MARK_CLASS = 'findip-highlight';
13
+ const MARK_ACTIVE_CLASS = 'findip-active';
14
+
15
+ export default function FindInPage({ containerRef, onClose }: FindInPageProps) {
16
+ const { t } = useLocale();
17
+ const [query, setQuery] = useState('');
18
+ const [current, setCurrent] = useState(0);
19
+ const inputRef = useRef<HTMLInputElement>(null);
20
+ const marksRef = useRef<HTMLElement[]>([]);
21
+
22
+ // Focus input on mount
23
+ useEffect(() => {
24
+ inputRef.current?.focus();
25
+ }, []);
26
+
27
+ // Clear highlights helper
28
+ const clearHighlights = useCallback(() => {
29
+ for (const el of marksRef.current) {
30
+ const parent = el.parentNode;
31
+ if (parent) {
32
+ parent.replaceChild(document.createTextNode(el.textContent || ''), el);
33
+ parent.normalize();
34
+ }
35
+ }
36
+ marksRef.current = [];
37
+ }, []);
38
+
39
+ // Search and highlight
40
+ useEffect(() => {
41
+ clearHighlights();
42
+ setCurrent(0);
43
+
44
+ const container = containerRef.current;
45
+ if (!container || !query.trim()) return;
46
+
47
+ const needle = query.toLowerCase();
48
+ const walker = document.createTreeWalker(container, NodeFilter.SHOW_TEXT, {
49
+ acceptNode(node) {
50
+ // Skip text nodes inside the find bar itself
51
+ if (node.parentElement?.closest('[data-find-in-page]')) return NodeFilter.FILTER_REJECT;
52
+ return NodeFilter.FILTER_ACCEPT;
53
+ },
54
+ });
55
+ const textNodes: Text[] = [];
56
+ while (walker.nextNode()) {
57
+ textNodes.push(walker.currentNode as Text);
58
+ }
59
+
60
+ const newMarks: HTMLElement[] = [];
61
+ for (const node of textNodes) {
62
+ const text = node.textContent || '';
63
+ const lower = text.toLowerCase();
64
+ let startIdx = 0;
65
+ const positions: number[] = [];
66
+
67
+ while (true) {
68
+ const idx = lower.indexOf(needle, startIdx);
69
+ if (idx === -1) break;
70
+ positions.push(idx);
71
+ startIdx = idx + needle.length;
72
+ }
73
+
74
+ if (positions.length === 0) continue;
75
+
76
+ // Split text node into fragments with <mark> wrappers
77
+ const parent = node.parentNode;
78
+ if (!parent) continue;
79
+
80
+ const frag = document.createDocumentFragment();
81
+ let lastEnd = 0;
82
+
83
+ for (const pos of positions) {
84
+ if (pos > lastEnd) {
85
+ frag.appendChild(document.createTextNode(text.slice(lastEnd, pos)));
86
+ }
87
+ const mark = document.createElement('mark');
88
+ mark.className = MARK_CLASS;
89
+ mark.textContent = text.slice(pos, pos + needle.length);
90
+ frag.appendChild(mark);
91
+ newMarks.push(mark);
92
+ lastEnd = pos + needle.length;
93
+ }
94
+
95
+ if (lastEnd < text.length) {
96
+ frag.appendChild(document.createTextNode(text.slice(lastEnd)));
97
+ }
98
+
99
+ parent.replaceChild(frag, node);
100
+ }
101
+
102
+ marksRef.current = newMarks;
103
+ setCurrent(newMarks.length > 0 ? 1 : 0);
104
+
105
+ // Highlight first match
106
+ if (newMarks.length > 0) {
107
+ newMarks[0].classList.add(MARK_ACTIVE_CLASS);
108
+ newMarks[0].scrollIntoView({ block: 'center', behavior: 'smooth' });
109
+ }
110
+
111
+ return () => {
112
+ // cleanup on query change is handled by next effect run
113
+ };
114
+ }, [query, containerRef, clearHighlights]);
115
+
116
+ // Cleanup on unmount
117
+ useEffect(() => {
118
+ return () => {
119
+ clearHighlights();
120
+ };
121
+ }, [clearHighlights]);
122
+
123
+ const totalMarks = marksRef.current.length;
124
+
125
+ const goTo = useCallback((index: number) => {
126
+ const marks = marksRef.current;
127
+ if (marks.length === 0) return;
128
+ // Remove active from previous
129
+ for (const m of marks) m.classList.remove(MARK_ACTIVE_CLASS);
130
+ // Wrap around
131
+ const wrapped = ((index - 1) % marks.length + marks.length) % marks.length;
132
+ marks[wrapped].classList.add(MARK_ACTIVE_CLASS);
133
+ marks[wrapped].scrollIntoView({ block: 'center', behavior: 'smooth' });
134
+ setCurrent(wrapped + 1);
135
+ }, []);
136
+
137
+ const goNext = useCallback(() => goTo(current + 1), [current, goTo]);
138
+ const goPrev = useCallback(() => goTo(current - 1), [current, goTo]);
139
+
140
+ // Keyboard handling
141
+ const handleKeyDown = useCallback((e: React.KeyboardEvent) => {
142
+ if (e.key === 'Escape') {
143
+ e.preventDefault();
144
+ onClose();
145
+ } else if (e.key === 'Enter') {
146
+ e.preventDefault();
147
+ if (e.shiftKey) goPrev();
148
+ else goNext();
149
+ }
150
+ }, [onClose, goNext, goPrev]);
151
+
152
+ return (
153
+ <>
154
+ <style>{`
155
+ mark.${MARK_CLASS} {
156
+ background: rgba(250, 204, 21, 0.3);
157
+ color: inherit;
158
+ border-radius: 2px;
159
+ padding: 0;
160
+ }
161
+ mark.${MARK_ACTIVE_CLASS} {
162
+ background: rgba(250, 204, 21, 0.7);
163
+ outline: 2px solid rgba(250, 204, 21, 0.5);
164
+ }
165
+ `}</style>
166
+ <div className="sticky top-[96px] md:top-[44px] z-30 flex justify-end px-4 md:px-6 pointer-events-none" data-find-in-page>
167
+ <div className="pointer-events-auto flex items-center gap-1.5 px-3 py-1.5 rounded-lg border border-border bg-card shadow-lg">
168
+ <input
169
+ ref={inputRef}
170
+ type="text"
171
+ value={query}
172
+ onChange={e => setQuery(e.target.value)}
173
+ onKeyDown={handleKeyDown}
174
+ placeholder={t.findInPage.placeholder}
175
+ className="w-[180px] sm:w-[220px] px-2 py-1 text-sm bg-transparent text-foreground placeholder:text-muted-foreground outline-none"
176
+ />
177
+ <span className="text-xs text-muted-foreground tabular-nums shrink-0 min-w-[48px] text-center font-display">
178
+ {query.trim()
179
+ ? totalMarks > 0
180
+ ? t.findInPage.matchCount(current, totalMarks)
181
+ : t.findInPage.noResults
182
+ : ''}
183
+ </span>
184
+ <button
185
+ onClick={goPrev}
186
+ disabled={totalMarks === 0}
187
+ className="p-1 rounded text-muted-foreground hover:text-foreground disabled:opacity-30 transition-colors"
188
+ aria-label="Previous match"
189
+ >
190
+ <ChevronUp size={14} />
191
+ </button>
192
+ <button
193
+ onClick={goNext}
194
+ disabled={totalMarks === 0}
195
+ className="p-1 rounded text-muted-foreground hover:text-foreground disabled:opacity-30 transition-colors"
196
+ aria-label="Next match"
197
+ >
198
+ <ChevronDown size={14} />
199
+ </button>
200
+ <button
201
+ onClick={onClose}
202
+ className="p-1 rounded text-muted-foreground hover:text-foreground transition-colors"
203
+ aria-label="Close find"
204
+ >
205
+ <X size={14} />
206
+ </button>
207
+ </div>
208
+ </div>
209
+ </>
210
+ );
211
+ }
@@ -1,8 +1,8 @@
1
1
  'use client';
2
2
 
3
3
  import Link from 'next/link';
4
- import { FileText, Table, Clock, Sparkles, Puzzle, ArrowRight, FilePlus, Search, ChevronDown, Terminal } from 'lucide-react';
5
- import { useState } from 'react';
4
+ import { FileText, Table, Clock, Sparkles, Puzzle, ArrowRight, FilePlus, Search, ChevronDown } from 'lucide-react';
5
+ import { useState, useEffect, useRef } from 'react';
6
6
  import { useLocale } from '@/lib/LocaleContext';
7
7
  import { encodePath, relativeTime } from '@/lib/utils';
8
8
  import { getAllRenderers } from '@/lib/renderers/registry';
@@ -14,37 +14,45 @@ interface RecentFile {
14
14
  mtime: number;
15
15
  }
16
16
 
17
- // Maps a renderer id to a canonical entry file path
18
- const RENDERER_ENTRY: Record<string, string> = {
19
- todo: 'TODO.md',
20
- csv: 'Resources/Products.csv',
21
- graph: 'README.md',
22
- timeline: 'CHANGELOG.md',
23
- backlinks: 'BACKLINKS.md',
24
- summary: 'DAILY.md',
25
- 'agent-inspector': '.agent-log.json',
26
- workflow: 'Workflow.md',
27
- 'diff-viewer': 'Agent-Diff.md',
28
- 'config-panel': 'CONFIG.json',
29
- };
30
-
31
- function deriveEntryPath(id: string): string | null {
32
- return RENDERER_ENTRY[id] ?? null;
33
- }
34
-
35
17
  function triggerSearch() {
36
- // Dispatch ⌘K to open the Sidebar's SearchModal
37
18
  window.dispatchEvent(new KeyboardEvent('keydown', { key: 'k', metaKey: true, bubbles: true }));
38
19
  }
39
20
 
40
21
  function triggerAsk() {
41
- // Dispatch ⌘/ to open the Sidebar's AskModal
42
22
  window.dispatchEvent(new KeyboardEvent('keydown', { key: '/', metaKey: true, bubbles: true }));
43
23
  }
44
24
 
45
- export default function HomeContent({ recent }: { recent: RecentFile[] }) {
25
+ export default function HomeContent({ recent, existingFiles }: { recent: RecentFile[]; existingFiles?: string[] }) {
46
26
  const { t } = useLocale();
47
27
  const [showAll, setShowAll] = useState(false);
28
+ const [suggestionIdx, setSuggestionIdx] = useState(0);
29
+ const [hintId, setHintId] = useState<string | null>(null);
30
+ const hintTimer = useRef<ReturnType<typeof setTimeout>>(null);
31
+
32
+ const suggestions = t.ask?.suggestions ?? [
33
+ 'Summarize this document',
34
+ 'List all action items and TODOs',
35
+ 'What are the key points?',
36
+ 'Find related notes on this topic',
37
+ ];
38
+
39
+ useEffect(() => {
40
+ const interval = setInterval(() => {
41
+ setSuggestionIdx(i => (i + 1) % suggestions.length);
42
+ }, 3500);
43
+ return () => clearInterval(interval);
44
+ }, [suggestions.length]);
45
+
46
+ // Cleanup hint timer on unmount
47
+ useEffect(() => () => { if (hintTimer.current) clearTimeout(hintTimer.current); }, []);
48
+
49
+ function showHint(id: string) {
50
+ if (hintTimer.current) clearTimeout(hintTimer.current);
51
+ setHintId(id);
52
+ hintTimer.current = setTimeout(() => setHintId(null), 3000);
53
+ }
54
+
55
+ const existingSet = new Set(existingFiles ?? []);
48
56
 
49
57
  // Empty knowledge base → show onboarding
50
58
  if (recent.length === 0) {
@@ -63,7 +71,7 @@ export default function HomeContent({ recent }: { recent: RecentFile[] }) {
63
71
  <div className="mb-10">
64
72
  <div className="flex items-center gap-2 mb-3">
65
73
  <div className="w-1 h-5 rounded-full" style={{ background: 'var(--amber)' }} />
66
- <h1 className="text-2xl font-semibold tracking-tight" style={{ fontFamily: "'IBM Plex Mono', monospace", color: 'var(--foreground)' }}>
74
+ <h1 className="text-2xl font-semibold tracking-tight font-display" style={{ color: 'var(--foreground)' }}>
67
75
  MindOS
68
76
  </h1>
69
77
  </div>
@@ -86,8 +94,8 @@ export default function HomeContent({ recent }: { recent: RecentFile[] }) {
86
94
  style={{ background: 'var(--card)', borderColor: 'var(--border)' }}
87
95
  >
88
96
  <Sparkles size={15} style={{ color: 'var(--amber)' }} className="shrink-0" />
89
- <span className="text-sm flex-1 text-left" style={{ color: 'var(--foreground)', fontFamily: "'IBM Plex Sans', sans-serif" }}>
90
- {t.home.shortcuts.askAI}
97
+ <span className="text-sm flex-1 text-left" style={{ color: 'var(--foreground)' }}>
98
+ {suggestions[suggestionIdx]}
91
99
  </span>
92
100
  <kbd
93
101
  className="hidden sm:inline-flex items-center gap-0.5 px-2 py-0.5 rounded text-[11px] font-mono font-medium"
@@ -102,7 +110,7 @@ export default function HomeContent({ recent }: { recent: RecentFile[] }) {
102
110
  onClick={triggerSearch}
103
111
  title="⌘K"
104
112
  className="flex items-center gap-2 px-3 py-3 rounded-xl border text-sm transition-colors shrink-0 hover:bg-muted"
105
- style={{ borderColor: 'var(--border)', color: 'var(--muted-foreground)', fontFamily: "'IBM Plex Sans', sans-serif" }}
113
+ style={{ borderColor: 'var(--border)', color: 'var(--muted-foreground)' }}
106
114
  >
107
115
  <Search size={14} />
108
116
  <span className="hidden sm:inline">{t.home.shortcuts.searchFiles}</span>
@@ -121,7 +129,6 @@ export default function HomeContent({ recent }: { recent: RecentFile[] }) {
121
129
  style={{
122
130
  background: 'var(--amber-dim)',
123
131
  color: 'var(--amber)',
124
- fontFamily: "'IBM Plex Sans', sans-serif",
125
132
  }}
126
133
  >
127
134
  <ArrowRight size={14} />
@@ -137,7 +144,6 @@ export default function HomeContent({ recent }: { recent: RecentFile[] }) {
137
144
  style={{
138
145
  background: 'var(--muted)',
139
146
  color: 'var(--muted-foreground)',
140
- fontFamily: "'IBM Plex Sans', sans-serif",
141
147
  }}
142
148
  >
143
149
  <FilePlus size={14} />
@@ -147,35 +153,81 @@ export default function HomeContent({ recent }: { recent: RecentFile[] }) {
147
153
 
148
154
  </div>
149
155
 
150
- {/* Plugins — compact 3-column grid */}
156
+ {/* Plugins */}
151
157
  {renderers.length > 0 && (
152
158
  <section className="mb-12">
153
159
  <div className="flex items-center gap-2 mb-4">
154
160
  <Puzzle size={13} style={{ color: 'var(--amber)' }} />
155
- <h2 className="text-xs font-semibold uppercase tracking-[0.08em]" style={{ color: 'var(--muted-foreground)', fontFamily: "'IBM Plex Mono', monospace" }}>
161
+ <h2 className="text-xs font-semibold uppercase tracking-[0.08em] font-display" style={{ color: 'var(--muted-foreground)' }}>
156
162
  {t.home.plugins}
157
163
  </h2>
158
- <span className="text-xs" style={{ color: 'var(--muted-foreground)', opacity: 0.5 }}>
164
+ <span className="text-xs" style={{ color: 'var(--muted-foreground)', opacity: 0.65 }}>
159
165
  {renderers.length}
160
166
  </span>
161
167
  </div>
162
168
 
163
- <div className="grid grid-cols-2 sm:grid-cols-3 gap-2">
169
+ <div className="grid grid-cols-1 sm:grid-cols-2 gap-2.5 items-start">
164
170
  {renderers.map((r) => {
165
- const entryPath = deriveEntryPath(r.id);
171
+ const entryPath = r.entryPath ?? null;
172
+ const available = !entryPath || existingSet.has(entryPath);
173
+
174
+ if (!available) {
175
+ return (
176
+ <button
177
+ key={r.id}
178
+ onClick={() => showHint(r.id)}
179
+ className="group flex flex-col gap-1.5 px-3.5 py-3 rounded-lg border transition-all opacity-60 cursor-pointer hover:opacity-80 text-left"
180
+ style={{ borderColor: 'var(--border)' }}
181
+ >
182
+ <div className="flex items-center gap-2.5">
183
+ <span className="text-base leading-none shrink-0" suppressHydrationWarning>{r.icon}</span>
184
+ <span className="text-xs font-semibold truncate font-display" style={{ color: 'var(--foreground)' }}>
185
+ {r.name}
186
+ </span>
187
+ </div>
188
+ <p className="text-[11px] leading-relaxed line-clamp-2" style={{ color: 'var(--muted-foreground)' }}>
189
+ {r.description}
190
+ </p>
191
+ {hintId === r.id ? (
192
+ <p className="text-[10px] animate-in" style={{ color: 'var(--amber)' }} role="status">
193
+ {(t.home.createToActivate ?? 'Create {file} to activate').replace('{file}', entryPath ?? '')}
194
+ </p>
195
+ ) : (
196
+ <div className="flex flex-wrap gap-1">
197
+ {r.tags.slice(0, 3).map(tag => (
198
+ <span key={tag} className="text-[10px] px-1.5 py-0.5 rounded-full" style={{ background: 'var(--muted)', color: 'var(--muted-foreground)' }}>
199
+ {tag}
200
+ </span>
201
+ ))}
202
+ </div>
203
+ )}
204
+ </button>
205
+ );
206
+ }
207
+
166
208
  return (
167
209
  <Link
168
210
  key={r.id}
169
211
  href={entryPath ? `/view/${encodePath(entryPath)}` : '#'}
170
- className="group flex items-center gap-2.5 px-3 py-2.5 rounded-lg border transition-all hover:border-amber-500/30 hover:bg-muted/50"
212
+ className="group flex flex-col gap-1.5 px-3.5 py-3 rounded-lg border transition-all hover:border-amber-500/30 hover:bg-muted/50"
171
213
  style={{ borderColor: 'var(--border)' }}
172
214
  >
173
- <span className="text-base leading-none shrink-0" suppressHydrationWarning>{r.icon}</span>
174
- <div className="flex-1 min-w-0">
175
- <span className="text-xs font-semibold truncate block" style={{ color: 'var(--foreground)', fontFamily: "'IBM Plex Sans', sans-serif" }}>
215
+ <div className="flex items-center gap-2.5">
216
+ <span className="text-base leading-none shrink-0" suppressHydrationWarning>{r.icon}</span>
217
+ <span className="text-xs font-semibold truncate font-display" style={{ color: 'var(--foreground)' }}>
176
218
  {r.name}
177
219
  </span>
178
220
  </div>
221
+ <p className="text-[11px] leading-relaxed line-clamp-2" style={{ color: 'var(--muted-foreground)' }}>
222
+ {r.description}
223
+ </p>
224
+ <div className="flex flex-wrap gap-1">
225
+ {r.tags.slice(0, 3).map(tag => (
226
+ <span key={tag} className="text-[10px] px-1.5 py-0.5 rounded-full" style={{ background: 'var(--muted)', color: 'var(--muted-foreground)' }}>
227
+ {tag}
228
+ </span>
229
+ ))}
230
+ </div>
179
231
  </Link>
180
232
  );
181
233
  })}
@@ -193,7 +245,7 @@ export default function HomeContent({ recent }: { recent: RecentFile[] }) {
193
245
  <section className="mb-12">
194
246
  <div className="flex items-center gap-2 mb-5">
195
247
  <Clock size={13} style={{ color: 'var(--amber)' }} />
196
- <h2 className="text-xs font-semibold uppercase tracking-[0.08em]" style={{ color: 'var(--muted-foreground)', fontFamily: "'IBM Plex Mono', monospace" }}>
248
+ <h2 className="text-xs font-semibold uppercase tracking-[0.08em] font-display" style={{ color: 'var(--muted-foreground)' }}>
197
249
  {t.home.recentlyModified}
198
250
  </h2>
199
251
  </div>
@@ -211,7 +263,7 @@ export default function HomeContent({ recent }: { recent: RecentFile[] }) {
211
263
  <div key={filePath} className="relative group">
212
264
  {/* Timeline dot */}
213
265
  <div
214
- className="absolute -left-4 top-1/2 -translate-y-1/2 w-1.5 h-1.5 rounded-full transition-all duration-150 group-hover:scale-150"
266
+ 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'}`}
215
267
  style={{
216
268
  background: idx === 0 ? 'var(--amber)' : 'var(--border)',
217
269
  outline: idx === 0 ? '2px solid var(--amber-dim)' : 'none',
@@ -229,7 +281,7 @@ export default function HomeContent({ recent }: { recent: RecentFile[] }) {
229
281
  <span className="text-sm font-medium truncate block" style={{ color: 'var(--foreground)' }} suppressHydrationWarning>{name}</span>
230
282
  {dir && <span className="text-xs truncate block" style={{ color: 'var(--muted-foreground)', opacity: 0.6 }} suppressHydrationWarning>{dir}</span>}
231
283
  </div>
232
- <span className="text-xs shrink-0 tabular-nums" style={{ color: 'var(--muted-foreground)', opacity: 0.5, fontFamily: "'IBM Plex Mono', monospace" }} suppressHydrationWarning>
284
+ <span className="text-xs shrink-0 tabular-nums font-display" style={{ color: 'var(--muted-foreground)', opacity: 0.5 }} suppressHydrationWarning>
233
285
  {formatTime(mtime)}
234
286
  </span>
235
287
  </Link>
@@ -242,8 +294,9 @@ export default function HomeContent({ recent }: { recent: RecentFile[] }) {
242
294
  {hasMore && (
243
295
  <button
244
296
  onClick={() => setShowAll(v => !v)}
245
- className="flex items-center gap-1.5 mt-2 ml-3 text-xs font-medium transition-colors hover:opacity-80 cursor-pointer"
246
- style={{ color: 'var(--amber)', fontFamily: "'IBM Plex Mono', monospace" }}
297
+ aria-expanded={showAll}
298
+ style={{ color: 'var(--amber)' }}
299
+ className="flex items-center gap-1.5 mt-2 ml-3 text-xs font-medium transition-colors hover:opacity-80 cursor-pointer font-display"
247
300
  >
248
301
  <ChevronDown
249
302
  size={12}
@@ -259,7 +312,7 @@ export default function HomeContent({ recent }: { recent: RecentFile[] }) {
259
312
  })()}
260
313
 
261
314
  {/* Footer */}
262
- <div className="mt-16 flex items-center gap-1.5 text-xs" style={{ color: 'var(--muted-foreground)', opacity: 0.4, fontFamily: "'IBM Plex Mono', monospace" }}>
315
+ <div className="mt-16 flex items-center gap-1.5 text-xs font-display" style={{ color: 'var(--muted-foreground)', opacity: 0.6 }}>
263
316
  <Sparkles size={10} style={{ color: 'var(--amber)' }} />
264
317
  <span>{t.app.footer}</span>
265
318
  </div>
@@ -17,8 +17,7 @@ export default function JsonView({ content }: JsonViewProps) {
17
17
 
18
18
  return (
19
19
  <pre
20
- className="rounded-xl border border-border bg-card px-4 py-3 overflow-x-auto text-sm leading-relaxed"
21
- style={{ fontFamily: "'IBM Plex Mono', monospace" }}
20
+ className="rounded-xl border border-border bg-card px-4 py-3 overflow-x-auto text-sm leading-relaxed font-display"
22
21
  suppressHydrationWarning
23
22
  >
24
23
  <code>{pretty}</code>
@@ -35,12 +35,11 @@ export default function MarkdownEditor({ value, onChange, viewMode, onViewModeCh
35
35
  <button
36
36
  key={m.id}
37
37
  onClick={() => onViewModeChange(m.id)}
38
- className={`flex items-center gap-1.5 px-2.5 py-1.5 rounded text-xs font-medium transition-colors ${
38
+ className={`flex items-center gap-1.5 px-2.5 py-1.5 rounded text-xs font-medium transition-colors font-display ${
39
39
  viewMode === m.id
40
40
  ? 'bg-card text-foreground shadow-sm'
41
41
  : 'text-muted-foreground hover:text-foreground'
42
42
  }`}
43
- style={{ fontFamily: "'IBM Plex Mono', monospace" }}
44
43
  >
45
44
  {m.icon}
46
45
  {m.label}
@@ -62,8 +62,8 @@ export default function OnboardingView() {
62
62
  <div className="inline-flex items-center gap-2 mb-4">
63
63
  <Sparkles size={18} style={{ color: 'var(--amber)' }} />
64
64
  <h1
65
- className="text-2xl font-semibold tracking-tight"
66
- style={{ fontFamily: "'IBM Plex Mono', monospace", color: 'var(--foreground)' }}
65
+ className="text-2xl font-semibold tracking-tight font-display"
66
+ style={{ color: 'var(--foreground)' }}
67
67
  >
68
68
  MindOS
69
69
  </h1>
@@ -94,7 +94,7 @@ export default function OnboardingView() {
94
94
  <span style={{ color: 'var(--amber)' }}>{tpl.icon}</span>
95
95
  <span
96
96
  className="text-sm font-semibold"
97
- style={{ color: 'var(--foreground)', fontFamily: "'IBM Plex Sans', sans-serif" }}
97
+ style={{ color: 'var(--foreground)' }}
98
98
  >
99
99
  {ob.templates[tpl.id].title}
100
100
  </span>
@@ -110,10 +110,9 @@ export default function OnboardingView() {
110
110
 
111
111
  {/* Directory preview */}
112
112
  <div
113
- className="w-full rounded-lg px-3 py-2 text-[11px] leading-relaxed"
113
+ className="w-full rounded-lg px-3 py-2 text-[11px] leading-relaxed font-display"
114
114
  style={{
115
115
  background: 'var(--muted)',
116
- fontFamily: "'IBM Plex Mono', monospace",
117
116
  color: 'var(--muted-foreground)',
118
117
  opacity: 0.8,
119
118
  }}
@@ -129,8 +128,8 @@ export default function OnboardingView() {
129
128
 
130
129
  {/* Import hint */}
131
130
  <p
132
- className="text-center text-xs leading-relaxed max-w-sm mx-auto"
133
- style={{ color: 'var(--muted-foreground)', opacity: 0.6, fontFamily: "'IBM Plex Mono', monospace" }}
131
+ className="text-center text-xs leading-relaxed max-w-sm mx-auto font-display"
132
+ style={{ color: 'var(--muted-foreground)', opacity: 0.6 }}
134
133
  >
135
134
  {ob.importHint}
136
135
  </p>