@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,180 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* risk-register: Composing preset for the table + quadrant pattern
|
|
3
|
+
*
|
|
4
|
+
* Reads risk rows from a <template> child (or src URL) and renders both
|
|
5
|
+
* a sortable <data-table> with a computed Severity column AND a
|
|
6
|
+
* <quadrant-grid> mapping each risk to a labelled dot at
|
|
7
|
+
* (likelihood, impact). The two views stay in sync from one source.
|
|
8
|
+
*
|
|
9
|
+
* The pairing recurs as a single tool, justifying a wrapper instead of
|
|
10
|
+
* a recipe. Most of the code is glue; both children do the work.
|
|
11
|
+
*
|
|
12
|
+
* Row shape (from <template>'s <tr> cells, in order):
|
|
13
|
+
* 1. Title — short risk identifier
|
|
14
|
+
* 2. Likelihood — integer 1..5
|
|
15
|
+
* 3. Impact — integer 1..5
|
|
16
|
+
* 4. Owner — optional, free text
|
|
17
|
+
* 5. Mitigation — optional, free text
|
|
18
|
+
* <tr data-id="..."> on the row provides a stable id; falls back to
|
|
19
|
+
* the title.
|
|
20
|
+
*
|
|
21
|
+
* @attr {string} src - URL to JSON: [{ id, title, likelihood, impact, owner, mitigation }]
|
|
22
|
+
* @attr {string} label - Optional heading shown above the table
|
|
23
|
+
*
|
|
24
|
+
* @fires risk-register:ready - { count } after first render
|
|
25
|
+
* @fires risk-register:change - { row, field, value } on inline edit
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
import { registerComponent } from '../../lib/bundle-registry.js';
|
|
29
|
+
import { VBElement } from '../../lib/vb-element.js';
|
|
30
|
+
|
|
31
|
+
const SCALE = 5;
|
|
32
|
+
|
|
33
|
+
class RiskRegister extends VBElement {
|
|
34
|
+
setup() {
|
|
35
|
+
const tmpl = this.querySelector(':scope > template');
|
|
36
|
+
let rows = tmpl ? RiskRegister.#parseTemplate(tmpl) : [];
|
|
37
|
+
|
|
38
|
+
if (this.hasAttribute('src')) {
|
|
39
|
+
this.#loadFromSrc(this.getAttribute('src'));
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
this.#render(rows);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async #loadFromSrc(url) {
|
|
46
|
+
try {
|
|
47
|
+
const res = await fetch(url);
|
|
48
|
+
const data = await res.json();
|
|
49
|
+
this.#render(Array.isArray(data) ? data : []);
|
|
50
|
+
} catch (e) {
|
|
51
|
+
console.warn('risk-register: failed to load src', url, e);
|
|
52
|
+
this.#render([]);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
static #parseTemplate(tmpl) {
|
|
57
|
+
const rows = [];
|
|
58
|
+
const trs = tmpl.content.querySelectorAll('tr');
|
|
59
|
+
for (const tr of trs) {
|
|
60
|
+
const cells = [...tr.children];
|
|
61
|
+
const title = cells[0]?.textContent?.trim() || '';
|
|
62
|
+
const id = tr.getAttribute('data-id') || title.toLowerCase().replace(/\s+/g, '-') || `risk-${rows.length}`;
|
|
63
|
+
rows.push({
|
|
64
|
+
id,
|
|
65
|
+
title,
|
|
66
|
+
likelihood: parseInt(cells[1]?.textContent ?? '', 10) || 0,
|
|
67
|
+
impact: parseInt(cells[2]?.textContent ?? '', 10) || 0,
|
|
68
|
+
owner: cells[3]?.textContent?.trim() || '',
|
|
69
|
+
mitigation: cells[4]?.textContent?.trim() || '',
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
return rows;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
#render(rows) {
|
|
76
|
+
this.#rows = rows;
|
|
77
|
+
|
|
78
|
+
// Wipe prior children except the source <template> so re-renders work.
|
|
79
|
+
[...this.children].forEach((c) => { if (c.tagName !== 'TEMPLATE') c.remove(); });
|
|
80
|
+
|
|
81
|
+
const label = this.getAttribute('label');
|
|
82
|
+
if (label) {
|
|
83
|
+
const h = document.createElement('header');
|
|
84
|
+
h.className = 'rr-label';
|
|
85
|
+
h.textContent = label;
|
|
86
|
+
this.appendChild(h);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const layout = document.createElement('div');
|
|
90
|
+
layout.className = 'rr-layout';
|
|
91
|
+
layout.appendChild(this.#buildTable(rows));
|
|
92
|
+
layout.appendChild(this.#buildGrid(rows));
|
|
93
|
+
this.appendChild(layout);
|
|
94
|
+
|
|
95
|
+
queueMicrotask(() => {
|
|
96
|
+
this.dispatchEvent(new CustomEvent('risk-register:ready', {
|
|
97
|
+
bubbles: true,
|
|
98
|
+
detail: { count: rows.length },
|
|
99
|
+
}));
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
#buildTable(rows) {
|
|
104
|
+
const wrapper = document.createElement('data-table');
|
|
105
|
+
const table = document.createElement('table');
|
|
106
|
+
|
|
107
|
+
table.innerHTML = `
|
|
108
|
+
<thead>
|
|
109
|
+
<tr>
|
|
110
|
+
<th data-sort="string">Title</th>
|
|
111
|
+
<th data-sort="number">Likelihood</th>
|
|
112
|
+
<th data-sort="number">Impact</th>
|
|
113
|
+
<th data-sort="number" data-rollup="product" data-heatmap="low-good">Severity</th>
|
|
114
|
+
<th data-sort="string">Owner</th>
|
|
115
|
+
<th data-sort="string">Mitigation</th>
|
|
116
|
+
</tr>
|
|
117
|
+
</thead>
|
|
118
|
+
<tbody>
|
|
119
|
+
${rows.map((r) => `
|
|
120
|
+
<tr data-id="${RiskRegister.#escape(r.id)}">
|
|
121
|
+
<td>${RiskRegister.#escape(r.title)}</td>
|
|
122
|
+
<td>${r.likelihood || ''}</td>
|
|
123
|
+
<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('')}
|
|
129
|
+
</tbody>
|
|
130
|
+
`;
|
|
131
|
+
wrapper.appendChild(table);
|
|
132
|
+
return wrapper;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
#buildGrid(rows) {
|
|
136
|
+
const grid = document.createElement('quadrant-grid');
|
|
137
|
+
grid.setAttribute('x-label', 'Likelihood');
|
|
138
|
+
grid.setAttribute('y-label', 'Impact');
|
|
139
|
+
grid.setAttribute('x-low', 'Rare');
|
|
140
|
+
grid.setAttribute('x-high', 'Likely');
|
|
141
|
+
grid.setAttribute('y-low', 'Minor');
|
|
142
|
+
grid.setAttribute('y-high', 'Severe');
|
|
143
|
+
grid.setAttribute('q1-label', 'Avoid');
|
|
144
|
+
grid.setAttribute('q2-label', 'Plan');
|
|
145
|
+
grid.setAttribute('q3-label', 'Accept');
|
|
146
|
+
grid.setAttribute('q4-label', 'Mitigate');
|
|
147
|
+
|
|
148
|
+
for (const r of rows) {
|
|
149
|
+
if (!r.likelihood || !r.impact) continue;
|
|
150
|
+
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));
|
|
155
|
+
dot.setAttribute('data-id', r.id);
|
|
156
|
+
dot.setAttribute('title', `${r.title} — likelihood ${r.likelihood}, impact ${r.impact}`);
|
|
157
|
+
dot.textContent = r.title;
|
|
158
|
+
grid.appendChild(dot);
|
|
159
|
+
}
|
|
160
|
+
return grid;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
static #escape(s) {
|
|
164
|
+
return String(s ?? '').replace(/[&<>"']/g, (c) =>
|
|
165
|
+
({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c]));
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/** @type {Array<{id:string,title:string,likelihood:number,impact:number,owner:string,mitigation:string}>} */
|
|
169
|
+
#rows = [];
|
|
170
|
+
|
|
171
|
+
get rows() { return [...this.#rows]; }
|
|
172
|
+
|
|
173
|
+
set rows(value) {
|
|
174
|
+
this.#render(Array.isArray(value) ? value : []);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
registerComponent('risk-register', RiskRegister);
|
|
179
|
+
|
|
180
|
+
export { RiskRegister };
|
|
@@ -0,0 +1,46 @@
|
|
|
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>Risk Register — Static Fallback</title>
|
|
7
|
+
<style>
|
|
8
|
+
body { font-family: system-ui, sans-serif; padding: 2rem; }
|
|
9
|
+
table { border-collapse: collapse; }
|
|
10
|
+
th, td { padding: 0.25rem 0.75rem; border-block-end: 1px solid #ddd; text-align: start; }
|
|
11
|
+
</style>
|
|
12
|
+
</head>
|
|
13
|
+
<body>
|
|
14
|
+
<!--
|
|
15
|
+
Static fallback for <risk-register>.
|
|
16
|
+
|
|
17
|
+
Upgrade relationship:
|
|
18
|
+
- Without JS: the <template> below is invisible (templates do not
|
|
19
|
+
render); the <noscript>-style fallback table preserves the data.
|
|
20
|
+
- With JS: the component reads rows from the <template>, computes
|
|
21
|
+
severity = likelihood × impact, renders a sortable <data-table>
|
|
22
|
+
with low-good severity heatmap, and a <quadrant-grid> mapping each
|
|
23
|
+
risk to a labelled dot at (likelihood, impact).
|
|
24
|
+
-->
|
|
25
|
+
|
|
26
|
+
<risk-register label="Q3 Project risks">
|
|
27
|
+
<template>
|
|
28
|
+
<tr data-id="vendor-delay"><td>Vendor delay</td><td>4</td><td>3</td><td>PM</td><td>Pre-stage assets in week 6</td></tr>
|
|
29
|
+
<tr data-id="data-breach"><td>Data breach</td><td>2</td><td>5</td><td>Security</td><td>Tabletop exercise + WAF tuning</td></tr>
|
|
30
|
+
<tr data-id="scope-creep"><td>Scope creep</td><td>5</td><td>3</td><td>EM</td><td>Strict change-control board</td></tr>
|
|
31
|
+
<tr data-id="staff-churn"><td>Staff churn</td><td>2</td><td>2</td><td>EM</td><td>Document playbooks + cross-train</td></tr>
|
|
32
|
+
</template>
|
|
33
|
+
<table>
|
|
34
|
+
<thead>
|
|
35
|
+
<tr><th>Risk</th><th>Likelihood</th><th>Impact</th><th>Owner</th></tr>
|
|
36
|
+
</thead>
|
|
37
|
+
<tbody>
|
|
38
|
+
<tr><td>Vendor delay</td><td>4</td><td>3</td><td>PM</td></tr>
|
|
39
|
+
<tr><td>Data breach</td><td>2</td><td>5</td><td>Security</td></tr>
|
|
40
|
+
<tr><td>Scope creep</td><td>5</td><td>3</td><td>EM</td></tr>
|
|
41
|
+
<tr><td>Staff churn</td><td>2</td><td>2</td><td>EM</td></tr>
|
|
42
|
+
</tbody>
|
|
43
|
+
</table>
|
|
44
|
+
</risk-register>
|
|
45
|
+
</body>
|
|
46
|
+
</html>
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/* risk-register: Composing preset (data-table + quadrant-grid) */
|
|
2
|
+
|
|
3
|
+
risk-register:not(:defined) {
|
|
4
|
+
display: block;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
risk-register {
|
|
8
|
+
display: block;
|
|
9
|
+
font-family: var(--font-sans, system-ui);
|
|
10
|
+
color: var(--color-text, #1a1a1a);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
@scope (risk-register) {
|
|
14
|
+
.rr-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
|
+
.rr-layout {
|
|
25
|
+
display: grid;
|
|
26
|
+
grid-template-columns: 1fr;
|
|
27
|
+
gap: var(--size-l, 1.5rem);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/* Table on top, quadrant below by default; side-by-side on wide screens */
|
|
31
|
+
@media (width >= 64rem) {
|
|
32
|
+
.rr-layout {
|
|
33
|
+
grid-template-columns: minmax(0, 1.4fr) minmax(0, 1fr);
|
|
34
|
+
align-items: start;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "../../../schemas/api.schema.json",
|
|
3
|
+
"element": "story-map",
|
|
4
|
+
"type": "web-component",
|
|
5
|
+
"description": "Horizontal user story map with activity columns and drag-and-drop between columns",
|
|
6
|
+
"htmlvalidate": { "flow": true, "permittedContent": ["section"] },
|
|
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" }
|
|
11
|
+
],
|
|
12
|
+
"childAttributes": [
|
|
13
|
+
{ "name": "data-activity", "on": "section", "type": "string", "description": "Activity column identifier" },
|
|
14
|
+
{ "name": "data-activity-label", "on": "section", "type": "string", "description": "Display label for the column header" },
|
|
15
|
+
{ "name": "data-journey-phase", "on": "section", "type": "string", "description": "Optional link to a user-journey phase" }
|
|
16
|
+
],
|
|
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" }
|
|
21
|
+
]
|
|
22
|
+
}
|