@vielzeug/codex 26.11.4 → 26.11.6

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 (51) hide show
  1. package/data/catalog.json +47 -45
  2. package/data/llms-full.txt +3271 -2943
  3. package/data/llms.txt +1 -1
  4. package/data/manifest.json +1 -1
  5. package/data/packages/archive.json +3 -3
  6. package/data/packages/arsenal.json +2 -2
  7. package/data/packages/assay.json +4 -3
  8. package/data/packages/beacon.json +3 -3
  9. package/data/packages/clockwork.json +3 -3
  10. package/data/packages/codex.json +3 -3
  11. package/data/packages/coins.json +2 -2
  12. package/data/packages/conduit.json +2 -2
  13. package/data/packages/courier.json +3 -3
  14. package/data/packages/dnd.json +2 -2
  15. package/data/packages/familiar.json +2 -2
  16. package/data/packages/flux.json +3 -3
  17. package/data/packages/focus.json +3 -3
  18. package/data/packages/forge.json +3 -3
  19. package/data/packages/gesture.json +3 -3
  20. package/data/packages/herald.json +3 -3
  21. package/data/packages/illusionist.json +4 -4
  22. package/data/packages/keymap.json +3 -3
  23. package/data/packages/ledger.json +3 -3
  24. package/data/packages/lingua.json +3 -3
  25. package/data/packages/mesh.json +3 -3
  26. package/data/packages/necromancer.json +9 -5
  27. package/data/packages/orbit.json +3 -3
  28. package/data/packages/ore.json +2 -2
  29. package/data/packages/postmaster.json +2 -2
  30. package/data/packages/prism.json +3 -3
  31. package/data/packages/pulse.json +3 -3
  32. package/data/packages/refine.json +2 -2
  33. package/data/packages/ripple.json +2 -2
  34. package/data/packages/rune.json +3 -3
  35. package/data/packages/sandbox.json +2 -2
  36. package/data/packages/scout.json +3 -3
  37. package/data/packages/scroll.json +3 -3
  38. package/data/packages/sentinel.json +2 -2
  39. package/data/packages/sigil.json +3 -3
  40. package/data/packages/sourcerer.json +2 -2
  41. package/data/packages/spell.json +3 -3
  42. package/data/packages/tandem.json +2 -2
  43. package/data/packages/tavern.json +2 -2
  44. package/data/packages/tempo.json +2 -2
  45. package/data/packages/vault.json +4 -4
  46. package/data/packages/ward.json +2 -2
  47. package/data/packages/wayfinder.json +3 -3
  48. package/data/refine.json +4656 -4216
  49. package/data/search.json +118 -118
  50. package/package.json +14 -14
  51. package/skills/vielzeug/SKILL.md +19 -17
