egregore-artifacts 0.2.0 → 0.3.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,459 @@
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
+ style: {
35
+ background: 'var(--surface)',
36
+ border: '1px solid var(--border)',
37
+ borderLeft: `4px solid ${pColor}`,
38
+ borderRadius: '8px',
39
+ padding: '12px 16px',
40
+ marginBottom: '8px',
41
+ },
42
+ },
43
+ // Title row
44
+ h('div', { style: { display: 'flex', alignItems: 'baseline', gap: '8px', marginBottom: '6px' } },
45
+ h('span', {
46
+ style: { fontFamily: fonts.mono, fontSize: '11px', color: pColor, fontWeight: 600, flexShrink: 0 },
47
+ }, PRIORITY_LABELS[card.priority]),
48
+ h('span', {
49
+ style: { fontSize: '14px', fontWeight: 500, color: 'var(--black)', flex: 1 },
50
+ }, card.title),
51
+ ),
52
+ // Meta row
53
+ h('div', { style: { display: 'flex', alignItems: 'center', gap: '8px', flexWrap: 'wrap' } },
54
+ // Status badge
55
+ h('span', {
56
+ style: {
57
+ fontFamily: fonts.mono, fontSize: '11px', padding: '2px 8px',
58
+ borderRadius: '4px', background: statusBg, color: statusFg,
59
+ },
60
+ }, `${STATUS_SIGILS[card.status] || ''} ${STATUS_LABELS[card.status] || card.status}`),
61
+ // Owners
62
+ ...(card.owners || []).map((owner, i) =>
63
+ h('span', {
64
+ key: `o-${i}`,
65
+ style: {
66
+ fontFamily: fonts.mono, fontSize: '11px', color: 'var(--blue-muted)',
67
+ background: 'var(--blue-chip)', padding: '2px 6px', borderRadius: '3px',
68
+ },
69
+ }, owner)
70
+ ),
71
+ // Activity badge (for person view)
72
+ showActivity && card.activity && h('span', {
73
+ style: { fontFamily: fonts.mono, fontSize: '10px', color: 'var(--muted)' },
74
+ }, card.subactivity ? `${card.activity} · ${card.subactivity}` : card.activity),
75
+ // Quest link
76
+ card.quest && h('span', {
77
+ style: { fontFamily: fonts.mono, fontSize: '10px', color: 'var(--terracotta)' },
78
+ }, `→ ${card.quest}`),
79
+ // Due date
80
+ card.dueDate && h('span', {
81
+ style: { fontFamily: fonts.mono, fontSize: '10px', color: 'var(--muted)' },
82
+ }, `due ${card.dueDate}`),
83
+ ),
84
+ // Description
85
+ card.description && h('p', {
86
+ style: { fontSize: '13px', color: 'var(--muted)', margin: '6px 0 0', lineHeight: 1.4 },
87
+ }, card.description),
88
+ );
89
+ }
90
+
91
+ // ── Activity View ──────────────────────────────────────────────
92
+
93
+ function ActivityView({ activities }) {
94
+ const sections = [];
95
+
96
+ for (const activity of activities) {
97
+ if (activity.subactivities) {
98
+ for (const sub of activity.subactivities) {
99
+ const cards = sub.cards || [];
100
+ if (cards.length === 0) continue;
101
+ sections.push(
102
+ h('div', { key: `${activity.id}-${sub.id}`, style: { marginBottom: '1.5rem' } },
103
+ h('div', {
104
+ style: {
105
+ fontFamily: fonts.mono, fontSize: '12px', textTransform: 'uppercase',
106
+ letterSpacing: '0.06em', color: 'var(--muted)', marginBottom: '8px', paddingBottom: '4px',
107
+ borderBottom: '1px solid var(--border)',
108
+ },
109
+ }, `${activity.label} · ${sub.label}`),
110
+ ...cards.sort((a, b) => a.priority - b.priority).map((card, i) =>
111
+ h(BoardCardEl, { key: i, card, showActivity: false })
112
+ ),
113
+ )
114
+ );
115
+ }
116
+ }
117
+ const cards = activity.cards || [];
118
+ if (cards.length === 0) continue;
119
+ sections.push(
120
+ h('div', { key: activity.id, style: { marginBottom: '1.5rem' } },
121
+ h('div', {
122
+ style: {
123
+ fontFamily: fonts.mono, fontSize: '12px', textTransform: 'uppercase',
124
+ letterSpacing: '0.06em', color: 'var(--muted)', marginBottom: '8px', paddingBottom: '4px',
125
+ borderBottom: '1px solid var(--border)',
126
+ },
127
+ }, activity.label),
128
+ ...cards.sort((a, b) => a.priority - b.priority).map((card, i) =>
129
+ h(BoardCardEl, { key: i, card, showActivity: false })
130
+ ),
131
+ )
132
+ );
133
+ }
134
+
135
+ return h('div', null, ...sections);
136
+ }
137
+
138
+ // ── Person View ────────────────────────────────────────────────
139
+
140
+ function PersonView({ people }) {
141
+ return h('div', null,
142
+ ...people.map((person, pi) =>
143
+ h('div', { key: pi, style: { marginBottom: '1.5rem' } },
144
+ h('div', {
145
+ style: {
146
+ display: 'flex', alignItems: 'baseline', gap: '12px',
147
+ fontFamily: fonts.mono, fontSize: '12px', textTransform: 'uppercase',
148
+ letterSpacing: '0.06em', color: 'var(--muted)', marginBottom: '8px', paddingBottom: '4px',
149
+ borderBottom: '1px solid var(--border)',
150
+ },
151
+ },
152
+ h('span', { style: { color: 'var(--black)', fontWeight: 600 } }, person.name),
153
+ h('span', null, `${person.stats.total} cards`),
154
+ person.stats.p0 > 0 && h('span', { style: { color: 'var(--terracotta)' } }, `${person.stats.p0} P0`),
155
+ person.stats.inProgress > 0 && h('span', null, `${person.stats.inProgress} active`),
156
+ ),
157
+ ...person.cards.map((card, ci) =>
158
+ h(BoardCardEl, { key: ci, card, showActivity: true })
159
+ ),
160
+ )
161
+ ),
162
+ );
163
+ }
164
+
165
+ // ── Timeline View ──────────────────────────────────────────────
166
+
167
+ function shortLabel(title) {
168
+ // Take first meaningful words, drop filler like "—", stop at 30 chars
169
+ const cleaned = title.replace(/\s*—\s*.*$/, '').replace(/\s*\+\s*.*$/, '');
170
+ if (cleaned.length <= 30) return cleaned;
171
+ const words = cleaned.split(/\s+/);
172
+ let result = '';
173
+ for (const w of words) {
174
+ if ((result + ' ' + w).trim().length > 28) break;
175
+ result = (result + ' ' + w).trim();
176
+ }
177
+ return result || words[0];
178
+ }
179
+
180
+ function TimelineView({ timeline, allCards }) {
181
+ if (timeline.length === 0) {
182
+ return h('p', { style: { color: 'var(--muted)', fontStyle: 'italic' } }, 'No cards with dates set.');
183
+ }
184
+
185
+ // Find date range
186
+ const dates = timeline.flatMap(c => [c.startDate, c.dueDate].filter(Boolean));
187
+ const minDate = dates.reduce((a, b) => a < b ? a : b);
188
+ const maxDate = dates.reduce((a, b) => a > b ? a : b);
189
+
190
+ // Snap to week boundaries (Monday)
191
+ const startRaw = new Date(minDate + 'T00:00:00');
192
+ const startDay = startRaw.getDay();
193
+ const mondayOffset = startDay === 0 ? -6 : 1 - startDay;
194
+ const start = new Date(startRaw.getTime() + mondayOffset * 24 * 60 * 60 * 1000);
195
+
196
+ // Ensure at least 2 full weeks
197
+ const twoWeeksOut = new Date(start.getTime() + 14 * 24 * 60 * 60 * 1000);
198
+ const endRaw = new Date(maxDate + 'T00:00:00');
199
+ const effectiveEnd = endRaw > twoWeeksOut ? endRaw : twoWeeksOut;
200
+ // Snap end to Sunday
201
+ const endDay = effectiveEnd.getDay();
202
+ const sundayAdd = endDay === 0 ? 0 : 7 - endDay;
203
+ const end = new Date(effectiveEnd.getTime() + sundayAdd * 24 * 60 * 60 * 1000);
204
+ const totalDays = Math.ceil((end - start) / (1000 * 60 * 60 * 24));
205
+
206
+ const LABEL_WIDTH = '240px';
207
+ const ROW_HEIGHT = '32px';
208
+
209
+ function dayOffset(dateStr) {
210
+ if (!dateStr) return 0;
211
+ const d = new Date(dateStr + 'T00:00:00');
212
+ return Math.max(0, Math.ceil((d - start) / (1000 * 60 * 60 * 24)));
213
+ }
214
+
215
+ function formatDate(d) {
216
+ return d.toLocaleDateString('en-US', { month: 'short', day: 'numeric' });
217
+ }
218
+
219
+ // Generate week markers
220
+ const weeks = [];
221
+ const cursor = new Date(start.getTime());
222
+ while (cursor <= end) {
223
+ weeks.push({
224
+ label: formatDate(cursor),
225
+ offset: Math.ceil((cursor - start) / (1000 * 60 * 60 * 24)),
226
+ });
227
+ cursor.setDate(cursor.getDate() + 7);
228
+ }
229
+
230
+ // Today marker
231
+ const today = new Date().toISOString().split('T')[0];
232
+ const todayOffset = dayOffset(today);
233
+ const todayPct = (todayOffset / totalDays) * 100;
234
+
235
+ return h('div', { style: { width: '100%' } },
236
+ // Header row: label column + week markers
237
+ h('div', {
238
+ style: { display: 'flex', height: '24px', marginBottom: '4px', borderBottom: '1px solid var(--border)' },
239
+ },
240
+ // Empty label column
241
+ h('div', { style: { width: LABEL_WIDTH, flexShrink: 0 } }),
242
+ // Week header area
243
+ h('div', { style: { flex: 1, position: 'relative' } },
244
+ ...weeks.map((w, i) =>
245
+ h('span', {
246
+ key: `wh-${i}`,
247
+ style: {
248
+ position: 'absolute', left: `${(w.offset / totalDays) * 100}%`,
249
+ fontFamily: fonts.mono, fontSize: '11px', color: 'var(--muted)', fontWeight: 500, whiteSpace: 'nowrap',
250
+ },
251
+ }, w.label)
252
+ ),
253
+ todayPct >= 0 && todayPct <= 100 && h('span', {
254
+ style: {
255
+ position: 'absolute', left: `${todayPct}%`, transform: 'translateX(-50%)',
256
+ fontFamily: fonts.mono, fontSize: '10px', color: 'var(--terracotta)', fontWeight: 700,
257
+ },
258
+ }, 'today'),
259
+ ),
260
+ ),
261
+ // Card rows
262
+ ...timeline.map((card, i) => {
263
+ const startOff = dayOffset(card.startDate || card.dueDate);
264
+ const endOff = dayOffset(card.dueDate || card.startDate);
265
+ const barLeft = (startOff / totalDays) * 100;
266
+ const barWidth = Math.max(((endOff - startOff) / totalDays) * 100, 2);
267
+ const pColor = PRIORITY_COLORS[card.priority] || 'var(--muted)';
268
+ const label = shortLabel(card.title);
269
+
270
+ return h('div', {
271
+ key: i,
272
+ style: { display: 'flex', alignItems: 'center', height: ROW_HEIGHT, marginBottom: '1px' },
273
+ },
274
+ // Left label: priority + short name + owners
275
+ h('div', {
276
+ style: {
277
+ width: LABEL_WIDTH, flexShrink: 0, display: 'flex', alignItems: 'center', gap: '6px',
278
+ paddingRight: '12px', overflow: 'hidden',
279
+ },
280
+ },
281
+ h('span', {
282
+ style: { fontFamily: fonts.mono, fontSize: '10px', color: pColor, fontWeight: 700, flexShrink: 0 },
283
+ }, PRIORITY_LABELS[card.priority]),
284
+ h('span', {
285
+ style: {
286
+ fontSize: '12px', color: 'var(--dark)', whiteSpace: 'nowrap',
287
+ overflow: 'hidden', textOverflow: 'ellipsis', flex: 1,
288
+ },
289
+ }, label),
290
+ h('span', {
291
+ style: { fontFamily: fonts.mono, fontSize: '10px', color: 'var(--muted)', flexShrink: 0, whiteSpace: 'nowrap' },
292
+ }, (card.owners || []).slice(0, 2).join(', ')),
293
+ ),
294
+ // Gantt area
295
+ h('div', { style: { flex: 1, position: 'relative', height: '100%' } },
296
+ // Week grid lines
297
+ ...weeks.map((w, wi) =>
298
+ h('div', {
299
+ key: `g-${wi}`,
300
+ style: {
301
+ position: 'absolute', left: `${(w.offset / totalDays) * 100}%`,
302
+ top: 0, bottom: 0, width: '1px', background: 'var(--border)', opacity: 0.3,
303
+ },
304
+ })
305
+ ),
306
+ // Today line
307
+ todayPct >= 0 && todayPct <= 100 && h('div', {
308
+ style: {
309
+ position: 'absolute', left: `${todayPct}%`, top: 0, bottom: 0,
310
+ width: '2px', background: 'var(--terracotta)', opacity: 0.5, zIndex: 1,
311
+ },
312
+ }),
313
+ // Bar
314
+ h('div', {
315
+ style: {
316
+ position: 'absolute', left: `${barLeft}%`, width: `${barWidth}%`,
317
+ top: '6px', bottom: '6px', borderRadius: '4px',
318
+ background: pColor, opacity: card.status === 'done' ? 0.25 : 0.75,
319
+ minWidth: '6px',
320
+ },
321
+ }),
322
+ ),
323
+ );
324
+ }),
325
+ );
326
+ }
327
+
328
+ // ── Summary Metrics ────────────────────────────────────────────
329
+
330
+ function SummaryBar({ summary }) {
331
+ const { byPriority, byStatus, totalCards } = summary;
332
+ return h('div', {
333
+ style: {
334
+ display: 'flex', gap: '16px', flexWrap: 'wrap', padding: '12px 0',
335
+ fontFamily: fonts.mono, fontSize: '12px', color: 'var(--muted)',
336
+ },
337
+ },
338
+ h('span', { style: { fontWeight: 600, color: 'var(--black)' } }, `${totalCards} cards`),
339
+ byPriority.P0 > 0 && h('span', { style: { color: PRIORITY_COLORS[0] } }, `${byPriority.P0} P0`),
340
+ byPriority.P1 > 0 && h('span', { style: { color: PRIORITY_COLORS[1] } }, `${byPriority.P1} P1`),
341
+ byPriority.P2 > 0 && h('span', { style: { color: PRIORITY_COLORS[2] } }, `${byPriority.P2} P2`),
342
+ byPriority.P3 > 0 && h('span', null, `${byPriority.P3} P3`),
343
+ h('span', null, '·'),
344
+ h('span', null, `${byStatus['in-progress']} active`),
345
+ h('span', null, `${byStatus.todo} to do`),
346
+ byStatus.review > 0 && h('span', null, `${byStatus.review} in review`),
347
+ byStatus.done > 0 && h('span', null, `${byStatus.done} done`),
348
+ );
349
+ }
350
+
351
+ // ── Tab Switcher (rendered as HTML with inline JS) ─────────────
352
+
353
+ function ViewTabs() {
354
+ // The tabs work via a small inline script injected in the shell
355
+ return h('div', {
356
+ style: {
357
+ display: 'flex', gap: '4px', marginBottom: '1.5rem',
358
+ borderBottom: '2px solid var(--border)', paddingBottom: '0',
359
+ },
360
+ },
361
+ ['Activity', 'Person', 'Timeline'].map(tab =>
362
+ h('button', {
363
+ key: tab,
364
+ className: 'eg-board-tab',
365
+ 'data-view': tab.toLowerCase(),
366
+ style: {
367
+ fontFamily: fonts.mono, fontSize: '13px', padding: '8px 16px',
368
+ background: 'none', border: 'none', borderBottom: '2px solid transparent',
369
+ marginBottom: '-2px', cursor: 'pointer', color: 'var(--muted)',
370
+ },
371
+ }, tab)
372
+ ),
373
+ );
374
+ }
375
+
376
+ // ── Main Template ──────────────────────────────────────────────
377
+
378
+ export function boardTemplate(data) {
379
+ const sections = [];
380
+
381
+ // Header
382
+ sections.push(
383
+ h(ArtifactHeader, {
384
+ key: 'header',
385
+ title: data.title,
386
+ type: 'board',
387
+ date: data.date,
388
+ author: data.updatedBy,
389
+ status: 'active',
390
+ priority: 0,
391
+ projects: [],
392
+ })
393
+ );
394
+
395
+ // Summary bar
396
+ sections.push(h(SummaryBar, { key: 'summary', summary: data.summary }));
397
+
398
+ // Tab switcher
399
+ sections.push(h(ViewTabs, { key: 'tabs' }));
400
+
401
+ // Activity view (default visible)
402
+ sections.push(
403
+ h('div', { key: 'view-activity', className: 'eg-board-view', 'data-view': 'activity' },
404
+ h(ActivityView, { activities: data.activities })
405
+ )
406
+ );
407
+
408
+ // Person view (hidden by default)
409
+ sections.push(
410
+ h('div', { key: 'view-person', className: 'eg-board-view', 'data-view': 'person', style: { display: 'none' } },
411
+ h(PersonView, { people: data.people })
412
+ )
413
+ );
414
+
415
+ // Timeline view (hidden by default)
416
+ sections.push(
417
+ h('div', { key: 'view-timeline', className: 'eg-board-view', 'data-view': 'timeline', style: { display: 'none' } },
418
+ h(TimelineView, { timeline: data.timeline, allCards: data.allCards })
419
+ )
420
+ );
421
+
422
+ // Inline script for tab switching
423
+ sections.push(
424
+ h('script', {
425
+ key: 'tabs-script',
426
+ dangerouslySetInnerHTML: {
427
+ __html: `
428
+ (function() {
429
+ var tabs = document.querySelectorAll('.eg-board-tab');
430
+ var views = document.querySelectorAll('.eg-board-view');
431
+ function activate(viewName) {
432
+ tabs.forEach(function(t) {
433
+ t.style.color = t.dataset.view === viewName ? 'var(--black)' : 'var(--muted)';
434
+ t.style.borderBottomColor = t.dataset.view === viewName ? 'var(--terracotta)' : 'transparent';
435
+ t.style.fontWeight = t.dataset.view === viewName ? '600' : '400';
436
+ });
437
+ views.forEach(function(v) {
438
+ v.style.display = v.dataset.view === viewName ? 'block' : 'none';
439
+ });
440
+ }
441
+ tabs.forEach(function(t) { t.addEventListener('click', function() { activate(t.dataset.view); }); });
442
+ activate('activity');
443
+ })();
444
+ `,
445
+ },
446
+ })
447
+ );
448
+
449
+ // Footer
450
+ sections.push(
451
+ h(ArtifactFooter, {
452
+ key: 'footer',
453
+ generatedAt: new Date().toISOString(),
454
+ source: 'memory/board/board.json',
455
+ })
456
+ );
457
+
458
+ return h('div', null, ...sections);
459
+ }
@@ -5,7 +5,7 @@ import {
5
5
  TextBlock, ArtifactFooter,
6
6
  } from '../components.js';
