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.
- package/bin/cli.js +3 -3
- package/lib/components.js +2 -2
- package/lib/index.js +6 -2
- package/lib/markdown.js +54 -17
- package/lib/parsers/board.js +138 -0
- package/lib/parsers/network.js +73 -0
- package/lib/registry.js +15 -15
- package/lib/shell.js +160 -9
- package/lib/templates/activity.js +28 -28
- package/lib/templates/board.js +742 -0
- package/lib/templates/handoff.js +9 -9
- package/lib/templates/network.js +189 -0
- package/lib/tokens.js +12 -0
- package/package.json +1 -1
|
@@ -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
|
+
}
|