kanban-lite 1.0.9 → 1.0.13

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 (38) hide show
  1. package/dist/cli.js +1015 -407
  2. package/dist/extension.js +897 -347
  3. package/dist/mcp-server.js +550 -189
  4. package/dist/sdk/index.cjs +1223 -0
  5. package/dist/sdk/index.mjs +1168 -0
  6. package/dist/sdk/sdk/KanbanSDK.d.ts +39 -21
  7. package/dist/sdk/sdk/index.d.ts +4 -3
  8. package/dist/sdk/sdk/migration.d.ts +6 -0
  9. package/dist/sdk/sdk/types.d.ts +3 -5
  10. package/dist/sdk/shared/config.d.ts +23 -10
  11. package/dist/sdk/shared/types.d.ts +18 -4
  12. package/dist/standalone-webview/index.js +27 -27
  13. package/dist/standalone-webview/index.js.map +1 -1
  14. package/dist/standalone-webview/style.css +1 -1
  15. package/dist/standalone.js +831 -328
  16. package/dist/webview/index.js +45 -45
  17. package/dist/webview/index.js.map +1 -1
  18. package/dist/webview/style.css +1 -1
  19. package/package.json +4 -3
  20. package/src/cli/index.ts +217 -95
  21. package/src/extension/KanbanPanel.ts +49 -22
  22. package/src/mcp-server/index.ts +138 -62
  23. package/src/sdk/KanbanSDK.ts +283 -77
  24. package/src/sdk/__tests__/KanbanSDK.test.ts +5 -5
  25. package/src/sdk/__tests__/migration.test.ts +269 -0
  26. package/src/sdk/__tests__/multi-board.test.ts +449 -0
  27. package/src/sdk/index.ts +4 -3
  28. package/src/sdk/migration.ts +52 -0
  29. package/src/sdk/types.ts +3 -6
  30. package/src/shared/config.ts +144 -22
  31. package/src/shared/types.ts +14 -5
  32. package/src/standalone/__tests__/server.integration.test.ts +38 -37
  33. package/src/standalone/server.ts +239 -21
  34. package/src/webview/App.tsx +17 -7
  35. package/src/webview/components/Toolbar.tsx +99 -3
  36. package/src/webview/store/index.ts +11 -3
  37. package/.kanban/backlog/1-test1.md +0 -30
  38. package/.kanban.json +0 -42
@@ -3,13 +3,14 @@ import * as fs from 'fs'
3
3
  import * as path from 'path'
4
4
  import { WebSocketServer, WebSocket } from 'ws'
5
5
  import chokidar from 'chokidar'
6
- import type { Comment, Feature, FeatureStatus, Priority, KanbanColumn, FeatureFrontmatter, CardDisplaySettings } from '../shared/types'
6
+ import { generateSlug, type Comment, type Feature, type Priority, type KanbanColumn, type FeatureFrontmatter, type CardDisplaySettings } from '../shared/types'
7
7
  import { KanbanSDK } from '../sdk/KanbanSDK'
8
8
  import { serializeFeature } from '../sdk/parser'
9
+ import { readConfig } from '../shared/config'
9
10
  import { fireWebhooks, loadWebhooks, createWebhook, deleteWebhook } from './webhooks'
10
11
 
