Send a node's emit pulse round the rim instead of scaling it out

The ring hung off the node itself, and `inset` on an absolute child
resolves against the padding box — so on the brain's thick status ring it
landed inside the outer edge and painted over the status the ring carries.
Both node shapes now hang it off a border-less wrapper, where the same
offsets clear the rim whatever border the node draws (measured: 4px on all
four sides, on a 1px card border and a 7px neuron ring alike).

The shape is a conic-gradient arc masked to the padding band, running one
lap in `--duration-pulse` and going out on the way past.

Note: BrainNode.tsx also carries an unrelated in-flight change from a
concurrent session (the `seen` ref gating the pulse on a real emit); the
two could not be separated in one file.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-20 11:51:21 +02:00
co-authored by Claude Opus 5
parent e84163d8ad
commit ff1b690449
4 changed files with 223 additions and 163 deletions
+47 -7
View File
@@ -99,26 +99,66 @@
}
}
/* A node that just published something says so, once, and settles. */
/*
* A node that just published something says so, once, and settles: an arc runs
* one lap of the rim and goes out.
*
* The angle has to be registered to be animatable at all — an unregistered
* custom property is a token stream, and swapping one for another mid-gradient
* would step rather than sweep. Declared outside the guard below because it is
* a type, not motion.
*/
@property --node-pulse-angle {
syntax: "<angle>";
inherits: false;
initial-value: 0turn;
}
@media (prefers-reduced-motion: no-preference) {
/*
* Hangs off the wrapper the node's border is *not* on: `inset` on an absolute
* child resolves against the padding box, so a ring measured from the node
* itself lands inside a thick border — the brain's status ring is one — and
* paints over it. Measured from the wrapper, the same offsets clear the rim
* whatever border the node draws. The wrapper carries the radius too, which
* is what lets one rule fit both a rounded card and a circle.
*
* The band is the padding: the content box is cut back out of the mask, which
* leaves a ring rather than the filled shape the gradient paints.
*/
.node-pulse {
position: absolute;
inset: -3px;
inset: -4px;
border-radius: inherit;
border: 2px solid var(--primary);
box-shadow: 0 0 0 3px color-mix(in srgb, var(--primary) 18%, transparent);
padding: 2px;
pointer-events: none;
animation: node-pulse var(--duration-pulse) var(--ease-emphasized) forwards;
background: conic-gradient(
from var(--node-pulse-angle),
transparent,
var(--primary) 90deg,
transparent 90deg
);
mask:
linear-gradient(#000 0 0) content-box exclude,
linear-gradient(#000 0 0);
animation: node-pulse var(--duration-pulse) linear forwards;
}
/*
* One lap at a constant rate, because the arc is travelling: an ease would
* read as braking into the finish. The element stays mounted until the next
* emit remounts it, so the lap has to put itself out on the way past.
*/
@keyframes node-pulse {
from {
--node-pulse-angle: 0turn;
}
75% {
opacity: 1;
transform: scale(1);
}
to {
--node-pulse-angle: 1turn;
opacity: 0;
transform: scale(1.09);
}
}
}