Skip to content

Replace browser mode with entity focus system#9

Draft
lklyne wants to merge 4 commits into
mainfrom
claude/browser-to-focus-mode-cF04v
Draft

Replace browser mode with entity focus system#9
lklyne wants to merge 4 commits into
mainfrom
claude/browser-to-focus-mode-cF04v

Conversation

@lklyne
Copy link
Copy Markdown
Owner

@lklyne lklyne commented Apr 19, 2026

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

  • Replaced viewMode with focus: The UI state now tracks a focused entity (with kind, id, and prior camera state) instead of a binary canvas/browser mode
  • Added animateCameraTo(): Implements smooth zoom + pan transitions with easeInOutQuart easing (220ms default) when entering/exiting focus
  • Entity bounds helpers: New entityCanvasBounds() and computeFocusCamera() functions calculate viewport-centered camera positions for any entity kind

Frame Size Modes

  • Renamed browserSizeModesizeMode with expanded type: 'fill' | 'fit' | 'device'
  • 'fill' mode fills the viewport (browser-like behavior)
  • 'fit' and 'device' modes center the entity with padding
  • Updated geometry calculations to use computeIsFocusFillFrame() instead of computeIsFillBrowserPage()

UI State & Actions

  • setFocus(entityId, entityKind) / clearFocus() replace setBrowserMode() / setCanvasMode()
  • focusSelectedEntity() enters focus on the current selection
  • Focus stashes the prior camera on first entry so exitFocus() can restore it
  • Added setSidebarFilter() for filtering sidebar items by entity kind

Toolbar & IPC

  • Replaced toolbar-toggle-browser-mode with toolbar-focus-selected-entity and toolbar-exit-focus
  • Updated toolbar icons: LayoutTemplateMaximize2 / Minimize2 for focus enter/exit
  • Removed BrowserTabBar component (browser-specific tab strip)

Canvas Rendering

  • Focused frames with 'fill' sizeMode hide sibling frames (mirrors old browser behavior)
  • Added hiddenUntilHover prop to frame selection outlines for focused entities
  • Updated device shell rendering to respect focus state instead of view mode

Geometry & Layout

  • Removed BROWSER_HEADER_HEIGHT and browserHeaderHeight parameters from viewport calculations
  • Simplified computeCanvasOrigin() and computeAvailableCanvasViewportRect() (no longer mode-dependent)
  • Renamed computeFillBrowserViewportSize()computeFocusFillViewportSize()

Keyboard & Navigation

  • Escape key now exits focus (via clearFocus() in keyboard shortcuts)
  • Arrow navigation works transparently within focused frames
  • Focus transitions clear transient state (hover, pending placement, interactions)

Type Updates

  • Added UiFocus type with { entityId, entityKind, priorCamera }
  • Added SidebarFilter type for filtering sidebar items
  • Updated LayoutUpdateData to use focusedEntityId instead of viewMode / activeBrowserTabId
  • Removed WorkspaceViewMode and BrowserTabMode types

Implementation Details

  • Camera animation uses a token-based cancellation system to prevent race conditions
  • Focus state is persisted in snapshots via the priorCamera stash
  • Sidebar filtering is applied post-build based on the current filter setting
  • Device frame visibility logic now checks focus state instead of view mode

https://claude.ai/code/session_01VVNq5g2t2267Fqs8Wp4BGu

claude added 4 commits April 18, 2026 18:47
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
@lklyne lklyne marked this pull request as draft April 23, 2026 03:14
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants