@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,445 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* story-map: Horizontal user story map with activity columns
|
|
3
|
+
*
|
|
4
|
+
* Light DOM component that creates a horizontal story map with activity
|
|
5
|
+
* columns, each containing a <drag-surface> for cross-column drag-and-drop.
|
|
6
|
+
*
|
|
7
|
+
* @attr {string} src - URL to JSON data
|
|
8
|
+
* @attr {string} title - Optional heading displayed above the map
|
|
9
|
+
* @attr {boolean} compact - Reduced spacing variant
|
|
10
|
+
*
|
|
11
|
+
* Children use:
|
|
12
|
+
* @attr {string} data-activity - Activity column identifier (on <section>)
|
|
13
|
+
* @attr {string} data-activity-label - Display label for the column header (on <section>)
|
|
14
|
+
* @attr {string} data-journey-phase - Optional link to a user-journey phase (on <section>)
|
|
15
|
+
*
|
|
16
|
+
* @fires story-map:reorder - Item reordered within a column
|
|
17
|
+
* @fires story-map:transfer - Item moved between columns
|
|
18
|
+
* @fires story-map:ready - Fired after component initializes
|
|
19
|
+
*
|
|
20
|
+
* @example
|
|
21
|
+
* <story-map>
|
|
22
|
+
* <section data-activity="discovery" data-activity-label="Discovery">
|
|
23
|
+
* <article draggable="true" data-id="PROJ-1">Interview users</article>
|
|
24
|
+
* </section>
|
|
25
|
+
* </story-map>
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
import { registerComponent } from '../../lib/bundle-registry.js';
|
|
29
|
+
import { VBElement } from '../../lib/vb-element.js';
|
|
30
|
+
import { viewTransitionSwap } from '../../lib/vb-view-transition.js';
|
|
31
|
+
|
|
32
|
+
class StoryMap extends VBElement {
|
|
33
|
+
static get observedAttributes() { return ['src', 'compact', 'title']; }
|
|
34
|
+
|
|
35
|
+
static #instanceCount = 0;
|
|
36
|
+
|
|
37
|
+
/** @type {HTMLDivElement | null} */
|
|
38
|
+
#scroll = null;
|
|
39
|
+
/** @type {HTMLDivElement | null} */
|
|
40
|
+
#columnsEl = null;
|
|
41
|
+
/** @type {Record<string, HTMLElement>} */
|
|
42
|
+
#surfaces = {};
|
|
43
|
+
/** @type {HTMLDivElement | null} */
|
|
44
|
+
#liveRegion = null;
|
|
45
|
+
/** @type {string} */
|
|
46
|
+
#groupId = '';
|
|
47
|
+
/** @type {Array<{ id: string, label: string, journeyPhase: string | null, children: HTMLElement[] }>} */
|
|
48
|
+
#activities = [];
|
|
49
|
+
|
|
50
|
+
setup() {
|
|
51
|
+
this.#groupId = `sm-${++StoryMap.#instanceCount}`;
|
|
52
|
+
|
|
53
|
+
// 1. Snapshot <section data-activity> children
|
|
54
|
+
const sections = /** @type {HTMLElement[]} */ ([
|
|
55
|
+
...this.querySelectorAll(':scope > section[data-activity]')
|
|
56
|
+
]);
|
|
57
|
+
|
|
58
|
+
this.#activities = sections.map(section => {
|
|
59
|
+
const id = section.getAttribute('data-activity') || '';
|
|
60
|
+
const rawLabel = section.getAttribute('data-activity-label') || '';
|
|
61
|
+
const label = rawLabel || this.#titleCase(id);
|
|
62
|
+
const journeyPhase = section.getAttribute('data-journey-phase') || null;
|
|
63
|
+
const children = /** @type {HTMLElement[]} */ ([...section.children]);
|
|
64
|
+
return { id, label, journeyPhase, children };
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
// 2. Remove original sections (children already captured)
|
|
68
|
+
for (const section of sections) {
|
|
69
|
+
section.remove();
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// 3. Build scroll container
|
|
73
|
+
this.#scroll = document.createElement('div');
|
|
74
|
+
this.#scroll.className = 'sm-scroll';
|
|
75
|
+
this.#scroll.setAttribute('role', 'region');
|
|
76
|
+
this.#scroll.setAttribute('aria-label', 'Story map');
|
|
77
|
+
this.#scroll.setAttribute('tabindex', '0');
|
|
78
|
+
|
|
79
|
+
// 4. Build columns container
|
|
80
|
+
this.#columnsEl = document.createElement('div');
|
|
81
|
+
this.#columnsEl.className = 'sm-columns';
|
|
82
|
+
|
|
83
|
+
// 5. Build each activity column
|
|
84
|
+
let totalStories = 0;
|
|
85
|
+
|
|
86
|
+
for (const activity of this.#activities) {
|
|
87
|
+
const column = document.createElement('section');
|
|
88
|
+
column.className = 'sm-column';
|
|
89
|
+
column.setAttribute('data-activity-column', activity.id);
|
|
90
|
+
|
|
91
|
+
// Header with count badge
|
|
92
|
+
const header = document.createElement('header');
|
|
93
|
+
header.className = 'sm-activity-header';
|
|
94
|
+
const h3 = document.createElement('h3');
|
|
95
|
+
h3.textContent = activity.label;
|
|
96
|
+
const countBadge = document.createElement('span');
|
|
97
|
+
countBadge.className = 'sm-activity-count';
|
|
98
|
+
countBadge.textContent = String(activity.children.length);
|
|
99
|
+
h3.appendChild(countBadge);
|
|
100
|
+
header.appendChild(h3);
|
|
101
|
+
column.appendChild(header);
|
|
102
|
+
|
|
103
|
+
// Drag surface
|
|
104
|
+
const surface = document.createElement('drag-surface');
|
|
105
|
+
surface.setAttribute('group', this.#groupId);
|
|
106
|
+
surface.setAttribute('aria-label', `${activity.label} stories`);
|
|
107
|
+
surface.setAttribute('data-layout', 'stack');
|
|
108
|
+
surface.setAttribute('data-layout-gap', 'xs');
|
|
109
|
+
|
|
110
|
+
// Move captured children into the drag-surface
|
|
111
|
+
if (activity.children.length > 0) {
|
|
112
|
+
for (const child of activity.children) {
|
|
113
|
+
surface.appendChild(child);
|
|
114
|
+
}
|
|
115
|
+
totalStories += activity.children.length;
|
|
116
|
+
} else {
|
|
117
|
+
const placeholder = document.createElement('p');
|
|
118
|
+
placeholder.className = 'sm-empty';
|
|
119
|
+
placeholder.textContent = 'No stories yet';
|
|
120
|
+
surface.appendChild(placeholder);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
column.appendChild(surface);
|
|
124
|
+
this.#columnsEl.appendChild(column);
|
|
125
|
+
this.#surfaces[activity.id] = surface;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// 6. Assemble: title + scroll > columns, append to this
|
|
129
|
+
const title = this.getAttribute('title');
|
|
130
|
+
if (title) {
|
|
131
|
+
const heading = document.createElement('h2');
|
|
132
|
+
heading.className = 'sm-title';
|
|
133
|
+
heading.textContent = title;
|
|
134
|
+
this.appendChild(heading);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
this.#scroll.appendChild(this.#columnsEl);
|
|
138
|
+
this.appendChild(this.#scroll);
|
|
139
|
+
|
|
140
|
+
// 7. Create live region
|
|
141
|
+
this.#liveRegion = document.createElement('div');
|
|
142
|
+
this.#liveRegion.className = 'sm-live-region';
|
|
143
|
+
this.#liveRegion.setAttribute('role', 'status');
|
|
144
|
+
this.#liveRegion.setAttribute('aria-live', 'polite');
|
|
145
|
+
this.#liveRegion.setAttribute('aria-atomic', 'true');
|
|
146
|
+
this.appendChild(this.#liveRegion);
|
|
147
|
+
|
|
148
|
+
// 8. Listen for drag-surface:reorder
|
|
149
|
+
this.listen(this, 'drag-surface:reorder', (e) => {
|
|
150
|
+
const detail = /** @type {CustomEvent} */ (e).detail;
|
|
151
|
+
const surface = /** @type {HTMLElement | null} */ (/** @type {HTMLElement} */ (e.target).closest('drag-surface'));
|
|
152
|
+
const activityId = this.#findActivityForSurface(surface);
|
|
153
|
+
if (!activityId) return;
|
|
154
|
+
|
|
155
|
+
this.#updateCount(activityId);
|
|
156
|
+
|
|
157
|
+
this.dispatchEvent(new CustomEvent('story-map:reorder', {
|
|
158
|
+
bubbles: true,
|
|
159
|
+
detail: {
|
|
160
|
+
itemId: detail.itemId,
|
|
161
|
+
activity: activityId,
|
|
162
|
+
oldIndex: detail.oldIndex,
|
|
163
|
+
newIndex: detail.newIndex,
|
|
164
|
+
},
|
|
165
|
+
}));
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
// 9. Listen for drag-surface:transfer
|
|
169
|
+
this.listen(this, 'drag-surface:transfer', (e) => {
|
|
170
|
+
const detail = /** @type {CustomEvent} */ (e).detail;
|
|
171
|
+
const fromSurface = detail.fromSurface;
|
|
172
|
+
const toSurface = detail.toSurface;
|
|
173
|
+
const fromActivity = this.#findActivityForSurface(fromSurface);
|
|
174
|
+
const toActivity = this.#findActivityForSurface(toSurface);
|
|
175
|
+
if (!fromActivity || !toActivity) return;
|
|
176
|
+
|
|
177
|
+
// Update count badges
|
|
178
|
+
this.#updateCount(fromActivity);
|
|
179
|
+
this.#updateCount(toActivity);
|
|
180
|
+
|
|
181
|
+
// Remove placeholder from receiving column if it had one
|
|
182
|
+
const toPlaceholder = toSurface.querySelector('.sm-empty');
|
|
183
|
+
if (toPlaceholder) toPlaceholder.remove();
|
|
184
|
+
|
|
185
|
+
// Add placeholder to sending column if now empty
|
|
186
|
+
const fromDraggables = fromSurface.querySelectorAll(':scope > [draggable="true"]');
|
|
187
|
+
if (fromDraggables.length === 0 && !fromSurface.querySelector('.sm-empty')) {
|
|
188
|
+
const placeholder = document.createElement('p');
|
|
189
|
+
placeholder.className = 'sm-empty';
|
|
190
|
+
placeholder.textContent = 'No stories yet';
|
|
191
|
+
fromSurface.appendChild(placeholder);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// Announce via live region
|
|
195
|
+
const itemLabel = this.#itemLabel(detail.item);
|
|
196
|
+
this.#announce(`${itemLabel} moved from ${fromActivity} to ${toActivity}`);
|
|
197
|
+
|
|
198
|
+
this.dispatchEvent(new CustomEvent('story-map:transfer', {
|
|
199
|
+
bubbles: true,
|
|
200
|
+
detail: {
|
|
201
|
+
itemId: detail.itemId,
|
|
202
|
+
fromActivity,
|
|
203
|
+
toActivity,
|
|
204
|
+
newIndex: detail.newIndex,
|
|
205
|
+
},
|
|
206
|
+
}));
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
// 10. Handle src attribute for data mode
|
|
210
|
+
const src = this.getAttribute('src');
|
|
211
|
+
if (src) {
|
|
212
|
+
this._loadSrc(src);
|
|
213
|
+
return; // ready event fires after src loads
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// 11. Dispatch ready event
|
|
217
|
+
this.dispatchEvent(new CustomEvent('story-map:ready', {
|
|
218
|
+
bubbles: true,
|
|
219
|
+
detail: {
|
|
220
|
+
activityCount: this.#activities.length,
|
|
221
|
+
storyCount: totalStories,
|
|
222
|
+
},
|
|
223
|
+
}));
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
teardown() {
|
|
227
|
+
if (this.#scroll) {
|
|
228
|
+
this.#scroll.remove();
|
|
229
|
+
this.#scroll = null;
|
|
230
|
+
}
|
|
231
|
+
if (this.#liveRegion) {
|
|
232
|
+
this.#liveRegion.remove();
|
|
233
|
+
this.#liveRegion = null;
|
|
234
|
+
}
|
|
235
|
+
this.#columnsEl = null;
|
|
236
|
+
this.#surfaces = {};
|
|
237
|
+
this.#activities = [];
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// ── Data API (HTML-first / JS-first dual contract) ──────────────
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Read the current activities + stories as a nested data array.
|
|
244
|
+
* Each activity: `{ id, label, journeyPhase?, stories: [...] }`.
|
|
245
|
+
* Each story: `{ id?, title?, ... }` (passed through to renderItem).
|
|
246
|
+
*/
|
|
247
|
+
get activities() {
|
|
248
|
+
return this.#activities.map(a => ({
|
|
249
|
+
id: a.id,
|
|
250
|
+
label: a.label,
|
|
251
|
+
journeyPhase: a.journeyPhase || undefined,
|
|
252
|
+
stories: [...(this.#surfaces[a.id]?.querySelectorAll(':scope > [draggable="true"]') || [])]
|
|
253
|
+
.map(el => ({
|
|
254
|
+
id: el.getAttribute('data-id') || undefined,
|
|
255
|
+
storyId: el.getAttribute('story-id') || undefined,
|
|
256
|
+
title: el.querySelector('[slot="title"]')?.textContent?.trim() || undefined,
|
|
257
|
+
})),
|
|
258
|
+
}));
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Replace the entire story map. Accepts a nested tree:
|
|
263
|
+
* [{ id, label, journeyPhase?, stories: [{ id, ... }] }]
|
|
264
|
+
* Stories are rendered as <user-story> elements (via .data) by default;
|
|
265
|
+
* override via `.renderStory`.
|
|
266
|
+
*
|
|
267
|
+
* v1 is record-shaped — full rebuild on assignment. Per-story
|
|
268
|
+
* preservation across diffs is on the roadmap.
|
|
269
|
+
*
|
|
270
|
+
* Emits story-map:activities-changed { activities, source: 'property' }.
|
|
271
|
+
*/
|
|
272
|
+
set activities(value) {
|
|
273
|
+
const next = Array.isArray(value) ? value : [];
|
|
274
|
+
|
|
275
|
+
// Clear children, then synthesize the <section data-activity> markup
|
|
276
|
+
// that setup() expects. Re-run setup().
|
|
277
|
+
while (this.firstChild) this.firstChild.remove();
|
|
278
|
+
for (const a of next) {
|
|
279
|
+
const section = document.createElement('section');
|
|
280
|
+
section.setAttribute('data-activity', a.id || '');
|
|
281
|
+
if (a.label) section.setAttribute('data-activity-label', a.label);
|
|
282
|
+
if (a.journeyPhase) section.setAttribute('data-journey-phase', a.journeyPhase);
|
|
283
|
+
for (const story of (a.stories || [])) {
|
|
284
|
+
let el;
|
|
285
|
+
const self = /** @type {any} */ (this);
|
|
286
|
+
if (typeof self.renderStory === 'function') {
|
|
287
|
+
const out = self.renderStory(story);
|
|
288
|
+
el = out instanceof Element ? out : null;
|
|
289
|
+
}
|
|
290
|
+
if (!el) {
|
|
291
|
+
if (customElements.get('user-story')) {
|
|
292
|
+
el = document.createElement('user-story');
|
|
293
|
+
/** @type {any} */ (el).data = story;
|
|
294
|
+
} else {
|
|
295
|
+
el = document.createElement('article');
|
|
296
|
+
el.className = 'sm-card';
|
|
297
|
+
el.textContent = story.title || story.id || '';
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
if (!el.hasAttribute('draggable')) el.setAttribute('draggable', 'true');
|
|
301
|
+
if (story.id && !el.hasAttribute('data-id')) el.setAttribute('data-id', String(story.id));
|
|
302
|
+
section.appendChild(el);
|
|
303
|
+
}
|
|
304
|
+
this.appendChild(section);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
const swap = () => {
|
|
308
|
+
this.teardown();
|
|
309
|
+
this.removeAttribute('data-upgraded');
|
|
310
|
+
this.setup();
|
|
311
|
+
};
|
|
312
|
+
if (this.hasAttribute('data-upgraded') && this.#scroll) {
|
|
313
|
+
viewTransitionSwap(this, swap, 'sm-vt');
|
|
314
|
+
} else {
|
|
315
|
+
swap();
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
this.dispatchEvent(new CustomEvent('story-map:activities-changed', {
|
|
319
|
+
detail: { activities: next, source: 'property' },
|
|
320
|
+
bubbles: true,
|
|
321
|
+
}));
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// ── Attribute changes ────────────────────────────────
|
|
325
|
+
|
|
326
|
+
attributeChangedCallback(name, oldValue, newValue) {
|
|
327
|
+
if (oldValue === newValue || !this.isConnected) return;
|
|
328
|
+
if (name === 'src') this._loadSrc(newValue);
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// ── JSON loading ────────────────────────────────────
|
|
332
|
+
|
|
333
|
+
async _loadSrc(url) {
|
|
334
|
+
if (!url) return;
|
|
335
|
+
try {
|
|
336
|
+
const res = await fetch(url);
|
|
337
|
+
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
338
|
+
const data = await res.json();
|
|
339
|
+
// Clear existing children
|
|
340
|
+
while (this.firstChild) this.firstChild.remove();
|
|
341
|
+
// Create sections from activities
|
|
342
|
+
for (const activity of (data.activities || [])) {
|
|
343
|
+
const section = document.createElement('section');
|
|
344
|
+
section.setAttribute('data-activity', activity.id);
|
|
345
|
+
section.setAttribute('data-activity-label', activity.label || activity.id);
|
|
346
|
+
if (activity.journeyPhase) section.setAttribute('data-journey-phase', activity.journeyPhase);
|
|
347
|
+
for (const story of (activity.stories || [])) {
|
|
348
|
+
const el = document.createElement('user-story');
|
|
349
|
+
el.setAttribute('draggable', 'true');
|
|
350
|
+
el.dataset.id = story.id || story.storyId;
|
|
351
|
+
if (story.storyId || story.id) el.setAttribute('story-id', story.storyId || story.id);
|
|
352
|
+
if (story.title) el.setAttribute('title', story.title);
|
|
353
|
+
if (story.persona) el.setAttribute('persona', story.persona);
|
|
354
|
+
if (story.action) el.setAttribute('action', story.action);
|
|
355
|
+
if (story.benefit) el.setAttribute('benefit', story.benefit);
|
|
356
|
+
if (story.priority) el.setAttribute('priority', story.priority);
|
|
357
|
+
if (story.status) el.setAttribute('status', story.status);
|
|
358
|
+
if (story.points) el.setAttribute('points', String(story.points));
|
|
359
|
+
el.setAttribute('detail', story.detail || 'compact');
|
|
360
|
+
section.appendChild(el);
|
|
361
|
+
}
|
|
362
|
+
this.appendChild(section);
|
|
363
|
+
}
|
|
364
|
+
const swap = () => {
|
|
365
|
+
this.teardown();
|
|
366
|
+
this.removeAttribute('data-upgraded');
|
|
367
|
+
this.setup();
|
|
368
|
+
};
|
|
369
|
+
if (this.hasAttribute('data-upgraded') && this.#scroll) {
|
|
370
|
+
viewTransitionSwap(this, swap, 'sm-vt');
|
|
371
|
+
} else {
|
|
372
|
+
swap();
|
|
373
|
+
}
|
|
374
|
+
} catch (err) {
|
|
375
|
+
console.warn(`[story-map] Failed to load src="${url}":`, err);
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
// ── Helpers ──────────────────────────────────────────
|
|
380
|
+
|
|
381
|
+
/**
|
|
382
|
+
* Reverse-lookup: find the activity id that owns a given drag-surface.
|
|
383
|
+
* @param {HTMLElement | null} surface
|
|
384
|
+
* @returns {string | null}
|
|
385
|
+
*/
|
|
386
|
+
#findActivityForSurface(surface) {
|
|
387
|
+
if (!surface) return null;
|
|
388
|
+
for (const [id, ref] of Object.entries(this.#surfaces)) {
|
|
389
|
+
if (ref === surface) return id;
|
|
390
|
+
}
|
|
391
|
+
return null;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
/**
|
|
395
|
+
* Update the count badge in the column header for a given activity.
|
|
396
|
+
* @param {string} activityId
|
|
397
|
+
*/
|
|
398
|
+
#updateCount(activityId) {
|
|
399
|
+
const surface = this.#surfaces[activityId];
|
|
400
|
+
if (!surface) return;
|
|
401
|
+
const count = surface.querySelectorAll(':scope > [draggable="true"]').length;
|
|
402
|
+
const column = surface.closest('[data-activity-column]');
|
|
403
|
+
if (!column) return;
|
|
404
|
+
const badge = column.querySelector('.sm-activity-count');
|
|
405
|
+
if (badge) badge.textContent = String(count);
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
/**
|
|
409
|
+
* Announce a message via the live region.
|
|
410
|
+
* @param {string} msg
|
|
411
|
+
*/
|
|
412
|
+
#announce(msg) {
|
|
413
|
+
if (!this.#liveRegion) return;
|
|
414
|
+
this.#liveRegion.textContent = '';
|
|
415
|
+
requestAnimationFrame(() => {
|
|
416
|
+
if (this.#liveRegion) this.#liveRegion.textContent = msg;
|
|
417
|
+
});
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
/**
|
|
421
|
+
* Get a human-readable label for a story item.
|
|
422
|
+
* @param {HTMLElement} item
|
|
423
|
+
* @returns {string}
|
|
424
|
+
*/
|
|
425
|
+
#itemLabel(item) {
|
|
426
|
+
return item.getAttribute('story-id')
|
|
427
|
+
|| item.getAttribute('data-id')
|
|
428
|
+
|| item.textContent?.trim().slice(0, 40)
|
|
429
|
+
|| 'item';
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
/**
|
|
433
|
+
* Convert a hyphenated or lowercase id to Title Case.
|
|
434
|
+
* @param {string} str
|
|
435
|
+
* @returns {string}
|
|
436
|
+
*/
|
|
437
|
+
#titleCase(str) {
|
|
438
|
+
return str
|
|
439
|
+
.replace(/[-_]/g, ' ')
|
|
440
|
+
.replace(/\b\w/g, c => c.toUpperCase());
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
registerComponent('story-map', StoryMap);
|
|
445
|
+
export { StoryMap };
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
story-map — static (zero-JS) form
|
|
3
|
+
Activity sections with user-story children render as vertical flow before
|
|
4
|
+
JS upgrades to horizontal columns with drag-and-drop.
|
|
5
|
+
-->
|
|
6
|
+
<story-map>
|
|
7
|
+
<section data-activity="discovery" data-activity-label="Discovery">
|
|
8
|
+
<user-story draggable="true" data-id="PROJ-101"
|
|
9
|
+
story-id="PROJ-101" persona="Researcher" action="interview target users"
|
|
10
|
+
priority="high" status="done" points="5" detail="compact">
|
|
11
|
+
</user-story>
|
|
12
|
+
<user-story draggable="true" data-id="PROJ-102"
|
|
13
|
+
story-id="PROJ-102" persona="PM" action="review analytics data"
|
|
14
|
+
priority="medium" status="done" points="3" detail="compact">
|
|
15
|
+
</user-story>
|
|
16
|
+
</section>
|
|
17
|
+
<section data-activity="design" data-activity-label="Design">
|
|
18
|
+
<user-story draggable="true" data-id="PROJ-201"
|
|
19
|
+
story-id="PROJ-201" persona="Designer" action="create wireframes"
|
|
20
|
+
priority="high" status="in-progress" points="5" detail="compact">
|
|
21
|
+
</user-story>
|
|
22
|
+
</section>
|
|
23
|
+
<section data-activity="build" data-activity-label="Build">
|
|
24
|
+
<user-story draggable="true" data-id="PROJ-301"
|
|
25
|
+
story-id="PROJ-301" persona="Developer" action="implement API endpoints"
|
|
26
|
+
priority="high" status="to-do" points="8" detail="compact">
|
|
27
|
+
</user-story>
|
|
28
|
+
</section>
|
|
29
|
+
<section data-activity="validate" data-activity-label="Validate">
|
|
30
|
+
<user-story draggable="true" data-id="PROJ-401"
|
|
31
|
+
story-id="PROJ-401" persona="QA" action="run usability testing"
|
|
32
|
+
priority="high" status="backlog" points="5" detail="compact">
|
|
33
|
+
</user-story>
|
|
34
|
+
</section>
|
|
35
|
+
</story-map>
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
/* story-map — Light DOM styles (element selector) */
|
|
2
|
+
|
|
3
|
+
story-map {
|
|
4
|
+
display: block;
|
|
5
|
+
container-type: inline-size;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
story-map:not(:defined) {
|
|
9
|
+
display: block;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/* ── Scroll region ────────────────────────────────────── */
|
|
13
|
+
|
|
14
|
+
.sm-scroll {
|
|
15
|
+
overflow-x: auto;
|
|
16
|
+
overflow-y: visible;
|
|
17
|
+
-webkit-overflow-scrolling: touch;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/* ── Columns container ────────────────────────────────── */
|
|
21
|
+
|
|
22
|
+
.sm-columns {
|
|
23
|
+
display: flex;
|
|
24
|
+
gap: 1px;
|
|
25
|
+
background: var(--color-border, #e0e0e0);
|
|
26
|
+
min-inline-size: min-content;
|
|
27
|
+
border: 1px solid var(--color-border, #e0e0e0);
|
|
28
|
+
border-radius: var(--radius-l, 0.75rem);
|
|
29
|
+
overflow: hidden;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/* ── Individual column ────────────────────────────────── */
|
|
33
|
+
|
|
34
|
+
.sm-column {
|
|
35
|
+
min-inline-size: 14rem;
|
|
36
|
+
max-inline-size: 22rem;
|
|
37
|
+
flex: 1 0 14rem;
|
|
38
|
+
display: flex;
|
|
39
|
+
flex-direction: column;
|
|
40
|
+
background: var(--color-surface, #fff);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/* ── Activity header ──────────────────────────────────── */
|
|
44
|
+
|
|
45
|
+
.sm-activity-header {
|
|
46
|
+
position: sticky;
|
|
47
|
+
inset-block-start: 0;
|
|
48
|
+
z-index: 2;
|
|
49
|
+
padding: var(--size-s, 12px) var(--size-m, 16px);
|
|
50
|
+
background: var(--color-surface-raised, #f8f9fa);
|
|
51
|
+
border-block-end: 2px solid var(--color-border, #e0e0e0);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
.sm-activity-header h3 {
|
|
55
|
+
margin: 0;
|
|
56
|
+
font-size: 0.875rem;
|
|
57
|
+
font-weight: 700;
|
|
58
|
+
text-transform: uppercase;
|
|
59
|
+
letter-spacing: 0.04em;
|
|
60
|
+
color: var(--color-text, #1a1a1a);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/* ── Count badge ──────────────────────────────────────── */
|
|
64
|
+
|
|
65
|
+
.sm-activity-count {
|
|
66
|
+
font-size: 0.7rem;
|
|
67
|
+
font-weight: 600;
|
|
68
|
+
color: var(--color-text-muted, #666);
|
|
69
|
+
background: var(--color-border, #e0e0e0);
|
|
70
|
+
border-radius: var(--radius-full, 9999px);
|
|
71
|
+
padding: 1px 8px;
|
|
72
|
+
margin-inline-start: 8px;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/* ── Drag surfaces inside columns ─────────────────────── */
|
|
76
|
+
|
|
77
|
+
story-map drag-surface {
|
|
78
|
+
padding: var(--size-s, 12px);
|
|
79
|
+
min-block-size: 8rem;
|
|
80
|
+
flex: 1;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
story-map drag-surface > * {
|
|
84
|
+
margin-block-end: var(--size-xs, 8px);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
story-map drag-surface > *:last-child {
|
|
88
|
+
margin-block-end: 0;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/* ── Empty column placeholder ─────────────────────────── */
|
|
92
|
+
|
|
93
|
+
.sm-empty {
|
|
94
|
+
color: var(--color-text-muted, #888);
|
|
95
|
+
font-size: 0.8rem;
|
|
96
|
+
font-style: italic;
|
|
97
|
+
padding: var(--size-s, 12px);
|
|
98
|
+
margin: 0;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/* ── Live region (visually hidden) ────────────────────── */
|
|
102
|
+
|
|
103
|
+
.sm-live-region {
|
|
104
|
+
position: absolute;
|
|
105
|
+
width: 1px;
|
|
106
|
+
height: 1px;
|
|
107
|
+
overflow: hidden;
|
|
108
|
+
clip: rect(0, 0, 0, 0);
|
|
109
|
+
white-space: nowrap;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/* ── Compact variant ──────────────────────────────────── */
|
|
113
|
+
|
|
114
|
+
story-map[compact] .sm-column {
|
|
115
|
+
min-inline-size: 11rem;
|
|
116
|
+
flex: 1 0 11rem;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
story-map[compact] .sm-activity-header {
|
|
120
|
+
padding: var(--size-xs, 8px) var(--size-s, 12px);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
story-map[compact] .sm-activity-header h3 {
|
|
124
|
+
font-size: 0.75rem;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
story-map[compact] drag-surface {
|
|
128
|
+
padding: var(--size-xs, 8px);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/* ── Container query: single-column stacking ──────────── */
|
|
132
|
+
|
|
133
|
+
@container (max-width: 30rem) {
|
|
134
|
+
.sm-columns {
|
|
135
|
+
flex-direction: column;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
.sm-column {
|
|
139
|
+
max-inline-size: none;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/* ── Reduced motion ───────────────────────────────────── */
|
|
144
|
+
|
|
145
|
+
@media (prefers-reduced-motion: reduce) {
|
|
146
|
+
.sm-scroll {
|
|
147
|
+
scroll-behavior: auto;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/* ── Print ────────────────────────────────────────────── */
|
|
152
|
+
|
|
153
|
+
@media print {
|
|
154
|
+
.sm-scroll {
|
|
155
|
+
overflow: visible;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
.sm-activity-header {
|
|
159
|
+
position: static;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
.sm-columns {
|
|
163
|
+
flex-wrap: wrap;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
.sm-column {
|
|
167
|
+
break-inside: avoid;
|
|
168
|
+
}
|
|
169
|
+
}
|