Brain: terracotta for fault, a ring set off from the fill, and a dotted ground
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XC2jX6Hdj7pxGGKzBTrbqB
This commit is contained in:
@@ -19,21 +19,24 @@ export type BrainNodeData = {
|
||||
}
|
||||
|
||||
/**
|
||||
* 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`.
|
||||
* The mark's own proportions, measured off `fluksio-light.svg`: a 1.4545 ring
|
||||
* and a 0.4848 gap on a 6.303 outer diameter, leaving the disc the 38.5% in
|
||||
* the middle.
|
||||
*
|
||||
* A share rather than a fixed width because the neurons run from 32px to 96px:
|
||||
* Shares rather than fixed widths 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
|
||||
const RING = 0.231
|
||||
const GAP = 0.077
|
||||
|
||||
function BrainNodeComponent({ data }: NodeProps) {
|
||||
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".
|
||||
// Two faults, told apart the way the mark's two parts are: the ring is the
|
||||
// wiring around the node, so a flow that cannot run as written colours the
|
||||
// ring; the disc is the node itself, so a run that broke colours the disc.
|
||||
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.
|
||||
@@ -53,11 +56,19 @@ function BrainNodeComponent({ data }: NodeProps) {
|
||||
// Nothing seen yet this session is not the same as seen and gone quiet.
|
||||
emits === 0 && "brain-idle",
|
||||
firing && "brain-hot",
|
||||
problem ? "border-destructive" : "border-foreground",
|
||||
failed && "brain-fault",
|
||||
issue ? "border-brand-secondary" : "border-foreground",
|
||||
)}
|
||||
// Border-box, so the outer rim stays at `size` and the edges keep landing
|
||||
// on it however thick the ring gets.
|
||||
style={{ width: size, height: size, borderWidth: size * RING }}
|
||||
// on it however thick the ring gets. The gap between ring and disc is an
|
||||
// inset shadow rather than padding: it paints over the fill without
|
||||
// taking part in the layout, so the count stays centred in the disc.
|
||||
style={{
|
||||
width: size,
|
||||
height: size,
|
||||
borderWidth: size * RING,
|
||||
boxShadow: `inset 0 0 0 ${size * GAP}px var(--brain-gap)`,
|
||||
}}
|
||||
title={`${label} · ${kind} · ${members.join(", ")}${issue ? ` · ${issue}` : ""}`}
|
||||
>
|
||||
{/* Remounting on each emit is what restarts the animation. */}
|
||||
@@ -82,10 +93,14 @@ 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
|
||||
* 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.
|
||||
* status, and here it carries two of them. 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.
|
||||
*
|
||||
* The word is `--foreground` rather than the terracotta beside it:
|
||||
* `--brand-secondary` measures 2.2:1 on `--card` in light, which is a
|
||||
* fill colour, not a text colour.
|
||||
*/}
|
||||
<span className={cn("brain-label", problem && "brain-label-on")}>
|
||||
<span className="max-w-[140px] truncate text-xs font-medium">
|
||||
@@ -94,8 +109,10 @@ function BrainNodeComponent({ data }: NodeProps) {
|
||||
{/* 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 ? (
|
||||
<span className="whitespace-nowrap text-xs font-medium text-destructive">
|
||||
{failed ? "failed" : "cannot run"}
|
||||
<span className="whitespace-nowrap text-xs font-medium text-foreground">
|
||||
{[failed && "failed", issue && "cannot run"]
|
||||
.filter(Boolean)
|
||||
.join(" · ")}
|
||||
</span>
|
||||
) : null}
|
||||
</span>
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { useQuery } from "@tanstack/react-query"
|
||||
import { useNavigate } from "@tanstack/react-router"
|
||||
import {
|
||||
Background,
|
||||
BackgroundVariant,
|
||||
type Edge,
|
||||
type Node,
|
||||
ReactFlow,
|
||||
@@ -209,7 +211,13 @@ function BrainCanvas() {
|
||||
navigate({ to: "/flows/$flowName", params: { flowName: flow } })
|
||||
}}
|
||||
className="brain-flat h-full w-full"
|
||||
/>
|
||||
>
|
||||
{/* The same dot grid the editor's canvas uses, quieter and masked back
|
||||
to nothing at the edges — see flow.css. No zoom compensation here:
|
||||
this canvas only ever sits at whatever `fitView` chose and cannot
|
||||
be zoomed, so there is no moiré to chase. */}
|
||||
<Background variant={BackgroundVariant.Dots} gap={24} size={1.5} />
|
||||
</ReactFlow>
|
||||
</motion.div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -137,34 +137,71 @@
|
||||
.react-flow.brain-flat {
|
||||
--xy-background-color: transparent;
|
||||
--brain-decay: 90s;
|
||||
/* Thicker than the editor's, to match the weight the mark draws them at. */
|
||||
--xy-edge-stroke-width: 2;
|
||||
--xy-background-pattern-dots-color: color-mix(
|
||||
in srgb,
|
||||
var(--muted-foreground) 22%,
|
||||
transparent
|
||||
);
|
||||
}
|
||||
|
||||
/*
|
||||
* 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 ground the mark's nodes sit on. The band has no surface of its own and no
|
||||
* border, so a dot grid that ran to the edges would draw the rectangle the band
|
||||
* otherwise does not have. An elliptical mask fades it out well before any
|
||||
* edge — the same shape the layout settles into, so the dots run out roughly
|
||||
* where the graph does. The stops are the mask's alpha, not a colour: opaque
|
||||
* keeps the dots, transparent drops them.
|
||||
*/
|
||||
.brain-flat .react-flow__background {
|
||||
mask-image: radial-gradient(
|
||||
ellipse 62% 62% at 50% 50%,
|
||||
black 0%,
|
||||
transparent 100%
|
||||
);
|
||||
}
|
||||
|
||||
/*
|
||||
* A neuron is the node of the brand mark: a disc of the identity blue, a gap in
|
||||
* the surface colour, then a thick ring. Ring and gap are shares of the
|
||||
* diameter and live in `BrainNode`, which is where the diameter is known; the
|
||||
* gap is what the mark's tile shows between the two, which here is the card the
|
||||
* band sits flat on.
|
||||
*
|
||||
* 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.
|
||||
* The two parts carry the two faults, the way the mark is built: the ring is
|
||||
* the wiring, so a flow that cannot run as written turns it `--brand-secondary`;
|
||||
* the disc is the node, so a run that broke turns the fill the same terracotta.
|
||||
* Both at once still reads, since the gap keeps them apart. This is the one
|
||||
* place terracotta means fault rather than a call to action — see the root
|
||||
* DESIGN-GUIDELINES.md → Colour tokens.
|
||||
*
|
||||
* 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 saturated 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 {
|
||||
--brain-fill: var(--primary);
|
||||
--brain-gap: var(--card);
|
||||
cursor: pointer;
|
||||
background-color: color-mix(in srgb, var(--primary) 50%, transparent);
|
||||
background-color: color-mix(in srgb, var(--brain-fill) 50%, transparent);
|
||||
transition: border-color var(--duration-base) var(--ease-standard);
|
||||
}
|
||||
|
||||
.brain-cell.brain-fault {
|
||||
--brain-fill: var(--brand-secondary);
|
||||
}
|
||||
|
||||
.brain-cell.brain-idle {
|
||||
background-color: color-mix(in srgb, var(--primary) 65%, transparent);
|
||||
background-color: color-mix(in srgb, var(--brain-fill) 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);
|
||||
background-color: color-mix(in srgb, var(--brain-fill) 90%, transparent);
|
||||
transition: none;
|
||||
}
|
||||
|
||||
@@ -174,17 +211,27 @@
|
||||
border-color var(--duration-base) var(--ease-standard),
|
||||
background-color var(--brain-decay) linear;
|
||||
}
|
||||
|
||||
/*
|
||||
* A fault is a state change, not a falloff. Sharing the decay's transition
|
||||
* would have the disc creep from blue to terracotta over a minute and a half,
|
||||
* so the fill takes the same 200ms the ring beside it does.
|
||||
*/
|
||||
.brain-cell.brain-fault {
|
||||
transition:
|
||||
border-color var(--duration-base) var(--ease-standard),
|
||||
background-color var(--duration-base) var(--ease-standard);
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* 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.
|
||||
* The pointer lights the gap rather than adding a fourth band around a circle
|
||||
* that already has three. Ring and disc both carry a status, so neither can be
|
||||
* recoloured to mark a hover; the surface between them carries nothing.
|
||||
*/
|
||||
.brain-cell:hover,
|
||||
.react-flow__node:focus-visible .brain-cell {
|
||||
outline: 2px solid var(--ring);
|
||||
outline-offset: 2px;
|
||||
--brain-gap: var(--ring);
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -215,18 +262,23 @@
|
||||
* 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.
|
||||
*
|
||||
* Drawn from `--foreground`, the same token as the rings: in the mark a
|
||||
* connection is the same stroke as the ring it runs into, and mixing the two
|
||||
* families instead left the graph looking like rims in one drawing and wires
|
||||
* from another.
|
||||
*/
|
||||
.brain-wire {
|
||||
--brain-ink: color-mix(in srgb, var(--muted-foreground) 38%, transparent);
|
||||
--brain-ink: color-mix(in srgb, var(--foreground) 32%, transparent);
|
||||
}
|
||||
|
||||
.brain-wire.brain-idle {
|
||||
--brain-ink: color-mix(in srgb, var(--muted-foreground) 55%, transparent);
|
||||
--brain-ink: color-mix(in srgb, var(--foreground) 48%, 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);
|
||||
--brain-ink: color-mix(in srgb, var(--foreground) 80%, transparent);
|
||||
}
|
||||
|
||||
.react-flow__edge-path.brain-edge {
|
||||
|
||||
Reference in New Issue
Block a user