@builderos/create-agent-os 0.0.2
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 +39 -0
- package/bin/cli.js +133 -0
- package/package.json +40 -0
- package/src/template/App.tsx +68 -0
- package/src/template/agent-os/commands/create-tasks/1-get-spec-requirements.md +19 -0
- package/src/template/agent-os/commands/create-tasks/2-create-tasks-list.md +234 -0
- package/src/template/agent-os/commands/create-tasks/create-tasks.md +254 -0
- package/src/template/agent-os/commands/design-screen/design-screen.md +32 -0
- package/src/template/agent-os/commands/design-shell/design-shell.md +34 -0
- package/src/template/agent-os/commands/design-tokens/design-tokens.md +36 -0
- package/src/template/agent-os/commands/export-product/export-product.md +44 -0
- package/src/template/agent-os/commands/implement-tasks/1-determine-tasks.md +13 -0
- package/src/template/agent-os/commands/implement-tasks/2-implement-tasks.md +63 -0
- package/src/template/agent-os/commands/implement-tasks/3-verify-implementation.md +113 -0
- package/src/template/agent-os/commands/implement-tasks/implement-tasks.md +207 -0
- package/src/template/agent-os/commands/initialize-design/initialize-design.md +54 -0
- package/src/template/agent-os/commands/orchestrate-tasks/orchestrate-tasks.md +180 -0
- package/src/template/agent-os/commands/plan-product/1-product-concept.md +53 -0
- package/src/template/agent-os/commands/plan-product/2-create-mission.md +78 -0
- package/src/template/agent-os/commands/plan-product/3-create-roadmap.md +73 -0
- package/src/template/agent-os/commands/plan-product/4-create-tech-stack.md +46 -0
- package/src/template/agent-os/commands/plan-product/plan-product.md +241 -0
- package/src/template/agent-os/commands/sample-data/sample-data.md +51 -0
- package/src/template/agent-os/commands/scaffold-implementation/scaffold-implementation.md +36 -0
- package/src/template/agent-os/commands/screenshot-design/screenshot-design.md +21 -0
- package/src/template/agent-os/commands/shape-spec/1-initialize-spec.md +95 -0
- package/src/template/agent-os/commands/shape-spec/2-shape-spec.md +300 -0
- package/src/template/agent-os/commands/shape-spec/shape-spec.md +40 -0
- package/src/template/agent-os/commands/write-spec/write-spec.md +134 -0
- package/src/template/agent-os/config.yml +13 -0
- package/src/template/agent-os/product/mission.md +29 -0
- package/src/template/agent-os/product/roadmap.md +9 -0
- package/src/template/agent-os/product/tech-stack.md +14 -0
- package/src/template/agent-os/specs/README.md +1 -0
- package/src/template/agent-os/standards/backend/api.md +10 -0
- package/src/template/agent-os/standards/backend/migrations.md +9 -0
- package/src/template/agent-os/standards/backend/models.md +10 -0
- package/src/template/agent-os/standards/backend/queries.md +9 -0
- package/src/template/agent-os/standards/frontend/accessibility.md +10 -0
- package/src/template/agent-os/standards/frontend/components.md +11 -0
- package/src/template/agent-os/standards/frontend/css.md +7 -0
- package/src/template/agent-os/standards/frontend/responsive.md +11 -0
- package/src/template/agent-os/standards/global/coding-style.md +10 -0
- package/src/template/agent-os/standards/global/commenting.md +5 -0
- package/src/template/agent-os/standards/global/conventions.md +11 -0
- package/src/template/agent-os/standards/global/error-handling.md +9 -0
- package/src/template/agent-os/standards/global/tech-stack.md +31 -0
- package/src/template/agent-os/standards/global/validation.md +11 -0
- package/src/template/agent-os/standards/testing/test-writing.md +9 -0
- package/src/template/agent-os-ui/README.md +73 -0
- package/src/template/agent-os-ui/package-lock.json +5028 -0
- package/src/template/agent-os-ui/package.json +52 -0
- package/src/template/agent-os-ui/postcss.config.js +6 -0
- package/src/template/agent-os-ui/src/components/AgentShell.tsx +31 -0
- package/src/template/agent-os-ui/src/components/AgentSidebar.tsx +65 -0
- package/src/template/agent-os-ui/src/components/GuidanceCard.tsx +75 -0
- package/src/template/agent-os-ui/src/components/MarkdownViewer.tsx +25 -0
- package/src/template/agent-os-ui/src/components/PromptButton.tsx +28 -0
- package/src/template/agent-os-ui/src/components/StatusItem.tsx +45 -0
- package/src/template/agent-os-ui/src/components/ThemeToggle.tsx +72 -0
- package/src/template/agent-os-ui/src/index.ts +11 -0
- package/src/template/agent-os-ui/src/style.css +3 -0
- package/src/template/agent-os-ui/tailwind.config.js +50 -0
- package/src/template/agent-os-ui/tsconfig.json +33 -0
- package/src/template/agent-os-ui/vite.config.ts +32 -0
- package/src/template/control-center/backend/backend.log +2 -0
- package/src/template/control-center/backend/index.js +228 -0
- package/src/template/control-center/backend/package-lock.json +951 -0
- package/src/template/control-center/backend/package.json +19 -0
- package/src/template/control-center/frontend/README.md +73 -0
- package/src/template/control-center/frontend/eslint.config.js +23 -0
- package/src/template/control-center/frontend/index.html +21 -0
- package/src/template/control-center/frontend/package-lock.json +5752 -0
- package/src/template/control-center/frontend/package.json +42 -0
- package/src/template/control-center/frontend/public/runtime-config.json +11 -0
- package/src/template/control-center/frontend/public/vite.svg +1 -0
- package/src/template/control-center/frontend/src/App.css +42 -0
- package/src/template/control-center/frontend/src/App.tsx +738 -0
- package/src/template/control-center/frontend/src/assets/react.svg +1 -0
- package/src/template/control-center/frontend/src/components/ThemeToggle.tsx +64 -0
- package/src/template/control-center/frontend/src/components/ui/ToastContext.tsx +81 -0
- package/src/template/control-center/frontend/src/index.css +194 -0
- package/src/template/control-center/frontend/src/main.tsx +14 -0
- package/src/template/control-center/frontend/src/vite-env.d.ts +1 -0
- package/src/template/control-center/frontend/tsconfig.app.json +28 -0
- package/src/template/control-center/frontend/tsconfig.json +7 -0
- package/src/template/control-center/frontend/tsconfig.node.json +26 -0
- package/src/template/control-center/frontend/vite.config.ts +22 -0
- package/src/template/design/.claude/commands/design-os/data-model.md +122 -0
- package/src/template/design/.claude/commands/design-os/design-screen.md +309 -0
- package/src/template/design/.claude/commands/design-os/design-shell.md +238 -0
- package/src/template/design/.claude/commands/design-os/design-tokens.md +166 -0
- package/src/template/design/.claude/commands/design-os/export-product.md +1105 -0
- package/src/template/design/.claude/commands/design-os/product-roadmap.md +121 -0
- package/src/template/design/.claude/commands/design-os/product-vision.md +99 -0
- package/src/template/design/.claude/commands/design-os/sample-data.md +263 -0
- package/src/template/design/.claude/commands/design-os/screenshot-design.md +112 -0
- package/src/template/design/.claude/commands/design-os/shape-section.md +138 -0
- package/src/template/design/.claude/skills/frontend-design/SKILL.md +42 -0
- package/src/template/design/.github/CODE_OF_CONDUCT.md +5 -0
- package/src/template/design/.github/CONTRIBUTING.md +51 -0
- package/src/template/design/.github/ISSUE_TEMPLATE/config.yml +22 -0
- package/src/template/design/.github/PULL_REQUEST_TEMPLATE.md +20 -0
- package/src/template/design/.github/SECURITY.yml +5 -0
- package/src/template/design/.github/SUPPORT.md +19 -0
- package/src/template/design/.github/workflows/pr-decline.yml +135 -0
- package/src/template/design/.github/workflows/stale.yml +25 -0
- package/src/template/design/CHANGELOG.md +13 -0
- package/src/template/design/LICENSE +21 -0
- package/src/template/design/README.md +54 -0
- package/src/template/design/agents.md +218 -0
- package/src/template/design/claude.md +1 -0
- package/src/template/design/components.json +22 -0
- package/src/template/design/docs/codebase-implementation.md +153 -0
- package/src/template/design/docs/design-section.md +135 -0
- package/src/template/design/docs/export.md +149 -0
- package/src/template/design/docs/getting-started.md +59 -0
- package/src/template/design/docs/index.md +56 -0
- package/src/template/design/docs/product-planning.md +113 -0
- package/src/template/design/docs/requirements.md +22 -0
- package/src/template/design/docs/usage.md +62 -0
- package/src/template/design/eslint.config.js +23 -0
- package/src/template/design/index.html +21 -0
- package/src/template/design/package-lock.json +5473 -0
- package/src/template/design/package.json +47 -0
- package/src/template/design/product-plan.zip +0 -0
- package/src/template/design/public/vite.svg +1 -0
- package/src/template/design/src/assets/react.svg +1 -0
- package/src/template/design/src/components/AppLayout.tsx +95 -0
- package/src/template/design/src/components/DataCard.tsx +139 -0
- package/src/template/design/src/components/DataModelPage.tsx +120 -0
- package/src/template/design/src/components/DesignPage.tsx +284 -0
- package/src/template/design/src/components/EmptyState.tsx +155 -0
- package/src/template/design/src/components/ExportPage.tsx +344 -0
- package/src/template/design/src/components/NextPhaseButton.tsx +33 -0
- package/src/template/design/src/components/PhaseNav.tsx +152 -0
- package/src/template/design/src/components/PhaseWarningBanner.tsx +81 -0
- package/src/template/design/src/components/ProductOverviewCard.tsx +102 -0
- package/src/template/design/src/components/ProductPage.tsx +97 -0
- package/src/template/design/src/components/ScreenDesignPage.tsx +370 -0
- package/src/template/design/src/components/ScreenDesignsCard.tsx +49 -0
- package/src/template/design/src/components/SectionPage.tsx +256 -0
- package/src/template/design/src/components/SectionsCard.tsx +47 -0
- package/src/template/design/src/components/SectionsPage.tsx +181 -0
- package/src/template/design/src/components/ShellCard.tsx +85 -0
- package/src/template/design/src/components/ShellDesignPage.tsx +242 -0
- package/src/template/design/src/components/SpecCard.tsx +121 -0
- package/src/template/design/src/components/StepIndicator.tsx +75 -0
- package/src/template/design/src/components/ThemeToggle.tsx +86 -0
- package/src/template/design/src/components/ui/ToastContext.tsx +81 -0
- package/src/template/design/src/components/ui/avatar.tsx +53 -0
- package/src/template/design/src/components/ui/badge.tsx +46 -0
- package/src/template/design/src/components/ui/button.tsx +60 -0
- package/src/template/design/src/components/ui/card.tsx +92 -0
- package/src/template/design/src/components/ui/collapsible.tsx +48 -0
- package/src/template/design/src/components/ui/dialog.tsx +143 -0
- package/src/template/design/src/components/ui/dropdown-menu.tsx +255 -0
- package/src/template/design/src/components/ui/input.tsx +21 -0
- package/src/template/design/src/components/ui/label.tsx +22 -0
- package/src/template/design/src/components/ui/progress.tsx +24 -0
- package/src/template/design/src/components/ui/scroll-area.tsx +18 -0
- package/src/template/design/src/components/ui/select.tsx +67 -0
- package/src/template/design/src/components/ui/separator.tsx +28 -0
- package/src/template/design/src/components/ui/sheet.tsx +137 -0
- package/src/template/design/src/components/ui/skeleton.tsx +13 -0
- package/src/template/design/src/components/ui/switch.tsx +46 -0
- package/src/template/design/src/components/ui/table.tsx +116 -0
- package/src/template/design/src/components/ui/tabs.tsx +64 -0
- package/src/template/design/src/index.css +284 -0
- package/src/template/design/src/lib/data-model-loader.ts +91 -0
- package/src/template/design/src/lib/design-system-loader.ts +101 -0
- package/src/template/design/src/lib/product-loader.ts +221 -0
- package/src/template/design/src/lib/router.tsx +52 -0
- package/src/template/design/src/lib/section-loader.ts +272 -0
- package/src/template/design/src/lib/shell-loader.ts +175 -0
- package/src/template/design/src/lib/utils.ts +6 -0
- package/src/template/design/src/main.tsx +15 -0
- package/src/template/design/src/sections/.gitkeep +0 -0
- package/src/template/design/src/sections/ai-orchestration-engine-oai/OrchestrationEngine.tsx +348 -0
- package/src/template/design/src/sections/core-platform-shell/AppShell.tsx +403 -0
- package/src/template/design/src/sections/gemini-live-integration/GeminiIntegration.tsx +332 -0
- package/src/template/design/src/sections/interactive-2d-canvas/WhiteboardCanvas.tsx +334 -0
- package/src/template/design/src/sections/participation-equity-tracker/EquityTracker.tsx +383 -0
- package/src/template/design/src/sections/persistent-memory-system/PersistentMemory.tsx +308 -0
- package/src/template/design/src/sections/real-time-communication-layer/VideoSession.tsx +342 -0
- package/src/template/design/src/sections/visual-intelligence-agents/VisualAgents.tsx +311 -0
- package/src/template/design/src/types/product.ts +97 -0
- package/src/template/design/src/types/section.ts +33 -0
- package/src/template/design/tsconfig.app.json +34 -0
- package/src/template/design/tsconfig.json +13 -0
- package/src/template/design/tsconfig.node.json +26 -0
- package/src/template/design/vite.config.ts +18 -0
- package/src/template/package.json +27 -0
- package/src/template/vite.config.ts +16 -0
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { useState } from 'react'
|
|
2
|
+
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
|
|
3
|
+
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@/components/ui/collapsible'
|
|
4
|
+
import { ArrowRight, ChevronDown } from 'lucide-react'
|
|
5
|
+
import type { ProductOverview } from '@/types/product'
|
|
6
|
+
|
|
7
|
+
interface ProductOverviewCardProps {
|
|
8
|
+
overview: ProductOverview
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function ProductOverviewCard({ overview }: ProductOverviewCardProps) {
|
|
12
|
+
const [problemsOpen, setProblemsOpen] = useState(false)
|
|
13
|
+
const [featuresOpen, setFeaturesOpen] = useState(false)
|
|
14
|
+
|
|
15
|
+
return (
|
|
16
|
+
<Card className="border-stone-200 dark:border-stone-700 shadow-sm">
|
|
17
|
+
<CardHeader className="pb-4">
|
|
18
|
+
<CardTitle className="text-lg font-semibold text-stone-900 dark:text-stone-100">
|
|
19
|
+
Product overview: {overview.name}
|
|
20
|
+
</CardTitle>
|
|
21
|
+
</CardHeader>
|
|
22
|
+
<CardContent className="space-y-4">
|
|
23
|
+
{/* Description */}
|
|
24
|
+
{overview.description && (
|
|
25
|
+
<p className="text-stone-600 dark:text-stone-400 leading-relaxed">
|
|
26
|
+
{overview.description}
|
|
27
|
+
</p>
|
|
28
|
+
)}
|
|
29
|
+
|
|
30
|
+
{/* Problems & Solutions - Expandable */}
|
|
31
|
+
{overview.problems.length > 0 && (
|
|
32
|
+
<Collapsible open={problemsOpen} onOpenChange={setProblemsOpen}>
|
|
33
|
+
<CollapsibleTrigger className="flex items-center justify-between w-full py-2 text-left group">
|
|
34
|
+
<span className="text-sm font-medium text-stone-500 dark:text-stone-400 uppercase tracking-wide">
|
|
35
|
+
Problems & Solutions
|
|
36
|
+
<span className="ml-2 text-stone-400 dark:text-stone-500 normal-case tracking-normal">
|
|
37
|
+
({overview.problems.length})
|
|
38
|
+
</span>
|
|
39
|
+
</span>
|
|
40
|
+
<ChevronDown
|
|
41
|
+
className={`w-4 h-4 text-stone-400 dark:text-stone-500 transition-transform ${
|
|
42
|
+
problemsOpen ? 'rotate-180' : ''
|
|
43
|
+
}`}
|
|
44
|
+
strokeWidth={1.5}
|
|
45
|
+
/>
|
|
46
|
+
</CollapsibleTrigger>
|
|
47
|
+
<CollapsibleContent>
|
|
48
|
+
<ul className="space-y-3 pt-2">
|
|
49
|
+
{overview.problems.map((problem, index) => (
|
|
50
|
+
<li key={index} className="flex items-start gap-3">
|
|
51
|
+
<ArrowRight className="w-4 h-4 text-stone-900 dark:text-stone-100 mt-1 shrink-0" strokeWidth={2} />
|
|
52
|
+
<div>
|
|
53
|
+
<span className="font-medium text-stone-800 dark:text-stone-200">
|
|
54
|
+
{problem.title}
|
|
55
|
+
</span>
|
|
56
|
+
<span className="text-stone-500 dark:text-stone-400 mx-2">—</span>
|
|
57
|
+
<span className="text-stone-600 dark:text-stone-400">
|
|
58
|
+
{problem.solution}
|
|
59
|
+
</span>
|
|
60
|
+
</div>
|
|
61
|
+
</li>
|
|
62
|
+
))}
|
|
63
|
+
</ul>
|
|
64
|
+
</CollapsibleContent>
|
|
65
|
+
</Collapsible>
|
|
66
|
+
)}
|
|
67
|
+
|
|
68
|
+
{/* Key Features - Expandable */}
|
|
69
|
+
{overview.features.length > 0 && (
|
|
70
|
+
<Collapsible open={featuresOpen} onOpenChange={setFeaturesOpen}>
|
|
71
|
+
<CollapsibleTrigger className="flex items-center justify-between w-full py-2 text-left group">
|
|
72
|
+
<span className="text-sm font-medium text-stone-500 dark:text-stone-400 uppercase tracking-wide">
|
|
73
|
+
Key Features
|
|
74
|
+
<span className="ml-2 text-stone-400 dark:text-stone-500 normal-case tracking-normal">
|
|
75
|
+
({overview.features.length})
|
|
76
|
+
</span>
|
|
77
|
+
</span>
|
|
78
|
+
<ChevronDown
|
|
79
|
+
className={`w-4 h-4 text-stone-400 dark:text-stone-500 transition-transform ${
|
|
80
|
+
featuresOpen ? 'rotate-180' : ''
|
|
81
|
+
}`}
|
|
82
|
+
strokeWidth={1.5}
|
|
83
|
+
/>
|
|
84
|
+
</CollapsibleTrigger>
|
|
85
|
+
<CollapsibleContent>
|
|
86
|
+
<ul className="space-y-2 pt-2 ml-1">
|
|
87
|
+
{overview.features.map((feature, index) => (
|
|
88
|
+
<li key={index} className="flex items-start gap-4">
|
|
89
|
+
<span className="w-1.5 h-1.5 rounded-full bg-stone-900 dark:bg-stone-100 mt-2 shrink-0" />
|
|
90
|
+
<span className="text-stone-700 dark:text-stone-300">
|
|
91
|
+
{feature}
|
|
92
|
+
</span>
|
|
93
|
+
</li>
|
|
94
|
+
))}
|
|
95
|
+
</ul>
|
|
96
|
+
</CollapsibleContent>
|
|
97
|
+
</Collapsible>
|
|
98
|
+
)}
|
|
99
|
+
</CardContent>
|
|
100
|
+
</Card>
|
|
101
|
+
)
|
|
102
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { useMemo } from 'react'
|
|
2
|
+
import { useNavigate } from 'react-router-dom'
|
|
3
|
+
import { loadProductData } from '@/lib/product-loader'
|
|
4
|
+
import { AppLayout } from '@/components/AppLayout'
|
|
5
|
+
import { EmptyState } from '@/components/EmptyState'
|
|
6
|
+
import { ProductOverviewCard } from '@/components/ProductOverviewCard'
|
|
7
|
+
import { SectionsCard } from '@/components/SectionsCard'
|
|
8
|
+
import { StepIndicator, type StepStatus } from '@/components/StepIndicator'
|
|
9
|
+
import { NextPhaseButton } from '@/components/NextPhaseButton'
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Determine the status of each step on the Product page
|
|
13
|
+
* Steps: 1. Product Vision, 2. Roadmap
|
|
14
|
+
*/
|
|
15
|
+
function getProductPageStepStatuses(
|
|
16
|
+
hasOverview: boolean,
|
|
17
|
+
hasRoadmap: boolean
|
|
18
|
+
): StepStatus[] {
|
|
19
|
+
const statuses: StepStatus[] = []
|
|
20
|
+
|
|
21
|
+
// Step 1: Product Vision
|
|
22
|
+
if (hasOverview) {
|
|
23
|
+
statuses.push('completed')
|
|
24
|
+
} else {
|
|
25
|
+
statuses.push('current')
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Step 2: Roadmap
|
|
29
|
+
if (hasRoadmap) {
|
|
30
|
+
statuses.push('completed')
|
|
31
|
+
} else if (hasOverview) {
|
|
32
|
+
statuses.push('current')
|
|
33
|
+
} else {
|
|
34
|
+
statuses.push('upcoming')
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return statuses
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function ProductPage() {
|
|
41
|
+
const navigate = useNavigate()
|
|
42
|
+
const productData = useMemo(() => loadProductData(), [])
|
|
43
|
+
|
|
44
|
+
const hasOverview = !!productData.overview
|
|
45
|
+
const hasRoadmap = !!productData.roadmap
|
|
46
|
+
const allStepsComplete = hasOverview && hasRoadmap
|
|
47
|
+
|
|
48
|
+
const stepStatuses = getProductPageStepStatuses(hasOverview, hasRoadmap)
|
|
49
|
+
|
|
50
|
+
return (
|
|
51
|
+
<AppLayout>
|
|
52
|
+
<div className="space-y-6">
|
|
53
|
+
{/* Page intro */}
|
|
54
|
+
<div className="mb-8">
|
|
55
|
+
<h1 className="text-2xl font-semibold text-stone-900 dark:text-stone-100 mb-2">
|
|
56
|
+
Product Definition
|
|
57
|
+
</h1>
|
|
58
|
+
<p className="text-stone-600 dark:text-stone-400">
|
|
59
|
+
Define your product vision and break it into development sections.
|
|
60
|
+
</p>
|
|
61
|
+
</div>
|
|
62
|
+
|
|
63
|
+
{/* Step 1: Product Vision */}
|
|
64
|
+
<div id="step-overview">
|
|
65
|
+
<StepIndicator step={1} status={stepStatuses[0]}>
|
|
66
|
+
{productData.overview ? (
|
|
67
|
+
<ProductOverviewCard overview={productData.overview} />
|
|
68
|
+
) : (
|
|
69
|
+
<EmptyState type="overview" />
|
|
70
|
+
)}
|
|
71
|
+
</StepIndicator>
|
|
72
|
+
</div>
|
|
73
|
+
|
|
74
|
+
{/* Step 2: Roadmap / Sections Definition */}
|
|
75
|
+
<div id="step-roadmap">
|
|
76
|
+
<StepIndicator step={2} status={stepStatuses[1]} isLast={!allStepsComplete}>
|
|
77
|
+
{productData.roadmap ? (
|
|
78
|
+
<SectionsCard
|
|
79
|
+
roadmap={productData.roadmap}
|
|
80
|
+
onSectionClick={(sectionId) => navigate(`/sections/${sectionId}`)}
|
|
81
|
+
/>
|
|
82
|
+
) : (
|
|
83
|
+
<EmptyState type="roadmap" />
|
|
84
|
+
)}
|
|
85
|
+
</StepIndicator>
|
|
86
|
+
</div>
|
|
87
|
+
|
|
88
|
+
{/* Next Phase Button - shown when all steps complete */}
|
|
89
|
+
{allStepsComplete && (
|
|
90
|
+
<StepIndicator step={3} status="current" isLast>
|
|
91
|
+
<NextPhaseButton nextPhase="data-model" />
|
|
92
|
+
</StepIndicator>
|
|
93
|
+
)}
|
|
94
|
+
</div>
|
|
95
|
+
</AppLayout>
|
|
96
|
+
)
|
|
97
|
+
}
|
|
@@ -0,0 +1,370 @@
|
|
|
1
|
+
import { Suspense, useMemo, useState, useRef, useCallback, useEffect } from 'react'
|
|
2
|
+
import { useParams, useNavigate } from 'react-router-dom'
|
|
3
|
+
import { ArrowLeft, Maximize2, GripVertical, Layout, Smartphone, Tablet, Monitor } from 'lucide-react'
|
|
4
|
+
import { Button } from '@/components/ui/button'
|
|
5
|
+
import { ThemeToggle } from '@/components/ThemeToggle'
|
|
6
|
+
import { loadScreenDesignComponent, sectionUsesShell } from '@/lib/section-loader'
|
|
7
|
+
import { loadAppShell, hasShellComponents, loadShellInfo } from '@/lib/shell-loader'
|
|
8
|
+
import { loadProductData } from '@/lib/product-loader'
|
|
9
|
+
import React from 'react'
|
|
10
|
+
|
|
11
|
+
const MIN_WIDTH = 320
|
|
12
|
+
const DEFAULT_WIDTH_PERCENT = 100
|
|
13
|
+
|
|
14
|
+
export function ScreenDesignPage() {
|
|
15
|
+
const { sectionId, screenDesignName } = useParams<{ sectionId: string; screenDesignName: string }>()
|
|
16
|
+
const navigate = useNavigate()
|
|
17
|
+
const [widthPercent, setWidthPercent] = useState(DEFAULT_WIDTH_PERCENT)
|
|
18
|
+
const containerRef = useRef<HTMLDivElement>(null)
|
|
19
|
+
const isDragging = useRef(false)
|
|
20
|
+
|
|
21
|
+
// Load product data to get section title
|
|
22
|
+
const productData = useMemo(() => loadProductData(), [])
|
|
23
|
+
const section = productData.roadmap?.sections.find((s) => s.id === sectionId)
|
|
24
|
+
|
|
25
|
+
// Handle resize drag
|
|
26
|
+
const handleMouseDown = useCallback(() => {
|
|
27
|
+
isDragging.current = true
|
|
28
|
+
|
|
29
|
+
const handleMouseMove = (e: MouseEvent) => {
|
|
30
|
+
if (!isDragging.current || !containerRef.current) return
|
|
31
|
+
|
|
32
|
+
const containerRect = containerRef.current.getBoundingClientRect()
|
|
33
|
+
const containerWidth = containerRect.width
|
|
34
|
+
const containerCenter = containerRect.left + containerWidth / 2
|
|
35
|
+
|
|
36
|
+
// Calculate distance from center
|
|
37
|
+
const distanceFromCenter = Math.abs(e.clientX - containerCenter)
|
|
38
|
+
const maxDistance = containerWidth / 2
|
|
39
|
+
|
|
40
|
+
// Convert to percentage (distance from center * 2 = total width)
|
|
41
|
+
let newWidthPercent = (distanceFromCenter / maxDistance) * 100
|
|
42
|
+
|
|
43
|
+
// Clamp between min width and 100%
|
|
44
|
+
const minPercent = (MIN_WIDTH / containerWidth) * 100
|
|
45
|
+
newWidthPercent = Math.max(minPercent, Math.min(100, newWidthPercent))
|
|
46
|
+
|
|
47
|
+
setWidthPercent(newWidthPercent)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const handleMouseUp = () => {
|
|
51
|
+
isDragging.current = false
|
|
52
|
+
document.removeEventListener('mousemove', handleMouseMove)
|
|
53
|
+
document.removeEventListener('mouseup', handleMouseUp)
|
|
54
|
+
document.body.style.cursor = ''
|
|
55
|
+
document.body.style.userSelect = ''
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
document.addEventListener('mousemove', handleMouseMove)
|
|
59
|
+
document.addEventListener('mouseup', handleMouseUp)
|
|
60
|
+
document.body.style.cursor = 'ew-resize'
|
|
61
|
+
document.body.style.userSelect = 'none'
|
|
62
|
+
}, [])
|
|
63
|
+
|
|
64
|
+
const previewWidth = `${widthPercent}%`
|
|
65
|
+
|
|
66
|
+
return (
|
|
67
|
+
<div className="h-screen bg-stone-100 dark:bg-stone-900 animate-fade-in flex flex-col overflow-hidden">
|
|
68
|
+
{/* Header */}
|
|
69
|
+
<header className="border-b border-stone-200 dark:border-stone-800 bg-white dark:bg-stone-950 shrink-0 z-50">
|
|
70
|
+
<div className="px-4 py-2 flex items-center gap-4">
|
|
71
|
+
<Button
|
|
72
|
+
variant="ghost"
|
|
73
|
+
size="sm"
|
|
74
|
+
onClick={() => navigate(`/sections/${sectionId}`)}
|
|
75
|
+
className="text-stone-600 dark:text-stone-400 hover:text-stone-900 dark:hover:text-stone-100 -ml-2"
|
|
76
|
+
>
|
|
77
|
+
<ArrowLeft className="w-4 h-4 mr-2" strokeWidth={1.5} />
|
|
78
|
+
Back
|
|
79
|
+
</Button>
|
|
80
|
+
<div className="h-4 w-px bg-stone-200 dark:bg-stone-700" />
|
|
81
|
+
<div className="flex items-center gap-2 min-w-0">
|
|
82
|
+
<Layout className="w-4 h-4 text-stone-400 shrink-0" strokeWidth={1.5} />
|
|
83
|
+
{section && (
|
|
84
|
+
<span className="text-sm text-stone-500 dark:text-stone-400 truncate">
|
|
85
|
+
{section.title}
|
|
86
|
+
</span>
|
|
87
|
+
)}
|
|
88
|
+
<span className="text-stone-300 dark:text-stone-600">/</span>
|
|
89
|
+
<span className="text-sm font-medium text-stone-700 dark:text-stone-300 truncate">
|
|
90
|
+
{screenDesignName}
|
|
91
|
+
</span>
|
|
92
|
+
</div>
|
|
93
|
+
|
|
94
|
+
{/* Width indicator and device presets */}
|
|
95
|
+
<div className="ml-auto flex items-center gap-4">
|
|
96
|
+
{/* Device size presets */}
|
|
97
|
+
<div className="flex items-center gap-1 border-r border-stone-200 dark:border-stone-700 pr-4">
|
|
98
|
+
<button
|
|
99
|
+
onClick={() => setWidthPercent(30)}
|
|
100
|
+
className={`p-1.5 rounded transition-colors ${
|
|
101
|
+
widthPercent <= 40
|
|
102
|
+
? 'bg-stone-200 dark:bg-stone-700 text-stone-900 dark:text-stone-100'
|
|
103
|
+
: 'text-stone-400 dark:text-stone-500 hover:text-stone-600 dark:hover:text-stone-300 hover:bg-stone-100 dark:hover:bg-stone-800'
|
|
104
|
+
}`}
|
|
105
|
+
title="Mobile (30%)"
|
|
106
|
+
>
|
|
107
|
+
<Smartphone className="w-4 h-4" strokeWidth={1.5} />
|
|
108
|
+
</button>
|
|
109
|
+
<button
|
|
110
|
+
onClick={() => setWidthPercent(60)}
|
|
111
|
+
className={`p-1.5 rounded transition-colors ${
|
|
112
|
+
widthPercent > 40 && widthPercent <= 60
|
|
113
|
+
? 'bg-stone-200 dark:bg-stone-700 text-stone-900 dark:text-stone-100'
|
|
114
|
+
: 'text-stone-400 dark:text-stone-500 hover:text-stone-600 dark:hover:text-stone-300 hover:bg-stone-100 dark:hover:bg-stone-800'
|
|
115
|
+
}`}
|
|
116
|
+
title="Tablet (60%)"
|
|
117
|
+
>
|
|
118
|
+
<Tablet className="w-4 h-4" strokeWidth={1.5} />
|
|
119
|
+
</button>
|
|
120
|
+
<button
|
|
121
|
+
onClick={() => setWidthPercent(100)}
|
|
122
|
+
className={`p-1.5 rounded transition-colors ${
|
|
123
|
+
widthPercent > 60
|
|
124
|
+
? 'bg-stone-200 dark:bg-stone-700 text-stone-900 dark:text-stone-100'
|
|
125
|
+
: 'text-stone-400 dark:text-stone-500 hover:text-stone-600 dark:hover:text-stone-300 hover:bg-stone-100 dark:hover:bg-stone-800'
|
|
126
|
+
}`}
|
|
127
|
+
title="Desktop (100%)"
|
|
128
|
+
>
|
|
129
|
+
<Monitor className="w-4 h-4" strokeWidth={1.5} />
|
|
130
|
+
</button>
|
|
131
|
+
</div>
|
|
132
|
+
<span className="text-xs text-stone-500 dark:text-stone-400 font-mono w-10 text-right">
|
|
133
|
+
{Math.round(widthPercent)}%
|
|
134
|
+
</span>
|
|
135
|
+
<ThemeToggle />
|
|
136
|
+
<a
|
|
137
|
+
href={`/sections/${sectionId}/screen-designs/${screenDesignName}/fullscreen`}
|
|
138
|
+
target="_blank"
|
|
139
|
+
rel="noopener noreferrer"
|
|
140
|
+
className="flex items-center gap-1.5 text-xs text-stone-500 dark:text-stone-400 hover:text-stone-700 dark:hover:text-stone-200 transition-colors"
|
|
141
|
+
>
|
|
142
|
+
<Maximize2 className="w-3.5 h-3.5" strokeWidth={1.5} />
|
|
143
|
+
Fullscreen
|
|
144
|
+
</a>
|
|
145
|
+
</div>
|
|
146
|
+
</div>
|
|
147
|
+
</header>
|
|
148
|
+
|
|
149
|
+
{/* Preview area with resizable container */}
|
|
150
|
+
<div
|
|
151
|
+
ref={containerRef}
|
|
152
|
+
className="flex-1 overflow-hidden flex items-stretch justify-center p-6"
|
|
153
|
+
>
|
|
154
|
+
{/* Left resize handle */}
|
|
155
|
+
<div
|
|
156
|
+
className="w-4 flex items-center justify-center cursor-ew-resize group shrink-0"
|
|
157
|
+
onMouseDown={handleMouseDown}
|
|
158
|
+
>
|
|
159
|
+
<div className="w-1 h-16 rounded-full bg-stone-300 dark:bg-stone-600 group-hover:bg-stone-400 dark:group-hover:bg-stone-500 transition-colors flex items-center justify-center">
|
|
160
|
+
<GripVertical className="w-3 h-3 text-stone-500 dark:text-stone-400 opacity-0 group-hover:opacity-100 transition-opacity" strokeWidth={2} />
|
|
161
|
+
</div>
|
|
162
|
+
</div>
|
|
163
|
+
|
|
164
|
+
{/* Preview container using iframe for true isolation */}
|
|
165
|
+
<div
|
|
166
|
+
className="bg-white dark:bg-stone-950 rounded-lg shadow-xl border border-stone-200 dark:border-stone-700 overflow-hidden"
|
|
167
|
+
style={{ width: previewWidth, minWidth: MIN_WIDTH, maxWidth: '100%' }}
|
|
168
|
+
>
|
|
169
|
+
<iframe
|
|
170
|
+
src={`/sections/${sectionId}/screen-designs/${screenDesignName}/fullscreen`}
|
|
171
|
+
className="w-full h-full border-0"
|
|
172
|
+
title="Screen Design Preview"
|
|
173
|
+
/>
|
|
174
|
+
</div>
|
|
175
|
+
|
|
176
|
+
{/* Right resize handle */}
|
|
177
|
+
<div
|
|
178
|
+
className="w-4 flex items-center justify-center cursor-ew-resize group shrink-0"
|
|
179
|
+
onMouseDown={handleMouseDown}
|
|
180
|
+
>
|
|
181
|
+
<div className="w-1 h-16 rounded-full bg-stone-300 dark:bg-stone-600 group-hover:bg-stone-400 dark:group-hover:bg-stone-500 transition-colors flex items-center justify-center">
|
|
182
|
+
<GripVertical className="w-3 h-3 text-stone-500 dark:text-stone-400 opacity-0 group-hover:opacity-100 transition-opacity" strokeWidth={2} />
|
|
183
|
+
</div>
|
|
184
|
+
</div>
|
|
185
|
+
</div>
|
|
186
|
+
</div>
|
|
187
|
+
)
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Fullscreen version of a screen design (for screenshots)
|
|
192
|
+
* Syncs theme with parent window via localStorage
|
|
193
|
+
* Wraps screen design in AppShell if shell components exist
|
|
194
|
+
*/
|
|
195
|
+
export function ScreenDesignFullscreen() {
|
|
196
|
+
const { sectionId, screenDesignName } = useParams<{ sectionId: string; screenDesignName: string }>()
|
|
197
|
+
|
|
198
|
+
// Load screen design component
|
|
199
|
+
const ScreenDesignComponent = useMemo(() => {
|
|
200
|
+
if (!sectionId || !screenDesignName) return null
|
|
201
|
+
const loader = loadScreenDesignComponent(sectionId, screenDesignName)
|
|
202
|
+
if (!loader) return null
|
|
203
|
+
// Wrap the loader to handle potential export issues
|
|
204
|
+
return React.lazy(async () => {
|
|
205
|
+
try {
|
|
206
|
+
const module = await loader()
|
|
207
|
+
if (module && typeof module.default === 'function') {
|
|
208
|
+
return module
|
|
209
|
+
}
|
|
210
|
+
console.error('Screen design does not have a valid default export:', screenDesignName)
|
|
211
|
+
return { default: () => <div>Invalid screen design: {screenDesignName}</div> }
|
|
212
|
+
} catch (e) {
|
|
213
|
+
console.error('Failed to load screen design:', screenDesignName, e)
|
|
214
|
+
return { default: () => <div>Failed to load: {screenDesignName}</div> }
|
|
215
|
+
}
|
|
216
|
+
})
|
|
217
|
+
}, [sectionId, screenDesignName])
|
|
218
|
+
|
|
219
|
+
// Load AppShell component if it exists AND this section uses the shell
|
|
220
|
+
const AppShellComponent = useMemo(() => {
|
|
221
|
+
// Check if this section should use the shell (based on spec.md config)
|
|
222
|
+
if (sectionId && !sectionUsesShell(sectionId)) {
|
|
223
|
+
console.log('[ScreenDesignFullscreen] Section configured to not use shell')
|
|
224
|
+
return null
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// Check if shell components exist
|
|
228
|
+
const shellExists = hasShellComponents()
|
|
229
|
+
console.log('[ScreenDesignFullscreen] Shell exists:', shellExists)
|
|
230
|
+
if (!shellExists) return null
|
|
231
|
+
|
|
232
|
+
const loader = loadAppShell()
|
|
233
|
+
console.log('[ScreenDesignFullscreen] AppShell loader:', loader)
|
|
234
|
+
if (!loader) {
|
|
235
|
+
console.warn('[ScreenDesignFullscreen] hasShellComponents() returned true but loadAppShell() returned null')
|
|
236
|
+
return null
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// Wrap the loader to provide default props to the shell
|
|
240
|
+
return React.lazy(async () => {
|
|
241
|
+
try {
|
|
242
|
+
const module = await loader() as Record<string, unknown>
|
|
243
|
+
const ShellComponent = (module?.default || module?.AppShell) as React.ComponentType<Record<string, unknown>>
|
|
244
|
+
|
|
245
|
+
if (typeof ShellComponent !== 'function') {
|
|
246
|
+
console.warn('[ScreenDesignFullscreen] AppShell does not have a valid export')
|
|
247
|
+
return { default: ({ children }: { children?: React.ReactNode }) => <>{children}</> }
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// Create a wrapper that provides default props to the shell
|
|
251
|
+
const ShellWrapper = ({ children }: { children?: React.ReactNode }) => {
|
|
252
|
+
// Try to get navigation items from shell spec
|
|
253
|
+
const shellInfo = loadShellInfo()
|
|
254
|
+
const specNavItems = shellInfo?.spec?.navigationItems || []
|
|
255
|
+
|
|
256
|
+
// Parse navigation items from spec (format: "**Label** → Description")
|
|
257
|
+
const navigationItems = specNavItems.length > 0
|
|
258
|
+
? specNavItems.map((item, index) => {
|
|
259
|
+
// Extract label from **Label** format
|
|
260
|
+
const labelMatch = item.match(/\*\*([^*]+)\*\*/)
|
|
261
|
+
const label = labelMatch ? labelMatch[1] : item.split('→')[0]?.trim() || `Item ${index + 1}`
|
|
262
|
+
return {
|
|
263
|
+
label,
|
|
264
|
+
href: `/${label.toLowerCase().replace(/\s+/g, '-')}`,
|
|
265
|
+
isActive: index === 0,
|
|
266
|
+
}
|
|
267
|
+
})
|
|
268
|
+
: [
|
|
269
|
+
{ label: 'Dashboard', href: '/', isActive: true },
|
|
270
|
+
{ label: 'Items', href: '/items' },
|
|
271
|
+
{ label: 'Settings', href: '/settings' },
|
|
272
|
+
]
|
|
273
|
+
|
|
274
|
+
const defaultUser = {
|
|
275
|
+
name: 'Demo User',
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// Pass props dynamically - the shell component decides what it needs
|
|
279
|
+
return (
|
|
280
|
+
<ShellComponent
|
|
281
|
+
navigationItems={navigationItems}
|
|
282
|
+
user={defaultUser}
|
|
283
|
+
onNavigate={() => {}}
|
|
284
|
+
onLogout={() => {}}
|
|
285
|
+
>
|
|
286
|
+
{children}
|
|
287
|
+
</ShellComponent>
|
|
288
|
+
)
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
return { default: ShellWrapper }
|
|
292
|
+
} catch (e) {
|
|
293
|
+
console.error('[ScreenDesignFullscreen] Failed to load AppShell:', e)
|
|
294
|
+
return { default: ({ children }: { children?: React.ReactNode }) => <>{children}</> }
|
|
295
|
+
}
|
|
296
|
+
})
|
|
297
|
+
}, [sectionId]) // Depends on sectionId to check section-specific shell config
|
|
298
|
+
|
|
299
|
+
// Sync theme with parent window
|
|
300
|
+
useEffect(() => {
|
|
301
|
+
const applyTheme = () => {
|
|
302
|
+
const theme = localStorage.getItem('theme') || 'system'
|
|
303
|
+
const root = document.documentElement
|
|
304
|
+
|
|
305
|
+
if (theme === 'system') {
|
|
306
|
+
const systemDark = window.matchMedia('(prefers-color-scheme: dark)').matches
|
|
307
|
+
root.classList.toggle('dark', systemDark)
|
|
308
|
+
} else {
|
|
309
|
+
root.classList.toggle('dark', theme === 'dark')
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// Apply on mount
|
|
314
|
+
applyTheme()
|
|
315
|
+
|
|
316
|
+
// Listen for storage changes (from parent window)
|
|
317
|
+
const handleStorageChange = (e: StorageEvent) => {
|
|
318
|
+
if (e.key === 'theme') {
|
|
319
|
+
applyTheme()
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
window.addEventListener('storage', handleStorageChange)
|
|
323
|
+
|
|
324
|
+
// Also poll for changes since storage event doesn't fire in same window
|
|
325
|
+
const interval = setInterval(applyTheme, 100)
|
|
326
|
+
|
|
327
|
+
return () => {
|
|
328
|
+
window.removeEventListener('storage', handleStorageChange)
|
|
329
|
+
clearInterval(interval)
|
|
330
|
+
}
|
|
331
|
+
}, [])
|
|
332
|
+
|
|
333
|
+
if (!ScreenDesignComponent) {
|
|
334
|
+
return (
|
|
335
|
+
<div className="h-screen flex items-center justify-center bg-background">
|
|
336
|
+
<p className="text-stone-600 dark:text-stone-400">Screen design not found.</p>
|
|
337
|
+
</div>
|
|
338
|
+
)
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
// If shell exists, wrap screen design in AppShell
|
|
342
|
+
if (AppShellComponent) {
|
|
343
|
+
return (
|
|
344
|
+
<Suspense
|
|
345
|
+
fallback={
|
|
346
|
+
<div className="h-screen flex items-center justify-center bg-background">
|
|
347
|
+
<div className="text-stone-500 dark:text-stone-400">Loading...</div>
|
|
348
|
+
</div>
|
|
349
|
+
}
|
|
350
|
+
>
|
|
351
|
+
<AppShellComponent>
|
|
352
|
+
<ScreenDesignComponent />
|
|
353
|
+
</AppShellComponent>
|
|
354
|
+
</Suspense>
|
|
355
|
+
)
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
// No shell, render screen design directly
|
|
359
|
+
return (
|
|
360
|
+
<Suspense
|
|
361
|
+
fallback={
|
|
362
|
+
<div className="h-screen flex items-center justify-center bg-background">
|
|
363
|
+
<div className="text-stone-500 dark:text-stone-400">Loading...</div>
|
|
364
|
+
</div>
|
|
365
|
+
}
|
|
366
|
+
>
|
|
367
|
+
<ScreenDesignComponent />
|
|
368
|
+
</Suspense>
|
|
369
|
+
)
|
|
370
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { Link } from 'react-router-dom'
|
|
2
|
+
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
|
|
3
|
+
import { ChevronRight, Layout } from 'lucide-react'
|
|
4
|
+
import { EmptyState } from '@/components/EmptyState'
|
|
5
|
+
import type { ScreenDesignInfo } from '@/types/section'
|
|
6
|
+
|
|
7
|
+
interface ScreenDesignsCardProps {
|
|
8
|
+
screenDesigns: ScreenDesignInfo[]
|
|
9
|
+
sectionId: string
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function ScreenDesignsCard({ screenDesigns, sectionId }: ScreenDesignsCardProps) {
|
|
13
|
+
// Empty state
|
|
14
|
+
if (screenDesigns.length === 0) {
|
|
15
|
+
return <EmptyState type="screen-designs" />
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
return (
|
|
19
|
+
<Card className="border-stone-200 dark:border-stone-700 shadow-sm">
|
|
20
|
+
<CardHeader className="pb-4">
|
|
21
|
+
<CardTitle className="text-lg font-semibold text-stone-900 dark:text-stone-100">
|
|
22
|
+
Screen Designs
|
|
23
|
+
</CardTitle>
|
|
24
|
+
</CardHeader>
|
|
25
|
+
<CardContent className="p-0">
|
|
26
|
+
<ul className="divide-y divide-stone-200 dark:divide-stone-700">
|
|
27
|
+
{screenDesigns.map((screenDesign) => (
|
|
28
|
+
<li key={screenDesign.name}>
|
|
29
|
+
<Link
|
|
30
|
+
to={`/sections/${sectionId}/screen-designs/${screenDesign.name}`}
|
|
31
|
+
className="flex items-center justify-between gap-4 px-6 py-4 hover:bg-stone-50 dark:hover:bg-stone-800/50 transition-colors"
|
|
32
|
+
>
|
|
33
|
+
<div className="flex items-center gap-3 min-w-0">
|
|
34
|
+
<div className="w-8 h-8 rounded-md bg-stone-200 dark:bg-stone-700 flex items-center justify-center shrink-0">
|
|
35
|
+
<Layout className="w-4 h-4 text-stone-600 dark:text-stone-300" strokeWidth={1.5} />
|
|
36
|
+
</div>
|
|
37
|
+
<span className="font-medium text-stone-900 dark:text-stone-100 truncate">
|
|
38
|
+
{screenDesign.name}
|
|
39
|
+
</span>
|
|
40
|
+
</div>
|
|
41
|
+
<ChevronRight className="w-4 h-4 text-stone-400 dark:text-stone-500 shrink-0" strokeWidth={1.5} />
|
|
42
|
+
</Link>
|
|
43
|
+
</li>
|
|
44
|
+
))}
|
|
45
|
+
</ul>
|
|
46
|
+
</CardContent>
|
|
47
|
+
</Card>
|
|
48
|
+
)
|
|
49
|
+
}
|