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,359 @@
1
+ import { useCallback, useRef, useEffect, useState, memo } from 'react';
2
+ import { Link } from 'react-router-dom';
3
+ import { useDroppable } from '@dnd-kit/core';
4
+ import type { WorkItem, InFlightItem } from '@/lib/db';
5
+ import { KanbanCard } from './KanbanCard';
6
+ import { useDragContext } from './DragContext';
7
+ import { DraggableCard } from './DraggableCard';
8
+ import { TypeIcon } from './TypeIcon';
9
+
10
+ // Safe bounds for display_order to prevent overflow
11
+ export const MIN_DISPLAY_ORDER = 0;
12
+ export const MAX_DISPLAY_ORDER = Number.MAX_SAFE_INTEGER - 1000;
13
+ export const DISPLAY_ORDER_INCREMENT = 10;
14
+
15
+ export interface EpicGroupProps {
16
+ epicId: number | null;
17
+ epicTitle: string | null;
18
+ items: WorkItem[];
19
+ isInFlight?: boolean;
20
+ inFlightItems?: InFlightItem[];
21
+ isDraggable?: boolean;
22
+ onTitleSave?: (id: number, newTitle: string) => Promise<void>;
23
+ onStatusChange?: (id: number, newStatus: string) => Promise<void | { success: boolean; notFound?: boolean }>;
24
+ onReject?: (id: number, reason: string) => Promise<void>;
25
+ onRestart?: (id: number) => void;
26
+ onEpicAssign?: (id: number, epicId: number | null) => Promise<void>;
27
+ onOrderChange?: (id: number, newOrder: number) => Promise<void>;
28
+ onTriggerClaude?: (id: number, title: string, type: string, conversational?: boolean, description?: string | null) => void;
29
+ activeSessionIds?: Set<string>;
30
+ onOpenSession?: (id: string) => void;
31
+ onCloseSession?: (id: string) => void;
32
+ onError?: (message: string) => void;
33
+ usageAllowed?: boolean;
34
+ // Animation state lifted to board level
35
+ animatingItemId?: number | null;
36
+ onAnimationComplete?: () => void;
37
+ }
38
+
39
+ export const EpicGroup = memo(function EpicGroup({ epicId, epicTitle, items, isInFlight = false, inFlightItems, isDraggable = true, onTitleSave, onStatusChange, onReject, onRestart, onEpicAssign, onOrderChange, onTriggerClaude, activeSessionIds, onOpenSession, onCloseSession, onError, usageAllowed = true, animatingItemId, onAnimationComplete }: EpicGroupProps) {
40
+ const containerRef = useRef<HTMLDivElement>(null);
41
+ const { isDragging, draggedItem, activeEpicZone, activeDropZone, registerEpicDropZone, unregisterEpicDropZone, getCardPositions, pointerPositionRef } = useDragContext();
42
+
43
+ // Use @dnd-kit's useDroppable for epic zone collision detection
44
+ const zoneId = epicId !== null ? `epic-${epicId}` : undefined;
45
+ const { setNodeRef } = useDroppable({
46
+ id: zoneId || 'ungrouped',
47
+ disabled: epicId === null, // Don't use droppable for ungrouped section
48
+ data: { epicId },
49
+ });
50
+
51
+ // Combine refs
52
+ const setRefs = useCallback((node: HTMLDivElement | null) => {
53
+ if (epicId !== null) {
54
+ setNodeRef(node);
55
+ }
56
+ (containerRef as React.MutableRefObject<HTMLDivElement | null>).current = node;
57
+ }, [epicId, setNodeRef]);
58
+
59
+ // Use ref for items to avoid re-registering drop zone when items change
60
+ const itemsRef = useRef(items);
61
+ itemsRef.current = items;
62
+
63
+ // Use ref for callbacks to keep drop zone registration stable
64
+ const onOrderChangeRef = useRef(onOrderChange);
65
+ onOrderChangeRef.current = onOrderChange;
66
+
67
+ // Use ref for error handler to keep reorder handler stable
68
+ const onErrorRef = useRef(onError);
69
+ onErrorRef.current = onError;
70
+
71
+ // Stable reorder handler that reads from refs
72
+ const handleEpicReorder = useCallback(async (itemId: number, pointerY: number) => {
73
+ if (!onOrderChangeRef.current) {
74
+ return;
75
+ }
76
+
77
+ const currentItems = itemsRef.current.filter(item => item.id !== itemId);
78
+ if (currentItems.length === 0) {
79
+ return;
80
+ }
81
+
82
+ // Read fresh positions from DOM (not stale cache)
83
+ const allPositions = getCardPositions();
84
+ const itemIds = new Set(currentItems.map(item => item.id));
85
+ const cardPositions = allPositions
86
+ .filter(pos => itemIds.has(pos.id))
87
+ .map(pos => ({
88
+ id: pos.id,
89
+ midY: (pos.rect.top + pos.rect.bottom) / 2,
90
+ }))
91
+ .sort((a, b) => a.midY - b.midY);
92
+
93
+ if (cardPositions.length === 0) {
94
+ return;
95
+ }
96
+
97
+ // Find insertion index based on pointer Y
98
+ let insertIndex = cardPositions.length;
99
+ for (let i = 0; i < cardPositions.length; i++) {
100
+ if (pointerY < cardPositions[i].midY) {
101
+ insertIndex = i;
102
+ break;
103
+ }
104
+ }
105
+
106
+ // Map visual positions to items for display_order midpoint calculation
107
+ const itemMap = new Map(currentItems.map(item => [item.id, item]));
108
+ const visualOrder = cardPositions.map(pos => itemMap.get(pos.id)!).filter(Boolean);
109
+
110
+ // Calculate proper midpoint display_order between surrounding items.
111
+ // Fallback uses id * INCREMENT to match the sort comparator and give proper gaps.
112
+ let newOrder: number;
113
+ if (visualOrder.length === 0) {
114
+ newOrder = DISPLAY_ORDER_INCREMENT;
115
+ } else if (insertIndex === 0) {
116
+ const firstOrder = visualOrder[0].display_order ?? visualOrder[0].id * DISPLAY_ORDER_INCREMENT;
117
+ newOrder = firstOrder - DISPLAY_ORDER_INCREMENT;
118
+ } else if (insertIndex >= visualOrder.length) {
119
+ const lastOrder = visualOrder[visualOrder.length - 1].display_order ?? visualOrder[visualOrder.length - 1].id * DISPLAY_ORDER_INCREMENT;
120
+ newOrder = lastOrder + DISPLAY_ORDER_INCREMENT;
121
+ } else {
122
+ const before = visualOrder[insertIndex - 1].display_order ?? visualOrder[insertIndex - 1].id * DISPLAY_ORDER_INCREMENT;
123
+ const after = visualOrder[insertIndex].display_order ?? visualOrder[insertIndex].id * DISPLAY_ORDER_INCREMENT;
124
+ newOrder = Math.floor((before + after) / 2);
125
+ }
126
+
127
+ newOrder = Math.max(MIN_DISPLAY_ORDER, Math.min(MAX_DISPLAY_ORDER, newOrder));
128
+
129
+ try {
130
+ await onOrderChangeRef.current(itemId, newOrder);
131
+ } catch (error) {
132
+ const errorMessage = error instanceof Error ? error.message : 'Failed to reorder item. Please try again.';
133
+ onErrorRef.current?.(errorMessage);
134
+ }
135
+ }, [getCardPositions]);
136
+
137
+ // Register as epic drop zone - stable registration that doesn't change with items
138
+ useEffect(() => {
139
+ if (!containerRef.current || !onEpicAssign || epicId === null) return;
140
+
141
+ const zoneId = `epic-${epicId}`;
142
+ registerEpicDropZone(zoneId, {
143
+ epicId,
144
+ element: containerRef.current,
145
+ onEpicAssign,
146
+ onReorder: handleEpicReorder,
147
+ });
148
+
149
+ return () => {
150
+ unregisterEpicDropZone(zoneId);
151
+ };
152
+ }, [epicId, onEpicAssign, handleEpicReorder, registerEpicDropZone, unregisterEpicDropZone]);
153
+
154
+ // Check if this epic zone is the active drop target
155
+ const isActiveTarget = activeEpicZone === `epic-${epicId}`;
156
+
157
+ // Check if the dragged item is from a different epic or same epic
158
+ const draggedItemEpicId = draggedItem ? (draggedItem.parent_id || draggedItem.epic_id) : null;
159
+ const isDifferentEpic = isDragging && draggedItem && draggedItemEpicId !== epicId;
160
+ const isSameEpic = isDragging && draggedItem && draggedItemEpicId === epicId;
161
+
162
+ // Show highlight when dragging an item from different epic over this group (indigo)
163
+ const showHighlight = isActiveTarget && isDifferentEpic;
164
+ // Show reorder highlight when dragging within same epic (purple)
165
+ const showReorderHighlight = isActiveTarget && isSameEpic;
166
+
167
+ // For ungrouped section (epicId === null)
168
+ const isUngroupedSection = epicId === null;
169
+ // Check if cursor is over this ungrouped section (not over any epic zone, but over backlog drop zone)
170
+ const isOverUngroupedSection = isUngroupedSection && !activeEpicZone && activeDropZone;
171
+
172
+ // Render the ungrouped zone when dragging from an epic (provides drop target), but only highlight when cursor is over it
173
+ const shouldRenderUngroupedZone = isUngroupedSection && isDragging && draggedItemEpicId !== null;
174
+ const showRemoveFromEpicZone = isOverUngroupedSection && isDragging && draggedItemEpicId !== null;
175
+
176
+ // Show reorder for ungrouped section when dragging an ungrouped card and cursor is over it
177
+ const showUngroupedReorder = isOverUngroupedSection && isDragging && draggedItemEpicId === null;
178
+
179
+ // Insertion preview — tracks pointer position via rAF loop so the line
180
+ // moves continuously even when DragContext change guards suppress re-renders.
181
+ const showPreview = (showReorderHighlight || showRemoveFromEpicZone || showHighlight || showUngroupedReorder) && draggedItem;
182
+ const [insertAfterItemId, setInsertAfterItemId] = useState<number | null | undefined>(undefined);
183
+
184
+ useEffect(() => {
185
+ if (!showPreview || !draggedItem) {
186
+ setInsertAfterItemId(undefined);
187
+ return;
188
+ }
189
+
190
+ // Build item ID set once — items array is stable during a drag
191
+ const itemIds = new Set(items.map(item => item.id));
192
+ const dragId = draggedItem.id;
193
+
194
+ let rafId: number;
195
+ let lastTickTime = 0;
196
+ // Reusable array to avoid allocations per frame
197
+ const groupPositions: Array<{ id: number; midY: number }> = [];
198
+
199
+ const tick = () => {
200
+ const now = performance.now();
201
+ // Throttle to ~100ms — fast enough for smooth preview, avoids layout
202
+ // thrashing on slower hardware (Intel Macs).
203
+ if (now - lastTickTime < 100) {
204
+ rafId = requestAnimationFrame(tick);
205
+ return;
206
+ }
207
+ lastTickTime = now;
208
+
209
+ const allPositions = getCardPositions();
210
+ groupPositions.length = 0;
211
+ for (const pos of allPositions) {
212
+ if (itemIds.has(pos.id) && pos.id !== dragId) {
213
+ groupPositions.push({ id: pos.id, midY: (pos.rect.top + pos.rect.bottom) / 2 });
214
+ }
215
+ }
216
+ groupPositions.sort((a, b) => a.midY - b.midY);
217
+
218
+ const currentPointerY = pointerPositionRef.current.y;
219
+ let newInsert: number | null = null;
220
+ for (const pos of groupPositions) {
221
+ if (currentPointerY > pos.midY) {
222
+ newInsert = pos.id;
223
+ } else {
224
+ break;
225
+ }
226
+ }
227
+
228
+ setInsertAfterItemId(prev => prev === newInsert ? prev : newInsert);
229
+ rafId = requestAnimationFrame(tick);
230
+ };
231
+
232
+ rafId = requestAnimationFrame(tick);
233
+ return () => cancelAnimationFrame(rafId);
234
+ }, [showPreview, draggedItem, items, getCardPositions, pointerPositionRef]);
235
+
236
+ if (items.length === 0 && !showHighlight && !showReorderHighlight && !shouldRenderUngroupedZone) return null;
237
+
238
+ // Standalone done items (single item, no epic) use tighter spacing
239
+ const isStandaloneItem = !epicTitle && items.length === 1;
240
+
241
+ return (
242
+ <div
243
+ ref={setRefs}
244
+ className={`${isStandaloneItem ? 'mb-2' : 'mb-6 p-3 -mx-3'} rounded-lg transition-[color,background-color] duration-200 ease-out ${
245
+ showHighlight
246
+ ? 'ring-2 ring-[#819D9F] bg-[#E8EEEF]/50 dark:bg-[#819D9F]/20'
247
+ : showReorderHighlight
248
+ ? 'ring-2 ring-[#E3D985] bg-[#F9F7E8]/50 dark:bg-[#E3D985]/20'
249
+ : showRemoveFromEpicZone
250
+ ? 'ring-2 ring-[#E57A44] bg-[#FCEEE6]/50 dark:bg-[#E57A44]/20'
251
+ : ''
252
+ }`}
253
+ data-epic-id={epicId}
254
+ >
255
+ {epicTitle && (
256
+ <div className="flex items-center gap-3 mb-3">
257
+ <Link
258
+ to={`/work/${epicId}`}
259
+ viewTransition
260
+ className="group/epic flex items-center gap-1.5 text-base font-medium text-zinc-500 dark:text-zinc-400 hover:text-zinc-700 dark:hover:text-zinc-300"
261
+ >
262
+ <TypeIcon type="epic" />
263
+ <span className="group-hover/epic:underline">{epicTitle}</span>
264
+ </Link>
265
+ {isInFlight && (
266
+ <span
267
+ className="relative group/inflight text-xs px-2 py-1 rounded bg-[#e8f0f0] text-[#5a7d7f] dark:bg-[#819D9F]/20 dark:text-[#a3bfc0] cursor-default"
268
+ >
269
+ in flight
270
+ {inFlightItems && inFlightItems.length > 0 && (
271
+ <span className="pointer-events-none absolute left-1/2 -translate-x-1/2 top-full mt-2 z-50 hidden group-hover/inflight:block w-max max-w-xs">
272
+ <span className="block rounded-lg bg-zinc-800 dark:bg-zinc-700 text-zinc-100 text-xs px-3 py-2 shadow-lg">
273
+ {inFlightItems.map(item => (
274
+ <span key={item.id} className="flex items-center gap-1 py-0.5 truncate">
275
+ <TypeIcon type={item.type} /> {item.title}
276
+ </span>
277
+ ))}
278
+ </span>
279
+ </span>
280
+ )}
281
+ </span>
282
+ )}
283
+ {showHighlight && (
284
+ <span className="text-xs px-2 py-1 rounded bg-[#E8EEEF] text-[#4A6365] dark:bg-[#819D9F]/20 dark:text-[#819D9F]">
285
+ drop to assign
286
+ </span>
287
+ )}
288
+ {showReorderHighlight && (
289
+ <span className="text-xs px-2 py-1 rounded bg-[#F9F7E8] text-[#8B7D2F] dark:bg-[#E3D985]/20 dark:text-[#E3D985]">
290
+ reorder
291
+ </span>
292
+ )}
293
+ </div>
294
+ )}
295
+ {/* Ungrouped section header - shown when dragging from epic */}
296
+ {isUngroupedSection && showRemoveFromEpicZone && items.length === 0 && (
297
+ <div className="flex items-center gap-3 py-4">
298
+ <span className="text-base font-medium text-[#E57A44] dark:text-[#E57A44]">
299
+ Drop here to remove from epic
300
+ </span>
301
+ </div>
302
+ )}
303
+ {isUngroupedSection && items.length > 0 && isDraggable && showRemoveFromEpicZone && (
304
+ <div className="flex items-center gap-3 mb-3">
305
+ <span className="text-xs px-2 py-1 rounded bg-[#FCEEE6] text-[#9E4A1E] dark:bg-[#E57A44]/20 dark:text-[#E57A44]">
306
+ drop to remove from epic
307
+ </span>
308
+ </div>
309
+ )}
310
+ <div className="space-y-3">
311
+ {items.map((item, index) => (
312
+ <div key={item.id}>
313
+ {/* CSS insertion indicator — replaces AnimatePresence+PlaceholderCard for less overhead */}
314
+ {index === 0 && insertAfterItemId === null && (
315
+ <div
316
+ data-testid="drag-placeholder"
317
+ style={{
318
+ height: 3,
319
+ borderRadius: 2,
320
+ background: 'linear-gradient(90deg, transparent, #819D9F, transparent)',
321
+ margin: '2px 8px 8px',
322
+ }}
323
+ />
324
+ )}
325
+ <DraggableCard item={item} disabled={!isDraggable}>
326
+ <KanbanCard
327
+ item={item}
328
+ onTitleSave={onTitleSave}
329
+ onStatusChange={onStatusChange}
330
+ onReject={onReject}
331
+ onRestart={onRestart}
332
+ onTriggerClaude={onTriggerClaude}
333
+ hasActiveSession={activeSessionIds?.has(String(item.id))}
334
+ onOpenSession={onOpenSession}
335
+ onCloseSession={onCloseSession}
336
+ usageAllowed={usageAllowed}
337
+ isCompletingAnimation={animatingItemId === item.id}
338
+ onAnimationComplete={onAnimationComplete}
339
+ isHighlighted={false}
340
+ />
341
+ </DraggableCard>
342
+ {/* CSS insertion indicator after this card */}
343
+ {insertAfterItemId === item.id && (
344
+ <div
345
+ data-testid="drag-placeholder"
346
+ style={{
347
+ height: 3,
348
+ borderRadius: 2,
349
+ background: 'linear-gradient(90deg, transparent, #819D9F, transparent)',
350
+ margin: '8px 8px 2px',
351
+ }}
352
+ />
353
+ )}
354
+ </div>
355
+ ))}
356
+ </div>
357
+ </div>
358
+ );
359
+ });
@@ -1,4 +1,3 @@
1
- 'use client';
2
1
 