@@ -1,9 +1,9 @@
1
1
  {
2
- "apiSource": "export { walkFlatTree } from './dom';\nexport { AssayError, AssayQueryError, AssayTimeoutError } from './errors';\nexport {\n dispatch,\n fireBlur,\n fireChange,\n fireClick,\n fireCustom,\n fireFocus,\n fireInput,\n fireKeyDown,\n fireKeyUp,\n fireSubmit,\n} from './events';\nexport { installFormInternalsPolyfill } from './form-internals';\nexport { getSlotted, type QueryScope, queryAllInShadow, queryInShadow, queryPart, within } from './query';\nexport {\n type DelayOptions,\n delay,\n type EventuallyOptions,\n eventually,\n type WaitOptions,\n waitForEvent,\n waitUntil,\n} from './wait';\n",
2
+ "apiSource": "export { walkFlatTree } from './dom';\nexport { AssayError, AssayQueryError, AssayTimeoutError } from './errors';\nexport {\n dispatch,\n fireBlur,\n fireChange,\n fireClick,\n fireCustom,\n fireFocus,\n fireInput,\n fireKeyDown,\n fireKeyUp,\n fireSubmit,\n} from './events';\nexport { installFormInternalsPolyfill } from './form-internals';\nexport { installPopoverPolyfill } from './popover';\nexport { getSlotted, type QueryScope, queryAllInShadow, queryInShadow, queryPart, within } from './query';\nexport {\n type DelayOptions,\n delay,\n type EventuallyOptions,\n eventually,\n type WaitOptions,\n waitForEvent,\n waitUntil,\n} from './wait';\n",
3
3
  "docs": {
4
4
  "index": "---\ntitle: 'Assay: Framework-agnostic DOM testing primitives'\ndescription: Scoped DOM queries, exact event dispatch, and cancellable async waiting for browser tests.\npackage: assay\ncategory: testing\nkeywords: [testing, dom, events, queries, custom-elements]\nrelated: [ore, refine]\nexports:\n [\n within,\n queryInShadow,\n queryAllInShadow,\n queryPart,\n getSlotted,\n dispatch,\n fireBlur,\n fireChange,\n fireClick,\n fireCustom,\n fireFocus,\n fireInput,\n fireKeyDown,\n fireKeyUp,\n fireSubmit,\n waitUntil,\n eventually,\n waitForEvent,\n delay,\n walkFlatTree,\n installFormInternalsPolyfill,\n AssayError,\n AssayQueryError,\n AssayTimeoutError,\n ]\nenvironments: [browser]\n---\n\n<!-- markdownlint-disable MD025 MD033 MD060 -->\n\n<PackageHero package=\"assay\" />\n\n## Why Assay?\n\nAssay provides focused DOM test primitives that work with vanilla elements, custom elements, and framework-rendered output. It scopes queries, dispatches exact browser event classes, and waits on explicit conditions without imposing a renderer or browser automation stack.\n\n```ts\n// Before\nbutton.dispatchEvent(new MouseEvent('click', { bubbles: true }));\nawait new Promise((resolve) => setTimeout(resolve, 100));\n\n// After\nfireClick(view.get('button.submit'));\nawait waitUntil(() => view.queryByText('Saved') !== null);\n```\n\n| Feature | Assay | Testing Library DOM | Browser automation |\n| ------------------- | -------------------------------------------- | ---------------------------------------- | ---------------------------------------- |\n| Bundle size | <PackageInfo package=\"assay\" type=\"size\" /> | Larger query layer | Browser runtime required |\n| Zero dependencies | <ore-icon name=\"check\" size=\"16\"></ore-icon> | <ore-icon name=\"x\" size=\"16\"></ore-icon> | <ore-icon name=\"x\" size=\"16\"></ore-icon> |\n| Scoped DOM queries | `within()` and shadow helpers | Renderer-oriented queries | Manual selectors |\n| Deterministic waits | `waitUntil()` and `waitForEvent()` | Framework-dependent | Full browser timing |\n\n<div class=\"decision-callout\">\n\n**Use Assay when** a DOM unit test needs readable queries, dispatched events, or a bounded async wait without adopting a rendering framework.\n\n**Consider browser integration tests when** correctness depends on browser default actions, focus behavior, pointer capture, layout, or accessibility-tree behavior.\n\n</div>\n\n## Installation\n\n::: code-group\n\n```sh [pnpm]\npnpm add -D @vielzeug/assay\n```\n\n```sh [npm]\nnpm install -D @vielzeug/assay\n```\n\n```sh [yarn]\nyarn add -D @vielzeug/assay\n```\n\n:::\n\n## Quick Start\n\nScope a test fixture, dispatch an event, and wait for resulting DOM state.\n\n```ts\nimport { fireClick, waitUntil, within } from '@vielzeug/assay';\n\nconst panel = document.createElement('section');\npanel.innerHTML = '<button>Save</button><output></output>';\npanel.querySelector('button')!.addEventListener('click', () => {\n panel.querySelector('output')!.textContent = 'Saved';\n});\n\nconst view = within(panel);\nfireClick(view.get('button'));\nawait waitUntil(() => view.queryByText('Saved') !== null);\n```\n\n## Features\n\n<div class=\"features-grid\">\n\n- `within(root)` scopes nullable and required DOM queries.\n- `queryInShadow`, `queryPart`, and `getSlotted` cross custom-element boundaries explicitly.\n- `fireClick`, `fireInput`, `fireKeyDown`, and peers dispatch exact synchronous events.\n- `waitUntil`, `eventually`, and `waitForEvent` provide bounded, abortable async waiting.\n- `delay` models explicit cancellable timer scheduling.\n- `walkFlatTree` traverses the flat tree across slot boundaries jsdom's light-DOM queries miss.\n- `installFormInternalsPolyfill` gives jsdom the `ElementInternals` form-association API.\n- `AssayError`, `AssayQueryError`, and `AssayTimeoutError` provide typed failures.\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- [Ore](/ore/): component authoring and test fixtures that pair with Assay DOM helpers.\n- [Refine](/refine/): accessible components with component-specific test assertions.\n\n</div>\n\n<!-- markdownlint-enable MD025 MD033 MD060 -->\n",
5
- "api": "---\ntitle: 'Assay: API Reference'\ndescription: API reference for @vielzeug/assay queries, event dispatch, async waiting, and test-environment helpers.\n---\n\n[[toc]]\n\n## API Overview\n\n| Symbol | Purpose | Execution mode | Common gotcha |\n| -------------------------------------------- | ------------------------------------------- | -------------- | ------------------------------------------------ |\n| `within` | Creates scoped query API | Sync | Required `get*` methods throw `AssayQueryError` |\n| `queryInShadow` / `queryPart` / `getSlotted` | Crosses custom-element boundaries | Sync | Open shadow roots are required |\n\n| `fire*` / `dispatch` | Dispatches platform event instances | Sync | Does not reproduce browser default behavior |\n| `waitUntil` / `eventually` / `waitForEvent` | Waits for conditions, assertions, or events | Async | Use a signal or timeout for bounded waits |\n| `delay` | Schedules a cancellable timer | Async | Pass a signal to cancel a pending wait |\n| `walkFlatTree` | Walks the flat tree across slot boundaries | Sync | jsdom-only need: browsers already use the flat tree |\n| `installFormInternalsPolyfill` | Polyfills `ElementInternals` form association for jsdom | Sync | Global patches: only install in suites that need them |\n\n## Package Entry Point\n\n| Import | Purpose |\n| ----------------- | ---------------------------------------------------- |\n| `@vielzeug/assay` | DOM queries, events, wait helpers, test-environment helpers, errors, and types |\n\n## Queries\n\n### `within(root)`\n\nCreates a `QueryScope` for an `Element`, `ShadowRoot`, `Document`, or `DocumentFragment`.\n\n| Method | Returns | Use |\n| --------------------------------- | ----------------- | -------------------------------------------- |\n| `get(selector)` | `Element` | Required CSS match; throws `AssayQueryError` |\n| `query(selector)` | `Element \\| null` | Optional CSS match |\n| `queryAll(selector)` | `Element[]` | All CSS matches |\n| `getByText(text, selector?)` | `Element` | Required exact trimmed-text match |\n| `queryByText(text, selector?)` | `Element \\| null` | Optional exact trimmed-text match |\n| `queryAllByText(text, selector?)` | `Element[]` | All exact trimmed-text matches |\n| `getByTestId(id)` | `Element` | Required `data-testid` match |\n| `queryByTestId(id)` | `Element \\| null` | Optional `data-testid` match |\n| `queryAllByTestId(id)` | `Element[]` | All `data-testid` matches |\n\nText selectors default to `'*'`. Required-query failures include the lookup and a bounded view of the scoped DOM.\n\n### Shadow and slot helpers\n\n| Function | Returns | Description |\n| ---------------------------------- | ----------------- | ---------------------------------------------------- |\n| `queryInShadow(host, selector)` | `Element \\| null` | First match in an open shadow root |\n| `queryAllInShadow(host, selector)` | `Element[]` | All matches in an open shadow root |\n| `queryPart(host, part)` | `Element \\| null` | First shadow element whose `part` token matches |\n| `getSlotted(host, slotName?)` | `Element[]` | Direct light-DOM children in a named or default slot |\n\nThese helpers return `null` or `[]` when there is no shadow root. Dynamic test IDs, parts, and slot names are matched\nas attribute values rather than interpolated into CSS selectors.\n\n## Event dispatch\n\nAll event helpers synchronously return `dispatchEvent()`'s boolean result.\n\n```ts\nimport {\n dispatch,\n fireBlur,\n fireChange,\n fireClick,\n fireCustom,\n fireFocus,\n fireInput,\n fireKeyDown,\n fireKeyUp,\n fireSubmit,\n} from '@vielzeug/assay';\n\nfireClick(button, { clientX: 20 });\nfireInput(input);\nfireKeyDown(input, { key: 'Enter' });\nfireCustom(element, 'item-added', { detail: { id: '42' } });\ndispatch(element, new Event('ready'));\n```\n\n| Function | Event class | Defaults |\n| --------------------------- | --------------- | ------------------------------------------------------ |\n| `fireBlur` / `fireFocus` | `FocusEvent` | Platform defaults (`bubbles: false`) |\n| `fireChange` | `Event` | `bubbles: true` |\n| `fireInput` | `InputEvent` | `bubbles: true` |\n| `fireClick` | `MouseEvent` | `bubbles: true`, `cancelable: true` |\n| `fireKeyDown` / `fireKeyUp` | `KeyboardEvent` | `bubbles: true`, `cancelable: true` |\n| `fireSubmit` | `SubmitEvent` | `bubbles: true`, `cancelable: true` |\n| `fireCustom` | `CustomEvent` | `bubbles: true`, `cancelable: true`, `composed: false` |\n\n`fireCustom(target, type, init?)` dispatches a `CustomEvent` with the given type. Assay intentionally does not\nprovide browser-default or fallback pointer/touch simulation.\n\n## Async waiting\n\n```ts\nawait waitUntil(() => ready, { interval: 20, signal, timeout: 1000 });\nawait eventually(() => expect(spy).toHaveBeenCalled(), { signal, timeout: 1000 });\nawait waitForEvent(target, 'ready', { signal, timeout: 1000 });\nawait delay(100, { signal });\n```\n\n| Function | Success condition | Options |\n| -------------------------------------- | ------------------------ | ------------------------------------------ |\n| `waitUntil(predicate, options?)` | Predicate returns `true` | `timeout`, `interval`, `signal` |\n| `eventually(assertion, options?)` | Assertion stops throwing | `timeout`, `interval`, `signal`, `message` |\n| `waitForEvent(target, type, options?)` | Target emits `type` | `timeout`, `signal` |\n| `delay(ms?, options?)` | Timer elapses | `signal` |\n\n\n`waitUntil()`, `eventually()`, and `waitForEvent()` reject with `AssayTimeoutError` when their shared deadline expires. `waitUntil()` and `eventually()` remain bounded while an asynchronous callback is pending. Abort rejects with the signal reason and removes Assay-owned timers and listeners. Callback work itself must cooperate with application cancellation.\n\n`timeout` and delay milliseconds must be finite, non-negative, and no greater than 2,147,483,647 ms; `interval` must be finite, positive, and within the same timer limit. Invalid durations throw `RangeError`. `eventually()` also accepts `message` for diagnostic timeout context.\n\n## Test-environment helpers\n\n### `walkFlatTree(root, visit)`\n\n```ts\nfunction walkFlatTree(root: Element, visit: (element: HTMLElement) => void): void;\n```\n\nWalks the flat tree below `root`, calling `visit` for each assigned or ordinary descendant (not for `root` itself).\nAt each `<slot>` it expands `assignedElements()` instead of descending into its (always empty, in jsdom) literal\nchildren. jsdom only\nimplements the light DOM tree, so `querySelectorAll()` never crosses a slot into its assigned elements; real\nbrowsers use the flat tree for that. This helper closes the gap for tests that must find form-associated (or\notherwise slotted) elements through shadow boundaries.\n\n```ts\nconst named: HTMLElement[] = [];\n\nwalkFlatTree(form, (el) => {\n if (el.hasAttribute('name')) named.push(el);\n});\n```\n\n### `installFormInternalsPolyfill()`\n\n```ts\nfunction installFormInternalsPolyfill(): () => void;\n```\n\njsdom implements none of the `ElementInternals` form-association API: not `setFormValue`, not `setValidity`, not\nthe `checkValidity`/`reportValidity`/`validity`/`validationMessage` mixin real browsers put on every\n`formAssociated: true` element, not even `FormData` collecting a form-associated element's set value, or\n`<form>.reset()` invoking `formResetCallback()`. Every one of those gaps is exercised by `useField()` from\n`@vielzeug/ore`, so any package testing a form-associated component needs all of them.\n\nCall it from a test setup when the suite tests form-associated components; it returns an `uninstall()` that\nrestores every patched global. The patches are global monkey-patches and skip APIs already implemented by the\nenvironment, so suites without form-associated components shouldn't install them. If `ElementInternals` is absent,\nthe installer is a no-op. A downstream package should\nrely on this instead of hand-rolling its own copy.\n\n```ts\n// vitest.setup.ts\nimport { installFormInternalsPolyfill } from '@vielzeug/assay';\nimport { install } from '@vielzeug/ore/testing';\nimport { afterAll, afterEach } from 'vitest';\n\ninstall(afterEach);\nafterAll(installFormInternalsPolyfill());\n```\n\n## Types\n\n```ts\nexport interface QueryScope {\n get<E extends Element = Element>(selector: string): E;\n getByTestId<E extends Element = Element>(testId: string): E;\n getByText<E extends Element = Element>(text: string, selector?: string): E;\n query<E extends Element = Element>(selector: string): E | null;\n queryAll<E extends Element = Element>(selector: string): E[];\n queryAllByTestId<E extends Element = Element>(testId: string): E[];\n queryAllByText<E extends Element = Element>(text: string, selector?: string): E[];\n queryByTestId<E extends Element = Element>(testId: string): E | null;\n queryByText<E extends Element = Element>(text: string, selector?: string): E | null;\n}\n```\n\nScoped query helpers returned by `within(root)`.\n\n```ts\nexport interface WaitOptions {\n /** Polling interval in ms (default: 50). */\n interval?: number;\n /** Cancel the pending wait. */\n signal?: AbortSignal;\n /** Maximum wait time in ms (default: 1000). */\n timeout?: number;\n}\n\nexport interface EventuallyOptions extends WaitOptions {\n /** Context included in the timeout error. */\n message?: string;\n}\n\nexport interface DelayOptions {\n /** Cancel the pending delay. */\n signal?: AbortSignal;\n}\n```\n\n`WaitOptions` configures `waitUntil()`. `EventuallyOptions` configures `eventually()`. `DelayOptions` configures `delay()`.\n\n## Errors\n\n| Error | Meaning |\n| ------------------- | -------------------------------------- |\n| `AssayError` | Base class for Assay-originated errors |\n| `AssayQueryError` | A required `get*` query had no match |\n| `AssayTimeoutError` | A wait operation reached its timeout |\n\nUse `instanceof AssayError` to narrow any value to the Assay error hierarchy.\n",
6
- "usage": "---\ntitle: 'Assay: Usage Guide'\ndescription: Scoped DOM queries, exact event dispatch, and cancellable waiting with @vielzeug/assay.\n---\n\n[[toc]]\n\n## Basic Usage\n\n`within(root)` is Assay's query API. It accepts an element, document fragment, or shadow root and keeps every lookup\nin that scope. Use `get*` when a match is required and nullable `query*` methods when absence is part of the assertion.\n\n```ts\nimport { within } from '@vielzeug/assay';\n\nconst view = within(panel.shadowRoot!);\n\nconst save = view.get<HTMLButtonElement>('button.save');\nconst status = view.queryByText('Saved');\n\nexpect(view.query('.error')).toBeNull();\nexpect(view.getByTestId('summary').textContent).toContain('Complete');\n```\n\n`get()`, `getByText()`, and `getByTestId()` throw `AssayQueryError` with the lookup and a bounded rendering of the\nscoped DOM. This keeps a failed required lookup diagnosable without non-null assertions.\n\nUse the cross-boundary helpers for custom elements:\n\n```ts\nimport { getSlotted, queryAllInShadow, queryPart } from '@vielzeug/assay';\n\nconst trigger = queryPart(menu, 'trigger');\nconst options = queryAllInShadow(menu, '[role=\"option\"]');\nconst footerActions = getSlotted(dialog, 'footer');\n```\n\n## Exact event dispatch\n\nAssay dispatches the platform event class named by each helper. It does not model browser activation, focus\nmanagement, or form defaults. `fireFocus()` and `fireBlur()` use their non-bubbling platform defaults; use\n`focusin`/`focusout` events when testing delegated focus listeners. Use `element.click()` when native activation is\nthe behavior under test; use Assay when testing an event listener or a controlled state transition.\n\n```ts\nimport { fireClick, fireCustom, fireInput, fireKeyDown } from '@vielzeug/assay';\n\ninput.value = 'Ada';\nfireInput(input);\n\nfireKeyDown(input, { key: 'Enter' });\nfireClick(saveButton);\nfireCustom(panel, 'value-change', { detail: { value: 42 } });\n```\n\nEvery helper returns `dispatchEvent()`'s boolean result. `dispatch(target, event)` is available when an existing\nevent instance is the clearest expression of the test.\n\n## Waiting\n\nChoose the waiting primitive by the test's assertion shape:\n\n```ts\nimport { delay, eventually, waitForEvent, waitUntil } from '@vielzeug/assay';\n\nawait waitUntil(() => panel.querySelector('.status')?.textContent === 'Ready');\n\nawait eventually(() => {\n expect(onSave).toHaveBeenCalledOnce();\n}, { message: 'save callback did not run' });\n\nconst completed = waitForEvent<CustomEvent<{ id: string }>>(panel, 'save-complete', {\n signal: AbortSignal.timeout(1000),\n});\nfireClick(saveButton);\nexpect((await completed).detail.id).toBeDefined();\n\nawait delay(100); // real timer dependency, such as a debounce\n```\n\n`waitUntil()` polls a boolean predicate. `eventually()` retries a throwing assertion and can include a diagnostic `message`. Both enforce one hard deadline while asynchronous callbacks are pending. Abort rejects immediately with the signal reason, but cannot stop callback-owned work; forward an application signal into that work when needed.\n\nTimeouts reject with `AssayTimeoutError`. Timeout and delay values must be finite and non-negative; polling intervals must be finite and positive. All durations must fit the platform timer limit of 2,147,483,647 ms.\n\n## Testing custom elements\n\nUse Ore for component mounting and Assay for generic DOM concerns:\n\n```ts\nimport { fireClick } from '@vielzeug/assay';\nimport { html } from '@vielzeug/ore';\nimport { mount } from '@vielzeug/ore/testing';\n\nconst fixture = await mount(\n () => html`\n <button @click=${onSave}>Save</button>\n `,\n);\n\nfireClick(fixture.get('button'));\nawait fixture.flush();\n\nexpect(onSave).toHaveBeenCalledOnce();\n```\n\nRefine's testing entry point contains only Refine-specific assertions and typed mount wrappers. Import Assay helpers\ndirectly instead of routing generic DOM operations through another package.\n\nWhen a suite tests a form-associated custom element (one built on `useField()` from `@vielzeug/ore`), install the\nform-internals polyfill in the setup file so jsdom gains the `ElementInternals` API it lacks:\n\n```ts\n// vitest.setup.ts\nimport { installFormInternalsPolyfill } from '@vielzeug/assay';\nimport { install } from '@vielzeug/ore/testing';\nimport { afterAll, afterEach } from 'vitest';\n\ninstall(afterEach);\nafterAll(installFormInternalsPolyfill());\n```\n\n## Traverse slotted content in jsdom\n\nUse `walkFlatTree()` when a test must visit descendants assigned through slots. It expands each slot into its assigned\nelements, including nested slots, instead of relying on jsdom's light-DOM-only `querySelectorAll()`.\n\n```ts\nimport { walkFlatTree } from '@vielzeug/assay';\n\nconst named: HTMLElement[] = [];\nwalkFlatTree(form, (element) => {\n if (element.hasAttribute('name')) named.push(element);\n});\n```\n\nThe callback receives descendants in the flat tree, not the root element or slot elements themselves. Browsers already\nuse the flat tree for DOM algorithms; this helper is for test code that needs equivalent traversal in jsdom.\n\n## Best Practices\n\n- Scope multiple assertions with `within()` rather than repeatedly querying the document.\n- Prefer `get*` for required controls and `query*` for intentional absence checks.\n- Make form state and event boundaries explicit: assign `.value`, then call `fireInput()` or `fireChange()`.\n- Use browser integration tests for focus, disabled activation, pointer capture, and other browser-default behavior.\n- Use `waitForEvent()` for an emitted event, `waitUntil()` for a condition, and `eventually()` for assertions.\n- Add a diagnostic `message` to waits whose final assertion does not identify the tested workflow.\n",
5
+ "api": "---\ntitle: 'Assay: API Reference'\ndescription: API reference for @vielzeug/assay queries, event dispatch, async waiting, and test-environment helpers.\n---\n\n[[toc]]\n\n## API Overview\n\n| Symbol | Purpose | Execution mode | Common gotcha |\n| -------------------------------------------- | --------------------------------- | -------------- | ----------------------------------------------- |\n| `within` | Creates scoped query API | Sync | Required `get*` methods throw `AssayQueryError` |\n| `queryInShadow` / `queryPart` / `getSlotted` | Crosses custom-element boundaries | Sync | Open shadow roots are required |\n\n| `fire*` / `dispatch` | Dispatches platform event instances | Sync | Does not reproduce browser default behavior |\n| `waitUntil` / `eventually` / `waitForEvent` | Waits for conditions, assertions, or events | Async | Use a signal or timeout for bounded waits |\n| `delay` | Schedules a cancellable timer | Async | Pass a signal to cancel a pending wait |\n| `walkFlatTree` | Walks the flat tree across slot boundaries | Sync | jsdom-only need: browsers already use the flat tree |\n| `installFormInternalsPolyfill` | Polyfills `ElementInternals` form association for jsdom | Sync | Global patches: only install in suites that need them |\n\n## Package Entry Point\n\n| Import | Purpose |\n| ----------------- | ------------------------------------------------------------------------------ |\n| `@vielzeug/assay` | DOM queries, events, wait helpers, test-environment helpers, errors, and types |\n\n## Queries\n\n### `within(root)`\n\nCreates a `QueryScope` for an `Element`, `ShadowRoot`, `Document`, or `DocumentFragment`.\n\n| Method | Returns | Use |\n| --------------------------------- | ----------------- | -------------------------------------------- |\n| `get(selector)` | `Element` | Required CSS match; throws `AssayQueryError` |\n| `query(selector)` | `Element \\| null` | Optional CSS match |\n| `queryAll(selector)` | `Element[]` | All CSS matches |\n| `getByText(text, selector?)` | `Element` | Required exact trimmed-text match |\n| `queryByText(text, selector?)` | `Element \\| null` | Optional exact trimmed-text match |\n| `queryAllByText(text, selector?)` | `Element[]` | All exact trimmed-text matches |\n| `getByTestId(id)` | `Element` | Required `data-testid` match |\n| `queryByTestId(id)` | `Element \\| null` | Optional `data-testid` match |\n| `queryAllByTestId(id)` | `Element[]` | All `data-testid` matches |\n\nText selectors default to `'*'`. Required-query failures include the lookup and a bounded view of the scoped DOM.\n\n### Shadow and slot helpers\n\n| Function | Returns | Description |\n| ---------------------------------- | ----------------- | ---------------------------------------------------- |\n| `queryInShadow(host, selector)` | `Element \\| null` | First match in an open shadow root |\n| `queryAllInShadow(host, selector)` | `Element[]` | All matches in an open shadow root |\n| `queryPart(host, part)` | `Element \\| null` | First shadow element whose `part` token matches |\n| `getSlotted(host, slotName?)` | `Element[]` | Direct light-DOM children in a named or default slot |\n\nThese helpers return `null` or `[]` when there is no shadow root. Dynamic test IDs, parts, and slot names are matched\nas attribute values rather than interpolated into CSS selectors.\n\n## Event dispatch\n\nAll event helpers synchronously return `dispatchEvent()`'s boolean result.\n\n```ts\nimport {\n dispatch,\n fireBlur,\n fireChange,\n fireClick,\n fireCustom,\n fireFocus,\n fireInput,\n fireKeyDown,\n fireKeyUp,\n fireSubmit,\n} from '@vielzeug/assay';\n\nfireClick(button, { clientX: 20 });\nfireInput(input);\nfireKeyDown(input, { key: 'Enter' });\nfireCustom(element, 'item-added', { detail: { id: '42' } });\ndispatch(element, new Event('ready'));\n```\n\n| Function | Event class | Defaults |\n| --------------------------- | --------------- | ------------------------------------------------------ |\n| `fireBlur` / `fireFocus` | `FocusEvent` | Platform defaults (`bubbles: false`) |\n| `fireChange` | `Event` | `bubbles: true` |\n| `fireInput` | `InputEvent` | `bubbles: true` |\n| `fireClick` | `MouseEvent` | `bubbles: true`, `cancelable: true` |\n| `fireKeyDown` / `fireKeyUp` | `KeyboardEvent` | `bubbles: true`, `cancelable: true` |\n| `fireSubmit` | `SubmitEvent` | `bubbles: true`, `cancelable: true` |\n| `fireCustom` | `CustomEvent` | `bubbles: true`, `cancelable: true`, `composed: false` |\n\n`fireCustom(target, type, init?)` dispatches a `CustomEvent` with the given type. Assay intentionally does not\nprovide browser-default or fallback pointer/touch simulation.\n\n## Async waiting\n\n```ts\nawait waitUntil(() => ready, { interval: 20, signal, timeout: 1000 });\nawait eventually(() => expect(spy).toHaveBeenCalled(), { signal, timeout: 1000 });\nawait waitForEvent(target, 'ready', { signal, timeout: 1000 });\nawait delay(100, { signal });\n```\n\n| Function | Success condition | Options |\n| -------------------------------------- | ------------------------ | ------------------------------------------ |\n| `waitUntil(predicate, options?)` | Predicate returns `true` | `timeout`, `interval`, `signal` |\n| `eventually(assertion, options?)` | Assertion stops throwing | `timeout`, `interval`, `signal`, `message` |\n| `waitForEvent(target, type, options?)` | Target emits `type` | `timeout`, `signal` |\n| `delay(ms?, options?)` | Timer elapses | `signal` |\n\n`waitUntil()`, `eventually()`, and `waitForEvent()` reject with `AssayTimeoutError` when their shared deadline expires. `waitUntil()` and `eventually()` remain bounded while an asynchronous callback is pending. Abort rejects with the signal reason and removes Assay-owned timers and listeners. Callback work itself must cooperate with application cancellation.\n\n`timeout` and delay milliseconds must be finite, non-negative, and no greater than 2,147,483,647 ms; `interval` must be finite, positive, and within the same timer limit. Invalid durations throw `RangeError`. `eventually()` also accepts `message` for diagnostic timeout context.\n\n## Test-environment helpers\n\n### `walkFlatTree(root, visit)`\n\n```ts\nfunction walkFlatTree(root: Element, visit: (element: HTMLElement) => void): void;\n```\n\nWalks the flat tree below `root`, calling `visit` for each assigned or ordinary descendant (not for `root` itself).\nAt each `<slot>` it expands `assignedElements()` instead of descending into its (always empty, in jsdom) literal\nchildren. jsdom only\nimplements the light DOM tree, so `querySelectorAll()` never crosses a slot into its assigned elements; real\nbrowsers use the flat tree for that. This helper closes the gap for tests that must find form-associated (or\notherwise slotted) elements through shadow boundaries.\n\n```ts\nconst named: HTMLElement[] = [];\n\nwalkFlatTree(form, (el) => {\n if (el.hasAttribute('name')) named.push(el);\n});\n```\n\n### `installFormInternalsPolyfill()`\n\n```ts\nfunction installFormInternalsPolyfill(): () => void;\n```\n\njsdom implements none of the `ElementInternals` form-association API: not `setFormValue`, not `setValidity`, not\nthe `checkValidity`/`reportValidity`/`validity`/`validationMessage` mixin real browsers put on every\n`formAssociated: true` element, not even `FormData` collecting a form-associated element's set value, or\n`<form>.reset()` invoking `formResetCallback()`. Every one of those gaps is exercised by `useField()` from\n`@vielzeug/ore`, so any package testing a form-associated component needs all of them.\n\nCall it from a test setup when the suite tests form-associated components; it returns an `uninstall()` that\nrestores every patched global. The patches are global monkey-patches and skip APIs already implemented by the\nenvironment, so suites without form-associated components shouldn't install them. If `ElementInternals` is absent,\nthe installer is a no-op. A downstream package should\nrely on this instead of hand-rolling its own copy.\n\n```ts\n// vitest.setup.ts\nimport { installFormInternalsPolyfill } from '@vielzeug/assay';\nimport { install } from '@vielzeug/ore/testing';\nimport { afterAll, afterEach } from 'vitest';\n\ninstall(afterEach);\nafterAll(installFormInternalsPolyfill());\n```\n\n## Types\n\n```ts\nexport interface QueryScope {\n get<E extends Element = Element>(selector: string): E;\n getByTestId<E extends Element = Element>(testId: string): E;\n getByText<E extends Element = Element>(text: string, selector?: string): E;\n query<E extends Element = Element>(selector: string): E | null;\n queryAll<E extends Element = Element>(selector: string): E[];\n queryAllByTestId<E extends Element = Element>(testId: string): E[];\n queryAllByText<E extends Element = Element>(text: string, selector?: string): E[];\n queryByTestId<E extends Element = Element>(testId: string): E | null;\n queryByText<E extends Element = Element>(text: string, selector?: string): E | null;\n}\n```\n\nScoped query helpers returned by `within(root)`.\n\n```ts\nexport interface WaitOptions {\n /** Polling interval in ms (default: 50). */\n interval?: number;\n /** Cancel the pending wait. */\n signal?: AbortSignal;\n /** Maximum wait time in ms (default: 1000). */\n timeout?: number;\n}\n\nexport interface EventuallyOptions extends WaitOptions {\n /** Context included in the timeout error. */\n message?: string;\n}\n\nexport interface DelayOptions {\n /** Cancel the pending delay. */\n signal?: AbortSignal;\n}\n```\n\n`WaitOptions` configures `waitUntil()`. `EventuallyOptions` configures `eventually()`. `DelayOptions` configures `delay()`.\n\n## Errors\n\n| Error | Meaning |\n| ------------------- | -------------------------------------- |\n| `AssayError` | Base class for Assay-originated errors |\n| `AssayQueryError` | A required `get*` query had no match |\n| `AssayTimeoutError` | A wait operation reached its timeout |\n\nUse `instanceof AssayError` to narrow any value to the Assay error hierarchy.\n",
6
+ "usage": "---\ntitle: 'Assay: Usage Guide'\ndescription: Scoped DOM queries, exact event dispatch, and cancellable waiting with @vielzeug/assay.\n---\n\n[[toc]]\n\n## Basic Usage\n\n`within(root)` is Assay's query API. It accepts an element, document fragment, or shadow root and keeps every lookup\nin that scope. Use `get*` when a match is required and nullable `query*` methods when absence is part of the assertion.\n\n```ts\nimport { within } from '@vielzeug/assay';\n\nconst view = within(panel.shadowRoot!);\n\nconst save = view.get<HTMLButtonElement>('button.save');\nconst status = view.queryByText('Saved');\n\nexpect(view.query('.error')).toBeNull();\nexpect(view.getByTestId('summary').textContent).toContain('Complete');\n```\n\n`get()`, `getByText()`, and `getByTestId()` throw `AssayQueryError` with the lookup and a bounded rendering of the\nscoped DOM. This keeps a failed required lookup diagnosable without non-null assertions.\n\nUse the cross-boundary helpers for custom elements:\n\n```ts\nimport { getSlotted, queryAllInShadow, queryPart } from '@vielzeug/assay';\n\nconst trigger = queryPart(menu, 'trigger');\nconst options = queryAllInShadow(menu, '[role=\"option\"]');\nconst footerActions = getSlotted(dialog, 'footer');\n```\n\n## Exact event dispatch\n\nAssay dispatches the platform event class named by each helper. It does not model browser activation, focus\nmanagement, or form defaults. `fireFocus()` and `fireBlur()` use their non-bubbling platform defaults; use\n`focusin`/`focusout` events when testing delegated focus listeners. Use `element.click()` when native activation is\nthe behavior under test; use Assay when testing an event listener or a controlled state transition.\n\n```ts\nimport { fireClick, fireCustom, fireInput, fireKeyDown } from '@vielzeug/assay';\n\ninput.value = 'Ada';\nfireInput(input);\n\nfireKeyDown(input, { key: 'Enter' });\nfireClick(saveButton);\nfireCustom(panel, 'value-change', { detail: { value: 42 } });\n```\n\nEvery helper returns `dispatchEvent()`'s boolean result. `dispatch(target, event)` is available when an existing\nevent instance is the clearest expression of the test.\n\n## Waiting\n\nChoose the waiting primitive by the test's assertion shape:\n\n```ts\nimport { delay, eventually, waitForEvent, waitUntil } from '@vielzeug/assay';\n\nawait waitUntil(() => panel.querySelector('.status')?.textContent === 'Ready');\n\nawait eventually(\n () => {\n expect(onSave).toHaveBeenCalledOnce();\n },\n { message: 'save callback did not run' },\n);\n\nconst completed = waitForEvent<CustomEvent<{ id: string }>>(panel, 'save-complete', {\n signal: AbortSignal.timeout(1000),\n});\nfireClick(saveButton);\nexpect((await completed).detail.id).toBeDefined();\n\nawait delay(100); // real timer dependency, such as a debounce\n```\n\n`waitUntil()` polls a boolean predicate. `eventually()` retries a throwing assertion and can include a diagnostic `message`. Both enforce one hard deadline while asynchronous callbacks are pending. Abort rejects immediately with the signal reason, but cannot stop callback-owned work; forward an application signal into that work when needed.\n\nTimeouts reject with `AssayTimeoutError`. Timeout and delay values must be finite and non-negative; polling intervals must be finite and positive. All durations must fit the platform timer limit of 2,147,483,647 ms.\n\n## Testing custom elements\n\nUse Ore for component mounting and Assay for generic DOM concerns:\n\n```ts\nimport { fireClick } from '@vielzeug/assay';\nimport { html } from '@vielzeug/ore';\nimport { mount } from '@vielzeug/ore/testing';\n\nconst fixture = await mount(\n () => html`\n <button @click=${onSave}>Save</button>\n `,\n);\n\nfireClick(fixture.get('button'));\nawait fixture.flush();\n\nexpect(onSave).toHaveBeenCalledOnce();\n```\n\nRefine's testing entry point contains only Refine-specific assertions and typed mount wrappers. Import Assay helpers\ndirectly instead of routing generic DOM operations through another package.\n\nWhen a suite tests a form-associated custom element (one built on `useField()` from `@vielzeug/ore`), install the\nform-internals polyfill in the setup file so jsdom gains the `ElementInternals` API it lacks:\n\n```ts\n// vitest.setup.ts\nimport { installFormInternalsPolyfill } from '@vielzeug/assay';\nimport { install } from '@vielzeug/ore/testing';\nimport { afterAll, afterEach } from 'vitest';\n\ninstall(afterEach);\nafterAll(installFormInternalsPolyfill());\n```\n\n## Traverse slotted content in jsdom\n\nUse `walkFlatTree()` when a test must visit descendants assigned through slots. It expands each slot into its assigned\nelements, including nested slots, instead of relying on jsdom's light-DOM-only `querySelectorAll()`.\n\n```ts\nimport { walkFlatTree } from '@vielzeug/assay';\n\nconst named: HTMLElement[] = [];\nwalkFlatTree(form, (element) => {\n if (element.hasAttribute('name')) named.push(element);\n});\n```\n\nThe callback receives descendants in the flat tree, not the root element or slot elements themselves. Browsers already\nuse the flat tree for DOM algorithms; this helper is for test code that needs equivalent traversal in jsdom.\n\n## Best Practices\n\n- Scope multiple assertions with `within()` rather than repeatedly querying the document.\n- Prefer `get*` for required controls and `query*` for intentional absence checks.\n- Make form state and event boundaries explicit: assign `.value`, then call `fireInput()` or `fireChange()`.\n- Use browser integration tests for focus, disabled activation, pointer capture, and other browser-default behavior.\n- Use `waitForEvent()` for an emitted event, `waitUntil()` for a condition, and `eventually()` for assertions.\n- Add a diagnostic `message` to waits whose final assertion does not identify the tested workflow.\n",
7
7
  "examples": "---\ntitle: 'Assay: Examples'\ndescription: Practical examples and recipes for assay.\n---\n\n## Examples\n\n- [Custom Element Interaction](./examples/custom-element-interaction.md)\n- [Waiting for Async Updates](./examples/waiting-for-async-updates.md)\n"
8
8
  },
9
9
  "examples": [],
@@ -23,6 +23,7 @@
23
23
  "fireKeyUp": "export {\n dispatch,\n fireBlur,\n fireChange,\n fireClick,\n fireCustom,\n fireFocus,\n fireInput,\n fireKeyDown,\n fireKeyUp,\n fireSubmit,\n} from './events';",
24
24
  "fireSubmit": "export {\n dispatch,\n fireBlur,\n fireChange,\n fireClick,\n fireCustom,\n fireFocus,\n fireInput,\n fireKeyDown,\n fireKeyUp,\n fireSubmit,\n} from './events';",
25
25
  "installFormInternalsPolyfill": "export { installFormInternalsPolyfill } from './form-internals';",
26
+ "installPopoverPolyfill": "export { installPopoverPolyfill } from './popover';",
26
27
  "getSlotted": "export { getSlotted, type QueryScope, queryAllInShadow, queryInShadow, queryPart, within } from './query';",
27
28
  "QueryScope": "export { getSlotted, type QueryScope, queryAllInShadow, queryInShadow, queryPart, within } from './query';",
28
29
  "queryAllInShadow": "export { getSlotted, type QueryScope, queryAllInShadow, queryInShadow, queryPart, within } from './query';",
@@ -1,9 +1,9 @@
1
1
  {
2
2
  "apiSource": "// Public root surface of @vielzeug/beacon. Every public export goes through this file.\nexport { createCoachmark } from './coachmark';\nexport { BeaconConfigError, BeaconDisposedError, BeaconError } from './errors';\nexport { resolveTarget } from './target';\nexport { createTour } from './tour';\nexport type {\n BeaconBaseEvent,\n BeaconCoachmarkConfig,\n BeaconCoachmarkEvent,\n BeaconCoachmarkHandle,\n BeaconCurrentStep,\n BeaconPlacement,\n BeaconPresentation,\n BeaconProgress,\n BeaconRect,\n BeaconStatus,\n BeaconStep,\n BeaconStorage,\n BeaconTarget,\n BeaconTargetResolver,\n BeaconTourConfig,\n BeaconTourEvent,\n BeaconTourHandle,\n} from './types';\n",
3
3
  "docs": {
4
- "index": "---\ntitle: 'Beacon: Product tours and coachmarks'\ndescription: \"Guided onboarding and contextual product tours for web applications\"\npackage: beacon\ncategory: ui-interaction\nkeywords: [onboarding, product-tour, coachmark, walkthrough, step-by-step, spotlight, headless]\nrelated: [refine, orbit, ripple, vault]\nexports: [createTour, createCoachmark, resolveTarget]\nenvironments: [browser]\n---\n\n<!-- markdownlint-disable MD025 MD033 MD060 -->\n\n<PackageHero package=\"beacon\" />\n\n## Why Beacon?\n\nProduct tours rot fast: hand-wired step sequences hard-code positioning, persistence, and target waiting into application code, and break the first time the UI moves. Beacon is the headless half of a tour — it owns step order, target waiting, resumable progress, and events — while your markup (or `@vielzeug/refine`'s `ore-tour` components) decides what a step looks like.\n\n```ts\n// Before: step logic tangled with rendering and storage\nlet step = Number(localStorage.getItem('tour-step') ?? 0);\nfunction showStep() {\n const el = document.querySelector(steps[step].selector);\n card.style.top = `${el.getBoundingClientRect().bottom + 8}px`; // breaks on scroll\n card.innerHTML = steps[step].html;\n}\n\n// After: the engine owns order, persistence, and the anchor rect\nimport { createTour } from '@vielzeug/beacon';\n\nconst tour = createTour({ id: 'welcome', steps, storage });\ntour.current.subscribe(() => renderCard(tour.current.value)); // rect is the live anchor\ntour.start();\n```\n\n| Feature | Beacon | Shepherd.js | Intro.js |\n| --- | --- | --- | --- |\n| Bundle size | <PackageInfo package=\"beacon\" type=\"size\" /> | ~50 kB | ~25 kB |\n| External dependencies | <ore-icon name=\"check\" size=\"16\"></ore-icon> (none) | <ore-icon name=\"check\" size=\"16\"></ore-icon> | <ore-icon name=\"check\" size=\"16\"></ore-icon> |\n| Headless (you own the UI) | <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| Resumable persisted progress | <ore-icon name=\"check\" size=\"16\"></ore-icon> | <ore-icon name=\"check\" size=\"16\"></ore-icon> | <ore-icon name=\"triangle-alert\" size=\"16\"></ore-icon> |\n| Versioned re-offers | <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| Coachmarks (single hints) | <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\n<div class=\"decision-callout\">\n\n**Use Beacon when** tours must survive UI changes: steps anchor to your real DOM, progress persists and resumes, and the card markup is yours (or refine's ready-made `ore-tour`).\n\n**Consider Shepherd or Intro.js when** you want a fully-rendered, opinionated tour widget you never style yourself and can accept their DOM and CSS.\n\n</div>\n\n## Installation\n\n::: code-group\n\n```sh [pnpm]\npnpm add @vielzeug/beacon\n```\n\n```sh [npm]\nnpm install @vielzeug/beacon\n```\n\n```sh [yarn]\nyarn add @vielzeug/beacon\n```\n\n:::\n\n## Quick Start\n\nDefine the tour as data, run it, and render the current step wherever you like. The handle is disposable, so component teardown cleans everything up.\n\n```ts\nimport { createTour, type BeaconTourConfig } from '@vielzeug/beacon';\n\nconst config = {\n id: 'getting-started',\n steps: [\n { id: 'welcome', title: 'Welcome', description: 'A two-minute look around.' },\n { id: 'nav', target: '[data-tour=\"nav\"]', title: 'Navigation', placement: 'bottom-start' },\n ],\n} satisfies BeaconTourConfig;\n\nconst tour = createTour(config);\n\n// Render from the live snapshot: rect is the anchor, null centers the card.\nconst unsubscribe = tour.current.subscribe(() => {\n const { presentation, rect, step } = tour.current.value;\n console.log({ presentation, rect, step }); // drive your card from this\n});\n\ntour.start();\n\n// On teardown:\nunsubscribe();\ntour.dispose();\n```\n\n## Features\n\n<div class=\"features-grid\">\n\n- `createTour()` runs a sequential, resumable walkthrough over your own DOM.\n- `createCoachmark()` shows a single contextual hint once per user per version.\n- `current` is a live `Readable<BeaconCurrentStep>` snapshot: step, index, placement, presentation, and resolved anchor rect, kept fresh on scroll, resize, and layout changes.\n- `target` accepts a selector, `Element`, rect, or lazy resolver, and beacon waits for lazily-mounted targets before showing the step.\n- `storage` persists progress through any `BeaconStorage` bridge (localStorage, IndexedDB via `@vielzeug/vault`, a server API).\n- `version` re-offers a finished tour when the product changes underneath it.\n- `tap()` subscribes to lifecycle events (`step:start`, `tour:complete`, `target-missing`, ...) with `AbortSignal` support.\n- `resolveTarget()` turns any target into a viewport rect compatible with `@vielzeug/orbit` positioners.\n- `ore-tour`'s `bind()` in `@vielzeug/refine` renders a handle and routes its actions with zero glue code.\n\n</div>\n\n## Documentation\n\n<div class=\"doc-links\">\n\n- [Usage Guide](./usage.md)\n- [API Reference](./api.md)\n- [Examples](./examples.md)\n- [Migration Guide](./migration.md)\n\n</div>\n\n## See Also\n\n<div class=\"see-also\">\n\n- [Refine](/refine/): ships `ore-tour` and `ore-spotlight`, the ready-made dialog/popover/bottom-sheet presentation layer for a beacon handle.\n- [Orbit](/orbit/): compute exact popover coordinates from `current.rect` when you build a custom presentation.\n- [Vault](/vault/): durable storage backends that plug into `BeaconStorage` with a six-line bridge.\n- [Ripple](/ripple/): the `Readable` signals every beacon handle exposes come from here.\n\n</div>\n\n<!-- markdownlint-enable MD025 MD033 MD060 -->\n",
5
- "api": "---\ntitle: 'Beacon: API Reference'\ndescription: Public API of @vielzeug/beacon.\n---\n\n[[toc]]\n\n## API Overview\n\n| Symbol | Purpose | Execution mode | Common gotcha |\n| --- | --- | --- | --- |\n| `createTour(config)` | Runs a sequential, resumable walkthrough over the app's DOM | Sync (storage reads may be async) | `start()` is a no-op when the stored record says the tour is completed |\n| `createCoachmark(config)` | Shows a single contextual hint anchored to one target | Sync (storage reads may be async) | `show()` is a no-op after `dismiss()` until `reset()` or a `version` bump |\n| `resolveTarget(target)` | Resolves any target to a viewport-space rect or `null` | Sync | Call it per position update; a scrolled target's rect changes |\n| `BeaconError` | Base class for all beacon-originated errors | — | Catch this to catch any beacon error |\n| `BeaconConfigError` | Invalid tour/coachmark configuration | Thrown at creation | Empty `steps` or duplicate step ids fail at `createTour()` |\n| `BeaconDisposedError` | Handle used after `dispose()` | Thrown on call | `dispose()` is idempotent; later method calls throw |\n\n## Package Entry Point\n\n| Import | Purpose |\n| --- | --- |\n| `@vielzeug/beacon` | Complete public Beacon API |\n\n---\n\n## Core Functions\n\n### createTour\n\n```ts\nfunction createTour(config: BeaconTourConfig): BeaconTourHandle\n```\n\nReturns a live handle for one guided tour. Throws `BeaconConfigError` when `id` is empty, `steps` is empty, or step ids are duplicated.\n\nParameters: see [`BeaconTourConfig`](#beacontourconfig).\n\n**Returns:** a [`BeaconTourHandle`](#beacontourhandle) that is also `Disposable`.\n\n**Example:**\n\n```ts\nimport { createTour } from '@vielzeug/beacon';\n\nconst tour = createTour({\n id: 'release-tour',\n steps: [{ id: 'intro', title: 'What changed' }],\n autoStart: true,\n});\n\ntour.status.subscribe(() => console.log(tour.status.value));\n```\n\nHandle methods:\n\n| Member | Behavior |\n| --- | --- |\n| `start()` | Begin or resume the tour. No-op when completed; throws after dispose. |\n| `next()` | Advance; past the last step, finishes the tour. |\n| `back()` | Return to the previous step. No-op on the first step. |\n| `goto(step)` | Jump to a step by index or id. Unknown values warn and no-op. |\n| `pause()` | Stop showing, keep progress so `start()` resumes it. |\n| `skip()` | End from a \"Skip\" control. Persisted as dismissed, reported with reason `'skip'`. |\n| `finish()` | Mark finished and stop showing. |\n| `dismiss()` | End by closing (Escape, backdrop). Persisted as dismissed. |\n| `reset()` | Discard persisted progress; the tour is eligible again. |\n| `tap(handler, options?)` | Observe [`BeaconTourEvent`](#beacontourentype)s; returns an unsubscribe, honors `options.signal`. |\n| `dispose()` / `[Symbol.dispose]()` | Release all listeners and observers. Idempotent. |\nHandle properties:\n\n| Member | Type | Notes |\n| --- | --- | --- |\n| `current` | `Readable<BeaconCurrentStep>` | The snapshot to render; `step` is `null` while closed. Beacon re-publishes the rect on scroll, resize, and target layout changes while a step is active. |\n| `status` | `Readable<BeaconStatus>` | Lifecycle, persisted on every change. |\n| `eligible` | `Readable<boolean>` | Whether the tour should be offered to this user. |\n| `steps` | `readonly BeaconStep[]` | The configured steps, in order. |\n| `id` | `string` | The configured id (also the storage key suffix). |\n| `disposed` | `boolean` | Whether `dispose()` has run. |\n| `disposalSignal` | `AbortSignal` | Aborted on dispose; pass it to your own listeners. |\n\n---\n\n### createCoachmark\n\n```ts\nfunction createCoachmark(config: BeaconCoachmarkConfig): BeaconCoachmarkHandle\n```\n\nReturns a handle for one contextual hint. Throws `BeaconConfigError` when `id` is empty.\n\nParameters: see [`BeaconCoachmarkConfig`](#beaconcoachmarkconfig).\n\n**Returns:** a [`BeaconCoachmarkHandle`](#beaconcoachmarkhandle) that is also `Disposable`.\n\n**Example:**\n\n```ts\nimport { createCoachmark } from '@vielzeug/beacon';\n\nconst hint = createCoachmark({\n id: 'share-hint',\n target: '[data-tour=\"share\"]',\n title: 'Share this board',\n});\n\nhint.show(); // waits for the target to mount if it is not on screen yet\n```\n\nHandle members:\n\n| Member | Behavior |\n| --- | --- |\n| `show()` | Show the hint. No-op once dismissed without a newer version. |\n| `hide()` | Hide without recording it as seen; it can show again. |\n| `dismiss()` | Hide and record as seen permanently (until `reset()`/`version`). |\n| `reset()` | Discard the persisted record; the hint is eligible again. |\n| `tap(handler, options?)` | Observe `coachmark:show`, `coachmark:dismiss`, and common events. |\n| `current` | `Readable<BeaconCurrentStep>` — the hint as a single-step snapshot: `step` is the hint while visible, `null` while hidden. The rect stays live on scroll, resize, and target layout changes. |\n| `eligible` | `Readable<boolean>` — never seen, or superseded by a newer version. |\n\n---\n\n### resolveTarget\n\n```ts\nfunction resolveTarget(target: BeaconTarget): BeaconRect | null\n```\n\nResolves a target to a viewport-space rect, or `null` when it is not available yet. Strings resolve through `document.querySelector`; functions are called and their result normalized.\n\n| Parameter | Type | Description |\n| --- | --- | --- |\n| `target` | `BeaconTarget` | Selector, `Element`, rect, or lazy resolver. |\n\n**Returns:** `BeaconRect | null` — shape-compatible with `getBoundingClientRect()` and `@vielzeug/orbit`'s `Rect`.\n\n**Example:**\n\n```ts\nimport { resolveTarget } from '@vielzeug/beacon';\n\nconst rect = resolveTarget('[data-tour=\"share\"]');\n// Anchor an orbit positioner from a virtual reference:\nconst reference = { getBoundingClientRect: () => resolveTarget('[data-tour=\"share\"]') ?? ZERO_RECT };\n```\n\n---\n\n## Types\n\n### BeaconTourConfig\n\n| Option | Type | Default | Description |\n| --- | --- | --- | --- |\n| `id` | `string` | required | Unique identity; used as the storage key. |\n| `steps` | `readonly BeaconStep[]` | required | Every step, in order. At least one. |\n| `autoScroll` | `boolean` | `true` | Scroll each anchored step's target into view when it activates. |\n| `autoStart` | `boolean` | `false` | Start on the next microtask when eligible. |\n| `placement` | `BeaconPlacement` | `'auto'` | Default placement handed to the presentation layer. |\n| `presentation` | `BeaconPresentation` | `'auto'` | Default presentation handed to the presentation layer. |\n| `scrollOffset` | `number \\| (() => number)` | `0` | Pixels kept clear at the viewport top for a sticky header, read at scroll time. |\n| `storage` | `BeaconStorage` | memory-only | Progress persistence. |\n| `targetTimeoutMs` | `number` | `Infinity` | How long to wait for a step's target before skipping that step. `0` publishes immediately with a `null` rect. |\n| `version` | `number` | `1` | Bump to re-show a finished tour. |\n\n### BeaconCoachmarkConfig\n\n| Option | Type | Default | Description |\n| --- | --- | --- | --- |\n| `id` | `string` | required | Unique identity; used as the storage key. |\n| `target` | `BeaconTarget` | required | Where the hint points. |\n| `autoShow` | `boolean` | `true` | Show automatically when not yet seen. |\n| `data` | `unknown` | — | Free-form data for the presentation layer. |\n| `description` | `string` | — | Supporting copy. |\n| `placement` | `BeaconPlacement` | `'auto'` | Preferred side. |\n| `presentation` | `BeaconPresentation` | `'auto'` | Preferred presentation. |\n| `storage` | `BeaconStorage` | memory-only | Seen-record persistence. |\n| `title` | `string` | — | Headline copy. |\n| `version` | `number` | `1` | Bump to re-show a seen coachmark. |\n\n### BeaconStep\n\n```ts\ninterface BeaconStep {\n readonly data?: unknown;\n readonly description?: string;\n readonly id: string;\n readonly placement?: BeaconPlacement;\n readonly presentation?: BeaconPresentation;\n readonly target?: BeaconTarget;\n readonly title?: string;\n}\n```\n\nOne stop in a tour. `id` is stable and persisted — renaming it resets progress for that step. Omit `target` for a centered intro/outro step. `placement`/`presentation` override the tour-level defaults.\n\n### BeaconTarget\n\n```ts\ntype BeaconTarget = BeaconTargetResolver | BeaconRect | Element | string;\n```\n\nWhat a step points at. Selectors and elements anchor to real DOM; rects (or resolvers returning one) anchor to element-less geometry such as a canvas region.\n\n### BeaconTargetResolver\n\n```ts\ntype BeaconTargetResolver = () => Element | BeaconRect | null;\n```\n\nResolves a target lazily; `null` means \"not available yet\".\n\n### BeaconRect\n\n```ts\ninterface BeaconRect {\n readonly height: number;\n readonly width: number;\n readonly x: number;\n readonly y: number;\n}\n```\n\nA viewport-space rectangle in CSS pixels. Matches `@vielzeug/orbit`'s `Rect` shape.\n\n### BeaconPlacement\n\n```ts\ntype BeaconPlacement =\n | 'auto'\n | 'bottom' | 'bottom-end' | 'bottom-start'\n | 'left' | 'left-end' | 'left-start'\n | 'right' | 'right-end' | 'right-start'\n | 'top' | 'top-end' | 'top-start';\n```\n\nPreferred side for an anchored presentation; `'auto'` asks the positioning layer to choose.\n\n### BeaconPresentation\n\n```ts\ntype BeaconPresentation = 'auto' | 'bottom-sheet' | 'modal' | 'popover';\n```\n\nHow a step should be presented. `'auto'` lets the presentation layer pick by available space.\n\n### BeaconStatus\n\n```ts\ntype BeaconStatus = 'active' | 'completed' | 'dismissed' | 'idle' | 'paused';\n```\n\nTour lifecycle, persisted across visits. `'idle'` means never started.\n\n### BeaconProgress\n\n```ts\ninterface BeaconProgress {\n readonly status: BeaconStatus;\n readonly stepIndex: number;\n readonly version: number;\n}\n```\n\nThe persisted record for one tour or coachmark. `stepIndex` is `-1` before the first step. Records are validated on read; invalid records fall back to a fresh tour.\n\n### BeaconStorage\n\n```ts\ninterface BeaconStorage {\n get(key: string): Promise<BeaconProgress | null | undefined> | BeaconProgress | null | undefined;\n remove(key: string): Promise<unknown> | unknown;\n set(key: string, value: BeaconProgress): Promise<unknown> | unknown;\n}\n```\n\nMinimal storage contract; sync or async. Values are JSON. Keys arrive namespaced (`@vielzeug/beacon:<id>`). A `@vielzeug/vault` store plugs in with:\n\n```ts\nconst storage: BeaconStorage = {\n get: (key) => store.get('progress', key),\n remove: (key) => store.delete('progress', key),\n set: (key, value) => store.put('progress', { id: key, ...value }),\n};\n```\n\n### BeaconCurrentStep\n\n```ts\ninterface BeaconCurrentStep {\n readonly placement: BeaconPlacement;\n readonly presentation: BeaconPresentation;\n readonly rect: BeaconRect | null;\n readonly step: BeaconStep | null;\n readonly stepIndex: number;\n readonly totalSteps: number;\n}\n```\n\nThe snapshot a presentation layer renders from. `step` is `null` before start and after the tour ends; `rect` is `null` for centered steps and unresolved targets. While a step is active, Beacon keeps `rect` fresh on scroll, resize, and target layout changes — no app-side refresh call is needed.\n\n### BeaconTourEvent\n\n```ts\ntype BeaconTourEvent =\n | { readonly index: number; readonly step: BeaconStep; readonly type: 'step:start' }\n | { readonly index: number; readonly step: BeaconStep; readonly type: 'step:complete' }\n | { readonly reason: 'finish' | 'skip'; readonly type: 'tour:complete' }\n | { readonly type: 'tour:dismiss' }\n | { readonly status: BeaconStatus; readonly type: 'status-change' }\n | { readonly message: string; readonly type: 'target-missing' }\n | { readonly index: number; readonly message: string; readonly step: BeaconStep; readonly type: 'target-waiting' }\n | { readonly error: unknown; readonly message: string; readonly type: 'warning' }\n | { readonly type: 'dispose' };\n```\n\nNotable moments a tour reports through `tap()`. `target-missing` means a step was skipped because its target did not appear in time; `target-waiting` fires when a step begins waiting for its target to mount; `warning` carries storage or handler failures.\n\n### BeaconCoachmarkEvent\n\n```ts\ntype BeaconCoachmarkEvent =\n | { readonly type: 'coachmark:show' }\n | { readonly type: 'coachmark:dismiss' }\n | { readonly type: 'target-waiting' }\n | { readonly error: unknown; readonly message: string; readonly type: 'warning' }\n | { readonly type: 'dispose' };\n```\n\nNotable moments a coachmark reports through `tap()`.\n\n### BeaconTourHandle / BeaconCoachmarkHandle\n\nThe objects returned by `createTour()` and `createCoachmark()`, documented in [Core Functions](#core-functions). Both expose `disposalSignal`, `disposed`, `tap()`, `dispose()`, and `[Symbol.dispose]()`.\n\n## Errors\n\n| Error | Triggered by | Notes |\n| --- | --- | --- |\n| `BeaconError` | Base for all beacon errors | `name` is the subclass name; `cause` supported via `ErrorOptions`. |\n| `BeaconConfigError` | Empty `id`, empty `steps`, missing/duplicate step ids | Thrown synchronously from `createTour()`/`createCoachmark()`. |\n| `BeaconDisposedError` | Calling handle methods after `dispose()` | `dispose()` itself never throws. |\n",
6
- "usage": "---\ntitle: 'Beacon: Usage Guide'\ndescription: How to use @vielzeug/beacon.\n---\n\n[[toc]]\n\n## Basic Usage\n\nA tour is a config object plus a handle you drive from your own UI. The handle never renders anything: it publishes a `current` snapshot, and your card (or refine's `ore-tour`) renders it.\n\n```ts\nimport { createTour } from '@vielzeug/beacon';\n\nconst tour = createTour({\n id: 'product-tour',\n steps: [\n { id: 'welcome', title: 'Welcome', description: 'See what changed in this release.' },\n { id: 'nav', target: '[data-tour=\"nav\"]', title: 'Find everything', placement: 'bottom-start' },\n { id: 'share', target: '[data-tour=\"share\"]', title: 'Share the board', placement: 'bottom-end' },\n ],\n autoStart: true,\n});\n\n// Readable<T> from @vielzeug/ripple: the listener takes no arguments;\n// read .value inside it.\nconst unsubscribe = tour.current.subscribe(() => {\n const current = tour.current.value;\n if (!current.step) {\n hideCard();\n return;\n }\n renderCard({\n anchor: current.rect, // viewport rect of the target, null for centered steps\n index: current.stepIndex,\n placement: current.placement,\n presentation: current.presentation,\n title: current.step.title,\n total: current.totalSteps,\n });\n});\n\n// Wire your card's controls back to the handle.\ncardEl.addEventListener('next', () => tour.next());\ncardEl.addEventListener('back', () => tour.back());\ncardEl.addEventListener('skip', () => tour.skip());\n\n// On teardown:\nunsubscribe();\ntour.dispose();\n```\n\n`start()` is a no-op when the stored record says this user already completed the tour, so calling it on every page load is safe.\n\n## Steps and Targets\n\nA step without a `target` centers (intro/outro copy). A step with one anchors, and beacon waits for the target to exist before publishing the step — a tour can walk through lazily-mounted routes without any coordination code.\n\n```ts\nconst tour = createTour({\n id: 'deep-tour',\n steps: [\n // Selector: resolved with document.querySelector on activation.\n { id: 'sidebar', target: '[data-tour=\"sidebar\"]', title: 'Your libraries' },\n // Element: a ref your framework already holds.\n { id: 'editor', target: editorEl, title: 'The editor' },\n // Rect: geometry with no element, such as a canvas region.\n { id: 'chart', target: { height: 120, width: 240, x: 400, y: 300 }, title: 'This trend' },\n // Resolver: computed on every activation, and on scroll/resize while the step is active.\n { id: 'selection', target: () => window.getSelection()?.getRangeAt(0).getBoundingClientRect() ?? null, title: 'Your selection' },\n ],\n // Give up waiting after 4s and skip the step instead of hanging the tour.\n targetTimeoutMs: 4000,\n});\n```\n\nWith the default `targetTimeoutMs: Infinity`, a step whose target never mounts simply waits: the tour resumes the moment the user navigates to it. Set `0` to publish immediately with a `null` rect instead of waiting.\n\n## Persisting Progress\n\nWithout `storage`, progress lives in memory for the handle's lifetime only. Any object matching `BeaconStorage` makes it durable — values are JSON, keys are namespaced under `@vielzeug/beacon:`.\n\n```ts\nimport type { BeaconStorage } from '@vielzeug/beacon';\n\nconst storage: BeaconStorage = {\n get: (key) => JSON.parse(localStorage.getItem(key) ?? 'null'),\n remove: (key) => localStorage.removeItem(key),\n set: (key, value) => localStorage.setItem(key, JSON.stringify(value)),\n};\n\nconst tour = createTour({ id: 'product-tour', steps, storage });\n```\n\nAsync storage works too: `get` may return a promise, and the tour boots as soon as it resolves. A `set` that rejects is reported through `tap()` as a `warning` event, never thrown.\n\nRecords are validated on read, so a hand-edited or stale record falls back to a fresh tour instead of crashing.\n\n### Re-offering a finished tour\n\nBump `version` when the product changes and every user who finished the old tour becomes eligible again — users mid-tour on the old version keep their progress.\n\n```ts\nconst tour = createTour({\n id: 'product-tour',\n steps: newSteps,\n version: 2, // stored records at version 1 no longer block the tour\n});\n```\n\n`reset()` clears the persisted record for the current user, which is what a \"Replay tour\" or \"Reset onboarding\" control should call before `start()`.\n\n## Driving a Refine Presentation\n\n`@vielzeug/refine` ships `ore-tour` (a dialog with step footer, progress, and focus handling) and `ore-spotlight` (a decorative cutout that dims everything but the target). A beacon handle satisfies `ore-tour`'s `TourController` shape, so one call wires both directions:\n\n```ts\nimport type { OreTourElement } from '@vielzeug/refine/tour';\n\nconst card = document.querySelector<OreTourElement>('#tour-card');\n\nconst tour = createTour({ id: 'product-tour', presentation: 'auto', steps });\n\n// bind() renders `current` into the card's props and routes the card's\n// next/back/skip/finish/dismiss actions back into the handle.\nconst unbind = card.bind(tour);\n\ntour.start();\n\n// On teardown:\nunbind();\ntour.dispose();\n```\n\n`presentation: 'auto'` maps to a popover on wide viewports and a bottom sheet on narrow ones; ore-tour makes that switch itself and freezes the choice while the card is open, so a mid-step resize never rebuilds the card. Beacon keeps `current.rect` fresh on scroll, resize, and layout changes, so the anchored card follows the layout with no app-side listeners.\n\nApps that render their own markup read `current` directly instead (see [Basic Usage](#basic-usage)).\n\n## Coachmarks\n\nA coachmark is one hint anchored to one target, shown the first time the target appears and dismissed permanently after. Use it for the \"explain this button once\" moment a tour would be too heavy for — or as the follow-up a tour hands off to.\n\n```ts\nimport { createCoachmark } from '@vielzeug/beacon';\n\nconst share = createCoachmark({\n id: 'share-hint',\n target: '[data-tour=\"share\"]',\n title: 'Share this board',\n description: 'Send a link that opens straight into this view.',\n storage,\n});\n\n// Render from `current` — the same single-step snapshot a tour publishes.\nshare.current.subscribe(() => console.log(share.current.value.step !== null, share.current.value.rect));\n\n// The target may never appear on this page; show() waits for it.\nshare.show();\n\n// \"Got it\" button:\nshare.dismiss(); // records it as seen; never shows again for this version\n```\n\n`hide()` closes the hint without recording it, so it can appear again later; `dismiss()` is permanent until `reset()` or a `version` bump.\n\n## Events and Analytics\n\n`tap()` observes everything the engine considers notable. It returns an unsubscribe function and accepts an `AbortSignal`, so one controller can tear down every subscription.\n\n```ts\nconst controller = new AbortController();\n\ntour.tap((event) => {\n switch (event.type) {\n case 'step:start':\n analytics.track('tour_step', { id: tour.id, step: event.step.id });\n break;\n case 'tour:complete':\n analytics.track('tour_complete', { id: tour.id, reason: event.reason });\n break;\n case 'target-missing':\n // A step's target never mounted: your selector drifted from the DOM.\n console.warn(event.message);\n break;\n case 'warning':\n console.warn(event.message, event.error);\n break;\n }\n}, { signal: controller.signal });\n```\n\n`target-missing` is the event to alert on: it means a step was skipped because its target did not appear within `targetTimeoutMs`, which is exactly how stale tours announce themselves.\n\n## Testing\n\nBeacon runs without a rendering layer, so plain DOM tests are enough. The default in-memory storage keeps each handle's progress isolated, and `jsdom`'s `getBoundingClientRect()` returns zero-sized rects, which is fine for step-order assertions.\n\n```ts\nimport { createTour } from '@vielzeug/beacon';\nimport { expect, test } from 'vitest';\n\ntest('walks steps in order and completes', () => {\n const tour = createTour({\n id: 'test-tour',\n steps: [{ id: 'a' }, { id: 'b' }],\n targetTimeoutMs: 0, // publish immediately even without targets\n });\n\n tour.start();\n expect(tour.current.value.step?.id).toBe('a');\n\n tour.next();\n expect(tour.current.value.step?.id).toBe('b');\n\n tour.next(); // past the last step finishes\n expect(tour.status.value).toBe('completed');\n});\n```\n\nFor persistence assertions, pass a `Map`-backed `BeaconStorage` and a second handle with the same `id` to check it resumes where the first stopped.\n\n## Framework Integration\n\nBeacon is framework-neutral: bridge the `Readable` snapshot with whatever primitive your framework already has.\n\n::: code-group\n\n```tsx [React]\nimport { useEffect, useState } from 'react';\nimport { createTour } from '@vielzeug/beacon';\n\nfunction useTourState(tour: ReturnType<typeof createTour>) {\n const [current, setCurrent] = useState(tour.current.value);\n useEffect(() => tour.current.subscribe(() => setCurrent(tour.current.value)), [tour]);\n return current;\n}\n```\n\n```vue [Vue 3]\n<script setup lang=\"ts\">\nimport { onScopeDispose, shallowRef } from 'vue';\nimport { createTour } from '@vielzeug/beacon';\n\nconst current = shallowRef(tour.current.value);\nconst unsubscribe = tour.current.subscribe(() => (current.value = tour.current.value));\nonScopeDispose(unsubscribe);\n</script>\n```\n\n```svelte [Svelte]\n<script lang=\"ts\">\n import { onDestroy } from 'svelte';\n import { readable } from 'svelte/store';\n\n const current = readable(tour.current.value, (set) => {\n set(tour.current.value);\n return tour.current.subscribe(() => set(tour.current.value));\n });\n onDestroy(() => tour.dispose());\n</script>\n```\n\n:::\n\n## Working with Other Vielzeug Libraries\n\n- **Refine**: `ore-tour` and `ore-spotlight` are the intended presentation layer; see [Driving a Refine Presentation](#driving-a-refine-presentation).\n- **Orbit**: `current.rect` matches orbit's `Rect` shape, so a custom card positions with `computePosition({ getBoundingClientRect: () => current.rect ?? ZERO_RECT }, floatingEl, middleware)`.\n- **Vault**: any vault store becomes a `BeaconStorage` with a six-line bridge (see the `BeaconStorage` docs in [API Reference](./api.md)).\n- **Ripple**: every handle exposes ripple `Readable`s; `createRipple().isBatchingObservable` semantics apply when you subscribe from effects.\n\n## Best Practices\n\n- Give every tour and coachmark a stable `id` — it is the storage key; treat renames as data migrations.\n- Keep tour configs in their own modules as plain data checked with `satisfies BeaconTourConfig`, so steps stay reviewable data, not component code.\n- Prefer `data-tour=\"...\"` attributes as target selectors: they survive class renames and read as intent.\n- Set a finite `targetTimeoutMs` in production and alert on `target-missing` to catch stale selectors.\n- Call `reset()` before `start()` for replay controls; never delete storage keys by hand.\n- Bump `version` when a tour's meaning changes, not when its copy is typo-fixed.\n- Dispose handles on teardown; `tap()` subscriptions with a shared `AbortSignal` clean up in one call.\n",
4
+ "index": "---\ntitle: 'Beacon: Product tours and coachmarks'\ndescription: 'Guided onboarding and contextual product tours for web applications'\npackage: beacon\ncategory: ui-interaction\nkeywords: [onboarding, product-tour, coachmark, walkthrough, step-by-step, spotlight, headless]\nrelated: [refine, orbit, ripple, vault]\nexports: [createTour, createCoachmark, resolveTarget]\nenvironments: [browser]\n---\n\n<!-- markdownlint-disable MD025 MD033 MD060 -->\n\n<PackageHero package=\"beacon\" />\n\n## Why Beacon?\n\nProduct tours rot fast: hand-wired step sequences hard-code positioning, persistence, and target waiting into application code, and break the first time the UI moves. Beacon is the headless half of a tour — it owns step order, target waiting, resumable progress, and events — while your markup (or `@vielzeug/refine`'s `ore-tour` components) decides what a step looks like.\n\n```ts\n// Before: step logic tangled with rendering and storage\nlet step = Number(localStorage.getItem('tour-step') ?? 0);\nfunction showStep() {\n const el = document.querySelector(steps[step].selector);\n card.style.top = `${el.getBoundingClientRect().bottom + 8}px`; // breaks on scroll\n card.innerHTML = steps[step].html;\n}\n\n// After: the engine owns order, persistence, and the anchor rect\nimport { createTour } from '@vielzeug/beacon';\n\nconst tour = createTour({ id: 'welcome', steps, storage });\ntour.current.subscribe(() => renderCard(tour.current.value)); // rect is the live anchor\ntour.start();\n```\n\n| Feature | Beacon | Shepherd.js | Intro.js |\n| ---------------------------- | --------------------------------------------------- | -------------------------------------------- | ----------------------------------------------------- |\n| Bundle size | <PackageInfo package=\"beacon\" type=\"size\" /> | ~50 kB | ~25 kB |\n| External dependencies | <ore-icon name=\"check\" size=\"16\"></ore-icon> (none) | <ore-icon name=\"check\" size=\"16\"></ore-icon> | <ore-icon name=\"check\" size=\"16\"></ore-icon> |\n| Headless (you own the UI) | <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| Resumable persisted progress | <ore-icon name=\"check\" size=\"16\"></ore-icon> | <ore-icon name=\"check\" size=\"16\"></ore-icon> | <ore-icon name=\"triangle-alert\" size=\"16\"></ore-icon> |\n| Versioned re-offers | <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| Coachmarks (single hints) | <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\n<div class=\"decision-callout\">\n\n**Use Beacon when** tours must survive UI changes: steps anchor to your real DOM, progress persists and resumes, and the card markup is yours (or refine's ready-made `ore-tour`).\n\n**Consider Shepherd or Intro.js when** you want a fully-rendered, opinionated tour widget you never style yourself and can accept their DOM and CSS.\n\n</div>\n\n## Installation\n\n::: code-group\n\n```sh [pnpm]\npnpm add @vielzeug/beacon\n```\n\n```sh [npm]\nnpm install @vielzeug/beacon\n```\n\n```sh [yarn]\nyarn add @vielzeug/beacon\n```\n\n:::\n\n## Quick Start\n\nDefine the tour as data, run it, and render the current step wherever you like. The handle is disposable, so component teardown cleans everything up.\n\n```ts\nimport { createTour, type BeaconTourConfig } from '@vielzeug/beacon';\n\nconst config = {\n id: 'getting-started',\n steps: [\n { id: 'welcome', title: 'Welcome', description: 'A two-minute look around.' },\n { id: 'nav', target: '[data-tour=\"nav\"]', title: 'Navigation', placement: 'bottom-start' },\n ],\n} satisfies BeaconTourConfig;\n\nconst tour = createTour(config);\n\n// Render from the live snapshot: rect is the anchor, null centers the card.\nconst unsubscribe = tour.current.subscribe(() => {\n const { presentation, rect, step } = tour.current.value;\n console.log({ presentation, rect, step }); // drive your card from this\n});\n\ntour.start();\n\n// On teardown:\nunsubscribe();\ntour.dispose();\n```\n\n## Features\n\n<div class=\"features-grid\">\n\n- `createTour()` runs a sequential, resumable walkthrough over your own DOM.\n- `createCoachmark()` shows a single contextual hint once per user per version.\n- `current` is a live `Readable<BeaconCurrentStep>` snapshot: step, index, placement, presentation, and resolved anchor rect, kept fresh on scroll, resize, and layout changes.\n- `target` accepts a selector, `Element`, rect, or lazy resolver, and beacon waits for lazily-mounted targets before showing the step.\n- `storage` persists progress through any `BeaconStorage` bridge (localStorage, IndexedDB via `@vielzeug/vault`, a server API).\n- `version` re-offers a finished tour when the product changes underneath it.\n- `tap()` subscribes to lifecycle events (`step:start`, `tour:complete`, `target-missing`, ...) with `AbortSignal` support.\n- `resolveTarget()` turns any target into a viewport rect compatible with `@vielzeug/orbit` positioners.\n- `ore-tour`'s `bind()` in `@vielzeug/refine` renders a handle and routes its actions with zero glue code.\n\n</div>\n\n## Documentation\n\n<div class=\"doc-links\">\n\n- [Usage Guide](./usage.md)\n- [API Reference](./api.md)\n- [Examples](./examples.md)\n- [Migration Guide](./migration.md)\n\n</div>\n\n## See Also\n\n<div class=\"see-also\">\n\n- [Refine](/refine/): ships `ore-tour` and `ore-spotlight`, the ready-made dialog/popover/bottom-sheet presentation layer for a beacon handle.\n- [Orbit](/orbit/): compute exact popover coordinates from `current.rect` when you build a custom presentation.\n- [Vault](/vault/): durable storage backends that plug into `BeaconStorage` with a six-line bridge.\n- [Ripple](/ripple/): the `Readable` signals every beacon handle exposes come from here.\n\n</div>\n\n<!-- markdownlint-enable MD025 MD033 MD060 -->\n",
5
+ "api": "---\ntitle: 'Beacon: API Reference'\ndescription: Public API of @vielzeug/beacon.\n---\n\n[[toc]]\n\n## API Overview\n\n| Symbol | Purpose | Execution mode | Common gotcha |\n| ------------------------- | ----------------------------------------------------------- | --------------------------------- | ------------------------------------------------------------------------- |\n| `createTour(config)` | Runs a sequential, resumable walkthrough over the app's DOM | Sync (storage reads may be async) | `start()` is a no-op when the stored record says the tour is completed |\n| `createCoachmark(config)` | Shows a single contextual hint anchored to one target | Sync (storage reads may be async) | `show()` is a no-op after `dismiss()` until `reset()` or a `version` bump |\n| `resolveTarget(target)` | Resolves any target to a viewport-space rect or `null` | Sync | Call it per position update; a scrolled target's rect changes |\n| `BeaconError` | Base class for all beacon-originated errors | — | Catch this to catch any beacon error |\n| `BeaconConfigError` | Invalid tour/coachmark configuration | Thrown at creation | Empty `steps` or duplicate step ids fail at `createTour()` |\n| `BeaconDisposedError` | Handle used after `dispose()` | Thrown on call | `dispose()` is idempotent; later method calls throw |\n\n## Package Entry Point\n\n| Import | Purpose |\n| ------------------ | -------------------------- |\n| `@vielzeug/beacon` | Complete public Beacon API |\n\n---\n\n## Core Functions\n\n### createTour\n\n```ts\nfunction createTour(config: BeaconTourConfig): BeaconTourHandle;\n```\n\nReturns a live handle for one guided tour. Throws `BeaconConfigError` when `id` is empty, `steps` is empty, or step ids are duplicated.\n\nParameters: see [`BeaconTourConfig`](#beacontourconfig).\n\n**Returns:** a [`BeaconTourHandle`](#beacontourhandle) that is also `Disposable`.\n\n**Example:**\n\n```ts\nimport { createTour } from '@vielzeug/beacon';\n\nconst tour = createTour({\n id: 'release-tour',\n steps: [{ id: 'intro', title: 'What changed' }],\n autoStart: true,\n});\n\ntour.status.subscribe(() => console.log(tour.status.value));\n```\n\nHandle methods:\n\n| Member | Behavior |\n| ---------------------------------- | ------------------------------------------------------------------------------------------------- |\n| `start()` | Begin or resume the tour. No-op when completed; throws after dispose. |\n| `next()` | Advance; past the last step, finishes the tour. |\n| `back()` | Return to the previous step. No-op on the first step. |\n| `goto(step)` | Jump to a step by index or id. Unknown values warn and no-op. |\n| `pause()` | Stop showing, keep progress so `start()` resumes it. |\n| `skip()` | End from a \"Skip\" control. Persisted as dismissed, reported with reason `'skip'`. |\n| `finish()` | Mark finished and stop showing. |\n| `dismiss()` | End by closing (Escape, backdrop). Persisted as dismissed. |\n| `reset()` | Discard persisted progress; the tour is eligible again. |\n| `tap(handler, options?)` | Observe [`BeaconTourEvent`](#beacontourentype)s; returns an unsubscribe, honors `options.signal`. |\n| `dispose()` / `[Symbol.dispose]()` | Release all listeners and observers. Idempotent. |\n| Handle properties: |\n\n| Member | Type | Notes |\n| ---------------- | ----------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| `current` | `Readable<BeaconCurrentStep>` | The snapshot to render; `step` is `null` while closed. Beacon re-publishes the rect on scroll, resize, and target layout changes while a step is active. |\n| `status` | `Readable<BeaconStatus>` | Lifecycle, persisted on every change. |\n| `eligible` | `Readable<boolean>` | Whether the tour should be offered to this user. |\n| `steps` | `readonly BeaconStep[]` | The configured steps, in order. |\n| `id` | `string` | The configured id (also the storage key suffix). |\n| `disposed` | `boolean` | Whether `dispose()` has run. |\n| `disposalSignal` | `AbortSignal` | Aborted on dispose; pass it to your own listeners. |\n\n---\n\n### createCoachmark\n\n```ts\nfunction createCoachmark(config: BeaconCoachmarkConfig): BeaconCoachmarkHandle;\n```\n\nReturns a handle for one contextual hint. Throws `BeaconConfigError` when `id` is empty.\n\nParameters: see [`BeaconCoachmarkConfig`](#beaconcoachmarkconfig).\n\n**Returns:** a [`BeaconCoachmarkHandle`](#beaconcoachmarkhandle) that is also `Disposable`.\n\n**Example:**\n\n```ts\nimport { createCoachmark } from '@vielzeug/beacon';\n\nconst hint = createCoachmark({\n id: 'share-hint',\n target: '[data-tour=\"share\"]',\n title: 'Share this board',\n});\n\nhint.show(); // waits for the target to mount if it is not on screen yet\n```\n\nHandle members:\n\n| Member | Behavior |\n| ------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| `show()` | Show the hint. No-op once dismissed without a newer version. |\n| `hide()` | Hide without recording it as seen; it can show again. |\n| `dismiss()` | Hide and record as seen permanently (until `reset()`/`version`). |\n| `reset()` | Discard the persisted record; the hint is eligible again. |\n| `tap(handler, options?)` | Observe `coachmark:show`, `coachmark:dismiss`, and common events. |\n| `current` | `Readable<BeaconCurrentStep>` — the hint as a single-step snapshot: `step` is the hint while visible, `null` while hidden. The rect stays live on scroll, resize, and target layout changes. |\n| `eligible` | `Readable<boolean>` — never seen, or superseded by a newer version. |\n\n---\n\n### resolveTarget\n\n```ts\nfunction resolveTarget(target: BeaconTarget): BeaconRect | null;\n```\n\nResolves a target to a viewport-space rect, or `null` when it is not available yet. Strings resolve through `document.querySelector`; functions are called and their result normalized.\n\n| Parameter | Type | Description |\n| --------- | -------------- | -------------------------------------------- |\n| `target` | `BeaconTarget` | Selector, `Element`, rect, or lazy resolver. |\n\n**Returns:** `BeaconRect | null` — shape-compatible with `getBoundingClientRect()` and `@vielzeug/orbit`'s `Rect`.\n\n**Example:**\n\n```ts\nimport { resolveTarget } from '@vielzeug/beacon';\n\nconst rect = resolveTarget('[data-tour=\"share\"]');\n// Anchor an orbit positioner from a virtual reference:\nconst reference = { getBoundingClientRect: () => resolveTarget('[data-tour=\"share\"]') ?? ZERO_RECT };\n```\n\n---\n\n## Types\n\n### BeaconTourConfig\n\n| Option | Type | Default | Description |\n| ----------------- | -------------------------- | ----------- | ------------------------------------------------------------------------------------------------------------- |\n| `id` | `string` | required | Unique identity; used as the storage key. |\n| `steps` | `readonly BeaconStep[]` | required | Every step, in order. At least one. |\n| `autoScroll` | `boolean` | `true` | Scroll each anchored step's target into view when it activates. |\n| `autoStart` | `boolean` | `false` | Start on the next microtask when eligible. |\n| `placement` | `BeaconPlacement` | `'auto'` | Default placement handed to the presentation layer. |\n| `presentation` | `BeaconPresentation` | `'auto'` | Default presentation handed to the presentation layer. |\n| `scrollOffset` | `number \\| (() => number)` | `0` | Pixels kept clear at the viewport top for a sticky header, read at scroll time. |\n| `storage` | `BeaconStorage` | memory-only | Progress persistence. |\n| `targetTimeoutMs` | `number` | `Infinity` | How long to wait for a step's target before skipping that step. `0` publishes immediately with a `null` rect. |\n| `version` | `number` | `1` | Bump to re-show a finished tour. |\n\n### BeaconCoachmarkConfig\n\n| Option | Type | Default | Description |\n| -------------- | -------------------- | ----------- | ------------------------------------------ |\n| `id` | `string` | required | Unique identity; used as the storage key. |\n| `target` | `BeaconTarget` | required | Where the hint points. |\n| `autoShow` | `boolean` | `true` | Show automatically when not yet seen. |\n| `data` | `unknown` | — | Free-form data for the presentation layer. |\n| `description` | `string` | — | Supporting copy. |\n| `placement` | `BeaconPlacement` | `'auto'` | Preferred side. |\n| `presentation` | `BeaconPresentation` | `'auto'` | Preferred presentation. |\n| `storage` | `BeaconStorage` | memory-only | Seen-record persistence. |\n| `title` | `string` | — | Headline copy. |\n| `version` | `number` | `1` | Bump to re-show a seen coachmark. |\n\n### BeaconStep\n\n```ts\ninterface BeaconStep {\n readonly data?: unknown;\n readonly description?: string;\n readonly id: string;\n readonly placement?: BeaconPlacement;\n readonly presentation?: BeaconPresentation;\n readonly target?: BeaconTarget;\n readonly title?: string;\n}\n```\n\nOne stop in a tour. `id` is stable and persisted — renaming it resets progress for that step. Omit `target` for a centered intro/outro step. `placement`/`presentation` override the tour-level defaults.\n\n### BeaconTarget\n\n```ts\ntype BeaconTarget = BeaconTargetResolver | BeaconRect | Element | string;\n```\n\nWhat a step points at. Selectors and elements anchor to real DOM; rects (or resolvers returning one) anchor to element-less geometry such as a canvas region.\n\n### BeaconTargetResolver\n\n```ts\ntype BeaconTargetResolver = () => Element | BeaconRect | null;\n```\n\nResolves a target lazily; `null` means \"not available yet\".\n\n### BeaconRect\n\n```ts\ninterface BeaconRect {\n readonly height: number;\n readonly width: number;\n readonly x: number;\n readonly y: number;\n}\n```\n\nA viewport-space rectangle in CSS pixels. Matches `@vielzeug/orbit`'s `Rect` shape.\n\n### BeaconPlacement\n\n```ts\ntype BeaconPlacement =\n | 'auto'\n | 'bottom'\n | 'bottom-end'\n | 'bottom-start'\n | 'left'\n | 'left-end'\n | 'left-start'\n | 'right'\n | 'right-end'\n | 'right-start'\n | 'top'\n | 'top-end'\n | 'top-start';\n```\n\nPreferred side for an anchored presentation; `'auto'` asks the positioning layer to choose.\n\n### BeaconPresentation\n\n```ts\ntype BeaconPresentation = 'auto' | 'bottom-sheet' | 'modal' | 'popover';\n```\n\nHow a step should be presented. `'auto'` lets the presentation layer pick by available space.\n\n### BeaconStatus\n\n```ts\ntype BeaconStatus = 'active' | 'completed' | 'dismissed' | 'idle' | 'paused';\n```\n\nTour lifecycle, persisted across visits. `'idle'` means never started.\n\n### BeaconProgress\n\n```ts\ninterface BeaconProgress {\n readonly status: BeaconStatus;\n readonly stepIndex: number;\n readonly version: number;\n}\n```\n\nThe persisted record for one tour or coachmark. `stepIndex` is `-1` before the first step. Records are validated on read; invalid records fall back to a fresh tour.\n\n### BeaconStorage\n\n```ts\ninterface BeaconStorage {\n get(key: string): Promise<BeaconProgress | null | undefined> | BeaconProgress | null | undefined;\n remove(key: string): Promise<unknown> | unknown;\n set(key: string, value: BeaconProgress): Promise<unknown> | unknown;\n}\n```\n\nMinimal storage contract; sync or async. Values are JSON. Keys arrive namespaced (`@vielzeug/beacon:<id>`). A `@vielzeug/vault` store plugs in with:\n\n```ts\nconst storage: BeaconStorage = {\n get: (key) => store.get('progress', key),\n remove: (key) => store.delete('progress', key),\n set: (key, value) => store.put('progress', { id: key, ...value }),\n};\n```\n\n### BeaconCurrentStep\n\n```ts\ninterface BeaconCurrentStep {\n readonly placement: BeaconPlacement;\n readonly presentation: BeaconPresentation;\n readonly rect: BeaconRect | null;\n readonly step: BeaconStep | null;\n readonly stepIndex: number;\n readonly totalSteps: number;\n}\n```\n\nThe snapshot a presentation layer renders from. `step` is `null` before start and after the tour ends; `rect` is `null` for centered steps and unresolved targets. While a step is active, Beacon keeps `rect` fresh on scroll, resize, and target layout changes — no app-side refresh call is needed.\n\n### BeaconTourEvent\n\n```ts\ntype BeaconTourEvent =\n | { readonly index: number; readonly step: BeaconStep; readonly type: 'step:start' }\n | { readonly index: number; readonly step: BeaconStep; readonly type: 'step:complete' }\n | { readonly reason: 'finish' | 'skip'; readonly type: 'tour:complete' }\n | { readonly type: 'tour:dismiss' }\n | { readonly status: BeaconStatus; readonly type: 'status-change' }\n | { readonly message: string; readonly type: 'target-missing' }\n | { readonly index: number; readonly message: string; readonly step: BeaconStep; readonly type: 'target-waiting' }\n | { readonly error: unknown; readonly message: string; readonly type: 'warning' }\n | { readonly type: 'dispose' };\n```\n\nNotable moments a tour reports through `tap()`. `target-missing` means a step was skipped because its target did not appear in time; `target-waiting` fires when a step begins waiting for its target to mount; `warning` carries storage or handler failures.\n\n### BeaconCoachmarkEvent\n\n```ts\ntype BeaconCoachmarkEvent =\n | { readonly type: 'coachmark:show' }\n | { readonly type: 'coachmark:dismiss' }\n | { readonly type: 'target-waiting' }\n | { readonly error: unknown; readonly message: string; readonly type: 'warning' }\n | { readonly type: 'dispose' };\n```\n\nNotable moments a coachmark reports through `tap()`.\n\n### BeaconTourHandle / BeaconCoachmarkHandle\n\nThe objects returned by `createTour()` and `createCoachmark()`, documented in [Core Functions](#core-functions). Both expose `disposalSignal`, `disposed`, `tap()`, `dispose()`, and `[Symbol.dispose]()`.\n\n## Errors\n\n| Error | Triggered by | Notes |\n| --------------------- | ----------------------------------------------------- | ------------------------------------------------------------------ |\n| `BeaconError` | Base for all beacon errors | `name` is the subclass name; `cause` supported via `ErrorOptions`. |\n| `BeaconConfigError` | Empty `id`, empty `steps`, missing/duplicate step ids | Thrown synchronously from `createTour()`/`createCoachmark()`. |\n| `BeaconDisposedError` | Calling handle methods after `dispose()` | `dispose()` itself never throws. |\n",
6
+ "usage": "---\ntitle: 'Beacon: Usage Guide'\ndescription: How to use @vielzeug/beacon.\n---\n\n[[toc]]\n\n## Basic Usage\n\nA tour is a config object plus a handle you drive from your own UI. The handle never renders anything: it publishes a `current` snapshot, and your card (or refine's `ore-tour`) renders it.\n\n```ts\nimport { createTour } from '@vielzeug/beacon';\n\nconst tour = createTour({\n id: 'product-tour',\n steps: [\n { id: 'welcome', title: 'Welcome', description: 'See what changed in this release.' },\n { id: 'nav', target: '[data-tour=\"nav\"]', title: 'Find everything', placement: 'bottom-start' },\n { id: 'share', target: '[data-tour=\"share\"]', title: 'Share the board', placement: 'bottom-end' },\n ],\n autoStart: true,\n});\n\n// Readable<T> from @vielzeug/ripple: the listener takes no arguments;\n// read .value inside it.\nconst unsubscribe = tour.current.subscribe(() => {\n const current = tour.current.value;\n if (!current.step) {\n hideCard();\n return;\n }\n renderCard({\n anchor: current.rect, // viewport rect of the target, null for centered steps\n index: current.stepIndex,\n placement: current.placement,\n presentation: current.presentation,\n title: current.step.title,\n total: current.totalSteps,\n });\n});\n\n// Wire your card's controls back to the handle.\ncardEl.addEventListener('next', () => tour.next());\ncardEl.addEventListener('back', () => tour.back());\ncardEl.addEventListener('skip', () => tour.skip());\n\n// On teardown:\nunsubscribe();\ntour.dispose();\n```\n\n`start()` is a no-op when the stored record says this user already completed the tour, so calling it on every page load is safe.\n\n## Steps and Targets\n\nA step without a `target` centers (intro/outro copy). A step with one anchors, and beacon waits for the target to exist before publishing the step — a tour can walk through lazily-mounted routes without any coordination code.\n\n```ts\nconst tour = createTour({\n id: 'deep-tour',\n steps: [\n // Selector: resolved with document.querySelector on activation.\n { id: 'sidebar', target: '[data-tour=\"sidebar\"]', title: 'Your libraries' },\n // Element: a ref your framework already holds.\n { id: 'editor', target: editorEl, title: 'The editor' },\n // Rect: geometry with no element, such as a canvas region.\n { id: 'chart', target: { height: 120, width: 240, x: 400, y: 300 }, title: 'This trend' },\n // Resolver: computed on every activation, and on scroll/resize while the step is active.\n {\n id: 'selection',\n target: () => window.getSelection()?.getRangeAt(0).getBoundingClientRect() ?? null,\n title: 'Your selection',\n },\n ],\n // Give up waiting after 4s and skip the step instead of hanging the tour.\n targetTimeoutMs: 4000,\n});\n```\n\nWith the default `targetTimeoutMs: Infinity`, a step whose target never mounts simply waits: the tour resumes the moment the user navigates to it. Set `0` to publish immediately with a `null` rect instead of waiting.\n\n## Persisting Progress\n\nWithout `storage`, progress lives in memory for the handle's lifetime only. Any object matching `BeaconStorage` makes it durable — values are JSON, keys are namespaced under `@vielzeug/beacon:`.\n\n```ts\nimport type { BeaconStorage } from '@vielzeug/beacon';\n\nconst storage: BeaconStorage = {\n get: (key) => JSON.parse(localStorage.getItem(key) ?? 'null'),\n remove: (key) => localStorage.removeItem(key),\n set: (key, value) => localStorage.setItem(key, JSON.stringify(value)),\n};\n\nconst tour = createTour({ id: 'product-tour', steps, storage });\n```\n\nAsync storage works too: `get` may return a promise, and the tour boots as soon as it resolves. A `set` that rejects is reported through `tap()` as a `warning` event, never thrown.\n\nRecords are validated on read, so a hand-edited or stale record falls back to a fresh tour instead of crashing.\n\n### Re-offering a finished tour\n\nBump `version` when the product changes and every user who finished the old tour becomes eligible again — users mid-tour on the old version keep their progress.\n\n```ts\nconst tour = createTour({\n id: 'product-tour',\n steps: newSteps,\n version: 2, // stored records at version 1 no longer block the tour\n});\n```\n\n`reset()` clears the persisted record for the current user, which is what a \"Replay tour\" or \"Reset onboarding\" control should call before `start()`.\n\n## Driving a Refine Presentation\n\n`@vielzeug/refine` ships `ore-tour` (a dialog with step footer, progress, and focus handling) and `ore-spotlight` (a decorative cutout that dims everything but the target). A beacon handle satisfies `ore-tour`'s `TourController` shape, so one call wires both directions:\n\n```ts\nimport type { OreTourElement } from '@vielzeug/refine/tour';\n\nconst card = document.querySelector<OreTourElement>('#tour-card');\n\nconst tour = createTour({ id: 'product-tour', presentation: 'auto', steps });\n\n// bind() renders `current` into the card's props and routes the card's\n// next/back/skip/finish/dismiss actions back into the handle.\nconst unbind = card.bind(tour);\n\ntour.start();\n\n// On teardown:\nunbind();\ntour.dispose();\n```\n\n`presentation: 'auto'` maps to a popover on wide viewports and a bottom sheet on narrow ones; ore-tour makes that switch itself and freezes the choice while the card is open, so a mid-step resize never rebuilds the card. Beacon keeps `current.rect` fresh on scroll, resize, and layout changes, so the anchored card follows the layout with no app-side listeners.\n\nApps that render their own markup read `current` directly instead (see [Basic Usage](#basic-usage)).\n\n## Coachmarks\n\nA coachmark is one hint anchored to one target, shown the first time the target appears and dismissed permanently after. Use it for the \"explain this button once\" moment a tour would be too heavy for — or as the follow-up a tour hands off to.\n\n```ts\nimport { createCoachmark } from '@vielzeug/beacon';\n\nconst share = createCoachmark({\n id: 'share-hint',\n target: '[data-tour=\"share\"]',\n title: 'Share this board',\n description: 'Send a link that opens straight into this view.',\n storage,\n});\n\n// Render from `current` — the same single-step snapshot a tour publishes.\nshare.current.subscribe(() => console.log(share.current.value.step !== null, share.current.value.rect));\n\n// The target may never appear on this page; show() waits for it.\nshare.show();\n\n// \"Got it\" button:\nshare.dismiss(); // records it as seen; never shows again for this version\n```\n\n`hide()` closes the hint without recording it, so it can appear again later; `dismiss()` is permanent until `reset()` or a `version` bump.\n\n## Events and Analytics\n\n`tap()` observes everything the engine considers notable. It returns an unsubscribe function and accepts an `AbortSignal`, so one controller can tear down every subscription.\n\n```ts\nconst controller = new AbortController();\n\ntour.tap(\n (event) => {\n switch (event.type) {\n case 'step:start':\n analytics.track('tour_step', { id: tour.id, step: event.step.id });\n break;\n case 'tour:complete':\n analytics.track('tour_complete', { id: tour.id, reason: event.reason });\n break;\n case 'target-missing':\n // A step's target never mounted: your selector drifted from the DOM.\n console.warn(event.message);\n break;\n case 'warning':\n console.warn(event.message, event.error);\n break;\n }\n },\n { signal: controller.signal },\n);\n```\n\n`target-missing` is the event to alert on: it means a step was skipped because its target did not appear within `targetTimeoutMs`, which is exactly how stale tours announce themselves.\n\n## Testing\n\nBeacon runs without a rendering layer, so plain DOM tests are enough. The default in-memory storage keeps each handle's progress isolated, and `jsdom`'s `getBoundingClientRect()` returns zero-sized rects, which is fine for step-order assertions.\n\n```ts\nimport { createTour } from '@vielzeug/beacon';\nimport { expect, test } from 'vitest';\n\ntest('walks steps in order and completes', () => {\n const tour = createTour({\n id: 'test-tour',\n steps: [{ id: 'a' }, { id: 'b' }],\n targetTimeoutMs: 0, // publish immediately even without targets\n });\n\n tour.start();\n expect(tour.current.value.step?.id).toBe('a');\n\n tour.next();\n expect(tour.current.value.step?.id).toBe('b');\n\n tour.next(); // past the last step finishes\n expect(tour.status.value).toBe('completed');\n});\n```\n\nFor persistence assertions, pass a `Map`-backed `BeaconStorage` and a second handle with the same `id` to check it resumes where the first stopped.\n\n## Framework Integration\n\nBeacon is framework-neutral: bridge the `Readable` snapshot with whatever primitive your framework already has.\n\n::: code-group\n\n```tsx [React]\nimport { useEffect, useState } from 'react';\nimport { createTour } from '@vielzeug/beacon';\n\nfunction useTourState(tour: ReturnType<typeof createTour>) {\n const [current, setCurrent] = useState(tour.current.value);\n useEffect(() => tour.current.subscribe(() => setCurrent(tour.current.value)), [tour]);\n return current;\n}\n```\n\n```vue [Vue 3]\n<script setup lang=\"ts\">\nimport { onScopeDispose, shallowRef } from 'vue';\nimport { createTour } from '@vielzeug/beacon';\n\nconst current = shallowRef(tour.current.value);\nconst unsubscribe = tour.current.subscribe(() => (current.value = tour.current.value));\nonScopeDispose(unsubscribe);\n</script>\n```\n\n```svelte [Svelte]\n<script lang=\"ts\">\n import { onDestroy } from 'svelte';\n import { readable } from 'svelte/store';\n\n const current = readable(tour.current.value, (set) => {\n set(tour.current.value);\n return tour.current.subscribe(() => set(tour.current.value));\n });\n onDestroy(() => tour.dispose());\n</script>\n```\n\n:::\n\n## Working with Other Vielzeug Libraries\n\n- **Refine**: `ore-tour` and `ore-spotlight` are the intended presentation layer; see [Driving a Refine Presentation](#driving-a-refine-presentation).\n- **Orbit**: `current.rect` matches orbit's `Rect` shape, so a custom card positions with `computePosition({ getBoundingClientRect: () => current.rect ?? ZERO_RECT }, floatingEl, middleware)`.\n- **Vault**: any vault store becomes a `BeaconStorage` with a six-line bridge (see the `BeaconStorage` docs in [API Reference](./api.md)).\n- **Ripple**: every handle exposes ripple `Readable`s; `createRipple().isBatchingObservable` semantics apply when you subscribe from effects.\n\n## Best Practices\n\n- Give every tour and coachmark a stable `id` — it is the storage key; treat renames as data migrations.\n- Keep tour configs in their own modules as plain data checked with `satisfies BeaconTourConfig`, so steps stay reviewable data, not component code.\n- Prefer `data-tour=\"...\"` attributes as target selectors: they survive class renames and read as intent.\n- Set a finite `targetTimeoutMs` in production and alert on `target-missing` to catch stale selectors.\n- Call `reset()` before `start()` for replay controls; never delete storage keys by hand.\n- Bump `version` when a tour's meaning changes, not when its copy is typo-fixed.\n- Dispose handles on teardown; `tap()` subscriptions with a shared `AbortSignal` clean up in one call.\n",
7
7
  "examples": "---\ntitle: 'Beacon: Examples'\ndescription: Practical examples and recipes for @vielzeug/beacon.\n---\n\n## Examples\n\n- [Getting Started](./examples/getting-started.md)\n"
8
8
  },
9
9
  "examples": [
@@ -1,9 +1,9 @@
1
1
  {
2
2
  "apiSource": "export {\n ClockworkDefinitionError,\n ClockworkError,\n ClockworkSnapshotError,\n ClockworkTransitionLimitError,\n} from './errors.js';\nexport { defineMachine } from './interpret.js';\nexport type {\n Actor,\n ActorErrorContext,\n ActorOptions,\n ActorTapEvent,\n After,\n Effect,\n EffectArgs,\n EventByType,\n EventType,\n Guard,\n Invoke,\n InvokeArgs,\n Machine,\n MachineConfig,\n MachineEvent,\n MachineSnapshot,\n Reducer,\n StateNode,\n Transition,\n TransitionInput,\n TransitionResult,\n} from './types.js';\n",
3
3
  "docs": {
4
- "index": "---\ntitle: 'Clockwork: Typed finite state machines for TypeScript'\ndescription: Framework-neutral typed state machines with pure transitions, actor-owned runtime work, timers, invokes, and explicit effects.\npackage: clockwork\ncategory: state\nkeywords: [state-machine, finite-state, typed, actor, async-tasks]\nrelated: [herald, ripple, ward]\nexports: [defineMachine, ClockworkError, Machine, Actor, MachineConfig, MachineSnapshot]\nenvironments: [browser, node, ssr, deno]\n---\n\n<!-- markdownlint-disable MD025 MD033 MD060 -->\n\n<PackageHero package=\"clockwork\" />\n\n## Why Clockwork?\n\nApplication workflows often mix state changes with timers, requests, rendering, and cleanup. Clockwork keeps transition logic pure while each disposable actor owns runtime work. You can test state decisions without starting effects or invokes.\n\n```ts\nimport { defineMachine } from '@vielzeug/clockwork';\n\n// Before\nif (status === 'idle') status = 'loading';\nfetchItems().then((items) => {\n status = 'ready';\n data = items;\n});\n\n// After\ntype Event = { type: 'FETCH' } | { items: string[]; type: 'DONE' };\nconst machine = defineMachine<{ items: string[] }, Event>()({\n context: { items: [] },\n initial: 'idle',\n states: {\n idle: { on: { FETCH: { target: 'loading' } } },\n loading: {\n invoke: [{\n src: ({ signal }) => fetch('/api/items', { signal }).then((response) => response.json() as Promise<string[]>),\n onDone: ({ result }) => ({ items: result, type: 'DONE' }),\n }],\n on: { DONE: { reduce: ({ event }) => ({ items: event.items }), target: 'ready' } },\n },\n ready: {},\n },\n});\n```\n\n| Feature | Clockwork | XState | Zustand |\n| --- | --- | --- | --- |\n| Bundle size | <PackageInfo package=\"clockwork\" type=\"size\" /> | Larger actor/statechart runtime | Smaller store runtime |\n| Zero dependencies | <ore-icon name=\"check\" size=\"16\"></ore-icon> | <ore-icon name=\"x\" size=\"16\"></ore-icon> | <ore-icon name=\"check\" size=\"16\"></ore-icon> |\n| Pure transition API | <ore-icon name=\"check\" size=\"16\"></ore-icon> | <ore-icon name=\"triangle-alert\" size=\"16\"></ore-icon> Statechart-focused | <ore-icon name=\"x\" size=\"16\"></ore-icon> |\n| Owned cancellation | <ore-icon name=\"check\" size=\"16\"></ore-icon> Actor disposal | <ore-icon name=\"check\" size=\"16\"></ore-icon> | <ore-icon name=\"x\" size=\"16\"></ore-icon> |\n| Framework coupling | <ore-icon name=\"check\" size=\"16\"></ore-icon> None | <ore-icon name=\"check\" size=\"16\"></ore-icon> None | <ore-icon name=\"check\" size=\"16\"></ore-icon> None |\n\n<div class=\"decision-callout\">\n\n**Use Clockwork when** your feature has explicit workflow states, cancellable work, or effects that must run after a state commit.\n\n**Consider XState when** you need statecharts, visual tooling, or its broader actor ecosystem.\n\n</div>\n\n## Installation\n\n::: code-group\n\n```sh [pnpm]\npnpm add @vielzeug/clockwork\n```\n\n```sh [npm]\nnpm install @vielzeug/clockwork\n```\n\n```sh [yarn]\nyarn add @vielzeug/clockwork\n```\n\n:::\n\n## Quick Start\n\nDefine the context and event union, create an actor, observe its snapshot, then dispose it when its owner ends.\n\n```ts\nimport { defineMachine } from '@vielzeug/clockwork';\n\ntype Event = { type: 'DEC' } | { type: 'INC' };\n\nconst counter = defineMachine<{ count: number }, Event>()({\n context: { count: 0 },\n initial: 'idle',\n states: {\n idle: {\n on: {\n DEC: { reduce: ({ context }) => ({ count: context.count - 1 }), target: 'idle' },\n INC: { reduce: ({ context }) => ({ count: context.count + 1 }), target: 'idle' },\n },\n },\n },\n});\n\nusing actor = counter.createActor();\nactor.subscribe((snapshot) => console.log(snapshot));\nactor.send({ type: 'INC' });\n// { context: { count: 1 }, state: 'idle' }\n```\n\n## Features\n\n<div class=\"features-grid\">\n\n- **`defineMachine()`**: defines one flat, typed machine and rejects the footguns types cannot express.\n- **`machine.transition()`**: evaluates a transition without actor runtime work.\n- **`machine.createActor()`**: creates isolated, disposable runtime ownership.\n- **`reduce`**: returns a replacement context from a transition.\n- **`effects`**: run after commit and stable subscriber notification.\n- **`invoke`**: runs cancellable asynchronous work on state entry.\n- **`after`**: schedules cancellable delayed transitions.\n- **`actor.snapshot`**: exposes the current shallow-frozen state/context value.\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- [Herald](/herald/): publish events between independent actors without coupling machine definitions.\n- [Ripple](/ripple/): bridge actor snapshots into a reactive graph when you need fine-grained rendering.\n- [Ward](/ward/): call authorization predicates from transition guards.\n\n</div>\n\n<!-- markdownlint-enable MD025 MD033 MD060 -->\n",
5
- "api": "---\ntitle: 'Clockwork: API Reference'\ndescription: Reference for Clockwork machine definitions, actors, and types.\n---\n\n[[toc]]\n\n## API Overview\n\n| Symbol | Purpose | Execution mode | Common gotcha |\n| --- | --- | --- | --- |\n| `defineMachine()` | Define a typed flat machine | Sync | Call the generic factory before supplying the definition |\n| `Machine.transition()` | Resolve a pure next snapshot | Sync | Does not run effects, invokes, or timers |\n| `Machine.createActor()` | Create a runtime owner | Sync | Fresh and restored actors have different entry behavior |\n| `Actor.send()` | Dispatch an event | Sync | Returns `void`; re-entrant events queue internally |\n| `Actor.subscribe()` | Observe committed snapshots | Sync | Observes only; it does not trace sends or errors |\n| `Actor.tap()` | Observe transitions, ignores, errors, disposal | Sync | Handler errors are swallowed; it cannot control disposition |\n| `ClockworkError` | Report definition, snapshot, and transition-limit failures | Sync | Narrow with the error subtypes, not message text |\n\n## Package Entry Points\n\n| Import | Purpose |\n| --- | --- |\n| `@vielzeug/clockwork` | Machine factory, actor runtime, errors, and types |\n\n## Exported Surface\n\nCallback parameter types are inferred for inline definitions. Clockwork also exports reusable callback, transition, state-node, actor-option, result, and error-context types for shared helpers.\n\n| Primary export | Kind |\n| --- | --- |\n| `defineMachine` | Function |\n| `Machine` | Type |\n| `Actor` | Type |\n| `MachineSnapshot` | Type |\n| `MachineConfig` | Type |\n| `ClockworkError` | Class |\n| `ClockworkDefinitionError` | Class |\n| `ClockworkSnapshotError` | Class |\n| `ClockworkTransitionLimitError` | Class |\n\nAdditional exported types: `ActorErrorContext`, `ActorOptions`, `ActorTapEvent`, `After`, `Effect`, `EffectArgs`, `EventByType`, `EventType`, `Guard`, `Invoke`, `InvokeArgs`, `MachineEvent`, `Reducer`, `StateNode`, `Transition`, `TransitionInput`, and `TransitionResult`.\n\n## Core Functions\n\n### `defineMachine()`\n\n```ts\nfunction defineMachine<\n Context extends Record<string, unknown> = Record<string, never>,\n Event extends MachineEvent = MachineEvent,\n>(): <State extends string>(definition: MachineConfig<State, Context, Event>) => Machine<State, Context, Event>;\n```\n\nReturns a factory that accepts a typed flat machine definition. The definition is trusted as typed: targets, callback shapes, and state keys are compiler-checked. `defineMachine()` rejects only what types cannot express: timer delays outside `0..2,147,483,647` ms, empty transition arrays, and a context that is not a plain record (arrays, `null`, and class instances fail). Omit `context` only when the context type has no keys.\n\nThe curried signature is required so TypeScript can infer `State` from the `states` object while you explicitly provide `Context` and `Event`. TypeScript does not support partial type argument inference, so the state-key union cannot be inferred in a single non-curried call when the context and event generics are explicit.\n\n**Returns:** A definition function that returns `Machine`.\n\n**Example:**\n\n```ts\nimport { defineMachine } from '@vielzeug/clockwork';\n\ntype Event = { type: 'START' };\n\nconst machine = defineMachine<Record<string, never>, Event>()({\n initial: 'idle',\n states: { idle: { on: { START: { target: 'running' } } }, running: {} },\n});\n```\n\nThrows `ClockworkDefinitionError` when a definition carries a timer delay outside the platform range, an empty transition array, or a context that is not a plain record.\n\n---\n\n### `Actor.subscribe()`\n\n```ts\nsubscribe(listener: (snapshot: MachineSnapshot<State, Context>) => void): () => void;\n```\n\nSubscribes to future committed actor snapshots. It does not run immediately; read `actor.snapshot` for the initial value. Returns an unsubscribe function. Subscriber failures are reported through `tap()` as `error` events with `phase: 'subscriber'` without stopping remaining subscribers, declared effects, or the actor.\n\n**Example:**\n\n```ts\nimport { defineMachine } from '@vielzeug/clockwork';\n\nconst machine = defineMachine<Record<string, never>, { type: 'NEXT' }>()({\n initial: 'idle',\n states: { idle: { on: { NEXT: { target: 'idle' } } } },\n});\n\nconst actor = machine.createActor();\nconst stop = actor.subscribe((snapshot) => console.debug(snapshot));\nactor.send({ type: 'NEXT' });\nstop();\nactor.dispose();\n```\n\n## Machine Methods\n\n### `machine.transition()`\n\n```ts\ntransition(\n snapshot: MachineSnapshot<State, Context>,\n event: Event,\n): TransitionResult<State, Context>;\n```\n\nResolves a snapshot for one user event without actor runtime work.\n\n| Parameter | Type | Description |\n| --- | --- | --- |\n| `snapshot` | `MachineSnapshot<State, Context>` | Input state and context |\n| `event` | `Event` | User event to evaluate |\n\n**Returns:** A `TransitionResult` with `transition` or `ignored` type.\n\n**Example:**\n\n```ts\nconst result = machine.transition(machine.initialSnapshot, { type: 'START' });\n```\n\n---\n\n### `machine.can()`\n\n```ts\ncan(snapshot: MachineSnapshot<State, Context>, event: Event): boolean;\n```\n\nReturns whether a transition exists and its guard passes.\n\n**Returns:** `true` when the supplied snapshot accepts the event.\n\n---\n\n### `machine.createActor()`\n\n```ts\ncreateActor(options?: ActorOptions<State, Context>): Actor<State, Context, Event>;\n```\n\nCreates an independent actor for event dispatch, timers, invokes, effects, subscriptions, and disposal. A fresh actor starts the initial state's entry effects and resources. An actor restored with `options.snapshot` starts only the restored state's resources: invokes and timers, not entry effects.\n\n| Parameter | Type | Description |\n| --- | --- | --- |\n| `options.snapshot` | `MachineSnapshot<State, Context>` | Optional restored actor snapshot |\n\n**Returns:** Disposable `Actor`.\n\n**Example:**\n\n```ts\nconst actor = machine.createActor({ snapshot: { context: {}, state: 'idle' } });\n```\n\n## Actor Methods\n\n### `actor.send()`\n\n```ts\nsend(event: Event): void;\n```\n\nDispatches a user event to the current actor state. Events sent while the actor is processing queue and flush synchronously; sends to a disposed actor are ignored. For active actors, malformed events without a string `type` log a development warning and are ignored. Valid but unhandled event types are ignored without a warning. Use `actor.snapshot` after sending to read the current snapshot.\n\n**Returns:** Nothing.\n\n---\n\n### `actor.can()`\n\n```ts\ncan(event: Event): boolean;\n```\n\nReturns whether the current actor snapshot accepts an event. A guard failure is reported as a fatal transition error, disposes the actor, and returns `false`. Returns `false` after disposal.\n\n**Returns:** Boolean transition availability.\n\n---\n\n### `actor.subscribe()`\n\n```ts\nsubscribe(listener: (snapshot: MachineSnapshot<State, Context>) => void): () => void;\n```\n\nRegisters a listener for committed snapshots. The listener does not run immediately.\n\n**Returns:** An unsubscribe function.\n\n---\n\n### `actor.dispose()`\n\n```ts\ndispose(): void;\n[Symbol.dispose](): void;\n```\n\nCancels timers and invokes, clears queued events, tappers and listeners, emits a final `dispose` tap event, and aborts `disposalSignal`.\n\n**Returns:** Nothing. Idempotent.\n\n### `actor.tap()`\n\n```ts\ntap(\n handler: (event: ActorTapEvent<State, Context, Event>) => void,\n options?: { signal?: AbortSignal },\n): () => void;\n```\n\nObserves actor lifecycle without affecting machine behavior: `{ type: 'transition' | 'ignored' }` carries the event and resulting snapshot, `{ type: 'error' }` carries the error with its `phase`/`state`/`event` context, and `{ type: 'dispose' }` is the final event before tappers are cleared. Handler errors are swallowed. Returns an unsubscribe function.\n\n```ts\nactor.tap((event) => {\n if (event.type === 'error') console.error(event.phase, event.state, event.error);\n});\n```\n\n## Error Handling Policy\n\nMachine-execution failures are fail-stop: guard, reducer, effect, invoke, timer, and queued-transition-limit failures dispose the actor. The queued-transition limit is a fixed internal guard against runaway loops, not a configurable option. Subscriber failures are observational: Clockwork reports them through `tap()`, continues the stable subscriber snapshot and declared effects, and keeps the actor active. Tap handlers cannot control disposition.\n\nSubscriber iteration uses a stable snapshot: listeners added during notification begin with the next transition.\n\n## Types\n\n### `MachineSnapshot<State, Context>`\n\n```ts\ntype MachineSnapshot<State extends string, Context extends Record<string, unknown>> = {\n readonly context: Readonly<Context>;\n readonly state: State;\n};\n```\n\nClockwork shallow-clones and freezes each snapshot and its context record. Nested context values remain caller-owned and are not deep-frozen.\n\n### `MachineConfig<State, Context, Event>`\n\n```ts\ntype MachineConfig<State extends string, Context extends Record<string, unknown>, Event extends MachineEvent> =\n (keyof Context extends never ? { readonly context?: Context } : { readonly context: Context }) & {\n readonly initial: State;\n readonly states: Record<State, StateNode<State, Context, Event>>;\n };\n```\n\nA flat machine definition. State nodes cannot contain child states. The `StateNode`, `Transition`, `After`, `Invoke`, `Effect`, `Guard`, and `Reducer` shapes are inferred inline and exported for reusable helpers. `After.delay` must be finite and between 0 and 2,147,483,647 milliseconds; delayed guards and reducers receive `event: undefined`.\n\n### `Actor<State, Context, Event>`\n\n```ts\ntype Actor<State extends string, Context extends Record<string, unknown>, Event extends MachineEvent> = {\n [Symbol.dispose](): void;\n can(event: Event): boolean;\n readonly disposalSignal: AbortSignal;\n dispose(): void;\n readonly disposed: boolean;\n send(event: Event): void;\n readonly snapshot: MachineSnapshot<State, Context>;\n subscribe(listener: (snapshot: MachineSnapshot<State, Context>) => void): () => void;\n tap(\n handler: (event: ActorTapEvent<State, Context, Event>) => void,\n options?: { readonly signal?: AbortSignal },\n ): () => void;\n};\n```\n\nAn actor's `snapshot` is the current plain readonly snapshot.\n\n### `ActorTapEvent<State, Context, Event>`\n\n```ts\ntype ActorTapEvent<State, Context, Event> =\n | (ActorErrorContext<State, Event> & { readonly error: unknown; readonly type: 'error' })\n | {\n readonly event: Event | undefined;\n readonly snapshot: MachineSnapshot<State, Context>;\n readonly type: 'transition' | 'ignored';\n }\n | { readonly type: 'dispose' };\n\ntype ActorErrorContext<State, Event> = {\n readonly event?: Event;\n readonly phase: 'effect' | 'invoke' | 'subscriber' | 'transition';\n readonly state: State;\n};\n```\n\n### `Machine<State, Context, Event>`\n\n```ts\ntype Machine<State extends string, Context extends Record<string, unknown>, Event extends MachineEvent> = {\n can(snapshot: MachineSnapshot<State, Context>, event: Event): boolean;\n createActor(options?: ActorOptions<State, Context>): Actor<State, Context, Event>;\n readonly initialSnapshot: MachineSnapshot<State, Context>;\n transition(snapshot: MachineSnapshot<State, Context>, event: Event): TransitionResult<State, Context>;\n};\n```\n\nA reusable machine. Its state and event lookups are own-property guarded, so unknown or prototype-colliding names such as `__proto__` or `constructor` are safely ignored when no transition exists.\n\n## Errors\n\n### `ClockworkError`\n\n`ClockworkError` is the base class for every Clockwork failure, with `details` and standard `Error` fields. Use `instanceof ClockworkError` to narrow an unknown error. Three subtypes carry the failure category:\n\n| Subtype | Raised when |\n| --- | --- |\n| `ClockworkDefinitionError` | `defineMachine()` rejects a definition footgun types cannot express: a timer delay outside `0..2,147,483,647` ms, an empty transition array, or a context that is not a plain record |\n| `ClockworkSnapshotError` | A snapshot, its context, or a reducer result is not a plain object record, or names an undeclared state |\n| `ClockworkTransitionLimitError` | An actor exceeds the fixed queued-transition limit and disposes itself |\n\n```ts\nif (error instanceof ClockworkDefinitionError) {\n console.error(error.message, error.details);\n}\n```\n",
6
- "usage": "---\ntitle: 'Clockwork: Usage Guide'\ndescription: Build deterministic state machines with pure transitions and actor-owned runtime work.\n---\n\n[[toc]]\n\n## Basic Usage\n\nCall `defineMachine<Context, Event>()` first to bind context and event types; the returned definition function infers state labels from `states`. Context is optional only when its type has no keys. Create one actor for each independently owned workflow.\n\n```ts\nimport { defineMachine } from '@vielzeug/clockwork';\n\ntype Event = { type: 'TOGGLE' };\n\nconst machine = defineMachine<Record<string, never>, Event>()({\n initial: 'on',\n states: {\n off: { on: { TOGGLE: { target: 'on' } } },\n on: { on: { TOGGLE: { target: 'off' } } },\n },\n});\n\nconst actor = machine.createActor();\nactor.send({ type: 'TOGGLE' });\nconsole.log(actor.snapshot.state); // 'off'\nactor.dispose();\n```\n\nDispose actors when a feature, request, or test ends. You can use `using` when the surrounding runtime supports `Symbol.dispose`.\n\n```ts\nusing actor = machine.createActor();\nactor.send({ type: 'TOGGLE' });\n```\n\n## Context reducers\n\nA reducer receives readonly context and returns the next context. Clockwork shallow-clones and freezes each snapshot and context record; nested values are not deep-cloned, so make explicit nested replacements.\n\n```ts\ntype Event = { type: 'DEC' } | { type: 'INC' } | { type: 'RESET' };\n\nconst counter = defineMachine<{ count: number }, Event>()({\n context: { count: 0 },\n initial: 'idle',\n states: {\n idle: {\n on: {\n DEC: { reduce: ({ context }) => ({ count: context.count - 1 }), target: 'idle' },\n INC: { reduce: ({ context }) => ({ count: context.count + 1 }), target: 'idle' },\n RESET: { reduce: () => ({ count: 0 }), target: 'idle' },\n },\n },\n },\n});\n```\n\nKeep reducers pure. Make nested copies yourself when nested data changes.\n\n```ts\nSAVE: {\n reduce: ({ context, event }) => ({\n ...context,\n profile: { ...context.profile, name: event.name },\n }),\n target: 'editing',\n}\n```\n\n## Guards\n\nGuards decide whether a transition can run. They receive readonly context and the matching event. For several choices, use an ordered array; the first passing guard wins.\n\n```ts\nPAY: [\n {\n guard: ({ context }) => context.balance >= context.total,\n reduce: ({ context }) => ({ ...context, balance: context.balance - context.total }),\n target: 'success',\n },\n { target: 'insufficientFunds' },\n]\n```\n\nCall `actor.can(event)` for the current actor snapshot or `machine.can(snapshot, event)` for an arbitrary snapshot.\n\n## Pure transitions\n\n`machine.transition()` enables isolated unit tests and decision UIs. It returns the unchanged snapshot with `type: 'ignored'` when no transition matches; it does not expose or run effects.\n\n```ts\nconst result = counter.transition(\n { context: { count: 3 }, state: 'idle' },\n { type: 'INC' },\n);\n\nif (result.type === 'transition') {\n console.log(result.snapshot.context.count); // 4\n}\n```\n\n## Effects\n\nEntry, exit, and transition effects run only through an actor. The actor commits, establishes target-state timers and invokes, notifies a stable subscriber snapshot, then runs exit, transition, and entry effects. Subscriber failures are reported but cannot skip declared effects. Effects cannot change context; send a regular event for another state change.\n\n```ts\ntype WorkflowEvent = { type: 'SUBMIT' };\nconst workflow = defineMachine<{ orderId: string }, WorkflowEvent>()({\n context: { orderId: '' },\n initial: 'draft',\n states: {\n draft: {\n on: {\n SUBMIT: {\n effects: [({ context }) => console.debug('submitted', context)],\n target: 'submitted',\n },\n },\n },\n submitted: { entry: [({ context }) => console.log(`Submitted ${context.orderId}`)] },\n },\n});\n```\n\nEffects receive `context`, the triggering `event` (or `undefined` for initial entry), actor `send`, and the actor lifetime `signal`.\n\n## Async invokes\n\nInvokes start on state entry. `src` gets readonly entry context, the triggering event or `undefined`, and an `AbortSignal`. `onDone` or `onError` map settlement to ordinary events. All invokes are cancelled when the actor exits the state or disposes.\n\n```ts\ntype LoadEvent =\n | { type: 'FETCH' }\n | { items: string[]; type: 'SUCCESS' }\n | { message: string; type: 'FAILURE' }\n | { type: 'RETRY' };\n\nconst loader = defineMachine<{ error: string; items: string[] }, LoadEvent>()({\n context: { error: '', items: [] },\n initial: 'idle',\n states: {\n idle: { on: { FETCH: { target: 'loading' } } },\n loading: {\n invoke: [{\n src: async ({ signal }) => {\n const response = await fetch('/api/items', { signal });\n if (!response.ok) throw new Error(`HTTP ${response.status}`);\n return response.json() as Promise<string[]>;\n },\n onDone: ({ result }) => ({ items: result, type: 'SUCCESS' }),\n onError: ({ error }) => ({ message: String(error), type: 'FAILURE' }),\n }],\n on: {\n FAILURE: { reduce: ({ event }) => ({ error: event.message, items: [] }), target: 'error' },\n SUCCESS: { reduce: ({ event }) => ({ error: '', items: event.items }), target: 'ready' },\n },\n },\n ready: {},\n error: { on: { RETRY: { target: 'loading' } } },\n },\n});\n```\n\n## Delayed transitions\n\n`after` starts timers on state entry and cancels them on exit or disposal. Delay must be finite and between 0 and 2,147,483,647 milliseconds. Guards and reducers receive `event: undefined`; a user event with `type: '$after'` remains a normal user event.\n\n```ts\ntype NotificationEvent = { type: 'DISMISS' } | { message: string; type: 'SHOW' };\nconst notification = defineMachine<{ message: string }, NotificationEvent>()({\n context: { message: '' },\n initial: 'hidden',\n states: {\n hidden: { on: { SHOW: { reduce: ({ event }) => ({ message: event.message }), target: 'visible' } } },\n visible: {\n after: [{ delay: 5_000, target: 'hidden' }],\n on: { DISMISS: { target: 'hidden' } },\n },\n },\n});\n```\n\n## Snapshot observation and persistence\n\n`actor.snapshot` is the current shallow-frozen `MachineSnapshot`; read it directly rather than calling a snapshot method. Use `subscribe()` to integrate a state library or persist future committed snapshots. Fresh actors run their initial entry effects and resources; restored actors start only the restored state's invokes and timers, not its entry effects.\n\n```ts\nconst stored = sessionStorage.getItem('wizard');\nconst actor = machine.createActor({\n snapshot: stored ? JSON.parse(stored) : undefined,\n});\n\nconst stopSaving = actor.subscribe((snapshot) => {\n sessionStorage.setItem('wizard', JSON.stringify(snapshot));\n});\n\nconsole.log(actor.snapshot);\nstopSaving();\nactor.dispose();\n```\n\nValidate untrusted persisted data before passing it to `createActor()`. Clockwork validates the restored state name but cannot validate application-specific context fields.\n\n## Error handling\n\nGuard, reducer, effect, invoke, timer, and transition-limit failures dispose the actor. The transition limit is a fixed internal guard against runaway loops. Subscriber failures are reported but do not dispose the actor or interrupt remaining subscribers and declared effects.\n\nUse `tap()` to observe either class without controlling disposition. The `error` event carries the error and its `phase` and `state`; handler failures are swallowed so observation cannot alter actor behavior.\n\n```ts\nconst actor = machine.createActor();\nactor.tap((event) => {\n if (event.type === 'error') console.error({ error: event.error, event: event.event, phase: event.phase, state: event.state });\n});\n```\n\nFor fatal machine failures, the `error` event is emitted before disposal. Without a tap handler, fatal failures dispose silently.\n\n## Debugging\n\nUse `actor.subscribe()` to observe committed snapshots during development. It observes snapshots only; it does not trace dispatched events or runtime errors.\n\n```ts\nconst actor = machine.createActor();\nconst stop = actor.subscribe((snapshot) => console.debug(snapshot));\nactor.send({ type: 'NEXT' });\nstop();\nactor.dispose();\n```\n\nFor richer inspection, route snapshots to application devtools. Clockwork intentionally has no internal trace buffer.\n\n## Flat state maps\n\nClockwork has flat state IDs. Prefer explicit states such as `editingDraft` and `editingSaving`, or compose several actors when domains have independent lifecycles.\n\n## SSR\n\nReuse one machine definition, but create and dispose an actor per request. Never share an actor across concurrent requests.\n\n## Testing\n\nTest deterministic state behavior through `machine.transition()`. Create actors only for timers, invokes, effects, queueing, subscriptions, or disposal behavior.\n\n```ts\nimport { expect, test } from 'vitest';\n\ntest('increments without an actor', () => {\n const result = counter.transition(\n { context: { count: 2 }, state: 'idle' },\n { type: 'INC' },\n );\n\n expect(result).toMatchObject({\n snapshot: { context: { count: 3 }, state: 'idle' },\n type: 'transition',\n });\n});\n```\n\n## Framework Integration\n\nBridge the current actor snapshot into renderer state through one subscription. Dispose that subscription with component lifecycle.\n\n::: code-group\n\n```ts [React]\nimport { useSyncExternalStore } from 'react';\n\nfunction useActor<Value>(actor: { readonly snapshot: Value; subscribe(listener: (snapshot: Value) => void): () => void }) {\n return useSyncExternalStore(\n (notify) => actor.subscribe(() => notify()),\n () => actor.snapshot,\n () => actor.snapshot,\n );\n}\n```\n\n```ts [Vue 3]\nimport { onUnmounted, shallowRef } from 'vue';\n\nconst snapshot = shallowRef(actor.snapshot);\nconst stop = actor.subscribe((next) => (snapshot.value = next));\nonUnmounted(stop);\n```\n\n```ts [Svelte]\nimport { onDestroy } from 'svelte';\n\nlet snapshot = actor.snapshot;\nconst stop = actor.subscribe((next) => (snapshot = next));\nonDestroy(stop);\n```\n\n:::\n\n## Working with Other Vielzeug Libraries\n\nUse Herald when separate actors exchange application events. Bridge Clockwork snapshots into Ripple only at a UI or application boundary.\n\n```ts\nimport { createBus } from '@vielzeug/herald';\n\nconst bus = createBus<{ REFRESH: void }>();\nbus.on('REFRESH', () => actor.send({ type: 'FETCH' }));\n```\n\n## Best Practices\n\n- Define context and event unions with `defineMachine<Context, Event>()`.\n- Return replacement context from reducers; do not rely on runtime copying or freezing.\n- Keep guards and reducers pure.\n- Use actors for effects, timers, invokes, subscriptions, and cancellation.\n- Read the current snapshot from `actor.snapshot`, not a wrapper value.\n- Validate persisted context before restoring a snapshot.\n- Dispose every actor at its ownership boundary.\n- Route runtime failures through the `tap()` observer for logging and diagnostics.\n",
4
+ "index": "---\ntitle: 'Clockwork: Typed finite state machines for TypeScript'\ndescription: Framework-neutral typed state machines with pure transitions, actor-owned runtime work, timers, invokes, and explicit effects.\npackage: clockwork\ncategory: state\nkeywords: [state-machine, finite-state, typed, actor, async-tasks]\nrelated: [herald, ripple, ward]\nexports: [defineMachine, ClockworkError, Machine, Actor, MachineConfig, MachineSnapshot]\nenvironments: [browser, node, ssr, deno]\n---\n\n<!-- markdownlint-disable MD025 MD033 MD060 -->\n\n<PackageHero package=\"clockwork\" />\n\n## Why Clockwork?\n\nApplication workflows often mix state changes with timers, requests, rendering, and cleanup. Clockwork keeps transition logic pure while each disposable actor owns runtime work. You can test state decisions without starting effects or invokes.\n\n```ts\nimport { defineMachine } from '@vielzeug/clockwork';\n\n// Before\nif (status === 'idle') status = 'loading';\nfetchItems().then((items) => {\n status = 'ready';\n data = items;\n});\n\n// After\ntype Event = { type: 'FETCH' } | { items: string[]; type: 'DONE' };\nconst machine = defineMachine<{ items: string[] }, Event>()({\n context: { items: [] },\n initial: 'idle',\n states: {\n idle: { on: { FETCH: { target: 'loading' } } },\n loading: {\n invoke: [\n {\n src: ({ signal }) => fetch('/api/items', { signal }).then((response) => response.json() as Promise<string[]>),\n onDone: ({ result }) => ({ items: result, type: 'DONE' }),\n },\n ],\n on: { DONE: { reduce: ({ event }) => ({ items: event.items }), target: 'ready' } },\n },\n ready: {},\n },\n});\n```\n\n| Feature | Clockwork | XState | Zustand |\n| ------------------- | ----------------------------------------------------------- | ------------------------------------------------------------------------ | ------------------------------------------------- |\n| Bundle size | <PackageInfo package=\"clockwork\" type=\"size\" /> | Larger actor/statechart runtime | Smaller store runtime |\n| Zero dependencies | <ore-icon name=\"check\" size=\"16\"></ore-icon> | <ore-icon name=\"x\" size=\"16\"></ore-icon> | <ore-icon name=\"check\" size=\"16\"></ore-icon> |\n| Pure transition API | <ore-icon name=\"check\" size=\"16\"></ore-icon> | <ore-icon name=\"triangle-alert\" size=\"16\"></ore-icon> Statechart-focused | <ore-icon name=\"x\" size=\"16\"></ore-icon> |\n| Owned cancellation | <ore-icon name=\"check\" size=\"16\"></ore-icon> Actor disposal | <ore-icon name=\"check\" size=\"16\"></ore-icon> | <ore-icon name=\"x\" size=\"16\"></ore-icon> |\n| Framework coupling | <ore-icon name=\"check\" size=\"16\"></ore-icon> None | <ore-icon name=\"check\" size=\"16\"></ore-icon> None | <ore-icon name=\"check\" size=\"16\"></ore-icon> None |\n\n<div class=\"decision-callout\">\n\n**Use Clockwork when** your feature has explicit workflow states, cancellable work, or effects that must run after a state commit.\n\n**Consider XState when** you need statecharts, visual tooling, or its broader actor ecosystem.\n\n</div>\n\n## Installation\n\n::: code-group\n\n```sh [pnpm]\npnpm add @vielzeug/clockwork\n```\n\n```sh [npm]\nnpm install @vielzeug/clockwork\n```\n\n```sh [yarn]\nyarn add @vielzeug/clockwork\n```\n\n:::\n\n## Quick Start\n\nDefine the context and event union, create an actor, observe its snapshot, then dispose it when its owner ends.\n\n```ts\nimport { defineMachine } from '@vielzeug/clockwork';\n\ntype Event = { type: 'DEC' } | { type: 'INC' };\n\nconst counter = defineMachine<{ count: number }, Event>()({\n context: { count: 0 },\n initial: 'idle',\n states: {\n idle: {\n on: {\n DEC: { reduce: ({ context }) => ({ count: context.count - 1 }), target: 'idle' },\n INC: { reduce: ({ context }) => ({ count: context.count + 1 }), target: 'idle' },\n },\n },\n },\n});\n\nusing actor = counter.createActor();\nactor.subscribe((snapshot) => console.log(snapshot));\nactor.send({ type: 'INC' });\n// { context: { count: 1 }, state: 'idle' }\n```\n\n## Features\n\n<div class=\"features-grid\">\n\n- **`defineMachine()`**: defines one flat, typed machine and rejects the footguns types cannot express.\n- **`machine.transition()`**: evaluates a transition without actor runtime work.\n- **`machine.createActor()`**: creates isolated, disposable runtime ownership.\n- **`reduce`**: returns a replacement context from a transition.\n- **`effects`**: run after commit and stable subscriber notification.\n- **`invoke`**: runs cancellable asynchronous work on state entry.\n- **`after`**: schedules cancellable delayed transitions.\n- **`actor.snapshot`**: exposes the current shallow-frozen state/context value.\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- [Herald](/herald/): publish events between independent actors without coupling machine definitions.\n- [Ripple](/ripple/): bridge actor snapshots into a reactive graph when you need fine-grained rendering.\n- [Ward](/ward/): call authorization predicates from transition guards.\n\n</div>\n\n<!-- markdownlint-enable MD025 MD033 MD060 -->\n",
5
+ "api": "---\ntitle: 'Clockwork: API Reference'\ndescription: Reference for Clockwork machine definitions, actors, and types.\n---\n\n[[toc]]\n\n## API Overview\n\n| Symbol | Purpose | Execution mode | Common gotcha |\n| ----------------------- | ---------------------------------------------------------- | -------------- | ----------------------------------------------------------- |\n| `defineMachine()` | Define a typed flat machine | Sync | Call the generic factory before supplying the definition |\n| `Machine.transition()` | Resolve a pure next snapshot | Sync | Does not run effects, invokes, or timers |\n| `Machine.createActor()` | Create a runtime owner | Sync | Fresh and restored actors have different entry behavior |\n| `Actor.send()` | Dispatch an event | Sync | Returns `void`; re-entrant events queue internally |\n| `Actor.subscribe()` | Observe committed snapshots | Sync | Observes only; it does not trace sends or errors |\n| `Actor.tap()` | Observe transitions, ignores, errors, disposal | Sync | Handler errors are swallowed; it cannot control disposition |\n| `ClockworkError` | Report definition, snapshot, and transition-limit failures | Sync | Narrow with the error subtypes, not message text |\n\n## Package Entry Points\n\n| Import | Purpose |\n| --------------------- | ------------------------------------------------- |\n| `@vielzeug/clockwork` | Machine factory, actor runtime, errors, and types |\n\n## Exported Surface\n\nCallback parameter types are inferred for inline definitions. Clockwork also exports reusable callback, transition, state-node, actor-option, result, and error-context types for shared helpers.\n\n| Primary export | Kind |\n| ------------------------------- | -------- |\n| `defineMachine` | Function |\n| `Machine` | Type |\n| `Actor` | Type |\n| `MachineSnapshot` | Type |\n| `MachineConfig` | Type |\n| `ClockworkError` | Class |\n| `ClockworkDefinitionError` | Class |\n| `ClockworkSnapshotError` | Class |\n| `ClockworkTransitionLimitError` | Class |\n\nAdditional exported types: `ActorErrorContext`, `ActorOptions`, `ActorTapEvent`, `After`, `Effect`, `EffectArgs`, `EventByType`, `EventType`, `Guard`, `Invoke`, `InvokeArgs`, `MachineEvent`, `Reducer`, `StateNode`, `Transition`, `TransitionInput`, and `TransitionResult`.\n\n## Core Functions\n\n### `defineMachine()`\n\n```ts\nfunction defineMachine<\n Context extends Record<string, unknown> = Record<string, never>,\n Event extends MachineEvent = MachineEvent,\n>(): <State extends string>(definition: MachineConfig<State, Context, Event>) => Machine<State, Context, Event>;\n```\n\nReturns a factory that accepts a typed flat machine definition. The definition is trusted as typed: targets, callback shapes, and state keys are compiler-checked. `defineMachine()` rejects only what types cannot express: timer delays outside `0..2,147,483,647` ms, empty transition arrays, and a context that is not a plain record (arrays, `null`, and class instances fail). Omit `context` only when the context type has no keys.\n\nThe curried signature is required so TypeScript can infer `State` from the `states` object while you explicitly provide `Context` and `Event`. TypeScript does not support partial type argument inference, so the state-key union cannot be inferred in a single non-curried call when the context and event generics are explicit.\n\n**Returns:** A definition function that returns `Machine`.\n\n**Example:**\n\n```ts\nimport { defineMachine } from '@vielzeug/clockwork';\n\ntype Event = { type: 'START' };\n\nconst machine = defineMachine<Record<string, never>, Event>()({\n initial: 'idle',\n states: { idle: { on: { START: { target: 'running' } } }, running: {} },\n});\n```\n\nThrows `ClockworkDefinitionError` when a definition carries a timer delay outside the platform range, an empty transition array, or a context that is not a plain record.\n\n---\n\n### `Actor.subscribe()`\n\n```ts\nsubscribe(listener: (snapshot: MachineSnapshot<State, Context>) => void): () => void;\n```\n\nSubscribes to future committed actor snapshots. It does not run immediately; read `actor.snapshot` for the initial value. Returns an unsubscribe function. Subscriber failures are reported through `tap()` as `error` events with `phase: 'subscriber'` without stopping remaining subscribers, declared effects, or the actor.\n\n**Example:**\n\n```ts\nimport { defineMachine } from '@vielzeug/clockwork';\n\nconst machine = defineMachine<Record<string, never>, { type: 'NEXT' }>()({\n initial: 'idle',\n states: { idle: { on: { NEXT: { target: 'idle' } } } },\n});\n\nconst actor = machine.createActor();\nconst stop = actor.subscribe((snapshot) => console.debug(snapshot));\nactor.send({ type: 'NEXT' });\nstop();\nactor.dispose();\n```\n\n## Machine Methods\n\n### `machine.transition()`\n\n```ts\ntransition(\n snapshot: MachineSnapshot<State, Context>,\n event: Event,\n): TransitionResult<State, Context>;\n```\n\nResolves a snapshot for one user event without actor runtime work.\n\n| Parameter | Type | Description |\n| ---------- | --------------------------------- | ----------------------- |\n| `snapshot` | `MachineSnapshot<State, Context>` | Input state and context |\n| `event` | `Event` | User event to evaluate |\n\n**Returns:** A `TransitionResult` with `transition` or `ignored` type.\n\n**Example:**\n\n```ts\nconst result = machine.transition(machine.initialSnapshot, { type: 'START' });\n```\n\n---\n\n### `machine.can()`\n\n```ts\ncan(snapshot: MachineSnapshot<State, Context>, event: Event): boolean;\n```\n\nReturns whether a transition exists and its guard passes.\n\n**Returns:** `true` when the supplied snapshot accepts the event.\n\n---\n\n### `machine.createActor()`\n\n```ts\ncreateActor(options?: ActorOptions<State, Context>): Actor<State, Context, Event>;\n```\n\nCreates an independent actor for event dispatch, timers, invokes, effects, subscriptions, and disposal. A fresh actor starts the initial state's entry effects and resources. An actor restored with `options.snapshot` starts only the restored state's resources: invokes and timers, not entry effects.\n\n| Parameter | Type | Description |\n| ------------------ | --------------------------------- | -------------------------------- |\n| `options.snapshot` | `MachineSnapshot<State, Context>` | Optional restored actor snapshot |\n\n**Returns:** Disposable `Actor`.\n\n**Example:**\n\n```ts\nconst actor = machine.createActor({ snapshot: { context: {}, state: 'idle' } });\n```\n\n## Actor Methods\n\n### `actor.send()`\n\n```ts\nsend(event: Event): void;\n```\n\nDispatches a user event to the current actor state. Events sent while the actor is processing queue and flush synchronously; sends to a disposed actor are ignored. For active actors, malformed events without a string `type` log a development warning and are ignored. Valid but unhandled event types are ignored without a warning. Use `actor.snapshot` after sending to read the current snapshot.\n\n**Returns:** Nothing.\n\n---\n\n### `actor.can()`\n\n```ts\ncan(event: Event): boolean;\n```\n\nReturns whether the current actor snapshot accepts an event. A guard failure is reported as a fatal transition error, disposes the actor, and returns `false`. Returns `false` after disposal.\n\n**Returns:** Boolean transition availability.\n\n---\n\n### `actor.subscribe()`\n\n```ts\nsubscribe(listener: (snapshot: MachineSnapshot<State, Context>) => void): () => void;\n```\n\nRegisters a listener for committed snapshots. The listener does not run immediately.\n\n**Returns:** An unsubscribe function.\n\n---\n\n### `actor.dispose()`\n\n```ts\ndispose(): void;\n[Symbol.dispose](): void;\n```\n\nCancels timers and invokes, clears queued events, tappers and listeners, emits a final `dispose` tap event, and aborts `disposalSignal`.\n\n**Returns:** Nothing. Idempotent.\n\n### `actor.tap()`\n\n```ts\ntap(\n handler: (event: ActorTapEvent<State, Context, Event>) => void,\n options?: { signal?: AbortSignal },\n): () => void;\n```\n\nObserves actor lifecycle without affecting machine behavior: `{ type: 'transition' | 'ignored' }` carries the event and resulting snapshot, `{ type: 'error' }` carries the error with its `phase`/`state`/`event` context, and `{ type: 'dispose' }` is the final event before tappers are cleared. Handler errors are swallowed. Returns an unsubscribe function.\n\n```ts\nactor.tap((event) => {\n if (event.type === 'error') console.error(event.phase, event.state, event.error);\n});\n```\n\n## Error Handling Policy\n\nMachine-execution failures are fail-stop: guard, reducer, effect, invoke, timer, and queued-transition-limit failures dispose the actor. The queued-transition limit is a fixed internal guard against runaway loops, not a configurable option. Subscriber failures are observational: Clockwork reports them through `tap()`, continues the stable subscriber snapshot and declared effects, and keeps the actor active. Tap handlers cannot control disposition.\n\nSubscriber iteration uses a stable snapshot: listeners added during notification begin with the next transition.\n\n## Types\n\n### `MachineSnapshot<State, Context>`\n\n```ts\ntype MachineSnapshot<State extends string, Context extends Record<string, unknown>> = {\n readonly context: Readonly<Context>;\n readonly state: State;\n};\n```\n\nClockwork shallow-clones and freezes each snapshot and its context record. Nested context values remain caller-owned and are not deep-frozen.\n\n### `MachineConfig<State, Context, Event>`\n\n```ts\ntype MachineConfig<\n State extends string,\n Context extends Record<string, unknown>,\n Event extends MachineEvent,\n> = (keyof Context extends never ? { readonly context?: Context } : { readonly context: Context }) & {\n readonly initial: State;\n readonly states: Record<State, StateNode<State, Context, Event>>;\n};\n```\n\nA flat machine definition. State nodes cannot contain child states. The `StateNode`, `Transition`, `After`, `Invoke`, `Effect`, `Guard`, and `Reducer` shapes are inferred inline and exported for reusable helpers. `After.delay` must be finite and between 0 and 2,147,483,647 milliseconds; delayed guards and reducers receive `event: undefined`.\n\n### `Actor<State, Context, Event>`\n\n```ts\ntype Actor<State extends string, Context extends Record<string, unknown>, Event extends MachineEvent> = {\n [Symbol.dispose](): void;\n can(event: Event): boolean;\n readonly disposalSignal: AbortSignal;\n dispose(): void;\n readonly disposed: boolean;\n send(event: Event): void;\n readonly snapshot: MachineSnapshot<State, Context>;\n subscribe(listener: (snapshot: MachineSnapshot<State, Context>) => void): () => void;\n tap(\n handler: (event: ActorTapEvent<State, Context, Event>) => void,\n options?: { readonly signal?: AbortSignal },\n ): () => void;\n};\n```\n\nAn actor's `snapshot` is the current plain readonly snapshot.\n\n### `ActorTapEvent<State, Context, Event>`\n\n```ts\ntype ActorTapEvent<State, Context, Event> =\n | (ActorErrorContext<State, Event> & { readonly error: unknown; readonly type: 'error' })\n | {\n readonly event: Event | undefined;\n readonly snapshot: MachineSnapshot<State, Context>;\n readonly type: 'transition' | 'ignored';\n }\n | { readonly type: 'dispose' };\n\ntype ActorErrorContext<State, Event> = {\n readonly event?: Event;\n readonly phase: 'effect' | 'invoke' | 'subscriber' | 'transition';\n readonly state: State;\n};\n```\n\n### `Machine<State, Context, Event>`\n\n```ts\ntype Machine<State extends string, Context extends Record<string, unknown>, Event extends MachineEvent> = {\n can(snapshot: MachineSnapshot<State, Context>, event: Event): boolean;\n createActor(options?: ActorOptions<State, Context>): Actor<State, Context, Event>;\n readonly initialSnapshot: MachineSnapshot<State, Context>;\n transition(snapshot: MachineSnapshot<State, Context>, event: Event): TransitionResult<State, Context>;\n};\n```\n\nA reusable machine. Its state and event lookups are own-property guarded, so unknown or prototype-colliding names such as `__proto__` or `constructor` are safely ignored when no transition exists.\n\n## Errors\n\n### `ClockworkError`\n\n`ClockworkError` is the base class for every Clockwork failure, with `details` and standard `Error` fields. Use `instanceof ClockworkError` to narrow an unknown error. Three subtypes carry the failure category:\n\n| Subtype | Raised when |\n| ------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |\n| `ClockworkDefinitionError` | `defineMachine()` rejects a definition footgun types cannot express: a timer delay outside `0..2,147,483,647` ms, an empty transition array, or a context that is not a plain record |\n| `ClockworkSnapshotError` | A snapshot, its context, or a reducer result is not a plain object record, or names an undeclared state |\n| `ClockworkTransitionLimitError` | An actor exceeds the fixed queued-transition limit and disposes itself |\n\n```ts\nif (error instanceof ClockworkDefinitionError) {\n console.error(error.message, error.details);\n}\n```\n",
6
+ "usage": "---\ntitle: 'Clockwork: Usage Guide'\ndescription: Build deterministic state machines with pure transitions and actor-owned runtime work.\n---\n\n[[toc]]\n\n## Basic Usage\n\nCall `defineMachine<Context, Event>()` first to bind context and event types; the returned definition function infers state labels from `states`. Context is optional only when its type has no keys. Create one actor for each independently owned workflow.\n\n```ts\nimport { defineMachine } from '@vielzeug/clockwork';\n\ntype Event = { type: 'TOGGLE' };\n\nconst machine = defineMachine<Record<string, never>, Event>()({\n initial: 'on',\n states: {\n off: { on: { TOGGLE: { target: 'on' } } },\n on: { on: { TOGGLE: { target: 'off' } } },\n },\n});\n\nconst actor = machine.createActor();\nactor.send({ type: 'TOGGLE' });\nconsole.log(actor.snapshot.state); // 'off'\nactor.dispose();\n```\n\nDispose actors when a feature, request, or test ends. You can use `using` when the surrounding runtime supports `Symbol.dispose`.\n\n```ts\nusing actor = machine.createActor();\nactor.send({ type: 'TOGGLE' });\n```\n\n## Context reducers\n\nA reducer receives readonly context and returns the next context. Clockwork shallow-clones and freezes each snapshot and context record; nested values are not deep-cloned, so make explicit nested replacements.\n\n```ts\ntype Event = { type: 'DEC' } | { type: 'INC' } | { type: 'RESET' };\n\nconst counter = defineMachine<{ count: number }, Event>()({\n context: { count: 0 },\n initial: 'idle',\n states: {\n idle: {\n on: {\n DEC: { reduce: ({ context }) => ({ count: context.count - 1 }), target: 'idle' },\n INC: { reduce: ({ context }) => ({ count: context.count + 1 }), target: 'idle' },\n RESET: { reduce: () => ({ count: 0 }), target: 'idle' },\n },\n },\n },\n});\n```\n\nKeep reducers pure. Make nested copies yourself when nested data changes.\n\n```ts\nSAVE: {\n reduce: ({ context, event }) => ({\n ...context,\n profile: { ...context.profile, name: event.name },\n }),\n target: 'editing',\n}\n```\n\n## Guards\n\nGuards decide whether a transition can run. They receive readonly context and the matching event. For several choices, use an ordered array; the first passing guard wins.\n\n```ts\nPAY: [\n {\n guard: ({ context }) => context.balance >= context.total,\n reduce: ({ context }) => ({ ...context, balance: context.balance - context.total }),\n target: 'success',\n },\n { target: 'insufficientFunds' },\n];\n```\n\nCall `actor.can(event)` for the current actor snapshot or `machine.can(snapshot, event)` for an arbitrary snapshot.\n\n## Pure transitions\n\n`machine.transition()` enables isolated unit tests and decision UIs. It returns the unchanged snapshot with `type: 'ignored'` when no transition matches; it does not expose or run effects.\n\n```ts\nconst result = counter.transition({ context: { count: 3 }, state: 'idle' }, { type: 'INC' });\n\nif (result.type === 'transition') {\n console.log(result.snapshot.context.count); // 4\n}\n```\n\n## Effects\n\nEntry, exit, and transition effects run only through an actor. The actor commits, establishes target-state timers and invokes, notifies a stable subscriber snapshot, then runs exit, transition, and entry effects. Subscriber failures are reported but cannot skip declared effects. Effects cannot change context; send a regular event for another state change.\n\n```ts\ntype WorkflowEvent = { type: 'SUBMIT' };\nconst workflow = defineMachine<{ orderId: string }, WorkflowEvent>()({\n context: { orderId: '' },\n initial: 'draft',\n states: {\n draft: {\n on: {\n SUBMIT: {\n effects: [({ context }) => console.debug('submitted', context)],\n target: 'submitted',\n },\n },\n },\n submitted: { entry: [({ context }) => console.log(`Submitted ${context.orderId}`)] },\n },\n});\n```\n\nEffects receive `context`, the triggering `event` (or `undefined` for initial entry), actor `send`, and the actor lifetime `signal`.\n\n## Async invokes\n\nInvokes start on state entry. `src` gets readonly entry context, the triggering event or `undefined`, and an `AbortSignal`. `onDone` or `onError` map settlement to ordinary events. All invokes are cancelled when the actor exits the state or disposes.\n\n```ts\ntype LoadEvent =\n | { type: 'FETCH' }\n | { items: string[]; type: 'SUCCESS' }\n | { message: string; type: 'FAILURE' }\n | { type: 'RETRY' };\n\nconst loader = defineMachine<{ error: string; items: string[] }, LoadEvent>()({\n context: { error: '', items: [] },\n initial: 'idle',\n states: {\n idle: { on: { FETCH: { target: 'loading' } } },\n loading: {\n invoke: [\n {\n src: async ({ signal }) => {\n const response = await fetch('/api/items', { signal });\n if (!response.ok) throw new Error(`HTTP ${response.status}`);\n return response.json() as Promise<string[]>;\n },\n onDone: ({ result }) => ({ items: result, type: 'SUCCESS' }),\n onError: ({ error }) => ({ message: String(error), type: 'FAILURE' }),\n },\n ],\n on: {\n FAILURE: { reduce: ({ event }) => ({ error: event.message, items: [] }), target: 'error' },\n SUCCESS: { reduce: ({ event }) => ({ error: '', items: event.items }), target: 'ready' },\n },\n },\n ready: {},\n error: { on: { RETRY: { target: 'loading' } } },\n },\n});\n```\n\n## Delayed transitions\n\n`after` starts timers on state entry and cancels them on exit or disposal. Delay must be finite and between 0 and 2,147,483,647 milliseconds. Guards and reducers receive `event: undefined`; a user event with `type: '$after'` remains a normal user event.\n\n```ts\ntype NotificationEvent = { type: 'DISMISS' } | { message: string; type: 'SHOW' };\nconst notification = defineMachine<{ message: string }, NotificationEvent>()({\n context: { message: '' },\n initial: 'hidden',\n states: {\n hidden: { on: { SHOW: { reduce: ({ event }) => ({ message: event.message }), target: 'visible' } } },\n visible: {\n after: [{ delay: 5_000, target: 'hidden' }],\n on: { DISMISS: { target: 'hidden' } },\n },\n },\n});\n```\n\n## Snapshot observation and persistence\n\n`actor.snapshot` is the current shallow-frozen `MachineSnapshot`; read it directly rather than calling a snapshot method. Use `subscribe()` to integrate a state library or persist future committed snapshots. Fresh actors run their initial entry effects and resources; restored actors start only the restored state's invokes and timers, not its entry effects.\n\n```ts\nconst stored = sessionStorage.getItem('wizard');\nconst actor = machine.createActor({\n snapshot: stored ? JSON.parse(stored) : undefined,\n});\n\nconst stopSaving = actor.subscribe((snapshot) => {\n sessionStorage.setItem('wizard', JSON.stringify(snapshot));\n});\n\nconsole.log(actor.snapshot);\nstopSaving();\nactor.dispose();\n```\n\nValidate untrusted persisted data before passing it to `createActor()`. Clockwork validates the restored state name but cannot validate application-specific context fields.\n\n## Error handling\n\nGuard, reducer, effect, invoke, timer, and transition-limit failures dispose the actor. The transition limit is a fixed internal guard against runaway loops. Subscriber failures are reported but do not dispose the actor or interrupt remaining subscribers and declared effects.\n\nUse `tap()` to observe either class without controlling disposition. The `error` event carries the error and its `phase` and `state`; handler failures are swallowed so observation cannot alter actor behavior.\n\n```ts\nconst actor = machine.createActor();\nactor.tap((event) => {\n if (event.type === 'error')\n console.error({ error: event.error, event: event.event, phase: event.phase, state: event.state });\n});\n```\n\nFor fatal machine failures, the `error` event is emitted before disposal. Without a tap handler, fatal failures dispose silently.\n\n## Debugging\n\nUse `actor.subscribe()` to observe committed snapshots during development. It observes snapshots only; it does not trace dispatched events or runtime errors.\n\n```ts\nconst actor = machine.createActor();\nconst stop = actor.subscribe((snapshot) => console.debug(snapshot));\nactor.send({ type: 'NEXT' });\nstop();\nactor.dispose();\n```\n\nFor richer inspection, route snapshots to application devtools. Clockwork intentionally has no internal trace buffer.\n\n## Flat state maps\n\nClockwork has flat state IDs. Prefer explicit states such as `editingDraft` and `editingSaving`, or compose several actors when domains have independent lifecycles.\n\n## SSR\n\nReuse one machine definition, but create and dispose an actor per request. Never share an actor across concurrent requests.\n\n## Testing\n\nTest deterministic state behavior through `machine.transition()`. Create actors only for timers, invokes, effects, queueing, subscriptions, or disposal behavior.\n\n```ts\nimport { expect, test } from 'vitest';\n\ntest('increments without an actor', () => {\n const result = counter.transition({ context: { count: 2 }, state: 'idle' }, { type: 'INC' });\n\n expect(result).toMatchObject({\n snapshot: { context: { count: 3 }, state: 'idle' },\n type: 'transition',\n });\n});\n```\n\n## Framework Integration\n\nBridge the current actor snapshot into renderer state through one subscription. Dispose that subscription with component lifecycle.\n\n::: code-group\n\n```ts [React]\nimport { useSyncExternalStore } from 'react';\n\nfunction useActor<Value>(actor: {\n readonly snapshot: Value;\n subscribe(listener: (snapshot: Value) => void): () => void;\n}) {\n return useSyncExternalStore(\n (notify) => actor.subscribe(() => notify()),\n () => actor.snapshot,\n () => actor.snapshot,\n );\n}\n```\n\n```ts [Vue 3]\nimport { onUnmounted, shallowRef } from 'vue';\n\nconst snapshot = shallowRef(actor.snapshot);\nconst stop = actor.subscribe((next) => (snapshot.value = next));\nonUnmounted(stop);\n```\n\n```ts [Svelte]\nimport { onDestroy } from 'svelte';\n\nlet snapshot = actor.snapshot;\nconst stop = actor.subscribe((next) => (snapshot = next));\nonDestroy(stop);\n```\n\n:::\n\n## Working with Other Vielzeug Libraries\n\nUse Herald when separate actors exchange application events. Bridge Clockwork snapshots into Ripple only at a UI or application boundary.\n\n```ts\nimport { createBus } from '@vielzeug/herald';\n\nconst bus = createBus<{ REFRESH: void }>();\nbus.on('REFRESH', () => actor.send({ type: 'FETCH' }));\n```\n\n## Best Practices\n\n- Define context and event unions with `defineMachine<Context, Event>()`.\n- Return replacement context from reducers; do not rely on runtime copying or freezing.\n- Keep guards and reducers pure.\n- Use actors for effects, timers, invokes, subscriptions, and cancellation.\n- Read the current snapshot from `actor.snapshot`, not a wrapper value.\n- Validate persisted context before restoring a snapshot.\n- Dispose every actor at its ownership boundary.\n- Route runtime failures through the `tap()` observer for logging and diagnostics.\n",
7
7
  "examples": "---\ntitle: 'Clockwork: Examples'\ndescription: Practical state machine patterns with pure transitions and actors.\n---\n\n- [Counter with Reset](./examples/counter-with-reset.md)\n- [Form Validation](./examples/form-validation.md)\n- [Auto-Dismiss Notification](./examples/auto-dismiss-notification.md)\n- [Model Nested Workflows with Flat States](./examples/hierarchical-states.md)\n- [Pure Transition Testing](./examples/unit-testing.md)\n- [Auth Flow with Guards](./examples/auth-flow.md)\n- [Data Fetching with Error Recovery](./examples/data-fetching.md)\n- [Fetch with Retry](./examples/fetch-retry.md)\n- [Paginated Data Loading](./examples/paginated-data-loading.md)\n- [Media Player](./examples/media-player.md)\n- [Persisted Wizard](./examples/persisted-wizard.md)\n- [Multi-Step Wizard with Routing](./examples/wizard-with-routing.md)\n- [Shopping Cart Checkout](./examples/checkout.md)\n- [Permission-Based Access Control](./examples/permission-based-access.md)\n- [Event Boundaries](./examples/middleware-pipeline.md)\n- [Multi-Machine Coordination](./examples/multi-machine-coordination.md)\n- [Debugging Transitions](./examples/debugging-transitions.md)\n"
8
8
  },
9
9
  "examples": [
@@ -1,9 +1,9 @@
1
1
  {
2
2
  "apiSource": "/**\n * `@vielzeug/codex`: supported public contract.\n *\n * Exposes the generic catalog, snapshot loader, errors, and server hosts.\n * Refine catalog types and tools live behind `@vielzeug/codex/refine`.\n */\nexport { StdioServerTransport } from '@modelcontextprotocol/server/stdio';\nexport { type Catalog, CatalogError, type SearchHit, SnapshotCatalog } from './catalog.js';\nexport { CodexError } from './errors.js';\nexport { type HttpHost, type HttpHostOptions, startHttpHost } from './http.js';\nexport { createMcpServer } from './server.js';\nexport { type LoadedSnapshot, loadSnapshot } from './snapshot.js';\nexport type {\n DocPage,\n Example,\n PackageMeta,\n SnapshotManifest,\n} from './types.js';\n",
3
3
  "docs": {
4
- "index": "---\ntitle: Codex\ndescription: Local MCP access to Vielzeug documentation and package metadata.\npackage: codex\ncategory: AI\nkeywords: [mcp, docs, ai]\nrelated: [refine]\nexports: [loadSnapshot, SnapshotCatalog, createMcpServer, createMcpServerWithRefine, startHttpHost, StdioServerTransport, CatalogError, CodexError]\nenvironments: [node]\n---\n\n<PackageHero package=\"codex\" />\n\n## Why Codex?\n\nCodex exposes current Vielzeug catalog data through MCP without scanning source at request time.\n\n## Installation\n\n```sh\npnpm add @vielzeug/codex\n```\n\n## Quick Start\n\n```sh\nnpx -y @vielzeug/codex\n```\n\n## Features\n\n- `loadSnapshot` validates chunked snapshot metadata.\n- `SnapshotCatalog` loads package content only when requested.\n- `createMcpServer` registers generic package tools; `/refine` adds component tools explicitly.\n- Root and `/refine` entry points publish TypeScript declarations.\n- `startHttpHost` enforces loopback binding plus localhost Host and Origin validation.\n\n## Documentation\n\n- [Usage](./usage.md)\n- [API](./api.md)\n- [Examples](./examples.md)\n- [Migration Guide](./migration.md)\n\n## See Also\n\n- [Refine](../refine/) provides component metadata bundled by Codex.\n",
5
- "api": "---\ntitle: Codex API\ndescription: Snapshot, catalog, MCP server, and local HTTP host APIs.\n---\n\n[[toc]]\n\n## API Overview\n\n| Symbol | Purpose | Execution mode | Common gotcha |\n| --- | --- | --- | --- |\n| `loadSnapshot` | Read validated snapshot metadata | Sync | Content chunks load lazily |\n| `SnapshotCatalog` | Query package corpus | Sync | Construct from loaded snapshot |\n| `createMcpServer` | MCP adapter factory (generic tools) | Sync | Requires catalog and version |\n| `createMcpServerWithRefine` | MCP adapter factory (all package and Refine tools) | Sync | Import from `@vielzeug/codex/refine` |\n| `startHttpHost` | Loopback Streamable HTTP host | Async | HTTP remains local-only |\n\n## Package Entry Points\n\n| Import | Purpose |\n| --- | --- |\n| `@vielzeug/codex` | `Catalog`, `SnapshotCatalog`, `loadSnapshot`, MCP server, and HTTP host |\n| `@vielzeug/codex/refine` | Refine catalog and MCP server with component tools |\n\n## Snapshot\n\n### `loadSnapshot`\n\n```ts\nloadSnapshot(snapshotRoot?: string, options?: { validateContents?: boolean }): LoadedSnapshot;\n```\n\nLoads catalog/search metadata and Refine metadata. `snapshotRoot` defaults to bundled `data/`. Package content chunks stay lazy unless `validateContents: true`, which validates every package content chunk during loading.\n\n### `SnapshotCatalog`\n\n```ts\nnew SnapshotCatalog(snapshot: LoadedSnapshot)\n```\n\nProvides generic package lookup, docs/source/example/signature access, and deterministic search. Use `SnapshotRefineCatalog` from `/refine` when component methods are needed.\n\n---\n\n## MCP\n\n### `createMcpServer`\n\n```ts\ncreateMcpServer(catalog: Catalog, options: { version: string; debug?: boolean }): Server;\n```\n\nRegisters generic package tools over `Catalog`. Refine tools are opt-in through `/refine`. The root also re-exports `StdioServerTransport` for connecting the server without a second package import.\n\n## Refine tools (`@vielzeug/codex/refine`)\n\n### `createMcpServerWithRefine`\n\n```ts\ncreateMcpServerWithRefine(catalog: RefineCatalog, options: { version: string; debug?: boolean }): Server;\n```\n\nCreates a server with the general Vielzeug package catalog tools and the additional `refine-*` component tools.\n\n### `SnapshotRefineCatalog`\n\n```ts\nnew SnapshotRefineCatalog(snapshot: LoadedSnapshot)\n```\n\nExtends `SnapshotCatalog` with `getComponent()` and `listComponents()`.\n\nComponent declarations and their nested data use readonly TypeScript types.\n\n## HTTP\n\n### `startHttpHost`\n\n```ts\nstartHttpHost(options: HttpHostOptions): Promise<HttpHost>;\n```\n\nStarts Streamable HTTP on `127.0.0.1` by default. Runtime validation accepts only `127.0.0.1` or `::1`; every request must also pass localhost Host and Origin checks. `createServer` must return a fresh, unconnected server with its request handlers registered.\n\n## Types\n\n```ts\ninterface SnapshotPointer {\n readonly directory: string;\n}\n\ninterface SnapshotManifest {\n readonly catalog: 'catalog.json';\n readonly contentDirectory: 'packages';\n readonly refine: 'refine.json' | null;\n readonly schemaVersion: typeof SNAPSHOT_SCHEMA_VERSION;\n readonly search: 'search.json';\n readonly version: string;\n}\n```\n\nDev snapshots use `SnapshotPointer` (via `current.json`); published snapshots are static directories.\n\n```ts\ninterface LoadedSnapshot {\n readonly catalog: CatalogFile;\n readonly contentDirectory: string;\n readonly manifest: SnapshotManifest;\n readonly refineComponents: readonly CemDeclaration[];\n readonly search: readonly SearchRecord[];\n}\n```\n\n```ts\ninterface CatalogFile {\n readonly packages: readonly PackageMeta[];\n readonly version: string;\n}\n\ninterface SearchRecord {\n readonly category: string;\n readonly description: string;\n readonly docs: Readonly<Partial<Record<DocPage, string>>>;\n readonly examples: readonly { readonly id: string; readonly text: string }[];\n readonly exports: string;\n readonly keywords: string;\n readonly name: string;\n readonly related: string;\n readonly slug: string;\n readonly source: string | null;\n}\n\ninterface PackageMeta {\n readonly availableDocPages: readonly DocPage[];\n readonly category: string;\n readonly description: string;\n readonly exampleIds: readonly string[];\n readonly exports: readonly string[];\n readonly hasSource: boolean;\n readonly keywords: readonly string[];\n readonly name: string;\n readonly related: readonly string[];\n readonly slug: string;\n readonly version: string;\n}\n\ninterface Example {\n readonly code: string;\n readonly id: string;\n readonly name: string;\n}\n```\n\n```ts\ninterface Catalog {\n getDocs(slug: string, page: DocPage): string;\n getExample(slug: string, exampleId: string): Example;\n getPackage(slug: string): PackageMeta;\n getSource(slug: string): string;\n getTypeSignature(slug: string, symbol: string): string;\n listExamples(slug: string): readonly Pick<Example, 'id' | 'name'>[];\n listPackages(): readonly PackageMeta[];\n search(query: string): readonly SearchHit[];\n}\n\ninterface RefineCatalog extends Catalog {\n getComponent(tagName: string): CemDeclaration;\n listComponents(): readonly CemDeclaration[];\n}\n\ninterface SearchHit {\n readonly matchedExamples?: readonly string[];\n readonly matchedIn: readonly ('docs' | 'examples' | 'exports' | 'keywords' | 'metadata' | 'related' | 'source')[];\n readonly matchedPages?: readonly DocPage[];\n readonly name: string;\n readonly slug: string;\n}\n```\n\nCatalog exposes semantic reads. It does not expose its cached content chunks.\n\n```ts\ninterface HttpHost {\n readonly disposalSignal: AbortSignal;\n dispose(): Promise<void>;\n readonly disposed: boolean;\n readonly host: string;\n readonly port: number;\n [Symbol.asyncDispose](): Promise<void>;\n}\n\ninterface HttpHostOptions {\n createServer: () => Server;\n host?: '127.0.0.1' | '::1';\n port: number;\n version: string;\n}\n```\n\n```ts\nconst DOC_PAGES = ['index', 'api', 'usage', 'examples'] as const;\ntype DocPage = (typeof DOC_PAGES)[number];\n\nconst SNAPSHOT_SCHEMA_VERSION = 1 as const;\n```\n\nCEM types and `RefineCatalog` are exported from `/refine`.\n\n```ts\ninterface CemDeclaration {\n readonly attributes?: readonly CemAttribute[];\n readonly cssParts?: readonly CemCssPart[];\n readonly cssProperties?: readonly CemCssProperty[];\n readonly description?: string;\n readonly events?: readonly CemEvent[];\n readonly members?: readonly CemMember[];\n readonly name?: string;\n readonly slots?: readonly CemSlot[];\n readonly superclass?: { readonly name: string; readonly package?: string };\n readonly tagName?: string;\n readonly [key: string]: unknown;\n}\n\ninterface CemAttribute {\n readonly default?: string;\n readonly description?: string;\n readonly fieldName?: string;\n readonly name: string;\n readonly type?: { readonly text: string };\n}\n\ninterface CemCssPart {\n readonly description?: string;\n readonly name: string;\n}\n\ninterface CemCssProperty {\n readonly default?: string;\n readonly description?: string;\n readonly name: string;\n}\n\ninterface CemEvent {\n readonly description?: string;\n readonly name: string;\n readonly type?: { readonly text: string };\n}\n\ninterface CemMember {\n readonly description?: string;\n readonly kind?: 'field' | 'method';\n readonly name: string;\n readonly type?: { readonly text: string };\n}\n\ninterface CemSlot {\n readonly description?: string;\n readonly name: string;\n}\n```\n\n## Errors\n\n`CodexError` signals malformed snapshots or host failures. `CatalogError` adds `INVALID_ARG`, `NOT_FOUND`, or `UNAVAILABLE` for expected tool failures.\n",
6
- "usage": "---\ntitle: 'Codex: Usage Guide'\ndescription: Install, connect, develop, and debug the Vielzeug MCP server.\n---\n\n[[toc]]\n\n## Basic Usage\n\nRun local stdio server:\n\n```sh\nnpx -y @vielzeug/codex\n```\n\nUse shipped `mcp-setup.json` for machine-readable generic configuration. Client-specific configuration must use its documented MCP format.\n\n## HTTP Mode\n\nHTTP uses Streamable HTTP and binds loopback only:\n\n```sh\nnpx -y @vielzeug/codex --port=3100\ncurl http://127.0.0.1:3100/health\n```\n\nResponse includes snapshot version. Runtime bind validation and Host/Origin allowlists restrict access to localhost; no remote host mode exists. Programmatic HTTP hosting takes a `createServer` factory that returns a fresh, unconnected server with its request handlers registered. Dispose the host asynchronously and observe `disposed` or `disposalSignal` when coordinating shutdown.\n\n## Agent Skill\n\nCodex ships a `vielzeug` agent skill: a `SKILL.md` that tells an AI agent working in *your* project to discover packages through the MCP tools instead of guessing, to use the owning `@vielzeug/*` package instead of hand-rolling, and to build UI from Refine components, tokens, and theme whenever `@vielzeug/refine` is installed. Install it into the project:\n\n```sh\nnpx -y @vielzeug/codex skills install\nnpx -y @vielzeug/codex skills install --target=.claude/skills\nnpx -y @vielzeug/codex skills install --force # overwrite an existing copy\n```\n\nWithout `--target`, the first existing directory among `.agents/skills`, `.github/skills`, and `.claude/skills` is used; when none exists, `.agents/skills` is created. The command refuses to overwrite an installed skill unless `--force` is passed. Rerun after upgrading codex to pick up the current package list, which is generated from the published manifests. The skill pairs with the MCP server: its lookups (`get-docs`, `refine-get-component`, `refine-validate-usage`, …) assume codex is connected, and it documents `node_modules` fallbacks for when it is not.\n\n## Local Development\n\nRequires Node 22+ and root setup:\n\n```sh\npnpm setup\ncd packages/codex\npnpm test:unit\npnpm test:integration\npnpm dev\n```\n\n`test:unit` uses fixtures only. `test:integration` regenerates a current snapshot then checks real monorepo inputs.\n\n`pnpm dev` watches documentation and package inputs, atomically publishes snapshots, then restarts server when snapshot changes.\n\n## Debugging\n\n```sh\npnpm dev\nnode src/cli.ts --port=3100 --debug\ncurl http://127.0.0.1:3100/health\n```\n\n`--debug` logs tool durations and expected catalog errors to stderr. Build `@vielzeug/refine` before generating snapshot when component metadata changes.\n\n## Programmatic Usage\n\n```ts\nimport { SnapshotCatalog, StdioServerTransport, createMcpServer, loadSnapshot } from '@vielzeug/codex';\n\nconst snapshot = loadSnapshot();\nconst catalog = new SnapshotCatalog(snapshot);\nawait createMcpServer(catalog, { version: snapshot.manifest.version }).connect(new StdioServerTransport());\n```\n\nTo include Refine component tools, create a server with the `@vielzeug/codex/refine` subpath:\n\n```ts\nimport { StdioServerTransport, loadSnapshot } from '@vielzeug/codex';\nimport { createMcpServerWithRefine, SnapshotRefineCatalog } from '@vielzeug/codex/refine';\n\nconst snapshot = loadSnapshot();\nconst catalog = new SnapshotRefineCatalog(snapshot);\nconst server = createMcpServerWithRefine(catalog, { version: snapshot.manifest.version });\nawait server.connect(new StdioServerTransport());\n```\n\n## Best Practices\n\n- Use `search-packages` for capability discovery before loading broad source.\n- Use `get-type-signature` before loading full source.\n- Published package snapshots are static directories; local dev snapshots are immutable generations selected by `.dev/current.json`.\n- Pass `validateContents: true` to `loadSnapshot()` for artifact verification; normal startup keeps package chunks lazy.\n- Return a fresh configured server from `startHttpHost`'s `createServer` factory.\n- Keep HTTP local. Use stdio for normal client integration.\n- Run `pnpm test:unit` before `pnpm test:integration`.\n",
4
+ "index": "---\ntitle: Codex\ndescription: Local MCP access to Vielzeug documentation and package metadata.\npackage: codex\ncategory: AI\nkeywords: [mcp, docs, ai]\nrelated: [refine]\nexports:\n [\n loadSnapshot,\n SnapshotCatalog,\n createMcpServer,\n createMcpServerWithRefine,\n startHttpHost,\n StdioServerTransport,\n CatalogError,\n CodexError,\n ]\nenvironments: [node]\n---\n\n<PackageHero package=\"codex\" />\n\n## Why Codex?\n\nCodex exposes current Vielzeug catalog data through MCP without scanning source at request time.\n\n## Installation\n\n```sh\npnpm add @vielzeug/codex\n```\n\n## Quick Start\n\n```sh\nnpx -y @vielzeug/codex\n```\n\n## Features\n\n- `loadSnapshot` validates chunked snapshot metadata.\n- `SnapshotCatalog` loads package content only when requested.\n- `createMcpServer` registers generic package tools; `/refine` adds component tools explicitly.\n- Root and `/refine` entry points publish TypeScript declarations.\n- `startHttpHost` enforces loopback binding plus localhost Host and Origin validation.\n\n## Documentation\n\n- [Usage](./usage.md)\n- [API](./api.md)\n- [Examples](./examples.md)\n- [Migration Guide](./migration.md)\n\n## See Also\n\n- [Refine](../refine/) provides component metadata bundled by Codex.\n",
5
+ "api": "---\ntitle: Codex API\ndescription: Snapshot, catalog, MCP server, and local HTTP host APIs.\n---\n\n[[toc]]\n\n## API Overview\n\n| Symbol | Purpose | Execution mode | Common gotcha |\n| --------------------------- | -------------------------------------------------- | -------------- | ------------------------------------ |\n| `loadSnapshot` | Read validated snapshot metadata | Sync | Content chunks load lazily |\n| `SnapshotCatalog` | Query package corpus | Sync | Construct from loaded snapshot |\n| `createMcpServer` | MCP adapter factory (generic tools) | Sync | Requires catalog and version |\n| `createMcpServerWithRefine` | MCP adapter factory (all package and Refine tools) | Sync | Import from `@vielzeug/codex/refine` |\n| `startHttpHost` | Loopback Streamable HTTP host | Async | HTTP remains local-only |\n\n## Package Entry Points\n\n| Import | Purpose |\n| ------------------------ | ----------------------------------------------------------------------- |\n| `@vielzeug/codex` | `Catalog`, `SnapshotCatalog`, `loadSnapshot`, MCP server, and HTTP host |\n| `@vielzeug/codex/refine` | Refine catalog and MCP server with component tools |\n\n## Snapshot\n\n### `loadSnapshot`\n\n```ts\nloadSnapshot(snapshotRoot?: string, options?: { validateContents?: boolean }): LoadedSnapshot;\n```\n\nLoads catalog/search metadata and Refine metadata. `snapshotRoot` defaults to bundled `data/`. Package content chunks stay lazy unless `validateContents: true`, which validates every package content chunk during loading.\n\n### `SnapshotCatalog`\n\n```ts\nnew SnapshotCatalog(snapshot: LoadedSnapshot)\n```\n\nProvides generic package lookup, docs/source/example/signature access, and deterministic search. Use `SnapshotRefineCatalog` from `/refine` when component methods are needed.\n\n---\n\n## MCP\n\n### `createMcpServer`\n\n```ts\ncreateMcpServer(catalog: Catalog, options: { version: string; debug?: boolean }): Server;\n```\n\nRegisters generic package tools over `Catalog`. Refine tools are opt-in through `/refine`. The root also re-exports `StdioServerTransport` for connecting the server without a second package import.\n\n## Refine tools (`@vielzeug/codex/refine`)\n\n### `createMcpServerWithRefine`\n\n```ts\ncreateMcpServerWithRefine(catalog: RefineCatalog, options: { version: string; debug?: boolean }): Server;\n```\n\nCreates a server with the general Vielzeug package catalog tools and the additional `refine-*` component tools.\n\n### `SnapshotRefineCatalog`\n\n```ts\nnew SnapshotRefineCatalog(snapshot: LoadedSnapshot)\n```\n\nExtends `SnapshotCatalog` with `getComponent()` and `listComponents()`.\n\nComponent declarations and their nested data use readonly TypeScript types.\n\n## HTTP\n\n### `startHttpHost`\n\n```ts\nstartHttpHost(options: HttpHostOptions): Promise<HttpHost>;\n```\n\nStarts Streamable HTTP on `127.0.0.1` by default. Runtime validation accepts only `127.0.0.1` or `::1`; every request must also pass localhost Host and Origin checks. `createServer` must return a fresh, unconnected server with its request handlers registered.\n\n## Types\n\n```ts\ninterface SnapshotPointer {\n readonly directory: string;\n}\n\ninterface SnapshotManifest {\n readonly catalog: 'catalog.json';\n readonly contentDirectory: 'packages';\n readonly refine: 'refine.json' | null;\n readonly schemaVersion: typeof SNAPSHOT_SCHEMA_VERSION;\n readonly search: 'search.json';\n readonly version: string;\n}\n```\n\nDev snapshots use `SnapshotPointer` (via `current.json`); published snapshots are static directories.\n\n```ts\ninterface LoadedSnapshot {\n readonly catalog: CatalogFile;\n readonly contentDirectory: string;\n readonly manifest: SnapshotManifest;\n readonly refineComponents: readonly CemDeclaration[];\n readonly search: readonly SearchRecord[];\n}\n```\n\n```ts\ninterface CatalogFile {\n readonly packages: readonly PackageMeta[];\n readonly version: string;\n}\n\ninterface SearchRecord {\n readonly category: string;\n readonly description: string;\n readonly docs: Readonly<Partial<Record<DocPage, string>>>;\n readonly examples: readonly { readonly id: string; readonly text: string }[];\n readonly exports: string;\n readonly keywords: string;\n readonly name: string;\n readonly related: string;\n readonly slug: string;\n readonly source: string | null;\n}\n\ninterface PackageMeta {\n readonly availableDocPages: readonly DocPage[];\n readonly category: string;\n readonly description: string;\n readonly exampleIds: readonly string[];\n readonly exports: readonly string[];\n readonly hasSource: boolean;\n readonly keywords: readonly string[];\n readonly name: string;\n readonly related: readonly string[];\n readonly slug: string;\n readonly version: string;\n}\n\ninterface Example {\n readonly code: string;\n readonly id: string;\n readonly name: string;\n}\n```\n\n```ts\ninterface Catalog {\n getDocs(slug: string, page: DocPage): string;\n getExample(slug: string, exampleId: string): Example;\n getPackage(slug: string): PackageMeta;\n getSource(slug: string): string;\n getTypeSignature(slug: string, symbol: string): string;\n listExamples(slug: string): readonly Pick<Example, 'id' | 'name'>[];\n listPackages(): readonly PackageMeta[];\n search(query: string): readonly SearchHit[];\n}\n\ninterface RefineCatalog extends Catalog {\n getComponent(tagName: string): CemDeclaration;\n listComponents(): readonly CemDeclaration[];\n}\n\ninterface SearchHit {\n readonly matchedExamples?: readonly string[];\n readonly matchedIn: readonly ('docs' | 'examples' | 'exports' | 'keywords' | 'metadata' | 'related' | 'source')[];\n readonly matchedPages?: readonly DocPage[];\n readonly name: string;\n readonly slug: string;\n}\n```\n\nCatalog exposes semantic reads. It does not expose its cached content chunks.\n\n```ts\ninterface HttpHost {\n readonly disposalSignal: AbortSignal;\n dispose(): Promise<void>;\n readonly disposed: boolean;\n readonly host: string;\n readonly port: number;\n [Symbol.asyncDispose](): Promise<void>;\n}\n\ninterface HttpHostOptions {\n createServer: () => Server;\n host?: '127.0.0.1' | '::1';\n port: number;\n version: string;\n}\n```\n\n```ts\nconst DOC_PAGES = ['index', 'api', 'usage', 'examples'] as const;\ntype DocPage = (typeof DOC_PAGES)[number];\n\nconst SNAPSHOT_SCHEMA_VERSION = 1 as const;\n```\n\nCEM types and `RefineCatalog` are exported from `/refine`.\n\n```ts\ninterface CemDeclaration {\n readonly attributes?: readonly CemAttribute[];\n readonly cssParts?: readonly CemCssPart[];\n readonly cssProperties?: readonly CemCssProperty[];\n readonly description?: string;\n readonly events?: readonly CemEvent[];\n readonly members?: readonly CemMember[];\n readonly name?: string;\n readonly slots?: readonly CemSlot[];\n readonly superclass?: { readonly name: string; readonly package?: string };\n readonly tagName?: string;\n readonly [key: string]: unknown;\n}\n\ninterface CemAttribute {\n readonly default?: string;\n readonly description?: string;\n readonly fieldName?: string;\n readonly name: string;\n readonly type?: { readonly text: string };\n}\n\ninterface CemCssPart {\n readonly description?: string;\n readonly name: string;\n}\n\ninterface CemCssProperty {\n readonly default?: string;\n readonly description?: string;\n readonly name: string;\n}\n\ninterface CemEvent {\n readonly description?: string;\n readonly name: string;\n readonly type?: { readonly text: string };\n}\n\ninterface CemMember {\n readonly description?: string;\n readonly kind?: 'field' | 'method';\n readonly name: string;\n readonly type?: { readonly text: string };\n}\n\ninterface CemSlot {\n readonly description?: string;\n readonly name: string;\n}\n```\n\n## Errors\n\n`CodexError` signals malformed snapshots or host failures. `CatalogError` adds `INVALID_ARG`, `NOT_FOUND`, or `UNAVAILABLE` for expected tool failures.\n",
6
+ "usage": "---\ntitle: 'Codex: Usage Guide'\ndescription: Install, connect, develop, and debug the Vielzeug MCP server.\n---\n\n[[toc]]\n\n## Basic Usage\n\nRun local stdio server:\n\n```sh\nnpx -y @vielzeug/codex\n```\n\nUse shipped `mcp-setup.json` for machine-readable generic configuration. Client-specific configuration must use its documented MCP format.\n\n## HTTP Mode\n\nHTTP uses Streamable HTTP and binds loopback only:\n\n```sh\nnpx -y @vielzeug/codex --port=3100\ncurl http://127.0.0.1:3100/health\n```\n\nResponse includes snapshot version. Runtime bind validation and Host/Origin allowlists restrict access to localhost; no remote host mode exists. Programmatic HTTP hosting takes a `createServer` factory that returns a fresh, unconnected server with its request handlers registered. Dispose the host asynchronously and observe `disposed` or `disposalSignal` when coordinating shutdown.\n\n## Agent Skill\n\nCodex ships a `vielzeug` agent skill: a `SKILL.md` that tells an AI agent working in _your_ project to discover packages through the MCP tools instead of guessing, to use the owning `@vielzeug/*` package instead of hand-rolling, and to build UI from Refine components, tokens, and theme whenever `@vielzeug/refine` is installed. Install it into the project:\n\n```sh\nnpx -y @vielzeug/codex skills install\nnpx -y @vielzeug/codex skills install --target=.claude/skills\nnpx -y @vielzeug/codex skills install --force # overwrite an existing copy\n```\n\nWithout `--target`, the first existing directory among `.agents/skills`, `.github/skills`, and `.claude/skills` is used; when none exists, `.agents/skills` is created. The command refuses to overwrite an installed skill unless `--force` is passed. Rerun after upgrading codex to pick up the current package list, which is generated from the published manifests. The skill pairs with the MCP server: its lookups (`get-docs`, `refine-get-component`, `refine-validate-usage`, …) assume codex is connected, and it documents `node_modules` fallbacks for when it is not.\n\n## Local Development\n\nRequires Node 22+ and root setup:\n\n```sh\nbun install\ncd packages/codex\nbun run test:unit\nbun run test:integration\nbun run dev\n```\n\n`test:unit` uses fixtures only. `test:integration` regenerates a current snapshot then checks real monorepo inputs.\n\n`bun run dev` watches documentation and package inputs, atomically publishes snapshots, then restarts server when snapshot changes.\n\n## Debugging\n\n```sh\nbun run dev\nnode src/cli.ts --port=3100 --debug\ncurl http://127.0.0.1:3100/health\n```\n\n`--debug` logs tool durations and expected catalog errors to stderr. Build `@vielzeug/refine` before generating snapshot when component metadata changes.\n\n## Programmatic Usage\n\n```ts\nimport { SnapshotCatalog, StdioServerTransport, createMcpServer, loadSnapshot } from '@vielzeug/codex';\n\nconst snapshot = loadSnapshot();\nconst catalog = new SnapshotCatalog(snapshot);\nawait createMcpServer(catalog, { version: snapshot.manifest.version }).connect(new StdioServerTransport());\n```\n\nTo include Refine component tools, create a server with the `@vielzeug/codex/refine` subpath:\n\n```ts\nimport { StdioServerTransport, loadSnapshot } from '@vielzeug/codex';\nimport { createMcpServerWithRefine, SnapshotRefineCatalog } from '@vielzeug/codex/refine';\n\nconst snapshot = loadSnapshot();\nconst catalog = new SnapshotRefineCatalog(snapshot);\nconst server = createMcpServerWithRefine(catalog, { version: snapshot.manifest.version });\nawait server.connect(new StdioServerTransport());\n```\n\n## Best Practices\n\n- Use `search-packages` for capability discovery before loading broad source.\n- Use `get-type-signature` before loading full source.\n- Published package snapshots are static directories; local dev snapshots are immutable generations selected by `.dev/current.json`.\n- Pass `validateContents: true` to `loadSnapshot()` for artifact verification; normal startup keeps package chunks lazy.\n- Return a fresh configured server from `startHttpHost`'s `createServer` factory.\n- Keep HTTP local. Use stdio for normal client integration.\n- Run `bun run test:unit` before `bun run test:integration`.\n",
7
7
  "examples": "---\ntitle: 'Codex: Examples'\ndescription: Practical MCP tool-call examples for package discovery, docs lookup, and Refine component queries.\n---\n\n## Examples\n\n- [Listing Packages](./examples/listing-packages.md)\n- [Searching Packages](./examples/searching-packages.md)\n- [Package Metadata](./examples/package-metadata.md)\n- [Reading Docs](./examples/reading-docs.md)\n- [Running REPL Examples](./examples/running-repl-examples.md)\n- [Looking Up Components](./examples/looking-up-components.md)\n- [Inspector](./examples/inspector.md)\n"
8
8
  },
9
9
  "examples": [],