kanban-lite 1.0.4

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 (99) hide show
  1. package/.editorconfig +9 -0
  2. package/.github/workflows/ci.yml +59 -0
  3. package/.github/workflows/release.yml +75 -0
  4. package/.prettierignore +6 -0
  5. package/.prettierrc.yaml +4 -0
  6. package/.vscode/extensions.json +3 -0
  7. package/.vscode/launch.json +17 -0
  8. package/.vscode/settings.json +21 -0
  9. package/.vscode/tasks.json +22 -0
  10. package/.vscodeignore +11 -0
  11. package/CHANGELOG.md +184 -0
  12. package/CLAUDE.md +58 -0
  13. package/CONTRIBUTING.md +114 -0
  14. package/LICENSE +22 -0
  15. package/README.md +482 -0
  16. package/SKILL.md +237 -0
  17. package/dist/cli.js +8716 -0
  18. package/dist/extension.js +8463 -0
  19. package/dist/mcp-server.js +1327 -0
  20. package/dist/standalone-webview/icons-Dx9MGYqN.js +180 -0
  21. package/dist/standalone-webview/icons-Dx9MGYqN.js.map +1 -0
  22. package/dist/standalone-webview/index.js +85 -0
  23. package/dist/standalone-webview/index.js.map +1 -0
  24. package/dist/standalone-webview/react-vendor-DkYdDBET.js +25 -0
  25. package/dist/standalone-webview/react-vendor-DkYdDBET.js.map +1 -0
  26. package/dist/standalone-webview/style.css +1 -0
  27. package/dist/standalone.js +7513 -0
  28. package/dist/webview/icons-Dx9MGYqN.js +180 -0
  29. package/dist/webview/icons-Dx9MGYqN.js.map +1 -0
  30. package/dist/webview/index.js +85 -0
  31. package/dist/webview/index.js.map +1 -0
  32. package/dist/webview/react-vendor-DkYdDBET.js +25 -0
  33. package/dist/webview/react-vendor-DkYdDBET.js.map +1 -0
  34. package/dist/webview/style.css +1 -0
  35. package/docs/images/board-overview.png +0 -0
  36. package/docs/images/editor-view.png +0 -0
  37. package/docs/plans/2026-02-20-kanban-json-config-design.md +74 -0
  38. package/docs/plans/2026-02-20-kanban-json-config.md +690 -0
  39. package/eslint.config.mjs +31 -0
  40. package/package.json +161 -0
  41. package/postcss.config.js +6 -0
  42. package/resources/icon-light.png +0 -0
  43. package/resources/icon-light.svg +105 -0
  44. package/resources/icon.png +0 -0
  45. package/resources/icon.svg +105 -0
  46. package/resources/kanban-dark.svg +21 -0
  47. package/resources/kanban-light.svg +21 -0
  48. package/resources/kanban.svg +21 -0
  49. package/src/cli/index.ts +846 -0
  50. package/src/extension/FeatureHeaderProvider.ts +370 -0
  51. package/src/extension/KanbanPanel.ts +973 -0
  52. package/src/extension/SidebarViewProvider.ts +507 -0
  53. package/src/extension/featureFileUtils.ts +82 -0
  54. package/src/extension/index.ts +234 -0
  55. package/src/mcp-server/index.ts +632 -0
  56. package/src/sdk/KanbanSDK.ts +349 -0
  57. package/src/sdk/__tests__/KanbanSDK.test.ts +468 -0
  58. package/src/sdk/__tests__/parser.test.ts +170 -0
  59. package/src/sdk/fileUtils.ts +76 -0
  60. package/src/sdk/index.ts +6 -0
  61. package/src/sdk/parser.ts +70 -0
  62. package/src/sdk/types.ts +15 -0
  63. package/src/shared/config.ts +113 -0
  64. package/src/shared/editorTypes.ts +14 -0
  65. package/src/shared/types.ts +120 -0
  66. package/src/standalone/__tests__/server.integration.test.ts +1916 -0
  67. package/src/standalone/__tests__/webhooks.test.ts +357 -0
  68. package/src/standalone/fileUtils.ts +70 -0
  69. package/src/standalone/index.ts +71 -0
  70. package/src/standalone/server.ts +1046 -0
  71. package/src/standalone/webhooks.ts +135 -0
  72. package/src/webview/App.tsx +469 -0
  73. package/src/webview/assets/main.css +329 -0
  74. package/src/webview/assets/standalone-theme.css +130 -0
  75. package/src/webview/components/ColumnDialog.tsx +119 -0
  76. package/src/webview/components/CreateFeatureDialog.tsx +524 -0
  77. package/src/webview/components/DatePicker.tsx +185 -0
  78. package/src/webview/components/FeatureCard.tsx +186 -0
  79. package/src/webview/components/FeatureEditor.tsx +623 -0
  80. package/src/webview/components/KanbanBoard.tsx +144 -0
  81. package/src/webview/components/KanbanColumn.tsx +159 -0
  82. package/src/webview/components/MarkdownEditor.tsx +291 -0
  83. package/src/webview/components/PrioritySelect.tsx +39 -0
  84. package/src/webview/components/QuickAddInput.tsx +72 -0
  85. package/src/webview/components/SettingsPanel.tsx +284 -0
  86. package/src/webview/components/Toolbar.tsx +175 -0
  87. package/src/webview/components/UndoToast.tsx +70 -0
  88. package/src/webview/index.html +12 -0
  89. package/src/webview/lib/utils.ts +6 -0
  90. package/src/webview/main.tsx +11 -0
  91. package/src/webview/standalone-main.tsx +13 -0
  92. package/src/webview/standalone-shim.ts +132 -0
  93. package/src/webview/standalone.html +12 -0
  94. package/src/webview/store/index.ts +241 -0
  95. package/tailwind.config.js +53 -0
  96. package/tsconfig.json +22 -0
  97. package/vite.config.ts +36 -0
  98. package/vite.standalone.config.ts +62 -0
  99. package/vitest.config.ts +15 -0
