@vielzeug/codex 3.1.0 → 26.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/data/catalog.json +124 -36
  2. package/data/llms-full.txt +2335 -679
  3. package/data/llms.txt +5 -3
  4. package/data/manifest.json +1 -1
  5. package/data/packages/arsenal.json +30 -6
  6. package/data/packages/assay.json +11 -19
  7. package/data/packages/clockwork.json +30 -27
  8. package/data/packages/codex.json +4 -4
  9. package/data/packages/conduit.json +33 -34
  10. package/data/packages/courier.json +15 -14
  11. package/data/packages/dnd.json +2 -2
  12. package/data/packages/familiar.json +24 -3
  13. package/data/packages/flux.json +9 -6
  14. package/data/packages/focus.json +16 -6
  15. package/data/packages/gesture.json +10 -4
  16. package/data/packages/herald.json +7 -8
  17. package/data/packages/illusionist.json +1 -1
  18. package/data/packages/keymap.json +11 -7
  19. package/data/packages/ledger.json +25 -25
  20. package/data/packages/lingua.json +2 -2
  21. package/data/packages/mesh.json +29 -31
  22. package/data/packages/necromancer.json +3 -2
  23. package/data/packages/orbit.json +4 -3
  24. package/data/packages/ore.json +4 -3
  25. package/data/packages/postmaster.json +24 -24
  26. package/data/packages/pulse.json +28 -28
  27. package/data/packages/refine.json +6 -4
  28. package/data/packages/ripple.json +2 -2
  29. package/data/packages/sandbox.json +2 -2
  30. package/data/packages/scout.json +13 -7
  31. package/data/packages/scroll.json +7 -7
  32. package/data/packages/sentinel.json +26 -15
  33. package/data/packages/sigil.json +3 -3
  34. package/data/packages/spell.json +25 -30
  35. package/data/packages/tandem.json +29 -0
  36. package/data/packages/tavern.json +23 -0
  37. package/data/packages/tempo.json +44 -42
  38. package/data/packages/vault.json +9 -19
  39. package/data/packages/ward.json +26 -29
  40. package/data/packages/wayfinder.json +20 -13
  41. package/data/refine.json +4002 -3266
  42. package/data/search.json +198 -163
  43. package/dist/index.d.ts +2 -3
  44. package/dist/index.js +2 -3
  45. package/dist/index.js.map +1 -1
  46. package/package.json +5 -12
  47. package/skills/vielzeug/SKILL.md +2 -0
  48. package/dist/advanced.d.ts +0 -12
  49. package/dist/advanced.js +0 -13
  50. package/dist/advanced.js.map +0 -1
