jettypod 4.4.118 → 4.4.121

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.
Files changed (240) hide show
  1. package/.env +4 -3
  2. package/Cargo.lock +6450 -0
  3. package/Cargo.toml +35 -0
  4. package/README.md +5 -1
  5. package/TAURI-MIGRATION-PLAN.md +840 -0
  6. package/apps/dashboard/app/connect-claude/page.tsx +5 -6
  7. package/apps/dashboard/app/decision/[id]/page.tsx +63 -58
  8. package/apps/dashboard/app/demo/gates/page.tsx +43 -45
  9. package/apps/dashboard/app/design-system/page.tsx +868 -0
  10. package/apps/dashboard/app/globals.css +80 -4
  11. package/apps/dashboard/app/install-claude/page.tsx +4 -6
  12. package/apps/dashboard/app/login/page.tsx +72 -54
  13. package/apps/dashboard/app/page.tsx +101 -48
  14. package/apps/dashboard/app/settings/page.tsx +61 -13
  15. package/apps/dashboard/app/signup/page.tsx +242 -0
  16. package/apps/dashboard/app/subscribe/page.tsx +0 -2
  17. package/apps/dashboard/app/tests/page.tsx +37 -4
  18. package/apps/dashboard/app/welcome/page.tsx +13 -16
  19. package/apps/dashboard/app/work/[id]/page.tsx +117 -118
  20. package/apps/dashboard/app/work/[id]/proof/page.tsx +1489 -0
  21. package/apps/dashboard/components/AppShell.tsx +92 -85
  22. package/apps/dashboard/components/CardMenu.tsx +45 -12
  23. package/apps/dashboard/components/ClaudePanel.tsx +771 -850
  24. package/apps/dashboard/components/ClaudePanelInput.tsx +43 -15
  25. package/apps/dashboard/components/ConnectClaudeScreen.tsx +17 -34
  26. package/apps/dashboard/components/CopyableId.tsx +3 -4
  27. package/apps/dashboard/components/DetailReviewActions.tsx +100 -0
  28. package/apps/dashboard/components/DragContext.tsx +134 -63
  29. package/apps/dashboard/components/DraggableCard.tsx +3 -5
  30. package/apps/dashboard/components/DropZone.tsx +6 -7
  31. package/apps/dashboard/components/EditableDetailDescription.tsx +7 -13
  32. package/apps/dashboard/components/EditableDetailTitle.tsx +6 -13
  33. package/apps/dashboard/components/EditableTitle.tsx +26 -7
  34. package/apps/dashboard/components/ElapsedTimer.tsx +66 -0
  35. package/apps/dashboard/components/EpicGroup.tsx +359 -0
  36. package/apps/dashboard/components/GateCard.tsx +79 -17
  37. package/apps/dashboard/components/GateChoiceCard.tsx +15 -18
  38. package/apps/dashboard/components/InstallClaudeScreen.tsx +15 -32
  39. package/apps/dashboard/components/JettyLoader.tsx +37 -0
  40. package/apps/dashboard/components/KanbanBoard.tsx +368 -958
  41. package/apps/dashboard/components/KanbanCard.tsx +740 -0
  42. package/apps/dashboard/components/LazyCard.tsx +62 -0
  43. package/apps/dashboard/components/LazyMarkdown.tsx +11 -0
  44. package/apps/dashboard/components/MainNav.tsx +38 -73
  45. package/apps/dashboard/components/MessageBlock.tsx +468 -0
  46. package/apps/dashboard/components/ModeStartCard.tsx +15 -16
  47. package/apps/dashboard/components/OnboardingWelcome.tsx +213 -0
  48. package/apps/dashboard/components/PlaceholderCard.tsx +3 -4
  49. package/apps/dashboard/components/ProjectSwitcher.tsx +30 -30
  50. package/apps/dashboard/components/PrototypeTimeline.tsx +72 -51
  51. package/apps/dashboard/components/RealTimeKanbanWrapper.tsx +406 -388
  52. package/apps/dashboard/components/RealTimeTestsWrapper.tsx +373 -235
  53. package/apps/dashboard/components/ReviewFooter.tsx +139 -0
  54. package/apps/dashboard/components/SessionList.tsx +19 -19
  55. package/apps/dashboard/components/SubscribeContent.tsx +91 -47
  56. package/apps/dashboard/components/TestTree.tsx +16 -16
  57. package/apps/dashboard/components/TipCard.tsx +16 -17
  58. package/apps/dashboard/components/Toast.tsx +5 -6
  59. package/apps/dashboard/components/TypeIcon.tsx +55 -0
  60. package/apps/dashboard/components/ViewModeToolbar.tsx +104 -0
  61. package/apps/dashboard/components/WaveCompletionAnimation.tsx +52 -65
  62. package/apps/dashboard/components/WelcomeScreen.tsx +19 -35
  63. package/apps/dashboard/components/WorkItemHeader.tsx +4 -5
  64. package/apps/dashboard/components/WorkItemTree.tsx +11 -32
  65. package/apps/dashboard/components/settings/AccountSection.tsx +55 -35
  66. package/apps/dashboard/components/settings/AiContextSection.tsx +89 -0
  67. package/apps/dashboard/components/settings/ContextDocumentsSection.tsx +317 -0
  68. package/apps/dashboard/components/settings/EnvVarsSection.tsx +74 -152
  69. package/apps/dashboard/components/settings/GeneralSection.tsx +162 -56
  70. package/apps/dashboard/components/settings/ProjectStackSection.tsx +948 -0
  71. package/apps/dashboard/components/settings/SettingsLayout.tsx +4 -5
  72. package/apps/dashboard/components/ui/Button.tsx +104 -0
  73. package/apps/dashboard/components/ui/Input.tsx +78 -0
  74. package/apps/dashboard/components.json +1 -1
  75. package/apps/dashboard/contexts/ClaudeSessionContext.tsx +711 -418
  76. package/apps/dashboard/contexts/ConnectionStatusContext.tsx +25 -5
  77. package/apps/dashboard/contexts/UsageContext.tsx +87 -32
  78. package/apps/dashboard/dev.sh +35 -0
  79. package/apps/dashboard/eslint.config.mjs +9 -9
  80. package/apps/dashboard/hooks/useKanbanAnimation.ts +29 -0
  81. package/apps/dashboard/hooks/useKanbanUndo.ts +83 -0
  82. package/apps/dashboard/hooks/useWebSocket.ts +138 -83
  83. package/apps/dashboard/index.html +73 -0
  84. package/apps/dashboard/lib/constants.ts +43 -0
  85. package/apps/dashboard/lib/data-bridge.ts +722 -0
  86. package/apps/dashboard/lib/db.ts +69 -1265
  87. package/apps/dashboard/lib/environment-config.ts +173 -0
  88. package/apps/dashboard/lib/environment-verification.ts +119 -0
  89. package/apps/dashboard/lib/kanban-utils.ts +270 -0
  90. package/apps/dashboard/lib/proof-run.ts +495 -0
  91. package/apps/dashboard/lib/proof-scenario-runner.ts +346 -0
  92. package/apps/dashboard/lib/run-migrations.js +27 -2
  93. package/apps/dashboard/lib/service-recovery.ts +326 -0
  94. package/apps/dashboard/lib/session-state-machine.ts +1 -0
  95. package/apps/dashboard/lib/session-state-utils.ts +0 -164
  96. package/apps/dashboard/lib/session-stream-manager.ts +308 -134
  97. package/apps/dashboard/lib/shadows.ts +7 -0
  98. package/apps/dashboard/lib/stream-manager-registry.ts +46 -6
  99. package/apps/dashboard/lib/tauri-bridge.ts +102 -0
  100. package/apps/dashboard/lib/tauri.ts +106 -0
  101. package/apps/dashboard/lib/utils.ts +6 -0
  102. package/apps/dashboard/next-env.d.ts +1 -1
  103. package/apps/dashboard/package.json +21 -32
  104. package/apps/dashboard/public/bug-icon.png +0 -0
  105. package/apps/dashboard/public/buoy-icon.png +0 -0
  106. package/apps/dashboard/public/fonts/Satoshi-Variable.woff2 +0 -0
  107. package/apps/dashboard/public/fonts/Satoshi-VariableItalic.woff2 +0 -0
  108. package/apps/dashboard/public/in-flight-seagull.png +0 -0
  109. package/apps/dashboard/public/jetty-icon-loading-alt.svg +11 -0
  110. package/apps/dashboard/public/jetty-icon-loading.svg +11 -0
  111. package/apps/dashboard/public/jettypod_logo.png +0 -0
  112. package/apps/dashboard/public/pier-icon.png +0 -0
  113. package/apps/dashboard/public/star-icon.png +0 -0
  114. package/apps/dashboard/public/wrench-icon.png +0 -0
  115. package/apps/dashboard/scripts/tauri-build.js +228 -0
  116. package/apps/dashboard/scripts/upload-tauri-to-r2.js +125 -0
  117. package/apps/dashboard/scripts/ws-server.js +191 -0
  118. package/apps/dashboard/src/main.tsx +12 -0
  119. package/apps/dashboard/src/router.tsx +107 -0
  120. package/apps/dashboard/src/vite-env.d.ts +1 -0
  121. package/apps/dashboard/tsconfig.json +7 -12
  122. package/apps/dashboard/tsconfig.tsbuildinfo +1 -1
  123. package/apps/dashboard/vite.config.ts +33 -0
  124. package/apps/update-server/src/index.ts +228 -80
  125. package/claude-hooks/global-guardrails.js +14 -13
  126. package/crates/jettypod-cli/Cargo.toml +19 -0
  127. package/crates/jettypod-cli/src/commands.rs +1249 -0
  128. package/crates/jettypod-cli/src/main.rs +595 -0
  129. package/crates/jettypod-core/Cargo.toml +26 -0
  130. package/crates/jettypod-core/build.rs +98 -0
  131. package/crates/jettypod-core/migrations/V1__baseline.sql +197 -0
  132. package/crates/jettypod-core/migrations/V2__work_items_indexes.sql +6 -0
  133. package/crates/jettypod-core/migrations/V3__qa_steps.sql +2 -0
  134. package/crates/jettypod-core/src/auth.rs +294 -0
  135. package/crates/jettypod-core/src/config.rs +397 -0
  136. package/crates/jettypod-core/src/db/mod.rs +507 -0
  137. package/crates/jettypod-core/src/db/recovery.rs +114 -0
  138. package/crates/jettypod-core/src/db/startup.rs +101 -0
  139. package/crates/jettypod-core/src/db/validate.rs +149 -0
  140. package/crates/jettypod-core/src/error.rs +76 -0
  141. package/crates/jettypod-core/src/git.rs +458 -0
  142. package/crates/jettypod-core/src/lib.rs +20 -0
  143. package/crates/jettypod-core/src/sessions.rs +625 -0
  144. package/crates/jettypod-core/src/skills.rs +556 -0
  145. package/crates/jettypod-core/src/work.rs +1086 -0
  146. package/crates/jettypod-core/src/worktree.rs +628 -0
  147. package/crates/jettypod-core/src/ws.rs +767 -0
  148. package/cucumber-test.cjs +6 -0
  149. package/cucumber.js +9 -3
  150. package/docs/COMMAND_REFERENCE.md +34 -0
  151. package/hooks/post-checkout +32 -75
  152. package/hooks/post-merge +111 -10
  153. package/jest.setup.js +1 -0
  154. package/jettypod.js +145 -116
  155. package/lib/bdd-preflight.js +96 -0
  156. package/lib/chore-taxonomy.js +33 -10
  157. package/lib/database.js +36 -16
  158. package/lib/db-watcher.js +1 -1
  159. package/lib/git-hooks/pre-commit +1 -1
  160. package/lib/jettypod-backup.js +27 -4
  161. package/lib/merge-lock.js +111 -253
  162. package/lib/migrations/027-plan-at-creation-column.js +3 -1
  163. package/lib/migrations/029-remove-autoincrement.js +307 -0
  164. package/lib/migrations/029-rename-corrupted-to-cleaned.js +149 -0
  165. package/lib/migrations/030-rejection-round-columns.js +54 -0
  166. package/lib/migrations/031-session-isolation-index.js +17 -0
  167. package/lib/migrations/index.js +47 -4
  168. package/lib/schema.js +10 -5
  169. package/lib/seed-onboarding.js +1 -1
  170. package/lib/update-command/index.js +9 -175
  171. package/lib/work-commands/index.js +144 -19
  172. package/lib/work-tracking/index.js +148 -27
  173. package/lib/worktree-diagnostics.js +16 -16
  174. package/lib/worktree-facade.js +1 -1
  175. package/lib/worktree-manager.js +8 -8
  176. package/lib/worktree-reconciler.js +5 -5
  177. package/package.json +9 -2
  178. package/scripts/ndjson-to-cucumber-json.js +152 -0
  179. package/scripts/postinstall.js +25 -0
  180. package/skills-templates/bug-mode/SKILL.md +79 -20
  181. package/skills-templates/bug-planning/SKILL.md +25 -29
  182. package/skills-templates/chore-mode/SKILL.md +171 -69
  183. package/skills-templates/chore-mode/verification.js +51 -10
  184. package/skills-templates/chore-planning/SKILL.md +47 -18
  185. package/skills-templates/design-system-selection/SKILL.md +273 -0
  186. package/skills-templates/epic-planning/SKILL.md +82 -48
  187. package/skills-templates/external-transition/SKILL.md +47 -47
  188. package/skills-templates/feature-planning/SKILL.md +173 -74
  189. package/skills-templates/production-mode/SKILL.md +69 -49
  190. package/skills-templates/request-routing/SKILL.md +4 -4
  191. package/skills-templates/simple-improvement/SKILL.md +74 -29
  192. package/skills-templates/speed-mode/SKILL.md +217 -141
  193. package/skills-templates/stable-mode/SKILL.md +148 -89
  194. package/apps/dashboard/README.md +0 -36
  195. package/apps/dashboard/app/api/claude/[workItemId]/message/route.ts +0 -386
  196. package/apps/dashboard/app/api/claude/[workItemId]/pin/route.ts +0 -24
  197. package/apps/dashboard/app/api/claude/[workItemId]/route.ts +0 -167
  198. package/apps/dashboard/app/api/claude/sessions/[sessionId]/content/route.ts +0 -52
  199. package/apps/dashboard/app/api/claude/sessions/[sessionId]/message/route.ts +0 -378
  200. package/apps/dashboard/app/api/claude/sessions/[sessionId]/pin/route.ts +0 -24
  201. package/apps/dashboard/app/api/claude/sessions/cleanup/route.ts +0 -34
  202. package/apps/dashboard/app/api/claude/sessions/route.ts +0 -184
  203. package/apps/dashboard/app/api/decisions/[id]/route.ts +0 -25
  204. package/apps/dashboard/app/api/internal/set-project/route.ts +0 -17
  205. package/apps/dashboard/app/api/kanban/route.ts +0 -15
  206. package/apps/dashboard/app/api/settings/env-vars/route.ts +0 -125
  207. package/apps/dashboard/app/api/settings/general/route.ts +0 -21
  208. package/apps/dashboard/app/api/tests/route.ts +0 -9
  209. package/apps/dashboard/app/api/tests/run/route.ts +0 -82
  210. package/apps/dashboard/app/api/tests/run/stream/route.ts +0 -71
  211. package/apps/dashboard/app/api/tests/undefined/route.ts +0 -9
  212. package/apps/dashboard/app/api/usage/route.ts +0 -17
  213. package/apps/dashboard/app/api/work/[id]/description/route.ts +0 -21
  214. package/apps/dashboard/app/api/work/[id]/epic/route.ts +0 -21
  215. package/apps/dashboard/app/api/work/[id]/order/route.ts +0 -21
  216. package/apps/dashboard/app/api/work/[id]/status/route.ts +0 -21
  217. package/apps/dashboard/app/api/work/[id]/title/route.ts +0 -21
  218. package/apps/dashboard/app/layout.tsx +0 -43
  219. package/apps/dashboard/components/UpgradeBanner.tsx +0 -29
  220. package/apps/dashboard/electron/ipc-handlers.js +0 -1028
  221. package/apps/dashboard/electron/main.js +0 -2124
  222. package/apps/dashboard/electron/preload.js +0 -123
  223. package/apps/dashboard/electron/session-manager.js +0 -141
  224. package/apps/dashboard/electron-builder.config.js +0 -357
  225. package/apps/dashboard/hooks/useClaudeSessions.ts +0 -299
  226. package/apps/dashboard/lib/claude-process-manager.ts +0 -492
  227. package/apps/dashboard/lib/db-bridge.ts +0 -282
  228. package/apps/dashboard/lib/prototypes.ts +0 -202
  229. package/apps/dashboard/lib/test-results-db.ts +0 -307
  230. package/apps/dashboard/lib/tests.ts +0 -282
  231. package/apps/dashboard/next.config.js +0 -50
  232. package/apps/dashboard/postcss.config.mjs +0 -7
  233. package/apps/dashboard/public/file.svg +0 -1
  234. package/apps/dashboard/public/globe.svg +0 -1
  235. package/apps/dashboard/public/next.svg +0 -1
  236. package/apps/dashboard/public/vercel.svg +0 -1
  237. package/apps/dashboard/public/window.svg +0 -1
  238. package/apps/dashboard/scripts/download-node.js +0 -104
  239. package/apps/dashboard/scripts/upload-to-r2.js +0 -89
  240. package/docs/bdd-guidance.md +0 -390
