desktop: tray menu panels and pinnable context menus

The tray's right-click menu is drawn by the app: one tree from tray.rs
(stable submenu keys), one frameless always-on-top menu-N window per
panel rendered by tray-menu.html with the design system, placed inside
the monitor's work area. A pure panel book keeps transient and pinned
panels apart; pinned panels (pin button, Ctrl+click, middle click) are
draggable, step aside from each other and are saved in state.json
(menuPins) and restored at start. tray_update pushes the tree to the
open panels, which update in place; closed panels are destroyed. The
native menu, rendered from the same tree, stays as the fallback and on
Linux. Only menu-* windows get the menu_* commands (menu.json).

In-app context menus get the same behaviour through a pure menu stack
(ui/menu-stack.ts): pin a menu, tear off a submenu, pinned panels
survive the next menu, are draggable and re-read their entries from
live sources so data changes update them in place.

Also drops a stray diff3 marker line from CHANGELOG.md.
This commit is contained in:
CRTkafa 2026-10-24 13:44:08 +08:00
parent 445ab1adbc
commit 5236f002f1
35 changed files with 2308 additions and 123 deletions

View file

@ -490,6 +490,25 @@ The CLI and the app are one product: one version number, one set of store accoun
starts silently in the tray, without a window. Closing the window keeps it running in the tray; the tray icon
opens it, the tray menu's Quit (macOS: Cmd+Q) quits. Turn the logon entry off in the OS (Windows: Settings > Apps >
Startup). Window state never restores "visible", so a `--hidden` start stays hidden.
- **Tray menu panels** (`src-tauri/src/menu_panel.rs`, `src/traymenu/`, page `tray-menu.html`): a right click on the
tray icon opens the app's own menu instead of the OS menu. tray.rs builds one tree (`menu_model`, submenus with a
stable key such as `apps`, the same in every language); every panel (the root and each submenu) is its own
frameless, always-on-top `menu-N` window that measures itself and calls `menu_ready`, and Rust places it inside the
monitor's work area (the root away from the taskbar or menu bar, a submenu beside its parent, flipped at the edge).
The panel book (pure, `tests/menu_panel.rs`) keeps transient and pinned panels apart: a new menu, Esc, a pick or
focus leaving every menu closes the transient ones; pinned ones (pin button, Ctrl+click, middle click) stay, can be
dragged by the header, step aside from other pins and are saved in state.json (`menuPins`) and restored at start.
`tray_update` pushes the tree to the open panels (`menu-model`), which update in place; nothing polls, and a closed
panel is destroyed. Only `menu-*` windows get the `menu_*` commands (`capabilities/menu.json`, `guard_menu`). If a
panel cannot be built or never reports ready, the native menu (rendered from the same tree) goes on the tray and
pops once; Linux always uses it (its tray sends no clicks). The left-click card stays as it was. Look at the panels
without Tauri: `bunx vite` in apps/desktop and open `/tray-menu.html` (a dev-only demo on fixture data; `?still=1`
stops the ticking values).
- **Pinned context menus in the window** (`src/ui/ContextMenu.tsx`, pure stack in `src/ui/menu-stack.ts`): every menu
has a pin button; a submenu's pin button, Ctrl / Cmd + click or a middle click on a submenu row tears it off. Pinned
panels survive the next menu, are dragged by the header and re-read their entries from the newest builder the screen
registered (`useContextMenu()` / `useMenuButton()` register on every render; pass an id such as `app:<key>` when rows
can reorder), so a store read updates them in place; when the thing is gone they say so instead of closing.
- **`storebound desktop` install / update**: before the installer runs it stops the running app (only processes
started from the exact install path: `%LOCALAPPDATA%\storebound\storebound-desktop.exe`,
`storebound.app/Contents/MacOS/storebound-desktop`, the AppImage; a graceful close, then by PID after 10 s). After the