kanbango 2.1.0 → 2.4.0

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/index.html CHANGED
@@ -71,6 +71,15 @@ header h1 { font-size: 14px; font-weight: 700; letter-spacing: -0.2px; }
71
71
  flex-shrink: 0;
72
72
  }
73
73
  .new-epic-strip.open { display: flex; }
74
+ .ne-field { display: flex; flex-direction: column; gap: 3px; }
75
+ .ne-label {
76
+ font-size: 10px;
77
+ font-weight: 700;
78
+ text-transform: uppercase;
79
+ letter-spacing: 0.6px;
80
+ color: var(--muted);
81
+ }
82
+ .ne-buttons { display: flex; gap: 8px; align-items: flex-end; }
74
83
  .ne-input {
75
84
  border: 1px solid var(--border);
76
85
  border-radius: var(--radius);
@@ -378,6 +387,20 @@ header h1 { font-size: 14px; font-weight: 700; letter-spacing: -0.2px; }
378
387
  line-height: 1.45;
379
388
  color: var(--text);
380
389
  }
390
+ .detail-section.out-of-scope {
391
+ border-left: 3px solid #e8b4b4;
392
+ padding-left: 8px;
393
+ }
394
+ .detail-section.out-of-scope .detail-label {
395
+ color: #a86a6a;
396
+ }
397
+ .detail-section.out-of-scope .detail-list {
398
+ color: #7a5a5a;
399
+ }
400
+ .edit-block.out-of-scope {
401
+ border-color: #e8c4c4;
402
+ background: #fffbfb;
403
+ }
381
404
  .edit-meta {
382
405
  display: flex;
383
406
  flex-direction: column;
@@ -396,6 +419,13 @@ header h1 { font-size: 14px; font-weight: 700; letter-spacing: -0.2px; }
396
419
  flex-direction: column;
397
420
  gap: 6px;
398
421
  }
