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
package/dist/extension.js CHANGED
@@ -3845,6 +3845,8 @@ var DEFAULT_CONFIG = {
3845
3845
  compactMode: false,
3846
3846
  markdownEditorMode: false,
3847
3847
  showDeletedColumn: false,
3848
+ boardZoom: 100,
3849
+ cardZoom: 100,
3848
3850
  port: 3e3,
3849
3851
  labels: {}
3850
3852
  };
@@ -3895,6 +3897,8 @@ function migrateConfigV1ToV2(raw) {
3895
3897
  compactMode: v1.compactMode,
3896
3898
  markdownEditorMode: v1.markdownEditorMode,
3897
3899
  showDeletedColumn: false,
3900
+ boardZoom: 100,
3901
+ cardZoom: 100,
3898
3902
  port: 3e3
3899
3903
  };
3900
3904
  }
@@ -3968,7 +3972,9 @@ function configToSettings(config) {
3968
3972
  markdownEditorMode: config.markdownEditorMode,
3969
3973
  showDeletedColumn: config.showDeletedColumn,
3970
3974
  defaultPriority: config.defaultPriority,
3971
- defaultStatus: config.defaultStatus
3975
+ defaultStatus: config.defaultStatus,
3976
+ boardZoom: config.boardZoom ?? 100,
3977
+ cardZoom: config.cardZoom ?? 100
3972
3978
  };
3973
3979
  }
3974
3980
  function settingsToConfig(config, settings) {
@@ -3982,7 +3988,9 @@ function settingsToConfig(config, settings) {
3982
3988
  compactMode: settings.compactMode,
3983
3989
  showDeletedColumn: settings.showDeletedColumn,
3984
3990
  defaultPriority: settings.defaultPriority,
3985
- defaultStatus: settings.defaultStatus
3991
+ defaultStatus: settings.defaultStatus,
3992
+ boardZoom: settings.boardZoom,
3993
+ cardZoom: settings.cardZoom
3986
3994
  };
3987
3995
  }
3988
3996
 
@@ -6859,6 +6867,7 @@ function renamed(from, to) {
6859
6867
  throw new Error("Function yaml." + from + " is removed in js-yaml 4. Use yaml." + to + " instead, which is now safe by default.");
6860
6868
  };
6861
6869
  }
6870
+ var JSON_SCHEMA = json;
6862
6871
  var load = loader.load;
6863
6872
  var loadAll = loader.loadAll;
6864
6873
  var dump = dumper.dump;
@@ -6898,48 +6907,26 @@ function parseFeatureFile(content, filePath) {
6898
6907
  return null;
6899
6908
  const frontmatter = frontmatterMatch[1];
6900
6909
  const rest = frontmatterMatch[2] || "";
6901
- const getValue = (key) => {
6902
- const match = frontmatter.match(new RegExp(`^${key}:\\s*(.*)$`, "m"));
6903
- if (!match)
6910
+ let parsed;
6911
+ try {
6912
+ const loaded = load(frontmatter, { schema: JSON_SCHEMA });
6913
+ if (!loaded || typeof loaded !== "object" || Array.isArray(loaded))
6914
+ return null;
6915
+ parsed = loaded;
6916
+ } catch {
6917
+ return null;
6918
+ }
6919
+ const str2 = (key) => {
6920
+ const val = parsed[key];
6921
+ if (val == null)
6904
6922
  return "";
6905
- const value = match[1].trim().replace(/^["']|["']$/g, "");
6906
- return value === "null" ? "" : value;
6923
+ return String(val);
6907
6924
  };
6908
- const getArrayValue = (key) => {
6909
- const match = frontmatter.match(new RegExp(`^${key}:\\s*\\[([^\\]]*)\\]`, "m"));
6910
- if (!match)
6925
+ const arr = (key) => {
6926
+ const val = parsed[key];
6927
+ if (!Array.isArray(val))
6911
6928
  return [];
6912
- return match[1].split(",").map((s) => s.trim().replace(/^["']|["']$/g, "")).filter(Boolean);
6913
- };
6914
- const getMetadata = () => {
6915
- const lines = frontmatter.split("\n");
6916
- let metaStart = -1;
6917
- for (let j = 0; j < lines.length; j++) {
6918
- if (/^metadata:\s*$/.test(lines[j])) {
6919
- metaStart = j + 1;
6920
- break;
6921
- }
6922
- }
6923
- if (metaStart === -1)
6924
- return void 0;
6925
- const indentedLines = [];
6926
- for (let j = metaStart; j < lines.length; j++) {
6927
- if (/^\s/.test(lines[j]) || lines[j].trim() === "") {
6928
- indentedLines.push(lines[j]);
6929
- } else {
6930
- break;
6931
- }
6932
- }
6933
- if (indentedLines.length === 0)
6934
- return void 0;
6935
- try {
6936
- const parsed = load(indentedLines.join("\n"));
6937
- if (parsed && typeof parsed === "object")
6938
- return parsed;
6939
- return void 0;
6940
- } catch {
6941
- return void 0;
6942
- }
6929
+ return val.filter((v) => v != null).map(String);
6943
6930
  };
6944
6931
  const sections = rest.split(/\n---\n/);
6945
6932
  let body = sections[0] || "";
@@ -6960,22 +6947,23 @@ ${section}`;
6960
6947
  i += 1;
6961
6948
  }
6962
6949
  }
6963
- const meta = getMetadata();
6964
- const actions = getArrayValue("actions");
6950
+ const actions = arr("actions");
6951
+ const rawMeta = parsed.metadata;
6952
+ const meta = rawMeta != null && typeof rawMeta === "object" && !Array.isArray(rawMeta) ? rawMeta : void 0;
6965
6953
  return {
6966
- version: parseInt(getValue("version"), 10) || 0,
6967
- id: getValue("id") || extractIdFromFilename(filePath),
6968
- status: getValue("status") || "backlog",
6969
- priority: getValue("priority") || "medium",
6970
- assignee: getValue("assignee") || null,
6971
- dueDate: getValue("dueDate") || null,
6972
- created: getValue("created") || (/* @__PURE__ */ new Date()).toISOString(),
6973
- modified: getValue("modified") || (/* @__PURE__ */ new Date()).toISOString(),
6974
- completedAt: getValue("completedAt") || null,
6975
- labels: getArrayValue("labels"),
6976
- attachments: getArrayValue("attachments"),
6954
+ version: typeof parsed.version === "number" ? parsed.version : parseInt(str2("version"), 10) || 0,
6955
+ id: str2("id") || extractIdFromFilename(filePath),
6956
+ status: str2("status") || "backlog",
6957
+ priority: str2("priority") || "medium",
6958
+ assignee: parsed.assignee != null ? String(parsed.assignee) : null,
6959
+ dueDate: parsed.dueDate != null ? String(parsed.dueDate) : null,
6960
+ created: str2("created") || (/* @__PURE__ */ new Date()).toISOString(),
6961
+ modified: str2("modified") || (/* @__PURE__ */ new Date()).toISOString(),
6962
+ completedAt: parsed.completedAt != null ? String(parsed.completedAt) : null,
6963
+ labels: arr("labels"),
6964
+ attachments: arr("attachments"),
6977
6965
  comments,
6978
- order: getValue("order") || "a0",
6966
+ order: str2("order") || "a0",
6979
6967
  content: body.trim(),
6980
6968
  ...meta ? { metadata: meta } : {},
6981
6969
  ...actions.length > 0 ? { actions } : {},
@@ -6983,37 +6971,28 @@ ${section}`;
6983
6971
  };
6984
6972
  }
6985
6973
  function serializeFeature(feature) {
6986
- const lines = [
6987
- "---",
6988
- `version: ${feature.version ?? CARD_FORMAT_VERSION}`,
6989
- `id: "${feature.id}"`,
6990
- `status: "${feature.status}"`,
6991
- `priority: "${feature.priority}"`,
6992
- `assignee: ${feature.assignee ? `"${feature.assignee}"` : "null"}`,
6993
- `dueDate: ${feature.dueDate ? `"${feature.dueDate}"` : "null"}`,
6994
- `created: "${feature.created}"`,
6995
- `modified: "${feature.modified}"`,
6996
- `completedAt: ${feature.completedAt ? `"${feature.completedAt}"` : "null"}`,
6997
- `labels: [${feature.labels.map((l) => `"${l}"`).join(", ")}]`,
6998
- `attachments: [${(feature.attachments || []).map((a) => `"${a}"`).join(", ")}]`,
6999
- `order: "${feature.order}"`
7000
- ];
7001
- if (feature.actions && feature.actions.length > 0) {
7002
- lines.push(`actions: [${feature.actions.map((a) => `"${a}"`).join(", ")}]`);
7003
- }
7004
- if (feature.metadata && Object.keys(feature.metadata).length > 0) {
7005
- const metaYaml = dump(feature.metadata, { indent: 2, lineWidth: -1 });
7006
- lines.push("metadata:");
7007
- for (const line of metaYaml.trimEnd().split("\n")) {
7008
- lines.push(" " + line);
7009
- }
7010
- }
7011
- lines.push("---");
7012
- lines.push("");
7013
- const frontmatter = lines.join("\n");
7014
- let result = frontmatter + feature.content;
7015
- const comments = feature.comments || [];
7016
- for (const comment of comments) {
6974
+ const frontmatterObj = {
6975
+ version: feature.version ?? CARD_FORMAT_VERSION,
6976
+ id: feature.id,
6977
+ status: feature.status,
6978
+ priority: feature.priority,
6979
+ assignee: feature.assignee ?? null,
6980
+ dueDate: feature.dueDate ?? null,
6981
+ created: feature.created,
6982
+ modified: feature.modified,
6983
+ completedAt: feature.completedAt ?? null,
6984
+ labels: feature.labels,
6985
+ attachments: feature.attachments || [],
6986
+ order: feature.order,
6987
+ ...feature.actions?.length ? { actions: feature.actions } : {},
6988
+ ...feature.metadata && Object.keys(feature.metadata).length > 0 ? { metadata: feature.metadata } : {}
6989
+ };
6990
+ const yamlStr = dump(frontmatterObj, { lineWidth: -1, quotingType: '"', forceQuotes: true });
6991
+ let result = `---
6992
+ ${yamlStr}---
6993
+
6994
+ ${feature.content}`;
6995
+ for (const comment of feature.comments || []) {
7017
6996
  result += "\n\n---\n";
7018
6997
  result += `comment: true
7019
6998
  `;
@@ -8006,25 +7985,28 @@ var KanbanSDK = class {
8006
7985
  writeConfig(this.workspaceRoot, config);
8007
7986
  }
8008
7987
  /**
8009
- * Removes a label definition from the workspace configuration.
8010
- *
8011
- * This only removes the color/group definition — cards that use this
8012
- * label keep their label strings. Those labels will render with default
8013
- * gray styling in the UI.
7988
+ * Removes a label definition from the workspace configuration and cascades
7989
+ * the deletion to all cards by removing the label from their `labels` array.
8014
7990
  *
8015
7991
  * @param name - The label name to remove.
8016
7992
  *
8017
7993
  * @example
8018
7994
  * ```ts
8019
- * sdk.deleteLabel('bug')
7995
+ * await sdk.deleteLabel('bug')
8020
7996
  * ```
8021
7997
  */
8022
- deleteLabel(name) {
7998
+ async deleteLabel(name) {
8023
7999
  const config = readConfig(this.workspaceRoot);
8024
8000
  if (config.labels) {
8025
8001
  delete config.labels[name];
8026
8002
  writeConfig(this.workspaceRoot, config);
8027
8003
  }
8004
+ const cards = await this.listCards();
8005
+ for (const card of cards) {
8006
+ if (card.labels.includes(name)) {
8007
+ await this.updateCard(card.id, { labels: card.labels.filter((l) => l !== name) });
8008
+ }
8009
+ }
8028
8010
  }
8029
8011
  /**
8030
8012
  * Renames a label in the configuration and cascades the change to all cards.
@@ -8449,6 +8431,57 @@ var KanbanSDK = class {
8449
8431
  this.emitEvent("column.deleted", removed);
8450
8432
  return board.columns;
8451
8433
  }
8434
+ /**
8435
+ * Moves all cards in the specified column to the `deleted` (soft-delete) column.
8436
+ *
8437
+ * This is a non-destructive operation — cards are moved to the reserved
8438
+ * `deleted` status and can be restored or permanently deleted later.
8439
+ * The column itself is not removed.
8440
+ *
8441
+ * @param columnId - The ID of the column whose cards should be moved to `deleted`.
8442
+ * @param boardId - Optional board ID. Defaults to the workspace's default board.
8443
+ * @returns A promise resolving to the number of cards that were moved.
8444
+ * @throws {Error} If the column is `'deleted'` (no-op protection).
8445
+ *
8446
+ * @example
8447
+ * ```ts
8448
+ * const moved = await sdk.cleanupColumn('blocked')
8449
+ * console.log(`Moved ${moved} cards to deleted`)
8450
+ * ```
8451
+ */
8452
+ async cleanupColumn(columnId, boardId) {
8453
+ if (columnId === DELETED_STATUS_ID)
8454
+ return 0;
8455
+ const cards = await this.listCards(void 0, boardId);
8456
+ const cardsToMove = cards.filter((c) => c.status === columnId);
8457
+ for (const card of cardsToMove) {
8458
+ await this.moveCard(card.id, DELETED_STATUS_ID, 0, boardId);
8459
+ }
8460
+ return cardsToMove.length;
8461
+ }
8462
+ /**
8463
+ * Permanently deletes all cards currently in the `deleted` column.
8464
+ *
8465
+ * This is equivalent to "empty trash". All soft-deleted cards are
8466
+ * removed from disk. This operation cannot be undone.
8467
+ *
8468
+ * @param boardId - Optional board ID. Defaults to the workspace's default board.
8469
+ * @returns A promise resolving to the number of cards that were permanently deleted.
8470
+ *
8471
+ * @example
8472
+ * ```ts
8473
+ * const count = await sdk.purgeDeletedCards()
8474
+ * console.log(`Permanently deleted ${count} cards`)
8475
+ * ```
8476
+ */
8477
+ async purgeDeletedCards(boardId) {
8478
+ const cards = await this.listCards(void 0, boardId);
8479
+ const deleted = cards.filter((c) => c.status === DELETED_STATUS_ID);
8480
+ for (const card of deleted) {
8481
+ await this.permanentlyDeleteCard(card.id, boardId);
8482
+ }
8483
+ return deleted.length;
8484
+ }
8452
8485
  /**
8453
8486
  * Reorders the columns of a board.
8454
8487
  *
@@ -10561,10 +10594,7 @@ function startServer(featuresDir, port, webviewDir) {
10561
10594
  }
10562
10595
  async function doPurgeDeletedCards() {
10563
10596
  try {
10564
- const deletedCards = features.filter((f) => f.status === "deleted");
10565
- for (const card of deletedCards) {
10566
- await sdk.permanentlyDeleteCard(card.id, currentBoardId);
10567
- }
10597
+ await sdk.purgeDeletedCards(currentBoardId);
10568
10598
  await loadFeatures();
10569
10599
  broadcast(buildInitMessage());
10570
10600
  return true;
@@ -10611,6 +10641,20 @@ function startServer(featuresDir, port, webviewDir) {
10611
10641
  return { removed: false, error: String(err) };
10612
10642
  }
10613
10643
  }
10644
+ async function doCleanupColumn(columnId) {
10645
+ try {
10646
+ migrating = true;
10647
+ await sdk.cleanupColumn(columnId, currentBoardId);
10648
+ await loadFeatures();
10649
+ broadcast(buildInitMessage());
10650
+ return true;
10651
+ } catch (err) {
10652
+ console.error("Failed to cleanup column:", err);
10653
+ return false;
10654
+ } finally {
10655
+ migrating = false;
10656
+ }
10657
+ }
10614
10658
  function doSaveSettings(newSettings) {
10615
10659
  sdk.updateSettings(newSettings);
10616
10660
  broadcast(buildInitMessage());
@@ -10797,6 +10841,9 @@ function startServer(featuresDir, port, webviewDir) {
10797
10841
  case "removeColumn":
10798
10842
  await doRemoveColumn(msg.columnId);
10799
10843
  break;
10844
+ case "cleanupColumn":
10845
+ await doCleanupColumn(msg.columnId);
10846
+ break;
10800
10847
  case "removeAttachment": {
10801
10848
  const featureId = msg.featureId;
10802
10849
  const feature = await doRemoveAttachment(featureId, msg.attachment);
@@ -10945,13 +10992,16 @@ function startServer(featuresDir, port, webviewDir) {
10945
10992
  }
10946
10993
  case "renameLabel": {
10947
10994
  await sdk.renameLabel(msg.oldName, msg.newName);
10995
+ await loadFeatures();
10948
10996
  broadcast({ type: "labelsUpdated", labels: sdk.getLabels() });
10949
10997
  broadcast(buildInitMessage());
10950
10998
  break;
10951
10999
  }
10952
11000
  case "deleteLabel": {
10953
- sdk.deleteLabel(msg.name);
11001
+ await sdk.deleteLabel(msg.name);
11002
+ await loadFeatures();
10954
11003
  broadcast({ type: "labelsUpdated", labels: sdk.getLabels() });
11004
+ broadcast(buildInitMessage());
10955
11005
  break;
10956
11006
  }
10957
11007
  case "triggerAction": {
@@ -11563,6 +11613,9 @@ function startServer(featuresDir, port, webviewDir) {
11563
11613
  if (!newName)
11564
11614
  return jsonError(res, 400, "newName is required");
11565
11615
  await sdk.renameLabel(name, newName);
11616
+ await loadFeatures();
11617
+ broadcast({ type: "labelsUpdated", labels: sdk.getLabels() });
11618
+ broadcast(buildInitMessage());
11566
11619
  return jsonOk(res, sdk.getLabels());
11567
11620
  } catch (err) {
11568
11621
  return jsonError(res, 400, String(err));
@@ -11572,7 +11625,10 @@ function startServer(featuresDir, port, webviewDir) {
11572
11625
  if (params) {
11573
11626
  try {
11574
11627
  const name = decodeURIComponent(params.name);
11575
- sdk.deleteLabel(name);
11628
+ await sdk.deleteLabel(name);
11629
+ await loadFeatures();
11630
+ broadcast({ type: "labelsUpdated", labels: sdk.getLabels() });
11631
+ broadcast(buildInitMessage());
11576
11632
  return jsonOk(res, { success: true });
11577
11633
  } catch (err) {
11578
11634
  return jsonError(res, 400, String(err));
@@ -11885,6 +11941,9 @@ var KanbanPanel = class _KanbanPanel {
11885
11941
  case "removeColumn":
11886
11942
  await this._removeColumn(message.columnId);
11887
11943
  break;
11944
+ case "cleanupColumn":
11945
+ await this._cleanupColumn(message.columnId);
11946
+ break;
11888
11947
  case "transferCard": {
11889
11948
  const sdk = this._getSDK();
11890
11949
  if (!sdk || !this._currentBoardId)
@@ -11951,7 +12010,8 @@ var KanbanPanel = class _KanbanPanel {
11951
12010
  const sdk = this._getSDK();
11952
12011
  if (!sdk)
11953
12012
  break;
11954
- sdk.deleteLabel(message.name);
12013
+ await sdk.deleteLabel(message.name);
12014
+ await this._loadFeatures();
11955
12015
  this._sendFeaturesToWebview();
11956
12016
  this._panel.webview.postMessage({ type: "labelsUpdated", labels: sdk.getLabels() });
11957
12017
  break;
@@ -12240,10 +12300,7 @@ var KanbanPanel = class _KanbanPanel {
12240
12300
  if (!sdk)
12241
12301
  return;
12242
12302
  try {
12243
- const deletedCards = this._features.filter((f) => f.status === "deleted");
12244
- for (const card of deletedCards) {
12245
- await sdk.permanentlyDeleteCard(card.id, this._currentBoardId);
12246
- }
12303
+ await sdk.purgeDeletedCards(this._currentBoardId);
12247
12304
  this._features = this._features.filter((f) => f.status !== "deleted");
12248
12305
  this._sendFeaturesToWebview();
12249
12306
  } catch (err) {
@@ -12582,6 +12639,23 @@ var KanbanPanel = class _KanbanPanel {
12582
12639
  sdk.removeColumn(columnId, this._currentBoardId);
12583
12640
  this._sendFeaturesToWebview();
12584
12641
  }
12642
+ async _cleanupColumn(columnId) {
12643
+ const sdk = this._getSDK();
12644
+ if (!sdk)
12645
+ return;
12646
+ this._migrating = true;
12647
+ try {
12648
+ await sdk.cleanupColumn(columnId, this._currentBoardId);
12649
+ this._features = this._features.map(
12650
+ (f) => f.status === columnId ? { ...f, status: "deleted" } : f
12651
+ );
12652
+ this._sendFeaturesToWebview();
12653
+ } catch (err) {
12654
+ vscode.window.showErrorMessage(`Failed to cleanup list: ${err}`);
12655
+ } finally {
12656
+ this._migrating = false;
12657
+ }
12658
+ }
12585
12659
  };
12586
12660
 
12587
12661
  // src/extension/SidebarViewProvider.ts