@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.
Files changed (106) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +93 -0
  3. package/dist/vb-project-planning.css +1603 -0
  4. package/dist/vb-project-planning.js +11138 -0
  5. package/dist/vb-project-planning.min.css +1 -0
  6. package/dist/vb-project-planning.min.js +3312 -0
  7. package/package.json +80 -0
  8. package/src/index.css +14 -0
  9. package/src/index.js +26 -0
  10. package/src/lib/bundle-registry.js +156 -0
  11. package/src/lib/diff-by-key.js +86 -0
  12. package/src/lib/portrait-url.js +87 -0
  13. package/src/lib/vb-collection.js +122 -0
  14. package/src/lib/vb-element.js +96 -0
  15. package/src/lib/vb-store.js +200 -0
  16. package/src/lib/vb-view-transition.js +44 -0
  17. package/src/utils/copy-init.js +287 -0
  18. package/src/web-components/_ux-base.js +106 -0
  19. package/src/web-components/adr-wc/README.md +77 -0
  20. package/src/web-components/adr-wc/api.json +26 -0
  21. package/src/web-components/adr-wc/logic.js +376 -0
  22. package/src/web-components/adr-wc/static.html +17 -0
  23. package/src/web-components/adr-wc/styles.js +338 -0
  24. package/src/web-components/burndown-chart/api.json +74 -0
  25. package/src/web-components/burndown-chart/logic.js +198 -0
  26. package/src/web-components/burndown-chart/static.html +31 -0
  27. package/src/web-components/burndown-chart/styles.css +59 -0
  28. package/src/web-components/capacity-plan/_capacity-utils.js +56 -0
  29. package/src/web-components/capacity-plan/api.json +20 -0
  30. package/src/web-components/capacity-plan/logic.js +222 -0
  31. package/src/web-components/capacity-plan/static.html +24 -0
  32. package/src/web-components/capacity-plan/styles.css +84 -0
  33. package/src/web-components/empathy-map/api.json +31 -0
  34. package/src/web-components/empathy-map/logic.js +502 -0
  35. package/src/web-components/empathy-map/static.html +32 -0
  36. package/src/web-components/empathy-map/styles.js +534 -0
  37. package/src/web-components/gantt-chart/README.md +91 -0
  38. package/src/web-components/gantt-chart/api.json +20 -0
  39. package/src/web-components/gantt-chart/logic.js +812 -0
  40. package/src/web-components/gantt-chart/static.html +60 -0
  41. package/src/web-components/gantt-chart/styles.css +391 -0
  42. package/src/web-components/impact-effort/api.json +21 -0
  43. package/src/web-components/impact-effort/logic.js +392 -0
  44. package/src/web-components/impact-effort/static.html +23 -0
  45. package/src/web-components/impact-effort/styles.css +193 -0
  46. package/src/web-components/iron-triangle/_capacity.js +83 -0
  47. package/src/web-components/iron-triangle/_triangle-geometry.js +322 -0
  48. package/src/web-components/iron-triangle/api.json +26 -0
  49. package/src/web-components/iron-triangle/logic.js +474 -0
  50. package/src/web-components/iron-triangle/static.html +28 -0
  51. package/src/web-components/iron-triangle/styles.css +158 -0
  52. package/src/web-components/kanban-board/api.json +24 -0
  53. package/src/web-components/kanban-board/logic.js +698 -0
  54. package/src/web-components/kanban-board/static.html +20 -0
  55. package/src/web-components/kanban-board/styles.css +242 -0
  56. package/src/web-components/product-roadmap/api.json +118 -0
  57. package/src/web-components/product-roadmap/logic.js +460 -0
  58. package/src/web-components/product-roadmap/static.html +44 -0
  59. package/src/web-components/product-roadmap/styles.css +158 -0
  60. package/src/web-components/quadrant-grid/api.json +125 -0
  61. package/src/web-components/quadrant-grid/logic.js +397 -0
  62. package/src/web-components/quadrant-grid/static.html +49 -0
  63. package/src/web-components/quadrant-grid/styles.css +184 -0
  64. package/src/web-components/quality-target/_quality-utils.js +460 -0
  65. package/src/web-components/quality-target/api.json +30 -0
  66. package/src/web-components/quality-target/logic.js +516 -0
  67. package/src/web-components/quality-target/static.html +22 -0
  68. package/src/web-components/quality-target/styles.css +203 -0
  69. package/src/web-components/requirement-card/api.json +25 -0
  70. package/src/web-components/requirement-card/logic.js +166 -0
  71. package/src/web-components/requirement-card/static.html +36 -0
  72. package/src/web-components/requirement-card/styles.js +114 -0
  73. package/src/web-components/review-surface/README.md +178 -0
  74. package/src/web-components/review-surface/api.json +30 -0
  75. package/src/web-components/review-surface/logic.js +807 -0
  76. package/src/web-components/review-surface/static.html +33 -0
  77. package/src/web-components/review-surface/styles.js +459 -0
  78. package/src/web-components/risk-register/api.json +47 -0
  79. package/src/web-components/risk-register/logic.js +180 -0
  80. package/src/web-components/risk-register/static.html +46 -0
  81. package/src/web-components/risk-register/styles.css +37 -0
  82. package/src/web-components/story-map/api.json +22 -0
  83. package/src/web-components/story-map/logic.js +445 -0
  84. package/src/web-components/story-map/static.html +35 -0
  85. package/src/web-components/story-map/styles.css +169 -0
  86. package/src/web-components/traceability-matrix/api.json +84 -0
  87. package/src/web-components/traceability-matrix/logic.js +243 -0
  88. package/src/web-components/traceability-matrix/static.html +41 -0
  89. package/src/web-components/traceability-matrix/styles.css +94 -0
  90. package/src/web-components/user-journey/api.json +84 -0
  91. package/src/web-components/user-journey/logic.js +368 -0
  92. package/src/web-components/user-journey/static.html +14 -0
  93. package/src/web-components/user-journey/styles.js +317 -0
  94. package/src/web-components/user-persona/api.json +35 -0
  95. package/src/web-components/user-persona/logic.js +502 -0
  96. package/src/web-components/user-persona/static.html +20 -0
  97. package/src/web-components/user-persona/styles.js +299 -0
  98. package/src/web-components/user-story/api.json +35 -0
  99. package/src/web-components/user-story/logic.js +567 -0
  100. package/src/web-components/user-story/static.html +27 -0
  101. package/src/web-components/user-story/styles.js +332 -0
  102. package/src/web-components/work-item/README.md +85 -0
  103. package/src/web-components/work-item/api.json +30 -0
  104. package/src/web-components/work-item/logic.js +414 -0
  105. package/src/web-components/work-item/static.html +24 -0
  106. 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
+ }