From d422ba8827d6a54bcc4a0673a99e9c29d63947dc Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 6 Feb 2026 18:23:14 +0000 Subject: [PATCH 1/3] Add Tech Stack, Cost Estimator, and QA Test Cases phases Add three new phases to the Design OS navigation between Sections and Export: - Tech Stack & Architecture: define technology choices and architecture layers - Cost Estimator & Optimizer: estimate infrastructure costs by tier with optimizations - QA Test Cases: generate and display test cases grouped by section and priority Each phase includes a page component, data loader, types, empty state, and proper phase navigation chaining. https://claude.ai/code/session_0174cnM1vH9SFg62nrc7poKr --- src/components/CostEstimatorPage.tsx | 141 +++++++++++++++++++++++ src/components/EmptyState.tsx | 22 +++- src/components/NextPhaseButton.tsx | 5 +- src/components/PhaseNav.tsx | 20 +++- src/components/QaTestsPage.tsx | 164 +++++++++++++++++++++++++++ src/components/SectionsPage.tsx | 2 +- src/components/TechStackPage.tsx | 154 +++++++++++++++++++++++++ src/lib/cost-estimator-loader.ts | 99 ++++++++++++++++ src/lib/product-loader.ts | 8 +- src/lib/qa-tests-loader.ts | 108 ++++++++++++++++++ src/lib/router.tsx | 15 +++ src/lib/tech-stack-loader.ts | 112 ++++++++++++++++++ src/types/product.ts | 66 +++++++++++ 13 files changed, 909 insertions(+), 7 deletions(-) create mode 100644 src/components/CostEstimatorPage.tsx create mode 100644 src/components/QaTestsPage.tsx create mode 100644 src/components/TechStackPage.tsx create mode 100644 src/lib/cost-estimator-loader.ts create mode 100644 src/lib/qa-tests-loader.ts create mode 100644 src/lib/tech-stack-loader.ts diff --git a/src/components/CostEstimatorPage.tsx b/src/components/CostEstimatorPage.tsx new file mode 100644 index 00000000..65a2d295 --- /dev/null +++ b/src/components/CostEstimatorPage.tsx @@ -0,0 +1,141 @@ +import { useMemo } from 'react' +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card' +import { AppLayout } from '@/components/AppLayout' +import { EmptyState } from '@/components/EmptyState' +import { StepIndicator, type StepStatus } from '@/components/StepIndicator' +import { NextPhaseButton } from '@/components/NextPhaseButton' +import { loadProductData } from '@/lib/product-loader' + +export function CostEstimatorPage() { + const productData = useMemo(() => loadProductData(), []) + const costEstimate = productData.costEstimate + + const hasCostEstimate = !!costEstimate + const stepStatus: StepStatus = hasCostEstimate ? 'completed' : 'current' + + return ( + +
+ {/* Page intro */} +
+

+ Cost Estimator & Optimizer +

+

+ Estimate infrastructure and service costs, and identify optimizations. +

