@vielzeug/codex 2.2.8 → 2.2.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,9 +1,9 @@
1
1
  {
2
- "apiSource": "export { toFormData } from './adapters/form-data';\nexport { ForgeConfigError, ForgeDisposedError, ForgeError, ForgeSubmitError, ForgeValidationError } from './errors';\nexport { createForm } from './form';\nexport * from './types';\n",
2
+ "apiSource": "export { ForgeConfigError, ForgeDisposedError, ForgeError, ForgeSubmitError, ForgeValidationError } from './errors';\nexport { createForm } from './form';\nexport * from './types';\n",
3
3
  "docs": {
4
- "index": "---\ntitle: Forge — Immutable form state for TypeScript\ndescription: Framework-agnostic immutable form state with focused object fields and explicit validation results.\npackage: forge\ncategory: forms\nkeywords: [form-state, validation, immutable, input, submission]\nrelated: [spell, vault, courier]\nexports: [createForm, toFormData, bindField, customValidator, saveForm, loadForm]\nenvironments: [browser, node, ssr, deno]\n---\n\n<!-- markdownlint-disable MD025 MD033 MD060 -->\n\n<PackageHero package=\"forge\" />\n\n## Why Forge?\n\nNative form state becomes difficult to inspect once values, validation, draft restoration, and UI bindings share mutable objects. Forge owns one immutable value tree and gives you typed handles for object branches without string paths, scoped controllers, or framework state.\n\n```ts\n// Before\nconst values = { email: '', password: '' };\nconst errors: Record<string, string> = {};\n\nfunction submit() {\n errors.email = values.email.includes('@') ? '' : 'Invalid email';\n errors.password = values.password.length >= 8 ? '' : 'Use at least eight characters';\n}\n\n// After\nconst form = createForm({\n initialValues: { email: '', password: '' },\n validate: (value) => ({\n fields: {\n email: value.email.includes('@') ? undefined : 'Invalid email',\n password: value.password.length >= 8 ? undefined : 'Use at least eight characters',\n },\n }),\n});\n```\n\n| Feature | Forge | Native form state | Framework-owned form state |\n| --- | --- | --- | --- |\n| Bundle size | <PackageInfo package=\"forge\" type=\"size\" /> | <ore-icon name=\"check\" size=\"16\"></ore-icon> | Varies |\n| Zero external dependencies | <ore-icon name=\"check\" size=\"16\"></ore-icon> | <ore-icon name=\"check\" size=\"16\"></ore-icon> | <ore-icon name=\"x\" size=\"16\"></ore-icon> |\n| Immutable nested values | <ore-icon name=\"check\" size=\"16\"></ore-icon> | <ore-icon name=\"x\" size=\"16\"></ore-icon> | Varies |\n| Typed object field handles | <ore-icon name=\"check\" size=\"16\"></ore-icon> | <ore-icon name=\"x\" size=\"16\"></ore-icon> | Varies |\n| Framework-independent state | <ore-icon name=\"check\" size=\"16\"></ore-icon> | <ore-icon name=\"check\" size=\"16\"></ore-icon> | <ore-icon name=\"x\" size=\"16\"></ore-icon> |\n\n<div class=\"decision-callout\">\n\n**Use Forge when** form state needs framework-independent immutable values, typed object fields, and one explicit validation boundary.\n\n**Consider framework-owned form state when** application only needs a single UI framework's native input bindings.\n\n</div>\n\n## Installation\n\n::: code-group\n\n```sh [pnpm]\npnpm add @vielzeug/forge\n```\n\n```sh [npm]\nnpm install @vielzeug/forge\n```\n\n```sh [yarn]\nyarn add @vielzeug/forge\n```\n\n:::\n\nInstall `@vielzeug/spell` or `@vielzeug/vault` only when importing Forge's matching optional adapter.\n\n## Quick Start\n\nCreate a form, update a focused field, and submit only after validation passes.\n\n```ts\nimport { createForm } from '@vielzeug/forge';\n\nconst form = createForm({\n initialValues: { profile: { email: '', name: '' } },\n validate: (value) => ({\n fields: { profile: { email: value.profile.email.includes('@') ? undefined : 'Invalid email' } },\n }),\n});\n\nform.field('profile').field('email').set('ada@example.com');\n\nconst result = await form.submit(async (value) => {\n const response = await fetch('/api/profile', {\n body: JSON.stringify(value),\n headers: { 'Content-Type': 'application/json' },\n method: 'POST',\n });\n\n return response.ok;\n});\n\nif (!result.ok && result.type === 'validation') console.log(result.errors);\n```\n\n## Features\n\n<div class=\"features-grid\">\n\n- `form.value` exposes one immutable nested value tree.\n- `form.field(key)` selects typed object branches without string paths.\n- `field.set(updater)` replaces array values without index handles.\n- `form.validate()` returns valid, invalid, or aborted results.\n- `form.submit(handler)` touches, validates, and invokes the handler when valid.\n- `bindField()` connects one DOM element without owning validation timing.\n- `customValidator()` maps Spell schema errors into Forge fields.\n- `saveForm()` and `loadForm()` persist explicit Vault draft records.\n\n</div>\n\n## Documentation\n\n<div class=\"doc-links\">\n\n- [Usage Guide](./usage.md)\n- [API Reference](./api.md)\n- [Examples](./examples.md)\n- [Migration Guide](./migration.md)\n\n</div>\n\n## See Also\n\n<div class=\"see-also\">\n\n- [Spell](/spell/) — adapt a Spell schema through `customValidator()`.\n- [Vault](/vault/) — save and restore explicit Forge draft records.\n- [Courier](/courier/) — send a validated form value through a mutation.\n\n</div>\n\n<!-- markdownlint-enable -->\n",
5
- "api": "---\ntitle: Forge — API Reference\ndescription: Complete reference for immutable forms, fields, validation, serialization, and optional adapters.\n---\n\n[[toc]]\n\n## API Overview\n\n| Symbol | Purpose | Execution mode | Common gotcha |\n| --- | --- | --- | --- |\n| `createForm()` | Create immutable form state | Sync | `initialValues` cannot contain mutable class instances |\n| `form.field()` | Select a top-level or object child field | Sync | Arrays have no index field handles |\n| `form.validate()` | Validate complete value | Async | Handle `aborted` separately |\n| `form.submit()` | Touch, validate, then invoke handler | Async | Concurrent calls reject |\n| `form.reset()` | Restore or replace baseline | Sync | `reset(next)` makes `next` clean |\n| `form.subscribe()` | Observe form metadata | Sync | Throws after disposal |\n| `toFormData()` | Serialize values for multipart transport | Sync | `FileList` is transport-only |\n| `bindField()` | Bind one DOM element | Sync | Does not schedule validation |\n| `customValidator()` | Adapt a Spell schema | Async | Does not transform `form.value` |\n| `saveForm()` / `loadForm()` | Persist explicit Vault records | Async | FormDraftCodec owns record shape |\n\n## Package Entry Point\n\n| Import | Purpose |\n| --- | --- |\n| `@vielzeug/forge` | Core form factory, serialization helper, types, and errors |\n| `@vielzeug/forge/dom` | `bindField()` and DOM binding types |\n| `@vielzeug/forge/spell` | `customValidator()` |\n| `@vielzeug/forge/vault` | `saveForm()`, `loadForm()`, and `FormDraftCodec` |\n\n## Core Functions\n\n### `createForm(options)`\n\n```ts\nfunction createForm<TValues extends Record<string, unknown>>(options: FormOptions<TValues>): Form<TValues>;\n```\n\nCreates a form with immutable initial values and an optional full-form validator.\n\n| Parameter | Type | Description |\n| --- | --- | --- |\n| `options.initialValues` | `TValues` | Initial value and reset baseline. Supports primitives, plain objects, arrays, `File`, and `Blob`. |\n| `options.validate` | `FormValidator<TValues>` | Optional validator for the entire current value. |\n| `options.onSubscriberError` | `(error: unknown) => void` | Optional subscriber failure reporter. |\n\n**Returns:** `Form<TValues>`.\n\n**Example:**\n\n```ts\nimport { createForm } from '@vielzeug/forge';\n\nconst form = createForm({ initialValues: { email: '' } });\n```\n\n---\n\n### `toFormData(values)`\n\n```ts\nfunction toFormData(values: Record<string, unknown>): FormData;\n```\n\nConverts nested values into `FormData` with dot-separated object keys and repeated array keys.\n\n**Returns:** a populated `FormData` instance.\n\n**Example:**\n\n```ts\nimport { toFormData } from '@vielzeug/forge';\n\nconst body = toFormData({ profile: { email: 'ada@example.com' }, tags: ['typescript', 'forms'] });\n```\n\n## Form Handles\n\n### `Form<TValues>`\n\n`createForm()` returns this handle.\n\n| Member | Signature | Description |\n| --- | --- | --- |\n| `value` | `ReadonlyDeep<TValues>` | Current immutable value. |\n| `state` | `FormState<TValues>` | Submission, validation, touch, and error metadata. |\n| `field(key)` | `Field<TValues[K]>` | Select a top-level field. |\n| `set(next)` | `void` | Replace the complete value or derive a replacement. |\n| `reset(next?)` | `void` | Restore baseline or make `next` the baseline. |\n| `validate(signal?)` | `Promise<ValidationResult<TValues>>` | Run full-form validation. |\n| `submit(handler)` | `Promise<SubmitResult<TResult, TValues>>` | Touch, validate, and invoke handler when valid. |\n| `subscribe(listener, options?)` | `Unsubscribe` | Observe form state; throws after disposal. |\n| `dispose()` | `void` | Abort validation and clear subscribers. |\n| `disposed` | `boolean` | Whether the form has been disposed. |\n| `disposalSignal` | `AbortSignal` | Aborts on disposal. |\n\n### `Field<V>`\n\n`form.field(key)` and object-field `.field(key)` return this handle.\n\n| Member | Signature | Description |\n| --- | --- | --- |\n| `value` | `ReadonlyDeep<V>` | Current immutable branch value. |\n| `error` | `string \\| undefined` | Current field error. |\n| `dirty` | `boolean` | Whether branch differs from baseline. |\n| `touched` | `boolean` | Whether field was touched. |\n| `field(key)` | `Field<V[K]>` | Select child object field only. |\n| `set(next)` | `void` | Replace branch or derive a replacement. |\n| `reset()` | `void` | Restore exact baseline branch. |\n| `touch()` | `void` | Mark field touched. |\n| `subscribe(listener, options?)` | `Unsubscribe` | Observe field transitions; throws after disposal. |\n\n## Validation Results\n\n### `form.validate(signal?)`\n\n```ts\nfunction validate(signal?: AbortSignal): Promise<ValidationResult<TValues>>;\n```\n\nRuns the configured validator against the complete value. A newer validation aborts the older run.\n\n**Returns:** `ValidationResult<TValues>`.\n\n```ts\nconst result = await form.validate();\n\nif (result.status === 'invalid') console.log(result.errors, result.formError);\n```\n\n### `form.submit(handler)`\n\n```ts\nfunction submit<TResult = void>(handler: (values: ReadonlyDeep<TValues>) => MaybePromise<TResult>): Promise<SubmitResult<TResult, TValues>>;\n```\n\nTouches all fields, validates once, and invokes `handler` when validation is valid.\n\n**Returns:** `SubmitResult<TResult, TValues>`. Handler failures reject normally.\n\n```ts\nconst result = await form.submit((value) => Promise.resolve(value));\n```\n\n## Adapters\n\n### `bindField(element, field, options)`\n\n```ts\nfunction bindField<Element extends HTMLElement, V>(\n element: Element,\n field: Field<V>,\n options: FieldBindingOptions<Element, V>,\n): () => void;\n```\n\nBinds one field to one element, marks it touched on blur, suppresses writeback from its own input event, and returns teardown.\n\n**Example:**\n\n```ts\nimport { bindField } from '@vielzeug/forge/dom';\n\nconst stop = bindField(input, form.field('email'), {\n read: (element) => element.value,\n write: (element, value) => {\n element.value = value;\n },\n});\n```\n\n---\n\n### `customValidator(schema)`\n\n```ts\nfunction customValidator<TValues extends Record<string, unknown>>(\n schema: Schema<unknown, TValues, SchemaMode>,\n): FormValidator<TValues>;\n```\n\nAdapts a Spell schema. Every failing union maps its closest branch while preserving unrelated errors. Array item issues map to the parent array field; duplicate paths retain the first message.\n\n**Example:**\n\n```ts\nimport { customValidator } from '@vielzeug/forge/spell';\nimport { s } from '@vielzeug/spell';\n\nconst Profile = s.object({ email: s.string().email() });\nconst form = createForm({ initialValues: { email: '' }, validate: customValidator(Profile) });\n```\n\n---\n\n### `saveForm()` and `loadForm()`\n\n```ts\nfunction saveForm<TValues extends Record<string, unknown>, S extends AnySchema, K extends keyof S & string>(\n form: Form<TValues>, adapter: VaultStore<S>, table: K, codec: FormDraftCodec<TValues, S, K>,\n): Promise<void>;\n\nfunction loadForm<TValues extends Record<string, unknown>, S extends AnySchema, K extends keyof S & string>(\n form: Form<TValues>, adapter: VaultStore<S>, table: K, key: KeyOf<S, K>, codec: FormDraftCodec<TValues, S, K>,\n): Promise<boolean>;\n```\n\nPersists or restores a codec-defined Vault record. `loadForm()` calls `form.reset()` when the codec decodes a record.\n\n**Returns:** `loadForm()` returns `false` for a missing or rejected record.\n\n## Types\n\n```ts\ntype Unsubscribe = () => void;\ntype MaybePromise<T> = T | PromiseLike<T>;\ntype ReadonlyDeep<T> = T extends (...args: never[]) => unknown\n ? T\n : T extends readonly (infer Item)[]\n ? readonly ReadonlyDeep<Item>[]\n : T extends Record<string, unknown>\n ? { readonly [K in keyof T]: ReadonlyDeep<T[K]> }\n : T;\n\ntype FormErrors<T> = T extends readonly unknown[]\n ? string\n : T extends Record<string, unknown>\n ? string | { readonly [K in keyof T]?: FormErrors<T[K]> }\n : string;\n\ntype ValidationErrors<TValues extends Record<string, unknown>> = Readonly<{\n fields?: FormErrors<TValues>;\n formError?: string;\n}>;\n\ntype FormValidator<TValues extends Record<string, unknown>> = (\n values: ReadonlyDeep<TValues>, signal: AbortSignal,\n) => MaybePromise<ValidationErrors<TValues> | undefined>;\n\ntype FormOptions<TValues extends Record<string, unknown>> = Readonly<{\n initialValues: TValues;\n onSubscriberError?: (error: unknown) => void;\n validate?: FormValidator<NoInfer<TValues>>;\n}>;\n\ntype SubscribeOptions = Readonly<{ immediate?: boolean }>;\n\ntype FieldState<V> = Readonly<{\n dirty: boolean;\n error: string | undefined;\n touched: boolean;\n value: ReadonlyDeep<V>;\n}>;\n\ntype FormState<TValues extends Record<string, unknown>> = Readonly<{\n error: string | undefined;\n errors: FormErrors<TValues> | undefined;\n submitCount: number;\n submitting: boolean;\n touched: boolean;\n valid: boolean;\n validating: boolean;\n}>;\n\ntype ValidationResult<TValues extends Record<string, unknown>> =\n | Readonly<{ status: 'aborted' }>\n | Readonly<{ status: 'valid' }>\n | Readonly<{ errors: FormErrors<TValues> | undefined; formError: string | undefined; status: 'invalid' }>;\n\ntype SubmitResult<TResult = void, TValues extends Record<string, unknown> = Record<string, unknown>> =\n | Readonly<{ ok: true; value: TResult }>\n | Readonly<{ ok: false; type: 'aborted' }>\n | Readonly<{ errors: FormErrors<TValues> | undefined; formError: string | undefined; ok: false; type: 'validation' }>;\n```\n\n```ts\ntype Field<V> = {\n readonly dirty: boolean;\n readonly error: string | undefined;\n readonly touched: boolean;\n readonly value: ReadonlyDeep<V>;\n field<K extends keyof NonNullable<V> & string>(key: K): Field<NonNullable<V>[K]>;\n reset(): void;\n set(next: V | ((previous: ReadonlyDeep<V>) => V)): void;\n subscribe(listener: (state: FieldState<V>) => void, options?: SubscribeOptions): Unsubscribe;\n touch(): void;\n};\n\ntype Form<TValues extends Record<string, unknown>> = {\n [Symbol.dispose](): void;\n readonly disposalSignal: AbortSignal;\n readonly disposed: boolean;\n readonly state: FormState<TValues>;\n readonly value: ReadonlyDeep<TValues>;\n dispose(): void;\n field<K extends keyof TValues & string>(key: K): Field<TValues[K]>;\n reset(next?: TValues): void;\n set(next: TValues | ((previous: ReadonlyDeep<TValues>) => TValues)): void;\n submit<TResult = void>(handler: (values: ReadonlyDeep<TValues>) => MaybePromise<TResult>): Promise<SubmitResult<TResult, TValues>>;\n subscribe(listener: (state: FormState<TValues>) => void, options?: SubscribeOptions): Unsubscribe;\n validate(signal?: AbortSignal): Promise<ValidationResult<TValues>>;\n};\n\ntype FieldBindingOptions<Element extends HTMLElement, V> = Readonly<{\n event?: keyof HTMLElementEventMap;\n read(element: Element): V;\n write?: (element: Element, value: ReadonlyDeep<V>) => void;\n}>;\n\ntype FormDraftCodec<TValues extends Record<string, unknown>, S extends AnySchema, K extends keyof S & string> = Readonly<{\n fromRecord(record: RecordOf<S, K>): TValues | undefined;\n toRecord(values: ReadonlyDeep<TValues>): RecordOf<S, K>;\n}>;\n```\n\n## Errors\n\n| Error | Trigger | Notable properties |\n| --- | --- | --- |\n| `ForgeError` | Base Forge error | `ForgeError.is(error)` narrows unknown values. |\n| `ForgeConfigError` | Unsafe key or unsupported form value | Extends `ForgeError`. |\n| `ForgeDisposedError` | Operation or subscription after disposal | Message names the attempted operation. |\n| `ForgeSubmitError` | Concurrent `submit()` call | Extends `ForgeError`. |\n| `ForgeValidationError` | Validator throws unexpectedly | Preserves original error as `cause`. |\n",
6
- "usage": "---\ntitle: Forge — Usage Guide\ndescription: Build immutable forms, validate whole values, and use optional adapters.\n---\n\n[[toc]]\n\n## Basic Usage\n\nCreate one form value and update object branches through stable typed operations. Form values support primitives, plain objects, arrays, `File`, and `Blob`; mutable class instances such as `Date`, `Map`, and `Set` are rejected.\n\n```ts\nimport { createForm } from '@vielzeug/forge';\n\nconst form = createForm({\n initialValues: { profile: { email: '', name: '' }, tags: [] as string[] },\n validate: (value) => ({\n fields: { profile: { email: value.profile.email.includes('@') ? undefined : 'Invalid email' } },\n }),\n});\n\nconst email = form.field('profile').field('email');\nemail.set('ada@example.com');\nform.field('tags').set((tags) => [...tags, 'typescript']);\n\nconsole.log(form.value.profile.email);\n```\n\n## Reset Values and Branches\n\nReset a field when one branch should return to its exact baseline. Reset the form with a value when newly loaded data should become the clean baseline.\n\n```ts\nconst name = form.field('profile').field('name');\n\nname.set('Ada');\nname.touch();\nname.reset();\n\nform.reset({ profile: { email: 'ada@example.com', name: 'Ada' }, tags: [] });\n```\n\nAn absent optional parent remains absent after a child reset. Arrays are complete values; replace them with an updater instead of retaining index handles.\n\n## Validate and Submit\n\nReturn `fields` and an optional `formError` from one validator. `validate()` replaces the complete validation snapshot and returns an explicit status.\n\n```ts\nconst passwordForm = createForm({\n initialValues: { password: '', passwordConfirmation: '' },\n validate: (value) => ({\n fields: {\n password: value.password.length >= 8 ? undefined : 'Use at least eight characters',\n passwordConfirmation: value.password === value.passwordConfirmation ? undefined : 'Passwords must match',\n },\n }),\n});\n\nconst validation = await passwordForm.validate();\n\nif (validation.status === 'invalid') console.log(validation.errors);\nif (validation.status === 'aborted') console.log('Validation cancelled');\n\nconst result = await passwordForm.submit((value) => Promise.resolve(value.password.length));\n\nif (result.ok) console.log(result.value);\n```\n\nStarting another validation aborts the previous run. Field edits preserve existing errors until the next validation replaces them. Unexpected validator failures reject as `ForgeValidationError` with the original error as `cause`.\n\n## Observe State\n\nUse form subscriptions for aggregate metadata and field subscriptions for one branch. Subscribing after disposal throws `ForgeDisposedError`.\n\n```ts\nconst errors: unknown[] = [];\nconst observedForm = createForm({\n initialValues: { email: '' },\n onSubscriberError: (error) => errors.push(error),\n});\n\nconst stopForm = observedForm.subscribe((state) => {\n console.log(state.valid, state.submitting);\n}, { immediate: true });\nconst stopField = observedForm.field('email').subscribe((state) => {\n console.log(state.value, state.error);\n}, { immediate: true });\n\nstopField();\nstopForm();\n```\n\nWithout `onSubscriberError`, Forge rethrows subscriber failures asynchronously after completing its state transition.\n\n## Testing\n\nTest the form without a DOM. Read its immutable value, invoke a method, then assert the resulting state or validation result.\n\n```ts\nimport { expect, test } from 'vitest';\nimport { createForm } from '@vielzeug/forge';\n\ntest('requires an email address', async () => {\n const form = createForm({\n initialValues: { email: '' },\n validate: (value) => ({ fields: { email: value.email.includes('@') ? undefined : 'Invalid email' } }),\n });\n\n await expect(form.validate()).resolves.toEqual({\n errors: { email: 'Invalid email' },\n formError: undefined,\n status: 'invalid',\n });\n});\n```\n\n## Framework Integration\n\nUse `form.value` and subscriptions with any renderer. Bind one DOM input through `/dom`; validation scheduling remains application policy.\n\n::: code-group\n\n```ts [React]\nimport { useEffect, useState } from 'react';\nimport { createForm } from '@vielzeug/forge';\n\nconst form = createForm({ initialValues: { email: '' } });\n\nexport function EmailForm() {\n const [, rerender] = useState(0);\n\n useEffect(() => {\n const stop = form.subscribe(() => rerender((revision) => revision + 1));\n\n return () => stop();\n }, []);\n\n return <input value={form.field('email').value} onChange={(event) => form.field('email').set(event.target.value)} />;\n}\n```\n\n```ts [Vue 3]\nimport { onUnmounted, ref } from 'vue';\nimport { createForm } from '@vielzeug/forge';\n\nconst form = createForm({ initialValues: { email: '' } });\nconst revision = ref(0);\nconst stop = form.subscribe(() => revision.value++);\n\nonUnmounted(stop);\n```\n\n```ts [Svelte]\n<script lang=\"ts\">\n import { onDestroy } from 'svelte';\n import { createForm } from '@vielzeug/forge';\n\n const form = createForm({ initialValues: { email: '' } });\n let revision = 0;\n const stop = form.subscribe(() => revision++);\n\n onDestroy(stop);\n</script>\n\n<input value={form.field('email').value} on:input={(event) => form.field('email').set(event.currentTarget.value)} />\n```\n\n:::\n\n## Working with Other Vielzeug Libraries\n\nUse Spell when one schema owns validation and Vault when an explicit record codec owns persistence.\n\n```ts\nimport { createForm } from '@vielzeug/forge';\nimport { customValidator } from '@vielzeug/forge/spell';\nimport { s } from '@vielzeug/spell';\n\nconst Profile = s.object({ email: s.string().email() });\nconst form = createForm({ initialValues: { email: '' }, validate: customValidator(Profile) });\n```\n\n`customValidator()` preserves unrelated Spell errors, maps each union to its closest branch, and maps array-item failures to the parent array field. Parse again at the submit boundary when a Spell transform must produce the outgoing payload.\n\n```ts\nimport { loadForm, saveForm } from '@vielzeug/forge/vault';\n\nawait saveForm(form, db, 'drafts', codec);\nconst restored = await loadForm(form, db, 'drafts', 'profile', codec);\nconsole.log(restored);\n```\n\n`loadForm()` uses `form.reset()`, so a restored value is clean. Store a selected `File`, not `FileList`, in form state; `FileList` is transport-only for `toFormData()`.\n\n## Best Practices\n\n- Keep form values to primitives, plain objects, arrays, `File`, and `Blob`.\n- Update array fields through immutable replacement functions.\n- Validate complete values instead of rebuilding field-validator graphs.\n- Handle `aborted` validation results before rendering errors.\n- Preserve errors through field edits until a deliberate validation refresh.\n- Return subscription cleanup from framework lifecycle hooks.\n- Provide `onSubscriberError` when application subscribers can throw.\n- Decode Vault records before passing them to `loadForm()`.\n",
4
+ "index": "---\ntitle: Forge — Immutable form state for TypeScript\ndescription: Framework-agnostic immutable form state with focused object fields and explicit validation results.\npackage: forge\ncategory: forms\nkeywords: [form-state, validation, immutable, input, submission]\nrelated: [spell, vault, courier]\nexports: [createForm, bindField, customValidator, saveForm, loadForm]\nenvironments: [browser, node, ssr, deno]\n---\n\n<!-- markdownlint-disable MD025 MD033 MD060 -->\n\n<PackageHero package=\"forge\" />\n\n## Why Forge?\n\nNative form state becomes difficult to inspect once values, validation, draft restoration, and UI bindings share mutable objects. Forge owns one immutable value tree and gives you typed handles for object branches without string paths, scoped controllers, or framework state.\n\n```ts\n// Before\nconst values = { email: '', password: '' };\nconst errors: Record<string, string> = {};\n\nfunction submit() {\n errors.email = values.email.includes('@') ? '' : 'Invalid email';\n errors.password = values.password.length >= 8 ? '' : 'Use at least eight characters';\n}\n\n// After\nconst form = createForm({\n initialValues: { email: '', password: '' },\n validate: (value) => ({\n fields: {\n email: value.email.includes('@') ? undefined : 'Invalid email',\n password: value.password.length >= 8 ? undefined : 'Use at least eight characters',\n },\n }),\n});\n```\n\n| Feature | Forge | Native form state | Framework-owned form state |\n| --- | --- | --- | --- |\n| Bundle size | <PackageInfo package=\"forge\" type=\"size\" /> | <ore-icon name=\"check\" size=\"16\"></ore-icon> | Varies |\n| Zero external dependencies | <ore-icon name=\"check\" size=\"16\"></ore-icon> | <ore-icon name=\"check\" size=\"16\"></ore-icon> | <ore-icon name=\"x\" size=\"16\"></ore-icon> |\n| Immutable nested values | <ore-icon name=\"check\" size=\"16\"></ore-icon> | <ore-icon name=\"x\" size=\"16\"></ore-icon> | Varies |\n| Typed object field handles | <ore-icon name=\"check\" size=\"16\"></ore-icon> | <ore-icon name=\"x\" size=\"16\"></ore-icon> | Varies |\n| Framework-independent state | <ore-icon name=\"check\" size=\"16\"></ore-icon> | <ore-icon name=\"check\" size=\"16\"></ore-icon> | <ore-icon name=\"x\" size=\"16\"></ore-icon> |\n\n<div class=\"decision-callout\">\n\n**Use Forge when** form state needs framework-independent immutable values, typed object fields, and one explicit validation boundary.\n\n**Consider framework-owned form state when** application only needs a single UI framework's native input bindings.\n\n</div>\n\n## Installation\n\n::: code-group\n\n```sh [pnpm]\npnpm add @vielzeug/forge\n```\n\n```sh [npm]\nnpm install @vielzeug/forge\n```\n\n```sh [yarn]\nyarn add @vielzeug/forge\n```\n\n:::\n\nInstall `@vielzeug/spell` or `@vielzeug/vault` only when importing Forge's matching optional adapter.\n\n## Quick Start\n\nCreate a form, update a focused field, and submit only after validation passes.\n\n```ts\nimport { createForm } from '@vielzeug/forge';\n\nconst form = createForm({\n initialValues: { profile: { email: '', name: '' } },\n validate: (value) => ({\n fields: { profile: { email: value.profile.email.includes('@') ? undefined : 'Invalid email' } },\n }),\n});\n\nform.field('profile').field('email').set('ada@example.com');\n\nconst result = await form.submit(async (value) => {\n const response = await fetch('/api/profile', {\n body: JSON.stringify(value),\n headers: { 'Content-Type': 'application/json' },\n method: 'POST',\n });\n\n return response.ok;\n});\n\nif (result.status === 'invalid') console.log(result.errors);\n```\n\n## Features\n\n<div class=\"features-grid\">\n\n- `form.value` exposes one immutable nested value tree.\n- `form.field(key)` selects typed object branches without string paths.\n- `field.set(updater)` replaces array values through immutable updater functions.\n- `field.field(index)` selects typed array item fields by index.\n- `form.validate()` returns valid, invalid, or aborted results.\n- `form.submit(handler, signal?)` touches, validates, and invokes the handler when valid.\n- `bindField()` connects one DOM element without owning validation timing.\n- `customValidator()` maps Spell schema errors into Forge fields.\n- `saveForm()` and `loadForm()` persist explicit Vault draft records.\n\n</div>\n\n## Documentation\n\n<div class=\"doc-links\">\n\n- [Usage Guide](./usage.md)\n- [API Reference](./api.md)\n- [Examples](./examples.md)\n- [Migration Guide](./migration.md)\n\n</div>\n\n## See Also\n\n<div class=\"see-also\">\n\n- [Spell](/spell/) — adapt a Spell schema through `customValidator()`.\n- [Vault](/vault/) — save and restore explicit Forge draft records.\n- [Courier](/courier/) — send a validated form value through a mutation.\n\n</div>\n\n<!-- markdownlint-enable -->\n",
5
+ "api": "---\ntitle: Forge — API Reference\ndescription: Complete reference for immutable forms, fields, validation, serialization, and optional adapters.\n---\n\n[[toc]]\n\n## API Overview\n\n| Symbol | Purpose | Execution mode | Common gotcha |\n| --- | --- | --- | --- |\n| `createForm()` | Create immutable form state | Sync | `initialValues` cannot contain mutable class instances |\n| `form.field()` | Select a top-level or object child field | Sync | Unsafe keys (`__proto__`, `constructor`, `prototype`) are rejected |\n| `form.validate()` | Validate complete value | Async | Handle `aborted` separately |\n| `form.submit(handler, signal?)` | Touch, validate, then invoke handler | Async | Concurrent calls reject |\n| `form.reset()` | Restore or replace baseline | Sync | `reset(next)` makes `next` clean |\n| `form.subscribe()` | Observe form metadata | Sync | Throws after disposal |\n| `toFormData()` | Serialize values for multipart transport | Sync | `FileList` is transport-only |\n| `bindField()` | Bind one DOM element | Sync | Does not schedule validation |\n| `customValidator()` | Adapt a Spell schema | Async | Does not transform `form.value` |\n| `saveForm()` / `loadForm()` | Persist explicit Vault records | Async | FormDraftCodec owns record shape |\n\n## Package Entry Point\n\n| Import | Purpose |\n| --- | --- |\n| `@vielzeug/forge` | Core form factory, types, and errors |\n| `@vielzeug/forge/dom` | `bindField()` and DOM binding types |\n| `@vielzeug/forge/form-data` | `toFormData()` |\n| `@vielzeug/forge/spell` | `customValidator()` |\n| `@vielzeug/forge/vault` | `saveForm()`, `loadForm()`, and `FormDraftCodec` |\n\n## Core Functions\n\n### `createForm(options)`\n\n```ts\nfunction createForm<TValues extends Record<string, unknown>>(options: FormOptions<TValues>): Form<TValues>;\n```\n\nCreates a form with immutable initial values and an optional full-form validator.\n\n| Parameter | Type | Description |\n| --- | --- | --- |\n| `options.initialValues` | `TValues` | Initial value and reset baseline. Supports primitives, plain objects, arrays, `Date`, `File`, and `Blob`. |\n| `options.validate` | `FormValidator<TValues>` | Optional validator for the entire current value. |\n| `options.onSubscriberError` | `(error: unknown) => void` | Optional subscriber failure reporter. |\n\n**Returns:** `Form<TValues>`.\n\n**Example:**\n\n```ts\nimport { createForm } from '@vielzeug/forge';\n\nconst form = createForm({ initialValues: { email: '' } });\n```\n\n---\n\n### `toFormData(values)`\n\n```ts\nfunction toFormData(values: Record<string, unknown>): FormData;\n```\n\nConverts nested values into `FormData` with dot-separated object keys and repeated array keys.\n\n**Returns:** a populated `FormData` instance.\n\n**Example:**\n\n```ts\nimport { toFormData } from '@vielzeug/forge/form-data';\n\nconst body = toFormData({ profile: { email: 'ada@example.com' }, tags: ['typescript', 'forms'] });\n```\n\n## Form Handles\n\n### `Form<TValues>`\n\n`createForm()` returns this handle.\n\n| Member | Signature | Description |\n| --- | --- | --- |\n| `value` | `ReadonlyDeep<TValues>` | Current immutable value. |\n| `state` | `FormState<TValues>` | Submission, validation, touch, and error metadata. |\n| `field(key)` | `Field<TValues[K]>` | Select a top-level field. |\n| `set(next)` | `void` | Replace the complete value or derive a replacement. |\n| `reset(next?)` | `void` | Restore baseline or make `next` the baseline. |\n| `validate(signal?)` | `Promise<ValidationResult<TValues>>` | Run full-form validation. |\n| `submit(handler, signal?)` | `Promise<SubmitResult<TResult, TValues>>` | Touch, validate, and invoke handler when valid. |\n| `subscribe(listener, options?)` | `Unsubscribe` | Observe form state; throws after disposal. |\n| `dispose()` | `void` | Abort validation and clear subscribers. |\n| `disposed` | `boolean` | Whether the form has been disposed. |\n| `disposalSignal` | `AbortSignal` | Aborts on disposal. |\n\n### `Field<V>`\n\n`form.field(key)` and object-field `.field(key)` return this handle. Array-item `.field(index)` returns a per-item field handle.\n\n| Member | Signature | Description |\n| --- | --- | --- |\n| `value` | `ReadonlyDeep<V>` | Current immutable branch value. |\n| `error` | `string \\| undefined` | Current field error. |\n| `dirty` | `boolean` | Whether branch differs from baseline. |\n| `touched` | `boolean` | Whether field was touched. |\n| `state` | `FieldState<V>` | Snapshot of `dirty`, `error`, `touched`, and `value` in one read. |\n| `field(key)` | `Field<V[K]>` | Select child object field or array item by index. |\n| `set(next)` | `void` | Replace branch or derive a replacement. |\n| `reset()` | `void` | Restore exact baseline branch. |\n| `touch()` | `void` | Mark field touched. |\n| `subscribe(listener, options?)` | `Unsubscribe` | Observe field transitions; throws after disposal. |\n\n## Validation Results\n\n### `form.validate(signal?)`\n\n```ts\nfunction validate(signal?: AbortSignal): Promise<ValidationResult<TValues>>;\n```\n\nRuns the configured validator against the complete value. A newer validation aborts the older run.\n\n**Returns:** `ValidationResult<TValues>`.\n\n```ts\nconst result = await form.validate();\n\nif (result.status === 'invalid') console.log(result.errors, result.formError);\n```\n\n### `form.submit(handler, signal?)`\n\n```ts\nfunction submit<TResult = void>(\n handler: (values: ReadonlyDeep<TValues>, signal: AbortSignal) => MaybePromise<TResult>,\n signal?: AbortSignal,\n): Promise<SubmitResult<TResult, TValues>>;\n```\n\nTouches all fields, validates once, and invokes `handler` when validation is valid. The handler receives an `AbortSignal` that is aborted when the external `signal` (or the form's disposal signal) aborts.\n\n**Returns:** `SubmitResult<TResult, TValues>`. Handler failures reject normally unless caused by signal abort, which returns `{ status: 'aborted' }`.\n\n```ts\nconst result = await form.submit((value) => Promise.resolve(value));\n```\n\n## Adapters\n\n### `bindField(element, field, options)`\n\n```ts\nfunction bindField<Element extends HTMLElement, V>(\n element: Element,\n field: Field<V>,\n options: FieldBindingOptions<Element, V>,\n): () => void;\n```\n\nBinds one field to one element, marks it touched on blur, suppresses writeback from its own input event, and returns teardown.\n\n**Example:**\n\n```ts\nimport { bindField } from '@vielzeug/forge/dom';\n\nconst stop = bindField(input, form.field('email'), {\n read: (element) => element.value,\n write: (element, value) => {\n element.value = value;\n },\n});\n```\n\n---\n\n### `customValidator(schema)`\n\n```ts\nfunction customValidator<TValues extends Record<string, unknown>>(\n schema: Schema<unknown, TValues, SchemaMode>,\n): FormValidator<TValues>;\n```\n\nAdapts a Spell schema. Every failing union maps its closest branch while preserving unrelated errors. Array item issues map to per-item array fields; duplicate paths retain the first message.\n\n**Example:**\n\n```ts\nimport { customValidator } from '@vielzeug/forge/spell';\nimport { s } from '@vielzeug/spell';\n\nconst Profile = s.object({ email: s.string().email() });\nconst form = createForm({ initialValues: { email: '' }, validate: customValidator(Profile) });\n```\n\n---\n\n### `saveForm()` and `loadForm()`\n\n```ts\nfunction saveForm<TValues extends Record<string, unknown>, S extends AnySchema, K extends keyof S & string>(\n form: Form<TValues>, adapter: VaultStore<S>, table: K, codec: FormDraftCodec<TValues, S, K>,\n): Promise<void>;\n\nfunction loadForm<TValues extends Record<string, unknown>, S extends AnySchema, K extends keyof S & string>(\n form: Form<TValues>, adapter: VaultStore<S>, table: K, key: KeyOf<S, K>, codec: FormDraftCodec<TValues, S, K>,\n): Promise<boolean>;\n```\n\nPersists or restores a codec-defined Vault record. `loadForm()` calls `form.reset()` when the codec decodes a record.\n\n**Returns:** `loadForm()` returns `false` for a missing or rejected record.\n\n## Types\n\n```ts\ntype Unsubscribe = () => void;\ntype MaybePromise<T> = T | PromiseLike<T>;\ntype ReadonlyDeep<T> = T extends (...args: never[]) => unknown\n ? T\n : T extends readonly (infer Item)[]\n ? readonly ReadonlyDeep<Item>[]\n : T extends Record<string, unknown>\n ? { readonly [K in keyof T]: ReadonlyDeep<T[K]> }\n : T;\n\ntype FormErrors<T> = T extends readonly (infer Item)[]\n ? string | readonly (FormErrors<Item> | undefined)[]\n : T extends Record<string, unknown>\n ? string | { readonly [K in keyof T]?: FormErrors<T[K]> }\n : string;\n\ntype ValidationErrors<TValues extends Record<string, unknown>> = Readonly<{\n fields?: FormErrors<TValues>;\n formError?: string;\n}>;\n\ntype FormValidator<TValues extends Record<string, unknown>> = (\n values: ReadonlyDeep<TValues>, signal: AbortSignal,\n) => MaybePromise<ValidationErrors<TValues> | undefined>;\n\ntype FormOptions<TValues extends Record<string, unknown>> = Readonly<{\n initialValues: TValues;\n onSubscriberError?: (error: unknown) => void;\n validate?: FormValidator<NoInfer<TValues>>;\n}>;\n\ntype SubscribeOptions = Readonly<{ immediate?: boolean }>;\n\ntype FieldState<V> = Readonly<{\n dirty: boolean;\n error: string | undefined;\n touched: boolean;\n value: ReadonlyDeep<V>;\n}>;\n\ntype FormState<TValues extends Record<string, unknown>> = Readonly<{\n errors: FormErrors<TValues> | undefined;\n formError: string | undefined;\n hasErrors: boolean;\n submitCount: number;\n submitting: boolean;\n touched: boolean;\n validity: 'invalid' | 'unknown' | 'valid';\n validating: boolean;\n}>;\n\ntype ValidationResult<TValues extends Record<string, unknown>> =\n | Readonly<{ status: 'aborted' }>\n | Readonly<{ status: 'valid' }>\n | Readonly<{ errors: FormErrors<TValues> | undefined; formError: string | undefined; status: 'invalid' }>;\n\ntype SubmitResult<TResult = void, TValues extends Record<string, unknown> = Record<string, unknown>> =\n | Readonly<{ status: 'aborted' }>\n | Readonly<{ errors: FormErrors<TValues> | undefined; formError: string | undefined; status: 'invalid' }>\n | Readonly<{ status: 'ok'; value: TResult }>;\n```\n\n```ts\ntype ChildField<V> =\n NonNullable<V> extends readonly (infer Item)[]\n ? { field(index: number): Field<Item> }\n : NonNullable<V> extends Record<string, unknown>\n ? { field<K extends keyof NonNullable<V> & string>(key: K): Field<NonNullable<V>[K]> }\n : Record<never, never>;\n\ntype Field<V> = ChildField<V> & {\n readonly dirty: boolean;\n readonly error: string | undefined;\n readonly state: FieldState<V>;\n readonly touched: boolean;\n readonly value: ReadonlyDeep<V>;\n reset(): void;\n set(next: V | ((previous: ReadonlyDeep<V>) => V)): void;\n subscribe(listener: (state: FieldState<V>) => void, options?: SubscribeOptions): Unsubscribe;\n touch(): void;\n};\n\ntype Form<TValues extends Record<string, unknown>> = {\n [Symbol.dispose](): void;\n readonly disposalSignal: AbortSignal;\n readonly disposed: boolean;\n readonly state: FormState<TValues>;\n readonly value: ReadonlyDeep<TValues>;\n dispose(): void;\n field<K extends keyof TValues & string>(key: K): Field<TValues[K]>;\n reset(next?: TValues): void;\n set(next: TValues | ((previous: ReadonlyDeep<TValues>) => TValues)): void;\n submit<TResult = void>(\n handler: (values: ReadonlyDeep<TValues>, signal: AbortSignal) => MaybePromise<TResult>,\n signal?: AbortSignal,\n ): Promise<SubmitResult<TResult, TValues>>;\n subscribe(listener: (state: FormState<TValues>) => void, options?: SubscribeOptions): Unsubscribe;\n validate(signal?: AbortSignal): Promise<ValidationResult<TValues>>;\n};\n\ntype FieldBindingOptions<Element extends HTMLElement, V> = Readonly<{\n event?: keyof HTMLElementEventMap;\n read(element: Element): V;\n write?: (element: Element, value: ReadonlyDeep<V>) => void;\n}>;\n\ntype FormDraftCodec<TValues extends Record<string, unknown>, S extends AnySchema, K extends keyof S & string> = Readonly<{\n fromRecord(record: RecordOf<S, K>): TValues | undefined;\n toRecord(values: ReadonlyDeep<TValues>): RecordOf<S, K>;\n}>;\n```\n\n## Errors\n\n| Error | Trigger | Notable properties |\n| --- | --- | --- |\n| `ForgeError` | Base Forge error | `ForgeError.is(error)` narrows unknown values. |\n| `ForgeConfigError` | Unsafe key or unsupported form value | Extends `ForgeError`. |\n| `ForgeDisposedError` | Operation or subscription after disposal | Message names the attempted operation. |\n| `ForgeSubmitError` | Concurrent `submit()` call | Extends `ForgeError`. |\n| `ForgeValidationError` | Validator throws unexpectedly | Preserves original error as `cause`. |\n",
6
+ "usage": "---\ntitle: Forge — Usage Guide\ndescription: Build immutable forms, validate whole values, and use optional adapters.\n---\n\n[[toc]]\n\n## Basic Usage\n\nCreate one form value and update object branches through stable typed operations. Form values support primitives, plain objects, arrays, `Date`, `File`, and `Blob`; mutable class instances such as `Map` and `Set` are rejected.\n\n```ts\nimport { createForm } from '@vielzeug/forge';\n\nconst form = createForm({\n initialValues: { profile: { email: '', name: '' }, tags: [] as string[] },\n validate: (value) => ({\n fields: { profile: { email: value.profile.email.includes('@') ? undefined : 'Invalid email' } },\n }),\n});\n\nconst email = form.field('profile').field('email');\nemail.set('ada@example.com');\nform.field('tags').set((tags) => [...tags, 'typescript']);\n\nconsole.log(form.value.profile.email);\n```\n\n## Reset Values and Branches\n\nReset a field when one branch should return to its exact baseline. Reset the form with a value when newly loaded data should become the clean baseline.\n\n```ts\nconst name = form.field('profile').field('name');\n\nname.set('Ada');\nname.touch();\nname.reset();\n\nform.reset({ profile: { email: 'ada@example.com', name: 'Ada' }, tags: [] });\n```\n\nAn absent optional parent remains absent after a child reset. Array items support per-index field handles for reads, updates, and resets.\n\n## Validate and Submit\n\nReturn `fields` and an optional `formError` from one validator. `validate()` replaces the complete validation snapshot and returns an explicit status.\n\n```ts\nconst passwordForm = createForm({\n initialValues: { password: '', passwordConfirmation: '' },\n validate: (value) => ({\n fields: {\n password: value.password.length >= 8 ? undefined : 'Use at least eight characters',\n passwordConfirmation: value.password === value.passwordConfirmation ? undefined : 'Passwords must match',\n },\n }),\n});\n\nconst validation = await passwordForm.validate();\n\nif (validation.status === 'invalid') console.log(validation.errors);\nif (validation.status === 'aborted') console.log('Validation cancelled');\n\nconst result = await passwordForm.submit((value) => Promise.resolve(value.password.length));\n\nif (result.status === 'ok') console.log(result.value);\n```\n\nStarting another validation aborts the previous run. Field edits preserve existing errors until the next validation replaces them. Unexpected validator failures reject as `ForgeValidationError` with the original error as `cause`.\n\n## Observe State\n\nUse form subscriptions for aggregate metadata and field subscriptions for one branch. Subscribing after disposal throws `ForgeDisposedError`.\n\n```ts\nconst errors: unknown[] = [];\nconst observedForm = createForm({\n initialValues: { email: '' },\n onSubscriberError: (error) => errors.push(error),\n});\n\nconst stopForm = observedForm.subscribe((state) => {\n console.log(state.validity, state.submitting);\n}, { immediate: true });\nconst stopField = observedForm.field('email').subscribe((state) => {\n console.log(state.value, state.error);\n}, { immediate: true });\n\nstopField();\nstopForm();\n```\n\nWithout `onSubscriberError`, Forge rethrows subscriber failures asynchronously after completing its state transition.\n\n## Testing\n\nTest the form without a DOM. Read its immutable value, invoke a method, then assert the resulting state or validation result.\n\n```ts\nimport { expect, test } from 'vitest';\nimport { createForm } from '@vielzeug/forge';\n\ntest('requires an email address', async () => {\n const form = createForm({\n initialValues: { email: '' },\n validate: (value) => ({ fields: { email: value.email.includes('@') ? undefined : 'Invalid email' } }),\n });\n\n await expect(form.validate()).resolves.toEqual({\n errors: { email: 'Invalid email' },\n formError: undefined,\n status: 'invalid',\n });\n});\n```\n\n## Framework Integration\n\nUse `form.value` and subscriptions with any renderer. Bind one DOM input through `/dom`; validation scheduling remains application policy.\n\n::: code-group\n\n```ts [React]\nimport { useEffect, useState } from 'react';\nimport { createForm } from '@vielzeug/forge';\n\nconst form = createForm({ initialValues: { email: '' } });\n\nexport function EmailForm() {\n const [, rerender] = useState(0);\n\n useEffect(() => {\n const stop = form.subscribe(() => rerender((revision) => revision + 1));\n\n return () => stop();\n }, []);\n\n return <input value={form.field('email').value} onChange={(event) => form.field('email').set(event.target.value)} />;\n}\n```\n\n```ts [Vue 3]\nimport { onUnmounted, ref } from 'vue';\nimport { createForm } from '@vielzeug/forge';\n\nconst form = createForm({ initialValues: { email: '' } });\nconst revision = ref(0);\nconst stop = form.subscribe(() => revision.value++);\n\nonUnmounted(stop);\n```\n\n```ts [Svelte]\n<script lang=\"ts\">\n import { onDestroy } from 'svelte';\n import { createForm } from '@vielzeug/forge';\n\n const form = createForm({ initialValues: { email: '' } });\n let revision = 0;\n const stop = form.subscribe(() => revision++);\n\n onDestroy(stop);\n</script>\n\n<input value={form.field('email').value} on:input={(event) => form.field('email').set(event.currentTarget.value)} />\n```\n\n:::\n\n## Working with Other Vielzeug Libraries\n\nUse Spell when one schema owns validation and Vault when an explicit record codec owns persistence.\n\n```ts\nimport { createForm } from '@vielzeug/forge';\nimport { customValidator } from '@vielzeug/forge/spell';\nimport { s } from '@vielzeug/spell';\n\nconst Profile = s.object({ email: s.string().email() });\nconst form = createForm({ initialValues: { email: '' }, validate: customValidator(Profile) });\n```\n\n`customValidator()` preserves unrelated Spell errors, maps each union to its closest branch, and maps array-item failures to per-item array fields. Parse again at the submit boundary when a Spell transform must produce the outgoing payload.\n\n```ts\nimport { loadForm, saveForm } from '@vielzeug/forge/vault';\n\nawait saveForm(form, db, 'drafts', codec);\nconst restored = await loadForm(form, db, 'drafts', 'profile', codec);\nconsole.log(restored);\n```\n\n`loadForm()` uses `form.reset()`, so a restored value is clean. Store a selected `File`, not `FileList`, in form state; `FileList` is transport-only for `toFormData()`.\n\n## Best Practices\n\n- Keep form values to primitives, plain objects, arrays, `Date`, `File`, and `Blob`.\n- Update array fields through immutable replacement functions.\n- Validate complete values instead of rebuilding field-validator graphs.\n- Handle `aborted` validation results before rendering errors.\n- Preserve errors through field edits until a deliberate validation refresh.\n- Return subscription cleanup from framework lifecycle hooks.\n- Provide `onSubscriberError` when application subscribers can throw.\n- Decode Vault records before passing them to `loadForm()`.\n",
7
7
  "examples": "---\ntitle: Forge — Examples\ndescription: Practical immutable form recipes.\n---\n\n## Examples\n\n- [Login form](./examples/login-form.md)\n- [Conditional values](./examples/form-with-conditional-fields.md)\n- [Dynamic arrays](./examples/dynamic-form-fields.md)\n- [Contact form with file upload](./examples/contact-form-with-file-upload.md)\n- [Registration form](./examples/registration-form.md)\n- [Multi-step wizard](./examples/multi-step-wizard.md)\n- [Search form with debounce](./examples/search-form-with-debounce.md)\n"
8
8
  },
9
9
  "examples": [
@@ -59,7 +59,6 @@
59
59
  }
60
60
  ],
