Files
ScadaBridge/src/ZB.MOM.WW.ScadaBridge.CentralUI/Components/Shared/FlowGraphView.razor.css
T
Joseph Doherty 859922ba17 fix(central-ui): Flow Debugger spawn edges — real arrowhead + bolt badge; spawner picked by trigger attribute
The trigger-cascade (Spawn) edge used a lightning-bolt marker IN PLACE of the arrowhead. On the upward
Tags→Site hop the bolt sat on top of the riser and read as a broken arrow (rig screenshot 2026-09-08).
Every connector now ends in an ordinary arrowhead; the bolt is a badge (circle + bolt) centred on the
connector's vertical run (FlowGraphView.SpawnBadgePoint, kept in step with OrthogonalPath/AsyncPath).

Same screenshot, second defect: the cascade spawned by "RecipeDownloadFlag = false" was drawn from the
LAST write of the burst (RecipeProcessedFlag) because all four writes share a millisecond and the
spawner rule was time-only. FlowGraphBuilder now prefers the parent's last AttributeWrite of the attribute
the child's triggerAttribute names (at or before the child, else the last such write); the time/position
rules remain the fallback when the child names no attribute or the parent never wrote it.

Verified live on the rebuilt rig (DelmiaSim01 / DelmiaReceiverSimulator): both spawn edges show an
arrowhead + badge, the second one originates from the RecipeDownloadFlag write. bUnit + builder tests
updated/added; Playwright FlowDebuggerTests 4/4.

Claude-Session: https://claude.ai/code/session_01NcMG6xXdsJbmD2wYAgkK29
2026-09-08 09:04:39 -04:00

134 lines
6.2 KiB
CSS

/* Flow Debugger swimlane — mirrors the design mockup's values through the kit's
CSS variables (theme.css light defaults; site.css dark overrides) so dark mode
follows automatically. Border #d2d2cb = --rule-strong, selected 2px #2f5fd0 on
#eef2fc = --accent on --active-bg. */
.fg-wrap {
overflow: auto;
border: 1px solid var(--rule);
border-radius: 4px;
background: var(--bs-body-bg, #fff);
}
.fg-svg {
display: block;
font-family: var(--bs-body-font-family, system-ui, sans-serif);
}
/* Lanes. The gutter labels are emitted as raw markup (SvgText — Razor reserves the <text> tag),
so they carry no CSS-isolation scope attribute and must be reached through ::deep. */
.fg-lane-bg { fill: transparent; }
.fg-lane-bg.alt { fill: var(--rule); fill-opacity: 0.25; }
.fg-lane-rule { stroke: var(--rule); stroke-width: 1; }
.fg-wrap ::deep .fg-lane-label { fill: var(--ink-soft); font-size: 0.7rem; font-weight: 600; letter-spacing: 0.04em; text-transform: uppercase; }
.fg-wrap ::deep .fg-lane-sublabel{ fill: var(--ink-faint); font-size: 0.65rem; font-family: var(--mono); }
.fg-wrap ::deep .fg-lane-prefix { fill: var(--ink-faint); font-size: 0.65rem; font-family: var(--mono); }
.fg-wrap ::deep .fg-empty { fill: var(--ink-faint); font-size: 0.85rem; font-style: italic; }
/* Nodes */
.fg-node { cursor: pointer; outline: none; }
.fg-node .fg-node-box {
fill: var(--bs-body-bg, #fff);
stroke: var(--rule-strong);
stroke-width: 1;
}
.fg-node:hover .fg-node-box,
.fg-node:focus-visible .fg-node-box { stroke: var(--accent); }
.fg-node.selected .fg-node-box {
stroke: var(--accent);
stroke-width: 2;
fill: var(--active-bg);
}
.fg-node.predicted .fg-node-box { stroke-dasharray: 4 3; }
.fg-node.input-dependent .fg-node-box { stroke-dasharray: 2 3; }
.fg-node.ghost { opacity: 0.45; }
.fg-node.ghost .fg-node-box { fill: var(--rule); fill-opacity: 0.35; }
.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 {
box-sizing: border-box;
width: 100%;
height: 100%;
padding: 6px 8px 5px;
display: flex;
flex-direction: column;
gap: 1px;
overflow: hidden;
line-height: 1.2;
color: var(--ink);
font-family: var(--bs-body-font-family, system-ui, sans-serif);
}
.fg-head { display: flex; justify-content: space-between; align-items: baseline; gap: 6px; }
.fg-eyebrow { color: var(--ink-faint); font-size: 0.6rem; font-weight: 600; letter-spacing: 0.06em; text-transform: uppercase; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.fg-duration { color: var(--ink-soft); font-size: 0.6rem; font-family: var(--mono); white-space: nowrap; flex-shrink: 0; }
.fg-title {
font-size: 0.78rem;
font-weight: 600;
overflow-wrap: anywhere;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
.fg-subtitle { color: var(--ink-soft); font-size: 0.66rem; margin-top: auto; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.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;
stroke: var(--ink-faint);
stroke-width: 1.5;
}
.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 with an ordinary arrowhead plus a bolt badge sitting
on the riser (a trigger cascade) — the bolt is NOT the arrowhead, that read as a broken arrow. */
.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-spawn-badge-bg { fill: var(--bs-body-bg, #fff); stroke: var(--ink-soft); stroke-width: 1; }
.fg-spawn-bolt { fill: var(--ink-soft); }
.fg-wrap ::deep .fg-edge-tag { fill: var(--ink-faint); font-size: 0.6rem; font-family: var(--mono); }