fix(ui3): resolve dashboard crash and refine left panel data
This commit is contained in:
parent
c79dfff0c6
commit
fcbe7df804
5 changed files with 221 additions and 116 deletions
|
|
@ -1 +1 @@
|
|||
97492
|
||||
117304
|
||||
|
|
|
|||
|
|
@ -14,6 +14,15 @@ export interface LeftPanelProps {
|
|||
interface EpicNode {
|
||||
epic: BeadIssue;
|
||||
children: BeadIssue[];
|
||||
stats: {
|
||||
total: number;
|
||||
closed: number;
|
||||
in_progress: number;
|
||||
blocked: number;
|
||||
ready: number;
|
||||
lastActivity: number;
|
||||
};
|
||||
status: 'blocked' | 'in_progress' | 'ready' | 'done' | 'empty';
|
||||
}
|
||||
|
||||
function buildEpicTree(issues: BeadIssue[]): EpicNode[] {
|
||||
|
|
@ -21,7 +30,12 @@ function buildEpicTree(issues: BeadIssue[]): EpicNode[] {
|
|||
const epicMap = new Map<string, EpicNode>();
|
||||
|
||||
for (const epic of epics) {
|
||||
epicMap.set(epic.id, { epic, children: [] });
|
||||
epicMap.set(epic.id, {
|
||||
epic,
|
||||
children: [],
|
||||
stats: { total: 0, closed: 0, in_progress: 0, blocked: 0, ready: 0, lastActivity: new Date(epic.updated_at).getTime() },
|
||||
status: 'empty'
|
||||
});
|
||||
}
|
||||
|
||||
for (const issue of issues) {
|
||||
|
|
@ -29,13 +43,48 @@ function buildEpicTree(issues: BeadIssue[]): EpicNode[] {
|
|||
|
||||
const parentDep = issue.dependencies.find(dep => dep.type === 'parent');
|
||||
if (parentDep && epicMap.has(parentDep.target)) {
|
||||
epicMap.get(parentDep.target)!.children.push(issue);
|
||||
const node = epicMap.get(parentDep.target)!;
|
||||
node.children.push(issue);
|
||||
|
||||
node.stats.total++;
|
||||
if (issue.status === 'closed') node.stats.closed++;
|
||||
else if (issue.status === 'blocked') node.stats.blocked++;
|
||||
else if (issue.status === 'in_progress') node.stats.in_progress++;
|
||||
else node.stats.ready++; // open/ready
|
||||
|
||||
const issueTime = new Date(issue.updated_at).getTime();
|
||||
if (issueTime > node.stats.lastActivity) node.stats.lastActivity = issueTime;
|
||||
}
|
||||
}
|
||||
|
||||
return Array.from(epicMap.values()).sort((a, b) =>
|
||||
a.epic.id.localeCompare(b.epic.id)
|
||||
);
|
||||
// Determine Aggregate Status
|
||||
for (const node of epicMap.values()) {
|
||||
if (node.stats.blocked > 0) node.status = 'blocked';
|
||||
else if (node.stats.in_progress > 0) node.status = 'in_progress';
|
||||
else if (node.stats.ready > 0) node.status = 'ready';
|
||||
else if (node.stats.total > 0 && node.stats.closed === node.stats.total) node.status = 'done';
|
||||
else node.status = 'empty';
|
||||
}
|
||||
|
||||
return Array.from(epicMap.values()).sort((a, b) => {
|
||||
// Sort by status priority (Blocked > In Progress > Ready > Done) then Recency
|
||||
const statusScore = { blocked: 4, in_progress: 3, ready: 2, done: 1, empty: 0 };
|
||||
const scoreDiff = statusScore[b.status] - statusScore[a.status];
|
||||
if (scoreDiff !== 0) return scoreDiff;
|
||||
return b.stats.lastActivity - a.stats.lastActivity;
|
||||
});
|
||||
}
|
||||
|
||||
function StatusIndicator({ status }: { status: string }) {
|
||||
const styles = {
|
||||
blocked: 'bg-rose-500 shadow-[0_0_8px_#f43f5e]',
|
||||
in_progress: 'bg-amber-500 shadow-[0_0_8px_#f59e0b]',
|
||||
ready: 'bg-teal-500 shadow-[0_0_8px_#14b8a6]',
|
||||
done: 'bg-slate-500',
|
||||
empty: 'bg-white/10'
|
||||
}[status] || 'bg-slate-500';
|
||||
|
||||
return <div className={cn("w-1.5 h-1.5 rounded-full shrink-0", styles)} />;
|
||||
}
|
||||
|
||||
export function LeftPanel({
|
||||
|
|
@ -61,28 +110,25 @@ export function LeftPanel({
|
|||
};
|
||||
|
||||
const handleEpicClick = (epicId: string) => {
|
||||
onEpicSelect?.(epicId === selectedEpicId ? null : epicId); // Toggle selection
|
||||
onEpicSelect?.(epicId === selectedEpicId ? null : epicId);
|
||||
toggleEpic(epicId);
|
||||
};
|
||||
|
||||
if (isTablet) {
|
||||
return (
|
||||
<div
|
||||
className="w-16 overflow-y-auto flex flex-col items-center py-4 gap-2 bg-[#1a1a1a]/95 backdrop-blur-xl border-r border-white/5"
|
||||
data-testid="left-panel"
|
||||
>
|
||||
{epicTree.map(({ epic }) => (
|
||||
<div className="w-16 overflow-y-auto flex flex-col items-center py-4 gap-2 bg-[#1a1a1a]/95 backdrop-blur-xl border-r border-white/5">
|
||||
{epicTree.map(({ epic, status }) => (
|
||||
<button
|
||||
key={epic.id}
|
||||
type="button"
|
||||
onClick={() => handleEpicClick(epic.id)}
|
||||
className={cn(
|
||||
'w-10 h-10 rounded-xl flex items-center justify-center text-xs font-bold transition-all duration-200',
|
||||
'w-10 h-10 rounded-xl flex items-center justify-center text-xs font-bold transition-all duration-200 ring-1',
|
||||
selectedEpicId === epic.id
|
||||
? 'bg-teal-500/20 text-teal-400 ring-1 ring-teal-500/30 shadow-[0_0_10px_rgba(45,212,191,0.2)]'
|
||||
: 'hover:bg-white/5 text-text-muted hover:text-white'
|
||||
? 'bg-white/10 ring-white/30 text-white'
|
||||
: 'hover:bg-white/5 ring-transparent text-text-muted',
|
||||
status === 'blocked' && 'ring-rose-500/50',
|
||||
status === 'in_progress' && 'ring-amber-500/50'
|
||||
)}
|
||||
title={epic.title || epic.id}
|
||||
>
|
||||
{epic.id.slice(0, 2).toUpperCase()}
|
||||
</button>
|
||||
|
|
@ -97,103 +143,117 @@ export function LeftPanel({
|
|||
'flex flex-col h-full overflow-hidden transition-all duration-300',
|
||||
!isDesktop && 'hidden lg:flex'
|
||||
)}
|
||||
style={{
|
||||
width: '18rem', // Wider panel
|
||||
}}
|
||||
style={{ width: '20rem' }}
|
||||
data-testid="left-panel"
|
||||
>
|
||||
<div className="h-full bg-[#1a1a1a]/95 backdrop-blur-xl border-r border-white/5 flex flex-col">
|
||||
<div className="h-full bg-[#151515]/95 backdrop-blur-2xl border-r border-white/5 flex flex-col">
|
||||
{/* Header */}
|
||||
<div className="p-4 border-b border-white/5 flex items-center justify-between">
|
||||
<span className="text-xs font-bold uppercase tracking-[0.2em] text-text-muted">Channels</span>
|
||||
<span className="text-[10px] font-mono text-text-muted/40">{epicTree.length}</span>
|
||||
<div className="p-5 border-b border-white/5 flex items-center justify-between bg-white/[0.02]">
|
||||
<span className="text-xs font-bold uppercase tracking-[0.2em] text-text-muted">Workstreams</span>
|
||||
<div className="flex gap-2 text-[10px] font-mono text-text-muted/40">
|
||||
<span>{epicTree.length} ACTIVE</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Tree */}
|
||||
<div className="flex-1 overflow-y-auto custom-scrollbar p-2">
|
||||
{epicTree.map(({ epic, children }) => {
|
||||
<div className="flex-1 overflow-y-auto custom-scrollbar p-3 space-y-3">
|
||||
{epicTree.map(({ epic, children, stats, status }) => {
|
||||
const isExpanded = expandedEpics.has(epic.id);
|
||||
const isSelected = selectedEpicId === epic.id;
|
||||
const childCount = children.length;
|
||||
|
||||
// Dynamic Styling based on Status
|
||||
const statusStyle = {
|
||||
blocked: 'border-rose-500/30 bg-rose-500/5 hover:bg-rose-500/10',
|
||||
in_progress: 'border-amber-500/30 bg-amber-500/5 hover:bg-amber-500/10',
|
||||
ready: 'border-teal-500/30 bg-teal-500/5 hover:bg-teal-500/10',
|
||||
done: 'border-white/5 bg-white/[0.02] opacity-60',
|
||||
empty: 'border-white/5 bg-transparent opacity-40'
|
||||
}[status];
|
||||
|
||||
const activeStyle = isSelected ? 'ring-1 ring-white/20 shadow-lg scale-[1.02]' : '';
|
||||
|
||||
return (
|
||||
<div key={epic.id} className="mb-1">
|
||||
<div key={epic.id} className="group">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleEpicClick(epic.id)}
|
||||
className={cn(
|
||||
'w-full flex items-center gap-3 px-3 py-2.5 rounded-lg text-left transition-all duration-200 group',
|
||||
isSelected
|
||||
? 'bg-teal-500/10 text-teal-400 border border-teal-500/20'
|
||||
: 'hover:bg-white/5 text-text-secondary hover:text-white border border-transparent'
|
||||
'w-full flex flex-col p-3 rounded-xl text-left transition-all duration-300 border relative overflow-hidden',
|
||||
statusStyle,
|
||||
activeStyle
|
||||
)}
|
||||
>
|
||||
{/* Status Bar Indicator */}
|
||||
<div className={cn(
|
||||
"w-1 h-4 rounded-full transition-colors",
|
||||
isSelected ? "bg-teal-500 shadow-[0_0_8px_#14b8a6]" : "bg-white/10 group-hover:bg-white/30"
|
||||
"absolute left-0 top-0 bottom-0 w-1",
|
||||
status === 'blocked' ? 'bg-rose-500' :
|
||||
status === 'in_progress' ? 'bg-amber-500' :
|
||||
status === 'ready' ? 'bg-teal-500' : 'bg-transparent'
|
||||
)} />
|
||||
|
||||
<span className="flex-1 truncate text-sm font-medium tracking-tight">
|
||||
{epic.title || epic.id}
|
||||
</span>
|
||||
|
||||
{childCount > 0 && (
|
||||
<span className={cn(
|
||||
"text-[10px] font-mono px-1.5 py-0.5 rounded transition-colors",
|
||||
isSelected ? "bg-teal-500/20 text-teal-300" : "bg-white/5 text-text-muted group-hover:bg-white/10"
|
||||
)}>
|
||||
{childCount}
|
||||
</span>
|
||||
)}
|
||||
|
||||
<div className="pl-2.5 w-full">
|
||||
<div className="flex items-center justify-between w-full mb-1">
|
||||
<span className="text-[10px] font-mono text-text-muted/70 tracking-tight">{epic.id}</span>
|
||||
{stats.blocked > 0 && (
|
||||
<span className="text-[9px] font-bold text-rose-400 bg-rose-500/10 px-1.5 rounded animate-pulse">
|
||||
{stats.blocked} BLOCKED
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="truncate text-sm font-bold text-white/90 mb-2 leading-snug">
|
||||
{epic.title}
|
||||
</div>
|
||||
|
||||
{/* Progress / Stats Bar */}
|
||||
<div className="flex items-center gap-1 h-1.5 w-full bg-black/20 rounded-full overflow-hidden">
|
||||
<div style={{ width: `${(stats.closed / stats.total) * 100}%` }} className="h-full bg-slate-500/40" />
|
||||
<div style={{ width: `${(stats.in_progress / stats.total) * 100}%` }} className="h-full bg-amber-500" />
|
||||
<div style={{ width: `${(stats.blocked / stats.total) * 100}%` }} className="h-full bg-rose-500" />
|
||||
<div style={{ width: `${(stats.ready / stats.total) * 100}%` }} className="h-full bg-teal-500/60" />
|
||||
</div>
|
||||
|
||||
<div className="flex justify-between mt-1.5 text-[9px] font-mono text-text-muted/50">
|
||||
<span>{Math.round(((stats.closed + stats.in_progress) / (stats.total || 1)) * 100)}% Done</span>
|
||||
<span>{stats.total} Tasks</span>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
{/* Sub-items (Agents/Tasks usually, but here listed as children) */}
|
||||
{isExpanded && childCount > 0 && (
|
||||
<div className="ml-4 mt-1 pl-3 border-l border-white/5 space-y-0.5">
|
||||
{/* Sub-items (Tasks) */}
|
||||
{isExpanded && children.length > 0 && (
|
||||
<div className="ml-4 mt-2 space-y-1 border-l border-white/5 pl-3">
|
||||
{children.slice(0, 5).map(child => (
|
||||
<div
|
||||
key={child.id}
|
||||
className="px-3 py-1.5 text-xs text-text-muted/60 truncate hover:text-text-muted cursor-default"
|
||||
className="px-2 py-1.5 rounded hover:bg-white/5 cursor-pointer flex items-center justify-between group/child transition-colors"
|
||||
>
|
||||
{child.title}
|
||||
<span className="text-[10px] font-mono text-text-muted/60">{child.id}</span>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-[10px] text-text-muted/60 truncate max-w-[80px]">{child.title}</span>
|
||||
<StatusIndicator status={child.status} />
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
{childCount > 5 && (
|
||||
<div className="px-3 py-1 text-[10px] text-text-muted/30 italic">
|
||||
+{childCount - 5} more
|
||||
</div>
|
||||
{children.length > 5 && (
|
||||
<div className="px-2 py-1 text-[9px] text-text-muted/30 italic">+{children.length - 5} more</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
|
||||
{epicTree.length === 0 && (
|
||||
<div className="p-8 text-center opacity-40 flex flex-col items-center">
|
||||
<div className="text-2xl mb-2">📡</div>
|
||||
<p className="text-xs font-mono">NO_CHANNELS</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Footer Scope */}
|
||||
{/* Footer */}
|
||||
<div className="p-4 border-t border-white/5 bg-black/20">
|
||||
<label className="flex items-center gap-3 cursor-pointer group">
|
||||
<div className="relative flex items-center justify-center">
|
||||
<input
|
||||
type="checkbox"
|
||||
defaultChecked
|
||||
onChange={() => onEpicSelect?.(null)} // Clear selection
|
||||
checked={selectedEpicId === null}
|
||||
className="peer appearance-none w-4 h-4 rounded border border-white/20 bg-transparent checked:bg-teal-500 checked:border-teal-500 transition-all"
|
||||
/>
|
||||
<svg className="absolute w-3 h-3 text-white opacity-0 peer-checked:opacity-100 pointer-events-none" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="3" strokeLinecap="round" strokeLinejoin="round">
|
||||
<polyline points="20 6 9 17 4 12"></polyline>
|
||||
</svg>
|
||||
</div>
|
||||
<span className="text-xs font-medium text-text-secondary group-hover:text-white transition-colors">
|
||||
Global Scope (All)
|
||||
<label className="flex items-center gap-3 cursor-pointer group px-2 py-1 rounded hover:bg-white/5 transition-colors">
|
||||
<div className={`w-3 h-3 rounded-full border ${selectedEpicId === null ? 'bg-teal-500 border-teal-500' : 'border-white/20'}`} />
|
||||
<span className={cn(
|
||||
"text-xs font-medium transition-colors",
|
||||
selectedEpicId === null ? "text-teal-400" : "text-text-muted group-hover:text-text-secondary"
|
||||
)}>
|
||||
Global Scope
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
|
|
@ -202,4 +262,4 @@ export function LeftPanel({
|
|||
);
|
||||
}
|
||||
|
||||
export default LeftPanel;
|
||||
export default LeftPanel;
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ export interface UnifiedShellProps {
|
|||
|
||||
export function UnifiedShell({
|
||||
issues,
|
||||
projectScopeOptions,
|
||||
}: UnifiedShellProps) {
|
||||
const { view, taskId, setTaskId, swarmId, setSwarmId, graphTab, setGraphTab, panel, drawer, setDrawer, epicId, setEpicId } = useUrlState();
|
||||
|
||||
|
|
@ -108,6 +109,7 @@ export function UnifiedShell({
|
|||
issues={filteredIssues}
|
||||
selectedId={taskId ?? undefined}
|
||||
onSelect={handleCardSelect}
|
||||
projectScopeOptions={projectScopeOptions}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
@ -134,7 +136,7 @@ export function UnifiedShell({
|
|||
{/* Increased Left Panel width to 18rem per redesign request */}
|
||||
<div
|
||||
className="flex-1 grid overflow-hidden transition-all duration-300"
|
||||
style={{ gridTemplateColumns: `18rem 1fr ${rightPanelWidth}` }}
|
||||
style={{ gridTemplateColumns: `20rem 1fr ${rightPanelWidth}` }}
|
||||
data-testid="main-area"
|
||||
>
|
||||
{/* LEFT PANEL: 18rem channel tree */}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
import { useMemo } from 'react';
|
||||
import type { BeadIssue } from '../../lib/types';
|
||||
import type { ProjectScopeOption } from '../../lib/project-scope';
|
||||
import { buildSocialCards } from '../../lib/social-cards';
|
||||
import { SocialCard } from './social-card';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
|
@ -10,9 +11,10 @@ interface SocialPageProps {
|
|||
issues: BeadIssue[];
|
||||
selectedId?: string;
|
||||
onSelect: (id: string) => void;
|
||||
projectScopeOptions?: ProjectScopeOption[];
|
||||
}
|
||||
|
||||
export function SocialPage({ issues, selectedId, onSelect }: SocialPageProps) {
|
||||
export function SocialPage({ issues, selectedId, onSelect, projectScopeOptions = [] }: SocialPageProps) {
|
||||
const cards = useMemo(() => buildSocialCards(issues), [issues]);
|
||||
|
||||
const selectedTask = useMemo(() =>
|
||||
|
|
@ -23,59 +25,100 @@ export function SocialPage({ issues, selectedId, onSelect }: SocialPageProps) {
|
|||
cards.filter(c => c.id !== selectedId),
|
||||
[cards, selectedId]);
|
||||
|
||||
// Dashboard Metrics
|
||||
const metrics = useMemo(() => {
|
||||
return {
|
||||
blocked: issues.filter(i => i.status === 'blocked'),
|
||||
p0: issues.filter(i => i.priority === 0 && i.status !== 'closed'),
|
||||
active: issues.filter(i => i.status === 'in_progress'),
|
||||
ready: issues.filter(i => i.status === 'open' || i.status === 'ready'),
|
||||
};
|
||||
}, [issues]);
|
||||
|
||||
return (
|
||||
<div className="h-full flex flex-col bg-[#1a1a1a] overflow-hidden">
|
||||
{/* Background Atmosphere */}
|
||||
<div className="absolute inset-0 bg-earthy-gradient opacity-20 pointer-events-none" />
|
||||
|
||||
<div className="flex-1 overflow-y-auto custom-scrollbar relative z-10">
|
||||
<div className="max-w-[1400px] mx-auto p-8 space-y-12">
|
||||
|
||||
{/* STAGE: Featured / Selected Task (Media Player "Now Playing") */}
|
||||
<section className="relative min-h-[400px] flex flex-col justify-center">
|
||||
{/* STAGE AREA */}
|
||||
<section className="relative min-h-[350px] flex flex-col justify-center">
|
||||
{selectedTask ? (
|
||||
<div className="animate-in fade-in zoom-in-95 duration-700 ease-out">
|
||||
<div className="mb-8 flex items-center gap-4">
|
||||
<div className="h-[1px] flex-1 bg-gradient-to-r from-transparent via-teal-500/20 to-transparent" />
|
||||
<div className="flex flex-col items-center">
|
||||
<span className="text-[10px] font-bold uppercase tracking-[0.4em] text-teal-500/60 mb-1">Module Active</span>
|
||||
<div className="w-1.5 h-1.5 rounded-full bg-teal-500 shadow-[0_0_12px_#14b8a6]" />
|
||||
</div>
|
||||
<div className="h-[1px] flex-1 bg-gradient-to-r from-transparent via-teal-500/20 to-transparent" />
|
||||
// FOCUSED TASK MODE
|
||||
<div className="animate-in fade-in zoom-in-95 duration-500 ease-out">
|
||||
<div className="mb-6 flex items-center gap-4 opacity-60">
|
||||
<div className="h-[1px] flex-1 bg-gradient-to-r from-transparent via-teal-500/30 to-transparent" />
|
||||
<span className="text-[10px] font-bold uppercase tracking-[0.3em] text-teal-400">Active Module</span>
|
||||
<div className="h-[1px] flex-1 bg-gradient-to-r from-transparent via-teal-500/30 to-transparent" />
|
||||
</div>
|
||||
<div className="flex justify-center relative">
|
||||
<div className="absolute inset-0 bg-teal-500/5 blur-[100px] rounded-full scale-150" />
|
||||
<div className="flex justify-center">
|
||||
<SocialCard
|
||||
data={selectedTask}
|
||||
selected={true}
|
||||
className="w-full max-w-3xl scale-110 shadow-soft-xl border-teal-500/20 ring-1 ring-teal-500/30 relative z-10"
|
||||
className="w-full max-w-3xl scale-105 shadow-soft-2xl ring-1 ring-teal-500/20"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="group relative py-24 rounded-[4rem] bg-gradient-to-b from-white/[0.03] to-transparent border border-white/5 overflow-hidden flex flex-col items-center justify-center transition-all duration-1000">
|
||||
{/* Holographic Ring Effect */}
|
||||
<div className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-[300px] h-[300px] border border-teal-500/10 rounded-full animate-[spin_10s_linear_infinite]" />
|
||||
<div className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-[260px] h-[260px] border border-dashed border-white/5 rounded-full animate-[spin_15s_linear_infinite_reverse]" />
|
||||
|
||||
<div className="relative z-10 flex flex-col items-center">
|
||||
<div className="w-16 h-16 rounded-3xl bg-white/5 flex items-center justify-center mb-6 ring-1 ring-white/10 group-hover:ring-teal-500/30 transition-all duration-500">
|
||||
<svg width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1" className="text-text-muted/40 group-hover:text-teal-500/60 transition-colors">
|
||||
<path d="M12 2v20M2 12h20M12 2a10 10 0 0 1 10 10M12 2a10 10 0 0 0-10 10M12 22a10 10 0 0 0 10-10M12 22a10 10 0 0 1-10-10"></path>
|
||||
</svg>
|
||||
// DASHBOARD MODE (System Overview)
|
||||
<div className="w-full grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||
{/* 1. Projects Overview */}
|
||||
<div className="md:col-span-2 p-8 rounded-[2rem] bg-white/[0.03] border border-white/5 backdrop-blur-md relative overflow-hidden group hover:border-white/10 transition-colors">
|
||||
<div className="flex items-start justify-between mb-6">
|
||||
<div>
|
||||
<h2 className="text-xl font-bold text-white tracking-tight">System Overview</h2>
|
||||
<p className="text-sm text-text-muted/60 mt-1">Multi-project command scope</p>
|
||||
</div>
|
||||
<div className="px-3 py-1 rounded-full bg-white/5 text-xs font-mono text-text-muted border border-white/5">
|
||||
v2.0
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-wrap gap-3">
|
||||
{projectScopeOptions.slice(0, 5).map(p => (
|
||||
<div key={p.key} className="flex items-center gap-2 px-4 py-2 rounded-xl bg-black/20 border border-white/5 hover:border-teal-500/30 transition-colors cursor-pointer group/pill">
|
||||
<div className="w-2 h-2 rounded-full bg-teal-500/50 group-hover/pill:bg-teal-400" />
|
||||
<span className="text-sm font-medium text-text-secondary group-hover/pill:text-white">{p.name}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 2. Critical Alerts */}
|
||||
<div className="p-8 rounded-[2rem] bg-gradient-to-br from-rose-500/10 to-transparent border border-rose-500/20 backdrop-blur-md flex flex-col relative overflow-hidden">
|
||||
<div className="absolute top-0 right-0 p-4 opacity-20">
|
||||
<svg width="64" height="64" viewBox="0 0 24 24" fill="currentColor" className="text-rose-500"><path d="M12 2L1 21h22M12 6l7.53 13H4.47M11 10v4h2v-4m-2 6v2h2v-2"/></svg>
|
||||
</div>
|
||||
<h3 className="text-sm font-bold uppercase tracking-widest text-rose-400 mb-4">Critical Attention</h3>
|
||||
<div className="space-y-3 flex-1">
|
||||
{metrics.blocked.length > 0 ? (
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="text-4xl font-bold text-white">{metrics.blocked.length}</span>
|
||||
<span className="text-sm text-rose-200/80 leading-tight">Blocked<br/>Modules</span>
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-emerald-400 font-medium">All systems nominal</div>
|
||||
)}
|
||||
{metrics.p0.length > 0 && (
|
||||
<div className="text-xs text-rose-300/60 font-mono mt-2">
|
||||
{metrics.p0.length} P0 Priority Items
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<h2 className="text-xl font-bold text-white/20 tracking-widest uppercase mb-2">Initialize Focus</h2>
|
||||
<p className="text-sm text-text-muted/20 font-mono">STANDBY_MODE // AWAITING_INPUT</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
|
||||
{/* LIBRARY: The Feed (Masonry Grid) */}
|
||||
{/* LIBRARY */}
|
||||
<section className="space-y-6">
|
||||
<div className="flex items-center justify-between px-4">
|
||||
<h3 className="text-sm font-bold uppercase tracking-[0.2em] text-text-muted/60">Module Library</h3>
|
||||
<div className="text-[10px] text-text-muted/40 font-mono">{otherCards.length} Units Available</div>
|
||||
<div className="flex items-center justify-between px-4 pb-2 border-b border-white/5">
|
||||
<h3 className="text-xs font-bold uppercase tracking-[0.2em] text-text-muted/60 py-2">Module Stream</h3>
|
||||
<div className="flex gap-4 text-[10px] font-mono text-text-muted/40">
|
||||
<span>{metrics.active.length} ACTIVE</span>
|
||||
<span>{metrics.ready.length} READY</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 2xl:grid-cols-4 gap-6 pb-20">
|
||||
|
|
@ -85,7 +128,7 @@ export function SocialPage({ issues, selectedId, onSelect }: SocialPageProps) {
|
|||
data={card}
|
||||
selected={false}
|
||||
onClick={() => onSelect(card.id)}
|
||||
className="hover:scale-[1.02] active:scale-[0.98]"
|
||||
className="hover:translate-y-[-4px] transition-transform duration-300"
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ describe('URL State Integration - bb-ui2.22', () => {
|
|||
assert.strictEqual(state.view, 'social');
|
||||
assert.strictEqual(state.taskId, null);
|
||||
assert.strictEqual(state.swarmId, null);
|
||||
assert.strictEqual(state.panel, 'closed');
|
||||
assert.strictEqual(state.panel, 'open');
|
||||
});
|
||||
|
||||
it('/?view=social&task=bb-buff.1&panel=open - task selected, panel open', () => {
|
||||
|
|
@ -137,10 +137,10 @@ describe('URL State Integration - bb-ui2.22', () => {
|
|||
assert.strictEqual(state.graphTab, 'flow');
|
||||
});
|
||||
|
||||
it('/?panel=invalid - invalid panel defaults to closed', () => {
|
||||
it('/?panel=invalid - invalid panel defaults to open', () => {
|
||||
const sp = createMockSearchParams({ panel: 'invalid' });
|
||||
const state = parseUrlState(sp);
|
||||
assert.strictEqual(state.panel, 'closed');
|
||||
assert.strictEqual(state.panel, 'open');
|
||||
});
|
||||
|
||||
it('/?task=invalid-id - invalid task ID still parsed (no validation)', () => {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue