super-backlog 0.9.0 → 1.0.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.
@@ -4,6 +4,7 @@
4
4
  <meta charset="utf-8">
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1">
6
6
  <title>__PROJECT_NAME__ &middot; Project Dashboard</title>
7
+ <link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'%3E%3Ccircle cx='50' cy='50' r='42' fill='%235cc8ff'/%3E%3C/svg%3E">
7
8
  <style>
8
9
  :root {
9
10
  --bg:#0a0e16;
@@ -26,10 +27,25 @@
26
27
  --danger-bg:#331718;
27
28
  --mono:"Cascadia Code",Consolas,"Courier New",monospace;
28
29
  }
30
+ @font-face {
31
+ font-family: 'Inter';
32
+ src: local('Inter'), local('Inter-Regular');
33
+ font-weight: 400; font-style: normal;
34
+ }
35
+ @font-face {
36
+ font-family: 'Inter';
37
+ src: local('Inter Medium'), local('Inter-Medium');
38
+ font-weight: 500; font-style: normal;
39
+ }
40
+ @font-face {
41
+ font-family: 'Inter';
42
+ src: local('Inter SemiBold'), local('Inter-SemiBold');
43
+ font-weight: 600; font-style: normal;
44
+ }
29
45
  * { box-sizing: border-box; margin: 0; padding: 0; }
30
46
  html { scroll-behavior: smooth; }
31
47
  body {
32
- font-family: "Segoe UI", system-ui, sans-serif;
48
+ font-family: Inter, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
33
49
  background:
34
50
  radial-gradient(1100px 500px at 85% -10%, var(--bg-glow-1), transparent 60%),
35
51
  radial-gradient(900px 500px at -10% 30%, var(--bg-glow-2), transparent 55%),
@@ -91,15 +107,22 @@
91
107
  }
92
108
 
93
109
  /* ---------- Detail panel ---------- */
94
- #sbl-backdrop { position: fixed; inset: 0; z-index: 40; background: rgba(4,7,12,.55); }
95
- #sbl-detail {
96
- position: fixed; top: 0; right: 0; z-index: 50; height: 100vh; width: min(400px, 92vw);
97
- overflow-y: auto; padding: 22px 24px 32px;
98
- background: var(--surface); border-left: 1px solid var(--line-strong);
99
- box-shadow: -18px 0 44px rgba(0,0,0,.45);
100
- animation: sbl-slide-in .18s ease-out;
101
- }
102
- @keyframes sbl-slide-in { from { transform: translateX(26px); opacity: 0; } to { transform: none; opacity: 1; } }
110
+ #task-dialog {
111
+ width: min(640px, 92vw); max-height: 85vh; overflow-y: auto; padding: 0;
112
+ border: 1px solid var(--line-strong); border-radius: 14px;
113
+ background: var(--surface); color: var(--text);
114
+ box-shadow: 0 24px 80px rgba(0,0,0,.55);
115
+ }
116
+ #task-dialog::backdrop {
117
+ background: rgba(4,7,12,.65);
118
+ backdrop-filter: blur(2px);
119
+ }
120
+ #task-dialog[open] { animation: sbl-dialog-in .18s ease-out; }
121
+ @media (prefers-reduced-motion: reduce) {
122
+ #task-dialog[open] { animation: none; }
123
+ }
124
+ @keyframes sbl-dialog-in { from { transform: scale(.96); opacity: 0; } to { transform: none; opacity: 1; } }
125
+ .dialog-content { padding: 22px 24px 28px; }
103
126
  .detail-head { display: flex; align-items: center; gap: 10px; margin-bottom: 10px; }
104
127
  .detail-id { font-family: var(--mono); color: var(--accent); font-weight: 700; }
105
128
  .status-chip { font-family: var(--mono); font-size: .68rem; padding: 2px 10px; border-radius: 999px; border: 1px solid var(--line-strong); background: var(--surface-2); color: var(--muted); white-space: nowrap; }
