"use client"; import { useEffect, useState, useTransition } from "react"; import { useRouter } from "next/navigation"; // THE button. Six components used to carry a copy of everything below — the same // two class strings, the same hydration guard, the same run() and the same // inline error span — and copies of a hydration guard are exactly the kind of // thing that goes missing in one place. // // `ariaLabel` is explicit and required because these labels are a CONTRACT: the // e2e suite selects several of these buttons by accessible name, and in the // dense (rail) frame the visible label collapses to a glyph, at which point the // aria-label is the only name left. export type LaneActionResult = { ok: boolean; error?: string } | void; export function LaneActionButton({ label, glyph, ariaLabel, title, variant, disabled, dense, action, onChange, }: { label: string; // What the label collapses to in a dense frame. Falls back to the first // character, which is never what you want — pass one. glyph?: string; ariaLabel: string; title?: string; // `active` = this control is the one holding/stopping something right now // (Resume, Stop): warning-filled, so a held lane is visible from across the // room. `idle` = the quiet outline. variant: "active" | "idle"; disabled?: boolean; dense?: boolean; action: () => Promise; onChange?: () => void | Promise; }) { const [pending, startTransition] = useTransition(); const [error, setError] = useState(null); const router = useRouter(); // Disabled until hydrated. A click on a server-rendered button before React // attaches fires NOTHING — no request, no job, no error — which is the // recorded root cause of the digest pilot's "un-created job". const [mounted, setMounted] = useState(false); useEffect(() => setMounted(true), []); function run() { setError(null); startTransition(async () => { const result = await action(); if (result && !result.ok) setError(result.error ?? "Failed."); if (onChange) await onChange(); else router.refresh(); }); } const busy = pending || !mounted; const tone = variant === "active" ? "bg-warning text-warning-foreground font-medium hover:bg-warning/90" : "border border-border hover:bg-muted"; const size = dense ? "size-6 rounded-md text-xs leading-none" : "px-3 py-1.5 rounded-md text-sm"; return ( <> {error && ( {error} )} ); }