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.
Files changed (162) hide show
  1. package/.env +7 -0
  2. package/apps/dashboard/app/api/claude/[workItemId]/message/route.ts +124 -48
  3. package/apps/dashboard/app/api/claude/[workItemId]/route.ts +171 -58
  4. package/apps/dashboard/app/api/claude/sessions/[sessionId]/message/route.ts +161 -10
  5. package/apps/dashboard/app/api/tests/run/stream/route.ts +13 -1
  6. package/apps/dashboard/app/api/usage/route.ts +17 -0
  7. package/apps/dashboard/app/api/work/[id]/route.ts +35 -0
  8. package/apps/dashboard/app/api/work/[id]/status/route.ts +43 -1
  9. package/apps/dashboard/app/connect-claude/page.tsx +24 -0
  10. package/apps/dashboard/app/decision/[id]/page.tsx +14 -14
  11. package/apps/dashboard/app/demo/gates/page.tsx +42 -42
  12. package/apps/dashboard/app/design-system/page.tsx +868 -0
  13. package/apps/dashboard/app/globals.css +6 -2
  14. package/apps/dashboard/app/install-claude/page.tsx +9 -7
  15. package/apps/dashboard/app/layout.tsx +17 -5
  16. package/apps/dashboard/app/login/page.tsx +250 -0
  17. package/apps/dashboard/app/page.tsx +11 -9
  18. package/apps/dashboard/app/settings/page.tsx +4 -2
  19. package/apps/dashboard/app/signup/page.tsx +245 -0
  20. package/apps/dashboard/app/subscribe/page.tsx +11 -0
  21. package/apps/dashboard/app/welcome/page.tsx +24 -1
  22. package/apps/dashboard/app/work/[id]/page.tsx +34 -50
  23. package/apps/dashboard/components/AppShell.tsx +95 -55
  24. package/apps/dashboard/components/CardMenu.tsx +56 -13
  25. package/apps/dashboard/components/ClaudePanel.tsx +301 -582
  26. package/apps/dashboard/components/ClaudePanelInput.tsx +23 -14
  27. package/apps/dashboard/components/ConnectClaudeScreen.tsx +210 -0
  28. package/apps/dashboard/components/CopyableId.tsx +3 -3
  29. package/apps/dashboard/components/DetailReviewActions.tsx +109 -0
  30. package/apps/dashboard/components/DragContext.tsx +75 -65
  31. package/apps/dashboard/components/DraggableCard.tsx +6 -46
  32. package/apps/dashboard/components/DropZone.tsx +2 -2
  33. package/apps/dashboard/components/EditableDetailDescription.tsx +1 -1
  34. package/apps/dashboard/components/EditableTitle.tsx +26 -6
  35. package/apps/dashboard/components/ElapsedTimer.tsx +54 -0
  36. package/apps/dashboard/components/EpicGroup.tsx +329 -0
  37. package/apps/dashboard/components/GateCard.tsx +100 -16
  38. package/apps/dashboard/components/GateChoiceCard.tsx +15 -17
  39. package/apps/dashboard/components/InstallClaudeScreen.tsx +140 -51
  40. package/apps/dashboard/components/JettyLoader.tsx +38 -0
  41. package/apps/dashboard/components/KanbanBoard.tsx +147 -766
  42. package/apps/dashboard/components/KanbanCard.tsx +506 -0
  43. package/apps/dashboard/components/LazyMarkdown.tsx +12 -0
  44. package/apps/dashboard/components/MainNav.tsx +20 -54
  45. package/apps/dashboard/components/MessageBlock.tsx +391 -0
  46. package/apps/dashboard/components/ModeStartCard.tsx +15 -15
  47. package/apps/dashboard/components/OnboardingWelcome.tsx +214 -0
  48. package/apps/dashboard/components/PlaceholderCard.tsx +11 -21
  49. package/apps/dashboard/components/ProjectSwitcher.tsx +36 -8
  50. package/apps/dashboard/components/PrototypeTimeline.tsx +25 -25
  51. package/apps/dashboard/components/RealTimeKanbanWrapper.tsx +265 -301
  52. package/apps/dashboard/components/RealTimeTestsWrapper.tsx +97 -74
  53. package/apps/dashboard/components/ReviewFooter.tsx +141 -0
  54. package/apps/dashboard/components/SessionList.tsx +19 -18
  55. package/apps/dashboard/components/SubscribeContent.tsx +206 -0
  56. package/apps/dashboard/components/TestTree.tsx +15 -14
  57. package/apps/dashboard/components/TipCard.tsx +177 -0
  58. package/apps/dashboard/components/Toast.tsx +5 -5
  59. package/apps/dashboard/components/TypeIcon.tsx +56 -0
  60. package/apps/dashboard/components/UpgradeBanner.tsx +30 -0
  61. package/apps/dashboard/components/WaveCompletionAnimation.tsx +61 -62
  62. package/apps/dashboard/components/WelcomeScreen.tsx +25 -27
  63. package/apps/dashboard/components/WorkItemHeader.tsx +4 -4
  64. package/apps/dashboard/components/WorkItemTree.tsx +9 -28
  65. package/apps/dashboard/components/settings/AccountSection.tsx +169 -0
  66. package/apps/dashboard/components/settings/EnvVarsSection.tsx +54 -79
  67. package/apps/dashboard/components/settings/GeneralSection.tsx +26 -31
  68. package/apps/dashboard/components/settings/SettingsLayout.tsx +4 -4
  69. package/apps/dashboard/components/ui/Button.tsx +104 -0
  70. package/apps/dashboard/components/ui/Input.tsx +78 -0
  71. package/apps/dashboard/contexts/ClaudeSessionContext.tsx +408 -105
  72. package/apps/dashboard/contexts/ConnectionStatusContext.tsx +25 -4
  73. package/apps/dashboard/contexts/UsageContext.tsx +155 -0
  74. package/apps/dashboard/contexts/usageHelpers.js +9 -0
  75. package/apps/dashboard/electron/ipc-handlers.js +281 -88
  76. package/apps/dashboard/electron/main.js +691 -131
  77. package/apps/dashboard/electron/preload.js +25 -4
  78. package/apps/dashboard/electron/session-manager.js +163 -0
  79. package/apps/dashboard/electron-builder.config.js +3 -5
  80. package/apps/dashboard/hooks/useKanbanAnimation.ts +29 -0
  81. package/apps/dashboard/hooks/useKanbanUndo.ts +83 -0
  82. package/apps/dashboard/lib/backlog-parser.ts +50 -0
  83. package/apps/dashboard/lib/claude-process-manager.ts +50 -11
  84. package/apps/dashboard/lib/constants.ts +43 -0
  85. package/apps/dashboard/lib/db-bridge.ts +33 -0
  86. package/apps/dashboard/lib/db.ts +136 -20
  87. package/apps/dashboard/lib/kanban-utils.ts +70 -0
  88. package/apps/dashboard/lib/run-migrations.js +27 -2
  89. package/apps/dashboard/lib/session-state-machine.ts +3 -0
  90. package/apps/dashboard/lib/session-stream-manager.ts +144 -38
  91. package/apps/dashboard/lib/shadows.ts +7 -0
  92. package/apps/dashboard/lib/tests.ts +3 -1
  93. package/apps/dashboard/lib/utils.ts +6 -0
  94. package/apps/dashboard/next.config.js +35 -14
  95. package/apps/dashboard/package.json +6 -3
  96. package/apps/dashboard/public/bug-icon.svg +9 -0
  97. package/apps/dashboard/public/buoy-icon.svg +9 -0
  98. package/apps/dashboard/public/fonts/Satoshi-Variable.woff2 +0 -0
  99. package/apps/dashboard/public/fonts/Satoshi-VariableItalic.woff2 +0 -0
  100. package/apps/dashboard/public/in-flight-seagull.svg +9 -0
  101. package/apps/dashboard/public/jetty-icon-loading-alt.svg +11 -0
  102. package/apps/dashboard/public/jetty-icon-loading.svg +11 -0
  103. package/apps/dashboard/public/jettypod_logo.png +0 -0
  104. package/apps/dashboard/public/pier-icon.svg +14 -0
  105. package/apps/dashboard/public/star-icon.svg +9 -0
  106. package/apps/dashboard/public/wrench-icon.svg +9 -0
  107. package/apps/dashboard/scripts/upload-to-r2.js +89 -0
  108. package/apps/dashboard/scripts/ws-server.js +191 -0
  109. package/apps/dashboard/tsconfig.tsbuildinfo +1 -0
  110. package/apps/update-server/package.json +16 -0
  111. package/apps/update-server/schema.sql +31 -0
  112. package/apps/update-server/src/index.ts +1085 -0
  113. package/apps/update-server/tsconfig.json +16 -0
  114. package/apps/update-server/wrangler.toml +35 -0
  115. package/cucumber.js +9 -3
  116. package/docs/COMMAND_REFERENCE.md +34 -0
  117. package/hooks/post-checkout +32 -75
  118. package/hooks/post-merge +111 -10
  119. package/jest.setup.js +1 -0
  120. package/jettypod.js +54 -116
  121. package/lib/chore-taxonomy.js +33 -10
  122. package/lib/database.js +36 -16
  123. package/lib/db-watcher.js +1 -1
  124. package/lib/git-hooks/pre-commit +1 -1
  125. package/lib/jettypod-backup.js +27 -4
  126. package/lib/migrations/027-plan-at-creation-column.js +33 -0
  127. package/lib/migrations/028-ready-for-review-column.js +27 -0
  128. package/lib/migrations/029-remove-autoincrement.js +307 -0
  129. package/lib/migrations/029-rename-corrupted-to-cleaned.js +149 -0
  130. package/lib/migrations/index.js +47 -4
  131. package/lib/schema.js +13 -6
  132. package/lib/seed-onboarding.js +101 -69
  133. package/lib/update-command/index.js +9 -175
  134. package/lib/work-commands/index.js +129 -16
  135. package/lib/work-tracking/index.js +86 -46
  136. package/lib/worktree-diagnostics.js +16 -16
  137. package/lib/worktree-facade.js +1 -1
  138. package/lib/worktree-manager.js +8 -8
  139. package/lib/worktree-reconciler.js +5 -5
  140. package/package.json +9 -2
  141. package/scripts/ndjson-to-cucumber-json.js +152 -0
  142. package/scripts/postinstall.js +25 -0
  143. package/skills-templates/bug-mode/SKILL.md +39 -28
  144. package/skills-templates/bug-planning/SKILL.md +25 -29
  145. package/skills-templates/chore-mode/SKILL.md +131 -68
  146. package/skills-templates/chore-mode/verification.js +51 -10
  147. package/skills-templates/chore-planning/SKILL.md +47 -18
  148. package/skills-templates/epic-planning/SKILL.md +68 -48
  149. package/skills-templates/external-transition/SKILL.md +47 -47
  150. package/skills-templates/feature-planning/SKILL.md +83 -73
  151. package/skills-templates/production-mode/SKILL.md +49 -49
  152. package/skills-templates/request-routing/SKILL.md +27 -14
  153. package/skills-templates/simple-improvement/SKILL.md +68 -44
  154. package/skills-templates/speed-mode/SKILL.md +209 -128
  155. package/skills-templates/stable-mode/SKILL.md +105 -94
  156. package/templates/bdd-guidance.md +139 -0
  157. package/templates/bdd-scaffolding/wait.js +18 -0
  158. package/templates/bdd-scaffolding/world.js +19 -0
  159. package/.jettypod-backup/work.db +0 -0
  160. package/apps/dashboard/app/access-code/page.tsx +0 -110
  161. package/lib/discovery-checkpoint.js +0 -123
  162. 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' | 'content-fade' | 'video-playing' | 'card-fade' | 'complete';
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
- // Phase 1: Content fades out (0.5s)
50
- setPhase('content-fade');
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('video-playing');
64
- }, 500);
64
+ setPhase('collapsing');
65
+ }, 1500);
65
66
  timeoutRefs.current.push(t1);