422
+ .lane-add-label {
423
+ font-size: 10px;
424
+ font-weight: 700;
425
+ text-transform: uppercase;
426
+ letter-spacing: 0.6px;
427
+ color: var(--muted);
428
+ }
399
429
  .lane-add-title {
400
430
  width: 100%;
401
431
  border: 1px solid var(--planned);
@@ -457,20 +487,31 @@ header h1 { font-size: 14px; font-weight: 700; letter-spacing: -0.2px; }
457
487
  <h1>Donna Kanban</h1>
458
488
  <span class="hstats" id="hstats">—</span>
459
489
  <span class="hspacer"></span>
460
- <button class="btn-new" id="btn-new" onclick="toggleNewForm()">+ Nowy task</button>
490
+ <button class="btn-new" id="btn-new" onclick="toggleNewForm()">+ New task</button>
461
491
  </header>
462
492
 
463
493
  <div class="new-epic-strip" id="new-strip">
464
- <input class="ne-input ne-title" id="ne-title" placeholder="Nazwa taska…" autocomplete="off">
465
- <select class="ne-input ne-col" id="ne-col">
466
- <option value="icebox">🧊 Icebox</option>
467
- <option value="planned" selected>📦 Planned</option>
468
- <option value="active">🔥 Active</option>
469
- <option value="done">✅ Done</option>
470
- </select>
471
- <input class="ne-input ne-group" id="ne-group" placeholder="Epik / Faza (opcjonalnie)…" autocomplete="off">
472
- <button class="btn-sm btn-confirm" onclick="submitNew()">Dodaj</button>
473
- <button class="btn-sm btn-cancel" onclick="toggleNewForm()">Anuluj</button>
494
+ <div class="ne-field">
495
+ <div class="ne-label">Title</div>
496
+ <input class="ne-input ne-title" id="ne-title" placeholder="Task name…" autocomplete="off">
497
+ </div>
498
+ <div class="ne-field">
499
+ <div class="ne-label">Column</div>
500
+ <select class="ne-input ne-col" id="ne-col">
501
+ <option value="icebox">🧊 Icebox</option>
502
+ <option value="planned" selected>📦 Planned</option>
503
+ <option value="active">🔥 Active</option>
504
+ <option value="done">✅ Done</option>
505
+ </select>
506
+ </div>
507
+ <div class="ne-field">
508
+ <div class="ne-label">Epic</div>
509
+ <input class="ne-input ne-group" id="ne-group" placeholder="Epic group (optional)…" autocomplete="off">
510
+ </div>
511
+ <div class="ne-buttons">
512
+ <button class="btn-sm btn-confirm" onclick="submitNew()">Add</button>
513
+ <button class="btn-sm btn-cancel" onclick="toggleNewForm()">Cancel</button>
514
+ </div>
474
515
  </div>
475
516
 
476
517
  <div id="board-wrap">
@@ -511,7 +552,7 @@ function render() {
511
552
  // Header stats
512
553
  const total = allEpics.length;
513
554
  const done = allEpics.filter(e => e.column === "done").length;
514
- document.getElementById("hstats").textContent = `${done}/${total} ukończone`;
555
+ document.getElementById("hstats").textContent = `${done}/${total} done`;
515
556
 
516
557
  // Col headers
517
558
  const hdr = document.getElementById("col-headers");
@@ -551,15 +592,15 @@ function renderSwimlane({ name, epics }) {
551
592
 
552
593
  // Header
553
594
  const hdr = el("div", `sl-header${isCollapsed ? " collapsed" : ""}`);
554
- const totalTasks = epics.reduce((s, e) => s + e.tasks.length, 0);
555
- const doneTasks = epics.reduce((s, e) => s + e.tasks.filter(t => t.done).length, 0);
595
+ const totalTasks = epics.reduce((s, e) => s + e.subtasks.length, 0);
596
+ const doneTasks = epics.reduce((s, e) => s + e.subtasks.filter(t => t.done).length, 0);
556
597
  const pct = totalTasks > 0 ? Math.round(doneTasks / totalTasks * 100) : null;
557
598
 
558
599
  hdr.innerHTML = `
559
600
  <span class="sl-arrow">▼</span>
560
- <span class="sl-name">${escHtml(name === "—" ? "Pozostałe (bez epiku)" : name)}</span>
601
+ <span class="sl-name">${escHtml(name === "—" ? "Uncategorized (no epic)" : name)}</span>
561
602
  <span class="sl-cnt">${epics.length}</span>
562
- ${pct !== null ? `<span class="sl-progress" style="margin-left:auto">${pct}% tasków</span>` : ""}
603
+ ${pct !== null ? `<span class="sl-progress" style="margin-left:auto">${pct}% tasks done</span>` : ""}
563
604
  `;
564
605
  hdr.onclick = () => {
565
606
  if (collapsed.has(name)) collapsed.delete(name);
@@ -577,7 +618,7 @@ function renderSwimlane({ name, epics }) {
577
618
  if (col.id !== "done") {
578
619
  const addBtn = document.createElement("button");
579
620
  addBtn.className = "lane-add";
580
- addBtn.textContent = "+ Dodaj tutaj";
621
+ addBtn.textContent = "+ Add here";
581
622
  addBtn.onclick = e => { e.stopPropagation(); showLaneAdd(lane, col.id, name, addBtn); };
582
623
  lane.appendChild(addBtn);
583
624
  }
@@ -613,8 +654,8 @@ function renderCard(epic, colId) {
613
654
  const card = el("div", `card col-${colId}${isExpanded ? " open" : ""}${isEditing ? " editing" : ""}`);
614
655
  card.dataset.id = epic.id;
615
656
 
616
- const totalT = epic.tasks.length;
617
- const doneT = epic.tasks.filter(t => t.done).length;
657
+ const totalT = epic.subtasks.length;
658
+ const doneT = epic.subtasks.filter(t => t.done).length;
618
659
  const pct = totalT > 0 ? Math.round(doneT / totalT * 100) : 0;
619
660
  const shortId = (epic.id.match(/^PI-\d+/) || [epic.id])[0];
620
661
  const displayTitle = epic.title.replace(/^[\w.-]+:\s*/, "");
@@ -646,7 +687,7 @@ function renderCard(epic, colId) {
646
687
  // For icebox show just a count badge
647
688
  const badge = el("span", "prog-txt");
648
689
  badge.style.cssText = "display:inline-block;margin-top:5px;background:rgba(100,116,139,0.12);border-radius:4px;padding:1px 6px;";
649
- badge.textContent = `${totalT} subtasków`;
690
+ badge.textContent = `${totalT} subtasks`;
650
691
  card.appendChild(badge);
651
692
  }
652
693
 
@@ -673,57 +714,57 @@ function renderCard(epic, colId) {
673
714
  actions.appendChild(rb);
674
715
  }
675
716
 
676
- // Expand subtasks button
677
- if (totalT > 0) {
678
- const expBtn = document.createElement("button");
679
- expBtn.className = "cbtn";
680
- expBtn.textContent = isExpanded ? "▲" : "▼ Subtaski";
681
- expBtn.onclick = e => { e.stopPropagation(); toggleExpand(epic.id); };
682
- actions.appendChild(expBtn);
683
- }
717
+ // Expand details button
718
+ const expBtn = document.createElement("button");
719
+ expBtn.className = "cbtn";
720
+ expBtn.textContent = isExpanded ? "" : "▼ Details";
721
+ expBtn.onclick = e => { e.stopPropagation(); toggleExpand(epic.id); };
722
+ actions.appendChild(expBtn);
684
723
 
685
724
  // Edit button
686
725
  const editBtn = document.createElement("button");
687
726
  editBtn.className = "cbtn edit-btn";
688
727
  editBtn.textContent = "✎";
689
- editBtn.title = "Edytuj";
728
+ editBtn.title = "Edit";
690
729
  editBtn.onclick = e => { e.stopPropagation(); startEdit(epic.id); };
691
730
  actions.appendChild(editBtn);
692
731
 
693
732
  card.appendChild(actions);
694
733
 
695
- // Subtask list (expanded, view mode)
696
- if (isExpanded && totalT > 0) {
697
- const lbl = el("div", "subtask-label");
698
- lbl.textContent = "Subtaski";
699
- card.appendChild(lbl);
700
-
734
+ // Details + subtasks (expanded, view mode)
735
+ if (isExpanded) {
701
736
  appendDetailSections(card, epic);
702
737
 
703
- const list = el("div", "tasks-list");
704
- epic.tasks.forEach((task, idx) => {
705
- const row = el("div", `task-row${task.done ? " done" : ""}`);
706
- const cbId = `v-${epic.id}-${idx}`;
707
- row.innerHTML = `
708
- <input type="checkbox" id="${cbId}" ${task.done ? "checked" : ""}>
709
- <div class="task-copy">
710
- <label for="${cbId}">${escHtml(task.text)}</label>
711
- ${task.description ? `<div class="task-desc">${escHtml(task.description)}</div>` : ""}
712
- </div>
713
- `;
714
- row.querySelector("input").onchange = async function() {
715
- this.disabled = true;
716
- await fetch(`/api/epics/${enc(epic.id)}/tasks/${idx}`, { method: "PATCH" });
717
- await load();
718
- };
719
- list.appendChild(row);
720
- });
721
- card.appendChild(list);
738
+ if (totalT > 0) {
739
+ const lbl = el("div", "subtask-label");
740
+ lbl.textContent = "Subtasks";
741
+ card.appendChild(lbl);
742
+
743
+ const list = el("div", "tasks-list");
744
+ epic.subtasks.forEach((task, idx) => {
745
+ const row = el("div", `task-row${task.done ? " done" : ""}`);
746
+ const cbId = `v-${epic.id}-${idx}`;
747
+ row.innerHTML = `
748
+ <input type="checkbox" id="${cbId}" ${task.done ? "checked" : ""}>
749
+ <div class="task-copy">
750
+ <label for="${cbId}">${escHtml(task.text)}</label>
751
+ ${task.description ? `<div class="task-desc">${escHtml(task.description)}</div>` : ""}
752
+ </div>
753
+ `;
754
+ row.querySelector("input").onchange = async function() {
755
+ this.disabled = true;
756
+ await fetch(`/api/epics/${enc(epic.id)}/tasks/${idx}`, { method: "PATCH" });
757
+ await load();
758
+ };
759
+ list.appendChild(row);
760
+ });
761
+ card.appendChild(list);
762
+ }
722
763
  }
723
764
 
724
765
  card.onclick = e => {
725
766
  if (e.target.tagName === "BUTTON" || e.target.tagName === "INPUT" || e.target.tagName === "LABEL") return;
726
- if (totalT > 0) toggleExpand(epic.id);
767
+ toggleExpand(epic.id);
727
768
  };
728
769
  }
729
770
 
@@ -736,9 +777,12 @@ function renderEditMode(card, epic, colId, shortId) {
736
777
  title: displayTitle,
737
778
  description: epic.description || "",
738
779
  specs: epic.specs || "",
780
+ in_scope: (epic.in_scope || []).join("\n"),
781
+ out_of_scope: (epic.out_of_scope || []).join("\n"),
739
782
  acceptance_criteria: (epic.acceptance_criteria || []).join("\n"),
783
+ test_cases: (epic.test_cases || []).join("\n"),
740
784
  notes: epic.notes || "",
741
- tasks: epic.tasks.map(t => ({ ...t }))
785
+ tasks: epic.subtasks.map(t => ({ ...t }))
742
786
  };
743
787
  dirtyEdits[epic.id] = dirty;
744
788
 
@@ -756,28 +800,40 @@ function renderEditMode(card, epic, colId, shortId) {
756
800
  card.appendChild(titleInput);
757
801
 
758
802
  const meta = el("div", "edit-meta");
759
- meta.appendChild(renderEditBlock("Opis", dirty.description, value => {
803
+ meta.appendChild(renderEditBlock("Description", dirty.description, value => {
760
804
  dirty.description = value;
761
805
  dirtyEdits[epic.id] = dirty;
762
- }, "Kontekst i plan implementacji..."));
806
+ }, "Context and implementation plan..."));
763
807
  meta.appendChild(renderEditBlock("Specs", dirty.specs, value => {
764
808
  dirty.specs = value;
765
809
  dirtyEdits[epic.id] = dirty;
766
- }, "API, ograniczenia, edge case'y..."));
810
+ }, "APIs, constraints, edge cases..."));
811
+ meta.appendChild(renderEditBlock("In Scope", dirty.in_scope, value => {
812
+ dirty.in_scope = value;
813
+ dirtyEdits[epic.id] = dirty;
814
+ }, "What this task covers (one item per line)"));
815
+ meta.appendChild(renderEditBlock("Out of Scope", dirty.out_of_scope, value => {
816
+ dirty.out_of_scope = value;
817
+ dirtyEdits[epic.id] = dirty;
818
+ }, "What is explicitly not included (one item per line)", "out-of-scope"));
767
819
  meta.appendChild(renderEditBlock("Acceptance Criteria", dirty.acceptance_criteria, value => {
768
820
  dirty.acceptance_criteria = value;
769
821
  dirtyEdits[epic.id] = dirty;
770
- }, "Jeden warunek na linie"));
822
+ }, "One condition per line"));
823
+ meta.appendChild(renderEditBlock("Test Cases", dirty.test_cases, value => {
824
+ dirty.test_cases = value;
825
+ dirtyEdits[epic.id] = dirty;
826
+ }, "One scenario per line"));
771
827
  meta.appendChild(renderEditBlock("Notes", dirty.notes, value => {
772
828
  dirty.notes = value;
773
829
  dirtyEdits[epic.id] = dirty;
774
- }, "Opcjonalne notatki"));
830
+ }, "Optional notes"));
775
831
  card.appendChild(meta);
