Files
app/frontend/src/components/Flow/flow.css
T

267 lines
7.6 KiB
CSS

/*
* React Flow, routed through the design tokens.
*
* Scoped to `.react-flow` rather than added to index.css, so the canvas needs no
* tokens of its own and follows the theme in both modes without a `colorMode`
* prop. See the root DESIGN-GUIDELINES.md → Shells.
*/
.react-flow {
--xy-background-color: var(--background);
--xy-background-pattern-dots-color: color-mix(
in srgb,
var(--muted-foreground) 30%,
transparent
);
--xy-edge-stroke: color-mix(
in srgb,
var(--muted-foreground) 55%,
transparent
);
--xy-edge-stroke-selected: var(--primary);
--xy-edge-stroke-width: 1.5;
--xy-handle-background-color: var(--card);
--xy-handle-border-color: var(--muted-foreground);
--xy-selection-background-color: color-mix(
in srgb,
var(--primary) 8%,
transparent
);
--xy-selection-border: 1px solid var(--primary);
--xy-attribution-background-color: transparent;
}
/*
* Where a pulse lands: the opaque twin of the stroke the edge rests in, so the
* last step back is invisible. A selected edge rests in blue and so decays into
* it rather than crossing to grey and snapping back at the end.
*/
.react-flow__edge {
--edge-rest: var(--muted-foreground);
}
.react-flow__edge.selected {
--edge-rest: var(--primary);
}
/* A message arriving lights its edge, then decays back to rest. */
@media (prefers-reduced-motion: no-preference) {
.react-flow__edge-path.edge-live {
animation: edge-pulse var(--duration-pulse) var(--ease-emphasized);
}
/*
* Both ends are plain colours. The implied `to` would be the edge's resting
* stroke, which is a `color-mix()` — and interpolating a hex against one of
* those goes through oklab and leaves the gamut on the way, which turned
* every pulse fluorescent yellow.
*/
@keyframes edge-pulse {
from {
stroke: var(--primary);
stroke-width: 2.5;
}
to {
stroke: var(--edge-rest);
stroke-width: var(--xy-edge-stroke-width);
}
}
}
/* A node that just published something says so, once, and settles. */
@media (prefers-reduced-motion: no-preference) {
.node-pulse {
position: absolute;
inset: -3px;
border-radius: inherit;
border: 2px solid var(--primary);
box-shadow: 0 0 0 3px color-mix(in srgb, var(--primary) 18%, transparent);
pointer-events: none;
animation: node-pulse var(--duration-pulse) var(--ease-emphasized) forwards;
}
@keyframes node-pulse {
from {
opacity: 1;
transform: scale(1);
}
to {
opacity: 0;
transform: scale(1.09);
}
}
}
/* Handles are neutral: the one brand-secondary affordance here is Run. */
.react-flow__handle {
width: 12px;
height: 12px;
border-width: 2px;
}
.react-flow__handle.unbound {
border-style: dashed;
border-color: color-mix(in srgb, var(--muted-foreground) 40%, transparent);
}
.react-flow__handle-connecting {
border-color: var(--primary);
}
.react-flow__node:focus-visible,
.react-flow__node:focus {
outline: none;
}
/*
* Brain graph. A neuron meets its connections all round its rim, so these two
* handles are only there to make React Flow treat the node as wired at all —
* where a connection actually lands is `BrainEdge`'s own geometry, measured
* from the centre they sit at.
*/
.brain-handle {
opacity: 0;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
}
/*
* Embedded on Home rather than on a canvas of its own: the graph paints no
* surface, so the page's own background shows through behind the neurons.
*
* `--brain-decay` is how long a neuron or a connection takes to dim from its
* brightest back to the floor. Not a UI transition but the falloff of a signal,
* so it is measured in minutes rather than in the `--duration-*` tokens.
*/
.react-flow.brain-flat {
--xy-background-color: transparent;
--brain-decay: 90s;
}
/*
* A neuron is the node of the brand mark: a disc of the identity blue inside a
* thick ring, wired to its neighbours. The ring thickness is a share of the
* diameter and lives in `BrainNode`, which is where the diameter is known.
*
* The fill is identity and carries no status — that is the ring's job, and the
* word under it. What the fill's lightness carries is how recently this page
* saw the neuron publish: brightest just after it fires, decaying to a floor it
* never drops below, so a graph left open sorts itself into what is busy and
* what is not. The floor stays blue enough to still read as the mark. A fresh
* page has seen nothing either way, so everything starts at `.brain-idle`,
* halfway, claiming no history it does not have.
*/
.brain-cell {
cursor: pointer;
background-color: color-mix(in srgb, var(--primary) 50%, transparent);
transition: border-color var(--duration-base) var(--ease-standard);
}
.brain-cell.brain-idle {
background-color: color-mix(in srgb, var(--primary) 65%, transparent);
}
/* Snapped into, decayed out of — never the other way round. */
.brain-cell.brain-hot {
background-color: color-mix(in srgb, var(--primary) 90%, transparent);
transition: none;
}
@media (prefers-reduced-motion: no-preference) {
.brain-cell {
transition:
border-color var(--duration-base) var(--ease-standard),
background-color var(--brain-decay) linear;
}
}
/*
* Outside the ring rather than in it: the ring is what says whether this neuron
* can run, and the disc is already `--primary`, so recolouring either to mark
* the pointer would either overwrite a status or vanish into the fill.
*/
.brain-cell:hover,
.react-flow__node:focus-visible .brain-cell {
outline: 2px solid var(--ring);
outline-offset: 2px;
}
/*
* A name under every circle is what makes the graph unreadable, so only the
* hovered or focused neuron carries one. Absolutely positioned: the layout's
* collide radius already holds this space clear, and revealing a label must not
* move anything.
*/
.brain-label {
position: absolute;
top: 100%;
left: 50%;
transform: translateX(-50%);
margin-top: 6px;
display: flex;
flex-direction: column;
align-items: center;
visibility: hidden;
}
.brain-cell:hover .brain-label,
.react-flow__node:focus-visible .brain-label,
.brain-label.brain-label-on {
visibility: visible;
}
/*
* The same falloff as the neurons, on the connection instead of the disc. It
* carries the line and its two end dots alike, so the ink is a variable on the
* group they share rather than a stroke on each.
*/
.brain-wire {
--brain-ink: color-mix(in srgb, var(--muted-foreground) 38%, transparent);
}
.brain-wire.brain-idle {
--brain-ink: color-mix(in srgb, var(--muted-foreground) 55%, transparent);
}
/* Snapped into, decayed out of — never the other way round. */
.brain-wire.brain-hot {
--brain-ink: color-mix(in srgb, var(--muted-foreground) 85%, transparent);
}
.react-flow__edge-path.brain-edge {
stroke: var(--brain-ink);
stroke-linecap: round;
stroke-linejoin: round;
}
/*
* Where a connection meets a neuron: a flow node's port, sat on the rim the same
* way, but told apart by direction — filled at the end the value leaves, hollow
* at the end it arrives. The hollow one takes the surface the band sits flat on,
* so it reads as a hole rather than as a disc of its own.
*/
.brain-dot {
fill: var(--card);
stroke: var(--brain-ink);
stroke-width: 1.5;
}
.brain-dot.brain-dot-out {
fill: var(--brain-ink);
}
@media (prefers-reduced-motion: no-preference) {
.react-flow__edge-path.brain-edge,
.brain-dot {
transition:
stroke var(--brain-decay) linear,
fill var(--brain-decay) linear;
}
.brain-wire.brain-hot .react-flow__edge-path.brain-edge,
.brain-wire.brain-hot .brain-dot {
transition: none;
}
}