@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.
Files changed (66) hide show
  1. package/README.md +10 -5
  2. package/dist/vb-project-planning.css +69 -3
  3. package/dist/vb-project-planning.js +3191 -2544
  4. package/dist/vb-project-planning.min.css +1 -1
  5. package/dist/vb-project-planning.min.js +221 -197
  6. package/package.json +7 -4
  7. package/src/lib/board-surface.js +158 -0
  8. package/src/lib/bundle-registry.js +2 -75
  9. package/src/lib/dates.js +125 -0
  10. package/src/lib/escape.js +34 -0
  11. package/src/lib/hash.js +16 -0
  12. package/src/lib/portrait-url.js +3 -29
  13. package/src/lib/src-loader.js +78 -0
  14. package/src/lib/vb-view-transition.js +7 -1
  15. package/src/utils/copy-init.js +56 -17
  16. package/src/web-components/_ux-base.js +409 -24
  17. package/src/web-components/adr-wc/README.md +18 -16
  18. package/src/web-components/adr-wc/api.json +7 -5
  19. package/src/web-components/adr-wc/logic.js +50 -136
  20. package/src/web-components/burndown-chart/api.json +8 -6
  21. package/src/web-components/burndown-chart/logic.js +75 -39
  22. package/src/web-components/empathy-map/api.json +13 -13
  23. package/src/web-components/empathy-map/logic.js +161 -168
  24. package/src/web-components/empathy-map/static.html +3 -3
  25. package/src/web-components/empathy-map/styles.js +1 -0
  26. package/src/web-components/gantt-chart/README.md +12 -6
  27. package/src/web-components/gantt-chart/api.json +23 -7
  28. package/src/web-components/gantt-chart/logic.js +456 -431
  29. package/src/web-components/gantt-chart/styles.css +14 -1
  30. package/src/web-components/impact-effort/api.json +9 -7
  31. package/src/web-components/impact-effort/logic.js +238 -209
  32. package/src/web-components/impact-effort/static.html +12 -4
  33. package/src/web-components/iron-triangle/_capacity.js +0 -18
  34. package/src/web-components/kanban-board/api.json +8 -6
  35. package/src/web-components/kanban-board/logic.js +289 -333
  36. package/src/web-components/product-roadmap/api.json +14 -13
  37. package/src/web-components/product-roadmap/logic.js +393 -246
  38. package/src/web-components/product-roadmap/styles.css +40 -0
  39. package/src/web-components/quadrant-grid/api.json +9 -4
  40. package/src/web-components/quadrant-grid/logic.js +80 -12
  41. package/src/web-components/quadrant-grid/styles.css +7 -0
  42. package/src/web-components/quality-target/_quality-utils.js +2 -46
  43. package/src/web-components/review-surface/README.md +9 -5
  44. package/src/web-components/review-surface/api.json +10 -8
  45. package/src/web-components/review-surface/logic.js +349 -218
  46. package/src/web-components/risk-register/api.json +1 -6
  47. package/src/web-components/risk-register/logic.js +71 -24
  48. package/src/web-components/risk-register/styles.css +9 -1
  49. package/src/web-components/story-map/api.json +8 -6
  50. package/src/web-components/story-map/logic.js +280 -281
  51. package/src/web-components/story-map/static.html +15 -5
  52. package/src/web-components/traceability-matrix/api.json +10 -3
  53. package/src/web-components/traceability-matrix/logic.js +112 -63
  54. package/src/web-components/traceability-matrix/styles.css +22 -1
  55. package/src/web-components/user-journey/api.json +25 -24
  56. package/src/web-components/user-journey/logic.js +76 -127
  57. package/src/web-components/user-journey/static.html +2 -2
  58. package/src/web-components/user-persona/api.json +15 -14
  59. package/src/web-components/user-persona/logic.js +71 -117
  60. package/src/web-components/user-persona/styles.js +1 -0
  61. package/src/web-components/user-story/api.json +13 -11
  62. package/src/web-components/user-story/logic.js +164 -195
  63. package/src/web-components/user-story/styles.js +28 -1
  64. package/src/web-components/work-item/README.md +3 -3
  65. package/src/web-components/work-item/api.json +13 -11
  66. 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" persona="Researcher" action="interview target users"
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" persona="PM" action="review analytics data"
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" persona="Designer" action="create wireframes"
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" persona="Developer" action="implement API endpoints"
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" persona="QA" action="run usability testing"
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 ids; comma-separated values supported"
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: \u2713)"
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 id. Surfaces
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 — clicking a cell toggles
17
- * `data-state-highlighted` on the row AND column source elements
18
- * page-wide, so authors can see the connection in surrounding
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 id
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 ids
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(() => this.#build(rowsSel, colsSel, linkAttr));
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 links = []; // [{ rowEl, colEl }]
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
- for (const ref of refs) {
84
- const colEl = colEls.find((c) => c.id === ref);
85
- if (!colEl) continue;
86
- links.push({ rowEl, colEl });
87
- rowHits.set(rowEl, (rowHits.get(rowEl) || 0) + 1);
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
- for (const colEl of colEls) {
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 = TraceabilityMatrix.#labelOf(colEl);
108
- th.dataset.colId = colEl.id || '';
109
- if (flagOrphans && !colHits.get(colEl)) th.dataset.orphan = '';
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
- for (const rowEl of rowEls) {
144
+ rowEls.forEach((rowEl, i) => {
118
145
  const tr = document.createElement('tr');
119
146
  tr.dataset.rowId = rowEl.id || '';
120
- if (flagOrphans && !rowHits.get(rowEl)) tr.dataset.orphan = '';
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.textContent = TraceabilityMatrix.#labelOf(rowEl);
153
+ labelCell.appendChild(TraceabilityMatrix.#button(rowLabel, `Highlight ${rowLabel}`));
125
154
  tr.appendChild(labelCell);
126
155
 
127
- const refs = TraceabilityMatrix.#parseRefs(rowEl.getAttribute(linkAttr));
128
- for (const colEl of colEls) {
156
+ colEls.forEach((colEl, j) => {
129
157
  const td = document.createElement('td');
130
- const matched = refs.includes(colEl.id);
158
+ const matched = linked[i][j];
131
159
  td.setAttribute('data-sort-value', matched ? '1' : '0');
132
- if (matched) {
133
- td.dataset.linked = '';
134
- td.textContent = cellMark;
135
- td.setAttribute('aria-label', 'linked');
136
- } else {
137
- td.setAttribute('aria-label', 'not linked');
138
- }
139
- td.dataset.colId = colEl.id || '';
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
- Any <td>/<th> click highlights its row + column source elements. */
161
- this.#wrapper = wrapper;
162
- this.#rowsByDom = new Map(rowEls.map((el) => [el.id, el]));
163
- this.#colsByDom = new Map(colEls.map((el) => [el.id, el]));
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: links.length },
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 rowId = tr.dataset.rowId;
180
- const colId = cell.dataset.colId;
181
- const rowEl = rowId ? this.#rowsByDom.get(rowId) : null;
182
- const colEl = colId ? this.#colsByDom.get(colId) : null;
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
- if (rowEl) rowEl.setAttribute('data-state-highlighted', '');
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
- if (!this.#wrapper) return;
207
- this.#wrapper.querySelectorAll('[data-state-highlighted]').forEach((el) => el.removeAttribute('data-state-highlighted'));
208
- document.querySelectorAll('[data-state-highlighted]').forEach((el) => {
209
- if (this.#rowsByDom.has(el.id) || this.#colsByDom.has(el.id)) el.removeAttribute('data-state-highlighted');
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 {HTMLElement | null} */
234
- #wrapper = null;
235
- /** @type {Map<string, Element>} */
236
- #rowsByDom = new Map();
237
- /** @type {Map<string, Element>} */
238
- #colsByDom = new Map();
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
- td[data-linked] {
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 to JSON file containing phase data"
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 title (slot fallback for attribute)"
52
+ "description": "Journey heading (e.g. <h2>). Defaults to \"User Journey\"; also labels the phase table."
67
53
  },
68
54
  {
69
- "name": "persona",
70
- "description": "Persona name (slot fallback for attribute)"
55
+ "name": "summary",
56
+ "description": "One-line description (e.g. <p>)"
71
57
  },
72
58
  {
73
- "name": "summary",
74
- "description": "One-line description (slot fallback for attribute)"
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": "Fired after render completes"
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
  }