@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,368 @@
1
+ /**
2
+ * <user-journey> — User journey map web component
3
+ *
4
+ * Visualises a multi-phase user journey with an SVG emotion curve
5
+ * and a structured phase-breakdown grid table.
6
+ *
7
+ * Data sources:
8
+ * 1. `src` attribute — URL to a JSON file containing phase data
9
+ * 2. `.phases` property — set programmatically
10
+ *
11
+ * JSON schema:
12
+ * {
13
+ * "title": "...",
14
+ * "persona": "...",
15
+ * "personaId": "...",
16
+ * "summary": "...",
17
+ * "phases": [
18
+ * {
19
+ * "name": "Awareness",
20
+ * "emotion": "curious",
21
+ * "storyIds": ["PROJ-142"],
22
+ * "actions": ["Searches for solutions"],
23
+ * "thoughts": ["There must be a better way"],
24
+ * "touchpoints": ["Google", "Hacker News"],
25
+ * "painPoints": ["Too many options to compare"],
26
+ * "opportunities": ["Clear landing page"]
27
+ * }
28
+ * ]
29
+ * }
30
+ */
31
+
32
+ import { styles } from './styles.js';
33
+ import { registerComponent } from '../../lib/bundle-registry.js';
34
+ import { viewTransitionSwap } from '../../lib/vb-view-transition.js';
35
+ import { esc, EMOTION_META, lucideSvg, UX_ICONS } from '../_ux-base.js';
36
+
37
+ const ROWS = [
38
+ { key: 'actions', label: 'Actions' },
39
+ { key: 'thoughts', label: 'Thoughts' },
40
+ { key: 'touchpoints', label: 'Touchpoints' },
41
+ { key: 'painPoints', label: 'Pain Points' },
42
+ { key: 'opportunities', label: 'Opportunities' },
43
+ ];
44
+
45
+ class UserJourney extends HTMLElement {
46
+ static get observedAttributes() {
47
+ return ['src', 'persona', 'persona-id', 'story-ids', 'compact'];
48
+ }
49
+
50
+ #slotCache = new Map();
51
+
52
+ constructor() {
53
+ super();
54
+ this.attachShadow({ mode: 'open' });
55
+ this.__phases = null;
56
+ }
57
+
58
+ /** @returns {Array|null} */
59
+ get phases() { return this.__phases; }
60
+
61
+ /**
62
+ * Replace the phase list. Idempotent (skip on same array reference).
63
+ * Emits user-journey:phases-changed { phases, source: 'property' }.
64
+ * @param {Array|null} data
65
+ */
66
+ set phases(data) {
67
+ if (this.__phases === data) return;
68
+ this.__phases = data;
69
+ if (this.isConnected) this._render();
70
+ this.dispatchEvent(new CustomEvent('user-journey:phases-changed', {
71
+ detail: { phases: data, source: 'property' },
72
+ bubbles: true, composed: true,
73
+ }));
74
+ }
75
+
76
+ /**
77
+ * Read the journey as a plain data object combining state, slotted
78
+ * content, and the phase array. Mirrors what a consumer would assign
79
+ * to .data.
80
+ */
81
+ get data() {
82
+ return {
83
+ persona: this.getAttribute('persona') || undefined,
84
+ personaId: this.getAttribute('persona-id') || undefined,
85
+ title: this.querySelector('[slot="title"]')?.textContent?.trim() || undefined,
86
+ summary: this.querySelector('[slot="summary"]')?.textContent?.trim() || undefined,
87
+ phases: this.__phases || undefined,
88
+ };
89
+ }
90
+
91
+ /**
92
+ * Set state attributes, slotted content, and phases in one assignment.
93
+ * Emits user-journey:data-changed { data, source: 'property' }.
94
+ */
95
+ set data(value) {
96
+ if (!value || typeof value !== 'object') return;
97
+ if (value.persona) this.setAttribute('persona', String(value.persona));
98
+ if (value.personaId) this.setAttribute('persona-id', String(value.personaId));
99
+ if (value.title && !this.querySelector('[slot="title"]')) {
100
+ const el = document.createElement('h2');
101
+ el.slot = 'title';
102
+ el.textContent = value.title;
103
+ this.appendChild(el);
104
+ }
105
+ if (value.summary && !this.querySelector('[slot="summary"]')) {
106
+ const el = document.createElement('p');
107
+ el.slot = 'summary';
108
+ el.textContent = value.summary;
109
+ this.appendChild(el);
110
+ }
111
+ if (value.phases != null) this.__phases = value.phases;
112
+ if (this.isConnected) this._render();
113
+ this.dispatchEvent(new CustomEvent('user-journey:data-changed', {
114
+ detail: { data: this.data, source: 'property' },
115
+ bubbles: true, composed: true,
116
+ }));
117
+ }
118
+
119
+ /* ── Slot caching ──────────────────────────── */
120
+
121
+ #cacheSlotValues() {
122
+ for (const child of this.children) {
123
+ const slot = child.getAttribute('slot');
124
+ if (slot) this.#slotCache.set(slot, child.textContent.trim());
125
+ }
126
+ }
127
+
128
+ _resolve(attr) {
129
+ return this.getAttribute(attr) || this.#slotCache.get(attr) || '';
130
+ }
131
+
132
+ /* ── Lifecycle ─────────────────────────────── */
133
+
134
+ connectedCallback() {
135
+ this.#cacheSlotValues();
136
+ this.setAttribute('data-upgraded', '');
137
+ if (this.hasAttribute('src')) {
138
+ this._loadSrc(this.getAttribute('src'));
139
+ } else {
140
+ this._render();
141
+ }
142
+ }
143
+
144
+ disconnectedCallback() {
145
+ this.removeAttribute('data-upgraded');
146
+ }
147
+
148
+ attributeChangedCallback(name) {
149
+ if (!this.isConnected) return;
150
+ if (name === 'src') {
151
+ this._loadSrc(this.getAttribute('src'));
152
+ } else {
153
+ this._render();
154
+ }
155
+ }
156
+
157
+ /* ── JSON loading ──────────────────────────── */
158
+
159
+ async _loadSrc(src) {
160
+ if (!src) return;
161
+ const root = /** @type {ShadowRoot} */ (this.shadowRoot);
162
+ root.innerHTML = `<style>${styles}</style>` +
163
+ `<div class="state-msg">Loading\u2026</div>`;
164
+ try {
165
+ const res = await fetch(src);
166
+ if (!res.ok) throw new Error(`HTTP ${res.status}`);
167
+ const data = await res.json();
168
+ if (data.persona) this.setAttribute('persona', data.persona);
169
+ if (data.personaId) this.setAttribute('persona-id', data.personaId);
170
+ if (data.title && !this.querySelector('[slot="title"]')) {
171
+ const el = document.createElement('h2');
172
+ el.slot = 'title';
173
+ el.textContent = data.title;
174
+ this.appendChild(el);
175
+ }
176
+ if (data.summary && !this.querySelector('[slot="summary"]')) {
177
+ const el = document.createElement('p');
178
+ el.slot = 'summary';
179
+ el.textContent = data.summary;
180
+ this.appendChild(el);
181
+ }
182
+ this.__phases = data.phases || [];
183
+ this._render();
184
+ } catch (err) {
185
+ root.innerHTML = `<style>${styles}</style>` +
186
+ `<div class="state-msg state-msg--error">Could not load journey: ${esc(/** @type {Error} */ (err).message)}</div>`;
187
+ }
188
+ }
189
+
190
+ /* ── Render ────────────────────────────────── */
191
+
192
+ _render() {
193
+ const persona = this._resolve('persona') || '';
194
+ const personaId = this._resolve('persona-id') || '';
195
+ const storyIds = (this.getAttribute('story-ids') || '')
196
+ .split(',').map(s => s.trim()).filter(Boolean);
197
+ const compact = this.hasAttribute('compact');
198
+ const phases = this.__phases;
199
+ const hasSummary = !!this.querySelector('[slot="summary"]') || this.#slotCache.has('summary');
200
+ const title = this.querySelector('[slot="title"]')?.textContent?.trim()
201
+ || this.#slotCache.get('title') || '';
202
+
203
+ const html = `<style>${styles}</style>
204
+ <article class="journey${compact ? ' journey--compact' : ''}">
205
+
206
+ <header class="journey__header">
207
+ <div class="journey__header-top">
208
+ <div class="journey__chips">
209
+ <span class="chip chip--type">Journey Map</span>
210
+ ${storyIds.map(id =>
211
+ `<a class="chip chip--story" href="#${id}">${this._label(id)}</a>`
212
+ ).join('')}
213
+ </div>
214
+ ${persona ? `
215
+ <div class="journey__persona">
216
+ ${personaId
217
+ ? `<a class="persona-ref" href="#${personaId}">${lucideSvg(UX_ICONS.user)} ${esc(persona)}</a>`
218
+ : `<span class="persona-ref">${lucideSvg(UX_ICONS.user)} ${esc(persona)}</span>`}
219
+ </div>` : ''}
220
+ </div>
221
+ <div class="journey__title-wrap">
222
+ <slot name="title"><h2 class="journey__title">User Journey</h2></slot>
223
+ </div>
224
+ ${hasSummary ? `<div class="journey__summary-wrap"><slot name="summary"></slot></div>` : ''}
225
+ </header>
226
+
227
+ ${phases && phases.length
228
+ ? this._curve(phases) + this._grid(phases)
229
+ : `<div class="journey__placeholder">
230
+ <p>Add phase data via <code>src</code> (JSON) or set <code>.phases</code> programmatically.</p>
231
+ </div>`
232
+ }
233
+
234
+ </article>`;
235
+
236
+ /* Pin view-transition-name on the host so the browser crossfades the
237
+ light-DOM snapshot (which includes the shadow paint) when the
238
+ shadowRoot innerHTML swap replaces all content. Skip on the first
239
+ render — there's nothing to fade from. */
240
+ const root = /** @type {ShadowRoot} */ (this.shadowRoot);
241
+ const swap = () => { root.innerHTML = html; };
242
+ if (root.querySelector('article')) {
243
+ viewTransitionSwap(this, swap, 'uj-vt');
244
+ } else {
245
+ swap();
246
+ }
247
+
248
+ /* Dispatch ready event */
249
+ this.dispatchEvent(new CustomEvent('journey-ready', {
250
+ bubbles: true,
251
+ composed: true,
252
+ detail: {
253
+ title,
254
+ persona,
255
+ phaseCount: phases ? phases.length : 0,
256
+ },
257
+ }));
258
+ }
259
+
260
+ /* ── SVG curve ─────────────────────────────── */
261
+
262
+ _curve(phases) {
263
+ const W = 1000, H = 100, PX = 28, PY = 14;
264
+ const uw = W - PX * 2, uh = H - PY * 2;
265
+ const n = phases.length;
266
+
267
+ const toX = i => PX + (n < 2 ? uw / 2 : (i / (n - 1)) * uw);
268
+ const toY = ph => {
269
+ const m = EMOTION_META[ph.emotion] || EMOTION_META.neutral;
270
+ return PY + (1 - m.score) * uh;
271
+ };
272
+
273
+ const pts = phases.map((ph, i) => ({ x: toX(i), y: toY(ph), ph }));
274
+
275
+ let d = `M ${pts[0].x},${pts[0].y}`;
276
+ for (let i = 1; i < pts.length; i++) {
277
+ const a = pts[i - 1], b = pts[i], cx = (a.x + b.x) / 2;
278
+ d += ` C ${cx},${a.y} ${cx},${b.y} ${b.x},${b.y}`;
279
+ }
280
+
281
+ const uid = `uj-${Math.random().toString(36).slice(2, 8)}`;
282
+ const last = pts.at(-1);
283
+
284
+ return `
285
+ <div class="journey__curve" aria-hidden="true">
286
+ <svg viewBox="0 0 ${W} ${H}" preserveAspectRatio="none" class="curve-svg">
287
+ <defs>
288
+ <linearGradient id="${uid}" x1="0" y1="0" x2="0" y2="1">
289
+ <stop offset="0%" stop-color="#6366f1" stop-opacity="0.22"/>
290
+ <stop offset="100%" stop-color="#6366f1" stop-opacity="0"/>
291
+ </linearGradient>
292
+ </defs>
293
+ <rect x="0" y="0" width="${W}" height="${H * 0.4}" class="zone zone--pos"/>
294
+ <rect x="0" y="${H * 0.4}" width="${W}" height="${H * 0.2}" class="zone zone--neu"/>
295
+ <rect x="0" y="${H * 0.6}" width="${W}" height="${H * 0.4}" class="zone zone--neg"/>
296
+ ${pts.map(({x}) => `<line x1="${x}" y1="0" x2="${x}" y2="${H}" class="vline"/>`).join('')}
297
+ <path d="${d} L ${last.x},${H} L ${pts[0].x},${H} Z" fill="url(#${uid})"/>
298
+ <path d="${d}" fill="none" class="curve-line"/>
299
+ ${pts.map(({x, y, ph}) => {
300
+ const m = EMOTION_META[ph.emotion] || EMOTION_META.neutral;
301
+ return `<circle cx="${x}" cy="${y}" r="5" class="dot" style="fill:${m.color}"/>`;
302
+ }).join('')}
303
+ </svg>
304
+ </div>`;
305
+ }
306
+
307
+ /* ── Phase grid ────────────────────────────── */
308
+
309
+ _grid(phases) {
310
+ const headCells = phases.map((ph, i) => {
311
+ const m = EMOTION_META[ph.emotion] || EMOTION_META.neutral;
312
+ const stories = ph.storyIds || [];
313
+ return `
314
+ <th class="phase-head" data-emotion="${ph.emotion || 'neutral'}"
315
+ style="--ec:${m.color}">
316
+ <span class="ph-num">${i + 1}</span>
317
+ <span class="ph-name">${esc(ph.name || '')}</span>
318
+ <span class="ph-emoji" title="${ph.emotion || 'neutral'}"><span role="img" aria-label="${esc(ph.emotion || 'neutral')}">${m.emoji}</span></span>
319
+ ${stories.length
320
+ ? `<div class="ph-stories">${stories.map(id =>
321
+ `<a class="chip chip--story" href="#${id}">${this._label(id)}</a>`
322
+ ).join('')}</div>`
323
+ : ''}
324
+ </th>`;
325
+ }).join('');
326
+
327
+ const bodyRows = ROWS.map(({ key, label }) => {
328
+ const cells = phases.map(ph => {
329
+ const items = ph[key] || [];
330
+ if (!items.length) return `<td class="data-cell data-cell--empty">\u2014</td>`;
331
+ return `<td class="data-cell data-cell--${key.toLowerCase()}">
332
+ ${items.map(t => `<p>${esc(t)}</p>`).join('')}
333
+ </td>`;
334
+ }).join('');
335
+ return `
336
+ <tr class="grid-row grid-row--${key.toLowerCase()}">
337
+ <th class="row-label">${label}</th>
338
+ ${cells}
339
+ </tr>`;
340
+ }).join('');
341
+
342
+ return `
343
+ <div class="journey__grid-wrap">
344
+ <table class="journey__grid"
345
+ aria-label="${esc(this.getAttribute('title') || 'User Journey')} \u2014 phase breakdown">
346
+ <thead>
347
+ <tr>
348
+ <th class="corner">Stage</th>
349
+ ${headCells}
350
+ </tr>
351
+ </thead>
352
+ <tbody>${bodyRows}</tbody>
353
+ </table>
354
+ </div>`;
355
+ }
356
+
357
+ /* ── Helpers ───────────────────────────────── */
358
+
359
+ _label(id) {
360
+ return id.replace(/^(activity|persona|journey|story|user)-/, '')
361
+ .replace(/-/g, ' ')
362
+ .replace(/\b\w/g, c => c.toUpperCase());
363
+ }
364
+ }
365
+
366
+ registerComponent('user-journey', UserJourney);
367
+
368
+ export { UserJourney };
@@ -0,0 +1,14 @@
1
+ <!--
2
+ user-journey — static (zero-JS) form
3
+ Without JS, shows the journey metadata. Full visualization requires JS.
4
+ -->
5
+ <user-journey
6
+ title="API Platform Evaluation"
7
+ persona="Sarah Chen"
8
+ persona-id="persona-sarah-chen"
9
+ summary="How a developer discovers, evaluates, and adopts our platform"
10
+ story-ids="PROJ-142,PROJ-156">
11
+ <noscript>
12
+ <p>This journey map requires JavaScript to render the emotion curve and phase grid.</p>
13
+ </noscript>
14
+ </user-journey>
@@ -0,0 +1,317 @@
1
+ /**
2
+ * Shadow DOM styles for <user-journey>
3
+ *
4
+ * Theme integration:
5
+ * --user-journey-* (per-instance author override)
6
+ * --color-* (VB theme token — adapts to data-theme and color mode)
7
+ * #fallback (last-resort hex for component used outside VB)
8
+ *
9
+ * VB tokens already swap on color-scheme change and across themes, so we
10
+ * deliberately DO NOT define our own @media (prefers-color-scheme: dark)
11
+ * overrides — those would override the user's chosen VB theme. Semantic
12
+ * tints (positive/neutral/negative emotion zones, painpoint/opportunity
13
+ * row tints) are derived via color-mix from the corresponding theme
14
+ * tokens (--color-success/--color-warning/--color-error) so they shift
15
+ * automatically with the active theme.
16
+ */
17
+ const styles = `
18
+ :host {
19
+ display: block;
20
+ font-family: var(--_font-sans);
21
+ line-height: 1.6;
22
+ color: var(--_text);
23
+ container-type: inline-size;
24
+
25
+ --_bg: var(--user-journey-bg, var(--color-surface-raised, #f8f9fa));
26
+ --_card: var(--user-journey-card, var(--color-surface, #ffffff));
27
+ --_border: var(--user-journey-border, var(--color-border, #e0e0e0));
28
+ --_muted: var(--user-journey-muted, var(--color-text-muted, #666666));
29
+ --_text: var(--user-journey-text, var(--color-text, #1a1a1a));
30
+ --_inverted: var(--user-journey-text-inverted, var(--color-text-inverted, #ffffff));
31
+ --_primary: var(--user-journey-primary, var(--color-primary, var(--color-interactive, #6366f1)));
32
+ --_accent: var(--user-journey-accent, var(--color-accent, #8b5cf6));
33
+ --_link: var(--user-journey-link, var(--color-interactive, var(--color-primary, #6366f1)));
34
+ --_curve-stroke: var(--user-journey-curve-stroke, var(--_primary));
35
+ --_radius: var(--user-journey-radius, var(--radius-l, 0.75rem));
36
+
37
+ --_font-sans: var(--user-journey-font, var(--font-sans, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif));
38
+ --_font-mono: var(--user-journey-font-mono, var(--font-mono, ui-monospace, "Cascadia Code", "Source Code Pro", Menlo, monospace));
39
+ --_font-xs: var(--user-journey-font-xs, var(--font-size-xs, 0.75rem));
40
+ --_font-sm: var(--user-journey-font-sm, var(--font-size-sm, 0.875rem));
41
+ --_font-md: var(--user-journey-font-md, var(--font-size-md, 1rem));
42
+ --_font-xl: var(--user-journey-font-xl, var(--font-size-xl, 1.25rem));
43
+ --_space-2xs: var(--user-journey-space-2xs, var(--size-2xs, 0.25rem));
44
+ --_space-xs: var(--user-journey-space-xs, var(--size-xs, 0.5rem));
45
+ --_space-s: var(--user-journey-space-s, var(--size-s, 0.75rem));
46
+ --_space-m: var(--user-journey-space-m, var(--size-m, 1rem));
47
+ --_space-l: var(--user-journey-space-l, var(--size-l, 1.5rem));
48
+
49
+ /* Semantic tints derived from theme tokens via color-mix.
50
+ Subtle backgrounds (10–14%) for body cells, header at 22%. */
51
+ --_tint-pos: color-mix(in oklch, var(--color-success, #22c55e) 14%, var(--_card));
52
+ --_tint-neu: color-mix(in oklch, var(--color-warning, #f59e0b) 12%, var(--_card));
53
+ --_tint-neg: color-mix(in oklch, var(--color-error, #ef4444) 12%, var(--_card));
54
+ --_tint-row-pain: color-mix(in oklch, var(--color-error, #ef4444) 8%, var(--_card));
55
+ --_tint-row-opp: color-mix(in oklch, var(--color-success, #22c55e) 8%, var(--_card));
56
+ --_chip-type-bg: color-mix(in oklch, var(--_accent) 18%, var(--_card));
57
+ --_chip-type-fg: var(--color-accent-text, var(--_accent));
58
+ --_chip-story-bg: color-mix(in oklch, var(--_link) 18%, var(--_card));
59
+ --_chip-story-bg-hover: color-mix(in oklch, var(--_link) 28%, var(--_card));
60
+ --_chip-story-fg: var(--_link);
61
+ --_grid-head-bg: color-mix(in oklch, var(--_primary) 85%, var(--color-text, #000));
62
+ }
63
+
64
+ *, *::before, *::after { box-sizing: border-box; margin: 0; }
65
+
66
+ /* Card */
67
+ .journey {
68
+ background: var(--_card);
69
+ border: 1px solid var(--_border);
70
+ border-radius: var(--_radius);
71
+ overflow: hidden;
72
+ }
73
+
74
+ /* Header */
75
+ .journey__header {
76
+ padding: 20px 24px 16px 28px;
77
+ border-block-end: 1px solid var(--_border);
78
+ position: relative;
79
+ }
80
+
81
+ /* Left accent bar */
82
+ .journey__header::before {
83
+ content: '';
84
+ position: absolute;
85
+ inset-block: 0;
86
+ inset-inline-start: 0;
87
+ width: 4px;
88
+ background: linear-gradient(135deg, var(--_primary), var(--_accent));
89
+ }
90
+
91
+ .journey__header-top {
92
+ display: flex;
93
+ align-items: flex-start;
94
+ justify-content: space-between;
95
+ gap: var(--_space-s);
96
+ flex-wrap: wrap;
97
+ margin-block-end: var(--_space-xs);
98
+ }
99
+
100
+ .journey__chips {
101
+ display: flex;
102
+ flex-wrap: wrap;
103
+ align-items: center;
104
+ gap: 6px;
105
+ }
106
+
107
+ /* Chips */
108
+ .chip {
109
+ display: inline-block;
110
+ font-size: 11px;
111
+ font-weight: 700;
112
+ letter-spacing: 0.05em;
113
+ padding: 2px 9px;
114
+ border-radius: 99px;
115
+ text-decoration: none;
116
+ line-height: 1.6;
117
+ }
118
+
119
+ .chip--type {
120
+ color: var(--_chip-type-fg);
121
+ background: var(--_chip-type-bg);
122
+ }
123
+
124
+ .chip--story {
125
+ color: var(--_chip-story-fg);
126
+ background: var(--_chip-story-bg);
127
+ }
128
+
129
+ .chip--story:hover { background: var(--_chip-story-bg-hover); }
130
+
131
+ /* Persona ref */
132
+ .persona-ref {
133
+ display: inline-flex;
134
+ align-items: center;
135
+ gap: 4px;
136
+ font-size: 13px;
137
+ font-weight: 600;
138
+ color: var(--_muted);
139
+ text-decoration: none;
140
+ white-space: nowrap;
141
+ }
142
+ .persona-ref svg { width: 14px; height: 14px; flex-shrink: 0; }
143
+ a.persona-ref:hover { color: var(--_link); text-decoration: underline; }
144
+
145
+ /* Title & summary (slotted content) */
146
+ .journey__title-wrap {
147
+ margin-block-end: var(--_space-2xs);
148
+ }
149
+
150
+ .journey__title {
151
+ font-size: var(--_font-xl);
152
+ font-weight: 700;
153
+ color: var(--_text);
154
+ margin: 0;
155
+ }
156
+
157
+ ::slotted([slot="title"]) {
158
+ font-size: var(--_font-xl) !important;
159
+ font-weight: 700 !important;
160
+ color: var(--_text) !important;
161
+ margin: 0 !important;
162
+ }
163
+
164
+ .journey--compact .journey__title,
165
+ .journey--compact ::slotted([slot="title"]) { font-size: var(--_font-md) !important; }
166
+
167
+ .journey__summary-wrap {
168
+ max-width: 72ch;
169
+ }
170
+
171
+ ::slotted([slot="summary"]) {
172
+ font-size: var(--_font-sm) !important;
173
+ color: var(--_muted) !important;
174
+ margin: 0 !important;
175
+ }
176
+
177
+ /* Emotion curve */
178
+ .journey__curve {
179
+ background: var(--_bg);
180
+ border-block-end: 1px solid var(--_border);
181
+ overflow: hidden;
182
+ }
183
+
184
+ .curve-svg {
185
+ display: block;
186
+ width: 100%;
187
+ height: 80px;
188
+ }
189
+
190
+ .journey--compact .curve-svg { height: 54px; }
191
+
192
+ .zone { opacity: 0.55; }
193
+ .zone--pos { fill: var(--_tint-pos); }
194
+ .zone--neu { fill: var(--_tint-neu); }
195
+ .zone--neg { fill: var(--_tint-neg); }
196
+
197
+ .vline { stroke: var(--_border); stroke-width: 1; stroke-dasharray: 3 4; }
198
+ .curve-line { stroke: var(--_curve-stroke); stroke-width: 2.5; stroke-linecap: round; }
199
+ .dot { stroke: var(--_card); stroke-width: 2.5; }
200
+
201
+ /* Grid */
202
+ .journey__grid-wrap { overflow-x: auto; }
203
+
204
+ .journey__grid {
205
+ width: 100%;
206
+ border-collapse: collapse;
207
+ font-size: 13px;
208
+ }
209
+
210
+ /* Head row */
211
+ .journey__grid thead tr {
212
+ background: var(--_grid-head-bg);
213
+ color: var(--_inverted);
214
+ }
215
+
216
+ .corner {
217
+ padding: 10px 12px;
218
+ font-size: 10px;
219
+ font-weight: 700;
220
+ text-transform: uppercase;
221
+ letter-spacing: 0.08em;
222
+ opacity: 0.55;
223
+ text-align: left;
224
+ white-space: nowrap;
225
+ min-width: 100px;
226
+ position: sticky;
227
+ left: 0;
228
+ background: var(--_grid-head-bg);
229
+ z-index: 2;
230
+ }
231
+
232
+ .phase-head {
233
+ padding: 10px 14px;
234
+ text-align: left;
235
+ vertical-align: top;
236
+ border-inline-start: 1px solid color-mix(in oklch, var(--_inverted) 12%, transparent);
237
+ min-width: 160px;
238
+ position: relative;
239
+ }
240
+
241
+ /* Emotion-coloured top accent per phase */
242
+ .phase-head::before {
243
+ content: '';
244
+ position: absolute;
245
+ inset-block-start: 0;
246
+ inset-inline: 0;
247
+ height: 3px;
248
+ background: var(--ec, var(--_primary));
249
+ }
250
+
251
+ .ph-num { display: block; font-size: 10px; opacity: 0.5; margin-block-end: 2px; }
252
+ .ph-name { display: block; font-size: var(--_font-sm); font-weight: 700; line-height: 1.2; }
253
+ .ph-emoji { display: block; font-size: var(--_font-xl); line-height: 1; margin-block: 4px 2px; }
254
+
255
+ .ph-stories { display: flex; flex-wrap: wrap; gap: var(--_space-2xs); margin-block-start: var(--_space-2xs); }
256
+
257
+ /* Body rows */
258
+ .grid-row th,
259
+ .grid-row td {
260
+ padding: 10px 12px;
261
+ border-block-end: 1px solid var(--_border);
262
+ border-inline-start: 1px solid var(--_border);
263
+ vertical-align: top;
264
+ }
265
+
266
+ .grid-row th:first-child { border-inline-start: none; }
267
+
268
+ .row-label {
269
+ font-size: 11px;
270
+ font-weight: 700;
271
+ text-transform: uppercase;
272
+ letter-spacing: 0.07em;
273
+ color: var(--_muted);
274
+ white-space: nowrap;
275
+ background: var(--_bg);
276
+ text-align: left;
277
+ position: sticky;
278
+ left: 0;
279
+ z-index: 1;
280
+ }
281
+
282
+ .data-cell { font-size: 13px; line-height: 1.45; }
283
+ .data-cell p { margin-block: 0 4px; }
284
+ .data-cell p:last-child { margin-block-end: 0; }
285
+ .data-cell--empty { color: var(--_muted); opacity: 0.35; }
286
+
287
+ /* Semantic row tints */
288
+ .grid-row--painpoints .data-cell { background: var(--_tint-row-pain); }
289
+ .grid-row--opportunities .data-cell { background: var(--_tint-row-opp); }
290
+
291
+ /* Compact */
292
+ .journey--compact .phase-head { min-width: 120px; padding: 8px 10px; }
293
+ .journey--compact .data-cell { font-size: 12px; padding: 7px 10px; }
294
+ .journey--compact .corner { min-width: 80px; }
295
+
296
+ /* Utility */
297
+ .state-msg { padding: var(--_space-l); font-size: var(--_font-sm); color: var(--_muted); font-style: italic; }
298
+ .state-msg--error { color: var(--color-error-text, var(--color-error, #dc2626)); }
299
+ .journey__placeholder { padding: 20px 24px; font-size: var(--_font-sm); color: var(--_muted); }
300
+ code { font-family: var(--_font-mono); font-size: 0.88em; }
301
+
302
+ /* Responsive */
303
+ @container (max-width: 500px) {
304
+ .journey__header { padding: 14px 16px 12px 20px; }
305
+ .journey__title { font-size: 17px; }
306
+ .corner, .row-label { min-width: 72px; font-size: 9px; }
307
+ .phase-head { min-width: 110px; padding: 8px 10px; }
308
+ .data-cell { font-size: 12px; padding: 8px 10px; }
309
+ }
310
+
311
+ @media print {
312
+ .journey__grid-wrap { overflow: visible; }
313
+ .row-label, .corner { position: relative; }
314
+ }
315
+ `;
316
+
317
+ export { styles };