2026-02-15 23:12:16 -08:00
|
|
|
'use client';
|
|
|
|
|
|
2026-02-16 00:26:31 -08:00
|
|
|
import { useMemo } from 'react';
|
2026-02-15 23:12:16 -08:00
|
|
|
import type { BeadIssue } from '../../lib/types';
|
|
|
|
|
import type { ProjectScopeOption } from '../../lib/project-scope';
|
|
|
|
|
import { TopBar } from './top-bar';
|
|
|
|
|
import { LeftPanel } from './left-panel';
|
|
|
|
|
import { RightPanel } from './right-panel';
|
|
|
|
|
import { MobileNav } from './mobile-nav';
|
2026-02-16 10:16:33 -08:00
|
|
|
import { ThreadDrawer } from './thread-drawer';
|
2026-02-15 23:12:16 -08:00
|
|
|
import { useUrlState } from '../../hooks/use-url-state';
|
|
|
|
|
import { GraphView } from '../graph/graph-view';
|
2026-02-16 00:26:31 -08:00
|
|
|
import { SocialPage } from '../social/social-page';
|
|
|
|
|
import { SwarmPage } from '../swarm/swarm-page';
|
|
|
|
|
import { buildSocialCards } from '../../lib/social-cards';
|
|
|
|
|
import { buildSwarmCards } from '../../lib/swarm-cards';
|
2026-02-15 23:12:16 -08:00
|
|
|
|
|
|
|
|
export interface UnifiedShellProps {
|
|
|
|
|
issues: BeadIssue[];
|
|
|
|
|
projectRoot: string;
|
|
|
|
|
projectScopeKey: string;
|
|
|
|
|
projectScopeOptions: ProjectScopeOption[];
|
|
|
|
|
projectScopeMode: 'single' | 'aggregate';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function UnifiedShell({
|
|
|
|
|
issues,
|
|
|
|
|
}: UnifiedShellProps) {
|
2026-02-16 10:16:33 -08:00
|
|
|
const { view, taskId, setTaskId, swarmId, setSwarmId, graphTab, setGraphTab, panel, drawer, setDrawer } = useUrlState();
|
2026-02-16 00:26:31 -08:00
|
|
|
|
|
|
|
|
const socialCards = useMemo(() => buildSocialCards(issues), [issues]);
|
|
|
|
|
const swarmCards = useMemo(() => buildSwarmCards(issues), [issues]);
|
|
|
|
|
|
|
|
|
|
const selectedSocialCard = taskId ? socialCards.find(c => c.id === taskId) : null;
|
|
|
|
|
const selectedSwarmCard = swarmId ? swarmCards.find(c => c.swarmId === swarmId) : null;
|
2026-02-15 23:12:16 -08:00
|
|
|
|
|
|
|
|
const handleGraphSelect = (id: string) => {
|
|
|
|
|
setTaskId(id);
|
|
|
|
|
};
|
|
|
|
|
|
2026-02-16 10:16:33 -08:00
|
|
|
const handleCardSelect = (id: string) => {
|
|
|
|
|
if (view === 'social') {
|
|
|
|
|
setTaskId(id);
|
|
|
|
|
} else if (view === 'swarm') {
|
|
|
|
|
setSwarmId(id);
|
2026-02-16 00:26:31 -08:00
|
|
|
}
|
2026-02-16 10:16:33 -08:00
|
|
|
setDrawer('open');
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const handleCloseDrawer = () => {
|
|
|
|
|
setDrawer('closed');
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// Thread drawer - shows when card selected
|
|
|
|
|
const isDrawerOpen = drawer === 'open' && (!!taskId || !!swarmId);
|
|
|
|
|
const drawerTitle = selectedSocialCard?.title || selectedSwarmCard?.title || '';
|
|
|
|
|
const drawerId = taskId || swarmId || '';
|
|
|
|
|
|
|
|
|
|
const renderRightPanel = () => {
|
|
|
|
|
// TODO: Wire up ActivityPanel (bb-ui2.29) - for now show placeholder
|
|
|
|
|
return (
|
|
|
|
|
<div className="p-4 text-center text-text-muted text-sm">
|
|
|
|
|
Activity Panel coming
|
|
|
|
|
<br />
|
|
|
|
|
<span className="text-xs">(bb-ui2.29)</span>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
2026-02-16 00:26:31 -08:00
|
|
|
};
|
|
|
|
|
|
2026-02-15 23:12:16 -08:00
|
|
|
const renderMiddleContent = () => {
|
|
|
|
|
if (view === 'graph') {
|
|
|
|
|
return (
|
|
|
|
|
<GraphView
|
|
|
|
|
beads={issues}
|
|
|
|
|
selectedId={taskId ?? undefined}
|
|
|
|
|
onSelect={handleGraphSelect}
|
|
|
|
|
graphTab={graphTab}
|
|
|
|
|
onGraphTabChange={setGraphTab}
|
|
|
|
|
hideClosed={false}
|
|
|
|
|
/>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-16 00:26:31 -08:00
|
|
|
if (view === 'social') {
|
|
|
|
|
return (
|
|
|
|
|
<SocialPage
|
|
|
|
|
issues={issues}
|
|
|
|
|
selectedId={taskId ?? undefined}
|
|
|
|
|
onSelect={setTaskId}
|
|
|
|
|
/>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (view === 'swarm') {
|
|
|
|
|
return (
|
|
|
|
|
<SwarmPage
|
|
|
|
|
issues={issues}
|
|
|
|
|
selectedId={swarmId ?? undefined}
|
|
|
|
|
onSelect={setSwarmId}
|
|
|
|
|
/>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return null;
|
2026-02-15 23:12:16 -08:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div className="flex flex-col h-screen" style={{ backgroundColor: 'var(--color-bg-base)' }} data-testid="unified-shell">
|
|
|
|
|
{/* TOP BAR: 3rem fixed */}
|
|
|
|
|
<TopBar />
|
|
|
|
|
|
|
|
|
|
{/* MAIN AREA: CSS Grid [13rem | 1fr | 17rem] */}
|
|
|
|
|
<div
|
|
|
|
|
className="flex-1 grid overflow-hidden"
|
|
|
|
|
style={{ gridTemplateColumns: '13rem 1fr 17rem' }}
|
|
|
|
|
data-testid="main-area"
|
|
|
|
|
>
|
|
|
|
|
{/* LEFT PANEL: 13rem channel tree */}
|
|
|
|
|
<LeftPanel issues={issues} />
|
|
|
|
|
|
|
|
|
|
{/* MIDDLE CONTENT: flex-1 */}
|
|
|
|
|
<div className="overflow-y-auto" data-testid="middle-content">
|
|
|
|
|
{renderMiddleContent()}
|
|
|
|
|
</div>
|
|
|
|
|
|
2026-02-16 10:16:33 -08:00
|
|
|
{/* RIGHT PANEL: 17rem - Always shows Activity (bb-ui2.29) */}
|
2026-02-16 00:26:31 -08:00
|
|
|
<RightPanel isOpen={panel === 'open'}>
|
|
|
|
|
{renderRightPanel()}
|
|
|
|
|
</RightPanel>
|
2026-02-15 23:12:16 -08:00
|
|
|
</div>
|
|
|
|
|
|
2026-02-16 10:16:33 -08:00
|
|
|
{/* THREAD DRAWER: 24rem - Slides from right edge of middle when card selected */}
|
|
|
|
|
<ThreadDrawer
|
|
|
|
|
isOpen={isDrawerOpen}
|
|
|
|
|
onClose={handleCloseDrawer}
|
|
|
|
|
title={drawerTitle}
|
|
|
|
|
id={drawerId}
|
|
|
|
|
/>
|
|
|
|
|
|
2026-02-15 23:12:16 -08:00
|
|
|
{/* MOBILE NAV: Bottom tab bar */}
|
|
|
|
|
<MobileNav />
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|