super-backlog 1.3.0 → 1.3.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -2
- package/dist/cli.js +8 -3
- package/dist/commands/dashboard.js +40 -12
- package/dist/commands/update.js +87 -8
- package/dist/dashboard/data.js +44 -13
- package/dist/dashboard/hub.js +1 -1
- package/dist/dashboard/metrics.js +97 -0
- package/dist/lib/hub-state.js +2 -2
- package/dist/lib/self-update.js +57 -0
- package/dist/lib/version-check.js +7 -3
- package/dist/templates/dashboard.html +536 -133
- package/package.json +1 -1
|
@@ -5,6 +5,9 @@
|
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
6
6
|
<title>__PROJECT_NAME__ · Project Dashboard</title>
|
|
7
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">
|
|
8
|
+
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
9
|
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
10
|
+
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;700&family=Plus+Jakarta+Sans:wght@400;500;600;700;800&display=swap">
|
|
8
11
|
<style>
|
|
9
12
|
:root {
|
|
10
13
|
--bg:#0a0e16;
|
|
@@ -16,7 +19,7 @@
|
|
|
16
19
|
--line-strong:#2c3b57;
|
|
17
20
|
--text:#e8edf6;
|
|
18
21
|
--muted:#8fa0ba;
|
|
19
|
-
--dim:#
|
|
22
|
+
--dim:#72839f;
|
|
20
23
|
--accent:#5cc8ff;
|
|
21
24
|
--accent-dim:#234257;
|
|
22
25
|
--ok:#3ecf8e;
|
|
@@ -25,27 +28,59 @@
|
|
|
25
28
|
--warn-bg:#33260f;
|
|
26
29
|
--danger:#ff7a7a;
|
|
27
30
|
--danger-bg:#331718;
|
|
28
|
-
--
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
31
|
+
--sans:"Plus Jakarta Sans","Segoe UI",system-ui,sans-serif;
|
|
32
|
+
--mono:"JetBrains Mono",Consolas,"Courier New",monospace;
|
|
33
|
+
--ok-line:#2b5642;
|
|
34
|
+
--accent-bg:#10202e;
|
|
35
|
+
--accent-line:#274a63;
|
|
36
|
+
--warn-line:#5c4520;
|
|
37
|
+
--danger-line:#5c2c2c;
|
|
38
|
+
--backdrop:rgba(4,7,12,.65);
|
|
39
|
+
--tip-bg:rgba(13,19,32,.97);
|
|
40
|
+
--shadow-dialog:0 24px 80px rgba(0,0,0,.55);
|
|
41
|
+
--shadow-tip:0 10px 28px rgba(0,0,0,.5);
|
|
42
|
+
--glow-accent:rgba(92,200,255,.6);
|
|
43
|
+
--glow-warn:rgba(255,180,84,.25);
|
|
44
|
+
--glow-warn-soft:rgba(255,180,84,.3);
|
|
45
|
+
--focus-ring:rgba(92,200,255,.15);
|
|
46
|
+
}
|
|
47
|
+
:root[data-theme="light"] {
|
|
48
|
+
--bg:#f3f6fb;
|
|
49
|
+
--bg-glow-1: rgba(11,116,181,.05);
|
|
50
|
+
--bg-glow-2: rgba(23,122,78,.04);
|
|
51
|
+
--surface:#ffffff;
|
|
52
|
+
--surface-2:#e9eef7;
|
|
53
|
+
--line:#d7dfeb;
|
|
54
|
+
--line-strong:#b6c3d8;
|
|
55
|
+
--text:#17202f;
|
|
56
|
+
--muted:#4c5d77;
|
|
57
|
+
--dim:#5a6881;
|
|
58
|
+
--accent:#0a6ca8;
|
|
59
|
+
--accent-dim:#bcdcf0;
|
|
60
|
+
--ok:#16764b;
|
|
61
|
+
--ok-bg:#dff3e8;
|
|
62
|
+
--warn:#8a5a00;
|
|
63
|
+
--warn-bg:#fbecd2;
|
|
64
|
+
--danger:#b83a3a;
|
|
65
|
+
--danger-bg:#fbe3e3;
|
|
66
|
+
--ok-line:#9fd4b8;
|
|
67
|
+
--accent-bg:#e2f0f9;
|
|
68
|
+
--accent-line:#a8cfe6;
|
|
69
|
+
--warn-line:#e3c68e;
|
|
70
|
+
--danger-line:#eab5b5;
|
|
71
|
+
--backdrop:rgba(23,32,47,.45);
|
|
72
|
+
--tip-bg:rgba(255,255,255,.98);
|
|
73
|
+
--shadow-dialog:0 24px 80px rgba(23,32,47,.25);
|
|
74
|
+
--shadow-tip:0 10px 28px rgba(23,32,47,.18);
|
|
75
|
+
--glow-accent:rgba(11,116,181,.35);
|
|
76
|
+
--glow-warn:rgba(138,90,0,.2);
|
|
77
|
+
--glow-warn-soft:rgba(138,90,0,.25);
|
|
78
|
+
--focus-ring:rgba(11,116,181,.18);
|
|
44
79
|
}
|
|
45
80
|
* { box-sizing: border-box; margin: 0; padding: 0; }
|
|
46
81
|
html { scroll-behavior: smooth; }
|
|
47
82
|
body {
|
|
48
|
-
font-family:
|
|
83
|
+
font-family: var(--sans);
|
|
49
84
|
background:
|
|
50
85
|
radial-gradient(1100px 500px at 85% -10%, var(--bg-glow-1), transparent 60%),
|
|
51
86
|
radial-gradient(900px 500px at -10% 30%, var(--bg-glow-2), transparent 55%),
|
|
@@ -66,8 +101,15 @@
|
|
|
66
101
|
|
|
67
102
|
/* ---------- Sidebar ---------- */
|
|
68
103
|
.brand { display: flex; align-items: center; gap: 10px; margin-bottom: 6px; }
|
|
69
|
-
.brand-glyph { color: var(--accent); font-size: .8rem; line-height: 1; text-shadow: 0 0 12px
|
|
104
|
+
.brand-glyph { color: var(--accent); font-size: .8rem; line-height: 1; text-shadow: 0 0 12px var(--glow-accent); }
|
|
70
105
|
.brand b { font-size: .98rem; letter-spacing: .4px; overflow-wrap: anywhere; }
|
|
106
|
+
.theme-toggle {
|
|
107
|
+
margin-left: auto; width: 28px; height: 28px; border-radius: 50%;
|
|
108
|
+
font: inherit; font-size: .9rem; line-height: 1; cursor: pointer;
|
|
109
|
+
color: var(--muted); background: var(--surface-2); border: 1px solid var(--line-strong);
|
|
110
|
+
}
|
|
111
|
+
.theme-toggle:hover { color: var(--accent); border-color: var(--accent); }
|
|
112
|
+
.theme-toggle:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
|
|
71
113
|
.kicker {
|
|
72
114
|
color: var(--dim); font-size: .72rem; letter-spacing: 1.6px;
|
|
73
115
|
text-transform: uppercase; margin-bottom: 22px;
|
|
@@ -88,10 +130,10 @@
|
|
|
88
130
|
white-space: nowrap; cursor: pointer;
|
|
89
131
|
}
|
|
90
132
|
.pill .n { color: inherit; font-weight: 700; margin-right: 4px; }
|
|
91
|
-
.pill[data-tone="ok"] { color: var(--ok); border-color:
|
|
92
|
-
.pill[data-tone="accent"] { color: var(--accent); border-color:
|
|
93
|
-
.pill[data-tone="warn"] { color: var(--warn); border-color:
|
|
94
|
-
.pill[data-tone="danger"] { color: var(--danger); border-color:
|
|
133
|
+
.pill[data-tone="ok"] { color: var(--ok); border-color: var(--ok-line); background: var(--ok-bg); }
|
|
134
|
+
.pill[data-tone="accent"] { color: var(--accent); border-color: var(--accent-line); background: var(--accent-bg); }
|
|
135
|
+
.pill[data-tone="warn"] { color: var(--warn); border-color: var(--warn-line); background: var(--warn-bg); }
|
|
136
|
+
.pill[data-tone="danger"] { color: var(--danger); border-color: var(--danger-line); background: var(--danger-bg); }
|
|
95
137
|
.pill:hover { filter: brightness(1.25); }
|
|
96
138
|
.pill.active { outline: 2px solid var(--accent); outline-offset: 1px; }
|
|
97
139
|
.pill-empty { color: var(--dim); font-size: .78rem; }
|
|
@@ -101,9 +143,9 @@
|
|
|
101
143
|
.term:hover { color: var(--accent); border-color: var(--accent); }
|
|
102
144
|
#sbl-tip {
|
|
103
145
|
position: fixed; z-index: 70; max-width: 340px; padding: 8px 12px;
|
|
104
|
-
background:
|
|
146
|
+
background: var(--tip-bg); border: 1px solid var(--line-strong); border-radius: 8px;
|
|
105
147
|
font-size: .78rem; line-height: 1.45; color: var(--text);
|
|
106
|
-
box-shadow:
|
|
148
|
+
box-shadow: var(--shadow-tip); pointer-events: none;
|
|
107
149
|
}
|
|
108
150
|
|
|
109
151
|
/* ---------- Detail panel ---------- */
|
|
@@ -112,10 +154,10 @@
|
|
|
112
154
|
width: min(720px, 92vw); max-height: 85vh; overflow-y: auto; overflow-x: hidden; padding: 0;
|
|
113
155
|
border: 1px solid var(--line-strong); border-radius: 14px;
|
|
114
156
|
background: var(--surface); color: var(--text);
|
|
115
|
-
box-shadow:
|
|
157
|
+
box-shadow: var(--shadow-dialog);
|
|
116
158
|
}
|
|
117
159
|
#task-dialog::backdrop {
|
|
118
|
-
background:
|
|
160
|
+
background: var(--backdrop);
|
|
119
161
|
backdrop-filter: blur(2px);
|
|
120
162
|
}
|
|
121
163
|
#task-dialog[open] { animation: sbl-dialog-in .18s ease-out; }
|
|
@@ -130,10 +172,10 @@
|
|
|
130
172
|
display: none; flex-direction: column;
|
|
131
173
|
border: 1px solid var(--line-strong); border-radius: 14px;
|
|
132
174
|
background: var(--surface); color: var(--text);
|
|
133
|
-
box-shadow:
|
|
175
|
+
box-shadow: var(--shadow-dialog);
|
|
134
176
|
}
|
|
135
177
|
#backlog-dialog[open] { display: flex; animation: sbl-dialog-in .18s ease-out; }
|
|
136
|
-
#backlog-dialog::backdrop { background:
|
|
178
|
+
#backlog-dialog::backdrop { background: var(--backdrop); backdrop-filter: blur(2px); }
|
|
137
179
|
@media (prefers-reduced-motion: reduce) {
|
|
138
180
|
#backlog-dialog[open] { animation: none; }
|
|
139
181
|
}
|
|
@@ -164,10 +206,10 @@
|
|
|
164
206
|
}
|
|
165
207
|
.detail-id { font-family: var(--mono); color: var(--accent); font-weight: 700; }
|
|
166
208
|
.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; }
|
|
167
|
-
.status-chip[data-tone="ok"] { color: var(--ok); border-color:
|
|
168
|
-
.status-chip[data-tone="accent"] { color: var(--accent); border-color:
|
|
169
|
-
.status-chip[data-tone="warn"] { color: var(--warn); border-color:
|
|
170
|
-
.status-chip[data-tone="danger"] { color: var(--danger); border-color:
|
|
209
|
+
.status-chip[data-tone="ok"] { color: var(--ok); border-color: var(--ok-line); background: var(--ok-bg); }
|
|
210
|
+
.status-chip[data-tone="accent"] { color: var(--accent); border-color: var(--accent-line); background: var(--accent-bg); }
|
|
211
|
+
.status-chip[data-tone="warn"] { color: var(--warn); border-color: var(--warn-line); background: var(--warn-bg); }
|
|
212
|
+
.status-chip[data-tone="danger"] { color: var(--danger); border-color: var(--danger-line); background: var(--danger-bg); }
|
|
171
213
|
.detail-close { margin-left: auto; background: none; border: none; color: var(--dim); font-size: 1.3rem; cursor: pointer; line-height: 1; }
|
|
172
214
|
.detail-close:hover { color: var(--danger); }
|
|
173
215
|
.detail-title { font-size: 1.3rem; font-weight: 700; line-height: 1.3; margin-bottom: 10px; overflow-wrap: anywhere; }
|
|
@@ -197,7 +239,7 @@
|
|
|
197
239
|
.detail-cmd { margin-top: 18px; }
|
|
198
240
|
.dep-link {
|
|
199
241
|
font-family: var(--mono); font-size: .74rem; padding: 2px 10px; border-radius: 999px;
|
|
200
|
-
border: 1px solid
|
|
242
|
+
border: 1px solid var(--accent-line); background: var(--accent-bg); color: var(--accent); cursor: pointer;
|
|
201
243
|
}
|
|
202
244
|
.dep-link:hover { filter: brightness(1.25); }
|
|
203
245
|
|
|
@@ -213,7 +255,7 @@
|
|
|
213
255
|
background: linear-gradient(180deg, var(--accent), transparent 130%);
|
|
214
256
|
-webkit-background-clip: text; background-clip: text; color: transparent;
|
|
215
257
|
}
|
|
216
|
-
.sec-head h2 { font-size: 1.15rem; letter-spacing: .5px; }
|
|
258
|
+
.sec-head h2 { font-size: 1.15rem; letter-spacing: .5px; font-weight: 700; }
|
|
217
259
|
.sec-head .tagline { color: var(--dim); font-size: .82rem; margin-left: auto; text-align: right; }
|
|
218
260
|
.mount { min-height: 24px; }
|
|
219
261
|
|
|
@@ -228,12 +270,11 @@
|
|
|
228
270
|
}
|
|
229
271
|
.cmd-btn:hover { background: var(--surface-2); border-color: var(--accent); }
|
|
230
272
|
.cmd-btn:active { transform: translateY(1px); }
|
|
231
|
-
.cmd-title { font-size: .95rem; font-weight:
|
|
273
|
+
.cmd-title { font-size: .95rem; font-weight: 700; color: var(--text); }
|
|
232
274
|
.cmd-btn:hover .cmd-title { color: var(--accent); }
|
|
233
275
|
.cmd-line { font-family: var(--mono); font-size: .72rem; color: var(--dim); }
|
|
234
276
|
.hint { color: var(--dim); font-size: .8rem; margin-top: 12px; }
|
|
235
277
|
.drafts-list { list-style: none; margin: 0; padding: 0; display: grid; gap: 8px; }
|
|
236
|
-
.drafts-list li { background: var(--surface); border: 1px solid var(--line); border-radius: 8px; padding: 8px 12px; font-size: .88rem; }
|
|
237
278
|
code.inline {
|
|
238
279
|
font-family: var(--mono); font-size: .85em;
|
|
239
280
|
background: var(--surface-2); border: 1px solid var(--line);
|
|
@@ -246,7 +287,7 @@
|
|
|
246
287
|
background: var(--surface-2); color: var(--text); border: 1px solid var(--line-strong);
|
|
247
288
|
border-radius: 999px; padding: 7px 16px; min-width: 260px; font: inherit;
|
|
248
289
|
}
|
|
249
|
-
input[type="search"]:focus { outline: none; border-color: var(--accent-dim); box-shadow: 0 0 0 2px
|
|
290
|
+
input[type="search"]:focus { outline: none; border-color: var(--accent-dim); box-shadow: 0 0 0 2px var(--focus-ring); }
|
|
250
291
|
.table-wrap { overflow-x: auto; background: var(--surface); border: 1px solid var(--line); border-radius: 12px; }
|
|
251
292
|
table { width: 100%; border-collapse: collapse; font-size: .89rem; }
|
|
252
293
|
th, td { text-align: left; padding: 8px 14px; border-bottom: 1px solid var(--line); vertical-align: top; white-space: nowrap; }
|
|
@@ -262,7 +303,8 @@
|
|
|
262
303
|
.task-row:hover td { background: var(--surface-2); }
|
|
263
304
|
.cell-id { color: var(--accent); font-family: var(--mono); font-variant-numeric: tabular-nums; }
|
|
264
305
|
.cell-title { font-weight: 400; white-space: normal; min-width: 220px; }
|
|
265
|
-
.cell-updated { font-family: var(--mono); color: var(--muted); }
|
|
306
|
+
.cell-updated { font-family: var(--mono); color: var(--muted); text-align: center; }
|
|
307
|
+
th[data-key="updated"] { text-align: center; }
|
|
266
308
|
|
|
267
309
|
/* ---------- Footer ---------- */
|
|
268
310
|
.footer {
|
|
@@ -281,6 +323,30 @@
|
|
|
281
323
|
.legend-list li:hover { color: var(--text); }
|
|
282
324
|
.legend-list .swatch { width: 10px; height: 10px; border-radius: 3px; flex: none; }
|
|
283
325
|
|
|
326
|
+
/* ---------- KPI tiles ---------- */
|
|
327
|
+
.kpi-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(190px, 1fr)); gap: 12px; margin-bottom: 22px; }
|
|
328
|
+
.kpi {
|
|
329
|
+
display: block; text-align: left; font: inherit; color: inherit; cursor: default;
|
|
330
|
+
background: var(--surface); border: 1px solid var(--line); border-radius: 12px; padding: 14px 16px;
|
|
331
|
+
}
|
|
332
|
+
.kpi-big { font-family: var(--mono); font-size: 1.7rem; font-weight: 700; line-height: 1.1; font-variant-numeric: tabular-nums; }
|
|
333
|
+
.kpi-cap { display: block; color: var(--dim); font-size: .66rem; letter-spacing: 1.4px; text-transform: uppercase; margin-top: 4px; }
|
|
334
|
+
.kpi-sub { display: block; font-size: .78rem; color: var(--muted); margin-top: 8px; }
|
|
335
|
+
.kpi-sub[data-tone="ok"] { color: var(--ok); }
|
|
336
|
+
.kpi-sub[data-tone="danger"] { color: var(--danger); }
|
|
337
|
+
.kpi-link { font: inherit; background: none; border: none; padding: 0; color: var(--accent); cursor: pointer; font-family: var(--mono); font-size: .78rem; }
|
|
338
|
+
.kpi-link:hover { filter: brightness(1.25); }
|
|
339
|
+
.kpi-special-btn { display: block; width: 100%; text-align: left; font: inherit; color: inherit; background: none; border: none; border-radius: 8px; padding: 2px 4px; margin: -2px -4px; cursor: pointer; }
|
|
340
|
+
.kpi-special-btn:hover { background: var(--surface-2); }
|
|
341
|
+
.kpi-special-btn:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
|
|
342
|
+
.kpi-special-btn[aria-pressed="true"] { background: var(--accent-bg); }
|
|
343
|
+
/* ---------- Aging strip ---------- */
|
|
344
|
+
.aging-lane-label { fill: var(--text); font-size: 12px; font-family: var(--mono); }
|
|
345
|
+
.aging-axis { fill: var(--dim); font-size: 10px; font-family: var(--mono); }
|
|
346
|
+
.aging-track { stroke: var(--line); }
|
|
347
|
+
.age-dot { cursor: pointer; }
|
|
348
|
+
.age-dot:hover { opacity: .8; }
|
|
349
|
+
|
|
284
350
|
/* ---------- Milestone bars ---------- */
|
|
285
351
|
.bars text { font-family: var(--mono); }
|
|
286
352
|
.bar-label { fill: var(--text); font-size: 12.5px; }
|
|
@@ -289,16 +355,17 @@
|
|
|
289
355
|
.bar-fill { fill: var(--accent); }
|
|
290
356
|
.bar-fill.complete { fill: var(--ok); }
|
|
291
357
|
|
|
292
|
-
/* ----------
|
|
293
|
-
.
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
}
|
|
298
|
-
.
|
|
299
|
-
.
|
|
300
|
-
.
|
|
301
|
-
|
|
358
|
+
/* ---------- Activity heatmap ---------- */
|
|
359
|
+
.hm-cell { cursor: pointer; }
|
|
360
|
+
.hm-cell:hover { stroke: var(--accent); stroke-width: 1; }
|
|
361
|
+
.hm-0 { fill: var(--surface-2); }
|
|
362
|
+
.hm-1 { fill: var(--accent); opacity: .3; }
|
|
363
|
+
.hm-2 { fill: var(--accent); opacity: .55; }
|
|
364
|
+
.hm-3 { fill: var(--accent); opacity: .8; }
|
|
365
|
+
.hm-4 { fill: var(--accent); }
|
|
366
|
+
.hm-axis { fill: var(--dim); font-size: 9px; font-family: var(--mono); }
|
|
367
|
+
#day-panel { margin-top: 16px; }
|
|
368
|
+
#day-panel h4 { font-family: var(--mono); }
|
|
302
369
|
|
|
303
370
|
/* ---------- Pipeline stepper ---------- */
|
|
304
371
|
.stepper { display: grid; grid-template-columns: repeat(9, minmax(0, 1fr)); gap: 2px; margin: 4px 0 6px; }
|
|
@@ -322,7 +389,7 @@
|
|
|
322
389
|
border-radius: 50%; font-family: var(--mono); font-size: .9rem; font-weight: 700;
|
|
323
390
|
background: var(--surface-2); border: 2px solid var(--line-strong); color: var(--muted);
|
|
324
391
|
}
|
|
325
|
-
.step.gate .step-num { border-color: var(--warn); color: var(--warn); background: var(--warn-bg); box-shadow: 0 0 14px
|
|
392
|
+
.step.gate .step-num { border-color: var(--warn); color: var(--warn); background: var(--warn-bg); box-shadow: 0 0 14px var(--glow-warn); }
|
|
326
393
|
.step-label {
|
|
327
394
|
display: block; font-size: .8rem; font-weight: 600; color: var(--text); line-height: 1.3;
|
|
328
395
|
overflow-wrap: break-word;
|
|
@@ -367,25 +434,18 @@
|
|
|
367
434
|
color: var(--warn); background: var(--warn-bg); border: 1px solid var(--warn);
|
|
368
435
|
border-radius: 999px; padding: 2px 9px; transition: box-shadow .15s ease;
|
|
369
436
|
}
|
|
370
|
-
.update-badge:hover { box-shadow: 0 0 10px
|
|
437
|
+
.update-badge:hover { box-shadow: 0 0 10px var(--glow-warn-soft); }
|
|
371
438
|
.update-badge:focus-visible { outline: 2px solid var(--warn); outline-offset: 2px; }
|
|
372
439
|
.update-badge .cmd-title { font-size: inherit; font-weight: 600; color: inherit; }
|
|
373
|
-
.side-models {
|
|
374
|
-
display: block; margin: 4px 0 0; padding: 2px 0; cursor: pointer; text-align: left;
|
|
375
|
-
font: inherit; font-family: var(--mono); font-size: .78rem; color: var(--muted);
|
|
376
|
-
background: none; border: none;
|
|
377
|
-
}
|
|
378
|
-
.side-models:hover { color: var(--accent); }
|
|
379
|
-
.side-models:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
|
|
380
440
|
|
|
381
441
|
/* ---------- Models modal ---------- */
|
|
382
442
|
#models-dialog {
|
|
383
443
|
width: min(560px, 92vw); max-height: 80vh; overflow-y: auto; overflow-x: hidden; padding: 0;
|
|
384
444
|
border: 1px solid var(--line-strong); border-radius: 14px;
|
|
385
445
|
background: var(--surface); color: var(--text);
|
|
386
|
-
box-shadow:
|
|
446
|
+
box-shadow: var(--shadow-dialog);
|
|
387
447
|
}
|
|
388
|
-
#models-dialog::backdrop { background:
|
|
448
|
+
#models-dialog::backdrop { background: var(--backdrop); backdrop-filter: blur(2px); }
|
|
389
449
|
#models-dialog[open] { animation: sbl-dialog-in .18s ease-out; }
|
|
390
450
|
@media (prefers-reduced-motion: reduce) {
|
|
391
451
|
#models-dialog[open] { animation: none; }
|
|
@@ -426,23 +486,35 @@
|
|
|
426
486
|
main { padding: 24px 20px 40px; }
|
|
427
487
|
}
|
|
428
488
|
</style>
|
|
489
|
+
<script>
|
|
490
|
+
(function () {
|
|
491
|
+
var theme = null;
|
|
492
|
+
try { theme = localStorage.getItem('sbl-theme'); } catch (e) { theme = null; }
|
|
493
|
+
if (theme !== 'light' && theme !== 'dark') {
|
|
494
|
+
theme = window.matchMedia && window.matchMedia('(prefers-color-scheme: light)').matches ? 'light' : 'dark';
|
|
495
|
+
}
|
|
496
|
+
document.documentElement.setAttribute('data-theme', theme);
|
|
497
|
+
})();
|
|
498
|
+
</script>
|
|
429
499
|
</head>
|
|
430
500
|
<body>
|
|
431
501
|
<div class="layout">
|
|
432
502
|
|
|
433
503
|
<aside class="sbl-side">
|
|
434
|
-
<div class="brand"><span class="brand-glyph">●</span><b>__PROJECT_NAME__</b
|
|
504
|
+
<div class="brand"><span class="brand-glyph">●</span><b>__PROJECT_NAME__</b>
|
|
505
|
+
<button type="button" id="theme-toggle" class="theme-toggle" aria-label="Switch color theme">◐</button>
|
|
506
|
+
</div>
|
|
435
507
|
<div class="kicker">SUPERPOWERS × BACKLOG.MD</div>
|
|
436
508
|
<div class="side-version" id="side-version"><span>v__KIT_VERSION__</span></div>
|
|
437
|
-
<button type="button" id="models-btn" class="side-models">model router</button>
|
|
438
509
|
<nav aria-label="Dashboard sections">
|
|
439
510
|
<a href="#sec-01"><span class="n">01</span>Board & Quick Actions</a>
|
|
440
511
|
<a href="#sec-02"><span class="n">02</span>Status</a>
|
|
441
|
-
<a href="#sec-03"><span class="n">03</span>
|
|
442
|
-
<a href="#sec-04"><span class="n">04</span>
|
|
443
|
-
<a href="#sec-05"><span class="n">05</span>
|
|
444
|
-
<a href="#sec-06"><span class="n">06</span>
|
|
445
|
-
<a href="#sec-07"><span class="n">07</span>
|
|
512
|
+
<a href="#sec-03"><span class="n">03</span>Feature Cycle</a>
|
|
513
|
+
<a href="#sec-04"><span class="n">04</span>Milestones</a>
|
|
514
|
+
<a href="#sec-05"><span class="n">05</span>Drafts</a>
|
|
515
|
+
<a href="#sec-06"><span class="n">06</span>Tasks</a>
|
|
516
|
+
<a href="#sec-07"><span class="n">07</span>Activity</a>
|
|
517
|
+
<a href="#sec-08"><span class="n">08</span>Decisions & Docs</a>
|
|
446
518
|
</nav>
|
|
447
519
|
<div class="legend">
|
|
448
520
|
<h4>Status</h4>
|
|
@@ -459,27 +531,42 @@
|
|
|
459
531
|
<div id="quickactions" class="mount">
|
|
460
532
|
<div class="cmd-row" id="cmd-buttons">
|
|
461
533
|
<button type="button" class="cmd-btn" id="backlog-btn"><span class="cmd-title">Backlog</span><span class="cmd-line">board · tasks · docs · decisions</span></button>
|
|
534
|
+
<button type="button" class="cmd-btn" id="models-btn"><span class="cmd-title">Model Router</span><span class="cmd-line">workhorse · budget · tiers</span></button>
|
|
462
535
|
</div>
|
|
463
536
|
<p class="hint">Opens the Backlog.md browser in an overlay — served locally per project, started on demand.</p>
|
|
464
537
|
</div>
|
|
465
|
-
<div id="drafts" class="mount">
|
|
466
|
-
<h3 class="sub-head">Drafts</h3>
|
|
467
|
-
<ul id="drafts-list" class="drafts-list"></ul>
|
|
468
|
-
</div>
|
|
469
538
|
</section>
|
|
470
539
|
|
|
471
540
|
<section id="sec-02">
|
|
472
|
-
<div class="sec-head"><span class="sec-num">02</span><h2>Status</h2><span class="tagline">
|
|
541
|
+
<div class="sec-head"><span class="sec-num">02</span><h2>Status</h2><span class="tagline">progress · velocity · wip · age</span></div>
|
|
542
|
+
<div id="kpis" class="mount"></div>
|
|
473
543
|
<div id="donut" class="mount"></div>
|
|
544
|
+
<h3 class="sub-head">Aging — open tasks by days in the backlog</h3>
|
|
545
|
+
<div id="aging" class="mount"></div>
|
|
474
546
|
</section>
|
|
475
547
|
|
|
476
548
|
<section id="sec-03">
|
|
477
|
-
<div class="sec-head"><span class="sec-num">03</span><h2>
|
|
478
|
-
<div id="
|
|
549
|
+
<div class="sec-head"><span class="sec-num">03</span><h2>Feature Cycle</h2><span class="tagline">idea → merge · every <span class="term" data-term="Review Gate">Review Gate</span> included · click a step for details</span></div>
|
|
550
|
+
<div id="stepper" class="mount"></div>
|
|
551
|
+
<div id="phase-detail" hidden></div>
|
|
552
|
+
<h3 class="sub-head">Flow</h3>
|
|
553
|
+
<div id="depgraph" class="mount"></div>
|
|
479
554
|
</section>
|
|
480
555
|
|
|
481
556
|
<section id="sec-04">
|
|
482
|
-
<div class="sec-head"><span class="sec-num">04</span><h2>
|
|
557
|
+
<div class="sec-head"><span class="sec-num">04</span><h2>Milestones</h2><span class="tagline">done / total per <span class="term" data-term="Milestone">Milestone</span></span></div>
|
|
558
|
+
<div id="bars" class="mount"></div>
|
|
559
|
+
</section>
|
|
560
|
+
|
|
561
|
+
<section id="sec-05">
|
|
562
|
+
<div class="sec-head"><span class="sec-num">05</span><h2>Drafts</h2><span class="tagline">ideas before they enter the backlog · click a card for details</span></div>
|
|
563
|
+
<div id="drafts" class="mount">
|
|
564
|
+
<ul id="drafts-list" class="drafts-list"></ul>
|
|
565
|
+
</div>
|
|
566
|
+
</section>
|
|
567
|
+
|
|
568
|
+
<section id="sec-06">
|
|
569
|
+
<div class="sec-head"><span class="sec-num">06</span><h2>Tasks</h2><span class="tagline">sort · filter · click row for details</span></div>
|
|
483
570
|
<div id="tasks" class="mount">
|
|
484
571
|
<div class="toolbar">
|
|
485
572
|
<input id="taskfilter" type="search" placeholder="Filter tasks…" aria-label="Filter tasks">
|
|
@@ -503,21 +590,15 @@
|
|
|
503
590
|
</div>
|
|
504
591
|
</section>
|
|
505
592
|
|
|
506
|
-
<section id="sec-
|
|
507
|
-
<div class="sec-head"><span class="sec-num">
|
|
508
|
-
<div id="
|
|
509
|
-
<div id="
|
|
510
|
-
<
|
|
511
|
-
<div id="depgraph" class="mount"></div>
|
|
512
|
-
</section>
|
|
513
|
-
|
|
514
|
-
<section id="sec-06">
|
|
515
|
-
<div class="sec-head"><span class="sec-num">06</span><h2>Activity</h2><span class="tagline">last 30 days</span></div>
|
|
516
|
-
<div id="spark" class="mount"></div>
|
|
593
|
+
<section id="sec-07">
|
|
594
|
+
<div class="sec-head"><span class="sec-num">07</span><h2>Activity</h2><span class="tagline">last 26 weeks · click a day for its tasks</span></div>
|
|
595
|
+
<div id="activity-kpis" class="mount"></div>
|
|
596
|
+
<div id="heatmap" class="mount"></div>
|
|
597
|
+
<div id="day-panel" class="flow-block" hidden></div>
|
|
517
598
|
</section>
|
|
518
599
|
|
|
519
|
-
<section id="sec-
|
|
520
|
-
<div class="sec-head"><span class="sec-num">
|
|
600
|
+
<section id="sec-08">
|
|
601
|
+
<div class="sec-head"><span class="sec-num">08</span><h2>Decisions & Docs</h2><span class="tagline">decisions · docs · glossary</span></div>
|
|
521
602
|
<div id="docs" class="mount"></div>
|
|
522
603
|
<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 — extend them via <code class="inline">backlog/docs/glossary.md</code>.</p>
|
|
523
604
|
</section>
|
|
@@ -578,13 +659,13 @@
|
|
|
578
659
|
var upd = el('button', 'update-badge');
|
|
579
660
|
upd.type = 'button';
|
|
580
661
|
upd.appendChild(el('span', 'cmd-title', 'v' + data.latestVersion + ' available'));
|
|
581
|
-
upd.setAttribute('data-tip', 'Update: npm i -g super-backlog
|
|
662
|
+
upd.setAttribute('data-tip', 'Update: run sbl update (click copies npm i -g super-backlog)');
|
|
582
663
|
upd.addEventListener('click', function () { copyCommand(upd, 'npm i -g super-backlog'); });
|
|
583
664
|
sideVersion.appendChild(upd);
|
|
584
665
|
}
|
|
585
666
|
|
|
586
667
|
var KEYS = ['id', 'title', 'status', 'milestone', 'priority', 'assignee', 'updated'];
|
|
587
|
-
var state = { key: 'id', dir: 1, query: '', status: null, hoverStatus: null };
|
|
668
|
+
var state = { key: 'id', dir: 1, query: '', status: null, hoverStatus: null, special: null };
|
|
588
669
|
function field(task, key) {
|
|
589
670
|
var v = task[key];
|
|
590
671
|
return v === undefined || v === null ? '' : String(v);
|
|
@@ -595,6 +676,8 @@
|
|
|
595
676
|
function matches(task) {
|
|
596
677
|
var eff = activeStatus();
|
|
597
678
|
if (eff && field(task, 'status').toLowerCase() !== eff.toLowerCase()) return false;
|
|
679
|
+
if (state.special === 'wip' && !isWipStatus(task.status)) return false;
|
|
680
|
+
if (state.special === 'blocked' && !isBlockedTask(task)) return false;
|
|
598
681
|
if (!state.query) return true;
|
|
599
682
|
var hit = KEYS.some(function (k) {
|
|
600
683
|
return field(task, k).toLowerCase().indexOf(state.query) !== -1;
|
|
@@ -603,13 +686,68 @@
|
|
|
603
686
|
return ac.text.toLowerCase().indexOf(state.query) !== -1;
|
|
604
687
|
});
|
|
605
688
|
}
|
|
689
|
+
function isWipStatus(status) {
|
|
690
|
+
var s = String(status || '').toLowerCase();
|
|
691
|
+
return s.indexOf('progress') !== -1 || s.indexOf('review') !== -1;
|
|
692
|
+
}
|
|
693
|
+
function isBlockedTask(task) {
|
|
694
|
+
var s = String(task.status || '').toLowerCase();
|
|
695
|
+
if (isDoneStatus(task.status)) return false;
|
|
696
|
+
if (s.indexOf('block') !== -1) return true;
|
|
697
|
+
if (!depsOut) return false;
|
|
698
|
+
var blockers = depsOut[task.id] || [];
|
|
699
|
+
for (var i = 0; i < blockers.length; i++) {
|
|
700
|
+
var dep = findTask(blockers[i]);
|
|
701
|
+
if (dep && !isDoneStatus(dep.status)) return true;
|
|
702
|
+
}
|
|
703
|
+
return false;
|
|
704
|
+
}
|
|
705
|
+
function setSpecialFilter(kind) {
|
|
706
|
+
state.special = state.special === kind ? null : kind;
|
|
707
|
+
state.status = null;
|
|
708
|
+
document.querySelectorAll('#pills .pill').forEach(function (pill) { pill.classList.remove('active'); });
|
|
709
|
+
document.querySelectorAll('[data-special]').forEach(function (tile) {
|
|
710
|
+
tile.setAttribute('aria-pressed', String(state.special === tile.getAttribute('data-special')));
|
|
711
|
+
});
|
|
712
|
+
renderTasks();
|
|
713
|
+
}
|
|
606
714
|
var tbody = $('#task-rows');
|
|
715
|
+
function compareTasks(a, b, key) {
|
|
716
|
+
if (key === 'updated') {
|
|
717
|
+
var ta = Date.parse(field(a, key)) || 0;
|
|
718
|
+
var tb = Date.parse(field(b, key)) || 0;
|
|
719
|
+
if (ta !== tb) return ta - tb;
|
|
720
|
+
}
|
|
721
|
+
return field(a, key).localeCompare(field(b, key), undefined, { numeric: true, sensitivity: 'base' });
|
|
722
|
+
}
|
|
723
|
+
var relFmt = window.Intl && Intl.RelativeTimeFormat ? new Intl.RelativeTimeFormat(undefined, { numeric: 'auto' }) : null;
|
|
724
|
+
var exactFmt = window.Intl && Intl.DateTimeFormat ? new Intl.DateTimeFormat(undefined, { dateStyle: 'medium', timeStyle: 'short' }) : null;
|
|
725
|
+
var REL_UNITS = [['year', 31536000], ['month', 2592000], ['week', 604800], ['day', 86400], ['hour', 3600], ['minute', 60]];
|
|
726
|
+
function parseWhen(value) {
|
|
727
|
+
if (!value) return null;
|
|
728
|
+
/* date-only strings get a local noon so the day never shifts across timezones */
|
|
729
|
+
var t = Date.parse(/^\d{4}-\d{2}-\d{2}$/.test(value) ? value + 'T12:00:00' : value);
|
|
730
|
+
return isNaN(t) ? null : t;
|
|
731
|
+
}
|
|
732
|
+
function formatRelative(value) {
|
|
733
|
+
var t = parseWhen(value);
|
|
734
|
+
if (t === null || !relFmt) return value;
|
|
735
|
+
var diff = (t - Date.now()) / 1000;
|
|
736
|
+
for (var i = 0; i < REL_UNITS.length; i++) {
|
|
737
|
+
if (Math.abs(diff) >= REL_UNITS[i][1] || i === REL_UNITS.length - 1) {
|
|
738
|
+
return relFmt.format(Math.round(diff / REL_UNITS[i][1]), REL_UNITS[i][0]);
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
return value;
|
|
742
|
+
}
|
|
743
|
+
function formatExact(value) {
|
|
744
|
+
var t = parseWhen(value);
|
|
745
|
+
return t === null || !exactFmt ? '' : exactFmt.format(new Date(t));
|
|
746
|
+
}
|
|
607
747
|
function renderTasks() {
|
|
608
748
|
var rows = data.tasks
|
|
609
749
|
.filter(matches)
|
|
610
|
-
.sort(function (a, b) {
|
|
611
|
-
return field(a, state.key).localeCompare(field(b, state.key)) * state.dir;
|
|
612
|
-
});
|
|
750
|
+
.sort(function (a, b) { return compareTasks(a, b, state.key) * state.dir; });
|
|
613
751
|
tbody.textContent = '';
|
|
614
752
|
if (rows.length === 0) {
|
|
615
753
|
var emptyTr = el('tr');
|
|
@@ -623,7 +761,19 @@
|
|
|
623
761
|
var tr = el('tr', 'task-row');
|
|
624
762
|
tr.setAttribute('data-task', task.id);
|
|
625
763
|
KEYS.forEach(function (k) {
|
|
626
|
-
|
|
764
|
+
var td = el('td', 'cell-' + k);
|
|
765
|
+
if (k === 'status') {
|
|
766
|
+
var chip = el('span', 'status-chip', field(task, k));
|
|
767
|
+
chip.setAttribute('data-tone', toneOf(task.status));
|
|
768
|
+
td.appendChild(chip);
|
|
769
|
+
} else if (k === 'updated') {
|
|
770
|
+
td.textContent = formatRelative(field(task, k));
|
|
771
|
+
var exact = formatExact(field(task, k));
|
|
772
|
+
if (exact) td.setAttribute('data-tip', exact);
|
|
773
|
+
} else {
|
|
774
|
+
td.textContent = field(task, k);
|
|
775
|
+
}
|
|
776
|
+
tr.appendChild(td);
|
|
627
777
|
});
|
|
628
778
|
tr.addEventListener('click', function () { openDetail(task.id); });
|
|
629
779
|
tbody.appendChild(tr);
|
|
@@ -657,9 +807,13 @@
|
|
|
657
807
|
}
|
|
658
808
|
function setStatusFilter(status) {
|
|
659
809
|
state.status = status || null;
|
|
810
|
+
state.special = null;
|
|
660
811
|
document.querySelectorAll('#pills .pill').forEach(function (pill) {
|
|
661
812
|
pill.classList.toggle('active', !!state.status && pill.getAttribute('data-status') === state.status);
|
|
662
813
|
});
|
|
814
|
+
document.querySelectorAll('[data-special]').forEach(function (tile) {
|
|
815
|
+
tile.setAttribute('aria-pressed', 'false');
|
|
816
|
+
});
|
|
663
817
|
renderTasks();
|
|
664
818
|
}
|
|
665
819
|
document.querySelectorAll('#pills .pill').forEach(function (pill) {
|
|
@@ -719,6 +873,24 @@
|
|
|
719
873
|
});
|
|
720
874
|
}
|
|
721
875
|
|
|
876
|
+
/* ---------- Theme toggle ---------- */
|
|
877
|
+
var themeToggle = document.getElementById('theme-toggle');
|
|
878
|
+
function syncThemeToggle() {
|
|
879
|
+
if (!themeToggle) return;
|
|
880
|
+
var mode = document.documentElement.getAttribute('data-theme');
|
|
881
|
+
themeToggle.textContent = mode === 'light' ? '☾' : '☀';
|
|
882
|
+
themeToggle.setAttribute('aria-label', mode === 'light' ? 'Switch to dark theme' : 'Switch to light theme');
|
|
883
|
+
}
|
|
884
|
+
if (themeToggle) {
|
|
885
|
+
themeToggle.addEventListener('click', function () {
|
|
886
|
+
var next = document.documentElement.getAttribute('data-theme') === 'light' ? 'dark' : 'light';
|
|
887
|
+
document.documentElement.setAttribute('data-theme', next);
|
|
888
|
+
try { localStorage.setItem('sbl-theme', next); } catch (e) { /* private mode */ }
|
|
889
|
+
syncThemeToggle();
|
|
890
|
+
});
|
|
891
|
+
syncThemeToggle();
|
|
892
|
+
}
|
|
893
|
+
|
|
722
894
|
/* ---------- Models modal ---------- */
|
|
723
895
|
var modelsBtn = document.getElementById('models-btn');
|
|
724
896
|
var modelsDialog = document.getElementById('models-dialog');
|
|
@@ -802,16 +974,6 @@
|
|
|
802
974
|
});
|
|
803
975
|
}
|
|
804
976
|
|
|
805
|
-
/* ---------- Drafts ---------- */
|
|
806
|
-
function renderDrafts(drafts) {
|
|
807
|
-
var list = document.getElementById('drafts-list');
|
|
808
|
-
if (!list) return;
|
|
809
|
-
list.textContent = '';
|
|
810
|
-
if (!drafts || drafts.length === 0) { list.appendChild(el('li', '', 'No drafts.')); return; }
|
|
811
|
-
drafts.forEach(function (d) { list.appendChild(el('li', '', d.id + ' \u2014 ' + d.title)); });
|
|
812
|
-
}
|
|
813
|
-
renderDrafts(data.drafts);
|
|
814
|
-
|
|
815
977
|
/* ---------- Shared diagram helpers ---------- */
|
|
816
978
|
var SVGNS = 'http://www.w3.org/2000/svg';
|
|
817
979
|
var TONE_VAR = { ok: 'var(--ok)', accent: 'var(--accent)', warn: 'var(--warn)', danger: 'var(--danger)', dim: 'var(--dim)' };
|
|
@@ -896,6 +1058,123 @@
|
|
|
896
1058
|
mount.appendChild(wrap);
|
|
897
1059
|
}
|
|
898
1060
|
|
|
1061
|
+
var APPROX_TIP = 'Approximation from created/updated timestamps — Backlog.md keeps no status history.';
|
|
1062
|
+
function kpiTile(opts) {
|
|
1063
|
+
var node = el('div', 'kpi');
|
|
1064
|
+
node.setAttribute('data-tip', APPROX_TIP);
|
|
1065
|
+
node.appendChild(el('span', 'kpi-big', opts.big));
|
|
1066
|
+
node.appendChild(el('span', 'kpi-cap', opts.cap));
|
|
1067
|
+
if (opts.sub) {
|
|
1068
|
+
var sub = el('span', 'kpi-sub', opts.sub);
|
|
1069
|
+
if (opts.tone) sub.setAttribute('data-tone', opts.tone);
|
|
1070
|
+
node.appendChild(sub);
|
|
1071
|
+
}
|
|
1072
|
+
return node;
|
|
1073
|
+
}
|
|
1074
|
+
function wipBlockedTile(k) {
|
|
1075
|
+
var node = el('div', 'kpi');
|
|
1076
|
+
node.setAttribute('data-tip', APPROX_TIP);
|
|
1077
|
+
var wipBtn = el('button', 'kpi-special-btn');
|
|
1078
|
+
wipBtn.type = 'button';
|
|
1079
|
+
wipBtn.setAttribute('data-special', 'wip');
|
|
1080
|
+
wipBtn.setAttribute('aria-pressed', 'false');
|
|
1081
|
+
wipBtn.addEventListener('click', function () { setSpecialFilter('wip'); });
|
|
1082
|
+
wipBtn.appendChild(el('span', 'kpi-big', String(k.wip)));
|
|
1083
|
+
wipBtn.appendChild(el('span', 'kpi-cap', 'in progress'));
|
|
1084
|
+
node.appendChild(wipBtn);
|
|
1085
|
+
var blockedBtn = el('button', 'kpi-special-btn');
|
|
1086
|
+
blockedBtn.type = 'button';
|
|
1087
|
+
blockedBtn.setAttribute('data-special', 'blocked');
|
|
1088
|
+
blockedBtn.setAttribute('aria-pressed', 'false');
|
|
1089
|
+
blockedBtn.addEventListener('click', function () { setSpecialFilter('blocked'); });
|
|
1090
|
+
var sub = el('span', 'kpi-sub', k.blocked + ' blocked · click to filter');
|
|
1091
|
+
if (k.blocked > 0) sub.setAttribute('data-tone', 'danger');
|
|
1092
|
+
blockedBtn.appendChild(sub);
|
|
1093
|
+
node.appendChild(blockedBtn);
|
|
1094
|
+
return node;
|
|
1095
|
+
}
|
|
1096
|
+
function renderKpis(mount, k) {
|
|
1097
|
+
mount.textContent = '';
|
|
1098
|
+
if (!k) return;
|
|
1099
|
+
var grid = el('div', 'kpi-grid');
|
|
1100
|
+
grid.appendChild(kpiTile({
|
|
1101
|
+
big: k.progressPct + '%', cap: 'complete',
|
|
1102
|
+
sub: k.done + ' / ' + k.total + ' done' + (k.forecastDate ? ' · at this pace done ~' + k.forecastDate : ''),
|
|
1103
|
+
}));
|
|
1104
|
+
var delta = k.velocity7 - k.velocityPrev7;
|
|
1105
|
+
grid.appendChild(kpiTile({
|
|
1106
|
+
big: String(k.velocity7), cap: 'done / last 7 days',
|
|
1107
|
+
sub: delta === 0 ? '± 0 vs previous week' : (delta > 0 ? '▲ +' : '▼ ') + delta + ' vs previous week',
|
|
1108
|
+
tone: delta > 0 ? 'ok' : delta < 0 ? 'danger' : undefined,
|
|
1109
|
+
}));
|
|
1110
|
+
grid.appendChild(wipBlockedTile(k));
|
|
1111
|
+
var age = kpiTile({
|
|
1112
|
+
big: k.oldestOpenDays === null ? '—' : k.oldestOpenDays + 'd', cap: 'oldest open task',
|
|
1113
|
+
sub: k.medianOpenAgeDays === null ? '' : 'median ' + k.medianOpenAgeDays + 'd open',
|
|
1114
|
+
});
|
|
1115
|
+
if (k.oldestOpenId) {
|
|
1116
|
+
var link = el('button', 'kpi-link', k.oldestOpenId);
|
|
1117
|
+
link.type = 'button';
|
|
1118
|
+
link.addEventListener('click', function () { openDetail(k.oldestOpenId); });
|
|
1119
|
+
age.appendChild(link);
|
|
1120
|
+
}
|
|
1121
|
+
grid.appendChild(age);
|
|
1122
|
+
mount.appendChild(grid);
|
|
1123
|
+
}
|
|
1124
|
+
function ageInDays(task) {
|
|
1125
|
+
var raw = task.created || task.updated;
|
|
1126
|
+
if (!raw) return null;
|
|
1127
|
+
var t = Date.parse(/^\d{4}-\d{2}-\d{2}$/.test(raw) ? raw + 'T12:00:00' : raw);
|
|
1128
|
+
if (isNaN(t)) return null;
|
|
1129
|
+
return Math.max(0, Math.round((Date.now() - t) / 86400000));
|
|
1130
|
+
}
|
|
1131
|
+
var STALE_DAYS = 14;
|
|
1132
|
+
function renderAging(mount, tasks) {
|
|
1133
|
+
mount.textContent = '';
|
|
1134
|
+
var lanes = [];
|
|
1135
|
+
var byStatus = {};
|
|
1136
|
+
tasks.forEach(function (t) {
|
|
1137
|
+
if (isDoneStatus(t.status)) return;
|
|
1138
|
+
var days = ageInDays(t);
|
|
1139
|
+
if (days === null) return;
|
|
1140
|
+
if (!byStatus[t.status]) { byStatus[t.status] = []; lanes.push(t.status); }
|
|
1141
|
+
byStatus[t.status].push({ task: t, days: days });
|
|
1142
|
+
});
|
|
1143
|
+
if (lanes.length === 0) {
|
|
1144
|
+
mount.appendChild(el('p', 'hint', 'No datable open tasks.'));
|
|
1145
|
+
return;
|
|
1146
|
+
}
|
|
1147
|
+
var maxDays = 1;
|
|
1148
|
+
lanes.forEach(function (s) { byStatus[s].forEach(function (e) { if (e.days > maxDays) maxDays = e.days; }); });
|
|
1149
|
+
var labelW = 130, w = 780, laneH = 34, padR = 20;
|
|
1150
|
+
var h = lanes.length * laneH + 24;
|
|
1151
|
+
var svg = svgEl('svg', { viewBox: '0 0 ' + w + ' ' + h, width: '100%', 'class': 'aging', role: 'img', 'aria-label': 'Open tasks by age in days' });
|
|
1152
|
+
lanes.forEach(function (s, i) {
|
|
1153
|
+
var y = i * laneH + 22;
|
|
1154
|
+
var lbl = svgEl('text', { x: labelW - 12, y: y + 4, 'text-anchor': 'end', 'class': 'aging-lane-label' });
|
|
1155
|
+
lbl.textContent = s;
|
|
1156
|
+
svg.appendChild(lbl);
|
|
1157
|
+
svg.appendChild(svgEl('line', { x1: labelW, y1: y, x2: w - padR, y2: y, 'class': 'aging-track' }));
|
|
1158
|
+
byStatus[s].forEach(function (e) {
|
|
1159
|
+
var cx = labelW + (w - labelW - padR) * e.days / maxDays;
|
|
1160
|
+
var dot = svgEl('circle', {
|
|
1161
|
+
cx: cx.toFixed(1), cy: y, r: 6, 'class': 'age-dot',
|
|
1162
|
+
fill: e.days >= STALE_DAYS ? 'var(--warn)' : TONE_VAR[toneOf(s)]
|
|
1163
|
+
});
|
|
1164
|
+
attachTitle(dot, e.task.id + ' · ' + e.task.title + ' · ' + e.days + 'd');
|
|
1165
|
+
dot.addEventListener('click', function () { openDetail(e.task.id); });
|
|
1166
|
+
svg.appendChild(dot);
|
|
1167
|
+
});
|
|
1168
|
+
});
|
|
1169
|
+
var axis0 = svgEl('text', { x: labelW, y: h - 4, 'class': 'aging-axis' });
|
|
1170
|
+
axis0.textContent = '0d';
|
|
1171
|
+
var axisMax = svgEl('text', { x: w - padR, y: h - 4, 'text-anchor': 'end', 'class': 'aging-axis' });
|
|
1172
|
+
axisMax.textContent = maxDays + 'd';
|
|
1173
|
+
svg.appendChild(axis0);
|
|
1174
|
+
svg.appendChild(axisMax);
|
|
1175
|
+
mount.appendChild(svg);
|
|
1176
|
+
}
|
|
1177
|
+
|
|
899
1178
|
function renderBars(mount, milestones) {
|
|
900
1179
|
mount.textContent = '';
|
|
901
1180
|
if (milestones.length === 0) {
|
|
@@ -931,44 +1210,95 @@
|
|
|
931
1210
|
mount.appendChild(svg);
|
|
932
1211
|
}
|
|
933
1212
|
|
|
934
|
-
function
|
|
1213
|
+
function weekdayName(idx, style) {
|
|
1214
|
+
/* 2024-01-07 was a Sunday; offset from it gives any weekday */
|
|
1215
|
+
var ref = new Date(Date.UTC(2024, 0, 7 + idx));
|
|
1216
|
+
try {
|
|
1217
|
+
return new Intl.DateTimeFormat(undefined, { weekday: style, timeZone: 'UTC' }).format(ref);
|
|
1218
|
+
} catch (e) {
|
|
1219
|
+
return ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'][idx];
|
|
1220
|
+
}
|
|
1221
|
+
}
|
|
1222
|
+
function renderActivityKpis(mount, k) {
|
|
935
1223
|
mount.textContent = '';
|
|
936
|
-
if (!
|
|
1224
|
+
if (!k) return;
|
|
1225
|
+
var grid = el('div', 'kpi-grid');
|
|
1226
|
+
grid.appendChild(kpiTile({ big: String(k.activityTotal30), cap: 'tasks touched / 30 days' }));
|
|
1227
|
+
grid.appendChild(kpiTile({ big: String(k.activityAvgPerWeek), cap: 'avg per week (26w)' }));
|
|
1228
|
+
grid.appendChild(kpiTile({
|
|
1229
|
+
big: k.busiestWeekday === null ? '—' : weekdayName(k.busiestWeekday, 'long'),
|
|
1230
|
+
cap: 'most active day'
|
|
1231
|
+
}));
|
|
1232
|
+
grid.appendChild(kpiTile({ big: k.streakDays + 'd', cap: 'current streak' }));
|
|
1233
|
+
mount.appendChild(grid);
|
|
1234
|
+
}
|
|
1235
|
+
function renderDayPanel(panel, bucket) {
|
|
1236
|
+
panel.textContent = '';
|
|
1237
|
+
panel.hidden = false;
|
|
1238
|
+
var head = el('h4', '', bucket.date + ' — ' + bucket.count + ' task' + (bucket.count === 1 ? '' : 's'));
|
|
1239
|
+
panel.appendChild(head);
|
|
1240
|
+
var list = el('ul', 'flow-list');
|
|
1241
|
+
var shown = 0;
|
|
1242
|
+
bucket.ids.forEach(function (id) {
|
|
1243
|
+
var t = findTask(id);
|
|
1244
|
+
if (!t) return;
|
|
1245
|
+
shown++;
|
|
1246
|
+
var li = el('li', 'flow-card');
|
|
1247
|
+
var h = el('div', 'flow-card-head');
|
|
1248
|
+
h.appendChild(el('span', 'flow-card-id', t.id));
|
|
1249
|
+
var chip = el('span', 'status-chip', t.status);
|
|
1250
|
+
chip.setAttribute('data-tone', toneOf(t.status));
|
|
1251
|
+
h.appendChild(chip);
|
|
1252
|
+
li.appendChild(h);
|
|
1253
|
+
li.appendChild(el('div', 'flow-card-title', t.title));
|
|
1254
|
+
li.addEventListener('click', function () { openDetail(t.id); });
|
|
1255
|
+
list.appendChild(li);
|
|
1256
|
+
});
|
|
1257
|
+
if (shown === 0) panel.appendChild(el('p', 'flow-empty', 'No task details for this day.'));
|
|
1258
|
+
else panel.appendChild(list);
|
|
1259
|
+
}
|
|
1260
|
+
function renderHeatmap(mount, activity) {
|
|
1261
|
+
mount.textContent = '';
|
|
1262
|
+
if (!activity || activity.length === 0) {
|
|
937
1263
|
mount.appendChild(el('p', 'hint', 'No activity data.'));
|
|
938
1264
|
return;
|
|
939
1265
|
}
|
|
940
|
-
var
|
|
1266
|
+
var panel = document.getElementById('day-panel');
|
|
1267
|
+
var cell = 12, gap = 3, padL = 34, padT = 16;
|
|
1268
|
+
var first = new Date(activity[0].date + 'T00:00:00Z');
|
|
1269
|
+
var offset = first.getUTCDay();
|
|
1270
|
+
var cols = Math.ceil((activity.length + offset) / 7);
|
|
1271
|
+
var w = padL + cols * (cell + gap);
|
|
1272
|
+
var h = padT + 7 * (cell + gap) + 14;
|
|
941
1273
|
var max = 1;
|
|
942
1274
|
activity.forEach(function (b) { if (b.count > max) max = b.count; });
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
});
|
|
949
|
-
var coords = activity.map(function (b, i) {
|
|
950
|
-
return px(i).toFixed(1) + ',' + py(b.count).toFixed(1);
|
|
1275
|
+
var svg = svgEl('svg', { viewBox: '0 0 ' + w + ' ' + h, width: '100%', 'class': 'heatmap', role: 'img', 'aria-label': 'Daily task activity, last 26 weeks' });
|
|
1276
|
+
[1, 3, 5].forEach(function (row) {
|
|
1277
|
+
var lbl = svgEl('text', { x: padL - 6, y: padT + row * (cell + gap) + cell - 2, 'text-anchor': 'end', 'class': 'hm-axis' });
|
|
1278
|
+
lbl.textContent = weekdayName(row, 'short');
|
|
1279
|
+
svg.appendChild(lbl);
|
|
951
1280
|
});
|
|
952
|
-
var
|
|
953
|
-
' L ' + coords.join(' L ') +
|
|
954
|
-
' L ' + px(activity.length - 1).toFixed(1) + ',' + (h - padB) + ' Z';
|
|
955
|
-
svg.appendChild(svgEl('path', { d: areaD, 'class': 'spark-area' }));
|
|
956
|
-
svg.appendChild(svgEl('polyline', { points: coords.join(' '), 'class': 'spark-line' }));
|
|
1281
|
+
var lastMonth = '';
|
|
957
1282
|
activity.forEach(function (b, i) {
|
|
958
|
-
var
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
1283
|
+
var pos = i + offset;
|
|
1284
|
+
var col = Math.floor(pos / 7), row = pos % 7;
|
|
1285
|
+
var level = b.count === 0 ? 0 : Math.max(1, Math.ceil((b.count / max) * 4));
|
|
1286
|
+
var rect = svgEl('rect', {
|
|
1287
|
+
x: padL + col * (cell + gap), y: padT + row * (cell + gap),
|
|
1288
|
+
width: cell, height: cell, rx: 2.5,
|
|
1289
|
+
'class': 'hm-cell hm-' + level, 'data-date': b.date
|
|
962
1290
|
});
|
|
963
|
-
attachTitle(
|
|
964
|
-
|
|
1291
|
+
attachTitle(rect, b.date + ': ' + b.count);
|
|
1292
|
+
rect.addEventListener('click', function () { if (panel) renderDayPanel(panel, b); });
|
|
1293
|
+
svg.appendChild(rect);
|
|
1294
|
+
var month = b.date.slice(0, 7);
|
|
1295
|
+
if (row === 0 && month !== lastMonth) {
|
|
1296
|
+
lastMonth = month;
|
|
1297
|
+
var m = svgEl('text', { x: padL + col * (cell + gap), y: padT - 5, 'class': 'hm-axis' });
|
|
1298
|
+
m.textContent = b.date.slice(5, 7);
|
|
1299
|
+
svg.appendChild(m);
|
|
1300
|
+
}
|
|
965
1301
|
});
|
|
966
|
-
var first = svgEl('text', { x: padL, y: h - 7, 'class': 'spark-axis' });
|
|
967
|
-
first.textContent = activity[0].date;
|
|
968
|
-
var last = svgEl('text', { x: w - padR, y: h - 7, 'text-anchor': 'end', 'class': 'spark-axis' });
|
|
969
|
-
last.textContent = activity[activity.length - 1].date;
|
|
970
|
-
svg.appendChild(first);
|
|
971
|
-
svg.appendChild(last);
|
|
972
1302
|
mount.appendChild(svg);
|
|
973
1303
|
}
|
|
974
1304
|
|
|
@@ -1039,9 +1369,12 @@
|
|
|
1039
1369
|
phases = [];
|
|
1040
1370
|
}
|
|
1041
1371
|
if ($('#donut')) renderDonut($('#donut'), data.statuses);
|
|
1372
|
+
if ($('#kpis')) renderKpis($('#kpis'), data.kpis);
|
|
1373
|
+
if ($('#aging')) renderAging($('#aging'), data.tasks);
|
|
1042
1374
|
if ($('#bars')) renderBars($('#bars'), data.milestones);
|
|
1043
1375
|
if ($('#stepper')) renderStepper($('#stepper'), phases);
|
|
1044
|
-
if ($('#
|
|
1376
|
+
if ($('#activity-kpis')) renderActivityKpis($('#activity-kpis'), data.kpis);
|
|
1377
|
+
if ($('#heatmap')) renderHeatmap($('#heatmap'), data.activity);
|
|
1045
1378
|
|
|
1046
1379
|
/* ---------- Glossary tooltips ---------- */
|
|
1047
1380
|
var glossary = {};
|
|
@@ -1244,6 +1577,76 @@
|
|
|
1244
1577
|
window.__sblOpenDetail = openDetail;
|
|
1245
1578
|
if (dialog) dialog.addEventListener('click', function (ev) { if (ev.target === dialog) closeDetail(); });
|
|
1246
1579
|
|
|
1580
|
+
/* ---------- Drafts ---------- */
|
|
1581
|
+
function draftMetaGrid(d) {
|
|
1582
|
+
var grid = el('div', 'detail-meta');
|
|
1583
|
+
function cell(label, value, tone) {
|
|
1584
|
+
var c = el('div', 'meta-cell');
|
|
1585
|
+
c.appendChild(el('span', 'meta-label', label));
|
|
1586
|
+
var v = el('span', 'meta-value' + (value ? '' : ' empty'), value || '—');
|
|
1587
|
+
if (value && tone) v.setAttribute('data-tone', tone);
|
|
1588
|
+
c.appendChild(v);
|
|
1589
|
+
return c;
|
|
1590
|
+
}
|
|
1591
|
+
grid.appendChild(cell('Priority', d.priority, priorityTone(d.priority)));
|
|
1592
|
+
grid.appendChild(cell('Assignee', d.assignee));
|
|
1593
|
+
grid.appendChild(cell('Created', d.created));
|
|
1594
|
+
grid.appendChild(cell('Updated', d.updated));
|
|
1595
|
+
return grid;
|
|
1596
|
+
}
|
|
1597
|
+
function openDraftDetail(d) {
|
|
1598
|
+
if (!dialog) return;
|
|
1599
|
+
var content = el('div', 'dialog-content');
|
|
1600
|
+
var head = el('div', 'detail-head');
|
|
1601
|
+
head.appendChild(el('span', 'detail-id', d.id));
|
|
1602
|
+
var chip = el('span', 'status-chip', d.status);
|
|
1603
|
+
chip.setAttribute('data-tone', toneOf(d.status));
|
|
1604
|
+
head.appendChild(chip);
|
|
1605
|
+
var closeBtn = el('button', 'detail-close', '×');
|
|
1606
|
+
closeBtn.type = 'button';
|
|
1607
|
+
closeBtn.setAttribute('aria-label', 'Close details');
|
|
1608
|
+
closeBtn.addEventListener('click', closeDetail);
|
|
1609
|
+
head.appendChild(closeBtn);
|
|
1610
|
+
content.appendChild(head);
|
|
1611
|
+
var title = el('h3', 'detail-title', d.title);
|
|
1612
|
+
title.id = 'detail-title-h';
|
|
1613
|
+
dialog.setAttribute('aria-labelledby', 'detail-title-h');
|
|
1614
|
+
content.appendChild(title);
|
|
1615
|
+
if (d.description) content.appendChild(descParagraphs(d.description));
|
|
1616
|
+
else content.appendChild(el('p', 'detail-desc', 'No description.'));
|
|
1617
|
+
content.appendChild(draftMetaGrid(d));
|
|
1618
|
+
if (d.acs && d.acs.length > 0) content.appendChild(acSection(d));
|
|
1619
|
+
var cmd = el('button', 'phase-cmd detail-cmd');
|
|
1620
|
+
cmd.type = 'button';
|
|
1621
|
+
var cmdLine = 'backlog draft promote ' + d.id.replace(/^task-/i, '');
|
|
1622
|
+
cmd.appendChild(el('span', 'cmd-line', cmdLine));
|
|
1623
|
+
cmd.appendChild(el('span', 'cmd-title', 'copy'));
|
|
1624
|
+
cmd.addEventListener('click', function () { copyCommand(cmd, cmdLine); });
|
|
1625
|
+
content.appendChild(cmd);
|
|
1626
|
+
dialog.textContent = '';
|
|
1627
|
+
dialog.appendChild(content);
|
|
1628
|
+
dialog.showModal();
|
|
1629
|
+
}
|
|
1630
|
+
function renderDrafts(drafts) {
|
|
1631
|
+
var list = document.getElementById('drafts-list');
|
|
1632
|
+
if (!list) return;
|
|
1633
|
+
list.textContent = '';
|
|
1634
|
+
if (!drafts || drafts.length === 0) { list.appendChild(el('li', '', 'No drafts.')); return; }
|
|
1635
|
+
drafts.forEach(function (d) {
|
|
1636
|
+
var li = el('li', 'flow-card');
|
|
1637
|
+
var head = el('div', 'flow-card-head');
|
|
1638
|
+
head.appendChild(el('span', 'flow-card-id', d.id));
|
|
1639
|
+
var chip = el('span', 'status-chip', d.status);
|
|
1640
|
+
chip.setAttribute('data-tone', toneOf(d.status));
|
|
1641
|
+
head.appendChild(chip);
|
|
1642
|
+
li.appendChild(head);
|
|
1643
|
+
li.appendChild(el('div', 'flow-card-title', d.title));
|
|
1644
|
+
li.addEventListener('click', function () { openDraftDetail(d); });
|
|
1645
|
+
list.appendChild(li);
|
|
1646
|
+
});
|
|
1647
|
+
}
|
|
1648
|
+
renderDrafts(data.drafts);
|
|
1649
|
+
|
|
1247
1650
|
/* ---------- Keyboard shortcuts ---------- */
|
|
1248
1651
|
document.addEventListener('keydown', function (ev) {
|
|
1249
1652
|
if (ev.key === 'Escape') {
|