@@ -0,0 +1,524 @@
1
+ import { useState, useEffect, useRef, useMemo } from 'react'
2
+ import { X, ChevronDown, User, Tag, Check, CircleDot, Signal, Calendar } from 'lucide-react'
3
+ import type { FeatureStatus, Priority } from '../../shared/types'
4
+ import { useStore } from '../store'
5
+ import { cn } from '../lib/utils'
6
+ import { DatePicker } from './DatePicker'
7
+ import { MarkdownEditor } from './MarkdownEditor'
8
+
9
+ interface CreateFeatureDialogProps {
10
+ isOpen: boolean
11
+ onClose: () => void
12
+ onCreate: (data: { status: FeatureStatus; priority: Priority; content: string; assignee: string | null; dueDate: string | null; labels: string[] }) => void
13
+ initialStatus?: FeatureStatus
14
+ }
15
+
16
+ const priorityConfig: { value: Priority; label: string; dot: string }[] = [
17
+ { value: 'critical', label: 'Critical', dot: 'bg-red-500' },
18
+ { value: 'high', label: 'High', dot: 'bg-orange-500' },
19
+ { value: 'medium', label: 'Medium', dot: 'bg-yellow-500' },
20
+ { value: 'low', label: 'Low', dot: 'bg-green-500' }
21
+ ]
22
+
23
+ const statusConfig: { value: FeatureStatus; label: string; dot: string }[] = [
24
+ { value: 'backlog', label: 'Backlog', dot: 'bg-zinc-400' },
25
+ { value: 'todo', label: 'To Do', dot: 'bg-blue-400' },
26
+ { value: 'in-progress', label: 'In Progress', dot: 'bg-amber-400' },
27
+ { value: 'review', label: 'Review', dot: 'bg-purple-400' },
28
+ { value: 'done', label: 'Done', dot: 'bg-emerald-400' }
29
+ ]
30
+
31
+ interface DropdownProps {
32
+ value: string
33
+ options: { value: string; label: string; dot?: string }[]
34
+ onChange: (value: string) => void
35
+ className?: string
36
+ }
37
+
38
+ function Dropdown({ value, options, onChange, className }: DropdownProps) {
39
+ const [isOpen, setIsOpen] = useState(false)
40
+ const current = options.find(o => o.value === value)
41
+
42
+ return (
43
+ <div className={cn('relative', className)}>
44
+ <button
45
+ type="button"
46
+ onClick={() => setIsOpen(!isOpen)}
47
+ className="flex items-center gap-2 px-2 py-1 text-xs font-medium rounded transition-colors"
48
+ style={{
49
+ color: 'var(--vscode-foreground)',
50
+ }}
51
+ onMouseEnter={e => e.currentTarget.style.background = 'var(--vscode-list-hoverBackground)'}
52
+ onMouseLeave={e => e.currentTarget.style.background = 'transparent'}
53
+ >
54
+ {current?.dot && <span className={cn('w-2 h-2 rounded-full shrink-0', current.dot)} />}
55
+ <span>{current?.label}</span>
56
+ <ChevronDown size={12} style={{ color: 'var(--vscode-descriptionForeground)' }} className="ml-0.5" />
57
+ </button>
58
+ {isOpen && (
59
+ <>
60
+ <div className="fixed inset-0 z-10" onClick={() => setIsOpen(false)} />
61
+ <div
62
+ className="absolute top-full left-0 mt-1 z-20 rounded-lg shadow-lg py-1 min-w-[140px]"
63
+ style={{
64
+ background: 'var(--vscode-dropdown-background)',
65
+ border: '1px solid var(--vscode-dropdown-border, var(--vscode-panel-border))',
66
+ }}
67
+ >
68
+ {options.map(option => (
69
+ <button
70
+ key={option.value}
71
+ type="button"
72
+ onClick={() => {
73
+ onChange(option.value)
74
+ setIsOpen(false)
75
+ }}
76
+ className="w-full flex items-center gap-2 px-3 py-1.5 text-xs transition-colors"
77
+ style={{
78
+ color: 'var(--vscode-dropdown-foreground)',
79
+ background: option.value === value ? 'var(--vscode-list-activeSelectionBackground)' : undefined,
80
+ }}
81
+ onMouseEnter={e => {
82
+ if (option.value !== value) e.currentTarget.style.background = 'var(--vscode-list-hoverBackground)'
83
+ }}
84
+ onMouseLeave={e => {
85
+ if (option.value !== value) e.currentTarget.style.background = 'transparent'
86
+ }}
87
+ >
88
+ {option.dot && <span className={cn('w-2 h-2 rounded-full shrink-0', option.dot)} />}
89
+ <span className="flex-1 text-left">{option.label}</span>
90
+ {option.value === value && <Check size={12} style={{ color: 'var(--vscode-focusBorder)' }} className="shrink-0" />}
91
+ </button>
92
+ ))}
93
+ </div>
94
+ </>
95
+ )}
96
+ </div>
97
+ )
98
+ }
99
+
100
+ function AssigneeInput({ value, onChange }: { value: string; onChange: (value: string) => void }) {
101
+ const [isFocused, setIsFocused] = useState(false)
102
+ const inputRef = useRef<HTMLInputElement>(null)
103
+ const containerRef = useRef<HTMLDivElement>(null)
104
+ const features = useStore(s => s.features)
105
+
106
+ const existingAssignees = useMemo(() => {
107
+ const assignees = new Set<string>()
108
+ features.forEach(f => { if (f.assignee) assignees.add(f.assignee) })
109
+ return Array.from(assignees).sort()
110
+ }, [features])
111
+
112
+ const suggestions = useMemo(() => {
113
+ if (!value.trim()) return existingAssignees
114
+ return existingAssignees.filter(a => a.toLowerCase().includes(value.toLowerCase()) && a !== value)
115
+ }, [value, existingAssignees])
116
+
117
+ const showSuggestions = isFocused && suggestions.length > 0
118
+
119
+ const initials = value.trim()
120
+ ? value.trim().split(/\s+/).map(w => w[0]).join('').toUpperCase().slice(0, 2)
121
+ : null
122
+
123
+ return (
124
+ <div ref={containerRef} className="relative flex-1">
125
+ <div
126
+ className="flex items-center gap-2 cursor-text"
127
+ onClick={() => inputRef.current?.focus()}
128
+ >
129
+ {initials && (
130
+ <span
131
+ className="shrink-0 w-4 h-4 rounded-full flex items-center justify-center text-[8px] font-bold"
132
+ style={{
133
+ background: 'var(--vscode-badge-background)',
134
+ color: 'var(--vscode-badge-foreground)',
135
+ }}
136
+ >{initials}</span>
137
+ )}
138
+ <input
139
+ ref={inputRef}
140
+ type="text"
141
+ value={value}
142
+ onChange={(e) => onChange(e.target.value)}
143
+ onFocus={() => setIsFocused(true)}
144
+ onBlur={() => setTimeout(() => setIsFocused(false), 150)}
145
+ placeholder="No assignee"
146
+ className="flex-1 bg-transparent border-none outline-none text-xs"
147
+ style={{ color: value ? 'var(--vscode-foreground)' : 'var(--vscode-descriptionForeground)' }}
148
+ />
149
+ </div>
150
+ {showSuggestions && (
151
+ <div
152
+ className="absolute top-full left-0 mt-1 z-20 rounded-lg shadow-lg py-1 max-h-[160px] overflow-auto min-w-[180px]"
153
+ style={{
154
+ background: 'var(--vscode-dropdown-background)',
155
+ border: '1px solid var(--vscode-dropdown-border, var(--vscode-panel-border))',
156
+ }}
157
+ >
158
+ {suggestions.map(assignee => (
159
+ <button
160
+ key={assignee}
161
+ type="button"
162
+ onMouseDown={(e) => { e.preventDefault(); onChange(assignee) }}
163
+ className="w-full flex items-center gap-2 px-3 py-1.5 text-xs transition-colors"
164
+ style={{ color: 'var(--vscode-dropdown-foreground)' }}
165
+ onMouseEnter={e => e.currentTarget.style.background = 'var(--vscode-list-hoverBackground)'}
166
+ onMouseLeave={e => e.currentTarget.style.background = 'transparent'}
167
+ >
168
+ <span
169
+ className="shrink-0 w-4 h-4 rounded-full flex items-center justify-center text-[8px] font-bold"
170
+ style={{
171
+ background: 'var(--vscode-badge-background)',
172
+ color: 'var(--vscode-badge-foreground)',
173
+ }}
174
+ >{assignee.split(/\s+/).map(w => w[0]).join('').toUpperCase().slice(0, 2)}</span>
175
+ <span>{assignee}</span>
176
+ </button>
177
+ ))}
178
+ </div>
179
+ )}
180
+ </div>
181
+ )
182
+ }
183
+
184
+ function LabelInput({ labels, onChange }: { labels: string[]; onChange: (labels: string[]) => void }) {
185
+ const [newLabel, setNewLabel] = useState('')
186
+ const [isFocused, setIsFocused] = useState(false)
187
+ const inputRef = useRef<HTMLInputElement>(null)
188
+ const features = useStore(s => s.features)
189
+
190
+ const existingLabels = useMemo(() => {
191
+ const labelSet = new Set<string>()
192
+ features.forEach(f => f.labels.forEach(l => labelSet.add(l)))
193
+ return Array.from(labelSet).sort()
194
+ }, [features])
195
+
196
+ const suggestions = useMemo(() => {
197
+ const available = existingLabels.filter(l => !labels.includes(l))
198
+ if (!newLabel.trim()) return available
199
+ return available.filter(l => l.toLowerCase().includes(newLabel.toLowerCase()))
200
+ }, [newLabel, existingLabels, labels])
201
+
202
+ const showSuggestions = isFocused && suggestions.length > 0
203
+
204
+ const addLabel = (label?: string) => {
205
+ const l = (label || newLabel).trim()
206
+ if (l && !labels.includes(l)) {
207
+ onChange([...labels, l])
208
+ }
209
+ setNewLabel('')
210
+ }
211
+
212
+ return (
213
+ <div className="relative flex-1">
214
+ <div
215
+ className="flex items-center gap-1.5 flex-wrap cursor-text"
216
+ onClick={() => inputRef.current?.focus()}
217
+ >
218
+ {labels.map(label => (
219
+ <span
220
+ key={label}
221
+ className="inline-flex items-center gap-1 px-1.5 py-0.5 text-[10px] font-medium rounded"
222
+ style={{
223
+ background: 'var(--vscode-badge-background)',
224
+ color: 'var(--vscode-badge-foreground)',
225
+ }}
226
+ >
227
+ {label}
228
+ <button
229
+ onClick={(e) => { e.stopPropagation(); onChange(labels.filter(l => l !== label)) }}
230
+ className="hover:text-red-500 transition-colors"
231
+ >
232
+ <X size={9} />
233
+ </button>
234
+ </span>
235
+ ))}
236
+ <input
237
+ ref={inputRef}
238
+ type="text"
239
+ value={newLabel}
240
+ onChange={(e) => setNewLabel(e.target.value)}
241
+ onFocus={() => setIsFocused(true)}
242
+ onBlur={() => setTimeout(() => setIsFocused(false), 150)}
243
+ onKeyDown={(e) => {
244
+ if (e.key === 'Enter') { e.preventDefault(); addLabel() }
245
+ if (e.key === 'Backspace' && !newLabel && labels.length > 0) {
246
+ onChange(labels.slice(0, -1))
247
+ }
248
+ if (e.key === 'Escape') { setNewLabel(''); inputRef.current?.blur() }
249
+ }}
250
+ placeholder={labels.length === 0 ? 'Add labels...' : ''}
251
+ className="flex-1 min-w-[60px] bg-transparent border-none outline-none text-xs"
252
+ style={{ color: 'var(--vscode-foreground)' }}
253
+ />
254
+ </div>
255
+ {showSuggestions && (
256
+ <div
257
+ className="absolute top-full left-0 mt-1 z-20 rounded-lg shadow-lg py-1 max-h-[160px] overflow-auto min-w-[180px]"
258
+ style={{
259
+ background: 'var(--vscode-dropdown-background)',
260
+ border: '1px solid var(--vscode-dropdown-border, var(--vscode-panel-border))',
261
+ }}
262
+ >
263
+ {suggestions.map(label => (
264
+ <button
265
+ key={label}
266
+ type="button"
267
+ onMouseDown={(e) => { e.preventDefault(); addLabel(label) }}
268
+ className="w-full flex items-center gap-2 px-3 py-1.5 text-xs transition-colors"
269
+ style={{ color: 'var(--vscode-dropdown-foreground)' }}
270
+ onMouseEnter={e => e.currentTarget.style.background = 'var(--vscode-list-hoverBackground)'}
271
+ onMouseLeave={e => e.currentTarget.style.background = 'transparent'}
272
+ >
273
+ <span
274
+ className="inline-block px-1.5 py-0.5 text-[10px] font-medium rounded"
275
+ style={{
276
+ background: 'var(--vscode-badge-background)',
277
+ color: 'var(--vscode-badge-foreground)',
278
+ }}
279
+ >{label}</span>
280
+ </button>
281
+ ))}
282
+ </div>
283
+ )}
284
+ </div>
285
+ )
286
+ }
287
+
288
+ function PropertyRow({ label, icon, children }: { label: string; icon: React.ReactNode; children: React.ReactNode }) {
289
+ return (
290
+ <div
291
+ className="flex items-center gap-3 px-4 py-[5px] transition-colors"
292
+ onMouseEnter={e => e.currentTarget.style.background = 'var(--vscode-list-hoverBackground)'}
293
+ onMouseLeave={e => e.currentTarget.style.background = 'transparent'}
294
+ >
295
+ <div className="flex items-center gap-2 w-[90px] shrink-0">
296
+ <span style={{ color: 'var(--vscode-descriptionForeground)' }}>{icon}</span>
297
+ <span className="text-[11px]" style={{ color: 'var(--vscode-descriptionForeground)' }}>{label}</span>
298
+ </div>
299
+ <div className="flex-1 min-w-0">
300
+ {children}
301
+ </div>
302
+ </div>
303
+ )
304
+ }
305
+
306
+ // Wrapper that unmounts and remounts content when dialog opens to reset state
307
+ export function CreateFeatureDialog({ isOpen, ...props }: CreateFeatureDialogProps) {
308
+ if (!isOpen) return null
309
+ return <CreateFeatureDialogContent isOpen={isOpen} {...props} />
310
+ }
311
+
312
+ function CreateFeatureDialogContent({
313
+ isOpen,
314
+ onClose,
315
+ onCreate,
316
+ initialStatus
317
+ }: CreateFeatureDialogProps) {
318
+ const { cardSettings } = useStore()
319
+ const [title, setTitle] = useState('')
320
+ const [status, setStatus] = useState<FeatureStatus>(initialStatus ?? cardSettings.defaultStatus)
321
+ const [priority, setPriority] = useState<Priority>(cardSettings.defaultPriority)
322
+ const [assignee, setAssignee] = useState('')
323
+ const [dueDate, setDueDate] = useState('')
324
+ const [labels, setLabels] = useState<string[]>([])
325
+ const [description, setDescription] = useState('')
326
+ const inputRef = useRef<HTMLTextAreaElement>(null)
327
+
328
+ // Focus input on mount
329
+ useEffect(() => {
330
+ const timer = setTimeout(() => inputRef.current?.focus(), 50)
331
+ return () => clearTimeout(timer)
332
+ }, [])
333
+
334
+ const handleSubmit = () => {
335
+ const desc = description.trim()
336
+ const heading = title.trim()
337
+ const content = heading
338
+ ? `# ${heading}${desc ? `\n\n${desc}` : ''}`
339
+ : desc
340
+ onCreate({ status, priority, content, assignee: assignee.trim() || null, dueDate: dueDate || null, labels })
341
+ }
342
+
343
+ const handleSaveAndClose = () => {
344
+ handleSubmit()
345
+ onClose()
346
+ }
347
+
348
+ const handleCancel = () => {
349
+ onClose()
350
+ }
351
+
352
+ useEffect(() => {
353
+ const handleKeyDown = (e: KeyboardEvent) => {
354
+ if (e.key === 'Escape') {
355
+ handleCancel()
356
+ }
357
+ if ((e.metaKey || e.ctrlKey) && e.key === 'Enter') {
358
+ handleSaveAndClose()
359
+ }
360
+ if ((e.metaKey || e.ctrlKey) && e.key === 's') {
361
+ e.preventDefault()
362
+ handleSaveAndClose()
363
+ }
364
+ }
365
+
366
+ if (isOpen) {
367
+ document.addEventListener('keydown', handleKeyDown)
368
+ return () => document.removeEventListener('keydown', handleKeyDown)
369
+ }
370
+ })
371
+
372
+ return (
373
+ <div className="fixed inset-0 z-50 flex justify-end">
374
+ <div className="absolute inset-0 bg-black/30" onClick={handleCancel} />
375
+ <div
376
+ className="relative h-full w-1/2 shadow-xl flex flex-col animate-in slide-in-from-right duration-200"
377
+ style={{
378
+ background: 'var(--vscode-editor-background)',
379
+ borderLeft: '1px solid var(--vscode-panel-border)',
380
+ }}
381
+ >
382
+ {/* Header */}
383
+ <div
384
+ className="flex items-center justify-between px-4 py-3"
385
+ style={{ borderBottom: '1px solid var(--vscode-panel-border)' }}
386
+ >
387
+ <div className="flex items-center gap-3">
388
+ <h2 className="font-medium" style={{ color: 'var(--vscode-foreground)' }}>
389
+ Create Feature
390
+ </h2>
391
+ </div>
392
+ <button
393
+ onClick={handleCancel}
394
+ className="p-1.5 rounded transition-colors"
395
+ style={{ color: 'var(--vscode-descriptionForeground)' }}
396
+ onMouseEnter={e => e.currentTarget.style.background = 'var(--vscode-list-hoverBackground)'}
397
+ onMouseLeave={e => e.currentTarget.style.background = 'transparent'}
398
+ >
399
+ <X size={18} />
400
+ </button>
401
+ </div>
402
+
403
+ {/* Metadata */}
404
+ <div
405
+ className="flex flex-col py-0.5"
406
+ style={{
407
+ borderBottom: '1px solid var(--vscode-panel-border)',
408
+ }}
409
+ >
410
+ <PropertyRow label="Status" icon={<CircleDot size={13} />}>
411
+ <Dropdown
412
+ value={status}
413
+ options={statusConfig.map(s => ({ value: s.value, label: s.label, dot: s.dot }))}
414
+ onChange={(v) => setStatus(v as FeatureStatus)}
415
+ />
416
+ </PropertyRow>
417
+ {cardSettings.showPriorityBadges && (
418
+ <PropertyRow label="Priority" icon={<Signal size={13} />}>
419
+ <Dropdown
420
+ value={priority}
421
+ options={priorityConfig.map(p => ({ value: p.value, label: p.label, dot: p.dot }))}
422
+ onChange={(v) => setPriority(v as Priority)}
423
+ />
424
+ </PropertyRow>
425
+ )}
426
+ {cardSettings.showAssignee && (
427
+ <PropertyRow label="Assignee" icon={<User size={13} />}>
428
+ <AssigneeInput value={assignee} onChange={setAssignee} />
429
+ </PropertyRow>
430
+ )}
431
+ {cardSettings.showDueDate && (
432
+ <PropertyRow label="Due date" icon={<Calendar size={13} />}>
433
+ <DatePicker value={dueDate} onChange={setDueDate} />
434
+ </PropertyRow>
435
+ )}
436
+ {cardSettings.showLabels && (
437
+ <PropertyRow label="Labels" icon={<Tag size={13} />}>
438
+ <LabelInput labels={labels} onChange={setLabels} />
439
+ </PropertyRow>
440
+ )}
441
+ </div>
442
+
443
+ {/* Content */}
444
+ <div className="flex-1 overflow-auto p-4">
445
+ <textarea
446
+ ref={inputRef}
447
+ value={title}
448
+ onChange={(e) => setTitle(e.target.value)}
449
+ placeholder="Feature title..."
450
+ className="w-full text-lg font-medium bg-transparent border-none outline-none resize-none mb-4"
451
+ style={{
452
+ color: 'var(--vscode-foreground)',
453
+ }}
454
+ rows={1}
455
+ onInput={(e) => {
456
+ const target = e.target as HTMLTextAreaElement
457
+ target.style.height = 'auto'
458
+ target.style.height = target.scrollHeight + 'px'
459
+ }}
460
+ onKeyDown={(e) => {
461
+ if (e.key === 'Enter' && !e.shiftKey) {
462
+ e.preventDefault()
463
+ }
464
+ }}
465
+ />
466
+ <MarkdownEditor
467
+ value={description}
468
+ onChange={setDescription}
469
+ placeholder="Add a description..."
470
+ />
471
+ </div>
472
+
473
+ {/* Footer with Cancel / Save buttons */}
474
+ <div
475
+ className="flex items-center justify-between px-4 py-3"
476
+ style={{
477
+ borderTop: '1px solid var(--vscode-panel-border)',
478
+ background: 'var(--vscode-sideBar-background, var(--vscode-editor-background))',
479
+ }}
480
+ >
481
+ <p className="text-xs" style={{ color: 'var(--vscode-descriptionForeground)' }}>
482
+ <kbd
483
+ className="px-1.5 py-0.5 rounded text-[10px] font-mono"
484
+ style={{ background: 'var(--vscode-keybindingLabel-background, var(--vscode-badge-background))', color: 'var(--vscode-keybindingLabel-foreground, var(--vscode-foreground))', border: '1px solid var(--vscode-keybindingLabel-border, var(--vscode-panel-border))' }}
485
+ >Esc</kbd>{' '}cancel ·{' '}
486
+ <kbd
487
+ className="px-1.5 py-0.5 rounded text-[10px] font-mono"
488
+ style={{ background: 'var(--vscode-keybindingLabel-background, var(--vscode-badge-background))', color: 'var(--vscode-keybindingLabel-foreground, var(--vscode-foreground))', border: '1px solid var(--vscode-keybindingLabel-border, var(--vscode-panel-border))' }}
489
+ >⌘S</kbd>{' '}save
490
+ </p>
491
+ <div className="flex items-center gap-2">
492
+ <button
493
+ type="button"
494
+ onClick={handleCancel}
495
+ className="px-3 py-1.5 text-xs font-medium rounded transition-colors"
496
+ style={{
497
+ color: 'var(--vscode-foreground)',
498
+ background: 'transparent',
499
+ border: '1px solid var(--vscode-panel-border)',
500
+ }}
501
+ onMouseEnter={e => e.currentTarget.style.background = 'var(--vscode-list-hoverBackground)'}
502
+ onMouseLeave={e => e.currentTarget.style.background = 'transparent'}
503
+ >
504
+ Cancel
505
+ </button>
506
+ <button
507
+ type="button"
508
+ onClick={handleSaveAndClose}
509
+ className="px-3 py-1.5 text-xs font-medium rounded transition-colors"
510
+ style={{
511
+ color: 'var(--vscode-button-foreground)',
512
+ background: 'var(--vscode-button-background)',
513
+ }}
514
+ onMouseEnter={e => e.currentTarget.style.background = 'var(--vscode-button-hoverBackground)'}
515
+ onMouseLeave={e => e.currentTarget.style.background = 'var(--vscode-button-background)'}
516
+ >
517
+ Save
518
+ </button>
519
+ </div>
520
+ </div>
521
+ </div>
522
+ </div>
523
+ )
524
+ }