ReviewOS

also looking at this

craft-native/craft

feat(prefs): a Settings… item and a preference store that keeps its types

#58
Merged glennmichael123 wants to merge feat/settings-and-prefs into main
14 files +2289 -10
docs/BRIDGE_API.mdmodified+99-0
Changes to docs/BRIDGE_API.md
@@ -9,6 +9,7 @@ The Craft JavaScript Bridge provides a seamless interface for your web applicati
99- [System Tray API](#system-tray-api)
1010- [Application Menu (macOS)](#application-menu-macos)
1111- [Global Shortcuts (macOS)](#global-shortcuts-macos)
12- [Settings and Preferences (macOS)](#settings-and-preferences-macos)
1213- [Window API](#window-api)
1314- [App API](#app-api)
1415- [TypeScript Support](#typescript-support)
@@ -367,6 +368,102 @@ unsubscribe function.
367368| `isRegistered(id): Promise<boolean>` | whether the id is known, enabled or not |
368369| `list(): Promise<Shortcut[]>` | `{ id, accelerator, key, enabled }` for each |
369370
371## Settings and Preferences (macOS)
372
373### The Settings… item
374
375craft's default App menu ships a **Settings… ⌘,** where every Mac app has it,
376between About and Hide. Choosing it — or pressing ⌘, — fires a `craft:settings:open`
377event into the page. An app only has to listen:
378
379```javascript
380window.craft.settings.onOpen(({ source }) => {
381 showSettingsPanel()
382})
383```
384
385`source` is `'menu'` for the menu item. Your own UI can reach the same handler
386rather than duplicating it:
387
388```javascript
389gearButton.onclick = () => window.craft.settings.open('toolbar')
390```
391
392That call is page-local; nothing crosses the bridge for it.
393
394It is **not** delivered as `craft:menu:action` with a reserved id, deliberately.
395An app that never calls `craft.menu.set()` should not have to subscribe to the
396menu API to catch ⌘,, and a "reserved" menu id would be indistinguishable from
397an app-declared item that happened to pick the same string.
398
399Calling `craft.menu.set()` replaces the whole bar, so the default item goes with
400it — as documented above. Declare it yourself to keep it:
401
402```javascript
403{ id: 'settings', role: 'settings', label: 'Settings…', shortcut: 'cmd+,' }
404```
405
406`role: 'settings'` (or `'preferences'`) resolves to craft's own handler, so the
407same `craft:settings:open` event arrives either way. The `id` is required —
408items without one are skipped.
409
410### `window.craft.prefs`
411
412A small preference store over the platform's own mechanism, so `defaults read`
413and a native settings pane both see what the app wrote.
414
415```javascript
416await window.craft.prefs.set('theme', 'dark')
417await window.craft.prefs.set('fontSize', 13)
418await window.craft.prefs.set('firstRunDone', true)
419
420const theme = await window.craft.prefs.get('theme', 'light')
421```
422
423| Call | What it does |
424|---|---|
425| `set(key, value)` | Write. Resolves once the value is **on disk**, not merely once the message was posted |
426| `get(key, fallback?)` | Read. `fallback` is returned for an absent key |
427| `delete(key)` | Remove. Resolves to whether it was there |
428| `clear()` | Remove every preference this app set. Resolves to how many |
429| `keys()` | Every key this app set, sorted |
430| `info()` | `{ domain, prefix, count, readCommand }` — see below |
431
432**Strings, numbers and booleans only.** Not a corner cut: the preferences API
433raises an Objective-C exception for a value that is not a property-list type,
434and craft cannot catch one, so refusing containers in the page is what makes
435that crash unreachable rather than merely unlikely. Serialise structure
436yourself:
437
438```javascript
439await craft.prefs.set('recents', JSON.stringify(list))
440const list = JSON.parse(await craft.prefs.get('recents', '[]'))
441```
442
443Refusals reject with a `code` you can branch on: `PREFS_UNSUPPORTED_VALUE`,
444`PREFS_NON_FINITE`, `PREFS_VALUE_TOO_LARGE` (8 KiB), `PREFS_BAD_KEY` (keys must
445match `/^[A-Za-z0-9][A-Za-z0-9_.-]{0,63}$/`), and `PREFS_FOREIGN_VALUE` when
446something outside craft wrote a value craft cannot represent.
447
448**Types survive the round trip.** A stored `true` reads back as `true`, not as
449`1`; an integer does not come back as a float. Passing a `fallback` also
450declares the expected type, so a value left behind by an older build of the app
451degrades to the default instead of to a surprise.
452
453**Where it goes.** Preferences land in the app's own domain — the bundle
454identifier inside a packaged `.app`, the executable's name otherwise. That
455means preferences set while developing against the shared `craft` binary are
456in a different domain from the packaged app's, and every dev-mode app shares
457one. `info()` reports which domain is live and how to read it:
458
459```javascript
460const { domain, readCommand } = await craft.prefs.info()
461// readCommand: "defaults read com.example.myapp"
462```
463
464craft namespaces its keys, so `clear()` and `keys()` cannot disturb the window
465positions and inspector state AppKit and WebKit keep in the same domain.
466
370467## Window API
371468
372469Control the application window from JavaScript.
@@ -601,6 +698,8 @@ async function onComplete() {
601698| Window Control | ✅ | ✅ | ✅ |
602699| Hide Dock Icon | ✅ | ➖ | ➖ |
603700| Global Shortcuts | ✅ | �� | �� |
701| Settings… item | ✅ | �� | �� |
702| Preferences | ✅ | �� | �� |
604703
605704✅ Implemented | �� In Progress | ➖ Not Applicable
606705