mirror of
https://github.com/openswarm-ai/openswarm.git
synced 2026-08-28 10:49:46 +02:00
[eric] canvas: tiled cards restyle in the same task as every camera write (zoom jitter gone); chat header gets a hover runway so the pop-above handle is actually grabbable
This commit is contained in:
@@ -641,7 +641,7 @@ const AgentCard: React.FC<Props> = ({
|
||||
}, [tileZone]);
|
||||
void tileTick;
|
||||
const cam = getCanvasState();
|
||||
const tiledStyle = useTiledStyle(tileZone, cam.panX, cam.panY, cam.zoom);
|
||||
const tiledStyle = useTiledStyle(tileZone, cam.panX, cam.panY, cam.zoom, getCanvasState, session.id);
|
||||
// A collapsed chat can never stay tiled: collapsing while fullscreen left a white full-window shell
|
||||
// (the header collapse control still fires in full size view). Seal the state instead of the path.
|
||||
useEffect(() => {
|
||||
@@ -792,6 +792,19 @@ const AgentCard: React.FC<Props> = ({
|
||||
}}
|
||||
sx={{
|
||||
position: 'relative',
|
||||
// Hover runway for the pop-above header: the header is pointer-events:none until the CARD
|
||||
// is hovered, but it floats ABOVE the card's box, so without this strip the pointer leaving
|
||||
// the card to reach it dropped :hover and the header died mid-approach (chats ungrabbable).
|
||||
...(expanded && !tiledStyle && !pillMode && {
|
||||
'&::before': {
|
||||
content: '""',
|
||||
position: 'absolute',
|
||||
left: 0,
|
||||
right: 0,
|
||||
top: -40,
|
||||
height: 40,
|
||||
},
|
||||
}),
|
||||
// contain: streaming chat updates inside don't reflow the dashboard. Skipping `paint` here because the highlighted/selected/glow boxShadows legitimately extend past the card border, `paint` containment would clip those visuals.
|
||||
contain: 'layout style',
|
||||
// Each card gets its own compositor layer; hover-cross used to cost 100-200ms PRESENTATION by re-painting the whole canvas.
|
||||
|
||||
@@ -234,7 +234,7 @@ const BrowserCard: React.FC<Props> = ({
|
||||
}, [tileZone]);
|
||||
void tileTick;
|
||||
const cam = getCanvasState();
|
||||
const tiledStyle = useTiledStyle(tileZone, cam.panX, cam.panY, cam.zoom);
|
||||
const tiledStyle = useTiledStyle(tileZone, cam.panX, cam.panY, cam.zoom, getCanvasState, browserId);
|
||||
const onTile = useCallback((zone: string): void => {
|
||||
if (zone === 'restore') dispatch(clearTiledCard(browserId));
|
||||
else dispatch(setTiledCard({ cardId: browserId, zone }));
|
||||
|
||||
@@ -164,7 +164,7 @@ const DashboardViewCard: React.FC<Props> = ({
|
||||
}, [tileZone]);
|
||||
void tileTick;
|
||||
const cam = getCanvasState();
|
||||
const tiledStyle = useTiledStyle(tileZone, cam.panX, cam.panY, cam.zoom);
|
||||
const tiledStyle = useTiledStyle(tileZone, cam.panX, cam.panY, cam.zoom, getCanvasState, cardKey);
|
||||
const isFullscreen = tileZone === 'fullscreen';
|
||||
|
||||
// Keep the live preview mounted only when the user can actually see/use this app card. Always live
|
||||
|
||||
@@ -270,7 +270,7 @@ const NoteCard: React.FC<Props> = ({
|
||||
}, [tileZone]);
|
||||
void tileTick;
|
||||
const cam = getCanvasState();
|
||||
const tiledStyle = useTiledStyle(tileZone, cam.panX, cam.panY, cam.zoom);
|
||||
const tiledStyle = useTiledStyle(tileZone, cam.panX, cam.panY, cam.zoom, getCanvasState, noteId);
|
||||
const isFullscreen = tileZone === 'fullscreen';
|
||||
|
||||
const mdDx = (!isDragging && isSelected && multiDragDelta) ? multiDragDelta.dx : 0;
|
||||
|
||||
@@ -78,7 +78,17 @@ export function computeTiledStyle(zone: string, panX: number, panY: number, zoom
|
||||
// Tiled geometry depends on live DOM measurements, so re-render when the workspace resizes:
|
||||
// the chrome collapsing on fullscreen-enter, a window resize, a banner appearing. The initial
|
||||
// ResizeObserver fire also re-measures right after the same-commit layout change that set the zone.
|
||||
export function useTiledStyle(zone: string | undefined, panX: number, panY: number, zoom: number): TiledStyle | null {
|
||||
export function useTiledStyle(
|
||||
zone: string | undefined,
|
||||
panX: number,
|
||||
panY: number,
|
||||
zoom: number,
|
||||
// Live sync: with these, every camera write restyles the card element IN THE SAME TASK as the
|
||||
// canvas transform. The React path alone lands a commit behind the compositor, so tiled cards
|
||||
// visibly wobbled against the viewport on every zoom/pan frame.
|
||||
getLive?: () => { panX: number; panY: number; zoom: number },
|
||||
selectId?: string,
|
||||
): TiledStyle | null {
|
||||
const [, bump] = React.useReducer((n: number) => n + 1, 0);
|
||||
React.useEffect(() => {
|
||||
if (!zone) return undefined;
|
||||
@@ -94,5 +104,32 @@ export function useTiledStyle(zone: string | undefined, panX: number, panY: numb
|
||||
const timers = [60, 250, 700].map((ms) => window.setTimeout(() => bump(), ms));
|
||||
return () => { ro.disconnect(); window.removeEventListener('resize', onResize); timers.forEach((t) => window.clearTimeout(t)); };
|
||||
}, [zone]);
|
||||
React.useEffect(() => {
|
||||
if (!zone || !getLive || !selectId) return undefined;
|
||||
const el = document.querySelector(`[data-select-id="${CSS.escape(selectId)}"]`) as HTMLElement | null;
|
||||
if (!el) return undefined;
|
||||
const props = ['left', 'top', 'width', 'height', 'transform', 'transform-origin'];
|
||||
const apply = (): void => {
|
||||
// A parked card (kept alive off-screen / minimized) must keep its sx parking position.
|
||||
if (el.getAttribute('data-keepalive-hidden') === '1') { props.forEach((pr) => el.style.removeProperty(pr)); return; }
|
||||
const cam = getLive();
|
||||
const s = computeTiledStyle(zone, cam.panX, cam.panY, cam.zoom);
|
||||
if (!s) return;
|
||||
el.style.left = `${s.left}px`;
|
||||
el.style.top = `${s.top}px`;
|
||||
el.style.width = `${s.width}px`;
|
||||
el.style.height = `${s.height}px`;
|
||||
el.style.transform = s.transform;
|
||||
el.style.transformOrigin = s.transformOrigin;
|
||||
};
|
||||
apply();
|
||||
window.addEventListener('openswarm:canvas-pan-changed', apply);
|
||||
window.addEventListener('resize', apply);
|
||||
return () => {
|
||||
window.removeEventListener('openswarm:canvas-pan-changed', apply);
|
||||
window.removeEventListener('resize', apply);
|
||||
props.forEach((pr) => el.style.removeProperty(pr));
|
||||
};
|
||||
}, [zone, getLive, selectId]);
|
||||
return zone ? computeTiledStyle(zone, panX, panY, zoom) : null;
|
||||
}
|
||||
|
||||
@@ -65,7 +65,7 @@ const WorkflowsAppCard: React.FC<Props> = ({
|
||||
return () => window.removeEventListener('openswarm:canvas-pan-changed', onPan);
|
||||
}, [isFullscreen]);
|
||||
const cam = getCanvasState();
|
||||
const fsStyle = useTiledStyle(isFullscreen ? 'fullscreen' : undefined, cam.panX, cam.panY, cam.zoom);
|
||||
const fsStyle = useTiledStyle(isFullscreen ? 'fullscreen' : undefined, cam.panX, cam.panY, cam.zoom, getCanvasState, 'workflows-hub');
|
||||
|
||||
|
||||
// ---- Drag (title bar is the handle) ----
|
||||
|
||||
Reference in New Issue
Block a user