61
61
  "typeSignatures": {
62
- "toFormData": "export { toFormData } from './adapters/form-data';",
63
62
  "ForgeConfigError": "export { ForgeConfigError, ForgeDisposedError, ForgeError, ForgeSubmitError, ForgeValidationError } from './errors';",
64
63
  "ForgeDisposedError": "export { ForgeConfigError, ForgeDisposedError, ForgeError, ForgeSubmitError, ForgeValidationError } from './errors';",
65
64
  "ForgeError": "export { ForgeConfigError, ForgeDisposedError, ForgeError, ForgeSubmitError, ForgeValidationError } from './errors';",
@@ -69,16 +68,16 @@
69
68
  "Unsubscribe": "export type Unsubscribe = () => void;",
70
69
  "MaybePromise": "export type MaybePromise<T> = T | PromiseLike<T>;",
71
70
  "ReadonlyDeep": "export type ReadonlyDeep<T> = T extends (...args: never[]) => unknown\n ? T\n : T extends readonly (infer Item)[]\n ? readonly ReadonlyDeep<Item>[]\n : T extends Record<string, unknown>\n ? { readonly [K in keyof T]: ReadonlyDeep<T[K]> }\n : T;",
72
- "FormErrors": "export type FormErrors<T> = T extends readonly unknown[]\n ? string\n : T extends Record<string, unknown>\n ? string | { readonly [K in keyof T]?: FormErrors<T[K]> }\n : string;",
71
+ "FormErrors": "export type FormErrors<T> = T extends readonly (infer Item)[]\n ? string | readonly (FormErrors<Item> | undefined)[]\n : T extends Record<string, unknown>\n ? string | { readonly [K in keyof T]?: FormErrors<T[K]> }\n : string;",
73
72
  "ValidationErrors": "export type ValidationErrors<TValues extends Record<string, unknown>> = Readonly<{\n fields?: FormErrors<TValues>;\n formError?: string;\n}>;",
74
73
  "FormValidator": "export type FormValidator<TValues extends Record<string, unknown>> = (\n values: ReadonlyDeep<TValues>,\n signal: AbortSignal,\n) => MaybePromise<ValidationErrors<TValues> | undefined>;",
75
74
  "FormOptions": "export type FormOptions<TValues extends Record<string, unknown>> = Readonly<{\n initialValues: TValues;\n onSubscriberError?: (error: unknown) => void;\n validate?: FormValidator<NoInfer<TValues>>;\n}>;",
76
75
  "SubscribeOptions": "export type SubscribeOptions = Readonly<{\n immediate?: boolean;\n}>;",
77
76
  "FieldState": "export type FieldState<V> = Readonly<{\n dirty: boolean;\n error: string | undefined;\n touched: boolean;\n value: ReadonlyDeep<V>;\n}>;",
78
- "FormState": "export type FormState<TValues extends Record<string, unknown>> = Readonly<{\n error: string | undefined;\n errors: FormErrors<TValues> | undefined;\n submitCount: number;\n submitting: boolean;\n touched: boolean;\n valid: boolean;\n validating: boolean;\n}>;",
77
+ "FormState": "export type FormState<TValues extends Record<string, unknown>> = Readonly<{\n errors: FormErrors<TValues> | undefined;\n formError: string | undefined;\n hasErrors: boolean;\n submitCount: number;\n submitting: boolean;\n touched: boolean;\n validity: 'invalid' | 'unknown' | 'valid';\n validating: boolean;\n}>;",
79
78
  "ValidationResult": "export type ValidationResult<TValues extends Record<string, unknown>> =\n | Readonly<{ status: 'aborted' }>\n | Readonly<{ status: 'valid' }>\n | Readonly<{ errors: FormErrors<TValues> | undefined; formError: string | undefined; status: 'invalid' }>;",
80
- "SubmitResult": "export type SubmitResult<TResult = void, TValues extends Record<string, unknown> = Record<string, unknown>> =\n | Readonly<{ ok: true; value: TResult }>\n | Readonly<{ ok: false; type: 'aborted' }>\n | Readonly<{ errors: FormErrors<TValues> | undefined; formError: string | undefined; ok: false; type: 'validation' }>;",
81
- "Field": "export type Field<V> = ChildField<V> & {\n readonly dirty: boolean;\n readonly error: string | undefined;\n reset(): void;\n set(next: V | ((previous: ReadonlyDeep<V>) => V)): void;\n subscribe(listener: (state: FieldState<V>) => void, options?: SubscribeOptions): Unsubscribe;\n touch(): void;\n readonly touched: boolean;\n readonly value: ReadonlyDeep<V>;\n};",
82
- "Form": "export type Form<TValues extends Record<string, unknown>> = {\n [Symbol.dispose](): void;\n readonly disposalSignal: AbortSignal;\n dispose(): void;\n readonly disposed: boolean;\n field<K extends keyof TValues & string>(key: K): Field<TValues[K]>;\n reset(next?: TValues): void;\n set(next: TValues | ((previous: ReadonlyDeep<TValues>) => TValues)): void;\n readonly state: FormState<TValues>;\n submit<TResult = void>(\n handler: (values: ReadonlyDeep<TValues>) => MaybePromise<TResult>,\n ): Promise<SubmitResult<TResult, TValues>>;\n subscribe(listener: (state: FormState<TValues>) => void, options?: SubscribeOptions): Unsubscribe;\n validate(signal?: AbortSignal): Promise<ValidationResult<TValues>>;\n readonly value: ReadonlyDeep<TValues>;\n};"
79
+ "SubmitResult": "export type SubmitResult<TResult = void, TValues extends Record<string, unknown> = Record<string, unknown>> =\n | Readonly<{ status: 'aborted' }>\n | Readonly<{ status: 'invalid'; errors: FormErrors<TValues> | undefined; formError: string | undefined }>\n | Readonly<{ status: 'ok'; value: TResult }>;",
80
+ "Field": "export type Field<V> = ChildField<V> & {\n readonly dirty: boolean;\n readonly error: string | undefined;\n reset(): void;\n set(next: V | ((previous: ReadonlyDeep<V>) => V)): void;\n readonly state: FieldState<V>;\n subscribe(listener: (state: FieldState<V>) => void, options?: SubscribeOptions): Unsubscribe;\n touch(): void;\n readonly touched: boolean;\n readonly value: ReadonlyDeep<V>;\n};",
81
+ "Form": "export type Form<TValues extends Record<string, unknown>> = {\n [Symbol.dispose](): void;\n readonly disposalSignal: AbortSignal;\n dispose(): void;\n readonly disposed: boolean;\n field<K extends keyof TValues & string>(key: K): Field<TValues[K]>;\n reset(next?: TValues): void;\n set(next: TValues | ((previous: ReadonlyDeep<TValues>) => TValues)): void;\n readonly state: FormState<TValues>;\n submit<TResult = void>(\n handler: (values: ReadonlyDeep<TValues>, signal: AbortSignal) => MaybePromise<TResult>,\n signal?: AbortSignal,\n ): Promise<SubmitResult<TResult, TValues>>;\n subscribe(listener: (state: FormState<TValues>) => void, options?: SubscribeOptions): Unsubscribe;\n validate(signal?: AbortSignal): Promise<ValidationResult<TValues>>;\n readonly value: ReadonlyDeep<TValues>;\n};"
83
82
  }