7
7
  import { renderMarkdownLite } from '../markdown.js';
8
- import { colors, fonts } from '../tokens.js';
8
+ import { fonts } from '../tokens.js';
9
9
 
10
10
  const h = React.createElement;
11
11
 
@@ -38,7 +38,7 @@ export function handoffTemplate(handoff) {
38
38
  marginBottom: '1.5rem',
39
39
  fontFamily: fonts.mono,
40
40
  fontSize: '13px',
41
- color: colors.muted,
41
+ color: 'var(--muted)',
42
42
  },
43
43
  },
44
44
  h('span', null, 'To:'),
@@ -47,9 +47,9 @@ export function handoffTemplate(handoff) {
47
47
  key: i,
48
48
  style: {
49
49
  padding: '2px 10px',
50
- background: 'rgba(123, 157, 183, 0.1)',
50
+ background: 'var(--blue-chip)',
51
51
  borderRadius: '50px',
52
- color: colors.blueMuted,
52
+ color: 'var(--blue-muted)',
53
53
  fontSize: '12px',
54
54
  fontWeight: 500,
55
55
  },
@@ -71,14 +71,14 @@ export function handoffTemplate(handoff) {
71
71
  marginBottom: '2rem',
72
72
  fontFamily: fonts.mono,
73
73
  fontSize: '12px',
74
- color: colors.muted,
74
+ color: 'var(--muted)',
75
75
  },
76
76
  },
