@vielzeug/codex 26.11.2 → 26.11.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,9 +1,9 @@
1
1
  {
2
2
  "apiSource": "export { animate } from './animate';\nexport { animateEach } from './animate-each';\nexport { NecromancerConfigError, NecromancerError, NecromancerUnsupportedError } from './errors';\nexport { captureLayout } from './layout';\nexport { shouldReduceMotion } from './motion';\nexport type {\n AnimateEachOptions,\n AnimateOptions,\n AnimationGroup,\n AnimationHandle,\n AnimationResult,\n KeyframeFactory,\n Keyframes,\n LayoutAnimationOptions,\n LayoutCaptureOptions,\n LayoutTransition,\n MotionMode,\n} from './types';\n",
3
3
  "docs": {
4
- "index": "---\ntitle: 'Necromancer: Lifecycle-owned DOM animations'\ndescription: Lifecycle-owned Web Animations API primitives for native playback, groups, and additive FLIP transitions.\npackage: necromancer\ncategory: ui\nkeywords: [animation, web-animations-api, waapi, flip, stagger, reduced-motion]\nrelated: [orbit, ore]\nexports: [animate, animateEach, captureLayout]\nenvironments: [browser]\n---\n\n<!-- markdownlint-disable MD025 MD033 MD060 -->\n\n<PackageHero package=\"necromancer\" />\n\n## Why Necromancer?\n\nNative Web Animations API calls do not provide lifecycle ownership, reduced-motion policy, grouped playback, or layout transitions. Necromancer retains native keyframes and timing semantics while making ownership explicit for a component or DOM feature.\n\n```ts\n// Before\nconst animation = element.animate(keyframes, { duration: 180 });\nanimation.addEventListener('cancel', removeListeners);\n\n// After\nconst animation = animate(element, keyframes, { duration: 180 });\nanimation.dispose();\n```\n\n| Feature | Native WAAPI | Necromancer | Motion One |\n| --- | --- | --- | --- |\n| Bundle size | 0 B | <PackageInfo package=\"necromancer\" type=\"size\" /> | ~18 kB |\n| Root dependencies | <ore-icon name=\"check\" size=\"16\"></ore-icon> | <ore-icon name=\"check\" size=\"16\"></ore-icon> | <ore-icon name=\"x\" size=\"16\"></ore-icon> |\n| Lifecycle handle | Manual | `dispose()` | Library-specific controls |\n| Reduced motion | Manual | `motion: 'system'` default | Configuration required |\n| Layout transitions | Manual FLIP math | `captureLayout().animate()` | Separate API |\n\n<div class=\"decision-callout\">\n\n**Use Necromancer when** you need native browser animations with explicit cancellation, reduced-motion behavior, staggered groups, or positional FLIP transitions.\n\n**Consider CSS transitions when** a static style change needs no playback control, cleanup, or layout measurement.\n\n</div>\n\n## Installation\n\n::: code-group\n\n```sh [pnpm]\npnpm add @vielzeug/necromancer\n```\n\n```sh [npm]\nnpm install @vielzeug/necromancer\n```\n\n```sh [yarn]\nyarn add @vielzeug/necromancer\n```\n\n:::\n\n## Quick Start\n\nStart the animation after its DOM element mounts and release it when its UI owner is removed.\n\n```ts\nimport { animate } from '@vielzeug/necromancer';\n\nconst notice = document.createElement('p');\nnotice.textContent = 'Saved';\ndocument.body.append(notice);\n\nconst animation = animate(\n notice,\n [{ opacity: 0, transform: 'translateY(8px)' }, { opacity: 1, transform: 'translateY(0)' }],\n { duration: 180, easing: 'ease-out' },\n);\n\nawait animation.result;\nanimation.dispose();\n```\n\n## Features\n\n<div class=\"features-grid\">\n\n- `animate()`: Native element animation with lifecycle ownership and direct native access\n- `animateEach()`: Group ownership with stable keyframe factories and `stagger`\n- `captureLayout()`: One-shot FLIP transition with additive `translate` (position) and `scale` (size)\n- `motion`: `'system'` reduced-motion support with explicit reduced outcomes\n- Native timing: Omitted timing fields retain Web Animations API behavior\n- `signal`: Abort a handle from its parent lifecycle\n- `dispose()`: Idempotent cleanup with `[Symbol.dispose]()`\n\n</div>\n\n## Deliberate Scope\n\nNecromancer owns explicit WAAPI keyframes. It does not generate CSS keyframes, observe CSS transitions, watch mutations, simulate springs, interpolate SVG paths, or run a JavaScript tween loop. Use CSS for declarative style changes and choose a dedicated tool when those capabilities are required.\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- [3.0 Migration](./migration.md)\n\n</div>\n\n## See Also\n\n<div class=\"see-also\">\n\n- [Orbit](/orbit/): Position floating UI before animating its appearance.\n- [Ore](/ore/): Own Necromancer handles in a custom element's mount and disposal lifecycle.\n</div>\n\n<!-- markdownlint-enable MD025 MD033 MD060 -->\n",
5
- "api": "---\ntitle: 'Necromancer: API Reference'\ndescription: API reference for @vielzeug/necromancer animation ownership, groups, reduced motion, and FLIP transitions.\n---\n\n[[toc]]\n\n## API Overview\n\n| Symbol | Purpose | Execution mode | Common gotcha |\n| --- | --- | --- | --- |\n| `animate()` | Animate one element | Sync | Omitted timing fields use native WAAPI behavior |\n| `animateEach()` | Animate a unique element group | Sync | Non-zero `stagger` needs numeric `delay` |\n| `captureLayout()` | Capture positions and create a one-shot FLIP transition | Sync | Capture before changing layout |\n| `shouldReduceMotion()` | Report whether a motion mode reduces visible movement | Sync | `'system'` reads `prefers-reduced-motion`; false without `matchMedia` |\n| `NecromancerError` | Base package error | Sync | Use `instanceof NecromancerError` to narrow unknown errors |\n\n## Package Entry Point\n\n| Import | Purpose |\n| --- | --- |\n| `@vielzeug/necromancer` | Animation functions, types, and errors |\n| `@vielzeug/necromancer/testing` | jsdom test fakes for `Element.animate()` and `getBoundingClientRect()` |\n\n## Animation Functions\n\n### `animate()`\n\n```ts\nfunction animate(element: Element, keyframes: Keyframes, options?: AnimateOptions): AnimationHandle;\n```\n\nStarts a lifecycle-owned native Web Animation. Native timing options, including an omitted or zero `duration`, are preserved.\n\n| Parameter | Type | Description |\n| --- | --- | --- |\n| `element` | `Element` | Element animated through its native `animate()` method. |\n| `keyframes` | `Keyframes` | Native keyframe array or property-indexed keyframes. |\n| `options` | `AnimateOptions` | Native timing plus motion and cancellation ownership. |\n\n**Returns:** An `AnimationHandle` with native playback access and explicit disposal.\n\n**Example:**\n\n```ts\nimport { animate } from '@vielzeug/necromancer';\n\nconst handle = animate(element, [{ opacity: 0 }, { opacity: 1 }], { duration: 180 });\nhandle.animation.pause();\nconst result = await handle.result;\nhandle.dispose();\n```\n\n---\n\n### `animateEach()`\n\n```ts\nfunction animateEach<ElementType extends Element>(\n elements: Iterable<ElementType>,\n keyframes: Keyframes | KeyframeFactory<ElementType>,\n options?: AnimateEachOptions,\n): AnimationGroup;\n```\n\nStarts animations for unique elements in first-seen order. Necromancer resolves every keyframe factory before starting the first native animation.\n\n| Parameter | Type | Description |\n| --- | --- | --- |\n| `elements` | `Iterable<ElementType>` | Elements animated once in first-seen order. |\n| `keyframes` | `Keyframes \\| KeyframeFactory<ElementType>` | Shared keyframes or a subtype-preserving factory. |\n| `options` | `AnimateEachOptions` | Animation options plus a stagger interval. |\n\n**Returns:** An `AnimationGroup` containing child handles and ordered results.\n\n**Example:**\n\n```ts\nimport { animateEach } from '@vielzeug/necromancer';\n\nconst group = animateEach(document.querySelectorAll<HTMLElement>('.item'), [{ opacity: 0 }, { opacity: 1 }], {\n duration: 180,\n stagger: 30,\n});\nawait group.results;\n```\n\n### `shouldReduceMotion()`\n\n```ts\nfunction shouldReduceMotion(mode: MotionMode): boolean;\n```\n\nReports whether the requested motion preference reduces visible movement: the same check `animate()` applies internally, exposed so hand-rolled effects (scroll behavior, tweens) stay consistent with it. `'full'` is always `false`, `'reduced'` is always `true`, and `'system'` reads the `prefers-reduced-motion` media query (`false` where `matchMedia` is unavailable).\n\n```ts\nimport { shouldReduceMotion } from '@vielzeug/necromancer';\n\nelement.scrollIntoView({ behavior: shouldReduceMotion('system') ? 'auto' : 'smooth' });\n```\n\n## Layout Functions\n\n### `captureLayout()`\n\n```ts\nfunction captureLayout<ElementType extends Element>(\n elements: Iterable<ElementType>,\n options?: LayoutCaptureOptions<ElementType>,\n): LayoutTransition<ElementType>;\n```\n\nCaptures unique elements' positions and sizes and returns a one-shot transition. Rotation and other transforms are not captured or compensated.\n\n| Parameter | Type | Description |\n| --- | --- | --- |\n| `elements` | `Iterable<ElementType>` | Elements measured before a layout change. |\n| `options` | `LayoutCaptureOptions<ElementType>` | Optional stable key mapping for replacement nodes. |\n\n**Returns:** A `LayoutTransition<ElementType>` that measures committed layout once and creates an `AnimationGroup`.\n\n**Example:**\n\n```ts\nimport { captureLayout } from '@vielzeug/necromancer';\n\nconst transition = captureLayout(beforeItems, {\n getKey: (element) => element.getAttribute('data-id')!,\n});\n\nrenderReorderedItems();\n\nconst group = transition.animate({\n duration: 220,\n easing: 'ease-out',\n elements: afterItems,\n});\n```\n\nCalling `animate()` twice on the same transition throws `NecromancerConfigError`.\n\n## Types\n\n### `MotionMode`\n\n```ts\ntype MotionMode = 'full' | 'reduced' | 'system';\n```\n\n`'system'` is the default. Reduced motion preserves the supplied keyframes while normalizing delay, duration, and end delay to `0`, and iterations to `1`.\n\n### `AnimationResult`\n\n```ts\ntype AnimationResult =\n | { readonly status: 'finished' }\n | { readonly status: 'reduced' }\n | { readonly reason?: unknown; readonly status: 'cancelled' };\n```\n\n`cancelled` describes native cancellation and includes its native rejection reason. A reason passed to `dispose()` or an abort signal takes precedence. The independent `disposed` property becomes `true` only when the lifecycle owner is explicitly disposed.\n\n### `AnimateOptions`\n\n```ts\ntype AnimateOptions = KeyframeAnimationOptions & {\n readonly motion?: MotionMode;\n readonly signal?: AbortSignal;\n};\n```\n\nNative timing fields pass through unchanged unless reduced motion normalizes timing. `signal` disposes the returned handle when aborted.\n\n### `AnimateEachOptions`\n\n```ts\ntype AnimateEachOptions = AnimateOptions & {\n readonly stagger?: number;\n};\n```\n\n`stagger` is a finite, non-negative millisecond offset.\n\n### `LayoutCaptureOptions`\n\n```ts\ninterface LayoutCaptureOptions<ElementType extends Element = Element> {\n readonly getKey?: (element: ElementType) => string;\n}\n```\n\n`getKey` maps a captured element and its committed replacement to the same stable, non-empty string. Duplicate or empty keys throw `NecromancerConfigError`.\n\n### `LayoutAnimationOptions`\n\n```ts\ntype LayoutAnimationOptions<ElementType extends Element = Element> = AnimateEachOptions & {\n readonly elements?: Iterable<ElementType>;\n};\n```\n\n`elements` is the collection in its committed layout. Omit it to animate the same captured elements. With `getKey`, replacement elements animate from the positions of their captured predecessors. Unmatched, removed, and newly entered elements are ignored.\n\n### `Keyframes` and `KeyframeFactory`\n\n```ts\ntype Keyframes = readonly Keyframe[] | PropertyIndexedKeyframes;\ntype KeyframeFactory<ElementType extends Element = Element> = (\n element: ElementType,\n index: number,\n total: number,\n) => Keyframes;\n```\n\nAccepts a `readonly` array so a reusable `as const` keyframe list can be passed without a cast.\n\n### `AnimationHandle`\n\n```ts\ninterface AnimationHandle {\n readonly animation: Animation;\n readonly result: Promise<AnimationResult>;\n readonly disposed: boolean;\n dispose(reason?: unknown): void;\n [Symbol.dispose](): void;\n}\n```\n\n| Member | Description |\n| --- | --- |\n| `animation` | Owned native animation used for playback control. |\n| `result` | Resolves with the first terminal outcome. |\n| `disposed` | Reports explicit owner disposal. |\n| `dispose(reason?)` | Cancels the animation and releases its abort listener. |\n| `[Symbol.dispose]()` | Delegates to `dispose()`. |\n\n### `AnimationGroup`\n\n```ts\ninterface AnimationGroup {\n readonly handles: readonly AnimationHandle[];\n readonly results: Promise<readonly AnimationResult[]>;\n readonly disposed: boolean;\n dispose(reason?: unknown): void;\n [Symbol.dispose](): void;\n}\n```\n\n| Member | Description |\n| --- | --- |\n| `handles` | Child handles in input order. |\n| `results` | Resolves with every child outcome in handle order. |\n| `disposed` | Reports explicit group disposal. |\n| `dispose(reason?)` | Disposes every child. |\n| `[Symbol.dispose]()` | Delegates to `dispose()`. |\n\n### `LayoutTransition`\n\n```ts\ninterface LayoutTransition<ElementType extends Element = Element> {\n animate(options?: LayoutAnimationOptions<ElementType>): AnimationGroup;\n}\n```\n\n| Method | Description |\n| --- | --- |\n| `animate(options?)` | Measures committed elements and creates the one allowed layout animation group. |\n\n## Errors\n\n| Error | Trigger |\n| --- | --- |\n| `NecromancerError` | Base class for package errors |\n| `NecromancerConfigError` | Invalid stagger, incompatible delay, or reused layout transition |\n| `NecromancerUnsupportedError` | `Element.animate()` is unavailable |\n\n## Testing (`@vielzeug/necromancer/testing`)\n\njsdom (and most non-browser DOM environments) do not implement `Element.animate()`. Import these from the `/testing` sub-path, not the root entry point.\n\n### `AnimationCall`\n\n```ts\ntype AnimationCall = {\n readonly animation: FakeAnimation;\n readonly keyframes: Keyframe[] | PropertyIndexedKeyframes;\n readonly options?: KeyframeAnimationOptions;\n};\n```\n\nOne recorded invocation of `Element.prototype.animate` from `installFakeAnimations()`.\n\n### `installFakeAnimations()`\n\n```ts\nfunction installFakeAnimations(): {\n readonly calls: readonly AnimationCall[];\n restore(): void;\n [Symbol.dispose](): void;\n};\n```\n\nReplaces `Element.prototype.animate` with a deterministic lifecycle fake for the duration of a test. `calls` records every invocation in order. Call `restore()` or use an explicit resource-management `using` declaration to put the original implementation back. The fake does not implement native playback controls such as `pause()` or `reverse()`.\n\n```ts\nimport { installFakeAnimations } from '@vielzeug/necromancer/testing';\n\nusing animations = installFakeAnimations();\nconst handle = animate(element, [{ opacity: 0 }, { opacity: 1 }]);\n\nanimations.calls[0]?.animation.finish();\nawait handle.result; // { status: 'finished' }\n```\n\n### `FakeAnimation`\n\n```ts\nclass FakeAnimation {\n cancelCallCount: number;\n finishCallCount: number;\n finished: Promise<void>;\n cancel(): void;\n finish(): void;\n}\n```\n\nA minimal `Animation` stand-in. `cancel()` rejects `finished` with an `AbortError`; `finish()` resolves it. `cancelCallCount`/`finishCallCount` track how many times each was called, in place of a test-runner-specific spy.\n\n### `createRect()`\n\n```ts\nfunction createRect(x: number, y: number, width?: number, height?: number): DOMRect;\n```\n\nBuilds a `DOMRect` for mocking `Element.getBoundingClientRect()` in `captureLayout()` tests. `width`/`height` default to `20`.\n",
6
- "usage": "---\ntitle: 'Necromancer: Usage Guide'\ndescription: Animate DOM elements, coordinate groups, and create FLIP transitions with @vielzeug/necromancer.\n---\n\n[[toc]]\n\n## Basic Usage\n\nCreate an animation after its element mounts, control playback through the native `Animation`, and dispose its owner with the UI lifecycle.\n\n```ts\nimport { animate } from '@vielzeug/necromancer';\n\nconst handle = animate(\n element,\n [{ opacity: 0, transform: 'translateY(8px)' }, { opacity: 1, transform: 'translateY(0)' }],\n { duration: 180, easing: 'ease-out', fill: 'both' },\n);\n\nhandle.animation.reverse();\nconst result = await handle.result;\nhandle.dispose();\n```\n\n`result` distinguishes natural completion, reduced timing, and cancellation. `disposed` reports only whether the owner was explicitly disposed.\n\nOmitted timing fields retain native Web Animations API behavior. Pass an explicit `duration` when the animation should run over time.\n\n## Motion Preferences\n\nUse `motion` to select how the animation responds to the operating system preference.\n\n```ts\nconst handle = animate(element, [{ opacity: 0 }, { opacity: 1 }], {\n duration: 200,\n motion: 'system',\n});\n\nconst result = await handle.result;\n```\n\n`'system'` is the default and reduces movement when `prefers-reduced-motion: reduce` matches. `'full'` preserves the requested timing, while `'reduced'` always reduces it.\n\nReduced motion keeps the supplied keyframes but normalizes delay, duration, and end delay to zero and iterations to one. The result is `{ status: 'reduced' }`, and `handle.animation` still represents the requested visual transition.\n\n## Parent Cancellation\n\nPass a parent `AbortSignal` to release an animation when its owning work is cancelled.\n\n```ts\nconst controller = new AbortController();\nconst handle = animate(element, [{ scale: 0.96 }, { scale: 1 }], {\n duration: 160,\n signal: controller.signal,\n});\n\ncontroller.abort('route changed');\nconst result = await handle.result;\n// { status: 'cancelled', reason: 'route changed' }\n```\n\nAn already-aborted signal throws its reason before an animation starts.\n\n## Staggering a Group\n\nPass an iterable of elements to `animateEach()`. Duplicate elements are animated once in first-seen order.\n\n```ts\nimport { animateEach } from '@vielzeug/necromancer';\n\nconst group = animateEach(\n document.querySelectorAll('.card'),\n (_card, index) => [\n { opacity: 0, transform: `translateY(${12 + index * 2}px)` },\n { opacity: 1, transform: 'translateY(0)' },\n ],\n { duration: 220, easing: 'ease-out', stagger: 45 },\n);\n\nconst results = await group.results;\ngroup.dispose();\n```\n\nA group owns child lifecycles only. `results` preserves every child result in handle order; use `group.handles` when native playback control is required.\n\n## Serial Application Flow\n\nJavaScript control flow is the clearest way to express serial, conditional, or branching animations:\n\n```ts\nfor (const step of steps) {\n const handle = animate(step.element, step.keyframes, {\n ...step.options,\n signal: controller.signal,\n });\n const result = await handle.result;\n\n if (result.status === 'cancelled') break;\n}\n```\n\nOne parent `AbortSignal` cancels the active step without introducing a separate timeline abstraction.\n\n## Animating a Reorder with FLIP\n\nCapture positions before changing layout, then animate through the returned one-shot transition.\n\n```ts\nimport { captureLayout } from '@vielzeug/necromancer';\n\nconst transition = captureLayout(items);\nlist.prepend(items[2]!);\n\nconst group = transition.animate({ duration: 220, easing: 'ease-out' });\nawait group.results;\ngroup.dispose();\n```\n\nThe transition only animates changed, connected elements and can be animated once. It additively composes the individual CSS `translate` and `scale` properties, preserving authored `transform`, `translate`, and `scale`. A resized element (for example a list item whose content changed) animates from its captured size as well as its captured position.\n\n### Replacing rendered elements\n\nWhen a framework replaces list nodes rather than reorders the captured elements, give `captureLayout()` a stable key and pass the committed nodes to `animate()`. Capture before updating state, then call `animate()` only after the renderer has committed the new DOM.\n\n```ts\nconst transition = captureLayout(beforeItems, {\n getKey: (element) => element.getAttribute('data-id')!,\n});\n\nrenderReorderedItems();\n\ntransition.animate({\n duration: 220,\n easing: 'ease-out',\n elements: afterItems,\n});\n```\n\nKeys must be unique, non-empty strings in both collections. Items with no matching predecessor are not enter animations; animate those explicitly with `animate()` or `animateEach()`.\n\nFor sortable lists, DnD exposes its pre-commit layout seam through `onBeforeReorder`; see the [DnD optimistic-reorder recipe](/dnd/examples/optimistic-reorder-with-revert.md).\n\n## Scope\n\nNecromancer creates and owns explicit Web Animations API work. It does not observe CSS-authored transitions or animations, inject `@keyframes`, watch DOM mutations, generate springs, interpolate SVG geometry, or provide a JavaScript tween fallback. Keep CSS as the owner of declarative component styling and use a dedicated charting or tweening tool when the animation needs capabilities beyond WAAPI keyframes.\n\n## Testing\n\njsdom does not implement `Element.animate()`, so lifecycle tests need a fake. `@vielzeug/necromancer/testing` has no test-runner import and supports explicit resource management.\n\n```ts\nimport { animate } from '@vielzeug/necromancer';\nimport { installFakeAnimations } from '@vielzeug/necromancer/testing';\n\nusing animations = installFakeAnimations();\nconst handle = animate(element, [{ opacity: 0 }, { opacity: 1 }]);\n\nanimations.calls[0]?.animation.finish();\nawait handle.result; // { status: 'finished' }\n```\n\nUse `restore()` when `using` is unavailable. The lifecycle fake supports `finish()`, `cancel()`, and `finished`; use browser tests for native playback controls. Use `createRect()` to mock `Element.getBoundingClientRect()` when testing code that calls `captureLayout()`.\n\n## Framework Integration\n\nCreate handles in a client mount lifecycle and dispose them during unmount. The same composition works with reactive effect systems: start the animation in the effect and return `handle.dispose()` as its cleanup.\n\n```tsx\nimport { useEffect, useRef } from 'react';\nimport { animate } from '@vielzeug/necromancer';\n\nexport function Notice() {\n const elementRef = useRef<HTMLDivElement>(null);\n\n useEffect(() => {\n const element = elementRef.current;\n if (!element) return;\n\n const handle = animate(element, [{ opacity: 0 }, { opacity: 1 }], { duration: 180 });\n return () => handle.dispose();\n }, []);\n\n return <div ref={elementRef}>Saved</div>;\n}\n```\n\n## Working with Other Vielzeug Libraries\n\nUse Dnd's `onBeforeReorder` hook to capture item layout before a sortable commits its DOM update. Animate the captured transition after `onReorder`; see the [optimistic reorder recipe](/dnd/examples/optimistic-reorder-with-revert.md).\n\n## Best Practices\n\n- Start animations only after their elements mount in the browser.\n- Set animation timing explicitly when native zero-duration behavior is not intended.\n- Dispose each handle or group with its UI owner.\n- Use native `Animation` objects for playback control.\n- Respect the default `'system'` motion setting unless movement is essential.\n- Use a parent `AbortSignal` for cancellable application flow.\n- Keep `delay` numeric when combining it with non-zero `stagger`.\n- Capture layout before mutation and animate each transition exactly once.\n",
4
+ "index": "---\ntitle: 'Necromancer: Lifecycle-owned DOM animations'\ndescription: Lifecycle-owned Web Animations API primitives for native playback, groups, and additive FLIP transitions.\npackage: necromancer\ncategory: ui\nkeywords: [animation, web-animations-api, waapi, flip, stagger, reduced-motion]\nrelated: [orbit, ore]\nexports: [animate, animateEach, captureLayout]\nenvironments: [browser]\n---\n\n<!-- markdownlint-disable MD025 MD033 MD060 -->\n\n<PackageHero package=\"necromancer\" />\n\n## Why Necromancer?\n\nNative Web Animations API calls do not provide lifecycle ownership, reduced-motion policy, grouped playback, or layout transitions. Necromancer retains native keyframes and timing semantics while making ownership explicit for a component or DOM feature.\n\n```ts\n// Before\nconst animation = element.animate(keyframes, { duration: 180 });\nanimation.addEventListener('cancel', removeListeners);\n\n// After\nconst animation = animate(element, keyframes, { duration: 180 });\nanimation.dispose();\n```\n\n| Feature | Native WAAPI | Necromancer | Motion One |\n| --- | --- | --- | --- |\n| Bundle size | 0 B | <PackageInfo package=\"necromancer\" type=\"size\" /> | ~18 kB |\n| Root dependencies | <ore-icon name=\"check\" size=\"16\"></ore-icon> | <ore-icon name=\"check\" size=\"16\"></ore-icon> | <ore-icon name=\"x\" size=\"16\"></ore-icon> |\n| Lifecycle handle | Manual | `dispose()` | Library-specific controls |\n| Reduced motion | Manual | `motion: 'system'` default | Configuration required |\n| Layout transitions | Manual FLIP math | `captureLayout().animate()` | Separate API |\n\n<div class=\"decision-callout\">\n\n**Use Necromancer when** you need native browser animations with explicit cancellation, reduced-motion behavior, staggered groups, or positional FLIP transitions.\n\n**Consider CSS transitions when** a static style change needs no playback control, cleanup, or layout measurement.\n\n</div>\n\n## Installation\n\n::: code-group\n\n```sh [pnpm]\npnpm add @vielzeug/necromancer\n```\n\n```sh [npm]\nnpm install @vielzeug/necromancer\n```\n\n```sh [yarn]\nyarn add @vielzeug/necromancer\n```\n\n:::\n\n## Quick Start\n\nStart the animation after its DOM element mounts and release it when its UI owner is removed.\n\n```ts\nimport { animate } from '@vielzeug/necromancer';\n\nconst notice = document.createElement('p');\nnotice.textContent = 'Saved';\ndocument.body.append(notice);\n\nconst animation = animate(\n notice,\n [{ opacity: 0, transform: 'translateY(8px)' }, { opacity: 1, transform: 'translateY(0)' }],\n { duration: 180, easing: 'ease-out' },\n);\n\nawait animation.result;\nanimation.dispose();\n```\n\n## Features\n\n<div class=\"features-grid\">\n\n- `animate()`: Native element animation with lifecycle ownership and direct native access\n- `animateEach()`: Group ownership with stable keyframe factories and `stagger`\n- `captureLayout()`: One-shot FLIP transition with additive `translate` (position) and `scale` (size)\n- `motion`: `'system'` reduced-motion support with the same keyframes on instant timing\n- Native timing: Omitted timing fields retain Web Animations API behavior\n- `signal`: Abort a handle from its parent lifecycle\n- `dispose()`: Idempotent cleanup with `[Symbol.dispose]()`\n\n</div>\n\n## Deliberate Scope\n\nNecromancer owns explicit WAAPI keyframes. It does not generate CSS keyframes, observe CSS transitions, watch mutations, simulate springs, interpolate SVG paths, or run a JavaScript tween loop. Use CSS for declarative style changes and choose a dedicated tool when those capabilities are required.\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- [Orbit](/orbit/): Position floating UI before animating its appearance.\n- [Ore](/ore/): Own Necromancer handles in a custom element's mount and disposal lifecycle.\n</div>\n\n<!-- markdownlint-enable MD025 MD033 MD060 -->\n",
5
+ "api": "---\ntitle: 'Necromancer: API Reference'\ndescription: API reference for @vielzeug/necromancer animation ownership, groups, reduced motion, and FLIP transitions.\n---\n\n[[toc]]\n\n## API Overview\n\n| Symbol | Purpose | Execution mode | Common gotcha |\n| --- | --- | --- | --- |\n| `animate()` | Animate one element | Sync | Omitted timing fields use native WAAPI behavior |\n| `animateEach()` | Animate a unique element group | Sync | Non-zero `stagger` needs numeric `delay` |\n| `captureLayout()` | Capture positions and create a one-shot FLIP transition | Sync | Capture before changing layout |\n| `shouldReduceMotion()` | Report whether a motion mode reduces visible movement | Sync | `'system'` reads `prefers-reduced-motion`; false without `matchMedia` |\n| `NecromancerError` | Base package error | Sync | Use `instanceof NecromancerError` to narrow unknown errors |\n\n## Package Entry Point\n\n| Import | Purpose |\n| --- | --- |\n| `@vielzeug/necromancer` | Animation functions, types, and errors |\n| `@vielzeug/necromancer/testing` | jsdom test fakes for `Element.animate()` and `getBoundingClientRect()` |\n\n## Animation Functions\n\n### `animate()`\n\n```ts\nfunction animate(element: Element, keyframes: Keyframes, options?: AnimateOptions): AnimationHandle;\n```\n\nStarts a lifecycle-owned native Web Animation. Native timing options, including an omitted or zero `duration`, are preserved.\n\n| Parameter | Type | Description |\n| --- | --- | --- |\n| `element` | `Element` | Element animated through its native `animate()` method. |\n| `keyframes` | `Keyframes` | Native keyframe array or property-indexed keyframes. |\n| `options` | `AnimateOptions` | Native timing plus motion and cancellation ownership. |\n\n**Returns:** An `AnimationHandle` with native playback access and explicit disposal.\n\n**Example:**\n\n```ts\nimport { animate } from '@vielzeug/necromancer';\n\nconst handle = animate(element, [{ opacity: 0 }, { opacity: 1 }], { duration: 180 });\nhandle.animation.pause();\nconst result = await handle.result;\nhandle.dispose();\n```\n\n---\n\n### `animateEach()`\n\n```ts\nfunction animateEach<ElementType extends Element>(\n elements: Iterable<ElementType>,\n keyframes: Keyframes | KeyframeFactory<ElementType>,\n options?: AnimateEachOptions,\n): AnimationGroup;\n```\n\nStarts animations for unique elements in first-seen order. Necromancer resolves every keyframe factory before starting the first native animation.\n\n| Parameter | Type | Description |\n| --- | --- | --- |\n| `elements` | `Iterable<ElementType>` | Elements animated once in first-seen order. |\n| `keyframes` | `Keyframes \\| KeyframeFactory<ElementType>` | Shared keyframes or a subtype-preserving factory. |\n| `options` | `AnimateEachOptions` | Animation options plus a stagger interval. |\n\n**Returns:** An `AnimationGroup` containing child handles and ordered results.\n\n**Example:**\n\n```ts\nimport { animateEach } from '@vielzeug/necromancer';\n\nconst group = animateEach(document.querySelectorAll<HTMLElement>('.item'), [{ opacity: 0 }, { opacity: 1 }], {\n duration: 180,\n stagger: 30,\n});\nawait group.results;\n```\n\n### `shouldReduceMotion()`\n\n```ts\nfunction shouldReduceMotion(mode: MotionMode): boolean;\n```\n\nReports whether the requested motion preference reduces visible movement: the same check `animate()` applies internally, exposed so hand-rolled effects (scroll behavior, tweens) stay consistent with it. `'full'` is always `false`, `'reduced'` is always `true`, and `'system'` reads the `prefers-reduced-motion` media query (`false` where `matchMedia` is unavailable).\n\n```ts\nimport { shouldReduceMotion } from '@vielzeug/necromancer';\n\nelement.scrollIntoView({ behavior: shouldReduceMotion('system') ? 'auto' : 'smooth' });\n```\n\n## Layout Functions\n\n### `captureLayout()`\n\n```ts\nfunction captureLayout<ElementType extends Element>(\n elements: Iterable<ElementType>,\n options?: LayoutCaptureOptions<ElementType>,\n): LayoutTransition<ElementType>;\n```\n\nCaptures unique elements' positions and sizes and returns a one-shot transition. Rotation and other transforms are not captured or compensated.\n\n| Parameter | Type | Description |\n| --- | --- | --- |\n| `elements` | `Iterable<ElementType>` | Elements measured before a layout change. |\n| `options` | `LayoutCaptureOptions<ElementType>` | Optional stable key mapping for replacement nodes. |\n\n**Returns:** A `LayoutTransition<ElementType>` that measures committed layout once and creates an `AnimationGroup`.\n\n**Example:**\n\n```ts\nimport { captureLayout } from '@vielzeug/necromancer';\n\nconst transition = captureLayout(beforeItems, {\n getKey: (element) => element.getAttribute('data-id')!,\n});\n\nrenderReorderedItems();\n\nconst group = transition.animate({\n duration: 220,\n easing: 'ease-out',\n elements: afterItems,\n});\n```\n\nCalling `animate()` twice on the same transition throws `NecromancerConfigError`.\n\n## Types\n\n### `MotionMode`\n\n```ts\ntype MotionMode = 'full' | 'reduced' | 'system';\n```\n\n`'system'` is the default. Reduced motion preserves the supplied keyframes while normalizing delay, duration, and end delay to `0`, and iterations to `1`.\n\n### `AnimationResult`\n\n```ts\ntype AnimationResult =\n | { readonly status: 'finished' }\n | { readonly reason?: unknown; readonly status: 'cancelled' };\n```\n\nA reduced animation finishes: reduction is decided at call time and `shouldReduceMotion()` answers it, so the result reports only what the caller could not know. `cancelled` carries the reason the animation was cancelled: the value passed to `dispose()`, the abort reason of the owning `signal`, or the native rejection reason when something outside Necromancer cancelled the animation. A bare `dispose()` carries no reason. The independent `disposed` property becomes `true` only when the lifecycle owner is explicitly disposed.\n\n### `AnimateOptions`\n\n```ts\ntype AnimateOptions = KeyframeAnimationOptions & {\n readonly motion?: MotionMode;\n readonly signal?: AbortSignal;\n};\n```\n\nNative timing fields pass through unchanged unless reduced motion normalizes timing. `signal` disposes the returned handle when aborted.\n\n### `AnimateEachOptions`\n\n```ts\ntype AnimateEachOptions = AnimateOptions & {\n readonly stagger?: number;\n};\n```\n\n`stagger` is a finite, non-negative millisecond offset.\n\n### `LayoutCaptureOptions`\n\n```ts\ninterface LayoutCaptureOptions<ElementType extends Element = Element> {\n readonly getKey?: (element: ElementType) => string;\n}\n```\n\n`getKey` maps a captured element and its committed replacement to the same stable, non-empty string. Duplicate or empty keys throw `NecromancerConfigError`.\n\n### `LayoutAnimationOptions`\n\n```ts\ntype LayoutAnimationOptions<ElementType extends Element = Element> = AnimateEachOptions & {\n readonly elements?: Iterable<ElementType>;\n};\n```\n\n`elements` is the collection in its committed layout. Omit it to animate the same captured elements. With `getKey`, replacement elements animate from the positions of their captured predecessors. Unmatched, removed, and newly entered elements are ignored.\n\n### `Keyframes` and `KeyframeFactory`\n\n```ts\ntype Keyframes = readonly Keyframe[] | PropertyIndexedKeyframes;\ntype KeyframeFactory<ElementType extends Element = Element> = (\n element: ElementType,\n index: number,\n total: number,\n) => Keyframes;\n```\n\nAccepts a `readonly` array so a reusable `as const` keyframe list can be passed without a cast.\n\n### `AnimationHandle`\n\n```ts\ninterface AnimationHandle {\n readonly animation: Animation;\n readonly result: Promise<AnimationResult>;\n readonly disposed: boolean;\n dispose(reason?: unknown): void;\n [Symbol.dispose](): void;\n}\n```\n\n| Member | Description |\n| --- | --- |\n| `animation` | Owned native animation used for playback control. |\n| `result` | Resolves with the first terminal outcome. |\n| `disposed` | Reports explicit owner disposal. |\n| `dispose(reason?)` | Cancels the animation and releases its abort listener. |\n| `[Symbol.dispose]()` | Delegates to `dispose()`. |\n\n### `AnimationGroup`\n\n```ts\ninterface AnimationGroup {\n readonly handles: readonly AnimationHandle[];\n readonly results: Promise<readonly AnimationResult[]>;\n readonly disposed: boolean;\n dispose(reason?: unknown): void;\n [Symbol.dispose](): void;\n}\n```\n\n| Member | Description |\n| --- | --- |\n| `handles` | Child handles in input order. |\n| `results` | Resolves with every child outcome in handle order. |\n| `disposed` | Reports explicit group disposal. |\n| `dispose(reason?)` | Disposes every child. |\n| `[Symbol.dispose]()` | Delegates to `dispose()`. |\n\n### `LayoutTransition`\n\n```ts\ninterface LayoutTransition<ElementType extends Element = Element> {\n animate(options?: LayoutAnimationOptions<ElementType>): AnimationGroup;\n}\n```\n\n| Method | Description |\n| --- | --- |\n| `animate(options?)` | Measures committed elements and creates the one allowed layout animation group. |\n\n## Errors\n\n| Error | Trigger |\n| --- | --- |\n| `NecromancerError` | Base class for package errors |\n| `NecromancerConfigError` | Invalid stagger, incompatible delay, or reused layout transition |\n| `NecromancerUnsupportedError` | `Element.animate()` is unavailable |\n\n## Testing (`@vielzeug/necromancer/testing`)\n\njsdom (and most non-browser DOM environments) do not implement `Element.animate()`. Import these from the `/testing` sub-path, not the root entry point.\n\n### `AnimationCall`\n\n```ts\ntype AnimationCall = {\n readonly animation: FakeAnimation;\n readonly keyframes: Keyframe[] | PropertyIndexedKeyframes;\n readonly options?: KeyframeAnimationOptions;\n};\n```\n\nOne recorded invocation of `Element.prototype.animate` from `installFakeAnimations()`.\n\n### `installFakeAnimations()`\n\n```ts\nfunction installFakeAnimations(): {\n readonly calls: readonly AnimationCall[];\n restore(): void;\n [Symbol.dispose](): void;\n};\n```\n\nReplaces `Element.prototype.animate` with a deterministic lifecycle fake for the duration of a test. `calls` records every invocation in order. Call `restore()` or use an explicit resource-management `using` declaration to put the original implementation back. The fake does not implement native playback controls such as `pause()` or `reverse()`.\n\n```ts\nimport { installFakeAnimations } from '@vielzeug/necromancer/testing';\n\nusing animations = installFakeAnimations();\nconst handle = animate(element, [{ opacity: 0 }, { opacity: 1 }]);\n\nanimations.calls[0]?.animation.finish();\nawait handle.result; // { status: 'finished' }\n```\n\n### `FakeAnimation`\n\n```ts\nclass FakeAnimation {\n cancelCallCount: number;\n finishCallCount: number;\n finished: Promise<void>;\n cancel(): void;\n finish(): void;\n}\n```\n\nA minimal `Animation` stand-in. `cancel()` rejects `finished` with an `AbortError`; `finish()` resolves it. `cancelCallCount`/`finishCallCount` track how many times each was called, in place of a test-runner-specific spy.\n\n### `createRect()`\n\n```ts\nfunction createRect(x: number, y: number, width?: number, height?: number): DOMRect;\n```\n\nBuilds a `DOMRect` for mocking `Element.getBoundingClientRect()` in `captureLayout()` tests. `width`/`height` default to `20`.\n",
6
+ "usage": "---\ntitle: 'Necromancer: Usage Guide'\ndescription: Animate DOM elements, coordinate groups, and create FLIP transitions with @vielzeug/necromancer.\n---\n\n[[toc]]\n\n## Basic Usage\n\nCreate an animation after its element mounts, control playback through the native `Animation`, and dispose its owner with the UI lifecycle.\n\n```ts\nimport { animate } from '@vielzeug/necromancer';\n\nconst handle = animate(\n element,\n [{ opacity: 0, transform: 'translateY(8px)' }, { opacity: 1, transform: 'translateY(0)' }],\n { duration: 180, easing: 'ease-out', fill: 'both' },\n);\n\nhandle.animation.reverse();\nconst result = await handle.result;\nhandle.dispose();\n```\n\n`result` distinguishes natural completion from cancellation. `disposed` reports only whether the owner was explicitly disposed.\n\nOmitted timing fields retain native Web Animations API behavior. Pass an explicit `duration` when the animation should run over time.\n\n## Motion Preferences\n\nUse `motion` to select how the animation responds to the operating system preference.\n\n```ts\nconst handle = animate(element, [{ opacity: 0 }, { opacity: 1 }], {\n duration: 200,\n motion: 'system',\n});\n\nconst result = await handle.result;\n```\n\n`'system'` is the default and reduces movement when `prefers-reduced-motion: reduce` matches. `'full'` preserves the requested timing, while `'reduced'` always reduces it.\n\nReduced motion keeps the supplied keyframes but normalizes delay, duration, and end delay to zero and iterations to one. The animation finishes immediately, so its result is `{ status: 'finished' }`; call `shouldReduceMotion()` with the same mode when your own code needs to know whether movement was reduced. `handle.animation` still represents the requested visual transition.\n\n## Parent Cancellation\n\nPass a parent `AbortSignal` to release an animation when its owning work is cancelled.\n\n```ts\nconst controller = new AbortController();\nconst handle = animate(element, [{ scale: 0.96 }, { scale: 1 }], {\n duration: 160,\n signal: controller.signal,\n});\n\ncontroller.abort('route changed');\nconst result = await handle.result;\n// { status: 'cancelled', reason: 'route changed' }\n```\n\nAn already-aborted signal throws its reason before an animation starts.\n\n## Staggering a Group\n\nPass an iterable of elements to `animateEach()`. Duplicate elements are animated once in first-seen order.\n\n```ts\nimport { animateEach } from '@vielzeug/necromancer';\n\nconst group = animateEach(\n document.querySelectorAll('.card'),\n (_card, index) => [\n { opacity: 0, transform: `translateY(${12 + index * 2}px)` },\n { opacity: 1, transform: 'translateY(0)' },\n ],\n { duration: 220, easing: 'ease-out', stagger: 45 },\n);\n\nconst results = await group.results;\ngroup.dispose();\n```\n\nA group owns child lifecycles only. `results` preserves every child result in handle order; use `group.handles` when native playback control is required.\n\n## Serial Application Flow\n\nJavaScript control flow is the clearest way to express serial, conditional, or branching animations:\n\n```ts\nfor (const step of steps) {\n const handle = animate(step.element, step.keyframes, {\n ...step.options,\n signal: controller.signal,\n });\n const result = await handle.result;\n\n if (result.status === 'cancelled') break;\n}\n```\n\nOne parent `AbortSignal` cancels the active step without introducing a separate timeline abstraction.\n\n## Animating a Reorder with FLIP\n\nCapture positions before changing layout, then animate through the returned one-shot transition.\n\n```ts\nimport { captureLayout } from '@vielzeug/necromancer';\n\nconst transition = captureLayout(items);\nlist.prepend(items[2]!);\n\nconst group = transition.animate({ duration: 220, easing: 'ease-out' });\nawait group.results;\ngroup.dispose();\n```\n\nThe transition only animates changed, connected elements and can be animated once. It additively composes the individual CSS `translate` and `scale` properties, preserving authored `transform`, `translate`, and `scale`. A resized element (for example a list item whose content changed) animates from its captured size as well as its captured position.\n\n### Replacing rendered elements\n\nWhen a framework replaces list nodes rather than reorders the captured elements, give `captureLayout()` a stable key and pass the committed nodes to `animate()`. Capture before updating state, then call `animate()` only after the renderer has committed the new DOM.\n\n```ts\nconst transition = captureLayout(beforeItems, {\n getKey: (element) => element.getAttribute('data-id')!,\n});\n\nrenderReorderedItems();\n\ntransition.animate({\n duration: 220,\n easing: 'ease-out',\n elements: afterItems,\n});\n```\n\nKeys must be unique, non-empty strings in both collections. Items with no matching predecessor are not enter animations; animate those explicitly with `animate()` or `animateEach()`.\n\nFor sortable lists, DnD exposes its pre-commit layout seam through `onBeforeReorder`; see the [DnD optimistic-reorder recipe](/dnd/examples/optimistic-reorder-with-revert.md).\n\n## Scope\n\nNecromancer creates and owns explicit Web Animations API work. It does not observe CSS-authored transitions or animations, inject `@keyframes`, watch DOM mutations, generate springs, interpolate SVG geometry, or provide a JavaScript tween fallback. Keep CSS as the owner of declarative component styling and use a dedicated charting or tweening tool when the animation needs capabilities beyond WAAPI keyframes.\n\n## Testing\n\njsdom does not implement `Element.animate()`, so lifecycle tests need a fake. `@vielzeug/necromancer/testing` has no test-runner import and supports explicit resource management.\n\n```ts\nimport { animate } from '@vielzeug/necromancer';\nimport { installFakeAnimations } from '@vielzeug/necromancer/testing';\n\nusing animations = installFakeAnimations();\nconst handle = animate(element, [{ opacity: 0 }, { opacity: 1 }]);\n\nanimations.calls[0]?.animation.finish();\nawait handle.result; // { status: 'finished' }\n```\n\nUse `restore()` when `using` is unavailable. The lifecycle fake supports `finish()`, `cancel()`, and `finished`; use browser tests for native playback controls. Use `createRect()` to mock `Element.getBoundingClientRect()` when testing code that calls `captureLayout()`.\n\n## Framework Integration\n\nCreate handles in a client mount lifecycle and dispose them during unmount. The same composition works with reactive effect systems: start the animation in the effect and return `handle.dispose()` as its cleanup.\n\n```tsx\nimport { useEffect, useRef } from 'react';\nimport { animate } from '@vielzeug/necromancer';\n\nexport function Notice() {\n const elementRef = useRef<HTMLDivElement>(null);\n\n useEffect(() => {\n const element = elementRef.current;\n if (!element) return;\n\n const handle = animate(element, [{ opacity: 0 }, { opacity: 1 }], { duration: 180 });\n return () => handle.dispose();\n }, []);\n\n return <div ref={elementRef}>Saved</div>;\n}\n```\n\n## Working with Other Vielzeug Libraries\n\nUse Dnd's `onBeforeReorder` hook to capture item layout before a sortable commits its DOM update. Animate the captured transition after `onReorder`; see the [optimistic reorder recipe](/dnd/examples/optimistic-reorder-with-revert.md).\n\n## Best Practices\n\n- Start animations only after their elements mount in the browser.\n- Set animation timing explicitly when native zero-duration behavior is not intended.\n- Dispose each handle or group with its UI owner.\n- Use native `Animation` objects for playback control.\n- Respect the default `'system'` motion setting unless movement is essential.\n- Use a parent `AbortSignal` for cancellable application flow.\n- Keep `delay` numeric when combining it with non-zero `stagger`.\n- Capture layout before mutation and animate each transition exactly once.\n",
7
7
  "examples": "---\ntitle: 'Necromancer: Examples'\ndescription: Practical animation and FLIP layout recipes for @vielzeug/necromancer.\n---\n\n## Examples\n\n- [Animate on Mount](./examples/animate-on-mount.md)\n- [Stagger a List](./examples/stagger-a-list.md)\n- [Animate a Reorder](./examples/animate-a-reorder.md)\n\n"
8
8
  },
9
9
  "examples": [
@@ -14,12 +14,12 @@
14
14
  },
15
15
  {
16
16
  "id": "lifecycle",
17
- "code": "import { animate } from '@vielzeug/necromancer'\n\nconst panel = document.createElement('section')\npanel.style.cssText = 'display: grid; gap: 12px; max-width: 320px; padding: 20px; border: 1px solid #cbd5e1; border-radius: 12px; background: #fff;'\n\nconst card = document.createElement('div')\ncard.textContent = 'Lifecycle-owned animation'\ncard.style.cssText = 'padding: 18px; border-radius: 8px; color: #fff; background: #2563eb; font: 600 16px system-ui;'\n\nconst replay = document.createElement('button')\nreplay.textContent = 'Replay animation'\n\nconst status = document.createElement('output')\nstatus.textContent = 'Ready'\n\npanel.append(card, replay, status)\ndocument.body.appendChild(panel)\n\nlet current\n\nfunction run() {\n current?.dispose('replayed')\n current = animate(\n card,\n [\n { opacity: 0, transform: 'translateY(14px) scale(.96)' },\n { opacity: 1, transform: 'translateY(0) scale(1)' },\n ],\n { duration: 280, easing: 'ease-out', fill: 'both' },\n )\n status.textContent = 'Animating…'\n current.result.then((result) => {\n status.textContent = result.status === 'finished' ? 'Finished: handle remains disposable' : result.status === 'reduced' ? 'Finished with reduced timing' : 'Cancelled'\n })\n}\n\nreplay.addEventListener('click', run)\n",
17
+ "code": "import { animate } from '@vielzeug/necromancer'\n\nconst panel = document.createElement('section')\npanel.style.cssText = 'display: grid; gap: 12px; max-width: 320px; padding: 20px; border: 1px solid #cbd5e1; border-radius: 12px; background: #fff;'\n\nconst card = document.createElement('div')\ncard.textContent = 'Lifecycle-owned animation'\ncard.style.cssText = 'padding: 18px; border-radius: 8px; color: #fff; background: #2563eb; font: 600 16px system-ui;'\n\nconst replay = document.createElement('button')\nreplay.textContent = 'Replay animation'\n\nconst status = document.createElement('output')\nstatus.textContent = 'Ready'\n\npanel.append(card, replay, status)\ndocument.body.appendChild(panel)\n\nlet current\n\nfunction run() {\n current?.dispose('replayed')\n current = animate(\n card,\n [\n { opacity: 0, transform: 'translateY(14px) scale(.96)' },\n { opacity: 1, transform: 'translateY(0) scale(1)' },\n ],\n { duration: 280, easing: 'ease-out', fill: 'both' },\n )\n status.textContent = 'Animating…'\n current.result.then((result) => {\n status.textContent = result.status === 'finished' ? 'Finished: handle remains disposable' : 'Cancelled'\n })\n}\n\nreplay.addEventListener('click', run)\n",
18
18
  "name": "animate() - Lifecycle Handle"
19
19
  },
20
20
  {
21
21
  "id": "reduced-motion",
22
- "code": "import { animate } from '@vielzeug/necromancer'\n\nconst panel = document.createElement('section')\npanel.style.cssText = 'display: grid; gap: 12px; max-width: 320px; padding: 20px; border: 1px solid #cbd5e1; border-radius: 12px; background: #fff;'\n\nconst card = document.createElement('div')\ncard.textContent = 'Motion preference'\ncard.style.cssText = 'padding: 18px; border-radius: 8px; color: #fff; background: #7c3aed; font: 600 16px system-ui;'\n\nconst systemButton = document.createElement('button')\nsystemButton.textContent = 'Animate with system preference'\n\nconst skipButton = document.createElement('button')\nskipButton.textContent = 'Use reduced motion'\n\nconst status = document.createElement('output')\nstatus.textContent = 'Choose a mode'\n\npanel.append(card, systemButton, skipButton, status)\ndocument.body.appendChild(panel)\n\nfunction run(motion) {\n const handle = animate(\n card,\n [\n { opacity: 0, transform: 'translateX(-18px)' },\n { opacity: 1, transform: 'translateX(0)' },\n ],\n { duration: 320, easing: 'ease-out', motion, fill: 'both' },\n )\n status.textContent = 'Running with motion: ' + motion\n handle.result.then((result) => {\n status.textContent = result.status === 'reduced' ? 'Keyframes finished with reduced timing' : 'Finished'\n })\n}\n\nsystemButton.addEventListener('click', () => run('system'))\nskipButton.addEventListener('click', () => run('reduced'))",
22
+ "code": "import { animate, shouldReduceMotion } from '@vielzeug/necromancer'\n\nconst panel = document.createElement('section')\npanel.style.cssText = 'display: grid; gap: 12px; max-width: 320px; padding: 20px; border: 1px solid #cbd5e1; border-radius: 12px; background: #fff;'\n\nconst card = document.createElement('div')\ncard.textContent = 'Motion preference'\ncard.style.cssText = 'padding: 18px; border-radius: 8px; color: #fff; background: #7c3aed; font: 600 16px system-ui;'\n\nconst systemButton = document.createElement('button')\nsystemButton.textContent = 'Animate with system preference'\n\nconst skipButton = document.createElement('button')\nskipButton.textContent = 'Use reduced motion'\n\nconst status = document.createElement('output')\nstatus.textContent = 'Choose a mode'\n\npanel.append(card, systemButton, skipButton, status)\ndocument.body.appendChild(panel)\n\nfunction run(motion) {\n const handle = animate(\n card,\n [\n { opacity: 0, transform: 'translateX(-18px)' },\n { opacity: 1, transform: 'translateX(0)' },\n ],\n { duration: 320, easing: 'ease-out', motion, fill: 'both' },\n )\n status.textContent = 'Running with motion: ' + motion\n handle.result.then((result) => {\n status.textContent =\n result.status === 'cancelled' ? 'Cancelled' : shouldReduceMotion(motion) ? 'Finished with reduced timing' : 'Finished'\n })\n}\n\nsystemButton.addEventListener('click', () => run('system'))\nskipButton.addEventListener('click', () => run('reduced'))",
23
23
  "name": "motion - Reduced Motion"
24
24
  },
25
25
  {
package/data/refine.json CHANGED
@@ -40,7 +40,7 @@
40
40
  "text": "['full', 'reduced', 'system']"
41
41
  },
42
42
  "default": "['full', 'reduced', 'system']",
43
- "description": "App-level motion mode, mirroring `@vielzeug/necromancer`'s `MotionMode`:\nthe operating-system preference is the default, and an application with its\nown \"reduce motion\" setting can force either direction.\n\nThe mode is one attribute on `<html>` (`data-motion`), which the Reduced\nMotion block in `theme.css` reads: `'reduced'` zeroes every duration and\ntransition token and switches the `--_motion-*` escape hatches to `none`,\n`'full'` opts back in despite an OS-level reduction, and `'system'` (the\ndefault) removes the attribute. Because the tokens drive every CSS\nanimation and transition in the library, one call restyles all motion."
43
+ "description": "App-level motion mode, built on `@vielzeug/necromancer`'s `MotionMode`:\nthe operating-system preference is the default, and an application with its\nown \"reduce motion\" setting can force either direction.\n\nThe mode is one attribute on `<html>` (`data-motion`), which the Reduced\nMotion block in `theme.css` reads: `'reduced'` zeroes every duration and\ntransition token and switches the `--_motion-*` escape hatches to `none`,\n`'full'` opts back in despite an OS-level reduction, and `'system'` (the\ndefault) removes the attribute. Because the tokens drive every CSS\nanimation and transition in the library, one call restyles all motion."
44
44
  },
45
45
  {
46
46
  "kind": "function",
@@ -9079,6 +9079,10 @@
9079
9079
  "name": "viewfinder",
9080
9080
  "description": "Decorative scan frame over the preview"
9081
9081
  },
9082
+ {
9083
+ "name": "spinner",
9084
+ "description": "Busy ring shown over the stage while the camera starts"
9085
+ },
9082
9086
  {
9083
9087
  "name": "state",
9084
9088
  "description": "Idle/error/denied/unsupported/result panel"
package/data/search.json CHANGED
@@ -259,18 +259,18 @@
259
259
  "category": "ai",
260
260
  "description": "local mcp access to vielzeug documentation and package metadata.",
261
261
  "docs": {
262
- "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, 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",
263
- "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| `starthttphost` | loopback streamable http host | async | http remains local only |\n| `registerrefinetools` | opt in refine tool registration | sync | import from `@vielzeug/codex/refine` |\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` | opt in refine component tools (`registerrefinetools`) |\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### `snapshotrefinecatalog`\n\n```ts\nnew snapshotrefinecatalog(snapshot: loadedsnapshot)\n```\n\nextends `snapshotcatalog` with `getcomponent()` and `listcomponents()`.\n\n### `registerrefinetools`\n\n```ts\nregisterrefinetools(server: server, catalog: refinecatalog, debug?: boolean): void;\n```\n\nupgrades a server created by `createmcpserver()` with the combined generic and `refine *` tool set. `refinetools` is exported as a readonly registry.\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. `configureserver` runs once for each lazily created mcp request server and is not called during host startup.\n\n## types\n\n```ts\ninterface snapshotpointer {\n directory: string;\n}\n\ninterface snapshotmanifest {\n catalog: 'catalog.json';\n contentdirectory: 'packages';\n refine: 'refine.json' | null;\n schemaversion: typeof snapshot_schema_version;\n search: 'search.json';\n version: string;\n}\n```\n\ndev snapshots use `snapshotpointer` (via `current.json`); published snapshots are static directories.\n\n```ts\ninterface loadedsnapshot {\n catalog: catalogfile;\n contentdirectory: string;\n manifest: snapshotmanifest;\n refinecomponents: cemdeclaration[];\n search: searchrecord[];\n}\n```\n\n```ts\ninterface catalogfile {\n packages: packagemeta[];\n version: string;\n}\n\ninterface searchrecord {\n category: string;\n description: string;\n docs: partial<record<docpage, string>>;\n examples: array<{ id: string; text: string }>;\n exports: string;\n keywords: string;\n name: string;\n related: string;\n slug: string;\n source: string | null;\n}\n\ninterface packagemeta {\n availabledocpages: docpage[];\n category: string;\n description: string;\n exampleids: string[];\n exports: string[];\n hassource: boolean;\n keywords: string[];\n name: string;\n related: string[];\n slug: string;\n version: string;\n}\n\ninterface packagecontent {\n apisource: string | null;\n docs: partial<record<docpage, string>>;\n examples: example[];\n typesignatures: record<string, string>;\n}\n\ninterface example {\n code: string;\n id: string;\n name: string;\n}\n```\n\n```ts\ninterface catalog {\n getcontent(slug: string): packagecontent;\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): array<pick<example, 'id' | 'name'>>;\n listpackages(): packagemeta[];\n search(query: string): searchhit[];\n}\n\ninterface refinecatalog extends catalog {\n getcomponent(tagname: string): cemdeclaration;\n listcomponents(): cemdeclaration[];\n}\n\ninterface searchhit {\n matchedexamples?: string[];\n matchedin: array<'docs' | 'examples' | 'exports' | 'keywords' | 'metadata' | 'related' | 'source'>;\n matchedpages?: docpage[];\n name: string;\n slug: string;\n}\n```\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 catalog: catalog;\n configureserver?: (server: server) => void;\n debug?: boolean;\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 attributes?: cemattribute[];\n cssparts?: cemcsspart[];\n cssproperties?: cemcssproperty[];\n description?: string;\n events?: cemevent[];\n members?: cemmember[];\n name?: string;\n slots?: cemslot[];\n superclass?: { name: string; package?: string };\n tagname?: string;\n [key: string]: unknown;\n}\n\ninterface cemattribute {\n default?: string;\n description?: string;\n fieldname?: string;\n name: string;\n type?: { text: string };\n}\n\ninterface cemcsspart {\n description?: string;\n name: string;\n}\n\ninterface cemcssproperty {\n default?: string;\n description?: string;\n name: string;\n}\n\ninterface cemevent {\n description?: string;\n name: string;\n type?: { text: string };\n}\n\ninterface cemmember {\n description?: string;\n kind?: 'field' | 'method';\n name: string;\n type?: { text: string };\n}\n\ninterface cemslot {\n description?: string;\n 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",
264
- "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 `configureserver` hooks run once per mcp request server, not at host startup. 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, upgrade the server with the `@vielzeug/codex/refine` subpath:\n\n```ts\nimport { stdioservertransport, createmcpserver, loadsnapshot } from '@vielzeug/codex';\nimport { snapshotrefinecatalog, registerrefinetools } from '@vielzeug/codex/refine';\n\nconst snapshot = loadsnapshot();\nconst catalog = new snapshotrefinecatalog(snapshot);\nconst server = createmcpserver(catalog, { version: snapshot.manifest.version });\nregisterrefinetools(server, catalog);\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 treat `configureserver` as a per request factory hook and avoid process global side effects.\n keep http local. use stdio for normal client integration.\n run `pnpm test:unit` before `pnpm test:integration`.\n",
262
+ "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",
263
+ "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",
264
+ "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",
265
265
  "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"
266
266
  },
267
267
  "examples": [],
268
- "exports": "loadsnapshot snapshotcatalog createmcpserver starthttphost stdioservertransport catalogerror codexerror",
268
+ "exports": "loadsnapshot snapshotcatalog createmcpserver createmcpserverwithrefine starthttphost stdioservertransport catalogerror codexerror",
269
269
  "keywords": "mcp docs ai",
270
270
  "name": "@vielzeug/codex",
271
271
  "related": "refine",
272
272
  "slug": "codex",
273
- "source": "/**\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 packagecontent,\n packagemeta,\n snapshotmanifest,\n} from './types.js';\n"
273
+ "source": "/**\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"
274
274
  },
