super-backlog 1.2.0 → 1.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.
- package/README.md +1 -1
- package/dist/dashboard/data.js +120 -17
- package/dist/dashboard/metrics.js +97 -0
- package/dist/models/dashboard-api.js +35 -6
- package/dist/models/install.js +9 -2
- package/dist/templates/dashboard.html +747 -153
- 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,20 +143,21 @@
|
|
|
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 ---------- */
|
|
152
|
+
dialog { margin: auto; } /* the global reset removes the UA margin that centers dialogs */
|
|
110
153
|
#task-dialog {
|
|
111
|
-
width: min(
|
|
154
|
+
width: min(720px, 92vw); max-height: 85vh; overflow-y: auto; overflow-x: hidden; padding: 0;
|
|
112
155
|
border: 1px solid var(--line-strong); border-radius: 14px;
|
|
113
156
|
background: var(--surface); color: var(--text);
|
|
114
|
-
box-shadow:
|
|
157
|
+
box-shadow: var(--shadow-dialog);
|
|
115
158
|
}
|
|
116
159
|
#task-dialog::backdrop {
|
|
117
|
-
background:
|
|
160
|
+
background: var(--backdrop);
|
|
118
161
|
backdrop-filter: blur(2px);
|
|
119
162
|
}
|
|
120
163
|
#task-dialog[open] { animation: sbl-dialog-in .18s ease-out; }
|
|
@@ -129,10 +172,10 @@
|
|
|
129
172
|
display: none; flex-direction: column;
|
|
130
173
|
border: 1px solid var(--line-strong); border-radius: 14px;
|
|
131
174
|
background: var(--surface); color: var(--text);
|
|
132
|
-
box-shadow:
|
|
175
|
+
box-shadow: var(--shadow-dialog);
|
|
133
176
|
}
|
|
134
177
|
#backlog-dialog[open] { display: flex; animation: sbl-dialog-in .18s ease-out; }
|
|
135
|
-
#backlog-dialog::backdrop { background:
|
|
178
|
+
#backlog-dialog::backdrop { background: var(--backdrop); backdrop-filter: blur(2px); }
|
|
136
179
|
@media (prefers-reduced-motion: reduce) {
|
|
137
180
|
#backlog-dialog[open] { animation: none; }
|
|
138
181
|
}
|
|
@@ -154,26 +197,49 @@
|
|
|
154
197
|
#backlog-close:hover { color: var(--text); border-color: var(--line); }
|
|
155
198
|
#backlog-close:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
|
|
156
199
|
#backlog-frame { flex: 1; width: 100%; border: 0; background: var(--bg); }
|
|
157
|
-
.dialog-content { padding:
|
|
158
|
-
.detail-head {
|
|
200
|
+
.dialog-content { padding: 0 24px 26px; }
|
|
201
|
+
.detail-head {
|
|
202
|
+
position: sticky; top: 0; z-index: 1;
|
|
203
|
+
display: flex; align-items: center; gap: 10px;
|
|
204
|
+
margin: 0 -24px 12px; padding: 16px 24px 12px;
|
|
205
|
+
background: var(--surface); border-bottom: 1px solid var(--line);
|
|
206
|
+
}
|
|
159
207
|
.detail-id { font-family: var(--mono); color: var(--accent); font-weight: 700; }
|
|
160
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; }
|
|
161
|
-
.status-chip[data-tone="ok"] { color: var(--ok); border-color:
|
|
162
|
-
.status-chip[data-tone="accent"] { color: var(--accent); border-color:
|
|
163
|
-
.status-chip[data-tone="warn"] { color: var(--warn); border-color:
|
|
164
|
-
.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); }
|
|
165
213
|
.detail-close { margin-left: auto; background: none; border: none; color: var(--dim); font-size: 1.3rem; cursor: pointer; line-height: 1; }
|
|
166
214
|
.detail-close:hover { color: var(--danger); }
|
|
167
|
-
.detail-title { font-size: 1.
|
|
168
|
-
#task-dialog .detail-desc { color: var(--muted); margin-bottom:
|
|
169
|
-
#task-dialog h4 { color: var(--dim); font-size: .7rem; letter-spacing: 1.3px; text-transform: uppercase; margin:
|
|
170
|
-
.meta
|
|
171
|
-
|
|
172
|
-
|
|
215
|
+
.detail-title { font-size: 1.3rem; font-weight: 700; line-height: 1.3; margin-bottom: 10px; overflow-wrap: anywhere; }
|
|
216
|
+
#task-dialog .detail-desc { color: var(--muted); line-height: 1.6; margin-bottom: 10px; white-space: normal; }
|
|
217
|
+
#task-dialog h4 { color: var(--dim); font-size: .7rem; letter-spacing: 1.3px; text-transform: uppercase; margin: 18px 0 8px; }
|
|
218
|
+
.detail-meta {
|
|
219
|
+
display: grid; grid-template-columns: repeat(auto-fit, minmax(130px, 1fr));
|
|
220
|
+
gap: 12px 18px; margin: 16px 0 4px; padding: 14px 16px;
|
|
221
|
+
border: 1px solid var(--line); border-radius: 10px; background: var(--surface-2);
|
|
222
|
+
}
|
|
223
|
+
.meta-cell .meta-label {
|
|
224
|
+
display: block; color: var(--dim); font-size: .66rem;
|
|
225
|
+
letter-spacing: 1.2px; text-transform: uppercase; margin-bottom: 4px;
|
|
226
|
+
}
|
|
227
|
+
.meta-value { font-size: .88rem; color: var(--text); overflow-wrap: anywhere; }
|
|
228
|
+
.meta-value.empty { color: var(--dim); }
|
|
229
|
+
.meta-value[data-tone="danger"] { color: var(--danger); }
|
|
230
|
+
.meta-value[data-tone="warn"] { color: var(--warn); }
|
|
231
|
+
.meta-value[data-tone="dim"] { color: var(--muted); }
|
|
232
|
+
.ac-progress { display: flex; align-items: center; gap: 12px; margin: 0 0 10px; }
|
|
233
|
+
.ac-count { font-family: var(--mono); font-size: .78rem; color: var(--muted); white-space: nowrap; }
|
|
234
|
+
.ac-bar { flex: 1; height: 4px; border-radius: 999px; background: var(--surface-2); overflow: hidden; }
|
|
235
|
+
.ac-bar-fill { height: 100%; border-radius: 999px; background: var(--ok); }
|
|
173
236
|
.dep-row { display: flex; flex-wrap: wrap; gap: 6px; }
|
|
237
|
+
.dep-dot { width: 7px; height: 7px; border-radius: 50%; display: inline-block; margin-right: 7px; background: var(--dim); }
|
|
238
|
+
.dep-dot.done { background: var(--ok); }
|
|
239
|
+
.detail-cmd { margin-top: 18px; }
|
|
174
240
|
.dep-link {
|
|
175
241
|
font-family: var(--mono); font-size: .74rem; padding: 2px 10px; border-radius: 999px;
|
|
176
|
-
border: 1px solid
|
|
242
|
+
border: 1px solid var(--accent-line); background: var(--accent-bg); color: var(--accent); cursor: pointer;
|
|
177
243
|
}
|
|
178
244
|
.dep-link:hover { filter: brightness(1.25); }
|
|
179
245
|
|
|
@@ -189,7 +255,7 @@
|
|
|
189
255
|
background: linear-gradient(180deg, var(--accent), transparent 130%);
|
|
190
256
|
-webkit-background-clip: text; background-clip: text; color: transparent;
|
|
191
257
|
}
|
|
192
|
-
.sec-head h2 { font-size: 1.15rem; letter-spacing: .5px; }
|
|
258
|
+
.sec-head h2 { font-size: 1.15rem; letter-spacing: .5px; font-weight: 700; }
|
|
193
259
|
.sec-head .tagline { color: var(--dim); font-size: .82rem; margin-left: auto; text-align: right; }
|
|
194
260
|
.mount { min-height: 24px; }
|
|
195
261
|
|
|
@@ -204,12 +270,11 @@
|
|
|
204
270
|
}
|
|
205
271
|
.cmd-btn:hover { background: var(--surface-2); border-color: var(--accent); }
|
|
206
272
|
.cmd-btn:active { transform: translateY(1px); }
|
|
207
|
-
.cmd-title { font-size: .95rem; font-weight:
|
|
273
|
+
.cmd-title { font-size: .95rem; font-weight: 700; color: var(--text); }
|
|
208
274
|
.cmd-btn:hover .cmd-title { color: var(--accent); }
|
|
209
275
|
.cmd-line { font-family: var(--mono); font-size: .72rem; color: var(--dim); }
|
|
210
276
|
.hint { color: var(--dim); font-size: .8rem; margin-top: 12px; }
|
|
211
277
|
.drafts-list { list-style: none; margin: 0; padding: 0; display: grid; gap: 8px; }
|
|
212
|
-
.drafts-list li { background: var(--surface); border: 1px solid var(--line); border-radius: 8px; padding: 8px 12px; font-size: .88rem; }
|
|
213
278
|
code.inline {
|
|
214
279
|
font-family: var(--mono); font-size: .85em;
|
|
215
280
|
background: var(--surface-2); border: 1px solid var(--line);
|
|
@@ -222,7 +287,7 @@
|
|
|
222
287
|
background: var(--surface-2); color: var(--text); border: 1px solid var(--line-strong);
|
|
223
288
|
border-radius: 999px; padding: 7px 16px; min-width: 260px; font: inherit;
|
|
224
289
|
}
|
|
225
|
-
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); }
|
|
226
291
|
.table-wrap { overflow-x: auto; background: var(--surface); border: 1px solid var(--line); border-radius: 12px; }
|
|
227
292
|
table { width: 100%; border-collapse: collapse; font-size: .89rem; }
|
|
228
293
|
th, td { text-align: left; padding: 8px 14px; border-bottom: 1px solid var(--line); vertical-align: top; white-space: nowrap; }
|
|
@@ -238,7 +303,8 @@
|
|
|
238
303
|
.task-row:hover td { background: var(--surface-2); }
|
|
239
304
|
.cell-id { color: var(--accent); font-family: var(--mono); font-variant-numeric: tabular-nums; }
|
|
240
305
|
.cell-title { font-weight: 400; white-space: normal; min-width: 220px; }
|
|
241
|
-
.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; }
|
|
242
308
|
|
|
243
309
|
/* ---------- Footer ---------- */
|
|
244
310
|
.footer {
|
|
@@ -257,6 +323,30 @@
|
|
|
257
323
|
.legend-list li:hover { color: var(--text); }
|
|
258
324
|
.legend-list .swatch { width: 10px; height: 10px; border-radius: 3px; flex: none; }
|
|
259
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
|
+
|
|
260
350
|
/* ---------- Milestone bars ---------- */
|
|
261
351
|
.bars text { font-family: var(--mono); }
|
|
262
352
|
.bar-label { fill: var(--text); font-size: 12.5px; }
|
|
@@ -265,16 +355,17 @@
|
|
|
265
355
|
.bar-fill { fill: var(--accent); }
|
|
266
356
|
.bar-fill.complete { fill: var(--ok); }
|
|
267
357
|
|
|
268
|
-
/* ----------
|
|
269
|
-
.
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
}
|
|
274
|
-
.
|
|
275
|
-
.
|
|
276
|
-
.
|
|
277
|
-
|
|
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); }
|
|
278
369
|
|
|
279
370
|
/* ---------- Pipeline stepper ---------- */
|
|
280
371
|
.stepper { display: grid; grid-template-columns: repeat(9, minmax(0, 1fr)); gap: 2px; margin: 4px 0 6px; }
|
|
@@ -298,7 +389,7 @@
|
|
|
298
389
|
border-radius: 50%; font-family: var(--mono); font-size: .9rem; font-weight: 700;
|
|
299
390
|
background: var(--surface-2); border: 2px solid var(--line-strong); color: var(--muted);
|
|
300
391
|
}
|
|
301
|
-
.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); }
|
|
302
393
|
.step-label {
|
|
303
394
|
display: block; font-size: .8rem; font-weight: 600; color: var(--text); line-height: 1.3;
|
|
304
395
|
overflow-wrap: break-word;
|
|
@@ -343,10 +434,26 @@
|
|
|
343
434
|
color: var(--warn); background: var(--warn-bg); border: 1px solid var(--warn);
|
|
344
435
|
border-radius: 999px; padding: 2px 9px; transition: box-shadow .15s ease;
|
|
345
436
|
}
|
|
346
|
-
.update-badge:hover { box-shadow: 0 0 10px
|
|
437
|
+
.update-badge:hover { box-shadow: 0 0 10px var(--glow-warn-soft); }
|
|
347
438
|
.update-badge:focus-visible { outline: 2px solid var(--warn); outline-offset: 2px; }
|
|
348
439
|
.update-badge .cmd-title { font-size: inherit; font-weight: 600; color: inherit; }
|
|
349
440
|
|
|
441
|
+
/* ---------- Models modal ---------- */
|
|
442
|
+
#models-dialog {
|
|
443
|
+
width: min(560px, 92vw); max-height: 80vh; overflow-y: auto; overflow-x: hidden; padding: 0;
|
|
444
|
+
border: 1px solid var(--line-strong); border-radius: 14px;
|
|
445
|
+
background: var(--surface); color: var(--text);
|
|
446
|
+
box-shadow: var(--shadow-dialog);
|
|
447
|
+
}
|
|
448
|
+
#models-dialog::backdrop { background: var(--backdrop); backdrop-filter: blur(2px); }
|
|
449
|
+
#models-dialog[open] { animation: sbl-dialog-in .18s ease-out; }
|
|
450
|
+
@media (prefers-reduced-motion: reduce) {
|
|
451
|
+
#models-dialog[open] { animation: none; }
|
|
452
|
+
}
|
|
453
|
+
.phase-cmd:disabled { opacity: .6; cursor: default; }
|
|
454
|
+
.models-head-title { font-weight: 700; }
|
|
455
|
+
.models-row { display: flex; align-items: center; gap: 10px; margin: 14px 0; flex-wrap: wrap; }
|
|
456
|
+
|
|
350
457
|
/* ---------- Dependency flow ---------- */
|
|
351
458
|
.sub-head {
|
|
352
459
|
font-size: .78rem; letter-spacing: 1.4px; text-transform: uppercase;
|
|
@@ -379,22 +486,35 @@
|
|
|
379
486
|
main { padding: 24px 20px 40px; }
|
|
380
487
|
}
|
|
381
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>
|
|
382
499
|
</head>
|
|
383
500
|
<body>
|
|
384
501
|
<div class="layout">
|
|
385
502
|
|
|
386
503
|
<aside class="sbl-side">
|
|
387
|
-
<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>
|
|
388
507
|
<div class="kicker">SUPERPOWERS × BACKLOG.MD</div>
|
|
389
508
|
<div class="side-version" id="side-version"><span>v__KIT_VERSION__</span></div>
|
|
390
509
|
<nav aria-label="Dashboard sections">
|
|
391
510
|
<a href="#sec-01"><span class="n">01</span>Board & Quick Actions</a>
|
|
392
511
|
<a href="#sec-02"><span class="n">02</span>Status</a>
|
|
393
|
-
<a href="#sec-03"><span class="n">03</span>
|
|
394
|
-
<a href="#sec-04"><span class="n">04</span>
|
|
395
|
-
<a href="#sec-05"><span class="n">05</span>
|
|
396
|
-
<a href="#sec-06"><span class="n">06</span>
|
|
397
|
-
<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>
|
|
398
518
|
</nav>
|
|
399
519
|
<div class="legend">
|
|
400
520
|
<h4>Status</h4>
|
|
@@ -411,27 +531,42 @@
|
|
|
411
531
|
<div id="quickactions" class="mount">
|
|
412
532
|
<div class="cmd-row" id="cmd-buttons">
|
|
413
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>
|
|
414
535
|
</div>
|
|
415
536
|
<p class="hint">Opens the Backlog.md browser in an overlay — served locally per project, started on demand.</p>
|
|
416
537
|
</div>
|
|
417
|
-
<div id="drafts" class="mount">
|
|
418
|
-
<h3 class="sub-head">Drafts</h3>
|
|
419
|
-
<ul id="drafts-list" class="drafts-list"></ul>
|
|
420
|
-
</div>
|
|
421
538
|
</section>
|
|
422
539
|
|
|
423
540
|
<section id="sec-02">
|
|
424
|
-
<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>
|
|
425
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>
|
|
426
546
|
</section>
|
|
427
547
|
|
|
428
548
|
<section id="sec-03">
|
|
429
|
-
<div class="sec-head"><span class="sec-num">03</span><h2>
|
|
430
|
-
<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>
|
|
431
554
|
</section>
|
|
432
555
|
|
|
433
556
|
<section id="sec-04">
|
|
434
|
-
<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>
|
|
435
570
|
<div id="tasks" class="mount">
|
|
436
571
|
<div class="toolbar">
|
|
437
572
|
<input id="taskfilter" type="search" placeholder="Filter tasks…" aria-label="Filter tasks">
|
|
@@ -455,21 +590,15 @@
|
|
|
455
590
|
</div>
|
|
456
591
|
</section>
|
|
457
592
|
|
|
458
|
-
<section id="sec-
|
|
459
|
-
<div class="sec-head"><span class="sec-num">
|
|
460
|
-
<div id="
|
|
461
|
-
<div id="
|
|
462
|
-
<
|
|
463
|
-
<div id="depgraph" class="mount"></div>
|
|
464
|
-
</section>
|
|
465
|
-
|
|
466
|
-
<section id="sec-06">
|
|
467
|
-
<div class="sec-head"><span class="sec-num">06</span><h2>Activity</h2><span class="tagline">last 30 days</span></div>
|
|
468
|
-
<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>
|
|
469
598
|
</section>
|
|
470
599
|
|
|
471
|
-
<section id="sec-
|
|
472
|
-
<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>
|
|
473
602
|
<div id="docs" class="mount"></div>
|
|
474
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>
|
|
475
604
|
</section>
|
|
@@ -482,7 +611,7 @@
|
|
|
482
611
|
</main>
|
|
483
612
|
</div>
|
|
484
613
|
|
|
485
|
-
<dialog id="task-dialog"
|
|
614
|
+
<dialog id="task-dialog"></dialog>
|
|
486
615
|
|
|
487
616
|
<dialog id="backlog-dialog" aria-label="Backlog browser">
|
|
488
617
|
<div class="backlog-dialog-bar">
|
|
@@ -492,6 +621,17 @@
|
|
|
492
621
|
</div>
|
|
493
622
|
<iframe id="backlog-frame" title="Backlog.md browser"></iframe>
|
|
494
623
|
</dialog>
|
|
624
|
+
|
|
625
|
+
<dialog id="models-dialog" aria-label="Model router">
|
|
626
|
+
<div class="dialog-content">
|
|
627
|
+
<div class="detail-head">
|
|
628
|
+
<span class="detail-id">MODELS</span>
|
|
629
|
+
<span class="models-head-title">Model router</span>
|
|
630
|
+
<button type="button" class="detail-close" id="models-close" aria-label="Close">×</button>
|
|
631
|
+
</div>
|
|
632
|
+
<div id="models-body"></div>
|
|
633
|
+
</div>
|
|
634
|
+
</dialog>
|
|
495
635
|
<div id="sbl-tip" role="tooltip" hidden></div>
|
|
496
636
|
|
|
497
637
|
<script type="application/json" id="sbl-data">__SBL_DATA_JSON__</script>
|
|
@@ -525,7 +665,7 @@
|
|
|
525
665
|
}
|
|
526
666
|
|
|
527
667
|
var KEYS = ['id', 'title', 'status', 'milestone', 'priority', 'assignee', 'updated'];
|
|
528
|
-
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 };
|
|
529
669
|
function field(task, key) {
|
|
530
670
|
var v = task[key];
|
|
531
671
|
return v === undefined || v === null ? '' : String(v);
|
|
@@ -536,6 +676,8 @@
|
|
|
536
676
|
function matches(task) {
|
|
537
677
|
var eff = activeStatus();
|
|
538
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;
|
|
539
681
|
if (!state.query) return true;
|
|
540
682
|
var hit = KEYS.some(function (k) {
|
|
541
683
|
return field(task, k).toLowerCase().indexOf(state.query) !== -1;
|
|
@@ -544,13 +686,68 @@
|
|
|
544
686
|
return ac.text.toLowerCase().indexOf(state.query) !== -1;
|
|
545
687
|
});
|
|
546
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
|
+
}
|
|
547
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
|
+
}
|
|
548
747
|
function renderTasks() {
|
|
549
748
|
var rows = data.tasks
|
|
550
749
|
.filter(matches)
|
|
551
|
-
.sort(function (a, b) {
|
|
552
|
-
return field(a, state.key).localeCompare(field(b, state.key)) * state.dir;
|
|
553
|
-
});
|
|
750
|
+
.sort(function (a, b) { return compareTasks(a, b, state.key) * state.dir; });
|
|
554
751
|
tbody.textContent = '';
|
|
555
752
|
if (rows.length === 0) {
|
|
556
753
|
var emptyTr = el('tr');
|
|
@@ -564,7 +761,19 @@
|
|
|
564
761
|
var tr = el('tr', 'task-row');
|
|
565
762
|
tr.setAttribute('data-task', task.id);
|
|
566
763
|
KEYS.forEach(function (k) {
|
|
567
|
-
|
|
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);
|
|
568
777
|
});
|
|
569
778
|
tr.addEventListener('click', function () { openDetail(task.id); });
|
|
570
779
|
tbody.appendChild(tr);
|
|
@@ -598,9 +807,13 @@
|
|
|
598
807
|
}
|
|
599
808
|
function setStatusFilter(status) {
|
|
600
809
|
state.status = status || null;
|
|
810
|
+
state.special = null;
|
|
601
811
|
document.querySelectorAll('#pills .pill').forEach(function (pill) {
|
|
602
812
|
pill.classList.toggle('active', !!state.status && pill.getAttribute('data-status') === state.status);
|
|
603
813
|
});
|
|
814
|
+
document.querySelectorAll('[data-special]').forEach(function (tile) {
|
|
815
|
+
tile.setAttribute('aria-pressed', 'false');
|
|
816
|
+
});
|
|
604
817
|
renderTasks();
|
|
605
818
|
}
|
|
606
819
|
document.querySelectorAll('#pills .pill').forEach(function (pill) {
|
|
@@ -660,15 +873,106 @@
|
|
|
660
873
|
});
|
|
661
874
|
}
|
|
662
875
|
|
|
663
|
-
/* ----------
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
if (!
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
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
|
+
|
|
894
|
+
/* ---------- Models modal ---------- */
|
|
895
|
+
var modelsBtn = document.getElementById('models-btn');
|
|
896
|
+
var modelsDialog = document.getElementById('models-dialog');
|
|
897
|
+
var modelsBody = document.getElementById('models-body');
|
|
898
|
+
function tierCell(label, value) {
|
|
899
|
+
var c = el('div', 'meta-cell');
|
|
900
|
+
c.appendChild(el('span', 'meta-label', label));
|
|
901
|
+
c.appendChild(el('span', 'meta-value' + (value ? '' : ' empty'), value || '—'));
|
|
902
|
+
return c;
|
|
903
|
+
}
|
|
904
|
+
function actionButton(cmdLine, label, onClick) {
|
|
905
|
+
var b = el('button', 'phase-cmd');
|
|
906
|
+
b.type = 'button';
|
|
907
|
+
b.appendChild(el('span', 'cmd-line', cmdLine));
|
|
908
|
+
b.appendChild(el('span', 'cmd-title', label));
|
|
909
|
+
b.addEventListener('click', onClick);
|
|
910
|
+
return b;
|
|
911
|
+
}
|
|
912
|
+
function renderTiers(mount, tiers) {
|
|
913
|
+
mount.textContent = '';
|
|
914
|
+
if (!tiers || tiers.error || (!tiers.workhorse && !tiers.budget)) {
|
|
915
|
+
mount.appendChild(el('p', 'hint', 'Discovery failed — is the OpenCode CLI available?'));
|
|
916
|
+
return;
|
|
917
|
+
}
|
|
918
|
+
mount.appendChild(tierCell('Workhorse', tiers.workhorse));
|
|
919
|
+
mount.appendChild(tierCell('Budget', tiers.budget));
|
|
920
|
+
}
|
|
921
|
+
function renderModelsDialog(info) {
|
|
922
|
+
if (!modelsBody) return;
|
|
923
|
+
modelsBody.textContent = '';
|
|
924
|
+
var enabled = !!(info.config && info.config.enabled);
|
|
925
|
+
var row = el('div', 'models-row');
|
|
926
|
+
var chip = el('span', 'status-chip', info.installed ? (enabled ? 'enabled' : 'disabled') : 'not installed');
|
|
927
|
+
if (info.installed) chip.setAttribute('data-tone', enabled ? 'ok' : 'warn');
|
|
928
|
+
row.appendChild(chip);
|
|
929
|
+
var toggle = actionButton(
|
|
930
|
+
enabled ? 'sbl models disable' : 'sbl models enable',
|
|
931
|
+
enabled ? 'disable' : 'enable',
|
|
932
|
+
function () {
|
|
933
|
+
fetch('api/models/' + (enabled ? 'disable' : 'enable'), { method: 'POST', headers: { 'content-type': 'application/json' }, body: '{}' })
|
|
934
|
+
.then(function (res) { if (!res.ok) throw new Error('toggle failed'); return res.json(); })
|
|
935
|
+
.then(function (next) { renderModelsDialog(next); })
|
|
936
|
+
.catch(function () { cmdFeedback(toggle, 'failed ✗'); });
|
|
937
|
+
},
|
|
938
|
+
);
|
|
939
|
+
row.appendChild(toggle);
|
|
940
|
+
var discover = actionButton('sbl models discover', 'run', function () {
|
|
941
|
+
discover.disabled = true;
|
|
942
|
+
cmdFeedback(discover, 'running…');
|
|
943
|
+
fetch('api/models/discover', { method: 'POST', headers: { 'content-type': 'application/json' }, body: '{}' })
|
|
944
|
+
.then(function (res) { return res.json(); })
|
|
945
|
+
.then(function (r) { renderTiers(document.getElementById('models-tiers'), r); })
|
|
946
|
+
.catch(function () { cmdFeedback(discover, 'failed ✗'); })
|
|
947
|
+
.then(function () { discover.disabled = false; });
|
|
948
|
+
});
|
|
949
|
+
row.appendChild(discover);
|
|
950
|
+
modelsBody.appendChild(row);
|
|
951
|
+
var tiers = el('div', 'detail-meta');
|
|
952
|
+
tiers.id = 'models-tiers';
|
|
953
|
+
var resolved = (info.config && info.config.resolved) || {};
|
|
954
|
+
tiers.appendChild(tierCell('Workhorse', resolved.workhorse));
|
|
955
|
+
tiers.appendChild(tierCell('Budget', resolved.budget));
|
|
956
|
+
modelsBody.appendChild(tiers);
|
|
957
|
+
if (!info.installed) {
|
|
958
|
+
modelsBody.appendChild(el('p', 'hint', 'Install the router first, then enable it:'));
|
|
959
|
+
var hint = actionButton('sbl init --models', 'copy', function () { copyCommand(hint, 'sbl init --models'); });
|
|
960
|
+
modelsBody.appendChild(hint);
|
|
961
|
+
}
|
|
962
|
+
}
|
|
963
|
+
if (modelsBtn && modelsDialog) {
|
|
964
|
+
modelsBtn.addEventListener('click', function () {
|
|
965
|
+
fetch('api/models')
|
|
966
|
+
.then(function (res) { if (!res.ok) throw new Error('load failed'); return res.json(); })
|
|
967
|
+
.then(function (info) { renderModelsDialog(info); modelsDialog.showModal(); })
|
|
968
|
+
.catch(function () {});
|
|
969
|
+
});
|
|
970
|
+
var modelsClose = document.getElementById('models-close');
|
|
971
|
+
if (modelsClose) modelsClose.addEventListener('click', function () { modelsDialog.close(); });
|
|
972
|
+
modelsDialog.addEventListener('click', function (e) {
|
|
973
|
+
if (e.target === modelsDialog) modelsDialog.close();
|
|
974
|
+
});
|
|
670
975
|
}
|
|
671
|
-
renderDrafts(data.drafts);
|
|
672
976
|
|
|
673
977
|
/* ---------- Shared diagram helpers ---------- */
|
|
674
978
|
var SVGNS = 'http://www.w3.org/2000/svg';
|
|
@@ -754,6 +1058,123 @@
|
|
|
754
1058
|
mount.appendChild(wrap);
|
|
755
1059
|
}
|
|
756
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
|
+
|
|
757
1178
|
function renderBars(mount, milestones) {
|
|
758
1179
|
mount.textContent = '';
|
|
759
1180
|
if (milestones.length === 0) {
|
|
@@ -789,44 +1210,95 @@
|
|
|
789
1210
|
mount.appendChild(svg);
|
|
790
1211
|
}
|
|
791
1212
|
|
|
792
|
-
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) {
|
|
793
1223
|
mount.textContent = '';
|
|
794
|
-
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) {
|
|
795
1263
|
mount.appendChild(el('p', 'hint', 'No activity data.'));
|
|
796
1264
|
return;
|
|
797
1265
|
}
|
|
798
|
-
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;
|
|
799
1273
|
var max = 1;
|
|
800
1274
|
activity.forEach(function (b) { if (b.count > max) max = b.count; });
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
});
|
|
807
|
-
var coords = activity.map(function (b, i) {
|
|
808
|
-
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);
|
|
809
1280
|
});
|
|
810
|
-
var
|
|
811
|
-
' L ' + coords.join(' L ') +
|
|
812
|
-
' L ' + px(activity.length - 1).toFixed(1) + ',' + (h - padB) + ' Z';
|
|
813
|
-
svg.appendChild(svgEl('path', { d: areaD, 'class': 'spark-area' }));
|
|
814
|
-
svg.appendChild(svgEl('polyline', { points: coords.join(' '), 'class': 'spark-line' }));
|
|
1281
|
+
var lastMonth = '';
|
|
815
1282
|
activity.forEach(function (b, i) {
|
|
816
|
-
var
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
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
|
|
820
1290
|
});
|
|
821
|
-
attachTitle(
|
|
822
|
-
|
|
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
|
+
}
|
|
823
1301
|
});
|
|
824
|
-
var first = svgEl('text', { x: padL, y: h - 7, 'class': 'spark-axis' });
|
|
825
|
-
first.textContent = activity[0].date;
|
|
826
|
-
var last = svgEl('text', { x: w - padR, y: h - 7, 'text-anchor': 'end', 'class': 'spark-axis' });
|
|
827
|
-
last.textContent = activity[activity.length - 1].date;
|
|
828
|
-
svg.appendChild(first);
|
|
829
|
-
svg.appendChild(last);
|
|
830
1302
|
mount.appendChild(svg);
|
|
831
1303
|
}
|
|
832
1304
|
|
|
@@ -897,9 +1369,12 @@
|
|
|
897
1369
|
phases = [];
|
|
898
1370
|
}
|
|
899
1371
|
if ($('#donut')) renderDonut($('#donut'), data.statuses);
|
|
1372
|
+
if ($('#kpis')) renderKpis($('#kpis'), data.kpis);
|
|
1373
|
+
if ($('#aging')) renderAging($('#aging'), data.tasks);
|
|
900
1374
|
if ($('#bars')) renderBars($('#bars'), data.milestones);
|
|
901
1375
|
if ($('#stepper')) renderStepper($('#stepper'), phases);
|
|
902
|
-
if ($('#
|
|
1376
|
+
if ($('#activity-kpis')) renderActivityKpis($('#activity-kpis'), data.kpis);
|
|
1377
|
+
if ($('#heatmap')) renderHeatmap($('#heatmap'), data.activity);
|
|
903
1378
|
|
|
904
1379
|
/* ---------- Glossary tooltips ---------- */
|
|
905
1380
|
var glossary = {};
|
|
@@ -995,32 +1470,67 @@
|
|
|
995
1470
|
return list;
|
|
996
1471
|
}
|
|
997
1472
|
function depSection(titleText, ids) {
|
|
1473
|
+
var valid = ids.filter(function (id) { return findTask(id); });
|
|
1474
|
+
if (valid.length === 0) return null;
|
|
998
1475
|
var block = el('div');
|
|
999
1476
|
block.appendChild(el('h4', '', titleText));
|
|
1000
1477
|
var row = el('div', 'dep-row');
|
|
1001
|
-
|
|
1002
|
-
var
|
|
1478
|
+
valid.forEach(function (id) {
|
|
1479
|
+
var dep = findTask(id);
|
|
1480
|
+
var btn = el('button', 'dep-link');
|
|
1003
1481
|
btn.type = 'button';
|
|
1482
|
+
var dot = el('span', 'dep-dot' + (dep && isDoneStatus(dep.status) ? ' done' : ''));
|
|
1483
|
+
dot.setAttribute('aria-hidden', 'true');
|
|
1484
|
+
btn.appendChild(dot);
|
|
1485
|
+
btn.appendChild(document.createTextNode(id));
|
|
1004
1486
|
btn.addEventListener('click', function () { openDetail(id); });
|
|
1005
1487
|
row.appendChild(btn);
|
|
1006
1488
|
});
|
|
1007
|
-
if (row.children.length === 0) row.appendChild(el('p', 'detail-empty', 'None'));
|
|
1008
1489
|
block.appendChild(row);
|
|
1009
1490
|
return block;
|
|
1010
1491
|
}
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1492
|
+
var PRIORITY_TONE = { high: 'danger', medium: 'warn', low: 'dim' };
|
|
1493
|
+
function priorityTone(priority) {
|
|
1494
|
+
return PRIORITY_TONE[String(priority || '').toLowerCase()] || '';
|
|
1495
|
+
}
|
|
1496
|
+
function metaGrid(task) {
|
|
1497
|
+
var grid = el('div', 'detail-meta');
|
|
1498
|
+
function cell(label, value, tone) {
|
|
1499
|
+
var c = el('div', 'meta-cell');
|
|
1500
|
+
c.appendChild(el('span', 'meta-label', label));
|
|
1501
|
+
var v = el('span', 'meta-value' + (value ? '' : ' empty'), value || '—');
|
|
1502
|
+
if (value && tone) v.setAttribute('data-tone', tone);
|
|
1503
|
+
c.appendChild(v);
|
|
1504
|
+
return c;
|
|
1018
1505
|
}
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
return
|
|
1506
|
+
grid.appendChild(cell('Milestone', task.milestone));
|
|
1507
|
+
grid.appendChild(cell('Priority', task.priority, priorityTone(task.priority)));
|
|
1508
|
+
grid.appendChild(cell('Assignee', task.assignee));
|
|
1509
|
+
grid.appendChild(cell('Updated', task.updated));
|
|
1510
|
+
return grid;
|
|
1511
|
+
}
|
|
1512
|
+
function descParagraphs(text) {
|
|
1513
|
+
var frag = document.createDocumentFragment();
|
|
1514
|
+
String(text).split(/\n\s*\n/).forEach(function (para) {
|
|
1515
|
+
var trimmed = para.trim();
|
|
1516
|
+
if (trimmed !== '') frag.appendChild(el('p', 'detail-desc', trimmed));
|
|
1517
|
+
});
|
|
1518
|
+
return frag;
|
|
1519
|
+
}
|
|
1520
|
+
function acSection(task) {
|
|
1521
|
+
var frag = document.createDocumentFragment();
|
|
1522
|
+
var done = task.acs.filter(function (ac) { return ac.checked; }).length;
|
|
1523
|
+
frag.appendChild(el('h4', '', 'Acceptance criteria'));
|
|
1524
|
+
var progress = el('div', 'ac-progress');
|
|
1525
|
+
progress.appendChild(el('span', 'ac-count', done + ' / ' + task.acs.length + ' done'));
|
|
1526
|
+
var bar = el('div', 'ac-bar');
|
|
1527
|
+
var fill = el('div', 'ac-bar-fill');
|
|
1528
|
+
fill.style.width = task.acs.length > 0 ? Math.round((done / task.acs.length) * 100) + '%' : '0%';
|
|
1529
|
+
bar.appendChild(fill);
|
|
1530
|
+
progress.appendChild(bar);
|
|
1531
|
+
frag.appendChild(progress);
|
|
1532
|
+
frag.appendChild(acList(task));
|
|
1533
|
+
return frag;
|
|
1024
1534
|
}
|
|
1025
1535
|
function openDetail(id) {
|
|
1026
1536
|
var t = findTask(id);
|
|
@@ -1037,15 +1547,29 @@
|
|
|
1037
1547
|
closeBtn.addEventListener('click', closeDetail);
|
|
1038
1548
|
head.appendChild(closeBtn);
|
|
1039
1549
|
content.appendChild(head);
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
content.appendChild(
|
|
1550
|
+
var title = el('h3', 'detail-title', t.title);
|
|
1551
|
+
title.id = 'detail-title-h';
|
|
1552
|
+
dialog.setAttribute('aria-labelledby', 'detail-title-h');
|
|
1553
|
+
content.appendChild(title);
|
|
1554
|
+
if (t.description) {
|
|
1555
|
+
content.appendChild(descParagraphs(t.description));
|
|
1556
|
+
} else {
|
|
1557
|
+
content.appendChild(el('p', 'detail-desc', 'No description.'));
|
|
1046
1558
|
}
|
|
1047
|
-
content.appendChild(
|
|
1048
|
-
content.appendChild(
|
|
1559
|
+
content.appendChild(metaGrid(t));
|
|
1560
|
+
if (t.acs.length > 0) content.appendChild(acSection(t));
|
|
1561
|
+
var out = depSection('Depends on', depsOut[id] || []);
|
|
1562
|
+
if (out) content.appendChild(out);
|
|
1563
|
+
var inn = depSection('Needed by', depsIn[id] || []);
|
|
1564
|
+
if (inn) content.appendChild(inn);
|
|
1565
|
+
var cmd = el('button', 'phase-cmd detail-cmd');
|
|
1566
|
+
cmd.type = 'button';
|
|
1567
|
+
/* backlog task ids are TASK-<n>; other prefixes fall back to the raw id */
|
|
1568
|
+
var cmdLine = 'backlog task edit ' + t.id.replace(/^task-/i, '');
|
|
1569
|
+
cmd.appendChild(el('span', 'cmd-line', cmdLine));
|
|
1570
|
+
cmd.appendChild(el('span', 'cmd-title', 'copy'));
|
|
1571
|
+
cmd.addEventListener('click', function () { copyCommand(cmd, cmdLine); });
|
|
1572
|
+
content.appendChild(cmd);
|
|
1049
1573
|
dialog.textContent = '';
|
|
1050
1574
|
dialog.appendChild(content);
|
|
1051
1575
|
dialog.showModal();
|
|
@@ -1053,6 +1577,76 @@
|
|
|
1053
1577
|
window.__sblOpenDetail = openDetail;
|
|
1054
1578
|
if (dialog) dialog.addEventListener('click', function (ev) { if (ev.target === dialog) closeDetail(); });
|
|
1055
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
|
+
|
|
1056
1650
|
/* ---------- Keyboard shortcuts ---------- */
|
|
1057
1651
|
document.addEventListener('keydown', function (ev) {
|
|
1058
1652
|
if (ev.key === 'Escape') {
|