kanban-lite 1.0.21 → 1.0.23

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 (44) hide show
  1. package/{CLAUDE.md → AGENTS.md} +13 -0
  2. package/CHANGELOG.md +68 -0
  3. package/README.md +10 -0
  4. package/dist/cli.js +168 -102
  5. package/dist/extension.js +178 -104
  6. package/dist/mcp-server.js +145 -95
  7. package/dist/sdk/index.cjs +126 -93
  8. package/dist/sdk/index.mjs +126 -93
  9. package/dist/sdk/sdk/KanbanSDK.d.ts +39 -7
  10. package/dist/sdk/shared/config.d.ts +4 -0
  11. package/dist/sdk/shared/types.d.ts +4 -0
  12. package/dist/standalone-webview/index.js +58 -58
  13. package/dist/standalone-webview/index.js.map +1 -1
  14. package/dist/standalone-webview/style.css +1 -1
  15. package/dist/standalone.js +606 -364
  16. package/dist/webview/index.js +57 -57
  17. package/dist/webview/index.js.map +1 -1
  18. package/dist/webview/style.css +1 -1
  19. package/docs/plans/2026-02-26-settings-tabs-design.md +40 -0
  20. package/docs/plans/2026-02-26-settings-tabs.md +166 -0
  21. package/docs/plans/2026-02-27-zoom-settings-design.md +82 -0
  22. package/docs/plans/2026-02-27-zoom-settings.md +395 -0
  23. package/docs/sdk.md +3 -6
  24. package/package.json +1 -1
  25. package/src/cli/index.ts +12 -2
  26. package/src/extension/KanbanPanel.ts +25 -5
  27. package/src/mcp-server/index.ts +20 -2
  28. package/src/sdk/KanbanSDK.ts +64 -7
  29. package/src/sdk/__tests__/KanbanSDK.test.ts +17 -1
  30. package/src/sdk/__tests__/metadata.test.ts +3 -1
  31. package/src/sdk/__tests__/multi-board.test.ts +2 -0
  32. package/src/sdk/parser.ts +50 -83
  33. package/src/shared/config.ts +14 -2
  34. package/src/shared/types.ts +4 -0
  35. package/src/standalone/__tests__/server.integration.test.ts +2 -2
  36. package/src/standalone/index.ts +7 -4
  37. package/src/standalone/server.ts +31 -6
  38. package/src/webview/App.tsx +42 -3
  39. package/src/webview/assets/main.css +31 -2
  40. package/src/webview/components/KanbanBoard.tsx +35 -3
  41. package/src/webview/components/KanbanColumn.tsx +40 -4
  42. package/src/webview/components/SettingsPanel.tsx +179 -77
  43. package/src/webview/components/Toolbar.tsx +127 -32
  44. package/src/webview/store/index.ts +26 -28
@@ -1,10 +1,18 @@
1
1
  import { useState, useRef, useEffect } from 'react'
2
- import { Plus, MoreVertical, Pencil, Trash2 } from 'lucide-react'
2
+ import { Plus, MoreVertical, Pencil, Trash2, Check } from 'lucide-react'
3
3
  import { FeatureCard } from './FeatureCard'
4
4
  import type { Feature, KanbanColumn as KanbanColumnType } from '../../shared/types'
5
- import type { LayoutMode } from '../store'
5
+ import type { LayoutMode, SortOrder } from '../store'
6
6
  import type { DropTarget } from './KanbanBoard'
7
7
 
