fix(team): kill done-twitch (glow on ::after) + only final node flashes

- Move the arrival/completion glow onto a dedicated ::after overlay animated by
  opacity only, so it no longer collides with the node's box-shadow transition
  — the card stays put on done (transform verified identity throughout).
- A pulse now lights only its FINAL destination; waypoint nodes it merely passes
  through (e.g. the CEO on an architect→strategist peer handoff) are no longer
  flashed as 'involved'.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
chahinebrini 2026-07-08 05:52:20 +02:00
parent b097ad71de
commit 55efae028a

View File

@ -234,11 +234,16 @@ export function renderTeamHtml(cwd: string): string {
.node.active::before { opacity: 1; animation: borderSweep 2.6s linear infinite; }
@keyframes borderSweep { 0% { background-position: -50% 0; } 100% { background-position: 150% 0; } }
.node.reviewing { border-color: rgba(210,153,34,.55); box-shadow: 0 0 0 1px rgba(210,153,34,.45), 0 0 20px rgba(210,153,34,.12); }
/* Gentle, non-jerky glow fades for pulse-arrival and task completion. */
.node.arrive { animation: arriveGlow 700ms ease; }
@keyframes arriveGlow { 0% { box-shadow: 0 0 0 1px rgba(34,197,94,.55), 0 0 18px rgba(34,197,94,.28); } 100% { box-shadow: 0 0 0 1px rgba(34,197,94,0), 0 0 0 rgba(34,197,94,0); } }
.node.done-flash { animation: doneFade 900ms ease; }
@keyframes doneFade { 0% { box-shadow: 0 0 0 1px rgba(34,197,94,.5); } 45% { box-shadow: 0 0 0 1px rgba(34,197,94,.4), 0 0 22px rgba(34,197,94,.22); } 100% { box-shadow: 0 0 0 1px rgba(34,197,94,0); } }
/* Arrival + completion glows live on a separate ::after overlay (opacity only)
so they never fight the node's own box-shadow transition no twitch. */
.node::after {
content: ''; position: absolute; inset: 0; border-radius: 14px; pointer-events: none;
box-shadow: 0 0 0 1px rgba(34,197,94,.55), 0 0 22px rgba(34,197,94,.25);
opacity: 0;
}
.node.arrive::after { animation: glowInOut 700ms ease; }
.node.done-flash::after { animation: glowInOut 950ms ease; }
@keyframes glowInOut { 0% { opacity: 0; } 38% { opacity: 1; } 100% { opacity: 0; } }
.edge-base { fill: none; stroke: var(--border); stroke-width: 1.5; animation: edgeBreath 4.5s ease-in-out infinite; }
@keyframes edgeBreath { 0%,100% { opacity: .35; } 50% { opacity: .62; } }
@ -321,7 +326,7 @@ export function renderTeamHtml(cwd: string): string {
});
}
function firePulseEdge(nodeId, dir) {
function firePulseEdge(nodeId, dir, doFlash) {
var e = edgeMap[nodeId]; var svg = document.getElementById('edges');
if (!e || !svg) return;
var p = document.createElementNS(NS, 'path');
@ -333,7 +338,9 @@ export function renderTeamHtml(cwd: string): string {
p.style.strokeDashoffset = String(len + 20);
var done = function() {
try { p.remove(); } catch (_) {}
flash(dir === 'up' ? nodeById(parentOf[nodeId]) : nodeById(nodeId));
// Only the final destination lights up — waypoints (e.g. CEO on a
// peer-to-peer handoff) are just passed through, not "involved".
if (doFlash) flash(dir === 'up' ? nodeById(parentOf[nodeId]) : nodeById(nodeId));
};
if (p.animate) { p.animate([{ strokeDashoffset: len + 20 }, { strokeDashoffset: 0 }], { duration: EDGE_DUR, easing: 'cubic-bezier(.4,0,.5,1)' }).onfinish = done; }
else setTimeout(done, EDGE_DUR);
@ -344,7 +351,7 @@ export function renderTeamHtml(cwd: string): string {
var chain = []; var cur = nodeId;
while (cur && edgeMap[cur]) { chain.push(cur); cur = parentOf[cur]; } // node → topChild
if (dir === 'down') chain.reverse(); // topChild → node
chain.forEach(function(id, i) { setTimeout(function() { firePulseEdge(id, dir); }, i * SEG); });
chain.forEach(function(id, i) { setTimeout(function() { firePulseEdge(id, dir, i === chain.length - 1); }, i * SEG); });
}
function pulseToAgent(agent, dir) {
document.querySelectorAll('.node[data-agent="' + (agent||'').replace(/"/g,'') + '"]').forEach(function(n) {
@ -478,7 +485,7 @@ export function renderTeamHtml(cwd: string): string {
var seq = [];
for (var u = 0; u < ai; u++) seq.push({ nodeId: A[u], dir: 'up' }); // from → LCA
for (var d = Bidx[lca] - 1; d >= 0; d--) seq.push({ nodeId: B[d], dir: 'down' }); // LCA → to
seq.forEach(function(step, k) { setTimeout(function(){ firePulseEdge(step.nodeId, step.dir); }, k * SEG); });
seq.forEach(function(step, k) { setTimeout(function(){ firePulseEdge(step.nodeId, step.dir, k === seq.length - 1); }, k * SEG); });
return Math.max(EDGE_DUR, seq.length * SEG + EDGE_DUR);
}