[eric] dashboard: scroll zooms the canvas over any card (Google Maps); click into a card to scroll its content

This commit is contained in:
ciregenz
2026-07-15 19:17:08 -07:00
parent d405c221ed
commit dd4605c7b3
6 changed files with 96 additions and 11 deletions
@@ -51,6 +51,7 @@ import {
type BrowserWebview,
} from '@/shared/browserRegistry';
import { setLastInteractedBrowser } from '@/shared/browserFocus';
import { onScrollFocusChange, getScrollFocusedCard } from '@/shared/cardScrollFocus';
import { registerCapsuleForRestore } from '@/shared/browserStateCapsule';
import BrowserFindBar from './BrowserFindBar';
import { useBrowserActivity } from '@/shared/useBrowserActivity';
@@ -271,6 +272,19 @@ const BrowserCard: React.FC<Props> = ({
setRegistryActiveTab(browserId, activeTabId);
}, [browserId, activeTabId]);
// Tell the (out-of-process) guest whether plain wheel should scroll the page or zoom the canvas.
// Focused (clicked-into) = scroll the page; otherwise scroll-to-zoom works while hovering it (Google Maps).
useEffect(() => {
const push = (focusedId: string | null) => {
const focused = focusedId === browserId;
for (const wv of webviewMap.current.values()) {
try { wv.send?.('openswarm:set-scroll-focus', { focused }); } catch (_e) { /* guest not ready */ }
}
};
push(getScrollFocusedCard());
return onScrollFocusChange(push);
}, [browserId, activeTabId]);
// Open the find bar when AppShell routes a Ctrl/Cmd+F to this browser; re-trigger re-focuses the input.
useEffect(() => {
const onFind = (e: Event) => {
@@ -1,6 +1,7 @@
import { useState, useCallback, useRef, useEffect, useMemo, RefObject } from 'react';
import { setCanvasInteractionActive } from '@/shared/canvasInteractionState';
import { getLastInteractedBrowser } from '@/shared/browserFocus';
import { getScrollFocusedCard } from '@/shared/cardScrollFocus';
import { getWebview } from '@/shared/browserRegistry';
import { applyBrowserZoom } from '@/shared/browserZoom';
@@ -264,6 +265,13 @@ export function useCanvasControls(zoomSensitivity: number = 50, contentBounds?:
}
if (cls === 'scrollable' && !isModifierWheel) {
// Google Maps model: plain scroll zooms the canvas over ANY card (chat, app, scheduled task) UNLESS you've clicked INTO that card to read it. So a scrollable child only eats the wheel when its card is the scroll-focused one; otherwise fall through to canvas zoom.
const cardEl = target.closest('[data-select-id]');
const cardId = cardEl?.getAttribute('data-select-id') ?? null;
if (!cardId || cardId !== getScrollFocusedCard()) {
target = target.parentElement;
continue;
}
// Re-read scrollHeight/clientHeight; cached decision is structural, scroll position is dynamic.
const canScrollY = target.scrollHeight > target.clientHeight;
const canScrollX = target.scrollWidth > target.clientWidth;
@@ -4,6 +4,7 @@ import { useAppDispatch } from '@/shared/hooks';
import { store } from '@/shared/state/store';
import { collapseSession, expandSession } from '@/shared/state/agentsSlice';
import { bringToFront } from '@/shared/state/dashboardLayoutSlice';
import { setScrollFocusedCard } from '@/shared/cardScrollFocus';
import type { CardType, useDashboardSelection } from '../state/useDashboardSelection';
import type { useCanvasControls } from './useCanvasControls';
@@ -52,6 +53,8 @@ export function useDashboardInteractions({
selection.selectCard(id, type, false);
dispatch(bringToFront({ id, type }));
// Clicking INTO a card focuses it for scrolling: plain wheel now reads its content instead of zooming the canvas (Google Maps model). Clicking blank canvas clears it (below).
setScrollFocusedCard(id);
// 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;
@@ -109,6 +112,8 @@ export function useDashboardInteractions({
if (agentDriven) return;
selection.selectCard(browserId, 'browser', false);
dispatch(bringToFront({ id: browserId, type: 'browser' }));
// Clicking inside a browser's page focuses it: plain wheel now scrolls the page instead of zooming the canvas.
setScrollFocusedCard(browserId);
};
window.addEventListener('openswarm:browser-guest-select', onGuestSelect);
return () => window.removeEventListener('openswarm:browser-guest-select', onGuestSelect);
@@ -130,6 +135,9 @@ export function useDashboardInteractions({
if (e.button !== 0) return;
if (isCardTarget(e.target, e.currentTarget)) return;
// Clicking blank canvas leaves every card: plain scroll zooms the canvas again (Google Maps model).
setScrollFocusedCard(null);
// Canvas click, drop any lingering input focus so arrow-key nav works immediately without the user having to press Escape first.
const active = document.activeElement as HTMLElement | null;
const activeTag = active?.tagName;
+1
View File
@@ -25,6 +25,7 @@ export interface BrowserWebview extends HTMLElement {
isLoading: () => boolean;
// Optional: present on real Electron webviews; the iframe fallback lacks it, callers must ?.() it.
isCurrentlyAudible?: () => boolean;
send?: (channel: string, ...args: any[]) => void;
capturePage: (rect?: { x: number; y: number; width: number; height: number }) => Promise<ElectronNativeImage>;
executeJavaScript: (code: string) => Promise<any>;
sendInputEvent: (event: any) => void;
+22
View File
@@ -0,0 +1,22 @@
// The card you've clicked INTO, so plain scroll reads its content (chat transcript, web page)
// while scroll everywhere else zooms the canvas (Google Maps model). Imperative + read on the
// wheel handler so no re-render; cleared when you click blank canvas.
let scrollFocusedCardId: string | null = null;
type Listener = (id: string | null) => void;
const listeners = new Set<Listener>();
export function setScrollFocusedCard(id: string | null): void {
if (scrollFocusedCardId === id) return;
scrollFocusedCardId = id;
for (const l of listeners) l(id);
}
export function getScrollFocusedCard(): string | null {
return scrollFocusedCardId;
}
// Browser cards subscribe so they can tell their (out-of-process) guest whether plain wheel should scroll the page or zoom the canvas.
export function onScrollFocusChange(l: Listener): () => void {
listeners.add(l);
return () => { listeners.delete(l); };
}