@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,322 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure geometry + SVG builder for the click-to-edit `<iron-triangle>`.
|
|
3
|
+
*
|
|
4
|
+
* Triangle layout matches the design sketch:
|
|
5
|
+
* - SCOPE at the top (angle -π/2)
|
|
6
|
+
* - TIME at the bottom-left (5π/6)
|
|
7
|
+
* - COST at the bottom-right (π/6)
|
|
8
|
+
*
|
|
9
|
+
* Each vertex is pushed outward from the centroid by a per-constraint
|
|
10
|
+
* stretch factor in [MIN_STRETCH, MAX_STRETCH]. The capacity number
|
|
11
|
+
* sits at the centroid. The whole SVG is the UI: each vertex group
|
|
12
|
+
* is a clickable hit target that opens a per-axis dialog; the center
|
|
13
|
+
* "Quality" group fires `iron-triangle:open-quality` (and optionally
|
|
14
|
+
* navigates to data-quality-href).
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
const MIN_STRETCH = 0.55;
|
|
18
|
+
const MAX_STRETCH = 1.45;
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Reduce each constraint's snapshot to one comparable scalar.
|
|
22
|
+
* @param {{ time?: any, cost?: any, scope?: any }} [value]
|
|
23
|
+
*/
|
|
24
|
+
export function relativeMagnitudes({ time = {}, cost = {}, scope = {} } = {}) {
|
|
25
|
+
return {
|
|
26
|
+
t: numeric(time.sprintCount, 1) * numeric(time.sprintWeeks, 1),
|
|
27
|
+
c: numeric(cost.teamFTE, 0) * numeric(cost.hoursPerWeek, 40),
|
|
28
|
+
s: numeric(scope.mustHaveCount, 0) + 0.5 * numeric(scope.shouldHaveCount, 0),
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Map magnitudes to per-vertex stretch factors clamped to [0.55, 1.45]. */
|
|
33
|
+
export function stretchFactors(mag) {
|
|
34
|
+
const positives = [mag.t, mag.c, mag.s].filter(v => v > 0);
|
|
35
|
+
if (positives.length === 0) return { time: 1, cost: 1, scope: 1 };
|
|
36
|
+
const max = Math.max(...positives);
|
|
37
|
+
const norm = v => v <= 0
|
|
38
|
+
? MIN_STRETCH
|
|
39
|
+
: MIN_STRETCH + (MAX_STRETCH - MIN_STRETCH) * (v / max);
|
|
40
|
+
return { time: norm(mag.t), cost: norm(mag.c), scope: norm(mag.s) };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Returns three SVG-coordinate vertices.
|
|
45
|
+
* scope at top, time bottom-left, cost bottom-right.
|
|
46
|
+
*/
|
|
47
|
+
export function triangleVertices(value, radius = 80) {
|
|
48
|
+
const baseAngles = {
|
|
49
|
+
scope: -Math.PI / 2, // top
|
|
50
|
+
time: 5 * Math.PI / 6, // bottom-left
|
|
51
|
+
cost: Math.PI / 6, // bottom-right (SVG-y down)
|
|
52
|
+
};
|
|
53
|
+
const fs = stretchFactors(relativeMagnitudes(value));
|
|
54
|
+
const at = key => ({
|
|
55
|
+
x: Math.cos(baseAngles[key]) * radius * fs[key],
|
|
56
|
+
y: Math.sin(baseAngles[key]) * radius * fs[key],
|
|
57
|
+
factor: fs[key],
|
|
58
|
+
});
|
|
59
|
+
return { scope: at('scope'), time: at('time'), cost: at('cost') };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// ── Per-vertex summary formatters ─────────────────────────────────
|
|
63
|
+
|
|
64
|
+
export function formatTimeSummary(time = {}) {
|
|
65
|
+
const sprintCount = numeric(time.sprintCount, 0);
|
|
66
|
+
const sprintWeeks = numeric(time.sprintWeeks, 0);
|
|
67
|
+
if (sprintCount > 0 && sprintWeeks > 0) {
|
|
68
|
+
const total = sprintCount * sprintWeeks;
|
|
69
|
+
return sprintCount > 1
|
|
70
|
+
? `${total} weeks (${sprintCount} × ${sprintWeeks}wk)`
|
|
71
|
+
: `${total} week${total === 1 ? '' : 's'}`;
|
|
72
|
+
}
|
|
73
|
+
if (sprintWeeks > 0) return `${sprintWeeks} week${sprintWeeks === 1 ? '' : 's'}`;
|
|
74
|
+
if (time.deadline) return `until ${time.deadline}`;
|
|
75
|
+
return 'TBD';
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function formatCostSummary(cost = {}) {
|
|
79
|
+
const fte = Number(cost.teamFTE);
|
|
80
|
+
const tier = cost.budgetTier;
|
|
81
|
+
const parts = [];
|
|
82
|
+
if (Number.isFinite(fte) && fte > 0) parts.push(`${fte} FTE`);
|
|
83
|
+
if (tier && tier !== 'unset') parts.push(tier);
|
|
84
|
+
return parts.length > 0 ? parts.join(' · ') : 'TBD';
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function formatScopeSummary(scope = {}) {
|
|
88
|
+
const must = numeric(scope.mustHaveCount, 0);
|
|
89
|
+
const should = numeric(scope.shouldHaveCount, 0);
|
|
90
|
+
if (must > 0 && should > 0) return `${must} must · ${should} should`;
|
|
91
|
+
if (must > 0) return `${must} must-have`;
|
|
92
|
+
if (should > 0) return `${should} should-have`;
|
|
93
|
+
return 'TBD';
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export function formatQualitySummary(qualitySummary) {
|
|
97
|
+
if (typeof qualitySummary === 'string' && qualitySummary.trim().length > 0) {
|
|
98
|
+
return qualitySummary;
|
|
99
|
+
}
|
|
100
|
+
return 'TBD — click to set';
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// ── SVG builder ───────────────────────────────────────────────────
|
|
104
|
+
|
|
105
|
+
const SVG_NS = 'http://www.w3.org/2000/svg';
|
|
106
|
+
|
|
107
|
+
const VERTEX_LABEL_OFFSETS = {
|
|
108
|
+
scope: { dx: 0, dy: -22, anchor: 'middle' },
|
|
109
|
+
time: { dx: -10, dy: 14, anchor: 'end' },
|
|
110
|
+
cost: { dx: 10, dy: 14, anchor: 'start' },
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
const VERTEX_SUMMARY_OFFSETS = {
|
|
114
|
+
scope: { dx: 0, dy: -8, anchor: 'middle' },
|
|
115
|
+
time: { dx: -10, dy: 28, anchor: 'end' },
|
|
116
|
+
cost: { dx: 10, dy: 28, anchor: 'start' },
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
const VERTEX_LABELS = {
|
|
120
|
+
scope: { name: 'Scope', unit: '# features / story points' },
|
|
121
|
+
time: { name: 'Time', unit: 'hours / days / weeks' },
|
|
122
|
+
cost: { name: 'Cost', unit: 'FTE / $' },
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Build the entire interactive triangle SVG. Each vertex group is
|
|
127
|
+
* `<g class="vertex" data-axis="…" tabindex="0">` so click + keyboard
|
|
128
|
+
* activation work; the center group is `<g class="center"
|
|
129
|
+
* data-target="quality" tabindex="0">`.
|
|
130
|
+
*
|
|
131
|
+
* The component (logic.js) attaches click/keydown listeners to these
|
|
132
|
+
* groups by `data-axis` / `data-target`.
|
|
133
|
+
*/
|
|
134
|
+
/**
|
|
135
|
+
* @param {{ value?: any, vertices?: any, capacityPoints?: number, capacitySource?: string, qualitySummary?: string }} [options]
|
|
136
|
+
*/
|
|
137
|
+
export function buildTriangleSvg({
|
|
138
|
+
value = {},
|
|
139
|
+
vertices,
|
|
140
|
+
capacityPoints = 0,
|
|
141
|
+
capacitySource = 'formula',
|
|
142
|
+
qualitySummary = '',
|
|
143
|
+
} = {}) {
|
|
144
|
+
const v = vertices || triangleVertices(value);
|
|
145
|
+
const svg = document.createElementNS(SVG_NS, 'svg');
|
|
146
|
+
// viewBox padded generously so even the longest summary
|
|
147
|
+
// ("6 weeks (3 × 2wk)") rendered at TIME's bottom-left vertex
|
|
148
|
+
// (max stretched to x ≈ -116) plus its ~80px text width never
|
|
149
|
+
// clips the left edge.
|
|
150
|
+
svg.setAttribute('viewBox', '-220 -170 440 340');
|
|
151
|
+
svg.setAttribute('role', 'group');
|
|
152
|
+
svg.setAttribute('aria-label',
|
|
153
|
+
`Project shape: ${capacityPoints || 0} capacity points. Click each corner to edit, or the center to open the quality compass.`);
|
|
154
|
+
|
|
155
|
+
// 1. The triangle polygon — decorative; hit-targets are the vertex groups.
|
|
156
|
+
const tri = document.createElementNS(SVG_NS, 'polygon');
|
|
157
|
+
tri.setAttribute('class', 'triangle');
|
|
158
|
+
tri.setAttribute('points', [
|
|
159
|
+
`${round(v.scope.x)},${round(v.scope.y)}`,
|
|
160
|
+
`${round(v.time.x)},${round(v.time.y)}`,
|
|
161
|
+
`${round(v.cost.x)},${round(v.cost.y)}`,
|
|
162
|
+
].join(' '));
|
|
163
|
+
svg.append(tri);
|
|
164
|
+
|
|
165
|
+
// 2. Center "Quality" hit target. Order top→bottom:
|
|
166
|
+
// "Quality" header (small) → big capacity number → "pts" subtitle.
|
|
167
|
+
// A backdrop circle keeps the polygon stroke out of the text area.
|
|
168
|
+
const center = document.createElementNS(SVG_NS, 'g');
|
|
169
|
+
center.setAttribute('class', 'center');
|
|
170
|
+
center.setAttribute('data-target', 'quality');
|
|
171
|
+
center.setAttribute('tabindex', '0');
|
|
172
|
+
center.setAttribute('role', 'button');
|
|
173
|
+
center.setAttribute(
|
|
174
|
+
'aria-label',
|
|
175
|
+
`Quality — ${formatQualitySummary(qualitySummary)}. Activate to open the NFR compass.`,
|
|
176
|
+
);
|
|
177
|
+
|
|
178
|
+
// Backdrop circle — sits behind the text, masks the polygon stroke.
|
|
179
|
+
const backdrop = document.createElementNS(SVG_NS, 'circle');
|
|
180
|
+
backdrop.setAttribute('class', 'capacity-backdrop');
|
|
181
|
+
backdrop.setAttribute('cx', '0');
|
|
182
|
+
backdrop.setAttribute('cy', '4');
|
|
183
|
+
backdrop.setAttribute('r', '32');
|
|
184
|
+
center.append(backdrop);
|
|
185
|
+
|
|
186
|
+
// Hit-rect overlays the backdrop so the entire center is clickable.
|
|
187
|
+
const centerHit = document.createElementNS(SVG_NS, 'rect');
|
|
188
|
+
centerHit.setAttribute('class', 'hit');
|
|
189
|
+
centerHit.setAttribute('x', '-36');
|
|
190
|
+
centerHit.setAttribute('y', '-30');
|
|
191
|
+
centerHit.setAttribute('width', '72');
|
|
192
|
+
centerHit.setAttribute('height', '68');
|
|
193
|
+
centerHit.setAttribute('rx', '34');
|
|
194
|
+
center.append(centerHit);
|
|
195
|
+
|
|
196
|
+
// "Quality" header (small, top of stack).
|
|
197
|
+
const quality = document.createElementNS(SVG_NS, 'text');
|
|
198
|
+
quality.setAttribute('class', 'quality-label');
|
|
199
|
+
quality.setAttribute('x', '0');
|
|
200
|
+
quality.setAttribute('y', '-15');
|
|
201
|
+
quality.setAttribute('text-anchor', 'middle');
|
|
202
|
+
quality.textContent = 'Quality';
|
|
203
|
+
center.append(quality);
|
|
204
|
+
|
|
205
|
+
// Capacity number (hero).
|
|
206
|
+
const cap = document.createElementNS(SVG_NS, 'text');
|
|
207
|
+
cap.setAttribute('class', 'capacity');
|
|
208
|
+
cap.setAttribute('x', '0');
|
|
209
|
+
cap.setAttribute('y', '7');
|
|
210
|
+
cap.setAttribute('text-anchor', 'middle');
|
|
211
|
+
cap.setAttribute('dominant-baseline', 'middle');
|
|
212
|
+
cap.textContent = capacityPoints > 0 ? String(capacityPoints) : '—';
|
|
213
|
+
center.append(cap);
|
|
214
|
+
|
|
215
|
+
// "pts" subtitle.
|
|
216
|
+
const unit = document.createElementNS(SVG_NS, 'text');
|
|
217
|
+
unit.setAttribute('class', 'capacity-unit');
|
|
218
|
+
unit.setAttribute('x', '0');
|
|
219
|
+
unit.setAttribute('y', '26');
|
|
220
|
+
unit.setAttribute('text-anchor', 'middle');
|
|
221
|
+
unit.textContent = capacitySource === 'manual' ? 'pts (manual)' : 'pts';
|
|
222
|
+
center.append(unit);
|
|
223
|
+
|
|
224
|
+
// Native <title> tooltip — the quality summary.
|
|
225
|
+
const centerTitle = document.createElementNS(SVG_NS, 'title');
|
|
226
|
+
centerTitle.textContent = formatQualitySummary(qualitySummary);
|
|
227
|
+
center.append(centerTitle);
|
|
228
|
+
svg.append(center);
|
|
229
|
+
|
|
230
|
+
// 3. Three vertex hit targets.
|
|
231
|
+
for (const axis of ['scope', 'time', 'cost']) {
|
|
232
|
+
svg.append(buildVertex(axis, v[axis], value));
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
return svg;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function buildVertex(axis, vertex, value) {
|
|
239
|
+
const labelMeta = VERTEX_LABEL_OFFSETS[axis];
|
|
240
|
+
const summaryMeta = VERTEX_SUMMARY_OFFSETS[axis];
|
|
241
|
+
const labels = VERTEX_LABELS[axis];
|
|
242
|
+
|
|
243
|
+
const summary = axis === 'scope'
|
|
244
|
+
? formatScopeSummary(value.scope)
|
|
245
|
+
: axis === 'cost'
|
|
246
|
+
? formatCostSummary(value.cost)
|
|
247
|
+
: formatTimeSummary(value.time);
|
|
248
|
+
|
|
249
|
+
const g = document.createElementNS(SVG_NS, 'g');
|
|
250
|
+
g.setAttribute('class', 'vertex');
|
|
251
|
+
g.setAttribute('data-axis', axis);
|
|
252
|
+
g.setAttribute('tabindex', '0');
|
|
253
|
+
g.setAttribute('role', 'button');
|
|
254
|
+
g.setAttribute('aria-label', `${labels.name} — ${summary}. Activate to edit.`);
|
|
255
|
+
|
|
256
|
+
// Invisible hit area: a roomy rect that fully contains the
|
|
257
|
+
// {label, summary} text cluster. The longest summary
|
|
258
|
+
// ("6 weeks (3 × 2wk)") needs ≈ 130px at 0.7rem.
|
|
259
|
+
const hit = document.createElementNS(SVG_NS, 'rect');
|
|
260
|
+
hit.setAttribute('class', 'hit');
|
|
261
|
+
const hitW = 160;
|
|
262
|
+
const hitH = 50;
|
|
263
|
+
// Position the hit rect by aligning its inner edge with the vertex
|
|
264
|
+
// and accounting for each label's anchor offset.
|
|
265
|
+
let hitX;
|
|
266
|
+
if (labelMeta.anchor === 'end') {
|
|
267
|
+
// Right edge of hit aligns with vertex (text grows leftward).
|
|
268
|
+
hitX = vertex.x - hitW + 6;
|
|
269
|
+
} else if (labelMeta.anchor === 'start') {
|
|
270
|
+
// Left edge of hit aligns with vertex (text grows rightward).
|
|
271
|
+
hitX = vertex.x - 6;
|
|
272
|
+
} else {
|
|
273
|
+
// Centered on vertex for the SCOPE corner.
|
|
274
|
+
hitX = vertex.x - hitW / 2;
|
|
275
|
+
}
|
|
276
|
+
// Vertical position depends on whether the labels render above
|
|
277
|
+
// (scope, top corner) or below (time/cost, bottom corners) the vertex.
|
|
278
|
+
const hitY = axis === 'scope'
|
|
279
|
+
? vertex.y - hitH + 6
|
|
280
|
+
: vertex.y - 6;
|
|
281
|
+
hit.setAttribute('x', String(round(hitX)));
|
|
282
|
+
hit.setAttribute('y', String(round(hitY)));
|
|
283
|
+
hit.setAttribute('width', String(hitW));
|
|
284
|
+
hit.setAttribute('height', String(hitH));
|
|
285
|
+
hit.setAttribute('rx', '6');
|
|
286
|
+
g.append(hit);
|
|
287
|
+
|
|
288
|
+
// Vertex name (Scope / Time / Cost).
|
|
289
|
+
const name = document.createElementNS(SVG_NS, 'text');
|
|
290
|
+
name.setAttribute('class', 'vertex-name');
|
|
291
|
+
name.setAttribute('x', String(round(vertex.x + labelMeta.dx)));
|
|
292
|
+
name.setAttribute('y', String(round(vertex.y + labelMeta.dy)));
|
|
293
|
+
name.setAttribute('text-anchor', labelMeta.anchor);
|
|
294
|
+
name.textContent = labels.name;
|
|
295
|
+
g.append(name);
|
|
296
|
+
|
|
297
|
+
// Computed summary or "TBD".
|
|
298
|
+
const summaryEl = document.createElementNS(SVG_NS, 'text');
|
|
299
|
+
summaryEl.setAttribute('class', 'vertex-summary');
|
|
300
|
+
summaryEl.setAttribute('x', String(round(vertex.x + summaryMeta.dx)));
|
|
301
|
+
summaryEl.setAttribute('y', String(round(vertex.y + summaryMeta.dy)));
|
|
302
|
+
summaryEl.setAttribute('text-anchor', summaryMeta.anchor);
|
|
303
|
+
summaryEl.textContent = summary;
|
|
304
|
+
g.append(summaryEl);
|
|
305
|
+
|
|
306
|
+
// Native <title> tooltip — unit hint.
|
|
307
|
+
const title = document.createElementNS(SVG_NS, 'title');
|
|
308
|
+
title.textContent = `${labels.name}: ${summary} — ${labels.unit}`;
|
|
309
|
+
g.append(title);
|
|
310
|
+
|
|
311
|
+
return g;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// ── helpers ────────────────────────────────────────────────────────
|
|
315
|
+
|
|
316
|
+
function numeric(value, fallback) {
|
|
317
|
+
if (value === '' || value == null) return fallback;
|
|
318
|
+
const n = Number(value);
|
|
319
|
+
return Number.isFinite(n) && n > 0 ? n : fallback;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
function round(n) { return Math.round(n * 100) / 100; }
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "../../../schemas/api.schema.json",
|
|
3
|
+
"element": "iron-triangle",
|
|
4
|
+
"type": "web-component",
|
|
5
|
+
"description": "Triangle-as-UI project-shape constraint surface. Three vertex hit-targets (Scope/Time/Cost) open native <dialog> editors; the center 'Quality' target fires iron-triangle:open-quality with a data-quality-href fallback. Computes capacityPoints from sprintWeeks × sprintCount × teamFTE × focusFactor. Form-associated.",
|
|
6
|
+
"htmlvalidate": {
|
|
7
|
+
"flow": true,
|
|
8
|
+
"permittedContent": ["@flow"]
|
|
9
|
+
},
|
|
10
|
+
"attributes": [
|
|
11
|
+
{ "name": "name", "kind": "native", "purpose": "config", "type": "string", "default": "triangle", "description": "Form-association field name" },
|
|
12
|
+
{ "name": "data-focus-factor", "kind": "data", "purpose": "config", "type": "number", "default": "0.6", "description": "Multiplier in the default capacity formula" },
|
|
13
|
+
{ "name": "data-min-capacity", "kind": "data", "purpose": "config", "type": "number", "default": "1", "description": "Floor for capacityPoints" },
|
|
14
|
+
{ "name": "data-quality-href", "kind": "data", "purpose": "config", "type": "string", "description": "Navigation fallback when iron-triangle:open-quality isn't preventDefault-ed" },
|
|
15
|
+
{ "name": "data-quality-summary", "kind": "data", "purpose": "config", "type": "string", "description": "Short summary text shown in the center's native tooltip + aria-label (e.g. '3 critical: perf, sec, a11y')" },
|
|
16
|
+
{ "name": "disabled", "kind": "native", "purpose": "config", "type": "boolean", "description": "All inputs disabled" },
|
|
17
|
+
{ "name": "locked", "kind": "host-api", "purpose": "semantic-state", "type": "boolean", "description": "Read-only mode for shipped vectors" }
|
|
18
|
+
],
|
|
19
|
+
"slots": [],
|
|
20
|
+
"events": [
|
|
21
|
+
{ "name": "iron-triangle:change", "detail": "{ time, cost, scope, capacityPoints, capacitySource, hash, source, field }", "description": "Any input or property change" },
|
|
22
|
+
{ "name": "iron-triangle:revise", "detail": "{ field, from, to, reason }", "description": "A revision is committed via revise()" },
|
|
23
|
+
{ "name": "iron-triangle:mode", "detail": "{ from, to }", "description": "Capacity source flips between formula and manual" },
|
|
24
|
+
{ "name": "iron-triangle:open-quality", "detail": "{ qualitySummary, capacityPoints }", "description": "Center 'Quality' target activated; cancelable. If not prevented and data-quality-href is set, navigates to that URL." }
|
|
25
|
+
]
|
|
26
|
+
}
|