@vielzeug/codex 2.2.7 → 2.2.9

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 (43) hide show
  1. package/data/catalog.json +1842 -0
  2. package/data/llms-full.txt +30872 -0
  3. package/data/llms.txt +44 -0
  4. package/data/manifest.json +8 -0
  5. package/data/packages/arsenal.json +210 -0
  6. package/data/packages/assay.json +39 -0
  7. package/data/packages/clockwork.json +67 -0
  8. package/data/packages/codex.json +43 -0
  9. package/data/packages/coins.json +102 -0
  10. package/data/packages/conduit.json +60 -0
  11. package/data/packages/courier.json +58 -0
  12. package/data/packages/dnd.json +77 -0
  13. package/data/packages/familiar.json +40 -0
  14. package/data/packages/flux.json +93 -0
  15. package/data/packages/focus.json +37 -0
  16. package/data/packages/forge.json +83 -0
  17. package/data/packages/gesture.json +25 -0
  18. package/data/packages/herald.json +108 -0
  19. package/data/packages/illusionist.json +132 -0
  20. package/data/packages/keymap.json +60 -0
  21. package/data/packages/ledger.json +57 -0
  22. package/data/packages/lingua.json +68 -0
  23. package/data/packages/necromancer.json +50 -0
  24. package/data/packages/orbit.json +99 -0
  25. package/data/packages/ore.json +68 -0
  26. package/data/packages/prism.json +66 -0
  27. package/data/packages/pulse.json +69 -0
  28. package/data/packages/refine.json +12 -0
  29. package/data/packages/ripple.json +83 -0
  30. package/data/packages/rune.json +79 -0
  31. package/data/packages/sandbox.json +40 -0
  32. package/data/packages/scout.json +60 -0
  33. package/data/packages/scroll.json +109 -0
  34. package/data/packages/sentinel.json +35 -0
  35. package/data/packages/sourcerer.json +73 -0
  36. package/data/packages/spell.json +133 -0
  37. package/data/packages/tempo.json +81 -0
  38. package/data/packages/vault.json +85 -0
  39. package/data/packages/ward.json +114 -0
  40. package/data/packages/wayfinder.json +110 -0
  41. package/data/refine.json +11887 -0
  42. package/data/search.json +1556 -0
  43. package/package.json +1 -1
@@ -0,0 +1,93 @@
1
+ {
2
+ "apiSource": "export { toAsyncIterable } from './async';\nexport { stream } from './core';\nexport { FluxError, FluxTimeoutError } from './errors';\nexport { combineLatest, concat, merge } from './operators/combination';\nexport type { IntervalOptions, TimerOptions } from './operators/creation';\nexport { from, fromEvent, interval, of, timer } from './operators/creation';\nexport type { DebounceOptions, TimeoutOptions } from './operators/filtering';\nexport { debounce, take, takeUntil, timeout } from './operators/filtering';\nexport type { ConcatMapOptions } from './operators/transformation';\nexport { concatMap, filter, map, mergeMap, scan, switchMap } from './operators/transformation';\nexport type { RetryOptions, ToArrayOptions, ValueOptions } from './operators/utility';\nexport { first, last, retry, toArray } from './operators/utility';\nexport { pipe } from './pipe';\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';\n",
3
+ "docs": {
4
+ "index": "---\ntitle: Flux — Explicit push streams for TypeScript\ndescription: Reusable push streams with subscription-owned cancellation, bounded buffering, and optional ecosystem adapters.\npackage: flux\ncategory: reactive\nkeywords: [streams, reactive, operators, cancellation, buffering, channels]\nrelated: [ripple, herald, pulse, courier]\nexports: [stream, 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({ every: 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- `fromSignal()` / `toSignal()` — bridge Ripple signals\n- `fromQuery()` — adapt Courier query state\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/) — adapt reactive signal state through `@vielzeug/flux/ripple`.\n- [Courier](/courier/) — adapt query snapshots and SSE events through `@vielzeug/flux/courier`.\n- [Herald](/herald/) — adapt typed bus events through `@vielzeug/flux/herald`.\n- [Pulse](/pulse/) — adapt connection and presence events through `@vielzeug/flux/pulse`.\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 adapters.\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 | `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\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| `@vielzeug/flux/ripple` | Ripple signal adapters |\n| `@vielzeug/flux/courier` | Courier query and SSE adapters |\n| `@vielzeug/flux/herald` | Herald bus adapters |\n| `@vielzeug/flux/pulse` | Pulse event and presence adapters |\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(options: IntervalOptions): Stream<number>\n```\n\nEmits incrementing values starting at zero.\n\n| Option | Type | Description |\n| --- | --- | --- |\n| `every` | `number` | Non-negative interval duration in 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` | Non-negative initial delay in milliseconds |\n| `interval` | `number` | Optional non-negative repeat duration |\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>): Operator<A, B>\n```\n\nRuns every inner stream concurrently.\n\n---\n\n### `concatMap()`\n\n```ts\nconcatMap<A, B>(project: (value: A) => Stream<B>, options: ConcatMapOptions): Operator<A, B>\n```\n\nRuns inner streams in order. Exceeding capacity errors output.\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>(options: DebounceOptions): Operator<T, T>\n```\n\nEmits latest value after configured silence. Pending value flushes on source completion.\n\n| Option | Type | Description |\n| --- | --- | --- |\n| `for` | `number` | Non-negative silence duration in milliseconds |\n\n---\n\n### `timeout()`\n\n```ts\ntimeout<T>(options: TimeoutOptions): Operator<T, T>\n```\n\nErrors with `FluxTimeoutError` when source is silent too long.\n\n| Option | Type | Description |\n| --- | --- | --- |\n| `after` | `number` | Non-negative inactivity duration in 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 or backoff function |\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, abort, 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): Promise<T>\n```\n\nResolves first value and cancels source. Rejects on source error or abort.\n\n---\n\n### `last()`\n\n```ts\nlast<T>(source: Stream<T>, options?: ValueOptions): Promise<T | undefined>\n```\n\nResolves last value on completion, or `undefined` when source completes empty.\n\n## Async Conversion\n\n### `toAsyncIterable()`\n\n```ts\ntoAsyncIterable<T>(source: Stream<T>, options: AsyncIterableOptions): AsyncIterable<T>\n```\n\nConverts push stream to async iterable with bounded queue.\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 imperative multicast boundary. Disposal completes subscribers.\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## Adapters\n\n### `@vielzeug/flux/ripple`\n\n```ts\nfromSignal<T>(source: Readable<T>): Stream<T>\ntoSignal<T>(source: Stream<T>, options: ToSignalOptions<T>): SignalBinding<T>\n```\n\n`fromSignal()` emits current value first. `toSignal()` preserves final value then disposes binding when source completes, errors, or supplied signal aborts. On source error, `toSignal()` calls `options.onError` if provided (otherwise logs via `console.error` in dev — in production the log is stripped and the error is silently swallowed), then disposes — the signal freezes at its last value. Pass `onError` to surface source errors in production builds.\n\n### `@vielzeug/flux/courier`\n\n```ts\nfromQuery<T extends { key: readonly unknown[]; fetch: (...args: never[]) => Promise<unknown> }>(\n cache: { getSnapshot<T>(key: readonly unknown[]): T | null; subscribe(key: readonly unknown[], listener: () => void): () => void },\n definition: T,\n): Stream<AsyncState<Awaited<ReturnType<T['fetch']>>> | null>\n```\n\n`fromQuery()` infers data from `definition.fetch` and emits Courier-compatible `AsyncState` snapshots.\n\n### `@vielzeug/flux/herald`\n\n```ts\nfromBus<T extends EventMap, K extends EventKey<T>>(bus: Bus<T>, event: K): Stream<T[K]>\ntoBus<T extends EventMap, K extends EventKey<T>>(bus: Bus<T>, event: K): Operator<T[K], T[K]>\n```\n\n### `@vielzeug/flux/pulse`\n\n```ts\nfromPulse<S extends PulseSchema, K extends EventKey<ServerEvents<S>>>(pulse: Pulse<S>, event: K): Stream<ServerEvents<S>[K]>\nfromRoomPresence<T>(room: PresenceRoomScope<T>): Stream<ReadonlyMap<string, T>>\n```\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 IntervalOptions = { every: number };\ntype TimerOptions = { delay: number; interval?: number };\ntype DebounceOptions = { for: number };\ntype TimeoutOptions = { after: number };\ntype ConcatMapOptions = { capacity: number };\ntype RetryOptions = { attempts: number; delay?: number | ((attempt: number) => number) };\ntype ToArrayOptions = { maxItems: number; signal?: AbortSignal };\ntype ValueOptions = { signal?: AbortSignal };\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};\ntype ToSignalOptions<T> = { initial: T; onError?: (reason: unknown) => void; signal?: AbortSignal };\ntype SignalBinding<T> = {\n [Symbol.dispose](): void;\n readonly disposalSignal: AbortSignal;\n dispose(): void;\n readonly disposed: boolean;\n readonly signal: Readable<T>;\n readonly value: 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",
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, 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. Pass `{ signal }` to cancel waiting; cancellation rejects with `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({ every: 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 loop permanently completes iterator. 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({ after: 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\nImport adapters from dedicated subpaths. Core Flux does not require adapter peers.\n\n```ts\nimport { fromQuery } from '@vielzeug/flux/courier';\nimport { fromBus } from '@vielzeug/flux/herald';\nimport { fromRoomPresence } from '@vielzeug/flux/pulse';\nimport { fromSignal, toSignal } from '@vielzeug/flux/ripple';\n```\n\n`toSignal()` preserves final source value, then disposes binding when source completes, errors, or external signal aborts.\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 `concatMap()` queue capacity.\n- Bound `toArray()` with realistic `maxItems`.\n- Choose async iterator overflow policy deliberately.\n- Keep `Channel.send()` at integration boundaries.\n",
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- [Ripple Signal Integration](./examples/signal-integration.md)\n"
8
+ },
9
+ "examples": [
10
+ {
11
+ "id": "async-conversion",
12
+ "code": "// Consume a push stream with an explicit bounded async queue.\nimport { of, toAsyncIterable } from '@vielzeug/flux'\n\nasync function run() {\n const values = toAsyncIterable(of(1, 2, 3), {\n capacity: 3,\n overflow: 'error',\n })\n\n for await (const value of values) {\n console.log('value:', value)\n }\n\n console.log('iterator complete')\n}\n\nvoid run()",
13
+ "name": "Async Conversion"
14
+ },
15
+ {
16
+ "id": "basic-flux",
17
+ "code": "// Build a cold stream and convert a bounded derived sequence to an array.\nimport { toArray, map, pipe, stream, take } from '@vielzeug/flux'\n\nconst integers = stream((sink) => {\n let value = 0\n const id = setInterval(() => sink.next(value++), 50)\n\n return () => clearInterval(id)\n})\n\nconst values = pipe(\n integers,\n map((value) => value * 2),\n take(3),\n)\n\ntoArray(values, { maxItems: 3 })\n .then((result) => console.log('values:', result))\n .catch(console.error)",
18
+ "name": "Creating a Stream"
19
+ },
20
+ {
21
+ "id": "cancellation",
22
+ "code": "// Stop synchronous iterable work as soon as take() reaches its limit.\nimport { from, pipe, take } from '@vielzeug/flux'\n\nfunction* ids() {\n for (let value = 1; value <= 5; value++) {\n console.log('produced:', value)\n yield value\n }\n}\n\npipe(from(ids()), take(2)).subscribe({\n complete: () => console.log('complete'),\n error: console.error,\n next: (value) => console.log('consumed:', value),\n})",
23
+ "name": "Cancelling an Iterable"
24
+ },
25
+ {
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()",
28
+ "name": "Combining Streams"
29
+ },
30
+ {
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)",
33
+ "name": "Error Handling"
34
+ },
35
+ {
36
+ "id": "operators",
37
+ "code": "// Build a typed transformation pipeline with filter, map, and scan.\nimport { toArray, filter, map, of, pipe, scan } from '@vielzeug/flux'\n\nconst result = pipe(\n of(1, 2, 3, 4, 5),\n filter((value) => value % 2 !== 0),\n map((value) => value * 10),\n scan((total, value) => total + value, 0),\n)\n\ntoArray(result, { maxItems: 3 })\n .then((values) => console.log('running totals:', values))\n .catch(console.error)",
38
+ "name": "Operators"
39
+ },
40
+ {
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()",
43
+ "name": "Channels"
44
+ }
45
+ ],
46
+ "typeSignatures": {
47
+ "toAsyncIterable": "export { toAsyncIterable } from './async';",
48
+ "stream": "export { stream } from './core';",
49
+ "FluxError": "export { FluxError, FluxTimeoutError } from './errors';",
50
+ "FluxTimeoutError": "export { FluxError, FluxTimeoutError } from './errors';",
51
+ "combineLatest": "export { combineLatest, concat, merge } from './operators/combination';",
52
+ "concat": "export { combineLatest, concat, merge } from './operators/combination';",
53
+ "merge": "export { combineLatest, concat, merge } from './operators/combination';",
54
+ "IntervalOptions": "export type { IntervalOptions, TimerOptions } from './operators/creation';",
55
+ "TimerOptions": "export type { IntervalOptions, TimerOptions } from './operators/creation';",
56
+ "from": "export { from, fromEvent, interval, of, timer } from './operators/creation';",
57
+ "fromEvent": "export { from, fromEvent, interval, of, timer } from './operators/creation';",
58
+ "interval": "export { from, fromEvent, interval, of, timer } from './operators/creation';",
59
+ "of": "export { from, fromEvent, interval, of, timer } from './operators/creation';",
60
+ "timer": "export { from, fromEvent, interval, of, timer } from './operators/creation';",
61
+ "DebounceOptions": "export type { DebounceOptions, TimeoutOptions } from './operators/filtering';",
62
+ "TimeoutOptions": "export type { DebounceOptions, TimeoutOptions } from './operators/filtering';",
63
+ "debounce": "export { debounce, take, takeUntil, timeout } from './operators/filtering';",
64
+ "take": "export { debounce, take, takeUntil, timeout } from './operators/filtering';",
65
+ "takeUntil": "export { debounce, take, takeUntil, timeout } from './operators/filtering';",
66
+ "timeout": "export { debounce, take, takeUntil, timeout } from './operators/filtering';",
67
+ "ConcatMapOptions": "export type { ConcatMapOptions } from './operators/transformation';",
68
+ "concatMap": "export { concatMap, filter, map, mergeMap, scan, switchMap } from './operators/transformation';",
69
+ "filter": "export { concatMap, filter, map, mergeMap, scan, switchMap } from './operators/transformation';",
70
+ "map": "export { concatMap, filter, map, mergeMap, scan, switchMap } from './operators/transformation';",
71
+ "mergeMap": "export { concatMap, filter, map, mergeMap, scan, switchMap } from './operators/transformation';",
72
+ "scan": "export { concatMap, filter, map, mergeMap, scan, switchMap } from './operators/transformation';",
73
+ "switchMap": "export { concatMap, filter, map, mergeMap, scan, switchMap } from './operators/transformation';",
74
+ "RetryOptions": "export type { RetryOptions, ToArrayOptions, ValueOptions } from './operators/utility';",
75
+ "ToArrayOptions": "export type { RetryOptions, ToArrayOptions, ValueOptions } from './operators/utility';",
76
+ "ValueOptions": "export type { RetryOptions, ToArrayOptions, ValueOptions } from './operators/utility';",
77
+ "first": "export { first, last, retry, toArray } from './operators/utility';",
78
+ "last": "export { first, last, retry, toArray } from './operators/utility';",
79
+ "retry": "export { first, last, retry, toArray } from './operators/utility';",
80
+ "toArray": "export { first, last, retry, toArray } from './operators/utility';",
81
+ "pipe": "export { pipe } from './pipe';",
82
+ "AsyncIterableOptions": "export type {\n AsyncIterableOptions,\n Observer,\n Operator,\n OverflowPolicy,\n Producer,\n Sink,\n Stream,\n SubscribeOptions,\n Subscription,\n Teardown,\n} from './types';",
83
+ "Observer": "export type {\n AsyncIterableOptions,\n Observer,\n Operator,\n OverflowPolicy,\n Producer,\n Sink,\n Stream,\n SubscribeOptions,\n Subscription,\n Teardown,\n} from './types';",
84
+ "Operator": "export type {\n AsyncIterableOptions,\n Observer,\n Operator,\n OverflowPolicy,\n Producer,\n Sink,\n Stream,\n SubscribeOptions,\n Subscription,\n Teardown,\n} from './types';",
85
+ "OverflowPolicy": "export type {\n AsyncIterableOptions,\n Observer,\n Operator,\n OverflowPolicy,\n Producer,\n Sink,\n Stream,\n SubscribeOptions,\n Subscription,\n Teardown,\n} from './types';",
86
+ "Producer": "export type {\n AsyncIterableOptions,\n Observer,\n Operator,\n OverflowPolicy,\n Producer,\n Sink,\n Stream,\n SubscribeOptions,\n Subscription,\n Teardown,\n} from './types';",
87
+ "Sink": "export type {\n AsyncIterableOptions,\n Observer,\n Operator,\n OverflowPolicy,\n Producer,\n Sink,\n Stream,\n SubscribeOptions,\n Subscription,\n Teardown,\n} from './types';",
88
+ "Stream": "export type {\n AsyncIterableOptions,\n Observer,\n Operator,\n OverflowPolicy,\n Producer,\n Sink,\n Stream,\n SubscribeOptions,\n Subscription,\n Teardown,\n} from './types';",
89
+ "SubscribeOptions": "export type {\n AsyncIterableOptions,\n Observer,\n Operator,\n OverflowPolicy,\n Producer,\n Sink,\n Stream,\n SubscribeOptions,\n Subscription,\n Teardown,\n} from './types';",
90
+ "Subscription": "export type {\n AsyncIterableOptions,\n Observer,\n Operator,\n OverflowPolicy,\n Producer,\n Sink,\n Stream,\n SubscribeOptions,\n Subscription,\n Teardown,\n} from './types';",
91
+ "Teardown": "export type {\n AsyncIterableOptions,\n Observer,\n Operator,\n OverflowPolicy,\n Producer,\n Sink,\n Stream,\n SubscribeOptions,\n Subscription,\n Teardown,\n} from './types';"
92
+ }
93
+ }
@@ -0,0 +1,37 @@
1
+ {
2
+ "apiSource": "export type {\n ListKeyAction,\n ListNavigation,\n ListNavigationAction,\n ListNavigationChange,\n ListNavigationOptions,\n ListNavigationTypeaheadOptions,\n MaybeGetter,\n} from './list-navigation';\nexport { createListNavigation } from './list-navigation';\nexport type {\n CaptureFocusOptions,\n FocusRestorer,\n FocusTarget,\n RestoreFocusOptions,\n} from './restore-focus';\nexport { captureFocus, restoreFocus } from './restore-focus';\n",
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, onNavigate: ({ item }) => item.focus() });\nlist.addEventListener('keydown', nav.handleKeydown);\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 onNavigate: ({ item }) => item.focus(),\n});\n\ncontainer.addEventListener('keydown', nav.handleKeydown);\n\nrestore();\nnav.dispose();\n```\n\n## Features\n\n<div class=\"features-grid\">\n\n- `createListNavigation()` — reusable composite-widget keyboard navigation\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\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 | Disabled items require an explicit predicate |\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, typeahead, and lifecycle 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 onNavigate: ({ item }) => item.focus(),\n});\n```\n\n| Member | Return | Contract |\n| --- | --- | --- |\n| `handleKeydown(event)` | `boolean` | Handles configured navigation keys and optional typeahead. |\n| `navigate(action)` | `number` | Moves programmatically and returns the active index, or `-1`. |\n| `set(index)` | `number` | Sets the active index when usable, or resets it 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| `dispose()` | `void` | Permanently disables the controller and aborts `disposalSignal`. |\n| `disposed` | `boolean` | Indicates whether the controller is permanently disabled. |\n| `disposalSignal` | `AbortSignal` | Aborts when the controller is disposed. |\n| `[Symbol.dispose]()` | `void` | Calls `dispose()`. |\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.\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 action: ListKeyAction;\n event?: KeyboardEvent;\n index: number;\n item: T;\n};\n\ntype ListNavigationTypeaheadOptions<T> = {\n delayMs?: number;\n getLabel: (item: T, index: number) => string;\n};\n\ntype ListNavigationOptions<T> = {\n direction?: MaybeGetter<'ltr' | 'rtl'>;\n disabled?: MaybeGetter<boolean | undefined>;\n getItems: () => readonly T[];\n isItemDisabled?: (item: T, index: number) => boolean;\n keys?: Partial<Record<ListNavigationAction, readonly string[]>>;\n loop?: boolean;\n onNavigate?: (change: ListNavigationChange<T>) => void;\n orientation?: MaybeGetter<'both' | 'horizontal' | 'vertical'>;\n signal?: AbortSignal;\n typeahead?: ListNavigationTypeaheadOptions<T>;\n};\n\ntype ListNavigation<T> = {\n readonly disposalSignal: AbortSignal;\n readonly disposed: boolean;\n dispose(): void;\n getActiveItem(): T | undefined;\n getIndex(): number;\n handleKeydown(event: KeyboardEvent): boolean;\n navigate(action: ListNavigationAction): number;\n reset(): void;\n set(index: number): number;\n [Symbol.dispose](): 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`. Non-finite or non-positive values use the default.\n\n## Errors\n\n`@vielzeug/focus` does not export custom error classes.\n",
6
+ "usage": "---\ntitle: Focus — Usage Guide\ndescription: Build keyboard-focus navigation and restoration into composite widgets.\n---\n\n[[toc]]\n\n## Basic Usage\n\nCreate one navigation handle for a composite widget and forward `keydown` events to it.\n\n```ts\nimport { createListNavigation } from '@vielzeug/focus';\n\nconst nav = createListNavigation({\n getItems: () => items,\n loop: true,\n onNavigate: ({ item }) => item.focus(),\n});\n\nlist.addEventListener('keydown', nav.handleKeydown);\n```\n\n## Orientation and Direction\n\nUse orientation and direction to derive default key bindings.\n\n```ts\nconst nav = createListNavigation({\n direction: () => (document.dir === 'rtl' ? 'rtl' : 'ltr'),\n getItems: () => tabs,\n orientation: 'horizontal',\n});\n```\n\n## Disabled and Dynamic Items\n\nProvide `isItemDisabled` when disabled state is data-driven.\n\n```ts\nconst nav = createListNavigation({\n getItems: () => rows,\n isItemDisabled: (item) => item.hasAttribute('aria-disabled'),\n});\n```\n\n## Typeahead\n\nEnable character-based navigation with the `typeahead` option.\n\n```ts\nconst nav = createListNavigation({\n getItems: () => menuItems,\n typeahead: {\n delayMs: 300,\n getLabel: (item) => item.textContent ?? '',\n },\n});\n```\n\n`typeahead.delayMs` defaults to `500`. Repeated characters cycle matching items without waiting for the timeout.\n\n## Focus Restoration\n\nCapture focus before opening a floating surface and restore it after closing.\n\n```ts\nimport { captureFocus } from '@vielzeug/focus';\n\nconst restore = captureFocus();\n\nopenDialog();\ncloseDialog();\nrestore();\n```\n\n## Framework Integration\n\nCreate the navigation handle once per component instance and dispose it on unmount. The handle is framework-neutral — wire `keydown` from whatever element owns the composite widget's keyboard surface.\n\n::: code-group\n\n```tsx [React]\nimport { useEffect, useRef } from 'react';\nimport { createListNavigation } from '@vielzeug/focus';\n\nfunction Tabs({ tabs }: { tabs: Array<{ id: string; label: 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((el): el is HTMLButtonElement => el !== null),\n loop: true,\n onNavigate: ({ item }) => item.focus(),\n orientation: 'horizontal',\n });\n\n list.addEventListener('keydown', nav.handleKeydown);\n return () => {\n list.removeEventListener('keydown', nav.handleKeydown);\n nav.dispose();\n };\n }, []);\n\n return (\n <div ref={listRef} role=\"tablist\">\n {tabs.map((tab, i) => (\n <button\n key={tab.id}\n ref={(el) => { tabRefs.current[i] = el; }}\n role=\"tab\"\n >\n {tab.label}\n </button>\n ))}\n </div>\n );\n}\n```\n\n```vue [Vue 3]\n<script setup lang=\"ts\">\nimport { onMounted, onUnmounted, ref } from 'vue';\nimport { createListNavigation } from '@vielzeug/focus';\n\nconst props = defineProps<{ tabs: Array<{ id: string; label: string }> }>();\n\nconst listEl = ref<HTMLDivElement | null>(null);\nconst tabEls = ref<Array<HTMLButtonElement | null>>([]);\n\nlet nav: ReturnType<typeof createListNavigation> | undefined;\n\nonMounted(() => {\n if (!listEl.value) return;\n\n nav = createListNavigation({\n getItems: () => tabEls.value.filter((el): el is HTMLButtonElement => el !== null),\n loop: true,\n onNavigate: ({ item }) => item.focus(),\n orientation: 'horizontal',\n });\n\n listEl.value.addEventListener('keydown', nav.handleKeydown);\n});\n\nonUnmounted(() => {\n if (nav) listEl.value?.removeEventListener('keydown', nav.handleKeydown);\n nav?.dispose();\n});\n</script>\n\n<template>\n <div ref=\"listEl\" role=\"tablist\">\n <button\n v-for=\"(tab, i) in tabs\"\n :key=\"tab.id\"\n :ref=\"(el) => { tabEls[i] = el as HTMLButtonElement | null; }\"\n role=\"tab\"\n >\n {{ tab.label }}\n </button>\n </div>\n</template>\n```\n\n```svelte [Svelte]\n<script lang=\"ts\">\n import { onMount } from 'svelte';\n import { createListNavigation } from '@vielzeug/focus';\n\n let { tabs }: { tabs: Array<{ id: string; label: string }> } = $props();\n\n let listEl: HTMLDivElement;\n let tabEls: HTMLButtonElement[] = [];\n\n onMount(() => {\n const nav = createListNavigation({\n getItems: () => tabEls,\n loop: true,\n onNavigate: ({ item }) => item.focus(),\n orientation: 'horizontal',\n });\n\n listEl.addEventListener('keydown', nav.handleKeydown);\n return () => {\n listEl.removeEventListener('keydown', nav.handleKeydown);\n nav.dispose();\n };\n });\n</script>\n\n<div bind:this={listEl} role=\"tablist\">\n {#each tabs as tab, i}\n <button bind:this={tabEls[i]} role=\"tab\">{tab.label}</button>\n {/each}\n</div>\n```\n\n:::\n\n## Working with Other Vielzeug Libraries\n\n### Focus + Refine\n\nRefine's `ore-menu`, `ore-dialog`, and `ore-list` use Focus internally for keyboard navigation and focus restoration. When building custom composite widgets on top of Refine components, use `createListNavigation` for the keyboard layer and let Refine handle rendering.\n\n```ts\nimport { createListNavigation } from '@vielzeug/focus';\n\n// Custom tab bar built alongside ore-tab panels\nconst tabNav = createListNavigation({\n getItems: () => Array.from(host.querySelectorAll('[role=\"tab\"]')),\n loop: true,\n onNavigate: ({ item }) => item.focus(),\n orientation: 'horizontal',\n});\n\nhost.addEventListener('keydown', tabNav.handleKeydown);\n```\n\n### Focus + Keymap\n\nUse Keymap for global shortcuts and Focus for composite-widget navigation. They operate on different event layers without conflict.\n\n```ts\nimport { createKeymap } from '@vielzeug/keymap';\nimport { createListNavigation } from '@vielzeug/focus';\n\nconst nav = createListNavigation({ getItems: () => items, onNavigate: ({ item }) => item.focus() });\n\nconst map = createKeymap({\n 'mod+k': () => openPalette(),\n escape: () => nav.reset(),\n});\n\nlist.addEventListener('keydown', nav.handleKeydown);\nmap.mount(document);\n```\n\n## Best Practices\n\n- **Keep** item discovery in one function.\n- **Drive** focus side effects from `onNavigate`.\n- **Reset** navigation on overlay close when focus context changes.\n- **Use** typeahead only when labels are stable and meaningful.\n- **Capture** return focus before opening transient surfaces.\n- **Dispose** handles when owners unmount.\n",
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
+ },
9
+ "examples": [
10
+ {
11
+ "id": "list-navigation",
12
+ "code": "import { createListNavigation } from '@vielzeug/focus'\n\nconst labels = ['Apple', 'Banana', 'Cherry']\nconst list = document.createElement('div')\nlist.setAttribute('role', 'listbox')\n\nconst items = labels.map((label, index) => {\n const item = document.createElement('button')\n item.textContent = label\n item.disabled = index === 1\n item.tabIndex = index === 0 ? 0 : -1\n list.appendChild(item)\n return item\n})\n\ndocument.body.appendChild(list)\n\nconst navigation = createListNavigation({\n getItems: () => items,\n isItemDisabled: (item) => item.disabled,\n loop: true,\n onNavigate: ({ item }) => {\n items.forEach((candidate) => {\n candidate.tabIndex = candidate === item ? 0 : -1\n })\n item.focus()\n },\n})\n\nnavigation.set(0)\nlist.addEventListener('keydown', navigation.handleKeydown)\nitems[0].focus()\nitems[0].dispatchEvent(new KeyboardEvent('keydown', { bubbles: true, key: 'ArrowDown' }))\n\nconsole.log(document.activeElement?.textContent) // 'Cherry'",
13
+ "name": "List Navigation"
14
+ },
15
+ {
16
+ "id": "restore-focus",
17
+ "code": "import { captureFocus } from '@vielzeug/focus'\n\nconst trigger = document.createElement('button')\ntrigger.textContent = 'Open dialog'\n\nconst dialogButton = document.createElement('button')\ndialogButton.textContent = 'Close dialog'\n\ndocument.body.append(trigger, dialogButton)\ntrigger.focus()\n\nconst restore = captureFocus()\ndialogButton.focus()\n\nconsole.log(restore()) // true\nconsole.log(document.activeElement === trigger) // true\nconsole.log(restore()) // false: restorers are one-shot",
18
+ "name": "Restore Captured Focus"
19
+ }
20
+ ],
21
+ "typeSignatures": {
22
+ "ListKeyAction": "export type {\n ListKeyAction,\n ListNavigation,\n ListNavigationAction,\n ListNavigationChange,\n ListNavigationOptions,\n ListNavigationTypeaheadOptions,\n MaybeGetter,\n} from './list-navigation';",
23
+ "ListNavigation": "export type {\n ListKeyAction,\n ListNavigation,\n ListNavigationAction,\n ListNavigationChange,\n ListNavigationOptions,\n ListNavigationTypeaheadOptions,\n MaybeGetter,\n} from './list-navigation';",
24
+ "ListNavigationAction": "export type {\n ListKeyAction,\n ListNavigation,\n ListNavigationAction,\n ListNavigationChange,\n ListNavigationOptions,\n ListNavigationTypeaheadOptions,\n MaybeGetter,\n} from './list-navigation';",
25
+ "ListNavigationChange": "export type {\n ListKeyAction,\n ListNavigation,\n ListNavigationAction,\n ListNavigationChange,\n ListNavigationOptions,\n ListNavigationTypeaheadOptions,\n MaybeGetter,\n} from './list-navigation';",
26
+ "ListNavigationOptions": "export type {\n ListKeyAction,\n ListNavigation,\n ListNavigationAction,\n ListNavigationChange,\n ListNavigationOptions,\n ListNavigationTypeaheadOptions,\n MaybeGetter,\n} from './list-navigation';",
27
+ "ListNavigationTypeaheadOptions": "export type {\n ListKeyAction,\n ListNavigation,\n ListNavigationAction,\n ListNavigationChange,\n ListNavigationOptions,\n ListNavigationTypeaheadOptions,\n MaybeGetter,\n} from './list-navigation';",
28
+ "MaybeGetter": "export type {\n ListKeyAction,\n ListNavigation,\n ListNavigationAction,\n ListNavigationChange,\n ListNavigationOptions,\n ListNavigationTypeaheadOptions,\n MaybeGetter,\n} from './list-navigation';",
29
+ "createListNavigation": "export { createListNavigation } from './list-navigation';",
30
+ "CaptureFocusOptions": "export type {\n CaptureFocusOptions,\n FocusRestorer,\n FocusTarget,\n RestoreFocusOptions,\n} from './restore-focus';",
31
+ "FocusRestorer": "export type {\n CaptureFocusOptions,\n FocusRestorer,\n FocusTarget,\n RestoreFocusOptions,\n} from './restore-focus';",
32
+ "FocusTarget": "export type {\n CaptureFocusOptions,\n FocusRestorer,\n FocusTarget,\n RestoreFocusOptions,\n} from './restore-focus';",
33
+ "RestoreFocusOptions": "export type {\n CaptureFocusOptions,\n FocusRestorer,\n FocusTarget,\n RestoreFocusOptions,\n} from './restore-focus';",
34
+ "captureFocus": "export { captureFocus, restoreFocus } from './restore-focus';",
35
+ "restoreFocus": "export { captureFocus, restoreFocus } from './restore-focus';"
36
+ }
37
+ }
@@ -0,0 +1,83 @@
1
+ {
2
+ "apiSource": "export { ForgeConfigError, ForgeDisposedError, ForgeError, ForgeSubmitError, ForgeValidationError } from './errors';\nexport { createForm } from './form';\nexport * from './types';\n",
3
+ "docs": {
4
+ "index": "---\ntitle: Forge — Immutable form state for TypeScript\ndescription: Framework-agnostic immutable form state with focused object fields and explicit validation results.\npackage: forge\ncategory: forms\nkeywords: [form-state, validation, immutable, input, submission]\nrelated: [spell, vault, courier]\nexports: [createForm, bindField, customValidator, saveForm, loadForm]\nenvironments: [browser, node, ssr, deno]\n---\n\n<!-- markdownlint-disable MD025 MD033 MD060 -->\n\n<PackageHero package=\"forge\" />\n\n## Why Forge?\n\nNative form state becomes difficult to inspect once values, validation, draft restoration, and UI bindings share mutable objects. Forge owns one immutable value tree and gives you typed handles for object branches without string paths, scoped controllers, or framework state.\n\n```ts\n// Before\nconst values = { email: '', password: '' };\nconst errors: Record<string, string> = {};\n\nfunction submit() {\n errors.email = values.email.includes('@') ? '' : 'Invalid email';\n errors.password = values.password.length >= 8 ? '' : 'Use at least eight characters';\n}\n\n// After\nconst form = createForm({\n initialValues: { email: '', password: '' },\n validate: (value) => ({\n fields: {\n email: value.email.includes('@') ? undefined : 'Invalid email',\n password: value.password.length >= 8 ? undefined : 'Use at least eight characters',\n },\n }),\n});\n```\n\n| Feature | Forge | Native form state | Framework-owned form state |\n| --- | --- | --- | --- |\n| Bundle size | <PackageInfo package=\"forge\" type=\"size\" /> | <ore-icon name=\"check\" size=\"16\"></ore-icon> | Varies |\n| Zero external 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| Immutable nested values | <ore-icon name=\"check\" size=\"16\"></ore-icon> | <ore-icon name=\"x\" size=\"16\"></ore-icon> | Varies |\n| Typed object field handles | <ore-icon name=\"check\" size=\"16\"></ore-icon> | <ore-icon name=\"x\" size=\"16\"></ore-icon> | Varies |\n| Framework-independent state | <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\n<div class=\"decision-callout\">\n\n**Use Forge when** form state needs framework-independent immutable values, typed object fields, and one explicit validation boundary.\n\n**Consider framework-owned form state when** application only needs a single UI framework's native input bindings.\n\n</div>\n\n## Installation\n\n::: code-group\n\n```sh [pnpm]\npnpm add @vielzeug/forge\n```\n\n```sh [npm]\nnpm install @vielzeug/forge\n```\n\n```sh [yarn]\nyarn add @vielzeug/forge\n```\n\n:::\n\nInstall `@vielzeug/spell` or `@vielzeug/vault` only when importing Forge's matching optional adapter.\n\n## Quick Start\n\nCreate a form, update a focused field, and submit only after validation passes.\n\n```ts\nimport { createForm } from '@vielzeug/forge';\n\nconst form = createForm({\n initialValues: { profile: { email: '', name: '' } },\n validate: (value) => ({\n fields: { profile: { email: value.profile.email.includes('@') ? undefined : 'Invalid email' } },\n }),\n});\n\nform.field('profile').field('email').set('ada@example.com');\n\nconst result = await form.submit(async (value) => {\n const response = await fetch('/api/profile', {\n body: JSON.stringify(value),\n headers: { 'Content-Type': 'application/json' },\n method: 'POST',\n });\n\n return response.ok;\n});\n\nif (result.status === 'invalid') console.log(result.errors);\n```\n\n## Features\n\n<div class=\"features-grid\">\n\n- `form.value` exposes one immutable nested value tree.\n- `form.field(key)` selects typed object branches without string paths.\n- `field.set(updater)` replaces array values through immutable updater functions.\n- `field.field(index)` selects typed array item fields by index.\n- `form.validate()` returns valid, invalid, or aborted results.\n- `form.submit(handler, signal?)` touches, validates, and invokes the handler when valid.\n- `bindField()` connects one DOM element without owning validation timing.\n- `customValidator()` maps Spell schema errors into Forge fields.\n- `saveForm()` and `loadForm()` persist explicit Vault draft records.\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- [Spell](/spell/) — adapt a Spell schema through `customValidator()`.\n- [Vault](/vault/) — save and restore explicit Forge draft records.\n- [Courier](/courier/) — send a validated form value through a mutation.\n\n</div>\n\n<!-- markdownlint-enable -->\n",
5
+ "api": "---\ntitle: Forge — API Reference\ndescription: Complete reference for immutable forms, fields, validation, serialization, and optional adapters.\n---\n\n[[toc]]\n\n## API Overview\n\n| Symbol | Purpose | Execution mode | Common gotcha |\n| --- | --- | --- | --- |\n| `createForm()` | Create immutable form state | Sync | `initialValues` cannot contain mutable class instances |\n| `form.field()` | Select a top-level or object child field | Sync | Unsafe keys (`__proto__`, `constructor`, `prototype`) are rejected |\n| `form.validate()` | Validate complete value | Async | Handle `aborted` separately |\n| `form.submit(handler, signal?)` | Touch, validate, then invoke handler | Async | Concurrent calls reject |\n| `form.reset()` | Restore or replace baseline | Sync | `reset(next)` makes `next` clean |\n| `form.subscribe()` | Observe form metadata | Sync | Throws after disposal |\n| `toFormData()` | Serialize values for multipart transport | Sync | `FileList` is transport-only |\n| `bindField()` | Bind one DOM element | Sync | Does not schedule validation |\n| `customValidator()` | Adapt a Spell schema | Async | Does not transform `form.value` |\n| `saveForm()` / `loadForm()` | Persist explicit Vault records | Async | FormDraftCodec owns record shape |\n\n## Package Entry Point\n\n| Import | Purpose |\n| --- | --- |\n| `@vielzeug/forge` | Core form factory, types, and errors |\n| `@vielzeug/forge/dom` | `bindField()` and DOM binding types |\n| `@vielzeug/forge/form-data` | `toFormData()` |\n| `@vielzeug/forge/spell` | `customValidator()` |\n| `@vielzeug/forge/vault` | `saveForm()`, `loadForm()`, and `FormDraftCodec` |\n\n## Core Functions\n\n### `createForm(options)`\n\n```ts\nfunction createForm<TValues extends Record<string, unknown>>(options: FormOptions<TValues>): Form<TValues>;\n```\n\nCreates a form with immutable initial values and an optional full-form validator.\n\n| Parameter | Type | Description |\n| --- | --- | --- |\n| `options.initialValues` | `TValues` | Initial value and reset baseline. Supports primitives, plain objects, arrays, `Date`, `File`, and `Blob`. |\n| `options.validate` | `FormValidator<TValues>` | Optional validator for the entire current value. |\n| `options.onSubscriberError` | `(error: unknown) => void` | Optional subscriber failure reporter. |\n\n**Returns:** `Form<TValues>`.\n\n**Example:**\n\n```ts\nimport { createForm } from '@vielzeug/forge';\n\nconst form = createForm({ initialValues: { email: '' } });\n```\n\n---\n\n### `toFormData(values)`\n\n```ts\nfunction toFormData(values: Record<string, unknown>): FormData;\n```\n\nConverts nested values into `FormData` with dot-separated object keys and repeated array keys.\n\n**Returns:** a populated `FormData` instance.\n\n**Example:**\n\n```ts\nimport { toFormData } from '@vielzeug/forge/form-data';\n\nconst body = toFormData({ profile: { email: 'ada@example.com' }, tags: ['typescript', 'forms'] });\n```\n\n## Form Handles\n\n### `Form<TValues>`\n\n`createForm()` returns this handle.\n\n| Member | Signature | Description |\n| --- | --- | --- |\n| `value` | `ReadonlyDeep<TValues>` | Current immutable value. |\n| `state` | `FormState<TValues>` | Submission, validation, touch, and error metadata. |\n| `field(key)` | `Field<TValues[K]>` | Select a top-level field. |\n| `set(next)` | `void` | Replace the complete value or derive a replacement. |\n| `reset(next?)` | `void` | Restore baseline or make `next` the baseline. |\n| `validate(signal?)` | `Promise<ValidationResult<TValues>>` | Run full-form validation. |\n| `submit(handler, signal?)` | `Promise<SubmitResult<TResult, TValues>>` | Touch, validate, and invoke handler when valid. |\n| `subscribe(listener, options?)` | `Unsubscribe` | Observe form state; throws after disposal. |\n| `dispose()` | `void` | Abort validation and clear subscribers. |\n| `disposed` | `boolean` | Whether the form has been disposed. |\n| `disposalSignal` | `AbortSignal` | Aborts on disposal. |\n\n### `Field<V>`\n\n`form.field(key)` and object-field `.field(key)` return this handle. Array-item `.field(index)` returns a per-item field handle.\n\n| Member | Signature | Description |\n| --- | --- | --- |\n| `value` | `ReadonlyDeep<V>` | Current immutable branch value. |\n| `error` | `string \\| undefined` | Current field error. |\n| `dirty` | `boolean` | Whether branch differs from baseline. |\n| `touched` | `boolean` | Whether field was touched. |\n| `state` | `FieldState<V>` | Snapshot of `dirty`, `error`, `touched`, and `value` in one read. |\n| `field(key)` | `Field<V[K]>` | Select child object field or array item by index. |\n| `set(next)` | `void` | Replace branch or derive a replacement. |\n| `reset()` | `void` | Restore exact baseline branch. |\n| `touch()` | `void` | Mark field touched. |\n| `subscribe(listener, options?)` | `Unsubscribe` | Observe field transitions; throws after disposal. |\n\n## Validation Results\n\n### `form.validate(signal?)`\n\n```ts\nfunction validate(signal?: AbortSignal): Promise<ValidationResult<TValues>>;\n```\n\nRuns the configured validator against the complete value. A newer validation aborts the older run.\n\n**Returns:** `ValidationResult<TValues>`.\n\n```ts\nconst result = await form.validate();\n\nif (result.status === 'invalid') console.log(result.errors, result.formError);\n```\n\n### `form.submit(handler, signal?)`\n\n```ts\nfunction submit<TResult = void>(\n handler: (values: ReadonlyDeep<TValues>, signal: AbortSignal) => MaybePromise<TResult>,\n signal?: AbortSignal,\n): Promise<SubmitResult<TResult, TValues>>;\n```\n\nTouches all fields, validates once, and invokes `handler` when validation is valid. The handler receives an `AbortSignal` that is aborted when the external `signal` (or the form's disposal signal) aborts.\n\n**Returns:** `SubmitResult<TResult, TValues>`. Handler failures reject normally unless caused by signal abort, which returns `{ status: 'aborted' }`.\n\n```ts\nconst result = await form.submit((value) => Promise.resolve(value));\n```\n\n## Adapters\n\n### `bindField(element, field, options)`\n\n```ts\nfunction bindField<Element extends HTMLElement, V>(\n element: Element,\n field: Field<V>,\n options: FieldBindingOptions<Element, V>,\n): () => void;\n```\n\nBinds one field to one element, marks it touched on blur, suppresses writeback from its own input event, and returns teardown.\n\n**Example:**\n\n```ts\nimport { bindField } from '@vielzeug/forge/dom';\n\nconst stop = bindField(input, form.field('email'), {\n read: (element) => element.value,\n write: (element, value) => {\n element.value = value;\n },\n});\n```\n\n---\n\n### `customValidator(schema)`\n\n```ts\nfunction customValidator<TValues extends Record<string, unknown>>(\n schema: Schema<unknown, TValues, SchemaMode>,\n): FormValidator<TValues>;\n```\n\nAdapts a Spell schema. Every failing union maps its closest branch while preserving unrelated errors. Array item issues map to per-item array fields; duplicate paths retain the first message.\n\n**Example:**\n\n```ts\nimport { customValidator } from '@vielzeug/forge/spell';\nimport { s } from '@vielzeug/spell';\n\nconst Profile = s.object({ email: s.string().email() });\nconst form = createForm({ initialValues: { email: '' }, validate: customValidator(Profile) });\n```\n\n---\n\n### `saveForm()` and `loadForm()`\n\n```ts\nfunction saveForm<TValues extends Record<string, unknown>, S extends AnySchema, K extends keyof S & string>(\n form: Form<TValues>, adapter: VaultStore<S>, table: K, codec: FormDraftCodec<TValues, S, K>,\n): Promise<void>;\n\nfunction loadForm<TValues extends Record<string, unknown>, S extends AnySchema, K extends keyof S & string>(\n form: Form<TValues>, adapter: VaultStore<S>, table: K, key: KeyOf<S, K>, codec: FormDraftCodec<TValues, S, K>,\n): Promise<boolean>;\n```\n\nPersists or restores a codec-defined Vault record. `loadForm()` calls `form.reset()` when the codec decodes a record.\n\n**Returns:** `loadForm()` returns `false` for a missing or rejected record.\n\n## Types\n\n```ts\ntype Unsubscribe = () => void;\ntype MaybePromise<T> = T | PromiseLike<T>;\ntype ReadonlyDeep<T> = T extends (...args: never[]) => unknown\n ? T\n : T extends readonly (infer Item)[]\n ? readonly ReadonlyDeep<Item>[]\n : T extends Record<string, unknown>\n ? { readonly [K in keyof T]: ReadonlyDeep<T[K]> }\n : T;\n\ntype FormErrors<T> = T extends readonly (infer Item)[]\n ? string | readonly (FormErrors<Item> | undefined)[]\n : T extends Record<string, unknown>\n ? string | { readonly [K in keyof T]?: FormErrors<T[K]> }\n : string;\n\ntype ValidationErrors<TValues extends Record<string, unknown>> = Readonly<{\n fields?: FormErrors<TValues>;\n formError?: string;\n}>;\n\ntype FormValidator<TValues extends Record<string, unknown>> = (\n values: ReadonlyDeep<TValues>, signal: AbortSignal,\n) => MaybePromise<ValidationErrors<TValues> | undefined>;\n\ntype FormOptions<TValues extends Record<string, unknown>> = Readonly<{\n initialValues: TValues;\n onSubscriberError?: (error: unknown) => void;\n validate?: FormValidator<NoInfer<TValues>>;\n}>;\n\ntype SubscribeOptions = Readonly<{ immediate?: boolean }>;\n\ntype FieldState<V> = Readonly<{\n dirty: boolean;\n error: string | undefined;\n touched: boolean;\n value: ReadonlyDeep<V>;\n}>;\n\ntype FormState<TValues extends Record<string, unknown>> = Readonly<{\n errors: FormErrors<TValues> | undefined;\n formError: string | undefined;\n hasErrors: boolean;\n submitCount: number;\n submitting: boolean;\n touched: boolean;\n validity: 'invalid' | 'unknown' | 'valid';\n validating: boolean;\n}>;\n\ntype ValidationResult<TValues extends Record<string, unknown>> =\n | Readonly<{ status: 'aborted' }>\n | Readonly<{ status: 'valid' }>\n | Readonly<{ errors: FormErrors<TValues> | undefined; formError: string | undefined; status: 'invalid' }>;\n\ntype SubmitResult<TResult = void, TValues extends Record<string, unknown> = Record<string, unknown>> =\n | Readonly<{ status: 'aborted' }>\n | Readonly<{ errors: FormErrors<TValues> | undefined; formError: string | undefined; status: 'invalid' }>\n | Readonly<{ status: 'ok'; value: TResult }>;\n```\n\n```ts\ntype ChildField<V> =\n NonNullable<V> extends readonly (infer Item)[]\n ? { field(index: number): Field<Item> }\n : NonNullable<V> extends Record<string, unknown>\n ? { field<K extends keyof NonNullable<V> & string>(key: K): Field<NonNullable<V>[K]> }\n : Record<never, never>;\n\ntype Field<V> = ChildField<V> & {\n readonly dirty: boolean;\n readonly error: string | undefined;\n readonly state: FieldState<V>;\n readonly touched: boolean;\n readonly value: ReadonlyDeep<V>;\n reset(): void;\n set(next: V | ((previous: ReadonlyDeep<V>) => V)): void;\n subscribe(listener: (state: FieldState<V>) => void, options?: SubscribeOptions): Unsubscribe;\n touch(): void;\n};\n\ntype Form<TValues extends Record<string, unknown>> = {\n [Symbol.dispose](): void;\n readonly disposalSignal: AbortSignal;\n readonly disposed: boolean;\n readonly state: FormState<TValues>;\n readonly value: ReadonlyDeep<TValues>;\n dispose(): void;\n field<K extends keyof TValues & string>(key: K): Field<TValues[K]>;\n reset(next?: TValues): void;\n set(next: TValues | ((previous: ReadonlyDeep<TValues>) => TValues)): void;\n submit<TResult = void>(\n handler: (values: ReadonlyDeep<TValues>, signal: AbortSignal) => MaybePromise<TResult>,\n signal?: AbortSignal,\n ): Promise<SubmitResult<TResult, TValues>>;\n subscribe(listener: (state: FormState<TValues>) => void, options?: SubscribeOptions): Unsubscribe;\n validate(signal?: AbortSignal): Promise<ValidationResult<TValues>>;\n};\n\ntype FieldBindingOptions<Element extends HTMLElement, V> = Readonly<{\n event?: keyof HTMLElementEventMap;\n read(element: Element): V;\n write?: (element: Element, value: ReadonlyDeep<V>) => void;\n}>;\n\ntype FormDraftCodec<TValues extends Record<string, unknown>, S extends AnySchema, K extends keyof S & string> = Readonly<{\n fromRecord(record: RecordOf<S, K>): TValues | undefined;\n toRecord(values: ReadonlyDeep<TValues>): RecordOf<S, K>;\n}>;\n```\n\n## Errors\n\n| Error | Trigger | Notable properties |\n| --- | --- | --- |\n| `ForgeError` | Base Forge error | `ForgeError.is(error)` narrows unknown values. |\n| `ForgeConfigError` | Unsafe key or unsupported form value | Extends `ForgeError`. |\n| `ForgeDisposedError` | Operation or subscription after disposal | Message names the attempted operation. |\n| `ForgeSubmitError` | Concurrent `submit()` call | Extends `ForgeError`. |\n| `ForgeValidationError` | Validator throws unexpectedly | Preserves original error as `cause`. |\n",
6
+ "usage": "---\ntitle: Forge — Usage Guide\ndescription: Build immutable forms, validate whole values, and use optional adapters.\n---\n\n[[toc]]\n\n## Basic Usage\n\nCreate one form value and update object branches through stable typed operations. Form values support primitives, plain objects, arrays, `Date`, `File`, and `Blob`; mutable class instances such as `Map` and `Set` are rejected.\n\n```ts\nimport { createForm } from '@vielzeug/forge';\n\nconst form = createForm({\n initialValues: { profile: { email: '', name: '' }, tags: [] as string[] },\n validate: (value) => ({\n fields: { profile: { email: value.profile.email.includes('@') ? undefined : 'Invalid email' } },\n }),\n});\n\nconst email = form.field('profile').field('email');\nemail.set('ada@example.com');\nform.field('tags').set((tags) => [...tags, 'typescript']);\n\nconsole.log(form.value.profile.email);\n```\n\n## Reset Values and Branches\n\nReset a field when one branch should return to its exact baseline. Reset the form with a value when newly loaded data should become the clean baseline.\n\n```ts\nconst name = form.field('profile').field('name');\n\nname.set('Ada');\nname.touch();\nname.reset();\n\nform.reset({ profile: { email: 'ada@example.com', name: 'Ada' }, tags: [] });\n```\n\nAn absent optional parent remains absent after a child reset. Array items support per-index field handles for reads, updates, and resets.\n\n## Validate and Submit\n\nReturn `fields` and an optional `formError` from one validator. `validate()` replaces the complete validation snapshot and returns an explicit status.\n\n```ts\nconst passwordForm = createForm({\n initialValues: { password: '', passwordConfirmation: '' },\n validate: (value) => ({\n fields: {\n password: value.password.length >= 8 ? undefined : 'Use at least eight characters',\n passwordConfirmation: value.password === value.passwordConfirmation ? undefined : 'Passwords must match',\n },\n }),\n});\n\nconst validation = await passwordForm.validate();\n\nif (validation.status === 'invalid') console.log(validation.errors);\nif (validation.status === 'aborted') console.log('Validation cancelled');\n\nconst result = await passwordForm.submit((value) => Promise.resolve(value.password.length));\n\nif (result.status === 'ok') console.log(result.value);\n```\n\nStarting another validation aborts the previous run. Field edits preserve existing errors until the next validation replaces them. Unexpected validator failures reject as `ForgeValidationError` with the original error as `cause`.\n\n## Observe State\n\nUse form subscriptions for aggregate metadata and field subscriptions for one branch. Subscribing after disposal throws `ForgeDisposedError`.\n\n```ts\nconst errors: unknown[] = [];\nconst observedForm = createForm({\n initialValues: { email: '' },\n onSubscriberError: (error) => errors.push(error),\n});\n\nconst stopForm = observedForm.subscribe((state) => {\n console.log(state.validity, state.submitting);\n}, { immediate: true });\nconst stopField = observedForm.field('email').subscribe((state) => {\n console.log(state.value, state.error);\n}, { immediate: true });\n\nstopField();\nstopForm();\n```\n\nWithout `onSubscriberError`, Forge rethrows subscriber failures asynchronously after completing its state transition.\n\n## Testing\n\nTest the form without a DOM. Read its immutable value, invoke a method, then assert the resulting state or validation result.\n\n```ts\nimport { expect, test } from 'vitest';\nimport { createForm } from '@vielzeug/forge';\n\ntest('requires an email address', async () => {\n const form = createForm({\n initialValues: { email: '' },\n validate: (value) => ({ fields: { email: value.email.includes('@') ? undefined : 'Invalid email' } }),\n });\n\n await expect(form.validate()).resolves.toEqual({\n errors: { email: 'Invalid email' },\n formError: undefined,\n status: 'invalid',\n });\n});\n```\n\n## Framework Integration\n\nUse `form.value` and subscriptions with any renderer. Bind one DOM input through `/dom`; validation scheduling remains application policy.\n\n::: code-group\n\n```ts [React]\nimport { useEffect, useState } from 'react';\nimport { createForm } from '@vielzeug/forge';\n\nconst form = createForm({ initialValues: { email: '' } });\n\nexport function EmailForm() {\n const [, rerender] = useState(0);\n\n useEffect(() => {\n const stop = form.subscribe(() => rerender((revision) => revision + 1));\n\n return () => stop();\n }, []);\n\n return <input value={form.field('email').value} onChange={(event) => form.field('email').set(event.target.value)} />;\n}\n```\n\n```ts [Vue 3]\nimport { onUnmounted, ref } from 'vue';\nimport { createForm } from '@vielzeug/forge';\n\nconst form = createForm({ initialValues: { email: '' } });\nconst revision = ref(0);\nconst stop = form.subscribe(() => revision.value++);\n\nonUnmounted(stop);\n```\n\n```ts [Svelte]\n<script lang=\"ts\">\n import { onDestroy } from 'svelte';\n import { createForm } from '@vielzeug/forge';\n\n const form = createForm({ initialValues: { email: '' } });\n let revision = 0;\n const stop = form.subscribe(() => revision++);\n\n onDestroy(stop);\n</script>\n\n<input value={form.field('email').value} on:input={(event) => form.field('email').set(event.currentTarget.value)} />\n```\n\n:::\n\n## Working with Other Vielzeug Libraries\n\nUse Spell when one schema owns validation and Vault when an explicit record codec owns persistence.\n\n```ts\nimport { createForm } from '@vielzeug/forge';\nimport { customValidator } from '@vielzeug/forge/spell';\nimport { s } from '@vielzeug/spell';\n\nconst Profile = s.object({ email: s.string().email() });\nconst form = createForm({ initialValues: { email: '' }, validate: customValidator(Profile) });\n```\n\n`customValidator()` preserves unrelated Spell errors, maps each union to its closest branch, and maps array-item failures to per-item array fields. Parse again at the submit boundary when a Spell transform must produce the outgoing payload.\n\n```ts\nimport { loadForm, saveForm } from '@vielzeug/forge/vault';\n\nawait saveForm(form, db, 'drafts', codec);\nconst restored = await loadForm(form, db, 'drafts', 'profile', codec);\nconsole.log(restored);\n```\n\n`loadForm()` uses `form.reset()`, so a restored value is clean. Store a selected `File`, not `FileList`, in form state; `FileList` is transport-only for `toFormData()`.\n\n## Best Practices\n\n- Keep form values to primitives, plain objects, arrays, `Date`, `File`, and `Blob`.\n- Update array fields through immutable replacement functions.\n- Validate complete values instead of rebuilding field-validator graphs.\n- Handle `aborted` validation results before rendering errors.\n- Preserve errors through field edits until a deliberate validation refresh.\n- Return subscription cleanup from framework lifecycle hooks.\n- Provide `onSubscriberError` when application subscribers can throw.\n- Decode Vault records before passing them to `loadForm()`.\n",
7
+ "examples": "---\ntitle: Forge — Examples\ndescription: Practical immutable form recipes.\n---\n\n## Examples\n\n- [Login form](./examples/login-form.md)\n- [Conditional values](./examples/form-with-conditional-fields.md)\n- [Dynamic arrays](./examples/dynamic-form-fields.md)\n- [Contact form with file upload](./examples/contact-form-with-file-upload.md)\n- [Registration form](./examples/registration-form.md)\n- [Multi-step wizard](./examples/multi-step-wizard.md)\n- [Search form with debounce](./examples/search-form-with-debounce.md)\n"
8
+ },
9
+ "examples": [
10
+ {
11
+ "id": "array-fields",
12
+ "code": "import { createForm } from '@vielzeug/forge'\n\nconst form = createForm({ initialValues: { tags: ['typescript'] } })\nconst tags = form.field('tags')\n\ntags.set((previous) => [...previous, 'forms'])\ntags.set((previous) => previous.filter((tag) => tag !== 'typescript'))\nconsole.log(form.value.tags)",
13
+ "name": "Immutable Array Updates"
14
+ },
15
+ {
16
+ "id": "create-form",
17
+ "code": "import { createForm } from '@vielzeug/forge'\n\nconst form = createForm({ initialValues: { account: { email: '' }, name: '' } })\nconst email = form.field('account').field('email')\n\nemail.set('ada@example.com')\nconsole.log(form.value)\nconsole.log(form.state.valid)",
18
+ "name": "Create Immutable Form"
19
+ },
20
+ {
21
+ "id": "dynamic-fields",
22
+ "code": "import { createForm } from '@vielzeug/forge'\n\nconst form = createForm({ initialValues: { contacts: [] as { email: string }[] } })\nconst contacts = form.field('contacts')\n\ncontacts.set((previous) => [...previous, { email: 'ada@example.com' }])\ncontacts.set((previous) => previous.slice(1))\nconsole.log(form.value)",
23
+ "name": "Dynamic Values"
24
+ },
25
+ {
26
+ "id": "field-binding",
27
+ "code": "import { createForm } from '@vielzeug/forge'\nimport { bindField } from '@vielzeug/forge/dom'\n\nconst form = createForm({\n initialValues: { email: '' },\n validate: (value) => ({ fields: { email: value.email.includes('@') ? undefined : 'Invalid email' } }),\n})\nconst email = form.field('email')\nconst input = document.createElement('input')\nconst stop = bindField(input, email, {\n read: (element) => element.value,\n write: (element, value) => { element.value = value },\n})\n\ninput.value = 'ada'\ninput.dispatchEvent(new Event('input'))\ninput.dispatchEvent(new Event('blur'))\nconsole.log(email.value, email.touched)\nconsole.log(await form.validate())\nstop()",
28
+ "name": "DOM Field Binding"
29
+ },
30
+ {
31
+ "id": "field-operations",
32
+ "code": "import { createForm } from '@vielzeug/forge'\n\nconst form = createForm({ initialValues: { profile: { name: 'Ada' } } })\nconst name = form.field('profile').field('name')\n\nname.set('Grace')\nname.touch()\nconsole.log(name.value, name.dirty, name.touched)\nname.reset()\nconsole.log(form.value)",
33
+ "name": "Focused Field Operations"
34
+ },
35
+ {
36
+ "id": "form-submission",
37
+ "code": "import { createForm } from '@vielzeug/forge'\n\nconst form = createForm({\n initialValues: { email: '' },\n validate: (value) => ({ fields: { email: value.email.includes('@') ? undefined : 'Invalid email' } }),\n})\n\nform.field('email').set('ada@example.com')\nconst result = await form.submit(async (value) => ({ ...value, saved: true }))\nconsole.log(result)",
38
+ "name": "Form Submission"
39
+ },
40
+ {
41
+ "id": "form-subscriptions",
42
+ "code": "import { createForm } from '@vielzeug/forge'\n\nconst form = createForm({\n initialValues: { email: '', name: '' },\n onSubscriberError: (error) => console.log('Subscriber error:', error),\n})\nconst stopForm = form.subscribe((state) => console.log('Valid:', state.valid), { immediate: true })\nconst stopEmail = form.field('email').subscribe((state) => console.log('Email:', state), { immediate: true })\n\nform.field('name').set('Ada')\nform.field('email').set('ada@example.com')\nstopEmail()\nstopForm()",
43
+ "name": "Form and Field Subscriptions"
44
+ },
45
+ {
46
+ "id": "form-validation",
47
+ "code": "import { createForm } from '@vielzeug/forge'\n\nconst form = createForm({\n initialValues: { password: '', passwordConfirmation: '' },\n validate: (value) => ({\n fields: {\n password: value.password.length >= 8 ? undefined : 'Use at least eight characters',\n passwordConfirmation: value.password === value.passwordConfirmation ? undefined : 'Passwords must match',\n },\n }),\n})\n\nform.field('password').set('short')\nconsole.log(await form.validate())\n\nform.field('password').set('strong-password')\nform.field('passwordConfirmation').set('strong-password')\nconsole.log(await form.validate())",
48
+ "name": "Whole-Value Validation"
49
+ },
50
+ {
51
+ "id": "schema-integration",
52
+ "code": "import { createForm } from '@vielzeug/forge'\nimport { customValidator } from '@vielzeug/forge/spell'\nimport { s } from '@vielzeug/spell'\n\nconst Profile = s.object({ email: s.string().email() })\nconst form = createForm({\n initialValues: { email: '' },\n validate: customValidator(Profile),\n})\n\nform.field('email').set('ada')\nconsole.log(await form.validate())\nconsole.log(form.field('email').error)",
53
+ "name": "Spell Schema Integration"
54
+ },
55
+ {
56
+ "id": "scoped-sub-forms",
57
+ "code": "import { createForm } from '@vielzeug/forge'\n\nconst form = createForm({ initialValues: { shipping: { city: '', street: '' } } })\nconst shipping = form.field('shipping')\n\nshipping.field('street').set('123 Main Street')\nshipping.field('city').set('Portland')\nconsole.log(shipping.value)\nconsole.log(form.value.shipping)",
58
+ "name": "Nested Field Handles"
59
+ }
60
+ ],
61
+ "typeSignatures": {
62
+ "ForgeConfigError": "export { ForgeConfigError, ForgeDisposedError, ForgeError, ForgeSubmitError, ForgeValidationError } from './errors';",
63
+ "ForgeDisposedError": "export { ForgeConfigError, ForgeDisposedError, ForgeError, ForgeSubmitError, ForgeValidationError } from './errors';",
64
+ "ForgeError": "export { ForgeConfigError, ForgeDisposedError, ForgeError, ForgeSubmitError, ForgeValidationError } from './errors';",
65
+ "ForgeSubmitError": "export { ForgeConfigError, ForgeDisposedError, ForgeError, ForgeSubmitError, ForgeValidationError } from './errors';",
66
+ "ForgeValidationError": "export { ForgeConfigError, ForgeDisposedError, ForgeError, ForgeSubmitError, ForgeValidationError } from './errors';",
67
+ "createForm": "export { createForm } from './form';",
68
+ "Unsubscribe": "export type Unsubscribe = () => void;",
69
+ "MaybePromise": "export type MaybePromise<T> = T | PromiseLike<T>;",
70
+ "ReadonlyDeep": "export type ReadonlyDeep<T> = T extends (...args: never[]) => unknown\n ? T\n : T extends readonly (infer Item)[]\n ? readonly ReadonlyDeep<Item>[]\n : T extends Record<string, unknown>\n ? { readonly [K in keyof T]: ReadonlyDeep<T[K]> }\n : T;",
71
+ "FormErrors": "export type FormErrors<T> = T extends readonly (infer Item)[]\n ? string | readonly (FormErrors<Item> | undefined)[]\n : T extends Record<string, unknown>\n ? string | { readonly [K in keyof T]?: FormErrors<T[K]> }\n : string;",
72
+ "ValidationErrors": "export type ValidationErrors<TValues extends Record<string, unknown>> = Readonly<{\n fields?: FormErrors<TValues>;\n formError?: string;\n}>;",
73
+ "FormValidator": "export type FormValidator<TValues extends Record<string, unknown>> = (\n values: ReadonlyDeep<TValues>,\n signal: AbortSignal,\n) => MaybePromise<ValidationErrors<TValues> | undefined>;",
74
+ "FormOptions": "export type FormOptions<TValues extends Record<string, unknown>> = Readonly<{\n initialValues: TValues;\n onSubscriberError?: (error: unknown) => void;\n validate?: FormValidator<NoInfer<TValues>>;\n}>;",
75
+ "SubscribeOptions": "export type SubscribeOptions = Readonly<{\n immediate?: boolean;\n}>;",
76
+ "FieldState": "export type FieldState<V> = Readonly<{\n dirty: boolean;\n error: string | undefined;\n touched: boolean;\n value: ReadonlyDeep<V>;\n}>;",
77
+ "FormState": "export type FormState<TValues extends Record<string, unknown>> = Readonly<{\n errors: FormErrors<TValues> | undefined;\n formError: string | undefined;\n hasErrors: boolean;\n submitCount: number;\n submitting: boolean;\n touched: boolean;\n validity: 'invalid' | 'unknown' | 'valid';\n validating: boolean;\n}>;",
78
+ "ValidationResult": "export type ValidationResult<TValues extends Record<string, unknown>> =\n | Readonly<{ status: 'aborted' }>\n | Readonly<{ status: 'valid' }>\n | Readonly<{ errors: FormErrors<TValues> | undefined; formError: string | undefined; status: 'invalid' }>;",
79
+ "SubmitResult": "export type SubmitResult<TResult = void, TValues extends Record<string, unknown> = Record<string, unknown>> =\n | Readonly<{ status: 'aborted' }>\n | Readonly<{ status: 'invalid'; errors: FormErrors<TValues> | undefined; formError: string | undefined }>\n | Readonly<{ status: 'ok'; value: TResult }>;",
80
+ "Field": "export type Field<V> = ChildField<V> & {\n readonly dirty: boolean;\n readonly error: string | undefined;\n reset(): void;\n set(next: V | ((previous: ReadonlyDeep<V>) => V)): void;\n readonly state: FieldState<V>;\n subscribe(listener: (state: FieldState<V>) => void, options?: SubscribeOptions): Unsubscribe;\n touch(): void;\n readonly touched: boolean;\n readonly value: ReadonlyDeep<V>;\n};",
81
+ "Form": "export type Form<TValues extends Record<string, unknown>> = {\n [Symbol.dispose](): void;\n readonly disposalSignal: AbortSignal;\n dispose(): void;\n readonly disposed: boolean;\n field<K extends keyof TValues & string>(key: K): Field<TValues[K]>;\n reset(next?: TValues): void;\n set(next: TValues | ((previous: ReadonlyDeep<TValues>) => TValues)): void;\n readonly state: FormState<TValues>;\n submit<TResult = void>(\n handler: (values: ReadonlyDeep<TValues>, signal: AbortSignal) => MaybePromise<TResult>,\n signal?: AbortSignal,\n ): Promise<SubmitResult<TResult, TValues>>;\n subscribe(listener: (state: FormState<TValues>) => void, options?: SubscribeOptions): Unsubscribe;\n validate(signal?: AbortSignal): Promise<ValidationResult<TValues>>;\n readonly value: ReadonlyDeep<TValues>;\n};"
82
+ }
83
+ }
@@ -0,0 +1,25 @@
1
+ {
2
+ "apiSource": "export type {\n PanAxis,\n PanEndReason,\n PanGesture,\n PanGestureDetail,\n PanGestureEndDetail,\n PanGestureOptions,\n} from './pan-gesture';\nexport { createPanGesture } from './pan-gesture';\n",
3
+ "docs": {
4
+ "index": "---\ntitle: Gesture — Pointer pan primitives\ndescription: Framework-neutral one-axis pointer pan recognition with lifecycle-owned handles.\npackage: gesture\ncategory: input\nkeywords: [pointer, pan, swipe, gesture, touch, drag]\nexports: [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 movement tracking without coupling input recognition to rendering or product-specific thresholds.\n\n```ts\n// Before\nelement.addEventListener('pointermove', (event) => {\n // Coordinate tracking, pointer identity, direction locking, and cleanup\n});\n\n// After\nconst pan = createPanGesture(element, {\n axis: 'x',\n onMove: ({ distance }) => render(distance),\n onEnd: ({ distance, reason }) => finish(distance, reason),\n});\n```\n\n| Feature | Ad-hoc pointer handling | Gesture |\n| --- | --- | --- |\n| Bundle size | n/a | <PackageInfo package=\"gesture\" type=\"size\" /> |\n| Zero dependencies | n/a | <ore-icon name=\"check\" size=\"16\"></ore-icon> |\n| Axis intent recognition | Manual | Built in |\n| Pointer ownership | Manual | Tracked across the document |\n| Lifecycle cleanup | Manual | `dispose()` + `disposalSignal` |\n\n<div class=\"decision-callout\">\n\n**Use Gesture when** several UI surfaces need consistent one-axis pointer tracking while retaining their own 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 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\n if (reason === 'release' && Math.abs(distance) >= 48) {\n dismiss();\n }\n },\n});\n```\n\n## Features\n\n<div class=\"features-grid\">\n\n- `createPanGesture()` — one-axis pointer movement tracking\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- Consumer-owned policy — thresholds, snapping, and outcomes stay in application code\n- Stable completion — one `onEnd` callback for release and cancellation\n- Lifecycle ownership — `dispose()`, `disposed`, and `disposalSignal`\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\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/) — drag-and-drop behavior with drop targets and reordering.\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 @vielzeug/gesture pointer pan recognition.\n---\n\n[[toc]]\n\n## API Overview\n\n| Symbol | Purpose | Execution mode | Common gotcha |\n| --- | --- | --- | --- |\n| `createPanGesture()` | Track one-axis pointer movement on an element | Sync | `onStart` runs after direction intent is recognized |\n| `PanGesture` | Lifecycle-owned pan handle | Sync | `dispose()` does not emit `onEnd` |\n| `PanGestureOptions` | Configure axis, admission, capture, and callbacks | Sync | Completion thresholds belong in `onEnd` |\n\n## Package Entry Point\n\n| Import | Purpose |\n| --- | --- |\n| `@vielzeug/gesture` | Pan recognizer and related types. |\n\n## Core Functions\n\n### `createPanGesture()`\n\n```ts\nfunction createPanGesture(target: Element, options?: PanGestureOptions): PanGesture;\n```\n\nAttaches a one-axis pointer pan recognizer to `target`.\n\n| Parameter | Type | Description |\n| --- | --- | --- |\n| `target` | `Element` | Element that owns the pointer interaction. |\n| `options` | `PanGestureOptions` | Axis, disabled state, admission guard, capture policy, and lifecycle callbacks. |\n\n**Returns:** A `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\n| Member | Return | Contract |\n| --- | --- | --- |\n| `active` | `boolean` | `true` after direction intent is accepted and before the interaction ends. |\n| `cancel()` | `boolean` | Cancels the pending or active pointer interaction. Active pans emit `onEnd` with `reason: 'cancel'`. |\n| `dispose()` | `void` | Detaches listeners, releases pointer ownership, and aborts `disposalSignal`. Idempotent. |\n| `disposed` | `boolean` | `true` after the first `dispose()`. |\n| `disposalSignal` | `AbortSignal` | Aborts when the handle is disposed. |\n| `[Symbol.dispose]()` | `void` | Calls `dispose()`. |\n\n## Types\n\n```ts\ntype PanAxis = 'x' | 'y';\ntype PanEndReason = 'cancel' | 'release';\n\ntype PanGestureDetail = {\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 reason: PanEndReason;\n};\n\ntype PanGestureOptions = {\n axis?: PanAxis | (() => PanAxis);\n disabled?: boolean | (() => boolean | undefined);\n pointerCapture?: boolean;\n onEnd?: (detail: PanGestureEndDetail) => void;\n onMove?: (detail: PanGestureDetail) => void;\n onStart?: (detail: PanGestureDetail) => void;\n shouldStart?: (event: PointerEvent) => boolean;\n};\n\ntype PanGesture = {\n readonly active: boolean;\n [Symbol.dispose](): void;\n cancel(): boolean;\n readonly disposalSignal: AbortSignal;\n dispose(): void;\n readonly disposed: boolean;\n};\n```\n\n| Option | Type | Default | Contract |\n| --- | --- | --- | --- |\n| `axis` | `PanAxis \\| (() => PanAxis)` | `'x'` | Axis resolved when each pointer interaction starts |\n| `disabled` | `boolean \\| (() => boolean \\| undefined)` | `false` | Blocks new pans and cancels an active pan on the next pointer event |\n| `pointerCapture` | `boolean` | `true` | Captures the pointer on `target` after axis intent is accepted |\n| `shouldStart` | `(event: PointerEvent) => boolean` | — | Rejects a primary pointer start before tracking begins |\n| `onStart` | `(detail: PanGestureDetail) => void` | — | Runs once when axis intent is accepted |\n| `onMove` | `(detail: PanGestureDetail) => void` | — | Runs for the activating move and later moves |\n| `onEnd` | `(detail: PanGestureEndDetail) => void` | — | Runs for active release or cancellation |\n\nGesture tracks an accepted pan with capture-phase listeners on `target.ownerDocument` regardless of the pointer-capture setting. Set `pointerCapture: false` when nested or newly revealed controls must retain native pointer-up and click targeting.\n\n## Errors\n\n`@vielzeug/gesture` does not export custom error classes.\n",
6
+ "usage": "---\ntitle: Gesture — Usage Guide\ndescription: Track one-axis pointer movement and apply application-specific completion rules.\n---\n\n[[toc]]\n\n## Basic Usage\n\nCreate one pan handle for the element that owns the interaction.\n\n```ts\nimport { createPanGesture } from '@vielzeug/gesture';\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\n if (reason === 'release' && Math.abs(distance) >= 64) archive();\n },\n});\n```\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\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 captures the pointer on the bound target while continuing to track movement through document-level listeners. This is the reliable default for ordinary drag surfaces.\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.\n\n## Lifecycle\n\nDispose the target-bound handle when its owning UI scope unmounts.\n\n```ts\nconst pan = createPanGesture(element, { onEnd, onMove });\n\nonCleanup(() => pan.dispose());\n```\n\nUse `cancel()` to stop a pending or active interaction without disposing the handle. An active interaction emits `onEnd` with `reason: 'cancel'`.\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 tracks a constrained pointer pan. Dnd owns draggable items, sortable lists, and drop targets. Keep them separate.\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** thresholds and direction rules in `onEnd`.\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
+ "examples": "---\ntitle: Gesture — Examples\ndescription: Worked examples for @vielzeug/gesture.\n---\n\n## Examples\n\n- [Carousel Swipe Navigation](./examples/carousel-swipe-navigation.md)\n- [Swipe-to-Dismiss Notifications](./examples/swipe-dismiss-notifications.md)\n"
8
+ },
9
+ "examples": [
10
+ {
11
+ "id": "pan-basic",
12
+ "code": "import { createPanGesture } from '@vielzeug/gesture'\n\nconst surface = document.createElement('div')\nsurface.textContent = 'Drag horizontally'\nsurface.style.cssText = 'width:240px;padding:32px;text-align:center;background:#e0e7ff;border-radius:12px;touch-action:pan-y;user-select:none;'\ndocument.body.appendChild(surface)\n\nconst output = document.createElement('pre')\ndocument.body.appendChild(output)\n\nconst pan = createPanGesture(surface, {\n axis: 'x',\n onMove: ({ distance }) => {\n surface.style.transform = `translateX(${distance}px)`\n output.textContent = `distance: ${Math.round(distance)}px`\n },\n onEnd: ({ distance, reason }) => {\n surface.style.transform = ''\n output.textContent = reason === 'release' && Math.abs(distance) >= 48\n ? `swipe: ${distance < 0 ? 'left' : 'right'}`\n : `ended: ${reason}`\n },\n})\n\nconsole.log('Pan gesture ready:', pan.disposed === false)",
13
+ "name": "createPanGesture - Basic"
14
+ }
15
+ ],
16
+ "typeSignatures": {
17
+ "PanAxis": "export type {\n PanAxis,\n PanEndReason,\n PanGesture,\n PanGestureDetail,\n PanGestureEndDetail,\n PanGestureOptions,\n} from './pan-gesture';",
18
+ "PanEndReason": "export type {\n PanAxis,\n PanEndReason,\n PanGesture,\n PanGestureDetail,\n PanGestureEndDetail,\n PanGestureOptions,\n} from './pan-gesture';",
19
+ "PanGesture": "export type {\n PanAxis,\n PanEndReason,\n PanGesture,\n PanGestureDetail,\n PanGestureEndDetail,\n PanGestureOptions,\n} from './pan-gesture';",
20
+ "PanGestureDetail": "export type {\n PanAxis,\n PanEndReason,\n PanGesture,\n PanGestureDetail,\n PanGestureEndDetail,\n PanGestureOptions,\n} from './pan-gesture';",
21
+ "PanGestureEndDetail": "export type {\n PanAxis,\n PanEndReason,\n PanGesture,\n PanGestureDetail,\n PanGestureEndDetail,\n PanGestureOptions,\n} from './pan-gesture';",
22
+ "PanGestureOptions": "export type {\n PanAxis,\n PanEndReason,\n PanGesture,\n PanGestureDetail,\n PanGestureEndDetail,\n PanGestureOptions,\n} from './pan-gesture';",
23
+ "createPanGesture": "export { createPanGesture } from './pan-gesture';"
24
+ }
25
+ }