8
+ const SORT_OPTIONS: { value: SortOrder; label: string }[] = [
9
+ { value: 'order', label: 'Board Order' },
10
+ { value: 'created:asc', label: 'Created (oldest)' },
11
+ { value: 'created:desc', label: 'Created (newest)' },
12
+ { value: 'modified:asc', label: 'Modified (oldest)' },
13
+ { value: 'modified:desc', label: 'Modified (newest)' },
14
+ ]
15
+
8
16
  interface KanbanColumnProps {
9
17
  column: KanbanColumnType
10
18
  features: Feature[]
@@ -12,6 +20,7 @@ interface KanbanColumnProps {
12
20
  onAddFeature: (status: string) => void
13
21
  onEditColumn: (columnId: string) => void
14
22
  onRemoveColumn: (columnId: string) => void
23
+ onCleanupColumn: (columnId: string) => void
15
24
  onDragStart: (e: React.DragEvent, feature: Feature) => void
16
25
  onDragOver: (e: React.DragEvent) => void
17
26
  onDragOverCard: (e: React.DragEvent, columnId: string, cardIndex: number) => void
@@ -23,6 +32,8 @@ interface KanbanColumnProps {
23
32
  isDeletedColumn?: boolean
24
33
  onPurgeColumn?: () => void
25
34
  selectedFeatureId?: string
35
+ sort: SortOrder
36
+ onSortChange: (sort: SortOrder) => void
26
37
  }
27
38
 
28
39
  export function KanbanColumn({
@@ -32,6 +43,7 @@ export function KanbanColumn({
32
43
  onAddFeature,
33
44
  onEditColumn,
34
45
  onRemoveColumn,
46
+ onCleanupColumn,
35
47
  onDragStart,
36
48
  onDragOver,
37
49
  onDragOverCard,
@@ -42,7 +54,9 @@ export function KanbanColumn({
42
54
  layout,
43
55
  isDeletedColumn,
44
56
  onPurgeColumn,
45
- selectedFeatureId
57
+ selectedFeatureId,
58
+ sort,
59
+ onSortChange
46
60
  }: KanbanColumnProps) {
47
61
  const isVertical = layout === 'vertical'
48
62
  const isDropTarget = dropTarget && dropTarget.columnId === column.id
@@ -114,7 +128,20 @@ export function KanbanColumn({
114
128
  <MoreVertical size={16} className="text-zinc-500" />
115
129
  </button>
116
130
  {menuOpen && (
117
- <div className="absolute right-0 top-full mt-1 z-50 bg-white dark:bg-zinc-800 border border-zinc-200 dark:border-zinc-600 rounded-md shadow-lg py-1 min-w-[140px]">
131
+ <div className="absolute right-0 top-full mt-1 z-50 bg-white dark:bg-zinc-800 border border-zinc-200 dark:border-zinc-600 rounded-md shadow-lg py-1 min-w-[170px]">
132
+ <div className="px-3 py-1 text-xs font-medium text-zinc-400 dark:text-zinc-500 uppercase tracking-wide">Sort by</div>
133
+ {SORT_OPTIONS.map((opt) => (
134
+ <button
135
+ key={opt.value}
136
+ type="button"
137
+ onClick={() => { onSortChange(opt.value) }}
138
+ className="flex items-center gap-2 w-full px-3 py-1.5 text-sm text-zinc-700 dark:text-zinc-200 hover:bg-zinc-100 dark:hover:bg-zinc-700 transition-colors"
139
+ >
140
+ <Check size={14} className={sort === opt.value ? 'text-blue-500' : 'invisible'} />
141
+ {opt.label}
142
+ </button>
143
+ ))}
144
+ <div className="border-t border-zinc-200 dark:border-zinc-600 my-1" />
118
145
  <button
119
146
  type="button"
120
147
  onClick={() => { setMenuOpen(false); onEditColumn(column.id) }}
@@ -131,6 +158,14 @@ export function KanbanColumn({
131
158
  <Trash2 size={14} />
132
159
  Remove List
133
160
  </button>
161
+ <button
162
+ type="button"
163
+ onClick={() => { setMenuOpen(false); onCleanupColumn(column.id) }}
164
+ className="flex items-center gap-2 w-full px-3 py-1.5 text-sm text-orange-600 dark:text-orange-400 hover:bg-zinc-100 dark:hover:bg-zinc-700 transition-colors"
165
+ >
166
+ <Trash2 size={14} />
167
+ Cleanup List
168
+ </button>
134
169
  </div>
135
170
  )}
136
171
  </div>
@@ -155,6 +190,7 @@ export function KanbanColumn({
155
190
  )}
156
191
  <div
157
192
  draggable
193
+ data-card-id={feature.id}
158
194
  onDragStart={(e) => onDragStart(e, feature)}
159
195
  onDragOver={(e) => onDragOverCard(e, column.id, index)}
160
196
  onDragEnd={onDragEnd}
@@ -182,6 +182,53 @@ function SettingsDropdown({ label, value, options, onChange }: {
182
182
  )
183
183
  }
184
184
 
185
+ function SettingsSlider({ label, description, value, min, max, step, unit, onChange }: {
186
+ label: string
187
+ description?: string
188
+ value: number
189
+ min: number
190
+ max: number
191
+ step: number
192
+ unit?: string
193
+ onChange: (v: number) => void
194
+ }) {
195
+ const pct = ((value - min) / (max - min)) * 100
196
+ return (
197
+ <div
198
+ className="flex items-center justify-between gap-4 px-4 py-2 transition-colors"
199
+ onMouseEnter={e => e.currentTarget.style.background = 'var(--vscode-list-hoverBackground)'}
200
+ onMouseLeave={e => e.currentTarget.style.background = 'transparent'}
201
+ >
202
+ <div className="flex-1 min-w-0">
203
+ <div className="text-sm" style={{ color: 'var(--vscode-foreground)' }}>{label}</div>
204
+ {description && (
205
+ <div className="text-xs mt-0.5" style={{ color: 'var(--vscode-descriptionForeground)' }}>{description}</div>
206
+ )}
207
+ </div>
208
+ <div className="flex items-center gap-2 shrink-0">
209
+ <input
210
+ type="range"
211
+ min={min}
212
+ max={max}
213
+ step={step}
214
+ value={value}
215
+ onChange={e => onChange(Number(e.target.value))}
216
+ className="settings-slider"
217
+ style={{
218
+ background: `linear-gradient(to right, var(--vscode-button-background) ${pct}%, var(--vscode-badge-background, #6b7280) ${pct}%)`,
219
+ }}
220
+ />
221
+ <span
222
+ className="text-xs font-mono w-12 text-right tabular-nums"
223
+ style={{ color: 'var(--vscode-foreground)' }}
224
+ >
225
+ {value}{unit || '%'}
226
+ </span>
227
+ </div>
228
+ </div>
229
+ )
230
+ }
231
+
185
232
  function ColorPicker({ value, onChange }: { value: string; onChange: (color: string) => void }) {
186
233
  const [isOpen, setIsOpen] = useState(false)
187
234
  const [customHex, setCustomHex] = useState('')
@@ -509,6 +556,7 @@ function LabelsSection({ onSetLabel, onRenameLabel, onDeleteLabel }: {
509
556
 
510
557
  function SettingsPanelContent({ settings, workspace, onClose, onSave, onSetLabel, onRenameLabel, onDeleteLabel }: Omit<SettingsPanelProps, 'isOpen'>) {
511
558
  const [local, setLocal] = useState<CardDisplaySettings>(settings)
559
+ const [activeTab, setActiveTab] = useState<'general' | 'defaults' | 'labels'>('general')
512
560
 
513
561
  useEffect(() => { setLocal(settings) }, [settings])
514
562
 
@@ -556,90 +604,144 @@ function SettingsPanelContent({ settings, workspace, onClose, onSave, onSetLabel
556
604
  </button>
557
605
  </div>
558
606
 
607
+ {/* Tab Bar */}
608
+ <div
609
+ className="flex"
610
+ style={{ borderBottom: '1px solid var(--vscode-panel-border)' }}
611
+ >
612
+ {(['general', 'defaults', 'labels'] as const).map(tab => (
613
+ <button
614
+ key={tab}
615
+ type="button"
616
+ onClick={() => setActiveTab(tab)}
617
+ className="px-4 py-2.5 text-xs font-medium transition-colors relative"
618
+ style={{
619
+ color: activeTab === tab
620
+ ? 'var(--vscode-foreground)'
621
+ : 'var(--vscode-descriptionForeground)',
622
+ background: 'transparent',
623
+ }}
624
+ >
625
+ {tab.charAt(0).toUpperCase() + tab.slice(1)}
626
+ {activeTab === tab && (
627
+ <span
628
+ className="absolute bottom-0 left-0 right-0 h-0.5"
629
+ style={{ background: 'var(--vscode-button-background)' }}
630
+ />
631
+ )}
632
+ </button>
633
+ ))}
634
+ </div>
635
+
559
636
  {/* Content */}
560
637
  <div className="flex-1 overflow-auto">
561
- {workspace && (
638
+ {activeTab === 'general' && (
562
639
  <>
563
- <SettingsSection title="Workspace">
564
- <SettingsInfo label="Project Path" value={workspace.projectPath} />
565
- <SettingsInfo label="Features Directory" value={workspace.featuresDirectory} />
566
- <SettingsInfo label="Server Port" value={String(workspace.port)} />
567
- <SettingsInfo label="Config Version" value={String(workspace.configVersion)} />
640
+ {workspace && (
641
+ <>
642
+ <SettingsSection title="Workspace">
643
+ <SettingsInfo label="Project Path" value={workspace.projectPath} />
644
+ <SettingsInfo label="Features Directory" value={workspace.featuresDirectory} />
645
+ <SettingsInfo label="Server Port" value={String(workspace.port)} />
646
+ <SettingsInfo label="Config Version" value={String(workspace.configVersion)} />
647
+ </SettingsSection>
648
+ <div style={{ borderTop: '1px solid var(--vscode-panel-border)' }} />
649
+ </>
650
+ )}
651
+ <SettingsSection title="Card Display">
652
+ <SettingsToggle
653
+ label="Show Priority Badges"
654
+ description="Display priority indicators on feature cards"
655
+ checked={local.showPriorityBadges}
656
+ onChange={v => update({ showPriorityBadges: v })}
657
+ />
658
+ <SettingsToggle
659
+ label="Show Assignee"
660
+ description="Display assigned person on feature cards"
661
+ checked={local.showAssignee}
662
+ onChange={v => update({ showAssignee: v })}
663
+ />
664
+ <SettingsToggle
665
+ label="Show Due Date"
666
+ description="Display due dates on feature cards"
667
+ checked={local.showDueDate}
668
+ onChange={v => update({ showDueDate: v })}
669
+ />
670
+ <SettingsToggle
671
+ label="Show Labels"
672
+ description="Display labels on feature cards and in editors"
673
+ checked={local.showLabels}
674
+ onChange={v => update({ showLabels: v })}
675
+ />
676
+ <SettingsToggle
677
+ label="Show Filename"
678
+ description="Display the source markdown filename on cards"
679
+ checked={local.showFileName}
680
+ onChange={v => update({ showFileName: v })}
681
+ />
682
+ <SettingsToggle
683
+ label="Compact Mode"
684
+ description="Use compact card layout to show more features"
685
+ checked={local.compactMode}
686
+ onChange={v => update({ compactMode: v })}
687
+ />
688
+ <SettingsToggle
689
+ label="Show Deleted Column"
690
+ description="Display the Deleted column to manage soft-deleted cards"
691
+ checked={local.showDeletedColumn}
692
+ onChange={v => update({ showDeletedColumn: v })}
693
+ />
568
694
  </SettingsSection>
569
695
  <div style={{ borderTop: '1px solid var(--vscode-panel-border)' }} />
696
+ <SettingsSection title="Zoom">
697
+ <SettingsSlider
698
+ label="Board Zoom"
699
+ description="Scale text size on the board view"
700
+ value={local.boardZoom}
701
+ min={75}
702
+ max={150}
703
+ step={5}
704
+ onChange={v => update({ boardZoom: v })}
705
+ />
706
+ <SettingsSlider
707
+ label="Card Detail Zoom"
708
+ description="Scale text size in the card detail panel"
709
+ value={local.cardZoom}
710
+ min={75}
711
+ max={150}
712
+ step={5}
713
+ onChange={v => update({ cardZoom: v })}
714
+ />
715
+ </SettingsSection>
570
716
  </>
571
717
  )}
572
- <SettingsSection title="Card Display">
573
- <SettingsToggle
574
- label="Show Priority Badges"
575
- description="Display priority indicators on feature cards"
576
- checked={local.showPriorityBadges}
577
- onChange={v => update({ showPriorityBadges: v })}
578
- />
579
- <SettingsToggle
580
- label="Show Assignee"
581
- description="Display assigned person on feature cards"
582
- checked={local.showAssignee}
583
- onChange={v => update({ showAssignee: v })}
584
- />
585
- <SettingsToggle
586
- label="Show Due Date"
587
- description="Display due dates on feature cards"
588
- checked={local.showDueDate}
589
- onChange={v => update({ showDueDate: v })}
590
- />
591
- <SettingsToggle
592
- label="Show Labels"
593
- description="Display labels on feature cards and in editors"
594
- checked={local.showLabels}
595
- onChange={v => update({ showLabels: v })}
596
- />
597
- <SettingsToggle
598
- label="Show Filename"
599
- description="Display the source markdown filename on cards"
600
- checked={local.showFileName}
601
- onChange={v => update({ showFileName: v })}
602
- />
603
- <SettingsToggle
604
- label="Compact Mode"
605
- description="Use compact card layout to show more features"
606
- checked={local.compactMode}
607
- onChange={v => update({ compactMode: v })}
608
- />
609
- <SettingsToggle
610
- label="Show Deleted Column"
611
- description="Display the Deleted column to manage soft-deleted cards"
612
- checked={local.showDeletedColumn}
613
- onChange={v => update({ showDeletedColumn: v })}
614
- />
615
- </SettingsSection>
616
-
617
- <div style={{ borderTop: '1px solid var(--vscode-panel-border)' }} />
618
-
619
- <SettingsSection title="Defaults">
620
- <SettingsDropdown
621
- label="Default Priority"
622
- value={local.defaultPriority}
623
- options={priorityConfig}
624
- onChange={v => update({ defaultPriority: v as Priority })}
625
- />
626
- <SettingsDropdown
627
- label="Default Status"
628
- value={local.defaultStatus}
629
- options={statusConfig}
630
- onChange={v => update({ defaultStatus: v as FeatureStatus })}
631
- />
632
- </SettingsSection>
633
-
634
- <div style={{ borderTop: '1px solid var(--vscode-panel-border)' }} />
635
-
636
- <SettingsSection title="Labels">
637
- <LabelsSection
638
- onSetLabel={onSetLabel}
639
- onRenameLabel={onRenameLabel}
640
- onDeleteLabel={onDeleteLabel}
641
- />
642
- </SettingsSection>
718
+
719
+ {activeTab === 'defaults' && (
720
+ <SettingsSection title="Defaults">
721
+ <SettingsDropdown
722
+ label="Default Priority"
723
+ value={local.defaultPriority}
724
+ options={priorityConfig}
725
+ onChange={v => update({ defaultPriority: v as Priority })}
726
+ />
727
+ <SettingsDropdown
728
+ label="Default Status"
729
+ value={local.defaultStatus}
730
+ options={statusConfig}
731
+ onChange={v => update({ defaultStatus: v as FeatureStatus })}
732
+ />
733
+ </SettingsSection>
734
+ )}
735
+
736
+ {activeTab === 'labels' && (
737
+ <SettingsSection title="Labels">
738
+ <LabelsSection
739
+ onSetLabel={onSetLabel}
740
+ onRenameLabel={onRenameLabel}
741
+ onDeleteLabel={onDeleteLabel}
742
+ />
743
+ </SettingsSection>
744
+ )}
643
745
  </div>
644
746
 
645
747
  {/* Footer */}
@@ -1,6 +1,6 @@
1
1
  import { useState, useRef, useEffect, useMemo } from 'react'
2
- import { Search, X, Columns, Rows, Settings, Plus, Moon, Sun, ChevronDown, Check } from 'lucide-react'
3
- import { useStore, type DueDateFilter, type SortOrder } from '../store'
2
+ import { Search, X, Columns, Rows, Settings, Plus, Moon, Sun, ChevronDown, Check, Tag } from 'lucide-react'
3
+ import { useStore, type DueDateFilter } from '../store'
4
4
  import type { Priority } from '../../shared/types'
5
5
 
6
6
  const priorities: { value: Priority | 'all'; label: string }[] = [
@@ -34,8 +34,6 @@ export function Toolbar({ onOpenSettings, onAddColumn, onToggleTheme, onSwitchBo
34
34
  setLabelFilter,
35
35
  dueDateFilter,
36
36
  setDueDateFilter,
37
- sortOrder,
38
- setSortOrder,
39
37
  clearAllFilters,
40
38
  getUniqueAssignees,
41
39
  getUniqueLabels,
@@ -71,6 +69,21 @@ export function Toolbar({ onOpenSettings, onAddColumn, onToggleTheme, onSwitchBo
71
69
  const dropdownRef = useRef<HTMLDivElement>(null)
72
70
  const newBoardInputRef = useRef<HTMLInputElement>(null)
73
71
 
72
+ const [labelDropdownOpen, setLabelDropdownOpen] = useState(false)
73
+ const [labelSearch, setLabelSearch] = useState('')
74
+ const labelDropdownRef = useRef<HTMLDivElement>(null)
75
+
76
+ const filteredGroupedLabels = useMemo(() => {
77
+ if (!labelSearch) return groupedLabels
78
+ const q = labelSearch.toLowerCase()
79
+ const result: Record<string, string[]> = {}
80
+ Object.entries(groupedLabels).forEach(([group, groupLabels]) => {
81
+ const matched = groupLabels.filter(l => l.toLowerCase().includes(q))
82
+ if (matched.length > 0) result[group] = matched
83
+ })
84
+ return result
85
+ }, [groupedLabels, labelSearch])
86
+
74
87
  useEffect(() => {
75
88
  const handleClickOutside = (e: MouseEvent) => {
76
89
  if (dropdownRef.current && !dropdownRef.current.contains(e.target as Node)) {
@@ -85,6 +98,19 @@ export function Toolbar({ onOpenSettings, onAddColumn, onToggleTheme, onSwitchBo
85
98
  return () => document.removeEventListener('mousedown', handleClickOutside)
86
99
  }, [boardDropdownOpen])
87
100
 
101
+ useEffect(() => {
102
+ const handleClickOutside = (e: MouseEvent) => {
103
+ if (labelDropdownRef.current && !labelDropdownRef.current.contains(e.target as Node)) {
104
+ setLabelDropdownOpen(false)
105
+ setLabelSearch('')
106
+ }
107
+ }
108
+ if (labelDropdownOpen) {
109
+ document.addEventListener('mousedown', handleClickOutside)
110
+ }
111
+ return () => document.removeEventListener('mousedown', handleClickOutside)
112
+ }, [labelDropdownOpen])
113
+
88
114
  useEffect(() => {
89
115
  if (creatingBoard && newBoardInputRef.current) {
90
116
  newBoardInputRef.current.focus()
@@ -209,21 +235,103 @@ export function Toolbar({ onOpenSettings, onAddColumn, onToggleTheme, onSwitchBo
209
235
 
210
236
  {/* Label Filter */}
211
237
  {cardSettings.showLabels && (
212
- <select
213
- value={labelFilter}
214
- onChange={(e) => setLabelFilter(e.target.value)}
215
- className={selectClassName}
216
- >
217
- <option value="all">All Labels</option>
218
- {Object.entries(groupedLabels).map(([group, groupLabels]) => (
219
- <optgroup key={group} label={group}>
220
- <option value={`group:${group}`}>All {group}</option>
221
- {groupLabels.map((l) => (
222
- <option key={l} value={l}>{l}</option>
223
- ))}
224
- </optgroup>
225
- ))}
226
- </select>
238
+ <div className="relative" ref={labelDropdownRef}>
239
+ <button
240
+ type="button"
241
+ onClick={() => setLabelDropdownOpen(!labelDropdownOpen)}
242
+ className={`flex items-center gap-1.5 text-sm bg-white dark:bg-zinc-800 border rounded-md px-2 py-1.5 focus:outline-none focus:ring-2 focus:ring-blue-500 text-zinc-900 dark:text-zinc-100 transition-colors ${
243
+ labelFilter.length > 0
244
+ ? 'border-blue-500 dark:border-blue-400'
245
+ : 'border-zinc-200 dark:border-zinc-600'
246
+ }`}
247
+ >
248
+ <Tag size={14} className="text-zinc-400 shrink-0" />
249
+ <span className="max-w-[100px] truncate">
250
+ {labelFilter.length === 0
251
+ ? 'All Labels'
252
+ : labelFilter.length === 1
253
+ ? labelFilter[0]
254
+ : `${labelFilter.length} Labels`}
255
+ </span>
256
+ <ChevronDown size={14} className={`text-zinc-400 transition-transform ${labelDropdownOpen ? 'rotate-180' : ''}`} />
257
+ </button>
258
+ {labelDropdownOpen && (
259
+ <div className="absolute top-full left-0 mt-1 min-w-[200px] max-h-72 overflow-y-auto bg-white dark:bg-zinc-800 border border-zinc-200 dark:border-zinc-600 rounded-md shadow-lg z-50 py-1">
260
+ {/* Search */}
261
+ <div className="px-2 pb-1">
262
+ <input
263
+ type="text"
264
+ value={labelSearch}
265
+ onChange={(e) => setLabelSearch(e.target.value)}
266
+ placeholder="Filter labels..."
267
+ autoFocus
268
+ 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"
269
+ />
270
+ </div>
271
+ {/* All Labels option */}
272
+ {!labelSearch && (
273
+ <button
274
+ type="button"
275
+ onClick={() => setLabelFilter([])}
276
+ 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 transition-colors ${
277
+ labelFilter.length === 0 ? 'text-blue-600 dark:text-blue-400 font-medium' : 'text-zinc-900 dark:text-zinc-100'
278
+ }`}
279
+ >
280
+ <div className={`w-3.5 h-3.5 rounded border flex items-center justify-center shrink-0 ${
281
+ labelFilter.length === 0 ? 'border-blue-500 bg-blue-500' : 'border-zinc-300 dark:border-zinc-500'
282
+ }`}>
283
+ {labelFilter.length === 0 && <Check size={10} className="text-white" />}
284
+ </div>
285
+ <span>All Labels</span>
286
+ </button>
287
+ )}
288
+ {/* Grouped labels */}
289
+ {Object.entries(filteredGroupedLabels).map(([group, groupLabels]) => (
290
+ <div key={group}>
291
+ {Object.keys(filteredGroupedLabels).length > 1 && (
292
+ <div className="px-3 pt-1.5 pb-0.5 text-xs font-semibold text-zinc-400 dark:text-zinc-500 uppercase tracking-wide">
293
+ {group}
294
+ </div>
295
+ )}
296
+ {groupLabels.map((l) => {
297
+ const def = labelDefs[l]
298
+ const checked = labelFilter.includes(l)
299
+ return (
300
+ <button
301
+ key={l}
302
+ type="button"
303
+ onClick={() => {
304
+ if (checked) {
305
+ setLabelFilter(labelFilter.filter(x => x !== l))
306
+ } else {
307
+ setLabelFilter([...labelFilter, l])
308
+ }
309
+ }}
310
+ 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"
311
+ >
312
+ <div className={`w-3.5 h-3.5 rounded border flex items-center justify-center shrink-0 ${
313
+ checked ? 'border-blue-500 bg-blue-500' : 'border-zinc-300 dark:border-zinc-500'
314
+ }`}>
315
+ {checked && <Check size={10} className="text-white" />}
316
+ </div>
317
+ {def?.color && (
318
+ <span
319
+ className="w-2.5 h-2.5 rounded-full shrink-0"
320
+ style={{ backgroundColor: def.color }}
321
+ />
322
+ )}
323
+ <span className="truncate">{l}</span>
324
+ </button>
325
+ )
326
+ })}
327
+ </div>
328
+ ))}
329
+ {Object.keys(filteredGroupedLabels).length === 0 && (
330
+ <div className="px-3 py-2 text-sm text-zinc-400 dark:text-zinc-500">No labels found</div>
331
+ )}
332
+ </div>
333
+ )}
334
+ </div>
227
335
  )}
228
336
 
229
337
  {/* Due Date Filter */}
@@ -241,19 +349,6 @@ export function Toolbar({ onOpenSettings, onAddColumn, onToggleTheme, onSwitchBo
241
349
  </select>
242
350
  )}
243
351
 
244
- {/* Sort Order */}
245
- <select
246
- value={sortOrder}
247
- onChange={(e) => setSortOrder(e.target.value as SortOrder)}
248
- className={selectClassName}
249
- >
250
- <option value="order">Board Order</option>
251
- <option value="created:asc">Created (oldest)</option>
252
- <option value="created:desc">Created (newest)</option>
253
- <option value="modified:asc">Modified (oldest)</option>
254
- <option value="modified:desc">Modified (newest)</option>
255
- </select>
256
-
257
352
  {/* Clear Filters Button */}
258
353
  {filtersActive && (
259
354
  <button