fix(ui3): split view layout and hard-style dependency lists
This commit is contained in:
parent
395f90ed2a
commit
24c904554b
2 changed files with 134 additions and 124 deletions
|
|
@ -2,7 +2,6 @@ import type { ReactNode, MouseEventHandler } from 'react';
|
||||||
import { cn } from '../../lib/utils';
|
import { cn } from '../../lib/utils';
|
||||||
import type { SocialCard as SocialCardData, AgentStatus } from '../../lib/social-cards';
|
import type { SocialCard as SocialCardData, AgentStatus } from '../../lib/social-cards';
|
||||||
import { AgentAvatar } from '../shared/agent-avatar';
|
import { AgentAvatar } from '../shared/agent-avatar';
|
||||||
import { BaseCard } from '../shared/base-card';
|
|
||||||
|
|
||||||
interface SocialCardProps {
|
interface SocialCardProps {
|
||||||
data: SocialCardData;
|
data: SocialCardData;
|
||||||
|
|
@ -13,41 +12,31 @@ interface SocialCardProps {
|
||||||
onJumpToKanban?: (id: string) => void;
|
onJumpToKanban?: (id: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
function DependencyPill({ id, type }: { id: string; type: 'blocked-by' | 'blocking' }) {
|
// "Hard Style" Dependency Item (from TaskCardGrid inspiration)
|
||||||
// Soft, friendly pills. Rose for "blocked by", Amber for "blocking".
|
function DependencyItem({ id, type }: { id: string; type: 'blocked-by' | 'blocking' }) {
|
||||||
const styles = type === 'blocked-by'
|
const styles = type === 'blocked-by'
|
||||||
? 'bg-rose-500/10 text-rose-200 hover:bg-rose-500/20'
|
? 'border-rose-500/20 hover:border-rose-500/40 hover:bg-rose-500/10'
|
||||||
: 'bg-amber-500/10 text-amber-200 hover:bg-amber-500/20';
|
: 'border-amber-500/20 hover:border-amber-500/40 hover:bg-amber-500/10';
|
||||||
|
|
||||||
|
const dotColor = type === 'blocked-by' ? 'bg-rose-500' : 'bg-amber-500';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<span className={cn(
|
<div className={cn(
|
||||||
"inline-flex items-center px-2.5 py-1 rounded-full text-[10px] font-medium transition-colors cursor-default",
|
"flex items-center gap-2 px-2.5 py-2 rounded-md border bg-white/5 transition-all duration-200 cursor-default",
|
||||||
styles
|
styles
|
||||||
)}>
|
)}>
|
||||||
{type === 'blocked-by' ? 'Waiting on ' : 'Blocks '}
|
<div className={cn("w-1.5 h-1.5 rounded-full shadow-[0_0_8px_currentColor]", dotColor)} />
|
||||||
<span className="font-mono ml-1 opacity-80">{id}</span>
|
<span className="font-mono text-[10px] text-text-secondary">{id}</span>
|
||||||
</span>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function ActionButton({
|
function ActionButton({ icon, label, onClick }: { icon: ReactNode; label: string; onClick?: () => void }) {
|
||||||
icon,
|
|
||||||
label,
|
|
||||||
onClick,
|
|
||||||
}: {
|
|
||||||
icon: ReactNode;
|
|
||||||
label: string;
|
|
||||||
onClick?: () => void;
|
|
||||||
}) {
|
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
aria-label={label}
|
onClick={(e) => { e.stopPropagation(); onClick?.(); }}
|
||||||
onClick={(e) => {
|
className="group flex items-center justify-center p-2 rounded-full hover:bg-white/10 text-text-muted hover:text-white transition-all active:scale-95"
|
||||||
e.stopPropagation();
|
|
||||||
onClick?.();
|
|
||||||
}}
|
|
||||||
className="p-2 text-text-muted hover:text-white hover:bg-white/10 rounded-full transition-all duration-200"
|
|
||||||
title={label}
|
title={label}
|
||||||
>
|
>
|
||||||
{icon}
|
{icon}
|
||||||
|
|
@ -55,42 +44,21 @@ function ActionButton({
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function GraphIcon() {
|
function StatusIndicator({ status }: { status: string }) {
|
||||||
return (
|
const color = {
|
||||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
ready: 'bg-teal-400 shadow-[0_0_10px_rgba(45,212,191,0.5)]',
|
||||||
<circle cx="18" cy="5" r="3"></circle>
|
in_progress: 'bg-emerald-400 shadow-[0_0_10px_rgba(52,211,153,0.5)]',
|
||||||
<circle cx="6" cy="12" r="3"></circle>
|
blocked: 'bg-rose-500 shadow-[0_0_10px_rgba(244,63,94,0.5)]',
|
||||||
<circle cx="18" cy="19" r="3"></circle>
|
closed: 'bg-slate-500',
|
||||||
<line x1="8.59" y1="13.51" x2="15.42" y2="17.49"></line>
|
}[status] || 'bg-slate-500';
|
||||||
<line x1="15.41" y1="6.51" x2="8.59" y2="10.49"></line>
|
|
||||||
</svg>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function KanbanIcon() {
|
|
||||||
return (
|
|
||||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
|
||||||
<rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect>
|
|
||||||
<line x1="9" y1="3" x2="9" y2="21"></line>
|
|
||||||
</svg>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function StatusBadge({ status }: { status: string }) {
|
|
||||||
const styles = {
|
|
||||||
ready: 'bg-teal-500/10 text-teal-300 border-teal-500/20',
|
|
||||||
in_progress: 'bg-emerald-500/10 text-emerald-300 border-emerald-500/20',
|
|
||||||
blocked: 'bg-amber-500/10 text-amber-300 border-amber-500/20',
|
|
||||||
closed: 'bg-slate-500/10 text-slate-400 border-slate-500/20',
|
|
||||||
}[status as keyof typeof styles] || 'bg-slate-500/10 text-slate-400 border-slate-500/20';
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<span className={cn(
|
<div className="flex items-center gap-2">
|
||||||
"px-2.5 py-0.5 rounded-full text-[10px] font-bold uppercase tracking-wider border",
|
<div className={cn("w-2 h-2 rounded-full animate-pulse", color)} />
|
||||||
styles
|
<span className="text-[10px] font-bold uppercase tracking-widest text-text-muted/80">
|
||||||
)}>
|
{status.replace('_', ' ')}
|
||||||
{status.replace('_', ' ')}
|
</span>
|
||||||
</span>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -106,53 +74,81 @@ export function SocialCard({
|
||||||
const hasUnblocks = data.unblocks.length > 0;
|
const hasUnblocks = data.unblocks.length > 0;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<BaseCard
|
<div
|
||||||
// "Post" Styling: hover lift, soft shadow handled by BaseCard update
|
|
||||||
className={cn('flex flex-col gap-4 p-5 min-h-[180px]', className)}
|
|
||||||
selected={selected}
|
|
||||||
status={data.status}
|
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
|
role="button"
|
||||||
|
tabIndex={0}
|
||||||
|
className={cn(
|
||||||
|
"group relative flex flex-col p-6 gap-4 transition-all duration-300 ease-out",
|
||||||
|
"rounded-[2rem]", // Elegant roundness
|
||||||
|
"bg-[#252525]/90 backdrop-blur-xl", // Glassy dark
|
||||||
|
"border border-white/5 hover:border-white/10",
|
||||||
|
"shadow-lg hover:shadow-2xl hover:-translate-y-1",
|
||||||
|
selected ? "ring-2 ring-amber-500/50 shadow-amber-900/20" : "",
|
||||||
|
className
|
||||||
|
)}
|
||||||
>
|
>
|
||||||
{/* Header: ID & Status */}
|
{/* Header: Status & ID */}
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<span className="font-mono text-xs font-medium text-teal-400/80">
|
<div className="flex items-center gap-3">
|
||||||
{data.id}
|
<span className="font-mono text-xs font-bold text-teal-500/90 tracking-tight">
|
||||||
</span>
|
{data.id}
|
||||||
<StatusBadge status={data.status} />
|
</span>
|
||||||
|
<div className="h-3 w-[1px] bg-white/10" />
|
||||||
|
<StatusIndicator status={data.status} />
|
||||||
|
</div>
|
||||||
|
<div className="text-[10px] text-text-muted/40 font-mono">
|
||||||
|
{new Date(data.lastActivity).toLocaleDateString()}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Hero: Title */}
|
{/* Hero: Title */}
|
||||||
<h3 className="text-lg font-bold text-text-primary leading-tight">
|
<h3 className="text-xl font-bold text-white leading-snug tracking-tight group-hover:text-amber-50 transition-colors">
|
||||||
{data.title}
|
{data.title}
|
||||||
</h3>
|
</h3>
|
||||||
|
|
||||||
{/* Content: Dependencies (Pill Cloud) */}
|
{/* Content: Dependencies (Hard Style List) */}
|
||||||
{(hasBlocks || hasUnblocks) && (
|
{(hasBlocks || hasUnblocks) && (
|
||||||
<div className="flex flex-wrap gap-2 mt-auto pt-2">
|
<div className="flex flex-col gap-3 mt-2">
|
||||||
{/* Unblocks = Blocked By me? No.
|
{/* Blocked By */}
|
||||||
data.unblocks = tasks blocking THIS task (upstream) -> "Waiting on"
|
{hasUnblocks && (
|
||||||
data.blocks = tasks THIS task blocks (downstream) -> "Blocks"
|
<div className="flex flex-col gap-1.5 p-2 rounded-xl bg-black/20 border border-white/5">
|
||||||
*/}
|
<span className="text-[9px] font-bold uppercase tracking-widest text-rose-400/70 pl-1">Blocked By</span>
|
||||||
{data.unblocks.slice(0, 3).map((id) => (
|
<div className="flex flex-col gap-1.5">
|
||||||
<DependencyPill key={id} id={id} type="blocked-by" />
|
{data.unblocks.slice(0, 3).map((id) => (
|
||||||
))}
|
<DependencyItem key={id} id={id} type="blocked-by" />
|
||||||
{data.blocks.slice(0, 3).map((id) => (
|
))}
|
||||||
<DependencyPill key={id} id={id} type="blocking" />
|
{data.unblocks.length > 3 && (
|
||||||
))}
|
<div className="px-2 text-[10px] text-rose-400/50 italic">+{data.unblocks.length - 3} more</div>
|
||||||
{(data.unblocks.length + data.blocks.length > 6) && (
|
)}
|
||||||
<span className="px-2 py-1 text-[10px] text-text-muted/60 italic">
|
</div>
|
||||||
+{data.unblocks.length + data.blocks.length - 6} more
|
</div>
|
||||||
</span>
|
)}
|
||||||
|
|
||||||
|
{/* Blocking */}
|
||||||
|
{hasBlocks && (
|
||||||
|
<div className="flex flex-col gap-1.5 p-2 rounded-xl bg-black/20 border border-white/5">
|
||||||
|
<span className="text-[9px] font-bold uppercase tracking-widest text-amber-400/70 pl-1">Blocking</span>
|
||||||
|
<div className="flex flex-col gap-1.5">
|
||||||
|
{data.blocks.slice(0, 3).map((id) => (
|
||||||
|
<DependencyItem key={id} id={id} type="blocking" />
|
||||||
|
))}
|
||||||
|
{data.blocks.length > 3 && (
|
||||||
|
<div className="px-2 text-[10px] text-amber-400/50 italic">+{data.blocks.length - 3} more</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Footer: Agents & Actions */}
|
{/* Footer: Social Actions & Crew */}
|
||||||
<div className="flex items-center justify-between pt-4 border-t border-white/5 mt-2">
|
<div className="mt-auto pt-4 flex items-center justify-between border-t border-white/5">
|
||||||
{/* Crew */}
|
|
||||||
<div className="flex items-center -space-x-2 pl-1">
|
{/* Crew (Left) */}
|
||||||
{data.agents.map((agent) => (
|
<div className="flex items-center -space-x-3 pl-2">
|
||||||
<div key={agent.name} className="relative z-0 hover:z-10 transition-transform hover:scale-110">
|
{data.agents.slice(0, 4).map((agent) => (
|
||||||
|
<div key={agent.name} className="relative transition-transform hover:scale-110 hover:z-10 ring-2 ring-[#252525] rounded-full">
|
||||||
<AgentAvatar
|
<AgentAvatar
|
||||||
name={agent.name}
|
name={agent.name}
|
||||||
status={agent.status as AgentStatus}
|
status={agent.status as AgentStatus}
|
||||||
|
|
@ -162,24 +158,29 @@ export function SocialCard({
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
{data.agents.length === 0 && (
|
{data.agents.length === 0 && (
|
||||||
<span className="text-xs text-text-muted/40 italic">Unassigned</span>
|
<span className="text-xs text-text-muted/30 font-medium">No Crew</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Actions (Share/View) */}
|
{/* Action Dock (Right) */}
|
||||||
<div className="flex items-center gap-1">
|
<div className="flex items-center gap-1 bg-black/20 rounded-full px-2 py-1 border border-white/5">
|
||||||
<ActionButton
|
<ActionButton
|
||||||
icon={<GraphIcon />}
|
icon={
|
||||||
label="View Graph"
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><circle cx="18" cy="5" r="3"></circle><circle cx="6" cy="12" r="3"></circle><circle cx="18" cy="19" r="3"></circle><line x1="8.59" y1="13.51" x2="15.42" y2="17.49"></line><line x1="15.41" y1="6.51" x2="8.59" y2="10.49"></line></svg>
|
||||||
|
}
|
||||||
|
label="Graph"
|
||||||
onClick={() => onJumpToGraph?.(data.id)}
|
onClick={() => onJumpToGraph?.(data.id)}
|
||||||
/>
|
/>
|
||||||
|
<div className="w-[1px] h-4 bg-white/10" />
|
||||||
<ActionButton
|
<ActionButton
|
||||||
icon={<KanbanIcon />}
|
icon={
|
||||||
label="View Kanban"
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect><line x1="9" y1="3" x2="9" y2="21"></line></svg>
|
||||||
|
}
|
||||||
|
label="Kanban"
|
||||||
onClick={() => onJumpToKanban?.(data.id)}
|
onClick={() => onJumpToKanban?.(data.id)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</BaseCard>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -15,32 +15,41 @@ export function SocialPage({ issues, selectedId, onSelect }: SocialPageProps) {
|
||||||
const cards = useMemo(() => buildSocialCards(issues), [issues]);
|
const cards = useMemo(() => buildSocialCards(issues), [issues]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col h-full bg-earthy-gradient">
|
<div className="flex flex-col h-full bg-earthy-gradient overflow-hidden">
|
||||||
{/* Feed Container */}
|
{/* Top: Card Stream (Restricted Height) */}
|
||||||
<div className="flex-1 overflow-y-auto custom-scrollbar p-6 md:p-8">
|
<div className="flex-none h-[55vh] min-h-[500px] overflow-y-auto custom-scrollbar border-b border-white/5 bg-black/10 shadow-inner">
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-6 max-w-7xl mx-auto">
|
<div className="p-6 md:p-8">
|
||||||
{cards.map((card) => (
|
<div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-6 max-w-7xl mx-auto">
|
||||||
<SocialCard
|
{cards.map((card) => (
|
||||||
key={card.id}
|
<SocialCard
|
||||||
data={card}
|
key={card.id}
|
||||||
selected={selectedId === card.id}
|
data={card}
|
||||||
onClick={() => onSelect(card.id)}
|
selected={selectedId === card.id}
|
||||||
/>
|
onClick={() => onSelect(card.id)}
|
||||||
))}
|
/>
|
||||||
{cards.length === 0 && (
|
))}
|
||||||
<div className="col-span-full flex flex-col items-center justify-center py-20 text-text-muted opacity-60">
|
{cards.length === 0 && (
|
||||||
<div className="text-4xl mb-4">📭</div>
|
<div className="col-span-full flex flex-col items-center justify-center py-20 text-text-muted opacity-60">
|
||||||
<p>No active tasks found in stream.</p>
|
<div className="text-4xl mb-4">📭</div>
|
||||||
</div>
|
<p>No active tasks found in stream.</p>
|
||||||
)}
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Bottom Console (Conversation Deck) - Placeholder for future chat integration */}
|
{/* Bottom: Conversation Deck (Fills remaining space) */}
|
||||||
<div className="flex-none h-16 border-t border-white/5 bg-black/20 backdrop-blur-md flex items-center justify-center">
|
<div className="flex-1 bg-surface-muted/20 backdrop-blur-xl p-6 flex items-center justify-center relative">
|
||||||
<p className="text-xs font-medium text-text-muted/60 tracking-wide uppercase">
|
{/* Placeholder for Chat Interface */}
|
||||||
Select a task to view conversation
|
<div className="text-center space-y-2 max-w-md">
|
||||||
</p>
|
<div className="w-12 h-12 rounded-2xl bg-white/5 mx-auto flex items-center justify-center mb-4 ring-1 ring-white/10">
|
||||||
|
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" className="text-text-muted">
|
||||||
|
<path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"></path>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<h3 className="text-lg font-medium text-text-primary">Conversation Deck</h3>
|
||||||
|
<p className="text-sm text-text-muted/70">Select a task above to open its secure communication channel and activity log.</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue