Files
commitea/docs/design/ui_kits/app/Chart.js.txt
Christian LeDoux 7a5cacc54c Scaffold CommiTea: yarn workspaces, Electron shell, core label schema, design system
- apps/desktop: electron-vite + React + Tailwind mapped onto design tokens
  (preflight off; tokens/base.css owns the reset); boots to a Reginald
  placeholder proving fonts/tokens/core wiring
- packages/core: pure TS; gitea label schema (est/*, p/*, deadline/hard)
  with pessimistic conflict resolution + 15 unit tests
- docs/design: full design handoff (tokens, 16 component contracts,
  interactive 14-screen prototype, Reginald voice rules)
- docs/PLAN.md: product plan (purity rule, pm-state repo, deterministic
  scheduler + Monte Carlo, directive log)
- Deliberate deviation from novelpad stack: no ElectricSQL/PGlite — local
  store is a rebuildable cache over gitea REST/webhooks (better-sqlite3
  in main process, arriving in P1)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-07 20:42:46 -04:00

71 lines
3.9 KiB
Plaintext
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// Burn-up chart with Monte Carlo forecast cone — geometry, not decoration.
function BurnUpCone({ width = 640, height = 220 }) {
const pad = { l: 34, r: 96, t: 16, b: 26 };
const W = width - pad.l - pad.r;
const H = height - pad.t - pad.b;
const x = (f) => pad.l + f * W;
const y = (f) => pad.t + (1 - f) * H;
// scope line (total work), actual completed, cone from today
const today = 0.58;
const actual = [[0, 0], [0.08, 0.05], [0.18, 0.13], [0.26, 0.16], [0.36, 0.27], [0.46, 0.38], [0.58, 0.47]];
const coneHi = [[0.58, 0.47], [0.72, 0.66], [0.86, 0.88], [0.95, 1.0]];
const coneLo = [[0.58, 0.47], [0.74, 0.58], [0.9, 0.74], [1.0, 0.86]];
const mid = [[0.58, 0.47], [0.76, 0.63], [0.92, 0.83], [1.0, 0.93]];
const pts = (arr) => arr.map(([a, b]) => `${x(a)},${y(b)}`).join(' ');
const cone = [...coneHi, ...[...coneLo].reverse()];
return (
<svg width="100%" viewBox={`0 0 ${width} ${height}`} style={{ display: 'block' }}>
{/* gridlines */}
{[0, 0.25, 0.5, 0.75, 1].map((f) => (
<line key={f} x1={pad.l} x2={width - pad.r} y1={y(f)} y2={y(f)}
stroke="var(--line-1)" strokeWidth="1" />
))}
{/* scope */}
<line x1={pad.l} x2={width - pad.r} y1={y(1)} y2={y(1)} stroke="var(--line-2)" strokeWidth="1.5" />
<text x={pad.l} y={y(1) - 6} style={{ font: '400 10.5px var(--font-mono)', fill: 'var(--ink-3)' }}>scope · 42 issues</text>
{/* cone */}
<polygon points={pts(cone)} fill="var(--cone-fill)" />
<polyline points={pts(coneHi)} fill="none" stroke="var(--cone-line)" strokeWidth="1.2" strokeDasharray="3 3" />
<polyline points={pts(coneLo)} fill="none" stroke="var(--cone-line)" strokeWidth="1.2" strokeDasharray="3 3" />
<polyline points={pts(mid)} fill="none" stroke="var(--cone-line)" strokeWidth="1.4" />
{/* actual */}
<polyline points={pts(actual)} fill="none" stroke="var(--cone-actual)" strokeWidth="2" />
<circle cx={x(0.58)} cy={y(0.47)} r="3.5" fill="var(--cone-actual)" />
{/* today rule */}
<line x1={x(today)} x2={x(today)} y1={pad.t} y2={height - pad.b} stroke="var(--jade)" strokeWidth="1" />
<text x={x(today) + 5} y={pad.t + 10} style={{ font: '400 10.5px var(--font-mono)', fill: 'var(--jade-7)' }}>today</text>
{/* 80% band bracket */}
<line x1={x(0.95) + 4} x2={x(0.95) + 4} y1={y(1.0)} y2={y(0.86) + H * 0.14 * 0 + (y(0.86) - y(0.86))} stroke="none" />
<text x={width - pad.r + 10} y={y(0.95)} style={{ font: '500 11.5px var(--font-mono)', fill: 'var(--ink-1)' }}>80%</text>
<text x={width - pad.r + 10} y={y(0.95) + 14} style={{ font: '400 11px var(--font-mono)', fill: 'var(--ink-2)' }}>Mar 312</text>
{/* x labels */}
<text x={pad.l} y={height - 8} style={{ font: '400 10.5px var(--font-mono)', fill: 'var(--ink-3)' }}>Jan 6</text>
<text x={width - pad.r - 34} y={height - 8} style={{ font: '400 10.5px var(--font-mono)', fill: 'var(--ink-3)' }}>Mar 15</text>
</svg>
);
}
// Runway bar: milestone due date vs forecast range position
function RunwayBar({ m }) {
const toneColor = m.tone === 'warn' ? 'var(--warn)' : 'var(--ok)';
const left = Math.max(0, (m.pos - m.spread / 2) * 100);
const w = Math.min(100 - left, m.spread * 100);
return (
<div style={{ position: 'relative', height: 22, background: 'var(--paper-2)', borderRadius: 4, overflow: 'hidden' }}>
<div style={{
position: 'absolute', left: `${left}%`, width: `${w}%`, top: 0, bottom: 0,
background: 'var(--cone-fill)', borderLeft: `1.5px solid ${toneColor}`, borderRight: `1.5px solid ${toneColor}`,
}}></div>
<div style={{
position: 'absolute', left: `${m.pos * 100}%`, top: 0, bottom: 0, width: 2, background: toneColor,
}}></div>
{/* due marker */}
<div style={{ position: 'absolute', left: 'calc(88% - 1px)', top: 0, bottom: 0, width: 2, background: 'var(--ink-1)' }}></div>
</div>
);
}
Object.assign(window, { BurnUpCone, RunwayBar });