@vielzeug/codex 3.0.2 → 3.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/data/catalog.json +1899 -0
  2. package/data/llms-full.txt +30099 -0
  3. package/data/llms.txt +45 -0
  4. package/data/manifest.json +8 -0
  5. package/data/packages/arsenal.json +216 -0
  6. package/data/packages/assay.json +46 -0
  7. package/data/packages/clockwork.json +66 -0
  8. package/data/packages/codex.json +29 -0
  9. package/data/packages/coins.json +101 -0
  10. package/data/packages/conduit.json +68 -0
  11. package/data/packages/courier.json +55 -0
  12. package/data/packages/dnd.json +60 -0
  13. package/data/packages/familiar.json +17 -0
  14. package/data/packages/flux.json +94 -0
  15. package/data/packages/focus.json +38 -0
  16. package/data/packages/forge.json +59 -0
  17. package/data/packages/gesture.json +37 -0
  18. package/data/packages/herald.json +81 -0
  19. package/data/packages/illusionist.json +132 -0
  20. package/data/packages/keymap.json +51 -0
  21. package/data/packages/ledger.json +60 -0
  22. package/data/packages/lingua.json +70 -0
  23. package/data/packages/necromancer.json +50 -0
  24. package/data/packages/orbit.json +99 -0
  25. package/data/packages/ore.json +65 -0
  26. package/data/packages/postmaster.json +49 -0
  27. package/data/packages/prism.json +66 -0
  28. package/data/packages/pulse.json +71 -0
  29. package/data/packages/refine.json +12 -0
  30. package/data/packages/ripple.json +88 -0
  31. package/data/packages/rune.json +78 -0
  32. package/data/packages/sandbox.json +26 -0
  33. package/data/packages/scout.json +63 -0
  34. package/data/packages/scroll.json +110 -0
  35. package/data/packages/sentinel.json +35 -0
  36. package/data/packages/sourcerer.json +70 -0
  37. package/data/packages/spell.json +128 -0
  38. package/data/packages/tempo.json +81 -0
  39. package/data/packages/vault.json +93 -0
  40. package/data/packages/ward.json +80 -0
  41. package/data/packages/wayfinder.json +114 -0
  42. package/data/refine.json +12235 -0
  43. package/data/search.json +1534 -0
  44. package/package.json +4 -5
@@ -0,0 +1,35 @@
1
+ {
2
+ "apiSource": "export { createElementSize } from './element-size.ts';\nexport { SentinelError, SentinelUnavailableError } from './errors.ts';\nexport type { CreateIntersectionOptions } from './intersection.ts';\nexport { createIntersection } from './intersection.ts';\nexport { createMediaQuery } from './media-query.ts';\nexport { createNetwork } from './network.ts';\nexport type {\n ElementSizeState,\n IntersectionState,\n MediaQueryState,\n NetworkConnectionSnapshot,\n NetworkState,\n Sentinel,\n SentinelOptions,\n ViewportState,\n WindowSentinelOptions,\n} from './types.ts';\nexport { createViewport } from './viewport.ts';\n",
3
+ "docs": {
4
+ "index": "---\ntitle: Sentinel — Subscribable environment snapshots\ndescription: Subscribable browser and DOM snapshots for viewport, network, media query, element size, and intersection state.\npackage: sentinel\ncategory: Environment\nkeywords: [subscribable, browser, viewport, network, media-query, resize-observer, intersection-observer]\nrelated: [ripple, ore, focus, gesture]\nexports: [createViewport, createNetwork, createMediaQuery, createElementSize, createIntersection, SentinelError, SentinelUnavailableError, Sentinel]\nenvironments: [browser]\n---\n\n<!-- markdownlint-disable MD025 MD033 MD060 -->\n\n<PackageHero package=\"sentinel\" />\n\n## Why Sentinel?\n\nBrowser environment APIs use different events, observer callbacks, initial states, and cleanup methods. Sentinel gives them one explicit external-store shape with snapshot reads, subscriptions, and disposal.\n\n```ts\n// Before\n{\n const panel = document.querySelector<HTMLElement>('[data-panel]');\n if (!panel) throw new Error('Panel not found');\n\n const observer = new ResizeObserver(([entry]) => {\n console.log(entry?.contentRect.width);\n });\n observer.observe(panel);\n\n // Later\n observer.disconnect();\n}\n\n// After\nimport { createElementSize } from '@vielzeug/sentinel';\n\n{\n const panel = document.querySelector<HTMLElement>('[data-panel]');\n if (!panel) throw new Error('Panel not found');\n\n const size = createElementSize(panel);\n const unsubscribe = size.subscribe(() => {\n console.log(size.getSnapshot()?.width);\n });\n\n // Later\n unsubscribe();\n size.dispose();\n}\n```\n\n| Feature | Sentinel | Native observer APIs | Ad hoc event listeners |\n| --- | --- | --- | --- |\n| Bundle size | <PackageInfo package=\"sentinel\" type=\"size\" /> | Built in | Application-defined |\n| Zero dependencies | <ore-icon name=\"check\" size=\"16\"></ore-icon> | <ore-icon name=\"check\" size=\"16\"></ore-icon> | <ore-icon name=\"check\" size=\"16\"></ore-icon> |\n| Subscribable current snapshot | <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| Consistent disposable handle | <ore-icon name=\"check\" size=\"16\"></ore-icon> | <ore-icon name=\"triangle-alert\" size=\"16\"></ore-icon> | <ore-icon name=\"x\" size=\"16\"></ore-icon> |\n| Shared abort ownership | <ore-icon name=\"check\" size=\"16\"></ore-icon> | <ore-icon name=\"x\" size=\"16\"></ore-icon> | <ore-icon name=\"triangle-alert\" size=\"16\"></ore-icon> |\n| Ripple composition | <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\n<div class=\"decision-callout\">\n\n**Use Sentinel when** browser or DOM observations need consistent snapshot reads, subscriptions, and ownership.\n\n**Consider native APIs when** one isolated observer is sufficient and a shared lifecycle abstraction adds no value.\n\n</div>\n\n## Installation\n\n::: code-group\n\n```sh [pnpm]\npnpm add @vielzeug/sentinel\n```\n\n```sh [npm]\nnpm install @vielzeug/sentinel\n```\n\n```sh [yarn]\nyarn add @vielzeug/sentinel\n```\n\n:::\n\n## Quick Start\n\nCreate a viewport Sentinel, render its initial state, then react to changes until the page lifetime ends.\n\n```ts\nimport { createViewport } from '@vielzeug/sentinel';\n\nfunction observeViewport(): () => void {\n const viewport = createViewport();\n const render = () => {\n const { dpr, height, width } = viewport.getSnapshot();\n console.log(`${width}×${height} at ${dpr}dpr`);\n };\n\n render();\n const unsubscribe = viewport.subscribe(render);\n\n return () => {\n unsubscribe();\n viewport.dispose();\n };\n}\n\nconst stopObserving = observeViewport();\n// Call stopObserving() when the owning view unmounts.\n```\n\n<div class=\"features-grid\">\n\n## Features\n\n- `createViewport()` — Observe viewport dimensions and device pixel ratio.\n- `createNetwork()` — Track online status and optional connection details.\n- `createMediaQuery()` — Observe one media query.\n- `createElementSize()` — Read content-box dimensions from `ResizeObserver`.\n- `createIntersection()` — Track normalized intersection state.\n- `dispose()` — Release owned browser observers and listeners.\n- `SentinelOptions.signal` — Abort several Sentinels through one external lifetime.\n\n</div>\n\n<div class=\"doc-links\">\n\n## Documentation\n\n- [**Usage Guide**](./usage.md)\n- [**API Reference**](./api.md)\n- [**Examples**](./examples.md)\n- [**3.0 Migration Guide**](./migration.md)\n\n</div>\n\n<div class=\"see-also\">\n\n## See Also\n\n- [@vielzeug/ripple](../ripple/) — Derive and watch values from Sentinel state.\n- [@vielzeug/ore](../ore/) — Bind Sentinels to web-component mount and cleanup lifecycles.\n- [@vielzeug/focus](../focus/) — Manage keyboard focus alongside observed UI state.\n- [@vielzeug/gesture](../gesture/) — Handle pointer gestures alongside environmental observations.\n\n</div>\n\n<!-- markdownlint-enable MD025 MD033 MD060 -->\n",
5
+ "api": "---\ntitle: Sentinel — API Reference\ndescription: Factory signatures, options, state types, lifecycle handles, and errors for Sentinel.\n---\n\n[[toc]]\n\n## API Overview\n\n| Symbol | Purpose | Execution mode | Common gotcha |\n| --- | --- | --- | --- |\n| `createViewport()` | Observe layout viewport dimensions and device pixel ratio | Sync | Requires a browser Window |\n| `createNetwork()` | Observe online status and optional connection information | Sync | `connection` is often `null` |\n| `createMediaQuery()` | Observe one media query | Sync | Throws when `matchMedia` is unavailable |\n| `createElementSize()` | Observe element content-box dimensions | Sync | Value is `null` before the first delivery |\n| `createIntersection()` | Observe element intersection state | Sync | Value is `null` before the first delivery |\n| `Sentinel<T>` | Expose an external-store snapshot with explicit browser-resource ownership | Sync | Call `getSnapshot()` inside subscription listeners |\n| `SentinelError` | Base class for package-defined errors | Sync | Catch a subtype when recovery is specific |\n| `SentinelUnavailableError` | Report an unavailable browser API | Sync | Invalid observer inputs retain their native errors |\n\n## Package Entry Point\n\n| Import | Purpose |\n| --- | --- |\n| `@vielzeug/sentinel` | All factories, state types, option types, and error classes |\n\n## Factories\n\n### `createViewport()`\n\n```ts\nfunction createViewport(options?: WindowSentinelOptions): Sentinel<ViewportState>;\n```\n\nReturns a Sentinel initialized from the layout viewport's `innerWidth`, `innerHeight`, and `devicePixelRatio`.\n\n| Parameter | Type | Description |\n| --- | --- | --- |\n| `options.target` | `Window` | Window to observe instead of the global browser window |\n| `options.signal` | `AbortSignal` | External signal that disposes the Sentinel |\n\n**Returns:** `Sentinel<ViewportState>`.\n\n**Example**\n\n```ts\nimport { createViewport } from '@vielzeug/sentinel';\n\nconst viewport = createViewport();\nconsole.log(viewport.getSnapshot().width);\nviewport.dispose();\n```\n\n---\n\n### `createNetwork()`\n\n```ts\nfunction createNetwork(options?: WindowSentinelOptions): Sentinel<NetworkState>;\n```\n\nReturns a Sentinel initialized from `navigator.onLine` and the optional Network Information API.\n\n| Parameter | Type | Description |\n| --- | --- | --- |\n| `options.target` | `Window` | Window whose navigator and events are observed |\n| `options.signal` | `AbortSignal` | External signal that disposes the Sentinel |\n\n**Returns:** `Sentinel<NetworkState>`.\n\n**Example**\n\n```ts\nimport { createNetwork } from '@vielzeug/sentinel';\n\nconst network = createNetwork();\nconsole.log(network.getSnapshot().online);\nnetwork.dispose();\n```\n\n---\n\n### `createMediaQuery()`\n\n```ts\nfunction createMediaQuery(query: string, options?: WindowSentinelOptions): Sentinel<MediaQueryState>;\n```\n\nReturns a Sentinel initialized from `matchMedia(query).matches`.\n\n| Parameter | Type | Description |\n| --- | --- | --- |\n| `query` | `string` | CSS media query to observe |\n| `options.target` | `Window` | Window whose `matchMedia` method is used |\n| `options.signal` | `AbortSignal` | External signal that disposes the Sentinel |\n\n**Returns:** `Sentinel<MediaQueryState>`.\n\n**Example**\n\n```ts\nimport { createMediaQuery } from '@vielzeug/sentinel';\n\nconst darkMode = createMediaQuery('(prefers-color-scheme: dark)');\nconsole.log(darkMode.getSnapshot().matches);\ndarkMode.dispose();\n```\n\n---\n\n### `createElementSize()`\n\n```ts\nfunction createElementSize(element: Element, options?: SentinelOptions): Sentinel<ElementSizeState | null>;\n```\n\nReturns a Sentinel containing the latest `ResizeObserverEntry.contentRect` dimensions.\n\n| Parameter | Type | Description |\n| --- | --- | --- |\n| `element` | `Element` | Element to observe |\n| `options.signal` | `AbortSignal` | External signal that disposes the Sentinel |\n\n**Returns:** `Sentinel<ElementSizeState | null>`. The initial value is `null`.\n\n**Example**\n\n```ts\nimport { createElementSize } from '@vielzeug/sentinel';\n\nconst size = createElementSize(document.body);\nconst unsubscribe = size.subscribe(() => {\n console.log(size.getSnapshot()?.width);\n});\n\nunsubscribe();\nsize.dispose();\n```\n\n---\n\n### `createIntersection()`\n\n```ts\nfunction createIntersection(\n element: Element,\n options?: CreateIntersectionOptions,\n): Sentinel<IntersectionState | null>;\n```\n\nReturns a Sentinel containing normalized fields from the latest IntersectionObserver entry.\n\n| Parameter | Type | Description |\n| --- | --- | --- |\n| `element` | `Element` | Element to observe |\n| `options.root` | `Element \\| Document \\| null` | Intersection root |\n| `options.rootMargin` | `string` | Margin applied to the root |\n| `options.scrollMargin` | `string` | Margin applied to nested scroll containers |\n| `options.threshold` | `number \\| number[]` | Intersection ratio threshold or thresholds |\n| `options.signal` | `AbortSignal` | External signal that disposes the Sentinel |\n\n**Returns:** `Sentinel<IntersectionState | null>`. The initial value is `null`.\n\n**Example**\n\n```ts\nimport { createIntersection } from '@vielzeug/sentinel';\n\nconst intersection = createIntersection(document.body, { threshold: 0.5 });\nconst unsubscribe = intersection.subscribe(() => {\n console.log(intersection.getSnapshot()?.isIntersecting);\n});\n\nunsubscribe();\nintersection.dispose();\n```\n\n## Types\n\n### `Sentinel<T>`\n\n```ts\ninterface Sentinel<T> extends Readable<T>, Disposable {}\n\ninterface Readable<T> {\n getSnapshot(): T;\n subscribe(listener: () => void): () => void;\n}\n```\n\n`Sentinel<T>` follows the standard external-store snapshot/subscription contract and owns the underlying browser resource. `getSnapshot()`, `subscribe()`, and `dispose()` are stable callback-safe functions. `dispose()` stops observation, clears listeners, and aborts `disposalSignal`.\n\n| Member | Type | Description |\n| --- | --- | --- |\n| `getSnapshot()` | `() => T` | Return the current immutable snapshot |\n| `subscribe(listener)` | `(listener: () => void) => () => void` | Subscribe to snapshot changes and return an unsubscribe function |\n| `disposed` | `boolean` | Whether observation has ended |\n| `disposalSignal` | `AbortSignal` | Aborts when observation ends |\n| `dispose()` | `() => void` | Stop observation and release owned browser resources |\n| `[Symbol.dispose]()` | `() => void` | Dispose through the explicit resource-management protocol |\n\n---\n\n### `SentinelOptions`\n\n```ts\ninterface SentinelOptions {\n readonly signal?: AbortSignal;\n}\n```\n\n---\n\n### `WindowSentinelOptions`\n\n```ts\ninterface WindowSentinelOptions extends SentinelOptions {\n readonly target?: Window;\n}\n```\n\n---\n\n### `CreateIntersectionOptions`\n\n```ts\ninterface CreateIntersectionOptions extends SentinelOptions {\n readonly root?: Element | Document | null;\n readonly rootMargin?: string;\n readonly scrollMargin?: string;\n readonly threshold?: number | number[];\n}\n```\n\n---\n\n### `ViewportState`\n\n```ts\ninterface ViewportState {\n readonly dpr: number;\n readonly height: number;\n readonly width: number;\n}\n```\n\n---\n\n### `NetworkConnectionSnapshot`\n\n```ts\ninterface NetworkConnectionSnapshot {\n readonly downlink?: number;\n readonly effectiveType?: 'slow-2g' | '2g' | '3g' | '4g';\n readonly rtt?: number;\n readonly saveData?: boolean;\n}\n```\n\n---\n\n### `NetworkState`\n\n```ts\ninterface NetworkState {\n readonly connection: NetworkConnectionSnapshot | null;\n readonly online: boolean;\n}\n```\n\n---\n\n### `MediaQueryState`\n\n```ts\ninterface MediaQueryState {\n readonly matches: boolean;\n}\n```\n\n---\n\n### `ElementSizeState`\n\n```ts\ninterface ElementSizeState {\n readonly height: number;\n readonly width: number;\n}\n```\n\n---\n\n### `IntersectionState`\n\n```ts\ninterface IntersectionState {\n readonly intersectionRatio: number;\n readonly isIntersecting: boolean;\n}\n```\n\n## Errors\n\n### `SentinelError`\n\n```ts\nclass SentinelError extends Error {\n constructor(message: string, options?: ErrorOptions);\n}\n```\n\nBase class for package-defined errors.\n\n---\n\n### `SentinelUnavailableError`\n\n```ts\nclass SentinelUnavailableError extends SentinelError {}\n```\n\nThrown when a required browser API or Window is unavailable:\n\n- `createViewport()` and `createNetwork()` when no browser Window is available.\n- `createMediaQuery()` when `matchMedia` is unavailable.\n- `createElementSize()` when the element has no Window or `ResizeObserver` is unavailable.\n- `createIntersection()` when the element has no Window or `IntersectionObserver` is unavailable.\n\nNative setup errors remain unchanged, including invalid observer options or targets.\n",
6
+ "usage": "---\ntitle: Sentinel — Usage Guide\ndescription: Observe browser and DOM state through subscribable snapshots with explicit lifecycles.\n---\n\n[[toc]]\n\n## Basic Usage\n\nCreate a Sentinel, read its current state, subscribe to invalidations, and release both resources when the owner ends.\n\n```ts\nimport { createViewport } from '@vielzeug/sentinel';\n\nfunction observeViewport(): () => void {\n const viewport = createViewport();\n\n const render = () => {\n const { dpr, height, width } = viewport.getSnapshot();\n console.log(`${width}×${height} at ${dpr}dpr`);\n };\n\n render();\n const unsubscribe = viewport.subscribe(render);\n\n return () => {\n unsubscribe();\n viewport.dispose();\n };\n}\n\nconst stopObserving = observeViewport();\n// Call stopObserving() when the owning view unmounts.\n```\n\n`subscribe()` notifies you that the snapshot changed; call `getSnapshot()` inside the listener to read it. Disposing a Sentinel stops its browser observer, removes its listeners, and aborts `disposalSignal`.\n\n## Observe Window State\n\nUse `createViewport()` for viewport dimensions and device pixel ratio.\n\n```ts\nimport { createViewport } from '@vielzeug/sentinel';\n\nconst viewport = createViewport();\nconsole.log(viewport.getSnapshot().width);\nconsole.log(viewport.getSnapshot().height);\nconsole.log(viewport.getSnapshot().dpr);\n```\n\nUse `createNetwork()` for online status and the optional Network Information API snapshot.\n\n```ts\nimport { createNetwork } from '@vielzeug/sentinel';\n\nconst network = createNetwork();\nconsole.log(network.getSnapshot().online);\nconsole.log(network.getSnapshot().connection);\n```\n\n`connection` is `null` when `navigator.connection` is unavailable.\n\n## Observe Media Queries\n\nUse `createMediaQuery()` to react to a browser media query.\n\n```ts\nimport { createMediaQuery, SentinelUnavailableError } from '@vielzeug/sentinel';\n\nfunction observeReducedMotion(): () => void {\n try {\n const reducedMotion = createMediaQuery('(prefers-reduced-motion: reduce)');\n\n const applyPreference = () => {\n document.documentElement.classList.toggle('reduce-motion', reducedMotion.getSnapshot().matches);\n };\n\n applyPreference();\n const unsubscribe = reducedMotion.subscribe(applyPreference);\n\n return () => {\n unsubscribe();\n reducedMotion.dispose();\n };\n } catch (error) {\n if (!(error instanceof SentinelUnavailableError)) throw error;\n return () => {};\n }\n}\n\nconst stopObserving = observeReducedMotion();\n// Call stopObserving() when the owning view unmounts.\n```\n\n`createMediaQuery()` throws `SentinelUnavailableError` when `matchMedia` is unavailable.\n\n## Observe Elements\n\n### Element Size\n\nUse `createElementSize()` after the target element exists.\n\n```ts\nimport { createElementSize } from '@vielzeug/sentinel';\n\nconst panel = document.querySelector<HTMLElement>('[data-panel]');\nif (!panel) throw new Error('Panel not found');\n\nconst size = createElementSize(panel);\nconst unsubscribe = size.subscribe(() => {\n const current = size.getSnapshot();\n if (current) panel.dataset.width = String(current.width);\n});\n```\n\nThe initial state is `null` until `ResizeObserver` reports its first measurement.\n\n### Intersection\n\nUse `createIntersection()` to observe visibility relative to the viewport or a custom root.\n\n```ts\nimport { createIntersection } from '@vielzeug/sentinel';\n\nconst target = document.querySelector<HTMLElement>('[data-lazy-section]');\nif (!target) throw new Error('Section not found');\n\nconst intersection = createIntersection(target, {\n rootMargin: '100px',\n threshold: [0, 0.5, 1],\n});\n\nconst unsubscribe = intersection.subscribe(() => {\n target.hidden = !intersection.getSnapshot()?.isIntersecting;\n});\n```\n\nThe initial state is `null` until `IntersectionObserver` reports its first entry.\n\n## Control Ownership\n\nCall `dispose()` to stop observation. Disposal is idempotent.\n\n```ts\nconst viewport = createViewport();\n\nviewport.dispose();\nviewport.dispose();\n```\n\nPass an `AbortSignal` when several Sentinels share one lifetime.\n\n```ts\nconst controller = new AbortController();\nconst viewport = createViewport({ signal: controller.signal });\nconst network = createNetwork({ signal: controller.signal });\n\ncontroller.abort();\n```\n\nSentinel follows the standard external-store shape, so reactive libraries can bridge it without a Sentinel-specific adapter.\n\n## Handle Unavailable APIs\n\n`createMediaQuery()`, `createElementSize()`, and `createIntersection()` report unavailable platform APIs with `SentinelUnavailableError`.\n\n```ts\nimport { createElementSize, SentinelUnavailableError } from '@vielzeug/sentinel';\n\ntry {\n const size = createElementSize(document.body);\n size.dispose();\n} catch (error) {\n if (error instanceof SentinelUnavailableError) {\n console.warn(error.message);\n } else {\n throw error;\n }\n}\n```\n\nInvoke all factories only in a browser client lifecycle. Package imports are safe during SSR, but factories require browser or DOM APIs.\n\n## Framework Integration\n\nSentinel's callback-safe external-store methods integrate directly with framework subscription APIs. Create browser-bound Sentinels in a client owner and dispose them when that owner ends.\n\n::: code-group\n\n```tsx [React]\nimport { type Sentinel, type ViewportState } from '@vielzeug/sentinel';\nimport { useSyncExternalStore } from 'react';\n\ntype ViewportSizeProps = {\n readonly viewport: Sentinel<ViewportState>;\n};\n\nexport function ViewportSize({ viewport }: ViewportSizeProps) {\n const viewportState = useSyncExternalStore(viewport.subscribe, viewport.getSnapshot);\n\n return <output>{`${viewportState.width}×${viewportState.height}`}</output>;\n}\n```\n\n```vue [Vue 3]\n<script setup lang=\"ts\">\nimport { createViewport, type Sentinel, type ViewportState } from '@vielzeug/sentinel';\nimport { onMounted, onUnmounted, ref } from 'vue';\n\nconst viewportState = ref<ViewportState | null>(null);\nlet viewport: Sentinel<ViewportState> | undefined;\nlet unsubscribe: (() => void) | undefined;\n\nonMounted(() => {\n viewport = createViewport();\n const update = () => {\n viewportState.value = viewport?.getSnapshot() ?? null;\n };\n\n update();\n unsubscribe = viewport.subscribe(update);\n});\n\nonUnmounted(() => {\n unsubscribe?.();\n viewport?.dispose();\n});\n</script>\n\n<template>\n <output>\n {{ viewportState ? `${viewportState.width}×${viewportState.height}` : 'Measuring…' }}\n </output>\n</template>\n```\n\n```svelte [Svelte]\n<script lang=\"ts\">\n import { createViewport, type ViewportState } from '@vielzeug/sentinel';\n import { onMount } from 'svelte';\n\n let viewportState: ViewportState | null = null;\n\n onMount(() => {\n const viewport = createViewport();\n const update = () => {\n viewportState = viewport.getSnapshot();\n };\n\n update();\n const unsubscribe = viewport.subscribe(update);\n\n return () => {\n unsubscribe();\n viewport.dispose();\n };\n });\n</script>\n\n<output>\n {viewportState ? `${viewportState.width}×${viewportState.height}` : 'Measuring…'}\n</output>\n```\n\n:::\n\n## Working with Other Vielzeug Libraries\n\n### Sentinel + Ripple\n\nBridge Sentinel stores with `fromSubscribable()` before deriving reactive values. Dispose the watcher separately; disposing each Sentinel also disposes its bridge through `disposalSignal`.\n\n```ts\nimport { computed, fromSubscribable, watch } from '@vielzeug/ripple';\nimport { createMediaQuery, createViewport } from '@vielzeug/sentinel';\n\nconst viewport = createViewport();\nconst mobileQuery = createMediaQuery('(max-width: 768px)');\nconst viewportState = fromSubscribable(viewport, { signal: viewport.disposalSignal });\nconst mobileState = fromSubscribable(mobileQuery, { signal: mobileQuery.disposalSignal });\nconst compact = computed(() => mobileState.value.matches || viewportState.value.width < 400);\nconst compactWatcher = watch(compact, (value) => console.log('Compact layout:', value), { immediate: true });\n\ncompactWatcher.dispose();\nmobileQuery.dispose();\nviewport.dispose();\n```\n\n### Sentinel + Ore\n\nCreate DOM-dependent Sentinels in `onMounted()` and register both subscription and Sentinel cleanup with the component.\n\n```ts\nimport { define, html, onCleanup, onMounted, ref } from '@vielzeug/ore';\nimport { createElementSize } from '@vielzeug/sentinel';\n\ndefine('measured-panel', {\n setup() {\n const panel = ref<HTMLElement>();\n\n onMounted(() => {\n const element = panel.value;\n if (!element) return;\n\n const size = createElementSize(element);\n const update = () => {\n element.dataset.width = String(size.getSnapshot()?.width ?? 0);\n };\n const unsubscribe = size.subscribe(update);\n\n onCleanup(() => {\n unsubscribe();\n size.dispose();\n });\n });\n\n return html`<section ref=${panel}>Measured panel</section>`;\n },\n});\n```\n\n## Best Practices\n\n- **Create** DOM-dependent Sentinels only after their target elements exist.\n- **Read** the latest snapshot with `getSnapshot()` inside subscription listeners.\n- **Unsubscribe** Ripple listeners when their owner ends.\n- **Dispose** every Sentinel to release browser observers and event listeners.\n- **Share** an `AbortSignal` when multiple Sentinels have the same lifetime.\n- **Guard** APIs that can throw `SentinelUnavailableError`.\n- **Treat** `NetworkState.connection` as optional browser enhancement data.\n- **Invoke** factories only in browser client lifecycles.\n",
7
+ "examples": "---\ntitle: Sentinel — Examples\ndescription: Focused browser and DOM observation examples for Sentinel.\n---\n\n## Examples\n\n- [Responsive Viewport Tracking](./examples/responsive-viewport-tracking.md)\n- [Monitor Network Condition](./examples/monitor-network-condition.md)\n- [Respect Reduced Motion Preference](./examples/respect-reduced-motion-preference.md)\n- [Responsive Column Layout](./examples/responsive-column-layout.md)\n- [Lazy Load Images on Intersection](./examples/lazy-load-images-on-intersection.md)\n"
8
+ },
9
+ "examples": [
10
+ {
11
+ "id": "viewport-basic",
12
+ "code": "import { createViewport } from '@vielzeug/sentinel'\n\nconst viewport = createViewport()\nconst logViewport = () => {\n const { dpr, height, width } = viewport.getSnapshot()\n console.log(`${width}x${height} at ${dpr}dpr`)\n}\n\nlogViewport()\nconst unsubscribe = viewport.subscribe(logViewport)\nwindow.dispatchEvent(new Event('resize'))\n\nunsubscribe()\nviewport.dispose()\nconsole.log('disposed:', viewport.disposed)",
13
+ "name": "createViewport - Basic"
14
+ }
15
+ ],
16
+ "typeSignatures": {
17
+ "createElementSize": "export { createElementSize } from './element-size.ts';",
18
+ "SentinelError": "export { SentinelError, SentinelUnavailableError } from './errors.ts';",
19
+ "SentinelUnavailableError": "export { SentinelError, SentinelUnavailableError } from './errors.ts';",
20
+ "CreateIntersectionOptions": "export type { CreateIntersectionOptions } from './intersection.ts';",
21
+ "createIntersection": "export { createIntersection } from './intersection.ts';",
22
+ "createMediaQuery": "export { createMediaQuery } from './media-query.ts';",
23
+ "createNetwork": "export { createNetwork } from './network.ts';",
24
+ "ElementSizeState": "export type {\n ElementSizeState,\n IntersectionState,\n MediaQueryState,\n NetworkConnectionSnapshot,\n NetworkState,\n Sentinel,\n SentinelOptions,\n ViewportState,\n WindowSentinelOptions,\n} from './types.ts';",
25
+ "IntersectionState": "export type {\n ElementSizeState,\n IntersectionState,\n MediaQueryState,\n NetworkConnectionSnapshot,\n NetworkState,\n Sentinel,\n SentinelOptions,\n ViewportState,\n WindowSentinelOptions,\n} from './types.ts';",
26
+ "MediaQueryState": "export type {\n ElementSizeState,\n IntersectionState,\n MediaQueryState,\n NetworkConnectionSnapshot,\n NetworkState,\n Sentinel,\n SentinelOptions,\n ViewportState,\n WindowSentinelOptions,\n} from './types.ts';",
27
+ "NetworkConnectionSnapshot": "export type {\n ElementSizeState,\n IntersectionState,\n MediaQueryState,\n NetworkConnectionSnapshot,\n NetworkState,\n Sentinel,\n SentinelOptions,\n ViewportState,\n WindowSentinelOptions,\n} from './types.ts';",
28
+ "NetworkState": "export type {\n ElementSizeState,\n IntersectionState,\n MediaQueryState,\n NetworkConnectionSnapshot,\n NetworkState,\n Sentinel,\n SentinelOptions,\n ViewportState,\n WindowSentinelOptions,\n} from './types.ts';",
29
+ "Sentinel": "export type {\n ElementSizeState,\n IntersectionState,\n MediaQueryState,\n NetworkConnectionSnapshot,\n NetworkState,\n Sentinel,\n SentinelOptions,\n ViewportState,\n WindowSentinelOptions,\n} from './types.ts';",
30
+ "SentinelOptions": "export type {\n ElementSizeState,\n IntersectionState,\n MediaQueryState,\n NetworkConnectionSnapshot,\n NetworkState,\n Sentinel,\n SentinelOptions,\n ViewportState,\n WindowSentinelOptions,\n} from './types.ts';",
31
+ "ViewportState": "export type {\n ElementSizeState,\n IntersectionState,\n MediaQueryState,\n NetworkConnectionSnapshot,\n NetworkState,\n Sentinel,\n SentinelOptions,\n ViewportState,\n WindowSentinelOptions,\n} from './types.ts';",
32
+ "WindowSentinelOptions": "export type {\n ElementSizeState,\n IntersectionState,\n MediaQueryState,\n NetworkConnectionSnapshot,\n NetworkState,\n Sentinel,\n SentinelOptions,\n ViewportState,\n WindowSentinelOptions,\n} from './types.ts';",
33
+ "createViewport": "export { createViewport } from './viewport.ts';"
34
+ }
35
+ }
@@ -0,0 +1,70 @@
1
+ {
2
+ "apiSource": "export { createCursorSource } from './cursorSource';\nexport { SourcererConfigurationError, SourcererDisposedError, SourcererError } from './errors';\nexport { createInfiniteSource } from './infiniteSource';\nexport { createLocalSource } from './localSource';\nexport { createPageSource } from './pageSource';\nexport type {\n CursorLoadContext,\n CursorPagination,\n CursorResult,\n CursorSource,\n CursorSourceConfig,\n CursorSourceState,\n InfiniteLoadContext,\n InfinitePagination,\n InfiniteSource,\n InfiniteSourceConfig,\n InfiniteSourceState,\n LocalSource,\n LocalSourceConfig,\n LocalSourceState,\n PageLoadContext,\n PagePagination,\n PageResult,\n PageSource,\n PageSourceConfig,\n PageSourceState,\n} from './types';\n",
3
+ "docs": {
4
+ "index": "---\ntitle: Sourcerer — Reactive collection sources\ndescription: Framework-agnostic local and remote collection state with page, cursor, and infinite pagination.\npackage: sourcerer\ncategory: data\nkeywords: [pagination, data-source, cursor, infinite-scroll, collection]\nrelated: [courier, ripple, scout, wayfinder]\nexports:\n [\n createCursorSource,\n createInfiniteSource,\n createLocalSource,\n createPageSource,\n CursorLoadContext,\n CursorPagination,\n CursorResult,\n CursorSource,\n CursorSourceConfig,\n CursorSourceState,\n InfiniteLoadContext,\n InfinitePagination,\n InfiniteSource,\n InfiniteSourceConfig,\n InfiniteSourceState,\n LocalSource,\n LocalSourceConfig,\n LocalSourceState,\n PageLoadContext,\n PagePagination,\n PageResult,\n PageSource,\n PageSourceConfig,\n PageSourceState,\n SourcererError,\n SourcererConfigurationError,\n SourcererDisposedError,\n ]\nenvironments: [browser, node, ssr, deno]\n---\n\n<!-- markdownlint-disable MD025 MD033 MD060 -->\n\n<PackageHero package=\"sourcerer\" />\n\n## Why Sourcerer?\n\nPaginated views need collection data, loading state, errors, parameters, and navigation to change coherently. Sourcerer owns that state and request succession without coupling it to a framework or transport client.\n\n```ts\n// Before\nlet items = [];\nlet loading = false;\nlet page = 1;\n\n// After\nconst source = createPageSource({\n load: async ({ page, pageSize }) => ({ items: await loadUsers(page, pageSize), totalItems: 100 }),\n});\nsource.subscribe((state) => render(state));\n```\n\n| Feature | Sourcerer | Manual store | General query cache |\n| --- | --- | --- | --- |\n| Bundle size | <PackageInfo package=\"sourcerer\" type=\"size\" /> | Application-defined | Package-defined |\n| Zero runtime dependencies | <ore-icon name=\"x\" size=\"16\"></ore-icon> | <ore-icon name=\"check\" size=\"16\"></ore-icon> | <ore-icon name=\"triangle-alert\" size=\"16\"></ore-icon> |\n| Page, cursor, and infinite navigation | <ore-icon name=\"check\" size=\"16\"></ore-icon> | Application-defined | Application-defined |\n| Framework-neutral subscriptions | <ore-icon name=\"check\" size=\"16\"></ore-icon> | Application-defined | <ore-icon name=\"triangle-alert\" size=\"16\"></ore-icon> |\n| HTTP transport policy | Loader-defined | Application-defined | Loader-defined |\n\n<div class=\"decision-callout\">\n\n**Use Sourcerer when** a collection view needs explicit pagination, observable state, cancellation, and lifecycle ownership.\n\n**Consider a general query cache when** your primary requirement is shared keyed caching, invalidation, mutations, or server-state normalization rather than collection navigation.\n\n</div>\n\n## Installation\n\n::: code-group\n\n```sh [pnpm]\npnpm add @vielzeug/sourcerer\n```\n\n```sh [npm]\nnpm install @vielzeug/sourcerer\n```\n\n```sh [yarn]\nyarn add @vielzeug/sourcerer\n```\n\n:::\n\n## Quick Start\n\nCreate a page source, load it, read its state, and dispose it with its owner.\n\n```ts\nimport { createPageSource } from '@vielzeug/sourcerer';\n\ntype User = { id: number; name: string };\nconst users: User[] = [\n { id: 1, name: 'Ada' },\n { id: 2, name: 'Grace' },\n];\nconst source = createPageSource({\n load: async ({ page, pageSize }) => {\n const start = (page - 1) * pageSize;\n return { items: users.slice(start, start + pageSize), totalItems: users.length };\n },\n pageSize: 1,\n});\n\ntry {\n await source.reload();\n console.log(source.state.items);\n} catch (error) {\n console.error(error);\n} finally {\n source.dispose();\n}\n```\n\n## Features\n\n<div class=\"features-grid\">\n\n- `createLocalSource()` — filters and paginates an in-memory collection synchronously\n- `createPageSource()` — loads numbered pages and exposes direct navigation commands\n- `createCursorSource()` — follows opaque cursors returned by the loader\n- `createInfiniteSource()` — appends pages while preserving loaded items\n- `setParams()` — replaces consumer-owned loader parameters and resets pagination\n- `subscribe()` — publishes complete immutable state replacements\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- [Courier](/courier/) — provide HTTP transport, middleware, and structured transport errors inside loaders\n- [Scout](/scout/) — index larger in-memory collections before passing matches to a local source\n- [Ripple](/ripple/) — project source state into reactive computations\n- [Wayfinder](/wayfinder/) — validate and synchronize source parameters with route state\n\n</div>\n\n<!-- markdownlint-enable MD025 MD033 MD060 -->\n",
5
+ "api": "---\ntitle: Sourcerer — API Reference\ndescription: Public API for @vielzeug/sourcerer.\n---\n\n[[toc]]\n\n## API Overview\n\n| Symbol | Purpose | Execution mode | Common gotcha |\n| --- | --- | --- | --- |\n| `createLocalSource()` | Filter and paginate in-memory items | Sync | `params` replacement uses `Object.is` |\n| `createPageSource()` | Load numbered pages | Async | Construction is inert |\n| `createCursorSource()` | Load server-issued cursor pages | Async | `after` and `before` cannot coexist initially |\n| `createInfiniteSource()` | Append numbered pages | Async | `loadMore()` is a no-op while loading or exhausted |\n| `SourcererError` | Base package error | Sync | Loader errors retain their original type |\n\n## Package Entry Point\n\n| Import | Purpose |\n| --- | --- |\n| `@vielzeug/sourcerer` | All factories, states, pagination types, loader contexts, and errors |\n\n## Source Factories\n\n### `createLocalSource()`\n\n```ts\nfunction createLocalSource<T>(items: readonly T[]): LocalSource<T>\nfunction createLocalSource<T, TParams = undefined>(\n items: readonly T[],\n config: LocalSourceConfig<T, TParams>,\n): LocalSource<T, TParams>\n```\n\nReturns a synchronous in-memory source.\n\n| Parameter | Type | Description |\n| --- | --- | --- |\n| `items` | `readonly T[]` | Initial collection |\n| `config.filter` | `(item: T, params: TParams) => boolean` | Optional inclusion predicate |\n| `config.pageSize` | `number` | Positive page size; defaults to `20` |\n| `config.params` | `TParams` | Initial params; required when `TParams` excludes `undefined` |\n\n**Returns:** `LocalSource<T, TParams>`.\n\n```ts\nimport { createLocalSource } from '@vielzeug/sourcerer';\n\nconst source = createLocalSource(['Ada', 'Grace'], {\n filter: (name, search: string) => name.includes(search),\n params: '',\n});\nsource.setParams('Ada');\n```\n\n| Method | Returns | Description |\n| --- | --- | --- |\n| `first()` | `void` | Move to page one |\n| `goTo(page)` | `void` | Move to a positive page, clamped to the known range |\n| `last()` | `void` | Move to the final page |\n| `next()` | `void` | Move forward when available |\n| `previous()` | `void` | Move backward when available |\n| `setItems(items)` | `void` | Replace the collection and recompute state |\n| `setPageSize(pageSize)` | `void` | Replace page size and reset to page one |\n| `setParams(params)` | `void` | Replace params and reset to page one |\n| `subscribe(listener)` | `() => void` | Register a state listener and return its unsubscribe function |\n| `dispose()` | `void` | Stop notifications and reject later mutations |\n\n---\n\n### `createPageSource()`\n\n```ts\nfunction createPageSource<T, TParams = undefined>(\n config: PageSourceConfig<T, TParams>,\n): PageSource<T, TParams>\n```\n\nReturns an inert numbered-page source.\n\n| Parameter | Type | Description |\n| --- | --- | --- |\n| `config.load` | `(context: PageLoadContext<TParams>) => Promise<PageResult<T>>` | Page loader |\n| `config.pageSize` | `number` | Positive page size; defaults to `20` |\n| `config.params` | `TParams` | Initial params; required when `TParams` excludes `undefined` |\n\n**Returns:** `PageSource<T, TParams>`.\n\n```ts\nimport { createPageSource } from '@vielzeug/sourcerer';\n\nconst source = createPageSource({\n load: async ({ page }) => ({ items: [page], totalItems: 10 }),\n});\nawait source.reload();\n```\n\n| Method | Returns | Description |\n| --- | --- | --- |\n| `first()` | `Promise<void>` | Load page one |\n| `goTo(page)` | `Promise<void>` | Load a positive page |\n| `last()` | `Promise<void>` | Load the last known page |\n| `next()` | `Promise<void>` | Load the next known page |\n| `previous()` | `Promise<void>` | Load the previous page |\n| `reload()` | `Promise<void>` | Reload the requested page and params |\n| `setPageSize(pageSize)` | `Promise<void>` | Reset to page one and load with a new size |\n| `setParams(params)` | `Promise<void>` | Reset to page one and load replacement params |\n| `subscribe(listener)` | `() => void` | Register a state listener and return its unsubscribe function |\n| `dispose()` | `void` | Abort work and stop notifications |\n\n---\n\n### `createCursorSource()`\n\n```ts\nfunction createCursorSource<T, TParams = undefined, TCursor = string>(\n config: CursorSourceConfig<T, TParams, TCursor>,\n): CursorSource<T, TParams, TCursor>\n```\n\nReturns an inert cursor source.\n\n| Parameter | Type | Description |\n| --- | --- | --- |\n| `config.after` | `TCursor` | Optional initial forward cursor |\n| `config.before` | `TCursor` | Optional initial backward cursor |\n| `config.load` | `(context: CursorLoadContext<TParams, TCursor>) => Promise<CursorResult<T, TCursor>>` | Cursor loader |\n| `config.pageSize` | `number` | Positive page size; defaults to `20` |\n| `config.params` | `TParams` | Initial params; required when `TParams` excludes `undefined` |\n\n**Returns:** `CursorSource<T, TParams, TCursor>`.\n\n```ts\nimport { createCursorSource } from '@vielzeug/sourcerer';\n\nconst source = createCursorSource({ load: async () => ({ items: ['first'], nextCursor: 'next' }) });\nawait source.reload();\nawait source.next();\n```\n\n| Method | Returns | Description |\n| --- | --- | --- |\n| `next()` | `Promise<void>` | Load `state.pagination.nextCursor` when available |\n| `previous()` | `Promise<void>` | Load `state.pagination.previousCursor` when available |\n| `reload()` | `Promise<void>` | Reload the requested cursor and params |\n| `setPageSize(pageSize)` | `Promise<void>` | Clear cursors and reload with a new size |\n| `setParams(params)` | `Promise<void>` | Clear cursors and load replacement params |\n| `subscribe(listener)` | `() => void` | Register a state listener and return its unsubscribe function |\n| `dispose()` | `void` | Abort work and stop notifications |\n\n---\n\n### `createInfiniteSource()`\n\n```ts\nfunction createInfiniteSource<T, TParams = undefined>(\n config: InfiniteSourceConfig<T, TParams>,\n): InfiniteSource<T, TParams>\n```\n\nReturns an inert source that appends numbered pages.\n\n| Parameter | Type | Description |\n| --- | --- | --- |\n| `config.load` | `(context: InfiniteLoadContext<TParams>) => Promise<PageResult<T>>` | Page loader |\n| `config.pageSize` | `number` | Positive page size; defaults to `20` |\n| `config.params` | `TParams` | Initial params; required when `TParams` excludes `undefined` |\n\n**Returns:** `InfiniteSource<T, TParams>`.\n\n```ts\nimport { createInfiniteSource } from '@vielzeug/sourcerer';\n\nconst source = createInfiniteSource({ load: async () => ({ items: ['post'], totalItems: 1 }) });\nawait source.reload();\nawait source.loadMore();\n```\n\n| Method | Returns | Description |\n| --- | --- | --- |\n| `loadMore()` | `Promise<void>` | Append the next page when available |\n| `reload()` | `Promise<void>` | Replace items from page one |\n| `setPageSize(pageSize)` | `Promise<void>` | Replace items from page one with a new size |\n| `setParams(params)` | `Promise<void>` | Replace items from page one with new params |\n| `subscribe(listener)` | `() => void` | Register a state listener and return its unsubscribe function |\n| `dispose()` | `void` | Abort work and stop notifications |\n\n## Types\n\n### Page source types\n\n```ts\ntype PagePagination = Readonly<{\n hasNext: boolean;\n hasPrevious: boolean;\n page: number;\n pageCount: number;\n pageSize: number;\n totalItems: number;\n}>;\n\ntype PageSourceState<T, TParams = undefined> = Readonly<{\n error: Error | null;\n items: readonly T[];\n loading: boolean;\n pagination: PagePagination;\n params: TParams;\n pendingParams?: TParams;\n}>;\n\ntype PageLoadContext<TParams = undefined> = Readonly<{\n page: number;\n pageSize: number;\n params: TParams;\n signal: AbortSignal;\n}>;\n\ntype PageResult<T> = Readonly<{\n items: readonly T[];\n totalItems: number;\n}>;\n\ntype PageSourceConfig<T, TParams = undefined> = Readonly<{\n load(context: PageLoadContext<TParams>): Promise<PageResult<T>>;\n pageSize?: number;\n } & (undefined extends TParams ? { params?: TParams } : { params: TParams })\n>;\n\ntype PageSource<T, TParams = undefined> = {\n [Symbol.dispose](): void;\n readonly disposalSignal: AbortSignal;\n dispose(): void;\n readonly disposed: boolean;\n first(): Promise<void>;\n goTo(page: number): Promise<void>;\n last(): Promise<void>;\n next(): Promise<void>;\n previous(): Promise<void>;\n reload(): Promise<void>;\n setPageSize(pageSize: number): Promise<void>;\n setParams(params: TParams): Promise<void>;\n readonly state: PageSourceState<T, TParams>;\n subscribe(listener: (state: PageSourceState<T, TParams>) => void): () => void;\n};\n```\n\nThe conditional config field makes `params` optional only when `TParams` includes `undefined`.\n\n### Local source types\n\n```ts\ntype LocalSourceState<T, TParams = undefined> = Readonly<{\n error: null;\n items: readonly T[];\n loading: false;\n pagination: PagePagination;\n params: TParams;\n}>;\n\ntype LocalSourceConfig<T, TParams = undefined> = Readonly<{\n filter?: (item: T, params: TParams) => boolean;\n pageSize?: number;\n } & (undefined extends TParams ? { params?: TParams } : { params: TParams })\n>;\n\ntype LocalSource<T, TParams = undefined> = {\n [Symbol.dispose](): void;\n readonly disposalSignal: AbortSignal;\n dispose(): void;\n readonly disposed: boolean;\n first(): void;\n goTo(page: number): void;\n last(): void;\n next(): void;\n previous(): void;\n setItems(items: readonly T[]): void;\n setPageSize(pageSize: number): void;\n setParams(params: TParams): void;\n readonly state: LocalSourceState<T, TParams>;\n subscribe(listener: (state: LocalSourceState<T, TParams>) => void): () => void;\n};\n```\n\n### Cursor source types\n\n```ts\ntype CursorPagination<TCursor = string> = Readonly<{\n nextCursor?: TCursor;\n pageSize: number;\n previousCursor?: TCursor;\n totalItems?: number;\n}>;\n\ntype CursorLoadContext<TParams = undefined, TCursor = string> = Readonly<{\n after?: TCursor;\n before?: TCursor;\n pageSize: number;\n params: TParams;\n signal: AbortSignal;\n}>;\n\ntype CursorResult<T, TCursor = string> = Readonly<{\n items: readonly T[];\n nextCursor?: TCursor;\n previousCursor?: TCursor;\n totalItems?: number;\n}>;\n\ntype CursorSourceState<T, TParams = undefined, TCursor = string> = Readonly<{\n error: Error | null;\n items: readonly T[];\n loading: boolean;\n pagination: CursorPagination<TCursor>;\n params: TParams;\n pendingParams?: TParams;\n}>;\n\ntype CursorSourceConfig<T, TParams = undefined, TCursor = string> = Readonly<{\n after?: TCursor;\n before?: TCursor;\n load(context: CursorLoadContext<TParams, TCursor>): Promise<CursorResult<T, TCursor>>;\n pageSize?: number;\n } & (undefined extends TParams ? { params?: TParams } : { params: TParams })\n>;\n\ntype CursorSource<T, TParams = undefined, TCursor = string> = {\n [Symbol.dispose](): void;\n readonly disposalSignal: AbortSignal;\n dispose(): void;\n readonly disposed: boolean;\n next(): Promise<void>;\n previous(): Promise<void>;\n reload(): Promise<void>;\n setPageSize(pageSize: number): Promise<void>;\n setParams(params: TParams): Promise<void>;\n readonly state: CursorSourceState<T, TParams, TCursor>;\n subscribe(listener: (state: CursorSourceState<T, TParams, TCursor>) => void): () => void;\n};\n```\n\n### Infinite source types\n\n```ts\ntype InfinitePagination = Readonly<{\n hasMore: boolean;\n loadedItems: number;\n pageSize: number;\n totalItems: number;\n}>;\n\ntype InfiniteLoadContext<TParams = undefined> = Readonly<{\n page: number;\n pageSize: number;\n params: TParams;\n signal: AbortSignal;\n}>;\n\ntype InfiniteSourceState<T, TParams = undefined> = Readonly<{\n error: Error | null;\n items: readonly T[];\n loading: boolean;\n pagination: InfinitePagination;\n params: TParams;\n pendingParams?: TParams;\n}>;\n\ntype InfiniteSourceConfig<T, TParams = undefined> = Readonly<{\n load(context: InfiniteLoadContext<TParams>): Promise<PageResult<T>>;\n pageSize?: number;\n } & (undefined extends TParams ? { params?: TParams } : { params: TParams })\n>;\n\ntype InfiniteSource<T, TParams = undefined> = {\n [Symbol.dispose](): void;\n readonly disposalSignal: AbortSignal;\n dispose(): void;\n readonly disposed: boolean;\n loadMore(): Promise<void>;\n reload(): Promise<void>;\n setPageSize(pageSize: number): Promise<void>;\n setParams(params: TParams): Promise<void>;\n readonly state: InfiniteSourceState<T, TParams>;\n subscribe(listener: (state: InfiniteSourceState<T, TParams>) => void): () => void;\n};\n```\n\n## Errors\n\n| Error | Trigger | Notable properties |\n| --- | --- | --- |\n| `SourcererError` | Base class for package-originated configuration and lifecycle errors | `instanceof SourcererError` narrows the hierarchy |\n| `SourcererConfigurationError` | Invalid page, page size, total, or initial cursor direction | Extends `SourcererError` |\n| `SourcererDisposedError` | A source command runs after disposal | Extends `SourcererError` |\n\nLoader failures are preserved when they are `Error` instances. Non-Error rejections become `Error` objects whose `cause` contains the original value.\n",
6
+ "usage": "---\ntitle: Sourcerer — Usage Guide\ndescription: Build local, page, cursor, and infinite collection sources.\n---\n\n[[toc]]\n\n## Basic Usage\n\nCreate a page source and call `reload()` to start the first request. Construction performs no I/O.\n\n```ts\nimport { createPageSource } from '@vielzeug/sourcerer';\n\nconst names = ['Ada', 'Grace', 'Linus'];\nconst source = createPageSource({\n load: async ({ page, pageSize }) => {\n const start = (page - 1) * pageSize;\n return { items: names.slice(start, start + pageSize), totalItems: names.length };\n },\n pageSize: 2,\n});\n\nawait source.reload();\nconsole.log(source.state.items);\nsource.dispose();\n```\n\nRead one `state` object per render. It contains `items`, `loading`, `error`, `params`, and pagination metadata from one committed result. Commands return `Promise<void>`; read `source.state` after awaiting them.\n\n## Pass Loader Parameters\n\nUse `params` for application-owned filters, search terms, sort state, or request options. `setParams()` replaces the entire value and resets pagination.\n\n```ts\nimport { createPageSource } from '@vielzeug/sourcerer';\n\ntype UserParams = { role: 'admin' | 'user'; search: string };\nconst users = ['Ada', 'Grace', 'Linus'];\nconst source = createPageSource<string, UserParams>({\n load: async ({ page, pageSize, params }) => {\n const matching = users.filter((name) => name.toLowerCase().includes(params.search.toLowerCase()));\n const start = (page - 1) * pageSize;\n return { items: matching.slice(start, start + pageSize), totalItems: matching.length };\n },\n params: { role: 'admin', search: '' },\n});\n\nawait source.setParams({ role: 'admin', search: 'ada' });\nconsole.log(source.state.params);\nsource.dispose();\n```\n\nA non-optional parameter type requires an initial `params` value. Sourcerer compares parameter values with `Object.is`; reuse a value when setting it again should be a no-op.\n\nWhile replacement parameters load, `state.params` continues to describe the committed items and `state.pendingParams` contains the requested value. Page-only navigation does not set `pendingParams`.\n\n## Navigate Numbered Pages\n\nUse direct commands for numbered-page navigation. `setPageSize()` resets to page one.\n\n```ts\nawait source.reload();\nawait source.next();\nawait source.goTo(4);\nawait source.previous();\nawait source.first();\nawait source.last();\nawait source.setPageSize(50);\n```\n\nAfter the first successful load, navigation is clamped to the known `pageCount`. Before that load, `goTo()` accepts any positive page because the total is unknown.\n\n## Filter Local Collections\n\nUse a local source when data is already in memory. Its commands are synchronous.\n\n```ts\nimport { createLocalSource } from '@vielzeug/sourcerer';\n\nconst source = createLocalSource(['Ada', 'Grace', 'Linus'], {\n filter: (name, search: string) => name.toLowerCase().includes(search.toLowerCase()),\n pageSize: 2,\n params: '',\n});\n\nsource.setParams('a');\nsource.next();\nsource.setItems(['Ada', 'Grace']);\nconsole.log(source.state.items);\nsource.dispose();\n```\n\nUse ordinary arrays for one-off transformations. A local source is useful when pagination, subscriptions, and lifecycle ownership are also required.\n\n## Use Cursor Pagination\n\nReturn opaque cursors from the loader. `next()` and `previous()` use only cursors from the committed state.\n\n```ts\nimport { createCursorSource } from '@vielzeug/sourcerer';\n\nconst rows = ['A', 'B', 'C', 'D'];\nconst source = createCursorSource({\n load: async ({ after, pageSize }) => {\n const start = after ? Number(after) : 0;\n const items = rows.slice(start, start + pageSize);\n const next = start + items.length;\n return {\n items,\n nextCursor: next < rows.length ? String(next) : undefined,\n previousCursor: start > 0 ? String(Math.max(0, start - pageSize)) : undefined,\n };\n },\n pageSize: 2,\n});\n\nawait source.reload();\nawait source.next();\nconsole.log(source.state.items);\nsource.dispose();\n```\n\nChanging params or page size clears current cursor direction. A source cannot start with both `after` and `before`.\n\n## Build an Infinite Feed\n\nUse an infinite source when later pages should append to committed items.\n\n```ts\nimport { createInfiniteSource } from '@vielzeug/sourcerer';\n\nconst values = [1, 2, 3, 4, 5];\nconst source = createInfiniteSource({\n load: async ({ page, pageSize }) => {\n const start = (page - 1) * pageSize;\n return { items: values.slice(start, start + pageSize), totalItems: values.length };\n },\n pageSize: 2,\n});\n\nawait source.reload();\nawait source.loadMore();\nconsole.log(source.state.items);\nsource.dispose();\n```\n\n`loadMore()` is a no-op while another request is active or after `pagination.hasMore` becomes false. `setParams()` and `setPageSize()` replace the feed from page one.\n\n## Handle Requests and Errors\n\nA new remote command aborts older work. Superseded or disposed work settles without committing. A current loader failure preserves committed items, writes the error to state, and rejects the command.\n\n```ts\ntry {\n await source.reload();\n} catch (error) {\n console.error(error);\n console.log(source.state.error);\n}\n```\n\nLoader result arrays are copied before publication. Commands after disposal throw or reject `SourcererDisposedError`.\n\n## Testing\n\nInject deterministic loaders and await commands before asserting final state.\n\n```ts\nimport { expect, it } from 'vitest';\nimport { createPageSource } from '@vielzeug/sourcerer';\n\nit('loads a page', async () => {\n const source = createPageSource({ load: async () => ({ items: ['Ada'], totalItems: 1 }) });\n\n await source.reload();\n\n expect(source.state.items).toEqual(['Ada']);\n source.dispose();\n});\n```\n\n## Framework Integration\n\nCreate one source per component lifetime and bridge `subscribe()` to the framework lifecycle.\n\n::: code-group\n\n```tsx [React]\nimport { createPageSource } from '@vielzeug/sourcerer';\nimport { useEffect, useMemo, useSyncExternalStore } from 'react';\n\nexport function Users() {\n const source = useMemo(\n () => createPageSource({ load: async () => ({ items: [{ id: 1, name: 'Ada' }], totalItems: 1 }) }),\n [],\n );\n const state = useSyncExternalStore(source.subscribe, () => source.state);\n\n useEffect(() => {\n void source.reload().catch(() => undefined);\n return () => source.dispose();\n }, [source]);\n\n return <p>{state.loading ? 'Loading' : state.items.length}</p>;\n}\n```\n\n```ts [Vue 3]\nimport { onUnmounted, shallowRef } from 'vue';\nimport { createPageSource } from '@vielzeug/sourcerer';\n\nconst source = createPageSource({ load: async () => ({ items: [{ id: 1, name: 'Ada' }], totalItems: 1 }) });\nconst state = shallowRef(source.state);\nconst stop = source.subscribe((next) => (state.value = next));\nvoid source.reload().catch(() => undefined);\n\nonUnmounted(() => {\n stop();\n source.dispose();\n});\n```\n\n```svelte [Svelte]\n<script lang=\"ts\">\n import { onDestroy } from 'svelte';\n import { createPageSource } from '@vielzeug/sourcerer';\n\n const source = createPageSource({ load: async () => ({ items: [{ id: 1, name: 'Ada' }], totalItems: 1 }) });\n let state = source.state;\n const stop = source.subscribe((next) => (state = next));\n void source.reload().catch(() => undefined);\n\n onDestroy(() => {\n stop();\n source.dispose();\n });\n</script>\n\n{#if state.loading}Loading{/if}\n{#each state.items as user}{user.name}{/each}\n```\n\n:::\n\n## Working with Other Vielzeug Libraries\n\nUse Courier for transport policy and pass the source-owned cancellation signal through.\n\n```ts\nimport { createCourier } from '@vielzeug/courier';\nimport { createPageSource } from '@vielzeug/sourcerer';\n\ntype User = { id: number; name: string };\ntype Response = { data: User[]; total: number };\nconst courier = createCourier({ baseUrl: '/api' });\nconst source = createPageSource({\n load: async ({ page, pageSize, params: search, signal }) => {\n const result = await courier.get<Response>('/users', { query: { page, pageSize, search }, signal });\n return { items: result.data, totalItems: result.total };\n },\n params: '',\n});\n```\n\nUse Scout to compute indexed local matches, Ripple to project state into reactive computations, and Wayfinder to validate and synchronize params with the URL.\n\n## Best Practices\n\n- Dispose each source with its owning view, request, or service scope.\n- Read one `state` object per render.\n- Await direct commands or handle rejected current-request failures.\n- Keep transport, caching, and persistence policy outside Sourcerer.\n- Treat `params` as an immutable value and replace it atomically.\n- Debounce text input before calling `setParams()` on a remote source.\n- Validate route and form values before constructing params or page numbers.\n- Keep rendering committed items while `loading` is true.\n",
7
+ "examples": "---\ntitle: Sourcerer — Examples\ndescription: Recipes for local, page, cursor, infinite, and framework source usage.\n---\n\n## Examples\n\n- [Local Pagination and Filtering](./examples/local-pagination-and-filtering.md)\n- [Page Params with URL State](./examples/remote-search-with-url-state.md)\n- [Cursor-Based Pagination](./examples/cursor-based-pagination.md)\n- [Infinite Scroll](./examples/infinite-scroll.md)\n- [Framework Integration](./examples/framework-integration.md)\n- [Remote Data with Courier](./examples/sourcerer-with-courier.md)\n- [Reactive Controls with Ripple](./examples/sourcerer-with-ripple.md)\n- [URL-Synced List with Wayfinder](./examples/sourcerer-with-wayfinder.md)\n"
8
+ },
9
+ "examples": [
10
+ {
11
+ "id": "cursor-source",
12
+ "code": "import { createCursorSource } from '@vielzeug/sourcerer'\n\nconst items = Array.from({ length: 30 }, (_, index) => ({ id: index + 1, label: `Item ${index + 1}` }))\n\nconst source = createCursorSource({\n load: async ({ after, pageSize }) => {\n const start = after ? Number(after) : 0\n const pageItems = items.slice(start, start + pageSize)\n const next = start + pageItems.length\n return { items: pageItems, nextCursor: next < items.length ? String(next) : undefined, previousCursor: start ? String(Math.max(0, start - pageSize)) : undefined }\n },\n pageSize: 10,\n})\n\nawait source.reload()\nawait source.next()\nconsole.log(source.state.items.map((item) => item.label))\nconsole.log(source.state.pagination)\n\nsource.dispose()",
13
+ "name": "Cursor Source"
14
+ },
15
+ {
16
+ "id": "error-handling",
17
+ "code": "import { createPageSource } from '@vielzeug/sourcerer'\n\nconst source = createPageSource({\n load: async () => { throw new Error('network down') },\n})\n\ntry {\n await source.reload()\n} catch (error) {\n console.log((error as Error).message)\n}\n\nconsole.log(source.state.error?.message)\nsource.dispose()",
18
+ "name": "Error Handling"
19
+ },
20
+ {
21
+ "id": "infinite-source",
22
+ "code": "import { createInfiniteSource } from '@vielzeug/sourcerer'\n\nconst posts = Array.from({ length: 25 }, (_, index) => ({ id: index + 1, title: `Post ${index + 1}` }))\n\nconst source = createInfiniteSource({\n load: async ({ page, pageSize }) => {\n const start = (page - 1) * pageSize\n return { items: posts.slice(start, start + pageSize), totalItems: posts.length }\n },\n pageSize: 8,\n})\n\nawait source.reload()\nawait source.loadMore()\nconsole.log(source.state.items.length)\nconsole.log(source.state.pagination)\n\nsource.dispose()",
23
+ "name": "Infinite Source"
24
+ },
25
+ {
26
+ "id": "lifecycle",
27
+ "code": "import { createPageSource } from '@vielzeug/sourcerer'\n\nconst source = createPageSource({\n load: async () => ({ items: ['item'], totalItems: 1 }),\n})\n\nconsole.log(source.disposed)\nsource.disposalSignal.addEventListener('abort', () => console.log('disposed'))\nawait source.reload()\nconsole.log(source.state.items)\nsource.dispose()\nconsole.log(source.disposalSignal.aborted)",
28
+ "name": "Source Lifecycle"
29
+ },
30
+ {
31
+ "id": "local-source",
32
+ "code": "import { createLocalSource } from '@vielzeug/sourcerer'\n\nconst users = [\n { id: 1, name: 'Ada', role: 'admin' },\n { id: 2, name: 'Grace', role: 'admin' },\n { id: 3, name: 'Linus', role: 'user' },\n]\n\nconst source = createLocalSource(users, {\n filter: (user, search) => user.name.toLowerCase().includes(search.toLowerCase()),\n pageSize: 2,\n params: '',\n})\n\nsource.setParams('a')\nconsole.log(source.state.items)\nconsole.log(source.state.pagination)\n\nsource.dispose()",
33
+ "name": "Local Source"
34
+ },
35
+ {
36
+ "id": "page-source",
37
+ "code": "import { createPageSource } from '@vielzeug/sourcerer'\n\nconst allItems = Array.from({ length: 47 }, (_, index) => ({ id: index + 1, name: `Item ${index + 1}` }))\n\nconst source = createPageSource({\n load: async ({ page, pageSize, params: search }) => {\n const filtered = search ? allItems.filter((item) => item.name.includes(search)) : allItems\n const start = (page - 1) * pageSize\n return { items: filtered.slice(start, start + pageSize), totalItems: filtered.length }\n },\n pageSize: 10,\n params: '',\n})\n\nawait source.reload()\nawait source.setParams('Item 4')\nconsole.log(source.state.items.map((item) => item.name))\nconsole.log(source.state.pagination)\n\nsource.dispose()",
38
+ "name": "Page Source"
39
+ }
40
+ ],
41
+ "typeSignatures": {
42
+ "createCursorSource": "export { createCursorSource } from './cursorSource';",
43
+ "SourcererConfigurationError": "export { SourcererConfigurationError, SourcererDisposedError, SourcererError } from './errors';",
44
+ "SourcererDisposedError": "export { SourcererConfigurationError, SourcererDisposedError, SourcererError } from './errors';",
45
+ "SourcererError": "export { SourcererConfigurationError, SourcererDisposedError, SourcererError } from './errors';",
46
+ "createInfiniteSource": "export { createInfiniteSource } from './infiniteSource';",
47
+ "createLocalSource": "export { createLocalSource } from './localSource';",
48
+ "createPageSource": "export { createPageSource } from './pageSource';",
49
+ "CursorLoadContext": "export type {\n CursorLoadContext,\n CursorPagination,\n CursorResult,\n CursorSource,\n CursorSourceConfig,\n CursorSourceState,\n InfiniteLoadContext,\n InfinitePagination,\n InfiniteSource,\n InfiniteSourceConfig,\n InfiniteSourceState,\n LocalSource,\n LocalSourceConfig,\n LocalSourceState,\n PageLoadContext,\n PagePagination,\n PageResult,\n PageSource,\n PageSourceConfig,\n PageSourceState,\n} from './types';",
50
+ "CursorPagination": "export type {\n CursorLoadContext,\n CursorPagination,\n CursorResult,\n CursorSource,\n CursorSourceConfig,\n CursorSourceState,\n InfiniteLoadContext,\n InfinitePagination,\n InfiniteSource,\n InfiniteSourceConfig,\n InfiniteSourceState,\n LocalSource,\n LocalSourceConfig,\n LocalSourceState,\n PageLoadContext,\n PagePagination,\n PageResult,\n PageSource,\n PageSourceConfig,\n PageSourceState,\n} from './types';",
51
+ "CursorResult": "export type {\n CursorLoadContext,\n CursorPagination,\n CursorResult,\n CursorSource,\n CursorSourceConfig,\n CursorSourceState,\n InfiniteLoadContext,\n InfinitePagination,\n InfiniteSource,\n InfiniteSourceConfig,\n InfiniteSourceState,\n LocalSource,\n LocalSourceConfig,\n LocalSourceState,\n PageLoadContext,\n PagePagination,\n PageResult,\n PageSource,\n PageSourceConfig,\n PageSourceState,\n} from './types';",
52
+ "CursorSource": "export type {\n CursorLoadContext,\n CursorPagination,\n CursorResult,\n CursorSource,\n CursorSourceConfig,\n CursorSourceState,\n InfiniteLoadContext,\n InfinitePagination,\n InfiniteSource,\n InfiniteSourceConfig,\n InfiniteSourceState,\n LocalSource,\n LocalSourceConfig,\n LocalSourceState,\n PageLoadContext,\n PagePagination,\n PageResult,\n PageSource,\n PageSourceConfig,\n PageSourceState,\n} from './types';",
53
+ "CursorSourceConfig": "export type {\n CursorLoadContext,\n CursorPagination,\n CursorResult,\n CursorSource,\n CursorSourceConfig,\n CursorSourceState,\n InfiniteLoadContext,\n InfinitePagination,\n InfiniteSource,\n InfiniteSourceConfig,\n InfiniteSourceState,\n LocalSource,\n LocalSourceConfig,\n LocalSourceState,\n PageLoadContext,\n PagePagination,\n PageResult,\n PageSource,\n PageSourceConfig,\n PageSourceState,\n} from './types';",
54
+ "CursorSourceState": "export type {\n CursorLoadContext,\n CursorPagination,\n CursorResult,\n CursorSource,\n CursorSourceConfig,\n CursorSourceState,\n InfiniteLoadContext,\n InfinitePagination,\n InfiniteSource,\n InfiniteSourceConfig,\n InfiniteSourceState,\n LocalSource,\n LocalSourceConfig,\n LocalSourceState,\n PageLoadContext,\n PagePagination,\n PageResult,\n PageSource,\n PageSourceConfig,\n PageSourceState,\n} from './types';",
55
+ "InfiniteLoadContext": "export type {\n CursorLoadContext,\n CursorPagination,\n CursorResult,\n CursorSource,\n CursorSourceConfig,\n CursorSourceState,\n InfiniteLoadContext,\n InfinitePagination,\n InfiniteSource,\n InfiniteSourceConfig,\n InfiniteSourceState,\n LocalSource,\n LocalSourceConfig,\n LocalSourceState,\n PageLoadContext,\n PagePagination,\n PageResult,\n PageSource,\n PageSourceConfig,\n PageSourceState,\n} from './types';",
56
+ "InfinitePagination": "export type {\n CursorLoadContext,\n CursorPagination,\n CursorResult,\n CursorSource,\n CursorSourceConfig,\n CursorSourceState,\n InfiniteLoadContext,\n InfinitePagination,\n InfiniteSource,\n InfiniteSourceConfig,\n InfiniteSourceState,\n LocalSource,\n LocalSourceConfig,\n LocalSourceState,\n PageLoadContext,\n PagePagination,\n PageResult,\n PageSource,\n PageSourceConfig,\n PageSourceState,\n} from './types';",
57
+ "InfiniteSource": "export type {\n CursorLoadContext,\n CursorPagination,\n CursorResult,\n CursorSource,\n CursorSourceConfig,\n CursorSourceState,\n InfiniteLoadContext,\n InfinitePagination,\n InfiniteSource,\n InfiniteSourceConfig,\n InfiniteSourceState,\n LocalSource,\n LocalSourceConfig,\n LocalSourceState,\n PageLoadContext,\n PagePagination,\n PageResult,\n PageSource,\n PageSourceConfig,\n PageSourceState,\n} from './types';",
58
+ "InfiniteSourceConfig": "export type {\n CursorLoadContext,\n CursorPagination,\n CursorResult,\n CursorSource,\n CursorSourceConfig,\n CursorSourceState,\n InfiniteLoadContext,\n InfinitePagination,\n InfiniteSource,\n InfiniteSourceConfig,\n InfiniteSourceState,\n LocalSource,\n LocalSourceConfig,\n LocalSourceState,\n PageLoadContext,\n PagePagination,\n PageResult,\n PageSource,\n PageSourceConfig,\n PageSourceState,\n} from './types';",
59
+ "InfiniteSourceState": "export type {\n CursorLoadContext,\n CursorPagination,\n CursorResult,\n CursorSource,\n CursorSourceConfig,\n CursorSourceState,\n InfiniteLoadContext,\n InfinitePagination,\n InfiniteSource,\n InfiniteSourceConfig,\n InfiniteSourceState,\n LocalSource,\n LocalSourceConfig,\n LocalSourceState,\n PageLoadContext,\n PagePagination,\n PageResult,\n PageSource,\n PageSourceConfig,\n PageSourceState,\n} from './types';",
60
+ "LocalSource": "export type {\n CursorLoadContext,\n CursorPagination,\n CursorResult,\n CursorSource,\n CursorSourceConfig,\n CursorSourceState,\n InfiniteLoadContext,\n InfinitePagination,\n InfiniteSource,\n InfiniteSourceConfig,\n InfiniteSourceState,\n LocalSource,\n LocalSourceConfig,\n LocalSourceState,\n PageLoadContext,\n PagePagination,\n PageResult,\n PageSource,\n PageSourceConfig,\n PageSourceState,\n} from './types';",
61
+ "LocalSourceConfig": "export type {\n CursorLoadContext,\n CursorPagination,\n CursorResult,\n CursorSource,\n CursorSourceConfig,\n CursorSourceState,\n InfiniteLoadContext,\n InfinitePagination,\n InfiniteSource,\n InfiniteSourceConfig,\n InfiniteSourceState,\n LocalSource,\n LocalSourceConfig,\n LocalSourceState,\n PageLoadContext,\n PagePagination,\n PageResult,\n PageSource,\n PageSourceConfig,\n PageSourceState,\n} from './types';",
62
+ "LocalSourceState": "export type {\n CursorLoadContext,\n CursorPagination,\n CursorResult,\n CursorSource,\n CursorSourceConfig,\n CursorSourceState,\n InfiniteLoadContext,\n InfinitePagination,\n InfiniteSource,\n InfiniteSourceConfig,\n InfiniteSourceState,\n LocalSource,\n LocalSourceConfig,\n LocalSourceState,\n PageLoadContext,\n PagePagination,\n PageResult,\n PageSource,\n PageSourceConfig,\n PageSourceState,\n} from './types';",
63
+ "PageLoadContext": "export type {\n CursorLoadContext,\n CursorPagination,\n CursorResult,\n CursorSource,\n CursorSourceConfig,\n CursorSourceState,\n InfiniteLoadContext,\n InfinitePagination,\n InfiniteSource,\n InfiniteSourceConfig,\n InfiniteSourceState,\n LocalSource,\n LocalSourceConfig,\n LocalSourceState,\n PageLoadContext,\n PagePagination,\n PageResult,\n PageSource,\n PageSourceConfig,\n PageSourceState,\n} from './types';",
64
+ "PagePagination": "export type {\n CursorLoadContext,\n CursorPagination,\n CursorResult,\n CursorSource,\n CursorSourceConfig,\n CursorSourceState,\n InfiniteLoadContext,\n InfinitePagination,\n InfiniteSource,\n InfiniteSourceConfig,\n InfiniteSourceState,\n LocalSource,\n LocalSourceConfig,\n LocalSourceState,\n PageLoadContext,\n PagePagination,\n PageResult,\n PageSource,\n PageSourceConfig,\n PageSourceState,\n} from './types';",
65
+ "PageResult": "export type {\n CursorLoadContext,\n CursorPagination,\n CursorResult,\n CursorSource,\n CursorSourceConfig,\n CursorSourceState,\n InfiniteLoadContext,\n InfinitePagination,\n InfiniteSource,\n InfiniteSourceConfig,\n InfiniteSourceState,\n LocalSource,\n LocalSourceConfig,\n LocalSourceState,\n PageLoadContext,\n PagePagination,\n PageResult,\n PageSource,\n PageSourceConfig,\n PageSourceState,\n} from './types';",
66
+ "PageSource": "export type {\n CursorLoadContext,\n CursorPagination,\n CursorResult,\n CursorSource,\n CursorSourceConfig,\n CursorSourceState,\n InfiniteLoadContext,\n InfinitePagination,\n InfiniteSource,\n InfiniteSourceConfig,\n InfiniteSourceState,\n LocalSource,\n LocalSourceConfig,\n LocalSourceState,\n PageLoadContext,\n PagePagination,\n PageResult,\n PageSource,\n PageSourceConfig,\n PageSourceState,\n} from './types';",
67
+ "PageSourceConfig": "export type {\n CursorLoadContext,\n CursorPagination,\n CursorResult,\n CursorSource,\n CursorSourceConfig,\n CursorSourceState,\n InfiniteLoadContext,\n InfinitePagination,\n InfiniteSource,\n InfiniteSourceConfig,\n InfiniteSourceState,\n LocalSource,\n LocalSourceConfig,\n LocalSourceState,\n PageLoadContext,\n PagePagination,\n PageResult,\n PageSource,\n PageSourceConfig,\n PageSourceState,\n} from './types';",
68
+ "PageSourceState": "export type {\n CursorLoadContext,\n CursorPagination,\n CursorResult,\n CursorSource,\n CursorSourceConfig,\n CursorSourceState,\n InfiniteLoadContext,\n InfinitePagination,\n InfiniteSource,\n InfiniteSourceConfig,\n InfiniteSourceState,\n LocalSource,\n LocalSourceConfig,\n LocalSourceState,\n PageLoadContext,\n PagePagination,\n PageResult,\n PageSource,\n PageSourceConfig,\n PageSourceState,\n} from './types';"
69
+ }
70
+ }
@@ -0,0 +1,128 @@
1
+ {
2
+ "apiSource": "export type {\n AnySchema,\n CheckContext,\n FlatError,\n FlatErrorFirst,\n Infer,\n InferInput,\n InferOutput,\n Issue,\n JsonSchema,\n MessageFn,\n Messages,\n ParseContext,\n ParseResult,\n SchemaDescriptor,\n SchemaWalker,\n StandardSchemaV1,\n ValidateResult,\n} from './core';\nexport { ErrorCode, Schema, SpellDefinitionError, SpellError, SpellValidationError } from './core';\nexport { createParseContext, type DeepPartial } from './messages';\nexport { s } from './s';\n",
3
+ "docs": {
4
+ "index": "---\ntitle: Spell — Schema validation for TypeScript\ndescription: Zero-dependency schema validation with explicit async checks, Standard Schema interoperability, and JSON Schema export.\npackage: spell\ncategory: validation\nkeywords: [schema, validation, parsing, standard-schema, json-schema, locale, typescript, descriptors]\nrelated: [forge, courier, vault]\nexports:\n [s, createParseContext, SpellValidationError, SpellDefinitionError, StandardSchemaV1, ErrorCode, './json', './predicates']\nenvironments: [browser, node, ssr, deno]\n---\n\n<!-- markdownlint-disable MD025 MD033 MD060 -->\n\n<PackageHero package=\"spell\" />\n\n## Why Spell?\n\nSpell keeps runtime validation, static inference, and portable definitions in one API. Use `s` for schema construction; import JSON conversion and predicates from dedicated subpaths.\n\nThis example shows the difference between manual branching and a single reusable schema.\n\n```ts\n// Before\nfunction parseUserBefore(value: unknown) {\n if (typeof value !== 'object' || value === null) throw new Error('Expected object');\n\n const candidate = value as Record<string, unknown>;\n\n if (typeof candidate.email !== 'string' || !candidate.email.includes('@')) {\n throw new Error('Expected valid email');\n }\n\n if (typeof candidate.role !== 'string' || !['admin', 'editor', 'viewer'].includes(candidate.role)) {\n throw new Error('Expected valid role');\n }\n\n return {\n email: candidate.email,\n role: candidate.role,\n };\n}\n\n// After\nimport { s } from '@vielzeug/spell';\n\nconst User = s.object({\n email: s.string().email(),\n role: s.enum(['admin', 'editor', 'viewer'] as const),\n});\n\nconst user = User.parse({ email: 'ada@example.com', role: 'admin' });\n```\n\n| Feature | Spell | Zod | Yup |\n| ----------------- | --------------------------------------------------------------------------- | -------------------------------------------- | -------------------------------------------- |\n| Bundle size | <PackageInfo package=\"spell\" type=\"size\" /> | ~62 kB | ~14 kB |\n| Type inference | <ore-icon name=\"check\" size=\"16\"></ore-icon> `Infer<T>` | <ore-icon name=\"check\" size=\"16\"></ore-icon> | Partial |\n| Coercion API | <ore-icon name=\"check\" size=\"16\"></ore-icon> `s.coerce.*` | <ore-icon name=\"check\" size=\"16\"></ore-icon> | <ore-icon name=\"check\" size=\"16\"></ore-icon> |\n| Async validation | <ore-icon name=\"check\" size=\"16\"></ore-icon> `.checkAsync()` | <ore-icon name=\"check\" size=\"16\"></ore-icon> | <ore-icon name=\"check\" size=\"16\"></ore-icon> |\n| Error flattening | <ore-icon name=\"check\" size=\"16\"></ore-icon> `flatten()` + `flattenFirst()` | <ore-icon name=\"check\" size=\"16\"></ore-icon> | Partial |\n| Zero 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\n<div class=\"decision-callout\">\n\n**Use Spell when** you want a fluent schema API with strong TypeScript inference, structured errors, and no third-party runtime dependencies.\n\n**Consider alternatives when** you are already standardized on another validator ecosystem and migration cost outweighs the API benefits.\n\n</div>\n\n## Installation\n\nUse your workspace package manager to add Spell.\n\n::: code-group\n\n```sh [pnpm]\npnpm add @vielzeug/spell\n```\n\n```sh [npm]\nnpm install @vielzeug/spell\n```\n\n```sh [yarn]\nyarn add @vielzeug/spell\n```\n\n:::\n\n## Quick Start\n\nStart with a schema, then parse unknown input and use the inferred output type everywhere else.\n\n```ts\nimport { s, type Infer } from '@vielzeug/spell';\n\nconst User = s\n .object({\n email: s.string().email(),\n name: s.string().min(1),\n role: s.enum(['admin', 'editor', 'viewer'] as const),\n })\n .relaxed(); // allow extra keys — omit for strict-mode (default)\n\ntype User = Infer<typeof User>;\n\nconst payload: unknown = {\n email: 'ada@example.com',\n name: 'Ada',\n role: 'admin',\n team: 'platform',\n};\n\nconst result = User.safeParse(payload);\n\nif (!result.success) throw result.error;\nconst user = result.data;\n```\n\n## Features\n\n<div class=\"features-grid\">\n\n- Namespace and tree-shakeable schema builders.\n- Sync and async parsing with `parse()`, `safeParse()`, `parseAsync()`, and `safeParseAsync()`.\n- Dependency-free Standard Schema v1 interoperability through `schema['~standard']`.\n- Explicit `check()` and `checkAsync()` rules; sync parsing never skips an async check.\n- Wrapper modes for `optional`, `nullable`, `nullish`, `default`, `catch`, and `required`.\n- Frozen declarative definitions through `definition()` and JSON Schema export via `fromDefinition()` from `@vielzeug/spell/json`.\n- Request-local message customization through `createParseContext()`.\n- Ordered union parsing produces the same selected branch in sync and async modes.\n- Structured errors with direct path lookup and flattened views.\n- Object parsing is hardened against prototype-pollution-style keys.\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- [Forge](/forge/) — typed form state that uses Spell schemas as its validation layer\n- [Courier](/courier/) — HTTP client for validating request and response payloads at service boundaries\n- [Vault](/vault/) — unified storage API that accepts Spell schemas to type-gate persisted data\n\n</div>\n\n<!-- markdownlint-enable MD025 MD033 MD060 -->\n",
5
+ "api": "---\ntitle: Spell — API Reference\ndescription: Reference for Spell schema builders, parsing, errors, context, and tooling exports.\n---\n\n[[toc]]\n\n## API Overview\n\n| Symbol | Purpose | Execution mode | Common gotcha |\n| ------------------------ | ------------------------------- | ---------------------------------- | ------------------------------------------ |\n| `s` | Creates schemas | Sync or async, depending on checks | `checkAsync()` requires async parsing |\n| `createParseContext()` | Creates request-local messages | Sync | Pass the context to each parse call |\n| `SpellValidationError` | Provides validation issues | Sync/async parse failures | Use `safeParse()` for expected invalid data |\n\n## Package Entry Point\n\n| Import | Purpose |\n| ---------------------------- | ----------------------------------------------- |\n| `@vielzeug/spell` | Schema builders, errors, types, and parse contexts |\n| `@vielzeug/spell/json` | Convert portable definitions to JSON Schema |\n| `@vielzeug/spell/predicates` | Standalone format and type predicates |\n\n```ts\nimport { createParseContext, s, type Infer } from '@vielzeug/spell';\nimport { fromDefinition } from '@vielzeug/spell/json';\nimport { isEmail } from '@vielzeug/spell/predicates';\n```\n\n## `s`\n\nAll builders live under `s`.\n\n| Builder | Purpose |\n| ----------------------------------------------------------------- | -------------------------- |\n| `string`, `number`, `boolean`, `bigint`, `date` | Primitive values |\n| `literal`, `enum`, `null`, `undefined`, `unknown`, `never` | Exact and universal values |\n| `array`, `tuple`, `set`, `map`, `record`, `object` | Collections |\n| `union`, `intersect`, `discriminatedUnion`, `lazy` | Composition |\n| `coerce.*` | Coercing primitive schemas |\n\n```ts\nconst User = s.object({\n email: s.string().email(),\n id: s.string().uuid(),\n role: s.enum(['admin', 'member'] as const),\n});\n\ntype User = Infer<typeof User>;\n```\n\nObject schemas reject unknown keys. Use `.relaxed()` to retain extras.\n\n## Standard Schema interoperability\n\nEvery schema implements the dependency-free Standard Schema v1 structural protocol through `schema['~standard']`. Consumers that accept Standard Schema validators can use Spell schemas directly without an adapter.\n\n```ts\nconst User = s.object({ name: s.string() });\nconst result = await User['~standard'].validate({ name: 'Ada' });\n```\n\nSuccessful results contain `value`; failures contain portable `{ message, path }` issues. Failed unions remain one stable union issue instead of selecting a branch heuristically.\n\nThe exported `StandardSchemaV1` interface mirrors the full v1 protocol, including `validate(value, options?)` and the `InferInput` and `InferOutput` namespace helpers.\n\n## Parsing\n\nEvery schema provides:\n\n```ts\nschema.parse(value, context?); // Output or SpellValidationError\nschema.safeParse(value, context?); // ParseResult<Output>\nschema.parseAsync(value, context?); // Promise<Output>\nschema.safeParseAsync(value, context?); // Promise<ParseResult<Output>>\nschema.is(value); // value is Output\nschema.assert(value, label?); // assertion\n```\n\n`parse()`, `safeParse()`, `is()`, and `assert()` are available on synchronous schemas. Calling `checkAsync()` returns an async-only schema, where TypeScript exposes only `parseAsync()` and `safeParseAsync()`. That async-only mode propagates through compositional schemas when a child is asynchronous.\n\n## Custom Checks\n\n`check()` is synchronous. `checkAsync()` is asynchronous. Do not return a Promise from `check()`.\n\n```ts\nconst Signup = s.object({ confirm: s.string(), password: s.string() }).check((value, context) => {\n if (value.password !== value.confirm) {\n context.addIssue({ code: 'custom', message: 'Passwords must match', path: ['confirm'] });\n }\n});\n\nconst AvailableEmail = s\n .string()\n .email()\n .checkAsync(async (value) => {\n return (await emailAvailable(value)) || 'Email is already registered';\n });\n```\n\n`CheckContext.addIssue()` takes `{ code, message, params?, path? }`. Paths are relative to current schema.\n\n## Modifiers and Transforms\n\n```ts\ns.string().optional();\ns.string().nullable();\ns.string().nullish();\ns.string().required();\ns.string().default('guest');\ns.string().catch('guest');\ns.string()\n .trim()\n .transform((value) => value.toLowerCase());\ns.string().pipe(s.string().slug());\ns.string().label('User name');\n```\n\n`default()`, `catch()`, preprocessors, transforms, and checks are runtime behavior. They cannot become portable definitions.\n\n## Definitions and JSON Schema\n\n`definition()` is only for schemas containing declarative structure. It returns frozen data and throws `SpellDefinitionError` when runtime behavior is present.\n\n```ts\nimport { s } from '@vielzeug/spell';\nimport { fromDefinition } from '@vielzeug/spell/json';\n\nconst Product = s.object({\n id: s.string().uuid(),\n name: s.string().min(1),\n});\n\nconst definition = Product.definition();\nconst jsonSchema = fromDefinition(definition);\n```\n\nNo implicit schema-to-JSON conversion exists. Make definition boundary explicit.\n\n## Parse Contexts\n\n`createParseContext()` creates immutable request-local validation messages.\n\n```ts\nfunction createParseContext(messages?: DeepPartial<Messages>): ParseContext;\n```\n\n**Returns:** A parse context with built-in messages merged with the supplied overrides.\n\n```ts\nimport { createParseContext, s } from '@vielzeug/spell';\n\nconst context = createParseContext({\n object: { invalidKeys: () => 'Unsupported field' },\n});\n\nconst result = s.object({ email: s.string().email() }).safeParse({ email: 'ada@example.com', extra: true }, context);\n\nif (!result.success) {\n const messages = result.error.messagesAt('email');\n console.log(messages);\n}\n```\n\n## Types\n\n### Inference types\n\n| Type | Resolves to |\n| --- | --- |\n| `Infer<TSchema>` | Parsed output; alias of `InferOutput<TSchema>` |\n| `InferInput<TSchema>` | Input accepted before parsing or transformation |\n| `InferOutput<TSchema>` | Value returned after parsing and transformation |\n\nComposite schemas preserve child input types. Object input types make fields optional when their field schema accepts `undefined`.\n\n### Standard Schema\n\n```ts\ninterface StandardSchemaV1<Input = unknown, Output = Input> {\n readonly '~standard': StandardSchemaV1.Props<Input, Output>;\n}\n\nnamespace StandardSchemaV1 {\n interface Props<Input = unknown, Output = Input> {\n readonly version: 1;\n readonly vendor: string;\n readonly validate: (value: unknown, options?: Options) => Result<Output> | Promise<Result<Output>>;\n readonly types?: Types<Input, Output>;\n }\n\n type Result<Output> = { readonly value: Output; readonly issues?: undefined } | { readonly issues: readonly Issue[] };\n interface Options { readonly libraryOptions?: Record<string, unknown> }\n interface Issue { readonly message: string; readonly path?: readonly (PropertyKey | PathSegment)[] }\n interface PathSegment { readonly key: PropertyKey }\n interface Types<Input = unknown, Output = Input> { readonly input: Input; readonly output: Output }\n type InferInput<Schema extends StandardSchemaV1> = NonNullable<Schema['~standard']['types']>['input'];\n type InferOutput<Schema extends StandardSchemaV1> = NonNullable<Schema['~standard']['types']>['output'];\n}\n```\n\n### Parse result and issues\n\n```ts\ntype ParseResult<T> = { data: T; success: true } | { error: SpellValidationError; success: false };\n\ntype Issue =\n | { code: 'custom'; message: string; params?: Record<string, unknown>; path: (string | number)[] }\n | { code: 'invalid_base64'; message: string; params: { format: string }; path: (string | number)[] }\n | { code: 'invalid_date'; message: string; params?: undefined; path: (string | number)[] }\n | { code: 'invalid_duration'; message: string; params: { format: string }; path: (string | number)[] }\n | { code: 'invalid_enum'; message: string; params: { values: readonly unknown[] }; path: (string | number)[] }\n | { code: 'invalid_finite'; message: string; params?: undefined; path: (string | number)[] }\n | { code: 'invalid_integer'; message: string; params?: undefined; path: (string | number)[] }\n | { code: 'invalid_keys'; message: string; params: { keys: string[] }; path: (string | number)[] }\n | { code: 'invalid_length'; message: string; params: { exact: number }; path: (string | number)[] }\n | { code: 'invalid_literal'; message: string; params: { expected: unknown }; path: (string | number)[] }\n | { code: 'invalid_multiple_of'; message: string; params: { step: number | bigint }; path: (string | number)[] }\n | { code: 'invalid_safe'; message: string; params?: undefined; path: (string | number)[] }\n | {\n code: 'invalid_string';\n message: string;\n params: { format?: string; includes?: string; pattern?: string; prefix?: string; suffix?: string };\n path: (string | number)[];\n }\n | { code: 'invalid_type'; message: string; params?: undefined; path: (string | number)[] }\n | { code: 'invalid_union'; message: string; params: { errors: Issue[][] }; path: (string | number)[] }\n | { code: 'invalid_unique'; message: string; params: { unique: true }; path: (string | number)[] }\n | { code: 'invalid_url'; message: string; params: { format: string }; path: (string | number)[] }\n | {\n code: 'invalid_variant';\n message: string;\n params: { discriminator: string; expected: string[] };\n path: (string | number)[];\n }\n | {\n code: 'too_big';\n message: string;\n params: { exclusive?: boolean; max: number | bigint | Date };\n path: (string | number)[];\n }\n | {\n code: 'too_small';\n message: string;\n params: { exclusive?: boolean; min: number | bigint | Date };\n path: (string | number)[];\n }\n | { code: string & {}; message: string; params?: Record<string, unknown>; path: (string | number)[] };\n```\n\n`ErrorCode` is a const object mapping each issue code to its string literal.\n\n### Validation contracts\n\n```ts\ntype ParseContext = { messages: Messages };\n\ntype CheckContext = {\n addIssue: (issue: {\n code: string;\n message: string;\n params?: Record<string, unknown>;\n path?: (string | number)[];\n }) => void;\n};\n\ntype ValidateResult = boolean | null | undefined | string;\n```\n\n### Messages\n\n```ts\ntype MessageFn<Ctx extends Record<string, unknown> = Record<string, unknown>> = string | ((ctx: Ctx) => string);\n\ntype Messages = {\n array: { length: (ctx: { exact: number; value: unknown[] }) => string; max: (ctx: { max: number; value: unknown[] }) => string; min: (ctx: { min: number; value: unknown[] }) => string; nonEmpty: () => string; type: () => string; unique: () => string };\n bigint: { max: (ctx: { max: bigint; value: bigint }) => string; min: (ctx: { min: bigint; value: bigint }) => string; multipleOf: (ctx: { step: bigint; value: bigint }) => string; negative: () => string; nonNegative: () => string; nonPositive: () => string; positive: () => string; type: () => string };\n boolean: { type: () => string };\n check: { default: () => string };\n date: { max: (ctx: { max: Date; value: Date }) => string; min: (ctx: { min: Date; value: Date }) => string; type: () => string };\n enum: { invalid: (ctx: { values: readonly unknown[] }) => string };\n instanceof: { type: (ctx: { className: string }) => string };\n literal: { expected: (ctx: { expected: unknown }) => string };\n map: { max: (ctx: { max: number; value: Map<unknown, unknown> }) => string; min: (ctx: { min: number; value: Map<unknown, unknown> }) => string; nonEmpty: () => string; size: (ctx: { exact: number; value: Map<unknown, unknown> }) => string; type: () => string };\n never: { invalid: () => string };\n number: { finite: () => string; int: () => string; max: (ctx: { max: number; value: number }) => string; min: (ctx: { min: number; value: number }) => string; multipleOf: (ctx: { step: number; value: number }) => string; negative: () => string; nonNegative: () => string; nonPositive: () => string; positive: () => string; safe: () => string; type: () => string };\n object: { invalidKeys: (ctx: { keys: string[] }) => string; type: () => string };\n set: { max: (ctx: { max: number; value: Set<unknown> }) => string; min: (ctx: { min: number; value: Set<unknown> }) => string; nonEmpty: () => string; size: (ctx: { exact: number; value: Set<unknown> }) => string; type: () => string };\n string: { base64: () => string; base64url: () => string; cuid: () => string; cuid2: () => string; date: () => string; dateTime: () => string; duration: () => string; email: () => string; emoji: () => string; endsWith: (ctx: { suffix: string; value: string }) => string; hex: () => string; hexColor: () => string; includes: (ctx: { substr: string; value: string }) => string; ip: () => string; jwt: () => string; length: (ctx: { exact: number; value: string }) => string; max: (ctx: { max: number; value: string }) => string; min: (ctx: { min: number; value: string }) => string; nanoid: () => string; nonEmpty: () => string; numeric: () => string; regex: (ctx: { value: string }) => string; semver: () => string; slug: () => string; startsWith: (ctx: { prefix: string; value: string }) => string; time: () => string; type: () => string; ulid: () => string; url: () => string; uuid: () => string };\n tuple: { length: (ctx: { exact: number }) => string; min: (ctx: { min: number }) => string; type: () => string };\n union: { invalid: () => string };\n variant: { invalidDiscriminator: (ctx: { discriminator: string; expected: string[] }) => string; type: () => string };\n};\n\ntype DeepPartial<T> = {\n [K in keyof T]?: T[K] extends Record<string, unknown> ? DeepPartial<T[K]> : T[K];\n};\n```\n\n### Descriptor and JSON Schema\n\n```ts\ntype SchemaDescriptor = BaseDescriptor &\n (\n | { kind: 'unknown' | 'never' | 'boolean' | 'bigint' | 'date' | 'lazy' }\n | { className: string; kind: 'instanceof' }\n | { contentEncoding?: string; format?: string; kind: 'string'; maxLength?: number; minLength?: number; pattern?: string | null }\n | { exclusiveMaximum?: number; exclusiveMinimum?: number; kind: 'number'; maximum?: number; minimum?: number; multipleOf?: number; typeHint?: 'integer' }\n | { kind: 'literal'; value: string | number | boolean | null | undefined }\n | { kind: 'enum'; values: readonly (string | number)[] }\n | { items: SchemaDescriptor; kind: 'array'; maxItems?: number; minItems?: number }\n | { items: SchemaDescriptor[]; kind: 'tuple'; rest: SchemaDescriptor | null }\n | { fields: Record<string, SchemaDescriptor>; kind: 'object'; strict: boolean }\n | { key: SchemaDescriptor; kind: 'record'; value: SchemaDescriptor }\n | { items: SchemaDescriptor; kind: 'set' }\n | { key: SchemaDescriptor; kind: 'map'; value: SchemaDescriptor }\n | { branches: SchemaDescriptor[]; kind: 'union' | 'intersect' }\n | { branches: Record<string, SchemaDescriptor>; discriminator: string; kind: 'variant' }\n | { from: SchemaDescriptor; kind: 'pipe'; to: SchemaDescriptor }\n );\n\ntype JsonSchema = Record<string, unknown>;\n```\n\n### Schema traversal\n\n`SchemaWalker<R>` is the visitor type accepted by `schema.walk()`. Handlers are optional and correspond to schema kinds; composite handlers also receive their walked children. Use `unknown` as the fallback handler.\n\n### Error helpers\n\n```ts\ntype FlatError = { messages: string[]; path: (string | number)[] };\ntype FlatErrorFirst = { message: string; path: (string | number)[] };\n```\n\n## Errors\n\n- `SpellError` — base class. Use `instanceof SpellError` for cross-boundary narrowing.\n- `SpellValidationError` — validation failure with `issues`, `messagesAt()`, `flatten()`, and `flattenFirst()`.\n- `SpellDefinitionError` — schema cannot create portable definition.\n\n```ts\nconst result = s.object({ email: s.string().email() }).safeParse({ email: 'invalid' });\n\nif (!result.success) {\n const { fieldErrors, formErrors } = result.error.flatten();\n console.log(fieldErrors, formErrors);\n}\n```\n",
6
+ "usage": "---\ntitle: Spell — Usage Guide\ndescription: Learn how to build schemas, compose wrappers, customize locales, and integrate spell with other Vielzeug packages.\n---\n\n[[toc]]\n\n## Basic Usage\n\nStart with `safeParse()` when you want explicit success and failure branches.\n\n```ts\nimport { s } from '@vielzeug/spell';\n\nconst Signup = s.object({\n email: s.string().email(),\n password: s.string().min(12),\n referralCode: s.string().optional(),\n});\n\nconst result = Signup.safeParse({\n email: 'ada@example.com',\n password: 'horse-battery-staple',\n});\n\nif (!result.success) {\n console.error(result.error.issues);\n} else {\n console.log(result.data.email);\n}\n```\n\nUse `parse()` when invalid input should throw immediately. Use `safeParse()` when invalid input is part of normal control flow.\n\n## Building Schemas\n\nUse the namespace form when readability matters more than bundle trimming.\n\n```ts\nimport { s } from '@vielzeug/spell';\n\nconst Article = s.object({\n id: s.string().uuid(),\n title: s.string().trim().min(1).max(120),\n slug: s.string().slug(),\n tags: s.array(s.string().min(1)).default(() => []),\n meta: s\n .object({\n published: s.boolean(),\n publishedAt: s.date().nullable(),\n })\n .relaxed(),\n});\n```\n\n```ts\nimport { s } from '@vielzeug/spell';\n\nconst Todo = s.object({\n done: s.boolean(),\n tags: s.array(s.string().min(1)).default(() => []),\n title: s.string().min(1),\n});\n```\n\nObject schemas reject unknown keys by default. Call `.relaxed()` when you need to preserve extra properties.\n\nCall `.defaults()` to get a fully default-filled object without providing any input. Every required field must have a `.default()` set, or a `SpellValidationError` is thrown. Call `.partialDefaults()` when only some fields have defaults — fields without a default are silently omitted instead of throwing.\n\n```ts\nconst Config = s.object({\n host: s.string().default('localhost'),\n port: s.number().default(3000),\n});\n\nConfig.defaults(); // { host: 'localhost', port: 3000 }\n\nconst Form = s.object({ name: s.string(), role: s.string().default('viewer') });\nForm.partialDefaults(); // { role: 'viewer' }\n```\n\n## Wrapper Modes, Defaults, and Fallbacks\n\nChain wrappers to describe missing values and recovery rules without losing schema metadata.\n\n```ts\nimport { s } from '@vielzeug/spell';\n\nconst DisplayName = s.string().trim().min(2).label('Display name').optional().default('Guest').nullable();\n\nDisplayName.parse(undefined); // 'Guest'\nDisplayName.parse(null); // null\nDisplayName.description; // 'Display name'\n```\n\nCall `.required()` to remove `undefined` without removing `null`.\n\n```ts\nimport { s } from '@vielzeug/spell';\n\nconst NullableButRequired = s.string().optional().nullable().required();\n\nNullableButRequired.parse('Ada');\nNullableButRequired.parse(null);\n// NullableButRequired.parse(undefined); // throws\n```\n\nUse `.catch()` when you want a fallback output after validation fails.\n\n```ts\nimport { s } from '@vielzeug/spell';\n\nconst Port = s.number().int().min(1).max(65535).catch(3000);\n\nPort.parse('not-a-number'); // 3000\n```\n\n## Custom Validation\n\nUse `check()` for synchronous domain rules and `checkAsync()` for asynchronous rules. Sync parsing rejects schemas with asynchronous checks.\n\n```ts\nimport { s } from '@vielzeug/spell';\n\n// Boolean shorthand: return false to fail with default message\nconst EvenNumber = s.number().check((n) => n % 2 === 0);\n\n// String shorthand: return the message as a string\nconst Username = s\n .string()\n .min(3)\n .check((v) => !v.startsWith('_') || 'Cannot start with underscore');\n\n// Multiple issues via ctx.addIssue()\nconst Signup = s.object({ confirm: s.string(), password: s.string() }).check((v, ctx) => {\n if (v.password !== v.confirm) {\n ctx.addIssue({ code: 'custom', message: 'Passwords must match', path: ['confirm'] });\n }\n});\n```\n\n`checkAsync()` returns an async-only schema: TypeScript exposes `parseAsync()` and `safeParseAsync()` but not `parse()` or `safeParse()`. This mode survives fluent modifiers and propagates through nested arrays, objects, unions, intersections, tuples, maps, records, sets, lazy schemas, pipelines, and `s.discriminatedUnion(...)` branches. Sync parsing also fails at runtime instead of accepting an unchecked value.\n\n```ts\nimport { s } from '@vielzeug/spell';\n\nconst takenEmails = new Set(['ada@example.com']);\n\nconst AccountEmail = s\n .string()\n .email()\n .checkAsync(async (value, ctx) => {\n if (takenEmails.has(value)) {\n ctx.addIssue({ code: 'custom', message: 'Email is already taken', path: [] });\n }\n });\n\n// Async checks require parseAsync\nawait AccountEmail.parseAsync('grace@example.com');\n```\n\nUse `check()` for predicate-only rules too. Return `true` on success or message on failure.\n\n```ts\nimport { s } from '@vielzeug/spell';\n\nconst PositivePrice = s.number().check((value) => value > 0 || 'Must be positive');\nPositivePrice.parse(9.99);\n```\n\n## Strings, Numbers, and Safe Regex Usage\n\nUse schema helpers for common string and number constraints instead of hand-written predicates.\n\n```ts\nimport { s } from '@vielzeug/spell';\n\nconst Password = s.string().min(12).regex(/[A-Z]/).regex(/[0-9]/);\nconst Price = s.number().nonNegative().multipleOf(0.01);\nconst LaunchWindow = s.date().min(new Date('2025-01-01T00:00:00.000Z'));\n```\n\nSpell strips stateful `/g` and `/y` flags from `regex()` patterns before validation. Repeated parses stay deterministic even when the original regular expression is reused.\n\n## Coercion and Transforms\n\nUse coercion when input arrives as strings, query parameters, or form values.\n\n```ts\nimport { s } from '@vielzeug/spell';\n\nconst Query = s.object({\n draft: s.coerce.boolean().default(false),\n limit: s.coerce.number().int().positive().default(20),\n publishedAt: s.coerce.date().nullable(),\n search: s.coerce.string().trim().min(1).optional(),\n});\n\nconst parsed = Query.parse({\n draft: 'true',\n limit: '50',\n publishedAt: '2025-04-01T12:00:00.000Z',\n search: ' vielzeug ',\n});\n```\n\nUse `transform()` or `pipe()` after validation when downstream code needs a different output shape.\n\n```ts\nimport { s } from '@vielzeug/spell';\n\nconst TrimmedTags = s.array(s.string().trim().min(1)).transform((tags) => tags.map((tag) => tag.toLowerCase()));\nconst Slug = s.string().trim().min(1).pipe(s.string().slug());\n```\n\n## Introspection, Round-Trips, and JSON Schema\n\nUse declarative definitions when schemas need to cross process boundaries or feed tooling.\n\n```ts\nimport { s } from '@vielzeug/spell';\nimport { fromDefinition } from '@vielzeug/spell/json';\n\nconst Product = s\n .object({\n id: s.string().uuid(),\n name: s.string().min(1),\n price: s.number().positive().multipleOf(0.01),\n })\n .label('Product');\n\nconst definition = Product.definition();\nconst jsonSchema = fromDefinition(definition);\n\nProduct.parse({ id: '550e8400-e29b-41d4-a716-446655440000', name: 'Keyboard', price: 129.99 });\nconsole.log(jsonSchema.title);\n```\n\nDefinitions are frozen serializable snapshots of declarative schema structure. Use `definition()` and `fromDefinition()` for external tooling. Schemas with runtime checks, transforms, defaults, catches, or preprocessors intentionally have no definition.\n\n## Messages\n\nSpell has no mutable process-wide configuration. Build one parse context per request, locale, or form, then pass it explicitly.\n\n```ts\nimport { createParseContext, s } from '@vielzeug/spell';\n\nconst User = s.object({ email: s.string().email() });\nconst german = createParseContext({\n object: { invalidKeys: () => 'Keine unbekannten Felder erlaubt' },\n});\n\nUser.safeParse({ email: 'ada@example.com', extra: true }, german);\n```\n\nInternal development warnings always use `console.warn` in development builds. Route application diagnostics in application code instead of mutating library-wide logger state.\n\n## Working with Validation Errors\n\nUse `SpellValidationError` helpers when you need UI-ready error structures.\n\n```ts\nimport { s, SpellValidationError } from '@vielzeug/spell';\n\nconst User = s.object({\n email: s.string().email(),\n profile: s.object({\n name: s.string().min(2),\n }),\n});\n\nconst result = User.safeParse({ email: 'nope', profile: { name: '' } });\n\nif (!result.success && result.error instanceof SpellValidationError) {\n const profileErrors = result.error.messagesAt('profile', 'name');\n console.log(profileErrors);\n}\n```\n\nUnion failures produce one stable `invalid_union` issue. Use `s.discriminatedUnion()` when a discriminator can identify the intended object branch and provide field-specific errors.\n\n## Schema Traversal with walk()\n\nUse `walk()` to inspect or transform a schema tree without importing internal implementation classes.\n\n```ts\nimport { s, type SchemaWalker } from '@vielzeug/spell';\n\nconst fields: string[] = [];\n\nconst collectFields: SchemaWalker<void> = {\n object(schema) {\n for (const [key, child] of Object.entries(schema.shape)) {\n fields.push(key);\n child.walk(collectFields);\n }\n },\n unknown() {},\n};\n\nconst User = s.object({\n email: s.string().email(),\n profile: s.object({ name: s.string() }),\n});\n\nUser.walk(collectFields);\nconsole.log(fields); // ['email', 'profile', 'name']\n```\n\n`walk()` dispatches by `schema.kind`. If no handler matches and no `unknown` fallback is provided, `walk()` returns `null`. Add an `unknown` handler to capture any kind not explicitly listed in your visitor.\n\n## Framework Integration\n\nSpell works anywhere you can call a function before state enters your app.\n\n::: code-group\n\n```tsx [React]\nimport { s } from '@vielzeug/spell';\n\nconst SearchParams = s\n .object({\n page: s.coerce.number().int().positive().default(1),\n q: s.string().trim().optional(),\n })\n .relaxed();\n\nexport function SearchPage({ rawParams }: { rawParams: unknown }) {\n const params = SearchParams.parse(rawParams);\n\n return (\n <div>\n {params.q ?? 'All results'} — page {params.page}\n </div>\n );\n}\n```\n\n```ts [Vue]\nimport { computed, ref } from 'vue';\nimport { s } from '@vielzeug/spell';\n\nconst Settings = s.object({\n locale: s.string().min(2),\n compact: s.coerce.boolean().default(false),\n});\n\nconst raw = ref<unknown>({ locale: 'en', compact: 'true' });\nconst settings = computed(() => Settings.parse(raw.value));\n```\n\n:::\n\nUse `safeParse()` at event boundaries and `parse()` inside trusted data flows.\n\n## Working with Other Vielzeug Libraries\n\nUse Spell as the validation layer and let other packages focus on transport, forms, or storage.\n\n```ts\nimport { createForm } from '@vielzeug/forge';\nimport { schemaValidator } from '@vielzeug/forge/schema';\nimport { createCourier } from '@vielzeug/courier';\nimport { s } from '@vielzeug/spell';\n\nconst Profile = s.object({\n displayName: s.string().min(2),\n newsletter: s.boolean(),\n});\n\nconst form = createForm({\n initialValues: {\n displayName: '',\n newsletter: false,\n },\n validate: schemaValidator(Profile),\n});\n\nconst courier = createCourier({ baseUrl: '/api' });\nconst profile = Profile.parse(await courier.get('/profile'));\n```\n\nUse Spell definitions with `@vielzeug/codex` or other tooling when you need generated docs or external schema consumers.\n\n## Best Practices\n\n- Keep schemas close to the boundary where unknown data enters your app.\n- Use `s` consistently for construction; use explicit `/json` and `/predicates` subpaths for tooling.\n- Use `.default(() => value)` for mutable defaults such as arrays, objects, `Map`, and `Set`.\n- Call `.required()` when you want to remove `undefined` but keep `null` semantics intact.\n- Use `check()` with a `ctx` argument when you need `ctx.addIssue()`; return a message for simple predicate failures.\n- Use `checkAsync()` and `parseAsync()` for every asynchronous domain rule.\n- Build a parse context per request or test; never rely on mutable process-wide configuration.\n- Use `definition()` with `fromDefinition()` from `@vielzeug/spell/json` for external tooling.\n",
7
+ "examples": "---\ntitle: Spell — Examples\ndescription: Practical examples and recipes for spell.\n---\n\n## Examples\n\n- [Validating API Payloads](./examples/api.md)\n- [Form-Safe Parsing](./examples/forms.md)\n- [Async Business Rules](./examples/async.md)\n- [Schema Introspection and Round-Trips](./examples/introspection.md)\n- [Unions, Intersections, and Variants](./examples/unions.md)\n- [Schema Traversal with walk()](./examples/walk.md)\n"
8
+ },
9
+ "examples": [
10
+ {
11
+ "id": "array-validation",
12
+ "code": "// Validate a product tag list before it hits search filters.\nimport { s } from '@vielzeug/spell'\n\nconst ProductTags = s.array(s.string().trim().min(2)).min(1).max(4).unique()\n\nconsole.log('Valid tags:', ProductTags.safeParse(['ui', 'forms', 'docs']).success)\n\nconst invalid = ProductTags.safeParse(['ui', 'ui', 'x', 'search', 'extra'])\nconsole.log('Invalid tags:', invalid.success)\n\nif (!invalid.success) {\n console.log('Issues:', invalid.error.issues.map((issue) => issue.message))\n}",
13
+ "name": "Array Validation"
14
+ },
15
+ {
16
+ "id": "async-validate",
17
+ "code": "// checkAsync() declares asynchronous domain rules.\n// Use safeParseAsync() or parseAsync() for schemas containing async checks.\nimport { s } from '@vielzeug/spell'\n\n// Simulated async check (e.g. database lookup)\nfunction isUsernameAvailable(name) {\n return new Promise(resolve => setTimeout(() => resolve(name !== 'taken'), 50))\n}\n\nconst UsernameSchema = s.string()\n .min(3)\n .checkAsync(async (name) => {\n const available = await isUsernameAvailable(name)\n return available || 'Username is already taken'\n })\n\n// Async checks require safeParseAsync() or parseAsync()\nconst ok = await UsernameSchema.safeParseAsync('alice')\nconsole.log('alice:', ok.success ? 'available' : ok.error.issues[0].message)\n\nconst fail = await UsernameSchema.safeParseAsync('taken')\nconsole.log('taken:', fail.success ? 'available' : fail.error.issues[0].message)\n\nconst tooShort = await UsernameSchema.safeParseAsync('ab')\nconsole.log('ab:', tooShort.success ? 'available' : tooShort.error.issues[0].message)",
18
+ "name": "Async Validation"
19
+ },
20
+ {
21
+ "id": "basic-parsing",
22
+ "code": "// Schema definition, type inference, and safe parsing\nimport { s } from '@vielzeug/spell'\n\nconst Product = s.object({\n id: s.string().uuid(),\n name: s.string().min(1).max(120),\n price: s.number().positive().multipleOf(0.01),\n tags: s.array(s.string().min(1)).default(() => []),\n})\n\n// Infer the TypeScript type directly from the schema\n// type Product = { id: string; name: string; price: number; tags: string[] }\n\n// parse() throws on failure — use when invalid input is a programmer error\nconst product = Product.parse({\n id: '550e8400-e29b-41d4-a716-446655440000',\n name: 'Mechanical Keyboard',\n price: 129.99,\n})\nconsole.log('Parsed:', product.name, '— tags:', product.tags)\n\n// safeParse() returns a tagged result union — use at untrusted boundaries\nconst bad = Product.safeParse({ id: 'not-a-uuid', name: '', price: -5 })\nif (!bad.success) {\n const paths = bad.error.issues.map(i => i.path.join('.') || 'root')\n console.log('Validation failed at:', paths.join(', '))\n}",
23
+ "name": "Basic Parsing"
24
+ },
25
+ {
26
+ "id": "basic-schema",
27
+ "code": "// Validate a signup payload before it enters application state.\nimport { s } from '@vielzeug/spell'\n\nconst Signup = s.object({\n email: s.string().email(),\n password: s.string().min(12),\n referralCode: s.string().optional(),\n})\n\nconsole.log('Accepted:', Signup.parse({\n email: 'ada@example.com',\n password: 'horse-battery-staple',\n}))\n\nconst invalid = Signup.safeParse({\n email: 'not-an-email',\n password: 'short',\n})\n\nif (!invalid.success) {\n console.log('Email errors:', invalid.error.messagesAt('email'))\n console.log('Password errors:', invalid.error.messagesAt('password'))\n}",
28
+ "name": "Basic Schema Validation"
29
+ },
30
+ {
31
+ "id": "coercion",
32
+ "code": "// Coerce query params into typed search options with safe defaults.\nimport { s } from '@vielzeug/spell'\n\nconst SearchQuery = s.object({\n draft: s.coerce.boolean().default(false),\n limit: s.coerce.number().int().positive().default(20),\n page: s.coerce.number().int().positive().default(1),\n q: s.coerce.string().trim().min(1).optional(),\n})\n\nconst parsed = SearchQuery.parse({\n draft: 'true',\n limit: '50',\n page: '2',\n q: ' vielzeug ',\n})\n\nconsole.log(parsed)\nconsole.log('limit type:', typeof parsed.limit)",
33
+ "name": "Type Coercion"
34
+ },
35
+ {
36
+ "id": "descriptor-roundtrip",
37
+ "code": "import { s } from '@vielzeug/spell'\nimport { fromDefinition } from '@vielzeug/spell/json'\n\nconst Product = s.object({\n id: s.string().uuid(),\n name: s.string().min(1),\n price: s.number().positive(),\n})\n\nconst definition = Product.definition()\nconst jsonSchema = fromDefinition(definition)\n\nconsole.log(definition.kind)\nconsole.log(jsonSchema)",
38
+ "name": "Declarative Definition Export"
39
+ },
40
+ {
41
+ "id": "discriminated-union",
42
+ "code": "// s.discriminatedUnion() validates a discriminated union — objects sharing a common tag field.\n// Spell automatically injects the discriminator literal into each branch.\nimport { s } from '@vielzeug/spell'\n\nconst Event = s.discriminatedUnion('type', {\n click: s.object({ x: s.number(), y: s.number() }),\n keydown: s.object({ key: s.string(), repeat: s.boolean() }),\n resize: s.object({ width: s.number(), height: s.number() }),\n})\n\nconst click = Event.parse({ type: 'click', x: 100, y: 200 })\nconsole.log('click:', click)\n\nconst key = Event.parse({ type: 'keydown', key: 'Enter', repeat: false })\nconsole.log('keydown:', key)\n\n// Wrong discriminator value\nconst bad = Event.safeParse({ type: 'unknown', x: 0 })\nconsole.log('unknown type:', bad.success ? 'ok' : bad.error.issues[0].message)\n\n// Missing required field in matched branch\nconst missingField = Event.safeParse({ type: 'resize', width: 800 })\nconsole.log('missing height:', missingField.success ? 'ok' : missingField.error.issues[0].message)",
43
+ "name": "Discriminated Union"
44
+ },
45
+ {
46
+ "id": "format-validators",
47
+ "code": "import { s } from '@vielzeug/spell'\nimport { isEmail, isUuid } from '@vielzeug/spell/predicates'\n\nconsole.log(isEmail('ada@example.com'))\nconsole.log(isEmail('not-an-email'))\nconsole.log(isUuid('550e8400-e29b-41d4-a716-446655440000'))\nconsole.log(isUuid('short'))\n\nconst UserId = s.string().uuid()\nconsole.log(UserId.safeParse('550e8400-e29b-41d4-a716-446655440000').success)",
48
+ "name": "Format Predicates"
49
+ },
50
+ {
51
+ "id": "messages-override",
52
+ "code": "import { createParseContext, s } from '@vielzeug/spell'\n\nconst context = createParseContext({\n object: { invalidKeys: () => 'Use only supported fields' },\n})\n\nconsole.log(s.object({ email: s.string().email() }).safeParse({ email: 'ada@example.com', extra: true }, context).success)",
53
+ "name": "Request-local messages"
54
+ },
55
+ {
56
+ "id": "nested-objects",
57
+ "code": "// Model an API response with a discriminator instead of a loose union.\nimport { s } from '@vielzeug/spell'\n\nconst SearchResponse = s.discriminatedUnion('status', {\n error: s.object({\n message: s.string().min(1),\n status: s.literal('error'),\n }),\n success: s.object({\n results: s.array(s.object({ id: s.string().uuid(), title: s.string().min(1) })).default(() => []),\n status: s.literal('success'),\n }),\n})\n\nconsole.log('Success branch:', SearchResponse.parse({\n status: 'success',\n results: [{ id: '550e8400-e29b-41d4-a716-446655440000', title: 'Spell docs' }],\n}))\n\nconst invalid = SearchResponse.safeParse({ status: 'success', message: 'no results here' })\nconsole.log('Invalid branch accepted:', invalid.success)",
58
+ "name": "Variant Responses"
59
+ },
60
+ {
61
+ "id": "number-validation",
62
+ "code": "// Enforce money-like numeric constraints for a checkout amount.\nimport { s } from '@vielzeug/spell'\n\nconst CheckoutTotal = s.number().nonNegative().multipleOf(0.01).max(9999)\n\nfor (const value of [129.99, -4, 19.999, 15000]) {\n const result = CheckoutTotal.safeParse(value)\n console.log(value, '=>', result.success ? 'accepted' : result.error.issues[0].message)\n}",
63
+ "name": "Number Validation"
64
+ },
65
+ {
66
+ "id": "object-defaults",
67
+ "code": "import { s } from '@vielzeug/spell';\n\n// Schema where all fields have defaults\nconst ServerConfig = s.object({\n host: s.string().default('localhost'),\n port: s.number().int().positive().default(3000),\n tls: s.boolean().default(false),\n});\n\n// Get a fully filled config without providing any input\nconst config = ServerConfig.defaults();\nconsole.log(config);\n// { host: 'localhost', port: 3000, tls: false }\n\n// Works with nested schemas too\nconst AppConfig = s.object({\n server: ServerConfig,\n debug: s.boolean().default(false),\n});\n\n// Parse with partial input — missing fields use their defaults\nconst parsed = AppConfig.parse({ server: { host: 'prod.example.com', port: 443, tls: true }, debug: true });\nconsole.log(parsed.server.host); // 'prod.example.com'\n\n// Schema with required field (no default) — throws if .defaults() called\nconst Strict = s.object({ name: s.string() });\nconst result = Strict.safeParse({});\nconsole.log(result.success); // false — name is required\n",
68
+ "name": "Object Defaults"
69
+ },
70
+ {
71
+ "id": "object-merge",
72
+ "code": "import { s } from '@vielzeug/spell';\n\n// merge() combines two object schemas (right-hand fields win on conflict)\nconst Base = s.object({\n id: s.string().uuid(),\n createdAt: s.date(),\n});\n\nconst WithMeta = s.object({\n description: s.string().optional(),\n tags: s.array(s.string()).default(() => []),\n});\n\nconst Resource = Base.merge(WithMeta);\n\nconst result = Resource.parse({\n createdAt: new Date('2025-01-01'),\n id: '550e8400-e29b-41d4-a716-446655440000',\n tags: ['api', 'v2'],\n});\nconsole.log(result.tags); // ['api', 'v2']\nconsole.log(result.id); // '550e8400-...'\n\n// merge() inherits the right-hand schema's strict/relaxed mode\nconst Strict = s.object({ a: s.string() });\nconst Relaxed = s.object({ b: s.number() }).relaxed();\n\nconst Merged = Strict.merge(Relaxed);\n// Extra keys are allowed because Relaxed is the right-hand schema\nconsole.log(Merged.safeParse({ a: 'hi', b: 1, extra: true }).success); // true\n\nconst IdOrSlug = s.union(s.string().uuid(), s.string().slug());\nconsole.log(IdOrSlug.safeParse('550e8400-e29b-41d4-a716-446655440000').success); // true\nconsole.log(IdOrSlug.safeParse('my-slug').success); // true\nconsole.log(IdOrSlug.safeParse(42).success); // false\n\nconst NonEmptyString = s.intersect(s.string(), s.string().min(1));\nconsole.log(NonEmptyString.parse('hello')); // 'hello'\n",
73
+ "name": "Object Merge & Aliases"
74
+ },
75
+ {
76
+ "id": "optional-nullable",
77
+ "code": "// Preserve defaults and validators while tightening undefined away with required().\nimport { s } from '@vielzeug/spell'\n\nconst DisplayName = s.string().trim().min(2).optional().default('Guest').nullable()\nconst RequiredDisplayName = DisplayName.required()\n\nconsole.log('default for undefined:', DisplayName.parse(undefined))\nconsole.log('null stays null:', DisplayName.parse(null))\n\nconst short = RequiredDisplayName.safeParse('A')\nconsole.log('short name accepted:', short.success)\n\nconst missing = RequiredDisplayName.safeParse(undefined)\nconsole.log('undefined accepted after required():', missing.success)\n\nconsole.log('null accepted after required():', RequiredDisplayName.parse(null))",
78
+ "name": "Optional and Nullable Fields"
79
+ },
80
+ {
81
+ "id": "refinements",
82
+ "code": "// check() and checkAsync() — explicit custom domain rules\nimport { s } from '@vielzeug/spell'\n\nconst reserved = new Set(['admin', 'root'])\n\n// check() is synchronous; return a string to fail with that message\nconst Username = s.string().min(3).check((value) =>\n !reserved.has(value) || value + ' is reserved'\n)\n\n// check() receives context for multiple issues or custom error codes\nconst Signup = s.object({ password: s.string().min(8), confirm: s.string() })\n .check((v, ctx) => {\n if (v.password !== v.confirm)\n ctx.addIssue({ code: 'custom', message: 'Passwords must match', path: ['confirm'] })\n })\n\n// check() also covers predicate-only domain rules\nconst EvenPort = s.number().int().min(1).max(65535)\n .check((n) => n % 2 === 0 || 'Port must be even')\n\nfor (const name of ['ad', 'admin', 'grace']) {\n const r = Username.safeParse(name)\n console.log(name, '->', r.success ? 'ok' : r.error.issues[0].message)\n}\n\nconst signupResult = Signup.safeParse({ password: 'secure123', confirm: 'different' })\nconsole.log('signup:', signupResult.success ? 'ok' : signupResult.error.issues[0].message)\n\nfor (const port of [8080, 3001, 443]) {\n const r = EvenPort.safeParse(port)\n console.log('port', port, '->', r.success ? 'ok' : r.error.issues[0].message)\n}",
83
+ "name": "Custom Validation"
84
+ },
85
+ {
86
+ "id": "schema-walk",
87
+ "code": "// Traverse a schema tree with walk() to extract field metadata.\nimport { s } from '@vielzeug/spell'\n\nconst Order = s.object({\n id: s.string().uuid(),\n amount: s.number().positive(),\n customer: s.object({\n email: s.string().email(),\n name: s.string().min(1),\n }),\n tags: s.array(s.string()).optional(),\n})\n\n// Collect every field name and whether it is optional.\nconst fields: { name: string; required: boolean }[] = []\n\nOrder.walk({\n object(node) {\n for (const [key, child] of Object.entries(node.shape)) {\n fields.push({ name: key, required: !child.isOptional })\n child.walk(this)\n }\n },\n // unknown() catches any kind without a handler; omitting it returns null instead of throwing\n unknown() {},\n})\n\nconsole.log('Fields:')\nfields.forEach(f => console.log(' ', f.name, f.required ? '(required)' : '(optional)'))\nconsole.log('Total:', fields.length)",
88
+ "name": "Schema Traversal"
89
+ },
90
+ {
91
+ "id": "string-validation",
92
+ "code": "// Reuse one stateful regex safely across repeated parses in the browser REPL.\nimport { s } from '@vielzeug/spell'\n\nconst HexColor = s.string().regex(/#[0-9a-f]{6}/gy)\n\nfor (const value of ['#ff8800', '#ff8800', 'oops']) {\n const result = HexColor.safeParse(value)\n console.log(value, '=>', result.success)\n}",
93
+ "name": "String Validation"
94
+ },
95
+ {
96
+ "id": "wrappers-and-defaults",
97
+ "code": "// optional(), nullable(), default(), catch() — missing-value semantics\nimport { s } from '@vielzeug/spell'\n\n// optional: accepts undefined, passes through validation otherwise\nconst Nickname = s.string().min(2).optional().default('Guest')\n\nconsole.log(Nickname.parse(undefined)) // 'Guest'\nconsole.log(Nickname.parse('Ada')) // 'Ada'\n\n// nullable: accepts null explicitly\nconst Bio = s.string().max(200).nullable()\n\nconsole.log(Bio.parse(null)) // null\nconsole.log(Bio.parse('Loves types')) // 'Loves types'\n\n// nullish: accepts both null and undefined\nconst Avatar = s.string().url().nullish()\n\nconsole.log(Avatar.parse(null)) // null\nconsole.log(Avatar.parse(undefined)) // undefined\n\n// required(): strips undefined without removing null\nconst NullableButRequired = s.string().optional().nullable().required()\nconsole.log(NullableButRequired.parse(null)) // null\nconsole.log(NullableButRequired.safeParse(undefined).success) // false\n\n// catch(): returns a fallback when validation fails — never throws\nconst Port = s.number().int().min(1).max(65535).catch(3000)\nconsole.log(Port.parse(8080)) // 8080\nconsole.log(Port.parse('not-a-port')) // 3000",
98
+ "name": "Wrappers & Defaults"
99
+ }
100
+ ],
101
+ "typeSignatures": {
102
+ "AnySchema": "export type {\n AnySchema,\n CheckContext,\n FlatError,\n FlatErrorFirst,\n Infer,\n InferInput,\n InferOutput,\n Issue,\n JsonSchema,\n MessageFn,\n Messages,\n ParseContext,\n ParseResult,\n SchemaDescriptor,\n SchemaWalker,\n StandardSchemaV1,\n ValidateResult,\n} from './core';",
103
+ "CheckContext": "export type {\n AnySchema,\n CheckContext,\n FlatError,\n FlatErrorFirst,\n Infer,\n InferInput,\n InferOutput,\n Issue,\n JsonSchema,\n MessageFn,\n Messages,\n ParseContext,\n ParseResult,\n SchemaDescriptor,\n SchemaWalker,\n StandardSchemaV1,\n ValidateResult,\n} from './core';",
104
+ "FlatError": "export type {\n AnySchema,\n CheckContext,\n FlatError,\n FlatErrorFirst,\n Infer,\n InferInput,\n InferOutput,\n Issue,\n JsonSchema,\n MessageFn,\n Messages,\n ParseContext,\n ParseResult,\n SchemaDescriptor,\n SchemaWalker,\n StandardSchemaV1,\n ValidateResult,\n} from './core';",
105
+ "FlatErrorFirst": "export type {\n AnySchema,\n CheckContext,\n FlatError,\n FlatErrorFirst,\n Infer,\n InferInput,\n InferOutput,\n Issue,\n JsonSchema,\n MessageFn,\n Messages,\n ParseContext,\n ParseResult,\n SchemaDescriptor,\n SchemaWalker,\n StandardSchemaV1,\n ValidateResult,\n} from './core';",
106
+ "Infer": "export type {\n AnySchema,\n CheckContext,\n FlatError,\n FlatErrorFirst,\n Infer,\n InferInput,\n InferOutput,\n Issue,\n JsonSchema,\n MessageFn,\n Messages,\n ParseContext,\n ParseResult,\n SchemaDescriptor,\n SchemaWalker,\n StandardSchemaV1,\n ValidateResult,\n} from './core';",
107
+ "InferInput": "export type {\n AnySchema,\n CheckContext,\n FlatError,\n FlatErrorFirst,\n Infer,\n InferInput,\n InferOutput,\n Issue,\n JsonSchema,\n MessageFn,\n Messages,\n ParseContext,\n ParseResult,\n SchemaDescriptor,\n SchemaWalker,\n StandardSchemaV1,\n ValidateResult,\n} from './core';",
108
+ "InferOutput": "export type {\n AnySchema,\n CheckContext,\n FlatError,\n FlatErrorFirst,\n Infer,\n InferInput,\n InferOutput,\n Issue,\n JsonSchema,\n MessageFn,\n Messages,\n ParseContext,\n ParseResult,\n SchemaDescriptor,\n SchemaWalker,\n StandardSchemaV1,\n ValidateResult,\n} from './core';",
109
+ "Issue": "export type {\n AnySchema,\n CheckContext,\n FlatError,\n FlatErrorFirst,\n Infer,\n InferInput,\n InferOutput,\n Issue,\n JsonSchema,\n MessageFn,\n Messages,\n ParseContext,\n ParseResult,\n SchemaDescriptor,\n SchemaWalker,\n StandardSchemaV1,\n ValidateResult,\n} from './core';",
110
+ "JsonSchema": "export type {\n AnySchema,\n CheckContext,\n FlatError,\n FlatErrorFirst,\n Infer,\n InferInput,\n InferOutput,\n Issue,\n JsonSchema,\n MessageFn,\n Messages,\n ParseContext,\n ParseResult,\n SchemaDescriptor,\n SchemaWalker,\n StandardSchemaV1,\n ValidateResult,\n} from './core';",
111
+ "MessageFn": "export type {\n AnySchema,\n CheckContext,\n FlatError,\n FlatErrorFirst,\n Infer,\n InferInput,\n InferOutput,\n Issue,\n JsonSchema,\n MessageFn,\n Messages,\n ParseContext,\n ParseResult,\n SchemaDescriptor,\n SchemaWalker,\n StandardSchemaV1,\n ValidateResult,\n} from './core';",
112
+ "Messages": "export type {\n AnySchema,\n CheckContext,\n FlatError,\n FlatErrorFirst,\n Infer,\n InferInput,\n InferOutput,\n Issue,\n JsonSchema,\n MessageFn,\n Messages,\n ParseContext,\n ParseResult,\n SchemaDescriptor,\n SchemaWalker,\n StandardSchemaV1,\n ValidateResult,\n} from './core';",
113
+ "ParseContext": "export type {\n AnySchema,\n CheckContext,\n FlatError,\n FlatErrorFirst,\n Infer,\n InferInput,\n InferOutput,\n Issue,\n JsonSchema,\n MessageFn,\n Messages,\n ParseContext,\n ParseResult,\n SchemaDescriptor,\n SchemaWalker,\n StandardSchemaV1,\n ValidateResult,\n} from './core';",
114
+ "ParseResult": "export type {\n AnySchema,\n CheckContext,\n FlatError,\n FlatErrorFirst,\n Infer,\n InferInput,\n InferOutput,\n Issue,\n JsonSchema,\n MessageFn,\n Messages,\n ParseContext,\n ParseResult,\n SchemaDescriptor,\n SchemaWalker,\n StandardSchemaV1,\n ValidateResult,\n} from './core';",
115
+ "SchemaDescriptor": "export type {\n AnySchema,\n CheckContext,\n FlatError,\n FlatErrorFirst,\n Infer,\n InferInput,\n InferOutput,\n Issue,\n JsonSchema,\n MessageFn,\n Messages,\n ParseContext,\n ParseResult,\n SchemaDescriptor,\n SchemaWalker,\n StandardSchemaV1,\n ValidateResult,\n} from './core';",
116
+ "SchemaWalker": "export type {\n AnySchema,\n CheckContext,\n FlatError,\n FlatErrorFirst,\n Infer,\n InferInput,\n InferOutput,\n Issue,\n JsonSchema,\n MessageFn,\n Messages,\n ParseContext,\n ParseResult,\n SchemaDescriptor,\n SchemaWalker,\n StandardSchemaV1,\n ValidateResult,\n} from './core';",
117
+ "StandardSchemaV1": "export type {\n AnySchema,\n CheckContext,\n FlatError,\n FlatErrorFirst,\n Infer,\n InferInput,\n InferOutput,\n Issue,\n JsonSchema,\n MessageFn,\n Messages,\n ParseContext,\n ParseResult,\n SchemaDescriptor,\n SchemaWalker,\n StandardSchemaV1,\n ValidateResult,\n} from './core';",
118
+ "ValidateResult": "export type {\n AnySchema,\n CheckContext,\n FlatError,\n FlatErrorFirst,\n Infer,\n InferInput,\n InferOutput,\n Issue,\n JsonSchema,\n MessageFn,\n Messages,\n ParseContext,\n ParseResult,\n SchemaDescriptor,\n SchemaWalker,\n StandardSchemaV1,\n ValidateResult,\n} from './core';",
119
+ "ErrorCode": "export { ErrorCode, Schema, SpellDefinitionError, SpellError, SpellValidationError } from './core';",
120
+ "Schema": "export { ErrorCode, Schema, SpellDefinitionError, SpellError, SpellValidationError } from './core';",
121
+ "SpellDefinitionError": "export { ErrorCode, Schema, SpellDefinitionError, SpellError, SpellValidationError } from './core';",
122
+ "SpellError": "export { ErrorCode, Schema, SpellDefinitionError, SpellError, SpellValidationError } from './core';",
123
+ "SpellValidationError": "export { ErrorCode, Schema, SpellDefinitionError, SpellError, SpellValidationError } from './core';",
124
+ "createParseContext": "export { createParseContext, type DeepPartial } from './messages';",
125
+ "DeepPartial": "export { createParseContext, type DeepPartial } from './messages';",
126
+ "s": "export { s } from './s';"
127
+ }
128
+ }