jettypod 4.4.116 → 4.4.120
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/.env +7 -0
- package/apps/dashboard/app/api/claude/[workItemId]/message/route.ts +124 -48
- package/apps/dashboard/app/api/claude/[workItemId]/route.ts +171 -58
- package/apps/dashboard/app/api/claude/sessions/[sessionId]/message/route.ts +161 -10
- package/apps/dashboard/app/api/tests/run/stream/route.ts +13 -1
- package/apps/dashboard/app/api/usage/route.ts +17 -0
- package/apps/dashboard/app/api/work/[id]/route.ts +35 -0
- package/apps/dashboard/app/api/work/[id]/status/route.ts +43 -1
- package/apps/dashboard/app/connect-claude/page.tsx +24 -0
- package/apps/dashboard/app/decision/[id]/page.tsx +14 -14
- package/apps/dashboard/app/demo/gates/page.tsx +42 -42
- package/apps/dashboard/app/design-system/page.tsx +868 -0
- package/apps/dashboard/app/globals.css +6 -2
- package/apps/dashboard/app/install-claude/page.tsx +9 -7
- package/apps/dashboard/app/layout.tsx +17 -5
- package/apps/dashboard/app/login/page.tsx +250 -0
- package/apps/dashboard/app/page.tsx +11 -9
- package/apps/dashboard/app/settings/page.tsx +4 -2
- package/apps/dashboard/app/signup/page.tsx +245 -0
- package/apps/dashboard/app/subscribe/page.tsx +11 -0
- package/apps/dashboard/app/welcome/page.tsx +24 -1
- package/apps/dashboard/app/work/[id]/page.tsx +34 -50
- package/apps/dashboard/components/AppShell.tsx +95 -55
- package/apps/dashboard/components/CardMenu.tsx +56 -13
- package/apps/dashboard/components/ClaudePanel.tsx +301 -582
- package/apps/dashboard/components/ClaudePanelInput.tsx +23 -14
- package/apps/dashboard/components/ConnectClaudeScreen.tsx +210 -0
- package/apps/dashboard/components/CopyableId.tsx +3 -3
- package/apps/dashboard/components/DetailReviewActions.tsx +109 -0
- package/apps/dashboard/components/DragContext.tsx +75 -65
- package/apps/dashboard/components/DraggableCard.tsx +6 -46
- package/apps/dashboard/components/DropZone.tsx +2 -2
- package/apps/dashboard/components/EditableDetailDescription.tsx +1 -1
- package/apps/dashboard/components/EditableTitle.tsx +26 -6
- package/apps/dashboard/components/ElapsedTimer.tsx +54 -0
- package/apps/dashboard/components/EpicGroup.tsx +329 -0
- package/apps/dashboard/components/GateCard.tsx +100 -16
- package/apps/dashboard/components/GateChoiceCard.tsx +15 -17
- package/apps/dashboard/components/InstallClaudeScreen.tsx +140 -51
- package/apps/dashboard/components/JettyLoader.tsx +38 -0
- package/apps/dashboard/components/KanbanBoard.tsx +147 -766
- package/apps/dashboard/components/KanbanCard.tsx +506 -0
- package/apps/dashboard/components/LazyMarkdown.tsx +12 -0
- package/apps/dashboard/components/MainNav.tsx +20 -54
- package/apps/dashboard/components/MessageBlock.tsx +391 -0
- package/apps/dashboard/components/ModeStartCard.tsx +15 -15
- package/apps/dashboard/components/OnboardingWelcome.tsx +214 -0
- package/apps/dashboard/components/PlaceholderCard.tsx +11 -21
- package/apps/dashboard/components/ProjectSwitcher.tsx +36 -8
- package/apps/dashboard/components/PrototypeTimeline.tsx +25 -25
- package/apps/dashboard/components/RealTimeKanbanWrapper.tsx +265 -301
- package/apps/dashboard/components/RealTimeTestsWrapper.tsx +97 -74
- package/apps/dashboard/components/ReviewFooter.tsx +141 -0
- package/apps/dashboard/components/SessionList.tsx +19 -18
- package/apps/dashboard/components/SubscribeContent.tsx +206 -0
- package/apps/dashboard/components/TestTree.tsx +15 -14
- package/apps/dashboard/components/TipCard.tsx +177 -0
- package/apps/dashboard/components/Toast.tsx +5 -5
- package/apps/dashboard/components/TypeIcon.tsx +56 -0
- package/apps/dashboard/components/UpgradeBanner.tsx +30 -0
- package/apps/dashboard/components/WaveCompletionAnimation.tsx +61 -62
- package/apps/dashboard/components/WelcomeScreen.tsx +25 -27
- package/apps/dashboard/components/WorkItemHeader.tsx +4 -4
- package/apps/dashboard/components/WorkItemTree.tsx +9 -28
- package/apps/dashboard/components/settings/AccountSection.tsx +169 -0
- package/apps/dashboard/components/settings/EnvVarsSection.tsx +54 -79
- package/apps/dashboard/components/settings/GeneralSection.tsx +26 -31
- package/apps/dashboard/components/settings/SettingsLayout.tsx +4 -4
- package/apps/dashboard/components/ui/Button.tsx +104 -0
- package/apps/dashboard/components/ui/Input.tsx +78 -0
- package/apps/dashboard/contexts/ClaudeSessionContext.tsx +408 -105
- package/apps/dashboard/contexts/ConnectionStatusContext.tsx +25 -4
- package/apps/dashboard/contexts/UsageContext.tsx +155 -0
- package/apps/dashboard/contexts/usageHelpers.js +9 -0
- package/apps/dashboard/electron/ipc-handlers.js +281 -88
- package/apps/dashboard/electron/main.js +691 -131
- package/apps/dashboard/electron/preload.js +25 -4
- package/apps/dashboard/electron/session-manager.js +163 -0
- package/apps/dashboard/electron-builder.config.js +3 -5
- package/apps/dashboard/hooks/useKanbanAnimation.ts +29 -0
- package/apps/dashboard/hooks/useKanbanUndo.ts +83 -0
- package/apps/dashboard/lib/backlog-parser.ts +50 -0
- package/apps/dashboard/lib/claude-process-manager.ts +50 -11
- package/apps/dashboard/lib/constants.ts +43 -0
- package/apps/dashboard/lib/db-bridge.ts +33 -0
- package/apps/dashboard/lib/db.ts +136 -20
- package/apps/dashboard/lib/kanban-utils.ts +70 -0
- package/apps/dashboard/lib/run-migrations.js +27 -2
- package/apps/dashboard/lib/session-state-machine.ts +3 -0
- package/apps/dashboard/lib/session-stream-manager.ts +144 -38
- package/apps/dashboard/lib/shadows.ts +7 -0
- package/apps/dashboard/lib/tests.ts +3 -1
- package/apps/dashboard/lib/utils.ts +6 -0
- package/apps/dashboard/next.config.js +35 -14
- package/apps/dashboard/package.json +6 -3
- package/apps/dashboard/public/bug-icon.svg +9 -0
- package/apps/dashboard/public/buoy-icon.svg +9 -0
- package/apps/dashboard/public/fonts/Satoshi-Variable.woff2 +0 -0
- package/apps/dashboard/public/fonts/Satoshi-VariableItalic.woff2 +0 -0
- package/apps/dashboard/public/in-flight-seagull.svg +9 -0
- package/apps/dashboard/public/jetty-icon-loading-alt.svg +11 -0
- package/apps/dashboard/public/jetty-icon-loading.svg +11 -0
- package/apps/dashboard/public/jettypod_logo.png +0 -0
- package/apps/dashboard/public/pier-icon.svg +14 -0
- package/apps/dashboard/public/star-icon.svg +9 -0
- package/apps/dashboard/public/wrench-icon.svg +9 -0
- package/apps/dashboard/scripts/upload-to-r2.js +89 -0
- package/apps/dashboard/scripts/ws-server.js +191 -0
- package/apps/dashboard/tsconfig.tsbuildinfo +1 -0
- package/apps/update-server/package.json +16 -0
- package/apps/update-server/schema.sql +31 -0
- package/apps/update-server/src/index.ts +1085 -0
- package/apps/update-server/tsconfig.json +16 -0
- package/apps/update-server/wrangler.toml +35 -0
- package/cucumber.js +9 -3
- package/docs/COMMAND_REFERENCE.md +34 -0
- package/hooks/post-checkout +32 -75
- package/hooks/post-merge +111 -10
- package/jest.setup.js +1 -0
- package/jettypod.js +54 -116
- package/lib/chore-taxonomy.js +33 -10
- package/lib/database.js +36 -16
- package/lib/db-watcher.js +1 -1
- package/lib/git-hooks/pre-commit +1 -1
- package/lib/jettypod-backup.js +27 -4
- package/lib/migrations/027-plan-at-creation-column.js +33 -0
- package/lib/migrations/028-ready-for-review-column.js +27 -0
- package/lib/migrations/029-remove-autoincrement.js +307 -0
- package/lib/migrations/029-rename-corrupted-to-cleaned.js +149 -0
- package/lib/migrations/index.js +47 -4
- package/lib/schema.js +13 -6
- package/lib/seed-onboarding.js +101 -69
- package/lib/update-command/index.js +9 -175
- package/lib/work-commands/index.js +129 -16
- package/lib/work-tracking/index.js +86 -46
- package/lib/worktree-diagnostics.js +16 -16
- package/lib/worktree-facade.js +1 -1
- package/lib/worktree-manager.js +8 -8
- package/lib/worktree-reconciler.js +5 -5
- package/package.json +9 -2
- package/scripts/ndjson-to-cucumber-json.js +152 -0
- package/scripts/postinstall.js +25 -0
- package/skills-templates/bug-mode/SKILL.md +39 -28
- package/skills-templates/bug-planning/SKILL.md +25 -29
- package/skills-templates/chore-mode/SKILL.md +131 -68
- package/skills-templates/chore-mode/verification.js +51 -10
- package/skills-templates/chore-planning/SKILL.md +47 -18
- package/skills-templates/epic-planning/SKILL.md +68 -48
- package/skills-templates/external-transition/SKILL.md +47 -47
- package/skills-templates/feature-planning/SKILL.md +83 -73
- package/skills-templates/production-mode/SKILL.md +49 -49
- package/skills-templates/request-routing/SKILL.md +27 -14
- package/skills-templates/simple-improvement/SKILL.md +68 -44
- package/skills-templates/speed-mode/SKILL.md +209 -128
- package/skills-templates/stable-mode/SKILL.md +105 -94
- package/templates/bdd-guidance.md +139 -0
- package/templates/bdd-scaffolding/wait.js +18 -0
- package/templates/bdd-scaffolding/world.js +19 -0
- package/.jettypod-backup/work.db +0 -0
- package/apps/dashboard/app/access-code/page.tsx +0 -110
- package/lib/discovery-checkpoint.js +0 -123
- package/skills-templates/project-discovery/SKILL.md +0 -372
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useState, useCallback } from 'react';
|
|
4
|
+
import { m, AnimatePresence, useReducedMotion } from 'framer-motion';
|
|
5
|
+
import type { WorkItem } from '@/lib/db';
|
|
6
|
+
import { shadow } from '@/lib/shadows';
|
|
7
|
+
import { Button } from '@/components/ui/Button';
|
|
8
|
+
import { TypeIcon } from '@/components/TypeIcon';
|
|
9
|
+
|
|
10
|
+
interface OnboardingWelcomeProps {
|
|
11
|
+
onboardingItems: WorkItem[];
|
|
12
|
+
onStartChore: (id: number, title: string) => void;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const REVEAL_EASE = [0.22, 1, 0.36, 1] as const;
|
|
16
|
+
|
|
17
|
+
export function OnboardingWelcome({ onboardingItems, onStartChore }: OnboardingWelcomeProps) {
|
|
18
|
+
const prefersReducedMotion = useReducedMotion();
|
|
19
|
+
const [phase, setPhase] = useState<'welcome' | 'transitioning' | 'revealed'>('welcome');
|
|
20
|
+
|
|
21
|
+
const handleLetsGo = useCallback(() => {
|
|
22
|
+
setPhase('transitioning');
|
|
23
|
+
}, []);
|
|
24
|
+
|
|
25
|
+
const handleExitComplete = useCallback(() => {
|
|
26
|
+
setPhase('revealed');
|
|
27
|
+
}, []);
|
|
28
|
+
|
|
29
|
+
const handleStartChore = useCallback((item: WorkItem) => {
|
|
30
|
+
onStartChore(item.id, item.title);
|
|
31
|
+
}, [onStartChore]);
|
|
32
|
+
|
|
33
|
+
const hasItems = onboardingItems.length > 0;
|
|
34
|
+
const isRevealed = phase === 'revealed';
|
|
35
|
+
const dur = prefersReducedMotion ? 0.05 : undefined;
|
|
36
|
+
|
|
37
|
+
return (
|
|
38
|
+
<div
|
|
39
|
+
className={`h-full flex justify-center overflow-hidden relative ${isRevealed ? 'items-start pt-16' : 'items-center pt-0'}`}
|
|
40
|
+
style={{ transition: prefersReducedMotion ? 'none' : 'padding-top 0.8s cubic-bezier(0.22, 1, 0.36, 1)' }}
|
|
41
|
+
>
|
|
42
|
+
<div className="flex items-start gap-12">
|
|
43
|
+
{/* Primary column — starts as single centered column, narrows to left column on reveal */}
|
|
44
|
+
<m.div
|
|
45
|
+
className="shrink-0 flex flex-col"
|
|
46
|
+
animate={{ width: isRevealed ? 340 : 480 }}
|
|
47
|
+
initial={false}
|
|
48
|
+
transition={{ duration: dur ?? 0.8, ease: REVEAL_EASE }}
|
|
49
|
+
>
|
|
50
|
+
{/* Greeting + intro — fade out on "Let's go" */}
|
|
51
|
+
<AnimatePresence onExitComplete={handleExitComplete}>
|
|
52
|
+
{phase === 'welcome' && (
|
|
53
|
+
<>
|
|
54
|
+
<m.div
|
|
55
|
+
key="greeting"
|
|
56
|
+
className="text-base leading-relaxed mb-4"
|
|
57
|
+
initial={{ opacity: 0, y: 10 }}
|
|
58
|
+
animate={{ opacity: 1, y: 0 }}
|
|
59
|
+
exit={{ opacity: 0, y: -5, transition: { duration: dur ?? 0.25 } }}
|
|
60
|
+
transition={{ duration: dur ?? 0.4, delay: prefersReducedMotion ? 0 : 0.2 }}
|
|
61
|
+
>
|
|
62
|
+
Ahoy. Welcome to your new project.
|
|
63
|
+
</m.div>
|
|
64
|
+
|
|
65
|
+
<m.div
|
|
66
|
+
key="intro"
|
|
67
|
+
className="text-base leading-relaxed mb-4"
|
|
68
|
+
initial={{ opacity: 0, y: 10 }}
|
|
69
|
+
animate={{ opacity: 1, y: 0 }}
|
|
70
|
+
exit={{ opacity: 0, y: -5, transition: { duration: dur ?? 0.25 } }}
|
|
71
|
+
transition={{ duration: dur ?? 0.4, delay: prefersReducedMotion ? 0 : 0.6 }}
|
|
72
|
+
>
|
|
73
|
+
Here’s what we’ll work through:
|
|
74
|
+
</m.div>
|
|
75
|
+
</>
|
|
76
|
+
)}
|
|
77
|
+
</AnimatePresence>
|
|
78
|
+
|
|
79
|
+
{/* Epic label — appears above cards on reveal */}
|
|
80
|
+
<AnimatePresence>
|
|
81
|
+
{isRevealed && (
|
|
82
|
+
<m.div
|
|
83
|
+
key="epic-label"
|
|
84
|
+
className="flex items-center gap-1.5 text-xs font-semibold text-muted-foreground mb-2.5 uppercase tracking-wide"
|
|
85
|
+
initial={{ opacity: 0 }}
|
|
86
|
+
animate={{ opacity: 1 }}
|
|
87
|
+
transition={{ duration: dur ?? 0.3, delay: prefersReducedMotion ? 0 : 0.3 }}
|
|
88
|
+
>
|
|
89
|
+
<TypeIcon type="epic" className="w-4 h-4 inline" />
|
|
90
|
+
<span>Project Planning</span>
|
|
91
|
+
</m.div>
|
|
92
|
+
)}
|
|
93
|
+
</AnimatePresence>
|
|
94
|
+
|
|
95
|
+
{/* Cards — SAME DOM elements throughout, never unmount */}
|
|
96
|
+
{onboardingItems.map((item, i) => {
|
|
97
|
+
const isFirst = i === 0;
|
|
98
|
+
return (
|
|
99
|
+
<m.div
|
|
100
|
+
key={item.id}
|
|
101
|
+
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"
|
|
102
|
+
style={{
|
|
103
|
+
boxShadow: isRevealed && isFirst
|
|
104
|
+
? `${shadow.sm}, 0 0 0 3px rgba(129,157,159,0.35)`
|
|
105
|
+
: shadow.sm,
|
|
106
|
+
animation: isRevealed && isFirst ? 'onboarding-pulse-glow 2s ease-in-out infinite' : undefined,
|
|
107
|
+
}}
|
|
108
|
+
initial={{ opacity: 0, y: 10 }}
|
|
109
|
+
animate={{ opacity: 1, y: 0 }}
|
|
110
|
+
transition={{
|
|
111
|
+
duration: dur ?? 0.4,
|
|
112
|
+
delay: prefersReducedMotion ? 0 : 0.8 + i * 0.1,
|
|
113
|
+
}}
|
|
114
|
+
>
|
|
115
|
+
<div className="flex items-center gap-2.5">
|
|
116
|
+
<TypeIcon type="chore" className="w-5 h-5 inline" />
|
|
117
|
+
<span className="text-sm font-medium leading-snug">{item.title}</span>
|
|
118
|
+
</div>
|
|
119
|
+
|
|
120
|
+
{/* Start button — grows in after cards settle */}
|
|
121
|
+
{isRevealed && (
|
|
122
|
+
<m.div
|
|
123
|
+
initial={{ opacity: 0, scale: 0.9 }}
|
|
124
|
+
animate={{ opacity: 1, scale: 1 }}
|
|
125
|
+
transition={{ duration: dur ?? 0.3, delay: prefersReducedMotion ? 0 : 0.5 }}
|
|
126
|
+
>
|
|
127
|
+
{isFirst ? (
|
|
128
|
+
<button
|
|
129
|
+
onClick={() => handleStartChore(item)}
|
|
130
|
+
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"
|
|
131
|
+
style={{ animation: 'onboarding-pulse-bg 2s ease-in-out infinite' }}
|
|
132
|
+
>
|
|
133
|
+
start
|
|
134
|
+
</button>
|
|
135
|
+
) : (
|
|
136
|
+
<button
|
|
137
|
+
disabled
|
|
138
|
+
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"
|
|
139
|
+
>
|
|
140
|
+
start
|
|
141
|
+
</button>
|
|
142
|
+
)}
|
|
143
|
+
</m.div>
|
|
144
|
+
)}
|
|
145
|
+
</m.div>
|
|
146
|
+
);
|
|
147
|
+
})}
|
|
148
|
+
|
|
149
|
+
{/* Let's go button — fades out with greeting */}
|
|
150
|
+
<AnimatePresence>
|
|
151
|
+
{phase === 'welcome' && (
|
|
152
|
+
<m.div
|
|
153
|
+
key="button"
|
|
154
|
+
className="flex gap-2 mt-2"
|
|
155
|
+
initial={{ opacity: 0 }}
|
|
156
|
+
animate={{ opacity: 1 }}
|
|
157
|
+
exit={{ opacity: 0, transition: { duration: dur ?? 0.15 } }}
|
|
158
|
+
transition={{ duration: dur ?? 0.4, delay: prefersReducedMotion ? 0 : 1.0 + onboardingItems.length * 0.1 }}
|
|
159
|
+
>
|
|
160
|
+
<Button onClick={handleLetsGo} size="sm">
|
|
161
|
+
Let’s go
|
|
162
|
+
</Button>
|
|
163
|
+
</m.div>
|
|
164
|
+
)}
|
|
165
|
+
</AnimatePresence>
|
|
166
|
+
</m.div>
|
|
167
|
+
|
|
168
|
+
{/* Conversation column — expands in from right on reveal */}
|
|
169
|
+
{isRevealed && (
|
|
170
|
+
<m.div
|
|
171
|
+
className="shrink-0 overflow-hidden"
|
|
172
|
+
initial={{ width: 0 }}
|
|
173
|
+
animate={{ width: 480 }}
|
|
174
|
+
transition={{ duration: dur ?? 0.8, ease: REVEAL_EASE }}
|
|
175
|
+
>
|
|
176
|
+
<div className="w-[480px] flex flex-col gap-4">
|
|
177
|
+
<m.div
|
|
178
|
+
className="text-base leading-relaxed"
|
|
179
|
+
initial={{ opacity: 0, x: 20 }}
|
|
180
|
+
animate={{ opacity: 1, x: 0 }}
|
|
181
|
+
transition={{ duration: dur ?? 0.5, delay: prefersReducedMotion ? 0 : 0.3 }}
|
|
182
|
+
>
|
|
183
|
+
Each card is a short conversation — we’ll knock them out one at a time.
|
|
184
|
+
</m.div>
|
|
185
|
+
|
|
186
|
+
{hasItems && (
|
|
187
|
+
<m.div
|
|
188
|
+
className="text-sm leading-normal px-3.5 py-2.5 bg-yellow-50 border-2 border-yellow-200 rounded-lg"
|
|
189
|
+
initial={{ opacity: 0, y: 8 }}
|
|
190
|
+
animate={{ opacity: 1, y: 0 }}
|
|
191
|
+
transition={{ duration: dur ?? 0.4, delay: prefersReducedMotion ? 0 : 0.6 }}
|
|
192
|
+
>
|
|
193
|
+
👈 Click <strong>start</strong> on the first card to begin.
|
|
194
|
+
</m.div>
|
|
195
|
+
)}
|
|
196
|
+
</div>
|
|
197
|
+
</m.div>
|
|
198
|
+
)}
|
|
199
|
+
</div>
|
|
200
|
+
|
|
201
|
+
{/* Keyframe animations for the highlighted card */}
|
|
202
|
+
<style>{`
|
|
203
|
+
@keyframes onboarding-pulse-glow {
|
|
204
|
+
0%, 100% { box-shadow: ${shadow.sm}, 0 0 0 0px rgba(129,157,159,0); }
|
|
205
|
+
50% { box-shadow: ${shadow.sm}, 0 0 0 3px rgba(129,157,159,0.35); }
|
|
206
|
+
}
|
|
207
|
+
@keyframes onboarding-pulse-bg {
|
|
208
|
+
0%, 100% { background: #ffffff; }
|
|
209
|
+
50% { background: rgba(129,157,159,0.12); border-color: rgba(129,157,159,0.4); color: #3d4d4e; }
|
|
210
|
+
}
|
|
211
|
+
`}</style>
|
|
212
|
+
</div>
|
|
213
|
+
);
|
|
214
|
+
}
|
|
@@ -1,30 +1,20 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
interface PlaceholderCardProps {
|
|
6
|
-
height: number;
|
|
7
|
-
minHeight?: number;
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
const DEFAULT_MIN_HEIGHT = 40;
|
|
11
|
-
|
|
12
|
-
export function PlaceholderCard({ height, minHeight = DEFAULT_MIN_HEIGHT }: PlaceholderCardProps) {
|
|
13
|
-
const effectiveHeight = Math.max(height, minHeight);
|
|
3
|
+
import { m } from 'framer-motion';
|
|
14
4
|
|
|
5
|
+
export function PlaceholderCard() {
|
|
15
6
|
return (
|
|
16
|
-
<
|
|
7
|
+
<m.div
|
|
17
8
|
data-testid="drag-placeholder"
|
|
18
|
-
initial={{ opacity: 0,
|
|
19
|
-
animate={{ opacity:
|
|
20
|
-
exit={{ opacity: 0,
|
|
21
|
-
transition={{ duration: 0.
|
|
9
|
+
initial={{ opacity: 0, scaleX: 0.3 }}
|
|
10
|
+
animate={{ opacity: 1, scaleX: 1 }}
|
|
11
|
+
exit={{ opacity: 0, scaleX: 0.3 }}
|
|
12
|
+
transition={{ duration: 0.15, ease: 'easeOut' }}
|
|
22
13
|
style={{
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
marginBottom: 8,
|
|
14
|
+
height: 3,
|
|
15
|
+
borderRadius: 2,
|
|
16
|
+
background: 'linear-gradient(90deg, transparent, #819D9F, transparent)',
|
|
17
|
+
margin: '2px 8px',
|
|
28
18
|
}}
|
|
29
19
|
/>
|
|
30
20
|
);
|
|
@@ -72,6 +72,19 @@ export function ProjectSwitcher({ projectName }: ProjectSwitcherProps) {
|
|
|
72
72
|
}
|
|
73
73
|
};
|
|
74
74
|
|
|
75
|
+
const handleNewProject = async () => {
|
|
76
|
+
setError(null);
|
|
77
|
+
setIsOpen(false);
|
|
78
|
+
try {
|
|
79
|
+
const result = await window.electronAPI!.project.newProject();
|
|
80
|
+
if (result.success) {
|
|
81
|
+
window.location.reload();
|
|
82
|
+
}
|
|
83
|
+
} catch {
|
|
84
|
+
setError('Failed to create new project.');
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
|
|
75
88
|
const handleOpenProject = async () => {
|
|
76
89
|
setError(null);
|
|
77
90
|
setIsOpen(false);
|
|
@@ -89,7 +102,7 @@ export function ProjectSwitcher({ projectName }: ProjectSwitcherProps) {
|
|
|
89
102
|
// Non-Electron: render static pill (no switching possible)
|
|
90
103
|
if (!isElectron) {
|
|
91
104
|
return (
|
|
92
|
-
<span className="px-
|
|
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">
|
|
93
106
|
{projectName}
|
|
94
107
|
</span>
|
|
95
108
|
);
|
|
@@ -98,26 +111,30 @@ export function ProjectSwitcher({ projectName }: ProjectSwitcherProps) {
|
|
|
98
111
|
const dropdownContent = (
|
|
99
112
|
<div
|
|
100
113
|
ref={dropdownRef}
|
|
101
|
-
className="fixed z-50 bg-white dark:bg-zinc-800 rounded-lg shadow-lg
|
|
114
|
+
className="fixed z-50 bg-white dark:bg-zinc-800 rounded-lg shadow-lg py-1.5 min-w-[200px] max-w-[320px]"
|
|
102
115
|
style={{
|
|
103
116
|
top: dropdownPosition?.top ?? 0,
|
|
104
117
|
left: dropdownPosition?.left ?? 0,
|
|
105
118
|
}}
|
|
106
119
|
>
|
|
107
120
|
{/* Current project */}
|
|
108
|
-
<div className="px-
|
|
121
|
+
<div className="px-4 py-3 text-base text-zinc-400 dark:text-zinc-500 flex items-center gap-3">
|
|
109
122
|
<span className="w-1.5 h-1.5 rounded-full bg-green-500 flex-shrink-0" />
|
|
110
123
|
<span className="truncate">{projectName}</span>
|
|
111
124
|
</div>
|
|
112
125
|
|
|
113
126
|
{/* Other recent projects */}
|
|
127
|
+
{recentProjects.filter(p => p.name !== projectName).length > 0 && (
|
|
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
|
+
)}
|
|
114
130
|
{recentProjects
|
|
115
131
|
.filter(p => p.name !== projectName)
|
|
132
|
+
.slice(0, 4)
|
|
116
133
|
.map((project) => (
|
|
117
134
|
<button
|
|
118
135
|
key={project.path}
|
|
119
136
|
onClick={() => handleProjectClick(project)}
|
|
120
|
-
className="w-full px-
|
|
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"
|
|
121
138
|
>
|
|
122
139
|
<span className="w-1.5 h-1.5 flex-shrink-0" />
|
|
123
140
|
<span className="truncate">{project.name}</span>
|
|
@@ -125,12 +142,23 @@ export function ProjectSwitcher({ projectName }: ProjectSwitcherProps) {
|
|
|
125
142
|
))}
|
|
126
143
|
|
|
127
144
|
{/* Divider */}
|
|
128
|
-
<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
|
+
|
|
147
|
+
{/* New Project action */}
|
|
148
|
+
<button
|
|
149
|
+
onClick={handleNewProject}
|
|
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
|
+
>
|
|
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
|
+
<path strokeLinecap="round" strokeLinejoin="round" d="M12 4v16m8-8H4" />
|
|
154
|
+
</svg>
|
|
155
|
+
<span>New Project...</span>
|
|
156
|
+
</button>
|
|
129
157
|
|
|
130
158
|
{/* Open Project action */}
|
|
131
159
|
<button
|
|
132
160
|
onClick={handleOpenProject}
|
|
133
|
-
className="w-full px-
|
|
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"
|
|
134
162
|
>
|
|
135
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}>
|
|
136
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" />
|
|
@@ -145,7 +173,7 @@ export function ProjectSwitcher({ projectName }: ProjectSwitcherProps) {
|
|
|
145
173
|
<button
|
|
146
174
|
ref={buttonRef}
|
|
147
175
|
onClick={() => { setError(null); setIsOpen(!isOpen); }}
|
|
148
|
-
className="px-
|
|
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"
|
|
149
177
|
>
|
|
150
178
|
{projectName}
|
|
151
179
|
<svg className="w-3 h-3" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
|
@@ -154,7 +182,7 @@ export function ProjectSwitcher({ projectName }: ProjectSwitcherProps) {
|
|
|
154
182
|
</button>
|
|
155
183
|
|
|
156
184
|
{error && (
|
|
157
|
-
<div className="absolute top-full left-0 mt-1 px-
|
|
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">
|
|
158
186
|
{error}
|
|
159
187
|
</div>
|
|
160
188
|
)}
|
|
@@ -19,30 +19,30 @@ function PrototypeCard({ prototype, isSelected, onSelect, compact }: PrototypeCa
|
|
|
19
19
|
return (
|
|
20
20
|
<div
|
|
21
21
|
onClick={onSelect}
|
|
22
|
-
className={`flex bg-white
|
|
23
|
-
isSelected ? 'border-
|
|
22
|
+
className={`flex bg-white rounded-lg overflow-hidden cursor-pointer transition-colors duration-200 ease-out ${
|
|
23
|
+
isSelected ? 'border-2 border-[#819D9F] bg-[#e8f0f0]' : ''
|
|
24
24
|
}`}
|
|
25
25
|
>
|
|
26
26
|
{/* Date column */}
|
|
27
|
-
<div className="w-12 shrink-0 bg-zinc-100 flex flex-col items-center justify-center py-
|
|
27
|
+
<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
28
|
<span className="text-base font-semibold text-zinc-900 leading-none">{day}</span>
|
|
29
29
|
<span className="text-[10px] text-zinc-500 uppercase leading-none mt-0.5">{weekday}</span>
|
|
30
30
|
</div>
|
|
31
31
|
|
|
32
32
|
{/* Content */}
|
|
33
|
-
<div className="flex-1 py-
|
|
34
|
-
<div className="flex items-center gap-
|
|
35
|
-
<span className="font-medium text-
|
|
33
|
+
<div className="flex-1 py-3 px-4">
|
|
34
|
+
<div className="flex items-center gap-3">
|
|
35
|
+
<span className="font-medium text-base text-zinc-900 truncate">{prototype.title}</span>
|
|
36
36
|
{prototype.files.length > 1 && (
|
|
37
|
-
<span className="text-[10px] px-
|
|
37
|
+
<span className="text-[10px] px-2 py-1 bg-green-100 text-green-700 rounded-full whitespace-nowrap">
|
|
38
38
|
{prototype.files.length} opts
|
|
39
39
|
</span>
|
|
40
40
|
)}
|
|
41
41
|
</div>
|
|
42
42
|
{!compact && (
|
|
43
|
-
<div className="flex items-center gap-
|
|
43
|
+
<div className="flex items-center gap-3 text-base">
|
|
44
44
|
{prototype.feature ? (
|
|
45
|
-
<span className="text-
|
|
45
|
+
<span className="text-[#5a7d7f]">#{prototype.feature.id}</span>
|
|
46
46
|
) : (
|
|
47
47
|
<span className="text-zinc-500">Research</span>
|
|
48
48
|
)}
|
|
@@ -66,7 +66,7 @@ function MonthGroup({ month, prototypes, selectedId, onSelect, compact }: MonthG
|
|
|
66
66
|
return (
|
|
67
67
|
<div className="mb-4">
|
|
68
68
|
{/* Month header */}
|
|
69
|
-
<div className="flex items-center gap-
|
|
69
|
+
<div className="flex items-center gap-3 mb-3">
|
|
70
70
|
<span className="text-[10px] font-semibold text-zinc-500 uppercase tracking-wider">
|
|
71
71
|
{month}
|
|
72
72
|
</span>
|
|
@@ -74,7 +74,7 @@ function MonthGroup({ month, prototypes, selectedId, onSelect, compact }: MonthG
|
|
|
74
74
|
</div>
|
|
75
75
|
|
|
76
76
|
{/* Prototype cards */}
|
|
77
|
-
<div className="space-y-
|
|
77
|
+
<div className="space-y-2">
|
|
78
78
|
{prototypes.map(prototype => (
|
|
79
79
|
<PrototypeCard
|
|
80
80
|
key={prototype.id}
|
|
@@ -125,23 +125,23 @@ function PreviewPanel({ prototype, onClose }: PreviewPanelProps) {
|
|
|
125
125
|
};
|
|
126
126
|
|
|
127
127
|
return (
|
|
128
|
-
<div className="flex flex-col h-full bg-white
|
|
128
|
+
<div className="flex flex-col h-full bg-white rounded-lg overflow-hidden">
|
|
129
129
|
{/* Header */}
|
|
130
|
-
<div className="flex items-center justify-between p-
|
|
130
|
+
<div className="flex items-center justify-between p-6 border-b border-zinc-200">
|
|
131
131
|
<div>
|
|
132
132
|
<h3 className="font-semibold text-zinc-900">{prototype.title}</h3>
|
|
133
|
-
<p className="text-
|
|
133
|
+
<p className="text-base text-zinc-500">{selectedFile?.name || prototype.description}</p>
|
|
134
134
|
</div>
|
|
135
135
|
<div className="flex items-center gap-2">
|
|
136
136
|
<button
|
|
137
137
|
onClick={handleOpen}
|
|
138
|
-
className="px-
|
|
138
|
+
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
139
|
>
|
|
140
140
|
Open
|
|
141
141
|
</button>
|
|
142
142
|
<button
|
|
143
143
|
onClick={onClose}
|
|
144
|
-
className="p-2 text-zinc-400 hover:text-zinc-700 transition-colors"
|
|
144
|
+
className="p-2 text-zinc-400 hover:text-zinc-700 transition-colors duration-200 ease-out"
|
|
145
145
|
>
|
|
146
146
|
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
147
147
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
|
|
@@ -152,16 +152,16 @@ function PreviewPanel({ prototype, onClose }: PreviewPanelProps) {
|
|
|
152
152
|
|
|
153
153
|
{/* File selector */}
|
|
154
154
|
{prototype.files.length > 1 && (
|
|
155
|
-
<div className="px-
|
|
156
|
-
<div className="flex flex-wrap gap-
|
|
155
|
+
<div className="px-5 py-3 border-b border-zinc-200">
|
|
156
|
+
<div className="flex flex-wrap gap-3">
|
|
157
157
|
{prototype.files.map(file => (
|
|
158
158
|
<button
|
|
159
159
|
key={file.path}
|
|
160
160
|
onClick={() => setSelectedFile(file)}
|
|
161
|
-
className={`px-
|
|
161
|
+
className={`px-3 py-1.5 text-base rounded transition-colors duration-200 ease-out ${
|
|
162
162
|
selectedFile?.path === file.path
|
|
163
|
-
? 'bg-
|
|
164
|
-
: 'bg-zinc-100 border border-zinc-300 text-zinc-700 hover:border-
|
|
163
|
+
? 'bg-[#819D9F] text-white'
|
|
164
|
+
: 'bg-zinc-100 border border-zinc-300 text-zinc-700 hover:border-[#819D9F]'
|
|
165
165
|
}`}
|
|
166
166
|
>
|
|
167
167
|
{file.name}
|
|
@@ -190,8 +190,8 @@ function PreviewPanel({ prototype, onClose }: PreviewPanelProps) {
|
|
|
190
190
|
) : (
|
|
191
191
|
<div className="flex items-center justify-center h-full text-zinc-500">
|
|
192
192
|
<div className="text-center">
|
|
193
|
-
<p className="mb-
|
|
194
|
-
<p className="text-
|
|
193
|
+
<p className="mb-3">Cannot preview this file type</p>
|
|
194
|
+
<p className="text-base">{selectedFile?.name}</p>
|
|
195
195
|
</div>
|
|
196
196
|
</div>
|
|
197
197
|
)}
|
|
@@ -233,9 +233,9 @@ export function PrototypeTimeline({ data }: PrototypeTimelineProps) {
|
|
|
233
233
|
: null;
|
|
234
234
|
|
|
235
235
|
return (
|
|
236
|
-
<div className="flex gap-
|
|
236
|
+
<div className="flex gap-6">
|
|
237
237
|
{/* Timeline list */}
|
|
238
|
-
<div className={`${selectedPrototype ? 'w-[320px] shrink-0' : 'w-full'} transition-
|
|
238
|
+
<div className={`${selectedPrototype ? 'w-[320px] shrink-0' : 'w-full'} transition-[width] duration-200 ease-out`}>
|
|
239
239
|
{months.map(month => (
|
|
240
240
|
<MonthGroup
|
|
241
241
|
key={month}
|