@@ -109,9 +132,12 @@
109
132
  .status-chip[data-tone="danger"] { color: var(--danger); border-color: #5c2c2c; background: var(--danger-bg); }
110
133
  .detail-close { margin-left: auto; background: none; border: none; color: var(--dim); font-size: 1.3rem; cursor: pointer; line-height: 1; }
111
134
  .detail-close:hover { color: var(--danger); }
112
- .detail-title { font-size: 1.06rem; margin-bottom: 8px; overflow-wrap: anywhere; }
113
- #sbl-detail .detail-desc { color: var(--muted); margin-bottom: 14px; white-space: normal; }
114
- #sbl-detail h4 { color: var(--dim); font-size: .7rem; letter-spacing: 1.3px; text-transform: uppercase; margin: 16px 0 6px; }
135
+ .detail-title { font-size: 1.18rem; font-weight: 600; margin-bottom: 8px; overflow-wrap: anywhere; }
136
+ #task-dialog .detail-desc { color: var(--muted); margin-bottom: 14px; white-space: normal; }
137
+ #task-dialog h4 { color: var(--dim); font-size: .7rem; letter-spacing: 1.3px; text-transform: uppercase; margin: 16px 0 6px; }
138
+ .meta-row { display: flex; flex-wrap: wrap; gap: 14px; font-size: .85rem; color: var(--muted); }
139
+ .meta-row span { display: flex; align-items: center; gap: 4px; }
140
+ .meta-label { color: var(--dim); }
115
141
  .dep-row { display: flex; flex-wrap: wrap; gap: 6px; }
116
142
  .dep-link {
117
143
  font-family: var(--mono); font-size: .74rem; padding: 2px 10px; border-radius: 999px;
@@ -137,12 +163,21 @@
137
163
 
138
164
  /* ---------- Quick actions ---------- */
139
165
  .cmd-row { display: flex; flex-wrap: wrap; gap: 10px; align-items: center; }
140
- .cmd {
141
- font-family: var(--mono); font-size: .84rem; padding: 6px 14px; border-radius: 999px;
166
+ .cmd-row { display: flex; flex-wrap: wrap; gap: 10px; }
167
+ .cmd-btn {
168
+ display: flex; flex-direction: column; align-items: flex-start; gap: 3px;
169
+ padding: 10px 18px; border-radius: 10px; min-width: 180px; text-align: left;
142
170
  background: var(--surface); border: 1px solid var(--line-strong); color: var(--text);
171
+ cursor: pointer;
143
172
  }
144
- .cmd::before { content: "$ "; color: var(--accent); }
173
+ .cmd-btn:hover { background: var(--surface-2); border-color: var(--accent); }
174
+ .cmd-btn:active { transform: translateY(1px); }
175
+ .cmd-title { font-size: .95rem; font-weight: 600; color: var(--text); }
176
+ .cmd-btn:hover .cmd-title { color: var(--accent); }
177
+ .cmd-line { font-family: var(--mono); font-size: .72rem; color: var(--dim); }
145
178
  .hint { color: var(--dim); font-size: .8rem; margin-top: 12px; }
179
+ .drafts-list { list-style: none; margin: 0; padding: 0; display: grid; gap: 8px; }
180
+ .drafts-list li { background: var(--surface); border: 1px solid var(--line); border-radius: 8px; padding: 8px 12px; font-size: .88rem; }
146
181
  code.inline {
147
182
  font-family: var(--mono); font-size: .85em;
148
183
  background: var(--surface-2); border: 1px solid var(--line);
@@ -170,14 +205,8 @@
170
205
  .task-row { cursor: pointer; }
171
206
  .task-row:hover td { background: var(--surface-2); }
172
207
  .cell-id { color: var(--accent); font-family: var(--mono); font-variant-numeric: tabular-nums; }
173
- .cell-title { font-weight: 550; white-space: normal; min-width: 220px; }
208
+ .cell-title { font-weight: 400; white-space: normal; min-width: 220px; }
174
209
  .cell-updated { font-family: var(--mono); color: var(--muted); }
175
- .task-detail td { white-space: normal; background: var(--surface-2); border-top: none; }
176
- .detail-desc { margin: 0 0 8px; color: var(--muted); }
177
- .detail-empty { margin: 0; color: var(--dim); }
178
- .ac-list { margin: 0; padding: 0; list-style: none; }
179
- .ac { display: flex; align-items: baseline; gap: 8px; padding: 2px 0; }
180
- .ac.done span { text-decoration: line-through; color: var(--dim); }
181
210
 
182
211
  /* ---------- Footer ---------- */
183
212
  .footer {
@@ -234,24 +263,26 @@
234
263
  .step-label b { display: block; color: var(--text); font-size: .71rem; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
235
264
  .step.gate .step-label b { color: var(--warn); }
236
265
 
237
- /* ---------- Dependency graph ---------- */
266
+ /* ---------- Dependency flow ---------- */
238
267
  .sub-head {
239
268
  font-size: .78rem; letter-spacing: 1.4px; text-transform: uppercase;
240
269
  color: var(--dim); margin: 30px 0 12px; font-weight: 600;
241
270
  }
242
- .depgraph-wrap {
243
- overflow-x: auto; background: var(--surface); border: 1px solid var(--line);
244
- border-radius: 12px; padding: 14px;
245
- }
246
- .depgraph { display: block; min-width: 640px; }
247
- .depgraph .edge { fill: none; stroke: var(--line-strong); stroke-width: 1.4; transition: stroke .15s ease, stroke-width .15s ease; }
248
- .depgraph .edge.hot { stroke: var(--accent); stroke-width: 2.2; }
249
- .depgraph .node { cursor: pointer; outline: none; }
250
- .depgraph .node-box { fill: var(--surface-2); stroke: var(--line-strong); transition: stroke .15s ease, filter .15s ease; }
251
- .depgraph .node:hover .node-box, .depgraph .node:focus .node-box { stroke: var(--accent); filter: drop-shadow(0 0 8px rgba(92,200,255,.35)); }
252
- .depgraph .node-id { fill: var(--accent); font-family: var(--mono); font-size: 11px; font-weight: 700; }
253
- .depgraph .node-title { fill: var(--text); font-size: 11px; }
254
- .depgraph .node-status { font-family: var(--mono); font-size: 9px; letter-spacing: .6px; }
271
+ .flow-wrap { display: grid; gap: 20px; }
272
+ .flow-block { background: var(--surface); border: 1px solid var(--line); border-radius: 12px; padding: 14px 16px 18px; }
273
+ .flow-block h4 { margin: 0 0 10px; color: var(--text); font-size: .82rem; font-weight: 500; letter-spacing: .4px; }
274
+ .flow-list { list-style: none; margin: 0; padding: 0; display: grid; gap: 8px; }
275
+ .flow-card {
276
+ background: var(--surface-2); border: 1px solid var(--line-strong); border-radius: 10px;
277
+ padding: 10px 12px; cursor: pointer; transition: background .12s ease, border-color .12s ease;
278
+ }
279
+ .flow-card:hover { background: var(--surface); border-color: var(--accent); }
280
+ .flow-card-head { display: flex; align-items: center; gap: 10px; margin-bottom: 5px; }
281
+ .flow-card-id { font-family: var(--mono); color: var(--accent); font-weight: 700; font-size: .78rem; }
282
+ .flow-card-title { color: var(--text); font-size: .92rem; }
283
+ .flow-card-blockers { margin-top: 7px; font-size: .8rem; color: var(--dim); }
284
+ .flow-card-blockers .dep-link { margin-left: 4px; }
285
+ .flow-empty { color: var(--dim); font-size: .85rem; margin: 0; }
255
286
 
256
287
  @media (max-width:900px) {
257
288
  .layout { grid-template-columns: 1fr; }
@@ -293,13 +324,17 @@
293
324
  <section id="sec-01">
294
325
  <div class="sec-head"><span class="sec-num">01</span><h2>Board &amp; Quick Actions</h2><span class="tagline">commands &middot; board mirror only</span></div>
295
326
  <div id="quickactions" class="mount">
296
- <div class="cmd-row">
297
- <code class="cmd">backlog browser</code>
298
- <code class="cmd">backlog board</code>
299
- <code class="cmd">sbl dashboard --serve</code>
327
+ <div class="cmd-row" id="cmd-buttons">
328
+ <button type="button" class="cmd-btn" data-cmd="browser"><span class="cmd-title">Backlog Browser</span><span class="cmd-line">backlog browser</span></button>
329
+ <button type="button" class="cmd-btn" data-cmd="board"><span class="cmd-title">Backlog Board</span><span class="cmd-line">backlog board</span></button>
330
+ <button type="button" class="cmd-btn" data-copy="sbl dashboard"><span class="cmd-title">Live Dashboard</span><span class="cmd-line">sbl dashboard</span></button>
300
331
  </div>
301
332
  <p class="hint">The board mirrors Backlog.md &mdash; it never writes execution state.</p>
302
333
  </div>
334
+ <div id="drafts" class="mount">
335
+ <h3 class="sub-head">Drafts</h3>
336
+ <ul id="drafts-list" class="drafts-list"></ul>
337
+ </div>
303
338
  </section>
304
339
 
305
340
  <section id="sec-02">
@@ -313,7 +348,7 @@
313
348
  </section>
314
349
 
315
350
  <section id="sec-04">
316
- <div class="sec-head"><span class="sec-num">04</span><h2>Tasks</h2><span class="tagline">sort &middot; filter &middot; expand &mdash; <span class="term" data-term="AC">AC</span>s inline</span></div>
351
+ <div class="sec-head"><span class="sec-num">04</span><h2>Tasks</h2><span class="tagline">sort &middot; filter &middot; click row for details</span></div>
317
352
  <div id="tasks" class="mount">
318
353
  <div class="toolbar">
319
354
  <input id="taskfilter" type="search" placeholder="Filter tasks&hellip;" aria-label="Filter tasks">
@@ -340,7 +375,7 @@
340
375
  <section id="sec-05">
341
376
  <div class="sec-head"><span class="sec-num">05</span><h2>Feature Cycle</h2><span class="tagline">idea &rarr; merge &middot; every <span class="term" data-term="Review Gate">Review Gate</span> included</span></div>
342
377
  <div id="stepper" class="mount"></div>
343
- <h3 class="sub-head">Dependency flow</h3>
378
+ <h3 class="sub-head">Flow</h3>
344
379
  <div id="depgraph" class="mount"></div>
345
380
  </section>
346
381
 
@@ -357,14 +392,13 @@
357
392
 
358
393
  <footer class="footer">
359
394
  Generated __GENERATED_AT__ &middot; super-backlog v__KIT_VERSION__ &middot; <span id="source-badge"></span> &middot;
360
- regenerated automatically on commits touching backlog/.
395
+ served live by <code class="inline">sbl dashboard</code> &mdash; reloads automatically while the server is running.
361
396
  </footer>
362
397
 
363
398
  </main>
364
399
  </div>
365
400
 
366
- <div id="sbl-backdrop" hidden></div>
367
- <aside id="sbl-detail" hidden aria-label="Task details"></aside>
401
+ <dialog id="task-dialog" aria-label="Task details"></dialog>
368
402
  <div id="sbl-tip" role="tooltip" hidden></div>
369
403
 
370
404
  <script type="application/json" id="sbl-data">__SBL_DATA_JSON__</script>
@@ -425,37 +459,12 @@
425
459
  }
426
460
  rows.forEach(function (task) {
427
461
  var tr = el('tr', 'task-row');
462
+ tr.setAttribute('data-task', task.id);
428
463
  KEYS.forEach(function (k) {
429
464
  tr.appendChild(el('td', 'cell-' + k, field(task, k)));
430
465
  });
431
- var detail = el('tr', 'task-detail');
432
- var td = el('td');
433
- td.colSpan = KEYS.length;
434
- if (task.description) td.appendChild(el('p', 'detail-desc', task.description));
435
- if (task.acs.length > 0) {
436
- var list = el('ul', 'ac-list');
437
- task.acs.forEach(function (ac) {
438
- var li = el('li', ac.checked ? 'ac done' : 'ac');
439
- var box = document.createElement('input');
440
- box.type = 'checkbox';
441
- box.checked = ac.checked;
442
- box.disabled = true;
443
- li.appendChild(box);
444
- li.appendChild(document.createTextNode(ac.text));
445
- list.appendChild(li);
446
- });
447
- td.appendChild(list);
448
- } else {
449
- td.appendChild(el('p', 'detail-empty', 'No acceptance criteria.'));
450
- }
451
- detail.style.display = 'none';
452
- tr.addEventListener('click', function () {
453
- var opening = detail.style.display === 'none';
454
- detail.style.display = opening ? '' : 'none';
455
- tr.classList.toggle('expanded', opening);
456
- });
466
+ tr.addEventListener('click', function () { openDetail(task.id); });
457
467
  tbody.appendChild(tr);
458
- tbody.appendChild(detail);
459
468
  });
460
469
  }
461
470
  document.querySelectorAll('#tasks-table th').forEach(function (th) {
@@ -499,6 +508,40 @@
499
508
  });
500
509
  renderTasks();
501
510
 
511
+ /* ---------- Quick action buttons ---------- */
512
+ function cmdFeedback(btn, text) {
513
+ var title = btn.querySelector('.cmd-title');
514
+ if (!title || btn.getAttribute('data-busy')) return;
515
+ btn.setAttribute('data-busy', '1');
516
+ var orig = title.textContent;
517
+ title.textContent = text;
518
+ setTimeout(function () { title.textContent = orig; btn.removeAttribute('data-busy'); }, 1200);
519
+ }
520
+ function copyCommand(btn, command) {
521
+ if (navigator.clipboard) navigator.clipboard.writeText(command).catch(function () {});
522
+ cmdFeedback(btn, 'copied \u2713');
523
+ }
524
+ document.querySelectorAll('#cmd-buttons .cmd-btn').forEach(function (b) {
525
+ b.addEventListener('click', function () {
526
+ var copy = b.getAttribute('data-copy');
527
+ if (copy) { copyCommand(b, copy); return; }
528
+ var cmd = b.getAttribute('data-cmd');
529
+ fetch('/api/run', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ command: cmd }) })
530
+ .then(function (res) { if (!res.ok) throw new Error('run failed'); cmdFeedback(b, 'started \u2713'); })
531
+ .catch(function () {});
532
+ });
533
+ });
534
+
535
+ /* ---------- Drafts ---------- */
536
+ function renderDrafts(drafts) {
537
+ var list = document.getElementById('drafts-list');
538
+ if (!list) return;
539
+ list.textContent = '';
540
+ if (!drafts || drafts.length === 0) { list.appendChild(el('li', '', 'No drafts.')); return; }
541
+ drafts.forEach(function (d) { list.appendChild(el('li', '', d.id + ' \u2014 ' + d.title)); });
542
+ }
543
+ renderDrafts(data.drafts);
544
+
502
545
  /* ---------- Shared diagram helpers ---------- */
503
546
  var SVGNS = 'http://www.w3.org/2000/svg';
504
547
  var TONE_VAR = { ok: 'var(--ok)', accent: 'var(--accent)', warn: 'var(--warn)', danger: 'var(--danger)', dim: 'var(--dim)' };
@@ -750,9 +793,8 @@
750
793
  document.addEventListener('focusout', tipHide);
751
794
  }
752
795
 
753
- /* ---------- Detail panel ---------- */
754
- var panel = document.getElementById('sbl-detail');
755
- var backdrop = document.getElementById('sbl-backdrop');
796
+ /* ---------- Detail dialog ---------- */
797
+ var dialog = document.getElementById('task-dialog');
756
798
  var depsOut = {};
757
799
  var depsIn = {};
758
800
  data.deps.forEach(function (d) {
@@ -766,11 +808,13 @@
766
808
  });
767
809
  return found;
768
810
  }