11
12
  interface CreateFeatureData {
12
- status: FeatureStatus
13
+ status: string
13
14
  priority: Priority
14
15
  content: string
15
16
  assignee: string | null
@@ -35,6 +36,7 @@ export function startServer(featuresDir: string, port: number, webviewDir?: stri
35
36
  let migrating = false
36
37
  let currentEditingFeatureId: string | null = null
37
38
  let lastWrittenContent = ''
39
+ let currentBoardId: string | undefined
38
40
 
39
41
  // Resolve webview static files directory
40
42
  const resolvedWebviewDir = webviewDir || path.join(__dirname, 'standalone-webview')
@@ -121,20 +123,23 @@ export function startServer(featuresDir: string, port: number, webviewDir?: stri
121
123
  // --- Feature loading ---
122
124
 
123
125
  async function loadFeatures(): Promise<void> {
124
- features = await sdk.listCards(sdk.listColumns().map(c => c.id))
126
+ features = await sdk.listCards(sdk.listColumns(currentBoardId).map(c => c.id), currentBoardId)
125
127
  }
126
128
 
127
129
  // --- Message building & broadcast ---
128
130
 
129
131
  function buildInitMessage(): unknown {
132
+ const config = readConfig(workspaceRoot)
130
133
  const settings = sdk.getSettings()
131
134
  settings.showBuildWithAI = false
132
135
  settings.markdownEditorMode = false
133
136
  return {
134
137
  type: 'init',
135
138
  features,
136
- columns: sdk.listColumns(),
137
- settings
139
+ columns: sdk.listColumns(currentBoardId),
140
+ settings,
141
+ boards: sdk.listBoards(),
142
+ currentBoard: currentBoardId || config.defaultBoard
138
143
  }
139
144
  }
140
145
 
@@ -160,6 +165,7 @@ export function startServer(featuresDir: string, port: number, webviewDir?: stri
160
165
  assignee: data.assignee,
161
166
  dueDate: data.dueDate,
162
167
  labels: data.labels,
168
+ boardId: currentBoardId,
163
169
  })
164
170
  await loadFeatures()
165
171
  broadcast(buildInitMessage())
@@ -177,7 +183,7 @@ export function startServer(featuresDir: string, port: number, webviewDir?: stri
177
183
 
178
184
  migrating = true
179
185
  try {
180
- const updated = await sdk.moveCard(featureId, newStatus as FeatureStatus, newOrder)
186
+ const updated = await sdk.moveCard(featureId, newStatus, newOrder, currentBoardId)
181
187
  await loadFeatures()
182
188
  broadcast(buildInitMessage())
183
189
  fireWebhooks(workspaceRoot, 'task.moved', {
@@ -196,7 +202,7 @@ export function startServer(featuresDir: string, port: number, webviewDir?: stri
196
202
 
197
203
  migrating = true
198
204
  try {
199
- const updated = await sdk.updateCard(featureId, updates)
205
+ const updated = await sdk.updateCard(featureId, updates, currentBoardId)
200
206
  lastWrittenContent = serializeFeature(updated)
201
207
  await loadFeatures()
202
208
  broadcast(buildInitMessage())
@@ -213,7 +219,7 @@ export function startServer(featuresDir: string, port: number, webviewDir?: stri
213
219
 
214
220
  try {
215
221
  const deleted = sanitizeFeature(feature)
216
- await sdk.deleteCard(featureId)
222
+ await sdk.deleteCard(featureId, currentBoardId)
217
223
  await loadFeatures()
218
224
  broadcast(buildInitMessage())
219
225
  fireWebhooks(workspaceRoot, 'task.deleted', deleted)
@@ -225,7 +231,7 @@ export function startServer(featuresDir: string, port: number, webviewDir?: stri
225
231
  }
226
232
 
227
233
  function doAddColumn(name: string, color: string): KanbanColumn {
228
- const existingColumns = sdk.listColumns()
234
+ const existingColumns = sdk.listColumns(currentBoardId)
229
235
  const id = name.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '')
230
236
  let uniqueId = id
231
237
  let counter = 1
@@ -233,7 +239,7 @@ export function startServer(featuresDir: string, port: number, webviewDir?: stri
233
239
  uniqueId = `${id}-${counter++}`
234
240
  }
235
241
  const column: KanbanColumn = { id: uniqueId, name, color }
236
- sdk.addColumn(column)
242
+ sdk.addColumn(column, currentBoardId)
237
243
  broadcast(buildInitMessage())
238
244
  fireWebhooks(workspaceRoot, 'column.created', column)
239
245
  return column
@@ -241,7 +247,7 @@ export function startServer(featuresDir: string, port: number, webviewDir?: stri
241
247
 
242
248
  function doEditColumn(columnId: string, updates: { name: string; color: string }): KanbanColumn | null {
243
249
  try {
244
- const columns = sdk.updateColumn(columnId, { name: updates.name, color: updates.color })
250
+ const columns = sdk.updateColumn(columnId, { name: updates.name, color: updates.color }, currentBoardId)
245
251
  const updated = columns.find(c => c.id === columnId) ?? null
246
252
  broadcast(buildInitMessage())
247
253
  if (updated) fireWebhooks(workspaceRoot, 'column.updated', updated)
@@ -253,11 +259,11 @@ export function startServer(featuresDir: string, port: number, webviewDir?: stri
253
259
 
254
260
  async function doRemoveColumn(columnId: string): Promise<{ removed: boolean; error?: string }> {
255
261
  try {
256
- const columns = sdk.listColumns()
262
+ const columns = sdk.listColumns(currentBoardId)
257
263
  if (columns.length <= 1) return { removed: false, error: 'Cannot remove last column' }
258
264
  const col = columns.find(c => c.id === columnId)
259
265
  if (!col) return { removed: false, error: 'Column not found' }
260
- await sdk.removeColumn(columnId)
266
+ await sdk.removeColumn(columnId, currentBoardId)
261
267
  broadcast(buildInitMessage())
262
268
  fireWebhooks(workspaceRoot, 'column.deleted', col)
263
269
  return { removed: true }
@@ -282,7 +288,7 @@ export function startServer(featuresDir: string, port: number, webviewDir?: stri
282
288
  // Register attachment via SDK (skips copy since file is already in place)
283
289
  migrating = true
284
290
  try {
285
- const updated = await sdk.addAttachment(featureId, path.join(featureDir, filename))
291
+ const updated = await sdk.addAttachment(featureId, path.join(featureDir, filename), currentBoardId)
286
292
  lastWrittenContent = serializeFeature(updated)
287
293
  await loadFeatures()
288
294
  } finally {
@@ -297,7 +303,7 @@ export function startServer(featuresDir: string, port: number, webviewDir?: stri
297
303
 
298
304
  migrating = true
299
305
  try {
300
- const updated = await sdk.removeAttachment(featureId, attachment)
306
+ const updated = await sdk.removeAttachment(featureId, attachment, currentBoardId)
301
307
  lastWrittenContent = serializeFeature(updated)
302
308
  await loadFeatures()
303
309
  broadcast(buildInitMessage())
@@ -312,7 +318,7 @@ export function startServer(featuresDir: string, port: number, webviewDir?: stri
312
318
  async function doAddComment(featureId: string, author: string, content: string): Promise<Comment | null> {
313
319
  migrating = true
314
320
  try {
315
- const updated = await sdk.addComment(featureId, author, content)
321
+ const updated = await sdk.addComment(featureId, author, content, currentBoardId)
316
322
  lastWrittenContent = serializeFeature(updated)
317
323
  const comment = updated.comments[updated.comments.length - 1]
318
324
  await loadFeatures()
@@ -329,7 +335,7 @@ export function startServer(featuresDir: string, port: number, webviewDir?: stri
329
335
  async function doUpdateComment(featureId: string, commentId: string, content: string): Promise<Comment | null> {
330
336
  migrating = true
331
337
  try {
332
- const updated = await sdk.updateComment(featureId, commentId, content)
338
+ const updated = await sdk.updateComment(featureId, commentId, content, currentBoardId)
333
339
  lastWrittenContent = serializeFeature(updated)
334
340
  const comment = (updated.comments || []).find(c => c.id === commentId)
335
341
  await loadFeatures()
@@ -351,7 +357,7 @@ export function startServer(featuresDir: string, port: number, webviewDir?: stri
351
357
 
352
358
  migrating = true
353
359
  try {
354
- const updated = await sdk.deleteComment(featureId, commentId)
360
+ const updated = await sdk.deleteComment(featureId, commentId, currentBoardId)
355
361
  lastWrittenContent = serializeFeature(updated)
356
362
  await loadFeatures()
357
363
  broadcast(buildInitMessage())
@@ -519,6 +525,36 @@ export function startServer(featuresDir: string, port: number, webviewDir?: stri
519
525
  break
520
526
  }
521
527
 
528
+ case 'switchBoard':
529
+ currentBoardId = msg.boardId as string
530
+ migrating = true
531
+ try {
532
+ await loadFeatures()
533
+ broadcast(buildInitMessage())
534
+ } finally {
535
+ migrating = false
536
+ }
537
+ break
538
+
539
+ case 'createBoard': {
540
+ const boardName = msg.name as string
541
+ const boardId = generateSlug(boardName) || 'board'
542
+ try {
543
+ sdk.createBoard(boardId, boardName)
544
+ currentBoardId = boardId
545
+ migrating = true
546
+ try {
547
+ await loadFeatures()
548
+ broadcast(buildInitMessage())
549
+ } finally {
550
+ migrating = false
551
+ }
552
+ } catch (err) {
553
+ console.error('Failed to create board:', err)
554
+ }
555
+ break
556
+ }
557
+
522
558
  // VSCode-specific actions — no-ops in standalone
523
559
  case 'openFile':
524
560
  case 'focusMenuBar':
@@ -552,9 +588,191 @@ export function startServer(featuresDir: string, port: number, webviewDir?: stri
552
588
  const route = (expectedMethod: string, pattern: string): Record<string, string> | null =>
553
589
  matchRoute(expectedMethod, method, pathname, pattern)
554
590
 
591
+ // ==================== BOARDS API ====================
592
+
593
+ let params = route('GET', '/api/boards')
594
+ if (params) {
595
+ return jsonOk(res, sdk.listBoards())
596
+ }
597
+
598
+ params = route('POST', '/api/boards')
599
+ if (params) {
600
+ try {
601
+ const body = await readBody(req)
602
+ const id = body.id as string
603
+ const name = body.name as string
604
+ if (!id) return jsonError(res, 400, 'id is required')
605
+ if (!name) return jsonError(res, 400, 'name is required')
606
+ const board = sdk.createBoard(id, name, {
607
+ description: body.description as string | undefined,
608
+ columns: body.columns as KanbanColumn[] | undefined,
609
+ })
610
+ return jsonOk(res, board, 201)
611
+ } catch (err) {
612
+ return jsonError(res, 400, String(err))
613
+ }
614
+ }
615
+
616
+ params = route('GET', '/api/boards/:boardId')
617
+ if (params) {
618
+ try {
619
+ const { boardId } = params
620
+ const board = sdk.getBoard(boardId)
621
+ return jsonOk(res, board)
622
+ } catch (err) {
623
+ return jsonError(res, 404, String(err))
624
+ }
625
+ }
626
+
627
+ params = route('PUT', '/api/boards/:boardId')
628
+ if (params) {
629
+ try {
630
+ const { boardId } = params
631
+ const body = await readBody(req)
632
+ const board = sdk.updateBoard(boardId, body as Record<string, unknown>)
633
+ return jsonOk(res, board)
634
+ } catch (err) {
635
+ return jsonError(res, 400, String(err))
636
+ }
637
+ }
638
+
639
+ params = route('DELETE', '/api/boards/:boardId')
640
+ if (params) {
641
+ try {
642
+ const { boardId } = params
643
+ await sdk.deleteBoard(boardId)
644
+ return jsonOk(res, { deleted: true })
645
+ } catch (err) {
646
+ return jsonError(res, 400, String(err))
647
+ }
648
+ }
649
+
650
+ // Transfer a task between boards
651
+ params = route('POST', '/api/boards/:boardId/tasks/:id/transfer')
652
+ if (params) {
653
+ try {
654
+ const { boardId, id } = params
655
+ const body = await readBody(req)
656
+ const config = readConfig(workspaceRoot)
657
+ const fromBoard = currentBoardId || config.defaultBoard
658
+ const targetStatus = body.targetStatus as string | undefined
659
+ const card = await sdk.transferCard(id, fromBoard, boardId, targetStatus)
660
+ return jsonOk(res, sanitizeFeature(card))
661
+ } catch (err) {
662
+ return jsonError(res, 400, String(err))
663
+ }
664
+ }
665
+
666
+ // ==================== BOARD-SCOPED TASKS API ====================
667
+
668
+ params = route('GET', '/api/boards/:boardId/tasks')
669
+ if (params) {
670
+ try {
671
+ const { boardId } = params
672
+ const boardColumns = sdk.listColumns(boardId)
673
+ const boardTasks = await sdk.listCards(boardColumns.map(c => c.id), boardId)
674
+ let result = boardTasks.map(sanitizeFeature)
675
+ const status = url.searchParams.get('status')
676
+ if (status) result = result.filter(f => f.status === status)
677
+ const priority = url.searchParams.get('priority')
678
+ if (priority) result = result.filter(f => f.priority === priority)
679
+ const assignee = url.searchParams.get('assignee')
680
+ if (assignee) result = result.filter(f => f.assignee === assignee)
681
+ const label = url.searchParams.get('label')
682
+ if (label) result = result.filter(f => f.labels.includes(label))
683
+ return jsonOk(res, result)
684
+ } catch (err) {
685
+ return jsonError(res, 400, String(err))
686
+ }
687
+ }
688
+
689
+ params = route('POST', '/api/boards/:boardId/tasks')
690
+ if (params) {
691
+ try {
692
+ const { boardId } = params
693
+ const body = await readBody(req)
694
+ const content = (body.content as string) || ''
695
+ if (!content) return jsonError(res, 400, 'content is required')
696
+ const feature = await sdk.createCard({
697
+ content,
698
+ status: (body.status as string) || 'backlog',
699
+ priority: (body.priority as Priority) || 'medium',
700
+ assignee: (body.assignee as string) || null,
701
+ dueDate: (body.dueDate as string) || null,
702
+ labels: (body.labels as string[]) || [],
703
+ boardId,
704
+ })
705
+ return jsonOk(res, sanitizeFeature(feature), 201)
706
+ } catch (err) {
707
+ return jsonError(res, 400, String(err))
708
+ }
709
+ }
710
+
711
+ params = route('GET', '/api/boards/:boardId/tasks/:id')
712
+ if (params) {
713
+ try {
714
+ const { boardId, id } = params
715
+ const card = await sdk.getCard(id, boardId)
716
+ if (!card) return jsonError(res, 404, 'Task not found')
717
+ return jsonOk(res, sanitizeFeature(card))
718
+ } catch (err) {
719
+ return jsonError(res, 400, String(err))
720
+ }
721
+ }
722
+
723
+ params = route('PUT', '/api/boards/:boardId/tasks/:id')
724
+ if (params) {
725
+ try {
726
+ const { boardId, id } = params
727
+ const body = await readBody(req)
728
+ const feature = await sdk.updateCard(id, body as Partial<Feature>, boardId)
729
+ return jsonOk(res, sanitizeFeature(feature))
730
+ } catch (err) {
731
+ return jsonError(res, 400, String(err))
732
+ }
733
+ }
734
+
735
+ params = route('PATCH', '/api/boards/:boardId/tasks/:id/move')
736
+ if (params) {
737
+ try {
738
+ const { boardId, id } = params
739
+ const body = await readBody(req)
740
+ const newStatus = body.status as string
741
+ const position = body.position as number ?? 0
742
+ if (!newStatus) return jsonError(res, 400, 'status is required')
743
+ const feature = await sdk.moveCard(id, newStatus, position, boardId)
744
+ return jsonOk(res, sanitizeFeature(feature))
745
+ } catch (err) {
746
+ return jsonError(res, 400, String(err))
747
+ }
748
+ }
749
+
750
+ params = route('DELETE', '/api/boards/:boardId/tasks/:id')
751
+ if (params) {
752
+ try {
753
+ const { boardId, id } = params
754
+ await sdk.deleteCard(id, boardId)
755
+ return jsonOk(res, { deleted: true })
756
+ } catch (err) {
757
+ return jsonError(res, 400, String(err))
758
+ }
759
+ }
760
+
761
+ // ==================== BOARD-SCOPED COLUMNS API ====================
762
+
763
+ params = route('GET', '/api/boards/:boardId/columns')
764
+ if (params) {
765
+ try {
766
+ const { boardId } = params
767
+ return jsonOk(res, sdk.listColumns(boardId))
768
+ } catch (err) {
769
+ return jsonError(res, 400, String(err))
770
+ }
771
+ }
772
+
555
773
  // ==================== TASKS API ====================
556
774
 
557
- let params = route('GET', '/api/tasks')
775
+ params = route('GET', '/api/tasks')
558
776
  if (params) {
559
777
  await loadFeatures()
560
778
  let result = features.map(sanitizeFeature)
@@ -575,7 +793,7 @@ export function startServer(featuresDir: string, port: number, webviewDir?: stri
575
793
  const body = await readBody(req)
576
794
  const data: CreateFeatureData = {
577
795
  content: (body.content as string) || '',
578
- status: (body.status as FeatureStatus) || 'backlog',
796
+ status: (body.status as string) || 'backlog',
579
797
  priority: (body.priority as Priority) || 'medium',
580
798
  assignee: (body.assignee as string) || null,
581
799
  dueDate: (body.dueDate as string) || null,
@@ -744,7 +962,7 @@ export function startServer(featuresDir: string, port: number, webviewDir?: stri
744
962
 
745
963
  params = route('GET', '/api/columns')
746
964
  if (params) {
747
- return jsonOk(res, sdk.listColumns())
965
+ return jsonOk(res, sdk.listColumns(currentBoardId))
748
966
  }
749
967
 
750
968
  params = route('POST', '/api/columns')
@@ -8,7 +8,7 @@ import { Toolbar } from './components/Toolbar'
8
8
  import { UndoToast } from './components/UndoToast'
9
9
  import { SettingsPanel } from './components/SettingsPanel'
10
10
  import { ColumnDialog } from './components/ColumnDialog'
11
- import type { Comment, Feature, FeatureStatus, KanbanColumn, Priority, ExtensionMessage, FeatureFrontmatter, CardDisplaySettings } from '../shared/types'
11
+ import type { Comment, Feature, KanbanColumn, Priority, ExtensionMessage, FeatureFrontmatter, CardDisplaySettings } from '../shared/types'
12
12
  import { getTitleFromContent } from '../shared/types'
13
13
 
14
14
  // Declare vscode API type
@@ -28,13 +28,15 @@ function App(): React.JSX.Element {
28
28
  settingsOpen,
29
29
  setFeatures,
30
30
  setColumns,
31
+ setBoards,
32
+ setCurrentBoard,
31
33
  setIsDarkMode,
32
34
  setCardSettings,
33
35
  setSettingsOpen
34
36
  } = useStore()
35
37
 
36
38
  const [createFeatureOpen, setCreateFeatureOpen] = useState(false)
37
- const [createFeatureStatus, setCreateFeatureStatus] = useState<FeatureStatus>('backlog')
39
+ const [createFeatureStatus, setCreateFeatureStatus] = useState<string>('backlog')
38
40
 
39
41
  // Column dialog state
40
42
  const [columnDialogOpen, setColumnDialogOpen] = useState(false)
@@ -206,6 +208,8 @@ function App(): React.JSX.Element {
206
208
  case 'init':
207
209
  setFeatures(message.features)
208
210
  setColumns(message.columns)
211
+ if (message.boards) setBoards(message.boards)
212
+ if (message.currentBoard) setCurrentBoard(message.currentBoard)
209
213
  if (message.settings) {
210
214
  if (message.settings.markdownEditorMode && editingFeature) {
211
215
  setEditingFeature(null)
@@ -246,7 +250,7 @@ function App(): React.JSX.Element {
246
250
  vscode.postMessage({ type: 'ready' })
247
251
 
248
252
  return () => window.removeEventListener('message', handleMessage)
249
- }, [setFeatures, setColumns, setCardSettings, setSettingsOpen])
253
+ }, [setFeatures, setColumns, setBoards, setCurrentBoard, setCardSettings, setSettingsOpen])
250
254
 
251
255
  const handleFeatureClick = (feature: Feature): void => {
252
256
  // Request feature content for inline editing
@@ -354,12 +358,12 @@ function App(): React.JSX.Element {
354
358
  }
355
359
 
356
360
  const handleAddFeatureInColumn = (status: string): void => {
357
- setCreateFeatureStatus(status as FeatureStatus)
361
+ setCreateFeatureStatus(status)
358
362
  setCreateFeatureOpen(true)
359
363
  }
360
364
 
361
365
  const handleCreateFeature = (data: {
362
- status: FeatureStatus
366
+ status: string
363
367
  priority: Priority
364
368
  content: string
365
369
  }): void => {
@@ -390,7 +394,7 @@ function App(): React.JSX.Element {
390
394
 
391
395
  const updated = features.map(f =>
392
396
  f.id === featureId
393
- ? { ...f, status: newStatus as FeatureStatus, order: newOrderKey }
397
+ ? { ...f, status: newStatus, order: newOrderKey }
394
398
  : f
395
399
  )
396
400
  setFeatures(updated)
@@ -416,7 +420,13 @@ function App(): React.JSX.Element {
416
420
 
417
421
  return (
418
422
  <div className="h-full w-full flex flex-col bg-[var(--vscode-editor-background)]">
419
- <Toolbar onOpenSettings={() => vscode.postMessage({ type: 'openSettings' })} onAddColumn={handleAddColumn} onToggleTheme={() => vscode.postMessage({ type: 'toggleTheme' })} />
423
+ <Toolbar
424
+ onOpenSettings={() => vscode.postMessage({ type: 'openSettings' })}
425
+ onAddColumn={handleAddColumn}
426
+ onToggleTheme={() => vscode.postMessage({ type: 'toggleTheme' })}
427
+ onSwitchBoard={(boardId) => vscode.postMessage({ type: 'switchBoard', boardId })}
428
+ onCreateBoard={(name) => vscode.postMessage({ type: 'createBoard', name })}
429
+ />
420
430
  <div className="flex-1 flex overflow-hidden">
421
431
  <div className={editingFeature ? 'w-1/2' : 'w-full'}>
422
432
  <KanbanBoard
@@ -1,4 +1,5 @@
1
- import { Search, X, Columns, Rows, Settings, Plus, Moon, Sun } from 'lucide-react'
1
+ import { useState, useRef, useEffect } from 'react'
2
+ import { Search, X, Columns, Rows, Settings, Plus, Moon, Sun, ChevronDown, Check } from 'lucide-react'
2
3
  import { useStore, type DueDateFilter } from '../store'
3
4
  import type { Priority } from '../../shared/types'
4
5
 
@@ -21,7 +22,7 @@ const dueDateOptions: { value: DueDateFilter; label: string }[] = [
21
22
  const selectClassName =
22
23
  '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
 
24
- export function Toolbar({ onOpenSettings, onAddColumn, onToggleTheme }: { onOpenSettings: () => void; onAddColumn: () => void; onToggleTheme: () => void }) {
25
+ export function Toolbar({ onOpenSettings, onAddColumn, onToggleTheme, onSwitchBoard, onCreateBoard }: { onOpenSettings: () => void; onAddColumn: () => void; onToggleTheme: () => void; onSwitchBoard: (boardId: string) => void; onCreateBoard: (name: string) => void }) {
25
26
  const {
26
27
  searchQuery,
27
28
  setSearchQuery,
@@ -40,15 +41,110 @@ export function Toolbar({ onOpenSettings, onAddColumn, onToggleTheme }: { onOpen
40
41
  layout,
41
42
  toggleLayout,
42
43
  isDarkMode,
43
- cardSettings
44
+ cardSettings,
45
+ boards,
46
+ currentBoard
44
47
  } = useStore()
45
48
 
46
49
  const assignees = getUniqueAssignees()
47
50
  const labels = getUniqueLabels()
48
51
  const filtersActive = hasActiveFilters()
49
52
 
53
+ const [boardDropdownOpen, setBoardDropdownOpen] = useState(false)
54
+ const [creatingBoard, setCreatingBoard] = useState(false)
55
+ const [newBoardName, setNewBoardName] = useState('')
56
+ const dropdownRef = useRef<HTMLDivElement>(null)
57
+ const newBoardInputRef = useRef<HTMLInputElement>(null)
58
+
59
+ useEffect(() => {
60
+ const handleClickOutside = (e: MouseEvent) => {
61
+ if (dropdownRef.current && !dropdownRef.current.contains(e.target as Node)) {
62
+ setBoardDropdownOpen(false)
63
+ setCreatingBoard(false)
64
+ setNewBoardName('')
65
+ }
66
+ }
67
+ if (boardDropdownOpen) {
68
+ document.addEventListener('mousedown', handleClickOutside)
69
+ }
70
+ return () => document.removeEventListener('mousedown', handleClickOutside)
71
+ }, [boardDropdownOpen])
72
+
73
+ useEffect(() => {
74
+ if (creatingBoard && newBoardInputRef.current) {
75
+ newBoardInputRef.current.focus()
76
+ }
77
+ }, [creatingBoard])
78
+
79
+ const currentBoardName = boards.find(b => b.id === currentBoard)?.name || currentBoard
80
+
81
+ const handleCreateBoard = () => {
82
+ const name = newBoardName.trim()
83
+ if (!name) return
84
+ onCreateBoard(name)
85
+ setNewBoardName('')
86
+ setCreatingBoard(false)
87
+ setBoardDropdownOpen(false)
88
+ }
89
+
50
90
  return (
51
91
  <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">
92
+ {/* Board Selector */}
93
+ <div className="relative" ref={dropdownRef}>
94
+ <button
95
+ type="button"
96
+ onClick={() => setBoardDropdownOpen(!boardDropdownOpen)}
97
+ className="flex items-center gap-1.5 px-2 py-1.5 text-sm bg-white dark:bg-zinc-800 border border-zinc-200 dark:border-zinc-600 rounded-md hover:bg-zinc-50 dark:hover:bg-zinc-700 text-zinc-900 dark:text-zinc-100 transition-colors"
98
+ >
99
+ <span className="max-w-[120px] truncate">{currentBoardName}</span>
100
+ <ChevronDown size={14} className={`text-zinc-400 transition-transform ${boardDropdownOpen ? 'rotate-180' : ''}`} />
101
+ </button>
102
+ {boardDropdownOpen && (
103
+ <div className="absolute top-full left-0 mt-1 min-w-[180px] bg-white dark:bg-zinc-800 border border-zinc-200 dark:border-zinc-600 rounded-md shadow-lg z-50 py-1">
104
+ {boards.map((b) => (
105
+ <button
106
+ key={b.id}
107
+ type="button"
108
+ onClick={() => {
109
+ onSwitchBoard(b.id)
110
+ setBoardDropdownOpen(false)
111
+ }}
112
+ className="w-full flex items-center gap-2 px-3 py-1.5 text-sm text-left hover:bg-zinc-100 dark:hover:bg-zinc-700 text-zinc-900 dark:text-zinc-100 transition-colors"
113
+ >
114
+ <Check size={14} className={b.id === currentBoard ? 'text-blue-500' : 'invisible'} />
115
+ <span className="truncate">{b.name}</span>
116
+ </button>
117
+ ))}
118
+ <div className="border-t border-zinc-200 dark:border-zinc-600 my-1" />
119
+ {creatingBoard ? (
120
+ <div className="px-3 py-1.5">
121
+ <input
122
+ ref={newBoardInputRef}
123
+ type="text"
124
+ value={newBoardName}
125
+ onChange={(e) => setNewBoardName(e.target.value)}
126
+ onKeyDown={(e) => {
127
+ if (e.key === 'Enter') handleCreateBoard()
128
+ if (e.key === 'Escape') { setCreatingBoard(false); setNewBoardName('') }
129
+ }}
130
+ placeholder="Board name..."
131
+ className="w-full px-2 py-1 text-sm bg-zinc-50 dark:bg-zinc-900 border border-zinc-300 dark:border-zinc-500 rounded focus:outline-none focus:ring-2 focus:ring-blue-500 text-zinc-900 dark:text-zinc-100 placeholder-zinc-400"
132
+ />
133
+ </div>
134
+ ) : (
135
+ <button
136
+ type="button"
137
+ onClick={() => setCreatingBoard(true)}
138
+ className="w-full flex items-center gap-2 px-3 py-1.5 text-sm text-left hover:bg-zinc-100 dark:hover:bg-zinc-700 text-blue-600 dark:text-blue-400 transition-colors"
139
+ >
140
+ <Plus size={14} />
141
+ <span>New Board</span>
142
+ </button>
143
+ )}
144
+ </div>
145
+ )}
146
+ </div>
147
+
52
148
  {/* Search */}
53
149
  <div className="relative flex-1 min-w-[180px] max-w-xs">
54
150
  <Search
@@ -1,5 +1,5 @@
1
1
  import { create } from 'zustand'
2
- import type { Feature, FeatureStatus, KanbanColumn, Priority, CardDisplaySettings } from '../../shared/types'
2
+ import type { Feature, KanbanColumn, Priority, CardDisplaySettings, BoardInfo } from '../../shared/types'
3
3
 
4
4
  export type DueDateFilter = 'all' | 'overdue' | 'today' | 'this-week' | 'no-date'
5
5
  export type LayoutMode = 'horizontal' | 'vertical'
@@ -7,6 +7,8 @@ export type LayoutMode = 'horizontal' | 'vertical'
7
7
  interface KanbanState {
8
8
  features: Feature[]
9
9
  columns: KanbanColumn[]
10
+ boards: BoardInfo[]
11
+ currentBoard: string
10
12
  isDarkMode: boolean
11
13
  searchQuery: string
12
14
  priorityFilter: Priority | 'all'
@@ -19,6 +21,8 @@ interface KanbanState {
19
21
 
20
22
  setFeatures: (features: Feature[]) => void
21
23
  setColumns: (columns: KanbanColumn[]) => void
24
+ setBoards: (boards: BoardInfo[]) => void
25
+ setCurrentBoard: (boardId: string) => void
22
26
  setIsDarkMode: (dark: boolean) => void
23
27
  setCardSettings: (settings: CardDisplaySettings) => void
24
28
  setSettingsOpen: (open: boolean) => void
@@ -34,8 +38,8 @@ interface KanbanState {
34
38
  addFeature: (feature: Feature) => void
35
39
  updateFeature: (id: string, updates: Partial<Feature>) => void
36
40
  removeFeature: (id: string) => void
37
- getFeaturesByStatus: (status: FeatureStatus) => Feature[]
38
- getFilteredFeaturesByStatus: (status: FeatureStatus) => Feature[]
41
+ getFeaturesByStatus: (status: string) => Feature[]
42
+ getFilteredFeaturesByStatus: (status: string) => Feature[]
39
43
  getUniqueAssignees: () => string[]
40
44
  getUniqueLabels: () => string[]
41
45
  hasActiveFilters: () => boolean
@@ -80,6 +84,8 @@ const isOverdue = (date: Date): boolean => {
80
84
  export const useStore = create<KanbanState>((set, get) => ({
81
85
  features: [],
82
86
  columns: [],
87
+ boards: [],
88
+ currentBoard: 'default',
83
89
  isDarkMode: getInitialDarkMode(),
84
90
  searchQuery: '',
85
91
  priorityFilter: 'all',
@@ -103,6 +109,8 @@ export const useStore = create<KanbanState>((set, get) => ({
103
109
 
104
110
  setFeatures: (features) => set({ features }),
105
111
  setColumns: (columns) => set({ columns }),
112
+ setBoards: (boards) => set({ boards }),
113
+ setCurrentBoard: (boardId) => set({ currentBoard: boardId }),
106
114
  setIsDarkMode: (dark) => set({ isDarkMode: dark }),
107
115
  setCardSettings: (settings) => set({ cardSettings: settings }),
108
116
  setSettingsOpen: (open) => set({ settingsOpen: open }),