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
@@ -270,6 +270,8 @@ var DEFAULT_CONFIG = {
270
270
  compactMode: false,
271
271
  markdownEditorMode: false,
272
272
  showDeletedColumn: false,
273
+ boardZoom: 100,
274
+ cardZoom: 100,
273
275
  port: 3e3,
274
276
  labels: {}
275
277
  };
@@ -320,6 +322,8 @@ function migrateConfigV1ToV2(raw) {
320
322
  compactMode: v1.compactMode,
321
323
  markdownEditorMode: v1.markdownEditorMode,
322
324
  showDeletedColumn: false,
325
+ boardZoom: 100,
326
+ cardZoom: 100,
323
327
  port: 3e3
324
328
  };
325
329
  }
@@ -397,7 +401,9 @@ function configToSettings(config) {
397
401
  markdownEditorMode: config.markdownEditorMode,
398
402
  showDeletedColumn: config.showDeletedColumn,
399
403
  defaultPriority: config.defaultPriority,
400
- defaultStatus: config.defaultStatus
404
+ defaultStatus: config.defaultStatus,
405
+ boardZoom: config.boardZoom ?? 100,
406
+ cardZoom: config.cardZoom ?? 100
401
407
  };
402
408
  }
403
409
  function settingsToConfig(config, settings) {
@@ -411,7 +417,9 @@ function settingsToConfig(config, settings) {
411
417
  compactMode: settings.compactMode,
412
418
  showDeletedColumn: settings.showDeletedColumn,
413
419
  defaultPriority: settings.defaultPriority,
414
- defaultStatus: settings.defaultStatus
420
+ defaultStatus: settings.defaultStatus,
421
+ boardZoom: settings.boardZoom,
422
+ cardZoom: settings.cardZoom
415
423
  };
416
424
  }
417
425
 
@@ -3071,6 +3079,7 @@ function renamed(from, to) {
3071
3079
  throw new Error("Function yaml." + from + " is removed in js-yaml 4. Use yaml." + to + " instead, which is now safe by default.");
3072
3080
  };
3073
3081
  }
3082
+ var JSON_SCHEMA = json;
3074
3083
  var load = loader.load;
3075
3084
  var loadAll = loader.loadAll;
3076
3085
  var dump = dumper.dump;
