@profpowell/vb-project-planning 0.2.1 → 0.2.3
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 +10 -5
- package/dist/vb-project-planning.css +69 -3
- package/dist/vb-project-planning.js +3191 -2544
- package/dist/vb-project-planning.min.css +1 -1
- package/dist/vb-project-planning.min.js +221 -197
- 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/lib/vb-view-transition.js +7 -1
- 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 +14 -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 +0 -18
- 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/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/risk-register/styles.css +9 -1
- 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 +22 -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
|
@@ -6,30 +6,40 @@
|
|
|
6
6
|
<story-map>
|
|
7
7
|
<section data-activity="discovery" data-activity-label="Discovery">
|
|
8
8
|
<user-story draggable="true" data-id="PROJ-101"
|
|
9
|
-
story-id="PROJ-101"
|
|
9
|
+
story-id="PROJ-101"
|
|
10
10
|
priority="high" status="done" points="5" detail="compact">
|
|
11
|
+
<span slot="persona">Researcher</span>
|
|
12
|
+
<span slot="action">interview target users</span>
|
|
11
13
|
</user-story>
|
|
12
14
|
<user-story draggable="true" data-id="PROJ-102"
|
|
13
|
-
story-id="PROJ-102"
|
|
15
|
+
story-id="PROJ-102"
|
|
14
16
|
priority="medium" status="done" points="3" detail="compact">
|
|
17
|
+
<span slot="persona">PM</span>
|
|
18
|
+
<span slot="action">review analytics data</span>
|
|
15
19
|
</user-story>
|
|
16
20
|
</section>
|
|
17
21
|
<section data-activity="design" data-activity-label="Design">
|
|
18
22
|
<user-story draggable="true" data-id="PROJ-201"
|
|
19
|
-
story-id="PROJ-201"
|
|
23
|
+
story-id="PROJ-201"
|
|
20
24
|
priority="high" status="in-progress" points="5" detail="compact">
|
|
25
|
+
<span slot="persona">Designer</span>
|
|
26
|
+
<span slot="action">create wireframes</span>
|
|
21
27
|
</user-story>
|
|
22
28
|
</section>
|
|
23
29
|
<section data-activity="build" data-activity-label="Build">
|
|
24
30
|
<user-story draggable="true" data-id="PROJ-301"
|
|
25
|
-
story-id="PROJ-301"
|
|
31
|
+
story-id="PROJ-301"
|
|
26
32
|
priority="high" status="to-do" points="8" detail="compact">
|
|
33
|
+
<span slot="persona">Developer</span>
|
|
34
|
+
<span slot="action">implement API endpoints</span>
|
|
27
35
|
</user-story>
|
|
28
36
|
</section>
|
|
29
37
|
<section data-activity="validate" data-activity-label="Validate">
|
|
30
38
|
<user-story draggable="true" data-id="PROJ-401"
|
|
31
|
-
story-id="PROJ-401"
|
|
39
|
+
story-id="PROJ-401"
|
|
32
40
|
priority="high" status="backlog" points="5" detail="compact">
|
|
41
|
+
<span slot="persona">QA</span>
|
|
42
|
+
<span slot="action">run usability testing</span>
|
|
33
43
|
</user-story>
|
|
34
44
|
</section>
|
|
35
45
|
</story-map>
|
|
@@ -23,7 +23,14 @@
|
|
|
23
23
|
"kind": "host-api",
|
|
24
24
|
"purpose": "config",
|
|
25
25
|
"type": "string",
|
|
26
|
-
"description": "Attribute on the row element whose value(s) reference column
|
|
26
|
+
"description": "Attribute on the row element whose value(s) reference column identifiers (see match-attr); comma-separated values supported"
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
"name": "match-attr",
|
|
30
|
+
"kind": "host-api",
|
|
31
|
+
"purpose": "config",
|
|
32
|
+
"type": "string",
|
|
33
|
+
"description": "Attribute on column elements that link-attr values are matched against. Default: the column's id, story-id (<user-story>) or item-id (<work-item>), whichever matches"
|
|
27
34
|
},
|
|
28
35
|
{
|
|
29
36
|
"name": "label",
|
|
@@ -44,7 +51,7 @@
|
|
|
44
51
|
"kind": "host-api",
|
|
45
52
|
"purpose": "config",
|
|
46
53
|
"type": "string",
|
|
47
|
-
"description": "Glyph shown in linked cells (default:
|
|
54
|
+
"description": "Glyph shown in linked cells (default: ✓)"
|
|
48
55
|
},
|
|
49
56
|
{
|
|
50
57
|
"name": "flag-orphans",
|
|
@@ -71,7 +78,7 @@
|
|
|
71
78
|
{
|
|
72
79
|
"name": "traceability-matrix:highlight",
|
|
73
80
|
"detail": "{ rowEl, colEl, on }",
|
|
74
|
-
"description": "Fired when a cell toggles chain highlight"
|
|
81
|
+
"description": "Fired when a cell toggles chain highlight (click, or Enter/Space on the cell's button)"
|
|
75
82
|
}
|
|
76
83
|
],
|
|
77
84
|
"structure": [],
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Auto-discovers row and column source elements from the page and
|
|
5
5
|
* renders a <data-table> with checkmark intersections wherever a row
|
|
6
|
-
* element's `link-attr` references a column element's
|
|
6
|
+
* element's `link-attr` references a column element's identifier. Surfaces
|
|
7
7
|
* the canonical Requirements Traceability Matrix (RTM) shape:
|
|
8
8
|
* rows × columns at-a-glance, with empty rows/columns flagged as
|
|
9
9
|
* orphans.
|
|
@@ -13,13 +13,20 @@
|
|
|
13
13
|
* 1. Auto-discovery — build the intersection table from selectors
|
|
14
14
|
* (rows="user-story" cols="user-persona" link-attr="persona-id")
|
|
15
15
|
* instead of forcing the author to hand-build every cell.
|
|
16
|
-
* 2. Chain highlight —
|
|
17
|
-
* `data-state-highlighted` on the row AND
|
|
18
|
-
* page-wide, so authors can see the
|
|
19
|
-
* components (cards, timelines, etc.).
|
|
16
|
+
* 2. Chain highlight — activating a cell (click, or Enter/Space on the
|
|
17
|
+
* cell's button) toggles `data-state-highlighted` on the row AND
|
|
18
|
+
* column source elements page-wide, so authors can see the
|
|
19
|
+
* connection in surrounding components (cards, timelines, etc.).
|
|
20
|
+
* The highlight is cleared when the matrix is removed.
|
|
21
|
+
*
|
|
22
|
+
* Column identifiers: by default a column element is matched by its
|
|
23
|
+
* `id`, `story-id` (<user-story>) or `item-id` (<work-item>), so
|
|
24
|
+
* `rows="work-item" cols="user-story" link-attr="story-ids"` works even
|
|
25
|
+
* before (or without) the stories setting their DOM id. Set `match-attr`
|
|
26
|
+
* to match on one specific attribute instead.
|
|
20
27
|
*
|
|
21
28
|
* Cell semantics:
|
|
22
|
-
* ✓ — row's link-attr references this column's
|
|
29
|
+
* ✓ — row's link-attr references this column's identifier
|
|
23
30
|
* (empty) — no link
|
|
24
31
|
*
|
|
25
32
|
* `link-attr` values are split on comma so attributes like
|
|
@@ -32,7 +39,8 @@
|
|
|
32
39
|
*
|
|
33
40
|
* @attr {string} rows - CSS selector for row source elements
|
|
34
41
|
* @attr {string} cols - CSS selector for column source elements
|
|
35
|
-
* @attr {string} link-attr - Attribute on the row element whose value(s) reference column
|
|
42
|
+
* @attr {string} link-attr - Attribute on the row element whose value(s) reference column identifiers
|
|
43
|
+
* @attr {string} match-attr - Attribute on column elements to match against (default: id, story-id, item-id)
|
|
36
44
|
* @attr {string} label - Optional heading shown above the table
|
|
37
45
|
* @attr {string} row-label - Header cell text for the row axis (default: "")
|
|
38
46
|
* @attr {string} cell-mark - Glyph shown in linked cells (default: "✓")
|
|
@@ -45,6 +53,9 @@
|
|
|
45
53
|
import { registerComponent } from '../../lib/bundle-registry.js';
|
|
46
54
|
import { VBElement } from '../../lib/vb-element.js';
|
|
47
55
|
|
|
56
|
+
/** Column identity attributes tried, in order, when `match-attr` is unset. */
|
|
57
|
+
const DEFAULT_MATCH_ATTRS = ['id', 'story-id', 'item-id'];
|
|
58
|
+
|
|
48
59
|
class TraceabilityMatrix extends VBElement {
|
|
49
60
|
setup() {
|
|
50
61
|
const rowsSel = this.getAttribute('rows');
|
|
@@ -57,10 +68,29 @@ class TraceabilityMatrix extends VBElement {
|
|
|
57
68
|
|
|
58
69
|
/* Defer one microtask so any sibling components (e.g. user-persona)
|
|
59
70
|
have finished assigning ids and slot content before we read them. */
|
|
60
|
-
queueMicrotask(() =>
|
|
71
|
+
queueMicrotask(() => {
|
|
72
|
+
if (this.isConnected) this.#build(rowsSel, colsSel, linkAttr);
|
|
73
|
+
});
|
|
61
74
|
return true;
|
|
62
75
|
}
|
|
63
76
|
|
|
77
|
+
teardown() {
|
|
78
|
+
/* Don't leave page-wide highlight on source elements once the matrix
|
|
79
|
+
that set it is gone. */
|
|
80
|
+
this.#clearHighlight();
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Identifiers a column element answers to.
|
|
85
|
+
* @param {Element} el
|
|
86
|
+
* @returns {string[]}
|
|
87
|
+
*/
|
|
88
|
+
#keysOf(el) {
|
|
89
|
+
const matchAttr = this.getAttribute('match-attr');
|
|
90
|
+
const attrs = matchAttr ? [matchAttr] : DEFAULT_MATCH_ATTRS;
|
|
91
|
+
return attrs.map((a) => (el.getAttribute(a) || '').trim()).filter(Boolean);
|
|
92
|
+
}
|
|
93
|
+
|
|
64
94
|
#build(rowsSel, colsSel, linkAttr) {
|
|
65
95
|
const rowEls = [...document.querySelectorAll(rowsSel)].filter((el) => el !== this && !this.contains(el));
|
|
66
96
|
const colEls = [...document.querySelectorAll(colsSel)].filter((el) => el !== this && !this.contains(el));
|
|
@@ -73,21 +103,16 @@ class TraceabilityMatrix extends VBElement {
|
|
|
73
103
|
const rowAxisLabel = this.getAttribute('row-label') || '';
|
|
74
104
|
const cellMark = this.getAttribute('cell-mark') || '✓';
|
|
75
105
|
const flagOrphans = this.hasAttribute('flag-orphans');
|
|
106
|
+
const colKeys = colEls.map((el) => new Set(this.#keysOf(el)));
|
|
76
107
|
|
|
77
108
|
/* Compute links eagerly so we can flag orphans + emit a count. */
|
|
78
|
-
const
|
|
79
|
-
const rowHits = new WeakMap();
|
|
80
|
-
const colHits = new WeakMap();
|
|
81
|
-
for (const rowEl of rowEls) {
|
|
109
|
+
const linked = rowEls.map((rowEl) => {
|
|
82
110
|
const refs = TraceabilityMatrix.#parseRefs(rowEl.getAttribute(linkAttr));
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
colHits.set(colEl, (colHits.get(colEl) || 0) + 1);
|
|
89
|
-
}
|
|
90
|
-
}
|
|
111
|
+
return colKeys.map((keys) => refs.some((r) => keys.has(r)));
|
|
112
|
+
});
|
|
113
|
+
const rowHits = linked.map((cells) => cells.filter(Boolean).length);
|
|
114
|
+
const colHits = colEls.map((_, j) => linked.filter((cells) => cells[j]).length);
|
|
115
|
+
const linkCount = rowHits.reduce((a, b) => a + b, 0);
|
|
91
116
|
|
|
92
117
|
/* Build the data-table markup: header row of column labels,
|
|
93
118
|
one body row per row source. */
|
|
@@ -101,50 +126,54 @@ class TraceabilityMatrix extends VBElement {
|
|
|
101
126
|
corner.textContent = rowAxisLabel;
|
|
102
127
|
headerRow.appendChild(corner);
|
|
103
128
|
|
|
104
|
-
|
|
129
|
+
const colLabels = colEls.map((el) => TraceabilityMatrix.#labelOf(el));
|
|
130
|
+
colEls.forEach((colEl, j) => {
|
|
105
131
|
const th = document.createElement('th');
|
|
106
132
|
th.setAttribute('data-sort', 'number');
|
|
107
|
-
th.textContent =
|
|
108
|
-
th.dataset.colId = colEl.id || '';
|
|
109
|
-
|
|
133
|
+
th.textContent = colLabels[j];
|
|
134
|
+
th.dataset.colId = colEl.id || this.#keysOf(colEl)[0] || '';
|
|
135
|
+
th.dataset.colIndex = String(j);
|
|
136
|
+
if (flagOrphans && !colHits[j]) th.dataset.orphan = '';
|
|
110
137
|
headerRow.appendChild(th);
|
|
111
|
-
}
|
|
138
|
+
});
|
|
112
139
|
|
|
113
140
|
thead.appendChild(headerRow);
|
|
114
141
|
table.appendChild(thead);
|
|
115
142
|
|
|
116
143
|
const tbody = document.createElement('tbody');
|
|
117
|
-
|
|
144
|
+
rowEls.forEach((rowEl, i) => {
|
|
118
145
|
const tr = document.createElement('tr');
|
|
119
146
|
tr.dataset.rowId = rowEl.id || '';
|
|
120
|
-
|
|
147
|
+
tr.dataset.rowIndex = String(i);
|
|
148
|
+
if (flagOrphans && !rowHits[i]) tr.dataset.orphan = '';
|
|
121
149
|
|
|
150
|
+
const rowLabel = TraceabilityMatrix.#labelOf(rowEl);
|
|
122
151
|
const labelCell = document.createElement('th');
|
|
123
152
|
labelCell.scope = 'row';
|
|
124
|
-
labelCell.
|
|
153
|
+
labelCell.appendChild(TraceabilityMatrix.#button(rowLabel, `Highlight ${rowLabel}`));
|
|
125
154
|
tr.appendChild(labelCell);
|
|
126
155
|
|
|
127
|
-
|
|
128
|
-
for (const colEl of colEls) {
|
|
156
|
+
colEls.forEach((colEl, j) => {
|
|
129
157
|
const td = document.createElement('td');
|
|
130
|
-
const matched =
|
|
158
|
+
const matched = linked[i][j];
|
|
131
159
|
td.setAttribute('data-sort-value', matched ? '1' : '0');
|
|
132
|
-
if (matched)
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
160
|
+
if (matched) td.dataset.linked = '';
|
|
161
|
+
td.setAttribute('aria-label', matched ? 'linked' : 'not linked');
|
|
162
|
+
td.dataset.colId = colEl.id || this.#keysOf(colEl)[0] || '';
|
|
163
|
+
td.dataset.colIndex = String(j);
|
|
164
|
+
td.appendChild(TraceabilityMatrix.#button(
|
|
165
|
+
matched ? cellMark : '',
|
|
166
|
+
`${rowLabel} × ${colLabels[j]}: ${matched ? 'linked' : 'not linked'}`,
|
|
167
|
+
));
|
|
140
168
|
tr.appendChild(td);
|
|
141
|
-
}
|
|
169
|
+
});
|
|
142
170
|
tbody.appendChild(tr);
|
|
143
|
-
}
|
|
171
|
+
});
|
|
144
172
|
|
|
145
173
|
table.appendChild(tbody);
|
|
146
174
|
wrapper.appendChild(table);
|
|
147
175
|
|
|
176
|
+
this.#clearHighlight();
|
|
148
177
|
[...this.children].forEach((c) => { if (c.tagName !== 'TEMPLATE') c.remove(); });
|
|
149
178
|
|
|
150
179
|
const label = this.getAttribute('label');
|
|
@@ -157,29 +186,44 @@ class TraceabilityMatrix extends VBElement {
|
|
|
157
186
|
this.appendChild(wrapper);
|
|
158
187
|
|
|
159
188
|
/* Wire chain-highlight clicks via event delegation on the wrapper.
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
this.#
|
|
189
|
+
Each cell holds a <button>, so Enter/Space activate it too. Source
|
|
190
|
+
elements are keyed by position, so rows/cols without an id still
|
|
191
|
+
highlight. */
|
|
192
|
+
this.#rowEls = rowEls;
|
|
193
|
+
this.#colEls = colEls;
|
|
164
194
|
this.listen(wrapper, 'click', this.#onCellClick);
|
|
165
195
|
|
|
166
196
|
queueMicrotask(() => {
|
|
167
197
|
this.dispatchEvent(new CustomEvent('traceability-matrix:ready', {
|
|
168
198
|
bubbles: true,
|
|
169
|
-
detail: { rowCount: rowEls.length, colCount: colEls.length, linkCount
|
|
199
|
+
detail: { rowCount: rowEls.length, colCount: colEls.length, linkCount },
|
|
170
200
|
}));
|
|
171
201
|
});
|
|
172
202
|
}
|
|
173
203
|
|
|
204
|
+
/**
|
|
205
|
+
* @param {string} text visible text
|
|
206
|
+
* @param {string} name accessible name
|
|
207
|
+
*/
|
|
208
|
+
static #button(text, name) {
|
|
209
|
+
const btn = document.createElement('button');
|
|
210
|
+
btn.type = 'button';
|
|
211
|
+
btn.className = 'tm-cell-btn';
|
|
212
|
+
btn.textContent = text;
|
|
213
|
+
btn.setAttribute('aria-label', name);
|
|
214
|
+
btn.setAttribute('aria-pressed', 'false');
|
|
215
|
+
return btn;
|
|
216
|
+
}
|
|
217
|
+
|
|
174
218
|
#onCellClick = (e) => {
|
|
175
219
|
const cell = e.target.closest('td, th[scope="row"]');
|
|
176
220
|
if (!cell) return;
|
|
177
221
|
const tr = cell.closest('tr');
|
|
178
222
|
if (!tr) return;
|
|
179
|
-
const
|
|
180
|
-
const
|
|
181
|
-
const rowEl =
|
|
182
|
-
const colEl =
|
|
223
|
+
const rowIdx = tr.dataset.rowIndex;
|
|
224
|
+
const colIdx = cell.dataset.colIndex;
|
|
225
|
+
const rowEl = rowIdx !== undefined ? this.#rowEls[Number(rowIdx)] ?? null : null;
|
|
226
|
+
const colEl = colIdx !== undefined ? this.#colEls[Number(colIdx)] ?? null : null;
|
|
183
227
|
|
|
184
228
|
/* Toggle: if this cell is already highlighted, clear; else
|
|
185
229
|
set fresh highlight on row + col source elements. */
|
|
@@ -192,10 +236,15 @@ class TraceabilityMatrix extends VBElement {
|
|
|
192
236
|
return;
|
|
193
237
|
}
|
|
194
238
|
cell.setAttribute('data-state-highlighted', '');
|
|
195
|
-
|
|
196
|
-
if (colEl && colEl !== rowEl) colEl.setAttribute('data-state-highlighted', '');
|
|
239
|
+
cell.querySelector('.tm-cell-btn')?.setAttribute('aria-pressed', 'true');
|
|
197
240
|
/* Highlight the entire row in the matrix for visual scan. */
|
|
198
241
|
tr.setAttribute('data-state-highlighted', '');
|
|
242
|
+
for (const el of [rowEl, colEl]) {
|
|
243
|
+
if (el && !this.#highlighted.includes(el)) {
|
|
244
|
+
el.setAttribute('data-state-highlighted', '');
|
|
245
|
+
this.#highlighted.push(el);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
199
248
|
|
|
200
249
|
this.dispatchEvent(new CustomEvent('traceability-matrix:highlight', {
|
|
201
250
|
bubbles: true, detail: { rowEl, colEl, on: true },
|
|
@@ -203,14 +252,14 @@ class TraceabilityMatrix extends VBElement {
|
|
|
203
252
|
};
|
|
204
253
|
|
|
205
254
|
#clearHighlight() {
|
|
206
|
-
|
|
207
|
-
this
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
});
|
|
255
|
+
this.querySelectorAll('[data-state-highlighted]').forEach((el) => el.removeAttribute('data-state-highlighted'));
|
|
256
|
+
this.querySelectorAll('.tm-cell-btn[aria-pressed="true"]').forEach((el) => el.setAttribute('aria-pressed', 'false'));
|
|
257
|
+
for (const el of this.#highlighted) el.removeAttribute('data-state-highlighted');
|
|
258
|
+
this.#highlighted = [];
|
|
211
259
|
}
|
|
212
260
|
|
|
213
261
|
#renderEmpty() {
|
|
262
|
+
this.#clearHighlight();
|
|
214
263
|
[...this.children].forEach((c) => { if (c.tagName !== 'TEMPLATE') c.remove(); });
|
|
215
264
|
const note = document.createElement('p');
|
|
216
265
|
note.className = 'tm-empty';
|
|
@@ -230,12 +279,12 @@ class TraceabilityMatrix extends VBElement {
|
|
|
230
279
|
|| '(unnamed)';
|
|
231
280
|
}
|
|
232
281
|
|
|
233
|
-
/** @type {
|
|
234
|
-
#
|
|
235
|
-
/** @type {
|
|
236
|
-
#
|
|
237
|
-
/** @type {
|
|
238
|
-
#
|
|
282
|
+
/** @type {Element[]} */
|
|
283
|
+
#rowEls = [];
|
|
284
|
+
/** @type {Element[]} */
|
|
285
|
+
#colEls = [];
|
|
286
|
+
/** Source elements this matrix highlighted (cleared on re-highlight and teardown). @type {Element[]} */
|
|
287
|
+
#highlighted = [];
|
|
239
288
|
}
|
|
240
289
|
|
|
241
290
|
registerComponent('traceability-matrix', TraceabilityMatrix);
|
|
@@ -11,6 +11,14 @@ traceability-matrix {
|
|
|
11
11
|
}
|
|
12
12
|
|
|
13
13
|
@scope (traceability-matrix) {
|
|
14
|
+
/* Wide matrices scroll inside their own box instead of pushing the page
|
|
15
|
+
sideways on narrow screens. */
|
|
16
|
+
:scope > data-table {
|
|
17
|
+
display: block;
|
|
18
|
+
max-inline-size: 100%;
|
|
19
|
+
overflow-x: auto;
|
|
20
|
+
}
|
|
21
|
+
|
|
14
22
|
.tm-label {
|
|
15
23
|
display: block;
|
|
16
24
|
font-size: var(--font-size-xs, 0.75rem);
|
|
@@ -73,9 +81,22 @@ traceability-matrix {
|
|
|
73
81
|
cursor: default;
|
|
74
82
|
}
|
|
75
83
|
|
|
76
|
-
|
|
84
|
+
/* Each body cell and row header holds a button so chain highlight is
|
|
85
|
+
keyboard operable (Enter / Space). Reset it to look like cell text. */
|
|
86
|
+
.tm-cell-btn {
|
|
87
|
+
all: unset;
|
|
88
|
+
box-sizing: border-box;
|
|
89
|
+
display: block;
|
|
90
|
+
inline-size: 100%;
|
|
91
|
+
min-block-size: 1.5em;
|
|
92
|
+
text-align: inherit;
|
|
77
93
|
cursor: pointer;
|
|
78
94
|
}
|
|
95
|
+
|
|
96
|
+
.tm-cell-btn:focus-visible {
|
|
97
|
+
outline: 2px solid var(--color-accent, oklch(45% 0.18 260));
|
|
98
|
+
outline-offset: 2px;
|
|
99
|
+
}
|
|
79
100
|
}
|
|
80
101
|
|
|
81
102
|
/* Source-element highlight when the matrix triggers a chain */
|
|
@@ -10,47 +10,33 @@
|
|
|
10
10
|
]
|
|
11
11
|
},
|
|
12
12
|
"attributes": [
|
|
13
|
-
{
|
|
14
|
-
"name": "title",
|
|
15
|
-
"kind": "host-api",
|
|
16
|
-
"purpose": "config",
|
|
17
|
-
"type": "string",
|
|
18
|
-
"description": "Journey map title"
|
|
19
|
-
},
|
|
20
13
|
{
|
|
21
14
|
"name": "persona",
|
|
22
15
|
"kind": "host-api",
|
|
23
16
|
"purpose": "config",
|
|
24
17
|
"type": "string",
|
|
25
|
-
"description": "Persona name"
|
|
18
|
+
"description": "Persona name shown in the header"
|
|
26
19
|
},
|
|
27
20
|
{
|
|
28
21
|
"name": "persona-id",
|
|
29
22
|
"kind": "host-api",
|
|
30
23
|
"purpose": "config",
|
|
31
24
|
"type": "string",
|
|
32
|
-
"description": "Links to a user-persona element by id"
|
|
33
|
-
},
|
|
34
|
-
{
|
|
35
|
-
"name": "summary",
|
|
36
|
-
"kind": "host-api",
|
|
37
|
-
"purpose": "config",
|
|
38
|
-
"type": "string",
|
|
39
|
-
"description": "One-line journey description"
|
|
25
|
+
"description": "Links the persona name to a user-persona element by id"
|
|
40
26
|
},
|
|
41
27
|
{
|
|
42
28
|
"name": "story-ids",
|
|
43
29
|
"kind": "host-api",
|
|
44
30
|
"purpose": "config",
|
|
45
31
|
"type": "string",
|
|
46
|
-
"description": "Comma-separated related story IDs"
|
|
32
|
+
"description": "Comma-separated related story IDs, rendered as fragment links"
|
|
47
33
|
},
|
|
48
34
|
{
|
|
49
35
|
"name": "src",
|
|
50
36
|
"kind": "host-api",
|
|
51
37
|
"purpose": "config",
|
|
52
38
|
"type": "string",
|
|
53
|
-
"description": "URL
|
|
39
|
+
"description": "URL of a JSON journey record (title, persona, personaId, summary, phases). Fetched once per value; a newer value aborts the older request."
|
|
54
40
|
},
|
|
55
41
|
{
|
|
56
42
|
"name": "compact",
|
|
@@ -63,22 +49,37 @@
|
|
|
63
49
|
"slots": [
|
|
64
50
|
{
|
|
65
51
|
"name": "title",
|
|
66
|
-
"description": "Journey
|
|
52
|
+
"description": "Journey heading (e.g. <h2>). Defaults to \"User Journey\"; also labels the phase table."
|
|
67
53
|
},
|
|
68
54
|
{
|
|
69
|
-
"name": "
|
|
70
|
-
"description": "
|
|
55
|
+
"name": "summary",
|
|
56
|
+
"description": "One-line description (e.g. <p>)"
|
|
71
57
|
},
|
|
72
58
|
{
|
|
73
|
-
"name": "
|
|
74
|
-
"description": "
|
|
59
|
+
"name": "persona",
|
|
60
|
+
"description": "Not projected: the text of a slot=\"persona\" child is read as the persona name when the persona attribute is absent"
|
|
75
61
|
}
|
|
76
62
|
],
|
|
77
63
|
"events": [
|
|
78
64
|
{
|
|
79
65
|
"name": "journey-ready",
|
|
80
66
|
"detail": "{ title, persona, phaseCount }",
|
|
81
|
-
"description": "
|
|
67
|
+
"description": "Once per element, after the first render. With src, after the fetched data renders (not fired if the fetch fails). Not re-fired by later renders or moves."
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
"name": "user-journey:upgraded",
|
|
71
|
+
"detail": "",
|
|
72
|
+
"description": "Once, in a microtask after first connect. Wait for this before assigning .data or .phases from a framework."
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
"name": "user-journey:data-changed",
|
|
76
|
+
"detail": "{ data, source: 'property' }",
|
|
77
|
+
"description": "After .data is assigned"
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
"name": "user-journey:phases-changed",
|
|
81
|
+
"detail": "{ phases, source: 'property' }",
|
|
82
|
+
"description": "After .phases is assigned a different array"
|
|
82
83
|
}
|
|
83
84
|
]
|
|
84
85
|
}
|