From a2cf0d089339fda5c74f235612b4752d161a5b1a Mon Sep 17 00:00:00 2001 From: Jay Meistrich Date: Fri, 16 Oct 2026 10:04:48 -0700 Subject: [PATCH] docs: add Spark rich text, drag and drop, and keyboard shortcuts guides --- docs/content/spark/drag-drop.mdx | 76 ++++++++++++++ docs/content/spark/integrations.mdx | 2 +- docs/content/spark/keyboard-shortcuts.mdx | 115 ++++++++++++++++++++++ docs/content/spark/menus.mdx | 2 + docs/content/spark/meta.json | 3 + docs/content/spark/reference.mdx | 6 +- docs/content/spark/rich-text.mdx | 98 ++++++++++++++++++ docs/content/spark/ui.mdx | 2 + 8 files changed, 301 insertions(+), 3 deletions(-) create mode 100644 docs/content/spark/drag-drop.mdx create mode 100644 docs/content/spark/keyboard-shortcuts.mdx create mode 100644 docs/content/spark/rich-text.mdx diff --git a/docs/content/spark/drag-drop.mdx b/docs/content/spark/drag-drop.mdx new file mode 100644 index 00000000..54b08d8a --- /dev/null +++ b/docs/content/spark/drag-drop.mdx @@ -0,0 +1,76 @@ +--- +title: Drag and drop +description: Native drag sources and drop destinations for files, text, URLs, and custom data. +--- + +`@legendapp/spark/drag-drop` exports `DragDropView`, `getDragDropAvailability`, and payload/event types. One view can be a source, a destination, or both, and accepts ordinary React Native `ViewProps`. + +macOS 14+ (Apple Silicon) supported; Windows preview, not yet verified. Native drag interaction and Windows compilation still need acceptance for this API. + +## Sources and destinations + +```tsx +import { Text } from 'react-native'; +import { DragDropView } from '@legendapp/spark/drag-drop'; + +export function DragExample() { + return ( + <> + console.log(event.accepted, event.operation)} + onError={console.error} + > + Drag this item + + { + console.log({ files, data, operation, x, y }); + }} + onError={console.error} + > + Drop here + + + ); +} +``` + +## Payloads and negotiation + +`DragPayload` can contain several representations at once: + +| Field | Value | +| --- | --- | +| `files` | Array of absolute native paths. Local file URLs in a source are normalized. | +| `text` | A string. | +| `urls` | Array of absolute URLs. Windows sources allow one URL; multiple URLs reject with `E_UNSUPPORTED_OPTION`. | +| `data` | Map of lowercase custom MIME names to UTF-8 strings. The app owns JSON schemas and validation. | + +A source must contain at least one representation. `text/plain`, `text/uri-list`, and `application/x-spark-drag` are reserved; use the built-in fields for text and URLs. + +`sourceOperations` and `acceptedOperations` accept `copy`, `move`, and `link`, and each defaults to `['copy']`. Destinations default to `acceptedTypes={['files', 'text', 'urls']}`; custom MIME types require explicit opt-in. The first mutually supported target operation wins. Empty accepted operations or types reject all drops; `disabled` disables both directions. + +Accepted types determine drop eligibility. A payload may still include coexisting built-in representations, while custom data is restricted to requested custom types. + +## Events + +| Callback | Payload | +| --- | --- | +| `onDragEnter`, `onDrop` | `DropEvent`: payload fields plus `x`, `y`, and `operation`. | +| `onDragOver` | `DragOverEvent`: only `x`, `y`, and `operation`. | +| `onDragLeave` | No payload. | +| `onDragEnd` | `{ accepted: true, operation: 'copy' \| 'move' \| 'link' }` or `{ accepted: false, operation: 'none' }`. | + +Coordinates use logical units from the destination's top-left corner, positive downward. Acceptance describes native negotiation, not successful application persistence. For a move, coordinate persistence and source deletion in application code. + +## Availability and errors + +`getDragDropAvailability()` synchronously reports an unsupported platform or a missing native component. An unavailable host renders an ordinary `View` with the children intact. Remount after repairing the host to retry. + +`onError` receives `SparkError` for unavailable hosts, malformed native events, and native operation failures; its default is `console.error`. The view adds no error message to the UI. Invalid source/options throw during rendering and can be handled by a React error boundary. Invalid native events report through `onError` without invoking their event callback. + +See the [drag-and-drop source guide](https://github.com/LegendApp/legend-spark/blob/261e2c337905d501d86f8b38b9036feb4ed746b4/docs/drag-drop.md) and [shared error contracts](/spark/reference#shared-contracts). diff --git a/docs/content/spark/integrations.mdx b/docs/content/spark/integrations.mdx index 51a8ca19..c9f8e43f 100644 --- a/docs/content/spark/integrations.mdx +++ b/docs/content/spark/integrations.mdx @@ -9,7 +9,7 @@ description: Clipboard, notifications, system APIs, processes, databases, WebVie `/links` supplies `openURL`, `canOpenURL`, `getInitialURL`, and `addEventListener('url', listener)`. Desktop `openPath` opens a native file with its associated application. File/URL launch replay and recent history live under [/app/documents](/spark/documents). -`/drag-drop` handles generic files/text/URLs/custom MIME payloads with consumer-facing events and explicit operations/coordinates. Application-specific music/track models do not belong in the SDK. Use availability and `onError` rather than assuming every import has a native backend. +`/drag-drop` handles generic files/text/URLs/custom MIME payloads with consumer-facing events and explicit operations/coordinates. Application-specific music/track models do not belong in the SDK. Use availability and `onError` rather than assuming every import has a native backend. See [Drag and drop](/spark/drag-drop) for sources, destinations, and event contracts. ## Local notifications diff --git a/docs/content/spark/keyboard-shortcuts.mdx b/docs/content/spark/keyboard-shortcuts.mdx new file mode 100644 index 00000000..6fe50536 --- /dev/null +++ b/docs/content/spark/keyboard-shortcuts.mdx @@ -0,0 +1,115 @@ +--- +title: Keyboard shortcuts +description: Local and global accelerators, command routing, customizable bindings, and keyboard events. +--- + +Choose a shortcut surface according to its owner: + +| Import | Purpose | +| --- | --- | +| `@legendapp/spark/shortcuts` | Shortcuts in the focused application, optionally scoped to a window. | +| `@legendapp/spark/global-shortcuts` | System-wide shortcuts, including when the app is unfocused. | +| `@legendapp/spark/shortcuts/commands` | Named commands, routing, capture controls, and persisted bindings. | +| `@legendapp/spark/shortcuts/keyboard` | Low-level observation of keydown and keyup. | + +macOS 14+ (Apple Silicon) supported; Windows preview, not yet verified. Local and global registration have desktop backends; the command router and low-level keyboard backend currently require macOS. Native key dispatch, layouts, media keys, and focus transitions still need host acceptance. + +## Accelerator strings + +Menus and registered shortcuts share named accelerators such as `CmdOrCtrl+S`, `Alt+Left`, `Shift+Up`, and `F5`. `CmdOrCtrl` means Command on macOS and Control on Windows. Character shortcuts require a modifier on these surfaces. Use `Plus` for the plus key, for example `CmdOrCtrl+Plus`. + +Commands additionally allow unmodified character keys, `Home`, `End`, `PageUp`, `PageDown`, `MediaPlayPause`, `MediaNext`, and `MediaPrevious`, with explicit `Fn` and `CapsLock` modifiers. Other shortcut surfaces reject those extensions. Bindings use named strings; physical codes belong to low-level events. + +## Local and global registrations + +```ts +import { registerShortcut } from '@legendapp/spark/shortcuts'; +import { registerGlobalShortcut } from '@legendapp/spark/global-shortcuts'; + +const local = await registerShortcut('CmdOrCtrl+S', () => { + console.log('Save requested'); +}, { windowId: 'main' }); + +const global = await registerGlobalShortcut('CmdOrCtrl+Shift+K', () => { + console.log('Global shortcut requested'); +}); + +// When the owner ends: +await local.remove(); +await global.remove(); +``` + +Local shortcuts are consumed before insertion into text fields. Options are `windowId` and `repeat`; repeat defaults to `false`. Global options contain only `repeat`. Windows supports global repeat; requesting `repeat: true` on macOS rejects `E_UNSUPPORTED_OPTION`. Conflicting or reserved global shortcuts reject with `E_BUSY`. + +`getShortcutAvailability()` and `getGlobalShortcutAvailability()` report host/module availability from their respective imports. Registration failures reject. Await removal and retain a failed handle for cleanup retry; removal stops JavaScript callbacks immediately. + +To display a shortcut in an application menu, use the menu item's `shortcut` field; see [menus](/spark/menus). + +## Named commands and routing + +```ts +import { createHotkeyRouter } from '@legendapp/spark/shortcuts/commands'; + +const definitions = [ + { id: 'save', title: 'Save', defaultBindings: ['CmdOrCtrl+S'] }, +] as const; +const router = createHotkeyRouter(); +const registration = await router.register({ + definitions, + scope: { kind: 'window', windowId: 'editor' }, + handlers: { save: () => { console.log('Save requested'); } }, +}); + +await registration.setEnabled(false); +await registration.setEnabled(true); +const suspension = await router.suspend({ kind: 'window', windowId: 'editor' }); +await suspension.remove(); +await registration.remove(); +``` + +`defaultBindings` and per-command `bindings` overrides are arrays. An empty override disables a command; an omitted override uses its defaults. Registrations snapshot their configuration. Higher priority runs first: defaults are 100 for window scope and 0 for application scope. Equal priorities favor the newest registration. + +A synchronous handler returning `false` lets dispatch continue to the next Spark handler. Other synchronous results stop routing. Native consumption is decided before JavaScript runs, so returning `false` cannot send the key back to AppKit. Launch asynchronous work explicitly and handle its errors. Only definitions with `repeat: true` run on repeated keydowns; `allowExtraModifiers` opts into subset modifier matching. + +The native event's window ID determines ownership. Window changes reset pressed-key state; `setActiveWindowId` can also reset that state on app focus changes, but does not override native event ownership. `enabled` is a boolean. Await `setEnabled` for native acknowledgment. + +`router.suspend(scope?)` returns an async registration; await its `remove()` to resume. Window suspension also blocks application handlers in that window. The final command registration releases shared native observers and consumption rules. Setup, updates, and cleanup reject on failure, and cleanup can be retried on the same handle. + +### React ownership and capture + +`useRoutedHotkeys({ router, definitions, bindings, handlers, ... })` returns `loading`, `ready` with the registration, or `error` state. It uses current handlers and enabled state without replacing the registration for callback identity changes, and removes late registrations after unmount. `onError` reports setup/update failures; `onCleanupError(error, registration)` exposes failed cleanup for retry. + +`useHotkeySuspension({ active, router, scope?, onError?, onCleanupError? })` owns suspension for an effect lifetime. Both hooks share the commands import. + +`HotkeyCapture` takes `value`, `onChange`, and optional capture/error callbacks. It owns native capture only while recording, commits a named accelerator on key release, and cancels on Escape. Capture temporarily consumes monitored keys. `HotkeyBindingsSettingsContent` and `HotkeyBindingsSettingsPage` edit arrays of bindings through `definitions`, `values`, and `onChange(id, values)`; `maxBindingsPerCommand` can limit the number per command. + +## Persisting bindings + +`createHotkeyStore({ definitions, path, maxBindingsPerCommand?, storage?, debounceMs? })` comes from the commands import. Await the factory before using its Legend State `value$`, `error$`, `flush()`, and `close()`. The parent directory must exist; follow [observable persistence](/spark/settings#observable-persistence). + +The file format is: + +```json +{ "version": 1, "bindings": { "save": ["CmdOrCtrl+S"] } } +``` + +Serialization preserves portable `CmdOrCtrl`. Missing commands acquire defaults, removed IDs are discarded, and empty arrays stay empty. Invalid versions, scalar bindings, or malformed accelerators reject and preserve existing file bytes. A nonnegative integer `maxBindingsPerCommand` keeps the first N bindings. Conflicts remain visible through `getHotkeyBindingConflicts` and the settings UI rather than silently replacing another command. + +Await `flush()` to persist the current snapshot and `close()` when its owner ends. The store persists bindings; supply them to a router registration or `useRoutedHotkeys` to apply them. + +## Low-level keyboard events + +```ts +import { addKeyboardListener } from '@legendapp/spark/shortcuts/keyboard'; + +const listener = await addKeyboardListener('down', event => { + console.log(event.key, event.keyCode, event.consumed); +}, { windowIds: ['editor'] }); +await listener.remove(); +``` + +Use `'down'` or `'up'`. Events include `windowId` (or `null`), a layout-derived unmodified `key`, physical `keyCode`, a numeric `modifiers` mask, and `repeated`, `consumed`, and `captured` booleans. Window filtering applies to both event kinds. `KeyCodes`, `KeyText`, `hasModifier`, and `createModifierMask` are exported from the same path. + +Callbacks observe the native decision; their return values never consume a key. Native matching does not wait for JavaScript. `getKeyboardAvailability()` checks host/module support; registration explicitly rejects on unsupported hosts or a missing module. Removing the last registration stops monitoring and releases bridge observers. Failed removal can be retried. + +See the [commands source guide](https://github.com/LegendApp/legend-spark/blob/261e2c337905d501d86f8b38b9036feb4ed746b4/docs/commands.md) for routing and persistence details. diff --git a/docs/content/spark/menus.mdx b/docs/content/spark/menus.mdx index a7b1365a..cccf15b0 100644 --- a/docs/content/spark/menus.mdx +++ b/docs/content/spark/menus.mdx @@ -43,6 +43,8 @@ Targets bind native root menus, IDs, or macOS semantic roles. Role items dispatc ## Local and global shortcuts +See [Keyboard shortcuts](/spark/keyboard-shortcuts) for the full registration, routing, capture, and persistence guide. + ```ts import { registerShortcut } from '@legendapp/spark/shortcuts'; diff --git a/docs/content/spark/meta.json b/docs/content/spark/meta.json index 61696608..221038fd 100644 --- a/docs/content/spark/meta.json +++ b/docs/content/spark/meta.json @@ -14,8 +14,11 @@ "files", "settings", "menus", + "keyboard-shortcuts", + "drag-drop", "integrations", "ui", + "rich-text", "ai", "---Ship---", "distribution", diff --git a/docs/content/spark/reference.mdx b/docs/content/spark/reference.mdx index 3bd2f8bc..4288e076 100644 --- a/docs/content/spark/reference.mdx +++ b/docs/content/spark/reference.mdx @@ -13,18 +13,20 @@ Import application APIs from `@legendapp/spark/`. The single SDK archiv | `/app/documents` | File/URL requests, recent history, document controllers and reload watches | [Documents](/spark/documents) | | `/windows`, `/windows/macos` | Windows, displays, cursor, geometry, React roots, native macOS chrome | [Windows](/spark/windows) | | `/menus`, `/context-menu`, `/tray` | Owned app menus, native popups and tray items | [Menus](/spark/menus) | -| `/shortcuts`, `/global-shortcuts`, `/shortcuts/commands`, `/shortcuts/keyboard` | Accelerators, command routing, persisted bindings and low-level keyboard events | [Menus and shortcuts](/spark/menus) | +| `/shortcuts`, `/global-shortcuts`, `/shortcuts/commands`, `/shortcuts/keyboard` | Accelerators, command routing, persisted bindings and low-level keyboard events | [Keyboard shortcuts](/spark/keyboard-shortcuts) | | `/dialogs` | Open/save panels, message dialogs and confirmation | [Files and dialogs](/spark/files) | | `/files` | Paths, directories, byte/stream/positional IO, watches, scanning, Trash | [Files](/spark/files) | | `/settings`, `/settings/observable`, `/secure-storage` | JSON settings, Legend State persistence and selected SecureStore methods | [Settings](/spark/settings) | | `/settings/window` | Normal/virtualized settings-window composition and options | [Native UI](/spark/ui) | -| `/clipboard`, `/links`, `/drag-drop` | Clipboard formats, external URLs/paths and generic drag payloads | [Desktop integrations](/spark/integrations) | +| `/clipboard`, `/links` | Clipboard formats and external URLs/paths | [Desktop integrations](/spark/integrations) | +| `/drag-drop` | Native drag sources and drop destinations with generic payloads | [Drag and drop](/spark/drag-drop) | | `/notifications`, `/system` | Permissions/local notifications, Dock/taskbar, login startup and power | [Desktop integrations](/spark/integrations) | | `/processes` | Child processes and app-supplied helper executables | [Desktop integrations](/spark/integrations) | | `/sqlite`, `/webview` | Spark-owned database and embedded browser contracts | [Desktop integrations](/spark/integrations) | | `/audio`, `/auth-session` | Async players/media sessions and prepared browser-auth sessions | [Desktop integrations](/spark/integrations) | | `/updates` | Native whole-app update status, initiation, preferences and events | [Packaging and updates](/spark/distribution) | | `/ui`, `/ui/uniwind`, `/ui/classnames` | Common native controls and optional styling integrations | [Native UI](/spark/ui) | +| `/markdown`, `/html` | Read-only rich text, controlled inputs, and editable previews | [Rich text](/spark/rich-text) | | `/ui/search`, `/ui/sidebar`, `/ui/split-view`, `/ui/swipe`, `/ui/glass`, `/ui/symbol` | Specialized macOS views | [Native UI](/spark/ui) | | `/ai`, `/ai/codex` | Installed AI CLI execution and optional macOS Codex supervisor | [AI execution](/spark/ai) | | `/contracts`, `/diagnostics` | Shared error/availability/resource types and explicit diagnostic utilities | [Shared contracts](#shared-contracts) | diff --git a/docs/content/spark/rich-text.mdx b/docs/content/spark/rich-text.mdx new file mode 100644 index 00000000..f2932c12 --- /dev/null +++ b/docs/content/spark/rich-text.mdx @@ -0,0 +1,98 @@ +--- +title: Rich text +description: Read and edit Markdown and HTML with native text views. +--- + +Import `Markdown`, `MarkdownInput`, and `EditableMarkdown` from `@legendapp/spark/markdown`, or `Html`, `HtmlInput`, and `EditableHtml` from `@legendapp/spark/html`. Spark bundles the Enriched backends and applies their native compatibility patches during installation and native preparation; no separate backend installation is needed. + +macOS 14+ (Apple Silicon) supported; Windows preview, not yet verified. Rich text is unsupported on Windows. See [backend limits](#backend-limits) before choosing content and styles. + +## Read-only readers + +```tsx +import { Markdown } from '@legendapp/spark/markdown'; +import { Html } from '@legendapp/spark/html'; + +export function Readers() { + return ( + <> + console.log(url)} + /> + + {'

