@profpowell/vb-project-planning 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +93 -0
- package/dist/vb-project-planning.css +1603 -0
- package/dist/vb-project-planning.js +11138 -0
- package/dist/vb-project-planning.min.css +1 -0
- package/dist/vb-project-planning.min.js +3312 -0
- package/package.json +80 -0
- package/src/index.css +14 -0
- package/src/index.js +26 -0
- package/src/lib/bundle-registry.js +156 -0
- package/src/lib/diff-by-key.js +86 -0
- package/src/lib/portrait-url.js +87 -0
- package/src/lib/vb-collection.js +122 -0
- package/src/lib/vb-element.js +96 -0
- package/src/lib/vb-store.js +200 -0
- package/src/lib/vb-view-transition.js +44 -0
- package/src/utils/copy-init.js +287 -0
- package/src/web-components/_ux-base.js +106 -0
- package/src/web-components/adr-wc/README.md +77 -0
- package/src/web-components/adr-wc/api.json +26 -0
- package/src/web-components/adr-wc/logic.js +376 -0
- package/src/web-components/adr-wc/static.html +17 -0
- package/src/web-components/adr-wc/styles.js +338 -0
- package/src/web-components/burndown-chart/api.json +74 -0
- package/src/web-components/burndown-chart/logic.js +198 -0
- package/src/web-components/burndown-chart/static.html +31 -0
- package/src/web-components/burndown-chart/styles.css +59 -0
- package/src/web-components/capacity-plan/_capacity-utils.js +56 -0
- package/src/web-components/capacity-plan/api.json +20 -0
- package/src/web-components/capacity-plan/logic.js +222 -0
- package/src/web-components/capacity-plan/static.html +24 -0
- package/src/web-components/capacity-plan/styles.css +84 -0
- package/src/web-components/empathy-map/api.json +31 -0
- package/src/web-components/empathy-map/logic.js +502 -0
- package/src/web-components/empathy-map/static.html +32 -0
- package/src/web-components/empathy-map/styles.js +534 -0
- package/src/web-components/gantt-chart/README.md +91 -0
- package/src/web-components/gantt-chart/api.json +20 -0
- package/src/web-components/gantt-chart/logic.js +812 -0
- package/src/web-components/gantt-chart/static.html +60 -0
- package/src/web-components/gantt-chart/styles.css +391 -0
- package/src/web-components/impact-effort/api.json +21 -0
- package/src/web-components/impact-effort/logic.js +392 -0
- package/src/web-components/impact-effort/static.html +23 -0
- package/src/web-components/impact-effort/styles.css +193 -0
- package/src/web-components/iron-triangle/_capacity.js +83 -0
- package/src/web-components/iron-triangle/_triangle-geometry.js +322 -0
- package/src/web-components/iron-triangle/api.json +26 -0
- package/src/web-components/iron-triangle/logic.js +474 -0
- package/src/web-components/iron-triangle/static.html +28 -0
- package/src/web-components/iron-triangle/styles.css +158 -0
- package/src/web-components/kanban-board/api.json +24 -0
- package/src/web-components/kanban-board/logic.js +698 -0
- package/src/web-components/kanban-board/static.html +20 -0
- package/src/web-components/kanban-board/styles.css +242 -0
- package/src/web-components/product-roadmap/api.json +118 -0
- package/src/web-components/product-roadmap/logic.js +460 -0
- package/src/web-components/product-roadmap/static.html +44 -0
- package/src/web-components/product-roadmap/styles.css +158 -0
- package/src/web-components/quadrant-grid/api.json +125 -0
- package/src/web-components/quadrant-grid/logic.js +397 -0
- package/src/web-components/quadrant-grid/static.html +49 -0
- package/src/web-components/quadrant-grid/styles.css +184 -0
- package/src/web-components/quality-target/_quality-utils.js +460 -0
- package/src/web-components/quality-target/api.json +30 -0
- package/src/web-components/quality-target/logic.js +516 -0
- package/src/web-components/quality-target/static.html +22 -0
- package/src/web-components/quality-target/styles.css +203 -0
- package/src/web-components/requirement-card/api.json +25 -0
- package/src/web-components/requirement-card/logic.js +166 -0
- package/src/web-components/requirement-card/static.html +36 -0
- package/src/web-components/requirement-card/styles.js +114 -0
- package/src/web-components/review-surface/README.md +178 -0
- package/src/web-components/review-surface/api.json +30 -0
- package/src/web-components/review-surface/logic.js +807 -0
- package/src/web-components/review-surface/static.html +33 -0
- package/src/web-components/review-surface/styles.js +459 -0
- package/src/web-components/risk-register/api.json +47 -0
- package/src/web-components/risk-register/logic.js +180 -0
- package/src/web-components/risk-register/static.html +46 -0
- package/src/web-components/risk-register/styles.css +37 -0
- package/src/web-components/story-map/api.json +22 -0
- package/src/web-components/story-map/logic.js +445 -0
- package/src/web-components/story-map/static.html +35 -0
- package/src/web-components/story-map/styles.css +169 -0
- package/src/web-components/traceability-matrix/api.json +84 -0
- package/src/web-components/traceability-matrix/logic.js +243 -0
- package/src/web-components/traceability-matrix/static.html +41 -0
- package/src/web-components/traceability-matrix/styles.css +94 -0
- package/src/web-components/user-journey/api.json +84 -0
- package/src/web-components/user-journey/logic.js +368 -0
- package/src/web-components/user-journey/static.html +14 -0
- package/src/web-components/user-journey/styles.js +317 -0
- package/src/web-components/user-persona/api.json +35 -0
- package/src/web-components/user-persona/logic.js +502 -0
- package/src/web-components/user-persona/static.html +20 -0
- package/src/web-components/user-persona/styles.js +299 -0
- package/src/web-components/user-story/api.json +35 -0
- package/src/web-components/user-story/logic.js +567 -0
- package/src/web-components/user-story/static.html +27 -0
- package/src/web-components/user-story/styles.js +332 -0
- package/src/web-components/work-item/README.md +85 -0
- package/src/web-components/work-item/api.json +30 -0
- package/src/web-components/work-item/logic.js +414 -0
- package/src/web-components/work-item/static.html +24 -0
- package/src/web-components/work-item/styles.js +376 -0
|
@@ -0,0 +1,338 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shadow DOM styles for <adr-wc>
|
|
3
|
+
*
|
|
4
|
+
* 3-tier token pattern:
|
|
5
|
+
* var(--adr-wc-*, var(--color-*, fallback))
|
|
6
|
+
*
|
|
7
|
+
* MADR-format Architectural Decision Record card.
|
|
8
|
+
* Sibling to user-story and work-item in the UX Planning Pack.
|
|
9
|
+
*/
|
|
10
|
+
const styles = `
|
|
11
|
+
:host {
|
|
12
|
+
--_bg: var(--adr-wc-bg, var(--color-surface, #ffffff));
|
|
13
|
+
--_text: var(--adr-wc-text, var(--color-text, #1a1a1a));
|
|
14
|
+
--_muted: var(--adr-wc-muted, var(--color-text-muted, #666));
|
|
15
|
+
--_border: var(--adr-wc-border, var(--color-border, #e0e0e0));
|
|
16
|
+
--_accent: var(--adr-wc-accent, var(--color-interactive, #0066cc));
|
|
17
|
+
--_card-bg: var(--adr-wc-card-bg, var(--color-surface-raised, #f8f9fa));
|
|
18
|
+
--_highlight: var(--adr-wc-highlight, color-mix(in srgb, var(--_accent) 8%, transparent));
|
|
19
|
+
--_radius: var(--adr-wc-radius, var(--radius-l, 0.75rem));
|
|
20
|
+
--_shadow: var(--adr-wc-shadow, var(--shadow-sm));
|
|
21
|
+
--_shadow-hover: var(--adr-wc-shadow-hover, var(--shadow-md));
|
|
22
|
+
--_duration: var(--adr-wc-duration, var(--duration-normal, 200ms));
|
|
23
|
+
--_ease: var(--adr-wc-ease, var(--ease-default, ease));
|
|
24
|
+
--_space-2xs: var(--adr-wc-space-2xs, var(--size-2xs, 0.25rem));
|
|
25
|
+
--_space-xs: var(--adr-wc-space-xs, var(--size-xs, 0.5rem));
|
|
26
|
+
--_space-s: var(--adr-wc-space-s, var(--size-s, 0.75rem));
|
|
27
|
+
--_space-m: var(--adr-wc-space-m, var(--size-m, 1rem));
|
|
28
|
+
--_font-xs: var(--adr-wc-font-xs, var(--font-size-xs, 0.75rem));
|
|
29
|
+
--_font-sm: var(--adr-wc-font-sm, var(--font-size-sm, 0.875rem));
|
|
30
|
+
--_font-md: var(--adr-wc-font-md, var(--font-size-md, 1rem));
|
|
31
|
+
--_font-lg: var(--adr-wc-font-lg, var(--font-size-lg, 1.125rem));
|
|
32
|
+
--_font-mono: var(--adr-wc-font-mono, var(--font-mono, ui-monospace, "Cascadia Code", "Source Code Pro", Menlo, monospace));
|
|
33
|
+
--_radius-s: var(--adr-wc-radius-s, var(--radius-s, 0.25rem));
|
|
34
|
+
--_radius-full: var(--adr-wc-radius-full, var(--radius-full, 9999px));
|
|
35
|
+
|
|
36
|
+
display: block;
|
|
37
|
+
font-family: var(--adr-wc-font, var(--font-sans, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif));
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
*, *::before, *::after { box-sizing: border-box; margin: 0; }
|
|
41
|
+
|
|
42
|
+
/* ── Card ──────────────────────────────────────────── */
|
|
43
|
+
.adr-card {
|
|
44
|
+
background: var(--_bg);
|
|
45
|
+
border: 1px solid var(--_border);
|
|
46
|
+
border-radius: var(--_radius);
|
|
47
|
+
overflow: hidden;
|
|
48
|
+
box-shadow: var(--_shadow);
|
|
49
|
+
transition: box-shadow var(--_duration) var(--_ease);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
.adr-card:hover {
|
|
53
|
+
box-shadow: var(--_shadow-hover);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/* ── Header ────────────────────────────────────────── */
|
|
57
|
+
.adr-header {
|
|
58
|
+
display: flex;
|
|
59
|
+
align-items: center;
|
|
60
|
+
justify-content: space-between;
|
|
61
|
+
padding: var(--_space-s) var(--_space-m);
|
|
62
|
+
background: var(--_card-bg);
|
|
63
|
+
border-block-end: 1px solid var(--_border);
|
|
64
|
+
gap: var(--_space-s);
|
|
65
|
+
flex-wrap: wrap;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
.adr-meta {
|
|
69
|
+
display: flex;
|
|
70
|
+
align-items: center;
|
|
71
|
+
gap: var(--_space-xs);
|
|
72
|
+
flex-wrap: wrap;
|
|
73
|
+
min-width: 0;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
.adr-id {
|
|
77
|
+
font-size: var(--_font-xs);
|
|
78
|
+
font-weight: 600;
|
|
79
|
+
color: var(--_muted);
|
|
80
|
+
font-family: var(--_font-mono);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/* ── Status badge ──────────────────────────────────── */
|
|
84
|
+
.adr-badges {
|
|
85
|
+
display: flex;
|
|
86
|
+
align-items: center;
|
|
87
|
+
gap: var(--_space-xs);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
.adr-status {
|
|
91
|
+
font-size: 11px;
|
|
92
|
+
padding: 4px 10px;
|
|
93
|
+
border-radius: var(--_radius-full);
|
|
94
|
+
font-weight: 600;
|
|
95
|
+
text-transform: uppercase;
|
|
96
|
+
letter-spacing: 0.3px;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/* ── Date (slotted <time>) ─────────────────────────── */
|
|
100
|
+
.adr-date-wrap {
|
|
101
|
+
display: inline-flex;
|
|
102
|
+
align-items: center;
|
|
103
|
+
gap: 4px;
|
|
104
|
+
font-size: var(--_font-xs);
|
|
105
|
+
color: var(--_muted);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
.adr-date-wrap svg {
|
|
109
|
+
width: 12px;
|
|
110
|
+
height: 12px;
|
|
111
|
+
flex-shrink: 0;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
::slotted([slot="date"]) {
|
|
115
|
+
font-size: var(--_font-xs);
|
|
116
|
+
font-family: var(--_font-mono);
|
|
117
|
+
color: var(--_muted);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/* ── Body ──────────────────────────────────────────── */
|
|
121
|
+
.adr-body {
|
|
122
|
+
padding: 16px 20px;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/* ── Title (slotted heading) ───────────────────────── */
|
|
126
|
+
.adr-title-wrap {
|
|
127
|
+
margin-block-end: 0;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
::slotted([slot="title"]) {
|
|
131
|
+
font-size: var(--_font-lg) !important;
|
|
132
|
+
font-weight: 700 !important;
|
|
133
|
+
color: var(--_text) !important;
|
|
134
|
+
line-height: 1.4 !important;
|
|
135
|
+
margin: 0 !important;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/* Fallback title when no slot content */
|
|
139
|
+
.adr-title-fallback {
|
|
140
|
+
font-size: var(--_font-lg);
|
|
141
|
+
font-weight: 700;
|
|
142
|
+
color: var(--_text);
|
|
143
|
+
line-height: 1.4;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/* ── Supersedes / Superseded-by links ──────────────── */
|
|
147
|
+
.adr-links {
|
|
148
|
+
display: flex;
|
|
149
|
+
flex-wrap: wrap;
|
|
150
|
+
gap: 4px;
|
|
151
|
+
margin-block-start: var(--_space-s);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
.adr-links-label {
|
|
155
|
+
font-size: 11px;
|
|
156
|
+
font-weight: 600;
|
|
157
|
+
color: var(--_muted);
|
|
158
|
+
text-transform: uppercase;
|
|
159
|
+
letter-spacing: 0.3px;
|
|
160
|
+
margin-inline-end: var(--_space-2xs);
|
|
161
|
+
align-self: center;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
.adr-link {
|
|
165
|
+
display: inline-flex;
|
|
166
|
+
align-items: center;
|
|
167
|
+
gap: 3px;
|
|
168
|
+
font-size: 11px;
|
|
169
|
+
font-weight: 500;
|
|
170
|
+
padding: 2px 8px;
|
|
171
|
+
border-radius: var(--_radius-s);
|
|
172
|
+
background: var(--_highlight);
|
|
173
|
+
color: var(--_accent);
|
|
174
|
+
text-decoration: none;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
.adr-link:hover {
|
|
178
|
+
text-decoration: underline;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
.adr-link svg {
|
|
182
|
+
width: 11px;
|
|
183
|
+
height: 11px;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/* ── Sections ──────────────────────────────────────── */
|
|
187
|
+
.adr-sections {
|
|
188
|
+
border-block-start: 1px solid var(--_border);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
.adr-section {
|
|
192
|
+
padding: 14px 20px;
|
|
193
|
+
border-block-end: 1px solid var(--_border);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
.adr-section:last-child {
|
|
197
|
+
border-block-end: none;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
.adr-section-header {
|
|
201
|
+
display: flex;
|
|
202
|
+
align-items: center;
|
|
203
|
+
gap: var(--_space-xs);
|
|
204
|
+
margin-block-end: var(--_space-s);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
.adr-section-icon {
|
|
208
|
+
width: 22px;
|
|
209
|
+
height: 22px;
|
|
210
|
+
border-radius: 5px;
|
|
211
|
+
display: flex;
|
|
212
|
+
align-items: center;
|
|
213
|
+
justify-content: center;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
.adr-section-icon svg {
|
|
217
|
+
width: 13px;
|
|
218
|
+
height: 13px;
|
|
219
|
+
fill: var(--color-text-inverted, #fff);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
.adr-section-icon.context { background: var(--color-accent, #8b5cf6); }
|
|
223
|
+
.adr-section-icon.decision { background: var(--color-success, #22c55e); }
|
|
224
|
+
.adr-section-icon.consequences { background: var(--color-warning, #f59e0b); }
|
|
225
|
+
|
|
226
|
+
.adr-section-title {
|
|
227
|
+
font-size: var(--_font-xs);
|
|
228
|
+
font-weight: 600;
|
|
229
|
+
color: var(--_muted);
|
|
230
|
+
text-transform: uppercase;
|
|
231
|
+
letter-spacing: 0.5px;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
.adr-section-content {
|
|
235
|
+
color: var(--_text);
|
|
236
|
+
font-size: var(--_font-sm);
|
|
237
|
+
line-height: 1.6;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
.adr-section-content ::slotted(ul),
|
|
241
|
+
.adr-section-content ::slotted(ol) {
|
|
242
|
+
margin: 0;
|
|
243
|
+
padding-inline-start: 20px;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
.adr-section-content ::slotted(p) {
|
|
247
|
+
margin: 0;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
.slot-fallback {
|
|
251
|
+
color: var(--_muted);
|
|
252
|
+
font-style: italic;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/* ── Minimal detail ────────────────────────────────── */
|
|
256
|
+
.adr-card--minimal {
|
|
257
|
+
padding: var(--_space-xs);
|
|
258
|
+
cursor: pointer;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
.adr-card--minimal:hover {
|
|
262
|
+
box-shadow: var(--_shadow-hover);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
.adr-card--minimal:focus-visible {
|
|
266
|
+
outline: 2px solid var(--_accent);
|
|
267
|
+
outline-offset: 2px;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
.adr-card--minimal .adr-header { display: none; }
|
|
271
|
+
.adr-card--minimal .adr-sections { display: none; }
|
|
272
|
+
.adr-card--minimal .adr-links { display: none; }
|
|
273
|
+
.adr-card--minimal .adr-date-wrap { display: none; }
|
|
274
|
+
|
|
275
|
+
.adr-card--minimal .adr-body {
|
|
276
|
+
padding: var(--_space-xs) var(--_space-s);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
.adr-card--minimal .adr-title-fallback,
|
|
280
|
+
.adr-card--minimal ::slotted([slot="title"]) {
|
|
281
|
+
font-size: var(--_font-sm) !important;
|
|
282
|
+
font-weight: 600 !important;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
.adr-card--minimal .adr-id {
|
|
286
|
+
display: block;
|
|
287
|
+
margin-block-end: var(--_space-2xs);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
.adr-card--minimal .adr-status {
|
|
291
|
+
font-size: 10px;
|
|
292
|
+
padding: 2px 6px;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/* ── Compact detail — hide empty sections ──────────── */
|
|
296
|
+
.adr-card--compact .adr-section[data-empty] {
|
|
297
|
+
display: none;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
.adr-card--compact .slot-fallback {
|
|
301
|
+
display: none;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
.adr-card--compact .adr-body {
|
|
305
|
+
padding: var(--_space-s) var(--_space-m);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/* ── Responsive ────────────────────────────────────── */
|
|
309
|
+
@media (max-width: 480px) {
|
|
310
|
+
.adr-header {
|
|
311
|
+
flex-direction: column;
|
|
312
|
+
align-items: flex-start;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
.adr-badges {
|
|
316
|
+
width: 100%;
|
|
317
|
+
justify-content: flex-start;
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
@media (prefers-reduced-motion: reduce) {
|
|
322
|
+
.adr-card {
|
|
323
|
+
transition: none;
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
@media print {
|
|
328
|
+
.adr-card {
|
|
329
|
+
box-shadow: none;
|
|
330
|
+
break-inside: avoid;
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
.state-msg { padding: var(--_space-m); font-size: var(--_font-sm); color: var(--_muted); font-style: italic; }
|
|
335
|
+
.state-msg--error { color: var(--color-error-text, var(--color-error, #dc2626)); }
|
|
336
|
+
`;
|
|
337
|
+
|
|
338
|
+
export { styles };
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "../../../schemas/api.schema.json",
|
|
3
|
+
"element": "burndown-chart",
|
|
4
|
+
"type": "web-component",
|
|
5
|
+
"description": "Sprint burndown plot. Wraps <chart-wc> with auto-computed ideal line and scope-change annotations.",
|
|
6
|
+
"htmlvalidate": {
|
|
7
|
+
"flow": true,
|
|
8
|
+
"permittedContent": [
|
|
9
|
+
"@flow"
|
|
10
|
+
]
|
|
11
|
+
},
|
|
12
|
+
"attributes": [
|
|
13
|
+
{
|
|
14
|
+
"name": "start",
|
|
15
|
+
"kind": "host-api",
|
|
16
|
+
"purpose": "config",
|
|
17
|
+
"type": "string",
|
|
18
|
+
"description": "Sprint start date (ISO 8601, required)"
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
"name": "end",
|
|
22
|
+
"kind": "host-api",
|
|
23
|
+
"purpose": "config",
|
|
24
|
+
"type": "string",
|
|
25
|
+
"description": "Sprint end date (ISO 8601, inclusive, required)"
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
"name": "total",
|
|
29
|
+
"kind": "host-api",
|
|
30
|
+
"purpose": "config",
|
|
31
|
+
"type": "number",
|
|
32
|
+
"description": "Total points/items at sprint start (required)"
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
"name": "unit",
|
|
36
|
+
"kind": "host-api",
|
|
37
|
+
"purpose": "config",
|
|
38
|
+
"type": "string",
|
|
39
|
+
"description": "Display unit, default \"points\""
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
"name": "label",
|
|
43
|
+
"kind": "host-api",
|
|
44
|
+
"purpose": "config",
|
|
45
|
+
"type": "string",
|
|
46
|
+
"description": "Optional heading text (e.g. \"Sprint 14\")"
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
"name": "weekends",
|
|
50
|
+
"kind": "host-api",
|
|
51
|
+
"purpose": "config",
|
|
52
|
+
"type": "enum",
|
|
53
|
+
"values": [
|
|
54
|
+
"include",
|
|
55
|
+
"exclude"
|
|
56
|
+
],
|
|
57
|
+
"description": "Whether weekends count toward the ideal-line slope"
|
|
58
|
+
}
|
|
59
|
+
],
|
|
60
|
+
"structure": [
|
|
61
|
+
{
|
|
62
|
+
"element": "<template>",
|
|
63
|
+
"description": "Inline data source: <tr> rows with cells [date YYYY-MM-DD, remaining, optional ±delta scope-change]"
|
|
64
|
+
}
|
|
65
|
+
],
|
|
66
|
+
"childAttributes": [],
|
|
67
|
+
"events": [
|
|
68
|
+
{
|
|
69
|
+
"name": "burndown-chart:ready",
|
|
70
|
+
"detail": "{ dayCount, total, scopeChanges }",
|
|
71
|
+
"description": "Fired after the chart renders"
|
|
72
|
+
}
|
|
73
|
+
]
|
|
74
|
+
}
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* burndown-chart: Sprint burndown plot
|
|
3
|
+
*
|
|
4
|
+
* Thin composing preset that wraps <chart-wc type="line"> with two
|
|
5
|
+
* series: an `Actual` series from the author's daily remaining counts
|
|
6
|
+
* and an `Ideal` series the component computes from start/end/total.
|
|
7
|
+
*
|
|
8
|
+
* Replaces the boilerplate of hand-computing the ideal trend line and
|
|
9
|
+
* formatting two parallel data series. Authors supply only the daily
|
|
10
|
+
* actuals (and optional scope-change deltas) — the ideal slope is
|
|
11
|
+
* mechanical.
|
|
12
|
+
*
|
|
13
|
+
* Data source: a <template> child whose <tr> rows are
|
|
14
|
+
* <tr><td>YYYY-MM-DD</td><td>remaining</td></tr>
|
|
15
|
+
* <tr><td>YYYY-MM-DD</td><td>remaining</td><td>±N</td></tr>
|
|
16
|
+
* The optional 3rd cell is a scope-change delta on that day; the
|
|
17
|
+
* component renders an inline annotation note (no chart marker primitive
|
|
18
|
+
* exists in chart-wc yet, so we surface scope changes via a captioned
|
|
19
|
+
* list under the chart).
|
|
20
|
+
*
|
|
21
|
+
* @attr {string} start - Sprint start date (ISO 8601, required)
|
|
22
|
+
* @attr {string} end - Sprint end date (ISO 8601, required, inclusive)
|
|
23
|
+
* @attr {number} total - Total points/items at sprint start (required)
|
|
24
|
+
* @attr {string} unit - Display unit, default "points"
|
|
25
|
+
* @attr {string} label - Optional heading text (e.g., "Sprint 14")
|
|
26
|
+
* @attr {string} weekends - "include" (default) or "exclude" from ideal slope
|
|
27
|
+
*
|
|
28
|
+
* @fires burndown-chart:ready - { dayCount, total, scopeChanges }
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
import { registerComponent } from '../../lib/bundle-registry.js';
|
|
32
|
+
import { VBElement } from '../../lib/vb-element.js';
|
|
33
|
+
|
|
34
|
+
const MS_PER_DAY = 86_400_000;
|
|
35
|
+
|
|
36
|
+
class BurndownChart extends VBElement {
|
|
37
|
+
setup() {
|
|
38
|
+
const start = this.getAttribute('start');
|
|
39
|
+
const end = this.getAttribute('end');
|
|
40
|
+
const total = parseFloat(this.getAttribute('total') || '');
|
|
41
|
+
|
|
42
|
+
if (!start || !end || !Number.isFinite(total)) {
|
|
43
|
+
console.warn('burndown-chart: requires start, end, and total attributes');
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const startDate = BurndownChart.#parseDate(start);
|
|
48
|
+
const endDate = BurndownChart.#parseDate(end);
|
|
49
|
+
if (!startDate || !endDate || endDate < startDate) {
|
|
50
|
+
console.warn('burndown-chart: start/end dates invalid', start, end);
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const includeWeekends = (this.getAttribute('weekends') || 'include') !== 'exclude';
|
|
55
|
+
const unit = this.getAttribute('unit') || 'points';
|
|
56
|
+
const label = this.getAttribute('label') || '';
|
|
57
|
+
|
|
58
|
+
const tmpl = this.querySelector(':scope > template');
|
|
59
|
+
const samples = tmpl ? BurndownChart.#parseTemplate(tmpl) : [];
|
|
60
|
+
const scopeChanges = samples.filter((s) => s.delta !== 0);
|
|
61
|
+
|
|
62
|
+
const dayLabels = BurndownChart.#enumerateDays(startDate, endDate);
|
|
63
|
+
const ideal = BurndownChart.#idealSeries(dayLabels, total, includeWeekends);
|
|
64
|
+
const actual = BurndownChart.#actualSeries(dayLabels, samples);
|
|
65
|
+
|
|
66
|
+
this.#render({ label, unit, dayLabels, actual, ideal, scopeChanges });
|
|
67
|
+
|
|
68
|
+
queueMicrotask(() => {
|
|
69
|
+
this.dispatchEvent(new CustomEvent('burndown-chart:ready', {
|
|
70
|
+
bubbles: true,
|
|
71
|
+
detail: { dayCount: dayLabels.length, total, scopeChanges: scopeChanges.length },
|
|
72
|
+
}));
|
|
73
|
+
});
|
|
74
|
+
return true;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
#render({ label, unit, dayLabels, actual, ideal, scopeChanges }) {
|
|
78
|
+
[...this.children].forEach((c) => { if (c.tagName !== 'TEMPLATE') c.remove(); });
|
|
79
|
+
|
|
80
|
+
if (label) {
|
|
81
|
+
const h = document.createElement('header');
|
|
82
|
+
h.className = 'bdc-label';
|
|
83
|
+
h.textContent = label;
|
|
84
|
+
this.appendChild(h);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const chart = document.createElement('chart-wc');
|
|
88
|
+
chart.setAttribute('data-type', 'line');
|
|
89
|
+
chart.setAttribute('data-legend', '');
|
|
90
|
+
chart.setAttribute('data-tooltip', '');
|
|
91
|
+
chart.setAttribute('data-label-x', 'Day');
|
|
92
|
+
chart.setAttribute('data-label-y', unit);
|
|
93
|
+
|
|
94
|
+
const idealValues = {};
|
|
95
|
+
const actualValues = {};
|
|
96
|
+
dayLabels.forEach((d, i) => {
|
|
97
|
+
idealValues[d] = ideal[i];
|
|
98
|
+
if (actual[i] !== null) actualValues[d] = actual[i];
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
const payload = [
|
|
102
|
+
{ name: 'Actual', values: actualValues },
|
|
103
|
+
{ name: 'Ideal', values: idealValues },
|
|
104
|
+
];
|
|
105
|
+
chart.setAttribute('data-values', JSON.stringify(payload));
|
|
106
|
+
this.appendChild(chart);
|
|
107
|
+
|
|
108
|
+
if (scopeChanges.length) {
|
|
109
|
+
const notes = document.createElement('aside');
|
|
110
|
+
notes.className = 'bdc-scope-changes';
|
|
111
|
+
const heading = document.createElement('h4');
|
|
112
|
+
heading.textContent = 'Scope changes';
|
|
113
|
+
notes.appendChild(heading);
|
|
114
|
+
const list = document.createElement('ul');
|
|
115
|
+
for (const change of scopeChanges) {
|
|
116
|
+
const li = document.createElement('li');
|
|
117
|
+
const sign = change.delta > 0 ? '+' : '';
|
|
118
|
+
li.innerHTML = `<time datetime="${change.date}">${change.date}</time>: <strong>${sign}${change.delta} ${unit}</strong>`;
|
|
119
|
+
list.appendChild(li);
|
|
120
|
+
}
|
|
121
|
+
notes.appendChild(list);
|
|
122
|
+
this.appendChild(notes);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
static #parseTemplate(tmpl) {
|
|
127
|
+
const out = [];
|
|
128
|
+
const trs = tmpl.content.querySelectorAll('tr');
|
|
129
|
+
for (const tr of trs) {
|
|
130
|
+
const cells = [...tr.children];
|
|
131
|
+
const date = cells[0]?.textContent?.trim();
|
|
132
|
+
const remaining = parseFloat(cells[1]?.textContent ?? '');
|
|
133
|
+
const delta = parseFloat(cells[2]?.textContent ?? '0') || 0;
|
|
134
|
+
if (!date || !Number.isFinite(remaining)) continue;
|
|
135
|
+
out.push({ date, remaining, delta });
|
|
136
|
+
}
|
|
137
|
+
return out;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
static #parseDate(s) {
|
|
141
|
+
/* Treat YYYY-MM-DD as a local-midnight date so weekend math is
|
|
142
|
+
deterministic across viewer time zones. */
|
|
143
|
+
const m = String(s).match(/^(\d{4})-(\d{2})-(\d{2})/);
|
|
144
|
+
if (!m) return null;
|
|
145
|
+
return new Date(Number(m[1]), Number(m[2]) - 1, Number(m[3]));
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
static #toISO(d) {
|
|
149
|
+
const m = String(d.getMonth() + 1).padStart(2, '0');
|
|
150
|
+
const day = String(d.getDate()).padStart(2, '0');
|
|
151
|
+
return `${d.getFullYear()}-${m}-${day}`;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
static #enumerateDays(start, end) {
|
|
155
|
+
const days = [];
|
|
156
|
+
for (let t = start.getTime(); t <= end.getTime(); t += MS_PER_DAY) {
|
|
157
|
+
days.push(BurndownChart.#toISO(new Date(t)));
|
|
158
|
+
}
|
|
159
|
+
return days;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
static #idealSeries(dayLabels, total, includeWeekends) {
|
|
163
|
+
const isWeekend = (label) => {
|
|
164
|
+
const d = BurndownChart.#parseDate(label);
|
|
165
|
+
if (!d) return false;
|
|
166
|
+
const w = d.getDay();
|
|
167
|
+
return w === 0 || w === 6;
|
|
168
|
+
};
|
|
169
|
+
const workingIndices = dayLabels
|
|
170
|
+
.map((d, i) => ({ d, i, weekend: isWeekend(d) }))
|
|
171
|
+
.filter((entry) => includeWeekends || !entry.weekend);
|
|
172
|
+
const decrement = workingIndices.length > 1 ? total / (workingIndices.length - 1) : 0;
|
|
173
|
+
const out = new Array(dayLabels.length).fill(null);
|
|
174
|
+
let lastIdeal = total;
|
|
175
|
+
let workingSeen = 0;
|
|
176
|
+
for (let i = 0; i < dayLabels.length; i++) {
|
|
177
|
+
const isWeekendDay = isWeekend(dayLabels[i]);
|
|
178
|
+
if (includeWeekends || !isWeekendDay) {
|
|
179
|
+
out[i] = Math.max(0, total - decrement * workingSeen);
|
|
180
|
+
workingSeen++;
|
|
181
|
+
lastIdeal = out[i];
|
|
182
|
+
} else {
|
|
183
|
+
/* Weekend in exclude mode: hold previous ideal flat. */
|
|
184
|
+
out[i] = lastIdeal;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
return out;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
static #actualSeries(dayLabels, samples) {
|
|
191
|
+
const byDate = new Map(samples.map((s) => [s.date, s.remaining]));
|
|
192
|
+
return dayLabels.map((d) => (byDate.has(d) ? byDate.get(d) : null));
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
registerComponent('burndown-chart', BurndownChart);
|
|
197
|
+
|
|
198
|
+
export { BurndownChart };
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
6
|
+
<title>Burndown Chart — Static Fallback</title>
|
|
7
|
+
</head>
|
|
8
|
+
<body>
|
|
9
|
+
<!--
|
|
10
|
+
Static fallback for <burndown-chart>.
|
|
11
|
+
|
|
12
|
+
Upgrade relationship:
|
|
13
|
+
- Without JS: the <template> below is invisible (templates do not
|
|
14
|
+
render). The component is purely additive over <chart-wc>.
|
|
15
|
+
- With JS: the component reads start/end/total + the <template>
|
|
16
|
+
rows, computes the ideal-line series, builds a <chart-wc> with
|
|
17
|
+
two series (Actual + Ideal), and renders scope-change notes
|
|
18
|
+
below the chart when the optional 3rd cell is present.
|
|
19
|
+
-->
|
|
20
|
+
|
|
21
|
+
<burndown-chart start="2026-05-04" end="2026-05-15" total="42" label="Sprint 14">
|
|
22
|
+
<template>
|
|
23
|
+
<tr><td>2026-05-04</td><td>42</td></tr>
|
|
24
|
+
<tr><td>2026-05-05</td><td>40</td></tr>
|
|
25
|
+
<tr><td>2026-05-06</td><td>37</td></tr>
|
|
26
|
+
<tr><td>2026-05-07</td><td>32</td><td>+5</td></tr>
|
|
27
|
+
<tr><td>2026-05-08</td><td>28</td></tr>
|
|
28
|
+
</template>
|
|
29
|
+
</burndown-chart>
|
|
30
|
+
</body>
|
|
31
|
+
</html>
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/* burndown-chart: Sprint burndown plot */
|
|
2
|
+
|
|
3
|
+
burndown-chart:not(:defined) {
|
|
4
|
+
display: block;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
burndown-chart {
|
|
8
|
+
display: block;
|
|
9
|
+
font-family: var(--font-sans, system-ui);
|
|
10
|
+
color: var(--color-text, #1a1a1a);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
@scope (burndown-chart) {
|
|
14
|
+
.bdc-label {
|
|
15
|
+
display: block;
|
|
16
|
+
font-size: var(--font-size-xs, 0.75rem);
|
|
17
|
+
font-weight: 700;
|
|
18
|
+
text-transform: uppercase;
|
|
19
|
+
letter-spacing: 0.06em;
|
|
20
|
+
color: var(--color-text-muted, #666);
|
|
21
|
+
margin-block-end: var(--size-s, 0.75rem);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
chart-wc {
|
|
25
|
+
display: block;
|
|
26
|
+
min-block-size: var(--burndown-min-height, 18rem);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
.bdc-scope-changes {
|
|
30
|
+
margin-block-start: var(--size-m, 1rem);
|
|
31
|
+
padding: var(--size-s, 0.75rem) var(--size-m, 1rem);
|
|
32
|
+
background: var(--color-surface-raised, oklch(96% 0 0));
|
|
33
|
+
border-inline-start: var(--border-width-medium, 3px) solid var(--color-warning, oklch(70% 0.15 60));
|
|
34
|
+
border-radius: var(--radius-s, 0.25rem);
|
|
35
|
+
font-size: var(--font-size-sm, 0.875rem);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
.bdc-scope-changes h4 {
|
|
39
|
+
margin: 0 0 var(--size-2xs, 0.25rem);
|
|
40
|
+
font-size: var(--font-size-xs, 0.75rem);
|
|
41
|
+
text-transform: uppercase;
|
|
42
|
+
letter-spacing: 0.06em;
|
|
43
|
+
color: var(--color-text-muted, #666);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
.bdc-scope-changes ul {
|
|
47
|
+
margin: 0;
|
|
48
|
+
padding-inline-start: var(--size-m, 1rem);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
.bdc-scope-changes li {
|
|
52
|
+
margin-block: var(--size-3xs, 0.125rem);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
.bdc-scope-changes time {
|
|
56
|
+
font-family: var(--font-mono, ui-monospace, monospace);
|
|
57
|
+
color: var(--color-text-muted, #666);
|
|
58
|
+
}
|
|
59
|
+
}
|