@profpowell/vb-project-planning 0.2.1 → 0.2.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +10 -5
- package/dist/vb-project-planning.css +69 -3
- package/dist/vb-project-planning.js +3191 -2544
- package/dist/vb-project-planning.min.css +1 -1
- package/dist/vb-project-planning.min.js +221 -197
- package/package.json +7 -4
- package/src/lib/board-surface.js +158 -0
- package/src/lib/bundle-registry.js +2 -75
- package/src/lib/dates.js +125 -0
- package/src/lib/escape.js +34 -0
- package/src/lib/hash.js +16 -0
- package/src/lib/portrait-url.js +3 -29
- package/src/lib/src-loader.js +78 -0
- package/src/lib/vb-view-transition.js +7 -1
- package/src/utils/copy-init.js +56 -17
- package/src/web-components/_ux-base.js +409 -24
- package/src/web-components/adr-wc/README.md +18 -16
- package/src/web-components/adr-wc/api.json +7 -5
- package/src/web-components/adr-wc/logic.js +50 -136
- package/src/web-components/burndown-chart/api.json +8 -6
- package/src/web-components/burndown-chart/logic.js +75 -39
- package/src/web-components/empathy-map/api.json +13 -13
- package/src/web-components/empathy-map/logic.js +161 -168
- package/src/web-components/empathy-map/static.html +3 -3
- package/src/web-components/empathy-map/styles.js +1 -0
- package/src/web-components/gantt-chart/README.md +12 -6
- package/src/web-components/gantt-chart/api.json +23 -7
- package/src/web-components/gantt-chart/logic.js +456 -431
- package/src/web-components/gantt-chart/styles.css +14 -1
- package/src/web-components/impact-effort/api.json +9 -7
- package/src/web-components/impact-effort/logic.js +238 -209
- package/src/web-components/impact-effort/static.html +12 -4
- package/src/web-components/iron-triangle/_capacity.js +0 -18
- package/src/web-components/kanban-board/api.json +8 -6
- package/src/web-components/kanban-board/logic.js +289 -333
- package/src/web-components/product-roadmap/api.json +14 -13
- package/src/web-components/product-roadmap/logic.js +393 -246
- package/src/web-components/product-roadmap/styles.css +40 -0
- package/src/web-components/quadrant-grid/api.json +9 -4
- package/src/web-components/quadrant-grid/logic.js +80 -12
- package/src/web-components/quadrant-grid/styles.css +7 -0
- package/src/web-components/quality-target/_quality-utils.js +2 -46
- package/src/web-components/review-surface/README.md +9 -5
- package/src/web-components/review-surface/api.json +10 -8
- package/src/web-components/review-surface/logic.js +349 -218
- package/src/web-components/risk-register/api.json +1 -6
- package/src/web-components/risk-register/logic.js +71 -24
- package/src/web-components/risk-register/styles.css +9 -1
- package/src/web-components/story-map/api.json +8 -6
- package/src/web-components/story-map/logic.js +280 -281
- package/src/web-components/story-map/static.html +15 -5
- package/src/web-components/traceability-matrix/api.json +10 -3
- package/src/web-components/traceability-matrix/logic.js +112 -63
- package/src/web-components/traceability-matrix/styles.css +22 -1
- package/src/web-components/user-journey/api.json +25 -24
- package/src/web-components/user-journey/logic.js +76 -127
- package/src/web-components/user-journey/static.html +2 -2
- package/src/web-components/user-persona/api.json +15 -14
- package/src/web-components/user-persona/logic.js +71 -117
- package/src/web-components/user-persona/styles.js +1 -0
- package/src/web-components/user-story/api.json +13 -11
- package/src/web-components/user-story/logic.js +164 -195
- package/src/web-components/user-story/styles.js +28 -1
- package/src/web-components/work-item/README.md +3 -3
- package/src/web-components/work-item/api.json +13 -11
- package/src/web-components/work-item/logic.js +58 -154
|
@@ -27,12 +27,22 @@
|
|
|
27
27
|
* }
|
|
28
28
|
* ]
|
|
29
29
|
* }
|
|
30
|
+
*
|
|
31
|
+
* Title and summary are content: `<h2 slot="title">`, `<p slot="summary">`
|
|
32
|
+
* (or the `title` / `summary` JSON fields, which create those children).
|
|
33
|
+
*
|
|
34
|
+
* @fires journey-ready - Once, after the first render (after the src data renders when src is set), detail: { title, persona, phaseCount }
|
|
35
|
+
* @fires user-journey:upgraded - Once, after first connect
|
|
36
|
+
* @fires user-journey:data-changed - After .data assignment, detail: { data, source: 'property' }
|
|
37
|
+
* @fires user-journey:phases-changed - After .phases assignment, detail: { phases, source: 'property' }
|
|
30
38
|
*/
|
|
31
39
|
|
|
32
40
|
import { styles } from './styles.js';
|
|
33
41
|
import { registerComponent } from '../../lib/bundle-registry.js';
|
|
34
42
|
import { viewTransitionSwap } from '../../lib/vb-view-transition.js';
|
|
35
|
-
import {
|
|
43
|
+
import {
|
|
44
|
+
esc, emotionMeta, lucideSvg, UX_ICONS, UXShadowElement, fillSlot, slottedChild, splitList, asList, fragmentHref,
|
|
45
|
+
} from '../_ux-base.js';
|
|
36
46
|
|
|
37
47
|
const ROWS = [
|
|
38
48
|
{ key: 'actions', label: 'Actions' },
|
|
@@ -42,18 +52,15 @@ const ROWS = [
|
|
|
42
52
|
{ key: 'opportunities', label: 'Opportunities' },
|
|
43
53
|
];
|
|
44
54
|
|
|
45
|
-
class UserJourney extends
|
|
55
|
+
class UserJourney extends UXShadowElement {
|
|
56
|
+
static readyEvent = 'journey-ready';
|
|
57
|
+
|
|
46
58
|
static get observedAttributes() {
|
|
47
59
|
return ['src', 'persona', 'persona-id', 'story-ids', 'compact'];
|
|
48
60
|
}
|
|
49
61
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
constructor() {
|
|
53
|
-
super();
|
|
54
|
-
this.attachShadow({ mode: 'open' });
|
|
55
|
-
this.__phases = null;
|
|
56
|
-
}
|
|
62
|
+
/** @type {Array<Record<string, any>> | null} */
|
|
63
|
+
__phases = null;
|
|
57
64
|
|
|
58
65
|
/** @returns {Array|null} */
|
|
59
66
|
get phases() { return this.__phases; }
|
|
@@ -66,7 +73,7 @@ class UserJourney extends HTMLElement {
|
|
|
66
73
|
set phases(data) {
|
|
67
74
|
if (this.__phases === data) return;
|
|
68
75
|
this.__phases = data;
|
|
69
|
-
if (this.
|
|
76
|
+
if (this._isSetUp) this._render();
|
|
70
77
|
this.dispatchEvent(new CustomEvent('user-journey:phases-changed', {
|
|
71
78
|
detail: { phases: data, source: 'property' },
|
|
72
79
|
bubbles: true, composed: true,
|
|
@@ -82,8 +89,8 @@ class UserJourney extends HTMLElement {
|
|
|
82
89
|
return {
|
|
83
90
|
persona: this.getAttribute('persona') || undefined,
|
|
84
91
|
personaId: this.getAttribute('persona-id') || undefined,
|
|
85
|
-
title: this
|
|
86
|
-
summary: this
|
|
92
|
+
title: slottedChild(this, 'title')?.textContent?.trim() || undefined,
|
|
93
|
+
summary: slottedChild(this, 'summary')?.textContent?.trim() || undefined,
|
|
87
94
|
phases: this.__phases || undefined,
|
|
88
95
|
};
|
|
89
96
|
}
|
|
@@ -93,98 +100,56 @@ class UserJourney extends HTMLElement {
|
|
|
93
100
|
* Emits user-journey:data-changed { data, source: 'property' }.
|
|
94
101
|
*/
|
|
95
102
|
set data(value) {
|
|
96
|
-
|
|
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
|
-
}));
|
|
103
|
+
this._setData(value);
|
|
117
104
|
}
|
|
118
105
|
|
|
119
|
-
|
|
106
|
+
/**
|
|
107
|
+
* Apply a journey record (from .data or src). Title / summary create
|
|
108
|
+
* slotted children only when none are authored.
|
|
109
|
+
* @param {Record<string, any>} data
|
|
110
|
+
*/
|
|
111
|
+
_applyData(data) {
|
|
112
|
+
if (data.persona) this.setAttribute('persona', String(data.persona));
|
|
113
|
+
if (data.personaId) this.setAttribute('persona-id', String(data.personaId));
|
|
114
|
+
fillSlot(this, 'title', data.title, { tag: 'h2' });
|
|
115
|
+
fillSlot(this, 'summary', data.summary);
|
|
116
|
+
if (data.phases != null) this.__phases = data.phases;
|
|
117
|
+
}
|
|
120
118
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
119
|
+
_readyDetail() {
|
|
120
|
+
const phases = this.__phases;
|
|
121
|
+
return {
|
|
122
|
+
title: this._slotText('title'),
|
|
123
|
+
persona: this._resolve('persona'),
|
|
124
|
+
phaseCount: Array.isArray(phases) ? phases.length : 0,
|
|
125
|
+
};
|
|
126
126
|
}
|
|
127
127
|
|
|
128
|
+
/**
|
|
129
|
+
* Attribute value, else the text of a same-named light-DOM child (e.g.
|
|
130
|
+
* `<span slot="persona">`, read as data; it is not projected).
|
|
131
|
+
* @param {string} attr
|
|
132
|
+
*/
|
|
128
133
|
_resolve(attr) {
|
|
129
|
-
return this.getAttribute(attr) || this
|
|
134
|
+
return this.getAttribute(attr) || this._slotCache.get(attr) || '';
|
|
130
135
|
}
|
|
131
136
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
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
|
-
}
|
|
137
|
+
/** Journey title: the slotted title's text, or the default heading. */
|
|
138
|
+
#title() {
|
|
139
|
+
return this._slotText('title') || 'User Journey';
|
|
142
140
|
}
|
|
143
141
|
|
|
144
|
-
|
|
145
|
-
this.removeAttribute('data-upgraded');
|
|
146
|
-
}
|
|
142
|
+
/* ── JSON loading states ───────────────────── */
|
|
147
143
|
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
this._loadSrc(this.getAttribute('src'));
|
|
152
|
-
} else {
|
|
153
|
-
this._render();
|
|
154
|
-
}
|
|
144
|
+
_renderLoading() {
|
|
145
|
+
/** @type {ShadowRoot} */ (this.shadowRoot).innerHTML = `<style>${styles}</style>` +
|
|
146
|
+
`<div class="state-msg">Loading\u2026</div>`;
|
|
155
147
|
}
|
|
156
148
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
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
|
-
}
|
|
149
|
+
/** @param {Error} err */
|
|
150
|
+
_renderError(err) {
|
|
151
|
+
/** @type {ShadowRoot} */ (this.shadowRoot).innerHTML = `<style>${styles}</style>` +
|
|
152
|
+
`<div class="state-msg state-msg--error">Could not load journey: ${esc(err.message)}</div>`;
|
|
188
153
|
}
|
|
189
154
|
|
|
190
155
|
/* ── Render ────────────────────────────────── */
|
|
@@ -192,13 +157,10 @@ class UserJourney extends HTMLElement {
|
|
|
192
157
|
_render() {
|
|
193
158
|
const persona = this._resolve('persona') || '';
|
|
194
159
|
const personaId = this._resolve('persona-id') || '';
|
|
195
|
-
const storyIds = (this.getAttribute('story-ids')
|
|
196
|
-
.split(',').map(s => s.trim()).filter(Boolean);
|
|
160
|
+
const storyIds = splitList(this.getAttribute('story-ids'));
|
|
197
161
|
const compact = this.hasAttribute('compact');
|
|
198
|
-
const phases = this.__phases;
|
|
199
|
-
const hasSummary = !!this
|
|
200
|
-
const title = this.querySelector('[slot="title"]')?.textContent?.trim()
|
|
201
|
-
|| this.#slotCache.get('title') || '';
|
|
162
|
+
const phases = Array.isArray(this.__phases) ? this.__phases : null;
|
|
163
|
+
const hasSummary = !!slottedChild(this, 'summary') || this._slotCache.has('summary');
|
|
202
164
|
|
|
203
165
|
const html = `<style>${styles}</style>
|
|
204
166
|
<article class="journey${compact ? ' journey--compact' : ''}">
|
|
@@ -208,13 +170,13 @@ class UserJourney extends HTMLElement {
|
|
|
208
170
|
<div class="journey__chips">
|
|
209
171
|
<span class="chip chip--type">Journey Map</span>
|
|
210
172
|
${storyIds.map(id =>
|
|
211
|
-
`<a class="chip chip--story" href="
|
|
173
|
+
`<a class="chip chip--story" href="${fragmentHref(id)}">${esc(this._label(id))}</a>`
|
|
212
174
|
).join('')}
|
|
213
175
|
</div>
|
|
214
176
|
${persona ? `
|
|
215
177
|
<div class="journey__persona">
|
|
216
178
|
${personaId
|
|
217
|
-
? `<a class="persona-ref" href="
|
|
179
|
+
? `<a class="persona-ref" href="${fragmentHref(personaId)}">${lucideSvg(UX_ICONS.user)} ${esc(persona)}</a>`
|
|
218
180
|
: `<span class="persona-ref">${lucideSvg(UX_ICONS.user)} ${esc(persona)}</span>`}
|
|
219
181
|
</div>` : ''}
|
|
220
182
|
</div>
|
|
@@ -244,17 +206,6 @@ class UserJourney extends HTMLElement {
|
|
|
244
206
|
} else {
|
|
245
207
|
swap();
|
|
246
208
|
}
|
|
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
209
|
}
|
|
259
210
|
|
|
260
211
|
/* ── SVG curve ─────────────────────────────── */
|
|
@@ -265,10 +216,7 @@ class UserJourney extends HTMLElement {
|
|
|
265
216
|
const n = phases.length;
|
|
266
217
|
|
|
267
218
|
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
|
-
};
|
|
219
|
+
const toY = ph => PY + (1 - emotionMeta(ph?.emotion).score) * uh;
|
|
272
220
|
|
|
273
221
|
const pts = phases.map((ph, i) => ({ x: toX(i), y: toY(ph), ph }));
|
|
274
222
|
|
|
@@ -296,10 +244,9 @@ class UserJourney extends HTMLElement {
|
|
|
296
244
|
${pts.map(({x}) => `<line x1="${x}" y1="0" x2="${x}" y2="${H}" class="vline"/>`).join('')}
|
|
297
245
|
<path d="${d} L ${last.x},${H} L ${pts[0].x},${H} Z" fill="url(#${uid})"/>
|
|
298
246
|
<path d="${d}" fill="none" class="curve-line"/>
|
|
299
|
-
${pts.map(({x, y, ph}) =>
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
}).join('')}
|
|
247
|
+
${pts.map(({x, y, ph}) =>
|
|
248
|
+
`<circle cx="${x}" cy="${y}" r="5" class="dot" style="fill:${emotionMeta(ph?.emotion).color}"/>`
|
|
249
|
+
).join('')}
|
|
303
250
|
</svg>
|
|
304
251
|
</div>`;
|
|
305
252
|
}
|
|
@@ -308,17 +255,18 @@ class UserJourney extends HTMLElement {
|
|
|
308
255
|
|
|
309
256
|
_grid(phases) {
|
|
310
257
|
const headCells = phases.map((ph, i) => {
|
|
311
|
-
const m =
|
|
312
|
-
const
|
|
258
|
+
const m = emotionMeta(ph?.emotion);
|
|
259
|
+
const emotion = String(ph?.emotion || 'neutral');
|
|
260
|
+
const stories = asList(ph?.storyIds);
|
|
313
261
|
return `
|
|
314
|
-
<th class="phase-head" data-emotion="${
|
|
262
|
+
<th class="phase-head" data-emotion="${esc(emotion)}"
|
|
315
263
|
style="--ec:${m.color}">
|
|
316
264
|
<span class="ph-num">${i + 1}</span>
|
|
317
|
-
<span class="ph-name">${esc(ph
|
|
318
|
-
<span class="ph-emoji" title="${
|
|
265
|
+
<span class="ph-name">${esc(ph?.name || '')}</span>
|
|
266
|
+
<span class="ph-emoji" title="${esc(emotion)}"><span role="img" aria-label="${esc(emotion)}">${m.emoji}</span></span>
|
|
319
267
|
${stories.length
|
|
320
268
|
? `<div class="ph-stories">${stories.map(id =>
|
|
321
|
-
`<a class="chip chip--story" href="
|
|
269
|
+
`<a class="chip chip--story" href="${fragmentHref(id)}">${esc(this._label(id))}</a>`
|
|
322
270
|
).join('')}</div>`
|
|
323
271
|
: ''}
|
|
324
272
|
</th>`;
|
|
@@ -326,7 +274,7 @@ class UserJourney extends HTMLElement {
|
|
|
326
274
|
|
|
327
275
|
const bodyRows = ROWS.map(({ key, label }) => {
|
|
328
276
|
const cells = phases.map(ph => {
|
|
329
|
-
const items = ph[key]
|
|
277
|
+
const items = asList(ph?.[key]);
|
|
330
278
|
if (!items.length) return `<td class="data-cell data-cell--empty">\u2014</td>`;
|
|
331
279
|
return `<td class="data-cell data-cell--${key.toLowerCase()}">
|
|
332
280
|
${items.map(t => `<p>${esc(t)}</p>`).join('')}
|
|
@@ -342,7 +290,7 @@ class UserJourney extends HTMLElement {
|
|
|
342
290
|
return `
|
|
343
291
|
<div class="journey__grid-wrap">
|
|
344
292
|
<table class="journey__grid"
|
|
345
|
-
aria-label="${esc(this
|
|
293
|
+
aria-label="${esc(this.#title())} \u2014 phase breakdown">
|
|
346
294
|
<thead>
|
|
347
295
|
<tr>
|
|
348
296
|
<th class="corner">Stage</th>
|
|
@@ -356,8 +304,9 @@ class UserJourney extends HTMLElement {
|
|
|
356
304
|
|
|
357
305
|
/* ── Helpers ───────────────────────────────── */
|
|
358
306
|
|
|
307
|
+
/** @param {string} id */
|
|
359
308
|
_label(id) {
|
|
360
|
-
return id.replace(/^(activity|persona|journey|story|user)-/, '')
|
|
309
|
+
return String(id).replace(/^(activity|persona|journey|story|user)-/, '')
|
|
361
310
|
.replace(/-/g, ' ')
|
|
362
311
|
.replace(/\b\w/g, c => c.toUpperCase());
|
|
363
312
|
}
|
|
@@ -3,11 +3,11 @@
|
|
|
3
3
|
Without JS, shows the journey metadata. Full visualization requires JS.
|
|
4
4
|
-->
|
|
5
5
|
<user-journey
|
|
6
|
-
title="API Platform Evaluation"
|
|
7
6
|
persona="Sarah Chen"
|
|
8
7
|
persona-id="persona-sarah-chen"
|
|
9
|
-
summary="How a developer discovers, evaluates, and adopts our platform"
|
|
10
8
|
story-ids="PROJ-142,PROJ-156">
|
|
9
|
+
<h2 slot="title">API Platform Evaluation</h2>
|
|
10
|
+
<p slot="summary">How a developer discovers, evaluates, and adopts our platform</p>
|
|
11
11
|
<noscript>
|
|
12
12
|
<p>This journey map requires JavaScript to render the emotion curve and phase grid.</p>
|
|
13
13
|
</noscript>
|
|
@@ -2,34 +2,35 @@
|
|
|
2
2
|
"$schema": "../../../schemas/api.schema.json",
|
|
3
3
|
"element": "user-persona",
|
|
4
4
|
"type": "web-component",
|
|
5
|
-
"description": "Displays a user persona card with avatar, demographics, quote, and slotted sections for goals, frustrations, and behaviors",
|
|
5
|
+
"description": "Displays a user persona card with avatar, demographics, quote, and slotted sections for background, goals, frustrations, and behaviors",
|
|
6
6
|
"htmlvalidate": {
|
|
7
7
|
"flow": true,
|
|
8
8
|
"permittedContent": ["@flow"]
|
|
9
9
|
},
|
|
10
10
|
"attributes": [
|
|
11
|
-
{ "name": "name", "kind": "host-api", "purpose": "config", "type": "string", "description": "Persona name (
|
|
11
|
+
{ "name": "name", "kind": "host-api", "purpose": "config", "type": "string", "description": "Persona name, used when there is no slot=\"name\" child (the slot wins). Drives the avatar initials and color." },
|
|
12
12
|
{ "name": "role", "kind": "host-api", "purpose": "config", "type": "string", "description": "Job title or role" },
|
|
13
|
-
{ "name": "age", "kind": "host-api", "purpose": "config", "type": "string", "description": "Age
|
|
14
|
-
{ "name": "location", "kind": "host-api", "purpose": "config", "type": "string", "description": "Location
|
|
15
|
-
{ "name": "avatar", "kind": "host-api", "purpose": "config", "type": "string", "description": "
|
|
16
|
-
{ "name": "quote", "kind": "host-api", "purpose": "config", "type": "string", "description": "Key quote
|
|
13
|
+
{ "name": "age", "kind": "host-api", "purpose": "config", "type": "string", "description": "Age, shown as \"[value] years old\"" },
|
|
14
|
+
{ "name": "location", "kind": "host-api", "purpose": "config", "type": "string", "description": "Location, shown with a pin icon" },
|
|
15
|
+
{ "name": "avatar", "kind": "host-api", "purpose": "config", "type": "string", "description": "Avatar image URL (http(s), relative, or data:image). When omitted, initials are generated from the name." },
|
|
16
|
+
{ "name": "quote", "kind": "host-api", "purpose": "config", "type": "string", "description": "Key quote, used when there is no slot=\"quote\" child (the slot wins)" },
|
|
17
17
|
{ "name": "compact", "kind": "host-api", "purpose": "config", "type": "boolean", "description": "Reduced spacing variant" },
|
|
18
|
-
{ "name": "src", "kind": "host-api", "purpose": "config", "type": "string", "description": "URL
|
|
19
|
-
{ "name": "data-list-stories", "kind": "host-api", "purpose": "config", "type": "boolean", "description": "Render a Stories section that auto-lists every <user-story persona-id=\"…\"> matching this persona's id. Updates live when stories are added, removed, or relabeled." }
|
|
18
|
+
{ "name": "src", "kind": "host-api", "purpose": "config", "type": "string", "description": "URL of a JSON persona record. Fetched once per value; the record sets the attributes above and creates (or replaces) the name, quote, bio, goals, frustrations and behaviors slotted children." },
|
|
19
|
+
{ "name": "data-list-stories", "kind": "host-api", "purpose": "config", "type": "boolean", "description": "Render a Stories section that auto-lists every <user-story persona-id=\"…\"> matching this persona's id. Updates live when stories are added, removed, or relabeled." },
|
|
20
|
+
{ "name": "data-mock", "kind": "host-api", "purpose": "config", "type": "boolean", "description": "Fill any missing name, role, age, location, avatar, quote, goals, frustrations and behaviors with deterministic sample data (seeded by data-seed or the data-mock value)." },
|
|
21
|
+
{ "name": "id", "kind": "host-api", "purpose": "config", "type": "string", "description": "Observed: the id that <user-story persona-id> matches. Changing it re-renders the Stories section." }
|
|
20
22
|
],
|
|
21
23
|
"slots": [
|
|
22
|
-
{ "name": "name", "description": "Persona name (
|
|
23
|
-
{ "name": "
|
|
24
|
-
{ "name": "age", "description": "Age display (slot fallback for attribute)" },
|
|
25
|
-
{ "name": "location", "description": "Location display (slot fallback for attribute)" },
|
|
26
|
-
{ "name": "quote", "description": "Key quote (slot fallback for attribute)" },
|
|
24
|
+
{ "name": "name", "description": "Persona name (e.g. <h2>). Defaults to the name attribute, then \"Unnamed Persona\"." },
|
|
25
|
+
{ "name": "quote", "description": "Key quote (e.g. <p>). Defaults to the quote attribute." },
|
|
27
26
|
{ "name": "bio", "description": "Background narrative paragraph" },
|
|
28
27
|
{ "name": "goals", "description": "List of what the persona wants to achieve" },
|
|
29
28
|
{ "name": "frustrations", "description": "List of pain points and blockers" },
|
|
30
29
|
{ "name": "behaviors", "description": "List of observable patterns and habits" }
|
|
31
30
|
],
|
|
32
31
|
"events": [
|
|
33
|
-
{ "name": "persona-ready", "detail": "{ name, role }", "description": "
|
|
32
|
+
{ "name": "persona-ready", "detail": "{ name, role }", "description": "Once per element, after the first render. With src, after the fetched data renders (not fired if the fetch fails). Not re-fired by attribute changes or moves." },
|
|
33
|
+
{ "name": "user-persona:upgraded", "detail": "", "description": "Once, in a microtask after first connect. Wait for this before assigning .data from a framework." },
|
|
34
|
+
{ "name": "user-persona:data-changed", "detail": "{ data, source: 'property' }", "description": "After .data is assigned" }
|
|
34
35
|
]
|
|
35
36
|
}
|