@vielzeug/codex 2.3.1 → 2.3.2
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.
- package/package.json +1 -1
- package/data/catalog.json +0 -1886
- package/data/llms-full.txt +0 -32016
- package/data/llms.txt +0 -45
- package/data/manifest.json +0 -8
- package/data/packages/arsenal.json +0 -210
- package/data/packages/assay.json +0 -39
- package/data/packages/clockwork.json +0 -67
- package/data/packages/codex.json +0 -43
- package/data/packages/coins.json +0 -102
- package/data/packages/conduit.json +0 -60
- package/data/packages/courier.json +0 -59
- package/data/packages/dnd.json +0 -77
- package/data/packages/familiar.json +0 -40
- package/data/packages/flux.json +0 -93
- package/data/packages/focus.json +0 -37
- package/data/packages/forge.json +0 -83
- package/data/packages/gesture.json +0 -25
- package/data/packages/herald.json +0 -108
- package/data/packages/illusionist.json +0 -132
- package/data/packages/keymap.json +0 -60
- package/data/packages/ledger.json +0 -57
- package/data/packages/lingua.json +0 -68
- package/data/packages/necromancer.json +0 -50
- package/data/packages/orbit.json +0 -99
- package/data/packages/ore.json +0 -68
- package/data/packages/postmaster.json +0 -51
- package/data/packages/prism.json +0 -66
- package/data/packages/pulse.json +0 -70
- package/data/packages/refine.json +0 -12
- package/data/packages/ripple.json +0 -83
- package/data/packages/rune.json +0 -79
- package/data/packages/sandbox.json +0 -40
- package/data/packages/scout.json +0 -61
- package/data/packages/scroll.json +0 -109
- package/data/packages/sentinel.json +0 -35
- package/data/packages/sourcerer.json +0 -73
- package/data/packages/spell.json +0 -133
- package/data/packages/tempo.json +0 -81
- package/data/packages/vault.json +0 -79
- package/data/packages/ward.json +0 -114
- package/data/packages/wayfinder.json +0 -110
- package/data/refine.json +0 -11847
- package/data/search.json +0 -1582
|
@@ -1,108 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"apiSource": "export { combineSignals, createBus } from './bus';\nexport { BusDisposedError, HeraldConfigError, HeraldError } from './errors';\nexport { pipeEvents } from './pipe';\nexport type {\n Bus,\n BusOptions,\n EmissionErrorContext,\n EventKey,\n EventMap,\n EventStream,\n HeraldEvent,\n Listener,\n Middleware,\n PipeableKey,\n PipeEntry,\n SubscribeOptions,\n Unsubscribe,\n WaitAnyResult,\n} from './types';\n",
|
|
3
|
-
"docs": {
|
|
4
|
-
"index": "---\ntitle: Herald — Typed event bus for TypeScript\ndescription: Typed temporal event delivery with sync subscriptions, async waiting, streams, pipes, and AbortSignal lifecycle.\npackage: herald\ncategory: events\nkeywords: [event-bus, typed-events, pub-sub, async-streams, abort-signal]\nrelated: [ripple, wayfinder, familiar]\nexports: [createBus, pipeEvents, combineSignals, HeraldError, BusDisposedError, HeraldConfigError]\nenvironments: [browser, node, ssr, deno]\n---\n\n<!-- markdownlint-disable MD025 MD033 MD060 -->\n\n<PackageHero package=\"herald\" />\n\n## Why Herald?\n\nRaw event emitters lose payload inference and leave waiting, streaming, cancellation, and teardown to every caller. Herald keeps events temporal: use [Ripple](/ripple/) when you need retained state.\n\n```ts\n// Before\nconst listeners = new Set<(payload: unknown) => void>();\nlisteners.add((payload) => loadProfile((payload as { id: string }).id));\n\n// After\nimport { createBus } from '@vielzeug/herald';\n\ninterface AppEvents {\n 'user:login': { id: string };\n}\n\nfunction loadProfile(id: string): void {\n console.log(id);\n}\n\nconst bus = createBus<AppEvents>();\nbus.on('user:login', ({ id }) => loadProfile(id));\n```\n\n| Feature | Herald | mitt | EventEmitter3 |\n| --- | --- | --- | --- |\n| Bundle size | <PackageInfo package=\"herald\" type=\"size\" /> | ~200 B | ~1.5 kB |\n| Typed payloads | <ore-icon name=\"check\" size=\"16\"></ore-icon> | <ore-icon name=\"triangle-alert\" size=\"16\"></ore-icon> | <ore-icon name=\"triangle-alert\" size=\"16\"></ore-icon> |\n| Async wait and streams | <ore-icon name=\"check\" size=\"16\"></ore-icon> | <ore-icon name=\"x\" size=\"16\"></ore-icon> | <ore-icon name=\"x\" size=\"16\"></ore-icon> |\n| AbortSignal lifecycle | <ore-icon name=\"check\" size=\"16\"></ore-icon> | <ore-icon name=\"x\" size=\"16\"></ore-icon> | <ore-icon name=\"x\" size=\"16\"></ore-icon> |\n| Typed event pipes | <ore-icon name=\"check\" size=\"16\"></ore-icon> | <ore-icon name=\"x\" size=\"16\"></ore-icon> | <ore-icon name=\"x\" size=\"16\"></ore-icon> |\n| Zero dependencies | <ore-icon name=\"check\" size=\"16\"></ore-icon> | <ore-icon name=\"check\" size=\"16\"></ore-icon> | <ore-icon name=\"check\" size=\"16\"></ore-icon> |\n\n<div class=\"decision-callout\">\n\n**Use Herald when** modules need typed temporal event delivery with owned lifecycle.\n\n**Consider Ripple when** consumers need current state and replayed values.\n\n</div>\n\n## Installation\n\n::: code-group\n\n```sh [pnpm]\npnpm add @vielzeug/herald\n```\n\n```sh [npm]\nnpm install @vielzeug/herald\n```\n\n```sh [yarn]\nyarn add @vielzeug/herald\n```\n\n:::\n\n## Quick Start\n\n```ts\nimport { createBus } from '@vielzeug/herald';\n\ninterface AppEvents {\n 'user:login': { id: string };\n 'user:logout': void;\n}\n\nconst bus = createBus<AppEvents>();\nconst stop = bus.on('user:login', ({ id }) => console.log(id));\n\nbus.emit('user:login', { id: '42' });\nstop();\nbus.dispose();\n```\n\n## Features\n\n<div class=\"features-grid\">\n\n- `on()` / `once()` — typed subscriptions with explicit teardown\n- `onAny()` — cross-cutting event observation\n- `tap()` — observe bus activity for logging and diagnostics\n- `wait()` / `waitAny()` — one-shot async coordination\n- `events()` — bounded async event streams\n- `pipeEvents()` — compatible cross-bus forwarding\n- `AbortSignal` — cancellation and disposal ownership\n- `createTestBus()` — emitted-payload recording for tests\n\n</div>\n\n## Documentation\n\n<div class=\"doc-links\">\n\n- [Usage Guide](./usage.md)\n- [API Reference](./api.md)\n- [Examples](./examples.md)\n- [Migration Guide](./migration.md)\n\n</div>\n\n## See Also\n\n<div class=\"see-also\">\n\n- [Ripple](/ripple/) — retained reactive state.\n- [Wayfinder](/wayfinder/) — route lifecycle events.\n- [Familiar](/familiar/) — worker completion events.\n\n</div>\n\n<!-- markdownlint-enable MD025 MD033 MD060 -->\n",
|
|
5
|
-
"api": "---\ntitle: Herald — API Reference\ndescription: Reference for typed temporal event delivery, lifecycle ownership, and compatible event piping.\n---\n\n[[toc]]\n\n## API Overview\n\n| Symbol | Purpose | Execution mode | Common gotcha |\n| --- | --- | --- | --- |\n| `createBus()` | Create typed temporal event bus | Sync | `emit()` and middleware are synchronous |\n| `pipeEvents()` | Forward compatible source events | Sync | Payloads must be assignable to target event |\n| `combineSignals()` | Abort when any input aborts | Sync | Public composition has no manual teardown |\n| `createTestBus()` | Record dispatched test events | Sync | Available from `/testing` only |\n\n## Package Entry Point\n\n| Import | Purpose |\n| --- | --- |\n| `@vielzeug/herald` | Runtime bus, pipes, public types, and errors |\n| `@vielzeug/herald/testing` | `createTestBus()` and `TestBus` |\n\n## Core Functions\n\n### `createBus()`\n\n```ts\nfunction createBus<T extends EventMap = Record<string, unknown>>(\n options?: BusOptions<T>,\n): Bus<T>;\n```\n\nCreates a synchronous bus for future event delivery.\n\n| Parameter | Type | Description |\n| --- | --- | --- |\n| `options` | `BusOptions<T>` | Optional middleware, validation, error handling, and listener threshold configuration. |\n\n**Returns:** `Bus<T>`.\n\n```ts\nimport { createBus } from '@vielzeug/herald';\n\ninterface Events {\n count: number;\n ready: void;\n}\n\nconst bus = createBus<Events>();\nbus.emit('count', 1);\nbus.emit('ready');\nbus.dispose();\n```\n\n---\n\n### `pipeEvents()`\n\n```ts\nfunction pipeEvents<S extends EventMap, T extends EventMap>(\n source: Bus<S>,\n target: Bus<T>,\n entries: readonly [NoInfer<PipeEntry<S, T>>, ...NoInfer<PipeEntry<S, T>>[]],\n opts?: { signal?: AbortSignal },\n): Unsubscribe;\n```\n\nForwards listed compatible events until manually stopped, either bus disposes, or `options.signal` aborts.\n\n| Parameter | Type | Description |\n| --- | --- | --- |\n| `source` | `Bus<S>` | Bus that emits source events. |\n| `target` | `Bus<T>` | Bus that receives compatible events. |\n| `entries` | non-empty `PipeEntry` tuple | Same-name keys or compatible `{ from, to }` mappings. |\n| `opts.signal` | `AbortSignal` | Optional pipe lifetime signal. |\n\n**Returns:** Idempotent `Unsubscribe` function.\n\n```ts\nimport { createBus, pipeEvents } from '@vielzeug/herald';\n\ninterface SourceEvents {\n 'auth:login': { id: string };\n}\n\ninterface TargetEvents {\n 'user:authenticated': { id: string };\n}\n\nconst source = createBus<SourceEvents>();\nconst target = createBus<TargetEvents>();\nconst stop = pipeEvents(source, target, [{ from: 'auth:login', to: 'user:authenticated' }]);\n\nstop();\nsource.dispose();\ntarget.dispose();\n```\n\n---\n\n### `combineSignals()`\n\n```ts\nfunction combineSignals(first: AbortSignal, ...rest: AbortSignal[]): AbortSignal;\n```\n\nReturns a signal aborted with first input signal's reason.\n\n**Returns:** `AbortSignal`.\n\n```ts\nimport { combineSignals } from '@vielzeug/herald';\n\nconst signal = combineSignals(AbortSignal.timeout(1_000), controller.signal);\n```\n\nInput listeners remain until an input aborts. Bus APIs that accept `{ signal }` clean their internal signal composition when their owned operation ends.\n\n## Types\n\n### `EventMap` and `EventKey`\n\n```ts\ntype EventMap = object;\ntype EventKey<T extends EventMap> = Extract<keyof T, string>;\n```\n\n`EventMap` accepts interfaces and type aliases. Only string keys are event names.\n\n---\n\n### `BusOptions`\n\n```ts\ntype BusOptions<T extends EventMap = EventMap> = {\n maxListeners?: number;\n middleware?: readonly Middleware<T>[];\n name?: string;\n onError?: (context: EmissionErrorContext<T>) => void;\n validatePayload?: <K extends EventKey<T>>(event: K, payload: T[K]) => void;\n};\n```\n\n| Field | Description |\n| --- | --- |\n| `maxListeners` | Warn when one event exceeds this active-listener count. |\n| `middleware` | Synchronous dispatch middleware. |\n| `name` | Display name in disposal errors. |\n| `onError` | Handles listener and validation errors instead of rethrowing. |\n| `validatePayload` | Runs before middleware and listeners. |\n\n---\n\n### `Bus`\n\n```ts\ntype Bus<T extends EventMap> = {\n [Symbol.dispose](): void;\n readonly disposalSignal: AbortSignal;\n dispose(): void;\n readonly disposed: boolean;\n emit<K extends EventKey<T>>(event: K, ...args: T[K] extends void ? [] : [payload: T[K]]): number;\n eventNames(): EventKey<T>[];\n events<K extends EventKey<T>>(event: K, opts?: { maxBuffer?: number; signal?: AbortSignal }): EventStream<T[K]>;\n listenerCount(event?: EventKey<T>): number;\n on<K extends EventKey<T>>(event: K, listener: Listener<T[K]>, opts?: SubscribeOptions): Unsubscribe;\n onAny(listener: (event: EventKey<T>, payload: unknown) => void, opts?: SubscribeOptions): Unsubscribe;\n once<K extends EventKey<T>>(event: K, listener: Listener<T[K]>, opts?: { signal?: AbortSignal }): Unsubscribe;\n tap(handler: (event: HeraldEvent<T>) => void, options?: { signal?: AbortSignal }): Unsubscribe;\n wait<K extends EventKey<T>>(event: K, opts?: { signal?: AbortSignal }): Promise<T[K]>;\n waitAny<const K extends readonly [EventKey<T>, EventKey<T>, ...EventKey<T>[]]>(\n events: K,\n opts?: { signal?: AbortSignal },\n ): Promise<WaitAnyResult<T, K>>;\n wildcardCount(): number;\n};\n```\n\n`emit()` returns listener count or `0` after disposal, blocked middleware, or handled validation rejection.\n\n`tap()` receives every `emit`, `subscribe`, `unsubscribe`, `listener-error`, and `dispose` event as a `HeraldEvent`. It is the supported way to observe bus activity for logging and diagnostics. The returned `Unsubscribe` stops the tap; pass `{ signal }` to bind its lifetime to an `AbortSignal`.\n\n```ts\nimport { createBus } from '@vielzeug/herald';\n\nconst bus = createBus<AppEvents>();\nconst stop = bus.tap((event) => console.debug(`herald:${event.type}`, event));\n```\n\n---\n\n### `Listener`, `SubscribeOptions`, and `Unsubscribe`\n\n```ts\ntype Listener<T> = (payload: T) => void;\ntype SubscribeOptions = { once?: boolean; signal?: AbortSignal };\ntype Unsubscribe = () => void;\n```\n\n---\n\n### `HeraldEvent`\n\n```ts\ntype HeraldEvent<T extends EventMap = EventMap> =\n | { type: 'emit'; event: EventKey<T>; payload: unknown; timestamp: number }\n | { type: 'subscribe'; event: EventKey<T>; timestamp: number }\n | { type: 'unsubscribe'; event: EventKey<T>; timestamp: number }\n | { type: 'listener-error'; event: EventKey<T>; err: unknown; timestamp: number }\n | { type: 'dispose'; timestamp: number };\n```\n\nDiscriminated union delivered to `tap()` handlers. Narrow on `event.type` to access type-specific fields.\n\n---\n\n### `EmissionErrorContext` and `Middleware`\n\n```ts\ntype EmissionErrorContext<T extends EventMap = EventMap> = {\n err: unknown;\n event: EventKey<T>;\n payload: unknown;\n timestamp: number;\n};\n\ntype Middleware<T extends EventMap = EventMap> = (\n event: EventKey<T>,\n payload: unknown,\n next: () => void,\n) => void;\n```\n\nCall middleware `next()` synchronously at most once. Omit it to block dispatch.\n\n---\n\n### `EventStream` and `WaitAnyResult`\n\n```ts\ntype EventStream<T> = AsyncGenerator<T> & AsyncDisposable;\n\ntype WaitAnyResult<T extends EventMap, K extends readonly EventKey<T>[]> = {\n [I in keyof K]: K[I] extends EventKey<T> ? { event: K[I]; payload: T[K[I]] } : never;\n}[number];\n```\n\n---\n\n### `PipeableKey`, `RenamedPipeEntry`, and `PipeEntry`\n\n```ts\ntype PipeableKey<S extends EventMap, T extends EventMap> = {\n [K in EventKey<S> & EventKey<T>]: S[K] extends T[K] ? K : never;\n}[EventKey<S> & EventKey<T>];\n\ntype RenamedPipeEntry<S extends EventMap, T extends EventMap> = {\n [From in EventKey<S>]: {\n [To in EventKey<T>]: S[From] extends T[To] ? { from: From; to: To } : never;\n }[EventKey<T>];\n}[EventKey<S>];\n\ntype PipeEntry<S extends EventMap, T extends EventMap> =\n | PipeableKey<S, T>\n | RenamedPipeEntry<S, T>;\n```\n\n## Testing\n\n### `createTestBus()`\n\n```ts\nfunction createTestBus<T extends EventMap = Record<string, unknown>>(\n options?: BusOptions<T>,\n): TestBus<T>;\n```\n\nCreates a bus that records dispatched payloads.\n\n**Returns:** `TestBus<T>`.\n\n### `TestBus`\n\n```ts\ntype TestBus<T extends EventMap> = Bus<T> & {\n allEmitted(): { [K in EventKey<T>]?: T[K][] };\n emitted<K extends EventKey<T>>(event: K): T[K][];\n emittedCount<K extends EventKey<T>>(event: K): number;\n reset(): void;\n};\n```\n\n## Errors\n\n| Error | Trigger | Notable properties |\n| --- | --- | --- |\n| `BusDisposedError` | `wait()` or `waitAny()` interrupted by disposal | Bus name appears when configured. |\n| `HeraldConfigError` | Invalid stream buffer, empty pipe entries, or fewer than two `waitAny()` events | — |\n| `HeraldError` | Base class for Herald-originated errors | `instanceof HeraldError` narrows subclasses. |\n",
|
|
6
|
-
"usage": "---\ntitle: Herald — Usage Guide\ndescription: Typed event maps, lifecycle-owned subscriptions, waits, streams, pipes, and testing.\n---\n\n[[toc]]\n\n## Basic Usage\n\nUse interface or type alias event maps. Events model facts that happened; use Ripple for current state.\n\n```ts\nimport { createBus } from '@vielzeug/herald';\n\ninterface AppEvents {\n 'cart:updated': { count: number };\n 'user:logout': void;\n}\n\nconst bus = createBus<AppEvents>();\nconst stop = bus.on('cart:updated', ({ count }) => console.log(count));\n\nbus.emit('cart:updated', { count: 1 });\nstop();\nbus.dispose();\n```\n\n## Subscriptions\n\nUse `once()` for one event and `{ signal }` for owned subscription lifetime.\n\n```ts\nconst controller = new AbortController();\n\nbus.on('cart:updated', renderCart, { signal: controller.signal });\nbus.once('user:logout', clearSession);\ncontroller.abort();\n```\n\n## Middleware and Validation\n\nMiddleware is synchronous. Call `next()` once to continue; omit it to block dispatch.\n\n```ts\nconst bus = createBus<AppEvents>({\n middleware: [\n (event, payload, next) => {\n audit(event, payload);\n next();\n },\n ],\n validatePayload: (event, payload) => {\n if (event === 'cart:updated' && payload.count < 0) throw new RangeError('count must be non-negative');\n },\n});\n```\n\n## Awaiting Events\n\n```ts\nconst cart = await bus.wait('cart:updated', { signal: AbortSignal.timeout(5_000) });\nconst winner = await bus.waitAny(['cart:updated', 'user:logout'], { signal: AbortSignal.timeout(5_000) });\n```\n\n## Streaming Events\n\n`events()` subscribes eagerly. Bound buffers for producers faster than consumers.\n\n```ts\nawait using stream = bus.events('cart:updated', { maxBuffer: 100 });\n\nfor await (const cart of stream) {\n renderCart(cart);\n}\n```\n\n## Piping Events\n\n`pipeEvents()` only accepts compatible payloads. Stop explicitly or tie pipe to signal.\n\n```ts\nconst stopPipe = pipeEvents(sourceBus, auditBus, ['cart:updated'], { signal: pageSignal });\nstopPipe();\n```\n\n## Testing\n\n`createTestBus()` records dispatched payloads without mocks.\n\n```ts\nimport { createTestBus } from '@vielzeug/herald/testing';\n\nconst bus = createTestBus<AppEvents>();\nbus.emit('cart:updated', { count: 2 });\nexpect(bus.emitted('cart:updated')).toEqual([{ count: 2 }]);\nbus.dispose();\n```\n\n## Debugging\n\n`tap()` observes every bus activity as a `HeraldEvent` — use it for logging and diagnostics.\n\n```ts\nimport { createBus } from '@vielzeug/herald';\n\nconst bus = createBus<AppEvents>();\nbus.tap((event) => console.debug(`herald:${event.type}`, event));\n```\n\nIntegrate with the Rune logger:\n\n```ts\nimport { createLogger } from '@vielzeug/rune';\n\nconst log = createLogger({ name: 'herald' });\nbus.tap((event) => log.debug(event, `herald:${event.type}`));\n```\n\n## Working with Other Vielzeug Libraries\n\nUse Herald for temporal events. Use Ripple for retained reactive state. Use Familiar or Courier completion handlers to emit application events.\n\n## Best Practices\n\n- Define one explicit event map per boundary.\n- Emit facts, not mutable application state.\n- Keep middleware synchronous and call `next()` once.\n- Pass AbortSignals for component/request scoped work.\n- Set `maxBuffer` for long-lived streams.\n- Use `wait()` only for one-off coordination.\n- Use unsubscribe handles instead of global listener removal.\n- Dispose owner-scoped buses.\n",
|
|
7
|
-
"examples": "---\ntitle: Herald — Examples\ndescription: Practical examples and recipes for herald.\n---\n\n## Examples\n\n- [Standalone Entry](./examples/standalone-entry.md)\n- [Module Level Bus](./examples/module-level-bus.md)\n- [Awaiting A One Time Event](./examples/awaiting-a-one-time-event.md)\n- [Inspecting Listener Counts](./examples/inspecting-listener-counts.md)\n- [Custom Error Boundary](./examples/custom-error-boundary.md)\n- [Handling Disposal In Async Code](./examples/handling-disposal-in-async-code.md)\n- [Request Scoping](./examples/request-scoping.md)\n- [Streaming With Events](./examples/streaming-with-events.md)\n- [Bus Bridging With pipeEvents](./examples/bus-bridging-with-pipeevents.md)\n- [Testing With Createtestbus](./examples/testing-with-createtestbus.md)\n"
|
|
8
|
-
},
|
|
9
|
-
"examples": [
|
|
10
|
-
{
|
|
11
|
-
"id": "abort-signal",
|
|
12
|
-
"code": "import { createBus, BusDisposedError } from '@vielzeug/herald'\n\n// Demonstrate AbortSignal auto-unsubscribe and BusDisposedError\nconst bus = createBus()\n\nconst controller = new AbortController()\nconst { signal } = controller\n\nbus.on('message', (msg) => {\n console.log('listener received:', msg)\n}, { signal })\n\nbus.emit('message', 'first') // fires\nbus.emit('message', 'second') // fires\n\ncontroller.abort() // removes the listener\n\nbus.emit('message', 'third') // ignored — no listeners\nconsole.log('listeners after abort:', bus.listenerCount())\n\n// BusDisposedError: pending wait() rejects when bus is disposed\nconst bus2 = createBus()\n\nvoid bus2.wait('done').catch((err) => {\n if (err instanceof BusDisposedError) {\n console.log('BusDisposedError caught:', err.message)\n }\n})\n\nbus2.dispose()",
|
|
13
|
-
"name": "AbortSignal & BusDisposedError"
|
|
14
|
-
},
|
|
15
|
-
{
|
|
16
|
-
"id": "async-generator",
|
|
17
|
-
"code": "import { createBus } from '@vielzeug/herald'\n\n// events() returns an async generator that yields each emitted value in order\nconst bus = createBus()\n\nasync function consumeTicks() {\n let received = 0\n for await (const tick of bus.events('tick', { maxBuffer: 2 })) {\n console.log('tick:', tick)\n received++\n if (received >= 3) break\n }\n console.log('done after', received, 'ticks')\n}\n\nvoid consumeTicks()\n\nlet count = 0\nconst interval = setInterval(() => {\n bus.emit('tick', ++count)\n if (count >= 5) {\n clearInterval(interval)\n bus.dispose()\n }\n}, 30)",
|
|
18
|
-
"name": "events() - Async Generator"
|
|
19
|
-
},
|
|
20
|
-
{
|
|
21
|
-
"id": "async-wait",
|
|
22
|
-
"code": "import { createBus } from '@vielzeug/herald'\n\n// Await a single event with wait(), race multiple with waitAny()\nconst bus = createBus()\n\n// Emit after a short delay\nsetTimeout(() => bus.emit('user:login', { userId: '42', email: 'alice@example.com' }), 50)\nsetTimeout(() => bus.emit('theme:change', 'dark'), 80)\n\n// wait() resolves on the next emit of that event\nconst loginPayload = await bus.wait('user:login')\nconsole.log('got login:', loginPayload.userId)\n\n// Reset and race two events — whichever fires first wins\nsetTimeout(() => bus.emit('user:login', { userId: '1', email: 'a@b.com' }), 20)\nsetTimeout(() => bus.emit('theme:change', 'light'), 60)\n\nconst result = await bus.waitAny(['user:login', 'theme:change'])\n\nif (result.event === 'user:login') {\n console.log('login won the race:', result.payload.userId)\n} else {\n console.log('theme won the race:', result.payload)\n}\n\nbus.dispose()",
|
|
23
|
-
"name": "Async wait()"
|
|
24
|
-
},
|
|
25
|
-
{
|
|
26
|
-
"id": "basic-bus",
|
|
27
|
-
"code": "import { createBus } from '@vielzeug/herald'\n\n// Typed pub/sub with on(), emit(), and once()\nconst bus = createBus()\n\nconst unsub = bus.on('user:login', ({ userId, email }) => {\n console.log('login:', userId, email)\n})\n\nbus.once('user:logout', () => {\n console.log('logged out (fires once)')\n})\n\nbus.emit('user:login', { userId: '1', email: 'alice@example.com' })\nbus.emit('user:logout')\nbus.emit('user:logout') // once() already removed; no output\n\nunsub()\nbus.emit('user:login', { userId: '2', email: 'bob@example.com' }) // no output — unsubscribed\n\nbus.dispose()",
|
|
28
|
-
"name": "Basic Bus"
|
|
29
|
-
},
|
|
30
|
-
{
|
|
31
|
-
"id": "bus-basics",
|
|
32
|
-
"code": "import { createBus } from '@vielzeug/herald'\n\n// on() returns an unsubscribe handle; emit() delivers to all active listeners\nconst bus = createBus()\n\nconst unsubLogin = bus.on('user:login', (payload) => {\n console.log('login:', payload.name, '(' + payload.userId + ')')\n})\n\nbus.on('user:logout', () => console.log('logged out'))\nbus.on('notification', (msg) => console.log('notification:', msg))\n\nbus.emit('user:login', { userId: '123', name: 'Alice' })\nbus.emit('notification', 'welcome back!')\n\nunsubLogin()\n\nbus.emit('user:login', { userId: '456', name: 'Bob' }) // no output — unsubscribed\nconsole.log('active listeners:', bus.listenerCount())\n\nbus.dispose()",
|
|
33
|
-
"name": "createBus - Basics"
|
|
34
|
-
},
|
|
35
|
-
{
|
|
36
|
-
"id": "disposal-signal",
|
|
37
|
-
"code": "import { createBus } from '@vielzeug/herald'\n\n// disposalSignal ties an external subscription's lifetime to this bus\nconst mainBus = createBus()\nconst childBus = createBus()\n\n// Pass disposalSignal so the child listener is removed when mainBus disposes\nchildBus.on('data:update', ({ value }) => {\n console.log('child received:', value)\n}, { signal: mainBus.disposalSignal })\n\nconsole.log('child listeners before dispose:', childBus.listenerCount())\n\nchildBus.emit('data:update', { value: 10 }) // fires — listener is active\n\nmainBus.dispose() // disposalSignal fires — child listener auto-removed\n\nconsole.log('child listeners after mainBus.dispose():', childBus.listenerCount())\n\nchildBus.emit('data:update', { value: 20 }) // no output — listener is gone\nconsole.log('mainBus.disposed:', mainBus.disposed)\nconsole.log('disposalSignal aborted:', mainBus.disposalSignal.aborted)",
|
|
38
|
-
"name": "disposalSignal"
|
|
39
|
-
},
|
|
40
|
-
{
|
|
41
|
-
"id": "error-handling",
|
|
42
|
-
"code": "import { createBus, HeraldError } from '@vielzeug/herald'\n\n// onError captures listener throws — every listener still runs, even a buggy one\nconst errors = []\n\nconst bus = createBus({\n onError: ({ err, event }) => errors.push({ event, message: err.message }),\n})\n\nbus.on('order:placed', () => console.log('confirmation email sent'))\nbus.on('order:placed', () => {\n throw new Error('inventory check failed')\n})\nbus.on('order:placed', () => console.log('analytics event recorded')) // still runs\n\nbus.emit('order:placed', { id: 'ORD-1', total: 49.99 })\n\nconsole.log('captured errors:', errors)\n// [{ event: 'order:placed', message: 'inventory check failed' }]\n\n// Without onError, the first error rethrows once every listener has run —\n// instanceof HeraldError catches it without importing every herald error subclass\ntry {\n bus.waitAny(['event-a']) // waitAny requires at least 2 event keys\n} catch (err) {\n console.log('caught herald error?', err instanceof HeraldError, '-', err.message)\n}\n\nbus.dispose()",
|
|
43
|
-
"name": "Error Handling"
|
|
44
|
-
},
|
|
45
|
-
{
|
|
46
|
-
"id": "event-stream-take",
|
|
47
|
-
"code": "import { createBus } from '@vielzeug/herald'\n\n// Collect the first 3 emits then break — await using ensures cleanup\nconst bus = createBus()\n\nasync function collectFirstThree() {\n const collected = []\n\n await using stream = bus.events('score')\n for await (const n of stream) {\n collected.push(n)\n console.log('score:', n)\n if (collected.length >= 3) break\n }\n\n console.log('done — collected:', collected.length, 'values')\n}\n\nvoid collectFirstThree()\n\nlet i = 0\nconst timer = setInterval(() => {\n bus.emit('score', ++i * 10)\n if (i >= 6) {\n clearInterval(timer)\n bus.dispose()\n }\n}, 40)",
|
|
48
|
-
"name": "events() with break"
|
|
49
|
-
},
|
|
50
|
-
{
|
|
51
|
-
"id": "logger-option",
|
|
52
|
-
"code": "import { createBus } from '@vielzeug/herald'\n\n// Custom logger — route or suppress debug and warn output\nconst logs = []\n\nconst bus = createBus({\n maxListeners: 2,\n logger: {\n debug: (msg) => logs.push('[debug] ' + msg),\n warn: (msg) => logs.push('[warn] ' + msg),\n },\n})\n\nbus.on('order:placed', (order) => console.log('order:', order.id))\nbus.on('order:placed', (order) => console.log('copy:', order.id))\nbus.on('order:placed', () => {}) // triggers maxListeners warning (> 2)\n\nbus.emit('order:placed', { id: 'ORD-001', total: 49.99 })\n\nbus.dispose()\n\nconsole.log('captured log lines:')\nlogs.forEach((l) => console.log(l))",
|
|
53
|
-
"name": "Custom Logger"
|
|
54
|
-
},
|
|
55
|
-
{
|
|
56
|
-
"id": "named-bus",
|
|
57
|
-
"code": "import { createBus } from '@vielzeug/herald'\n\n// name appears in log prefixes and BusDisposedError messages\nconst logs = []\nconst warns = []\n\nconst authBus = createBus({\n name: 'auth',\n logger: {\n debug: (msg) => { logs.push(msg); console.log(msg) },\n warn: (msg) => warns.push(msg),\n },\n maxListeners: 1,\n})\n\nauthBus.on('login', (userId) => console.log('user:', userId))\nauthBus.on('login', (userId) => console.log('audit:', userId)) // triggers warn\n\nauthBus.emit('login', 'alice')\n\nconst pending = authBus.wait('logout')\nauthBus.dispose()\n\npending.catch((err) => {\n console.log('error name:', err.name)\n console.log('error message:', err.message)\n console.log('warn included name:', warns[0].includes('auth'))\n})",
|
|
58
|
-
"name": "Named Bus"
|
|
59
|
-
},
|
|
60
|
-
{
|
|
61
|
-
"id": "once-and-wait",
|
|
62
|
-
"code": "import { createBus } from '@vielzeug/herald'\n\n// once() fires exactly once then auto-removes; wait() resolves on the next emit\nconst bus = createBus()\n\nbus.once('data:ready', (payload) => {\n console.log('data (once):', payload.items.join(', '))\n})\n\nbus.emit('data:ready', { items: ['alpha', 'beta', 'gamma'] }) // fires\nbus.emit('data:ready', { items: ['ignored'] }) // once already consumed\n\nasync function waitForTask() {\n console.log('waiting for task...')\n const result = await bus.wait('task:done')\n console.log('task done! result:', result.result)\n}\n\nvoid waitForTask()\n\nsetTimeout(() => {\n bus.emit('task:done', { result: 99 })\n}, 50)",
|
|
63
|
-
"name": "once() and wait()"
|
|
64
|
-
},
|
|
65
|
-
{
|
|
66
|
-
"id": "pipe-events",
|
|
67
|
-
"code": "import { createBus, pipeEvents } from '@vielzeug/herald'\n\n// pipeEvents forwards a subset of events from one bus to another\nconst appBus = createBus()\nconst auditBus = createBus()\n\n// auditBus only receives auth events, not cart events\nauditBus.on('user:login', ({ email, userId }) => {\n console.log('[audit] login:', email, '(id:', userId + ')')\n})\nauditBus.on('user:logout', () => {\n console.log('[audit] logout recorded')\n})\n\nconst controller = new AbortController()\nconst unpipe = pipeEvents(appBus, auditBus, ['user:login', 'user:logout'], { signal: controller.signal })\n\nappBus.emit('user:login', { email: 'alice@example.com', userId: '1' })\nappBus.emit('cart:updated', { total: 99 }) // not forwarded\nappBus.emit('user:logout')\n\nunpipe() // stop forwarding\n\nappBus.emit('user:login', { email: 'bob@example.com', userId: '2' }) // not forwarded\nconsole.log('auditBus listeners after unpipe:', auditBus.listenerCount())",
|
|
68
|
-
"name": "pipeEvents()"
|
|
69
|
-
},
|
|
70
|
-
{
|
|
71
|
-
"id": "test-bus",
|
|
72
|
-
"code": "import { createTestBus } from '@vielzeug/herald/testing'\n\n// TestBus wraps a normal bus with emission recording — no mocking required\nconst bus = createTestBus()\n\nconst stop = bus.on('cart:updated', (cart) => console.log('handler saw total:', cart.total))\n\nbus.emit('cart:updated', { items: 1, total: 19.99 })\nbus.emit('cart:updated', { items: 2, total: 39.98 })\nbus.emit('user:logout')\n\nconsole.log('emitted count:', bus.emittedCount('cart:updated')) // 2\nconsole.log('all payloads:', bus.emitted('cart:updated'))\nconsole.log('every recorded event:', bus.allEmitted())\n\nstop()\nbus.emit('cart:updated', { items: 3, total: 59.97 }) // still recorded\n\nconsole.log('after unsubscribe:', bus.emitted('cart:updated'))\n\nbus.dispose()",
|
|
73
|
-
"name": "createTestBus()"
|
|
74
|
-
},
|
|
75
|
-
{
|
|
76
|
-
"id": "wait-any",
|
|
77
|
-
"code": "import { createBus } from '@vielzeug/herald'\n\n// waitAny resolves with { event, payload } for whichever event fires first\nconst bus = createBus()\n\nasync function watchNextSessionEvent() {\n console.log('waiting for first session event...')\n\n const result = await bus.waitAny(['user:login', 'user:logout', 'session:expired'])\n\n if (result.event === 'user:login') {\n console.log('login:', result.payload.email, '(id:', result.payload.userId + ')')\n } else if (result.event === 'session:expired') {\n console.log('session expired:', result.payload.reason)\n } else {\n console.log('user logged out')\n }\n}\n\nvoid watchNextSessionEvent()\n\nsetTimeout(() => {\n bus.emit('user:login', { email: 'alice@example.com', userId: '42' })\n bus.emit('user:logout') // ignored — waitAny already resolved\n}, 30)",
|
|
78
|
-
"name": "waitAny()"
|
|
79
|
-
},
|
|
80
|
-
{
|
|
81
|
-
"id": "wildcard-listeners",
|
|
82
|
-
"code": "import { createBus } from '@vielzeug/herald'\n\ntype AppEvents = {\n 'user:login': { userId: string }\n 'user:logout': void\n 'cart:updated': { items: number }\n}\n\nconst bus = createBus<AppEvents>({ name: 'app' })\n\n// onAny receives every event — useful for logging, analytics, tracing\nconst unsub = bus.onAny((event, payload) => {\n console.log('[audit]', event, payload)\n})\n\nconsole.log('wildcard listeners:', bus.wildcardCount()) // 1\n\nbus.emit('user:login', { userId: 'alice' })\nbus.emit('cart:updated', { items: 3 })\nbus.emit('user:logout')\n\n// Remove the wildcard listener\nunsub()\nconsole.log('after unsub:', bus.wildcardCount()) // 0\n\nbus.dispose()",
|
|
83
|
-
"name": "onAny + wildcardCount()"
|
|
84
|
-
}
|
|
85
|
-
],
|
|
86
|
-
"typeSignatures": {
|
|
87
|
-
"combineSignals": "export { combineSignals, createBus } from './bus';",
|
|
88
|
-
"createBus": "export { combineSignals, createBus } from './bus';",
|
|
89
|
-
"BusDisposedError": "export { BusDisposedError, HeraldConfigError, HeraldError } from './errors';",
|
|
90
|
-
"HeraldConfigError": "export { BusDisposedError, HeraldConfigError, HeraldError } from './errors';",
|
|
91
|
-
"HeraldError": "export { BusDisposedError, HeraldConfigError, HeraldError } from './errors';",
|
|
92
|
-
"pipeEvents": "export { pipeEvents } from './pipe';",
|
|
93
|
-
"Bus": "export type {\n Bus,\n BusOptions,\n EmissionErrorContext,\n EventKey,\n EventMap,\n EventStream,\n HeraldEvent,\n Listener,\n Middleware,\n PipeableKey,\n PipeEntry,\n SubscribeOptions,\n Unsubscribe,\n WaitAnyResult,\n} from './types';",
|
|
94
|
-
"BusOptions": "export type {\n Bus,\n BusOptions,\n EmissionErrorContext,\n EventKey,\n EventMap,\n EventStream,\n HeraldEvent,\n Listener,\n Middleware,\n PipeableKey,\n PipeEntry,\n SubscribeOptions,\n Unsubscribe,\n WaitAnyResult,\n} from './types';",
|
|
95
|
-
"EmissionErrorContext": "export type {\n Bus,\n BusOptions,\n EmissionErrorContext,\n EventKey,\n EventMap,\n EventStream,\n HeraldEvent,\n Listener,\n Middleware,\n PipeableKey,\n PipeEntry,\n SubscribeOptions,\n Unsubscribe,\n WaitAnyResult,\n} from './types';",
|
|
96
|
-
"EventKey": "export type {\n Bus,\n BusOptions,\n EmissionErrorContext,\n EventKey,\n EventMap,\n EventStream,\n HeraldEvent,\n Listener,\n Middleware,\n PipeableKey,\n PipeEntry,\n SubscribeOptions,\n Unsubscribe,\n WaitAnyResult,\n} from './types';",
|
|
97
|
-
"EventMap": "export type {\n Bus,\n BusOptions,\n EmissionErrorContext,\n EventKey,\n EventMap,\n EventStream,\n HeraldEvent,\n Listener,\n Middleware,\n PipeableKey,\n PipeEntry,\n SubscribeOptions,\n Unsubscribe,\n WaitAnyResult,\n} from './types';",
|
|
98
|
-
"EventStream": "export type {\n Bus,\n BusOptions,\n EmissionErrorContext,\n EventKey,\n EventMap,\n EventStream,\n HeraldEvent,\n Listener,\n Middleware,\n PipeableKey,\n PipeEntry,\n SubscribeOptions,\n Unsubscribe,\n WaitAnyResult,\n} from './types';",
|
|
99
|
-
"HeraldEvent": "export type {\n Bus,\n BusOptions,\n EmissionErrorContext,\n EventKey,\n EventMap,\n EventStream,\n HeraldEvent,\n Listener,\n Middleware,\n PipeableKey,\n PipeEntry,\n SubscribeOptions,\n Unsubscribe,\n WaitAnyResult,\n} from './types';",
|
|
100
|
-
"Listener": "export type {\n Bus,\n BusOptions,\n EmissionErrorContext,\n EventKey,\n EventMap,\n EventStream,\n HeraldEvent,\n Listener,\n Middleware,\n PipeableKey,\n PipeEntry,\n SubscribeOptions,\n Unsubscribe,\n WaitAnyResult,\n} from './types';",
|
|
101
|
-
"Middleware": "export type {\n Bus,\n BusOptions,\n EmissionErrorContext,\n EventKey,\n EventMap,\n EventStream,\n HeraldEvent,\n Listener,\n Middleware,\n PipeableKey,\n PipeEntry,\n SubscribeOptions,\n Unsubscribe,\n WaitAnyResult,\n} from './types';",
|
|
102
|
-
"PipeableKey": "export type {\n Bus,\n BusOptions,\n EmissionErrorContext,\n EventKey,\n EventMap,\n EventStream,\n HeraldEvent,\n Listener,\n Middleware,\n PipeableKey,\n PipeEntry,\n SubscribeOptions,\n Unsubscribe,\n WaitAnyResult,\n} from './types';",
|
|
103
|
-
"PipeEntry": "export type {\n Bus,\n BusOptions,\n EmissionErrorContext,\n EventKey,\n EventMap,\n EventStream,\n HeraldEvent,\n Listener,\n Middleware,\n PipeableKey,\n PipeEntry,\n SubscribeOptions,\n Unsubscribe,\n WaitAnyResult,\n} from './types';",
|
|
104
|
-
"SubscribeOptions": "export type {\n Bus,\n BusOptions,\n EmissionErrorContext,\n EventKey,\n EventMap,\n EventStream,\n HeraldEvent,\n Listener,\n Middleware,\n PipeableKey,\n PipeEntry,\n SubscribeOptions,\n Unsubscribe,\n WaitAnyResult,\n} from './types';",
|
|
105
|
-
"Unsubscribe": "export type {\n Bus,\n BusOptions,\n EmissionErrorContext,\n EventKey,\n EventMap,\n EventStream,\n HeraldEvent,\n Listener,\n Middleware,\n PipeableKey,\n PipeEntry,\n SubscribeOptions,\n Unsubscribe,\n WaitAnyResult,\n} from './types';",
|
|
106
|
-
"WaitAnyResult": "export type {\n Bus,\n BusOptions,\n EmissionErrorContext,\n EventKey,\n EventMap,\n EventStream,\n HeraldEvent,\n Listener,\n Middleware,\n PipeableKey,\n PipeEntry,\n SubscribeOptions,\n Unsubscribe,\n WaitAnyResult,\n} from './types';"
|
|
107
|
-
}
|
|
108
|
-
}
|
|
@@ -1,132 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"apiSource": "export * from './commerce/commerce';\nexport * from './date/date';\nexport * from './errors';\nexport * from './factory';\nexport * from './finance/finance';\nexport * from './internet/internet';\nexport * from './location/location';\nexport * from './lorem/lorem';\nexport * from './person/person';\nexport * from './seed/create-seed';\nexport * from './seed/mulberry32';\nexport * from './types';\n",
|
|
3
|
-
"docs": {
|
|
4
|
-
"index": "---\ntitle: Illusionist — Fake Data Generator for TypeScript\ndescription: Typed, deterministic, locale-aware fake data generator with a seeded PRNG, eight data categories, and zero external runtime dependencies.\npackage: illusionist\ncategory: data\nkeywords: [fake-data, mock, seed, faker, test-fixtures, deterministic]\nexports: [createIllusion, createSeed, mulberry32]\nrelated: [arsenal, coins, tempo]\nenvironments: [browser, node, ssr]\n---\n\n<!-- markdownlint-disable MD025 MD033 MD060 -->\n\n<PackageHero package=\"illusionist\" />\n\n## Why Illusionist?\n\nIllusionist generates realistic fake data from a single seeded random source. The same seed always produces the same output, so test fixtures and snapshots stay reproducible across runs, machines, and CI. Every category shares one bound instance with one locale, so a person, their email, and their address stay internally consistent.\n\n```ts\n// Before\nconst user = {\n name: 'Test User',\n email: 'test@example.com',\n address: '123 Main St',\n};\n\n// After\nimport { createIllusion } from '@vielzeug/illusionist';\nimport { en } from '@vielzeug/illusionist/locales';\n\nconst illusion = createIllusion({ seed: 12345, locale: en });\n\nconst user = {\n name: illusion.person.fullName(),\n email: illusion.internet.email(),\n address: illusion.location.streetAddress(),\n};\n\nillusion.dispose();\n```\n\n| Feature | Illusionist | Faker.js | @faker-js/faker |\n| --- | --- | --- | --- |\n| Bundle size | <PackageInfo package=\"illusionist\" type=\"size\" /> | External dependency | External dependency |\n| Zero external dependencies | <ore-icon name=\"check\" size=\"16\"></ore-icon> | <ore-icon name=\"x\" size=\"16\"></ore-icon> | <ore-icon name=\"x\" size=\"16\"></ore-icon> |\n| Seeded determinism | <ore-icon name=\"check\" size=\"16\"></ore-icon> | Partial | <ore-icon name=\"check\" size=\"16\"></ore-icon> |\n| Locale-aware datasets | <ore-icon name=\"check\" size=\"16\"></ore-icon> | <ore-icon name=\"check\" size=\"16\"></ore-icon> | <ore-icon name=\"check\" size=\"16\"></ore-icon> |\n| TypeScript-native types | <ore-icon name=\"check\" size=\"16\"></ore-icon> | <ore-icon name=\"x\" size=\"16\"></ore-icon> | <ore-icon name=\"check\" size=\"16\"></ore-icon> |\n\n<div class=\"decision-callout\">\n\n**Use Illusionist when** test fixtures, mock APIs, or database seeds must be realistic and reproducible from a single seed value.\n\n**Consider @faker-js/faker when** you need a large catalog of locale datasets beyond `en` and `de` or a community plugin ecosystem.\n\n</div>\n\n## Installation\n\n::: code-group\n\n```sh [pnpm]\npnpm add @vielzeug/illusionist\n```\n\n```sh [npm]\nnpm install @vielzeug/illusionist\n```\n\n```sh [yarn]\nyarn add @vielzeug/illusionist\n```\n\n:::\n\n## Quick Start\n\nCreate a bound instance with a seed and locale. All categories share that seed, so output is deterministic.\n\n```ts\nimport { createIllusion } from '@vielzeug/illusionist';\nimport { en } from '@vielzeug/illusionist/locales';\n\nconst illusion = createIllusion({ seed: 12345, locale: en });\n\nillusion.person.fullName(); // 'Ashley Harris'\nillusion.internet.email(); // 'samantha.sanchez@mail.com'\nillusion.commerce.price(); // Money { amount: 76640n, currency: USD }\nillusion.date.past({ years: 2 }); // Temporal.ZonedDateTime\n\nillusion.dispose(); // release the instance; [Symbol.dispose]() also works\n```\n\n## Features\n\n<div class=\"features-grid\">\n\n- **`person`**: names, gender, prefixes, suffixes, job titles\n- **`internet`**: emails, usernames, passwords, URLs, IPs, MACs, HTTP metadata\n- **`commerce`**: product names, departments, prices as coins `Money`\n- **`date`**: past, future, recent, between, birthday as tempo `Temporal` objects\n- **`finance`**: amounts, IBANs, BICs, credit cards, crypto addresses\n- **`location`**: cities, streets, states, countries, GPS coordinates\n- **`lorem`**: words, sentences, paragraphs, slugs\n- **`system`**: file paths, semver, UUIDs, ports, cron expressions\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- [Arsenal](/arsenal/) — random primitives (`RandomSource`, `uuid`) that Illusionist builds on.\n- [Coins](/coins/) — exact money type returned by `commerce.price()` and `finance.amount()`.\n- [Tempo](/tempo/) — `Temporal` date utilities returned by every `date` function.\n\n</div>\n\n<!-- markdownlint-enable MD025 MD033 MD060 -->\n",
|
|
5
|
-
"api": "---\ntitle: Illusionist — API Reference\ndescription: createIllusion, all category functions, seed utilities, types, and errors.\n---\n\n[[toc]]\n\n## API Overview\n\n| Symbol | Purpose | Execution | Common gotcha |\n| --- | --- | --- | --- |\n| `createIllusion` | Create a bound, seeded instance | Sync | Locale is fixed for the instance lifetime |\n| `person.*` | Names, gender, job titles | Sync | Locale-specific datasets (`en`, `de`) |\n| `internet.*` | Emails, URLs, IPs, HTTP metadata | Sync | `ip()` defaults to IPv4 |\n| `commerce.*` | Product names, prices | Sync | `price()` returns coins `Money` |\n| `date.*` | Past, future, recent, birthday | Sync | Returns tempo `Temporal` objects |\n| `finance.*` | IBANs, cards, crypto addresses | Sync | IBANs pass mod-97; cards pass Luhn |\n| `location.*` | Cities, streets, GPS | Sync | Locale-specific datasets |\n| `lorem.*` | Words, sentences, paragraphs | Sync | Word pool is fixed |\n| `system.*` | Files, semver, UUIDs, ports | Sync | `port()` avoids well-known ports by default |\n| `createSeed` | Build a `RandomSource` from a seed | Sync | Non-finite numeric seeds throw |\n| `mulberry32` | Low-level 32-bit PRNG | Sync | Not cryptographically secure |\n\n## Package Entry Point\n\n| Import | Purpose |\n| --- | --- |\n| `@vielzeug/illusionist` | `createIllusion`, `Illusionist`, `IllusionistOptions`, `IllusionistLocale`, error classes |\n| `@vielzeug/illusionist/locales` | Tree-shakeable barrel — `en`, `de` locale objects |\n| `@vielzeug/illusionist/locales/en` | English locale object only |\n| `@vielzeug/illusionist/locales/de` | German locale object only |\n| `@vielzeug/illusionist/seed` | `createSeed`, `mulberry32` |\n| `@vielzeug/illusionist/person` | Person category functions |\n| `@vielzeug/illusionist/internet` | Internet category functions |\n| `@vielzeug/illusionist/commerce` | Commerce category functions |\n| `@vielzeug/illusionist/date` | Date category functions |\n| `@vielzeug/illusionist/finance` | Finance category functions |\n| `@vielzeug/illusionist/location` | Location category functions |\n| `@vielzeug/illusionist/lorem` | Lorem category functions |\n| `@vielzeug/illusionist/system` | System category functions |\n\n## createIllusion\n\n```ts\nfunction createIllusion(options: IllusionistOptions): Illusionist;\n```\n\nCreates a bound instance. All categories share one seeded random source and one locale. Locale data is included only when its dedicated subpath is imported; the root entry does not statically import a default locale. For dynamic switching, use `await import('@vielzeug/illusionist/locales')` before calling this synchronous factory.\n\n| Option | Type | Default | Description |\n| --- | --- | --- | --- |\n| `seed` | `number \\| string` | `undefined` | Seed for deterministic output. Omit for cryptographic randomness. |\n| `locale` | `IllusionistLocale` | Required | Explicit locale object for locale-aware categories. |\n\n**Returns:** `Illusionist` — an object with `person`, `internet`, `commerce`, `date`, `finance`, `location`, `lorem`, `system` categories, plus `seed`, `locale`, `dispose()`, `disposed`, `disposalSignal`, and `[Symbol.dispose]()`.\n\n```ts\nimport { createIllusion } from '@vielzeug/illusionist';\nimport { en } from '@vielzeug/illusionist/locales';\n\nconst illusion = createIllusion({ seed: 12345, locale: en });\nillusion.person.fullName();\nillusion.dispose();\n```\n\n---\n\n## Person\n\n### `person.firstName()`\n\n```ts\nfunction firstName(): string;\n```\n\nReturns a random first name from the locale dataset.\n\n### `person.lastName()`\n\n```ts\nfunction lastName(): string;\n```\n\nReturns a random last name from the locale dataset.\n\n### `person.fullName()`\n\n```ts\nfunction fullName(): string;\n```\n\nReturns a first and last name separated by a space.\n\n### `person.gender()`\n\n```ts\nfunction gender(): string;\n```\n\nReturns a random gender label from the locale dataset.\n\n### `person.prefix()`\n\n```ts\nfunction prefix(): string;\n```\n\nReturns a random name prefix (e.g. `Mr.`, `Dr.`).\n\n### `person.suffix()`\n\n```ts\nfunction suffix(): string;\n```\n\nReturns a random name suffix. Returns an empty string when the locale dataset has no suffixes.\n\n### `person.jobTitle()`\n\n```ts\nfunction jobTitle(): string;\n```\n\nReturns a job area and job type joined by a space.\n\n---\n\n## Internet\n\n### `internet.email()`\n\n```ts\nfunction email(): string;\n```\n\nReturns an email of the form `firstname.lastname@domain.tld`.\n\n### `internet.username()`\n\n```ts\nfunction username(): string;\n```\n\nReturns either a random alphanumeric string or a `firstname.lastname` pattern.\n\n### `internet.password(options?)`\n\n```ts\nfunction password(options?: PasswordOptions): string;\n```\n\n| Option | Type | Default | Description |\n| --- | --- | --- | --- |\n| `length` | `number` | `12` | Password length. |\n| `memorable` | `boolean` | `false` | Build from name fragments and digits. |\n\nReturns a password mixing upper/lowercase letters, digits, and special characters.\n\n### `internet.url()`\n\n```ts\nfunction url(): string;\n```\n\nReturns a URL of the form `protocol://sub.domain.tld/path/...`.\n\n### `internet.domainName()`\n\n```ts\nfunction domainName(): string;\n```\n\nReturns a domain of the form `domain.tld`.\n\n### `internet.ip(version?)`\n\n```ts\nfunction ip(version?: 4 | 6): string;\n```\n\nReturns an IPv4 or IPv6 address. Defaults to IPv4.\n\n### `internet.mac()`\n\n```ts\nfunction mac(): string;\n```\n\nReturns a MAC address of the form `XX:XX:XX:XX:XX:XX`.\n\n### `internet.userAgent()`\n\n```ts\nfunction userAgent(): string;\n```\n\nReturns a random user agent string.\n\n### `internet.httpMethod()`\n\n```ts\nfunction httpMethod(): string;\n```\n\nReturns a random HTTP method.\n\n### `internet.statusCode()`\n\n```ts\nfunction statusCode(): number;\n```\n\nReturns a random HTTP status code.\n\n### `internet.mimeType()`\n\n```ts\nfunction mimeType(): string;\n```\n\nReturns a random MIME type.\n\n---\n\n## Commerce\n\n### `commerce.productAdjective()`\n\n```ts\nfunction productAdjective(): string;\n```\n\nReturns a random product adjective.\n\n### `commerce.productMaterial()`\n\n```ts\nfunction productMaterial(): string;\n```\n\nReturns a random product material.\n\n### `commerce.productNoun()`\n\n```ts\nfunction productNoun(): string;\n```\n\nReturns a random product noun.\n\n### `commerce.productName()`\n\n```ts\nfunction productName(): string;\n```\n\nReturns an adjective, material, and noun joined by spaces.\n\n### `commerce.department()`\n\n```ts\nfunction department(): string;\n```\n\nReturns a random department name.\n\n### `commerce.price(options?)`\n\n```ts\nfunction price(options?: PriceOptions): Money;\n```\n\n| Option | Type | Default | Description |\n| --- | --- | --- | --- |\n| `min` | `number` | `0.01` | Minimum price. |\n| `max` | `number` | `1000` | Maximum price. |\n| `currency` | `'USD' \\| 'EUR' \\| 'GBP'` | `'USD'` | Currency code. |\n\nReturns a coins `Money` value with two decimal places.\n\n### `commerce.productDescription()`\n\n```ts\nfunction productDescription(): string;\n```\n\nReturns one or two sentences describing a product.\n\n---\n\n## Date\n\nAll date functions return tempo `Temporal` objects.\n\n### `date.past(options?)`\n\n```ts\nfunction past(options?: { years?: number; ref?: Temporal.ZonedDateTime }): Temporal.ZonedDateTime;\n```\n\nReturns a date in the past within `years` (default `1`) from `ref` (default now).\n\n### `date.future(options?)`\n\n```ts\nfunction future(options?: { years?: number; ref?: Temporal.ZonedDateTime }): Temporal.ZonedDateTime;\n```\n\nReturns a date in the future within `years` (default `1`) from `ref` (default now).\n\n### `date.recent(options?)`\n\n```ts\nfunction recent(options?: { days?: number; ref?: Temporal.ZonedDateTime }): Temporal.ZonedDateTime;\n```\n\nReturns a date within `days` (default `1`) in the past from `ref` (default now).\n\n### `date.between(from, to)`\n\n```ts\nfunction between(from: Temporal.ZonedDateTime, to: Temporal.ZonedDateTime): Temporal.ZonedDateTime;\n```\n\nReturns a date between `from` and `to`. Returns `from` if `from` is after `to`.\n\n### `date.birthday(options?)`\n\n```ts\nfunction birthday(options?: { minAge?: number; maxAge?: number; ref?: Temporal.ZonedDateTime }): Temporal.PlainDate;\n```\n\nReturns a `PlainDate` with a random age between `minAge` (default `18`) and `maxAge` (default `80`).\n\n### `date.weekday(locale?)`\n\n```ts\nfunction weekday(locale?: string): string;\n```\n\nReturns a random weekday name. Uses the instance locale unless overridden.\n\n### `date.month(locale?)`\n\n```ts\nfunction month(locale?: string): string;\n```\n\nReturns a random month name. Uses the instance locale unless overridden.\n\n---\n\n## Finance\n\n### `finance.amount(options?)`\n\n```ts\nfunction amount(options?: AmountOptions): Money;\n```\n\n| Option | Type | Default | Description |\n| --- | --- | --- | --- |\n| `min` | `number` | `100` | Minimum amount. |\n| `max` | `number` | `10000` | Maximum amount. |\n| `currency` | `'USD' \\| 'EUR' \\| 'GBP'` | `'USD'` | Currency code. |\n\nReturns a coins `Money` value with two decimal places.\n\n### `finance.iban(countryCode?)`\n\n```ts\nfunction iban(countryCode?: string): string;\n```\n\nReturns an IBAN. Pass a country code to fix the country; otherwise a random supported country is chosen. The check digits are computed so the IBAN passes mod-97 validation.\n\n### `finance.bic()`\n\n```ts\nfunction bic(): string;\n```\n\nReturns a BIC/SWIFT code of 8 or 11 characters.\n\n### `finance.creditCardNumber(type?)`\n\n```ts\nfunction creditCardNumber(type?: 'visa' | 'mastercard' | 'amex'): string;\n```\n\nReturns a card number with a valid Luhn check digit. Amex returns 15 digits; others return 16.\n\n### `finance.creditCardCVV(type?)`\n\n```ts\nfunction creditCardCVV(type?: 'visa' | 'mastercard' | 'amex'): string;\n```\n\nReturns a CVV. Amex returns 4 digits; others return 3.\n\n### `finance.bitcoinAddress()`\n\n```ts\nfunction bitcoinAddress(): string;\n```\n\nReturns a Bitcoin address with a `1`, `3`, or `bc1` prefix.\n\n### `finance.ethereumAddress()`\n\n```ts\nfunction ethereumAddress(): string;\n```\n\nReturns a 42-character Ethereum address prefixed with `0x`.\n\n### `finance.transactionType()`\n\n```ts\nfunction transactionType(): string;\n```\n\nReturns a random transaction type label.\n\n### `finance.bank()`\n\n```ts\nfunction bank(): string;\n```\n\nReturns a random bank name.\n\n---\n\n## Location\n\n### `location.city()`\n\n```ts\nfunction city(): string;\n```\n\nReturns a random city from the locale dataset.\n\n### `location.street()`\n\n```ts\nfunction street(): string;\n```\n\nReturns a random street from the locale dataset.\n\n### `location.streetAddress()`\n\n```ts\nfunction streetAddress(): string;\n```\n\nReturns a house number (1–999) followed by a street name.\n\n### `location.zipCode()`\n\n```ts\nfunction zipCode(): string;\n```\n\nReturns a ZIP code matching the locale's pattern.\n\n### `location.state()`\n\n```ts\nfunction state(): string;\n```\n\nReturns a random state or region from the locale dataset.\n\n### `location.country()`\n\n```ts\nfunction country(): string;\n```\n\nReturns a random country from the locale dataset.\n\n### `location.latitude()`\n\n```ts\nfunction latitude(): number;\n```\n\nReturns a latitude in the range `[-90, 90]`.\n\n### `location.longitude()`\n\n```ts\nfunction longitude(): number;\n```\n\nReturns a longitude in the range `[-180, 180]`.\n\n### `location.nearbyGPSCoordinate(ref?)`\n\n```ts\nfunction nearbyGPSCoordinate(ref?: Coordinate): Coordinate;\n```\n\nReturns a coordinate within ~1 degree of `ref`. When `ref` is omitted, a random coordinate is used as the base.\n\n---\n\n## Lorem\n\n### `lorem.word()`\n\n```ts\nfunction word(): string;\n```\n\nReturns a single random word.\n\n### `lorem.words(count?)`\n\n```ts\nfunction words(count?: number): string;\n```\n\nReturns `count` (default `3`) space-joined words.\n\n### `lorem.sentence(wordCount?)`\n\n```ts\nfunction sentence(wordCount?: number): string;\n```\n\nReturns a sentence of `wordCount` words (default 6–12) with a capital first letter and trailing period.\n\n### `lorem.sentences(count?)`\n\n```ts\nfunction sentences(count?: number): string;\n```\n\nReturns `count` (default `3`) space-joined sentences.\n\n### `lorem.paragraph(sentenceCount?)`\n\n```ts\nfunction paragraph(sentenceCount?: number): string;\n```\n\nReturns a paragraph of `sentenceCount` sentences (default 3–7).\n\n### `lorem.paragraphs(count?)`\n\n```ts\nfunction paragraphs(count?: number): string;\n```\n\nReturns `count` (default `3`) newline-joined paragraphs.\n\n### `lorem.slug(wordCount?)`\n\n```ts\nfunction slug(wordCount?: number): string;\n```\n\nReturns a hyphen-joined slug of `wordCount` (default `3`) words.\n\n### `lorem.lines(count?)`\n\n```ts\nfunction lines(count?: number): string;\n```\n\nReturns `count` (default `5`) newline-joined lines, each a sentence.\n\n---\n\n## System\n\n### `system.fileExtension()`\n\n```ts\nfunction fileExtension(): string;\n```\n\nReturns a random file extension.\n\n### `system.fileName()`\n\n```ts\nfunction fileName(): string;\n```\n\nReturns a random file name with extension.\n\n### `system.filePath()`\n\n```ts\nfunction filePath(): string;\n```\n\nReturns a path with 1–4 directory segments and a file name.\n\n### `system.mimeType()`\n\n```ts\nfunction mimeType(): string;\n```\n\nReturns a random MIME type.\n\n### `system.semver(options?)`\n\n```ts\nfunction semver(options?: { maxMajor?: number; includePrerelease?: boolean }): string;\n```\n\n| Option | Type | Default | Description |\n| --- | --- | --- | --- |\n| `maxMajor` | `number` | `20` | Maximum major version. |\n| `includePrerelease` | `boolean` | `false` | Occasionally append a prerelease label. |\n\nReturns a semver string.\n\n### `system.uuid()`\n\n```ts\nfunction uuid(): string;\n```\n\nReturns a random UUID via `crypto.randomUUID()`. **Not deterministic** — ignores the seeded `RandomSource`. Use only when uniqueness matters more than reproducibility.\n\n### `system.port(options?)`\n\n```ts\nfunction port(options?: { min?: number; max?: number }): number;\n```\n\n| Option | Type | Default | Description |\n| --- | --- | --- | --- |\n| `min` | `number` | `1024` | Minimum port. |\n| `max` | `number` | `65535` | Maximum port. |\n\nReturns a random port number.\n\n### `system.cron()`\n\n```ts\nfunction cron(): string;\n```\n\nReturns a random cron expression from common patterns.\n\n### `system.process()`\n\n```ts\nfunction process(): string;\n```\n\nReturns a random process name of the form `prefix_suffix`.\n\n---\n\n## Seed\n\nImport from the `seed` subpath:\n\n```ts\nimport { createSeed, mulberry32 } from '@vielzeug/illusionist/seed';\n```\n\n### `createSeed(seed?)`\n\n```ts\nfunction createSeed(seed?: number | string): RandomSource;\n```\n\nCreates a `RandomSource` from a seed. Number seeds are used directly as mulberry32 state. String seeds are hashed to a 32-bit integer. Omit the seed for cryptographic randomness via `crypto.getRandomValues`. Throws `IllusionistSeedError` for non-finite numeric seeds.\n\n```ts\nconst a = createSeed(12345); // deterministic\nconst b = createSeed('hello'); // deterministic (hashed)\nconst c = createSeed(); // cryptographic\n```\n\n### `mulberry32(seed)`\n\n```ts\nfunction mulberry32(seed: number): RandomSource;\n```\n\nLow-level 32-bit PRNG. Not cryptographically secure. Returns a `RandomSource` producing floats in `[0, 1)`.\n\n---\n\n## Types\n\n```ts\ntype PersonLocaleData = {\n readonly firstNameFemale: readonly string[];\n readonly firstNameMale: readonly string[];\n readonly gender: readonly string[];\n readonly jobAreas: readonly string[];\n readonly jobTypes: readonly string[];\n readonly lastName: readonly string[];\n readonly prefix: readonly string[];\n readonly suffix: readonly string[];\n};\n\ntype LocationLocaleData = {\n readonly cities: readonly string[];\n readonly countries: readonly string[];\n readonly states: readonly string[];\n readonly streets: readonly string[];\n readonly zipPattern: string;\n};\n\ntype IllusionistLocale = {\n readonly code: string;\n readonly person: PersonLocaleData;\n readonly location: LocationLocaleData;\n};\n\ntype IllusionistOptions = {\n seed?: number | string;\n locale: IllusionistLocale;\n};\n\ntype Illusionist = {\n readonly person: typeof person;\n readonly internet: typeof internet;\n readonly commerce: typeof commerce;\n readonly date: typeof date;\n readonly finance: typeof finance;\n readonly location: typeof location;\n readonly lorem: typeof lorem;\n readonly system: typeof system;\n readonly seed: number | string | undefined;\n readonly locale: IllusionistLocale;\n dispose(): void;\n readonly disposed: boolean;\n readonly disposalSignal: AbortSignal;\n [Symbol.dispose](): void;\n};\n\ntype Coordinate = {\n lat: number;\n lng: number;\n};\n\ntype PasswordOptions = {\n length?: number;\n memorable?: boolean;\n};\n\ntype PriceOptions = {\n readonly min?: number;\n readonly max?: number;\n readonly currency?: 'USD' | 'EUR' | 'GBP';\n};\n\ntype AmountOptions = {\n readonly min?: number;\n readonly max?: number;\n readonly currency?: 'USD' | 'EUR' | 'GBP';\n};\n\n// Re-exported from @vielzeug/arsenal\ntype RandomSource = {\n next(): number; // float in [0, 1)\n};\n```\n\n## Errors\n\nAll errors extend `IllusionistError`, which extends `Error`. Use `instanceof IllusionistError` to catch any illusionist-originated error.\n\n| Error | Trigger | Notable properties |\n| --- | --- | --- |\n| `IllusionistError` | Base class for all illusionist errors | `name`, `message` |\n| `IllusionistSeedError` | Non-finite numeric seed passed to `createSeed` (`NaN`, `Infinity`, `-Infinity`) | `name`, `message` |\n\n```ts\nimport { IllusionistError, IllusionistSeedError, createSeed } from '@vielzeug/illusionist';\n\ntry {\n createSeed(Number.NaN);\n} catch (error) {\n if (error instanceof IllusionistSeedError) {\n console.log(error.message);\n }\n}\n```\n",
|
|
6
|
-
"usage": "---\ntitle: Illusionist — Usage Guide\ndescription: Generate deterministic, locale-aware fake data with Illusionist.\n---\n\n[[toc]]\n\n## Basic Usage\n\nCreate an illusionist instance with `createIllusion`. Access data through the eight bound categories. Each call consumes from the shared random source, so output is deterministic for a given seed.\n\n```ts\nimport { createIllusion } from '@vielzeug/illusionist';\nimport { en } from '@vielzeug/illusionist/locales';\n\nconst illusion = createIllusion({ seed: 12345, locale: en });\n\nillusion.person.firstName(); // 'Ashley'\nillusion.internet.username(); // 'fVEv9zc638m'\nillusion.commerce.productName(); // 'Intelligent Granite Table'\nillusion.date.recent({ days: 7 }); // Temporal.ZonedDateTime within the last week\nillusion.lorem.sentence(); // 'Enim ex non ea minim amet sint laborum proident nisi anim officia.'\n\nillusion.dispose();\n```\n\n## Seeded Determinism\n\nPass a number or string seed to make output reproducible. The same seed always produces the same sequence across runs, machines, and Node versions.\n\n```ts\nimport { createIllusion } from '@vielzeug/illusionist';\nimport { en } from '@vielzeug/illusionist/locales';\n\nconst a = createIllusion({ seed: 12345, locale: en });\nconst b = createIllusion({ seed: 12345, locale: en });\n\na.person.fullName() === b.person.fullName(); // true\n\nconst c = createIllusion({ seed: 'my-test-suite', locale: en });\nconst d = createIllusion({ seed: 'my-test-suite', locale: en });\n\nc.internet.email() === d.internet.email(); // true — string seeds are hashed\n```\n\nOmit the seed for cryptographic randomness backed by `crypto.getRandomValues`. Output is then non-deterministic and unsuitable for snapshots.\n\n```ts\nimport { createIllusion } from '@vielzeug/illusionist';\nimport { en } from '@vielzeug/illusionist/locales';\n\nconst random = createIllusion({ locale: en });\nrandom.person.fullName(); // different every run\n```\n\n## Locale Support\n\nImport a locale object and pass it at creation time. The `person` and `location` categories draw from that object's datasets. The `date.weekday` and `date.month` functions use its locale code.\n\n```ts\nimport { createIllusion } from '@vielzeug/illusionist';\nimport { de, en } from '@vielzeug/illusionist/locales';\n\nconst english = createIllusion({ seed: 1, locale: en });\nconst german = createIllusion({ seed: 1, locale: de });\n\nenglish.person.firstName(); // 'Mary'\ngerman.person.firstName(); // 'Mia'\n\nenglish.location.city(); // 'Austin'\ngerman.location.city(); // 'Bremen'\n\nenglish.date.month(); // 'December'\ngerman.date.month(); // 'Dezember'\n```\n\nThe locale is fixed for the lifetime of an instance. Each locale is a separate subpath, so locale data ships only when that subpath is imported; the root package does not statically include English or German data. Create a new instance to switch locales.\n\nFor dynamic app switching, load the desired locale before calling the synchronous factory:\n\n```ts\nconst { de } = await import('@vielzeug/illusionist/locales/de');\nconst german = createIllusion({ locale: de });\n```\n\n### Custom Locales\n\nThe shipped `en` and `de` objects are just plain data that `satisfies IllusionistLocale`. Build your own the same way — import the type, assemble the `person` and `location` datasets, and pass the result to `createIllusion`. No registration step; the factory accepts any object that matches the shape.\n\n```ts\nimport { createIllusion, type IllusionistLocale } from '@vielzeug/illusionist';\n\nconst fr: IllusionistLocale = {\n code: 'fr',\n person: {\n firstNameFemale: ['Marie', 'Camille', 'Sophie'],\n firstNameMale: ['Louis', 'Hugo', 'Léo'],\n lastName: ['Martin', 'Bernard', 'Dubois'],\n gender: ['féminin', 'masculin', 'non-binaire'],\n jobAreas: ['marketing', 'ingénierie', 'ventes'],\n jobTypes: ['directeur', 'ingénieur', 'analyste'],\n prefix: ['M.', 'Mme', 'Dr.'],\n suffix: ['PhD', 'Jr.'],\n },\n location: {\n cities: ['Paris', 'Lyon', 'Marseille'],\n countries: ['France', 'Belgique', 'Suisse'],\n states: ['Île-de-France', 'Auvergne-Rhône-Alpes', 'Provence-Alpes-Côte d\\'Azur'],\n streets: ['rue de la Paix', 'avenue des Champs-Élysées', 'boulevard Saint-Germain'],\n zipPattern: '#####',\n },\n};\n\nconst illusion = createIllusion({ seed: 42, locale: fr });\n\nillusion.person.fullName(); // 'Camille Dubois'\nillusion.location.city(); // 'Marseille'\nillusion.person.jobTitle(); // 'marketing ingénieur'\n```\n\n`date.weekday()` and `date.month()` currently ship English and German name arrays only; a custom locale code falls through to the English set. For other languages, format a generated `Temporal` date with `@vielzeug/tempo`'s `format()` and your own `Intl.DateTimeFormat` options.\n\nUse `satisfies IllusionistLocale` instead of a bare type annotation to get error locality — TypeScript points at the offending field rather than the whole object.\n\n## Category Overview\n\n| Category | Example call | Returns |\n| --- |-------------------------------------| --- |\n| `person` | `illusion.person.fullName()` | `string` |\n| `internet` | `illusion.internet.email()` | `string` |\n| `commerce` | `illusion.commerce.price()` | `Money` (coins) |\n| `date` | `illusion.date.past({ years: 1 })` | `Temporal.ZonedDateTime` (tempo) |\n| `finance` | `illusion.finance.iban()` | `string` |\n| `location` | `illusion.location.streetAddress()` | `string` |\n| `lorem` | `illusion.lorem.paragraph()` | `string` |\n| `system` | `illusion.system.uuid()` | `string` |\n\n## Working with Other Vielzeug Libraries\n\nIllusionist integrates with other Vielzeug packages at the return-type level. `commerce.price()` and `finance.amount()` return coins `Money`, so you can format, add, or allocate them directly. `date` functions return tempo `Temporal` objects, so you can shift, compare, or format them.\n\n```ts\nimport { format, add, money } from '@vielzeug/coins';\nimport { createIllusion } from '@vielzeug/illusionist';\nimport { en } from '@vielzeug/illusionist/locales';\nimport { formatZonedDateTimeISO } from '@vielzeug/tempo';\n\nconst illusion = createIllusion({ seed: 42, locale: en });\n\nconst price = illusion.commerce.price({ min: 10, max: 50, currency: 'EUR' });\nconst tax = money('5.00', price.currency);\nconst total = add(price, tax);\n\nconsole.log(format(total, { locale: 'de-DE' }));\n\nconst orderDate = illusion.date.recent({ days: 30 });\nconsole.log(formatZonedDateTimeISO(orderDate));\n```\n\n## Best Practices\n\n- Pass a seed in tests and CI; omit it only for one-off non-reproducible mocks.\n- Create one instance per test case so each test starts from a known random state.\n- Call `dispose()` (or use `using`) when an instance is no longer needed, especially in long-running processes.\n- Fix the locale at creation time; create a new instance to switch locales rather than mixing.\n- Use string seeds for named test suites — they are self-documenting and hash to a stable number.\n- Combine `person`, `internet`, and `location` to build internally consistent mock entities.\n- Treat `Money` and `Temporal` return values as first-class — pass them to coins and tempo functions directly.\n- Avoid sharing a single instance across concurrent async tasks; each call advances the shared random source.\n- `system.uuid()` uses `crypto.randomUUID()`, not the seeded source. Do not use it in deterministic fixtures or snapshot tests.\n",
|
|
7
|
-
"examples": "---\ntitle: Illusionist — Examples\ndescription: Practical examples and recipes for @vielzeug/illusionist.\n---\n\n[[toc]]\n\n## Generating Test Fixtures\n\nBuild a batch of realistic records from a fixed seed. The same seed reproduces the same fixtures in every run. For a full Vitest setup, see the [Test Fixtures recipe](./examples/test-fixtures.md).\n\n```ts\nimport { createIllusion } from '@vielzeug/illusionist';\nimport { en } from '@vielzeug/illusionist/locales';\n\nconst illusion = createIllusion({ seed: 'fixtures-v1', locale: en });\n\nconst users = Array.from({ length: 10 }, () => ({\n name: illusion.person.fullName(),\n email: illusion.internet.email(),\n address: illusion.location.streetAddress(),\n city: illusion.location.city(),\n zip: illusion.location.zipCode(),\n}));\n\nillusion.dispose();\n```\n\n## Seeded Test Data for Snapshot Testing\n\nUse a named string seed so snapshot output is stable across CI runs. Each test creates its own instance to avoid cross-test random-state drift.\n\n```ts\nimport { createIllusion } from '@vielzeug/illusionist';\nimport { en } from '@vielzeug/illusionist/locales';\n\ntest('order receipt snapshot', () => {\n const illusion = createIllusion({ seed: 'order-receipt', locale: en });\n\n const order = {\n customer: illusion.person.fullName(),\n product: illusion.commerce.productName(),\n price: illusion.commerce.price({ min: 5, max: 50 }),\n date: illusion.date.recent({ days: 7 }),\n };\n\n expect(order).toMatchInlineSnapshot();\n illusion.dispose();\n});\n```\n\n## Locale-Specific Data (German)\n\nImport the German locale object to draw names, cities, and weekday labels from its dataset.\n\n```ts\nimport { createIllusion } from '@vielzeug/illusionist';\nimport { de } from '@vielzeug/illusionist/locales';\n\nconst illusion = createIllusion({ seed: 42, locale: de });\n\nillusion.person.fullName(); // 'Mathilda Scholz'\nillusion.location.city(); // 'Nürnberg'\nillusion.location.zipCode(); // '15268'\nillusion.date.weekday(); // 'Donnerstag'\nillusion.date.month(); // 'März'\n\nillusion.dispose();\n```\n\n## E-commerce Mock Data\n\nCombine `person`, `commerce`, and `location` to build a consistent customer-order-shipping record. `commerce.price()` returns coins `Money`, so you can format it directly.\n\n```ts\nimport { format } from '@vielzeug/coins';\nimport { createIllusion } from '@vielzeug/illusionist';\nimport { en } from '@vielzeug/illusionist/locales';\n\nconst illusion = createIllusion({ seed: 'ecommerce-mock', locale: en });\n\nconst order = {\n customer: {\n name: illusion.person.fullName(),\n email: illusion.internet.email(),\n },\n item: illusion.commerce.productName(),\n price: illusion.commerce.price({ min: 20, max: 200, currency: 'EUR' }),\n shipping: {\n address: illusion.location.streetAddress(),\n city: illusion.location.city(),\n zip: illusion.location.zipCode(),\n country: illusion.location.country(),\n },\n};\n\nconsole.log(format(order.price, { locale: 'de-DE' }));\nillusion.dispose();\n```\n\n## Database Seeding Pattern\n\nGenerate rows for a database seed script. Use a stable seed so the seed file is reproducible and reviewable.\n\n```ts\nimport { createIllusion } from '@vielzeug/illusionist';\nimport { en } from '@vielzeug/illusionist/locales';\n\nconst illusion = createIllusion({ seed: 'db-seed-2024', locale: en });\n\nconst products = Array.from({ length: 50 }, () => ({\n name: illusion.commerce.productName(),\n department: illusion.commerce.department(),\n price: illusion.commerce.price({ min: 1, max: 500 }),\n description: illusion.commerce.productDescription(),\n}));\n\nconst customers = Array.from({ length: 100 }, () => ({\n firstName: illusion.person.firstName(),\n lastName: illusion.person.lastName(),\n email: illusion.internet.email(),\n createdAt: illusion.date.past({ years: 2 }),\n}));\n\nillusion.dispose();\n```\n\n## Disposal in Long-Running Processes\n\nCall `dispose()` when an instance is no longer needed. In long-running processes, use `using` to release instances automatically at scope exit.\n\n```ts\nimport { createIllusion } from '@vielzeug/illusionist';\nimport { en } from '@vielzeug/illusionist/locales';\n\nfunction generateBatch(seed: number) {\n using illusion = createIllusion({ seed, locale: en });\n\n return Array.from({ length: 5 }, () => ({\n name: illusion.person.fullName(),\n email: illusion.internet.email(),\n }));\n // illusion.dispose() runs automatically at scope exit\n}\n```\n"
|
|
8
|
-
},
|
|
9
|
-
"examples": [
|
|
10
|
-
{
|
|
11
|
-
"id": "commerce-basic",
|
|
12
|
-
"code": "import { createIllusion } from '@vielzeug/illusionist'\nimport { en } from '@vielzeug/illusionist/locales'\n\nconst illusion = createIllusion({ seed: 12345, locale: en })\n\nconsole.log(illusion.commerce.productName())\nconsole.log(illusion.commerce.department())\nconsole.log(illusion.commerce.price({ min: 10, max: 50, currency: 'EUR' }))\nconsole.log(illusion.commerce.productDescription())",
|
|
13
|
-
"name": "commerce - Products, departments, and prices"
|
|
14
|
-
},
|
|
15
|
-
{
|
|
16
|
-
"id": "date-basic",
|
|
17
|
-
"code": "import { createIllusion } from '@vielzeug/illusionist'\nimport { en } from '@vielzeug/illusionist/locales'\n\nconst illusion = createIllusion({ seed: 12345, locale: en })\n\nconsole.log(illusion.date.past({ years: 2 }).toString())\nconsole.log(illusion.date.future({ years: 1 }).toString())\nconsole.log(illusion.date.recent({ days: 7 }).toString())\nconsole.log(illusion.date.birthday({ minAge: 25, maxAge: 35 }).toString())\nconsole.log(illusion.date.weekday())\nconsole.log(illusion.date.month())",
|
|
18
|
-
"name": "date - Past, future, birthdays, and locale labels"
|
|
19
|
-
},
|
|
20
|
-
{
|
|
21
|
-
"id": "determinism-basic",
|
|
22
|
-
"code": "import { createIllusion } from '@vielzeug/illusionist'\nimport { en } from '@vielzeug/illusionist/locales'\n\nconst a = createIllusion({ seed: 'test-fixture', locale: en })\nconst b = createIllusion({ seed: 'test-fixture', locale: en })\n\nconsole.log(a.person.fullName() === b.person.fullName())\nconsole.log(a.internet.email() === b.internet.email())\n\na.dispose()\nb.dispose()",
|
|
23
|
-
"name": "seed - Deterministic output from the same seed"
|
|
24
|
-
},
|
|
25
|
-
{
|
|
26
|
-
"id": "finance-basic",
|
|
27
|
-
"code": "import { createIllusion } from '@vielzeug/illusionist'\nimport { en } from '@vielzeug/illusionist/locales'\n\nconst illusion = createIllusion({ seed: 12345, locale: en })\n\nconsole.log(illusion.finance.iban())\nconsole.log(illusion.finance.iban('DE'))\nconsole.log(illusion.finance.bic())\nconsole.log(illusion.finance.creditCardNumber('visa'))\nconsole.log(illusion.finance.creditCardCVV('amex'))\nconsole.log(illusion.finance.ethereumAddress())",
|
|
28
|
-
"name": "finance - IBANs, cards, BICs, and crypto addresses"
|
|
29
|
-
},
|
|
30
|
-
{
|
|
31
|
-
"id": "internet-basic",
|
|
32
|
-
"code": "import { createIllusion } from '@vielzeug/illusionist'\nimport { en } from '@vielzeug/illusionist/locales'\n\nconst illusion = createIllusion({ seed: 12345, locale: en })\n\nconsole.log(illusion.internet.email())\nconsole.log(illusion.internet.username())\nconsole.log(illusion.internet.url())\nconsole.log(illusion.internet.ip())\nconsole.log(illusion.internet.ip(6))\nconsole.log(illusion.internet.mac())",
|
|
33
|
-
"name": "internet - Emails, URLs, and network addresses"
|
|
34
|
-
},
|
|
35
|
-
{
|
|
36
|
-
"id": "locale-basic",
|
|
37
|
-
"code": "import { createIllusion } from '@vielzeug/illusionist'\nimport { en, de } from '@vielzeug/illusionist/locales'\n\nconst illusion = {\n en: createIllusion({ seed: 42, locale: en }),\n de: createIllusion({ seed: 42, locale: de })\n};\n\nconsole.log(illusion.en.person.fullName())\nconsole.log(illusion.de.person.fullName())\nconsole.log(illusion.en.location.city())\nconsole.log(illusion.de.location.city())\nconsole.log(illusion.en.date.month())\nconsole.log(illusion.de.date.month())",
|
|
38
|
-
"name": "locales - English and German side-by-side"
|
|
39
|
-
},
|
|
40
|
-
{
|
|
41
|
-
"id": "location-basic",
|
|
42
|
-
"code": "import { createIllusion } from '@vielzeug/illusionist'\nimport { en } from '@vielzeug/illusionist/locales'\n\nconst illusion = createIllusion({ seed: 12345, locale: en })\n\nconsole.log(illusion.location.city())\nconsole.log(illusion.location.streetAddress())\nconsole.log(illusion.location.zipCode())\nconsole.log(illusion.location.state())\nconsole.log(illusion.location.country())\nconsole.log(illusion.location.latitude())\nconsole.log(illusion.location.longitude())",
|
|
43
|
-
"name": "location - Addresses, regions, and coordinates"
|
|
44
|
-
},
|
|
45
|
-
{
|
|
46
|
-
"id": "person-basic",
|
|
47
|
-
"code": "import { createIllusion } from '@vielzeug/illusionist'\nimport { en } from '@vielzeug/illusionist/locales'\n\nconst illusion = createIllusion({ seed: 12345, locale: en })\n\nconsole.log(illusion.person.fullName())\nconsole.log(illusion.person.firstName())\nconsole.log(illusion.person.lastName())\nconsole.log(illusion.person.jobTitle())\nconsole.log(illusion.person.gender())",
|
|
48
|
-
"name": "person - Names, gender, and job titles"
|
|
49
|
-
},
|
|
50
|
-
{
|
|
51
|
-
"id": "system-basic",
|
|
52
|
-
"code": "import { createIllusion } from '@vielzeug/illusionist'\nimport { en } from '@vielzeug/illusionist/locales'\n\nconst illusion = createIllusion({ seed: 12345, locale: en })\n\nconsole.log(illusion.system.filePath())\nconsole.log(illusion.system.semver({ includePrerelease: true }))\nconsole.log(illusion.system.uuid())\nconsole.log(illusion.system.port())\nconsole.log(illusion.system.cron())\nconsole.log(illusion.system.process())",
|
|
53
|
-
"name": "system - Files, semver, UUIDs, ports, and cron"
|
|
54
|
-
}
|
|
55
|
-
],
|
|
56
|
-
"typeSignatures": {
|
|
57
|
-
"PriceOptions": "export type PriceOptions = {\n readonly min?: number;\n readonly max?: number;\n readonly currency?: 'USD' | 'EUR' | 'GBP';\n};",
|
|
58
|
-
"productAdjective": "export function productAdjective(ctx: IllusionistContext): string {\n return pick(COMMERCE_DATA.productAdjectives, ctx.source)!;\n}",
|
|
59
|
-
"productMaterial": "export function productMaterial(ctx: IllusionistContext): string {\n return pick(COMMERCE_DATA.productMaterials, ctx.source)!;\n}",
|
|
60
|
-
"productNoun": "export function productNoun(ctx: IllusionistContext): string {\n return pick(COMMERCE_DATA.productNouns, ctx.source)!;\n}",
|
|
61
|
-
"productName": "export function productName(ctx: IllusionistContext): string {\n return `${productAdjective(ctx)} ${productMaterial(ctx)} ${productNoun(ctx)}`;\n}",
|
|
62
|
-
"department": "export function department(ctx: IllusionistContext): string {\n return pick(COMMERCE_DATA.departments, ctx.source)!;\n}",
|
|
63
|
-
"price": "export function price(ctx: IllusionistContext, opts: PriceOptions = {}): Money {\n const min = opts.min ?? 0.01;\n const max = opts.max ?? 1000;\n const code = opts.currency ?? 'USD';\n const amount = floatFixed(min, max, 2, ctx.source);\n\n return money(amount.toFixed(2), CURRENCIES[code]);\n}",
|
|
64
|
-
"productDescription": "export function productDescription(ctx: IllusionistContext): string {\n const sentences = int(1, 2, ctx.source);\n const parts: string[] = [];\n\n for (let i = 0; i < sentences; i++) {\n const adjective = pick(COMMERCE_DATA.productAdjectives, ctx.source)!;\n const material = pick(COMMERCE_DATA.productMaterials, ctx.source)!;\n const noun = pick(COMMERCE_DATA.productNouns, ctx.source)!;\n const department = pick(COMMERCE_DATA.departments, ctx.source)!;\n\n parts.push(\n `The ${adjective.toLowerCase()} ${material.toLowerCase()} ${noun.toLowerCase()} is a great choice for your ${department.toLowerCase()} needs.`,\n );\n }\n\n return parts.join(' ');\n}",
|
|
65
|
-
"past": "export function past(ctx: IllusionistContext, options?: DateOptions): Temporal.ZonedDateTime {\n const ref = resolveRef(options?.ref);\n const years = options?.years ?? 1;\n const maxSeconds = years * 365 * 24 * 60 * 60;\n const minSeconds = 1;\n\n return shift(ref, { seconds: -int(minSeconds, maxSeconds, ctx.source) });\n}",
|
|
66
|
-
"future": "export function future(ctx: IllusionistContext, options?: DateOptions): Temporal.ZonedDateTime {\n const ref = resolveRef(options?.ref);\n const years = options?.years ?? 1;\n const maxSeconds = years * 365 * 24 * 60 * 60;\n\n return shift(ref, { seconds: int(1, maxSeconds, ctx.source) });\n}",
|
|
67
|
-
"recent": "export function recent(\n ctx: IllusionistContext,\n options?: { days?: number; ref?: Temporal.ZonedDateTime },\n): Temporal.ZonedDateTime {\n const ref = resolveRef(options?.ref);\n const days = options?.days ?? 1;\n const maxSeconds = days * 24 * 60 * 60;\n\n return shift(ref, { seconds: -int(1, maxSeconds, ctx.source) });\n}",
|
|
68
|
-
"between": "export function between(\n ctx: IllusionistContext,\n from: Temporal.ZonedDateTime,\n to: Temporal.ZonedDateTime,\n): Temporal.ZonedDateTime {\n const fromNs = from.toInstant().epochNanoseconds;\n const toNs = to.toInstant().epochNanoseconds;\n\n if (fromNs > toNs) {\n return from;\n }\n\n const spanNs = toNs - fromNs;\n const spanSeconds = Number(spanNs / 1_000_000_000n);\n const offsetSeconds = Math.floor(ctx.source.next() * spanSeconds);\n\n return from.add({ seconds: offsetSeconds });\n}",
|
|
69
|
-
"birthday": "export function birthday(\n ctx: IllusionistContext,\n options?: { minAge?: number; maxAge?: number; ref?: Temporal.ZonedDateTime },\n): Temporal.PlainDate {\n const ref = resolveRef(options?.ref);\n const minAge = options?.minAge ?? 18;\n const maxAge = options?.maxAge ?? 80;\n const age = int(minAge, maxAge, ctx.source);\n const month = int(1, 12, ctx.source);\n const day = int(1, 28, ctx.source);\n\n return ref.toPlainDate().subtract({ years: age }).with({ day, month });\n}",
|
|
70
|
-
"weekday": "export function weekday(ctx: IllusionistContext, locale?: string): string {\n const days = ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday'];\n const loc = locale ?? ctx.locale.code;\n const localized =\n loc === 'de' ? ['Montag', 'Dienstag', 'Mittwoch', 'Donnerstag', 'Freitag', 'Samstag', 'Sonntag'] : days;\n\n return localized[int(0, 6, ctx.source)]!;\n}",
|
|
71
|
-
"month": "export function month(ctx: IllusionistContext, locale?: string): string {\n const months = [\n 'January',\n 'February',\n 'March',\n 'April',\n 'May',\n 'June',\n 'July',\n 'August',\n 'September',\n 'October',\n 'November',\n 'December',\n ];\n const loc = locale ?? ctx.locale.code;\n const localized =\n loc === 'de'\n ? [\n 'Januar',\n 'Februar',\n 'März',\n 'April',\n 'Mai',\n 'Juni',\n 'Juli',\n 'August',\n 'September',\n 'Oktober',\n 'November',\n 'Dezember',\n ]\n : months;\n\n return localized[int(0, 11, ctx.source)]!;\n}",
|
|
72
|
-
"IllusionistError": "export class IllusionistError extends Error {\n constructor(message: string, opts?: ErrorOptions) {\n super(message, opts);\n this.name = new.target.name;\n Object.setPrototypeOf(this, new.target.prototype);\n }\n}",
|
|
73
|
-
"IllusionistSeedError": "export class IllusionistSeedError extends IllusionistError {}",
|
|
74
|
-
"IllusionistOptions": "export type { IllusionistOptions } from './types';\n\nexport type IllusionistOptions = {\n /** Numeric or string seed for deterministic output. Omit for cryptographic randomness. */\n seed?: number | string;\n /** Locale data for locale-aware categories. */\n locale: IllusionistLocale;\n};",
|
|
75
|
-
"Illusionist": "export type Illusionist = {\n readonly person: BoundApi<typeof personApi>;\n readonly internet: BoundApi<typeof internetApi>;\n readonly commerce: BoundApi<typeof commerceApi>;\n readonly date: BoundApi<typeof dateApi>;\n readonly finance: BoundApi<typeof financeApi>;\n readonly location: BoundApi<typeof locationApi>;\n readonly lorem: BoundApi<typeof loremApi>;\n readonly system: BoundApi<typeof systemApi>;\n\n /** The seed used to initialize this instance, or `undefined` for cryptographic randomness. */\n readonly seed: number | string | undefined;\n /** The active locale data. */\n readonly locale: IllusionistLocale;\n\n dispose(): void;\n readonly disposed: boolean;\n readonly disposalSignal: AbortSignal;\n [Symbol.dispose](): void;\n};",
|
|
76
|
-
"createIllusion": "export function createIllusion(options: IllusionistOptions): Illusionist {\n const { locale, seed } = options;\n const source: RandomSource = createSeed(seed);\n const controller = new AbortController();\n let disposed = false;\n\n const ctx: IllusionistContext = { locale, source };\n\n const bind = <T extends Record<string, (ctx: IllusionistContext, ...args: never[]) => unknown>>(\n api: T,\n ): BoundApi<T> => {\n const bound = {} as Record<string, (...args: never[]) => unknown>;\n\n for (const [key, fn] of Object.entries(api)) {\n if (typeof fn === 'function') {\n bound[key] = (...args: never[]) => (fn as (ctx: IllusionistContext, ...args: never[]) => unknown)(ctx, ...args);\n }\n }\n\n return bound as unknown as BoundApi<T>;\n };\n\n const dispose = (): void => {\n if (disposed) return;\n\n disposed = true;\n controller.abort();\n };\n\n return {\n commerce: bind(commerceApi),\n date: bind(dateApi),\n get disposalSignal(): AbortSignal {\n return controller.signal;\n },\n dispose,\n get disposed(): boolean {\n return disposed;\n },\n finance: bind(financeApi),\n internet: bind(internetApi),\n locale,\n location: bind(locationApi),\n lorem: bind(loremApi),\n person: bind(personApi),\n seed,\n system: bind(systemApi),\n [Symbol.dispose]: dispose,\n };\n}",
|
|
77
|
-
"AmountOptions": "export type AmountOptions = {\n readonly min?: number;\n readonly max?: number;\n readonly currency?: 'USD' | 'EUR' | 'GBP';\n};",
|
|
78
|
-
"amount": "export function amount(ctx: IllusionistContext, opts: AmountOptions = {}): Money {\n const min = opts.min ?? 100;\n const max = opts.max ?? 10000;\n const code = opts.currency ?? 'USD';\n const value = floatFixed(min, max, 2, ctx.source);\n\n return money(value.toFixed(2), CURRENCIES[code]);\n}",
|
|
79
|
-
"iban": "export function iban(ctx: IllusionistContext, countryCode?: string): string {\n const country = (countryCode ?? pick(IBAN_COUNTRIES, ctx.source)!) as keyof typeof FINANCE_DATA.ibanLengths;\n\n if (!(country in FINANCE_DATA.ibanLengths)) {\n throw new RangeError(`iban: unsupported country code \"${countryCode}\". Supported: ${IBAN_COUNTRIES.join(', ')}`);\n }\n\n const totalLength = FINANCE_DATA.ibanLengths[country];\n const bbanLength = totalLength - 4;\n const bban = numericString(bbanLength, ctx.source);\n const checkDigits = ibanCheckDigits(country, bban);\n\n return `${country}${checkDigits}${bban}`;\n}",
|
|
80
|
-
"bic": "export function bic(ctx: IllusionistContext): string {\n const bankCode = letters(4, ctx);\n const country = letters(2, ctx);\n const location = alphanumeric(2, ctx.source).toUpperCase();\n const useBranch = int(0, 1, ctx.source) === 1;\n const branch = useBranch ? alphanumeric(3, ctx.source).toUpperCase() : '';\n\n return `${bankCode}${country}${location}${branch}`;\n}",
|
|
81
|
-
"creditCardNumber": "export function creditCardNumber(ctx: IllusionistContext, type?: CreditCardType): string {\n const cardType = type ?? pick(CREDIT_CARD_TYPES, ctx.source)!;\n const iin = pick(FINANCE_DATA.creditCardIins[cardType], ctx.source)!;\n const totalLength = cardType === 'amex' ? 15 : 16;\n const partial = iin + numericString(totalLength - iin.length - 1, ctx.source);\n const checkDigit = luhnCheckDigit(partial);\n\n return `${partial}${checkDigit}`;\n}",
|
|
82
|
-
"creditCardCVV": "export function creditCardCVV(ctx: IllusionistContext, type?: CreditCardType): string {\n const cardType = type ?? pick(CREDIT_CARD_TYPES, ctx.source)!;\n const length = cardType === 'amex' ? 4 : 3;\n\n return numericString(length, ctx.source);\n}",
|
|
83
|
-
"bitcoinAddress": "export function bitcoinAddress(ctx: IllusionistContext): string {\n const prefix = pick(['1', '3', 'bc1'], ctx.source)!;\n const suffixLength = int(25, 34, ctx.source);\n\n return `${prefix}${base58String(suffixLength, ctx.source)}`;\n}",
|
|
84
|
-
"ethereumAddress": "export function ethereumAddress(ctx: IllusionistContext): string {\n return `0x${hexString(40, ctx.source)}`;\n}",
|
|
85
|
-
"transactionType": "export function transactionType(ctx: IllusionistContext): string {\n return pick(FINANCE_DATA.transactionTypes, ctx.source)!;\n}",
|
|
86
|
-
"bank": "export function bank(ctx: IllusionistContext): string {\n return pick(FINANCE_DATA.banks, ctx.source)!;\n}",
|
|
87
|
-
"email": "export function email(ctx: IllusionistContext): string {\n const first = (\n pick(ctx.locale.person.firstNameFemale, ctx.source) ??\n pick(ctx.locale.person.firstNameMale, ctx.source) ??\n 'user'\n ).toLowerCase();\n const last = (pick(ctx.locale.person.lastName, ctx.source) ?? 'name').toLowerCase();\n const domain = pick(INTERNET_DATA.domains, ctx.source) ?? 'example';\n const tld = pick(INTERNET_DATA.tlds, ctx.source) ?? 'com';\n return `${first}.${last}@${domain}.${tld}`;\n}",
|
|
88
|
-
"username": "export function username(ctx: IllusionistContext): string {\n if (int(0, 1, ctx.source) === 0) {\n return alphanumeric(int(6, 12, ctx.source), ctx.source);\n }\n const first = (\n pick(ctx.locale.person.firstNameFemale, ctx.source) ??\n pick(ctx.locale.person.firstNameMale, ctx.source) ??\n 'user'\n ).toLowerCase();\n const last = (pick(ctx.locale.person.lastName, ctx.source) ?? 'name').toLowerCase();\n return `${first}.${last}`;\n}",
|
|
89
|
-
"PasswordOptions": "export type PasswordOptions = {\n /** Password length. Defaults to `12`. */\n length?: number;\n /** When `true`, builds a memorable password from name fragments and digits. */\n memorable?: boolean;\n};",
|
|
90
|
-
"password": "export function password(ctx: IllusionistContext, opts: PasswordOptions = {}): string {\n const length = opts.length ?? 12;\n\n if (opts.memorable) {\n const first = (\n pick(ctx.locale.person.firstNameFemale, ctx.source) ??\n pick(ctx.locale.person.firstNameMale, ctx.source) ??\n 'user'\n ).toLowerCase();\n const last = (pick(ctx.locale.person.lastName, ctx.source) ?? 'name').toLowerCase();\n const num = String(int(10, 99, ctx.source));\n const special = pick(SPECIAL_CHARS.split(''), ctx.source) ?? '!';\n const base = `${first}${last}${num}${special}`;\n if (base.length <= length) return base + alphanumeric(length - base.length, ctx.source);\n // Truncation would cut the special char/number — put them first, then fill from the name.\n const essential = `${num}${special}`;\n const remaining = length - essential.length;\n const namePart = remaining > 0 ? (first + last).slice(0, remaining) : '';\n const result = essential + namePart;\n\n return result.length < length ? result + alphanumeric(length - result.length, ctx.source) : result.slice(0, length);\n }\n\n const upper = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';\n const lower = 'abcdefghijklmnopqrstuvwxyz';\n const digits = '0123456789';\n const pools = [upper, lower, digits, SPECIAL_CHARS];\n\n // Guarantee at least one char from each pool, then fill the rest randomly.\n const chars: string[] = [];\n for (const pool of pools) {\n chars.push(pool[Math.floor(ctx.source.next() * pool.length)] ?? upper[0]!);\n }\n const all = upper + lower + digits + SPECIAL_CHARS;\n while (chars.length < length) {\n chars.push(all[Math.floor(ctx.source.next() * all.length)] ?? 'a');\n }\n\n // Shuffle via Fisher-Yates using ctx.source.\n for (let i = chars.length - 1; i > 0; i--) {\n const j = Math.floor(ctx.source.next() * (i + 1));\n [chars[i], chars[j]] = [chars[j]!, chars[i]!];\n }\n\n return chars.slice(0, length).join('');\n}",
|
|
91
|
-
"url": "export function url(ctx: IllusionistContext): string {\n const protocol = pick(INTERNET_DATA.protocols, ctx.source) ?? 'https';\n const sub = pick(['www', 'api', 'app', 'mail', 'static', 'cdn'], ctx.source) ?? 'www';\n const domain = pick(INTERNET_DATA.domains, ctx.source) ?? 'example';\n const tld = pick(INTERNET_DATA.tlds, ctx.source) ?? 'com';\n const host = `${sub}.${domain}.${tld}`;\n\n const segmentCount = int(1, 4, ctx.source);\n const segments: string[] = [];\n for (let i = 0; i < segmentCount; i++) {\n const word = pick(URL_PATH_WORDS, ctx.source) ?? 'api';\n // Occasionally append a numeric or short alphanumeric suffix to a segment.\n if (int(0, 2, ctx.source) === 0) {\n segments.push(`${word}-${alphanumeric(int(2, 5), ctx.source).toLowerCase()}`);\n } else {\n segments.push(word);\n }\n }\n\n return `${protocol}://${host}/${segments.join('/')}`;\n}",
|
|
92
|
-
"domainName": "export function domainName(ctx: IllusionistContext): string {\n const domain = pick(INTERNET_DATA.domains, ctx.source) ?? 'example';\n const tld = pick(INTERNET_DATA.tlds, ctx.source) ?? 'com';\n return `${domain}.${tld}`;\n}",
|
|
93
|
-
"ip": "export function ip(ctx: IllusionistContext, version: 4 | 6 = 4): string {\n if (version === 6) {\n const groups: string[] = [];\n for (let i = 0; i < 8; i++) {\n groups.push(hexString(4, ctx.source));\n }\n return groups.join(':');\n }\n const octets: number[] = [];\n for (let i = 0; i < 4; i++) {\n octets.push(int(0, 255, ctx.source));\n }\n return octets.join('.');\n}",
|
|
94
|
-
"mac": "export function mac(ctx: IllusionistContext): string {\n const parts: string[] = [];\n for (let i = 0; i < 6; i++) {\n parts.push(hexString(2, ctx.source));\n }\n return parts.join(':');\n}",
|
|
95
|
-
"userAgent": "export function userAgent(ctx: IllusionistContext): string {\n return pick(INTERNET_DATA.userAgents, ctx.source) ?? INTERNET_DATA.userAgents[0]!;\n}",
|
|
96
|
-
"httpMethod": "export function httpMethod(ctx: IllusionistContext): string {\n return pick(INTERNET_DATA.httpMethods, ctx.source) ?? 'GET';\n}",
|
|
97
|
-
"statusCode": "export function statusCode(ctx: IllusionistContext): number {\n return pick(INTERNET_DATA.statusCodes, ctx.source) ?? 200;\n}",
|
|
98
|
-
"mimeType": "export function mimeType(ctx: IllusionistContext): string {\n return pick(INTERNET_DATA.mimeTypes, ctx.source) ?? 'text/plain';\n}",
|
|
99
|
-
"Coordinate": "export type Coordinate = {\n lat: number;\n lng: number;\n};",
|
|
100
|
-
"city": "export function city(ctx: IllusionistContext): string {\n return pick(data(ctx).cities, ctx.source)!;\n}",
|
|
101
|
-
"street": "export function street(ctx: IllusionistContext): string {\n return pick(data(ctx).streets, ctx.source)!;\n}",
|
|
102
|
-
"streetAddress": "export function streetAddress(ctx: IllusionistContext): string {\n const houseNumber = Math.floor(float(1, 1000, ctx.source));\n return `${houseNumber} ${street(ctx)}`;\n}",
|
|
103
|
-
"zipCode": "export function zipCode(ctx: IllusionistContext): string {\n const pattern = data(ctx).zipPattern;\n\n return pattern.replace(/#/g, () => String(Math.floor((ctx.source.next() ?? 0) * 10)));\n}",
|
|
104
|
-
"state": "export function state(ctx: IllusionistContext): string {\n return pick(data(ctx).states, ctx.source)!;\n}",
|
|
105
|
-
"country": "export function country(ctx: IllusionistContext): string {\n return pick(data(ctx).countries, ctx.source)!;\n}",
|
|
106
|
-
"latitude": "export function latitude(ctx: IllusionistContext): number {\n return float(-90, 90, ctx.source);\n}",
|
|
107
|
-
"longitude": "export function longitude(ctx: IllusionistContext): number {\n return float(-180, 180, ctx.source);\n}",
|
|
108
|
-
"nearbyGPSCoordinate": "export function nearbyGPSCoordinate(ctx: IllusionistContext, ref?: Coordinate): Coordinate {\n const base = ref ?? { lat: latitude(ctx), lng: longitude(ctx) };\n return {\n lat: float(base.lat - 1, base.lat + 1, ctx.source),\n lng: float(base.lng - 1, base.lng + 1, ctx.source),\n };\n}",
|
|
109
|
-
"word": "export function word(ctx: IllusionistContext): string {\n return pick(LOREM_DATA.words, ctx.source)!;\n}",
|
|
110
|
-
"words": "export function words(ctx: IllusionistContext, count = 3): string {\n const out: string[] = [];\n for (let i = 0; i < count; i++) {\n out.push(word(ctx));\n }\n return out.join(' ');\n}",
|
|
111
|
-
"sentence": "export function sentence(ctx: IllusionistContext, wordCount?: number): string {\n const count = wordCount ?? Math.floor(float(6, 13, ctx.source));\n const text = words(ctx, count);\n return `${text.charAt(0).toUpperCase()}${text.slice(1)}.`;\n}",
|
|
112
|
-
"sentences": "export function sentences(ctx: IllusionistContext, count = 3): string {\n const out: string[] = [];\n for (let i = 0; i < count; i++) {\n out.push(sentence(ctx));\n }\n return out.join(' ');\n}",
|
|
113
|
-
"paragraph": "export function paragraph(ctx: IllusionistContext, sentenceCount?: number): string {\n const count = sentenceCount ?? Math.floor(float(3, 8, ctx.source));\n return sentences(ctx, count);\n}",
|
|
114
|
-
"paragraphs": "export function paragraphs(ctx: IllusionistContext, count = 3): string {\n const out: string[] = [];\n for (let i = 0; i < count; i++) {\n out.push(paragraph(ctx));\n }\n return out.join('\\n');\n}",
|
|
115
|
-
"slug": "export function slug(ctx: IllusionistContext, wordCount = 3): string {\n const out: string[] = [];\n for (let i = 0; i < wordCount; i++) {\n out.push(word(ctx));\n }\n return out.join('-');\n}",
|
|
116
|
-
"lines": "export function lines(ctx: IllusionistContext, count = 5): string {\n const out: string[] = [];\n for (let i = 0; i < count; i++) {\n out.push(sentence(ctx));\n }\n return out.join('\\n');\n}",
|
|
117
|
-
"firstName": "export function firstName(ctx: IllusionistContext): string {\n const d = data(ctx);\n const pool = boolean(ctx.source) ? d.firstNameMale : d.firstNameFemale;\n return pick(pool, ctx.source)!;\n}",
|
|
118
|
-
"lastName": "export function lastName(ctx: IllusionistContext): string {\n return pick(data(ctx).lastName, ctx.source)!;\n}",
|
|
119
|
-
"fullName": "export function fullName(ctx: IllusionistContext): string {\n return `${firstName(ctx)} ${lastName(ctx)}`;\n}",
|
|
120
|
-
"gender": "export function gender(ctx: IllusionistContext): string {\n return pick(data(ctx).gender, ctx.source)!;\n}",
|
|
121
|
-
"prefix": "export function prefix(ctx: IllusionistContext): string {\n return pick(data(ctx).prefix, ctx.source)!;\n}",
|
|
122
|
-
"suffix": "export function suffix(ctx: IllusionistContext): string {\n const d = data(ctx);\n if (d.suffix.length === 0) return '';\n return pick(d.suffix, ctx.source)!;\n}",
|
|
123
|
-
"jobTitle": "export function jobTitle(ctx: IllusionistContext): string {\n const d = data(ctx);\n return `${pick(d.jobAreas, ctx.source)!} ${pick(d.jobTypes, ctx.source)!}`;\n}",
|
|
124
|
-
"createSeed": "export function createSeed(seed?: number | string): RandomSource {\n if (seed == null) return cryptoSource();\n\n if (typeof seed === 'number') {\n if (!Number.isFinite(seed)) throw new IllusionistSeedError(`createSeed: numeric seed must be finite, got ${seed}`);\n\n return mulberry32(Math.trunc(seed));\n }\n\n const hashed = hash(seed);\n\n // FNV-1a-ish fold from the hash string into a 32-bit integer.\n let state = 0;\n\n for (let i = 0; i < hashed.length; i++) {\n state = (Math.imul(state, 31) + hashed.charCodeAt(i)) >>> 0;\n }\n\n return mulberry32(state);\n}",
|
|
125
|
-
"mulberry32": "export function mulberry32(seed: number): RandomSource {\n let state = seed >>> 0;\n\n return {\n next(): number {\n state = (state + 0x6d2b79f5) >>> 0;\n let t = state;\n\n t = Math.imul(t ^ (t >>> 15), t | 1);\n t ^= t + Math.imul(t ^ (t >>> 7), t | 61);\n\n return ((t ^ (t >>> 14)) >>> 0) / 4294967296;\n },\n };\n}",
|
|
126
|
-
"PersonLocaleData": "export type PersonLocaleData = {\n readonly firstNameFemale: readonly string[];\n readonly firstNameMale: readonly string[];\n readonly gender: readonly string[];\n readonly jobAreas: readonly string[];\n readonly jobTypes: readonly string[];\n readonly lastName: readonly string[];\n readonly prefix: readonly string[];\n readonly suffix: readonly string[];\n};",
|
|
127
|
-
"LocationLocaleData": "export type LocationLocaleData = {\n readonly cities: readonly string[];\n readonly countries: readonly string[];\n readonly states: readonly string[];\n readonly streets: readonly string[];\n readonly zipPattern: string;\n};",
|
|
128
|
-
"IllusionistLocale": "export type IllusionistLocale = {\n readonly code: string;\n readonly person: PersonLocaleData;\n readonly location: LocationLocaleData;\n};",
|
|
129
|
-
"IllusionistContext": "export type IllusionistContext = {\n readonly source: RandomSource;\n readonly locale: IllusionistLocale;\n};",
|
|
130
|
-
"RandomSource": "export type { RandomSource } from '@vielzeug/arsenal/random';"
|
|
131
|
-
}
|
|
132
|
-
}
|
|
@@ -1,60 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"apiSource": "// Core API — most users only need these\nexport type { ConflictOptions } from './conflicts';\nexport { findShortcutConflicts } from './conflicts';\nexport { KeymapError, KeymapParseError } from './errors';\nexport { formatShortcut } from './format';\nexport { createKeymap } from './keymap';\n// Power-user API — use if building custom tooling, validators, or framework integrations\nexport type { ModifierKey, Shortcut, ShortcutStep } from './parser';\nexport { canonicalizeShortcut, detectModKey, matchStep, parseShortcut, parseStep } from './parser';\nexport type {\n BindingEntry,\n BindingOptions,\n BindingValue,\n ChordStateChange,\n Handler,\n Keymap,\n KeymapOptions,\n When,\n} from './types';\n",
|
|
3
|
-
"docs": {
|
|
4
|
-
"index": "---\ntitle: Keymap — Headless keyboard shortcut manager\ndescription: Target-local keyboard shortcut manager with chords, event-aware guards, modifier aliases, and terminal disposal.\npackage: keymap\ncategory: app-infrastructure\nkeywords: [keyboard, shortcuts, hotkeys, chord, keybinding, headless, accessibility]\nexports:\n [\n canonicalizeShortcut,\n createKeymap,\n detectModKey,\n findShortcutConflicts,\n formatShortcut,\n KeymapError,\n KeymapParseError,\n matchStep,\n parseShortcut,\n parseStep,\n ]\nrelated: [herald, refine, ore]\nenvironments: [browser, node, ssr, deno]\n---\n\n<!-- markdownlint-disable MD025 MD033 MD060 -->\n\n<PackageHero package=\"keymap\" />\n\n## Why Keymap?\n\nBrowser keyboard handling needs modifier normalization, chord state, context policy, and listener ownership. Keymap keeps those concerns in one headless, zero-dependency handle.\n\n```ts\n// Before\nwindow.addEventListener('keydown', (event) => {\n if ((event.ctrlKey || event.metaKey) && event.key === 's') event.preventDefault();\n});\n\n// After\nimport { createKeymap } from '@vielzeug/keymap';\n\nconst map = createKeymap({ 'mod+s': () => console.log('save') });\nconst unmount = map.mount(document);\n\nunmount();\nmap.dispose();\n```\n\n| Feature | Raw `addEventListener` | Keymap |\n| ------------------- | -------------------------------------------- | -------------------------------------------- |\n| Bundle size | 0 B (built-in) | <PackageInfo package=\"keymap\" type=\"size\" /> |\n| Zero dependencies | <ore-icon name=\"check\" size=\"16\"></ore-icon> | <ore-icon name=\"check\" size=\"16\"></ore-icon> |\n| Chord sequences | <ore-icon name=\"x\" size=\"16\"></ore-icon> | <ore-icon name=\"check\" size=\"16\"></ore-icon> |\n| Modifier aliases | <ore-icon name=\"x\" size=\"16\"></ore-icon> | `cmd`, `win`, `option` → canonical |\n| Context guards | Manual `if` in handler | Event-aware `when(event)` predicate |\n| Chord ownership | Application-managed state | Per mounted target |\n| Disposable | Manual `removeEventListener` | Terminal `dispose()` + `[Symbol.dispose]()` |\n\n<div class=\"decision-callout\">\n\n**Use Keymap when** you need chord sequences (`g g`, `ctrl+k ctrl+s`), modifier aliases, or context-scoped hotkeys that can be cleanly mounted and unmounted.\n\n**Consider raw `addEventListener` when** you have a single, static, never-removed hotkey and don't need chords.\n\n</div>\n\n## Installation\n\n::: code-group\n\n```sh [pnpm]\npnpm add @vielzeug/keymap\n```\n\n```sh [npm]\nnpm install @vielzeug/keymap\n```\n\n```sh [yarn]\nyarn add @vielzeug/keymap\n```\n\n:::\n\n## Quick Start\n\nCreate, mount, then dispose one map owned by your UI scope.\n\n```ts\nimport { createKeymap } from '@vielzeug/keymap';\n\nconst map = createKeymap({\n 'mod+k mod+s': () => console.log('save'),\n 'mod+shift+p': () => console.log('open palette'),\n 'g g': () => window.scrollTo({ top: 0 }),\n escape: () => console.log('close panel'),\n});\n\nconst unmount = map.mount(document);\n\nunmount();\nmap.dispose();\n```\n\n## Features\n\n<div class=\"features-grid\">\n\n- `createKeymap()` — Create a keymap from a bindings record; mount to any `EventTarget`\n- Chord sequences — `\"g g\"`, `\"ctrl+k ctrl+s\"` with configurable timeout (default 1 s)\n- Modifier aliases — `cmd`/`command`/`win` → `meta`; `opt`/`option` → `alt`; `mod` → platform-aware\n- `BindingOptions` — per-binding `{ handler, when?, trigger? }` object syntax\n- `modKey` option — explicit platform override for SSR and cross-platform tests\n- `formatShortcut()` — platform-aware display (`⇧⌘P` on Mac, `Ctrl+Shift+P` elsewhere)\n- `parseShortcut()` / `parseStep()` / `matchStep()` — exposed for building custom matchers or testing\n- `canonicalizeShortcut()` — convert any shortcut alias to a stable key for conflict detection\n- `detectModKey()` — platform modifier detection (`'meta'` on Mac, `'ctrl'` elsewhere)\n- `listBindings()` — snapshot all active bindings (shortcut and trigger) for palette UIs\n- `findShortcutConflicts()` — detect prefix/duplicate conflicts before binding a user-customized shortcut\n- Disposable — `dispose()` + `[Symbol.dispose]` for `using` declarations\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 to 2.0](./migration.md)\n\n</div>\n\n## See Also\n\n<div class=\"see-also\">\n\n- [Herald](/herald/) — Typed event bus; pair with Keymap by publishing shortcut events to a bus instead of calling handlers directly\n- [Refine](/refine/) — `ore-command-palette` uses Keymap internally; register your own shortcuts alongside it\n- [Ore](/ore/) — Attach a keymap inside a `define()` setup function for component-scoped shortcuts\n\n</div>\n\n<!-- markdownlint-enable MD025 MD033 MD060 -->\n",
|
|
5
|
-
"api": "---\ntitle: Keymap — API Reference\ndescription: Complete API reference for @vielzeug/keymap bindings, chords, parsing, formatting, and lifecycle.\n---\n\n[[toc]]\n\n## API Overview\n\n### Core API (Most Users)\n\n| Symbol | Purpose | Execution mode | Common gotcha |\n| --- | --- | --- | --- |\n| `createKeymap()` | Create shortcut manager | Sync | `dispose()` is terminal |\n| `findShortcutConflicts()` | Find duplicate and prefix paths | Sync | Invalid non-empty input throws |\n| `formatShortcut()` | Format shortcut labels | Sync | Invalid input returns `''` |\n| `ChordStateChange` | Type for chord state callback events | — | No 'completed' event; handler fires immediately when matched |\n\n### Power-User API (Custom Tooling)\n\nUse the power-user API if you're building keyboard-aware config validators, custom UI, or framework integrations.\n\n| Symbol | Purpose | Execution mode | Common gotcha |\n| --- | --- | --- | --- |\n| `parseShortcut()` | Strictly parse full shortcut | Sync | Empty input throws |\n| `parseStep()` | Parse one step without throwing | Sync | Invalid input returns `null` |\n| `canonicalizeShortcut()` | Create stable shortcut key | Sync | Input must already be parsed |\n| `matchStep()` | Test event against parsed step | Sync | Extra modifiers prevent a match |\n| `detectModKey()` | Resolve platform primary modifier | Sync | Returns `ctrl` without `navigator` |\n\n### Errors\n\n| Symbol | Purpose | Execution mode | Common gotcha |\n| --- | --- | --- | --- |\n| `KeymapError` | Base Keymap error | Sync | Includes parse and lifecycle errors |\n| `KeymapParseError` | Strict parser error | Sync | `parseStep()` never throws it |\n\n## Package Entry Point\n\n| Import | Purpose |\n| --- | --- |\n| `@vielzeug/keymap` | Root entry point for every runtime function, error class, and public type listed here. |\n\n## Core Manager\n\n### `createKeymap()`\n\n```ts\nfunction createKeymap(\n bindings?: Record<string, BindingValue>,\n options?: KeymapOptions,\n): Keymap;\n```\n\nCreates shortcut manager with independent chord state for each mounted target.\n\n| Parameter | Type | Description |\n| --- | --- | --- |\n| `bindings` | `Record<string, BindingValue>` | Initial bindings. Keys must be non-empty valid shortcut strings. |\n| `options` | `KeymapOptions` | Chord, modifier, event, and global-guard configuration. |\n\n**Returns:** `Keymap`.\n\n```ts\nimport { createKeymap } from '@vielzeug/keymap';\n\nconst map = createKeymap({ 'ctrl+s': () => console.log('save') });\nconst unmount = map.mount(document);\n\nunmount();\nmap.dispose();\n```\n\n| `Keymap` member | Return | Contract |\n| --- | --- | --- |\n| `bind(shortcut, value)` | `() => void` | Adds or replaces canonical shortcut. Returned callback removes that binding while active. |\n| `mount(target)` | `() => void` | Adds target listener. Repeat mounts of same target are reference-counted. |\n| `unbind(shortcut)` | `void` | Removes canonical shortcut. Warns in development when unknown. |\n| `listBindings()` | `readonly BindingEntry[]` | Returns a detached binding snapshot. |\n| `dispose()` | `void` | Removes all listeners, aborts signal, and permanently disposes map. Idempotent. |\n| `disposed` | `boolean` | `true` after first `dispose()`. |\n| `disposalSignal` | `AbortSignal` | Aborts when map is disposed. |\n| `[Symbol.dispose]()` | `void` | Calls `dispose()`. |\n\nAfter disposal, `bind()`, `unbind()`, and `mount()` throw `KeymapError`.\n\n## Conflict Analysis\n\n### `findShortcutConflicts()`\n\n```ts\nfunction findShortcutConflicts(\n shortcut: string,\n entries: readonly BindingEntry[],\n options?: ConflictOptions,\n): BindingEntry[];\n```\n\nReturns entries with same-trigger exact or prefix-conflicting shortcut paths.\n\n| Parameter | Type | Description |\n| --- | --- | --- |\n| `shortcut` | `string` | Proposed shortcut. Empty or whitespace-only input returns no conflicts. |\n| `entries` | `readonly BindingEntry[]` | Bindings to compare, commonly `map.listBindings()`. |\n| `options` | `ConflictOptions` | Optional modifier resolution and trigger filter. |\n\n**Returns:** Matching entries. Returns `[]` when no conflict exists.\n\n```ts\nimport { createKeymap, findShortcutConflicts } from '@vielzeug/keymap';\n\nconst map = createKeymap({ g: () => console.log('top') });\nconst conflicts = findShortcutConflicts('g g', map.listBindings());\n\nconsole.log(conflicts.length); // 1\n```\n\n## Formatting\n\n### `formatShortcut()`\n\n```ts\nfunction formatShortcut(shortcut: string, modKey?: 'ctrl' | 'meta'): string;\n```\n\nFormats parsed shortcut into Mac symbols for `meta` or word labels for `ctrl`.\n\n| Parameter | Type | Description |\n| --- | --- | --- |\n| `shortcut` | `string` | Shortcut string to format. |\n| `modKey` | `'ctrl' \\| 'meta'` | Platform primary modifier. Defaults to `detectModKey()`. |\n\n**Returns:** Display label, or `''` for invalid input.\n\n```ts\nimport { formatShortcut } from '@vielzeug/keymap';\n\nformatShortcut('mod+shift+p', 'meta'); // ⇧⌘P\nformatShortcut('mod+shift+p', 'ctrl'); // Ctrl+Shift+P\n```\n\n## Parsing and Matching\n\n### `parseShortcut()`\n\n```ts\nfunction parseShortcut(raw: string, modKey?: 'ctrl' | 'meta'): Shortcut;\n```\n\nStrictly parses one or more space-separated shortcut steps.\n\n| Parameter | Type | Description |\n| --- | --- | --- |\n| `raw` | `string` | Full shortcut string. |\n| `modKey` | `'ctrl' \\| 'meta'` | Platform primary modifier. Defaults to `detectModKey()`. |\n\n**Returns:** Parsed `Shortcut`.\n\n```ts\nimport { parseShortcut } from '@vielzeug/keymap';\n\nconst shortcut = parseShortcut('ctrl+k ctrl+s', 'ctrl');\nconsole.log(shortcut.length); // 2\n```\n\nThrows `KeymapParseError` for empty, modifier-only, or ambiguous steps.\n\n---\n\n### `parseStep()`\n\n```ts\nfunction parseStep(raw: string, modKey?: 'ctrl' | 'meta'): ShortcutStep | null;\n```\n\nParses one shortcut step without throwing.\n\n| Parameter | Type | Description |\n| --- | --- | --- |\n| `raw` | `string` | One shortcut step. |\n| `modKey` | `'ctrl' \\| 'meta'` | Platform primary modifier. Defaults to `detectModKey()`. |\n\n**Returns:** Parsed `ShortcutStep`, or `null` for empty, modifier-only, or ambiguous input.\n\n```ts\nimport { parseStep } from '@vielzeug/keymap';\n\nparseStep('ctrl+k', 'ctrl'); // { key: 'k', modifiers: Set(['ctrl']) }\nparseStep('ctrl+k+j', 'ctrl'); // null\n```\n\n---\n\n### `canonicalizeShortcut()`\n\n```ts\nfunction canonicalizeShortcut(steps: readonly ShortcutStep[]): string;\n```\n\nConverts parsed steps into stable canonical string with sorted modifier order.\n\n| Parameter | Type | Description |\n| --- | --- | --- |\n| `steps` | `readonly ShortcutStep[]` | Parsed shortcut steps. |\n\n**Returns:** Canonical shortcut string.\n\n```ts\nimport { canonicalizeShortcut, parseShortcut } from '@vielzeug/keymap';\n\ncanonicalizeShortcut(parseShortcut('shift+ctrl+k', 'ctrl')); // ctrl+shift+k\n```\n\n---\n\n### `matchStep()`\n\n```ts\nfunction matchStep(event: KeyboardEvent, step: ShortcutStep): boolean;\n```\n\nTests exact key and modifier equality for one parsed step.\n\n| Parameter | Type | Description |\n| --- | --- | --- |\n| `event` | `KeyboardEvent` | Event to match. Missing runtime `key` returns `false`. |\n| `step` | `ShortcutStep` | Parsed step. |\n\n**Returns:** `true` only when key and all modifier states match.\n\n```ts\nimport { matchStep, parseStep } from '@vielzeug/keymap';\n\nconst step = parseStep('ctrl+k', 'ctrl')!;\nmatchStep(new KeyboardEvent('keydown', { ctrlKey: true, key: 'k' }), step); // true\n```\n\n---\n\n### `detectModKey()`\n\n```ts\nfunction detectModKey(): 'ctrl' | 'meta';\n```\n\nDetects Mac platform from `navigator` and otherwise returns `ctrl`.\n\n**Returns:** `'meta'` on Mac platforms; `'ctrl'` elsewhere or without `navigator`.\n\n```ts\nimport { detectModKey } from '@vielzeug/keymap';\n\nconst modKey = detectModKey();\n```\n\n## Types\n\n### `Keymap`\n\nStateful shortcut manager returned by `createKeymap()`.\n\n```ts\ninterface Keymap {\n [Symbol.dispose](): void;\n bind(shortcut: string, value: BindingValue): () => void;\n dispose(): void;\n readonly disposalSignal: AbortSignal;\n readonly disposed: boolean;\n listBindings(): readonly BindingEntry[];\n mount(target: EventTarget): () => void;\n unbind(shortcut: string): void;\n}\n```\n\n### `KeymapOptions`\n\nOptions applied to every binding owned by one manager.\n\n```ts\ninterface KeymapOptions {\n chordTimeout?: number;\n modKey?: 'ctrl' | 'meta';\n preventDefault?: boolean;\n stopPropagation?: boolean;\n when?: When;\n onChordState?: (change: ChordStateChange) => void;\n}\n```\n\n- `when`: Guard function called for all bindings. When combined with per-binding `when` guards, both must return `true` for the handler to fire (AND composition). Global guard is checked first.\n- `onChordState`: Optional callback to observe chord state changes (started, progressed, or timeout). Useful for debugging, testing, logging, or implementing chord UI hints. Callback errors are caught and logged in development. Note: when a chord completes, the binding handler fires immediately; no separate 'completed' event is emitted.\n\n### `BindingOptions`\n\nPer-binding handler configuration.\n\n```ts\ntype BindingOptions = {\n handler: Handler;\n trigger?: 'keydown' | 'keyup';\n when?: When;\n};\n```\n\n### `BindingValue`, `Handler`, and `When`\n\nAccepted values when registering a shortcut.\n\n```ts\ntype Handler = (event: KeyboardEvent) => void;\ntype When = (event: KeyboardEvent) => boolean;\ntype BindingValue = Handler | BindingOptions;\n```\n\n### `BindingEntry`\n\nDetached binding metadata returned by `listBindings()`.\n\n```ts\ntype BindingEntry = {\n readonly shortcut: readonly ShortcutStep[];\n readonly trigger: 'keydown' | 'keyup';\n};\n```\n\n### `ModifierKey`, `Shortcut`, and `ShortcutStep`\n\nParser types used by `parseShortcut()`, `parseStep()`, `matchStep()`, and `canonicalizeShortcut()`.\n\n```ts\ntype ModifierKey = 'alt' | 'ctrl' | 'meta' | 'shift';\n\ntype ShortcutStep = {\n key: string;\n modifiers: Set<ModifierKey>;\n};\n\ntype Shortcut = ShortcutStep[];\n```\n\n### `ConflictOptions`\n\nComparison options for `findShortcutConflicts()`.\n\n```ts\ninterface ConflictOptions {\n modKey?: 'ctrl' | 'meta';\n trigger?: 'keydown' | 'keyup';\n}\n```\n\n### `ChordStateChange`\n\nDiscriminated union type for chord state events emitted by `onChordState` callback. When a chord fully matches, the binding handler fires immediately; no separate 'completed' event is emitted.\n\n```ts\ntype ChordStateChange =\n | { type: 'started'; target: EventTarget; step: ShortcutStep; trigger: 'keydown' | 'keyup' }\n | { type: 'progressed'; target: EventTarget; steps: readonly ShortcutStep[]; trigger: 'keydown' | 'keyup' }\n | { type: 'timeout'; target: EventTarget; trigger: 'keydown' | 'keyup' };\n```\n\n| Event | Fields | When | Use case |\n| --- | --- | --- | --- |\n| `started` | `target`, `step`, `trigger` | First key of a chord is pressed. | Show \"waiting for next key\" UI hint. |\n| `progressed` | `target`, `steps`, `trigger` | Additional step(s) added to pending chord. | Update chord hint with current progress. |\n| `timeout` | `target`, `trigger` | Chord was pending but timed out without completing. | Clear \"waiting\" UI state; log timeout for debugging. |\n\n```ts\nimport { createKeymap } from '@vielzeug/keymap';\n\nconst map = createKeymap(\n { 'g g': () => scrollToTop() },\n {\n onChordState: (change) => {\n if (change.type === 'started') {\n console.log(`Chord started: ${change.step.key}`);\n }\n if (change.type === 'progressed') {\n console.log(`Chord progress: ${change.steps.map((s) => s.key).join(' ')}`);\n }\n if (change.type === 'timeout') {\n console.log('Chord timed out');\n }\n },\n },\n);\n```\n\n## Errors\n\n| Error | Trigger | Notable properties |\n| --- | --- | --- |\n| `KeymapError` | Lifecycle operation after disposal | Use `instanceof KeymapError` to narrow Keymap errors. |\n| `KeymapParseError` | Strict shortcut parser receives invalid input | Extends `KeymapError`. |\n",
|
|
6
|
-
"usage": "---\ntitle: Keymap — Usage Guide\ndescription: Bind keyboard shortcuts, chords, event-aware guards, and target-local listeners with @vielzeug/keymap.\n---\n\n[[toc]]\n\n## Basic Usage\n\nMount one keymap, then release its target listener and dispose its owner during teardown.\n\n```ts\nimport { createKeymap } from '@vielzeug/keymap';\n\nconst map = createKeymap({\n 'ctrl+s': () => console.log('save'),\n 'ctrl+z': () => console.log('undo'),\n escape: () => console.log('close'),\n});\n\nconst unmount = map.mount(document);\n\n// Call this when the owning UI scope ends.\nunmount();\nmap.dispose();\n```\n\n`unmount()` only releases that target. `dispose()` releases every target, aborts `disposalSignal`, and makes `bind()`, `unbind()`, and `mount()` unavailable.\n\n## Modifier Aliases\n\nUse aliases to accept platform terminology while Keymap stores one canonical shortcut.\n\n| Input | Canonical modifier |\n| --- | --- |\n| `cmd`, `command`, `win` | `meta` |\n| `opt`, `option` | `alt` |\n| `ctrl`, `control` | `ctrl` |\n| `mod` | `meta` on Mac; `ctrl` elsewhere |\n\nPass `modKey` when rendering or testing a specific platform.\n\n```ts\nimport { createKeymap } from '@vielzeug/keymap';\n\nconst map = createKeymap(\n { 'mod+k': () => console.log('open palette') },\n { modKey: 'ctrl' },\n);\n\nmap.mount(document);\n```\n\n## Chord Sequences\n\nSeparate chord steps with spaces. Keymap resets an incomplete sequence after `chordTimeout` milliseconds.\n\n```ts\nconst map = createKeymap(\n {\n 'ctrl+k ctrl+s': () => console.log('save'),\n 'g g': () => window.scrollTo({ top: 0 }),\n 'g e': () => window.scrollTo({ top: document.body.scrollHeight }),\n },\n { chordTimeout: 800 },\n);\n```\n\nDo not bind a complete shortcut and a longer chord beginning with that shortcut. `g` fires immediately, so `g g` cannot complete. Check proposed user bindings with `findShortcutConflicts()`.\n\n## Binding Options\n\nAdd a guard or choose `keyup` with `BindingOptions`.\n\n```ts\nconst map = createKeymap({\n 'ctrl+s': () => saveDocument(),\n escape: { handler: closePanel, when: (event) => event.target === panel },\n space: { handler: togglePlayback, trigger: 'keyup' },\n});\n```\n\nA matching binding calls `preventDefault()` by default. Set `preventDefault: false` for shortcuts that must retain browser behavior.\n\n## Context Guards\n\nUse global `when(event)` for policy shared by every binding. Use per-binding `when(event)` when one shortcut needs a narrower policy.\n\n```ts\nconst map = createKeymap(\n {\n escape: { handler: closePanel, when: (event) => event.target === panel },\n 'ctrl+s': () => saveDocument(),\n },\n { when: (event) => !modalIsOpen() && event.isTrusted },\n);\n```\n\nZero-argument callbacks continue to work. Accept `KeyboardEvent` when guard logic needs target, modifier, composition, or shadow-DOM context.\n\n### Guard Composition: Global + Per-Binding\n\nWhen you provide both a global `when` (in `KeymapOptions`) and per-binding `when` guards, both must return `true` for the handler to fire. This is AND composition.\n\n**Guard evaluation and chord tracking order:**\n\n1. **Chord state is tracked independently of guards.** The chord tracker progresses through steps before any guard is checked.\n2. **Global guard checked first.** If it returns `false`, all bindings are skipped and the handler does not fire — but chord state events still emit.\n3. **Per-binding guard checked only after global passes.** Enables mixing global policy (e.g., \"skip when modal open\") with binding-specific checks (e.g., \"only in this panel\").\n\nThink of it as: chord tracking (independent observation) → global gate (app-level policy) AND per-binding gate (binding-level context).\n\n```ts\nconst map = createKeymap(\n {\n 'escape': { handler: closePanel, when: (event) => event.target === panel },\n 'ctrl+s': () => saveDocument(),\n },\n { when: (event) => !isModalOpen() && event.isTrusted },\n);\n\n// Global guard runs first; if false, both bindings are skipped (handler doesn't fire).\n// If global passes:\n// - 'ctrl+s' handler fires immediately.\n// - 'escape' handler fires only if event.target is the panel.\n// But chord state events emit regardless of guards.\n```\n\n### Preserve Native Text Editing\n\nUse `event.composedPath()` to keep browser undo and redo inside inputs, textareas, and `contenteditable` elements. Kanban app shell uses this policy for its global undo and redo shortcuts.\n\n```ts\nconst isTypingInField = (event: KeyboardEvent): boolean =>\n event.composedPath().some(\n (target) =>\n target instanceof HTMLElement &&\n (target instanceof HTMLInputElement || target instanceof HTMLTextAreaElement || target.isContentEditable),\n );\n\nconst map = createKeymap(\n {\n 'mod+z': () => undo(),\n 'mod+shift+z': () => redo(),\n },\n { when: (event) => !isTypingInField(event) },\n);\n```\n\nDo not make editable-field suppression a hidden package default. Applications may intentionally bind shortcuts inside editable controls.\n\n## Trigger Control\n\nBind on `keyup` when an action must run after key release.\n\n```ts\nconst map = createKeymap({\n space: { handler: confirmAction, trigger: 'keyup' },\n});\n```\n\n`keydown` and `keyup` maintain independent chord state.\n\n## Replace Bindings at Runtime\n\nBind replaces an existing binding with same canonical shortcut and returns a targeted removal callback.\n\n```ts\nconst map = createKeymap({ 'ctrl+k': defaultAction });\nconst removePluginBinding = map.bind('ctrl+k', pluginAction);\n\nremovePluginBinding();\nmap.bind('ctrl+k', defaultAction);\n```\n\n`unbind(shortcut)` removes canonicalized aliases and warns in development when no binding exists.\n\n## Format Shortcut Labels\n\nFormat labels with explicit platform behavior when your UI is cross-platform.\n\n```ts\nimport { formatShortcut } from '@vielzeug/keymap';\n\nconsole.log(formatShortcut('mod+shift+p', 'meta')); // ⇧⌘P\nconsole.log(formatShortcut('mod+shift+p', 'ctrl')); // Ctrl+Shift+P\n```\n\n`formatShortcut()` returns `''` and emits a development warning for invalid input.\n\n## Detect Conflicts\n\nCheck a custom shortcut before binding it to prevent duplicate or unreachable chord paths.\n\n```ts\nimport { createKeymap, findShortcutConflicts } from '@vielzeug/keymap';\n\nconst map = createKeymap({ g: () => scrollToTop() });\nconst conflicts = findShortcutConflicts('g g', map.listBindings());\n\nif (conflicts.length === 0) map.bind('g g', () => scrollToBottom());\n```\n\nConflict detection compares only bindings with same trigger. An empty proposal returns no conflicts; other invalid proposals throw `KeymapParseError`.\n\n## Observe Chord State\n\nTrack chord progression for debugging, logging, testing, or implementing chord UI hints (e.g., \"you pressed 'g', press again to scroll\").\n\n**Chord state tracking is independent of guards.** Events emit even if the global or per-binding guard would prevent the handler from firing. This allows you to show UI hints regardless of whether the binding is allowed to execute.\n\n```ts\nimport { createKeymap } from '@vielzeug/keymap';\n\nconst map = createKeymap(\n {\n 'g g': () => window.scrollTo({ top: 0 }),\n 'ctrl+k ctrl+s': () => save(),\n },\n {\n onChordState: (change) => {\n switch (change.type) {\n case 'started':\n console.log(`Chord started: ${change.step.key} (${change.trigger})`);\n showHint(`Press '${change.step.key}' again...`);\n break;\n case 'progressed':\n console.log(`Waiting for: ${change.steps.map((s) => s.key).join(' → ?')}`);\n updateHint(`${change.steps.map((s) => s.key).join(' → ?')}`);\n break;\n case 'timeout':\n console.log('Chord timed out; resetting');\n hideHint();\n break;\n }\n },\n },\n);\n```\n\n**Error handling:** Callback errors are caught and logged in development mode; they don't break binding execution. Use error handling in your callback to prevent typos from blocking shortcuts.\n\n**Per-target isolation:** Each mounted target maintains independent chord state. Use `change.target` when mounting the same keymap on multiple targets to distinguish progress per target.\n\n## Mount Targets\n\nMount one keymap on multiple independent targets when each target should own its own chord progression.\n\n```ts\nconst map = createKeymap({ 'g g': () => console.log('go to top') });\nconst unmountEditor = map.mount(editor);\nconst unmountPreview = map.mount(preview);\n```\n\nA chord started on `editor` cannot complete on `preview`. Repeated `mount(editor)` calls share one listener and require one unmount call each. For nested targets, Keymap handles one bubbled event at its innermost mounted target.\n\n## Scoped Maps\n\nCreate separate keymaps for separate UI owners. If maps share a target and shortcut, guards must be mutually exclusive because Keymap has no implicit layer precedence.\n\n```ts\nconst baseMap = createKeymap(\n { escape: () => closeSidebar() },\n { when: () => !modalIsOpen() },\n);\n\nconst modalMap = createKeymap(\n { escape: () => closeModal() },\n { when: () => modalIsOpen() },\n);\n\nconst unmountBase = baseMap.mount(document);\nconst unmountModal = modalMap.mount(document);\n```\n\n## Testing\n\nDispatch `KeyboardEvent` instances against a mounted DOM target to test handlers and default prevention.\n\n```ts\nimport { expect, it, vi } from 'vitest';\n\nimport { createKeymap } from '@vielzeug/keymap';\n\nit('handles save', () => {\n const save = vi.fn();\n const target = document.createElement('button');\n const map = createKeymap({ 'ctrl+s': save });\n const unmount = map.mount(target);\n\n target.dispatchEvent(new KeyboardEvent('keydown', { bubbles: true, ctrlKey: true, key: 's' }));\n\n expect(save).toHaveBeenCalledOnce();\n unmount();\n map.dispose();\n});\n```\n\nMount nested DOM targets in tests when your application uses both a container and a descendant listener. This verifies one bubbled event cannot complete a chord twice.\n\n## Framework Integration\n\nCreate map during framework lifecycle, then dispose it during teardown.\n\n::: code-group\n\n```tsx [React]\nimport { useEffect } from 'react';\n\nimport { createKeymap } from '@vielzeug/keymap';\n\nexport function App() {\n useEffect(() => {\n const map = createKeymap({ 'ctrl+k': () => console.log('open palette') });\n const unmount = map.mount(document);\n\n return () => {\n unmount();\n map.dispose();\n };\n }, []);\n\n return null;\n}\n```\n\n```vue [Vue 3]\n<script setup lang=\"ts\">\nimport { onMounted, onUnmounted } from 'vue';\n\nimport { createKeymap } from '@vielzeug/keymap';\n\nconst map = createKeymap({ escape: () => console.log('close palette') });\nlet unmount: (() => void) | undefined;\n\nonMounted(() => {\n unmount = map.mount(document);\n});\n\nonUnmounted(() => {\n unmount?.();\n map.dispose();\n});\n</script>\n```\n\n```ts [Svelte]\nimport { onMount } from 'svelte';\n\nimport { createKeymap } from '@vielzeug/keymap';\n\nconst map = createKeymap({ escape: () => console.log('close palette') });\n\nonMount(() => {\n const unmount = map.mount(document);\n\n return () => {\n unmount();\n map.dispose();\n };\n});\n```\n\n:::\n\n## Working with Other Vielzeug Libraries\n\n### Keymap + Ledger\n\nConnect undo and redo handlers to a Ledger owner.\n\n```ts\nimport { createKeymap } from '@vielzeug/keymap';\nimport { createLedger } from '@vielzeug/ledger';\n\nconst ledger = createLedger();\nconst reportHistoryError = (error: unknown): void => console.error(error);\nconst map = createKeymap({\n 'mod+z': () => void ledger.undo().catch(reportHistoryError),\n 'mod+shift+z': () => void ledger.redo().catch(reportHistoryError),\n});\n\nmap.mount(document);\n```\n\n### Keymap + Herald\n\nEmit domain events instead of calling application actions from shortcut handlers.\n\n```ts\nimport { createBus } from '@vielzeug/herald';\nimport { createKeymap } from '@vielzeug/keymap';\n\nconst bus = createBus<{ 'shortcut:save': void }>();\nconst map = createKeymap({\n 'ctrl+s': () => bus.emit('shortcut:save'),\n});\n\nmap.mount(document);\n```\n\n## Best Practices\n\n- **Dispose** every map when its owner ends.\n- **Unmount** temporary target listeners instead of disposing reusable maps.\n- **Guard** global text-editing shortcuts with `event.composedPath()`.\n- **Check** conflicts before accepting customized shortcuts.\n- **Keep** shared-target guards mutually exclusive.\n- **Use** `mod` for primary cross-platform shortcuts.\n- **Avoid** prefix pairs such as `g` and `g g`.\n",
|
|
7
|
-
"examples": "---\ntitle: Keymap — Examples\ndescription: Worked examples for @vielzeug/keymap.\n---\n\n## Examples\n\n- [Global Shortcuts](./examples/global-shortcuts.md)\n- [Vim-style Navigation](./examples/vim-navigation.md)\n"
|
|
8
|
-
},
|
|
9
|
-
"examples": [
|
|
10
|
-
{
|
|
11
|
-
"id": "basic-shortcuts",
|
|
12
|
-
"code": "import { createKeymap, formatShortcut } from '@vielzeug/keymap'\n\n// Create a keymap — bindings fire on keydown by default.\nconst map = createKeymap({\n 'ctrl+s': () => console.log('save triggered'),\n escape: { handler: () => console.log('close panel'), when: () => true },\n space: { handler: () => console.log('toggle play'), trigger: 'keyup' },\n}, { modKey: 'ctrl' })\n\n// Mount to document (required for event listening).\nconst unmount = map.mount(document)\n\n// Simulate events for demonstration.\ndocument.dispatchEvent(new KeyboardEvent('keydown', { key: 's', ctrlKey: true, bubbles: true }))\ndocument.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape', bubbles: true }))\ndocument.dispatchEvent(new KeyboardEvent('keyup', { key: ' ', bubbles: true }))\n\n// Format shortcuts for display in UI tooltips or menus.\nconsole.log(formatShortcut('ctrl+s', 'ctrl')) // 'Ctrl+S'\nconsole.log(formatShortcut('mod+shift+p', 'meta')) // '⇧⌘P'\nconsole.log(formatShortcut('ctrl+k ctrl+s', 'ctrl')) // 'Ctrl+K Ctrl+S'\n\nunmount()",
|
|
13
|
-
"name": "Basic Shortcuts"
|
|
14
|
-
},
|
|
15
|
-
{
|
|
16
|
-
"id": "chord-sequences",
|
|
17
|
-
"code": "import { createKeymap, findShortcutConflicts } from '@vielzeug/keymap'\n\n// Chord sequences fire only after all steps are pressed in order within the timeout.\n// Shortcut strings are lowercased before matching, so 'g g' and 'g G' are the SAME\n// binding — writing both would silently overwrite one. Vim's actual 'G' is shift+g,\n// a single keystroke, not a 'g'-prefixed chord.\nconst map = createKeymap({\n 'ctrl+k ctrl+s': () => console.log('save all (VS Code-style)'),\n 'g g': () => console.log('go to top (Vim-style)'),\n 'shift+g': () => console.log('go to bottom'),\n}, { chordTimeout: 800, modKey: 'ctrl' })\n\nconst unmount = map.mount(document)\n\n// Simulate completing 'ctrl+k ctrl+s' chord.\ndocument.dispatchEvent(new KeyboardEvent('keydown', { key: 'k', ctrlKey: true, bubbles: true }))\ndocument.dispatchEvent(new KeyboardEvent('keydown', { key: 's', ctrlKey: true, bubbles: true }))\n\n// Simulate Vim 'g g' chord.\ndocument.dispatchEvent(new KeyboardEvent('keydown', { key: 'g', bubbles: true }))\ndocument.dispatchEvent(new KeyboardEvent('keydown', { key: 'g', bubbles: true }))\n\n// Gotcha: a single-key binding sharing a chord's first step always wins immediately,\n// making the longer chord unreachable — findShortcutConflicts() catches this up front.\nconsole.log('Would ctrl+k (alone) conflict with the chord above?')\nconsole.log(findShortcutConflicts('ctrl+k', map.listBindings()))\n\nunmount()",
|
|
18
|
-
"name": "Chord Sequences"
|
|
19
|
-
},
|
|
20
|
-
{
|
|
21
|
-
"id": "conflict-detection",
|
|
22
|
-
"code": "import { createKeymap, findShortcutConflicts } from '@vielzeug/keymap'\n\n// findShortcutConflicts() catches unreachable bindings before you register them —\n// useful for a shortcut-customization UI driven by user input.\nconst map = createKeymap({\n g: () => console.log('go to top'),\n})\n\nconst proposed = 'g g'\nconst conflicts = findShortcutConflicts(proposed, map.listBindings())\n\nif (conflicts.length > 0) {\n console.log(`\"${proposed}\" would never fire — shadowed by an existing binding`)\n} else {\n map.bind(proposed, () => console.log('go to bottom'))\n}\n\n// A shortcut with no relationship to existing bindings reports no conflicts.\nconsole.log('ctrl+s conflicts:', findShortcutConflicts('ctrl+s', map.listBindings()).length)\n\n// keydown and keyup bindings never conflict — they're matched independently.\nconst withKeyup = createKeymap({ space: { handler: () => {}, trigger: 'keyup' } })\nconsole.log(\n 'space (keydown) vs space (keyup):',\n findShortcutConflicts('space', withKeyup.listBindings(), { trigger: 'keydown' }).length,\n)",
|
|
23
|
-
"name": "Conflict Detection"
|
|
24
|
-
},
|
|
25
|
-
{
|
|
26
|
-
"id": "parse-and-match",
|
|
27
|
-
"code": "import { KeymapError, KeymapParseError, formatShortcut, matchStep, parseShortcut } from '@vielzeug/keymap'\n\n// Parse shortcut strings into structured step objects.\nconst steps = parseShortcut('ctrl+k ctrl+s', 'ctrl')\nconsole.log('Steps:', steps.length)\nconsole.log('Step 0 key:', steps[0].key)\nconsole.log('Step 0 modifiers:', [...steps[0].modifiers])\n\n// matchStep tests a single KeyboardEvent against a parsed step.\nconst event = new KeyboardEvent('keydown', { key: 'k', ctrlKey: true })\nconsole.log('event matches ctrl+k:', matchStep(event, steps[0])) // true\nconsole.log('event matches ctrl+s:', matchStep(event, steps[1])) // false\n\n// formatShortcut turns a shortcut string into a display label.\nconst shortcuts = [\n ['mod+shift+p', 'meta'],\n ['mod+shift+p', 'ctrl'],\n ['ctrl+k ctrl+s', 'ctrl'],\n ['escape', 'ctrl'],\n ['space', 'meta'],\n]\n\nfor (const [shortcut, modKey] of shortcuts) {\n console.log(shortcut, '→', formatShortcut(shortcut, modKey))\n}\n\n// parseShortcut() throws KeymapParseError for ambiguous or invalid steps.\n// Catch it with instanceof KeymapError to handle any keymap error.\ntry {\n parseShortcut('ctrl+k+j', 'ctrl') // two non-modifier keys in one step — ambiguous\n} catch (err) {\n console.log('Caught:', err instanceof KeymapError, err instanceof KeymapParseError, err.message)\n}",
|
|
28
|
-
"name": "Parse & Match"
|
|
29
|
-
},
|
|
30
|
-
{
|
|
31
|
-
"id": "shortcut-utilities",
|
|
32
|
-
"code": "import {\n canonicalizeShortcut,\n createKeymap,\n detectModKey,\n parseShortcut,\n parseStep,\n} from '@vielzeug/keymap'\n\n// detectModKey() — platform modifier detection.\nconst modKey = detectModKey()\nconsole.log('Platform modifier:', modKey)\n\n// parseStep() — parse a single chord step (no throw on invalid input).\nconst step = parseStep('ctrl+k', modKey)\nconsole.log('parseStep ctrl+k:', step?.key, [...(step?.modifiers ?? [])])\n\nconst invalid = parseStep('', modKey)\nconsole.log('parseStep empty string:', invalid) // null\n\n// canonicalizeShortcut() — stable canonical key for conflict detection.\n// Different aliases for the same shortcut resolve to the same canonical key.\nconst a = canonicalizeShortcut(parseShortcut('cmd+k', modKey))\nconst b = canonicalizeShortcut(parseShortcut('meta+k', modKey))\nconsole.log('cmd+k canonical:', a)\nconsole.log('meta+k canonical:', b)\nconsole.log('Same canonical?', a === b)\n\n// listBindings() — inspect active bindings at runtime.\nconst map = createKeymap(\n {\n 'ctrl+k': () => console.log('ctrl+k fired'),\n 'ctrl+shift+s': { handler: () => console.log('save fired'), trigger: 'keyup' },\n },\n { modKey },\n)\n\nconst entries = map.listBindings()\nconsole.log('Bindings:', entries.length)\n\nfor (const entry of entries) {\n const canonical = canonicalizeShortcut(entry.shortcut)\n console.log(` ${canonical} — trigger: ${entry.trigger}`)\n}\n\n// bind() returns an unbind closure — uses canonical key internally.\nconst unbind = map.bind('ctrl+j', () => console.log('ctrl+j'))\nconsole.log('After bind:', map.listBindings().length)\n\nunbind()\nconsole.log('After unbind:', map.listBindings().length)",
|
|
33
|
-
"name": "Shortcut Utilities"
|
|
34
|
-
}
|
|
35
|
-
],
|
|
36
|
-
"typeSignatures": {
|
|
37
|
-
"ConflictOptions": "export type { ConflictOptions } from './conflicts';",
|
|
38
|
-
"findShortcutConflicts": "export { findShortcutConflicts } from './conflicts';",
|
|
39
|
-
"KeymapError": "export { KeymapError, KeymapParseError } from './errors';",
|
|
40
|
-
"KeymapParseError": "export { KeymapError, KeymapParseError } from './errors';",
|
|
41
|
-
"formatShortcut": "export { formatShortcut } from './format';",
|
|
42
|
-
"createKeymap": "export { createKeymap } from './keymap';",
|
|
43
|
-
"ModifierKey": "export type { ModifierKey, Shortcut, ShortcutStep } from './parser';",
|
|
44
|
-
"Shortcut": "export type { ModifierKey, Shortcut, ShortcutStep } from './parser';",
|
|
45
|
-
"ShortcutStep": "export type { ModifierKey, Shortcut, ShortcutStep } from './parser';",
|
|
46
|
-
"canonicalizeShortcut": "export { canonicalizeShortcut, detectModKey, matchStep, parseShortcut, parseStep } from './parser';",
|
|
47
|
-
"detectModKey": "export { canonicalizeShortcut, detectModKey, matchStep, parseShortcut, parseStep } from './parser';",
|
|
48
|
-
"matchStep": "export { canonicalizeShortcut, detectModKey, matchStep, parseShortcut, parseStep } from './parser';",
|
|
49
|
-
"parseShortcut": "export { canonicalizeShortcut, detectModKey, matchStep, parseShortcut, parseStep } from './parser';",
|
|
50
|
-
"parseStep": "export { canonicalizeShortcut, detectModKey, matchStep, parseShortcut, parseStep } from './parser';",
|
|
51
|
-
"BindingEntry": "export type {\n BindingEntry,\n BindingOptions,\n BindingValue,\n ChordStateChange,\n Handler,\n Keymap,\n KeymapOptions,\n When,\n} from './types';",
|
|
52
|
-
"BindingOptions": "export type {\n BindingEntry,\n BindingOptions,\n BindingValue,\n ChordStateChange,\n Handler,\n Keymap,\n KeymapOptions,\n When,\n} from './types';",
|
|
53
|
-
"BindingValue": "export type {\n BindingEntry,\n BindingOptions,\n BindingValue,\n ChordStateChange,\n Handler,\n Keymap,\n KeymapOptions,\n When,\n} from './types';",
|
|
54
|
-
"ChordStateChange": "export type {\n BindingEntry,\n BindingOptions,\n BindingValue,\n ChordStateChange,\n Handler,\n Keymap,\n KeymapOptions,\n When,\n} from './types';",
|
|
55
|
-
"Handler": "export type {\n BindingEntry,\n BindingOptions,\n BindingValue,\n ChordStateChange,\n Handler,\n Keymap,\n KeymapOptions,\n When,\n} from './types';",
|
|
56
|
-
"Keymap": "export type {\n BindingEntry,\n BindingOptions,\n BindingValue,\n ChordStateChange,\n Handler,\n Keymap,\n KeymapOptions,\n When,\n} from './types';",
|
|
57
|
-
"KeymapOptions": "export type {\n BindingEntry,\n BindingOptions,\n BindingValue,\n ChordStateChange,\n Handler,\n Keymap,\n KeymapOptions,\n When,\n} from './types';",
|
|
58
|
-
"When": "export type {\n BindingEntry,\n BindingOptions,\n BindingValue,\n ChordStateChange,\n Handler,\n Keymap,\n KeymapOptions,\n When,\n} from './types';"
|
|
59
|
-
}
|
|
60
|
-
}
|