refactor: BaseCard hard style shadow, SocialCard blocking lists, AgentAvatar ZFC states
This commit is contained in:
parent
54729c72f6
commit
c74a4098e7
9 changed files with 231 additions and 85 deletions
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
)}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
/>
|
||||
))}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue