refactor: BaseCard hard style shadow, SocialCard blocking lists, AgentAvatar ZFC states

This commit is contained in:
zenchantlive 2026-02-16 22:41:56 -08:00
parent 54729c72f6
commit c74a4098e7
9 changed files with 231 additions and 85 deletions

View file

@ -1,21 +1,38 @@
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
import { cn } from '@/lib/utils';
type AgentStatus = 'active' | 'stale' | 'stuck' | 'dead';
export type AgentStatus = 'idle' | 'spawning' | 'running' | 'working' | 'stuck' | 'done' | 'stopped' | 'dead' | 'active' | 'stale';
export type AgentRole = 'ui' | 'graph' | 'orchestrator' | 'agent' | 'researcher';
type AvatarSize = 'sm' | 'md' | 'lg';
interface AgentAvatarProps {
name: string;
status: AgentStatus;
role?: AgentRole;
src?: string;
size?: AvatarSize;
}
const STATUS_GLOW: Record<AgentStatus, string> = {
const ROLE_BORDER_COLORS: Record<AgentRole, string> = {
ui: 'border-l-[var(--agent-role-ui)]',
graph: 'border-l-[var(--agent-role-graph)]',
orchestrator: 'border-l-[var(--agent-role-orchestrator)]',
agent: 'border-l-[var(--agent-role-agent)]',
researcher: 'border-l-[var(--agent-role-researcher)]',
};
const STATUS_VISUALS: Record<AgentStatus, string> = {
idle: 'shadow-none ring-1 ring-white/10',
spawning: 'animate-pulse shadow-[0_0_12px_rgba(91,168,160,0.4)] ring-2 ring-teal-500/40',
running: 'shadow-[0_0_12px_rgba(124,185,122,0.4)] ring-2 ring-emerald-500/40',
working: 'animate-pulse shadow-[0_0_15px_rgba(124,185,122,0.6)] ring-2 ring-emerald-500/50',
stuck: 'shadow-none ring-2 ring-amber-500/50 after:content-[""] after:absolute after:-bottom-0.5 after:-right-0.5 after:w-2.5 after:h-2.5 after:bg-[#D4A574] after:rounded-full after:border-2 after:border-[#363636]',
done: 'shadow-[0_0_10px_rgba(124,185,122,0.3)] ring-1 ring-emerald-500/30',
stopped: 'shadow-none ring-1 ring-white/5 opacity-80',
dead: 'shadow-[0_0_8px_rgba(201,122,122,0.4)] ring-2 ring-rose-900/40 opacity-60',
// Legacy mappings for safety
active: 'shadow-[0_0_12px_rgba(74,222,128,0.4)] ring-2 ring-emerald-500/40',
stale: 'shadow-[0_0_10px_rgba(251,191,36,0.3)] ring-2 ring-amber-500/30',
stuck: 'shadow-[0_0_12px_rgba(248,113,113,0.4)] ring-2 ring-rose-500/40',
dead: 'shadow-[0_0_8px_rgba(127,29,29,0.4)] ring-2 ring-red-900/40 opacity-60',
};
const SIZE_CLASSES: Record<AvatarSize, string> = {
@ -33,11 +50,18 @@ function getInitials(name: string): string {
.slice(0, 2);
}
export function AgentAvatar({ name, status, src, size = 'md' }: AgentAvatarProps) {
export function AgentAvatar({ name, status, role, src, size = 'md' }: AgentAvatarProps) {
const roleClass = role ? cn('border-l-[3px]', ROLE_BORDER_COLORS[role]) : '';
return (
<Avatar className={cn(SIZE_CLASSES[size], STATUS_GLOW[status], 'transition-all duration-200')}>
{src && <AvatarImage src={src} alt={name} />}
<AvatarFallback className="bg-surface-muted text-text-body text-xs font-semibold">
<Avatar className={cn(
SIZE_CLASSES[size],
STATUS_VISUALS[status],
roleClass,
'relative overflow-visible transition-all duration-200 bg-surface-muted'
)}>
{src && <AvatarImage src={src} alt={name} className="object-cover rounded-full overflow-hidden" />}
<AvatarFallback className="bg-transparent text-text-body text-xs font-semibold rounded-full overflow-hidden">
{getInitials(name)}
</AvatarFallback>
</Avatar>

View file

@ -1,17 +1,32 @@
import type { ReactNode, MouseEventHandler } from 'react';
import { cn } from '@/lib/utils';
import { cn } from '../../lib/utils';
import type { SocialCardStatus } from '../../lib/social-cards';
interface BaseCardProps {
children: ReactNode;
className?: string;
selected?: boolean;
status?: SocialCardStatus;
onClick?: MouseEventHandler<HTMLDivElement>;
}
export function BaseCard({ children, className, selected = false, onClick }: BaseCardProps) {
const selectedClass = selected
? 'ring-1 ring-amber-200/20 shadow-[0_24px_48px_-18px_rgba(0,0,0,0.88),0_0_26px_rgba(251,191,36,0.14)]'
: 'shadow-[0_18px_38px_-18px_rgba(0,0,0,0.82),0_6px_18px_-10px_rgba(0,0,0,0.72)] hover:shadow-[0_24px_52px_-16px_rgba(0,0,0,0.9),0_10px_26px_-10px_rgba(0,0,0,0.78)]';
const STATUS_BORDERS: Record<SocialCardStatus, string> = {
ready: 'border-[var(--status-ready)]',
in_progress: 'border-[var(--status-in-progress)]',
blocked: 'border-[var(--status-blocked)]',
closed: 'border-[var(--status-closed)]',
};
export function BaseCard({
children,
className,
selected = false,
status,
onClick
}: BaseCardProps) {
const borderClass = status
? STATUS_BORDERS[status]
: 'border-white/[0.06]';
return (
<div
@ -25,9 +40,11 @@ export function BaseCard({ children, className, selected = false, onClick }: Bas
}
}}
className={cn(
'rounded-xl border border-white/[0.06] bg-[#363636] px-3.5 py-3 transition duration-200 shadow-[inset_0_1px_0_rgba(255,255,255,0.06)]',
'rounded-[var(--radius-card)] border bg-[var(--color-bg-card)] px-3.5 py-3 transition duration-200',
'shadow-[0_4px_24px_rgba(0,0,0,0.35),inset_0_1px_0_rgba(255,255,255,0.06)] hover:shadow-[0_8px_30px_rgba(0,0,0,0.4)]',
borderClass,
onClick && 'cursor-pointer hover:border-white/[0.10]',
selectedClass,
selected && 'ring-1 ring-amber-500/30 shadow-md',
className
)}
>

View file

@ -13,58 +13,20 @@ interface SocialCardProps {
onJumpToKanban?: (id: string) => void;
}
const RELATIONSHIP_COLORS = {
// NEW: unlocks = what blocks ME (rose)
unlocks: 'text-rose-400',
// NEW: blocks = what I block (amber)
blocks: 'text-amber-400',
};
const DOT_COLORS = {
// NEW: unlocks = what blocks ME (rose)
unlocks: 'bg-rose-400',
// NEW: blocks = what I block (amber)
blocks: 'bg-amber-400',
};
function Dot({ color }: { color: 'unlocks' | 'blocks' }) {
return (
<span
className={cn(
'inline-block h-1.5 w-1.5 rounded-full mr-1.5',
DOT_COLORS[color]
)}
/>
);
}
function RelationshipSection({
label,
items,
color,
}: {
label: string;
items: string[];
color: 'unlocks' | 'blocks';
}) {
if (items.length === 0) return null;
function RelationshipItem({ id, color }: { id: string; color: 'unlocks' | 'blocks' }) {
const dotColor = color === 'unlocks' ? 'bg-rose-400' : 'bg-amber-400';
const borderColor = color === 'unlocks' ? 'border-rose-500/20' : 'border-amber-500/20';
const hoverBorder = color === 'unlocks' ? 'group-hover:border-rose-500/40' : 'group-hover:border-amber-500/40';
return (
<div className="flex items-center gap-1 text-[11px]">
<span className={cn('font-medium', RELATIONSHIP_COLORS[color])}>
{label}:
</span>
<div className="flex flex-wrap gap-x-2">
{items.slice(0, 3).map((id) => (
<span key={id} className="text-text-muted flex items-center">
<Dot color={color} />
{id}
</span>
))}
{items.length > 3 && (
<span className="text-text-muted">+{items.length - 3}</span>
)}
</div>
<div className={cn(
"group flex items-center gap-2 rounded border bg-white/5 px-2.5 py-2 transition-colors",
borderColor,
hoverBorder,
"hover:bg-white/10"
)}>
<span className={cn("h-1.5 w-1.5 rounded-full shrink-0", dotColor)} />
<span className="font-mono text-[10px] text-text-muted">{id}</span>
</div>
);
}
@ -158,9 +120,10 @@ export function SocialCard({
<BaseCard
className={cn('min-w-[220px] max-w-[320px]', className)}
selected={selected}
status={data.status}
onClick={onClick}
>
<div className="space-y-2">
<div className="space-y-3">
<div className="flex items-center justify-between">
<span className="text-teal-400 font-mono text-sm font-medium">
{data.id}
@ -179,22 +142,42 @@ export function SocialCard({
</h3>
{(hasBlocks || hasUnblocks) && (
<div className="space-y-1">
{/* UNLOCKS: tasks blocking THIS task (rose) - what blocks me */}
<RelationshipSection label="UNLOCKS" items={data.unblocks} color="unlocks" />
{/* BLOCKS: tasks THIS task blocks (amber) - what I block */}
<RelationshipSection label="BLOCKS" items={data.blocks} color="blocks" />
<div className="space-y-2 pt-1">
{/* BLOCKED BY: tasks blocking THIS task (rose) */}
{hasUnblocks && (
<div className="rounded-lg bg-black/20 p-2 border border-white/5">
<p className="mb-1.5 text-[9px] font-bold uppercase tracking-widest text-rose-400/80 pl-0.5">Blocked By</p>
<div className="flex flex-col gap-1.5">
{data.unblocks.map((id) => (
<RelationshipItem key={id} id={id} color="unlocks" />
))}
</div>
</div>
)}
{/* BLOCKING: tasks THIS task blocks (amber) */}
{hasBlocks && (
<div className="rounded-lg bg-black/20 p-2 border border-white/5">
<p className="mb-1.5 text-[9px] font-bold uppercase tracking-widest text-amber-400/80 pl-0.5">Blocking</p>
<div className="flex flex-col gap-1.5">
{data.blocks.map((id) => (
<RelationshipItem key={id} id={id} color="blocks" />
))}
</div>
</div>
)}
</div>
)}
<div className="flex items-center justify-between pt-1">
<div className="flex items-center justify-between pt-2 border-t border-white/5">
<div className="flex items-center gap-1">
{data.agents.slice(0, 3).map((agent) => (
<AgentAvatar
key={agent.name}
name={agent.name}
status={agent.status as AgentStatus}
role={agent.role}
size="sm"
/>
))}