Hello

Read Spark.

'} + + + ); +} +``` + +Both readers accept these props: + +| Prop | Behavior | +| --- | --- | +| `value?: string`, `children?: string` | `value` takes precedence, including an empty string. With neither, the reader is empty. | +| `selectable?: boolean` | Enables native text selection; defaults to `true`. | +| `style` | React Native text style or style array. Markdown applies it to its container; HTML passes it to its text view. | +| `onLinkPress({ url })` | The application owns link navigation. The example logs the URL. | +| View props | Ordinary view accessibility and test props are accepted. | + +`Markdown` also accepts `markdownStyle` for text and block formatting. It renders headings, lists, links, code, and images; task checkboxes stay read-only. `Html` accepts `htmlStyle`, with element and style support determined by the backend. + +## Controlled inputs + +`MarkdownInput` and `HtmlInput` stay in editor mode. `EditableMarkdown` and `EditableHtml` show rendered content while unfocused: click to edit, then blur or press Escape to finish. + +```tsx +import { useState } from 'react'; +import { MarkdownInput, EditableMarkdown } from '@legendapp/spark/markdown'; +import { HtmlInput, EditableHtml } from '@legendapp/spark/html'; + +export function Editors() { + const [markdown, setMarkdown] = useState('Hello **Spark**'); + const [html, setHtml] = useState('

