super-backlog 0.3.1

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.
@@ -0,0 +1,1001 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1">
6
+ <title>__PROJECT_NAME__ &middot; Project Dashboard</title>
7
+ <style>
8
+ :root {
9
+ --bg:#0a0e16;
10
+ --bg-glow-1: rgba(92,200,255,.06);
11
+ --bg-glow-2: rgba(62,207,142,.04);
12
+ --surface:#111826;
13
+ --surface-2:#0d1320;
14
+ --line:#1e293c;
15
+ --line-strong:#2c3b57;
16
+ --text:#e8edf6;
17
+ --muted:#8fa0ba;
18
+ --dim:#5d6d88;
19
+ --accent:#5cc8ff;
20
+ --accent-dim:#234257;
21
+ --ok:#3ecf8e;
22
+ --ok-bg:#12291e;
23
+ --warn:#ffb454;
24
+ --warn-bg:#33260f;
25
+ --danger:#ff7a7a;
26
+ --danger-bg:#331718;
27
+ --mono:"Cascadia Code",Consolas,"Courier New",monospace;
28
+ }
29
+ * { box-sizing: border-box; margin: 0; padding: 0; }
30
+ html { scroll-behavior: smooth; }
31
+ body {
32
+ font-family: "Segoe UI", system-ui, sans-serif;
33
+ background:
34
+ radial-gradient(1100px 500px at 85% -10%, var(--bg-glow-1), transparent 60%),
35
+ radial-gradient(900px 500px at -10% 30%, var(--bg-glow-2), transparent 55%),
36
+ var(--bg);
37
+ color: var(--text);
38
+ line-height: 1.55;
39
+ }
40
+
41
+ /* ---------- Layout: sticky sidebar + main ---------- */
42
+ .layout { display: grid; grid-template-columns: 232px minmax(0,1fr); max-width: 1300px; margin: 0 auto; }
43
+ .sbl-side {
44
+ position: sticky; top: 0; align-self: start;
45
+ height: 100vh; overflow-y: auto;
46
+ padding: 28px 18px 24px 24px;
47
+ border-right: 1px solid var(--line);
48
+ }
49
+ main { padding: 34px 38px 48px 36px; min-width: 0; }
50
+
51
+ /* ---------- Sidebar ---------- */
52
+ .brand { display: flex; align-items: center; gap: 10px; margin-bottom: 6px; }
53
+ .brand-glyph { color: var(--accent); font-size: .8rem; line-height: 1; text-shadow: 0 0 12px rgba(92,200,255,.6); }
54
+ .brand b { font-size: .98rem; letter-spacing: .4px; overflow-wrap: anywhere; }
55
+ .kicker {
56
+ color: var(--dim); font-size: .72rem; letter-spacing: 1.6px;
57
+ text-transform: uppercase; margin-bottom: 22px;
58
+ }
59
+ nav a {
60
+ display: block; padding: 7px 12px; border-radius: 8px;
61
+ color: var(--muted); text-decoration: none; font-size: .88rem;
62
+ border: 1px solid transparent; white-space: nowrap;
63
+ }
64
+ nav a:hover { color: var(--text); background: var(--surface); border-color: var(--line); }
65
+ nav a .n { color: var(--accent); font-family: var(--mono); font-size: .78rem; margin-right: 8px; }
66
+ .legend { margin-top: 26px; padding-top: 18px; border-top: 1px solid var(--line); }
67
+ .legend h4 { color: var(--dim); font-size: .7rem; letter-spacing: 1.4px; text-transform: uppercase; margin-bottom: 10px; }
68
+ .pill-row { display: flex; flex-wrap: wrap; gap: 6px; }
69
+ .pill {
70
+ font-family: var(--mono); font-size: .72rem; padding: 3px 10px; border-radius: 999px;
71
+ border: 1px solid var(--line-strong); color: var(--muted); background: var(--surface-2);
72
+ white-space: nowrap; cursor: pointer;
73
+ }
74
+ .pill .n { color: inherit; font-weight: 700; margin-right: 4px; }
75
+ .pill[data-tone="ok"] { color: var(--ok); border-color: #2b5642; background: var(--ok-bg); }
76
+ .pill[data-tone="accent"] { color: var(--accent); border-color: #274a63; background: #10202e; }
77
+ .pill[data-tone="warn"] { color: var(--warn); border-color: #5c4520; background: var(--warn-bg); }
78
+ .pill[data-tone="danger"] { color: var(--danger); border-color: #5c2c2c; background: var(--danger-bg); }
79
+ .pill:hover { filter: brightness(1.25); }
80
+ .pill.active { outline: 2px solid var(--accent); outline-offset: 1px; }
81
+ .pill-empty { color: var(--dim); font-size: .78rem; }
82
+
83
+ /* ---------- Glossary tooltips ---------- */
84
+ .term { border-bottom: 1px dotted var(--dim); cursor: help; }
85
+ .term:hover { color: var(--accent); border-color: var(--accent); }
86
+ #sbl-tip {
87
+ position: fixed; z-index: 70; max-width: 340px; padding: 8px 12px;
88
+ background: rgba(13,19,32,.97); border: 1px solid var(--line-strong); border-radius: 8px;
89
+ font-size: .78rem; line-height: 1.45; color: var(--text);
90
+ box-shadow: 0 10px 28px rgba(0,0,0,.5); pointer-events: none;
91
+ }
92
+
93
+ /* ---------- 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; } }
103
+ .detail-head { display: flex; align-items: center; gap: 10px; margin-bottom: 10px; }
104
+ .detail-id { font-family: var(--mono); color: var(--accent); font-weight: 700; }
105
+ .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; }
106
+ .status-chip[data-tone="ok"] { color: var(--ok); border-color: #2b5642; background: var(--ok-bg); }
107
+ .status-chip[data-tone="accent"] { color: var(--accent); border-color: #274a63; background: #10202e; }
108
+ .status-chip[data-tone="warn"] { color: var(--warn); border-color: #5c4520; background: var(--warn-bg); }
109
+ .status-chip[data-tone="danger"] { color: var(--danger); border-color: #5c2c2c; background: var(--danger-bg); }
110
+ .detail-close { margin-left: auto; background: none; border: none; color: var(--dim); font-size: 1.3rem; cursor: pointer; line-height: 1; }
111
+ .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; }
115
+ .dep-row { display: flex; flex-wrap: wrap; gap: 6px; }
116
+ .dep-link {
117
+ font-family: var(--mono); font-size: .74rem; padding: 2px 10px; border-radius: 999px;
118
+ border: 1px solid #274a63; background: #10202e; color: var(--accent); cursor: pointer;
119
+ }
120
+ .dep-link:hover { filter: brightness(1.25); }
121
+
122
+ /* ---------- Sections ---------- */
123
+ section { margin-top: 44px; scroll-margin-top: 20px; }
124
+ section:first-of-type { margin-top: 0; }
125
+ .sec-head {
126
+ display: flex; align-items: baseline; gap: 16px;
127
+ border-bottom: 1px solid var(--line); padding-bottom: 10px; margin-bottom: 20px;
128
+ }
129
+ .sec-num {
130
+ font-family: var(--mono); font-size: 2rem; font-weight: 700;
131
+ background: linear-gradient(180deg, var(--accent), transparent 130%);
132
+ -webkit-background-clip: text; background-clip: text; color: transparent;
133
+ }
134
+ .sec-head h2 { font-size: 1.15rem; letter-spacing: .5px; }
135
+ .sec-head .tagline { color: var(--dim); font-size: .82rem; margin-left: auto; text-align: right; }
136
+ .mount { min-height: 24px; }
137
+
138
+ /* ---------- Quick actions ---------- */
139
+ .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;
142
+ background: var(--surface); border: 1px solid var(--line-strong); color: var(--text);
143
+ }
144
+ .cmd::before { content: "$ "; color: var(--accent); }
145
+ .hint { color: var(--dim); font-size: .8rem; margin-top: 12px; }
146
+ code.inline {
147
+ font-family: var(--mono); font-size: .85em;
148
+ background: var(--surface-2); border: 1px solid var(--line);
149
+ border-radius: 5px; padding: 1px 6px; white-space: nowrap;
150
+ }
151
+
152
+ /* ---------- Tasks table ---------- */
153
+ .toolbar { display: flex; justify-content: flex-end; margin-bottom: var(--sp, 12px); }
154
+ input[type="search"] {
155
+ background: var(--surface-2); color: var(--text); border: 1px solid var(--line-strong);
156
+ border-radius: 999px; padding: 7px 16px; min-width: 260px; font: inherit;
157
+ }
158
+ input[type="search"]:focus { outline: none; border-color: var(--accent-dim); box-shadow: 0 0 0 2px rgba(92,200,255,.15); }
159
+ .table-wrap { overflow-x: auto; background: var(--surface); border: 1px solid var(--line); border-radius: 12px; }
160
+ table { width: 100%; border-collapse: collapse; font-size: .89rem; }
161
+ th, td { text-align: left; padding: 8px 14px; border-bottom: 1px solid var(--line); vertical-align: top; white-space: nowrap; }
162
+ tr:last-child td { border-bottom: none; }
163
+ thead th {
164
+ color: var(--dim); font-weight: 600; font-size: .74rem; letter-spacing: 1.2px;
165
+ text-transform: uppercase; cursor: pointer; user-select: none;
166
+ position: sticky; top: 0; background: var(--surface);
167
+ }
168
+ thead th.sorted-asc::after { content: " \2191"; color: var(--accent); }
169
+ thead th.sorted-desc::after { content: " \2193"; color: var(--accent); }
170
+ .task-row { cursor: pointer; }
171
+ .task-row:hover td { background: var(--surface-2); }
172
+ .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; }
174
+ .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
+
182
+ /* ---------- Footer ---------- */
183
+ .footer {
184
+ margin-top: 48px; padding-top: 14px; border-top: 1px solid var(--line);
185
+ color: var(--dim); font-size: .8rem;
186
+ }
187
+
188
+ /* ---------- Donut ---------- */
189
+ .donut-wrap { display: flex; align-items: center; gap: 30px; flex-wrap: wrap; }
190
+ .donut-total { font-family: var(--mono); font-size: 2rem; font-weight: 700; fill: var(--text); }
191
+ .donut-caption { font-family: var(--mono); font-size: .6rem; letter-spacing: 2px; fill: var(--dim); }
192
+ .donut .seg { cursor: pointer; transition: opacity .15s ease; }
193
+ .donut .seg:hover, .donut .seg.hot { opacity: .82; }
194
+ .legend-list { display: grid; gap: 6px; font-size: .84rem; color: var(--muted); list-style: none; margin: 0; padding: 0; }
195
+ .legend-list li { display: flex; align-items: center; gap: 9px; cursor: default; }
196
+ .legend-list li:hover { color: var(--text); }
197
+ .legend-list .swatch { width: 10px; height: 10px; border-radius: 3px; flex: none; }
198
+
199
+ /* ---------- Milestone bars ---------- */
200
+ .bars text { font-family: var(--mono); }
201
+ .bar-label { fill: var(--text); font-size: 12.5px; }
202
+ .bar-val { fill: var(--muted); font-size: 11px; }
203
+ .bar-track { fill: var(--surface-2); stroke: var(--line); }
204
+ .bar-fill { fill: var(--accent); }
205
+ .bar-fill.complete { fill: var(--ok); }
206
+
207
+ /* ---------- Sparkline ---------- */
208
+ .spark-line {
209
+ fill: none; stroke: var(--accent); stroke-width: 2;
210
+ stroke-linejoin: round; stroke-linecap: round;
211
+ filter: drop-shadow(0 0 6px rgba(92,200,255,.4));
212
+ }
213
+ .spark-area { fill: rgba(92,200,255,.09); stroke: none; }
214
+ .spark-dot { fill: var(--bg); stroke: var(--dim); stroke-width: 1; }
215
+ .spark-dot.nonzero { fill: var(--accent); stroke: var(--accent); }
216
+ .spark-axis { fill: var(--dim); font-size: 10px; font-family: var(--mono); }
217
+
218
+ /* ---------- Pipeline stepper ---------- */
219
+ .stepper { display: grid; grid-template-columns: repeat(9, minmax(0, 1fr)); margin: 4px 0 8px; }
220
+ .step { position: relative; min-width: 0; padding: 0 3px; text-align: center; }
221
+ .step::before {
222
+ content: ""; position: absolute; top: 15px; left: -50%; width: 100%; height: 2px;
223
+ background: var(--line-strong); z-index: 0;
224
+ }
225
+ .step:first-child::before { display: none; }
226
+ .step-num {
227
+ position: relative; z-index: 1; width: 32px; height: 32px; margin: 0 auto 8px;
228
+ display: flex; align-items: center; justify-content: center;
229
+ border-radius: 50%; font-family: var(--mono); font-size: .82rem; font-weight: 700;
230
+ background: var(--surface-2); border: 2px solid var(--line-strong); color: var(--muted);
231
+ }
232
+ .step.gate .step-num { border-color: var(--warn); color: var(--warn); background: var(--warn-bg); box-shadow: 0 0 14px rgba(255,180,84,.25); }
233
+ .step-label { font-size: .63rem; color: var(--muted); line-height: 1.35; }
234
+ .step-label b { display: block; color: var(--text); font-size: .71rem; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
235
+ .step.gate .step-label b { color: var(--warn); }
236
+
237
+ /* ---------- Dependency graph ---------- */
238
+ .sub-head {
239
+ font-size: .78rem; letter-spacing: 1.4px; text-transform: uppercase;
240
+ color: var(--dim); margin: 30px 0 12px; font-weight: 600;
241
+ }
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; }
255
+
256
+ @media (max-width:900px) {
257
+ .layout { grid-template-columns: 1fr; }
258
+ .sbl-side {
259
+ position: static; height: auto; overflow: visible;
260
+ border-right: none; border-bottom: 1px solid var(--line);
261
+ padding: 18px 20px 16px;
262
+ }
263
+ nav { display: flex; flex-wrap: wrap; gap: 4px; }
264
+ main { padding: 24px 20px 40px; }
265
+ }
266
+ </style>
267
+ </head>
268
+ <body>
269
+ <div class="layout">
270
+
271
+ <aside class="sbl-side">
272
+ <div class="brand"><span class="brand-glyph">●</span><b>__PROJECT_NAME__</b></div>
273
+ <div class="kicker">SUPERPOWERS × BACKLOG.MD</div>
274
+ <nav aria-label="Dashboard sections">
275
+ <a href="#sec-01"><span class="n">01</span>Board &amp; Quick Actions</a>
276
+ <a href="#sec-02"><span class="n">02</span>Status</a>
277
+ <a href="#sec-03"><span class="n">03</span>Milestones</a>
278
+ <a href="#sec-04"><span class="n">04</span>Tasks</a>
279
+ <a href="#sec-05"><span class="n">05</span>Feature Cycle</a>
280
+ <a href="#sec-06"><span class="n">06</span>Activity</a>
281
+ <a href="#sec-07"><span class="n">07</span>Decisions &amp; Docs</a>
282
+ </nav>
283
+ <div class="legend">
284
+ <h4>Status</h4>
285
+ <div class="pill-row" id="pills">
286
+ __STATUS_PILLS__
287
+ </div>
288
+ </div>
289
+ </aside>
290
+
291
+ <main>
292
+
293
+ <section id="sec-01">
294
+ <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
+ <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>
300
+ </div>
301
+ <p class="hint">The board mirrors Backlog.md &mdash; it never writes execution state.</p>
302
+ </div>
303
+ </section>
304
+
305
+ <section id="sec-02">
306
+ <div class="sec-head"><span class="sec-num">02</span><h2>Status</h2><span class="tagline">tasks per status</span></div>
307
+ <div id="donut" class="mount"></div>
308
+ </section>
309
+
310
+ <section id="sec-03">
311
+ <div class="sec-head"><span class="sec-num">03</span><h2>Milestones</h2><span class="tagline">done / total per <span class="term" data-term="Milestone">Milestone</span></span></div>
312
+ <div id="bars" class="mount"></div>
313
+ </section>
314
+
315
+ <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>
317
+ <div id="tasks" class="mount">
318
+ <div class="toolbar">
319
+ <input id="taskfilter" type="search" placeholder="Filter tasks&hellip;" aria-label="Filter tasks">
320
+ </div>
321
+ <div class="table-wrap">
322
+ <table id="tasks-table">
323
+ <thead>
324
+ <tr>
325
+ <th data-key="id">ID</th>
326
+ <th data-key="title">Title</th>
327
+ <th data-key="status">Status</th>
328
+ <th data-key="milestone">Milestone</th>
329
+ <th data-key="priority">Priority</th>
330
+ <th data-key="assignee">Assignee</th>
331
+ <th data-key="updated">Updated</th>
332
+ </tr>
333
+ </thead>
334
+ <tbody id="task-rows"></tbody>
335
+ </table>
336
+ </div>
337
+ </div>
338
+ </section>
339
+
340
+ <section id="sec-05">
341
+ <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
+ <div id="stepper" class="mount"></div>
343
+ <h3 class="sub-head">Dependency flow</h3>
344
+ <div id="depgraph" class="mount"></div>
345
+ </section>
346
+
347
+ <section id="sec-06">
348
+ <div class="sec-head"><span class="sec-num">06</span><h2>Activity</h2><span class="tagline">last 30 days</span></div>
349
+ <div id="spark" class="mount"></div>
350
+ </section>
351
+
352
+ <section id="sec-07">
353
+ <div class="sec-head"><span class="sec-num">07</span><h2>Decisions &amp; Docs</h2><span class="tagline">decisions &middot; docs &middot; glossary</span></div>
354
+ <div id="docs" class="mount"></div>
355
+ <p class="hint">The <span class="term" data-term="TDD">TDD</span> loop and the <span class="term" data-term="DoD">DoD</span> ship as built-in glossary terms &mdash; extend them via <code class="inline">backlog/docs/glossary.md</code>.</p>
356
+ </section>
357
+
358
+ <footer class="footer">
359
+ Generated __GENERATED_AT__ &middot; super-backlog v__KIT_VERSION__ &middot; <span id="source-badge"></span> &middot;
360
+ regenerated automatically on commits touching backlog/.
361
+ </footer>
362
+
363
+ </main>
364
+ </div>
365
+
366
+ <div id="sbl-backdrop" hidden></div>
367
+ <aside id="sbl-detail" hidden aria-label="Task details"></aside>
368
+ <div id="sbl-tip" role="tooltip" hidden></div>
369
+
370
+ <script type="application/json" id="sbl-data">__SBL_DATA_JSON__</script>
371
+ <script type="application/json" id="sbl-glossary">__SBL_GLOSSARY_JSON__</script>
372
+ <script type="application/json" id="sbl-phases">__SBL_PHASES_JSON__</script>
373
+ <script id="sbl-app">
374
+ (function () {
375
+ 'use strict';
376
+ var data = JSON.parse(document.getElementById('sbl-data').textContent);
377
+ function $(sel) { return document.querySelector(sel); }
378
+ function el(tag, cls, text) {
379
+ var node = document.createElement(tag);
380
+ if (cls) node.className = cls;
381
+ if (text !== undefined && text !== '') node.textContent = text;
382
+ return node;
383
+ }
384
+
385
+ var badge = $('#source-badge');
386
+ if (badge) {
387
+ badge.textContent = data.source === 'fallback-empty' ? 'no live data' : 'live backlog data';
388
+ }
389
+
390
+ var KEYS = ['id', 'title', 'status', 'milestone', 'priority', 'assignee', 'updated'];
391
+ var state = { key: 'id', dir: 1, query: '', status: null, hoverStatus: null };
392
+ function field(task, key) {
393
+ var v = task[key];
394
+ return v === undefined || v === null ? '' : String(v);
395
+ }
396
+ function activeStatus() {
397
+ return state.hoverStatus || state.status;
398
+ }
399
+ function matches(task) {
400
+ var eff = activeStatus();
401
+ if (eff && field(task, 'status').toLowerCase() !== eff.toLowerCase()) return false;
402
+ if (!state.query) return true;
403
+ var hit = KEYS.some(function (k) {
404
+ return field(task, k).toLowerCase().indexOf(state.query) !== -1;
405
+ });
406
+ return hit || task.acs.some(function (ac) {
407
+ return ac.text.toLowerCase().indexOf(state.query) !== -1;
408
+ });
409
+ }
410
+ var tbody = $('#task-rows');
411
+ function renderTasks() {
412
+ var rows = data.tasks
413
+ .filter(matches)
414
+ .sort(function (a, b) {
415
+ return field(a, state.key).localeCompare(field(b, state.key)) * state.dir;
416
+ });
417
+ tbody.textContent = '';
418
+ if (rows.length === 0) {
419
+ var emptyTr = el('tr');
420
+ var emptyTd = el('td', '', 'No matching tasks.');
421
+ emptyTd.colSpan = KEYS.length;
422
+ emptyTr.appendChild(emptyTd);
423
+ tbody.appendChild(emptyTr);
424
+ return;
425
+ }
426
+ rows.forEach(function (task) {
427
+ var tr = el('tr', 'task-row');
428
+ KEYS.forEach(function (k) {
429
+ tr.appendChild(el('td', 'cell-' + k, field(task, k)));
430
+ });
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
+ });
457
+ tbody.appendChild(tr);
458
+ tbody.appendChild(detail);
459
+ });
460
+ }
461
+ document.querySelectorAll('#tasks-table th').forEach(function (th) {
462
+ th.addEventListener('click', function () {
463
+ var key = th.getAttribute('data-key');
464
+ if (!key) return;
465
+ if (state.key === key) {
466
+ state.dir = -state.dir;
467
+ } else {
468
+ state.key = key;
469
+ state.dir = 1;
470
+ }
471
+ document.querySelectorAll('#tasks-table th').forEach(function (other) {
472
+ other.classList.remove('sorted-asc', 'sorted-desc');
473
+ other.removeAttribute('aria-sort');
474
+ });
475
+ th.classList.add(state.dir === 1 ? 'sorted-asc' : 'sorted-desc');
476
+ th.setAttribute('aria-sort', state.dir === 1 ? 'ascending' : 'descending');
477
+ renderTasks();
478
+ });
479
+ });
480
+ var filterInput = document.getElementById('taskfilter');
481
+ if (filterInput) {
482
+ filterInput.addEventListener('input', function (event) {
483
+ state.query = event.target.value.trim().toLowerCase();
484
+ renderTasks();
485
+ });
486
+ }
487
+ function setStatusFilter(status) {
488
+ state.status = status || null;
489
+ document.querySelectorAll('#pills .pill').forEach(function (pill) {
490
+ pill.classList.toggle('active', !!state.status && pill.getAttribute('data-status') === state.status);
491
+ });
492
+ renderTasks();
493
+ }
494
+ document.querySelectorAll('#pills .pill').forEach(function (pill) {
495
+ pill.addEventListener('click', function () {
496
+ var s = pill.getAttribute('data-status');
497
+ setStatusFilter(state.status === s ? null : s);
498
+ });
499
+ });
500
+ renderTasks();
501
+
502
+ /* ---------- Shared diagram helpers ---------- */
503
+ var SVGNS = 'http://www.w3.org/2000/svg';
504
+ var TONE_VAR = { ok: 'var(--ok)', accent: 'var(--accent)', warn: 'var(--warn)', danger: 'var(--danger)', dim: 'var(--dim)' };
505
+ function svgEl(tag, attrs) {
506
+ var node = document.createElementNS(SVGNS, tag);
507
+ if (attrs) {
508
+ Object.keys(attrs).forEach(function (k) { node.setAttribute(k, attrs[k]); });
509
+ }
510
+ return node;
511
+ }
512
+ function attachTitle(node, text) {
513
+ var t = svgEl('title');
514
+ t.textContent = text;
515
+ node.appendChild(t);
516
+ return node;
517
+ }
518
+ function toneOf(status) {
519
+ var s = String(status || '').toLowerCase();
520
+ if (s === 'done' || s === 'complete' || s === 'completed') return 'ok';
521
+ if (s.indexOf('progress') !== -1 || s === 'review') return 'accent';
522
+ if (s.indexOf('block') !== -1 || s === 'rejected') return 'danger';
523
+ if (s.indexOf('hold') !== -1 || s.indexOf('wait') !== -1 || s === 'draft') return 'warn';
524
+ return 'dim';
525
+ }
526
+ function setHoverStatus(status) {
527
+ state.hoverStatus = status || null;
528
+ renderTasks();
529
+ }
530
+
531
+ function renderDonut(mount, statuses) {
532
+ mount.textContent = '';
533
+ var total = statuses.reduce(function (sum, s) { return sum + s.count; }, 0);
534
+ var wrap = el('div', 'donut-wrap');
535
+ var size = 220, r = 76, cx = size / 2, cy = size / 2, sw = 34;
536
+ var circ = 2 * Math.PI * r;
537
+ var svg = svgEl('svg', {
538
+ viewBox: '0 0 ' + size + ' ' + size, width: '220', height: '220',
539
+ 'class': 'donut', role: 'img', 'aria-label': 'Tasks per status'
540
+ });
541
+ svg.appendChild(svgEl('circle', { cx: cx, cy: cy, r: r, fill: 'none', stroke: 'var(--surface-2)', 'stroke-width': sw }));
542
+ var offset = 0;
543
+ statuses.forEach(function (s) {
544
+ if (total <= 0 || s.count <= 0) return;
545
+ var frac = s.count / total;
546
+ var seg = svgEl('circle', {
547
+ cx: cx, cy: cy, r: r, fill: 'none',
548
+ 'class': 'seg', 'data-status': s.status,
549
+ stroke: TONE_VAR[toneOf(s.status)], 'stroke-width': sw,
550
+ 'stroke-dasharray': (frac * circ).toFixed(2) + ' ' + (circ - frac * circ).toFixed(2),
551
+ 'stroke-dashoffset': (-offset * circ).toFixed(2),
552
+ transform: 'rotate(-90 ' + cx + ' ' + cy + ')'
553
+ });
554
+ attachTitle(seg, s.status + ': ' + s.count);
555
+ seg.addEventListener('mouseenter', function () { setHoverStatus(s.status); });
556
+ seg.addEventListener('mouseleave', function () { setHoverStatus(null); });
557
+ seg.addEventListener('click', function () {
558
+ setStatusFilter(state.status === s.status ? null : s.status);
559
+ });
560
+ svg.appendChild(seg);
561
+ offset += frac;
562
+ });
563
+ var num = svgEl('text', { x: cx, y: cy - 1, 'text-anchor': 'middle', 'class': 'donut-total' });
564
+ num.textContent = String(total);
565
+ var cap = svgEl('text', { x: cx, y: cy + 19, 'text-anchor': 'middle', 'class': 'donut-caption' });
566
+ cap.textContent = 'TASKS';
567
+ svg.appendChild(num);
568
+ svg.appendChild(cap);
569
+ wrap.appendChild(svg);
570
+ var list = el('ul', 'legend-list');
571
+ statuses.forEach(function (s) {
572
+ var li = el('li');
573
+ li.setAttribute('data-status', s.status);
574
+ var swatch = el('span', 'swatch');
575
+ swatch.style.background = TONE_VAR[toneOf(s.status)];
576
+ li.appendChild(swatch);
577
+ li.appendChild(el('span', '', s.status + ' (' + s.count + ')'));
578
+ li.addEventListener('mouseenter', function () { setHoverStatus(s.status); });
579
+ li.addEventListener('mouseleave', function () { setHoverStatus(null); });
580
+ list.appendChild(li);
581
+ });
582
+ wrap.appendChild(list);
583
+ mount.appendChild(wrap);
584
+ }
585
+
586
+ function renderBars(mount, milestones) {
587
+ mount.textContent = '';
588
+ if (milestones.length === 0) {
589
+ mount.appendChild(el('p', 'hint', 'No milestones defined.'));
590
+ return;
591
+ }
592
+ var labelW = 170, valW = 56, padX = 12, rowH = 42, trackW = 420;
593
+ var w = labelW + trackW + valW + padX * 2;
594
+ var h = milestones.length * rowH + 8;
595
+ var svg = svgEl('svg', {
596
+ viewBox: '0 0 ' + w + ' ' + h, width: '100%', 'class': 'bars',
597
+ role: 'img', 'aria-label': 'Progress per milestone'
598
+ });
599
+ milestones.forEach(function (m, i) {
600
+ var g = attachTitle(svgEl('g'), m.name + ': ' + m.done + '/' + m.total + ' done');
601
+ var y = i * rowH + 22;
602
+ var lbl = svgEl('text', { x: labelW - 12, y: y, 'text-anchor': 'end', 'class': 'bar-label' });
603
+ lbl.textContent = m.name;
604
+ g.appendChild(lbl);
605
+ g.appendChild(svgEl('rect', { x: labelW, y: y - 11, width: trackW, height: 13, rx: 6.5, 'class': 'bar-track' }));
606
+ var frac = m.total > 0 ? m.done / m.total : 0;
607
+ if (frac > 0) {
608
+ g.appendChild(svgEl('rect', {
609
+ x: labelW, y: y - 11, width: Math.max(trackW * frac, 10), height: 13, rx: 6.5,
610
+ 'class': frac >= 1 ? 'bar-fill complete' : 'bar-fill'
611
+ }));
612
+ }
613
+ var val = svgEl('text', { x: labelW + trackW + 14, y: y, 'class': 'bar-val' });
614
+ val.textContent = m.done + '/' + m.total;
615
+ g.appendChild(val);
616
+ svg.appendChild(g);
617
+ });
618
+ mount.appendChild(svg);
619
+ }
620
+
621
+ function renderSparkline(mount, activity) {
622
+ mount.textContent = '';
623
+ if (!activity || activity.length < 2) {
624
+ mount.appendChild(el('p', 'hint', 'No activity data.'));
625
+ return;
626
+ }
627
+ var w = 780, h = 150, padL = 14, padR = 14, padT = 18, padB = 26;
628
+ var max = 1;
629
+ activity.forEach(function (b) { if (b.count > max) max = b.count; });
630
+ function px(i) { return padL + (w - padL - padR) * i / (activity.length - 1); }
631
+ function py(c) { return padT + (h - padT - padB) * (1 - c / max); }
632
+ var svg = svgEl('svg', {
633
+ viewBox: '0 0 ' + w + ' ' + h, width: '100%', 'class': 'spark',
634
+ role: 'img', 'aria-label': 'Task activity over the last 30 days'
635
+ });
636
+ var coords = activity.map(function (b, i) {
637
+ return px(i).toFixed(1) + ',' + py(b.count).toFixed(1);
638
+ });
639
+ var areaD = 'M' + px(0).toFixed(1) + ',' + (h - padB) +
640
+ ' L ' + coords.join(' L ') +
641
+ ' L ' + px(activity.length - 1).toFixed(1) + ',' + (h - padB) + ' Z';
642
+ svg.appendChild(svgEl('path', { d: areaD, 'class': 'spark-area' }));
643
+ svg.appendChild(svgEl('polyline', { points: coords.join(' '), 'class': 'spark-line' }));
644
+ activity.forEach(function (b, i) {
645
+ var dot = svgEl('circle', {
646
+ cx: px(i).toFixed(1), cy: py(b.count).toFixed(1),
647
+ r: b.count > 0 ? 3.5 : 2,
648
+ 'class': b.count > 0 ? 'spark-dot nonzero' : 'spark-dot'
649
+ });
650
+ attachTitle(dot, b.date + ': ' + b.count);
651
+ svg.appendChild(dot);
652
+ });
653
+ var first = svgEl('text', { x: padL, y: h - 7, 'class': 'spark-axis' });
654
+ first.textContent = activity[0].date;
655
+ var last = svgEl('text', { x: w - padR, y: h - 7, 'text-anchor': 'end', 'class': 'spark-axis' });
656
+ last.textContent = activity[activity.length - 1].date;
657
+ svg.appendChild(first);
658
+ svg.appendChild(last);
659
+ mount.appendChild(svg);
660
+ }
661
+
662
+ function renderStepper(mount, phases) {
663
+ mount.textContent = '';
664
+ if (!phases || phases.length === 0) {
665
+ mount.appendChild(el('p', 'hint', 'Pipeline unavailable.'));
666
+ return;
667
+ }
668
+ var wrap = el('div', 'stepper');
669
+ phases.forEach(function (p) {
670
+ var step = el('div', 'step' + (/gate/i.test(String(p.name)) ? ' gate' : ''));
671
+ step.setAttribute('data-phase', p.n);
672
+ step.appendChild(el('div', 'step-num', String(p.n)));
673
+ var lab = el('div', 'step-label');
674
+ var b = document.createElement('b');
675
+ b.textContent = p.name;
676
+ lab.appendChild(b);
677
+ lab.appendChild(document.createTextNode(p.gate));
678
+ step.appendChild(lab);
679
+ wrap.appendChild(step);
680
+ });
681
+ mount.appendChild(wrap);
682
+ }
683
+
684
+ var phases = [];
685
+ try {
686
+ var phasesNode = document.getElementById('sbl-phases');
687
+ if (phasesNode) phases = JSON.parse(phasesNode.textContent);
688
+ } catch (e) {
689
+ phases = [];
690
+ }
691
+ if ($('#donut')) renderDonut($('#donut'), data.statuses);
692
+ if ($('#bars')) renderBars($('#bars'), data.milestones);
693
+ if ($('#stepper')) renderStepper($('#stepper'), phases);
694
+ if ($('#spark')) renderSparkline($('#spark'), data.activity);
695
+
696
+ /* ---------- Glossary tooltips ---------- */
697
+ var glossary = {};
698
+ try {
699
+ JSON.parse(document.getElementById('sbl-glossary').textContent).forEach(function (entry) {
700
+ glossary[String(entry.term).toLowerCase()] = entry.definition;
701
+ });
702
+ } catch (e) {
703
+ glossary = {};
704
+ }
705
+ var tip = document.getElementById('sbl-tip');
706
+ function tipTarget(node) {
707
+ return node instanceof Element ? node.closest('[data-tip],[data-term]') : null;
708
+ }
709
+ function tipTextFor(target) {
710
+ if (target.hasAttribute('data-tip')) return target.getAttribute('data-tip');
711
+ var key = String(target.getAttribute('data-term')).toLowerCase();
712
+ return glossary[key] === undefined ? '' : target.getAttribute('data-term') + ': ' + glossary[key];
713
+ }
714
+ function tipShow(text, x, y) {
715
+ if (!tip) return;
716
+ if (!text) { tip.hidden = true; return; }
717
+ tip.textContent = text;
718
+ tip.hidden = false;
719
+ var rect = tip.getBoundingClientRect();
720
+ var px = Math.min(Math.max(8, x + 14), window.innerWidth - rect.width - 8);
721
+ var py = y + 18 + rect.height > window.innerHeight - 8 ? y - rect.height - 12 : y + 16;
722
+ py = Math.min(Math.max(8, py), window.innerHeight - rect.height - 8);
723
+ tip.style.left = px + 'px';
724
+ tip.style.top = py + 'px';
725
+ }
726
+ function tipHide() {
727
+ if (tip) tip.hidden = true;
728
+ }
729
+ if (tip) {
730
+ document.addEventListener('mouseover', function (ev) {
731
+ var t = tipTarget(ev.target);
732
+ if (!t) return;
733
+ tipShow(tipTextFor(t), ev.clientX, ev.clientY);
734
+ });
735
+ document.addEventListener('mousemove', function (ev) {
736
+ if (tip.hidden) return;
737
+ var t = tipTarget(ev.target);
738
+ if (!t) {
739
+ tipHide();
740
+ return;
741
+ }
742
+ tipShow(tipTextFor(t), ev.clientX, ev.clientY);
743
+ });
744
+ document.addEventListener('focusin', function (ev) {
745
+ var t = tipTarget(ev.target);
746
+ if (!t) return;
747
+ var r = t.getBoundingClientRect();
748
+ tipShow(tipTextFor(t), r.left + r.width / 2, r.bottom);
749
+ });
750
+ document.addEventListener('focusout', tipHide);
751
+ }
752
+
753
+ /* ---------- Detail panel ---------- */
754
+ var panel = document.getElementById('sbl-detail');
755
+ var backdrop = document.getElementById('sbl-backdrop');
756
+ var depsOut = {};
757
+ var depsIn = {};
758
+ data.deps.forEach(function (d) {
759
+ (depsOut[d.from] = depsOut[d.from] || []).push(d.to);
760
+ (depsIn[d.to] = depsIn[d.to] || []).push(d.from);
761
+ });
762
+ function findTask(id) {
763
+ var found = null;
764
+ data.tasks.forEach(function (t) {
765
+ if (t.id === id) found = t;
766
+ });
767
+ return found;
768
+ }
769
+ function closeDetail() {
770
+ if (panel) panel.hidden = true;
771
+ if (backdrop) backdrop.hidden = true;
772
+ }
773
+ window.closeDetail = closeDetail;
774
+ function acList(task) {
775
+ var list = el('ul', 'ac-list');
776
+ task.acs.forEach(function (ac) {
777
+ var li = el('li', ac.checked ? 'ac done' : 'ac');
778
+ var box = document.createElement('input');
779
+ box.type = 'checkbox';
780
+ box.checked = ac.checked;
781
+ box.disabled = true;
782
+ li.appendChild(box);
783
+ li.appendChild(document.createTextNode(ac.text));
784
+ list.appendChild(li);
785
+ });
786
+ return list;
787
+ }
788
+ function depSection(titleText, ids) {
789
+ var block = el('div');
790
+ block.appendChild(el('h4', '', titleText));
791
+ var row = el('div', 'dep-row');
792
+ ids.filter(function (id) { return findTask(id); }).forEach(function (id) {
793
+ var btn = el('button', 'dep-link', id);
794
+ btn.type = 'button';
795
+ btn.setAttribute('data-dep', id);
796
+ btn.addEventListener('click', function () { openDetail(id); });
797
+ row.appendChild(btn);
798
+ });
799
+ if (row.children.length === 0) row.appendChild(el('p', 'detail-empty', 'None'));
800
+ block.appendChild(row);
801
+ return block;
802
+ }
803
+ function openDetail(id) {
804
+ var t = findTask(id);
805
+ if (!t || !panel) return;
806
+ panel.textContent = '';
807
+ var head = el('div', 'detail-head');
808
+ head.appendChild(el('span', 'detail-id', t.id));
809
+ var chip = el('span', 'status-chip', t.status);
810
+ chip.setAttribute('data-tone', toneOf(t.status));
811
+ head.appendChild(chip);
812
+ var closeBtn = el('button', 'detail-close', '\u00d7');
813
+ closeBtn.type = 'button';
814
+ closeBtn.setAttribute('aria-label', 'Close details');
815
+ closeBtn.addEventListener('click', closeDetail);
816
+ 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.'));
820
+ if (t.acs.length > 0) {
821
+ panel.appendChild(el('h4', '', 'Acceptance criteria'));
822
+ panel.appendChild(acList(t));
823
+ }
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;
829
+ }
830
+ window.__sblOpenDetail = openDetail;
831
+ if (backdrop) backdrop.addEventListener('click', closeDetail);
832
+
833
+ /* ---------- Keyboard shortcuts ---------- */
834
+ document.addEventListener('keydown', function (ev) {
835
+ if (ev.key === 'Escape') {
836
+ tipHide();
837
+ closeDetail();
838
+ return;
839
+ }
840
+ if (ev.key === '/') {
841
+ var active = document.activeElement;
842
+ var typing = active && (active.tagName === 'INPUT' || active.tagName === 'TEXTAREA' || active.isContentEditable);
843
+ if (typing) return;
844
+ ev.preventDefault();
845
+ var f = document.getElementById('taskfilter');
846
+ if (f) f.focus();
847
+ }
848
+ });
849
+
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) {
913
+ 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
+ var byId = {};
921
+ 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)'
961
+ });
962
+ svg.appendChild(path);
963
+ });
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);
988
+ }
989
+ });
990
+ svg.appendChild(g);
991
+ });
992
+ });
993
+ wrap.appendChild(svg);
994
+ mount.appendChild(wrap);
995
+ }
996
+
997
+ renderDepGraph($('#depgraph'), data.tasks, data.deps);
998
+ })();
999
+ </script>
1000
+ </body>
1001
+ </html>