@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,414 @@
1
+ /**
2
+ * <work-item> — General-purpose work unit card
3
+ *
4
+ * The engineering sibling of <user-story>. Represents tasks, bugs,
5
+ * chores, spikes, and features — work that may or may not map to
6
+ * a user story. Can live in kanban-board, gantt-chart, story-map,
7
+ * or standalone.
8
+ *
9
+ * Content slots:
10
+ * @slot title - Work item heading (e.g., <h3>)
11
+ * @slot description - Work item description (e.g., <p>)
12
+ * @slot checklist - Subtasks or checklist (e.g., <ul>)
13
+ * @slot notes - Additional notes (e.g., <p>)
14
+ *
15
+ * State/config attributes:
16
+ * @attr {string} item-id - Unique identifier (e.g., "PROJ-42")
17
+ * @attr {enum} type - task | bug | chore | spike | feature
18
+ * @attr {enum} priority - critical | high | medium | low
19
+ * @attr {enum} status - backlog | to-do | in-progress | review | done | blocked
20
+ * @attr {string} estimate - Effort estimate (points, hours, t-shirt size)
21
+ * @attr {string} assignee - Assignee name
22
+ * @attr {string} story-ids - Comma-separated linked story IDs
23
+ * @attr {string} detail - full | compact | minimal
24
+ * @attr {boolean} compact - Alias for detail="compact"
25
+ * @attr {string} src - URL to JSON data
26
+ *
27
+ * @fires work-item:ready - After render
28
+ * @fires work-item:status - When updateStatus() is called
29
+ * @fires work-item:priority - When updatePriority() is called
30
+ *
31
+ * @example
32
+ * <work-item
33
+ * item-id="PROJ-42"
34
+ * title="Implement search API"
35
+ * type="task"
36
+ * status="in-progress"
37
+ * priority="high"
38
+ * assignee="Sarah Chen"
39
+ * estimate="5"
40
+ * story-ids="PROJ-101,PROJ-102">
41
+ * <p slot="description">Build the /search endpoint with filters.</p>
42
+ * <ul slot="checklist">
43
+ * <li>Schema design</li>
44
+ * <li>Endpoint implementation</li>
45
+ * <li>Tests</li>
46
+ * </ul>
47
+ * </work-item>
48
+ */
49
+
50
+ import { styles } from './styles.js';
51
+ import { registerComponent } from '../../lib/bundle-registry.js';
52
+ import { esc, initials, hashColor, lucideSvg, UX_ICONS } from '../_ux-base.js';
53
+
54
+ /* ── Type icons (Lucide SVG fragments) ─────────────── */
55
+
56
+ const TYPE_ICONS = {
57
+ task: '<rect x="3" y="3" width="18" height="18" rx="2"/><path d="m9 12 2 2 4-4"/>',
58
+ bug: '<path d="m8 2 1.88 1.88"/><path d="M14.12 3.88 16 2"/><path d="M9 7.13v-1a3.003 3.003 0 1 1 6 0v1"/><path d="M12 20c-3.3 0-6-2.7-6-6v-3a4 4 0 0 1 4-4h4a4 4 0 0 1 4 4v3c0 3.3-2.7 6-6 6"/><path d="M12 20v-9"/><path d="M6.53 9C4.6 8.8 3 7.1 3 5"/><path d="M6 13H2"/><path d="M3 21c0-2.1 1.7-3.9 3.8-4"/><path d="M20.97 5c0 2.1-1.6 3.8-3.5 4"/><path d="M22 13h-4"/><path d="M17.2 17c2.1.1 3.8 1.9 3.8 4"/>',
59
+ chore: '<path d="M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.106-3.105c.32-.322.863-.22.983.218a6 6 0 0 1-8.259 7.057l-7.91 7.91a1 1 0 0 1-2.999-3l7.91-7.91a6 6 0 0 1 7.057-8.259c.438.12.54.662.219.984z"/>',
60
+ spike: '<path d="M15 14c.2-1 .7-1.7 1.5-2.5 1-.9 1.5-2.2 1.5-3.5A6 6 0 0 0 6 8c0 1 .2 2.2 1.5 3.5.7.7 1.3 1.5 1.5 2.5"/><path d="M9 18h6"/><path d="M10 22h4"/>',
61
+ feature: '<circle cx="12" cy="12" r="10"/><path d="m9 12 2 2 4-4"/>',
62
+ };
63
+
64
+ /* ── Static lookups ────────────────────────────────── */
65
+
66
+ const PRIORITIES = {
67
+ critical: { label: 'Critical', color: '#dc2626', bg: 'rgba(220, 38, 38, 0.1)' },
68
+ high: { label: 'High', color: '#ea580c', bg: 'rgba(234, 88, 12, 0.1)' },
69
+ medium: { label: 'Medium', color: '#ca8a04', bg: 'rgba(202, 138, 4, 0.1)' },
70
+ low: { label: 'Low', color: '#16a34a', bg: 'rgba(22, 163, 74, 0.1)' },
71
+ };
72
+
73
+ const STATUSES = {
74
+ backlog: { label: 'Backlog', color: '#6b7280', bg: 'rgba(107, 114, 128, 0.1)' },
75
+ 'to-do': { label: 'To Do', color: '#3b82f6', bg: 'rgba(59, 130, 246, 0.1)' },
76
+ 'in-progress': { label: 'In Progress', color: '#8b5cf6', bg: 'rgba(139, 92, 246, 0.1)' },
77
+ review: { label: 'Review', color: '#f59e0b', bg: 'rgba(245, 158, 11, 0.1)' },
78
+ done: { label: 'Done', color: '#22c55e', bg: 'rgba(34, 197, 94, 0.1)' },
79
+ blocked: { label: 'Blocked', color: '#dc2626', bg: 'rgba(220, 38, 38, 0.1)' },
80
+ };
81
+
82
+ /* ── Component ─────────────────────────────────────── */
83
+
84
+ class WorkItem extends HTMLElement {
85
+ static get observedAttributes() {
86
+ return [
87
+ 'item-id', 'type', 'priority', 'status',
88
+ 'estimate', 'assignee', 'story-ids', 'detail', 'compact', 'src',
89
+ ];
90
+ }
91
+
92
+ #slotCache = new Map();
93
+
94
+ constructor() {
95
+ super();
96
+ this.attachShadow({ mode: 'open' });
97
+ }
98
+
99
+ /* ── Slot caching ───────────────────────────────── */
100
+
101
+ #cacheSlotValues() {
102
+ for (const child of [...this.children]) {
103
+ const slotName = child.getAttribute('slot');
104
+ if (slotName) {
105
+ this.#slotCache.set(slotName, child.textContent.trim());
106
+ }
107
+ }
108
+ }
109
+
110
+ _resolve(attr) {
111
+ return this.getAttribute(attr) || this.#slotCache.get(attr) || '';
112
+ }
113
+
114
+ /* ── Lifecycle ─────────────────────────────────── */
115
+
116
+ connectedCallback() {
117
+ this.#cacheSlotValues();
118
+ if (this.itemId && !this.id) this.id = this.itemId;
119
+ if (this.hasAttribute('src')) {
120
+ this._loadSrc(this.getAttribute('src'));
121
+ }
122
+ this.#render();
123
+ this.setAttribute('data-upgraded', '');
124
+ }
125
+
126
+ disconnectedCallback() {
127
+ this.removeAttribute('data-upgraded');
128
+ }
129
+
130
+ attributeChangedCallback(name, oldValue, newValue) {
131
+ if (oldValue !== newValue && this.shadowRoot) {
132
+ if (name === 'src' && this.isConnected) {
133
+ this._loadSrc(newValue);
134
+ } else {
135
+ this.#render();
136
+ }
137
+ }
138
+ }
139
+
140
+ /* ── Getters ───────────────────────────────────── */
141
+
142
+ get itemId() { return this.getAttribute('item-id') || ''; }
143
+ get itemTitle() {
144
+ const slotted = this.querySelector('[slot="title"]');
145
+ return slotted?.textContent?.trim() || this.#slotCache.get('title') || '';
146
+ }
147
+ get itemType() { return this.getAttribute('type') || 'task'; }
148
+ get priority() { return this.getAttribute('priority') || 'medium'; }
149
+ get status() { return this.getAttribute('status') || 'backlog'; }
150
+ get estimate() { return this.getAttribute('estimate') || ''; }
151
+ get assignee() { return this.getAttribute('assignee') || ''; }
152
+
153
+ get storyIds() {
154
+ const raw = this.getAttribute('story-ids') || '';
155
+ return raw ? raw.split(',').map(s => s.trim()).filter(Boolean) : [];
156
+ }
157
+
158
+ get _detailLevel() {
159
+ if (this.getAttribute('detail')) return this.getAttribute('detail');
160
+ if (this.hasAttribute('compact')) return 'compact';
161
+ return 'full';
162
+ }
163
+
164
+ get _minimalLabel() {
165
+ return this.itemTitle || this.itemId || 'Work item';
166
+ }
167
+
168
+ /* ── Public API ────────────────────────────────── */
169
+
170
+ updateStatus(newStatus) {
171
+ if (STATUSES[newStatus]) {
172
+ this.setAttribute('status', newStatus);
173
+ this.dispatchEvent(new CustomEvent('work-item:status', {
174
+ detail: { status: newStatus, itemId: this.itemId },
175
+ bubbles: true, composed: true,
176
+ }));
177
+ }
178
+ }
179
+
180
+ updatePriority(newPriority) {
181
+ if (PRIORITIES[newPriority]) {
182
+ this.setAttribute('priority', newPriority);
183
+ this.dispatchEvent(new CustomEvent('work-item:priority', {
184
+ detail: { priority: newPriority, itemId: this.itemId },
185
+ bubbles: true, composed: true,
186
+ }));
187
+ }
188
+ }
189
+
190
+ /* ── Data API ──────────────────────────────────── */
191
+
192
+ /**
193
+ * Read the work item as a plain data object. Mirrors what a consumer
194
+ * would assign to .data — useful for diffing, persistence, or echo.
195
+ */
196
+ get data() {
197
+ return {
198
+ itemId: this.itemId || undefined,
199
+ type: this.itemType,
200
+ priority: this.priority,
201
+ status: this.status,
202
+ estimate: this.estimate || undefined,
203
+ assignee: this.assignee || undefined,
204
+ storyIds: this.storyIds.length ? this.storyIds : undefined,
205
+ detail: this.getAttribute('detail') || undefined,
206
+ title: this.itemTitle || undefined,
207
+ };
208
+ }
209
+
210
+ /**
211
+ * Set state attributes and slotted content from a plain object in one call.
212
+ * Replaces nine setAttribute calls + manual slotted-child creation.
213
+ * Idempotent for repeat calls.
214
+ */
215
+ set data(value) {
216
+ if (!value || typeof value !== 'object') return;
217
+ this._applyData(value);
218
+ if (this.shadowRoot) this.#render();
219
+ this.dispatchEvent(new CustomEvent('work-item:data-changed', {
220
+ detail: { data: this.data, source: 'property' },
221
+ bubbles: true, composed: true,
222
+ }));
223
+ }
224
+
225
+ /**
226
+ * Apply a data record to attributes + slotted children. Used by both the
227
+ * .data setter and async _loadSrc. No render or event emit — caller decides.
228
+ * @param {Record<string, unknown>} data
229
+ */
230
+ _applyData(data) {
231
+ for (const [jsonKey, attr] of [
232
+ ['itemId', 'item-id'], ['type', 'type'], ['priority', 'priority'],
233
+ ['status', 'status'], ['estimate', 'estimate'], ['assignee', 'assignee'],
234
+ ['detail', 'detail'],
235
+ ]) {
236
+ if (data[jsonKey] != null) this.setAttribute(attr, String(data[jsonKey]));
237
+ }
238
+ if (data.storyIds) {
239
+ this.setAttribute('story-ids', Array.isArray(data.storyIds) ? data.storyIds.join(',') : String(data.storyIds));
240
+ }
241
+
242
+ if (data.title && !this.querySelector('[slot="title"]')) {
243
+ const el = document.createElement('h3');
244
+ el.slot = 'title';
245
+ el.textContent = String(data.title);
246
+ this.appendChild(el);
247
+ }
248
+ for (const key of ['description', 'notes']) {
249
+ if (data[key] && !this.querySelector(`[slot="${key}"]`)) {
250
+ const el = document.createElement('p');
251
+ el.slot = key;
252
+ el.textContent = String(data[key]);
253
+ this.appendChild(el);
254
+ }
255
+ }
256
+ if (data.checklist && !this.querySelector('[slot="checklist"]')) {
257
+ const ul = document.createElement('ul');
258
+ ul.slot = 'checklist';
259
+ const items = Array.isArray(data.checklist) ? data.checklist : [data.checklist];
260
+ for (const item of items) {
261
+ const li = document.createElement('li');
262
+ li.textContent = String(item);
263
+ ul.appendChild(li);
264
+ }
265
+ this.appendChild(ul);
266
+ }
267
+ }
268
+
269
+ /* ── JSON loading ──────────────────────────────── */
270
+
271
+ async _loadSrc(url) {
272
+ if (!url) return;
273
+ const root = /** @type {ShadowRoot} */ (this.shadowRoot);
274
+ root.innerHTML = `<style>${styles}</style><div class="state-msg">Loading\u2026</div>`;
275
+ try {
276
+ const res = await fetch(url);
277
+ if (!res.ok) throw new Error(`HTTP ${res.status}`);
278
+ const data = await res.json();
279
+ this._applyData(data);
280
+ this.#render();
281
+ } catch (err) {
282
+ root.innerHTML = `<style>${styles}</style>` +
283
+ `<div class="state-msg state-msg--error">Could not load: ${esc(/** @type {Error} */ (err).message)}</div>`;
284
+ }
285
+ }
286
+
287
+ /* ── Render ────────────────────────────────────── */
288
+
289
+ #render() {
290
+ const root = /** @type {ShadowRoot} */ (this.shadowRoot);
291
+ const priorityInfo = PRIORITIES[this.priority] || PRIORITIES.medium;
292
+ const statusInfo = STATUSES[this.status] || STATUSES.backlog;
293
+ const type = this.itemType;
294
+ const typeIcon = TYPE_ICONS[type] || TYPE_ICONS.task;
295
+ const level = this._detailLevel;
296
+ const ariaLabel = this.itemId ? `Work item: ${esc(this.itemId)}` : 'Work item';
297
+
298
+ if (level === 'minimal') {
299
+ root.innerHTML = `<style>${styles}</style>
300
+ <article class="wi-card wi-card--minimal" role="article" aria-label="${ariaLabel}"
301
+ tabindex="0">
302
+ <div class="wi-body">
303
+ <div class="wi-meta">
304
+ ${this.itemId ? `<span class="wi-id">${esc(this.itemId)}</span>` : ''}
305
+ <span class="wi-type" data-type="${esc(type)}">${lucideSvg(typeIcon)} ${esc(type)}</span>
306
+ </div>
307
+ <div class="wi-title-wrap">
308
+ <slot name="title"><span class="wi-title-fallback">${esc(this._minimalLabel)}</span></slot>
309
+ </div>
310
+ </div>
311
+ </article>`;
312
+ return;
313
+ }
314
+
315
+ root.innerHTML = `<style>${styles}</style>
316
+ <article class="wi-card wi-card--${level}" role="article" aria-label="${ariaLabel}">
317
+
318
+ <header class="wi-header">
319
+ <div class="wi-meta">
320
+ ${this.itemId ? `<span class="wi-id">${esc(this.itemId)}</span>` : ''}
321
+ <span class="wi-type" data-type="${esc(type)}">${lucideSvg(typeIcon)} ${esc(type)}</span>
322
+ </div>
323
+ <div class="wi-badges">
324
+ <span class="wi-priority" style="color:${priorityInfo.color};background:${priorityInfo.bg}">${esc(priorityInfo.label)}</span>
325
+ <span class="wi-status" style="color:${statusInfo.color};background:${statusInfo.bg}">${esc(statusInfo.label)}</span>
326
+ ${this.estimate ? `<span class="wi-estimate" title="Estimate">${esc(this.estimate)}</span>` : ''}
327
+ </div>
328
+ </header>
329
+
330
+ <div class="wi-body">
331
+ <div class="wi-title-wrap">
332
+ <slot name="title"><span class="wi-title-fallback">[Untitled work item]</span></slot>
333
+ </div>
334
+
335
+ ${this.assignee ? `
336
+ <div class="wi-assignee">
337
+ <span class="wi-assignee__avatar" style="background:${hashColor(this.assignee)}">${initials(this.assignee)}</span>
338
+ ${esc(this.assignee)}
339
+ </div>` : ''}
340
+
341
+ ${this.storyIds.length ? `
342
+ <div class="wi-links">
343
+ ${this.storyIds.map(id =>
344
+ `<a class="wi-link" href="#${esc(id)}">${lucideSvg('<rect x="3" y="3" width="18" height="18" rx="2"/><path d="M7 7h10"/><path d="M7 12h10"/><path d="M7 17h10"/>')} ${esc(id)}</a>`
345
+ ).join('')}
346
+ </div>` : ''}
347
+ </div>
348
+
349
+ <div class="wi-sections">
350
+ <div class="wi-section">
351
+ <div class="wi-section-header">
352
+ <div class="wi-section-icon description">
353
+ <svg viewBox="0 0 24 24"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/></svg>
354
+ </div>
355
+ <span class="wi-section-title">Description</span>
356
+ </div>
357
+ <div class="wi-section-content">
358
+ <slot name="description"><em class="slot-fallback">No description.</em></slot>
359
+ </div>
360
+ </div>
361
+
362
+ <div class="wi-section">
363
+ <div class="wi-section-header">
364
+ <div class="wi-section-icon checklist">
365
+ <svg viewBox="0 0 24 24"><path d="M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z"/></svg>
366
+ </div>
367
+ <span class="wi-section-title">Checklist</span>
368
+ </div>
369
+ <div class="wi-section-content">
370
+ <slot name="checklist"><em class="slot-fallback">No checklist items.</em></slot>
371
+ </div>
372
+ </div>
373
+
374
+ <div class="wi-section">
375
+ <div class="wi-section-header">
376
+ <div class="wi-section-icon notes">
377
+ <svg viewBox="0 0 24 24"><path d="M3 17.25V21h3.75L17.81 9.94l-3.75-3.75L3 17.25zM20.71 7.04c.39-.39.39-1.02 0-1.41l-2.34-2.34c-.39-.39-1.02-.39-1.41 0l-1.83 1.83 3.75 3.75 1.83-1.83z"/></svg>
378
+ </div>
379
+ <span class="wi-section-title">Notes</span>
380
+ </div>
381
+ <div class="wi-section-content">
382
+ <slot name="notes"><em class="slot-fallback">No notes.</em></slot>
383
+ </div>
384
+ </div>
385
+ </div>
386
+
387
+ </article>`;
388
+
389
+ // Compact: mark empty slot sections
390
+ if (level === 'compact') {
391
+ for (const section of root.querySelectorAll('.wi-section')) {
392
+ const slot = section.querySelector('slot');
393
+ if (slot && slot.assignedNodes().length === 0) {
394
+ section.setAttribute('data-empty', '');
395
+ }
396
+ }
397
+ }
398
+
399
+ this.dispatchEvent(new CustomEvent('work-item:ready', {
400
+ detail: {
401
+ itemId: this.itemId,
402
+ title: this.itemTitle,
403
+ type,
404
+ priority: this.priority,
405
+ status: this.status,
406
+ },
407
+ bubbles: true, composed: true,
408
+ }));
409
+ }
410
+ }
411
+
412
+ registerComponent('work-item', WorkItem);
413
+
414
+ export { WorkItem };
@@ -0,0 +1,24 @@
1
+ <!--
2
+ work-item — static (zero-JS) form
3
+ Slotted content renders as children before upgrade.
4
+ Title is content (slot), state uses attributes.
5
+ -->
6
+ <work-item
7
+ item-id="PROJ-42"
8
+ type="task"
9
+ priority="high"
10
+ status="in-progress"
11
+ assignee="Sarah Chen"
12
+ estimate="5"
13
+ story-ids="PROJ-101,PROJ-102">
14
+ <h3 slot="title">Implement search API endpoint</h3>
15
+ <p slot="description">Build the /api/search endpoint with full-text search, filters for status and assignee, and pagination. Must support both JSON and CSV response formats.</p>
16
+ <ul slot="checklist">
17
+ <li>Database schema for search index</li>
18
+ <li>Endpoint implementation with filters</li>
19
+ <li>Pagination and cursor support</li>
20
+ <li>CSV export format</li>
21
+ <li>Integration tests</li>
22
+ </ul>
23
+ <p slot="notes">Depends on the database migration in PROJ-38. Coordinate with Tom on the index strategy.</p>
24
+ </work-item>