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.
- package/.editorconfig +9 -0
- package/.github/workflows/ci.yml +59 -0
- package/.github/workflows/release.yml +75 -0
- package/.prettierignore +6 -0
- package/.prettierrc.yaml +4 -0
- package/.vscode/extensions.json +3 -0
- package/.vscode/launch.json +17 -0
- package/.vscode/settings.json +21 -0
- package/.vscode/tasks.json +22 -0
- package/.vscodeignore +11 -0
- package/CHANGELOG.md +184 -0
- package/CLAUDE.md +58 -0
- package/CONTRIBUTING.md +114 -0
- package/LICENSE +22 -0
- package/README.md +482 -0
- package/SKILL.md +237 -0
- package/dist/cli.js +8716 -0
- package/dist/extension.js +8463 -0
- package/dist/mcp-server.js +1327 -0
- package/dist/standalone-webview/icons-Dx9MGYqN.js +180 -0
- package/dist/standalone-webview/icons-Dx9MGYqN.js.map +1 -0
- package/dist/standalone-webview/index.js +85 -0
- package/dist/standalone-webview/index.js.map +1 -0
- package/dist/standalone-webview/react-vendor-DkYdDBET.js +25 -0
- package/dist/standalone-webview/react-vendor-DkYdDBET.js.map +1 -0
- package/dist/standalone-webview/style.css +1 -0
- package/dist/standalone.js +7513 -0
- package/dist/webview/icons-Dx9MGYqN.js +180 -0
- package/dist/webview/icons-Dx9MGYqN.js.map +1 -0
- package/dist/webview/index.js +85 -0
- package/dist/webview/index.js.map +1 -0
- package/dist/webview/react-vendor-DkYdDBET.js +25 -0
- package/dist/webview/react-vendor-DkYdDBET.js.map +1 -0
- package/dist/webview/style.css +1 -0
- package/docs/images/board-overview.png +0 -0
- package/docs/images/editor-view.png +0 -0
- package/docs/plans/2026-02-20-kanban-json-config-design.md +74 -0
- package/docs/plans/2026-02-20-kanban-json-config.md +690 -0
- package/eslint.config.mjs +31 -0
- package/package.json +161 -0
- package/postcss.config.js +6 -0
- package/resources/icon-light.png +0 -0
- package/resources/icon-light.svg +105 -0
- package/resources/icon.png +0 -0
- package/resources/icon.svg +105 -0
- package/resources/kanban-dark.svg +21 -0
- package/resources/kanban-light.svg +21 -0
- package/resources/kanban.svg +21 -0
- package/src/cli/index.ts +846 -0
- package/src/extension/FeatureHeaderProvider.ts +370 -0
- package/src/extension/KanbanPanel.ts +973 -0
- package/src/extension/SidebarViewProvider.ts +507 -0
- package/src/extension/featureFileUtils.ts +82 -0
- package/src/extension/index.ts +234 -0
- package/src/mcp-server/index.ts +632 -0
- package/src/sdk/KanbanSDK.ts +349 -0
- package/src/sdk/__tests__/KanbanSDK.test.ts +468 -0
- package/src/sdk/__tests__/parser.test.ts +170 -0
- package/src/sdk/fileUtils.ts +76 -0
- package/src/sdk/index.ts +6 -0
- package/src/sdk/parser.ts +70 -0
- package/src/sdk/types.ts +15 -0
- package/src/shared/config.ts +113 -0
- package/src/shared/editorTypes.ts +14 -0
- package/src/shared/types.ts +120 -0
- package/src/standalone/__tests__/server.integration.test.ts +1916 -0
- package/src/standalone/__tests__/webhooks.test.ts +357 -0
- package/src/standalone/fileUtils.ts +70 -0
- package/src/standalone/index.ts +71 -0
- package/src/standalone/server.ts +1046 -0
- package/src/standalone/webhooks.ts +135 -0
- package/src/webview/App.tsx +469 -0
- package/src/webview/assets/main.css +329 -0
- package/src/webview/assets/standalone-theme.css +130 -0
- package/src/webview/components/ColumnDialog.tsx +119 -0
- package/src/webview/components/CreateFeatureDialog.tsx +524 -0
- package/src/webview/components/DatePicker.tsx +185 -0
- package/src/webview/components/FeatureCard.tsx +186 -0
- package/src/webview/components/FeatureEditor.tsx +623 -0
- package/src/webview/components/KanbanBoard.tsx +144 -0
- package/src/webview/components/KanbanColumn.tsx +159 -0
- package/src/webview/components/MarkdownEditor.tsx +291 -0
- package/src/webview/components/PrioritySelect.tsx +39 -0
- package/src/webview/components/QuickAddInput.tsx +72 -0
- package/src/webview/components/SettingsPanel.tsx +284 -0
- package/src/webview/components/Toolbar.tsx +175 -0
- package/src/webview/components/UndoToast.tsx +70 -0
- package/src/webview/index.html +12 -0
- package/src/webview/lib/utils.ts +6 -0
- package/src/webview/main.tsx +11 -0
- package/src/webview/standalone-main.tsx +13 -0
- package/src/webview/standalone-shim.ts +132 -0
- package/src/webview/standalone.html +12 -0
- package/src/webview/store/index.ts +241 -0
- package/tailwind.config.js +53 -0
- package/tsconfig.json +22 -0
- package/vite.config.ts +36 -0
- package/vite.standalone.config.ts +62 -0
- 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,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
|
+
)
|