diff --git a/apps/fabro-web/app/components/stage-sidebar.tsx b/apps/fabro-web/app/components/stage-sidebar.tsx index a71ef183e..f9718077e 100644 --- a/apps/fabro-web/app/components/stage-sidebar.tsx +++ b/apps/fabro-web/app/components/stage-sidebar.tsx @@ -1,4 +1,4 @@ -import { type ComponentType } from "react"; +import { type ComponentType, type ReactNode, useCallback, useState } from "react"; import { Link } from "react-router"; import type { StageHandler, StageState } from "@qltysh/fabro-api-client"; import { @@ -12,6 +12,8 @@ import { import { Bars3BottomLeftIcon, BoltIcon, + ChevronDoubleLeftIcon, + ChevronDoubleRightIcon, DocumentTextIcon, MapIcon, PaperClipIcon, @@ -42,6 +44,90 @@ export const statusConfig: Record; + iconClass: string; + label: string; + /** Right-aligned text (e.g. stage duration); hidden when collapsed. */ + trailing?: string; + active: boolean; + collapsed: boolean; + /** Spin the icon to signal an in-flight stage. */ + spin?: boolean; +} + +/** A single sidebar link. The icon stays visible when collapsed; the label + * becomes screen-reader-only and a `title` tooltip stands in for sighted users. */ +function SidebarRow({ to, icon: Icon, iconClass, label, trailing, active, collapsed, spin }: SidebarRowProps) { + return ( +
  • + + + {label} + {trailing != null && !collapsed && ( + {trailing} + )} + +
  • + ); +} + +/** Section heading. When a `toggle` is given it sits inline with the heading + * (or stands alone, centered, when collapsed) rather than on its own row. */ +function SectionHeading({ + title, + collapsed, + toggle, +}: { + title: string; + collapsed: boolean; + toggle?: ReactNode; +}) { + if (collapsed) { + return ( + <> +

    {title}

    + {toggle &&
    {toggle}
    } + + ); + } + return ( +
    +

    {title}

    + {toggle} +
    + ); +} + interface StageSidebarProps { stages: Stage[]; runId: string; @@ -49,7 +135,30 @@ interface StageSidebarProps { activeLink?: "settings" | "source" | "logs" | "artifacts" | "events"; } +const WORKFLOW_LINKS: ReadonlyArray<{ + key: NonNullable; + path: string; + icon: ComponentType<{ className?: string }>; + label: string; +}> = [ + { key: "source", path: "source", icon: MapIcon, label: "Graph Source" }, + { key: "logs", path: "logs", icon: Bars3BottomLeftIcon, label: "Run Logs" }, + { key: "events", path: "events", icon: BoltIcon, label: "Run Events" }, + { key: "artifacts", path: "artifacts", icon: PaperClipIcon, label: "Artifacts" }, + { key: "settings", path: "settings", icon: DocumentTextIcon, label: "Run Settings" }, +]; + export function StageSidebar({ stages, runId, selectedStageId, activeLink }: StageSidebarProps) { + // Persisted so the choice carries across the Overview and Stages tabs. + const [collapsed, setCollapsed] = useState(loadStoredCollapsed); + const toggleCollapsed = useCallback(() => { + setCollapsed((prev) => { + const next = !prev; + persistCollapsed(next); + return next; + }); + }, []); + // Tick every second while any stage is running so the elapsed clock keeps up. const hasActive = stages.some((s) => ACTIVE_STAGE_STATES.has(s.status)); const now = useTickingNow(hasActive); @@ -62,108 +171,75 @@ export function StageSidebar({ stages, runId, selectedStageId, activeLink }: Sta return stage.duration; } - const linkBase = "flex items-center gap-2 rounded-md px-2 py-1.5 text-sm transition-colors"; + const toggleButton = ( + + ); return ( - ); diff --git a/apps/fabro-web/app/routes/run-detail.test.ts b/apps/fabro-web/app/routes/run-detail.test.ts index 84a012e8a..a68b4a678 100644 --- a/apps/fabro-web/app/routes/run-detail.test.ts +++ b/apps/fabro-web/app/routes/run-detail.test.ts @@ -382,7 +382,7 @@ describe("RunDetail full-height child routes", () => { const outletWrappers = renderer.root.findAll( (node) => node.type === "div" && - hasClasses(node.props.className, ["mt-6", "min-h-0", "flex-1"]), + hasClasses(node.props.className, ["pt-3", "min-h-0", "flex-1"]), ); expect(outletWrappers).toHaveLength(1); }); @@ -541,7 +541,7 @@ describe("RunDetail full-height child routes", () => { (node) => node.type === "div" && hasClasses(node.props.className, [ - "mt-6", + "pt-3", "pb-[var(--fabro-interview-dock-clearance)]", ]), ); diff --git a/apps/fabro-web/app/routes/run-detail.tsx b/apps/fabro-web/app/routes/run-detail.tsx index 77eab29bc..ddc487430 100644 --- a/apps/fabro-web/app/routes/run-detail.tsx +++ b/apps/fabro-web/app/routes/run-detail.tsx @@ -700,8 +700,8 @@ export default function RunDetail({ params }: { params: { id: string } }) {
    @@ -744,10 +744,9 @@ const ASK_FABRO_UNAVAILABLE_TOOLTIPS: Record< AskFabroUnavailableReasonEnum, string > = { - [AskFabroUnavailableReasonEnum.FEATURE_DISABLED]: "Ask Fabro is disabled", - [AskFabroUnavailableReasonEnum.NO_SANDBOX]: "Run sandbox isn't ready", - [AskFabroUnavailableReasonEnum.SANDBOX_NOT_READY]:"Run sandbox isn't ready", - [AskFabroUnavailableReasonEnum.LLM_UNCONFIGURED]: "No LLM configured", + [AskFabroUnavailableReasonEnum.NO_SANDBOX]: "Run sandbox isn't ready", + [AskFabroUnavailableReasonEnum.SANDBOX_NOT_READY]: "Run sandbox isn't ready", + [AskFabroUnavailableReasonEnum.LLM_UNCONFIGURED]: "No LLM configured", }; function AskFabroTriggerButton({ diff --git a/apps/fabro-web/app/routes/run-events.tsx b/apps/fabro-web/app/routes/run-events.tsx index 2d12128a4..2f855a39c 100644 --- a/apps/fabro-web/app/routes/run-events.tsx +++ b/apps/fabro-web/app/routes/run-events.tsx @@ -28,8 +28,8 @@ export default function RunEvents() { ); return ( -
    -
    +
    +
    @@ -110,7 +110,7 @@ function EventsView({ if (error) { return ( -
    +
    +
    ); diff --git a/apps/fabro-web/app/routes/run-sandbox.tsx b/apps/fabro-web/app/routes/run-sandbox.tsx index b25c53704..729ac8f28 100644 --- a/apps/fabro-web/app/routes/run-sandbox.tsx +++ b/apps/fabro-web/app/routes/run-sandbox.tsx @@ -287,14 +287,13 @@ export default function RunSandbox({ params }: { params: { id: string } }) { }; // The outer flex spans from the tab bar's bottom border down to the - // steer bar — `-mt-6` cancels the outlet wrapper's top gap, and we - // intentionally omit `pb-[clearance]` here so the column divider can - // run the full height. Each column adds its own `pt-6` and dock - // clearance to its content instead. + // steer bar — `-mt-3` cancels the outlet wrapper's top padding so the + // column divider runs the full height, and we omit `pb-[clearance]` + // here. Each column adds its own `pt-3` and dock clearance instead. return ( -
    +