Skip to content

Commit 8ae76a3

Browse files
Necmttnclaude
andauthored
feat(site): profile reskin + dedicated comparison + shared @ax/recap-deck charts (#523)
* feat(recap-deck): shared nullframe chart deck package Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(studio): consume @ax/recap-deck for charts (de-dup) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(site): reskin shared profile-dossier (landing-v2 + recap deck + avatars) Reskins ProfileDossier so both /u/<login> and the /u/<a>/vs/<b> duel route get the landing-v2 hero, @ax/recap-deck wrapped deck, GitHub avatars, and grounded chart-viz. Comparison page inherits everything via the shared component. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(site): landing wrapped popups via @ax/recap-deck (kill mc-* charts) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(recap-deck): noUncheckedIndexedAccess-safe card-viz for strict site typecheck Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * docs(site): profile reskin + recap-deck design spec Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(recap-deck): isolate package from root CLI typecheck Root tsconfig swept packages/**/*.ts → index.ts re-exports the DOM/React card-viz.tsx, typechecking it under the CLI base config (no DOM lib, exactOptionalPropertyTypes, OpenTUI jsxImportSource). Exclude the package from the root typecheck (like apps/studio) — it's checked by its own tsconfig + by site/studio which import it with DOM. Pin the .tsx files to React's JSX runtime via @jsxImportSource pragma as belt-and-suspenders. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 241f6f5 commit 8ae76a3

25 files changed

Lines changed: 1222 additions & 1010 deletions

apps/site/app/components/landing-v2/dashboard-preview.tsx

Lines changed: 23 additions & 278 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { Link } from "@tanstack/react-router";
44
import { AGENT_ONBOARDING_WITH_INSTALL } from "@ax/onboarding-prompt";
55
import { HeroLogoField, PROVIDERS } from "./supports-strip";
66
import { RetroTerminal } from "./retro-terminal";
7+
import { CardViz, type VizSpec } from "@ax/recap-deck";
78

89
// ============================================================================
910
// Mission Control preview - a static recreation of the studio home (instrument
@@ -148,268 +149,6 @@ function McClock() {
148149
}
149150

150151

151-
// ---- Wrapped recap charts (static recreations of the studio card-viz registry:
152-
// apps/studio/src/instrument/card-viz.tsx). Each keys --card-accent. ----
153-
function McBars({ data }: { data: number[] }) {
154-
const max = Math.max(...data, 1);
155-
const last = data.length - 1;
156-
return (
157-
<div className="mc-bars" aria-hidden="true">
158-
{data.map((b, i) => (
159-
<i
160-
key={i}
161-
className={i === last ? "is-now" : undefined}
162-
style={{ height: `${Math.max(6, (b / max) * 100)}%` }}
163-
/>
164-
))}
165-
</div>
166-
);
167-
}
168-
169-
function McLine({ data }: { data: number[] }) {
170-
const W = 100;
171-
const H = 46;
172-
const padX = 2;
173-
const padT = 5;
174-
const padB = 5;
175-
const uh = H - padT - padB;
176-
const n = data.length;
177-
const max = Math.max(...data, 1);
178-
const xy = data.map((v, i) => [padX + (i / (n - 1)) * (W - 2 * padX), padT + (1 - v / max) * uh] as const);
179-
const stroke = `M ${xy.map((p) => `${p[0].toFixed(1)} ${p[1].toFixed(1)}`).join(" L ")}`;
180-
const area = `${stroke} L ${xy[n - 1][0].toFixed(1)} ${H - padB} L ${xy[0][0].toFixed(1)} ${H - padB} Z`;
181-
return (
182-
<svg className="mc-line" viewBox={`0 0 ${W} ${H}`} preserveAspectRatio="none" aria-hidden="true">
183-
<defs>
184-
<linearGradient id="mcLineFill" x1="0" y1="0" x2="0" y2="1">
185-
<stop offset="0%" stopColor="var(--card-accent)" stopOpacity="0.32" />
186-
<stop offset="100%" stopColor="var(--card-accent)" stopOpacity="0" />
187-
</linearGradient>
188-
</defs>
189-
<path d={area} fill="url(#mcLineFill)" />
190-
<path d={stroke} className="mc-line-stroke" fill="none" />
191-
</svg>
192-
);
193-
}
194-
195-
function McRadar({ data }: { data: number[] }) {
196-
const axes = data.length;
197-
const cx = 23;
198-
const cy = 23;
199-
const R = 21;
200-
const max = Math.max(...data, 1);
201-
const ang = (i: number) => -Math.PI / 2 + (i / axes) * Math.PI * 2;
202-
const at = (i: number, r: number) => [cx + Math.cos(ang(i)) * r, cy + Math.sin(ang(i)) * r] as const;
203-
const rad = (v: number) => ((v / max) * 0.84 + 0.08) * R;
204-
const fmt = (p: readonly [number, number]) => `${p[0].toFixed(1)},${p[1].toFixed(1)}`;
205-
const poly = data.map((v, i) => fmt(at(i, rad(v)))).join(" ");
206-
return (
207-
<svg className="mc-radar" viewBox="0 0 46 46" aria-hidden="true">
208-
{[0.4, 0.7, 1].map((rr, k) => (
209-
<polygon key={k} className="web" points={Array.from({ length: axes }, (_, i) => fmt(at(i, rr * R))).join(" ")} />
210-
))}
211-
<polygon className="mc-radar-fill" points={poly} />
212-
{data.map((v, i) => {
213-
const [x, y] = at(i, rad(v));
214-
return <circle key={i} className="mc-radar-node" cx={x.toFixed(1)} cy={y.toFixed(1)} r={1.5} />;
215-
})}
216-
</svg>
217-
);
218-
}
219-
220-
function McRing({ pct }: { pct: number }) {
221-
const R = 15.5;
222-
const C = 2 * Math.PI * R;
223-
const dash = C * 0.75;
224-
const frac = Math.max(0.04, pct / 100);
225-
return (
226-
<div className="mc-ring" aria-hidden="true">
227-
<svg viewBox="0 0 40 40">
228-
<circle cx="20" cy="20" r={R} className="bg" strokeDasharray={`${dash} ${C}`} />
229-
<circle cx="20" cy="20" r={R} className="fg" strokeDasharray={`${dash} ${C}`} strokeDashoffset={dash * (1 - frac)} />
230-
</svg>
231-
<span className="mc-ring-val">{pct}</span>
232-
</div>
233-
);
234-
}
235-
236-
function McWaffle({ pct }: { pct: number }) {
237-
const ROWS = 5;
238-
const COLS = 14;
239-
const total = ROWS * COLS;
240-
const on = Math.round((pct / 100) * total);
241-
return (
242-
<div className="mc-waffle" aria-hidden="true">
243-
{Array.from({ length: total }, (_, i) => {
244-
const col = i % COLS;
245-
const row = Math.floor(i / COLS);
246-
const order = col * ROWS + (ROWS - 1 - row);
247-
return <span key={i} className={`mc-waffle-cell${order < on ? " on" : ""}`} />;
248-
})}
249-
</div>
250-
);
251-
}
252-
253-
function McCandles({ data }: { data: number[] }) {
254-
const max = Math.max(...data, 1);
255-
const min = Math.min(...data, 0);
256-
const span = Math.max(1, max - min);
257-
const Y = (v: number) => 4 + (1 - (v - min) / span) * 36;
258-
return (
259-
<div className="mc-candles" aria-hidden="true">
260-
{data.map((v, i) => {
261-
const prev = i === 0 ? data[0] : data[i - 1];
262-
const up = v >= prev;
263-
const oY = Y(prev);
264-
const cY = Y(v);
265-
const top = Math.min(oY, cY);
266-
const bodyH = Math.max(2, Math.abs(cY - oY));
267-
const wickTop = Math.min(top, Y(Math.max(v, prev)) - 3);
268-
const wickBot = Math.max(top + bodyH, Y(Math.min(v, prev)) + 3);
269-
return (
270-
<span key={i} className={`mc-candle ${up ? "up" : "dn"}`}>
271-
<span className="mc-candle-wick" style={{ top: `${wickTop}px`, height: `${Math.max(3, wickBot - wickTop)}px` }} />
272-
<span className="mc-candle-body" style={{ top: `${top}px`, height: `${bodyH}px` }} />
273-
</span>
274-
);
275-
})}
276-
</div>
277-
);
278-
}
279-
280-
function McComet({ pct }: { pct: number }) {
281-
const cx = 23;
282-
const cy = 23;
283-
const rx = 19;
284-
const ry = 15;
285-
const frac = Math.max(0.02, pct / 100);
286-
const C = Math.PI * (3 * (rx + ry) - Math.sqrt((3 * rx + ry) * (rx + 3 * ry)));
287-
const dash = C * frac;
288-
const ang = -Math.PI / 2 + frac * Math.PI * 2;
289-
const hx = cx + Math.cos(ang) * rx;
290-
const hy = cy + Math.sin(ang) * ry;
291-
return (
292-
<div className="mc-comet" aria-hidden="true">
293-
<svg viewBox="0 0 46 46">
294-
<ellipse className="mc-comet-orbit" cx={cx} cy={cy} rx={rx} ry={ry} />
295-
<ellipse
296-
className="mc-comet-trail"
297-
cx={cx}
298-
cy={cy}
299-
rx={rx}
300-
ry={ry}
301-
strokeDasharray={`${dash.toFixed(1)} ${C.toFixed(1)}`}
302-
transform={`rotate(-90 ${cx} ${cy})`}
303-
/>
304-
<circle className="mc-comet-head" cx={hx.toFixed(1)} cy={hy.toFixed(1)} r={2.6} />
305-
</svg>
306-
</div>
307-
);
308-
}
309-
310-
function McWave({ data }: { data: number[] }) {
311-
const W = 100;
312-
const H = 46;
313-
const mid = H / 2;
314-
const n = data.length;
315-
const max = Math.max(...data, 1);
316-
const min = Math.min(...data, 0);
317-
const span = Math.max(1, max - min);
318-
const d = data
319-
.map((v, i) => {
320-
const x = (i / (n - 1)) * (W - 2) + 1;
321-
const y = mid + (0.5 - (v - min) / span) * (H - 10);
322-
return `${i === 0 ? "M" : "L"} ${x.toFixed(1)} ${y.toFixed(1)}`;
323-
})
324-
.join(" ");
325-
return (
326-
<svg className="mc-wave" viewBox={`0 0 ${W} ${H}`} preserveAspectRatio="none" aria-hidden="true">
327-
<line className="mc-wave-grat" x1="0" y1={mid} x2={W} y2={mid} />
328-
<line className="mc-wave-grat dash" x1={W * 0.5} y1="2" x2={W * 0.5} y2={H - 2} />
329-
<path className="mc-wave-trace" d={d} fill="none" vectorEffect="non-scaling-stroke" />
330-
</svg>
331-
);
332-
}
333-
334-
function McBullet({ actual, target }: { actual: number; target: number }) {
335-
const onTrack = actual >= target;
336-
return (
337-
<div className="mc-bullet" aria-hidden="true">
338-
<div className="mc-bullet-track">
339-
<span className="mc-bullet-fill" style={{ width: `${actual}%` }} />
340-
<span className={`mc-bullet-tick${onTrack ? " ok" : ""}`} style={{ left: `${target}%` }} />
341-
</div>
342-
<div className="mc-bullet-foot">
343-
<span className="mc-bullet-val">{actual}</span>
344-
<span className="mc-bullet-goal">/ {target} goal</span>
345-
</div>
346-
</div>
347-
);
348-
}
349-
350-
function McSignal({ on, bars = 6 }: { on: number; bars?: number }) {
351-
return (
352-
<div className="mc-signal" aria-hidden="true">
353-
{Array.from({ length: bars }, (_, i) => (
354-
<span
355-
key={i}
356-
className={`mc-signal-bar${i < on ? " on" : ""}`}
357-
style={{ height: `${28 + (i / (bars - 1)) * 70}%` }}
358-
/>
359-
))}
360-
</div>
361-
);
362-
}
363-
364-
function McScatter({ data }: { data: number[] }) {
365-
const n = data.length;
366-
const max = Math.max(...data, 1);
367-
const min = Math.min(...data, 0);
368-
const span = Math.max(1, max - min);
369-
const mean = data.reduce((a, b) => a + b, 0) / n;
370-
const meanY = 4 + (1 - (mean - min) / span) * 38;
371-
return (
372-
<div className="mc-scatter" aria-hidden="true">
373-
<svg className="mc-scatter-svg" viewBox="0 0 100 46" preserveAspectRatio="none">
374-
<line className="mc-scatter-mean" x1="0" y1={meanY.toFixed(1)} x2="100" y2={meanY.toFixed(1)} vectorEffect="non-scaling-stroke" />
375-
</svg>
376-
<div className="mc-scatter-dots">
377-
{data.map((v, i) => {
378-
const x = (i / (n - 1)) * 100;
379-
const y = 4 + (1 - (v - min) / span) * 38;
380-
return <span key={i} className={`mc-scatter-dot${v >= mean ? " hot" : ""}`} style={{ left: `${x}%`, top: `${y}px` }} />;
381-
})}
382-
</div>
383-
</div>
384-
);
385-
}
386-
387-
function McDelta({ data }: { data: number[] }) {
388-
const last = data[data.length - 1];
389-
const first = data[0];
390-
const diff = last - first;
391-
const up = diff >= 0;
392-
const n = data.length;
393-
const max = Math.max(...data, 1);
394-
const min = Math.min(...data, 0);
395-
const span = Math.max(1, max - min);
396-
const spark = data.map((v, i) => `${(i / (n - 1)) * 100},${(14 - ((v - min) / span) * 12).toFixed(1)}`).join(" ");
397-
return (
398-
<div className={`mc-delta ${up ? "up" : "dn"}`} aria-hidden="true">
399-
<div className="mc-delta-num">
400-
<span className="mc-delta-val">{Math.round(last)}</span>
401-
<span className="mc-delta-trend">
402-
<i>{up ? "▲" : "▼"}</i>
403-
{Math.abs(Math.round(diff))}
404-
</span>
405-
</div>
406-
<svg className="mc-delta-spark" viewBox="0 0 100 14" preserveAspectRatio="none">
407-
<polyline points={spark} fill="none" stroke="var(--card-accent)" strokeWidth="1.25" vectorEffect="non-scaling-stroke" />
408-
</svg>
409-
</div>
410-
);
411-
}
412-
413152
// The 12 wrapped readouts the floating popups cycle through - each keys a channel
414153
// accent and carries one chart + a one-line caption. (Recreated from the studio
415154
// card-viz registry: apps/studio/src/instrument/card-viz.tsx.)
@@ -418,20 +157,22 @@ const POP_CHARTS: ReadonlyArray<{
418157
accent: string;
419158
q: string;
420159
h: string;
421-
viz: React.ReactNode;
160+
viz: VizSpec;
422161
}> = [
423-
{ key: "bars", accent: "acc-green", q: "when you ship", h: "Tuesday nights", viz: <McBars data={[22, 31, 28, 44, 39, 58, 47, 63, 71, 55, 82, 90, 74, 61, 88, 96]} /> },
424-
{ key: "ring", accent: "acc-gold", q: "fixes stick", h: "82% hold", viz: <McRing pct={82} /> },
425-
{ key: "line", accent: "acc-blue", q: "tokens / year", h: "1.4B", viz: <McLine data={[18, 24, 21, 33, 40, 38, 52, 60, 57, 71, 80, 78, 92, 100]} /> },
426-
{ key: "radar", accent: "acc-violet", q: "skill profile", h: "systems mind", viz: <McRadar data={[82, 64, 91, 48, 73]} /> },
427-
{ key: "waffle", accent: "acc-green", q: "paths covered", h: "73%", viz: <McWaffle pct={73} /> },
428-
{ key: "candles", accent: "acc-rose", q: "session swings", h: "big nights", viz: <McCandles data={[40, 60, 55, 75, 70, 50, 62, 80, 72, 88]} /> },
429-
{ key: "comet", accent: "acc-blue", q: "quota burn", h: "61%", viz: <McComet pct={61} /> },
430-
{ key: "wave", accent: "acc-violet", q: "latency", h: "steady", viz: <McWave data={[50, 80, 20, 60, 35, 70, 45, 66, 30, 72]} /> },
431-
{ key: "scatter", accent: "acc-rose", q: "cost vs reward", h: "lands cheap", viz: <McScatter data={[30, 50, 45, 70, 60, 85, 55, 40, 66, 52]} /> },
432-
{ key: "bullet", accent: "acc-gold", q: "ship goal", h: "82 / 70", viz: <McBullet actual={82} target={70} /> },
433-
{ key: "signal", accent: "acc-green", q: "harness health", h: "5 / 6", viz: <McSignal on={5} /> },
434-
{ key: "delta", accent: "acc-blue", q: "weekly tokens", h: "+18", viz: <McDelta data={[40, 55, 48, 62, 70, 66, 78, 84]} /> },
162+
{ key: "bars", accent: "acc-green", q: "when you ship", h: "Tuesday nights", viz: { kind: "bars", data: [22, 31, 28, 44, 39, 58, 47, 63, 71, 55, 82, 90, 74, 61, 88, 96] } },
163+
{ key: "ring", accent: "acc-gold", q: "fixes stick", h: "82% hold", viz: { kind: "ring", data: [82] } },
164+
{ key: "line", accent: "acc-blue", q: "tokens / year", h: "1.4B", viz: { kind: "line", data: [18, 24, 21, 33, 40, 38, 52, 60, 57, 71, 80, 78, 92, 100] } },
165+
{ key: "radar", accent: "acc-violet", q: "skill profile", h: "systems mind", viz: { kind: "radar", data: [82, 64, 91, 48, 73] } },
166+
{ key: "waffle", accent: "acc-green", q: "paths covered", h: "73%", viz: { kind: "waffle", data: [73] } },
167+
{ key: "candles", accent: "acc-rose", q: "session swings", h: "big nights", viz: { kind: "candles", data: [40, 60, 55, 75, 70, 50, 62, 80, 72, 88] } },
168+
{ key: "comet", accent: "acc-blue", q: "quota burn", h: "61%", viz: { kind: "comet", data: [61] } },
169+
{ key: "wave", accent: "acc-violet", q: "latency", h: "steady", viz: { kind: "wave", data: [50, 80, 20, 60, 35, 70, 45, 66, 30, 72] } },
170+
{ key: "scatter", accent: "acc-rose", q: "cost vs reward", h: "lands cheap", viz: { kind: "scatter", data: [30, 50, 45, 70, 60, 85, 55, 40, 66, 52] } },
171+
// package bullet reads [target, actual]: target=first, actual=last -> 70 goal, 82 actual
172+
{ key: "bullet", accent: "acc-gold", q: "ship goal", h: "82 / 70", viz: { kind: "bullet", data: [70, 82] } },
173+
// package signal reads avg(data) as a 0..100 pct -> on = round(83/100*6) = 5 of 6
174+
{ key: "signal", accent: "acc-green", q: "harness health", h: "5 / 6", viz: { kind: "signal", data: [83] } },
175+
{ key: "delta", accent: "acc-blue", q: "weekly tokens", h: "+18", viz: { kind: "delta", data: [40, 55, 48, 62, 70, 66, 78, 84] } },
435176
];
436177

437178
// chart kinds that render small/fixed-size (center them in the popup viz row);
@@ -720,13 +461,17 @@ export function DashboardPreview() {
720461
{/* floating wrapped highlights - cycle through all 12 charts */}
721462
<div className="mc-pop-strip">
722463
{POP_SLOTS.map(({ off, place }) => {
723-
const c = POP_CHARTS[(popTick + off) % POP_CHARTS.length];
464+
const c = POP_CHARTS[(popTick + off) % POP_CHARTS.length]!;
724465
return (
725-
<article key={place} className={`mc-pop ${place} ${c.accent} browser--instrument`}>
466+
<article
467+
key={place}
468+
className={`mc-pop ${place} ${c.accent} browser--instrument rdx`}
469+
data-theme="dark"
470+
>
726471
<span className="mc-pop-badge">wrapped</span>
727472
<div key={c.key} className="mc-pop-swap">
728473
<div className={`mc-pop-viz${POP_CENTERED.has(c.key) ? " mc-pop-viz--center" : ""}`}>
729-
{c.viz}
474+
<CardViz spec={c.viz} />
730475
</div>
731476
<span className="mc-pop-q">$ {c.q}</span>
732477
<h4 className="mc-pop-h">{c.h}</h4>

0 commit comments

Comments
 (0)