275
275
  {
276
276
  "category": "finance",
@@ -882,9 +882,9 @@
882
882
  "category": "ui",
883
883
  "description": "lifecycle owned web animations api primitives for native playback, groups, and additive flip transitions.",
884
884
  "docs": {
885
- "index": " \ntitle: 'necromancer: lifecycle owned dom animations'\ndescription: lifecycle owned web animations api primitives for native playback, groups, and additive flip transitions.\npackage: necromancer\ncategory: ui\nkeywords: [animation, web animations api, waapi, flip, stagger, reduced motion]\nrelated: [orbit, ore]\nexports: [animate, animateeach, capturelayout]\nenvironments: [browser]\n \n\n<! markdownlint disable md025 md033 md060 >\n\n<packagehero package=\"necromancer\" />\n\n## why necromancer?\n\nnative web animations api calls do not provide lifecycle ownership, reduced motion policy, grouped playback, or layout transitions. necromancer retains native keyframes and timing semantics while making ownership explicit for a component or dom feature.\n\n```ts\n// before\nconst animation = element.animate(keyframes, { duration: 180 });\nanimation.addeventlistener('cancel', removelisteners);\n\n// after\nconst animation = animate(element, keyframes, { duration: 180 });\nanimation.dispose();\n```\n\n| feature | native waapi | necromancer | motion one |\n| | | | |\n| bundle size | 0 b | <packageinfo package=\"necromancer\" type=\"size\" /> | ~18 kb |\n| root dependencies | <ore icon name=\"check\" size=\"16\"></ore icon> | <ore icon name=\"check\" size=\"16\"></ore icon> | <ore icon name=\"x\" size=\"16\"></ore icon> |\n| lifecycle handle | manual | `dispose()` | library specific controls |\n| reduced motion | manual | `motion: 'system'` default | configuration required |\n| layout transitions | manual flip math | `capturelayout().animate()` | separate api |\n\n<div class=\"decision callout\">\n\n**use necromancer when** you need native browser animations with explicit cancellation, reduced motion behavior, staggered groups, or positional flip transitions.\n\n**consider css transitions when** a static style change needs no playback control, cleanup, or layout measurement.\n\n</div>\n\n## installation\n\n::: code group\n\n```sh [pnpm]\npnpm add @vielzeug/necromancer\n```\n\n```sh [npm]\nnpm install @vielzeug/necromancer\n```\n\n```sh [yarn]\nyarn add @vielzeug/necromancer\n```\n\n:::\n\n## quick start\n\nstart the animation after its dom element mounts and release it when its ui owner is removed.\n\n```ts\nimport { animate } from '@vielzeug/necromancer';\n\nconst notice = document.createelement('p');\nnotice.textcontent = 'saved';\ndocument.body.append(notice);\n\nconst animation = animate(\n notice,\n [{ opacity: 0, transform: 'translatey(8px)' }, { opacity: 1, transform: 'translatey(0)' }],\n { duration: 180, easing: 'ease out' },\n);\n\nawait animation.result;\nanimation.dispose();\n```\n\n## features\n\n<div class=\"features grid\">\n\n `animate()`: native element animation with lifecycle ownership and direct native access\n `animateeach()`: group ownership with stable keyframe factories and `stagger`\n `capturelayout()`: one shot flip transition with additive `translate` (position) and `scale` (size)\n `motion`: `'system'` reduced motion support with explicit reduced outcomes\n native timing: omitted timing fields retain web animations api behavior\n `signal`: abort a handle from its parent lifecycle\n `dispose()`: idempotent cleanup with `[symbol.dispose]()`\n\n</div>\n\n## deliberate scope\n\nnecromancer owns explicit waapi keyframes. it does not generate css keyframes, observe css transitions, watch mutations, simulate springs, interpolate svg paths, or run a javascript tween loop. use css for declarative style changes and choose a dedicated tool when those capabilities are required.\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 [3.0 migration](./migration.md)\n\n</div>\n\n## see also\n\n<div class=\"see also\">\n\n [orbit](/orbit/): position floating ui before animating its appearance.\n [ore](/ore/): own necromancer handles in a custom element's mount and disposal lifecycle.\n</div>\n\n<! markdownlint enable md025 md033 md060 >\n",
886
- "api": " \ntitle: 'necromancer: api reference'\ndescription: api reference for @vielzeug/necromancer animation ownership, groups, reduced motion, and flip transitions.\n \n\n[[toc]]\n\n## api overview\n\n| symbol | purpose | execution mode | common gotcha |\n| | | | |\n| `animate()` | animate one element | sync | omitted timing fields use native waapi behavior |\n| `animateeach()` | animate a unique element group | sync | non zero `stagger` needs numeric `delay` |\n| `capturelayout()` | capture positions and create a one shot flip transition | sync | capture before changing layout |\n| `shouldreducemotion()` | report whether a motion mode reduces visible movement | sync | `'system'` reads `prefers reduced motion`; false without `matchmedia` |\n| `necromancererror` | base package error | sync | use `instanceof necromancererror` to narrow unknown errors |\n\n## package entry point\n\n| import | purpose |\n| | |\n| `@vielzeug/necromancer` | animation functions, types, and errors |\n| `@vielzeug/necromancer/testing` | jsdom test fakes for `element.animate()` and `getboundingclientrect()` |\n\n## animation functions\n\n### `animate()`\n\n```ts\nfunction animate(element: element, keyframes: keyframes, options?: animateoptions): animationhandle;\n```\n\nstarts a lifecycle owned native web animation. native timing options, including an omitted or zero `duration`, are preserved.\n\n| parameter | type | description |\n| | | |\n| `element` | `element` | element animated through its native `animate()` method. |\n| `keyframes` | `keyframes` | native keyframe array or property indexed keyframes. |\n| `options` | `animateoptions` | native timing plus motion and cancellation ownership. |\n\n**returns:** an `animationhandle` with native playback access and explicit disposal.\n\n**example:**\n\n```ts\nimport { animate } from '@vielzeug/necromancer';\n\nconst handle = animate(element, [{ opacity: 0 }, { opacity: 1 }], { duration: 180 });\nhandle.animation.pause();\nconst result = await handle.result;\nhandle.dispose();\n```\n\n \n\n### `animateeach()`\n\n```ts\nfunction animateeach<elementtype extends element>(\n elements: iterable<elementtype>,\n keyframes: keyframes | keyframefactory<elementtype>,\n options?: animateeachoptions,\n): animationgroup;\n```\n\nstarts animations for unique elements in first seen order. necromancer resolves every keyframe factory before starting the first native animation.\n\n| parameter | type | description |\n| | | |\n| `elements` | `iterable<elementtype>` | elements animated once in first seen order. |\n| `keyframes` | `keyframes \\| keyframefactory<elementtype>` | shared keyframes or a subtype preserving factory. |\n| `options` | `animateeachoptions` | animation options plus a stagger interval. |\n\n**returns:** an `animationgroup` containing child handles and ordered results.\n\n**example:**\n\n```ts\nimport { animateeach } from '@vielzeug/necromancer';\n\nconst group = animateeach(document.queryselectorall<htmlelement>('.item'), [{ opacity: 0 }, { opacity: 1 }], {\n duration: 180,\n stagger: 30,\n});\nawait group.results;\n```\n\n### `shouldreducemotion()`\n\n```ts\nfunction shouldreducemotion(mode: motionmode): boolean;\n```\n\nreports whether the requested motion preference reduces visible movement: the same check `animate()` applies internally, exposed so hand rolled effects (scroll behavior, tweens) stay consistent with it. `'full'` is always `false`, `'reduced'` is always `true`, and `'system'` reads the `prefers reduced motion` media query (`false` where `matchmedia` is unavailable).\n\n```ts\nimport { shouldreducemotion } from '@vielzeug/necromancer';\n\nelement.scrollintoview({ behavior: shouldreducemotion('system') ? 'auto' : 'smooth' });\n```\n\n## layout functions\n\n### `capturelayout()`\n\n```ts\nfunction capturelayout<elementtype extends element>(\n elements: iterable<elementtype>,\n options?: layoutcaptureoptions<elementtype>,\n): layouttransition<elementtype>;\n```\n\ncaptures unique elements' positions and sizes and returns a one shot transition. rotation and other transforms are not captured or compensated.\n\n| parameter | type | description |\n| | | |\n| `elements` | `iterable<elementtype>` | elements measured before a layout change. |\n| `options` | `layoutcaptureoptions<elementtype>` | optional stable key mapping for replacement nodes. |\n\n**returns:** a `layouttransition<elementtype>` that measures committed layout once and creates an `animationgroup`.\n\n**example:**\n\n```ts\nimport { capturelayout } from '@vielzeug/necromancer';\n\nconst transition = capturelayout(beforeitems, {\n getkey: (element) => element.getattribute('data id')!,\n});\n\nrenderreordereditems();\n\nconst group = transition.animate({\n duration: 220,\n easing: 'ease out',\n elements: afteritems,\n});\n```\n\ncalling `animate()` twice on the same transition throws `necromancerconfigerror`.\n\n## types\n\n### `motionmode`\n\n```ts\ntype motionmode = 'full' | 'reduced' | 'system';\n```\n\n`'system'` is the default. reduced motion preserves the supplied keyframes while normalizing delay, duration, and end delay to `0`, and iterations to `1`.\n\n### `animationresult`\n\n```ts\ntype animationresult =\n | { readonly status: 'finished' }\n | { readonly status: 'reduced' }\n | { readonly reason?: unknown; readonly status: 'cancelled' };\n```\n\n`cancelled` describes native cancellation and includes its native rejection reason. a reason passed to `dispose()` or an abort signal takes precedence. the independent `disposed` property becomes `true` only when the lifecycle owner is explicitly disposed.\n\n### `animateoptions`\n\n```ts\ntype animateoptions = keyframeanimationoptions & {\n readonly motion?: motionmode;\n readonly signal?: abortsignal;\n};\n```\n\nnative timing fields pass through unchanged unless reduced motion normalizes timing. `signal` disposes the returned handle when aborted.\n\n### `animateeachoptions`\n\n```ts\ntype animateeachoptions = animateoptions & {\n readonly stagger?: number;\n};\n```\n\n`stagger` is a finite, non negative millisecond offset.\n\n### `layoutcaptureoptions`\n\n```ts\ninterface layoutcaptureoptions<elementtype extends element = element> {\n readonly getkey?: (element: elementtype) => string;\n}\n```\n\n`getkey` maps a captured element and its committed replacement to the same stable, non empty string. duplicate or empty keys throw `necromancerconfigerror`.\n\n### `layoutanimationoptions`\n\n```ts\ntype layoutanimationoptions<elementtype extends element = element> = animateeachoptions & {\n readonly elements?: iterable<elementtype>;\n};\n```\n\n`elements` is the collection in its committed layout. omit it to animate the same captured elements. with `getkey`, replacement elements animate from the positions of their captured predecessors. unmatched, removed, and newly entered elements are ignored.\n\n### `keyframes` and `keyframefactory`\n\n```ts\ntype keyframes = readonly keyframe[] | propertyindexedkeyframes;\ntype keyframefactory<elementtype extends element = element> = (\n element: elementtype,\n index: number,\n total: number,\n) => keyframes;\n```\n\naccepts a `readonly` array so a reusable `as const` keyframe list can be passed without a cast.\n\n### `animationhandle`\n\n```ts\ninterface animationhandle {\n readonly animation: animation;\n readonly result: promise<animationresult>;\n readonly disposed: boolean;\n dispose(reason?: unknown): void;\n [symbol.dispose](): void;\n}\n```\n\n| member | description |\n| | |\n| `animation` | owned native animation used for playback control. |\n| `result` | resolves with the first terminal outcome. |\n| `disposed` | reports explicit owner disposal. |\n| `dispose(reason?)` | cancels the animation and releases its abort listener. |\n| `[symbol.dispose]()` | delegates to `dispose()`. |\n\n### `animationgroup`\n\n```ts\ninterface animationgroup {\n readonly handles: readonly animationhandle[];\n readonly results: promise<readonly animationresult[]>;\n readonly disposed: boolean;\n dispose(reason?: unknown): void;\n [symbol.dispose](): void;\n}\n```\n\n| member | description |\n| | |\n| `handles` | child handles in input order. |\n| `results` | resolves with every child outcome in handle order. |\n| `disposed` | reports explicit group disposal. |\n| `dispose(reason?)` | disposes every child. |\n| `[symbol.dispose]()` | delegates to `dispose()`. |\n\n### `layouttransition`\n\n```ts\ninterface layouttransition<elementtype extends element = element> {\n animate(options?: layoutanimationoptions<elementtype>): animationgroup;\n}\n```\n\n| method | description |\n| | |\n| `animate(options?)` | measures committed elements and creates the one allowed layout animation group. |\n\n## errors\n\n| error | trigger |\n| | |\n| `necromancererror` | base class for package errors |\n| `necromancerconfigerror` | invalid stagger, incompatible delay, or reused layout transition |\n| `necromancerunsupportederror` | `element.animate()` is unavailable |\n\n## testing (`@vielzeug/necromancer/testing`)\n\njsdom (and most non browser dom environments) do not implement `element.animate()`. import these from the `/testing` sub path, not the root entry point.\n\n### `animationcall`\n\n```ts\ntype animationcall = {\n readonly animation: fakeanimation;\n readonly keyframes: keyframe[] | propertyindexedkeyframes;\n readonly options?: keyframeanimationoptions;\n};\n```\n\none recorded invocation of `element.prototype.animate` from `installfakeanimations()`.\n\n### `installfakeanimations()`\n\n```ts\nfunction installfakeanimations(): {\n readonly calls: readonly animationcall[];\n restore(): void;\n [symbol.dispose](): void;\n};\n```\n\nreplaces `element.prototype.animate` with a deterministic lifecycle fake for the duration of a test. `calls` records every invocation in order. call `restore()` or use an explicit resource management `using` declaration to put the original implementation back. the fake does not implement native playback controls such as `pause()` or `reverse()`.\n\n```ts\nimport { installfakeanimations } from '@vielzeug/necromancer/testing';\n\nusing animations = installfakeanimations();\nconst handle = animate(element, [{ opacity: 0 }, { opacity: 1 }]);\n\nanimations.calls[0]?.animation.finish();\nawait handle.result; // { status: 'finished' }\n```\n\n### `fakeanimation`\n\n```ts\nclass fakeanimation {\n cancelcallcount: number;\n finishcallcount: number;\n finished: promise<void>;\n cancel(): void;\n finish(): void;\n}\n```\n\na minimal `animation` stand in. `cancel()` rejects `finished` with an `aborterror`; `finish()` resolves it. `cancelcallcount`/`finishcallcount` track how many times each was called, in place of a test runner specific spy.\n\n### `createrect()`\n\n```ts\nfunction createrect(x: number, y: number, width?: number, height?: number): domrect;\n```\n\nbuilds a `domrect` for mocking `element.getboundingclientrect()` in `capturelayout()` tests. `width`/`height` default to `20`.\n",
887
- "usage": " \ntitle: 'necromancer: usage guide'\ndescription: animate dom elements, coordinate groups, and create flip transitions with @vielzeug/necromancer.\n \n\n[[toc]]\n\n## basic usage\n\ncreate an animation after its element mounts, control playback through the native `animation`, and dispose its owner with the ui lifecycle.\n\n```ts\nimport { animate } from '@vielzeug/necromancer';\n\nconst handle = animate(\n element,\n [{ opacity: 0, transform: 'translatey(8px)' }, { opacity: 1, transform: 'translatey(0)' }],\n { duration: 180, easing: 'ease out', fill: 'both' },\n);\n\nhandle.animation.reverse();\nconst result = await handle.result;\nhandle.dispose();\n```\n\n`result` distinguishes natural completion, reduced timing, and cancellation. `disposed` reports only whether the owner was explicitly disposed.\n\nomitted timing fields retain native web animations api behavior. pass an explicit `duration` when the animation should run over time.\n\n## motion preferences\n\nuse `motion` to select how the animation responds to the operating system preference.\n\n```ts\nconst handle = animate(element, [{ opacity: 0 }, { opacity: 1 }], {\n duration: 200,\n motion: 'system',\n});\n\nconst result = await handle.result;\n```\n\n`'system'` is the default and reduces movement when `prefers reduced motion: reduce` matches. `'full'` preserves the requested timing, while `'reduced'` always reduces it.\n\nreduced motion keeps the supplied keyframes but normalizes delay, duration, and end delay to zero and iterations to one. the result is `{ status: 'reduced' }`, and `handle.animation` still represents the requested visual transition.\n\n## parent cancellation\n\npass a parent `abortsignal` to release an animation when its owning work is cancelled.\n\n```ts\nconst controller = new abortcontroller();\nconst handle = animate(element, [{ scale: 0.96 }, { scale: 1 }], {\n duration: 160,\n signal: controller.signal,\n});\n\ncontroller.abort('route changed');\nconst result = await handle.result;\n// { status: 'cancelled', reason: 'route changed' }\n```\n\nan already aborted signal throws its reason before an animation starts.\n\n## staggering a group\n\npass an iterable of elements to `animateeach()`. duplicate elements are animated once in first seen order.\n\n```ts\nimport { animateeach } from '@vielzeug/necromancer';\n\nconst group = animateeach(\n document.queryselectorall('.card'),\n (_card, index) => [\n { opacity: 0, transform: `translatey(${12 + index * 2}px)` },\n { opacity: 1, transform: 'translatey(0)' },\n ],\n { duration: 220, easing: 'ease out', stagger: 45 },\n);\n\nconst results = await group.results;\ngroup.dispose();\n```\n\na group owns child lifecycles only. `results` preserves every child result in handle order; use `group.handles` when native playback control is required.\n\n## serial application flow\n\njavascript control flow is the clearest way to express serial, conditional, or branching animations:\n\n```ts\nfor (const step of steps) {\n const handle = animate(step.element, step.keyframes, {\n ...step.options,\n signal: controller.signal,\n });\n const result = await handle.result;\n\n if (result.status === 'cancelled') break;\n}\n```\n\none parent `abortsignal` cancels the active step without introducing a separate timeline abstraction.\n\n## animating a reorder with flip\n\ncapture positions before changing layout, then animate through the returned one shot transition.\n\n```ts\nimport { capturelayout } from '@vielzeug/necromancer';\n\nconst transition = capturelayout(items);\nlist.prepend(items[2]!);\n\nconst group = transition.animate({ duration: 220, easing: 'ease out' });\nawait group.results;\ngroup.dispose();\n```\n\nthe transition only animates changed, connected elements and can be animated once. it additively composes the individual css `translate` and `scale` properties, preserving authored `transform`, `translate`, and `scale`. a resized element (for example a list item whose content changed) animates from its captured size as well as its captured position.\n\n### replacing rendered elements\n\nwhen a framework replaces list nodes rather than reorders the captured elements, give `capturelayout()` a stable key and pass the committed nodes to `animate()`. capture before updating state, then call `animate()` only after the renderer has committed the new dom.\n\n```ts\nconst transition = capturelayout(beforeitems, {\n getkey: (element) => element.getattribute('data id')!,\n});\n\nrenderreordereditems();\n\ntransition.animate({\n duration: 220,\n easing: 'ease out',\n elements: afteritems,\n});\n```\n\nkeys must be unique, non empty strings in both collections. items with no matching predecessor are not enter animations; animate those explicitly with `animate()` or `animateeach()`.\n\nfor sortable lists, dnd exposes its pre commit layout seam through `onbeforereorder`; see the [dnd optimistic reorder recipe](/dnd/examples/optimistic reorder with revert.md).\n\n## scope\n\nnecromancer creates and owns explicit web animations api work. it does not observe css authored transitions or animations, inject `@keyframes`, watch dom mutations, generate springs, interpolate svg geometry, or provide a javascript tween fallback. keep css as the owner of declarative component styling and use a dedicated charting or tweening tool when the animation needs capabilities beyond waapi keyframes.\n\n## testing\n\njsdom does not implement `element.animate()`, so lifecycle tests need a fake. `@vielzeug/necromancer/testing` has no test runner import and supports explicit resource management.\n\n```ts\nimport { animate } from '@vielzeug/necromancer';\nimport { installfakeanimations } from '@vielzeug/necromancer/testing';\n\nusing animations = installfakeanimations();\nconst handle = animate(element, [{ opacity: 0 }, { opacity: 1 }]);\n\nanimations.calls[0]?.animation.finish();\nawait handle.result; // { status: 'finished' }\n```\n\nuse `restore()` when `using` is unavailable. the lifecycle fake supports `finish()`, `cancel()`, and `finished`; use browser tests for native playback controls. use `createrect()` to mock `element.getboundingclientrect()` when testing code that calls `capturelayout()`.\n\n## framework integration\n\ncreate handles in a client mount lifecycle and dispose them during unmount. the same composition works with reactive effect systems: start the animation in the effect and return `handle.dispose()` as its cleanup.\n\n```tsx\nimport { useeffect, useref } from 'react';\nimport { animate } from '@vielzeug/necromancer';\n\nexport function notice() {\n const elementref = useref<htmldivelement>(null);\n\n useeffect(() => {\n const element = elementref.current;\n if (!element) return;\n\n const handle = animate(element, [{ opacity: 0 }, { opacity: 1 }], { duration: 180 });\n return () => handle.dispose();\n }, []);\n\n return <div ref={elementref}>saved</div>;\n}\n```\n\n## working with other vielzeug libraries\n\nuse dnd's `onbeforereorder` hook to capture item layout before a sortable commits its dom update. animate the captured transition after `onreorder`; see the [optimistic reorder recipe](/dnd/examples/optimistic reorder with revert.md).\n\n## best practices\n\n start animations only after their elements mount in the browser.\n set animation timing explicitly when native zero duration behavior is not intended.\n dispose each handle or group with its ui owner.\n use native `animation` objects for playback control.\n respect the default `'system'` motion setting unless movement is essential.\n use a parent `abortsignal` for cancellable application flow.\n keep `delay` numeric when combining it with non zero `stagger`.\n capture layout before mutation and animate each transition exactly once.\n",
885
+ "index": " \ntitle: 'necromancer: lifecycle owned dom animations'\ndescription: lifecycle owned web animations api primitives for native playback, groups, and additive flip transitions.\npackage: necromancer\ncategory: ui\nkeywords: [animation, web animations api, waapi, flip, stagger, reduced motion]\nrelated: [orbit, ore]\nexports: [animate, animateeach, capturelayout]\nenvironments: [browser]\n \n\n<! markdownlint disable md025 md033 md060 >\n\n<packagehero package=\"necromancer\" />\n\n## why necromancer?\n\nnative web animations api calls do not provide lifecycle ownership, reduced motion policy, grouped playback, or layout transitions. necromancer retains native keyframes and timing semantics while making ownership explicit for a component or dom feature.\n\n```ts\n// before\nconst animation = element.animate(keyframes, { duration: 180 });\nanimation.addeventlistener('cancel', removelisteners);\n\n// after\nconst animation = animate(element, keyframes, { duration: 180 });\nanimation.dispose();\n```\n\n| feature | native waapi | necromancer | motion one |\n| | | | |\n| bundle size | 0 b | <packageinfo package=\"necromancer\" type=\"size\" /> | ~18 kb |\n| root dependencies | <ore icon name=\"check\" size=\"16\"></ore icon> | <ore icon name=\"check\" size=\"16\"></ore icon> | <ore icon name=\"x\" size=\"16\"></ore icon> |\n| lifecycle handle | manual | `dispose()` | library specific controls |\n| reduced motion | manual | `motion: 'system'` default | configuration required |\n| layout transitions | manual flip math | `capturelayout().animate()` | separate api |\n\n<div class=\"decision callout\">\n\n**use necromancer when** you need native browser animations with explicit cancellation, reduced motion behavior, staggered groups, or positional flip transitions.\n\n**consider css transitions when** a static style change needs no playback control, cleanup, or layout measurement.\n\n</div>\n\n## installation\n\n::: code group\n\n```sh [pnpm]\npnpm add @vielzeug/necromancer\n```\n\n```sh [npm]\nnpm install @vielzeug/necromancer\n```\n\n```sh [yarn]\nyarn add @vielzeug/necromancer\n```\n\n:::\n\n## quick start\n\nstart the animation after its dom element mounts and release it when its ui owner is removed.\n\n```ts\nimport { animate } from '@vielzeug/necromancer';\n\nconst notice = document.createelement('p');\nnotice.textcontent = 'saved';\ndocument.body.append(notice);\n\nconst animation = animate(\n notice,\n [{ opacity: 0, transform: 'translatey(8px)' }, { opacity: 1, transform: 'translatey(0)' }],\n { duration: 180, easing: 'ease out' },\n);\n\nawait animation.result;\nanimation.dispose();\n```\n\n## features\n\n<div class=\"features grid\">\n\n `animate()`: native element animation with lifecycle ownership and direct native access\n `animateeach()`: group ownership with stable keyframe factories and `stagger`\n `capturelayout()`: one shot flip transition with additive `translate` (position) and `scale` (size)\n `motion`: `'system'` reduced motion support with the same keyframes on instant timing\n native timing: omitted timing fields retain web animations api behavior\n `signal`: abort a handle from its parent lifecycle\n `dispose()`: idempotent cleanup with `[symbol.dispose]()`\n\n</div>\n\n## deliberate scope\n\nnecromancer owns explicit waapi keyframes. it does not generate css keyframes, observe css transitions, watch mutations, simulate springs, interpolate svg paths, or run a javascript tween loop. use css for declarative style changes and choose a dedicated tool when those capabilities are required.\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 [orbit](/orbit/): position floating ui before animating its appearance.\n [ore](/ore/): own necromancer handles in a custom element's mount and disposal lifecycle.\n</div>\n\n<! markdownlint enable md025 md033 md060 >\n",
886
+ "api": " \ntitle: 'necromancer: api reference'\ndescription: api reference for @vielzeug/necromancer animation ownership, groups, reduced motion, and flip transitions.\n \n\n[[toc]]\n\n## api overview\n\n| symbol | purpose | execution mode | common gotcha |\n| | | | |\n| `animate()` | animate one element | sync | omitted timing fields use native waapi behavior |\n| `animateeach()` | animate a unique element group | sync | non zero `stagger` needs numeric `delay` |\n| `capturelayout()` | capture positions and create a one shot flip transition | sync | capture before changing layout |\n| `shouldreducemotion()` | report whether a motion mode reduces visible movement | sync | `'system'` reads `prefers reduced motion`; false without `matchmedia` |\n| `necromancererror` | base package error | sync | use `instanceof necromancererror` to narrow unknown errors |\n\n## package entry point\n\n| import | purpose |\n| | |\n| `@vielzeug/necromancer` | animation functions, types, and errors |\n| `@vielzeug/necromancer/testing` | jsdom test fakes for `element.animate()` and `getboundingclientrect()` |\n\n## animation functions\n\n### `animate()`\n\n```ts\nfunction animate(element: element, keyframes: keyframes, options?: animateoptions): animationhandle;\n```\n\nstarts a lifecycle owned native web animation. native timing options, including an omitted or zero `duration`, are preserved.\n\n| parameter | type | description |\n| | | |\n| `element` | `element` | element animated through its native `animate()` method. |\n| `keyframes` | `keyframes` | native keyframe array or property indexed keyframes. |\n| `options` | `animateoptions` | native timing plus motion and cancellation ownership. |\n\n**returns:** an `animationhandle` with native playback access and explicit disposal.\n\n**example:**\n\n```ts\nimport { animate } from '@vielzeug/necromancer';\n\nconst handle = animate(element, [{ opacity: 0 }, { opacity: 1 }], { duration: 180 });\nhandle.animation.pause();\nconst result = await handle.result;\nhandle.dispose();\n```\n\n \n\n### `animateeach()`\n\n```ts\nfunction animateeach<elementtype extends element>(\n elements: iterable<elementtype>,\n keyframes: keyframes | keyframefactory<elementtype>,\n options?: animateeachoptions,\n): animationgroup;\n```\n\nstarts animations for unique elements in first seen order. necromancer resolves every keyframe factory before starting the first native animation.\n\n| parameter | type | description |\n| | | |\n| `elements` | `iterable<elementtype>` | elements animated once in first seen order. |\n| `keyframes` | `keyframes \\| keyframefactory<elementtype>` | shared keyframes or a subtype preserving factory. |\n| `options` | `animateeachoptions` | animation options plus a stagger interval. |\n\n**returns:** an `animationgroup` containing child handles and ordered results.\n\n**example:**\n\n```ts\nimport { animateeach } from '@vielzeug/necromancer';\n\nconst group = animateeach(document.queryselectorall<htmlelement>('.item'), [{ opacity: 0 }, { opacity: 1 }], {\n duration: 180,\n stagger: 30,\n});\nawait group.results;\n```\n\n### `shouldreducemotion()`\n\n```ts\nfunction shouldreducemotion(mode: motionmode): boolean;\n```\n\nreports whether the requested motion preference reduces visible movement: the same check `animate()` applies internally, exposed so hand rolled effects (scroll behavior, tweens) stay consistent with it. `'full'` is always `false`, `'reduced'` is always `true`, and `'system'` reads the `prefers reduced motion` media query (`false` where `matchmedia` is unavailable).\n\n```ts\nimport { shouldreducemotion } from '@vielzeug/necromancer';\n\nelement.scrollintoview({ behavior: shouldreducemotion('system') ? 'auto' : 'smooth' });\n```\n\n## layout functions\n\n### `capturelayout()`\n\n```ts\nfunction capturelayout<elementtype extends element>(\n elements: iterable<elementtype>,\n options?: layoutcaptureoptions<elementtype>,\n): layouttransition<elementtype>;\n```\n\ncaptures unique elements' positions and sizes and returns a one shot transition. rotation and other transforms are not captured or compensated.\n\n| parameter | type | description |\n| | | |\n| `elements` | `iterable<elementtype>` | elements measured before a layout change. |\n| `options` | `layoutcaptureoptions<elementtype>` | optional stable key mapping for replacement nodes. |\n\n**returns:** a `layouttransition<elementtype>` that measures committed layout once and creates an `animationgroup`.\n\n**example:**\n\n```ts\nimport { capturelayout } from '@vielzeug/necromancer';\n\nconst transition = capturelayout(beforeitems, {\n getkey: (element) => element.getattribute('data id')!,\n});\n\nrenderreordereditems();\n\nconst group = transition.animate({\n duration: 220,\n easing: 'ease out',\n elements: afteritems,\n});\n```\n\ncalling `animate()` twice on the same transition throws `necromancerconfigerror`.\n\n## types\n\n### `motionmode`\n\n```ts\ntype motionmode = 'full' | 'reduced' | 'system';\n```\n\n`'system'` is the default. reduced motion preserves the supplied keyframes while normalizing delay, duration, and end delay to `0`, and iterations to `1`.\n\n### `animationresult`\n\n```ts\ntype animationresult =\n | { readonly status: 'finished' }\n | { readonly reason?: unknown; readonly status: 'cancelled' };\n```\n\na reduced animation finishes: reduction is decided at call time and `shouldreducemotion()` answers it, so the result reports only what the caller could not know. `cancelled` carries the reason the animation was cancelled: the value passed to `dispose()`, the abort reason of the owning `signal`, or the native rejection reason when something outside necromancer cancelled the animation. a bare `dispose()` carries no reason. the independent `disposed` property becomes `true` only when the lifecycle owner is explicitly disposed.\n\n### `animateoptions`\n\n```ts\ntype animateoptions = keyframeanimationoptions & {\n readonly motion?: motionmode;\n readonly signal?: abortsignal;\n};\n```\n\nnative timing fields pass through unchanged unless reduced motion normalizes timing. `signal` disposes the returned handle when aborted.\n\n### `animateeachoptions`\n\n```ts\ntype animateeachoptions = animateoptions & {\n readonly stagger?: number;\n};\n```\n\n`stagger` is a finite, non negative millisecond offset.\n\n### `layoutcaptureoptions`\n\n```ts\ninterface layoutcaptureoptions<elementtype extends element = element> {\n readonly getkey?: (element: elementtype) => string;\n}\n```\n\n`getkey` maps a captured element and its committed replacement to the same stable, non empty string. duplicate or empty keys throw `necromancerconfigerror`.\n\n### `layoutanimationoptions`\n\n```ts\ntype layoutanimationoptions<elementtype extends element = element> = animateeachoptions & {\n readonly elements?: iterable<elementtype>;\n};\n```\n\n`elements` is the collection in its committed layout. omit it to animate the same captured elements. with `getkey`, replacement elements animate from the positions of their captured predecessors. unmatched, removed, and newly entered elements are ignored.\n\n### `keyframes` and `keyframefactory`\n\n```ts\ntype keyframes = readonly keyframe[] | propertyindexedkeyframes;\ntype keyframefactory<elementtype extends element = element> = (\n element: elementtype,\n index: number,\n total: number,\n) => keyframes;\n```\n\naccepts a `readonly` array so a reusable `as const` keyframe list can be passed without a cast.\n\n### `animationhandle`\n\n```ts\ninterface animationhandle {\n readonly animation: animation;\n readonly result: promise<animationresult>;\n readonly disposed: boolean;\n dispose(reason?: unknown): void;\n [symbol.dispose](): void;\n}\n```\n\n| member | description |\n| | |\n| `animation` | owned native animation used for playback control. |\n| `result` | resolves with the first terminal outcome. |\n| `disposed` | reports explicit owner disposal. |\n| `dispose(reason?)` | cancels the animation and releases its abort listener. |\n| `[symbol.dispose]()` | delegates to `dispose()`. |\n\n### `animationgroup`\n\n```ts\ninterface animationgroup {\n readonly handles: readonly animationhandle[];\n readonly results: promise<readonly animationresult[]>;\n readonly disposed: boolean;\n dispose(reason?: unknown): void;\n [symbol.dispose](): void;\n}\n```\n\n| member | description |\n| | |\n| `handles` | child handles in input order. |\n| `results` | resolves with every child outcome in handle order. |\n| `disposed` | reports explicit group disposal. |\n| `dispose(reason?)` | disposes every child. |\n| `[symbol.dispose]()` | delegates to `dispose()`. |\n\n### `layouttransition`\n\n```ts\ninterface layouttransition<elementtype extends element = element> {\n animate(options?: layoutanimationoptions<elementtype>): animationgroup;\n}\n```\n\n| method | description |\n| | |\n| `animate(options?)` | measures committed elements and creates the one allowed layout animation group. |\n\n## errors\n\n| error | trigger |\n| | |\n| `necromancererror` | base class for package errors |\n| `necromancerconfigerror` | invalid stagger, incompatible delay, or reused layout transition |\n| `necromancerunsupportederror` | `element.animate()` is unavailable |\n\n## testing (`@vielzeug/necromancer/testing`)\n\njsdom (and most non browser dom environments) do not implement `element.animate()`. import these from the `/testing` sub path, not the root entry point.\n\n### `animationcall`\n\n```ts\ntype animationcall = {\n readonly animation: fakeanimation;\n readonly keyframes: keyframe[] | propertyindexedkeyframes;\n readonly options?: keyframeanimationoptions;\n};\n```\n\none recorded invocation of `element.prototype.animate` from `installfakeanimations()`.\n\n### `installfakeanimations()`\n\n```ts\nfunction installfakeanimations(): {\n readonly calls: readonly animationcall[];\n restore(): void;\n [symbol.dispose](): void;\n};\n```\n\nreplaces `element.prototype.animate` with a deterministic lifecycle fake for the duration of a test. `calls` records every invocation in order. call `restore()` or use an explicit resource management `using` declaration to put the original implementation back. the fake does not implement native playback controls such as `pause()` or `reverse()`.\n\n```ts\nimport { installfakeanimations } from '@vielzeug/necromancer/testing';\n\nusing animations = installfakeanimations();\nconst handle = animate(element, [{ opacity: 0 }, { opacity: 1 }]);\n\nanimations.calls[0]?.animation.finish();\nawait handle.result; // { status: 'finished' }\n```\n\n### `fakeanimation`\n\n```ts\nclass fakeanimation {\n cancelcallcount: number;\n finishcallcount: number;\n finished: promise<void>;\n cancel(): void;\n finish(): void;\n}\n```\n\na minimal `animation` stand in. `cancel()` rejects `finished` with an `aborterror`; `finish()` resolves it. `cancelcallcount`/`finishcallcount` track how many times each was called, in place of a test runner specific spy.\n\n### `createrect()`\n\n```ts\nfunction createrect(x: number, y: number, width?: number, height?: number): domrect;\n```\n\nbuilds a `domrect` for mocking `element.getboundingclientrect()` in `capturelayout()` tests. `width`/`height` default to `20`.\n",
887
+ "usage": " \ntitle: 'necromancer: usage guide'\ndescription: animate dom elements, coordinate groups, and create flip transitions with @vielzeug/necromancer.\n \n\n[[toc]]\n\n## basic usage\n\ncreate an animation after its element mounts, control playback through the native `animation`, and dispose its owner with the ui lifecycle.\n\n```ts\nimport { animate } from '@vielzeug/necromancer';\n\nconst handle = animate(\n element,\n [{ opacity: 0, transform: 'translatey(8px)' }, { opacity: 1, transform: 'translatey(0)' }],\n { duration: 180, easing: 'ease out', fill: 'both' },\n);\n\nhandle.animation.reverse();\nconst result = await handle.result;\nhandle.dispose();\n```\n\n`result` distinguishes natural completion from cancellation. `disposed` reports only whether the owner was explicitly disposed.\n\nomitted timing fields retain native web animations api behavior. pass an explicit `duration` when the animation should run over time.\n\n## motion preferences\n\nuse `motion` to select how the animation responds to the operating system preference.\n\n```ts\nconst handle = animate(element, [{ opacity: 0 }, { opacity: 1 }], {\n duration: 200,\n motion: 'system',\n});\n\nconst result = await handle.result;\n```\n\n`'system'` is the default and reduces movement when `prefers reduced motion: reduce` matches. `'full'` preserves the requested timing, while `'reduced'` always reduces it.\n\nreduced motion keeps the supplied keyframes but normalizes delay, duration, and end delay to zero and iterations to one. the animation finishes immediately, so its result is `{ status: 'finished' }`; call `shouldreducemotion()` with the same mode when your own code needs to know whether movement was reduced. `handle.animation` still represents the requested visual transition.\n\n## parent cancellation\n\npass a parent `abortsignal` to release an animation when its owning work is cancelled.\n\n```ts\nconst controller = new abortcontroller();\nconst handle = animate(element, [{ scale: 0.96 }, { scale: 1 }], {\n duration: 160,\n signal: controller.signal,\n});\n\ncontroller.abort('route changed');\nconst result = await handle.result;\n// { status: 'cancelled', reason: 'route changed' }\n```\n\nan already aborted signal throws its reason before an animation starts.\n\n## staggering a group\n\npass an iterable of elements to `animateeach()`. duplicate elements are animated once in first seen order.\n\n```ts\nimport { animateeach } from '@vielzeug/necromancer';\n\nconst group = animateeach(\n document.queryselectorall('.card'),\n (_card, index) => [\n { opacity: 0, transform: `translatey(${12 + index * 2}px)` },\n { opacity: 1, transform: 'translatey(0)' },\n ],\n { duration: 220, easing: 'ease out', stagger: 45 },\n);\n\nconst results = await group.results;\ngroup.dispose();\n```\n\na group owns child lifecycles only. `results` preserves every child result in handle order; use `group.handles` when native playback control is required.\n\n## serial application flow\n\njavascript control flow is the clearest way to express serial, conditional, or branching animations:\n\n```ts\nfor (const step of steps) {\n const handle = animate(step.element, step.keyframes, {\n ...step.options,\n signal: controller.signal,\n });\n const result = await handle.result;\n\n if (result.status === 'cancelled') break;\n}\n```\n\none parent `abortsignal` cancels the active step without introducing a separate timeline abstraction.\n\n## animating a reorder with flip\n\ncapture positions before changing layout, then animate through the returned one shot transition.\n\n```ts\nimport { capturelayout } from '@vielzeug/necromancer';\n\nconst transition = capturelayout(items);\nlist.prepend(items[2]!);\n\nconst group = transition.animate({ duration: 220, easing: 'ease out' });\nawait group.results;\ngroup.dispose();\n```\n\nthe transition only animates changed, connected elements and can be animated once. it additively composes the individual css `translate` and `scale` properties, preserving authored `transform`, `translate`, and `scale`. a resized element (for example a list item whose content changed) animates from its captured size as well as its captured position.\n\n### replacing rendered elements\n\nwhen a framework replaces list nodes rather than reorders the captured elements, give `capturelayout()` a stable key and pass the committed nodes to `animate()`. capture before updating state, then call `animate()` only after the renderer has committed the new dom.\n\n```ts\nconst transition = capturelayout(beforeitems, {\n getkey: (element) => element.getattribute('data id')!,\n});\n\nrenderreordereditems();\n\ntransition.animate({\n duration: 220,\n easing: 'ease out',\n elements: afteritems,\n});\n```\n\nkeys must be unique, non empty strings in both collections. items with no matching predecessor are not enter animations; animate those explicitly with `animate()` or `animateeach()`.\n\nfor sortable lists, dnd exposes its pre commit layout seam through `onbeforereorder`; see the [dnd optimistic reorder recipe](/dnd/examples/optimistic reorder with revert.md).\n\n## scope\n\nnecromancer creates and owns explicit web animations api work. it does not observe css authored transitions or animations, inject `@keyframes`, watch dom mutations, generate springs, interpolate svg geometry, or provide a javascript tween fallback. keep css as the owner of declarative component styling and use a dedicated charting or tweening tool when the animation needs capabilities beyond waapi keyframes.\n\n## testing\n\njsdom does not implement `element.animate()`, so lifecycle tests need a fake. `@vielzeug/necromancer/testing` has no test runner import and supports explicit resource management.\n\n```ts\nimport { animate } from '@vielzeug/necromancer';\nimport { installfakeanimations } from '@vielzeug/necromancer/testing';\n\nusing animations = installfakeanimations();\nconst handle = animate(element, [{ opacity: 0 }, { opacity: 1 }]);\n\nanimations.calls[0]?.animation.finish();\nawait handle.result; // { status: 'finished' }\n```\n\nuse `restore()` when `using` is unavailable. the lifecycle fake supports `finish()`, `cancel()`, and `finished`; use browser tests for native playback controls. use `createrect()` to mock `element.getboundingclientrect()` when testing code that calls `capturelayout()`.\n\n## framework integration\n\ncreate handles in a client mount lifecycle and dispose them during unmount. the same composition works with reactive effect systems: start the animation in the effect and return `handle.dispose()` as its cleanup.\n\n```tsx\nimport { useeffect, useref } from 'react';\nimport { animate } from '@vielzeug/necromancer';\n\nexport function notice() {\n const elementref = useref<htmldivelement>(null);\n\n useeffect(() => {\n const element = elementref.current;\n if (!element) return;\n\n const handle = animate(element, [{ opacity: 0 }, { opacity: 1 }], { duration: 180 });\n return () => handle.dispose();\n }, []);\n\n return <div ref={elementref}>saved</div>;\n}\n```\n\n## working with other vielzeug libraries\n\nuse dnd's `onbeforereorder` hook to capture item layout before a sortable commits its dom update. animate the captured transition after `onreorder`; see the [optimistic reorder recipe](/dnd/examples/optimistic reorder with revert.md).\n\n## best practices\n\n start animations only after their elements mount in the browser.\n set animation timing explicitly when native zero duration behavior is not intended.\n dispose each handle or group with its ui owner.\n use native `animation` objects for playback control.\n respect the default `'system'` motion setting unless movement is essential.\n use a parent `abortsignal` for cancellable application flow.\n keep `delay` numeric when combining it with non zero `stagger`.\n capture layout before mutation and animate each transition exactly once.\n",
888
888
  "examples": " \ntitle: 'necromancer: examples'\ndescription: practical animation and flip layout recipes for @vielzeug/necromancer.\n \n\n## examples\n\n [animate on mount](./examples/animate on mount.md)\n [stagger a list](./examples/stagger a list.md)\n [animate a reorder](./examples/animate a reorder.md)\n\n"
889
889
  },
890
890
  "examples": [
@@ -894,11 +894,11 @@
894
894
  },
895
895
  {
896
896
  "id": "lifecycle",
897
- "text": "animate() lifecycle handle import { animate } from '@vielzeug/necromancer'\n\nconst panel = document.createelement('section')\npanel.style.csstext = 'display: grid; gap: 12px; max width: 320px; padding: 20px; border: 1px solid #cbd5e1; border radius: 12px; background: #fff;'\n\nconst card = document.createelement('div')\ncard.textcontent = 'lifecycle owned animation'\ncard.style.csstext = 'padding: 18px; border radius: 8px; color: #fff; background: #2563eb; font: 600 16px system ui;'\n\nconst replay = document.createelement('button')\nreplay.textcontent = 'replay animation'\n\nconst status = document.createelement('output')\nstatus.textcontent = 'ready'\n\npanel.append(card, replay, status)\ndocument.body.appendchild(panel)\n\nlet current\n\nfunction run() {\n current?.dispose('replayed')\n current = animate(\n card,\n [\n { opacity: 0, transform: 'translatey(14px) scale(.96)' },\n { opacity: 1, transform: 'translatey(0) scale(1)' },\n ],\n { duration: 280, easing: 'ease out', fill: 'both' },\n )\n status.textcontent = 'animating…'\n current.result.then((result) => {\n status.textcontent = result.status === 'finished' ? 'finished: handle remains disposable' : result.status === 'reduced' ? 'finished with reduced timing' : 'cancelled'\n })\n}\n\nreplay.addeventlistener('click', run)\n"
897
+ "text": "animate() lifecycle handle import { animate } from '@vielzeug/necromancer'\n\nconst panel = document.createelement('section')\npanel.style.csstext = 'display: grid; gap: 12px; max width: 320px; padding: 20px; border: 1px solid #cbd5e1; border radius: 12px; background: #fff;'\n\nconst card = document.createelement('div')\ncard.textcontent = 'lifecycle owned animation'\ncard.style.csstext = 'padding: 18px; border radius: 8px; color: #fff; background: #2563eb; font: 600 16px system ui;'\n\nconst replay = document.createelement('button')\nreplay.textcontent = 'replay animation'\n\nconst status = document.createelement('output')\nstatus.textcontent = 'ready'\n\npanel.append(card, replay, status)\ndocument.body.appendchild(panel)\n\nlet current\n\nfunction run() {\n current?.dispose('replayed')\n current = animate(\n card,\n [\n { opacity: 0, transform: 'translatey(14px) scale(.96)' },\n { opacity: 1, transform: 'translatey(0) scale(1)' },\n ],\n { duration: 280, easing: 'ease out', fill: 'both' },\n )\n status.textcontent = 'animating…'\n current.result.then((result) => {\n status.textcontent = result.status === 'finished' ? 'finished: handle remains disposable' : 'cancelled'\n })\n}\n\nreplay.addeventlistener('click', run)\n"
898
898
  },
899
899
  {
900
900
  "id": "reduced-motion",
901
- "text": "motion reduced motion import { animate } from '@vielzeug/necromancer'\n\nconst panel = document.createelement('section')\npanel.style.csstext = 'display: grid; gap: 12px; max width: 320px; padding: 20px; border: 1px solid #cbd5e1; border radius: 12px; background: #fff;'\n\nconst card = document.createelement('div')\ncard.textcontent = 'motion preference'\ncard.style.csstext = 'padding: 18px; border radius: 8px; color: #fff; background: #7c3aed; font: 600 16px system ui;'\n\nconst systembutton = document.createelement('button')\nsystembutton.textcontent = 'animate with system preference'\n\nconst skipbutton = document.createelement('button')\nskipbutton.textcontent = 'use reduced motion'\n\nconst status = document.createelement('output')\nstatus.textcontent = 'choose a mode'\n\npanel.append(card, systembutton, skipbutton, status)\ndocument.body.appendchild(panel)\n\nfunction run(motion) {\n const handle = animate(\n card,\n [\n { opacity: 0, transform: 'translatex( 18px)' },\n { opacity: 1, transform: 'translatex(0)' },\n ],\n { duration: 320, easing: 'ease out', motion, fill: 'both' },\n )\n status.textcontent = 'running with motion: ' + motion\n handle.result.then((result) => {\n status.textcontent = result.status === 'reduced' ? 'keyframes finished with reduced timing' : 'finished'\n })\n}\n\nsystembutton.addeventlistener('click', () => run('system'))\nskipbutton.addeventlistener('click', () => run('reduced'))"
901
+ "text": "motion reduced motion import { animate, shouldreducemotion } from '@vielzeug/necromancer'\n\nconst panel = document.createelement('section')\npanel.style.csstext = 'display: grid; gap: 12px; max width: 320px; padding: 20px; border: 1px solid #cbd5e1; border radius: 12px; background: #fff;'\n\nconst card = document.createelement('div')\ncard.textcontent = 'motion preference'\ncard.style.csstext = 'padding: 18px; border radius: 8px; color: #fff; background: #7c3aed; font: 600 16px system ui;'\n\nconst systembutton = document.createelement('button')\nsystembutton.textcontent = 'animate with system preference'\n\nconst skipbutton = document.createelement('button')\nskipbutton.textcontent = 'use reduced motion'\n\nconst status = document.createelement('output')\nstatus.textcontent = 'choose a mode'\n\npanel.append(card, systembutton, skipbutton, status)\ndocument.body.appendchild(panel)\n\nfunction run(motion) {\n const handle = animate(\n card,\n [\n { opacity: 0, transform: 'translatex( 18px)' },\n { opacity: 1, transform: 'translatex(0)' },\n ],\n { duration: 320, easing: 'ease out', motion, fill: 'both' },\n )\n status.textcontent = 'running with motion: ' + motion\n handle.result.then((result) => {\n status.textcontent =\n result.status === 'cancelled' ? 'cancelled' : shouldreducemotion(motion) ? 'finished with reduced timing' : 'finished'\n })\n}\n\nsystembutton.addeventlistener('click', () => run('system'))\nskipbutton.addeventlistener('click', () => run('reduced'))"
902
902
  },
903
903
  {
904
904
  "id": "stagger",
package/dist/catalog.d.ts CHANGED
@@ -1,41 +1,41 @@
1
1
  import { CodexError } from './errors.js';
2
2
  import { type LoadedSnapshot } from './snapshot.js';
3
- import type { DocPage, Example, PackageContent, PackageMeta } from './types.js';
3
+ import type { DocPage, Example, PackageMeta } from './types.js';
4
4
  export type CatalogErrorCode = 'INVALID_ARG' | 'NOT_FOUND' | 'UNAVAILABLE';
5
5
  export declare class CatalogError extends CodexError {
6
6
  readonly code: CatalogErrorCode;
7
7
  constructor(code: CatalogErrorCode, message: string);
8
8
  }
9
9
  export interface SearchHit {
10
- matchedExamples?: string[];
11
- matchedIn: Array<'docs' | 'examples' | 'exports' | 'keywords' | 'metadata' | 'related' | 'source'>;
12
- matchedPages?: DocPage[];
13
- name: string;
14
- slug: string;
10
+ readonly matchedExamples?: readonly string[];
11
+ readonly matchedIn: readonly ('docs' | 'examples' | 'exports' | 'keywords' | 'metadata' | 'related' | 'source')[];
12
+ readonly matchedPages?: readonly DocPage[];
13
+ readonly name: string;
14
+ readonly slug: string;
15
15
  }
16
16
  export interface Catalog {
17
- getContent(slug: string): PackageContent;
18
17
  getDocs(slug: string, page: DocPage): string;
19
18
  getExample(slug: string, exampleId: string): Example;
20
19
  getPackage(slug: string): PackageMeta;
21
20
  getSource(slug: string): string;
22
21
  getTypeSignature(slug: string, symbol: string): string;
23
- listExamples(slug: string): Array<Pick<Example, 'id' | 'name'>>;
24
- listPackages(): PackageMeta[];
25
- search(query: string): SearchHit[];
22
+ listExamples(slug: string): readonly Pick<Example, 'id' | 'name'>[];
23
+ listPackages(): readonly PackageMeta[];
24
+ search(query: string): readonly SearchHit[];
26
25
  }
27
26
  export declare class SnapshotCatalog implements Catalog {
28
27
  private readonly bySlug;
29
28
  private readonly contentCache;
30
29
  private readonly snapshot;
31
30
  constructor(snapshot: LoadedSnapshot);
31
+ private requirePackage;
32
32
  getPackage(slug: string): PackageMeta;
33
- getContent(slug: string): PackageContent;
34
- listPackages(): PackageMeta[];
33
+ private readContent;
34
+ listPackages(): readonly PackageMeta[];
35
35
  getDocs(slug: string, page: DocPage): string;
36
36
  getSource(slug: string): string;
37
- listExamples(slug: string): Array<Pick<Example, 'id' | 'name'>>;
37
+ listExamples(slug: string): readonly Pick<Example, 'id' | 'name'>[];
38
38
  getExample(slug: string, exampleId: string): Example;
39
39
  getTypeSignature(slug: string, symbol: string): string;
40
- search(query: string): SearchHit[];
40
+ search(query: string): readonly SearchHit[];
41
41
  }
package/dist/catalog.js CHANGED
@@ -12,6 +12,16 @@ export class CatalogError extends CodexError {
12
12
  function matches(haystack, terms) {
13
13
  return terms.every((term) => haystack.includes(term));
14
14
  }
15
+ function copyPackage(pkg) {
16
+ return {
17
+ ...pkg,
18
+ availableDocPages: [...pkg.availableDocPages],
19
+ exampleIds: [...pkg.exampleIds],
20
+ exports: [...pkg.exports],
21
+ keywords: [...pkg.keywords],
22
+ related: [...pkg.related],
23
+ };
24
+ }
15
25
  function searchRecord(record, query) {
16
26
  const terms = query.toLowerCase().replaceAll('-', ' ').split(/\s+/).filter(Boolean);
17
27
  if (terms.length === 0)
@@ -70,14 +80,17 @@ export class SnapshotCatalog {
70
80
  this.snapshot = snapshot;
71
81
  this.bySlug = new Map(snapshot.catalog.packages.map((pkg) => [pkg.slug, pkg]));
72
82
  }
73
- getPackage(slug) {
83
+ requirePackage(slug) {
74
84
  const pkg = this.bySlug.get(slug);
75
85
  if (!pkg)
76
86
  throw new CatalogError('NOT_FOUND', `Package "${slug}" not found. Available slugs: ${[...this.bySlug.keys()].join(', ')}`);
77
87
  return pkg;
78
88
  }
79
- getContent(slug) {
80
- this.getPackage(slug);
89
+ getPackage(slug) {
90
+ return copyPackage(this.requirePackage(slug));
91
+ }
92
+ readContent(slug) {
93
+ this.requirePackage(slug);
81
94
  const cached = this.contentCache.get(slug);
82
95
  if (cached)
83
96
  return cached;
@@ -95,32 +108,32 @@ export class SnapshotCatalog {
95
108
  return content;
96
109
  }
97
110
  listPackages() {
98
- return [...this.bySlug.values()];
111
+ return [...this.bySlug.values()].map(copyPackage);
99
112
  }
100
113
  getDocs(slug, page) {
101
- const content = this.getContent(slug).docs[page];
102
- if (!content)
103
- throw new CatalogError('NOT_FOUND', `No "${page}" page for "${slug}". Available: ${this.getPackage(slug).availableDocPages.join(', ') || 'none'}.`);
114
+ const content = this.readContent(slug).docs[page];
115
+ if (content === undefined)
116
+ throw new CatalogError('NOT_FOUND', `No "${page}" page for "${slug}". Available: ${this.requirePackage(slug).availableDocPages.join(', ') || 'none'}.`);
104
117
  return content;
105
118
  }
106
119
  getSource(slug) {
107
- const source = this.getContent(slug).apiSource;
108
- if (!source)
120
+ const source = this.readContent(slug).apiSource;
121
+ if (source === null)
109
122
  throw new CatalogError('UNAVAILABLE', `Package "${slug}" has no bundled source.`);
110
123
  return source;
111
124
  }
112
125
  listExamples(slug) {
113
- return this.getContent(slug).examples.map(({ id, name }) => ({ id, name }));
126
+ return this.readContent(slug).examples.map(({ id, name }) => ({ id, name }));
114
127
  }
115
128
  getExample(slug, exampleId) {
116
- const example = this.getContent(slug).examples.find((item) => item.id === exampleId);
129
+ const example = this.readContent(slug).examples.find((item) => item.id === exampleId);
117
130
  if (!example)
118
131
  throw new CatalogError('NOT_FOUND', `No example "${exampleId}" for "${slug}".`);
119
- return example;
132
+ return { ...example };
120
133
  }
121
134
  getTypeSignature(slug, symbol) {
122
- const signature = this.getContent(slug).typeSignatures[symbol];
123
- if (!signature)
135
+ const signature = this.readContent(slug).typeSignatures[symbol];
136
+ if (signature === undefined)
124
137
  throw new CatalogError('NOT_FOUND', `No exported symbol "${symbol}" for "${slug}".`);
125
138
  return signature;
126
139
  }
@@ -1 +1 @@
1
- {"version":3,"file":"catalog.js","sourceRoot":"","sources":["../src/catalog.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"catalog.js","sourceRoot":"","sources":["../src/catalog.ts"],"names":[],"mappings":"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"}