@donotdev/cli 0.0.20 → 0.0.21

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 (103) hide show
  1. package/README.md +31 -0
  2. package/dependencies-matrix.json +86 -19
  3. package/dist/bin/commands/agent-setup.js +2 -2
  4. package/dist/bin/commands/build.js +6 -6
  5. package/dist/bin/commands/bump.js +491 -69
  6. package/dist/bin/commands/cacheout.js +6 -6
  7. package/dist/bin/commands/coach.js +6 -6
  8. package/dist/bin/commands/create-app.js +23 -15
  9. package/dist/bin/commands/create-project.js +101 -16
  10. package/dist/bin/commands/db.js +142136 -0
  11. package/dist/bin/commands/deploy.js +336 -126
  12. package/dist/bin/commands/dev.js +6 -6
  13. package/dist/bin/commands/doctor.js +140 -33
  14. package/dist/bin/commands/emu.js +6 -6
  15. package/dist/bin/commands/format.js +6 -6
  16. package/dist/bin/commands/get-demo.js +11 -6
  17. package/dist/bin/commands/make-admin.js +14210 -13770
  18. package/dist/bin/commands/preview.js +6 -6
  19. package/dist/bin/commands/seed.js +142426 -0
  20. package/dist/bin/commands/setup-cicd.js +8904 -0
  21. package/dist/bin/commands/setup.js +256 -212
  22. package/dist/bin/commands/staging.js +343 -127
  23. package/dist/bin/commands/sync-secrets.js +55 -33
  24. package/dist/bin/commands/type-check.js +6 -6
  25. package/dist/bin/commands/wai.js +6 -6
  26. package/dist/bin/dndev.js +76 -11
  27. package/dist/bin/donotdev.js +21 -12
  28. package/dist/index.js +437 -142
  29. package/package.json +1 -1
  30. package/templates/app-demo/.env.example +1 -0
  31. package/templates/{root-consumer → app-demo}/entities/ExampleEntity.ts.example +15 -9
  32. package/templates/app-demo/index.html.example +1 -1
  33. package/templates/app-dndev/index.html.example +164 -0
  34. package/templates/app-dndev/public/logo.svg.example +1 -0
  35. package/templates/app-dndev/public/manifest.json.example +10 -0
  36. package/templates/app-dndev/src/App.tsx.example +35 -0
  37. package/templates/app-dndev/src/components/CockpitLayout.css.example +181 -0
  38. package/templates/app-dndev/src/components/CockpitLayout.tsx.example +209 -0
  39. package/templates/app-dndev/src/components/Kanban.css.example +385 -0
  40. package/templates/app-dndev/src/components/ModeToggle.tsx.example +32 -0
  41. package/templates/app-dndev/src/components/OverlaySlot.tsx.example +68 -0
  42. package/templates/app-dndev/src/components/TerminalPanel.css.example +228 -0
  43. package/templates/app-dndev/src/components/TerminalPanel.tsx.example +714 -0
  44. package/templates/app-dndev/src/components/markdown-prose.css.example +49 -0
  45. package/templates/app-dndev/src/components/phases/CaptainLog.tsx.example +107 -0
  46. package/templates/app-dndev/src/components/phases/ContextTabs.tsx.example +352 -0
  47. package/templates/app-dndev/src/components/phases/PhaseCard.tsx.example +126 -0
  48. package/templates/app-dndev/src/components/phases/PhaseDetail.tsx.example +147 -0
  49. package/templates/app-dndev/src/components/phases/ReviewPanel.tsx.example +115 -0
  50. package/templates/app-dndev/src/components/phases/phaseData.ts.example +366 -0
  51. package/templates/app-dndev/src/config/app.ts.example +103 -0
  52. package/templates/app-dndev/src/config/commands.ts.example +171 -0
  53. package/templates/app-dndev/src/config/legal.ts.example +170 -0
  54. package/templates/app-dndev/src/config/providers.ts.example +7 -0
  55. package/templates/app-dndev/src/globals.css.example +10 -0
  56. package/templates/app-dndev/src/hooks/useDndevFile.ts.example +144 -0
  57. package/templates/app-dndev/src/main.tsx.example +21 -0
  58. package/templates/app-dndev/src/pages/BoardPage.tsx.example +640 -0
  59. package/templates/app-dndev/src/pages/GrillPage.tsx.example +658 -0
  60. package/templates/app-dndev/src/pages/HomePage.tsx.example +347 -0
  61. package/templates/app-dndev/src/pages/NotFoundPage.tsx.example +33 -0
  62. package/templates/app-dndev/src/pages/PhasesPage.tsx.example +137 -0
  63. package/templates/app-dndev/src/pages/SettingsPage.tsx.example +64 -0
  64. package/templates/app-dndev/src/pages/legal/LegalNoticePage.tsx.example +75 -0
  65. package/templates/app-dndev/src/pages/legal/PrivacyPage.tsx.example +69 -0
  66. package/templates/app-dndev/src/pages/legal/TermsPage.tsx.example +71 -0
  67. package/templates/app-dndev/src/stores/dndevStore.ts.example +386 -0
  68. package/templates/app-dndev/src/themes.css.example +161 -0
  69. package/templates/app-dndev/terminal-sidecar.cjs.example +341 -0
  70. package/templates/app-dndev/tsconfig.json.example +9 -0
  71. package/templates/app-dndev/vite.config.ts.example +24 -0
  72. package/templates/app-next/src/locales/home_en.json.example +6 -6
  73. package/templates/app-vite/index.html.example +1 -1
  74. package/templates/app-vite/src/locales/home_en.json.example +6 -6
  75. package/templates/functions-supabase/supabase/functions/.env.example +0 -2
  76. package/templates/root-consumer/.claude/commands/grill.md.example +86 -8
  77. package/templates/root-consumer/.dndev.secrets.example +32 -0
  78. package/templates/root-consumer/.gitignore.example +3 -0
  79. package/templates/root-consumer/AI.md.example +4 -0
  80. package/templates/root-consumer/entities/index.ts.example +2 -5
  81. package/templates/root-consumer/guides/dndev/COMPONENTS_ATOMIC.md.example +4 -0
  82. package/templates/root-consumer/guides/dndev/ENV_SETUP.md.example +23 -20
  83. package/templates/root-consumer/guides/dndev/INDEX.md.example +1 -0
  84. package/templates/root-consumer/guides/dndev/SETUP_BILLING.md.example +3 -7
  85. package/templates/root-consumer/guides/dndev/SETUP_CICD.md.example +115 -0
  86. package/templates/root-consumer/guides/dndev/SETUP_CRUD.md.example +41 -0
  87. package/templates/root-consumer/guides/dndev/SETUP_SUPABASE.md.example +13 -18
  88. package/templates/root-consumer/guides/dndev/SETUP_VERCEL.md.example +17 -12
  89. package/templates/root-consumer/guides/dndev/advanced/COOKIE_REFERENCE.md.example +252 -252
  90. package/templates/root-consumer/guides/dndev/advanced/VERSION_CONTROL.md.example +174 -174
  91. package/templates/root-consumer/guides/wai-way/WAI_WAY_CLI.md.example +185 -251
  92. package/templates/root-consumer/guides/wai-way/agents/extractor.md.example +26 -8
  93. package/templates/root-consumer/guides/wai-way/blueprints/0_brainstorm.md.example +66 -49
  94. package/templates/root-consumer/guides/wai-way/blueprints/1_scaffold.md.example +6 -5
  95. package/templates/root-consumer/guides/wai-way/blueprints/2_entities.md.example +9 -9
  96. package/templates/root-consumer/guides/wai-way/blueprints/3_compose.md.example +1 -1
  97. package/templates/root-consumer/guides/wai-way/blueprints/4_configure.md.example +7 -6
  98. package/templates/root-consumer/guides/wai-way/context_map.json.example +51 -20
  99. package/templates/root-consumer/guides/wai-way/hld_template.md.example +138 -0
  100. package/templates/root-consumer/guides/wai-way/lld_template.md.example +103 -0
  101. package/templates/root-consumer/guides/wai-way/prd_template.md.example +140 -0
  102. /package/templates/{root-consumer → app-demo}/entities/Contact.ts.example +0 -0
  103. /package/templates/{root-consumer → app-demo}/entities/demo.ts.example +0 -0
