@profpowell/vb-project-planning 0.2.1 → 0.2.3

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.
Files changed (66) hide show
  1. package/README.md +10 -5
  2. package/dist/vb-project-planning.css +69 -3
  3. package/dist/vb-project-planning.js +3191 -2544
  4. package/dist/vb-project-planning.min.css +1 -1
  5. package/dist/vb-project-planning.min.js +221 -197
  6. package/package.json +7 -4
  7. package/src/lib/board-surface.js +158 -0
  8. package/src/lib/bundle-registry.js +2 -75
  9. package/src/lib/dates.js +125 -0
  10. package/src/lib/escape.js +34 -0
  11. package/src/lib/hash.js +16 -0
  12. package/src/lib/portrait-url.js +3 -29
  13. package/src/lib/src-loader.js +78 -0
  14. package/src/lib/vb-view-transition.js +7 -1
  15. package/src/utils/copy-init.js +56 -17
  16. package/src/web-components/_ux-base.js +409 -24
  17. package/src/web-components/adr-wc/README.md +18 -16
  18. package/src/web-components/adr-wc/api.json +7 -5
  19. package/src/web-components/adr-wc/logic.js +50 -136
  20. package/src/web-components/burndown-chart/api.json +8 -6
  21. package/src/web-components/burndown-chart/logic.js +75 -39
  22. package/src/web-components/empathy-map/api.json +13 -13
  23. package/src/web-components/empathy-map/logic.js +161 -168
  24. package/src/web-components/empathy-map/static.html +3 -3
  25. package/src/web-components/empathy-map/styles.js +1 -0
  26. package/src/web-components/gantt-chart/README.md +12 -6
  27. package/src/web-components/gantt-chart/api.json +23 -7
  28. package/src/web-components/gantt-chart/logic.js +456 -431
  29. package/src/web-components/gantt-chart/styles.css +14 -1
  30. package/src/web-components/impact-effort/api.json +9 -7
  31. package/src/web-components/impact-effort/logic.js +238 -209
  32. package/src/web-components/impact-effort/static.html +12 -4
  33. package/src/web-components/iron-triangle/_capacity.js +0 -18
  34. package/src/web-components/kanban-board/api.json +8 -6
  35. package/src/web-components/kanban-board/logic.js +289 -333
  36. package/src/web-components/product-roadmap/api.json +14 -13
  37. package/src/web-components/product-roadmap/logic.js +393 -246
  38. package/src/web-components/product-roadmap/styles.css +40 -0
  39. package/src/web-components/quadrant-grid/api.json +9 -4
  40. package/src/web-components/quadrant-grid/logic.js +80 -12
  41. package/src/web-components/quadrant-grid/styles.css +7 -0
  42. package/src/web-components/quality-target/_quality-utils.js +2 -46
  43. package/src/web-components/review-surface/README.md +9 -5
  44. package/src/web-components/review-surface/api.json +10 -8
  45. package/src/web-components/review-surface/logic.js +349 -218
  46. package/src/web-components/risk-register/api.json +1 -6
  47. package/src/web-components/risk-register/logic.js +71 -24
  48. package/src/web-components/risk-register/styles.css +9 -1
  49. package/src/web-components/story-map/api.json +8 -6
  50. package/src/web-components/story-map/logic.js +280 -281
  51. package/src/web-components/story-map/static.html +15 -5
  52. package/src/web-components/traceability-matrix/api.json +10 -3
  53. package/src/web-components/traceability-matrix/logic.js +112 -63
  54. package/src/web-components/traceability-matrix/styles.css +22 -1
  55. package/src/web-components/user-journey/api.json +25 -24
  56. package/src/web-components/user-journey/logic.js +76 -127
  57. package/src/web-components/user-journey/static.html +2 -2
  58. package/src/web-components/user-persona/api.json +15 -14
  59. package/src/web-components/user-persona/logic.js +71 -117
  60. package/src/web-components/user-persona/styles.js +1 -0
  61. package/src/web-components/user-story/api.json +13 -11
  62. package/src/web-components/user-story/logic.js +164 -195
  63. package/src/web-components/user-story/styles.js +28 -1
  64. package/src/web-components/work-item/README.md +3 -3
  65. package/src/web-components/work-item/api.json +13 -11
  66. package/src/web-components/work-item/logic.js +58 -154
