@profpowell/vb-project-planning 0.2.0 → 0.2.2
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/README.md +5 -2
- package/dist/vb-project-planning.css +70 -3
- package/dist/vb-project-planning.js +3333 -2565
- package/dist/vb-project-planning.min.css +1 -1
- package/dist/vb-project-planning.min.js +224 -200
- package/package.json +7 -4
- package/src/lib/board-surface.js +158 -0
- package/src/lib/bundle-registry.js +2 -75
- package/src/lib/dates.js +125 -0
- package/src/lib/escape.js +34 -0
- package/src/lib/hash.js +16 -0
- package/src/lib/portrait-url.js +3 -29
- package/src/lib/src-loader.js +78 -0
- package/src/utils/copy-init.js +56 -17
- package/src/web-components/_ux-base.js +409 -24
- package/src/web-components/adr-wc/README.md +18 -16
- package/src/web-components/adr-wc/api.json +7 -5
- package/src/web-components/adr-wc/logic.js +50 -136
- package/src/web-components/burndown-chart/api.json +8 -6
- package/src/web-components/burndown-chart/logic.js +75 -39
- package/src/web-components/empathy-map/api.json +13 -13
- package/src/web-components/empathy-map/logic.js +161 -168
- package/src/web-components/empathy-map/static.html +3 -3
- package/src/web-components/empathy-map/styles.js +1 -0
- package/src/web-components/gantt-chart/README.md +12 -6
- package/src/web-components/gantt-chart/api.json +23 -7
- package/src/web-components/gantt-chart/logic.js +456 -431
- package/src/web-components/gantt-chart/styles.css +9 -1
- package/src/web-components/impact-effort/api.json +9 -7
- package/src/web-components/impact-effort/logic.js +238 -209
- package/src/web-components/impact-effort/static.html +12 -4
- package/src/web-components/iron-triangle/_capacity.js +1 -19
- package/src/web-components/iron-triangle/_triangle-geometry.js +1 -1
- package/src/web-components/iron-triangle/api.json +2 -2
- package/src/web-components/iron-triangle/logic.js +47 -9
- package/src/web-components/iron-triangle/styles.css +22 -1
- package/src/web-components/kanban-board/api.json +8 -6
- package/src/web-components/kanban-board/logic.js +289 -333
- package/src/web-components/product-roadmap/api.json +14 -13
- package/src/web-components/product-roadmap/logic.js +393 -246
- package/src/web-components/product-roadmap/styles.css +40 -0
- package/src/web-components/quadrant-grid/api.json +9 -4
- package/src/web-components/quadrant-grid/logic.js +80 -12
- package/src/web-components/quadrant-grid/styles.css +7 -0
- package/src/web-components/quality-target/_quality-utils.js +2 -46
- package/src/web-components/quality-target/api.json +2 -3
- package/src/web-components/quality-target/logic.js +85 -13
- package/src/web-components/requirement-card/api.json +1 -1
- package/src/web-components/requirement-card/logic.js +26 -2
- package/src/web-components/requirement-card/static.html +1 -1
- package/src/web-components/review-surface/README.md +9 -5
- package/src/web-components/review-surface/api.json +10 -8
- package/src/web-components/review-surface/logic.js +349 -218
- package/src/web-components/risk-register/api.json +1 -6
- package/src/web-components/risk-register/logic.js +71 -24
- package/src/web-components/story-map/api.json +8 -6
- package/src/web-components/story-map/logic.js +280 -281
- package/src/web-components/story-map/static.html +15 -5
- package/src/web-components/traceability-matrix/api.json +10 -3
- package/src/web-components/traceability-matrix/logic.js +112 -63
- package/src/web-components/traceability-matrix/styles.css +14 -1
- package/src/web-components/user-journey/api.json +25 -24
- package/src/web-components/user-journey/logic.js +76 -127
- package/src/web-components/user-journey/static.html +2 -2
- package/src/web-components/user-persona/api.json +15 -14
- package/src/web-components/user-persona/logic.js +71 -117
- package/src/web-components/user-persona/styles.js +1 -0
- package/src/web-components/user-story/api.json +13 -11
- package/src/web-components/user-story/logic.js +164 -195
- package/src/web-components/user-story/styles.js +28 -1
- package/src/web-components/work-item/README.md +3 -3
- package/src/web-components/work-item/api.json +13 -11
- package/src/web-components/work-item/logic.js +58 -154
|
@@ -19,28 +19,10 @@ export function defaultFormula(time, cost, focusFactor) {
|
|
|
19
19
|
return Math.ceil(weeks * fte * f);
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
-
/**
|
|
23
|
-
* Format the formula string used as the human-readable explanation
|
|
24
|
-
* shown beneath the readout. Matches the formula above; if either
|
|
25
|
-
* factor is missing the string degrades gracefully.
|
|
26
|
-
*/
|
|
27
|
-
export function defaultFormulaText(time, cost, focusFactor) {
|
|
28
|
-
const sprintCount = num(time?.sprintCount);
|
|
29
|
-
const sprintWeeks = num(time?.sprintWeeks) || 1;
|
|
30
|
-
const totalWeeks = sprintCount > 0 ? sprintCount * sprintWeeks : sprintWeeks;
|
|
31
|
-
const fte = num(cost?.teamFTE);
|
|
32
|
-
const f = clamp(num(focusFactor), 0, 1);
|
|
33
|
-
const points = defaultFormula(time, cost, focusFactor);
|
|
34
|
-
if (!points) return 'Set sprint length, team size, and focus factor to compute capacity.';
|
|
35
|
-
const weeksLabel = sprintCount > 0
|
|
36
|
-
? `${sprintCount} × ${sprintWeeks}wk = ${totalWeeks}wk`
|
|
37
|
-
: `${sprintWeeks}wk`;
|
|
38
|
-
return `${weeksLabel} × ${fte} FTE × ${f} = ${(totalWeeks * fte * f).toFixed(1)} → ${points} points`;
|
|
39
|
-
}
|
|
40
22
|
|
|
41
23
|
/**
|
|
42
24
|
* FNV-1a (32-bit) hash over a stable JSON serialization of the triangle.
|
|
43
|
-
* Used for drift detection by downstream consumers (notably <
|
|
25
|
+
* Used for drift detection by downstream consumers (notably <quality-target>'s
|
|
44
26
|
* ironTriangleHash field).
|
|
45
27
|
*
|
|
46
28
|
* Matches the fnv1a in src/web-components/color-picker/_color-utils.js so
|
|
@@ -172,7 +172,7 @@ export function buildTriangleSvg({
|
|
|
172
172
|
center.setAttribute('role', 'button');
|
|
173
173
|
center.setAttribute(
|
|
174
174
|
'aria-label',
|
|
175
|
-
`Quality — ${formatQualitySummary(qualitySummary)}. Activate to open the
|
|
175
|
+
`Quality — ${formatQualitySummary(qualitySummary)}. Activate to open the quality decision.`,
|
|
176
176
|
);
|
|
177
177
|
|
|
178
178
|
// Backdrop circle — sits behind the text, masks the polygon stroke.
|
|
@@ -13,8 +13,8 @@
|
|
|
13
13
|
{ "name": "data-min-capacity", "kind": "data", "purpose": "config", "type": "number", "default": "1", "description": "Floor for capacityPoints" },
|
|
14
14
|
{ "name": "data-quality-href", "kind": "data", "purpose": "config", "type": "string", "description": "Navigation fallback when iron-triangle:open-quality isn't preventDefault-ed" },
|
|
15
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": "
|
|
17
|
-
{ "name": "locked", "kind": "host-api", "purpose": "semantic-state", "type": "boolean", "description": "Read-only mode for shipped
|
|
16
|
+
{ "name": "disabled", "kind": "native", "purpose": "config", "type": "boolean", "description": "Fully off: vertex and centre Quality targets leave the tab order (aria-disabled), openEditor() is a no-op, the mode switch and all dialog controls are disabled. setManual() / setFormula() / revise() still work for the host." },
|
|
17
|
+
{ "name": "locked", "kind": "host-api", "purpose": "semantic-state", "type": "boolean", "description": "Read-only mode for shipped shapes: the same editing lock as disabled, but the centre Quality target still opens the quality decision (iron-triangle:open-quality / data-quality-href)" }
|
|
18
18
|
],
|
|
19
19
|
"slots": [],
|
|
20
20
|
"events": [
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
* is never colour-only.
|
|
26
26
|
*
|
|
27
27
|
* Optional: pass a `data-quality-summary` attribute or set the
|
|
28
|
-
* `qualitySummary` property to surface the bound
|
|
28
|
+
* `qualitySummary` property to surface the bound quality-target's
|
|
29
29
|
* decision on hover (e.g. "3 critical: perf, sec, a11y").
|
|
30
30
|
*/
|
|
31
31
|
|
|
@@ -171,7 +171,7 @@ class IronTriangle extends VBElement {
|
|
|
171
171
|
|
|
172
172
|
/** Imperatively open a vertex editor. */
|
|
173
173
|
openEditor(axis) {
|
|
174
|
-
if (!SECTIONS.includes(axis)) return;
|
|
174
|
+
if (!SECTIONS.includes(axis) || this.#inert) return;
|
|
175
175
|
const dialog = this.#ensureDialog(axis);
|
|
176
176
|
if (typeof dialog.showModal === 'function') dialog.showModal();
|
|
177
177
|
else dialog.setAttribute('open', '');
|
|
@@ -235,7 +235,7 @@ class IronTriangle extends VBElement {
|
|
|
235
235
|
const center = event.target.closest?.('.center[data-target="quality"]');
|
|
236
236
|
if (center && this.contains(center)) {
|
|
237
237
|
event.preventDefault();
|
|
238
|
-
this.#fireQualityOpen();
|
|
238
|
+
if (!this.hasAttribute('disabled')) this.#fireQualityOpen();
|
|
239
239
|
}
|
|
240
240
|
}
|
|
241
241
|
|
|
@@ -250,7 +250,7 @@ class IronTriangle extends VBElement {
|
|
|
250
250
|
const center = event.target.closest?.('.center[data-target="quality"]');
|
|
251
251
|
if (center && this.contains(center)) {
|
|
252
252
|
event.preventDefault();
|
|
253
|
-
this.#fireQualityOpen();
|
|
253
|
+
if (!this.hasAttribute('disabled')) this.#fireQualityOpen();
|
|
254
254
|
}
|
|
255
255
|
}
|
|
256
256
|
|
|
@@ -314,6 +314,7 @@ class IronTriangle extends VBElement {
|
|
|
314
314
|
this.#recompute({ source: 'dialog', field: axis });
|
|
315
315
|
});
|
|
316
316
|
|
|
317
|
+
this.#syncDisabledLocked();
|
|
317
318
|
return dialog;
|
|
318
319
|
}
|
|
319
320
|
|
|
@@ -399,6 +400,7 @@ class IronTriangle extends VBElement {
|
|
|
399
400
|
const existing = this.querySelector(':scope > svg');
|
|
400
401
|
if (existing) existing.replaceWith(svg);
|
|
401
402
|
else this.prepend(svg);
|
|
403
|
+
this.#syncHitTargetsInert();
|
|
402
404
|
this.dataset.capacityPoints = String(this.#value.capacityPoints);
|
|
403
405
|
this.dataset.capacitySource = this.#value.capacitySource;
|
|
404
406
|
}
|
|
@@ -422,12 +424,40 @@ class IronTriangle extends VBElement {
|
|
|
422
424
|
} catch { /* older Safari */ }
|
|
423
425
|
}
|
|
424
426
|
|
|
427
|
+
get #inert() { return this.hasAttribute('disabled') || this.hasAttribute('locked'); }
|
|
428
|
+
|
|
429
|
+
/**
|
|
430
|
+
* disabled / locked: no editing path stays open. styles.css already sets
|
|
431
|
+
* pointer-events: none; this closes the keyboard and programmatic paths
|
|
432
|
+
* (vertex targets leave the tab order, openEditor() is a no-op) and
|
|
433
|
+
* disables the mode switch and every dialog control, including dialogs
|
|
434
|
+
* built later.
|
|
435
|
+
*
|
|
436
|
+
* The centre Quality target is navigation, not editing, so under `locked`
|
|
437
|
+
* it stays focusable and clickable (styles.css re-enables its pointer).
|
|
438
|
+
* Under `disabled` it goes inert too: browsers suppress mouse events
|
|
439
|
+
* inside a disabled form-associated element, so keeping only its
|
|
440
|
+
* keyboard path alive would be inconsistent.
|
|
441
|
+
*/
|
|
425
442
|
#syncDisabledLocked() {
|
|
426
|
-
const off = this
|
|
443
|
+
const off = this.#inert;
|
|
427
444
|
if (this.#modeButton) this.#modeButton.disabled = off;
|
|
428
445
|
for (const dialog of Object.values(this.#dialogs)) {
|
|
429
446
|
for (const el of dialog.querySelectorAll('input, select, textarea, button')) /** @type {any} */ (el).disabled = off;
|
|
430
447
|
}
|
|
448
|
+
this.#syncHitTargetsInert();
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
/** Hit targets follow disabled / locked across re-renders. */
|
|
452
|
+
#syncHitTargetsInert() {
|
|
453
|
+
const editingOff = this.#inert;
|
|
454
|
+
const navOff = this.hasAttribute('disabled');
|
|
455
|
+
for (const target of this.querySelectorAll(':scope > svg [role="button"][tabindex]')) {
|
|
456
|
+
const off = target.matches('.center[data-target="quality"]') ? navOff : editingOff;
|
|
457
|
+
target.setAttribute('tabindex', off ? '-1' : '0');
|
|
458
|
+
if (off) target.setAttribute('aria-disabled', 'true');
|
|
459
|
+
else target.removeAttribute('aria-disabled');
|
|
460
|
+
}
|
|
431
461
|
}
|
|
432
462
|
|
|
433
463
|
// ── Internal: status strip (a11y hooks) ──────────────────────────
|
|
@@ -448,7 +478,9 @@ class IronTriangle extends VBElement {
|
|
|
448
478
|
|
|
449
479
|
const button = document.createElement('button');
|
|
450
480
|
button.type = 'button';
|
|
451
|
-
|
|
481
|
+
// VB's `ghost` variant keeps the switch secondary to the triangle;
|
|
482
|
+
// styles.css trims it further to sit inside the small status strip.
|
|
483
|
+
button.className = 'iron-triangle-mode ghost';
|
|
452
484
|
button.setAttribute('aria-pressed', 'false');
|
|
453
485
|
button.textContent = 'Manual capacity';
|
|
454
486
|
button.addEventListener('click', () => this.#openCapacityDialog());
|
|
@@ -486,9 +518,14 @@ class IronTriangle extends VBElement {
|
|
|
486
518
|
|
|
487
519
|
if (this.#modeButton) {
|
|
488
520
|
this.#modeButton.setAttribute('aria-pressed', manual ? 'true' : 'false');
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
521
|
+
// Pressed state is carried by the visible text too, not only by the
|
|
522
|
+
// underline. The aria-label starts with that visible text so voice
|
|
523
|
+
// users can say what they see (WCAG 2.5.3 Label in Name).
|
|
524
|
+
const label = manual && capacityPoints > 0 ? `Manual capacity: ${capacityPoints} pts` : 'Manual capacity';
|
|
525
|
+
if (this.#modeButton.textContent !== label) this.#modeButton.textContent = label;
|
|
526
|
+
this.#modeButton.setAttribute('aria-label', manual
|
|
527
|
+
? `${label}. Activate to edit or return to the formula.`
|
|
528
|
+
: `${label}. Activate to override the formula with a fixed number.`);
|
|
492
529
|
}
|
|
493
530
|
if (this.#deadlineWarning) {
|
|
494
531
|
this.#deadlineWarning.textContent = overDeadline ? `Deadline has passed (${deadline})` : '';
|
|
@@ -566,6 +603,7 @@ class IronTriangle extends VBElement {
|
|
|
566
603
|
if (Number.isFinite(n) && n > 0) this.setManual(n);
|
|
567
604
|
});
|
|
568
605
|
|
|
606
|
+
this.#syncDisabledLocked();
|
|
569
607
|
return dialog;
|
|
570
608
|
}
|
|
571
609
|
|
|
@@ -150,13 +150,27 @@ iron-triangle > .iron-triangle-status .iron-triangle-readout {
|
|
|
150
150
|
border: 0;
|
|
151
151
|
}
|
|
152
152
|
|
|
153
|
+
/* Quiet, secondary switch: ghost button, compact, muted until hovered or
|
|
154
|
+
pressed. VB's touch-target minimum block size is kept. Pressed adds an underline (plus the "Manual capacity: N pts" label) so
|
|
155
|
+
the state never rests on colour alone. */
|
|
153
156
|
iron-triangle > .iron-triangle-status .iron-triangle-mode {
|
|
154
157
|
font-size: inherit;
|
|
158
|
+
font-weight: var(--font-weight-normal, 400);
|
|
159
|
+
padding-block: 0;
|
|
160
|
+
padding-inline: var(--size-xs, 0.5rem);
|
|
161
|
+
border-color: transparent;
|
|
162
|
+
color: var(--color-text-muted);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
iron-triangle > .iron-triangle-status .iron-triangle-mode:hover {
|
|
166
|
+
color: var(--color-text);
|
|
155
167
|
}
|
|
156
168
|
|
|
157
169
|
iron-triangle > .iron-triangle-status .iron-triangle-mode[aria-pressed="true"] {
|
|
158
|
-
border-color: var(--color-interactive);
|
|
159
170
|
color: var(--color-interactive);
|
|
171
|
+
text-decoration: underline;
|
|
172
|
+
text-decoration-thickness: var(--border-width-medium, 2px);
|
|
173
|
+
text-underline-offset: 0.25em;
|
|
160
174
|
}
|
|
161
175
|
|
|
162
176
|
/* Visible, text-bearing warning — never colour-only. */
|
|
@@ -193,6 +207,13 @@ iron-triangle[locked] {
|
|
|
193
207
|
pointer-events: none;
|
|
194
208
|
}
|
|
195
209
|
|
|
210
|
+
/* The centre Quality target navigates to the quality decision rather than
|
|
211
|
+
editing the shape, so it keeps working on a locked (read-only) triangle.
|
|
212
|
+
A disabled triangle stays fully off, like any disabled control. */
|
|
213
|
+
iron-triangle[locked]:not([disabled]) svg .center[data-target="quality"] {
|
|
214
|
+
pointer-events: auto;
|
|
215
|
+
}
|
|
216
|
+
|
|
196
217
|
/* ── Dialog chrome ────────────────────────────────────────────── */
|
|
197
218
|
|
|
198
219
|
iron-triangle > dialog.iron-triangle-dialog {
|
|
@@ -5,8 +5,8 @@
|
|
|
5
5
|
"description": "Columnar drag-and-drop board with user-defined columns, count badges, and optional WIP limits",
|
|
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 for columns and items" },
|
|
9
|
-
{ "name": "title", "kind": "host-api", "purpose": "config", "type": "string", "description": "Optional heading above the board" },
|
|
8
|
+
{ "name": "src", "kind": "host-api", "purpose": "config", "type": "string", "description": "URL to JSON data for columns and items. Fetched once per value; a new value replaces the board and aborts an older in-flight request" },
|
|
9
|
+
{ "name": "title", "kind": "host-api", "purpose": "config", "type": "string", "description": "Optional heading above the board; also names the board region" },
|
|
10
10
|
{ "name": "compact", "kind": "host-api", "purpose": "config", "type": "boolean", "description": "Reduced spacing variant" }
|
|
11
11
|
],
|
|
12
12
|
"childAttributes": [
|
|
@@ -16,9 +16,11 @@
|
|
|
16
16
|
{ "name": "data-column-color", "on": "section", "type": "string", "description": "Color token for column tint (success, warning, error, info)" }
|
|
17
17
|
],
|
|
18
18
|
"events": [
|
|
19
|
-
{ "name": "kanban-board:transfer",
|
|
20
|
-
{ "name": "kanban-board:reorder",
|
|
21
|
-
{ "name": "kanban-board:ready",
|
|
22
|
-
{ "name": "kanban-board:wip-exceeded",
|
|
19
|
+
{ "name": "kanban-board:transfer", "detail": "{ itemId, fromColumn, toColumn, newIndex, item }", "description": "Item moved between columns" },
|
|
20
|
+
{ "name": "kanban-board:reorder", "detail": "{ itemId, column, oldIndex, newIndex }", "description": "Item reordered within a column" },
|
|
21
|
+
{ "name": "kanban-board:ready", "detail": "{ columnCount, itemCount }", "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 items-changed with source 'src') or when the board is moved in the DOM" },
|
|
22
|
+
{ "name": "kanban-board:wip-exceeded", "detail": "{ column, limit, count }", "description": "Fired when a drop takes a column over its WIP limit" },
|
|
23
|
+
{ "name": "kanban-board:items-changed", "detail": "{ items, source: 'api' | 'drag' | 'src' }", "description": "Fired after the items array changes: an .items assignment, or a .columns assignment that drops cards (source 'api'), a drag (source 'drag'), or each src load once its data has rendered (source 'src'). Filter on source to avoid feedback loops" },
|
|
24
|
+
{ "name": "kanban-board:upgraded", "detail": "", "description": "Fired (in a microtask) each time the board connects and upgrades; a safe signal for assigning .columns / .items from a framework" }
|
|
23
25
|
]
|
|
24
26
|
}
|