trackops 2.0.3 → 2.0.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (103) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +695 -402
  3. package/bin/trackops.js +116 -116
  4. package/lib/config.js +326 -326
  5. package/lib/control.js +208 -208
  6. package/lib/env.js +244 -244
  7. package/lib/init.js +325 -325
  8. package/lib/locale.js +24 -0
  9. package/lib/opera-bootstrap.js +941 -874
  10. package/lib/opera.js +494 -477
  11. package/lib/preferences.js +74 -74
  12. package/lib/registry.js +214 -196
  13. package/lib/release.js +56 -56
  14. package/lib/runtime-state.js +144 -144
  15. package/lib/server.js +312 -207
  16. package/lib/skills.js +74 -57
  17. package/lib/workspace.js +260 -260
  18. package/locales/en.json +192 -166
  19. package/locales/es.json +192 -166
  20. package/package.json +61 -58
  21. package/scripts/postinstall-locale.js +21 -21
  22. package/scripts/skills-marketplace-smoke.js +124 -124
  23. package/scripts/smoke-tests.js +558 -554
  24. package/scripts/sync-skill-version.js +21 -21
  25. package/scripts/validate-skill.js +103 -103
  26. package/skills/trackops/SKILL.md +126 -122
  27. package/skills/trackops/agents/openai.yaml +7 -7
  28. package/skills/trackops/locales/en/SKILL.md +126 -122
  29. package/skills/trackops/locales/en/references/activation.md +94 -75
  30. package/skills/trackops/locales/en/references/troubleshooting.md +73 -55
  31. package/skills/trackops/locales/en/references/workflow.md +55 -32
  32. package/skills/trackops/references/activation.md +94 -75
  33. package/skills/trackops/references/troubleshooting.md +73 -55
  34. package/skills/trackops/references/workflow.md +55 -32
  35. package/skills/trackops/skill.json +29 -29
  36. package/templates/hooks/post-checkout +2 -2
  37. package/templates/hooks/post-commit +2 -2
  38. package/templates/hooks/post-merge +2 -2
  39. package/templates/opera/agent.md +28 -27
  40. package/templates/opera/architecture/dependency-graph.md +24 -24
  41. package/templates/opera/architecture/runtime-automation.md +24 -24
  42. package/templates/opera/architecture/runtime-operations.md +34 -34
  43. package/templates/opera/en/agent.md +22 -21
  44. package/templates/opera/en/architecture/dependency-graph.md +24 -24
  45. package/templates/opera/en/architecture/runtime-automation.md +24 -24
  46. package/templates/opera/en/architecture/runtime-operations.md +34 -34
  47. package/templates/opera/en/reviews/delivery-audit.md +18 -18
  48. package/templates/opera/en/reviews/integration-audit.md +18 -18
  49. package/templates/opera/en/router.md +24 -19
  50. package/templates/opera/references/autonomy-and-recovery.md +117 -117
  51. package/templates/opera/references/opera-cycle.md +193 -193
  52. package/templates/opera/registry.md +28 -28
  53. package/templates/opera/reviews/delivery-audit.md +18 -18
  54. package/templates/opera/reviews/integration-audit.md +18 -18
  55. package/templates/opera/router.md +54 -49
  56. package/templates/skills/changelog-updater/SKILL.md +69 -69
  57. package/templates/skills/commiter/SKILL.md +99 -99
  58. package/templates/skills/opera-contract-auditor/SKILL.md +38 -38
  59. package/templates/skills/opera-contract-auditor/locales/en/SKILL.md +38 -38
  60. package/templates/skills/opera-policy-guard/SKILL.md +26 -26
  61. package/templates/skills/opera-policy-guard/locales/en/SKILL.md +26 -26
  62. package/templates/skills/opera-skill/SKILL.md +279 -0
  63. package/templates/skills/opera-skill/locales/en/SKILL.md +279 -0
  64. package/templates/skills/opera-skill/locales/en/references/phase-dod.md +138 -0
  65. package/templates/skills/opera-skill/references/phase-dod.md +138 -0
  66. package/templates/skills/project-starter-skill/SKILL.md +150 -131
  67. package/templates/skills/project-starter-skill/locales/en/SKILL.md +143 -105
  68. package/templates/skills/project-starter-skill/references/opera-cycle.md +195 -193
  69. package/ui/css/base.css +284 -266
  70. package/ui/css/charts.css +425 -327
  71. package/ui/css/components.css +1107 -570
  72. package/ui/css/onboarding.css +133 -0
  73. package/ui/css/panels.css +345 -406
  74. package/ui/css/terminal.css +125 -0
  75. package/ui/css/timeline.css +58 -0
  76. package/ui/css/tokens.css +284 -227
  77. package/ui/favicon.svg +5 -5
  78. package/ui/index.html +99 -96
  79. package/ui/js/api.js +49 -13
  80. package/ui/js/app.js +28 -32
  81. package/ui/js/charts.js +526 -0
  82. package/ui/js/console-logger.js +172 -172
  83. package/ui/js/filters.js +247 -0
  84. package/ui/js/icons.js +129 -104
  85. package/ui/js/keyboard.js +229 -0
  86. package/ui/js/onboarding.js +33 -42
  87. package/ui/js/router.js +142 -125
  88. package/ui/js/theme.js +100 -100
  89. package/ui/js/time-tracker.js +248 -248
  90. package/ui/js/views/board.js +84 -114
  91. package/ui/js/views/dashboard.js +870 -0
  92. package/ui/js/views/flash.js +47 -47
  93. package/ui/js/views/projects.js +745 -0
  94. package/ui/js/views/scrum.js +476 -0
  95. package/ui/js/views/settings.js +153 -203
  96. package/ui/js/views/sidebar.js +37 -31
  97. package/ui/js/views/tasks.js +218 -101
  98. package/ui/js/views/timeline.js +265 -0
  99. package/ui/js/views/topbar.js +94 -107
  100. package/ui/app.js +0 -950
  101. package/ui/js/views/insights.js +0 -340
  102. package/ui/js/views/overview.js +0 -369
  103. package/ui/styles.css +0 -688