@@ -1,9 +1,9 @@
1
1
  {
2
- "apiSource": "export { toAsyncIterable } from './async.js';\nexport { fromStore, fromSubscribe, stream } from './core.js';\nexport { FluxCapacityError, FluxEmptyError, FluxError, FluxTimeoutError } from './errors.js';\nexport { combineLatest, concat, merge } from './operators/combination.js';\nexport type { TimerOptions } from './operators/creation.js';\nexport { from, fromEvent, interval, of, timer } from './operators/creation.js';\nexport { debounce, take, takeUntil, timeout } from './operators/filtering.js';\nexport type { FlattenOptions } from './operators/transformation.js';\nexport { concatMap, filter, map, mergeMap, scan, switchMap } from './operators/transformation.js';\nexport type { RetryOptions, ToArrayOptions, ValueOptions } from './operators/utility.js';\nexport { first, last, retry, toArray } from './operators/utility.js';\nexport { pipe } from './pipe.js';\nexport type {\n AsyncIterableOptions,\n Observer,\n Operator,\n OverflowPolicy,\n Producer,\n Sink,\n Stream,\n SubscribeOptions,\n Subscription,\n Teardown,\n} from './types.js';\n",
2
+ "apiSource": "export { toAsyncIterable } from './async.js';\nexport type { Channel, ChannelOptions } from './channel.js';\nexport { createChannel } from './channel.js';\nexport { fromStore, fromSubscribe, stream } from './core.js';\nexport { FluxCapacityError, FluxEmptyError, FluxError, FluxTimeoutError } from './errors.js';\nexport { combineLatest, concat, merge } from './operators/combination.js';\nexport type { TimerOptions } from './operators/creation.js';\nexport { from, fromEvent, interval, of, timer } from './operators/creation.js';\nexport { debounce, take, takeUntil, timeout } from './operators/filtering.js';\nexport type { FlattenOptions } from './operators/transformation.js';\nexport { concatMap, filter, map, mergeMap, scan, switchMap } from './operators/transformation.js';\nexport type { RetryOptions, ToArrayOptions, ValueOptions } from './operators/utility.js';\nexport { first, last, retry, toArray } from './operators/utility.js';\nexport { pipe } from './pipe.js';\nexport type {\n AsyncIterableOptions,\n Observer,\n Operator,\n OverflowPolicy,\n Producer,\n Sink,\n Stream,\n SubscribeOptions,\n Subscription,\n Teardown,\n} from './types.js';\n",
3
3
  "docs": {
4
4
  "index": "---\ntitle: Flux — Explicit push streams for TypeScript\ndescription: Reusable push streams with subscription-owned cancellation, bounded buffering, channels, and dependency-free structural bridges.\npackage: flux\ncategory: reactive\nkeywords: [streams, reactive, operators, cancellation, buffering, channels]\nrelated: [ripple, herald, pulse, sourcerer, courier]\nexports: [stream, fromStore, fromSubscribe, pipe, of, from, fromEvent, interval, timer, map, filter, scan, switchMap, mergeMap, concatMap, take, takeUntil, debounce, timeout, merge, concat, combineLatest, retry, toArray, first, last, toAsyncIterable]\nenvironments: [browser, node, ssr, deno]\n---\n\n<!-- markdownlint-disable MD025 MD033 MD060 -->\n\n<PackageHero package=\"flux\" />\n\n## Why Flux?\n\nUse Flux when an API pushes many values over time and consumers need independent cancellation. Streams describe reusable work; subscriptions own cleanup. Explicit queue capacity keeps async iteration from silently growing memory.\n\n```ts\n// Before\nconst controller = new AbortController();\nconst render = (value: string) => console.log(value);\nconst handler = (event: Event) => render((event.target as HTMLInputElement).value);\ninput.addEventListener('input', handler);\nsetTimeout(() => controller.abort(), 5_000);\n\n// After\nimport { fromEvent, map, pipe, takeUntil } from '@vielzeug/flux';\n\nconst updates = pipe(\n fromEvent<InputEvent>(input, 'input'),\n map((event) => (event.target as HTMLInputElement).value),\n takeUntil(controller.signal),\n);\n\nupdates.subscribe({ error: console.error, next: render });\n```\n\n| Feature | Flux | RxJS | TC39 Observable |\n| --- | --- | --- | --- |\n| Bundle size | <PackageInfo package=\"flux\" type=\"size\" /> | Varies by imported operators | Native proposal / polyfill |\n| Runtime dependencies | <ore-icon name=\"check\" size=\"16\"></ore-icon> | <ore-icon name=\"check\" size=\"16\"></ore-icon> | <ore-icon name=\"x\" size=\"16\"></ore-icon> |\n| Subscription-owned cancellation | <ore-icon name=\"check\" size=\"16\"></ore-icon> | <ore-icon name=\"check\" size=\"16\"></ore-icon> | <ore-icon name=\"triangle-alert\" size=\"16\"></ore-icon> |\n| Explicit async queue policy | <ore-icon name=\"check\" size=\"16\"></ore-icon> | Operator-dependent | No standard policy |\n| Vielzeug adapters | Ripple, Courier, Herald, Pulse | Manual adapters | Manual adapters |\n\n<div class=\"decision-callout\">\n\n**Use Flux when** you need a small TypeScript stream primitive, explicit cancellation, and first-party Vielzeug adapters.\n\n**Consider RxJS when** you need its larger operator catalog or third-party Observable integrations.\n\n</div>\n\n## Installation\n\n::: code-group\n\n```sh [pnpm]\npnpm add @vielzeug/flux\n```\n\n```sh [npm]\nnpm install @vielzeug/flux\n```\n\n```sh [yarn]\nyarn add @vielzeug/flux\n```\n\n:::\n\n## Quick Start\n\n```ts\nimport { toArray, interval, map, pipe, take } from '@vielzeug/flux';\n\nconst firstThree = pipe(\n interval(100),\n map((value) => value * 2),\n take(3),\n);\n\ntry {\n console.log(await toArray(firstThree, { maxItems: 3 })); // [0, 2, 4]\n} catch (reason) {\n console.error('Stream failed', reason);\n}\n```\n\n## Features\n\n<div class=\"features-grid\">\n\n- `stream()` — define cold reusable work with one teardown function\n- `pipe()` — compose any number of typed operators\n- `Subscription` — own cancellation through `unsubscribe()` or `AbortSignal`\n- `createChannel()` — mutable multicast state with bounded replay\n- `toAsyncIterable()` — explicit capacity and overflow policy for pull consumers\n- `retry()` — retry failures with optional backoff\n- `fromStore()` — bridge snapshot-based state such as Ripple signals or Sourcerer queries\n- `fromSubscribe()` — bridge callback-delivered Herald or Pulse events\n\n</div>\n\n## Documentation\n\n<div class=\"doc-links\">\n\n- [Usage Guide](./usage.md)\n- [API Reference](./api.md)\n- [Examples](./examples.md)\n- [Migration Guide](./migration.md)\n\n</div>\n\n## See Also\n\n<div class=\"see-also\">\n\n- [Ripple](/ripple/) — bridge snapshot state with `fromStore()`.\n- [Courier](/courier/) — compose transport promises with `from()` or custom producers.\n- [Herald](/herald/) — bridge explicit subscriptions with `stream()`.\n- [Pulse](/pulse/) — bridge events with `fromSubscribe()` and presence snapshots with `fromStore()`.\n\n</div>\n\n<!-- markdownlint-enable MD025 MD033 MD060 -->\n",
5
- "api": "---\ntitle: Flux — API Reference\ndescription: Complete reference for @vielzeug/flux streams, operators, channels, and structural bridges.\n---\n\n[[toc]]\n\n## API Overview\n\n| Symbol | Purpose | Execution mode | Common gotcha |\n| --- | --- | --- | --- |\n| `stream()` | Create cold stream | Lazy | Return one teardown function |\n| `pipe()` | Compose operators | Lazy | Source is first argument |\n| `of()` / `from()` | Convert known values | Sync / mixed | `from()` promise cannot be aborted |\n| `fromEvent()` | Adapt event target | Async | Unsubscribe removes listener |\n| `interval()` / `timer()` | Create timed values | Async | Use `take()` or unsubscribe for intervals |\n| `map()` / `filter()` / `scan()` | Transform values | Sync | Callback throws terminate stream |\n| `switchMap()` / `mergeMap()` / `concatMap()` | Flatten streams | Mixed | `mergeMap()` concurrency and queue are bounded; `concatMap()` queue is bounded |\n| `take()` / `takeUntil()` | Stop values | Mixed | Notifier emission completes output |\n| `debounce()` / `timeout()` / `retry()` | Control time and failures | Async | `timeout()` measures inactivity |\n| `merge()` / `concat()` / `combineLatest()` | Combine streams | Mixed | `combineLatest()` waits for every source |\n| `toArray()` / `first()` / `last()` | Consume finite values | Async | Bound `toArray()` with `maxItems` |\n| `toAsyncIterable()` | Use `for await` | Async | Capacity and overflow required |\n| `createChannel()` | Imperative multicast boundary | Sync | Dispose to complete subscribers |\n| `fromStore()` | Bridge snapshot-based state | Lazy | Snapshot and subscribe are both required |\n| `fromSubscribe()` | Bridge callback-delivered values | Lazy | Registration must return teardown |\n\n## Package Entry Point\n\n| Import | Purpose |\n| --- | --- |\n| `@vielzeug/flux` | Core streams, operators, consumers, errors, and types |\n| `@vielzeug/flux/async` | `toAsyncIterable()` only |\n| `@vielzeug/flux/subjects` | `createChannel()` and channel types |\n\n## Core\n\n### `stream()`\n\n```ts\nstream<T>(producer: Producer<T>): Stream<T>\n```\n\nCreates cold reusable work. Producer runs once for every subscription.\n\n| Parameter | Type | Description |\n| --- | --- | --- |\n| `producer` | `Producer<T>` | Emits through sink and returns optional teardown |\n\n**Returns:** `Stream<T>`.\n\n```ts\nimport { stream } from '@vielzeug/flux';\n\nconst ticks = stream<number>((sink) => {\n const id = setInterval(() => sink.next(Date.now()), 1_000);\n return () => clearInterval(id);\n});\n```\n\n---\n\n### `pipe()`\n\n```ts\npipe<Input, Operators>(source: Stream<Input>, ...operators: Operators): Stream<Output>\n```\n\nApplies operators left to right while inferring output value type.\n\n| Parameter | Type | Description |\n| --- | --- | --- |\n| `source` | `Stream<Input>` | Source stream |\n| `operators` | `Operator[]` | Operators applied in order |\n\n**Returns:** transformed `Stream<Output>`.\n\n```ts\nimport { map, of, pipe } from '@vielzeug/flux';\n\nconst labels = pipe(of(1, 2), map((value) => `#${value}`));\n```\n\n## Creation\n\n### `of()`\n\n```ts\nof<T>(...values: T[]): Stream<T>\n```\n\nEmits every value synchronously, then completes.\n\n```ts\nimport { of } from '@vielzeug/flux';\n\nof(1, 2, 3).subscribe(console.log);\n```\n\n---\n\n### `from()`\n\n```ts\nfrom<T>(source: Iterable<T> | AsyncIterable<T> | Promise<T>): Stream<T>\n```\n\nConverts iterable, async iterable, or promise into a stream. Cancellation stops iterable consumption and calls `return()` when available.\n\n```ts\nimport { from } from '@vielzeug/flux';\n\nfrom(Promise.resolve('ready')).subscribe({ error: console.error, next: console.log });\n```\n\n---\n\n### `fromEvent()`\n\n```ts\nfromEvent<T = Event>(\n target: {\n addEventListener(type: string, listener: (event: T) => void): void;\n removeEventListener(type: string, listener: (event: T) => void): void;\n },\n type: string,\n): Stream<T>\n```\n\nEmits target events until subscription ends.\n\n```ts\nimport { fromEvent } from '@vielzeug/flux';\n\nfromEvent<MouseEvent>(document, 'click').subscribe(console.log);\n```\n\n---\n\n### `interval()`\n\n```ts\ninterval(every: number): Stream<number>\n```\n\nEmits incrementing values starting at zero.\n\n| Parameter | Type | Description |\n| --- | --- | --- |\n| `every` | `number` | Interval duration from 0 through 2,147,483,647 milliseconds |\n\n---\n\n### `timer()`\n\n```ts\ntimer(options: TimerOptions): Stream<number>\n```\n\nEmits zero after `delay`; optionally continues at `interval`.\n\n| Option | Type | Description |\n| --- | --- | --- |\n| `delay` | `number` | Initial delay from 0 through 2,147,483,647 milliseconds |\n| `interval` | `number` | Optional repeat duration from 0 through 2,147,483,647 milliseconds |\n\n## Transformation Operators\n\n### `map()`\n\n```ts\nmap<A, B>(project: (value: A) => B): Operator<A, B>\n```\n\nMaps every value. A thrown callback error terminates output.\n\n---\n\n### `filter()`\n\n```ts\nfilter<T>(predicate: (value: T) => boolean): Operator<T, T>\n```\n\nForwards values matching predicate.\n\n---\n\n### `scan()`\n\n```ts\nscan<T, A>(reducer: (state: A, value: T) => A, initial: A): Operator<T, A>\n```\n\nEmits accumulated state after every source value.\n\n---\n\n### `switchMap()`\n\n```ts\nswitchMap<A, B>(project: (value: A) => Stream<B>): Operator<A, B>\n```\n\nCancels previous inner stream when source emits.\n\n---\n\n### `mergeMap()`\n\n```ts\nmergeMap<A, B>(project: (value: A) => Stream<B>, options: FlattenOptions): Operator<A, B>\n```\n\nRuns inner streams with bounded concurrency and queue capacity. Exceeding capacity errors output with `FluxCapacityError`.\n\n| Option | Type | Description |\n| --- | --- | --- |\n| `concurrency` | `number \\| Infinity` | Maximum concurrently active inner subscriptions |\n| `capacity` | `number` | Positive maximum queued source values (required when `concurrency` is finite) |\n\n---\n\n### `concatMap()`\n\n```ts\nconcatMap<A, B>(project: (value: A) => Stream<B>, options: { capacity: number }): Operator<A, B>\n```\n\nRuns inner streams in order (`concurrency: 1`). Exceeding capacity errors output with `FluxCapacityError`.\n\n| Option | Type | Description |\n| --- | --- | --- |\n| `capacity` | `number` | Positive maximum queued source values |\n\n## Control Operators\n\n### `take()`\n\n```ts\ntake<T>(count: number): Operator<T, T>\n```\n\nForwards `count` values, cancels upstream, then completes. Count must be non-negative integer.\n\n---\n\n### `takeUntil()`\n\n```ts\ntakeUntil<T>(notifier: AbortSignal | Stream<unknown>): Operator<T, T>\n```\n\nCompletes when notifier aborts or emits.\n\n---\n\n### `debounce()`\n\n```ts\ndebounce<T>(duration: number): Operator<T, T>\n```\n\nEmits latest value after configured silence. Pending value flushes on source completion.\n\n| Parameter | Type | Description |\n| --- | --- | --- |\n| `duration` | `number` | Silence duration from 0 through 2,147,483,647 milliseconds |\n\n---\n\n### `timeout()`\n\n```ts\ntimeout<T>(duration: number): Operator<T, T>\n```\n\nErrors with `FluxTimeoutError` when source is silent too long.\n\n| Parameter | Type | Description |\n| --- | --- | --- |\n| `duration` | `number` | Inactivity duration from 0 through 2,147,483,647 milliseconds |\n\n---\n\n### `retry()`\n\n```ts\nretry<T>(options: RetryOptions): Operator<T, T>\n```\n\nResubscribes after source errors until attempts are exhausted.\n\n| Option | Type | Description |\n| --- | --- | --- |\n| `attempts` | `number` | Non-negative retry count |\n| `delay` | `number \\| (attempt: number) => number` | Optional delay/backoff returning 0 through 2,147,483,647 milliseconds |\n\n## Combination\n\n### `merge()`\n\n```ts\nmerge<T>(...sources: Stream<T>[]): Stream<T>\n```\n\nForwards values from all sources and completes after every source completes.\n\n---\n\n### `concat()`\n\n```ts\nconcat<T>(...sources: Stream<T>[]): Stream<T>\n```\n\nSubscribes to each source only after previous source completes.\n\n---\n\n### `combineLatest()`\n\n```ts\ncombineLatest<T extends readonly Stream<unknown>[]>(...sources: T): Stream<{ [K in keyof T]: T[K] extends Stream<infer V> ? V : never }>\n```\n\nEmits latest tuple after every source emits once. Completes without emission when a source completes before first value.\n\n## Value Consumers\n\n### `toArray()`\n\n```ts\ntoArray<T>(source: Stream<T>, options: ToArrayOptions): Promise<T[]>\n```\n\nCollects finite output. Rejects on source error, the supplied signal's abort reason, or `maxItems` overflow.\n\n| Option | Type | Description |\n| --- | --- | --- |\n| `maxItems` | `number` | Non-negative maximum toArrayed values |\n| `signal` | `AbortSignal` | Optional cancellation signal |\n\n---\n\n### `first()`\n\n```ts\nfirst<T>(source: Stream<T>, options?: ValueOptions<T>): Promise<T>\n```\n\nResolves first value and cancels source. Rejects on source error, the supplied signal's abort reason, or empty completion (`FluxEmptyError`); pass `defaultValue` to resolve instead.\n\n---\n\n### `last()`\n\n```ts\nlast<T>(source: Stream<T>, options?: ValueOptions<T>): Promise<T>\n```\n\nResolves last value on completion. Rejects on source error, the supplied signal's abort reason, or empty completion (`FluxEmptyError`); pass `defaultValue` to resolve instead.\n\n## Async Conversion\n\n### `toAsyncIterable()`\n\n```ts\ntoAsyncIterable<T>(source: Stream<T>, options: AsyncIterableOptions): AsyncIterable<T>\n```\n\nConverts a push stream to an async iterable with a bounded queue. External cancellation rejects pending iteration with `signal.reason`; explicit iterator `return()` completes normally. Overflow `\"error\"` rejects with `FluxCapacityError`; unsupported policies throw `RangeError` before subscription.\n\n| Option | Type | Description |\n| --- | --- | --- |\n| `capacity` | `number` | Positive queue capacity |\n| `overflow` | `OverflowPolicy` | `error`, `drop-oldest`, or `drop-newest` |\n| `signal` | `AbortSignal` | Optional cancellation signal |\n\n## Channels\n\n### `createChannel()`\n\n```ts\ncreateChannel<T>(options?: ChannelOptions<T>): Channel<T>\n```\n\nCreates an imperative multicast boundary. Disposal completes subscribers and releases replay and pending values.\n\n> **`initial` + `replay` interaction:** When `initial` is set and `replay` is omitted, `replay` defaults to `1` so the initial value is retained. Setting `replay: 0` with `initial` throws `RangeError` — the initial value would be immediately dropped.\n\n| Option | Type | Description |\n| --- | --- | --- |\n| `initial` | `T` | Optional initial replay value |\n| `replay` | `number` | Non-negative retained value count |\n\n## Structural adapters\n\n### `fromStore()`\n\n```ts\nfromStore<T>(source: {\n getSnapshot(): T;\n subscribe(listener: () => void): () => void;\n}): Stream<T>\n```\n\nBridges snapshot-based state without package-specific dependencies. It emits the current snapshot, subscribes, rechecks for a subscription-time change, and reads the latest snapshot after each notification. Snapshot failure terminates without subscribing.\n\n### `fromSubscribe()`\n\n```ts\nfromSubscribe<T>(subscribe: (listener: (value: T) => void) => () => void): Stream<T>\n```\n\nBridges callback-delivered event values. Registration runs once per Flux subscription and its teardown runs when that subscription closes.\n\n## Types\n\n```ts\ntype Teardown = () => void;\n\ntype Subscription = {\n [Symbol.dispose](): void;\n readonly closed: boolean;\n unsubscribe(): void;\n};\n\ntype Observer<T> = {\n complete?: () => void;\n error?: (reason: unknown) => void;\n next: (value: T) => void;\n};\n\ntype SubscribeOptions = { signal?: AbortSignal };\n\ntype Sink<T> = {\n complete(): void;\n error(reason: unknown): void;\n next(value: T): void;\n};\n\ntype Producer<T> = (sink: Sink<T>, signal: AbortSignal) => Teardown | undefined;\ntype Operator<A = unknown, B = unknown> = (source: Stream<A>) => Stream<B>;\n\ninterface Stream<T> {\n subscribe(observer: Observer<T> | ((value: T) => void), options?: SubscribeOptions): Subscription;\n}\n\ntype OverflowPolicy = 'drop-newest' | 'drop-oldest' | 'error';\ntype AsyncIterableOptions = { capacity: number; overflow: OverflowPolicy; signal?: AbortSignal };\ntype TimerOptions = { delay: number; interval?: number };\ntype FlattenOptions = { concurrency: number; capacity: number };\ntype RetryOptions = { attempts: number; delay?: number | ((attempt: number) => number) };\ntype ToArrayOptions = { maxItems: number; signal?: AbortSignal };\ntype ValueOptions<T> = { signal?: AbortSignal; defaultValue?: T };\ntype ChannelOptions<T> = { initial?: T; replay?: number };\ntype Channel<T> = {\n [Symbol.dispose](): void;\n readonly disposalSignal: AbortSignal;\n dispose(): void;\n readonly disposed: boolean;\n send(value: T): void;\n readonly stream: Stream<T>;\n};\n```\n\n## Errors\n\n### `FluxError`\n\nBase Flux error. Use `instanceof FluxError` to narrow unknown values.\n\n### `FluxTimeoutError`\n\nRaised by `timeout()`. `ms` contains configured inactivity duration.\n\n### `FluxEmptyError`\n\nRaised by `first()` and `last()` when the source completes without emitting any value.\n\n### `FluxCapacityError`\n\nRaised when a bounded buffer (`mergeMap`, `concatMap`, `toArray`, async iteration) overflows. `capacity` contains the configured limit.\n",
6
- "usage": "---\ntitle: Flux — Usage Guide\ndescription: Create streams, compose operators, consume values safely, and bridge Vielzeug primitives.\n---\n\n[[toc]]\n\n## Basic Usage\n\nDefine one cold stream. Return teardown work from producer. Every subscription runs producer independently.\n\n```ts\nimport { stream } from '@vielzeug/flux';\n\nconst clock = stream<number>((sink) => {\n let value = 0;\n const id = setInterval(() => sink.next(value++), 1_000);\n\n return () => clearInterval(id);\n});\n\nconst subscription = clock.subscribe({\n error: console.error,\n next: console.log,\n});\n\nsubscription.unsubscribe();\n```\n\nPass `AbortSignal` when another owner controls lifetime.\n\n```ts\nconst controller = new AbortController();\nclock.subscribe(console.log, { signal: controller.signal });\ncontroller.abort();\n```\n\n## Compose Streams\n\nPass source first to `pipe()`. Operators retain inferred value types across chains.\n\n```ts\nimport { filter, fromEvent, map, pipe, take } from '@vielzeug/flux';\n\nconst clicks = pipe(\n fromEvent<MouseEvent>(document, 'click'),\n filter((event) => event.button === 0),\n map((event) => ({ x: event.clientX, y: event.clientY })),\n take(10),\n);\n\nclicks.subscribe({\n complete: () => console.log('done'),\n error: console.error,\n next: console.log,\n});\n```\n\nUse `switchMap()` for latest-only work, `mergeMap()` for concurrent work with bounded concurrency and queue capacity, and `concatMap()` for ordered work with bounded queue capacity.\n\n```ts\nimport { from, pipe, retry, switchMap } from '@vielzeug/flux';\n\nconst results = pipe(\n queries,\n switchMap((query) => from(fetch(`/api/search?q=${encodeURIComponent(query)}`).then((response) => response.json()))),\n retry({ attempts: 2, delay: (attempt) => 250 * (attempt + 1) }),\n);\n```\n\n## Consume Values\n\nUse bounded array conversion for finite streams. `toArray()` rejects once source exceeds `maxItems`.\n\n```ts\nimport { toArray, of } from '@vielzeug/flux';\n\ntry {\n const values = await toArray(of(1, 2, 3), { maxItems: 3 });\n console.log(values);\n} catch (reason) {\n console.error('Collection failed', reason);\n}\n```\n\nUse `first()` for first emission and `last()` for last value before completion. Both reject with `FluxEmptyError` when the source completes without emitting; pass `{ defaultValue }` to resolve instead. Pass `{ signal }` to cancel waiting; cancellation rejects with `signal.reason` (the default reason is `AbortError`).\n\n## Channels\n\nUse channels only at imperative boundaries. Expose `channel.stream` to consumers; keep `send()` near event producer.\n\n```ts\nimport { createChannel } from '@vielzeug/flux/subjects';\n\nconst status = createChannel({ initial: 'starting', replay: 1 });\nstatus.stream.subscribe({ error: console.error, next: console.log });\nstatus.send('ready');\nstatus.dispose();\n```\n\nDisposal completes active and future subscribers. Replay retains only configured latest values.\n\n## Async Iteration and Bounds\n\nConvert push stream only when pull syntax is required. Capacity and overflow policy are mandatory.\n\n```ts\nimport { interval, toAsyncIterable } from '@vielzeug/flux';\n\nconst values = toAsyncIterable(interval(100), {\n capacity: 32,\n overflow: 'error',\n});\n\nfor await (const value of values) {\n console.log(value);\n if (value === 2) break;\n}\n```\n\n`return()` from a loop permanently completes its iterator; external signal cancellation rejects with `signal.reason`. Use `drop-oldest` or `drop-newest` only when loss is acceptable.\n\n## Testing\n\nUse fake timers for time operators. Test producer cleanup through returned subscription.\n\n```ts\nimport { expect, it, vi } from 'vitest';\nimport { first, pipe, stream, timeout } from '@vielzeug/flux';\n\nit('fails after inactivity', async () => {\n vi.useFakeTimers();\n const result = first(pipe(stream(() => {}), timeout(500)));\n const expectation = expect(result).rejects.toThrow('Timeout after 500ms');\n\n await vi.advanceTimersByTimeAsync(500);\n await expectation;\n vi.useRealTimers();\n});\n```\n\n## Framework Integration\n\n::: code-group\n\n```tsx [React]\nimport { useEffect, useState } from 'react';\nimport type { Stream } from '@vielzeug/flux';\n\nexport function useStream<T>(source: Stream<T>, initial: T): T {\n const [value, setValue] = useState(initial);\n\n useEffect(() => {\n const subscription = source.subscribe({ error: console.error, next: setValue });\n return () => subscription.unsubscribe();\n }, [source]);\n\n return value;\n}\n```\n\n```ts [Vue 3]\nimport { onUnmounted, ref } from 'vue';\nimport type { Stream } from '@vielzeug/flux';\n\nexport function useStream<T>(source: Stream<T>, initial: T) {\n const value = ref(initial);\n const subscription = source.subscribe({ error: console.error, next: (next) => (value.value = next) });\n\n onUnmounted(() => subscription.unsubscribe());\n\n return value;\n}\n```\n\n```ts [Svelte]\nimport type { Stream } from '@vielzeug/flux';\n\nexport function streamStore<T>(source: Stream<T>, initial: T) {\n return {\n subscribe(run: (value: T) => void) {\n run(initial);\n const subscription = source.subscribe({ error: console.error, next: run });\n return () => subscription.unsubscribe();\n },\n };\n}\n```\n\n:::\n\n## Working with Other Vielzeug Libraries\n\nUse `fromStore()` for snapshot state and `fromSubscribe()` for callback-delivered event values. Write stream output into the owning state or event layer explicitly.\n\n```ts\nimport { fromStore, fromSubscribe } from '@vielzeug/flux';\n\nconst state = fromStore({\n getSnapshot: () => store.value,\n subscribe: (listener) => store.subscribe(listener),\n});\n\nconst events = fromSubscribe<Message>((listener) => bus.on('message', listener));\n```\n\n## Best Practices\n\n- Return one idempotent producer teardown function.\n- Pass `{ signal }` from component, request, or task owner.\n- Provide `error` when subscription can recover locally.\n- Use `pipe(source, ...)`; never mutate stream definitions.\n- Bound `mergeMap()` concurrency and queue capacity.\n- Bound `concatMap()` queue capacity.\n- Bound `toArray()` with realistic `maxItems`.\n- Choose async iterator overflow policy deliberately.\n- Keep timer-backed durations between 0 and 2,147,483,647 milliseconds.\n- Keep `Channel.send()` at integration boundaries.\n",
5
+ "api": "---\ntitle: Flux — API Reference\ndescription: Complete reference for @vielzeug/flux streams, operators, channels, and structural bridges.\n---\n\n[[toc]]\n\n## API Overview\n\n| Symbol | Purpose | Execution mode | Common gotcha |\n| --- | --- | --- | --- |\n| `stream()` | Create cold stream | Lazy | Return one teardown function |\n| `pipe()` | Compose operators | Lazy | Source is first argument |\n| `of()` / `from()` | Convert known values | Sync / mixed | `from()` promise cannot be aborted |\n| `fromEvent()` | Adapt event target | Async | Unsubscribe removes listener |\n| `interval()` / `timer()` | Create timed values | Async | Use `take()` or unsubscribe for intervals |\n| `map()` / `filter()` / `scan()` | Transform values | Sync | Callback throws terminate stream |\n| `switchMap()` / `mergeMap()` / `concatMap()` | Flatten streams | Mixed | `mergeMap()` concurrency and queue are bounded; `concatMap()` queue is bounded |\n| `take()` / `takeUntil()` | Stop values | Mixed | Notifier emission completes output |\n| `debounce()` / `timeout()` / `retry()` | Control time and failures | Async | `timeout()` measures inactivity |\n| `merge()` / `concat()` / `combineLatest()` | Combine streams | Mixed | `combineLatest()` waits for every source |\n| `toArray()` / `first()` / `last()` | Consume finite values | Async | Bound `toArray()` with `maxItems` |\n| `toAsyncIterable()` | Use `for await` | Async | Capacity and overflow required |\n| `createChannel()` | Imperative multicast boundary | Sync | Dispose to complete subscribers |\n| `fromStore()` | Bridge snapshot-based state | Lazy | Snapshot and subscribe are both required |\n| `fromSubscribe()` | Bridge callback-delivered values | Lazy | Registration must return teardown |\n\n## Package Entry Point\n\n| Import | Purpose |\n| --- | --- |\n| `@vielzeug/flux` | Core streams, operators, consumers, errors, types, and `createChannel()` |\n| `@vielzeug/flux/async` | `toAsyncIterable()` only |\n\n## Core\n\n### `stream()`\n\n```ts\nstream<T>(producer: Producer<T>): Stream<T>\n```\n\nCreates cold reusable work. Producer runs once for every subscription.\n\n| Parameter | Type | Description |\n| --- | --- | --- |\n| `producer` | `Producer<T>` | Emits through sink and returns optional teardown |\n\n**Returns:** `Stream<T>`.\n\n```ts\nimport { stream } from '@vielzeug/flux';\n\nconst ticks = stream<number>((sink) => {\n const id = setInterval(() => sink.next(Date.now()), 1_000);\n return () => clearInterval(id);\n});\n```\n\n---\n\n### `pipe()`\n\n```ts\npipe<Input, Operators>(source: Stream<Input>, ...operators: Operators): Stream<Output>\n```\n\nApplies operators left to right while inferring output value type.\n\n| Parameter | Type | Description |\n| --- | --- | --- |\n| `source` | `Stream<Input>` | Source stream |\n| `operators` | `Operator[]` | Operators applied in order |\n\n**Returns:** transformed `Stream<Output>`.\n\n```ts\nimport { map, of, pipe } from '@vielzeug/flux';\n\nconst labels = pipe(of(1, 2), map((value) => `#${value}`));\n```\n\n## Creation\n\n### `of()`\n\n```ts\nof<T>(...values: T[]): Stream<T>\n```\n\nEmits every value synchronously, then completes.\n\n```ts\nimport { of } from '@vielzeug/flux';\n\nof(1, 2, 3).subscribe(console.log);\n```\n\n---\n\n### `from()`\n\n```ts\nfrom<T>(source: Iterable<T> | AsyncIterable<T> | Promise<T>): Stream<T>\n```\n\nConverts iterable, async iterable, or promise into a stream. Cancellation stops iterable consumption and calls `return()` when available.\n\n```ts\nimport { from } from '@vielzeug/flux';\n\nfrom(Promise.resolve('ready')).subscribe({ error: console.error, next: console.log });\n```\n\n---\n\n### `fromEvent()`\n\n```ts\nfromEvent<T = Event>(\n target: {\n addEventListener(type: string, listener: (event: T) => void): void;\n removeEventListener(type: string, listener: (event: T) => void): void;\n },\n type: string,\n): Stream<T>\n```\n\nEmits target events until subscription ends.\n\n```ts\nimport { fromEvent } from '@vielzeug/flux';\n\nfromEvent<MouseEvent>(document, 'click').subscribe(console.log);\n```\n\n---\n\n### `interval()`\n\n```ts\ninterval(every: number): Stream<number>\n```\n\nEmits incrementing values starting at zero.\n\n| Parameter | Type | Description |\n| --- | --- | --- |\n| `every` | `number` | Interval duration from 0 through 2,147,483,647 milliseconds |\n\n---\n\n### `timer()`\n\n```ts\ntimer(options: TimerOptions): Stream<number>\n```\n\nEmits zero after `delay`; optionally continues at `interval`.\n\n| Option | Type | Description |\n| --- | --- | --- |\n| `delay` | `number` | Initial delay from 0 through 2,147,483,647 milliseconds |\n| `interval` | `number` | Optional repeat duration from 0 through 2,147,483,647 milliseconds |\n\n## Transformation Operators\n\n### `map()`\n\n```ts\nmap<A, B>(project: (value: A) => B): Operator<A, B>\n```\n\nMaps every value. A thrown callback error terminates output.\n\n---\n\n### `filter()`\n\n```ts\nfilter<T>(predicate: (value: T) => boolean): Operator<T, T>\n```\n\nForwards values matching predicate.\n\n---\n\n### `scan()`\n\n```ts\nscan<T, A>(reducer: (state: A, value: T) => A, initial: A): Operator<T, A>\n```\n\nEmits accumulated state after every source value.\n\n---\n\n### `switchMap()`\n\n```ts\nswitchMap<A, B>(project: (value: A) => Stream<B>): Operator<A, B>\n```\n\nCancels previous inner stream when source emits.\n\n---\n\n### `mergeMap()`\n\n```ts\nmergeMap<A, B>(project: (value: A) => Stream<B>, options: FlattenOptions): Operator<A, B>\n```\n\nRuns inner streams with bounded concurrency and queue capacity. Exceeding capacity errors output with `FluxCapacityError`.\n\n| Option | Type | Description |\n| --- | --- | --- |\n| `concurrency` | `number \\| Infinity` | Maximum concurrently active inner subscriptions |\n| `capacity` | `number` | Positive maximum queued source values (required when `concurrency` is finite) |\n\n---\n\n### `concatMap()`\n\n```ts\nconcatMap<A, B>(project: (value: A) => Stream<B>, options: { capacity: number }): Operator<A, B>\n```\n\nRuns inner streams in order (`concurrency: 1`). Exceeding capacity errors output with `FluxCapacityError`.\n\n| Option | Type | Description |\n| --- | --- | --- |\n| `capacity` | `number` | Positive maximum queued source values |\n\n## Control Operators\n\n### `take()`\n\n```ts\ntake<T>(count: number): Operator<T, T>\n```\n\nForwards `count` values, cancels upstream, then completes. Count must be non-negative integer.\n\n---\n\n### `takeUntil()`\n\n```ts\ntakeUntil<T>(notifier: AbortSignal | Stream<unknown>): Operator<T, T>\n```\n\nCompletes when notifier aborts or emits.\n\n---\n\n### `debounce()`\n\n```ts\ndebounce<T>(duration: number): Operator<T, T>\n```\n\nEmits latest value after configured silence. Pending value flushes on source completion.\n\n| Parameter | Type | Description |\n| --- | --- | --- |\n| `duration` | `number` | Silence duration from 0 through 2,147,483,647 milliseconds |\n\n---\n\n### `timeout()`\n\n```ts\ntimeout<T>(duration: number): Operator<T, T>\n```\n\nErrors with `FluxTimeoutError` when source is silent too long.\n\n| Parameter | Type | Description |\n| --- | --- | --- |\n| `duration` | `number` | Inactivity duration from 0 through 2,147,483,647 milliseconds |\n\n---\n\n### `retry()`\n\n```ts\nretry<T>(options: RetryOptions): Operator<T, T>\n```\n\nResubscribes after source errors until attempts are exhausted.\n\n| Option | Type | Description |\n| --- | --- | --- |\n| `times` | `number` | Non-negative retry count |\n| `delay` | `number \\| (attempt: number) => number` | Optional delay/backoff returning 0 through 2,147,483,647 milliseconds |\n\n## Combination\n\n### `merge()`\n\n```ts\nmerge<T>(...sources: Stream<T>[]): Stream<T>\n```\n\nForwards values from all sources and completes after every source completes.\n\n---\n\n### `concat()`\n\n```ts\nconcat<T>(...sources: Stream<T>[]): Stream<T>\n```\n\nSubscribes to each source only after previous source completes.\n\n---\n\n### `combineLatest()`\n\n```ts\ncombineLatest<T extends readonly Stream<unknown>[]>(...sources: T): Stream<{ [K in keyof T]: T[K] extends Stream<infer V> ? V : never }>\n```\n\nEmits latest tuple after every source emits once. Completes without emission when a source completes before first value.\n\n## Value Consumers\n\n### `toArray()`\n\n```ts\ntoArray<T>(source: Stream<T>, options: ToArrayOptions): Promise<T[]>\n```\n\nCollects finite output. Rejects on source error, the supplied signal's abort reason, or `maxItems` overflow.\n\n| Option | Type | Description |\n| --- | --- | --- |\n| `maxItems` | `number` | Non-negative maximum toArrayed values |\n| `signal` | `AbortSignal` | Optional cancellation signal |\n\n---\n\n### `first()`\n\n```ts\nfirst<T>(source: Stream<T>, options?: ValueOptions<T>): Promise<T>\n```\n\nResolves first value and cancels source. Rejects on source error, the supplied signal's abort reason, or empty completion (`FluxEmptyError`); pass `defaultValue` to resolve instead.\n\n---\n\n### `last()`\n\n```ts\nlast<T>(source: Stream<T>, options?: ValueOptions<T>): Promise<T>\n```\n\nResolves last value on completion. Rejects on source error, the supplied signal's abort reason, or empty completion (`FluxEmptyError`); pass `defaultValue` to resolve instead.\n\n## Async Conversion\n\n### `toAsyncIterable()`\n\n```ts\ntoAsyncIterable<T>(source: Stream<T>, options: AsyncIterableOptions): AsyncIterable<T>\n```\n\nConverts a push stream to an async iterable with a bounded queue. External cancellation rejects pending iteration with `signal.reason`; explicit iterator `return()` completes normally. Overflow `\"error\"` rejects with `FluxCapacityError`; unsupported policies throw `RangeError` before subscription.\n\n| Option | Type | Description |\n| --- | --- | --- |\n| `capacity` | `number` | Positive queue capacity |\n| `overflow` | `OverflowPolicy` | `error`, `drop-oldest`, or `drop-newest` |\n| `signal` | `AbortSignal` | Optional cancellation signal |\n\n## Channels\n\n### `createChannel()`\n\n```ts\ncreateChannel<T>(options?: ChannelOptions<T>): Channel<T>\n```\n\nCreates an imperative multicast boundary. Disposal completes subscribers and releases replay and pending values.\n\n> **`initial` + `replay` interaction:** When `initial` is set and `replay` is omitted, `replay` defaults to `1` so the initial value is retained. Setting `replay: 0` with `initial` throws `RangeError` — the initial value would be immediately dropped.\n\n| Option | Type | Description |\n| --- | --- | --- |\n| `initial` | `T` | Optional initial replay value |\n| `replay` | `number` | Non-negative retained value count |\n\n## Structural adapters\n\n### `fromStore()`\n\n```ts\nfromStore<T>(source: {\n getSnapshot(): T;\n subscribe(listener: () => void): () => void;\n}): Stream<T>\n```\n\nBridges snapshot-based state without package-specific dependencies. It emits the current snapshot, subscribes, rechecks for a subscription-time change, and reads the latest snapshot after each notification. Snapshot failure terminates without subscribing.\n\n### `fromSubscribe()`\n\n```ts\nfromSubscribe<T>(subscribe: (listener: (value: T) => void) => () => void): Stream<T>\n```\n\nBridges callback-delivered event values. Registration runs once per Flux subscription and its teardown runs when that subscription closes.\n\n## Types\n\n```ts\ntype Teardown = () => void;\n\ntype Subscription = {\n [Symbol.dispose](): void;\n readonly closed: boolean;\n unsubscribe(): void;\n};\n\ntype Observer<T> = {\n complete?: () => void;\n error?: (reason: unknown) => void;\n next: (value: T) => void;\n};\n\ntype SubscribeOptions = { signal?: AbortSignal };\n\ntype Sink<T> = {\n complete(): void;\n error(reason: unknown): void;\n next(value: T): void;\n};\n\ntype Producer<T> = (sink: Sink<T>, signal: AbortSignal) => Teardown | undefined;\ntype Operator<A = unknown, B = unknown> = (source: Stream<A>) => Stream<B>;\n\ninterface Stream<T> {\n subscribe(observer: Observer<T> | ((value: T) => void), options?: SubscribeOptions): Subscription;\n}\n\ntype OverflowPolicy = 'drop-newest' | 'drop-oldest' | 'error';\ntype AsyncIterableOptions = { capacity: number; overflow: OverflowPolicy; signal?: AbortSignal };\ntype TimerOptions = { delay: number; interval?: number };\ntype FlattenOptions = { concurrency: number; capacity: number };\ntype RetryOptions = { times: number; delay?: number | ((attempt: number) => number) };\ntype ToArrayOptions = { maxItems: number; signal?: AbortSignal };\ntype ValueOptions<T> = { signal?: AbortSignal; defaultValue?: T };\ntype ChannelOptions<T> = { initial?: T; replay?: number };\ntype Channel<T> = {\n [Symbol.dispose](): void;\n readonly disposalSignal: AbortSignal;\n dispose(): void;\n readonly disposed: boolean;\n send(value: T): void;\n readonly stream: Stream<T>;\n};\n```\n\n## Errors\n\n### `FluxError`\n\nBase Flux error. Use `instanceof FluxError` to narrow unknown values.\n\n### `FluxTimeoutError`\n\nRaised by `timeout()`. `ms` contains configured inactivity duration.\n\n### `FluxEmptyError`\n\nRaised by `first()` and `last()` when the source completes without emitting any value.\n\n### `FluxCapacityError`\n\nRaised when a bounded buffer (`mergeMap`, `concatMap`, `toArray`, async iteration) overflows. `capacity` contains the configured limit.\n",
6
+ "usage": "---\ntitle: Flux — Usage Guide\ndescription: Create streams, compose operators, consume values safely, and bridge Vielzeug primitives.\n---\n\n[[toc]]\n\n## Basic Usage\n\nDefine one cold stream. Return teardown work from producer. Every subscription runs producer independently.\n\n```ts\nimport { stream } from '@vielzeug/flux';\n\nconst clock = stream<number>((sink) => {\n let value = 0;\n const id = setInterval(() => sink.next(value++), 1_000);\n\n return () => clearInterval(id);\n});\n\nconst subscription = clock.subscribe({\n error: console.error,\n next: console.log,\n});\n\nsubscription.unsubscribe();\n```\n\nPass `AbortSignal` when another owner controls lifetime.\n\n```ts\nconst controller = new AbortController();\nclock.subscribe(console.log, { signal: controller.signal });\ncontroller.abort();\n```\n\n## Compose Streams\n\nPass source first to `pipe()`. Operators retain inferred value types across chains.\n\n```ts\nimport { filter, fromEvent, map, pipe, take } from '@vielzeug/flux';\n\nconst clicks = pipe(\n fromEvent<MouseEvent>(document, 'click'),\n filter((event) => event.button === 0),\n map((event) => ({ x: event.clientX, y: event.clientY })),\n take(10),\n);\n\nclicks.subscribe({\n complete: () => console.log('done'),\n error: console.error,\n next: console.log,\n});\n```\n\nUse `switchMap()` for latest-only work, `mergeMap()` for concurrent work with bounded concurrency and queue capacity, and `concatMap()` for ordered work with bounded queue capacity.\n\n```ts\nimport { from, pipe, retry, switchMap } from '@vielzeug/flux';\n\nconst results = pipe(\n queries,\n switchMap((query) => from(fetch(`/api/search?q=${encodeURIComponent(query)}`).then((response) => response.json()))),\n retry({ times: 2, delay: (attempt) => 250 * (attempt + 1) }),\n);\n```\n\n## Consume Values\n\nUse bounded array conversion for finite streams. `toArray()` rejects once source exceeds `maxItems`.\n\n```ts\nimport { toArray, of } from '@vielzeug/flux';\n\ntry {\n const values = await toArray(of(1, 2, 3), { maxItems: 3 });\n console.log(values);\n} catch (reason) {\n console.error('Collection failed', reason);\n}\n```\n\nUse `first()` for first emission and `last()` for last value before completion. Both reject with `FluxEmptyError` when the source completes without emitting; pass `{ defaultValue }` to resolve instead. Pass `{ signal }` to cancel waiting; cancellation rejects with `signal.reason` (the default reason is `AbortError`).\n\n## Channels\n\nUse channels only at imperative boundaries. Expose `channel.stream` to consumers; keep `send()` near event producer.\n\n```ts\nimport { createChannel } from '@vielzeug/flux';\n\nconst status = createChannel({ initial: 'starting', replay: 1 });\nstatus.stream.subscribe({ error: console.error, next: console.log });\nstatus.send('ready');\nstatus.dispose();\n```\n\nDisposal completes active and future subscribers. Replay retains only configured latest values.\n\n## Async Iteration and Bounds\n\nConvert push stream only when pull syntax is required. Capacity and overflow policy are mandatory.\n\n```ts\nimport { interval, toAsyncIterable } from '@vielzeug/flux';\n\nconst values = toAsyncIterable(interval(100), {\n capacity: 32,\n overflow: 'error',\n});\n\nfor await (const value of values) {\n console.log(value);\n if (value === 2) break;\n}\n```\n\n`return()` from a loop permanently completes its iterator; external signal cancellation rejects with `signal.reason`. Use `drop-oldest` or `drop-newest` only when loss is acceptable.\n\n## Testing\n\nUse fake timers for time operators. Test producer cleanup through returned subscription.\n\n```ts\nimport { expect, it, vi } from 'vitest';\nimport { first, pipe, stream, timeout } from '@vielzeug/flux';\n\nit('fails after inactivity', async () => {\n vi.useFakeTimers();\n const result = first(pipe(stream(() => {}), timeout(500)));\n const expectation = expect(result).rejects.toThrow('Timeout after 500ms');\n\n await vi.advanceTimersByTimeAsync(500);\n await expectation;\n vi.useRealTimers();\n});\n```\n\n## Framework Integration\n\n::: code-group\n\n```tsx [React]\nimport { useEffect, useState } from 'react';\nimport type { Stream } from '@vielzeug/flux';\n\nexport function useStream<T>(source: Stream<T>, initial: T): T {\n const [value, setValue] = useState(initial);\n\n useEffect(() => {\n const subscription = source.subscribe({ error: console.error, next: setValue });\n return () => subscription.unsubscribe();\n }, [source]);\n\n return value;\n}\n```\n\n```ts [Vue 3]\nimport { onUnmounted, ref } from 'vue';\nimport type { Stream } from '@vielzeug/flux';\n\nexport function useStream<T>(source: Stream<T>, initial: T) {\n const value = ref(initial);\n const subscription = source.subscribe({ error: console.error, next: (next) => (value.value = next) });\n\n onUnmounted(() => subscription.unsubscribe());\n\n return value;\n}\n```\n\n```ts [Svelte]\nimport type { Stream } from '@vielzeug/flux';\n\nexport function streamStore<T>(source: Stream<T>, initial: T) {\n return {\n subscribe(run: (value: T) => void) {\n run(initial);\n const subscription = source.subscribe({ error: console.error, next: run });\n return () => subscription.unsubscribe();\n },\n };\n}\n```\n\n:::\n\n## Working with Other Vielzeug Libraries\n\nUse `fromStore()` for snapshot state and `fromSubscribe()` for callback-delivered event values. Write stream output into the owning state or event layer explicitly.\n\n```ts\nimport { fromStore, fromSubscribe } from '@vielzeug/flux';\n\nconst state = fromStore({\n getSnapshot: () => store.value,\n subscribe: (listener) => store.subscribe(listener),\n});\n\nconst events = fromSubscribe<Message>((listener) => bus.on('message', listener));\n```\n\n## Best Practices\n\n- Return one idempotent producer teardown function.\n- Pass `{ signal }` from component, request, or task owner.\n- Provide `error` when subscription can recover locally.\n- Use `pipe(source, ...)`; never mutate stream definitions.\n- Bound `mergeMap()` concurrency and queue capacity.\n- Bound `concatMap()` queue capacity.\n- Bound `toArray()` with realistic `maxItems`.\n- Choose async iterator overflow policy deliberately.\n- Keep timer-backed durations between 0 and 2,147,483,647 milliseconds.\n- Keep `Channel.send()` at integration boundaries.\n",
7
7
  "examples": "---\ntitle: Flux — Examples\ndescription: Practical examples and recipes for @vielzeug/flux.\n---\n\n## Examples\n\n- [Debounced Search Input](./examples/debounce-search.md)\n- [Combining Streams with combineLatest](./examples/combine-streams.md)\n- [Structural State Integration](./examples/signal-integration.md)\n"
8
8
  },
9
9
  "examples": [
@@ -24,12 +24,12 @@
24
24
  },
25
25
  {
26
26
  "id": "combination",
27
- "code": "// Combine current filter and page state from replaying channels.\nimport { combineLatest } from '@vielzeug/flux'\nimport { createChannel } from '@vielzeug/flux/subjects'\n\nconst count = createChannel({ initial: 0 })\nconst label = createChannel({ initial: 'items' })\n\ncombineLatest(count.stream, label.stream).subscribe({\n error: console.error,\n next: ([value, text]) => console.log(value, text),\n})\n\ncount.send(1) // 1 items\nlabel.send('tasks') // 1 tasks\ncount.dispose()\nlabel.dispose()",
27
+ "code": "// Combine current filter and page state from replaying channels.\nimport { combineLatest } from '@vielzeug/flux'\nimport { createChannel } from '@vielzeug/flux'\n\nconst count = createChannel({ initial: 0 })\nconst label = createChannel({ initial: 'items' })\n\ncombineLatest(count.stream, label.stream).subscribe({\n error: console.error,\n next: ([value, text]) => console.log(value, text),\n})\n\ncount.send(1) // 1 items\nlabel.send('tasks') // 1 tasks\ncount.dispose()\nlabel.dispose()",
28
28
  "name": "Combining Streams"
29
29
  },
30
30
  {
31
31
  "id": "error-handling",
32
- "code": "// Retry a transient producer error with a bounded attempt count.\nimport { toArray, pipe, retry, stream } from '@vielzeug/flux'\n\nlet attempt = 0\nconst source = stream((sink) => {\n attempt++\n console.log('attempt:', attempt)\n\n if (attempt < 3) {\n sink.error(new Error('temporary failure'))\n return\n }\n\n sink.next('success')\n sink.complete()\n})\n\ntoArray(pipe(source, retry({ attempts: 2 })), { maxItems: 1 })\n .then((values) => console.log('result:', values))\n .catch(console.error)",
32
+ "code": "// Retry a transient producer error with a bounded attempt count.\nimport { toArray, pipe, retry, stream } from '@vielzeug/flux'\n\nlet attempt = 0\nconst source = stream((sink) => {\n attempt++\n console.log('attempt:', attempt)\n\n if (attempt < 3) {\n sink.error(new Error('temporary failure'))\n return\n }\n\n sink.next('success')\n sink.complete()\n})\n\ntoArray(pipe(source, retry({ times: 2 })), { maxItems: 1 })\n .then((values) => console.log('result:', values))\n .catch(console.error)",
33
33
  "name": "Error Handling"
34
34
  },
35
35
  {
@@ -39,12 +39,15 @@
39
39
  },
40
40
  {
41
41
  "id": "subjects",
42
- "code": "// Replay latest events to late subscribers while keeping send() at one boundary.\nimport { createChannel } from '@vielzeug/flux/subjects'\n\nconst events = createChannel({ replay: 2 })\n\nevents.stream.subscribe({\n error: console.error,\n next: (value) => console.log('first:', value),\n})\nevents.send('connected')\nevents.send('ready')\nevents.send('updated')\n\nevents.stream.subscribe({\n error: console.error,\n next: (value) => console.log('late:', value),\n})\n// late: ready\n// late: updated\n\nevents.dispose()",
42
+ "code": "// Replay latest events to late subscribers while keeping send() at one boundary.\nimport { createChannel } from '@vielzeug/flux'\n\nconst events = createChannel({ replay: 2 })\n\nevents.stream.subscribe({\n error: console.error,\n next: (value) => console.log('first:', value),\n})\nevents.send('connected')\nevents.send('ready')\nevents.send('updated')\n\nevents.stream.subscribe({\n error: console.error,\n next: (value) => console.log('late:', value),\n})\n// late: ready\n// late: updated\n\nevents.dispose()",
43
43
  "name": "Channels"
44
44
  }
45
45
  ],