@@ -2,7 +2,7 @@
2
2
  "$schema": "../../../schemas/api.schema.json",
3
3
  "element": "risk-register",
4
4
  "type": "web-component",
5
- "description": "Composing preset that renders a sortable risk table + a likelihood \u00d7 impact quadrant grid from one source",
5
+ "description": "Composing preset that renders a sortable risk table + a likelihood × impact quadrant grid from one source",
6
6
  "attributes": [
7
7
  {
8
8
  "name": "src",
@@ -31,11 +31,6 @@
31
31
  "name": "risk-register:ready",
32
32
  "detail": "{ count }",
33
33
  "description": "Fired after the table + grid render"
34
- },
35
- {
36
- "name": "risk-register:change",
37
- "detail": "{ row, field, value }",
38
- "description": "Reserved: emitted on inline edits (future)"
39
34
  }
40
35
  ],
41
36
  "htmlvalidate": {
@@ -18,17 +18,63 @@
18
18
  * <tr data-id="..."> on the row provides a stable id; falls back to
19
19
  * the title.
20
20
  *
21
+ * Severity = likelihood × impact is computed here and written into the
22
+ * Severity cell (text + data-sort-value) so <data-table> sorts and
23
+ * heat-maps it. It deliberately does NOT use data-rollup="product": that
24
+ * multiplies every numeric-looking cell in the row ("2FA rollout",
25
+ * "3 vendor quotes"). A row missing either score has an empty severity.
26
+ *
27
+ * Quadrant placement: scores 1..SCALE map to 0..1 via (v - 1) / (SCALE - 1),
28
+ * so 1 sits on the low edge and SCALE on the high edge. A score is "high"
29
+ * only when it is above the midpoint (> 3 on the 1–5 scale); the midpoint
30
+ * itself (3) counts as low and is drawn on the centre line. Each dot gets
31
+ * an explicit data-quadrant so <quadrant-grid> doesn't apply its own
32
+ * "midpoint belongs to the higher quadrant" rule.
33
+ *
21
34
  * @attr {string} src - URL to JSON: [{ id, title, likelihood, impact, owner, mitigation }]
22
35
  * @attr {string} label - Optional heading shown above the table
23
36
  *
24
- * @fires risk-register:ready - { count } after first render
25
- * @fires risk-register:change - { row, field, value } on inline edit
37
+ * @fires risk-register:ready - { count } after first render
26
38
  */
27
39
 
28
40
  import { registerComponent } from '../../lib/bundle-registry.js';
29
41
  import { VBElement } from '../../lib/vb-element.js';
42
+ import { esc } from '../../lib/escape.js';
30
43
 
31
44
  const SCALE = 5;
45
+ /** Scores strictly above this are in the high half of an axis. */
46
+ const MIDPOINT = (SCALE + 1) / 2;
47
+
48
+ /**
49
+ * Normalize a likelihood/impact score to an integer 1..SCALE, or 0 when
50
+ * missing / not a number.
51
+ * @param {unknown} v
52
+ * @returns {number}
53
+ */
54
+ function score(v) {
55
+ const n = parseInt(String(v ?? ''), 10);
56
+ if (!Number.isFinite(n) || n < 1) return 0;
57
+ return Math.min(SCALE, n);
58
+ }
59
+
60
+ /**
61
+ * Map a 1..SCALE score to a 0..1 coordinate.
62
+ * @param {number} v
63
+ */
64
+ const toUnit = (v) => (v - 1) / (SCALE - 1);
65
+
66
+ /**
67
+ * Quadrant index (Q1=top-right, Q2=top-left, Q3=bottom-left,
68
+ * Q4=bottom-right) for a likelihood (x) / impact (y) pair.
69
+ * @param {number} likelihood
70
+ * @param {number} impact
71
+ */
72
+ function quadrantFor(likelihood, impact) {
73
+ const right = likelihood > MIDPOINT;
74
+ const top = impact > MIDPOINT;
75
+ if (top) return right ? 0 : 1;
76
+ return right ? 3 : 2;
77
+ }
32
78
 
33
79
  class RiskRegister extends VBElement {
34
80
  setup() {
@@ -63,8 +109,8 @@ class RiskRegister extends VBElement {
63
109
  rows.push({
64
110
  id,
65
111
  title,
66
- likelihood: parseInt(cells[1]?.textContent ?? '', 10) || 0,
67
- impact: parseInt(cells[2]?.textContent ?? '', 10) || 0,
112
+ likelihood: score(cells[1]?.textContent),
113
+ impact: score(cells[2]?.textContent),
68
114
  owner: cells[3]?.textContent?.trim() || '',
69
115
  mitigation: cells[4]?.textContent?.trim() || '',
70
116
  });
@@ -73,7 +119,12 @@ class RiskRegister extends VBElement {
73
119
  }
74
120
 
75
121
  #render(rows) {
76
- this.#rows = rows;
122
+ this.#rows = rows.map((r) => ({
123
+ ...r,
124
+ likelihood: score(r?.likelihood),
125
+ impact: score(r?.impact),
126
+ }));
127
+ rows = this.#rows;
77
128
 
78
129
  // Wipe prior children except the source <template> so re-renders work.
79
130
  [...this.children].forEach((c) => { if (c.tagName !== 'TEMPLATE') c.remove(); });
@@ -110,22 +161,24 @@ class RiskRegister extends VBElement {
110
161
  <th data-sort="string">Title</th>
111
162
  <th data-sort="number">Likelihood</th>
112
163
  <th data-sort="number">Impact</th>
113
- <th data-sort="number" data-rollup="product" data-heatmap="low-good">Severity</th>
164
+ <th data-sort="number" data-severity data-heatmap="low-good">Severity</th>
114
165
  <th data-sort="string">Owner</th>
115
166
  <th data-sort="string">Mitigation</th>
116
167
  </tr>
117
168
  </thead>
118
169
  <tbody>
119
- ${rows.map((r) => `
120
- <tr data-id="${RiskRegister.#escape(r.id)}">
121
- <td>${RiskRegister.#escape(r.title)}</td>
170
+ ${rows.map((r) => {
171
+ const sev = r.likelihood && r.impact ? r.likelihood * r.impact : null;
172
+ return `
173
+ <tr data-id="${esc(r.id)}">
174
+ <td>${esc(r.title)}</td>
122
175
  <td>${r.likelihood || ''}</td>
123
176
  <td>${r.impact || ''}</td>
124
- <td></td>
125
- <td>${RiskRegister.#escape(r.owner)}</td>
126
- <td>${RiskRegister.#escape(r.mitigation)}</td>
127
- </tr>
128
- `).join('')}
177
+ <td${sev === null ? '' : ` data-sort-value="${sev}"`}>${sev ?? ''}</td>
178
+ <td>${esc(r.owner)}</td>
179
+ <td>${esc(r.mitigation)}</td>
180
+ </tr>`;
181
+ }).join('')}
129
182
  </tbody>
130
183
  `;
131
184
  wrapper.appendChild(table);
@@ -148,23 +201,17 @@ class RiskRegister extends VBElement {
148
201
  for (const r of rows) {
149
202
  if (!r.likelihood || !r.impact) continue;
150
203
  const dot = document.createElement('span');
151
- const x = Math.min(1, Math.max(0, r.likelihood / SCALE));
152
- const y = Math.min(1, Math.max(0, r.impact / SCALE));
153
- dot.setAttribute('data-x', x.toFixed(3));
154
- dot.setAttribute('data-y', y.toFixed(3));
204
+ dot.setAttribute('data-x', toUnit(r.likelihood).toFixed(3));
205
+ dot.setAttribute('data-y', toUnit(r.impact).toFixed(3));
206
+ dot.setAttribute('data-quadrant', String(quadrantFor(r.likelihood, r.impact)));
155
207
  dot.setAttribute('data-id', r.id);
156
- dot.setAttribute('title', `${r.title} — likelihood ${r.likelihood}, impact ${r.impact}`);
208
+ dot.setAttribute('title', `${r.title} — likelihood ${r.likelihood}, impact ${r.impact}, severity ${r.likelihood * r.impact}`);
157
209
  dot.textContent = r.title;
158
210
  grid.appendChild(dot);
159
211
  }
160
212
  return grid;
161
213
  }
162
214
 
163
- static #escape(s) {
164
- return String(s ?? '').replace(/[&<>"']/g, (c) =>
165
- ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[c]));
166
- }
167
-
168
215
  /** @type {Array<{id:string,title:string,likelihood:number,impact:number,owner:string,mitigation:string}>} */
169
216
  #rows = [];
170
217
 
@@ -23,10 +23,18 @@ risk-register {
23
23
 
24
24
  .rr-layout {
25
25
  display: grid;
26
- grid-template-columns: 1fr;
26
+ grid-template-columns: minmax(0, 1fr);
27
27
  gap: var(--size-l, 1.5rem);
28
28
  }
29
29
 
30
+ /* The table scrolls inside its own box on narrow screens instead of
31
+ pushing the whole page sideways. */
32
+ .rr-layout > data-table {
33
+ display: block;
34
+ min-inline-size: 0;
35
+ overflow-x: auto;
36
+ }
37
+
30
38
  /* Table on top, quadrant below by default; side-by-side on wide screens */
31
39
  @media (width >= 64rem) {
32
40
  .rr-layout {
@@ -5,9 +5,9 @@
5
5
  "description": "Horizontal user story map with activity columns and drag-and-drop between columns",
6
6
  "htmlvalidate": { "flow": true, "permittedContent": ["section"] },
7
7
  "attributes": [
8
- { "name": "src", "kind": "host-api", "purpose": "config", "type": "string", "description": "URL to JSON data with activities and stories" },
9
- { "name": "title", "kind": "host-api", "purpose": "config", "type": "string", "description": "Optional heading above the story map" },
10
- { "name": "compact", "kind": "host-api", "purpose": "config", "type": "boolean", "description": "Reduced spacing variant" }
8
+ { "name": "src", "kind": "host-api", "purpose": "config", "type": "string", "description": "URL to JSON data with activities and stories. Fetched once per value; a new value replaces the map and aborts an older in-flight request" },
9
+ { "name": "title", "kind": "host-api", "purpose": "config", "type": "string", "description": "Optional heading above the story map; also names the scroll region" },
10
+ { "name": "compact", "kind": "host-api", "purpose": "config", "type": "boolean", "description": "Reduces column widths, padding, and header font sizes for denser layouts" }
11
11
  ],
12
12
  "childAttributes": [
13
13
  { "name": "data-activity", "on": "section", "type": "string", "description": "Activity column identifier" },
@@ -15,8 +15,10 @@
15
15
  { "name": "data-journey-phase", "on": "section", "type": "string", "description": "Optional link to a user-journey phase" }
16
16
  ],
17
17
  "events": [
18
- { "name": "story-map:reorder", "detail": "{ itemId, activity, oldIndex, newIndex }", "description": "Item reordered within a column" },
19
- { "name": "story-map:transfer", "detail": "{ itemId, fromActivity, toActivity, newIndex }", "description": "Item moved between columns" },
20
- { "name": "story-map:ready", "detail": "{ activityCount, storyCount }", "description": "Fired after component initializes" }
18
+ { "name": "story-map:reorder", "detail": "{ itemId, activity, oldIndex, newIndex }", "description": "Item reordered within a column" },
19
+ { "name": "story-map:transfer", "detail": "{ itemId, fromActivity, toActivity, newIndex }", "description": "Item moved between columns" },
20
+ { "name": "story-map:ready", "detail": "{ activityCount, storyCount }", "description": "Fired once after the first render. With src, fired once the JSON has loaded and rendered. Once per element: not re-fired by a later src change (watch activities-changed with source 'src') or when the map is moved in the DOM" },
21
+ { "name": "story-map:activities-changed", "detail": "{ activities, source: 'property' | 'src' }", "description": "Fired after each .activities assignment (source 'property') and after each src load has rendered (source 'src')" },
22
+ { "name": "story-map:upgraded", "detail": "", "description": "Fired (in a microtask) each time the map connects and upgrades" }
21
23
  ]
22
24
  }