ReviewOS

also looking at this

craft-native/craft

docs(menu): delete the dead NativeMenus class, document the real menu API

#36
Merged glennmichael123 wants to merge fix/remove-dead-native-menus into main
3 files +188 -59
docs/BRIDGE_API.mdmodified+129-0
Changes to docs/BRIDGE_API.md
@@ -7,6 +7,7 @@ The Craft JavaScript Bridge provides a seamless interface for your web applicati
77- [Overview](#overview)
88- [Getting Started](#getting-started)
99- [System Tray API](#system-tray-api)
10- [Application Menu (macOS)](#application-menu-macos)
1011- [Window API](#window-api)
1112- [App API](#app-api)
1213- [TypeScript Support](#typescript-support)
@@ -157,6 +158,134 @@ interface MenuItem {
157158}
158159```
159160
161## Application Menu (macOS)
162
163The menu bar across the top of the screen, distinct from the tray menu above.
164macOS only — on Linux and Windows these calls are accepted and ignored.
165
166Craft installs a default bar at startup, so an app that never touches this API
167still gets the standard shortcuts. Calling `set()` replaces that bar
168**entirely** — if your app has text input anywhere, declare an Edit menu
169yourself, or Cmd+X/C/V stop reaching it.
170
171### `window.craft.menu.set(options: { menus: MenuDefinition[] }): Promise<void>`
172
173Replace the whole bar.
174
175```javascript
176await window.craft.menu.set({
177 menus: [
178 {
179 label: 'File',
180 items: [
181 { id: 'new', label: 'New', shortcut: 'cmd+n' },
182 { id: 'open', label: 'Open...', shortcut: 'cmd+o' },
183 { separator: true },
184 { role: 'close', label: 'Close Window', shortcut: 'cmd+w' },
185 ],
186 },
187 {
188 label: 'Edit',
189 items: [
190 { role: 'undo', label: 'Undo', shortcut: 'cmd+z' },
191 { role: 'redo', label: 'Redo', shortcut: 'cmd+shift+z' },
192 { separator: true },
193 { role: 'cut', label: 'Cut', shortcut: 'cmd+x' },
194 { role: 'copy', label: 'Copy', shortcut: 'cmd+c' },
195 { role: 'paste', label: 'Paste', shortcut: 'cmd+v' },
196 { role: 'selectAll', label: 'Select All', shortcut: 'cmd+a' },
197 ],
198 },
199 ],
200});
201```
202
203The returned promise resolves once the message has been posted, not once the
204menu exists — native acknowledges nothing on success. A failure arrives on the
205console rather than as a rejection.
206
207### `window.craft.menu.onAction(handler: (event: { id: string }) => void): () => void`
208
209Menu clicks arrive here. Without it there is no way to respond to a menu at all.
210Returns an unsubscribe function.
211
212```javascript
213const off = window.craft.menu.onAction(({ id }) => {
214 if (id === 'new') createDocument();
215 if (id === 'open') openDocument();
216});
217```
218
219Only items with an `id` fire this. Role items are performed by the system and
220never reach your handler.
221
222### Items and roles
223
224```typescript
225interface MenuDefinition {
226 label: string
227 items: MenuItemDefinition[]
228}
229
230interface MenuItemDefinition {
231 /** Echoed back by `onAction` when this item is clicked. */
232 id?: string
233 label?: string
234 /** Accelerator, e.g. `cmd+n`, `cmd+shift+r`. */
235 shortcut?: string
236 /** Icon name (SF Symbol, resolved through Craft's cross-platform icon table). */
237 icon?: string
238 /** Renders a divider; every other field is ignored when this is set. */
239 separator?: boolean
240 /** A standard behavior the system performs — see the table below. */
241 role?: string
242}
243```
244
245This is the whole of it. The native decoder parses exactly these fields and
246ignores anything else, so extra keys are dropped in silence rather than
247rejected. Two consequences worth knowing:
248
249- a separator is `{ separator: true }`, not `{ type: 'separator' }`
250- there are no nested submenus yet; the bar is one level deep
251
252Checkmarks and enablement are set after the fact, with `checkItem()` /
253`enableItem()`, not with a field here.
254
255**Roles.** A role item is wired to an AppKit selector through the responder
256chain, exactly as a hand-built Mac menu would be. Use one wherever the system
257already knows how to do the job:
258
259| role | performs | role | performs |
260|---|---|---|---|
261| `about` | About panel | `selectAll` | select all |
262| `hide` | hide app | `close` | close window |
263| `hideOthers` | hide other apps | `minimize` | minimize |
264| `showAll` | unhide all | `zoom` | zoom |
265| `quit` | quit | `front` | bring all to front |
266| `undo` / `redo` | undo, redo | `fullscreen` | toggle full screen |
267| `cut` / `copy` / `paste` | clipboard | `reload` | reload the webview |
268| `delete` | delete selection | `forceReload` | reload ignoring cache |
269
270Cut, copy and paste **must** be roles. A round trip through JavaScript cannot
271reach the field editor inside a text field or the WKWebView's own clipboard
272handling, so an `id` item named "Copy" will not copy anything.
273
274An unrecognized role degrades to an ordinary `id` item, which fires `onAction` —
275a newer bridge surface therefore fails soft on an older binary.
276
277### The rest of the surface
278
279| method | effect |
280|---|---|
281| `setDock({ items })` | replace the Dock menu (right-click the Dock icon) |
282| `clearDock()` | remove it |
283| `addItem(menuId, item)` | append one item to an existing menu; `menuId` is the menu's **label**, e.g. `'File'`. Pass `index` on the item to insert instead of append |
284| `removeItem(itemId)` | remove by id |
285| `enableItem(itemId)` / `disableItem(itemId)` | greyed out or not |
286| `checkItem(itemId)` / `uncheckItem(itemId)` | checkmark |
287| `setItemLabel(itemId, label)` | rename in place |
288
160289## Window API
161290
162291Control the application window from JavaScript.
docs/advanced/cross-platform.mdmodified+51-6
Changes to docs/advanced/cross-platform.md
@@ -59,7 +59,55 @@ const platformFeatures = await import(
5959
6060## Platform-Specific Features
6161
62### macOS
62### macOS: the application menu bar
63
64The top-of-screen menu bar is reached through the injected bridge, not through
65an import. It is available on `window` in any Craft window, with no setup:
66
67```typescript
68// Replaces the whole bar, including the default one Craft installs.
69await window.craft.menu.set({
70 menus: [
71 {
72 label: 'File',
73 items: [
74 { id: 'new', label: 'New', shortcut: 'cmd+n' },
75 { separator: true },
76 { role: 'close', label: 'Close Window', shortcut: 'cmd+w' },
77 ],
78 },
79 {
80 label: 'Edit',
81 items: [
82 { role: 'undo', label: 'Undo', shortcut: 'cmd+z' },
83 { separator: true },
84 { role: 'cut', label: 'Cut', shortcut: 'cmd+x' },
85 { role: 'copy', label: 'Copy', shortcut: 'cmd+c' },
86 { role: 'paste', label: 'Paste', shortcut: 'cmd+v' },
87 ],
88 },
89 ],
90})
91
92// Items with an `id` come back here when clicked. Items with a `role` do not —
93// the system performs those.
94window.craft.menu.onAction(({ id }) => {
95 if (id === 'new') openNewDocument()
96})
97```
98
99Use `role` for anything the system already knows how to do, and `id` for your
100own commands. See [Bridge API](../BRIDGE_API.md#application-menu-macos) for the
101full surface and the list of roles.
102
103### Planned platform namespaces
104
105The `craft-native/platform` namespaces below are **not implemented yet** — they
106describe the intended shape of the API, not something you can call today. The
107menu bar above and the [tray API](../BRIDGE_API.md) are the platform surfaces
108that exist now.
109
110#### macOS
63111
64112```typescript
65113import { mac } from 'craft-native/platform'
@@ -75,14 +123,11 @@ mac.touchBar.setItems([
75123 { type: 'slider', min: 0, max: 100, value: 50 },
76124])
77125
78// Menu Bar
79mac.systemMenu.setApplicationMenu(menu)
80
81126// Vibrancy
82127window.setVibrancy('under-window')
83128```
84129
85### Windows
130#### Windows
86131
87132```typescript
88133import { win } from 'craft-native/platform'
@@ -109,7 +154,7 @@ win.thumbnailToolbar.setButtons([
109154])
110155```
111156
112### Linux
157#### Linux
113158
114159```typescript
115160import { linux } from 'craft-native/platform'
packages/typescript/src/bridge/core.tsmodified+8-53
Changes to packages/typescript/src/bridge/core.ts
@@ -859,51 +859,14 @@ export function createTypedBridge<T extends Record<string, BridgeMethod<any, any
859859 }
860860}
861861
862// Native Menu System
863export interface MenuItem {
864 id: string
865 label: string
866 accelerator?: string
867 type?: 'normal' | 'separator' | 'checkbox' | 'radio'
868 checked?: boolean
869 enabled?: boolean
870 visible?: boolean
871 icon?: string
872 submenu?: MenuItem[]
873}
874
875export class NativeMenus {
876 constructor(private bridge: NativeBridge) {}
877
878 /**
879 * Set application menu
880 */
881 async setApplicationMenu(menu: MenuItem[]): Promise<void> {
882 await this.bridge.request('menu.setApplicationMenu', { menu })
883 }
884
885 /**
886 * Show context menu
887 */
888 async showContextMenu(menu: MenuItem[], position?: { x: number; y: number }): Promise<string | null> {
889 return this.bridge.request('menu.showContextMenu', { menu, position })
890 }
891
892 /**
893 * Update menu item
894 */
895 async updateMenuItem(id: string, updates: Partial<MenuItem>): Promise<void> {
896 await this.bridge.request('menu.updateMenuItem', { id, updates })
897 }
898
899 /**
900 * Listen for menu item clicks
901 */
902 onMenuClick(callback: (id: string) => void): () => void {
903 this.bridge.on('menu.click', callback)
904 return () => this.bridge.off('menu.click', callback)
905 }
906}
862// There is deliberately no menu class here. The application menu bar is a
863// fire-and-forget action bridge (`menu` / `setAppMenu`), not a request/response
864// method, and it is reached through the injected `window.craft.menu` — see
865// packages/zig/src/js/craft-bridge.js and its types in
866// packages/zig/types/craft-bridge.d.ts. A `NativeMenus` class used to sit here
867// calling `bridge.request('menu.setApplicationMenu')`, a transport no zig
868// handler serves; nothing noticed because `bridge/core` is not re-exported from
869// the package index, so it was unreachable through the public API either way.
907870
908871// Native Dialogs
909872export interface OpenDialogOptions {
@@ -1177,10 +1140,6 @@ export function resetGlobalBridge(): void {
11771140}
11781141
11791142// Export convenience instances
1180export function getMenus(): NativeMenus {
1181 return new NativeMenus(getBridge())
1182}
1183
11841143export function getDialogs(): NativeDialogs {
11851144 return new NativeDialogs(getBridge())
11861145}
@@ -1194,12 +1153,10 @@ const bridgeCore: {
11941153 BridgeError: typeof BridgeError
11951154 BridgeErrorCodes: typeof BridgeErrorCodes
11961155 createTypedBridge: typeof createTypedBridge
1197 NativeMenus: typeof NativeMenus
11981156 NativeDialogs: typeof NativeDialogs
11991157 NativeComponentBridge: typeof NativeComponentBridge
12001158 getBridge: typeof getBridge
12011159 createBridge: typeof createBridge
1202 getMenus: typeof getMenus
12031160 getDialogs: typeof getDialogs
12041161 getComponents: typeof getComponents
12051162} = {
@@ -1207,12 +1164,10 @@ const bridgeCore: {
12071164 BridgeError,
12081165 BridgeErrorCodes,
12091166 createTypedBridge,
1210 NativeMenus,
12111167 NativeDialogs,
12121168 NativeComponentBridge,
12131169 getBridge,
12141170 createBridge,
1215 getMenus,
12161171 getDialogs,
12171172 getComponents,
12181173}