diff --git a/apps/yaak-client/components/Settings/SettingsHotkeys.tsx b/apps/yaak-client/components/Settings/SettingsHotkeys.tsx index 1d8c82f3..14709ec2 100644 --- a/apps/yaak-client/components/Settings/SettingsHotkeys.tsx +++ b/apps/yaak-client/components/Settings/SettingsHotkeys.tsx @@ -124,7 +124,7 @@ export function SettingsHotkeys() { { const newHotkeys = { ...settings.hotkeys }; diff --git a/apps/yaak-client/hooks/useHotKey.ts b/apps/yaak-client/hooks/useHotKey.ts index f0efe8d6..237786b5 100644 --- a/apps/yaak-client/hooks/useHotKey.ts +++ b/apps/yaak-client/hooks/useHotKey.ts @@ -112,9 +112,12 @@ export const hotkeysAtom = atom((get) => { // Merge default hotkeys with custom hotkeys from settings // Custom hotkeys override defaults for the same action // An empty array means the hotkey is intentionally disabled - const merged: Record = { ...defaultHotkeys }; + const merged: Partial> = {}; + for (const action of hotkeyActions) { + merged[action] = defaultHotkeys[action]; + } for (const [action, keys] of Object.entries(customHotkeys)) { - if (action in defaultHotkeys && Array.isArray(keys)) { + if (action in merged && Array.isArray(keys)) { merged[action as HotkeyAction] = keys; } } @@ -122,7 +125,7 @@ export const hotkeysAtom = atom((get) => { }); /** Helper function to get current hotkeys from the store */ -function getHotkeys(): Record { +function getHotkeys(): Partial> { return jotaiStore.get(hotkeysAtom); } @@ -165,16 +168,25 @@ const layoutInsensitiveKeys = [ "Space", ]; +/** Zoom is the browser's own on these keys, so the app has no such action there. */ +const ZOOM_ACTIONS: HotkeyAction[] = ["app.zoom_in", "app.zoom_out", "app.zoom_reset"]; + +/** + * The actions this host actually has. An action left out of here has no keys in + * `hotkeysAtom`, so it never matches and never claims the keystroke. + */ export const hotkeyActions: HotkeyAction[] = ( Object.keys(defaultHotkeys) as (keyof typeof defaultHotkeys)[] -).sort((a, b) => { - const scopeA = a.split(".")[0] || ""; - const scopeB = b.split(".")[0] || ""; - if (scopeA !== scopeB) { - return scopeA.localeCompare(scopeB); - } - return hotkeyLabels[a].localeCompare(hotkeyLabels[b]); -}); +) + .filter((a) => platform.capabilities.interfaceZoom || !ZOOM_ACTIONS.includes(a)) + .sort((a, b) => { + const scopeA = a.split(".")[0] || ""; + const scopeB = b.split(".")[0] || ""; + if (scopeA !== scopeB) { + return scopeA.localeCompare(scopeB); + } + return hotkeyLabels[a].localeCompare(hotkeyLabels[b]); + }); export type HotKeyOptions = { enable?: boolean | (() => boolean); diff --git a/packages/platform/src/tauri/index.ts b/packages/platform/src/tauri/index.ts index 271ed37f..3db70afa 100644 --- a/packages/platform/src/tauri/index.ts +++ b/packages/platform/src/tauri/index.ts @@ -59,6 +59,7 @@ const ALL_CAPABILITIES: PlatformCapabilities = { timeline: true, multiWindow: true, windowChrome: true, + interfaceZoom: true, plugins: true, encryption: true, updater: true, diff --git a/packages/platform/src/types.ts b/packages/platform/src/types.ts index de077348..2969d301 100644 --- a/packages/platform/src/types.ts +++ b/packages/platform/src/types.ts @@ -262,6 +262,11 @@ export interface PlatformCapabilities { * chrome should be reserved or drawn. */ windowChrome: boolean; + /** + * The app zooms its own interface, and so owns Cmd/Ctrl `+`, `-` and `0`. + * False in a browser, where those keys are already the browser's. + */ + interfaceZoom: boolean; /** The plugin runtime. */ plugins: boolean; /** Workspace encryption backed by a key the host keeps. */ diff --git a/packages/platform/src/web/README.md b/packages/platform/src/web/README.md index c80cd97d..5bdae5aa 100644 --- a/packages/platform/src/web/README.md +++ b/packages/platform/src/web/README.md @@ -129,7 +129,10 @@ Reported honestly, so callers gate on the question rather than on the host: | True | False | | --- | --- | -| `cookieJar` (the jar stores and edits here; only filling it needs the sender) | `grpc`, `websocket`, `git`, `sync`, `tlsOptions`, `localFiles`, `timeline`, `multiWindow`, `windowChrome`, `plugins`, `encryption`, `updater`, `clipboardRead`, `systemFonts`, `license` | +| `cookieJar` (the jar stores and edits here; only filling it needs the sender) | `grpc`, `websocket`, `git`, `sync`, `tlsOptions`, `localFiles`, `timeline`, `multiWindow`, `windowChrome`, `interfaceZoom`, `plugins`, `encryption`, `updater`, `clipboardRead`, `systemFonts`, `license` | + +`interfaceZoom: false` leaves Cmd/Ctrl `+`, `-` and `0` to the browser instead +of swallowing them, and drops those three rows from the hotkeys screen. `multiWindow: false` means the host cannot open a *second window* on demand — what `cmd_new_child_window` does for Settings and workspace switching. It is not diff --git a/packages/platform/src/web/index.ts b/packages/platform/src/web/index.ts index 530ae4e4..77cac0e1 100644 --- a/packages/platform/src/web/index.ts +++ b/packages/platform/src/web/index.ts @@ -52,6 +52,9 @@ function capabilitiesFor(): PlatformCapabilities { // The browser draws the frame around the page. There are no traffic lights // to leave room for and no window controls to draw. windowChrome: false, + // The browser already zooms the page, on the same keys, and remembers it + // per site. The app stays out of the way. + interfaceZoom: false, plugins: false, encryption: false, updater: false,