776
832
 
777
833
  // Subtasks section
778
834
  const editTasks = el("div", "edit-tasks");
779
835
  const subtaskLbl = el("div", "subtask-label");
780
- subtaskLbl.textContent = "Subtaski";
836
+ subtaskLbl.textContent = "Subtasks";
781
837
  editTasks.appendChild(subtaskLbl);
782
838
 
783
839
  const taskRows = el("div");
@@ -808,14 +864,14 @@ function renderEditMode(card, epic, colId, shortId) {
808
864
 
809
865
  const desc = document.createElement("textarea");
810
866
  desc.className = "edit-task-desc";
811
- desc.placeholder = "Opis subtaska...";
867
+ desc.placeholder = "Subtask description...";
812
868
  desc.value = task.description || "";
813
869
  desc.oninput = () => { dirty.tasks[idx].description = desc.value; dirtyEdits[epic.id] = dirty; };
814
870
 
815
871
  const delBtn = document.createElement("button");
816
872
  delBtn.className = "del-task";
817
873
  delBtn.textContent = "×";
818
- delBtn.title = "Usuń subtask";
874
+ delBtn.title = "Remove subtask";
819
875
  delBtn.onclick = () => { dirty.tasks.splice(idx, 1); dirtyEdits[epic.id] = dirty; refreshRows(); };
820
876
 
821
877
  fields.appendChild(inp);
@@ -830,7 +886,7 @@ function renderEditMode(card, epic, colId, shortId) {
830
886
  // Add subtask input
831
887
  const addSubtaskInput = document.createElement("input");
832
888
  addSubtaskInput.className = "add-task-inp";
833
- addSubtaskInput.placeholder = "+ Nowy subtask… (Enter)";
889
+ addSubtaskInput.placeholder = "+ New subtask… (Enter)";
834
890
  addSubtaskInput.onkeydown = e => {
835
891
  if (e.key === "Enter" && addSubtaskInput.value.trim()) {
836
892
  dirty.tasks.push({ done: false, text: addSubtaskInput.value.trim(), description: "" });
@@ -846,12 +902,12 @@ function renderEditMode(card, epic, colId, shortId) {
846
902
  const footer = el("div", "edit-footer");
847
903
  const saveBtn = document.createElement("button");
848
904
  saveBtn.className = "btn-sm btn-confirm";
849
- saveBtn.textContent = "✓ Zapisz";
905
+ saveBtn.textContent = "✓ Save";
850
906
  saveBtn.onclick = async e => { e.stopPropagation(); await saveEdit(epic.id); };
851
907
 
852
- const cancelBtn = document.createElement("button");
908
+ const cancelBtn = document.createElement("button");
853
909
  cancelBtn.className = "btn-sm btn-cancel";
854
- cancelBtn.textContent = "Anuluj";
910
+ cancelBtn.textContent = "Cancel";
855
911
  cancelBtn.onclick = e => {
856
912
  e.stopPropagation(); delete dirtyEdits[epic.id]; editingId = null; render();
857
913
  };
@@ -881,9 +937,12 @@ async function saveEdit(epicId) {
881
937
  if (dirty.title !== origTitle) body.title = dirty.title;
882
938
  body.description = dirty.description;
883
939
  body.specs = dirty.specs;
940
+ body.in_scope = splitLines(dirty.in_scope);
941
+ body.out_of_scope = splitLines(dirty.out_of_scope);
884
942
  body.acceptance_criteria = splitLines(dirty.acceptance_criteria);
943
+ body.test_cases = splitLines(dirty.test_cases);
885
944
  body.notes = dirty.notes;
886
- body.tasks = dirty.tasks.filter(t => t.text.trim()).map((task, idx) => ({
945
+ body.subtasks = dirty.tasks.filter(t => t.text.trim()).map((task, idx) => ({
887
946
  id: task.id || `st-${idx + 1}`,
888
947
  text: task.text.trim(),
889
948
  done: Boolean(task.done),
@@ -897,8 +956,8 @@ async function saveEdit(epicId) {
897
956
  body: JSON.stringify(body),
898
957
  });
899
958
  if (!res.ok) throw new Error();
900
- toast("Zapisano ✓");
901
- } catch { toast("Błąd zapisu!", true); }
959
+ toast("Saved ✓");
960
+ } catch { toast("Save error!", true); }
902
961
  delete dirtyEdits[epicId];
903
962
  editingId = null;
904
963
  await load();
@@ -917,22 +976,31 @@ function showLaneAdd(lane, colId, groupName, addBtn) {
917
976
  if (existing) { existing.remove(); return; }
918
977
 
919
978
  const wrap = el("div", "lane-addinp");
979
+
980
+ const titleLabel = el("div", "lane-add-label");
981
+ titleLabel.textContent = "Task title";
982
+ wrap.appendChild(titleLabel);
983
+
920
984
  const inp = document.createElement("input");
921
985
  inp.className = "lane-add-title";
922
- inp.placeholder = "Nazwa nowego taska…";
986
+ inp.placeholder = "New task name…";
923
987
  inp.autocomplete = "off";
924
988
 
989
+ const descLabel = el("div", "lane-add-label");
990
+ descLabel.textContent = "Description";
991
+ wrap.appendChild(descLabel);
992
+
925
993
  const description = document.createElement("textarea");
926
994
  description.className = "lane-add-textarea";
927
- description.placeholder = "Opis taska (opcjonalnie)...";
995
+ description.placeholder = "Task description (optional)...";
928
996
 
929
997
  const actions = el("div", "lane-add-actions");
930
998
  const confirmBtn = document.createElement("button");
931
999
  confirmBtn.className = "btn-sm btn-confirm";
932
- confirmBtn.textContent = "Dodaj";
1000
+ confirmBtn.textContent = "Add";
933
1001
  const cancelBtn = document.createElement("button");
934
1002
  cancelBtn.className = "btn-sm btn-cancel";
935
- cancelBtn.textContent = "Anuluj";
1003
+ cancelBtn.textContent = "Cancel";
936
1004
 
937
1005
  const doAdd = async () => {
938
1006
  const title = inp.value.trim();
@@ -950,11 +1018,11 @@ function showLaneAdd(lane, colId, groupName, addBtn) {
950
1018
  }),
951
1019
  });
952
1020
  if (res.ok) {
953
- toast("Dodano ✓");
1021
+ toast("Added ✓");
954
1022
  wrap.remove();
955
1023
  await load();
956
1024
  } else {
957
- toast("Błąd!", true);
1025
+ toast("Error!", true);
958
1026
  inp.disabled = false;
959
1027
  description.disabled = false;
960
1028
  inp.focus();
@@ -996,13 +1064,13 @@ async function submitNew() {
996
1064
  body: JSON.stringify({ title, column: col, epic_group: group || "—" }),
997
1065
  });
998
1066
  if (res.ok) {
999
- toast("Dodano task ✓");
1067
+ toast("Task added ✓");
1000
1068
  document.getElementById("ne-title").value = "";
1001
1069
  document.getElementById("ne-group").value = "";
1002
1070
  toggleNewForm();
1003
1071
  await load();
1004
1072
  } else {
1005
- toast("Błąd!", true);
1073
+ toast("Error!", true);
1006
1074
  }
1007
1075
  }
1008
1076
 
@@ -1024,14 +1092,23 @@ function splitLines(value) {
1024
1092
  }
1025
1093
  function appendDetailSections(card, epic) {
1026
1094
  if (epic.description) {
1027
- card.appendChild(renderDetailSection("Opis", epic.description));
1095
+ card.appendChild(renderDetailSection("Description", epic.description));
1028
1096
  }
1029
1097
  if (epic.specs) {
1030
1098
  card.appendChild(renderDetailSection("Specs", epic.specs));
1031
1099
  }
1100
+ if (epic.in_scope && epic.in_scope.length > 0) {
1101
+ card.appendChild(renderDetailListSection("In Scope", epic.in_scope));
1102
+ }
1103
+ if (epic.out_of_scope && epic.out_of_scope.length > 0) {
1104
+ card.appendChild(renderDetailListSection("Out of Scope", epic.out_of_scope, "out-of-scope"));
1105
+ }
1032
1106
  if (epic.acceptance_criteria && epic.acceptance_criteria.length > 0) {
1033
1107
  card.appendChild(renderDetailListSection("Acceptance Criteria", epic.acceptance_criteria));
1034
1108
  }
1109
+ if (epic.test_cases && epic.test_cases.length > 0) {
1110
+ card.appendChild(renderDetailListSection("Test Cases", epic.test_cases));
1111
+ }
1035
1112
  if (epic.notes) {
1036
1113
  card.appendChild(renderDetailSection("Notes", epic.notes));
1037
1114
  }
@@ -1046,8 +1123,8 @@ function renderDetailSection(label, text) {
1046
1123
  wrap.appendChild(textEl);
1047
1124
  return wrap;
1048
1125
  }
1049
- function renderDetailListSection(label, items) {
1050
- const wrap = el("div", "detail-section");
1126
+ function renderDetailListSection(label, items, extraClass) {
1127
+ const wrap = el("div", extraClass ? `detail-section ${extraClass}` : "detail-section");
1051
1128
  const labelEl = el("div", "detail-label");
1052
1129
  labelEl.textContent = label;
1053
1130
  const list = el("ul", "detail-list");
@@ -1060,12 +1137,12 @@ function renderDetailListSection(label, items) {
1060
1137
  wrap.appendChild(list);
1061
1138
  return wrap;
1062
1139
  }
1063
- function renderEditBlock(label, value, onInput, placeholder) {
1064
- const wrap = el("div");
1140
+ function renderEditBlock(label, value, onInput, placeholder, extraClass) {
1141
+ const wrap = el("div", extraClass ? `detail-section ${extraClass}` : null);
1065
1142
  const labelEl = el("div", "detail-label");
1066
1143
  labelEl.textContent = label;
1067
1144
  const textarea = document.createElement("textarea");
1068
- textarea.className = "edit-block";
1145
+ textarea.className = extraClass ? `edit-block ${extraClass}` : "edit-block";
1069
1146
  textarea.value = value;
1070
1147
  textarea.placeholder = placeholder;
1071
1148
  textarea.oninput = () => onInput(textarea.value);
package/index.js CHANGED
@@ -9,7 +9,9 @@
9
9
  */
10
10
 
11
11
  const kanban = require('./kanban.js');
12
+ const plan = require('./plan.js');
12
13
 
13
14
  module.exports = {
14
15
  kanban,
16
+ plan,
15
17
  };
package/kan2.md ADDED
@@ -0,0 +1,76 @@
1
+ # Plan kan2: Kanbango Plan Workflow
2
+
3
+ ## Cel
4
+
5
+ Rozszerzyć Kanbango o niezależny workflow planów, który może być używany przez
6
+ OpenCode, inne agenty i CLI. Kanbango pozostaje osobnym repozytorium.
7
+
8
+ ## Zasada architektoniczna
9
+
10
+ Kanbango nie zna API OpenCode ani hooków pluginu. Udostępnia stabilne operacje
11
+ na planie i evidence. Plugin OpenCode jest tylko klientem oraz strażnikiem sesji.
12
+
13
+ ## Funkcjonalność
14
+
15
+ Dodać bibliotekę domenową oraz odpowiednie operacje CLI/MCP:
16
+
17
+ - utworzenie taska z zaakceptowanym planem,
18
+ - zapis kroków planu jako subtasks,
19
+ - oznaczenie bieżącego kroku jako zakończonego,
20
+ - zapis evidence: diff, komenda testowa, stdout, stderr i exit code,
21
+ - zakończenie workflow i przeniesienie taska do `done`,
22
+ - odczyt pełnego statusu planu w stabilnym formacie JSON.
23
+
24
+ ## Domyślne kroki
25
+
26
+ Każdy zaakceptowany plan dostaje kolejno:
27
+
28
+ 1. `Write tests`
29
+ 2. `Run tests and confirm red`
30
+ 3. Kroki implementacyjne dostarczone przez advisora
31
+ 4. `Run tests and confirm green`
32
+
33
+ ## Test runner
34
+
35
+ Autodetekcja musi działać per projekt, bez założenia Node:
36
+
37
+ - Rust: `cargo test` przy `Cargo.toml`,
38
+ - Go: `go test ./...` przy `go.mod`,
39
+ - Python: `python -m pytest` przy `pyproject.toml` lub `pytest.ini`,
40
+ - JavaScript/TypeScript: `npm test`, `pnpm test`, `yarn test` albo `bun test`
41
+ zgodnie z lockfilem i skryptem `test` w `package.json`,
42
+ - jawny override przez `OPENCODE_TEST_COMMAND`.
43
+
44
+ Autodetekcja ma zwracać także powód wyboru komendy i czytelny błąd, gdy nie
45
+ znaleziono testów.
46
+
47
+ ## Interfejs
48
+
49
+ Preferowany jest wspólny moduł JS oraz JSON CLI/MCP, zamiast parsowania tekstu:
50
+
51
+ - `plan create --json <payload>`,
52
+ - `plan advance --json <payload>`,
53
+ - `plan evidence --json <payload>`,
54
+ - `plan done --json <payload>`,
55
+ - odpowiadające operacje w `kanban_manage` lub osobnym narzędziu MCP.
56
+
57
+ CLI musi zwracać stabilny JSON z `ok`, `task_id`, `subtasks` i `error`.
58
+
59
+ ## Testy TDD
60
+
61
+ - autodetekcja runnera dla Rust, Go, Python i Node,
62
+ - utworzenie planu z czterema grupami kroków,
63
+ - aktualizacja kroku bez kasowania pozostałych subtasks,
64
+ - zapis evidence bez silent fail,
65
+ - zakończenie planu i przejście do `done`,
66
+ - błędna komenda testowa i brak testów,
67
+ - wywołanie przez CLI i MCP z tym samym rezultatem.
68
+
69
+ ## Kolejność implementacji
70
+
71
+ 1. Model planu i runner detection.
72
+ 2. Operacje biblioteki Kanbango.
73
+ 3. CLI i JSON output.
74
+ 4. MCP adapter.
75
+ 5. Integracja pluginu OpenCode z tym kontraktem.
76
+ 6. Test end-to-end w przykładowym projekcie.