feat(team): active border sweep, white chip, slower flow, gentle done

- Active card: a green highlight sweeps along the border left→right while the
  task runs (masked gradient ::before), signalling live work.
- Status chip: white background (green/amber text), Paperclip-style.
- Slower, more readable animation (edge 1500ms, stagger 680ms, longer flow gaps).
- Arrival and task-completion are now soft glow fades with NO transform, so the
  card no longer twitches on done; a dedicated doneFade plays when a task the
  agent held goes done/cancelled.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
chahinebrini 2026-07-08 05:43:31 +02:00
parent 92a5b63954
commit 38bea3b594

View File

@ -202,8 +202,8 @@ export function renderTeamHtml(cwd: string): string {
/* Floating status chip that straddles the top-right edge (Paperclip style). */
.node-badge { position: absolute; top: -9px; right: 12px; z-index: 3; font: 700 9.5px/1 var(--font-mono); text-transform: capitalize; letter-spacing: .02em; padding: 4px 9px; border-radius: 999px; }
.node-badge:empty { display: none; }
.node-badge.active { color: #0b3d1e; background: #7ee2a8; box-shadow: 0 2px 12px rgba(34,197,94,.4); }
.node-badge.review { color: #3d2e07; background: #f0cf7a; box-shadow: 0 2px 12px rgba(210,153,34,.35); }
.node-badge.active { color: #15803d; background: #ffffff; box-shadow: 0 2px 12px rgba(34,197,94,.45); }
.node-badge.review { color: #7a5a12; background: #ffffff; box-shadow: 0 2px 12px rgba(210,153,34,.4); }
.node-info-btn { flex: 0 0 auto; width: 20px; height: 20px; border-radius: 50%; border: 1px solid var(--border); background: var(--raised); color: var(--muted); font: 700 11px/1 var(--font-mono); cursor: pointer; align-self: flex-start; transition: color 150ms, border-color 150ms; }
.node-info-btn:hover { color: var(--text); border-color: var(--accent); }
@ -219,10 +219,26 @@ export function renderTeamHtml(cwd: string): string {
.st-link.review { color: var(--status-review); }
.st-link:hover { text-decoration: underline; }
.node.active { border-color: rgba(34,197,94,.6); box-shadow: 0 0 0 1px rgba(34,197,94,.5), 0 0 24px rgba(34,197,94,.15); }
/* Active card: a green highlight sweeps along the border L→R while the task runs. */
.node::before {
content: ''; position: absolute; inset: 0; border-radius: 14px; padding: 1.6px;
background: linear-gradient(90deg, rgba(34,197,94,0) 32%, #22c55e 50%, rgba(34,197,94,0) 68%);
background-size: 260% 100%; background-position: -50% 0;
-webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
-webkit-mask-composite: xor;
mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
mask-composite: exclude;
opacity: 0; transition: opacity 340ms ease; pointer-events: none; z-index: 2;
}
.node.active { border-color: rgba(34,197,94,.4); box-shadow: 0 0 20px rgba(34,197,94,.10); }
.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); }
.node.arrive { animation: arrivePop 560ms ease; }
@keyframes arrivePop { 0% { box-shadow: 0 0 0 0 rgba(34,197,94,.6); } 100% { box-shadow: 0 0 0 15px rgba(34,197,94,0); } }
/* 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); } }
.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; } }
@ -257,7 +273,7 @@ export function renderTeamHtml(cwd: string): string {
<script>
(function() {
var NS = 'http://www.w3.org/2000/svg';
var SEG = 420, EDGE_DUR = 900;
var SEG = 680, EDGE_DUR = 1500;
var connDot = document.getElementById('conn-dot');
var edgeMap = {}; // nodeId -> { fwd, rev } path from its parent
var parentOf = {}; // nodeId -> parent nodeId
@ -269,7 +285,13 @@ export function renderTeamHtml(cwd: string): string {
function centerTop(el, box) { var r = el.getBoundingClientRect(); return { x: r.left - box.left + r.width/2, y: r.top - box.top }; }
function centerBottom(el, box) { var r = el.getBoundingClientRect(); return { x: r.left - box.left + r.width/2, y: r.bottom - box.top }; }
function nodeById(id) { return document.querySelector('.node[data-node-id="' + (id||'').replace(/"/g,'') + '"]'); }
function flash(el) { if (!el) return; el.classList.remove('arrive'); void el.offsetWidth; el.classList.add('arrive'); setTimeout(function(){ el.classList.remove('arrive'); }, 560); }
function flash(el) { if (!el) return; el.classList.remove('arrive'); void el.offsetWidth; el.classList.add('arrive'); setTimeout(function(){ el.classList.remove('arrive'); }, 700); }
function doneFlashAgent(agent) {
document.querySelectorAll('.node[data-agent="' + (agent || '').replace(/"/g, '') + '"]').forEach(function(n) {
n.classList.remove('done-flash'); void n.offsetWidth; n.classList.add('done-flash');
setTimeout(function(){ n.classList.remove('done-flash'); }, 900);
});
}
function layoutEdges() {
var svg = document.getElementById('edges');
@ -355,6 +377,7 @@ export function renderTeamHtml(cwd: string): string {
if (t.status === 'in_progress' && was !== 'in_progress') pulseToAgent(agent, 'down');
else if (t.status === 'review' && was !== 'review') pulseToAgent(agent, 'up');
else if (t.status === 'open' && pv && t.assignedTo && t.assignedTo !== pv.assignedTo) pulseToAgent(agent, 'down');
else if ((t.status === 'done' || t.status === 'cancelled') && (was === 'in_progress' || was === 'review')) doneFlashAgent(agent);
});
}
var next = {}; tasks.forEach(function(t) { next[t.id] = { status: t.status, assignedTo: t.assignedTo }; });
@ -487,7 +510,7 @@ export function renderTeamHtml(cwd: string): string {
var s = DEMO[i];
showCaption((i + 1) + '/' + DEMO.length + ' · ' + s.cap, s.rev);
var dur = pulseBetween(s.from, s.to);
await sleep(dur + 550);
await sleep(dur + 850);
}
showCaption('');
if (btn) btn.removeAttribute('disabled');