@profpowell/vb-project-planning 0.1.0 → 0.2.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/README.md +27 -5
- package/dist/vb-project-planning.css +48 -5
- package/dist/vb-project-planning.js +165 -12
- package/dist/vb-project-planning.min.css +1 -1
- package/dist/vb-project-planning.min.js +44 -44
- package/package.json +5 -4
- package/src/web-components/iron-triangle/api.json +1 -1
- package/src/web-components/iron-triangle/logic.js +157 -0
- package/src/web-components/iron-triangle/styles.css +60 -3
- package/src/web-components/product-roadmap/logic.js +25 -13
- package/src/web-components/requirement-card/logic.js +1 -1
- package/src/web-components/user-persona/logic.js +1 -1
- package/src/web-components/user-story/logic.js +2 -1
package/package.json
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@profpowell/vb-project-planning",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Project- and UX-planning Web Components for Vanilla Breeze: personas, stories, journeys, empathy maps, impact/effort and quadrant grids, risk and traceability matrices, burndown, roadmap, kanban, story map, gantt, review surface, work items, ADRs, iron triangle, capacity plans, quality targets, and requirement cards.",
|
|
5
5
|
"license": "MIT",
|
|
6
|
-
"author": "
|
|
6
|
+
"author": "Thomas A. Powell",
|
|
7
7
|
"type": "module",
|
|
8
8
|
"homepage": "https://github.com/ProfPowell/vb-project-planning#readme",
|
|
9
9
|
"repository": {
|
|
@@ -63,10 +63,11 @@
|
|
|
63
63
|
"build:js": "esbuild src/index.js --bundle --format=esm --outfile=dist/vb-project-planning.js && esbuild src/index.js --bundle --format=esm --minify --outfile=dist/vb-project-planning.min.js",
|
|
64
64
|
"build:css": "esbuild src/index.css --bundle --outfile=dist/vb-project-planning.css && esbuild src/index.css --bundle --minify --outfile=dist/vb-project-planning.min.css",
|
|
65
65
|
"prepublishOnly": "npm run build",
|
|
66
|
-
"test:components": "playwright test"
|
|
66
|
+
"test:components": "playwright test",
|
|
67
|
+
"test": "playwright test"
|
|
67
68
|
},
|
|
68
69
|
"peerDependencies": {
|
|
69
|
-
"vanilla-breeze": ">=0.
|
|
70
|
+
"vanilla-breeze": ">=0.3.0"
|
|
70
71
|
},
|
|
71
72
|
"peerDependenciesMeta": {
|
|
72
73
|
"vanilla-breeze": {
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"$schema": "../../../schemas/api.schema.json",
|
|
3
3
|
"element": "iron-triangle",
|
|
4
4
|
"type": "web-component",
|
|
5
|
-
"description": "Triangle-as-UI project-shape constraint surface. Three vertex hit-targets (Scope/Time/Cost) open native <dialog> editors; the center 'Quality' target fires iron-triangle:open-quality with a data-quality-href fallback. Computes capacityPoints from sprintWeeks × sprintCount × teamFTE × focusFactor. Form-associated.",
|
|
5
|
+
"description": "Triangle-as-UI project-shape constraint surface. Three vertex hit-targets (Scope/Time/Cost) open native <dialog> editors; the center 'Quality' target fires iron-triangle:open-quality with a data-quality-href fallback. Computes capacityPoints from sprintWeeks × sprintCount × teamFTE × focusFactor. Form-associated. Renders a status strip below the SVG: a visually-hidden <output aria-live=polite> capacity readout, a <button aria-pressed> formula/manual switch that opens a manual-capacity dialog, and a visible 'Deadline has passed' warning.",
|
|
6
6
|
"htmlvalidate": {
|
|
7
7
|
"flow": true,
|
|
8
8
|
"permittedContent": ["@flow"]
|
|
@@ -16,6 +16,14 @@
|
|
|
16
16
|
* markup needed in the host page. The component is form-associated;
|
|
17
17
|
* the value serializes as JSON via setFormValue().
|
|
18
18
|
*
|
|
19
|
+
* Accessibility hooks (rendered below the SVG in `.iron-triangle-status`):
|
|
20
|
+
* - an `<output aria-live="polite">` capacity readout (visually hidden)
|
|
21
|
+
* that announces capacity, mode, and any deadline warning;
|
|
22
|
+
* - the formula / manual switch as a real `<button aria-pressed>` that
|
|
23
|
+
* opens a manual-capacity dialog;
|
|
24
|
+
* - a visible "Deadline has passed" warning so the over-deadline state
|
|
25
|
+
* is never colour-only.
|
|
26
|
+
*
|
|
19
27
|
* Optional: pass a `data-quality-summary` attribute or set the
|
|
20
28
|
* `qualitySummary` property to surface the bound NFR compass's
|
|
21
29
|
* decision on hover (e.g. "3 critical: perf, sec, a11y").
|
|
@@ -88,6 +96,12 @@ class IronTriangle extends VBElement {
|
|
|
88
96
|
#qualitySummary = '';
|
|
89
97
|
/** @type {Record<string, HTMLDialogElement>} */
|
|
90
98
|
#dialogs = {};
|
|
99
|
+
/** @type {HTMLOutputElement | null} */
|
|
100
|
+
#readout = null;
|
|
101
|
+
/** @type {HTMLButtonElement | null} */
|
|
102
|
+
#modeButton = null;
|
|
103
|
+
/** @type {HTMLElement | null} */
|
|
104
|
+
#deadlineWarning = null;
|
|
91
105
|
|
|
92
106
|
constructor() {
|
|
93
107
|
super();
|
|
@@ -100,7 +114,9 @@ class IronTriangle extends VBElement {
|
|
|
100
114
|
this.#qualitySummary = this.getAttribute('data-quality-summary') || '';
|
|
101
115
|
// Seed initial value from FIELD_DEFS so the formula has something to chew on.
|
|
102
116
|
this.#seedDefaults();
|
|
117
|
+
this.#renderStatus();
|
|
103
118
|
this.#renderViz();
|
|
119
|
+
this.#syncStatus();
|
|
104
120
|
|
|
105
121
|
this.listen(this, 'click', (e) => this.#onClick(e));
|
|
106
122
|
this.listen(this, 'keydown', (e) => this.#onKeydown(e));
|
|
@@ -363,6 +379,7 @@ class IronTriangle extends VBElement {
|
|
|
363
379
|
|
|
364
380
|
this.#renderViz();
|
|
365
381
|
this.#syncStateFlags();
|
|
382
|
+
this.#syncStatus();
|
|
366
383
|
this.#publishFormValue();
|
|
367
384
|
|
|
368
385
|
this.dispatchEvent(new CustomEvent('iron-triangle:change', {
|
|
@@ -407,11 +424,151 @@ class IronTriangle extends VBElement {
|
|
|
407
424
|
|
|
408
425
|
#syncDisabledLocked() {
|
|
409
426
|
const off = this.hasAttribute('disabled') || this.hasAttribute('locked');
|
|
427
|
+
if (this.#modeButton) this.#modeButton.disabled = off;
|
|
410
428
|
for (const dialog of Object.values(this.#dialogs)) {
|
|
411
429
|
for (const el of dialog.querySelectorAll('input, select, textarea, button')) /** @type {any} */ (el).disabled = off;
|
|
412
430
|
}
|
|
413
431
|
}
|
|
414
432
|
|
|
433
|
+
// ── Internal: status strip (a11y hooks) ──────────────────────────
|
|
434
|
+
|
|
435
|
+
/**
|
|
436
|
+
* Builds the status strip once: live readout, mode toggle, deadline
|
|
437
|
+
* warning. Content is refreshed by #syncStatus() on every recompute.
|
|
438
|
+
*/
|
|
439
|
+
#renderStatus() {
|
|
440
|
+
if (this.querySelector(':scope > .iron-triangle-status')) return;
|
|
441
|
+
const strip = document.createElement('div');
|
|
442
|
+
strip.className = 'iron-triangle-status';
|
|
443
|
+
|
|
444
|
+
const readout = document.createElement('output');
|
|
445
|
+
readout.className = 'iron-triangle-readout';
|
|
446
|
+
readout.setAttribute('aria-live', 'polite');
|
|
447
|
+
readout.setAttribute('aria-atomic', 'true');
|
|
448
|
+
|
|
449
|
+
const button = document.createElement('button');
|
|
450
|
+
button.type = 'button';
|
|
451
|
+
button.className = 'iron-triangle-mode';
|
|
452
|
+
button.setAttribute('aria-pressed', 'false');
|
|
453
|
+
button.textContent = 'Manual capacity';
|
|
454
|
+
button.addEventListener('click', () => this.#openCapacityDialog());
|
|
455
|
+
|
|
456
|
+
const warning = document.createElement('p');
|
|
457
|
+
warning.className = 'iron-triangle-warning';
|
|
458
|
+
warning.setAttribute('data-warning', 'over-deadline');
|
|
459
|
+
warning.hidden = true;
|
|
460
|
+
|
|
461
|
+
strip.append(readout, button, warning);
|
|
462
|
+
const svg = this.querySelector(':scope > svg');
|
|
463
|
+
if (svg) svg.after(strip);
|
|
464
|
+
else this.prepend(strip);
|
|
465
|
+
|
|
466
|
+
this.#readout = readout;
|
|
467
|
+
this.#modeButton = button;
|
|
468
|
+
this.#deadlineWarning = warning;
|
|
469
|
+
this.#syncDisabledLocked();
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
#syncStatus() {
|
|
473
|
+
if (!this.#readout) return;
|
|
474
|
+
const { capacityPoints, capacitySource } = this.#value;
|
|
475
|
+
const manual = capacitySource === 'manual';
|
|
476
|
+
const deadline = this.#value.time?.deadline;
|
|
477
|
+
const overDeadline = this.matches(':state(over-deadline)');
|
|
478
|
+
|
|
479
|
+
const parts = [];
|
|
480
|
+
parts.push(capacityPoints > 0
|
|
481
|
+
? `Capacity: ${capacityPoints} points (${manual ? 'manual' : 'formula'}).`
|
|
482
|
+
: 'Capacity: not yet budgeted.');
|
|
483
|
+
if (overDeadline) parts.push(`Deadline has passed (${deadline}).`);
|
|
484
|
+
const text = parts.join(' ');
|
|
485
|
+
if (this.#readout.textContent !== text) this.#readout.textContent = text;
|
|
486
|
+
|
|
487
|
+
if (this.#modeButton) {
|
|
488
|
+
this.#modeButton.setAttribute('aria-pressed', manual ? 'true' : 'false');
|
|
489
|
+
this.#modeButton.setAttribute('aria-label',
|
|
490
|
+
manual ? `Manual capacity: ${capacityPoints} points. Activate to edit or return to the formula.`
|
|
491
|
+
: 'Manual capacity. Activate to override the formula with a fixed number.');
|
|
492
|
+
}
|
|
493
|
+
if (this.#deadlineWarning) {
|
|
494
|
+
this.#deadlineWarning.textContent = overDeadline ? `Deadline has passed (${deadline})` : '';
|
|
495
|
+
this.#deadlineWarning.hidden = !overDeadline;
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
#openCapacityDialog() {
|
|
500
|
+
const dialog = this.#ensureCapacityDialog();
|
|
501
|
+
const input = /** @type {HTMLInputElement} */ (dialog.querySelector('[name="capacity.points"]'));
|
|
502
|
+
input.value = String(this.#value.capacityPoints > 0 ? this.#value.capacityPoints : 1);
|
|
503
|
+
const useFormula = /** @type {HTMLButtonElement} */ (dialog.querySelector('button[value="formula"]'));
|
|
504
|
+
useFormula.hidden = this.#value.capacitySource !== 'manual';
|
|
505
|
+
if (typeof dialog.showModal === 'function') dialog.showModal();
|
|
506
|
+
else dialog.setAttribute('open', '');
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
#ensureCapacityDialog() {
|
|
510
|
+
if (this.#dialogs.capacity) return this.#dialogs.capacity;
|
|
511
|
+
const dialog = document.createElement('dialog');
|
|
512
|
+
dialog.className = 'iron-triangle-dialog iron-triangle-dialog--capacity';
|
|
513
|
+
dialog.setAttribute('aria-label', 'Capacity — manual override');
|
|
514
|
+
|
|
515
|
+
const form = document.createElement('form');
|
|
516
|
+
form.method = 'dialog';
|
|
517
|
+
form.setAttribute('data-layout', 'stack');
|
|
518
|
+
form.setAttribute('data-layout-gap', 'm');
|
|
519
|
+
|
|
520
|
+
const heading = document.createElement('h3');
|
|
521
|
+
heading.textContent = 'Manual capacity';
|
|
522
|
+
form.append(heading);
|
|
523
|
+
|
|
524
|
+
const hint = document.createElement('p');
|
|
525
|
+
hint.className = 'iron-triangle-dialog__hint';
|
|
526
|
+
hint.textContent = 'Fix the capacity to a number the team committed to. The Time / Cost / Scope inputs stay saved but stop driving the readout.';
|
|
527
|
+
form.append(hint);
|
|
528
|
+
|
|
529
|
+
form.append(buildFormField('capacity', {
|
|
530
|
+
key: 'points', label: 'Capacity (points)', type: 'number', min: 1, step: 1, width: '6rem',
|
|
531
|
+
}, this.#value.capacityPoints));
|
|
532
|
+
|
|
533
|
+
const actions = document.createElement('div');
|
|
534
|
+
actions.setAttribute('data-layout', 'cluster');
|
|
535
|
+
actions.setAttribute('data-layout-gap', 's');
|
|
536
|
+
actions.setAttribute('data-layout-justify', 'end');
|
|
537
|
+
const useFormula = document.createElement('button');
|
|
538
|
+
useFormula.type = 'button';
|
|
539
|
+
useFormula.value = 'formula';
|
|
540
|
+
useFormula.textContent = 'Use formula';
|
|
541
|
+
useFormula.addEventListener('click', () => {
|
|
542
|
+
dialog.close('formula');
|
|
543
|
+
this.setFormula();
|
|
544
|
+
});
|
|
545
|
+
const cancel = document.createElement('button');
|
|
546
|
+
cancel.type = 'button';
|
|
547
|
+
cancel.value = 'cancel';
|
|
548
|
+
cancel.textContent = 'Cancel';
|
|
549
|
+
cancel.addEventListener('click', () => dialog.close('cancel'));
|
|
550
|
+
const save = document.createElement('button');
|
|
551
|
+
save.type = 'submit';
|
|
552
|
+
save.value = 'save';
|
|
553
|
+
save.textContent = 'Save';
|
|
554
|
+
actions.append(useFormula, cancel, save);
|
|
555
|
+
form.append(actions);
|
|
556
|
+
|
|
557
|
+
dialog.append(form);
|
|
558
|
+
this.append(dialog);
|
|
559
|
+
this.#dialogs.capacity = dialog;
|
|
560
|
+
|
|
561
|
+
form.addEventListener('submit', (e) => {
|
|
562
|
+
e.preventDefault();
|
|
563
|
+
const input = /** @type {HTMLInputElement} */ (form.querySelector('[name="capacity.points"]'));
|
|
564
|
+
const n = Number(input.value);
|
|
565
|
+
dialog.close('save');
|
|
566
|
+
if (Number.isFinite(n) && n > 0) this.setManual(n);
|
|
567
|
+
});
|
|
568
|
+
|
|
569
|
+
return dialog;
|
|
570
|
+
}
|
|
571
|
+
|
|
415
572
|
// ── Internal: revise() field accessors ───────────────────────────
|
|
416
573
|
|
|
417
574
|
#getField(path) {
|
|
@@ -124,11 +124,68 @@ iron-triangle svg .center:focus-visible .quality-label {
|
|
|
124
124
|
fill: var(--color-interactive);
|
|
125
125
|
}
|
|
126
126
|
|
|
127
|
+
/* ── Status strip: live readout, mode toggle, deadline warning ─── */
|
|
128
|
+
|
|
129
|
+
iron-triangle > .iron-triangle-status {
|
|
130
|
+
display: flex;
|
|
131
|
+
flex-wrap: wrap;
|
|
132
|
+
align-items: center;
|
|
133
|
+
justify-content: center;
|
|
134
|
+
gap: var(--size-s, 0.75rem);
|
|
135
|
+
margin-block-start: var(--size-xs, 0.5rem);
|
|
136
|
+
font-size: var(--font-size-sm, 0.875rem);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/* The SVG already shows the number; the <output> exists for assistive
|
|
140
|
+
tech (aria-live) and stays visually hidden. */
|
|
141
|
+
iron-triangle > .iron-triangle-status .iron-triangle-readout {
|
|
142
|
+
position: absolute;
|
|
143
|
+
inline-size: 1px;
|
|
144
|
+
block-size: 1px;
|
|
145
|
+
margin: -1px;
|
|
146
|
+
padding: 0;
|
|
147
|
+
overflow: hidden;
|
|
148
|
+
clip-path: inset(50%);
|
|
149
|
+
white-space: nowrap;
|
|
150
|
+
border: 0;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
iron-triangle > .iron-triangle-status .iron-triangle-mode {
|
|
154
|
+
font-size: inherit;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
iron-triangle > .iron-triangle-status .iron-triangle-mode[aria-pressed="true"] {
|
|
158
|
+
border-color: var(--color-interactive);
|
|
159
|
+
color: var(--color-interactive);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/* Visible, text-bearing warning — never colour-only. */
|
|
163
|
+
iron-triangle > .iron-triangle-status .iron-triangle-warning {
|
|
164
|
+
margin: 0;
|
|
165
|
+
padding: var(--size-2xs, 0.25rem) var(--size-xs, 0.5rem);
|
|
166
|
+
border-radius: var(--radius-s, 0.25rem);
|
|
167
|
+
border: var(--border-width-thin, 1px) solid var(--iron-triangle-warning-color, var(--color-warning));
|
|
168
|
+
color: var(--iron-triangle-warning-color, var(--color-warning));
|
|
169
|
+
font-weight: var(--font-weight-semibold, 600);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
iron-triangle > .iron-triangle-status .iron-triangle-warning::before {
|
|
173
|
+
content: "\26A0\FE0E "; /* ⚠ text presentation */
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/* VB's button rules set display; keep the hidden attribute authoritative. */
|
|
177
|
+
iron-triangle > dialog.iron-triangle-dialog [hidden] { display: none; }
|
|
178
|
+
|
|
179
|
+
iron-triangle > dialog.iron-triangle-dialog .iron-triangle-dialog__hint {
|
|
180
|
+
margin: 0;
|
|
181
|
+
color: var(--color-text-muted);
|
|
182
|
+
font-size: var(--font-size-sm, 0.875rem);
|
|
183
|
+
}
|
|
184
|
+
|
|
127
185
|
/* ── State hooks ──────────────────────────────────────────────── */
|
|
128
186
|
|
|
129
|
-
iron-triangle:state(over-deadline) svg .triangle { stroke: var(--color-warning); }
|
|
130
|
-
iron-triangle:state(unbudgeted) svg .capacity { fill: var(--color-warning); }
|
|
131
|
-
iron-triangle:state(manual) svg .quality-label::after { content: " (manual)"; }
|
|
187
|
+
iron-triangle:state(over-deadline) svg .triangle { stroke: var(--iron-triangle-warning-color, var(--color-warning)); }
|
|
188
|
+
iron-triangle:state(unbudgeted) svg .capacity { fill: var(--iron-triangle-warning-color, var(--color-warning)); }
|
|
132
189
|
|
|
133
190
|
iron-triangle[disabled],
|
|
134
191
|
iron-triangle[locked] {
|
|
@@ -37,7 +37,28 @@ import { VBElement } from '../../lib/vb-element.js';
|
|
|
37
37
|
const MS_PER_DAY = 86_400_000;
|
|
38
38
|
|
|
39
39
|
class Roadmap extends VBElement {
|
|
40
|
+
static get observedAttributes() {
|
|
41
|
+
return ['start', 'end', 'view', 'editable', 'today-marker'];
|
|
42
|
+
}
|
|
43
|
+
|
|
40
44
|
setup() {
|
|
45
|
+
if (!this.#readConfig()) return false;
|
|
46
|
+
|
|
47
|
+
/* Snapshot original lane sections (auto-discovery) before we wipe
|
|
48
|
+
the host. We do not modify the original elements until/unless
|
|
49
|
+
drag interactions change their data-* attributes. */
|
|
50
|
+
const lanes = [...this.querySelectorAll(':scope > section[data-lane]')];
|
|
51
|
+
if (!lanes.length) {
|
|
52
|
+
console.warn('product-roadmap: no <section data-lane> children found');
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
this.#sourceLanes = lanes;
|
|
56
|
+
this.#render();
|
|
57
|
+
return true;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Parse start/end/view/editable from attributes. Returns false when invalid. */
|
|
61
|
+
#readConfig() {
|
|
41
62
|
const startAttr = this.getAttribute('start');
|
|
42
63
|
const endAttr = this.getAttribute('end');
|
|
43
64
|
if (!startAttr || !endAttr) {
|
|
@@ -55,22 +76,13 @@ class Roadmap extends VBElement {
|
|
|
55
76
|
this.#totalMs = end.getTime() - start.getTime();
|
|
56
77
|
this.#view = (this.getAttribute('view') === 'month') ? 'month' : 'quarter';
|
|
57
78
|
this.#editable = this.hasAttribute('editable');
|
|
58
|
-
|
|
59
|
-
/* Snapshot original lane sections (auto-discovery) before we wipe
|
|
60
|
-
the host. We do not modify the original elements until/unless
|
|
61
|
-
drag interactions change their data-* attributes. */
|
|
62
|
-
const lanes = [...this.querySelectorAll(':scope > section[data-lane]')];
|
|
63
|
-
if (!lanes.length) {
|
|
64
|
-
console.warn('product-roadmap: no <section data-lane> children found');
|
|
65
|
-
return false;
|
|
66
|
-
}
|
|
67
|
-
this.#sourceLanes = lanes;
|
|
68
|
-
this.#render();
|
|
69
79
|
return true;
|
|
70
80
|
}
|
|
71
81
|
|
|
72
|
-
attributeChangedCallback() {
|
|
73
|
-
if (
|
|
82
|
+
attributeChangedCallback(_name, oldValue, newValue) {
|
|
83
|
+
if (oldValue === newValue) return;
|
|
84
|
+
if (!this.isConnected || !this.#sourceLanes.length) return;
|
|
85
|
+
if (this.#readConfig()) this.#render();
|
|
74
86
|
}
|
|
75
87
|
|
|
76
88
|
#render() {
|
|
@@ -125,7 +125,7 @@ class RequirementCard extends VBElement {
|
|
|
125
125
|
}
|
|
126
126
|
|
|
127
127
|
#syncSlot(name, slot) {
|
|
128
|
-
const hasContent = !!slot && slot.assignedNodes(
|
|
128
|
+
const hasContent = !!slot && slot.assignedNodes().some(n =>
|
|
129
129
|
n.nodeType === Node.ELEMENT_NODE ||
|
|
130
130
|
(n.nodeType === Node.TEXT_NODE && n.textContent.trim().length > 0)
|
|
131
131
|
);
|
|
@@ -105,7 +105,7 @@ class UserPersona extends HTMLElement {
|
|
|
105
105
|
if (!this.id) return [];
|
|
106
106
|
const root = /** @type {ParentNode} */ (this.getRootNode());
|
|
107
107
|
const scope = /** @type {ParentNode} */ (/** @type {any} */ (root).querySelectorAll ? root : document);
|
|
108
|
-
return Array.from(scope.querySelectorAll(`user-story[persona-id="${cssEscape(this.id)}"]`));
|
|
108
|
+
return Array.from(scope.querySelectorAll(`user-story[persona-id="${cssEscape(this.id)}"]:not([data-detail-clone])`));
|
|
109
109
|
}
|
|
110
110
|
|
|
111
111
|
#cacheSlotValues() {
|
|
@@ -210,7 +210,7 @@ class UserStory extends HTMLElement {
|
|
|
210
210
|
|
|
211
211
|
connectedCallback() {
|
|
212
212
|
this.#cacheSlotValues();
|
|
213
|
-
if (this.storyId && !this.id) this.id = this.storyId;
|
|
213
|
+
if (this.storyId && !this.id && !this.hasAttribute('data-detail-clone')) this.id = this.storyId;
|
|
214
214
|
if (this.hasAttribute('src')) {
|
|
215
215
|
this._loadSrc(this.getAttribute('src'));
|
|
216
216
|
}
|
|
@@ -410,6 +410,7 @@ class UserStory extends HTMLElement {
|
|
|
410
410
|
const hasSlottedContent = [...this.children].some(c => c.getAttribute('slot') && c.tagName !== 'DIALOG');
|
|
411
411
|
full.setAttribute('detail', hasSlottedContent ? 'full' : 'compact');
|
|
412
412
|
full.removeAttribute('id');
|
|
413
|
+
full.setAttribute('data-detail-clone', ''); // keep the clone out of id/persona lookups
|
|
413
414
|
for (const child of [...this.children]) {
|
|
414
415
|
if (child.tagName === 'DIALOG') continue;
|
|
415
416
|
full.appendChild(child.cloneNode(true));
|