77
77
  handoff.source && h('span', null, `Source: ${handoff.source}`),
78
78
  handoff.branch && h('span', {
79
79
  style: {
80
80
  padding: '2px 8px',
81
- background: 'rgba(59, 45, 33, 0.06)',
81
+ background: 'var(--subtle-fill)',
82
82
  borderRadius: '4px',
83
83
  fontFamily: fonts.mono,
84
84
  },
@@ -143,7 +143,7 @@ export function handoffTemplate(handoff) {
143
143
  display: 'flex',
144
144
  gap: '12px',
145
145
  padding: '8px 0',
146
- borderBottom: i < handoff.nextSteps.length - 1 ? '1px solid rgba(224, 216, 204, 0.5)' : 'none',
146
+ borderBottom: i < handoff.nextSteps.length - 1 ? '1px solid var(--hairline)' : 'none',
147
147
  fontSize: '15px',
148
148
  lineHeight: 1.5,
149
149
  },
@@ -154,8 +154,8 @@ export function handoffTemplate(handoff) {
154
154
  width: '24px',
155
155
  height: '24px',
156
156
  borderRadius: '50%',
157
- background: colors.terracotta,
158
- color: colors.cream,
157
+ background: 'var(--terracotta)',
158
+ color: 'var(--cream)',
159
159
  display: 'flex',
160
160
  alignItems: 'center',
161
161
  justifyContent: 'center',