@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,203 @@
|
|
|
1
|
+
/* quality-target — polygon-as-UI quality picker + viz.
|
|
2
|
+
|
|
3
|
+
Three concentric reference rings (Critical 1.0 / Important 0.6 /
|
|
4
|
+
Acceptable 0.3) replace the old envelope polygon. Each axis
|
|
5
|
+
labels with the full ility name; set/unset states tint the label.
|
|
6
|
+
Capacity readout sits in a reserved hollow center. */
|
|
7
|
+
|
|
8
|
+
quality-target {
|
|
9
|
+
display: block;
|
|
10
|
+
inline-size: 100%;
|
|
11
|
+
max-inline-size: var(--quality-target-max, 36rem);
|
|
12
|
+
margin-inline: auto;
|
|
13
|
+
container-type: inline-size;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
quality-target > svg {
|
|
17
|
+
inline-size: 100%;
|
|
18
|
+
block-size: auto;
|
|
19
|
+
display: block;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/* ── Reference rings (Critical / Important / Acceptable) ──────── */
|
|
23
|
+
|
|
24
|
+
quality-target svg .ring {
|
|
25
|
+
fill: none;
|
|
26
|
+
stroke: var(--color-border-muted, var(--color-border, #e5e7eb));
|
|
27
|
+
stroke-width: 1;
|
|
28
|
+
stroke-dasharray: 3 3;
|
|
29
|
+
pointer-events: none;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
quality-target svg .ring-critical { stroke: color-mix(in oklab, var(--color-error) 60%, transparent); }
|
|
33
|
+
quality-target svg .ring-important { stroke: color-mix(in oklab, var(--color-warning) 50%, transparent); }
|
|
34
|
+
quality-target svg .ring-acceptable { stroke: color-mix(in oklab, var(--color-success) 50%, transparent); }
|
|
35
|
+
|
|
36
|
+
/* ── Spokes ────────────────────────────────────────────────── */
|
|
37
|
+
|
|
38
|
+
quality-target svg .spoke {
|
|
39
|
+
stroke: var(--color-border, #e5e7eb);
|
|
40
|
+
stroke-width: 0.75;
|
|
41
|
+
stroke-dasharray: 1 4;
|
|
42
|
+
pointer-events: none;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/* ── Vector polygon ───────────────────────────────────────── */
|
|
46
|
+
|
|
47
|
+
quality-target svg .vector {
|
|
48
|
+
fill: var(--quality-target-vector-fill, color-mix(in oklab, var(--color-interactive) 22%, transparent));
|
|
49
|
+
stroke: var(--quality-target-vector-stroke, var(--color-interactive));
|
|
50
|
+
stroke-width: 1.5;
|
|
51
|
+
stroke-linejoin: round;
|
|
52
|
+
pointer-events: none;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
quality-target svg .vector-spoke {
|
|
56
|
+
stroke: var(--quality-target-vector-stroke, var(--color-interactive));
|
|
57
|
+
stroke-width: 2;
|
|
58
|
+
stroke-linecap: round;
|
|
59
|
+
pointer-events: none;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/* ── Axis hit targets ──────────────────────────────────────── */
|
|
63
|
+
|
|
64
|
+
quality-target svg .axis {
|
|
65
|
+
cursor: pointer;
|
|
66
|
+
outline: none;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
quality-target svg .axis .hit {
|
|
70
|
+
fill: transparent;
|
|
71
|
+
stroke: transparent;
|
|
72
|
+
transition: fill 120ms ease, stroke 120ms ease;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
quality-target svg .axis:hover .hit,
|
|
76
|
+
quality-target svg .axis:focus-visible .hit {
|
|
77
|
+
fill: color-mix(in oklab, var(--color-interactive) 8%, transparent);
|
|
78
|
+
stroke: var(--color-interactive);
|
|
79
|
+
stroke-width: 1;
|
|
80
|
+
stroke-dasharray: 2 2;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
quality-target svg .axis-label {
|
|
84
|
+
font-size: var(--quality-target-label-size, 0.625rem);
|
|
85
|
+
fill: var(--color-text-muted);
|
|
86
|
+
letter-spacing: 0.02em;
|
|
87
|
+
pointer-events: none;
|
|
88
|
+
font-weight: var(--font-weight-regular, 400);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/* Set/unset label styling — set axes get a colored, bolder label;
|
|
92
|
+
unset axes stay muted gray. */
|
|
93
|
+
quality-target svg .axis[data-level] .axis-label {
|
|
94
|
+
font-weight: var(--font-weight-semibold, 600);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
quality-target svg .axis[data-level="critical"] .axis-label { fill: var(--color-error, #dc2626); }
|
|
98
|
+
quality-target svg .axis[data-level="important"] .axis-label { fill: var(--color-warning, #b45309); }
|
|
99
|
+
quality-target svg .axis[data-level="acceptable"] .axis-label { fill: var(--color-success, #15803d); }
|
|
100
|
+
quality-target svg .axis[data-level="not-relevant"] .axis-label {
|
|
101
|
+
fill: var(--color-text-muted, #6b7280);
|
|
102
|
+
text-decoration: line-through;
|
|
103
|
+
font-weight: var(--font-weight-regular, 400);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/* Markers — only render on set axes (component side); color-coded. */
|
|
107
|
+
quality-target svg .axis .marker {
|
|
108
|
+
fill: var(--color-interactive);
|
|
109
|
+
stroke: var(--color-surface);
|
|
110
|
+
stroke-width: 1;
|
|
111
|
+
pointer-events: none;
|
|
112
|
+
}
|
|
113
|
+
quality-target svg .axis[data-level="critical"] .marker { fill: var(--color-error, #dc2626); }
|
|
114
|
+
quality-target svg .axis[data-level="important"] .marker { fill: var(--color-warning, #b45309); }
|
|
115
|
+
quality-target svg .axis[data-level="acceptable"] .marker { fill: var(--color-success, #16a34a); }
|
|
116
|
+
quality-target svg .axis[data-level="not-relevant"] .marker { fill: var(--color-text-muted, #6b7280); opacity: 0.5; }
|
|
117
|
+
|
|
118
|
+
/* ── Center capacity readout ──────────────────────────────── */
|
|
119
|
+
|
|
120
|
+
quality-target svg .capacity-backdrop {
|
|
121
|
+
fill: var(--color-surface);
|
|
122
|
+
stroke: var(--color-border-muted, var(--color-border, #e5e7eb));
|
|
123
|
+
stroke-width: 1;
|
|
124
|
+
pointer-events: none;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
quality-target svg .capacity-sum {
|
|
128
|
+
font-size: var(--quality-target-capacity-size, 1.25rem);
|
|
129
|
+
font-weight: var(--font-weight-bold, 700);
|
|
130
|
+
fill: var(--color-text);
|
|
131
|
+
font-variant-numeric: tabular-nums;
|
|
132
|
+
pointer-events: none;
|
|
133
|
+
}
|
|
134
|
+
quality-target svg .capacity-denom {
|
|
135
|
+
font-size: 0.55rem;
|
|
136
|
+
fill: var(--color-text-muted);
|
|
137
|
+
font-variant-numeric: tabular-nums;
|
|
138
|
+
pointer-events: none;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
quality-target svg .center[data-over] .capacity-sum {
|
|
142
|
+
fill: var(--color-error);
|
|
143
|
+
}
|
|
144
|
+
quality-target:state(over-budget) svg .vector {
|
|
145
|
+
stroke: var(--color-warning);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/* ── Footer (overrun rationale + errors) ──────────────────── */
|
|
149
|
+
|
|
150
|
+
quality-target .quality-footer {
|
|
151
|
+
margin-block-start: var(--size-m, 1rem);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
quality-target [data-quality-overrun] {
|
|
155
|
+
padding: var(--size-m, 1rem);
|
|
156
|
+
border: var(--border-width-thin, 1px) solid var(--color-error);
|
|
157
|
+
border-radius: var(--radius-m);
|
|
158
|
+
background: var(--color-error-subtle, oklch(95% 0.05 27));
|
|
159
|
+
display: grid;
|
|
160
|
+
gap: var(--size-2xs, 0.25rem);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
quality-target [data-quality-overrun][hidden] {
|
|
164
|
+
display: none;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
quality-target [data-quality-errors] {
|
|
168
|
+
display: none;
|
|
169
|
+
}
|
|
170
|
+
quality-target:state(missing-rationale) [data-quality-errors] {
|
|
171
|
+
display: block;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/* ── Per-ility dialogs ──────────────────────────────────── */
|
|
175
|
+
|
|
176
|
+
quality-target > dialog.quality-dialog {
|
|
177
|
+
inline-size: min(28rem, 92vw);
|
|
178
|
+
border: var(--border-width-thin, 1px) solid var(--color-border);
|
|
179
|
+
border-radius: var(--radius-l, 0.75rem);
|
|
180
|
+
padding: var(--size-l, 1.5rem);
|
|
181
|
+
background: var(--color-surface);
|
|
182
|
+
color: var(--color-text);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
quality-target > dialog.quality-dialog::backdrop {
|
|
186
|
+
background: oklch(from var(--color-surface) l c h / 0.5);
|
|
187
|
+
backdrop-filter: blur(2px);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
quality-target > dialog.quality-dialog h3 {
|
|
191
|
+
margin: 0 0 var(--size-m) 0;
|
|
192
|
+
font-size: var(--font-size-l, 1.125rem);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
quality-target > dialog.quality-dialog fieldset {
|
|
196
|
+
border: 1px solid var(--color-border-muted, var(--color-border));
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
quality-target[disabled],
|
|
200
|
+
quality-target[locked] {
|
|
201
|
+
opacity: 0.85;
|
|
202
|
+
pointer-events: none;
|
|
203
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "../../../schemas/api.schema.json",
|
|
3
|
+
"element": "requirement-card",
|
|
4
|
+
"type": "web-component",
|
|
5
|
+
"description": "Display-only card for ONE NFR ility row. Renders priority + rationale + optional conflict flag for status dashboards and ADR appendices. The prioritization decision itself lives in <nfr-compass>.",
|
|
6
|
+
"htmlvalidate": {
|
|
7
|
+
"flow": true,
|
|
8
|
+
"permittedContent": ["@flow"]
|
|
9
|
+
},
|
|
10
|
+
"attributes": [
|
|
11
|
+
{ "name": "data-priority", "kind": "data", "purpose": "visual-variant", "type": "enum", "values": ["critical", "important", "acceptable", "not-relevant"], "description": "Drives :state(priority-*), default pill text, and accent color" },
|
|
12
|
+
{ "name": "data-conflict", "kind": "data", "purpose": "semantic-state", "type": "boolean", "description": "Flag a conflict / drift note; renders an emphasized border" },
|
|
13
|
+
{ "name": "tabindex", "kind": "native", "purpose": "config", "type": "number", "description": "Set to 0 to make the card directly focusable + activatable (otherwise wrap in <a>/<button>)" }
|
|
14
|
+
],
|
|
15
|
+
"slots": [
|
|
16
|
+
{ "name": "name", "description": "Ility label (e.g. \"Performance\")" },
|
|
17
|
+
{ "name": "badge", "description": "Category / version / source tag (small uppercase muted text)" },
|
|
18
|
+
{ "name": "priority-pill", "description": "Author-supplied custom pill; default pill renders from data-priority when this slot is empty" },
|
|
19
|
+
{ "name": "rationale", "description": "Why this priority was chosen (especially for Critical rows)" },
|
|
20
|
+
{ "name": "conflicts", "description": "Short conflict / drift note (paired with data-conflict)" }
|
|
21
|
+
],
|
|
22
|
+
"events": [
|
|
23
|
+
{ "name": "requirement-card:click", "detail": "{ priority, hasConflict, originalEvent }", "description": "Bubbles when the card is activated by mouse, keyboard, or touch (only when the card is interactive — wrapped in <a>/<button> or has tabindex)" }
|
|
24
|
+
]
|
|
25
|
+
}
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* <requirement-card> — Display atom for ONE NFR ility row.
|
|
3
|
+
*
|
|
4
|
+
* Display-only. The prioritization decision lives in <nfr-compass>;
|
|
5
|
+
* this component just renders one row's worth of state — name +
|
|
6
|
+
* category badge + priority pill + rationale + (optional) conflict flag —
|
|
7
|
+
* for use on status dashboards, ADR appendices, or anywhere a project
|
|
8
|
+
* wants to surface a single ility outside the compass.
|
|
9
|
+
*
|
|
10
|
+
* Slots:
|
|
11
|
+
* name — ility label (e.g. "Performance")
|
|
12
|
+
* badge — category / version / source tag
|
|
13
|
+
* priority-pill — author-supplied custom pill (otherwise the default
|
|
14
|
+
* pill renders from data-priority)
|
|
15
|
+
* rationale — why this priority was chosen (visible especially
|
|
16
|
+
* for Critical rows)
|
|
17
|
+
* conflicts — short conflict / drift note
|
|
18
|
+
*
|
|
19
|
+
* Attributes:
|
|
20
|
+
* data-priority — critical | important | acceptable | not-relevant
|
|
21
|
+
* data-conflict — boolean (presence flag)
|
|
22
|
+
*
|
|
23
|
+
* Emits:
|
|
24
|
+
* requirement-card:click — bubbles a structured detail when the card
|
|
25
|
+
* is activated (mouse, keyboard, or touch);
|
|
26
|
+
* parent components can wire navigation.
|
|
27
|
+
*
|
|
28
|
+
* Pattern reference: matches the shadow-DOM + slot conventions from
|
|
29
|
+
* <score-card> (also a single-row dashboard atom).
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
import { styles } from './styles.js';
|
|
33
|
+
import { VBElement } from '../../lib/vb-element.js';
|
|
34
|
+
import { registerComponent } from '../../lib/bundle-registry.js';
|
|
35
|
+
|
|
36
|
+
const PRIORITIES = Object.freeze(['critical', 'important', 'acceptable', 'not-relevant']);
|
|
37
|
+
const PRIORITY_LABELS = Object.freeze({
|
|
38
|
+
'critical': 'Critical',
|
|
39
|
+
'important': 'Important',
|
|
40
|
+
'acceptable': 'Acceptable',
|
|
41
|
+
'not-relevant': 'Not relevant',
|
|
42
|
+
});
|
|
43
|
+
const OPTIONAL_SLOTS = ['badge', 'priority-pill', 'rationale', 'conflicts'];
|
|
44
|
+
|
|
45
|
+
const TEMPLATE = `
|
|
46
|
+
<style>${styles}</style>
|
|
47
|
+
<article class="card" part="card">
|
|
48
|
+
<header class="head" part="head">
|
|
49
|
+
<span class="name" part="name"><slot name="name"></slot></span>
|
|
50
|
+
<span class="badge" part="badge"><slot name="badge"></slot></span>
|
|
51
|
+
</header>
|
|
52
|
+
<span class="pill" part="pill">
|
|
53
|
+
<slot name="priority-pill"><span class="priority-default" part="priority-default" data-priority-default></span></slot>
|
|
54
|
+
</span>
|
|
55
|
+
<p class="rationale" part="rationale"><slot name="rationale"></slot></p>
|
|
56
|
+
<p class="conflicts" part="conflicts"><slot name="conflicts"></slot></p>
|
|
57
|
+
</article>
|
|
58
|
+
`;
|
|
59
|
+
|
|
60
|
+
class RequirementCard extends VBElement {
|
|
61
|
+
static get observedAttributes() {
|
|
62
|
+
return ['data-priority', 'data-conflict', 'tabindex'];
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
setup() {
|
|
66
|
+
if (!this.shadowRoot) {
|
|
67
|
+
const root = this.attachShadow({ mode: 'open' });
|
|
68
|
+
root.innerHTML = TEMPLATE;
|
|
69
|
+
for (const name of OPTIONAL_SLOTS) {
|
|
70
|
+
const slot = root.querySelector(`slot[name="${name}"]`);
|
|
71
|
+
slot?.addEventListener('slotchange', () => this.#syncSlot(name, slot));
|
|
72
|
+
this.#syncSlot(name, slot);
|
|
73
|
+
}
|
|
74
|
+
} else {
|
|
75
|
+
for (const name of OPTIONAL_SLOTS) {
|
|
76
|
+
const slot = this.shadowRoot.querySelector(`slot[name="${name}"]`);
|
|
77
|
+
this.#syncSlot(name, slot);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
this.#syncPriority();
|
|
81
|
+
this.#syncDefaultPill();
|
|
82
|
+
this.#syncInteractive();
|
|
83
|
+
|
|
84
|
+
this.listen(this, 'click', (e) => this.#onActivate(e));
|
|
85
|
+
this.listen(this, 'keydown', (e) => {
|
|
86
|
+
const ke = /** @type {KeyboardEvent} */ (e);
|
|
87
|
+
if ((ke.key === 'Enter' || ke.key === ' ') && this.#interactive) {
|
|
88
|
+
ke.preventDefault();
|
|
89
|
+
this.#onActivate(ke);
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
attributeChangedCallback(name) {
|
|
95
|
+
if (!this.isConnected) return;
|
|
96
|
+
if (name === 'data-priority') {
|
|
97
|
+
this.#syncPriority();
|
|
98
|
+
this.#syncDefaultPill();
|
|
99
|
+
} else if (name === 'data-conflict') {
|
|
100
|
+
// Border + shadow are CSS-driven via [data-conflict]; no JS needed.
|
|
101
|
+
} else if (name === 'tabindex') {
|
|
102
|
+
this.#syncInteractive();
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// ── Public API ────────────────────────────────────────────────────
|
|
107
|
+
|
|
108
|
+
get priority() { return (this.dataset.priority || '').toLowerCase(); }
|
|
109
|
+
set priority(v) {
|
|
110
|
+
const next = String(v || '').toLowerCase();
|
|
111
|
+
if (PRIORITIES.includes(next)) this.dataset.priority = next;
|
|
112
|
+
else delete this.dataset.priority;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
get hasConflict() { return this.hasAttribute('data-conflict'); }
|
|
116
|
+
set hasConflict(v) {
|
|
117
|
+
if (v) this.setAttribute('data-conflict', '');
|
|
118
|
+
else this.removeAttribute('data-conflict');
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// ── Internal ──────────────────────────────────────────────────────
|
|
122
|
+
|
|
123
|
+
get #interactive() {
|
|
124
|
+
return this.hasAttribute('tabindex') || this.closest('a[href], button');
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
#syncSlot(name, slot) {
|
|
128
|
+
const hasContent = !!slot && slot.assignedNodes({ flatten: true }).some(n =>
|
|
129
|
+
n.nodeType === Node.ELEMENT_NODE ||
|
|
130
|
+
(n.nodeType === Node.TEXT_NODE && n.textContent.trim().length > 0)
|
|
131
|
+
);
|
|
132
|
+
this.setState(`has-${name}`, hasContent);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
#syncPriority() {
|
|
136
|
+
const value = (this.dataset.priority || '').toLowerCase();
|
|
137
|
+
for (const p of PRIORITIES) this.setState(`priority-${p}`, p === value);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
#syncDefaultPill() {
|
|
141
|
+
const value = (this.dataset.priority || '').toLowerCase();
|
|
142
|
+
const el = this.shadowRoot?.querySelector('[data-priority-default]');
|
|
143
|
+
if (!el) return;
|
|
144
|
+
el.textContent = PRIORITY_LABELS[value] || '';
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
#syncInteractive() {
|
|
148
|
+
this.setState('interactive', !!this.#interactive);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
#onActivate(event) {
|
|
152
|
+
if (!this.#interactive) return;
|
|
153
|
+
this.dispatchEvent(new CustomEvent('requirement-card:click', {
|
|
154
|
+
bubbles: true, composed: true,
|
|
155
|
+
detail: {
|
|
156
|
+
priority: this.priority || null,
|
|
157
|
+
hasConflict: this.hasConflict,
|
|
158
|
+
originalEvent: event,
|
|
159
|
+
},
|
|
160
|
+
}));
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
registerComponent('requirement-card', RequirementCard);
|
|
165
|
+
|
|
166
|
+
export { RequirementCard };
|
|
@@ -0,0 +1,36 @@
|
|
|
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>requirement-card (static fallback)</title>
|
|
7
|
+
</head>
|
|
8
|
+
<body>
|
|
9
|
+
<!--
|
|
10
|
+
Display-only card for ONE NFR ility row. The prioritization
|
|
11
|
+
decision lives in <nfr-compass>; this is the visual atom.
|
|
12
|
+
Pre-upgrade, the card renders as a plain article — no JS needed
|
|
13
|
+
for it to be readable.
|
|
14
|
+
-->
|
|
15
|
+
<requirement-card data-priority="critical">
|
|
16
|
+
<span slot="name">Performance</span>
|
|
17
|
+
<span slot="badge">Quality attribute</span>
|
|
18
|
+
<p slot="rationale">Sub-200ms TTI is our differentiator vs Competitor X.</p>
|
|
19
|
+
</requirement-card>
|
|
20
|
+
|
|
21
|
+
<requirement-card data-priority="not-relevant">
|
|
22
|
+
<span slot="name">Internationalization</span>
|
|
23
|
+
<span slot="badge">Quality attribute</span>
|
|
24
|
+
</requirement-card>
|
|
25
|
+
|
|
26
|
+
<!-- Drill-down: wrap in <a href> to make the whole card clickable -->
|
|
27
|
+
<a href="/requirements?ility=security">
|
|
28
|
+
<requirement-card data-priority="important" data-conflict>
|
|
29
|
+
<span slot="name">Security</span>
|
|
30
|
+
<span slot="badge">Quality attribute</span>
|
|
31
|
+
<p slot="rationale">SOC 2 audit pending Q2.</p>
|
|
32
|
+
<p slot="conflicts">Vector decided against an older Iron Triangle — re-review.</p>
|
|
33
|
+
</requirement-card>
|
|
34
|
+
</a>
|
|
35
|
+
</body>
|
|
36
|
+
</html>
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shadow DOM styles for <requirement-card>
|
|
3
|
+
*
|
|
4
|
+
* Display-only card for one ility's prioritization decision.
|
|
5
|
+
* Theme tokens swap automatically across light/dark and brand themes.
|
|
6
|
+
*/
|
|
7
|
+
export const styles = `
|
|
8
|
+
:host {
|
|
9
|
+
--_padding: var(--requirement-card-padding, var(--size-m, 1rem));
|
|
10
|
+
--_radius: var(--requirement-card-radius, var(--radius-m, 0.5rem));
|
|
11
|
+
--_gap: var(--requirement-card-gap, var(--size-2xs, 0.25rem));
|
|
12
|
+
--_surface: var(--requirement-card-surface, var(--color-surface, #fff));
|
|
13
|
+
--_border: var(--requirement-card-border, 1px solid var(--color-border, #e5e7eb));
|
|
14
|
+
--_text: var(--requirement-card-text, var(--color-text, #1a1a1a));
|
|
15
|
+
--_muted: var(--requirement-card-muted, var(--color-text-muted, #6b7280));
|
|
16
|
+
--_critical-bg: var(--requirement-card-critical-bg, var(--color-error-subtle, oklch(95% 0.05 27)));
|
|
17
|
+
--_critical-fg: var(--requirement-card-critical-fg, var(--color-error, #dc2626));
|
|
18
|
+
--_important-bg: var(--requirement-card-important-bg, var(--color-warning-subtle, oklch(95% 0.05 80)));
|
|
19
|
+
--_important-fg: var(--requirement-card-important-fg, var(--color-warning, #b45309));
|
|
20
|
+
--_acceptable-bg: var(--requirement-card-acceptable-bg, var(--color-success-subtle, oklch(95% 0.05 145)));
|
|
21
|
+
--_acceptable-fg: var(--requirement-card-acceptable-fg, var(--color-success, #15803d));
|
|
22
|
+
--_skipped-bg: var(--requirement-card-skipped-bg, var(--color-surface-raised, var(--color-surface, #f5f5f5)));
|
|
23
|
+
--_skipped-fg: var(--requirement-card-skipped-fg, var(--color-text-muted, #6b7280));
|
|
24
|
+
--_conflict: var(--requirement-card-conflict-color, var(--color-error, #dc2626));
|
|
25
|
+
|
|
26
|
+
display: block;
|
|
27
|
+
padding: var(--_padding);
|
|
28
|
+
border-radius: var(--_radius);
|
|
29
|
+
background: var(--_surface);
|
|
30
|
+
border: var(--_border);
|
|
31
|
+
color: var(--_text);
|
|
32
|
+
container-type: inline-size;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
.card {
|
|
36
|
+
display: grid;
|
|
37
|
+
gap: var(--_gap);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
.head {
|
|
41
|
+
display: flex;
|
|
42
|
+
align-items: baseline;
|
|
43
|
+
justify-content: space-between;
|
|
44
|
+
gap: var(--size-s, 0.75rem);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
.name {
|
|
48
|
+
font-weight: var(--font-weight-semibold, 600);
|
|
49
|
+
font-size: var(--font-size-md, 1rem);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
.badge ::slotted([slot="badge"]) {
|
|
53
|
+
font-size: var(--font-size-xs, 0.75em);
|
|
54
|
+
color: var(--_muted);
|
|
55
|
+
text-transform: uppercase;
|
|
56
|
+
letter-spacing: 0.05em;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
.pill ::slotted([slot="priority-pill"]),
|
|
60
|
+
.priority-default {
|
|
61
|
+
display: inline-block;
|
|
62
|
+
padding: 0.15em 0.6em;
|
|
63
|
+
border-radius: var(--radius-pill, 999px);
|
|
64
|
+
font-size: var(--font-size-xs, 0.75em);
|
|
65
|
+
font-weight: var(--font-weight-semibold, 600);
|
|
66
|
+
text-transform: uppercase;
|
|
67
|
+
letter-spacing: 0.05em;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/* Priority-driven surface tinting on the card itself (when no slotted pill) */
|
|
71
|
+
:host(:state(priority-critical)) { border-color: var(--_critical-fg); }
|
|
72
|
+
:host(:state(priority-important)) { border-color: var(--_important-fg); }
|
|
73
|
+
:host(:state(priority-acceptable)) { border-color: var(--_acceptable-fg); }
|
|
74
|
+
:host(:state(priority-not-relevant)) { opacity: 0.6; }
|
|
75
|
+
|
|
76
|
+
:host(:state(priority-critical)) .priority-default { background: var(--_critical-bg); color: var(--_critical-fg); }
|
|
77
|
+
:host(:state(priority-important)) .priority-default { background: var(--_important-bg); color: var(--_important-fg); }
|
|
78
|
+
:host(:state(priority-acceptable)) .priority-default { background: var(--_acceptable-bg); color: var(--_acceptable-fg); }
|
|
79
|
+
:host(:state(priority-not-relevant)) .priority-default { background: var(--_skipped-bg); color: var(--_skipped-fg); text-decoration: line-through; }
|
|
80
|
+
|
|
81
|
+
/* Hide the default pill when the author supplied one via slot */
|
|
82
|
+
:host(:state(has-priority-pill)) .priority-default { display: none; }
|
|
83
|
+
|
|
84
|
+
.rationale ::slotted([slot="rationale"]) {
|
|
85
|
+
color: var(--_muted);
|
|
86
|
+
font-size: var(--font-size-sm, 0.875rem);
|
|
87
|
+
line-height: 1.45;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
.conflicts ::slotted([slot="conflicts"]) {
|
|
91
|
+
color: var(--_conflict);
|
|
92
|
+
font-size: var(--font-size-sm, 0.875rem);
|
|
93
|
+
font-weight: var(--font-weight-semibold, 600);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/* When no rationale or conflicts content, collapse the slot row */
|
|
97
|
+
:host(:not(:state(has-rationale))) .rationale,
|
|
98
|
+
:host(:not(:state(has-conflicts))) .conflicts { display: none; }
|
|
99
|
+
|
|
100
|
+
/* Conflict flag */
|
|
101
|
+
:host([data-conflict]) {
|
|
102
|
+
border-color: var(--_conflict);
|
|
103
|
+
box-shadow: inset 0 0 0 1px var(--_conflict);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/* Interactive: bubble click event so parents can wire up navigation */
|
|
107
|
+
:host { cursor: default; }
|
|
108
|
+
:host(:state(interactive)) { cursor: pointer; }
|
|
109
|
+
:host(:state(interactive)):hover { background: color-mix(in oklab, var(--_surface) 92%, currentColor 8%); }
|
|
110
|
+
:host(:focus-visible) {
|
|
111
|
+
outline: 2px solid var(--color-focus-ring, var(--color-interactive, currentColor));
|
|
112
|
+
outline-offset: 2px;
|
|
113
|
+
}
|
|
114
|
+
`;
|