3
2
  import { useState } from 'react';
4
3
  import type { ClaudeMessage } from '../lib/session-stream-manager';
@@ -6,6 +5,7 @@ import { GateChoiceCard } from './GateChoiceCard';
6
5
  import type { ChoiceOption } from './GateChoiceCard';
7
6
  import { ModeStartCard, isModeStartGate } from './ModeStartCard';
8
7
  import { TipCard } from './TipCard';
8
+ import { TypeIcon } from './TypeIcon';
9
9
 
10
10
  // Gate display configuration matching JettyPod design system
11
11
  const GATE_CONFIG: Record<string, {
@@ -90,11 +90,11 @@ const GATE_CONFIG: Record<string, {
90
90
  label: 'Saving Changes',
91
91
  bg: 'bg-white',
92
92
  darkBg: 'dark:bg-zinc-800',
93
- border: 'border-blue-200',
94
- darkBorder: 'dark:border-blue-800',
93
+ border: 'border-[#819D9F]/30',
94
+ darkBorder: 'dark:border-[#819D9F]/30',
95
95
  text: 'text-zinc-700',
96
96
  darkText: 'dark:text-zinc-300',
97
- fileMono: 'text-blue-600',
97
+ fileMono: 'text-[#5a7d7f]',
98
98
  },
99
99
  'complete': {
100
100
  emoji: '✨',
@@ -118,6 +118,17 @@ const GATE_CONFIG: Record<string, {
118
118
  darkText: 'dark:text-zinc-300',
119
119
  fileMono: 'text-indigo-600',
120
120
  },
121
+ 'work-item-card': {
122
+ emoji: '📋',
123
+ label: 'Added to Backlog',
124
+ bg: 'bg-white',
125
+ darkBg: 'dark:bg-zinc-800',
126
+ border: 'border-emerald-200',
127
+ darkBorder: 'dark:border-emerald-800',
128
+ text: 'text-zinc-700',
129
+ darkText: 'dark:text-zinc-300',
130
+ fileMono: 'text-emerald-600',
131
+ },
121
132
  'tip': {
122
133
  emoji: '💡',
123
134
  label: 'Tip',
@@ -129,6 +140,17 @@ const GATE_CONFIG: Record<string, {
129
140
  darkText: 'dark:text-zinc-300',
130
141
  fileMono: 'text-teal-600',
131
142
  },
143
+ 'rejection': {
144
+ emoji: '❌',
145
+ label: 'Work Rejected',
146
+ bg: 'bg-red-50',
147
+ darkBg: 'dark:bg-red-900/20',
148
+ border: 'border-red-200',
149
+ darkBorder: 'dark:border-red-800',
150
+ text: 'text-zinc-700',
151
+ darkText: 'dark:text-zinc-300',
152
+ fileMono: 'text-red-600',
153
+ },
132
154
  };
133
155
 
134
156
  const DEFAULT_CONFIG = {
@@ -143,9 +165,7 @@ const DEFAULT_CONFIG = {
143
165
  fileMono: 'text-zinc-500',
144
166
  };
145
167
 
146
- // Multi-layer shadow matching kanban card elevation
147
- const CARD_SHADOW = '0 1px 2px rgba(0,0,0,0.03), 0 2px 4px rgba(0,0,0,0.03), 0 4px 8px rgba(0,0,0,0.02)';
148
- const CARD_SHADOW_ACTIVE = '0 2px 4px rgba(0,0,0,0.04), 0 4px 8px rgba(0,0,0,0.04), 0 8px 16px rgba(0,0,0,0.03)';
168
+ import { shadow } from '@/lib/shadows';
149
169
 
150
170
  /**
151
171
  * Render a human-friendly description based on gate type and data
@@ -163,7 +183,8 @@ function getGateDescription(gateType: string, data: Record<string, unknown>): st
163
183
  };
164
184
  return routeLabels[route || ''] || `Routing to ${route || 'workflow'}`;
165
185
  }
166
- case 'work-created': {
186
+ case 'work-created':
187
+ case 'work-item-card': {
167
188
  const title = data.title as string | undefined;
168
189
  const id = data.id as number | undefined;
169
190
  return title ? `#${id || '?'} ${title}` : 'Work item created';
@@ -195,6 +216,10 @@ function getGateDescription(gateType: string, data: Record<string, unknown>): st
195
216
  const question = data.question as string | undefined;
196
217
  return question || 'A decision is needed';
197
218
  }
219
+ case 'rejection': {
220
+ const reason = data.reason as string | undefined;
221
+ return reason || 'Work was rejected';
222
+ }
198
223
  default:
199
224
  return (data.message as string) || gateType;
200
225
  }
@@ -205,9 +230,10 @@ interface GateCardProps {
205
230
  isLatest?: boolean;
206
231
  onAnswerQuestion?: (optionId: string, optionLabel: string) => void;
207
232
  answeredQuestionId?: string | null;
233
+ onStartWorkItem?: (id: number, title: string, type: string) => void;
208
234
  }
209
235
 
210
- export function GateCard({ message, isLatest = false, onAnswerQuestion, answeredQuestionId }: GateCardProps) {
236
+ export function GateCard({ message, isLatest = false, onAnswerQuestion, answeredQuestionId, onStartWorkItem }: GateCardProps) {
211
237
  const gateType = message.gateType || 'unknown';
212
238
  const gateData = message.gateData || {};
213
239
  const config = GATE_CONFIG[gateType] || DEFAULT_CONFIG;
@@ -228,6 +254,42 @@ export function GateCard({ message, isLatest = false, onAnswerQuestion, answered
228
254
  return <TipCard tipId={tipId} icon={icon} title={title} body={body} />;
229
255
  }
230
256
 
257
+ // Work item card gates render as mini kanban cards with Start button
258
+ if (gateType === 'work-item-card') {
259
+ const itemId = gateData.id as number;
260
+ const itemTitle = gateData.title as string;
261
+ const itemType = (gateData.type as string) || 'feature';
262
+ return (
263
+ <div
264
+ className="bg-white dark:bg-zinc-800 border-2 border-emerald-200 dark:border-emerald-800 rounded-xl p-4 transition-shadow duration-200 ease-out"
265
+ style={{ boxShadow: shadow.sm }}
266
+ data-testid="gate-work-item-card"
267
+ >
268
+ <div className="flex items-center justify-between gap-3">
269
+ <div className="flex items-center gap-3 min-w-0">
270
+ <span className="text-base flex-shrink-0"><TypeIcon type={itemType} /></span>
271
+ <div className="min-w-0">
272
+ <span className="text-xs font-medium text-zinc-400 dark:text-zinc-500">
273
+ #{itemId} · {itemType}
274
+ </span>
275
+ <p className="text-sm font-semibold text-zinc-800 dark:text-zinc-200 truncate">
276
+ {itemTitle}
277
+ </p>
278
+ </div>
279
+ </div>
280
+ {onStartWorkItem && (
281
+ <button
282
+ onClick={() => onStartWorkItem(itemId, itemTitle, itemType)}
283
+ className="flex-shrink-0 px-3 py-1.5 text-xs font-medium rounded-lg bg-[#819D9F] hover:bg-[#6b8587] text-white transition-colors duration-150"
284
+ >
285
+ Start
286
+ </button>
287
+ )}
288
+ </div>
289
+ </div>
290
+ );
291
+ }
292
+
231
293
  // Question gates render as interactive choice cards
232
294
  if (gateType === 'question') {
233
295
  const question = (gateData.question as string) || 'A decision is needed';
@@ -259,24 +321,24 @@ export function GateCard({ message, isLatest = false, onAnswerQuestion, answered
259
321
  <div
260
322
  className={`
261
323
  ${config.bg} ${config.darkBg}
262
- border ${config.border} ${config.darkBorder}
263
- rounded-xl p-3 transition-all duration-200
264
- ${isLatest ? 'ring-2 ring-blue-400/50 ring-offset-1' : ''}
324
+ border-2 ${config.border} ${config.darkBorder}
325
+ rounded-xl p-4 transition-shadow duration-200 ease-out
326
+ ${isLatest ? 'ring-2 ring-[#819D9F]/50 ring-offset-1' : ''}
265
327
  `}
266
- style={{ boxShadow: isLatest ? CARD_SHADOW_ACTIVE : CARD_SHADOW }}
328
+ style={{ boxShadow: isLatest ? shadow.md : shadow.sm }}
267
329
  data-testid={`gate-card-${gateType}`}
268
330
  >
269
- <div className="flex items-start gap-3">
331
+ <div className="flex items-start gap-4">
270
332
  <span className="text-base flex-shrink-0 mt-0.5">{config.emoji}</span>
271
333
  <div className="flex-1 min-w-0">
272
- <span className={`text-xs font-semibold ${config.text} ${config.darkText}`}>
334
+ <span className={`text-base font-semibold ${config.text} ${config.darkText}`}>
273
335
  {config.label}
274
336
  </span>
275
- <p className={`text-sm mt-0.5 ${config.text} ${config.darkText}`}>
337
+ <p className={`text-base mt-1 ${config.text} ${config.darkText}`}>
276
338
  {description}
277
339
  </p>
278
340
  {hasFiles && (
279
- <div className="mt-2 space-y-0.5">
341
+ <div className="mt-3 space-y-1">
280
342
  {(gateData.files as string[]).map((file, i) => (
281
343
  <div key={i} className={`text-xs font-mono truncate ${config.fileMono}`}>
282
344
  {file}
@@ -1,10 +1,7 @@
1
- 'use client';
2
1
 
3
2
  import { useState } from 'react';
4
3
 
5
- // Multi-layer shadow matching kanban card system
6
- const CARD_SHADOW = '0 1px 2px rgba(0,0,0,0.03), 0 2px 4px rgba(0,0,0,0.03), 0 4px 8px rgba(0,0,0,0.02)';
7
- const CARD_SHADOW_HOVER = '0 2px 4px rgba(0,0,0,0.04), 0 4px 8px rgba(0,0,0,0.04), 0 8px 16px rgba(0,0,0,0.03), 0 12px 24px rgba(129,157,159,0.08)';
4
+ import { shadow } from '@/lib/shadows';
8
5
 
9
6
  export interface ChoiceOption {
10
7
  id: string;
@@ -32,22 +29,22 @@ export function GateChoiceCard({
32
29
 
33
30
  return (
34
31
  <div
35
- className="bg-white dark:bg-zinc-800 border border-zinc-200 dark:border-zinc-700 rounded-xl p-3 transition-all duration-200"
36
- style={{ boxShadow: CARD_SHADOW }}
32
+ className="bg-white dark:bg-zinc-800 rounded-xl p-4 transition-shadow duration-200 ease-out"
33
+ style={{ boxShadow: shadow.sm }}
37
34
  data-testid="gate-choice-card"
38
35
  >
39
- <div className="flex items-start gap-3">
36
+ <div className="flex items-start gap-4">
40
37
  <span className="text-base flex-shrink-0 mt-0.5">💬</span>
41
38
  <div className="flex-1 min-w-0">
42
- <span className="text-xs font-semibold text-zinc-700 dark:text-zinc-300">
39
+ <span className="text-base font-semibold text-zinc-700 dark:text-zinc-300">
43
40
  Input Needed
44
41
  </span>
45
- <p className="text-sm mt-0.5 text-zinc-700 dark:text-zinc-300">
42
+ <p className="text-base mt-1 text-zinc-700 dark:text-zinc-300">
46
43
  {question}
47
44
  </p>
48
45
 
49
46
  {/* Option cards */}
50
- <div className="mt-3 space-y-2">
47
+ <div className="mt-4 space-y-3">
51
48
  {options.map((option) => {
52
49
  const isSelected = selectedId === option.id;
53
50
  const isHovered = hoveredId === option.id;
@@ -60,35 +57,35 @@ export function GateChoiceCard({
60
57
  onMouseLeave={() => setHoveredId(null)}
61
58
  disabled={disabled && !isSelected}
62
59
  className={`
63
- w-full text-left rounded-xl border p-3 transition-all duration-200
60
+ w-full text-left rounded-xl border-2 p-4 transition-[color,background-color,border-color] duration-200 ease-out
64
61
  ${isSelected
65
- ? 'border-blue-400 dark:border-blue-500 bg-blue-50 dark:bg-blue-900/20 ring-2 ring-blue-400/30'
62
+ ? 'border-[#819D9F] dark:border-[#819D9F] bg-[#e8f0f0] dark:bg-[#819D9F]/20 ring-2 ring-[#819D9F]/30'
66
63
  : 'border-zinc-200 dark:border-zinc-700 bg-white dark:bg-zinc-800 hover:border-zinc-300 dark:hover:border-zinc-600'
67
64
  }
68
65
  ${disabled && !isSelected ? 'opacity-50 cursor-default' : 'cursor-pointer'}
69
66
  ${!disabled && !isSelected ? 'hover:-translate-y-0.5' : ''}
70
67
  `}
71
68
  style={{
72
- boxShadow: isSelected ? CARD_SHADOW_HOVER : isHovered && !disabled ? CARD_SHADOW_HOVER : CARD_SHADOW,
69
+ boxShadow: isSelected ? shadow.lg : isHovered && !disabled ? shadow.lg : shadow.sm,
73
70
  }}
74
71
  data-testid={`choice-option-${option.id}`}
75
72
  >
76
- <div className="flex items-start gap-3">
73
+ <div className="flex items-start gap-4">
77
74
  {option.emoji && (
78
75
  <span className="text-base flex-shrink-0">{option.emoji}</span>
79
76
  )}
80
77
  <div className="flex-1 min-w-0">
81
- <div className="flex items-center gap-2">
82
- <span className={`text-sm font-medium ${isSelected ? 'text-blue-700 dark:text-blue-300' : 'text-zinc-900 dark:text-zinc-100'}`}>
78
+ <div className="flex items-center gap-3">
79
+ <span className={`text-base font-medium ${isSelected ? 'text-[#5a7d7f] dark:text-[#a3bfc0]' : 'text-zinc-900 dark:text-zinc-100'}`}>
83
80
  {option.label}
84
81
  </span>
85
82
  {isSelected && (
86
- <svg className="w-4 h-4 text-blue-500 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
83
+ <svg className="w-4 h-4 text-[#819D9F] flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
87
84
  <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
88
85
  </svg>
89
86
  )}
90
87
  </div>
91
- <p className={`text-xs mt-0.5 ${isSelected ? 'text-blue-600/70 dark:text-blue-400/70' : 'text-zinc-500 dark:text-zinc-400'}`}>
88
+ <p className={`text-base mt-1 ${isSelected ? 'text-[#5a7d7f]/70 dark:text-[#a3bfc0]/70' : 'text-zinc-500 dark:text-zinc-400'}`}>
92
89
  {option.description}
93
90
  </p>
94
91
  </div>