@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,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure helpers for <capacity-plan>.
|
|
3
|
+
*
|
|
4
|
+
* Sums: capacity (from iron-triangle) − quality spend (from
|
|
5
|
+
* quality-target's criticalSum) − feature spend (from slotted
|
|
6
|
+
* work-items by data-capacity-cost) = slack.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/** Read a numeric data-capacity-cost from one element; default 0. */
|
|
10
|
+
export function workItemCost(el) {
|
|
11
|
+
if (!el) return 0;
|
|
12
|
+
const raw = el.dataset?.capacityCost ?? el.getAttribute?.('data-capacity-cost');
|
|
13
|
+
const n = Number(raw);
|
|
14
|
+
return Number.isFinite(n) && n > 0 ? n : 0;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** Sum capacity-cost across an iterable of elements (work-items, etc.). */
|
|
18
|
+
export function sumWorkCosts(elements) {
|
|
19
|
+
let total = 0;
|
|
20
|
+
for (const el of elements || []) total += workItemCost(el);
|
|
21
|
+
return total;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** Compute the ledger numbers given the three inputs. */
|
|
25
|
+
export function computeLedger({ capacityPoints, qualitySum, featureSum }) {
|
|
26
|
+
const cap = Number.isFinite(capacityPoints) ? capacityPoints : Infinity;
|
|
27
|
+
const q = Math.max(0, Number(qualitySum) || 0);
|
|
28
|
+
const f = Math.max(0, Number(featureSum) || 0);
|
|
29
|
+
const slack = Number.isFinite(cap) ? cap - q - f : Infinity;
|
|
30
|
+
return { capacity: cap, quality: q, features: f, slack };
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Compute proportional bar widths (as percentages 0..100) for the
|
|
35
|
+
* stacked bar visualization. When over budget, the chunks normalize
|
|
36
|
+
* against the actual spend so the bars overflow visibly.
|
|
37
|
+
*/
|
|
38
|
+
export function barProportions({ capacity, quality, features }) {
|
|
39
|
+
if (!Number.isFinite(capacity) || capacity <= 0) {
|
|
40
|
+
// Unbounded budget: scale against spend so chunks have any width at all
|
|
41
|
+
const total = quality + features;
|
|
42
|
+
if (total <= 0) return { quality: 0, features: 0, slack: 100 };
|
|
43
|
+
return {
|
|
44
|
+
quality: (quality / total) * 100,
|
|
45
|
+
features: (features / total) * 100,
|
|
46
|
+
slack: 0,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
const spend = quality + features;
|
|
50
|
+
const denom = Math.max(capacity, spend);
|
|
51
|
+
return {
|
|
52
|
+
quality: (quality / denom) * 100,
|
|
53
|
+
features: (features / denom) * 100,
|
|
54
|
+
slack: Math.max(0, (capacity - spend) / denom * 100),
|
|
55
|
+
};
|
|
56
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "../../../schemas/api.schema.json",
|
|
3
|
+
"element": "capacity-plan",
|
|
4
|
+
"type": "web-component",
|
|
5
|
+
"description": "Compact ledger that ties iron-triangle capacity, quality-target spend, and slotted work-item costs into a stacked-bar plan view. Auto-binds via data-bind-triangle and data-bind-quality.",
|
|
6
|
+
"htmlvalidate": {
|
|
7
|
+
"flow": true,
|
|
8
|
+
"permittedContent": ["@flow"]
|
|
9
|
+
},
|
|
10
|
+
"attributes": [
|
|
11
|
+
{ "name": "data-bind-triangle", "kind": "data", "purpose": "config", "type": "string", "description": "ID of a sibling <iron-triangle> to read capacityPoints from" },
|
|
12
|
+
{ "name": "data-bind-quality", "kind": "data", "purpose": "config", "type": "string", "description": "ID of a sibling <quality-target> to read criticalSum from" }
|
|
13
|
+
],
|
|
14
|
+
"slots": [
|
|
15
|
+
{ "name": "(default)", "description": "Slot any elements with data-capacity-cost (notably <work-item>) and the plan sums them as the feature spend" }
|
|
16
|
+
],
|
|
17
|
+
"events": [
|
|
18
|
+
{ "name": "capacity-plan:overdrawn", "detail": "{ ledger, source }", "description": "Fires when the slack crosses zero in either direction (edge-triggered)" }
|
|
19
|
+
]
|
|
20
|
+
}
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* <capacity-plan> — Tiny ledger that ties the planning chain together.
|
|
3
|
+
*
|
|
4
|
+
* Reads:
|
|
5
|
+
* capacityPoints ← <iron-triangle> via data-bind-triangle
|
|
6
|
+
* qualitySum ← <quality-target>'s criticalSum via data-bind-quality
|
|
7
|
+
* featureSum ← summed from slotted work-items' data-capacity-cost,
|
|
8
|
+
* OR set as a property
|
|
9
|
+
*
|
|
10
|
+
* Renders a compact two-column ledger:
|
|
11
|
+
* numeric column (label + signed delta) + proportional stacked bar.
|
|
12
|
+
*
|
|
13
|
+
* Re-renders on bound triangle/quality changes and on slotted-children
|
|
14
|
+
* mutations.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { VBElement } from '../../lib/vb-element.js';
|
|
18
|
+
import { registerComponent } from '../../lib/bundle-registry.js';
|
|
19
|
+
import {
|
|
20
|
+
sumWorkCosts,
|
|
21
|
+
computeLedger,
|
|
22
|
+
barProportions,
|
|
23
|
+
} from './_capacity-utils.js';
|
|
24
|
+
|
|
25
|
+
class CapacityPlan extends VBElement {
|
|
26
|
+
static get observedAttributes() {
|
|
27
|
+
return ['data-bind-triangle', 'data-bind-quality'];
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** @type {(() => void) | null} */
|
|
31
|
+
#unbindTriangle = null;
|
|
32
|
+
/** @type {(() => void) | null} */
|
|
33
|
+
#unbindQuality = null;
|
|
34
|
+
/** @type {MutationObserver | null} */
|
|
35
|
+
#slotObserver = null;
|
|
36
|
+
/** @type {number | null} */
|
|
37
|
+
#featureSumOverride = null;
|
|
38
|
+
/** @type {boolean} */
|
|
39
|
+
#wasOver = false;
|
|
40
|
+
|
|
41
|
+
setup() {
|
|
42
|
+
this.#bindTriangle();
|
|
43
|
+
this.#bindQuality();
|
|
44
|
+
this.#observeSlotted();
|
|
45
|
+
this.#render();
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
teardown() {
|
|
49
|
+
this.#unbindTriangle?.();
|
|
50
|
+
this.#unbindQuality?.();
|
|
51
|
+
this.#slotObserver?.disconnect();
|
|
52
|
+
this.#unbindTriangle = null;
|
|
53
|
+
this.#unbindQuality = null;
|
|
54
|
+
this.#slotObserver = null;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
attributeChangedCallback(name) {
|
|
58
|
+
if (!this.isConnected) return;
|
|
59
|
+
if (name === 'data-bind-triangle') { this.#bindTriangle(); this.#render(); }
|
|
60
|
+
else if (name === 'data-bind-quality') { this.#bindQuality(); this.#render(); }
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// ── Public API ────────────────────────────────────────────────────
|
|
64
|
+
|
|
65
|
+
get capacityPoints() { return this.#triangleCap(); }
|
|
66
|
+
get qualitySum() { return this.#qualitySum(); }
|
|
67
|
+
|
|
68
|
+
get featureSum() {
|
|
69
|
+
if (this.#featureSumOverride != null) return this.#featureSumOverride;
|
|
70
|
+
return sumWorkCosts(this.querySelectorAll('[data-capacity-cost]'));
|
|
71
|
+
}
|
|
72
|
+
set featureSum(n) {
|
|
73
|
+
this.#featureSumOverride = Number.isFinite(+n) ? +n : null;
|
|
74
|
+
this.#render();
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
get slack() {
|
|
78
|
+
return computeLedger({
|
|
79
|
+
capacityPoints: this.#triangleCap(),
|
|
80
|
+
qualitySum: this.#qualitySum(),
|
|
81
|
+
featureSum: this.featureSum,
|
|
82
|
+
}).slack;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// ── Internal: bindings ────────────────────────────────────────────
|
|
86
|
+
|
|
87
|
+
#findById(id) {
|
|
88
|
+
if (!id) return null;
|
|
89
|
+
const root = /** @type {any} */ (this.getRootNode());
|
|
90
|
+
return root.getElementById ? root.getElementById(id) : document.getElementById(id);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
#bindTriangle() {
|
|
94
|
+
this.#unbindTriangle?.();
|
|
95
|
+
this.#unbindTriangle = null;
|
|
96
|
+
const el = this.#findById(this.dataset.bindTriangle);
|
|
97
|
+
if (!el || el.localName !== 'iron-triangle') return;
|
|
98
|
+
const handler = () => this.#render();
|
|
99
|
+
el.addEventListener('iron-triangle:change', handler);
|
|
100
|
+
this.#unbindTriangle = () => el.removeEventListener('iron-triangle:change', handler);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
#bindQuality() {
|
|
104
|
+
this.#unbindQuality?.();
|
|
105
|
+
this.#unbindQuality = null;
|
|
106
|
+
const el = this.#findById(this.dataset.bindQuality);
|
|
107
|
+
if (!el || el.localName !== 'quality-target') return;
|
|
108
|
+
const handler = () => this.#render();
|
|
109
|
+
el.addEventListener('quality-target:change', handler);
|
|
110
|
+
this.#unbindQuality = () => el.removeEventListener('quality-target:change', handler);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
#observeSlotted() {
|
|
114
|
+
if (this.#slotObserver) return;
|
|
115
|
+
this.#slotObserver = new MutationObserver((records) => {
|
|
116
|
+
// Ignore mutations that happen inside the rendered ledger table —
|
|
117
|
+
// otherwise we'd loop on our own innerHTML writes.
|
|
118
|
+
const meaningful = records.some(r => {
|
|
119
|
+
const target = /** @type {any} */ (r.target);
|
|
120
|
+
return !target.closest?.('[data-capacity-table]');
|
|
121
|
+
});
|
|
122
|
+
if (meaningful) this.#render();
|
|
123
|
+
});
|
|
124
|
+
this.#slotObserver.observe(this, {
|
|
125
|
+
childList: true,
|
|
126
|
+
subtree: true,
|
|
127
|
+
attributes: true,
|
|
128
|
+
attributeFilter: ['data-capacity-cost'],
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
#triangleCap() {
|
|
133
|
+
const el = this.#findById(this.dataset.bindTriangle);
|
|
134
|
+
if (!el || el.localName !== 'iron-triangle') return Infinity;
|
|
135
|
+
const v = el.capacityPoints;
|
|
136
|
+
return Number.isFinite(v) && v > 0 ? v : Infinity;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
#qualitySum() {
|
|
140
|
+
const el = this.#findById(this.dataset.bindQuality);
|
|
141
|
+
if (!el || el.localName !== 'quality-target') return 0;
|
|
142
|
+
return Number(el.criticalSum) || 0;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// ── Internal: render ──────────────────────────────────────────────
|
|
146
|
+
|
|
147
|
+
#render() {
|
|
148
|
+
const ledger = computeLedger({
|
|
149
|
+
capacityPoints: this.#triangleCap(),
|
|
150
|
+
qualitySum: this.#qualitySum(),
|
|
151
|
+
featureSum: this.featureSum,
|
|
152
|
+
});
|
|
153
|
+
const bars = barProportions(ledger);
|
|
154
|
+
|
|
155
|
+
let table = /** @type {HTMLElement | null} */ (this.querySelector(':scope > [data-capacity-table]'));
|
|
156
|
+
if (!table) {
|
|
157
|
+
table = document.createElement('section');
|
|
158
|
+
table.dataset.capacityTable = '';
|
|
159
|
+
// Prepend so the ledger is the headline; slotted work-items
|
|
160
|
+
// (the underlying data) render below.
|
|
161
|
+
this.prepend(table);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const capCell = formatPts(ledger.capacity);
|
|
165
|
+
const qCell = ledger.quality > 0 ? `−${ledger.quality} pts` : '0 pts';
|
|
166
|
+
const fCell = ledger.features > 0 ? `−${ledger.features} pts` : '0 pts';
|
|
167
|
+
const slackCell = formatSignedSlack(ledger.slack);
|
|
168
|
+
|
|
169
|
+
table.innerHTML = `
|
|
170
|
+
<dl data-layout="grid" class="capacity-ledger" aria-label="Capacity ledger">
|
|
171
|
+
<div data-row="capacity">
|
|
172
|
+
<dt>Capacity</dt>
|
|
173
|
+
<dd class="num">${capCell}</dd>
|
|
174
|
+
<dd class="bar"><span class="bar-track"><span class="bar-fill bar-capacity" style="--w:100%"></span></span></dd>
|
|
175
|
+
</div>
|
|
176
|
+
<div data-row="quality">
|
|
177
|
+
<dt>Quality</dt>
|
|
178
|
+
<dd class="num">${qCell}</dd>
|
|
179
|
+
<dd class="bar"><span class="bar-track"><span class="bar-fill bar-quality" style="--w:${bars.quality.toFixed(1)}%"></span></span></dd>
|
|
180
|
+
</div>
|
|
181
|
+
<div data-row="features">
|
|
182
|
+
<dt>Features</dt>
|
|
183
|
+
<dd class="num">${fCell}</dd>
|
|
184
|
+
<dd class="bar"><span class="bar-track"><span class="bar-fill bar-features" style="--w:${bars.features.toFixed(1)}%"></span></span></dd>
|
|
185
|
+
</div>
|
|
186
|
+
<div data-row="slack" data-state="${ledger.slack < 0 ? 'over' : 'under'}">
|
|
187
|
+
<dt>Slack</dt>
|
|
188
|
+
<dd class="num">${slackCell}</dd>
|
|
189
|
+
<dd class="bar"><span class="bar-track"><span class="bar-fill bar-slack" style="--w:${bars.slack.toFixed(1)}%"></span></span></dd>
|
|
190
|
+
</div>
|
|
191
|
+
</dl>
|
|
192
|
+
`;
|
|
193
|
+
|
|
194
|
+
this.setState('overdrawn', ledger.slack < 0);
|
|
195
|
+
|
|
196
|
+
// Edge-triggered overdrawn event.
|
|
197
|
+
const isOver = ledger.slack < 0;
|
|
198
|
+
if (isOver !== this.#wasOver) {
|
|
199
|
+
this.dispatchEvent(new CustomEvent('capacity-plan:overdrawn', {
|
|
200
|
+
bubbles: true, composed: true,
|
|
201
|
+
detail: { ledger, source: 'render' },
|
|
202
|
+
}));
|
|
203
|
+
this.#wasOver = isOver;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function formatPts(n) {
|
|
209
|
+
if (!Number.isFinite(n)) return '∞';
|
|
210
|
+
return `${n} pts`;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function formatSignedSlack(n) {
|
|
214
|
+
if (!Number.isFinite(n)) return '∞';
|
|
215
|
+
if (n === 0) return '0 pts';
|
|
216
|
+
if (n > 0) return `+${n} pts`;
|
|
217
|
+
return `${n} pts`; // already signed (negative)
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
registerComponent('capacity-plan', CapacityPlan);
|
|
221
|
+
|
|
222
|
+
export { CapacityPlan };
|
|
@@ -0,0 +1,24 @@
|
|
|
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>Capacity Plan (static fallback)</title>
|
|
7
|
+
<link rel="stylesheet" href="/src/main.css">
|
|
8
|
+
</head>
|
|
9
|
+
<body>
|
|
10
|
+
<!--
|
|
11
|
+
The ledger ties iron-triangle's capacity, quality-target's spend,
|
|
12
|
+
and slotted work-items' data-capacity-cost into one view.
|
|
13
|
+
Pre-upgrade: empty (the math is JS-only). For a no-JS path the
|
|
14
|
+
server can render the same ledger as a static <dl>.
|
|
15
|
+
-->
|
|
16
|
+
<iron-triangle id="shape"></iron-triangle>
|
|
17
|
+
<quality-target id="quality" data-bind-to="shape"></quality-target>
|
|
18
|
+
|
|
19
|
+
<capacity-plan data-bind-triangle="shape" data-bind-quality="quality">
|
|
20
|
+
<work-item data-capacity-cost="3">Bulk import</work-item>
|
|
21
|
+
<work-item data-capacity-cost="2">API webhooks</work-item>
|
|
22
|
+
</capacity-plan>
|
|
23
|
+
</body>
|
|
24
|
+
</html>
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/* capacity-plan — compact ledger tying triangle, quality, and work-items.
|
|
2
|
+
|
|
3
|
+
Pure CSS layout (no SVG). Two-column rows: numeric label/value on
|
|
4
|
+
the left, proportional stacked bar on the right. */
|
|
5
|
+
|
|
6
|
+
capacity-plan {
|
|
7
|
+
display: block;
|
|
8
|
+
inline-size: 100%;
|
|
9
|
+
max-inline-size: var(--capacity-plan-max, 32rem);
|
|
10
|
+
margin-inline: auto;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
capacity-plan .capacity-ledger {
|
|
14
|
+
display: grid;
|
|
15
|
+
grid-template-columns: minmax(8rem, 14rem) 1fr;
|
|
16
|
+
gap: var(--size-2xs, 0.25rem) var(--size-m, 1rem);
|
|
17
|
+
margin: 0;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
capacity-plan .capacity-ledger > div {
|
|
21
|
+
display: contents;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
capacity-plan .capacity-ledger dt {
|
|
25
|
+
font-weight: var(--font-weight-semibold, 600);
|
|
26
|
+
color: var(--color-text);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
capacity-plan .capacity-ledger .num {
|
|
30
|
+
margin: 0;
|
|
31
|
+
font-variant-numeric: tabular-nums;
|
|
32
|
+
color: var(--color-text-muted);
|
|
33
|
+
text-align: end;
|
|
34
|
+
padding-inline-end: var(--size-s);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
capacity-plan .capacity-ledger .bar {
|
|
38
|
+
margin: 0;
|
|
39
|
+
align-self: center;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
capacity-plan .bar-track {
|
|
43
|
+
display: block;
|
|
44
|
+
inline-size: 100%;
|
|
45
|
+
block-size: 0.6rem;
|
|
46
|
+
background: var(--color-surface-raised, color-mix(in oklab, var(--color-surface) 92%, currentColor 8%));
|
|
47
|
+
border-radius: var(--radius-pill, 999px);
|
|
48
|
+
overflow: hidden;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
capacity-plan .bar-fill {
|
|
52
|
+
display: block;
|
|
53
|
+
block-size: 100%;
|
|
54
|
+
inline-size: var(--w, 0%);
|
|
55
|
+
border-radius: inherit;
|
|
56
|
+
transition: inline-size 200ms ease;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
capacity-plan .bar-capacity {
|
|
60
|
+
background: color-mix(in oklab, var(--color-text) 25%, transparent);
|
|
61
|
+
}
|
|
62
|
+
capacity-plan .bar-quality {
|
|
63
|
+
background: var(--color-interactive);
|
|
64
|
+
}
|
|
65
|
+
capacity-plan .bar-features {
|
|
66
|
+
background: var(--color-success);
|
|
67
|
+
}
|
|
68
|
+
capacity-plan .bar-slack {
|
|
69
|
+
background: color-mix(in oklab, var(--color-text-muted) 35%, transparent);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
capacity-plan [data-row="slack"][data-state="over"] .num {
|
|
73
|
+
color: var(--color-error);
|
|
74
|
+
font-weight: var(--font-weight-semibold, 600);
|
|
75
|
+
}
|
|
76
|
+
capacity-plan [data-row="slack"][data-state="over"] .bar-slack {
|
|
77
|
+
background: var(--color-error);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
capacity-plan:state(overdrawn) .capacity-ledger {
|
|
81
|
+
outline: 2px solid var(--color-error);
|
|
82
|
+
outline-offset: var(--size-2xs);
|
|
83
|
+
border-radius: var(--radius-m);
|
|
84
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "../../../schemas/api.schema.json",
|
|
3
|
+
"element": "empathy-map",
|
|
4
|
+
"type": "web-component",
|
|
5
|
+
"description": "Four-quadrant empathy map visualization (Says, Thinks, Does, Feels) with optional flip-to-edit interaction",
|
|
6
|
+
"htmlvalidate": { "flow": true, "permittedContent": ["@flow"] },
|
|
7
|
+
"attributes": [
|
|
8
|
+
{ "name": "title", "kind": "host-api", "purpose": "config", "type": "string", "description": "Map heading" },
|
|
9
|
+
{ "name": "persona", "kind": "host-api", "purpose": "config", "type": "string", "description": "Persona name displayed in header" },
|
|
10
|
+
{ "name": "persona-id", "kind": "host-api", "purpose": "config", "type": "string", "description": "Links to a user-persona element by id" },
|
|
11
|
+
{ "name": "summary", "kind": "host-api", "purpose": "config", "type": "string", "description": "One-line description" },
|
|
12
|
+
{ "name": "src", "kind": "host-api", "purpose": "config", "type": "string", "description": "URL to JSON file containing quadrant data" },
|
|
13
|
+
{ "name": "editable", "kind": "host-api", "purpose": "config", "type": "boolean", "description": "Enables flip-to-edit on quadrants" },
|
|
14
|
+
{ "name": "compact", "kind": "host-api", "purpose": "config", "type": "boolean", "description": "Reduced spacing variant" }
|
|
15
|
+
],
|
|
16
|
+
"slots": [
|
|
17
|
+
{ "name": "title", "description": "Map heading (slot fallback for attribute)" },
|
|
18
|
+
{ "name": "persona", "description": "Persona name (slot fallback for attribute)" },
|
|
19
|
+
{ "name": "summary", "description": "One-line description (slot fallback for attribute)" },
|
|
20
|
+
{ "name": "says", "description": "Content for the Says quadrant" },
|
|
21
|
+
{ "name": "thinks", "description": "Content for the Thinks quadrant" },
|
|
22
|
+
{ "name": "does", "description": "Content for the Does quadrant" },
|
|
23
|
+
{ "name": "feels", "description": "Content for the Feels quadrant" },
|
|
24
|
+
{ "name": "goals", "description": "Optional goals summary below the grid" },
|
|
25
|
+
{ "name": "pain-points", "description": "Optional pain points summary below the grid" }
|
|
26
|
+
],
|
|
27
|
+
"events": [
|
|
28
|
+
{ "name": "empathy-map:ready", "detail": "{ title, persona }", "description": "Fired after initial render" },
|
|
29
|
+
{ "name": "empathy-map:update", "detail": "{ quadrant, items }", "description": "Fired when quadrant content changes via edit" }
|
|
30
|
+
]
|
|
31
|
+
}
|