811
+ function isDoneStatus(status) {
812
+ var s = String(status).toLowerCase();
813
+ return s === 'done' || s === 'complete' || s === 'completed';
814
+ }
769
815
  function closeDetail() {
770
- if (panel) panel.hidden = true;
771
- if (backdrop) backdrop.hidden = true;
816
+ if (dialog && dialog.open) dialog.close();
772
817
  }
773
- window.closeDetail = closeDetail;
774
818
  function acList(task) {
775
819
  var list = el('ul', 'ac-list');
776
820
  task.acs.forEach(function (ac) {
@@ -792,7 +836,6 @@
792
836
  ids.filter(function (id) { return findTask(id); }).forEach(function (id) {
793
837
  var btn = el('button', 'dep-link', id);
794
838
  btn.type = 'button';
795
- btn.setAttribute('data-dep', id);
796
839
  btn.addEventListener('click', function () { openDetail(id); });
797
840
  row.appendChild(btn);
798
841
  });
@@ -800,10 +843,24 @@
800
843
  block.appendChild(row);
801
844
  return block;
802
845
  }
846
+ function metaRow(task) {
847
+ var row = el('div', 'meta-row');
848
+ function item(label, value) {
849
+ var s = el('span');
850
+ s.appendChild(el('span', 'meta-label', label + ': '));
851
+ s.appendChild(document.createTextNode(value || '—'));
852
+ return s;
853
+ }
854
+ row.appendChild(item('Milestone', task.milestone));
855
+ row.appendChild(item('Priority', task.priority));
856
+ row.appendChild(item('Assignee', task.assignee));
857
+ row.appendChild(item('Updated', task.updated));
858
+ return row;
859
+ }
803
860
  function openDetail(id) {
804
861
  var t = findTask(id);
805
- if (!t || !panel) return;
806
- panel.textContent = '';
862
+ if (!t || !dialog) return;
863
+ var content = el('div', 'dialog-content');
807
864
  var head = el('div', 'detail-head');
808
865
  head.appendChild(el('span', 'detail-id', t.id));
809
866
  var chip = el('span', 'status-chip', t.status);
@@ -814,21 +871,22 @@
814
871
  closeBtn.setAttribute('aria-label', 'Close details');
815
872
  closeBtn.addEventListener('click', closeDetail);
816
873
  head.appendChild(closeBtn);
817
- panel.appendChild(head);
818
- panel.appendChild(el('h3', 'detail-title', t.title));
819
- panel.appendChild(el('p', 'detail-desc', t.description || 'No description.'));
874
+ content.appendChild(head);
875
+ content.appendChild(el('h3', 'detail-title', t.title));
876
+ content.appendChild(el('p', 'detail-desc', t.description || 'No description.'));
877
+ content.appendChild(metaRow(t));
820
878
  if (t.acs.length > 0) {
821
- panel.appendChild(el('h4', '', 'Acceptance criteria'));
822
- panel.appendChild(acList(t));
879
+ content.appendChild(el('h4', '', 'Acceptance criteria'));
880
+ content.appendChild(acList(t));
823
881
  }
824
- panel.appendChild(depSection('Depends on', depsOut[id] || []));
825
- panel.appendChild(depSection('Needed by', depsIn[id] || []));
826
- panel.scrollTop = 0;
827
- panel.hidden = false;
828
- backdrop.hidden = false;
882
+ content.appendChild(depSection('Depends on', depsOut[id] || []));
883
+ content.appendChild(depSection('Needed by', depsIn[id] || []));
884
+ dialog.textContent = '';
885
+ dialog.appendChild(content);
886
+ dialog.showModal();
829
887
  }
830
888
  window.__sblOpenDetail = openDetail;
831
- if (backdrop) backdrop.addEventListener('click', closeDetail);
889
+ if (dialog) dialog.addEventListener('click', function (ev) { if (ev.target === dialog) closeDetail(); });
832
890
 
833
891
  /* ---------- Keyboard shortcuts ---------- */
834
892
  document.addEventListener('keydown', function (ev) {
@@ -847,154 +905,79 @@
847
905
  }
848
906
  });
849
907
 
850
- /* ---------- Dependency graph (mirrors src/dashboard/layering.ts semantics) ---------- */
851
- function assignLayers(nodes, deps) {
852
- var nodeSet = {};
853
- nodes.forEach(function (n) { nodeSet[n] = true; });
854
- var prereqs = {};
855
- nodes.forEach(function (n) { prereqs[n] = []; });
856
- deps.forEach(function (d) {
857
- if (!nodeSet[d.from] || !nodeSet[d.to]) return; // dangling ref
858
- if (prereqs[d.from].indexOf(d.to) === -1) prereqs[d.from].push(d.to);
859
- });
860
- var layers = {};
861
- var remaining = {};
862
- nodes.forEach(function (n) { remaining[n] = true; });
863
- var left = nodes.length;
864
- var current = 0;
865
- function take(n) {
866
- layers[n] = current;
867
- delete remaining[n];
868
- left -= 1;
869
- }
870
- while (left > 0) {
871
- var ready = nodes.filter(function (n) {
872
- if (!remaining[n]) return false;
873
- return prereqs[n].every(function (p) { return !remaining[p]; });
874
- });
875
- current += 1;
876
- if (ready.length === 0) {
877
- // stalled behind a cycle: snapshot self-reachable members first, then
878
- // append them as one group (mirrors cycleMembers() in layering.ts)
879
- var cycleSnap = [];
880
- nodes.forEach(function (start) {
881
- if (!remaining[start]) return;
882
- var seen = {};
883
- var stack = prereqs[start].filter(function (p) { return remaining[p]; });
884
- var found = false;
885
- while (stack.length > 0 && !found) {
886
- var cur = stack.pop();
887
- if (cur === start) { found = true; break; }
888
- if (seen[cur]) continue;
889
- seen[cur] = true;
890
- stack = stack.concat(prereqs[cur].filter(function (p) { return remaining[p]; }));
891
- }
892
- if (found) cycleSnap.push(start);
893
- });
894
- cycleSnap.forEach(take);
895
- if (cycleSnap.length === 0) break; // unreachable safety net
896
- continue;
897
- }
898
- ready.forEach(take);
899
- }
900
- var out = {};
901
- nodes.forEach(function (n) { out[n] = layers[n] || 1; });
902
- return out;
903
- }
904
-
905
- function hotEdges(id, on) {
906
- document.querySelectorAll('.depgraph .edge').forEach(function (edge) {
907
- var hit = edge.getAttribute('data-from') === id || edge.getAttribute('data-to') === id;
908
- edge.classList.toggle('hot', on && hit);
909
- });
910
- }
911
-
912
- function renderDepGraph(mount, tasks, deps) {
908
+ /* ---------- Dependency flow ---------- */
909
+ function renderFlow(mount, tasks) {
913
910
  mount.textContent = '';
914
- if (tasks.length === 0) {
915
- mount.appendChild(el('p', 'hint', 'No tasks to graph.'));
916
- return;
917
- }
918
- var ids = tasks.map(function (t) { return t.id; });
919
- var layers = assignLayers(ids, deps);
920
911
  var byId = {};
921
912
  tasks.forEach(function (t) { byId[t.id] = t; });
922
- var maxLayer = 1;
923
- ids.forEach(function (id) { if (layers[id] > maxLayer) maxLayer = layers[id]; });
924
- var cols = [];
925
- for (var i = 0; i < maxLayer; i++) cols.push([]);
926
- ids.forEach(function (id) { cols[layers[id] - 1].push(byId[id]); });
927
- var CHIP_W = 172, CHIP_H = 40, GAP_X = 84, GAP_Y = 14, PAD = 18;
928
- var rows = 1;
929
- cols.forEach(function (c) { if (c.length > rows) rows = c.length; });
930
- var width = PAD * 2 + cols.length * CHIP_W + (cols.length - 1) * GAP_X;
931
- var height = PAD * 2 + Math.max(rows, 1) * CHIP_H + (Math.max(rows, 1) - 1) * GAP_Y;
932
- var wrap = el('div', 'depgraph-wrap');
933
- var svg = svgEl('svg', {
934
- viewBox: '0 0 ' + width + ' ' + height, width: width, height: height,
935
- 'class': 'depgraph', role: 'img', 'aria-label': 'Task dependency graph'
936
- });
937
- var defs = svgEl('defs');
938
- var marker = svgEl('marker', { id: 'sbl-arrow', markerWidth: '9', markerHeight: '7', refX: '8.5', refY: '3.5', orient: 'auto' });
939
- marker.appendChild(svgEl('polygon', { points: '0 0, 9 3.5, 0 7', fill: 'var(--line-strong)' }));
940
- defs.appendChild(marker);
941
- svg.appendChild(defs);
942
- var pos = {};
943
- cols.forEach(function (col, ci) {
944
- col.forEach(function (t, ri) {
945
- pos[t.id] = {
946
- x: PAD + ci * (CHIP_W + GAP_X),
947
- y: PAD + ri * (CHIP_H + GAP_Y)
948
- };
949
- });
950
- });
951
- deps.forEach(function (d) {
952
- if (!pos[d.from] || !pos[d.to] || d.from === d.to) return;
953
- var a = pos[d.from];
954
- var b = pos[d.to];
955
- var x1 = a.x + CHIP_W, y1 = a.y + CHIP_H / 2, x2 = b.x, y2 = b.y + CHIP_H / 2;
956
- var mx = (x1 + x2) / 2;
957
- var path = svgEl('path', {
958
- 'class': 'edge', 'data-from': d.from, 'data-to': d.to,
959
- d: 'M ' + x1 + ' ' + y1 + ' C ' + mx + ' ' + y1 + ', ' + mx + ' ' + y2 + ', ' + x2 + ' ' + y2,
960
- 'marker-end': 'url(#sbl-arrow)'
913
+ var pending = tasks.filter(function (t) { return !isDoneStatus(t.status); });
914
+ var upNext = [];
915
+ var blocked = [];
916
+ pending.forEach(function (t) {
917
+ var blockers = (depsOut[t.id] || []).filter(function (id) {
918
+ var d = byId[id];
919
+ return d && !isDoneStatus(d.status);
961
920
  });
962
- svg.appendChild(path);
921
+ if (blockers.length === 0) upNext.push({ task: t, blockers: [] });
922
+ else blocked.push({ task: t, blockers: blockers });
963
923
  });
964
- cols.forEach(function (col) {
965
- col.forEach(function (t) {
966
- var p = pos[t.id];
967
- var tone = toneOf(t.status);
968
- var g = svgEl('g', { 'class': 'node', 'data-id': t.id, tabindex: '0', role: 'button', 'aria-label': t.id + ': ' + t.title });
969
- attachTitle(g, t.id + ' \u2014 ' + t.title + ' (' + t.status + ')' + (t.milestone ? ' [' + t.milestone + ']' : ''));
970
- g.appendChild(svgEl('rect', { x: p.x, y: p.y, width: CHIP_W, height: CHIP_H, rx: 9, 'class': 'node-box' }));
971
- g.appendChild(svgEl('rect', { x: p.x, y: p.y + 6, width: 4, height: CHIP_H - 12, rx: 2, fill: TONE_VAR[tone] }));
972
- var idText = svgEl('text', { x: p.x + 13, y: p.y + 17, 'class': 'node-id' });
973
- idText.textContent = t.id;
974
- g.appendChild(idText);
975
- var st = svgEl('text', { x: p.x + CHIP_W - 10, y: p.y + 16, 'text-anchor': 'end', 'class': 'node-status', fill: TONE_VAR[tone] });
976
- st.textContent = String(t.status).toUpperCase();
977
- g.appendChild(st);
978
- var title = svgEl('text', { x: p.x + 13, y: p.y + 31, 'class': 'node-title' });
979
- title.textContent = String(t.title).length > 26 ? String(t.title).slice(0, 25) + '\u2026' : t.title;
980
- g.appendChild(title);
981
- g.addEventListener('mouseenter', function () { hotEdges(t.id, true); });
982
- g.addEventListener('mouseleave', function () { hotEdges(t.id, false); });
983
- g.addEventListener('click', function () { window.__sblOpenDetail(t.id); });
984
- g.addEventListener('keydown', function (ev) {
985
- if (ev.key === 'Enter' || ev.key === ' ') {
986
- ev.preventDefault();
987
- window.__sblOpenDetail(t.id);
924
+ function section(titleText, items, emptyText) {
925
+ var wrap = el('div', 'flow-block');
926
+ wrap.appendChild(el('h4', '', titleText));
927
+ if (items.length === 0) {
928
+ wrap.appendChild(el('p', 'flow-empty', emptyText));
929
+ } else {
930
+ var list = el('ul', 'flow-list');
931
+ items.forEach(function (item) {
932
+ var li = el('li', 'flow-card');
933
+ var head = el('div', 'flow-card-head');
934
+ head.appendChild(el('span', 'flow-card-id', item.task.id));
935
+ var chip = el('span', 'status-chip', item.task.status);
936
+ chip.setAttribute('data-tone', toneOf(item.task.status));
937
+ head.appendChild(chip);
938
+ li.appendChild(head);
939
+ li.appendChild(el('div', 'flow-card-title', item.task.title));
940
+ if (item.blockers.length > 0) {
941
+ var blockRow = el('div', 'flow-card-blockers');
942
+ blockRow.appendChild(document.createTextNode('Blocked by: '));
943
+ item.blockers.forEach(function (id, i) {
944
+ if (i > 0) blockRow.appendChild(document.createTextNode(', '));
945
+ var btn = el('button', 'dep-link', id);
946
+ btn.type = 'button';
947
+ btn.addEventListener('click', function () { openDetail(id); });
948
+ blockRow.appendChild(btn);
949
+ });
950
+ li.appendChild(blockRow);
988
951
  }
952
+ li.addEventListener('click', function (ev) {
953
+ if (ev.target.closest('.dep-link')) return;
954
+ openDetail(item.task.id);
955
+ });
956
+ list.appendChild(li);
989
957
  });
990
- svg.appendChild(g);
991
- });
992
- });
993
- wrap.appendChild(svg);
958
+ wrap.appendChild(list);
959
+ }
960
+ return wrap;
961
+ }
962
+ if (upNext.length === 0 && blocked.length === 0) {
963
+ mount.appendChild(el('p', 'hint', 'No pending tasks — everything is done.'));
964
+ return;
965
+ }
966
+ var wrap = el('div', 'flow-wrap');
967
+ wrap.appendChild(section('Up Next', upNext, 'No tasks ready to start.'));
968
+ wrap.appendChild(section('Blocked', blocked, 'No blocked tasks.'));
994
969
  mount.appendChild(wrap);
995
970
  }
996
971
 
997
- renderDepGraph($('#depgraph'), data.tasks, data.deps);
972
+ renderFlow($('#depgraph'), data.tasks);
973
+ })();
974
+ </script>
975
+ <script>
976
+ (function () {
977
+ if (!window.EventSource || (location.protocol !== 'http:' && location.protocol !== 'https:')) return;
978
+ var es = new EventSource('/api/events');
979
+ es.addEventListener('reload', function () { location.reload(); });
980
+ es.addEventListener('error', function () {});
998
981
  })();
999
982
  </script>
1000
983
  </body>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "super-backlog",
3
- "version": "0.9.0",
3
+ "version": "1.0.0",
4
4
  "description": "One command to equip any project with Backlog.md + Superpowers, plus a Project Dashboard.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -1,28 +0,0 @@
1
- // src/dashboard/regen.ts
2
- // Standalone regeneration entry invoked by the post-commit freshness hook:
3
- // node "<repo>/node_modules/super-backlog/dist/dashboard/regen.js"
4
- // Contract: never throw, never exit non-zero - a broken dashboard generation
5
- // must never block or fail a user's commit. All errors go to console.error.
6
- import { join, resolve } from 'node:path';
7
- import { pathToFileURL } from 'node:url';
8
- import process from 'node:process';
9
- import { collectDashboardData } from './data.js';
10
- import { renderDashboard } from './render.js';
11
- import { atomicWrite } from '../lib/atomic.js';
12
- import { KIT_VERSION } from '../lib/version.js';
13
- /** Regenerate <cwd>/dashboard.html. Throws on failure; the direct-run wrapper swallows. */
14
- export function regenerateDashboard(cwd) {
15
- const html = renderDashboard(collectDashboardData(cwd, { kitVersion: KIT_VERSION }));
16
- atomicWrite(join(cwd, 'dashboard.html'), html);
17
- }
18
- const invokedDirectly = process.argv[1] !== undefined &&
19
- import.meta.url === pathToFileURL(resolve(process.argv[1])).href;
20
- if (invokedDirectly) {
21
- try {
22
- regenerateDashboard(process.cwd());
23
- }
24
- catch (err) {
25
- console.error(err instanceof Error ? (err.stack ?? err.message) : err);
26
- }
27
- process.exit(0); // always - success or failure
28
- }