fabro/apps/fabro-web/app/components/graph-toolbar.tsx
Bryan Helmkamp 174c62e2f7
style(runs): tighten stages, graph toolbar, billing, and stage sidebar
run-stages:
- replace the full-width tinted System/Assistant cards with a subtler
  left-accent bar and header so dense streams read cleanly
- unify the Running/Timed out/exit/duration indicators under a shared
  StatusPill, and bump stdout/stderr labels from 10px to 11px
- raise the selected stage header from text-sm font-medium to
  text-base font-semibold
- command output preformatted text is now text-sm on mobile

run-overview and run-graph:
- extract the floating direction/fit/zoom controls into a single
  GraphToolbar capsule with internal dividers, shared between both
  graph views
- drop the translucent canvas in favor of solid bg-panel-alt
- wrap the "no workflow graph" message in a proper empty-state panel

run-billing:
- tfoot now uses bg-overlay so totals read heavier than the body
- table headers gain font-medium and a readable fg-3 instead of
  fg-muted
- "By model" heading is a real section heading, not an eyebrow
- add an empty state when a run has no billing yet

stage-sidebar:
- cancelled stages now use NoSymbolIcon so they don't look identical
  to failed stages at a glance

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 16:51:55 -04:00

84 lines
3 KiB
TypeScript

import { ArrowDownIcon, ArrowRightIcon, MinusIcon, PlusIcon } from "@heroicons/react/20/solid";
type Direction = "LR" | "TB";
export const GRAPH_ZOOM_STEPS = [25, 50, 75, 100, 150, 200];
export const GRAPH_DEFAULT_ZOOM_INDEX = 2;
export function GraphToolbar({
direction,
setDirection,
fitToWindow,
zoomIndex,
setZoomIndex,
}: {
direction: Direction;
setDirection: (d: Direction) => void;
fitToWindow: () => void;
zoomIndex: number;
setZoomIndex: (updater: (i: number) => number) => void;
}) {
const group =
"flex items-center gap-0.5 px-0.5 [&:not(:first-child)]:border-l [&:not(:first-child)]:border-line-strong [&:not(:first-child)]:pl-1 [&:not(:first-child)]:ml-1";
const btn =
"flex size-7 items-center justify-center rounded text-fg-muted transition-colors hover:bg-overlay hover:text-fg-3 focus-visible:outline-2 focus-visible:outline-offset-1 focus-visible:outline-teal-500";
const btnActive = "bg-overlay-strong text-fg-3";
const btnDisabled =
"disabled:opacity-30 disabled:hover:bg-transparent disabled:hover:text-fg-muted";
return (
<div
role="toolbar"
aria-label="Graph controls"
className="absolute right-3 top-3 z-10 flex items-center rounded-md border border-line bg-panel p-0.5"
>
<div className={group}>
<button
type="button"
title="Left to right"
onClick={() => setDirection("LR")}
aria-pressed={direction === "LR"}
className={`${btn} ${direction === "LR" ? btnActive : ""}`}
>
<ArrowRightIcon className="size-3.5" aria-hidden="true" />
</button>
<button
type="button"
title="Top to bottom"
onClick={() => setDirection("TB")}
aria-pressed={direction === "TB"}
className={`${btn} ${direction === "TB" ? btnActive : ""}`}
>
<ArrowDownIcon className="size-3.5" aria-hidden="true" />
</button>
</div>
<div className={group}>
<button type="button" title="Fit to window" onClick={fitToWindow} className={btn}>
<svg viewBox="0 0 14 14" fill="none" stroke="currentColor" className="size-3.5" aria-hidden="true">
<rect x="1" y="1" width="12" height="12" rx="1.5" strokeWidth="1.5" strokeDasharray="3 2" />
</svg>
</button>
</div>
<div className={group}>
<button
type="button"
title="Zoom out"
onClick={() => setZoomIndex((i) => Math.max(0, i - 1))}
disabled={zoomIndex === 0}
className={`${btn} ${btnDisabled}`}
>
<MinusIcon className="size-4" aria-hidden="true" />
</button>
<button
type="button"
title="Zoom in"
onClick={() => setZoomIndex((i) => Math.min(GRAPH_ZOOM_STEPS.length - 1, i + 1))}
disabled={zoomIndex === GRAPH_ZOOM_STEPS.length - 1}
className={`${btn} ${btnDisabled}`}
>
<PlusIcon className="size-4" aria-hidden="true" />
</button>
</div>
</div>
);
}