Five UI problems, all reported from using the page. Expanding "How this is measured" shoved every control down, because the disclosure and the run controls shared one flex row. They no longer do: run status and the controls that start a new run sit together on one line, and the explainer is below them where growing it moves nothing. A long strategy name wrapped the dropdown trigger onto three lines and dragged the row out of alignment. The trigger now truncates with the full text on hover — a wrapping dropdown is broken anywhere, so the fix is in the primitive — and the twelve-character "Production: " prefix is a bullet. "Sortino 2.72" answered nothing. Each risk-adjusted metric now carries a meter: a track showing where the value sits, ticks at the band edges, and the band word. Colour never travels alone. Bands are deliberately stricter than textbook ranges because this universe is today's survivors replayed backward, which flatters every ratio — that caveat is stated next to them rather than left implied. The two tile rows were different sizes, which read as inconsistent rather than as hierarchy. Every tile is the same size now and grouping carries the ranking: top row is raw outcome and takes no meters, second row is risk-adjusted ratios and all take meters. Sharpe moved down to join them — it is one of those ratios, and leaving it above made it the only metered tile in a row of bare ones. The recommendation led with a long bold sentence that describes the configuration, not a verdict, while the actual findings were small grey text. Findings now come first, each split into label and detail on the colon the backend strings already carry, and the configuration is a footer. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
72 lines
2.2 KiB
TypeScript
72 lines
2.2 KiB
TypeScript
import {
|
|
BAND_STYLE,
|
|
bandTicks,
|
|
classifyMetric,
|
|
meterFraction,
|
|
} from '../../lib/metricBands';
|
|
|
|
/**
|
|
* One labelled metric.
|
|
*
|
|
* Optionally carries a quality meter: pass `metric` (a key in METRIC_BANDS) and
|
|
* the numeric `raw` value. The meter is the answer to "2.72 — is that good?" —
|
|
* a track showing where the value sits, ticks at the band edges, and the band
|
|
* word. Colour never travels alone; the word is always rendered beside it.
|
|
*
|
|
* Every tile is the same size. Hierarchy comes from grouping and section
|
|
* labels, not from shrinking one row — two sizes read as inconsistent rather
|
|
* than as a deliberate ranking.
|
|
*/
|
|
export function StatTile({
|
|
label,
|
|
value,
|
|
valueClass = 'text-gray-100',
|
|
sub,
|
|
title,
|
|
metric,
|
|
raw,
|
|
}: {
|
|
label: string;
|
|
value: string;
|
|
valueClass?: string;
|
|
sub?: string;
|
|
/** Native tooltip — how the metric is defined. */
|
|
title?: string;
|
|
/** Key into METRIC_BANDS; enables the quality meter. */
|
|
metric?: string;
|
|
/** Numeric value the meter reads (the formatted `value` is display-only). */
|
|
raw?: number | null;
|
|
}) {
|
|
const band = metric ? classifyMetric(metric, raw) : null;
|
|
const style = band ? BAND_STYLE[band] : null;
|
|
|
|
return (
|
|
<div className="glass flex flex-col p-4" title={title}>
|
|
<p className="section-index">{label}</p>
|
|
<p className={`num mt-1.5 text-2xl font-semibold ${valueClass}`}>{value}</p>
|
|
|
|
{style && metric && (
|
|
<div className="mt-2.5">
|
|
<div className="relative h-1.5 overflow-hidden rounded-full bg-white/[0.07]">
|
|
<div
|
|
className={`h-full rounded-full ${style.fill}`}
|
|
style={{ width: `${meterFraction(metric, raw) * 100}%` }}
|
|
/>
|
|
{/* Band edges — where "fair" becomes "good", and so on. */}
|
|
{bandTicks(metric).map((t) => (
|
|
<span
|
|
key={t}
|
|
className="absolute top-0 h-full w-px bg-black/50"
|
|
style={{ left: `${t * 100}%` }}
|
|
/>
|
|
))}
|
|
</div>
|
|
<p className={`mt-1.5 text-[11px] font-medium ${style.text}`}>{style.label}</p>
|
|
</div>
|
|
)}
|
|
|
|
{sub && <p className="mt-1 text-xs text-gray-500">{sub}</p>}
|
|
</div>
|
|
);
|
|
}
|