Skip to content

feat(menu): native File/View menus with Open Recent - #19

Merged
nprudhomme merged 1 commit into
ekino:mainfrom
padupuy:feat/native-file-menu
Jul 2, 2026
Merged

nprudhomme merged 1 commit into
ekino:mainfrom
padupuy:feat/native-file-menu

Conversation

@padupuy

@padupuy padupuy commented Jul 1, 2026

Copy link
Copy Markdown
Contributor

Closes #3

What

Restructures the native menu to match standard macOS apps and surfaces existing features (Print, Export PDF, theme) from the menu bar.

File

  • Open File… (⌘O)
  • Open Folder… (⇧⌘O — moved off ⌘O)
  • Open Recent ▸ (dynamic)
  • Print… (⌘P)
  • Export as PDF… (⇧⌘S)
  • Close Window

View

  • Toggle Dark Mode

How

  • Print / Export PDF / theme toggle were extracted into named functions (printDocument, exportPdf, toggleTheme) so the titlebar buttons and the new menu items share one code path — no behavior change for the buttons.
  • Open Recent is dynamic. Recents state lives in the frontend store (recentEntries, capped at 10, deduped by path, most-recent-first) and is rendered into a Rust-owned submenu via a new update_recent_menu command (state stays on the TS side per the app's conventions; Rust owns the menu). Entries are recorded on top-level opens (picker, file association, CLI arg, folder restore), not on sidebar navigation. Menu-item ids embed the path behind a fixed prefix (recent-file: / recent-folder:) so paths containing : round-trip correctly.
  • Pure list-merge logic extracted to utils.mergeRecent + unit tests.

Scope decisions (from #3)

Implemented the items that fit a read-only viewer. Deliberately deferred, happy to split into follow-up issues:

  • New Tab (⌘T) — needs a multi-document tabbing system; a feature on its own, not a menu entry.
  • Save (⌘S) — no-op for a read-only viewer; the closest equivalent (Export as PDF) is included.
  • Share — native NSSharingServicePicker, doable via objc2 (already used for PDF export) but out of this PR's scope.
  • Close Window and Quit (⌘Q) already existed.

Known follow-ups

  • Native menu labels are English-only (pre-existing convention); full menu i18n would be a separate change.
  • Recent entries pointing to deleted/moved paths aren't pruned yet.

Tests

  • npx tsc --noEmit ✅
  • cargo check ✅
  • npm test ✅ 295 passed (5 new for mergeRecent)

Not manually run in a packaged build yet.

Restructure the native menu to match standard macOS apps:

- File: Open File… (⌘O), Open Folder… (⇧⌘O), Open Recent ▸,
  Print… (⌘P), Export as PDF… (⇧⌘S), Close Window
- View: Toggle Dark Mode

Print / Export PDF / theme toggle reuse the existing frontend
handlers (extracted into named functions), so the titlebar buttons
and menu items share one code path.

Open Recent is dynamic: recents state lives in the frontend store
(recentEntries, capped at 10, deduped by path, most-recent-first)
and is pushed to a Rust-owned submenu via a new update_recent_menu
command. Entries are recorded on top-level opens (picker, file
association, CLI, restore), not on sidebar navigation. Item ids
embed the path behind a fixed prefix so paths with ':' survive.

The pure list-merge logic is extracted to utils.mergeRecent with
tests.

Deferred (documented in the PR): New Tab (needs a tabbing system),
Save (no-op for a read-only viewer), Share (native sharing picker).

Closes ekino#3
@nprudhomme
nprudhomme merged commit ab2f9ca into ekino:main Jul 2, 2026
4 checks passed
@nprudhomme nprudhomme mentioned this pull request Jul 3, 2026
7 tasks
nprudhomme added a commit that referenced this pull request Aug 13, 2026
Bump version to 0.11.0 and document the user-facing changes since 0.10.0:
native File/View menus with Open Recent (#19), fast document open with
in-memory cache, cloud-sync UX and a debug HUD (#18), and a search
highlight fix on document switch / 1->0 clear (#17).
nprudhomme added a commit that referenced this pull request Aug 13, 2026
* chore(release): v0.11.0

Bump version to 0.11.0 and document the user-facing changes since 0.10.0:
native File/View menus with Open Recent (#19), fast document open with
in-memory cache, cloud-sync UX and a debug HUD (#18), and a search
highlight fix on document switch / 1->0 clear (#17).

* fix(ui): stop transparent modal backdrops from locking the window

A modal backdrop could be left in `display: flex` while fully transparent,
covering the whole window at z-index 2000. It swallowed every click and
scroll while staying invisible, and because the close path had already
detached its listeners, the user had no way to dismiss it — the app was
unusable until restart. Reported on 0.10.0: no clicks on Open Folder, no
file selection in the sidebar, no scrolling, while the native menu bar
still worked.

The backdrops were shown and hidden through deferred callbacks. WebKit
suspends both animation frames and timers while a window is occluded, so
neither the reveal nor the hide is guaranteed to run — leaving the layer
displayed but never marked `visible`.

Make interactivity follow opacity in CSS: an overlay without `.visible`
is now click-through and hidden from the tab order and the accessibility
tree. That is the load-bearing guarantee — it holds whatever the reason
the layer got stuck, including a keyboard user reaching a destructive
button inside an invisible dialog.

Defence in depth on top of it:
- reveal synchronously (forced reflow, then class) instead of from a
  frame callback, so a dialog opened on an occluded window is still
  visible and therefore dismissable;
- track an open generation so a pending hide never acts on a backdrop a
  newer open has claimed, in both confirm-dialog and Preferences;
- focus the trap immediately, since callers now reveal before trapping.

Tests cover the two suspension modes that produce the field state:
frames never delivered, and timers never delivered.

Claude-Session: https://claude.ai/code/session_01NcJuB2iR4WEfuQVLpYqWi4

* docs(changelog): record multi-window support in 0.11.0

PR #22 landed without a changelog entry. Covers the new windows, the
native Window menu, the arrangement commands, and the switch from
broadcast to frontmost-window menu delivery.

Claude-Session: https://claude.ai/code/session_01NcJuB2iR4WEfuQVLpYqWi4

* docs(changelog): set the 0.11.0 release date

The heading still carried the date the release branch was prepared.

Claude-Session: https://claude.ai/code/session_01NcJuB2iR4WEfuQVLpYqWi4
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.

[FEATURE] Add support for native MacOS File Menu

2 participants