@@ -0,0 +1,213 @@
1
+
2
+ import { useState, useCallback } from 'react';
3
+ import { m, AnimatePresence, useReducedMotion } from 'framer-motion';
4
+ import type { WorkItem } from '@/lib/db';
5
+ import { shadow } from '@/lib/shadows';
6
+ import { Button } from '@/components/ui/Button';
7
+ import { TypeIcon } from '@/components/TypeIcon';
8
+
9
+ interface OnboardingWelcomeProps {
10
+ onboardingItems: WorkItem[];
11
+ onStartChore: (id: number, title: string) => void;
12
+ }
13
+
14
+ const REVEAL_EASE = [0.22, 1, 0.36, 1] as const;
15
+
16
+ export function OnboardingWelcome({ onboardingItems, onStartChore }: OnboardingWelcomeProps) {
17
+ const prefersReducedMotion = useReducedMotion();
18
+ const [phase, setPhase] = useState<'welcome' | 'transitioning' | 'revealed'>('welcome');
19
+
20
+ const handleLetsGo = useCallback(() => {
21
+ setPhase('transitioning');
22
+ }, []);
23
+
24
+ const handleExitComplete = useCallback(() => {
25
+ setPhase('revealed');
26
+ }, []);
27
+
28
+ const handleStartChore = useCallback((item: WorkItem) => {
29
+ onStartChore(item.id, item.title);
30
+ }, [onStartChore]);
31
+
32
+ const hasItems = onboardingItems.length > 0;
33
+ const isRevealed = phase === 'revealed';
34
+ const dur = prefersReducedMotion ? 0.05 : undefined;
35
+
36
+ return (
37
+ <div
38
+ className={`h-full flex justify-center overflow-hidden relative ${isRevealed ? 'items-start pt-16' : 'items-center pt-0'}`}
39
+ style={{ transition: prefersReducedMotion ? 'none' : 'padding-top 0.8s cubic-bezier(0.22, 1, 0.36, 1)' }}
40
+ >
41
+ <div className="flex items-start gap-12">
42
+ {/* Primary column — starts as single centered column, narrows to left column on reveal */}
43
+ <m.div
44
+ className="shrink-0 flex flex-col"
45
+ animate={{ width: isRevealed ? 340 : 480 }}
46
+ initial={false}
47
+ transition={{ duration: dur ?? 0.8, ease: REVEAL_EASE }}
48
+ >
49
+ {/* Greeting + intro — fade out on "Let's go" */}
50
+ <AnimatePresence onExitComplete={handleExitComplete}>
51
+ {phase === 'welcome' && (
52
+ <>
53
+ <m.div
54
+ key="greeting"
55
+ className="text-base leading-relaxed mb-4"
56
+ initial={{ opacity: 0, y: 10 }}
57
+ animate={{ opacity: 1, y: 0 }}
58
+ exit={{ opacity: 0, y: -5, transition: { duration: dur ?? 0.25 } }}
59
+ transition={{ duration: dur ?? 0.4, delay: prefersReducedMotion ? 0 : 0.2 }}
60
+ >
61
+ Ahoy. Welcome to your new project.
62
+ </m.div>
63
+
64
+ <m.div
65
+ key="intro"
66
+ className="text-base leading-relaxed mb-4"
67
+ initial={{ opacity: 0, y: 10 }}
68
+ animate={{ opacity: 1, y: 0 }}
69
+ exit={{ opacity: 0, y: -5, transition: { duration: dur ?? 0.25 } }}
70
+ transition={{ duration: dur ?? 0.4, delay: prefersReducedMotion ? 0 : 0.6 }}
71
+ >
72
+ Here&rsquo;s what we&rsquo;ll work through:
73
+ </m.div>
74
+ </>
75
+ )}
76
+ </AnimatePresence>
77
+
78
+ {/* Epic label — appears above cards on reveal */}
79
+ <AnimatePresence>
80
+ {isRevealed && (
81
+ <m.div
82
+ key="epic-label"
83
+ className="flex items-center gap-1.5 text-xs font-semibold text-muted-foreground mb-2.5 uppercase tracking-wide"
84
+ initial={{ opacity: 0 }}
85
+ animate={{ opacity: 1 }}
86
+ transition={{ duration: dur ?? 0.3, delay: prefersReducedMotion ? 0 : 0.3 }}
87
+ >
88
+ <TypeIcon type="epic" className="w-4 h-4 inline" />
89
+ <span>Project Planning</span>
90
+ </m.div>
91
+ )}
92
+ </AnimatePresence>
93
+
94
+ {/* Cards — SAME DOM elements throughout, never unmount */}
95
+ {onboardingItems.map((item, i) => {
96
+ const isFirst = i === 0;
97
+ return (
98
+ <m.div
99
+ key={item.id}
100
+ className="bg-white dark:bg-zinc-900 border-2 border-zinc-200 dark:border-zinc-700 rounded-xl px-3.5 py-3 mb-2 flex items-center justify-between"
101
+ style={{
102
+ boxShadow: isRevealed && isFirst
103
+ ? `${shadow.sm}, 0 0 0 3px rgba(129,157,159,0.35)`
104
+ : shadow.sm,
105
+ animation: isRevealed && isFirst ? 'onboarding-pulse-glow 2s ease-in-out infinite' : undefined,
106
+ }}
107
+ initial={{ opacity: 0, y: 10 }}
108
+ animate={{ opacity: 1, y: 0 }}
109
+ transition={{
110
+ duration: dur ?? 0.4,
111
+ delay: prefersReducedMotion ? 0 : 0.8 + i * 0.1,
112
+ }}
113
+ >
114
+ <div className="flex items-center gap-2.5">
115
+ <TypeIcon type="chore" className="w-5 h-5 inline" />
116
+ <span className="text-sm font-medium leading-snug">{item.title}</span>
117
+ </div>
118
+
119
+ {/* Start button — grows in after cards settle */}
120
+ {isRevealed && (
121
+ <m.div
122
+ initial={{ opacity: 0, scale: 0.9 }}
123
+ animate={{ opacity: 1, scale: 1 }}
124
+ transition={{ duration: dur ?? 0.3, delay: prefersReducedMotion ? 0 : 0.5 }}
125
+ >
126
+ {isFirst ? (
127
+ <button
128
+ onClick={() => handleStartChore(item)}
129
+ className="px-3 py-1 text-[11px] font-semibold rounded-sm border-2 border-zinc-200 dark:border-zinc-700 bg-white dark:bg-zinc-900 text-muted-foreground cursor-pointer whitespace-nowrap"
130
+ style={{ animation: 'onboarding-pulse-bg 2s ease-in-out infinite' }}
131
+ >
132
+ start
133
+ </button>
134
+ ) : (
135
+ <button
136
+ disabled
137
+ className="px-3 py-1 text-[11px] font-semibold rounded-sm border-2 border-zinc-200 dark:border-zinc-700 bg-white dark:bg-zinc-900 text-muted-foreground opacity-40 cursor-default whitespace-nowrap"
138
+ >
139
+ start
140
+ </button>
141
+ )}
142
+ </m.div>
143
+ )}
144
+ </m.div>
145
+ );
146
+ })}
147
+
148
+ {/* Let's go button — fades out with greeting */}
149
+ <AnimatePresence>
150
+ {phase === 'welcome' && (
151
+ <m.div
152
+ key="button"
153
+ className="flex gap-2 mt-2"
154
+ initial={{ opacity: 0 }}
155
+ animate={{ opacity: 1 }}
156
+ exit={{ opacity: 0, transition: { duration: dur ?? 0.15 } }}
157
+ transition={{ duration: dur ?? 0.4, delay: prefersReducedMotion ? 0 : 1.0 + onboardingItems.length * 0.1 }}
158
+ >
159
+ <Button onClick={handleLetsGo} size="sm">
160
+ Let&rsquo;s go
161
+ </Button>
162
+ </m.div>
163
+ )}
164
+ </AnimatePresence>
165
+ </m.div>
166
+
167
+ {/* Conversation column — expands in from right on reveal */}
168
+ {isRevealed && (
169
+ <m.div
170
+ className="shrink-0 overflow-hidden"
171
+ initial={{ width: 0 }}
172
+ animate={{ width: 480 }}
173
+ transition={{ duration: dur ?? 0.8, ease: REVEAL_EASE }}
174
+ >
175
+ <div className="w-[480px] flex flex-col gap-4">
176
+ <m.div
177
+ className="text-base leading-relaxed"
178
+ initial={{ opacity: 0, x: 20 }}
179
+ animate={{ opacity: 1, x: 0 }}
180
+ transition={{ duration: dur ?? 0.5, delay: prefersReducedMotion ? 0 : 0.3 }}
181
+ >
182
+ Each card is a short conversation &mdash; we&rsquo;ll knock them out one at a time.
183
+ </m.div>
184
+
185
+ {hasItems && (
186
+ <m.div
187
+ className="text-sm leading-normal px-3.5 py-2.5 bg-yellow-50 border-2 border-yellow-200 rounded-lg"
188
+ initial={{ opacity: 0, y: 8 }}
189
+ animate={{ opacity: 1, y: 0 }}
190
+ transition={{ duration: dur ?? 0.4, delay: prefersReducedMotion ? 0 : 0.6 }}
191
+ >
192
+ 👈 Click <strong>start</strong> on the first card to begin.
193
+ </m.div>
194
+ )}
195
+ </div>
196
+ </m.div>
197
+ )}
198
+ </div>
199
+
200
+ {/* Keyframe animations for the highlighted card */}
201
+ <style>{`
202
+ @keyframes onboarding-pulse-glow {
203
+ 0%, 100% { box-shadow: ${shadow.sm}, 0 0 0 0px rgba(129,157,159,0); }
204
+ 50% { box-shadow: ${shadow.sm}, 0 0 0 3px rgba(129,157,159,0.35); }
205
+ }
206
+ @keyframes onboarding-pulse-bg {
207
+ 0%, 100% { background: #ffffff; }
208
+ 50% { background: rgba(129,157,159,0.12); border-color: rgba(129,157,159,0.4); color: #3d4d4e; }
209
+ }
210
+ `}</style>
211
+ </div>
212
+ );
213
+ }
@@ -1,10 +1,9 @@
1
- 'use client';
2
1
 
