@vielzeug/codex 26.10.0 → 26.10.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/data/catalog.json +56 -60
- package/data/llms-full.txt +1473 -1069
- package/data/llms.txt +6 -6
- package/data/manifest.json +1 -1
- package/data/packages/arsenal.json +13 -13
- package/data/packages/assay.json +4 -4
- package/data/packages/clockwork.json +4 -4
- package/data/packages/codex.json +3 -3
- package/data/packages/coins.json +4 -4
- package/data/packages/conduit.json +4 -4
- package/data/packages/courier.json +4 -4
- package/data/packages/dnd.json +10 -10
- package/data/packages/familiar.json +4 -4
- package/data/packages/flux.json +4 -4
- package/data/packages/focus.json +8 -5
- package/data/packages/forge.json +4 -4
- package/data/packages/gesture.json +4 -4
- package/data/packages/herald.json +12 -12
- package/data/packages/illusionist.json +5 -5
- package/data/packages/keymap.json +9 -9
- package/data/packages/ledger.json +4 -4
- package/data/packages/lingua.json +4 -4
- package/data/packages/mesh.json +5 -5
- package/data/packages/necromancer.json +5 -5
- package/data/packages/orbit.json +7 -7
- package/data/packages/ore.json +5 -5
- package/data/packages/postmaster.json +4 -4
- package/data/packages/prism.json +50 -42
- package/data/packages/pulse.json +8 -8
- package/data/packages/refine.json +3 -3
- package/data/packages/ripple.json +4 -4
- package/data/packages/rune.json +9 -9
- package/data/packages/sandbox.json +4 -4
- package/data/packages/scout.json +7 -7
- package/data/packages/scroll.json +10 -10
- package/data/packages/sentinel.json +4 -4
- package/data/packages/sigil.json +6 -6
- package/data/packages/sourcerer.json +4 -4
- package/data/packages/spell.json +9 -9
- package/data/packages/tandem.json +5 -5
- package/data/packages/tavern.json +12 -12
- package/data/packages/tempo.json +4 -4
- package/data/packages/vault.json +13 -13
- package/data/packages/ward.json +9 -9
- package/data/packages/wayfinder.json +17 -17
- package/data/refine.json +259 -87
- package/data/search.json +249 -249
- package/dist/http.js +2 -2
- package/dist/http.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/refine.d.ts +1 -1
- package/dist/refine.js +1 -1
- package/dist/server.d.ts +1 -1
- package/dist/server.js +1 -1
- package/dist/tools/refine.js +1 -1
- package/package.json +2 -2
- package/skills/vielzeug/SKILL.md +57 -57
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"apiSource": "export type { CreateSentinelOptions } from './core.ts';\nexport { createSentinel } from './core.ts';\nexport { createElementSize } from './element-size.ts';\nexport { SentinelError, SentinelUnavailableError } from './errors.ts';\nexport { createFullscreen } from './fullscreen.ts';\nexport type { CreateIntersectionOptions } from './intersection.ts';\nexport { createIntersection } from './intersection.ts';\nexport { createMediaQuery } from './media-query.ts';\nexport { createNetwork } from './network.ts';\nexport type {\n ElementSizeState,\n FullscreenSentinel,\n FullscreenState,\n IntersectionState,\n MediaQueryState,\n NetworkConnectionSnapshot,\n NetworkState,\n Sentinel,\n SentinelEvent,\n SentinelOptions,\n Subscribable,\n Unsubscribe,\n ViewportState,\n WakeLockSentinel,\n WakeLockState,\n WindowSentinelOptions,\n} from './types.ts';\nexport { createViewport } from './viewport.ts';\nexport { createWakeLock } from './wake-lock.ts';\n",
|
|
3
3
|
"docs": {
|
|
4
|
-
"index": "---\ntitle: Sentinel
|
|
5
|
-
"api": "---\ntitle: Sentinel — API Reference\ndescription: Factory signatures, options, state types, lifecycle handles, and errors for Sentinel.\n---\n\n[[toc]]\n\n## API Overview\n\n| Symbol | Purpose | Execution mode | Common gotcha |\n| --- | --- | --- | --- |\n| `createViewport()` | Observe layout viewport dimensions and device pixel ratio | Sync | Requires a browser Window |\n| `createNetwork()` | Observe online status and optional connection information | Sync | `connection` is often `null` |\n| `createMediaQuery()` | Observe one media query | Sync | Throws when `matchMedia` is unavailable |\n| `createElementSize()` | Observe element content-box dimensions | Sync | Value is `null` before the first delivery |\n| `createIntersection()` | Observe element intersection state | Sync | Value is `null` before the first delivery |\n| `createWakeLock()` | Prevent the screen from sleeping | Async | Degrades silently when the Wake Lock API is unavailable |\n| `createFullscreen()` | Track and drive document fullscreen state | Sync | Degrades silently when the Fullscreen API is unavailable |\n| `Sentinel<T>` | Expose an external-store snapshot with explicit browser-resource ownership | Sync | Call `getSnapshot()` inside subscription listeners |\n| `SentinelError` | Base class for package-defined errors | Sync | Catch a subtype when recovery is specific |\n| `SentinelUnavailableError` | Report an unavailable browser API | Sync | Invalid observer inputs retain their native errors |\n\n## Package Entry Point\n\n| Import | Purpose |\n| --- | --- |\n| `@vielzeug/sentinel` | All factories, state types, option types, and error classes |\n\n## Factories\n\n### `createViewport()`\n\n```ts\nfunction createViewport(options?: WindowSentinelOptions): Sentinel<ViewportState>;\n```\n\nReturns a Sentinel initialized from the layout viewport's `innerWidth`, `innerHeight`, and `devicePixelRatio`.\n\n| Parameter | Type | Description |\n| --- | --- | --- |\n| `options.target` | `Window` | Window to observe instead of the global browser window |\n| `options.signal` | `AbortSignal` | External signal that disposes the Sentinel |\n\n**Returns:** `Sentinel<ViewportState>`.\n\n**Example**\n\n```ts\nimport { createViewport } from '@vielzeug/sentinel';\n\nconst viewport = createViewport();\nconsole.log(viewport.getSnapshot().width);\nviewport.dispose();\n```\n\n---\n\n### `createNetwork()`\n\n```ts\nfunction createNetwork(options?: WindowSentinelOptions): Sentinel<NetworkState>;\n```\n\nReturns a Sentinel initialized from `navigator.onLine` and the optional Network Information API.\n\n| Parameter | Type | Description |\n| --- | --- | --- |\n| `options.target` | `Window` | Window whose navigator and events are observed |\n| `options.signal` | `AbortSignal` | External signal that disposes the Sentinel |\n\n**Returns:** `Sentinel<NetworkState>`.\n\n**Example**\n\n```ts\nimport { createNetwork } from '@vielzeug/sentinel';\n\nconst network = createNetwork();\nconsole.log(network.getSnapshot().online);\nnetwork.dispose();\n```\n\n---\n\n### `createMediaQuery()`\n\n```ts\nfunction createMediaQuery(query: string, options?: WindowSentinelOptions): Sentinel<MediaQueryState>;\n```\n\nReturns a Sentinel initialized from `matchMedia(query).matches`.\n\n| Parameter | Type | Description |\n| --- | --- | --- |\n| `query` | `string` | CSS media query to observe |\n| `options.target` | `Window` | Window whose `matchMedia` method is used |\n| `options.signal` | `AbortSignal` | External signal that disposes the Sentinel |\n\n**Returns:** `Sentinel<MediaQueryState>`.\n\n**Example**\n\n```ts\nimport { createMediaQuery } from '@vielzeug/sentinel';\n\nconst darkMode = createMediaQuery('(prefers-color-scheme: dark)');\nconsole.log(darkMode.getSnapshot().matches);\ndarkMode.dispose();\n```\n\n---\n\n### `createElementSize()`\n\n```ts\nfunction createElementSize(element: Element, options?: SentinelOptions): Sentinel<ElementSizeState | null>;\n```\n\nReturns a Sentinel containing the latest `ResizeObserverEntry.contentRect` dimensions.\n\n| Parameter | Type | Description |\n| --- | --- | --- |\n| `element` | `Element` | Element to observe |\n| `options.signal` | `AbortSignal` | External signal that disposes the Sentinel |\n\n**Returns:** `Sentinel<ElementSizeState | null>`. The initial value is `null`.\n\n**Example**\n\n```ts\nimport { createElementSize } from '@vielzeug/sentinel';\n\nconst size = createElementSize(document.body);\nconst unsubscribe = size.subscribe(() => {\n console.log(size.getSnapshot()?.width);\n});\n\nunsubscribe();\nsize.dispose();\n```\n\n---\n\n### `createIntersection()`\n\n```ts\nfunction createIntersection(\n element: Element,\n options?: CreateIntersectionOptions,\n): Sentinel<IntersectionState | null>;\n```\n\nReturns a Sentinel containing normalized fields from the latest IntersectionObserver entry.\n\n| Parameter | Type | Description |\n| --- | --- | --- |\n| `element` | `Element` | Element to observe |\n| `options.root` | `Element \\| Document \\| null` | Intersection root |\n| `options.rootMargin` | `string` | Margin applied to the root |\n| `options.scrollMargin` | `string` | Margin applied to nested scroll containers |\n| `options.threshold` | `number \\| number[]` | Intersection ratio threshold or thresholds |\n| `options.signal` | `AbortSignal` | External signal that disposes the Sentinel |\n\n**Returns:** `Sentinel<IntersectionState | null>`. The initial value is `null`.\n\n**Example**\n\n```ts\nimport { createIntersection } from '@vielzeug/sentinel';\n\nconst intersection = createIntersection(document.body, { threshold: 0.5 });\nconst unsubscribe = intersection.subscribe(() => {\n console.log(intersection.getSnapshot()?.isIntersecting);\n});\n\nunsubscribe();\nintersection.dispose();\n```\n\n---\n\n### `createWakeLock()`\n\n```ts\nfunction createWakeLock(options?: SentinelOptions & { readonly target?: Window }): WakeLockSentinel;\n```\n\nReturns a Sentinel-backed screen wake lock. `request()` acquires the lock; `release()` frees it. When the tab is hidden the browser auto-releases the lock; the Sentinel re-acquires it automatically on `visibilitychange` if the lock was active. Degrades silently when the Screen Wake Lock API is unavailable.\n\n| Parameter | Type | Description |\n| --- | --- | --- |\n| `options.target` | `Window` | Window whose document and navigator are used |\n| `options.signal` | `AbortSignal` | External signal that disposes the Sentinel |\n\n**Returns:** `WakeLockSentinel`.\n\n**Example**\n\n```ts\nimport { createWakeLock } from '@vielzeug/sentinel';\n\nconst wakeLock = createWakeLock();\nwakeLock.request();\n\nconst unsubscribe = wakeLock.subscribe(() => {\n console.log('Wake lock active:', wakeLock.getSnapshot().active);\n});\n\n// Later\nwakeLock.release();\nunsubscribe();\nwakeLock.dispose();\n```\n\n---\n\n### `createFullscreen()`\n\n```ts\nfunction createFullscreen(options?: SentinelOptions & { readonly target?: Window }): FullscreenSentinel;\n```\n\nReturns a Sentinel-backed fullscreen view. `request()` enters fullscreen for an element (default: the document element), `exit()` leaves it, and `toggle()` flips it. All degrade silently when the Fullscreen API is missing or the browser refuses (user-activation requirements). Disposing the sentinel exits fullscreen and stops tracking `fullscreenchange`.\n\n| Parameter | Type | Description |\n| --- | --- | --- |\n| `options.target` | `Window` | Window whose document is tracked |\n| `options.signal` | `AbortSignal` | External signal that disposes the Sentinel |\n\n**Returns:** `FullscreenSentinel`.\n\n**Example**\n\n```ts\nimport { createFullscreen } from '@vielzeug/sentinel';\n\nconst fullscreen = createFullscreen();\nfullscreen.request();\n\nconst unsubscribe = fullscreen.subscribe(() => {\n console.log('Fullscreen active:', fullscreen.getSnapshot().active);\n});\n\n// Later\nfullscreen.toggle();\nunsubscribe();\nfullscreen.dispose();\n```\n\n## Types\n\n### `Sentinel<T>`\n\n```ts\ninterface Sentinel<T> extends Subscribable<T>, Disposable {\n tap(handler: (event: SentinelEvent) => void, options?: { signal?: AbortSignal }): () => void;\n}\n\ninterface Subscribable<T> {\n getSnapshot(): T;\n subscribe(listener: () => void): () => void;\n}\n```\n\n`Sentinel<T>` follows the standard external-store snapshot/subscription contract and owns the underlying browser resource. `getSnapshot()`, `subscribe()`, and `dispose()` are stable callback-safe functions. `dispose()` stops observation, clears listeners, and aborts `disposalSignal`.\n\n| Member | Type | Description |\n| --- | --- | --- |\n| `getSnapshot()` | `() => T` | Return the current immutable snapshot |\n| `subscribe(listener)` | `(listener: () => void) => () => void` | Subscribe to snapshot changes and return an unsubscribe function |\n| `disposed` | `boolean` | Whether observation has ended |\n| `disposalSignal` | `AbortSignal` | Aborts when observation ends |\n| `dispose()` | `() => void` | Stop observation and release owned browser resources |\n| `tap(handler, options?)` | `(handler: (event: SentinelEvent) => void, options?: { signal?: AbortSignal }) => () => void` | Observe `{ type: 'error' }` when a listener throws and `{ type: 'dispose' }` on disposal; handler errors are swallowed |\n| `[Symbol.dispose]()` | `() => void` | Dispose through the explicit resource-management protocol |\n\n---\n\n### `SentinelOptions`\n\n```ts\ninterface SentinelOptions {\n readonly signal?: AbortSignal;\n}\n```\n\n`options.signal` disposes the Sentinel when it aborts. Passing an already-aborted signal throws `SentinelError` instead of constructing a dead handle.\n\n---\n\n### `createSentinel()`\n\n```ts\nfunction createSentinel<T>(\n options: CreateSentinelOptions<T>,\n setup: (update: (value: T) => void) => () => void,\n): Sentinel<T>\n```\n\nThe generic primitive every built-in sensor is built on. `setup` starts observing and returns its cleanup; it calls `update(value)` whenever the underlying source changes. Use it for platform sensors the built-ins do not cover (for example `prefers-reduced-motion`).\n\n---\n\n### `WindowSentinelOptions`\n\n```ts\ninterface WindowSentinelOptions extends SentinelOptions {\n readonly target?: Window;\n}\n```\n\n---\n\n### `CreateIntersectionOptions`\n\n```ts\ninterface CreateIntersectionOptions extends SentinelOptions {\n readonly root?: Element | Document | null;\n readonly rootMargin?: string;\n readonly scrollMargin?: string;\n readonly threshold?: number | number[];\n}\n```\n\n---\n\n### `ViewportState`\n\n```ts\ninterface ViewportState {\n readonly dpr: number;\n readonly height: number;\n readonly width: number;\n}\n```\n\n---\n\n### `NetworkConnectionSnapshot`\n\n```ts\ninterface NetworkConnectionSnapshot {\n readonly downlink?: number;\n readonly effectiveType?: 'slow-2g' | '2g' | '3g' | '4g';\n readonly rtt?: number;\n readonly saveData?: boolean;\n}\n```\n\n---\n\n### `NetworkState`\n\n```ts\ninterface NetworkState {\n readonly connection: NetworkConnectionSnapshot | null;\n readonly online: boolean;\n}\n```\n\n---\n\n### `MediaQueryState`\n\n```ts\ninterface MediaQueryState {\n readonly matches: boolean;\n}\n```\n\n---\n\n### `ElementSizeState`\n\n```ts\ninterface ElementSizeState {\n readonly height: number;\n readonly width: number;\n}\n```\n\n---\n\n### `IntersectionState`\n\n```ts\ninterface IntersectionState {\n readonly intersectionRatio: number;\n readonly isIntersecting: boolean;\n}\n```\n\n---\n\n### `WakeLockState`\n\n```ts\ninterface WakeLockState {\n readonly active: boolean;\n readonly supported: boolean;\n}\n```\n\n| Field | Type | Description |\n| --- | --- | --- |\n| `active` | `boolean` | Whether a wake lock is currently held |\n| `supported` | `boolean` | Whether the Screen Wake Lock API is available in this environment |\n\n---\n\n### `WakeLockSentinel`\n\n```ts\ninterface WakeLockSentinel extends Sentinel<WakeLockState> {\n request(): void;\n release(): void;\n}\n```\n\nA `Sentinel<WakeLockState>` with two additional methods:\n\n| Member | Type | Description |\n| --- | --- | --- |\n| `request()` | `() => void` | Acquire the screen wake lock. No-op if already active or unsupported |\n| `release()` | `() => void` | Release the wake lock. No-op if not active |\n\n---\n\n### `FullscreenState`\n\n```ts\ninterface FullscreenState {\n readonly active: boolean;\n readonly supported: boolean;\n}\n```\n\n| Property | Type | Description |\n| --- | --- | --- |\n| `active` | `boolean` | Whether an element is currently shown fullscreen |\n| `supported` | `boolean` | Whether the Fullscreen API is available in this environment |\n\n---\n\n### `FullscreenSentinel`\n\n```ts\ninterface FullscreenSentinel extends Sentinel<FullscreenState> {\n request(element?: Element): void;\n exit(): void;\n toggle(element?: Element): void;\n}\n```\n\nA `Sentinel<FullscreenState>` with three additional methods:\n\n| Member | Type | Description |\n| --- | --- | --- |\n| `request(element?)` | `() => void` | Enter fullscreen for `element` (default: the document element). No-op when already active or unsupported |\n| `exit()` | `() => void` | Leave fullscreen. No-op when not active |\n| `toggle(element?)` | `() => void` | Flip fullscreen for `element` (default: the document element) |\n\n## Errors\n\n### `SentinelError`\n\n```ts\nclass SentinelError extends Error {\n constructor(message: string, options?: ErrorOptions);\n}\n```\n\nBase class for package-defined errors.\n\n---\n\n### `SentinelUnavailableError`\n\n```ts\nclass SentinelUnavailableError extends SentinelError {}\n```\n\nThrown when a required browser API or Window is unavailable:\n\n- `createViewport()` and `createNetwork()` when no browser Window is available.\n- `createMediaQuery()` when `matchMedia` is unavailable.\n- `createElementSize()` when the element has no Window or `ResizeObserver` is unavailable.\n- `createIntersection()` when the element has no Window or `IntersectionObserver` is unavailable.\n- `createWakeLock()` when no browser Window is available.\n- `createFullscreen()` when no browser Window is available.\n\nNative setup errors remain unchanged, including invalid observer options or targets.\n",
|
|
6
|
-
"usage": "---\ntitle: Sentinel — Usage Guide\ndescription: Observe browser and DOM state through subscribable snapshots with explicit lifecycles.\n---\n\n[[toc]]\n\n## Basic Usage\n\nCreate a Sentinel, read its current state, subscribe to invalidations, and release both resources when the owner ends.\n\n```ts\nimport { createViewport } from '@vielzeug/sentinel';\n\nfunction observeViewport(): () => void {\n const viewport = createViewport();\n\n const render = () => {\n const { dpr, height, width } = viewport.getSnapshot();\n console.log(`${width}×${height} at ${dpr}dpr`);\n };\n\n render();\n const unsubscribe = viewport.subscribe(render);\n\n return () => {\n unsubscribe();\n viewport.dispose();\n };\n}\n\nconst stopObserving = observeViewport();\n// Call stopObserving() when the owning view unmounts.\n```\n\n`subscribe()` notifies you that the snapshot changed; call `getSnapshot()` inside the listener to read it. Disposing a Sentinel stops its browser observer, removes its listeners, and aborts `disposalSignal`.\n\n## Observe Window State\n\nUse `createViewport()` for viewport dimensions and device pixel ratio.\n\n```ts\nimport { createViewport } from '@vielzeug/sentinel';\n\nconst viewport = createViewport();\nconsole.log(viewport.getSnapshot().width);\nconsole.log(viewport.getSnapshot().height);\nconsole.log(viewport.getSnapshot().dpr);\n```\n\nUse `createNetwork()` for online status and the optional Network Information API snapshot.\n\n```ts\nimport { createNetwork } from '@vielzeug/sentinel';\n\nconst network = createNetwork();\nconsole.log(network.getSnapshot().online);\nconsole.log(network.getSnapshot().connection);\n```\n\n`connection` is `null` when `navigator.connection` is unavailable.\n\n## Observe Media Queries\n\nUse `createMediaQuery()` to react to a browser media query.\n\n```ts\nimport { createMediaQuery, SentinelUnavailableError } from '@vielzeug/sentinel';\n\nfunction observeReducedMotion(): () => void {\n try {\n const reducedMotion = createMediaQuery('(prefers-reduced-motion: reduce)');\n\n const applyPreference = () => {\n document.documentElement.classList.toggle('reduce-motion', reducedMotion.getSnapshot().matches);\n };\n\n applyPreference();\n const unsubscribe = reducedMotion.subscribe(applyPreference);\n\n return () => {\n unsubscribe();\n reducedMotion.dispose();\n };\n } catch (error) {\n if (!(error instanceof SentinelUnavailableError)) throw error;\n return () => {};\n }\n}\n\nconst stopObserving = observeReducedMotion();\n// Call stopObserving() when the owning view unmounts.\n```\n\n`createMediaQuery()` throws `SentinelUnavailableError` when `matchMedia` is unavailable.\n\n## Observe Elements\n\n### Element Size\n\nUse `createElementSize()` after the target element exists.\n\n```ts\nimport { createElementSize } from '@vielzeug/sentinel';\n\nconst panel = document.querySelector<HTMLElement>('[data-panel]');\nif (!panel) throw new Error('Panel not found');\n\nconst size = createElementSize(panel);\nconst unsubscribe = size.subscribe(() => {\n const current = size.getSnapshot();\n if (current) panel.dataset.width = String(current.width);\n});\n```\n\nThe initial state is `null` until `ResizeObserver` reports its first measurement.\n\n### Intersection\n\nUse `createIntersection()` to observe visibility relative to the viewport or a custom root.\n\n```ts\nimport { createIntersection } from '@vielzeug/sentinel';\n\nconst target = document.querySelector<HTMLElement>('[data-lazy-section]');\nif (!target) throw new Error('Section not found');\n\nconst intersection = createIntersection(target, {\n rootMargin: '100px',\n threshold: [0, 0.5, 1],\n});\n\nconst unsubscribe = intersection.subscribe(() => {\n target.hidden = !intersection.getSnapshot()?.isIntersecting;\n});\n```\n\nThe initial state is `null` until `IntersectionObserver` reports its first entry.\n\n## Control Ownership\n\nCall `dispose()` to stop observation. Disposal is idempotent.\n\n```ts\nconst viewport = createViewport();\n\nviewport.dispose();\nviewport.dispose();\n```\n\nPass an `AbortSignal` when several Sentinels share one lifetime.\n\n```ts\nconst controller = new AbortController();\nconst viewport = createViewport({ signal: controller.signal });\nconst network = createNetwork({ signal: controller.signal });\n\ncontroller.abort();\n```\n\nSentinel follows the standard external-store shape, so reactive libraries can bridge it without a Sentinel-specific adapter.\n\n## Prevent Screen Sleep\n\nUse `createWakeLock()` to keep the screen awake during active use — for example, during a game, presentation, or long-running task.\n\n```ts\nimport { createWakeLock } from '@vielzeug/sentinel';\n\nconst wakeLock = createWakeLock();\nwakeLock.request();\n\nconst unsubscribe = wakeLock.subscribe(() => {\n console.log('Wake lock active:', wakeLock.getSnapshot().active);\n});\n\n// Later — release the lock and dispose\nwakeLock.release();\nunsubscribe();\nwakeLock.dispose();\n```\n\nThe lock auto-releases when the tab is hidden (the browser does this). The Sentinel re-acquires it automatically when the tab becomes visible again if `request()` was called and `release()` was not.\n\nCheck `getSnapshot().supported` before acquiring if you need to show different UI when the Wake Lock API is unavailable.\n\n```ts\nconst wakeLock = createWakeLock();\n\nif (wakeLock.getSnapshot().supported) {\n wakeLock.request();\n}\n```\n\n## Track Fullscreen State\n\nUse `createFullscreen()` to observe and drive document fullscreen state — for example, immersing a board or map view:\n\n```ts\nimport { createFullscreen } from '@vielzeug/sentinel';\n\nconst fullscreen = createFullscreen();\nconst unsubscribe = fullscreen.subscribe(() => {\n fullscreenButton.pressed = fullscreen.getSnapshot().active;\n});\n\nfullscreenButton.addEventListener('click', () => fullscreen.toggle());\n```\n\n`request()`, `exit()`, and `toggle()` degrade silently when the Fullscreen API is unavailable or the browser refuses (user activation is required in some browsers). Disposing the sentinel exits fullscreen, so leaving the owning view releases it.\n\n## Handle Unavailable APIs\n\n`createMediaQuery()`, `createElementSize()`, and `createIntersection()` report unavailable platform APIs with `SentinelUnavailableError`.\n\n```ts\nimport { createElementSize, SentinelUnavailableError } from '@vielzeug/sentinel';\n\ntry {\n const size = createElementSize(document.body);\n size.dispose();\n} catch (error) {\n if (error instanceof SentinelUnavailableError) {\n console.warn(error.message);\n } else {\n throw error;\n }\n}\n```\n\nInvoke all factories only in a browser client lifecycle. Package imports are safe during SSR, but factories require browser or DOM APIs.\n\n## Framework Integration\n\nSentinel's callback-safe external-store methods integrate directly with framework subscription APIs. Create browser-bound Sentinels in a client owner and dispose them when that owner ends.\n\n::: code-group\n\n```tsx [React]\nimport { type Sentinel, type ViewportState } from '@vielzeug/sentinel';\nimport { useSyncExternalStore } from 'react';\n\ntype ViewportSizeProps = {\n readonly viewport: Sentinel<ViewportState>;\n};\n\nexport function ViewportSize({ viewport }: ViewportSizeProps) {\n const viewportState = useSyncExternalStore(viewport.subscribe, viewport.getSnapshot);\n\n return <output>{`${viewportState.width}×${viewportState.height}`}</output>;\n}\n```\n\n```vue [Vue 3]\n<script setup lang=\"ts\">\nimport { createViewport, type Sentinel, type ViewportState } from '@vielzeug/sentinel';\nimport { onMounted, onUnmounted, ref } from 'vue';\n\nconst viewportState = ref<ViewportState | null>(null);\nlet viewport: Sentinel<ViewportState> | undefined;\nlet unsubscribe: (() => void) | undefined;\n\nonMounted(() => {\n viewport = createViewport();\n const update = () => {\n viewportState.value = viewport?.getSnapshot() ?? null;\n };\n\n update();\n unsubscribe = viewport.subscribe(update);\n});\n\nonUnmounted(() => {\n unsubscribe?.();\n viewport?.dispose();\n});\n</script>\n\n<template>\n <output>\n {{ viewportState ? `${viewportState.width}×${viewportState.height}` : 'Measuring…' }}\n </output>\n</template>\n```\n\n```svelte [Svelte]\n<script lang=\"ts\">\n import { createViewport, type ViewportState } from '@vielzeug/sentinel';\n import { onMount } from 'svelte';\n\n let viewportState: ViewportState | null = null;\n\n onMount(() => {\n const viewport = createViewport();\n const update = () => {\n viewportState = viewport.getSnapshot();\n };\n\n update();\n const unsubscribe = viewport.subscribe(update);\n\n return () => {\n unsubscribe();\n viewport.dispose();\n };\n });\n</script>\n\n<output>\n {viewportState ? `${viewportState.width}×${viewportState.height}` : 'Measuring…'}\n</output>\n```\n\n:::\n\n## Working with Other Vielzeug Libraries\n\n### Sentinel + Ripple\n\nBridge Sentinel stores with `fromSubscribable()` before deriving reactive values. Dispose the watcher separately; disposing each Sentinel also disposes its bridge through `disposalSignal`.\n\n```ts\nimport { computed, fromSubscribable, watch } from '@vielzeug/ripple';\nimport { createMediaQuery, createViewport } from '@vielzeug/sentinel';\n\nconst viewport = createViewport();\nconst mobileQuery = createMediaQuery('(max-width: 768px)');\nconst viewportState = fromSubscribable(viewport, { signal: viewport.disposalSignal });\nconst mobileState = fromSubscribable(mobileQuery, { signal: mobileQuery.disposalSignal });\nconst compact = computed(() => mobileState.value.matches || viewportState.value.width < 400);\nconst compactWatcher = watch(compact, (value) => console.log('Compact layout:', value), { immediate: true });\n\ncompactWatcher.dispose();\nmobileQuery.dispose();\nviewport.dispose();\n```\n\n### Sentinel + Ore\n\nCreate DOM-dependent Sentinels in `onMounted()` and register both subscription and Sentinel cleanup with the component.\n\n```ts\nimport { define, html, onCleanup, onMounted, ref } from '@vielzeug/ore';\nimport { createElementSize } from '@vielzeug/sentinel';\n\ndefine('measured-panel', {\n setup() {\n const panel = ref<HTMLElement>();\n\n onMounted(() => {\n const element = panel.value;\n if (!element) return;\n\n const size = createElementSize(element);\n const update = () => {\n element.dataset.width = String(size.getSnapshot()?.width ?? 0);\n };\n const unsubscribe = size.subscribe(update);\n\n onCleanup(() => {\n unsubscribe();\n size.dispose();\n });\n });\n\n return html`<section ref=${panel}>Measured panel</section>`;\n },\n});\n```\n\n## Best Practices\n\n- **Create** DOM-dependent Sentinels only after their target elements exist.\n- **Read** the latest snapshot with `getSnapshot()` inside subscription listeners.\n- **Unsubscribe** Ripple listeners when their owner ends.\n- **Dispose** every Sentinel to release browser observers and event listeners.\n- **Share** an `AbortSignal` when multiple Sentinels have the same lifetime.\n- **Guard** APIs that can throw `SentinelUnavailableError`.\n- **Treat** `NetworkState.connection` as optional browser enhancement data.\n- **Invoke** factories only in browser client lifecycles.\n",
|
|
7
|
-
"examples": "---\ntitle: Sentinel
|
|
4
|
+
"index": "---\ntitle: 'Sentinel: Subscribable environment snapshots'\ndescription: Subscribable browser and DOM snapshots for viewport, network, media query, element size, intersection state, and screen wake lock.\npackage: sentinel\ncategory: Environment\nkeywords: [subscribable, browser, viewport, network, media-query, resize-observer, intersection-observer, wake-lock, screen, fullscreen]\nrelated: [ripple, ore, focus, gesture]\nexports: [createViewport, createNetwork, createMediaQuery, createElementSize, createIntersection, createWakeLock, createFullscreen, SentinelError, SentinelUnavailableError, Sentinel]\nenvironments: [browser]\n---\n\n<!-- markdownlint-disable MD025 MD033 MD060 -->\n\n<PackageHero package=\"sentinel\" />\n\n## Why Sentinel?\n\nBrowser environment APIs use different events, observer callbacks, initial states, and cleanup methods. Sentinel gives them one explicit external-store shape with snapshot reads, subscriptions, and disposal.\n\n```ts\n// Before\n{\n const panel = document.querySelector<HTMLElement>('[data-panel]');\n if (!panel) throw new Error('Panel not found');\n\n const observer = new ResizeObserver(([entry]) => {\n console.log(entry?.contentRect.width);\n });\n observer.observe(panel);\n\n // Later\n observer.disconnect();\n}\n\n// After\nimport { createElementSize } from '@vielzeug/sentinel';\n\n{\n const panel = document.querySelector<HTMLElement>('[data-panel]');\n if (!panel) throw new Error('Panel not found');\n\n const size = createElementSize(panel);\n const unsubscribe = size.subscribe(() => {\n console.log(size.getSnapshot()?.width);\n });\n\n // Later\n unsubscribe();\n size.dispose();\n}\n```\n\n| Feature | Sentinel | Native observer APIs | Ad hoc event listeners |\n| --- | --- | --- | --- |\n| Bundle size | <PackageInfo package=\"sentinel\" type=\"size\" /> | Built in | Application-defined |\n| Zero dependencies | <ore-icon name=\"check\" size=\"16\"></ore-icon> | <ore-icon name=\"check\" size=\"16\"></ore-icon> | <ore-icon name=\"check\" size=\"16\"></ore-icon> |\n| Subscribable current snapshot | <ore-icon name=\"check\" size=\"16\"></ore-icon> | <ore-icon name=\"x\" size=\"16\"></ore-icon> | <ore-icon name=\"x\" size=\"16\"></ore-icon> |\n| Consistent disposable handle | <ore-icon name=\"check\" size=\"16\"></ore-icon> | <ore-icon name=\"triangle-alert\" size=\"16\"></ore-icon> | <ore-icon name=\"x\" size=\"16\"></ore-icon> |\n| Shared abort ownership | <ore-icon name=\"check\" size=\"16\"></ore-icon> | <ore-icon name=\"x\" size=\"16\"></ore-icon> | <ore-icon name=\"triangle-alert\" size=\"16\"></ore-icon> |\n| Ripple composition | <ore-icon name=\"check\" size=\"16\"></ore-icon> | <ore-icon name=\"x\" size=\"16\"></ore-icon> | <ore-icon name=\"x\" size=\"16\"></ore-icon> |\n\n<div class=\"decision-callout\">\n\n**Use Sentinel when** browser or DOM observations need consistent snapshot reads, subscriptions, and ownership.\n\n**Consider native APIs when** one isolated observer is sufficient and a shared lifecycle abstraction adds no value.\n\n</div>\n\n## Installation\n\n::: code-group\n\n```sh [pnpm]\npnpm add @vielzeug/sentinel\n```\n\n```sh [npm]\nnpm install @vielzeug/sentinel\n```\n\n```sh [yarn]\nyarn add @vielzeug/sentinel\n```\n\n:::\n\n## Quick Start\n\nCreate a viewport Sentinel, render its initial state, then react to changes until the page lifetime ends.\n\n```ts\nimport { createViewport } from '@vielzeug/sentinel';\n\nfunction observeViewport(): () => void {\n const viewport = createViewport();\n const render = () => {\n const { dpr, height, width } = viewport.getSnapshot();\n console.log(`${width}×${height} at ${dpr}dpr`);\n };\n\n render();\n const unsubscribe = viewport.subscribe(render);\n\n return () => {\n unsubscribe();\n viewport.dispose();\n };\n}\n\nconst stopObserving = observeViewport();\n// Call stopObserving() when the owning view unmounts.\n```\n\n<div class=\"features-grid\">\n\n## Features\n\n- `createViewport()`: Observe viewport dimensions and device pixel ratio.\n- `createNetwork()`: Track online status and optional connection details.\n- `createMediaQuery()`: Observe one media query.\n- `createElementSize()`: Read content-box dimensions from `ResizeObserver`.\n- `createIntersection()`: Track normalized intersection state.\n- `createWakeLock()`: Prevent the screen from sleeping during active use.\n- `createFullscreen()`: Track and drive document fullscreen state.\n- `dispose()`: Release owned browser observers and listeners.\n- `SentinelOptions.signal`: Abort several Sentinels through one external lifetime.\n\n</div>\n\n<div class=\"doc-links\">\n\n## Documentation\n\n- [**Usage Guide**](./usage.md)\n- [**API Reference**](./api.md)\n- [**Examples**](./examples.md)\n- [**3.0 Migration Guide**](./migration.md)\n\n</div>\n\n<div class=\"see-also\">\n\n## See Also\n\n- [@vielzeug/ripple](../ripple/): Derive and watch values from Sentinel state.\n- [@vielzeug/ore](../ore/): Bind Sentinels to web-component mount and cleanup lifecycles.\n- [@vielzeug/focus](../focus/): Manage keyboard focus alongside observed UI state.\n- [@vielzeug/gesture](../gesture/): Handle pointer gestures alongside environmental observations.\n\n</div>\n\n<!-- markdownlint-enable MD025 MD033 MD060 -->\n",
|
|
5
|
+
"api": "---\ntitle: 'Sentinel: API Reference'\ndescription: Factory signatures, options, state types, lifecycle handles, and errors for Sentinel.\n---\n\n[[toc]]\n\n## API Overview\n\n| Symbol | Purpose | Execution mode | Common gotcha |\n| --- | --- | --- | --- |\n| `createViewport()` | Observe layout viewport dimensions and device pixel ratio | Sync | Requires a browser Window |\n| `createNetwork()` | Observe online status and optional connection information | Sync | `connection` is often `null` |\n| `createMediaQuery()` | Observe one media query | Sync | Throws when `matchMedia` is unavailable |\n| `createElementSize()` | Observe element content-box dimensions | Sync | Value is `null` before the first delivery |\n| `createIntersection()` | Observe element intersection state | Sync | Value is `null` before the first delivery |\n| `createWakeLock()` | Prevent the screen from sleeping | Async | Degrades silently when the Wake Lock API is unavailable |\n| `createFullscreen()` | Track and drive document fullscreen state | Sync | Degrades silently when the Fullscreen API is unavailable |\n| `Sentinel<T>` | Expose an external-store snapshot with explicit browser-resource ownership | Sync | Call `getSnapshot()` inside subscription listeners |\n| `SentinelError` | Base class for package-defined errors | Sync | Catch a subtype when recovery is specific |\n| `SentinelUnavailableError` | Report an unavailable browser API | Sync | Invalid observer inputs retain their native errors |\n\n## Package Entry Point\n\n| Import | Purpose |\n| --- | --- |\n| `@vielzeug/sentinel` | All factories, state types, option types, and error classes |\n\n## Factories\n\n### `createViewport()`\n\n```ts\nfunction createViewport(options?: WindowSentinelOptions): Sentinel<ViewportState>;\n```\n\nReturns a Sentinel initialized from the layout viewport's `innerWidth`, `innerHeight`, and `devicePixelRatio`.\n\n| Parameter | Type | Description |\n| --- | --- | --- |\n| `options.target` | `Window` | Window to observe instead of the global browser window |\n| `options.signal` | `AbortSignal` | External signal that disposes the Sentinel |\n\n**Returns:** `Sentinel<ViewportState>`.\n\n**Example**\n\n```ts\nimport { createViewport } from '@vielzeug/sentinel';\n\nconst viewport = createViewport();\nconsole.log(viewport.getSnapshot().width);\nviewport.dispose();\n```\n\n---\n\n### `createNetwork()`\n\n```ts\nfunction createNetwork(options?: WindowSentinelOptions): Sentinel<NetworkState>;\n```\n\nReturns a Sentinel initialized from `navigator.onLine` and the optional Network Information API.\n\n| Parameter | Type | Description |\n| --- | --- | --- |\n| `options.target` | `Window` | Window whose navigator and events are observed |\n| `options.signal` | `AbortSignal` | External signal that disposes the Sentinel |\n\n**Returns:** `Sentinel<NetworkState>`.\n\n**Example**\n\n```ts\nimport { createNetwork } from '@vielzeug/sentinel';\n\nconst network = createNetwork();\nconsole.log(network.getSnapshot().online);\nnetwork.dispose();\n```\n\n---\n\n### `createMediaQuery()`\n\n```ts\nfunction createMediaQuery(query: string, options?: WindowSentinelOptions): Sentinel<MediaQueryState>;\n```\n\nReturns a Sentinel initialized from `matchMedia(query).matches`.\n\n| Parameter | Type | Description |\n| --- | --- | --- |\n| `query` | `string` | CSS media query to observe |\n| `options.target` | `Window` | Window whose `matchMedia` method is used |\n| `options.signal` | `AbortSignal` | External signal that disposes the Sentinel |\n\n**Returns:** `Sentinel<MediaQueryState>`.\n\n**Example**\n\n```ts\nimport { createMediaQuery } from '@vielzeug/sentinel';\n\nconst darkMode = createMediaQuery('(prefers-color-scheme: dark)');\nconsole.log(darkMode.getSnapshot().matches);\ndarkMode.dispose();\n```\n\n---\n\n### `createElementSize()`\n\n```ts\nfunction createElementSize(element: Element, options?: SentinelOptions): Sentinel<ElementSizeState | null>;\n```\n\nReturns a Sentinel containing the latest `ResizeObserverEntry.contentRect` dimensions.\n\n| Parameter | Type | Description |\n| --- | --- | --- |\n| `element` | `Element` | Element to observe |\n| `options.signal` | `AbortSignal` | External signal that disposes the Sentinel |\n\n**Returns:** `Sentinel<ElementSizeState | null>`. The initial value is `null`.\n\n**Example**\n\n```ts\nimport { createElementSize } from '@vielzeug/sentinel';\n\nconst size = createElementSize(document.body);\nconst unsubscribe = size.subscribe(() => {\n console.log(size.getSnapshot()?.width);\n});\n\nunsubscribe();\nsize.dispose();\n```\n\n---\n\n### `createIntersection()`\n\n```ts\nfunction createIntersection(\n element: Element,\n options?: CreateIntersectionOptions,\n): Sentinel<IntersectionState | null>;\n```\n\nReturns a Sentinel containing normalized fields from the latest IntersectionObserver entry.\n\n| Parameter | Type | Description |\n| --- | --- | --- |\n| `element` | `Element` | Element to observe |\n| `options.root` | `Element \\| Document \\| null` | Intersection root |\n| `options.rootMargin` | `string` | Margin applied to the root |\n| `options.scrollMargin` | `string` | Margin applied to nested scroll containers |\n| `options.threshold` | `number \\| number[]` | Intersection ratio threshold or thresholds |\n| `options.signal` | `AbortSignal` | External signal that disposes the Sentinel |\n\n**Returns:** `Sentinel<IntersectionState | null>`. The initial value is `null`.\n\n**Example**\n\n```ts\nimport { createIntersection } from '@vielzeug/sentinel';\n\nconst intersection = createIntersection(document.body, { threshold: 0.5 });\nconst unsubscribe = intersection.subscribe(() => {\n console.log(intersection.getSnapshot()?.isIntersecting);\n});\n\nunsubscribe();\nintersection.dispose();\n```\n\n---\n\n### `createWakeLock()`\n\n```ts\nfunction createWakeLock(options?: SentinelOptions & { readonly target?: Window }): WakeLockSentinel;\n```\n\nReturns a Sentinel-backed screen wake lock. `request()` acquires the lock; `release()` frees it. When the tab is hidden the browser auto-releases the lock; the Sentinel re-acquires it automatically on `visibilitychange` if the lock was active. Degrades silently when the Screen Wake Lock API is unavailable.\n\n| Parameter | Type | Description |\n| --- | --- | --- |\n| `options.target` | `Window` | Window whose document and navigator are used |\n| `options.signal` | `AbortSignal` | External signal that disposes the Sentinel |\n\n**Returns:** `WakeLockSentinel`.\n\n**Example**\n\n```ts\nimport { createWakeLock } from '@vielzeug/sentinel';\n\nconst wakeLock = createWakeLock();\nwakeLock.request();\n\nconst unsubscribe = wakeLock.subscribe(() => {\n console.log('Wake lock active:', wakeLock.getSnapshot().active);\n});\n\n// Later\nwakeLock.release();\nunsubscribe();\nwakeLock.dispose();\n```\n\n---\n\n### `createFullscreen()`\n\n```ts\nfunction createFullscreen(options?: SentinelOptions & { readonly target?: Window }): FullscreenSentinel;\n```\n\nReturns a Sentinel-backed fullscreen view. `request()` enters fullscreen for an element (default: the document element), `exit()` leaves it, and `toggle()` flips it. All degrade silently when the Fullscreen API is missing or the browser refuses (user-activation requirements). Disposing the sentinel exits fullscreen and stops tracking `fullscreenchange`.\n\n| Parameter | Type | Description |\n| --- | --- | --- |\n| `options.target` | `Window` | Window whose document is tracked |\n| `options.signal` | `AbortSignal` | External signal that disposes the Sentinel |\n\n**Returns:** `FullscreenSentinel`.\n\n**Example**\n\n```ts\nimport { createFullscreen } from '@vielzeug/sentinel';\n\nconst fullscreen = createFullscreen();\nfullscreen.request();\n\nconst unsubscribe = fullscreen.subscribe(() => {\n console.log('Fullscreen active:', fullscreen.getSnapshot().active);\n});\n\n// Later\nfullscreen.toggle();\nunsubscribe();\nfullscreen.dispose();\n```\n\n## Types\n\n### `Sentinel<T>`\n\n```ts\ninterface Sentinel<T> extends Subscribable<T>, Disposable {\n tap(handler: (event: SentinelEvent) => void, options?: { signal?: AbortSignal }): () => void;\n}\n\ninterface Subscribable<T> {\n getSnapshot(): T;\n subscribe(listener: () => void): () => void;\n}\n```\n\n`Sentinel<T>` follows the standard external-store snapshot/subscription contract and owns the underlying browser resource. `getSnapshot()`, `subscribe()`, and `dispose()` are stable callback-safe functions. `dispose()` stops observation, clears listeners, and aborts `disposalSignal`.\n\n| Member | Type | Description |\n| --- | --- | --- |\n| `getSnapshot()` | `() => T` | Return the current immutable snapshot |\n| `subscribe(listener)` | `(listener: () => void) => () => void` | Subscribe to snapshot changes and return an unsubscribe function |\n| `disposed` | `boolean` | Whether observation has ended |\n| `disposalSignal` | `AbortSignal` | Aborts when observation ends |\n| `dispose()` | `() => void` | Stop observation and release owned browser resources |\n| `tap(handler, options?)` | `(handler: (event: SentinelEvent) => void, options?: { signal?: AbortSignal }) => () => void` | Observe `{ type: 'error' }` when a listener throws and `{ type: 'dispose' }` on disposal; handler errors are swallowed |\n| `[Symbol.dispose]()` | `() => void` | Dispose through the explicit resource-management protocol |\n\n---\n\n### `SentinelOptions`\n\n```ts\ninterface SentinelOptions {\n readonly signal?: AbortSignal;\n}\n```\n\n`options.signal` disposes the Sentinel when it aborts. Passing an already-aborted signal throws `SentinelError` instead of constructing a dead handle.\n\n---\n\n### `createSentinel()`\n\n```ts\nfunction createSentinel<T>(\n options: CreateSentinelOptions<T>,\n setup: (update: (value: T) => void) => () => void,\n): Sentinel<T>\n```\n\nThe generic primitive every built-in sensor is built on. `setup` starts observing and returns its cleanup; it calls `update(value)` whenever the underlying source changes. Use it for platform sensors the built-ins do not cover (for example `prefers-reduced-motion`).\n\n---\n\n### `WindowSentinelOptions`\n\n```ts\ninterface WindowSentinelOptions extends SentinelOptions {\n readonly target?: Window;\n}\n```\n\n---\n\n### `CreateIntersectionOptions`\n\n```ts\ninterface CreateIntersectionOptions extends SentinelOptions {\n readonly root?: Element | Document | null;\n readonly rootMargin?: string;\n readonly scrollMargin?: string;\n readonly threshold?: number | number[];\n}\n```\n\n---\n\n### `ViewportState`\n\n```ts\ninterface ViewportState {\n readonly dpr: number;\n readonly height: number;\n readonly width: number;\n}\n```\n\n---\n\n### `NetworkConnectionSnapshot`\n\n```ts\ninterface NetworkConnectionSnapshot {\n readonly downlink?: number;\n readonly effectiveType?: 'slow-2g' | '2g' | '3g' | '4g';\n readonly rtt?: number;\n readonly saveData?: boolean;\n}\n```\n\n---\n\n### `NetworkState`\n\n```ts\ninterface NetworkState {\n readonly connection: NetworkConnectionSnapshot | null;\n readonly online: boolean;\n}\n```\n\n---\n\n### `MediaQueryState`\n\n```ts\ninterface MediaQueryState {\n readonly matches: boolean;\n}\n```\n\n---\n\n### `ElementSizeState`\n\n```ts\ninterface ElementSizeState {\n readonly height: number;\n readonly width: number;\n}\n```\n\n---\n\n### `IntersectionState`\n\n```ts\ninterface IntersectionState {\n readonly intersectionRatio: number;\n readonly isIntersecting: boolean;\n}\n```\n\n---\n\n### `WakeLockState`\n\n```ts\ninterface WakeLockState {\n readonly active: boolean;\n readonly supported: boolean;\n}\n```\n\n| Field | Type | Description |\n| --- | --- | --- |\n| `active` | `boolean` | Whether a wake lock is currently held |\n| `supported` | `boolean` | Whether the Screen Wake Lock API is available in this environment |\n\n---\n\n### `WakeLockSentinel`\n\n```ts\ninterface WakeLockSentinel extends Sentinel<WakeLockState> {\n request(): void;\n release(): void;\n}\n```\n\nA `Sentinel<WakeLockState>` with two additional methods:\n\n| Member | Type | Description |\n| --- | --- | --- |\n| `request()` | `() => void` | Acquire the screen wake lock. No-op if already active or unsupported |\n| `release()` | `() => void` | Release the wake lock. No-op if not active |\n\n---\n\n### `FullscreenState`\n\n```ts\ninterface FullscreenState {\n readonly active: boolean;\n readonly supported: boolean;\n}\n```\n\n| Property | Type | Description |\n| --- | --- | --- |\n| `active` | `boolean` | Whether an element is currently shown fullscreen |\n| `supported` | `boolean` | Whether the Fullscreen API is available in this environment |\n\n---\n\n### `FullscreenSentinel`\n\n```ts\ninterface FullscreenSentinel extends Sentinel<FullscreenState> {\n request(element?: Element): void;\n exit(): void;\n toggle(element?: Element): void;\n}\n```\n\nA `Sentinel<FullscreenState>` with three additional methods:\n\n| Member | Type | Description |\n| --- | --- | --- |\n| `request(element?)` | `() => void` | Enter fullscreen for `element` (default: the document element). No-op when already active or unsupported |\n| `exit()` | `() => void` | Leave fullscreen. No-op when not active |\n| `toggle(element?)` | `() => void` | Flip fullscreen for `element` (default: the document element) |\n\n## Errors\n\n### `SentinelError`\n\n```ts\nclass SentinelError extends Error {\n constructor(message: string, options?: ErrorOptions);\n}\n```\n\nBase class for package-defined errors.\n\n---\n\n### `SentinelUnavailableError`\n\n```ts\nclass SentinelUnavailableError extends SentinelError {}\n```\n\nThrown when a required browser API or Window is unavailable:\n\n- `createViewport()` and `createNetwork()` when no browser Window is available.\n- `createMediaQuery()` when `matchMedia` is unavailable.\n- `createElementSize()` when the element has no Window or `ResizeObserver` is unavailable.\n- `createIntersection()` when the element has no Window or `IntersectionObserver` is unavailable.\n- `createWakeLock()` when no browser Window is available.\n- `createFullscreen()` when no browser Window is available.\n\nNative setup errors remain unchanged, including invalid observer options or targets.\n",
|
|
6
|
+
"usage": "---\ntitle: 'Sentinel: Usage Guide'\ndescription: Observe browser and DOM state through subscribable snapshots with explicit lifecycles.\n---\n\n[[toc]]\n\n## Basic Usage\n\nCreate a Sentinel, read its current state, subscribe to invalidations, and release both resources when the owner ends.\n\n```ts\nimport { createViewport } from '@vielzeug/sentinel';\n\nfunction observeViewport(): () => void {\n const viewport = createViewport();\n\n const render = () => {\n const { dpr, height, width } = viewport.getSnapshot();\n console.log(`${width}×${height} at ${dpr}dpr`);\n };\n\n render();\n const unsubscribe = viewport.subscribe(render);\n\n return () => {\n unsubscribe();\n viewport.dispose();\n };\n}\n\nconst stopObserving = observeViewport();\n// Call stopObserving() when the owning view unmounts.\n```\n\n`subscribe()` notifies you that the snapshot changed; call `getSnapshot()` inside the listener to read it. Disposing a Sentinel stops its browser observer, removes its listeners, and aborts `disposalSignal`.\n\n## Observe Window State\n\nUse `createViewport()` for viewport dimensions and device pixel ratio.\n\n```ts\nimport { createViewport } from '@vielzeug/sentinel';\n\nconst viewport = createViewport();\nconsole.log(viewport.getSnapshot().width);\nconsole.log(viewport.getSnapshot().height);\nconsole.log(viewport.getSnapshot().dpr);\n```\n\nUse `createNetwork()` for online status and the optional Network Information API snapshot.\n\n```ts\nimport { createNetwork } from '@vielzeug/sentinel';\n\nconst network = createNetwork();\nconsole.log(network.getSnapshot().online);\nconsole.log(network.getSnapshot().connection);\n```\n\n`connection` is `null` when `navigator.connection` is unavailable.\n\n## Observe Media Queries\n\nUse `createMediaQuery()` to react to a browser media query.\n\n```ts\nimport { createMediaQuery, SentinelUnavailableError } from '@vielzeug/sentinel';\n\nfunction observeReducedMotion(): () => void {\n try {\n const reducedMotion = createMediaQuery('(prefers-reduced-motion: reduce)');\n\n const applyPreference = () => {\n document.documentElement.classList.toggle('reduce-motion', reducedMotion.getSnapshot().matches);\n };\n\n applyPreference();\n const unsubscribe = reducedMotion.subscribe(applyPreference);\n\n return () => {\n unsubscribe();\n reducedMotion.dispose();\n };\n } catch (error) {\n if (!(error instanceof SentinelUnavailableError)) throw error;\n return () => {};\n }\n}\n\nconst stopObserving = observeReducedMotion();\n// Call stopObserving() when the owning view unmounts.\n```\n\n`createMediaQuery()` throws `SentinelUnavailableError` when `matchMedia` is unavailable.\n\n## Observe Elements\n\n### Element Size\n\nUse `createElementSize()` after the target element exists.\n\n```ts\nimport { createElementSize } from '@vielzeug/sentinel';\n\nconst panel = document.querySelector<HTMLElement>('[data-panel]');\nif (!panel) throw new Error('Panel not found');\n\nconst size = createElementSize(panel);\nconst unsubscribe = size.subscribe(() => {\n const current = size.getSnapshot();\n if (current) panel.dataset.width = String(current.width);\n});\n```\n\nThe initial state is `null` until `ResizeObserver` reports its first measurement.\n\n### Intersection\n\nUse `createIntersection()` to observe visibility relative to the viewport or a custom root.\n\n```ts\nimport { createIntersection } from '@vielzeug/sentinel';\n\nconst target = document.querySelector<HTMLElement>('[data-lazy-section]');\nif (!target) throw new Error('Section not found');\n\nconst intersection = createIntersection(target, {\n rootMargin: '100px',\n threshold: [0, 0.5, 1],\n});\n\nconst unsubscribe = intersection.subscribe(() => {\n target.hidden = !intersection.getSnapshot()?.isIntersecting;\n});\n```\n\nThe initial state is `null` until `IntersectionObserver` reports its first entry.\n\n## Control Ownership\n\nCall `dispose()` to stop observation. Disposal is idempotent.\n\n```ts\nconst viewport = createViewport();\n\nviewport.dispose();\nviewport.dispose();\n```\n\nPass an `AbortSignal` when several Sentinels share one lifetime.\n\n```ts\nconst controller = new AbortController();\nconst viewport = createViewport({ signal: controller.signal });\nconst network = createNetwork({ signal: controller.signal });\n\ncontroller.abort();\n```\n\nSentinel follows the standard external-store shape, so reactive libraries can bridge it without a Sentinel-specific adapter.\n\n## Prevent Screen Sleep\n\nUse `createWakeLock()` to keep the screen awake during active use: for example, during a game, presentation, or long-running task.\n\n```ts\nimport { createWakeLock } from '@vielzeug/sentinel';\n\nconst wakeLock = createWakeLock();\nwakeLock.request();\n\nconst unsubscribe = wakeLock.subscribe(() => {\n console.log('Wake lock active:', wakeLock.getSnapshot().active);\n});\n\n// Later: release the lock and dispose\nwakeLock.release();\nunsubscribe();\nwakeLock.dispose();\n```\n\nThe lock auto-releases when the tab is hidden (the browser does this). The Sentinel re-acquires it automatically when the tab becomes visible again if `request()` was called and `release()` was not.\n\nCheck `getSnapshot().supported` before acquiring if you need to show different UI when the Wake Lock API is unavailable.\n\n```ts\nconst wakeLock = createWakeLock();\n\nif (wakeLock.getSnapshot().supported) {\n wakeLock.request();\n}\n```\n\n## Track Fullscreen State\n\nUse `createFullscreen()` to observe and drive document fullscreen state: for example, immersing a board or map view:\n\n```ts\nimport { createFullscreen } from '@vielzeug/sentinel';\n\nconst fullscreen = createFullscreen();\nconst unsubscribe = fullscreen.subscribe(() => {\n fullscreenButton.pressed = fullscreen.getSnapshot().active;\n});\n\nfullscreenButton.addEventListener('click', () => fullscreen.toggle());\n```\n\n`request()`, `exit()`, and `toggle()` degrade silently when the Fullscreen API is unavailable or the browser refuses (user activation is required in some browsers). Disposing the sentinel exits fullscreen, so leaving the owning view releases it.\n\n## Handle Unavailable APIs\n\n`createMediaQuery()`, `createElementSize()`, and `createIntersection()` report unavailable platform APIs with `SentinelUnavailableError`.\n\n```ts\nimport { createElementSize, SentinelUnavailableError } from '@vielzeug/sentinel';\n\ntry {\n const size = createElementSize(document.body);\n size.dispose();\n} catch (error) {\n if (error instanceof SentinelUnavailableError) {\n console.warn(error.message);\n } else {\n throw error;\n }\n}\n```\n\nInvoke all factories only in a browser client lifecycle. Package imports are safe during SSR, but factories require browser or DOM APIs.\n\n## Framework Integration\n\nSentinel's callback-safe external-store methods integrate directly with framework subscription APIs. Create browser-bound Sentinels in a client owner and dispose them when that owner ends.\n\n::: code-group\n\n```tsx [React]\nimport { type Sentinel, type ViewportState } from '@vielzeug/sentinel';\nimport { useSyncExternalStore } from 'react';\n\ntype ViewportSizeProps = {\n readonly viewport: Sentinel<ViewportState>;\n};\n\nexport function ViewportSize({ viewport }: ViewportSizeProps) {\n const viewportState = useSyncExternalStore(viewport.subscribe, viewport.getSnapshot);\n\n return <output>{`${viewportState.width}×${viewportState.height}`}</output>;\n}\n```\n\n```vue [Vue 3]\n<script setup lang=\"ts\">\nimport { createViewport, type Sentinel, type ViewportState } from '@vielzeug/sentinel';\nimport { onMounted, onUnmounted, ref } from 'vue';\n\nconst viewportState = ref<ViewportState | null>(null);\nlet viewport: Sentinel<ViewportState> | undefined;\nlet unsubscribe: (() => void) | undefined;\n\nonMounted(() => {\n viewport = createViewport();\n const update = () => {\n viewportState.value = viewport?.getSnapshot() ?? null;\n };\n\n update();\n unsubscribe = viewport.subscribe(update);\n});\n\nonUnmounted(() => {\n unsubscribe?.();\n viewport?.dispose();\n});\n</script>\n\n<template>\n <output>\n {{ viewportState ? `${viewportState.width}×${viewportState.height}` : 'Measuring…' }}\n </output>\n</template>\n```\n\n```svelte [Svelte]\n<script lang=\"ts\">\n import { createViewport, type ViewportState } from '@vielzeug/sentinel';\n import { onMount } from 'svelte';\n\n let viewportState: ViewportState | null = null;\n\n onMount(() => {\n const viewport = createViewport();\n const update = () => {\n viewportState = viewport.getSnapshot();\n };\n\n update();\n const unsubscribe = viewport.subscribe(update);\n\n return () => {\n unsubscribe();\n viewport.dispose();\n };\n });\n</script>\n\n<output>\n {viewportState ? `${viewportState.width}×${viewportState.height}` : 'Measuring…'}\n</output>\n```\n\n:::\n\n## Working with Other Vielzeug Libraries\n\n### Sentinel + Ripple\n\nBridge Sentinel stores with `fromSubscribable()` before deriving reactive values. Dispose the watcher separately; disposing each Sentinel also disposes its bridge through `disposalSignal`.\n\n```ts\nimport { computed, fromSubscribable, watch } from '@vielzeug/ripple';\nimport { createMediaQuery, createViewport } from '@vielzeug/sentinel';\n\nconst viewport = createViewport();\nconst mobileQuery = createMediaQuery('(max-width: 768px)');\nconst viewportState = fromSubscribable(viewport, { signal: viewport.disposalSignal });\nconst mobileState = fromSubscribable(mobileQuery, { signal: mobileQuery.disposalSignal });\nconst compact = computed(() => mobileState.value.matches || viewportState.value.width < 400);\nconst compactWatcher = watch(compact, (value) => console.log('Compact layout:', value), { immediate: true });\n\ncompactWatcher.dispose();\nmobileQuery.dispose();\nviewport.dispose();\n```\n\n### Sentinel + Ore\n\nCreate DOM-dependent Sentinels in `onMounted()` and register both subscription and Sentinel cleanup with the component.\n\n```ts\nimport { define, html, onCleanup, onMounted, ref } from '@vielzeug/ore';\nimport { createElementSize } from '@vielzeug/sentinel';\n\ndefine('measured-panel', {\n setup() {\n const panel = ref<HTMLElement>();\n\n onMounted(() => {\n const element = panel.value;\n if (!element) return;\n\n const size = createElementSize(element);\n const update = () => {\n element.dataset.width = String(size.getSnapshot()?.width ?? 0);\n };\n const unsubscribe = size.subscribe(update);\n\n onCleanup(() => {\n unsubscribe();\n size.dispose();\n });\n });\n\n return html`<section ref=${panel}>Measured panel</section>`;\n },\n});\n```\n\n## Best Practices\n\n- **Create** DOM-dependent Sentinels only after their target elements exist.\n- **Read** the latest snapshot with `getSnapshot()` inside subscription listeners.\n- **Unsubscribe** Ripple listeners when their owner ends.\n- **Dispose** every Sentinel to release browser observers and event listeners.\n- **Share** an `AbortSignal` when multiple Sentinels have the same lifetime.\n- **Guard** APIs that can throw `SentinelUnavailableError`.\n- **Treat** `NetworkState.connection` as optional browser enhancement data.\n- **Invoke** factories only in browser client lifecycles.\n",
|
|
7
|
+
"examples": "---\ntitle: 'Sentinel: Examples'\ndescription: Focused browser and DOM observation examples for Sentinel.\n---\n\n## Examples\n\n- [Responsive Viewport Tracking](./examples/responsive-viewport-tracking.md)\n- [Monitor Network Condition](./examples/monitor-network-condition.md)\n- [Respect Reduced Motion Preference](./examples/respect-reduced-motion-preference.md)\n- [Responsive Column Layout](./examples/responsive-column-layout.md)\n- [Lazy Load Images on Intersection](./examples/lazy-load-images-on-intersection.md)\n- [Prevent Screen Sleep](./examples/prevent-screen-sleep.md)\n"
|
|
8
8
|
},
|
|
9
9
|
"examples": [
|
|
10
10
|
{
|
package/data/packages/sigil.json
CHANGED
|
@@ -1,20 +1,20 @@
|
|
|
1
1
|
{
|
|
2
2
|
"apiSource": "// Public root surface of @vielzeug/sigil. Every public export goes through this file.\nexport { drawToCanvas } from './canvas';\nexport { encodeQr, qrCapacity } from './encode';\nexport {\n SigilCapacityError,\n SigilDisposedError,\n SigilError,\n SigilOptionError,\n SigilPermissionError,\n SigilUnsupportedError,\n} from './errors';\nexport { createQrScanner, detectQr, isQrScanSupported, qrScanSupport } from './scan';\nexport { toSvg } from './svg';\nexport type {\n QrCanvasOptions,\n QrDetector,\n QrEncodeOptions,\n QrErrorCorrection,\n QrMatrix,\n QrMode,\n QrScanner,\n QrScannerOptions,\n QrScannerStatus,\n QrScanResult,\n QrSvgOptions,\n SigilEvent,\n} from './types';\n",
|
|
3
3
|
"docs": {
|
|
4
|
-
"index": "---\ntitle: Sigil
|
|
5
|
-
"api": "---\ntitle: Sigil
|
|
6
|
-
"usage": "---\ntitle: Sigil
|
|
7
|
-
"examples": "---\ntitle: Sigil
|
|
4
|
+
"index": "---\ntitle: 'Sigil: QR code generation and scanning'\ndescription: \"QR code generation and scanning: pure matrix encoder, SVG and canvas renderers, native BarcodeDetector scanning\"\npackage: sigil\ncategory: utilities\nkeywords: [qr, qrcode, barcode, encoder, scanner, svg, canvas, barcodedetector, pairing]\nexports: [encodeQr, toSvg, createQrScanner, qrCapacity, detectQr]\nrelated: [mesh, refine]\nenvironments: [browser, node]\n---\n\n<!-- markdownlint-disable MD025 MD033 MD060 -->\n\n<PackageHero package=\"sigil\" />\n\n## Why Sigil?\n\nQR workflows usually pull in a general-purpose barcode library that renders to canvas and guesses at colors, or a from-scratch decoder you do not need. Sigil is a pure, zero-dependency encoder (numeric, alphanumeric, and byte modes; versions 1–40; all four error-correction levels; automatic mask selection) with renderers that stay out of the way: an SVG string that themes with CSS, and a canvas painter for print/bitmap flows. Scanning is a thin lifecycle wrapper over the platform's native `BarcodeDetector`, so there is no decoder weight and no polyfill to ship.\n\n```ts\n// Before: a QR library per concern, canvas-only output, hardcoded colors\nimport QRCode from 'qrcode';\nconst dataUrl = await QRCode.toDataURL(text, { width: 256, color: { dark: '#000' } });\nimg.src = dataUrl; // raster, fixed colors, no theme support\n\n// After: encode once, render anywhere\nimport { encodeQr, toSvg } from '@vielzeug/sigil';\nconst matrix = encodeQr(text); // smallest version that fits, auto mask\nelement.innerHTML = toSvg(matrix); // currentColor modules: themes with CSS\n```\n\n| Feature | Sigil | `qrcode` (node-style) | `jsqr` |\n| --- | --- | --- | --- |\n| Bundle size | <PackageInfo package=\"sigil\" type=\"size\" /> | ~100 kB+ | decoder only |\n| Runtime dependencies | <ore-icon name=\"check\" size=\"16\"></ore-icon> none | <ore-icon name=\"x\" size=\"16\"></ore-icon> | <ore-icon name=\"check\" size=\"16\"></ore-icon> none |\n| Output | Matrix, SVG string, canvas | Canvas/DataURL/UTF-8 | N/A |\n| Themeable output | `currentColor` by default | Hardcoded color strings | N/A |\n| Scanning | Native `BarcodeDetector` wrapper | <ore-icon name=\"x\" size=\"16\"></ore-icon> | Pure-JS decoder |\n| SSR-safe import | <ore-icon name=\"check\" size=\"16\"></ore-icon> | <ore-icon name=\"x\" size=\"16\"></ore-icon> | <ore-icon name=\"check\" size=\"16\"></ore-icon> |\n\n<div class=\"decision-callout\">\n\n**Use Sigil when** you need QR output that themes with your UI (SVG/`currentColor`), a minimal encoder with no runtime dependencies, or a scanner that uses the platform decoder instead of shipping one.\n\n**Consider `jsqr`-style decoding when** you must scan in browsers without `BarcodeDetector`: sigil reports `SigilUnsupportedError` rather than bundling a fallback decoder.\n\n</div>\n\n## Installation\n\n::: code-group\n\n```sh [pnpm]\npnpm add @vielzeug/sigil\n```\n\n```sh [npm]\nnpm install @vielzeug/sigil\n```\n\n```sh [yarn]\nyarn add @vielzeug/sigil\n```\n\n:::\n\n## Quick Start\n\nEncode to an SVG string: works in Node and the browser:\n\n```ts\nimport { encodeQr, qrCapacity, toSvg } from '@vielzeug/sigil';\n\nconst payload = 'https://vielzeug.dev';\nconst matrix = encodeQr(payload, { errorCorrection: 'M' });\ndocument.querySelector('#qr').innerHTML = toSvg(matrix, { label: 'Vielzeug link' });\n```\n\nScan with the camera (browser only: see support notes in the usage guide):\n\n```ts\nimport { createQrScanner } from '@vielzeug/sigil';\n\nconst video = document.querySelector('video');\nconst scanner = createQrScanner({ video });\nscanner.onResult((result) => {\n console.log('scanned:', result.value); // once: true stops after first decode\n});\nawait scanner.start(); // throws SigilUnsupportedError / SigilPermissionError\n```\n\n## Features\n\n<div class=\"features-grid\">\n\n- `encodeQr`: pure matrix encoder: numeric/alphanumeric/byte modes, versions 1–40, EC levels L/M/Q/H, automatic or forced mask\n- `qrCapacity`: byte capacity per version/mode/level, for pre-flight validation\n- `toSvg`: compact path-merged SVG string; `currentColor` modules, `<title>` + `role=\"img\"` built in\n- `drawToCanvas`: bitmap rendering with DPR-aware scaling for print and download flows\n- `detectQr`: one-shot detection on any `ImageBitmapSource`\n- `createQrScanner`: camera scan loop with `once` mode, status events, `tap()` observability, and strict disposal\n- `isQrScanSupported`/`qrScanSupport`: sync and async feature detection that never assumes\n\n</div>\n\n## Documentation\n\n<div class=\"doc-links\">\n\n- [Usage Guide](./usage.md): encoding modes, capacity, rendering, scanning, and testing\n- [API Reference](./api.md): every export, option, type, and error\n- [Examples](./examples.md): copy-paste recipes, including pairing two devices with `meshQrCodec`\n\n</div>\n\n## See Also\n\n<div class=\"see-also\">\n\n- [Mesh](../mesh/): backendless P2P sessions; `meshQrCodec` produces compact QR-friendly pairing payloads\n- [Refine](../refine/): `ore-qr-code` and `ore-qr-scanner` wrap sigil in ready-made, themeable components\n\n</div>\n\n<!-- markdownlint-enable -->\n",
|
|
5
|
+
"api": "---\ntitle: 'Sigil: API Reference'\ndescription: 'Public API of @vielzeug/sigil: encoder, renderers, scanner, types, and errors.'\n---\n\n[[toc]]\n\n## API Overview\n\n| Symbol | Purpose | Execution | Common gotcha |\n| --- | --- | --- | --- |\n| `encodeQr` | Encode a payload into a `QrMatrix` | Sync | Throws `SigilCapacityError` over capacity |\n| `qrCapacity` | Payload limit for a version/level/mode | Sync | Returns *characters* for numeric/alphanumeric, *bytes* for byte mode |\n| `toSvg` | Render a matrix as an SVG string | Sync | Escapes `dark`/`light` as raw CSS: pass values, not markup |\n| `drawToCanvas` | Paint a matrix onto a canvas | Sync | Browser only; resizes the canvas for DPR |\n| `detectQr` | One-shot detection on an image source | Async | Throws `SigilUnsupportedError` without `BarcodeDetector` |\n| `createQrScanner` | Camera scan loop | Async (start) | Must `dispose()`; camera tracks persist otherwise |\n| `isQrScanSupported` | Sync feature check | Sync | Constructor exists ≠ `qr_code` supported: prefer `qrScanSupport()` |\n| `qrScanSupport` | Async feature check incl. formats | Async | Never assume support per browser/version |\n\n## Package Entry Point\n\n| Import | Purpose |\n| --- | --- |\n| `@vielzeug/sigil` | Complete public Sigil API |\n\n## Encoding\n\n### `encodeQr(data, options?)`\n\n```ts\nfunction encodeQr(data: string | Uint8Array, options?: QrEncodeOptions): QrMatrix;\n```\n\nEncodes `data` into a QR symbol. Picks the most compact mode covering the whole input, the smallest fitting version, and the lowest-penalty mask.\n\n| Parameter | Type | Default | Description |\n| --- | --- | --- | --- |\n| `data` | `string \\| Uint8Array` | N/A | Payload; `Uint8Array` is raw binary and always byte mode (no UTF-8 round-trip) |\n| `options.errorCorrection` | `'L' \\| 'M' \\| 'Q' \\| 'H'` | `'M'` | Error-correction level |\n| `options.version` | `1–40` | auto | Pin the version; throws if payload can't fit |\n| `options.minVersion` | `1–40` | `1` | Smallest acceptable version |\n| `options.mode` | `QrMode` | auto | Force a segment mode |\n| `options.mask` | `0–7` | auto | Force the mask (test vectors only) |\n\n**Returns** a frozen `QrMatrix` with `size`, `version`, `mode`, `errorCorrection`, `mask`, and `get(x, y)`.\n\n**Throws** `SigilCapacityError` (payload exceeds capacity), `SigilOptionError` (invalid mode/version/mask combination).\n\n```ts\nimport { encodeQr } from '@vielzeug/sigil';\n\nconst matrix = encodeQr('HELLO WORLD', { errorCorrection: 'Q' });\nmatrix.size; // 21 (v1)\nmatrix.get(0, 0); // finder pattern → true\n```\n\n---\n\n### `qrCapacity(version, errorCorrection, mode)`\n\n```ts\nfunction qrCapacity(version: number, errorCorrection: QrErrorCorrection, mode: QrMode): number;\n```\n\nReturns the payload limit: characters for `numeric`/`alphanumeric`, bytes for `byte`.\n\n```ts\nqrCapacity(1, 'M', 'byte'); // → 14\nqrCapacity(40, 'L', 'numeric'); // → 7089\n```\n\n## Rendering\n\n### `toSvg(matrix, options?)`\n\n```ts\nfunction toSvg(matrix: QrMatrix, options?: QrSvgOptions): string;\n```\n\nReturns a complete `<svg>` string. Runs in any environment: no DOM required.\n\n| Parameter | Type | Default | Description |\n| --- | --- | --- | --- |\n| `options.dark` | `string` | `'currentColor'` | Dark module color |\n| `options.light` | `string` | `'transparent'` | Background color |\n| `options.margin` | `number` | `4` | Quiet-zone modules |\n| `options.scale` | `number` | `1` | User units per module |\n| `options.optimizePath` | `boolean` | `true` | Merge dark runs into one `<path>` |\n| `options.label` | `string` | N/A | `<title>` + `aria-label`; enables `role=\"img\"` |\n\n```ts\nconst svg = toSvg(matrix, { label: 'Pairing code', dark: 'var(--qr-dark)' });\n```\n\n---\n\n### `drawToCanvas(matrix, canvas, options?)`: browser only\n\n```ts\nfunction drawToCanvas(matrix: QrMatrix, canvas: HTMLCanvasElement, options?: QrCanvasOptions): number;\n```\n\nPaints the matrix and returns the CSS-pixel edge length. Sizes the backing store for `devicePixelRatio` so output stays crisp on retina displays.\n\n| Parameter | Type | Default | Description |\n| --- | --- | --- | --- |\n| `options.scale` | `number` | `1` | CSS px per module |\n| `options.margin` | `number` | `4` | Quiet-zone modules |\n| `options.dark` / `options.light` | `string` | `'#000'` / `'#fff'` | Fill colors |\n\n## Scanning (browser only)\n\n### `createQrScanner(options)`\n\n```ts\nfunction createQrScanner(options: QrScannerOptions): QrScanner;\n```\n\nCreates a camera scan loop around the native `BarcodeDetector`.\n\n| Parameter | Type | Default | Description |\n| --- | --- | --- | --- |\n| `options.video` | `HTMLVideoElement` | N/A | Element the stream attaches to (needs `autoplay playsinline muted`) |\n| `options.constraints` | `MediaTrackConstraints` | `{ facingMode: 'environment' }` | `getUserMedia` video constraints |\n| `options.intervalMs` | `number` | `200` | Minimum ms between detect passes |\n| `options.once` | `boolean` | `true` | Stop after the first result |\n| `options.signal` | `AbortSignal` | N/A | Abort → `stop()` |\n| `options.detector` | `QrDetector` | native | Injected detector for tests |\n| `options.mediaDevices` | `Pick<MediaDevices, 'getUserMedia'>` | `navigator.mediaDevices` | Injected media for tests |\n\n**Returns** a `QrScanner`:\n\n| Member | Signature | Description |\n| --- | --- | --- |\n| `start()` | `() => Promise<void>` | Request camera, attach stream, run the detect loop. Rejects with `SigilUnsupportedError` / `SigilPermissionError`. `stop()`/`dispose()` while the camera prompt is pending win: the late stream is released and the call resolves |\n| `stop()` | `() => void` | Stop the loop and tracks, return to `'idle'`; instance stays usable |\n| `onResult(handler)` | `(handler) => () => void` | Decode results; returns unsubscribe |\n| `tap(handler, options?)` | `(handler, { signal? }) => () => void` | Observe `SigilEvent`s; handler errors are swallowed |\n| `status` | `QrScannerStatus` | `'idle' \\| 'starting' \\| 'scanning' \\| 'disposed'` |\n| `dispose()` / `[Symbol.dispose]` | `() => void` | Terminal teardown; `disposalSignal` aborts |\n| `disposed` | `boolean` | Whether `dispose()` ran |\n\n```ts\nconst scanner = createQrScanner({ video });\nconst off = scanner.onResult(({ value }) => console.log(value));\nawait scanner.start();\n```\n\n---\n\n### `detectQr(source, options?)`\n\n```ts\nfunction detectQr(\n source: ImageBitmapSource,\n options?: { signal?: AbortSignal; detector?: QrDetector },\n): Promise<QrScanResult | null>;\n```\n\nSingle-pass detection on an image source. Returns the first result's `{ value, cornerPoints }`, or `null` when nothing decodes. Throws `SigilUnsupportedError` when no detector is available.\n\n---\n\n### `isQrScanSupported()` / `qrScanSupport()`\n\n```ts\nfunction isQrScanSupported(): boolean;\nfunction qrScanSupport(): Promise<boolean>;\n```\n\n`isQrScanSupported` checks the `BarcodeDetector` constructor synchronously. `qrScanSupport` additionally asks `getSupportedFormats()` for `'qr_code'`: use it before showing scan affordances.\n\n## Types\n\n```ts\ntype QrErrorCorrection = 'L' | 'M' | 'Q' | 'H';\ntype QrMode = 'numeric' | 'alphanumeric' | 'byte';\ntype QrScannerStatus = 'idle' | 'starting' | 'scanning' | 'disposed';\n\ninterface QrMatrix {\n readonly errorCorrection: QrErrorCorrection;\n readonly mask: number;\n readonly mode: QrMode;\n readonly size: number; // 17 + 4 * version\n readonly version: number; // 1–40\n get(x: number, y: number): boolean; // true = dark; false outside bounds\n}\n\ninterface QrScanResult {\n readonly cornerPoints?: ReadonlyArray<{ readonly x: number; readonly y: number }>;\n readonly value: string;\n}\n\ninterface QrDetector {\n detect(source: ImageBitmapSource): Promise<\n ReadonlyArray<{ rawValue: string; cornerPoints?: ReadonlyArray<{ x: number; y: number }> }>\n >;\n}\n\ntype SigilEvent =\n | { readonly type: 'status-change'; readonly status: QrScannerStatus }\n | { readonly type: 'detect'; readonly value: string; readonly elapsedMs: number }\n | { readonly type: 'frame-skipped'; readonly reason: 'not-ready' | 'busy' }\n | { readonly type: 'error'; readonly error: SigilError }\n | { readonly type: 'dispose' };\n```\n\n`QrEncodeOptions`, `QrSvgOptions`, `QrCanvasOptions`, `QrScannerOptions`, and `QrScanner` are documented under their factories above.\n\n## Errors\n\n| Class | Trigger | Notable members |\n| --- | --- | --- |\n| `SigilError` | Base class for all sigil errors | N/A |\n| `SigilCapacityError` | Payload exceeds the version/mode limit | `bytes`, `maxBytes`, `version` |\n| `SigilOptionError` | Invalid option combination (forced mode can't represent input, bad mask/version) | N/A |\n| `SigilUnsupportedError` | `BarcodeDetector`/`getUserMedia`/canvas 2D context missing | N/A |\n| `SigilPermissionError` | Camera `NotAllowedError` | N/A |\n| `SigilDisposedError` | `start()` on a disposed scanner | N/A |\n\nAll carry `cause` when wrapping a platform error.\n",
|
|
6
|
+
"usage": "---\ntitle: 'Sigil: Usage Guide'\ndescription: Encoding modes, capacity checks, SVG/canvas rendering, camera scanning, and testing for @vielzeug/sigil.\n---\n\n[[toc]]\n\n## Basic Usage\n\n`encodeQr` produces a `QrMatrix`: a frozen square of dark/light modules. Render it with `toSvg` (string, works anywhere) or `drawToCanvas` (browser).\n\n```ts\nimport { encodeQr, qrCapacity, toSvg } from '@vielzeug/sigil';\n\nconst matrix = encodeQr('https://vielzeug.dev');\n// Pick the smallest version automatically, or pin one:\nconst pinned = encodeQr('https://vielzeug.dev', { version: 5, errorCorrection: 'Q' });\n\nconst svg = toSvg(matrix); // inline <svg> string: inject or serialize\n```\n\nThe encoder chooses the most compact mode for the whole input and the smallest version that fits. `mask` is chosen by the lowest ISO penalty score: override it only for test vectors.\n\n## Encoding Modes and Capacity\n\nThree segment modes cover every payload:\n\n| Mode | Alphabet | Max payload (v40-L) |\n| --- | --- | --- |\n| `numeric` | `0–9` | 7,089 digits |\n| `alphanumeric` | `0–9 A-Z space $%*+-./:` | 4,296 chars |\n| `byte` | UTF-8 (no ECI header) | 2,953 bytes |\n\nMode selection is automatic: `encodeQr('01234567')` uses `numeric`, `'HELLO WORLD'` uses `alphanumeric`, anything else uses `byte`. Force a mode with `mode`, but a forced mode that cannot represent the input throws `SigilOptionError`.\n\n`Uint8Array` input is raw binary: it always encodes in byte mode, byte-for-byte, with no UTF-8 round-trip: invalid UTF-8 (tokens, provisioning payloads) survives intact. Forcing a non-byte mode with binary input throws `SigilOptionError`.\n\nCheck capacity before encoding with `qrCapacity(version, errorCorrection, mode)`: it returns the maximum *bytes* (for `byte` mode) or *characters* the symbol holds:\n\n```ts\nimport { qrCapacity, SigilCapacityError } from '@vielzeug/sigil';\n\nqrCapacity(10, 'M', 'byte'); // → 213 (data codewords available at v10-M)\n\ntry {\n encodeQr(hugePayload, { version: 1 });\n} catch (error) {\n if (error instanceof SigilCapacityError) {\n console.log(`${error.bytes} > ${error.maxBytes} at v${error.version}`);\n }\n}\n```\n\nUse `minVersion` to keep a stable symbol size while a payload changes: useful when a layout must not reflow between renders.\n\n## Error Correction\n\n| Level | Recovery | Trade-off |\n| --- | --- | --- |\n| `L` | ~7% | Smallest matrix |\n| `M` | ~15% | Default: good balance |\n| `Q` | ~25% | Print, wear |\n| `H` | ~30% | Logos/overlays, dense codes |\n\nHigher levels shrink capacity at the same version. For an overlaid logo, prefer `H` *and* keep the logo inside the quiet zone's margin of safety.\n\n## Rendering\n\n### SVG: `toSvg`\n\n```ts\nconst svg = toSvg(matrix, {\n dark: 'currentColor', // default: inherits text color\n light: 'transparent', // default\n margin: 4, // quiet zone in modules (spec minimum is 4)\n scale: 1, // user units per module: prefer scaling via CSS\n optimizePath: true, // merge adjacent modules into one path (default)\n label: 'Pairing code', // <title> + role=\"img\"\n});\n```\n\n`optimizePath` merges each row's dark runs into a single `<path>`: orders of magnitude smaller markup. Set `dark`/`light` to CSS variables to theme the code (`dark: 'var(--qr-dark)'`).\n\n### Canvas: `drawToCanvas` (browser only)\n\n```ts\nconst sizePx = drawToCanvas(matrix, canvas, { scale: 8, margin: 4 });\n// canvas is resized for devicePixelRatio; returns the CSS-pixel edge length\n```\n\nUse canvas for print, download (`canvas.toBlob`), or compositing. SVG is the better default for display.\n\n## Scanning (browser only)\n\nScanning wraps the native `BarcodeDetector`. It is **not** universally available: feature-detect, never assume:\n\n| Engine | `BarcodeDetector` | Notes |\n| --- | --- | --- |\n| Chrome / Edge / Android WebView | Yes | `qr_code` format supported |\n| Firefox | No (behind flag in some builds) | `isQrScanSupported()` → `false` |\n| Safari | Varies by version | Check at runtime; do not rely on version sniffing |\n\n```ts\nimport { createQrScanner, isQrScanSupported, qrScanSupport } from '@vielzeug/sigil';\n\nisQrScanSupported(); // sync: BarcodeDetector constructor exists\nawait qrScanSupport(); // async: also verifies 'qr_code' is a supported format\n\nconst scanner = createQrScanner({\n video, // <video autoplay playsinline muted>\n constraints: { facingMode: 'environment' },\n intervalMs: 200, // min ms between detect passes\n once: true, // stop after first result\n});\n\nscanner.onResult((result) => showPairing(result.value));\nscanner.tap((event) => console.debug(event.type)); // status-change, detect, frame-skipped…\n\nawait scanner.start(); // requests camera, attaches stream, starts the loop\nscanner.stop(); // releases the camera, back to 'idle'; start() resumes\nscanner.dispose(); // terminal: releases everything\n```\n\nStatuses are `'idle' | 'starting' | 'scanning' | 'disposed'`. A `stop()` or `dispose()` that lands while the camera permission prompt is pending wins the race: the late stream is released and `start()` resolves without restarting the loop.\n\n`start()` rejects with `SigilUnsupportedError` (no detector/getUserMedia) or `SigilPermissionError` (`NotAllowedError`). Detection failures mid-scan surface through `tap` `error` events and stop the scanner.\n\n### One-shot detection\n\n`detectQr(source)` runs a single pass over any `ImageBitmapSource`: `ImageData`, `Blob`, `VideoFrame`, an `<img>`, or a `<canvas>`:\n\n```ts\nimport { detectQr } from '@vielzeug/sigil';\n\nconst result = await detectQr(imageBitmap); // → { value, cornerPoints } | null\n```\n\n## Testing\n\nBoth seams are injectable: never touch globals in tests:\n\n```ts\nimport { createQrScanner } from '@vielzeug/sigil';\n\nconst fakeScanner = createQrScanner({\n video: document.createElement('video'),\n detector: { detect: async () => [{ rawValue: 'payload' }] }, // fake BarcodeDetector\n mediaDevices: { getUserMedia: async () => fakeStream }, // fake camera\n});\n```\n\nFor the web components, `ore-qr-scanner` exposes a `scannerFactory` JS property so tests inject a fake `QrScanner` without stubbing `BarcodeDetector` or `getUserMedia`.\n\n## Framework Integration\n\nEncoding is synchronous and SSR-safe: render `toSvg` output as part of server markup:\n\n::: code-group\n\n```tsx [React]\nfunction Qr({ value }: { value: string }) {\n const svg = useMemo(() => toSvg(encodeQr(value)), [value]);\n return <span dangerouslySetInnerHTML={{ __html: svg }} />;\n}\n```\n\n```vue [Vue]\n<script setup>\nimport { computed } from 'vue';\nimport { encodeQr, toSvg } from '@vielzeug/sigil';\nconst props = defineProps({ value: String });\nconst svg = computed(() => toSvg(encodeQr(props.value ?? '')));\n</script>\n<template><span v-html=\"svg\" /></template>\n```\n\n```html [Svelte]\n<script>\n import { encodeQr, toSvg } from '@vielzeug/sigil';\n export let value;\n $: svg = toSvg(encodeQr(value));\n</script>\n<span>{@html svg}</span>\n```\n\n:::\n\nScanning needs lifecycle: start in `onMounted`/`onMount`, always `dispose()` in cleanup.\n\n## Working with Other Vielzeug Libraries\n\n- **Mesh**: `meshQrCodec` compresses WebRTC pairing payloads (offer/answer JSON) to QR-friendly strings. The [Pair Two Devices with QR](./examples/pair-two-devices.md) recipe shows the full flow.\n- **Refine**: prefer `ore-qr-code` / `ore-qr-scanner` over hand-rolled wrappers: they compose sigil with themeable markup, a11y labels, and state handling.\n\n## Best Practices\n\n- Pre-validate payloads with `qrCapacity` before surfacing an encode control.\n- Keep the quiet zone: `margin` below 4 risks unreadable codes; never overlay content on it.\n- Prefer `toSvg` + `currentColor` for display; reserve `drawToCanvas` for print/download.\n- Feature-detect scanning with `qrScanSupport()` before showing a scan affordance.\n- Always `dispose()` scanners: camera tracks outlive route changes otherwise.\n- Use `once: false` only when the UX genuinely expects repeated decodes.\n- Treat scanned strings as untrusted input: validate before navigation or pairing.\n- Pin `minVersion` in animated/live payloads so the symbol doesn't resize mid-flow.\n",
|
|
7
|
+
"examples": "---\ntitle: 'Sigil: Examples'\ndescription: Practical examples and recipes for @vielzeug/sigil.\n---\n\n## Examples\n\n- [Themed SVG QR Code](./examples/themed-svg-qr-code.md)\n- [Pair Two Devices with QR](./examples/pair-two-devices.md)\n"
|
|
8
8
|
},
|
|
9
9
|
"examples": [
|
|
10
10
|
{
|
|
11
11
|
"id": "capacity-check",
|
|
12
|
-
"code": "import { encodeQr, qrCapacity, SigilCapacityError } from '@vielzeug/sigil'\n\n// qrCapacity returns the payload limit per version/level/mode
|
|
12
|
+
"code": "import { encodeQr, qrCapacity, SigilCapacityError } from '@vielzeug/sigil'\n\n// qrCapacity returns the payload limit per version/level/mode :\n// characters for numeric/alphanumeric, bytes for byte mode.\nfor (const [v, ec, mode] of [\n [1, 'L', 'byte'],\n [1, 'M', 'byte'],\n [10, 'M', 'byte'],\n [40, 'L', 'byte'],\n [40, 'L', 'numeric'],\n]) {\n console.log(`v${v}-${ec} ${mode}: ${qrCapacity(v, ec, mode)}`)\n}\n\n// Encoding beyond capacity throws SigilCapacityError with the numbers.\nconst payload = 'x'.repeat(qrCapacity(1, 'M', 'byte') + 1)\ntry {\n encodeQr(payload, { version: 1 })\n} catch (e) {\n if (e instanceof SigilCapacityError) {\n console.log('capacity exceeded:', e.bytes, '>', e.maxBytes, 'at v' + e.version)\n }\n}\n\n// Without a pinned version the encoder just grows to the next fitting one\nconst auto = encodeQr(payload)\nconsole.log('auto-picked version:', auto.version)",
|
|
13
13
|
"name": "Capacity Check"
|
|
14
14
|
},
|
|
15
15
|
{
|
|
16
16
|
"id": "encode-svg",
|
|
17
|
-
"code": "import { encodeQr, toSvg } from '@vielzeug/sigil'\n\n// The encoder picks the most compact mode, smallest version, and best mask.\nconst matrix = encodeQr('https://vielzeug.dev', { errorCorrection: 'M' })\nconsole.log('version:', matrix.version, '| size:', matrix.size, 'modules | mode:', matrix.mode)\n\n// toSvg returns a complete <svg> string
|
|
17
|
+
"code": "import { encodeQr, toSvg } from '@vielzeug/sigil'\n\n// The encoder picks the most compact mode, smallest version, and best mask.\nconst matrix = encodeQr('https://vielzeug.dev', { errorCorrection: 'M' })\nconsole.log('version:', matrix.version, '| size:', matrix.size, 'modules | mode:', matrix.mode)\n\n// toSvg returns a complete <svg> string: works in Node (no DOM needed).\n// dark defaults to currentColor so the code themes with CSS.\nconst svg = toSvg(matrix, { label: 'Vielzeug link' })\nconsole.log(svg.slice(0, 120) + '…')\nconsole.log('svg bytes:', svg.length)\n\n// Longer payloads bump the version automatically\nconst bigger = encodeQr('https://vielzeug.dev/sigil/examples/pair-two-devices')\nconsole.log('longer payload → version:', bigger.version, '| size:', bigger.size)",
|
|
18
18
|
"name": "Encode to SVG"
|
|
19
19
|
}
|
|
20
20
|
],
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"apiSource": "export { createCursorSource } from './cursorSource';\nexport { SourcererConfigurationError, SourcererDisposedError, SourcererError } from './errors';\nexport { createInfiniteSource } from './infiniteSource';\nexport { createLocalSource } from './localSource';\nexport { createPageSource } from './pageSource';\nexport type {\n CursorLoadContext,\n CursorPagination,\n CursorResult,\n CursorSource,\n CursorSourceConfig,\n CursorSourceState,\n InfiniteLoadContext,\n InfinitePagination,\n InfiniteSource,\n InfiniteSourceConfig,\n InfiniteSourceState,\n LocalSource,\n LocalSourceConfig,\n LocalSourceState,\n PageLoadContext,\n PagePagination,\n PageResult,\n PageSource,\n PageSourceConfig,\n PageSourceState,\n} from './types';\n",
|
|
3
3
|
"docs": {
|
|
4
|
-
"index": "---\ntitle: Sourcerer
|
|
5
|
-
"api": "---\ntitle: Sourcerer — API Reference\ndescription: Public API for @vielzeug/sourcerer.\n---\n\n[[toc]]\n\n## API Overview\n\n| Symbol | Purpose | Execution mode | Common gotcha |\n| --- | --- | --- | --- |\n| `createLocalSource()` | Filter and paginate in-memory items | Sync | `params` replacement uses `Object.is` |\n| `createPageSource()` | Load numbered pages | Async | Construction is inert |\n| `createCursorSource()` | Load server-issued cursor pages | Async | `after` and `before` cannot coexist initially |\n| `createInfiniteSource()` | Append numbered pages | Async | `loadMore()` is a no-op while loading or exhausted |\n| `SourcererError` | Base package error | Sync | Loader errors retain their original type |\n\n## Package Entry Point\n\n| Import | Purpose |\n| --- | --- |\n| `@vielzeug/sourcerer` | All factories, states, pagination types, loader contexts, and errors |\n\n## Source Factories\n\n### `createLocalSource()`\n\n```ts\nfunction createLocalSource<T>(items: readonly T[]): LocalSource<T>\nfunction createLocalSource<T, TParams = undefined>(\n items: readonly T[],\n config: LocalSourceConfig<T, TParams>,\n): LocalSource<T, TParams>\n```\n\nReturns a synchronous in-memory source.\n\n| Parameter | Type | Description |\n| --- | --- | --- |\n| `items` | `readonly T[]` | Initial collection |\n| `config.filter` | `(item: T, params: TParams) => boolean` | Optional inclusion predicate |\n| `config.pageSize` | `number` | Positive page size; defaults to `20` |\n| `config.params` | `TParams` | Initial params; required when `TParams` excludes `undefined` |\n\n**Returns:** `LocalSource<T, TParams>`.\n\n```ts\nimport { createLocalSource } from '@vielzeug/sourcerer';\n\nconst source = createLocalSource(['Ada', 'Grace'], {\n filter: (name, search: string) => name.includes(search),\n params: '',\n});\nsource.setParams('Ada');\n```\n\n| Method | Returns | Description |\n| --- | --- | --- |\n| `first()` | `void` | Move to page one |\n| `goTo(page)` | `void` | Move to a positive page, clamped to the known range |\n| `last()` | `void` | Move to the final page |\n| `next()` | `void` | Move forward when available |\n| `previous()` | `void` | Move backward when available |\n| `setItems(items)` | `void` | Replace the collection and recompute state |\n| `setPageSize(pageSize)` | `void` | Replace page size and reset to page one |\n| `setParams(params)` | `void` | Replace params and reset to page one |\n| `subscribe(listener)` | `() => void` | Register a state listener and return its unsubscribe function |\n| `dispose()` | `void` | Stop notifications and reject later mutations |\n\n---\n\n### `createPageSource()`\n\n```ts\nfunction createPageSource<T, TParams = undefined>(\n config: PageSourceConfig<T, TParams>,\n): PageSource<T, TParams>\n```\n\nReturns an inert numbered-page source.\n\n| Parameter | Type | Description |\n| --- | --- | --- |\n| `config.load` | `(context: PageLoadContext<TParams>) => Promise<PageResult<T>>` | Page loader |\n| `config.pageSize` | `number` | Positive page size; defaults to `20` |\n| `config.params` | `TParams` | Initial params; required when `TParams` excludes `undefined` |\n\n**Returns:** `PageSource<T, TParams>`.\n\n```ts\nimport { createPageSource } from '@vielzeug/sourcerer';\n\nconst source = createPageSource({\n load: async ({ page }) => ({ items: [page], totalItems: 10 }),\n});\nawait source.reload();\n```\n\n| Method | Returns | Description |\n| --- | --- | --- |\n| `first()` | `Promise<void>` | Load page one |\n| `goTo(page)` | `Promise<void>` | Load a positive page |\n| `last()` | `Promise<void>` | Load the last known page |\n| `next()` | `Promise<void>` | Load the next known page |\n| `previous()` | `Promise<void>` | Load the previous page |\n| `reload()` | `Promise<void>` | Reload the requested page and params |\n| `setPageSize(pageSize)` | `Promise<void>` | Reset to page one and load with a new size |\n| `setParams(params)` | `Promise<void>` | Reset to page one and load replacement params |\n| `subscribe(listener)` | `() => void` | Register a state listener and return its unsubscribe function |\n| `dispose()` | `void` | Abort work and stop notifications |\n\n---\n\n### `createCursorSource()`\n\n```ts\nfunction createCursorSource<T, TParams = undefined, TCursor = string>(\n config: CursorSourceConfig<T, TParams, TCursor>,\n): CursorSource<T, TParams, TCursor>\n```\n\nReturns an inert cursor source.\n\n| Parameter | Type | Description |\n| --- | --- | --- |\n| `config.after` | `TCursor` | Optional initial forward cursor |\n| `config.before` | `TCursor` | Optional initial backward cursor |\n| `config.load` | `(context: CursorLoadContext<TParams, TCursor>) => Promise<CursorResult<T, TCursor>>` | Cursor loader |\n| `config.pageSize` | `number` | Positive page size; defaults to `20` |\n| `config.params` | `TParams` | Initial params; required when `TParams` excludes `undefined` |\n\n**Returns:** `CursorSource<T, TParams, TCursor>`.\n\n```ts\nimport { createCursorSource } from '@vielzeug/sourcerer';\n\nconst source = createCursorSource({ load: async () => ({ items: ['first'], nextCursor: 'next' }) });\nawait source.reload();\nawait source.next();\n```\n\n| Method | Returns | Description |\n| --- | --- | --- |\n| `next()` | `Promise<void>` | Load `state.pagination.nextCursor` when available |\n| `previous()` | `Promise<void>` | Load `state.pagination.previousCursor` when available |\n| `reload()` | `Promise<void>` | Reload the requested cursor and params |\n| `setPageSize(pageSize)` | `Promise<void>` | Clear cursors and reload with a new size |\n| `setParams(params)` | `Promise<void>` | Clear cursors and load replacement params |\n| `subscribe(listener)` | `() => void` | Register a state listener and return its unsubscribe function |\n| `dispose()` | `void` | Abort work and stop notifications |\n\n---\n\n### `createInfiniteSource()`\n\n```ts\nfunction createInfiniteSource<T, TParams = undefined>(\n config: InfiniteSourceConfig<T, TParams>,\n): InfiniteSource<T, TParams>\n```\n\nReturns an inert source that appends numbered pages.\n\n| Parameter | Type | Description |\n| --- | --- | --- |\n| `config.load` | `(context: InfiniteLoadContext<TParams>) => Promise<PageResult<T>>` | Page loader |\n| `config.pageSize` | `number` | Positive page size; defaults to `20` |\n| `config.params` | `TParams` | Initial params; required when `TParams` excludes `undefined` |\n\n**Returns:** `InfiniteSource<T, TParams>`.\n\n```ts\nimport { createInfiniteSource } from '@vielzeug/sourcerer';\n\nconst source = createInfiniteSource({ load: async () => ({ items: ['post'], totalItems: 1 }) });\nawait source.reload();\nawait source.loadMore();\n```\n\n| Method | Returns | Description |\n| --- | --- | --- |\n| `loadMore()` | `Promise<void>` | Append the next page when available |\n| `reload()` | `Promise<void>` | Replace items from page one |\n| `setPageSize(pageSize)` | `Promise<void>` | Replace items from page one with a new size |\n| `setParams(params)` | `Promise<void>` | Replace items from page one with new params |\n| `subscribe(listener)` | `() => void` | Register a state listener and return its unsubscribe function |\n| `dispose()` | `void` | Abort work and stop notifications |\n\n## Types\n\n### Page source types\n\n```ts\ntype PagePagination = Readonly<{\n hasNext: boolean;\n hasPrevious: boolean;\n page: number;\n pageCount: number;\n pageSize: number;\n totalItems: number;\n}>;\n\ntype PageSourceState<T, TParams = undefined> = Readonly<{\n error: Error | null;\n items: readonly T[];\n loading: boolean;\n pagination: PagePagination;\n params: TParams;\n pendingParams?: TParams;\n}>;\n\ntype PageLoadContext<TParams = undefined> = Readonly<{\n page: number;\n pageSize: number;\n params: TParams;\n signal: AbortSignal;\n}>;\n\ntype PageResult<T> = Readonly<{\n items: readonly T[];\n totalItems: number;\n}>;\n\ntype PageSourceConfig<T, TParams = undefined> = Readonly<{\n load(context: PageLoadContext<TParams>): Promise<PageResult<T>>;\n pageSize?: number;\n } & (undefined extends TParams ? { params?: TParams } : { params: TParams })\n>;\n\ntype PageSource<T, TParams = undefined> = {\n [Symbol.dispose](): void;\n readonly disposalSignal: AbortSignal;\n dispose(): void;\n readonly disposed: boolean;\n first(): Promise<void>;\n goTo(page: number): Promise<void>;\n last(): Promise<void>;\n next(): Promise<void>;\n previous(): Promise<void>;\n reload(): Promise<void>;\n setPageSize(pageSize: number): Promise<void>;\n setParams(params: TParams): Promise<void>;\n readonly state: PageSourceState<T, TParams>;\n subscribe(listener: (state: PageSourceState<T, TParams>) => void): () => void;\n};\n```\n\nThe conditional config field makes `params` optional only when `TParams` includes `undefined`.\n\n### Local source types\n\n```ts\ntype LocalSourceState<T, TParams = undefined> = Readonly<{\n error: null;\n items: readonly T[];\n loading: false;\n pagination: PagePagination;\n params: TParams;\n}>;\n\ntype LocalSourceConfig<T, TParams = undefined> = Readonly<{\n filter?: (item: T, params: TParams) => boolean;\n pageSize?: number;\n } & (undefined extends TParams ? { params?: TParams } : { params: TParams })\n>;\n\ntype LocalSource<T, TParams = undefined> = {\n [Symbol.dispose](): void;\n readonly disposalSignal: AbortSignal;\n dispose(): void;\n readonly disposed: boolean;\n first(): void;\n goTo(page: number): void;\n last(): void;\n next(): void;\n previous(): void;\n setItems(items: readonly T[]): void;\n setPageSize(pageSize: number): void;\n setParams(params: TParams): void;\n readonly state: LocalSourceState<T, TParams>;\n subscribe(listener: (state: LocalSourceState<T, TParams>) => void): () => void;\n};\n```\n\n### Cursor source types\n\n```ts\ntype CursorPagination<TCursor = string> = Readonly<{\n nextCursor?: TCursor;\n pageSize: number;\n previousCursor?: TCursor;\n totalItems?: number;\n}>;\n\ntype CursorLoadContext<TParams = undefined, TCursor = string> = Readonly<{\n after?: TCursor;\n before?: TCursor;\n pageSize: number;\n params: TParams;\n signal: AbortSignal;\n}>;\n\ntype CursorResult<T, TCursor = string> = Readonly<{\n items: readonly T[];\n nextCursor?: TCursor;\n previousCursor?: TCursor;\n totalItems?: number;\n}>;\n\ntype CursorSourceState<T, TParams = undefined, TCursor = string> = Readonly<{\n error: Error | null;\n items: readonly T[];\n loading: boolean;\n pagination: CursorPagination<TCursor>;\n params: TParams;\n pendingParams?: TParams;\n}>;\n\ntype CursorSourceConfig<T, TParams = undefined, TCursor = string> = Readonly<{\n after?: TCursor;\n before?: TCursor;\n load(context: CursorLoadContext<TParams, TCursor>): Promise<CursorResult<T, TCursor>>;\n pageSize?: number;\n } & (undefined extends TParams ? { params?: TParams } : { params: TParams })\n>;\n\ntype CursorSource<T, TParams = undefined, TCursor = string> = {\n [Symbol.dispose](): void;\n readonly disposalSignal: AbortSignal;\n dispose(): void;\n readonly disposed: boolean;\n next(): Promise<void>;\n previous(): Promise<void>;\n reload(): Promise<void>;\n setPageSize(pageSize: number): Promise<void>;\n setParams(params: TParams): Promise<void>;\n readonly state: CursorSourceState<T, TParams, TCursor>;\n subscribe(listener: (state: CursorSourceState<T, TParams, TCursor>) => void): () => void;\n};\n```\n\n### Infinite source types\n\n```ts\ntype InfinitePagination = Readonly<{\n hasMore: boolean;\n loadedItems: number;\n pageSize: number;\n totalItems: number;\n}>;\n\ntype InfiniteLoadContext<TParams = undefined> = Readonly<{\n page: number;\n pageSize: number;\n params: TParams;\n signal: AbortSignal;\n}>;\n\ntype InfiniteSourceState<T, TParams = undefined> = Readonly<{\n error: Error | null;\n items: readonly T[];\n loading: boolean;\n pagination: InfinitePagination;\n params: TParams;\n pendingParams?: TParams;\n}>;\n\ntype InfiniteSourceConfig<T, TParams = undefined> = Readonly<{\n load(context: InfiniteLoadContext<TParams>): Promise<PageResult<T>>;\n pageSize?: number;\n } & (undefined extends TParams ? { params?: TParams } : { params: TParams })\n>;\n\ntype InfiniteSource<T, TParams = undefined> = {\n [Symbol.dispose](): void;\n readonly disposalSignal: AbortSignal;\n dispose(): void;\n readonly disposed: boolean;\n loadMore(): Promise<void>;\n reload(): Promise<void>;\n setPageSize(pageSize: number): Promise<void>;\n setParams(params: TParams): Promise<void>;\n readonly state: InfiniteSourceState<T, TParams>;\n subscribe(listener: (state: InfiniteSourceState<T, TParams>) => void): () => void;\n};\n```\n\n## Errors\n\n| Error | Trigger | Notable properties |\n| --- | --- | --- |\n| `SourcererError` | Base class for package-originated configuration and lifecycle errors | `instanceof SourcererError` narrows the hierarchy |\n| `SourcererConfigurationError` | Invalid page, page size, total, or initial cursor direction | Extends `SourcererError` |\n| `SourcererDisposedError` | A source command runs after disposal | Extends `SourcererError` |\n\nLoader failures are preserved when they are `Error` instances. Non-Error rejections become `Error` objects whose `cause` contains the original value.\n",
|
|
6
|
-
"usage": "---\ntitle: Sourcerer
|
|
7
|
-
"examples": "---\ntitle: Sourcerer
|
|
4
|
+
"index": "---\ntitle: 'Sourcerer: Reactive collection sources'\ndescription: Framework-agnostic local and remote collection state with page, cursor, and infinite pagination.\npackage: sourcerer\ncategory: data\nkeywords: [pagination, data-source, cursor, infinite-scroll, collection]\nrelated: [courier, ripple, scout, wayfinder]\nexports:\n [\n createCursorSource,\n createInfiniteSource,\n createLocalSource,\n createPageSource,\n CursorLoadContext,\n CursorPagination,\n CursorResult,\n CursorSource,\n CursorSourceConfig,\n CursorSourceState,\n InfiniteLoadContext,\n InfinitePagination,\n InfiniteSource,\n InfiniteSourceConfig,\n InfiniteSourceState,\n LocalSource,\n LocalSourceConfig,\n LocalSourceState,\n PageLoadContext,\n PagePagination,\n PageResult,\n PageSource,\n PageSourceConfig,\n PageSourceState,\n SourcererError,\n SourcererConfigurationError,\n SourcererDisposedError,\n ]\nenvironments: [browser, node, ssr, deno]\n---\n\n<!-- markdownlint-disable MD025 MD033 MD060 -->\n\n<PackageHero package=\"sourcerer\" />\n\n## Why Sourcerer?\n\nPaginated views need collection data, loading state, errors, parameters, and navigation to change coherently. Sourcerer owns that state and request succession without coupling it to a framework or transport client.\n\n```ts\n// Before\nlet items = [];\nlet loading = false;\nlet page = 1;\n\n// After\nconst source = createPageSource({\n load: async ({ page, pageSize }) => ({ items: await loadUsers(page, pageSize), totalItems: 100 }),\n});\nsource.subscribe((state) => render(state));\n```\n\n| Feature | Sourcerer | Manual store | General query cache |\n| --- | --- | --- | --- |\n| Bundle size | <PackageInfo package=\"sourcerer\" type=\"size\" /> | Application-defined | Package-defined |\n| Zero runtime dependencies | <ore-icon name=\"x\" size=\"16\"></ore-icon> | <ore-icon name=\"check\" size=\"16\"></ore-icon> | <ore-icon name=\"triangle-alert\" size=\"16\"></ore-icon> |\n| Page, cursor, and infinite navigation | <ore-icon name=\"check\" size=\"16\"></ore-icon> | Application-defined | Application-defined |\n| Framework-neutral subscriptions | <ore-icon name=\"check\" size=\"16\"></ore-icon> | Application-defined | <ore-icon name=\"triangle-alert\" size=\"16\"></ore-icon> |\n| HTTP transport policy | Loader-defined | Application-defined | Loader-defined |\n\n<div class=\"decision-callout\">\n\n**Use Sourcerer when** a collection view needs explicit pagination, observable state, cancellation, and lifecycle ownership.\n\n**Consider a general query cache when** your primary requirement is shared keyed caching, invalidation, mutations, or server-state normalization rather than collection navigation.\n\n</div>\n\n## Installation\n\n::: code-group\n\n```sh [pnpm]\npnpm add @vielzeug/sourcerer\n```\n\n```sh [npm]\nnpm install @vielzeug/sourcerer\n```\n\n```sh [yarn]\nyarn add @vielzeug/sourcerer\n```\n\n:::\n\n## Quick Start\n\nCreate a page source, load it, read its state, and dispose it with its owner.\n\n```ts\nimport { createPageSource } from '@vielzeug/sourcerer';\n\ntype User = { id: number; name: string };\nconst users: User[] = [\n { id: 1, name: 'Ada' },\n { id: 2, name: 'Grace' },\n];\nconst source = createPageSource({\n load: async ({ page, pageSize }) => {\n const start = (page - 1) * pageSize;\n return { items: users.slice(start, start + pageSize), totalItems: users.length };\n },\n pageSize: 1,\n});\n\ntry {\n await source.reload();\n console.log(source.state.items);\n} catch (error) {\n console.error(error);\n} finally {\n source.dispose();\n}\n```\n\n## Features\n\n<div class=\"features-grid\">\n\n- `createLocalSource()`: filters and paginates an in-memory collection synchronously\n- `createPageSource()`: loads numbered pages and exposes direct navigation commands\n- `createCursorSource()`: follows opaque cursors returned by the loader\n- `createInfiniteSource()`: appends pages while preserving loaded items\n- `setParams()`: replaces consumer-owned loader parameters and resets pagination\n- `subscribe()`: publishes complete immutable state replacements\n\n</div>\n\n## Documentation\n\n<div class=\"doc-links\">\n\n- [Usage Guide](./usage.md)\n- [API Reference](./api.md)\n- [Examples](./examples.md)\n- [Migration Guide](./migration.md)\n\n</div>\n\n## See Also\n\n<div class=\"see-also\">\n\n- [Courier](/courier/): provide HTTP transport, middleware, and structured transport errors inside loaders\n- [Scout](/scout/): index larger in-memory collections before passing matches to a local source\n- [Ripple](/ripple/): project source state into reactive computations\n- [Wayfinder](/wayfinder/): validate and synchronize source parameters with route state\n\n</div>\n\n<!-- markdownlint-enable MD025 MD033 MD060 -->\n",
|
|
5
|
+
"api": "---\ntitle: 'Sourcerer: API Reference'\ndescription: Public API for @vielzeug/sourcerer.\n---\n\n[[toc]]\n\n## API Overview\n\n| Symbol | Purpose | Execution mode | Common gotcha |\n| --- | --- | --- | --- |\n| `createLocalSource()` | Filter and paginate in-memory items | Sync | `params` replacement uses `Object.is` |\n| `createPageSource()` | Load numbered pages | Async | Construction is inert |\n| `createCursorSource()` | Load server-issued cursor pages | Async | `after` and `before` cannot coexist initially |\n| `createInfiniteSource()` | Append numbered pages | Async | `loadMore()` is a no-op while loading or exhausted |\n| `SourcererError` | Base package error | Sync | Loader errors retain their original type |\n\n## Package Entry Point\n\n| Import | Purpose |\n| --- | --- |\n| `@vielzeug/sourcerer` | All factories, states, pagination types, loader contexts, and errors |\n\n## Source Factories\n\n### `createLocalSource()`\n\n```ts\nfunction createLocalSource<T>(items: readonly T[]): LocalSource<T>\nfunction createLocalSource<T, TParams = undefined>(\n items: readonly T[],\n config: LocalSourceConfig<T, TParams>,\n): LocalSource<T, TParams>\n```\n\nReturns a synchronous in-memory source.\n\n| Parameter | Type | Description |\n| --- | --- | --- |\n| `items` | `readonly T[]` | Initial collection |\n| `config.filter` | `(item: T, params: TParams) => boolean` | Optional inclusion predicate |\n| `config.pageSize` | `number` | Positive page size; defaults to `20` |\n| `config.params` | `TParams` | Initial params; required when `TParams` excludes `undefined` |\n\n**Returns:** `LocalSource<T, TParams>`.\n\n```ts\nimport { createLocalSource } from '@vielzeug/sourcerer';\n\nconst source = createLocalSource(['Ada', 'Grace'], {\n filter: (name, search: string) => name.includes(search),\n params: '',\n});\nsource.setParams('Ada');\n```\n\n| Method | Returns | Description |\n| --- | --- | --- |\n| `first()` | `void` | Move to page one |\n| `goTo(page)` | `void` | Move to a positive page, clamped to the known range |\n| `last()` | `void` | Move to the final page |\n| `next()` | `void` | Move forward when available |\n| `previous()` | `void` | Move backward when available |\n| `setItems(items)` | `void` | Replace the collection and recompute state |\n| `setPageSize(pageSize)` | `void` | Replace page size and reset to page one |\n| `setParams(params)` | `void` | Replace params and reset to page one |\n| `subscribe(listener)` | `() => void` | Register a state listener and return its unsubscribe function |\n| `dispose()` | `void` | Stop notifications and reject later mutations |\n\n---\n\n### `createPageSource()`\n\n```ts\nfunction createPageSource<T, TParams = undefined>(\n config: PageSourceConfig<T, TParams>,\n): PageSource<T, TParams>\n```\n\nReturns an inert numbered-page source.\n\n| Parameter | Type | Description |\n| --- | --- | --- |\n| `config.load` | `(context: PageLoadContext<TParams>) => Promise<PageResult<T>>` | Page loader |\n| `config.pageSize` | `number` | Positive page size; defaults to `20` |\n| `config.params` | `TParams` | Initial params; required when `TParams` excludes `undefined` |\n\n**Returns:** `PageSource<T, TParams>`.\n\n```ts\nimport { createPageSource } from '@vielzeug/sourcerer';\n\nconst source = createPageSource({\n load: async ({ page }) => ({ items: [page], totalItems: 10 }),\n});\nawait source.reload();\n```\n\n| Method | Returns | Description |\n| --- | --- | --- |\n| `first()` | `Promise<void>` | Load page one |\n| `goTo(page)` | `Promise<void>` | Load a positive page |\n| `last()` | `Promise<void>` | Load the last known page |\n| `next()` | `Promise<void>` | Load the next known page |\n| `previous()` | `Promise<void>` | Load the previous page |\n| `reload()` | `Promise<void>` | Reload the requested page and params |\n| `setPageSize(pageSize)` | `Promise<void>` | Reset to page one and load with a new size |\n| `setParams(params)` | `Promise<void>` | Reset to page one and load replacement params |\n| `subscribe(listener)` | `() => void` | Register a state listener and return its unsubscribe function |\n| `dispose()` | `void` | Abort work and stop notifications |\n\n---\n\n### `createCursorSource()`\n\n```ts\nfunction createCursorSource<T, TParams = undefined, TCursor = string>(\n config: CursorSourceConfig<T, TParams, TCursor>,\n): CursorSource<T, TParams, TCursor>\n```\n\nReturns an inert cursor source.\n\n| Parameter | Type | Description |\n| --- | --- | --- |\n| `config.after` | `TCursor` | Optional initial forward cursor |\n| `config.before` | `TCursor` | Optional initial backward cursor |\n| `config.load` | `(context: CursorLoadContext<TParams, TCursor>) => Promise<CursorResult<T, TCursor>>` | Cursor loader |\n| `config.pageSize` | `number` | Positive page size; defaults to `20` |\n| `config.params` | `TParams` | Initial params; required when `TParams` excludes `undefined` |\n\n**Returns:** `CursorSource<T, TParams, TCursor>`.\n\n```ts\nimport { createCursorSource } from '@vielzeug/sourcerer';\n\nconst source = createCursorSource({ load: async () => ({ items: ['first'], nextCursor: 'next' }) });\nawait source.reload();\nawait source.next();\n```\n\n| Method | Returns | Description |\n| --- | --- | --- |\n| `next()` | `Promise<void>` | Load `state.pagination.nextCursor` when available |\n| `previous()` | `Promise<void>` | Load `state.pagination.previousCursor` when available |\n| `reload()` | `Promise<void>` | Reload the requested cursor and params |\n| `setPageSize(pageSize)` | `Promise<void>` | Clear cursors and reload with a new size |\n| `setParams(params)` | `Promise<void>` | Clear cursors and load replacement params |\n| `subscribe(listener)` | `() => void` | Register a state listener and return its unsubscribe function |\n| `dispose()` | `void` | Abort work and stop notifications |\n\n---\n\n### `createInfiniteSource()`\n\n```ts\nfunction createInfiniteSource<T, TParams = undefined>(\n config: InfiniteSourceConfig<T, TParams>,\n): InfiniteSource<T, TParams>\n```\n\nReturns an inert source that appends numbered pages.\n\n| Parameter | Type | Description |\n| --- | --- | --- |\n| `config.load` | `(context: InfiniteLoadContext<TParams>) => Promise<PageResult<T>>` | Page loader |\n| `config.pageSize` | `number` | Positive page size; defaults to `20` |\n| `config.params` | `TParams` | Initial params; required when `TParams` excludes `undefined` |\n\n**Returns:** `InfiniteSource<T, TParams>`.\n\n```ts\nimport { createInfiniteSource } from '@vielzeug/sourcerer';\n\nconst source = createInfiniteSource({ load: async () => ({ items: ['post'], totalItems: 1 }) });\nawait source.reload();\nawait source.loadMore();\n```\n\n| Method | Returns | Description |\n| --- | --- | --- |\n| `loadMore()` | `Promise<void>` | Append the next page when available |\n| `reload()` | `Promise<void>` | Replace items from page one |\n| `setPageSize(pageSize)` | `Promise<void>` | Replace items from page one with a new size |\n| `setParams(params)` | `Promise<void>` | Replace items from page one with new params |\n| `subscribe(listener)` | `() => void` | Register a state listener and return its unsubscribe function |\n| `dispose()` | `void` | Abort work and stop notifications |\n\n## Types\n\n### Page source types\n\n```ts\ntype PagePagination = Readonly<{\n hasNext: boolean;\n hasPrevious: boolean;\n page: number;\n pageCount: number;\n pageSize: number;\n totalItems: number;\n}>;\n\ntype PageSourceState<T, TParams = undefined> = Readonly<{\n error: Error | null;\n items: readonly T[];\n loading: boolean;\n pagination: PagePagination;\n params: TParams;\n pendingParams?: TParams;\n}>;\n\ntype PageLoadContext<TParams = undefined> = Readonly<{\n page: number;\n pageSize: number;\n params: TParams;\n signal: AbortSignal;\n}>;\n\ntype PageResult<T> = Readonly<{\n items: readonly T[];\n totalItems: number;\n}>;\n\ntype PageSourceConfig<T, TParams = undefined> = Readonly<{\n load(context: PageLoadContext<TParams>): Promise<PageResult<T>>;\n pageSize?: number;\n } & (undefined extends TParams ? { params?: TParams } : { params: TParams })\n>;\n\ntype PageSource<T, TParams = undefined> = {\n [Symbol.dispose](): void;\n readonly disposalSignal: AbortSignal;\n dispose(): void;\n readonly disposed: boolean;\n first(): Promise<void>;\n goTo(page: number): Promise<void>;\n last(): Promise<void>;\n next(): Promise<void>;\n previous(): Promise<void>;\n reload(): Promise<void>;\n setPageSize(pageSize: number): Promise<void>;\n setParams(params: TParams): Promise<void>;\n readonly state: PageSourceState<T, TParams>;\n subscribe(listener: (state: PageSourceState<T, TParams>) => void): () => void;\n};\n```\n\nThe conditional config field makes `params` optional only when `TParams` includes `undefined`.\n\n### Local source types\n\n```ts\ntype LocalSourceState<T, TParams = undefined> = Readonly<{\n error: null;\n items: readonly T[];\n loading: false;\n pagination: PagePagination;\n params: TParams;\n}>;\n\ntype LocalSourceConfig<T, TParams = undefined> = Readonly<{\n filter?: (item: T, params: TParams) => boolean;\n pageSize?: number;\n } & (undefined extends TParams ? { params?: TParams } : { params: TParams })\n>;\n\ntype LocalSource<T, TParams = undefined> = {\n [Symbol.dispose](): void;\n readonly disposalSignal: AbortSignal;\n dispose(): void;\n readonly disposed: boolean;\n first(): void;\n goTo(page: number): void;\n last(): void;\n next(): void;\n previous(): void;\n setItems(items: readonly T[]): void;\n setPageSize(pageSize: number): void;\n setParams(params: TParams): void;\n readonly state: LocalSourceState<T, TParams>;\n subscribe(listener: (state: LocalSourceState<T, TParams>) => void): () => void;\n};\n```\n\n### Cursor source types\n\n```ts\ntype CursorPagination<TCursor = string> = Readonly<{\n nextCursor?: TCursor;\n pageSize: number;\n previousCursor?: TCursor;\n totalItems?: number;\n}>;\n\ntype CursorLoadContext<TParams = undefined, TCursor = string> = Readonly<{\n after?: TCursor;\n before?: TCursor;\n pageSize: number;\n params: TParams;\n signal: AbortSignal;\n}>;\n\ntype CursorResult<T, TCursor = string> = Readonly<{\n items: readonly T[];\n nextCursor?: TCursor;\n previousCursor?: TCursor;\n totalItems?: number;\n}>;\n\ntype CursorSourceState<T, TParams = undefined, TCursor = string> = Readonly<{\n error: Error | null;\n items: readonly T[];\n loading: boolean;\n pagination: CursorPagination<TCursor>;\n params: TParams;\n pendingParams?: TParams;\n}>;\n\ntype CursorSourceConfig<T, TParams = undefined, TCursor = string> = Readonly<{\n after?: TCursor;\n before?: TCursor;\n load(context: CursorLoadContext<TParams, TCursor>): Promise<CursorResult<T, TCursor>>;\n pageSize?: number;\n } & (undefined extends TParams ? { params?: TParams } : { params: TParams })\n>;\n\ntype CursorSource<T, TParams = undefined, TCursor = string> = {\n [Symbol.dispose](): void;\n readonly disposalSignal: AbortSignal;\n dispose(): void;\n readonly disposed: boolean;\n next(): Promise<void>;\n previous(): Promise<void>;\n reload(): Promise<void>;\n setPageSize(pageSize: number): Promise<void>;\n setParams(params: TParams): Promise<void>;\n readonly state: CursorSourceState<T, TParams, TCursor>;\n subscribe(listener: (state: CursorSourceState<T, TParams, TCursor>) => void): () => void;\n};\n```\n\n### Infinite source types\n\n```ts\ntype InfinitePagination = Readonly<{\n hasMore: boolean;\n loadedItems: number;\n pageSize: number;\n totalItems: number;\n}>;\n\ntype InfiniteLoadContext<TParams = undefined> = Readonly<{\n page: number;\n pageSize: number;\n params: TParams;\n signal: AbortSignal;\n}>;\n\ntype InfiniteSourceState<T, TParams = undefined> = Readonly<{\n error: Error | null;\n items: readonly T[];\n loading: boolean;\n pagination: InfinitePagination;\n params: TParams;\n pendingParams?: TParams;\n}>;\n\ntype InfiniteSourceConfig<T, TParams = undefined> = Readonly<{\n load(context: InfiniteLoadContext<TParams>): Promise<PageResult<T>>;\n pageSize?: number;\n } & (undefined extends TParams ? { params?: TParams } : { params: TParams })\n>;\n\ntype InfiniteSource<T, TParams = undefined> = {\n [Symbol.dispose](): void;\n readonly disposalSignal: AbortSignal;\n dispose(): void;\n readonly disposed: boolean;\n loadMore(): Promise<void>;\n reload(): Promise<void>;\n setPageSize(pageSize: number): Promise<void>;\n setParams(params: TParams): Promise<void>;\n readonly state: InfiniteSourceState<T, TParams>;\n subscribe(listener: (state: InfiniteSourceState<T, TParams>) => void): () => void;\n};\n```\n\n## Errors\n\n| Error | Trigger | Notable properties |\n| --- | --- | --- |\n| `SourcererError` | Base class for package-originated configuration and lifecycle errors | `instanceof SourcererError` narrows the hierarchy |\n| `SourcererConfigurationError` | Invalid page, page size, total, or initial cursor direction | Extends `SourcererError` |\n| `SourcererDisposedError` | A source command runs after disposal | Extends `SourcererError` |\n\nLoader failures are preserved when they are `Error` instances. Non-Error rejections become `Error` objects whose `cause` contains the original value.\n",
|
|
6
|
+
"usage": "---\ntitle: 'Sourcerer: Usage Guide'\ndescription: Build local, page, cursor, and infinite collection sources.\n---\n\n[[toc]]\n\n## Basic Usage\n\nCreate a page source and call `reload()` to start the first request. Construction performs no I/O.\n\n```ts\nimport { createPageSource } from '@vielzeug/sourcerer';\n\nconst names = ['Ada', 'Grace', 'Linus'];\nconst source = createPageSource({\n load: async ({ page, pageSize }) => {\n const start = (page - 1) * pageSize;\n return { items: names.slice(start, start + pageSize), totalItems: names.length };\n },\n pageSize: 2,\n});\n\nawait source.reload();\nconsole.log(source.state.items);\nsource.dispose();\n```\n\nRead one `state` object per render. It contains `items`, `loading`, `error`, `params`, and pagination metadata from one committed result. Commands return `Promise<void>`; read `source.state` after awaiting them.\n\n## Pass Loader Parameters\n\nUse `params` for application-owned filters, search terms, sort state, or request options. `setParams()` replaces the entire value and resets pagination.\n\n```ts\nimport { createPageSource } from '@vielzeug/sourcerer';\n\ntype UserParams = { role: 'admin' | 'user'; search: string };\nconst users = ['Ada', 'Grace', 'Linus'];\nconst source = createPageSource<string, UserParams>({\n load: async ({ page, pageSize, params }) => {\n const matching = users.filter((name) => name.toLowerCase().includes(params.search.toLowerCase()));\n const start = (page - 1) * pageSize;\n return { items: matching.slice(start, start + pageSize), totalItems: matching.length };\n },\n params: { role: 'admin', search: '' },\n});\n\nawait source.setParams({ role: 'admin', search: 'ada' });\nconsole.log(source.state.params);\nsource.dispose();\n```\n\nA non-optional parameter type requires an initial `params` value. Sourcerer compares parameter values with `Object.is`; reuse a value when setting it again should be a no-op.\n\nWhile replacement parameters load, `state.params` continues to describe the committed items and `state.pendingParams` contains the requested value. Page-only navigation does not set `pendingParams`.\n\n## Navigate Numbered Pages\n\nUse direct commands for numbered-page navigation. `setPageSize()` resets to page one.\n\n```ts\nawait source.reload();\nawait source.next();\nawait source.goTo(4);\nawait source.previous();\nawait source.first();\nawait source.last();\nawait source.setPageSize(50);\n```\n\nAfter the first successful load, navigation is clamped to the known `pageCount`. Before that load, `goTo()` accepts any positive page because the total is unknown.\n\n## Filter Local Collections\n\nUse a local source when data is already in memory. Its commands are synchronous.\n\n```ts\nimport { createLocalSource } from '@vielzeug/sourcerer';\n\nconst source = createLocalSource(['Ada', 'Grace', 'Linus'], {\n filter: (name, search: string) => name.toLowerCase().includes(search.toLowerCase()),\n pageSize: 2,\n params: '',\n});\n\nsource.setParams('a');\nsource.next();\nsource.setItems(['Ada', 'Grace']);\nconsole.log(source.state.items);\nsource.dispose();\n```\n\nUse ordinary arrays for one-off transformations. A local source is useful when pagination, subscriptions, and lifecycle ownership are also required.\n\n## Use Cursor Pagination\n\nReturn opaque cursors from the loader. `next()` and `previous()` use only cursors from the committed state.\n\n```ts\nimport { createCursorSource } from '@vielzeug/sourcerer';\n\nconst rows = ['A', 'B', 'C', 'D'];\nconst source = createCursorSource({\n load: async ({ after, pageSize }) => {\n const start = after ? Number(after) : 0;\n const items = rows.slice(start, start + pageSize);\n const next = start + items.length;\n return {\n items,\n nextCursor: next < rows.length ? String(next) : undefined,\n previousCursor: start > 0 ? String(Math.max(0, start - pageSize)) : undefined,\n };\n },\n pageSize: 2,\n});\n\nawait source.reload();\nawait source.next();\nconsole.log(source.state.items);\nsource.dispose();\n```\n\nChanging params or page size clears current cursor direction. A source cannot start with both `after` and `before`.\n\n## Build an Infinite Feed\n\nUse an infinite source when later pages should append to committed items.\n\n```ts\nimport { createInfiniteSource } from '@vielzeug/sourcerer';\n\nconst values = [1, 2, 3, 4, 5];\nconst source = createInfiniteSource({\n load: async ({ page, pageSize }) => {\n const start = (page - 1) * pageSize;\n return { items: values.slice(start, start + pageSize), totalItems: values.length };\n },\n pageSize: 2,\n});\n\nawait source.reload();\nawait source.loadMore();\nconsole.log(source.state.items);\nsource.dispose();\n```\n\n`loadMore()` is a no-op while another request is active or after `pagination.hasMore` becomes false. `setParams()` and `setPageSize()` replace the feed from page one.\n\n## Handle Requests and Errors\n\nA new remote command aborts older work. Superseded or disposed work settles without committing. A current loader failure preserves committed items, writes the error to state, and rejects the command.\n\n```ts\ntry {\n await source.reload();\n} catch (error) {\n console.error(error);\n console.log(source.state.error);\n}\n```\n\nLoader result arrays are copied before publication. Commands after disposal throw or reject `SourcererDisposedError`.\n\n## Testing\n\nInject deterministic loaders and await commands before asserting final state.\n\n```ts\nimport { expect, it } from 'vitest';\nimport { createPageSource } from '@vielzeug/sourcerer';\n\nit('loads a page', async () => {\n const source = createPageSource({ load: async () => ({ items: ['Ada'], totalItems: 1 }) });\n\n await source.reload();\n\n expect(source.state.items).toEqual(['Ada']);\n source.dispose();\n});\n```\n\n## Framework Integration\n\nCreate one source per component lifetime and bridge `subscribe()` to the framework lifecycle.\n\n::: code-group\n\n```tsx [React]\nimport { createPageSource } from '@vielzeug/sourcerer';\nimport { useEffect, useMemo, useSyncExternalStore } from 'react';\n\nexport function Users() {\n const source = useMemo(\n () => createPageSource({ load: async () => ({ items: [{ id: 1, name: 'Ada' }], totalItems: 1 }) }),\n [],\n );\n const state = useSyncExternalStore(source.subscribe, () => source.state);\n\n useEffect(() => {\n void source.reload().catch(() => undefined);\n return () => source.dispose();\n }, [source]);\n\n return <p>{state.loading ? 'Loading' : state.items.length}</p>;\n}\n```\n\n```ts [Vue 3]\nimport { onUnmounted, shallowRef } from 'vue';\nimport { createPageSource } from '@vielzeug/sourcerer';\n\nconst source = createPageSource({ load: async () => ({ items: [{ id: 1, name: 'Ada' }], totalItems: 1 }) });\nconst state = shallowRef(source.state);\nconst stop = source.subscribe((next) => (state.value = next));\nvoid source.reload().catch(() => undefined);\n\nonUnmounted(() => {\n stop();\n source.dispose();\n});\n```\n\n```svelte [Svelte]\n<script lang=\"ts\">\n import { onDestroy } from 'svelte';\n import { createPageSource } from '@vielzeug/sourcerer';\n\n const source = createPageSource({ load: async () => ({ items: [{ id: 1, name: 'Ada' }], totalItems: 1 }) });\n let state = source.state;\n const stop = source.subscribe((next) => (state = next));\n void source.reload().catch(() => undefined);\n\n onDestroy(() => {\n stop();\n source.dispose();\n });\n</script>\n\n{#if state.loading}Loading{/if}\n{#each state.items as user}{user.name}{/each}\n```\n\n:::\n\n## Working with Other Vielzeug Libraries\n\nUse Courier for transport policy and pass the source-owned cancellation signal through.\n\n```ts\nimport { createCourier } from '@vielzeug/courier';\nimport { createPageSource } from '@vielzeug/sourcerer';\n\ntype User = { id: number; name: string };\ntype Response = { data: User[]; total: number };\nconst courier = createCourier({ baseUrl: '/api' });\nconst source = createPageSource({\n load: async ({ page, pageSize, params: search, signal }) => {\n const result = await courier.get<Response>('/users', { query: { page, pageSize, search }, signal });\n return { items: result.data, totalItems: result.total };\n },\n params: '',\n});\n```\n\nUse Scout to compute indexed local matches, Ripple to project state into reactive computations, and Wayfinder to validate and synchronize params with the URL.\n\n## Best Practices\n\n- Dispose each source with its owning view, request, or service scope.\n- Read one `state` object per render.\n- Await direct commands or handle rejected current-request failures.\n- Keep transport, caching, and persistence policy outside Sourcerer.\n- Treat `params` as an immutable value and replace it atomically.\n- Debounce text input before calling `setParams()` on a remote source.\n- Validate route and form values before constructing params or page numbers.\n- Keep rendering committed items while `loading` is true.\n",
|
|
7
|
+
"examples": "---\ntitle: 'Sourcerer: Examples'\ndescription: Recipes for local, page, cursor, infinite, and framework source usage.\n---\n\n## Examples\n\n- [Local Pagination and Filtering](./examples/local-pagination-and-filtering.md)\n- [Page Params with URL State](./examples/remote-search-with-url-state.md)\n- [Cursor-Based Pagination](./examples/cursor-based-pagination.md)\n- [Infinite Scroll](./examples/infinite-scroll.md)\n- [Framework Integration](./examples/framework-integration.md)\n- [Remote Data with Courier](./examples/sourcerer-with-courier.md)\n- [Reactive Controls with Ripple](./examples/sourcerer-with-ripple.md)\n- [URL-Synced List with Wayfinder](./examples/sourcerer-with-wayfinder.md)\n"
|
|
8
8
|
},
|
|
9
9
|
"examples": [
|
|
10
10
|
{
|