fix(agent-graph): harden pan and launch stepper visibility
This commit is contained in:
parent
82a0e3e6bb
commit
57ba5b57b5
5 changed files with 127 additions and 53 deletions
|
|
@ -266,24 +266,49 @@ function drawLaunchStage(
|
||||||
ctx.save();
|
ctx.save();
|
||||||
switch (visualState) {
|
switch (visualState) {
|
||||||
case 'waiting': {
|
case 'waiting': {
|
||||||
const ringR = r + 7 + Math.sin(time * 3.2) * 1.2;
|
const ringR = r + 8 + Math.sin(time * 3.2) * 1.4;
|
||||||
const pulseAlpha = 0.2 + 0.14 * (0.5 + 0.5 * Math.sin(time * 3.2));
|
const pulseAlpha = 0.28 + 0.18 * (0.5 + 0.5 * Math.sin(time * 3.2));
|
||||||
|
const dotOrbit = r + 11;
|
||||||
ctx.beginPath();
|
ctx.beginPath();
|
||||||
ctx.arc(x, y, ringR, 0, Math.PI * 2);
|
ctx.arc(x, y, ringR, 0, Math.PI * 2);
|
||||||
ctx.strokeStyle = hexWithAlpha('#d4d4d8', pulseAlpha);
|
ctx.strokeStyle = hexWithAlpha('#d4d4d8', pulseAlpha);
|
||||||
ctx.lineWidth = 2.2;
|
ctx.lineWidth = 2.5;
|
||||||
|
ctx.setLineDash([4, 5]);
|
||||||
ctx.stroke();
|
ctx.stroke();
|
||||||
|
ctx.setLineDash([]);
|
||||||
|
for (let index = 0; index < 3; index += 1) {
|
||||||
|
const angle = time * 1.2 + (Math.PI * 2 * index) / 3;
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.arc(x + Math.cos(angle) * dotOrbit, y + Math.sin(angle) * dotOrbit, 1.7, 0, Math.PI * 2);
|
||||||
|
ctx.fillStyle = hexWithAlpha('#e4e4e7', 0.72);
|
||||||
|
ctx.fill();
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case 'spawning': {
|
case 'spawning': {
|
||||||
const ringR = r + 7;
|
const ringR = r + 7;
|
||||||
const rotation = time * 2.4;
|
const rotation = time * 2.7;
|
||||||
ctx.beginPath();
|
ctx.beginPath();
|
||||||
ctx.arc(x, y, ringR, rotation, rotation + Math.PI * 1.15);
|
ctx.arc(x, y, ringR, rotation, rotation + Math.PI * 1.15);
|
||||||
ctx.strokeStyle = hexWithAlpha('#f59e0b', 0.8);
|
ctx.strokeStyle = hexWithAlpha('#f59e0b', 0.8);
|
||||||
ctx.lineWidth = 2.2;
|
ctx.lineWidth = 2.8;
|
||||||
ctx.lineCap = 'round';
|
ctx.lineCap = 'round';
|
||||||
ctx.stroke();
|
ctx.stroke();
|
||||||
|
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.arc(x, y, ringR + 4, rotation + Math.PI, rotation + Math.PI + Math.PI * 0.4);
|
||||||
|
ctx.strokeStyle = hexWithAlpha('#fbbf24', 0.65);
|
||||||
|
ctx.lineWidth = 1.8;
|
||||||
|
ctx.lineCap = 'round';
|
||||||
|
ctx.stroke();
|
||||||
|
|
||||||
|
const glow = ctx.createRadialGradient(x, y, r * 0.5, x, y, ringR + 12);
|
||||||
|
glow.addColorStop(0, hexWithAlpha('#f59e0b', 0.18));
|
||||||
|
glow.addColorStop(1, hexWithAlpha('#f59e0b', 0));
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.arc(x, y, ringR + 12, 0, Math.PI * 2);
|
||||||
|
ctx.fillStyle = glow;
|
||||||
|
ctx.fill();
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case 'runtime_pending': {
|
case 'runtime_pending': {
|
||||||
|
|
@ -291,27 +316,37 @@ function drawLaunchStage(
|
||||||
ctx.beginPath();
|
ctx.beginPath();
|
||||||
ctx.arc(x, y, ringR, 0, Math.PI * 2);
|
ctx.arc(x, y, ringR, 0, Math.PI * 2);
|
||||||
ctx.strokeStyle = hexWithAlpha('#38bdf8', 0.48);
|
ctx.strokeStyle = hexWithAlpha('#38bdf8', 0.48);
|
||||||
ctx.lineWidth = 1.75;
|
ctx.lineWidth = 1.9;
|
||||||
|
ctx.setLineDash([5, 4]);
|
||||||
ctx.stroke();
|
ctx.stroke();
|
||||||
|
ctx.setLineDash([]);
|
||||||
|
|
||||||
const orbit = time * 1.6;
|
const orbit = time * 1.8;
|
||||||
const dotR = 2.2;
|
for (let index = 0; index < 2; index += 1) {
|
||||||
const dotX = x + Math.cos(orbit) * ringR;
|
const angle = orbit + Math.PI * index;
|
||||||
const dotY = y + Math.sin(orbit) * ringR;
|
const dotX = x + Math.cos(angle) * ringR;
|
||||||
ctx.beginPath();
|
const dotY = y + Math.sin(angle) * ringR;
|
||||||
ctx.arc(dotX, dotY, dotR, 0, Math.PI * 2);
|
ctx.beginPath();
|
||||||
ctx.fillStyle = hexWithAlpha('#67e8f9', 0.9);
|
ctx.arc(dotX, dotY, 2.3, 0, Math.PI * 2);
|
||||||
ctx.fill();
|
ctx.fillStyle = hexWithAlpha(index === 0 ? '#67e8f9' : '#38bdf8', 0.92);
|
||||||
|
ctx.fill();
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case 'settling': {
|
case 'settling': {
|
||||||
const ringR = r + 6;
|
const ringR = r + 6;
|
||||||
const arc = 0.65 + 0.08 * Math.sin(time * 2.2);
|
const arc = 0.72 + 0.08 * Math.sin(time * 2.2);
|
||||||
const rotation = time * 1.25;
|
const rotation = time * 1.25;
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.arc(x, y, ringR, 0, Math.PI * 2);
|
||||||
|
ctx.strokeStyle = hexWithAlpha('#22c55e', 0.18);
|
||||||
|
ctx.lineWidth = 1.4;
|
||||||
|
ctx.stroke();
|
||||||
|
|
||||||
ctx.beginPath();
|
ctx.beginPath();
|
||||||
ctx.arc(x, y, ringR, rotation, rotation + Math.PI * arc);
|
ctx.arc(x, y, ringR, rotation, rotation + Math.PI * arc);
|
||||||
ctx.strokeStyle = hexWithAlpha('#22c55e', 0.62);
|
ctx.strokeStyle = hexWithAlpha('#22c55e', 0.62);
|
||||||
ctx.lineWidth = 2;
|
ctx.lineWidth = 2.2;
|
||||||
ctx.lineCap = 'round';
|
ctx.lineCap = 'round';
|
||||||
ctx.stroke();
|
ctx.stroke();
|
||||||
break;
|
break;
|
||||||
|
|
@ -321,9 +356,14 @@ function drawLaunchStage(
|
||||||
ctx.beginPath();
|
ctx.beginPath();
|
||||||
ctx.arc(x, y, ringR, Math.PI * 0.2, Math.PI * 1.15);
|
ctx.arc(x, y, ringR, Math.PI * 0.2, Math.PI * 1.15);
|
||||||
ctx.strokeStyle = hexWithAlpha('#ef4444', 0.72);
|
ctx.strokeStyle = hexWithAlpha('#ef4444', 0.72);
|
||||||
ctx.lineWidth = 2.2;
|
ctx.lineWidth = 2.4;
|
||||||
ctx.lineCap = 'round';
|
ctx.lineCap = 'round';
|
||||||
ctx.stroke();
|
ctx.stroke();
|
||||||
|
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.arc(x + ringR * 0.52, y - ringR * 0.5, 2.2, 0, Math.PI * 2);
|
||||||
|
ctx.fillStyle = hexWithAlpha('#f87171', 0.92);
|
||||||
|
ctx.fill();
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -49,6 +49,7 @@ export interface GraphControlsProps {
|
||||||
teamColor?: string;
|
teamColor?: string;
|
||||||
isAlive?: boolean;
|
isAlive?: boolean;
|
||||||
topToolbarContent?: React.ReactNode;
|
topToolbarContent?: React.ReactNode;
|
||||||
|
interactionLocked?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const TOPBAR_BUTTON_SIZE = 25;
|
const TOPBAR_BUTTON_SIZE = 25;
|
||||||
|
|
@ -69,6 +70,7 @@ export function GraphControls({
|
||||||
isSidebarVisible = true,
|
isSidebarVisible = true,
|
||||||
teamColor,
|
teamColor,
|
||||||
topToolbarContent,
|
topToolbarContent,
|
||||||
|
interactionLocked = false,
|
||||||
}: GraphControlsProps): React.JSX.Element {
|
}: GraphControlsProps): React.JSX.Element {
|
||||||
const [isSettingsOpen, setIsSettingsOpen] = useState(false);
|
const [isSettingsOpen, setIsSettingsOpen] = useState(false);
|
||||||
const settingsRef = useRef<HTMLDivElement>(null);
|
const settingsRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
@ -104,6 +106,9 @@ export function GraphControls({
|
||||||
}, [isSettingsOpen]);
|
}, [isSettingsOpen]);
|
||||||
|
|
||||||
const nameColor = teamColor ?? '#aaeeff';
|
const nameColor = teamColor ?? '#aaeeff';
|
||||||
|
const chromeInteractivityClass = interactionLocked
|
||||||
|
? 'pointer-events-none select-none'
|
||||||
|
: 'pointer-events-auto';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
|
@ -111,7 +116,7 @@ export function GraphControls({
|
||||||
<div className="absolute left-0 top-0 flex shrink-0 items-center gap-0.5">
|
<div className="absolute left-0 top-0 flex shrink-0 items-center gap-0.5">
|
||||||
{onToggleSidebar ? (
|
{onToggleSidebar ? (
|
||||||
<div
|
<div
|
||||||
className="pointer-events-auto flex items-center rounded-md p-0 backdrop-blur-sm"
|
className={`${chromeInteractivityClass} flex items-center rounded-md p-0 backdrop-blur-sm`}
|
||||||
style={{
|
style={{
|
||||||
background: 'rgba(8, 12, 24, 0.8)',
|
background: 'rgba(8, 12, 24, 0.8)',
|
||||||
border: `1px solid ${nameColor}25`,
|
border: `1px solid ${nameColor}25`,
|
||||||
|
|
@ -133,7 +138,7 @@ export function GraphControls({
|
||||||
) : null}
|
) : null}
|
||||||
{onOpenTeamPage ? (
|
{onOpenTeamPage ? (
|
||||||
<div
|
<div
|
||||||
className="pointer-events-auto flex items-center rounded-md p-0 backdrop-blur-sm"
|
className={`${chromeInteractivityClass} flex items-center rounded-md p-0 backdrop-blur-sm`}
|
||||||
style={{
|
style={{
|
||||||
background: 'rgba(8, 12, 24, 0.8)',
|
background: 'rgba(8, 12, 24, 0.8)',
|
||||||
border: `1px solid ${nameColor}25`,
|
border: `1px solid ${nameColor}25`,
|
||||||
|
|
@ -149,7 +154,7 @@ export function GraphControls({
|
||||||
) : null}
|
) : null}
|
||||||
{onCreateTask ? (
|
{onCreateTask ? (
|
||||||
<div
|
<div
|
||||||
className="pointer-events-auto flex items-center rounded-md p-0 backdrop-blur-sm"
|
className={`${chromeInteractivityClass} flex items-center rounded-md p-0 backdrop-blur-sm`}
|
||||||
style={{
|
style={{
|
||||||
background: 'rgba(8, 12, 24, 0.8)',
|
background: 'rgba(8, 12, 24, 0.8)',
|
||||||
border: `1px solid ${nameColor}25`,
|
border: `1px solid ${nameColor}25`,
|
||||||
|
|
@ -167,7 +172,7 @@ export function GraphControls({
|
||||||
|
|
||||||
<div className="absolute left-1/2 top-0 w-[min(360px,38vw)] -translate-x-1/2 px-2">
|
<div className="absolute left-1/2 top-0 w-[min(360px,38vw)] -translate-x-1/2 px-2">
|
||||||
{topToolbarContent ? (
|
{topToolbarContent ? (
|
||||||
<div className="pointer-events-auto min-w-0">
|
<div className={`${chromeInteractivityClass} min-w-0`}>
|
||||||
{topToolbarContent}
|
{topToolbarContent}
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
@ -175,7 +180,7 @@ export function GraphControls({
|
||||||
|
|
||||||
<div className="absolute right-0 top-0 flex shrink-0 items-center gap-0.5">
|
<div className="absolute right-0 top-0 flex shrink-0 items-center gap-0.5">
|
||||||
<div
|
<div
|
||||||
className="pointer-events-auto flex items-center rounded-md p-0 backdrop-blur-sm"
|
className={`${chromeInteractivityClass} flex items-center rounded-md p-0 backdrop-blur-sm`}
|
||||||
style={{
|
style={{
|
||||||
background: 'rgba(8, 12, 24, 0.8)',
|
background: 'rgba(8, 12, 24, 0.8)',
|
||||||
border: '1px solid rgba(100, 200, 255, 0.08)',
|
border: '1px solid rgba(100, 200, 255, 0.08)',
|
||||||
|
|
@ -189,7 +194,7 @@ export function GraphControls({
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div ref={settingsRef} className="relative pointer-events-auto">
|
<div ref={settingsRef} className={`relative ${chromeInteractivityClass}`}>
|
||||||
<div
|
<div
|
||||||
className="flex items-center gap-0.5 rounded-md p-0 backdrop-blur-sm"
|
className="flex items-center gap-0.5 rounded-md p-0 backdrop-blur-sm"
|
||||||
style={{
|
style={{
|
||||||
|
|
@ -240,7 +245,7 @@ export function GraphControls({
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
className="pointer-events-auto flex items-center gap-0.5 rounded-md p-0 backdrop-blur-sm"
|
className={`${chromeInteractivityClass} flex items-center gap-0.5 rounded-md p-0 backdrop-blur-sm`}
|
||||||
style={{
|
style={{
|
||||||
background: 'rgba(8, 12, 24, 0.8)',
|
background: 'rgba(8, 12, 24, 0.8)',
|
||||||
border: '1px solid rgba(100, 200, 255, 0.08)',
|
border: '1px solid rgba(100, 200, 255, 0.08)',
|
||||||
|
|
|
||||||
|
|
@ -104,6 +104,7 @@ export function GraphView({
|
||||||
// ─── React state (user-facing only) ─────────────────────────────────────
|
// ─── React state (user-facing only) ─────────────────────────────────────
|
||||||
const [selectedNodeId, setSelectedNodeId] = useState<string | null>(null);
|
const [selectedNodeId, setSelectedNodeId] = useState<string | null>(null);
|
||||||
const [selectedEdgeId, setSelectedEdgeId] = useState<string | null>(null);
|
const [selectedEdgeId, setSelectedEdgeId] = useState<string | null>(null);
|
||||||
|
const [interactionLocked, setInteractionLocked] = useState(false);
|
||||||
const [filters, setFilters] = useState<GraphFilterState>({
|
const [filters, setFilters] = useState<GraphFilterState>({
|
||||||
showTasks: config?.showTasks ?? true,
|
showTasks: config?.showTasks ?? true,
|
||||||
showProcesses: config?.showProcesses ?? true,
|
showProcesses: config?.showProcesses ?? true,
|
||||||
|
|
@ -136,6 +137,7 @@ export function GraphView({
|
||||||
color?: string | null;
|
color?: string | null;
|
||||||
} | null>(null);
|
} | null>(null);
|
||||||
const selectionLockRef = useRef<{ userSelect: string; webkitUserSelect: string } | null>(null);
|
const selectionLockRef = useRef<{ userSelect: string; webkitUserSelect: string } | null>(null);
|
||||||
|
const activePrimaryInteractionRef = useRef(false);
|
||||||
|
|
||||||
// ─── Hooks ──────────────────────────────────────────────────────────────
|
// ─── Hooks ──────────────────────────────────────────────────────────────
|
||||||
const simulation = useGraphSimulation();
|
const simulation = useGraphSimulation();
|
||||||
|
|
@ -280,6 +282,15 @@ export function GraphView({
|
||||||
selectionLockRef.current = null;
|
selectionLockRef.current = null;
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
const setInteractionGuards = useCallback(
|
||||||
|
(active: boolean) => {
|
||||||
|
activePrimaryInteractionRef.current = active;
|
||||||
|
setInteractionLocked(active);
|
||||||
|
setInteractionSelectionDisabled(active);
|
||||||
|
},
|
||||||
|
[setInteractionSelectionDisabled]
|
||||||
|
);
|
||||||
|
|
||||||
const animate = useCallback(() => {
|
const animate = useCallback(() => {
|
||||||
if (!runningRef.current) return;
|
if (!runningRef.current) return;
|
||||||
|
|
||||||
|
|
@ -413,6 +424,7 @@ export function GraphView({
|
||||||
dragPreviewRef.current = null;
|
dragPreviewRef.current = null;
|
||||||
isPanningRef.current = false;
|
isPanningRef.current = false;
|
||||||
edgeMouseDownRef.current = null;
|
edgeMouseDownRef.current = null;
|
||||||
|
setInteractionGuards(false);
|
||||||
}, [interaction, isSurfaceActive, simulation]);
|
}, [interaction, isSurfaceActive, simulation]);
|
||||||
|
|
||||||
const handleWheel = useCallback(
|
const handleWheel = useCallback(
|
||||||
|
|
@ -432,11 +444,11 @@ export function GraphView({
|
||||||
if (e.button !== 0) return; // only left click
|
if (e.button !== 0) return; // only left click
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
dragPreviewRef.current = null;
|
dragPreviewRef.current = null;
|
||||||
setInteractionSelectionDisabled(true);
|
setInteractionGuards(true);
|
||||||
|
|
||||||
const canvas = canvasHandle.current?.getCanvas();
|
const canvas = canvasHandle.current?.getCanvas();
|
||||||
if (!canvas) {
|
if (!canvas) {
|
||||||
setInteractionSelectionDisabled(false);
|
setInteractionGuards(false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const rect = canvas.getBoundingClientRect();
|
const rect = canvas.getBoundingClientRect();
|
||||||
|
|
@ -482,13 +494,14 @@ export function GraphView({
|
||||||
getVisibleNodes,
|
getVisibleNodes,
|
||||||
interaction,
|
interaction,
|
||||||
markUserInteracted,
|
markUserInteracted,
|
||||||
|
setInteractionGuards,
|
||||||
simulation.stateRef,
|
simulation.stateRef,
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
const processActivePointerMove = useCallback(
|
const processActivePointerMove = useCallback(
|
||||||
(clientX: number, clientY: number, buttons: number) => {
|
(clientX: number, clientY: number) => {
|
||||||
if ((buttons & 1) === 0) {
|
if (!activePrimaryInteractionRef.current) {
|
||||||
dragPreviewRef.current = null;
|
dragPreviewRef.current = null;
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
@ -545,7 +558,7 @@ export function GraphView({
|
||||||
if (isPanningRef.current) {
|
if (isPanningRef.current) {
|
||||||
camera.handlePanEnd();
|
camera.handlePanEnd();
|
||||||
isPanningRef.current = false;
|
isPanningRef.current = false;
|
||||||
setInteractionSelectionDisabled(false);
|
setInteractionGuards(false);
|
||||||
dragPreviewRef.current = null;
|
dragPreviewRef.current = null;
|
||||||
setSelectedNodeId(null);
|
setSelectedNodeId(null);
|
||||||
setSelectedEdgeId(null);
|
setSelectedEdgeId(null);
|
||||||
|
|
@ -556,7 +569,7 @@ export function GraphView({
|
||||||
|
|
||||||
const clickedId = interaction.handleMouseUp();
|
const clickedId = interaction.handleMouseUp();
|
||||||
if (wasDragging && draggedNodeId) {
|
if (wasDragging && draggedNodeId) {
|
||||||
setInteractionSelectionDisabled(false);
|
setInteractionGuards(false);
|
||||||
const draggedNode = simulation.stateRef.current.nodes.find((node) => node.id === draggedNodeId);
|
const draggedNode = simulation.stateRef.current.nodes.find((node) => node.id === draggedNodeId);
|
||||||
if (draggedNode?.kind === 'member' && draggedNode.x != null && draggedNode.y != null) {
|
if (draggedNode?.kind === 'member' && draggedNode.x != null && draggedNode.y != null) {
|
||||||
const nearest = simulation.resolveNearestOwnerSlot(
|
const nearest = simulation.resolveNearestOwnerSlot(
|
||||||
|
|
@ -585,7 +598,7 @@ export function GraphView({
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
setInteractionSelectionDisabled(false);
|
setInteractionGuards(false);
|
||||||
if (clickedId) {
|
if (clickedId) {
|
||||||
setSelectedNodeId(clickedId);
|
setSelectedNodeId(clickedId);
|
||||||
setSelectedEdgeId(null);
|
setSelectedEdgeId(null);
|
||||||
|
|
@ -624,12 +637,12 @@ export function GraphView({
|
||||||
}
|
}
|
||||||
dragPreviewRef.current = null;
|
dragPreviewRef.current = null;
|
||||||
},
|
},
|
||||||
[camera, events, interaction, onOwnerSlotDrop, setInteractionSelectionDisabled, simulation]
|
[camera, events, interaction, onOwnerSlotDrop, setInteractionGuards, simulation]
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleMouseMove = useCallback(
|
const handleMouseMove = useCallback(
|
||||||
(e: React.MouseEvent) => {
|
(e: React.MouseEvent) => {
|
||||||
if (processActivePointerMove(e.clientX, e.clientY, e.buttons)) {
|
if (processActivePointerMove(e.clientX, e.clientY)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -678,11 +691,8 @@ export function GraphView({
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleWindowMouseMove = (event: MouseEvent): void => {
|
const handleWindowMouseMove = (event: MouseEvent): void => {
|
||||||
if ((event.buttons & 1) === 0) {
|
|
||||||
setInteractionSelectionDisabled(false);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (
|
if (
|
||||||
|
!activePrimaryInteractionRef.current &&
|
||||||
!isPanningRef.current &&
|
!isPanningRef.current &&
|
||||||
!interaction.dragNodeId.current &&
|
!interaction.dragNodeId.current &&
|
||||||
!interaction.isDragging.current &&
|
!interaction.isDragging.current &&
|
||||||
|
|
@ -691,30 +701,47 @@ export function GraphView({
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
processActivePointerMove(event.clientX, event.clientY, event.buttons);
|
processActivePointerMove(event.clientX, event.clientY);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleWindowMouseUp = (event: MouseEvent): void => {
|
const handleWindowMouseUp = (event: MouseEvent): void => {
|
||||||
if (
|
if (
|
||||||
|
!activePrimaryInteractionRef.current &&
|
||||||
!isPanningRef.current &&
|
!isPanningRef.current &&
|
||||||
!interaction.dragNodeId.current &&
|
!interaction.dragNodeId.current &&
|
||||||
!interaction.isDragging.current &&
|
!interaction.isDragging.current &&
|
||||||
!edgeMouseDownRef.current
|
!edgeMouseDownRef.current
|
||||||
) {
|
) {
|
||||||
setInteractionSelectionDisabled(false);
|
setInteractionGuards(false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
completePointerInteraction(event.clientX, event.clientY);
|
completePointerInteraction(event.clientX, event.clientY);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const clearInteraction = (): void => {
|
||||||
|
if (!activePrimaryInteractionRef.current && !isPanningRef.current && !interaction.isDragging.current) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
interaction.handleMouseUp();
|
||||||
|
camera.handlePanEnd();
|
||||||
|
isPanningRef.current = false;
|
||||||
|
edgeMouseDownRef.current = null;
|
||||||
|
dragPreviewRef.current = null;
|
||||||
|
setInteractionGuards(false);
|
||||||
|
};
|
||||||
|
|
||||||
window.addEventListener('mousemove', handleWindowMouseMove);
|
window.addEventListener('mousemove', handleWindowMouseMove);
|
||||||
window.addEventListener('mouseup', handleWindowMouseUp);
|
window.addEventListener('mouseup', handleWindowMouseUp);
|
||||||
|
window.addEventListener('blur', clearInteraction);
|
||||||
|
window.addEventListener('dragstart', clearInteraction);
|
||||||
return () => {
|
return () => {
|
||||||
window.removeEventListener('mousemove', handleWindowMouseMove);
|
window.removeEventListener('mousemove', handleWindowMouseMove);
|
||||||
window.removeEventListener('mouseup', handleWindowMouseUp);
|
window.removeEventListener('mouseup', handleWindowMouseUp);
|
||||||
setInteractionSelectionDisabled(false);
|
window.removeEventListener('blur', clearInteraction);
|
||||||
|
window.removeEventListener('dragstart', clearInteraction);
|
||||||
|
setInteractionGuards(false);
|
||||||
};
|
};
|
||||||
}, [completePointerInteraction, interaction, processActivePointerMove, setInteractionSelectionDisabled]);
|
}, [camera, completePointerInteraction, interaction, processActivePointerMove, setInteractionGuards]);
|
||||||
|
|
||||||
const handleDoubleClick = useCallback(
|
const handleDoubleClick = useCallback(
|
||||||
(e: React.MouseEvent) => {
|
(e: React.MouseEvent) => {
|
||||||
|
|
@ -911,6 +938,7 @@ export function GraphView({
|
||||||
teamColor={data.teamColor}
|
teamColor={data.teamColor}
|
||||||
isAlive={data.isAlive}
|
isAlive={data.isAlive}
|
||||||
topToolbarContent={renderTopToolbarContent?.()}
|
topToolbarContent={renderTopToolbarContent?.()}
|
||||||
|
interactionLocked={interactionLocked}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{renderHud ? (
|
{renderHud ? (
|
||||||
|
|
|
||||||
|
|
@ -34,7 +34,7 @@ const HUD_STEPPER_STYLE: CSSProperties = {
|
||||||
};
|
};
|
||||||
|
|
||||||
function shouldRenderLaunchHud(presentation: TeamProvisioningPresentation | null): boolean {
|
function shouldRenderLaunchHud(presentation: TeamProvisioningPresentation | null): boolean {
|
||||||
return presentation != null;
|
return presentation != null && (presentation.isActive || presentation.isFailed);
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface GraphProvisioningHudProps {
|
export interface GraphProvisioningHudProps {
|
||||||
|
|
@ -80,8 +80,10 @@ export const GraphProvisioningHud = ({
|
||||||
<>
|
<>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="focus-visible:ring-white/18 w-full rounded-xl bg-transparent px-1 py-0.5 text-left text-slate-100 transition-opacity hover:opacity-95 focus-visible:outline-none focus-visible:ring-1"
|
className="focus-visible:ring-white/18 w-full touch-none select-none rounded-xl bg-transparent px-1 py-0.5 text-left text-slate-100 transition-opacity hover:opacity-95 focus-visible:outline-none focus-visible:ring-1"
|
||||||
onClick={() => setDetailsOpen(true)}
|
onClick={() => setDetailsOpen(true)}
|
||||||
|
onMouseDown={(event) => event.preventDefault()}
|
||||||
|
onDragStart={(event) => event.preventDefault()}
|
||||||
aria-label={ariaLabel}
|
aria-label={ariaLabel}
|
||||||
>
|
>
|
||||||
<div className="px-1 py-0.5" style={HUD_STEPPER_STYLE}>
|
<div className="px-1 py-0.5" style={HUD_STEPPER_STYLE}>
|
||||||
|
|
|
||||||
|
|
@ -53,7 +53,7 @@ describe('GraphProvisioningHud', () => {
|
||||||
hookState.runInstanceKey = 'team:run-1:2026-04-13T10:00:00.000Z';
|
hookState.runInstanceKey = 'team:run-1:2026-04-13T10:00:00.000Z';
|
||||||
});
|
});
|
||||||
|
|
||||||
it('keeps successful ready launch summary visible until dismissed', async () => {
|
it('hides the graph launch hud once provisioning is ready', async () => {
|
||||||
vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true);
|
vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true);
|
||||||
hookState.presentation = {
|
hookState.presentation = {
|
||||||
isActive: false,
|
isActive: false,
|
||||||
|
|
@ -80,9 +80,8 @@ describe('GraphProvisioningHud', () => {
|
||||||
await Promise.resolve();
|
await Promise.resolve();
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(host.textContent).toContain('Team launched');
|
expect(host.textContent).toBe('');
|
||||||
expect(host.textContent).toContain('All 3 teammates joined');
|
expect(host.querySelector('[data-testid="stepper"]')).toBeNull();
|
||||||
expect(host.querySelector('[data-testid="stepper"]')).not.toBeNull();
|
|
||||||
expect(document.body.textContent).not.toContain('provisioning-panel');
|
expect(document.body.textContent).not.toContain('provisioning-panel');
|
||||||
|
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
|
|
@ -94,14 +93,14 @@ describe('GraphProvisioningHud', () => {
|
||||||
it('opens launch details in a separate dialog when the stepper is clicked', async () => {
|
it('opens launch details in a separate dialog when the stepper is clicked', async () => {
|
||||||
vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true);
|
vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true);
|
||||||
hookState.presentation = {
|
hookState.presentation = {
|
||||||
isActive: false,
|
isActive: true,
|
||||||
isFailed: false,
|
isFailed: false,
|
||||||
hasMembersStillJoining: false,
|
hasMembersStillJoining: true,
|
||||||
failedSpawnCount: 0,
|
failedSpawnCount: 0,
|
||||||
compactTone: 'success',
|
compactTone: 'default',
|
||||||
compactTitle: 'Team launched',
|
compactTitle: 'Launching team',
|
||||||
compactDetail: 'All 3 teammates joined',
|
compactDetail: '1 teammate still joining',
|
||||||
currentStepIndex: 4,
|
currentStepIndex: 2,
|
||||||
progress: { runId: 'run-3' },
|
progress: { runId: 'run-3' },
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue