@evolve.labs/devflow 0.8.0

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 (106) hide show
  1. package/.claude/commands/agents/architect.md +1162 -0
  2. package/.claude/commands/agents/architect.meta.yaml +124 -0
  3. package/.claude/commands/agents/builder.md +1432 -0
  4. package/.claude/commands/agents/builder.meta.yaml +117 -0
  5. package/.claude/commands/agents/chronicler.md +633 -0
  6. package/.claude/commands/agents/chronicler.meta.yaml +217 -0
  7. package/.claude/commands/agents/guardian.md +456 -0
  8. package/.claude/commands/agents/guardian.meta.yaml +127 -0
  9. package/.claude/commands/agents/strategist.md +483 -0
  10. package/.claude/commands/agents/strategist.meta.yaml +158 -0
  11. package/.claude/commands/agents/system-designer.md +1137 -0
  12. package/.claude/commands/agents/system-designer.meta.yaml +156 -0
  13. package/.claude/commands/devflow-help.md +93 -0
  14. package/.claude/commands/devflow-status.md +60 -0
  15. package/.claude/commands/quick/create-adr.md +82 -0
  16. package/.claude/commands/quick/new-feature.md +57 -0
  17. package/.claude/commands/quick/security-check.md +54 -0
  18. package/.claude/commands/quick/system-design.md +58 -0
  19. package/.claude_project +52 -0
  20. package/.devflow/agents/architect.meta.yaml +122 -0
  21. package/.devflow/agents/builder.meta.yaml +116 -0
  22. package/.devflow/agents/chronicler.meta.yaml +222 -0
  23. package/.devflow/agents/guardian.meta.yaml +127 -0
  24. package/.devflow/agents/strategist.meta.yaml +158 -0
  25. package/.devflow/agents/system-designer.meta.yaml +265 -0
  26. package/.devflow/project.yaml +242 -0
  27. package/.gitignore-template +84 -0
  28. package/LICENSE +21 -0
  29. package/README.md +249 -0
  30. package/bin/devflow.js +54 -0
  31. package/lib/autopilot.js +235 -0
  32. package/lib/autopilotConstants.js +213 -0
  33. package/lib/constants.js +95 -0
  34. package/lib/init.js +200 -0
  35. package/lib/update.js +181 -0
  36. package/lib/utils.js +157 -0
  37. package/lib/web.js +119 -0
  38. package/package.json +57 -0
  39. package/web/CHANGELOG.md +192 -0
  40. package/web/README.md +156 -0
  41. package/web/app/api/autopilot/execute/route.ts +102 -0
  42. package/web/app/api/autopilot/terminal-execute/route.ts +124 -0
  43. package/web/app/api/files/route.ts +280 -0
  44. package/web/app/api/files/tree/route.ts +160 -0
  45. package/web/app/api/git/route.ts +201 -0
  46. package/web/app/api/health/route.ts +94 -0
  47. package/web/app/api/project/open/route.ts +134 -0
  48. package/web/app/api/search/route.ts +247 -0
  49. package/web/app/api/specs/route.ts +405 -0
  50. package/web/app/api/terminal/route.ts +222 -0
  51. package/web/app/globals.css +160 -0
  52. package/web/app/ide/layout.tsx +43 -0
  53. package/web/app/ide/page.tsx +216 -0
  54. package/web/app/layout.tsx +34 -0
  55. package/web/app/page.tsx +303 -0
  56. package/web/components/agents/AgentIcons.tsx +281 -0
  57. package/web/components/autopilot/AutopilotConfigModal.tsx +245 -0
  58. package/web/components/autopilot/AutopilotPanel.tsx +299 -0
  59. package/web/components/dashboard/DashboardPanel.tsx +393 -0
  60. package/web/components/editor/Breadcrumbs.tsx +134 -0
  61. package/web/components/editor/EditorPanel.tsx +120 -0
  62. package/web/components/editor/EditorTabs.tsx +229 -0
  63. package/web/components/editor/MarkdownPreview.tsx +154 -0
  64. package/web/components/editor/MermaidDiagram.tsx +113 -0
  65. package/web/components/editor/MonacoEditor.tsx +177 -0
  66. package/web/components/editor/TabContextMenu.tsx +207 -0
  67. package/web/components/git/GitPanel.tsx +534 -0
  68. package/web/components/layout/Shell.tsx +15 -0
  69. package/web/components/layout/StatusBar.tsx +100 -0
  70. package/web/components/modals/CommandPalette.tsx +393 -0
  71. package/web/components/modals/GlobalSearch.tsx +348 -0
  72. package/web/components/modals/QuickOpen.tsx +241 -0
  73. package/web/components/modals/RecentFiles.tsx +208 -0
  74. package/web/components/projects/ProjectSelector.tsx +147 -0
  75. package/web/components/settings/SettingItem.tsx +150 -0
  76. package/web/components/settings/SettingsPanel.tsx +323 -0
  77. package/web/components/specs/SpecsPanel.tsx +1091 -0
  78. package/web/components/terminal/TerminalPanel.tsx +683 -0
  79. package/web/components/ui/ContextMenu.tsx +182 -0
  80. package/web/components/ui/LoadingSpinner.tsx +66 -0
  81. package/web/components/ui/ResizeHandle.tsx +110 -0
  82. package/web/components/ui/Skeleton.tsx +108 -0
  83. package/web/components/ui/SkipLinks.tsx +37 -0
  84. package/web/components/ui/Toaster.tsx +57 -0
  85. package/web/hooks/useFocusTrap.ts +141 -0
  86. package/web/hooks/useKeyboardShortcuts.ts +169 -0
  87. package/web/hooks/useListNavigation.ts +237 -0
  88. package/web/lib/autopilotConstants.ts +213 -0
  89. package/web/lib/constants/agents.ts +67 -0
  90. package/web/lib/git.ts +339 -0
  91. package/web/lib/ptyManager.ts +191 -0
  92. package/web/lib/specsParser.ts +299 -0
  93. package/web/lib/stores/autopilotStore.ts +288 -0
  94. package/web/lib/stores/fileStore.ts +550 -0
  95. package/web/lib/stores/gitStore.ts +386 -0
  96. package/web/lib/stores/projectStore.ts +196 -0
  97. package/web/lib/stores/settingsStore.ts +126 -0
  98. package/web/lib/stores/specsStore.ts +297 -0
  99. package/web/lib/stores/uiStore.ts +175 -0
  100. package/web/lib/types/index.ts +177 -0
  101. package/web/lib/utils.ts +98 -0
  102. package/web/next.config.js +50 -0
  103. package/web/package.json +54 -0
  104. package/web/postcss.config.js +6 -0
  105. package/web/tailwind.config.ts +68 -0
  106. package/web/tsconfig.json +41 -0
