@donotdev/cli 0.0.19 → 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.
- package/README.md +31 -0
- package/dependencies-matrix.json +205 -50
- package/dist/bin/commands/agent-setup.js +2 -2
- package/dist/bin/commands/build.js +6 -6
- package/dist/bin/commands/bump.js +495 -70
- package/dist/bin/commands/cacheout.js +6 -6
- package/dist/bin/commands/coach.js +6 -6
- package/dist/bin/commands/create-app.js +24 -16
- package/dist/bin/commands/create-project.js +114 -18
- package/dist/bin/commands/db.js +142136 -0
- package/dist/bin/commands/deploy.js +354 -126
- package/dist/bin/commands/dev.js +6 -6
- package/dist/bin/commands/doctor.js +140 -33
- package/dist/bin/commands/emu.js +6 -6
- package/dist/bin/commands/format.js +6 -6
- package/dist/bin/commands/get-demo.js +11 -6
- package/dist/bin/commands/make-admin.js +14210 -13770
- package/dist/bin/commands/preview.js +6 -6
- package/dist/bin/commands/seed.js +142426 -0
- package/dist/bin/commands/setup-cicd.js +8904 -0
- package/dist/bin/commands/setup.js +259 -212
- package/dist/bin/commands/staging.js +361 -127
- package/dist/bin/commands/sync-secrets.js +55 -33
- package/dist/bin/commands/type-check.js +16 -10
- package/dist/bin/commands/wai.js +6 -6
- package/dist/bin/dndev.js +194 -188
- package/dist/bin/donotdev.js +139 -189
- package/dist/index.js +468 -144
- package/package.json +1 -1
- package/templates/app-demo/.env.example +1 -0
- package/templates/{root-consumer → app-demo}/entities/ExampleEntity.ts.example +15 -9
- package/templates/app-demo/index.html.example +1 -1
- package/templates/app-demo/public/apple-touch-icon.png.example +0 -0
- package/templates/app-demo/public/favicon.svg.example +1 -0
- package/templates/app-demo/public/icon-192x192.png.example +0 -0
- package/templates/app-demo/public/icon-512x512.png.example +0 -0
- package/templates/app-demo/src/App.tsx.example +3 -1
- package/templates/app-demo/src/config/app.ts.example +1 -0
- package/templates/app-demo/src/entities/booking.ts.example +75 -0
- package/templates/app-demo/src/entities/onboarding.ts.example +160 -0
- package/templates/app-demo/src/entities/product.ts.example +12 -0
- package/templates/app-demo/src/entities/quote.ts.example +70 -0
- package/templates/app-demo/src/pages/ChangelogPage.tsx.example +28 -1
- package/templates/app-demo/src/pages/ConditionalFormPage.tsx.example +88 -0
- package/templates/app-demo/src/pages/DashboardPage.tsx.example +2 -0
- package/templates/app-demo/src/pages/HomePage.tsx.example +355 -2
- package/templates/app-demo/src/pages/OnboardingPage.tsx.example +47 -0
- package/templates/app-demo/src/pages/PricingPage.tsx.example +28 -1
- package/templates/app-demo/src/pages/ProductsPage.tsx.example +2 -0
- package/templates/app-demo/src/pages/ProfilePage.tsx.example +2 -0
- package/templates/app-demo/src/pages/SettingsPage.tsx.example +2 -0
- package/templates/app-demo/src/pages/ShowcaseDetailPage.tsx.example +22 -16
- package/templates/app-demo/src/pages/ShowcasePage.tsx.example +3 -1
- package/templates/app-demo/src/pages/components/ComponentRenderer.tsx.example +147 -51
- package/templates/app-demo/src/pages/components/ComponentsData.tsx.example +103 -21
- package/templates/app-demo/src/pages/components/componentConfig.ts.example +139 -59
- package/templates/app-demo/src/pages/legal/LegalPage.tsx.example +12 -1
- package/templates/app-demo/src/pages/legal/PrivacyPage.tsx.example +10 -1
- package/templates/app-demo/src/pages/legal/TermsPage.tsx.example +10 -1
- package/templates/app-demo/src/themes.css.example +289 -77
- package/templates/app-demo/stats.html.example +4949 -0
- package/templates/app-dndev/index.html.example +164 -0
- package/templates/app-dndev/public/logo.svg.example +1 -0
- package/templates/app-dndev/public/manifest.json.example +10 -0
- package/templates/app-dndev/src/App.tsx.example +35 -0
- package/templates/app-dndev/src/components/CockpitLayout.css.example +181 -0
- package/templates/app-dndev/src/components/CockpitLayout.tsx.example +209 -0
- package/templates/app-dndev/src/components/Kanban.css.example +385 -0
- package/templates/app-dndev/src/components/ModeToggle.tsx.example +32 -0
- package/templates/app-dndev/src/components/OverlaySlot.tsx.example +68 -0
- package/templates/app-dndev/src/components/TerminalPanel.css.example +228 -0
- package/templates/app-dndev/src/components/TerminalPanel.tsx.example +714 -0
- package/templates/app-dndev/src/components/markdown-prose.css.example +49 -0
- package/templates/app-dndev/src/components/phases/CaptainLog.tsx.example +107 -0
- package/templates/app-dndev/src/components/phases/ContextTabs.tsx.example +352 -0
- package/templates/app-dndev/src/components/phases/PhaseCard.tsx.example +126 -0
- package/templates/app-dndev/src/components/phases/PhaseDetail.tsx.example +147 -0
- package/templates/app-dndev/src/components/phases/ReviewPanel.tsx.example +115 -0
- package/templates/app-dndev/src/components/phases/phaseData.ts.example +366 -0
- package/templates/app-dndev/src/config/app.ts.example +103 -0
- package/templates/app-dndev/src/config/commands.ts.example +171 -0
- package/templates/app-dndev/src/config/legal.ts.example +170 -0
- package/templates/app-dndev/src/config/providers.ts.example +7 -0
- package/templates/app-dndev/src/globals.css.example +10 -0
- package/templates/app-dndev/src/hooks/useDndevFile.ts.example +144 -0
- package/templates/app-dndev/src/main.tsx.example +21 -0
- package/templates/app-dndev/src/pages/BoardPage.tsx.example +640 -0
- package/templates/app-dndev/src/pages/GrillPage.tsx.example +658 -0
- package/templates/app-dndev/src/pages/HomePage.tsx.example +347 -0
- package/templates/app-dndev/src/pages/NotFoundPage.tsx.example +33 -0
- package/templates/app-dndev/src/pages/PhasesPage.tsx.example +137 -0
- package/templates/app-dndev/src/pages/SettingsPage.tsx.example +64 -0
- package/templates/app-dndev/src/pages/legal/LegalNoticePage.tsx.example +75 -0
- package/templates/app-dndev/src/pages/legal/PrivacyPage.tsx.example +69 -0
- package/templates/app-dndev/src/pages/legal/TermsPage.tsx.example +71 -0
- package/templates/app-dndev/src/stores/dndevStore.ts.example +386 -0
- package/templates/app-dndev/src/themes.css.example +161 -0
- package/templates/app-dndev/terminal-sidecar.cjs.example +341 -0
- package/templates/app-dndev/tsconfig.json.example +9 -0
- package/templates/app-dndev/vite.config.ts.example +24 -0
- package/templates/app-vite/index.html.example +1 -1
- package/templates/functions-supabase/supabase/functions/.env.example +0 -2
- package/templates/root-consumer/.claude/commands/grill.md.example +86 -8
- package/templates/root-consumer/.dndev.secrets.example +32 -0
- package/templates/root-consumer/.gitignore.example +3 -0
- package/templates/root-consumer/AI.md.example +4 -0
- package/templates/root-consumer/entities/index.ts.example +2 -5
- package/templates/root-consumer/guides/dndev/COMPONENTS_ATOMIC.md.example +4 -0
- package/templates/root-consumer/guides/dndev/ENV_SETUP.md.example +23 -20
- package/templates/root-consumer/guides/dndev/INDEX.md.example +1 -0
- package/templates/root-consumer/guides/dndev/SETUP_BILLING.md.example +3 -7
- package/templates/root-consumer/guides/dndev/SETUP_CICD.md.example +115 -0
- package/templates/root-consumer/guides/dndev/SETUP_CRUD.md.example +41 -0
- package/templates/root-consumer/guides/dndev/SETUP_SUPABASE.md.example +13 -18
- package/templates/root-consumer/guides/dndev/SETUP_VERCEL.md.example +17 -12
- package/templates/root-consumer/guides/wai-way/WAI_WAY_CLI.md.example +185 -251
- package/templates/root-consumer/guides/wai-way/agents/extractor.md.example +26 -8
- package/templates/root-consumer/guides/wai-way/blueprints/0_brainstorm.md.example +66 -49
- package/templates/root-consumer/guides/wai-way/blueprints/1_scaffold.md.example +6 -5
- package/templates/root-consumer/guides/wai-way/blueprints/2_entities.md.example +9 -9
- package/templates/root-consumer/guides/wai-way/blueprints/3_compose.md.example +1 -1
- package/templates/root-consumer/guides/wai-way/blueprints/4_configure.md.example +7 -6
- package/templates/root-consumer/guides/wai-way/context_map.json.example +51 -20
- package/templates/root-consumer/guides/wai-way/hld_template.md.example +138 -0
- package/templates/root-consumer/guides/wai-way/lld_template.md.example +103 -0
- package/templates/root-consumer/guides/wai-way/prd_template.md.example +140 -0
- /package/templates/{root-consumer → app-demo}/entities/Contact.ts.example +0 -0
- /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
|
+
}
|