84
83
  }
@@ -0,0 +1,25 @@
1
+ {
2
+ "apiSource": "export type {\n PanAxis,\n PanEndReason,\n PanGesture,\n PanGestureDetail,\n PanGestureEndDetail,\n PanGestureOptions,\n} from './pan-gesture';\nexport { createPanGesture } from './pan-gesture';\n",
3
+ "docs": {
4
+ "index": "---\ntitle: Gesture — Pointer pan primitives\ndescription: Framework-neutral one-axis pointer pan recognition with lifecycle-owned handles.\npackage: gesture\ncategory: input\nkeywords: [pointer, pan, swipe, gesture, touch, drag]\nexports: [createPanGesture]\nrelated: [refine, dnd, keymap]\nenvironments: [browser]\n---\n\n<!-- markdownlint-disable MD025 MD033 MD060 -->\n\n<PackageHero package=\"gesture\" />\n\n## Why Gesture?\n\nPointer-driven interfaces need reliable movement tracking without coupling input recognition to rendering or product-specific thresholds.\n\n```ts\n// Before\nelement.addEventListener('pointermove', (event) => {\n // Coordinate tracking, pointer identity, direction locking, and cleanup\n});\n\n// After\nconst pan = createPanGesture(element, {\n axis: 'x',\n onMove: ({ distance }) => render(distance),\n onEnd: ({ distance, reason }) => finish(distance, reason),\n});\n```\n\n| Feature | Ad-hoc pointer handling | Gesture |\n| --- | --- | --- |\n| Bundle size | n/a | <PackageInfo package=\"gesture\" type=\"size\" /> |\n| Zero dependencies | n/a | <ore-icon name=\"check\" size=\"16\"></ore-icon> |\n| Axis intent recognition | Manual | Built in |\n| Pointer ownership | Manual | Tracked across the document |\n| Lifecycle cleanup | Manual | `dispose()` + `disposalSignal` |\n\n<div class=\"decision-callout\">\n\n**Use Gesture when** several UI surfaces need consistent one-axis pointer tracking while retaining their own completion rules.\n\n**Consider direct pointer handling when** the interaction is isolated and does not need reusable lifecycle or direction-lock behavior.\n\n</div>\n\n## Installation\n\n::: code-group\n\n```sh [pnpm]\npnpm add @vielzeug/gesture\n```\n\n```sh [npm]\nnpm install @vielzeug/gesture\n```\n\n```sh [yarn]\nyarn add @vielzeug/gesture\n```\n\n:::\n\n## Quick Start\n\n```ts\nimport { createPanGesture } from '@vielzeug/gesture';\n\nconst pan = createPanGesture(element, {\n axis: 'x',\n onMove: ({ distance }) => {\n element.style.transform = `translateX(${distance}px)`;\n },\n onEnd: ({ distance, reason }) => {\n element.style.transform = '';\n\n if (reason === 'release' && Math.abs(distance) >= 48) {\n dismiss();\n }\n },\n});\n```\n\n## Features\n\n<div class=\"features-grid\">\n\n- `createPanGesture()` — one-axis pointer movement tracking\n- Direction locking — activates only when movement favors the configured axis\n- Configurable pointer capture — own the pointer by default or preserve native targeting\n- Consumer-owned policy — thresholds, snapping, and outcomes stay in application code\n- Stable completion — one `onEnd` callback for release and cancellation\n- Lifecycle ownership — `dispose()`, `disposed`, and `disposalSignal`\n\n</div>\n\n## Documentation\n\n<div class=\"doc-links\">\n\n- [Usage Guide](./usage.md)\n- [API Reference](./api.md)\n- [Examples](./examples.md)\n\n</div>\n\n## See Also\n\n<div class=\"see-also\">\n\n- [Refine](/refine/) — components that use pan recognition for carousel, drawer, toast, and list interactions.\n- [Dnd](/dnd/) — drag-and-drop behavior with drop targets and reordering.\n- [Keymap](/keymap/) — keyboard interaction primitives for complementary input paths.\n\n</div>\n\n<!-- markdownlint-enable MD025 MD033 MD060 -->\n",
5
+ "api": "---\ntitle: Gesture — API Reference\ndescription: API reference for @vielzeug/gesture pointer pan recognition.\n---\n\n[[toc]]\n\n## API Overview\n\n| Symbol | Purpose | Execution mode | Common gotcha |\n| --- | --- | --- | --- |\n| `createPanGesture()` | Track one-axis pointer movement on an element | Sync | `onStart` runs after direction intent is recognized |\n| `PanGesture` | Lifecycle-owned pan handle | Sync | `dispose()` does not emit `onEnd` |\n| `PanGestureOptions` | Configure axis, admission, capture, and callbacks | Sync | Completion thresholds belong in `onEnd` |\n\n## Package Entry Point\n\n| Import | Purpose |\n| --- | --- |\n| `@vielzeug/gesture` | Pan recognizer and related types. |\n\n## Core Functions\n\n### `createPanGesture()`\n\n```ts\nfunction createPanGesture(target: Element, options?: PanGestureOptions): PanGesture;\n```\n\nAttaches a one-axis pointer pan recognizer to `target`.\n\n| Parameter | Type | Description |\n| --- | --- | --- |\n| `target` | `Element` | Element that owns the pointer interaction. |\n| `options` | `PanGestureOptions` | Axis, disabled state, admission guard, capture policy, and lifecycle callbacks. |\n\n**Returns:** A `PanGesture` handle.\n\n**Example**\n\n```ts\nimport { createPanGesture } from '@vielzeug/gesture';\n\nconst pan = createPanGesture(element, {\n axis: 'x',\n onEnd: ({ distance, reason }) => {\n if (reason === 'release' && Math.abs(distance) >= 48) dismiss();\n },\n});\n```\n\n| Member | Return | Contract |\n| --- | --- | --- |\n| `active` | `boolean` | `true` after direction intent is accepted and before the interaction ends. |\n| `cancel()` | `boolean` | Cancels the pending or active pointer interaction. Active pans emit `onEnd` with `reason: 'cancel'`. |\n| `dispose()` | `void` | Detaches listeners, releases pointer ownership, and aborts `disposalSignal`. Idempotent. |\n| `disposed` | `boolean` | `true` after the first `dispose()`. |\n| `disposalSignal` | `AbortSignal` | Aborts when the handle is disposed. |\n| `[Symbol.dispose]()` | `void` | Calls `dispose()`. |\n\n## Types\n\n```ts\ntype PanAxis = 'x' | 'y';\ntype PanEndReason = 'cancel' | 'release';\n\ntype PanGestureDetail = {\n axis: PanAxis;\n current: number;\n distance: number;\n event: PointerEvent;\n pointerId: number;\n pointerType: string;\n start: number;\n target: Element;\n};\n\ntype PanGestureEndDetail = PanGestureDetail & {\n reason: PanEndReason;\n};\n\ntype PanGestureOptions = {\n axis?: PanAxis | (() => PanAxis);\n disabled?: boolean | (() => boolean | undefined);\n pointerCapture?: boolean;\n onEnd?: (detail: PanGestureEndDetail) => void;\n onMove?: (detail: PanGestureDetail) => void;\n onStart?: (detail: PanGestureDetail) => void;\n shouldStart?: (event: PointerEvent) => boolean;\n};\n\ntype PanGesture = {\n readonly active: boolean;\n [Symbol.dispose](): void;\n cancel(): boolean;\n readonly disposalSignal: AbortSignal;\n dispose(): void;\n readonly disposed: boolean;\n};\n```\n\n| Option | Type | Default | Contract |\n| --- | --- | --- | --- |\n| `axis` | `PanAxis \\| (() => PanAxis)` | `'x'` | Axis resolved when each pointer interaction starts |\n| `disabled` | `boolean \\| (() => boolean \\| undefined)` | `false` | Blocks new pans and cancels an active pan on the next pointer event |\n| `pointerCapture` | `boolean` | `true` | Captures the pointer on `target` after axis intent is accepted |\n| `shouldStart` | `(event: PointerEvent) => boolean` | — | Rejects a primary pointer start before tracking begins |\n| `onStart` | `(detail: PanGestureDetail) => void` | — | Runs once when axis intent is accepted |\n| `onMove` | `(detail: PanGestureDetail) => void` | — | Runs for the activating move and later moves |\n| `onEnd` | `(detail: PanGestureEndDetail) => void` | — | Runs for active release or cancellation |\n\nGesture tracks an accepted pan with capture-phase listeners on `target.ownerDocument` regardless of the pointer-capture setting. Set `pointerCapture: false` when nested or newly revealed controls must retain native pointer-up and click targeting.\n\n## Errors\n\n`@vielzeug/gesture` does not export custom error classes.\n",
6
+ "usage": "---\ntitle: Gesture — Usage Guide\ndescription: Track one-axis pointer movement and apply application-specific completion rules.\n---\n\n[[toc]]\n\n## Basic Usage\n\nCreate one pan handle for the element that owns the interaction.\n\n```ts\nimport { createPanGesture } from '@vielzeug/gesture';\n\nconst pan = createPanGesture(row, {\n axis: 'x',\n onMove: ({ distance }) => {\n row.style.transform = `translateX(${distance}px)`;\n },\n onEnd: ({ distance, reason }) => {\n row.style.transform = '';\n\n if (reason === 'release' && Math.abs(distance) >= 64) archive();\n },\n});\n```\n\n## Completion Rules\n\nGesture reports movement and terminal state but does not decide what constitutes a swipe. Apply thresholds and allowed directions in `onEnd`.\n\n```ts\nconst pan = createPanGesture(panel, {\n axis: 'x',\n onEnd: ({ distance, reason }) => {\n if (reason === 'release' && distance <= -80) {\n openNext();\n } else {\n resetPanel();\n }\n },\n});\n```\n\n## Direction Recognition\n\nThe gesture remains pending during small movement. It activates only after movement favors the configured axis. Cross-axis movement ends the pending interaction without invoking callbacks.\n\nUse the corresponding `touch-action` value so the browser retains native scrolling on the other axis.\n\n```css\n.swipe-row {\n touch-action: pan-y;\n}\n```\n\n```ts\nconst pan = createPanGesture(row, { axis: 'x', onMove });\n```\n\n## Pointer Capture\n\nPointer capture is enabled by default. After axis intent is accepted, Gesture captures the pointer on the bound target while continuing to track movement through document-level listeners. This is the reliable default for ordinary drag surfaces.\n\nDisable capture when nested or newly revealed controls must retain native pointer-up and click targeting:\n\n```ts\nconst pan = createPanGesture(row, {\n axis: 'x',\n pointerCapture: false,\n onMove: renderReveal,\n onEnd: settleReveal,\n});\n```\n\nDocument-level tracking still keeps the pan active outside the target. Disabling capture changes event targeting, not gesture tracking.\n\n## Interactive Descendants\n\nUse `shouldStart` when buttons, links, or form controls inside the surface must not start a pan.\n\n```ts\nconst pan = createPanGesture(notification, {\n axis: 'x',\n pointerCapture: false,\n shouldStart: (event) =>\n !event\n .composedPath()\n .some((node) => node instanceof Element && node.matches('button, a, input, select, textarea')),\n onMove,\n onEnd,\n});\n```\n\n`shouldStart` protects controls under the initial pointer. `pointerCapture: false` additionally protects controls that appear beneath the pointer during a reveal interaction.\n\n## Disabled State\n\nA boolean disables the recognizer permanently. A getter supports state that changes while the handle is alive.\n\n```ts\nconst pan = createPanGesture(row, {\n disabled: () => isLocked,\n onEnd: ({ reason }) => {\n if (reason === 'cancel') resetRow();\n },\n});\n```\n\nWhen the getter becomes `true`, the next pointer event cancels an active pan.\n\n## Lifecycle\n\nDispose the target-bound handle when its owning UI scope unmounts.\n\n```ts\nconst pan = createPanGesture(element, { onEnd, onMove });\n\nonCleanup(() => pan.dispose());\n```\n\nUse `cancel()` to stop a pending or active interaction without disposing the handle. An active interaction emits `onEnd` with `reason: 'cancel'`.\n\n## Framework Integration\n\nCreate the handle after the target element exists and dispose it on unmount.\n\n::: code-group\n\n```tsx [React]\nimport { useEffect, useRef } from 'react';\nimport { createPanGesture } from '@vielzeug/gesture';\n\nfunction SwipeRow({ onDismiss }: { onDismiss: () => void }) {\n const rowRef = useRef<HTMLDivElement>(null);\n\n useEffect(() => {\n const row = rowRef.current;\n if (!row) return;\n\n const pan = createPanGesture(row, {\n axis: 'x',\n onMove: ({ distance }) => {\n row.style.transform = `translateX(${distance}px)`;\n },\n onEnd: ({ distance, reason }) => {\n row.style.transform = '';\n if (reason === 'release' && Math.abs(distance) >= 64) onDismiss();\n },\n });\n\n return () => pan.dispose();\n }, [onDismiss]);\n\n return <div ref={rowRef}>Swipe me</div>;\n}\n```\n\n```vue [Vue 3]\n<script setup lang=\"ts\">\nimport { onMounted, onUnmounted, ref } from 'vue';\nimport { createPanGesture, type PanGesture } from '@vielzeug/gesture';\n\nconst emit = defineEmits<{ dismiss: [] }>();\nconst rowEl = ref<HTMLDivElement | null>(null);\nlet pan: PanGesture | undefined;\n\nonMounted(() => {\n const row = rowEl.value;\n if (!row) return;\n\n pan = createPanGesture(row, {\n axis: 'x',\n onEnd: ({ distance, reason }) => {\n if (reason === 'release' && Math.abs(distance) >= 64) emit('dismiss');\n },\n });\n});\n\nonUnmounted(() => pan?.dispose());\n</script>\n\n<template>\n <div ref=\"rowEl\">Swipe me</div>\n</template>\n```\n\n```svelte [Svelte]\n<script lang=\"ts\">\n import { onMount } from 'svelte';\n import { createPanGesture } from '@vielzeug/gesture';\n\n let { ondismiss = () => {} }: { ondismiss: () => void } = $props();\n let rowEl: HTMLDivElement;\n\n onMount(() => {\n const pan = createPanGesture(rowEl, {\n axis: 'x',\n onEnd: ({ distance, reason }) => {\n if (reason === 'release' && Math.abs(distance) >= 64) ondismiss();\n },\n });\n\n return () => pan.dispose();\n });\n</script>\n\n<div bind:this={rowEl}>Swipe me</div>\n```\n\n:::\n\n## Working with Other Vielzeug Libraries\n\n### Gesture + Refine\n\nRefine uses Gesture internally for carousel, drawer, toast, and list-item pointer interactions. Custom surfaces can use the same pan lifecycle while keeping visual state local.\n\n```ts\nimport { createPanGesture } from '@vielzeug/gesture';\n\nconst pan = createPanGesture(panel, {\n axis: 'x',\n onMove: ({ distance }) => {\n panel.style.transform = `translateX(${distance}px)`;\n },\n onEnd: ({ distance, reason }) => {\n panel.style.transform = '';\n if (reason === 'release' && Math.abs(distance) >= 80) revealActions();\n },\n});\n```\n\n### Gesture + Dnd\n\nGesture tracks a constrained pointer pan. Dnd owns draggable items, sortable lists, and drop targets. Keep them separate.\n\n## Best Practices\n\n- **Set** `touch-action` for the axis the browser should continue scrolling.\n- **Use** `shouldStart` to exclude nested interactive controls.\n- **Disable** pointer capture when nested or newly revealed controls must keep native release targeting.\n- **Apply** thresholds and direction rules in `onEnd`.\n- **Treat** `reason: 'cancel'` as a reset path, never a commit path.\n- **Keep** `onMove` rendering lightweight.\n- **Dispose** the handle when its target leaves the UI.\n",
7
+ "examples": "---\ntitle: Gesture — Examples\ndescription: Worked examples for @vielzeug/gesture.\n---\n\n## Examples\n\n- [Carousel Swipe Navigation](./examples/carousel-swipe-navigation.md)\n- [Swipe-to-Dismiss Notifications](./examples/swipe-dismiss-notifications.md)\n"
8
+ },
9
+ "examples": [
10
+ {
11
+ "id": "pan-basic",
12
+ "code": "import { createPanGesture } from '@vielzeug/gesture'\n\nconst surface = document.createElement('div')\nsurface.textContent = 'Drag horizontally'\nsurface.style.cssText = 'width:240px;padding:32px;text-align:center;background:#e0e7ff;border-radius:12px;touch-action:pan-y;user-select:none;'\ndocument.body.appendChild(surface)\n\nconst output = document.createElement('pre')\ndocument.body.appendChild(output)\n\nconst pan = createPanGesture(surface, {\n axis: 'x',\n onMove: ({ distance }) => {\n surface.style.transform = `translateX(${distance}px)`\n output.textContent = `distance: ${Math.round(distance)}px`\n },\n onEnd: ({ distance, reason }) => {\n surface.style.transform = ''\n output.textContent = reason === 'release' && Math.abs(distance) >= 48\n ? `swipe: ${distance < 0 ? 'left' : 'right'}`\n : `ended: ${reason}`\n },\n})\n\nconsole.log('Pan gesture ready:', pan.disposed === false)",
13
+ "name": "createPanGesture - Basic"
14
+ }
15
+ ],
16
+ "typeSignatures": {
17
+ "PanAxis": "export type {\n PanAxis,\n PanEndReason,\n PanGesture,\n PanGestureDetail,\n PanGestureEndDetail,\n PanGestureOptions,\n} from './pan-gesture';",
18
+ "PanEndReason": "export type {\n PanAxis,\n PanEndReason,\n PanGesture,\n PanGestureDetail,\n PanGestureEndDetail,\n PanGestureOptions,\n} from './pan-gesture';",
19
+ "PanGesture": "export type {\n PanAxis,\n PanEndReason,\n PanGesture,\n PanGestureDetail,\n PanGestureEndDetail,\n PanGestureOptions,\n} from './pan-gesture';",
20
+ "PanGestureDetail": "export type {\n PanAxis,\n PanEndReason,\n PanGesture,\n PanGestureDetail,\n PanGestureEndDetail,\n PanGestureOptions,\n} from './pan-gesture';",
21
+ "PanGestureEndDetail": "export type {\n PanAxis,\n PanEndReason,\n PanGesture,\n PanGestureDetail,\n PanGestureEndDetail,\n PanGestureOptions,\n} from './pan-gesture';",
22
+ "PanGestureOptions": "export type {\n PanAxis,\n PanEndReason,\n PanGesture,\n PanGestureDetail,\n PanGestureEndDetail,\n PanGestureOptions,\n} from './pan-gesture';",
23
+ "createPanGesture": "export { createPanGesture } from './pan-gesture';"
24
+ }
25
+ }
@@ -0,0 +1,132 @@
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
+ }