/** * @file The compact lane tile used in the Workspace's horizontal lane strip. * It carries only what you need to pick a lane — which lane, is it alive, * what stage — because the full card, its controls and its working-copy facts * live in the detail panel below it. Stage badges ride on the id row rather * than a line of their own, and progress (bar and percent) is deliberately * absent: the detail panel's pipeline map below shows the same thing with the * stage names attached. Keeping the tile small and fixed-width is what lets * many lanes stay scannable in one scrolling row. * * The tile is also the strip's reorder handle: native HTML5 drag-and-drop, no * dependency. Ceiling: desktop pointers only — there is no touch drag and no * auto-scroll when the strip overflows, so Alt+Arrow moves the focused tile as * the keyboard (and touch-free) path. * @author Nguyễn Ngọc Trí Vĩ */ import { useState } from "react"; import { useTranslation } from "react-i18next"; import type { Lane } from "../../lib/types"; const LIVENESS_DOT: Record = { active: "bg-status-success", idle: "bg-surface-4", dead: "bg-status-danger", }; /** * Whether the inferred stage sits ahead of the declared one in node order. * * The declaration is located by the node the server resolved it onto (`state * === "current"`), not by `indexOf(lane.stage)`: a stage declared under an * ALIAS (`tests`, `booting`, `ship` — most of what the ship-feature skill * declares) is not a node id, so indexOf answers -1 and a detection sitting * well BEHIND the declaration would still be badged as the live one. * * `>=`, not `>`: an undeclared lane is rendered AT its detected node by the * server (see `inferredDisplayStage`), so the detection and the "current" node * are the same one — and that lane is precisely the one whose stage the badge * is the only readout for. A declared lane can never tie, because * recordDetection refuses any detection that isn't strictly ahead. */ function detectionLeads(lane: Lane): boolean { if (!lane.detected_stage) return false; const nodes = lane.pipeline_nodes; return ( nodes.findIndex((n) => n.id === lane.detected_stage) >= nodes.findIndex((n) => n.state === "current") ); } export default function LaneStripCard({ lane, index, selected, onSelect, onReorder, }: { lane: Lane; /** Position in the lane array — the unit both reorder paths speak in. */ index: number; selected: boolean; onSelect: () => void; onReorder: (from: number, to: number) => void; }) { const { t } = useTranslation(["lanes"]); const [dropTarget, setDropTarget] = useState(false); return ( ); }