66
67
 
67
- // Phase 3: After 5 seconds, card fades out (video keeps playing)
68
+ // Phase 3: After collapse transition (0.5s), fire onComplete
68
69
  const t2 = setTimeout(() => {
69
- setPhase('card-fade');
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
- }, 6500); // 5000 + 1500
81
- timeoutRefs.current.push(t3);
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 contentOpacity = phase === 'idle' ? 1 : 0;
102
- const videoOpacity = phase === 'idle' ? 0 : 1;
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
- className="relative overflow-hidden rounded-xl border"
99
+ ref={containerRef}
108
100
  style={{
109
- opacity: cardOpacity,
110
- transition: 'opacity 1.5s ease-out',
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
- {/* Wave video - positioned behind content */}
114
- <video
115
- ref={videoRef}
116
- className="absolute inset-0 w-full h-full object-cover rounded-xl"
117
- style={{
118
- opacity: videoOpacity,
119
- transition: 'opacity 0.5s ease',
120
- zIndex: 1,
121
- }}
122
- muted
123
- playsInline
124
- src="/assets/wave-completion.mp4"
125
- onError={handleVideoError}
126
- />
127
-
128
- {/* Card content - positioned above video */}
129
- <div
130
- style={{
131
- opacity: contentOpacity,
132
- transition: 'opacity 0.5s ease',
133
- position: 'relative',
134
- zIndex: 2,
135
- background: 'inherit',
136
- }}
137
- >
138
- {children}
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 p-8">
19
- <div className="max-w-md w-full space-y-8">
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
- {/* Open Project Button */}
35
- <div className="pt-4">
36
- <button
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
- className="w-full py-3 px-6 rounded-xl font-medium transition-all duration-200 hover:-translate-y-1 hover:scale-[1.01] active:translate-y-0 active:scale-100"
39
- style={{
40
- cursor: 'pointer',
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
- </button>
55
+ </Button>
58
56
  </div>
59
57
 
60
58
  {/* Recent Projects Section */}
61
- <div className="pt-8 space-y-4" data-testid="recent-projects-section">
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="border border-zinc-200 dark:border-zinc-700 rounded-lg p-4 text-zinc-500 dark:text-zinc-400 text-sm text-center">
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 rounded-lg hover:bg-zinc-50 dark:hover:bg-zinc-800 transition-colors"
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-sm text-zinc-500 dark:text-zinc-400 truncate">
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, typeIcon, typeLabel }: WorkItemHeaderProps) {
13
+ export function WorkItemHeader({ id, title, type, typeLabel }: WorkItemHeaderProps) {
14
14
  return (
15
- <div className="flex items-center gap-2 text-sm text-zinc-500 mb-1">
16
- <span>{typeIcon} {typeLabel}</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
- const typeIcons: Record<string, string> = {
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-2 py-1.5 px-2 rounded hover:bg-zinc-100 dark:hover:bg-zinc-800 transition-colors group`}
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-sm">{typeIcons[item.type] || '📄'}</span>
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 ${statusColors[item.status]}`}
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 && modeLabels[item.mode] && (
72
- <span className={`text-xs px-1.5 py-0.5 rounded ${modeLabels[item.mode].color}`}>
73
- {modeLabels[item.mode].label}
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-sm">
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
+ }