3
- import { motion } from 'framer-motion';
2
+ import { m } from 'framer-motion';
4
3
 
5
4
  export function PlaceholderCard() {
6
5
  return (
7
- <motion.div
6
+ <m.div
8
7
  data-testid="drag-placeholder"
9
8
  initial={{ opacity: 0, scaleX: 0.3 }}
10
9
  animate={{ opacity: 1, scaleX: 1 }}
@@ -13,7 +12,7 @@ export function PlaceholderCard() {
13
12
  style={{
14
13
  height: 3,
15
14
  borderRadius: 2,
16
- background: 'linear-gradient(90deg, transparent, rgb(99, 102, 241), transparent)',
15
+ background: 'linear-gradient(90deg, transparent, #819D9F, transparent)',
17
16
  margin: '2px 8px',
18
17
  }}
19
18
  />
@@ -1,8 +1,8 @@
1
- 'use client';
2
1
 
3
2
  import { useState, useRef, useEffect } from 'react';
4
3
  import { createPortal } from 'react-dom';
5
- import type { RecentProject } from '@/lib/db-bridge';
4
+ import { isTauri, project } from '@/lib/tauri-bridge';
5
+ import type { RecentProject } from '@/lib/tauri-bridge';
6
6
 
7
7
  interface ProjectSwitcherProps {
8
8
  projectName: string;
@@ -13,21 +13,21 @@ export function ProjectSwitcher({ projectName }: ProjectSwitcherProps) {
13
13
  const [recentProjects, setRecentProjects] = useState<RecentProject[]>([]);
14
14
  const [dropdownPosition, setDropdownPosition] = useState<{ top: number; left: number } | null>(null);
15
15
  const [error, setError] = useState<string | null>(null);
16
- const [isElectron, setIsElectron] = useState(false);
16
+ const [isTauriApp, setIsTauriApp] = useState(false);
17
17
  const buttonRef = useRef<HTMLButtonElement>(null);
18
18
  const dropdownRef = useRef<HTMLDivElement>(null);
19
19
 
20
- // Detect Electron after mount to avoid hydration mismatch
20
+ // Detect Tauri after mount to avoid hydration mismatch
21
21
  useEffect(() => {
22
- setIsElectron(!!window.electronAPI?.isElectron);
22
+ setIsTauriApp(isTauri());
23
23
  }, []);
24
24
 
25
25
  // Load recent projects when dropdown opens
26
26
  useEffect(() => {
27
- if (isOpen && isElectron) {
28
- window.electronAPI!.project.getRecent().then(setRecentProjects);
27
+ if (isOpen && isTauriApp) {
28
+ project.getRecent().then(setRecentProjects);
29
29
  }
30
- }, [isOpen, isElectron]);
30
+ }, [isOpen, isTauriApp]);
31
31
 
32
32
  // Calculate dropdown position when opening
33
33
  useEffect(() => {
@@ -57,18 +57,18 @@ export function ProjectSwitcher({ projectName }: ProjectSwitcherProps) {
57
57
  }
58
58
  }, [isOpen]);
59
59
 
60
- const handleProjectClick = async (project: RecentProject) => {
60
+ const handleProjectClick = async (p: RecentProject) => {
61
61
  setError(null);
62
62
  setIsOpen(false);
63
63
  try {
64
- const result = await window.electronAPI!.project.openRecent(project.path);
64
+ const result = await project.openRecent(p.path);
65
65
  if (result.success) {
66
66
  window.location.reload();
67
67
  } else {
68
- setError(`Failed to switch to "${project.name}". The project may no longer exist.`);
68
+ setError(`Failed to switch to "${p.name}". The project may no longer exist.`);
69
69
  }
70
70
  } catch {
71
- setError(`Failed to switch to "${project.name}". The project may no longer exist.`);
71
+ setError(`Failed to switch to "${p.name}". The project may no longer exist.`);
72
72
  }
73
73
  };
74
74
 
@@ -76,7 +76,7 @@ export function ProjectSwitcher({ projectName }: ProjectSwitcherProps) {
76
76
  setError(null);
77
77
  setIsOpen(false);
78
78
  try {
79
- const result = await window.electronAPI!.project.newProject();
79
+ const result = await project.newProject();
80
80
  if (result.success) {
81
81
  window.location.reload();
82
82
  }
@@ -89,7 +89,7 @@ export function ProjectSwitcher({ projectName }: ProjectSwitcherProps) {
89
89
  setError(null);
90
90
  setIsOpen(false);
91
91
  try {
92
- const result = await window.electronAPI!.project.openDialog();
92
+ const result = await project.openDialog();
93
93
  if (result.success) {
94
94
  window.location.reload();
95
95
  }
@@ -99,10 +99,10 @@ export function ProjectSwitcher({ projectName }: ProjectSwitcherProps) {
99
99
  }
100
100
  };
101
101
 
102
- // Non-Electron: render static pill (no switching possible)
103
- if (!isElectron) {
102
+ // Non-Tauri: render static pill (no switching possible)
103
+ if (!isTauriApp) {
104
104
  return (
105
- <span className="px-2.5 py-1 text-sm bg-zinc-100 text-zinc-600 rounded-full border border-zinc-200 dark:bg-zinc-800 dark:text-zinc-400 dark:border-zinc-700">
105
+ <span className="px-5 py-1.5 text-base bg-zinc-100 text-zinc-600 rounded-full border-2 border-zinc-200 dark:bg-zinc-800 dark:text-zinc-400 dark:border-zinc-700">
106
106
  {projectName}
107
107
  </span>
108
108
  );
@@ -111,43 +111,43 @@ export function ProjectSwitcher({ projectName }: ProjectSwitcherProps) {
111
111
  const dropdownContent = (
112
112
  <div
113
113
  ref={dropdownRef}
114
- className="fixed z-50 bg-white dark:bg-zinc-800 rounded-lg shadow-lg border border-zinc-200 dark:border-zinc-700 py-1 min-w-[200px] max-w-[320px]"
114
+ className="fixed z-50 bg-white dark:bg-zinc-800 rounded-lg shadow-lg py-1.5 min-w-[200px] max-w-[320px]"
115
115
  style={{
116
116
  top: dropdownPosition?.top ?? 0,
117
117
  left: dropdownPosition?.left ?? 0,
118
118
  }}
119
119
  >
120
120
  {/* Current project */}
121
- <div className="px-3 py-2 text-sm text-zinc-400 dark:text-zinc-500 flex items-center gap-2">
121
+ <div className="px-4 py-3 text-base text-zinc-400 dark:text-zinc-500 flex items-center gap-3">
122
122
  <span className="w-1.5 h-1.5 rounded-full bg-green-500 flex-shrink-0" />
123
123
  <span className="truncate">{projectName}</span>
124
124
  </div>
125
125
 
126
126
  {/* Other recent projects */}
127
127
  {recentProjects.filter(p => p.name !== projectName).length > 0 && (
128
- <div className="px-3 pt-2 pb-1 text-xs font-medium text-zinc-400 dark:text-zinc-500 uppercase tracking-wider">Recent</div>
128
+ <div className="px-4 pt-3 pb-1.5 text-base font-medium text-zinc-400 dark:text-zinc-500 uppercase tracking-wider">Recent</div>
129
129
  )}
130
130
  {recentProjects
131
131
  .filter(p => p.name !== projectName)
132
132
  .slice(0, 4)
133
- .map((project) => (
133
+ .map((p) => (
134
134
  <button
135
- key={project.path}
136
- onClick={() => handleProjectClick(project)}
137
- className="w-full px-3 py-2 text-left text-sm text-zinc-700 dark:text-zinc-300 hover:bg-zinc-100 dark:hover:bg-zinc-700 flex items-center gap-2"
135
+ key={p.path}
136
+ onClick={() => handleProjectClick(p)}
137
+ className="w-full px-4 py-3 text-left text-base text-zinc-700 dark:text-zinc-300 hover:bg-zinc-100 dark:hover:bg-zinc-700 flex items-center gap-3"
138
138
  >
139
139
  <span className="w-1.5 h-1.5 flex-shrink-0" />
140
- <span className="truncate">{project.name}</span>
140
+ <span className="truncate">{p.name}</span>
141
141
  </button>
142
142
  ))}
143
143
 
144
144
  {/* Divider */}
145
- <div className="border-t border-zinc-200 dark:border-zinc-700 my-1" />
145
+ <div className="border-t border-zinc-200 dark:border-zinc-700 my-1.5" />
146
146
 
147
147
  {/* New Project action */}
148
148
  <button
149
149
  onClick={handleNewProject}
150
- className="w-full px-3 py-2 text-left text-sm text-zinc-700 dark:text-zinc-300 hover:bg-zinc-100 dark:hover:bg-zinc-700 flex items-center gap-2"
150
+ className="w-full px-4 py-3 text-left text-base text-zinc-700 dark:text-zinc-300 hover:bg-zinc-100 dark:hover:bg-zinc-700 flex items-center gap-3"
151
151
  >
152
152
  <svg className="w-3.5 h-3.5 text-zinc-400 flex-shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
153
153
  <path strokeLinecap="round" strokeLinejoin="round" d="M12 4v16m8-8H4" />
@@ -158,7 +158,7 @@ export function ProjectSwitcher({ projectName }: ProjectSwitcherProps) {
158
158
  {/* Open Project action */}
159
159
  <button
160
160
  onClick={handleOpenProject}
161
- className="w-full px-3 py-2 text-left text-sm text-zinc-700 dark:text-zinc-300 hover:bg-zinc-100 dark:hover:bg-zinc-700 flex items-center gap-2"
161
+ className="w-full px-4 py-3 text-left text-base text-zinc-700 dark:text-zinc-300 hover:bg-zinc-100 dark:hover:bg-zinc-700 flex items-center gap-3"
162
162
  >
163
163
  <svg className="w-3.5 h-3.5 text-zinc-400 flex-shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
164
164
  <path strokeLinecap="round" strokeLinejoin="round" d="M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-6l-2-2H5a2 2 0 00-2 2z" />
@@ -173,7 +173,7 @@ export function ProjectSwitcher({ projectName }: ProjectSwitcherProps) {
173
173
  <button
174
174
  ref={buttonRef}
175
175
  onClick={() => { setError(null); setIsOpen(!isOpen); }}
176
- className="px-2.5 py-1 text-sm bg-zinc-100 text-zinc-600 rounded-full border border-zinc-200 hover:bg-zinc-200 dark:bg-zinc-800 dark:text-zinc-400 dark:border-zinc-700 dark:hover:bg-zinc-700 transition-colors cursor-pointer flex items-center gap-1"
176
+ className="px-5 py-1.5 text-base bg-zinc-100 text-zinc-600 rounded-full border-2 border-zinc-200 hover:bg-zinc-200 dark:bg-zinc-800 dark:text-zinc-400 dark:border-zinc-700 dark:hover:bg-zinc-700 transition-colors duration-200 ease-out cursor-pointer flex items-center gap-1.5"
177
177
  >
178
178
  {projectName}
179
179
  <svg className="w-3 h-3" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
@@ -182,7 +182,7 @@ export function ProjectSwitcher({ projectName }: ProjectSwitcherProps) {
182
182
  </button>
183
183
 
184
184
  {error && (
185
- <div className="absolute top-full left-0 mt-1 px-2.5 py-1.5 text-xs text-red-700 bg-red-50 border border-red-200 rounded-lg dark:text-red-400 dark:bg-red-900/20 dark:border-red-800 whitespace-nowrap">
185
+ <div className="absolute top-full left-0 mt-1 px-3 py-2 text-xs text-red-700 bg-red-50 border-2 border-red-200 rounded-lg dark:text-red-400 dark:bg-red-900/20 dark:border-red-800 whitespace-nowrap">
186
186
  {error}
187
187
  </div>
188
188
  )}
@@ -1,7 +1,8 @@
1
- 'use client';
2
1
 
3
2
  import { useState, useEffect } from 'react';
4
- import type { PrototypeDashboardData, Prototype } from '@/lib/prototypes';
3
+ import type { PrototypeDashboardData, Prototype } from '@/lib/db';
4
+ import { shell } from '@/lib/tauri-bridge';
5
+ import { invoke } from '@/lib/tauri';
5
6
 
6
7
  interface PrototypeCardProps {
7
8
  prototype: Prototype;
@@ -19,30 +20,30 @@ function PrototypeCard({ prototype, isSelected, onSelect, compact }: PrototypeCa
19
20
  return (
20
21
  <div
21
22
  onClick={onSelect}
22
- className={`flex bg-white border rounded-lg overflow-hidden cursor-pointer transition-colors ${
23
- isSelected ? 'border-blue-500 bg-blue-50' : 'border-zinc-200 hover:border-zinc-400'
23
+ className={`flex bg-white rounded-lg overflow-hidden cursor-pointer transition-colors duration-200 ease-out ${
24
+ isSelected ? 'border-2 border-[#819D9F] bg-[#e8f0f0]' : ''
24
25
  }`}
25
26
  >
26
27
  {/* Date column */}
27
- <div className="w-12 shrink-0 bg-zinc-100 flex flex-col items-center justify-center py-2 px-1 border-r border-zinc-200">
28
+ <div className="w-12 shrink-0 bg-zinc-100 flex flex-col items-center justify-center py-3 px-1.5 border-r border-zinc-200">
28
29
  <span className="text-base font-semibold text-zinc-900 leading-none">{day}</span>
29
30
  <span className="text-[10px] text-zinc-500 uppercase leading-none mt-0.5">{weekday}</span>
30
31
  </div>
31
32
 
32
33
  {/* Content */}
33
- <div className="flex-1 py-2 px-3">
34
- <div className="flex items-center gap-2">
35
- <span className="font-medium text-sm text-zinc-900 truncate">{prototype.title}</span>
34
+ <div className="flex-1 py-3 px-4">
35
+ <div className="flex items-center gap-3">
36
+ <span className="font-medium text-base text-zinc-900 truncate">{prototype.title}</span>
36
37
  {prototype.files.length > 1 && (
37
- <span className="text-[10px] px-1.5 py-0.5 bg-green-100 text-green-700 rounded-full whitespace-nowrap">
38
+ <span className="text-[10px] px-2 py-1 bg-green-100 text-green-700 rounded-full whitespace-nowrap">
38
39
  {prototype.files.length} opts
39
40
  </span>
40
41
  )}
41
42
  </div>
42
43
  {!compact && (
43
- <div className="flex items-center gap-2 text-xs">
44
+ <div className="flex items-center gap-3 text-base">
44
45
  {prototype.feature ? (
45
- <span className="text-blue-600">#{prototype.feature.id}</span>
46
+ <span className="text-[#5a7d7f]">#{prototype.feature.id}</span>
46
47
  ) : (
47
48
  <span className="text-zinc-500">Research</span>
48
49
  )}
@@ -66,7 +67,7 @@ function MonthGroup({ month, prototypes, selectedId, onSelect, compact }: MonthG
66
67
  return (
67
68
  <div className="mb-4">
68
69
  {/* Month header */}
69
- <div className="flex items-center gap-2 mb-2">
70
+ <div className="flex items-center gap-3 mb-3">
70
71
  <span className="text-[10px] font-semibold text-zinc-500 uppercase tracking-wider">
71
72
  {month}
72
73
  </span>
@@ -74,7 +75,7 @@ function MonthGroup({ month, prototypes, selectedId, onSelect, compact }: MonthG
74
75
  </div>
75
76
 
76
77
  {/* Prototype cards */}
77
- <div className="space-y-1.5">
78
+ <div className="space-y-2">
78
79
  {prototypes.map(prototype => (
79
80
  <PrototypeCard
80
81
  key={prototype.id}
@@ -89,15 +90,6 @@ function MonthGroup({ month, prototypes, selectedId, onSelect, compact }: MonthG
89
90
  );
90
91
  }
91
92
 
92
- // Convert absolute file path to API URL
93
- // e.g., /Users/.../prototypes/feature-123/file.html -> /api/prototypes/feature-123/file.html
94
- function filePathToApiUrl(filePath: string): string {
95
- const prototypesIndex = filePath.indexOf('/prototypes/');
96
- if (prototypesIndex === -1) return filePath;
97
- const relativePath = filePath.slice(prototypesIndex + '/prototypes/'.length);
98
- return `/api/prototypes/${relativePath}`;
99
- }
100
-
101
93
  interface PreviewPanelProps {
102
94
  prototype: Prototype;
103
95
  onClose: () => void;
@@ -107,41 +99,52 @@ function PreviewPanel({ prototype, onClose }: PreviewPanelProps) {
107
99
  // Find first previewable file (HTML preferred)
108
100
  const getDefaultFile = () => prototype.files.find(f => f.type === 'html') || prototype.files[0];
109
101
  const [selectedFile, setSelectedFile] = useState(getDefaultFile);
102
+ const [fileContent, setFileContent] = useState<string | null>(null);
103
+ const [loadError, setLoadError] = useState<string | null>(null);
110
104
 
111
105
  // Reset selected file when prototype changes
112
106
  useEffect(() => {
113
107
  setSelectedFile(getDefaultFile());
114
108
  }, [prototype.id]);
115
109
 
110
+ // Load file content via Tauri IPC
111
+ useEffect(() => {
112
+ if (!selectedFile) return;
113
+ setFileContent(null);
114
+ setLoadError(null);
115
+ invoke<string>('read_prototype_file', { path: selectedFile.path })
116
+ .then(setFileContent)
117
+ .catch((err) => setLoadError(String(err)));
118
+ }, [selectedFile?.path]);
119
+
116
120
  // Check if file is previewable in iframe
117
121
  const previewableTypes = ['html', 'htm', 'txt', 'md', 'json', 'js', 'ts', 'css'];
118
122
  const isPreviewable = selectedFile && previewableTypes.includes(selectedFile.type);
119
- const previewUrl = selectedFile ? filePathToApiUrl(selectedFile.path) : null;
120
123
 
121
124
  const handleOpen = () => {
122
- if (window.electronAPI?.shell?.openPath && selectedFile) {
123
- window.electronAPI.shell.openPath(selectedFile.path);
125
+ if (selectedFile) {
126
+ shell.openPath(selectedFile.path);
124
127
  }
125
128
  };
126
129
 
127
130
  return (
128
- <div className="flex flex-col h-full bg-white border border-zinc-200 rounded-lg overflow-hidden">
131
+ <div className="flex flex-col h-full bg-white rounded-lg overflow-hidden">
129
132
  {/* Header */}
130
- <div className="flex items-center justify-between p-4 border-b border-zinc-200">
133
+ <div className="flex items-center justify-between p-6 border-b border-zinc-200">
131
134
  <div>
132
135
  <h3 className="font-semibold text-zinc-900">{prototype.title}</h3>
133
- <p className="text-sm text-zinc-500">{selectedFile?.name || prototype.description}</p>
136
+ <p className="text-base text-zinc-500">{selectedFile?.name || prototype.description}</p>
134
137
  </div>
135
138
  <div className="flex items-center gap-2">
136
139
  <button
137
140
  onClick={handleOpen}
138
- className="px-3 py-1.5 text-xs bg-zinc-100 border border-zinc-300 rounded text-zinc-700 hover:border-blue-500 hover:text-blue-600 transition-colors"
141
+ className="px-4 py-2 text-base bg-zinc-100 border border-zinc-300 rounded text-zinc-700 hover:border-[#819D9F] hover:text-[#5a7d7f] transition-colors duration-200 ease-out"
139
142
  >
140
143
  Open
141
144
  </button>
142
145
  <button
143
146
  onClick={onClose}
144
- className="p-2 text-zinc-400 hover:text-zinc-700 transition-colors"
147
+ className="p-2 text-zinc-400 hover:text-zinc-700 transition-colors duration-200 ease-out"
145
148
  >
146
149
  <svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
147
150
  <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
@@ -152,16 +155,16 @@ function PreviewPanel({ prototype, onClose }: PreviewPanelProps) {
152
155
 
153
156
  {/* File selector */}
154
157
  {prototype.files.length > 1 && (
155
- <div className="px-4 py-2 border-b border-zinc-200">
156
- <div className="flex flex-wrap gap-2">
158
+ <div className="px-5 py-3 border-b border-zinc-200">
159
+ <div className="flex flex-wrap gap-3">
157
160
  {prototype.files.map(file => (
158
161
  <button
159
162
  key={file.path}
160
163
  onClick={() => setSelectedFile(file)}
161
- className={`px-2 py-1 text-xs rounded transition-colors ${
164
+ className={`px-3 py-1.5 text-base rounded transition-colors duration-200 ease-out ${
162
165
  selectedFile?.path === file.path
163
- ? 'bg-blue-600 text-white'
164
- : 'bg-zinc-100 border border-zinc-300 text-zinc-700 hover:border-blue-500'
166
+ ? 'bg-[#819D9F] text-white'
167
+ : 'bg-zinc-100 border border-zinc-300 text-zinc-700 hover:border-[#819D9F]'
165
168
  }`}
166
169
  >
167
170
  {file.name}
@@ -173,25 +176,43 @@ function PreviewPanel({ prototype, onClose }: PreviewPanelProps) {
173
176
 
174
177
  {/* Preview content - 75% zoom using transform scale */}
175
178
  <div className="flex-1 overflow-hidden">
176
- {previewUrl && isPreviewable ? (
179
+ {loadError ? (
180
+ <div className="flex items-center justify-center h-full text-red-500">
181
+ <div className="text-center">
182
+ <p className="mb-3">Failed to load preview</p>
183
+ <p className="text-base">{loadError}</p>
184
+ </div>
185
+ </div>
186
+ ) : fileContent && isPreviewable ? (
177
187
  <div className="w-full h-full overflow-hidden">
178
- <iframe
179
- key={previewUrl}
180
- src={previewUrl}
181
- className="border-0 bg-white origin-top-left"
182
- style={{
183
- width: '133.33%',
184
- height: '133.33%',
185
- transform: 'scale(0.75)',
186
- }}
187
- title={`Preview: ${selectedFile?.name}`}
188
- />
188
+ {selectedFile && ['html', 'htm'].includes(selectedFile.type) ? (
189
+ <iframe
190
+ key={selectedFile.path}
191
+ srcDoc={fileContent}
192
+ className="border-0 bg-white origin-top-left"
193
+ style={{
194
+ width: '133.33%',
195
+ height: '133.33%',
196
+ transform: 'scale(0.75)',
197
+ }}
198
+ sandbox="allow-scripts allow-same-origin"
199
+ title={`Preview: ${selectedFile.name}`}
200
+ />
201
+ ) : (
202
+ <pre className="p-6 text-sm font-mono text-zinc-700 overflow-auto h-full whitespace-pre-wrap">
203
+ {fileContent}
204
+ </pre>
205
+ )}
206
+ </div>
207
+ ) : fileContent === null && !loadError ? (
208
+ <div className="flex items-center justify-center h-full text-zinc-400">
209
+ Loading...
189
210
  </div>
190
211
  ) : (
191
212
  <div className="flex items-center justify-center h-full text-zinc-500">
192
213
  <div className="text-center">
193
- <p className="mb-2">Cannot preview this file type</p>
194
- <p className="text-sm">{selectedFile?.name}</p>
214
+ <p className="mb-3">Cannot preview this file type</p>
215
+ <p className="text-base">{selectedFile?.name}</p>
195
216
  </div>
196
217
  </div>
197
218
  )}
@@ -233,9 +254,9 @@ export function PrototypeTimeline({ data }: PrototypeTimelineProps) {
233
254
  : null;
234
255
 
235
256
  return (
236
- <div className="flex gap-4">
257
+ <div className="flex gap-6">
237
258
  {/* Timeline list */}
238
- <div className={`${selectedPrototype ? 'w-[320px] shrink-0' : 'w-full'} transition-all`}>
259
+ <div className={`${selectedPrototype ? 'w-[320px] shrink-0' : 'w-full'} transition-[width] duration-200 ease-out`}>
239
260
  {months.map(month => (
240
261
  <MonthGroup
241
262
  key={month}