46
46
  "typeSignatures": {
47
47
  "toAsyncIterable": "export { toAsyncIterable } from './async.js';",
48
+ "Channel": "export type { Channel, ChannelOptions } from './channel.js';",
49
+ "ChannelOptions": "export type { Channel, ChannelOptions } from './channel.js';",
50
+ "createChannel": "export { createChannel } from './channel.js';",
48
51
  "fromStore": "export { fromStore, fromSubscribe, stream } from './core.js';",
49
52
  "fromSubscribe": "export { fromStore, fromSubscribe, stream } from './core.js';",
50
53
  "stream": "export { fromStore, fromSubscribe, stream } from './core.js';",
@@ -1,9 +1,9 @@
1
1
  {
2
- "apiSource": "export type {\n ListKeyAction,\n ListKeyResult,\n ListNavigation,\n ListNavigationAction,\n ListNavigationChange,\n ListNavigationOptions,\n ListNavigationTypeaheadOptions,\n MaybeGetter,\n} from './list-navigation.js';\nexport { createListNavigation } from './list-navigation.js';\nexport type {\n CaptureFocusOptions,\n FocusRestorer,\n FocusTarget,\n RestoreFocusOptions,\n} from './restore-focus.js';\nexport { captureFocus, restoreFocus } from './restore-focus.js';\n",
2
+ "apiSource": "export { FocusConfigError, FocusError } from './errors.js';\nexport type {\n GridColumns,\n GridKeyResult,\n GridNavigation,\n GridNavigationAction,\n GridNavigationChange,\n GridNavigationOptions,\n} from './grid-navigation.js';\nexport { createGridNavigation } from './grid-navigation.js';\nexport type {\n ListKeyAction,\n ListKeyResult,\n ListNavigation,\n ListNavigationAction,\n ListNavigationChange,\n ListNavigationOptions,\n ListNavigationTypeaheadOptions,\n MaybeGetter,\n} from './list-navigation.js';\nexport { createListNavigation } from './list-navigation.js';\nexport type {\n CaptureFocusOptions,\n FocusRestorer,\n FocusTarget,\n RestoreFocusOptions,\n} from './restore-focus.js';\nexport { captureFocus, rescueFocus, restoreFocus } from './restore-focus.js';\n",
3
3
  "docs": {
4
- "index": "---\ntitle: Focus — Navigation and restoration\ndescription: Framework-neutral list navigation and focus restoration primitives.\npackage: focus\ncategory: input\nkeywords: [focus, roving, keyboard, accessibility, list navigation]\nexports: [createListNavigation, captureFocus, restoreFocus]\nrelated: [refine, keymap, ore]\nenvironments: [browser]\n---\n\n<!-- markdownlint-disable MD025 MD033 MD060 -->\n\n<PackageHero package=\"focus\" />\n\n## Why Focus?\n\nComposite widgets need consistent keyboard navigation and predictable return focus behavior. Focus centralizes those primitives without coupling to component rendering or framework state.\n\n```ts\n// Before\nlist.addEventListener('keydown', (event) => {\n // arrow/home/end bookkeeping, disabled filtering, wrapping\n});\n\n// After\nconst nav = createListNavigation({ getItems });\nlist.addEventListener('keydown', (event) => {\n nav.handleKeydown(event)?.change?.item.focus();\n});\n```\n\n| Feature | Per-component navigation | Focus |\n| --- | --- | --- |\n| Bundle size | n/a | <PackageInfo package=\"focus\" type=\"size\" /> |\n| Zero dependencies | n/a | <ore-icon name=\"check\" size=\"16\"></ore-icon> |\n| RTL mirroring | Manual | Built in |\n| Typeahead | Manual | Optional via `typeahead` |\n| Focus restoration | Manual capture | `captureFocus()` / `restoreFocus()` |\n\n<div class=\"decision-callout\">\n\n**Use Focus when** a widget needs arrow-key navigation, Home/End, and controlled focus restoration.\n\n**Consider direct focus calls when** interaction is a single isolated element with no composite navigation.\n\n</div>\n\n## Installation\n\n::: code-group\n\n```sh [pnpm]\npnpm add @vielzeug/focus\n```\n\n```sh [npm]\nnpm install @vielzeug/focus\n```\n\n```sh [yarn]\nyarn add @vielzeug/focus\n```\n\n:::\n\n## Quick Start\n\n```ts\nimport { captureFocus, createListNavigation } from '@vielzeug/focus';\n\nconst restore = captureFocus();\nconst nav = createListNavigation({\n getItems: () => items,\n loop: true,\n});\nconst onKeydown = (event: KeyboardEvent) => nav.handleKeydown(event)?.change?.item.focus();\n\ncontainer.addEventListener('keydown', onKeydown);\n\nrestore();\ncontainer.removeEventListener('keydown', onKeydown);\n```\n\n## Features\n\n<div class=\"features-grid\">\n\n- `createListNavigation()` — pure composite navigation with explicit handled/change results\n- Orientation and direction support — vertical/horizontal/both with LTR/RTL defaults\n- Dynamic item queries — disabled filtering and loop control\n- Optional typeahead — label-based navigation in key-driven lists\n- `captureFocus()` and `restoreFocus()` — explicit return-focus helpers\n\n</div>\n\n## Documentation\n\n<div class=\"doc-links\">\n\n- [Usage Guide](./usage.md)\n- [API Reference](./api.md)\n- [Examples](./examples.md)\n- [Migration Guide](./migration.md)\n\n</div>\n\n## See Also\n\n<div class=\"see-also\">\n\n- [Refine](/refine/) — component primitives integrating list navigation.\n- [Keymap](/keymap/) — global and scoped keyboard shortcuts.\n- [Ore](/ore/) — lifecycle ownership used by consumer components.\n\n</div>\n\n<!-- markdownlint-enable MD025 MD033 MD060 -->\n",
5
- "api": "---\ntitle: Focus — API Reference\ndescription: API reference for @vielzeug/focus navigation and restoration primitives.\n---\n\n[[toc]]\n\n## API Overview\n\n| Symbol | Purpose | Execution mode | Common gotcha |\n| --- | --- | --- | --- |\n| `createListNavigation()` | Build keyboard navigation for composite widgets | Sync | Apply returned changes to DOM focus |\n| `restoreFocus()` | Restore focus to a target or fallback | Sync | Returns `false` when neither target can receive focus |\n| `captureFocus()` | Capture active focus for one later restoration | Sync | The returned function is one-shot |\n\n## Package Entry Point\n\n| Import | Purpose |\n| --- | --- |\n| `@vielzeug/focus` | List navigation and focus restoration primitives. |\n\n## Core Functions\n\n### `createListNavigation()`\n\n```ts\nfunction createListNavigation<T>(options: ListNavigationOptions<T>): ListNavigation<T>;\n```\n\nCreates a keyboard navigation controller with an internal active index.\n\n| Parameter | Type | Description |\n| --- | --- | --- |\n| `options` | `ListNavigationOptions<T>` | Item lookup, key mapping, navigation, dynamic direction/orientation, and typeahead options. |\n\n**Returns:** `ListNavigation<T>`.\n\n**Example**\n\n```ts\nimport { createListNavigation } from '@vielzeug/focus';\n\nconst nav = createListNavigation({\n getItems: () => rows,\n isItemDisabled: (item) => item.matches('[aria-disabled=\"true\"]'),\n});\n\nconst result = nav.handleKeydown(event);\nresult?.change?.item.focus();\n```\n\n| Member | Return | Contract |\n| --- | --- | --- |\n| `handleKeydown(event)` | `ListKeyResult<T> \\| null` | Returns explicit handled/change state for navigation and typeahead keys. |\n| `navigate(action)` | `ListNavigationChange<T> \\| null` | Moves programmatically and returns the committed change. |\n| `set(index)` | `number` | Sets an integer usable index, or resets to `-1`. |\n| `reset()` | `void` | Clears the active index and typeahead sequence. |\n| `getIndex()` | `number` | Returns the current usable index, or `-1`. |\n| `getActiveItem()` | `T \\| undefined` | Returns the item at the current usable index. |\n\nBoundary navigation keys return `{ handled: true, change: null }` and remain consumed. Unrecognized, already-prevented, composing, and disabled events return `null`. Successful typeahead returns a change and follows `typeahead.preventDefault`.\n\n---\n\n### `restoreFocus()`\n\n```ts\nfunction restoreFocus(target: FocusTarget, options?: RestoreFocusOptions): boolean;\n```\n\nAttempts to focus a connected target that is neither disabled nor inert. Throwing target getters or `focus()` implementations count as failed attempts and fall through to the configured fallback.\n\n| Parameter | Type | Description |\n| --- | --- | --- |\n| `target` | `FocusTarget` | Element or getter resolved when `restoreFocus()` is called. |\n| `options` | `RestoreFocusOptions` | Optional lazy fallback and `preventScroll` flag. |\n\n**Returns:** `boolean` — `true` when focus moved to the target or fallback.\n\n**Example**\n\n```ts\nimport { restoreFocus } from '@vielzeug/focus';\n\nrestoreFocus(() => triggerElement, {\n fallback: () => document.body,\n preventScroll: true,\n});\n```\n\n---\n\n### `captureFocus()`\n\n```ts\nfunction captureFocus(options?: CaptureFocusOptions): FocusRestorer;\n```\n\nCaptures the deepest active element immediately and returns a one-shot restoration function.\n\n| Parameter | Type | Description |\n| --- | --- | --- |\n| `options` | `CaptureFocusOptions` | Optional lazy fallback, `preventScroll`, and cancellation signal. |\n\n**Returns:** `FocusRestorer`. Its first call attempts restoration; later calls return `false`.\n\n**Example**\n\n```ts\nimport { captureFocus } from '@vielzeug/focus';\n\nconst restore = captureFocus({ fallback: () => document.body });\n\ndialog.showModal();\ndialog.addEventListener('close', restore, { once: true });\n```\n\n## Types\n\n```ts\ntype MaybeGetter<T> = T | (() => T);\n\ntype ListNavigationAction = 'first' | 'last' | 'next' | 'prev';\ntype ListKeyAction = ListNavigationAction | 'typeahead';\n\ntype ListNavigationChange<T> = {\n readonly action: ListKeyAction;\n readonly event?: KeyboardEvent;\n readonly index: number;\n readonly item: T;\n};\n\ntype ListKeyResult<T> = {\n readonly change: ListNavigationChange<T> | null;\n readonly handled: true;\n};\n\ntype ListNavigationTypeaheadOptions<T> = {\n delayMs?: number;\n getLabel: (item: T, index: number) => string;\n preventDefault?: boolean;\n};\n\ntype ListNavigationOptions<T> = {\n direction?: MaybeGetter<'ltr' | 'rtl'>;\n disabled?: MaybeGetter<boolean>;\n getItems: () => readonly T[];\n isItemDisabled?: (item: T, index: number) => boolean;\n keys?: Partial<Record<ListNavigationAction, readonly string[]>>;\n loop?: boolean;\n orientation?: MaybeGetter<'both' | 'horizontal' | 'vertical'>;\n typeahead?: ListNavigationTypeaheadOptions<T>;\n};\n\ntype ListNavigation<T> = {\n getActiveItem(): T | undefined;\n getIndex(): number;\n handleKeydown(event: KeyboardEvent): ListKeyResult<T> | null;\n navigate(action: ListNavigationAction): ListNavigationChange<T> | null;\n reset(): void;\n set(index: number): number;\n};\n\ntype FocusTarget = HTMLElement | SVGElement | null | undefined | (() => HTMLElement | SVGElement | null | undefined);\n\ntype RestoreFocusOptions = {\n fallback?: FocusTarget;\n preventScroll?: boolean;\n};\n\ntype CaptureFocusOptions = RestoreFocusOptions & {\n signal?: AbortSignal;\n};\n\ntype FocusRestorer = () => boolean;\n```\n\n`typeahead.delayMs` defaults to `500`; supplied values must be positive and finite. `preventDefault` defaults to `false`, which preserves editable combobox input. Set it to `true` for menu-style typeahead.\n\n## Errors\n\n`@vielzeug/focus` does not export custom error classes.\n",
6
- "usage": "---\ntitle: Focus — Usage Guide\ndescription: Build keyboard navigation and restore focus across transient browser interfaces.\n---\n\n[[toc]]\n\n## Basic Usage\n\nCreate one navigation model for the items in a composite widget. Focus remains an application side effect: apply the returned change after each key operation.\n\n```ts\nimport { createListNavigation } from '@vielzeug/focus';\n\nconst nav = createListNavigation({\n getItems: () => items,\n isItemDisabled: (item) => item.matches('[aria-disabled=\"true\"]'),\n loop: true,\n});\n\nconst onKeydown = (event: KeyboardEvent) => {\n const result = nav.handleKeydown(event);\n result?.change?.item.focus();\n};\n\nlist.addEventListener('keydown', onKeydown);\n```\n\n`createListNavigation()` owns only index and typeahead state. The layer that attaches the event listener owns its cleanup.\n\n## Inspect Keyboard Results\n\n`handleKeydown()` returns `null` when Focus did not handle the event. Recognized keys return explicit handling state:\n\n```ts\nconst result = nav.handleKeydown(event);\n\nif (result?.handled && result.change) {\n console.log(result.change.action, result.change.index, result.change.item);\n}\n```\n\nA boundary arrow may return `{ handled: true, change: null }`: the composite consumed the key but its active item did not move. Events that are already prevented, disabled, or part of IME composition return `null`.\n\nProgrammatic navigation returns only committed changes:\n\n```ts\nnav.navigate('first')?.item.focus();\nnav.navigate('next')?.item.focus();\nnav.navigate('prev')?.item.focus();\nnav.navigate('last')?.item.focus();\n```\n\n`set(index)` accepts integer indexes. Invalid or disabled targets reset navigation to `-1`.\n\n## Configure Orientation and Direction\n\nDirection, orientation, and disabled state may be live getters:\n\n```ts\nconst nav = createListNavigation({\n direction: () => document.documentElement.dir === 'rtl' ? 'rtl' : 'ltr',\n disabled: () => panel.hidden,\n getItems: () => items,\n orientation: () => 'horizontal',\n});\n```\n\nHorizontal arrows mirror in RTL. Vertical arrows do not. Custom key tables override defaults; assigning one key to multiple actions throws `RangeError`.\n\n## Add Typeahead\n\nProvide stable labels for menu, listbox, or command navigation:\n\n```ts\nconst nav = createListNavigation({\n getItems: () => menuItems,\n typeahead: {\n delayMs: 300,\n getLabel: (item) => item.textContent ?? '',\n preventDefault: true,\n },\n});\n```\n\n`delayMs` defaults to `500` and must be positive and finite. Repeated characters cycle matching items. Set `preventDefault: true` for menu-style typeahead; leave it false when an editable combobox input must receive the printable key.\n\n## Restore Focus\n\nCapture focus before opening a transient surface, then invoke the one-shot restorer when it closes:\n\n```ts\nimport { captureFocus } from '@vielzeug/focus';\n\nconst restore = captureFocus({\n fallback: () => document.querySelector<HTMLElement>('#main'),\n preventScroll: true,\n});\n\ndialog.showModal();\ndialog.addEventListener('close', restore, { once: true });\n```\n\nA supplied signal can cancel pending restoration:\n\n```ts\nconst controller = new AbortController();\nconst restore = captureFocus({ signal: controller.signal });\n\ncontroller.abort();\nrestore(); // false\n```\n\nUse `restoreFocus()` directly when the target is already known. Disconnected, disabled, inert, throwing, or non-focusable targets fall through to the lazy fallback.\n\n## Framework Integration\n\nCreate navigation once per mounted widget. Framework cleanup removes only the event listener because the navigation model owns no external resources.\n\n::: code-group\n\n```tsx [React]\nimport { useEffect, useRef } from 'react';\nimport { createListNavigation } from '@vielzeug/focus';\n\nfunction Tabs({ tabs }: { tabs: string[] }) {\n const listRef = useRef<HTMLDivElement>(null);\n const tabRefs = useRef<Array<HTMLButtonElement | null>>([]);\n\n useEffect(() => {\n const list = listRef.current;\n if (!list) return;\n\n const nav = createListNavigation({\n getItems: () => tabRefs.current.filter((item): item is HTMLButtonElement => item !== null),\n loop: true,\n orientation: 'horizontal',\n });\n const onKeydown = (event: KeyboardEvent) => nav.handleKeydown(event)?.change?.item.focus();\n\n list.addEventListener('keydown', onKeydown);\n return () => list.removeEventListener('keydown', onKeydown);\n }, []);\n\n return (\n <div ref={listRef} role=\"tablist\">\n {tabs.map((tab, index) => (\n <button key={tab} ref={(element) => { tabRefs.current[index] = element; }}>{tab}</button>\n ))}\n </div>\n );\n}\n```\n\n```ts [Vue]\nimport { onMounted, onUnmounted, ref } from 'vue';\nimport { createListNavigation } from '@vielzeug/focus';\n\nconst list = ref<HTMLElement>();\nlet onKeydown: ((event: KeyboardEvent) => void) | undefined;\n\nonMounted(() => {\n const nav = createListNavigation({ getItems: () => items, orientation: 'horizontal' });\n onKeydown = (event) => nav.handleKeydown(event)?.change?.item.focus();\n list.value?.addEventListener('keydown', onKeydown);\n});\n\nonUnmounted(() => {\n if (onKeydown) list.value?.removeEventListener('keydown', onKeydown);\n});\n```\n\n```ts [Svelte]\nimport { onMount } from 'svelte';\nimport { createListNavigation } from '@vielzeug/focus';\n\nonMount(() => {\n const nav = createListNavigation({ getItems: () => items, orientation: 'horizontal' });\n const onKeydown = (event: KeyboardEvent) => nav.handleKeydown(event)?.change?.item.focus();\n list.addEventListener('keydown', onKeydown);\n return () => list.removeEventListener('keydown', onKeydown);\n});\n```\n\n:::\n\n## Integrate with Keymap\n\nUse Focus for local composite navigation and Keymap for application shortcuts:\n\n```ts\nimport { createListNavigation } from '@vielzeug/focus';\nimport { createKeymap } from '@vielzeug/keymap';\n\nconst nav = createListNavigation({ getItems: () => items });\nconst onKeydown = (event: KeyboardEvent) => nav.handleKeydown(event)?.change?.item.focus();\nconst map = createKeymap([\n { id: 'palette', shortcut: 'mod+k', handler: () => openPalette() },\n { id: 'reset', shortcut: 'escape', handler: () => nav.reset() },\n]);\n\nlist.addEventListener('keydown', onKeydown);\nmap.mount(document);\n```\n\n## Best Practices\n\n- Keep item discovery in one live `getItems()` function.\n- Apply focus from returned changes rather than hiding effects in navigation state.\n- Treat `{ handled: true, change: null }` as a consumed boundary key.\n- Ignore events before Focus when an inner widget already called `preventDefault()`.\n- Reset navigation when an overlay closes or its item context changes.\n- Use typeahead only with stable labels, and set `preventDefault` according to whether the keyboard surface is editable.\n- Capture return focus before opening transient surfaces.\n- Remove the owning event listener when a widget unmounts.\n",
4
+ "index": "---\ntitle: Focus — Navigation and restoration\ndescription: Framework-neutral list navigation and focus restoration primitives.\npackage: focus\ncategory: input\nkeywords: [focus, roving, keyboard, accessibility, list navigation, grid navigation]\nexports: [createListNavigation, createGridNavigation, captureFocus, restoreFocus, rescueFocus]\nrelated: [refine, keymap, ore]\nenvironments: [browser]\n---\n\n<!-- markdownlint-disable MD025 MD033 MD060 -->\n\n<PackageHero package=\"focus\" />\n\n## Why Focus?\n\nComposite widgets need consistent keyboard navigation and predictable return focus behavior. Focus centralizes those primitives without coupling to component rendering or framework state.\n\n```ts\n// Before\nlist.addEventListener('keydown', (event) => {\n // arrow/home/end bookkeeping, disabled filtering, wrapping\n});\n\n// After\nconst nav = createListNavigation({ getItems });\nlist.addEventListener('keydown', (event) => {\n nav.handleKeydown(event)?.change?.item.focus();\n});\n```\n\n| Feature | Per-component navigation | Focus |\n| --- | --- | --- |\n| Bundle size | n/a | <PackageInfo package=\"focus\" type=\"size\" /> |\n| Zero dependencies | n/a | <ore-icon name=\"check\" size=\"16\"></ore-icon> |\n| RTL mirroring | Manual | Built in |\n| Grid navigation | Flat only | `createGridNavigation()` |\n| Typeahead | Manual | Optional via `typeahead` |\n| Focus restoration | Manual capture | `captureFocus()` / `restoreFocus()` |\n\n<div class=\"decision-callout\">\n\n**Use Focus when** a widget needs arrow-key navigation, Home/End, and controlled focus restoration.\n\n**Consider direct focus calls when** interaction is a single isolated element with no composite navigation.\n\n</div>\n\n## Installation\n\n::: code-group\n\n```sh [pnpm]\npnpm add @vielzeug/focus\n```\n\n```sh [npm]\nnpm install @vielzeug/focus\n```\n\n```sh [yarn]\nyarn add @vielzeug/focus\n```\n\n:::\n\n## Quick Start\n\n```ts\nimport { captureFocus, createListNavigation } from '@vielzeug/focus';\n\nconst restore = captureFocus();\nconst nav = createListNavigation({\n getItems: () => items,\n loop: true,\n});\nconst onKeydown = (event: KeyboardEvent) => nav.handleKeydown(event)?.change?.item.focus();\n\ncontainer.addEventListener('keydown', onKeydown);\n\nrestore();\ncontainer.removeEventListener('keydown', onKeydown);\n```\n\n## Features\n\n<div class=\"features-grid\">\n\n- `createListNavigation()` — pure composite navigation with explicit handled/change results\n- `createGridNavigation()` — two-dimensional arrow-key navigation with fixed or measured columns\n- Orientation and direction support — vertical/horizontal/both with LTR/RTL defaults\n- Dynamic item queries — disabled filtering and loop control\n- Optional typeahead — label-based navigation in key-driven lists\n- `captureFocus()`, `restoreFocus()`, and `rescueFocus()` — explicit return-focus and unmount-rescue helpers\n\n</div>\n\n## Documentation\n\n<div class=\"doc-links\">\n\n- [Usage Guide](./usage.md)\n- [API Reference](./api.md)\n- [Examples](./examples.md)\n- [Migration Guide](./migration.md)\n\n</div>\n\n## See Also\n\n<div class=\"see-also\">\n\n- [Refine](/refine/) — component primitives integrating list navigation.\n- [Keymap](/keymap/) — global and scoped keyboard shortcuts.\n- [Ore](/ore/) — lifecycle ownership used by consumer components.\n\n</div>\n\n<!-- markdownlint-enable MD025 MD033 MD060 -->\n",
5
+ "api": "---\ntitle: Focus — API Reference\ndescription: API reference for @vielzeug/focus navigation and restoration primitives.\n---\n\n[[toc]]\n\n## API Overview\n\n| Symbol | Purpose | Execution mode | Common gotcha |\n| --- | --- | --- | --- |\n| `createListNavigation()` | Build keyboard navigation for composite widgets | Sync | Apply returned changes to DOM focus |\n| `createGridNavigation()` | Build two-dimensional arrow-key navigation for grids | Sync | Columns resolve per navigation — responsive grids need a getter |\n| `restoreFocus()` | Restore focus to a target or fallback | Sync | Returns `false` when neither target can receive focus |\n| `rescueFocus()` | Re-home focus after the focused element unmounts | Sync | Returns `false` when focus is already on a real element |\n| `captureFocus()` | Capture active focus for one later restoration | Sync | The returned function is one-shot |\n\n## Package Entry Point\n\n| Import | Purpose |\n| --- | --- |\n| `@vielzeug/focus` | List navigation and focus restoration primitives. |\n\n## Core Functions\n\n### `createListNavigation()`\n\n```ts\nfunction createListNavigation<T>(options: ListNavigationOptions<T>): ListNavigation<T>;\n```\n\nCreates a keyboard navigation controller with an internal active index.\n\n| Parameter | Type | Description |\n| --- | --- | --- |\n| `options` | `ListNavigationOptions<T>` | Item lookup, key mapping, navigation, dynamic direction/orientation, and typeahead options. |\n\n**Returns:** `ListNavigation<T>`.\n\n**Example**\n\n```ts\nimport { createListNavigation } from '@vielzeug/focus';\n\nconst nav = createListNavigation({\n getItems: () => rows,\n isItemDisabled: (item) => item.matches('[aria-disabled=\"true\"]'),\n});\n\nconst result = nav.handleKeydown(event);\nresult?.change?.item.focus();\n```\n\n| Member | Return | Contract |\n| --- | --- | --- |\n| `handleKeydown(event)` | `ListKeyResult<T> \\| null` | Returns explicit handled/change state for navigation and typeahead keys. |\n| `navigate(action)` | `ListNavigationChange<T> \\| null` | Moves programmatically and returns the committed change. |\n| `set(index)` | `number` | Sets an integer usable index, or resets to `-1`. |\n| `reset()` | `void` | Clears the active index and typeahead sequence. |\n| `getIndex()` | `number` | Returns the current usable index, or `-1`. |\n| `getActiveItem()` | `T \\| undefined` | Returns the item at the current usable index. |\n\nBoundary navigation keys return `{ handled: true, change: null }` and remain consumed. Unrecognized, already-prevented, composing, and disabled events return `null`. Successful typeahead returns a change and follows `typeahead.preventDefault`.\n\nKeys are matched through `matchKey` from `@vielzeug/keymap`, so `keys` overrides accept shortcut patterns with aliases (`esc`, `space`, `up`) and modifiers (`shift+Home`), and modifier state must match exactly: a plain `ArrowDown` binding does not fire on Ctrl+ArrowDown.\n\n---\n\n### `createGridNavigation()`\n\n```ts\nfunction createGridNavigation<T>(options: GridNavigationOptions<T>): GridNavigation<T>;\n```\n\nCreates a two-dimensional keyboard navigation controller: horizontal arrows step one item, vertical arrows step one row (the column count), Home/End jump to the grid's ends.\n\n| Parameter | Type | Description |\n| --- | --- | --- |\n| `options` | `GridNavigationOptions<T>` | Item lookup, column resolution, active-index source, key mapping, and wrapping options. |\n\n**Returns:** `GridNavigation<T>`.\n\n**Example**\n\n```ts\nimport { createGridNavigation } from '@vielzeug/focus';\n\nconst grid = createGridNavigation<HTMLElement>({\n columns: 4,\n getActiveIndex: () => tiles().indexOf(document.activeElement as HTMLElement),\n getItems: tiles,\n});\n\nconst result = grid.handleKeydown(event);\nif (result?.change) result.change.item.focus();\n```\n\n| Member | Return | Contract |\n| --- | --- | --- |\n| `handleKeydown(event)` | `GridKeyResult<T> \\| null` | Returns handled state for navigation keys; recognized keys are always consumed, even at clamped edges. |\n| `navigate(action)` | `GridNavigationChange<T> \\| null` | Moves programmatically and returns the committed change. |\n| `set(index)` | `void` | Sets the tracked index for grids without `getActiveIndex`. |\n| `reset()` | `void` | Clears the tracked index. |\n| `getIndex()` | `number` | Returns the active index — derived from `getActiveIndex` when provided — or `-1`. |\n| `getActiveItem()` | `T \\| undefined` | Returns the item at the active index. |\n\nUnlike `createListNavigation`, items are not skipped when disabled — skipping in two dimensions would break row alignment. With no active index, forward moves start at the first item and backward moves at the last. `columns` may be a getter resolved on every navigation, so responsive grids can read a media query and measured grids can read the rendered row length. `FocusConfigError` is thrown when `columns` resolves below `1`.\n\n---\n\n### `restoreFocus()`\n\n```ts\nfunction restoreFocus(target: FocusTarget, options?: RestoreFocusOptions): boolean;\n```\n\nAttempts to focus a connected target that is neither disabled nor inert. Throwing target getters or `focus()` implementations count as failed attempts and fall through to the configured fallback.\n\n| Parameter | Type | Description |\n| --- | --- | --- |\n| `target` | `FocusTarget` | Element or getter resolved when `restoreFocus()` is called. |\n| `options` | `RestoreFocusOptions` | Optional lazy fallback and `preventScroll` flag. |\n\n**Returns:** `boolean` — `true` when focus moved to the target or fallback.\n\n**Example**\n\n```ts\nimport { restoreFocus } from '@vielzeug/focus';\n\nrestoreFocus(() => triggerElement, {\n fallback: () => document.body,\n preventScroll: true,\n});\n```\n\n---\n\n### `rescueFocus()`\n\n```ts\nfunction rescueFocus(target: FocusTarget, options?: RestoreFocusOptions): boolean;\n```\n\nHands focus to `target` when focus has been lost to the document body — the state left behind when the focused element unmounts mid-swap, where keydown never reaches a handler.\n\n| Parameter | Type | Description |\n| --- | --- | --- |\n| `target` | `FocusTarget` | Element or getter resolved when the rescue runs. |\n| `options` | `RestoreFocusOptions` | Optional lazy fallback and `preventScroll` flag, as in `restoreFocus()`. |\n\n**Returns:** `boolean` — `true` when focus was rescued; `false` when focus is already on a real element or neither target can receive focus.\n\n**Example**\n\n```ts\nimport { rescueFocus } from '@vielzeug/focus';\n\n// After a swap that may have unmounted the focused element.\nrescueFocus(() => dialog.querySelector<HTMLElement>('footer button'));\n```\n\n---\n\n### `captureFocus()`\n\n```ts\nfunction captureFocus(options?: CaptureFocusOptions): FocusRestorer;\n```\n\nCaptures the deepest active element immediately and returns a one-shot restoration function.\n\n| Parameter | Type | Description |\n| --- | --- | --- |\n| `options` | `CaptureFocusOptions` | Optional lazy fallback, `preventScroll`, and cancellation signal. |\n\n**Returns:** `FocusRestorer`. Its first call attempts restoration; later calls return `false`.\n\n**Example**\n\n```ts\nimport { captureFocus } from '@vielzeug/focus';\n\nconst restore = captureFocus({ fallback: () => document.body });\n\ndialog.showModal();\ndialog.addEventListener('close', restore, { once: true });\n```\n\n## Types\n\n```ts\ntype MaybeGetter<T> = T | (() => T);\n\ntype ListNavigationAction = 'first' | 'last' | 'next' | 'prev';\ntype ListKeyAction = ListNavigationAction | 'typeahead';\n\ntype ListNavigationChange<T> = {\n readonly action: ListKeyAction;\n readonly event?: KeyboardEvent;\n readonly index: number;\n readonly item: T;\n};\n\ntype ListKeyResult<T> = {\n readonly change: ListNavigationChange<T> | null;\n readonly handled: true;\n};\n\ntype ListNavigationTypeaheadOptions<T> = {\n delayMs?: number;\n getLabel: (item: T, index: number) => string;\n preventDefault?: boolean;\n};\n\ntype ListNavigationOptions<T> = {\n direction?: MaybeGetter<'ltr' | 'rtl'>;\n disabled?: MaybeGetter<boolean>;\n getItems: () => readonly T[];\n isItemDisabled?: (item: T, index: number) => boolean;\n keys?: Partial<Record<ListNavigationAction, readonly string[]>>; // shortcut patterns matched via @vielzeug/keymap\n loop?: boolean;\n orientation?: MaybeGetter<'both' | 'horizontal' | 'vertical'>;\n typeahead?: ListNavigationTypeaheadOptions<T>;\n};\n\ntype ListNavigation<T> = {\n getActiveItem(): T | undefined;\n getIndex(): number;\n handleKeydown(event: KeyboardEvent): ListKeyResult<T> | null;\n navigate(action: ListNavigationAction): ListNavigationChange<T> | null;\n reset(): void;\n set(index: number): number;\n};\n\ntype GridNavigationAction = 'first' | 'last' | 'next' | 'nextRow' | 'prev' | 'prevRow';\n\ntype GridNavigationChange<T> = {\n readonly action: GridNavigationAction;\n readonly event?: KeyboardEvent;\n readonly index: number;\n readonly item: T;\n};\n\ntype GridKeyResult<T> = {\n readonly change: GridNavigationChange<T> | null;\n readonly handled: true;\n};\n\ntype GridColumns = number | (() => number);\n\ntype GridNavigationOptions<T> = {\n columns: GridColumns;\n direction?: MaybeGetter<'ltr' | 'rtl'>;\n disabled?: MaybeGetter<boolean>;\n getActiveIndex?: () => number;\n getItems: () => readonly T[];\n keys?: Partial<Record<GridNavigationAction, readonly string[]>>;\n loop?: boolean;\n};\n\ntype GridNavigation<T> = {\n getActiveItem(): T | undefined;\n getIndex(): number;\n handleKeydown(event: KeyboardEvent): GridKeyResult<T> | null;\n navigate(action: GridNavigationAction): GridNavigationChange<T> | null;\n reset(): void;\n set(index: number): void;\n};\n\ntype FocusTarget = HTMLElement | SVGElement | null | undefined | (() => HTMLElement | SVGElement | null | undefined);\n\ntype RestoreFocusOptions = {\n fallback?: FocusTarget;\n preventScroll?: boolean;\n};\n\ntype CaptureFocusOptions = RestoreFocusOptions & {\n signal?: AbortSignal;\n};\n\ntype FocusRestorer = () => boolean;\n```\n\n`typeahead.delayMs` defaults to `500`; supplied values must be positive and finite. `preventDefault` defaults to `false`, which preserves editable combobox input. Set it to `true` for menu-style typeahead.\n\n## Errors\n\n| Error | Trigger | Notable properties |\n| --- | --- | --- |\n| `FocusError` | Base class for every focus-originated error | `instanceof FocusError` catches any focus error |\n| `FocusConfigError` | Invalid navigation configuration | Extends `FocusError`. Thrown for a key assigned to two actions, a non-positive `typeahead.delayMs`, or grid `columns` resolving below `1` |\n",
6
+ "usage": "---\ntitle: Focus — Usage Guide\ndescription: Build keyboard navigation and restore focus across transient browser interfaces.\n---\n\n[[toc]]\n\n## Basic Usage\n\nCreate one navigation model for the items in a composite widget. Focus remains an application side effect: apply the returned change after each key operation.\n\n```ts\nimport { createListNavigation } from '@vielzeug/focus';\n\nconst nav = createListNavigation({\n getItems: () => items,\n isItemDisabled: (item) => item.matches('[aria-disabled=\"true\"]'),\n loop: true,\n});\n\nconst onKeydown = (event: KeyboardEvent) => {\n const result = nav.handleKeydown(event);\n result?.change?.item.focus();\n};\n\nlist.addEventListener('keydown', onKeydown);\n```\n\n`createListNavigation()` owns only index and typeahead state. The layer that attaches the event listener owns its cleanup.\n\n## Inspect Keyboard Results\n\n`handleKeydown()` returns `null` when Focus did not handle the event. Recognized keys return explicit handling state:\n\n```ts\nconst result = nav.handleKeydown(event);\n\nif (result?.handled && result.change) {\n console.log(result.change.action, result.change.index, result.change.item);\n}\n```\n\nA boundary arrow may return `{ handled: true, change: null }`: the composite consumed the key but its active item did not move. Events that are already prevented, disabled, or part of IME composition return `null`.\n\nProgrammatic navigation returns only committed changes:\n\n```ts\nnav.navigate('first')?.item.focus();\nnav.navigate('next')?.item.focus();\nnav.navigate('prev')?.item.focus();\nnav.navigate('last')?.item.focus();\n```\n\n`set(index)` accepts integer indexes. Invalid or disabled targets reset navigation to `-1`.\n\n## Configure Orientation and Direction\n\nDirection, orientation, and disabled state may be live getters:\n\n```ts\nconst nav = createListNavigation({\n direction: () => document.documentElement.dir === 'rtl' ? 'rtl' : 'ltr',\n disabled: () => panel.hidden,\n getItems: () => items,\n orientation: () => 'horizontal',\n});\n```\n\nHorizontal arrows mirror in RTL. Vertical arrows do not. Custom key tables override defaults and are matched through `matchKey` from `@vielzeug/keymap`, so entries accept aliases and modifiers and require exact modifier state; assigning one key to multiple actions throws `FocusConfigError`.\n\n## Add Typeahead\n\nProvide stable labels for menu, listbox, or command navigation:\n\n```ts\nconst nav = createListNavigation({\n getItems: () => menuItems,\n typeahead: {\n delayMs: 300,\n getLabel: (item) => item.textContent ?? '',\n preventDefault: true,\n },\n});\n```\n\n`delayMs` defaults to `500` and must be positive and finite. Repeated characters cycle matching items. Set `preventDefault: true` for menu-style typeahead; leave it false when an editable combobox input must receive the printable key.\n\n## Navigate Grids\n\nUse `createGridNavigation()` when items lay out in rows: vertical arrows step one row, horizontal arrows one item, and Home/End jump to the ends. Point `getActiveIndex` at the DOM so focus moved by pointer or Tab stays in sync:\n\n```ts\nimport { createGridNavigation } from '@vielzeug/focus';\n\nconst tiles = () => [...grid.querySelectorAll<HTMLElement>('.tile')];\n\nconst nav = createGridNavigation<HTMLElement>({\n columns: 4,\n getActiveIndex: () => tiles().indexOf(document.activeElement as HTMLElement),\n getItems: tiles,\n});\n\ngrid.addEventListener('keydown', (event) => {\n nav.handleKeydown(event)?.change?.item.focus();\n});\n```\n\n`columns` resolves on every navigation, so responsive grids can read a media query and measured grids can read the rendered row length. Clamping is the default; set `loop: true` to wrap moves around the grid's edges by flat index. Items are never skipped when disabled — skipping in two dimensions would break row alignment.\n\n## Restore Focus\n\nCapture focus before opening a transient surface, then invoke the one-shot restorer when it closes:\n\n```ts\nimport { captureFocus } from '@vielzeug/focus';\n\nconst restore = captureFocus({\n fallback: () => document.querySelector<HTMLElement>('#main'),\n preventScroll: true,\n});\n\ndialog.showModal();\ndialog.addEventListener('close', restore, { once: true });\n```\n\nA supplied signal can cancel pending restoration:\n\n```ts\nconst controller = new AbortController();\nconst restore = captureFocus({ signal: controller.signal });\n\ncontroller.abort();\nrestore(); // false\n```\n\nUse `restoreFocus()` directly when the target is already known. Disconnected, disabled, inert, throwing, or non-focusable targets fall through to the lazy fallback.\n\nWhen the focused element unmounts — browsing a dialog whose swap removes the item that held focus — the browser drops focus to `<body>`, where keydown never reaches a handler. Call `rescueFocus()` after the swap to hand focus to a control that survives it:\n\n```ts\nimport { rescueFocus } from '@vielzeug/focus';\n\nconst onAfterSwap = () => {\n rescueFocus(() => footer.querySelector<HTMLElement>('button'));\n};\n```\n\n`rescueFocus()` returns `false` without touching anything when focus is already on a real element, so it is safe to call from both a watcher and a transition's after-leave hook.\n\n## Framework Integration\n\nCreate navigation once per mounted widget. Framework cleanup removes only the event listener because the navigation model owns no external resources.\n\n::: code-group\n\n```tsx [React]\nimport { useEffect, useRef } from 'react';\nimport { createListNavigation } from '@vielzeug/focus';\n\nfunction Tabs({ tabs }: { tabs: string[] }) {\n const listRef = useRef<HTMLDivElement>(null);\n const tabRefs = useRef<Array<HTMLButtonElement | null>>([]);\n\n useEffect(() => {\n const list = listRef.current;\n if (!list) return;\n\n const nav = createListNavigation({\n getItems: () => tabRefs.current.filter((item): item is HTMLButtonElement => item !== null),\n loop: true,\n orientation: 'horizontal',\n });\n const onKeydown = (event: KeyboardEvent) => nav.handleKeydown(event)?.change?.item.focus();\n\n list.addEventListener('keydown', onKeydown);\n return () => list.removeEventListener('keydown', onKeydown);\n }, []);\n\n return (\n <div ref={listRef} role=\"tablist\">\n {tabs.map((tab, index) => (\n <button key={tab} ref={(element) => { tabRefs.current[index] = element; }}>{tab}</button>\n ))}\n </div>\n );\n}\n```\n\n```ts [Vue]\nimport { onMounted, onUnmounted, ref } from 'vue';\nimport { createListNavigation } from '@vielzeug/focus';\n\nconst list = ref<HTMLElement>();\nlet onKeydown: ((event: KeyboardEvent) => void) | undefined;\n\nonMounted(() => {\n const nav = createListNavigation({ getItems: () => items, orientation: 'horizontal' });\n onKeydown = (event) => nav.handleKeydown(event)?.change?.item.focus();\n list.value?.addEventListener('keydown', onKeydown);\n});\n\nonUnmounted(() => {\n if (onKeydown) list.value?.removeEventListener('keydown', onKeydown);\n});\n```\n\n```ts [Svelte]\nimport { onMount } from 'svelte';\nimport { createListNavigation } from '@vielzeug/focus';\n\nonMount(() => {\n const nav = createListNavigation({ getItems: () => items, orientation: 'horizontal' });\n const onKeydown = (event: KeyboardEvent) => nav.handleKeydown(event)?.change?.item.focus();\n list.addEventListener('keydown', onKeydown);\n return () => list.removeEventListener('keydown', onKeydown);\n});\n```\n\n:::\n\n## Integrate with Keymap\n\nUse Focus for local composite navigation and Keymap for application shortcuts:\n\n```ts\nimport { createListNavigation } from '@vielzeug/focus';\nimport { createKeymap } from '@vielzeug/keymap';\n\nconst nav = createListNavigation({ getItems: () => items });\nconst onKeydown = (event: KeyboardEvent) => nav.handleKeydown(event)?.change?.item.focus();\nconst map = createKeymap([\n { id: 'palette', shortcut: 'mod+k', handler: () => openPalette() },\n { id: 'reset', shortcut: 'escape', handler: () => nav.reset() },\n]);\n\nlist.addEventListener('keydown', onKeydown);\nmap.mount(document);\n```\n\n## Best Practices\n\n- Keep item discovery in one live `getItems()` function.\n- Apply focus from returned changes rather than hiding effects in navigation state.\n- Treat `{ handled: true, change: null }` as a consumed boundary key.\n- Ignore events before Focus when an inner widget already called `preventDefault()`.\n- Reset navigation when an overlay closes or its item context changes.\n- Use typeahead only with stable labels, and set `preventDefault` according to whether the keyboard surface is editable.\n- Capture return focus before opening transient surfaces.\n- Rescue focus after swaps that can unmount the focused element.\n- Remove the owning event listener when a widget unmounts.\n",
7
7
  "examples": "---\ntitle: Focus — Examples\ndescription: Worked examples for @vielzeug/focus.\n---\n\n## Examples\n\n- [Roving Tabs Keyboard Navigation](./examples/roving-tabs-keyboard-navigation.md)\n- [Dialog Return Focus Restoration](./examples/dialog-return-focus-restoration.md)\n"
8
8
  },
9
9
  "examples": [
@@ -19,6 +19,15 @@
19
19
  }
20
20
  ],
21
21
  "typeSignatures": {
22
+ "FocusConfigError": "export { FocusConfigError, FocusError } from './errors.js';",
23
+ "FocusError": "export { FocusConfigError, FocusError } from './errors.js';",
24
+ "GridColumns": "export type {\n GridColumns,\n GridKeyResult,\n GridNavigation,\n GridNavigationAction,\n GridNavigationChange,\n GridNavigationOptions,\n} from './grid-navigation.js';",
25
+ "GridKeyResult": "export type {\n GridColumns,\n GridKeyResult,\n GridNavigation,\n GridNavigationAction,\n GridNavigationChange,\n GridNavigationOptions,\n} from './grid-navigation.js';",
26
+ "GridNavigation": "export type {\n GridColumns,\n GridKeyResult,\n GridNavigation,\n GridNavigationAction,\n GridNavigationChange,\n GridNavigationOptions,\n} from './grid-navigation.js';",
27
+ "GridNavigationAction": "export type {\n GridColumns,\n GridKeyResult,\n GridNavigation,\n GridNavigationAction,\n GridNavigationChange,\n GridNavigationOptions,\n} from './grid-navigation.js';",
28
+ "GridNavigationChange": "export type {\n GridColumns,\n GridKeyResult,\n GridNavigation,\n GridNavigationAction,\n GridNavigationChange,\n GridNavigationOptions,\n} from './grid-navigation.js';",
29
+ "GridNavigationOptions": "export type {\n GridColumns,\n GridKeyResult,\n GridNavigation,\n GridNavigationAction,\n GridNavigationChange,\n GridNavigationOptions,\n} from './grid-navigation.js';",
30
+ "createGridNavigation": "export { createGridNavigation } from './grid-navigation.js';",
22
31
  "ListKeyAction": "export type {\n ListKeyAction,\n ListKeyResult,\n ListNavigation,\n ListNavigationAction,\n ListNavigationChange,\n ListNavigationOptions,\n ListNavigationTypeaheadOptions,\n MaybeGetter,\n} from './list-navigation.js';",
23
32
  "ListKeyResult": "export type {\n ListKeyAction,\n ListKeyResult,\n ListNavigation,\n ListNavigationAction,\n ListNavigationChange,\n ListNavigationOptions,\n ListNavigationTypeaheadOptions,\n MaybeGetter,\n} from './list-navigation.js';",
24
33
  "ListNavigation": "export type {\n ListKeyAction,\n ListKeyResult,\n ListNavigation,\n ListNavigationAction,\n ListNavigationChange,\n ListNavigationOptions,\n ListNavigationTypeaheadOptions,\n MaybeGetter,\n} from './list-navigation.js';",
@@ -32,7 +41,8 @@
32
41
  "FocusRestorer": "export type {\n CaptureFocusOptions,\n FocusRestorer,\n FocusTarget,\n RestoreFocusOptions,\n} from './restore-focus.js';",
33
42
  "FocusTarget": "export type {\n CaptureFocusOptions,\n FocusRestorer,\n FocusTarget,\n RestoreFocusOptions,\n} from './restore-focus.js';",
34
43
  "RestoreFocusOptions": "export type {\n CaptureFocusOptions,\n FocusRestorer,\n FocusTarget,\n RestoreFocusOptions,\n} from './restore-focus.js';",
35
- "captureFocus": "export { captureFocus, restoreFocus } from './restore-focus.js';",
36
- "restoreFocus": "export { captureFocus, restoreFocus } from './restore-focus.js';"
44
+ "captureFocus": "export { captureFocus, rescueFocus, restoreFocus } from './restore-focus.js';",
45
+ "rescueFocus": "export { captureFocus, rescueFocus, restoreFocus } from './restore-focus.js';",
46
+ "restoreFocus": "export { captureFocus, rescueFocus, restoreFocus } from './restore-focus.js';"
37
47
  }
38
48
  }
@@ -1,9 +1,9 @@
1
1
  {
2
- "apiSource": "export {\n createDragGesture,\n type DragEndReason,\n type DragGesture,\n type DragGestureDetail,\n type DragGestureEndDetail,\n type DragGestureOptions,\n type DragPoint,\n} from './drag-gesture.js';\nexport {\n createPanGesture,\n type PanAxis,\n type PanEndReason,\n type PanGesture,\n type PanGestureDetail,\n type PanGestureEndDetail,\n type PanGestureOptions,\n} from './pan-gesture.js';\n",
2
+ "apiSource": "export {\n createDragGesture,\n type DragEndReason,\n type DragGesture,\n type DragGestureDetail,\n type DragGestureEndDetail,\n type DragGestureOptions,\n type DragPoint,\n} from './drag-gesture.js';\nexport { GestureConfigError, GestureError } from './errors.js';\nexport {\n createLongPress,\n type LongPress,\n type LongPressDetail,\n type LongPressOptions,\n} from './long-press.js';\nexport {\n createPanGesture,\n type PanAxis,\n type PanEndReason,\n type PanGesture,\n type PanGestureDetail,\n type PanGestureEndDetail,\n type PanGestureOptions,\n} from './pan-gesture.js';\n",
3
3
  "docs": {
4
- "index": "---\ntitle: Gesture — Pointer movement primitives\ndescription: Framework-neutral two-dimensional pointer drag and one-axis pan recognition with lifecycle-owned handles.\npackage: gesture\ncategory: input\nkeywords: [pointer, pan, swipe, gesture, touch, drag]\nexports: [createDragGesture, createPanGesture]\nrelated: [refine, dnd, keymap]\nenvironments: [browser]\n---\n\n<!-- markdownlint-disable MD025 MD033 MD060 -->\n\n<PackageHero package=\"gesture\" />\n\n## Why Gesture?\n\nPointer-driven interfaces need reliable one-pointer movement tracking without coupling recognition to rendering, drag-and-drop semantics, or product-specific completion policy.\n\n```ts\n// Before\nelement.addEventListener('pointermove', (event) => {\n // Coordinate tracking, pointer identity, direction locking, and cleanup\n});\n\n// After\nimport { createPanGesture } from '@vielzeug/gesture';\n\nconst pan = createPanGesture(element, {\n axis: 'x',\n onMove: ({ distance }) => console.log('distance', distance),\n onEnd: ({ reason }) => console.log('ended', reason),\n});\n```\n\n| Feature | Gesture | Ad-hoc pointer handling |\n| --- | --- | --- |\n| Bundle size | <PackageInfo package=\"gesture\" type=\"size\" /> | n/a |\n| Zero dependencies | <ore-icon name=\"check\" size=\"16\"></ore-icon> | n/a |\n| Two-dimensional drag tracking | Built in | Manual |\n| Axis intent recognition | Built in | Manual |\n| Pointer ownership | Tracked across the document | Manual |\n| Lifecycle cleanup | `dispose()` + `disposalSignal` | Manual |\n\n<div class=\"decision-callout\">\n\n**Use Gesture when** UI surfaces need consistent free drag or axis-locked pan tracking while retaining their own rendering and completion rules.\n\n**Consider direct pointer handling when** the interaction is isolated and does not need reusable lifecycle or direction-lock behavior.\n\n</div>\n\n## Installation\n\n::: code-group\n\n```sh [pnpm]\npnpm add @vielzeug/gesture\n```\n\n```sh [npm]\nnpm install @vielzeug/gesture\n```\n\n```sh [yarn]\nyarn add @vielzeug/gesture\n```\n\n:::\n\n## Quick Start\n\n```ts\nimport { createPanGesture } from '@vielzeug/gesture';\n\nconst element = document.querySelector<HTMLElement>('[data-swipe]');\nif (!element) throw new Error('Missing [data-swipe] element');\n\nconst pan = createPanGesture(element, {\n axis: 'x',\n onMove: ({ distance }) => {\n element.style.transform = `translateX(${distance}px)`;\n },\n onEnd: ({ distance, reason }) => {\n element.style.transform = '';\n if (reason === 'release' && Math.abs(distance) >= 48) {\n pan.dispose();\n element.remove();\n }\n },\n});\n\nwindow.addEventListener('pagehide', () => pan.dispose(), { once: true });\n```\n\n## Features\n\n<div class=\"features-grid\">\n\n- `createDragGesture()` — unrestricted two-dimensional pointer movement tracking\n- `createPanGesture()` — one-axis pointer movement with direction intent recognition\n- Direction locking — activates only when movement favors the configured axis\n- Configurable pointer capture — own the pointer by default or preserve native targeting\n- Configurable `activationDistance` — tune slop for touch density and component needs\n- Consumer-owned policy — completion thresholds, snapping, and outcomes stay in application code\n- Stable completion — one `onEnd` callback for release and cancellation\n- Lifecycle ownership — `dispose()`, `disposed`, `disposalSignal`, optional owner `signal`, and `[Symbol.dispose]()`\n\n</div>\n\n## Documentation\n\n<div class=\"doc-links\">\n\n- [Usage Guide](./usage.md)\n- [API Reference](./api.md)\n- [Examples](./examples.md)\n- [Migration Guide](./migration.md)\n\n</div>\n\n## See Also\n\n<div class=\"see-also\">\n\n- [Refine](/refine/) — components that use pan recognition for carousel, drawer, toast, and list interactions.\n- [Dnd](/dnd/) — builds touch sorting on `createDragGesture()` and adds files, previews, drop targets, keyboard reordering, and connected-list transactions.\n- [Keymap](/keymap/) — keyboard interaction primitives for complementary input paths.\n\n</div>\n\n<!-- markdownlint-enable MD025 MD033 MD060 -->\n",
5
- "api": "---\ntitle: Gesture — API Reference\ndescription: API reference for two-dimensional drag and one-axis pan recognition.\n---\n\n[[toc]]\n\n## API Overview\n\n| Symbol | Purpose | Execution mode | Common gotcha |\n| --- | --- | --- | --- |\n| `createDragGesture()` | Track unrestricted two-dimensional pointer movement | Sync | Does not provide DOM movement or drop semantics |\n| `createPanGesture()` | Track pointer movement projected onto one axis | Sync | Cross-axis intent ends a pending interaction |\n| `DragGesture` / `PanGesture` | Control recognition lifecycle | Sync | `dispose()` does not invoke `onEnd` |\n| `DragGestureOptions` | Configure free-drag recognition | Sync | `activationDistance` uses Euclidean distance |\n| `PanGestureOptions` | Configure axis-locked recognition | Sync | Completion thresholds belong in `onEnd` |\n\n## Package Entry Point\n\n| Import | Purpose |\n| --- | --- |\n| `@vielzeug/gesture` | Drag and pan factories, handles, callback details, options, axes, points, and end reasons |\n\n## Core Functions\n\n### `createDragGesture()`\n\n```ts\nfunction createDragGesture(target: Element, options?: DragGestureOptions): DragGesture;\n```\n\nTracks one primary pointer without restricting movement to an axis.\n\n| Parameter | Type | Description |\n| --- | --- | --- |\n| `target` | `Element` | Element that owns pointer admission and optional capture |\n| `options` | `DragGestureOptions` | Activation, admission, capture, callback, and lifecycle configuration |\n\n**Returns:** A lifecycle-owned `DragGesture` handle.\n\n**Example**\n\n```ts\nimport { createDragGesture } from '@vielzeug/gesture';\n\nconst drag = createDragGesture(element, {\n onMove: ({ delta }) => {\n element.style.translate = `${delta.x}px ${delta.y}px`;\n },\n onEnd: () => {\n element.style.translate = '';\n },\n});\n```\n\n---\n\n### `createPanGesture()`\n\n```ts\nfunction createPanGesture(target: Element, options?: PanGestureOptions): PanGesture;\n```\n\nTracks one primary pointer after movement establishes intent on the configured axis.\n\n| Parameter | Type | Description |\n| --- | --- | --- |\n| `target` | `Element` | Element that owns pointer admission and optional capture |\n| `options` | `PanGestureOptions` | Axis, activation, admission, capture, callback, and lifecycle configuration |\n\n**Returns:** A lifecycle-owned `PanGesture` handle.\n\n**Example**\n\n```ts\nimport { createPanGesture } from '@vielzeug/gesture';\n\nconst pan = createPanGesture(element, {\n axis: 'x',\n onEnd: ({ distance, reason }) => {\n if (reason === 'release' && Math.abs(distance) >= 48) dismiss();\n },\n});\n```\n\nBoth handles expose the same members:\n\n| Member | Type | Contract |\n| --- | --- | --- |\n| `active` | `boolean` | `true` after activation and before release or cancellation |\n| `cancel()` | `() => boolean` | Clears pending recognition or ends active recognition with `reason: 'cancel'`; returns whether a session existed |\n| `disposalSignal` | `AbortSignal` | Aborts when the handle is disposed |\n| `dispose()` | `() => void` | Aborts `disposalSignal`, clears recognition, releases capture, and detaches listeners; idempotent |\n| `disposed` | `boolean` | `true` after disposal |\n| `[Symbol.dispose]()` | `() => void` | Calls `dispose()` |\n\n## Types\n\n### Drag types\n\n```ts\ntype DragEndReason = 'cancel' | 'release';\n\ntype DragPoint = Readonly<{\n x: number;\n y: number;\n}>;\n\ntype DragGestureDetail = Readonly<{\n current: DragPoint;\n delta: DragPoint;\n event: PointerEvent;\n pointerId: number;\n pointerType: string;\n start: DragPoint;\n target: Element;\n}>;\n\ntype DragGestureEndDetail = DragGestureDetail &\n Readonly<{\n reason: DragEndReason;\n }>;\n\ntype DragGestureOptions = Readonly<{\n activationDistance?: number;\n disabled?: boolean | (() => boolean | undefined);\n onEnd?: (detail: DragGestureEndDetail) => void;\n onMove?: (detail: DragGestureDetail) => void;\n onStart?: (detail: DragGestureDetail) => void;\n pointerCapture?: boolean;\n shouldStart?: (event: PointerEvent) => boolean;\n signal?: AbortSignal;\n}>;\n```\n\n`start`, `current`, and `delta` use viewport `clientX` and `clientY` coordinates. Activation uses `Math.hypot(delta.x, delta.y)`.\n\n### Pan types\n\n```ts\ntype PanAxis = 'x' | 'y';\ntype PanEndReason = 'cancel' | 'release';\n\ntype PanGestureDetail = Readonly<{\n axis: PanAxis;\n current: number;\n distance: number;\n event: PointerEvent;\n pointerId: number;\n pointerType: string;\n start: number;\n target: Element;\n}>;\n\ntype PanGestureEndDetail = PanGestureDetail &\n Readonly<{\n reason: PanEndReason;\n }>;\n\ntype PanGestureOptions = Readonly<{\n activationDistance?: number;\n axis?: PanAxis | (() => PanAxis);\n disabled?: boolean | (() => boolean | undefined);\n onEnd?: (detail: PanGestureEndDetail) => void;\n onMove?: (detail: PanGestureDetail) => void;\n onStart?: (detail: PanGestureDetail) => void;\n pointerCapture?: boolean;\n shouldStart?: (event: PointerEvent) => boolean;\n signal?: AbortSignal;\n}>;\n```\n\nPan details project `start`, `current`, and `distance` onto `axis`. The default axis is `'x'`.\n\n### Handle types\n\n```ts\ntype DragGesture = {\n readonly active: boolean;\n [Symbol.dispose](): void;\n cancel(): boolean;\n readonly disposalSignal: AbortSignal;\n dispose(): void;\n readonly disposed: boolean;\n};\n\ntype PanGesture = DragGesture;\n```\n\n### Shared option behavior\n\n| Option | Default | Contract |\n| --- | --- | --- |\n| `activationDistance` | `6` | Non-negative finite CSS pixels; drag uses Euclidean distance, pan waits for dominant-axis intent |\n| `axis` | `'x'` | Pan only; fixed value or getter sampled when each interaction starts |\n| `disabled` | `false` | Fixed value or getter; blocks starts and cancels active recognition on the next matching pointer event |\n| `pointerCapture` | `true` | Best-effort capture after activation; document tracking continues when capture is disabled or fails |\n| `shouldStart` | — | Admission predicate evaluated for the primary pointer before tracking starts |\n| `signal` | — | Disposes the handle when the external owner aborts |\n| `onStart` | — | Runs once on the activating movement |\n| `onMove` | — | Runs on the activating movement and each later matching movement |\n| `onEnd` | — | Runs once for active release or cancellation; pending interactions end without callbacks |\n\nOptions and callback details are readonly. Fixed callbacks and capture policy are snapshotted at construction. Function-valued `axis` and `disabled` options remain dynamic.\n\nGesture tracks accepted movement with capture-phase listeners on the target's current owner document. Window blur, hidden-document transition, `pointercancel`, and `lostpointercapture` cancel active recognition.\n\n## Errors\n\nGesture does not export custom error classes.\n\n- Invalid `activationDistance` values throw `RangeError` during construction.\n- Invalid fixed `axis` values throw `RangeError` during construction.\n- Invalid values returned by a dynamic `axis` getter throw `RangeError` when a pointer interaction starts.\n",
6
- "usage": "---\ntitle: Gesture — Usage Guide\ndescription: Track unrestricted pointer dragging and axis-locked pans with application-owned rendering and completion.\n---\n\n[[toc]]\n\n## Basic Usage\n\nCreate one handle for the element that owns the interaction. This example creates its surface, applies the required touch policy, and disposes the handle when the page unloads.\n\n```ts\nimport { createPanGesture } from '@vielzeug/gesture';\n\nconst row = document.body.appendChild(document.createElement('div'));\nrow.textContent = 'Swipe to archive';\nrow.style.touchAction = 'pan-y';\n\nconst pan = createPanGesture(row, {\n axis: 'x',\n onMove: ({ distance }) => {\n row.style.transform = `translateX(${distance}px)`;\n },\n onEnd: ({ distance, reason }) => {\n row.style.transform = '';\n if (reason === 'release' && Math.abs(distance) >= 64) {\n pan.dispose();\n row.remove();\n }\n },\n});\n\nwindow.addEventListener('pagehide', () => pan.dispose(), { once: true });\n```\n\n## Track Two-Dimensional Dragging\n\nUse `createDragGesture()` when movement must remain unrestricted in both dimensions:\n\n```ts\nimport { createDragGesture } from '@vielzeug/gesture';\n\nconst card = document.querySelector<HTMLElement>('[data-draggable]');\nif (!card) throw new Error('Missing [data-draggable] element');\n\nconst drag = createDragGesture(card, {\n activationDistance: 6,\n onMove: ({ delta }) => {\n card.style.translate = `${delta.x}px ${delta.y}px`;\n },\n onEnd: () => {\n card.style.translate = '';\n },\n});\n\nwindow.addEventListener('pagehide', () => drag.dispose(), { once: true });\n```\n\n`start`, `current`, and `delta` are readonly `{ x, y }` points. Set `touch-action: none` when the surface owns movement in both dimensions. Drag recognition does not provide previews, drop targets, DOM movement, or `DataTransfer`; use Dnd for those semantics.\n\n## Completion Rules\n\nGesture reports movement and terminal state but does not decide what constitutes a swipe. Apply thresholds and allowed directions in `onEnd`.\n\n```ts\nconst pan = createPanGesture(panel, {\n axis: 'x',\n onEnd: ({ distance, reason }) => {\n if (reason === 'release' && distance <= -80) {\n openNext();\n } else {\n resetPanel();\n }\n },\n});\n```\n\n## Direction Recognition\n\nThe gesture remains pending during small movement. It activates only after movement favors the configured axis. Cross-axis movement ends the pending interaction without invoking callbacks.\n\nThe default `activationDistance` is 6 pixels. Tune direction-recognition slop for touch density or component-specific needs:\n\n```ts\nconst pan = createPanGesture(row, {\n axis: 'x',\n activationDistance: 12,\n onMove,\n onEnd,\n});\n```\n\nSet `activationDistance: 0` when the first pointer move with non-zero displacement should activate immediately. Zero-motion events remain pending.\n\nUse the corresponding `touch-action` value so the browser retains native scrolling on the other axis.\n\n```css\n.swipe-row {\n touch-action: pan-y;\n}\n```\n\n```ts\nconst pan = createPanGesture(row, { axis: 'x', onMove });\n```\n\n## Pointer Capture\n\nPointer capture is enabled by default. After axis intent is accepted, Gesture attempts to capture the pointer while continuing to track movement through document-level listeners. Capture failure—for example after target detachment or pointer termination—falls back to document tracking without stranding the session.\n\nDisable capture when nested or newly revealed controls must retain native pointer-up and click targeting:\n\n```ts\nconst pan = createPanGesture(row, {\n axis: 'x',\n pointerCapture: false,\n onMove: renderReveal,\n onEnd: settleReveal,\n});\n```\n\nDocument-level tracking still keeps the pan active outside the target. Disabling capture changes event targeting, not gesture tracking.\n\n## Interactive Descendants\n\nUse `shouldStart` when buttons, links, or form controls inside the surface must not start a pan.\n\n```ts\nconst pan = createPanGesture(notification, {\n axis: 'x',\n pointerCapture: false,\n shouldStart: (event) =>\n !event\n .composedPath()\n .some((node) => node instanceof Element && node.matches('button, a, input, select, textarea')),\n onMove,\n onEnd,\n});\n```\n\n`shouldStart` protects controls under the initial pointer. `pointerCapture: false` additionally protects controls that appear beneath the pointer during a reveal interaction.\n\n## Disabled State\n\nA boolean disables the recognizer permanently. A getter supports state that changes while the handle is alive.\n\n```ts\nconst pan = createPanGesture(row, {\n disabled: () => isLocked,\n onEnd: ({ reason }) => {\n if (reason === 'cancel') resetRow();\n },\n});\n```\n\nWhen the getter becomes `true`, the next pointer event cancels an active pan. Window blur or a hidden owner document also cancels active tracking so a lost release cannot block later gestures.\n\nGesture snapshots fixed callbacks and capture policy at construction. Use the `axis` and `disabled` functions for behavior that must remain dynamic instead of mutating the options object.\n\n## Lifecycle\n\nDispose the target-bound handle when its owning UI scope unmounts.\n\n```ts\nconst element = document.querySelector<HTMLElement>('[data-pan]');\nif (!element) throw new Error('Missing [data-pan] element');\n\nconst owner = new AbortController();\nconst request = new AbortController();\nconst pan = createPanGesture(element, {\n axis: 'x',\n signal: owner.signal,\n});\n\nconst childSignal = AbortSignal.any([pan.disposalSignal, request.signal]);\nwindow.addEventListener('pagehide', () => owner.abort(), { once: true });\n```\n\nPass `{ signal }` to dispose with an existing owner signal. `disposalSignal` always aborts when Gesture finishes teardown, so child work can compose with the handle regardless of how disposal starts.\n\nUse `cancel()` to stop a pending or active interaction without disposing the handle. An active interaction emits `onEnd` with `reason: 'cancel'`. Call `cancel()` before `dispose()` when a connected surface needs its `onEnd` reset path; disposal itself does not invoke callbacks.\n\n## Framework Integration\n\nCreate the handle after the target element exists and dispose it on unmount.\n\n::: code-group\n\n```tsx [React]\nimport { useEffect, useRef } from 'react';\nimport { createPanGesture } from '@vielzeug/gesture';\n\nfunction SwipeRow({ onDismiss }: { onDismiss: () => void }) {\n const rowRef = useRef<HTMLDivElement>(null);\n\n useEffect(() => {\n const row = rowRef.current;\n if (!row) return;\n\n const pan = createPanGesture(row, {\n axis: 'x',\n onMove: ({ distance }) => {\n row.style.transform = `translateX(${distance}px)`;\n },\n onEnd: ({ distance, reason }) => {\n row.style.transform = '';\n if (reason === 'release' && Math.abs(distance) >= 64) onDismiss();\n },\n });\n\n return () => pan.dispose();\n }, [onDismiss]);\n\n return <div ref={rowRef}>Swipe me</div>;\n}\n```\n\n```vue [Vue 3]\n<script setup lang=\"ts\">\nimport { onMounted, onUnmounted, ref } from 'vue';\nimport { createPanGesture, type PanGesture } from '@vielzeug/gesture';\n\nconst emit = defineEmits<{ dismiss: [] }>();\nconst rowEl = ref<HTMLDivElement | null>(null);\nlet pan: PanGesture | undefined;\n\nonMounted(() => {\n const row = rowEl.value;\n if (!row) return;\n\n pan = createPanGesture(row, {\n axis: 'x',\n onEnd: ({ distance, reason }) => {\n if (reason === 'release' && Math.abs(distance) >= 64) emit('dismiss');\n },\n });\n});\n\nonUnmounted(() => pan?.dispose());\n</script>\n\n<template>\n <div ref=\"rowEl\">Swipe me</div>\n</template>\n```\n\n```svelte [Svelte]\n<script lang=\"ts\">\n import { onMount } from 'svelte';\n import { createPanGesture } from '@vielzeug/gesture';\n\n let { ondismiss = () => {} }: { ondismiss: () => void } = $props();\n let rowEl: HTMLDivElement;\n\n onMount(() => {\n const pan = createPanGesture(rowEl, {\n axis: 'x',\n onEnd: ({ distance, reason }) => {\n if (reason === 'release' && Math.abs(distance) >= 64) ondismiss();\n },\n });\n\n return () => pan.dispose();\n });\n</script>\n\n<div bind:this={rowEl}>Swipe me</div>\n```\n\n:::\n\n## Working with Other Vielzeug Libraries\n\n### Gesture + Refine\n\nRefine uses Gesture internally for carousel, drawer, toast, and list-item pointer interactions. Custom surfaces can use the same pan lifecycle while keeping visual state local.\n\n```ts\nimport { createPanGesture } from '@vielzeug/gesture';\n\nconst pan = createPanGesture(panel, {\n axis: 'x',\n onMove: ({ distance }) => {\n panel.style.transform = `translateX(${distance}px)`;\n },\n onEnd: ({ distance, reason }) => {\n panel.style.transform = '';\n if (reason === 'release' && Math.abs(distance) >= 80) revealActions();\n },\n});\n```\n\n### Gesture + Dnd\n\nGesture owns pointer recognition: unrestricted two-dimensional drag and one-axis pan. Dnd builds touch sorting on `createDragGesture()`, then owns `DataTransfer`, previews, hit-testing, file drop zones, keyboard sorting, DOM reordering, and connected-list transactions. Use Dnd when an item is picked up and dropped; use Gesture directly when application code owns rendering and completion.\n\n## Best Practices\n\n- **Set** `touch-action` for the axis the browser should continue scrolling.\n- **Use** `shouldStart` to exclude nested interactive controls.\n- **Disable** pointer capture when nested or newly revealed controls must keep native release targeting.\n- **Apply** completion thresholds and direction rules in `onEnd`; use `activationDistance` only for recognition slop.\n- **Treat** `reason: 'cancel'` as a reset path, never a commit path.\n- **Keep** `onMove` rendering lightweight.\n- **Dispose** the handle when its target leaves the UI.\n",
4
+ "index": "---\ntitle: Gesture — Pointer movement primitives\ndescription: Framework-neutral two-dimensional pointer drag and one-axis pan recognition with lifecycle-owned handles.\npackage: gesture\ncategory: input\nkeywords: [pointer, pan, swipe, gesture, touch, drag, long press]\nexports: [createDragGesture, createPanGesture, createLongPress, GestureConfigError]\nrelated: [refine, dnd, keymap]\nenvironments: [browser]\n---\n\n<!-- markdownlint-disable MD025 MD033 MD060 -->\n\n<PackageHero package=\"gesture\" />\n\n## Why Gesture?\n\nPointer-driven interfaces need reliable one-pointer movement tracking without coupling recognition to rendering, drag-and-drop semantics, or product-specific completion policy.\n\n```ts\n// Before\nelement.addEventListener('pointermove', (event) => {\n // Coordinate tracking, pointer identity, direction locking, and cleanup\n});\n\n// After\nimport { createPanGesture } from '@vielzeug/gesture';\n\nconst pan = createPanGesture(element, {\n axis: 'x',\n onMove: ({ distance }) => console.log('distance', distance),\n onEnd: ({ reason }) => console.log('ended', reason),\n});\n```\n\n| Feature | Gesture | Ad-hoc pointer handling |\n| --- | --- | --- |\n| Bundle size | <PackageInfo package=\"gesture\" type=\"size\" /> | n/a |\n| Zero dependencies | <ore-icon name=\"check\" size=\"16\"></ore-icon> | n/a |\n| Two-dimensional drag tracking | Built in | Manual |\n| Axis intent recognition | Built in | Manual |\n| Pointer ownership | Tracked across the document | Manual |\n| Lifecycle cleanup | `dispose()` + `disposalSignal` | Manual |\n\n<div class=\"decision-callout\">\n\n**Use Gesture when** UI surfaces need consistent free drag or axis-locked pan tracking while retaining their own rendering and completion rules.\n\n**Consider direct pointer handling when** the interaction is isolated and does not need reusable lifecycle or direction-lock behavior.\n\n</div>\n\n## Installation\n\n::: code-group\n\n```sh [pnpm]\npnpm add @vielzeug/gesture\n```\n\n```sh [npm]\nnpm install @vielzeug/gesture\n```\n\n```sh [yarn]\nyarn add @vielzeug/gesture\n```\n\n:::\n\n## Quick Start\n\n```ts\nimport { createPanGesture } from '@vielzeug/gesture';\n\nconst element = document.querySelector<HTMLElement>('[data-swipe]');\nif (!element) throw new Error('Missing [data-swipe] element');\n\nconst pan = createPanGesture(element, {\n axis: 'x',\n onMove: ({ distance }) => {\n element.style.transform = `translateX(${distance}px)`;\n },\n onEnd: ({ distance, reason }) => {\n element.style.transform = '';\n if (reason === 'release' && Math.abs(distance) >= 48) {\n pan.dispose();\n element.remove();\n }\n },\n});\n\nwindow.addEventListener('pagehide', () => pan.dispose(), { once: true });\n```\n\n## Features\n\n<div class=\"features-grid\">\n\n- `createDragGesture()` — unrestricted two-dimensional pointer movement tracking\n- `createPanGesture()` — one-axis pointer movement with direction intent recognition\n- `createLongPress()` — hold recognition with slop cancellation and click swallowing\n- Direction locking — activates only when movement favors the configured axis\n- Configurable pointer capture — own the pointer by default or preserve native targeting\n- Configurable `activationDistance` — tune slop for touch density and component needs\n- Consumer-owned policy — completion thresholds, snapping, and outcomes stay in application code\n- Stable completion — one `onEnd` callback for release and cancellation\n- Lifecycle ownership — `dispose()`, `disposed`, `disposalSignal`, optional owner `signal`, and `[Symbol.dispose]()`\n\n</div>\n\n## Documentation\n\n<div class=\"doc-links\">\n\n- [Usage Guide](./usage.md)\n- [API Reference](./api.md)\n- [Examples](./examples.md)\n- [Migration Guide](./migration.md)\n\n</div>\n\n## See Also\n\n<div class=\"see-also\">\n\n- [Refine](/refine/) — components that use pan recognition for carousel, drawer, toast, and list interactions.\n- [Dnd](/dnd/) — builds touch sorting on `createDragGesture()` and adds files, previews, drop targets, keyboard reordering, and connected-list transactions.\n- [Keymap](/keymap/) — keyboard interaction primitives for complementary input paths.\n\n</div>\n\n<!-- markdownlint-enable MD025 MD033 MD060 -->\n",
5
+ "api": "---\ntitle: Gesture — API Reference\ndescription: API reference for two-dimensional drag and one-axis pan recognition.\n---\n\n[[toc]]\n\n## API Overview\n\n| Symbol | Purpose | Execution mode | Common gotcha |\n| --- | --- | --- | --- |\n| `createDragGesture()` | Track unrestricted two-dimensional pointer movement | Sync | Does not provide DOM movement or drop semantics |\n| `createPanGesture()` | Track pointer movement projected onto one axis | Sync | Cross-axis intent ends a pending interaction |\n| `createLongPress()` | Recognize a primary pointer held in place | Sync | The click that follows a fired hold is swallowed |\n| `DragGesture` / `PanGesture` / `LongPress` | Control recognition lifecycle | Sync | `dispose()` does not invoke `onEnd` |\n| `DragGestureOptions` | Configure free-drag recognition | Sync | `activationDistance` uses Euclidean distance |\n| `PanGestureOptions` | Configure axis-locked recognition | Sync | Completion thresholds belong in `onEnd` |\n| `LongPressOptions` | Configure hold recognition | Sync | `durationMs` and `slopPx` are validated at construction |\n\n## Package Entry Point\n\n| Import | Purpose |\n| --- | --- |\n| `@vielzeug/gesture` | Drag and pan factories, handles, callback details, options, axes, points, and end reasons |\n\n## Core Functions\n\n### `createDragGesture()`\n\n```ts\nfunction createDragGesture(target: Element, options?: DragGestureOptions): DragGesture;\n```\n\nTracks one primary pointer without restricting movement to an axis.\n\n| Parameter | Type | Description |\n| --- | --- | --- |\n| `target` | `Element` | Element that owns pointer admission and optional capture |\n| `options` | `DragGestureOptions` | Activation, admission, capture, callback, and lifecycle configuration |\n\n**Returns:** A lifecycle-owned `DragGesture` handle.\n\n**Example**\n\n```ts\nimport { createDragGesture } from '@vielzeug/gesture';\n\nconst drag = createDragGesture(element, {\n onMove: ({ delta }) => {\n element.style.translate = `${delta.x}px ${delta.y}px`;\n },\n onEnd: () => {\n element.style.translate = '';\n },\n});\n```\n\n---\n\n### `createPanGesture()`\n\n```ts\nfunction createPanGesture(target: Element, options?: PanGestureOptions): PanGesture;\n```\n\nTracks one primary pointer after movement establishes intent on the configured axis.\n\n| Parameter | Type | Description |\n| --- | --- | --- |\n| `target` | `Element` | Element that owns pointer admission and optional capture |\n| `options` | `PanGestureOptions` | Axis, activation, admission, capture, callback, and lifecycle configuration |\n\n**Returns:** A lifecycle-owned `PanGesture` handle.\n\n**Example**\n\n```ts\nimport { createPanGesture } from '@vielzeug/gesture';\n\nconst pan = createPanGesture(element, {\n axis: 'x',\n onEnd: ({ distance, reason }) => {\n if (reason === 'release' && Math.abs(distance) >= 48) dismiss();\n },\n});\n```\n\nBoth handles expose the same members:\n\n| Member | Type | Contract |\n| --- | --- | --- |\n| `active` | `boolean` | `true` after activation and before release or cancellation |\n| `cancel()` | `() => boolean` | Clears pending recognition or ends active recognition with `reason: 'cancel'`; returns whether a session existed |\n| `disposalSignal` | `AbortSignal` | Aborts when the handle is disposed |\n| `dispose()` | `() => void` | Aborts `disposalSignal`, clears recognition, releases capture, and detaches listeners; idempotent |\n| `disposed` | `boolean` | `true` after disposal |\n| `[Symbol.dispose]()` | `() => void` | Calls `dispose()` |\n\n---\n\n### `createLongPress()`\n\n```ts\nfunction createLongPress(target: Element, options?: LongPressOptions): LongPress;\n```\n\nRecognizes a primary pointer held in place: `onLongPress` fires with the originating pointerdown once the hold outlasts `durationMs`.\n\n| Parameter | Type | Description |\n| --- | --- | --- |\n| `target` | `Element` | Element that owns pointer admission, document tracking, and click swallowing |\n| `options` | `LongPressOptions` | Hold time, slop distance, admission, callback, and lifecycle configuration |\n\n**Returns:** A lifecycle-owned `LongPress` handle with the same members as the other gestures; `active` is `true` while a hold is pending.\n\n**Example**\n\n```ts\nimport { createLongPress } from '@vielzeug/gesture';\n\nconst hold = createLongPress(list, {\n shouldStart: (event) => event.pointerType !== 'mouse',\n onLongPress: (detail) => {\n const tile = (detail.event.target as HTMLElement).closest('.tile');\n openDetail(tile?.dataset.id);\n },\n});\n```\n\nMovement beyond `slopPx` cancels back to a normal press, and window blur or a hidden document cancels a pending hold. The click that follows a fired hold is swallowed through a capture-phase listener on `target`, so releasing never commits what the hold opened; bind `target` to a stable ancestor of the pressed controls so the swallow precedes their click handlers.\n\n## Types\n\n### Drag types\n\n```ts\ntype DragEndReason = 'cancel' | 'release';\n\ntype DragPoint = Readonly<{\n x: number;\n y: number;\n}>;\n\ntype DragGestureDetail = Readonly<{\n current: DragPoint;\n delta: DragPoint;\n event: PointerEvent;\n pointerId: number;\n pointerType: string;\n start: DragPoint;\n target: Element;\n}>;\n\ntype DragGestureEndDetail = DragGestureDetail &\n Readonly<{\n reason: DragEndReason;\n }>;\n\ntype DragGestureOptions = Readonly<{\n activationDistance?: number;\n disabled?: boolean | (() => boolean | undefined);\n onEnd?: (detail: DragGestureEndDetail) => void;\n onMove?: (detail: DragGestureDetail) => void;\n onStart?: (detail: DragGestureDetail) => void;\n pointerCapture?: boolean;\n shouldStart?: (event: PointerEvent) => boolean;\n signal?: AbortSignal;\n}>;\n```\n\n`start`, `current`, and `delta` use viewport `clientX` and `clientY` coordinates. Activation uses `Math.hypot(delta.x, delta.y)`.\n\n### Pan types\n\n```ts\ntype PanAxis = 'x' | 'y';\ntype PanEndReason = 'cancel' | 'release';\n\ntype PanGestureDetail = Readonly<{\n axis: PanAxis;\n current: number;\n distance: number;\n event: PointerEvent;\n pointerId: number;\n pointerType: string;\n start: number;\n target: Element;\n}>;\n\ntype PanGestureEndDetail = PanGestureDetail &\n Readonly<{\n reason: PanEndReason;\n }>;\n\ntype PanGestureOptions = Readonly<{\n activationDistance?: number;\n axis?: PanAxis | (() => PanAxis);\n disabled?: boolean | (() => boolean | undefined);\n onEnd?: (detail: PanGestureEndDetail) => void;\n onMove?: (detail: PanGestureDetail) => void;\n onStart?: (detail: PanGestureDetail) => void;\n pointerCapture?: boolean;\n shouldStart?: (event: PointerEvent) => boolean;\n signal?: AbortSignal;\n}>;\n```\n\nPan details project `start`, `current`, and `distance` onto `axis`. The default axis is `'x'`.\n\n### Long-press types\n\n```ts\ntype LongPressDetail = Readonly<{\n event: PointerEvent;\n pointerId: number;\n pointerType: string;\n target: Element;\n}>;\n\ntype LongPressOptions = Readonly<{\n durationMs?: number;\n disabled?: boolean | (() => boolean | undefined);\n onLongPress?: (detail: LongPressDetail) => void;\n shouldStart?: (event: PointerEvent) => boolean;\n slopPx?: number;\n signal?: AbortSignal;\n}>;\n```\n\n`durationMs` defaults to `500` and `slopPx` to `8`. `event` is the pointerdown that started the hold, so `event.target` is the pressed element.\n\n### Handle types\n\n```ts\ntype DragGesture = {\n readonly active: boolean;\n [Symbol.dispose](): void;\n cancel(): boolean;\n readonly disposalSignal: AbortSignal;\n dispose(): void;\n readonly disposed: boolean;\n};\n\ntype PanGesture = DragGesture;\n```\n\n### Shared option behavior\n\n| Option | Default | Contract |\n| --- | --- | --- |\n| `activationDistance` | `6` | Non-negative finite CSS pixels; drag uses Euclidean distance, pan waits for dominant-axis intent |\n| `axis` | `'x'` | Pan only; fixed value or getter sampled when each interaction starts |\n| `disabled` | `false` | Fixed value or getter; blocks starts and cancels active recognition on the next matching pointer event |\n| `pointerCapture` | `true` | Best-effort capture after activation; document tracking continues when capture is disabled or fails |\n| `shouldStart` | — | Admission predicate evaluated for the primary pointer before tracking starts |\n| `signal` | — | Disposes the handle when the external owner aborts |\n| `onStart` | — | Runs once on the activating movement |\n| `onMove` | — | Runs on the activating movement and each later matching movement |\n| `onEnd` | — | Runs once for active release or cancellation; pending interactions end without callbacks |\n\nOptions and callback details are readonly. Fixed callbacks and capture policy are snapshotted at construction. Function-valued `axis` and `disabled` options remain dynamic.\n\nGesture tracks accepted movement with capture-phase listeners on the target's current owner document. Window blur, hidden-document transition, `pointercancel`, and `lostpointercapture` cancel active recognition.\n\n## Errors\n\n| Error | Trigger | Notable properties |\n| --- | --- | --- |\n| `GestureError` | Base class for every gesture-originated error | `instanceof GestureError` catches any gesture error |\n| `GestureConfigError` | Invalid option value | Extends `GestureError` |\n\n- Invalid `activationDistance` values throw `GestureConfigError` during construction.\n- Invalid fixed `axis` values throw `GestureConfigError` during construction.\n- Invalid values returned by a dynamic `axis` getter throw `GestureConfigError` when a pointer interaction starts.\n",
6
+ "usage": "---\ntitle: Gesture — Usage Guide\ndescription: Track unrestricted pointer dragging and axis-locked pans with application-owned rendering and completion.\n---\n\n[[toc]]\n\n## Basic Usage\n\nCreate one handle for the element that owns the interaction. This example creates its surface, applies the required touch policy, and disposes the handle when the page unloads.\n\n```ts\nimport { createPanGesture } from '@vielzeug/gesture';\n\nconst row = document.body.appendChild(document.createElement('div'));\nrow.textContent = 'Swipe to archive';\nrow.style.touchAction = 'pan-y';\n\nconst pan = createPanGesture(row, {\n axis: 'x',\n onMove: ({ distance }) => {\n row.style.transform = `translateX(${distance}px)`;\n },\n onEnd: ({ distance, reason }) => {\n row.style.transform = '';\n if (reason === 'release' && Math.abs(distance) >= 64) {\n pan.dispose();\n row.remove();\n }\n },\n});\n\nwindow.addEventListener('pagehide', () => pan.dispose(), { once: true });\n```\n\n## Track Two-Dimensional Dragging\n\nUse `createDragGesture()` when movement must remain unrestricted in both dimensions:\n\n```ts\nimport { createDragGesture } from '@vielzeug/gesture';\n\nconst card = document.querySelector<HTMLElement>('[data-draggable]');\nif (!card) throw new Error('Missing [data-draggable] element');\n\nconst drag = createDragGesture(card, {\n activationDistance: 6,\n onMove: ({ delta }) => {\n card.style.translate = `${delta.x}px ${delta.y}px`;\n },\n onEnd: () => {\n card.style.translate = '';\n },\n});\n\nwindow.addEventListener('pagehide', () => drag.dispose(), { once: true });\n```\n\n`start`, `current`, and `delta` are readonly `{ x, y }` points. Set `touch-action: none` when the surface owns movement in both dimensions. Drag recognition does not provide previews, drop targets, DOM movement, or `DataTransfer`; use Dnd for those semantics.\n\n## Recognize Long Presses\n\nUse `createLongPress()` for hold interactions — the touch counterpart of a keyboard preview key. `onLongPress` receives the originating pointerdown once the hold outlasts `durationMs`:\n\n```ts\nimport { createLongPress } from '@vielzeug/gesture';\n\nconst hold = createLongPress(grid, {\n durationMs: 500,\n shouldStart: (event) =>\n event.pointerType !== 'mouse' &&\n (event.target as HTMLElement).closest('.tile') !== null,\n onLongPress: (detail) => {\n const tile = (detail.event.target as HTMLElement).closest<HTMLElement>('.tile');\n openPreview(tile?.dataset.id);\n },\n});\n```\n\nMovement beyond `slopPx` cancels back to a normal press, so scrolling never triggers the hold. The click that follows a fired hold is swallowed through a capture-phase listener on `target` — bind `target` to a stable ancestor of the pressed controls so the swallow precedes their click handlers, and gate `shouldStart` to the elements the hold belongs to.\n\n## Completion Rules\n\nGesture reports movement and terminal state but does not decide what constitutes a swipe. Apply thresholds and allowed directions in `onEnd`.\n\n```ts\nconst pan = createPanGesture(panel, {\n axis: 'x',\n onEnd: ({ distance, reason }) => {\n if (reason === 'release' && distance <= -80) {\n openNext();\n } else {\n resetPanel();\n }\n },\n});\n```\n\n## Direction Recognition\n\nThe gesture remains pending during small movement. It activates only after movement favors the configured axis. Cross-axis movement ends the pending interaction without invoking callbacks.\n\nThe default `activationDistance` is 6 pixels. Tune direction-recognition slop for touch density or component-specific needs:\n\n```ts\nconst pan = createPanGesture(row, {\n axis: 'x',\n activationDistance: 12,\n onMove,\n onEnd,\n});\n```\n\nSet `activationDistance: 0` when the first pointer move with non-zero displacement should activate immediately. Zero-motion events remain pending.\n\nUse the corresponding `touch-action` value so the browser retains native scrolling on the other axis.\n\n```css\n.swipe-row {\n touch-action: pan-y;\n}\n```\n\n```ts\nconst pan = createPanGesture(row, { axis: 'x', onMove });\n```\n\n## Pointer Capture\n\nPointer capture is enabled by default. After axis intent is accepted, Gesture attempts to capture the pointer while continuing to track movement through document-level listeners. Capture failure—for example after target detachment or pointer termination—falls back to document tracking without stranding the session.\n\nDisable capture when nested or newly revealed controls must retain native pointer-up and click targeting:\n\n```ts\nconst pan = createPanGesture(row, {\n axis: 'x',\n pointerCapture: false,\n onMove: renderReveal,\n onEnd: settleReveal,\n});\n```\n\nDocument-level tracking still keeps the pan active outside the target. Disabling capture changes event targeting, not gesture tracking.\n\n## Interactive Descendants\n\nUse `shouldStart` when buttons, links, or form controls inside the surface must not start a pan.\n\n```ts\nconst pan = createPanGesture(notification, {\n axis: 'x',\n pointerCapture: false,\n shouldStart: (event) =>\n !event\n .composedPath()\n .some((node) => node instanceof Element && node.matches('button, a, input, select, textarea')),\n onMove,\n onEnd,\n});\n```\n\n`shouldStart` protects controls under the initial pointer. `pointerCapture: false` additionally protects controls that appear beneath the pointer during a reveal interaction.\n\n## Disabled State\n\nA boolean disables the recognizer permanently. A getter supports state that changes while the handle is alive.\n\n```ts\nconst pan = createPanGesture(row, {\n disabled: () => isLocked,\n onEnd: ({ reason }) => {\n if (reason === 'cancel') resetRow();\n },\n});\n```\n\nWhen the getter becomes `true`, the next pointer event cancels an active pan. Window blur or a hidden owner document also cancels active tracking so a lost release cannot block later gestures.\n\nGesture snapshots fixed callbacks and capture policy at construction. Use the `axis` and `disabled` functions for behavior that must remain dynamic instead of mutating the options object.\n\n## Lifecycle\n\nDispose the target-bound handle when its owning UI scope unmounts.\n\n```ts\nconst element = document.querySelector<HTMLElement>('[data-pan]');\nif (!element) throw new Error('Missing [data-pan] element');\n\nconst owner = new AbortController();\nconst request = new AbortController();\nconst pan = createPanGesture(element, {\n axis: 'x',\n signal: owner.signal,\n});\n\nconst childSignal = AbortSignal.any([pan.disposalSignal, request.signal]);\nwindow.addEventListener('pagehide', () => owner.abort(), { once: true });\n```\n\nPass `{ signal }` to dispose with an existing owner signal. `disposalSignal` always aborts when Gesture finishes teardown, so child work can compose with the handle regardless of how disposal starts.\n\nUse `cancel()` to stop a pending or active interaction without disposing the handle. An active interaction emits `onEnd` with `reason: 'cancel'`. Call `cancel()` before `dispose()` when a connected surface needs its `onEnd` reset path; disposal itself does not invoke callbacks.\n\n## Framework Integration\n\nCreate the handle after the target element exists and dispose it on unmount.\n\n::: code-group\n\n```tsx [React]\nimport { useEffect, useRef } from 'react';\nimport { createPanGesture } from '@vielzeug/gesture';\n\nfunction SwipeRow({ onDismiss }: { onDismiss: () => void }) {\n const rowRef = useRef<HTMLDivElement>(null);\n\n useEffect(() => {\n const row = rowRef.current;\n if (!row) return;\n\n const pan = createPanGesture(row, {\n axis: 'x',\n onMove: ({ distance }) => {\n row.style.transform = `translateX(${distance}px)`;\n },\n onEnd: ({ distance, reason }) => {\n row.style.transform = '';\n if (reason === 'release' && Math.abs(distance) >= 64) onDismiss();\n },\n });\n\n return () => pan.dispose();\n }, [onDismiss]);\n\n return <div ref={rowRef}>Swipe me</div>;\n}\n```\n\n```vue [Vue 3]\n<script setup lang=\"ts\">\nimport { onMounted, onUnmounted, ref } from 'vue';\nimport { createPanGesture, type PanGesture } from '@vielzeug/gesture';\n\nconst emit = defineEmits<{ dismiss: [] }>();\nconst rowEl = ref<HTMLDivElement | null>(null);\nlet pan: PanGesture | undefined;\n\nonMounted(() => {\n const row = rowEl.value;\n if (!row) return;\n\n pan = createPanGesture(row, {\n axis: 'x',\n onEnd: ({ distance, reason }) => {\n if (reason === 'release' && Math.abs(distance) >= 64) emit('dismiss');\n },\n });\n});\n\nonUnmounted(() => pan?.dispose());\n</script>\n\n<template>\n <div ref=\"rowEl\">Swipe me</div>\n</template>\n```\n\n```svelte [Svelte]\n<script lang=\"ts\">\n import { onMount } from 'svelte';\n import { createPanGesture } from '@vielzeug/gesture';\n\n let { ondismiss = () => {} }: { ondismiss: () => void } = $props();\n let rowEl: HTMLDivElement;\n\n onMount(() => {\n const pan = createPanGesture(rowEl, {\n axis: 'x',\n onEnd: ({ distance, reason }) => {\n if (reason === 'release' && Math.abs(distance) >= 64) ondismiss();\n },\n });\n\n return () => pan.dispose();\n });\n</script>\n\n<div bind:this={rowEl}>Swipe me</div>\n```\n\n:::\n\n## Working with Other Vielzeug Libraries\n\n### Gesture + Refine\n\nRefine uses Gesture internally for carousel, drawer, toast, and list-item pointer interactions. Custom surfaces can use the same pan lifecycle while keeping visual state local.\n\n```ts\nimport { createPanGesture } from '@vielzeug/gesture';\n\nconst pan = createPanGesture(panel, {\n axis: 'x',\n onMove: ({ distance }) => {\n panel.style.transform = `translateX(${distance}px)`;\n },\n onEnd: ({ distance, reason }) => {\n panel.style.transform = '';\n if (reason === 'release' && Math.abs(distance) >= 80) revealActions();\n },\n});\n```\n\n### Gesture + Dnd\n\nGesture owns pointer recognition: unrestricted two-dimensional drag and one-axis pan. Dnd builds touch sorting on `createDragGesture()`, then owns `DataTransfer`, previews, hit-testing, file drop zones, keyboard sorting, DOM reordering, and connected-list transactions. Use Dnd when an item is picked up and dropped; use Gesture directly when application code owns rendering and completion.\n\n## Best Practices\n\n- **Set** `touch-action` for the axis the browser should continue scrolling.\n- **Use** `shouldStart` to exclude nested interactive controls.\n- **Disable** pointer capture when nested or newly revealed controls must keep native release targeting.\n- **Apply** completion thresholds and direction rules in `onEnd`; use `activationDistance` only for recognition slop.\n- **Treat** `reason: 'cancel'` as a reset path, never a commit path.\n- **Keep** `onMove` rendering lightweight.\n- **Dispose** the handle when its target leaves the UI.\n",
7
7
  "examples": "---\ntitle: Gesture — Examples\ndescription: Worked examples for @vielzeug/gesture.\n---\n\n## Examples\n\n- [Carousel Pan Navigation](./examples/carousel-swipe-navigation.md)\n- [Swipe-to-Dismiss Notifications](./examples/swipe-dismiss-notifications.md)\n"
8
8
  },
9
9
  "examples": [
@@ -26,6 +26,12 @@
26
26
  "DragGestureEndDetail": "export {\n createDragGesture,\n type DragEndReason,\n type DragGesture,\n type DragGestureDetail,\n type DragGestureEndDetail,\n type DragGestureOptions,\n type DragPoint,\n} from './drag-gesture.js';",
27
27
  "DragGestureOptions": "export {\n createDragGesture,\n type DragEndReason,\n type DragGesture,\n type DragGestureDetail,\n type DragGestureEndDetail,\n type DragGestureOptions,\n type DragPoint,\n} from './drag-gesture.js';",
28
28
  "DragPoint": "export {\n createDragGesture,\n type DragEndReason,\n type DragGesture,\n type DragGestureDetail,\n type DragGestureEndDetail,\n type DragGestureOptions,\n type DragPoint,\n} from './drag-gesture.js';",
29
+ "GestureConfigError": "export { GestureConfigError, GestureError } from './errors.js';",
30
+ "GestureError": "export { GestureConfigError, GestureError } from './errors.js';",
31
+ "createLongPress": "export {\n createLongPress,\n type LongPress,\n type LongPressDetail,\n type LongPressOptions,\n} from './long-press.js';",
32
+ "LongPress": "export {\n createLongPress,\n type LongPress,\n type LongPressDetail,\n type LongPressOptions,\n} from './long-press.js';",
33
+ "LongPressDetail": "export {\n createLongPress,\n type LongPress,\n type LongPressDetail,\n type LongPressOptions,\n} from './long-press.js';",
34
+ "LongPressOptions": "export {\n createLongPress,\n type LongPress,\n type LongPressDetail,\n type LongPressOptions,\n} from './long-press.js';",
29
35
  "createPanGesture": "export {\n createPanGesture,\n type PanAxis,\n type PanEndReason,\n type PanGesture,\n type PanGestureDetail,\n type PanGestureEndDetail,\n type PanGestureOptions,\n} from './pan-gesture.js';",
30
36
  "PanAxis": "export {\n createPanGesture,\n type PanAxis,\n type PanEndReason,\n type PanGesture,\n type PanGestureDetail,\n type PanGestureEndDetail,\n type PanGestureOptions,\n} from './pan-gesture.js';",
31
37
  "PanEndReason": "export {\n createPanGesture,\n type PanAxis,\n type PanEndReason,\n type PanGesture,\n type PanGestureDetail,\n type PanGestureEndDetail,\n type PanGestureOptions,\n} from './pan-gesture.js';",
@@ -1,9 +1,9 @@
1
1
  {
2
- "apiSource": "export { createBus } from './bus';\nexport { BusDisposedError, HeraldConfigError, HeraldError } from './errors';\nexport type {\n Bus,\n BusOptions,\n EventKey,\n EventMap,\n HeraldEvent,\n Listener,\n SubscribeOptions,\n Unsubscribe,\n WaitAnyResult,\n} from './types';\n",
2
+ "apiSource": "export { createBus } from './bus';\nexport { BusDisposedError, HeraldError } from './errors';\nexport type {\n Bus,\n BusOptions,\n EventKey,\n EventMap,\n HeraldEvent,\n Listener,\n SubscribeOptions,\n Unsubscribe,\n WaitAnyResult,\n} from './types';\n",
3
3
  "docs": {
4
- "index": "---\ntitle: Herald — Typed event bus for TypeScript\ndescription: Typed synchronous event delivery with wildcard subscriptions, one-shot waits, tracing, and AbortSignal lifecycle.\npackage: herald\ncategory: events\nkeywords: [event-bus, typed-events, pub-sub, async-wait, abort-signal]\nrelated: [ripple, wayfinder, familiar]\nexports: [createBus, HeraldError, BusDisposedError, HeraldConfigError]\nenvironments: [browser, node, ssr, deno]\n---\n\n<!-- markdownlint-disable MD025 MD033 MD060 -->\n\n<PackageHero package=\"herald\" />\n\n## Why Herald?\n\nRaw event emitters lose payload inference and leave one-shot waiting, cancellation, diagnostics, and teardown to every caller. Herald keeps events temporal: use [Ripple](/ripple/) when you need retained state.\n\n```ts\n// Before\nconst listeners = new Set<(payload: unknown) => void>();\nlisteners.add((payload) => loadProfile((payload as { id: string }).id));\n\n// After\nimport { createBus } from '@vielzeug/herald';\n\ninterface AppEvents {\n 'user:login': { id: string };\n}\n\nfunction loadProfile(id: string): void {\n console.log(id);\n}\n\nconst bus = createBus<AppEvents>();\nbus.on('user:login', ({ id }) => loadProfile(id));\n```\n\nDelivery is synchronous and temporal: events are not retained or replayed. Listener failures do not stop later listeners; `emit()` rethrows the first failure after dispatch, while `tap()` exposes every failure for diagnostics.\n\n| Feature | Herald | mitt | EventEmitter3 |\n| --- | --- | --- | --- |\n| Bundle size | <PackageInfo package=\"herald\" type=\"size\" /> | ~200 B | ~1.5 kB |\n| Typed payloads | <ore-icon name=\"check\" size=\"16\"></ore-icon> | <ore-icon name=\"triangle-alert\" size=\"16\"></ore-icon> | <ore-icon name=\"triangle-alert\" size=\"16\"></ore-icon> |\n| One-shot async wait | <ore-icon name=\"check\" size=\"16\"></ore-icon> | <ore-icon name=\"x\" size=\"16\"></ore-icon> | <ore-icon name=\"x\" size=\"16\"></ore-icon> |\n| AbortSignal lifecycle | <ore-icon name=\"check\" size=\"16\"></ore-icon> | <ore-icon name=\"x\" size=\"16\"></ore-icon> | <ore-icon name=\"x\" size=\"16\"></ore-icon> |\n| Runtime activity tracing | <ore-icon name=\"check\" size=\"16\"></ore-icon> | <ore-icon name=\"x\" size=\"16\"></ore-icon> | <ore-icon name=\"x\" size=\"16\"></ore-icon> |\n| Zero dependencies | <ore-icon name=\"check\" size=\"16\"></ore-icon> | <ore-icon name=\"check\" size=\"16\"></ore-icon> | <ore-icon name=\"check\" size=\"16\"></ore-icon> |\n\n<div class=\"decision-callout\">\n\n**Use Herald when** modules need typed temporal event delivery with owned lifecycle.\n\n**Consider Ripple when** consumers need current state and replayed values.\n\n</div>\n\n## Installation\n\n::: code-group\n\n```sh [pnpm]\npnpm add @vielzeug/herald\n```\n\n```sh [npm]\nnpm install @vielzeug/herald\n```\n\n```sh [yarn]\nyarn add @vielzeug/herald\n```\n\n:::\n\n## Quick Start\n\n```ts\nimport { createBus } from '@vielzeug/herald';\n\ninterface AppEvents {\n 'user:login': { id: string };\n 'user:logout': void;\n}\n\nconst bus = createBus<AppEvents>();\nconst stop = bus.on('user:login', ({ id }) => console.log(id));\n\nbus.emit('user:login', { id: '42' });\nstop();\nbus.dispose();\n```\n\n## Features\n\n<div class=\"features-grid\">\n\n- `on()` / `once()` — typed subscriptions with explicit teardown\n- `onAny()` — cross-cutting event observation\n- `tap()` — observe bus activity for logging and diagnostics\n- `wait()` / `waitAny()` — one-shot async coordination\n- `listenerCount()` / `wildcardCount()` / `eventNames()` — listener inspection\n- `error` tap events — isolate listener failures without interrupting delivery\n- `AbortSignal` — subscription, wait, tap, and disposal ownership\n- `createTestBus()` — emitted-payload recording for tests\n\n</div>\n\n## Documentation\n\n<div class=\"doc-links\">\n\n- [Usage Guide](./usage.md)\n- [API Reference](./api.md)\n- [Examples](./examples.md)\n- [Migration Guide](./migration.md)\n\n</div>\n\n## See Also\n\n<div class=\"see-also\">\n\n- [Ripple](/ripple/) — retained reactive state.\n- [Wayfinder](/wayfinder/) — route lifecycle events.\n- [Familiar](/familiar/) — worker completion events.\n\n</div>\n\n<!-- markdownlint-enable MD025 MD033 MD060 -->\n",
5
- "api": "---\ntitle: Herald — API Reference\ndescription: Reference for typed synchronous events, subscriptions, waits, tracing, lifecycle, and test helpers.\n---\n\n[[toc]]\n\n## API Overview\n\n| Symbol | Purpose | Execution | Common gotcha |\n| --- | --- | --- | --- |\n| `createBus()` | Create a typed temporal event bus | Sync | Events are not retained or replayed |\n| `emit()` | Deliver one event | Sync | Returns `void`; rethrows the first listener failure after dispatch |\n| `on()` / `once()` | Subscribe to one event | Sync | Keep the unsubscribe function or pass a signal |\n| `onAny()` | Subscribe to every event | Sync | Payload is `unknown` |\n| `wait()` / `waitAny()` | Await future one-shot events | Async | Disposal and external abort reject the promise |\n| `tap()` | Observe bus runtime activity | Sync | Tap-handler failures are swallowed |\n| `createTestBus()` | Record emitted payloads | Sync | Import from `/testing` |\n\n## Package Entry Points\n\n| Import | Purpose |\n| --- | --- |\n| `@vielzeug/herald` | Bus factory, errors, and public bus types |\n| `@vielzeug/herald/testing` | `createTestBus()` and `TestBus` |\n\n## `createBus()`\n\n```ts\nfunction createBus<T extends EventMap = Record<string, unknown>>(\n options?: BusOptions<T>,\n): Bus<T>\n```\n\nCreates a synchronous bus for future event delivery. Construction has no external side effects.\n\n```ts\nimport { createBus } from '@vielzeug/herald';\n\ninterface Events {\n count: number;\n ready: void;\n}\n\nconst bus = createBus<Events>({ maxListeners: 20, name: 'worker' });\nbus.emit('count', 1);\nbus.emit('ready');\nbus.dispose();\n```\n\n### `BusOptions`\n\n```ts\ntype BusOptions<T extends EventMap = EventMap> = {\n maxListeners?: number;\n name?: string;\n};\n```\n\n| Field | Description |\n| --- | --- |\n| `maxListeners` | Development warning threshold applied separately to one event's listeners and wildcard listeners. Omit to disable the check. |\n| `name` | Display name included in disposal errors and listener-threshold warnings. |\n\n## `Bus`\n\n```ts\ntype Bus<T extends EventMap> = {\n [Symbol.dispose](): void;\n readonly disposalSignal: AbortSignal;\n dispose(): void;\n readonly disposed: boolean;\n emit<K extends EventKey<T>>(event: K, ...args: T[K] extends void ? [] : [payload: T[K]]): void;\n eventNames(): EventKey<T>[];\n listenerCount(event?: EventKey<T>): number;\n on<K extends EventKey<T>>(event: K, listener: Listener<T[K]>, options?: SubscribeOptions): Unsubscribe;\n onAny(listener: (event: EventKey<T>, payload: unknown) => void, options?: SubscribeOptions): Unsubscribe;\n once<K extends EventKey<T>>(event: K, listener: Listener<T[K]>, options?: { signal?: AbortSignal }): Unsubscribe;\n tap(handler: (event: HeraldEvent<T>) => void, options?: { signal?: AbortSignal }): Unsubscribe;\n wait<K extends EventKey<T>>(event: K, options?: { signal?: AbortSignal }): Promise<T[K]>;\n waitAny<const K extends readonly [EventKey<T>, EventKey<T>, ...EventKey<T>[]]>(\n events: K,\n options?: { signal?: AbortSignal },\n ): Promise<WaitAnyResult<T, K>>;\n wildcardCount(): number;\n};\n```\n\n### `emit()`\n\nCalls a snapshot of event-specific listeners synchronously, then wildcard listeners, then emits an `emit` trace event. It always returns `void`. Synchronous listener failures produce `error` trace events without stopping delivery; after dispatch, the first failure is rethrown.\n\nAfter disposal, emission is a no-op.\n\n### `on()` and `once()`\n\n```ts\ntype Listener<T> = (payload: T) => void;\ntype SubscribeOptions = { once?: boolean; signal?: AbortSignal };\ntype Unsubscribe = () => void;\n```\n\n`on()` returns an idempotent unsubscribe function. `{ once: true }` is equivalent to `once()`. Once-listeners unsubscribe before invocation. An already-aborted signal creates no subscription.\n\n### `onAny()`\n\nReceives `(event, payload)` after event-specific listeners. It supports both `signal` and `once`. Wildcard listeners are excluded from `listenerCount()` and reported by `wildcardCount()`.\n\n### `wait()`\n\nResolves with the next payload for one event. It rejects with the external signal's reason when aborted, or `BusDisposedError` when bus disposal wins. Earlier emissions are not replayed.\n\n### `waitAny()`\n\nRequires at least two event keys at compile time and runtime. It resolves once with a typed `{ event, payload }` discriminated union and removes every losing subscription.\n\n```ts\nconst result = await bus.waitAny(['count', 'ready']);\nif (result.event === 'count') console.log(result.payload);\n```\n\n### Listener inspection\n\n- `listenerCount(event)` returns specific listeners for one key.\n- `listenerCount()` returns all specific listeners.\n- `wildcardCount()` returns `onAny()` listeners.\n- `eventNames()` returns keys with active specific listeners.\n\n### `tap()`\n\nObserves runtime activity without affecting delivery. It returns an idempotent unsubscribe function and accepts an ownership signal. Tap-handler errors are swallowed.\n\n```ts\nconst stop = bus.tap((event) => {\n if (event.type === 'error') recordListenerFailure(event.error);\n});\n```\n\n### Disposal\n\n`dispose()` is permanent and idempotent. It emits a final `dispose` trace event, aborts `disposalSignal` with `BusDisposedError`, rejects pending waits, removes subscriptions, and clears taps. `[Symbol.dispose]()` delegates to it.\n\n## Runtime Events\n\n```ts\ntype HeraldEvent<T extends EventMap = EventMap> =\n | { readonly event: EventKey<T>; readonly listeners: number; readonly payload: unknown; readonly type: 'emit' }\n | { readonly event: EventKey<T>; readonly type: 'subscribe' }\n | { readonly event: EventKey<T>; readonly type: 'unsubscribe' }\n | { readonly type: 'subscribe-any' }\n | { readonly type: 'unsubscribe-any' }\n | { readonly error: unknown; readonly event: EventKey<T>; readonly type: 'error' }\n | { readonly type: 'dispose' };\n```\n\n`listeners` on an `emit` event counts every specific and wildcard listener invoked, including listeners that threw. Runtime events intentionally contain no timestamp; observers can add one using their own clock.\n\n## Shared Types\n\n```ts\ntype EventMap = object;\ntype EventKey<T extends EventMap> = Extract<keyof T, string>;\n\ntype WaitAnyResult<T extends EventMap, K extends readonly EventKey<T>[]> = {\n [I in keyof K]: K[I] extends EventKey<T>\n ? { event: K[I]; payload: T[K[I]] }\n : never;\n}[number];\n```\n\nInterfaces and object type aliases are valid event maps. Only string keys participate in the bus API.\n\n## Testing\n\n### `createTestBus()`\n\n```ts\nimport { createTestBus } from '@vielzeug/herald/testing';\n\nfunction createTestBus<T extends EventMap = Record<string, unknown>>(\n options?: BusOptions<T>,\n): TestBus<T>\n```\n\nCreates a regular bus that records dispatches before listeners run.\n\n### `TestBus`\n\n```ts\ntype TestBus<T extends EventMap> = Bus<T> & {\n allEmitted(): { [K in EventKey<T>]?: T[K][] };\n emitted<K extends EventKey<T>>(event: K): T[K][];\n emittedCount<K extends EventKey<T>>(event: K): number;\n reset(): void;\n};\n```\n\n`emitted()` and `allEmitted()` return array snapshots. `reset()` clears records without affecting subscriptions. Disposal clears records and the underlying bus. To prevent prototype mutation in the returned plain object, `allEmitted()` omits records named `__proto__`, `constructor`, or `prototype`; `emitted(event)` remains available for those keys.\n\n## Errors\n\n| Error | Trigger | Notable properties |\n| --- | --- | --- |\n| `BusDisposedError` | Bus disposal aborts `disposalSignal` or interrupts `wait()` / `waitAny()` | Configured bus name appears in the message |\n| `HeraldConfigError` | Runtime `waitAny()` input contains fewer than two keys | Extends `HeraldError` |\n| `HeraldError` | Base class for Herald-originated errors | Use `instanceof HeraldError` |\n",
6
- "usage": "---\ntitle: Herald — Usage Guide\ndescription: Define typed events, own subscriptions, await one-shot events, trace failures, and test emissions.\n---\n\n[[toc]]\n\n## Basic Usage\n\nUse an interface or object type whose string keys are event names and whose values are payloads. Use `void` or `undefined` for payloadless events.\n\n```ts\nimport { createBus } from '@vielzeug/herald';\n\ninterface AppEvents {\n 'cart:updated': { count: number };\n 'user:logout': void;\n}\n\nusing bus = createBus<AppEvents>({ name: 'app' });\nconst stop = bus.on('cart:updated', ({ count }) => console.log(count));\n\nbus.emit('cart:updated', { count: 1 });\nbus.emit('user:logout');\nstop();\n```\n\n`emit()` calls listeners synchronously and returns `void`. Event-specific listeners run before wildcard listeners. Events emitted without listeners are discarded.\n\n## Own Subscription Lifetimes\n\nKeep the returned unsubscribe function or bind a subscription to an `AbortSignal`. `once()` and `{ once: true }` unsubscribe before invoking the listener.\n\n```ts\nconst controller = new AbortController();\n\nbus.on('cart:updated', renderCart, { signal: controller.signal });\nbus.once('user:logout', clearSession);\nbus.on('user:logout', clearSession, { once: true });\n\ncontroller.abort();\n```\n\nAn already-aborted signal creates no subscription and returns an idempotent no-op unsubscribe function.\n\n## Observe Every Event\n\nUse `onAny()` for cross-cutting application behavior. Wildcard listeners receive the event key and untyped payload after event-specific listeners.\n\n```ts\nconst stop = bus.onAny((event, payload) => {\n audit(event, payload);\n});\n\nstop();\n```\n\nPass `{ once: true }` to stop after the first event of any type. `wildcardCount()` reports wildcard listeners separately from `listenerCount()`.\n\n## Await One Event\n\n`wait()` resolves on the next matching event. `waitAny()` requires at least two keys and returns a discriminated union.\n\n```ts\nconst cart = await bus.wait('cart:updated', { signal: AbortSignal.timeout(5_000) });\nconst result = await bus.waitAny(['cart:updated', 'user:logout'], {\n signal: AbortSignal.timeout(5_000),\n});\n\nif (result.event === 'cart:updated') console.log(result.payload.count);\n```\n\nThese methods do not replay earlier events. External cancellation rejects with the signal reason. Bus disposal rejects pending waits with `BusDisposedError`.\n\n## Handle Listener Failures\n\nListener exceptions do not prevent remaining specific or wildcard listeners from running. Each failure produces a `error` tap event, then `emit()` rethrows the first error after dispatch.\n\n```ts\nconst stopTrace = bus.tap((event) => {\n if (event.type === 'error') {\n recordListenerFailure(event.error);\n }\n});\n\nstopTrace();\n```\n\nHerald captures synchronous listener throws only, and `tap()` does not suppress the error rethrown after dispatch. A listener that starts asynchronous work must handle its own rejected promises.\n\n## Trace Bus Activity\n\n`tap()` observes subscriptions, unsubscriptions, wildcard subscriptions, emissions, listener failures, and disposal without changing bus behavior. Tap-handler failures are swallowed.\n\n```ts\nconst stopTrace = bus.tap((event) => {\n recordHeraldEvent(event);\n}, { signal: owner.signal });\n```\n\nAn `emit` trace includes the event key, payload, and number of specific plus wildcard listeners invoked. Trace events do not include timestamps; add them in the observer if needed.\n\n## Inspect Listeners\n\n```ts\nbus.listenerCount('cart:updated'); // specific listeners for this key\nbus.listenerCount(); // all specific listeners\nbus.wildcardCount(); // onAny listeners\nbus.eventNames(); // keys with active specific listeners\n```\n\n`eventNames()` does not include events that only have wildcard listeners.\n\n## Dispose the Bus\n\n`dispose()` is permanent and idempotent. It emits a final tap event, aborts `disposalSignal` with `BusDisposedError`, clears all listeners and taps, and rejects pending waits. Later subscriptions and emissions are no-ops.\n\n```ts\nusing scopedBus = createBus<AppEvents>({ name: 'checkout' });\nscopedBus.disposalSignal.addEventListener('abort', () => releaseOwnedWork());\n```\n\nThe configured name appears in disposal errors and listener-threshold warnings.\n\n## Test Emissions\n\nImport test helpers from the focused testing entry point.\n\n```ts\nimport { createTestBus } from '@vielzeug/herald/testing';\n\nusing testBus = createTestBus<AppEvents>();\ntestBus.emit('cart:updated', { count: 2 });\n\nexpect(testBus.emitted('cart:updated')).toEqual([{ count: 2 }]);\nexpect(testBus.emittedCount('cart:updated')).toBe(1);\nexpect(testBus.allEmitted()).toEqual({ 'cart:updated': [{ count: 2 }] });\n\ntestBus.reset();\n```\n\nReturned recordings are snapshots. `reset()` clears recording history without removing listeners; disposal clears both.\n\n## Working with Other Vielzeug Libraries\n\nUse Herald for temporal facts, Ripple for retained state, Rune for structured logging inside `tap()`, and Postmaster when work must survive reloads or retry durably.\n\n## Best Practices\n\n- Define one explicit event map per module or application boundary.\n- Emit facts, not mutable retained state.\n- Keep synchronous listeners short.\n- Handle rejected promises inside asynchronous listener work.\n- Keep unsubscribe functions or pass ownership signals.\n- Use `wait()` and `waitAny()` only for future one-shot events.\n- Install a `tap()` observer when listener failures must be visible.\n- Dispose owner-scoped buses.\n",
4
+ "index": "---\ntitle: Herald — Typed event bus for TypeScript\ndescription: Typed synchronous event delivery with wildcard subscriptions, one-shot waits, tracing, and AbortSignal lifecycle.\npackage: herald\ncategory: events\nkeywords: [event-bus, typed-events, pub-sub, async-wait, abort-signal]\nrelated: [ripple, wayfinder, familiar]\nexports: [createBus, HeraldError, BusDisposedError]\nenvironments: [browser, node, ssr, deno]\n---\n\n<!-- markdownlint-disable MD025 MD033 MD060 -->\n\n<PackageHero package=\"herald\" />\n\n## Why Herald?\n\nRaw event emitters lose payload inference and leave one-shot waiting, cancellation, diagnostics, and teardown to every caller. Herald keeps events temporal: use [Ripple](/ripple/) when you need retained state.\n\n```ts\n// Before\nconst listeners = new Set<(payload: unknown) => void>();\nlisteners.add((payload) => loadProfile((payload as { id: string }).id));\n\n// After\nimport { createBus } from '@vielzeug/herald';\n\ninterface AppEvents {\n 'user:login': { id: string };\n}\n\nfunction loadProfile(id: string): void {\n console.log(id);\n}\n\nconst bus = createBus<AppEvents>();\nbus.on('user:login', ({ id }) => loadProfile(id));\n```\n\nDelivery is synchronous and temporal: events are not retained or replayed. Listener failures do not stop later listeners; `emit()` rethrows the first failure after dispatch, while `tap()` exposes every failure for diagnostics.\n\n| Feature | Herald | mitt | EventEmitter3 |\n| --- | --- | --- | --- |\n| Bundle size | <PackageInfo package=\"herald\" type=\"size\" /> | ~200 B | ~1.5 kB |\n| Typed payloads | <ore-icon name=\"check\" size=\"16\"></ore-icon> | <ore-icon name=\"triangle-alert\" size=\"16\"></ore-icon> | <ore-icon name=\"triangle-alert\" size=\"16\"></ore-icon> |\n| One-shot async wait | <ore-icon name=\"check\" size=\"16\"></ore-icon> | <ore-icon name=\"x\" size=\"16\"></ore-icon> | <ore-icon name=\"x\" size=\"16\"></ore-icon> |\n| AbortSignal lifecycle | <ore-icon name=\"check\" size=\"16\"></ore-icon> | <ore-icon name=\"x\" size=\"16\"></ore-icon> | <ore-icon name=\"x\" size=\"16\"></ore-icon> |\n| Runtime activity tracing | <ore-icon name=\"check\" size=\"16\"></ore-icon> | <ore-icon name=\"x\" size=\"16\"></ore-icon> | <ore-icon name=\"x\" size=\"16\"></ore-icon> |\n| Zero dependencies | <ore-icon name=\"check\" size=\"16\"></ore-icon> | <ore-icon name=\"check\" size=\"16\"></ore-icon> | <ore-icon name=\"check\" size=\"16\"></ore-icon> |\n\n<div class=\"decision-callout\">\n\n**Use Herald when** modules need typed temporal event delivery with owned lifecycle.\n\n**Consider Ripple when** consumers need current state and replayed values.\n\n</div>\n\n## Installation\n\n::: code-group\n\n```sh [pnpm]\npnpm add @vielzeug/herald\n```\n\n```sh [npm]\nnpm install @vielzeug/herald\n```\n\n```sh [yarn]\nyarn add @vielzeug/herald\n```\n\n:::\n\n## Quick Start\n\n```ts\nimport { createBus } from '@vielzeug/herald';\n\ninterface AppEvents {\n 'user:login': { id: string };\n 'user:logout': void;\n}\n\nconst bus = createBus<AppEvents>();\nconst stop = bus.on('user:login', ({ id }) => console.log(id));\n\nbus.emit('user:login', { id: '42' });\nstop();\nbus.dispose();\n```\n\n## Features\n\n<div class=\"features-grid\">\n\n- `on()` / `once()` — typed subscriptions with explicit teardown\n- `onAny()` — cross-cutting event observation\n- `tap()` — observe bus activity for logging and diagnostics\n- `wait()` / `waitAny()` — one-shot async coordination\n- `listenerCount()` / `wildcardCount()` / `eventNames()` — listener inspection\n- `error` tap events — isolate listener failures without interrupting delivery\n- `AbortSignal` — subscription, wait, tap, and disposal ownership\n- `createTestBus()` — emitted-payload recording for tests\n\n</div>\n\n## Documentation\n\n<div class=\"doc-links\">\n\n- [Usage Guide](./usage.md)\n- [API Reference](./api.md)\n- [Examples](./examples.md)\n- [Migration Guide](./migration.md)\n\n</div>\n\n## See Also\n\n<div class=\"see-also\">\n\n- [Ripple](/ripple/) — retained reactive state.\n- [Wayfinder](/wayfinder/) — route lifecycle events.\n- [Familiar](/familiar/) — worker completion events.\n\n</div>\n\n<!-- markdownlint-enable MD025 MD033 MD060 -->\n",
5
+ "api": "---\ntitle: Herald — API Reference\ndescription: Reference for typed synchronous events, subscriptions, waits, tracing, lifecycle, and test helpers.\n---\n\n[[toc]]\n\n## API Overview\n\n| Symbol | Purpose | Execution | Common gotcha |\n| --- | --- | --- | --- |\n| `createBus()` | Create a typed temporal event bus | Sync | Events are not retained or replayed |\n| `emit()` | Deliver one event | Sync | Returns `void`; rethrows the first listener failure after dispatch |\n| `on()` / `once()` | Subscribe to one event | Sync | Keep the unsubscribe function or pass a signal |\n| `onAny()` | Subscribe to every event | Sync | Payload is `unknown` |\n| `wait()` / `waitAny()` | Await future one-shot events | Async | Disposal and external abort reject the promise |\n| `tap()` | Observe bus runtime activity | Sync | Tap-handler failures are swallowed |\n| `createTestBus()` | Record emitted payloads | Sync | Import from `/testing` |\n\n## Package Entry Points\n\n| Import | Purpose |\n| --- | --- |\n| `@vielzeug/herald` | Bus factory, errors, and public bus types |\n| `@vielzeug/herald/testing` | `createTestBus()` and `TestBus` |\n\n## `createBus()`\n\n```ts\nfunction createBus<T extends EventMap = Record<string, unknown>>(\n options?: BusOptions,\n): Bus<T>\n```\n\nCreates a synchronous bus for future event delivery. Construction has no external side effects.\n\n```ts\nimport { createBus } from '@vielzeug/herald';\n\ninterface Events {\n count: number;\n ready: void;\n}\n\nconst bus = createBus<Events>({ maxListeners: 20, name: 'worker' });\nbus.emit('count', 1);\nbus.emit('ready');\nbus.dispose();\n```\n\n### `BusOptions`\n\n```ts\ntype BusOptions = {\n maxListeners?: number;\n name?: string;\n};\n```\n\n| Field | Description |\n| --- | --- |\n| `maxListeners` | Development warning threshold applied separately to one event's listeners and wildcard listeners. Omit to disable the check. |\n| `name` | Display name included in disposal errors and listener-threshold warnings. |\n\n## `Bus`\n\n```ts\ntype Bus<T extends EventMap> = {\n [Symbol.dispose](): void;\n readonly disposalSignal: AbortSignal;\n dispose(): void;\n readonly disposed: boolean;\n emit<K extends EventKey<T>>(event: K, ...args: T[K] extends void ? [] : [payload: T[K]]): void;\n eventNames(): EventKey<T>[];\n listenerCount(event?: EventKey<T>): number;\n on<K extends EventKey<T>>(event: K, listener: Listener<T[K]>, options?: SubscribeOptions): Unsubscribe;\n onAny(listener: (event: EventKey<T>, payload: unknown) => void, options?: SubscribeOptions): Unsubscribe;\n once<K extends EventKey<T>>(event: K, listener: Listener<T[K]>, options?: { signal?: AbortSignal }): Unsubscribe;\n tap(handler: (event: HeraldEvent<T>) => void, options?: { signal?: AbortSignal }): Unsubscribe;\n wait<K extends EventKey<T>>(event: K, options?: { signal?: AbortSignal }): Promise<T[K]>;\n waitAny<const K extends readonly EventKey<T>[]>(\n events: K,\n options?: { signal?: AbortSignal },\n ): Promise<WaitAnyResult<T, K>>;\n wildcardCount(): number;\n};\n```\n\n### `emit()`\n\nCalls a snapshot of event-specific listeners synchronously, then wildcard listeners, then emits an `emit` trace event. It always returns `void`. Synchronous listener failures produce `error` trace events without stopping delivery; after dispatch, the first failure is rethrown.\n\nAfter disposal, emission is a no-op.\n\n### `on()` and `once()`\n\n```ts\ntype Listener<T> = (payload: T) => void;\ntype SubscribeOptions = { signal?: AbortSignal };\ntype Unsubscribe = () => void;\n```\n\n`on()` returns an idempotent unsubscribe function. `once()` is the one-shot variant: it unsubscribes before invoking the listener. An already-aborted signal creates no subscription.\n\n### `onAny()`\n\nReceives `(event, payload)` after event-specific listeners and supports `signal`. Wildcard listeners are excluded from `listenerCount()` and reported by `wildcardCount()`.\n\n### `wait()`\n\nResolves with the next payload for one event. It rejects with the external signal's reason when aborted, or `BusDisposedError` when bus disposal wins. Earlier emissions are not replayed.\n\n### `waitAny()`\n\nAccepts one or more event keys. It resolves once with a typed `{ event, payload }` discriminated union and removes every losing subscription.\n\n```ts\nconst result = await bus.waitAny(['count', 'ready']);\nif (result.event === 'count') console.log(result.payload);\n```\n\n### Listener inspection\n\n- `listenerCount(event)` returns specific listeners for one key.\n- `listenerCount()` returns all specific listeners.\n- `wildcardCount()` returns `onAny()` listeners.\n- `eventNames()` returns keys with active specific listeners.\n\n### `tap()`\n\nObserves runtime activity without affecting delivery. It returns an idempotent unsubscribe function and accepts an ownership signal. Tap-handler errors are swallowed.\n\n```ts\nconst stop = bus.tap((event) => {\n if (event.type === 'error') recordListenerFailure(event.error);\n});\n```\n\n### Disposal\n\n`dispose()` is permanent and idempotent. It emits a final `dispose` trace event, aborts `disposalSignal` with `BusDisposedError`, rejects pending waits, removes subscriptions, and clears taps. `[Symbol.dispose]()` delegates to it.\n\n## Runtime Events\n\n```ts\ntype HeraldEvent<T extends EventMap = EventMap> =\n | { readonly event: EventKey<T>; readonly listeners: number; readonly payload: unknown; readonly type: 'emit' }\n | { readonly event: EventKey<T>; readonly type: 'subscribe' }\n | { readonly event: EventKey<T>; readonly type: 'unsubscribe' }\n | { readonly type: 'subscribe-any' }\n | { readonly type: 'unsubscribe-any' }\n | { readonly error: unknown; readonly event: EventKey<T>; readonly type: 'error' }\n | { readonly type: 'dispose' };\n```\n\n`listeners` on an `emit` event counts every specific and wildcard listener invoked, including listeners that threw. Runtime events intentionally contain no timestamp; observers can add one using their own clock.\n\n## Shared Types\n\n```ts\ntype EventMap = object;\ntype EventKey<T extends EventMap> = Extract<keyof T, string>;\n\ntype WaitAnyResult<T extends EventMap, K extends readonly EventKey<T>[]> = {\n [I in keyof K]: K[I] extends EventKey<T>\n ? { event: K[I]; payload: T[K[I]] }\n : never;\n}[number];\n```\n\nInterfaces and object type aliases are valid event maps. Only string keys participate in the bus API.\n\n## Testing\n\n### `createTestBus()`\n\n```ts\nimport { createTestBus } from '@vielzeug/herald/testing';\n\nfunction createTestBus<T extends EventMap = Record<string, unknown>>(\n options?: BusOptions,\n): TestBus<T>\n```\n\nCreates a regular bus that records every dispatched payload through a `tap()` observer, including dispatches whose listeners throw.\n\n### `TestBus`\n\n```ts\ntype TestBus<T extends EventMap> = Bus<T> & {\n allEmitted(): { [K in EventKey<T>]?: T[K][] };\n emitted<K extends EventKey<T>>(event: K): T[K][];\n emittedCount<K extends EventKey<T>>(event: K): number;\n reset(): void;\n};\n```\n\n`emitted()` and `allEmitted()` return array snapshots. `reset()` clears records without affecting subscriptions. Disposal clears records and the underlying bus. To prevent prototype mutation in the returned plain object, `allEmitted()` omits records named `__proto__`, `constructor`, or `prototype`; `emitted(event)` remains available for those keys.\n\n## Errors\n\n| Error | Trigger | Notable properties |\n| --- | --- | --- |\n| `BusDisposedError` | Bus disposal aborts `disposalSignal` or interrupts `wait()` / `waitAny()` | Configured bus name appears in the message |\n| `HeraldError` | Base class for Herald-originated errors | Use `instanceof HeraldError` |\n",
6
+ "usage": "---\ntitle: Herald — Usage Guide\ndescription: Define typed events, own subscriptions, await one-shot events, trace failures, and test emissions.\n---\n\n[[toc]]\n\n## Basic Usage\n\nUse an interface or object type whose string keys are event names and whose values are payloads. Use `void` or `undefined` for payloadless events.\n\n```ts\nimport { createBus } from '@vielzeug/herald';\n\ninterface AppEvents {\n 'cart:updated': { count: number };\n 'user:logout': void;\n}\n\nusing bus = createBus<AppEvents>({ name: 'app' });\nconst stop = bus.on('cart:updated', ({ count }) => console.log(count));\n\nbus.emit('cart:updated', { count: 1 });\nbus.emit('user:logout');\nstop();\n```\n\n`emit()` calls listeners synchronously and returns `void`. Event-specific listeners run before wildcard listeners. Events emitted without listeners are discarded.\n\n## Own Subscription Lifetimes\n\nKeep the returned unsubscribe function or bind a subscription to an `AbortSignal`. `once()` unsubscribes before invoking the listener.\n\n```ts\nconst controller = new AbortController();\n\nbus.on('cart:updated', renderCart, { signal: controller.signal });\nbus.once('user:logout', clearSession);\n\ncontroller.abort();\n```\n\nAn already-aborted signal creates no subscription and returns an idempotent no-op unsubscribe function.\n\n## Observe Every Event\n\nUse `onAny()` for cross-cutting application behavior. Wildcard listeners receive the event key and untyped payload after event-specific listeners.\n\n```ts\nconst stop = bus.onAny((event, payload) => {\n audit(event, payload);\n});\n\nstop();\n```\n\n`wildcardCount()` reports wildcard listeners separately from `listenerCount()`.\n\n## Await One Event\n\n`wait()` resolves on the next matching event. `waitAny()` accepts one or more keys and returns a discriminated union.\n\n```ts\nconst cart = await bus.wait('cart:updated', { signal: AbortSignal.timeout(5_000) });\nconst result = await bus.waitAny(['cart:updated', 'user:logout'], {\n signal: AbortSignal.timeout(5_000),\n});\n\nif (result.event === 'cart:updated') console.log(result.payload.count);\n```\n\nThese methods do not replay earlier events. External cancellation rejects with the signal reason. Bus disposal rejects pending waits with `BusDisposedError`.\n\n## Handle Listener Failures\n\nListener exceptions do not prevent remaining specific or wildcard listeners from running. Each failure produces a `error` tap event, then `emit()` rethrows the first error after dispatch.\n\n```ts\nconst stopTrace = bus.tap((event) => {\n if (event.type === 'error') {\n recordListenerFailure(event.error);\n }\n});\n\nstopTrace();\n```\n\nHerald captures synchronous listener throws only, and `tap()` does not suppress the error rethrown after dispatch. A listener that starts asynchronous work must handle its own rejected promises.\n\n## Trace Bus Activity\n\n`tap()` observes subscriptions, unsubscriptions, wildcard subscriptions, emissions, listener failures, and disposal without changing bus behavior. Tap-handler failures are swallowed.\n\n```ts\nconst stopTrace = bus.tap((event) => {\n recordHeraldEvent(event);\n}, { signal: owner.signal });\n```\n\nAn `emit` trace includes the event key, payload, and number of specific plus wildcard listeners invoked. Trace events do not include timestamps; add them in the observer if needed.\n\n## Inspect Listeners\n\n```ts\nbus.listenerCount('cart:updated'); // specific listeners for this key\nbus.listenerCount(); // all specific listeners\nbus.wildcardCount(); // onAny listeners\nbus.eventNames(); // keys with active specific listeners\n```\n\n`eventNames()` does not include events that only have wildcard listeners.\n\n## Dispose the Bus\n\n`dispose()` is permanent and idempotent. It emits a final tap event, aborts `disposalSignal` with `BusDisposedError`, clears all listeners and taps, and rejects pending waits. Later subscriptions and emissions are no-ops.\n\n```ts\nusing scopedBus = createBus<AppEvents>({ name: 'checkout' });\nscopedBus.disposalSignal.addEventListener('abort', () => releaseOwnedWork());\n```\n\nThe configured name appears in disposal errors and listener-threshold warnings.\n\n## Test Emissions\n\nImport test helpers from the focused testing entry point.\n\n```ts\nimport { createTestBus } from '@vielzeug/herald/testing';\n\nusing testBus = createTestBus<AppEvents>();\ntestBus.emit('cart:updated', { count: 2 });\n\nexpect(testBus.emitted('cart:updated')).toEqual([{ count: 2 }]);\nexpect(testBus.emittedCount('cart:updated')).toBe(1);\nexpect(testBus.allEmitted()).toEqual({ 'cart:updated': [{ count: 2 }] });\n\ntestBus.reset();\n```\n\nReturned recordings are snapshots. `reset()` clears recording history without removing listeners; disposal clears both.\n\n## Working with Other Vielzeug Libraries\n\nUse Herald for temporal facts, Ripple for retained state, Rune for structured logging inside `tap()`, and Postmaster when work must survive reloads or retry durably.\n\n## Best Practices\n\n- Define one explicit event map per module or application boundary.\n- Emit facts, not mutable retained state.\n- Keep synchronous listeners short.\n- Handle rejected promises inside asynchronous listener work.\n- Keep unsubscribe functions or pass ownership signals.\n- Use `wait()` and `waitAny()` only for future one-shot events.\n- Install a `tap()` observer when listener failures must be visible.\n- Dispose owner-scoped buses.\n",
7
7
  "examples": "---\ntitle: Herald — Examples\ndescription: Recipes for typed delivery, waits, lifecycle ownership, diagnostics, bridging, and testing.\n---\n\n## Examples\n\n- [Standalone Bus](./examples/standalone-entry.md)\n- [Module-Level Bus](./examples/module-level-bus.md)\n- [Await a One-Time Event](./examples/awaiting-a-one-time-event.md)\n- [Inspect Listener Counts](./examples/inspecting-listener-counts.md)\n- [Observe Listener Failures](./examples/custom-error-boundary.md)\n- [Handle Disposal in Async Code](./examples/handling-disposal-in-async-code.md)\n- [Scope a Bus to a Request](./examples/request-scoping.md)\n- [Consume Ongoing Events](./examples/streaming-with-events.md)\n- [Bridge Buses Explicitly](./examples/bus-bridging-with-pipeevents.md)\n- [Test with `createTestBus()`](./examples/testing-with-createtestbus.md)\n"
8
8
  },
9
9
  "examples": [
@@ -34,7 +34,7 @@
34
34
  },
35
35
  {
36
36
  "id": "error-handling",
37
- "code": "import { createBus, HeraldError } from '@vielzeug/herald'\n\n// tap() captures every listener error; emit() rethrows the first after all listeners run\nconst errors = []\n\nconst bus = createBus()\n\nbus.tap((event) => {\n if (event.type === 'error') {\n errors.push({ event: event.event, message: event.error instanceof Error ? event.error.message : String(event.error) })\n }\n})\n\nbus.on('order:placed', () => console.log('confirmation email sent'))\nbus.on('order:placed', () => {\n throw new Error('inventory check failed')\n})\nbus.on('order:placed', () => console.log('analytics event recorded')) // still runs\n\ntry {\n bus.emit('order:placed', { id: 'ORD-1', total: 49.99 })\n} catch (err) {\n console.log('first error rethrown:', err.message)\n}\n\nconsole.log('captured errors:', errors)\n// [{ event: 'order:placed', message: 'inventory check failed' }]\n\ntry {\n bus.waitAny(['event-a']) // waitAny requires at least 2 event keys\n} catch (err) {\n console.log('caught herald error?', err instanceof HeraldError, '-', err.message)\n}\n\nbus.dispose()",
37
+ "code": "import { createBus, HeraldError } from '@vielzeug/herald'\n\n// tap() captures every listener error; emit() rethrows the first after all listeners run\nconst errors = []\n\nconst bus = createBus()\n\nbus.tap((event) => {\n if (event.type === 'error') {\n errors.push({ event: event.event, message: event.error instanceof Error ? event.error.message : String(event.error) })\n }\n})\n\nbus.on('order:placed', () => console.log('confirmation email sent'))\nbus.on('order:placed', () => {\n throw new Error('inventory check failed')\n})\nbus.on('order:placed', () => console.log('analytics event recorded')) // still runs\n\ntry {\n bus.emit('order:placed', { id: 'ORD-1', total: 49.99 })\n} catch (err) {\n console.log('first error rethrown:', err.message)\n}\n\nconsole.log('captured errors:', errors)\n// [{ event: 'order:placed', message: 'inventory check failed' }]\n\n// Disposal interrupts pending waits with BusDisposedError\nconst pending = bus.wait('order:placed')\n\nbus.dispose()\n\npending.catch((err) => {\n console.log('caught herald error?', err instanceof HeraldError, '-', err.message)\n})",
38
38
  "name": "Error Handling"
39
39
  },
40
40
  {
@@ -65,9 +65,8 @@
65
65
  ],
66
66
  "typeSignatures": {
67
67
  "createBus": "export { createBus } from './bus';",
68
- "BusDisposedError": "export { BusDisposedError, HeraldConfigError, HeraldError } from './errors';",
69
- "HeraldConfigError": "export { BusDisposedError, HeraldConfigError, HeraldError } from './errors';",
70
- "HeraldError": "export { BusDisposedError, HeraldConfigError, HeraldError } from './errors';",
68
+ "BusDisposedError": "export { BusDisposedError, HeraldError } from './errors';",
69
+ "HeraldError": "export { BusDisposedError, HeraldError } from './errors';",
71
70
  "Bus": "export type {\n Bus,\n BusOptions,\n EventKey,\n EventMap,\n HeraldEvent,\n Listener,\n SubscribeOptions,\n Unsubscribe,\n WaitAnyResult,\n} from './types';",
72
71
  "BusOptions": "export type {\n Bus,\n BusOptions,\n EventKey,\n EventMap,\n HeraldEvent,\n Listener,\n SubscribeOptions,\n Unsubscribe,\n WaitAnyResult,\n} from './types';",
73
72
  "EventKey": "export type {\n Bus,\n BusOptions,\n EventKey,\n EventMap,\n HeraldEvent,\n Listener,\n SubscribeOptions,\n Unsubscribe,\n WaitAnyResult,\n} from './types';",