@@ -0,0 +1,1091 @@
1
+ 'use client';
2
+
3
+ import { useEffect, useState, useRef, useCallback, KeyboardEvent } from 'react';
4
+ import {
5
+ FileText,
6
+ Cpu,
7
+ ListTodo,
8
+ Plus,
9
+ ChevronRight,
10
+ CheckCircle2,
11
+ Circle,
12
+ Clock,
13
+ AlertCircle,
14
+ Sparkles,
15
+ RefreshCw,
16
+ ExternalLink,
17
+ X,
18
+ Loader2,
19
+ Rocket,
20
+ } from 'lucide-react';
21
+ import type { SpecPhase, Spec, Requirement, DesignDecision, Task } from '@/lib/types';
22
+ import { useSpecsStore, type SpecProgress } from '@/lib/stores/specsStore';
23
+ import { useFileStore } from '@/lib/stores/fileStore';
24
+ import { useAutopilotStore } from '@/lib/stores/autopilotStore';
25
+ import { AGENTS } from '@/lib/constants/agents';
26
+ import { cn } from '@/lib/utils';
27
+ import { useListNavigation } from '@/hooks/useListNavigation';
28
+
29
+ interface SpecsPanelProps {
30
+ projectPaths: string[];
31
+ activeProjectPath: string | null;
32
+ }
33
+
34
+ // Workflow phases
35
+ const PHASES: { id: SpecPhase; name: string; icon: React.ReactNode; color: string }[] = [
36
+ { id: 'requirements', name: 'Requirements', icon: <FileText className="w-4 h-4" />, color: 'text-blue-400' },
37
+ { id: 'design', name: 'Design', icon: <Cpu className="w-4 h-4" />, color: 'text-purple-400' },
38
+ { id: 'tasks', name: 'Tasks', icon: <ListTodo className="w-4 h-4" />, color: 'text-amber-400' },
39
+ ];
40
+
41
+ export function SpecsPanel({ projectPaths, activeProjectPath }: SpecsPanelProps) {
42
+ const [showCreateModal, setShowCreateModal] = useState(false);
43
+
44
+ const {
45
+ specs,
46
+ isLoading,
47
+ error,
48
+ activePhase,
49
+ selectedSpecId,
50
+ filterProject,
51
+ loadSpecs,
52
+ setActivePhase,
53
+ setSelectedSpec,
54
+ setFilterProject,
55
+ getFilteredRequirements,
56
+ getFilteredDecisions,
57
+ getFilteredTasks,
58
+ } = useSpecsStore();
59
+
60
+ const { openFile } = useFileStore();
61
+ const { status: autopilotStatus } = useAutopilotStore();
62
+
63
+ // Load specs when project paths change
64
+ useEffect(() => {
65
+ if (projectPaths.length > 0) {
66
+ loadSpecs(projectPaths);
67
+ }
68
+ }, [projectPaths.join(','), loadSpecs]);
69
+
70
+ // Refresh specs when autopilot completes
71
+ useEffect(() => {
72
+ if (autopilotStatus === 'completed' || autopilotStatus === 'failed') {
73
+ const timer = setTimeout(() => {
74
+ loadSpecs(projectPaths);
75
+ }, 1000);
76
+ return () => clearTimeout(timer);
77
+ }
78
+ }, [autopilotStatus, projectPaths, loadSpecs]);
79
+
80
+ const handleOpenSpec = (spec: Spec) => {
81
+ if (spec.filePath) {
82
+ openFile(spec.filePath);
83
+ }
84
+ };
85
+
86
+ // Get filtered data
87
+ const requirements = getFilteredRequirements();
88
+ const decisions = getFilteredDecisions();
89
+ const tasks = getFilteredTasks();
90
+
91
+ // Filter specs by phase and project
92
+ const requirementSpecs = specs.filter(s =>
93
+ s.phase === 'requirements' && (!filterProject || s.sourceProject === filterProject)
94
+ );
95
+ const designSpecs = specs.filter(s =>
96
+ s.phase === 'design' && (!filterProject || s.sourceProject === filterProject)
97
+ );
98
+
99
+ // Unique project names for filter
100
+ const projectNames = [...new Set(specs.map(s => s.sourceProject).filter(Boolean))] as string[];
101
+ const getProjectName = (p: string) => p.split('/').pop() || p;
102
+
103
+ return (
104
+ <div className="h-full flex flex-col bg-[#0a0a0f] text-white">
105
+ {/* Header */}
106
+ <div className="px-3 sm:px-4 py-2 sm:py-3 border-b border-white/10">
107
+ <div className="flex items-center justify-between mb-2 sm:mb-3">
108
+ <h2 className="font-semibold flex items-center gap-1.5 sm:gap-2 text-sm sm:text-base">
109
+ <Sparkles className="w-4 h-4 text-purple-400 flex-shrink-0" aria-hidden="true" />
110
+ <span className="truncate">Specs</span>
111
+ </h2>
112
+ <div className="flex items-center gap-0.5 sm:gap-1 flex-shrink-0" role="toolbar" aria-label="Specs actions">
113
+ <button
114
+ onClick={() => loadSpecs(projectPaths)}
115
+ className="p-1 sm:p-1.5 hover:bg-white/10 rounded-lg transition-colors text-gray-400 hover:text-white"
116
+ title="Refresh"
117
+ aria-label="Refresh specs"
118
+ >
119
+ <RefreshCw className={cn('w-4 h-4', isLoading && 'animate-spin')} aria-hidden="true" />
120
+ </button>
121
+ <button
122
+ onClick={() => setShowCreateModal(true)}
123
+ className="p-1 sm:p-1.5 hover:bg-white/10 rounded-lg transition-colors text-gray-400 hover:text-white"
124
+ title="New Spec"
125
+ aria-label="Create new spec"
126
+ >
127
+ <Plus className="w-4 h-4" aria-hidden="true" />
128
+ </button>
129
+ </div>
130
+ </div>
131
+
132
+ {/* Project Filter - only show with multiple projects */}
133
+ {projectNames.length > 1 && (
134
+ <div className="flex gap-1 mb-2 overflow-x-auto scrollbar-hide">
135
+ <button
136
+ onClick={() => setFilterProject(null)}
137
+ className={cn(
138
+ 'px-2 py-1 rounded-md text-[10px] font-medium transition-all whitespace-nowrap',
139
+ !filterProject
140
+ ? 'bg-purple-500/20 text-purple-400 border border-purple-500/30'
141
+ : 'bg-white/5 text-gray-400 hover:bg-white/10 border border-transparent'
142
+ )}
143
+ >
144
+ All
145
+ </button>
146
+ {projectNames.map((p) => (
147
+ <button
148
+ key={p}
149
+ onClick={() => setFilterProject(filterProject === p ? null : p)}
150
+ className={cn(
151
+ 'px-2 py-1 rounded-md text-[10px] font-medium transition-all whitespace-nowrap',
152
+ filterProject === p
153
+ ? 'bg-purple-500/20 text-purple-400 border border-purple-500/30'
154
+ : 'bg-white/5 text-gray-400 hover:bg-white/10 border border-transparent'
155
+ )}
156
+ >
157
+ {getProjectName(p)}
158
+ </button>
159
+ ))}
160
+ </div>
161
+ )}
162
+
163
+ {/* Phase Tabs - Responsive */}
164
+ <div
165
+ className="flex gap-1 bg-white/5 rounded-lg p-1 overflow-x-auto scrollbar-hide"
166
+ role="tablist"
167
+ aria-label="Spec phases"
168
+ >
169
+ {PHASES.map((phase) => {
170
+ const count = phase.id === 'requirements'
171
+ ? requirements.length
172
+ : phase.id === 'design'
173
+ ? decisions.length
174
+ : tasks.length;
175
+
176
+ return (
177
+ <button
178
+ key={phase.id}
179
+ onClick={() => setActivePhase(phase.id)}
180
+ role="tab"
181
+ aria-selected={activePhase === phase.id}
182
+ aria-controls={`${phase.id}-panel`}
183
+ id={`${phase.id}-tab`}
184
+ className={cn(
185
+ 'flex-1 min-w-0 flex items-center justify-center gap-1 px-2 py-1.5 rounded-md text-xs font-medium transition-all whitespace-nowrap',
186
+ activePhase === phase.id
187
+ ? 'bg-white/10 text-white'
188
+ : 'text-gray-400 hover:text-white hover:bg-white/5'
189
+ )}
190
+ >
191
+ <span className={cn('flex-shrink-0', phase.color)} aria-hidden="true">{phase.icon}</span>
192
+ <span className="hidden sm:inline truncate">{phase.name}</span>
193
+ {count > 0 && (
194
+ <span className="flex-shrink-0 px-1.5 py-0.5 bg-white/10 rounded text-[10px]" aria-label={`${count} items`}>
195
+ {count}
196
+ </span>
197
+ )}
198
+ </button>
199
+ );
200
+ })}
201
+ </div>
202
+ </div>
203
+
204
+ {/* Error Message */}
205
+ {error && (
206
+ <div className="mx-3 sm:mx-4 mt-2 sm:mt-3 p-2 sm:p-3 bg-red-500/10 border border-red-500/20 rounded-lg text-xs sm:text-sm text-red-400">
207
+ {error}
208
+ </div>
209
+ )}
210
+
211
+ {/* Content */}
212
+ <div
213
+ className="flex-1 overflow-auto p-3 sm:p-4"
214
+ role="tabpanel"
215
+ id={`${activePhase}-panel`}
216
+ aria-labelledby={`${activePhase}-tab`}
217
+ >
218
+ {isLoading ? (
219
+ <div className="h-full flex items-center justify-center" aria-label="Loading specs">
220
+ <RefreshCw className="w-6 h-6 animate-spin text-purple-400" aria-hidden="true" />
221
+ </div>
222
+ ) : (
223
+ <>
224
+ {activePhase === 'requirements' && (
225
+ <RequirementsView
226
+ requirements={requirements}
227
+ specs={requirementSpecs}
228
+ projectPath={activeProjectPath || projectPaths[0] || ''}
229
+ onOpenSpec={handleOpenSpec}
230
+ onCreateNew={() => setShowCreateModal(true)}
231
+ showProjectBadge={projectPaths.length > 1}
232
+ />
233
+ )}
234
+ {activePhase === 'design' && (
235
+ <DesignView
236
+ decisions={decisions}
237
+ specs={designSpecs}
238
+ onOpenSpec={handleOpenSpec}
239
+ onCreateNew={() => setShowCreateModal(true)}
240
+ showProjectBadge={projectPaths.length > 1}
241
+ />
242
+ )}
243
+ {activePhase === 'tasks' && (
244
+ <TasksView
245
+ tasks={tasks}
246
+ onCreateNew={() => setShowCreateModal(true)}
247
+ showProjectBadge={projectPaths.length > 1}
248
+ />
249
+ )}
250
+ </>
251
+ )}
252
+ </div>
253
+
254
+ {/* Create Modal */}
255
+ {showCreateModal && (
256
+ <CreateSpecModal
257
+ projectPath={activeProjectPath || projectPaths[0] || ''}
258
+ activePhase={activePhase}
259
+ onClose={() => setShowCreateModal(false)}
260
+ />
261
+ )}
262
+ </div>
263
+ );
264
+ }
265
+
266
+ // Progress Bar Component
267
+ function ProgressBar({ progress }: { progress: SpecProgress }) {
268
+ if (progress.total === 0) return null;
269
+
270
+ const getBarColor = () => {
271
+ if (progress.status === 'completed') return 'bg-green-500';
272
+ if (progress.percentage >= 50) return 'bg-blue-500';
273
+ return 'bg-amber-500';
274
+ };
275
+
276
+ return (
277
+ <div className="mt-2">
278
+ <div className="flex items-center justify-between text-[10px] text-gray-500 mb-1">
279
+ <span>{progress.completed}/{progress.total} tasks</span>
280
+ <span className={cn(
281
+ progress.status === 'completed' && 'text-green-400',
282
+ progress.status === 'in_progress' && 'text-blue-400'
283
+ )}>
284
+ {progress.percentage}%
285
+ </span>
286
+ </div>
287
+ <div className="h-1.5 bg-white/10 rounded-full overflow-hidden">
288
+ <div
289
+ className={cn('h-full rounded-full transition-all duration-300', getBarColor())}
290
+ style={{ width: `${progress.percentage}%` }}
291
+ />
292
+ </div>
293
+ </div>
294
+ );
295
+ }
296
+
297
+ // Requirements View
298
+ function RequirementsView({
299
+ requirements,
300
+ specs,
301
+ projectPath,
302
+ onOpenSpec,
303
+ onCreateNew,
304
+ showProjectBadge = false,
305
+ }: {
306
+ requirements: Requirement[];
307
+ specs: Spec[];
308
+ projectPath: string;
309
+ onOpenSpec: (spec: Spec) => void;
310
+ onCreateNew: () => void;
311
+ showProjectBadge?: boolean;
312
+ }) {
313
+ const { getSpecProgress } = useSpecsStore();
314
+ const containerRef = useRef<HTMLDivElement>(null);
315
+
316
+ const handleSelect = useCallback((req: Requirement) => {
317
+ const spec = specs.find(s => s.id === req.specId);
318
+ if (spec) {
319
+ onOpenSpec(spec);
320
+ }
321
+ }, [specs, onOpenSpec]);
322
+
323
+ const { selectedIndex, handleKeyDown, isSelected } = useListNavigation({
324
+ items: requirements,
325
+ onSelect: handleSelect,
326
+ getItemText: (req) => req.title,
327
+ typeAhead: true,
328
+ });
329
+
330
+ if (requirements.length === 0) {
331
+ return (
332
+ <EmptyState
333
+ icon={<FileText className="w-8 h-8" />}
334
+ title="No Requirements Yet"
335
+ description="Start by describing what you want to build. Create user stories with acceptance criteria."
336
+ action="Create Requirement"
337
+ onAction={onCreateNew}
338
+ />
339
+ );
340
+ }
341
+
342
+ return (
343
+ <div
344
+ ref={containerRef}
345
+ className="space-y-2 sm:space-y-3 focus:outline-none"
346
+ role="listbox"
347
+ aria-label="Requirements list"
348
+ tabIndex={0}
349
+ onKeyDown={handleKeyDown}
350
+ >
351
+ {requirements.map((req, index) => {
352
+ const spec = specs.find(s => s.id === req.specId);
353
+ const progress = getSpecProgress(req.specId);
354
+ return (
355
+ <RequirementCard
356
+ key={req.id}
357
+ requirement={req}
358
+ spec={spec}
359
+ progress={progress}
360
+ projectPath={projectPath}
361
+ onClick={() => spec && onOpenSpec(spec)}
362
+ isSelected={isSelected(index)}
363
+ showProjectBadge={showProjectBadge}
364
+ />
365
+ );
366
+ })}
367
+ </div>
368
+ );
369
+ }
370
+
371
+ function RequirementCard({
372
+ requirement,
373
+ spec,
374
+ progress,
375
+ projectPath,
376
+ onClick,
377
+ isSelected = false,
378
+ showProjectBadge = false,
379
+ }: {
380
+ requirement: Requirement;
381
+ spec?: Spec;
382
+ progress: SpecProgress;
383
+ projectPath: string;
384
+ onClick: () => void;
385
+ isSelected?: boolean;
386
+ showProjectBadge?: boolean;
387
+ }) {
388
+ const cardRef = useRef<HTMLDivElement>(null);
389
+
390
+ // Scroll into view when selected
391
+ useEffect(() => {
392
+ if (isSelected && cardRef.current) {
393
+ cardRef.current.scrollIntoView({ block: 'nearest', behavior: 'smooth' });
394
+ }
395
+ }, [isSelected]);
396
+ const { openConfigModal, status: autopilotStatus, specId: autopilotSpecId } = useAutopilotStore();
397
+
398
+ const priorityColors = {
399
+ must: 'bg-red-500/20 text-red-400 border-red-500/30',
400
+ should: 'bg-amber-500/20 text-amber-400 border-amber-500/30',
401
+ could: 'bg-blue-500/20 text-blue-400 border-blue-500/30',
402
+ wont: 'bg-gray-500/20 text-gray-400 border-gray-500/30',
403
+ };
404
+
405
+ // Dynamic status based on progress
406
+ const getStatusIcon = () => {
407
+ if (progress.status === 'completed') {
408
+ return <CheckCircle2 className="w-4 h-4 text-green-400" />;
409
+ }
410
+ if (progress.status === 'in_progress') {
411
+ return <Clock className="w-4 h-4 text-blue-400" />;
412
+ }
413
+ if (requirement.status === 'approved') {
414
+ return <CheckCircle2 className="w-4 h-4 text-amber-400" />;
415
+ }
416
+ return <Circle className="w-4 h-4 text-gray-400" />;
417
+ };
418
+
419
+ // Border color based on progress
420
+ const getBorderClass = () => {
421
+ if (progress.status === 'completed') return 'border-green-500/30 bg-green-500/5';
422
+ if (progress.status === 'in_progress') return 'border-blue-500/30 bg-blue-500/5';
423
+ return 'border-white/10';
424
+ };
425
+
426
+ // Handle Autopilot button click
427
+ const handleAutopilotClick = (e: React.MouseEvent) => {
428
+ e.stopPropagation();
429
+ if (spec) {
430
+ // Read spec content from the file
431
+ const specContent = `# ${requirement.title}\n\n${requirement.description}\n\n## Acceptance Criteria\n${requirement.acceptanceCriteria.map(c => `- ${c}`).join('\n')}`;
432
+ openConfigModal(spec.id, requirement.title, specContent, spec.filePath);
433
+ }
434
+ };
435
+
436
+ const isAutopilotRunning = autopilotStatus === 'running' && autopilotSpecId === spec?.id;
437
+
438
+ return (
439
+ <div
440
+ ref={cardRef}
441
+ onClick={onClick}
442
+ className={cn(
443
+ 'p-2 sm:p-3 bg-white/5 border rounded-lg hover:border-purple-500/30 hover:bg-white/[0.07] transition-all cursor-pointer group',
444
+ getBorderClass(),
445
+ isSelected && 'ring-1 ring-inset ring-purple-500/50'
446
+ )}
447
+ role="option"
448
+ aria-selected={isSelected}
449
+ tabIndex={isSelected ? 0 : -1}
450
+ >
451
+ <div className="flex items-start gap-2 sm:gap-3">
452
+ <div className="flex-shrink-0 mt-0.5" aria-hidden="true">
453
+ {getStatusIcon()}
454
+ </div>
455
+ <div className="flex-1 min-w-0 overflow-hidden">
456
+ <div className="flex items-start sm:items-center gap-1 sm:gap-2 mb-1 flex-wrap">
457
+ <span className="font-medium text-sm text-white break-words line-clamp-2 sm:line-clamp-1">{requirement.title}</span>
458
+ <span className={cn(
459
+ 'flex-shrink-0 px-1.5 py-0.5 rounded text-[10px] font-medium border',
460
+ priorityColors[requirement.priority]
461
+ )}>
462
+ {requirement.priority.toUpperCase()}
463
+ </span>
464
+ {progress.status === 'completed' && (
465
+ <span className="flex-shrink-0 px-1.5 py-0.5 rounded text-[10px] font-medium bg-green-500/20 text-green-400 border border-green-500/30">
466
+ DONE
467
+ </span>
468
+ )}
469
+ {showProjectBadge && requirement.sourceProject && (
470
+ <span className="flex-shrink-0 px-1.5 py-0.5 rounded text-[10px] bg-white/10 text-gray-400">
471
+ {requirement.sourceProject.split('/').pop()}
472
+ </span>
473
+ )}
474
+ </div>
475
+ <p className="text-xs text-gray-400 line-clamp-2 break-words">{requirement.description}</p>
476
+
477
+ {/* Progress Bar */}
478
+ <ProgressBar progress={progress} />
479
+
480
+ {/* Autopilot Button */}
481
+ {progress.status !== 'completed' && spec && (
482
+ <div className="mt-2 flex items-center gap-2">
483
+ <button
484
+ onClick={handleAutopilotClick}
485
+ disabled={!!isAutopilotRunning}
486
+ aria-label={isAutopilotRunning ? 'Autopilot running' : 'Start Autopilot'}
487
+ className={cn(
488
+ 'flex items-center gap-1 text-xs px-2 py-1 rounded-md transition-all',
489
+ isAutopilotRunning
490
+ ? 'bg-purple-500/20 text-purple-400 cursor-wait'
491
+ : 'bg-white/10 text-gray-400 hover:bg-purple-500/20 hover:text-purple-400 opacity-0 group-hover:opacity-100'
492
+ )}
493
+ >
494
+ {isAutopilotRunning ? (
495
+ <>
496
+ <Loader2 className="w-3 h-3 animate-spin" aria-hidden="true" />
497
+ Running...
498
+ </>
499
+ ) : (
500
+ <>
501
+ <Rocket className="w-3 h-3" aria-hidden="true" />
502
+ Autopilot
503
+ </>
504
+ )}
505
+ </button>
506
+ </div>
507
+ )}
508
+
509
+ {requirement.acceptanceCriteria.length > 0 && progress.total === 0 && !spec && (
510
+ <div className="mt-2 flex items-center gap-1 text-xs text-gray-500">
511
+ <CheckCircle2 className="w-3 h-3 flex-shrink-0" aria-hidden="true" />
512
+ <span className="truncate">{requirement.acceptanceCriteria.length} criteria</span>
513
+ </div>
514
+ )}
515
+ </div>
516
+ <ExternalLink className="w-4 h-4 flex-shrink-0 text-gray-500 opacity-0 group-hover:opacity-100 transition-opacity hidden sm:block" aria-hidden="true" />
517
+ </div>
518
+ </div>
519
+ );
520
+ }
521
+
522
+ // Design View
523
+ function DesignView({
524
+ decisions,
525
+ specs,
526
+ onOpenSpec,
527
+ onCreateNew,
528
+ showProjectBadge = false,
529
+ }: {
530
+ decisions: DesignDecision[];
531
+ specs: Spec[];
532
+ onOpenSpec: (spec: Spec) => void;
533
+ onCreateNew: () => void;
534
+ showProjectBadge?: boolean;
535
+ }) {
536
+ const { getSpecProgress } = useSpecsStore();
537
+ const containerRef = useRef<HTMLDivElement>(null);
538
+
539
+ const handleSelect = useCallback((dec: DesignDecision) => {
540
+ const spec = specs.find(s => s.id === dec.specId);
541
+ if (spec) {
542
+ onOpenSpec(spec);
543
+ }
544
+ }, [specs, onOpenSpec]);
545
+
546
+ const { selectedIndex, handleKeyDown, isSelected } = useListNavigation({
547
+ items: decisions,
548
+ onSelect: handleSelect,
549
+ getItemText: (dec) => dec.title,
550
+ typeAhead: true,
551
+ });
552
+
553
+ if (decisions.length === 0) {
554
+ return (
555
+ <EmptyState
556
+ icon={<Cpu className="w-8 h-8" />}
557
+ title="No Design Decisions"
558
+ description="Create Architecture Decision Records (ADRs) to document technical choices."
559
+ action="Create ADR"
560
+ onAction={onCreateNew}
561
+ />
562
+ );
563
+ }
564
+
565
+ return (
566
+ <div
567
+ ref={containerRef}
568
+ className="space-y-2 sm:space-y-3 focus:outline-none"
569
+ role="listbox"
570
+ aria-label="Design decisions list"
571
+ tabIndex={0}
572
+ onKeyDown={handleKeyDown}
573
+ >
574
+ {decisions.map((dec, index) => {
575
+ const spec = specs.find(s => s.id === dec.specId);
576
+ const progress = getSpecProgress(dec.specId);
577
+ return (
578
+ <DecisionCard
579
+ key={dec.id}
580
+ decision={dec}
581
+ progress={progress}
582
+ onClick={() => spec && onOpenSpec(spec)}
583
+ isSelected={isSelected(index)}
584
+ showProjectBadge={showProjectBadge}
585
+ />
586
+ );
587
+ })}
588
+ </div>
589
+ );
590
+ }
591
+
592
+ function DecisionCard({
593
+ decision,
594
+ progress,
595
+ onClick,
596
+ isSelected = false,
597
+ showProjectBadge = false,
598
+ }: {
599
+ decision: DesignDecision;
600
+ progress: SpecProgress;
601
+ onClick: () => void;
602
+ isSelected?: boolean;
603
+ showProjectBadge?: boolean;
604
+ }) {
605
+ const cardRef = useRef<HTMLDivElement>(null);
606
+
607
+ // Scroll into view when selected
608
+ useEffect(() => {
609
+ if (isSelected && cardRef.current) {
610
+ cardRef.current.scrollIntoView({ block: 'nearest', behavior: 'smooth' });
611
+ }
612
+ }, [isSelected]);
613
+
614
+ const statusColors = {
615
+ proposed: 'bg-amber-500/20 text-amber-400 border-amber-500/30',
616
+ accepted: 'bg-green-500/20 text-green-400 border-green-500/30',
617
+ deprecated: 'bg-red-500/20 text-red-400 border-red-500/30',
618
+ };
619
+
620
+ // Border color based on progress
621
+ const getBorderClass = () => {
622
+ if (progress.status === 'completed') return 'border-green-500/30 bg-green-500/5';
623
+ if (progress.status === 'in_progress') return 'border-purple-500/30 bg-purple-500/5';
624
+ return 'border-white/10';
625
+ };
626
+
627
+ return (
628
+ <div
629
+ ref={cardRef}
630
+ onClick={onClick}
631
+ className={cn(
632
+ 'p-2 sm:p-3 bg-white/5 border rounded-lg hover:border-purple-500/30 hover:bg-white/[0.07] transition-all cursor-pointer group',
633
+ getBorderClass(),
634
+ isSelected && 'ring-1 ring-inset ring-purple-500/50'
635
+ )}
636
+ role="option"
637
+ aria-selected={isSelected}
638
+ tabIndex={isSelected ? 0 : -1}
639
+ >
640
+ <div className="flex items-start gap-2 sm:gap-3">
641
+ <Cpu className="w-4 h-4 text-purple-400 mt-0.5 flex-shrink-0" aria-hidden="true" />
642
+ <div className="flex-1 min-w-0 overflow-hidden">
643
+ <div className="flex items-start sm:items-center gap-1 sm:gap-2 mb-1 flex-wrap">
644
+ <span className="font-medium text-sm text-white break-words line-clamp-2 sm:line-clamp-1">{decision.title}</span>
645
+ <span className={cn(
646
+ 'flex-shrink-0 px-1.5 py-0.5 rounded text-[10px] font-medium border',
647
+ statusColors[decision.status]
648
+ )}>
649
+ {decision.status.toUpperCase()}
650
+ </span>
651
+ {progress.status === 'completed' && (
652
+ <span className="flex-shrink-0 px-1.5 py-0.5 rounded text-[10px] font-medium bg-green-500/20 text-green-400 border border-green-500/30">
653
+ DONE
654
+ </span>
655
+ )}
656
+ {showProjectBadge && decision.sourceProject && (
657
+ <span className="flex-shrink-0 px-1.5 py-0.5 rounded text-[10px] bg-white/10 text-gray-400">
658
+ {decision.sourceProject.split('/').pop()}
659
+ </span>
660
+ )}
661
+ </div>
662
+ <p className="text-xs text-gray-400 line-clamp-2 break-words">{decision.context}</p>
663
+
664
+ {/* Progress Bar */}
665
+ <ProgressBar progress={progress} />
666
+
667
+ {decision.consequences.length > 0 && progress.total === 0 && (
668
+ <div className="mt-2 flex items-center gap-1 text-xs text-gray-500">
669
+ <AlertCircle className="w-3 h-3 flex-shrink-0" aria-hidden="true" />
670
+ <span className="truncate">{decision.consequences.length} consequences</span>
671
+ </div>
672
+ )}
673
+ </div>
674
+ <ExternalLink className="w-4 h-4 flex-shrink-0 text-gray-500 opacity-0 group-hover:opacity-100 transition-opacity hidden sm:block" aria-hidden="true" />
675
+ </div>
676
+ </div>
677
+ );
678
+ }
679
+
680
+ // Tasks View
681
+ function TasksView({
682
+ tasks,
683
+ onCreateNew,
684
+ showProjectBadge = false,
685
+ }: {
686
+ tasks: Task[];
687
+ onCreateNew: () => void;
688
+ showProjectBadge?: boolean;
689
+ }) {
690
+ const { updateTaskStatus } = useSpecsStore();
691
+
692
+ if (tasks.length === 0) {
693
+ return (
694
+ <EmptyState
695
+ icon={<ListTodo className="w-8 h-8" />}
696
+ title="No Tasks Yet"
697
+ description="Tasks are extracted from your specs. Create a spec with checkbox items to see tasks here."
698
+ action="Create Spec"
699
+ onAction={onCreateNew}
700
+ />
701
+ );
702
+ }
703
+
704
+ const groupedTasks = {
705
+ in_progress: tasks.filter(t => t.status === 'in_progress'),
706
+ pending: tasks.filter(t => t.status === 'pending'),
707
+ blocked: tasks.filter(t => t.status === 'blocked'),
708
+ completed: tasks.filter(t => t.status === 'completed'),
709
+ };
710
+
711
+ return (
712
+ <div className="space-y-3 sm:space-y-4">
713
+ {groupedTasks.in_progress.length > 0 && (
714
+ <TaskGroup
715
+ title="In Progress"
716
+ tasks={groupedTasks.in_progress}
717
+ color="text-blue-400"
718
+ onToggle={(id, current) => updateTaskStatus(id, current === 'completed' ? 'pending' : 'completed')}
719
+ showProjectBadge={showProjectBadge}
720
+ />
721
+ )}
722
+ {groupedTasks.pending.length > 0 && (
723
+ <TaskGroup
724
+ title="Pending"
725
+ tasks={groupedTasks.pending}
726
+ color="text-gray-400"
727
+ onToggle={(id, current) => updateTaskStatus(id, current === 'completed' ? 'pending' : 'completed')}
728
+ showProjectBadge={showProjectBadge}
729
+ />
730
+ )}
731
+ {groupedTasks.blocked.length > 0 && (
732
+ <TaskGroup
733
+ title="Blocked"
734
+ tasks={groupedTasks.blocked}
735
+ color="text-red-400"
736
+ onToggle={(id, current) => updateTaskStatus(id, current === 'completed' ? 'pending' : 'completed')}
737
+ showProjectBadge={showProjectBadge}
738
+ />
739
+ )}
740
+ {groupedTasks.completed.length > 0 && (
741
+ <TaskGroup
742
+ title="Completed"
743
+ tasks={groupedTasks.completed}
744
+ color="text-green-400"
745
+ onToggle={(id, current) => updateTaskStatus(id, current === 'completed' ? 'pending' : 'completed')}
746
+ showProjectBadge={showProjectBadge}
747
+ />
748
+ )}
749
+ </div>
750
+ );
751
+ }
752
+
753
+ function TaskGroup({
754
+ title,
755
+ tasks,
756
+ color,
757
+ onToggle,
758
+ showProjectBadge = false,
759
+ }: {
760
+ title: string;
761
+ tasks: Task[];
762
+ color: string;
763
+ onToggle: (id: string, currentStatus: Task['status']) => void;
764
+ showProjectBadge?: boolean;
765
+ }) {
766
+ return (
767
+ <div>
768
+ <h3 className={cn('text-xs font-medium mb-1.5 sm:mb-2 flex items-center gap-1.5 sm:gap-2', color)}>
769
+ <span className="truncate">{title}</span>
770
+ <span className="flex-shrink-0 px-1.5 py-0.5 bg-white/10 rounded text-[10px]">{tasks.length}</span>
771
+ </h3>
772
+ <div className="space-y-1.5 sm:space-y-2">
773
+ {tasks.map((task) => (
774
+ <TaskCard
775
+ key={task.id}
776
+ task={task}
777
+ onToggle={onToggle}
778
+ showProjectBadge={showProjectBadge}
779
+ />
780
+ ))}
781
+ </div>
782
+ </div>
783
+ );
784
+ }
785
+
786
+ function TaskCard({
787
+ task,
788
+ onToggle,
789
+ showProjectBadge = false,
790
+ }: {
791
+ task: Task;
792
+ onToggle: (id: string, currentStatus: Task['status']) => void;
793
+ showProjectBadge?: boolean;
794
+ }) {
795
+ const priorityDots = {
796
+ low: 'bg-gray-400',
797
+ medium: 'bg-blue-400',
798
+ high: 'bg-amber-400',
799
+ critical: 'bg-red-400',
800
+ };
801
+
802
+ const agentColors: Record<string, string> = {
803
+ strategist: 'text-blue-400 bg-blue-400/10',
804
+ architect: 'text-cyan-400 bg-cyan-400/10',
805
+ builder: 'text-amber-400 bg-amber-400/10',
806
+ guardian: 'text-green-400 bg-green-400/10',
807
+ chronicler: 'text-purple-400 bg-purple-400/10',
808
+ };
809
+
810
+ const isCompleted = task.status === 'completed';
811
+ const isInProgress = task.status === 'in_progress';
812
+ const isBlocked = task.status === 'blocked';
813
+
814
+ // Status-based styling
815
+ const getStatusStyles = () => {
816
+ if (isCompleted) return 'bg-green-500/5 border-green-500/20';
817
+ if (isInProgress) return 'bg-blue-500/10 border-blue-500/30';
818
+ if (isBlocked) return 'bg-red-500/5 border-red-500/20 opacity-60';
819
+ // Pending - not yet started
820
+ return 'bg-white/5 border-white/10 hover:border-white/20';
821
+ };
822
+
823
+ return (
824
+ <div
825
+ className={cn(
826
+ 'p-2 sm:p-3 border rounded-lg transition-all group',
827
+ getStatusStyles()
828
+ )}
829
+ >
830
+ <div className="flex items-start gap-2 sm:gap-3">
831
+ <button
832
+ onClick={() => onToggle(task.id, task.status)}
833
+ className="mt-0.5 flex-shrink-0"
834
+ >
835
+ {isCompleted ? (
836
+ <CheckCircle2 className="w-4 h-4 text-green-400" />
837
+ ) : isInProgress ? (
838
+ <Loader2 className="w-4 h-4 text-blue-400 animate-spin" />
839
+ ) : isBlocked ? (
840
+ <AlertCircle className="w-4 h-4 text-red-400" />
841
+ ) : (
842
+ <Circle className="w-4 h-4 text-gray-500 hover:text-white transition-colors" />
843
+ )}
844
+ </button>
845
+ <div className="flex-1 min-w-0 overflow-hidden">
846
+ <div className="flex items-center gap-1.5 sm:gap-2">
847
+ <div className={cn('w-2 h-2 rounded-full flex-shrink-0', priorityDots[task.priority])} />
848
+ <span className={cn(
849
+ 'font-medium text-sm break-words line-clamp-2',
850
+ isCompleted ? 'line-through text-gray-500' : 'text-white'
851
+ )}>
852
+ {task.title}
853
+ </span>
854
+ </div>
855
+ {task.description && (
856
+ <p className="text-xs text-gray-500 mt-1 line-clamp-1 break-words">{task.description}</p>
857
+ )}
858
+ <div className="flex items-center gap-2 mt-2 flex-wrap">
859
+ {/* Status Badge */}
860
+ {isInProgress && (
861
+ <span className="text-xs px-2 py-0.5 rounded-full bg-blue-500/20 text-blue-400">
862
+ In Progress
863
+ </span>
864
+ )}
865
+ {isBlocked && (
866
+ <span className="text-xs px-2 py-0.5 rounded-full bg-red-500/20 text-red-400">
867
+ Blocked
868
+ </span>
869
+ )}
870
+ {isCompleted && (
871
+ <span className="text-xs px-2 py-0.5 rounded-full bg-green-500/20 text-green-400">
872
+ Done
873
+ </span>
874
+ )}
875
+ {task.status === 'pending' && (
876
+ <span className="text-xs px-2 py-0.5 rounded-full bg-gray-500/20 text-gray-400">
877
+ Pending
878
+ </span>
879
+ )}
880
+
881
+ {/* Agent Badge */}
882
+ {task.assignedAgent && (
883
+ <span className={cn(
884
+ 'text-xs px-2 py-0.5 rounded-full',
885
+ agentColors[task.assignedAgent] || 'text-gray-400 bg-gray-400/10'
886
+ )}>
887
+ @{task.assignedAgent}
888
+ </span>
889
+ )}
890
+
891
+ {/* Project Badge */}
892
+ {showProjectBadge && task.sourceProject && (
893
+ <span className="text-xs px-2 py-0.5 rounded-full bg-white/10 text-gray-400">
894
+ {task.sourceProject.split('/').pop()}
895
+ </span>
896
+ )}
897
+ </div>
898
+ </div>
899
+ </div>
900
+ </div>
901
+ );
902
+ }
903
+
904
+ // Empty State
905
+ function EmptyState({
906
+ icon,
907
+ title,
908
+ description,
909
+ action,
910
+ onAction,
911
+ }: {
912
+ icon: React.ReactNode;
913
+ title: string;
914
+ description: string;
915
+ action: string;
916
+ onAction: () => void;
917
+ }) {
918
+ return (
919
+ <div className="h-full flex flex-col items-center justify-center text-center p-4 sm:p-6">
920
+ <div className="w-12 h-12 sm:w-16 sm:h-16 bg-white/5 rounded-xl sm:rounded-2xl flex items-center justify-center text-gray-500 mb-3 sm:mb-4">
921
+ {icon}
922
+ </div>
923
+ <h3 className="font-medium mb-1 sm:mb-2 text-white text-sm sm:text-base">{title}</h3>
924
+ <p className="text-xs sm:text-sm text-gray-500 mb-3 sm:mb-4 max-w-[200px] sm:max-w-[220px]">{description}</p>
925
+ <button
926
+ onClick={onAction}
927
+ className="px-3 sm:px-4 py-1.5 sm:py-2 bg-purple-600 hover:bg-purple-500 text-white text-xs sm:text-sm font-medium rounded-lg transition-colors"
928
+ >
929
+ {action}
930
+ </button>
931
+ </div>
932
+ );
933
+ }
934
+
935
+ // Create Spec Modal
936
+ function CreateSpecModal({
937
+ projectPath,
938
+ activePhase,
939
+ onClose,
940
+ }: {
941
+ projectPath: string;
942
+ activePhase: SpecPhase;
943
+ onClose: () => void;
944
+ }) {
945
+ const [type, setType] = useState<'story' | 'adr' | 'spec'>(
946
+ activePhase === 'design' ? 'adr' : 'story'
947
+ );
948
+ const [title, setTitle] = useState('');
949
+ const [description, setDescription] = useState('');
950
+ const [priority, setPriority] = useState('should');
951
+ const [isCreating, setIsCreating] = useState(false);
952
+
953
+ const { createSpec } = useSpecsStore();
954
+ const { openFile } = useFileStore();
955
+
956
+ const handleCreate = async () => {
957
+ if (!title.trim()) return;
958
+
959
+ setIsCreating(true);
960
+
961
+ const id = await createSpec(projectPath, {
962
+ type,
963
+ title: title.trim(),
964
+ description: description.trim() || undefined,
965
+ priority,
966
+ phase: activePhase,
967
+ });
968
+
969
+ setIsCreating(false);
970
+
971
+ if (id) {
972
+ onClose();
973
+ }
974
+ };
975
+
976
+ return (
977
+ <div className="fixed inset-0 bg-black/50 backdrop-blur-sm flex items-center justify-center z-50 p-4">
978
+ <div className="w-full max-w-md bg-[#12121a] border border-white/10 rounded-xl sm:rounded-2xl shadow-xl max-h-[90vh] overflow-hidden flex flex-col">
979
+ {/* Header */}
980
+ <div className="flex items-center justify-between px-4 sm:px-6 py-3 sm:py-4 border-b border-white/10 flex-shrink-0">
981
+ <h3 className="font-semibold text-white text-sm sm:text-base">Create New Spec</h3>
982
+ <button
983
+ onClick={onClose}
984
+ className="p-1 hover:bg-white/10 rounded-lg transition-colors text-gray-400 hover:text-white"
985
+ >
986
+ <X className="w-5 h-5" />
987
+ </button>
988
+ </div>
989
+
990
+ {/* Content */}
991
+ <div className="p-4 sm:p-6 space-y-4 overflow-y-auto flex-1">
992
+ {/* Type */}
993
+ <div>
994
+ <label className="block text-xs sm:text-sm font-medium text-gray-400 mb-2">Type</label>
995
+ <div className="flex gap-1.5 sm:gap-2">
996
+ {[
997
+ { id: 'story', label: 'Story', fullLabel: 'User Story', icon: <FileText className="w-4 h-4" /> },
998
+ { id: 'adr', label: 'ADR', fullLabel: 'ADR', icon: <Cpu className="w-4 h-4" /> },
999
+ { id: 'spec', label: 'Spec', fullLabel: 'Spec', icon: <ListTodo className="w-4 h-4" /> },
1000
+ ].map((t) => (
1001
+ <button
1002
+ key={t.id}
1003
+ onClick={() => setType(t.id as 'story' | 'adr' | 'spec')}
1004
+ className={cn(
1005
+ 'flex-1 flex items-center justify-center gap-1.5 sm:gap-2 px-2 sm:px-3 py-2 rounded-lg border text-xs sm:text-sm transition-all',
1006
+ type === t.id
1007
+ ? 'bg-purple-500/20 border-purple-500/50 text-purple-300'
1008
+ : 'bg-white/5 border-white/10 text-gray-400 hover:bg-white/10'
1009
+ )}
1010
+ >
1011
+ {t.icon}
1012
+ <span className="hidden sm:inline">{t.fullLabel}</span>
1013
+ <span className="sm:hidden">{t.label}</span>
1014
+ </button>
1015
+ ))}
1016
+ </div>
1017
+ </div>
1018
+
1019
+ {/* Title */}
1020
+ <div>
1021
+ <label className="block text-xs sm:text-sm font-medium text-gray-400 mb-2">Title</label>
1022
+ <input
1023
+ type="text"
1024
+ value={title}
1025
+ onChange={(e) => setTitle(e.target.value)}
1026
+ placeholder={type === 'story' ? 'User authentication flow' : type === 'adr' ? 'Use PostgreSQL for database' : 'Feature specification'}
1027
+ className="w-full px-3 sm:px-4 py-2 sm:py-2.5 bg-white/5 border border-white/10 rounded-lg text-white text-sm placeholder-gray-500 focus:outline-none focus:border-purple-500 transition-colors"
1028
+ autoFocus
1029
+ />
1030
+ </div>
1031
+
1032
+ {/* Description */}
1033
+ <div>
1034
+ <label className="block text-xs sm:text-sm font-medium text-gray-400 mb-2">Description</label>
1035
+ <textarea
1036
+ value={description}
1037
+ onChange={(e) => setDescription(e.target.value)}
1038
+ placeholder="Brief description..."
1039
+ rows={3}
1040
+ className="w-full px-3 sm:px-4 py-2 sm:py-2.5 bg-white/5 border border-white/10 rounded-lg text-white text-sm placeholder-gray-500 focus:outline-none focus:border-purple-500 transition-colors resize-none"
1041
+ />
1042
+ </div>
1043
+
1044
+ {/* Priority (for stories) */}
1045
+ {type === 'story' && (
1046
+ <div>
1047
+ <label className="block text-xs sm:text-sm font-medium text-gray-400 mb-2">Priority</label>
1048
+ <div className="flex gap-1.5 sm:gap-2">
1049
+ {[
1050
+ { id: 'must', label: 'Must', color: 'text-red-400' },
1051
+ { id: 'should', label: 'Should', color: 'text-amber-400' },
1052
+ { id: 'could', label: 'Could', color: 'text-blue-400' },
1053
+ ].map((p) => (
1054
+ <button
1055
+ key={p.id}
1056
+ onClick={() => setPriority(p.id)}
1057
+ className={cn(
1058
+ 'flex-1 px-2 sm:px-3 py-2 rounded-lg border text-xs sm:text-sm transition-all',
1059
+ priority === p.id
1060
+ ? 'bg-white/10 border-white/20 text-white'
1061
+ : 'bg-white/5 border-white/10 text-gray-400 hover:bg-white/10'
1062
+ )}
1063
+ >
1064
+ <span className={p.color}>{p.label}</span>
1065
+ </button>
1066
+ ))}
1067
+ </div>
1068
+ </div>
1069
+ )}
1070
+ </div>
1071
+
1072
+ {/* Footer */}
1073
+ <div className="flex items-center justify-end gap-2 sm:gap-3 px-4 sm:px-6 py-3 sm:py-4 border-t border-white/10 flex-shrink-0">
1074
+ <button
1075
+ onClick={onClose}
1076
+ className="px-3 sm:px-4 py-1.5 sm:py-2 text-xs sm:text-sm text-gray-400 hover:text-white transition-colors"
1077
+ >
1078
+ Cancel
1079
+ </button>
1080
+ <button
1081
+ onClick={handleCreate}
1082
+ disabled={!title.trim() || isCreating}
1083
+ className="px-3 sm:px-4 py-1.5 sm:py-2 bg-purple-600 hover:bg-purple-500 text-white text-xs sm:text-sm font-medium rounded-lg transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
1084
+ >
1085
+ {isCreating ? 'Creating...' : 'Create'}
1086
+ </button>
1087
+ </div>
1088
+ </div>
1089
+ </div>
1090
+ );
1091
+ }