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,72 @@
1
+ import { useState, useRef, useEffect } from 'react'
2
+ import { Plus } from 'lucide-react'
3
+ import type { FeatureStatus, Priority } from '../../shared/types'
4
+ import { useStore } from '../store'
5
+
6
+ interface QuickAddInputProps {
7
+ status: FeatureStatus
8
+ onAdd: (data: { status: FeatureStatus; priority: Priority; content: string }) => void
9
+ }
10
+
11
+ export function QuickAddInput({ status, onAdd }: QuickAddInputProps) {
12
+ const { cardSettings } = useStore()
13
+ const [isEditing, setIsEditing] = useState(false)
14
+ const [value, setValue] = useState('')
15
+ const inputRef = useRef<HTMLInputElement>(null)
16
+
17
+ useEffect(() => {
18
+ if (isEditing && inputRef.current) {
19
+ inputRef.current.focus()
20
+ }
21
+ }, [isEditing])
22
+
23
+ const handleSubmit = () => {
24
+ const title = value.trim()
25
+ if (title) {
26
+ // Build content with title as first # heading
27
+ const content = `# ${title}`
28
+ onAdd({
29
+ status,
30
+ priority: cardSettings.defaultPriority,
31
+ content
32
+ })
33
+ setValue('')
34
+ }
35
+ setIsEditing(false)
36
+ }
37
+
38
+ const handleKeyDown = (e: React.KeyboardEvent) => {
39
+ if (e.key === 'Enter') {
40
+ e.preventDefault()
41
+ handleSubmit()
42
+ } else if (e.key === 'Escape') {
43
+ setValue('')
44
+ setIsEditing(false)
45
+ }
46
+ }
47
+
48
+ if (!isEditing) {
49
+ return (
50
+ <button
51
+ onClick={() => setIsEditing(true)}
52
+ className="w-full flex items-center gap-1.5 px-2 py-1.5 text-sm text-zinc-500 hover:text-zinc-700 dark:hover:text-zinc-300 hover:bg-zinc-100 dark:hover:bg-zinc-800 rounded-md transition-colors"
53
+ >
54
+ <Plus size={14} />
55
+ <span>Add feature</span>
56
+ </button>
57
+ )
58
+ }
59
+
60
+ return (
61
+ <input
62
+ ref={inputRef}
63
+ type="text"
64
+ value={value}
65
+ onChange={(e) => setValue(e.target.value)}
66
+ onBlur={handleSubmit}
67
+ onKeyDown={handleKeyDown}
68
+ placeholder="Feature title..."
69
+ className="w-full px-2 py-1.5 text-sm bg-white dark:bg-zinc-800 border border-zinc-300 dark:border-zinc-600 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 text-zinc-900 dark:text-zinc-100 placeholder-zinc-400"
70
+ />
71
+ )
72
+ }
@@ -0,0 +1,284 @@
1
+ import { useState, useEffect } from 'react'
2
+ import { X, ChevronDown } from 'lucide-react'
3
+ import type { CardDisplaySettings, Priority, FeatureStatus } from '../../shared/types'
4
+ import { cn } from '../lib/utils'
5
+
6
+ const priorityConfig: { value: Priority; label: string; dot: string }[] = [
7
+ { value: 'critical', label: 'Critical', dot: 'bg-red-500' },
8
+ { value: 'high', label: 'High', dot: 'bg-orange-500' },
9
+ { value: 'medium', label: 'Medium', dot: 'bg-yellow-500' },
10
+ { value: 'low', label: 'Low', dot: 'bg-green-500' }
11
+ ]
12
+
13
+ const statusConfig: { value: FeatureStatus; label: string; dot: string }[] = [
14
+ { value: 'backlog', label: 'Backlog', dot: 'bg-zinc-400' },
15
+ { value: 'todo', label: 'To Do', dot: 'bg-blue-400' },
16
+ { value: 'in-progress', label: 'In Progress', dot: 'bg-amber-400' },
17
+ { value: 'review', label: 'Review', dot: 'bg-purple-400' },
18
+ { value: 'done', label: 'Done', dot: 'bg-emerald-400' }
19
+ ]
20
+
21
+ interface SettingsPanelProps {
22
+ isOpen: boolean
23
+ settings: CardDisplaySettings
24
+ onClose: () => void
25
+ onSave: (settings: CardDisplaySettings) => void
26
+ }
27
+
28
+ export function SettingsPanel({ isOpen, settings, onClose, onSave }: SettingsPanelProps) {
29
+ if (!isOpen) return null
30
+ return <SettingsPanelContent settings={settings} onClose={onClose} onSave={onSave} />
31
+ }
32
+
33
+ function ToggleSwitch({ checked, onChange }: { checked: boolean; onChange: (v: boolean) => void }) {
34
+ return (
35
+ <button
36
+ type="button"
37
+ onClick={() => onChange(!checked)}
38
+ className={cn(
39
+ 'relative inline-flex h-5 w-9 items-center rounded-full transition-colors shrink-0'
40
+ )}
41
+ style={{
42
+ background: checked
43
+ ? 'var(--vscode-button-background)'
44
+ : 'var(--vscode-badge-background, #6b7280)'
45
+ }}
46
+ >
47
+ <span
48
+ className="inline-block h-3.5 w-3.5 rounded-full bg-white transition-transform"
49
+ style={{
50
+ transform: checked ? 'translateX(18px)' : 'translateX(3px)'
51
+ }}
52
+ />
53
+ </button>
54
+ )
55
+ }
56
+
57
+ function SettingsSection({ title, children }: { title: string; children: React.ReactNode }) {
58
+ return (
59
+ <div className="py-3">
60
+ <h3
61
+ className="px-4 pb-2 text-xs font-semibold uppercase tracking-wider"
62
+ style={{ color: 'var(--vscode-descriptionForeground)' }}
63
+ >
64
+ {title}
65
+ </h3>
66
+ <div>{children}</div>
67
+ </div>
68
+ )
69
+ }
70
+
71
+ function SettingsToggle({ label, description, checked, onChange }: {
72
+ label: string
73
+ description?: string
74
+ checked: boolean
75
+ onChange: (v: boolean) => void
76
+ }) {
77
+ return (
78
+ <div
79
+ className="flex items-center justify-between gap-4 px-4 py-2 transition-colors cursor-pointer"
80
+ onClick={() => onChange(!checked)}
81
+ onMouseEnter={e => e.currentTarget.style.background = 'var(--vscode-list-hoverBackground)'}
82
+ onMouseLeave={e => e.currentTarget.style.background = 'transparent'}
83
+ >
84
+ <div className="flex-1 min-w-0">
85
+ <div className="text-sm" style={{ color: 'var(--vscode-foreground)' }}>{label}</div>
86
+ {description && (
87
+ <div className="text-xs mt-0.5" style={{ color: 'var(--vscode-descriptionForeground)' }}>{description}</div>
88
+ )}
89
+ </div>
90
+ <ToggleSwitch checked={checked} onChange={onChange} />
91
+ </div>
92
+ )
93
+ }
94
+
95
+ function SettingsDropdown({ label, value, options, onChange }: {
96
+ label: string
97
+ value: string
98
+ options: { value: string; label: string; dot?: string }[]
99
+ onChange: (value: string) => void
100
+ }) {
101
+ const [isOpen, setIsOpen] = useState(false)
102
+ const current = options.find(o => o.value === value)
103
+
104
+ return (
105
+ <div
106
+ className="flex items-center justify-between gap-4 px-4 py-2 transition-colors"
107
+ onMouseEnter={e => e.currentTarget.style.background = 'var(--vscode-list-hoverBackground)'}
108
+ onMouseLeave={e => e.currentTarget.style.background = 'transparent'}
109
+ >
110
+ <div className="text-sm" style={{ color: 'var(--vscode-foreground)' }}>{label}</div>
111
+ <div className="relative">
112
+ <button
113
+ type="button"
114
+ onClick={() => setIsOpen(!isOpen)}
115
+ className="flex items-center gap-2 px-2 py-1 text-xs font-medium rounded transition-colors"
116
+ style={{ color: 'var(--vscode-foreground)' }}
117
+ >
118
+ {current?.dot && <span className={cn('w-2 h-2 rounded-full shrink-0', current.dot)} />}
119
+ <span>{current?.label}</span>
120
+ <ChevronDown size={12} style={{ color: 'var(--vscode-descriptionForeground)' }} />
121
+ </button>
122
+ {isOpen && (
123
+ <>
124
+ <div className="fixed inset-0 z-10" onClick={() => setIsOpen(false)} />
125
+ <div
126
+ className="absolute top-full right-0 mt-1 z-20 rounded-lg shadow-lg py-1 min-w-[140px]"
127
+ style={{
128
+ background: 'var(--vscode-dropdown-background)',
129
+ border: '1px solid var(--vscode-dropdown-border, var(--vscode-panel-border))',
130
+ }}
131
+ >
132
+ {options.map(option => (
133
+ <button
134
+ key={option.value}
135
+ type="button"
136
+ onClick={() => {
137
+ onChange(option.value)
138
+ setIsOpen(false)
139
+ }}
140
+ className="w-full flex items-center gap-2 px-3 py-1.5 text-xs transition-colors"
141
+ style={{
142
+ color: 'var(--vscode-dropdown-foreground)',
143
+ background: option.value === value ? 'var(--vscode-list-activeSelectionBackground)' : undefined,
144
+ }}
145
+ onMouseEnter={e => {
146
+ if (option.value !== value) e.currentTarget.style.background = 'var(--vscode-list-hoverBackground)'
147
+ }}
148
+ onMouseLeave={e => {
149
+ if (option.value !== value) e.currentTarget.style.background = 'transparent'
150
+ }}
151
+ >
152
+ {option.dot && <span className={cn('w-2 h-2 rounded-full shrink-0', option.dot)} />}
153
+ <span className="flex-1 text-left">{option.label}</span>
154
+ </button>
155
+ ))}
156
+ </div>
157
+ </>
158
+ )}
159
+ </div>
160
+ </div>
161
+ )
162
+ }
163
+
164
+ function SettingsPanelContent({ settings, onClose, onSave }: Omit<SettingsPanelProps, 'isOpen'>) {
165
+ const [local, setLocal] = useState<CardDisplaySettings>(settings)
166
+
167
+ useEffect(() => { setLocal(settings) }, [settings])
168
+
169
+ const update = (patch: Partial<CardDisplaySettings>) => {
170
+ const next = { ...local, ...patch }
171
+ setLocal(next)
172
+ onSave(next)
173
+ }
174
+
175
+ useEffect(() => {
176
+ const handleKeyDown = (e: KeyboardEvent) => {
177
+ if (e.key === 'Escape') {
178
+ e.stopPropagation()
179
+ onClose()
180
+ }
181
+ }
182
+ document.addEventListener('keydown', handleKeyDown, true)
183
+ return () => document.removeEventListener('keydown', handleKeyDown, true)
184
+ }, [onClose])
185
+
186
+ return (
187
+ <div className="fixed inset-0 z-50 flex justify-end">
188
+ <div className="absolute inset-0 bg-black/30" onClick={onClose} />
189
+ <div
190
+ className="relative h-full w-1/2 max-w-lg shadow-xl flex flex-col animate-in slide-in-from-right duration-200"
191
+ style={{
192
+ background: 'var(--vscode-editor-background)',
193
+ borderLeft: '1px solid var(--vscode-panel-border)',
194
+ }}
195
+ >
196
+ {/* Header */}
197
+ <div
198
+ className="flex items-center justify-between px-4 py-3"
199
+ style={{ borderBottom: '1px solid var(--vscode-panel-border)' }}
200
+ >
201
+ <h2 className="font-medium" style={{ color: 'var(--vscode-foreground)' }}>Settings</h2>
202
+ <button
203
+ onClick={onClose}
204
+ className="p-1.5 rounded transition-colors"
205
+ style={{ color: 'var(--vscode-descriptionForeground)' }}
206
+ onMouseEnter={e => e.currentTarget.style.background = 'var(--vscode-list-hoverBackground)'}
207
+ onMouseLeave={e => e.currentTarget.style.background = 'transparent'}
208
+ >
209
+ <X size={18} />
210
+ </button>
211
+ </div>
212
+
213
+ {/* Content */}
214
+ <div className="flex-1 overflow-auto">
215
+ <SettingsSection title="Card Display">
216
+ <SettingsToggle
217
+ label="Show Priority Badges"
218
+ description="Display priority indicators on feature cards"
219
+ checked={local.showPriorityBadges}
220
+ onChange={v => update({ showPriorityBadges: v })}
221
+ />
222
+ <SettingsToggle
223
+ label="Show Assignee"
224
+ description="Display assigned person on feature cards"
225
+ checked={local.showAssignee}
226
+ onChange={v => update({ showAssignee: v })}
227
+ />
228
+ <SettingsToggle
229
+ label="Show Due Date"
230
+ description="Display due dates on feature cards"
231
+ checked={local.showDueDate}
232
+ onChange={v => update({ showDueDate: v })}
233
+ />
234
+ <SettingsToggle
235
+ label="Show Labels"
236
+ description="Display labels on feature cards and in editors"
237
+ checked={local.showLabels}
238
+ onChange={v => update({ showLabels: v })}
239
+ />
240
+ <SettingsToggle
241
+ label="Show Filename"
242
+ description="Display the source markdown filename on cards"
243
+ checked={local.showFileName}
244
+ onChange={v => update({ showFileName: v })}
245
+ />
246
+ <SettingsToggle
247
+ label="Compact Mode"
248
+ description="Use compact card layout to show more features"
249
+ checked={local.compactMode}
250
+ onChange={v => update({ compactMode: v })}
251
+ />
252
+ </SettingsSection>
253
+
254
+ <div style={{ borderTop: '1px solid var(--vscode-panel-border)' }} />
255
+
256
+ <SettingsSection title="Defaults">
257
+ <SettingsDropdown
258
+ label="Default Priority"
259
+ value={local.defaultPriority}
260
+ options={priorityConfig}
261
+ onChange={v => update({ defaultPriority: v as Priority })}
262
+ />
263
+ <SettingsDropdown
264
+ label="Default Status"
265
+ value={local.defaultStatus}
266
+ options={statusConfig}
267
+ onChange={v => update({ defaultStatus: v as FeatureStatus })}
268
+ />
269
+ </SettingsSection>
270
+ </div>
271
+
272
+ {/* Footer */}
273
+ <div
274
+ className="px-4 py-2"
275
+ style={{ borderTop: '1px solid var(--vscode-panel-border)' }}
276
+ >
277
+ <p className="text-xs" style={{ color: 'var(--vscode-descriptionForeground)' }}>
278
+ Settings are saved automatically and apply to all connected clients.
279
+ </p>
280
+ </div>
281
+ </div>
282
+ </div>
283
+ )
284
+ }
@@ -0,0 +1,175 @@
1
+ import { Search, X, Columns, Rows, Settings, Plus } from 'lucide-react'
2
+ import { useStore, type DueDateFilter } from '../store'
3
+ import type { Priority } from '../../shared/types'
4
+
5
+ const priorities: { value: Priority | 'all'; label: string }[] = [
6
+ { value: 'all', label: 'All Priorities' },
7
+ { value: 'critical', label: 'Critical' },
8
+ { value: 'high', label: 'High' },
9
+ { value: 'medium', label: 'Medium' },
10
+ { value: 'low', label: 'Low' }
11
+ ]
12
+
13
+ const dueDateOptions: { value: DueDateFilter; label: string }[] = [
14
+ { value: 'all', label: 'All Dates' },
15
+ { value: 'overdue', label: 'Overdue' },
16
+ { value: 'today', label: 'Due Today' },
17
+ { value: 'this-week', label: 'Due This Week' },
18
+ { value: 'no-date', label: 'No Due Date' }
19
+ ]
20
+
21
+ const selectClassName =
22
+ 'text-sm bg-white dark:bg-zinc-800 border border-zinc-200 dark:border-zinc-600 rounded-md px-2 py-1.5 focus:outline-none focus:ring-2 focus:ring-blue-500 text-zinc-900 dark:text-zinc-100'
23
+
24
+ export function Toolbar({ onOpenSettings, onAddColumn }: { onOpenSettings: () => void; onAddColumn: () => void }) {
25
+ const {
26
+ searchQuery,
27
+ setSearchQuery,
28
+ priorityFilter,
29
+ setPriorityFilter,
30
+ assigneeFilter,
31
+ setAssigneeFilter,
32
+ labelFilter,
33
+ setLabelFilter,
34
+ dueDateFilter,
35
+ setDueDateFilter,
36
+ clearAllFilters,
37
+ getUniqueAssignees,
38
+ getUniqueLabels,
39
+ hasActiveFilters,
40
+ layout,
41
+ toggleLayout,
42
+ cardSettings
43
+ } = useStore()
44
+
45
+ const assignees = getUniqueAssignees()
46
+ const labels = getUniqueLabels()
47
+ const filtersActive = hasActiveFilters()
48
+
49
+ return (
50
+ <div className="flex items-center gap-2 px-4 py-2 border-b border-zinc-200 dark:border-zinc-700 bg-zinc-50 dark:bg-zinc-900/50 flex-wrap">
51
+ {/* Search */}
52
+ <div className="relative flex-1 min-w-[180px] max-w-xs">
53
+ <Search
54
+ size={14}
55
+ className="absolute left-2.5 top-1/2 -translate-y-1/2 text-zinc-400"
56
+ />
57
+ <input
58
+ type="text"
59
+ value={searchQuery}
60
+ onChange={(e) => setSearchQuery(e.target.value)}
61
+ placeholder="Search features..."
62
+ className="w-full pl-8 pr-3 py-1.5 text-sm bg-white dark:bg-zinc-800 border border-zinc-200 dark:border-zinc-600 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 text-zinc-900 dark:text-zinc-100 placeholder-zinc-400"
63
+ />
64
+ </div>
65
+
66
+ {/* Priority Filter */}
67
+ {cardSettings.showPriorityBadges && (
68
+ <select
69
+ value={priorityFilter}
70
+ onChange={(e) => setPriorityFilter(e.target.value as Priority | 'all')}
71
+ className={selectClassName}
72
+ >
73
+ {priorities.map((p) => (
74
+ <option key={p.value} value={p.value}>
75
+ {p.label}
76
+ </option>
77
+ ))}
78
+ </select>
79
+ )}
80
+
81
+ {/* Assignee Filter */}
82
+ {cardSettings.showAssignee && (
83
+ <select
84
+ value={assigneeFilter}
85
+ onChange={(e) => setAssigneeFilter(e.target.value)}
86
+ className={selectClassName}
87
+ >
88
+ <option value="all">All Assignees</option>
89
+ <option value="unassigned">Unassigned</option>
90
+ {assignees.map((a) => (
91
+ <option key={a} value={a}>
92
+ {a}
93
+ </option>
94
+ ))}
95
+ </select>
96
+ )}
97
+
98
+ {/* Label Filter */}
99
+ {cardSettings.showLabels && (
100
+ <select
101
+ value={labelFilter}
102
+ onChange={(e) => setLabelFilter(e.target.value)}
103
+ className={selectClassName}
104
+ >
105
+ <option value="all">All Labels</option>
106
+ {labels.map((l) => (
107
+ <option key={l} value={l}>
108
+ {l}
109
+ </option>
110
+ ))}
111
+ </select>
112
+ )}
113
+
114
+ {/* Due Date Filter */}
115
+ {cardSettings.showDueDate && (
116
+ <select
117
+ value={dueDateFilter}
118
+ onChange={(e) => setDueDateFilter(e.target.value as DueDateFilter)}
119
+ className={selectClassName}
120
+ >
121
+ {dueDateOptions.map((d) => (
122
+ <option key={d.value} value={d.value}>
123
+ {d.label}
124
+ </option>
125
+ ))}
126
+ </select>
127
+ )}
128
+
129
+ {/* Clear Filters Button */}
130
+ {filtersActive && (
131
+ <button
132
+ onClick={clearAllFilters}
133
+ className="flex items-center gap-1 px-2 py-1.5 text-sm text-zinc-500 hover:text-zinc-700 dark:text-zinc-400 dark:hover:text-zinc-200 hover:bg-zinc-100 dark:hover:bg-zinc-800 rounded-md transition-colors"
134
+ title="Clear all filters"
135
+ >
136
+ <X size={14} />
137
+ <span>Clear</span>
138
+ </button>
139
+ )}
140
+
141
+ {/* Add List */}
142
+ <button
143
+ onClick={onAddColumn}
144
+ className="flex items-center gap-1 px-2 py-1.5 text-sm text-zinc-500 hover:text-zinc-700 dark:text-zinc-400 dark:hover:text-zinc-200 hover:bg-zinc-100 dark:hover:bg-zinc-800 rounded-md transition-colors"
145
+ title="Add new list"
146
+ >
147
+ <Plus size={16} />
148
+ <span>Add List</span>
149
+ </button>
150
+
151
+ {/* Layout Toggle */}
152
+ <button
153
+ onClick={toggleLayout}
154
+ className="flex items-center gap-1 px-2 py-1.5 text-sm text-zinc-500 hover:text-zinc-700 dark:text-zinc-400 dark:hover:text-zinc-200 hover:bg-zinc-100 dark:hover:bg-zinc-800 rounded-md transition-colors"
155
+ title={layout === 'horizontal' ? 'Switch to vertical layout' : 'Switch to horizontal layout'}
156
+ >
157
+ {layout === 'horizontal' ? <Rows size={16} /> : <Columns size={16} />}
158
+ </button>
159
+
160
+ {/* Settings */}
161
+ <button
162
+ onClick={onOpenSettings}
163
+ className="flex items-center gap-1 px-2 py-1.5 text-sm text-zinc-500 hover:text-zinc-700 dark:text-zinc-400 dark:hover:text-zinc-200 hover:bg-zinc-100 dark:hover:bg-zinc-800 rounded-md transition-colors"
164
+ title="Open settings"
165
+ >
166
+ <Settings size={16} />
167
+ </button>
168
+
169
+ {/* Keyboard hint */}
170
+ <div className="ml-auto text-xs text-zinc-400">
171
+ Press <kbd className="px-1.5 py-0.5 bg-zinc-200 dark:bg-zinc-700 rounded">n</kbd> to add
172
+ </div>
173
+ </div>
174
+ )
175
+ }
@@ -0,0 +1,70 @@
1
+ import { useEffect, useState } from 'react'
2
+
3
+ interface UndoToastProps {
4
+ message: string
5
+ onUndo: () => void
6
+ onExpire: () => void
7
+ duration: number
8
+ index: number
9
+ }
10
+
11
+ export function UndoToast({ message, onUndo, onExpire, duration, index }: UndoToastProps) {
12
+ const [progress, setProgress] = useState(100)
13
+
14
+ useEffect(() => {
15
+ const interval = 50
16
+ const step = (interval / duration) * 100
17
+ const timer = setInterval(() => {
18
+ setProgress(prev => {
19
+ const next = prev - step
20
+ if (next <= 0) {
21
+ clearInterval(timer)
22
+ return 0
23
+ }
24
+ return next
25
+ })
26
+ }, interval)
27
+
28
+ return () => clearInterval(timer)
29
+ }, [duration])
30
+
31
+ useEffect(() => {
32
+ if (progress <= 0) {
33
+ onExpire()
34
+ }
35
+ }, [progress, onExpire])
36
+
37
+ return (
38
+ <div
39
+ className="fixed right-4 z-50 flex flex-col min-w-[320px] max-w-[420px] shadow-[0_4px_16px_rgba(0,0,0,0.4)] transition-[bottom] duration-200 ease-out"
40
+ style={{
41
+ bottom: `${24 + index * 52}px`,
42
+ background: 'var(--vscode-notifications-background)',
43
+ color: 'var(--vscode-notifications-foreground)',
44
+ border: '1px solid var(--vscode-notifications-border, var(--vscode-widget-border))',
45
+ }}
46
+ >
47
+ <div className="flex items-center gap-3 px-3 py-2.5">
48
+ <span className="text-[13px] leading-snug flex-1 truncate">{message}</span>
49
+ <button
50
+ onClick={onUndo}
51
+ className="text-[13px] px-2 py-0.5 shrink-0"
52
+ style={{
53
+ background: 'var(--vscode-button-background)',
54
+ color: 'var(--vscode-button-foreground)',
55
+ }}
56
+ onMouseEnter={e => e.currentTarget.style.background = 'var(--vscode-button-hoverBackground)'}
57
+ onMouseLeave={e => e.currentTarget.style.background = 'var(--vscode-button-background)'}
58
+ >
59
+ Undo
60
+ </button>
61
+ </div>
62
+ <div className="h-[2px] w-full" style={{ background: 'var(--vscode-widget-border)' }}>
63
+ <div
64
+ className="h-full transition-none"
65
+ style={{ width: `${progress}%`, background: 'var(--vscode-progressBar-background)' }}
66
+ />
67
+ </div>
68
+ </div>
69
+ )
70
+ }
@@ -0,0 +1,12 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Kanban Board</title>
7
+ </head>
8
+ <body>
9
+ <div id="root"></div>
10
+ <script type="module" src="./main.tsx"></script>
11
+ </body>
12
+ </html>
@@ -0,0 +1,6 @@
1
+ import { clsx, type ClassValue } from 'clsx'
2
+ import { twMerge } from 'tailwind-merge'
3
+
4
+ export function cn(...inputs: ClassValue[]) {
5
+ return twMerge(clsx(inputs))
6
+ }
@@ -0,0 +1,11 @@
1
+ import './assets/main.css'
2
+
3
+ import { StrictMode } from 'react'
4
+ import { createRoot } from 'react-dom/client'
5
+ import App from './App'
6
+
7
+ createRoot(document.getElementById('root')!).render(
8
+ <StrictMode>
9
+ <App />
10
+ </StrictMode>
11
+ )
@@ -0,0 +1,13 @@
1
+ import './standalone-shim'
2
+ import './assets/main.css'
3
+ import './assets/standalone-theme.css'
4
+
5
+ import { StrictMode } from 'react'
6
+ import { createRoot } from 'react-dom/client'
7
+ import App from './App'
8
+
9
+ createRoot(document.getElementById('root')!).render(
10
+ <StrictMode>
11
+ <App />
12
+ </StrictMode>
13
+ )