From 18837e888085ab0426ba011801e9a2f00d1eb028 Mon Sep 17 00:00:00 2001 From: stroblme Date: Mon, 17 Aug 2026 14:50:39 +0200 Subject: [PATCH] Brain: neurons in the mark's blue, ringed in destructive when they cannot run Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01XC2jX6Hdj7pxGGKzBTrbqB --- frontend/src/components/Flow/BrainNode.tsx | 45 ++++++++++++++----- frontend/src/components/Flow/BrainView.tsx | 1 + frontend/src/components/Flow/flow.css | 44 +++++++++--------- .../src/components/Health/HealthOverview.tsx | 10 +++-- 4 files changed, 63 insertions(+), 37 deletions(-) diff --git a/frontend/src/components/Flow/BrainNode.tsx b/frontend/src/components/Flow/BrainNode.tsx index 7e32b1f..7de099d 100644 --- a/frontend/src/components/Flow/BrainNode.tsx +++ b/frontend/src/components/Flow/BrainNode.tsx @@ -13,13 +13,28 @@ export type BrainNodeData = { flows: string[] /** Circle diameter in pixels, from how many neurons this one is wired to. */ size: number + /** Why this neuron cannot run, if validation found something. */ + issue?: string | null [key: string]: unknown } +/** + * Ring thickness as a share of the diameter, measured off the brand mark: a + * 1.4545 stroke on a 6.3 outer diameter in `fluksio-light.svg`. + * + * A share rather than a fixed width because the neurons run from 32px to 96px: + * the same ring in pixels would swallow the smallest and read as a hairline on + * the largest, and it is the proportion that makes a circle look like the mark. + */ +const RING = 0.23 + function BrainNodeComponent({ data }: NodeProps) { - const { label, kind, members, flows, size } = data as BrainNodeData + const { label, kind, members, flows, size, issue } = data as BrainNodeData const emits = useGroupEmits(members) const failed = useGroupError(members) + // One ring for both kinds of trouble: a run that broke and wiring that can + // never run are the same answer to "can I trust this neuron right now". + const problem = failed || !!issue // Brightest right after a publish, then left to decay. Only the peak needs // holding: the floor is the resting style, so letting go is the whole decay. const [firing, setFiring] = useState(false) @@ -34,14 +49,16 @@ function BrainNodeComponent({ data }: NodeProps) { return ( {/* Remounting on each emit is what restarts the animation. */} {emits > 0 ? : null} @@ -63,15 +80,23 @@ function BrainNodeComponent({ data }: NodeProps) { {/* * A name under every circle is what makes the graph unreadable, so only - * the neuron under the pointer or the keyboard says what it is — except a - * failing one, since colour is never the only carrier of a status. + * the neuron under the pointer or the keyboard says what it is — except + * one with something wrong, since colour is never the only carrier of a + * status. Two words rather than the reason itself: a cycle names every + * node in it, which is a sentence no label under a 32px circle can hold, + * so the phrase matches what Home says and the full text stays in the + * tooltip. */} - + {label} - {failed ? ( - failed + {/* The label box is only as wide as the circle it hangs under, so two + words break onto two lines unless told not to. */} + {problem ? ( + + {failed ? "failed" : "cannot run"} + ) : null} diff --git a/frontend/src/components/Flow/BrainView.tsx b/frontend/src/components/Flow/BrainView.tsx index 8658941..d256c56 100644 --- a/frontend/src/components/Flow/BrainView.tsx +++ b/frontend/src/components/Flow/BrainView.tsx @@ -129,6 +129,7 @@ function build(graph: BrainGraph): { nodes: Node[]; edges: Edge[] } { members: source.members ?? [], flows: source.flows ?? [], size: node.size, + issue: source.issue, } satisfies BrainNodeData, } }), diff --git a/frontend/src/components/Flow/flow.css b/frontend/src/components/Flow/flow.css index 48487ea..148bc59 100644 --- a/frontend/src/components/Flow/flow.css +++ b/frontend/src/components/Flow/flow.css @@ -140,41 +140,31 @@ } /* - * A neuron is a filled disc rather than an outline: a `--card` circle on - * `--background` is a hairline in light and all but nothing in dark. + * 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 neutral, and stays neutral — status still speaks only through the - * border and the word under it. What its 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. A fresh page has seen nothing either way, so everything - * starts at `.brain-idle`, halfway, claiming no history it does not have. + * 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(--muted-foreground) 28%, - transparent - ); + 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(--muted-foreground) 42%, - transparent - ); + 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(--muted-foreground) 65%, - transparent - ); + background-color: color-mix(in srgb, var(--primary) 90%, transparent); transition: none; } @@ -186,9 +176,15 @@ } } +/* + * 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 { - border-color: var(--primary); + outline: 2px solid var(--ring); + outline-offset: 2px; } /* diff --git a/frontend/src/components/Health/HealthOverview.tsx b/frontend/src/components/Health/HealthOverview.tsx index 1019414..2088225 100644 --- a/frontend/src/components/Health/HealthOverview.tsx +++ b/frontend/src/components/Health/HealthOverview.tsx @@ -117,12 +117,16 @@ export function HealthOverview({