feat(shell): zoom shortcuts on Windows and Linux (#1109)

Cmd/Ctrl and +/−/0 (numpad included) now zoom the app on every platform.
Windows/Linux run without a menu (`setMenu(null)`), so the shortcuts are a
renderer key binding in `WorkspaceKeyboardShortcutsService` calling a new
synchronous, preload-local bridge method `adjustZoomLevel`, which steps the
frame-bound temporary level through `webFrame.setZoomLevel` — never a
main-process `webContents.setZoomLevel`, whose per-URL entry the app's
`file://` path routing resets. Step and limits live in
`libs/shared/interfaces` (`stepZoomLevel`: 0.5 per press like Electron's
zoomIn/zoomOut roles, clamped to levels −4…6). On macOS the renderer sees the
key before the application menu, and `preventDefault()` keeps the menu role
from stepping a second time (Electron only performs the menu key equivalent
in its unhandled-keyboard-event hook).

Persistence is unchanged: the main process still reads the live level back
on close, quit and reload. The zoom E2E now drives the real shortcuts (in,
out, numpad, reset). Help dialog entries added and translated for all
locales; contract updated in docs/architecture/workspace-shell.md.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
4grayandClaude Fable 5.1 committed 2026-09-18 22:09:31 +02:00
1 parent ab239b5043
commit 67327bfd29
38 files changed
+705 -31

No files matched your search

+6 -1
View File
@@ -210,7 +210,12 @@ the lazy workspace shell feature into the initial bundle.
Use scoped aliases from `tsconfig.base.json` and expose public imports through a
library's `src/index.ts`. Do not introduce legacy bare aliases such as
`services`, `components`, `shared-interfaces`, or `database`, and avoid deep
imports unless a sub-entrypoint is explicitly configured.
imports unless a sub-entrypoint is explicitly configured. The configured
sub-entrypoints are `@iptvnator/shared/interfaces/ipc-commands` and
`@iptvnator/shared/interfaces/zoom-level`, both for the Electron preload: the
`@iptvnator/shared/interfaces` barrel pulls in `ngx-indexed-db`, which the
preload bundle must not carry, so the preload only type-imports the barrel
and value-imports those two dependency-free modules directly.
For a buildable library that has a local `package.json`, its `name` must match
the scoped alias. Nx uses that package name when rewriting buildable dependency
+41 -10
View File
@@ -407,7 +407,7 @@ Startup window mode (`Settings.startupWindowMode`, issue #1455):
`window.events.spec.ts`, and the startup-window-mode cases in
`settings.e2e.ts`.
Zoom level (Cmd/Ctrl and +/−, issue #1109):
Zoom level (Cmd/Ctrl and +/−/0, issue #1109):
1. The packaged renderer runs under `file://` with path routing. Chromium
keys per-host zoom by the FULL URL when a URL has no host, so every
@@ -424,8 +424,8 @@ Zoom level (Cmd/Ctrl and +/−, issue #1109):
stored level over the synchronous `WINDOW:GET_ZOOM_LEVEL` IPC at preload
start and applies it with `webFrame.setZoomLevel`, which installs a
TEMPORARY, frame-bound zoom level. It survives in-page navigation and
resizes, the macOS menu roles (`zoomIn`/`zoomOut`/`resetZoom`) increment
it, and `getZoomLevel()` reports it regardless of the route.
resizes, the zoom shortcuts (point 4) step it through the same call, and
`getZoomLevel()` reports it regardless of the route.
`webContents.setZoomLevel` from the main process would write the per-URL
entry and re-create the bug. When nothing is stored the preload re-applies
the current level for the same reason: entering temporary mode makes the
@@ -453,13 +453,44 @@ Zoom level (Cmd/Ctrl and +/−, issue #1109):
document's preload has already read whatever was stored. That
navigation also releases ownership until the next preload answers, so a
close mid-reload cannot save the per-URL default over the user's level.
4. Windows/Linux have no zoom shortcuts today: the window calls
`setMenu(null)` and nothing handles `zoom-changed`, so only macOS' default
application menu zooms. Persistence is platform-neutral and would pick
up any future shortcut that goes through `webContents.setZoomLevel`.
Regression coverage lives in `window-zoom-level.e2e.ts`, which measures
the rendered factor (content width ÷ `window.innerWidth`) across a
section change, a resize, a reload and a restart.
4. The shortcuts are a renderer key binding, not a native menu: the
Windows/Linux window calls `setMenu(null)`, so no accelerator could reach
it there. `WorkspaceKeyboardShortcutsService` (`libs/workspace/shell`)
listens on the document like it does for F11 and resolves the chord with
`resolveZoomShortcutAction` (`libs/portal/shared/util`): Cmd on macOS,
Ctrl elsewhere, never Alt; `+`/`=` (so `Ctrl+=` and `Ctrl+Shift+=` both
zoom in), `-`/`_`, `0`, and the numpad `+`/`-`/`0` (by `code`, since a
NumLock-off `0` reports `Insert`). Keys are matched by `event.key`, so
non-US layouts zoom with their own `+`/`-` keys. Like F11 it is not gated
by the typing-target check — browsers zoom from any focus — and a key
another handler already `preventDefault`ed is left alone. The binding
calls the synchronous, preload-local `window.electron.adjustZoomLevel`
(`adjustFrameZoomLevel` in `api/preload-zoom-level.ts`), which steps the
frame's temporary level through the same `webFrame.setZoomLevel` as the
restore and returns the level applied — never a main-process
`webContents.setZoomLevel`, which would re-create the per-URL bug. The
step and limits live in `libs/shared/interfaces/src/lib/zoom-level.util.ts`
(`stepZoomLevel`): 0.5 per press, Electron's own `zoomIn`/`zoomOut` role
step (≈10 %), clamped to levels −4…6 (≈48 %…299 %, inside Chromium's
25–500 %), off-grid levels snapping to the next grid point in the pressed
direction; `Ctrl/Cmd+0` returns to level 0. Persistence needs nothing
extra: the main process reads the live level back (point 3). On macOS the
default application menu still carries the `zoomIn`/`zoomOut`/`resetZoom`
roles, but Chromium hands a key equivalent to the web contents first and
Electron performs the menu equivalent only in
`WebContents::PlatformHandleKeyboardEvent`
(`shell/browser/api/electron_api_web_contents_mac.mm`, Electron 43.3.0),
the unhandled-keyboard-event hook — a `preventDefault`ed keydown never
gets there, so the binding keeps one press at one step. CDP-dispatched
keys (the E2E) never reach the menu at all.
Without a bridge (PWA) the browser keeps its own zoom, and the help
dialog lists the chords as Electron-only. Regression coverage:
`window-zoom-level.e2e.ts` presses the real shortcuts (in, out, numpad,
reset) and measures the rendered factor (content width ÷
`window.innerWidth`) across a section change, a resize, a reload and a
restart; key resolution and the bridge step are unit-tested in
`keyboard-shortcuts.spec.ts`, `workspace-keyboard-shortcuts.service.spec.ts`
and `preload-zoom-level.spec.ts`.
Layout integration: