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,507 @@
|
|
|
1
|
+
import * as vscode from 'vscode'
|
|
2
|
+
import * as path from 'path'
|
|
3
|
+
import { getTitleFromContent } from '../shared/types'
|
|
4
|
+
import type { FeatureStatus, Priority, KanbanColumn } from '../shared/types'
|
|
5
|
+
import { readConfig, CONFIG_FILENAME } from '../shared/config'
|
|
6
|
+
import { KanbanPanel } from './KanbanPanel'
|
|
7
|
+
|
|
8
|
+
interface SidebarFeature {
|
|
9
|
+
id: string
|
|
10
|
+
title: string
|
|
11
|
+
status: FeatureStatus
|
|
12
|
+
priority: Priority
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export class SidebarViewProvider implements vscode.WebviewViewProvider {
|
|
16
|
+
public static readonly viewType = 'kanban-lite.boardView'
|
|
17
|
+
|
|
18
|
+
private _view?: vscode.WebviewView
|
|
19
|
+
private _features: SidebarFeature[] = []
|
|
20
|
+
private _fileWatcher?: vscode.FileSystemWatcher
|
|
21
|
+
private _debounceTimer?: NodeJS.Timeout
|
|
22
|
+
private _disposables: vscode.Disposable[] = []
|
|
23
|
+
|
|
24
|
+
constructor(private readonly _extensionUri: vscode.Uri, private readonly _context: vscode.ExtensionContext) {
|
|
25
|
+
this._setupFileWatcher()
|
|
26
|
+
|
|
27
|
+
// Watch .kanban.json for config changes
|
|
28
|
+
const workspaceFolders = vscode.workspace.workspaceFolders
|
|
29
|
+
if (workspaceFolders) {
|
|
30
|
+
const root = workspaceFolders[0].uri.fsPath
|
|
31
|
+
const configPattern = new vscode.RelativePattern(root, CONFIG_FILENAME)
|
|
32
|
+
const configWatcher = vscode.workspace.createFileSystemWatcher(configPattern)
|
|
33
|
+
|
|
34
|
+
const handleConfigChange = () => {
|
|
35
|
+
this._setupFileWatcher()
|
|
36
|
+
this._refresh()
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
configWatcher.onDidChange(handleConfigChange, null, this._disposables)
|
|
40
|
+
configWatcher.onDidCreate(handleConfigChange, null, this._disposables)
|
|
41
|
+
configWatcher.onDidDelete(handleConfigChange, null, this._disposables)
|
|
42
|
+
this._disposables.push(configWatcher)
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
public resolveWebviewView(
|
|
47
|
+
webviewView: vscode.WebviewView,
|
|
48
|
+
_context: vscode.WebviewViewResolveContext,
|
|
49
|
+
_token: vscode.CancellationToken
|
|
50
|
+
): void {
|
|
51
|
+
this._view = webviewView
|
|
52
|
+
|
|
53
|
+
webviewView.webview.options = {
|
|
54
|
+
enableScripts: true
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
webviewView.webview.onDidReceiveMessage(message => {
|
|
58
|
+
switch (message.type) {
|
|
59
|
+
case 'ready':
|
|
60
|
+
this._refresh()
|
|
61
|
+
break
|
|
62
|
+
case 'openBoard':
|
|
63
|
+
vscode.commands.executeCommand('kanban-lite.open')
|
|
64
|
+
break
|
|
65
|
+
case 'newFeature':
|
|
66
|
+
vscode.commands.executeCommand('kanban-lite.open')
|
|
67
|
+
// Wait for the panel to be ready, then trigger create dialog
|
|
68
|
+
setTimeout(() => {
|
|
69
|
+
KanbanPanel.currentPanel?.triggerCreateDialog()
|
|
70
|
+
}, 500)
|
|
71
|
+
break
|
|
72
|
+
case 'openFeature':
|
|
73
|
+
vscode.commands.executeCommand('kanban-lite.open')
|
|
74
|
+
setTimeout(() => {
|
|
75
|
+
KanbanPanel.currentPanel?.openFeature(message.featureId)
|
|
76
|
+
}, 500)
|
|
77
|
+
break
|
|
78
|
+
}
|
|
79
|
+
}, null, this._disposables)
|
|
80
|
+
|
|
81
|
+
webviewView.onDidChangeVisibility(() => {
|
|
82
|
+
if (webviewView.visible) {
|
|
83
|
+
vscode.commands.executeCommand('kanban-lite.open')
|
|
84
|
+
}
|
|
85
|
+
}, null, this._disposables)
|
|
86
|
+
|
|
87
|
+
webviewView.onDidDispose(() => {
|
|
88
|
+
this._view = undefined
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
// Auto-open the board when the sidebar first loads
|
|
92
|
+
vscode.commands.executeCommand('kanban-lite.open')
|
|
93
|
+
|
|
94
|
+
webviewView.webview.html = this._getHtml()
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
public setBoardOpen(open: boolean): void {
|
|
98
|
+
if (this._view) {
|
|
99
|
+
this._view.webview.postMessage({ type: 'boardOpenChanged', open })
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
public dispose(): void {
|
|
104
|
+
if (this._fileWatcher) {
|
|
105
|
+
this._fileWatcher.dispose()
|
|
106
|
+
}
|
|
107
|
+
if (this._debounceTimer) {
|
|
108
|
+
clearTimeout(this._debounceTimer)
|
|
109
|
+
}
|
|
110
|
+
for (const d of this._disposables) {
|
|
111
|
+
d.dispose()
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
private _setupFileWatcher(): void {
|
|
116
|
+
if (this._fileWatcher) {
|
|
117
|
+
this._fileWatcher.dispose()
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const featuresDir = this._getFeaturesDir()
|
|
121
|
+
if (!featuresDir) return
|
|
122
|
+
|
|
123
|
+
const pattern = new vscode.RelativePattern(featuresDir, '**/*.md')
|
|
124
|
+
this._fileWatcher = vscode.workspace.createFileSystemWatcher(pattern)
|
|
125
|
+
|
|
126
|
+
const handleChange = () => {
|
|
127
|
+
if (this._debounceTimer) clearTimeout(this._debounceTimer)
|
|
128
|
+
this._debounceTimer = setTimeout(() => this._refresh(), 300)
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
this._fileWatcher.onDidChange(handleChange, null, this._disposables)
|
|
132
|
+
this._fileWatcher.onDidCreate(handleChange, null, this._disposables)
|
|
133
|
+
this._fileWatcher.onDidDelete(handleChange, null, this._disposables)
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
private async _refresh(): Promise<void> {
|
|
137
|
+
await this._loadFeatures()
|
|
138
|
+
if (this._view) {
|
|
139
|
+
this._view.webview.postMessage({
|
|
140
|
+
type: 'update',
|
|
141
|
+
features: this._features,
|
|
142
|
+
columns: this._getColumns()
|
|
143
|
+
})
|
|
144
|
+
this._view.webview.postMessage({
|
|
145
|
+
type: 'boardOpenChanged',
|
|
146
|
+
open: !!KanbanPanel.currentPanel
|
|
147
|
+
})
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
private _getFeaturesDir(): string | null {
|
|
152
|
+
const workspaceFolders = vscode.workspace.workspaceFolders
|
|
153
|
+
if (!workspaceFolders || workspaceFolders.length === 0) return null
|
|
154
|
+
const root = workspaceFolders[0].uri.fsPath
|
|
155
|
+
const config = readConfig(root)
|
|
156
|
+
return path.join(root, config.featuresDirectory)
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
private _getColumns(): KanbanColumn[] {
|
|
160
|
+
const workspaceFolders = vscode.workspace.workspaceFolders
|
|
161
|
+
if (!workspaceFolders || workspaceFolders.length === 0) return []
|
|
162
|
+
const root = workspaceFolders[0].uri.fsPath
|
|
163
|
+
const config = readConfig(root)
|
|
164
|
+
return config.columns
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
private async _loadFeatures(): Promise<void> {
|
|
168
|
+
const featuresDir = this._getFeaturesDir()
|
|
169
|
+
if (!featuresDir) {
|
|
170
|
+
this._features = []
|
|
171
|
+
return
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const features: SidebarFeature[] = []
|
|
175
|
+
|
|
176
|
+
// Load .md files from ALL subdirectories
|
|
177
|
+
try {
|
|
178
|
+
const topEntries = await vscode.workspace.fs.readDirectory(vscode.Uri.file(featuresDir))
|
|
179
|
+
for (const [name, type] of topEntries) {
|
|
180
|
+
if (type !== vscode.FileType.Directory || name.startsWith('.')) continue
|
|
181
|
+
const subdir = path.join(featuresDir, name)
|
|
182
|
+
try {
|
|
183
|
+
const entries = await vscode.workspace.fs.readDirectory(vscode.Uri.file(subdir))
|
|
184
|
+
for (const [file, fileType] of entries) {
|
|
185
|
+
if (fileType !== vscode.FileType.File || !file.endsWith('.md')) continue
|
|
186
|
+
const filePath = path.join(subdir, file)
|
|
187
|
+
try {
|
|
188
|
+
const content = new TextDecoder().decode(await vscode.workspace.fs.readFile(vscode.Uri.file(filePath)))
|
|
189
|
+
const parsed = this._parseFrontmatter(content, file)
|
|
190
|
+
if (parsed) features.push(parsed)
|
|
191
|
+
} catch {
|
|
192
|
+
// Skip unreadable files
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
} catch {
|
|
196
|
+
// Skip unreadable directories
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
} catch {
|
|
200
|
+
// Root directory may not exist
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
this._features = features
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
private _parseFrontmatter(content: string, filename: string): SidebarFeature | null {
|
|
207
|
+
content = content.replace(/\r\n/g, '\n')
|
|
208
|
+
const match = content.match(/^---\n([\s\S]*?)\n---\n?([\s\S]*)$/)
|
|
209
|
+
if (!match) return null
|
|
210
|
+
|
|
211
|
+
const fm = match[1]
|
|
212
|
+
const body = match[2] || ''
|
|
213
|
+
|
|
214
|
+
const getValue = (key: string): string => {
|
|
215
|
+
const m = fm.match(new RegExp(`^${key}:\\s*(.*)$`, 'm'))
|
|
216
|
+
if (!m) return ''
|
|
217
|
+
const v = m[1].trim().replace(/^["']|["']$/g, '')
|
|
218
|
+
return v === 'null' ? '' : v
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
const basename = path.basename(filename, '.md')
|
|
222
|
+
const numericMatch = basename.match(/^(\d+)-/)
|
|
223
|
+
const id = getValue('id') || (numericMatch ? numericMatch[1] : basename)
|
|
224
|
+
const status = (getValue('status') as FeatureStatus) || 'backlog'
|
|
225
|
+
const priority = (getValue('priority') as Priority) || 'medium'
|
|
226
|
+
const title = getTitleFromContent(body)
|
|
227
|
+
|
|
228
|
+
return { id, title, status, priority }
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
private _getHtml(): string {
|
|
232
|
+
const nonce = this._getNonce()
|
|
233
|
+
|
|
234
|
+
return `<!DOCTYPE html>
|
|
235
|
+
<html lang="en">
|
|
236
|
+
<head>
|
|
237
|
+
<meta charset="UTF-8">
|
|
238
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
239
|
+
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; style-src 'nonce-${nonce}'; script-src 'nonce-${nonce}';">
|
|
240
|
+
<style nonce="${nonce}">
|
|
241
|
+
* { box-sizing: border-box; margin: 0; padding: 0; }
|
|
242
|
+
|
|
243
|
+
body {
|
|
244
|
+
font-family: var(--vscode-font-family);
|
|
245
|
+
font-size: var(--vscode-font-size);
|
|
246
|
+
color: var(--vscode-foreground);
|
|
247
|
+
background: transparent;
|
|
248
|
+
padding: 12px 14px;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
.actions {
|
|
252
|
+
display: flex;
|
|
253
|
+
flex-direction: column;
|
|
254
|
+
gap: 6px;
|
|
255
|
+
margin-bottom: 16px;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
button {
|
|
259
|
+
display: flex;
|
|
260
|
+
align-items: center;
|
|
261
|
+
justify-content: center;
|
|
262
|
+
gap: 6px;
|
|
263
|
+
width: 100%;
|
|
264
|
+
padding: 6px 12px;
|
|
265
|
+
border: none;
|
|
266
|
+
border-radius: 4px;
|
|
267
|
+
cursor: pointer;
|
|
268
|
+
font-family: var(--vscode-font-family);
|
|
269
|
+
font-size: var(--vscode-font-size);
|
|
270
|
+
line-height: 20px;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
.btn-primary {
|
|
274
|
+
background: var(--vscode-button-background);
|
|
275
|
+
color: var(--vscode-button-foreground);
|
|
276
|
+
}
|
|
277
|
+
.btn-primary:hover {
|
|
278
|
+
background: var(--vscode-button-hoverBackground);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
.btn-secondary {
|
|
282
|
+
background: var(--vscode-button-secondaryBackground);
|
|
283
|
+
color: var(--vscode-button-secondaryForeground);
|
|
284
|
+
}
|
|
285
|
+
.btn-secondary:hover {
|
|
286
|
+
background: var(--vscode-button-secondaryHoverBackground);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
.section {
|
|
290
|
+
margin-bottom: 14px;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
.section-header {
|
|
294
|
+
display: flex;
|
|
295
|
+
align-items: center;
|
|
296
|
+
justify-content: space-between;
|
|
297
|
+
margin-bottom: 8px;
|
|
298
|
+
font-size: 11px;
|
|
299
|
+
font-weight: 600;
|
|
300
|
+
text-transform: uppercase;
|
|
301
|
+
letter-spacing: 0.5px;
|
|
302
|
+
color: var(--vscode-sideBarSectionHeader-foreground, var(--vscode-foreground));
|
|
303
|
+
opacity: 0.8;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
.section-header .total {
|
|
307
|
+
font-weight: 400;
|
|
308
|
+
opacity: 0.7;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
.stat-row {
|
|
312
|
+
display: flex;
|
|
313
|
+
align-items: center;
|
|
314
|
+
justify-content: space-between;
|
|
315
|
+
padding: 3px 0;
|
|
316
|
+
font-size: var(--vscode-font-size);
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
.stat-label {
|
|
320
|
+
display: flex;
|
|
321
|
+
align-items: center;
|
|
322
|
+
gap: 7px;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
.dot {
|
|
326
|
+
width: 8px;
|
|
327
|
+
height: 8px;
|
|
328
|
+
border-radius: 50%;
|
|
329
|
+
flex-shrink: 0;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
.stat-count {
|
|
333
|
+
opacity: 0.7;
|
|
334
|
+
font-variant-numeric: tabular-nums;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
.feature-list {
|
|
338
|
+
list-style: none;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
.feature-item {
|
|
342
|
+
display: flex;
|
|
343
|
+
align-items: center;
|
|
344
|
+
gap: 7px;
|
|
345
|
+
padding: 4px 6px;
|
|
346
|
+
border-radius: 4px;
|
|
347
|
+
cursor: pointer;
|
|
348
|
+
font-size: var(--vscode-font-size);
|
|
349
|
+
white-space: nowrap;
|
|
350
|
+
overflow: hidden;
|
|
351
|
+
text-overflow: ellipsis;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
.feature-item:hover {
|
|
355
|
+
background: var(--vscode-list-hoverBackground);
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
.feature-dot {
|
|
359
|
+
width: 6px;
|
|
360
|
+
height: 6px;
|
|
361
|
+
border-radius: 50%;
|
|
362
|
+
flex-shrink: 0;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
.feature-title {
|
|
366
|
+
overflow: hidden;
|
|
367
|
+
text-overflow: ellipsis;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
.empty-state {
|
|
371
|
+
color: var(--vscode-descriptionForeground);
|
|
372
|
+
font-size: var(--vscode-font-size);
|
|
373
|
+
font-style: italic;
|
|
374
|
+
padding: 4px 0;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
.separator {
|
|
378
|
+
height: 1px;
|
|
379
|
+
background: var(--vscode-sideBarSectionHeader-border, var(--vscode-panel-border, transparent));
|
|
380
|
+
margin: 12px 0;
|
|
381
|
+
}
|
|
382
|
+
</style>
|
|
383
|
+
</head>
|
|
384
|
+
<body>
|
|
385
|
+
<div class="actions">
|
|
386
|
+
<button class="btn-primary" id="openBoard">
|
|
387
|
+
<svg width="14" height="14" viewBox="0 0 16 16" fill="currentColor"><path d="M14 1H2a1 1 0 0 0-1 1v12a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1V2a1 1 0 0 0-1-1zM2 0a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V2a2 2 0 0 0-2-2H2zm3 4a1 1 0 0 0-1 1v6a1 1 0 0 0 2 0V5a1 1 0 0 0-1-1zm3 0a1 1 0 0 0-1 1v4a1 1 0 0 0 2 0V5a1 1 0 0 0-1-1zm3 0a1 1 0 0 0-1 1v8a1 1 0 0 0 2 0V5a1 1 0 0 0-1-1z"/></svg>
|
|
388
|
+
Open Board
|
|
389
|
+
</button>
|
|
390
|
+
<button class="btn-secondary" id="newFeature">
|
|
391
|
+
<svg width="14" height="14" viewBox="0 0 16 16" fill="currentColor"><path d="M8 1a.5.5 0 0 1 .5.5V7h5.5a.5.5 0 0 1 0 1H8.5v5.5a.5.5 0 0 1-1 0V8H2a.5.5 0 0 1 0-1h5.5V1.5A.5.5 0 0 1 8 1z"/></svg>
|
|
392
|
+
New Feature
|
|
393
|
+
</button>
|
|
394
|
+
</div>
|
|
395
|
+
|
|
396
|
+
<div class="separator"></div>
|
|
397
|
+
|
|
398
|
+
<div class="section" id="overviewSection">
|
|
399
|
+
<div class="section-header">
|
|
400
|
+
<span>Overview</span>
|
|
401
|
+
<span class="total" id="totalCount">0 total</span>
|
|
402
|
+
</div>
|
|
403
|
+
<div id="statRows"></div>
|
|
404
|
+
</div>
|
|
405
|
+
|
|
406
|
+
<div class="separator"></div>
|
|
407
|
+
|
|
408
|
+
<div class="section" id="inProgressSection" style="display:none;">
|
|
409
|
+
<div class="section-header">
|
|
410
|
+
<span>In Progress</span>
|
|
411
|
+
</div>
|
|
412
|
+
<ul class="feature-list" id="inProgressList"></ul>
|
|
413
|
+
</div>
|
|
414
|
+
|
|
415
|
+
<script nonce="${nonce}">
|
|
416
|
+
(function() {
|
|
417
|
+
const vscode = acquireVsCodeApi();
|
|
418
|
+
let columns = [];
|
|
419
|
+
let features = [];
|
|
420
|
+
|
|
421
|
+
document.getElementById('openBoard').addEventListener('click', () => {
|
|
422
|
+
vscode.postMessage({ type: 'openBoard' });
|
|
423
|
+
});
|
|
424
|
+
document.getElementById('newFeature').addEventListener('click', () => {
|
|
425
|
+
vscode.postMessage({ type: 'newFeature' });
|
|
426
|
+
});
|
|
427
|
+
|
|
428
|
+
window.addEventListener('message', e => {
|
|
429
|
+
const msg = e.data;
|
|
430
|
+
if (msg.type === 'update') {
|
|
431
|
+
columns = msg.columns;
|
|
432
|
+
features = msg.features;
|
|
433
|
+
render();
|
|
434
|
+
} else if (msg.type === 'boardOpenChanged') {
|
|
435
|
+
document.getElementById('openBoard').style.display = msg.open ? 'none' : '';
|
|
436
|
+
}
|
|
437
|
+
});
|
|
438
|
+
|
|
439
|
+
function render() {
|
|
440
|
+
// Total count
|
|
441
|
+
document.getElementById('totalCount').textContent = features.length + ' total';
|
|
442
|
+
|
|
443
|
+
// Stat rows
|
|
444
|
+
const statRows = document.getElementById('statRows');
|
|
445
|
+
statRows.innerHTML = '';
|
|
446
|
+
for (const col of columns) {
|
|
447
|
+
const count = features.filter(f => f.status === col.id).length;
|
|
448
|
+
const row = document.createElement('div');
|
|
449
|
+
row.className = 'stat-row';
|
|
450
|
+
row.innerHTML =
|
|
451
|
+
'<span class="stat-label">' +
|
|
452
|
+
'<span class="dot" style="background:' + col.color + '"></span>' +
|
|
453
|
+
col.name +
|
|
454
|
+
'</span>' +
|
|
455
|
+
'<span class="stat-count">' + count + '</span>';
|
|
456
|
+
statRows.appendChild(row);
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
// In-progress features
|
|
460
|
+
const inProgressCol = columns.find(c => c.id === 'in-progress');
|
|
461
|
+
const inProgressColor = inProgressCol ? inProgressCol.color : '#f59e0b';
|
|
462
|
+
const inProgress = features.filter(f => f.status === 'in-progress');
|
|
463
|
+
const section = document.getElementById('inProgressSection');
|
|
464
|
+
const list = document.getElementById('inProgressList');
|
|
465
|
+
|
|
466
|
+
if (inProgress.length > 0) {
|
|
467
|
+
section.style.display = '';
|
|
468
|
+
list.innerHTML = '';
|
|
469
|
+
for (const f of inProgress) {
|
|
470
|
+
const li = document.createElement('li');
|
|
471
|
+
li.className = 'feature-item';
|
|
472
|
+
li.title = f.title;
|
|
473
|
+
li.innerHTML =
|
|
474
|
+
'<span class="feature-dot" style="background:' + inProgressColor + '"></span>' +
|
|
475
|
+
'<span class="feature-title">' + escapeHtml(f.title) + '</span>';
|
|
476
|
+
li.addEventListener('click', () => {
|
|
477
|
+
vscode.postMessage({ type: 'openFeature', featureId: f.id });
|
|
478
|
+
});
|
|
479
|
+
list.appendChild(li);
|
|
480
|
+
}
|
|
481
|
+
} else {
|
|
482
|
+
section.style.display = 'none';
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
function escapeHtml(str) {
|
|
487
|
+
const div = document.createElement('div');
|
|
488
|
+
div.textContent = str;
|
|
489
|
+
return div.innerHTML;
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
vscode.postMessage({ type: 'ready' });
|
|
493
|
+
})();
|
|
494
|
+
</script>
|
|
495
|
+
</body>
|
|
496
|
+
</html>`
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
private _getNonce(): string {
|
|
500
|
+
let text = ''
|
|
501
|
+
const possible = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'
|
|
502
|
+
for (let i = 0; i < 32; i++) {
|
|
503
|
+
text += possible.charAt(Math.floor(Math.random() * possible.length))
|
|
504
|
+
}
|
|
505
|
+
return text
|
|
506
|
+
}
|
|
507
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import * as path from 'path'
|
|
2
|
+
import * as vscode from 'vscode'
|
|
3
|
+
|
|
4
|
+
export function getFeatureFilePath(featuresDir: string, status: string, filename: string): string {
|
|
5
|
+
return path.join(featuresDir, status, `${filename}.md`)
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export async function ensureStatusSubfolders(featuresDir: string, statuses: string[]): Promise<void> {
|
|
9
|
+
for (const status of statuses) {
|
|
10
|
+
await vscode.workspace.fs.createDirectory(vscode.Uri.file(path.join(featuresDir, status)))
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export async function moveFeatureFile(
|
|
15
|
+
currentPath: string,
|
|
16
|
+
featuresDir: string,
|
|
17
|
+
newStatus: string,
|
|
18
|
+
attachments?: string[]
|
|
19
|
+
): Promise<string> {
|
|
20
|
+
const filename = path.basename(currentPath)
|
|
21
|
+
const targetDir = path.join(featuresDir, newStatus)
|
|
22
|
+
let targetPath = path.join(targetDir, filename)
|
|
23
|
+
|
|
24
|
+
if (currentPath === targetPath) return currentPath
|
|
25
|
+
|
|
26
|
+
const ext = path.extname(filename)
|
|
27
|
+
const base = path.basename(filename, ext)
|
|
28
|
+
let counter = 1
|
|
29
|
+
while (await fileExists(targetPath)) {
|
|
30
|
+
targetPath = path.join(targetDir, `${base}-${counter}${ext}`)
|
|
31
|
+
counter++
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
await vscode.workspace.fs.createDirectory(vscode.Uri.file(targetDir))
|
|
35
|
+
await vscode.workspace.fs.rename(vscode.Uri.file(currentPath), vscode.Uri.file(targetPath))
|
|
36
|
+
|
|
37
|
+
if (attachments && attachments.length > 0) {
|
|
38
|
+
const sourceDir = path.dirname(currentPath)
|
|
39
|
+
for (const attachment of attachments) {
|
|
40
|
+
const srcAttach = path.join(sourceDir, attachment)
|
|
41
|
+
const destAttach = path.join(targetDir, attachment)
|
|
42
|
+
try {
|
|
43
|
+
if (await fileExists(srcAttach)) {
|
|
44
|
+
await vscode.workspace.fs.rename(
|
|
45
|
+
vscode.Uri.file(srcAttach),
|
|
46
|
+
vscode.Uri.file(destAttach)
|
|
47
|
+
)
|
|
48
|
+
}
|
|
49
|
+
} catch {
|
|
50
|
+
// Best effort -- skip failed attachment moves
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return targetPath
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export async function renameFeatureFile(currentPath: string, newFilename: string): Promise<string> {
|
|
59
|
+
const dir = path.dirname(currentPath)
|
|
60
|
+
const newPath = path.join(dir, `${newFilename}.md`)
|
|
61
|
+
if (currentPath === newPath) return currentPath
|
|
62
|
+
await vscode.workspace.fs.rename(vscode.Uri.file(currentPath), vscode.Uri.file(newPath))
|
|
63
|
+
return newPath
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function getStatusFromPath(filePath: string, featuresDir: string): string | null {
|
|
67
|
+
const relative = path.relative(featuresDir, filePath)
|
|
68
|
+
const parts = relative.split(path.sep)
|
|
69
|
+
if (parts.length === 2) {
|
|
70
|
+
return parts[0]
|
|
71
|
+
}
|
|
72
|
+
return null
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
async function fileExists(filePath: string): Promise<boolean> {
|
|
76
|
+
try {
|
|
77
|
+
await vscode.workspace.fs.stat(vscode.Uri.file(filePath))
|
|
78
|
+
return true
|
|
79
|
+
} catch {
|
|
80
|
+
return false
|
|
81
|
+
}
|
|
82
|
+
}
|