diff --git a/src/components/shared/agent-avatar.tsx b/src/components/shared/agent-avatar.tsx new file mode 100644 index 0000000..90daca2 --- /dev/null +++ b/src/components/shared/agent-avatar.tsx @@ -0,0 +1,45 @@ +import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'; +import { cn } from '@/lib/utils'; + +type AgentStatus = 'active' | 'stale' | 'stuck' | 'dead'; +type AvatarSize = 'sm' | 'md' | 'lg'; + +interface AgentAvatarProps { + name: string; + status: AgentStatus; + src?: string; + size?: AvatarSize; +} + +const STATUS_GLOW: Record = { + 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 = { + sm: 'h-8 w-8', + md: 'h-10 w-10', + lg: 'h-12 w-12', +}; + +function getInitials(name: string): string { + return name + .split(' ') + .map((part) => part[0]) + .join('') + .toUpperCase() + .slice(0, 2); +} + +export function AgentAvatar({ name, status, src, size = 'md' }: AgentAvatarProps) { + return ( + + {src && } + + {getInitials(name)} + + + ); +} diff --git a/src/components/shared/base-card.tsx b/src/components/shared/base-card.tsx new file mode 100644 index 0000000..0921ff2 --- /dev/null +++ b/src/components/shared/base-card.tsx @@ -0,0 +1,37 @@ +import type { ReactNode, MouseEventHandler } from 'react'; +import { cn } from '@/lib/utils'; + +interface BaseCardProps { + children: ReactNode; + className?: string; + selected?: boolean; + onClick?: MouseEventHandler; +} + +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)]'; + + return ( +
{ + if (onClick && (e.key === 'Enter' || e.key === ' ')) { + e.preventDefault(); + e.currentTarget.click(); + } + }} + 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)]', + onClick && 'cursor-pointer hover:border-white/[0.10]', + selectedClass, + className + )} + > + {children} +
+ ); +} diff --git a/src/components/shared/index.ts b/src/components/shared/index.ts new file mode 100644 index 0000000..0cc6081 --- /dev/null +++ b/src/components/shared/index.ts @@ -0,0 +1,9 @@ +export { BaseCard } from './base-card'; +export { AgentAvatar } from './agent-avatar'; +export { StatusBadge } from './status-badge'; +export { Chip } from './chip'; +export { StatPill } from './stat-pill'; +export { statusGradient, statusBorder, statusDotColor, sessionStateGlow } from './status-utils'; +export { EpicChipStrip } from './epic-chip-strip'; +export { WorkspaceHero } from './workspace-hero'; +export { ProjectScopeControls } from './project-scope-controls'; diff --git a/src/components/shared/status-badge.tsx b/src/components/shared/status-badge.tsx new file mode 100644 index 0000000..3b76a12 --- /dev/null +++ b/src/components/shared/status-badge.tsx @@ -0,0 +1,44 @@ +import { Badge } from '@/components/ui/badge'; +import { cn } from '@/lib/utils'; + +type BeadStatus = 'ready' | 'in_progress' | 'blocked' | 'closed'; +type BadgeSize = 'sm' | 'md'; + +interface StatusBadgeProps { + status: BeadStatus; + size?: BadgeSize; +} + +const STATUS_CLASSES: Record = { + ready: 'border-teal-500/30 bg-teal-500/15 text-teal-200', + in_progress: 'border-green-500/30 bg-green-500/15 text-green-200', + blocked: 'border-amber-500/30 bg-amber-500/15 text-amber-200', + closed: 'border-slate-500/30 bg-slate-500/15 text-slate-300', +}; + +const SIZE_CLASSES: Record = { + sm: 'text-[10px] px-1.5 py-0.5', + md: 'text-xs px-2.5 py-0.5', +}; + +const STATUS_LABELS: Record = { + ready: 'Ready', + in_progress: 'In Progress', + blocked: 'Blocked', + closed: 'Closed', +}; + +export function StatusBadge({ status, size = 'md' }: StatusBadgeProps) { + return ( + + {STATUS_LABELS[status]} + + ); +}