kanbanqube 1.0.9 → 1.0.10

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/README.md CHANGED
@@ -23,7 +23,7 @@ The app provides lanes, cards, labels, checklists, comments, card covers, file a
23
23
  - [Vaults](#vaults)
24
24
  - [Board Workflow](#board-workflow)
25
25
  - [Git Sync](#git-sync)
26
- - [Import](#import)
26
+ - [Trello Import](#trello-import)
27
27
  - [Demo Board](#demo-board)
28
28
  - [Identity](#identity)
29
29
  - [Attachments And Covers](#attachments-and-covers)
@@ -125,7 +125,7 @@ vault/
125
125
  uploads/
126
126
  ```
127
127
 
128
- If `board/` does not exist, KanbanQube creates it automatically. If an older `board.json` exists, KanbanQube imports it into the split `board/` layout the first time it starts. The original `board.json` is left in place but is no longer the active storage file. If `uploads/` does not exist, it is created when the first file is uploaded.
128
+ If `board/` does not exist, KanbanQube creates it automatically. If `uploads/` does not exist, it is created when the first file is uploaded.
129
129
 
130
130
  Uploaded filenames are made unique while preserving the original name in the UI. For example:
131
131
 
@@ -180,9 +180,9 @@ git commit -m "Initialize KanbanQube board"
180
180
  git push -u origin main
181
181
  ```
182
182
 
183
- ## Import
183
+ ## Trello Import
184
184
 
185
- KanbanQube can import a board export JSON from Settings. Import is only enabled when the current board has no cards. This keeps accidental replacement of an active board from happening inside the app.
185
+ KanbanQube can import a Trello board export JSON from Settings. Import is only enabled when the current board has no cards. This keeps accidental replacement of an active board from happening inside the app.
186
186
 
187
187
  Imported data is normalized and written into the split `board/` vault layout.
188
188
 
@@ -278,10 +278,4 @@ brew install mathiasconradt/kanbanqube/kanbanqube
278
278
 
279
279
  ## Star History
280
280
 
281
- <a href="https://www.star-history.com/?repos=mathiasconradt%2Fkanbanqube&type=timeline&logscale=&legend=top-left">
282
- <picture>
283
- <source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/chart?repos=mathiasconradt/kanbanqube&type=timeline&theme=dark&logscale&legend=top-left" />
284
- <source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/chart?repos=mathiasconradt/kanbanqube&type=timeline&logscale&legend=top-left" />
285
- <img alt="Star History Chart" src="https://api.star-history.com/chart?repos=mathiasconradt/kanbanqube&type=timeline&logscale&legend=top-left" />
286
- </picture>
287
- </a>
281
+ [![Star History Chart](https://api.star-history.com/chart?repos=mathiasconradt/kanbanqube&type=timeline&logscale&legend=top-left&nocache)](https://www.star-history.com/?repos=mathiasconradt%2Fkanbanqube&type=timeline&logscale=&legend=top-left)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kanbanqube",
3
- "version": "1.0.9",
3
+ "version": "1.0.10",
4
4
  "description": "Local-first Kanban board backed by normal files",
5
5
  "license": "Apache-2.0",
6
6
  "author": "Mathias Conradt",
package/public/app.js CHANGED
@@ -75,6 +75,7 @@ const syncLogCloseButton = document.getElementById("syncLogCloseButton");
75
75
  const archiveDialog = document.getElementById("archiveDialog");
76
76
  const archiveList = document.getElementById("archiveList");
77
77
  const closeArchiveButton = document.getElementById("closeArchiveButton");
78
+ const deleteAllArchivedButton = document.getElementById("deleteAllArchivedButton");
78
79
 
79
80
  const cardDialog = document.getElementById("cardDialog");
80
81
  const cardTitleInput = document.getElementById("cardTitleInput");
@@ -247,6 +248,7 @@ function wireEvents() {
247
248
  boardTitle.addEventListener("click", startBoardTitleEdit);
248
249
  closeSettingsButton.addEventListener("click", () => settingsDialog.close());
249
250
  closeArchiveButton.addEventListener("click", () => archiveDialog.close());
251
+ deleteAllArchivedButton.addEventListener("click", deleteAllArchivedCards);
250
252
  saveSettingsButton.addEventListener("click", saveSettings);
251
253
  importBoardButton.addEventListener("click", () => importBoardInput.click());
252
254
  importBoardInput.addEventListener("change", () => {
@@ -620,6 +622,8 @@ function renderCardDialog() {
620
622
  function renderArchiveDialog() {
621
623
  archiveList.textContent = "";
622
624
  const cards = archivedCards();
625
+ deleteAllArchivedButton.disabled = cards.length === 0;
626
+ deleteAllArchivedButton.hidden = cards.length === 0;
623
627
 
624
628
  if (cards.length === 0) {
625
629
  const empty = document.createElement("div");
@@ -689,11 +693,13 @@ function renderLabelsEditor(card) {
689
693
 
690
694
  const assignedLabels = labelsForCard(card);
691
695
  if (assignedLabels.length === 0) {
692
- const empty = document.createElement("div");
693
- empty.className = "empty-state label-summary-trigger";
694
- empty.textContent = "No labels assigned.";
695
- empty.addEventListener("click", openLabelEditor);
696
- cardLabels.append(empty);
696
+ if (!state.labelEditorOpen) {
697
+ const empty = document.createElement("div");
698
+ empty.className = "empty-state label-summary-trigger";
699
+ empty.textContent = "No labels assigned.";
700
+ empty.addEventListener("click", openLabelEditor);
701
+ cardLabels.append(empty);
702
+ }
697
703
  } else {
698
704
  for (const label of assignedLabels) {
699
705
  const pill = document.createElement("button");
@@ -724,6 +730,7 @@ function renderLabelsEditor(card) {
724
730
  searchInput.type = "search";
725
731
  searchInput.placeholder = "Search labels…";
726
732
  searchInput.value = state.labelSearchTerm;
733
+ searchInput.addEventListener("keydown", stopLabelEditorShortcut);
727
734
  searchInput.addEventListener("input", () => {
728
735
  state.labelSearchTerm = searchInput.value;
729
736
  renderCardDialog();
@@ -773,11 +780,21 @@ function renderLabelsEditor(card) {
773
780
  nameInput.value = label.name || "";
774
781
  nameInput.placeholder = "Label name";
775
782
  nameInput.style.backgroundColor = colorForLabel(label.color);
783
+ let committedName = label.name || "";
784
+ nameInput.addEventListener("keydown", stopLabelEditorShortcut);
776
785
  nameInput.addEventListener("input", () => {
777
- label.name = nameInput.value.trim();
778
- queueSave("Label updated");
779
- renderCardDialog();
780
- renderBoard();
786
+ label.name = nameInput.value;
787
+ });
788
+ nameInput.addEventListener("blur", () => {
789
+ const trimmedName = nameInput.value.trim();
790
+ if (label.name !== trimmedName) {
791
+ label.name = trimmedName;
792
+ }
793
+ if (committedName !== label.name) {
794
+ committedName = label.name;
795
+ queueSave("Label updated");
796
+ renderBoard();
797
+ }
781
798
  });
782
799
 
783
800
  const colorSelect = document.createElement("select");
@@ -786,9 +803,10 @@ function renderLabelsEditor(card) {
786
803
  const option = document.createElement("option");
787
804
  option.value = color;
788
805
  option.textContent = color.replaceAll("_", " ");
789
- option.selected = color === label.color;
806
+ option.selected = color === label.color;
790
807
  colorSelect.append(option);
791
808
  }
809
+ colorSelect.addEventListener("keydown", stopLabelEditorShortcut);
792
810
  colorSelect.addEventListener("change", () => {
793
811
  label.color = colorSelect.value;
794
812
  queueSave("Label updated");
@@ -829,6 +847,14 @@ function renderLabelsEditor(card) {
829
847
  appendLabelEditorPanel(panel);
830
848
  }
831
849
 
850
+ function stopLabelEditorShortcut(event) {
851
+ event.stopPropagation();
852
+ if (event.key === "Enter") {
853
+ event.preventDefault();
854
+ event.currentTarget.blur();
855
+ }
856
+ }
857
+
832
858
  function appendLabelEditorPanel(panel) {
833
859
  const anchor = labelEditorContainer.closest(".sidebar-panel") || labelEditorContainer;
834
860
  const rect = anchor.getBoundingClientRect();
@@ -1461,6 +1487,27 @@ async function deleteArchivedCard(cardId) {
1461
1487
  render();
1462
1488
  }
1463
1489
 
1490
+ async function deleteAllArchivedCards() {
1491
+ const cards = archivedCards();
1492
+ if (cards.length === 0) return;
1493
+
1494
+ const confirmed = await openConfirmDialog({
1495
+ label: "Delete archive",
1496
+ title: "Delete all archived cards?",
1497
+ message: `Delete ${cards.length} archived card${cards.length === 1 ? "" : "s"} permanently? This cannot be undone.`,
1498
+ confirmLabel: "Delete all",
1499
+ danger: true
1500
+ });
1501
+ if (!confirmed) return;
1502
+
1503
+ for (const card of cards) {
1504
+ removeCardCompletely(card.id);
1505
+ }
1506
+ state.selectedCardId = null;
1507
+ queueSave("Archived cards deleted");
1508
+ render();
1509
+ }
1510
+
1464
1511
  async function deleteLane(listId) {
1465
1512
  const list = listById(listId);
1466
1513
  if (!list) return;
@@ -1623,11 +1670,7 @@ function toggleKeyboardLabel(card, labelIndex) {
1623
1670
  }
1624
1671
 
1625
1672
  function sortedBoardLabels() {
1626
- return [...(state.board?.labels || [])].sort((left, right) => {
1627
- const leftName = (left.name || left.color || "").toLowerCase();
1628
- const rightName = (right.name || right.color || "").toLowerCase();
1629
- return leftName.localeCompare(rightName);
1630
- });
1673
+ return [...(state.board?.labels || [])];
1631
1674
  }
1632
1675
 
1633
1676
  function setKeyboardCard(cardId, shouldRender = true) {
package/public/index.html CHANGED
@@ -215,7 +215,10 @@
215
215
  <p class="modal-label">Archive</p>
216
216
  <h2>Archived cards</h2>
217
217
  </div>
218
- <button id="closeArchiveButton" type="button" class="icon-button" aria-label="Close archive">✕</button>
218
+ <div class="archive-header-actions">
219
+ <button id="deleteAllArchivedButton" type="button" class="danger-button">Delete all</button>
220
+ <button id="closeArchiveButton" type="button" class="icon-button" aria-label="Close archive">✕</button>
221
+ </div>
219
222
  </header>
220
223
  <div id="archiveList" class="archive-list"></div>
221
224
  </form>
package/public/styles.css CHANGED
@@ -214,6 +214,7 @@ button {
214
214
  .topbar-actions,
215
215
  .board-meta,
216
216
  .lane-actions,
217
+ .archive-header-actions,
217
218
  .modal-footer,
218
219
  .panel-header,
219
220
  .settings-meta {
@@ -222,6 +223,14 @@ button {
222
223
  align-items: center;
223
224
  }
224
225
 
226
+ .archive-header-actions {
227
+ flex: 0 0 auto;
228
+ }
229
+
230
+ #promptDialog .modal-footer {
231
+ margin-top: 1rem;
232
+ }
233
+
225
234
  .primary-button,
226
235
  .ghost-button,
227
236
  .danger-button,