feat(central-ui): FlowGraphView External lane, outbound status pills, async/spawn connectors

Four lanes (LaneCount 4; External sublabel = distinct targets, three then +n);
ApiCall/DbCall/Notify nodes carry a status pill (Ok/Pending/Parked/Failed —
pending pulses, reduced-motion aware) plus a sync/cached/notify mode; folded
lifecycles show '+4.1 s' submit→terminal wall time. Async edges are dashed
and routed through the inter-lane strip with a gutter tag; spawn edges use a
bolt marker. Predicted async edges (Task 6's PredictedEdge.Async) render the
same way. de-DE height pinned at 319.2 = (4×110+16)×0.7.

Claude-Session: https://claude.ai/code/session_01NcMG6xXdsJbmD2wYAgkK29
This commit is contained in:
Joseph Doherty
2026-09-06 01:57:47 -04:00
parent ec9011429c
commit 56154fd1d2
5 changed files with 399 additions and 28 deletions
@@ -46,6 +46,7 @@
.fg-node.failed .fg-node-box,
.fg-node.timed-out .fg-node-box { stroke: var(--bs-danger, #c0392b); }
.fg-node.not-traced .fg-node-box { stroke-dasharray: 1 3; }
.fg-node.parked .fg-node-box { stroke: var(--warn, var(--bs-warning, #e8920c)); }
/* Node body: HTML inside a foreignObject so text is measured by the browser. */
.fg-body {
@@ -77,6 +78,40 @@
.fg-node.failed .fg-eyebrow,
.fg-node.timed-out .fg-eyebrow { color: var(--bs-danger, #c0392b); }
/* Outbound nodes (API call / database / notify): the folded outcome as a status pill, then the
call mode. Kit status tokens first (dark mode follows via site.css), Bootstrap's as fallback. */
.fg-status { margin-top: auto; display: flex; align-items: center; gap: 4px; min-width: 0; }
.fg-pill {
display: inline-flex;
align-items: center;
gap: 4px;
flex-shrink: 0;
font-size: 0.6rem;
font-weight: 500;
line-height: 1.3;
padding: 0 6px;
border-radius: 999px;
white-space: nowrap;
color: var(--ink-soft);
background: var(--rule);
}
.fg-pill::before { content: ""; width: 5px; height: 5px; border-radius: 50%; background: currentColor; }
.fg-pill.ok { color: var(--ok, var(--bs-success, #2f9e44)); background: var(--ok-bg, rgba(var(--bs-success-rgb, 25, 135, 84), 0.12)); }
.fg-pill.pending { color: var(--warn, var(--bs-warning, #e8920c)); background: var(--warn-bg, rgba(var(--bs-warning-rgb, 255, 193, 7), 0.15)); }
.fg-pill.parked { color: var(--warn, var(--bs-warning, #e8920c)); background: var(--warn-bg, rgba(var(--bs-warning-rgb, 255, 193, 7), 0.15)); }
.fg-pill.failed { color: var(--bad, var(--bs-danger, #e03131)); background: var(--bad-bg, rgba(var(--bs-danger-rgb, 220, 53, 69), 0.12)); }
.fg-mode { color: var(--ink-soft); font-size: 0.66rem; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
/* A pending pill pulses while the page keeps refreshing outcomes after a Live run. */
.fg-node.pending .fg-pill { animation: fg-pulse 1.6s ease-in-out infinite; }
@keyframes fg-pulse {
0%, 100% { opacity: 1; box-shadow: 0 0 0 0 var(--warn-bg, rgba(var(--bs-warning-rgb, 255, 193, 7), 0.15)); }
50% { opacity: 0.6; box-shadow: 0 0 0 4px var(--warn-bg, rgba(var(--bs-warning-rgb, 255, 193, 7), 0.15)); }
}
@media (prefers-reduced-motion: reduce) {
.fg-node.pending .fg-pill { animation: none; }
}
/* Connectors */
.fg-edge {
fill: none;
@@ -85,6 +120,12 @@
}
.fg-edge.boundary { stroke: var(--accent); }
.fg-edge.external { stroke-dasharray: 5 4; }
/* Async branch (cached call / notification): same dash as the echo edge, its own class — the run
carried on without waiting. Spawn: solid line ending in the bolt marker (a trigger cascade). */
.fg-edge.async { stroke-dasharray: 5 4; }
.fg-edge.spawn { stroke: var(--ink-soft); }
.fg-edge.predicted { stroke-opacity: 0.5; stroke-dasharray: 3 3; }
.fg-arrowhead { fill: var(--ink-faint); }
.fg-arrowhead.accent { fill: var(--accent); }
.fg-arrowhead.spawn { fill: var(--ink-soft); }
.fg-wrap ::deep .fg-edge-tag { fill: var(--ink-faint); font-size: 0.6rem; font-family: var(--mono); }