+
+ + {/* Step 1: Cost Estimate */} + + {!costEstimate ? ( + + ) : ( +
+ {/* Cost Tiers */} + {costEstimate.tiers.map((tier, index) => ( + + + + {tier.name} + + {tier.users} + + + + +
+ {/* Total */} +
+ + Estimated Monthly Cost + + + ${tier.monthlyCost.toLocaleString()}/mo + +
+ + {/* Line items */} + {tier.items.length > 0 && ( +
+ {tier.items.map((item, ii) => ( +
+
+
+ {item.category} +
+
+ {item.item} +
+ {item.notes && ( +
+ {item.notes} +
+ )} +
+
+ ${item.monthlyCost.toLocaleString()}/mo +
+
+ ))} +
+ )} +
+
+
+ ))} + + {/* Optimizations */} + {costEstimate.optimizations.length > 0 && ( + + + + Cost Optimizations + + ({costEstimate.optimizations.length}) + + + + +
    + {costEstimate.optimizations.map((opt, index) => ( +
  • + + + {opt} + +
  • + ))} +
+
+
+ )} + + {/* Edit hint */} +
+

+ To update the cost estimate, run{' '} + /cost-estimator{' '} + or edit the file directly at{' '} + + product/cost-estimator/cost-estimate.md + +

+
+
+ )} +
+ + {/* Next Phase Button */} + {hasCostEstimate && ( + + + + )} +
+
+ ) +} diff --git a/src/components/EmptyState.tsx b/src/components/EmptyState.tsx index 6a8e4d0e..be34f05b 100644 --- a/src/components/EmptyState.tsx +++ b/src/components/EmptyState.tsx @@ -1,7 +1,7 @@ -import { FileText, Map, ClipboardList, Database, Layout, Package, Boxes, Palette, PanelLeft } from 'lucide-react' +import { FileText, Map, ClipboardList, Database, Layout, Package, Boxes, Palette, PanelLeft, Server, DollarSign, TestTube } from 'lucide-react' import { Card, CardContent } from '@/components/ui/card' -type EmptyStateType = 'overview' | 'roadmap' | 'spec' | 'data' | 'screen-designs' | 'data-model' | 'design-system' | 'shell' | 'export' +type EmptyStateType = 'overview' | 'roadmap' | 'spec' | 'data' | 'screen-designs' | 'data-model' | 'design-system' | 'shell' | 'export' | 'tech-stack' | 'cost-estimator' | 'qa-tests' interface EmptyStateProps { type: EmptyStateType @@ -67,6 +67,24 @@ const config: Record, { label: string; icon: type 'data-model': { label: 'Data Model', icon: Boxes, path: '/data-model' }, 'design': { label: 'Design', icon: Layout, path: '/design' }, 'sections': { label: 'Sections', icon: LayoutList, path: '/sections' }, + 'tech-stack': { label: 'Tech Stack', icon: Server, path: '/tech-stack' }, + 'cost-estimator': { label: 'Cost Estimator', icon: DollarSign, path: '/cost-estimator' }, + 'qa-tests': { label: 'QA Tests', icon: TestTube, path: '/qa-tests' }, 'export': { label: 'Export', icon: Package, path: '/export' }, } diff --git a/src/components/PhaseNav.tsx b/src/components/PhaseNav.tsx index 9f876183..95329119 100644 --- a/src/components/PhaseNav.tsx +++ b/src/components/PhaseNav.tsx @@ -1,10 +1,10 @@ import { useLocation, useNavigate } from 'react-router-dom' import { useMemo } from 'react' -import { FileText, Boxes, Layout, LayoutList, Package } from 'lucide-react' +import { FileText, Boxes, Layout, LayoutList, Package, Server, DollarSign, TestTube } from 'lucide-react' import { loadProductData, hasExportZip } from '@/lib/product-loader' import { getAllSectionIds, getSectionScreenDesigns } from '@/lib/section-loader' -export type Phase = 'product' | 'data-model' | 'design' | 'sections' | 'export' +export type Phase = 'product' | 'data-model' | 'design' | 'sections' | 'tech-stack' | 'cost-estimator' | 'qa-tests' | 'export' interface PhaseConfig { id: Phase @@ -18,6 +18,9 @@ const phases: PhaseConfig[] = [ { id: 'data-model', label: 'Data Model', icon: Boxes, path: '/data-model' }, { id: 'design', label: 'Design', icon: Layout, path: '/design' }, { id: 'sections', label: 'Sections', icon: LayoutList, path: '/sections' }, + { id: 'tech-stack', label: 'Tech Stack', icon: Server, path: '/tech-stack' }, + { id: 'cost-estimator', label: 'Costs', icon: DollarSign, path: '/cost-estimator' }, + { id: 'qa-tests', label: 'QA Tests', icon: TestTube, path: '/qa-tests' }, { id: 'export', label: 'Export', icon: Package, path: '/export' }, ] @@ -58,6 +61,12 @@ function usePhaseStatuses(): PhaseInfo[] { currentPhaseId = 'design' } else if (currentPath === '/sections' || currentPath.startsWith('/sections/')) { currentPhaseId = 'sections' + } else if (currentPath === '/tech-stack') { + currentPhaseId = 'tech-stack' + } else if (currentPath === '/cost-estimator') { + currentPhaseId = 'cost-estimator' + } else if (currentPath === '/qa-tests') { + currentPhaseId = 'qa-tests' } else if (currentPath === '/export') { currentPhaseId = 'export' } @@ -65,12 +74,19 @@ function usePhaseStatuses(): PhaseInfo[] { // Check if export zip exists const exportZipExists = hasExportZip() + const hasTechStack = !!productData.techStack + const hasCostEstimate = !!productData.costEstimate + const hasQaTests = !!productData.qaTests + // Determine completion status const phaseComplete: Record = { 'product': hasOverview && hasRoadmap, 'data-model': hasDataModel, 'design': hasDesignSystem || hasShell, 'sections': hasSections, + 'tech-stack': hasTechStack, + 'cost-estimator': hasCostEstimate, + 'qa-tests': hasQaTests, 'export': exportZipExists, } diff --git a/src/components/QaTestsPage.tsx b/src/components/QaTestsPage.tsx new file mode 100644 index 00000000..568bf0f9 --- /dev/null +++ b/src/components/QaTestsPage.tsx @@ -0,0 +1,164 @@ +import { useMemo } from 'react' +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card' +import { AppLayout } from '@/components/AppLayout' +import { EmptyState } from '@/components/EmptyState' +import { StepIndicator, type StepStatus } from '@/components/StepIndicator' +import { NextPhaseButton } from '@/components/NextPhaseButton' +import { loadProductData } from '@/lib/product-loader' +import type { TestCase } from '@/types/product' + +const priorityStyles: Record = { + critical: 'bg-red-100 dark:bg-red-900/30 text-red-700 dark:text-red-400', + high: 'bg-amber-100 dark:bg-amber-900/30 text-amber-700 dark:text-amber-400', + medium: 'bg-blue-100 dark:bg-blue-900/30 text-blue-700 dark:text-blue-400', + low: 'bg-stone-100 dark:bg-stone-700 text-stone-600 dark:text-stone-400', +} + +export function QaTestsPage() { + const productData = useMemo(() => loadProductData(), []) + const qaTests = productData.qaTests + + const hasQaTests = !!qaTests + const stepStatus: StepStatus = hasQaTests ? 'completed' : 'current' + + // Group test cases by section + const groupedTests = useMemo(() => { + if (!qaTests) return new Map() + const groups = new Map() + for (const tc of qaTests.testCases) { + const section = tc.section || 'General' + if (!groups.has(section)) groups.set(section, []) + groups.get(section)!.push(tc) + } + return groups + }, [qaTests]) + + return ( + +
+ {/* Page intro */} +
+

+ QA Test Cases +

+

+ Generated test cases for validating your product's functionality. +

+
+ + {/* Step 1: Test Cases */} + + {!qaTests ? ( + + ) : ( +
+ {/* Coverage Summary */} + {qaTests.coverageSummary && ( + + + + Coverage Summary + + + +

+ {qaTests.coverageSummary} +

+
+
+ )} + + {/* Stats */} +
+ {(['critical', 'high', 'medium', 'low'] as const).map((priority) => { + const count = qaTests.testCases.filter(tc => tc.priority === priority).length + return ( +
+
{count}
+
c.startsWith('text-')).join(' ')}`}> + {priority} +
+
+ ) + })} +
+ + {/* Test Cases by Section */} + {[...groupedTests.entries()].map(([section, cases]) => ( + + + + {section} + + ({cases.length} tests) + + + + +
+ {cases.map((tc) => ( +
+
+ + {tc.id} + + + {tc.priority} + +
+

+ {tc.title} +

+ {tc.steps.length > 0 && ( +
    + {tc.steps.map((step, si) => ( +
  1. + + {si + 1}. + + {step} +
  2. + ))} +
+ )} + {tc.expectedResult && ( +
+ Expected: + {tc.expectedResult} +
+ )} +
+ ))} +
+
+
+ ))} + + {/* Edit hint */} +
+

+ To regenerate test cases, run{' '} + /qa-tests{' '} + or edit the file directly at{' '} + + product/qa-tests/qa-tests.md + +

+
+
+ )} +
+ + {/* Next Phase Button */} + {hasQaTests && ( + + + + )} +
+
+ ) +} diff --git a/src/components/SectionsPage.tsx b/src/components/SectionsPage.tsx index 3ccb8db3..dc860fa1 100644 --- a/src/components/SectionsPage.tsx +++ b/src/components/SectionsPage.tsx @@ -148,7 +148,7 @@ export function SectionsPage() { {/* Next Phase Button - shown when all sections are complete */} {sections.length > 0 && completedSections === sections.length && ( - + )} diff --git a/src/components/TechStackPage.tsx b/src/components/TechStackPage.tsx new file mode 100644 index 00000000..4c3265c3 --- /dev/null +++ b/src/components/TechStackPage.tsx @@ -0,0 +1,154 @@ +import { useMemo } from 'react' +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card' +import { AppLayout } from '@/components/AppLayout' +import { EmptyState } from '@/components/EmptyState' +import { StepIndicator, type StepStatus } from '@/components/StepIndicator' +import { NextPhaseButton } from '@/components/NextPhaseButton' +import { loadProductData } from '@/lib/product-loader' + +export function TechStackPage() { + const productData = useMemo(() => loadProductData(), []) + const techStack = productData.techStack + + const hasTechStack = !!techStack + const stepStatus: StepStatus = hasTechStack ? 'completed' : 'current' + + return ( + +
+ {/* Page intro */} +
+

+ Tech Stack & Architecture +

+

+ Define the technology choices and architecture for your product. +

+
+ + {/* Step 1: Tech Stack */} + + {!techStack ? ( + + ) : ( +
+ {/* Technology Choices */} + + + + Technology Choices + + ({techStack.choices.length}) + + + + + {techStack.choices.length === 0 ? ( +

No technology choices defined.

+ ) : ( +
+ {techStack.choices.map((choice, index) => ( +
+
+ {choice.category} +
+

+ {choice.choice} +

+

+ {choice.rationale} +

+
+ ))} +
+ )} +
+
+ + {/* Architecture Layers */} + + + + Architecture + + ({techStack.architecture.length} layers) + + + + + {techStack.architecture.length === 0 ? ( +

No architecture layers defined.

+ ) : ( +
+ {techStack.architecture.map((layer, index) => ( +
+

+ {layer.name} +

+

+ {layer.description} +

+ {layer.components.length > 0 && ( +
    + {layer.components.map((component, ci) => ( +
  • + + {component} +
  • + ))} +
+ )} +
+ ))} +
+ )} +
+
+ + {/* Architecture Diagram */} + {techStack.diagram && ( + + + + Architecture Diagram + + + +
+                      {techStack.diagram}
+                    
+
+
+ )} + + {/* Edit hint */} +
+

+ To update the tech stack, run{' '} + /tech-stack{' '} + or edit the file directly at{' '} + + product/tech-stack/tech-stack.md + +

+
+
+ )} +
+ + {/* Next Phase Button */} + {hasTechStack && ( + + + + )} +
+
+ ) +} diff --git a/src/lib/cost-estimator-loader.ts b/src/lib/cost-estimator-loader.ts new file mode 100644 index 00000000..b59304f4 --- /dev/null +++ b/src/lib/cost-estimator-loader.ts @@ -0,0 +1,99 @@ +/** + * Cost Estimator & Optimizer data loading utilities + */ + +import type { CostEstimate, CostTier, CostLineItem } from '@/types/product' + +// Load cost estimator files from /product/cost-estimator/ directory at build time +const costFiles = import.meta.glob('/product/cost-estimator/*.md', { + query: '?raw', + import: 'default', + eager: true, +}) as Record + +/** + * Parse cost-estimate.md content into CostEstimate structure + * + * Expected format: + * # Cost Estimate + * + * ## [Tier Name] ([User Count]) + * + * | Category | Item | Monthly Cost | Notes | + * |----------|------|-------------|-------| + * | Hosting | AWS | $50 | ... | + * + * ## Optimizations + * - Optimization 1 + * - Optimization 2 + */ +export function parseCostEstimate(md: string): CostEstimate | null { + if (!md || !md.trim()) return null + + try { + const tiers: CostTier[] = [] + const optimizations: string[] = [] + + // Extract tiers - ## Tier Name (user count) + const tierMatches = [...md.matchAll(/## (.+?)\s*\((.+?)\)\s*\n+([\s\S]*?)(?=\n## |\n#[^#]|$)/g)] + for (const match of tierMatches) { + const name = match[1].trim() + const users = match[2].trim() + const body = match[3].trim() + const items: CostLineItem[] = [] + + // Parse markdown table rows + const tableRows = body.split('\n').filter(line => line.includes('|') && !line.match(/^\s*\|[\s-|]+\|\s*$/)) + for (const row of tableRows) { + const cells = row.split('|').map(c => c.trim()).filter(Boolean) + if (cells.length >= 4 && cells[0] !== 'Category') { + const costStr = cells[2].replace(/[^0-9.]/g, '') + items.push({ + category: cells[0], + item: cells[1], + monthlyCost: parseFloat(costStr) || 0, + notes: cells[3], + }) + } + } + + const monthlyCost = items.reduce((sum, item) => sum + item.monthlyCost, 0) + tiers.push({ name, users, monthlyCost, items }) + } + + // Extract optimizations + const optSection = md.match(/## Optimizations\s*\n+([\s\S]*?)(?=\n## |\n#[^#]|$)/) + if (optSection?.[1]) { + const lines = optSection[1].split('\n') + for (const line of lines) { + const trimmed = line.trim() + if (trimmed.startsWith('- ')) { + optimizations.push(trimmed.slice(2).trim()) + } + } + } + + if (tiers.length === 0 && optimizations.length === 0) { + return null + } + + return { tiers, optimizations } + } catch { + return null + } +} + +/** + * Load cost estimate data + */ +export function loadCostEstimate(): CostEstimate | null { + const content = costFiles['/product/cost-estimator/cost-estimate.md'] + return content ? parseCostEstimate(content) : null +} + +/** + * Check if cost estimate has been defined + */ +export function hasCostEstimate(): boolean { + return '/product/cost-estimator/cost-estimate.md' in costFiles +} diff --git a/src/lib/product-loader.ts b/src/lib/product-loader.ts index 39508a9c..8c3a534b 100644 --- a/src/lib/product-loader.ts +++ b/src/lib/product-loader.ts @@ -6,6 +6,9 @@ import type { ProductOverview, ProductRoadmap, Problem, Section, ProductData } f import { loadDataModel, hasDataModel } from './data-model-loader' import { loadDesignSystem, hasDesignSystem } from './design-system-loader' import { loadShellInfo, hasShell } from './shell-loader' +import { loadTechStack, hasTechStack } from './tech-stack-loader' +import { loadCostEstimate, hasCostEstimate } from './cost-estimator-loader' +import { loadQaTests, hasQaTests } from './qa-tests-loader' // Load markdown files from /product/ directory at build time const productFiles = import.meta.glob('/product/*.md', { @@ -164,6 +167,9 @@ export function loadProductData(): ProductData { dataModel: loadDataModel(), designSystem: loadDesignSystem(), shell: loadShellInfo(), + techStack: loadTechStack(), + costEstimate: loadCostEstimate(), + qaTests: loadQaTests(), } } @@ -196,4 +202,4 @@ export function getExportZipUrl(): string | null { } // Re-export utility functions for checking individual pieces -export { hasDataModel, hasDesignSystem, hasShell } +export { hasDataModel, hasDesignSystem, hasShell, hasTechStack, hasCostEstimate, hasQaTests } diff --git a/src/lib/qa-tests-loader.ts b/src/lib/qa-tests-loader.ts new file mode 100644 index 00000000..32ef7502 --- /dev/null +++ b/src/lib/qa-tests-loader.ts @@ -0,0 +1,108 @@ +/** + * QA Test Case Generator data loading utilities + */ + +import type { QaTestSuite, TestCase } from '@/types/product' + +// Load QA test files from /product/qa-tests/ directory at build time +const qaFiles = import.meta.glob('/product/qa-tests/*.md', { + query: '?raw', + import: 'default', + eager: true, +}) as Record + +/** + * Parse qa-tests.md content into QaTestSuite structure + * + * Expected format: + * # QA Test Cases + * + * ## Coverage Summary + * [Summary text] + * + * ## Test Cases + * + * ### TC-001: [Title] + * **Section:** [Section Name] + * **Priority:** critical|high|medium|low + * **Steps:** + * 1. Step one + * 2. Step two + * **Expected Result:** [Result description] + */ +export function parseQaTests(md: string): QaTestSuite | null { + if (!md || !md.trim()) return null + + try { + const testCases: TestCase[] = [] + let coverageSummary = '' + + // Extract Coverage Summary + const summaryMatch = md.match(/## Coverage Summary\s*\n+([\s\S]*?)(?=\n## |\n#[^#]|$)/) + if (summaryMatch?.[1]) { + coverageSummary = summaryMatch[1].trim() + } + + // Extract Test Cases + const casesSection = md.match(/## Test Cases\s*\n+([\s\S]*?)(?=\n## [^#]|\n#[^#]|$)/) + if (casesSection?.[1]) { + const caseMatches = [...casesSection[1].matchAll(/### (TC-\d+):\s*(.+)\n+([\s\S]*?)(?=\n### |\n## |$)/g)] + for (const match of caseMatches) { + const id = match[1].trim() + const title = match[2].trim() + const body = match[3].trim() + + const sectionMatch = body.match(/\*\*Section:\*\*\s*(.+)/) + const priorityMatch = body.match(/\*\*Priority:\*\*\s*(.+)/) + const expectedMatch = body.match(/\*\*Expected Result:\*\*\s*(.+)/) + + const steps: string[] = [] + const stepsSection = body.match(/\*\*Steps:\*\*\s*\n+([\s\S]*?)(?=\*\*Expected|\n### |$)/) + if (stepsSection?.[1]) { + const lines = stepsSection[1].split('\n') + for (const line of lines) { + const trimmed = line.trim() + const stepMatch = trimmed.match(/^\d+\.\s+(.+)/) + if (stepMatch) { + steps.push(stepMatch[1].trim()) + } + } + } + + const priority = (priorityMatch?.[1]?.trim().toLowerCase() || 'medium') as TestCase['priority'] + + testCases.push({ + id, + title, + section: sectionMatch?.[1]?.trim() || '', + priority: ['critical', 'high', 'medium', 'low'].includes(priority) ? priority : 'medium', + steps, + expectedResult: expectedMatch?.[1]?.trim() || '', + }) + } + } + + if (testCases.length === 0) { + return null + } + + return { testCases, coverageSummary } + } catch { + return null + } +} + +/** + * Load QA test suite data + */ +export function loadQaTests(): QaTestSuite | null { + const content = qaFiles['/product/qa-tests/qa-tests.md'] + return content ? parseQaTests(content) : null +} + +/** + * Check if QA tests have been defined + */ +export function hasQaTests(): boolean { + return '/product/qa-tests/qa-tests.md' in qaFiles +} diff --git a/src/lib/router.tsx b/src/lib/router.tsx index 6f32118c..63459982 100644 --- a/src/lib/router.tsx +++ b/src/lib/router.tsx @@ -6,6 +6,9 @@ import { SectionsPage } from '@/components/SectionsPage' import { SectionPage } from '@/components/SectionPage' import { ScreenDesignPage, ScreenDesignFullscreen } from '@/components/ScreenDesignPage' import { ShellDesignPage, ShellDesignFullscreen } from '@/components/ShellDesignPage' +import { TechStackPage } from '@/components/TechStackPage' +import { CostEstimatorPage } from '@/components/CostEstimatorPage' +import { QaTestsPage } from '@/components/QaTestsPage' import { ExportPage } from '@/components/ExportPage' export const router = createBrowserRouter([ @@ -45,6 +48,18 @@ export const router = createBrowserRouter([ path: '/shell/design/fullscreen', element: , }, + { + path: '/tech-stack', + element: , + }, + { + path: '/cost-estimator', + element: , + }, + { + path: '/qa-tests', + element: , + }, { path: '/export', element: , diff --git a/src/lib/tech-stack-loader.ts b/src/lib/tech-stack-loader.ts new file mode 100644 index 00000000..fd1bcec9 --- /dev/null +++ b/src/lib/tech-stack-loader.ts @@ -0,0 +1,112 @@ +/** + * Tech Stack & Architecture data loading utilities + */ + +import type { TechStack, TechChoice, ArchitectureLayer } from '@/types/product' + +// Load tech stack files from /product/tech-stack/ directory at build time +const techStackFiles = import.meta.glob('/product/tech-stack/*.md', { + query: '?raw', + import: 'default', + eager: true, +}) as Record + +/** + * Parse tech-stack.md content into TechStack structure + * + * Expected format: + * # Tech Stack & Architecture + * + * ## Technology Choices + * + * ### [Category] + * **Choice:** [Technology] + * **Rationale:** [Why this choice] + * + * ## Architecture + * + * ### [Layer Name] + * [Description] + * - Component 1 + * - Component 2 + * + * ## Architecture Diagram + * ``` + * [ASCII or text diagram] + * ``` + */ +export function parseTechStack(md: string): TechStack | null { + if (!md || !md.trim()) return null + + try { + const choices: TechChoice[] = [] + const architecture: ArchitectureLayer[] = [] + let diagram = '' + + // Extract Technology Choices section + const choicesSection = md.match(/## Technology Choices\s*\n+([\s\S]*?)(?=\n## |\n#[^#]|$)/) + if (choicesSection?.[1]) { + const choiceMatches = [...choicesSection[1].matchAll(/### (.+)\n+([\s\S]*?)(?=\n### |\n## |$)/g)] + for (const match of choiceMatches) { + const category = match[1].trim() + const body = match[2].trim() + const choiceMatch = body.match(/\*\*Choice:\*\*\s*(.+)/) + const rationaleMatch = body.match(/\*\*Rationale:\*\*\s*(.+)/) + choices.push({ + category, + choice: choiceMatch?.[1]?.trim() || '', + rationale: rationaleMatch?.[1]?.trim() || '', + }) + } + } + + // Extract Architecture section + const archSection = md.match(/## Architecture\s*\n+([\s\S]*?)(?=\n## |\n#[^#]|$)/) + if (archSection?.[1]) { + const layerMatches = [...archSection[1].matchAll(/### (.+)\n+([\s\S]*?)(?=\n### |\n## |$)/g)] + for (const match of layerMatches) { + const name = match[1].trim() + const body = match[2].trim() + const lines = body.split('\n') + const description = lines[0]?.trim() || '' + const components: string[] = [] + for (const line of lines.slice(1)) { + const trimmed = line.trim() + if (trimmed.startsWith('- ')) { + components.push(trimmed.slice(2).trim()) + } + } + architecture.push({ name, description, components }) + } + } + + // Extract Architecture Diagram + const diagramMatch = md.match(/## Architecture Diagram\s*\n+```[\s\S]*?\n([\s\S]*?)```/) + if (diagramMatch?.[1]) { + diagram = diagramMatch[1].trim() + } + + if (choices.length === 0 && architecture.length === 0) { + return null + } + + return { choices, architecture, diagram } + } catch { + return null + } +} + +/** + * Load tech stack data + */ +export function loadTechStack(): TechStack | null { + const content = techStackFiles['/product/tech-stack/tech-stack.md'] + return content ? parseTechStack(content) : null +} + +/** + * Check if tech stack has been defined + */ +export function hasTechStack(): boolean { + return '/product/tech-stack/tech-stack.md' in techStackFiles +} diff --git a/src/types/product.ts b/src/types/product.ts index b0de80a9..82cbb867 100644 --- a/src/types/product.ts +++ b/src/types/product.ts @@ -84,6 +84,69 @@ export interface ShellInfo { hasComponents: boolean } +// ============================================================================= +// Tech Stack & Architecture +// ============================================================================= + +export interface TechChoice { + category: string + choice: string + rationale: string +} + +export interface ArchitectureLayer { + name: string + description: string + components: string[] +} + +export interface TechStack { + choices: TechChoice[] + architecture: ArchitectureLayer[] + diagram: string +} + +// ============================================================================= +// Cost Estimator +// ============================================================================= + +export interface CostLineItem { + category: string + item: string + monthlyCost: number + notes: string +} + +export interface CostTier { + name: string + users: string + monthlyCost: number + items: CostLineItem[] +} + +export interface CostEstimate { + tiers: CostTier[] + optimizations: string[] +} + +// ============================================================================= +// QA Test Cases +// ============================================================================= + +export interface TestCase { + id: string + title: string + section: string + priority: 'critical' | 'high' | 'medium' | 'low' + steps: string[] + expectedResult: string +} + +export interface QaTestSuite { + testCases: TestCase[] + coverageSummary: string +} + // ============================================================================= // Combined Product Data // ============================================================================= @@ -94,4 +157,7 @@ export interface ProductData { dataModel: DataModel | null designSystem: DesignSystem | null shell: ShellInfo | null + techStack: TechStack | null + costEstimate: CostEstimate | null + qaTests: QaTestSuite | null } From 828054bf7eea734fb8cb406b239530e4fdfc2f39 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 7 Feb 2026 10:26:34 +0000 Subject: [PATCH 2/3] Add Figma integration to Design tab Add a new step in the Design page for linking and previewing Figma files: - Embedded Figma preview via iframe for quick visual reference - Linked files list with type labels (file, prototype, board, frame) - Configurable via product/design-system/figma.json - New FigmaIntegration type with fileUrl, embedUrl, links, and accessToken - Dedicated figma-loader for build-time config loading - Empty state with guidance for setting up the integration https://claude.ai/code/session_0174cnM1vH9SFg62nrc7poKr --- src/components/DesignPage.tsx | 124 ++++++++++++++++++++++++++++++-- src/components/EmptyState.tsx | 10 ++- src/lib/design-system-loader.ts | 15 +++- src/lib/figma-loader.ts | 62 ++++++++++++++++ src/types/product.ts | 18 +++++ 5 files changed, 217 insertions(+), 12 deletions(-) create mode 100644 src/lib/figma-loader.ts diff --git a/src/components/DesignPage.tsx b/src/components/DesignPage.tsx index 46c43f4b..59e08de0 100644 --- a/src/components/DesignPage.tsx +++ b/src/components/DesignPage.tsx @@ -6,7 +6,7 @@ import { EmptyState } from '@/components/EmptyState' import { StepIndicator, type StepStatus } from '@/components/StepIndicator' import { NextPhaseButton } from '@/components/NextPhaseButton' import { loadProductData } from '@/lib/product-loader' -import { ChevronRight, Layout } from 'lucide-react' +import { ChevronRight, Layout, ExternalLink, Figma } from 'lucide-react' // Map Tailwind color names to actual color values for preview const colorMap: Record = { @@ -34,12 +34,20 @@ const colorMap: Record = stone: { light: '#d6d3d1', base: '#78716c', dark: '#57534e' }, } +const figmaLinkTypeLabels: Record = { + file: 'Design File', + prototype: 'Prototype', + board: 'Board', + frame: 'Frame', +} + /** * Determine the status of each step on the Design page - * Steps: 1. Design Tokens, 2. Shell Design + * Steps: 1. Design Tokens, 2. Figma Integration, 3. Shell Design */ function getDesignPageStepStatuses( hasDesignSystem: boolean, + hasFigma: boolean, hasShell: boolean ): StepStatus[] { const statuses: StepStatus[] = [] @@ -51,7 +59,16 @@ function getDesignPageStepStatuses( statuses.push('current') } - // Step 2: Shell + // Step 2: Figma Integration (optional — skip-friendly) + if (hasFigma) { + statuses.push('completed') + } else if (hasDesignSystem) { + statuses.push('current') + } else { + statuses.push('upcoming') + } + + // Step 3: Shell if (hasShell) { statuses.push('completed') } else if (hasDesignSystem) { @@ -69,10 +86,11 @@ export function DesignPage() { const shell = productData.shell const hasDesignSystem = !!(designSystem?.colors || designSystem?.typography) + const hasFigmaLinks = !!designSystem?.figma const hasShell = !!shell?.spec const allStepsComplete = hasDesignSystem && hasShell - const stepStatuses = getDesignPageStepStatuses(hasDesignSystem, hasShell) + const stepStatuses = getDesignPageStepStatuses(hasDesignSystem, hasFigmaLinks, hasShell) return ( @@ -162,8 +180,100 @@ export function DesignPage() { )} - {/* Step 2: Application Shell */} - + {/* Step 2: Figma Integration */} + + {!designSystem?.figma ? ( + + ) : ( + + + + + Figma Integration + + + + {/* Figma Embed */} + {designSystem.figma.embedUrl && ( +
+

+ Preview +

+
+