Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XC2jX6Hdj7pxGGKzBTrbqB
267 lines
7.6 KiB
CSS
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;
|
|
}
|
|
}
|