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
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import { useState, useEffect, useRef, ReactNode } from 'react';
|
|
4
4
|
|
|
5
|
-
type AnimationPhase = 'idle' | '
|
|
5
|
+
type AnimationPhase = 'idle' | 'video-playing' | 'collapsing' | 'complete';
|
|
6
6
|
|
|
7
7
|
interface WaveCompletionAnimationProps {
|
|
8
8
|
isPlaying: boolean;
|
|
@@ -13,16 +13,16 @@ interface WaveCompletionAnimationProps {
|
|
|
13
13
|
export function WaveCompletionAnimation({ isPlaying, onComplete, children }: WaveCompletionAnimationProps) {
|
|
14
14
|
const [phase, setPhase] = useState<AnimationPhase>('idle');
|
|
15
15
|
const videoRef = useRef<HTMLVideoElement>(null);
|
|
16
|
+
const containerRef = useRef<HTMLDivElement>(null);
|
|
16
17
|
const hasStartedRef = useRef(false);
|
|
17
18
|
const timeoutRefs = useRef<NodeJS.Timeout[]>([]);
|
|
19
|
+
const measuredHeightRef = useRef<number | null>(null);
|
|
18
20
|
|
|
19
|
-
// Clear all pending timeouts
|
|
20
21
|
const clearAllTimeouts = () => {
|
|
21
22
|
timeoutRefs.current.forEach(clearTimeout);
|
|
22
23
|
timeoutRefs.current = [];
|
|
23
24
|
};
|
|
24
25
|
|
|
25
|
-
// Handle video load errors - skip animation and complete immediately
|
|
26
26
|
const handleVideoError = () => {
|
|
27
27
|
if (hasStartedRef.current) {
|
|
28
28
|
clearAllTimeouts();
|
|
@@ -35,107 +35,106 @@ export function WaveCompletionAnimation({ isPlaying, onComplete, children }: Wav
|
|
|
35
35
|
if (isPlaying && !hasStartedRef.current) {
|
|
36
36
|
hasStartedRef.current = true;
|
|
37
37
|
|
|
38
|
-
// Check for reduced motion preference
|
|
39
38
|
const prefersReducedMotion = typeof window !== 'undefined' &&
|
|
40
39
|
window.matchMedia('(prefers-reduced-motion: reduce)').matches;
|
|
41
40
|
|
|
42
41
|
if (prefersReducedMotion) {
|
|
43
|
-
// Skip animation entirely - complete immediately
|
|
44
42
|
setPhase('complete');
|
|
45
43
|
onComplete();
|
|
46
44
|
return;
|
|
47
45
|
}
|
|
48
46
|
|
|
49
|
-
//
|
|
50
|
-
|
|
47
|
+
// Capture height before animation for smooth collapse later
|
|
48
|
+
if (containerRef.current) {
|
|
49
|
+
measuredHeightRef.current = containerRef.current.offsetHeight;
|
|
50
|
+
containerRef.current.style.maxHeight = `${measuredHeightRef.current}px`;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Phase 1: Content disappears instantly, video starts playing
|
|
54
|
+
setPhase('video-playing');
|
|
51
55
|
|
|
52
|
-
// Phase 2: Video starts playing
|
|
53
56
|
const video = videoRef.current;
|
|
54
57
|
if (video) {
|
|
55
58
|
video.currentTime = 0;
|
|
56
|
-
video.play().catch(() =>
|
|
57
|
-
// Video play failed - skip animation
|
|
58
|
-
handleVideoError();
|
|
59
|
-
});
|
|
59
|
+
video.play().catch(() => handleVideoError());
|
|
60
60
|
}
|
|
61
61
|
|
|
62
|
+
// Phase 2: After 1.5s of video, collapse the card (opacity + height)
|
|
62
63
|
const t1 = setTimeout(() => {
|
|
63
|
-
setPhase('
|
|
64
|
-
},
|
|
64
|
+
setPhase('collapsing');
|
|
65
|
+
}, 1500);
|
|
65
66
|
timeoutRefs.current.push(t1);
|
|
66
67
|
|
|
67
|
-
// Phase 3: After
|
|
68
|
+
// Phase 3: After collapse transition (0.5s), fire onComplete
|
|
68
69
|
const t2 = setTimeout(() => {
|
|
69
|
-
|
|
70
|
-
}, 5000);
|
|
71
|
-
timeoutRefs.current.push(t2);
|
|
72
|
-
|
|
73
|
-
// Phase 4: After fade completes (1.5s), call onComplete
|
|
74
|
-
const t3 = setTimeout(() => {
|
|
75
|
-
if (video) {
|
|
76
|
-
video.pause();
|
|
77
|
-
}
|
|
70
|
+
if (video) video.pause();
|
|
78
71
|
setPhase('complete');
|
|
79
72
|
onComplete();
|
|
80
|
-
},
|
|
81
|
-
timeoutRefs.current.push(
|
|
73
|
+
}, 2000);
|
|
74
|
+
timeoutRefs.current.push(t2);
|
|
82
75
|
}
|
|
83
76
|
}, [isPlaying, onComplete]);
|
|
84
77
|
|
|
85
|
-
// Reset when isPlaying becomes false — but only if not already complete.
|
|
86
|
-
// If the animation finished naturally (phase === 'complete'), skip the reset
|
|
87
|
-
// to avoid snapping opacity back to 1 before the CSS transition visually ends.
|
|
88
78
|
useEffect(() => {
|
|
89
79
|
if (!isPlaying && phase !== 'complete') {
|
|
90
80
|
clearAllTimeouts();
|
|
91
81
|
hasStartedRef.current = false;
|
|
92
82
|
setPhase('idle');
|
|
83
|
+
measuredHeightRef.current = null;
|
|
84
|
+
if (containerRef.current) {
|
|
85
|
+
containerRef.current.style.maxHeight = '';
|
|
86
|
+
}
|
|
93
87
|
}
|
|
94
88
|
}, [isPlaying, phase]);
|
|
95
89
|
|
|
96
|
-
// Cleanup on unmount
|
|
97
90
|
useEffect(() => {
|
|
98
91
|
return () => clearAllTimeouts();
|
|
99
92
|
}, []);
|
|
100
93
|
|
|
101
|
-
const
|
|
102
|
-
const
|
|
103
|
-
const cardOpacity = phase === 'card-fade' || phase === 'complete' ? 0 : 1;
|
|
94
|
+
const isIdle = phase === 'idle';
|
|
95
|
+
const isCollapsing = phase === 'collapsing' || phase === 'complete';
|
|
104
96
|
|
|
105
97
|
return (
|
|
106
98
|
<div
|
|
107
|
-
|
|
99
|
+
ref={containerRef}
|
|
108
100
|
style={{
|
|
109
|
-
|
|
110
|
-
|
|
101
|
+
overflow: 'hidden',
|
|
102
|
+
opacity: isCollapsing ? 0 : 1,
|
|
103
|
+
maxHeight: isCollapsing ? 0 : undefined,
|
|
104
|
+
transition: isCollapsing
|
|
105
|
+
? 'opacity 0.4s ease-out, max-height 0.5s ease-out'
|
|
106
|
+
: undefined,
|
|
111
107
|
}}
|
|
112
108
|
>
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
109
|
+
<div className="relative overflow-hidden rounded-xl">
|
|
110
|
+
{/* Wave video - positioned behind content */}
|
|
111
|
+
<video
|
|
112
|
+
ref={videoRef}
|
|
113
|
+
className="absolute inset-0 w-full h-full object-cover rounded-xl"
|
|
114
|
+
style={{
|
|
115
|
+
opacity: isIdle ? 0 : 1,
|
|
116
|
+
transition: 'opacity 0.5s ease-in',
|
|
117
|
+
zIndex: 1,
|
|
118
|
+
}}
|
|
119
|
+
muted
|
|
120
|
+
playsInline
|
|
121
|
+
preload="auto"
|
|
122
|
+
src="/assets/wave-completion.mp4"
|
|
123
|
+
onError={handleVideoError}
|
|
124
|
+
/>
|
|
125
|
+
|
|
126
|
+
{/* Card content - positioned above video */}
|
|
127
|
+
<div
|
|
128
|
+
style={{
|
|
129
|
+
opacity: isIdle ? 1 : 0,
|
|
130
|
+
transition: 'opacity 0.5s ease-out',
|
|
131
|
+
position: 'relative',
|
|
132
|
+
zIndex: 2,
|
|
133
|
+
background: 'inherit',
|
|
134
|
+
}}
|
|
135
|
+
>
|
|
136
|
+
{children}
|
|
137
|
+
</div>
|
|
139
138
|
</div>
|
|
140
139
|
</div>
|
|
141
140
|
);
|
|
@@ -1,22 +1,25 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
3
|
import Image from 'next/image';
|
|
4
|
+
import { Button } from '@/components/ui/Button';
|
|
4
5
|
import type { RecentProject } from '@/lib/db-bridge';
|
|
5
6
|
|
|
6
7
|
interface WelcomeScreenProps {
|
|
7
8
|
recentProjects?: RecentProject[];
|
|
9
|
+
onNewProject?: () => void;
|
|
8
10
|
onOpenProject?: () => void;
|
|
9
11
|
onSelectRecentProject?: (project: RecentProject) => void;
|
|
10
12
|
}
|
|
11
13
|
|
|
12
14
|
export function WelcomeScreen({
|
|
13
15
|
recentProjects = [],
|
|
16
|
+
onNewProject,
|
|
14
17
|
onOpenProject,
|
|
15
18
|
onSelectRecentProject,
|
|
16
19
|
}: WelcomeScreenProps) {
|
|
17
20
|
return (
|
|
18
|
-
<div className="flex flex-col items-center justify-center min-h-screen bg-white dark:bg-zinc-900
|
|
19
|
-
<div className="max-w-md w-full space-y-
|
|
21
|
+
<div className="flex flex-col items-center justify-center min-h-screen bg-white dark:bg-zinc-900 px-8 py-6 overflow-y-auto">
|
|
22
|
+
<div className="max-w-md w-full space-y-6">
|
|
20
23
|
{/* Logo */}
|
|
21
24
|
<div className="flex flex-col items-center space-y-4">
|
|
22
25
|
<Image
|
|
@@ -31,54 +34,49 @@ export function WelcomeScreen({
|
|
|
31
34
|
</p>
|
|
32
35
|
</div>
|
|
33
36
|
|
|
34
|
-
{/*
|
|
35
|
-
<div className="pt-4">
|
|
36
|
-
<
|
|
37
|
+
{/* Project Buttons */}
|
|
38
|
+
<div className="pt-4 space-y-4">
|
|
39
|
+
<Button
|
|
40
|
+
onClick={onNewProject}
|
|
41
|
+
size="lg"
|
|
42
|
+
fullWidth
|
|
43
|
+
data-testid="new-project-button"
|
|
44
|
+
>
|
|
45
|
+
New Project
|
|
46
|
+
</Button>
|
|
47
|
+
<Button
|
|
37
48
|
onClick={onOpenProject}
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
background: 'linear-gradient(145deg, #ffffff 0%, #faf9f7 10%, #f0f4f4 35%, #c8d9da 55%, #819D9F 90%)',
|
|
42
|
-
color: '#3d4d4e',
|
|
43
|
-
boxShadow: `
|
|
44
|
-
0 1px 1px rgba(0, 0, 0, 0.02),
|
|
45
|
-
0 2px 4px rgba(0, 0, 0, 0.03),
|
|
46
|
-
0 6px 12px rgba(0, 0, 0, 0.05),
|
|
47
|
-
0 12px 24px rgba(0, 0, 0, 0.06),
|
|
48
|
-
0 20px 40px rgba(129, 157, 159, 0.2),
|
|
49
|
-
0 32px 64px rgba(129, 157, 159, 0.18),
|
|
50
|
-
inset 0 2px 4px rgba(255, 255, 255, 1),
|
|
51
|
-
inset 0 -2px 4px rgba(129, 157, 159, 0.05)
|
|
52
|
-
`,
|
|
53
|
-
}}
|
|
49
|
+
variant="secondary"
|
|
50
|
+
size="lg"
|
|
51
|
+
fullWidth
|
|
54
52
|
data-testid="open-project-button"
|
|
55
53
|
>
|
|
56
54
|
Open Project
|
|
57
|
-
</
|
|
55
|
+
</Button>
|
|
58
56
|
</div>
|
|
59
57
|
|
|
60
58
|
{/* Recent Projects Section */}
|
|
61
|
-
<div className="pt-
|
|
59
|
+
<div className="pt-6 space-y-3" data-testid="recent-projects-section">
|
|
62
60
|
<h2 className="text-lg font-semibold text-zinc-900 dark:text-zinc-100">
|
|
63
61
|
Recent Projects
|
|
64
62
|
</h2>
|
|
65
63
|
{recentProjects.length === 0 ? (
|
|
66
|
-
<div className="
|
|
64
|
+
<div className="rounded-lg p-4 text-zinc-500 dark:text-zinc-400 text-base text-center">
|
|
67
65
|
No recent projects
|
|
68
66
|
</div>
|
|
69
67
|
) : (
|
|
70
68
|
<div className="space-y-2">
|
|
71
|
-
{recentProjects.map((project) => (
|
|
69
|
+
{recentProjects.slice(0, 4).map((project) => (
|
|
72
70
|
<button
|
|
73
71
|
key={project.path}
|
|
74
72
|
onClick={() => onSelectRecentProject?.(project)}
|
|
75
|
-
className="w-full text-left p-4 border border-zinc-200 dark:border-zinc-700
|
|
73
|
+
className="w-full text-left p-4 rounded-xl bg-zinc-100 dark:bg-zinc-800 border border-zinc-200 dark:border-zinc-700 hover:bg-zinc-200 dark:hover:bg-zinc-700 transition-colors duration-200 ease-out cursor-pointer"
|
|
76
74
|
data-testid={`recent-project-${project.name}`}
|
|
77
75
|
>
|
|
78
76
|
<div className="font-medium text-zinc-900 dark:text-zinc-100">
|
|
79
77
|
{project.name}
|
|
80
78
|
</div>
|
|
81
|
-
<div className="text-
|
|
79
|
+
<div className="text-base text-zinc-500 dark:text-zinc-400 truncate">
|
|
82
80
|
{project.path}
|
|
83
81
|
</div>
|
|
84
82
|
</button>
|
|
@@ -1,19 +1,19 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
3
|
import { CopyableId } from './CopyableId';
|
|
4
|
+
import { TypeIcon } from './TypeIcon';
|
|
4
5
|
|
|
5
6
|
interface WorkItemHeaderProps {
|
|
6
7
|
id: number;
|
|
7
8
|
title: string;
|
|
8
9
|
type: string;
|
|
9
|
-
typeIcon: string;
|
|
10
10
|
typeLabel: string;
|
|
11
11
|
}
|
|
12
12
|
|
|
13
|
-
export function WorkItemHeader({ id, title, type,
|
|
13
|
+
export function WorkItemHeader({ id, title, type, typeLabel }: WorkItemHeaderProps) {
|
|
14
14
|
return (
|
|
15
|
-
<div className="flex items-center gap-
|
|
16
|
-
<span
|
|
15
|
+
<div className="flex items-center gap-3 text-base text-zinc-500 mb-1.5">
|
|
16
|
+
<span className="flex items-center gap-1"><TypeIcon type={type} /> {typeLabel}</span>
|
|
17
17
|
<span>•</span>
|
|
18
18
|
<CopyableId id={id} title={title} type={type} />
|
|
19
19
|
</div>
|
|
@@ -4,27 +4,8 @@ import { useState } from 'react';
|
|
|
4
4
|
import Link from 'next/link';
|
|
5
5
|
import type { WorkItem } from '@/lib/db';
|
|
6
6
|
import { CopyableId } from './CopyableId';
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
epic: '🎯',
|
|
10
|
-
feature: '✨',
|
|
11
|
-
chore: '🔧',
|
|
12
|
-
bug: '🐛',
|
|
13
|
-
};
|
|
14
|
-
|
|
15
|
-
const statusColors: Record<string, string> = {
|
|
16
|
-
backlog: 'text-zinc-500',
|
|
17
|
-
todo: 'text-zinc-500',
|
|
18
|
-
in_progress: 'text-blue-600 dark:text-blue-400',
|
|
19
|
-
done: 'text-green-600 dark:text-green-400',
|
|
20
|
-
cancelled: 'text-red-500',
|
|
21
|
-
};
|
|
22
|
-
|
|
23
|
-
const modeLabels: Record<string, { label: string; color: string }> = {
|
|
24
|
-
speed: { label: 'speed', color: 'bg-amber-100 text-amber-800 dark:bg-amber-900 dark:text-amber-200' },
|
|
25
|
-
stable: { label: 'stable', color: 'bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200' },
|
|
26
|
-
production: { label: 'prod', color: 'bg-purple-100 text-purple-800 dark:bg-purple-900 dark:text-purple-200' },
|
|
27
|
-
};
|
|
7
|
+
import { STATUS_COLORS, MODE_LABELS } from '@/lib/constants';
|
|
8
|
+
import { TypeIcon } from './TypeIcon';
|
|
28
9
|
|
|
29
10
|
interface WorkItemNodeProps {
|
|
30
11
|
item: WorkItem;
|
|
@@ -38,7 +19,7 @@ function WorkItemNode({ item, depth = 0 }: WorkItemNodeProps) {
|
|
|
38
19
|
return (
|
|
39
20
|
<div className="select-none">
|
|
40
21
|
<div
|
|
41
|
-
className={`flex items-center gap-
|
|
22
|
+
className={`flex items-center gap-3 py-2 px-3 rounded hover:bg-zinc-100 dark:hover:bg-zinc-800 transition-colors duration-200 ease-out group`}
|
|
42
23
|
style={{ paddingLeft: `${depth * 20 + 8}px` }}
|
|
43
24
|
>
|
|
44
25
|
{/* Expand/collapse toggle */}
|
|
@@ -54,7 +35,7 @@ function WorkItemNode({ item, depth = 0 }: WorkItemNodeProps) {
|
|
|
54
35
|
)}
|
|
55
36
|
|
|
56
37
|
{/* Type icon */}
|
|
57
|
-
<span className="text-
|
|
38
|
+
<span className="text-base"><TypeIcon type={item.type} /></span>
|
|
58
39
|
|
|
59
40
|
{/* ID with click-to-copy */}
|
|
60
41
|
<CopyableId id={item.id} title={item.title} type={item.type} />
|
|
@@ -62,15 +43,15 @@ function WorkItemNode({ item, depth = 0 }: WorkItemNodeProps) {
|
|
|
62
43
|
{/* Title */}
|
|
63
44
|
<Link
|
|
64
45
|
href={`/work/${item.id}`}
|
|
65
|
-
className={`flex-1 truncate hover:underline ${
|
|
46
|
+
className={`flex-1 truncate hover:underline ${STATUS_COLORS[item.status]}`}
|
|
66
47
|
>
|
|
67
48
|
{item.title}
|
|
68
49
|
</Link>
|
|
69
50
|
|
|
70
51
|
{/* Mode badge */}
|
|
71
|
-
{item.mode &&
|
|
72
|
-
<span className={`text-xs px-
|
|
73
|
-
{
|
|
52
|
+
{item.mode && MODE_LABELS[item.mode] && (
|
|
53
|
+
<span className={`text-xs px-2 py-1 rounded ${MODE_LABELS[item.mode].color}`}>
|
|
54
|
+
{MODE_LABELS[item.mode].label}
|
|
74
55
|
</span>
|
|
75
56
|
)}
|
|
76
57
|
|
|
@@ -108,7 +89,7 @@ export function WorkItemTree({ items }: WorkItemTreeProps) {
|
|
|
108
89
|
}
|
|
109
90
|
|
|
110
91
|
return (
|
|
111
|
-
<div className="font-mono text-
|
|
92
|
+
<div className="font-mono text-base">
|
|
112
93
|
{items.map((item) => (
|
|
113
94
|
<WorkItemNode key={item.id} item={item} />
|
|
114
95
|
))}
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useState, useEffect } from 'react';
|
|
4
|
+
import { useRouter } from 'next/navigation';
|
|
5
|
+
import { useUsage } from '@/contexts/UsageContext';
|
|
6
|
+
import { SubscribeContent } from '@/components/SubscribeContent';
|
|
7
|
+
import { Button } from '@/components/ui/Button';
|
|
8
|
+
|
|
9
|
+
const planColors: Record<string, string> = {
|
|
10
|
+
free: 'bg-zinc-200 dark:bg-zinc-700 text-zinc-700 dark:text-zinc-300',
|
|
11
|
+
monthly: 'bg-[#e8f0f0] dark:bg-[#819D9F]/20 text-[#5a7d7f] dark:text-[#a3bfc0]',
|
|
12
|
+
lifetime: 'bg-purple-100 dark:bg-purple-900/40 text-purple-700 dark:text-purple-300',
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export function AccountSection() {
|
|
16
|
+
const router = useRouter();
|
|
17
|
+
const { plan, used, limit, loading, refresh } = useUsage();
|
|
18
|
+
const [email, setEmail] = useState<string | null>(null);
|
|
19
|
+
const [portalError, setPortalError] = useState<string | null>(null);
|
|
20
|
+
const [portalLoading, setPortalLoading] = useState(false);
|
|
21
|
+
const [showUpgrade, setShowUpgrade] = useState(false);
|
|
22
|
+
|
|
23
|
+
useEffect(() => {
|
|
24
|
+
async function loadAuth() {
|
|
25
|
+
if (!window.electronAPI?.isElectron) return;
|
|
26
|
+
try {
|
|
27
|
+
const status = await window.electronAPI.auth.getStatus();
|
|
28
|
+
if (status.authenticated && status.user) {
|
|
29
|
+
setEmail(status.user.email);
|
|
30
|
+
}
|
|
31
|
+
} catch {
|
|
32
|
+
// Auth status unavailable - email stays null, shows "Not signed in"
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
loadAuth();
|
|
36
|
+
}, []);
|
|
37
|
+
|
|
38
|
+
const isFree = plan === 'free';
|
|
39
|
+
const isAtCapacity = isFree && limit > 0 && used >= limit;
|
|
40
|
+
|
|
41
|
+
const handleManageSubscription = async () => {
|
|
42
|
+
if (!window.electronAPI?.billing?.openCustomerPortal) return;
|
|
43
|
+
setPortalError(null);
|
|
44
|
+
setPortalLoading(true);
|
|
45
|
+
try {
|
|
46
|
+
const result = await window.electronAPI.billing.openCustomerPortal();
|
|
47
|
+
if (result && !result.success) {
|
|
48
|
+
setPortalError(result.error || 'Unable to open billing portal');
|
|
49
|
+
}
|
|
50
|
+
} catch {
|
|
51
|
+
setPortalError('Unable to open billing portal');
|
|
52
|
+
} finally {
|
|
53
|
+
setPortalLoading(false);
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
const handleLogout = async () => {
|
|
58
|
+
if (!window.electronAPI?.auth?.logout) return;
|
|
59
|
+
await window.electronAPI.auth.logout();
|
|
60
|
+
router.push('/login');
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
const handleUpgradeClose = () => {
|
|
64
|
+
setShowUpgrade(false);
|
|
65
|
+
refresh();
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
if (showUpgrade) {
|
|
69
|
+
return (
|
|
70
|
+
<section id="account" className="relative min-h-[500px]">
|
|
71
|
+
<Button
|
|
72
|
+
onClick={handleUpgradeClose}
|
|
73
|
+
variant="ghost"
|
|
74
|
+
size="icon"
|
|
75
|
+
className="absolute top-0 right-0 z-10"
|
|
76
|
+
aria-label="Close upgrade"
|
|
77
|
+
>
|
|
78
|
+
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
79
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
|
|
80
|
+
</svg>
|
|
81
|
+
</Button>
|
|
82
|
+
<div className="flex flex-col items-center justify-center min-h-[500px] py-8">
|
|
83
|
+
<SubscribeContent onClose={handleUpgradeClose} />
|
|
84
|
+
</div>
|
|
85
|
+
</section>
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return (
|
|
90
|
+
<section id="account">
|
|
91
|
+
<div className="flex items-center justify-between mb-6">
|
|
92
|
+
<h2 className="text-lg font-medium text-zinc-900 dark:text-zinc-100">
|
|
93
|
+
Account
|
|
94
|
+
</h2>
|
|
95
|
+
</div>
|
|
96
|
+
|
|
97
|
+
{/* Email & Plan */}
|
|
98
|
+
<div className="p-6 bg-zinc-50 dark:bg-zinc-800/50 rounded-xl space-y-4">
|
|
99
|
+
<div className="flex items-center justify-between">
|
|
100
|
+
<div>
|
|
101
|
+
<label className="block text-base font-medium text-zinc-900 dark:text-zinc-100">
|
|
102
|
+
Email
|
|
103
|
+
</label>
|
|
104
|
+
<p className="text-base text-zinc-600 dark:text-zinc-400 mt-1">
|
|
105
|
+
{loading ? '...' : email || 'Not signed in'}
|
|
106
|
+
</p>
|
|
107
|
+
</div>
|
|
108
|
+
<div className="flex items-center gap-3">
|
|
109
|
+
<span className={`px-2 py-1 text-xs font-medium rounded-full ${planColors[plan] || planColors.free}`}>
|
|
110
|
+
{plan}
|
|
111
|
+
</span>
|
|
112
|
+
</div>
|
|
113
|
+
</div>
|
|
114
|
+
|
|
115
|
+
{/* Usage Bar (free users only) */}
|
|
116
|
+
{isFree && (
|
|
117
|
+
<div>
|
|
118
|
+
<div className="flex items-center justify-between text-base text-zinc-500 dark:text-zinc-400 mb-1.5">
|
|
119
|
+
<span>Weekly usage</span>
|
|
120
|
+
<span>{used} / {limit} work items</span>
|
|
121
|
+
</div>
|
|
122
|
+
<div className="w-full h-2 bg-zinc-200 dark:bg-zinc-700 rounded-full overflow-hidden">
|
|
123
|
+
<div
|
|
124
|
+
className={`h-full rounded-full transition-[width,background-color] duration-200 ease-out ${isAtCapacity ? 'bg-red-500' : 'bg-[#819D9F]'}`}
|
|
125
|
+
style={{ width: `${limit > 0 ? Math.min((used / limit) * 100, 100) : 0}%` }}
|
|
126
|
+
/>
|
|
127
|
+
</div>
|
|
128
|
+
</div>
|
|
129
|
+
)}
|
|
130
|
+
|
|
131
|
+
{/* Action Button */}
|
|
132
|
+
<div className="pt-2 border-t border-zinc-200 dark:border-zinc-700 flex items-center justify-between">
|
|
133
|
+
<div>
|
|
134
|
+
{isFree ? (
|
|
135
|
+
<Button
|
|
136
|
+
onClick={() => setShowUpgrade(true)}
|
|
137
|
+
variant="accent"
|
|
138
|
+
size="sm"
|
|
139
|
+
>
|
|
140
|
+
Upgrade
|
|
141
|
+
</Button>
|
|
142
|
+
) : (
|
|
143
|
+
<>
|
|
144
|
+
<Button
|
|
145
|
+
onClick={handleManageSubscription}
|
|
146
|
+
disabled={portalLoading}
|
|
147
|
+
variant="secondary"
|
|
148
|
+
size="sm"
|
|
149
|
+
>
|
|
150
|
+
{portalLoading ? 'Opening...' : 'Manage Subscription'}
|
|
151
|
+
</Button>
|
|
152
|
+
{portalError && (
|
|
153
|
+
<p className="text-base text-red-500 mt-1">{portalError}</p>
|
|
154
|
+
)}
|
|
155
|
+
</>
|
|
156
|
+
)}
|
|
157
|
+
</div>
|
|
158
|
+
<Button
|
|
159
|
+
onClick={handleLogout}
|
|
160
|
+
variant="destructive"
|
|
161
|
+
size="sm"
|
|
162
|
+
>
|
|
163
|
+
Log Out
|
|
164
|
+
</Button>
|
|
165
|
+
</div>
|
|
166
|
+
</div>
|
|
167
|
+
</section>
|
|
168
|
+
);
|
|
169
|
+
}
|