Files
roboco/panel/src/components/tg/charts.tsx
T
700dbcd285 feat(tg): Mini App V5 — brand typography, icon depth, motion, detail sheets (#583)
* feat(tg): Mini App V5 — brand typography, icon depth, motion, detail sheets

Share Tech Mono (the vendored motion-brand face) becomes the cockpit's
display voice via next/font/local scoped to #tg-shell; icon tiles, circle
actions, and avatars get gradient/ring depth; a dependency-free motion
vocabulary lands (spend count-up, tab rise-in, staggered sections,
sparkline draw-in, sheet slide-up with native BackButton dismiss); the
Board tab gains a tap-through task sheet (ACs, open findings, PR link),
Today's fleet opens a full-roster sheet, and Board/Inbox join the
/tg?demo=1 fixtures.

* feat(tg): custom RoboCo icon set + operations ring

The cockpit stops using stock lucide on its hero surfaces: a hand-drawn
duotone icon set (speedometer, seal, bell, kanban, brand-cursor bubble,
rocket, double-check, broom, robot head) covers the tab bar and the Today
ring. The ring itself stops duplicating the tab bar and becomes real
operations: Ship deep-focuses the release proposal in Approvals, Ack all
bulk-acknowledges pending notifications, Sweep runs the stale-branch
cleanup across every git-configured project behind a confirm sheet, and
Fleet opens the roster.

---------

Co-authored-by: Renn F <rennf93@users.noreply.github.com>
2026-07-19 18:59:54 +02:00

97 lines
3.2 KiB
TypeScript

"use client";
/**
* Hand-rolled inline-SVG charts for the cockpit — no charting library in the
* Mini App bundle. Both are theme-driven (stroke/fill ride `currentColor`,
* so the caller sets the hue via a text color) and degrade to a flat
* baseline for an all-zero series rather than dividing by zero.
*/
const SPARK_W = 300;
const SPARK_H = 72;
/** Smooth-ish area sparkline with a gradient fill and an emphasized last
* point — the hero's spend trend. `values` oldest → newest. */
export function Sparkline({ values }: { values: number[] }) {
const n = values.length;
const max = Math.max(...values, 0);
const min = Math.min(...values, 0);
const span = max - min || 1;
const gradId = "tg-spark-grad";
const x = (i: number) => (n <= 1 ? 0 : (i / (n - 1)) * SPARK_W);
// Leave 6px headroom top/bottom so the stroke + endpoint dot never clip.
const y = (v: number) => SPARK_H - 6 - ((v - min) / span) * (SPARK_H - 12);
const points = values.map((v, i) => [x(i), y(v)] as const);
const line = points.map(([px, py]) => `${px},${py}`).join(" ");
const area = `M0,${SPARK_H} L${line.replace(/ /g, " L")} L${SPARK_W},${SPARK_H} Z`;
const [lastX, lastY] = points[points.length - 1] ?? [SPARK_W, SPARK_H / 2];
return (
<svg
viewBox={`0 0 ${SPARK_W} ${SPARK_H}`}
preserveAspectRatio="none"
className="h-16 w-full overflow-visible text-primary"
aria-hidden="true"
>
<defs>
<linearGradient id={gradId} x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stopColor="currentColor" stopOpacity="0.28" />
<stop offset="100%" stopColor="currentColor" stopOpacity="0" />
</linearGradient>
</defs>
<path d={area} fill={`url(#${gradId})`} className="tg-backdrop" />
<polyline
points={line}
pathLength={1}
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
vectorEffect="non-scaling-stroke"
className="tg-draw-line"
/>
<circle cx={lastX} cy={lastY} r="3.5" fill="currentColor" />
</svg>
);
}
/** Compact day bars — the last bar (today) emphasized in the accent, the
* rest muted. `values` oldest → newest. */
export function DayBars({
values,
labels,
}: {
values: number[];
labels?: string[];
}) {
const max = Math.max(...values, 1);
return (
<div className="flex items-end gap-1.5" aria-hidden="true">
{values.map((v, i) => {
const isToday = i === values.length - 1;
const pct = Math.round((v / max) * 100);
return (
<div key={i} className="flex flex-1 flex-col items-center gap-1">
<div className="flex h-14 w-full items-end">
<div
className={`w-full rounded-sm ${
isToday ? "bg-primary" : "bg-muted-foreground/25"
}`}
style={{ height: `${Math.max(pct, v > 0 ? 8 : 3)}%` }}
/>
</div>
{labels && (
<span className="text-[9px] tabular-nums text-muted-foreground/60">
{labels[i]}
</span>
)}
</div>
);
})}
</div>
);
}