Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
483 changes: 483 additions & 0 deletions .claude/agents/design-research.md

Large diffs are not rendered by default.

12 changes: 12 additions & 0 deletions .claude/settings.local.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"permissions": {
"allow": [
"Skill(design-os:product-vision)",
"Bash(test:*)",
"mcp__MCP_DOCKER__mcp-find",
"mcp__MCP_DOCKER__search_repositories",
"WebSearch",
"Skill(validate-output)"
]
}
}
21 changes: 21 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,27 @@
Get notified of major releases by subscribing here:
https://buildermethods.com/design-os

## [0.1.3] - 2026-01-05

### Changed
- Replaced build-time `import.meta.glob` with runtime `fetch()` for loading product markdown and JSON files
- All page components now use async data loading pattern via `useProductData` hook

### Added
- New `useProductData` hook (`src/lib/use-product-data.ts`) for consistent async data loading across components
- Loading states for all pages while product data is being fetched

### Fixed
- Fixed markdown parsing failing on Windows due to CRLF line endings - now normalizes to LF before parsing
- Product files (overview, roadmap, data model, design system, shell spec) now hot-reload without dev server restart

### Technical Details
- `product-loader.ts`: Now uses `fetch()` with cache busting in dev mode
- `data-model-loader.ts`: Converted to async with CRLF normalization
- `design-system-loader.ts`: Converted to async for JSON loading
- `shell-loader.ts`: Spec loading now async; component loading remains build-time for bundling
- All page components updated: `ProductPage`, `SectionsPage`, `SectionPage`, `DataModelPage`, `DesignPage`, `ExportPage`, `PhaseNav`, `PhaseWarningBanner`, `ScreenDesignPage`

## [0.1.2] - 2025-12-19

- Fixed errors related to importing google fonts out of order.
Expand Down
14 changes: 14 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

24 changes: 20 additions & 4 deletions src/components/DataModelPage.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,34 @@
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 { useProductData } from '@/lib/use-product-data'

