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,185 @@
1
+ import { useState } from 'react'
2
+ import { X, ChevronLeft, ChevronRight } from 'lucide-react'
3
+ import { cn } from '../lib/utils'
4
+
5
+ const DAYS = ['Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa', 'Su']
6
+ const MONTHS = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December']
7
+
8
+ interface DatePickerProps {
9
+ value: string
10
+ onChange: (date: string) => void
11
+ placeholder?: string
12
+ }
13
+
14
+ export function DatePicker({ value, onChange, placeholder = 'Due date' }: DatePickerProps) {
15
+ const [isOpen, setIsOpen] = useState(false)
16
+ const today = new Date()
17
+ const selected = value ? new Date(value + 'T00:00:00') : null
18
+ const [viewYear, setViewYear] = useState(selected?.getFullYear() ?? today.getFullYear())
19
+ const [viewMonth, setViewMonth] = useState(selected?.getMonth() ?? today.getMonth())
20
+
21
+ const daysInMonth = new Date(viewYear, viewMonth + 1, 0).getDate()
22
+ // Monday = 0
23
+ const firstDay = (new Date(viewYear, viewMonth, 1).getDay() + 6) % 7
24
+ const cells: (number | null)[] = Array(firstDay).fill(null)
25
+ for (let d = 1; d <= daysInMonth; d++) cells.push(d)
26
+
27
+ const prevMonth = () => {
28
+ if (viewMonth === 0) { setViewMonth(11); setViewYear(y => y - 1) }
29
+ else setViewMonth(m => m - 1)
30
+ }
31
+ const nextMonth = () => {
32
+ if (viewMonth === 11) { setViewMonth(0); setViewYear(y => y + 1) }
33
+ else setViewMonth(m => m + 1)
34
+ }
35
+
36
+ const selectDay = (day: number) => {
37
+ const m = String(viewMonth + 1).padStart(2, '0')
38
+ const d = String(day).padStart(2, '0')
39
+ onChange(`${viewYear}-${m}-${d}`)
40
+ setIsOpen(false)
41
+ }
42
+
43
+ const isToday = (day: number) =>
44
+ day === today.getDate() && viewMonth === today.getMonth() && viewYear === today.getFullYear()
45
+ const isSelected = (day: number) =>
46
+ selected !== null && day === selected.getDate() && viewMonth === selected.getMonth() && viewYear === selected.getFullYear()
47
+
48
+ const formatDisplay = (dateStr: string) => {
49
+ const d = new Date(dateStr + 'T00:00:00')
50
+ return d.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' })
51
+ }
52
+
53
+ return (
54
+ <div className="relative">
55
+ <button
56
+ type="button"
57
+ onClick={() => setIsOpen(!isOpen)}
58
+ className="flex items-center gap-2 px-2 py-1 text-xs font-medium rounded transition-colors"
59
+ style={{
60
+ color: value ? 'var(--vscode-foreground)' : 'var(--vscode-descriptionForeground)',
61
+ }}
62
+ onMouseEnter={e => e.currentTarget.style.background = 'var(--vscode-list-hoverBackground)'}
63
+ onMouseLeave={e => e.currentTarget.style.background = 'transparent'}
64
+ >
65
+ <span>{value ? formatDisplay(value) : placeholder}</span>
66
+ {value && (
67
+ <span
68
+ role="button"
69
+ onClick={(e) => { e.stopPropagation(); onChange(''); setIsOpen(false) }}
70
+ className="ml-0.5 hover:text-red-500 transition-colors"
71
+ >
72
+ <X size={12} />
73
+ </span>
74
+ )}
75
+ </button>
76
+ {isOpen && (
77
+ <>
78
+ <div className="fixed inset-0 z-10" onClick={() => setIsOpen(false)} />
79
+ <div
80
+ className="absolute top-full left-0 mt-1 z-20 rounded-lg shadow-lg p-3 w-[252px]"
81
+ style={{
82
+ background: 'var(--vscode-dropdown-background)',
83
+ border: '1px solid var(--vscode-dropdown-border, var(--vscode-panel-border))',
84
+ }}
85
+ >
86
+ {/* Month/year nav */}
87
+ <div className="flex items-center justify-between mb-2">
88
+ <button
89
+ type="button"
90
+ onClick={prevMonth}
91
+ className="p-1 rounded transition-colors"
92
+ style={{ color: 'var(--vscode-descriptionForeground)' }}
93
+ onMouseEnter={e => e.currentTarget.style.background = 'var(--vscode-list-hoverBackground)'}
94
+ onMouseLeave={e => e.currentTarget.style.background = 'transparent'}
95
+ >
96
+ <ChevronLeft size={14} />
97
+ </button>
98
+ <span className="text-xs font-medium" style={{ color: 'var(--vscode-foreground)' }}>
99
+ {MONTHS[viewMonth]} {viewYear}
100
+ </span>
101
+ <button
102
+ type="button"
103
+ onClick={nextMonth}
104
+ className="p-1 rounded transition-colors"
105
+ style={{ color: 'var(--vscode-descriptionForeground)' }}
106
+ onMouseEnter={e => e.currentTarget.style.background = 'var(--vscode-list-hoverBackground)'}
107
+ onMouseLeave={e => e.currentTarget.style.background = 'transparent'}
108
+ >
109
+ <ChevronRight size={14} />
110
+ </button>
111
+ </div>
112
+ {/* Day headers */}
113
+ <div className="grid grid-cols-7 mb-1">
114
+ {DAYS.map(d => (
115
+ <div
116
+ key={d}
117
+ className="text-center text-[10px] font-medium py-1"
118
+ style={{ color: 'var(--vscode-descriptionForeground)' }}
119
+ >
120
+ {d}
121
+ </div>
122
+ ))}
123
+ </div>
124
+ {/* Day grid */}
125
+ <div className="grid grid-cols-7">
126
+ {cells.map((day, i) => (
127
+ <div key={i} className="flex items-center justify-center">
128
+ {day ? (
129
+ <button
130
+ type="button"
131
+ onClick={() => selectDay(day)}
132
+ className={cn('w-7 h-7 rounded-md text-[11px] transition-colors font-medium')}
133
+ style={{
134
+ background: isSelected(day)
135
+ ? 'var(--vscode-focusBorder)'
136
+ : isToday(day)
137
+ ? 'var(--vscode-editor-selectionBackground)'
138
+ : undefined,
139
+ color: isSelected(day)
140
+ ? 'var(--vscode-editor-background)'
141
+ : isToday(day)
142
+ ? 'var(--vscode-foreground)'
143
+ : 'var(--vscode-foreground)',
144
+ }}
145
+ onMouseEnter={e => {
146
+ if (!isSelected(day)) e.currentTarget.style.background = 'var(--vscode-list-hoverBackground)'
147
+ }}
148
+ onMouseLeave={e => {
149
+ if (!isSelected(day) && !isToday(day)) e.currentTarget.style.background = 'transparent'
150
+ else if (isToday(day) && !isSelected(day)) e.currentTarget.style.background = 'var(--vscode-editor-selectionBackground)'
151
+ }}
152
+ >
153
+ {day}
154
+ </button>
155
+ ) : <div className="w-7 h-7" />}
156
+ </div>
157
+ ))}
158
+ </div>
159
+ {/* Today shortcut */}
160
+ <div
161
+ className="mt-2 pt-2 flex justify-center"
162
+ style={{ borderTop: '1px solid var(--vscode-panel-border)' }}
163
+ >
164
+ <button
165
+ type="button"
166
+ onClick={() => {
167
+ const m = String(today.getMonth() + 1).padStart(2, '0')
168
+ const d = String(today.getDate()).padStart(2, '0')
169
+ onChange(`${today.getFullYear()}-${m}-${d}`)
170
+ setIsOpen(false)
171
+ }}
172
+ className="text-[11px] font-medium transition-colors"
173
+ style={{ color: 'var(--vscode-textLink-foreground)' }}
174
+ onMouseEnter={e => e.currentTarget.style.color = 'var(--vscode-textLink-activeForeground)'}
175
+ onMouseLeave={e => e.currentTarget.style.color = 'var(--vscode-textLink-foreground)'}
176
+ >
177
+ Today
178
+ </button>
179
+ </div>
180
+ </div>
181
+ </>
182
+ )}
183
+ </div>
184
+ )
185
+ }
@@ -0,0 +1,186 @@
1
+ import { marked } from 'marked'
2
+ import { Calendar, Check, FileText, Paperclip } from 'lucide-react'
3
+ import { getTitleFromContent } from '../../shared/types'
4
+ import type { Feature, Priority } from '../../shared/types'
5
+ import { useStore } from '../store'
6
+
7
+ interface FeatureCardProps {
8
+ feature: Feature
9
+ onClick: () => void
10
+ isDragging?: boolean
11
+ }
12
+
13
+ const priorityColors: Record<Priority, string> = {
14
+ critical: 'bg-red-100 text-red-700 dark:bg-red-900/30 dark:text-red-400',
15
+ high: 'bg-orange-100 text-orange-700 dark:bg-orange-900/30 dark:text-orange-400',
16
+ medium: 'bg-yellow-100 text-yellow-700 dark:bg-yellow-900/30 dark:text-yellow-400',
17
+ low: 'bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-400'
18
+ }
19
+
20
+ const priorityLabels: Record<Priority, string> = {
21
+ critical: 'Critical',
22
+ high: 'High',
23
+ medium: 'Med',
24
+ low: 'Low'
25
+ }
26
+
27
+ function getDescriptionFromContent(content: string): string {
28
+ // Remove the first # heading line, then grab the remaining content
29
+ const lines = content.split('\n')
30
+ const headingIndex = lines.findIndex(l => /^#\s+/.test(l))
31
+ const afterHeading = headingIndex >= 0 ? lines.slice(headingIndex + 1) : lines
32
+ // Trim leading/trailing blank lines but preserve internal structure (lists, etc.)
33
+ const trimmed = afterHeading.join('\n').trim()
34
+ return trimmed
35
+ }
36
+
37
+ function renderDescriptionHtml(text: string): string {
38
+ return marked.parse(text, { async: false, gfm: true, breaks: true }) as string
39
+ }
40
+
41
+ export function FeatureCard({ feature, onClick, isDragging }: FeatureCardProps) {
42
+ const { cardSettings } = useStore()
43
+ const title = getTitleFromContent(feature.content)
44
+ const description = getDescriptionFromContent(feature.content)
45
+ const fileName = feature.filePath ? feature.filePath.split('/').pop() || '' : ''
46
+
47
+ const formatDueDate = (dateStr: string | null) => {
48
+ if (!dateStr) return null
49
+ const date = new Date(dateStr)
50
+ const now = new Date()
51
+ const diff = date.getTime() - now.getTime()
52
+ const days = Math.ceil(diff / (1000 * 60 * 60 * 24))
53
+
54
+ if (days < 0) return { text: 'Overdue', className: 'text-red-500' }
55
+ if (days === 0) return { text: 'Today', className: 'text-orange-500' }
56
+ if (days === 1) return { text: 'Tomorrow', className: 'text-yellow-600 dark:text-yellow-400' }
57
+ if (days <= 7) return { text: `${days}d`, className: 'text-zinc-500 dark:text-zinc-400' }
58
+
59
+ return {
60
+ text: date.toLocaleDateString('en-US', { month: 'short', day: 'numeric' }),
61
+ className: 'text-zinc-500 dark:text-zinc-400'
62
+ }
63
+ }
64
+
65
+ const dueInfo = feature.status === 'done' ? null : formatDueDate(feature.dueDate)
66
+
67
+ const formatCompletedAt = (dateStr: string | null) => {
68
+ if (!dateStr) return null
69
+ const completed = new Date(dateStr)
70
+ const now = new Date()
71
+ const diffMs = now.getTime() - completed.getTime()
72
+ const diffMins = Math.floor(diffMs / (1000 * 60))
73
+ const diffHours = Math.floor(diffMs / (1000 * 60 * 60))
74
+ const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24))
75
+
76
+ if (diffMins < 1) return 'Just now'
77
+ if (diffMins < 60) return `${diffMins}m ago`
78
+ if (diffHours < 24) return `${diffHours}h ago`
79
+ if (diffDays === 1) return '1d ago'
80
+ if (diffDays < 30) return `${diffDays}d ago`
81
+ if (diffDays < 365) return `${Math.floor(diffDays / 30)}mo ago`
82
+ return `${Math.floor(diffDays / 365)}y ago`
83
+ }
84
+
85
+ const completedText = feature.status === 'done' ? formatCompletedAt(feature.completedAt) : null
86
+
87
+ return (
88
+ <div
89
+ onClick={onClick}
90
+ className={`group relative flex flex-col bg-white dark:bg-zinc-800 rounded-lg border border-zinc-200 dark:border-zinc-700 ${cardSettings.compactMode ? 'p-2 min-h-[4.5rem]' : 'p-3 min-h-[7rem]'} cursor-pointer hover:shadow-md transition-shadow ${
91
+ isDragging ? 'shadow-lg opacity-90' : ''
92
+ }`}
93
+ >
94
+ {/* Title & Content */}
95
+ <div className="flex-1">
96
+ {/* File Name + Priority badge row (when fileName enabled) */}
97
+ {cardSettings.showFileName && fileName && (
98
+ <div className="flex items-center gap-1.5 mb-1">
99
+ <FileText size={10} className="shrink-0 text-zinc-400 dark:text-zinc-500" />
100
+ <span className="text-[10px] font-mono text-zinc-400 dark:text-zinc-500 truncate flex-1">
101
+ {fileName}
102
+ </span>
103
+ {cardSettings.showPriorityBadges && (
104
+ <span
105
+ className={`text-[10px] font-medium px-1.5 py-0.5 rounded shrink-0 ${priorityColors[feature.priority]}`}
106
+ >
107
+ {priorityLabels[feature.priority]}
108
+ </span>
109
+ )}
110
+ </div>
111
+ )}
112
+
113
+ <div className={`flex items-start gap-2 ${description ? 'mb-1' : cardSettings.compactMode ? 'mb-1' : 'mb-2'}`}>
114
+ <h3 className="text-sm font-medium text-zinc-900 dark:text-zinc-100 line-clamp-2 flex-1">
115
+ {title}
116
+ </h3>
117
+ {cardSettings.showPriorityBadges && !(cardSettings.showFileName && fileName) && (
118
+ <span
119
+ className={`text-xs font-medium px-1.5 py-0.5 rounded shrink-0 ${priorityColors[feature.priority]}`}
120
+ >
121
+ {priorityLabels[feature.priority]}
122
+ </span>
123
+ )}
124
+ </div>
125
+
126
+ {/* Description */}
127
+ {/* eslint-disable-next-line react/no-danger */}
128
+ {description && !cardSettings.compactMode && (
129
+ <div
130
+ className="text-xs text-zinc-500 dark:text-zinc-400 line-clamp-2 mb-2 card-inline-markdown"
131
+ dangerouslySetInnerHTML={{ __html: renderDescriptionHtml(description) }}
132
+ />
133
+ )}
134
+
135
+ {/* Labels */}
136
+ {cardSettings.showLabels && feature.labels.length > 0 && (
137
+ <div className="flex flex-wrap gap-1 mb-2">
138
+ {feature.labels.slice(0, 3).map((label) => (
139
+ <span
140
+ key={label}
141
+ className="text-xs px-1.5 py-0.5 rounded bg-zinc-100 text-zinc-600 dark:bg-zinc-700 dark:text-zinc-300"
142
+ >
143
+ {label}
144
+ </span>
145
+ ))}
146
+ {feature.labels.length > 3 && (
147
+ <span className="text-xs text-zinc-400">+{feature.labels.length - 3}</span>
148
+ )}
149
+ </div>
150
+ )}
151
+ </div>
152
+
153
+ {/* Footer */}
154
+ <div className="flex items-center justify-between text-xs mt-auto">
155
+ <div className="flex items-center gap-1">
156
+ {cardSettings.showAssignee && feature.assignee && feature.assignee !== 'null' && (
157
+ <div className="flex items-center gap-1.5 text-zinc-500 dark:text-zinc-400">
158
+ <span className="shrink-0 w-4 h-4 rounded-full flex items-center justify-center text-[8px] font-bold bg-zinc-200 dark:bg-zinc-600 text-zinc-700 dark:text-zinc-300">
159
+ {feature.assignee.split(/\s+/).map((w: string) => w[0]).join('').toUpperCase().slice(0, 2)}
160
+ </span>
161
+ <span>{feature.assignee}</span>
162
+ </div>
163
+ )}
164
+ </div>
165
+ {feature.attachments.length > 0 && (
166
+ <div className="flex items-center gap-1 text-zinc-400 dark:text-zinc-500">
167
+ <Paperclip size={12} />
168
+ <span>{feature.attachments.length}</span>
169
+ </div>
170
+ )}
171
+ {cardSettings.showDueDate && dueInfo && (
172
+ <div className={`flex items-center gap-1 ${dueInfo.className}`}>
173
+ <Calendar size={12} />
174
+ <span>{dueInfo.text}</span>
175
+ </div>
176
+ )}
177
+ {completedText && (
178
+ <div className="flex items-center gap-1" style={{ color: 'var(--vscode-descriptionForeground)' }}>
179
+ <Check size={12} />
180
+ <span>{completedText}</span>
181
+ </div>
182
+ )}
183
+ </div>
184
+ </div>
185
+ )
186
+ }