feat(graph): default to tab mode + fullscreen button in tab
- Graph button opens dedicated tab (was: overlay) - Cmd+Shift+G opens tab (was: toggle overlay) - Tab mode has Fullscreen button → opens overlay on top - Tab actions (Message, Open) dispatch CustomEvents to TeamDetailView which opens corresponding dialogs (SendMessage, TaskDetail) - Overlay still available via Fullscreen button or TeamGraphOverlay
This commit is contained in:
parent
8a9121fc3e
commit
cb17e2158f
4 changed files with 108 additions and 8 deletions
|
|
@ -6,6 +6,7 @@
|
|||
import { useCallback } from 'react';
|
||||
import {
|
||||
Columns3,
|
||||
Expand,
|
||||
Eye,
|
||||
EyeOff,
|
||||
Maximize2,
|
||||
|
|
@ -33,6 +34,7 @@ export interface GraphControlsProps {
|
|||
onZoomToFit: () => void;
|
||||
onRequestClose?: () => void;
|
||||
onRequestPinAsTab?: () => void;
|
||||
onRequestFullscreen?: () => void;
|
||||
teamName: string;
|
||||
teamColor?: string;
|
||||
isAlive?: boolean;
|
||||
|
|
@ -46,6 +48,7 @@ export function GraphControls({
|
|||
onZoomToFit,
|
||||
onRequestClose,
|
||||
onRequestPinAsTab,
|
||||
onRequestFullscreen,
|
||||
teamName,
|
||||
teamColor,
|
||||
isAlive,
|
||||
|
|
@ -133,6 +136,16 @@ export function GraphControls({
|
|||
/>
|
||||
</>
|
||||
)}
|
||||
{onRequestFullscreen && (
|
||||
<>
|
||||
<Separator />
|
||||
<ToolbarButton
|
||||
onClick={onRequestFullscreen}
|
||||
icon={<Expand size={13} />}
|
||||
label="Fullscreen"
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
{onRequestClose && (
|
||||
<ToolbarButton onClick={onRequestClose} icon={<X size={13} />} />
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -31,6 +31,7 @@ export interface GraphViewProps {
|
|||
className?: string;
|
||||
onRequestClose?: () => void;
|
||||
onRequestPinAsTab?: () => void;
|
||||
onRequestFullscreen?: () => void;
|
||||
}
|
||||
|
||||
export function GraphView({
|
||||
|
|
@ -40,6 +41,7 @@ export function GraphView({
|
|||
className,
|
||||
onRequestClose,
|
||||
onRequestPinAsTab,
|
||||
onRequestFullscreen,
|
||||
}: GraphViewProps): React.JSX.Element {
|
||||
// ─── React state (user-facing only) ─────────────────────────────────────
|
||||
const [selectedNodeId, setSelectedNodeId] = useState<string | null>(null);
|
||||
|
|
@ -327,6 +329,7 @@ export function GraphView({
|
|||
}}
|
||||
onRequestClose={onRequestClose}
|
||||
onRequestPinAsTab={onRequestPinAsTab}
|
||||
onRequestFullscreen={onRequestFullscreen}
|
||||
teamName={data.teamName}
|
||||
teamColor={data.teamColor}
|
||||
isAlive={data.isAlive}
|
||||
|
|
|
|||
|
|
@ -214,18 +214,46 @@ export const TeamDetailView = ({ teamName }: TeamDetailViewProps): React.JSX.Ele
|
|||
}
|
||||
}, [editorOpen, graphOpen]);
|
||||
|
||||
// Listen for Cmd+Shift+G keyboard shortcut
|
||||
// Listen for Cmd+Shift+G keyboard shortcut — opens graph tab
|
||||
useEffect(() => {
|
||||
const handler = (e: Event) => {
|
||||
const detail = (e as CustomEvent).detail;
|
||||
if (detail?.teamName === teamName) {
|
||||
setGraphOpen((prev) => !prev);
|
||||
useStore.getState().openTab({
|
||||
type: 'graph',
|
||||
label: `${teamName} Graph`,
|
||||
teamName,
|
||||
});
|
||||
}
|
||||
};
|
||||
window.addEventListener('toggle-team-graph', handler);
|
||||
return () => window.removeEventListener('toggle-team-graph', handler);
|
||||
}, [teamName]);
|
||||
|
||||
// Listen for graph tab actions (open task, send message)
|
||||
useEffect(() => {
|
||||
const onOpenTask = (e: Event) => {
|
||||
const { teamName: tn, taskId } = (e as CustomEvent).detail ?? {};
|
||||
if (tn !== teamName || !data) return;
|
||||
const task = data.tasks.find((t: { id: string }) => t.id === taskId);
|
||||
if (task) setSelectedTask(task);
|
||||
};
|
||||
const onSendMsg = (e: Event) => {
|
||||
const { teamName: tn, memberName } = (e as CustomEvent).detail ?? {};
|
||||
if (tn !== teamName) return;
|
||||
setSendDialogRecipient(memberName);
|
||||
setSendDialogDefaultText(undefined);
|
||||
setSendDialogDefaultChip(undefined);
|
||||
setSendDialogOpen(true);
|
||||
};
|
||||
window.addEventListener('graph:open-task', onOpenTask);
|
||||
window.addEventListener('graph:send-message', onSendMsg);
|
||||
return () => {
|
||||
window.removeEventListener('graph:open-task', onOpenTask);
|
||||
window.removeEventListener('graph:send-message', onSendMsg);
|
||||
};
|
||||
});
|
||||
|
||||
const [sendDialogOpen, setSendDialogOpen] = useState(false);
|
||||
const [deleteConfirmOpen, setDeleteConfirmOpen] = useState(false);
|
||||
const [stoppingTeam, setStoppingTeam] = useState(false);
|
||||
|
|
@ -1432,7 +1460,11 @@ export const TeamDetailView = ({ teamName }: TeamDetailViewProps): React.JSX.Ele
|
|||
className="h-6 gap-1 px-2 text-xs text-[var(--color-text-muted)] hover:text-[var(--color-text)]"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setGraphOpen(true);
|
||||
useStore.getState().openTab({
|
||||
type: 'graph',
|
||||
label: `${data.config.name} Graph`,
|
||||
teamName,
|
||||
});
|
||||
}}
|
||||
>
|
||||
<Network size={12} />
|
||||
|
|
|
|||
|
|
@ -1,8 +1,9 @@
|
|||
/**
|
||||
* TeamGraphTab — wraps GraphView for use as a dedicated tab.
|
||||
* Provides Fullscreen button that opens the overlay.
|
||||
*/
|
||||
|
||||
import { useCallback } from 'react';
|
||||
import { useCallback, useState, lazy, Suspense } from 'react';
|
||||
|
||||
import { GraphView } from '@claude-teams/agent-graph';
|
||||
|
||||
|
|
@ -10,22 +11,73 @@ import { useTeamGraphAdapter } from '../adapters/useTeamGraphAdapter';
|
|||
|
||||
import type { GraphDomainRef, GraphEventPort } from '@claude-teams/agent-graph';
|
||||
|
||||
const TeamGraphOverlay = lazy(() =>
|
||||
import('./TeamGraphOverlay').then((m) => ({ default: m.TeamGraphOverlay }))
|
||||
);
|
||||
|
||||
export interface TeamGraphTabProps {
|
||||
teamName: string;
|
||||
}
|
||||
|
||||
export const TeamGraphTab = ({ teamName }: TeamGraphTabProps): React.JSX.Element => {
|
||||
const graphData = useTeamGraphAdapter(teamName);
|
||||
const [fullscreen, setFullscreen] = useState(false);
|
||||
|
||||
const events: GraphEventPort = {
|
||||
onNodeDoubleClick: useCallback((ref: GraphDomainRef) => {
|
||||
console.log('Double-click in tab:', ref);
|
||||
}, []),
|
||||
onNodeDoubleClick: useCallback(
|
||||
(ref: GraphDomainRef) => {
|
||||
// Dispatch to TeamDetailView's dialog system via CustomEvent
|
||||
if (ref.kind === 'task') {
|
||||
window.dispatchEvent(
|
||||
new CustomEvent('graph:open-task', { detail: { teamName, taskId: ref.taskId } })
|
||||
);
|
||||
} else if (ref.kind === 'member') {
|
||||
window.dispatchEvent(
|
||||
new CustomEvent('graph:send-message', {
|
||||
detail: { teamName, memberName: ref.memberName },
|
||||
})
|
||||
);
|
||||
}
|
||||
},
|
||||
[teamName]
|
||||
),
|
||||
onSendMessage: useCallback(
|
||||
(memberName: string) => {
|
||||
window.dispatchEvent(
|
||||
new CustomEvent('graph:send-message', { detail: { teamName, memberName } })
|
||||
);
|
||||
},
|
||||
[teamName]
|
||||
),
|
||||
onOpenTaskDetail: useCallback(
|
||||
(taskId: string) => {
|
||||
window.dispatchEvent(new CustomEvent('graph:open-task', { detail: { teamName, taskId } }));
|
||||
},
|
||||
[teamName]
|
||||
),
|
||||
onOpenMemberProfile: useCallback(
|
||||
(memberName: string) => {
|
||||
window.dispatchEvent(
|
||||
new CustomEvent('graph:send-message', { detail: { teamName, memberName } })
|
||||
);
|
||||
},
|
||||
[teamName]
|
||||
),
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="size-full" style={{ background: '#050510' }}>
|
||||
<GraphView data={graphData} events={events} className="size-full" />
|
||||
<GraphView
|
||||
data={graphData}
|
||||
events={events}
|
||||
className="size-full"
|
||||
onRequestFullscreen={() => setFullscreen(true)}
|
||||
/>
|
||||
{fullscreen && (
|
||||
<Suspense fallback={null}>
|
||||
<TeamGraphOverlay teamName={teamName} onClose={() => setFullscreen(false)} />
|
||||
</Suspense>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
Loading…
Reference in a new issue