@@ -0,0 +1,49 @@
1
+ /* Prose typography for ReactMarkdown rendered output */
2
+
3
+ .dndev-docs-rendered {
4
+ padding: 8px 0;
5
+ line-height: 1.7;
6
+ }
7
+
8
+ .dndev-docs-rendered h1,
9
+ .dndev-docs-rendered h2,
10
+ .dndev-docs-rendered h3 {
11
+ margin-top: 1.2em;
12
+ margin-bottom: 0.4em;
13
+ }
14
+
15
+ .dndev-docs-rendered code {
16
+ padding: 2px 5px;
17
+ border-radius: 3px;
18
+ background: var(--muted);
19
+ font-size: 0.9em;
20
+ }
21
+
22
+ .dndev-docs-rendered pre {
23
+ padding: 12px;
24
+ border-radius: var(--radius-surface, 8px);
25
+ background: var(--muted);
26
+ overflow-x: auto;
27
+ }
28
+
29
+ .dndev-docs-rendered pre code {
30
+ padding: 0;
31
+ background: transparent;
32
+ }
33
+
34
+ .dndev-docs-rendered table {
35
+ width: 100%;
36
+ border-collapse: collapse;
37
+ }
38
+
39
+ .dndev-docs-rendered th,
40
+ .dndev-docs-rendered td {
41
+ padding: 6px 12px;
42
+ border: 1px solid var(--border);
43
+ text-align: start;
44
+ }
45
+
46
+ .dndev-docs-rendered th {
47
+ background: var(--muted);
48
+ font-weight: 600;
49
+ }
@@ -0,0 +1,107 @@
1
+ /**
2
+ * @fileoverview Captain's Log — session history accordion with KPIs
3
+ *
4
+ * Reads captain-log.json and renders completed sessions as expandable items.
5
+ */
6
+
7
+ import { Accordion, Badge, DescriptionList, Section, Stack, Text } from '@donotdev/components';
8
+
9
+ import type { CaptainLogData } from './phaseData';
10
+
11
+ // ============================================================================
12
+ // PROPS
13
+ // ============================================================================
14
+
15
+ interface CaptainLogProps {
16
+ data: CaptainLogData;
17
+ /** When true, renders without the wrapping Section (for embedding in ContextTabs) */
18
+ embedded?: boolean;
19
+ }
20
+
21
+ // ============================================================================
22
+ // HELPERS
23
+ // ============================================================================
24
+
25
+ function formatDuration(start: string, end: string): string {
26
+ const ms = new Date(end).getTime() - new Date(start).getTime();
27
+ const hours = Math.floor(ms / 3_600_000);
28
+ const minutes = Math.floor((ms % 3_600_000) / 60_000);
29
+ if (hours > 0) return `${hours}h ${minutes}m`;
30
+ return `${minutes}m`;
31
+ }
32
+
33
+ // ============================================================================
34
+ // COMPONENT
35
+ // ============================================================================
36
+
37
+ export function CaptainLog({ data, embedded }: CaptainLogProps) {
38
+ const sessions = data.sessions ?? [];
39
+
40
+ if (sessions.length === 0) {
41
+ if (embedded) {
42
+ return <Text level="body" variant="muted">No sessions recorded yet.</Text>;
43
+ }
44
+ return null;
45
+ }
46
+
47
+ // Most recent first
48
+ const sorted = [...sessions].reverse();
49
+
50
+ const items = sorted.map((session) => {
51
+ const toolEntries = Object.entries(session.tool_calls ?? {}).filter(([, c]) => c > 0);
52
+
53
+ return {
54
+ value: String(session.id),
55
+ trigger: (
56
+ <Stack direction="row" align="center" gap="tight" style={{ flex: 1 }}>
57
+ <Text level="small" weight="medium">
58
+ Phase {session.phase}: {session.phase_name}
59
+ </Text>
60
+ {session.module && (
61
+ <Badge variant="muted">{session.module}</Badge>
62
+ )}
63
+ <Badge variant={session.validation === 'passed' ? 'success' : 'warning'}>
64
+ {session.validation}
65
+ </Badge>
66
+ <Text level="caption" variant="muted" style={{ marginInlineStart: 'auto' }}>
67
+ {session.date}
68
+ </Text>
69
+ </Stack>
70
+ ),
71
+ content: (
72
+ <Stack gap="tight">
73
+ <Text level="small">{session.outcome}</Text>
74
+ <DescriptionList
75
+ orientation="horizontal"
76
+ items={[
77
+ { label: 'Files touched', value: String(session.files_touched) },
78
+ { label: 'Symbols used', value: String(session.symbols_used) },
79
+ { label: 'Duration', value: formatDuration(session.started_at, session.completed_at) },
80
+ { label: 'Lessons', value: String(session.lessons_recorded) },
81
+ ...toolEntries.map(([name, count]) => ({
82
+ label: name.replace(/_/g, ' '),
83
+ value: String(count),
84
+ })),
85
+ ]}
86
+ />
87
+ </Stack>
88
+ ),
89
+ };
90
+ });
91
+
92
+ const accordion = (
93
+ <Accordion
94
+ type="single"
95
+ collapsible
96
+ items={items}
97
+ />
98
+ );
99
+
100
+ if (embedded) return accordion;
101
+
102
+ return (
103
+ <Section title="Captain's Log">
104
+ {accordion}
105
+ </Section>
106
+ );
107
+ }
@@ -0,0 +1,352 @@
1
+ /**
2
+ * @fileoverview Context panel — Progress, Specs, Lessons, Log
3
+ *
4
+ * Left 1/4 sidebar: category selector (accent) + sub-item list.
5
+ * Right 3/4: content for selected item.
6
+ * Lessons opens LESSONS.md for direct editing (same as specs).
7
+ * Log renders the Captain's Log accordion inline.
8
+ */
9
+
10
+ import { useEffect, useRef, useState } from 'react';
11
+ import { Pencil, Eye, Save, Undo2, ClipboardList, FileText, BookOpen, ScrollText } from 'lucide-react';
12
+ import ReactMarkdown from 'react-markdown';
13
+ import remarkGfm from 'remark-gfm';
14
+
15
+ import { Badge, Button, Card, Checkbox, Progress, ScrollArea, Section, Stack, Text, Textarea } from '@donotdev/components';
16
+
17
+ import { useDndevFile, writeDndevFile } from '../../hooks/useDndevFile';
18
+ import { CaptainLog } from './CaptainLog';
19
+
20
+ import type { CaptainLogData, ProgressSection, ProtocolData } from './phaseData';
21
+ import type { LucideIcon } from 'lucide-react';
22
+
23
+ // ============================================================================
24
+ // TYPES
25
+ // ============================================================================
26
+
27
+ interface ContextTabsProps {
28
+ protocol: ProtocolData;
29
+ progressSections: ProgressSection[];
30
+ lessonsCount: number;
31
+ captainLog: CaptainLogData;
32
+ onCheckboxToggle: (line: number, currentChecked: boolean) => void;
33
+ }
34
+
35
+ type Category = 'progress' | 'specs' | 'lessons' | 'log';
36
+
37
+ interface CategoryDef {
38
+ id: Category;
39
+ label: string;
40
+ icon: LucideIcon;
41
+ }
42
+
43
+ const CATEGORIES: CategoryDef[] = [
44
+ { id: 'progress', label: 'Progress', icon: ClipboardList },
45
+ { id: 'specs', label: 'Specs', icon: FileText },
46
+ { id: 'lessons', label: 'Lessons', icon: BookOpen },
47
+ { id: 'log', label: 'Log', icon: ScrollText },
48
+ ];
49
+
50
+ // ============================================================================
51
+ // SIDEBAR ITEM
52
+ // ============================================================================
53
+
54
+ function SidebarItem({
55
+ label,
56
+ icon,
57
+ active,
58
+ accent,
59
+ onClick,
60
+ }: {
61
+ label: string;
62
+ icon?: LucideIcon;
63
+ active?: boolean;
64
+ accent?: boolean;
65
+ onClick: () => void;
66
+ }) {
67
+ return (
68
+ <Button
69
+ variant={active ? (accent ? 'default' : 'secondary') : 'ghost'}
70
+ onClick={onClick}
71
+ style={{ justifyContent: 'flex-start', width: '100%' }}
72
+ icon={icon}
73
+ >
74
+ {label}
75
+ </Button>
76
+ );
77
+ }
78
+
79
+ // ============================================================================
80
+ // CONTENT: PROGRESS
81
+ // ============================================================================
82
+
83
+ function ProgressContent({
84
+ section,
85
+ onCheckboxToggle,
86
+ }: {
87
+ section: ProgressSection;
88
+ onCheckboxToggle: (line: number, currentChecked: boolean) => void;
89
+ }) {
90
+ const doneCount = section.items.filter((i) => i.checked).length;
91
+ const totalCount = section.items.length;
92
+ const pct = totalCount > 0 ? Math.round((doneCount / totalCount) * 100) : 0;
93
+
94
+ return (
95
+ <Card>
96
+ <Stack gap="tight">
97
+ <Stack direction="row" align="center" justify="between">
98
+ <Text level="small" weight="semibold">{section.title}</Text>
99
+ <Text level="caption" variant="muted">{doneCount}/{totalCount} ({pct}%)</Text>
100
+ </Stack>
101
+ <Progress value={pct} />
102
+ <Stack gap="none">
103
+ {section.items.map((item) => (
104
+ <Stack
105
+ key={item.line}
106
+ direction="row"
107
+ align="center"
108
+ gap="tight"
109
+ style={{ paddingBlock: '2px', cursor: 'pointer' }}
110
+ onClick={() => onCheckboxToggle(item.line, item.checked)}
111
+ >
112
+ <Checkbox
113
+ checked={item.checked}
114
+ onCheckedChange={() => onCheckboxToggle(item.line, item.checked)}
115
+ />
116
+ <Text level="small" variant={item.checked ? 'muted' : undefined}>
117
+ {item.text}
118
+ </Text>
119
+ </Stack>
120
+ ))}
121
+ </Stack>
122
+ </Stack>
123
+ </Card>
124
+ );
125
+ }
126
+
127
+ // ============================================================================
128
+ // CONTENT: DOCUMENT EDITOR (Specs + Lessons)
129
+ // ============================================================================
130
+
131
+ function DocContent({ path }: { path: string }) {
132
+ const [isEditing, setIsEditing] = useState(false);
133
+ const [editContent, setEditContent] = useState('');
134
+ const [isSaving, setIsSaving] = useState(false);
135
+ const userEdited = useRef(false);
136
+
137
+ const { data: fileContent, reload: reloadFile } = useDndevFile<string>(
138
+ path,
139
+ (raw) => raw as string,
140
+ { fallback: '' },
141
+ );
142
+
143
+ const isDirty = userEdited.current && editContent !== (fileContent ?? '');
144
+
145
+ // Reset edit state when switching docs
146
+ useEffect(() => {
147
+ userEdited.current = false;
148
+ setEditContent('');
149
+ setIsEditing(false);
150
+ }, [path]);
151
+
152
+ // Sync file content → edit buffer (only when file changes externally, not during user edits)
153
+ useEffect(() => {
154
+ if (fileContent !== null && !userEdited.current) setEditContent(fileContent);
155
+ }, [fileContent]);
156
+
157
+ async function handleSave() {
158
+ setIsSaving(true);
159
+ await writeDndevFile(path, editContent);
160
+ await reloadFile();
161
+ userEdited.current = false;
162
+ setIsEditing(false);
163
+ setIsSaving(false);
164
+ }
165
+
166
+ function handleDiscard() {
167
+ setEditContent(fileContent ?? '');
168
+ userEdited.current = false;
169
+ setIsEditing(false);
170
+ }
171
+
172
+ return (
173
+ <Card style={{ display: 'flex', flexDirection: 'column', flex: 1, minHeight: 0 }}>
174
+ <Stack direction="row" align="center" justify="between" style={{ padding: '8px 12px', borderBottom: '1px solid var(--border)', flexShrink: 0, flexWrap: 'nowrap' }}>
175
+ <Text level="caption" variant="muted" style={{ minWidth: 0, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{path}</Text>
176
+ <Stack direction="row" gap="tight" style={{ flexShrink: 0 }}>
177
+ {isEditing ? (
178
+ <Button variant="outline" icon={Eye} onClick={() => setIsEditing(false)}>
179
+ Preview
180
+ </Button>
181
+ ) : (
182
+ <Button variant="outline" icon={Pencil} onClick={() => setIsEditing(true)}>
183
+ Edit
184
+ </Button>
185
+ )}
186
+ {isDirty && (
187
+ <>
188
+ <Button variant="ghost" icon={Undo2} onClick={handleDiscard}>
189
+ Discard
190
+ </Button>
191
+ <Button variant="default" icon={Save} onClick={handleSave} disabled={isSaving}>
192
+ {isSaving ? 'Saving...' : 'Save'}
193
+ </Button>
194
+ </>
195
+ )}
196
+ </Stack>
197
+ </Stack>
198
+
199
+ <ScrollArea style={{ flex: 1, minHeight: 0 }}>
200
+ <Stack style={{ padding: '16px 20px' }}>
201
+ {isEditing ? (
202
+ <Textarea
203
+ value={editContent}
204
+ onChange={(e) => { userEdited.current = true; setEditContent(e.target.value); }}
205
+ spellCheck={false}
206
+ style={{ minHeight: '400px', fontFamily: "'JetBrains Mono', 'Fira Code', 'SF Mono', monospace", fontSize: '13px', lineHeight: 1.6 }}
207
+ />
208
+ ) : (
209
+ <div className="dndev-docs-rendered">
210
+ <ReactMarkdown remarkPlugins={[remarkGfm]}>
211
+ {isDirty ? editContent : (fileContent ?? '')}
212
+ </ReactMarkdown>
213
+ </div>
214
+ )}
215
+ </Stack>
216
+ </ScrollArea>
217
+ </Card>
218
+ );
219
+ }
220
+
221
+ // ============================================================================
222
+ // COMPONENT
223
+ // ============================================================================
224
+
225
+ export function ContextTabs({
226
+ protocol,
227
+ progressSections,
228
+ lessonsCount,
229
+ captainLog,
230
+ onCheckboxToggle,
231
+ }: ContextTabsProps) {
232
+ const [category, setCategory] = useState<Category>('progress');
233
+ const [selectedItem, setSelectedItem] = useState<string | null>(null);
234
+
235
+ // Discover spec docs
236
+ const [specDocs, setSpecDocs] = useState<{ path: string; label: string }[]>([]);
237
+ useEffect(() => {
238
+ (async () => {
239
+ try {
240
+ const res = await fetch('/api/dndev/tree');
241
+ if (!res.ok) return;
242
+ const data = await res.json();
243
+ const entries: { path: string; name: string; isDirectory: boolean }[] = data.entries ?? [];
244
+ const discovered: { path: string; label: string }[] = [];
245
+
246
+ if (entries.some((e) => e.path === '.dndev/implementation.md')) {
247
+ discovered.push({ path: '.dndev/implementation.md', label: 'Implementation' });
248
+ }
249
+ for (const entry of entries) {
250
+ if (!entry.isDirectory && entry.path.startsWith('.dndev/plans/') && entry.path.endsWith('.md')) {
251
+ discovered.push({ path: entry.path, label: entry.name.replace(/\.md$/, '') });
252
+ }
253
+ }
254
+ for (const name of ['HLD.md', 'LLD.md']) {
255
+ if (entries.some((e) => e.path === name)) {
256
+ discovered.push({ path: name, label: name.replace('.md', '') });
257
+ }
258
+ }
259
+ setSpecDocs(discovered);
260
+ } catch { /* silent */ }
261
+ })();
262
+ }, []);
263
+
264
+ // Auto-select first sub-item when category changes
265
+ useEffect(() => {
266
+ if (category === 'progress') {
267
+ setSelectedItem(progressSections.length > 0 ? progressSections[0]!.title : null);
268
+ } else if (category === 'specs') {
269
+ setSelectedItem(specDocs.length > 0 ? specDocs[0]!.path : null);
270
+ } else {
271
+ setSelectedItem(null);
272
+ }
273
+ }, [category, progressSections, specDocs]);
274
+
275
+ // Build sub-items for left sidebar
276
+ let subItems: { key: string; label: string }[] = [];
277
+ if (category === 'progress') {
278
+ subItems = progressSections.map((s) => ({ key: s.title, label: s.title }));
279
+ } else if (category === 'specs') {
280
+ subItems = specDocs.map((d) => ({ key: d.path, label: d.label }));
281
+ }
282
+ // Lessons + Log have no sub-items
283
+
284
+ // Render content
285
+ function renderContent() {
286
+ if (category === 'progress') {
287
+ const section = progressSections.find((s) => s.title === selectedItem);
288
+ if (!section) {
289
+ return (
290
+ <Card>
291
+ <Text level="body" variant="muted">No implementation plan found. Run Phase 0 to generate one.</Text>
292
+ </Card>
293
+ );
294
+ }
295
+ return <ProgressContent section={section} onCheckboxToggle={onCheckboxToggle} />;
296
+ }
297
+
298
+ if (category === 'specs') {
299
+ if (!selectedItem) {
300
+ return (
301
+ <Card>
302
+ <Text level="body" variant="muted">No spec documents found. Run Phase 0 to generate PRD, HLD, and LLD.</Text>
303
+ </Card>
304
+ );
305
+ }
306
+ return <DocContent path={selectedItem} />;
307
+ }
308
+
309
+ if (category === 'lessons') {
310
+ return <DocContent path=".dndev/LESSONS.md" />;
311
+ }
312
+
313
+ // Log
314
+ return <CaptainLog data={captainLog} embedded />;
315
+ }
316
+
317
+ return (
318
+ <Section gridCols={["1fr", "1fr", "1fr 3fr", "1fr 3fr"]} gridGap="medium">
319
+ {/* Left sidebar */}
320
+ <Stack gap="tight">
321
+ {/* Category selectors — accent */}
322
+ {CATEGORIES.map((cat) => (
323
+ <SidebarItem
324
+ key={cat.id}
325
+ label={cat.id === 'lessons' ? `${cat.label} (${lessonsCount})` : cat.label}
326
+ icon={cat.icon}
327
+ accent
328
+ active={category === cat.id}
329
+ onClick={() => setCategory(cat.id)}
330
+ />
331
+ ))}
332
+
333
+ {/* Sub-items below categories */}
334
+ {subItems.length > 0 && (
335
+ <Stack gap="none" style={{ marginBlockStart: '4px' }}>
336
+ {subItems.map((item) => (
337
+ <SidebarItem
338
+ key={item.key}
339
+ label={item.label}
340
+ active={selectedItem === item.key}
341
+ onClick={() => setSelectedItem(item.key)}
342
+ />
343
+ ))}
344
+ </Stack>
345
+ )}
346
+ </Stack>
347
+
348
+ {/* Right content */}
349
+ {renderContent()}
350
+ </Section>
351
+ );
352
+ }
@@ -0,0 +1,126 @@
1
+ /**
2
+ * @fileoverview Reusable phase card — Icon + Title + contextual subtitle
3
+ *
4
+ * Shared between HomePage (overview) and PhasesPage (roadmap).
5
+ * Subtitle adapts to phase status:
6
+ * - Pending: blueprint description (what will happen)
7
+ * - Active: module + time since started + progress
8
+ * - Completed: summary + when it finished
9
+ */
10
+
11
+ import { Badge, Card, Label, Progress, Stack, Text } from '@donotdev/components';
12
+
13
+ import { PHASE_BLUEPRINTS, getPhaseStatus } from './phaseData';
14
+
15
+ import type { ProtocolData, ProgressSection } from './phaseData';
16
+ import type { ReactNode } from 'react';
17
+
18
+ // ============================================================================
19
+ // HELPERS
20
+ // ============================================================================
21
+
22
+ export function getProgressForPhase(
23
+ phaseId: number,
24
+ phaseName: string,
25
+ sections: ProgressSection[],
26
+ ): { done: number; total: number } {
27
+ for (const section of sections) {
28
+ const matchesPhase =
29
+ section.title.toLowerCase().includes(`phase ${phaseId}`) ||
30
+ section.title.toLowerCase().includes(phaseName.toLowerCase());
31
+ if (matchesPhase) {
32
+ const done = section.items.filter((i) => i.checked).length;
33
+ return { done, total: section.items.length };
34
+ }
35
+ }
36
+ return { done: 0, total: 0 };
37
+ }
38
+
39
+ const STATUS_BADGE_VARIANT = {
40
+ completed: 'success',
41
+ active: 'warning',
42
+ pending: 'muted',
43
+ } as const;
44
+
45
+ // ============================================================================
46
+ // COMPONENT
47
+ // ============================================================================
48
+
49
+ interface PhaseCardProps {
50
+ phaseId: number;
51
+ protocol: ProtocolData;
52
+ progressSections?: ProgressSection[];
53
+ selected?: boolean;
54
+ onClick?: () => void;
55
+ }
56
+
57
+ export function PhaseCard({
58
+ phaseId,
59
+ protocol,
60
+ progressSections,
61
+ selected,
62
+ onClick,
63
+ }: PhaseCardProps): ReactNode {
64
+ const bp = PHASE_BLUEPRINTS[phaseId];
65
+ if (!bp) return null;
66
+
67
+ const status = getPhaseStatus(bp.id, protocol);
68
+ const Icon = bp.icon;
69
+
70
+ const progress = progressSections
71
+ ? getProgressForPhase(bp.id, bp.name, progressSections)
72
+ : { done: 0, total: 0 };
73
+ const pct = progress.total > 0 ? Math.round((progress.done / progress.total) * 100) : 0;
74
+
75
+ return (
76
+ <Card
77
+ onClick={onClick}
78
+ style={{
79
+ cursor: onClick ? 'pointer' : undefined,
80
+ ...(selected ? { borderColor: 'var(--primary)', boxShadow: '0 0 0 1px var(--primary)' } : {}),
81
+ }}
82
+ >
83
+ <Stack gap="tight">
84
+ <Stack direction="row" align="center" justify="between">
85
+ <Label icon={Icon} plain>{bp.name}</Label>
86
+ <Badge variant={STATUS_BADGE_VARIANT[status]}>{status}</Badge>
87
+ </Stack>
88
+ <Text level="caption" variant="muted">{bp.blueprint.split('.')[0]}.</Text>
89
+ {progress.total > 0 && <Progress value={pct} />}
90
+ </Stack>
91
+ </Card>
92
+ );
93
+ }
94
+
95
+ // ============================================================================
96
+ // LIST — renders all 5 phase cards
97
+ // ============================================================================
98
+
99
+ interface PhaseCardListProps {
100
+ protocol: ProtocolData;
101
+ progressSections?: ProgressSection[];
102
+ selectedPhase?: number;
103
+ onSelectPhase?: (phase: number) => void;
104
+ }
105
+
106
+ export function PhaseCardList({
107
+ protocol,
108
+ progressSections,
109
+ selectedPhase,
110
+ onSelectPhase,
111
+ }: PhaseCardListProps): ReactNode {
112
+ return (
113
+ <Stack gap="tight">
114
+ {PHASE_BLUEPRINTS.map((bp) => (
115
+ <PhaseCard
116
+ key={bp.id}
117
+ phaseId={bp.id}
118
+ protocol={protocol}
119
+ progressSections={progressSections}
120
+ selected={selectedPhase === bp.id}
121
+ onClick={onSelectPhase ? () => onSelectPhase(bp.id) : undefined}
122
+ />
123
+ ))}
124
+ </Stack>
125
+ );
126
+ }