From fbc0000e70b093c8d5739fb30fa9c2c297bf35a2 Mon Sep 17 00:00:00 2001 From: dietrich ayala Date: Tue, 13 Oct 2026 10:43:18 +0200 Subject: [PATCH] Specs: switcher search autofocus, switcher card close, display-relative peek/slide default sizes --- specs/peek-slide-relative-size.md | 88 ++++++++++++++++++++++++++++++ specs/switcher-search-autofocus.md | 63 +++++++++++++++++++++ specs/windows-view-card-close.md | 73 +++++++++++++++++++++++++ 3 files changed, 224 insertions(+) create mode 100644 specs/peek-slide-relative-size.md create mode 100644 specs/switcher-search-autofocus.md create mode 100644 specs/windows-view-card-close.md diff --git a/specs/peek-slide-relative-size.md b/specs/peek-slide-relative-size.md new file mode 100644 index 00000000..37f3d965 --- /dev/null +++ b/specs/peek-slide-relative-size.md @@ -0,0 +1,88 @@ +--- +enabled: true +touches: [apps/desktop/features/peeks/**, apps/desktop/features/slides/**, apps/desktop/main/window-placement.ts, apps/desktop/main/window-placement.test.ts, apps/desktop/main/ipc.ts, apps/desktop/tests/desktop/peeks.spec.ts, apps/desktop/tests/desktop/slides.spec.ts] +depends-on: [] +--- + +# Peeks and slides open at a size relative to the display when no size is set + +Peek item `36f54555`, its remaining half. Remembering a resize already ships +(`4b8e4d4e`, `69d315bc`): each peek (by window key) and slide (by edge) saves its settled +resize in the `sizes` setting, and an Options width/height edit made after a resize wins. What +is left is the default size: an item with no resize and no explicit Options size opens at the +config's 1024x768, or the 800x600 fallback, whatever the display. + +## What the code does now + +- `features/peeks/config.js` and `features/slides/config.js`: every default item carries + `width:1024, height:768`; each `itemSchema` defaults the same. Saved profiles carry these + values in their `items` rows. +- `peeks/background.js executeItem()` and `slides/background.js sizeFor()` pick the saved + resize if `resizeStillApplies`, else `item.width || 800` / `item.height || 600`, and pass the + pair to `api.window.open`. +- `main/ipc.ts`'s window-open handler maps a slide's `screenEdge` to an edge placement (peeks + use `center:true`) and calls `main/window-placement.ts computeInitialBounds`, which uses the + cursor display's work area and clamps the size to it (`clampSize`). + +## Decisions + +- Precedence at open, highest first: a saved resize that still applies; an explicit Options + width/height; the display-relative default. The first two are unchanged. +- An item's width/height of exactly 1024x768 (the old shipped default, present in nearly every + saved profile) counts as "no explicit size" when choosing the fallback. Rows are not rewritten, + so `resizeStillApplies`'s comparison against `optionsWidth`/`optionsHeight` is unaffected. +- Defaults are fractions of the opening display's work area (`wa`), computed in main where the + display is known: + - Peek: width `0.60 * wa.width`, height `0.80 * wa.height`; about 1152x830 on a 1920x1080 + display, roughly 10-15% larger each way than 1024x768. + - Slide: height `0.90 * wa.height`; width `0.35 * wa.width` with an 800 px floor so a page in a + slide stays above a common mobile breakpoint (768 px). The floor wins over "taller than wide" + on a display too short for both. + - The existing `clampSize` to the work area still applies last. +- `persistState` and `allowSound` are untouched; Peek item `92d39f76` decides their fate. + +## What changes + +- `window-placement.ts`: `computeInitialBounds` accepts an optional relative size (fractions of + the work area plus an optional minimum width), used when the open request carries no explicit + width/height. Keep it pure and platform-neutral (`apps/desktop/CLAUDE.md`, "Electron is ONE + backend"). +- `ipc.ts` window-open: passes the relative size from the open params through to placement. +- `peeks/background.js` and `slides/background.js`: when neither a saved resize nor an explicit + Options size applies, send the type's relative size instead of a pixel pair. Delete the + 800/600 fallbacks. +- `config.js` (both): remove `width`/`height` from the default items, and make the schema's + defaults empty so a new item opens at the relative default. Check the Features Manager Options + form still renders and saves an empty width/height, and that an explicit value still applies. + +## Tests to change + +- `main/window-placement.test.ts`: relative sizes resolve to the stated fractions of a given + work area; the slide width floor applies on a narrow work area; the result is still clamped to + the work area; an explicit width/height bypasses the relative size. +- `tests/desktop/peeks.spec.ts` and `slides.spec.ts`: existing placement tests keep passing + (adjust any that assert 1024x768). Add: a peek and an Up slide with no size set open at the + expected fractions of the test display's work area (allow a few px for window-manager + rounding); an item saved at 1024x768 opens at the relative size too; an item with an explicit + non-default Options size opens at that size; the slide's width is at least 800 or the work + area width, whichever is smaller. +- The existing resize-memory specs from `4b8e4d4e` / `69d315bc` keep passing unchanged. + +## Verify + +``` +yarn workspace @peek/desktop build +NODE_ENV=test ELECTRON_RUN_AS_NODE=1 npx electron --test apps/desktop/dist/main/*.test.js +NODE_ENV=test node --test apps/desktop/tests/unit/*.test.js +yarn workspace @peek/desktop test:grep "Peeks|Slides|peek|slide" +``` + +Done when the unit tallies report `fail 0`, the new tests pass, and no peek or slide spec fails +that is not recorded in `apps/desktop/tests/gate-baseline.md`. Record the `test:grep` pass/fail +count under `**Result**` in the handoff. Window sizing is display-dependent: the Linux run is +not evidence for macOS; note that under `**Result**`. + +## Working rules + +`apps/desktop/CLAUDE.md` governs the desktop app. Read it before editing. Run one command per +Bash call. diff --git a/specs/switcher-search-autofocus.md b/specs/switcher-search-autofocus.md new file mode 100644 index 00000000..96111676 --- /dev/null +++ b/specs/switcher-search-autofocus.md @@ -0,0 +1,63 @@ +--- +enabled: true +touches: [apps/desktop/features/windows/windows.js, apps/desktop/tests/desktop/switcher-search-autofocus.spec.ts] +depends-on: [] +--- + +# The window switcher opens with its search input focused + +Peek item `6d475cd9`. Opening the window switcher (command `windows`, the +`CommandOrControl+Shift+W` shortcut, or Escape's switcher fallback) shows the window list without +a caret in the search input, so typing to filter needs a click first. + +## What the code does now + +- `features/windows/background.js openWindowsView()` opens `peek://windows/windows.html` fresh + each time (`role:'switcher'`, overlay); `windows.js closeWindowsView()` destroys it, so every + open runs `windows.js init()` once. +- The search input is `` in `features/windows/windows.html`. + The only `searchInput.focus()` call is in `windows.js handleKeydown()`, on `/` or Cmd/Ctrl+F. + `init()` never focuses it. + +## What changes + +`windows.js init()`: after the first `renderWindows()`, focus the search input +(`searchInput.focus()`, the same call `handleKeydown` uses). No timer, no retry: `init()` already +runs after the DOM is ready. Keyboard navigation in `handleKeydown` (hjkl, arrows, Enter) must +keep working while the input holds focus; if a key handler today assumes the input is unfocused +(for example hjkl moving the selection), keep letters typing into the search and keep arrows and +Enter navigating, and record the choice in the handoff under `**Result**`. + +## Tests to change + +New `apps/desktop/tests/desktop/switcher-search-autofocus.spec.ts`, following +`windows-lazy-wake.spec.ts` (`createPerDescribeApp`, open with +`globalThis.__peek_test.publishCmdExecute('windows')`, `app.getWindow('windows/windows.html')`): + +1. After open, the search input is the focused element: `document.activeElement` is the + `peek-input.search-input` (or its shadow root's `activeElement` is its inner input). +2. Typing a few characters with `page.keyboard.type` lands in the search input and filters the + list, with no click first. +3. ArrowDown then Enter still activates a card, so focus in the input did not break keyboard + selection. + +Real OS focus is not observable headless; assert the page's focused element only. + +## Verify + +``` +yarn workspace @peek/desktop build +NODE_ENV=test ELECTRON_RUN_AS_NODE=1 npx electron --test apps/desktop/dist/main/*.test.js +NODE_ENV=test node --test apps/desktop/tests/unit/*.test.js +yarn workspace @peek/desktop test:grep "switcher" +``` + +Done when the unit tallies report `fail 0` and the `test:grep` run collects the three new tests +and they pass, with no new failure among the other switcher specs compared with +`apps/desktop/tests/gate-baseline.md`. Record the `test:grep` pass/fail count under `**Result**` +in the handoff. + +## Working rules + +`apps/desktop/CLAUDE.md` governs the desktop app. Read it before editing. Run one command per +Bash call. diff --git a/specs/windows-view-card-close.md b/specs/windows-view-card-close.md new file mode 100644 index 00000000..951c7da6 --- /dev/null +++ b/specs/windows-view-card-close.md @@ -0,0 +1,73 @@ +--- +enabled: true +touches: [apps/desktop/features/windows/windows.js, apps/desktop/features/windows/windows.html, apps/desktop/main/tile-preload.cts, apps/desktop/main/tile-ipc.ts, apps/desktop/main/windows.ts, apps/desktop/tests/desktop/windows-view-card-close.spec.ts] +depends-on: [switcher-search-autofocus] +--- + +# Each card in the window switcher has a close button + +Peek item `9c1c7637`. The item's premise that hidden windows are not listed is wrong: hidden +windows are listed with a "Hidden" badge. Every card, hidden or not, gets a close button. + +Decision: the card's close button closes the window for good, including a `keepLive` window that +an ordinary close (Cmd+W) would only hide. Picking a window in the switcher and pressing its close +button is an explicit request to end it, and hiding would leave the card in place marked "Hidden". + +## What the code does now + +- `features/windows/windows.js loadWindows()` loads the list once in `init()` via + `api.window.list({includeInternal:true})`; `renderWindows()` builds one `createWindowCard(win)` + `` per window (`dataset.windowId`, a "Hidden" badge when `win.visible === false`). + Clicking a card (`card-click`) shows it if hidden, sets it as the overlay focus target, and + closes the switcher. The empty state is `
` 'No windows open.' (or 'No + windows match your search.' with a query). +- Nothing refreshes the list after load; `window:closed` is not subscribed in `windows.js`. +- `api.window.close(id)` (`tile-preload.cts`, `tile:window:close` in `tile-ipc.ts`, needs + `window.manage`, which the windows tile has) calls `windows.ts closeOrHideWindow()`, which hides + `keepLive` windows. There is no force option. + +## What changes + +- `tile-preload.cts` / `tile-ipc.ts`: `api.window.close(id, { force: true })` closes the target + even when it is `keepLive`. Route it through `closeOrHideWindow` with a force flag (add one if + it has none) so hybrid page hosts and BrowserWindows take the same path; the capability check + is unchanged. Without the option, behaviour is unchanged. +- `windows.js createWindowCard`: a close button in the card header (`` or the + component library's icon button, per `apps/desktop/CLAUDE.md`'s component rule), labelled for + accessibility ("Close window"). Its click stops propagation so it never fires `card-click`, + calls `api.window.close(win.id, { force: true })`, removes the window from `state.windows`, and + calls `renderWindows()`, so the card goes away and the last close shows the empty state. The + switcher stays open. +- Keyboard: none added in this spec. + +## Tests to change + +New `apps/desktop/tests/desktop/windows-view-card-close.spec.ts`, following +`switcher-select-raises-target.spec.ts` (cards are `peek-card[data-window-id="…"]`): + +1. With two ordinary windows open, clicking one card's close button closes that window (it is + gone from main's window list) and removes its card; the other card remains and the switcher + is still open. +2. Clicking the other card's body still focuses that window and closes the switcher, without + closing the window. +3. Closing every listed card in turn leaves the switcher showing 'No windows open.' (the test + app may list windows besides the two it opened; close whatever is listed). +4. A `keepLive` window's card close button destroys the window rather than hiding it. + +## Verify + +``` +yarn workspace @peek/desktop build +NODE_ENV=test ELECTRON_RUN_AS_NODE=1 npx electron --test apps/desktop/dist/main/*.test.js +NODE_ENV=test node --test apps/desktop/tests/unit/*.test.js +yarn workspace @peek/desktop test:grep "switcher|windows-view|keepLive|keeplive" +``` + +Done when the unit tallies report `fail 0` and the four new tests pass, with no new failure in +the other collected specs compared with `apps/desktop/tests/gate-baseline.md`. Record the +`test:grep` pass/fail count under `**Result**` in the handoff. + +## Working rules + +`apps/desktop/CLAUDE.md` governs the desktop app. Read it before editing. Run one command per +Bash call. -- knotmirror 0.0.1