egregore-artifacts 0.2.0 → 0.4.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.
@@ -0,0 +1,742 @@
1
+ // Board data → React element tree with three views (activity, person, timeline)
2
+ import React from 'react';
3
+ import { ArtifactHeader, SectionCard, ArtifactFooter } from '../components.js';
4
+ import { fonts } from '../tokens.js';
5
+
6
+ const h = React.createElement;
7
+
8
+ // ── Priority & Status helpers ──────────────────────────────────
9
+ // All colors resolved as CSS var() strings so dark mode overrides apply.
10
+
11
+ const PRIORITY_COLORS = {
12
+ 0: 'var(--terracotta)', // P0 — breaking
13
+ 1: 'var(--green-p1)', // P1 — this cycle (green)
14
+ 2: 'var(--blue-muted)', // P2 — next cycle
15
+ 3: 'var(--muted)', // P3 — parked
16
+ };
17
+
18
+ const PRIORITY_LABELS = { 0: 'P0', 1: 'P1', 2: 'P2', 3: 'P3' };
19
+ const STATUS_LABELS = { 'todo': 'To Do', 'in-progress': 'In Progress', 'review': 'Review', 'done': 'Done' };
20
+ const STATUS_SIGILS = { 'todo': '□', 'in-progress': '▸', 'review': '◎', 'done': '✓' };
21
+
22
+ // ── Card component ─────────────────────────────────────────────
23
+
24
+ function BoardCardEl({ card, showActivity }) {
25
+ const pColor = PRIORITY_COLORS[card.priority] || 'var(--muted)';
26
+ const statusBg = card.status === 'done' ? 'var(--success-bg)'
27
+ : card.status === 'in-progress' ? 'var(--terracotta-chip)'
28
+ : 'var(--neutral-chip)';
29
+ const statusFg = card.status === 'done' ? 'var(--success-fg)'
30
+ : card.status === 'in-progress' ? 'var(--terracotta)'
31
+ : 'var(--muted)';
32
+
33
+ return h('div', {
34
+ className: 'eg-board-card',
35
+ 'data-card-id': card.id,
36
+ 'data-original-status': card.status,
37
+ 'data-original-priority': card.priority,
38
+ 'data-original-owners': (card.owners || []).join(','),
39
+ style: {
40
+ background: 'var(--surface)',
41
+ border: '1px solid var(--border)',
42
+ borderLeft: `4px solid ${pColor}`,
43
+ borderRadius: '8px',
44
+ padding: '12px 16px',
45
+ marginBottom: '8px',
46
+ position: 'relative',
47
+ },
48
+ },
49
+ // Edit toggle (top-right)
50
+ h('button', {
51
+ className: 'eg-card-edit-btn',
52
+ style: {
53
+ position: 'absolute', top: '8px', right: '8px',
54
+ background: 'none', border: 'none', cursor: 'pointer',
55
+ fontFamily: fonts.mono, fontSize: '11px', color: 'var(--muted)',
56
+ padding: '2px 6px', borderRadius: '3px',
57
+ },
58
+ }, 'edit'),
59
+ // Title row
60
+ h('div', { style: { display: 'flex', alignItems: 'baseline', gap: '8px', marginBottom: '6px', paddingRight: '40px' } },
61
+ h('span', {
62
+ style: { fontFamily: fonts.mono, fontSize: '11px', color: pColor, fontWeight: 600, flexShrink: 0 },
63
+ }, PRIORITY_LABELS[card.priority]),
64
+ h('span', {
65
+ style: { fontSize: '14px', fontWeight: 500, color: 'var(--black)', flex: 1 },
66
+ }, card.title),
67
+ ),
68
+ // Meta row
69
+ h('div', { style: { display: 'flex', alignItems: 'center', gap: '8px', flexWrap: 'wrap' } },
70
+ // Status badge
71
+ h('span', {
72
+ style: {
73
+ fontFamily: fonts.mono, fontSize: '11px', padding: '2px 8px',
74
+ borderRadius: '4px', background: statusBg, color: statusFg,
75
+ },
76
+ }, `${STATUS_SIGILS[card.status] || ''} ${STATUS_LABELS[card.status] || card.status}`),
77
+ // Owners
78
+ ...(card.owners || []).map((owner, i) =>
79
+ h('span', {
80
+ key: `o-${i}`,
81
+ style: {
82
+ fontFamily: fonts.mono, fontSize: '11px', color: 'var(--blue-muted)',
83
+ background: 'var(--blue-chip)', padding: '2px 6px', borderRadius: '3px',
84
+ },
85
+ }, owner)
86
+ ),
87
+ // Activity badge (for person view)
88
+ showActivity && card.activity && h('span', {
89
+ style: { fontFamily: fonts.mono, fontSize: '10px', color: 'var(--muted)' },
90
+ }, card.subactivity ? `${card.activity} · ${card.subactivity}` : card.activity),
91
+ // Quest link
92
+ card.quest && h('span', {
93
+ style: { fontFamily: fonts.mono, fontSize: '10px', color: 'var(--terracotta)' },
94
+ }, `→ ${card.quest}`),
95
+ // Due date
96
+ card.dueDate && h('span', {
97
+ style: { fontFamily: fonts.mono, fontSize: '10px', color: 'var(--muted)' },
98
+ }, `due ${card.dueDate}`),
99
+ ),
100
+ // Description
101
+ card.description && h('p', {
102
+ style: { fontSize: '13px', color: 'var(--muted)', margin: '6px 0 0', lineHeight: 1.4 },
103
+ }, card.description),
104
+ // Inline editor (hidden by default)
105
+ h('div', {
106
+ className: 'eg-card-editor',
107
+ style: {
108
+ display: 'none', marginTop: '10px', padding: '10px',
109
+ background: 'var(--neutral-chip)', borderRadius: '6px',
110
+ fontFamily: fonts.mono, fontSize: '12px',
111
+ },
112
+ },
113
+ h('div', { style: { display: 'flex', gap: '12px', alignItems: 'center', flexWrap: 'wrap' } },
114
+ h('label', null,
115
+ h('span', { style: { color: 'var(--muted)', marginRight: '6px' } }, 'status'),
116
+ h('select', {
117
+ className: 'eg-edit-status', 'data-card-id': card.id,
118
+ defaultValue: card.status,
119
+ style: { fontFamily: fonts.mono, fontSize: '12px', padding: '2px 4px' },
120
+ },
121
+ h('option', { value: 'todo' }, 'todo'),
122
+ h('option', { value: 'in-progress' }, 'in-progress'),
123
+ h('option', { value: 'review' }, 'review'),
124
+ h('option', { value: 'done' }, 'done'),
125
+ ),
126
+ ),
127
+ h('label', null,
128
+ h('span', { style: { color: 'var(--muted)', marginRight: '6px' } }, 'priority'),
129
+ h('select', {
130
+ className: 'eg-edit-priority', 'data-card-id': card.id,
131
+ defaultValue: String(card.priority),
132
+ style: { fontFamily: fonts.mono, fontSize: '12px', padding: '2px 4px' },
133
+ },
134
+ h('option', { value: '0' }, 'P0'),
135
+ h('option', { value: '1' }, 'P1'),
136
+ h('option', { value: '2' }, 'P2'),
137
+ h('option', { value: '3' }, 'P3'),
138
+ ),
139
+ ),
140
+ h('label', null,
141
+ h('span', { style: { color: 'var(--muted)', marginRight: '6px' } }, 'owners'),
142
+ h('input', {
143
+ className: 'eg-edit-owners', 'data-card-id': card.id,
144
+ type: 'text',
145
+ defaultValue: (card.owners || []).join(', '),
146
+ placeholder: 'comma-separated',
147
+ style: { fontFamily: fonts.mono, fontSize: '12px', padding: '2px 6px', width: '180px' },
148
+ }),
149
+ ),
150
+ ),
151
+ ),
152
+ );
153
+ }
154
+
155
+ // ── Activity View ──────────────────────────────────────────────
156
+
157
+ function ActivityView({ activities }) {
158
+ const sections = [];
159
+
160
+ for (const activity of activities) {
161
+ if (activity.subactivities) {
162
+ for (const sub of activity.subactivities) {
163
+ const cards = (sub.cards || []).filter(c => c.status !== 'done');
164
+ if (cards.length === 0) continue;
165
+ sections.push(
166
+ h('div', { key: `${activity.id}-${sub.id}`, style: { marginBottom: '1.5rem' } },
167
+ h('div', {
168
+ style: {
169
+ fontFamily: fonts.mono, fontSize: '12px', textTransform: 'uppercase',
170
+ letterSpacing: '0.06em', color: 'var(--muted)', marginBottom: '8px', paddingBottom: '4px',
171
+ borderBottom: '1px solid var(--border)',
172
+ },
173
+ }, `${activity.label} · ${sub.label}`),
174
+ ...cards.sort((a, b) => a.priority - b.priority).map((card, i) =>
175
+ h(BoardCardEl, { key: i, card, showActivity: false })
176
+ ),
177
+ )
178
+ );
179
+ }
180
+ }
181
+ const cards = (activity.cards || []).filter(c => c.status !== 'done');
182
+ if (cards.length === 0) continue;
183
+ sections.push(
184
+ h('div', { key: activity.id, style: { marginBottom: '1.5rem' } },
185
+ h('div', {
186
+ style: {
187
+ fontFamily: fonts.mono, fontSize: '12px', textTransform: 'uppercase',
188
+ letterSpacing: '0.06em', color: 'var(--muted)', marginBottom: '8px', paddingBottom: '4px',
189
+ borderBottom: '1px solid var(--border)',
190
+ },
191
+ }, activity.label),
192
+ ...cards.sort((a, b) => a.priority - b.priority).map((card, i) =>
193
+ h(BoardCardEl, { key: i, card, showActivity: false })
194
+ ),
195
+ )
196
+ );
197
+ }
198
+
199
+ return h('div', null, ...sections);
200
+ }
201
+
202
+ // ── Person View ────────────────────────────────────────────────
203
+
204
+ // ── Priority View ──────────────────────────────────────────────
205
+
206
+ function PriorityView({ activeCards }) {
207
+ const groups = [
208
+ { level: 0, label: 'P0 — Breaking', sub: 'Drop everything' },
209
+ { level: 1, label: 'P1 — This cycle', sub: 'The working set' },
210
+ { level: 2, label: 'P2 — Next cycle', sub: 'Scoped, waiting' },
211
+ { level: 3, label: 'P3 — Parked', sub: 'Captured for later' },
212
+ ];
213
+ return h('div', null,
214
+ ...groups.map(g => {
215
+ const cards = (activeCards || []).filter(c => c.priority === g.level);
216
+ if (cards.length === 0) return null;
217
+ const pColor = PRIORITY_COLORS[g.level];
218
+ return h('div', { key: g.level, style: { marginBottom: '1.5rem' } },
219
+ h('div', {
220
+ style: {
221
+ display: 'flex', alignItems: 'baseline', gap: '10px',
222
+ marginBottom: '8px', paddingBottom: '4px',
223
+ borderBottom: `2px solid ${pColor}`,
224
+ },
225
+ },
226
+ h('span', {
227
+ style: {
228
+ fontFamily: fonts.mono, fontSize: '13px', fontWeight: 700,
229
+ color: pColor, textTransform: 'uppercase', letterSpacing: '0.06em',
230
+ },
231
+ }, g.label),
232
+ h('span', {
233
+ style: { fontFamily: fonts.mono, fontSize: '11px', color: 'var(--muted)' },
234
+ }, `${cards.length} · ${g.sub}`),
235
+ ),
236
+ ...cards.map((card, i) => h(BoardCardEl, { key: i, card, showActivity: true })),
237
+ );
238
+ }).filter(Boolean),
239
+ );
240
+ }
241
+
242
+ // ── Done View ──────────────────────────────────────────────────
243
+
244
+ function DoneView({ doneCards, weekStart }) {
245
+ if (!doneCards || doneCards.length === 0) {
246
+ return h('div', { style: { padding: '2rem', textAlign: 'center', color: 'var(--muted)', fontStyle: 'italic' } },
247
+ `Nothing done yet this week. The Done tab clears every Monday.`
248
+ );
249
+ }
250
+ const byActivity = {};
251
+ for (const card of doneCards) {
252
+ const key = card.subactivity ? `${card.activity} · ${card.subactivity}` : card.activity;
253
+ if (!byActivity[key]) byActivity[key] = [];
254
+ byActivity[key].push(card);
255
+ }
256
+ return h('div', null,
257
+ h('div', {
258
+ style: {
259
+ fontFamily: fonts.mono, fontSize: '11px', color: 'var(--muted)',
260
+ marginBottom: '1rem', padding: '8px 12px', background: 'var(--neutral-chip)',
261
+ borderRadius: '6px',
262
+ },
263
+ }, `Showing ${doneCards.length} done this week (since ${weekStart}). This tab auto-clears every Monday.`),
264
+ ...Object.entries(byActivity).map(([activityLabel, cards], i) =>
265
+ h('div', { key: i, style: { marginBottom: '1.5rem' } },
266
+ h('div', {
267
+ style: {
268
+ fontFamily: fonts.mono, fontSize: '12px', textTransform: 'uppercase',
269
+ letterSpacing: '0.06em', color: 'var(--muted)', marginBottom: '8px', paddingBottom: '4px',
270
+ borderBottom: '1px solid var(--border)',
271
+ },
272
+ }, activityLabel),
273
+ ...cards.map((card, j) =>
274
+ h(BoardCardEl, { key: j, card, showActivity: false })
275
+ ),
276
+ )
277
+ ),
278
+ );
279
+ }
280
+
281
+ // ── Person View ────────────────────────────────────────────────
282
+
283
+ function PersonView({ people }) {
284
+ return h('div', null,
285
+ ...people.map((person, pi) =>
286
+ h('div', { key: pi, style: { marginBottom: '1.5rem' } },
287
+ h('div', {
288
+ style: {
289
+ display: 'flex', alignItems: 'baseline', gap: '12px',
290
+ fontFamily: fonts.mono, fontSize: '12px', textTransform: 'uppercase',
291
+ letterSpacing: '0.06em', color: 'var(--muted)', marginBottom: '8px', paddingBottom: '4px',
292
+ borderBottom: '1px solid var(--border)',
293
+ },
294
+ },
295
+ h('span', { style: { color: 'var(--black)', fontWeight: 600 } }, person.name),
296
+ h('span', null, `${person.stats.total} cards`),
297
+ person.stats.p0 > 0 && h('span', { style: { color: 'var(--terracotta)' } }, `${person.stats.p0} P0`),
298
+ person.stats.inProgress > 0 && h('span', null, `${person.stats.inProgress} active`),
299
+ ),
300
+ ...person.cards.map((card, ci) =>
301
+ h(BoardCardEl, { key: ci, card, showActivity: true })
302
+ ),
303
+ )
304
+ ),
305
+ );
306
+ }
307
+
308
+ // ── Timeline View ──────────────────────────────────────────────
309
+
310
+ function shortLabel(title) {
311
+ // Take first meaningful words, drop filler like "—", stop at 30 chars
312
+ const cleaned = title.replace(/\s*—\s*.*$/, '').replace(/\s*\+\s*.*$/, '');
313
+ if (cleaned.length <= 30) return cleaned;
314
+ const words = cleaned.split(/\s+/);
315
+ let result = '';
316
+ for (const w of words) {
317
+ if ((result + ' ' + w).trim().length > 28) break;
318
+ result = (result + ' ' + w).trim();
319
+ }
320
+ return result || words[0];
321
+ }
322
+
323
+ function TimelineView({ timeline, allCards }) {
324
+ if (timeline.length === 0) {
325
+ return h('p', { style: { color: 'var(--muted)', fontStyle: 'italic' } }, 'No cards with dates set.');
326
+ }
327
+
328
+ // Find date range
329
+ const dates = timeline.flatMap(c => [c.startDate, c.dueDate].filter(Boolean));
330
+ const minDate = dates.reduce((a, b) => a < b ? a : b);
331
+ const maxDate = dates.reduce((a, b) => a > b ? a : b);
332
+
333
+ // Snap to week boundaries (Monday)
334
+ const startRaw = new Date(minDate + 'T00:00:00');
335
+ const startDay = startRaw.getDay();
336
+ const mondayOffset = startDay === 0 ? -6 : 1 - startDay;
337
+ const start = new Date(startRaw.getTime() + mondayOffset * 24 * 60 * 60 * 1000);
338
+
339
+ // Ensure at least 2 full weeks
340
+ const twoWeeksOut = new Date(start.getTime() + 14 * 24 * 60 * 60 * 1000);
341
+ const endRaw = new Date(maxDate + 'T00:00:00');
342
+ const effectiveEnd = endRaw > twoWeeksOut ? endRaw : twoWeeksOut;
343
+ // Snap end to Sunday
344
+ const endDay = effectiveEnd.getDay();
345
+ const sundayAdd = endDay === 0 ? 0 : 7 - endDay;
346
+ const end = new Date(effectiveEnd.getTime() + sundayAdd * 24 * 60 * 60 * 1000);
347
+ const totalDays = Math.ceil((end - start) / (1000 * 60 * 60 * 24));
348
+
349
+ const LABEL_WIDTH = '240px';
350
+ const ROW_HEIGHT = '32px';
351
+
352
+ function dayOffset(dateStr) {
353
+ if (!dateStr) return 0;
354
+ const d = new Date(dateStr + 'T00:00:00');
355
+ return Math.max(0, Math.ceil((d - start) / (1000 * 60 * 60 * 24)));
356
+ }
357
+
358
+ function formatDate(d) {
359
+ return d.toLocaleDateString('en-US', { month: 'short', day: 'numeric' });
360
+ }
361
+
362
+ // Generate week markers
363
+ const weeks = [];
364
+ const cursor = new Date(start.getTime());
365
+ while (cursor <= end) {
366
+ weeks.push({
367
+ label: formatDate(cursor),
368
+ offset: Math.ceil((cursor - start) / (1000 * 60 * 60 * 24)),
369
+ });
370
+ cursor.setDate(cursor.getDate() + 7);
371
+ }
372
+
373
+ // Today marker
374
+ const today = new Date().toISOString().split('T')[0];
375
+ const todayOffset = dayOffset(today);
376
+ const todayPct = (todayOffset / totalDays) * 100;
377
+
378
+ return h('div', { style: { width: '100%' } },
379
+ // Header row: label column + week markers
380
+ h('div', {
381
+ style: { display: 'flex', height: '24px', marginBottom: '4px', borderBottom: '1px solid var(--border)' },
382
+ },
383
+ // Empty label column
384
+ h('div', { style: { width: LABEL_WIDTH, flexShrink: 0 } }),
385
+ // Week header area
386
+ h('div', { style: { flex: 1, position: 'relative' } },
387
+ ...weeks.map((w, i) =>
388
+ h('span', {
389
+ key: `wh-${i}`,
390
+ style: {
391
+ position: 'absolute', left: `${(w.offset / totalDays) * 100}%`,
392
+ fontFamily: fonts.mono, fontSize: '11px', color: 'var(--muted)', fontWeight: 500, whiteSpace: 'nowrap',
393
+ },
394
+ }, w.label)
395
+ ),
396
+ todayPct >= 0 && todayPct <= 100 && h('span', {
397
+ style: {
398
+ position: 'absolute', left: `${todayPct}%`, transform: 'translateX(-50%)',
399
+ fontFamily: fonts.mono, fontSize: '10px', color: 'var(--terracotta)', fontWeight: 700,
400
+ },
401
+ }, 'today'),
402
+ ),
403
+ ),
404
+ // Card rows
405
+ ...timeline.map((card, i) => {
406
+ const startOff = dayOffset(card.startDate || card.dueDate);
407
+ const endOff = dayOffset(card.dueDate || card.startDate);
408
+ const barLeft = (startOff / totalDays) * 100;
409
+ const barWidth = Math.max(((endOff - startOff) / totalDays) * 100, 2);
410
+ const pColor = PRIORITY_COLORS[card.priority] || 'var(--muted)';
411
+ const label = shortLabel(card.title);
412
+
413
+ return h('div', {
414
+ key: i,
415
+ style: { display: 'flex', alignItems: 'center', height: ROW_HEIGHT, marginBottom: '1px' },
416
+ },
417
+ // Left label: priority + short name + owners
418
+ h('div', {
419
+ style: {
420
+ width: LABEL_WIDTH, flexShrink: 0, display: 'flex', alignItems: 'center', gap: '6px',
421
+ paddingRight: '12px', overflow: 'hidden',
422
+ },
423
+ },
424
+ h('span', {
425
+ style: { fontFamily: fonts.mono, fontSize: '10px', color: pColor, fontWeight: 700, flexShrink: 0 },
426
+ }, PRIORITY_LABELS[card.priority]),
427
+ h('span', {
428
+ style: {
429
+ fontSize: '12px', color: 'var(--dark)', whiteSpace: 'nowrap',
430
+ overflow: 'hidden', textOverflow: 'ellipsis', flex: 1,
431
+ },
432
+ }, label),
433
+ h('span', {
434
+ style: { fontFamily: fonts.mono, fontSize: '10px', color: 'var(--muted)', flexShrink: 0, whiteSpace: 'nowrap' },
435
+ }, (card.owners || []).slice(0, 2).join(', ')),
436
+ ),
437
+ // Gantt area
438
+ h('div', { style: { flex: 1, position: 'relative', height: '100%' } },
439
+ // Week grid lines
440
+ ...weeks.map((w, wi) =>
441
+ h('div', {
442
+ key: `g-${wi}`,
443
+ style: {
444
+ position: 'absolute', left: `${(w.offset / totalDays) * 100}%`,
445
+ top: 0, bottom: 0, width: '1px', background: 'var(--border)', opacity: 0.3,
446
+ },
447
+ })
448
+ ),
449
+ // Today line
450
+ todayPct >= 0 && todayPct <= 100 && h('div', {
451
+ style: {
452
+ position: 'absolute', left: `${todayPct}%`, top: 0, bottom: 0,
453
+ width: '2px', background: 'var(--terracotta)', opacity: 0.5, zIndex: 1,
454
+ },
455
+ }),
456
+ // Bar
457
+ h('div', {
458
+ style: {
459
+ position: 'absolute', left: `${barLeft}%`, width: `${barWidth}%`,
460
+ top: '6px', bottom: '6px', borderRadius: '4px',
461
+ background: pColor, opacity: card.status === 'done' ? 0.25 : 0.75,
462
+ minWidth: '6px',
463
+ },
464
+ }),
465
+ ),
466
+ );
467
+ }),
468
+ );
469
+ }
470
+
471
+ // ── Summary Metrics ────────────────────────────────────────────
472
+
473
+ function SummaryBar({ summary }) {
474
+ const { byPriority, byStatus, totalCards } = summary;
475
+ return h('div', {
476
+ style: {
477
+ display: 'flex', gap: '16px', flexWrap: 'wrap', padding: '12px 0',
478
+ fontFamily: fonts.mono, fontSize: '12px', color: 'var(--muted)',
479
+ },
480
+ },
481
+ h('span', { style: { fontWeight: 600, color: 'var(--black)' } }, `${totalCards} cards`),
482
+ byPriority.P0 > 0 && h('span', { style: { color: PRIORITY_COLORS[0] } }, `${byPriority.P0} P0`),
483
+ byPriority.P1 > 0 && h('span', { style: { color: PRIORITY_COLORS[1] } }, `${byPriority.P1} P1`),
484
+ byPriority.P2 > 0 && h('span', { style: { color: PRIORITY_COLORS[2] } }, `${byPriority.P2} P2`),
485
+ byPriority.P3 > 0 && h('span', null, `${byPriority.P3} P3`),
486
+ h('span', null, '·'),
487
+ h('span', null, `${byStatus['in-progress']} active`),
488
+ h('span', null, `${byStatus.todo} to do`),
489
+ byStatus.review > 0 && h('span', null, `${byStatus.review} in review`),
490
+ byStatus.done > 0 && h('span', null, `${byStatus.done} done`),
491
+ );
492
+ }
493
+
494
+ // ── Tab Switcher (rendered as HTML with inline JS) ─────────────
495
+
496
+ function ViewTabs({ doneCount }) {
497
+ const tabs = [
498
+ { key: 'activity', label: 'Activity' },
499
+ { key: 'priority', label: 'Priority' },
500
+ { key: 'person', label: 'Person' },
501
+ { key: 'timeline', label: 'Timeline' },
502
+ { key: 'done', label: `Done${doneCount > 0 ? ` (${doneCount})` : ''}` },
503
+ ];
504
+ return h('div', {
505
+ style: {
506
+ display: 'flex', gap: '4px', marginBottom: '1.5rem',
507
+ borderBottom: '2px solid var(--border)', paddingBottom: '0',
508
+ },
509
+ },
510
+ ...tabs.map(tab =>
511
+ h('button', {
512
+ key: tab.key,
513
+ className: 'eg-board-tab',
514
+ 'data-view': tab.key,
515
+ style: {
516
+ fontFamily: fonts.mono, fontSize: '13px', padding: '8px 16px',
517
+ background: 'none', border: 'none', borderBottom: '2px solid transparent',
518
+ marginBottom: '-2px', cursor: 'pointer', color: 'var(--muted)',
519
+ },
520
+ }, tab.label)
521
+ ),
522
+ );
523
+ }
524
+
525
+ // ── Main Template ──────────────────────────────────────────────
526
+
527
+ export function boardTemplate(data) {
528
+ const sections = [];
529
+
530
+ // Header
531
+ sections.push(
532
+ h(ArtifactHeader, {
533
+ key: 'header',
534
+ title: data.title,
535
+ type: 'board',
536
+ date: data.date,
537
+ author: data.updatedBy,
538
+ status: 'active',
539
+ priority: 0,
540
+ projects: [],
541
+ })
542
+ );
543
+
544
+ // Summary bar
545
+ sections.push(h(SummaryBar, { key: 'summary', summary: data.summary }));
546
+
547
+ // Tab switcher
548
+ sections.push(h(ViewTabs, { key: 'tabs', doneCount: (data.doneCards || []).length }));
549
+
550
+ // Activity view (default visible)
551
+ sections.push(
552
+ h('div', { key: 'view-activity', className: 'eg-board-view', 'data-view': 'activity' },
553
+ h(ActivityView, { activities: data.activities })
554
+ )
555
+ );
556
+
557
+ // Priority view (hidden by default)
558
+ sections.push(
559
+ h('div', { key: 'view-priority', className: 'eg-board-view', 'data-view': 'priority', style: { display: 'none' } },
560
+ h(PriorityView, { activeCards: data.activeCards })
561
+ )
562
+ );
563
+
564
+ // Person view (hidden by default)
565
+ sections.push(
566
+ h('div', { key: 'view-person', className: 'eg-board-view', 'data-view': 'person', style: { display: 'none' } },
567
+ h(PersonView, { people: data.people })
568
+ )
569
+ );
570
+
571
+ // Timeline view (hidden by default)
572
+ sections.push(
573
+ h('div', { key: 'view-timeline', className: 'eg-board-view', 'data-view': 'timeline', style: { display: 'none' } },
574
+ h(TimelineView, { timeline: data.timeline, allCards: data.allCards })
575
+ )
576
+ );
577
+
578
+ // Done view (hidden by default)
579
+ sections.push(
580
+ h('div', { key: 'view-done', className: 'eg-board-view', 'data-view': 'done', style: { display: 'none' } },
581
+ h(DoneView, { doneCards: data.doneCards || [], weekStart: data.weekStart })
582
+ )
583
+ );
584
+
585
+ // Inline script for tab switching
586
+ sections.push(
587
+ h('script', {
588
+ key: 'tabs-script',
589
+ dangerouslySetInnerHTML: {
590
+ __html: `
591
+ (function() {
592
+ var tabs = document.querySelectorAll('.eg-board-tab');
593
+ var views = document.querySelectorAll('.eg-board-view');
594
+ function activate(viewName) {
595
+ tabs.forEach(function(t) {
596
+ t.style.color = t.dataset.view === viewName ? 'var(--black)' : 'var(--muted)';
597
+ t.style.borderBottomColor = t.dataset.view === viewName ? 'var(--terracotta)' : 'transparent';
598
+ t.style.fontWeight = t.dataset.view === viewName ? '600' : '400';
599
+ });
600
+ views.forEach(function(v) {
601
+ v.style.display = v.dataset.view === viewName ? 'block' : 'none';
602
+ });
603
+ }
604
+ tabs.forEach(function(t) { t.addEventListener('click', function() { activate(t.dataset.view); }); });
605
+ activate('activity');
606
+ })();
607
+ `,
608
+ },
609
+ })
610
+ );
611
+
612
+ // Floating "Copy changes" button — live editor output
613
+ sections.push(
614
+ h('button', {
615
+ key: 'copy-changes-btn',
616
+ id: 'eg-copy-changes-btn',
617
+ style: {
618
+ position: 'fixed', bottom: '24px', right: '24px', zIndex: 1000,
619
+ background: 'var(--terracotta)', color: 'white', border: 'none',
620
+ borderRadius: '24px', padding: '12px 20px', fontFamily: fonts.mono,
621
+ fontSize: '13px', fontWeight: 600, cursor: 'pointer',
622
+ boxShadow: '0 4px 12px rgba(0,0,0,0.15)', display: 'none',
623
+ },
624
+ }, 'Copy 0 changes')
625
+ );
626
+
627
+ // Interactive editor script — toggle editors, sync across duplicate cards,
628
+ // track changes, produce paste-back command on click
629
+ sections.push(
630
+ h('script', {
631
+ key: 'editor-script',
632
+ dangerouslySetInnerHTML: {
633
+ __html: `
634
+ (function() {
635
+ var editBtns = document.querySelectorAll('.eg-card-edit-btn');
636
+ var copyBtn = document.getElementById('eg-copy-changes-btn');
637
+
638
+ // Toggle editor — scope to clicked card's parent (each card appears in
639
+ // multiple views, so global querySelector would target the wrong one)
640
+ editBtns.forEach(function(btn) {
641
+ btn.addEventListener('click', function() {
642
+ var cardEl = btn.closest('.eg-board-card');
643
+ if (!cardEl) return;
644
+ var editor = cardEl.querySelector('.eg-card-editor');
645
+ if (!editor) return;
646
+ var isOpen = editor.style.display === 'block';
647
+ editor.style.display = isOpen ? 'none' : 'block';
648
+ btn.textContent = isOpen ? 'edit' : 'close';
649
+ });
650
+ });
651
+
652
+ // Sync edits across duplicate card instances (same card in Activity + Priority + Person)
653
+ function syncEdit(event) {
654
+ var input = event.target;
655
+ if (!input.classList) return;
656
+ var cls = input.className || '';
657
+ if (!cls.includes('eg-edit-')) return;
658
+ var cardId = input.dataset.cardId;
659
+ if (!cardId) return;
660
+ var className = cls.split(' ').find(function(c){return c.startsWith('eg-edit-');});
661
+ var selector = '.' + className + '[data-card-id="' + cardId + '"]';
662
+ document.querySelectorAll(selector).forEach(function(el) {
663
+ if (el !== input) el.value = input.value;
664
+ });
665
+ }
666
+ document.addEventListener('change', syncEdit);
667
+ document.addEventListener('input', syncEdit);
668
+
669
+ function collectChanges() {
670
+ var changes = [];
671
+ var seen = {};
672
+ document.querySelectorAll('.eg-board-card').forEach(function(card) {
673
+ var id = card.dataset.cardId;
674
+ if (seen[id]) return;
675
+ seen[id] = true;
676
+ var origStatus = card.dataset.originalStatus;
677
+ var origPriority = card.dataset.originalPriority;
678
+ var origOwners = card.dataset.originalOwners;
679
+ var statusEl = card.querySelector('.eg-edit-status');
680
+ var priorityEl = card.querySelector('.eg-edit-priority');
681
+ var ownersEl = card.querySelector('.eg-edit-owners');
682
+ if (!statusEl) return;
683
+ var newStatus = statusEl.value;
684
+ var newPriority = priorityEl.value;
685
+ var newOwners = ownersEl.value.split(',').map(function(s){return s.trim();}).filter(Boolean).join(',');
686
+ var cardChange = { id: id, changes: [] };
687
+ if (newStatus !== origStatus) cardChange.changes.push('status → ' + newStatus);
688
+ if (newPriority !== origPriority) cardChange.changes.push('priority → P' + newPriority);
689
+ if (newOwners !== origOwners) cardChange.changes.push('owners → [' + newOwners + ']');
690
+ if (cardChange.changes.length > 0) changes.push(cardChange);
691
+ });
692
+ return changes;
693
+ }
694
+
695
+ function updateCopyBtn() {
696
+ var changes = collectChanges();
697
+ if (changes.length === 0) {
698
+ copyBtn.style.display = 'none';
699
+ return;
700
+ }
701
+ copyBtn.style.display = 'block';
702
+ var total = changes.reduce(function(n, c) { return n + c.changes.length; }, 0);
703
+ copyBtn.textContent = 'Copy ' + total + ' change' + (total === 1 ? '' : 's');
704
+ }
705
+ document.addEventListener('change', updateCopyBtn);
706
+ document.addEventListener('input', updateCopyBtn);
707
+
708
+ copyBtn.addEventListener('click', function() {
709
+ var changes = collectChanges();
710
+ var lines = ['Apply these board changes:', ''];
711
+ changes.forEach(function(c) {
712
+ lines.push('- ' + c.id + ':');
713
+ c.changes.forEach(function(ch) { lines.push(' ' + ch); });
714
+ });
715
+ var text = lines.join('\\n');
716
+ navigator.clipboard.writeText(text).then(function() {
717
+ var original = copyBtn.textContent;
718
+ copyBtn.textContent = '✓ Copied — paste in Claude';
719
+ copyBtn.style.background = '#2e7d32';
720
+ setTimeout(function() {
721
+ copyBtn.textContent = original;
722
+ copyBtn.style.background = 'var(--terracotta)';
723
+ }, 2000);
724
+ });
725
+ });
726
+ })();
727
+ `,
728
+ },
729
+ })
730
+ );
731
+
732
+ // Footer
733
+ sections.push(
734
+ h(ArtifactFooter, {
735
+ key: 'footer',
736
+ generatedAt: new Date().toISOString(),
737
+ source: 'memory/board/board.json',
738
+ })
739
+ );
740
+
741
+ return h('div', null, ...sections);
742
+ }