@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
|
@@ -5,15 +5,22 @@
|
|
|
5
5
|
* sections for bio, goals, frustrations, and behaviors. Uses Shadow DOM
|
|
6
6
|
* for encapsulation and VB design tokens for theming.
|
|
7
7
|
*
|
|
8
|
-
*
|
|
8
|
+
* Name and quote come from the `name` / `quote` slots; the `name` / `quote`
|
|
9
|
+
* attributes are used when no slot child is present (a slot always wins).
|
|
10
|
+
*
|
|
11
|
+
* @attr {string} name - Persona name when there is no slot="name" child
|
|
9
12
|
* @attr {string} role - Job title or role
|
|
10
13
|
* @attr {string} age - Age display
|
|
11
14
|
* @attr {string} location - Location display
|
|
12
15
|
* @attr {string} avatar - Optional avatar image URL
|
|
13
|
-
* @attr {string} quote - Key quote
|
|
16
|
+
* @attr {string} quote - Key quote when there is no slot="quote" child
|
|
14
17
|
* @attr {boolean} compact - Reduced spacing variant
|
|
18
|
+
* @attr {string} src - URL to a JSON persona record (fetched once)
|
|
19
|
+
* @attr {boolean} data-list-stories - List <user-story persona-id> matching this id
|
|
15
20
|
*
|
|
16
|
-
* @fires persona-ready
|
|
21
|
+
* @fires persona-ready - Once, after the first render (after the src data renders when src is set), detail: { name, role }
|
|
22
|
+
* @fires user-persona:upgraded - Once, after first connect
|
|
23
|
+
* @fires user-persona:data-changed - After .data assignment, detail: { data, source: 'property' }
|
|
17
24
|
*
|
|
18
25
|
* @example
|
|
19
26
|
* <user-persona name="Sarah Chen" role="Product Manager" age="34"
|
|
@@ -27,15 +34,11 @@
|
|
|
27
34
|
*/
|
|
28
35
|
import { styles } from './styles.js';
|
|
29
36
|
import { registerComponent } from '../../lib/bundle-registry.js';
|
|
30
|
-
import {
|
|
37
|
+
import { cssUrl } from '../../lib/escape.js';
|
|
38
|
+
import { esc, initials, hashColor, UXShadowElement, fillSlot, readSlot, slottedChild } from '../_ux-base.js';
|
|
31
39
|
import { portraitUrl } from '../../lib/portrait-url.js';
|
|
40
|
+
import { stringHash as hash } from '../../lib/hash.js';
|
|
32
41
|
|
|
33
|
-
/** Simple string hash → integer (same algorithm as portrait-url.js). */
|
|
34
|
-
function hash(str) {
|
|
35
|
-
let h = 0;
|
|
36
|
-
for (let i = 0; i < str.length; i++) h = str.charCodeAt(i) + ((h << 5) - h);
|
|
37
|
-
return h;
|
|
38
|
-
}
|
|
39
42
|
|
|
40
43
|
// ── Mock persona corpus (~1 KB) ───────────────────────────────────────
|
|
41
44
|
const MOCK_NAMES = [
|
|
@@ -82,20 +85,19 @@ const MOCK_BEHAVIORS = [
|
|
|
82
85
|
'Bookmarks frequently used reports', 'Uses dark mode exclusively',
|
|
83
86
|
];
|
|
84
87
|
|
|
85
|
-
|
|
88
|
+
/** Text sections that `.data` / `src` turn into slotted children. */
|
|
89
|
+
const SECTION_SLOTS = ['bio', 'goals', 'frustrations', 'behaviors'];
|
|
90
|
+
|
|
91
|
+
class UserPersona extends UXShadowElement {
|
|
92
|
+
static readyEvent = 'persona-ready';
|
|
93
|
+
|
|
86
94
|
static get observedAttributes() {
|
|
87
|
-
return ['role', 'age', 'location', 'avatar', 'compact', 'src', 'data-list-stories', 'id'];
|
|
95
|
+
return ['name', 'role', 'age', 'location', 'avatar', 'quote', 'compact', 'src', 'data-list-stories', 'id'];
|
|
88
96
|
}
|
|
89
97
|
|
|
90
|
-
#slotCache = new Map();
|
|
91
98
|
/** @type {MutationObserver | null} */
|
|
92
99
|
#storyObserver = null;
|
|
93
100
|
|
|
94
|
-
constructor() {
|
|
95
|
-
super();
|
|
96
|
-
this.attachShadow({ mode: 'open' });
|
|
97
|
-
}
|
|
98
|
-
|
|
99
101
|
/**
|
|
100
102
|
* Find every <user-story> in the document whose `persona-id` matches
|
|
101
103
|
* this persona's id. Returns elements in document order.
|
|
@@ -108,24 +110,12 @@ class UserPersona extends HTMLElement {
|
|
|
108
110
|
return Array.from(scope.querySelectorAll(`user-story[persona-id="${cssEscape(this.id)}"]:not([data-detail-clone])`));
|
|
109
111
|
}
|
|
110
112
|
|
|
111
|
-
#cacheSlotValues() {
|
|
112
|
-
for (const child of [...this.children]) {
|
|
113
|
-
const slotName = child.getAttribute('slot');
|
|
114
|
-
if (slotName && !this.getAttribute(slotName)) {
|
|
115
|
-
this.#slotCache.set(slotName, child.textContent.trim());
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
_resolve(attr) {
|
|
121
|
-
return this.getAttribute(attr) || this.#slotCache.get(attr) || '';
|
|
122
|
-
}
|
|
123
|
-
|
|
124
113
|
/* ── Data API ──────────────────────────────────── */
|
|
125
114
|
|
|
126
115
|
/**
|
|
127
116
|
* Read the persona as a plain data object. Mirrors what a consumer
|
|
128
|
-
* would assign to .data — useful for diffing or persistence.
|
|
117
|
+
* would assign to .data — useful for diffing or persistence. List
|
|
118
|
+
* sections (`<ul>`/`<ol>`) read back as string arrays, others as text.
|
|
129
119
|
*/
|
|
130
120
|
get data() {
|
|
131
121
|
return {
|
|
@@ -135,89 +125,67 @@ class UserPersona extends HTMLElement {
|
|
|
135
125
|
location: this.location || undefined,
|
|
136
126
|
avatar: this.avatar || undefined,
|
|
137
127
|
quote: this.quote || undefined,
|
|
138
|
-
bio: this
|
|
139
|
-
goals: this
|
|
140
|
-
frustrations: this
|
|
141
|
-
behaviors: this
|
|
128
|
+
bio: readSlot(this, 'bio'),
|
|
129
|
+
goals: readSlot(this, 'goals'),
|
|
130
|
+
frustrations: readSlot(this, 'frustrations'),
|
|
131
|
+
behaviors: readSlot(this, 'behaviors'),
|
|
142
132
|
};
|
|
143
133
|
}
|
|
144
134
|
|
|
145
135
|
/**
|
|
146
136
|
* Set state attributes and slotted content from a plain object in one
|
|
147
|
-
* call.
|
|
137
|
+
* call. Fields present in the object replace the current value (slotted
|
|
138
|
+
* children are updated in place); absent fields are left alone.
|
|
139
|
+
* Emits user-persona:data-changed { data, source: 'property' }.
|
|
148
140
|
*/
|
|
149
141
|
set data(value) {
|
|
150
|
-
|
|
151
|
-
this._applyData(value);
|
|
152
|
-
if (this.shadowRoot) this.#render();
|
|
153
|
-
this.dispatchEvent(new CustomEvent('user-persona:data-changed', {
|
|
154
|
-
detail: { data: this.data, source: 'property' },
|
|
155
|
-
bubbles: true, composed: true,
|
|
156
|
-
}));
|
|
142
|
+
this._setData(value);
|
|
157
143
|
}
|
|
158
144
|
|
|
159
145
|
/**
|
|
160
146
|
* Apply a data record to attributes + slotted children. Used by both
|
|
161
|
-
* the .data setter and
|
|
147
|
+
* the .data setter and the src loader. String fields become `<p>`
|
|
148
|
+
* children, arrays become `<ul>` lists; the name is an `<h2>`.
|
|
162
149
|
* @param {Record<string, unknown>} data
|
|
163
150
|
*/
|
|
164
151
|
_applyData(data) {
|
|
165
152
|
for (const key of ['role', 'age', 'location', 'avatar']) {
|
|
166
|
-
if (data[key]) this.setAttribute(key, String(data[key]));
|
|
167
|
-
}
|
|
168
|
-
if (data.name && !this.querySelector('[slot="name"]')) {
|
|
169
|
-
const el = document.createElement('h2');
|
|
170
|
-
el.slot = 'name';
|
|
171
|
-
el.textContent = String(data.name);
|
|
172
|
-
this.appendChild(el);
|
|
153
|
+
if (data[key] != null && data[key] !== '') this.setAttribute(key, String(data[key]));
|
|
173
154
|
}
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
this.appendChild(el);
|
|
179
|
-
}
|
|
180
|
-
for (const key of ['bio', 'goals', 'frustrations', 'behaviors']) {
|
|
181
|
-
if (data[key]) this.#slotCache.set(key, String(data[key]));
|
|
155
|
+
fillSlot(this, 'name', data.name, { tag: 'h2', replace: true });
|
|
156
|
+
fillSlot(this, 'quote', data.quote, { tag: 'p', replace: true });
|
|
157
|
+
for (const key of SECTION_SLOTS) {
|
|
158
|
+
fillSlot(this, key, data[key], { tag: 'p', replace: true });
|
|
182
159
|
}
|
|
183
160
|
}
|
|
184
161
|
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
190
|
-
const data = await res.json();
|
|
191
|
-
this._applyData(data);
|
|
192
|
-
this.#render();
|
|
193
|
-
} catch (err) {
|
|
194
|
-
console.warn(`[user-persona] Failed to load src="${url}":`, err);
|
|
195
|
-
}
|
|
162
|
+
/* ── Lifecycle (see UXShadowElement) ──────────── */
|
|
163
|
+
|
|
164
|
+
_setup() {
|
|
165
|
+
if (this.hasAttribute('data-mock')) this.#applyMock();
|
|
196
166
|
}
|
|
197
167
|
|
|
198
|
-
|
|
199
|
-
this.#cacheSlotValues();
|
|
200
|
-
if (this.hasAttribute('data-mock')) {
|
|
201
|
-
this.#applyMock();
|
|
202
|
-
} else if (this.hasAttribute('src')) {
|
|
203
|
-
this._loadSrc(this.getAttribute('src'));
|
|
204
|
-
}
|
|
205
|
-
this.#render();
|
|
168
|
+
_connect() {
|
|
206
169
|
this.#syncStoryObserver();
|
|
207
|
-
this.
|
|
208
|
-
this
|
|
209
|
-
bubbles: true,
|
|
210
|
-
composed: true,
|
|
211
|
-
detail: { name: this.personaName, role: this.personaRole }
|
|
212
|
-
}));
|
|
170
|
+
// Stories may have been added or removed while this card was detached.
|
|
171
|
+
this.#renderStoriesSection();
|
|
213
172
|
}
|
|
214
173
|
|
|
215
|
-
|
|
216
|
-
this.removeAttribute('data-upgraded');
|
|
174
|
+
_disconnect() {
|
|
217
175
|
this.#storyObserver?.disconnect();
|
|
218
176
|
this.#storyObserver = null;
|
|
219
177
|
}
|
|
220
178
|
|
|
179
|
+
/** @param {string} name */
|
|
180
|
+
_attributeEffects(name) {
|
|
181
|
+
// Also runs after a .data / src record sets one of these.
|
|
182
|
+
if (name === 'data-list-stories' || name === 'id') this.#syncStoryObserver();
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
_readyDetail() {
|
|
186
|
+
return { name: this.personaName, role: this.personaRole };
|
|
187
|
+
}
|
|
188
|
+
|
|
221
189
|
/**
|
|
222
190
|
* Watch the document for added/removed/relabeled <user-story> elements
|
|
223
191
|
* so the auto-rendered Stories section stays current. Only runs when
|
|
@@ -260,7 +228,7 @@ class UserPersona extends HTMLElement {
|
|
|
260
228
|
<div class="section-icon stories" aria-hidden="true">
|
|
261
229
|
<svg viewBox="0 0 24 24"><path d="M14 2H6c-1.1 0-1.99.9-1.99 2L4 20c0 1.1.89 2 1.99 2H18c1.1 0 2-.9 2-2V8l-6-6zm2 16H8v-2h8v2zm0-4H8v-2h8v2zm-3-5V3.5L18.5 9H13z"/></svg>
|
|
262
230
|
</div>
|
|
263
|
-
<
|
|
231
|
+
<h3 class="section-title">Stories <span class="section-count">${stories.length}</span></h3>
|
|
264
232
|
</div>
|
|
265
233
|
<div class="section-content">
|
|
266
234
|
${stories.length === 0
|
|
@@ -285,24 +253,11 @@ class UserPersona extends HTMLElement {
|
|
|
285
253
|
`;
|
|
286
254
|
}
|
|
287
255
|
|
|
288
|
-
attributeChangedCallback(name, oldValue, newValue) {
|
|
289
|
-
if (oldValue === newValue || !this.shadowRoot) return;
|
|
290
|
-
if (name === 'src' && this.isConnected) {
|
|
291
|
-
this._loadSrc(newValue);
|
|
292
|
-
return;
|
|
293
|
-
}
|
|
294
|
-
if (name === 'data-list-stories' || name === 'id') {
|
|
295
|
-
this.#syncStoryObserver();
|
|
296
|
-
}
|
|
297
|
-
this.#render();
|
|
298
|
-
}
|
|
299
|
-
|
|
300
256
|
// ── Attribute getters ──────────────────────────────────────────────
|
|
301
257
|
|
|
302
|
-
/**
|
|
258
|
+
/** Name from the slotted heading, else the `name` attribute. */
|
|
303
259
|
get personaName() {
|
|
304
|
-
|
|
305
|
-
return slotted?.textContent?.trim() || this.#slotCache.get('name') || 'Unnamed Persona';
|
|
260
|
+
return this._slotText('name') || this.getAttribute('name')?.trim() || 'Unnamed Persona';
|
|
306
261
|
}
|
|
307
262
|
|
|
308
263
|
get personaRole() {
|
|
@@ -321,10 +276,9 @@ class UserPersona extends HTMLElement {
|
|
|
321
276
|
return this.getAttribute('avatar') || '';
|
|
322
277
|
}
|
|
323
278
|
|
|
324
|
-
/**
|
|
279
|
+
/** Quote from the slotted element, else the `quote` attribute. */
|
|
325
280
|
get quote() {
|
|
326
|
-
|
|
327
|
-
return slotted?.textContent?.trim() || this.#slotCache.get('quote') || '';
|
|
281
|
+
return this._slotText('quote') || this.getAttribute('quote')?.trim() || '';
|
|
328
282
|
}
|
|
329
283
|
|
|
330
284
|
get compact() {
|
|
@@ -345,7 +299,7 @@ class UserPersona extends HTMLElement {
|
|
|
345
299
|
};
|
|
346
300
|
|
|
347
301
|
// Name and quote are slotted content, not attributes
|
|
348
|
-
if (!this.
|
|
302
|
+
if (!slottedChild(this, 'name') && !this.getAttribute('name')) {
|
|
349
303
|
const h2 = document.createElement('h2');
|
|
350
304
|
h2.slot = 'name';
|
|
351
305
|
h2.textContent = pick(MOCK_NAMES);
|
|
@@ -355,10 +309,10 @@ class UserPersona extends HTMLElement {
|
|
|
355
309
|
if (!this.getAttribute('age')) this.setAttribute('age', String(25 + ((hash(seed) % 30) + 30) % 30));
|
|
356
310
|
if (!this.getAttribute('location')) this.setAttribute('location', pick(MOCK_LOCATIONS));
|
|
357
311
|
if (!this.getAttribute('avatar')) {
|
|
358
|
-
const name = this.
|
|
312
|
+
const name = this.personaName;
|
|
359
313
|
this.setAttribute('avatar', portraitUrl(name, 256));
|
|
360
314
|
}
|
|
361
|
-
if (!this.
|
|
315
|
+
if (!slottedChild(this, 'quote') && !this.getAttribute('quote')) {
|
|
362
316
|
const p = document.createElement('p');
|
|
363
317
|
p.slot = 'quote';
|
|
364
318
|
p.textContent = pick(MOCK_QUOTES);
|
|
@@ -367,7 +321,7 @@ class UserPersona extends HTMLElement {
|
|
|
367
321
|
|
|
368
322
|
// Inject slotted sections as light DOM so native <slot> picks them up
|
|
369
323
|
const injectList = (slotName, items) => {
|
|
370
|
-
if (this
|
|
324
|
+
if (slottedChild(this, slotName)) return;
|
|
371
325
|
const ul = document.createElement('ul');
|
|
372
326
|
ul.setAttribute('slot', slotName);
|
|
373
327
|
for (const item of items) {
|
|
@@ -384,7 +338,7 @@ class UserPersona extends HTMLElement {
|
|
|
384
338
|
|
|
385
339
|
// ── Render ─────────────────────────────────────────────────────────
|
|
386
340
|
|
|
387
|
-
|
|
341
|
+
_render() {
|
|
388
342
|
const name = this.personaName;
|
|
389
343
|
const role = this.personaRole;
|
|
390
344
|
const age = this.age;
|
|
@@ -394,7 +348,7 @@ class UserPersona extends HTMLElement {
|
|
|
394
348
|
const avatarBg = hashColor(name);
|
|
395
349
|
|
|
396
350
|
const avatarStyle = avatarUrl
|
|
397
|
-
? `background
|
|
351
|
+
? `background:${cssUrl(avatarUrl)} center/cover`
|
|
398
352
|
: `background:${avatarBg}`;
|
|
399
353
|
|
|
400
354
|
/** @type {ShadowRoot} */ (this.shadowRoot).innerHTML = `
|
|
@@ -430,7 +384,7 @@ class UserPersona extends HTMLElement {
|
|
|
430
384
|
</div>
|
|
431
385
|
</header>
|
|
432
386
|
|
|
433
|
-
${quote || this
|
|
387
|
+
${quote || slottedChild(this, 'quote') ? `
|
|
434
388
|
<div class="persona-quote" part="quote">
|
|
435
389
|
<span class="quote-mark" aria-hidden="true">“</span>
|
|
436
390
|
<div class="quote-text-wrap"><slot name="quote"><p class="quote-text">${esc(quote)}</p></slot></div>
|
|
@@ -443,7 +397,7 @@ class UserPersona extends HTMLElement {
|
|
|
443
397
|
<div class="section-icon bio" aria-hidden="true">
|
|
444
398
|
<svg viewBox="0 0 24 24"><path d="M12 12c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm0 2c-2.67 0-8 1.34-8 4v2h16v-2c0-2.66-5.33-4-8-4z"/></svg>
|
|
445
399
|
</div>
|
|
446
|
-
<
|
|
400
|
+
<h3 class="section-title">Background</h3>
|
|
447
401
|
</div>
|
|
448
402
|
<div class="section-content">
|
|
449
403
|
<slot name="bio">No background information provided.</slot>
|
|
@@ -455,7 +409,7 @@ class UserPersona extends HTMLElement {
|
|
|
455
409
|
<div class="section-icon goals" aria-hidden="true">
|
|
456
410
|
<svg viewBox="0 0 24 24"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-2 15l-5-5 1.41-1.41L10 14.17l7.59-7.59L19 8l-9 9z"/></svg>
|
|
457
411
|
</div>
|
|
458
|
-
<
|
|
412
|
+
<h3 class="section-title">Goals</h3>
|
|
459
413
|
</div>
|
|
460
414
|
<div class="section-content">
|
|
461
415
|
<slot name="goals">No goals specified.</slot>
|
|
@@ -467,7 +421,7 @@ class UserPersona extends HTMLElement {
|
|
|
467
421
|
<div class="section-icon frustrations" aria-hidden="true">
|
|
468
422
|
<svg viewBox="0 0 24 24"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1 15h-2v-2h2v2zm0-4h-2V7h2v6z"/></svg>
|
|
469
423
|
</div>
|
|
470
|
-
<
|
|
424
|
+
<h3 class="section-title">Frustrations</h3>
|
|
471
425
|
</div>
|
|
472
426
|
<div class="section-content">
|
|
473
427
|
<slot name="frustrations">No frustrations listed.</slot>
|
|
@@ -479,7 +433,7 @@ class UserPersona extends HTMLElement {
|
|
|
479
433
|
<div class="section-icon behaviors" aria-hidden="true">
|
|
480
434
|
<svg viewBox="0 0 24 24"><path d="M19 3H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm-5 14H7v-2h7v2zm3-4H7v-2h10v2zm0-4H7V7h10v2z"/></svg>
|
|
481
435
|
</div>
|
|
482
|
-
<
|
|
436
|
+
<h3 class="section-title">Behaviors</h3>
|
|
483
437
|
</div>
|
|
484
438
|
<div class="section-content">
|
|
485
439
|
<slot name="behaviors">No behaviors documented.</slot>
|
|
@@ -8,28 +8,30 @@
|
|
|
8
8
|
"permittedContent": ["@flow"]
|
|
9
9
|
},
|
|
10
10
|
"attributes": [
|
|
11
|
-
{ "name": "persona-id", "kind": "host-api", "purpose": "config", "type": "string", "description": "Links to a user-persona element by id" },
|
|
12
|
-
{ "name": "priority", "kind": "host-api", "purpose": "config", "type": "enum", "values": ["critical", "high", "medium", "low"], "description": "Story priority level" },
|
|
13
|
-
{ "name": "status", "kind": "host-api", "purpose": "config", "type": "enum", "values": ["backlog", "to-do", "in-progress", "review", "done"], "description": "Workflow status" },
|
|
14
|
-
{ "name": "points", "kind": "host-api", "purpose": "config", "type": "string", "description": "Story point estimate" },
|
|
11
|
+
{ "name": "persona-id", "kind": "host-api", "purpose": "config", "type": "string", "description": "Links to a user-persona element by id. Without a persona slot, the \"As a…\" text uses that persona's role (live-updating)." },
|
|
12
|
+
{ "name": "priority", "kind": "host-api", "purpose": "config", "type": "enum", "values": ["critical", "high", "medium", "low"], "default": "medium", "description": "Story priority level" },
|
|
13
|
+
{ "name": "status", "kind": "host-api", "purpose": "config", "type": "enum", "values": ["backlog", "to-do", "in-progress", "review", "done"], "default": "backlog", "description": "Workflow status" },
|
|
14
|
+
{ "name": "points", "kind": "host-api", "purpose": "config", "type": "string", "description": "Story point estimate (badge reads \"[n] story points\" to screen readers)" },
|
|
15
15
|
{ "name": "epic", "kind": "host-api", "purpose": "config", "type": "string", "description": "Parent epic label" },
|
|
16
|
-
{ "name": "story-id", "kind": "host-api", "purpose": "config", "type": "string", "description": "Ticket or story identifier
|
|
17
|
-
{ "name": "detail", "kind": "host-api", "purpose": "config", "type": "enum", "values": ["full", "compact", "minimal"], "description": "Detail level" },
|
|
16
|
+
{ "name": "story-id", "kind": "host-api", "purpose": "config", "type": "string", "description": "Ticket or story identifier; sets the HTML id on first connect when none is set" },
|
|
17
|
+
{ "name": "detail", "kind": "host-api", "purpose": "config", "type": "enum", "values": ["full", "compact", "minimal"], "default": "full", "description": "Detail level. minimal renders a button that opens the full story in a dialog." },
|
|
18
18
|
{ "name": "compact", "kind": "host-api", "purpose": "config", "type": "boolean", "description": "Alias for detail=compact" },
|
|
19
|
-
{ "name": "src", "kind": "host-api", "purpose": "config", "type": "string", "description": "URL
|
|
19
|
+
{ "name": "src", "kind": "host-api", "purpose": "config", "type": "string", "description": "URL of a JSON story record. Fetched once per value; a newer value aborts the older request." }
|
|
20
20
|
],
|
|
21
21
|
"slots": [
|
|
22
22
|
{ "name": "persona", "description": "The 'As a...' role (e.g., <span>)" },
|
|
23
23
|
{ "name": "action", "description": "The 'I want...' description (e.g., <span>)" },
|
|
24
24
|
{ "name": "benefit", "description": "The 'so that...' outcome (e.g., <span>)" },
|
|
25
|
-
{ "name": "title", "description": "Short label for minimal mode (e.g., <h3>)" },
|
|
25
|
+
{ "name": "title", "description": "Short label for minimal mode and the detail dialog heading (e.g., <h3>)" },
|
|
26
26
|
{ "name": "acceptance-criteria", "description": "Acceptance criteria checklist (e.g., <ul>)" },
|
|
27
27
|
{ "name": "tasks", "description": "Implementation task list (e.g., <ul>)" },
|
|
28
28
|
{ "name": "notes", "description": "Additional context (e.g., <p>)" }
|
|
29
29
|
],
|
|
30
30
|
"events": [
|
|
31
|
-
{ "name": "story-ready", "detail": "{ id, persona, action, benefit, priority, status, points }", "description": "
|
|
32
|
-
{ "name": "status-changed", "detail": "{ status, storyId }", "description": "Fired when updateStatus() is called" },
|
|
33
|
-
{ "name": "priority-changed", "detail": "{ priority, storyId }", "description": "Fired when updatePriority() is called" }
|
|
31
|
+
{ "name": "story-ready", "detail": "{ id, persona, action, benefit, priority, status, points }", "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." },
|
|
32
|
+
{ "name": "status-changed", "detail": "{ status, storyId }", "description": "Fired when updateStatus() is called with a known status" },
|
|
33
|
+
{ "name": "priority-changed", "detail": "{ priority, storyId }", "description": "Fired when updatePriority() is called with a known priority" },
|
|
34
|
+
{ "name": "user-story:upgraded", "detail": "", "description": "Once, in a microtask after first connect. Wait for this before assigning .data from a framework." },
|
|
35
|
+
{ "name": "user-story:data-changed", "detail": "{ data, source: 'property' }", "description": "After .data is assigned" }
|
|
34
36
|
]
|
|
35
37
|
}
|