From aa2a84935861cb9e569e29b58ca2370bb1fad897 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 18 Apr 2026 18:47:53 +0000 Subject: [PATCH 1/4] feat: replace browser mode with focus mode MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Focus is a view-level concept orthogonal to selection. Any entity can be focused (frames, text, files, groups — edges excluded). The focused frame renders at its sizeMode (fill/fit/device) while neighbors dim; the camera stashes on entry and restores on exit. State: - Remove `UiViewMode` union, `WorkspaceViewMode`, `BrowserTabMode` - Add `UiFocus { entityId, entityKind, priorCamera }` — ephemeral, never persisted - Extend per-frame `browserSizeMode` ('fill' | 'device') to `sizeMode` ('fill' | 'fit' | 'device'); new default is 'fit' - Add `SidebarFilter` with manual per-kind chips - Drop `browserTabMode` from snapshot/workspace record (legacy files ignored) Runtime: - `setFocus(entityId)` / `clearFocus()` replace `setBrowserMode`/`setCanvasMode`/`toggleBrowserMode` - `focusSelectedEntity()` binds to toolbar Focus button - Camera stash in `ui.focus.priorCamera` on first entry; preserved across switches; restored on exit - `FocusState.workspaceViewMode` → `focusedFrameId` - `GateInputs.viewMode` → `isFocused` - Geometry: `boundIsFillBrowserPage` → `boundIsFocusFillFrame`, `computeCanvasOrigin`/`computeAvailableCanvasViewport` drop `BROWSER_HEADER_HEIGHT` offset Renderer: - Delete `BrowserTabBar.tsx` entirely - `canvas-bg/App.tsx` replaces every `viewMode === 'browser'` branch with `focusedEntityId`/`isFocusFilling` - Dimming backdrop layer renders when focused - Toolbar gains Focus (Maximize2) + Exit Focus (Minimize2) buttons on the right, replacing the canvas/browser tab toggle - Escape in toolbar calls `exitFocus()` - `FrameSizeMode` type shared from `src/shared/types.ts` Persistence: - Drop `browserTabMode` from `WorkspaceSnapshot` read/write paths (kept in type as `@deprecated` for back-compat) - Drop `viewMode` from `PersistedWorkspaceRecord` + workspace-meta.json Tests: update `gate-predicate` and `focus-reconciler` unit tests to the new shape. Typecheck clean, 189/189 unit tests green. https://claude.ai/code/session_01VVNq5g2t2267Fqs8Wp4BGu --- src/main/app-control-server.ts | 6 +- src/main/ipc/register-canvas-ipc.ts | 38 ++---- src/main/ipc/register-toolbar-ipc.ts | 65 +++------- src/main/routes/session.ts | 4 +- src/main/runtime/app-menu.ts | 18 +-- src/main/runtime/canvas-layout-data.ts | 74 ++++------- src/main/runtime/focus-reconciler-runtime.ts | 4 +- src/main/runtime/focus-reconciler.ts | 12 +- src/main/runtime/gate-predicate.ts | 4 +- src/main/runtime/json-canvas-serializer.ts | 3 - src/main/runtime/layout-engine.ts | 33 ++--- src/main/runtime/overlay-manager.ts | 4 +- src/main/runtime/region-capture.ts | 4 +- src/main/runtime/runtime-core.ts | 11 -- src/main/runtime/runtime-entities.ts | 25 ++-- src/main/runtime/runtime-geometry.ts | 122 +++++++----------- src/main/runtime/selection-controller.ts | 15 ++- src/main/runtime/selection-state.ts | 111 ++++++++++------ src/main/runtime/sidebar-builder.ts | 28 +++- src/main/runtime/tool-mode.ts | 4 +- src/main/runtime/ui-actions.ts | 7 +- src/main/runtime/workspace-autosave.ts | 1 - src/main/runtime/workspace-persistence.ts | 18 +-- src/main/runtime/workspace-restore.ts | 23 +--- src/main/runtime/workspace-tab-operations.ts | 7 +- src/main/runtime/workspace-tabs.ts | 3 - src/main/sentry-context.ts | 2 +- src/main/sentry.ts | 4 +- src/main/ui-state.ts | 89 ++++++++----- src/preload/canvas-bg.ts | 6 +- src/preload/left-sidebar.ts | 7 +- src/preload/toolbar.ts | 3 +- src/renderer/above-view/FloatingUiLayer.tsx | 4 +- .../useAnnotationDrawingGestures.ts | 4 +- src/renderer/canvas-bg/App.tsx | 62 +++++---- src/renderer/canvas-bg/BrowserTabBar.tsx | 76 ----------- src/renderer/canvas-bg/DeviceShellLayer.tsx | 2 +- src/renderer/canvas-bg/FrameBorderLayer.tsx | 4 +- .../canvas-bg/SvgDeviceShellLayer.tsx | 2 +- src/renderer/canvas-bg/canvasBgConstants.ts | 7 +- .../canvas-bg/useCanvasViewportGestures.ts | 10 +- .../shared/hooks/useCanvasGlobalShortcuts.ts | 2 +- src/renderer/toolbar/App.tsx | 26 ++-- src/renderer/toolbar/toolbarSections.tsx | 73 ++++++----- src/renderer/toolbar/useToolbarState.ts | 8 +- src/shared/types.ts | 56 ++++---- tests/unit/focus-reconciler.test.ts | 14 +- tests/unit/gate-predicate.test.ts | 6 +- 48 files changed, 501 insertions(+), 610 deletions(-) delete mode 100644 src/renderer/canvas-bg/BrowserTabBar.tsx diff --git a/src/main/app-control-server.ts b/src/main/app-control-server.ts index bb4de7d2..313ae57c 100644 --- a/src/main/app-control-server.ts +++ b/src/main/app-control-server.ts @@ -14,7 +14,7 @@ import { endAutomationInteractiveFrame, sendInteractiveState, } from './runtime/overlay-manager' -import { boundIsFillBrowserPage } from './runtime/runtime-geometry' +import { boundIsFocusFillFrame } from './runtime/runtime-geometry' import { activeSessions, resolveSession, @@ -583,9 +583,9 @@ export async function startAppControlServer(): Promise { // Snapshot scale on mousePressed; reuse for mouseReleased so a // mid-click zoom change doesn't split the pair across scales. if (cdpType === 'mousePressed') { - clickEmulationScale = boundIsFillBrowserPage(page) ? 1 : getZoom() + clickEmulationScale = boundIsFocusFillFrame(page) ? 1 : getZoom() } - const emulationScale = clickEmulationScale ?? (boundIsFillBrowserPage(page) ? 1 : getZoom()) + const emulationScale = clickEmulationScale ?? (boundIsFocusFillFrame(page) ? 1 : getZoom()) if (cdpType === 'mouseReleased') { clickEmulationScale = null } diff --git a/src/main/ipc/register-canvas-ipc.ts b/src/main/ipc/register-canvas-ipc.ts index 6303950a..c9f730fe 100644 --- a/src/main/ipc/register-canvas-ipc.ts +++ b/src/main/ipc/register-canvas-ipc.ts @@ -18,15 +18,14 @@ import { saveImageBuffer } from '../runtime/image-assets' import { imageSizeFromBuffer } from '../runtime/image-sizing' import { cancelPendingPlacement, + clearFocus, focusSelectedPage, getSelectedEntityIds, - selectBrowserTab, selectEntity, selectPage, selectPageById, selectedPageId, - setBrowserMode, - setCanvasMode, + setFocus, setSelectedEntities, } from '../runtime/ui-actions' import { @@ -50,10 +49,8 @@ import { setActiveWorkspaceTab, setWorkspaceTabExpanded, } from '../runtime/workspace-session' -import { - setFrameBrowserSizeMode, - type BrowserSizeMode, -} from '../runtime/runtime-entities' +import { setFrameSizeMode } from '../runtime/runtime-entities' +import type { FrameSizeMode } from '../../shared/types' import { createEdges, deleteEdges } from '../workspace-edges' import { selectEntitiesInRect } from '../workspace-entities' import { createFileEntity } from '../runtime/document-commands' @@ -152,34 +149,27 @@ export function registerCanvasIpc(): void { } }) - // --- Browser mode --- + // --- Focus --- + + ipcMain.on('canvas-set-focus', (_event, { entityId, entityKind }: { entityId: string; entityKind: CanvasEntityKind }) => { + setFocus(entityId, entityKind) + }) - ipcMain.on('canvas-select-browser-tab', (_event, { frameId }: { frameId: string }) => { - selectBrowserTab(frameId) + ipcMain.on('canvas-clear-focus', () => { + clearFocus() }) ipcMain.on( - 'canvas-set-browser-size-mode', - (_event, { frameId, mode }: { frameId: string; mode: BrowserSizeMode }) => { + 'canvas-set-frame-size-mode', + (_event, { frameId, mode }: { frameId: string; mode: FrameSizeMode }) => { const page = pages.find((candidate) => candidate.id === frameId) if (!page) return - page.metadata = setFrameBrowserSizeMode(page.metadata, mode) + page.metadata = setFrameSizeMode(page.metadata, mode) scheduleWorkspaceAutosave() layoutAllViews() }, ) - ipcMain.on( - 'canvas-set-browser-mode', - (_event, { mode }: { mode: 'canvas' | 'browser' }) => { - if (mode === 'browser') { - setBrowserMode() - return - } - setCanvasMode() - }, - ) - // --- Tab management --- ipcMain.on('canvas-select-tab', (_event, { tabId }: { tabId: string }) => { diff --git a/src/main/ipc/register-toolbar-ipc.ts b/src/main/ipc/register-toolbar-ipc.ts index 7e0e80bc..3f3dabd0 100644 --- a/src/main/ipc/register-toolbar-ipc.ts +++ b/src/main/ipc/register-toolbar-ipc.ts @@ -12,14 +12,16 @@ import { } from '../runtime/surface-layout' import { cancelPendingPlacement, + clearFocus, clearToolMode, + focusSelectedEntity, focusSelectedPage, getSelectedEntityIds, openInspectPanel, selectedPageId, + setFocus, startPendingPlacement, toggleAnnotateMode, - toggleBrowserMode, toggleLeftSidebar, toggleDevTools, toggleDrawMode, @@ -27,14 +29,11 @@ import { toggleInspectMode, } from '../runtime/ui-actions' import { endDevtoolsResize, setDevtoolsWidthFromScreenX } from '../runtime/window-shell' -import { selectBrowserTab } from '../runtime/runtime-core' -import { findPageById, setPendingFocus } from '../runtime/runtime-context' -import { addFrameFromSource } from '../workspace-frames' import { applyNavigationToSelectedPages } from '../navigation-sync' -import { workspaceViewMode as uiWorkspaceViewMode } from '../ui-state' +import { isFocused as uiIsFocused } from '../ui-state' -function recenterBrowserSelectionIfNeeded(): void { - if (uiWorkspaceViewMode() !== 'browser') return +function recenterFocusIfNeeded(): void { + if (!uiIsFocused()) return focusSelectedPage() } @@ -83,8 +82,12 @@ export function registerToolbarIpc(): void { applyNavigationToSelectedPages({ type: 'reload', fallbackUrl: 'about:blank' }) }) - ipcMain.on('toolbar-toggle-browser-mode', () => { - toggleBrowserMode() + ipcMain.on('toolbar-focus-selected-entity', () => { + focusSelectedEntity() + }) + + ipcMain.on('toolbar-exit-focus', () => { + clearFocus() }) ipcMain.on('toolbar-toggle-inspect', () => { @@ -111,7 +114,7 @@ export function registerToolbarIpc(): void { ipcMain.on('toggle-devtools', () => { toggleDevTools() - recenterBrowserSelectionIfNeeded() + recenterFocusIfNeeded() }) ipcMain.on('toggle-left-sidebar', () => { @@ -120,12 +123,12 @@ export function registerToolbarIpc(): void { ipcMain.on('devtools-resize-start', (_event, { screenX }: { screenX: number }) => { setDevtoolsWidthFromScreenX(screenX) - recenterBrowserSelectionIfNeeded() + recenterFocusIfNeeded() }) ipcMain.on('devtools-resize-move', (_event, { screenX }: { screenX: number }) => { setDevtoolsWidthFromScreenX(screenX) - recenterBrowserSelectionIfNeeded() + recenterFocusIfNeeded() }) ipcMain.on('devtools-resize-end', () => { @@ -140,38 +143,12 @@ export function registerToolbarIpc(): void { }) }) - ipcMain.on('add-browser-frame', (_event, presetIndex: number | 'custom') => { - const result = addFrameFromSource({ - presetIndex: typeof presetIndex === 'number' ? presetIndex : 0, - customSize: presetIndex === 'custom', - mode: 'add_from_toolbar', - focus: true, - }) - selectBrowserTab(result.frameId) - - // Focus the address bar after the new page finishes loading. - // We must wait because Chromium auto-focuses a webContents when - // its load completes, which would steal focus from the toolbar. - const page = findPageById(result.frameId) - if (toolbarView && page) { - const focusToolbar = () => { - if (!toolbarView) return - setPendingFocus({ kind: 'toolbar' }) - requestLayout() - toolbarView.webContents.send('focus-address-bar') - } - const wc = page.pageView.webContents - if (wc.isLoading()) { - const onDestroyed = () => wc.removeListener('did-finish-load', focusToolbar) - wc.once('destroyed', onDestroyed) - wc.once('did-finish-load', () => { - wc.removeListener('destroyed', onDestroyed) - focusToolbar() - }) - } else { - focusToolbar() - } - } + ipcMain.on('canvas-bg-set-focus', (_event, { entityId, entityKind }: { entityId: string; entityKind: import('../../shared/types').CanvasEntityKind }) => { + setFocus(entityId, entityKind) + }) + + ipcMain.on('canvas-bg-clear-focus', () => { + clearFocus() }) ipcMain.on('cancel-pending-placement', () => { diff --git a/src/main/routes/session.ts b/src/main/routes/session.ts index df31eb4d..92b256e0 100644 --- a/src/main/routes/session.ts +++ b/src/main/routes/session.ts @@ -27,7 +27,7 @@ import { } from '../runtime/runtime-context' import { selectNone as clearSelection } from '../runtime/selection-controller' import { sendInteractiveState } from '../runtime/overlay-manager' -import { setCanvasMode as setUiCanvasMode } from '../ui-state' +import { clearFocus as setUiClearFocus } from '../ui-state' import { writeJson, notifyStatusListeners } from '../app-control-server' function resetSmokeTestState(): void { @@ -35,7 +35,7 @@ function resetSmokeTestState(): void { resetCdpProxyState() clearAutomationInteractiveFrameIds() clearSelection() - setUiCanvasMode() + setUiClearFocus() sendInteractiveState() } diff --git a/src/main/runtime/app-menu.ts b/src/main/runtime/app-menu.ts index 95e72c37..b67b471c 100644 --- a/src/main/runtime/app-menu.ts +++ b/src/main/runtime/app-menu.ts @@ -1,8 +1,8 @@ import { app, dialog, Menu } from 'electron' import { deleteFrames } from '../workspace-entities' import { pages, selectedPageId } from './runtime-context' -import { workspaceViewMode } from '../ui-state' -import { selectBrowserTab } from './runtime-core' +import { focusedFrameId } from '../ui-state' +import { setFocus } from './selection-state' import { checkForUpdatesManually } from '../auto-updater' import { showOnboardingWindow } from '../onboarding-window' import { @@ -81,19 +81,19 @@ function buildTemplate(): Electron.MenuItemConstructorOptions[] { const frameId = selectedPageId() if (!frameId) return - const isBrowser = workspaceViewMode() === 'browser' - let nextTabId: string | null = null - - if (isBrowser) { + // If we're focused on this frame, find a neighbor so we can switch focus after delete + const isFocusedOnThisFrame = focusedFrameId() === frameId + let nextFrameId: string | null = null + if (isFocusedOnThisFrame) { const idx = pages.findIndex((p) => p.id === frameId) const next = pages[idx + 1] ?? pages[idx - 1] ?? null - nextTabId = next?.id ?? null + nextFrameId = next?.id ?? null } deleteFrames({ frameIds: [frameId] }) - if (isBrowser && nextTabId) { - selectBrowserTab(nextTabId) + if (isFocusedOnThisFrame && nextFrameId) { + setFocus(nextFrameId, 'frame') } }, }, diff --git a/src/main/runtime/canvas-layout-data.ts b/src/main/runtime/canvas-layout-data.ts index 7f938112..3b13909e 100644 --- a/src/main/runtime/canvas-layout-data.ts +++ b/src/main/runtime/canvas-layout-data.ts @@ -41,22 +41,22 @@ import { } from './runtime-context' import { activeWorkspaceTabId, workspaceAnnotations, workspaceEdges, workspaceGroups } from './workspace-model' import { - activeBrowserFrameId as uiActiveBrowserFrameId, annotationMode as uiAnnotationMode, devtoolsOpen as uiDevtoolsOpen, devtoolsWidth as uiDevtoolsWidth, + focusedEntityId as uiFocusedEntityId, + focusedFrameId as uiFocusedFrameId, pendingPlacement as uiPendingPlacement, selectedCanvasTargets as uiSelectedCanvasTargets, selectedEntityIds as uiSelectedEntityIds, selectedGroupId as uiSelectedGroupId, - workspaceViewMode as uiWorkspaceViewMode, } from '../ui-state' import { pageContentSize, boundEffectivePageContentSize as effectivePageContentSize, boundAvailableCanvasViewport as localAvailableCanvasViewport, boundCanvasOrigin as localCanvasOrigin, - boundFillBrowserViewportSize as localFillBrowserViewportSize, + boundFocusFillViewportSize as localFocusFillViewportSize, boundScreenBoundsForPage as screenBoundsForPage, } from './runtime-geometry' import { frameDisplayLabel, viewportPresetForIndex } from './runtime-serialization' @@ -68,7 +68,7 @@ import { } from './text-entity-state' import { frameUsesCustomSize, - frameBrowserSizeModeFromMetadata, + frameSizeModeFromMetadata, deviceIdFromMetadata, deviceOrientationFromMetadata, showDeviceFrameFromMetadata, @@ -103,15 +103,15 @@ function mainWindowContentBounds(): { // --- Exported data builders --- export function backgroundFrameOverlays(): CanvasSceneFrameEntity[] { - const viewMode = uiWorkspaceViewMode() - const activeBrowserPageId = viewMode === 'browser' ? uiActiveBrowserFrameId() : null + const focusedFrameIdValue = uiFocusedFrameId() return pages.map((page) => { const { width, height } = effectivePageContentSize(page) const bounds = screenBoundsForPage(page) const deviceId = deviceIdFromMetadata(page.metadata) - // In browser mode, only show the device shell for the active tab + const isFocusedFrame = focusedFrameIdValue === page.id + // When a frame is focused, only show the device shell for the focused frame const showShell = showDeviceFrameFromMetadata(page.metadata) - && (viewMode === 'canvas' || page.id === activeBrowserPageId) + && (focusedFrameIdValue === null || isFocusedFrame) return { kind: 'frame' as const, id: page.id, @@ -122,7 +122,9 @@ export function backgroundFrameOverlays(): CanvasSceneFrameEntity[] { canGoForward: page.pageView.webContents.canGoForward(), isLoading: page.pageView.webContents.isLoading(), isCustomSize: frameUsesCustomSize(page.metadata), - browserSizeMode: viewMode === 'canvas' ? 'device' : frameBrowserSizeModeFromMetadata(page.metadata), + sizeMode: isFocusedFrame + ? frameSizeModeFromMetadata(page.metadata) + : 'fit', canvasX: page.canvasX, canvasY: page.canvasY, width, @@ -147,22 +149,6 @@ export function backgroundFrameOverlays(): CanvasSceneFrameEntity[] { }) } -function buildLiveBrowserTabSummaries() { - return pages.map((page) => { - const { width, height } = pageContentSize(page) - return { - id: page.id, - label: frameDisplayLabel(page), - name: page.name?.trim() || undefined, - url: page.url, - presetIndex: page.presetIndex, - faviconUrl: page.faviconUrl ?? null, - width, - height, - } - }) -} - export function activeCanvasSelection(): ActiveCanvasEntitySelection | null { const selectedFrameIds = uiSelectedEntityIds() const targets = selectedFrameIds @@ -273,9 +259,9 @@ export function buildCanvasLayoutData( frames: CanvasSceneFrameEntity[], activeSelection: ActiveCanvasEntitySelection | null, ): LayoutUpdateData { - const fillViewport = localFillBrowserViewportSize() + const fillViewport = localFocusFillViewportSize() const pending = uiPendingPlacement() - const viewMode = uiWorkspaceViewMode() + const focusedFrameIdValue = uiFocusedFrameId() const origin = localCanvasOrigin() const pendingPlacementData: PendingPlacement | null = pending @@ -284,7 +270,7 @@ export function buildCanvasLayoutData( const isFile = pending.entityKind === 'file' const sourcePage = pending.sourceFrameId ? findPageById(pending.sourceFrameId) : null const preset = (isText || isFile) ? null : viewportPresetForIndex(pending.presetIndex ?? 0) - const customSize = sourcePage ? pageContentSize(sourcePage) : localFillBrowserViewportSize() + const customSize = sourcePage ? pageContentSize(sourcePage) : localFocusFillViewportSize() const contentBounds = mainWindowContentBounds() const cursor = screen.getCursorScreenPoint() const initialClientX = @@ -339,20 +325,14 @@ export function buildCanvasLayoutData( ), ...groupEntities, ] as CanvasSceneEntity[], - browserTabs: buildLiveBrowserTabSummaries(), - browserFillViewport: fillViewport, + focusFillViewport: fillViewport, selectedEntityIds: uiSelectedEntityIds(), selection: uiSelectedCanvasTargets(), activeSelection, annotationMode: uiAnnotationMode(), annotations: [...workspaceAnnotations], fixProgress: getFixProgress(), - viewMode, - activeBrowserTabId: - viewMode === 'browser' - ? selectedPageId() - : null, - activeBrowserFrameId: uiActiveBrowserFrameId(), + focusedEntityId: uiFocusedEntityId(), selectedGroupId: uiSelectedGroupId(), hover: hoverTarget, interaction: interactionState, @@ -363,10 +343,9 @@ export function buildCanvasLayoutData( groups: groupEntities, presenceCursors: getPresenceCursors() .filter((c) => { - // In browser mode, hide cursors that explicitly target a different frame. - if (viewMode !== 'browser') return true - const activeFrameId = uiActiveBrowserFrameId() - if (c.surface === 'frame' && c.frameId && c.frameId !== activeFrameId) return false + // When focused on a frame, hide cursors that explicitly target a different frame. + if (focusedFrameIdValue === null) return true + if (c.surface === 'frame' && c.frameId && c.frameId !== focusedFrameIdValue) return false return true }) .map((c): AgentPresenceCursor => ({ @@ -381,8 +360,6 @@ export function buildCanvasLayoutData( fallbackX: frame.width / 2, fallbackY: frame.height / 2, }) - // Clamp to the frame's visible area so the cursor doesn't - // render outside the frame when targeting off-screen elements. const clampedX = Math.max(0, Math.min(point.x, frame.width)) const clampedY = Math.max(0, Math.min(point.y, frame.height)) return { @@ -391,13 +368,10 @@ export function buildCanvasLayoutData( } } } - // In browser mode, place canvas-surface cursors on the active frame + // When focused on a frame, place canvas-surface cursors on that frame // so they remain visible instead of mapping to off-screen canvas coords. - if (viewMode === 'browser') { - const activeFrameId = uiActiveBrowserFrameId() - const frame = activeFrameId - ? frames.find((candidate) => candidate.id === activeFrameId) - : null + if (focusedFrameIdValue) { + const frame = frames.find((candidate) => candidate.id === focusedFrameIdValue) if (frame) { return { screenX: frame.screenX + frame.screenWidth / 2, @@ -466,7 +440,7 @@ export function toolbarSelectionData(): ToolbarSelectionData { loadingPhase: 'idle', activeTabId: activeWorkspaceTabId, activeTabName, - viewMode: uiWorkspaceViewMode(), + focusedEntityId: uiFocusedEntityId(), pendingPlacementActive: uiPendingPlacement() !== null, } } @@ -501,7 +475,7 @@ export function toolbarSelectionData(): ToolbarSelectionData { loadingPhase, activeTabId: activeWorkspaceTabId, activeTabName, - viewMode: uiWorkspaceViewMode(), + focusedEntityId: uiFocusedEntityId(), pendingPlacementActive: uiPendingPlacement() !== null, } } diff --git a/src/main/runtime/focus-reconciler-runtime.ts b/src/main/runtime/focus-reconciler-runtime.ts index 6cce40e2..062e7500 100644 --- a/src/main/runtime/focus-reconciler-runtime.ts +++ b/src/main/runtime/focus-reconciler-runtime.ts @@ -10,7 +10,7 @@ import type { FocusTarget } from '../../shared/interaction-types' import { expectedFocus, focusKey, type FocusState } from './focus-reconciler' import { aboveView, bgView, toolbarView, leftSidebarView, win } from './view-refs' import { pages, interactionState, pendingFocus, setPendingFocus } from './runtime-context' -import { isCommentOverlayVisible, selectedPageIndex, workspaceViewMode } from '../ui-state' +import { focusedFrameId, isCommentOverlayVisible, selectedPageIndex } from '../ui-state' function interactionModeKey(): FocusState['interactionMode'] { switch (interactionState.kind) { @@ -31,7 +31,7 @@ function currentFocusState(): FocusState { interactionMode: interactionModeKey(), editingTextEntityId: interactionState.kind === 'editing-text' ? interactionState.entityId : null, selectedPageId: selectedPage?.id ?? null, - workspaceViewMode: workspaceViewMode(), + focusedFrameId: focusedFrameId(), commentOverlayActive: isCommentOverlayVisible(), pendingFocus, } diff --git a/src/main/runtime/focus-reconciler.ts b/src/main/runtime/focus-reconciler.ts index 5f3a25cd..b3488829 100644 --- a/src/main/runtime/focus-reconciler.ts +++ b/src/main/runtime/focus-reconciler.ts @@ -24,7 +24,8 @@ export type FocusState = { interactionMode: 'idle' | 'panning' | 'marquee' | 'dragging-entities' | 'resizing-entity' | 'dragging-edge' | 'editing-text' editingTextEntityId: string | null selectedPageId: string | null - workspaceViewMode: 'canvas' | 'browser' + /** Frame id currently focused (any entity kind other than frame → null). */ + focusedFrameId: string | null commentOverlayActive: boolean /** Explicit intent set by a subsystem (overrides derivation). Cleared after reconcile. */ pendingFocus: FocusTarget | null @@ -54,13 +55,12 @@ export function expectedFocus(state: FocusState): FocusTarget { return { kind: 'aboveView' } } - // Browser mode with a live page — the page should be focused so - // keyboard input reaches the web content. - if (state.workspaceViewMode === 'browser' && state.selectedPageId) { - return { kind: 'page', id: state.selectedPageId } + // Focus-on-frame — the page should be focused so keyboard input reaches the web content. + if (state.focusedFrameId) { + return { kind: 'page', id: state.focusedFrameId } } - // Canvas mode default: bgView (canvas keyboard shortcuts, undo). + // Default: bgView (canvas keyboard shortcuts, undo). return { kind: 'bgView' } } diff --git a/src/main/runtime/gate-predicate.ts b/src/main/runtime/gate-predicate.ts index 2cb10389..8a90f4f7 100644 --- a/src/main/runtime/gate-predicate.ts +++ b/src/main/runtime/gate-predicate.ts @@ -17,7 +17,7 @@ import type { CanvasEntityKind } from '../../shared/types' export type GateInputs = { interactionKind: InteractionMode['kind'] toolMode: 'select' | 'inspect' | 'annotate-comment' | 'annotate-draw' | 'annotate-region-select' - viewMode: 'canvas' | 'browser' + isFocused: boolean /** Imperative override set by IPC handlers that open annotation/comment UI. */ commentOverlayActive: boolean selectionMarqueeVisible: boolean @@ -53,7 +53,7 @@ function toolModeOpensGate(toolMode: GateInputs['toolMode']): boolean { } function hasFloatingMenu(inputs: GateInputs): boolean { - if (inputs.viewMode !== 'canvas') return false + if (inputs.isFocused) return false if (inputs.selectedEntityIds.length !== 1) return false const kind = inputs.selectedEntityKinds[0] return kind === 'text' || kind === 'drawing' diff --git a/src/main/runtime/json-canvas-serializer.ts b/src/main/runtime/json-canvas-serializer.ts index f486aed5..a0f97d48 100644 --- a/src/main/runtime/json-canvas-serializer.ts +++ b/src/main/runtime/json-canvas-serializer.ts @@ -7,7 +7,6 @@ import type { Annotation, - BrowserTabMode, DevtoolsPanelTab, PersistedCanvasEntity, PersistedDrawingEntity, @@ -216,7 +215,6 @@ function serializeAppState(snapshot: WorkspaceSnapshot): JsonCanvasAppState { devtoolsOpen: snapshot.devtoolsOpen, devtoolsPanelTab: snapshot.devtoolsPanelTab, devtoolsWidth: snapshot.devtoolsWidth, - browserTabMode: snapshot.browserTabMode, } } @@ -269,7 +267,6 @@ export function deserializeFromJsonCanvas(doc: JsonCanvasDocument): { devtoolsOpen: appState.devtoolsOpen ?? false, devtoolsPanelTab: (appState.devtoolsPanelTab as DevtoolsPanelTab) ?? 'elements', devtoolsWidth: appState.devtoolsWidth ?? 400, - browserTabMode: (appState.browserTabMode as BrowserTabMode) ?? 'frame', edges, } diff --git a/src/main/runtime/layout-engine.ts b/src/main/runtime/layout-engine.ts index 72a38826..67097122 100644 --- a/src/main/runtime/layout-engine.ts +++ b/src/main/runtime/layout-engine.ts @@ -3,10 +3,9 @@ import { boundsKey, boundApplyEmulation, boundEffectivePageContentSize, - boundIsFillBrowserPage, + boundIsFocusFillFrame, boundScreenBoundsForPage, boundSelectedPage, - boundSelectedPageId, boundCanvasOrigin, } from './runtime-geometry' import { @@ -45,11 +44,13 @@ import { devtoolsOpen as uiDevtoolsOpen, devtoolsPanelTab as uiDevtoolsPanelTab, devtoolsWidth as uiDevtoolsWidth, + focusedEntityId as uiFocusedEntityId, + focusedFrameId as uiFocusedFrameId, isCommentOverlayVisible as uiCommentOverlayVisible, + isFocused as uiIsFocused, leftSidebarOpen as uiLeftSidebarOpen, selectedEntityIds as uiSelectedEntityIds, setDevtoolsWidth as setUiDevtoolsWidth, - workspaceViewMode as uiWorkspaceViewMode, } from '../ui-state' import { backgroundFrameOverlays, @@ -88,7 +89,6 @@ export function setBoundsIfChanged( } import { - BROWSER_HEADER_HEIGHT, CARD_BORDER_RADIUS, DEVTOOLS_HEADER_GAP, DEVTOOLS_HEADER_HEIGHT, @@ -202,13 +202,15 @@ export function layoutAllViews(): void { if (!win || win.isDestroyed()) return const layoutStart = DEVTOOLS_PANEL_DEBUG ? Date.now() : 0 if (consumeDirty('stack')) applyStack() - const viewMode = uiWorkspaceViewMode() + const focusedEntityId = uiFocusedEntityId() + const focusedFrameIdValue = uiFocusedFrameId() + const isFocused = uiIsFocused() const devtoolsOpen = uiDevtoolsOpen() const devtoolsWidth = uiDevtoolsWidth() const devtoolsPanelTab = uiDevtoolsPanelTab() const selectedFrameIds = uiSelectedEntityIds() - const contentTopInset = layoutCache.toolbarHeight + (viewMode === 'browser' ? BROWSER_HEADER_HEIGHT : 0) + const contentTopInset = layoutCache.toolbarHeight const frameOverlays = backgroundFrameOverlays() const nextActiveSelection = activeCanvasSelection() @@ -269,7 +271,7 @@ export function layoutAllViews(): void { : interactionState.kind === 'editing-text' ? 'editing-text' : interactionState.kind, toolMode: getUiState().toolMode, - viewMode: uiWorkspaceViewMode(), + isFocused, commentOverlayActive: uiCommentOverlayVisible(), selectionMarqueeVisible: selectionOverlayActive, spaceHeld: spaceModifierHeld, @@ -328,10 +330,11 @@ export function layoutAllViews(): void { const windowRect = { x: 0, y: 0, width: winBounds.width, height: winBounds.height } // --- Per-page bounds, emulation, annotations --- - const visibleBrowserPageId = boundSelectedPageId() for (const page of pages) { const pageStart = DEVTOOLS_PANEL_DEBUG ? Date.now() : 0 - const isVisibleInCurrentMode = viewMode === 'canvas' || page.id === visibleBrowserPageId + // In focus-fill mode, only the focused frame renders (mirrors old browser mode) + const isVisibleInCurrentMode = + focusedFrameIdValue === null || page.id === focusedFrameIdValue if (!isVisibleInCurrentMode) { page.lastFrameBoundsKey = setBoundsIfChanged(page.frameView, HIDDEN_BOUNDS, page.lastFrameBoundsKey) page.lastPageBoundsKey = setBoundsIfChanged(page.pageView, HIDDEN_BOUNDS, page.lastPageBoundsKey) @@ -360,10 +363,10 @@ export function layoutAllViews(): void { continue } - const isFillBrowser = boundIsFillBrowserPage(page) + const isFocusFill = boundIsFocusFillFrame(page) const deviceId = deviceIdFromMetadata(page.metadata) const showShell = showDeviceFrameFromMetadata(page.metadata) - const borderRadius = isFillBrowser + const borderRadius = isFocusFill ? 0 : deviceId && showShell ? Math.round(contentCornerRadiusForDevice(deviceId, deviceOrientationFromMetadata(page.metadata)) * zoom) @@ -376,8 +379,8 @@ export function layoutAllViews(): void { const { width: emulatedWidth, height: emulatedHeight } = boundEffectivePageContentSize(page) const nativeScale = screen.getPrimaryDisplay().scaleFactor - const pageScale = isFillBrowser ? 1 : zoom - const pageEmulationKey = `${emulatedWidth}:${emulatedHeight}:${pageScale}:${nativeScale}:${viewMode}:${devtoolsOpen ? devtoolsWidth : 0}` + const pageScale = isFocusFill ? 1 : zoom + const pageEmulationKey = `${emulatedWidth}:${emulatedHeight}:${pageScale}:${nativeScale}:${focusedEntityId ?? ''}:${devtoolsOpen ? devtoolsWidth : 0}` if (pageEmulationKey !== page.lastPageEmulationKey) { const emulationStart = DEVTOOLS_PANEL_DEBUG ? Date.now() : 0 boundApplyEmulation(page.pageView.webContents, page.presetIndex, page) @@ -387,7 +390,7 @@ export function layoutAllViews(): void { durationMs: Date.now() - emulationStart, emulatedWidth, emulatedHeight, - viewMode, + focusedEntityId, devtoolsOpen, }) @@ -395,7 +398,7 @@ export function layoutAllViews(): void { // Inject or remove safe-area CSS padding when the device shell is active const orientation = deviceOrientationFromMetadata(page.metadata) - const safeAreaCss = deviceId && showShell && !isFillBrowser + const safeAreaCss = deviceId && showShell && !isFocusFill ? safeAreaCssForDevice(deviceId, orientation) : null const safeAreaKey = safeAreaCss ?? '' diff --git a/src/main/runtime/overlay-manager.ts b/src/main/runtime/overlay-manager.ts index 03fd35c1..b79646f2 100644 --- a/src/main/runtime/overlay-manager.ts +++ b/src/main/runtime/overlay-manager.ts @@ -18,10 +18,10 @@ import { } from './runtime-context' import { workspaceGroups } from './workspace-model' import { + focusedEntityId as uiFocusedEntityId, getUiState, isSelectionMarqueeVisible as uiSelectionMarqueeVisible, setSelectionMarqueeVisible as setUiSelectionMarqueeVisible, - workspaceViewMode as uiWorkspaceViewMode, } from '../ui-state' import { selectionDebug } from './runtime-constants' import { boundEffectivePageContentSize } from './runtime-geometry' @@ -83,7 +83,7 @@ export function sendInteractiveState(): void { const wc = pages[i].pageView.webContents safeSend(wc, 'set-interactive', isSelected) safeSend(wc, 'set-multi-selected', isMultiSelected) - safeSend(wc, 'set-workspace-view-mode', uiWorkspaceViewMode()) + safeSend(wc, 'set-focused-entity-id', uiFocusedEntityId()) } } diff --git a/src/main/runtime/region-capture.ts b/src/main/runtime/region-capture.ts index d752e72e..399dd92d 100644 --- a/src/main/runtime/region-capture.ts +++ b/src/main/runtime/region-capture.ts @@ -27,10 +27,10 @@ import { showDeviceFrameFromMetadata, } from './runtime-entities' import { contentCornerRadiusForDevice } from '../../shared/device-catalog' -import { boundIsFillBrowserPage } from './runtime-geometry' +import { boundIsFocusFillFrame } from './runtime-geometry' function pageCornerRadiusPx(page: Page, dpr: number): number { - if (boundIsFillBrowserPage(page)) return 0 + if (boundIsFocusFillFrame(page)) return 0 const deviceId = deviceIdFromMetadata(page.metadata) if (!deviceId || !showDeviceFrameFromMetadata(page.metadata)) return 0 const orientation = deviceOrientationFromMetadata(page.metadata) diff --git a/src/main/runtime/runtime-core.ts b/src/main/runtime/runtime-core.ts index f80fad63..6bd9d27b 100644 --- a/src/main/runtime/runtime-core.ts +++ b/src/main/runtime/runtime-core.ts @@ -12,7 +12,6 @@ import { selectedPageIndex as uiSelectedPageIndex, setCommentOverlayVisible as setUiCommentOverlayVisible, setDevtoolsWidth as setUiDevtoolsWidth, - workspaceViewMode as uiWorkspaceViewMode, } from '../ui-state' import type { DevtoolsPanelData, @@ -43,9 +42,6 @@ import { notifyDevtoolsPanelData, } from './inspect-session' import type { Page } from './runtime-entities' -import { - setBrowserMode, -} from './selection-state' import { selectEntities as commitSelectedEntities, selectEntity as commitSelectEntity, @@ -219,9 +215,6 @@ function collapseSelectionForBrowserMode(frameId?: string): boolean { } return true } -export function selectBrowserTab(frameId: string): boolean { - return setBrowserMode(frameId) -} export function removePage(senderWebContents: WebContents): void { if (!win) return const idx = pages.findIndex( @@ -262,10 +255,6 @@ export function setDevtoolsWidthFromScreenX(screenX: number): void { requestLayout() } -function currentViewMode(): string { - return uiWorkspaceViewMode() -} - function currentDevtoolsOpen(): boolean { return uiDevtoolsOpen() } diff --git a/src/main/runtime/runtime-entities.ts b/src/main/runtime/runtime-entities.ts index b5210efd..1fee0684 100644 --- a/src/main/runtime/runtime-entities.ts +++ b/src/main/runtime/runtime-entities.ts @@ -115,26 +115,29 @@ export function clearCustomFrameSizeMetadata( } // --------------------------------------------------------------------------- -// Browser size mode metadata (per-frame fill vs device in browser mode) +// Frame size mode metadata (per-frame fill / fit / device when focused) // --------------------------------------------------------------------------- -export type BrowserSizeMode = 'fill' | 'device' +import type { FrameSizeMode } from '../../shared/types' -export function frameBrowserSizeModeFromMetadata( +export function frameSizeModeFromMetadata( metadata: Record | undefined, -): BrowserSizeMode { - if (!metadata) return 'device' - const mode = metadata.browserSizeMode - return mode === 'fill' ? 'fill' : 'device' +): FrameSizeMode { + if (!metadata) return 'fit' + // Prefer the new key; fall back to legacy 'browserSizeMode' for older files + const raw = metadata.sizeMode ?? metadata.browserSizeMode + if (raw === 'fill' || raw === 'fit' || raw === 'device') return raw + return 'fit' } -export function setFrameBrowserSizeMode( +export function setFrameSizeMode( metadata: Record | undefined, - mode: BrowserSizeMode, + mode: FrameSizeMode, ): Record { + const { browserSizeMode: _legacy, ...rest } = (metadata ?? {}) as Record return { - ...(metadata ?? {}), - browserSizeMode: mode, + ...rest, + sizeMode: mode, } } diff --git a/src/main/runtime/runtime-geometry.ts b/src/main/runtime/runtime-geometry.ts index d4c8e019..a96235b4 100644 --- a/src/main/runtime/runtime-geometry.ts +++ b/src/main/runtime/runtime-geometry.ts @@ -3,7 +3,6 @@ import type { WebContents } from 'electron' import type { WorkspaceBounds } from '../../shared/types' import type { Page } from './runtime-entities' import { - BROWSER_HEADER_HEIGHT, CARD_BORDER_WIDTH, CHROME_PAGE_GAP, LEFT_SIDEBAR_WIDTH, @@ -11,7 +10,7 @@ import { } from './runtime-constants' import { frameCustomSizeFromMetadata, - frameBrowserSizeModeFromMetadata, + frameSizeModeFromMetadata, deviceIdFromMetadata, deviceOrientationFromMetadata, showDeviceFrameFromMetadata, @@ -23,9 +22,9 @@ import { pages, pan, zoom } from './runtime-context' import { devtoolsOpen as uiDevtoolsOpen, devtoolsWidth as uiDevtoolsWidth, + focusedFrameId as uiFocusedFrameId, leftSidebarOpen as uiLeftSidebarOpen, selectedPageIndex as uiSelectedPageIndex, - workspaceViewMode as uiWorkspaceViewMode, } from '../ui-state' import { viewportPresetForIndex } from './runtime-serialization' @@ -107,25 +106,20 @@ export function pageOuterCanvasBounds( // --------------------------------------------------------------------------- export function computeCanvasOrigin(input: { - currentViewMode: () => string toolbarHeight: number - browserHeaderHeight: number leftSidebarWidth: number }): { x: number; y: number } { - const viewMode = input.currentViewMode() return { x: input.leftSidebarWidth, - y: input.toolbarHeight + (viewMode === 'browser' ? input.browserHeaderHeight : 0), + y: input.toolbarHeight, } } export function computeAvailableCanvasViewport(input: { win: { getBounds(): { width: number; height: number } } | null - currentViewMode: () => string currentDevtoolsOpen: () => boolean currentDevtoolsWidth: () => number toolbarHeight: number - browserHeaderHeight: number leftSidebarWidth: number }): { width: number; height: number } { const viewport = computeAvailableCanvasViewportRect(input) @@ -134,17 +128,14 @@ export function computeAvailableCanvasViewport(input: { export function computeAvailableCanvasViewportRect(input: { win: { getBounds(): { width: number; height: number } } | null - currentViewMode: () => string currentDevtoolsOpen: () => boolean currentDevtoolsWidth: () => number toolbarHeight: number - browserHeaderHeight: number leftSidebarWidth: number }): { x: number; y: number; width: number; height: number } { const { width = 0, height = 0 } = input.win?.getBounds() ?? {} const leftInset = input.leftSidebarWidth - const topInset = - input.toolbarHeight + (input.currentViewMode() === 'browser' ? input.browserHeaderHeight : 0) + const topInset = input.toolbarHeight return { x: leftInset, y: topInset, @@ -156,7 +147,7 @@ export function computeAvailableCanvasViewportRect(input: { } } -function computeFillBrowserViewportSize(input: { +function computeFocusFillViewportSize(input: { availableCanvasViewport: () => { width: number; height: number } }): { width: number; height: number } { const viewport = input.availableCanvasViewport() @@ -166,25 +157,27 @@ function computeFillBrowserViewportSize(input: { } } -export function computeIsFillBrowserPage(input: { +/** + * True when the frame is the focused entity AND its size mode is 'fill' — + * the frame should be drawn at viewport dimensions (not canvas position). + */ +export function computeIsFocusFillFrame(input: { page: Pick - currentViewMode: () => string - selectedPageId: () => string | null + focusedFrameId: () => string | null }): boolean { return ( - input.currentViewMode() === 'browser' && - input.selectedPageId() === input.page.id && - frameBrowserSizeModeFromMetadata(input.page.metadata) === 'fill' + input.focusedFrameId() === input.page.id && + frameSizeModeFromMetadata(input.page.metadata) === 'fill' ) } export function computeEffectivePageContentSize(input: { page: Pick - isFillBrowserPage: (page: Pick) => boolean - fillBrowserViewportSize: () => { width: number; height: number } + isFocusFillFrame: (page: Pick) => boolean + focusFillViewportSize: () => { width: number; height: number } }): { width: number; height: number } { - if (input.isFillBrowserPage(input.page)) { - return input.fillBrowserViewportSize() + if (input.isFocusFillFrame(input.page)) { + return input.focusFillViewportSize() } return pageContentSize(input.page) } @@ -193,13 +186,11 @@ export function computeScreenBoundsForPage(input: { page: Page effectivePageContentSize: (page: Pick) => { width: number; height: number } availableCanvasViewportRect: () => { x: number; y: number; width: number; height: number } - currentViewMode: () => string - selectedPageId: () => string | null - isFillBrowserPage: (page: Pick) => boolean + focusedFrameId: () => string | null + isFocusFillFrame: (page: Pick) => boolean zoom: number pan: { x: number; y: number } toolbarHeight: number - browserHeaderHeight: number chromePageGap: number cardBorderWidth: number }): { @@ -210,43 +201,29 @@ export function computeScreenBoundsForPage(input: { } { const { width: w, height: h } = input.effectivePageContentSize(input.page) const bw = input.cardBorderWidth - const isBrowserActive = - input.currentViewMode() === 'browser' && input.selectedPageId() === input.page.id - const isFillBrowserActive = input.isFillBrowserPage(input.page) - const displayZoom = isFillBrowserActive ? 1 : input.zoom + const isFocusFillActive = input.isFocusFillFrame(input.page) + const displayZoom = isFocusFillActive ? 1 : input.zoom const chromeH = Math.round(input.page.chromeHeight * input.zoom) const gap = Math.round(input.chromePageGap * input.zoom) const contentW = Math.round(w * displayZoom) const fullPageH = Math.round(h * displayZoom) const viewport = input.availableCanvasViewportRect() - const browserViewportTop = input.toolbarHeight + input.browserHeaderHeight - const browserViewportHeight = viewport.height - const maxBrowserPageH = Math.max(0, browserViewportHeight) - const pageH = isFillBrowserActive - ? maxBrowserPageH - : isBrowserActive - ? Math.min(fullPageH, maxBrowserPageH) - : fullPageH - const rawChromeX = isBrowserActive - ? isFillBrowserActive - ? viewport.x - : Math.round(viewport.x + (viewport.width - w * input.zoom) / 2) + const viewportTop = input.toolbarHeight + const viewportHeight = viewport.height + const maxPageH = Math.max(0, viewportHeight) + const pageH = isFocusFillActive ? maxPageH : fullPageH + const rawChromeX = isFocusFillActive + ? viewport.x : Math.round(viewport.x + input.page.canvasX * input.zoom + input.pan.x) - const browserMinPageY = browserViewportTop - const centeredBrowserPageY = Math.round( - browserViewportTop + (browserViewportHeight - pageH) / 2, - ) - const pageY = isBrowserActive - ? fullPageH >= maxBrowserPageH - ? browserMinPageY - : Math.max(browserMinPageY, centeredBrowserPageY) + const pageY = isFocusFillActive + ? viewportTop : Math.round(input.page.canvasY * input.zoom + input.pan.y) + input.toolbarHeight + chromeH + gap - const chromeY = isBrowserActive - ? browserViewportTop + const chromeY = isFocusFillActive + ? viewportTop : Math.round(input.page.canvasY * input.zoom + input.pan.y) + input.toolbarHeight - // Compute shell rect (device frame bezel) — skip in fill-browser mode + // Compute shell rect (device frame bezel) — skip in focus-fill mode const insets = pageShellInsets(input.page) - const shellRect = insets && !isFillBrowserActive + const shellRect = insets && !isFocusFillActive ? { x: rawChromeX - Math.round(insets.left * displayZoom), y: pageY - Math.round(insets.top * displayZoom), @@ -289,13 +266,13 @@ export function computeApplyEmulation(input: { page?: Page zoom: number effectivePageContentSize: (page: Pick) => { width: number; height: number } - isFillBrowserPage: (page: Pick) => boolean + isFocusFillFrame: (page: Pick) => boolean viewportPresetForIndex: (presetIndex: number) => { width: number; height: number } }): void { const start = Date.now() const vp = input.viewportPresetForIndex(input.presetIndex) const nativeScale = screen.getPrimaryDisplay().scaleFactor - const fillScale = input.page && input.isFillBrowserPage(input.page) ? 1 : input.zoom + const fillScale = input.page && input.isFocusFillFrame(input.page) ? 1 : input.zoom const size = input.page ? input.effectivePageContentSize(input.page) : { width: vp.width, height: vp.height } @@ -333,22 +310,19 @@ export function boundSelectedPageId(): string | null { return page?.id ?? null } -export function boundIsFillBrowserPage(page: Pick): boolean { - return computeIsFillBrowserPage({ +export function boundIsFocusFillFrame(page: Pick): boolean { + return computeIsFocusFillFrame({ page, - currentViewMode: uiWorkspaceViewMode, - selectedPageId: boundSelectedPageId, + focusedFrameId: uiFocusedFrameId, }) } export function boundAvailableCanvasViewport(): { width: number; height: number } { return computeAvailableCanvasViewport({ win, - currentViewMode: uiWorkspaceViewMode, currentDevtoolsOpen: uiDevtoolsOpen, currentDevtoolsWidth: uiDevtoolsWidth, toolbarHeight: layoutCache.toolbarHeight, - browserHeaderHeight: BROWSER_HEADER_HEIGHT, leftSidebarWidth: uiLeftSidebarOpen() ? LEFT_SIDEBAR_WIDTH : 0, }) } @@ -356,17 +330,15 @@ export function boundAvailableCanvasViewport(): { width: number; height: number export function boundAvailableCanvasViewportRect(): { x: number; y: number; width: number; height: number } { return computeAvailableCanvasViewportRect({ win, - currentViewMode: uiWorkspaceViewMode, currentDevtoolsOpen: uiDevtoolsOpen, currentDevtoolsWidth: uiDevtoolsWidth, toolbarHeight: layoutCache.toolbarHeight, - browserHeaderHeight: BROWSER_HEADER_HEIGHT, leftSidebarWidth: uiLeftSidebarOpen() ? LEFT_SIDEBAR_WIDTH : 0, }) } -export function boundFillBrowserViewportSize(): { width: number; height: number } { - return computeFillBrowserViewportSize({ +export function boundFocusFillViewportSize(): { width: number; height: number } { + return computeFocusFillViewportSize({ availableCanvasViewport: boundAvailableCanvasViewport, }) } @@ -376,16 +348,14 @@ export function boundEffectivePageContentSize( ): { width: number; height: number } { return computeEffectivePageContentSize({ page, - isFillBrowserPage: boundIsFillBrowserPage, - fillBrowserViewportSize: boundFillBrowserViewportSize, + isFocusFillFrame: boundIsFocusFillFrame, + focusFillViewportSize: boundFocusFillViewportSize, }) } export function boundCanvasOrigin(): { x: number; y: number } { return computeCanvasOrigin({ - currentViewMode: uiWorkspaceViewMode, toolbarHeight: layoutCache.toolbarHeight, - browserHeaderHeight: BROWSER_HEADER_HEIGHT, leftSidebarWidth: uiLeftSidebarOpen() ? LEFT_SIDEBAR_WIDTH : 0, }) } @@ -399,13 +369,11 @@ export function boundScreenBoundsForPage(page: Page) { page, effectivePageContentSize: boundEffectivePageContentSize, availableCanvasViewportRect: boundAvailableCanvasViewportRect, - currentViewMode: uiWorkspaceViewMode, - selectedPageId: boundSelectedPageId, - isFillBrowserPage: boundIsFillBrowserPage, + focusedFrameId: uiFocusedFrameId, + isFocusFillFrame: boundIsFocusFillFrame, zoom, pan, toolbarHeight: layoutCache.toolbarHeight, - browserHeaderHeight: BROWSER_HEADER_HEIGHT, chromePageGap: CHROME_PAGE_GAP, cardBorderWidth: CARD_BORDER_WIDTH, }) @@ -418,7 +386,7 @@ export function boundApplyEmulation(webContents: WebContents, presetIndex: numbe page, zoom, effectivePageContentSize: boundEffectivePageContentSize, - isFillBrowserPage: boundIsFillBrowserPage, + isFocusFillFrame: boundIsFocusFillFrame, viewportPresetForIndex, }) } diff --git a/src/main/runtime/selection-controller.ts b/src/main/runtime/selection-controller.ts index 44be65fb..08841936 100644 --- a/src/main/runtime/selection-controller.ts +++ b/src/main/runtime/selection-controller.ts @@ -1,13 +1,13 @@ import type { CanvasEntityKind, UiState } from '../../shared/types' import { + clearFocus as setUiClearFocus, devtoolsPanelTab as uiDevtoolsPanelTab, + focusedEntity as uiFocusedEntity, getUiState, selectedEntityIds as uiSelectedEntityIds, selectedGroupId as uiSelectedGroupId, - setCanvasMode as setUiCanvasMode, setDevtoolsPanelTab as setUiDevtoolsPanelTab, setSelection as setUiSelection, - workspaceViewMode as uiWorkspaceViewMode, } from '../ui-state' import { findPageById, @@ -69,8 +69,8 @@ export function resolveEntityKind(entityId: string): CanvasEntityKind { return 'frame' } -function browserSelectionAllowed(nextSelection: SelectionCommand): boolean { - return nextSelection.kind === 'single-entity' && nextSelection.entityKind === 'frame' +function focusAllowed(nextSelection: SelectionCommand): boolean { + return nextSelection.kind === 'single-entity' } function describeSelection(selection: SelectionCommand): Record | undefined { @@ -118,8 +118,9 @@ function commitSelection( const shouldSyncInspection = options?.syncInspection ?? true const shouldNotifyDevtools = options?.notifyDevtools ?? true - if (uiWorkspaceViewMode(currentUi) === 'browser' && !browserSelectionAllowed(nextSelection)) { - setUiCanvasMode() + // If focus is active and selection changes to multi/none, exit focus + if (uiFocusedEntity(currentUi) && !focusAllowed(nextSelection)) { + setUiClearFocus() } if (selectionEquals(getUiState().selection, nextSelection)) { @@ -138,7 +139,7 @@ function commitSelection( setUiSelection(nextSelection) breadcrumb('selection', nextSelection.kind, describeSelection(nextSelection)) - if (!browserSelectionAllowed(nextSelection) && uiDevtoolsPanelTab() === 'browser-devtools') { + if (!focusAllowed(nextSelection) && uiDevtoolsPanelTab() === 'browser-devtools') { setUiDevtoolsPanelTab('comments') savePreferences() } diff --git a/src/main/runtime/selection-state.ts b/src/main/runtime/selection-state.ts index 13a06920..6f5efe29 100644 --- a/src/main/runtime/selection-state.ts +++ b/src/main/runtime/selection-state.ts @@ -1,18 +1,21 @@ +import type { CanvasEntityKind } from '../../shared/types' import { pages, + pan, setHoverTarget, + zoom, } from './runtime-context' import { activeWorkspaceTabId, workspaceTabs } from './workspace-model' import { + clearFocus as setUiClearFocus, devtoolsPanelTab as uiDevtoolsPanelTab, + focusedEntity as uiFocusedEntity, pendingPlacement as uiPendingPlacement, selectedEntityIds as uiSelectedEntityIds, selectedPageIndex as uiSelectedPageIndex, - setBrowserMode as setUiBrowserMode, - setCanvasMode as setUiCanvasMode, setDevtoolsPanelTab as setUiDevtoolsPanelTab, + setFocus as setUiFocus, setPendingPlacement as setUiPendingPlacement, - workspaceViewMode as uiWorkspaceViewMode, } from '../ui-state' import { selectFrames, @@ -21,6 +24,11 @@ import { } from './selection-controller' import { cancelActive as cancelActiveInteraction } from './interaction-controller' import { layoutAllViews } from './layout-engine' +import { setPan, setZoom } from './viewport-control' +import { textEntities } from './text-entity-state' +import { fileEntities } from './file-entity-state' +import { drawingEntities } from './drawing-entity-state' +import { workspaceGroups } from './workspace-model' type ArrowDirection = 'left' | 'right' | 'up' | 'down' @@ -41,64 +49,83 @@ export function deselectAll(): void { void selectNone() } +function lookupEntityKind(entityId: string): CanvasEntityKind | null { + if (pages.some((page) => page.id === entityId)) return 'frame' + if (textEntities.some((entity) => entity.id === entityId)) return 'text' + if (fileEntities.some((entity) => entity.id === entityId)) return 'file' + if (drawingEntities.some((entity) => entity.id === entityId)) return 'drawing' + if (workspaceGroups.some((group) => group.id === entityId)) return 'group' + return null +} + /** - * Single gate for all view-mode transitions. Clears transient state - * (interaction, hover, pending placement) so nothing leaks across modes. + * Enter or switch focus to an entity. Stashes the prior camera on first entry + * so exitFocus() can restore it. Edges cannot be focused. */ -function transitionViewMode(target: 'canvas' | 'browser', frameId?: string): boolean { - // 1. Clear transient interaction state +export function setFocus(entityId: string, entityKind?: CanvasEntityKind): boolean { + const resolvedKind = entityKind ?? lookupEntityKind(entityId) + if (!resolvedKind || resolvedKind === 'edge') return false + + // Clear transient state — nothing leaks across focus transitions cancelActiveInteraction('external') setHoverTarget(null) if (uiPendingPlacement()) { setUiPendingPlacement(null) } - // 2. Perform the mode-specific transition - if (target === 'browser') { - const selectedFrameIds = uiSelectedEntityIds() - const selectedIdx = uiSelectedPageIndex(pages.map((p) => p.id)) - const currentSelectedPageId = - selectedIdx !== null && selectedIdx >= 0 && selectedIdx < pages.length - ? pages[selectedIdx].id - : null - const targetId = - frameId ?? currentSelectedPageId ?? selectedFrameIds[0] ?? pages[0]?.id ?? null - if (!targetId) return false - const page = pages.find((p) => p.id === targetId) - if (!page) return false - if (currentSelectedPageId !== targetId || selectedFrameIds.length !== 1 || selectedFrameIds[0] !== targetId) { - selectPageById(targetId) - } - setUiBrowserMode({ frameId: targetId }) - } else { - if (uiWorkspaceViewMode() === 'canvas') return false - setUiCanvasMode() + const existing = uiFocusedEntity() + // First entry stashes camera; subsequent switches keep the original stash + const priorCamera = existing?.priorCamera ?? { + zoom, + pan: { x: pan.x, y: pan.y }, } - // 3. Validate devtools panel tab — browser-devtools only valid in browser mode - if (target === 'canvas' && uiDevtoolsPanelTab() === 'browser-devtools') { - setUiDevtoolsPanelTab('comments') + // Frame focus selects the page so navigation/keyboard work transparently + if (resolvedKind === 'frame') { + const page = pages.find((p) => p.id === entityId) + if (!page) return false + const selectedFrameIds = uiSelectedEntityIds() + if (selectedFrameIds.length !== 1 || selectedFrameIds[0] !== entityId) { + selectPageById(entityId) + } } - // 4. One layout pass at the end + setUiFocus({ entityId, entityKind: resolvedKind, priorCamera }) layoutAllViews() return true } -export function setBrowserMode(frameId?: string): boolean { - return transitionViewMode('browser', frameId) -} +/** + * Exit focus and restore the camera stashed at entry. + */ +export function clearFocus(): boolean { + const existing = uiFocusedEntity() + if (!existing) return false -export function setCanvasMode(): void { - transitionViewMode('canvas') -} + cancelActiveInteraction('external') + setHoverTarget(null) -export function toggleBrowserMode(): boolean { - if (uiWorkspaceViewMode() === 'browser') { - setCanvasMode() - return false + // Restore stashed camera before clearing focus + setZoom(existing.priorCamera.zoom) + setPan(existing.priorCamera.pan.x, existing.priorCamera.pan.y) + setUiClearFocus() + + // browser-devtools panel tab no longer valid outside focus + if (uiDevtoolsPanelTab() === 'browser-devtools') { + setUiDevtoolsPanelTab('comments') } - return setBrowserMode() + + layoutAllViews() + return true +} + +/** + * Focus the selected entity, or no-op if selection is empty/multi/edge. + */ +export function focusSelectedEntity(): boolean { + const selectedIds = uiSelectedEntityIds() + if (selectedIds.length !== 1) return false + return setFocus(selectedIds[0]) } export function selectAdjacentPage(direction: ArrowDirection): boolean { diff --git a/src/main/runtime/sidebar-builder.ts b/src/main/runtime/sidebar-builder.ts index f03e1e1d..4e4fee5b 100644 --- a/src/main/runtime/sidebar-builder.ts +++ b/src/main/runtime/sidebar-builder.ts @@ -20,10 +20,11 @@ import { import { activeWorkspaceTabId, workspaceGroups } from './workspace-model' import { leftSidebarView } from './view-refs' import { + focusedEntityId as uiFocusedEntityId, leftSidebarOpen as uiLeftSidebarOpen, selectedEntityIds as uiSelectedEntityIds, selectedGroupId as uiSelectedGroupId, - workspaceViewMode as uiWorkspaceViewMode, + sidebarFilter as uiSidebarFilter, } from '../ui-state' import { textEntities } from './text-entity-state' import { fileEntities } from './file-entity-state' @@ -207,16 +208,37 @@ export function buildSidebarItems(): SidebarCanvasItem[] { return sortSidebarItems([...rootLeafItems, ...rootGroupItems]) } +function applySidebarFilter( + items: SidebarCanvasItem[], + filter: ReturnType, +): SidebarCanvasItem[] { + if (filter.kind === 'all') return items + const keep = (item: SidebarCanvasItem): SidebarCanvasItem | null => { + if (item.kind === 'group') { + const filteredChildren = item.children + .map(keep) + .filter((c): c is SidebarCanvasItem => c !== null) + if (filteredChildren.length === 0) return null + return { ...item, children: filteredChildren, entityCount: filteredChildren.length } + } + return item.kind === filter.entityKind ? item : null + } + return items.map(keep).filter((item): item is SidebarCanvasItem => item !== null) +} + export function buildLeftSidebarData(): LeftSidebarData { + const filter = uiSidebarFilter() + const items = applySidebarFilter(buildSidebarItems(), filter) return { width: uiLeftSidebarOpen() ? LEFT_SIDEBAR_WIDTH : 0, selectedEntityIds: uiSelectedEntityIds(), selectedGroupId: uiSelectedGroupId(), tabs: workspaceTabSummaries(), activeTabId: activeWorkspaceTabId, - viewMode: uiWorkspaceViewMode(), + focusedEntityId: uiFocusedEntityId(), + filter, hasFrames: pages.length > 0, - items: buildSidebarItems(), + items, } } diff --git a/src/main/runtime/tool-mode.ts b/src/main/runtime/tool-mode.ts index ae0f59ed..37081964 100644 --- a/src/main/runtime/tool-mode.ts +++ b/src/main/runtime/tool-mode.ts @@ -8,13 +8,13 @@ import { pages } from './runtime-context' import { markDirty } from './layout-dirty' import { annotationMode as uiAnnotationMode, + clearFocus as setUiClearFocus, devtoolsPanelTab as uiDevtoolsPanelTab, inspectEnabled as uiInspectEnabled, pendingPlacement as uiPendingPlacement, selectedPageIndex as uiSelectedPageIndex, clearToolMode as clearUiToolMode, setAnnotationMode as setUiAnnotationMode, - setCanvasMode as setUiCanvasMode, setDevtoolsPanelTab as setUiDevtoolsPanelTab, setInspectEnabled as setUiInspectEnabled, setPendingPlacement as setUiPendingPlacement, @@ -96,7 +96,7 @@ export function startPendingPlacement(input: { customSize: input.customSize ?? false, sourceFrameId: input.sourceFrameId, }) - setUiCanvasMode() + setUiClearFocus() setUiInspectEnabled(false, { hasPages: pages.length > 0 }) setUiAnnotationMode('off', { hasPages: pages.length > 0 }) requestLayout() diff --git a/src/main/runtime/ui-actions.ts b/src/main/runtime/ui-actions.ts index c3926dcb..f45d727b 100644 --- a/src/main/runtime/ui-actions.ts +++ b/src/main/runtime/ui-actions.ts @@ -1,12 +1,12 @@ // Facade: re-exports UI action functions. export { + clearFocus, deselectAll, + focusSelectedEntity, selectPage, - setBrowserMode, - setCanvasMode, + setFocus, setSelectedFrames, - toggleBrowserMode, } from './selection-state' export { @@ -52,7 +52,6 @@ export { export { getSelectedEntityIds, getSelectedGroupId, - selectBrowserTab, selectEntity, selectPageById, setHoverEntity, diff --git a/src/main/runtime/workspace-autosave.ts b/src/main/runtime/workspace-autosave.ts index 3cd31034..6a27e8fc 100644 --- a/src/main/runtime/workspace-autosave.ts +++ b/src/main/runtime/workspace-autosave.ts @@ -67,7 +67,6 @@ export function saveWorkspaceStore(): void { writeAllTabsAsCanvasFiles(userDataPath, record.id, record.tabs) writeWorkspaceMetaSync(userDataPath, record.id, { activeTabId: record.activeTabId, - viewMode: record.viewMode, tabs: record.tabs.map((t) => ({ id: t.id, name: t.name, diff --git a/src/main/runtime/workspace-persistence.ts b/src/main/runtime/workspace-persistence.ts index fb4c390f..74b49698 100644 --- a/src/main/runtime/workspace-persistence.ts +++ b/src/main/runtime/workspace-persistence.ts @@ -3,7 +3,6 @@ import { existsSync, mkdirSync, readFileSync, renameSync, unlinkSync, writeFileS import { join } from 'path' import type { Annotation, - BrowserTabMode, DevtoolsPanelTab, LegacyPersistedWorkspaceStore, PersistedCanvasEntity, @@ -17,7 +16,6 @@ import type { WorkspacePageSnapshot, WorkspaceSnapshot, WorkspaceTabSummary, - WorkspaceViewMode, } from '../../shared/types' import { cloneAnnotationsForPersistence, @@ -95,14 +93,12 @@ export function buildWorkspaceTabSummary( export function buildPersistedWorkspaceRecord(params: { workspaceTabs: PersistedWorkspaceTab[] activeWorkspaceTabId: string - viewMode: WorkspaceViewMode }): PersistedWorkspaceRecord { return { id: DEFAULT_WORKSPACE_ID, name: DEFAULT_WORKSPACE_NAME, updatedAt: new Date().toISOString(), activeTabId: params.activeWorkspaceTabId, - viewMode: params.viewMode, tabs: params.workspaceTabs.map((tab) => ({ ...tab, expanded: tab.expanded ?? true, @@ -220,7 +216,6 @@ export function makeEmptyWorkspaceSnapshot(params: { devtoolsOpen: false, devtoolsPanelTab: params.devtoolsPanelTab, devtoolsWidth: params.devtoolsWidth, - browserTabMode: 'frame', groups: [], edges: [], } @@ -342,7 +337,6 @@ export function buildWorkspaceSnapshot(params: { devtoolsOpen: boolean devtoolsPanelTab: DevtoolsPanelTab devtoolsWidth: number - browserTabMode: BrowserTabMode groups: WorkspaceGroup[] edges: WorkspaceEdge[] }): WorkspaceSnapshot { @@ -361,7 +355,6 @@ export function buildWorkspaceSnapshot(params: { devtoolsOpen: params.devtoolsOpen, devtoolsPanelTab: params.devtoolsPanelTab, devtoolsWidth: params.devtoolsWidth, - browserTabMode: params.browserTabMode, groups: cloneWorkspaceGroupsForSnapshot(params.groups), edges: cloneWorkspaceEdgesForSnapshot(params.edges), } @@ -471,7 +464,6 @@ export function writeWorkspaceMetaSync( workspaceId: string, meta: { activeTabId: string - viewMode?: string tabs: Array<{ id: string; name: string; updatedAt: string; expanded?: boolean }> }, ): void { @@ -485,11 +477,15 @@ export function writeWorkspaceMetaSync( export function readWorkspaceMeta( userDataPath: string, workspaceId: string, -): { activeTabId: string; viewMode?: string; tabs: Array<{ id: string; name: string; updatedAt: string; expanded?: boolean }> } | null { +): { activeTabId: string; tabs: Array<{ id: string; name: string; updatedAt: string; expanded?: boolean }> } | null { const filePath = join(workspaceDir(userDataPath, workspaceId), 'workspace-meta.json') if (!existsSync(filePath)) return null try { - return JSON.parse(readFileSync(filePath, 'utf8')) + const raw = JSON.parse(readFileSync(filePath, 'utf8')) as { + activeTabId: string + tabs: Array<{ id: string; name: string; updatedAt: string; expanded?: boolean }> + } + return { activeTabId: raw.activeTabId, tabs: raw.tabs } } catch { return null } @@ -510,7 +506,6 @@ export function migrateWorkspaceStoreToCanvasFiles( // Write workspace metadata writeWorkspaceMetaSync(userDataPath, workspace.id, { activeTabId: workspace.activeTabId, - viewMode: workspace.viewMode, tabs: workspace.tabs.map((t) => ({ id: t.id, name: t.name, @@ -559,7 +554,6 @@ export function loadWorkspaceFromCanvasFiles( name: DEFAULT_WORKSPACE_NAME, updatedAt: new Date().toISOString(), activeTabId: meta.activeTabId, - viewMode: meta.viewMode as WorkspaceViewMode | undefined, tabs, } } diff --git a/src/main/runtime/workspace-restore.ts b/src/main/runtime/workspace-restore.ts index d5b34d77..c0a14dd7 100644 --- a/src/main/runtime/workspace-restore.ts +++ b/src/main/runtime/workspace-restore.ts @@ -9,9 +9,6 @@ import { setDevtoolsWidth as setUiDevtoolsWidth, setLeftSidebarOpen as setUiLeftSidebarOpen, setDevtoolsPanelTab as setUiDevtoolsPanelTab, - setBrowserMode as setUiBrowserMode, - setCanvasMode as setUiCanvasMode, - selectedEntityId as uiSelectedEntityId, selectedGroupId as uiSelectedGroupId, createDefaultUiState, resetUiState, @@ -84,7 +81,6 @@ import { import { deselectAll, selectPage, - setBrowserMode, setSelectedFrames, } from './selection-state' import { @@ -275,16 +271,7 @@ export function restoreWorkspaceSnapshot(snapshot: WorkspaceSnapshot): boolean { commitSelectGroup(snapshot.selectedGroupId) } - // Restore browser mode — legacy snapshots may have browserTabMode 'responsive' or 'frame', - // both now just mean "browser mode targeting a frame" - if (snapshot.browserTabMode === 'frame' || snapshot.browserTabMode === 'responsive') { - const frameId = snapshot.selectedFrameId ?? uiSelectedEntityId() - if (frameId) { - setUiBrowserMode({ frameId }) - } else { - setUiCanvasMode() - } - } + // Focus state is ephemeral — snapshots never restore focus. if (snapshot.devtoolsOpen && uiSelectedPageIndex(pages.map((p) => p.id)) !== null) { toggleDevTools() @@ -328,14 +315,6 @@ export function restorePersistedWorkspace( ) const activeTab = workspaceTabs.find((tab) => tab.id === activeWorkspaceTabId) if (!activeTab) return false - if (record.viewMode === 'browser') { - const frameId = activeTab.snapshot.selectedFrameId ?? activeTab.snapshot.selectedFrameIds?.[0] - if (frameId) { - setUiBrowserMode({ frameId }) - } - } else { - setUiCanvasMode() - } applyTabState(activeTab) // Startup path: UndoManager not yet created, so this initial hydration // won't generate an undo step. initializeDocObservers() handles the diff --git a/src/main/runtime/workspace-tab-operations.ts b/src/main/runtime/workspace-tab-operations.ts index b0f00b65..d8c2794c 100644 --- a/src/main/runtime/workspace-tab-operations.ts +++ b/src/main/runtime/workspace-tab-operations.ts @@ -14,8 +14,6 @@ import { devtoolsPanelTab as uiDevtoolsPanelTab, devtoolsWidth as uiDevtoolsWidth, setDevtoolsOpen as setUiDevtoolsOpen, - setBrowserMode as setUiBrowserMode, - setCanvasMode as setUiCanvasMode, } from '../ui-state' import { withSuppressedDocSync } from './workspace-doc' import { @@ -64,10 +62,7 @@ export function applyTabState(tab: PersistedWorkspaceTab): void { ...getUiState(), selection: { kind: 'none' }, toolMode: 'select', - viewMode: - tab.snapshot.browserTabMode === 'frame' && tab.snapshot.selectedFrameId - ? { kind: 'browser', frameId: tab.snapshot.selectedFrameId } - : { kind: 'canvas' }, + focus: null, devtools: { ...getUiState().devtools, open: false, diff --git a/src/main/runtime/workspace-tabs.ts b/src/main/runtime/workspace-tabs.ts index e15a5254..bea7acc4 100644 --- a/src/main/runtime/workspace-tabs.ts +++ b/src/main/runtime/workspace-tabs.ts @@ -25,7 +25,6 @@ import { selectedEntityIds as uiSelectedEntityIds, selectedGroupId as uiSelectedGroupId, devtoolsOpen as uiDevtoolsOpen, - workspaceViewMode as uiWorkspaceViewMode, } from '../ui-state' import { buildWorkspaceTabSummary, @@ -88,7 +87,6 @@ export function workspaceSnapshot(): WorkspaceSnapshot { devtoolsOpen: uiDevtoolsOpen(), devtoolsPanelTab: uiDevtoolsPanelTab(), devtoolsWidth: uiDevtoolsWidth(), - browserTabMode: 'frame', groups: workspaceGroups, edges: workspaceEdges, }) @@ -191,7 +189,6 @@ export function buildPersistedWorkspaceRecord(): PersistedWorkspaceRecord { return createPersistedWorkspaceRecord({ workspaceTabs, activeWorkspaceTabId: activeWorkspaceTabId ?? workspaceTabs[0]!.id, - viewMode: uiWorkspaceViewMode(), }) } diff --git a/src/main/sentry-context.ts b/src/main/sentry-context.ts index 2daa0534..8d18308f 100644 --- a/src/main/sentry-context.ts +++ b/src/main/sentry-context.ts @@ -43,7 +43,7 @@ export type BreadcrumbCategory = | 'page' | 'navigation' | 'undo' - | 'view-mode' + | 'focus' | 'mcp' | 'onboarding' diff --git a/src/main/sentry.ts b/src/main/sentry.ts index da52217b..25b14cab 100644 --- a/src/main/sentry.ts +++ b/src/main/sentry.ts @@ -10,7 +10,7 @@ import { pages } from './runtime/page-runtime' import { textEntities } from './runtime/text-entity-state' import { fileEntities } from './runtime/file-entity-state' import { drawingEntities } from './runtime/drawing-entity-state' -import { workspaceViewMode } from './ui-state' +import { isFocused } from './ui-state' /** * Initialize Sentry for the Electron main process. @@ -68,7 +68,7 @@ function readAppStateTags(): Record { drawingEntities.length + workspaceGroups.length, ), - view_mode: workspaceViewMode(), + focused: isFocused() ? 'true' : 'false', } } diff --git a/src/main/ui-state.ts b/src/main/ui-state.ts index 512e4e14..355aa139 100644 --- a/src/main/ui-state.ts +++ b/src/main/ui-state.ts @@ -3,11 +3,11 @@ import type { CanvasEntityKind, CanvasSelectableTarget, DevtoolsPanelTab, + SidebarFilter, + UiFocus, UiPendingPlacement, UiSelection, UiState, - UiViewMode, - WorkspaceViewMode, } from '../shared/types' import { isAnnotationModeEnabled, isCanvasEntityKindEnabled } from '../shared/featureFlags' import { markDirty } from './runtime/layout-dirty' @@ -22,10 +22,6 @@ type SelectionInput = entityKindsById: Partial> } -type BrowserTarget = { - frameId: string -} - const DEFAULT_DEVTOOLS_WIDTH = 400 let uiState: UiState = createDefaultUiState() @@ -52,7 +48,8 @@ export function createDefaultUiState(): UiState { return { selection: { kind: 'none' }, toolMode: 'select', - viewMode: { kind: 'canvas' }, + focus: null, + sidebarFilter: { kind: 'all' }, leftSidebarOpen: true, devtools: { open: false, @@ -115,8 +112,8 @@ export function setSelection(input: SelectionInput): UiState { entityKindsById: { ...nextInput.entityKindsById }, } } - if (uiState.viewMode.kind === 'browser') { - uiState.viewMode = { kind: 'canvas' } + if (uiState.focus !== null) { + uiState.focus = null } markDirty('canvas', 'sidebar', 'toolbar', 'floating-ui', 'devtools') return getUiState() @@ -159,25 +156,33 @@ export function setAnnotationMode( return getUiState() } -export function setCanvasMode(): UiState { - if (uiState.viewMode.kind !== 'canvas') { - breadcrumb('view-mode', 'canvas') +export function setFocus(focus: UiFocus): UiState { + const prev = uiState.focus + if (!prev) { + breadcrumb('focus', `enter:${focus.entityKind}`) + } else if (prev.entityId !== focus.entityId) { + breadcrumb('focus', `switch:${focus.entityKind}`) } - uiState.viewMode = { kind: 'canvas' } + uiState.selection = { kind: 'single-entity', entityId: focus.entityId, entityKind: focus.entityKind } + uiState.focus = focus markDirty('canvas', 'sidebar', 'toolbar', 'bounds', 'pages') return getUiState() } -export function setBrowserMode(target: BrowserTarget): UiState { - if (uiState.viewMode.kind !== 'browser' || uiState.viewMode.frameId !== target.frameId) { - breadcrumb('view-mode', 'browser') - } - uiState.selection = { kind: 'single-entity', entityId: target.frameId, entityKind: 'frame' } - uiState.viewMode = { kind: 'browser', frameId: target.frameId } +export function clearFocus(): UiState { + if (uiState.focus === null) return getUiState() + breadcrumb('focus', 'exit') + uiState.focus = null markDirty('canvas', 'sidebar', 'toolbar', 'bounds', 'pages') return getUiState() } +export function setSidebarFilter(filter: SidebarFilter): UiState { + uiState.sidebarFilter = filter + markDirty('sidebar') + return getUiState() +} + export function updateSelectionForRemovedEntity(entityId: string): UiState { if (uiState.selection.kind === 'single-entity') { if (uiState.selection.entityId === entityId) { @@ -205,8 +210,8 @@ export function updateSelectionForRemovedEntity(entityId: string): UiState { : { kind: 'none' } } - if (uiState.viewMode.kind === 'browser' && uiState.viewMode.frameId === entityId) { - uiState.viewMode = { kind: 'canvas' } + if (uiState.focus?.entityId === entityId) { + uiState.focus = null } return getUiState() } @@ -260,18 +265,27 @@ export function setDevtoolsWidth(width: number): UiState { return getUiState() } -export function workspaceViewMode(ui: UiState = uiState): WorkspaceViewMode { - return ui.viewMode.kind === 'canvas' ? 'canvas' : 'browser' +export function focusedEntity(ui: UiState = uiState): UiFocus | null { + return ui.focus } -export function activeBrowserFrameId(ui: UiState = uiState): string | null { - if (ui.viewMode.kind === 'browser') return ui.viewMode.frameId - return selectedEntityId(ui) +export function focusedEntityId(ui: UiState = uiState): string | null { + return ui.focus?.entityId ?? null } -export function activeBrowserTabId(ui: UiState = uiState): string | null { - if (ui.viewMode.kind === 'canvas') return null - return ui.viewMode.frameId +/** True when any entity is focused (replaces the old browser-mode check). */ +export function isFocused(ui: UiState = uiState): boolean { + return ui.focus !== null +} + +/** Frame id being focused, or null if focus is on a non-frame or none. */ +export function focusedFrameId(ui: UiState = uiState): string | null { + if (ui.focus?.entityKind === 'frame') return ui.focus.entityId + return null +} + +export function sidebarFilter(ui: UiState = uiState): SidebarFilter { + return ui.sidebarFilter } export function selectedEntityIds(ui: UiState = uiState): string[] { @@ -308,7 +322,7 @@ export function selectedCanvasTargets(ui: UiState = uiState): CanvasSelectableTa export function selectedEntityId(ui: UiState = uiState): string | null { if (ui.selection.kind === 'single-entity') return ui.selection.entityId if (ui.selection.kind === 'multi-entity') return ui.selection.entityIds[0] ?? null - if (ui.viewMode.kind === 'browser') return ui.viewMode.frameId + if (ui.focus) return ui.focus.entityId return null } @@ -386,7 +400,20 @@ function cloneUiState(input: UiState): UiState { } : { ...input.selection }, toolMode: input.toolMode, - viewMode: { ...input.viewMode }, + focus: input.focus + ? { + entityId: input.focus.entityId, + entityKind: input.focus.entityKind, + priorCamera: { + zoom: input.focus.priorCamera.zoom, + pan: { ...input.focus.priorCamera.pan }, + }, + } + : null, + sidebarFilter: + input.sidebarFilter.kind === 'by-kind' + ? { kind: 'by-kind', entityKind: input.sidebarFilter.entityKind } + : { kind: 'all' }, leftSidebarOpen: input.leftSidebarOpen, devtools: { ...input.devtools }, overlays: { ...input.overlays }, diff --git a/src/preload/canvas-bg.ts b/src/preload/canvas-bg.ts index 22ef81ca..48bac43e 100644 --- a/src/preload/canvas-bg.ts +++ b/src/preload/canvas-bg.ts @@ -78,14 +78,14 @@ const api: CanvasBgElectronAPI = { ipcRenderer.send('canvas-click-at', { screenX, screenY }), clearAnnotateHover: () => ipcRenderer.send('canvas-clear-annotate-hover'), selectFrame: (frameId) => ipcRenderer.send('canvas-select-frame', { frameId }), - selectBrowserTab: (frameId) => ipcRenderer.send('canvas-select-browser-tab', { frameId }), - addBrowserFrame: (presetIndex) => ipcRenderer.send('add-browser-frame', presetIndex), + setFocus: (entityId, entityKind) => ipcRenderer.send('canvas-set-focus', { entityId, entityKind }), + clearFocus: () => ipcRenderer.send('canvas-clear-focus'), navigateFrame: (frameId, url) => ipcRenderer.send('canvas-navigate-frame', { frameId, url }), goBackFrame: (frameId) => ipcRenderer.send('canvas-back-frame', { frameId }), goForwardFrame: (frameId) => ipcRenderer.send('canvas-forward-frame', { frameId }), reloadFrame: (frameId) => ipcRenderer.send('canvas-reload-frame', { frameId }), setFrameCustom: (frameId) => ipcRenderer.send('canvas-set-frame-custom', { frameId }), - setBrowserSizeMode: (frameId, mode) => ipcRenderer.send('canvas-set-browser-size-mode', { frameId, mode }), + setFrameSizeMode: (frameId, mode) => ipcRenderer.send('canvas-set-frame-size-mode', { frameId, mode }), updateFrameBounds: (frameId, patch) => ipcRenderer.send('canvas-update-frame-bounds', { frameId, patch }), placePendingEntity: (canvasX, canvasY) => ipcRenderer.send('canvas-place-pending-entity', { canvasX, canvasY }), diff --git a/src/preload/left-sidebar.ts b/src/preload/left-sidebar.ts index 79b29c03..cb440b66 100644 --- a/src/preload/left-sidebar.ts +++ b/src/preload/left-sidebar.ts @@ -3,6 +3,7 @@ import type { CanvasEntityKind, LeftSidebarData, LeftSidebarElectronAPI, + SidebarFilter, ThemeData, } from '../shared/types' @@ -32,7 +33,11 @@ const api: LeftSidebarElectronAPI = { setTabExpanded: (tabId, expanded) => ipcRenderer.send('canvas-set-tab-expanded', { tabId, expanded }), setTextEditing: (active) => ipcRenderer.send('canvas-set-text-editing', { active }), - toggleBrowserMode: () => ipcRenderer.send('toolbar-toggle-browser-mode'), + setFocus: (entityId: string, entityKind: CanvasEntityKind) => + ipcRenderer.send('canvas-set-focus', { entityId, entityKind }), + clearFocus: () => ipcRenderer.send('canvas-clear-focus'), + setSidebarFilter: (filter: SidebarFilter) => + ipcRenderer.send('left-sidebar-set-filter', { filter }), getInitialData: () => ipcRenderer.invoke('get-left-sidebar-bootstrap'), onThemeChanged: (callback) => { const handler = (_event: Electron.IpcRendererEvent, data: ThemeData) => callback(data) diff --git a/src/preload/toolbar.ts b/src/preload/toolbar.ts index c2514717..289389f0 100644 --- a/src/preload/toolbar.ts +++ b/src/preload/toolbar.ts @@ -24,7 +24,8 @@ const api: ToolbarElectronAPI = { toggleAnnotateMode: () => ipcRenderer.send('toolbar-toggle-annotate'), toggleDrawMode: () => ipcRenderer.send('toolbar-toggle-draw'), toggleRegionSelectMode: () => ipcRenderer.send('toolbar-toggle-region-select'), - toggleBrowserMode: () => ipcRenderer.send('toolbar-toggle-browser-mode'), + focusSelectedEntity: () => ipcRenderer.send('toolbar-focus-selected-entity'), + exitFocus: () => ipcRenderer.send('toolbar-exit-focus'), dropdownOpen: () => ipcRenderer.send('toolbar-dropdown-open'), dropdownClose: () => ipcRenderer.send('toolbar-dropdown-close'), setTextEditing: (active) => ipcRenderer.send('canvas-set-text-editing', { active }), diff --git a/src/renderer/above-view/FloatingUiLayer.tsx b/src/renderer/above-view/FloatingUiLayer.tsx index 25d6ad4b..f4b8dd48 100644 --- a/src/renderer/above-view/FloatingUiLayer.tsx +++ b/src/renderer/above-view/FloatingUiLayer.tsx @@ -53,7 +53,7 @@ export function FloatingUiLayer({ const [delayedTextMenuId, setDelayedTextMenuId] = useState(null) const shouldQueueTextMenu = - layoutData.viewMode === 'canvas' && + layoutData.focusedEntityId === null && layoutData.interaction.kind === 'idle' && selectedTextEntity !== null @@ -116,7 +116,7 @@ export function FloatingUiLayer({ /** Predicate for whether the floating UI should be visible (drives overlayActive). */ export function hasFloatingMenu(layoutData: LayoutUpdateData): boolean { - if (layoutData.viewMode !== 'canvas') return false + if (layoutData.focusedEntityId !== null) return false if (layoutData.selectedEntityIds.length !== 1) return false const [id] = layoutData.selectedEntityIds return layoutData.entities.some( diff --git a/src/renderer/above-view/useAnnotationDrawingGestures.ts b/src/renderer/above-view/useAnnotationDrawingGestures.ts index f6c2832e..5c188fc9 100644 --- a/src/renderer/above-view/useAnnotationDrawingGestures.ts +++ b/src/renderer/above-view/useAnnotationDrawingGestures.ts @@ -70,7 +70,7 @@ export function useAnnotationDrawingGestures({ if (event.pointerType === 'mouse' && event.button !== 0) return if (event.clientY < layoutData.canvasOrigin.y) return if ( - layoutData.viewMode === 'canvas' && + layoutData.focusedEntityId === null && event.clientX < layoutData.canvasOrigin.x ) { return @@ -125,7 +125,7 @@ export function useAnnotationDrawingGestures({ layoutData.canvasOrigin.x, layoutData.canvasOrigin.y, layoutData.selectedEntityIds, - layoutData.viewMode, + layoutData.focusedEntityId, layoutRef, pendingAnnotation, setDrawingSession, diff --git a/src/renderer/canvas-bg/App.tsx b/src/renderer/canvas-bg/App.tsx index c84a3d42..fe3cdf77 100644 --- a/src/renderer/canvas-bg/App.tsx +++ b/src/renderer/canvas-bg/App.tsx @@ -15,7 +15,6 @@ import { DRAW_CURSOR } from './canvasBgConstants' import { buildSelectedFrameIdSet } from './canvasBgSelectors' import { EntityHoverProvider } from './EntityHoverProvider' import { CanvasDebugBadge, CanvasGridSurface, PlacementPreviewLayer, DragCopyPreviewLayer, CanvasEntityViewportLayer } from './CanvasGridSurface' -import { BrowserTabBar } from './BrowserTabBar' import { CanvasSelectionOutlineLayer, GroupSelectionOverlayLayer } from './CanvasSelectionLayers' import { DeviceShellLayer } from './DeviceShellLayer' import { FrameBorderLayer } from './FrameBorderLayer' @@ -103,12 +102,24 @@ export default function App({ () => layoutData.entities.filter((e): e is CanvasSceneDrawingEntity => e.kind === 'drawing'), [layoutData.entities], ) + const focusedFrameId = + layoutData.focusedEntityId && frameEntities.some((f) => f.id === layoutData.focusedEntityId) + ? layoutData.focusedEntityId + : null + // When focused on a frame with 'fill' sizeMode, only the focused frame renders + // (mirrors the old browser-mode behavior where siblings were hidden). + const focusedFrameSizeMode = useMemo( + () => focusedFrameId ? frameEntities.find((f) => f.id === focusedFrameId)?.sizeMode ?? null : null, + [focusedFrameId, frameEntities], + ) + const isFocusFilling = focusedFrameSizeMode === 'fill' const borderFrames = useMemo( - () => layoutData.viewMode === 'browser' - ? frameEntities.filter((f) => f.id === layoutData.activeBrowserTabId) + () => isFocusFilling + ? frameEntities.filter((f) => f.id === focusedFrameId) : frameEntities, - [frameEntities, layoutData.viewMode, layoutData.activeBrowserTabId], + [frameEntities, isFocusFilling, focusedFrameId], ) + const isFocused = layoutData.focusedEntityId !== null const selectedEntityIdSet = useMemo( () => buildSelectedFrameIdSet(layoutData.selectedEntityIds), [layoutData.selectedEntityIds], @@ -119,7 +130,7 @@ export default function App({ }, [layoutData.groups, layoutData.selectedGroupId]) const [delayedSelectedGroupMenuId, setDelayedSelectedGroupMenuId] = useState(null) const shouldQueueSelectedGroupMenu = - layoutData.viewMode === 'canvas' && + !isFocused && layoutData.interaction.kind === 'idle' && selectedGroupEntity !== null useEffect(() => { @@ -170,7 +181,7 @@ export default function App({ ) : null} - {layoutData.viewMode === 'canvas' && (layoutData.groups?.length ?? 0) > 0 ? ( + {!isFocused && (layoutData.groups?.length ?? 0) > 0 ? ( ) : null} - {layoutData.viewMode === 'canvas' ? ( + {!isFocused ? ( ) : null} - {layoutData.viewMode === 'browser' ? ( - ) : null}
- {layoutData.viewMode === 'canvas' && !captureMode ? ( + {!isFocused && !captureMode ? ( ) : null} - {layoutData.viewMode === 'canvas' && layoutData.presenceCursors.length > 0 ? ( + {!isFocused && layoutData.presenceCursors.length > 0 ? ( !f.useSvgDeviceShell)} - fileEntities={layoutData.viewMode === 'browser' ? [] : fileEntities} + fileEntities={isFocusFilling ? [] : fileEntities} isDark={isDark} /> - {layoutData.viewMode === 'canvas' ? ( + {!isFocusFilling ? ( ) : null} - {layoutData.viewMode === 'canvas' ? ( + {!isFocusFilling ? ( ) : null} - {layoutData.viewMode === 'canvas' && !captureMode ? ( + {!isFocusFilling && !captureMode ? ( selectedEntityIdSet.has(e.id) || e.id === hoveredEntityId || marqueePreviewIds?.has(e.id))} allTextEntities={textEntities} @@ -357,7 +367,7 @@ export default function App({ {/* Selected frame menu now renders in the floating-ui view (above frames) */} - {layoutData.viewMode === 'canvas' ? ( + {!isFocusFilling ? ( ) : null} - {layoutData.viewMode === 'canvas' ? ( + {!isFocusFilling ? ( void - onDeleteFrame: (frameId: string) => void - onRenameFrame: (frameId: string, name: string) => void - onSelectBrowserTab: (frameId: string) => void -}) { - return ( -
-
-
-
- {browserTabs.map((frame) => ( -
- onSelectBrowserTab(frame.id)} - onRename={(name) => onRenameFrame(frame.id, name)} - onDelete={() => onDeleteFrame(frame.id)} - /> -
- ))} -
-
- -
-
- ) -} diff --git a/src/renderer/canvas-bg/DeviceShellLayer.tsx b/src/renderer/canvas-bg/DeviceShellLayer.tsx index 73ee1aba..07472a6b 100644 --- a/src/renderer/canvas-bg/DeviceShellLayer.tsx +++ b/src/renderer/canvas-bg/DeviceShellLayer.tsx @@ -32,7 +32,7 @@ export function DeviceShellLayer({ fileEntities?: CanvasSceneFileEntity[] isDark: boolean }) { - const framedFrames: DeviceShellItem[] = frames.filter((f) => f.showDeviceFrame && f.browserSizeMode !== 'fill') + const framedFrames: DeviceShellItem[] = frames.filter((f) => f.showDeviceFrame && f.sizeMode !== 'fill') const framedFiles: DeviceShellItem[] = (fileEntities ?? []).filter((f) => f.showDeviceFrame) if (!framedFrames.length && !framedFiles.length) return null diff --git a/src/renderer/canvas-bg/FrameBorderLayer.tsx b/src/renderer/canvas-bg/FrameBorderLayer.tsx index 21a5c7ed..582d6fa0 100644 --- a/src/renderer/canvas-bg/FrameBorderLayer.tsx +++ b/src/renderer/canvas-bg/FrameBorderLayer.tsx @@ -21,7 +21,7 @@ type BorderItem = { deviceOrientation?: 'portrait' | 'landscape' showDeviceFrame?: boolean useSvgDeviceShell?: boolean - browserSizeMode?: string + sizeMode?: string width: number } @@ -50,7 +50,7 @@ export function FrameBorderLayer({ const fw = frame.screenWidth const fh = frame.screenHeight - const hasShell = frame.showDeviceFrame && frame.browserSizeMode !== 'fill' + const hasShell = frame.showDeviceFrame && frame.sizeMode !== 'fill' // SVG device shell handles its own borders if (hasShell && frame.useSvgDeviceShell) return null diff --git a/src/renderer/canvas-bg/SvgDeviceShellLayer.tsx b/src/renderer/canvas-bg/SvgDeviceShellLayer.tsx index bd132997..e5651fdf 100644 --- a/src/renderer/canvas-bg/SvgDeviceShellLayer.tsx +++ b/src/renderer/canvas-bg/SvgDeviceShellLayer.tsx @@ -31,7 +31,7 @@ export function SvgDeviceShellLayer({ frames: CanvasSceneFrameEntity[] isDark: boolean }) { - const framedFrames = frames.filter((f) => f.showDeviceFrame && f.browserSizeMode !== 'fill') + const framedFrames = frames.filter((f) => f.showDeviceFrame && f.sizeMode !== 'fill') if (!framedFrames.length) return null diff --git a/src/renderer/canvas-bg/canvasBgConstants.ts b/src/renderer/canvas-bg/canvasBgConstants.ts index 91ece5b7..d79c4f87 100644 --- a/src/renderer/canvas-bg/canvasBgConstants.ts +++ b/src/renderer/canvas-bg/canvasBgConstants.ts @@ -16,17 +16,14 @@ export const EMPTY_LAYOUT: LayoutUpdateData = { pan: { x: 0, y: 0 }, canvasOrigin: { x: 0, y: 44 }, entities: [], - browserTabs: [], - browserFillViewport: { width: 0, height: 0 }, + focusFillViewport: { width: 0, height: 0 }, selectedEntityIds: [], selection: [], activeSelection: null, annotationMode: 'off', annotations: [], fixProgress: {}, - viewMode: 'canvas', - activeBrowserTabId: null, - activeBrowserFrameId: null, + focusedEntityId: null, selectedGroupId: null, hover: null, interaction: { kind: 'idle' }, diff --git a/src/renderer/canvas-bg/useCanvasViewportGestures.ts b/src/renderer/canvas-bg/useCanvasViewportGestures.ts index 3c48104a..19f51984 100644 --- a/src/renderer/canvas-bg/useCanvasViewportGestures.ts +++ b/src/renderer/canvas-bg/useCanvasViewportGestures.ts @@ -71,7 +71,7 @@ export function useCanvasViewportGestures({ // Mouse left-button gestures. if (ctx.pointerType === 'mouse' && ctx.button === 0) { - if (layout.viewMode === 'browser') return null + if (layout.focusedEntityId !== null) return null // Pending-placement click: commit the placement and decline the // gesture. Returning null cleanly releases capture. @@ -145,7 +145,7 @@ export function useCanvasViewportGestures({ } // marquee if (rect.width < 4 || rect.height < 4) { - if (layout.viewMode === 'canvas') api.canvasDeselect() + if (layout.focusedEntityId === null) api.canvasDeselect() } else { api.canvasSelectInRect(screenRectToCanvasRect(rect, layout)) } @@ -203,7 +203,7 @@ export function useCanvasViewportGestures({ const handleMiddleMouseDown = (event: MouseEvent) => { const layout = layoutRef.current if (event.button !== 1) return - if (layout.viewMode === 'browser') return + if (layout.focusedEntityId !== null) return if (isOverlayUiTarget(event.target)) return if (event.clientY < layout.canvasOrigin.y) return middleDrag = { screenX: event.screenX, screenY: event.screenY } @@ -232,7 +232,7 @@ export function useCanvasViewportGestures({ // File drag/drop onto the canvas. const handleDragOver = (event: DragEvent) => { const layout = layoutRef.current - if (layout.viewMode === 'browser') return + if (layout.focusedEntityId !== null) return if (!event.dataTransfer?.types.includes('Files')) return event.preventDefault() if (event.dataTransfer) event.dataTransfer.dropEffect = 'copy' @@ -240,7 +240,7 @@ export function useCanvasViewportGestures({ const handleDrop = (event: DragEvent) => { const layout = layoutRef.current - if (layout.viewMode === 'browser') return + if (layout.focusedEntityId !== null) return if (!event.dataTransfer?.files.length) return event.preventDefault() event.stopImmediatePropagation() diff --git a/src/renderer/shared/hooks/useCanvasGlobalShortcuts.ts b/src/renderer/shared/hooks/useCanvasGlobalShortcuts.ts index b0bf5bdc..b1eec28f 100644 --- a/src/renderer/shared/hooks/useCanvasGlobalShortcuts.ts +++ b/src/renderer/shared/hooks/useCanvasGlobalShortcuts.ts @@ -68,7 +68,7 @@ export function useCanvasGlobalShortcuts(input: { if (isTypingTarget(event.target)) return false const sel = window.getSelection() if (sel && sel.toString().length > 0) return false - return layoutRef.current.viewMode === 'canvas' + return layoutRef.current.focusedEntityId === null } const handleCopy = (event: ClipboardEvent) => { diff --git a/src/renderer/toolbar/App.tsx b/src/renderer/toolbar/App.tsx index 4148e39d..c69ac2e3 100644 --- a/src/renderer/toolbar/App.tsx +++ b/src/renderer/toolbar/App.tsx @@ -32,7 +32,7 @@ export default function App({ initialTheme }: { initialTheme: ThemeData }) { currentPresetValue, hasSelection, hasFrames, - isBrowserMode, + isFocused, defaultToolActive, agentCursors, } = useToolbarState() @@ -54,7 +54,7 @@ export default function App({ initialTheme }: { initialTheme: ThemeData }) { (selection.pendingPlacementActive || annotationMode !== 'off' || inspectEnabled || - selection.viewMode === 'browser') + selection.focusedEntityId !== null) ) { event.preventDefault() if (document.activeElement instanceof HTMLElement) { @@ -64,8 +64,8 @@ export default function App({ initialTheme }: { initialTheme: ThemeData }) { if (annotationMode !== 'off' || inspectEnabled) { toolbarApi.clearToolMode() } - if (selection.viewMode === 'browser') { - toolbarApi.toggleBrowserMode() + if (selection.focusedEntityId !== null) { + toolbarApi.exitFocus() } return } @@ -77,11 +77,11 @@ export default function App({ initialTheme }: { initialTheme: ThemeData }) { } window.addEventListener('keydown', onKeyDown) return () => window.removeEventListener('keydown', onKeyDown) - }, [annotationMode, inspectEnabled, selection.viewMode]) + }, [annotationMode, inspectEnabled, selection.focusedEntityId]) const isMac = navigator.userAgent.includes('Mac') const showMultiFrameAddressBar = selection.selectionCount > 1 - const showTabsModeAddressBar = isBrowserMode && hasSelection - const showCenterActionsOnly = !showMultiFrameAddressBar && !showTabsModeAddressBar + const showFocusAddressBar = isFocused && hasSelection + const showCenterActionsOnly = !showMultiFrameAddressBar && !showFocusAddressBar return ( <> @@ -133,7 +133,7 @@ export default function App({ initialTheme }: { initialTheme: ThemeData }) { onToggleLeftSidebar={toolbarApi.toggleLeftSidebar} /> - {showTabsModeAddressBar ? ( + {showFocusAddressBar ? (
@@ -155,7 +155,7 @@ export default function App({ initialTheme }: { initialTheme: ThemeData }) {
diff --git a/src/renderer/toolbar/toolbarSections.tsx b/src/renderer/toolbar/toolbarSections.tsx index 25f7a19f..25703325 100644 --- a/src/renderer/toolbar/toolbarSections.tsx +++ b/src/renderer/toolbar/toolbarSections.tsx @@ -1,17 +1,16 @@ import type { Dispatch, SetStateAction } from 'react' import { Select } from '@base-ui/react/select' -import { Tabs } from '@base-ui/react/tabs' import { ChevronDown, ChevronLeft, ChevronRight, Frame, - LayoutTemplate, + Maximize2, MessageCircle, + Minimize2, Moon, MousePointer2, PanelRight, - PanelTop, PencilLine, Pipette, RotateCw, @@ -115,7 +114,7 @@ export function LeftActions({ interface CenterActionsProps { isDark: boolean - isBrowserMode: boolean + isFocused: boolean defaultToolActive: boolean annotationMode: AnnotationMode annotateAvailable: boolean @@ -140,7 +139,7 @@ interface CenterActionsProps { export function CenterActions({ isDark, - isBrowserMode, + isFocused, defaultToolActive, annotationMode, annotateAvailable, @@ -185,7 +184,7 @@ export function CenterActions({ - {!isBrowserMode ? ( + {!isFocused ? (
) : null} - {!isBrowserMode ? ( + {!isFocused ? ( + ) : ( + + )} - {!isFocused ? ( + {true ? (
) : null} - {!isFocused ? ( + {true ? ( - ) : ( - - )} -