@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.
- package/LICENSE +21 -0
- package/README.md +93 -0
- package/dist/vb-project-planning.css +1603 -0
- package/dist/vb-project-planning.js +11138 -0
- package/dist/vb-project-planning.min.css +1 -0
- package/dist/vb-project-planning.min.js +3312 -0
- package/package.json +80 -0
- package/src/index.css +14 -0
- package/src/index.js +26 -0
- package/src/lib/bundle-registry.js +156 -0
- package/src/lib/diff-by-key.js +86 -0
- package/src/lib/portrait-url.js +87 -0
- package/src/lib/vb-collection.js +122 -0
- package/src/lib/vb-element.js +96 -0
- package/src/lib/vb-store.js +200 -0
- package/src/lib/vb-view-transition.js +44 -0
- package/src/utils/copy-init.js +287 -0
- package/src/web-components/_ux-base.js +106 -0
- package/src/web-components/adr-wc/README.md +77 -0
- package/src/web-components/adr-wc/api.json +26 -0
- package/src/web-components/adr-wc/logic.js +376 -0
- package/src/web-components/adr-wc/static.html +17 -0
- package/src/web-components/adr-wc/styles.js +338 -0
- package/src/web-components/burndown-chart/api.json +74 -0
- package/src/web-components/burndown-chart/logic.js +198 -0
- package/src/web-components/burndown-chart/static.html +31 -0
- package/src/web-components/burndown-chart/styles.css +59 -0
- package/src/web-components/capacity-plan/_capacity-utils.js +56 -0
- package/src/web-components/capacity-plan/api.json +20 -0
- package/src/web-components/capacity-plan/logic.js +222 -0
- package/src/web-components/capacity-plan/static.html +24 -0
- package/src/web-components/capacity-plan/styles.css +84 -0
- package/src/web-components/empathy-map/api.json +31 -0
- package/src/web-components/empathy-map/logic.js +502 -0
- package/src/web-components/empathy-map/static.html +32 -0
- package/src/web-components/empathy-map/styles.js +534 -0
- package/src/web-components/gantt-chart/README.md +91 -0
- package/src/web-components/gantt-chart/api.json +20 -0
- package/src/web-components/gantt-chart/logic.js +812 -0
- package/src/web-components/gantt-chart/static.html +60 -0
- package/src/web-components/gantt-chart/styles.css +391 -0
- package/src/web-components/impact-effort/api.json +21 -0
- package/src/web-components/impact-effort/logic.js +392 -0
- package/src/web-components/impact-effort/static.html +23 -0
- package/src/web-components/impact-effort/styles.css +193 -0
- package/src/web-components/iron-triangle/_capacity.js +83 -0
- package/src/web-components/iron-triangle/_triangle-geometry.js +322 -0
- package/src/web-components/iron-triangle/api.json +26 -0
- package/src/web-components/iron-triangle/logic.js +474 -0
- package/src/web-components/iron-triangle/static.html +28 -0
- package/src/web-components/iron-triangle/styles.css +158 -0
- package/src/web-components/kanban-board/api.json +24 -0
- package/src/web-components/kanban-board/logic.js +698 -0
- package/src/web-components/kanban-board/static.html +20 -0
- package/src/web-components/kanban-board/styles.css +242 -0
- package/src/web-components/product-roadmap/api.json +118 -0
- package/src/web-components/product-roadmap/logic.js +460 -0
- package/src/web-components/product-roadmap/static.html +44 -0
- package/src/web-components/product-roadmap/styles.css +158 -0
- package/src/web-components/quadrant-grid/api.json +125 -0
- package/src/web-components/quadrant-grid/logic.js +397 -0
- package/src/web-components/quadrant-grid/static.html +49 -0
- package/src/web-components/quadrant-grid/styles.css +184 -0
- package/src/web-components/quality-target/_quality-utils.js +460 -0
- package/src/web-components/quality-target/api.json +30 -0
- package/src/web-components/quality-target/logic.js +516 -0
- package/src/web-components/quality-target/static.html +22 -0
- package/src/web-components/quality-target/styles.css +203 -0
- package/src/web-components/requirement-card/api.json +25 -0
- package/src/web-components/requirement-card/logic.js +166 -0
- package/src/web-components/requirement-card/static.html +36 -0
- package/src/web-components/requirement-card/styles.js +114 -0
- package/src/web-components/review-surface/README.md +178 -0
- package/src/web-components/review-surface/api.json +30 -0
- package/src/web-components/review-surface/logic.js +807 -0
- package/src/web-components/review-surface/static.html +33 -0
- package/src/web-components/review-surface/styles.js +459 -0
- package/src/web-components/risk-register/api.json +47 -0
- package/src/web-components/risk-register/logic.js +180 -0
- package/src/web-components/risk-register/static.html +46 -0
- package/src/web-components/risk-register/styles.css +37 -0
- package/src/web-components/story-map/api.json +22 -0
- package/src/web-components/story-map/logic.js +445 -0
- package/src/web-components/story-map/static.html +35 -0
- package/src/web-components/story-map/styles.css +169 -0
- package/src/web-components/traceability-matrix/api.json +84 -0
- package/src/web-components/traceability-matrix/logic.js +243 -0
- package/src/web-components/traceability-matrix/static.html +41 -0
- package/src/web-components/traceability-matrix/styles.css +94 -0
- package/src/web-components/user-journey/api.json +84 -0
- package/src/web-components/user-journey/logic.js +368 -0
- package/src/web-components/user-journey/static.html +14 -0
- package/src/web-components/user-journey/styles.js +317 -0
- package/src/web-components/user-persona/api.json +35 -0
- package/src/web-components/user-persona/logic.js +502 -0
- package/src/web-components/user-persona/static.html +20 -0
- package/src/web-components/user-persona/styles.js +299 -0
- package/src/web-components/user-story/api.json +35 -0
- package/src/web-components/user-story/logic.js +567 -0
- package/src/web-components/user-story/static.html +27 -0
- package/src/web-components/user-story/styles.js +332 -0
- package/src/web-components/work-item/README.md +85 -0
- package/src/web-components/work-item/api.json +30 -0
- package/src/web-components/work-item/logic.js +414 -0
- package/src/web-components/work-item/static.html +24 -0
- 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>
|