total-recall-brain 3.12.1 → 3.13.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.
- package/frontend/package.json +2 -1
- package/frontend/src/App.tsx +44 -0
- package/frontend/src/api.ts +101 -1
- package/frontend/src/components/UsageChart.tsx +78 -0
- package/frontend/src/pages/DesignDocsPage.tsx +614 -0
- package/frontend/src/pages/InstructionsPage.tsx +583 -0
- package/frontend/src/pages/ModelsPage.tsx +280 -23
- package/frontend/src/pages/OkfPage.tsx +535 -0
- package/frontend/src/pages/OpenWikiPage.tsx +472 -0
- package/frontend/src/pages/SettingsPage.tsx +183 -0
- package/frontend/src/pages/SkillsPage.tsx +1054 -37
- package/frontend/src/pages/UsagePage.tsx +254 -1
- package/frontend/src/types.ts +160 -0
- package/package.json +1 -1
- package/src/cli/init.mjs +2 -2
- package/src/server/api.mjs +2 -0
- package/src/server/auth.mjs +2 -2
- package/src/server/rest.mjs +207 -43
- package/src/server/route-manifest.json +12 -0
- package/src/server/routes/_shared.mjs +1 -1
|
@@ -0,0 +1,614 @@
|
|
|
1
|
+
import { useState, useEffect } from 'react';
|
|
2
|
+
import { fetchDesignDocs, fetchDesignDocContent } from '../api';
|
|
3
|
+
|
|
4
|
+
// ─── Types ──────────────────────────────────────────────────────────────────────
|
|
5
|
+
|
|
6
|
+
interface DocItem {
|
|
7
|
+
name: string;
|
|
8
|
+
path: string;
|
|
9
|
+
icon: string;
|
|
10
|
+
category: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
interface KanbanCard {
|
|
14
|
+
id: string;
|
|
15
|
+
name: string;
|
|
16
|
+
emoji: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
interface KanbanColumn {
|
|
20
|
+
id: string;
|
|
21
|
+
title: string;
|
|
22
|
+
accent: string;
|
|
23
|
+
cards: KanbanCard[];
|
|
24
|
+
count?: number;
|
|
25
|
+
collapsible?: boolean;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// ─── Static Data ────────────────────────────────────────────────────────────────
|
|
29
|
+
|
|
30
|
+
const CORE_DOCS: DocItem[] = [
|
|
31
|
+
{ name: 'ARCHITECTURE.md', path: 'docs/ARCHITECTURE.md', icon: '🏗️', category: 'Core Docs' },
|
|
32
|
+
{ name: 'SSSS.md', path: 'docs/SSSS.md', icon: '🔐', category: 'Core Docs' },
|
|
33
|
+
{ name: 'IDE_INTEGRATION.md', path: 'docs/IDE_INTEGRATION.md', icon: '🔌', category: 'Core Docs' },
|
|
34
|
+
{ name: 'CHANGELOG.md', path: 'docs/CHANGELOG.md', icon: '📋', category: 'Core Docs' },
|
|
35
|
+
];
|
|
36
|
+
|
|
37
|
+
const DEV_GUIDES: DocItem[] = [
|
|
38
|
+
{ name: 'README.md', path: 'README.md', icon: '📖', category: 'Developer Guides' },
|
|
39
|
+
];
|
|
40
|
+
|
|
41
|
+
const KANBAN_COLUMNS: KanbanColumn[] = [
|
|
42
|
+
{
|
|
43
|
+
id: 'in-progress',
|
|
44
|
+
title: 'In Progress',
|
|
45
|
+
accent: '#6c5ce7',
|
|
46
|
+
cards: [
|
|
47
|
+
{ id: 'auto-update', name: 'Auto Update Feature', emoji: '🔄' },
|
|
48
|
+
{ id: 'compaction-cache', name: 'Compaction Cache Bypass', emoji: '⚡' },
|
|
49
|
+
{ id: 'sovereign-policy', name: 'Sovereign Policy Enforcement', emoji: '🛡️' },
|
|
50
|
+
],
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
id: 'planned',
|
|
54
|
+
title: 'Planned',
|
|
55
|
+
accent: '#74b9ff',
|
|
56
|
+
cards: [
|
|
57
|
+
{ id: 'system-resilience', name: 'System Resilience', emoji: '🧱' },
|
|
58
|
+
{ id: 'gpu-intelligence', name: 'GPU Intelligence Network', emoji: '🧠' },
|
|
59
|
+
{ id: 'living-memory', name: 'Living Memory Capsule (UltraChat)', emoji: '💊' },
|
|
60
|
+
{ id: 'expo-mobile', name: 'Expo Mobile (stub)', emoji: '📱' },
|
|
61
|
+
],
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
id: 'completed',
|
|
65
|
+
title: 'Completed',
|
|
66
|
+
accent: '#00cec9',
|
|
67
|
+
count: 35,
|
|
68
|
+
collapsible: true,
|
|
69
|
+
cards: [
|
|
70
|
+
{ id: 'okf-integration', name: 'OKF Integration', emoji: '✅' },
|
|
71
|
+
{ id: 'byom-architecture', name: 'BYOM Architecture', emoji: '✅' },
|
|
72
|
+
{ id: 'cli-performance', name: 'CLI Performance Optimization', emoji: '✅' },
|
|
73
|
+
{ id: 'ssss-sovereign', name: 'SSSS Sovereign AI OS', emoji: '✅' },
|
|
74
|
+
{ id: 'embedding-pipeline', name: 'Embedding Pipeline v2', emoji: '✅' },
|
|
75
|
+
{ id: 'surface-compiler', name: 'Surface Compiler', emoji: '✅' },
|
|
76
|
+
],
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
id: 'archived',
|
|
80
|
+
title: 'Archived',
|
|
81
|
+
accent: '#636e72',
|
|
82
|
+
count: 3,
|
|
83
|
+
collapsible: true,
|
|
84
|
+
cards: [
|
|
85
|
+
{ id: 'legacy-sync', name: 'Legacy Sync Protocol', emoji: '📦' },
|
|
86
|
+
{ id: 'deprecated-cli', name: 'Deprecated CLI v1', emoji: '📦' },
|
|
87
|
+
{ id: 'old-auth', name: 'Old Auth Flow', emoji: '📦' },
|
|
88
|
+
],
|
|
89
|
+
},
|
|
90
|
+
];
|
|
91
|
+
|
|
92
|
+
// ─── Simple Markdown Renderer ───────────────────────────────────────────────────
|
|
93
|
+
|
|
94
|
+
function renderMarkdown(md: string): JSX.Element {
|
|
95
|
+
if (!md) return <></>;
|
|
96
|
+
|
|
97
|
+
let html = md
|
|
98
|
+
.replace(/&/g, '&')
|
|
99
|
+
.replace(/</g, '<')
|
|
100
|
+
.replace(/>/g, '>');
|
|
101
|
+
|
|
102
|
+
// Tables (basic pipe tables) — process before other inline transforms
|
|
103
|
+
html = html.replace(
|
|
104
|
+
/^(\|.+\|)\n(\|[\s:|-]+\|)\n((?:\|.+\|\n?)+)/gm,
|
|
105
|
+
(_match, headerRow: string, _separator: string, bodyRows: string) => {
|
|
106
|
+
const headers = headerRow.split('|').filter((c: string) => c.trim());
|
|
107
|
+
const rows = bodyRows.trim().split('\n').map((r: string) =>
|
|
108
|
+
r.split('|').filter((c: string) => c.trim())
|
|
109
|
+
);
|
|
110
|
+
const thCells = headers.map((h: string) =>
|
|
111
|
+
`<th style="padding:8px 12px;text-align:left;border-bottom:2px solid var(--border-hover);font-weight:600;">${h.trim()}</th>`
|
|
112
|
+
).join('');
|
|
113
|
+
const tbodyRows = rows.map((row: string[]) => {
|
|
114
|
+
const cells = row.map((c: string) =>
|
|
115
|
+
`<td style="padding:8px 12px;border-bottom:1px solid var(--border);">${c.trim()}</td>`
|
|
116
|
+
).join('');
|
|
117
|
+
return `<tr>${cells}</tr>`;
|
|
118
|
+
}).join('');
|
|
119
|
+
return `<table style="width:100%;border-collapse:collapse;margin:16px 0;font-size:13px;"><thead><tr>${thCells}</tr></thead><tbody>${tbodyRows}</tbody></table>`;
|
|
120
|
+
}
|
|
121
|
+
);
|
|
122
|
+
|
|
123
|
+
// Headings
|
|
124
|
+
html = html.replace(/^#### (.*$)/gm, '<h4 style="margin:20px 0 8px;font-size:14px;color:var(--text-primary);">$1</h4>');
|
|
125
|
+
html = html.replace(/^### (.*$)/gm, '<h3 style="margin:24px 0 10px;font-size:16px;color:var(--text-primary);">$1</h3>');
|
|
126
|
+
html = html.replace(/^## (.*$)/gm, '<h2 style="margin:28px 0 12px;font-size:19px;color:var(--text-primary);border-bottom:1px solid var(--border);padding-bottom:8px;">$1</h2>');
|
|
127
|
+
html = html.replace(/^# (.*$)/gm, '<h1 style="margin:0 0 16px;font-size:24px;color:var(--text-primary);">$1</h1>');
|
|
128
|
+
|
|
129
|
+
// Code blocks (fenced)
|
|
130
|
+
html = html.replace(
|
|
131
|
+
/```[\w]*\n([\s\S]*?)```/g,
|
|
132
|
+
'<pre style="background:rgba(0,0,0,0.35);padding:14px 16px;border-radius:var(--radius-sm);border:1px solid var(--border);font-family:var(--font-mono);font-size:12.5px;overflow-x:auto;margin:12px 0;line-height:1.5;"><code>$1</code></pre>'
|
|
133
|
+
);
|
|
134
|
+
|
|
135
|
+
// Bold & Italic
|
|
136
|
+
html = html.replace(/\*\*\*(.*?)\*\*\*/g, '<strong><em>$1</em></strong>');
|
|
137
|
+
html = html.replace(/\*\*(.*?)\*\*/g, '<strong style="color:var(--text-primary);">$1</strong>');
|
|
138
|
+
html = html.replace(/(?<!\*)\*(?!\*)(.+?)(?<!\*)\*(?!\*)/g, '<em>$1</em>');
|
|
139
|
+
|
|
140
|
+
// Inline code
|
|
141
|
+
html = html.replace(
|
|
142
|
+
/`([^`]+)`/g,
|
|
143
|
+
'<code style="background:rgba(108,92,231,0.12);color:#b8b0f0;padding:2px 6px;border-radius:4px;font-family:var(--font-mono);font-size:12px;">$1</code>'
|
|
144
|
+
);
|
|
145
|
+
|
|
146
|
+
// Blockquotes
|
|
147
|
+
html = html.replace(
|
|
148
|
+
/^> (.*$)/gm,
|
|
149
|
+
'<blockquote style="border-left:3px solid var(--accent);padding:8px 14px;color:var(--text-secondary);margin:12px 0;background:rgba(108,92,231,0.05);border-radius:0 var(--radius-sm) var(--radius-sm) 0;">$1</blockquote>'
|
|
150
|
+
);
|
|
151
|
+
|
|
152
|
+
// Unordered lists
|
|
153
|
+
html = html.replace(
|
|
154
|
+
/^- (.*$)/gm,
|
|
155
|
+
'<li style="margin-left:20px;list-style-type:disc;margin-bottom:4px;">$1</li>'
|
|
156
|
+
);
|
|
157
|
+
|
|
158
|
+
// Ordered lists
|
|
159
|
+
html = html.replace(
|
|
160
|
+
/^\d+\. (.*$)/gm,
|
|
161
|
+
'<li style="margin-left:20px;list-style-type:decimal;margin-bottom:4px;">$1</li>'
|
|
162
|
+
);
|
|
163
|
+
|
|
164
|
+
// Line breaks
|
|
165
|
+
html = html.replace(/\n/g, '<br />');
|
|
166
|
+
|
|
167
|
+
return <div dangerouslySetInnerHTML={{ __html: html }} className="markdown-preview-block" />;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// ─── Component ──────────────────────────────────────────────────────────────────
|
|
171
|
+
|
|
172
|
+
export default function DesignDocsPage() {
|
|
173
|
+
const [activeTab, setActiveTab] = useState<'architecture' | 'board'>('architecture');
|
|
174
|
+
|
|
175
|
+
// Architecture docs state
|
|
176
|
+
const [selectedDoc, setSelectedDoc] = useState<DocItem>(CORE_DOCS[0]);
|
|
177
|
+
const [docContent, setDocContent] = useState<string>('');
|
|
178
|
+
const [docLoading, setDocLoading] = useState(false);
|
|
179
|
+
const [docError, setDocError] = useState<string | null>(null);
|
|
180
|
+
const [, setDocsFromApi] = useState<Array<{ name: string; path: string; size: number; modified: string; category: string }>>([]);
|
|
181
|
+
|
|
182
|
+
// Kanban state
|
|
183
|
+
const [expandedColumns, setExpandedColumns] = useState<Record<string, boolean>>({});
|
|
184
|
+
|
|
185
|
+
// Load docs list on mount
|
|
186
|
+
useEffect(() => {
|
|
187
|
+
async function loadDocsList() {
|
|
188
|
+
try {
|
|
189
|
+
const data = await fetchDesignDocs();
|
|
190
|
+
setDocsFromApi(data);
|
|
191
|
+
} catch {
|
|
192
|
+
// Silently fall back to static doc list
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
loadDocsList();
|
|
196
|
+
}, []);
|
|
197
|
+
|
|
198
|
+
// Load selected doc content
|
|
199
|
+
useEffect(() => {
|
|
200
|
+
if (activeTab !== 'architecture' || !selectedDoc) return;
|
|
201
|
+
let cancelled = false;
|
|
202
|
+
|
|
203
|
+
async function loadContent() {
|
|
204
|
+
setDocLoading(true);
|
|
205
|
+
setDocError(null);
|
|
206
|
+
try {
|
|
207
|
+
const data = await fetchDesignDocContent(selectedDoc.path);
|
|
208
|
+
if (!cancelled) setDocContent(data.content);
|
|
209
|
+
} catch {
|
|
210
|
+
if (!cancelled) {
|
|
211
|
+
setDocError(selectedDoc.path);
|
|
212
|
+
setDocContent('');
|
|
213
|
+
}
|
|
214
|
+
} finally {
|
|
215
|
+
if (!cancelled) setDocLoading(false);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
loadContent();
|
|
219
|
+
return () => { cancelled = true; };
|
|
220
|
+
}, [selectedDoc, activeTab]);
|
|
221
|
+
|
|
222
|
+
const toggleColumn = (colId: string) => {
|
|
223
|
+
setExpandedColumns(prev => ({ ...prev, [colId]: !prev[colId] }));
|
|
224
|
+
};
|
|
225
|
+
|
|
226
|
+
// ─── Sidebar Section Renderer ───────────────────────────────────────────────
|
|
227
|
+
|
|
228
|
+
const renderSidebarSection = (title: string, items: DocItem[]) => (
|
|
229
|
+
<div style={{ marginBottom: 20 }}>
|
|
230
|
+
<div
|
|
231
|
+
style={{
|
|
232
|
+
fontSize: 10,
|
|
233
|
+
fontWeight: 700,
|
|
234
|
+
textTransform: 'uppercase',
|
|
235
|
+
letterSpacing: '0.08em',
|
|
236
|
+
color: 'var(--text-tertiary)',
|
|
237
|
+
padding: '0 12px',
|
|
238
|
+
marginBottom: 8,
|
|
239
|
+
}}
|
|
240
|
+
>
|
|
241
|
+
{title}
|
|
242
|
+
</div>
|
|
243
|
+
{items.map(item => {
|
|
244
|
+
const isActive = selectedDoc.path === item.path;
|
|
245
|
+
return (
|
|
246
|
+
<button
|
|
247
|
+
id={`doc-nav-${item.name.replace(/\./g, '-').toLowerCase()}`}
|
|
248
|
+
key={item.path}
|
|
249
|
+
onClick={() => setSelectedDoc(item)}
|
|
250
|
+
style={{
|
|
251
|
+
display: 'flex',
|
|
252
|
+
alignItems: 'center',
|
|
253
|
+
gap: 10,
|
|
254
|
+
width: '100%',
|
|
255
|
+
textAlign: 'left',
|
|
256
|
+
padding: '9px 12px',
|
|
257
|
+
borderRadius: 'var(--radius-sm)',
|
|
258
|
+
background: isActive ? 'var(--accent-muted)' : 'transparent',
|
|
259
|
+
color: isActive ? 'var(--accent-hover)' : 'var(--text-secondary)',
|
|
260
|
+
border: 'none',
|
|
261
|
+
cursor: 'pointer',
|
|
262
|
+
transition: 'var(--transition-normal)',
|
|
263
|
+
fontSize: 13,
|
|
264
|
+
fontWeight: isActive ? 600 : 400,
|
|
265
|
+
}}
|
|
266
|
+
>
|
|
267
|
+
<span style={{ fontSize: 15, flexShrink: 0 }}>{item.icon}</span>
|
|
268
|
+
<span style={{ overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
|
|
269
|
+
{item.name}
|
|
270
|
+
</span>
|
|
271
|
+
</button>
|
|
272
|
+
);
|
|
273
|
+
})}
|
|
274
|
+
</div>
|
|
275
|
+
);
|
|
276
|
+
|
|
277
|
+
// ─── Architecture Tab ───────────────────────────────────────────────────────
|
|
278
|
+
|
|
279
|
+
const renderArchitectureTab = () => (
|
|
280
|
+
<div
|
|
281
|
+
id="architecture-panel"
|
|
282
|
+
style={{ display: 'flex', gap: 24, minHeight: '65vh' }}
|
|
283
|
+
>
|
|
284
|
+
{/* Sidebar */}
|
|
285
|
+
<div
|
|
286
|
+
id="docs-sidebar"
|
|
287
|
+
style={{
|
|
288
|
+
width: 220,
|
|
289
|
+
flexShrink: 0,
|
|
290
|
+
background: 'var(--bg-secondary)',
|
|
291
|
+
border: '1px solid var(--border)',
|
|
292
|
+
borderRadius: 'var(--radius-md)',
|
|
293
|
+
padding: '16px 8px',
|
|
294
|
+
overflowY: 'auto',
|
|
295
|
+
}}
|
|
296
|
+
>
|
|
297
|
+
{renderSidebarSection('Core Docs', CORE_DOCS)}
|
|
298
|
+
{renderSidebarSection('Developer Guides', DEV_GUIDES)}
|
|
299
|
+
</div>
|
|
300
|
+
|
|
301
|
+
{/* Main Viewer */}
|
|
302
|
+
<div
|
|
303
|
+
id="docs-viewer"
|
|
304
|
+
style={{
|
|
305
|
+
flex: 1,
|
|
306
|
+
background: 'var(--bg-secondary)',
|
|
307
|
+
border: '1px solid var(--border)',
|
|
308
|
+
borderRadius: 'var(--radius-md)',
|
|
309
|
+
padding: '24px 32px',
|
|
310
|
+
overflowY: 'auto',
|
|
311
|
+
maxHeight: '75vh',
|
|
312
|
+
}}
|
|
313
|
+
>
|
|
314
|
+
{/* Breadcrumb */}
|
|
315
|
+
<div
|
|
316
|
+
id="docs-breadcrumb"
|
|
317
|
+
style={{
|
|
318
|
+
display: 'flex',
|
|
319
|
+
alignItems: 'center',
|
|
320
|
+
gap: 6,
|
|
321
|
+
fontSize: 12,
|
|
322
|
+
color: 'var(--text-tertiary)',
|
|
323
|
+
marginBottom: 20,
|
|
324
|
+
fontFamily: 'var(--font-mono)',
|
|
325
|
+
}}
|
|
326
|
+
>
|
|
327
|
+
<span>Total Recall</span>
|
|
328
|
+
<span style={{ color: 'var(--text-tertiary)' }}>›</span>
|
|
329
|
+
<span>docs</span>
|
|
330
|
+
<span style={{ color: 'var(--text-tertiary)' }}>›</span>
|
|
331
|
+
<span style={{ color: 'var(--accent)' }}>{selectedDoc.name}</span>
|
|
332
|
+
</div>
|
|
333
|
+
|
|
334
|
+
{/* Content */}
|
|
335
|
+
{docLoading ? (
|
|
336
|
+
<div style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>
|
|
337
|
+
{[200, 120, 280, 80, 160].map((w, i) => (
|
|
338
|
+
<div
|
|
339
|
+
key={i}
|
|
340
|
+
className="skeleton"
|
|
341
|
+
style={{ height: 16, width: `${w}px`, maxWidth: '100%', borderRadius: 4 }}
|
|
342
|
+
/>
|
|
343
|
+
))}
|
|
344
|
+
</div>
|
|
345
|
+
) : docError ? (
|
|
346
|
+
<div
|
|
347
|
+
id="doc-fallback-message"
|
|
348
|
+
style={{
|
|
349
|
+
background: 'var(--warning-muted)',
|
|
350
|
+
border: '1px solid rgba(253, 203, 110, 0.2)',
|
|
351
|
+
borderRadius: 'var(--radius-sm)',
|
|
352
|
+
padding: '20px 24px',
|
|
353
|
+
color: 'var(--text-secondary)',
|
|
354
|
+
lineHeight: 1.7,
|
|
355
|
+
}}
|
|
356
|
+
>
|
|
357
|
+
<div style={{ marginBottom: 8, color: 'var(--warning)', fontWeight: 600 }}>
|
|
358
|
+
📄 Document not available via API
|
|
359
|
+
</div>
|
|
360
|
+
<div>
|
|
361
|
+
This document is available in your project at{' '}
|
|
362
|
+
<code
|
|
363
|
+
style={{
|
|
364
|
+
background: 'rgba(108,92,231,0.12)',
|
|
365
|
+
color: '#b8b0f0',
|
|
366
|
+
padding: '2px 6px',
|
|
367
|
+
borderRadius: 4,
|
|
368
|
+
fontFamily: 'var(--font-mono)',
|
|
369
|
+
fontSize: 12,
|
|
370
|
+
}}
|
|
371
|
+
>
|
|
372
|
+
{docError}
|
|
373
|
+
</code>
|
|
374
|
+
. Open it in your editor to view.
|
|
375
|
+
</div>
|
|
376
|
+
</div>
|
|
377
|
+
) : (
|
|
378
|
+
<div style={{ color: 'var(--text-primary)', lineHeight: 1.75, fontSize: 14 }}>
|
|
379
|
+
{renderMarkdown(docContent)}
|
|
380
|
+
</div>
|
|
381
|
+
)}
|
|
382
|
+
</div>
|
|
383
|
+
</div>
|
|
384
|
+
);
|
|
385
|
+
|
|
386
|
+
// ─── Kanban Board Tab ─────────────────────────────────────────────────────────
|
|
387
|
+
|
|
388
|
+
const renderKanbanCard = (card: KanbanCard) => (
|
|
389
|
+
<div
|
|
390
|
+
id={`kanban-card-${card.id}`}
|
|
391
|
+
key={card.id}
|
|
392
|
+
style={{
|
|
393
|
+
background: 'var(--bg-tertiary)',
|
|
394
|
+
border: '1px solid var(--border)',
|
|
395
|
+
borderRadius: 'var(--radius-sm)',
|
|
396
|
+
padding: '12px 14px',
|
|
397
|
+
cursor: 'default',
|
|
398
|
+
transition: 'var(--transition-normal)',
|
|
399
|
+
display: 'flex',
|
|
400
|
+
alignItems: 'center',
|
|
401
|
+
gap: 10,
|
|
402
|
+
}}
|
|
403
|
+
onMouseEnter={e => {
|
|
404
|
+
(e.currentTarget as HTMLElement).style.transform = 'translateY(-2px)';
|
|
405
|
+
(e.currentTarget as HTMLElement).style.boxShadow = '0 4px 12px rgba(0,0,0,0.3)';
|
|
406
|
+
(e.currentTarget as HTMLElement).style.borderColor = 'var(--border-hover)';
|
|
407
|
+
}}
|
|
408
|
+
onMouseLeave={e => {
|
|
409
|
+
(e.currentTarget as HTMLElement).style.transform = 'translateY(0)';
|
|
410
|
+
(e.currentTarget as HTMLElement).style.boxShadow = 'none';
|
|
411
|
+
(e.currentTarget as HTMLElement).style.borderColor = 'var(--border)';
|
|
412
|
+
}}
|
|
413
|
+
>
|
|
414
|
+
<span style={{ fontSize: 16, flexShrink: 0 }}>{card.emoji}</span>
|
|
415
|
+
<span style={{ fontSize: 13, fontWeight: 500, color: 'var(--text-primary)' }}>
|
|
416
|
+
{card.name}
|
|
417
|
+
</span>
|
|
418
|
+
</div>
|
|
419
|
+
);
|
|
420
|
+
|
|
421
|
+
const renderKanbanColumn = (col: KanbanColumn) => {
|
|
422
|
+
const isExpanded = expandedColumns[col.id] ?? !col.collapsible;
|
|
423
|
+
const visibleCards = isExpanded ? col.cards : [];
|
|
424
|
+
|
|
425
|
+
return (
|
|
426
|
+
<div
|
|
427
|
+
id={`kanban-col-${col.id}`}
|
|
428
|
+
key={col.id}
|
|
429
|
+
style={{
|
|
430
|
+
minWidth: 260,
|
|
431
|
+
flex: 1,
|
|
432
|
+
maxWidth: 320,
|
|
433
|
+
background: 'var(--bg-secondary)',
|
|
434
|
+
border: '1px solid var(--border)',
|
|
435
|
+
borderRadius: 'var(--radius-md)',
|
|
436
|
+
borderTop: `4px solid ${col.accent}`,
|
|
437
|
+
display: 'flex',
|
|
438
|
+
flexDirection: 'column',
|
|
439
|
+
overflow: 'hidden',
|
|
440
|
+
}}
|
|
441
|
+
>
|
|
442
|
+
{/* Column Header */}
|
|
443
|
+
<div
|
|
444
|
+
style={{
|
|
445
|
+
padding: '14px 16px 10px',
|
|
446
|
+
display: 'flex',
|
|
447
|
+
alignItems: 'center',
|
|
448
|
+
justifyContent: 'space-between',
|
|
449
|
+
}}
|
|
450
|
+
>
|
|
451
|
+
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
|
452
|
+
<span style={{ fontWeight: 600, fontSize: 14, color: 'var(--text-primary)' }}>
|
|
453
|
+
{col.title}
|
|
454
|
+
</span>
|
|
455
|
+
<span
|
|
456
|
+
className="badge"
|
|
457
|
+
style={{
|
|
458
|
+
background: `${col.accent}22`,
|
|
459
|
+
color: col.accent,
|
|
460
|
+
fontSize: 11,
|
|
461
|
+
padding: '2px 8px',
|
|
462
|
+
borderRadius: 10,
|
|
463
|
+
fontWeight: 600,
|
|
464
|
+
}}
|
|
465
|
+
>
|
|
466
|
+
{col.count ?? col.cards.length}
|
|
467
|
+
</span>
|
|
468
|
+
</div>
|
|
469
|
+
{col.collapsible && (
|
|
470
|
+
<button
|
|
471
|
+
id={`kanban-toggle-${col.id}`}
|
|
472
|
+
className="btn btn-ghost"
|
|
473
|
+
onClick={() => toggleColumn(col.id)}
|
|
474
|
+
style={{
|
|
475
|
+
fontSize: 12,
|
|
476
|
+
padding: '4px 10px',
|
|
477
|
+
color: 'var(--text-secondary)',
|
|
478
|
+
background: 'transparent',
|
|
479
|
+
borderRadius: 'var(--radius-sm)',
|
|
480
|
+
}}
|
|
481
|
+
>
|
|
482
|
+
{isExpanded ? 'Collapse' : 'Expand'}
|
|
483
|
+
</button>
|
|
484
|
+
)}
|
|
485
|
+
</div>
|
|
486
|
+
|
|
487
|
+
{/* Cards */}
|
|
488
|
+
<div
|
|
489
|
+
style={{
|
|
490
|
+
padding: '0 12px 14px',
|
|
491
|
+
display: 'flex',
|
|
492
|
+
flexDirection: 'column',
|
|
493
|
+
gap: 8,
|
|
494
|
+
flex: 1,
|
|
495
|
+
}}
|
|
496
|
+
>
|
|
497
|
+
{col.collapsible && !isExpanded ? (
|
|
498
|
+
<div
|
|
499
|
+
style={{
|
|
500
|
+
color: 'var(--text-tertiary)',
|
|
501
|
+
fontSize: 12,
|
|
502
|
+
textAlign: 'center',
|
|
503
|
+
padding: '12px 0',
|
|
504
|
+
cursor: 'pointer',
|
|
505
|
+
}}
|
|
506
|
+
onClick={() => toggleColumn(col.id)}
|
|
507
|
+
>
|
|
508
|
+
{col.count ?? col.cards.length} projects — click to expand
|
|
509
|
+
</div>
|
|
510
|
+
) : (
|
|
511
|
+
visibleCards.map(card => renderKanbanCard(card))
|
|
512
|
+
)}
|
|
513
|
+
</div>
|
|
514
|
+
</div>
|
|
515
|
+
);
|
|
516
|
+
};
|
|
517
|
+
|
|
518
|
+
const renderBoardTab = () => (
|
|
519
|
+
<div id="board-panel">
|
|
520
|
+
<div
|
|
521
|
+
id="kanban-board"
|
|
522
|
+
style={{
|
|
523
|
+
display: 'flex',
|
|
524
|
+
gap: 16,
|
|
525
|
+
overflowX: 'auto',
|
|
526
|
+
paddingBottom: 16,
|
|
527
|
+
}}
|
|
528
|
+
>
|
|
529
|
+
{KANBAN_COLUMNS.map(col => renderKanbanColumn(col))}
|
|
530
|
+
</div>
|
|
531
|
+
</div>
|
|
532
|
+
);
|
|
533
|
+
|
|
534
|
+
// ─── Main Render ──────────────────────────────────────────────────────────────
|
|
535
|
+
|
|
536
|
+
const tabs: { key: typeof activeTab; label: string; icon: string }[] = [
|
|
537
|
+
{ key: 'architecture', label: 'Architecture', icon: '📐' },
|
|
538
|
+
{ key: 'board', label: 'Project Board', icon: '📋' },
|
|
539
|
+
];
|
|
540
|
+
|
|
541
|
+
return (
|
|
542
|
+
<div className="page" style={{ padding: '24px 28px', maxWidth: 1200, margin: '0 auto' }}>
|
|
543
|
+
{/* Page Header */}
|
|
544
|
+
<div className="page-header" style={{ marginBottom: 24 }}>
|
|
545
|
+
<h1 style={{ fontSize: 22, fontWeight: 700 }}>Design Docs & Projects</h1>
|
|
546
|
+
<p style={{ color: 'var(--text-secondary)', fontSize: 13, marginTop: 4 }}>
|
|
547
|
+
Architecture documentation, specifications, and project tracking board.
|
|
548
|
+
</p>
|
|
549
|
+
</div>
|
|
550
|
+
|
|
551
|
+
{/* Tab Navigation */}
|
|
552
|
+
<div
|
|
553
|
+
id="design-docs-tabs"
|
|
554
|
+
style={{
|
|
555
|
+
display: 'inline-flex',
|
|
556
|
+
gap: 0,
|
|
557
|
+
marginBottom: 24,
|
|
558
|
+
borderBottom: '1px solid var(--border)',
|
|
559
|
+
}}
|
|
560
|
+
>
|
|
561
|
+
{tabs.map(tab => {
|
|
562
|
+
const isActive = activeTab === tab.key;
|
|
563
|
+
return (
|
|
564
|
+
<button
|
|
565
|
+
id={`tab-${tab.key}`}
|
|
566
|
+
key={tab.key}
|
|
567
|
+
onClick={() => setActiveTab(tab.key)}
|
|
568
|
+
style={{
|
|
569
|
+
display: 'flex',
|
|
570
|
+
alignItems: 'center',
|
|
571
|
+
gap: 7,
|
|
572
|
+
padding: '10px 20px',
|
|
573
|
+
background: 'transparent',
|
|
574
|
+
color: isActive ? 'var(--accent)' : 'var(--text-secondary)',
|
|
575
|
+
fontSize: 14,
|
|
576
|
+
fontWeight: isActive ? 600 : 400,
|
|
577
|
+
borderBottom: isActive ? '2px solid var(--accent)' : '2px solid transparent',
|
|
578
|
+
transition: 'var(--transition-normal)',
|
|
579
|
+
cursor: 'pointer',
|
|
580
|
+
marginBottom: -1,
|
|
581
|
+
}}
|
|
582
|
+
>
|
|
583
|
+
<span>{tab.icon}</span>
|
|
584
|
+
{tab.label}
|
|
585
|
+
</button>
|
|
586
|
+
);
|
|
587
|
+
})}
|
|
588
|
+
</div>
|
|
589
|
+
|
|
590
|
+
{/* Tab Content */}
|
|
591
|
+
{activeTab === 'architecture' ? renderArchitectureTab() : renderBoardTab()}
|
|
592
|
+
|
|
593
|
+
{/* ─── Responsive Styles ─────────────────────────────────────────────────── */}
|
|
594
|
+
<style>{`
|
|
595
|
+
@media (max-width: 768px) {
|
|
596
|
+
#architecture-panel {
|
|
597
|
+
flex-direction: column !important;
|
|
598
|
+
}
|
|
599
|
+
#docs-sidebar {
|
|
600
|
+
width: 100% !important;
|
|
601
|
+
max-height: 200px;
|
|
602
|
+
}
|
|
603
|
+
#kanban-board {
|
|
604
|
+
flex-direction: column !important;
|
|
605
|
+
}
|
|
606
|
+
#kanban-board > div {
|
|
607
|
+
max-width: 100% !important;
|
|
608
|
+
min-width: 100% !important;
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
`}</style>
|
|
612
|
+
</div>
|
|
613
|
+
);
|
|
614
|
+
}
|