@@ -0,0 +1,526 @@
1
+ /**
2
+ * charts.js — Motor de gráficos SVG para TrackOps Dashboard
3
+ * Vanilla JS, zero dependencies. Usa CSS custom properties de tokens.css.
4
+ */
5
+
6
+ // ─── Helpers ────────────────────────────────────────────────────
7
+
8
+ function clamp(v, lo, hi) { return Math.max(lo, Math.min(hi, v)); }
9
+
10
+ function scale(value, inMin, inMax, outMin, outMax) {
11
+ if (inMax === inMin) return (outMin + outMax) / 2;
12
+ return outMin + ((value - inMin) / (inMax - inMin)) * (outMax - outMin);
13
+ }
14
+
15
+ function esc(str) {
16
+ return String(str).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
17
+ }
18
+
19
+ function niceTicksCount(range) {
20
+ if (range <= 0) return [0, 1, 2, 3, 4];
21
+ const rough = range / 4;
22
+ const mag = Math.pow(10, Math.floor(Math.log10(rough)));
23
+ const res = rough / mag;
24
+ let step;
25
+ if (res <= 1.5) step = mag;
26
+ else if (res <= 3) step = 2 * mag;
27
+ else if (res <= 7) step = 5 * mag;
28
+ else step = 10 * mag;
29
+ return step;
30
+ }
31
+
32
+ function niceTicks(min, max, count = 5) {
33
+ if (min === max) {
34
+ const v = min;
35
+ return [v - 2, v - 1, v, v + 1, v + 2];
36
+ }
37
+ const step = niceTicksCount(max - min);
38
+ const lo = Math.floor(min / step) * step;
39
+ const hi = Math.ceil(max / step) * step;
40
+ const ticks = [];
41
+ for (let v = lo; v <= hi + step * 0.001; v += step) {
42
+ ticks.push(Math.round(v * 1e6) / 1e6);
43
+ }
44
+ // trim to reasonable count
45
+ while (ticks.length > count + 2) {
46
+ const newTicks = [];
47
+ for (let i = 0; i < ticks.length; i += 2) newTicks.push(ticks[i]);
48
+ ticks.length = 0;
49
+ ticks.push(...newTicks);
50
+ }
51
+ return ticks;
52
+ }
53
+
54
+ function fmtNum(n) {
55
+ if (Math.abs(n) >= 1e6) return (n / 1e6).toFixed(1) + 'M';
56
+ if (Math.abs(n) >= 1e3) return (n / 1e3).toFixed(1) + 'k';
57
+ return Number.isInteger(n) ? String(n) : n.toFixed(1);
58
+ }
59
+
60
+ function uid() {
61
+ return 'to_' + Math.random().toString(36).slice(2, 9);
62
+ }
63
+
64
+ // ─── Topological sort ───────────────────────────────────────────
65
+
66
+ function topoSort(tasks) {
67
+ const map = new Map();
68
+ tasks.forEach(t => map.set(t.id, t));
69
+
70
+ const levels = new Map();
71
+ const visited = new Set();
72
+
73
+ function depth(id) {
74
+ if (levels.has(id)) return levels.get(id);
75
+ if (visited.has(id)) return 0; // cycle guard
76
+ visited.add(id);
77
+ const t = map.get(id);
78
+ if (!t || !t.dependsOn || t.dependsOn.length === 0) {
79
+ levels.set(id, 0);
80
+ return 0;
81
+ }
82
+ let d = 0;
83
+ for (const dep of t.dependsOn) {
84
+ if (map.has(dep)) d = Math.max(d, depth(dep) + 1);
85
+ }
86
+ levels.set(id, d);
87
+ return d;
88
+ }
89
+
90
+ tasks.forEach(t => depth(t.id));
91
+ return levels;
92
+ }
93
+
94
+ // ─── 1. sparkline ───────────────────────────────────────────────
95
+
96
+ export function sparkline(values, options = {}) {
97
+ if (!Array.isArray(values) || values.length < 2) return '';
98
+
99
+ const {
100
+ width = 80,
101
+ height = 24,
102
+ color: userColor,
103
+ strokeWidth = 2,
104
+ } = options;
105
+
106
+ const first = values[0];
107
+ const last = values[values.length - 1];
108
+ let color = userColor;
109
+ if (!color) {
110
+ if (last > first) color = 'var(--success)';
111
+ else if (last < first) color = 'var(--danger)';
112
+ else color = 'var(--accent)';
113
+ }
114
+
115
+ const min = Math.min(...values);
116
+ const max = Math.max(...values);
117
+ const padY = strokeWidth;
118
+
119
+ const points = values.map((v, i) => {
120
+ const x = scale(i, 0, values.length - 1, strokeWidth, width - strokeWidth);
121
+ const y = scale(v, min, max, height - padY, padY);
122
+ return `${x.toFixed(1)},${y.toFixed(1)}`;
123
+ }).join(' ');
124
+
125
+ return `<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}" viewBox="0 0 ${width} ${height}" role="img" aria-label="Sparkline: ${first} to ${last}"><polyline fill="none" stroke="${color}" stroke-width="${strokeWidth}" stroke-linecap="round" stroke-linejoin="round" points="${points}"/></svg>`;
126
+ }
127
+
128
+ // ─── 2. lineChart ───────────────────────────────────────────────
129
+
130
+ export function lineChart(data, options = {}) {
131
+ if (!Array.isArray(data) || data.length === 0) return '';
132
+
133
+ const {
134
+ width = 480,
135
+ height = 200,
136
+ color = 'var(--accent)',
137
+ fill = true,
138
+ showDots = true,
139
+ showGrid = true,
140
+ animate = true,
141
+ yLabel = '',
142
+ } = options;
143
+
144
+ const values = data.map(d => d.value);
145
+ const labels = data.map(d => d.label);
146
+ const rawMin = Math.min(...values);
147
+ const rawMax = Math.max(...values);
148
+ const ticks = niceTicks(rawMin, rawMax, 5);
149
+ const yMin = ticks[0];
150
+ const yMax = ticks[ticks.length - 1];
151
+
152
+ // Layout constants
153
+ const padLeft = 48;
154
+ const padRight = 12;
155
+ const padTop = 12;
156
+ const padBottom = 36;
157
+ const plotW = width - padLeft - padRight;
158
+ const plotH = height - padTop - padBottom;
159
+
160
+ const gradId = uid();
161
+ const animId = uid();
162
+
163
+ // Map data to plot coords
164
+ const pts = data.map((d, i) => {
165
+ const x = padLeft + scale(i, 0, Math.max(data.length - 1, 1), 0, plotW);
166
+ const y = padTop + scale(d.value, yMin, yMax, plotH, 0);
167
+ return { x, y, v: d.value, l: d.label };
168
+ });
169
+
170
+ const polyPoints = pts.map(p => `${p.x.toFixed(1)},${p.y.toFixed(1)}`).join(' ');
171
+ const areaPath = pts.length
172
+ ? `M${pts[0].x.toFixed(1)},${(padTop + plotH).toFixed(1)} ` +
173
+ pts.map(p => `L${p.x.toFixed(1)},${p.y.toFixed(1)}`).join(' ') +
174
+ ` L${pts[pts.length - 1].x.toFixed(1)},${(padTop + plotH).toFixed(1)} Z`
175
+ : '';
176
+
177
+ let svg = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${width} ${height}" width="${width}" height="${height}" role="img" aria-label="Line chart${yLabel ? ': ' + esc(yLabel) : ''} — ${data.length} points, min ${fmtNum(rawMin)}, max ${fmtNum(rawMax)}">`;
178
+
179
+ // Defs: gradient for area fill + optional clip animation
180
+ svg += `<defs>`;
181
+ if (fill) {
182
+ svg += `<linearGradient id="${gradId}" x1="0" y1="0" x2="0" y2="1">` +
183
+ `<stop offset="0%" stop-color="${color}" stop-opacity="0.35"/>` +
184
+ `<stop offset="100%" stop-color="${color}" stop-opacity="0"/>` +
185
+ `</linearGradient>`;
186
+ }
187
+ if (animate) {
188
+ svg += `<clipPath id="${animId}"><rect x="${padLeft}" y="0" width="0" height="${height}">` +
189
+ `<animate attributeName="width" from="0" to="${plotW + padRight}" dur="0.8s" fill="freeze" calcMode="spline" keySplines="0.16 1 0.3 1"/></rect></clipPath>`;
190
+ }
191
+ svg += `</defs>`;
192
+
193
+ const clipAttr = animate ? ` clip-path="url(#${animId})"` : '';
194
+
195
+ // Grid lines
196
+ if (showGrid) {
197
+ for (const t of ticks) {
198
+ const gy = padTop + scale(t, yMin, yMax, plotH, 0);
199
+ svg += `<line class="chart-grid-line" x1="${padLeft}" y1="${gy.toFixed(1)}" x2="${width - padRight}" y2="${gy.toFixed(1)}" stroke="var(--border)" stroke-dasharray="4 3" stroke-width="1"/>`;
200
+ }
201
+ }
202
+
203
+ // Y-axis labels
204
+ for (const t of ticks) {
205
+ const gy = padTop + scale(t, yMin, yMax, plotH, 0);
206
+ svg += `<text class="chart-axis-label" x="${padLeft - 6}" y="${(gy + 3).toFixed(1)}" text-anchor="end" fill="var(--text-muted)" font-size="10" font-family="var(--font-ui)">${fmtNum(t)}</text>`;
207
+ }
208
+
209
+ // X-axis labels
210
+ const maxLabels = Math.floor(plotW / 40);
211
+ const step = Math.max(1, Math.ceil(data.length / maxLabels));
212
+ const rotateLabels = data.length > 10;
213
+ for (let i = 0; i < data.length; i += step) {
214
+ const lx = pts[i].x;
215
+ const ly = height - padBottom + 16;
216
+ if (rotateLabels) {
217
+ svg += `<text class="chart-axis-label" x="${lx.toFixed(1)}" y="${ly.toFixed(1)}" text-anchor="end" fill="var(--text-muted)" font-size="10" font-family="var(--font-ui)" transform="rotate(-45 ${lx.toFixed(1)} ${ly.toFixed(1)})">${esc(labels[i])}</text>`;
218
+ } else {
219
+ svg += `<text class="chart-axis-label" x="${lx.toFixed(1)}" y="${ly.toFixed(1)}" text-anchor="middle" fill="var(--text-muted)" font-size="10" font-family="var(--font-ui)">${esc(labels[i])}</text>`;
220
+ }
221
+ }
222
+
223
+ // Area fill
224
+ if (fill && areaPath) {
225
+ svg += `<path class="chart-area" d="${areaPath}" fill="url(#${gradId})"${clipAttr}/>`;
226
+ }
227
+
228
+ // Line
229
+ svg += `<polyline class="chart-line" fill="none" stroke="${color}" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" points="${polyPoints}"${clipAttr}/>`;
230
+
231
+ // Dots
232
+ if (showDots) {
233
+ for (const p of pts) {
234
+ svg += `<circle class="chart-dot" cx="${p.x.toFixed(1)}" cy="${p.y.toFixed(1)}" r="3" fill="${color}" stroke="var(--surface-2)" stroke-width="1.5"${clipAttr}><title>${esc(p.l)}: ${fmtNum(p.v)}</title></circle>`;
235
+ }
236
+ }
237
+
238
+ // Y-label
239
+ if (yLabel) {
240
+ svg += `<text class="chart-axis-label" x="4" y="${(padTop + plotH / 2).toFixed(1)}" transform="rotate(-90 4 ${(padTop + plotH / 2).toFixed(1)})" text-anchor="middle" fill="var(--text-secondary)" font-size="10" font-family="var(--font-ui)">${esc(yLabel)}</text>`;
241
+ }
242
+
243
+ svg += `</svg>`;
244
+ return svg;
245
+ }
246
+
247
+ // ─── 3. barChart ────────────────────────────────────────────────
248
+
249
+ export function barChart(data, options = {}) {
250
+ if (!Array.isArray(data) || data.length === 0) return '';
251
+
252
+ const {
253
+ maxWidth = 480,
254
+ barHeight = 28,
255
+ gap = 8,
256
+ showValues = true,
257
+ animate = true,
258
+ } = options;
259
+
260
+ const maxVal = Math.max(...data.map(d => d.value), 1);
261
+
262
+ const rows = data.map((d, i) => {
263
+ const pct = clamp((d.value / maxVal) * 100, 0, 100);
264
+ const barColor = d.color || 'var(--accent)';
265
+ const transition = animate ? `transition: width 0.6s var(--ease-out) ${i * 0.06}s;` : '';
266
+
267
+ return `<div class="bar-row" style="display:flex;align-items:center;gap:8px;height:${barHeight}px;margin-bottom:${gap}px;">` +
268
+ `<span class="bar-label" style="min-width:80px;font-size:var(--text-sm);color:var(--text-secondary);text-align:right;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-family:var(--font-ui);" title="${esc(d.label)}">${esc(d.label)}</span>` +
269
+ `<div class="bar-track" role="progressbar" aria-valuenow="${d.value}" aria-valuemin="0" aria-valuemax="${maxVal}" aria-label="${esc(d.label)}: ${d.value}" style="flex:1;height:100%;background:var(--surface-4);border-radius:var(--radius-xs);overflow:hidden;position:relative;">` +
270
+ `<div class="bar-fill" style="width:${pct.toFixed(1)}%;height:100%;background:${barColor};border-radius:var(--radius-xs);${transition}"></div>` +
271
+ `</div>` +
272
+ (showValues
273
+ ? `<span class="bar-value" style="min-width:40px;font-size:var(--text-sm);color:var(--text-primary);font-variant-numeric:tabular-nums;font-family:var(--font-mono);text-align:right;">${fmtNum(d.value)}</span>`
274
+ : '') +
275
+ `</div>`;
276
+ }).join('');
277
+
278
+ return `<div class="bar-chart" style="max-width:${maxWidth}px;width:100%;" role="img" aria-label="Bar chart — ${data.length} items">${rows}</div>`;
279
+ }
280
+
281
+ // ─── 4. donutChart ──────────────────────────────────────────────
282
+
283
+ export function donutChart(segments, options = {}) {
284
+ if (!Array.isArray(segments) || segments.length === 0) return '';
285
+
286
+ const {
287
+ size = 160,
288
+ strokeWidth = 14,
289
+ centerLabel = '',
290
+ centerSub = '',
291
+ animate = true,
292
+ } = options;
293
+
294
+ const total = segments.reduce((s, d) => s + d.value, 0);
295
+ if (total <= 0) return '';
296
+
297
+ const r = (size - strokeWidth) / 2;
298
+ const cx = size / 2;
299
+ const cy = size / 2;
300
+ const circumference = 2 * Math.PI * r;
301
+
302
+ // Build arcs
303
+ let offsetSoFar = 0;
304
+ const arcs = segments.map((seg, i) => {
305
+ const frac = seg.value / total;
306
+ const dash = frac * circumference;
307
+ const gap = circumference - dash;
308
+ const offset = -offsetSoFar + circumference * 0.25; // start at top
309
+
310
+ const animAttr = animate
311
+ ? `<animate attributeName="stroke-dashoffset" from="${circumference + circumference * 0.25}" to="${offset.toFixed(2)}" dur="0.7s" fill="freeze" calcMode="spline" keySplines="0.16 1 0.3 1" begin="${(i * 0.08).toFixed(2)}s"/>`
312
+ : '';
313
+
314
+ const arc = `<circle class="donut-arc" cx="${cx}" cy="${cy}" r="${r.toFixed(1)}" fill="none" stroke="${seg.color || 'var(--accent)'}" stroke-width="${strokeWidth}" stroke-dasharray="${dash.toFixed(2)} ${gap.toFixed(2)}" stroke-dashoffset="${offset.toFixed(2)}" stroke-linecap="round"><title>${esc(seg.label)}: ${seg.value} (${(frac * 100).toFixed(1)}%)</title>${animAttr}</circle>`;
315
+
316
+ offsetSoFar += dash;
317
+ return arc;
318
+ });
319
+
320
+ let svg = `<svg class="donut-svg" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${size} ${size}" width="${size}" height="${size}" role="img" aria-label="Donut chart — ${segments.map(s => esc(s.label) + ': ' + s.value).join(', ')}">`;
321
+
322
+ // Track background
323
+ svg += `<circle class="donut-track" cx="${cx}" cy="${cy}" r="${r.toFixed(1)}" fill="none" stroke="var(--surface-4)" stroke-width="${strokeWidth}"/>`;
324
+
325
+ // Arcs
326
+ svg += arcs.join('');
327
+
328
+ // Center label
329
+ if (centerLabel) {
330
+ svg += `<text class="donut-label" x="${cx}" y="${centerSub ? cy - 2 : cy}" text-anchor="middle" dominant-baseline="central" fill="var(--text-primary)" font-size="22" font-weight="700" font-family="var(--font-heading)">${esc(centerLabel)}</text>`;
331
+ }
332
+ if (centerSub) {
333
+ svg += `<text class="donut-label" x="${cx}" y="${cy + 16}" text-anchor="middle" dominant-baseline="central" fill="var(--text-muted)" font-size="10" font-family="var(--font-ui)">${esc(centerSub)}</text>`;
334
+ }
335
+
336
+ svg += `</svg>`;
337
+
338
+ // Legend
339
+ const legend = segments.map(seg => {
340
+ const pct = ((seg.value / total) * 100).toFixed(0);
341
+ return `<span style="display:inline-flex;align-items:center;gap:4px;margin-right:12px;font-size:var(--text-xs);color:var(--text-secondary);font-family:var(--font-ui);">` +
342
+ `<span style="width:8px;height:8px;border-radius:50%;background:${seg.color || 'var(--accent)'};flex-shrink:0;"></span>` +
343
+ `${esc(seg.label)} ${pct}%</span>`;
344
+ }).join('');
345
+
346
+ return `<div style="display:inline-flex;flex-direction:column;align-items:center;gap:8px;">${svg}<div style="display:flex;flex-wrap:wrap;justify-content:center;">${legend}</div></div>`;
347
+ }
348
+
349
+ // ─── 5. heatmap ─────────────────────────────────────────────────
350
+
351
+ export function heatmap(data, options = {}) {
352
+ if (!Array.isArray(data)) return '';
353
+
354
+ const {
355
+ weeks = 12,
356
+ cellSize = 14,
357
+ gap = 3,
358
+ colors = null,
359
+ } = options;
360
+
361
+ const intensityColors = colors || [
362
+ 'transparent',
363
+ 'rgba(99,102,241,0.15)',
364
+ 'rgba(99,102,241,0.30)',
365
+ 'rgba(99,102,241,0.55)',
366
+ 'rgba(99,102,241,0.85)',
367
+ ];
368
+
369
+ // Build lookup
370
+ const lookup = new Map();
371
+ let maxCount = 0;
372
+ data.forEach(d => {
373
+ lookup.set(d.date, d.count);
374
+ if (d.count > maxCount) maxCount = d.count;
375
+ });
376
+
377
+ function intensity(count) {
378
+ if (!count || count <= 0) return 0;
379
+ if (maxCount === 0) return 0;
380
+ const ratio = count / maxCount;
381
+ if (ratio <= 0.25) return 1;
382
+ if (ratio <= 0.50) return 2;
383
+ if (ratio <= 0.75) return 3;
384
+ return 4;
385
+ }
386
+
387
+ // Build grid: work backwards from today
388
+ const today = new Date();
389
+ // Find the most recent Sunday (start-of-week for grid alignment)
390
+ const endDate = new Date(today);
391
+ endDate.setDate(endDate.getDate() + (6 - endDate.getDay())); // end on Saturday
392
+
393
+ const startDate = new Date(endDate);
394
+ startDate.setDate(startDate.getDate() - (weeks * 7 - 1));
395
+ // Align to Monday
396
+ startDate.setDate(startDate.getDate() - ((startDate.getDay() + 6) % 7));
397
+
398
+ const dayLabels = ['L', '', 'X', '', 'V', '', 'D'];
399
+ const labelW = 18;
400
+ const svgW = labelW + weeks * (cellSize + gap);
401
+ const svgH = 7 * (cellSize + gap);
402
+
403
+ let svg = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${svgW} ${svgH}" width="${svgW}" height="${svgH}" role="img" aria-label="Activity heatmap — ${weeks} weeks">`;
404
+
405
+ // Day labels
406
+ for (let row = 0; row < 7; row++) {
407
+ if (dayLabels[row]) {
408
+ svg += `<text x="0" y="${row * (cellSize + gap) + cellSize - 2}" fill="var(--text-muted)" font-size="9" font-family="var(--font-ui)">${dayLabels[row]}</text>`;
409
+ }
410
+ }
411
+
412
+ // Cells
413
+ const cursor = new Date(startDate);
414
+ for (let w = 0; w < weeks; w++) {
415
+ for (let d = 0; d < 7; d++) {
416
+ const dateStr = cursor.toISOString().slice(0, 10);
417
+ const count = lookup.get(dateStr) || 0;
418
+ const level = intensity(count);
419
+ const x = labelW + w * (cellSize + gap);
420
+ const y = d * (cellSize + gap);
421
+ const fillColor = intensityColors[level];
422
+ const border = level === 0 ? ' stroke="var(--border)" stroke-width="1"' : '';
423
+ svg += `<rect class="heatmap-cell" x="${x}" y="${y}" width="${cellSize}" height="${cellSize}" rx="2" fill="${fillColor}"${border}><title>${dateStr}: ${count}</title></rect>`;
424
+ cursor.setDate(cursor.getDate() + 1);
425
+ }
426
+ }
427
+
428
+ svg += `</svg>`;
429
+ return svg;
430
+ }
431
+
432
+ // ─── 6. dependencyGraph ─────────────────────────────────────────
433
+
434
+ export function dependencyGraph(tasks, options = {}) {
435
+ if (!Array.isArray(tasks) || tasks.length === 0) return '';
436
+
437
+ // Only render if at least one task has dependencies
438
+ const hasDeps = tasks.some(t => t.dependsOn && t.dependsOn.length > 0);
439
+ if (!hasDeps) return '';
440
+
441
+ const {
442
+ nodeWidth = 140,
443
+ nodeHeight = 36,
444
+ levelGap = 60,
445
+ rowGap = 16,
446
+ } = options;
447
+
448
+ const statusColors = {
449
+ completed: 'var(--success)',
450
+ in_progress: 'var(--accent)',
451
+ blocked: 'var(--danger)',
452
+ pending: 'var(--text-muted)',
453
+ };
454
+
455
+ // Topological levels
456
+ const levels = topoSort(tasks);
457
+ const taskMap = new Map();
458
+ tasks.forEach(t => taskMap.set(t.id, t));
459
+
460
+ // Group tasks by level
461
+ const byLevel = new Map();
462
+ tasks.forEach(t => {
463
+ const lv = levels.get(t.id) || 0;
464
+ if (!byLevel.has(lv)) byLevel.set(lv, []);
465
+ byLevel.get(lv).push(t);
466
+ });
467
+
468
+ const maxLevel = Math.max(...byLevel.keys(), 0);
469
+ const maxRowCount = Math.max(...[...byLevel.values()].map(a => a.length), 1);
470
+
471
+ const colWidth = nodeWidth + levelGap;
472
+ const rowHeight = nodeHeight + rowGap;
473
+ const svgW = (maxLevel + 1) * colWidth + levelGap;
474
+ const svgH = maxRowCount * rowHeight + rowGap;
475
+
476
+ // Compute positions
477
+ const positions = new Map();
478
+ for (let lv = 0; lv <= maxLevel; lv++) {
479
+ const group = byLevel.get(lv) || [];
480
+ const totalH = group.length * rowHeight;
481
+ const startY = (svgH - totalH) / 2;
482
+ group.forEach((t, idx) => {
483
+ positions.set(t.id, {
484
+ x: levelGap / 2 + lv * colWidth,
485
+ y: startY + idx * rowHeight,
486
+ });
487
+ });
488
+ }
489
+
490
+ let svg = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${svgW} ${svgH}" width="${svgW}" height="${svgH}" role="img" aria-label="Dependency graph — ${tasks.length} tasks">`;
491
+
492
+ // Draw edges first (behind nodes)
493
+ tasks.forEach(t => {
494
+ if (!t.dependsOn) return;
495
+ const target = positions.get(t.id);
496
+ if (!target) return;
497
+ t.dependsOn.forEach(depId => {
498
+ const source = positions.get(depId);
499
+ if (!source) return;
500
+ const x1 = source.x + nodeWidth;
501
+ const y1 = source.y + nodeHeight / 2;
502
+ const x2 = target.x;
503
+ const y2 = target.y + nodeHeight / 2;
504
+ const cpx = (x1 + x2) / 2;
505
+ svg += `<path d="M${x1},${y1} Q${cpx},${y1} ${cpx},${(y1 + y2) / 2} Q${cpx},${y2} ${x2},${y2}" fill="none" stroke="var(--border-strong)" stroke-width="1.5" stroke-linecap="round"/>`;
506
+ // Arrow
507
+ svg += `<polygon points="${x2},${y2} ${x2 - 5},${y2 - 3} ${x2 - 5},${y2 + 3}" fill="var(--border-strong)"/>`;
508
+ });
509
+ });
510
+
511
+ // Draw nodes
512
+ tasks.forEach(t => {
513
+ const pos = positions.get(t.id);
514
+ if (!pos) return;
515
+ const col = statusColors[t.status] || 'var(--text-muted)';
516
+ const truncTitle = t.title.length > 16 ? t.title.slice(0, 15) + '\u2026' : t.title;
517
+
518
+ svg += `<rect x="${pos.x}" y="${pos.y}" width="${nodeWidth}" height="${nodeHeight}" rx="6" fill="var(--surface-2)" stroke="${col}" stroke-width="1.5"/>`;
519
+ svg += `<text x="${pos.x + nodeWidth / 2}" y="${pos.y + nodeHeight / 2 + 1}" text-anchor="middle" dominant-baseline="central" fill="var(--text-primary)" font-size="11" font-family="var(--font-ui)"><title>${esc(t.title)}</title>${esc(truncTitle)}</text>`;
520
+ // Status dot
521
+ svg += `<circle cx="${pos.x + 10}" cy="${pos.y + nodeHeight / 2}" r="3" fill="${col}"/>`;
522
+ });
523
+
524
+ svg += `</svg>`;
525
+ return svg;
526
+ }