Hello Spark

'); + return ( + <> + + + + + + ); +} +``` + +All four require `value: string` and `onChangeText(value: string)`. They also accept `placeholder`, `autoFocus`, `onFocus`, `onBlur`, React Native `style`, `accessibilityLabel`, and `testID`. Changes arrive while typing as Markdown or HTML strings, including whitespace. A ref exposes `focus()` and `blur()` through `MarkdownInputRef` or `HtmlInputRef`, exported from the corresponding capability path. + +`MarkdownInput` and `EditableMarkdown` also accept `markdownStyle`. Reader props such as `selectable`, `htmlStyle`, and `onLinkPress` are not input props. + +### Formatting + +Set `formatting` to show Bold, Italic, Link, Bullets, and Numbers buttons. Supply a synchronous `onRequestLink={() => url}` returning a string or `undefined` to enable Link. On macOS, formatting also enables Cmd-B, Cmd-I, Cmd-K, Cmd-Shift-7, and Cmd-Shift-8 for those actions while the input is focused. + +### Selection and controlled values + +The editable variants retain the native input across focus changes so native hit testing places the caret. Selection positions belong to the native plain-text view, including styled text and emoji; they are not Markdown source offsets. The public inputs do not expose a controlled selection prop. + +Echoing an accepted edit back through `value` leaves native selection and undo intact. An external replacement or rejected edit may reset selection. Editors serialize supported formatting, so editing does not guarantee the original Markdown or HTML spelling survives unchanged. + +Use an opaque `style.backgroundColor` for editable previews; the reader overlay defaults to white. Reader/editor layout parity, particularly custom block styles and line heights, still needs native acceptance. + +## Backend limits + +- The macOS HTML backend supports basic inline formatting, paragraphs, headings, links, and lists. +- Editing HTML flattens nested lists. Remote image URLs round-trip without loading their images; readers show placeholders. +- The macOS backend does not apply custom `htmlStyle`, including HTML block styles. +- Markdown math and syntax highlighting depend on application Enriched build settings. + +See the [rich-text source guide](https://github.com/LegendApp/legend-spark/blob/261e2c337905d501d86f8b38b9036feb4ed746b4/docs/rich-text.md) for backend configuration and native validation details. diff --git a/docs/content/spark/ui.mdx b/docs/content/spark/ui.mdx index 14cf691f..67457650 100644 --- a/docs/content/spark/ui.mdx +++ b/docs/content/spark/ui.mdx @@ -5,6 +5,8 @@ description: Controlled inputs, native selectors, macOS composition, settings wi ## Common controls +For Markdown and HTML readers, inputs, and editable previews, see [Rich text](/spark/rich-text). + Import `Button`, `TextInput`, `Select`, and `SegmentedControl` from `@legendapp/spark/ui`: ```tsx