export function DataModelPage() {
const productData = useMemo(() => loadProductData(), [])
const dataModel = productData.dataModel
const { productData, loading } = useProductData()
const dataModel = productData?.dataModel

const hasDataModel = !!dataModel
const stepStatus: StepStatus = hasDataModel ? 'completed' : 'current'

if (loading) {
return (
<AppLayout>
<div className="space-y-6">
<div className="mb-8">
<h1 className="text-2xl font-semibold text-stone-900 dark:text-stone-100 mb-2">
Data Model
</h1>
<p className="text-stone-600 dark:text-stone-400">
Loading...
</p>
</div>
</div>
</AppLayout>
)
}

return (
<AppLayout>
<div className="space-y-6">
Expand Down
26 changes: 21 additions & 5 deletions src/components/DesignPage.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
import { useMemo } from 'react'
import { Link } from 'react-router-dom'
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 { useProductData } from '@/lib/use-product-data'
import { ChevronRight, Layout } from 'lucide-react'

// Map Tailwind color names to actual color values for preview
Expand Down Expand Up @@ -64,16 +63,33 @@ function getDesignPageStepStatuses(
}

export function DesignPage() {
const productData = useMemo(() => loadProductData(), [])
const designSystem = productData.designSystem
const shell = productData.shell
const { productData, loading } = useProductData()
const designSystem = productData?.designSystem
const shell = productData?.shell

const hasDesignSystem = !!(designSystem?.colors || designSystem?.typography)
const hasShell = !!shell?.spec
const allStepsComplete = hasDesignSystem && hasShell

const stepStatuses = getDesignPageStepStatuses(hasDesignSystem, hasShell)

if (loading) {
return (
<AppLayout>
<div className="space-y-6">
<div className="mb-8">
<h1 className="text-2xl font-semibold text-stone-900 dark:text-stone-100 mb-2">
Design System
</h1>
<p className="text-stone-600 dark:text-stone-400">
Loading...
</p>
</div>
</div>
</AppLayout>
)
}

return (
<AppLayout>
<div className="space-y-6">
Expand Down
36 changes: 27 additions & 9 deletions src/components/ExportPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,28 +3,29 @@ import { Check, AlertTriangle, FileText, FolderTree, ChevronDown, Download, Pack
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@/components/ui/collapsible'
import { AppLayout } from '@/components/AppLayout'
import { loadProductData, hasExportZip, getExportZipUrl } from '@/lib/product-loader'
import { useProductData } from '@/lib/use-product-data'
import { hasExportZip, getExportZipUrl } from '@/lib/product-loader'
import { getAllSectionIds, getSectionScreenDesigns } from '@/lib/section-loader'

export function ExportPage() {
const productData = useMemo(() => loadProductData(), [])
const { productData, loading } = useProductData()

// Get section stats
const sectionStats = useMemo(() => {
const allSectionIds = getAllSectionIds()
const sectionCount = productData.roadmap?.sections.length || 0
const sectionCount = productData?.roadmap?.sections.length || 0
const sectionsWithScreenDesigns = allSectionIds.filter(id => {
const screenDesigns = getSectionScreenDesigns(id)
return screenDesigns.length > 0
}).length
return { sectionCount, sectionsWithScreenDesigns, allSectionIds }
}, [productData.roadmap])
}, [productData?.roadmap])

const hasOverview = !!productData.overview
const hasRoadmap = !!productData.roadmap
const hasDataModel = !!productData.dataModel
const hasDesignSystem = !!productData.designSystem
const hasShell = !!productData.shell
const hasOverview = !!productData?.overview
const hasRoadmap = !!productData?.roadmap
const hasDataModel = !!productData?.dataModel
const hasDesignSystem = !!productData?.designSystem
const hasShell = !!productData?.shell
const hasSections = sectionStats.sectionsWithScreenDesigns > 0

const requiredComplete = hasOverview && hasRoadmap && hasSections
Expand All @@ -33,6 +34,23 @@ export function ExportPage() {
const exportZipAvailable = hasExportZip()
const exportZipUrl = getExportZipUrl()

if (loading) {
return (
<AppLayout>
<div className="space-y-6">
<div className="mb-8">
<h1 className="text-2xl font-semibold text-stone-900 dark:text-stone-100 mb-2">
Export
</h1>
<p className="text-stone-600 dark:text-stone-400">
Loading...
</p>
</div>
</div>
</AppLayout>
)
}

return (
<AppLayout>
<div className="space-y-6">
Expand Down
51 changes: 41 additions & 10 deletions src/components/PhaseNav.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { useLocation, useNavigate } from 'react-router-dom'
import { useMemo } from 'react'
import { FileText, Boxes, Layout, LayoutList, Package } from 'lucide-react'
import { loadProductData, hasExportZip } from '@/lib/product-loader'
import { useProductData } from '@/lib/use-product-data'
import { hasExportZip } from '@/lib/product-loader'
import { getAllSectionIds, getSectionScreenDesigns } from '@/lib/section-loader'

export type Phase = 'product' | 'data-model' | 'design' | 'sections' | 'export'
Expand Down Expand Up @@ -29,16 +30,16 @@ interface PhaseInfo {
isComplete: boolean
}

function usePhaseStatuses(): PhaseInfo[] {
function usePhaseStatuses(): { phaseInfos: PhaseInfo[]; loading: boolean } {
const location = useLocation()
const productData = useMemo(() => loadProductData(), [])
const { productData, loading } = useProductData()

// Calculate completion status for each phase
const hasOverview = !!productData.overview
const hasRoadmap = !!productData.roadmap
const hasDataModel = !!productData.dataModel
const hasDesignSystem = !!productData.designSystem
const hasShell = !!productData.shell
const hasOverview = !!productData?.overview
const hasRoadmap = !!productData?.roadmap
const hasDataModel = !!productData?.dataModel
const hasDesignSystem = !!productData?.designSystem
const hasShell = !!productData?.shell

const sectionIds = useMemo(() => getAllSectionIds(), [])
const sectionsWithScreenDesigns = useMemo(() => {
Expand Down Expand Up @@ -74,7 +75,7 @@ function usePhaseStatuses(): PhaseInfo[] {
'export': exportZipExists,
}

return phases.map(phase => {
const phaseInfos = phases.map(phase => {
const isComplete = phaseComplete[phase.id]
let status: PhaseStatus
if (phase.id === currentPhaseId) {
Expand All @@ -86,11 +87,41 @@ function usePhaseStatuses(): PhaseInfo[] {
}
return { phase, status, isComplete }
})

return { phaseInfos, loading }
}

export function PhaseNav() {
const navigate = useNavigate()
const phaseInfos = usePhaseStatuses()
const { phaseInfos, loading } = usePhaseStatuses()

if (loading) {
return (
<nav className="flex items-center justify-center">
{phases.map((phase, index) => {
const Icon = phase.icon
const isFirst = index === 0

return (
<div key={phase.id} className="flex items-center">
{!isFirst && (
<div className="w-4 sm:w-8 lg:w-12 h-px bg-stone-200 dark:bg-stone-700" />
)}
<button
className="group relative flex items-center gap-1.5 sm:gap-2 px-2 sm:px-3 py-1.5 sm:py-2 rounded-lg text-stone-400 dark:text-stone-500"
disabled
>
<Icon className="w-4 h-4 shrink-0 opacity-60" strokeWidth={1.5} />
<span className="text-sm font-medium hidden sm:inline opacity-60">
{phase.label}
</span>
</button>
</div>
)
})}
</nav>
)
}

return (
<nav className="flex items-center justify-center">
Expand Down
Loading