mirror of
https://github.com/openswarm-ai/openswarm.git
synced 2026-09-10 03:37:44 +02:00
[eric] desktop: fullscreen is the arc layout (persistent dock rail, card fills the dashboard, pinned: no drag/collapse/camera until restored)
This commit is contained in:
@@ -203,60 +203,8 @@ const DashboardCanvas: React.FC<DashboardCanvasProps> = ({
|
||||
return () => window.removeEventListener('mousemove', onMove);
|
||||
}, [fullscreenCardId]);
|
||||
|
||||
// Arc/Zen fullscreen: the dock hides with the rest of the chrome but slides back on a left-edge
|
||||
// graze, and clicking a tile SWAPS which card owns the full screen instead of moving the camera.
|
||||
// Hover semantics are a 1:1 port of Zen's compact mode (ZenCompactMode.mjs + compact-mode.yaml):
|
||||
// sidebar-keep-hover 150ms grace after leaving, 1000ms retention when the cursor exits the window
|
||||
// across the dock's screen edge, and an 800ms flash when the active (fullscreen) card switches.
|
||||
const FS_KEEP_HOVER_MS = 150;
|
||||
const FS_WINDOW_LEAVE_MS = 1000;
|
||||
const FS_FLASH_MS = 800;
|
||||
const [fsDockRevealed, setFsDockRevealed] = React.useState(false);
|
||||
const fsDockRef = React.useRef<HTMLDivElement | null>(null);
|
||||
const fsRevealedRef = React.useRef(false);
|
||||
fsRevealedRef.current = fsDockRevealed;
|
||||
useEffect(() => {
|
||||
if (!fullscreenCardId) { setFsDockRevealed(false); return undefined; }
|
||||
let hideTimer: number | null = null;
|
||||
const cancelHide = (): void => { if (hideTimer != null) { window.clearTimeout(hideTimer); hideTimer = null; } };
|
||||
const scheduleHide = (ms: number): void => { cancelHide(); hideTimer = window.setTimeout(() => setFsDockRevealed(false), ms); };
|
||||
const overDock = (x: number, y: number): boolean => {
|
||||
const el = fsDockRef.current;
|
||||
if (!el) return false;
|
||||
const r = el.getBoundingClientRect();
|
||||
return x >= r.left - 8 && x <= r.right + 8 && y >= r.top - 8 && y <= r.bottom + 8;
|
||||
};
|
||||
const onMove = (e: MouseEvent): void => {
|
||||
if (e.clientX <= 16 || overDock(e.clientX, e.clientY)) {
|
||||
cancelHide();
|
||||
if (!fsRevealedRef.current) setFsDockRevealed(true);
|
||||
} else if (fsRevealedRef.current && hideTimer == null) {
|
||||
scheduleHide(FS_KEEP_HOVER_MS);
|
||||
}
|
||||
};
|
||||
// Cursor flung out of the window across the dock's edge: keep the dock up (Zen's screen-edge
|
||||
// retention), then let the next in-window move decide.
|
||||
const onOut = (e: MouseEvent): void => {
|
||||
if (!e.relatedTarget && e.clientX <= 24 && fsRevealedRef.current) scheduleHide(FS_WINDOW_LEAVE_MS);
|
||||
};
|
||||
window.addEventListener('mousemove', onMove);
|
||||
window.addEventListener('mouseout', onOut);
|
||||
return () => { window.removeEventListener('mousemove', onMove); window.removeEventListener('mouseout', onOut); cancelHide(); };
|
||||
}, [fullscreenCardId]);
|
||||
// Zen flashes the sidebar on tab switch in compact mode; swapping the fullscreen card is our tab switch.
|
||||
const prevFsCardRef = React.useRef<string | null>(null);
|
||||
useEffect(() => {
|
||||
const prev = prevFsCardRef.current;
|
||||
prevFsCardRef.current = fullscreenCardId;
|
||||
if (!fullscreenCardId || !prev || prev === fullscreenCardId) return undefined;
|
||||
setFsDockRevealed(true);
|
||||
const t = window.setTimeout(() => {
|
||||
const el = fsDockRef.current;
|
||||
if (el && el.matches(':hover')) return;
|
||||
setFsDockRevealed(false);
|
||||
}, FS_FLASH_MS);
|
||||
return () => window.clearTimeout(t);
|
||||
}, [fullscreenCardId]);
|
||||
// Arc fullscreen layout: the dock rail STAYS PUT (Arc's persistent sidebar) while the card
|
||||
// expands into the whole dashboard beside it; clicking a tile SWAPS which card owns the screen.
|
||||
const swapFullscreen = React.useCallback((cardId: string) => {
|
||||
if (!fullscreenCardId || cardId === fullscreenCardId) return;
|
||||
dispatch(clearTiledCard(fullscreenCardId));
|
||||
@@ -325,16 +273,11 @@ const DashboardCanvas: React.FC<DashboardCanvasProps> = ({
|
||||
/>
|
||||
)}
|
||||
|
||||
{(!fullscreenCardId || fsDockRevealed) && (
|
||||
{(
|
||||
<Box
|
||||
ref={fsDockRef}
|
||||
sx={fullscreenCardId ? {
|
||||
// Explicit width: the dock inside is position:absolute so the wrapper would collapse to
|
||||
// its padding, and the hover-keep hit test would miss the dock entirely.
|
||||
position: 'absolute', left: 0, top: 0, bottom: 0, width: 64, zIndex: 999995,
|
||||
display: 'flex', alignItems: 'center', pl: '4px',
|
||||
animation: 'osw-fsdock-in 160ms ease-out',
|
||||
'@keyframes osw-fsdock-in': { from: { transform: 'translateX(-52px)', opacity: 0 }, to: { transform: 'translateX(0)', opacity: 1 } },
|
||||
} : undefined}
|
||||
>
|
||||
<DesktopDock
|
||||
|
||||
@@ -472,8 +472,10 @@ const AgentCard: React.FC<Props> = ({
|
||||
const justDraggedRef = useRef(false);
|
||||
const lastPointerRef = useRef<{ clientX: number; clientY: number }>({ clientX: 0, clientY: 0 });
|
||||
|
||||
const tileZone = useAppSelector((s) => s.dashboardLayout.tiledCards[session.id]);
|
||||
const handleDragPointerDown = useCallback((e: React.PointerEvent) => {
|
||||
if (e.button !== 0) return;
|
||||
if (tileZone) return;
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
const cs = getCanvasState();
|
||||
@@ -483,7 +485,7 @@ const AgentCard: React.FC<Props> = ({
|
||||
setIsDragging(true);
|
||||
(e.currentTarget as HTMLElement).setPointerCapture(e.pointerId);
|
||||
onDragStart?.(session.id, 'agent');
|
||||
}, [cardX, cardY, onDragStart, session.id, getCanvasState]);
|
||||
}, [cardX, cardY, onDragStart, session.id, getCanvasState, tileZone]);
|
||||
|
||||
const recomputeDragPos = useCallback(() => {
|
||||
const ds = dragState.current;
|
||||
@@ -656,7 +658,6 @@ const AgentCard: React.FC<Props> = ({
|
||||
}
|
||||
};
|
||||
|
||||
const tileZone = useAppSelector((s) => s.dashboardLayout.tiledCards[session.id]);
|
||||
const isFullscreen = tileZone === 'fullscreen';
|
||||
// Fullscreen pins the card to the viewport, so while tiled the geometry must track canvas pan/zoom.
|
||||
// Chat cards read the camera via a getter (not props) to avoid re-rendering on every pan tick, so
|
||||
|
||||
@@ -712,6 +712,7 @@ const BrowserCard: React.FC<Props> = ({
|
||||
|
||||
const handleDragPointerDown = useCallback((e: React.PointerEvent) => {
|
||||
if (e.button !== 0) return;
|
||||
if (tileZone) return;
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
const cs = getCanvasState();
|
||||
@@ -721,7 +722,7 @@ const BrowserCard: React.FC<Props> = ({
|
||||
setIsDragging(true);
|
||||
(e.currentTarget as HTMLElement).setPointerCapture(e.pointerId);
|
||||
onDragStart?.(browserId, 'browser');
|
||||
}, [cardX, cardY, onDragStart, browserId, getCanvasState]);
|
||||
}, [cardX, cardY, onDragStart, browserId, getCanvasState, tileZone]);
|
||||
|
||||
const recomputeDragPos = useCallback(() => {
|
||||
const ds = dragState.current;
|
||||
|
||||
@@ -242,6 +242,7 @@ const DashboardViewCard: React.FC<Props> = ({
|
||||
|
||||
const handleDragPointerDown = useCallback((e: React.PointerEvent) => {
|
||||
if (e.button !== 0) return;
|
||||
if (tileZone) return;
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
const cs = getCanvasState();
|
||||
@@ -251,7 +252,7 @@ const DashboardViewCard: React.FC<Props> = ({
|
||||
setIsDragging(true);
|
||||
(e.currentTarget as HTMLElement).setPointerCapture(e.pointerId);
|
||||
onDragStart?.(cardKey, 'view');
|
||||
}, [cardX, cardY, onDragStart, cardKey, getCanvasState]);
|
||||
}, [cardX, cardY, onDragStart, cardKey, getCanvasState, tileZone]);
|
||||
|
||||
const recomputeDragPos = useCallback(() => {
|
||||
const ds = dragState.current;
|
||||
|
||||
@@ -108,6 +108,7 @@ const NoteCard: React.FC<Props> = ({
|
||||
|
||||
const handleDragPointerDown = useCallback((e: React.PointerEvent) => {
|
||||
if (e.button !== 0) return;
|
||||
if (tileZone) return;
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
const cs = getCanvasState();
|
||||
@@ -121,7 +122,7 @@ const NoteCard: React.FC<Props> = ({
|
||||
setIsDragging(true);
|
||||
(e.currentTarget as HTMLElement).setPointerCapture(e.pointerId);
|
||||
onDragStart?.(noteId, 'note');
|
||||
}, [cardX, cardY, noteId, onDragStart, getCanvasState]);
|
||||
}, [cardX, cardY, noteId, onDragStart, getCanvasState, tileZone]);
|
||||
|
||||
const recomputeDragPos = useCallback(() => {
|
||||
const ds = dragState.current;
|
||||
|
||||
@@ -23,6 +23,8 @@ export const TILE_ZONES: Record<string, { x: number; y: number; w: number; h: nu
|
||||
|
||||
// macOS Sequoia leaves a small gap between tiled windows; we match it.
|
||||
const GAP = 8;
|
||||
// Width the persistent dock rail owns while a card is fullscreen (the Arc sidebar analog).
|
||||
export const FULLSCREEN_DOCK_RAIL = 64;
|
||||
|
||||
export interface TiledStyle {
|
||||
left: number;
|
||||
@@ -46,20 +48,19 @@ function workspaceSize(): { w: number; h: number } {
|
||||
}
|
||||
|
||||
export function computeTiledStyle(zone: string, panX: number, panY: number, zoom: number): TiledStyle | null {
|
||||
// 'fullscreen' = macOS full screen: the app chrome hides (AppShell/DashboardCanvas react to
|
||||
// selectFullscreenCardId) and the card covers the WHOLE window. Target is WINDOW space here, so
|
||||
// the viewport origin does NOT cancel; once the chrome collapses that origin goes to ~0 anyway.
|
||||
// 'fullscreen' = the Arc layout: the app chrome hides except the dock rail, which stays put on
|
||||
// the left, and the card expands into the ENTIRE dashboard beside it, floating on the same gap
|
||||
// the tile zones use. Target is WINDOW space here, so the viewport origin does NOT cancel.
|
||||
if (zone === 'fullscreen') {
|
||||
const PEEK = 0;
|
||||
const el = document.querySelector('[data-canvas-viewport]');
|
||||
const r = el ? el.getBoundingClientRect() : null;
|
||||
const ox = r ? r.left : 0;
|
||||
const oy = r ? r.top : 0;
|
||||
return {
|
||||
left: (PEEK - ox - panX) / zoom,
|
||||
top: (PEEK - oy - panY) / zoom,
|
||||
width: window.innerWidth - PEEK * 2,
|
||||
height: window.innerHeight - PEEK * 2,
|
||||
left: (FULLSCREEN_DOCK_RAIL + GAP - ox - panX) / zoom,
|
||||
top: (GAP - oy - panY) / zoom,
|
||||
width: window.innerWidth - FULLSCREEN_DOCK_RAIL - GAP * 2,
|
||||
height: window.innerHeight - GAP * 2,
|
||||
transform: `scale(${1 / zoom})`,
|
||||
transformOrigin: 'top left',
|
||||
};
|
||||
|
||||
@@ -73,6 +73,10 @@ export function useDashboardInteractions({
|
||||
// The Workflows window is an app you click around inside, not a card you re-center every tap. Single-click only raises + selects it; double-click still zoom-to-fits (handleCardDoubleClick). Without this, clicking any button inside it yanked the canvas into a re-zoom.
|
||||
if (type === 'workflows-hub' || type === 'workflows-monitor') return;
|
||||
|
||||
// A tiled (fullscreen/snapped) card is pinned Arc-style: clicking inside it must not collapse
|
||||
// it or glide the camera; it leaves the mode via its own controls (yellow, Esc, dock swap).
|
||||
if (store.getState().dashboardLayout.tiledCards[id]) return;
|
||||
|
||||
const alreadyExpanded = type === 'agent' && expandedSessionIds.includes(id);
|
||||
|
||||
if (alreadyExpanded) {
|
||||
@@ -206,6 +210,7 @@ export function useDashboardInteractions({
|
||||
}
|
||||
dispatch(bringToFront({ id, type }));
|
||||
setFocusedCardId(id);
|
||||
if (store.getState().dashboardLayout.tiledCards[id]) return;
|
||||
setTimeout(() => {
|
||||
const rect = getCardRect(id, type);
|
||||
if (rect) canvas.actions.fitToCards([rect], 1.15, true);
|
||||
|
||||
Reference in New Issue
Block a user