@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,242 @@
|
|
|
1
|
+
import { Suspense, useMemo, useState, useRef, useCallback, useEffect } from 'react'
|
|
2
|
+
import { useNavigate } from 'react-router-dom'
|
|
3
|
+
import { ArrowLeft, PanelLeft, Maximize2, GripVertical, Smartphone, Tablet, Monitor } from 'lucide-react'
|
|
4
|
+
import { Button } from '@/components/ui/button'
|
|
5
|
+
import { ThemeToggle } from '@/components/ThemeToggle'
|
|
6
|
+
import { loadShellPreview } from '@/lib/shell-loader'
|
|
7
|
+
import React from 'react'
|
|
8
|
+
|
|
9
|
+
const MIN_WIDTH = 320
|
|
10
|
+
const DEFAULT_WIDTH_PERCENT = 100
|
|
11
|
+
|
|
12
|
+
export function ShellDesignPage() {
|
|
13
|
+
const navigate = useNavigate()
|
|
14
|
+
const [widthPercent, setWidthPercent] = useState(DEFAULT_WIDTH_PERCENT)
|
|
15
|
+
const containerRef = useRef<HTMLDivElement>(null)
|
|
16
|
+
const isDragging = useRef(false)
|
|
17
|
+
|
|
18
|
+
// Handle resize drag
|
|
19
|
+
const handleMouseDown = useCallback(() => {
|
|
20
|
+
isDragging.current = true
|
|
21
|
+
|
|
22
|
+
const handleMouseMove = (e: MouseEvent) => {
|
|
23
|
+
if (!isDragging.current || !containerRef.current) return
|
|
24
|
+
|
|
25
|
+
const containerRect = containerRef.current.getBoundingClientRect()
|
|
26
|
+
const containerWidth = containerRect.width
|
|
27
|
+
const containerCenter = containerRect.left + containerWidth / 2
|
|
28
|
+
|
|
29
|
+
// Calculate distance from center
|
|
30
|
+
const distanceFromCenter = Math.abs(e.clientX - containerCenter)
|
|
31
|
+
const maxDistance = containerWidth / 2
|
|
32
|
+
|
|
33
|
+
// Convert to percentage (distance from center * 2 = total width)
|
|
34
|
+
let newWidthPercent = (distanceFromCenter / maxDistance) * 100
|
|
35
|
+
|
|
36
|
+
// Clamp between min width and 100%
|
|
37
|
+
const minPercent = (MIN_WIDTH / containerWidth) * 100
|
|
38
|
+
newWidthPercent = Math.max(minPercent, Math.min(100, newWidthPercent))
|
|
39
|
+
|
|
40
|
+
setWidthPercent(newWidthPercent)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const handleMouseUp = () => {
|
|
44
|
+
isDragging.current = false
|
|
45
|
+
document.removeEventListener('mousemove', handleMouseMove)
|
|
46
|
+
document.removeEventListener('mouseup', handleMouseUp)
|
|
47
|
+
document.body.style.cursor = ''
|
|
48
|
+
document.body.style.userSelect = ''
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
document.addEventListener('mousemove', handleMouseMove)
|
|
52
|
+
document.addEventListener('mouseup', handleMouseUp)
|
|
53
|
+
document.body.style.cursor = 'ew-resize'
|
|
54
|
+
document.body.style.userSelect = 'none'
|
|
55
|
+
}, [])
|
|
56
|
+
|
|
57
|
+
const previewWidth = `${widthPercent}%`
|
|
58
|
+
|
|
59
|
+
return (
|
|
60
|
+
<div className="h-screen bg-stone-100 dark:bg-stone-900 animate-fade-in flex flex-col overflow-hidden">
|
|
61
|
+
{/* Header */}
|
|
62
|
+
<header className="border-b border-stone-200 dark:border-stone-800 bg-white dark:bg-stone-950 shrink-0 z-50">
|
|
63
|
+
<div className="px-4 py-2 flex items-center gap-4">
|
|
64
|
+
<Button
|
|
65
|
+
variant="ghost"
|
|
66
|
+
size="sm"
|
|
67
|
+
onClick={() => navigate('/design')}
|
|
68
|
+
className="text-stone-600 dark:text-stone-400 hover:text-stone-900 dark:hover:text-stone-100 -ml-2"
|
|
69
|
+
>
|
|
70
|
+
<ArrowLeft className="w-4 h-4 mr-2" strokeWidth={1.5} />
|
|
71
|
+
Back
|
|
72
|
+
</Button>
|
|
73
|
+
<div className="h-4 w-px bg-stone-200 dark:bg-stone-700" />
|
|
74
|
+
<div className="flex items-center gap-2">
|
|
75
|
+
<PanelLeft className="w-4 h-4 text-stone-400" strokeWidth={1.5} />
|
|
76
|
+
<span className="text-sm font-medium text-stone-700 dark:text-stone-300">
|
|
77
|
+
Shell Design
|
|
78
|
+
</span>
|
|
79
|
+
</div>
|
|
80
|
+
|
|
81
|
+
{/* Width indicator and device presets */}
|
|
82
|
+
<div className="ml-auto flex items-center gap-4">
|
|
83
|
+
{/* Device size presets */}
|
|
84
|
+
<div className="flex items-center gap-1 border-r border-stone-200 dark:border-stone-700 pr-4">
|
|
85
|
+
<button
|
|
86
|
+
onClick={() => setWidthPercent(30)}
|
|
87
|
+
className={`p-1.5 rounded transition-colors ${
|
|
88
|
+
widthPercent <= 40
|
|
89
|
+
? 'bg-stone-200 dark:bg-stone-700 text-stone-900 dark:text-stone-100'
|
|
90
|
+
: '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'
|
|
91
|
+
}`}
|
|
92
|
+
title="Mobile (30%)"
|
|
93
|
+
>
|
|
94
|
+
<Smartphone className="w-4 h-4" strokeWidth={1.5} />
|
|
95
|
+
</button>
|
|
96
|
+
<button
|
|
97
|
+
onClick={() => setWidthPercent(60)}
|
|
98
|
+
className={`p-1.5 rounded transition-colors ${
|
|
99
|
+
widthPercent > 40 && widthPercent <= 60
|
|
100
|
+
? 'bg-stone-200 dark:bg-stone-700 text-stone-900 dark:text-stone-100'
|
|
101
|
+
: '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'
|
|
102
|
+
}`}
|
|
103
|
+
title="Tablet (60%)"
|
|
104
|
+
>
|
|
105
|
+
<Tablet className="w-4 h-4" strokeWidth={1.5} />
|
|
106
|
+
</button>
|
|
107
|
+
<button
|
|
108
|
+
onClick={() => setWidthPercent(100)}
|
|
109
|
+
className={`p-1.5 rounded transition-colors ${
|
|
110
|
+
widthPercent > 60
|
|
111
|
+
? 'bg-stone-200 dark:bg-stone-700 text-stone-900 dark:text-stone-100'
|
|
112
|
+
: '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'
|
|
113
|
+
}`}
|
|
114
|
+
title="Desktop (100%)"
|
|
115
|
+
>
|
|
116
|
+
<Monitor className="w-4 h-4" strokeWidth={1.5} />
|
|
117
|
+
</button>
|
|
118
|
+
</div>
|
|
119
|
+
<span className="text-xs text-stone-500 dark:text-stone-400 font-mono w-10 text-right">
|
|
120
|
+
{Math.round(widthPercent)}%
|
|
121
|
+
</span>
|
|
122
|
+
<ThemeToggle />
|
|
123
|
+
<a
|
|
124
|
+
href="/shell/design/fullscreen"
|
|
125
|
+
target="_blank"
|
|
126
|
+
rel="noopener noreferrer"
|
|
127
|
+
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"
|
|
128
|
+
>
|
|
129
|
+
<Maximize2 className="w-3.5 h-3.5" strokeWidth={1.5} />
|
|
130
|
+
Fullscreen
|
|
131
|
+
</a>
|
|
132
|
+
</div>
|
|
133
|
+
</div>
|
|
134
|
+
</header>
|
|
135
|
+
|
|
136
|
+
{/* Preview area with resizable container */}
|
|
137
|
+
<div
|
|
138
|
+
ref={containerRef}
|
|
139
|
+
className="flex-1 overflow-hidden flex items-stretch justify-center p-6"
|
|
140
|
+
>
|
|
141
|
+
{/* Left resize handle */}
|
|
142
|
+
<div
|
|
143
|
+
className="w-4 flex items-center justify-center cursor-ew-resize group shrink-0"
|
|
144
|
+
onMouseDown={handleMouseDown}
|
|
145
|
+
>
|
|
146
|
+
<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">
|
|
147
|
+
<GripVertical className="w-3 h-3 text-stone-500 dark:text-stone-400 opacity-0 group-hover:opacity-100 transition-opacity" strokeWidth={2} />
|
|
148
|
+
</div>
|
|
149
|
+
</div>
|
|
150
|
+
|
|
151
|
+
{/* Preview container using iframe for true isolation */}
|
|
152
|
+
<div
|
|
153
|
+
className="bg-white dark:bg-stone-950 rounded-lg shadow-xl border border-stone-200 dark:border-stone-700 overflow-hidden"
|
|
154
|
+
style={{ width: previewWidth, minWidth: MIN_WIDTH, maxWidth: '100%' }}
|
|
155
|
+
>
|
|
156
|
+
<iframe
|
|
157
|
+
src="/shell/design/fullscreen"
|
|
158
|
+
className="w-full h-full border-0"
|
|
159
|
+
title="Shell Preview"
|
|
160
|
+
/>
|
|
161
|
+
</div>
|
|
162
|
+
|
|
163
|
+
{/* Right resize handle */}
|
|
164
|
+
<div
|
|
165
|
+
className="w-4 flex items-center justify-center cursor-ew-resize group shrink-0"
|
|
166
|
+
onMouseDown={handleMouseDown}
|
|
167
|
+
>
|
|
168
|
+
<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">
|
|
169
|
+
<GripVertical className="w-3 h-3 text-stone-500 dark:text-stone-400 opacity-0 group-hover:opacity-100 transition-opacity" strokeWidth={2} />
|
|
170
|
+
</div>
|
|
171
|
+
</div>
|
|
172
|
+
</div>
|
|
173
|
+
</div>
|
|
174
|
+
)
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Fullscreen version of the shell preview (for screenshots)
|
|
179
|
+
* Syncs theme with parent window via localStorage
|
|
180
|
+
*/
|
|
181
|
+
export function ShellDesignFullscreen() {
|
|
182
|
+
const shellPreviewLoader = loadShellPreview()
|
|
183
|
+
|
|
184
|
+
const ShellPreviewComponent = useMemo(() => {
|
|
185
|
+
if (!shellPreviewLoader) return null
|
|
186
|
+
return React.lazy(shellPreviewLoader)
|
|
187
|
+
}, [shellPreviewLoader])
|
|
188
|
+
|
|
189
|
+
// Sync theme with parent window
|
|
190
|
+
useEffect(() => {
|
|
191
|
+
const applyTheme = () => {
|
|
192
|
+
const theme = localStorage.getItem('theme') || 'system'
|
|
193
|
+
const root = document.documentElement
|
|
194
|
+
|
|
195
|
+
if (theme === 'system') {
|
|
196
|
+
const systemDark = window.matchMedia('(prefers-color-scheme: dark)').matches
|
|
197
|
+
root.classList.toggle('dark', systemDark)
|
|
198
|
+
} else {
|
|
199
|
+
root.classList.toggle('dark', theme === 'dark')
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// Apply on mount
|
|
204
|
+
applyTheme()
|
|
205
|
+
|
|
206
|
+
// Listen for storage changes (from parent window)
|
|
207
|
+
const handleStorageChange = (e: StorageEvent) => {
|
|
208
|
+
if (e.key === 'theme') {
|
|
209
|
+
applyTheme()
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
window.addEventListener('storage', handleStorageChange)
|
|
213
|
+
|
|
214
|
+
// Also poll for changes since storage event doesn't fire in same window
|
|
215
|
+
const interval = setInterval(applyTheme, 100)
|
|
216
|
+
|
|
217
|
+
return () => {
|
|
218
|
+
window.removeEventListener('storage', handleStorageChange)
|
|
219
|
+
clearInterval(interval)
|
|
220
|
+
}
|
|
221
|
+
}, [])
|
|
222
|
+
|
|
223
|
+
if (!ShellPreviewComponent) {
|
|
224
|
+
return (
|
|
225
|
+
<div className="h-screen flex items-center justify-center bg-background">
|
|
226
|
+
<p className="text-stone-600 dark:text-stone-400">Shell preview not found.</p>
|
|
227
|
+
</div>
|
|
228
|
+
)
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
return (
|
|
232
|
+
<Suspense
|
|
233
|
+
fallback={
|
|
234
|
+
<div className="h-screen flex items-center justify-center bg-background">
|
|
235
|
+
<div className="text-stone-500 dark:text-stone-400">Loading...</div>
|
|
236
|
+
</div>
|
|
237
|
+
}
|
|
238
|
+
>
|
|
239
|
+
<ShellPreviewComponent />
|
|
240
|
+
</Suspense>
|
|
241
|
+
)
|
|
242
|
+
}
|
|
@@ -0,0 +1,121 @@
|
|
|
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 { ChevronDown, PanelTop, Square } from 'lucide-react'
|
|
5
|
+
import { EmptyState } from '@/components/EmptyState'
|
|
6
|
+
import type { ParsedSpec } from '@/types/section'
|
|
7
|
+
|
|
8
|
+
interface SpecCardProps {
|
|
9
|
+
spec: ParsedSpec | null
|
|
10
|
+
sectionTitle?: string
|
|
11
|
+
sectionId?: string
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function SpecCard({ spec, sectionTitle, sectionId }: SpecCardProps) {
|
|
15
|
+
const [userFlowsOpen, setUserFlowsOpen] = useState(false)
|
|
16
|
+
const [uiReqOpen, setUiReqOpen] = useState(false)
|
|
17
|
+
|
|
18
|
+
// Empty state
|
|
19
|
+
if (!spec) {
|
|
20
|
+
return <EmptyState type="spec" context={sectionTitle} contextId={sectionId} />
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
return (
|
|
24
|
+
<Card className="border-stone-200 dark:border-stone-700 shadow-sm">
|
|
25
|
+
<CardHeader className="pb-4">
|
|
26
|
+
<CardTitle className="text-lg font-semibold text-stone-900 dark:text-stone-100">
|
|
27
|
+
{sectionTitle || 'Specification'}
|
|
28
|
+
</CardTitle>
|
|
29
|
+
</CardHeader>
|
|
30
|
+
<CardContent className="space-y-4">
|
|
31
|
+
{/* Overview */}
|
|
32
|
+
{spec.overview && (
|
|
33
|
+
<p className="text-stone-600 dark:text-stone-400 leading-relaxed">
|
|
34
|
+
{spec.overview}
|
|
35
|
+
</p>
|
|
36
|
+
)}
|
|
37
|
+
|
|
38
|
+
{/* User Flows - Expandable */}
|
|
39
|
+
{spec.userFlows.length > 0 && (
|
|
40
|
+
<Collapsible open={userFlowsOpen} onOpenChange={setUserFlowsOpen}>
|
|
41
|
+
<CollapsibleTrigger className="flex items-center justify-between w-full py-2 text-left group">
|
|
42
|
+
<span className="text-sm font-medium text-stone-500 dark:text-stone-400 uppercase tracking-wide">
|
|
43
|
+
User Flows
|
|
44
|
+
<span className="ml-2 text-stone-400 dark:text-stone-500 normal-case tracking-normal">
|
|
45
|
+
({spec.userFlows.length})
|
|
46
|
+
</span>
|
|
47
|
+
</span>
|
|
48
|
+
<ChevronDown
|
|
49
|
+
className={`w-4 h-4 text-stone-400 dark:text-stone-500 transition-transform ${userFlowsOpen ? 'rotate-180' : ''
|
|
50
|
+
}`}
|
|
51
|
+
strokeWidth={1.5}
|
|
52
|
+
/>
|
|
53
|
+
</CollapsibleTrigger>
|
|
54
|
+
<CollapsibleContent>
|
|
55
|
+
<ul className="space-y-2 pt-2">
|
|
56
|
+
{spec.userFlows.map((flow, index) => (
|
|
57
|
+
<li key={index} className="flex items-start gap-3">
|
|
58
|
+
<span className="w-1.5 h-1.5 rounded-full bg-stone-900 dark:bg-stone-100 mt-2 shrink-0" />
|
|
59
|
+
<span className="text-stone-700 dark:text-stone-300 text-sm">
|
|
60
|
+
{flow}
|
|
61
|
+
</span>
|
|
62
|
+
</li>
|
|
63
|
+
))}
|
|
64
|
+
</ul>
|
|
65
|
+
</CollapsibleContent>
|
|
66
|
+
</Collapsible>
|
|
67
|
+
)}
|
|
68
|
+
|
|
69
|
+
{/* UI Requirements - Expandable */}
|
|
70
|
+
{spec.uiRequirements.length > 0 && (
|
|
71
|
+
<Collapsible open={uiReqOpen} onOpenChange={setUiReqOpen}>
|
|
72
|
+
<CollapsibleTrigger className="flex items-center justify-between w-full py-2 text-left group">
|
|
73
|
+
<span className="text-sm font-medium text-stone-500 dark:text-stone-400 uppercase tracking-wide">
|
|
74
|
+
UI Requirements
|
|
75
|
+
<span className="ml-2 text-stone-400 dark:text-stone-500 normal-case tracking-normal">
|
|
76
|
+
({spec.uiRequirements.length})
|
|
77
|
+
</span>
|
|
78
|
+
</span>
|
|
79
|
+
<ChevronDown
|
|
80
|
+
className={`w-4 h-4 text-stone-400 dark:text-stone-500 transition-transform ${uiReqOpen ? 'rotate-180' : ''
|
|
81
|
+
}`}
|
|
82
|
+
strokeWidth={1.5}
|
|
83
|
+
/>
|
|
84
|
+
</CollapsibleTrigger>
|
|
85
|
+
<CollapsibleContent>
|
|
86
|
+
<ul className="space-y-2 pt-2">
|
|
87
|
+
{spec.uiRequirements.map((req, index) => (
|
|
88
|
+
<li key={index} className="flex items-start gap-3">
|
|
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 text-sm">
|
|
91
|
+
{req}
|
|
92
|
+
</span>
|
|
93
|
+
</li>
|
|
94
|
+
))}
|
|
95
|
+
</ul>
|
|
96
|
+
</CollapsibleContent>
|
|
97
|
+
</Collapsible>
|
|
98
|
+
)}
|
|
99
|
+
|
|
100
|
+
{/* Display Configuration */}
|
|
101
|
+
<div className="flex items-center gap-2 pt-2 border-t border-stone-100 dark:border-stone-800">
|
|
102
|
+
{spec.useShell ? (
|
|
103
|
+
<>
|
|
104
|
+
<PanelTop className="w-4 h-4 text-stone-400 dark:text-stone-500" strokeWidth={1.5} />
|
|
105
|
+
<span className="text-sm text-stone-500 dark:text-stone-400">
|
|
106
|
+
Displays inside app shell
|
|
107
|
+
</span>
|
|
108
|
+
</>
|
|
109
|
+
) : (
|
|
110
|
+
<>
|
|
111
|
+
<Square className="w-4 h-4 text-stone-400 dark:text-stone-500" strokeWidth={1.5} />
|
|
112
|
+
<span className="text-sm text-stone-500 dark:text-stone-400">
|
|
113
|
+
Standalone page (no shell)
|
|
114
|
+
</span>
|
|
115
|
+
</>
|
|
116
|
+
)}
|
|
117
|
+
</div>
|
|
118
|
+
</CardContent>
|
|
119
|
+
</Card>
|
|
120
|
+
)
|
|
121
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { Check, ArrowRight, AlertTriangle } from 'lucide-react'
|
|
2
|
+
import type { ReactNode } from 'react'
|
|
3
|
+
|
|
4
|
+
export type StepStatus = 'completed' | 'current' | 'upcoming' | 'skipped'
|
|
5
|
+
|
|
6
|
+
interface StepIndicatorProps {
|
|
7
|
+
step: number
|
|
8
|
+
status: StepStatus
|
|
9
|
+
children: ReactNode
|
|
10
|
+
isLast?: boolean
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function StepIndicator({ step, status, children, isLast = false }: StepIndicatorProps) {
|
|
14
|
+
return (
|
|
15
|
+
<div className="relative">
|
|
16
|
+
{/* Vertical connecting line - extends from this step to the next */}
|
|
17
|
+
{!isLast && (
|
|
18
|
+
<div
|
|
19
|
+
className="absolute left-[10px] top-[28px] w-[2px] h-[calc(100%+16px)] bg-stone-200 dark:bg-stone-700"
|
|
20
|
+
aria-hidden="true"
|
|
21
|
+
/>
|
|
22
|
+
)}
|
|
23
|
+
|
|
24
|
+
{/* Step badge positioned at top-left */}
|
|
25
|
+
<div className="absolute -left-[2px] top-0 z-10">
|
|
26
|
+
<StepBadge step={step} status={status} />
|
|
27
|
+
</div>
|
|
28
|
+
|
|
29
|
+
{/* Card content with left padding to accommodate the step indicator */}
|
|
30
|
+
<div className="pl-10">
|
|
31
|
+
{children}
|
|
32
|
+
</div>
|
|
33
|
+
</div>
|
|
34
|
+
)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
interface StepBadgeProps {
|
|
38
|
+
step: number
|
|
39
|
+
status: StepStatus
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function StepBadge({ step, status }: StepBadgeProps) {
|
|
43
|
+
const baseClasses = "w-6 h-6 rounded-full flex items-center justify-center text-xs font-semibold transition-all duration-200"
|
|
44
|
+
|
|
45
|
+
if (status === 'completed') {
|
|
46
|
+
return (
|
|
47
|
+
<div className={`${baseClasses} bg-stone-200 dark:bg-stone-700 text-stone-500 dark:text-stone-400`}>
|
|
48
|
+
<Check className="w-3 h-3" strokeWidth={2.5} />
|
|
49
|
+
</div>
|
|
50
|
+
)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (status === 'current') {
|
|
54
|
+
return (
|
|
55
|
+
<div className={`${baseClasses} bg-stone-900 dark:bg-stone-100 text-stone-100 dark:text-stone-900 shadow-sm`}>
|
|
56
|
+
<ArrowRight className="w-3 h-3" strokeWidth={2.5} />
|
|
57
|
+
</div>
|
|
58
|
+
)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (status === 'skipped') {
|
|
62
|
+
return (
|
|
63
|
+
<div className={`${baseClasses} bg-amber-100 dark:bg-amber-900/30 text-amber-600 dark:text-amber-400`}>
|
|
64
|
+
<AlertTriangle className="w-3 h-3" strokeWidth={2.5} />
|
|
65
|
+
</div>
|
|
66
|
+
)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// upcoming
|
|
70
|
+
return (
|
|
71
|
+
<div className={`${baseClasses} bg-stone-200 dark:bg-stone-700 text-stone-500 dark:text-stone-400`}>
|
|
72
|
+
{step}
|
|
73
|
+
</div>
|
|
74
|
+
)
|
|
75
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { useState, useEffect } from 'react'
|
|
2
|
+
import { Moon, Sun } from 'lucide-react'
|
|
3
|
+
import { Button } from '@/components/ui/button'
|
|
4
|
+
|
|
5
|
+
type Theme = 'light' | 'dark' | 'system'
|
|
6
|
+
|
|
7
|
+
export function ThemeToggle({ headless }: { headless?: boolean }) {
|
|
8
|
+
const [theme, setTheme] = useState<Theme>(() => {
|
|
9
|
+
if (typeof window !== 'undefined') {
|
|
10
|
+
// Check URL param first (from Agent OS)
|
|
11
|
+
const params = new URLSearchParams(window.location.search)
|
|
12
|
+
const urlTheme = params.get('theme') as Theme
|
|
13
|
+
if (urlTheme && ['light', 'dark', 'system'].includes(urlTheme)) {
|
|
14
|
+
return urlTheme
|
|
15
|
+
}
|
|
16
|
+
return (localStorage.getItem('theme') as Theme) || 'system'
|
|
17
|
+
}
|
|
18
|
+
return 'system'
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
useEffect(() => {
|
|
22
|
+
const root = document.documentElement
|
|
23
|
+
|
|
24
|
+
const applyTheme = (theme: Theme) => {
|
|
25
|
+
if (theme === 'system') {
|
|
26
|
+
const systemDark = window.matchMedia('(prefers-color-scheme: dark)').matches
|
|
27
|
+
root.classList.toggle('dark', systemDark)
|
|
28
|
+
} else {
|
|
29
|
+
root.classList.toggle('dark', theme === 'dark')
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
applyTheme(theme)
|
|
34
|
+
localStorage.setItem('theme', theme)
|
|
35
|
+
|
|
36
|
+
// Listen for system theme changes when in system mode
|
|
37
|
+
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)')
|
|
38
|
+
const handleChange = () => {
|
|
39
|
+
if (theme === 'system') {
|
|
40
|
+
applyTheme('system')
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
mediaQuery.addEventListener('change', handleChange)
|
|
44
|
+
|
|
45
|
+
// Listen for messages from Agent OS (Parent Window)
|
|
46
|
+
const handleMessage = (event: MessageEvent) => {
|
|
47
|
+
if (event.data?.type === 'THEME_CHANGE' && event.data.theme) {
|
|
48
|
+
setTheme(event.data.theme as Theme)
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
window.addEventListener('message', handleMessage)
|
|
52
|
+
|
|
53
|
+
return () => {
|
|
54
|
+
mediaQuery.removeEventListener('change', handleChange)
|
|
55
|
+
window.removeEventListener('message', handleMessage)
|
|
56
|
+
}
|
|
57
|
+
}, [theme])
|
|
58
|
+
|
|
59
|
+
const toggleTheme = () => {
|
|
60
|
+
setTheme((prev) => {
|
|
61
|
+
if (prev === 'light') return 'dark'
|
|
62
|
+
if (prev === 'dark') return 'system'
|
|
63
|
+
return 'light'
|
|
64
|
+
})
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const isDark = theme === 'dark' || (theme === 'system' && typeof window !== 'undefined' && window.matchMedia('(prefers-color-scheme: dark)').matches)
|
|
68
|
+
|
|
69
|
+
if (headless) return null
|
|
70
|
+
|
|
71
|
+
return (
|
|
72
|
+
<Button
|
|
73
|
+
variant="ghost"
|
|
74
|
+
size="icon"
|
|
75
|
+
onClick={toggleTheme}
|
|
76
|
+
className="w-8 h-8 text-stone-600 dark:text-stone-400 hover:text-stone-900 dark:hover:text-stone-100"
|
|
77
|
+
title={`Theme: ${theme}`}
|
|
78
|
+
>
|
|
79
|
+
{isDark ? (
|
|
80
|
+
<Moon className="w-4 h-4" strokeWidth={1.5} />
|
|
81
|
+
) : (
|
|
82
|
+
<Sun className="w-4 h-4" strokeWidth={1.5} />
|
|
83
|
+
)}
|
|
84
|
+
</Button>
|
|
85
|
+
)
|
|
86
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import React, { createContext, useContext, useState, useCallback, ReactNode } from 'react';
|
|
2
|
+
import { X, CheckCircle, Info, AlertTriangle } from 'lucide-react';
|
|
3
|
+
|
|
4
|
+
type ToastType = 'success' | 'error' | 'info' | 'warning';
|
|
5
|
+
|
|
6
|
+
interface Toast {
|
|
7
|
+
id: string;
|
|
8
|
+
title: string;
|
|
9
|
+
description?: string;
|
|
10
|
+
type?: ToastType;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
interface ToastContextType {
|
|
14
|
+
toast: (props: Omit<Toast, 'id'>) => void;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const ToastContext = createContext<ToastContextType | undefined>(undefined);
|
|
18
|
+
|
|
19
|
+
export function useToast() {
|
|
20
|
+
const context = useContext(ToastContext);
|
|
21
|
+
if (!context) {
|
|
22
|
+
throw new Error('useToast must be used within a ToastProvider');
|
|
23
|
+
}
|
|
24
|
+
return context;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function ToastProvider({ children }: { children: ReactNode }) {
|
|
28
|
+
const [toasts, setToasts] = useState<Toast[]>([]);
|
|
29
|
+
|
|
30
|
+
const toast = useCallback(({ title, description, type = 'info' }: Omit<Toast, 'id'>) => {
|
|
31
|
+
const id = Math.random().toString(36).substring(2, 9);
|
|
32
|
+
setToasts((prev) => [...prev, { id, title, description, type }]);
|
|
33
|
+
|
|
34
|
+
// Auto dismiss
|
|
35
|
+
setTimeout(() => {
|
|
36
|
+
removeToast(id);
|
|
37
|
+
}, 3000);
|
|
38
|
+
}, []);
|
|
39
|
+
|
|
40
|
+
const removeToast = useCallback((id: string) => {
|
|
41
|
+
setToasts((prev) => prev.filter((t) => t.id !== id));
|
|
42
|
+
}, []);
|
|
43
|
+
|
|
44
|
+
return (
|
|
45
|
+
<ToastContext.Provider value={{ toast }}>
|
|
46
|
+
{children}
|
|
47
|
+
<div className="fixed bottom-4 right-4 z-[9999] flex flex-col gap-2">
|
|
48
|
+
{toasts.map((t) => (
|
|
49
|
+
<div
|
|
50
|
+
key={t.id}
|
|
51
|
+
className={`
|
|
52
|
+
min-w-[300px] border rounded-lg shadow-lg p-4 transition-all duration-300 animate-in slide-in-from-right
|
|
53
|
+
bg-white dark:bg-stone-900 border-stone-200 dark:border-stone-800
|
|
54
|
+
`}
|
|
55
|
+
>
|
|
56
|
+
<div className="flex items-start gap-3">
|
|
57
|
+
<div className="shrink-0 pt-0.5">
|
|
58
|
+
{t.type === 'success' && <CheckCircle size={18} className="text-emerald-500" />}
|
|
59
|
+
{t.type === 'error' && <AlertTriangle size={18} className="text-red-500" />}
|
|
60
|
+
{t.type === 'warning' && <AlertTriangle size={18} className="text-amber-500" />}
|
|
61
|
+
{t.type === 'info' && <Info size={18} className="text-blue-500" />}
|
|
62
|
+
</div>
|
|
63
|
+
<div className="flex-1">
|
|
64
|
+
<h4 className="text-sm font-semibold text-stone-900 dark:text-stone-100">{t.title}</h4>
|
|
65
|
+
{t.description && (
|
|
66
|
+
<p className="text-sm text-stone-500 dark:text-stone-400 mt-1">{t.description}</p>
|
|
67
|
+
)}
|
|
68
|
+
</div>
|
|
69
|
+
<button
|
|
70
|
+
onClick={() => removeToast(t.id)}
|
|
71
|
+
className="shrink-0 text-stone-400 hover:text-stone-600 dark:hover:text-stone-300"
|
|
72
|
+
>
|
|
73
|
+
<X size={16} />
|
|
74
|
+
</button>
|
|
75
|
+
</div>
|
|
76
|
+
</div>
|
|
77
|
+
))}
|
|
78
|
+
</div>
|
|
79
|
+
</ToastContext.Provider>
|
|
80
|
+
);
|
|
81
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import * as React from "react"
|
|
4
|
+
import * as AvatarPrimitive from "@radix-ui/react-avatar"
|
|
5
|
+
|
|
6
|
+
import { cn } from "@/lib/utils"
|
|
7
|
+
|
|
8
|
+
function Avatar({
|
|
9
|
+
className,
|
|
10
|
+
...props
|
|
11
|
+
}: React.ComponentProps<typeof AvatarPrimitive.Root>) {
|
|
12
|
+
return (
|
|
13
|
+
<AvatarPrimitive.Root
|
|
14
|
+
data-slot="avatar"
|
|
15
|
+
className={cn(
|
|
16
|
+
"relative flex size-8 shrink-0 overflow-hidden rounded-full",
|
|
17
|
+
className
|
|
18
|
+
)}
|
|
19
|
+
{...props}
|
|
20
|
+
/>
|
|
21
|
+
)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function AvatarImage({
|
|
25
|
+
className,
|
|
26
|
+
...props
|
|
27
|
+
}: React.ComponentProps<typeof AvatarPrimitive.Image>) {
|
|
28
|
+
return (
|
|
29
|
+
<AvatarPrimitive.Image
|
|
30
|
+
data-slot="avatar-image"
|
|
31
|
+
className={cn("aspect-square size-full", className)}
|
|
32
|
+
{...props}
|
|
33
|
+
/>
|
|
34
|
+
)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function AvatarFallback({
|
|
38
|
+
className,
|
|
39
|
+
...props
|
|
40
|
+
}: React.ComponentProps<typeof AvatarPrimitive.Fallback>) {
|
|
41
|
+
return (
|
|
42
|
+
<AvatarPrimitive.Fallback
|
|
43
|
+
data-slot="avatar-fallback"
|
|
44
|
+
className={cn(
|
|
45
|
+
"bg-muted flex size-full items-center justify-center rounded-full",
|
|
46
|
+
className
|
|
47
|
+
)}
|
|
48
|
+
{...props}
|
|
49
|
+
/>
|
|
50
|
+
)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export { Avatar, AvatarImage, AvatarFallback }
|