Replace browser mode with entity focus system#9
Draft
lklyne wants to merge 4 commits into
Draft
Conversation
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
- Escape on any page/bgView webContents exits focus (via `before-input-event` in keyboard-shortcuts.ts). Intercepts before the web page's own Escape handler, so webapps with Escape UX lose it while focused — acceptable cost for a learned fullscreen-exit reflex. - Middle-click drag while focused now clears focus before starting the pan, making pan a deliberate exit gesture per the spec. https://claude.ai/code/session_01VVNq5g2t2267Fqs8Wp4BGu
…itch on neighbor **Camera animation.** `animateCameraTo(target, duration=220ms)` in viewport-control interpolates zoom + pan with easeInOutQuart. `setFocus` computes a zoom that fits the entity into the available viewport with 64px padding (`computeFocusCamera`) and animates to it. `clearFocus` animates back to the stashed `priorCamera`. User `setZoom`/`setPan` call `cancelCameraAnimation` so manual gestures cancel any in-flight animation. **Switch focus on neighbor click.** `commitSelection` now switches focus to the newly-selected single-entity while preserving the original `priorCamera`. Clicking a dimmed neighbor while focused re-targets focus + animates camera, rather than clearing focus. Multi-selection or edge selection still exits focus entirely (also animated). **Per-entity Focus button on chrome.** Added a Maximize2 button to `FrameChromeLayer` (beside back/forward/reload/context-menu) and to `FileChromeLayer` (Actions row, always rendered, with the existing wireframe settings popover beside it). Wired via `onSetFocus` prop + `api.setFocus(id, kind)`. **Selection outline hover reveal on focused entity.** `FrameSelectionOverlay` gains a `hiddenUntilHover` mode. When rendering the selection outline for the focused frame, opacity starts at 0 and fades in on mouse enter (150ms). Keeps the frame resize-able without a persistent blue border while focused. Passed through via new `focusedEntityId` prop on `CanvasSelectionOutlineLayer`. Typecheck clean, 189/189 unit tests green. https://claude.ai/code/session_01VVNq5g2t2267Fqs8Wp4BGu
**Centering bug fix.** The focused frame's visible assembly is chrome + content stacked, but previous math centered only the content bounds — chrome (drawn via translateY(-100%)) pulled the assembly upward. - `computeFocusCamera()` now reserves `FOCUS_CHROME_INSET` (top offset + chrome height + bottom gap = 60px) at the top of the viewport. Target zoom fits the entity into the reduced height, target pan centers it in the region *below* the chrome. - `computeScreenBoundsForPage()` focus-fill branch now places content below the chrome inset (previously chrome and content both started at viewportTop, causing visual overlap). - New constants `FOCUS_CHROME_TOP_OFFSET = 8`, `FOCUS_CHROME_BOTTOM_GAP = 8` in runtime-constants.ts; derived `FOCUS_CHROME_INSET` exported from runtime-geometry.ts. **Pinned chrome positioning.** `EntityChrome.Root` now accepts a discriminated `positioning` prop: - `inline` — canvas-relative, floats above the frame via `translateY(-100%)`, controls fade on hover (unchanged behavior). - `pinned` — absolute at viewport coords with a full backdrop-blur background, fixed 44px height, controls always visible. Used for the focused entity's chrome. **FocusChromeLayer.** New component in canvas-bg that renders the single focused entity's chrome in pinned mode. Width tracks the focused frame's visible width (so fill mode → full viewport; fit mode → matches the zoomed frame). Contains: - Frames: back / forward / reload / URL bar / fill-fit toggle / context-menu / exit-focus. - Files: rename / exit-focus. - Other kinds: label + exit-focus (minimal). Inline chrome (`FrameChromeLayer`, `FileChromeLayer`) now filters out the focused entity to avoid duplicate chrome. **Toolbar cleanup.** Focus/Exit buttons removed from `RightPanelToggle`; `focusSelectedEntity` / `exitFocus` removed from `ToolbarElectronAPI` and the preload bridge; `toolbar-focus-selected-entity` / `toolbar-exit-focus` IPC handlers removed. Dead focus-address-bar branch in toolbar App.tsx removed (the focused frame's URL now lives in the pinned chrome). Escape exits focus via the existing main-process `before-input-event` intercept — no toolbar path needed. Typecheck clean, 189/189 unit tests green. https://claude.ai/code/session_01VVNq5g2t2267Fqs8Wp4BGu
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
This PR replaces the binary "browser mode" view with a more flexible entity focus system that allows focusing on any canvas entity (frames, text, files, drawings, groups) with smooth camera animations and viewport-aware layout.
Key Changes
Core Focus System
viewModewithfocus: The UI state now tracks a focused entity (with kind, id, and prior camera state) instead of a binary canvas/browser modeanimateCameraTo(): Implements smooth zoom + pan transitions with easeInOutQuart easing (220ms default) when entering/exiting focusentityCanvasBounds()andcomputeFocusCamera()functions calculate viewport-centered camera positions for any entity kindFrame Size Modes
browserSizeMode→sizeModewith expanded type:'fill' | 'fit' | 'device''fill'mode fills the viewport (browser-like behavior)'fit'and'device'modes center the entity with paddingcomputeIsFocusFillFrame()instead ofcomputeIsFillBrowserPage()UI State & Actions
setFocus(entityId, entityKind)/clearFocus()replacesetBrowserMode()/setCanvasMode()focusSelectedEntity()enters focus on the current selectionexitFocus()can restore itsetSidebarFilter()for filtering sidebar items by entity kindToolbar & IPC
toolbar-toggle-browser-modewithtoolbar-focus-selected-entityandtoolbar-exit-focusLayoutTemplate→Maximize2/Minimize2for focus enter/exitBrowserTabBarcomponent (browser-specific tab strip)Canvas Rendering
'fill'sizeMode hide sibling frames (mirrors old browser behavior)hiddenUntilHoverprop to frame selection outlines for focused entitiesGeometry & Layout
BROWSER_HEADER_HEIGHTandbrowserHeaderHeightparameters from viewport calculationscomputeCanvasOrigin()andcomputeAvailableCanvasViewportRect()(no longer mode-dependent)computeFillBrowserViewportSize()→computeFocusFillViewportSize()Keyboard & Navigation
clearFocus()in keyboard shortcuts)Type Updates
UiFocustype with{ entityId, entityKind, priorCamera }SidebarFiltertype for filtering sidebar itemsLayoutUpdateDatato usefocusedEntityIdinstead ofviewMode/activeBrowserTabIdWorkspaceViewModeandBrowserTabModetypesImplementation Details
priorCamerastashhttps://claude.ai/code/session_01VVNq5g2t2267Fqs8Wp4BGu