Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
76 changes: 76 additions & 0 deletions docs/content/spark/drag-drop.mdx
Original file line number Diff line number Diff line change
@@ -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 (
<>
<DragDropView
source={{ data: { 'application/x-my-item': JSON.stringify({ id: '42' }) } }}
sourceOperations={['copy', 'move']}
onDragEnd={event => console.log(event.accepted, event.operation)}
onError={console.error}
>
<Text>Drag this item</Text>
</DragDropView>
<DragDropView
acceptedTypes={['files', 'application/x-my-item']}
acceptedOperations={['copy', 'move']}
onDrop={({ files, data, operation, x, y }) => {
console.log({ files, data, operation, x, y });
}}
onError={console.error}
>
<Text>Drop here</Text>
</DragDropView>
</>
);
}
```

## 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).
2 changes: 1 addition & 1 deletion docs/content/spark/integrations.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
115 changes: 115 additions & 0 deletions docs/content/spark/keyboard-shortcuts.mdx
Original file line number Diff line number Diff line change
@@ -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.
2 changes: 2 additions & 0 deletions docs/content/spark/menus.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down
3 changes: 3 additions & 0 deletions docs/content/spark/meta.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,11 @@
"files",
"settings",
"menus",
"keyboard-shortcuts",
"drag-drop",
"integrations",
"ui",
"rich-text",
"ai",
"---Ship---",
"distribution",
Expand Down
6 changes: 4 additions & 2 deletions docs/content/spark/reference.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -13,18 +13,20 @@ Import application APIs from `@legendapp/spark/<feature>`. 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) |
Expand Down
Loading
Loading