@@ -3109,48 +3118,26 @@ function parseFeatureFile(content, filePath) {
3109
3118
  return null;
3110
3119
  const frontmatter = frontmatterMatch[1];
3111
3120
  const rest = frontmatterMatch[2] || "";
3112
- const getValue = (key) => {
3113
- const match = frontmatter.match(new RegExp(`^${key}:\\s*(.*)$`, "m"));
3114
- if (!match)
3121
+ let parsed;
3122
+ try {
3123
+ const loaded = load(frontmatter, { schema: JSON_SCHEMA });
3124
+ if (!loaded || typeof loaded !== "object" || Array.isArray(loaded))
3125
+ return null;
3126
+ parsed = loaded;
3127
+ } catch {
3128
+ return null;
3129
+ }
3130
+ const str2 = (key) => {
3131
+ const val = parsed[key];
3132
+ if (val == null)
3115
3133
  return "";
3116
- const value = match[1].trim().replace(/^["']|["']$/g, "");
3117
- return value === "null" ? "" : value;
3134
+ return String(val);
3118
3135
  };
3119
- const getArrayValue = (key) => {
3120
- const match = frontmatter.match(new RegExp(`^${key}:\\s*\\[([^\\]]*)\\]`, "m"));
3121
- if (!match)
3136
+ const arr = (key) => {
3137
+ const val = parsed[key];
3138
+ if (!Array.isArray(val))
3122
3139
  return [];
3123
- return match[1].split(",").map((s) => s.trim().replace(/^["']|["']$/g, "")).filter(Boolean);
3124
- };
3125
- const getMetadata = () => {
3126
- const lines = frontmatter.split("\n");
3127
- let metaStart = -1;
3128
- for (let j = 0; j < lines.length; j++) {
3129
- if (/^metadata:\s*$/.test(lines[j])) {
3130
- metaStart = j + 1;
3131
- break;
3132
- }
3133
- }
3134
- if (metaStart === -1)
3135
- return void 0;
3136
- const indentedLines = [];
3137
- for (let j = metaStart; j < lines.length; j++) {
3138
- if (/^\s/.test(lines[j]) || lines[j].trim() === "") {
3139
- indentedLines.push(lines[j]);
3140
- } else {
3141
- break;
3142
- }
3143
- }
3144
- if (indentedLines.length === 0)
3145
- return void 0;
3146
- try {
3147
- const parsed = load(indentedLines.join("\n"));
3148
- if (parsed && typeof parsed === "object")
3149
- return parsed;
3150
- return void 0;
3151
- } catch {
3152
- return void 0;
3153
- }
3140
+ return val.filter((v) => v != null).map(String);
3154
3141
  };
3155
3142
  const sections = rest.split(/\n---\n/);
3156
3143
  let body = sections[0] || "";
@@ -3171,22 +3158,23 @@ ${section}`;
3171
3158
  i += 1;
3172
3159
  }
3173
3160
  }
3174
- const meta = getMetadata();
3175
- const actions = getArrayValue("actions");
3161
+ const actions = arr("actions");
3162
+ const rawMeta = parsed.metadata;
3163
+ const meta = rawMeta != null && typeof rawMeta === "object" && !Array.isArray(rawMeta) ? rawMeta : void 0;
3176
3164
  return {
3177
- version: parseInt(getValue("version"), 10) || 0,
3178
- id: getValue("id") || extractIdFromFilename(filePath),
3179
- status: getValue("status") || "backlog",
3180
- priority: getValue("priority") || "medium",
3181
- assignee: getValue("assignee") || null,
3182
- dueDate: getValue("dueDate") || null,
3183
- created: getValue("created") || (/* @__PURE__ */ new Date()).toISOString(),
3184
- modified: getValue("modified") || (/* @__PURE__ */ new Date()).toISOString(),
3185
- completedAt: getValue("completedAt") || null,
3186
- labels: getArrayValue("labels"),
3187
- attachments: getArrayValue("attachments"),
3165
+ version: typeof parsed.version === "number" ? parsed.version : parseInt(str2("version"), 10) || 0,
3166
+ id: str2("id") || extractIdFromFilename(filePath),
3167
+ status: str2("status") || "backlog",
3168
+ priority: str2("priority") || "medium",
3169
+ assignee: parsed.assignee != null ? String(parsed.assignee) : null,
3170
+ dueDate: parsed.dueDate != null ? String(parsed.dueDate) : null,
3171
+ created: str2("created") || (/* @__PURE__ */ new Date()).toISOString(),
3172
+ modified: str2("modified") || (/* @__PURE__ */ new Date()).toISOString(),
3173
+ completedAt: parsed.completedAt != null ? String(parsed.completedAt) : null,
3174
+ labels: arr("labels"),
3175
+ attachments: arr("attachments"),
3188
3176
  comments,
3189
- order: getValue("order") || "a0",
3177
+ order: str2("order") || "a0",
3190
3178
  content: body.trim(),
3191
3179
  ...meta ? { metadata: meta } : {},
3192
3180
  ...actions.length > 0 ? { actions } : {},
@@ -3194,37 +3182,28 @@ ${section}`;
3194
3182
  };
3195
3183
  }
3196
3184
  function serializeFeature(feature) {
3197
- const lines = [
3198
- "---",
3199
- `version: ${feature.version ?? CARD_FORMAT_VERSION}`,
3200
- `id: "${feature.id}"`,
3201
- `status: "${feature.status}"`,
3202
- `priority: "${feature.priority}"`,
3203
- `assignee: ${feature.assignee ? `"${feature.assignee}"` : "null"}`,
3204
- `dueDate: ${feature.dueDate ? `"${feature.dueDate}"` : "null"}`,
3205
- `created: "${feature.created}"`,
3206
- `modified: "${feature.modified}"`,
3207
- `completedAt: ${feature.completedAt ? `"${feature.completedAt}"` : "null"}`,
3208
- `labels: [${feature.labels.map((l) => `"${l}"`).join(", ")}]`,
3209
- `attachments: [${(feature.attachments || []).map((a) => `"${a}"`).join(", ")}]`,
3210
- `order: "${feature.order}"`
3211
- ];
3212
- if (feature.actions && feature.actions.length > 0) {
3213
- lines.push(`actions: [${feature.actions.map((a) => `"${a}"`).join(", ")}]`);
3214
- }
3215
- if (feature.metadata && Object.keys(feature.metadata).length > 0) {
3216
- const metaYaml = dump(feature.metadata, { indent: 2, lineWidth: -1 });
3217
- lines.push("metadata:");
3218
- for (const line of metaYaml.trimEnd().split("\n")) {
3219
- lines.push(" " + line);
3220
- }
3221
- }
3222
- lines.push("---");
3223
- lines.push("");
3224
- const frontmatter = lines.join("\n");
3225
- let result = frontmatter + feature.content;
3226
- const comments = feature.comments || [];
3227
- for (const comment of comments) {
3185
+ const frontmatterObj = {
3186
+ version: feature.version ?? CARD_FORMAT_VERSION,
3187
+ id: feature.id,
3188
+ status: feature.status,
3189
+ priority: feature.priority,
3190
+ assignee: feature.assignee ?? null,
3191
+ dueDate: feature.dueDate ?? null,
3192
+ created: feature.created,
3193
+ modified: feature.modified,
3194
+ completedAt: feature.completedAt ?? null,
3195
+ labels: feature.labels,
3196
+ attachments: feature.attachments || [],
3197
+ order: feature.order,
3198
+ ...feature.actions?.length ? { actions: feature.actions } : {},
3199
+ ...feature.metadata && Object.keys(feature.metadata).length > 0 ? { metadata: feature.metadata } : {}
3200
+ };
3201
+ const yamlStr = dump(frontmatterObj, { lineWidth: -1, quotingType: '"', forceQuotes: true });
3202
+ let result = `---
3203
+ ${yamlStr}---
3204
+
3205
+ ${feature.content}`;
3206
+ for (const comment of feature.comments || []) {
3228
3207
  result += "\n\n---\n";
3229
3208
  result += `comment: true
3230
3209
  `;
@@ -4217,25 +4196,28 @@ var KanbanSDK = class {
4217
4196
  writeConfig(this.workspaceRoot, config);
4218
4197
  }
4219
4198
  /**
4220
- * Removes a label definition from the workspace configuration.
4221
- *
4222
- * This only removes the color/group definition — cards that use this
4223
- * label keep their label strings. Those labels will render with default
4224
- * gray styling in the UI.
4199
+ * Removes a label definition from the workspace configuration and cascades
4200
+ * the deletion to all cards by removing the label from their `labels` array.
4225
4201
  *
4226
4202
  * @param name - The label name to remove.
4227
4203
  *
4228
4204
  * @example
4229
4205
  * ```ts
4230
- * sdk.deleteLabel('bug')
4206
+ * await sdk.deleteLabel('bug')
4231
4207
  * ```
4232
4208
  */
4233
- deleteLabel(name) {
4209
+ async deleteLabel(name) {
4234
4210
  const config = readConfig(this.workspaceRoot);
4235
4211
  if (config.labels) {
4236
4212
  delete config.labels[name];
4237
4213
  writeConfig(this.workspaceRoot, config);
4238
4214
  }
4215
+ const cards = await this.listCards();
4216
+ for (const card of cards) {
4217
+ if (card.labels.includes(name)) {
4218
+ await this.updateCard(card.id, { labels: card.labels.filter((l) => l !== name) });
4219
+ }
4220
+ }
4239
4221
  }
4240
4222
  /**
4241
4223
  * Renames a label in the configuration and cascades the change to all cards.
@@ -4660,6 +4642,57 @@ var KanbanSDK = class {
4660
4642
  this.emitEvent("column.deleted", removed);
4661
4643
  return board.columns;
4662
4644
  }
4645
+ /**
4646
+ * Moves all cards in the specified column to the `deleted` (soft-delete) column.
4647
+ *
4648
+ * This is a non-destructive operation — cards are moved to the reserved
4649
+ * `deleted` status and can be restored or permanently deleted later.
4650
+ * The column itself is not removed.
4651
+ *
4652
+ * @param columnId - The ID of the column whose cards should be moved to `deleted`.
4653
+ * @param boardId - Optional board ID. Defaults to the workspace's default board.
4654
+ * @returns A promise resolving to the number of cards that were moved.
4655
+ * @throws {Error} If the column is `'deleted'` (no-op protection).
4656
+ *
4657
+ * @example
4658
+ * ```ts
4659
+ * const moved = await sdk.cleanupColumn('blocked')
4660
+ * console.log(`Moved ${moved} cards to deleted`)
4661
+ * ```
4662
+ */
4663
+ async cleanupColumn(columnId, boardId) {
4664
+ if (columnId === DELETED_STATUS_ID)
4665
+ return 0;
4666
+ const cards = await this.listCards(void 0, boardId);
4667
+ const cardsToMove = cards.filter((c) => c.status === columnId);
4668
+ for (const card of cardsToMove) {
4669
+ await this.moveCard(card.id, DELETED_STATUS_ID, 0, boardId);
4670
+ }
4671
+ return cardsToMove.length;
4672
+ }
4673
+ /**
4674
+ * Permanently deletes all cards currently in the `deleted` column.
4675
+ *
4676
+ * This is equivalent to "empty trash". All soft-deleted cards are
4677
+ * removed from disk. This operation cannot be undone.
4678
+ *
4679
+ * @param boardId - Optional board ID. Defaults to the workspace's default board.
4680
+ * @returns A promise resolving to the number of cards that were permanently deleted.
4681
+ *
4682
+ * @example
4683
+ * ```ts
4684
+ * const count = await sdk.purgeDeletedCards()
4685
+ * console.log(`Permanently deleted ${count} cards`)
4686
+ * ```
4687
+ */
4688
+ async purgeDeletedCards(boardId) {
4689
+ const cards = await this.listCards(void 0, boardId);
4690
+ const deleted = cards.filter((c) => c.status === DELETED_STATUS_ID);
4691
+ for (const card of deleted) {
4692
+ await this.permanentlyDeleteCard(card.id, boardId);
4693
+ }
4694
+ return deleted.length;
4695
+ }
4663
4696
  /**
4664
4697
  * Reorders the columns of a board.
4665
4698
  *
@@ -484,20 +484,17 @@ export declare class KanbanSDK {
484
484
  */
485
485
  setLabel(name: string, definition: LabelDefinition): void;
486
486
  /**
487
- * Removes a label definition from the workspace configuration.
488
- *
489
- * This only removes the color/group definition — cards that use this
490
- * label keep their label strings. Those labels will render with default
491
- * gray styling in the UI.
487
+ * Removes a label definition from the workspace configuration and cascades
488
+ * the deletion to all cards by removing the label from their `labels` array.
492
489
  *
493
490
  * @param name - The label name to remove.
494
491
  *
495
492
  * @example
496
493
  * ```ts
497
- * sdk.deleteLabel('bug')
494
+ * await sdk.deleteLabel('bug')
498
495
  * ```
499
496
  */
500
- deleteLabel(name: string): void;
497
+ deleteLabel(name: string): Promise<void>;
501
498
  /**
502
499
  * Renames a label in the configuration and cascades the change to all cards.
503
500
  *
@@ -754,6 +751,41 @@ export declare class KanbanSDK {
754
751
  * ```
755
752
  */
756
753
  removeColumn(columnId: string, boardId?: string): Promise<KanbanColumn[]>;
754
+ /**
755
+ * Moves all cards in the specified column to the `deleted` (soft-delete) column.
756
+ *
757
+ * This is a non-destructive operation — cards are moved to the reserved
758
+ * `deleted` status and can be restored or permanently deleted later.
759
+ * The column itself is not removed.
760
+ *
761
+ * @param columnId - The ID of the column whose cards should be moved to `deleted`.
762
+ * @param boardId - Optional board ID. Defaults to the workspace's default board.
763
+ * @returns A promise resolving to the number of cards that were moved.
764
+ * @throws {Error} If the column is `'deleted'` (no-op protection).
765
+ *
766
+ * @example
767
+ * ```ts
768
+ * const moved = await sdk.cleanupColumn('blocked')
769
+ * console.log(`Moved ${moved} cards to deleted`)
770
+ * ```
771
+ */
772
+ cleanupColumn(columnId: string, boardId?: string): Promise<number>;
773
+ /**
774
+ * Permanently deletes all cards currently in the `deleted` column.
775
+ *
776
+ * This is equivalent to "empty trash". All soft-deleted cards are
777
+ * removed from disk. This operation cannot be undone.
778
+ *
779
+ * @param boardId - Optional board ID. Defaults to the workspace's default board.
780
+ * @returns A promise resolving to the number of cards that were permanently deleted.
781
+ *
782
+ * @example
783
+ * ```ts
784
+ * const count = await sdk.purgeDeletedCards()
785
+ * console.log(`Permanently deleted ${count} cards`)
786
+ * ```
787
+ */
788
+ purgeDeletedCards(boardId?: string): Promise<number>;
757
789
  /**
758
790
  * Reorders the columns of a board.
759
791
  *
@@ -78,6 +78,10 @@ export interface KanbanConfig {
78
78
  markdownEditorMode: boolean;
79
79
  /** Whether to show the deleted column in the UI. */
80
80
  showDeletedColumn: boolean;
81
+ /** Zoom level for the board view (75–150). */
82
+ boardZoom: number;
83
+ /** Zoom level for the card detail panel (75–150). */
84
+ cardZoom: number;
81
85
  /** Port number for the standalone HTTP server. */
82
86
  port: number;
83
87
  /** Registered webhook endpoints for event notifications. */
@@ -209,6 +209,10 @@ export interface CardDisplaySettings {
209
209
  defaultPriority: Priority;
210
210
  /** The default column/status assigned to newly created cards. */
211
211
  defaultStatus: string;
212
+ /** Zoom level for the board view as a percentage (75–150). Default 100. */
213
+ boardZoom: number;
214
+ /** Zoom level for the card detail panel as a percentage (75–150). Default 100. */
215
+ cardZoom: number;
212
216
  }
213
217
  export interface LabelDefinition {
214
218
  color: string;