@vielzeug/codex 2.2.8 → 2.2.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/data/catalog.json +171 -22
- package/data/llms-full.txt +14178 -11054
- package/data/llms.txt +6 -2
- package/data/manifest.json +1 -1
- package/data/packages/courier.json +2 -2
- package/data/packages/dnd.json +2 -2
- package/data/packages/focus.json +37 -0
- package/data/packages/forge.json +9 -10
- package/data/packages/gesture.json +25 -0
- package/data/packages/illusionist.json +132 -0
- package/data/packages/lingua.json +4 -3
- package/data/packages/ore.json +4 -9
- package/data/packages/ripple.json +1 -1
- package/data/packages/sentinel.json +35 -0
- package/data/packages/sourcerer.json +30 -29
- package/data/refine.json +3921 -3960
- package/data/search.json +148 -24
- package/package.json +1 -1
package/data/search.json
CHANGED
|
@@ -314,8 +314,8 @@
|
|
|
314
314
|
"description": "a framework neutral fetch client with explicit cache keys, direct mutations, and abortable streams.",
|
|
315
315
|
"docs": {
|
|
316
316
|
"index": " \ntitle: courier — http, queries, and streaming\ndescription: a framework neutral fetch client with explicit cache keys, direct mutations, and abortable streams.\npackage: courier\ncategory: http\nkeywords: [http client, fetch, caching, queries, mutations, sse, streaming, interceptors]\nrelated: [flux, ripple, spell]\nexports:\n [\n createcourier,\n couriererror,\n courierhttperror,\n couriernetworkerror,\n couriertimeouterror,\n courieraborterror,\n courierschemavalidationerror,\n withbearerauth,\n withrequestid,\n withlogging,\n ]\nenvironments: [browser, node, ssr, deno]\n \n\n<! markdownlint disable md025 md033 md060 >\n\n<packagehero package=\"courier\" />\n\n## why courier?\n\nnative `fetch` leaves request policy, cached reads, and stream lifecycles to each application. courier keeps\nthose concerns in one client while making cache identity and fetch policy explicit at every cached read.\n\n```ts\n// before\nconst response = await fetch(`/api/users/${userid}`);\nif (!response.ok) throw new error(`http ${response.status}`);\nconst user = await response.json();\n\n// after\nawait courier.queries.fetch({\n key: ['users', userid],\n fetch: ({ signal }) => courier.get('/users/{id}', { params: { id: userid }, signal }),\n});\n```\n\n| feature | courier | tanstack query | ky |\n| | | | |\n| bundle size | <packageinfo package=\"courier\" type=\"size\" /> | framework adapter required | separate package |\n| zero runtime dependencies | <ore icon name=\"check\" size=\"16\"></ore icon> | <ore icon name=\"x\" size=\"16\"></ore icon> | <ore icon name=\"check\" size=\"16\"></ore icon> |\n| native fetch transport | <ore icon name=\"check\" size=\"16\"></ore icon> | bring your own | <ore icon name=\"check\" size=\"16\"></ore icon> |\n| explicit cache keys | <ore icon name=\"check\" size=\"16\"></ore icon> | <ore icon name=\"check\" size=\"16\"></ore icon> | <ore icon name=\"x\" size=\"16\"></ore icon> |\n| sse and ndjson iteration | <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| external runtime dependencies | <ore icon name=\"check\" size=\"16\"></ore icon> | <ore icon name=\"triangle alert\" size=\"16\"></ore icon> | <ore icon name=\"check\" size=\"16\"></ore icon> |\n\n<div class=\"decision callout\">\n\n**use courier when** one application client should own typed http, explicit cached reads, direct writes, and\nabortable response streams.\n\n**consider tanstack query when** you need a maintained framework adapter or advanced cache features such as\ninfinite queries. **consider ky when** you only need a compact fetch wrapper without caching or streams.\n\n</div>\n\n## installation\n\n::: code group\n\n```sh [pnpm]\npnpm add @vielzeug/courier\n```\n\n```sh [npm]\nnpm install @vielzeug/courier\n```\n\n```sh [yarn]\nyarn add @vielzeug/courier\n```\n\n:::\n\n## quick start\n\ncreate one client for an application or request scope, then fetch a cache entry by its explicit key.\n\n```ts\nimport { courierhttperror, createcourier } from '@vielzeug/courier';\n\ntype user = { id: number; name: string };\n\nconst courier = createcourier({ baseurl: 'https://api.example.com', query: { staletime: 30_000 } });\nconst key = ['users', 42] as const;\n\ntry {\n await courier.queries.fetch({\n key,\n fetch: ({ signal }) => courier.get('/users/{id}', { params: { id: 42 }, signal }),\n });\n console.log(courier.queries.getsnapshot<user>(key)?.data);\n} catch (error) {\n if (courierhttperror.is(error, 404)) console.log('user not found');\n else throw error;\n} finally {\n courier.dispose();\n}\n```\n\n## features\n\n<div class=\"features grid\">\n\n **`createcourier()`** — one lifecycle, interceptor pipeline, header store, and cancellation boundary.\n **`get()` / `post()` / `put()` / `patch()` / `delete()`** — typed paths, query strings, request bodies, validation, and structured errors.\n **`queries.fetch()`** — key based cached reads, subscriptions, invalidation with refetch, and automatic garbage collection.\n **`mutate()`** — direct write operation with `invalidatekeys` for one step cache refetch, without hidden retries or a second state store.\n **`events()` / `read()`** — abortable sse, text, and ndjson iteration with normalized request errors.\n **`withbearerauth()` / `withrequestid()` / `withlogging()`** — composable transport policies.\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 [flux](/flux/) — adapts courier cache entries and event iterators into composable streams.\n [ripple](/ripple/) — stores courier snapshots in fine grained reactive state.\n [spell](/spell/) — validates parsed http payloads through courier's schema option.\n\n</div>\n\n<! markdownlint enable md025 md033 md060 >\n",
|
|
317
|
-
"api": " \ntitle: courier — api reference\ndescription: reference for courier http, cache, mutation, interceptor, and stream apis.\n \n\n[[toc]]\n\n## api overview\n\n| symbol | purpose | execution mode | common gotcha |\n| | | | |\n| `createcourier()` | creates unified application client | sync | dispose only when whole scope ends |\n| `courier` http methods | sends and parses http requests | async | direct calls never deduplicate |\n| `queries.fetch()` | fetches one keyed cache entry | async | key must include all response identity inputs |\n| `mutate()` | runs one write operation | async | it never retries automatically |\n| `events()` / `read()` | opens abortable response iterators | async iteration | breaking iteration aborts request |\n| `withbearerauth()` | adds authorization interceptor | sync | token provider runs per request |\n| `withrequestid()` | adds request identifier interceptor | sync | default generator uses `uuid()` |\n| `withlogging()` | logs request result metadata | sync | requires explicit logger; urls may contain sensitive query values |\n\n## package entry point\n\n| import | purpose |\n| | |\n| `@vielzeug/courier` | client factory, errors, interceptors, and public types |\n\n## client\n\n### `createcourier()`\n\n```ts\ncreatecourier(options?: courieroptions): courier;\n```\n\nreturns client sharing transport configuration, headers, interceptors, cancellation, cache, mutations, and streams.\n\n| `courieroptions` field | type | default | description |\n| | | | |\n| `baseurl` | `string` | `''` | prefix for relative request paths |\n| `fetch` | `typeof globalthis.fetch` | `globalthis.fetch` | fetch implementation |\n| `headers` | `record<string, string>` | `{}` | global request headers |\n| `timeout` | `number` | `30_000` | default http timeout in milliseconds |\n| `query.staletime` | `number` | `0` | cache freshness duration |\n| `query.gctime` | `number` | `300_000` | garbage collect entries with no subscribers after this duration (ms); `infinity` disables |\n\n**returns:** `courier`.\n\n```ts\nimport { createcourier } from '@vielzeug/courier';\n\nconst courier = createcourier({ baseurl: 'https://api.example.com' });\n```\n\n| `courier` member | signature | description |\n| | | |\n| `get` / `post` / `put` / `patch` / `delete` | `<t, p>(url: p, config?) => promise<t>` | sends one http request |\n| `setheaders` | `(updates) => void` | updates global headers |\n| `getheaders` | `() => readonly<record<string, string>>` | returns header snapshot |\n| `use` | `(interceptor) => () => void` | registers interceptor |\n| `cancelall` | `() => void` | aborts active http, cache, and mutation work |\n| `queries` | `querycache` | owns keyed cache entries |\n| `mutate` | `<t>(options) => promise<t>` | runs one write operation |\n| `events` | `<t, p>(url, options?) => asynciterableiterator<streamevent<t>>` | opens sse iterator |\n| `read` | `<t, p>(url, options?) => asynciterableiterator<t>` | opens text or ndjson iterator |\n| `dispose` | `() => void` | final disposal; aborts work and clears cache |\n| `disposed` | `boolean` | whether final disposal occurred |\n| `disposalsignal` | `abortsignal` | aborts on final disposal |\n\n \n\n## queries\n\n### `queries.fetch()`\n\n```ts\nfetch<t>(definition: querydefinition<t>, options?: { force?: boolean }): promise<t>;\n```\n\nregisters latest definition for `definition.key`, then returns fresh cached data or runs its fetch function.\n\n| parameter | type | description |\n| | | |\n| `definition.key` | `querykey` | cache identity; include every response identity input |\n| `definition.fetch` | `(context: querycontext) => promise<t>` | request function for this key |\n| `definition.staletime` | `number` | per entry freshness duration |\n| `options.force` | `boolean` | fetch even when cached data is fresh |\n\n**returns:** cached or fetched data.\n\n```ts\nconst key = ['profile', 1] as const;\nawait courier.queries.fetch({\n key,\n fetch: ({ signal }) => courier.get('/profile/{id}', { params: { id: 1 }, signal }),\n});\n```\n\n| `querycache` method | returns | description |\n| | | |\n| `get(key)` | `t \\| undefined` | returns successful cached data |\n| `getsnapshot(key)` | `asyncstate<t> \\| null` | returns snapshot by key |\n| `set(key, data, options?)` | `void` | sets successful cache value |\n| `invalidate(prefix, options?)` | `void` | marks matching key prefixes stale; `options.refetch` triggers background refetch |\n| `keys()` | `querykey[]` | lists known keys |\n| `subscribe(key, listener)` | `unsubscribe` | subscribes to one key |\n| `clear()` | `void` | removes every cache entry |\n\n \n\n## mutations\n\n### `mutate()`\n\n```ts\nmutate<t>(options: mutationoptions<t>): promise<t>;\n```\n\nruns `options.request` once, then calls `onsuccess` after successful completion, then invalidates (and refetches) each key in `invalidatekeys`.\n\n| `mutationoptions<t>` field | type | description |\n| | | |\n| `request` | `(context: mutationcontext) => promise<t>` | write operation |\n| `onsuccess` | `(data, queries) => void \\| promise<void>` | cache update callback |\n| `invalidatekeys` | `readonly (readonly unknown[])[]` | key prefixes to invalidate and refetch after success |\n| `signal` | `abortsignal` | caller controlled cancellation |\n\n**returns:** request result.\n\n \n\n## streams\n\n### `events()` and `read()`\n\n```ts\nevents<t, p extends string>(url: p, options?: streamoptions<p>): asynciterableiterator<streamevent<t>>;\nread<t, p extends string>(url: p, options?: streamoptions<p> & { parse?: 'ndjson' | 'text' }): asynciterableiterator<t>;\n```\n\nboth iterators abort request when `return()` runs or `for await` loop exits. `events()` parses `event` and `data`\nfields; it does not retain event ids or reconnect.\n\n`streamoptions<p>` extends `requestconfig<p>` (typed path params) with an optional `method` field. it omits\n`responsetype` and `schema` (not applicable to streaming).\n\n**returns:** abortable async iterator.\n\n \n\n## interceptors\n\n### interceptor helpers\n\n```ts\nwithbearerauth(token: string | (() => string | promise<string>)): interceptor;\nwithrequestid(options?: { generate?: () => string; header?: string }): interceptor;\nwithlogging(options: {\n logger: (message: string, meta: { duration: number; method: string; status: number; url: string }) => void;\n}): interceptor;\n```\n\neach helper returns an `interceptor` accepted by `courier.use()`. `withlogging` requires an explicit `logger`\nfunction — no default console output.\n\n## types\n\n```ts\ntype transportoptions = {\n baseurl?: string;\n fetch?: typeof globalthis.fetch;\n headers?: record<string, string>;\n timeout?: number;\n};\n\ntype courieroptions = transportoptions & {\n query?: { gctime?: number; staletime?: number };\n};\n\ntype fetchcontext = {\n readonly headers: readonly<record<string, string>>;\n readonly init: readonly<omit<requestinit, 'headers'>>;\n readonly url: string;\n withheaders(updates: record<string, string>): fetchcontext;\n};\n\ntype interceptor = (ctx: fetchcontext, next: (ctx: fetchcontext) => promise<response>) => promise<response>;\n\ntype asyncstate<t> =\n | { data: undefined; error: null; isfetching: boolean; status: 'loading'; updatedat: undefined }\n | { data: t; error: null; isfetching: boolean; status: 'success'; updatedat: number }\n | { data: t | undefined; error: error; isfetching: false; status: 'error'; updatedat: number };\n\ntype querycontext = { readonly key: querykey; readonly signal: abortsignal };\ntype querydefinition<t> = { fetch: (context: querycontext) => promise<t>; key: querykey; staletime?: number };\ntype querykey = readonly [querykeyatom, ...querykeyatom[]];\ntype querykeyatom = string | number | boolean | null;\ntype querycache = {\n clear(): void;\n fetch<t>(definition: querydefinition<t>, options?: { force?: boolean }): promise<t>;\n get<t>(key: querykey): t | undefined;\n getsnapshot<t>(key: querykey): asyncstate<t> | null;\n invalidate(prefix: readonly unknown[], options?: { refetch?: boolean }): void;\n keys(): querykey[];\n set<t>(key: querykey, data: t, options?: { updatedat?: number }): void;\n subscribe(key: querykey, listener: () => void): unsubscribe;\n};\ntype mutationcontext = { readonly signal: abortsignal };\ntype mutationoptions<t> = {\n invalidatekeys?: readonly (readonly unknown[])[];\n onsuccess?: (data: t, queries: querycache) => void | promise<void>;\n request: (context: mutationcontext) => promise<t>;\n signal?: abortsignal;\n};\ntype streamevent<t = unknown> = { readonly data: t; readonly event: string };\ntype streamoptions<p extends string = string> = omit<requestconfig<p>, 'responsetype' | 'schema'> & {\n method?: string;\n};\ntype unsubscribe = () => void;\n```\n\n```ts\ntype paramvalue = string | number | boolean | null | readonly (string | number | boolean | null)[] | undefined;\ntype params = record<string, paramvalue>;\ntype requestconfig<p extends string = string, t = unknown> = {\n body?: unknown;\n fetchinit?: omit<requestinit, 'body' | 'headers' | 'method' | 'signal'>;\n headers?: record<string, string>;\n params?: record<string, string | number | boolean>;\n query?: params;\n responsetype?: 'auto' | 'json' | 'text' | 'blob' | 'arraybuffer' | 'raw';\n schema?: { parse(data: unknown): t };\n signal?: abortsignal;\n timeout?: number;\n};\n```\n\n## errors\n\n| error | trigger | notable properties |\n| | | |\n| `couriererror` | base class for all courier errors | use `instanceof` to narrow |\n| `courierhttperror` | non 2xx http response | `status`, `data`, `headers`, `method`, `url`; `courierhttperror.is(e, status?)` narrows by status |\n| `couriernetworkerror` | request failure without response | `method`, `url`, `cause` |\n| `couriertimeouterror` | timeout signal aborts request | `method`, `url`, `cause` |\n| `courieraborterror` | caller, client, or iterator cancellation | `method`, `url`, `cause` |\n| `courierschemavalidationerror` | response schema fails | `data`, `cause` |\n| `courierparseerror` | response body cannot parse | — |\n| `courierdisposederror` | work starts after disposal | — |\n",
|
|
318
|
-
"usage": " \ntitle: courier — usage guide\ndescription: use one courier client for http, explicit cached reads, direct mutations, and abortable streams.\n \n\n[[toc]]\n\n## basic usage\n\ncreate one courier client for an application or request scope. its transport policy and disposal lifecycle apply\nto every request, cache entry, mutation, and stream.\n\n```ts\nimport { createcourier } from '@vielzeug/courier';\n\ntype user = { id: number; name: string };\n\nconst courier = createcourier({ baseurl: 'https://api.example.com', query: { staletime: 30_000 } });\nconst key = ['users', 1] as const;\n\nawait courier.queries.fetch({\n key,\n fetch: ({ signal }) => courier.get<user>('/users/{id}', { params: { id: 1 }, signal }),\n});\nconsole.log(courier.queries.get<user>(key)?.name);\n```\n\n## http requests\n\nuse root methods for rest requests. courier encodes path parameters, serializes plain object bodies, and parses\nsuccessful response bodies. each direct http call is independent; use a query key when concurrent cached reads\nshould share work.\n\n```ts\nconst posts = await courier.get<{ id: number; title: string }[]>('/users/{id}/posts', {\n params: { id: 1 },\n query: { limit: 20, status: 'published' },\n});\n\nawait courier.patch('/posts/{id}', {\n body: { title: 'updated title' },\n params: { id: posts[0].id },\n});\n```\n\ncall `courier.setheaders({ authorization: 'bearer token' })` to update subsequent calls.\n\n## interceptors\n\ninterceptors apply to http and streaming requests. register a policy once, then remove it when its containing\nscope ends.\n\n```ts\nimport { withbearerauth, withrequestid } from '@vielzeug/courier';\n\nconst removeauth = courier.use(withbearerauth(async () => sessionstorage.getitem('access token') ?? ''));\nconst removerequestid = courier.use(withrequestid());\n\nremoverequestid();\nremoveauth();\n```\n\nuse `withlogging()` with an explicit `logger` function to log requests during local development. `withlogging()`\nincludes full urls, so sanitize query values before persistent logging.\n\n```ts\nimport { withlogging } from '@vielzeug/courier';\n\ncourier.use(withlogging({ logger: (msg) => console.log(msg) }));\n```\n\n## cached queries\n\npass a stable key and fetch definition to `queries.fetch()`. the cache owns data, snapshots, subscriptions, and\nin flight deduplication for that key. entries with no subscribers are garbage collected after `gctime` (default\n5 min; `infinity` disables).\n\n```ts\nconst key = ['profile', 1] as const;\nconst definition = {\n key,\n fetch: ({ signal }) => courier.get<{ id: number; name: string }>('/profile/{id}', { params: { id: 1 }, signal }),\n staletime: 60_000,\n};\n\nconst stop = courier.queries.subscribe(key, () => {\n const state = courier.queries.getsnapshot<{ id: number; name: string }>(key);\n if (state?.status === 'success') console.log(state.data.name);\n if (state?.status === 'error') console.error(state.error);\n});\n\nawait courier.queries.fetch(definition);\nstop();\n```\n\n`queries.fetch(definition)` reuses fresh data. pass `{ force: true }` to fetch regardless of freshness.\n`invalidate(prefix, { refetch: true })` marks matching key prefixes stale and refetches them in the background\nin a single call.\n\n## direct mutations\n\nuse `mutate()` for a write operation. pass `invalidatekeys` to invalidate and refetch cache entries after a\nsuccessful write — no manual `invalidate()` + refetch boilerplate. use `onsuccess` for custom cache writes\n(e.g. seeding a created entity). courier never retries writes: retry only operations your application can prove\nidempotent.\n\n```ts\ntype user = { id: number; name: string };\n\nconst created = await courier.mutate<user>({\n request: ({ signal }) => courier.post<user>('/users', { body: { name: 'ada' }, signal }),\n onsuccess: (user, queries) => queries.set(['users', user.id], user),\n invalidatekeys: [['users']],\n});\n\nconsole.log(created.id);\n```\n\npass an external `signal` when caller owns cancellation. keep pending and error ui state in framework that owns\nthat ui.\n\n## server sent events\n\n`events()` returns an abortable `asynciterableiterator`. breaking loop, calling `return()`, aborting a provided\nsignal, or disposing client stops its request immediately. courier sends `accept: text/event stream` and\n`cache control: no cache` by default; pass headers to override either value.\n\n```ts\ntype notification = { text: string };\n\nfor await (const event of courier.events<notification>('/events')) {\n if (event.event !== 'message') continue;\n console.log(event.data.text);\n break;\n}\n```\n\ncourier parses valid json event data and otherwise returns text. it does not reconnect automatically or retain\nsse event ids; application owns reconnect policy.\n\n## http streaming\n\nuse `read()` for text chunks or ndjson records.\n\n```ts\ntype chatchunk = { done: boolean; delta: string };\n\nfor await (const chunk of courier.read<chatchunk>('/chat', {\n body: { prompt: 'explain cached queries.' },\n method: 'post',\n parse: 'ndjson',\n})) {\n console.log(chunk.delta);\n if (chunk.done) break;\n}\n```\n\nstreams have no timeout unless `timeout` is supplied. http, network, timeout, and cancellation failures use\ncourier error classes; starting a stream after disposal throws `courierdisposederror`.\n\n## framework integration\n\ncreate courier at application or route boundary. views read a key snapshot synchronously, subscribe during\ntheir lifecycle, and let framework own rendering state.\n\n::: code group\n\n```tsx [react]\nimport { useeffect, usesyncexternalstore } from 'react';\nimport { createcourier } from '@vielzeug/courier';\nimport type { asyncstate, querydefinition } from '@vielzeug/courier';\n\ntype user = { id: number; name: string };\n\nexport function profile({ courier, definition }: { courier: returntype<typeof createcourier>; definition: querydefinition<user> }) {\n const state = usesyncexternalstore(\n (listener) => courier.queries.subscribe(definition.key, listener),\n () => courier.queries.getsnapshot<user>(definition.key),\n () => courier.queries.getsnapshot<user>(definition.key),\n ) as asyncstate<user> | null;\n\n useeffect(() => void courier.queries.fetch(definition), [courier, definition]);\n\n if (!state || state.status === 'loading') return <p>loading...</p>;\n if (state.status === 'error') return <p role=\"alert\">{state.error.message}</p>;\n return <p>{state.data.name}</p>;\n}\n```\n\n```ts [vue 3]\nimport { onmounted, onunmounted, ref } from 'vue';\nimport { createcourier } from '@vielzeug/courier';\nimport type { asyncstate, querydefinition } from '@vielzeug/courier';\n\ntype user = { id: number; name: string };\n\nexport function useprofile(courier: returntype<typeof createcourier>, definition: querydefinition<user>) {\n const state = ref<asyncstate<user> | null>(courier.queries.getsnapshot(definition.key));\n const unsubscribe = courier.queries.subscribe(definition.key, () => {\n state.value = courier.queries.getsnapshot(definition.key);\n });\n\n onmounted(() => void courier.queries.fetch(definition));\n onunmounted(unsubscribe);\n\n return { state };\n}\n```\n\n```svelte [svelte]\n<script lang=\"ts\">\n import { onmount } from 'svelte';\n import { createcourier } from '@vielzeug/courier';\n import type { asyncstate, querydefinition } from '@vielzeug/courier';\n\n type user = { id: number; name: string };\n\n export let courier: returntype<typeof createcourier>;\n export let definition: querydefinition<user>;\n let state: asyncstate<user> | null = courier.queries.getsnapshot(definition.key);\n\n onmount(() => {\n const unsubscribe = courier.queries.subscribe(definition.key, () => (state = courier.queries.getsnapshot(definition.key)));\n void courier.queries.fetch(definition);\n return unsubscribe;\n });\n</script>\n\n{#if state?.status === 'success'}\n <p>{state.data.name}</p>\n{/if}\n```\n\n:::\n\ncourier exposes no framework specific loading or error store. render `asyncstate` in framework that owns view.\n\n## working with other vielzeug libraries\n\n### flux\n\nuse flux when cache snapshots or sse events need filtering, composition, or subscription lifecycle separate from\nui framework. pass cache and query definition to `fromquery()`.\n\n```ts\nimport { fromquery } from '@vielzeug/flux/courier';\n\nconst profile = {\n key: ['profile'] as const,\n fetch: ({ signal }: { signal: abortsignal }) => courier.get<{ id: number; name: string }>('/profile', { signal }),\n};\nconst profile$ = fromquery(courier.queries, profile);\n\nvoid courier.queries.fetch(profile);\n\nconst profilesubscription = profile$.subscribe((state) => console.log(state?.status));\n\nprofilesubscription.unsubscribe();\n```\n\n### ripple\n\nuse a ripple signal when courier data must participate in fine grained reactive state outside a component. mirror\nonly cache snapshot into signal.\n\n```ts\nimport { signal } from '@vielzeug/ripple';\n\nconst key = ['profile', 1] as const;\nconst profilestate = signal(courier.queries.getsnapshot<{ id: number; name: string }>(key));\nconst unsubscribe = courier.queries.subscribe(key, () => (profilestate.value = courier.queries.getsnapshot(key)));\n\nawait courier.queries.fetch({\n key,\n fetch: ({ signal }) => courier.get('/profile/{id}', { params: { id: 1 }, signal }),\n});\n\nunsubscribe();\n```\n\n## best practices\n\n create one courier client per application or ssr request scope.\n use stable, complete cache keys for every cached response identity.\n fetch through `queries.fetch()` when work should deduplicate and cache.\n use `invalidatekeys` on mutations to refetch affected cache entries in one step.\n keep retries outside mutations until operation idempotency is proven.\n dispose only at final application or request boundary.\n keep credentials out of urls when using logging interceptors.\n",
|
|
317
|
+
"api": " \ntitle: courier — api reference\ndescription: reference for courier http, cache, mutation, interceptor, and stream apis.\n \n\n[[toc]]\n\n## api overview\n\n| symbol | purpose | execution mode | common gotcha |\n| | | | |\n| `createcourier()` | creates unified application client | sync | dispose only when whole scope ends |\n| `courier` http methods | sends and parses http requests | async | direct calls never deduplicate |\n| `queries.fetch()` | fetches one keyed cache entry | async | key must include all response identity inputs |\n| `mutate()` | runs one write operation | async | it never retries automatically |\n| `events()` / `read()` | opens abortable response iterators | async iteration | breaking iteration aborts request |\n| `withbearerauth()` | adds authorization interceptor | sync | token provider runs per request |\n| `withrequestid()` | adds request identifier interceptor | sync | default generator uses `uuid()` |\n| `withlogging()` | logs request result metadata | sync | requires explicit logger; urls may contain sensitive query values |\n\n## package entry point\n\n| import | purpose |\n| | |\n| `@vielzeug/courier` | client factory, errors, interceptors, and public types |\n\n## client\n\n### `createcourier()`\n\n```ts\ncreatecourier(options?: courieroptions): courier;\n```\n\nreturns client sharing transport configuration, headers, interceptors, cancellation, cache, mutations, and streams.\n\n| `courieroptions` field | type | default | description |\n| | | | |\n| `baseurl` | `string` | `''` | prefix for relative request paths |\n| `fetch` | `typeof globalthis.fetch` | `globalthis.fetch` | fetch implementation |\n| `headers` | `record<string, string>` | `{}` | global request headers |\n| `timeout` | `number` | `30_000` | default http timeout in milliseconds |\n| `query.staletime` | `number` | `0` | cache freshness duration |\n| `query.gctime` | `number` | `300_000` | garbage collect entries with no subscribers after this duration (ms); `infinity` disables |\n\n**returns:** `courier`.\n\n```ts\nimport { createcourier } from '@vielzeug/courier';\n\nconst courier = createcourier({ baseurl: 'https://api.example.com' });\n```\n\n| `courier` member | signature | description |\n| | | |\n| `get` / `post` / `put` / `patch` / `delete` | `<t, p>(url: p, config?) => promise<t>` | sends one http request |\n| `setheaders` | `(updates) => void` | updates global headers |\n| `getheaders` | `() => readonly<record<string, string>>` | returns header snapshot |\n| `use` | `(interceptor) => () => void` | registers interceptor |\n| `cancelall` | `() => void` | aborts active http, cache, and mutation work; a subsequent `queries.fetch()` starts a fresh request |\n| `queries` | `querycache` | owns keyed cache entries |\n| `mutate` | `<t>(options) => promise<t>` | runs one write operation |\n| `events` | `<t, p>(url, options?) => asynciterableiterator<streamevent<t>>` | opens sse iterator |\n| `read` | `<t, p>(url, options?) => asynciterableiterator<t>` | opens text or ndjson iterator |\n| `dispose` | `() => void` | final disposal; aborts work and clears cache |\n| `disposed` | `boolean` | whether final disposal occurred |\n| `disposalsignal` | `abortsignal` | aborts on final disposal |\n\n \n\n## queries\n\n### `queries.fetch()`\n\n```ts\nfetch<t>(definition: querydefinition<t>, options?: { force?: boolean }): promise<t>;\n```\n\nregisters latest definition for `definition.key`, then returns fresh cached data or runs its fetch function.\n\n| parameter | type | description |\n| | | |\n| `definition.key` | `querykey` | cache identity; include every response identity input |\n| `definition.fetch` | `(context: querycontext) => promise<t>` | request function for this key |\n| `definition.staletime` | `number` | per entry freshness duration |\n| `options.force` | `boolean` | fetch even when cached data is fresh |\n\n**returns:** cached or fetched data.\n\n```ts\nconst key = ['profile', 1] as const;\nawait courier.queries.fetch({\n key,\n fetch: ({ signal }) => courier.get('/profile/{id}', { params: { id: 1 }, signal }),\n});\n```\n\n| `querycache` method | returns | description |\n| | | |\n| `get(key)` | `t \\| undefined` | returns successful cached data |\n| `getsnapshot(key)` | `asyncstate<t> \\| null` | returns snapshot by key |\n| `set(key, data, options?)` | `void` | sets successful cache value |\n| `delete(key)` | `void` | removes one cache entry and aborts its active fetch if present |\n| `invalidate(prefix, options?)` | `void` | marks matching key prefixes stale; `options.refetch` triggers background refetch |\n| `keys()` | `querykey[]` | lists known keys |\n| `subscribe(key, listener)` | `unsubscribe` | subscribes to one key |\n| `clear()` | `void` | removes every cache entry |\n\n \n\n## mutations\n\n### `mutate()`\n\n```ts\nmutate<t>(options: mutationoptions<t>): promise<t>;\n```\n\nruns `options.request` once, then calls `onsuccess` after successful completion, then invalidates (and refetches) each key in `invalidatekeys`.\n\n| `mutationoptions<t>` field | type | description |\n| | | |\n| `request` | `(context: mutationcontext) => promise<t>` | write operation |\n| `onsuccess` | `(data, queries) => void \\| promise<void>` | cache update callback |\n| `invalidatekeys` | `readonly (readonly unknown[])[]` | key prefixes to invalidate and refetch after success |\n| `signal` | `abortsignal` | caller controlled cancellation |\n\n**returns:** request result.\n\n \n\n## streams\n\n### `events()` and `read()`\n\n```ts\nevents<t, p extends string>(url: p, options?: streamoptions<p>): asynciterableiterator<streamevent<t>>;\nread<t, p extends string>(url: p, options?: streamoptions<p> & { parse?: 'ndjson' | 'text' }): asynciterableiterator<t>;\n```\n\nboth iterators abort request when `return()` runs or `for await` loop exits. `events()` parses `event` and `data`\nfields; it does not retain event ids or reconnect.\n\n`streamoptions<p>` extends `requestconfig<p>` (typed path params) with an optional `method` field. it omits\n`responsetype` and `schema` (not applicable to streaming).\n\n**returns:** abortable async iterator.\n\n \n\n## interceptors\n\n### interceptor helpers\n\n```ts\nwithbearerauth(token: string | (() => string | promise<string>)): interceptor;\nwithrequestid(options?: { generate?: () => string; header?: string }): interceptor;\nwithlogging(options: {\n logger: (message: string, meta: { duration: number; method: string; status: number; url: string }) => void;\n}): interceptor;\n```\n\neach helper returns an `interceptor` accepted by `courier.use()`. `withlogging` requires an explicit `logger`\nfunction — no default console output.\n\n## types\n\n```ts\ntype transportoptions = {\n baseurl?: string;\n fetch?: typeof globalthis.fetch;\n headers?: record<string, string>;\n timeout?: number;\n};\n\ntype courieroptions = transportoptions & {\n query?: { gctime?: number; staletime?: number };\n};\n\ntype fetchcontext = {\n readonly headers: readonly<record<string, string>>;\n readonly init: readonly<omit<requestinit, 'headers'>>;\n readonly url: string;\n withheaders(updates: record<string, string>): fetchcontext;\n};\n\ntype interceptor = (ctx: fetchcontext, next: (ctx: fetchcontext) => promise<response>) => promise<response>;\n\ntype asyncstate<t> =\n | { data: undefined; error: null; isfetching: boolean; status: 'loading'; updatedat: undefined }\n | { data: t; error: null; isfetching: boolean; status: 'success'; updatedat: number }\n | { data: t | undefined; error: error; isfetching: false; status: 'error'; updatedat: number };\n\ntype querycontext = { readonly key: querykey; readonly signal: abortsignal };\ntype querydefinition<t> = { fetch: (context: querycontext) => promise<t>; key: querykey; staletime?: number };\ntype querykey = readonly [querykeyatom, ...querykeyatom[]];\ntype querykeyatom = string | number | boolean | null;\ntype querycache = {\n clear(): void;\n delete(key: querykey): void;\n fetch<t>(definition: querydefinition<t>, options?: { force?: boolean }): promise<t>;\n get<t>(key: querykey): t | undefined;\n getsnapshot<t>(key: querykey): asyncstate<t> | null;\n invalidate(prefix: readonly unknown[], options?: { refetch?: boolean }): void;\n keys(): querykey[];\n set<t>(key: querykey, data: t, options?: { updatedat?: number }): void;\n subscribe(key: querykey, listener: () => void): unsubscribe;\n};\ntype mutationcontext = { readonly signal: abortsignal };\ntype mutationoptions<t> = {\n invalidatekeys?: readonly (readonly unknown[])[];\n onsuccess?: (data: t, queries: querycache) => void | promise<void>;\n request: (context: mutationcontext) => promise<t>;\n signal?: abortsignal;\n};\ntype streamevent<t = unknown> = { readonly data: t; readonly event: string };\ntype streamoptions<p extends string = string> = omit<requestconfig<p>, 'responsetype' | 'schema'> & {\n method?: string;\n};\ntype unsubscribe = () => void;\n```\n\n```ts\ntype paramvalue = string | number | boolean | null | readonly (string | number | boolean | null)[] | undefined;\ntype params = record<string, paramvalue>;\ntype requestconfig<p extends string = string, t = unknown> = {\n body?: unknown;\n fetchinit?: omit<requestinit, 'body' | 'headers' | 'method' | 'signal'>;\n headers?: record<string, string>;\n params?: record<string, string | number | boolean>;\n query?: params;\n responsetype?: 'auto' | 'json' | 'text' | 'blob' | 'arraybuffer' | 'raw';\n schema?: { parse(data: unknown): t };\n signal?: abortsignal;\n timeout?: number;\n};\n```\n\n## errors\n\n| error | trigger | notable properties |\n| | | |\n| `couriererror` | base class for all courier errors | use `instanceof` to narrow |\n| `courierhttperror` | non 2xx http response | `status`, `data`, `headers`, `method`, `url`; `courierhttperror.is(e, status?)` narrows by status |\n| `couriernetworkerror` | request failure without response | `method`, `url`, `cause` |\n| `couriertimeouterror` | timeout signal aborts request | `method`, `url`, `cause` |\n| `courieraborterror` | caller, client, or iterator cancellation | `method`, `url`, `cause` |\n| `courierschemavalidationerror` | response schema fails | `data`, `cause` |\n| `courierparseerror` | response body cannot parse | — |\n| `courierdisposederror` | work starts after disposal | — |\n",
|
|
318
|
+
"usage": " \ntitle: courier — usage guide\ndescription: use one courier client for http, explicit cached reads, direct mutations, and abortable streams.\n \n\n[[toc]]\n\n## basic usage\n\ncreate one courier client for an application or request scope. its transport policy and disposal lifecycle apply\nto every request, cache entry, mutation, and stream.\n\n```ts\nimport { createcourier } from '@vielzeug/courier';\n\ntype user = { id: number; name: string };\n\nconst courier = createcourier({ baseurl: 'https://api.example.com', query: { staletime: 30_000 } });\nconst key = ['users', 1] as const;\n\nawait courier.queries.fetch({\n key,\n fetch: ({ signal }) => courier.get<user>('/users/{id}', { params: { id: 1 }, signal }),\n});\nconsole.log(courier.queries.get<user>(key)?.name);\n```\n\n## http requests\n\nuse root methods for rest requests. courier encodes path parameters, serializes plain object bodies, and parses\nsuccessful response bodies. each direct http call is independent; use a query key when concurrent cached reads\nshould share work.\n\n```ts\nconst posts = await courier.get<{ id: number; title: string }[]>('/users/{id}/posts', {\n params: { id: 1 },\n query: { limit: 20, status: 'published' },\n});\n\nawait courier.patch('/posts/{id}', {\n body: { title: 'updated title' },\n params: { id: posts[0].id },\n});\n```\n\ncall `courier.setheaders({ authorization: 'bearer token' })` to update subsequent calls.\n\n## interceptors\n\ninterceptors apply to http and streaming requests. register a policy once, then remove it when its containing\nscope ends.\n\n```ts\nimport { withbearerauth, withrequestid } from '@vielzeug/courier';\n\nconst removeauth = courier.use(withbearerauth(async () => sessionstorage.getitem('access token') ?? ''));\nconst removerequestid = courier.use(withrequestid());\n\nremoverequestid();\nremoveauth();\n```\n\nuse `withlogging()` with an explicit `logger` function to log requests during local development. `withlogging()`\nincludes full urls, so sanitize query values before persistent logging.\n\n```ts\nimport { withlogging } from '@vielzeug/courier';\n\ncourier.use(withlogging({ logger: (msg) => console.log(msg) }));\n```\n\n## cached queries\n\npass a stable key and fetch definition to `queries.fetch()`. the cache owns data, snapshots, subscriptions, and\nin flight deduplication for that key. entries with no subscribers are garbage collected after `gctime` (default\n5 min; `infinity` disables).\n\n```ts\nconst key = ['profile', 1] as const;\nconst definition = {\n key,\n fetch: ({ signal }) => courier.get<{ id: number; name: string }>('/profile/{id}', { params: { id: 1 }, signal }),\n staletime: 60_000,\n};\n\nconst stop = courier.queries.subscribe(key, () => {\n const state = courier.queries.getsnapshot<{ id: number; name: string }>(key);\n if (state?.status === 'success') console.log(state.data.name);\n if (state?.status === 'error') console.error(state.error);\n});\n\nawait courier.queries.fetch(definition);\nstop();\n```\n\n`queries.fetch(definition)` reuses fresh data. pass `{ force: true }` to fetch regardless of freshness.\n`invalidate(prefix, { refetch: true })` marks matching key prefixes stale and refetches them in the background\nin a single call.\n\n## direct mutations\n\nuse `mutate()` for a write operation. pass `invalidatekeys` to invalidate and refetch cache entries after a\nsuccessful write — no manual `invalidate()` + refetch boilerplate. use `onsuccess` for custom cache writes\n(e.g. seeding a created entity). courier never retries writes: retry only operations your application can prove\nidempotent.\n\n```ts\ntype user = { id: number; name: string };\n\nconst created = await courier.mutate<user>({\n request: ({ signal }) => courier.post<user>('/users', { body: { name: 'ada' }, signal }),\n onsuccess: (user, queries) => queries.set(['users', user.id], user),\n invalidatekeys: [['users']],\n});\n\nconsole.log(created.id);\n```\n\npass an external `signal` when caller owns cancellation. keep pending and error ui state in framework that owns\nthat ui.\n\n## server sent events\n\n`events()` returns an abortable `asynciterableiterator`. breaking loop, calling `return()`, aborting a provided\nsignal, or disposing client stops its request immediately. courier sends `accept: text/event stream` and\n`cache control: no cache` by default; pass headers to override either value.\n\n```ts\ntype notification = { text: string };\n\nfor await (const event of courier.events<notification>('/events')) {\n if (event.event !== 'message') continue;\n console.log(event.data.text);\n break;\n}\n```\n\ncourier parses valid json event data and otherwise returns text. it does not reconnect automatically or retain\nsse event ids; application owns reconnect policy.\n\n## http streaming\n\nuse `read()` for text chunks or ndjson records.\n\n```ts\ntype chatchunk = { done: boolean; delta: string };\n\nfor await (const chunk of courier.read<chatchunk>('/chat', {\n body: { prompt: 'explain cached queries.' },\n method: 'post',\n parse: 'ndjson',\n})) {\n console.log(chunk.delta);\n if (chunk.done) break;\n}\n```\n\nstreams have no timeout unless `timeout` is supplied. http, network, timeout, and cancellation failures use\ncourier error classes; starting a stream after disposal throws `courierdisposederror`.\n\n## framework integration\n\ncreate courier at application or route boundary. views read a key snapshot synchronously, subscribe during\ntheir lifecycle, and let framework own rendering state.\n\n::: code group\n\n```tsx [react]\nimport { useeffect, usesyncexternalstore } from 'react';\nimport { createcourier } from '@vielzeug/courier';\nimport type { asyncstate, querydefinition } from '@vielzeug/courier';\n\ntype user = { id: number; name: string };\n\nexport function profile({ courier, definition }: { courier: returntype<typeof createcourier>; definition: querydefinition<user> }) {\n const state = usesyncexternalstore(\n (listener) => courier.queries.subscribe(definition.key, listener),\n () => courier.queries.getsnapshot<user>(definition.key),\n () => courier.queries.getsnapshot<user>(definition.key),\n ) as asyncstate<user> | null;\n\n useeffect(() => void courier.queries.fetch(definition), [courier, definition]);\n\n if (!state || state.status === 'loading') return <p>loading...</p>;\n if (state.status === 'error') return <p role=\"alert\">{state.error.message}</p>;\n return <p>{state.data.name}</p>;\n}\n```\n\n```ts [vue 3]\nimport { onmounted, onunmounted, ref } from 'vue';\nimport { createcourier } from '@vielzeug/courier';\nimport type { asyncstate, querydefinition } from '@vielzeug/courier';\n\ntype user = { id: number; name: string };\n\nexport function useprofile(courier: returntype<typeof createcourier>, definition: querydefinition<user>) {\n const state = ref<asyncstate<user> | null>(courier.queries.getsnapshot(definition.key));\n const unsubscribe = courier.queries.subscribe(definition.key, () => {\n state.value = courier.queries.getsnapshot(definition.key);\n });\n\n onmounted(() => void courier.queries.fetch(definition));\n onunmounted(unsubscribe);\n\n return { state };\n}\n```\n\n```svelte [svelte]\n<script lang=\"ts\">\n import { onmount } from 'svelte';\n import { createcourier } from '@vielzeug/courier';\n import type { asyncstate, querydefinition } from '@vielzeug/courier';\n\n type user = { id: number; name: string };\n\n export let courier: returntype<typeof createcourier>;\n export let definition: querydefinition<user>;\n let state: asyncstate<user> | null = courier.queries.getsnapshot(definition.key);\n\n onmount(() => {\n const unsubscribe = courier.queries.subscribe(definition.key, () => (state = courier.queries.getsnapshot(definition.key)));\n void courier.queries.fetch(definition);\n return unsubscribe;\n });\n</script>\n\n{#if state?.status === 'success'}\n <p>{state.data.name}</p>\n{/if}\n```\n\n:::\n\ncourier exposes no framework specific loading or error store. render `asyncstate` in framework that owns view.\n\n## working with other vielzeug libraries\n\n### flux\n\nuse flux when cache snapshots or sse events need filtering, composition, or subscription lifecycle separate from\nui framework. pass cache and query definition to `fromquery()`.\n\n```ts\nimport { fromquery } from '@vielzeug/flux/courier';\n\nconst profile = {\n key: ['profile'] as const,\n fetch: ({ signal }: { signal: abortsignal }) => courier.get<{ id: number; name: string }>('/profile', { signal }),\n};\nconst profile$ = fromquery(courier.queries, profile);\n\nvoid courier.queries.fetch(profile);\n\nconst profilesubscription = profile$.subscribe((state) => console.log(state?.status));\n\nprofilesubscription.unsubscribe();\n```\n\n### ripple\n\nuse a ripple signal when courier data must participate in fine grained reactive state outside a component. mirror\nonly cache snapshot into signal.\n\n```ts\nimport { signal } from '@vielzeug/ripple';\n\nconst key = ['profile', 1] as const;\nconst profilestate = signal(courier.queries.getsnapshot<{ id: number; name: string }>(key));\nconst unsubscribe = courier.queries.subscribe(key, () => (profilestate.value = courier.queries.getsnapshot(key)));\n\nawait courier.queries.fetch({\n key,\n fetch: ({ signal }) => courier.get('/profile/{id}', { params: { id: 1 }, signal }),\n});\n\nunsubscribe();\n```\n\n## gotchas\n\n### `mutate()` runs `onsuccess` before `invalidatekeys`\n\n`mutate()` executes in order: `request` → `onsuccess` → `invalidatekeys`. if `onsuccess` calls `queries.set()` for a key also in `invalidatekeys`, the seeded data is overwritten by the background refetch. this is correct — `invalidatekeys` means \"refetch to confirm\" — but the order matters when `onsuccess` seeds optimistic data that `invalidatekeys` then replaces.\n\n### optimistic rollback should use `getsnapshot()`, not `get()`\n\n`queries.get()` only returns successful data and drops snapshot metadata (`status`, `error`, `updatedat`). it also cannot distinguish a missing entry from a successful cached value of `undefined`. for optimistic rollback, capture `getsnapshot()` first so you can restore prior success data with `updatedat`, or `delete(key)` when no previous success snapshot existed.\n\n### `baseurl` should not include query parameters\n\n`buildurl` joins `baseurl` and path with `/`. a base url like `https://api.example.com?token=abc` produces broken urls (`https://api.example.com?token=abc/users`). pass query parameters per request via `config.query` instead.\n\n### background refetch after error transitions through `loading`\n\nwhen `invalidate({ refetch: true })` triggers a background refetch on an error state entry, the snapshot transitions to `loading` (losing the previous error). consumers building \"error + retrying\" ui should track retry state separately — `asyncstate` has no `error` with `isfetching: true` variant.\n\n## best practices\n\n create one courier client per application or ssr request scope.\n use stable, complete cache keys for every cached response identity.\n fetch through `queries.fetch()` when work should deduplicate and cache.\n use `invalidatekeys` on mutations to refetch affected cache entries in one step.\n keep retries outside mutations until operation idempotency is proven.\n dispose only at final application or request boundary.\n keep credentials out of urls when using logging interceptors.\n",
|
|
319
319
|
"examples": " \ntitle: courier — examples\ndescription: practical examples and recipes for courier.\n \n\n## examples\n\n [authentication](./examples/authentication.md)\n [crud operations](./examples/crud operations.md)\n [disposal](./examples/disposal.md)\n [error handling patterns](./examples/error handling patterns.md)\n [file uploads](./examples/file uploads.md)\n [optimistic updates](./examples/optimistic updates.md)\n [polling](./examples/polling.md)\n [real time events](./examples/sse events.md)\n [ai token stream](./examples/ai token stream.md)\n"
|
|
320
320
|
},
|
|
321
321
|
"examples": [
|
|
@@ -344,8 +344,8 @@
|
|
|
344
344
|
"description": "framework agnostic drag and drop. drop zones with mime filtering, sortable lists with drag handles, and explicit connected scopes — zero dependencies.",
|
|
345
345
|
"docs": {
|
|
346
346
|
"index": " \ntitle: dnd — drag and drop primitives for the dom\ndescription: framework agnostic drag and drop. drop zones with mime filtering, sortable lists with drag handles, and explicit connected scopes — zero dependencies.\npackage: dnd\ncategory: ui interaction\nkeywords: [drag drop, sortable, file upload, drop zone, dnd, reorder]\nrelated: [ore, scroll, refine]\nexports: [createdropzone, createsortable, createsortablescope, applyreorder, matchesaccept]\nenvironments: [browser]\n \n\n<! markdownlint disable md025 md033 md060 >\n\n<packagehero package=\"dnd\" />\n\n## why dnd?\n\nthe html5 drag & drop api requires careful counter tracking to avoid hover state flicker, has no mime type pre filtering, and provides no sortable list abstraction.\n\n```ts\n// before — raw html5 drag & drop\nlet entercount = 0;\ndropzone.addeventlistener('dragenter', () => {\n entercount++;\n dropzone.classlist.add('over');\n});\ndropzone.addeventlistener('dragleave', () => {\n if ( entercount === 0) dropzone.classlist.remove('over');\n});\ndropzone.addeventlistener('dragover', (e) => e.preventdefault());\ndropzone.addeventlistener('drop', (e) => {\n e.preventdefault();\n entercount = 0;\n const files = [...e.datatransfer!.files];\n if (!files.every((f) => f.type.startswith('image/'))) return showerror('images only');\n uploadfiles(files);\n});\n\n// after — dnd\nimport { createdropzone } from '@vielzeug/dnd';\nconst zone = createdropzone({\n element: dropzone,\n accept: ['image/*'],\n ondrop: (files) => uploadfiles(files),\n ondroprejected: (files) => showerror(`${files.length} file(s) not accepted`),\n onhoverchange: (hovered) => dropzone.classlist.toggle('over', hovered),\n});\n```\n\n| feature | dnd | sortablejs | dnd kit |\n| | | | |\n| bundle size | <packageinfo package=\"dnd\" type=\"size\" /> | ~15 kb | ~30 kb |\n| framework agnostic | <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| mime type filtering | <ore icon name=\"check\" size=\"16\"></ore icon> pre validated | <ore icon name=\"x\" size=\"16\"></ore icon> | <ore icon name=\"x\" size=\"16\"></ore icon> |\n| counter based hover | <ore icon name=\"check\" size=\"16\"></ore icon> | <ore icon name=\"x\" size=\"16\"></ore icon> | n/a |\n| sortable lists | <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| drag handles | <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| `using` support | <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| touch support | <ore icon name=\"check\" size=\"16\"></ore icon> scoped opt in | <ore icon name=\"check\" size=\"16\"></ore icon> | <ore icon name=\"check\" size=\"16\"></ore icon> |\n| zero dependencies | <ore icon name=\"check\" size=\"16\"></ore icon> | <ore icon name=\"check\" size=\"16\"></ore icon> | <ore icon name=\"x\" size=\"16\"></ore icon> |\n\n<div class=\"decision callout\">\n\n**use dnd when** you need reliable file drop zones with mime filtering or sortable lists in a framework agnostic environment.\n\n**consider dnd kit** if you are building a react app and need complex multi container drag interactions or accessibility first sortable trees.\n\n</div>\n\n## installation\n\n::: code group\n\n```sh [pnpm]\npnpm add @vielzeug/dnd\n```\n\n```sh [npm]\nnpm install @vielzeug/dnd\n```\n\n```sh [yarn]\nyarn add @vielzeug/dnd\n```\n\n:::\n\n## quick start\n\n```ts\nimport { createdropzone, createsortable } from '@vielzeug/dnd';\n\n// file drop zone — with async validation and paste support\nconst dropzone = document.getelementbyid('dropzone')!;\n\nusing zone = createdropzone({\n element: dropzone,\n accept: ['image/*', '.pdf'],\n paste: true,\n onvalidate: (files) => files.every((file) => file.size <= 5_000_000),\n ondrop: (files) => console.log('upload', files),\n ondroprejected: (files) => {\n console.warn(`${files.length} file(s) rejected`);\n },\n onhoverchange: (hovered) => {\n dropzone.classlist.toggle('drag over', hovered);\n },\n});\n\n// sortable list — with revert support for optimistic updates\nlet currentorder = ['a', 'b', 'c'];\n\nusing sortable = createsortable({\n element: document.getelementbyid('list')!,\n keyboard: true,\n onbeforereorder: (from, to) => {\n // record positions here before the dom commits (for flip animations)\n },\n getkey: (el) => el.dataset.sortid!,\n onreorder: ({ ids, setrevert }) => {\n const prev = currentorder;\n currentorder = ids;\n setrevert(() => {\n currentorder = prev;\n });\n },\n});\n```\n\n## features\n\n<div class=\"features grid\">\n\n **counter based hover state** — `onhoverchange` stays accurate when dragging over child elements; hover only activates when the drag payload passes the `accept` filter, with symmetric enter/leave pairing to prevent flicker\n **mime type pre validation** — queries `datatransfer.items` during drag to set `dropeffect='none'` before the drop; confirmed against `file.type` on drop\n **flexible accept patterns** — mime types (`image/png`), wildcards (`image/*`), and file extensions (`.pdf`)\n **`maxfiles` limit** — cap the number of accepted files per drop; excess files are forwarded to `ondroprejected`\n **`onvalidate` async gating** — optional cancellable async step after type filtering; `zone.validating` remains `true` until every pending validation settles\n **clipboard paste support** — `paste: true` routes pasted files through the same `accept`, `maxfiles`, and `onvalidate` pipeline; `onpaste` provides a separate callback; paste rejections are forwarded to `ondroprejected` with the same `(files: file[]) => void` signature as drop rejections\n **`ondroprejected`** — separate callback for files that didn't match `accept`, exceeded `maxfiles`, or were rejected by `onvalidate`; event type reflects whether the rejection came from a drop or a paste\n **sortable lists** — reorders dom children with a placeholder indicator; fires `onreorder` only when the order actually changes\n **drag handles** — scope dragging to a child selector via `handle`; whole item is draggable when omitted\n **custom drag preview** — pass an element or a `(id, item, event) => element | null` factory; control hotspot with `dragimageoffset`\n **`onbeforereorder` flip hook** — fires before commit for both drag and keyboard moves; pair it with [`capturelayout()`](/necromancer/api.md#capturelayout) for lifecycle owned flip animation\n **`sortable.revert()`** — register a revert function via `event.setrevert(fn)` inside `onreorder`; `sortable.revert()` invokes it and clears it for rolling back optimistic updates on server failure\n **boundary safe keyboard reordering** — arrow keys at the first/last item no longer suppress `preventdefault`, so the browser can scroll the page normally\n **transactional connected scopes** — one `onmove` callback receives each cross list transfer with both final orders\n **scoped touch support** — `createsortablescope({ touch: true })` handles only items registered to that scope and uses an inert outline preview\n **explicit dom sync** — call `sortable.sync()` after dom mutations instead of relying on hidden observers\n **`[symbol.dispose]`** — both primitives support the `using` keyword for automatic cleanup\n **reactive friendly options** — `disabled` is re read on each event (reassign `options.disabled = true` to toggle); `accept` captures the array reference, so push/splice mutations are reflected without recreating the zone\n **zero dependencies** — <packageinfo package=\"dnd\" type=\"size\" /> gzipped, <packageinfo package=\"dnd\" type=\"dependencies\" /> dependencies\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 [orbit](/orbit/) — floating element positioning; use alongside dnd to anchor drag previews and drop zone indicators to precise positions\n [ore](/ore/) — web component authoring framework; build draggable custom elements with dnd's pointer event primitives\n [refine](/refine/) — accessible web components; dnd powers the drag and drop inside refine's sortable list and kanban components\n\n</div>\n\n<! markdownlint enable md025 md033 md060 >\n",
|
|
347
|
-
"api": " \ntitle: dnd — api reference\ndescription: complete api reference for dnd.\n \n\n[[toc]]\n\n## api overview\n\n| symbol | purpose | execution mode | common gotcha |\n| | | | |\n| `createdropzone()` | create a typed drop zone controller | sync | dispose the controller during teardown |\n| `createsortable()` | add sortable drag and drop behavior to lists | sync | provide stable item identity for reorder operations |\n| `createsortablescope()` | create a shared scope for connected lists | sync | each set of connected containers needs its own scope instance |\n| `applyreorder()` | apply ordered ids to data arrays | sync | unknown ids are skipped; non mentioned items are appended |\n| `dropzoneoptions.accept` | filter file types before processing | sync | mismatch between mime and extension can reject files unexpectedly |\n| `dropzoneoptions.maxfiles` | cap accepted files per drop | sync | excess accepted files become rejected; `ondroprejected` is called |\n| `matchesaccept()` | test a single `file` against an accept list | sync | extension patterns are case insensitive; empty list accepts all |\n| `dnderror` | base class for dnd errors | sync | use `dnderror.is()` to narrow unknown errors |\n\n## package entry point\n\n| import | purpose |\n| | |\n| `@vielzeug/dnd` | main exports and types |\n\n## types\n\n### `disposable`\n\n```ts\ninterface disposable {\n readonly disposed: boolean;\n readonly disposalsignal: abortsignal;\n dispose(): void;\n [symbol.dispose](): void;\n}\n```\n\n### `dropzoneoptions`\n\n```ts\ninterface dropzoneoptions {\n element: htmlelement;\n accept?: string[];\n maxfiles?: number;\n onvalidate?: (files: file[], context: dropvalidationcontext) => boolean | promise<boolean>;\n disabled?: boolean;\n dropeffect?: datatransfer['dropeffect'];\n ondrop?: (files: file[]) => void;\n ondroprejected?: (files: file[]) => void;\n onhoverchange?: (hovered: boolean) => void;\n onvalidatingchange?: (validating: boolean) => void;\n paste?: boolean;\n onpaste?: (files: file[]) => void;\n}\n```\n\n### `dropzone`\n\n```ts\ninterface dropzone extends disposable {\n readonly hovered: boolean;\n readonly validating: boolean;\n}\n```\n\n### `dropvalidationcontext`\n\n```ts\ninterface dropvalidationcontext {\n readonly signal: abortsignal;\n}\n```\n\n### `sortableoptions`\n\n```ts\ninterface sortableoptions {\n element: htmlelement;\n getkey: (element: htmlelement) => string;\n scope?: sortablescope;\n handle?: string;\n keyboard?: boolean;\n axis?: 'vertical' | 'horizontal';\n autoscroll?: boolean | autoscrolloptions;\n dragimage?: htmlelement | ((id: string, item: htmlelement, event: dragevent) => htmlelement | null | undefined);\n dragimageoffset?: [number, number];\n placeholderclass?: string;\n disabled?: boolean;\n ondragstart?: (id: string, event: dragevent) => void;\n ondragend?: (id: string, event: dragevent) => void;\n onbeforereorder?: (from: string[], to: string[]) => void;\n onreorder?: (event: reorderevent) => void;\n}\n```\n\n### `autoscrolloptions`\n\n```ts\ninterface autoscrolloptions {\n edgethreshold?: number;\n speed?: number;\n container?: boolean;\n viewport?: boolean;\n}\n```\n\n### `reorderevent`\n\n```ts\ninterface reorderevent {\n ids: string[];\n setrevert(fn: () => void): void;\n}\n```\n\n### `sortable`\n\n```ts\ninterface sortable extends disposable {\n readonly isdragging: boolean;\n revert(): void;\n sync(): void;\n}\n```\n\n### `sortablescope`\n\n```ts\ninterface sortablescope extends disposable {\n readonly isdragging: boolean;\n revert(): void;\n}\n```\n\n### `sortablescopeoptions`\n\n```ts\ninterface sortablescopeoptions {\n onmove?: (event: sortablemoveevent) => void;\n touch?: boolean | sortabletouchoptions;\n}\n```\n\n### `sortablemoveevent`\n\n```ts\ninterface sortablemoveevent {\n readonly itemid: string;\n readonly source: htmlelement;\n readonly sourceids: string[];\n readonly target: htmlelement;\n readonly targetids: string[];\n setrevert(fn: () => void): void;\n}\n```\n\n### `sortabletouchoptions`\n\n```ts\ninterface sortabletouchoptions {\n preview?: false | ((item: htmlelement) => htmlelement | null);\n}\n```\n\n`preview` returns a template that dnd clones before mounting it as a transient touch preview, so returning an element from the sortable item does not reparent or remove caller owned dom. return `false` to disable the preview.\n\n## `createdropzone()`\n\n```ts\ndeclare function createdropzone(options: dropzoneoptions): dropzone;\n```\n\nattaches drag and drop file handling to a dom element. returns a `dropzone` handle.\n\n| option | type | default | description |\n| | | | |\n| `element` | `htmlelement` | — | **required.** the element to attach drag listeners to. |\n| `accept` | `string[]` | `[]` | accepted file types. empty array accepts everything. each entry is a mime type (`'image/png'`), mime wildcard (`'image/*'`), or file extension (`'.pdf'`). |\n| `maxfiles` | `number` | — | maximum files accepted per drop. files beyond this limit are passed to `ondroprejected`. when omitted there is no limit. |\n| `onvalidate` | `(files, { signal }) => boolean \\| promise<boolean>` | — | optional async gating step. return or resolve `false` to reject all accepted files. `validating` remains true until every operation settles; `signal` aborts on disposal. |\n| `disabled` | `boolean` | — | when `true`, all drag and paste events are ignored. a disabled zone does not call `preventdefault` on `dragenter`, `dragover`, `drop`, or `paste`, so underlying elements (text editors, etc.) receive them normally. |\n| `dropeffect` | `'copy' \\| 'move' \\| 'link' \\| 'none'` | `'copy'` | the `dropeffect` set on `datatransfer` during `dragover`. controls the cursor indicator. |\n| `ondrop` | `(files: file[]) => void` | — | called with accepted files only. not called if all dropped files are rejected. also receives paste events when `paste: true` and `onpaste` is omitted. |\n| `ondroprejected` | `(files: file[]) => void` | — | called with files that did not match `accept`, exceeded `maxfiles`, or were rejected by `onvalidate`. |\n| `onhoverchange` | `(hovered: boolean) => void` | — | called when hover state toggles. use this callback for drag over styling. |\n| `onvalidatingchange` | `(validating: boolean) => void` | — | called whenever the aggregate async validation state changes. |\n| `paste` | `boolean` | `false` | when `true`, attaches a `paste` listener to `window`. pasted files run through the same `accept`, `maxfiles`, and `onvalidate` pipeline as dropped files. |\n| `onpaste` | `(files: file[]) => void` | — | called when files are pasted from the clipboard. falls back to `ondrop` when omitted. only active when `paste: true`. |\n\n**returns:** `dropzone`\n\nnotes:\n\n extension accept patterns are approximate during pre check (`datatransferitem` has no filename); exact filtering is applied at drop time.\n hover state (`hovered`) only becomes `true` when the dragged payload passes the `accept` filter. drags carrying rejected file types enter and leave the zone without triggering `onhoverchange`.\n hover state is reset on element drop and also global `window` `drop`/`dragend` to avoid stuck hover state when drags leave the viewport.\n\n```ts\nconst zone = createdropzone({\n element: dropel,\n accept: ['image/*', '.pdf'],\n ondrop: (files) => {\n upload(files);\n },\n ondroprejected: (files) => {\n showerror(`${files.length} rejected`);\n },\n onhoverchange: (hovered) => {\n dropel.classlist.toggle('drag over', hovered);\n },\n});\n```\n\n## `dropzone` interface\n\n### `zone.hovered`\n\n`readonly hovered: boolean`\n\n`true` when a drag is currently over the zone. updated synchronously by the internal counter — safe to read at any time.\n\n### `zone.validating`\n\n`readonly validating: boolean`\n\n`true` while an `onvalidate` promise is pending. use this to render a loading indicator between file selection and the acceptance/rejection callbacks firing.\n\n```ts\nconsole.log(zone.validating); // true between drop and onvalidate resolution\n```\n\n### `zone.disposed`\n\n`readonly disposed: boolean`\n\n`true` once `dispose()` has been called. safe to read at any time.\n\n### `zone.disposalsignal`\n\n`readonly disposalsignal: abortsignal`\n\nan `abortsignal` that fires when `dispose()` is called. use it to cancel in flight requests tied to the zone's lifetime.\n\n### `zone.dispose()`\n\n`dispose(): void`\n\nremoves all event listeners from the element, resets the drag counter and hover state, and clears the `hovered` flag. idempotent — safe to call multiple times.\n\n```ts\nzone.dispose();\n```\n\n### `zone[symbol.dispose]()`\n\n`[symbol.dispose](): void`\n\nalias for `dispose()`. called automatically when used with the `using` keyword.\n\n```ts\n{\n using zone = createdropzone({ element: dropel, ondrop: handlefiles });\n} // zone.dispose() runs here\n```\n\n## `createsortable()`\n\n```ts\ndeclare function createsortable(options: sortableoptions): sortable;\n```\n\nmakes the direct children of a container element reorderable via drag. returns a `sortable` handle.\n\n`createsortable` adds drag and keyboard defaults only when callers have not already supplied semantics. every changed attribute and inline style is restored to its prior value on disposal.\n\n `element`: `htmlelement`, required. the container whose children become sortable.\n `getkey`: `(element: htmlelement) => string`, required. maps each item element to its stable string identity. children for which `getkey` returns a falsy value are skipped.\n `scope`: `sortablescope`, default private scope. connects sortable lists explicitly; containers only exchange items when they share the same scope instance.\n `handle`: `string`. css selector for a drag handle inside each item. when omitted, the whole item is draggable.\n `keyboard`: `boolean`, default `true`. enables keyboard reordering with arrow keys plus `home` and `end`.\n `axis`: `'vertical' | 'horizontal'`, default `'vertical'`. controls midpoint calculation for placeholder insertion.\n `autoscroll`: `boolean | autoscrolloptions`, default `true`. scrolls the container near its edges; enable viewport scrolling with `autoscroll.viewport`.\n `dragimage`: `htmlelement | ((id, item, event) => htmlelement | null | undefined)`. custom native drag preview passed to `datatransfer.setdragimage()`. a `null` or `undefined` return skips `setdragimage` entirely.\n `dragimageoffset`: `[number, number]`, default `[0, 0]`. the `[x, y]` hotspot offset passed to `setdragimage`. controls which point of the preview image follows the cursor.\n `placeholderclass`: `string`, default `'dnd placeholder'`. css class applied to the generated placeholder element.\n `disabled`: `boolean`. blocks drag interactions. if a list becomes disabled mid drag, dnd cancels the drag and restores the original order.\n `ondragstart`: `(id: string, event: dragevent) => void`. called when a drag starts.\n `ondragend`: `(id: string, event: dragevent) => void`. called when a drag ends, whether completed or cancelled.\n `onbeforereorder`: `(from: string[], to: string[]) => void`. called with the before/after order snapshots just before a successful reorder commits — for both drag and keyboard. items are still in their pre commit positions at the time of the call, making it ideal for [`capturelayout()`](/necromancer/api.md#capturelayout) setup.\n `onreorder`: `(event: reorderevent) => void`. called after a successful reorder (drag or keyboard), only when the order changed. use `event.setrevert(fn)` to register a revert function that `sortable.revert()` will invoke.\n\n**returns:** `sortable`\n\n```ts\nconst boardscope = createsortablescope({\n onmove: ({ itemid, sourceids, targetids }) => savemove(itemid, sourceids, targetids),\n touch: true,\n});\n\nconst sortable = createsortable({\n element: listel,\n getkey: (el) => el.dataset.id!,\n handle: '.drag handle',\n ondragstart: (id) => {\n listel.classlist.add('sorting');\n },\n ondragend: (id) => {\n listel.classlist.remove('sorting');\n },\n onreorder: ({ ids, setrevert }) => {\n const prev = currentorder;\n saveorder(ids);\n setrevert(() => saveorder(prev));\n },\n scope: boardscope,\n});\n```\n\n### `createsortablescope()`\n\n```ts\ndeclare function createsortablescope(options?: sortablescopeoptions): sortablescope;\n```\n\nuse one scope per connected set of containers. `onmove` fires once for cross list moves with both final orders; local reorders continue to call the sortable's `onreorder`.\n\n| parameter | type | description |\n| | | |\n| `options` | `sortablescopeoptions` | optional cross list move callback and scope owned touch configuration |\n\n**returns:** `sortablescope`.\n\n```ts\nimport { createsortablescope } from '@vielzeug/dnd';\n\nconst scope = createsortablescope({\n onmove: ({ itemid, sourceids, targetids }) => {\n persistmove(itemid, sourceids, targetids);\n },\n touch: true,\n});\n```\n\n## `sortable` interface\n\n### `sortable.isdragging`\n\n`readonly isdragging: boolean`\n\n`true` while an item drag is in progress.\n\n### `sortable.revert()`\n\n`revert(): void`\n\ncalls the revert function registered via `setrevert` in the last `onreorder` invocation (if any) and clears it. a no op when no revert function was registered or it has already been consumed. works for both drag based and keyboard based reorders.\n\nonly the most recent reorder can be reverted — a new reorder overwrites the stored function.\n\n```ts\nconst sortable = createsortable({\n element: listel,\n getkey: (el) => el.dataset.sortid!,\n onreorder: ({ ids, setrevert }) => {\n const prev = currentorder;\n setorder(ids);\n setrevert(() => setorder(prev)); // ← enable revert\n },\n});\n\n// on server error:\ntry {\n await api.saveorder(ids);\n} catch {\n sortable.revert();\n}\n```\n\n### `sortable.sync()`\n\n`sync(): void`\n\nre applies `draggable`, `role`, and handle attributes after dom mutations. call it after adding, removing, or replacing sortable children.\n\n### `sortable.disposed`\n\n`readonly disposed: boolean`\n\n`true` once `dispose()` has been called.\n\n### `sortable.disposalsignal`\n\n`readonly disposalsignal: abortsignal`\n\nan `abortsignal` that fires when `dispose()` is called.\n\n### `sortable.dispose()`\n\n`dispose(): void`\n\nremoves all event listeners from the container, strips sortable attributes from items and handles, and cancels any in progress drag by restoring the original order. idempotent — safe to call multiple times.\n\n### `sortable[symbol.dispose]()`\n\n`[symbol.dispose](): void`\n\nalias for `dispose()`.\n\n## `sortablescope` interface\n\n### `scope.isdragging`\n\n`readonly isdragging: boolean`\n\n`true` while any sortable registered to the scope is dragging.\n\n### `scope.revert()`\n\n`revert(): void`\n\ncalls and clears the rollback registered with `sortablemoveevent.setrevert()` for the latest cross list move. it is a no op when no rollback is registered.\n\n### `scope.dispose()`\n\n`dispose(): void`\n\ndisposes scope owned touch input and prevents registered lists from participating in future connected moves.\n\n## dom attributes\n\ndnd reads and writes the following dom attributes:\n\n `data dnd item`: internal marker applied by `createsortable` to children that return a truthy key from `getkey`. restored on `dispose()`.\n `draggable`, roles, tabindex, and `touchaction`: managed only as needed and restored to their exact prior values on `dispose()`.\n `data dragging`: set during drag, removed on `dragend` or `dispose()`. use it as your styling hook for drag state.\n `data dnd handle`: internal marker set by `createsortable` and `sortable.sync()`, removed by `dispose()`. lets dnd clean up only the handle attributes it applied.\n `aria hidden=\"true\"`: set on placeholder creation and removed with the placeholder. applied to the `.dnd placeholder` element.\n `style.touchaction = 'none'` (inline style): set by `createsortable` and `sortable.sync()` on the item (or the handle, when `handle` is set), then restored on `dispose()`.\n\n## css classes\n\n| class | applied to | when |\n| | | |\n| `dnd placeholder` | `<div>` inserted by sortable | while an item is being dragged, in the placeholder's position |\n\n## `matchesaccept()`\n\n```ts\ndeclare function matchesaccept(file: file, accept: string[]): boolean;\n```\n\ntests whether a `file` matches an accept pattern list. each pattern can be:\n\n a mime type: `'image/png'`\n a mime wildcard: `'image/*'`\n a file extension: `'.pdf'`\n\nan empty list accepts everything. extension matching is case insensitive.\n\n**returns:** `true` when the file matches at least one pattern, or when `accept` is empty.\n\n```ts\nimport { matchesaccept } from '@vielzeug/dnd';\n\nmatchesaccept(file, ['image/*', '.pdf']); // true or false\n```\n\n## `applyreorder()`\n\n```ts\ndeclare function applyreorder<t>(items: t[], ids: string[], getkey: (item: t) => string): t[];\n```\n\napplies a dom reorder result (`orderedids`) to your backing array.\n\n ids missing from `items` are ignored.\n items not listed in `ids` are appended in original order.\n duplicate ids in `ids` — first occurrence wins, later occurrences are ignored.\n\n**returns:** a new array ordered by `ids`, with omitted items appended in their original order.\n\n```ts\nconst next = applyreorder(items, orderedids, (item) => item.id);\n```\n\n## errors\n\n| error | trigger | notable property |\n| | | |\n| `dnderror` | base class for package errors | `dnderror.is(error)` |\n| `dndscopeerror` | a sortable receives a scope not created by `createsortablescope()` | — |\n",
|
|
348
|
-
"usage": " \ntitle: dnd — usage guide\ndescription: drop zones, sortable lists, explicit connected scopes, keyboard sorting, and cleanup patterns with dnd.\n \n\n[[toc]]\n\n## basic usage\n\n`createdropzone` attaches drag and drop behavior to any dom element and keeps hover state stable with a counter.\n\n```ts\nimport { createdropzone } from '@vielzeug/dnd';\n\nconst dropzone = document.getelementbyid('dropzone')!;\n\nconst zone = createdropzone({\n element: dropzone,\n ondrop: (files) => {\n console.log('accepted files:', files);\n },\n});\n```\n\n### accept filtering\n\n```ts\nconst zone = createdropzone({\n element: dropel,\n accept: ['image/*', '.pdf', 'application/json'],\n ondrop: (files) => {\n // accepted files only\n },\n ondroprejected: (files) => {\n showtoast(`${files.length} file(s) not accepted`);\n },\n});\n```\n\nthe `accept` list is read at drop time, so mutating the array dynamically adjusts what is accepted for the next drop.\n\n### hover state\n\n```ts\nconst zone = createdropzone({\n element: dropel,\n onhoverchange: (hovered) => {\n dropel.classlist.toggle('drag over', hovered);\n },\n});\n```\n\nread zone state imperatively:\n\n```ts\nconsole.log(zone.hovered);\nconsole.log(zone.validating);\n```\n\n### drop effect\n\n```ts\ncreatedropzone({\n element: dropel,\n dropeffect: 'move',\n ondrop: (files) => {\n // ...\n },\n});\n```\n\n### disabled state\n\n```ts\nconst options = { disabled: false, element: dropel, ondrop: handlefiles };\nconst zone = createdropzone(options);\n\n// options.disabled is read live on each event — mutate to toggle:\noptions.disabled = isreadonly;\n```\n\n### file limit\n\n```ts\nconst zone = createdropzone({\n element: dropel,\n accept: ['image/*'],\n maxfiles: 5,\n ondrop: (files) => {\n // 1 5 accepted files\n },\n ondroprejected: (files) => {\n showtoast(`only 5 files at a time. ${files.length} were ignored.`);\n },\n});\n```\n\n### cleanup\n\n```ts\nzone.dispose();\n// or:\nusing zone = createdropzone({ element: dropel, ondrop: handlefiles });\n```\n\n### async validation\n\ngate drops behind an async check with `onvalidate`. the zone remains `validating: true` until every pending validation settles, and disposal aborts each validation signal.\n\n```ts\nconst zone = createdropzone({\n element: dropel,\n accept: ['image/*'],\n onvalidate: async (files, { signal }) => {\n const ok = await checkserverquota(files, { signal });\n return ok; // false → all files forwarded to ondroprejected\n },\n ondrop: (files) => uploadfiles(files),\n ondroprejected: (files) => showerror('quota exceeded'),\n});\n\n// show a spinner while checking\nconsole.log(zone.validating); // true during pending check\n```\n\na synchronous boolean return skips the microtask queue entirely:\n\n```ts\nconst zone = createdropzone({\n element: dropel,\n onvalidate: (files) => files.every((f) => f.size < 5_000_000), // sync\n ondrop: handlefiles,\n});\n```\n\n### clipboard paste\n\nset `paste: true` to accept files pasted from the clipboard. the same `accept`, `maxfiles`, and `onvalidate` pipeline applies.\n\n```ts\nconst zone = createdropzone({\n element: dropel,\n paste: true,\n accept: ['image/*'],\n onpaste: (files) => {\n uploadfiles(files);\n },\n ondroprejected: (files) => {\n showerror(`${files.length} file(s) not accepted`);\n },\n});\n```\n\nwhen `onpaste` is omitted, accepted pasted files fall through to `ondrop`.\n\n## sortable\n\n`createsortable` makes direct children of a container reorderable via drag.\n\n### setup\n\n```html\n<ul id=\"task list\">\n <li data sort id=\"task 1\">design</li>\n <li data sort id=\"task 2\">develop</li>\n <li data sort id=\"task 3\">review</li>\n</ul>\n```\n\n```ts\nconst sortable = createsortable({\n element: document.getelementbyid('task list')!,\n getkey: (el) => el.dataset.sortid!,\n axis: 'vertical',\n onreorder: ({ ids }) => {\n savetaskorder(ids);\n },\n});\n```\n\ndnd automatically sets:\n\n `draggable=\"true\"` on sortable nodes (or handles)\n `role=\"listitem\"` on each item\n `role=\"list\"` on the container\n `tabindex=\"0\"` on each item for keyboard reordering\n\n### drag handles\n\n```ts\ncreatesortable({\n element: listel,\n getkey: (el) => el.dataset.sortid!,\n handle: '.drag handle',\n onreorder: ({ ids }) => saveorder(ids),\n});\n```\n\n### keyboard reordering\n\nfocus an item and use arrow keys to move it. `home` and `end` move to the boundary positions.\n\nwhen an item is already at the first or last position, the boundary key press is not consumed — the browser handles it normally (for example, scrolling the page). only keys that actually move an item call `preventdefault`.\n\n### connected lists\n\ncreate a shared scope when items should move between containers:\n\n```ts\nconst boardscope = createsortablescope({\n onmove: ({ itemid, sourceids, targetids }) => {\n persistmove(itemid, sourceids, targetids);\n },\n touch: true,\n});\n\ncreatesortable({\n element: todoel,\n getkey: (el) => el.dataset.sortid!,\n scope: boardscope,\n});\ncreatesortable({\n element: doneel,\n getkey: (el) => el.dataset.sortid!,\n scope: boardscope,\n});\n```\n\n### auto scroll and drag preview\n\n```ts\ncreatesortable({\n element: listel,\n getkey: (el) => el.dataset.sortid!,\n autoscroll: { edgethreshold: 40, speed: 24, viewport: true },\n dragimage: (id, item) => item,\n dragimageoffset: [8, 8],\n});\n```\n\nviewport scrolling is opt in. container scrolling stays enabled by default.\n\n### lifecycle hooks\n\n```ts\ncreatesortable({\n element: listel,\n getkey: (el) => el.dataset.sortid!,\n ondragstart: (id) => {\n listel.classlist.add('sorting');\n },\n ondragend: (id) => {\n listel.classlist.remove('sorting');\n },\n onreorder: ({ ids }) => saveorder(ids),\n});\n```\n\n### custom identity function\n\n```ts\ncreatesortable({\n element: listel,\n getkey: (el) => el.getattribute('data id')!,\n onreorder: ({ ids }) => saveorder(ids),\n});\n```\n\n### dynamic lists\n\ncall `sortable.sync()` after adding, removing, or replacing sortable items.\n\n```ts\nconst item = document.createelement('li');\nitem.dataset.sortid = 'task 4';\nitem.textcontent = 'deploy';\nlistel.appendchild(item);\nsortable.sync();\n```\n\n### disabled state\n\n```ts\nimport { createsortable, type sortableoptions } from '@vielzeug/dnd';\n\nconst options: sortableoptions = {\n disabled: false,\n element: listel,\n getkey: (el) => el.dataset.sortid!,\n onreorder: ({ ids }) => saveorder(ids),\n};\nconst sortable = createsortable(options);\n\n// options.disabled is read live on each event — mutate to toggle:\noptions.disabled = islocked;\n```\n\n### placeholder styling\n\n```css\n.dnd placeholder {\n background: var( color primary 50);\n border: 2px dashed var( color primary 300);\n border radius: 4px;\n box sizing: border box;\n}\n\n[data dragging] {\n opacity: 0.35;\n box shadow: 0 4px 12px rgba(0, 0, 0, 0.15);\n}\n```\n\n### mapping dom order back to data\n\n```ts\nimport { applyreorder, createsortable } from '@vielzeug/dnd';\n\nlet items = [\n { id: 'task 1', title: 'design' },\n { id: 'task 2', title: 'develop' },\n { id: 'task 3', title: 'review' },\n];\n\ncreatesortable({\n element: listel,\n getkey: (el) => el.dataset.sortid!,\n onreorder: ({ ids }) => {\n items = applyreorder(items, ids, (item) => item.id);\n },\n});\n```\n\n### cleanup\n\n```ts\nsortable.dispose();\n// or:\nusing sortable = createsortable({\n element: listel,\n getkey: (el) => el.dataset.sortid!,\n onreorder: ({ ids }) => saveorder(ids),\n});\n```\n\n### flip animation hook\n\n`onbeforereorder` fires just before the dom reorder commits, for both drag and keyboard moves. pair it with [`capturelayout()`](/necromancer/api.md#capturelayout) to animate the resulting layout without managing rectangles, transforms, or animation frames yourself.\n\n```ts\nimport { capturelayout, type layouttransition } from '@vielzeug/necromancer';\n\nlet layout: layouttransition | undefined;\n\nconst sortable = createsortable({\n element: listel,\n getkey: (el) => el.dataset.sortid!,\n onbeforereorder: () => {\n layout = capturelayout(listel.queryselectorall('[data sort id]'), {\n getkey: (el) => el.dataset.sortid!,\n });\n },\n onreorder: ({ ids }) => {\n saveorder(ids); // commit a framework render here when needed.\n layout?.animate({\n duration: 200,\n easing: 'ease out',\n elements: listel.queryselectorall('[data sort id]'),\n });\n layout = undefined;\n },\n});\n```\n\nif `saveorder()` triggers a render that replaces list items, call `layout?.animate({ elements: committeditems })` after that render commits. when dnd's own reordered elements remain in the dom, call `layout?.animate()` directly. dnd stays dependency free: the application chooses to install and import necromancer when it wants this integration.\n\n### optimistic updates and revert\n\ncall `sortable.revert()` to roll back the most recent reorder. register a revert function via `setrevert` inside `onreorder`.\n\n```ts\nconst sortable = createsortable({\n element: listel,\n getkey: (el) => el.dataset.sortid!,\n onreorder: ({ ids, setrevert }) => {\n const prev = currentorder;\n setorder(ids); // optimistic update\n setrevert(() => setorder(prev)); // registered for sortable.revert()\n },\n});\n\n// on server error:\ntry {\n await api.saveorder(currentorder);\n} catch {\n sortable.revert();\n}\n```\n\n## touch support\n\nhtml5 drag and drop has no native touch story. enable touch on a sortable scope; it only recognizes items registered to that scope, never unrelated `draggable` elements.\n\n```ts\nimport { createsortable, createsortablescope } from '@vielzeug/dnd';\n\nusing scope = createsortablescope({ touch: true });\nusing sortable = createsortable({ element: listel, getkey: (el) => el.dataset.id!, scope });\n```\n\n### touch preview\n\ntouch uses an inert outline by default, avoiding cloned application dom. provide a preview factory or opt out when your item styling supplies its own feedback.\n\n```ts\nconst scope = createsortablescope({\n touch: {\n // the returned element is cloned before dnd mounts it as a transient preview.\n preview: (item) => item.queryselector<htmlelement>('.drag preview'),\n },\n});\n```\n\n### why draggable items get `touch action: none`\n\n`createsortable` sets `touch action: none` on every element it marks as draggable (the item itself, or the handle when `handle` is set). this prevents a mobile browser from treating the initial movement as page scrolling before the scope controller can start the drag.\n\nthis has no effect on mouse/pointer input.\n\n## testing\n\ntest observable callbacks and controller state with your dom test runner. construct the zone in each test, dispatch a real `drop` event, then dispose it during teardown.\n\n```ts\nimport { aftereach, expect, it, vi } from 'vitest';\nimport { createdropzone } from '@vielzeug/dnd';\n\nconst zones: array<{ dispose(): void }> = [];\n\naftereach(() => zones.splice(0).foreach((zone) => zone.dispose()));\n\nit('forwards accepted files', async () => {\n const element = document.createelement('div');\n const ondrop = vi.fn();\n const zone = createdropzone({ element, ondrop });\n zones.push(zone);\n const file = new file(['content'], 'readme.txt', { type: 'text/plain' });\n const event = new event('drop') as dragevent;\n\n object.defineproperty(event, 'datatransfer', { value: { files: [file] } });\n element.dispatchevent(event);\n\n await promise.resolve();\n\n expect(ondrop).tohavebeencalledwith([file]);\n expect(zone.disposed).tobe(false);\n});\n```\n\n## framework integration\n\n::: code group\n\n```tsx [react]\nimport { useeffect, useref } from 'react';\nimport { createsortable, applyreorder } from '@vielzeug/dnd';\n\nfunction sortablelist({ initialitems }: { initialitems: { id: string; text: string }[] }) {\n const listref = useref<htmlulistelement>(null);\n const items = useref(initialitems);\n\n useeffect(() => {\n const sortable = createsortable({\n element: listref.current!,\n getkey: (el) => el.dataset.sortid!,\n onreorder: ({ ids }) => {\n items.current = applyreorder(items.current, ids, (i) => i.id);\n },\n });\n return () => sortable.dispose();\n }, []);\n\n return (\n <ul ref={listref}>\n {initialitems.map((item) => (\n <li key={item.id} data sort id={item.id}>\n {item.text}\n </li>\n ))}\n </ul>\n );\n}\n```\n\n```ts [vue 3]\nimport { ref, onmounted, onunmounted } from 'vue';\nimport { createsortable, applyreorder, type sortable } from '@vielzeug/dnd';\n\nfunction usesortable(items: { id: string; text: string }[]) {\n const listref = ref<htmlelement | null>(null);\n const ordereditems = ref(items);\n let sortable: sortable | null = null;\n\n onmounted(() => {\n sortable = createsortable({\n element: listref.value!,\n getkey: (el) => el.dataset.sortid!,\n onreorder: ({ ids }) => {\n ordereditems.value = applyreorder(ordereditems.value, ids, (i) => i.id);\n },\n });\n });\n\n onunmounted(() => sortable?.dispose());\n return { listref, ordereditems };\n}\n```\n\n```svelte [svelte]\n<script lang=\"ts\">\n import { onmount } from 'svelte';\n import { createsortable, applyreorder } from '@vielzeug/dnd';\n\n export let initialitems: { id: string; text: string }[] = [];\n let items = initialitems;\n let listel: htmlulistelement;\n\n onmount(() => {\n const sortable = createsortable({\n element: listel,\n getkey: (el) => el.dataset.sortid!,\n onreorder: ({ ids }) => { items = applyreorder(items, ids, (i) => i.id); },\n });\n return () => sortable.dispose();\n });\n</script>\n\n<ul bind:this={listel}>\n {#each items as item (item.id)}\n <li data sort id={item.id}>{item.text}</li>\n {/each}\n</ul>\n```\n\n:::\n\n## working with other vielzeug libraries\n\n### with ore\n\nuse dnd in custom web components by attaching behavior in component lifecycle hooks.\n\n```ts\nimport { createsortable } from '@vielzeug/dnd';\nimport { define, gethost, html, onmounted } from '@vielzeug/ore';\n\ndefine('task list', {\n setup(_props) {\n const el = gethost();\n\n onmounted(() => {\n const sortable = createsortable({\n element: el,\n getkey: (el) => el.dataset.sortid!,\n onreorder: ({ ids }) => save(ids),\n });\n return () => sortable.dispose();\n });\n\n return html`<slot></slot>`;\n },\n});\n```\n\n## best practices\n\n attach `createdropzone` and `createsortable` after the container element is in the dom — use `onmounted` in component frameworks.\n call `.dispose()` in the cleanup phase of your framework (useeffect return, onunmounted, ondestroy) to prevent memory leaks.\n use `data sort id` attributes that match your data's identity field — do not use dom index as an identifier.\n prefer `applyreorder()` over manual array splicing to keep your data array in sync with dom order.\n use `createsortablescope()` only when items should genuinely move between containers.\n use drag handles (`.handle` selector) when the full item surface area conflicts with other interactions such as text selection.\n test keyboard reordering explicitly — dnd sets `tabindex` on items and supports arrow keys by default.\n enable `touch: true` only on scopes that own touch sortable lists.\n",
|
|
347
|
+
"api": " \ntitle: dnd — api reference\ndescription: complete api reference for dnd.\n \n\n[[toc]]\n\n## api overview\n\n| symbol | purpose | execution mode | common gotcha |\n| | | | |\n| `createdropzone()` | create a typed drop zone controller | sync | dispose the controller during teardown |\n| `createsortable()` | add sortable drag and drop behavior to lists | sync | provide stable item identity for reorder operations |\n| `createsortablescope()` | create a shared scope for connected lists | sync | each set of connected containers needs its own scope instance |\n| `applyreorder()` | apply ordered ids to data arrays | sync | unknown ids are skipped; non mentioned items are appended |\n| `dropzoneoptions.accept` | filter file types before processing | sync | mismatch between mime and extension can reject files unexpectedly |\n| `dropzoneoptions.maxfiles` | cap accepted files per drop | sync | excess accepted files become rejected; `ondroprejected` is called |\n| `matchesaccept()` | test a single `file` against an accept list | sync | extension patterns are case insensitive; empty list accepts all |\n| `dnderror` | base class for dnd errors | sync | use `dnderror.is()` to narrow unknown errors |\n\n## package entry point\n\n| import | purpose |\n| | |\n| `@vielzeug/dnd` | main exports and types |\n\n## types\n\n### `disposable`\n\n```ts\ninterface disposable {\n readonly disposed: boolean;\n readonly disposalsignal: abortsignal;\n dispose(): void;\n [symbol.dispose](): void;\n}\n```\n\n### `dropzoneoptions`\n\n```ts\ninterface dropzoneoptions {\n element: htmlelement;\n accept?: string[];\n maxfiles?: number;\n onvalidate?: (files: file[], context: dropvalidationcontext) => boolean | promise<boolean>;\n disabled?: boolean;\n dropeffect?: datatransfer['dropeffect'];\n ondrop?: (files: file[]) => void;\n ondroprejected?: (files: file[]) => void;\n onhoverchange?: (hovered: boolean) => void;\n onvalidatingchange?: (validating: boolean) => void;\n paste?: boolean;\n onpaste?: (files: file[]) => void;\n}\n```\n\n### `dropzone`\n\n```ts\ninterface dropzone extends disposable {\n readonly hovered: boolean;\n readonly validating: boolean;\n}\n```\n\n### `dropvalidationcontext`\n\n```ts\ninterface dropvalidationcontext {\n readonly signal: abortsignal;\n}\n```\n\n### `sortableoptions`\n\n```ts\ninterface sortableoptions {\n element: htmlelement;\n getkey: (element: htmlelement) => string;\n scope?: sortablescope;\n handle?: string;\n keyboard?: boolean;\n axis?: 'vertical' | 'horizontal';\n autoscroll?: boolean | autoscrolloptions;\n dragimage?: htmlelement | ((id: string, item: htmlelement, event: dragevent) => htmlelement | null | undefined);\n dragimageoffset?: [number, number];\n placeholderclass?: string;\n disabled?: boolean;\n ondragstart?: (id: string, event: dragevent) => void;\n ondragend?: (id: string, event: dragevent) => void;\n onbeforereorder?: (from: string[], to: string[]) => void;\n onreorder?: (event: reorderevent) => void;\n}\n```\n\n### `autoscrolloptions`\n\n```ts\ninterface autoscrolloptions {\n edgethreshold?: number;\n speed?: number;\n container?: boolean;\n viewport?: boolean;\n}\n```\n\n### `reorderevent`\n\n```ts\ninterface reorderevent {\n ids: string[];\n setrevert(fn: () => void): void;\n}\n```\n\n### `sortable`\n\n```ts\ninterface sortable extends disposable {\n readonly isdragging: boolean;\n revert(): void;\n sync(): void;\n}\n```\n\n### `sortablescope`\n\n```ts\ninterface sortablescope extends disposable {\n readonly isdragging: boolean;\n revert(): void;\n}\n```\n\n### `sortablescopeoptions`\n\n```ts\ninterface sortablescopeoptions {\n onmove?: (event: sortablemoveevent) => void;\n touch?: boolean | sortabletouchoptions;\n}\n```\n\n### `sortablemoveevent`\n\n```ts\ninterface sortablemoveevent {\n readonly itemid: string;\n readonly source: htmlelement;\n readonly sourceids: string[];\n readonly target: htmlelement;\n readonly targetids: string[];\n setrevert(fn: () => void): void;\n}\n```\n\n### `sortabletouchoptions`\n\n```ts\ninterface sortabletouchoptions {\n preview?: false | ((item: htmlelement) => htmlelement | null);\n}\n```\n\n`preview` returns a template that dnd clones before mounting it as a transient touch preview, so returning an element from the sortable item does not reparent or remove caller owned dom. return `false` to disable the preview.\n\ntouch sorting tracks the initiating touch by identifier. secondary touches are ignored, and cancellation of the initiating touch restores the pre drag order without firing `onreorder`.\n\n## `createdropzone()`\n\n```ts\ndeclare function createdropzone(options: dropzoneoptions): dropzone;\n```\n\nattaches drag and drop file handling to a dom element. returns a `dropzone` handle.\n\n| option | type | default | description |\n| | | | |\n| `element` | `htmlelement` | — | **required.** the element to attach drag listeners to. |\n| `accept` | `string[]` | `[]` | accepted file types. empty array accepts everything. each entry is a mime type (`'image/png'`), mime wildcard (`'image/*'`), or file extension (`'.pdf'`). |\n| `maxfiles` | `number` | — | maximum files accepted per drop. files beyond this limit are passed to `ondroprejected`. when omitted there is no limit. |\n| `onvalidate` | `(files, { signal }) => boolean \\| promise<boolean>` | — | optional async gating step. return or resolve `false` to reject all accepted files. `validating` remains true until every operation settles; `signal` aborts on disposal. |\n| `disabled` | `boolean` | — | when `true`, all drag and paste events are ignored. a disabled zone does not call `preventdefault` on `dragenter`, `dragover`, `drop`, or `paste`, so underlying elements (text editors, etc.) receive them normally. |\n| `dropeffect` | `'copy' \\| 'move' \\| 'link' \\| 'none'` | `'copy'` | the `dropeffect` set on `datatransfer` during `dragover`. controls the cursor indicator. |\n| `ondrop` | `(files: file[]) => void` | — | called with accepted files only. not called if all dropped files are rejected. also receives paste events when `paste: true` and `onpaste` is omitted. |\n| `ondroprejected` | `(files: file[]) => void` | — | called with files that did not match `accept`, exceeded `maxfiles`, or were rejected by `onvalidate`. |\n| `onhoverchange` | `(hovered: boolean) => void` | — | called when hover state toggles. use this callback for drag over styling. |\n| `onvalidatingchange` | `(validating: boolean) => void` | — | called whenever the aggregate async validation state changes. |\n| `paste` | `boolean` | `false` | when `true`, attaches a `paste` listener to `window`. pasted files run through the same `accept`, `maxfiles`, and `onvalidate` pipeline as dropped files. |\n| `onpaste` | `(files: file[]) => void` | — | called when files are pasted from the clipboard. falls back to `ondrop` when omitted. only active when `paste: true`. |\n\n**returns:** `dropzone`\n\nnotes:\n\n extension accept patterns are approximate during pre check (`datatransferitem` has no filename); exact filtering is applied at drop time.\n hover state (`hovered`) only becomes `true` when the dragged payload passes the `accept` filter. drags carrying rejected file types enter and leave the zone without triggering `onhoverchange`.\n hover state is reset on element drop and also global `window` `drop`/`dragend` to avoid stuck hover state when drags leave the viewport.\n\n```ts\nconst zone = createdropzone({\n element: dropel,\n accept: ['image/*', '.pdf'],\n ondrop: (files) => {\n upload(files);\n },\n ondroprejected: (files) => {\n showerror(`${files.length} rejected`);\n },\n onhoverchange: (hovered) => {\n dropel.classlist.toggle('drag over', hovered);\n },\n});\n```\n\n## `dropzone` interface\n\n### `zone.hovered`\n\n`readonly hovered: boolean`\n\n`true` when a drag is currently over the zone. updated synchronously by the internal counter — safe to read at any time.\n\n### `zone.validating`\n\n`readonly validating: boolean`\n\n`true` while an `onvalidate` promise is pending. use this to render a loading indicator between file selection and the acceptance/rejection callbacks firing.\n\n```ts\nconsole.log(zone.validating); // true between drop and onvalidate resolution\n```\n\n### `zone.disposed`\n\n`readonly disposed: boolean`\n\n`true` once `dispose()` has been called. safe to read at any time.\n\n### `zone.disposalsignal`\n\n`readonly disposalsignal: abortsignal`\n\nan `abortsignal` that fires when `dispose()` is called. use it to cancel in flight requests tied to the zone's lifetime.\n\n### `zone.dispose()`\n\n`dispose(): void`\n\nremoves all event listeners from the element, resets the drag counter and hover state, and clears the `hovered` flag. idempotent — safe to call multiple times.\n\n```ts\nzone.dispose();\n```\n\n### `zone[symbol.dispose]()`\n\n`[symbol.dispose](): void`\n\nalias for `dispose()`. called automatically when used with the `using` keyword.\n\n```ts\n{\n using zone = createdropzone({ element: dropel, ondrop: handlefiles });\n} // zone.dispose() runs here\n```\n\n## `createsortable()`\n\n```ts\ndeclare function createsortable(options: sortableoptions): sortable;\n```\n\nmakes the direct children of a container element reorderable via drag. returns a `sortable` handle.\n\n`createsortable` adds drag and keyboard defaults only when callers have not already supplied semantics. every changed attribute and inline style is restored to its prior value on disposal.\n\n `element`: `htmlelement`, required. the container whose children become sortable.\n `getkey`: `(element: htmlelement) => string`, required. maps each item element to its stable string identity. children for which `getkey` returns a falsy value are skipped.\n `scope`: `sortablescope`, default private scope. connects sortable lists explicitly; containers only exchange items when they share the same scope instance.\n `handle`: `string`. css selector for a drag handle inside each item. when omitted, the whole item is draggable.\n `keyboard`: `boolean`, default `true`. enables keyboard reordering with arrow keys plus `home` and `end`.\n `axis`: `'vertical' | 'horizontal'`, default `'vertical'`. controls midpoint calculation for placeholder insertion.\n `autoscroll`: `boolean | autoscrolloptions`, default `true`. scrolls the container near its edges; enable viewport scrolling with `autoscroll.viewport`.\n `dragimage`: `htmlelement | ((id, item, event) => htmlelement | null | undefined)`. custom native drag preview passed to `datatransfer.setdragimage()`. a `null` or `undefined` return skips `setdragimage` entirely.\n `dragimageoffset`: `[number, number]`, default `[0, 0]`. the `[x, y]` hotspot offset passed to `setdragimage`. controls which point of the preview image follows the cursor.\n `placeholderclass`: `string`, default `'dnd placeholder'`. css class applied to the generated placeholder element.\n `disabled`: `boolean`. blocks drag interactions. if a list becomes disabled mid drag, dnd cancels the drag and restores the original order.\n `ondragstart`: `(id: string, event: dragevent) => void`. called when a drag starts.\n `ondragend`: `(id: string, event: dragevent) => void`. called when a drag ends, whether completed or cancelled.\n `onbeforereorder`: `(from: string[], to: string[]) => void`. called with the before/after order snapshots just before a successful reorder commits — for both drag and keyboard. items are still in their pre commit positions at the time of the call, making it ideal for [`capturelayout()`](/necromancer/api.md#capturelayout) setup.\n `onreorder`: `(event: reorderevent) => void`. called after a successful reorder (drag or keyboard), only when the order changed. use `event.setrevert(fn)` to register a revert function that `sortable.revert()` will invoke.\n\n**returns:** `sortable`\n\n```ts\nconst boardscope = createsortablescope({\n onmove: ({ itemid, sourceids, targetids }) => savemove(itemid, sourceids, targetids),\n touch: true,\n});\n\nconst sortable = createsortable({\n element: listel,\n getkey: (el) => el.dataset.id!,\n handle: '.drag handle',\n ondragstart: (id) => {\n listel.classlist.add('sorting');\n },\n ondragend: (id) => {\n listel.classlist.remove('sorting');\n },\n onreorder: ({ ids, setrevert }) => {\n const prev = currentorder;\n saveorder(ids);\n setrevert(() => saveorder(prev));\n },\n scope: boardscope,\n});\n```\n\n### `createsortablescope()`\n\n```ts\ndeclare function createsortablescope(options?: sortablescopeoptions): sortablescope;\n```\n\nuse one scope per connected set of containers. `onmove` fires once for cross list moves with both final orders; local reorders continue to call the sortable's `onreorder`.\n\n| parameter | type | description |\n| | | |\n| `options` | `sortablescopeoptions` | optional cross list move callback and scope owned touch configuration |\n\n**returns:** `sortablescope`.\n\n```ts\nimport { createsortablescope } from '@vielzeug/dnd';\n\nconst scope = createsortablescope({\n onmove: ({ itemid, sourceids, targetids }) => {\n persistmove(itemid, sourceids, targetids);\n },\n touch: true,\n});\n```\n\n## `sortable` interface\n\n### `sortable.isdragging`\n\n`readonly isdragging: boolean`\n\n`true` while an item drag is in progress.\n\n### `sortable.revert()`\n\n`revert(): void`\n\ncalls the revert function registered via `setrevert` in the last `onreorder` invocation (if any) and clears it. a no op when no revert function was registered or it has already been consumed. works for both drag based and keyboard based reorders.\n\nonly the most recent reorder can be reverted — a new reorder overwrites the stored function.\n\n```ts\nconst sortable = createsortable({\n element: listel,\n getkey: (el) => el.dataset.sortid!,\n onreorder: ({ ids, setrevert }) => {\n const prev = currentorder;\n setorder(ids);\n setrevert(() => setorder(prev)); // ← enable revert\n },\n});\n\n// on server error:\ntry {\n await api.saveorder(ids);\n} catch {\n sortable.revert();\n}\n```\n\n### `sortable.sync()`\n\n`sync(): void`\n\nre applies `draggable`, `role`, and handle attributes after dom mutations. call it after adding, removing, or replacing sortable children.\n\n### `sortable.disposed`\n\n`readonly disposed: boolean`\n\n`true` once `dispose()` has been called.\n\n### `sortable.disposalsignal`\n\n`readonly disposalsignal: abortsignal`\n\nan `abortsignal` that fires when `dispose()` is called.\n\n### `sortable.dispose()`\n\n`dispose(): void`\n\nremoves all event listeners from the container, strips sortable attributes from items and handles, and cancels any in progress drag by restoring the original order. idempotent — safe to call multiple times.\n\n### `sortable[symbol.dispose]()`\n\n`[symbol.dispose](): void`\n\nalias for `dispose()`.\n\n## `sortablescope` interface\n\n### `scope.isdragging`\n\n`readonly isdragging: boolean`\n\n`true` while any sortable registered to the scope is dragging.\n\n### `scope.revert()`\n\n`revert(): void`\n\ncalls and clears the rollback registered with `sortablemoveevent.setrevert()` for the latest cross list move. it is a no op when no rollback is registered.\n\n### `scope.dispose()`\n\n`dispose(): void`\n\ndisposes scope owned touch input and prevents registered lists from participating in future connected moves.\n\n## dom attributes\n\ndnd reads and writes the following dom attributes:\n\n `data dnd item`: internal marker applied by `createsortable` to children that return a truthy key from `getkey`. restored on `dispose()`.\n `draggable`, roles, tabindex, and `touchaction`: managed only as needed and restored to their exact prior values on `dispose()`.\n `data dragging`: set during drag, removed on `dragend` or `dispose()`. use it as your styling hook for drag state.\n `data dnd handle`: internal marker set by `createsortable` and `sortable.sync()`, removed by `dispose()`. lets dnd clean up only the handle attributes it applied.\n `aria hidden=\"true\"`: set on placeholder creation and removed with the placeholder. applied to the `.dnd placeholder` element.\n `style.touchaction = 'none'` (inline style): set by `createsortable` and `sortable.sync()` on the item (or the handle, when `handle` is set), then restored on `dispose()`.\n\n## css classes\n\n| class | applied to | when |\n| | | |\n| `dnd placeholder` | `<div>` inserted by sortable | while an item is being dragged, in the placeholder's position |\n\n## `matchesaccept()`\n\n```ts\ndeclare function matchesaccept(file: file, accept: string[]): boolean;\n```\n\ntests whether a `file` matches an accept pattern list. each pattern can be:\n\n a mime type: `'image/png'`\n a mime wildcard: `'image/*'`\n a file extension: `'.pdf'`\n\nan empty list accepts everything. extension matching is case insensitive.\n\n**returns:** `true` when the file matches at least one pattern, or when `accept` is empty.\n\n```ts\nimport { matchesaccept } from '@vielzeug/dnd';\n\nmatchesaccept(file, ['image/*', '.pdf']); // true or false\n```\n\n## `applyreorder()`\n\n```ts\ndeclare function applyreorder<t>(items: t[], ids: string[], getkey: (item: t) => string): t[];\n```\n\napplies a dom reorder result (`orderedids`) to your backing array.\n\n ids missing from `items` are ignored.\n items not listed in `ids` are appended in original order.\n duplicate ids in `ids` — first occurrence wins, later occurrences are ignored.\n\n**returns:** a new array ordered by `ids`, with omitted items appended in their original order.\n\n```ts\nconst next = applyreorder(items, orderedids, (item) => item.id);\n```\n\n## errors\n\n| error | trigger | notable property |\n| | | |\n| `dnderror` | base class for package errors | `dnderror.is(error)` |\n| `dndscopeerror` | a sortable receives a scope not created by `createsortablescope()` | — |\n",
|
|
348
|
+
"usage": " \ntitle: dnd — usage guide\ndescription: drop zones, sortable lists, explicit connected scopes, keyboard sorting, and cleanup patterns with dnd.\n \n\n[[toc]]\n\n## basic usage\n\n`createdropzone` attaches drag and drop behavior to any dom element and keeps hover state stable with a counter.\n\n```ts\nimport { createdropzone } from '@vielzeug/dnd';\n\nconst dropzone = document.getelementbyid('dropzone')!;\n\nconst zone = createdropzone({\n element: dropzone,\n ondrop: (files) => {\n console.log('accepted files:', files);\n },\n});\n```\n\n### accept filtering\n\n```ts\nconst zone = createdropzone({\n element: dropel,\n accept: ['image/*', '.pdf', 'application/json'],\n ondrop: (files) => {\n // accepted files only\n },\n ondroprejected: (files) => {\n showtoast(`${files.length} file(s) not accepted`);\n },\n});\n```\n\nthe `accept` list is read at drop time, so mutating the array dynamically adjusts what is accepted for the next drop.\n\n### hover state\n\n```ts\nconst zone = createdropzone({\n element: dropel,\n onhoverchange: (hovered) => {\n dropel.classlist.toggle('drag over', hovered);\n },\n});\n```\n\nread zone state imperatively:\n\n```ts\nconsole.log(zone.hovered);\nconsole.log(zone.validating);\n```\n\n### drop effect\n\n```ts\ncreatedropzone({\n element: dropel,\n dropeffect: 'move',\n ondrop: (files) => {\n // ...\n },\n});\n```\n\n### disabled state\n\n```ts\nconst options = { disabled: false, element: dropel, ondrop: handlefiles };\nconst zone = createdropzone(options);\n\n// options.disabled is read live on each event — mutate to toggle:\noptions.disabled = isreadonly;\n```\n\n### file limit\n\n```ts\nconst zone = createdropzone({\n element: dropel,\n accept: ['image/*'],\n maxfiles: 5,\n ondrop: (files) => {\n // 1 5 accepted files\n },\n ondroprejected: (files) => {\n showtoast(`only 5 files at a time. ${files.length} were ignored.`);\n },\n});\n```\n\n### cleanup\n\n```ts\nzone.dispose();\n// or:\nusing zone = createdropzone({ element: dropel, ondrop: handlefiles });\n```\n\n### async validation\n\ngate drops behind an async check with `onvalidate`. the zone remains `validating: true` until every pending validation settles, and disposal aborts each validation signal.\n\n```ts\nconst zone = createdropzone({\n element: dropel,\n accept: ['image/*'],\n onvalidate: async (files, { signal }) => {\n const ok = await checkserverquota(files, { signal });\n return ok; // false → all files forwarded to ondroprejected\n },\n ondrop: (files) => uploadfiles(files),\n ondroprejected: (files) => showerror('quota exceeded'),\n});\n\n// show a spinner while checking\nconsole.log(zone.validating); // true during pending check\n```\n\na synchronous boolean return skips the microtask queue entirely:\n\n```ts\nconst zone = createdropzone({\n element: dropel,\n onvalidate: (files) => files.every((f) => f.size < 5_000_000), // sync\n ondrop: handlefiles,\n});\n```\n\n### clipboard paste\n\nset `paste: true` to accept files pasted from the clipboard. the same `accept`, `maxfiles`, and `onvalidate` pipeline applies.\n\n```ts\nconst zone = createdropzone({\n element: dropel,\n paste: true,\n accept: ['image/*'],\n onpaste: (files) => {\n uploadfiles(files);\n },\n ondroprejected: (files) => {\n showerror(`${files.length} file(s) not accepted`);\n },\n});\n```\n\nwhen `onpaste` is omitted, accepted pasted files fall through to `ondrop`.\n\n## sortable\n\n`createsortable` makes direct children of a container reorderable via drag.\n\n### setup\n\n```html\n<ul id=\"task list\">\n <li data sort id=\"task 1\">design</li>\n <li data sort id=\"task 2\">develop</li>\n <li data sort id=\"task 3\">review</li>\n</ul>\n```\n\n```ts\nconst sortable = createsortable({\n element: document.getelementbyid('task list')!,\n getkey: (el) => el.dataset.sortid!,\n axis: 'vertical',\n onreorder: ({ ids }) => {\n savetaskorder(ids);\n },\n});\n```\n\ndnd automatically sets:\n\n `draggable=\"true\"` on sortable nodes (or handles)\n `role=\"listitem\"` on each item\n `role=\"list\"` on the container\n `tabindex=\"0\"` on each item for keyboard reordering\n\n### drag handles\n\n```ts\ncreatesortable({\n element: listel,\n getkey: (el) => el.dataset.sortid!,\n handle: '.drag handle',\n onreorder: ({ ids }) => saveorder(ids),\n});\n```\n\n### keyboard reordering\n\nfocus an item and use arrow keys to move it. `home` and `end` move to the boundary positions.\n\nwhen an item is already at the first or last position, the boundary key press is not consumed — the browser handles it normally (for example, scrolling the page). only keys that actually move an item call `preventdefault`.\n\n### connected lists\n\ncreate a shared scope when items should move between containers:\n\n```ts\nconst boardscope = createsortablescope({\n onmove: ({ itemid, sourceids, targetids }) => {\n persistmove(itemid, sourceids, targetids);\n },\n touch: true,\n});\n\ncreatesortable({\n element: todoel,\n getkey: (el) => el.dataset.sortid!,\n scope: boardscope,\n});\ncreatesortable({\n element: doneel,\n getkey: (el) => el.dataset.sortid!,\n scope: boardscope,\n});\n```\n\n### auto scroll and drag preview\n\n```ts\ncreatesortable({\n element: listel,\n getkey: (el) => el.dataset.sortid!,\n autoscroll: { edgethreshold: 40, speed: 24, viewport: true },\n dragimage: (id, item) => item,\n dragimageoffset: [8, 8],\n});\n```\n\nviewport scrolling is opt in. container scrolling stays enabled by default.\n\n### lifecycle hooks\n\n```ts\ncreatesortable({\n element: listel,\n getkey: (el) => el.dataset.sortid!,\n ondragstart: (id) => {\n listel.classlist.add('sorting');\n },\n ondragend: (id) => {\n listel.classlist.remove('sorting');\n },\n onreorder: ({ ids }) => saveorder(ids),\n});\n```\n\n### custom identity function\n\n```ts\ncreatesortable({\n element: listel,\n getkey: (el) => el.getattribute('data id')!,\n onreorder: ({ ids }) => saveorder(ids),\n});\n```\n\n### dynamic lists\n\ncall `sortable.sync()` after adding, removing, or replacing sortable items.\n\n```ts\nconst item = document.createelement('li');\nitem.dataset.sortid = 'task 4';\nitem.textcontent = 'deploy';\nlistel.appendchild(item);\nsortable.sync();\n```\n\n### disabled state\n\n```ts\nimport { createsortable, type sortableoptions } from '@vielzeug/dnd';\n\nconst options: sortableoptions = {\n disabled: false,\n element: listel,\n getkey: (el) => el.dataset.sortid!,\n onreorder: ({ ids }) => saveorder(ids),\n};\nconst sortable = createsortable(options);\n\n// options.disabled is read live on each event — mutate to toggle:\noptions.disabled = islocked;\n```\n\n### placeholder styling\n\n```css\n.dnd placeholder {\n background: var( color primary 50);\n border: 2px dashed var( color primary 300);\n border radius: 4px;\n box sizing: border box;\n}\n\n[data dragging] {\n opacity: 0.35;\n box shadow: 0 4px 12px rgba(0, 0, 0, 0.15);\n}\n```\n\n### mapping dom order back to data\n\n```ts\nimport { applyreorder, createsortable } from '@vielzeug/dnd';\n\nlet items = [\n { id: 'task 1', title: 'design' },\n { id: 'task 2', title: 'develop' },\n { id: 'task 3', title: 'review' },\n];\n\ncreatesortable({\n element: listel,\n getkey: (el) => el.dataset.sortid!,\n onreorder: ({ ids }) => {\n items = applyreorder(items, ids, (item) => item.id);\n },\n});\n```\n\n### cleanup\n\n```ts\nsortable.dispose();\n// or:\nusing sortable = createsortable({\n element: listel,\n getkey: (el) => el.dataset.sortid!,\n onreorder: ({ ids }) => saveorder(ids),\n});\n```\n\n### flip animation hook\n\n`onbeforereorder` fires just before the dom reorder commits, for both drag and keyboard moves. pair it with [`capturelayout()`](/necromancer/api.md#capturelayout) to animate the resulting layout without managing rectangles, transforms, or animation frames yourself.\n\n```ts\nimport { capturelayout, type layouttransition } from '@vielzeug/necromancer';\n\nlet layout: layouttransition | undefined;\n\nconst sortable = createsortable({\n element: listel,\n getkey: (el) => el.dataset.sortid!,\n onbeforereorder: () => {\n layout = capturelayout(listel.queryselectorall('[data sort id]'), {\n getkey: (el) => el.dataset.sortid!,\n });\n },\n onreorder: ({ ids }) => {\n saveorder(ids); // commit a framework render here when needed.\n layout?.animate({\n duration: 200,\n easing: 'ease out',\n elements: listel.queryselectorall('[data sort id]'),\n });\n layout = undefined;\n },\n});\n```\n\nif `saveorder()` triggers a render that replaces list items, call `layout?.animate({ elements: committeditems })` after that render commits. when dnd's own reordered elements remain in the dom, call `layout?.animate()` directly. dnd stays dependency free: the application chooses to install and import necromancer when it wants this integration.\n\n### optimistic updates and revert\n\ncall `sortable.revert()` to roll back the most recent reorder. register a revert function via `setrevert` inside `onreorder`.\n\n```ts\nconst sortable = createsortable({\n element: listel,\n getkey: (el) => el.dataset.sortid!,\n onreorder: ({ ids, setrevert }) => {\n const prev = currentorder;\n setorder(ids); // optimistic update\n setrevert(() => setorder(prev)); // registered for sortable.revert()\n },\n});\n\n// on server error:\ntry {\n await api.saveorder(currentorder);\n} catch {\n sortable.revert();\n}\n```\n\n## touch support\n\nhtml5 drag and drop has no native touch story. enable touch on a sortable scope; it only recognizes items registered to that scope, never unrelated `draggable` elements.\n\n```ts\nimport { createsortable, createsortablescope } from '@vielzeug/dnd';\n\nusing scope = createsortablescope({ touch: true });\nusing sortable = createsortable({ element: listel, getkey: (el) => el.dataset.id!, scope });\n```\n\nthe scope tracks the touch that initiated the drag by its identifier. additional fingers cannot move, finish, or replace the active drag. if the initiating touch is cancelled, dnd restores the original item order and removes the transient preview.\n\n### touch preview\n\ntouch uses an inert outline by default, avoiding cloned application dom. provide a preview factory or opt out when your item styling supplies its own feedback.\n\n```ts\nconst scope = createsortablescope({\n touch: {\n // the returned element is cloned before dnd mounts it as a transient preview.\n preview: (item) => item.queryselector<htmlelement>('.drag preview'),\n },\n});\n```\n\n### why draggable items get `touch action: none`\n\n`createsortable` sets `touch action: none` on every element it marks as draggable (the item itself, or the handle when `handle` is set). this prevents a mobile browser from treating the initial movement as page scrolling before the scope controller can start the drag.\n\nthis has no effect on mouse/pointer input.\n\n## testing\n\ntest observable callbacks and controller state with your dom test runner. construct the zone in each test, dispatch a real `drop` event, then dispose it during teardown.\n\n```ts\nimport { aftereach, expect, it, vi } from 'vitest';\nimport { createdropzone } from '@vielzeug/dnd';\n\nconst zones: array<{ dispose(): void }> = [];\n\naftereach(() => zones.splice(0).foreach((zone) => zone.dispose()));\n\nit('forwards accepted files', async () => {\n const element = document.createelement('div');\n const ondrop = vi.fn();\n const zone = createdropzone({ element, ondrop });\n zones.push(zone);\n const file = new file(['content'], 'readme.txt', { type: 'text/plain' });\n const event = new event('drop') as dragevent;\n\n object.defineproperty(event, 'datatransfer', { value: { files: [file] } });\n element.dispatchevent(event);\n\n await promise.resolve();\n\n expect(ondrop).tohavebeencalledwith([file]);\n expect(zone.disposed).tobe(false);\n});\n```\n\n## framework integration\n\n::: code group\n\n```tsx [react]\nimport { useeffect, useref } from 'react';\nimport { createsortable, applyreorder } from '@vielzeug/dnd';\n\nfunction sortablelist({ initialitems }: { initialitems: { id: string; text: string }[] }) {\n const listref = useref<htmlulistelement>(null);\n const items = useref(initialitems);\n\n useeffect(() => {\n const sortable = createsortable({\n element: listref.current!,\n getkey: (el) => el.dataset.sortid!,\n onreorder: ({ ids }) => {\n items.current = applyreorder(items.current, ids, (i) => i.id);\n },\n });\n return () => sortable.dispose();\n }, []);\n\n return (\n <ul ref={listref}>\n {initialitems.map((item) => (\n <li key={item.id} data sort id={item.id}>\n {item.text}\n </li>\n ))}\n </ul>\n );\n}\n```\n\n```ts [vue 3]\nimport { ref, onmounted, onunmounted } from 'vue';\nimport { createsortable, applyreorder, type sortable } from '@vielzeug/dnd';\n\nfunction usesortable(items: { id: string; text: string }[]) {\n const listref = ref<htmlelement | null>(null);\n const ordereditems = ref(items);\n let sortable: sortable | null = null;\n\n onmounted(() => {\n sortable = createsortable({\n element: listref.value!,\n getkey: (el) => el.dataset.sortid!,\n onreorder: ({ ids }) => {\n ordereditems.value = applyreorder(ordereditems.value, ids, (i) => i.id);\n },\n });\n });\n\n onunmounted(() => sortable?.dispose());\n return { listref, ordereditems };\n}\n```\n\n```svelte [svelte]\n<script lang=\"ts\">\n import { onmount } from 'svelte';\n import { createsortable, applyreorder } from '@vielzeug/dnd';\n\n export let initialitems: { id: string; text: string }[] = [];\n let items = initialitems;\n let listel: htmlulistelement;\n\n onmount(() => {\n const sortable = createsortable({\n element: listel,\n getkey: (el) => el.dataset.sortid!,\n onreorder: ({ ids }) => { items = applyreorder(items, ids, (i) => i.id); },\n });\n return () => sortable.dispose();\n });\n</script>\n\n<ul bind:this={listel}>\n {#each items as item (item.id)}\n <li data sort id={item.id}>{item.text}</li>\n {/each}\n</ul>\n```\n\n:::\n\n## working with other vielzeug libraries\n\n### with ore\n\nuse dnd in custom web components by attaching behavior in component lifecycle hooks.\n\n```ts\nimport { createsortable } from '@vielzeug/dnd';\nimport { define, gethost, html, onmounted } from '@vielzeug/ore';\n\ndefine('task list', {\n setup(_props) {\n const el = gethost();\n\n onmounted(() => {\n const sortable = createsortable({\n element: el,\n getkey: (el) => el.dataset.sortid!,\n onreorder: ({ ids }) => save(ids),\n });\n return () => sortable.dispose();\n });\n\n return html`<slot></slot>`;\n },\n});\n```\n\n## best practices\n\n attach `createdropzone` and `createsortable` after the container element is in the dom — use `onmounted` in component frameworks.\n call `.dispose()` in the cleanup phase of your framework (useeffect return, onunmounted, ondestroy) to prevent memory leaks.\n use `data sort id` attributes that match your data's identity field — do not use dom index as an identifier.\n prefer `applyreorder()` over manual array splicing to keep your data array in sync with dom order.\n use `createsortablescope()` only when items should genuinely move between containers.\n use drag handles (`.handle` selector) when the full item surface area conflicts with other interactions such as text selection.\n test keyboard reordering explicitly — dnd sets `tabindex` on items and supports arrow keys by default.\n enable `touch: true` only on scopes that own touch sortable lists.\n",
|
|
349
349
|
"examples": " \ntitle: dnd — examples\ndescription: practical examples and recipes for dnd.\n \n\n## examples\n\n [sortable list](./examples/sortable list.md)\n [touch enabled sortable list](./examples/touch enabled sortable list.md)\n [file upload drop zone](./examples/file upload drop zone.md)\n [optimistic reorder with revert and flip animation](./examples/optimistic reorder with revert.md)\n [combined sortable with inline editing](./examples/combined sortable with inline editing.md)\n [connected kanban keyboard sorting](./examples/connected kanban keyboard sorting.md)\n [web component with ore](./examples/web component with craft.md)\n [using `using` for scoped cleanup](./examples/using using for scoped cleanup.md)\n"
|
|
350
350
|
},
|
|
351
351
|
"examples": [
|
|
@@ -461,13 +461,39 @@
|
|
|
461
461
|
"slug": "flux",
|
|
462
462
|
"source": "export { toasynciterable } from './async';\nexport { stream } from './core';\nexport { fluxerror, fluxtimeouterror } from './errors';\nexport { combinelatest, concat, merge } from './operators/combination';\nexport type { intervaloptions, timeroptions } from './operators/creation';\nexport { from, fromevent, interval, of, timer } from './operators/creation';\nexport type { debounceoptions, timeoutoptions } from './operators/filtering';\nexport { debounce, take, takeuntil, timeout } from './operators/filtering';\nexport type { concatmapoptions } from './operators/transformation';\nexport { concatmap, filter, map, mergemap, scan, switchmap } from './operators/transformation';\nexport type { retryoptions, toarrayoptions, valueoptions } from './operators/utility';\nexport { first, last, retry, toarray } from './operators/utility';\nexport { pipe } from './pipe';\nexport type {\n asynciterableoptions,\n observer,\n operator,\n overflowpolicy,\n producer,\n sink,\n stream,\n subscribeoptions,\n subscription,\n teardown,\n} from './types';\n"
|
|
463
463
|
},
|
|
464
|
+
{
|
|
465
|
+
"category": "input",
|
|
466
|
+
"description": "framework neutral list navigation and focus restoration primitives.",
|
|
467
|
+
"docs": {
|
|
468
|
+
"index": " \ntitle: focus — navigation and restoration\ndescription: framework neutral list navigation and focus restoration primitives.\npackage: focus\ncategory: input\nkeywords: [focus, roving, keyboard, accessibility, list navigation]\nexports: [createlistnavigation, capturefocus, restorefocus]\nrelated: [refine, keymap, ore]\nenvironments: [browser]\n \n\n<! markdownlint disable md025 md033 md060 >\n\n<packagehero package=\"focus\" />\n\n## why focus?\n\ncomposite widgets need consistent keyboard navigation and predictable return focus behavior. focus centralizes those primitives without coupling to component rendering or framework state.\n\n```ts\n// before\nlist.addeventlistener('keydown', (event) => {\n // arrow/home/end bookkeeping, disabled filtering, wrapping\n});\n\n// after\nconst nav = createlistnavigation({ getitems, onnavigate: ({ item }) => item.focus() });\nlist.addeventlistener('keydown', nav.handlekeydown);\n```\n\n| feature | per component navigation | focus |\n| | | |\n| bundle size | n/a | <packageinfo package=\"focus\" type=\"size\" /> |\n| zero dependencies | n/a | <ore icon name=\"check\" size=\"16\"></ore icon> |\n| rtl mirroring | manual | built in |\n| typeahead | manual | optional via `typeahead` |\n| focus restoration | manual capture | `capturefocus()` / `restorefocus()` |\n\n<div class=\"decision callout\">\n\n**use focus when** a widget needs arrow key navigation, home/end, and controlled focus restoration.\n\n**consider direct focus calls when** interaction is a single isolated element with no composite navigation.\n\n</div>\n\n## installation\n\n::: code group\n\n```sh [pnpm]\npnpm add @vielzeug/focus\n```\n\n```sh [npm]\nnpm install @vielzeug/focus\n```\n\n```sh [yarn]\nyarn add @vielzeug/focus\n```\n\n:::\n\n## quick start\n\n```ts\nimport { capturefocus, createlistnavigation } from '@vielzeug/focus';\n\nconst restore = capturefocus();\nconst nav = createlistnavigation({\n getitems: () => items,\n loop: true,\n onnavigate: ({ item }) => item.focus(),\n});\n\ncontainer.addeventlistener('keydown', nav.handlekeydown);\n\nrestore();\nnav.dispose();\n```\n\n## features\n\n<div class=\"features grid\">\n\n `createlistnavigation()` — reusable composite widget keyboard navigation\n orientation and direction support — vertical/horizontal/both with ltr/rtl defaults\n dynamic item queries — disabled filtering and loop control\n optional typeahead — label based navigation in key driven lists\n `capturefocus()` and `restorefocus()` — explicit return focus helpers\n\n</div>\n\n## documentation\n\n<div class=\"doc links\">\n\n [usage guide](./usage.md)\n [api reference](./api.md)\n [examples](./examples.md)\n\n</div>\n\n## see also\n\n<div class=\"see also\">\n\n [refine](/refine/) — component primitives integrating list navigation.\n [keymap](/keymap/) — global and scoped keyboard shortcuts.\n [ore](/ore/) — lifecycle ownership used by consumer components.\n\n</div>\n\n<! markdownlint enable md025 md033 md060 >\n",
|
|
469
|
+
"api": " \ntitle: focus — api reference\ndescription: api reference for @vielzeug/focus navigation and restoration primitives.\n \n\n[[toc]]\n\n## api overview\n\n| symbol | purpose | execution mode | common gotcha |\n| | | | |\n| `createlistnavigation()` | build keyboard navigation for composite widgets | sync | disabled items require an explicit predicate |\n| `restorefocus()` | restore focus to a target or fallback | sync | returns `false` when neither target can receive focus |\n| `capturefocus()` | capture active focus for one later restoration | sync | the returned function is one shot |\n\n## package entry point\n\n| import | purpose |\n| | |\n| `@vielzeug/focus` | list navigation and focus restoration primitives. |\n\n## core functions\n\n### `createlistnavigation()`\n\n```ts\nfunction createlistnavigation<t>(options: listnavigationoptions<t>): listnavigation<t>;\n```\n\ncreates a keyboard navigation controller with an internal active index.\n\n| parameter | type | description |\n| | | |\n| `options` | `listnavigationoptions<t>` | item lookup, key mapping, navigation, typeahead, and lifecycle options. |\n\n**returns:** `listnavigation<t>`.\n\n**example**\n\n```ts\nimport { createlistnavigation } from '@vielzeug/focus';\n\nconst nav = createlistnavigation({\n getitems: () => rows,\n isitemdisabled: (item) => item.matches('[aria disabled=\"true\"]'),\n onnavigate: ({ item }) => item.focus(),\n});\n```\n\n| member | return | contract |\n| | | |\n| `handlekeydown(event)` | `boolean` | handles configured navigation keys and optional typeahead. |\n| `navigate(action)` | `number` | moves programmatically and returns the active index, or ` 1`. |\n| `set(index)` | `number` | sets the active index when usable, or resets it to ` 1`. |\n| `reset()` | `void` | clears the active index and typeahead sequence. |\n| `getindex()` | `number` | returns the current usable index, or ` 1`. |\n| `getactiveitem()` | `t \\| undefined` | returns the item at the current usable index. |\n| `dispose()` | `void` | permanently disables the controller and aborts `disposalsignal`. |\n| `disposed` | `boolean` | indicates whether the controller is permanently disabled. |\n| `disposalsignal` | `abortsignal` | aborts when the controller is disposed. |\n| `[symbol.dispose]()` | `void` | calls `dispose()`. |\n\n \n\n### `restorefocus()`\n\n```ts\nfunction restorefocus(target: focustarget, options?: restorefocusoptions): boolean;\n```\n\nattempts to focus a connected target that is neither disabled nor inert.\n\n| parameter | type | description |\n| | | |\n| `target` | `focustarget` | element or getter resolved when `restorefocus()` is called. |\n| `options` | `restorefocusoptions` | optional lazy fallback and `preventscroll` flag. |\n\n**returns:** `boolean` — `true` when focus moved to the target or fallback.\n\n**example**\n\n```ts\nimport { restorefocus } from '@vielzeug/focus';\n\nrestorefocus(() => triggerelement, {\n fallback: () => document.body,\n preventscroll: true,\n});\n```\n\n \n\n### `capturefocus()`\n\n```ts\nfunction capturefocus(options?: capturefocusoptions): focusrestorer;\n```\n\ncaptures the deepest active element immediately and returns a one shot restoration function.\n\n| parameter | type | description |\n| | | |\n| `options` | `capturefocusoptions` | optional lazy fallback, `preventscroll`, and cancellation signal. |\n\n**returns:** `focusrestorer`. its first call attempts restoration; later calls return `false`.\n\n**example**\n\n```ts\nimport { capturefocus } from '@vielzeug/focus';\n\nconst restore = capturefocus({ fallback: () => document.body });\n\ndialog.showmodal();\ndialog.addeventlistener('close', restore, { once: true });\n```\n\n## types\n\n```ts\ntype maybegetter<t> = t | (() => t);\n\ntype listnavigationaction = 'first' | 'last' | 'next' | 'prev';\ntype listkeyaction = listnavigationaction | 'typeahead';\n\ntype listnavigationchange<t> = {\n action: listkeyaction;\n event?: keyboardevent;\n index: number;\n item: t;\n};\n\ntype listnavigationtypeaheadoptions<t> = {\n delayms?: number;\n getlabel: (item: t, index: number) => string;\n};\n\ntype listnavigationoptions<t> = {\n direction?: maybegetter<'ltr' | 'rtl'>;\n disabled?: maybegetter<boolean | undefined>;\n getitems: () => readonly t[];\n isitemdisabled?: (item: t, index: number) => boolean;\n keys?: partial<record<listnavigationaction, readonly string[]>>;\n loop?: boolean;\n onnavigate?: (change: listnavigationchange<t>) => void;\n orientation?: maybegetter<'both' | 'horizontal' | 'vertical'>;\n signal?: abortsignal;\n typeahead?: listnavigationtypeaheadoptions<t>;\n};\n\ntype listnavigation<t> = {\n readonly disposalsignal: abortsignal;\n readonly disposed: boolean;\n dispose(): void;\n getactiveitem(): t | undefined;\n getindex(): number;\n handlekeydown(event: keyboardevent): boolean;\n navigate(action: listnavigationaction): number;\n reset(): void;\n set(index: number): number;\n [symbol.dispose](): void;\n};\n\ntype focustarget = htmlelement | svgelement | null | undefined | (() => htmlelement | svgelement | null | undefined);\n\ntype restorefocusoptions = {\n fallback?: focustarget;\n preventscroll?: boolean;\n};\n\ntype capturefocusoptions = restorefocusoptions & {\n signal?: abortsignal;\n};\n\ntype focusrestorer = () => boolean;\n```\n\n`typeahead.delayms` defaults to `500`. non finite or non positive values use the default.\n\n## errors\n\n`@vielzeug/focus` does not export custom error classes.\n",
|
|
470
|
+
"usage": " \ntitle: focus — usage guide\ndescription: build keyboard focus navigation and restoration into composite widgets.\n \n\n[[toc]]\n\n## basic usage\n\ncreate one navigation handle for a composite widget and forward `keydown` events to it.\n\n```ts\nimport { createlistnavigation } from '@vielzeug/focus';\n\nconst nav = createlistnavigation({\n getitems: () => items,\n loop: true,\n onnavigate: ({ item }) => item.focus(),\n});\n\nlist.addeventlistener('keydown', nav.handlekeydown);\n```\n\n## orientation and direction\n\nuse orientation and direction to derive default key bindings.\n\n```ts\nconst nav = createlistnavigation({\n direction: () => (document.dir === 'rtl' ? 'rtl' : 'ltr'),\n getitems: () => tabs,\n orientation: 'horizontal',\n});\n```\n\n## disabled and dynamic items\n\nprovide `isitemdisabled` when disabled state is data driven.\n\n```ts\nconst nav = createlistnavigation({\n getitems: () => rows,\n isitemdisabled: (item) => item.hasattribute('aria disabled'),\n});\n```\n\n## typeahead\n\nenable character based navigation with the `typeahead` option.\n\n```ts\nconst nav = createlistnavigation({\n getitems: () => menuitems,\n typeahead: {\n delayms: 300,\n getlabel: (item) => item.textcontent ?? '',\n },\n});\n```\n\n`typeahead.delayms` defaults to `500`. repeated characters cycle matching items without waiting for the timeout.\n\n## focus restoration\n\ncapture focus before opening a floating surface and restore it after closing.\n\n```ts\nimport { capturefocus } from '@vielzeug/focus';\n\nconst restore = capturefocus();\n\nopendialog();\nclosedialog();\nrestore();\n```\n\n## framework integration\n\ncreate the navigation handle once per component instance and dispose it on unmount. the handle is framework neutral — wire `keydown` from whatever element owns the composite widget's keyboard surface.\n\n::: code group\n\n```tsx [react]\nimport { useeffect, useref } from 'react';\nimport { createlistnavigation } from '@vielzeug/focus';\n\nfunction tabs({ tabs }: { tabs: array<{ id: string; label: string }> }) {\n const listref = useref<htmldivelement>(null);\n const tabrefs = useref<array<htmlbuttonelement | null>>([]);\n\n useeffect(() => {\n const list = listref.current;\n if (!list) return;\n\n const nav = createlistnavigation({\n getitems: () => tabrefs.current.filter((el): el is htmlbuttonelement => el !== null),\n loop: true,\n onnavigate: ({ item }) => item.focus(),\n orientation: 'horizontal',\n });\n\n list.addeventlistener('keydown', nav.handlekeydown);\n return () => {\n list.removeeventlistener('keydown', nav.handlekeydown);\n nav.dispose();\n };\n }, []);\n\n return (\n <div ref={listref} role=\"tablist\">\n {tabs.map((tab, i) => (\n <button\n key={tab.id}\n ref={(el) => { tabrefs.current[i] = el; }}\n role=\"tab\"\n >\n {tab.label}\n </button>\n ))}\n </div>\n );\n}\n```\n\n```vue [vue 3]\n<script setup lang=\"ts\">\nimport { onmounted, onunmounted, ref } from 'vue';\nimport { createlistnavigation } from '@vielzeug/focus';\n\nconst props = defineprops<{ tabs: array<{ id: string; label: string }> }>();\n\nconst listel = ref<htmldivelement | null>(null);\nconst tabels = ref<array<htmlbuttonelement | null>>([]);\n\nlet nav: returntype<typeof createlistnavigation> | undefined;\n\nonmounted(() => {\n if (!listel.value) return;\n\n nav = createlistnavigation({\n getitems: () => tabels.value.filter((el): el is htmlbuttonelement => el !== null),\n loop: true,\n onnavigate: ({ item }) => item.focus(),\n orientation: 'horizontal',\n });\n\n listel.value.addeventlistener('keydown', nav.handlekeydown);\n});\n\nonunmounted(() => {\n if (nav) listel.value?.removeeventlistener('keydown', nav.handlekeydown);\n nav?.dispose();\n});\n</script>\n\n<template>\n <div ref=\"listel\" role=\"tablist\">\n <button\n v for=\"(tab, i) in tabs\"\n :key=\"tab.id\"\n :ref=\"(el) => { tabels[i] = el as htmlbuttonelement | null; }\"\n role=\"tab\"\n >\n {{ tab.label }}\n </button>\n </div>\n</template>\n```\n\n```svelte [svelte]\n<script lang=\"ts\">\n import { onmount } from 'svelte';\n import { createlistnavigation } from '@vielzeug/focus';\n\n let { tabs }: { tabs: array<{ id: string; label: string }> } = $props();\n\n let listel: htmldivelement;\n let tabels: htmlbuttonelement[] = [];\n\n onmount(() => {\n const nav = createlistnavigation({\n getitems: () => tabels,\n loop: true,\n onnavigate: ({ item }) => item.focus(),\n orientation: 'horizontal',\n });\n\n listel.addeventlistener('keydown', nav.handlekeydown);\n return () => {\n listel.removeeventlistener('keydown', nav.handlekeydown);\n nav.dispose();\n };\n });\n</script>\n\n<div bind:this={listel} role=\"tablist\">\n {#each tabs as tab, i}\n <button bind:this={tabels[i]} role=\"tab\">{tab.label}</button>\n {/each}\n</div>\n```\n\n:::\n\n## working with other vielzeug libraries\n\n### focus + refine\n\nrefine's `ore menu`, `ore dialog`, and `ore list` use focus internally for keyboard navigation and focus restoration. when building custom composite widgets on top of refine components, use `createlistnavigation` for the keyboard layer and let refine handle rendering.\n\n```ts\nimport { createlistnavigation } from '@vielzeug/focus';\n\n// custom tab bar built alongside ore tab panels\nconst tabnav = createlistnavigation({\n getitems: () => array.from(host.queryselectorall('[role=\"tab\"]')),\n loop: true,\n onnavigate: ({ item }) => item.focus(),\n orientation: 'horizontal',\n});\n\nhost.addeventlistener('keydown', tabnav.handlekeydown);\n```\n\n### focus + keymap\n\nuse keymap for global shortcuts and focus for composite widget navigation. they operate on different event layers without conflict.\n\n```ts\nimport { createkeymap } from '@vielzeug/keymap';\nimport { createlistnavigation } from '@vielzeug/focus';\n\nconst nav = createlistnavigation({ getitems: () => items, onnavigate: ({ item }) => item.focus() });\n\nconst map = createkeymap({\n 'mod+k': () => openpalette(),\n escape: () => nav.reset(),\n});\n\nlist.addeventlistener('keydown', nav.handlekeydown);\nmap.mount(document);\n```\n\n## best practices\n\n **keep** item discovery in one function.\n **drive** focus side effects from `onnavigate`.\n **reset** navigation on overlay close when focus context changes.\n **use** typeahead only when labels are stable and meaningful.\n **capture** return focus before opening transient surfaces.\n **dispose** handles when owners unmount.\n",
|
|
471
|
+
"examples": " \ntitle: focus — examples\ndescription: worked examples for @vielzeug/focus.\n \n\n## examples\n\n [roving tabs keyboard navigation](./examples/roving tabs keyboard navigation.md)\n [dialog return focus restoration](./examples/dialog return focus restoration.md)\n"
|
|
472
|
+
},
|
|
473
|
+
"examples": [
|
|
474
|
+
{
|
|
475
|
+
"id": "list-navigation",
|
|
476
|
+
"text": "list navigation import { createlistnavigation } from '@vielzeug/focus'\n\nconst labels = ['apple', 'banana', 'cherry']\nconst list = document.createelement('div')\nlist.setattribute('role', 'listbox')\n\nconst items = labels.map((label, index) => {\n const item = document.createelement('button')\n item.textcontent = label\n item.disabled = index === 1\n item.tabindex = index === 0 ? 0 : 1\n list.appendchild(item)\n return item\n})\n\ndocument.body.appendchild(list)\n\nconst navigation = createlistnavigation({\n getitems: () => items,\n isitemdisabled: (item) => item.disabled,\n loop: true,\n onnavigate: ({ item }) => {\n items.foreach((candidate) => {\n candidate.tabindex = candidate === item ? 0 : 1\n })\n item.focus()\n },\n})\n\nnavigation.set(0)\nlist.addeventlistener('keydown', navigation.handlekeydown)\nitems[0].focus()\nitems[0].dispatchevent(new keyboardevent('keydown', { bubbles: true, key: 'arrowdown' }))\n\nconsole.log(document.activeelement?.textcontent) // 'cherry'"
|
|
477
|
+
},
|
|
478
|
+
{
|
|
479
|
+
"id": "restore-focus",
|
|
480
|
+
"text": "restore captured focus import { capturefocus } from '@vielzeug/focus'\n\nconst trigger = document.createelement('button')\ntrigger.textcontent = 'open dialog'\n\nconst dialogbutton = document.createelement('button')\ndialogbutton.textcontent = 'close dialog'\n\ndocument.body.append(trigger, dialogbutton)\ntrigger.focus()\n\nconst restore = capturefocus()\ndialogbutton.focus()\n\nconsole.log(restore()) // true\nconsole.log(document.activeelement === trigger) // true\nconsole.log(restore()) // false: restorers are one shot"
|
|
481
|
+
}
|
|
482
|
+
],
|
|
483
|
+
"exports": "createlistnavigation capturefocus restorefocus",
|
|
484
|
+
"keywords": "focus roving keyboard accessibility list navigation",
|
|
485
|
+
"name": "@vielzeug/focus",
|
|
486
|
+
"related": "refine keymap ore",
|
|
487
|
+
"slug": "focus",
|
|
488
|
+
"source": "export type {\n listkeyaction,\n listnavigation,\n listnavigationaction,\n listnavigationchange,\n listnavigationoptions,\n listnavigationtypeaheadoptions,\n maybegetter,\n} from './list navigation';\nexport { createlistnavigation } from './list navigation';\nexport type {\n capturefocusoptions,\n focusrestorer,\n focustarget,\n restorefocusoptions,\n} from './restore focus';\nexport { capturefocus, restorefocus } from './restore focus';\n"
|
|
489
|
+
},
|
|
464
490
|
{
|
|
465
491
|
"category": "forms",
|
|
466
492
|
"description": "framework agnostic immutable form state with focused object fields and explicit validation results.",
|
|
467
493
|
"docs": {
|
|
468
|
-
"index": " \ntitle: forge — immutable form state for typescript\ndescription: framework agnostic immutable form state with focused object fields and explicit validation results.\npackage: forge\ncategory: forms\nkeywords: [form state, validation, immutable, input, submission]\nrelated: [spell, vault, courier]\nexports: [createform,
|
|
469
|
-
"api": " \ntitle: forge — api reference\ndescription: complete reference for immutable forms, fields, validation, serialization, and optional adapters.\n \n\n[[toc]]\n\n## api overview\n\n| symbol | purpose | execution mode | common gotcha |\n| | | | |\n| `createform()` | create immutable form state | sync | `initialvalues` cannot contain mutable class instances |\n| `form.field()` | select a top level or object child field | sync | arrays have no index field handles |\n| `form.validate()` | validate complete value | async | handle `aborted` separately |\n| `form.submit()` | touch, validate, then invoke handler | async | concurrent calls reject |\n| `form.reset()` | restore or replace baseline | sync | `reset(next)` makes `next` clean |\n| `form.subscribe()` | observe form metadata | sync | throws after disposal |\n| `toformdata()` | serialize values for multipart transport | sync | `filelist` is transport only |\n| `bindfield()` | bind one dom element | sync | does not schedule validation |\n| `customvalidator()` | adapt a spell schema | async | does not transform `form.value` |\n| `saveform()` / `loadform()` | persist explicit vault records | async | formdraftcodec owns record shape |\n\n## package entry point\n\n| import | purpose |\n| | |\n| `@vielzeug/forge` | core form factory, serialization helper, types, and errors |\n| `@vielzeug/forge/dom` | `bindfield()` and dom binding types |\n| `@vielzeug/forge/spell` | `customvalidator()` |\n| `@vielzeug/forge/vault` | `saveform()`, `loadform()`, and `formdraftcodec` |\n\n## core functions\n\n### `createform(options)`\n\n```ts\nfunction createform<tvalues extends record<string, unknown>>(options: formoptions<tvalues>): form<tvalues>;\n```\n\ncreates a form with immutable initial values and an optional full form validator.\n\n| parameter | type | description |\n| | | |\n| `options.initialvalues` | `tvalues` | initial value and reset baseline. supports primitives, plain objects, arrays, `file`, and `blob`. |\n| `options.validate` | `formvalidator<tvalues>` | optional validator for the entire current value. |\n| `options.onsubscribererror` | `(error: unknown) => void` | optional subscriber failure reporter. |\n\n**returns:** `form<tvalues>`.\n\n**example:**\n\n```ts\nimport { createform } from '@vielzeug/forge';\n\nconst form = createform({ initialvalues: { email: '' } });\n```\n\n \n\n### `toformdata(values)`\n\n```ts\nfunction toformdata(values: record<string, unknown>): formdata;\n```\n\nconverts nested values into `formdata` with dot separated object keys and repeated array keys.\n\n**returns:** a populated `formdata` instance.\n\n**example:**\n\n```ts\nimport { toformdata } from '@vielzeug/forge';\n\nconst body = toformdata({ profile: { email: 'ada@example.com' }, tags: ['typescript', 'forms'] });\n```\n\n## form handles\n\n### `form<tvalues>`\n\n`createform()` returns this handle.\n\n| member | signature | description |\n| | | |\n| `value` | `readonlydeep<tvalues>` | current immutable value. |\n| `state` | `formstate<tvalues>` | submission, validation, touch, and error metadata. |\n| `field(key)` | `field<tvalues[k]>` | select a top level field. |\n| `set(next)` | `void` | replace the complete value or derive a replacement. |\n| `reset(next?)` | `void` | restore baseline or make `next` the baseline. |\n| `validate(signal?)` | `promise<validationresult<tvalues>>` | run full form validation. |\n| `submit(handler)` | `promise<submitresult<tresult, tvalues>>` | touch, validate, and invoke handler when valid. |\n| `subscribe(listener, options?)` | `unsubscribe` | observe form state; throws after disposal. |\n| `dispose()` | `void` | abort validation and clear subscribers. |\n| `disposed` | `boolean` | whether the form has been disposed. |\n| `disposalsignal` | `abortsignal` | aborts on disposal. |\n\n### `field<v>`\n\n`form.field(key)` and object field `.field(key)` return this handle.\n\n| member | signature | description |\n| | | |\n| `value` | `readonlydeep<v>` | current immutable branch value. |\n| `error` | `string \\| undefined` | current field error. |\n| `dirty` | `boolean` | whether branch differs from baseline. |\n| `touched` | `boolean` | whether field was touched. |\n| `field(key)` | `field<v[k]>` | select child object field only. |\n| `set(next)` | `void` | replace branch or derive a replacement. |\n| `reset()` | `void` | restore exact baseline branch. |\n| `touch()` | `void` | mark field touched. |\n| `subscribe(listener, options?)` | `unsubscribe` | observe field transitions; throws after disposal. |\n\n## validation results\n\n### `form.validate(signal?)`\n\n```ts\nfunction validate(signal?: abortsignal): promise<validationresult<tvalues>>;\n```\n\nruns the configured validator against the complete value. a newer validation aborts the older run.\n\n**returns:** `validationresult<tvalues>`.\n\n```ts\nconst result = await form.validate();\n\nif (result.status === 'invalid') console.log(result.errors, result.formerror);\n```\n\n### `form.submit(handler)`\n\n```ts\nfunction submit<tresult = void>(handler: (values: readonlydeep<tvalues>) => maybepromise<tresult>): promise<submitresult<tresult, tvalues>>;\n```\n\ntouches all fields, validates once, and invokes `handler` when validation is valid.\n\n**returns:** `submitresult<tresult, tvalues>`. handler failures reject normally.\n\n```ts\nconst result = await form.submit((value) => promise.resolve(value));\n```\n\n## adapters\n\n### `bindfield(element, field, options)`\n\n```ts\nfunction bindfield<element extends htmlelement, v>(\n element: element,\n field: field<v>,\n options: fieldbindingoptions<element, v>,\n): () => void;\n```\n\nbinds one field to one element, marks it touched on blur, suppresses writeback from its own input event, and returns teardown.\n\n**example:**\n\n```ts\nimport { bindfield } from '@vielzeug/forge/dom';\n\nconst stop = bindfield(input, form.field('email'), {\n read: (element) => element.value,\n write: (element, value) => {\n element.value = value;\n },\n});\n```\n\n \n\n### `customvalidator(schema)`\n\n```ts\nfunction customvalidator<tvalues extends record<string, unknown>>(\n schema: schema<unknown, tvalues, schemamode>,\n): formvalidator<tvalues>;\n```\n\nadapts a spell schema. every failing union maps its closest branch while preserving unrelated errors. array item issues map to the parent array field; duplicate paths retain the first message.\n\n**example:**\n\n```ts\nimport { customvalidator } from '@vielzeug/forge/spell';\nimport { s } from '@vielzeug/spell';\n\nconst profile = s.object({ email: s.string().email() });\nconst form = createform({ initialvalues: { email: '' }, validate: customvalidator(profile) });\n```\n\n \n\n### `saveform()` and `loadform()`\n\n```ts\nfunction saveform<tvalues extends record<string, unknown>, s extends anyschema, k extends keyof s & string>(\n form: form<tvalues>, adapter: vaultstore<s>, table: k, codec: formdraftcodec<tvalues, s, k>,\n): promise<void>;\n\nfunction loadform<tvalues extends record<string, unknown>, s extends anyschema, k extends keyof s & string>(\n form: form<tvalues>, adapter: vaultstore<s>, table: k, key: keyof<s, k>, codec: formdraftcodec<tvalues, s, k>,\n): promise<boolean>;\n```\n\npersists or restores a codec defined vault record. `loadform()` calls `form.reset()` when the codec decodes a record.\n\n**returns:** `loadform()` returns `false` for a missing or rejected record.\n\n## types\n\n```ts\ntype unsubscribe = () => void;\ntype maybepromise<t> = t | promiselike<t>;\ntype readonlydeep<t> = t extends (...args: never[]) => unknown\n ? t\n : t extends readonly (infer item)[]\n ? readonly readonlydeep<item>[]\n : t extends record<string, unknown>\n ? { readonly [k in keyof t]: readonlydeep<t[k]> }\n : t;\n\ntype formerrors<t> = t extends readonly unknown[]\n ? string\n : t extends record<string, unknown>\n ? string | { readonly [k in keyof t]?: formerrors<t[k]> }\n : string;\n\ntype validationerrors<tvalues extends record<string, unknown>> = readonly<{\n fields?: formerrors<tvalues>;\n formerror?: string;\n}>;\n\ntype formvalidator<tvalues extends record<string, unknown>> = (\n values: readonlydeep<tvalues>, signal: abortsignal,\n) => maybepromise<validationerrors<tvalues> | undefined>;\n\ntype formoptions<tvalues extends record<string, unknown>> = readonly<{\n initialvalues: tvalues;\n onsubscribererror?: (error: unknown) => void;\n validate?: formvalidator<noinfer<tvalues>>;\n}>;\n\ntype subscribeoptions = readonly<{ immediate?: boolean }>;\n\ntype fieldstate<v> = readonly<{\n dirty: boolean;\n error: string | undefined;\n touched: boolean;\n value: readonlydeep<v>;\n}>;\n\ntype formstate<tvalues extends record<string, unknown>> = readonly<{\n error: string | undefined;\n errors: formerrors<tvalues> | undefined;\n submitcount: number;\n submitting: boolean;\n touched: boolean;\n valid: boolean;\n validating: boolean;\n}>;\n\ntype validationresult<tvalues extends record<string, unknown>> =\n | readonly<{ status: 'aborted' }>\n | readonly<{ status: 'valid' }>\n | readonly<{ errors: formerrors<tvalues> | undefined; formerror: string | undefined; status: 'invalid' }>;\n\ntype submitresult<tresult = void, tvalues extends record<string, unknown> = record<string, unknown>> =\n | readonly<{ ok: true; value: tresult }>\n | readonly<{ ok: false; type: 'aborted' }>\n | readonly<{ errors: formerrors<tvalues> | undefined; formerror: string | undefined; ok: false; type: 'validation' }>;\n```\n\n```ts\ntype field<v> = {\n readonly dirty: boolean;\n readonly error: string | undefined;\n readonly touched: boolean;\n readonly value: readonlydeep<v>;\n field<k extends keyof nonnullable<v> & string>(key: k): field<nonnullable<v>[k]>;\n reset(): void;\n set(next: v | ((previous: readonlydeep<v>) => v)): void;\n subscribe(listener: (state: fieldstate<v>) => void, options?: subscribeoptions): unsubscribe;\n touch(): void;\n};\n\ntype form<tvalues extends record<string, unknown>> = {\n [symbol.dispose](): void;\n readonly disposalsignal: abortsignal;\n readonly disposed: boolean;\n readonly state: formstate<tvalues>;\n readonly value: readonlydeep<tvalues>;\n dispose(): void;\n field<k extends keyof tvalues & string>(key: k): field<tvalues[k]>;\n reset(next?: tvalues): void;\n set(next: tvalues | ((previous: readonlydeep<tvalues>) => tvalues)): void;\n submit<tresult = void>(handler: (values: readonlydeep<tvalues>) => maybepromise<tresult>): promise<submitresult<tresult, tvalues>>;\n subscribe(listener: (state: formstate<tvalues>) => void, options?: subscribeoptions): unsubscribe;\n validate(signal?: abortsignal): promise<validationresult<tvalues>>;\n};\n\ntype fieldbindingoptions<element extends htmlelement, v> = readonly<{\n event?: keyof htmlelementeventmap;\n read(element: element): v;\n write?: (element: element, value: readonlydeep<v>) => void;\n}>;\n\ntype formdraftcodec<tvalues extends record<string, unknown>, s extends anyschema, k extends keyof s & string> = readonly<{\n fromrecord(record: recordof<s, k>): tvalues | undefined;\n torecord(values: readonlydeep<tvalues>): recordof<s, k>;\n}>;\n```\n\n## errors\n\n| error | trigger | notable properties |\n| | | |\n| `forgeerror` | base forge error | `forgeerror.is(error)` narrows unknown values. |\n| `forgeconfigerror` | unsafe key or unsupported form value | extends `forgeerror`. |\n| `forgedisposederror` | operation or subscription after disposal | message names the attempted operation. |\n| `forgesubmiterror` | concurrent `submit()` call | extends `forgeerror`. |\n| `forgevalidationerror` | validator throws unexpectedly | preserves original error as `cause`. |\n",
|
|
470
|
-
"usage": " \ntitle: forge — usage guide\ndescription: build immutable forms, validate whole values, and use optional adapters.\n \n\n[[toc]]\n\n## basic usage\n\ncreate one form value and update object branches through stable typed operations. form values support primitives, plain objects, arrays, `file`, and `blob`; mutable class instances such as `
|
|
494
|
+
"index": " \ntitle: forge — immutable form state for typescript\ndescription: framework agnostic immutable form state with focused object fields and explicit validation results.\npackage: forge\ncategory: forms\nkeywords: [form state, validation, immutable, input, submission]\nrelated: [spell, vault, courier]\nexports: [createform, bindfield, customvalidator, saveform, loadform]\nenvironments: [browser, node, ssr, deno]\n \n\n<! markdownlint disable md025 md033 md060 >\n\n<packagehero package=\"forge\" />\n\n## why forge?\n\nnative form state becomes difficult to inspect once values, validation, draft restoration, and ui bindings share mutable objects. forge owns one immutable value tree and gives you typed handles for object branches without string paths, scoped controllers, or framework state.\n\n```ts\n// before\nconst values = { email: '', password: '' };\nconst errors: record<string, string> = {};\n\nfunction submit() {\n errors.email = values.email.includes('@') ? '' : 'invalid email';\n errors.password = values.password.length >= 8 ? '' : 'use at least eight characters';\n}\n\n// after\nconst form = createform({\n initialvalues: { email: '', password: '' },\n validate: (value) => ({\n fields: {\n email: value.email.includes('@') ? undefined : 'invalid email',\n password: value.password.length >= 8 ? undefined : 'use at least eight characters',\n },\n }),\n});\n```\n\n| feature | forge | native form state | framework owned form state |\n| | | | |\n| bundle size | <packageinfo package=\"forge\" type=\"size\" /> | <ore icon name=\"check\" size=\"16\"></ore icon> | varies |\n| zero external dependencies | <ore icon name=\"check\" size=\"16\"></ore icon> | <ore icon name=\"check\" size=\"16\"></ore icon> | <ore icon name=\"x\" size=\"16\"></ore icon> |\n| immutable nested values | <ore icon name=\"check\" size=\"16\"></ore icon> | <ore icon name=\"x\" size=\"16\"></ore icon> | varies |\n| typed object field handles | <ore icon name=\"check\" size=\"16\"></ore icon> | <ore icon name=\"x\" size=\"16\"></ore icon> | varies |\n| framework independent state | <ore icon name=\"check\" size=\"16\"></ore icon> | <ore icon name=\"check\" size=\"16\"></ore icon> | <ore icon name=\"x\" size=\"16\"></ore icon> |\n\n<div class=\"decision callout\">\n\n**use forge when** form state needs framework independent immutable values, typed object fields, and one explicit validation boundary.\n\n**consider framework owned form state when** application only needs a single ui framework's native input bindings.\n\n</div>\n\n## installation\n\n::: code group\n\n```sh [pnpm]\npnpm add @vielzeug/forge\n```\n\n```sh [npm]\nnpm install @vielzeug/forge\n```\n\n```sh [yarn]\nyarn add @vielzeug/forge\n```\n\n:::\n\ninstall `@vielzeug/spell` or `@vielzeug/vault` only when importing forge's matching optional adapter.\n\n## quick start\n\ncreate a form, update a focused field, and submit only after validation passes.\n\n```ts\nimport { createform } from '@vielzeug/forge';\n\nconst form = createform({\n initialvalues: { profile: { email: '', name: '' } },\n validate: (value) => ({\n fields: { profile: { email: value.profile.email.includes('@') ? undefined : 'invalid email' } },\n }),\n});\n\nform.field('profile').field('email').set('ada@example.com');\n\nconst result = await form.submit(async (value) => {\n const response = await fetch('/api/profile', {\n body: json.stringify(value),\n headers: { 'content type': 'application/json' },\n method: 'post',\n });\n\n return response.ok;\n});\n\nif (result.status === 'invalid') console.log(result.errors);\n```\n\n## features\n\n<div class=\"features grid\">\n\n `form.value` exposes one immutable nested value tree.\n `form.field(key)` selects typed object branches without string paths.\n `field.set(updater)` replaces array values through immutable updater functions.\n `field.field(index)` selects typed array item fields by index.\n `form.validate()` returns valid, invalid, or aborted results.\n `form.submit(handler, signal?)` touches, validates, and invokes the handler when valid.\n `bindfield()` connects one dom element without owning validation timing.\n `customvalidator()` maps spell schema errors into forge fields.\n `saveform()` and `loadform()` persist explicit vault draft records.\n\n</div>\n\n## documentation\n\n<div class=\"doc links\">\n\n [usage guide](./usage.md)\n [api reference](./api.md)\n [examples](./examples.md)\n [migration guide](./migration.md)\n\n</div>\n\n## see also\n\n<div class=\"see also\">\n\n [spell](/spell/) — adapt a spell schema through `customvalidator()`.\n [vault](/vault/) — save and restore explicit forge draft records.\n [courier](/courier/) — send a validated form value through a mutation.\n\n</div>\n\n<! markdownlint enable >\n",
|
|
495
|
+
"api": " \ntitle: forge — api reference\ndescription: complete reference for immutable forms, fields, validation, serialization, and optional adapters.\n \n\n[[toc]]\n\n## api overview\n\n| symbol | purpose | execution mode | common gotcha |\n| | | | |\n| `createform()` | create immutable form state | sync | `initialvalues` cannot contain mutable class instances |\n| `form.field()` | select a top level or object child field | sync | unsafe keys (`__proto__`, `constructor`, `prototype`) are rejected |\n| `form.validate()` | validate complete value | async | handle `aborted` separately |\n| `form.submit(handler, signal?)` | touch, validate, then invoke handler | async | concurrent calls reject |\n| `form.reset()` | restore or replace baseline | sync | `reset(next)` makes `next` clean |\n| `form.subscribe()` | observe form metadata | sync | throws after disposal |\n| `toformdata()` | serialize values for multipart transport | sync | `filelist` is transport only |\n| `bindfield()` | bind one dom element | sync | does not schedule validation |\n| `customvalidator()` | adapt a spell schema | async | does not transform `form.value` |\n| `saveform()` / `loadform()` | persist explicit vault records | async | formdraftcodec owns record shape |\n\n## package entry point\n\n| import | purpose |\n| | |\n| `@vielzeug/forge` | core form factory, types, and errors |\n| `@vielzeug/forge/dom` | `bindfield()` and dom binding types |\n| `@vielzeug/forge/form data` | `toformdata()` |\n| `@vielzeug/forge/spell` | `customvalidator()` |\n| `@vielzeug/forge/vault` | `saveform()`, `loadform()`, and `formdraftcodec` |\n\n## core functions\n\n### `createform(options)`\n\n```ts\nfunction createform<tvalues extends record<string, unknown>>(options: formoptions<tvalues>): form<tvalues>;\n```\n\ncreates a form with immutable initial values and an optional full form validator.\n\n| parameter | type | description |\n| | | |\n| `options.initialvalues` | `tvalues` | initial value and reset baseline. supports primitives, plain objects, arrays, `date`, `file`, and `blob`. |\n| `options.validate` | `formvalidator<tvalues>` | optional validator for the entire current value. |\n| `options.onsubscribererror` | `(error: unknown) => void` | optional subscriber failure reporter. |\n\n**returns:** `form<tvalues>`.\n\n**example:**\n\n```ts\nimport { createform } from '@vielzeug/forge';\n\nconst form = createform({ initialvalues: { email: '' } });\n```\n\n \n\n### `toformdata(values)`\n\n```ts\nfunction toformdata(values: record<string, unknown>): formdata;\n```\n\nconverts nested values into `formdata` with dot separated object keys and repeated array keys.\n\n**returns:** a populated `formdata` instance.\n\n**example:**\n\n```ts\nimport { toformdata } from '@vielzeug/forge/form data';\n\nconst body = toformdata({ profile: { email: 'ada@example.com' }, tags: ['typescript', 'forms'] });\n```\n\n## form handles\n\n### `form<tvalues>`\n\n`createform()` returns this handle.\n\n| member | signature | description |\n| | | |\n| `value` | `readonlydeep<tvalues>` | current immutable value. |\n| `state` | `formstate<tvalues>` | submission, validation, touch, and error metadata. |\n| `field(key)` | `field<tvalues[k]>` | select a top level field. |\n| `set(next)` | `void` | replace the complete value or derive a replacement. |\n| `reset(next?)` | `void` | restore baseline or make `next` the baseline. |\n| `validate(signal?)` | `promise<validationresult<tvalues>>` | run full form validation. |\n| `submit(handler, signal?)` | `promise<submitresult<tresult, tvalues>>` | touch, validate, and invoke handler when valid. |\n| `subscribe(listener, options?)` | `unsubscribe` | observe form state; throws after disposal. |\n| `dispose()` | `void` | abort validation and clear subscribers. |\n| `disposed` | `boolean` | whether the form has been disposed. |\n| `disposalsignal` | `abortsignal` | aborts on disposal. |\n\n### `field<v>`\n\n`form.field(key)` and object field `.field(key)` return this handle. array item `.field(index)` returns a per item field handle.\n\n| member | signature | description |\n| | | |\n| `value` | `readonlydeep<v>` | current immutable branch value. |\n| `error` | `string \\| undefined` | current field error. |\n| `dirty` | `boolean` | whether branch differs from baseline. |\n| `touched` | `boolean` | whether field was touched. |\n| `state` | `fieldstate<v>` | snapshot of `dirty`, `error`, `touched`, and `value` in one read. |\n| `field(key)` | `field<v[k]>` | select child object field or array item by index. |\n| `set(next)` | `void` | replace branch or derive a replacement. |\n| `reset()` | `void` | restore exact baseline branch. |\n| `touch()` | `void` | mark field touched. |\n| `subscribe(listener, options?)` | `unsubscribe` | observe field transitions; throws after disposal. |\n\n## validation results\n\n### `form.validate(signal?)`\n\n```ts\nfunction validate(signal?: abortsignal): promise<validationresult<tvalues>>;\n```\n\nruns the configured validator against the complete value. a newer validation aborts the older run.\n\n**returns:** `validationresult<tvalues>`.\n\n```ts\nconst result = await form.validate();\n\nif (result.status === 'invalid') console.log(result.errors, result.formerror);\n```\n\n### `form.submit(handler, signal?)`\n\n```ts\nfunction submit<tresult = void>(\n handler: (values: readonlydeep<tvalues>, signal: abortsignal) => maybepromise<tresult>,\n signal?: abortsignal,\n): promise<submitresult<tresult, tvalues>>;\n```\n\ntouches all fields, validates once, and invokes `handler` when validation is valid. the handler receives an `abortsignal` that is aborted when the external `signal` (or the form's disposal signal) aborts.\n\n**returns:** `submitresult<tresult, tvalues>`. handler failures reject normally unless caused by signal abort, which returns `{ status: 'aborted' }`.\n\n```ts\nconst result = await form.submit((value) => promise.resolve(value));\n```\n\n## adapters\n\n### `bindfield(element, field, options)`\n\n```ts\nfunction bindfield<element extends htmlelement, v>(\n element: element,\n field: field<v>,\n options: fieldbindingoptions<element, v>,\n): () => void;\n```\n\nbinds one field to one element, marks it touched on blur, suppresses writeback from its own input event, and returns teardown.\n\n**example:**\n\n```ts\nimport { bindfield } from '@vielzeug/forge/dom';\n\nconst stop = bindfield(input, form.field('email'), {\n read: (element) => element.value,\n write: (element, value) => {\n element.value = value;\n },\n});\n```\n\n \n\n### `customvalidator(schema)`\n\n```ts\nfunction customvalidator<tvalues extends record<string, unknown>>(\n schema: schema<unknown, tvalues, schemamode>,\n): formvalidator<tvalues>;\n```\n\nadapts a spell schema. every failing union maps its closest branch while preserving unrelated errors. array item issues map to per item array fields; duplicate paths retain the first message.\n\n**example:**\n\n```ts\nimport { customvalidator } from '@vielzeug/forge/spell';\nimport { s } from '@vielzeug/spell';\n\nconst profile = s.object({ email: s.string().email() });\nconst form = createform({ initialvalues: { email: '' }, validate: customvalidator(profile) });\n```\n\n \n\n### `saveform()` and `loadform()`\n\n```ts\nfunction saveform<tvalues extends record<string, unknown>, s extends anyschema, k extends keyof s & string>(\n form: form<tvalues>, adapter: vaultstore<s>, table: k, codec: formdraftcodec<tvalues, s, k>,\n): promise<void>;\n\nfunction loadform<tvalues extends record<string, unknown>, s extends anyschema, k extends keyof s & string>(\n form: form<tvalues>, adapter: vaultstore<s>, table: k, key: keyof<s, k>, codec: formdraftcodec<tvalues, s, k>,\n): promise<boolean>;\n```\n\npersists or restores a codec defined vault record. `loadform()` calls `form.reset()` when the codec decodes a record.\n\n**returns:** `loadform()` returns `false` for a missing or rejected record.\n\n## types\n\n```ts\ntype unsubscribe = () => void;\ntype maybepromise<t> = t | promiselike<t>;\ntype readonlydeep<t> = t extends (...args: never[]) => unknown\n ? t\n : t extends readonly (infer item)[]\n ? readonly readonlydeep<item>[]\n : t extends record<string, unknown>\n ? { readonly [k in keyof t]: readonlydeep<t[k]> }\n : t;\n\ntype formerrors<t> = t extends readonly (infer item)[]\n ? string | readonly (formerrors<item> | undefined)[]\n : t extends record<string, unknown>\n ? string | { readonly [k in keyof t]?: formerrors<t[k]> }\n : string;\n\ntype validationerrors<tvalues extends record<string, unknown>> = readonly<{\n fields?: formerrors<tvalues>;\n formerror?: string;\n}>;\n\ntype formvalidator<tvalues extends record<string, unknown>> = (\n values: readonlydeep<tvalues>, signal: abortsignal,\n) => maybepromise<validationerrors<tvalues> | undefined>;\n\ntype formoptions<tvalues extends record<string, unknown>> = readonly<{\n initialvalues: tvalues;\n onsubscribererror?: (error: unknown) => void;\n validate?: formvalidator<noinfer<tvalues>>;\n}>;\n\ntype subscribeoptions = readonly<{ immediate?: boolean }>;\n\ntype fieldstate<v> = readonly<{\n dirty: boolean;\n error: string | undefined;\n touched: boolean;\n value: readonlydeep<v>;\n}>;\n\ntype formstate<tvalues extends record<string, unknown>> = readonly<{\n errors: formerrors<tvalues> | undefined;\n formerror: string | undefined;\n haserrors: boolean;\n submitcount: number;\n submitting: boolean;\n touched: boolean;\n validity: 'invalid' | 'unknown' | 'valid';\n validating: boolean;\n}>;\n\ntype validationresult<tvalues extends record<string, unknown>> =\n | readonly<{ status: 'aborted' }>\n | readonly<{ status: 'valid' }>\n | readonly<{ errors: formerrors<tvalues> | undefined; formerror: string | undefined; status: 'invalid' }>;\n\ntype submitresult<tresult = void, tvalues extends record<string, unknown> = record<string, unknown>> =\n | readonly<{ status: 'aborted' }>\n | readonly<{ errors: formerrors<tvalues> | undefined; formerror: string | undefined; status: 'invalid' }>\n | readonly<{ status: 'ok'; value: tresult }>;\n```\n\n```ts\ntype childfield<v> =\n nonnullable<v> extends readonly (infer item)[]\n ? { field(index: number): field<item> }\n : nonnullable<v> extends record<string, unknown>\n ? { field<k extends keyof nonnullable<v> & string>(key: k): field<nonnullable<v>[k]> }\n : record<never, never>;\n\ntype field<v> = childfield<v> & {\n readonly dirty: boolean;\n readonly error: string | undefined;\n readonly state: fieldstate<v>;\n readonly touched: boolean;\n readonly value: readonlydeep<v>;\n reset(): void;\n set(next: v | ((previous: readonlydeep<v>) => v)): void;\n subscribe(listener: (state: fieldstate<v>) => void, options?: subscribeoptions): unsubscribe;\n touch(): void;\n};\n\ntype form<tvalues extends record<string, unknown>> = {\n [symbol.dispose](): void;\n readonly disposalsignal: abortsignal;\n readonly disposed: boolean;\n readonly state: formstate<tvalues>;\n readonly value: readonlydeep<tvalues>;\n dispose(): void;\n field<k extends keyof tvalues & string>(key: k): field<tvalues[k]>;\n reset(next?: tvalues): void;\n set(next: tvalues | ((previous: readonlydeep<tvalues>) => tvalues)): void;\n submit<tresult = void>(\n handler: (values: readonlydeep<tvalues>, signal: abortsignal) => maybepromise<tresult>,\n signal?: abortsignal,\n ): promise<submitresult<tresult, tvalues>>;\n subscribe(listener: (state: formstate<tvalues>) => void, options?: subscribeoptions): unsubscribe;\n validate(signal?: abortsignal): promise<validationresult<tvalues>>;\n};\n\ntype fieldbindingoptions<element extends htmlelement, v> = readonly<{\n event?: keyof htmlelementeventmap;\n read(element: element): v;\n write?: (element: element, value: readonlydeep<v>) => void;\n}>;\n\ntype formdraftcodec<tvalues extends record<string, unknown>, s extends anyschema, k extends keyof s & string> = readonly<{\n fromrecord(record: recordof<s, k>): tvalues | undefined;\n torecord(values: readonlydeep<tvalues>): recordof<s, k>;\n}>;\n```\n\n## errors\n\n| error | trigger | notable properties |\n| | | |\n| `forgeerror` | base forge error | `forgeerror.is(error)` narrows unknown values. |\n| `forgeconfigerror` | unsafe key or unsupported form value | extends `forgeerror`. |\n| `forgedisposederror` | operation or subscription after disposal | message names the attempted operation. |\n| `forgesubmiterror` | concurrent `submit()` call | extends `forgeerror`. |\n| `forgevalidationerror` | validator throws unexpectedly | preserves original error as `cause`. |\n",
|
|
496
|
+
"usage": " \ntitle: forge — usage guide\ndescription: build immutable forms, validate whole values, and use optional adapters.\n \n\n[[toc]]\n\n## basic usage\n\ncreate one form value and update object branches through stable typed operations. form values support primitives, plain objects, arrays, `date`, `file`, and `blob`; mutable class instances such as `map` and `set` are rejected.\n\n```ts\nimport { createform } from '@vielzeug/forge';\n\nconst form = createform({\n initialvalues: { profile: { email: '', name: '' }, tags: [] as string[] },\n validate: (value) => ({\n fields: { profile: { email: value.profile.email.includes('@') ? undefined : 'invalid email' } },\n }),\n});\n\nconst email = form.field('profile').field('email');\nemail.set('ada@example.com');\nform.field('tags').set((tags) => [...tags, 'typescript']);\n\nconsole.log(form.value.profile.email);\n```\n\n## reset values and branches\n\nreset a field when one branch should return to its exact baseline. reset the form with a value when newly loaded data should become the clean baseline.\n\n```ts\nconst name = form.field('profile').field('name');\n\nname.set('ada');\nname.touch();\nname.reset();\n\nform.reset({ profile: { email: 'ada@example.com', name: 'ada' }, tags: [] });\n```\n\nan absent optional parent remains absent after a child reset. array items support per index field handles for reads, updates, and resets.\n\n## validate and submit\n\nreturn `fields` and an optional `formerror` from one validator. `validate()` replaces the complete validation snapshot and returns an explicit status.\n\n```ts\nconst passwordform = createform({\n initialvalues: { password: '', passwordconfirmation: '' },\n validate: (value) => ({\n fields: {\n password: value.password.length >= 8 ? undefined : 'use at least eight characters',\n passwordconfirmation: value.password === value.passwordconfirmation ? undefined : 'passwords must match',\n },\n }),\n});\n\nconst validation = await passwordform.validate();\n\nif (validation.status === 'invalid') console.log(validation.errors);\nif (validation.status === 'aborted') console.log('validation cancelled');\n\nconst result = await passwordform.submit((value) => promise.resolve(value.password.length));\n\nif (result.status === 'ok') console.log(result.value);\n```\n\nstarting another validation aborts the previous run. field edits preserve existing errors until the next validation replaces them. unexpected validator failures reject as `forgevalidationerror` with the original error as `cause`.\n\n## observe state\n\nuse form subscriptions for aggregate metadata and field subscriptions for one branch. subscribing after disposal throws `forgedisposederror`.\n\n```ts\nconst errors: unknown[] = [];\nconst observedform = createform({\n initialvalues: { email: '' },\n onsubscribererror: (error) => errors.push(error),\n});\n\nconst stopform = observedform.subscribe((state) => {\n console.log(state.validity, state.submitting);\n}, { immediate: true });\nconst stopfield = observedform.field('email').subscribe((state) => {\n console.log(state.value, state.error);\n}, { immediate: true });\n\nstopfield();\nstopform();\n```\n\nwithout `onsubscribererror`, forge rethrows subscriber failures asynchronously after completing its state transition.\n\n## testing\n\ntest the form without a dom. read its immutable value, invoke a method, then assert the resulting state or validation result.\n\n```ts\nimport { expect, test } from 'vitest';\nimport { createform } from '@vielzeug/forge';\n\ntest('requires an email address', async () => {\n const form = createform({\n initialvalues: { email: '' },\n validate: (value) => ({ fields: { email: value.email.includes('@') ? undefined : 'invalid email' } }),\n });\n\n await expect(form.validate()).resolves.toequal({\n errors: { email: 'invalid email' },\n formerror: undefined,\n status: 'invalid',\n });\n});\n```\n\n## framework integration\n\nuse `form.value` and subscriptions with any renderer. bind one dom input through `/dom`; validation scheduling remains application policy.\n\n::: code group\n\n```ts [react]\nimport { useeffect, usestate } from 'react';\nimport { createform } from '@vielzeug/forge';\n\nconst form = createform({ initialvalues: { email: '' } });\n\nexport function emailform() {\n const [, rerender] = usestate(0);\n\n useeffect(() => {\n const stop = form.subscribe(() => rerender((revision) => revision + 1));\n\n return () => stop();\n }, []);\n\n return <input value={form.field('email').value} onchange={(event) => form.field('email').set(event.target.value)} />;\n}\n```\n\n```ts [vue 3]\nimport { onunmounted, ref } from 'vue';\nimport { createform } from '@vielzeug/forge';\n\nconst form = createform({ initialvalues: { email: '' } });\nconst revision = ref(0);\nconst stop = form.subscribe(() => revision.value++);\n\nonunmounted(stop);\n```\n\n```ts [svelte]\n<script lang=\"ts\">\n import { ondestroy } from 'svelte';\n import { createform } from '@vielzeug/forge';\n\n const form = createform({ initialvalues: { email: '' } });\n let revision = 0;\n const stop = form.subscribe(() => revision++);\n\n ondestroy(stop);\n</script>\n\n<input value={form.field('email').value} on:input={(event) => form.field('email').set(event.currenttarget.value)} />\n```\n\n:::\n\n## working with other vielzeug libraries\n\nuse spell when one schema owns validation and vault when an explicit record codec owns persistence.\n\n```ts\nimport { createform } from '@vielzeug/forge';\nimport { customvalidator } from '@vielzeug/forge/spell';\nimport { s } from '@vielzeug/spell';\n\nconst profile = s.object({ email: s.string().email() });\nconst form = createform({ initialvalues: { email: '' }, validate: customvalidator(profile) });\n```\n\n`customvalidator()` preserves unrelated spell errors, maps each union to its closest branch, and maps array item failures to per item array fields. parse again at the submit boundary when a spell transform must produce the outgoing payload.\n\n```ts\nimport { loadform, saveform } from '@vielzeug/forge/vault';\n\nawait saveform(form, db, 'drafts', codec);\nconst restored = await loadform(form, db, 'drafts', 'profile', codec);\nconsole.log(restored);\n```\n\n`loadform()` uses `form.reset()`, so a restored value is clean. store a selected `file`, not `filelist`, in form state; `filelist` is transport only for `toformdata()`.\n\n## best practices\n\n keep form values to primitives, plain objects, arrays, `date`, `file`, and `blob`.\n update array fields through immutable replacement functions.\n validate complete values instead of rebuilding field validator graphs.\n handle `aborted` validation results before rendering errors.\n preserve errors through field edits until a deliberate validation refresh.\n return subscription cleanup from framework lifecycle hooks.\n provide `onsubscribererror` when application subscribers can throw.\n decode vault records before passing them to `loadform()`.\n",
|
|
471
497
|
"examples": " \ntitle: forge — examples\ndescription: practical immutable form recipes.\n \n\n## examples\n\n [login form](./examples/login form.md)\n [conditional values](./examples/form with conditional fields.md)\n [dynamic arrays](./examples/dynamic form fields.md)\n [contact form with file upload](./examples/contact form with file upload.md)\n [registration form](./examples/registration form.md)\n [multi step wizard](./examples/multi step wizard.md)\n [search form with debounce](./examples/search form with debounce.md)\n"
|
|
472
498
|
},
|
|
473
499
|
"examples": [
|
|
@@ -512,12 +538,34 @@
|
|
|
512
538
|
"text": "nested field handles import { createform } from '@vielzeug/forge'\n\nconst form = createform({ initialvalues: { shipping: { city: '', street: '' } } })\nconst shipping = form.field('shipping')\n\nshipping.field('street').set('123 main street')\nshipping.field('city').set('portland')\nconsole.log(shipping.value)\nconsole.log(form.value.shipping)"
|
|
513
539
|
}
|
|
514
540
|
],
|
|
515
|
-
"exports": "createform
|
|
541
|
+
"exports": "createform bindfield customvalidator saveform loadform",
|
|
516
542
|
"keywords": "form state validation immutable input submission",
|
|
517
543
|
"name": "@vielzeug/forge",
|
|
518
544
|
"related": "spell vault courier",
|
|
519
545
|
"slug": "forge",
|
|
520
|
-
"source": "export {
|
|
546
|
+
"source": "export { forgeconfigerror, forgedisposederror, forgeerror, forgesubmiterror, forgevalidationerror } from './errors';\nexport { createform } from './form';\nexport * from './types';\n"
|
|
547
|
+
},
|
|
548
|
+
{
|
|
549
|
+
"category": "input",
|
|
550
|
+
"description": "framework neutral one axis pointer pan recognition with lifecycle owned handles.",
|
|
551
|
+
"docs": {
|
|
552
|
+
"index": " \ntitle: gesture — pointer pan primitives\ndescription: framework neutral one axis pointer pan recognition with lifecycle owned handles.\npackage: gesture\ncategory: input\nkeywords: [pointer, pan, swipe, gesture, touch, drag]\nexports: [createpangesture]\nrelated: [refine, dnd, keymap]\nenvironments: [browser]\n \n\n<! markdownlint disable md025 md033 md060 >\n\n<packagehero package=\"gesture\" />\n\n## why gesture?\n\npointer driven interfaces need reliable movement tracking without coupling input recognition to rendering or product specific thresholds.\n\n```ts\n// before\nelement.addeventlistener('pointermove', (event) => {\n // coordinate tracking, pointer identity, direction locking, and cleanup\n});\n\n// after\nconst pan = createpangesture(element, {\n axis: 'x',\n onmove: ({ distance }) => render(distance),\n onend: ({ distance, reason }) => finish(distance, reason),\n});\n```\n\n| feature | ad hoc pointer handling | gesture |\n| | | |\n| bundle size | n/a | <packageinfo package=\"gesture\" type=\"size\" /> |\n| zero dependencies | n/a | <ore icon name=\"check\" size=\"16\"></ore icon> |\n| axis intent recognition | manual | built in |\n| pointer ownership | manual | tracked across the document |\n| lifecycle cleanup | manual | `dispose()` + `disposalsignal` |\n\n<div class=\"decision callout\">\n\n**use gesture when** several ui surfaces need consistent one axis pointer tracking while retaining their own completion rules.\n\n**consider direct pointer handling when** the interaction is isolated and does not need reusable lifecycle or direction lock behavior.\n\n</div>\n\n## installation\n\n::: code group\n\n```sh [pnpm]\npnpm add @vielzeug/gesture\n```\n\n```sh [npm]\nnpm install @vielzeug/gesture\n```\n\n```sh [yarn]\nyarn add @vielzeug/gesture\n```\n\n:::\n\n## quick start\n\n```ts\nimport { createpangesture } from '@vielzeug/gesture';\n\nconst pan = createpangesture(element, {\n axis: 'x',\n onmove: ({ distance }) => {\n element.style.transform = `translatex(${distance}px)`;\n },\n onend: ({ distance, reason }) => {\n element.style.transform = '';\n\n if (reason === 'release' && math.abs(distance) >= 48) {\n dismiss();\n }\n },\n});\n```\n\n## features\n\n<div class=\"features grid\">\n\n `createpangesture()` — one axis pointer movement tracking\n direction locking — activates only when movement favors the configured axis\n configurable pointer capture — own the pointer by default or preserve native targeting\n consumer owned policy — thresholds, snapping, and outcomes stay in application code\n stable completion — one `onend` callback for release and cancellation\n lifecycle ownership — `dispose()`, `disposed`, and `disposalsignal`\n\n</div>\n\n## documentation\n\n<div class=\"doc links\">\n\n [usage guide](./usage.md)\n [api reference](./api.md)\n [examples](./examples.md)\n\n</div>\n\n## see also\n\n<div class=\"see also\">\n\n [refine](/refine/) — components that use pan recognition for carousel, drawer, toast, and list interactions.\n [dnd](/dnd/) — drag and drop behavior with drop targets and reordering.\n [keymap](/keymap/) — keyboard interaction primitives for complementary input paths.\n\n</div>\n\n<! markdownlint enable md025 md033 md060 >\n",
|
|
553
|
+
"api": " \ntitle: gesture — api reference\ndescription: api reference for @vielzeug/gesture pointer pan recognition.\n \n\n[[toc]]\n\n## api overview\n\n| symbol | purpose | execution mode | common gotcha |\n| | | | |\n| `createpangesture()` | track one axis pointer movement on an element | sync | `onstart` runs after direction intent is recognized |\n| `pangesture` | lifecycle owned pan handle | sync | `dispose()` does not emit `onend` |\n| `pangestureoptions` | configure axis, admission, capture, and callbacks | sync | completion thresholds belong in `onend` |\n\n## package entry point\n\n| import | purpose |\n| | |\n| `@vielzeug/gesture` | pan recognizer and related types. |\n\n## core functions\n\n### `createpangesture()`\n\n```ts\nfunction createpangesture(target: element, options?: pangestureoptions): pangesture;\n```\n\nattaches a one axis pointer pan recognizer to `target`.\n\n| parameter | type | description |\n| | | |\n| `target` | `element` | element that owns the pointer interaction. |\n| `options` | `pangestureoptions` | axis, disabled state, admission guard, capture policy, and lifecycle callbacks. |\n\n**returns:** a `pangesture` handle.\n\n**example**\n\n```ts\nimport { createpangesture } from '@vielzeug/gesture';\n\nconst pan = createpangesture(element, {\n axis: 'x',\n onend: ({ distance, reason }) => {\n if (reason === 'release' && math.abs(distance) >= 48) dismiss();\n },\n});\n```\n\n| member | return | contract |\n| | | |\n| `active` | `boolean` | `true` after direction intent is accepted and before the interaction ends. |\n| `cancel()` | `boolean` | cancels the pending or active pointer interaction. active pans emit `onend` with `reason: 'cancel'`. |\n| `dispose()` | `void` | detaches listeners, releases pointer ownership, and aborts `disposalsignal`. idempotent. |\n| `disposed` | `boolean` | `true` after the first `dispose()`. |\n| `disposalsignal` | `abortsignal` | aborts when the handle is disposed. |\n| `[symbol.dispose]()` | `void` | calls `dispose()`. |\n\n## types\n\n```ts\ntype panaxis = 'x' | 'y';\ntype panendreason = 'cancel' | 'release';\n\ntype pangesturedetail = {\n axis: panaxis;\n current: number;\n distance: number;\n event: pointerevent;\n pointerid: number;\n pointertype: string;\n start: number;\n target: element;\n};\n\ntype pangestureenddetail = pangesturedetail & {\n reason: panendreason;\n};\n\ntype pangestureoptions = {\n axis?: panaxis | (() => panaxis);\n disabled?: boolean | (() => boolean | undefined);\n pointercapture?: boolean;\n onend?: (detail: pangestureenddetail) => void;\n onmove?: (detail: pangesturedetail) => void;\n onstart?: (detail: pangesturedetail) => void;\n shouldstart?: (event: pointerevent) => boolean;\n};\n\ntype pangesture = {\n readonly active: boolean;\n [symbol.dispose](): void;\n cancel(): boolean;\n readonly disposalsignal: abortsignal;\n dispose(): void;\n readonly disposed: boolean;\n};\n```\n\n| option | type | default | contract |\n| | | | |\n| `axis` | `panaxis \\| (() => panaxis)` | `'x'` | axis resolved when each pointer interaction starts |\n| `disabled` | `boolean \\| (() => boolean \\| undefined)` | `false` | blocks new pans and cancels an active pan on the next pointer event |\n| `pointercapture` | `boolean` | `true` | captures the pointer on `target` after axis intent is accepted |\n| `shouldstart` | `(event: pointerevent) => boolean` | — | rejects a primary pointer start before tracking begins |\n| `onstart` | `(detail: pangesturedetail) => void` | — | runs once when axis intent is accepted |\n| `onmove` | `(detail: pangesturedetail) => void` | — | runs for the activating move and later moves |\n| `onend` | `(detail: pangestureenddetail) => void` | — | runs for active release or cancellation |\n\ngesture tracks an accepted pan with capture phase listeners on `target.ownerdocument` regardless of the pointer capture setting. set `pointercapture: false` when nested or newly revealed controls must retain native pointer up and click targeting.\n\n## errors\n\n`@vielzeug/gesture` does not export custom error classes.\n",
|
|
554
|
+
"usage": " \ntitle: gesture — usage guide\ndescription: track one axis pointer movement and apply application specific completion rules.\n \n\n[[toc]]\n\n## basic usage\n\ncreate one pan handle for the element that owns the interaction.\n\n```ts\nimport { createpangesture } from '@vielzeug/gesture';\n\nconst pan = createpangesture(row, {\n axis: 'x',\n onmove: ({ distance }) => {\n row.style.transform = `translatex(${distance}px)`;\n },\n onend: ({ distance, reason }) => {\n row.style.transform = '';\n\n if (reason === 'release' && math.abs(distance) >= 64) archive();\n },\n});\n```\n\n## completion rules\n\ngesture reports movement and terminal state but does not decide what constitutes a swipe. apply thresholds and allowed directions in `onend`.\n\n```ts\nconst pan = createpangesture(panel, {\n axis: 'x',\n onend: ({ distance, reason }) => {\n if (reason === 'release' && distance <= 80) {\n opennext();\n } else {\n resetpanel();\n }\n },\n});\n```\n\n## direction recognition\n\nthe gesture remains pending during small movement. it activates only after movement favors the configured axis. cross axis movement ends the pending interaction without invoking callbacks.\n\nuse the corresponding `touch action` value so the browser retains native scrolling on the other axis.\n\n```css\n.swipe row {\n touch action: pan y;\n}\n```\n\n```ts\nconst pan = createpangesture(row, { axis: 'x', onmove });\n```\n\n## pointer capture\n\npointer capture is enabled by default. after axis intent is accepted, gesture captures the pointer on the bound target while continuing to track movement through document level listeners. this is the reliable default for ordinary drag surfaces.\n\ndisable capture when nested or newly revealed controls must retain native pointer up and click targeting:\n\n```ts\nconst pan = createpangesture(row, {\n axis: 'x',\n pointercapture: false,\n onmove: renderreveal,\n onend: settlereveal,\n});\n```\n\ndocument level tracking still keeps the pan active outside the target. disabling capture changes event targeting, not gesture tracking.\n\n## interactive descendants\n\nuse `shouldstart` when buttons, links, or form controls inside the surface must not start a pan.\n\n```ts\nconst pan = createpangesture(notification, {\n axis: 'x',\n pointercapture: false,\n shouldstart: (event) =>\n !event\n .composedpath()\n .some((node) => node instanceof element && node.matches('button, a, input, select, textarea')),\n onmove,\n onend,\n});\n```\n\n`shouldstart` protects controls under the initial pointer. `pointercapture: false` additionally protects controls that appear beneath the pointer during a reveal interaction.\n\n## disabled state\n\na boolean disables the recognizer permanently. a getter supports state that changes while the handle is alive.\n\n```ts\nconst pan = createpangesture(row, {\n disabled: () => islocked,\n onend: ({ reason }) => {\n if (reason === 'cancel') resetrow();\n },\n});\n```\n\nwhen the getter becomes `true`, the next pointer event cancels an active pan.\n\n## lifecycle\n\ndispose the target bound handle when its owning ui scope unmounts.\n\n```ts\nconst pan = createpangesture(element, { onend, onmove });\n\noncleanup(() => pan.dispose());\n```\n\nuse `cancel()` to stop a pending or active interaction without disposing the handle. an active interaction emits `onend` with `reason: 'cancel'`.\n\n## framework integration\n\ncreate the handle after the target element exists and dispose it on unmount.\n\n::: code group\n\n```tsx [react]\nimport { useeffect, useref } from 'react';\nimport { createpangesture } from '@vielzeug/gesture';\n\nfunction swiperow({ ondismiss }: { ondismiss: () => void }) {\n const rowref = useref<htmldivelement>(null);\n\n useeffect(() => {\n const row = rowref.current;\n if (!row) return;\n\n const pan = createpangesture(row, {\n axis: 'x',\n onmove: ({ distance }) => {\n row.style.transform = `translatex(${distance}px)`;\n },\n onend: ({ distance, reason }) => {\n row.style.transform = '';\n if (reason === 'release' && math.abs(distance) >= 64) ondismiss();\n },\n });\n\n return () => pan.dispose();\n }, [ondismiss]);\n\n return <div ref={rowref}>swipe me</div>;\n}\n```\n\n```vue [vue 3]\n<script setup lang=\"ts\">\nimport { onmounted, onunmounted, ref } from 'vue';\nimport { createpangesture, type pangesture } from '@vielzeug/gesture';\n\nconst emit = defineemits<{ dismiss: [] }>();\nconst rowel = ref<htmldivelement | null>(null);\nlet pan: pangesture | undefined;\n\nonmounted(() => {\n const row = rowel.value;\n if (!row) return;\n\n pan = createpangesture(row, {\n axis: 'x',\n onend: ({ distance, reason }) => {\n if (reason === 'release' && math.abs(distance) >= 64) emit('dismiss');\n },\n });\n});\n\nonunmounted(() => pan?.dispose());\n</script>\n\n<template>\n <div ref=\"rowel\">swipe me</div>\n</template>\n```\n\n```svelte [svelte]\n<script lang=\"ts\">\n import { onmount } from 'svelte';\n import { createpangesture } from '@vielzeug/gesture';\n\n let { ondismiss = () => {} }: { ondismiss: () => void } = $props();\n let rowel: htmldivelement;\n\n onmount(() => {\n const pan = createpangesture(rowel, {\n axis: 'x',\n onend: ({ distance, reason }) => {\n if (reason === 'release' && math.abs(distance) >= 64) ondismiss();\n },\n });\n\n return () => pan.dispose();\n });\n</script>\n\n<div bind:this={rowel}>swipe me</div>\n```\n\n:::\n\n## working with other vielzeug libraries\n\n### gesture + refine\n\nrefine uses gesture internally for carousel, drawer, toast, and list item pointer interactions. custom surfaces can use the same pan lifecycle while keeping visual state local.\n\n```ts\nimport { createpangesture } from '@vielzeug/gesture';\n\nconst pan = createpangesture(panel, {\n axis: 'x',\n onmove: ({ distance }) => {\n panel.style.transform = `translatex(${distance}px)`;\n },\n onend: ({ distance, reason }) => {\n panel.style.transform = '';\n if (reason === 'release' && math.abs(distance) >= 80) revealactions();\n },\n});\n```\n\n### gesture + dnd\n\ngesture tracks a constrained pointer pan. dnd owns draggable items, sortable lists, and drop targets. keep them separate.\n\n## best practices\n\n **set** `touch action` for the axis the browser should continue scrolling.\n **use** `shouldstart` to exclude nested interactive controls.\n **disable** pointer capture when nested or newly revealed controls must keep native release targeting.\n **apply** thresholds and direction rules in `onend`.\n **treat** `reason: 'cancel'` as a reset path, never a commit path.\n **keep** `onmove` rendering lightweight.\n **dispose** the handle when its target leaves the ui.\n",
|
|
555
|
+
"examples": " \ntitle: gesture — examples\ndescription: worked examples for @vielzeug/gesture.\n \n\n## examples\n\n [carousel swipe navigation](./examples/carousel swipe navigation.md)\n [swipe to dismiss notifications](./examples/swipe dismiss notifications.md)\n"
|
|
556
|
+
},
|
|
557
|
+
"examples": [
|
|
558
|
+
{
|
|
559
|
+
"id": "pan-basic",
|
|
560
|
+
"text": "createpangesture basic import { createpangesture } from '@vielzeug/gesture'\n\nconst surface = document.createelement('div')\nsurface.textcontent = 'drag horizontally'\nsurface.style.csstext = 'width:240px;padding:32px;text align:center;background:#e0e7ff;border radius:12px;touch action:pan y;user select:none;'\ndocument.body.appendchild(surface)\n\nconst output = document.createelement('pre')\ndocument.body.appendchild(output)\n\nconst pan = createpangesture(surface, {\n axis: 'x',\n onmove: ({ distance }) => {\n surface.style.transform = `translatex(${distance}px)`\n output.textcontent = `distance: ${math.round(distance)}px`\n },\n onend: ({ distance, reason }) => {\n surface.style.transform = ''\n output.textcontent = reason === 'release' && math.abs(distance) >= 48\n ? `swipe: ${distance < 0 ? 'left' : 'right'}`\n : `ended: ${reason}`\n },\n})\n\nconsole.log('pan gesture ready:', pan.disposed === false)"
|
|
561
|
+
}
|
|
562
|
+
],
|
|
563
|
+
"exports": "createpangesture",
|
|
564
|
+
"keywords": "pointer pan swipe gesture touch drag",
|
|
565
|
+
"name": "@vielzeug/gesture",
|
|
566
|
+
"related": "refine dnd keymap",
|
|
567
|
+
"slug": "gesture",
|
|
568
|
+
"source": "export type {\n panaxis,\n panendreason,\n pangesture,\n pangesturedetail,\n pangestureenddetail,\n pangestureoptions,\n} from './pan gesture';\nexport { createpangesture } from './pan gesture';\n"
|
|
521
569
|
},
|
|
522
570
|
{
|
|
523
571
|
"category": "events",
|
|
@@ -597,6 +645,60 @@
|
|
|
597
645
|
"slug": "herald",
|
|
598
646
|
"source": "export { combinesignals, createbus } from './bus';\nexport { busdisposederror, heraldconfigerror, heralderror } from './errors';\nexport { pipeevents } from './pipe';\nexport type {\n bus,\n buslogger,\n busoptions,\n emissionerrorcontext,\n eventkey,\n eventmap,\n eventstream,\n listener,\n middleware,\n pipeablekey,\n pipeentry,\n subscribeoptions,\n unsubscribe,\n waitanyresult,\n} from './types';\n"
|
|
599
647
|
},
|
|
648
|
+
{
|
|
649
|
+
"category": "data",
|
|
650
|
+
"description": "typed, deterministic, locale aware fake data generator with a seeded prng, eight data categories, and zero external runtime dependencies.",
|
|
651
|
+
"docs": {
|
|
652
|
+
"index": " \ntitle: illusionist — fake data generator for typescript\ndescription: typed, deterministic, locale aware fake data generator with a seeded prng, eight data categories, and zero external runtime dependencies.\npackage: illusionist\ncategory: data\nkeywords: [fake data, mock, seed, faker, test fixtures, deterministic]\nexports: [createillusion, createseed, mulberry32]\nrelated: [arsenal, coins, tempo]\nenvironments: [browser, node, ssr]\n \n\n<! markdownlint disable md025 md033 md060 >\n\n<packagehero package=\"illusionist\" />\n\n## why illusionist?\n\nillusionist generates realistic fake data from a single seeded random source. the same seed always produces the same output, so test fixtures and snapshots stay reproducible across runs, machines, and ci. every category shares one bound instance with one locale, so a person, their email, and their address stay internally consistent.\n\n```ts\n// before\nconst user = {\n name: 'test user',\n email: 'test@example.com',\n address: '123 main st',\n};\n\n// after\nimport { createillusion } from '@vielzeug/illusionist';\nimport { en } from '@vielzeug/illusionist/locales';\n\nconst illusion = createillusion({ seed: 12345, locale: en });\n\nconst user = {\n name: illusion.person.fullname(),\n email: illusion.internet.email(),\n address: illusion.location.streetaddress(),\n};\n\nillusion.dispose();\n```\n\n| feature | illusionist | faker.js | @faker js/faker |\n| | | | |\n| bundle size | <packageinfo package=\"illusionist\" type=\"size\" /> | external dependency | external dependency |\n| zero external dependencies | <ore icon name=\"check\" size=\"16\"></ore icon> | <ore icon name=\"x\" size=\"16\"></ore icon> | <ore icon name=\"x\" size=\"16\"></ore icon> |\n| seeded determinism | <ore icon name=\"check\" size=\"16\"></ore icon> | partial | <ore icon name=\"check\" size=\"16\"></ore icon> |\n| locale aware datasets | <ore icon name=\"check\" size=\"16\"></ore icon> | <ore icon name=\"check\" size=\"16\"></ore icon> | <ore icon name=\"check\" size=\"16\"></ore icon> |\n| typescript native types | <ore icon name=\"check\" size=\"16\"></ore icon> | <ore icon name=\"x\" size=\"16\"></ore icon> | <ore icon name=\"check\" size=\"16\"></ore icon> |\n\n<div class=\"decision callout\">\n\n**use illusionist when** test fixtures, mock apis, or database seeds must be realistic and reproducible from a single seed value.\n\n**consider @faker js/faker when** you need a large catalog of locale datasets beyond `en` and `de` or a community plugin ecosystem.\n\n</div>\n\n## installation\n\n::: code group\n\n```sh [pnpm]\npnpm add @vielzeug/illusionist\n```\n\n```sh [npm]\nnpm install @vielzeug/illusionist\n```\n\n```sh [yarn]\nyarn add @vielzeug/illusionist\n```\n\n:::\n\n## quick start\n\ncreate a bound instance with a seed and locale. all categories share that seed, so output is deterministic.\n\n```ts\nimport { createillusion } from '@vielzeug/illusionist';\nimport { en } from '@vielzeug/illusionist/locales';\n\nconst illusion = createillusion({ seed: 12345, locale: en });\n\nillusion.person.fullname(); // 'ashley harris'\nillusion.internet.email(); // 'samantha.sanchez@mail.com'\nillusion.commerce.price(); // money { amount: 76640n, currency: usd }\nillusion.date.past({ years: 2 }); // temporal.zoneddatetime\n\nillusion.dispose(); // release the instance; [symbol.dispose]() also works\n```\n\n## features\n\n<div class=\"features grid\">\n\n **`person`**: names, gender, prefixes, suffixes, job titles\n **`internet`**: emails, usernames, passwords, urls, ips, macs, http metadata\n **`commerce`**: product names, departments, prices as coins `money`\n **`date`**: past, future, recent, between, birthday as tempo `temporal` objects\n **`finance`**: amounts, ibans, bics, credit cards, crypto addresses\n **`location`**: cities, streets, states, countries, gps coordinates\n **`lorem`**: words, sentences, paragraphs, slugs\n **`system`**: file paths, semver, uuids, ports, cron expressions\n\n</div>\n\n## documentation\n\n<div class=\"doc links\">\n\n [usage guide](./usage.md)\n [api reference](./api.md)\n [examples](./examples.md)\n\n</div>\n\n## see also\n\n<div class=\"see also\">\n\n [arsenal](/arsenal/) — random primitives (`randomsource`, `uuid`) that illusionist builds on.\n [coins](/coins/) — exact money type returned by `commerce.price()` and `finance.amount()`.\n [tempo](/tempo/) — `temporal` date utilities returned by every `date` function.\n\n</div>\n\n<! markdownlint enable md025 md033 md060 >\n",
|
|
653
|
+
"api": " \ntitle: illusionist — api reference\ndescription: createillusion, all category functions, seed utilities, types, and errors.\n \n\n[[toc]]\n\n## api overview\n\n| symbol | purpose | execution | common gotcha |\n| | | | |\n| `createillusion` | create a bound, seeded instance | sync | locale is fixed for the instance lifetime |\n| `person.*` | names, gender, job titles | sync | locale specific datasets (`en`, `de`) |\n| `internet.*` | emails, urls, ips, http metadata | sync | `ip()` defaults to ipv4 |\n| `commerce.*` | product names, prices | sync | `price()` returns coins `money` |\n| `date.*` | past, future, recent, birthday | sync | returns tempo `temporal` objects |\n| `finance.*` | ibans, cards, crypto addresses | sync | ibans pass mod 97; cards pass luhn |\n| `location.*` | cities, streets, gps | sync | locale specific datasets |\n| `lorem.*` | words, sentences, paragraphs | sync | word pool is fixed |\n| `system.*` | files, semver, uuids, ports | sync | `port()` avoids well known ports by default |\n| `createseed` | build a `randomsource` from a seed | sync | non finite numeric seeds throw |\n| `mulberry32` | low level 32 bit prng | sync | not cryptographically secure |\n\n## package entry point\n\n| import | purpose |\n| | |\n| `@vielzeug/illusionist` | `createillusion`, `illusionist`, `illusionistoptions`, `illusionistlocale`, error classes |\n| `@vielzeug/illusionist/locales` | tree shakeable barrel — `en`, `de` locale objects |\n| `@vielzeug/illusionist/locales/en` | english locale object only |\n| `@vielzeug/illusionist/locales/de` | german locale object only |\n| `@vielzeug/illusionist/seed` | `createseed`, `mulberry32` |\n| `@vielzeug/illusionist/person` | person category functions |\n| `@vielzeug/illusionist/internet` | internet category functions |\n| `@vielzeug/illusionist/commerce` | commerce category functions |\n| `@vielzeug/illusionist/date` | date category functions |\n| `@vielzeug/illusionist/finance` | finance category functions |\n| `@vielzeug/illusionist/location` | location category functions |\n| `@vielzeug/illusionist/lorem` | lorem category functions |\n| `@vielzeug/illusionist/system` | system category functions |\n\n## createillusion\n\n```ts\nfunction createillusion(options: illusionistoptions): illusionist;\n```\n\ncreates a bound instance. all categories share one seeded random source and one locale. locale data is included only when its dedicated subpath is imported; the root entry does not statically import a default locale. for dynamic switching, use `await import('@vielzeug/illusionist/locales')` before calling this synchronous factory.\n\n| option | type | default | description |\n| | | | |\n| `seed` | `number \\| string` | `undefined` | seed for deterministic output. omit for cryptographic randomness. |\n| `locale` | `illusionistlocale` | required | explicit locale object for locale aware categories. |\n\n**returns:** `illusionist` — an object with `person`, `internet`, `commerce`, `date`, `finance`, `location`, `lorem`, `system` categories, plus `seed`, `locale`, `dispose()`, `disposed`, `disposalsignal`, and `[symbol.dispose]()`.\n\n```ts\nimport { createillusion } from '@vielzeug/illusionist';\nimport { en } from '@vielzeug/illusionist/locales';\n\nconst illusion = createillusion({ seed: 12345, locale: en });\nillusion.person.fullname();\nillusion.dispose();\n```\n\n \n\n## person\n\n### `person.firstname()`\n\n```ts\nfunction firstname(): string;\n```\n\nreturns a random first name from the locale dataset.\n\n### `person.lastname()`\n\n```ts\nfunction lastname(): string;\n```\n\nreturns a random last name from the locale dataset.\n\n### `person.fullname()`\n\n```ts\nfunction fullname(): string;\n```\n\nreturns a first and last name separated by a space.\n\n### `person.gender()`\n\n```ts\nfunction gender(): string;\n```\n\nreturns a random gender label from the locale dataset.\n\n### `person.prefix()`\n\n```ts\nfunction prefix(): string;\n```\n\nreturns a random name prefix (e.g. `mr.`, `dr.`).\n\n### `person.suffix()`\n\n```ts\nfunction suffix(): string;\n```\n\nreturns a random name suffix. returns an empty string when the locale dataset has no suffixes.\n\n### `person.jobtitle()`\n\n```ts\nfunction jobtitle(): string;\n```\n\nreturns a job area and job type joined by a space.\n\n \n\n## internet\n\n### `internet.email()`\n\n```ts\nfunction email(): string;\n```\n\nreturns an email of the form `firstname.lastname@domain.tld`.\n\n### `internet.username()`\n\n```ts\nfunction username(): string;\n```\n\nreturns either a random alphanumeric string or a `firstname.lastname` pattern.\n\n### `internet.password(options?)`\n\n```ts\nfunction password(options?: passwordoptions): string;\n```\n\n| option | type | default | description |\n| | | | |\n| `length` | `number` | `12` | password length. |\n| `memorable` | `boolean` | `false` | build from name fragments and digits. |\n\nreturns a password mixing upper/lowercase letters, digits, and special characters.\n\n### `internet.url()`\n\n```ts\nfunction url(): string;\n```\n\nreturns a url of the form `protocol://sub.domain.tld/path/...`.\n\n### `internet.domainname()`\n\n```ts\nfunction domainname(): string;\n```\n\nreturns a domain of the form `domain.tld`.\n\n### `internet.ip(version?)`\n\n```ts\nfunction ip(version?: 4 | 6): string;\n```\n\nreturns an ipv4 or ipv6 address. defaults to ipv4.\n\n### `internet.mac()`\n\n```ts\nfunction mac(): string;\n```\n\nreturns a mac address of the form `xx:xx:xx:xx:xx:xx`.\n\n### `internet.useragent()`\n\n```ts\nfunction useragent(): string;\n```\n\nreturns a random user agent string.\n\n### `internet.httpmethod()`\n\n```ts\nfunction httpmethod(): string;\n```\n\nreturns a random http method.\n\n### `internet.statuscode()`\n\n```ts\nfunction statuscode(): number;\n```\n\nreturns a random http status code.\n\n### `internet.mimetype()`\n\n```ts\nfunction mimetype(): string;\n```\n\nreturns a random mime type.\n\n \n\n## commerce\n\n### `commerce.productadjective()`\n\n```ts\nfunction productadjective(): string;\n```\n\nreturns a random product adjective.\n\n### `commerce.productmaterial()`\n\n```ts\nfunction productmaterial(): string;\n```\n\nreturns a random product material.\n\n### `commerce.productnoun()`\n\n```ts\nfunction productnoun(): string;\n```\n\nreturns a random product noun.\n\n### `commerce.productname()`\n\n```ts\nfunction productname(): string;\n```\n\nreturns an adjective, material, and noun joined by spaces.\n\n### `commerce.department()`\n\n```ts\nfunction department(): string;\n```\n\nreturns a random department name.\n\n### `commerce.price(options?)`\n\n```ts\nfunction price(options?: priceoptions): money;\n```\n\n| option | type | default | description |\n| | | | |\n| `min` | `number` | `0.01` | minimum price. |\n| `max` | `number` | `1000` | maximum price. |\n| `currency` | `'usd' \\| 'eur' \\| 'gbp'` | `'usd'` | currency code. |\n\nreturns a coins `money` value with two decimal places.\n\n### `commerce.productdescription()`\n\n```ts\nfunction productdescription(): string;\n```\n\nreturns one or two sentences describing a product.\n\n \n\n## date\n\nall date functions return tempo `temporal` objects.\n\n### `date.past(options?)`\n\n```ts\nfunction past(options?: { years?: number; ref?: temporal.zoneddatetime }): temporal.zoneddatetime;\n```\n\nreturns a date in the past within `years` (default `1`) from `ref` (default now).\n\n### `date.future(options?)`\n\n```ts\nfunction future(options?: { years?: number; ref?: temporal.zoneddatetime }): temporal.zoneddatetime;\n```\n\nreturns a date in the future within `years` (default `1`) from `ref` (default now).\n\n### `date.recent(options?)`\n\n```ts\nfunction recent(options?: { days?: number; ref?: temporal.zoneddatetime }): temporal.zoneddatetime;\n```\n\nreturns a date within `days` (default `1`) in the past from `ref` (default now).\n\n### `date.between(from, to)`\n\n```ts\nfunction between(from: temporal.zoneddatetime, to: temporal.zoneddatetime): temporal.zoneddatetime;\n```\n\nreturns a date between `from` and `to`. returns `from` if `from` is after `to`.\n\n### `date.birthday(options?)`\n\n```ts\nfunction birthday(options?: { minage?: number; maxage?: number; ref?: temporal.zoneddatetime }): temporal.plaindate;\n```\n\nreturns a `plaindate` with a random age between `minage` (default `18`) and `maxage` (default `80`).\n\n### `date.weekday(locale?)`\n\n```ts\nfunction weekday(locale?: string): string;\n```\n\nreturns a random weekday name. uses the instance locale unless overridden.\n\n### `date.month(locale?)`\n\n```ts\nfunction month(locale?: string): string;\n```\n\nreturns a random month name. uses the instance locale unless overridden.\n\n \n\n## finance\n\n### `finance.amount(options?)`\n\n```ts\nfunction amount(options?: amountoptions): money;\n```\n\n| option | type | default | description |\n| | | | |\n| `min` | `number` | `100` | minimum amount. |\n| `max` | `number` | `10000` | maximum amount. |\n| `currency` | `'usd' \\| 'eur' \\| 'gbp'` | `'usd'` | currency code. |\n\nreturns a coins `money` value with two decimal places.\n\n### `finance.iban(countrycode?)`\n\n```ts\nfunction iban(countrycode?: string): string;\n```\n\nreturns an iban. pass a country code to fix the country; otherwise a random supported country is chosen. the check digits are computed so the iban passes mod 97 validation.\n\n### `finance.bic()`\n\n```ts\nfunction bic(): string;\n```\n\nreturns a bic/swift code of 8 or 11 characters.\n\n### `finance.creditcardnumber(type?)`\n\n```ts\nfunction creditcardnumber(type?: 'visa' | 'mastercard' | 'amex'): string;\n```\n\nreturns a card number with a valid luhn check digit. amex returns 15 digits; others return 16.\n\n### `finance.creditcardcvv(type?)`\n\n```ts\nfunction creditcardcvv(type?: 'visa' | 'mastercard' | 'amex'): string;\n```\n\nreturns a cvv. amex returns 4 digits; others return 3.\n\n### `finance.bitcoinaddress()`\n\n```ts\nfunction bitcoinaddress(): string;\n```\n\nreturns a bitcoin address with a `1`, `3`, or `bc1` prefix.\n\n### `finance.ethereumaddress()`\n\n```ts\nfunction ethereumaddress(): string;\n```\n\nreturns a 42 character ethereum address prefixed with `0x`.\n\n### `finance.transactiontype()`\n\n```ts\nfunction transactiontype(): string;\n```\n\nreturns a random transaction type label.\n\n### `finance.bank()`\n\n```ts\nfunction bank(): string;\n```\n\nreturns a random bank name.\n\n \n\n## location\n\n### `location.city()`\n\n```ts\nfunction city(): string;\n```\n\nreturns a random city from the locale dataset.\n\n### `location.street()`\n\n```ts\nfunction street(): string;\n```\n\nreturns a random street from the locale dataset.\n\n### `location.streetaddress()`\n\n```ts\nfunction streetaddress(): string;\n```\n\nreturns a house number (1–999) followed by a street name.\n\n### `location.zipcode()`\n\n```ts\nfunction zipcode(): string;\n```\n\nreturns a zip code matching the locale's pattern.\n\n### `location.state()`\n\n```ts\nfunction state(): string;\n```\n\nreturns a random state or region from the locale dataset.\n\n### `location.country()`\n\n```ts\nfunction country(): string;\n```\n\nreturns a random country from the locale dataset.\n\n### `location.latitude()`\n\n```ts\nfunction latitude(): number;\n```\n\nreturns a latitude in the range `[ 90, 90]`.\n\n### `location.longitude()`\n\n```ts\nfunction longitude(): number;\n```\n\nreturns a longitude in the range `[ 180, 180]`.\n\n### `location.nearbygpscoordinate(ref?)`\n\n```ts\nfunction nearbygpscoordinate(ref?: coordinate): coordinate;\n```\n\nreturns a coordinate within ~1 degree of `ref`. when `ref` is omitted, a random coordinate is used as the base.\n\n \n\n## lorem\n\n### `lorem.word()`\n\n```ts\nfunction word(): string;\n```\n\nreturns a single random word.\n\n### `lorem.words(count?)`\n\n```ts\nfunction words(count?: number): string;\n```\n\nreturns `count` (default `3`) space joined words.\n\n### `lorem.sentence(wordcount?)`\n\n```ts\nfunction sentence(wordcount?: number): string;\n```\n\nreturns a sentence of `wordcount` words (default 6–12) with a capital first letter and trailing period.\n\n### `lorem.sentences(count?)`\n\n```ts\nfunction sentences(count?: number): string;\n```\n\nreturns `count` (default `3`) space joined sentences.\n\n### `lorem.paragraph(sentencecount?)`\n\n```ts\nfunction paragraph(sentencecount?: number): string;\n```\n\nreturns a paragraph of `sentencecount` sentences (default 3–7).\n\n### `lorem.paragraphs(count?)`\n\n```ts\nfunction paragraphs(count?: number): string;\n```\n\nreturns `count` (default `3`) newline joined paragraphs.\n\n### `lorem.slug(wordcount?)`\n\n```ts\nfunction slug(wordcount?: number): string;\n```\n\nreturns a hyphen joined slug of `wordcount` (default `3`) words.\n\n### `lorem.lines(count?)`\n\n```ts\nfunction lines(count?: number): string;\n```\n\nreturns `count` (default `5`) newline joined lines, each a sentence.\n\n \n\n## system\n\n### `system.fileextension()`\n\n```ts\nfunction fileextension(): string;\n```\n\nreturns a random file extension.\n\n### `system.filename()`\n\n```ts\nfunction filename(): string;\n```\n\nreturns a random file name with extension.\n\n### `system.filepath()`\n\n```ts\nfunction filepath(): string;\n```\n\nreturns a path with 1–4 directory segments and a file name.\n\n### `system.mimetype()`\n\n```ts\nfunction mimetype(): string;\n```\n\nreturns a random mime type.\n\n### `system.semver(options?)`\n\n```ts\nfunction semver(options?: { maxmajor?: number; includeprerelease?: boolean }): string;\n```\n\n| option | type | default | description |\n| | | | |\n| `maxmajor` | `number` | `20` | maximum major version. |\n| `includeprerelease` | `boolean` | `false` | occasionally append a prerelease label. |\n\nreturns a semver string.\n\n### `system.uuid()`\n\n```ts\nfunction uuid(): string;\n```\n\nreturns a random uuid via `crypto.randomuuid()`. **not deterministic** — ignores the seeded `randomsource`. use only when uniqueness matters more than reproducibility.\n\n### `system.port(options?)`\n\n```ts\nfunction port(options?: { min?: number; max?: number }): number;\n```\n\n| option | type | default | description |\n| | | | |\n| `min` | `number` | `1024` | minimum port. |\n| `max` | `number` | `65535` | maximum port. |\n\nreturns a random port number.\n\n### `system.cron()`\n\n```ts\nfunction cron(): string;\n```\n\nreturns a random cron expression from common patterns.\n\n### `system.process()`\n\n```ts\nfunction process(): string;\n```\n\nreturns a random process name of the form `prefix_suffix`.\n\n \n\n## seed\n\nimport from the `seed` subpath:\n\n```ts\nimport { createseed, mulberry32 } from '@vielzeug/illusionist/seed';\n```\n\n### `createseed(seed?)`\n\n```ts\nfunction createseed(seed?: number | string): randomsource;\n```\n\ncreates a `randomsource` from a seed. number seeds are used directly as mulberry32 state. string seeds are hashed to a 32 bit integer. omit the seed for cryptographic randomness via `crypto.getrandomvalues`. throws `illusionistseederror` for non finite numeric seeds.\n\n```ts\nconst a = createseed(12345); // deterministic\nconst b = createseed('hello'); // deterministic (hashed)\nconst c = createseed(); // cryptographic\n```\n\n### `mulberry32(seed)`\n\n```ts\nfunction mulberry32(seed: number): randomsource;\n```\n\nlow level 32 bit prng. not cryptographically secure. returns a `randomsource` producing floats in `[0, 1)`.\n\n \n\n## types\n\n```ts\ntype personlocaledata = {\n readonly firstnamefemale: readonly string[];\n readonly firstnamemale: readonly string[];\n readonly gender: readonly string[];\n readonly jobareas: readonly string[];\n readonly jobtypes: readonly string[];\n readonly lastname: readonly string[];\n readonly prefix: readonly string[];\n readonly suffix: readonly string[];\n};\n\ntype locationlocaledata = {\n readonly cities: readonly string[];\n readonly countries: readonly string[];\n readonly states: readonly string[];\n readonly streets: readonly string[];\n readonly zippattern: string;\n};\n\ntype illusionistlocale = {\n readonly code: string;\n readonly person: personlocaledata;\n readonly location: locationlocaledata;\n};\n\ntype illusionistoptions = {\n seed?: number | string;\n locale: illusionistlocale;\n};\n\ntype illusionist = {\n readonly person: typeof person;\n readonly internet: typeof internet;\n readonly commerce: typeof commerce;\n readonly date: typeof date;\n readonly finance: typeof finance;\n readonly location: typeof location;\n readonly lorem: typeof lorem;\n readonly system: typeof system;\n readonly seed: number | string | undefined;\n readonly locale: illusionistlocale;\n dispose(): void;\n readonly disposed: boolean;\n readonly disposalsignal: abortsignal;\n [symbol.dispose](): void;\n};\n\ntype coordinate = {\n lat: number;\n lng: number;\n};\n\ntype passwordoptions = {\n length?: number;\n memorable?: boolean;\n};\n\ntype priceoptions = {\n readonly min?: number;\n readonly max?: number;\n readonly currency?: 'usd' | 'eur' | 'gbp';\n};\n\ntype amountoptions = {\n readonly min?: number;\n readonly max?: number;\n readonly currency?: 'usd' | 'eur' | 'gbp';\n};\n\n// re exported from @vielzeug/arsenal\ntype randomsource = {\n next(): number; // float in [0, 1)\n};\n```\n\n## errors\n\nall errors extend `illusionisterror`, which extends `error`. use `instanceof illusionisterror` to catch any illusionist originated error.\n\n| error | trigger | notable properties |\n| | | |\n| `illusionisterror` | base class for all illusionist errors | `name`, `message` |\n| `illusionistseederror` | non finite numeric seed passed to `createseed` (`nan`, `infinity`, ` infinity`) | `name`, `message` |\n\n```ts\nimport { illusionisterror, illusionistseederror, createseed } from '@vielzeug/illusionist';\n\ntry {\n createseed(number.nan);\n} catch (error) {\n if (error instanceof illusionistseederror) {\n console.log(error.message);\n }\n}\n```\n",
|
|
654
|
+
"usage": " \ntitle: illusionist — usage guide\ndescription: generate deterministic, locale aware fake data with illusionist.\n \n\n[[toc]]\n\n## basic usage\n\ncreate an illusionist instance with `createillusion`. access data through the eight bound categories. each call consumes from the shared random source, so output is deterministic for a given seed.\n\n```ts\nimport { createillusion } from '@vielzeug/illusionist';\nimport { en } from '@vielzeug/illusionist/locales';\n\nconst illusion = createillusion({ seed: 12345, locale: en });\n\nillusion.person.firstname(); // 'ashley'\nillusion.internet.username(); // 'fvev9zc638m'\nillusion.commerce.productname(); // 'intelligent granite table'\nillusion.date.recent({ days: 7 }); // temporal.zoneddatetime within the last week\nillusion.lorem.sentence(); // 'enim ex non ea minim amet sint laborum proident nisi anim officia.'\n\nillusion.dispose();\n```\n\n## seeded determinism\n\npass a number or string seed to make output reproducible. the same seed always produces the same sequence across runs, machines, and node versions.\n\n```ts\nimport { createillusion } from '@vielzeug/illusionist';\nimport { en } from '@vielzeug/illusionist/locales';\n\nconst a = createillusion({ seed: 12345, locale: en });\nconst b = createillusion({ seed: 12345, locale: en });\n\na.person.fullname() === b.person.fullname(); // true\n\nconst c = createillusion({ seed: 'my test suite', locale: en });\nconst d = createillusion({ seed: 'my test suite', locale: en });\n\nc.internet.email() === d.internet.email(); // true — string seeds are hashed\n```\n\nomit the seed for cryptographic randomness backed by `crypto.getrandomvalues`. output is then non deterministic and unsuitable for snapshots.\n\n```ts\nimport { createillusion } from '@vielzeug/illusionist';\nimport { en } from '@vielzeug/illusionist/locales';\n\nconst random = createillusion({ locale: en });\nrandom.person.fullname(); // different every run\n```\n\n## locale support\n\nimport a locale object and pass it at creation time. the `person` and `location` categories draw from that object's datasets. the `date.weekday` and `date.month` functions use its locale code.\n\n```ts\nimport { createillusion } from '@vielzeug/illusionist';\nimport { de, en } from '@vielzeug/illusionist/locales';\n\nconst english = createillusion({ seed: 1, locale: en });\nconst german = createillusion({ seed: 1, locale: de });\n\nenglish.person.firstname(); // 'mary'\ngerman.person.firstname(); // 'mia'\n\nenglish.location.city(); // 'austin'\ngerman.location.city(); // 'bremen'\n\nenglish.date.month(); // 'december'\ngerman.date.month(); // 'dezember'\n```\n\nthe locale is fixed for the lifetime of an instance. each locale is a separate subpath, so locale data ships only when that subpath is imported; the root package does not statically include english or german data. create a new instance to switch locales.\n\nfor dynamic app switching, load the desired locale before calling the synchronous factory:\n\n```ts\nconst { de } = await import('@vielzeug/illusionist/locales/de');\nconst german = createillusion({ locale: de });\n```\n\n### custom locales\n\nthe shipped `en` and `de` objects are just plain data that `satisfies illusionistlocale`. build your own the same way — import the type, assemble the `person` and `location` datasets, and pass the result to `createillusion`. no registration step; the factory accepts any object that matches the shape.\n\n```ts\nimport { createillusion, type illusionistlocale } from '@vielzeug/illusionist';\n\nconst fr: illusionistlocale = {\n code: 'fr',\n person: {\n firstnamefemale: ['marie', 'camille', 'sophie'],\n firstnamemale: ['louis', 'hugo', 'léo'],\n lastname: ['martin', 'bernard', 'dubois'],\n gender: ['féminin', 'masculin', 'non binaire'],\n jobareas: ['marketing', 'ingénierie', 'ventes'],\n jobtypes: ['directeur', 'ingénieur', 'analyste'],\n prefix: ['m.', 'mme', 'dr.'],\n suffix: ['phd', 'jr.'],\n },\n location: {\n cities: ['paris', 'lyon', 'marseille'],\n countries: ['france', 'belgique', 'suisse'],\n states: ['île de france', 'auvergne rhône alpes', 'provence alpes côte d\\'azur'],\n streets: ['rue de la paix', 'avenue des champs élysées', 'boulevard saint germain'],\n zippattern: '#####',\n },\n};\n\nconst illusion = createillusion({ seed: 42, locale: fr });\n\nillusion.person.fullname(); // 'camille dubois'\nillusion.location.city(); // 'marseille'\nillusion.person.jobtitle(); // 'marketing ingénieur'\n```\n\n`date.weekday()` and `date.month()` currently ship english and german name arrays only; a custom locale code falls through to the english set. for other languages, format a generated `temporal` date with `@vielzeug/tempo`'s `format()` and your own `intl.datetimeformat` options.\n\nuse `satisfies illusionistlocale` instead of a bare type annotation to get error locality — typescript points at the offending field rather than the whole object.\n\n## category overview\n\n| category | example call | returns |\n| | | |\n| `person` | `illusion.person.fullname()` | `string` |\n| `internet` | `illusion.internet.email()` | `string` |\n| `commerce` | `illusion.commerce.price()` | `money` (coins) |\n| `date` | `illusion.date.past({ years: 1 })` | `temporal.zoneddatetime` (tempo) |\n| `finance` | `illusion.finance.iban()` | `string` |\n| `location` | `illusion.location.streetaddress()` | `string` |\n| `lorem` | `illusion.lorem.paragraph()` | `string` |\n| `system` | `illusion.system.uuid()` | `string` |\n\n## working with other vielzeug libraries\n\nillusionist integrates with other vielzeug packages at the return type level. `commerce.price()` and `finance.amount()` return coins `money`, so you can format, add, or allocate them directly. `date` functions return tempo `temporal` objects, so you can shift, compare, or format them.\n\n```ts\nimport { format, add, money } from '@vielzeug/coins';\nimport { createillusion } from '@vielzeug/illusionist';\nimport { en } from '@vielzeug/illusionist/locales';\nimport { formatzoneddatetimeiso } from '@vielzeug/tempo';\n\nconst illusion = createillusion({ seed: 42, locale: en });\n\nconst price = illusion.commerce.price({ min: 10, max: 50, currency: 'eur' });\nconst tax = money('5.00', price.currency);\nconst total = add(price, tax);\n\nconsole.log(format(total, { locale: 'de de' }));\n\nconst orderdate = illusion.date.recent({ days: 30 });\nconsole.log(formatzoneddatetimeiso(orderdate));\n```\n\n## best practices\n\n pass a seed in tests and ci; omit it only for one off non reproducible mocks.\n create one instance per test case so each test starts from a known random state.\n call `dispose()` (or use `using`) when an instance is no longer needed, especially in long running processes.\n fix the locale at creation time; create a new instance to switch locales rather than mixing.\n use string seeds for named test suites — they are self documenting and hash to a stable number.\n combine `person`, `internet`, and `location` to build internally consistent mock entities.\n treat `money` and `temporal` return values as first class — pass them to coins and tempo functions directly.\n avoid sharing a single instance across concurrent async tasks; each call advances the shared random source.\n `system.uuid()` uses `crypto.randomuuid()`, not the seeded source. do not use it in deterministic fixtures or snapshot tests.\n",
|
|
655
|
+
"examples": " \ntitle: illusionist — examples\ndescription: practical examples and recipes for @vielzeug/illusionist.\n \n\n[[toc]]\n\n## generating test fixtures\n\nbuild a batch of realistic records from a fixed seed. the same seed reproduces the same fixtures in every run. for a full vitest setup, see the [test fixtures recipe](./examples/test fixtures.md).\n\n```ts\nimport { createillusion } from '@vielzeug/illusionist';\nimport { en } from '@vielzeug/illusionist/locales';\n\nconst illusion = createillusion({ seed: 'fixtures v1', locale: en });\n\nconst users = array.from({ length: 10 }, () => ({\n name: illusion.person.fullname(),\n email: illusion.internet.email(),\n address: illusion.location.streetaddress(),\n city: illusion.location.city(),\n zip: illusion.location.zipcode(),\n}));\n\nillusion.dispose();\n```\n\n## seeded test data for snapshot testing\n\nuse a named string seed so snapshot output is stable across ci runs. each test creates its own instance to avoid cross test random state drift.\n\n```ts\nimport { createillusion } from '@vielzeug/illusionist';\nimport { en } from '@vielzeug/illusionist/locales';\n\ntest('order receipt snapshot', () => {\n const illusion = createillusion({ seed: 'order receipt', locale: en });\n\n const order = {\n customer: illusion.person.fullname(),\n product: illusion.commerce.productname(),\n price: illusion.commerce.price({ min: 5, max: 50 }),\n date: illusion.date.recent({ days: 7 }),\n };\n\n expect(order).tomatchinlinesnapshot();\n illusion.dispose();\n});\n```\n\n## locale specific data (german)\n\nimport the german locale object to draw names, cities, and weekday labels from its dataset.\n\n```ts\nimport { createillusion } from '@vielzeug/illusionist';\nimport { de } from '@vielzeug/illusionist/locales';\n\nconst illusion = createillusion({ seed: 42, locale: de });\n\nillusion.person.fullname(); // 'mathilda scholz'\nillusion.location.city(); // 'nürnberg'\nillusion.location.zipcode(); // '15268'\nillusion.date.weekday(); // 'donnerstag'\nillusion.date.month(); // 'märz'\n\nillusion.dispose();\n```\n\n## e commerce mock data\n\ncombine `person`, `commerce`, and `location` to build a consistent customer order shipping record. `commerce.price()` returns coins `money`, so you can format it directly.\n\n```ts\nimport { format } from '@vielzeug/coins';\nimport { createillusion } from '@vielzeug/illusionist';\nimport { en } from '@vielzeug/illusionist/locales';\n\nconst illusion = createillusion({ seed: 'ecommerce mock', locale: en });\n\nconst order = {\n customer: {\n name: illusion.person.fullname(),\n email: illusion.internet.email(),\n },\n item: illusion.commerce.productname(),\n price: illusion.commerce.price({ min: 20, max: 200, currency: 'eur' }),\n shipping: {\n address: illusion.location.streetaddress(),\n city: illusion.location.city(),\n zip: illusion.location.zipcode(),\n country: illusion.location.country(),\n },\n};\n\nconsole.log(format(order.price, { locale: 'de de' }));\nillusion.dispose();\n```\n\n## database seeding pattern\n\ngenerate rows for a database seed script. use a stable seed so the seed file is reproducible and reviewable.\n\n```ts\nimport { createillusion } from '@vielzeug/illusionist';\nimport { en } from '@vielzeug/illusionist/locales';\n\nconst illusion = createillusion({ seed: 'db seed 2024', locale: en });\n\nconst products = array.from({ length: 50 }, () => ({\n name: illusion.commerce.productname(),\n department: illusion.commerce.department(),\n price: illusion.commerce.price({ min: 1, max: 500 }),\n description: illusion.commerce.productdescription(),\n}));\n\nconst customers = array.from({ length: 100 }, () => ({\n firstname: illusion.person.firstname(),\n lastname: illusion.person.lastname(),\n email: illusion.internet.email(),\n createdat: illusion.date.past({ years: 2 }),\n}));\n\nillusion.dispose();\n```\n\n## disposal in long running processes\n\ncall `dispose()` when an instance is no longer needed. in long running processes, use `using` to release instances automatically at scope exit.\n\n```ts\nimport { createillusion } from '@vielzeug/illusionist';\nimport { en } from '@vielzeug/illusionist/locales';\n\nfunction generatebatch(seed: number) {\n using illusion = createillusion({ seed, locale: en });\n\n return array.from({ length: 5 }, () => ({\n name: illusion.person.fullname(),\n email: illusion.internet.email(),\n }));\n // illusion.dispose() runs automatically at scope exit\n}\n```\n"
|
|
656
|
+
},
|
|
657
|
+
"examples": [
|
|
658
|
+
{
|
|
659
|
+
"id": "commerce-basic",
|
|
660
|
+
"text": "commerce products, departments, and prices import { createillusion } from '@vielzeug/illusionist'\nimport { en } from '@vielzeug/illusionist/locales'\n\nconst illusion = createillusion({ seed: 12345, locale: en })\n\nconsole.log(illusion.commerce.productname())\nconsole.log(illusion.commerce.department())\nconsole.log(illusion.commerce.price({ min: 10, max: 50, currency: 'eur' }))\nconsole.log(illusion.commerce.productdescription())"
|
|
661
|
+
},
|
|
662
|
+
{
|
|
663
|
+
"id": "date-basic",
|
|
664
|
+
"text": "date past, future, birthdays, and locale labels import { createillusion } from '@vielzeug/illusionist'\nimport { en } from '@vielzeug/illusionist/locales'\n\nconst illusion = createillusion({ seed: 12345, locale: en })\n\nconsole.log(illusion.date.past({ years: 2 }).tostring())\nconsole.log(illusion.date.future({ years: 1 }).tostring())\nconsole.log(illusion.date.recent({ days: 7 }).tostring())\nconsole.log(illusion.date.birthday({ minage: 25, maxage: 35 }).tostring())\nconsole.log(illusion.date.weekday())\nconsole.log(illusion.date.month())"
|
|
665
|
+
},
|
|
666
|
+
{
|
|
667
|
+
"id": "determinism-basic",
|
|
668
|
+
"text": "seed deterministic output from the same seed import { createillusion } from '@vielzeug/illusionist'\nimport { en } from '@vielzeug/illusionist/locales'\n\nconst a = createillusion({ seed: 'test fixture', locale: en })\nconst b = createillusion({ seed: 'test fixture', locale: en })\n\nconsole.log(a.person.fullname() === b.person.fullname())\nconsole.log(a.internet.email() === b.internet.email())\n\na.dispose()\nb.dispose()"
|
|
669
|
+
},
|
|
670
|
+
{
|
|
671
|
+
"id": "finance-basic",
|
|
672
|
+
"text": "finance ibans, cards, bics, and crypto addresses import { createillusion } from '@vielzeug/illusionist'\nimport { en } from '@vielzeug/illusionist/locales'\n\nconst illusion = createillusion({ seed: 12345, locale: en })\n\nconsole.log(illusion.finance.iban())\nconsole.log(illusion.finance.iban('de'))\nconsole.log(illusion.finance.bic())\nconsole.log(illusion.finance.creditcardnumber('visa'))\nconsole.log(illusion.finance.creditcardcvv('amex'))\nconsole.log(illusion.finance.ethereumaddress())"
|
|
673
|
+
},
|
|
674
|
+
{
|
|
675
|
+
"id": "internet-basic",
|
|
676
|
+
"text": "internet emails, urls, and network addresses import { createillusion } from '@vielzeug/illusionist'\nimport { en } from '@vielzeug/illusionist/locales'\n\nconst illusion = createillusion({ seed: 12345, locale: en })\n\nconsole.log(illusion.internet.email())\nconsole.log(illusion.internet.username())\nconsole.log(illusion.internet.url())\nconsole.log(illusion.internet.ip())\nconsole.log(illusion.internet.ip(6))\nconsole.log(illusion.internet.mac())"
|
|
677
|
+
},
|
|
678
|
+
{
|
|
679
|
+
"id": "locale-basic",
|
|
680
|
+
"text": "locales english and german side by side import { createillusion } from '@vielzeug/illusionist'\nimport { en, de } from '@vielzeug/illusionist/locales'\n\nconst illusion = {\n en: createillusion({ seed: 42, locale: en }),\n de: createillusion({ seed: 42, locale: de })\n};\n\nconsole.log(illusion.en.person.fullname())\nconsole.log(illusion.de.person.fullname())\nconsole.log(illusion.en.location.city())\nconsole.log(illusion.de.location.city())\nconsole.log(illusion.en.date.month())\nconsole.log(illusion.de.date.month())"
|
|
681
|
+
},
|
|
682
|
+
{
|
|
683
|
+
"id": "location-basic",
|
|
684
|
+
"text": "location addresses, regions, and coordinates import { createillusion } from '@vielzeug/illusionist'\nimport { en } from '@vielzeug/illusionist/locales'\n\nconst illusion = createillusion({ seed: 12345, locale: en })\n\nconsole.log(illusion.location.city())\nconsole.log(illusion.location.streetaddress())\nconsole.log(illusion.location.zipcode())\nconsole.log(illusion.location.state())\nconsole.log(illusion.location.country())\nconsole.log(illusion.location.latitude())\nconsole.log(illusion.location.longitude())"
|
|
685
|
+
},
|
|
686
|
+
{
|
|
687
|
+
"id": "person-basic",
|
|
688
|
+
"text": "person names, gender, and job titles import { createillusion } from '@vielzeug/illusionist'\nimport { en } from '@vielzeug/illusionist/locales'\n\nconst illusion = createillusion({ seed: 12345, locale: en })\n\nconsole.log(illusion.person.fullname())\nconsole.log(illusion.person.firstname())\nconsole.log(illusion.person.lastname())\nconsole.log(illusion.person.jobtitle())\nconsole.log(illusion.person.gender())"
|
|
689
|
+
},
|
|
690
|
+
{
|
|
691
|
+
"id": "system-basic",
|
|
692
|
+
"text": "system files, semver, uuids, ports, and cron import { createillusion } from '@vielzeug/illusionist'\nimport { en } from '@vielzeug/illusionist/locales'\n\nconst illusion = createillusion({ seed: 12345, locale: en })\n\nconsole.log(illusion.system.filepath())\nconsole.log(illusion.system.semver({ includeprerelease: true }))\nconsole.log(illusion.system.uuid())\nconsole.log(illusion.system.port())\nconsole.log(illusion.system.cron())\nconsole.log(illusion.system.process())"
|
|
693
|
+
}
|
|
694
|
+
],
|
|
695
|
+
"exports": "createillusion createseed mulberry32",
|
|
696
|
+
"keywords": "fake data mock seed faker test fixtures deterministic",
|
|
697
|
+
"name": "@vielzeug/illusionist",
|
|
698
|
+
"related": "arsenal coins tempo",
|
|
699
|
+
"slug": "illusionist",
|
|
700
|
+
"source": "export * from './commerce/commerce';\nexport * from './date/date';\nexport * from './errors';\nexport * from './factory';\nexport * from './finance/finance';\nexport * from './internet/internet';\nexport * from './location/location';\nexport * from './lorem/lorem';\nexport * from './person/person';\nexport * from './seed/create seed';\nexport * from './seed/mulberry32';\nexport * from './types';\n"
|
|
701
|
+
},
|
|
600
702
|
{
|
|
601
703
|
"category": "app infrastructure",
|
|
602
704
|
"description": "target local keyboard shortcut manager with chords, event aware guards, modifier aliases, and terminal disposal.",
|
|
@@ -682,8 +784,8 @@
|
|
|
682
784
|
"description": "framework neutral locale catalogs, typed translations, and explicit plural messages.",
|
|
683
785
|
"docs": {
|
|
684
786
|
"index": " \ntitle: lingua — explicit localization for typescript\ndescription: framework neutral locale catalogs, typed translations, and explicit plural messages.\npackage: lingua\ncategory: i18n\nkeywords: [internationalization, translations, pluralization, locale, i18n, catalog loading]\nrelated: [ripple, wayfinder, courier]\nexports: [createcatalogtranslator, createtranslationstore, createtranslator, hydratetranslationstore, linguaerror, linguadisposederror, linguainvalidcatalogerror, linguainvalidlocaleerror, linguainvalidpluralcounterror, linguainvalidstateerror, linguamissingcatalogerror]\nenvironments: [browser, node, ssr, deno]\n \n\n<! markdownlint disable md025 md033 md060 >\n\n<packagehero package=\"lingua\" />\n\n## why lingua?\n\nlingua separates immutable translation from mutable locale state. use one catalog per locale, then select static or stateful api from whether locale can change.\n\n```ts\n// before\nconst message = catalogs[locale]?.inbox?.[count === 1 ? 'one' : 'other'] ?? 'inbox';\n\n// after\nconst output = i18n.translate('inbox', { count });\n```\n\n| feature | lingua | i18next | formatjs |\n| | | | |\n| bundle size | <packageinfo package=\"lingua\" type=\"size\" /> | varies by selected modules | varies by selected modules |\n| zero runtime dependencies | <ore icon name=\"check\" size=\"16\"></ore icon> | <ore icon name=\"triangle alert\" size=\"16\"></ore icon> | <ore icon name=\"triangle alert\" size=\"16\"></ore icon> |\n| explicit plural catalog nodes | <ore icon name=\"check\" size=\"16\"></ore icon> | convention/config dependent | icu message dependent |\n| declared lazy locale catalogs | <ore icon name=\"check\" size=\"16\"></ore icon> | plugin/config dependent | application defined |\n| immutable locale snapshots | <ore icon name=\"check\" size=\"16\"></ore icon> | application defined | application defined |\n\n<div class=\"decision callout\">\n\n**use lingua when** you need a compact typescript runtime with explicit catalog structure, deterministic fallback, and framework neutral subscriptions.\n\n**consider i18next or formatjs when** you need their plugin ecosystems, message extraction pipelines, or framework specific integrations.\n\n</div>\n\n## installation\n\n::: code group\n\n```sh [pnpm]\npnpm add @vielzeug/lingua\n```\n\n```sh [npm]\nnpm install @vielzeug/lingua\n```\n\n```sh [yarn]\nyarn add @vielzeug/lingua\n```\n\n:::\n\n## quick start\n\ncreate locale store with static catalogs, then dispose it when owner ends.\n\n```ts\nimport { createtranslationstore } from '@vielzeug/lingua';\n\nconst i18n = createtranslationstore({\n catalogs: {\n de: { inbox: { plural: { one: 'eine nachricht', other: '{count} nachrichten' } } },\n en: { inbox: { plural: { one: 'one message', other: '{count} messages' } } },\n },\n locale: 'en',\n});\n\ntry {\n console.log(i18n.translate('inbox', { count: 3 }));\n await i18n.setlocale('de');\n console.log(i18n.translate('inbox', { count: 1 }));\n} finally {\n i18n.dispose();\n}\n```\n\n## features\n\n<div class=\"features grid\">\n\n `createcatalogtranslator()` compiles one immutable fixed locale catalog.\n `createtranslator()` compiles immutable locale keyed catalogs.\n `createtranslationstore()` manages locale changes and declared catalogs.\n `translate()` renders text and plural messages through explicit catalog nodes.\n `translatedynamic()` makes runtime key lookup explicit.\n `load()` deduplicates lazy catalog loading per locale.\n `getsnapshot()` and `subscribe()` expose immutable translator revisions.\n `serialize()` and `hydratetranslationstore()` transfer resolved ssr catalogs.\n `createformatter()` and `validatecatalog()` remain isolated subpath tools.\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 [ripple](../ripple/index.md) adapts lingua snapshots into reactive application state.\n [courier](../courier/index.md) can fetch locale catalogs before passing them to lingua loaders.\n [wayfinder](../wayfinder/index.md) can drive locale selection from route state.\n\n</div>\n\n<! markdownlint enable md025 md033 md060 >\n",
|
|
685
|
-
"api": " \ntitle: lingua — api reference\ndescription: complete api reference for @vielzeug/lingua.\n \n\n[[toc]]\n\n## api overview\n\n| symbol | purpose | execution mode | common gotcha |\n| | | | |\n| `createcatalogtranslator()` | compile one immutable locale catalog | sync | no fallback locales |\n| `createtranslator()` | compile immutable locale catalogs | sync | locale is fixed for translator lifetime |\n| `createtranslationstore()` | create mutable locale and catalog store | sync | load lazy locale explicitly |\n| `hydratetranslationstore()` | create store from serialized loaded catalogs | sync | serialized state never includes loaders |\n| `createformatter()` | format intl values from `/format` | sync | import from subpath |\n| `validatecatalog()` | check explicit plural forms from `/validate` | sync | import from subpath |\n| `linguaerror` | base class for lingua errors | sync | use `linguaerror.is()` for broad narrowing |\n\n## package entry point\n\n| import | purpose |\n| | |\n| `@vielzeug/lingua` | translation factories, state types, and lingua errors |\n| `@vielzeug/lingua/format` | `createformatter()` and formatter types |\n| `@vielzeug/lingua/validate` | `validatecatalog()` and `validationissue` |\n\n## translation factories\n\n### createcatalogtranslator\n\n```ts\nfunction createcatalogtranslator<c extends catalog>(\n catalog: c,\n options?: catalogtranslatoroptions,\n): translator<c>;\n```\n\ncompiles one catalog and returns an immutable fixed locale translator. locale defaults to `en` and controls plural selection and diagnostics.\n\n| parameter | type | description |\n| | | |\n| `catalog` | `c` | one catalog containing only messages and grouping objects |\n| `options` | `catalogtranslatoroptions` | locale and missing message handlers; fallback is unavailable |\n\n**returns:** `translator<c>`.\n\n**example:**\n\n```ts\nimport { createcatalogtranslator } from '@vielzeug/lingua';\n\nconst translator = createcatalogtranslator(\n { save: 'enregistrer' },\n { locale: 'fr' },\n);\n\ntranslator.translate('save');\n```\n\n \n\n### createtranslator\n\n```ts\nfunction createtranslator<c extends catalog>(catalogs: catalogs<c>, options?: translatoroptions): translator<c>;\n```\n\ncompiles locale catalogs and returns immutable translator.\n\n| parameter | type | description |\n| | | |\n| `catalogs` | `catalogs<c>` | locale keyed catalog objects |\n| `options` | `translatoroptions` | locale, fallback chain, and missing message handlers |\n\n**returns:** `translator<c>`.\n\n**example:**\n\n```ts\nimport { createtranslator } from '@vielzeug/lingua';\n\nconst translator = createtranslator(\n { en: { save: 'save' }, fr: { save: 'enregistrer' } },\n { locale: 'fr' },\n);\n\ntranslator.translate('save');\n```\n\n| method | signature | returns |\n| | | |\n| `translate` | `(textkey, options?)` or `(pluralkey, { count, ordinal?, values? })` | rendered string |\n| `translatedynamic` | `(key, options?)` | rendered string for runtime key |\n| `segments` | `(textkey, { values })` or `(pluralkey, { count, ordinal?, values? })` | string and typed value segments |\n| `segmentsdynamic` | `(key, options)` | segments for runtime key |\n| `locale` | `locale` | resolved active locale |\n\n \n\n### createtranslationstore\n\n```ts\nfunction createtranslationstore<c extends catalog>(options: translationstoreoptions<c>): translationstore<c>;\n```\n\ncreates catalog store, current locale state, and immutable translator snapshots.\n\n| parameter | type | description |\n| | | |\n| `options.catalogs` | `catalogsources<c>` | static catalogs or lazy locale loaders |\n| `options.locale` | `locale` | initial locale; defaults to `en` |\n| `options.fallback` | `locale \\| readonly locale[]` | fallback locale chain |\n| `options.onmissingkey` | `(key, locale) => string` | missing message handler |\n| `options.onmissingvalue` | `(name, key, locale) => string` | missing interpolation handler |\n\n**returns:** `translationstore<c>`, with every `translator<c>` method plus lifecycle methods.\n\n**example:**\n\n```ts\nimport { createtranslationstore } from '@vielzeug/lingua';\n\nconst translations = createtranslationstore({\n catalogs: { en: { title: 'home' }, fr: { title: 'accueil' } },\n locale: 'en',\n});\n\nawait translations.setlocale('fr');\ntranslations.translate('title');\n```\n\n| method or property | signature | returns |\n| | | |\n| `translate` | translator method | rendered string |\n| `segments` | translator method | string and typed value segments |\n| `load` | `({ locale? })` | `promise<void>` after catalog resolution |\n| `setlocale` | `(locale)` | `promise<void>` after locale commit; never loads implicitly |\n| `isloaded` | `({ locale? })` | `boolean` |\n| `getsnapshot` | `()` | `translationsnapshot<c>` |\n| `subscribe` | `(listener, { immediate?, signal? })` | unsubscribe function |\n| `serialize` | `()` | loader free `translationstate<c>` |\n| `dispose` | `()` | `void` |\n| `locale` | `locale` | current canonical locale |\n| `disposed` | `boolean` | disposal state |\n| `disposalsignal` | `abortsignal` | aborts on disposal |\n| `[symbol.dispose]` | `()` | delegates to `dispose()` |\n\n \n\n### hydratetranslationstore\n\n```ts\nfunction hydratetranslationstore<c extends catalog>(\n state: translationstate<c>,\n options?: omit<translationstoreoptions<c>, 'locale' | 'catalogs'>,\n): translationstore<c>;\n```\n\ncreates translation store from ssr state payload containing resolved raw catalogs.\n\n| parameter | type | description |\n| | | |\n| `state` | `translationstate<c>` | version `3`, active locale, and loader free catalogs |\n| `options` | `omit<translationstoreoptions<c>, 'locale' \\| 'catalogs'>` | fallback and missing message handlers |\n\n**returns:** `translationstore<c>`.\n\n**example:**\n\n```ts\nimport { createtranslationstore, hydratetranslationstore } from '@vielzeug/lingua';\n\nconst server = createtranslationstore({ catalogs: { en: { title: 'home' } }, locale: 'en' });\nconst client = hydratetranslationstore(server.serialize());\n\nclient.translate('title');\n```\n\n## formatting and validation\n\n### createformatter\n\n```ts\nfunction createformatter(source: string | (() => string)): formatter;\n```\n\ncreates cached intl formatters using static locale or locale getter.\n\n| parameter | type | description |\n| | | |\n| `source` | `string \\| (() => string)` | static locale or locale getter |\n\n**returns:** `formatter`.\n\n**example:**\n\n```ts\nimport { createformatter } from '@vielzeug/lingua/format';\n\nconst formatter = createformatter('en us');\nformatter.currency(19.99, 'usd');\n```\n\n| method | signature | returns |\n| | | |\n| `number` | `(value, options?)` | `string` |\n| `currency` | `(value, currency, options?)` | `string` |\n| `date` | `(value, options?)` | `string` |\n| `relative` | `(value, unit, options?)` | `string` |\n| `list` | `(value, options?)` | `string` |\n| `duration` | `(value, options?)` | `string` |\n\n### validatecatalog\n\n```ts\nfunction validatecatalog(catalog: catalog, locale: locale): validationissue[];\n```\n\nvalidates explicit plural messages against locale plural categories after catalog structural validation.\n\n| parameter | type | description |\n| | | |\n| `catalog` | `catalog` | explicit catalog to validate |\n| `locale` | `locale` | bcp 47 locale tag |\n\n**returns:** `validationissue[]`.\n\n**example:**\n\n```ts\nimport { validatecatalog } from '@vielzeug/lingua/validate';\n\nvalidatecatalog({ inbox: { plural: { one: 'one message' } } }, 'en');\n```\n\n## types\n\n```ts\ntype locale = string;\ntype pluralcategory = intl.ldmlpluralrule;\ntype pluralmessage = { readonly plural: partial<record<pluralcategory, string>> };\ntype catalognode = catalog | pluralmessage | string;\ntype catalog = { readonly [key: string]: catalognode };\ntype catalogs<c extends catalog = catalog> = record<locale, c>;\ntype catalogtranslatoroptions = omit<translatoroptions, 'fallback'>;\ntype catalogloader<c extends catalog = catalog> = () => promise<c>;\ntype catalogsource<c extends catalog = catalog> = c | catalogloader<c>;\ntype catalogsources<c extends catalog = catalog> = record<locale, catalogsource<c>>;\n\ntype translationstoreoptions<c extends catalog = catalog> = translatoroptions & {\n catalogs: catalogsources<c>;\n};\n\ntype translationstate<c extends catalog = catalog> = {\n readonly catalogs: catalogs<c>;\n readonly locale: locale;\n readonly version: 3;\n};\n\ntype translationsnapshot<c extends catalog = catalog> = {\n readonly locale: locale;\n readonly revision: number;\n readonly translator: translator<c>;\n};\n\ntype translationstore<c extends catalog = catalog> = translator<c> & {\n readonly disposalsignal: abortsignal;\n dispose(): void;\n readonly disposed: boolean;\n getsnapshot(): translationsnapshot<c>;\n isloaded(options?: { locale?: locale }): boolean;\n load(options?: { locale?: locale }): promise<void>;\n serialize(): translationstate<c>;\n setlocale(locale: locale): promise<void>;\n subscribe(listener: (snapshot: translationsnapshot<c>) => void, options?: subscribeoptions): () => void;\n [symbol.dispose](): void;\n};\n\ntype translator<c extends catalog = catalog> = {\n readonly locale: locale;\n segments<v>(key: textkey<c>, options: translateoptions & { values: record<string, v> }): array<string | v>;\n segments<v>(key: pluralkey<c>, options: pluraloptions & { values?: record<string, v> }): array<string | number | v>;\n segmentsdynamic<v>(\n key: string,\n options: (translateoptions | pluraloptions) & { values?: record<string, v> },\n ): array<string | number | v>;\n translate(key: textkey<c>, options?: translateoptions): string;\n translate(key: pluralkey<c>, options: pluraloptions): string;\n translatedynamic(key: string, options?: translateoptions | pluraloptions): string;\n};\n```\n\n```ts\ntype values = record<string, unknown>;\ntype translateoptions = { values?: values };\ntype pluraloptions = translateoptions & { count: number; ordinal?: boolean };\ntype translatoroptions = {\n fallback?: locale | readonly locale[];\n locale?: locale;\n onmissingkey?: (key: string, locale: locale) => string;\n onmissingvalue?: (name: string, key: string, locale: locale) => string;\n};\ntype subscribeoptions = { immediate?: boolean; signal?: abortsignal };\n\ntype messagekey<\n c,\n prefix extends string = '',\n depth extends readonly unknown[] = readonly [1, 1, 1, 1, 1, 1],\n> = depth extends readonly [unknown, ...infer rest]\n ? c extends string | pluralmessage\n ? prefix\n : c extends catalog\n ? {\n [k in string & keyof c]: messagekey<c[k], prefix extends '' ? k : `${prefix}.${k}`, rest>;\n }[string & keyof c]\n : never\n : never;\n\ntype textkey<\n c,\n prefix extends string = '',\n depth extends readonly unknown[] = readonly [1, 1, 1, 1, 1, 1],\n> = depth extends readonly [unknown, ...infer rest]\n ? c extends string\n ? prefix\n : c extends catalog\n ? {\n [k in string & keyof c]: textkey<c[k], prefix extends '' ? k : `${prefix}.${k}`, rest>;\n }[string & keyof c]\n : never\n : never;\n\ntype pluralkey<\n c,\n prefix extends string = '',\n depth extends readonly unknown[] = readonly [1, 1, 1, 1, 1, 1],\n> = depth extends readonly [unknown, ...infer rest]\n ? c extends pluralmessage\n ? prefix\n : c extends catalog\n ? {\n [k in string & keyof c]: pluralkey<c[k], prefix extends '' ? k : `${prefix}.${k}`, rest>;\n }[string & keyof c]\n : never\n : never;\n\ntype durationvalue = partial<record<\n 'days' | 'hours' | 'microseconds' | 'milliseconds' | 'minutes' | 'months' | 'nanoseconds' | 'seconds' | 'weeks' | 'years',\n number\n>>;\n\ntype durationformatoptions = {\n hours?: '2 digit' | 'numeric';\n microseconds?: 'numeric';\n milliseconds?: 'numeric';\n minutes?: '2 digit' | 'numeric';\n nanoseconds?: 'numeric';\n seconds?: '2 digit' | 'numeric';\n style?: 'digital' | 'long' | 'narrow' | 'short';\n};\n\ntype listformatoptions = { style?: 'long' | 'narrow' | 'short'; type?: 'and' | 'or' };\n\ntype formatter = {\n currency(value: number, currency: string, options?: omit<intl.numberformatoptions, 'currency' | 'style'>): string;\n date(value: date | number, options?: intl.datetimeformatoptions): string;\n duration(value: durationvalue, options?: durationformatoptions): string;\n list(value: array<string | number>, options?: listformatoptions): string;\n number(value: number, options?: intl.numberformatoptions): string;\n relative(value: number, unit: intl.relativetimeformatunit, options?: intl.relativetimeformatoptions): string;\n};\n\ntype validationissue = { key: string; locale: locale; missing: intl.ldmlpluralrule };\n```\n\n## errors\n\n| error | trigger |\n| | |\n| `linguadisposederror` | state mutation or subscription after `dispose()` |\n| `linguainvalidcatalogerror` | invalid catalog node or reserved key |\n| `linguainvalidlocaleerror` | invalid bcp 47 locale tag |\n| `linguainvalidpluralcounterror` | non finite plural count |\n| `linguainvalidstateerror` | unsupported serialized state version |\n| `linguamissingcatalogerror` | catalog has no source for requested locale |\n",
|
|
686
|
-
"usage": " \ntitle: lingua — usage guide\ndescription: translate explicit catalogs, load lazy locales, and connect locale snapshots to ui state.\n \n\n[[toc]]\n\n## basic usage\n\ncreate i18n store from locale keyed catalogs. strings are text messages; plural messages use `{ plural: ... }`.\n\n```ts\nimport { createtranslationstore } from '@vielzeug/lingua';\n\nconst i18n = createtranslationstore({\n catalogs: {\n en: {\n greeting: 'hello, {name}!',\n inbox: { plural: { one: 'one message', other: '{count} messages' } },\n },\n },\n locale: 'en',\n});\n\nconsole.log(i18n.translate('greeting', { values: { name: 'ada' } }));\nconsole.log(i18n.translate('inbox', { count: 3 }));\n```\n\ncall `dispose()` when store belongs to temporary request, test, or route owner.\n\n## define explicit catalogs\n\nuse nested objects only to group keys. a plural message always has `plural`, so regular objects containing `one` or `other` remain groups.\n\n```ts\nconst catalog = {\n account: {\n greeting: 'hello, {name}!',\n unread: { plural: { one: 'one unread message', other: '{count} unread messages' } },\n },\n};\n```\n\nuse `{ values }` for text replacements. pass `count` at top level for plural selection; lingua injects it into selected template. absent replacements render as `{name}` by default. `segments()` preserves an own `undefined` or `null` value; omit property to receive `{name}`.\n\ncatalogs contain strings, grouping objects, and explicit `{ plural: ... }` messages only. keep application data outside catalog, then translate display labels while constructing it.\n\n```ts\nimport { createcatalogtranslator } from '@vielzeug/lingua';\n\nconst messages = {\n status: { blocked: 'blocked', done: 'done', inprogress: 'in progress' },\n};\nconst statusdefinitions = [\n { labelkey: 'status.inprogress', value: 'in progress' },\n { labelkey: 'status.blocked', value: 'blocked' },\n { labelkey: 'status.done', value: 'done' },\n] as const;\nconst translator = createcatalogtranslator(messages);\nconst statusoptions = statusdefinitions.map(({ labelkey, value }) => ({ label: translator.translate(labelkey), value }));\n```\n\n## render framework content\n\nuse `segments()` when replacements are framework nodes, links, or other values that must not be stringified.\n\n```ts\nimport { createcatalogtranslator } from '@vielzeug/lingua';\n\nconst translator = createcatalogtranslator({ error: 'try {retry} or {support}.' });\n\nconst retry = { href: '/retry', label: 'retry' };\nconst support = { href: '/support', label: 'support' };\n\nconsole.log(translator.segments('error', { values: { retry, support } }));\n```\n\nrender returned array with framework fragment or list primitive. give ui values consumer owned keys before passing them to `segments()`; lingua preserves value identity and never clones or mutates them.\n\n## use static catalogs\n\nuse `createcatalogtranslator()` when one catalog and locale stay fixed for translator lifetime. it defaults locale to `en`; pass `locale` when plural rules or diagnostics need another locale. lingua snapshots catalog messages during construction. do not mutate source catalog objects afterward.\n\n```ts\nimport { createcatalogtranslator } from '@vielzeug/lingua';\n\nconst translator = createcatalogtranslator(\n { save: 'enregistrer' },\n { locale: 'fr' },\n);\n\nconsole.log(translator.translate('save'));\n```\n\nuse `createtranslator()` when fixed translation requires locale keyed catalogs and fallback resolution.\n\n```ts\nimport { createtranslator } from '@vielzeug/lingua';\n\nconst translator = createtranslator(\n { en: { save: 'save' }, fr: { save: 'enregistrer' } },\n { locale: 'fr' },\n);\n\nconsole.log(translator.translate('save'));\n```\n\n## load catalogs and switch locales\n\ndeclare one static catalog or lazy loader per locale. switch locale, then load it explicitly when source is lazy.\n\n```ts\nimport { createtranslationstore } from '@vielzeug/lingua';\n\nconst i18n = createtranslationstore({\n catalogs: {\n en: { navigation: { settings: 'settings' } },\n fr: async () => ({ navigation: { settings: 'réglages' } }),\n },\n locale: 'en',\n});\n\nawait i18n.setlocale('fr');\nawait i18n.load();\nconsole.log(i18n.translate('navigation.settings'));\n```\n\nconcurrent loads for same locale share work. `setlocale()` never triggers hidden loads.\n\n## subscribe to immutable snapshots\n\nsubscribe when ui state must change with locale or loaded active/fallback catalog. every callback receives snapshot containing translator for that revision.\n\n```ts\nconst unsubscribe = i18n.subscribe(\n ({ locale, translator }) => {\n console.log(locale, translator.translate('navigation.settings'));\n },\n { immediate: true },\n);\n\nunsubscribe();\n```\n\npass `{ signal }` when an `abortcontroller` owns subscription lifetime.\n\n## ssr state\n\nserialize resolved catalogs on server, then hydrate client store from same payload. `getsnapshot()` stays referentially stable until store revision changes, so use same hydrated store throughout initial client render.\n\n```ts\nimport { createtranslationstore, hydratetranslationstore } from '@vielzeug/lingua';\n\nconst servertranslationstore = createtranslationstore({\n catalogs: { en: { title: 'server title' } },\n locale: 'en',\n});\n\nconst state = servertranslationstore.serialize();\nconst clienttranslationstore = hydratetranslationstore(state, { fallback: 'en' });\n\nconsole.log(clienttranslationstore.translate('title'));\nservertranslationstore.dispose();\nclienttranslationstore.dispose();\n```\n\nstate contains raw loaded catalogs. it never contains loader functions.\n\n## formatting and validation\n\nimport formatting and catalog validation from dedicated subpaths to keep translation state focused.\n\n```ts\nimport { createformatter } from '@vielzeug/lingua/format';\nimport { validatecatalog } from '@vielzeug/lingua/validate';\n\nconst formatter = createformatter('en us');\nconst catalog = { inbox: { plural: { one: 'one message', other: '{count} messages' } } };\n\nconsole.log(formatter.currency(19.99, 'usd'));\nconsole.log(validatecatalog(catalog, 'en'));\n```\n\n## framework integration\n\npass stable `getsnapshot()` and `subscribe()` methods to framework state primitives. for ssr, create client store from same serialized state used by server before calling `usesyncexternalstore`.\n\n::: code group\n\n```ts [react]\nimport { usesyncexternalstore } from 'react';\n\nimport type { translationstore } from '@vielzeug/lingua';\n\nexport function usetranslator(i18n: translationstore) {\n const snapshot = usesyncexternalstore(i18n.subscribe, i18n.getsnapshot, i18n.getsnapshot);\n\n return snapshot.translator;\n}\n```\n\n```ts [vue 3]\nimport { onunmounted, shallowref } from 'vue';\n\nimport type { translationstore } from '@vielzeug/lingua';\n\nexport function usetranslator(i18n: translationstore) {\n const snapshot = shallowref(i18n.getsnapshot());\n const unsubscribe = i18n.subscribe((next) => {\n snapshot.value = next;\n });\n\n onunmounted(unsubscribe);\n return snapshot;\n}\n```\n\n```ts [svelte]\nimport { readable } from 'svelte/store';\n\nimport type { translationstore } from '@vielzeug/lingua';\n\nexport function translatorstore(i18n: translationstore) {\n return readable(i18n.getsnapshot().translator, (set) => i18n.subscribe(({ translator }) => set(translator)));\n}\n```\n\n:::\n\n## working with other vielzeug libraries\n\nbridge lingua subscriptions into ripple through flux when templates need reactive locale reads.\n\n```ts\nimport { stream } from '@vielzeug/flux';\nimport { tosignal } from '@vielzeug/flux/ripple';\nimport { computed } from '@vielzeug/ripple';\n\nconst localebinding = tosignal(\n stream<string>((observer) => {\n observer.next(i18n.locale);\n return i18n.subscribe(({ locale }) => observer.next(locale));\n }),\n { initial: i18n.locale },\n);\n\nexport const locale = computed(() => localebinding.value);\n```\n\nuse courier loaders when locale catalogs come from http rather than bundled modules; pass each loader to `catalogs`.\n\n## best practices\n\n define plural messages with `{ plural: ... }` and no sibling metadata.\n keep arrays and application metadata outside catalogs.\n treat source catalog objects as immutable after construction.\n use `translatedynamic()` only for runtime generated keys.\n load a lazy catalog before rendering it.\n give ui values keys before passing them to `segments()`.\n keep loader functions out of ssr payloads.\n dispose temporary stores after requests, tests, and route lifetimes.\n",
|
|
787
|
+
"api": " \ntitle: lingua — api reference\ndescription: complete api reference for @vielzeug/lingua.\n \n\n[[toc]]\n\n## api overview\n\n| symbol | purpose | execution mode | common gotcha |\n| | | | |\n| `createcatalogtranslator()` | compile one immutable locale catalog | sync | no fallback locales |\n| `createtranslator()` | compile immutable locale catalogs | sync | locale is fixed for translator lifetime |\n| `createtranslationstore()` | create mutable locale and catalog store | sync | load lazy locale explicitly |\n| `hydratetranslationstore()` | create store from serialized loaded catalogs | sync | serialized state never includes loaders |\n| `catalogkeys()` | enumerate message keys as dotted paths | sync | accepts store (current locale) or raw catalog; traverse subtrees for group scoped keys |\n| `createformatter()` | format intl values from `/format` | sync | import from subpath |\n| `validatecatalog()` | check explicit plural forms from `/validate` | sync | import from subpath |\n| `comparecatalogs()` | compare key parity across locales from `/validate` | sync | first locale is the base; import from subpath |\n| `linguaerror` | base class for lingua errors | sync | use `linguaerror.is()` for broad narrowing |\n\n## package entry point\n\n| import | purpose |\n| | |\n| `@vielzeug/lingua` | translation factories, state types, and lingua errors |\n| `@vielzeug/lingua/format` | `createformatter()` and formatter types |\n| `@vielzeug/lingua/validate` | `validatecatalog()`, `comparecatalogs()`, and `validationissue` |\n\n## translation factories\n\n### createcatalogtranslator\n\n```ts\nfunction createcatalogtranslator<c extends catalog>(\n catalog: c,\n options?: catalogtranslatoroptions,\n): translator<c>;\n```\n\ncompiles one catalog and returns an immutable fixed locale translator. locale defaults to `en` and controls plural selection and diagnostics.\n\n| parameter | type | description |\n| | | |\n| `catalog` | `c` | one catalog containing only messages and grouping objects |\n| `options` | `catalogtranslatoroptions` | locale and missing message handlers; fallback is unavailable |\n\n**returns:** `translator<c>`.\n\n**example:**\n\n```ts\nimport { createcatalogtranslator } from '@vielzeug/lingua';\n\nconst translator = createcatalogtranslator(\n { save: 'enregistrer' },\n { locale: 'fr' },\n);\n\ntranslator.translate('save');\n```\n\n \n\n### createtranslator\n\n```ts\nfunction createtranslator<c extends catalog>(catalogs: catalogs<c>, options?: translatoroptions): translator<c>;\n```\n\ncompiles locale catalogs and returns immutable translator.\n\n| parameter | type | description |\n| | | |\n| `catalogs` | `catalogs<c>` | locale keyed catalog objects |\n| `options` | `translatoroptions` | locale, fallback chain, and missing message handlers |\n\n**returns:** `translator<c>`.\n\n**example:**\n\n```ts\nimport { createtranslator } from '@vielzeug/lingua';\n\nconst translator = createtranslator(\n { en: { save: 'save' }, fr: { save: 'enregistrer' } },\n { locale: 'fr' },\n);\n\ntranslator.translate('save');\n```\n\n| method | signature | returns |\n| | | |\n| `translate` | `(textkey, options?)` or `(pluralkey, { count, ordinal?, values? })` | rendered string |\n| `translatedynamic` | `(key, options?)` | rendered string for runtime key |\n| `segments` | `(textkey, { values })` or `(pluralkey, { count, ordinal?, values? })` | string and typed value segments |\n| `segmentsdynamic` | `(key, options)` | segments for runtime key |\n| `locale` | `locale` | resolved active locale |\n\n \n\n### createtranslationstore\n\n```ts\nfunction createtranslationstore<c extends catalog>(options: translationstoreoptions<c>): translationstore<c>;\n```\n\ncreates catalog store, current locale state, and immutable translator snapshots.\n\n| parameter | type | description |\n| | | |\n| `options.catalogs` | `catalogsources<c>` | static catalogs or lazy locale loaders |\n| `options.locale` | `locale` | initial locale; defaults to `en` |\n| `options.fallback` | `locale \\| readonly locale[]` | fallback locale chain |\n| `options.onmissingkey` | `(key, locale) => string` | missing message handler |\n| `options.onmissingvalue` | `(name, key, locale) => string` | missing interpolation handler |\n\n**returns:** `translationstore<c>`, with every `translator<c>` method plus lifecycle methods.\n\n**example:**\n\n```ts\nimport { createtranslationstore } from '@vielzeug/lingua';\n\nconst translations = createtranslationstore({\n catalogs: { en: { title: 'home' }, fr: { title: 'accueil' } },\n locale: 'en',\n});\n\nawait translations.setlocale('fr');\ntranslations.translate('title');\n```\n\n| method or property | signature | returns |\n| | | |\n| `translate` | translator method | rendered string |\n| `segments` | translator method | string and typed value segments |\n| `load` | `({ locale? })` | `promise<void>` after catalog resolution |\n| `setlocale` | `(locale)` | `promise<void>` after locale commit; never loads implicitly |\n| `isloaded` | `({ locale? })` | `boolean` |\n| `getsnapshot` | `()` | `translationsnapshot<c>` |\n| `subscribe` | `(listener, { immediate?, signal? })` | unsubscribe function |\n| `serialize` | `()` | loader free `translationstate<c>` |\n| `dispose` | `()` | `void` |\n| `locale` | `locale` | current canonical locale |\n| `disposed` | `boolean` | disposal state |\n| `disposalsignal` | `abortsignal` | aborts on disposal |\n| `[symbol.dispose]` | `()` | delegates to `dispose()` |\n\n \n\n### hydratetranslationstore\n\n```ts\nfunction hydratetranslationstore<c extends catalog>(\n state: translationstate<c>,\n options?: omit<translationstoreoptions<c>, 'locale' | 'catalogs'>,\n): translationstore<c>;\n```\n\ncreates translation store from ssr state payload containing resolved raw catalogs.\n\n| parameter | type | description |\n| | | |\n| `state` | `translationstate<c>` | version `3`, active locale, and loader free catalogs |\n| `options` | `omit<translationstoreoptions<c>, 'locale' \\| 'catalogs'>` | fallback and missing message handlers |\n\n**returns:** `translationstore<c>`.\n\n**example:**\n\n```ts\nimport { createtranslationstore, hydratetranslationstore } from '@vielzeug/lingua';\n\nconst server = createtranslationstore({ catalogs: { en: { title: 'home' } }, locale: 'en' });\nconst client = hydratetranslationstore(server.serialize());\n\nclient.translate('title');\n```\n\n \n\n## catalog utilities\n\n### catalogkeys\n\n```ts\nfunction catalogkeys<c extends catalog>(source: translationstore<c> | c): readonlyarray<textkey<c>>;\n```\n\nenumerates every message key as a dotted path. traverses nested grouping objects and explicit `{ plural: ... }` messages, producing the same paths that `textkey<c>` represents at the type level. pass a `translationstore` to read from its current locale catalog; pass a raw catalog object to enumerate directly.\n\n| parameter | type | description |\n| | | |\n| `source` | `translationstore<c> \\| c` | store (uses current locale) or raw catalog object |\n\n**returns:** `readonlyarray<textkey<c>>` — dotted paths to every text and plural message.\n\n```ts\nimport { catalogkeys, createtranslationstore } from '@vielzeug/lingua';\n\nconst i18n = createtranslationstore({\n catalogs: { en: { nav: { home: 'home', settings: 'settings' } } },\n locale: 'en',\n});\n\nconst allkeys = catalogkeys(i18n); // ['nav.home', 'nav.settings']\nconst navkeys = catalogkeys(i18n.serialize().catalogs.en.nav); // ['home', 'settings']\n```\n\n \n\n## formatting and validation\n\n### createformatter\n\n```ts\nfunction createformatter(source: string | (() => string)): formatter;\n```\n\ncreates cached intl formatters using static locale or locale getter.\n\n| parameter | type | description |\n| | | |\n| `source` | `string \\| (() => string)` | static locale or locale getter |\n\n**returns:** `formatter`.\n\n**example:**\n\n```ts\nimport { createformatter } from '@vielzeug/lingua/format';\n\nconst formatter = createformatter('en us');\nformatter.currency(19.99, 'usd');\n```\n\n| method | signature | returns |\n| | | |\n| `number` | `(value, options?)` | `string` |\n| `currency` | `(value, currency, options?)` | `string` |\n| `date` | `(value, options?)` | `string` |\n| `relative` | `(value, unit, options?)` | `string` |\n| `list` | `(value, options?)` | `string` |\n| `duration` | `(value, options?)` | `string` |\n\n### validatecatalog\n\n```ts\nfunction validatecatalog(catalog: catalog, locale: locale): validationissue[];\n```\n\nvalidates explicit plural messages against locale plural categories after catalog structural validation.\n\n| parameter | type | description |\n| | | |\n| `catalog` | `catalog` | explicit catalog to validate |\n| `locale` | `locale` | bcp 47 locale tag |\n\n**returns:** `validationissue[]`.\n\n**example:**\n\n```ts\nimport { validatecatalog } from '@vielzeug/lingua/validate';\n\nvalidatecatalog({ inbox: { plural: { one: 'one message' } } }, 'en');\n```\n\n### comparecatalogs\n\n```ts\nfunction comparecatalogs<c extends catalog>(catalogs: catalogs<c>): catalogcomparison;\n```\n\ncompares key sets across locales. first locale is the base — reports keys missing in each target and keys present in targets but absent from base. validates each catalog structurally.\n\n| parameter | type | description |\n| | | |\n| `catalogs` | `catalogs<c>` | locale keyed catalogs to compare |\n\n**returns:** `catalogcomparison` with `missing` and `extra` arrays.\n\n```ts\nimport { comparecatalogs } from '@vielzeug/lingua/validate';\n\nconst result = comparecatalogs({\n en: { greeting: 'hello', farewell: 'goodbye' },\n de: { greeting: 'hallo' },\n});\n// { missing: [{ key: 'farewell', locale: 'de' }], extra: [] }\n```\n\n## types\n\n```ts\ntype locale = string;\ntype pluralcategory = intl.ldmlpluralrule;\ntype pluralmessage = { readonly plural: partial<record<pluralcategory, string>> };\ntype catalognode = catalog | pluralmessage | string;\ntype catalog = { readonly [key: string]: catalognode };\ntype catalogs<c extends catalog = catalog> = record<locale, c>;\ntype catalogtranslatoroptions = omit<translatoroptions, 'fallback'>;\ntype catalogloader<c extends catalog = catalog> = () => promise<c>;\ntype catalogsource<c extends catalog = catalog> = c | catalogloader<c>;\ntype catalogsources<c extends catalog = catalog> = record<locale, catalogsource<c>>;\n\ntype translationstoreoptions<c extends catalog = catalog> = translatoroptions & {\n catalogs: catalogsources<c>;\n};\n\ntype translationstate<c extends catalog = catalog> = {\n readonly catalogs: catalogs<c>;\n readonly locale: locale;\n readonly version: 3;\n};\n\ntype translationsnapshot<c extends catalog = catalog> = {\n readonly locale: locale;\n readonly revision: number;\n readonly translator: translator<c>;\n};\n\ntype translationstore<c extends catalog = catalog> = translator<c> & {\n readonly disposalsignal: abortsignal;\n dispose(): void;\n readonly disposed: boolean;\n getsnapshot(): translationsnapshot<c>;\n isloaded(options?: { locale?: locale }): boolean;\n load(options?: { locale?: locale }): promise<void>;\n serialize(): translationstate<c>;\n setlocale(locale: locale): promise<void>;\n subscribe(listener: (snapshot: translationsnapshot<c>) => void, options?: subscribeoptions): () => void;\n [symbol.dispose](): void;\n};\n\ntype translator<c extends catalog = catalog> = {\n readonly locale: locale;\n segments<v>(key: textkey<c>, options: translateoptions & { values: record<string, v> }): array<string | v>;\n segments<v>(key: pluralkey<c>, options: pluraloptions & { values?: record<string, v> }): array<string | number | v>;\n segmentsdynamic<v>(\n key: string,\n options: (translateoptions | pluraloptions) & { values?: record<string, v> },\n ): array<string | number | v>;\n translate(key: textkey<c>, options?: translateoptions): string;\n translate(key: pluralkey<c>, options: pluraloptions): string;\n translatedynamic(key: string, options?: translateoptions | pluraloptions): string;\n};\n```\n\n```ts\ntype values = record<string, unknown>;\ntype translateoptions = { values?: values };\ntype pluraloptions = translateoptions & { count: number; ordinal?: boolean };\ntype translatoroptions = {\n fallback?: locale | readonly locale[];\n locale?: locale;\n onmissingkey?: (key: string, locale: locale) => string;\n onmissingvalue?: (name: string, key: string, locale: locale) => string;\n};\ntype subscribeoptions = { immediate?: boolean; signal?: abortsignal };\n\ntype messagekey<\n c,\n prefix extends string = '',\n depth extends readonly unknown[] = readonly [1, 1, 1, 1, 1, 1],\n> = depth extends readonly [unknown, ...infer rest]\n ? c extends string | pluralmessage\n ? prefix\n : c extends catalog\n ? {\n [k in string & keyof c]: messagekey<c[k], prefix extends '' ? k : `${prefix}.${k}`, rest>;\n }[string & keyof c]\n : never\n : never;\n\ntype textkey<\n c,\n prefix extends string = '',\n depth extends readonly unknown[] = readonly [1, 1, 1, 1, 1, 1],\n> = depth extends readonly [unknown, ...infer rest]\n ? c extends string\n ? prefix\n : c extends catalog\n ? {\n [k in string & keyof c]: textkey<c[k], prefix extends '' ? k : `${prefix}.${k}`, rest>;\n }[string & keyof c]\n : never\n : never;\n\ntype pluralkey<\n c,\n prefix extends string = '',\n depth extends readonly unknown[] = readonly [1, 1, 1, 1, 1, 1],\n> = depth extends readonly [unknown, ...infer rest]\n ? c extends pluralmessage\n ? prefix\n : c extends catalog\n ? {\n [k in string & keyof c]: pluralkey<c[k], prefix extends '' ? k : `${prefix}.${k}`, rest>;\n }[string & keyof c]\n : never\n : never;\n\ntype durationvalue = partial<record<\n 'days' | 'hours' | 'microseconds' | 'milliseconds' | 'minutes' | 'months' | 'nanoseconds' | 'seconds' | 'weeks' | 'years',\n number\n>>;\n\ntype durationformatoptions = {\n hours?: '2 digit' | 'numeric';\n microseconds?: 'numeric';\n milliseconds?: 'numeric';\n minutes?: '2 digit' | 'numeric';\n nanoseconds?: 'numeric';\n seconds?: '2 digit' | 'numeric';\n style?: 'digital' | 'long' | 'narrow' | 'short';\n};\n\ntype listformatoptions = { style?: 'long' | 'narrow' | 'short'; type?: 'and' | 'or' };\n\ntype formatter = {\n currency(value: number, currency: string, options?: omit<intl.numberformatoptions, 'currency' | 'style'>): string;\n date(value: date | number, options?: intl.datetimeformatoptions): string;\n duration(value: durationvalue, options?: durationformatoptions): string;\n list(value: array<string | number>, options?: listformatoptions): string;\n number(value: number, options?: intl.numberformatoptions): string;\n relative(value: number, unit: intl.relativetimeformatunit, options?: intl.relativetimeformatoptions): string;\n};\n\ntype validationissue = { key: string; locale: locale; missing: intl.ldmlpluralrule };\ntype catalogcomparison = {\n readonly missing: readonlyarray<{ key: string; locale: locale }>;\n readonly extra: readonlyarray<{ key: string; locale: locale }>;\n};\n```\n\n## errors\n\n| error | trigger |\n| | |\n| `linguadisposederror` | state mutation or subscription after `dispose()` |\n| `linguainvalidcatalogerror` | invalid catalog node or reserved key |\n| `linguainvalidlocaleerror` | invalid bcp 47 locale tag |\n| `linguainvalidpluralcounterror` | non finite plural count |\n| `linguainvalidstateerror` | unsupported serialized state version |\n| `linguamissingcatalogerror` | catalog has no source for requested locale |\n",
|
|
788
|
+
"usage": " \ntitle: lingua — usage guide\ndescription: translate explicit catalogs, load lazy locales, and connect locale snapshots to ui state.\n \n\n[[toc]]\n\n## basic usage\n\ncreate i18n store from locale keyed catalogs. strings are text messages; plural messages use `{ plural: ... }`.\n\n```ts\nimport { createtranslationstore } from '@vielzeug/lingua';\n\nconst i18n = createtranslationstore({\n catalogs: {\n en: {\n greeting: 'hello, {name}!',\n inbox: { plural: { one: 'one message', other: '{count} messages' } },\n },\n },\n locale: 'en',\n});\n\nconsole.log(i18n.translate('greeting', { values: { name: 'ada' } }));\nconsole.log(i18n.translate('inbox', { count: 3 }));\n```\n\ncall `dispose()` when store belongs to temporary request, test, or route owner.\n\n## define explicit catalogs\n\nuse nested objects only to group keys. a plural message always has `plural`, so regular objects containing `one` or `other` remain groups.\n\n```ts\nconst catalog = {\n account: {\n greeting: 'hello, {name}!',\n unread: { plural: { one: 'one unread message', other: '{count} unread messages' } },\n },\n};\n```\n\nuse `{ values }` for text replacements. pass `count` at top level for plural selection; lingua injects it into selected template. absent replacements render as `{name}` by default. `segments()` preserves an own `undefined` or `null` value; omit property to receive `{name}`.\n\ncatalogs contain strings, grouping objects, and explicit `{ plural: ... }` messages only. keep application data outside catalog, then translate display labels while constructing it.\n\n```ts\nimport { createcatalogtranslator } from '@vielzeug/lingua';\n\nconst messages = {\n status: { blocked: 'blocked', done: 'done', inprogress: 'in progress' },\n};\nconst statusdefinitions = [\n { labelkey: 'status.inprogress', value: 'in progress' },\n { labelkey: 'status.blocked', value: 'blocked' },\n { labelkey: 'status.done', value: 'done' },\n] as const;\nconst translator = createcatalogtranslator(messages);\nconst statusoptions = statusdefinitions.map(({ labelkey, value }) => ({ label: translator.translate(labelkey), value }));\n```\n\n## enumerate catalog keys\n\nuse `catalogkeys()` to derive key arrays from the catalog itself instead of maintaining a parallel list that can go stale. it traverses nested grouping objects and explicit `{ plural: ... }` messages, returning the same dotted paths that `textkey<c>` represents at the type level.\n\npass a `translationstore` to enumerate keys from its current locale catalog without specifying a locale explicitly.\n\n```ts\nimport { catalogkeys, createtranslationstore } from '@vielzeug/lingua';\n\nconst i18n = createtranslationstore({\n catalogs: {\n en: {\n greeting: 'hello, {name}!',\n inbox: { plural: { one: 'one message', other: '{count} messages' } },\n nav: { home: 'home', settings: 'settings' },\n },\n },\n locale: 'en',\n});\n\nconst allkeys = catalogkeys(i18n);\n// ['greeting', 'inbox', 'nav.home', 'nav.settings']\n```\n\npass a raw catalog object to enumerate keys directly. call `catalogkeys()` on a nested subtree to get exactly the keys in that group — no filtering, no casts.\n\n```ts\nimport { catalogkeys } from '@vielzeug/lingua';\n\nconst messages = {\n nav: { home: 'home', settings: 'settings' },\n} as const;\n\nconst allkeys = catalogkeys(messages);\n// ['nav.home', 'nav.settings']\n\nconst navkeys = catalogkeys(messages.nav);\n// ['home', 'settings']\n```\n\nuse this for random message selection, cycling, or validation without a stale parallel array.\n\n## render framework content\n\nuse `segments()` when replacements are framework nodes, links, or other values that must not be stringified.\n\n```ts\nimport { createcatalogtranslator } from '@vielzeug/lingua';\n\nconst translator = createcatalogtranslator({ error: 'try {retry} or {support}.' });\n\nconst retry = { href: '/retry', label: 'retry' };\nconst support = { href: '/support', label: 'support' };\n\nconsole.log(translator.segments('error', { values: { retry, support } }));\n```\n\nrender returned array with framework fragment or list primitive. give ui values consumer owned keys before passing them to `segments()`; lingua preserves value identity and never clones or mutates them.\n\n## use static catalogs\n\nuse `createcatalogtranslator()` when one catalog and locale stay fixed for translator lifetime. it defaults locale to `en`; pass `locale` when plural rules or diagnostics need another locale. lingua snapshots catalog messages during construction. do not mutate source catalog objects afterward.\n\n```ts\nimport { createcatalogtranslator } from '@vielzeug/lingua';\n\nconst translator = createcatalogtranslator(\n { save: 'enregistrer' },\n { locale: 'fr' },\n);\n\nconsole.log(translator.translate('save'));\n```\n\nuse `createtranslator()` when fixed translation requires locale keyed catalogs and fallback resolution.\n\n```ts\nimport { createtranslator } from '@vielzeug/lingua';\n\nconst translator = createtranslator(\n { en: { save: 'save' }, fr: { save: 'enregistrer' } },\n { locale: 'fr' },\n);\n\nconsole.log(translator.translate('save'));\n```\n\n## load catalogs and switch locales\n\ndeclare one static catalog or lazy loader per locale. switch locale, then load it explicitly when source is lazy.\n\n```ts\nimport { createtranslationstore } from '@vielzeug/lingua';\n\nconst i18n = createtranslationstore({\n catalogs: {\n en: { navigation: { settings: 'settings' } },\n fr: async () => ({ navigation: { settings: 'réglages' } }),\n },\n locale: 'en',\n});\n\nawait i18n.setlocale('fr');\nawait i18n.load();\nconsole.log(i18n.translate('navigation.settings'));\n```\n\nconcurrent loads for same locale share work. `setlocale()` never triggers hidden loads.\n\n## subscribe to immutable snapshots\n\nsubscribe when ui state must change with locale or loaded active/fallback catalog. every callback receives snapshot containing translator for that revision.\n\n```ts\nconst unsubscribe = i18n.subscribe(\n ({ locale, translator }) => {\n console.log(locale, translator.translate('navigation.settings'));\n },\n { immediate: true },\n);\n\nunsubscribe();\n```\n\npass `{ signal }` when an `abortcontroller` owns subscription lifetime.\n\n## ssr state\n\nserialize resolved catalogs on server, then hydrate client store from same payload. `getsnapshot()` stays referentially stable until store revision changes, so use same hydrated store throughout initial client render.\n\n```ts\nimport { createtranslationstore, hydratetranslationstore } from '@vielzeug/lingua';\n\nconst servertranslationstore = createtranslationstore({\n catalogs: { en: { title: 'server title' } },\n locale: 'en',\n});\n\nconst state = servertranslationstore.serialize();\nconst clienttranslationstore = hydratetranslationstore(state, { fallback: 'en' });\n\nconsole.log(clienttranslationstore.translate('title'));\nservertranslationstore.dispose();\nclienttranslationstore.dispose();\n```\n\nstate contains raw loaded catalogs. it never contains loader functions.\n\n## formatting and validation\n\nimport formatting and catalog validation from dedicated subpaths to keep translation state focused.\n\n```ts\nimport { createformatter } from '@vielzeug/lingua/format';\nimport { comparecatalogs, validatecatalog } from '@vielzeug/lingua/validate';\n\nconst formatter = createformatter('en us');\nconst catalog = { inbox: { plural: { one: 'one message', other: '{count} messages' } } };\n\nconsole.log(formatter.currency(19.99, 'usd'));\nconsole.log(validatecatalog(catalog, 'en'));\n```\n\nuse `comparecatalogs()` to catch missing or extra keys across locales — the most common i18n defect. first locale is the base.\n\n```ts\nimport { comparecatalogs } from '@vielzeug/lingua/validate';\n\nconst result = comparecatalogs({\n en: { greeting: 'hello', farewell: 'goodbye' },\n de: { greeting: 'hallo' },\n});\n// { missing: [{ key: 'farewell', locale: 'de' }], extra: [] }\n```\n\n## framework integration\n\npass stable `getsnapshot()` and `subscribe()` methods to framework state primitives. for ssr, create client store from same serialized state used by server before calling `usesyncexternalstore`.\n\n::: code group\n\n```ts [react]\nimport { usesyncexternalstore } from 'react';\n\nimport type { translationstore } from '@vielzeug/lingua';\n\nexport function usetranslator(i18n: translationstore) {\n const snapshot = usesyncexternalstore(i18n.subscribe, i18n.getsnapshot, i18n.getsnapshot);\n\n return snapshot.translator;\n}\n```\n\n```ts [vue 3]\nimport { onunmounted, shallowref } from 'vue';\n\nimport type { translationstore } from '@vielzeug/lingua';\n\nexport function usetranslator(i18n: translationstore) {\n const snapshot = shallowref(i18n.getsnapshot());\n const unsubscribe = i18n.subscribe((next) => {\n snapshot.value = next;\n });\n\n onunmounted(unsubscribe);\n return snapshot;\n}\n```\n\n```ts [svelte]\nimport { readable } from 'svelte/store';\n\nimport type { translationstore } from '@vielzeug/lingua';\n\nexport function translatorstore(i18n: translationstore) {\n return readable(i18n.getsnapshot().translator, (set) => i18n.subscribe(({ translator }) => set(translator)));\n}\n```\n\n:::\n\n## working with other vielzeug libraries\n\nbridge lingua subscriptions into ripple through flux when templates need reactive locale reads.\n\n```ts\nimport { stream } from '@vielzeug/flux';\nimport { tosignal } from '@vielzeug/flux/ripple';\nimport { computed } from '@vielzeug/ripple';\n\nconst localebinding = tosignal(\n stream<string>((observer) => {\n observer.next(i18n.locale);\n return i18n.subscribe(({ locale }) => observer.next(locale));\n }),\n { initial: i18n.locale },\n);\n\nexport const locale = computed(() => localebinding.value);\n```\n\nuse courier loaders when locale catalogs come from http rather than bundled modules; pass each loader to `catalogs`.\n\n## best practices\n\n define plural messages with `{ plural: ... }` and no sibling metadata.\n keep arrays and application metadata outside catalogs.\n treat source catalog objects as immutable after construction.\n use `translatedynamic()` only for runtime generated keys.\n load a lazy catalog before rendering it.\n give ui values keys before passing them to `segments()`.\n keep loader functions out of ssr payloads.\n dispose temporary stores after requests, tests, and route lifetimes.\n",
|
|
687
789
|
"examples": " \ntitle: lingua — examples\ndescription: focused examples for explicit catalogs and locale resources.\n \n\n [static translator](./examples/static translator.md)\n [lazy locale catalog](./examples/feature resources.md)\n [ssr hydration](./examples/ssr hydration.md)\n"
|
|
688
790
|
},
|
|
689
791
|
"examples": [
|
|
@@ -709,7 +811,7 @@
|
|
|
709
811
|
"name": "@vielzeug/lingua",
|
|
710
812
|
"related": "ripple wayfinder courier",
|
|
711
813
|
"slug": "lingua",
|
|
712
|
-
"source": "export {\n linguadisposederror,\n linguaerror,\n linguainvalidcatalogerror,\n linguainvalidlocaleerror,\n linguainvalidpluralcounterror,\n linguainvalidstateerror,\n linguamissingcatalogerror,\n} from './errors';\nexport {\n createtranslationstore,\n hydratetranslationstore,\n type translationsnapshot,\n type translationstore,\n} from './i18n';\nexport { createcatalogtranslator, createtranslator, type translator } from './translator';\nexport type {\n catalog,\n catalogloader,\n catalognode,\n catalogsource,\n catalogsources,\n catalogs,\n catalogtranslatoroptions,\n locale,\n messagekey,\n pluralcategory,\n pluralkey,\n pluralmessage,\n pluraloptions,\n subscribeoptions,\n textkey,\n translateoptions,\n translationstate,\n translationstoreoptions,\n translatoroptions,\n values,\n} from './types';\n"
|
|
814
|
+
"source": "export { catalogkeys } from './catalog';\nexport {\n linguadisposederror,\n linguaerror,\n linguainvalidcatalogerror,\n linguainvalidlocaleerror,\n linguainvalidpluralcounterror,\n linguainvalidstateerror,\n linguamissingcatalogerror,\n} from './errors';\nexport {\n createtranslationstore,\n hydratetranslationstore,\n type translationsnapshot,\n type translationstore,\n} from './i18n';\nexport { createcatalogtranslator, createtranslator, type translator } from './translator';\nexport type {\n catalog,\n catalogloader,\n catalognode,\n catalogsource,\n catalogsources,\n catalogs,\n catalogtranslatoroptions,\n locale,\n messagekey,\n pluralcategory,\n pluralkey,\n pluralmessage,\n pluraloptions,\n subscribeoptions,\n textkey,\n translateoptions,\n translationstate,\n translationstoreoptions,\n translatoroptions,\n values,\n} from './types';\n"
|
|
713
815
|
},
|
|
714
816
|
{
|
|
715
817
|
"category": "ui",
|
|
@@ -789,20 +891,20 @@
|
|
|
789
891
|
},
|
|
790
892
|
{
|
|
791
893
|
"category": "ui primitives",
|
|
792
|
-
"description": "functional custom element authoring with typed props, reactive templates, lifecycle helpers,
|
|
894
|
+
"description": "functional custom element authoring with typed props, reactive templates, lifecycle helpers, and testing utilities.",
|
|
793
895
|
"docs": {
|
|
794
|
-
"index": " \ntitle: ore — web component authoring with signals\ndescription: functional custom element authoring with typed props, reactive templates, lifecycle helpers,
|
|
795
|
-
"api": " \ntitle: ore — api reference\ndescription: complete api reference for @vielzeug/ore and @vielzeug/ore/testing.\n \n\n[[toc]]\n\n## api overview\n\nall browser runtime symbols below are imported from `@vielzeug/ore`. lifecycle/context/binding functions (`onmounted`, `oncleanup`, `onevent`, `onelement`, `watcheffect`, `bind`, `provide`, `useemit`, `useslots`, `gethost`) resolve the active component through an implicit \"current component\" context — they work when called synchronously during `setup()`, or from any composable function `setup()` calls (transitively), but throw if called outside that window.\n\n> `watcheffect` is not named `watch` — `@vielzeug/ripple` already exports a `watch(source, callback)` with different semantics (explicit source + old/new value pair), and the two are frequently imported in the same file.\n\n| symbol | purpose | execution mode | common gotcha |\n| | | | |\n| `define()` | register a custom element with reactive setup | sync | tag must contain a hyphen; call before first use |\n| `html` | tagged template literal returning htmlresult | sync | expressions must be signals, functions, or primitives |\n| `prop.*` | typed prop helpers (string, bool, number, …) | sync | prop values are signals — read `.value` |\n| `provide()`/`inject()` | context api for parent to descendant sharing | setup only | must be called synchronously during `setup()` |\n| `ref()` | reactive reference to a dom element | sync | value is null until after first mount |\n| `createcontext()` | create a typed injection key | sync | context is scoped to the component tree |\n| `each()` | keyed list rendering with dom diffing | sync | duplicate keys report `ore:error`; plain `t[]` is a one time static render |\n| `when()` | conditional branch rendering | sync | getter fn computed disposed on cleanup; static bool skips subscription |\n| `live(signal)` | one way binding that skips stale writes during input | sync | use for controlled inputs alongside a manual `@input` handler |\n| `onmounted(fn)` | dom ready callback | setup only | must be called synchronously during `setup()` |\n| `oncleanup(fn)` | register teardown | setup only | called on component disconnect |\n| `onevent(target, …)` | scoped event listener with auto cleanup | setup only | no ops on null target; removed on disconnect |\n| `usefield(options)` | wire signal to form `elementinternals` | setup only | requires `formassociated: true` on the component definition |\n| `onformreset(fn)` | run work when the ancestor `<form>` resets | setup only | fires every reset (not one shot); only for `formassociated: true` components |\n| `useemit<emits>()` | typed `emit()` bound to the current host | setup only | call once per component; returns `dispatchevent`'s boolean (`false` if a listener called `preventdefault()`) |\n| `useslots<slotnames>()`| reactive slot presence/element signals | setup only | safe to call more than once — the underlying registry is created once |\n| `gethost()` | the current component's host element | setup only | prefer a higher level helper (`bind`, …) when one exists |\n\n## package entry points\n\n| import | purpose |\n| | |\n| `@vielzeug/ore` | all browser runtime apis, including directives, fields, and observers |\n| `@vielzeug/ore/testing` | ore specific mounting, lifecycle, hook, cleanup, and form test support |\n| `@vielzeug/assay` | generic dom events, scoped queries, and async waiting |\n\n## core component api\n\n### `define(tag, definition)`\n\n```ts\ndefine<props>(tag: string, definition: componentdefinition<props>): void;\n```\n\nthe `setup()` function receives only typed prop signals:\n\n```ts\nsetup(props) {\n return html`<div>${props.label}</div>`;\n}\n```\n\neverything else — lifecycle hooks, host bindings, context, slots, emit — is a plain function imported from `@vielzeug/ore`, called directly from `setup()` (or a composable it calls):\n\n```ts\nimport { define, html, onmounted, useemit, useslots } from '@vielzeug/ore';\n\ndefine('my card', {\n setup(_props) {\n const emit = useemit<{ close: undefined }>();\n const slots = useslots<'header' | 'footer'>();\n\n onmounted(() => console.log('mounted'));\n\n // emit() returns dispatchevent's boolean — false if a listener called preventdefault()\n const notcancelled = emit('close');\n\n return html`${when(slots.has('header'), () => html`<slot name=\"header\"></slot>`)}`;\n },\n});\n```\n\n`useemit<emits>()` and `useslots<slotnames>()` are factory hooks — call them once per setup run to get a typed\n`emit`/`slots` bound to the current host. `useslots()` is safe to call more than once within that setup run.\n\n### componentdefinition\n\n```ts\ntype componentdefinition<props> = {\n formassociated?: boolean;\n props?: propsdef<props>;\n setup: (props: inferprops<propsdef<props>>) => htmlresult | null;\n shadow?: partial<shadowrootinit> | false; // false = light dom (no shadow root)\n styles?: (string | cssstylesheet | cssresult)[];\n};\n```\n\n## runtime helpers\n\n`onmounted`, `oncleanup`, `onevent`, `onelement`, and `watcheffect` are plain functions imported from `@vielzeug/ore`. call them directly during `setup()`.\n\n```ts\nimport { html, oncleanup, onevent, onmounted } from '@vielzeug/ore';\n\nsetup(props) {\n onmounted(() => {\n // dom is ready; return a function for mount scoped cleanup\n return () => { /* cleanup on unmount */ };\n });\n\n oncleanup(() => { /* called on disconnect */ });\n\n onevent(window, 'keydown', (e) => { /* auto removed on disconnect */ });\n\n return html`...`;\n}\n```\n\nbecause these resolve the active component through an implicit context (rather than a value threaded through parameters), composable helper functions can call them directly too — no need to pass hooks in as options:\n\n```ts\nimport { oncleanup } from '@vielzeug/ore';\n\nfunction usemyhelper() {\n oncleanup(() => { /* teardown */ });\n}\n\n// in setup:\nsetup(_props) {\n usemyhelper();\n return html`...`;\n}\n```\n\n## props api\n\n| helper | signature | notes |\n| | | |\n| `prop.string(defaultvalue?)` | `propdef<string>` | reflects by default |\n| `prop.bool(defaultvalue?)` | `propdef<boolean>` | any non null attribute value other than `\"false\"` parses as `true`; `\"false\"` or absent attribute is `false` |\n| `prop.number(defaultvalue?)` | `propdef<number>` | returns default (not nan) and warns in dev when attribute is not a valid number |\n| `prop.oneof(allowed, defaultvalue)` | `propdef<t>` | restricts to provided string union |\n| `prop.json(defaultvalue)` | `propdef<t>` | json.parse from attribute; `reflect: false` |\n| `prop.data<t>(defaultvalue?)` | `propdef<t>` | js only — never reads/writes an attribute; use for objects, arrays, callbacks, or any non serialisable value |\n\n> **choosing the right prop helper:**\n>\n> **`prop.json`** — value can be declared in html (`<my el config='{\"x\":1}'>`); attribute string is `json.parse`d.\n> **`prop.data`** — value is always set from javascript (objects, arrays, callbacks, class instances); the attribute is never read. use this for both data and function props.\n\nwhen you need custom parsing or `reflect: false`, use a raw `propdef` object:\n\n```ts\nprops: {\n items: { default: [], parse: () => [], reflect: false },\n}\n```\n\nuse `prop.data` for props that hold js only values (including callbacks) that cannot be serialised through an html attribute:\n\n```ts\ndefine('data grid', {\n props: {\n getrowkey: prop.data<(row: unknown) => string>(),\n columns: prop.data<datagridcolumn[]>([]),\n onsort: prop.data<(key: string) => void>(),\n },\n setup(props) {\n // set from js: grid.getrowkey = (row) => row.id\n return html`...`;\n },\n});\n```\n\n## template and directives\n\n### `html`\n\ntagged template literal that returns an `htmlresult`. supports text interpolation, ordinary attributes (`attr=`),\nboolean attributes (`?attr=`), events (`@event=`), refs (`ref=`), and nested templates.\n\n### `css`\n\ntagged template literal that returns a `cssresult` for use in `styles`.\n\n### directives\n\n| directive | purpose |\n| | |\n| `each(source, key, render, fallback?)` | keyed reactive list; render receives `readable<t>` and `readable<number>`; plain `t[]` is a one time static snapshot |\n| `when(condition, truthy, falsy?)` | conditional rendering |\n| `classmap(record)` | reactive class string from object map |\n| `stylemap(record)` | reactive inline style string from object map |\n| `live(signal)` | one way binding that skips stale writes during active user input; use with `@input` handler |\n| `unsafehtml(value)` | html rendering sink; sanitize untrusted values before calling |\n\n### `unsafehtml`\n\n`unsafehtml()` is an explicit html injection sink. it has no global sanitizer: sanitize untrusted\ncontent before passing it to the directive, so the trust boundary remains at the call site.\n\n```ts\nimport { unsafehtml } from '@vielzeug/ore';\n\nconst safearticle = sanitize(usersuppliedarticle);\n\nreturn html`<article>${unsafehtml(safearticle)}</article>`;\n```\n\n## host bindings\n\n`bind(config, options?)` is a plain function imported from `@vielzeug/ore`:\n\n```ts\nbind({\n attr: { role: 'button', 'aria expanded': () => string(open.value) },\n class: { 'is open': open },\n style: { ' height': () => height.value + 'px' },\n on: { click: handleclick },\n});\n```\n\n`bind()` auto registers cleanup with the component scope — no manual `oncleanup` needed. returns a cleanup function for early teardown.\n\n### off host bindings\n\npass `{ target: el }` as a second argument to bind to any element other than the host:\n\n```ts\nbind(\n { attr: { 'aria expanded': () => string(isopen.value) } },\n { target: triggerel },\n);\n```\n\nevent listener options (`once`, `capture`, `passive`) are also accepted in the second argument. cleanup is auto registered with the component scope when called during setup.\n\n### reactive aria attributes\n\nfor reactive aria attribute syncing, use `bind({ aria: config }, { target })`. shorthand keys are normalised to `aria *` automatically (`expanded` → `aria expanded`; `role` is passed verbatim):\n\n```ts\n// inside setup — cleanup auto registered\nbind(\n {\n aria: {\n expanded: () => isopen.value,\n controls: panelid,\n haspopup: 'listbox',\n },\n },\n { target: triggerel },\n);\n\n// manage cleanup manually — bind() always returns a cleanup fn\nconst stoparia = bind({ aria: { expanded: () => isopen.value } }, { target: triggerel });\n// call stoparia() when the trigger is swapped out\n```\n\nstatic values (strings, numbers, booleans) are applied once. getter functions and signals create reactive effects. setting a value to `null`, `undefined`, or `false` removes the attribute.\n\n## slots\n\n `slots.has(name?)` — `readable<boolean>` — whether the named (or default) slot has assigned content\n `slots.elements(name?)` — `readable<element[]>` — the assigned elements for the slot\n\nslot signals update reactively when assigned content changes, including when slots are inserted dynamically (via `when()` or `each()`) after mount.\n\n## context api\n\n `createcontext<t>(description?)` — create a typed injection key\n `provide(key, value)` — provide a value to descendants\n `inject(key)` — resolve from nearest ancestor; returns `undefined` if not found\n `inject(key, fallback)` — resolve with a fallback value\n `injectstrict(key)` — resolve or throw if absent\n\n`provide()` and `inject()` must be called synchronously during `setup()`. calling them outside a setup context throws\n`'lifecycle hooks must be called during component setup'`. context resolution walks the ancestor chain including shadow\ndom boundaries. `inject()` resolves and caches its result once per consumer — provide a `readable` (signal/computed)\nrather than a raw value if descendants need to observe later changes; re calling `provide()` with a new raw value\nafterward is not seen by consumers that already resolved it (a dev mode warning fires when a key is provided twice on\nthe same element).\n\n## utilities\n\n `ref<t>()` — create a `signal<t | null>` element reference. set to the element via `ref=` in templates.\n `createid(prefix = 'id')` — generate a unique incremental string id (e.g. `'id 1'`, `'id 2'`). each call returns a new id — it does not deduplicate by prefix.\n `createstableid(prefix = 'id')` — generate a unique id that also embeds a short random tag shared across all ids generated in the session (e.g. `'field a3k21'`), reducing collision risk when multiple app instances run on the same page. like `createid()`, every call returns a new id.\n `resetstableidcounter()` — reset the `createstableid()` counter to 0. call in test `beforeeach` for deterministic ids. scoped to `createstableid()` only — `createid()` has no public reset (it's for uniqueness, not cross test determinism).\n\n## form associated api\n\nimport from `@vielzeug/ore`.\n\n### `usefield(options)`\n\nwire a form associated element to `elementinternals`. requires `formassociated: true` on the component definition. the `disabled` state tracking via `internals.states` (customstateset) is skipped with a dev warning if the api is unavailable in the current environment.\n\n```ts\ntype formfieldoptions<t> = {\n disabled?: readable<boolean>;\n /** defaults to the host element active during setup. */\n el?: htmlelement;\n /**\n * when true, a null/undefined value is submitted as '' instead of null,\n * keeping the field's key present in formdata even when the value is absent.\n * only applies to the default toformvalue; ignored if toformvalue is provided.\n * @default false\n */\n emptystringfornull?: boolean;\n /** called when the ancestor <form> resets (see onformreset) — restore local field state here. */\n onreset?: () => void;\n toformvalue?: (value: t) => file | formdata | string | null;\n /** recomputed reactively and passed straight to internals.setvalidity(). null = always valid. */\n validationmessage?: readable<string>;\n validity?: readable<validitystateflags | null>;\n value: signal<t> | readable<t>;\n};\n\ntype formfieldhandle = {\n checkvalidity(): boolean;\n readonly internals: elementinternals;\n reportvalidity(): boolean;\n /** set (non empty message) or clear (empty string) a custom validity error. */\n setcustomvalidity(message: string): void;\n};\n```\n\npass `validity`/`validationmessage` to make `required` style constraints participate in native constraint validation\nthrough `checkvalidity()` and `reportvalidity()`:\n\n```ts\nconst isblank = (v: string) => v.trim() === '';\n\nusefield({\n validationmessage: computed(() => (required.value && isblank(value.value) ? 'this field is required.' : '')),\n validity: computed(() => (required.value && isblank(value.value) ? { valuemissing: true } : null)),\n value,\n});\n```\n\n## observer apis\n\nimport from `@vielzeug/ore`.\n\n `resizeobserver(element)` — returns `readable<{ height: number; width: number }>`, initialised to `{ height: 0, width: 0 }`\n `intersectionobserver(element, options?)` — returns `readable<intersectionobserverentry | null>`, initialised to `null`\n `mutationobserver(element, options?)` — returns `readable<{ entries: mutationrecord[]; latest: mutationrecord | null }>`, initialised to `{ entries: [], latest: null }`\n `mediaobserver(query)` — returns `readable<boolean>`, initialised to the query's current `matches` state\n\n## testing apis\n\nimport from `@vielzeug/ore/testing`.\n\n| api | purpose |\n| | |\n| `mount(setup, options?)` | mount a component and return a test fixture |\n| `cleanup()` | remove all mounted elements and reset test state |\n| `install(aftereach, options?)` | register auto cleanup; pass `{ forminternals: true }` to also install the `elementinternals`/`formdata`/`<form>.reset()` jsdom polyfill (see below) |\n| `installforminternalspolyfill()` | installs the form internals polyfill directly (returns an `uninstall()` that restores every patched global). usually called via `install(aftereach, { forminternals: true })` |\n| `walkflattree(root, visit)` | walks the flat tree (expanding `<slot>` via `assignedelements()`) — for finding slotted content across a shadow boundary that `queryselectorall()` can't cross |\n| `flush(options?)` | drain reactive updates and animation frames |\n| `debugflush()` | run `flush()` with `console.debug` diagnostics |\n| `mock(tag, template?)` | register a no op stub custom element |\n| `renderhook(setup)` | run lifecycle hooks in isolation; overload accepts `propdefs` as first arg for typed props |\n| `resetorefortests()` | reset styles and id counters when mounting is managed manually |\n| `oretimeouterror` | error thrown when `flush()` cannot settle tracked ore work |\n\n> **test isolation:** `cleanup()` removes mounted elements and resets all cross test ore state (the stylesheet cache and id counters) via `resetorefortests()`. call it in `aftereach` (or use `install()`) to prevent state leaking between tests.\n\nimport `within`, named dispatchers such as `fireclick`, and waits such as `waituntil` or `waitforevent` from\n`@vielzeug/assay`.\n\n> **form associated component testing:** jsdom implements none of the `elementinternals` form association api — `install(aftereach, { forminternals: true })` polyfills `setformvalue`/`setvalidity`/`checkvalidity`/`reportvalidity`/`validationmessage`/`validity`/`states`, mixes `checkvalidity`/`reportvalidity`/`validity`/`validationmessage` onto the host element itself (real browsers do this for any `formassociated: true` element), makes `formdata` collect a form associated element's set value, and makes `<form>.reset()` invoke `formresetcallback()`. every patch is a guarded no op when its target already exists, and `installforminternalspolyfill()` returns an `uninstall()` that restores every patched global. the polyfill is opt in (`{ forminternals: true }`) because the patches are global — suites without form associated components shouldn't carry them. a downstream package (e.g. a component library built on `ore`) should rely on this instead of hand rolling its own copy.\n\n#### `fixture` interface\n\n```ts\ninterface fixture<t extends htmlelement = htmlelement> {\n [symbol.dispose](): void; // delegates to dispose() — enables `using` declarations\n element: t;\n readonly disposed: boolean; // true after dispose() has been called\n readonly shadow: shadowroot | null;\n get<e extends element>(selector: string): e;\n query<e extends element>(selector: string): e | null;\n queryall<e extends element>(selector: string): e[];\n getbytext<e extends element>(text: string, selector?: string): e;\n querybytext<e extends element>(text: string, selector?: string): e | null;\n queryallbytext<e extends element>(text: string, selector?: string): e[];\n getbytestid<e extends element>(testid: string): e;\n querybytestid<e extends element>(testid: string): e | null;\n queryallbytestid<e extends element>(testid: string): e[];\n attr(name: string, value: string | number | boolean): promise<void>;\n attrs(record: record<string, string | number | boolean>): promise<void>;\n flush(options?: flushoptions): promise<void>;\n act(fn: () => unknown): promise<void>;\n dispose(): void; // removes the component from the dom — idempotent\n}\n```\n\n#### `renderhook`\n\nuseful for testing composable lifecycle hooks (`onmounted`, `watcheffect`, `inject`, etc.) without a template. `onmounted`/`oncleanup`/`watcheffect`/... work exactly as inside a real `setup()`, since they resolve the same implicit current component context:\n\n```ts\n// without props\nconst { result, flush, dispose } = await renderhook(() => {\n const count = signal(0);\n onmounted(() => {\n count.value = 1;\n });\n return count;\n});\nexpect(result.value).tobe(1);\n\n// with typed props (prop defs overload)\nconst { result } = await renderhook({ label: prop.string('hello'), count: prop.number(0) }, (props) => props.label);\nexpect(result.value).tobe('hello');\n```\n\n## ripple primitives\n\nore does **not** re export reactive primitives. import them directly from `@vielzeug/ripple`:\n\n```ts\nimport { batch, computed, signal, watch } from '@vielzeug/ripple';\n```\n\nsee the [ripple documentation](/ripple/) for the full api.\n\n## lifecycle events\n\n| event | when |\n| | |\n| `ore:connect` | after every `connectedcallback` (including reconnects) |\n| `ore:disconnect` | after `disconnectedcallback`, before component state is reset |\n| `ore:error` | when a lifecycle callback fails — bubbles, composed; detail is `orelifecycleerror` |\n\n## types\n\n```ts\ntype propdef<t> = {\n readonly default: t;\n readonly parse: (value: string | null) => t;\n reflect?: boolean;\n};\n\ntype propsdef<t extends record<string, unknown>> = {\n [k in keyof required<t>]: propdef<t[k & keyof t]>;\n};\n\ntype propinputdefs = record<string, propdef<unknown>>;\n\n/**\n * infer reactive props type from a propinputdefs map.\n * each entry becomes readable<t> keyed by prop name.\n */\ntype inferprops<d extends propinputdefs> = {\n readonly [k in keyof d] ?: readable<inferpropvalue<d[k]>>;\n};\n\n// runtime hooks — all plain functions imported from '@vielzeug/ore', not fields on an object.\ntype onmountedcallback = () => cleanup | undefined;\ntype onformresetcallback = () => void;\n\ndeclare function onmounted(fn: onmountedcallback): void; // dom ready callback; runs after each connection's render\ndeclare function oncleanup(fn: cleanup): void; // register teardown; called on disconnect\ndeclare function onelement<t extends htmlelement>(\n ref: readable<t | null>,\n callback: (el: t) => cleanup | undefined,\n): () => void;\ndeclare function onevent<k extends keyof htmlelementeventmap>(\n target: eventtarget | null | undefined,\n event: k,\n listener: (e: htmlelementeventmap[k]) => void,\n options?: addeventlisteneroptions,\n): void;\ndeclare function onevent(\n target: eventtarget | null | undefined,\n event: string,\n listener: eventlistener,\n options?: addeventlisteneroptions,\n): void;\ndeclare function onformreset(fn: onformresetcallback): void; // runs on every ancestor <form> reset; formassociated only\ndeclare function watcheffect(fn: () => cleanup | undefined): () => void; // scoped reactive effect; auto cleaned on disconnect\ndeclare function bind(config: hostbindconfig, options?: bindoptions): () => void; // bindings for host or any target element\ndeclare function provide<t>(key: injectionkey<t>, value: t): void; // register a context value on the host element\ndeclare function inject<t>(key: injectionkey<t>): t | undefined;\ndeclare function inject<t>(key: injectionkey<t>, fallback: t): t;\ndeclare function gethost(): htmlelement; // the current component's host element\ndeclare function useemit<emits extends record<string, unknown> = record<string, never>>(): emitfn<emits>;\ndeclare function useslots<slotnames extends string = string>(): componentslots<slotnames>;\n\ntype componentdefinition<props extends record<string, unknown> = record<never, never>> = {\n formassociated?: boolean;\n props?: propsdef<props>;\n setup: (props: inferprops<propsdef<props>>) => htmlresult | null;\n shadow?: partial<shadowrootinit> | false; // false = light dom\n styles?: (string | cssstylesheet | cssresult)[];\n};\n\ntype hostbindingvalue =\n | (() => string | number | boolean | null | undefined)\n | readable<string | number | boolean | null | undefined>\n | string\n | number\n | boolean\n | null\n | undefined;\n\ntype reflectconfig = record<string, hostbindingvalue>;\n\ntype hostbindconfig = {\n aria?: reflectconfig;\n attr?: reflectconfig;\n class?: (() => record<string, boolean>) | record<string, readable<boolean> | (() => boolean) | boolean>;\n on?: record<string, ((event: event) => void) | undefined>;\n style?: record<string, hostbindingvalue>;\n};\n\ntype bindoptions = addeventlisteneroptions & {\n target?: element;\n};\n\ntype hostbindfn = (config: hostbindconfig, options?: bindoptions) => () => void;\n\ntype componentslots<s extends string = string> = {\n elements(name?: s): readable<element[]>;\n has(name?: s): readable<boolean>;\n};\n\ntype ref<t extends element> = signal<t | null>;\n\ntype refcallback<t extends element> = (el: t | null) => void;\n\ntype injectionkey<t> = symbol & { readonly __ore_injection_key?: t };\n\ninterface htmlresult {\n mount(\n parent: parentnode,\n anchor: node | null,\n registercleanup: (fn: () => void) => void,\n ): node[];\n}\n\ntype cssresult = {\n content: string;\n tostring(): string;\n};\n\ntype livebinding<t> = { readonly source: readable<t> };\n\ntype emitfn<t extends record<string, unknown>> = {\n <k extends keyswithoutdetail<t>>(event: k): boolean;\n <k extends exclude<keyof t, keyswithoutdetail<t>>>(event: k, detail: t[k]): boolean;\n};\n// keyswithoutdetail is an internal helper type, not exported.\n\ntype formfieldoptions<t = unknown> = {\n disabled?: readable<boolean>;\n el?: htmlelement;\n emptystringfornull?: boolean;\n onreset?: () => void;\n toformvalue?: (value: t) => file | formdata | string | null;\n validationmessage?: readable<string>;\n validity?: readable<validitystateflags | null>;\n value: signal<t> | readable<t>;\n};\n\ntype formfieldhandle = {\n checkvalidity: () => boolean;\n readonly internals: elementinternals;\n reportvalidity: () => boolean;\n setcustomvalidity: (message: string) => void;\n};\n\ntype mutationobservervalue = {\n entries: mutationrecord[];\n latest: mutationrecord | null;\n};\n\n/** phase in which a oreerror occurred. */\ntype oreerrorphase = 'each reconcile' | 'form reset' | 'mounted' | 'setup';\n```\n\n## errors\n\n`oreerror` is the base class for every ore error class — `err instanceof oreerror` catches all of them.\n`oreerror.is(err)` is the equivalent static type guard.\n\n **`oreapierror`** — thrown when the `ore` api itself is misused: calling `define()` with a duplicate tag, calling a lifecycle hook (`inject`, `onmounted`, `oncleanup`, `onevent`, …) outside of `setup()`, or passing an invalid prop definition to `define()`.\n **`oreinternalerror`** — thrown when an ore invariant fails, indicating a package bug rather than invalid application code.\n **`orelifecycleerror`** — reported in the `ore:error` event when component `setup()`, a mounted callback, a form reset callback, or `each()` reconciliation fails. extends `oreerror` with:\n `component: string` — the element's local name\n `phase: oreerrorphase` — `'setup'` | `'mounted'` | `'form reset'` | `'each reconcile'`\n `cause: error` — the original error thrown by `setup()`\n **`oretimeouterror`** — thrown by `flush()` (from `@vielzeug/ore/testing`) when pending ore work does not settle before its timeout.\n\nlifecycle failures dispatch a bubbling, composed `ore:error` event whose `detail` is the `orelifecycleerror`. setup\nfailures still rethrow their original error; mounted and form reset callback failures are reported through the same\nevent so their remaining callbacks can continue.\n",
|
|
796
|
-
"usage": " \ntitle: ore — usage guide\ndescription: practical ore usage patterns for components, props, templates, slots, context, forms, observers, and tests.\n \n\n[[toc]]\n\n## basic usage\n\n`define(tag, definition)` registers a custom element.\n\nyour `setup()` function receives typed prop signals and returns an `htmlresult` directly. its state belongs to the\ncurrent connection: disconnect disposes it, and reconnecting the same element runs setup again.\n\n```ts\nimport { signal } from '@vielzeug/ripple';\nimport { define, html } from '@vielzeug/ore';\n\ndefine('status chip', {\n setup() {\n const online = signal(true);\n\n return html`\n <button @click=${() => (online.value = !online.value)}>${() => (online.value ? 'online' : 'offline')}</button>\n `;\n },\n});\n```\n\neverything besides `props` — lifecycle hooks, host bindings, context, slots, emit — is a plain function imported from `@vielzeug/ore`, called directly from `setup()` (or a composable it calls):\n\n```ts\nimport { define, gethost, html, bind, useemit, useslots } from '@vielzeug/ore';\n\ndefine('my widget', {\n setup(_props) {\n const el = gethost(); // the host htmlelement\n const emit = useemit<{ close: undefined }>(); // typed event emitter\n const slots = useslots<'header'>(); // reactive slot observation\n\n bind({ attr: { role: 'group' } }); // host binding helper (attr, class, style, on)\n\n return html`<slot></slot>`;\n },\n});\n```\n\n## signals and effects\n\nore does not re export ripple primitives — import them directly from `@vielzeug/ripple`.\n\n```ts\nimport { batch, computed, effect, signal, watch } from '@vielzeug/ripple';\n\nconst count = signal(0);\nconst doubled = computed(() => count.value * 2);\n\neffect(() => {\n console.log('doubled =', doubled.value);\n});\n\nwatch(count, (next, prev) => {\n console.log('count changed', prev, ' >', next);\n});\n\nbatch(() => {\n count.value = 1;\n count.value = 2;\n});\n```\n\n## onmounted and lifecycle\n\nuse `onmounted()` for dom dependent initialization that must run after the template is mounted. use `onelement(ref, cb)` for work tied to a specific dom node. `onevent()` attaches a listener that is automatically removed on disconnect.\n\n```ts\nimport { signal } from '@vielzeug/ripple';\nimport { define, html, onelement, onevent, onmounted, ref, useslots } from '@vielzeug/ore';\n\ndefine('deferred init', {\n setup(_props) {\n const tabindex = signal(0);\n const inputref = ref<htmlinputelement>();\n const slots = useslots<'items'>();\n\n onmounted(() => {\n const items = slots.elements('items').value;\n console.log('found', items.length, 'items');\n });\n\n onelement(inputref, (input) => {\n input.focus();\n });\n\n onevent(window, 'keydown', (e: keyboardevent) => {\n if (e.key === 'escape') tabindex.value = 0;\n });\n\n return html`<div><slot name=\"items\"></slot><input ref=${inputref} /></div>`;\n },\n});\n```\n\n## prop definitions\n\nuse `prop.*` helpers for common cases, or raw `propdef` objects for custom parsing or `reflect: false`.\n\n```ts\nimport { define, html, prop } from '@vielzeug/ore';\n\ndefine('x button', {\n props: {\n label: prop.string('button'),\n disabled: prop.bool(false),\n variant: prop.oneof(['primary', 'secondary'] as const, 'primary'),\n count: prop.number(0),\n },\n setup(props) {\n return html`\n <button ?disabled=${props.disabled} data variant=${props.variant}>${props.label} (${props.count})</button>\n `;\n },\n});\n```\n\n## template bindings\n\n`html` supports text, attributes, booleans, properties, events, refs, and nested templates.\n\n```ts\nimport { computed, signal } from '@vielzeug/ripple';\nimport { define, html, ref } from '@vielzeug/ore';\n\ndefine('profile name', {\n setup() {\n const name = signal('alice');\n const inputref = ref<htmlinputelement>();\n\n return html`\n <label title=${computed(() => 'current: ' + name.value)}>name</label>\n <input\n ref=${inputref}\n value=${name}\n aria label=${() => 'current name ' + name.value}\n @input=${(event: event) => {\n name.value = (event.target as htmlinputelement).value;\n }} />\n <p>hello ${name}</p>\n `;\n },\n});\n```\n\n## directives\n\nore exports `each`, `classmap`, `stylemap`, `when`, `live`, and `unsafehtml` from `@vielzeug/ore`. use ordinary\nattribute bindings plus native event handlers for two way input state; no special model directive is required.\n\n```ts\nimport { signal } from '@vielzeug/ripple';\nimport { classmap, define, each, html, stylemap, when } from '@vielzeug/ore';\n\ndefine('task list', {\n setup() {\n const tasks = signal([{ id: 1, text: 'write tests' }]);\n const active = signal(true);\n\n return html`\n <ul\n class=\"${classmap({ ready: () => tasks.value.length > 0 })}\"\n style=${stylemap({ opacity: () => (active.value ? 1 : 0.5) })}>\n ${when(\n () => active.value,\n () => html`<li>active</li>`,\n () => html`<li>paused</li>`,\n )}\n ${each(\n tasks,\n (task) => task.id,\n (task) => html`<li>${() => task.value.text}</li>`,\n )}\n </ul>\n `;\n },\n});\n```\n\n### each() api\n\n`each(source, key, render, fallback?)` takes positional arguments:\n\n **source** — signal, getter, or plain array\n **key** — function returning a unique key per item\n **render** — receives reactive `item` and `index` signals\n **fallback** — optional, rendered when the list is empty\n\n```ts\neach(\n items,\n (item) => item.id,\n (item, index) => html`<li>#${index}: ${() => item.value.label}</li>`,\n () => html`<li>no items</li>`,\n);\n```\n\n## live form bindings\n\nuse `live(signal)` for inputs that should preserve in progress user edits instead of overwriting the dom on stale writes.\n\n```ts\nimport { signal } from '@vielzeug/ripple';\nimport { define, html, live } from '@vielzeug/ore';\n\ndefine('live search', {\n setup() {\n const query = signal('');\n\n return html`\n <input value=${live(query)} @input=${(e: event) => (query.value = (e.target as htmlinputelement).value)} />\n `;\n },\n});\n```\n\n## host bindings\n\n`bind()` wires reactive attrs, classes, styles, and events to the host element.\n\n```ts\nimport { signal } from '@vielzeug/ripple';\nimport { bind, define, html } from '@vielzeug/ore';\n\ndefine('x toggle', {\n setup(_props) {\n const open = signal(false);\n\n bind({\n attr: { 'aria expanded': () => string(open.value), role: 'button', tabindex: 0 },\n class: { 'is open': open },\n on: { click: () => (open.value = !open.value) },\n });\n\n return html`<slot></slot>`;\n },\n});\n```\n\nthe `bind` config supports `attr`, `class`, `style`, and `on` sections.\n\n## aria bindings\n\nuse `bind({ aria: config }, { target })` to reactively sync aria attributes to any element. shorthand keys are normalised to `aria *` automatically — `expanded` becomes `aria expanded`, `role` is set verbatim.\n\n```ts\nimport { signal } from '@vielzeug/ripple';\nimport { bind, define, html, onmounted } from '@vielzeug/ore';\n\ndefine('x disclosure', {\n setup(_props) {\n const open = signal(false);\n const panelid = 'disclosure panel';\n\n bind({\n attr: { role: 'button', tabindex: 0 },\n on: { click: () => (open.value = !open.value) },\n });\n\n onmounted(() => {\n const trigger = document.queryselector('#trigger') as htmlelement;\n if (trigger) {\n // bind() registers cleanup automatically when called inside setup\n bind(\n {\n aria: {\n controls: panelid,\n expanded: () => string(open.value),\n haspopup: 'region',\n },\n },\n { target: trigger },\n );\n }\n });\n\n return html`<slot></slot>`;\n },\n});\n```\n\nstatic values are applied once. getter functions create reactive effects. setting a value to `null`, `undefined`, or `false` removes the attribute.\n\n`bind()` always returns a cleanup function. use it to stop syncing early when a trigger element can be swapped out:\n\n```ts\nonmounted(() => {\n const trigger = document.queryselector('#trigger') as htmlelement;\n const stoparia = bind({ aria: { expanded: () => string(open.value) } }, { target: trigger });\n\n // stop syncing when the trigger is replaced\n oncleanup(stoparia);\n});\n```\n\n### binding a non host element with `bind()`\n\npass `{ target: el }` as a second argument to bind attributes, classes, styles, or events to any element:\n\n```ts\nimport { signal } from '@vielzeug/ripple';\nimport { bind, define, html, onmounted, ref } from '@vielzeug/ore';\n\ndefine('button wrapper', {\n setup(_props) {\n const visible = signal(false);\n const btnref = ref<htmlbuttonelement>();\n\n onmounted(() => {\n const btn = btnref.value;\n if (!btn) return;\n\n bind(\n {\n attr: { 'aria pressed': () => string(visible.value) },\n on: { click: () => (visible.value = !visible.value) },\n },\n { target: btn },\n );\n });\n\n return html`<button ref=${btnref}>toggle</button>`;\n },\n});\n```\n\n## slots and emits\n\n```ts\nimport { define, html, useemit, useslots, when } from '@vielzeug/ore';\n\ndefine('card with footer', {\n setup(_props) {\n const slots = useslots<'header' | 'footer'>();\n const emit = useemit<{ action: undefined }>();\n\n return html`\n <div class=\"card\">\n <slot name=\"header\"></slot>\n <slot></slot>\n ${when(slots.has('footer'), () => html`<footer><slot name=\"footer\"></slot></footer>`)}\n </div>\n <button @click=${() => emit('action')}>go</button>\n `;\n },\n});\n```\n\npass a `slotnames` type parameter to `useslots<slotnames>()` to get typed `slots.has()` and `slots.elements()` calls.\n\n## context provide/inject\n\n```ts\nimport { signal } from '@vielzeug/ripple';\nimport { createcontext, define, html, injectstrict, provide } from '@vielzeug/ore';\n\nconst count_ctx = createcontext<returntype<typeof signal<number>>>('count');\n\ndefine('count provider', {\n setup(_props) {\n const count = signal(0);\n provide(count_ctx, count);\n\n return html`<button @click=${() => count.value++}><slot></slot></button>`;\n },\n});\n\ndefine('count consumer', {\n setup() {\n const count = injectstrict(count_ctx);\n\n return html`<p>count: ${count}</p>`;\n },\n});\n```\n\n## form associated elements\n\n```ts\nimport { signal } from '@vielzeug/ripple';\nimport { define, html, prop } from '@vielzeug/ore';\nimport { usefield } from '@vielzeug/ore';\n\ndefine('rating input', {\n formassociated: true,\n setup() {\n const value = signal(0);\n const field = usefield({ value });\n\n return html`\n <button @click=${() => (value.value = 1)}>1</button>\n <button @click=${() => (value.value = 2)}>2</button>\n <button @click=${() => (value.value = 3)}>3</button>\n <button @click=${() => field.reportvalidity()}>validate</button>\n <p>current: ${value}</p>\n `;\n },\n});\n```\n\n## platform observers\n\nobserver helpers from `@vielzeug/ore` require real dom nodes, so call them inside `onmounted()`.\n\n```ts\nimport { effect } from '@vielzeug/ripple';\nimport { define, html, intersectionobserver, mediaobserver, onmounted, ref, resizeobserver } from '@vielzeug/ore';\n\ndefine('x observed', {\n setup(_props) {\n const boxref = ref<htmldivelement>();\n\n onmounted(() => {\n const element = boxref.value;\n if (!element) return;\n\n const size = resizeobserver(element);\n const visible = intersectionobserver(element, { threshold: 0.5 });\n const dark = mediaobserver('(prefers color scheme: dark)');\n\n // effect() auto tracks every signal read inside — re runs when any of the three change.\n effect(() => {\n console.log(size.value.width, visible.value?.isintersecting, dark.value);\n });\n });\n\n return html`<div ref=${boxref}>observe me</div>`;\n },\n});\n```\n\n## testing utilities\n\nimport from `@vielzeug/ore/testing`.\n\n```ts\nimport { aftereach, describe, expect, it } from 'vitest';\nimport { signal } from '@vielzeug/ripple';\nimport { fireclick } from '@vielzeug/assay';\nimport { html } from '@vielzeug/ore';\nimport { cleanup, mount } from '@vielzeug/ore/testing';\n\ndescribe('my counter', () => {\n aftereach(cleanup);\n\n it('increments on click', async () => {\n let count!: returntype<typeof signal<number>>;\n const { query, act } = await mount(() => {\n count = signal(0);\n return html`<button @click=${() => count.value++}>${count}</button>`;\n });\n\n expect(query('button')?.textcontent).tobe('0');\n\n await act(() => fireclick(query('button')!));\n\n expect(query('button')?.textcontent).tobe('1');\n });\n});\n```\n\n## framework integration\n\nore components are standard custom elements and work natively in any framework.\n\n::: code group\n\n```tsx [react]\n// react 19+ supports custom elements natively.\nimport './x toggle'; // wherever define('x toggle', { ... }) is called\n\nfunction app() {\n return <x toggle aria label=\"open menu\" />;\n}\n```\n\n```ts [vue 3]\n<script setup lang=\"ts\">\nimport './x toggle'; // wherever define('x toggle', { ... }) is called\nimport { ref } from 'vue';\n\nconst open = ref(false);\n</script>\n\n<template>\n <x toggle :aria label=\"'open menu'\" @click=\"open = !open\" />\n</template>\n```\n\n```svelte [svelte]\n<script>\n import './x toggle'; // wherever define('x toggle', { ... }) is called\n\n function handleclick() {\n console.log('toggled');\n }\n</script>\n\n<x toggle aria label=\"open menu\" on:click={handleclick} />\n```\n\n:::\n\n## working with other vielzeug libraries\n\n### with ripple\n\nimport ripple primitives directly from `@vielzeug/ripple` for standalone reactive state outside components.\n\n```ts\nimport { signal, computed } from '@vielzeug/ripple';\nimport { define, html } from '@vielzeug/ore';\n\n// shared state created outside any component\nconst theme = signal<'light' | 'dark'>('light');\nconst isdark = computed(() => theme.value === 'dark');\n\ndefine('theme toggle', {\n setup() {\n return html`\n <button @click=${() => (theme.value = isdark.value ? 'light' : 'dark')}>\n ${() =>\n isdark.value ? '<ore icon name=\"sun\" size=\"16\"></ore icon>' : '<ore icon name=\"moon\" size=\"16\"></ore icon>'}\n </button>\n `;\n },\n});\n```\n\n### with forge\n\nuse `@vielzeug/forge` for typed form state. `usefield()` remains intentionally narrow: it connects a form associated\ncustom element to native `elementinternals` without imposing submission, validation, or dirty state policy.\n\n```ts\nimport { createform } from '@vielzeug/forge';\nimport { define, html } from '@vielzeug/ore';\n\ndefine('signup form', {\n setup(_props) {\n const form = createform({ initialvalues: { email: '' } });\n\n return html`\n <form\n @submit=${(event: submitevent) => {\n event.preventdefault();\n void form.submit(async (values) => {\n console.log(values);\n });\n }}>\n <slot></slot>\n </form>\n `;\n },\n});\n```\n\n## best practices\n\n setup returns `html\\`...\\`` directly — not a function wrapping the template.\n use `watcheffect()` for reactive subscriptions tied to component lifetime — it auto registers cleanup on disconnect.\n use `onelement(ref, cb)` instead of `onmounted` when the work is tied to a single dom node.\n bind host attributes and classes via `bind()` rather than mutating the element directly.\n provide context at the nearest ancestor — avoid global context singletons.\n call `oncleanup()` for every resource allocated in `setup()` (websockets, intervals, external subscriptions).\n use `live(signal)` for form inputs to prevent clobbering user in progress edits.\n extract composable helper functions freely — `onmounted`/`oncleanup`/`bind`/... resolve the active component through implicit context, so they work from any function called (transitively) during `setup()`, with no need to pass them in as parameters.\n test component mounting and lifecycle with `@vielzeug/ore/testing`; import generic dom events, queries, and waits\n from `@vielzeug/assay`.\n",
|
|
896
|
+
"index": " \ntitle: ore — web component authoring with signals\ndescription: functional custom element authoring with typed props, reactive templates, lifecycle helpers, and testing utilities.\npackage: ore\ncategory: ui primitives\nkeywords: [web components, custom elements, reactive, templates, signals, lifecycle]\nrelated: [ripple, refine, orbit]\nexports: [define, prop, html, css, ref, createcontext, inject, injectstrict, provide, onmounted, oncleanup, onevent, onelement, onformreset, watcheffect, useemit, useslots, gethost, bind, each, when, classmap, stylemap, live, unsafehtml, usefield, createid, createstableid, resetstableidcounter, oreerror, oreapierror, oreinternalerror, orelifecycleerror, bindoptions]\nenvironments: [browser]\n \n\n<! markdownlint disable md025 md033 md060 >\n\n<packagehero package=\"ore\" />\n\n## why ore?\n\nore keeps custom elements functional and signal driven while giving you direct control over templates, lifecycle hooks, host bindings, and form associated behavior.\n\n```ts\n// before — vanilla custom element boilerplate\nclass mycounter extends htmlelement {\n #count = 0;\n connectedcallback() {\n this.attachshadow({ mode: 'open' });\n this.#render();\n }\n #render() {\n this.shadowroot!.innerhtml = `<button>${this.#count}</button>`;\n this.shadowroot!.queryselector('button')!.onclick = () => {\n this.#count++;\n this.#render();\n };\n }\n}\ncustomelements.define('my counter', mycounter);\n\n// after — ore\nimport { signal } from '@vielzeug/ripple';\nimport { define, html } from '@vielzeug/ore';\n\ndefine('my counter', {\n setup() {\n const count = signal(0);\n return html`<button @click=${() => count.value++}>${count}</button>`;\n },\n});\n```\n\n| feature | ore | lit | stencil |\n| | | | |\n| bundle size | <packageinfo package=\"ore\" type=\"size\" /> | ~12 kb | ~60 kb+ toolchain |\n| signal first runtime | <ore icon name=\"check\" size=\"16\"></ore icon> | <ore icon name=\"x\" size=\"16\"></ore icon> (separate signals package) | <ore icon name=\"x\" size=\"16\"></ore icon> |\n| functional component setup | <ore icon name=\"check\" size=\"16\"></ore icon> | partial | <ore icon name=\"x\" size=\"16\"></ore icon> |\n| typed prop helpers | <ore icon name=\"check\" size=\"16\"></ore icon> | partial | <ore icon name=\"check\" size=\"16\"></ore icon> |\n| host binding helpers | <ore icon name=\"check\" size=\"16\"></ore icon> | partial | partial |\n| form associated helpers | <ore icon name=\"check\" size=\"16\"></ore icon> | manual | partial |\n| zero dependencies | <ore icon name=\"check\" size=\"16\"></ore icon> | <ore icon name=\"check\" size=\"16\"></ore icon> | <ore icon name=\"x\" size=\"16\"></ore icon> |\n\n<div class=\"decision callout\">\n\n**use ore when** you want typed, signal driven custom elements with minimal runtime overhead and no framework lock in.\n\n**consider lit when** you need a mature ecosystem with wide community adoption and don't need signal based reactivity.\n\n</div>\n\n## installation\n\n::: code group\n\n```sh [pnpm]\npnpm add @vielzeug/ore @vielzeug/ripple\n```\n\n```sh [npm]\nnpm install @vielzeug/ore @vielzeug/ripple\n```\n\n```sh [yarn]\nyarn add @vielzeug/ore @vielzeug/ripple\n```\n\n:::\n\n## quick start\n\n```ts\nimport { computed, signal } from '@vielzeug/ripple';\nimport { bind, css, define, html, onmounted, prop } from '@vielzeug/ore';\n\ndefine('my counter', {\n props: {\n label: prop.string('count'),\n step: prop.number(1),\n },\n styles: [\n css`\n :host {\n display: inline grid;\n gap: 0.5rem;\n }\n `,\n ],\n setup(props) {\n const count = signal(0);\n const doubled = computed(() => count.value * 2);\n\n bind({ class: { 'is positive': () => count.value > 0 } });\n\n onmounted(() => console.log('mounted'));\n\n return html`\n <button @click=${() => (count.value += props.step.value)}>${props.label}: ${count}</button>\n <p>doubled: ${doubled}</p>\n `;\n },\n});\n```\n\n## features\n\n<div class=\"features grid\">\n\n signal first runtime with `signal`, `computed`, `watch`, `batch` from `@vielzeug/ripple` — import them directly\n functional component authoring via `define(tag, { props, setup, styles, formassociated })`\n props via `prop.*` helpers (`prop.string`, `prop.number`, `prop.bool`, `prop.oneof`, `prop.json`, `prop.data`) or raw `propdef` objects\n `setup(props)` takes only props and returns an `htmlresult` directly: `return html\\`...\\``\n lifecycle hooks — `onmounted`, `oncleanup`, `onevent`, `onelement`, `watcheffect` — plain functions imported from `@vielzeug/ore`, called directly from `setup()` or any composable it calls\n directives: `each` (keyed reactive list rendering), `classmap`, `stylemap`, `when`, `live`, `unsafehtml`\n host bindings via `bind({ attr, class, style, on })` — pass `{ target: el }` to bind any off host element\n reactive aria sync via `bind({ aria }, { target })` — applies `aria *` attributes reactively to any element, auto cleanup on disconnect\n context via `provide(key, value)` / `inject(key)`; typed emit/slots via `useemit<emits>()` / `useslots<slotnames>()`\n form associated `usefield()` and observer helpers are root exports\n testing utilities (`@vielzeug/ore/testing`) — `mount`, `renderhook`, `flush`, `cleanup`\n generic testing utilities (scoped queries, named event dispatchers, and async waits) are exported by `@vielzeug/assay`\n debug utilities (`@vielzeug/ore/testing`) — `debugflush()` for diagnosing update timing\n\n</div>\n\n## package entry points\n\n| import | purpose |\n| | |\n| `@vielzeug/ore` | all browser runtime apis: components, directives, `usefield`, and observers |\n| `@vielzeug/ore/testing` | ore specific mounting, lifecycle flushing, hooks, cleanup, and form internals |\n| `@vielzeug/assay` | generic dom events, scoped queries, and async waiting |\n\n## documentation\n\n<div class=\"doc links\">\n\n [usage guide](./usage.md)\n [api reference](./api.md)\n [examples](./examples.md)\n [migration guide](./migration.md)\n\n</div>\n\n## see also\n\n<div class=\"see also\">\n\n [refine](../refine/index.md) for prebuilt accessible components powered by ore.\n [ripple](../ripple/index.md) for reactive state used inside ore components.\n [forge](../forge/index.md) for typed form state that integrates with ore.\n\n</div>\n\n<! markdownlint enable md025 md033 md060 >\n",
|
|
897
|
+
"api": " \ntitle: ore — api reference\ndescription: complete api reference for @vielzeug/ore and @vielzeug/ore/testing.\n \n\n[[toc]]\n\n## api overview\n\nall browser runtime symbols below are imported from `@vielzeug/ore`. lifecycle/context/binding functions (`onmounted`, `oncleanup`, `onevent`, `onelement`, `watcheffect`, `bind`, `provide`, `useemit`, `useslots`, `gethost`) resolve the active component through an implicit \"current component\" context — they work when called synchronously during `setup()`, or from any composable function `setup()` calls (transitively), but throw if called outside that window.\n\n> `watcheffect` is not named `watch` — `@vielzeug/ripple` already exports a `watch(source, callback)` with different semantics (explicit source + old/new value pair), and the two are frequently imported in the same file.\n\n| symbol | purpose | execution mode | common gotcha |\n| | | | |\n| `define()` | register a custom element with reactive setup | sync | tag must contain a hyphen; call before first use |\n| `html` | tagged template literal returning htmlresult | sync | expressions must be signals, functions, or primitives |\n| `prop.*` | typed prop helpers (string, bool, number, …) | sync | prop values are signals — read `.value` |\n| `provide()`/`inject()` | context api for parent to descendant sharing | setup only | must be called synchronously during `setup()` |\n| `ref()` | reactive reference to a dom element | sync | value is null until after first mount |\n| `createcontext()` | create a typed injection key | sync | context is scoped to the component tree |\n| `each()` | keyed list rendering with dom diffing | sync | duplicate keys report `ore:error`; plain `t[]` is a one time static render |\n| `when()` | conditional branch rendering | sync | getter fn computed disposed on cleanup; static bool skips subscription |\n| `live(signal)` | one way binding that skips stale writes during input | sync | use for controlled inputs alongside a manual `@input` handler |\n| `onmounted(fn)` | dom ready callback | setup only | must be called synchronously during `setup()` |\n| `oncleanup(fn)` | register teardown | setup only | called on component disconnect |\n| `onevent(target, …)` | scoped event listener with auto cleanup | setup only | no ops on null target; removed on disconnect |\n| `usefield(options)` | wire signal to form `elementinternals` | setup only | requires `formassociated: true` on the component definition |\n| `onformreset(fn)` | run work when the ancestor `<form>` resets | setup only | fires every reset (not one shot); only for `formassociated: true` components |\n| `useemit<emits>()` | typed `emit()` bound to the current host | setup only | call once per component; returns `dispatchevent`'s boolean (`false` if a listener called `preventdefault()`) |\n| `useslots<slotnames>()`| reactive slot presence/element signals | setup only | safe to call more than once — the underlying registry is created once |\n| `gethost()` | the current component's host element | setup only | prefer a higher level helper (`bind`, …) when one exists |\n\n## package entry points\n\n| import | purpose |\n| | |\n| `@vielzeug/ore` | all browser runtime apis, including directives, fields, and lifecycle helpers |\n| `@vielzeug/ore/testing` | ore specific mounting, lifecycle, hook, cleanup, and form test support |\n| `@vielzeug/assay` | generic dom events, scoped queries, and async waiting |\n\n## core component api\n\n### `define(tag, definition)`\n\n```ts\ndefine<props>(tag: string, definition: componentdefinition<props>): void;\n```\n\nthe `setup()` function receives only typed prop signals:\n\n```ts\nsetup(props) {\n return html`<div>${props.label}</div>`;\n}\n```\n\neverything else — lifecycle hooks, host bindings, context, slots, emit — is a plain function imported from `@vielzeug/ore`, called directly from `setup()` (or a composable it calls):\n\n```ts\nimport { define, html, onmounted, useemit, useslots } from '@vielzeug/ore';\n\ndefine('my card', {\n setup(_props) {\n const emit = useemit<{ close: undefined }>();\n const slots = useslots<'header' | 'footer'>();\n\n onmounted(() => console.log('mounted'));\n\n // emit() returns dispatchevent's boolean — false if a listener called preventdefault()\n const notcancelled = emit('close');\n\n return html`${when(slots.has('header'), () => html`<slot name=\"header\"></slot>`)}`;\n },\n});\n```\n\n`useemit<emits>()` and `useslots<slotnames>()` are factory hooks — call them once per setup run to get a typed\n`emit`/`slots` bound to the current host. `useslots()` is safe to call more than once within that setup run.\n\n### componentdefinition\n\n```ts\ntype componentdefinition<props> = {\n formassociated?: boolean;\n props?: propsdef<props>;\n setup: (props: inferprops<propsdef<props>>) => htmlresult | null;\n shadow?: partial<shadowrootinit> | false; // false = light dom (no shadow root)\n styles?: (string | cssstylesheet | cssresult)[];\n};\n```\n\n## runtime helpers\n\n`onmounted`, `oncleanup`, `onevent`, `onelement`, and `watcheffect` are plain functions imported from `@vielzeug/ore`. call them directly during `setup()`.\n\n```ts\nimport { html, oncleanup, onevent, onmounted } from '@vielzeug/ore';\n\nsetup(props) {\n onmounted(() => {\n // dom is ready; return a function for mount scoped cleanup\n return () => { /* cleanup on unmount */ };\n });\n\n oncleanup(() => { /* called on disconnect */ });\n\n onevent(window, 'keydown', (e) => { /* auto removed on disconnect */ });\n\n return html`...`;\n}\n```\n\nbecause these resolve the active component through an implicit context (rather than a value threaded through parameters), composable helper functions can call them directly too — no need to pass hooks in as options:\n\n```ts\nimport { oncleanup } from '@vielzeug/ore';\n\nfunction usemyhelper() {\n oncleanup(() => { /* teardown */ });\n}\n\n// in setup:\nsetup(_props) {\n usemyhelper();\n return html`...`;\n}\n```\n\n## props api\n\n| helper | signature | notes |\n| | | |\n| `prop.string(defaultvalue?)` | `propdef<string>` | reflects by default |\n| `prop.bool(defaultvalue?)` | `propdef<boolean>` | any non null attribute value other than `\"false\"` parses as `true`; `\"false\"` or absent attribute is `false` |\n| `prop.number(defaultvalue?)` | `propdef<number>` | returns default (not nan) and warns in dev when attribute is not a valid number |\n| `prop.oneof(allowed, defaultvalue)` | `propdef<t>` | restricts to provided string union |\n| `prop.json(defaultvalue)` | `propdef<t>` | json.parse from attribute; `reflect: false` |\n| `prop.data<t>(defaultvalue?)` | `propdef<t>` | js only — never reads/writes an attribute; use for objects, arrays, callbacks, or any non serialisable value |\n\n> **choosing the right prop helper:**\n>\n> **`prop.json`** — value can be declared in html (`<my el config='{\"x\":1}'>`); attribute string is `json.parse`d.\n> **`prop.data`** — value is always set from javascript (objects, arrays, callbacks, class instances); the attribute is never read. use this for both data and function props.\n\nwhen you need custom parsing or `reflect: false`, use a raw `propdef` object:\n\n```ts\nprops: {\n items: { default: [], parse: () => [], reflect: false },\n}\n```\n\nuse `prop.data` for props that hold js only values (including callbacks) that cannot be serialised through an html attribute:\n\n```ts\ndefine('data grid', {\n props: {\n getrowkey: prop.data<(row: unknown) => string>(),\n columns: prop.data<datagridcolumn[]>([]),\n onsort: prop.data<(key: string) => void>(),\n },\n setup(props) {\n // set from js: grid.getrowkey = (row) => row.id\n return html`...`;\n },\n});\n```\n\n## template and directives\n\n### `html`\n\ntagged template literal that returns an `htmlresult`. supports text interpolation, ordinary attributes (`attr=`),\nboolean attributes (`?attr=`), events (`@event=`), refs (`ref=`), and nested templates.\n\n### `css`\n\ntagged template literal that returns a `cssresult` for use in `styles`.\n\n### directives\n\n| directive | purpose |\n| | |\n| `each(source, key, render, fallback?)` | keyed reactive list; render receives `readable<t>` and `readable<number>`; plain `t[]` is a one time static snapshot |\n| `when(condition, truthy, falsy?)` | conditional rendering |\n| `classmap(record)` | reactive class string from object map |\n| `stylemap(record)` | reactive inline style string from object map |\n| `live(signal)` | one way binding that skips stale writes during active user input; use with `@input` handler |\n| `unsafehtml(value)` | html rendering sink; sanitize untrusted values before calling |\n\n### `unsafehtml`\n\n`unsafehtml()` is an explicit html injection sink. it has no global sanitizer: sanitize untrusted\ncontent before passing it to the directive, so the trust boundary remains at the call site.\n\n```ts\nimport { unsafehtml } from '@vielzeug/ore';\n\nconst safearticle = sanitize(usersuppliedarticle);\n\nreturn html`<article>${unsafehtml(safearticle)}</article>`;\n```\n\n## host bindings\n\n`bind(config, options?)` is a plain function imported from `@vielzeug/ore`:\n\n```ts\nbind({\n attr: { role: 'button', 'aria expanded': () => string(open.value) },\n class: { 'is open': open },\n style: { ' height': () => height.value + 'px' },\n on: { click: handleclick },\n});\n```\n\n`bind()` auto registers cleanup with the component scope — no manual `oncleanup` needed. returns a cleanup function for early teardown.\n\n### off host bindings\n\npass `{ target: el }` as a second argument to bind to any element other than the host:\n\n```ts\nbind(\n { attr: { 'aria expanded': () => string(isopen.value) } },\n { target: triggerel },\n);\n```\n\nevent listener options (`once`, `capture`, `passive`) are also accepted in the second argument. cleanup is auto registered with the component scope when called during setup.\n\n### reactive aria attributes\n\nfor reactive aria attribute syncing, use `bind({ aria: config }, { target })`. shorthand keys are normalised to `aria *` automatically (`expanded` → `aria expanded`; `role` is passed verbatim):\n\n```ts\n// inside setup — cleanup auto registered\nbind(\n {\n aria: {\n expanded: () => isopen.value,\n controls: panelid,\n haspopup: 'listbox',\n },\n },\n { target: triggerel },\n);\n\n// manage cleanup manually — bind() always returns a cleanup fn\nconst stoparia = bind({ aria: { expanded: () => isopen.value } }, { target: triggerel });\n// call stoparia() when the trigger is swapped out\n```\n\nstatic values (strings, numbers, booleans) are applied once. getter functions and signals create reactive effects. setting a value to `null`, `undefined`, or `false` removes the attribute.\n\n## slots\n\n `slots.has(name?)` — `readable<boolean>` — whether the named (or default) slot has assigned content\n `slots.elements(name?)` — `readable<element[]>` — the assigned elements for the slot\n\nslot signals update reactively when assigned content changes, including when slots are inserted dynamically (via `when()` or `each()`) after mount.\n\n## context api\n\n `createcontext<t>(description?)` — create a typed injection key\n `provide(key, value)` — provide a value to descendants\n `inject(key)` — resolve from nearest ancestor; returns `undefined` if not found\n `inject(key, fallback)` — resolve with a fallback value\n `injectstrict(key)` — resolve or throw if absent\n\n`provide()` and `inject()` must be called synchronously during `setup()`. calling them outside a setup context throws\n`'lifecycle hooks must be called during component setup'`. context resolution walks the ancestor chain including shadow\ndom boundaries. `inject()` resolves and caches its result once per consumer — provide a `readable` (signal/computed)\nrather than a raw value if descendants need to observe later changes; re calling `provide()` with a new raw value\nafterward is not seen by consumers that already resolved it (a dev mode warning fires when a key is provided twice on\nthe same element). `provide()` registers cleanup automatically — context keys are removed from the registry when the\nproviding component disconnects, so reconnecting the same element runs `setup()` fresh without spurious \"overwriting\"\nwarnings or stale keys leaking to descendants.\n\n## utilities\n\n `ref<t>()` — create a `signal<t | null>` element reference. set to the element via `ref=` in templates.\n `createid(prefix = 'id')` — generate a unique incremental string id (e.g. `'id 1'`, `'id 2'`). each call returns a new id — it does not deduplicate by prefix.\n `createstableid(prefix = 'id')` — generate a unique id that also embeds a short random tag shared across all ids generated in the session (e.g. `'field a3k21'`), reducing collision risk when multiple app instances run on the same page. like `createid()`, every call returns a new id.\n `resetstableidcounter()` — reset the `createstableid()` counter to 0. call in test `beforeeach` for deterministic ids. scoped to `createstableid()` only — `createid()` has no public reset (it's for uniqueness, not cross test determinism).\n\n## form associated api\n\nimport from `@vielzeug/ore`.\n\n### `usefield(options)`\n\nwire a form associated element to `elementinternals`. requires `formassociated: true` on the component definition. the `disabled` state tracking via `internals.states` (customstateset) is skipped with a dev warning if the api is unavailable in the current environment.\n\n```ts\ntype formfieldoptions<t> = {\n disabled?: readable<boolean>;\n /** defaults to the host element active during setup. */\n el?: htmlelement;\n /**\n * when true, a null/undefined value is submitted as '' instead of null,\n * keeping the field's key present in formdata even when the value is absent.\n * only applies to the default toformvalue; ignored if toformvalue is provided.\n * @default false\n */\n emptystringfornull?: boolean;\n /** called when the ancestor <form> resets (see onformreset) — restore local field state here. */\n onreset?: () => void;\n toformvalue?: (value: t) => file | formdata | string | null;\n /** recomputed reactively and passed straight to internals.setvalidity(). null = always valid. */\n validationmessage?: readable<string>;\n validity?: readable<validitystateflags | null>;\n value: signal<t> | readable<t>;\n};\n\ntype formfieldhandle = {\n checkvalidity(): boolean;\n readonly internals: elementinternals;\n reportvalidity(): boolean;\n /** set (non empty message) or clear (empty string) a custom validity error. */\n setcustomvalidity(message: string): void;\n};\n```\n\npass `validity`/`validationmessage` to make `required` style constraints participate in native constraint validation\nthrough `checkvalidity()` and `reportvalidity()`:\n\n```ts\nconst isblank = (v: string) => v.trim() === '';\n\nusefield({\n validationmessage: computed(() => (required.value && isblank(value.value) ? 'this field is required.' : '')),\n validity: computed(() => (required.value && isblank(value.value) ? { valuemissing: true } : null)),\n value,\n});\n```\n\n## testing apis\n\nimport from `@vielzeug/ore/testing`.\n\n| api | purpose |\n| | |\n| `mount(setup, options?)` | mount a component and return a test fixture |\n| `cleanup()` | remove all mounted elements and reset test state |\n| `install(aftereach, options?)` | register auto cleanup; pass `{ forminternals: true }` to also install the `elementinternals`/`formdata`/`<form>.reset()` jsdom polyfill (see below) |\n| `installforminternalspolyfill()` | installs the form internals polyfill directly (returns an `uninstall()` that restores every patched global). usually called via `install(aftereach, { forminternals: true })` |\n| `walkflattree(root, visit)` | walks the flat tree (expanding `<slot>` via `assignedelements()`) — for finding slotted content across a shadow boundary that `queryselectorall()` can't cross |\n| `flush(options?)` | drain reactive updates and animation frames |\n| `debugflush()` | run `flush()` with `console.debug` diagnostics |\n| `mock(tag, template?)` | register a no op stub custom element |\n| `renderhook(setup)` | run lifecycle hooks in isolation; overload accepts `propdefs` as first arg for typed props |\n| `resetorefortests()` | reset styles and id counters when mounting is managed manually |\n| `oretimeouterror` | error thrown when `flush()` cannot settle tracked ore work |\n\n> **test isolation:** `cleanup()` removes mounted elements and resets all cross test ore state (the stylesheet cache and id counters) via `resetorefortests()`. call it in `aftereach` (or use `install()`) to prevent state leaking between tests.\n\nimport `within`, named dispatchers such as `fireclick`, and waits such as `waituntil` or `waitforevent` from\n`@vielzeug/assay`.\n\n> **form associated component testing:** jsdom implements none of the `elementinternals` form association api — `install(aftereach, { forminternals: true })` polyfills `setformvalue`/`setvalidity`/`checkvalidity`/`reportvalidity`/`validationmessage`/`validity`/`states`, mixes `checkvalidity`/`reportvalidity`/`validity`/`validationmessage` onto the host element itself (real browsers do this for any `formassociated: true` element), makes `formdata` collect a form associated element's set value, and makes `<form>.reset()` invoke `formresetcallback()`. every patch is a guarded no op when its target already exists, and `installforminternalspolyfill()` returns an `uninstall()` that restores every patched global. the polyfill is opt in (`{ forminternals: true }`) because the patches are global — suites without form associated components shouldn't carry them. a downstream package (e.g. a component library built on `ore`) should rely on this instead of hand rolling its own copy.\n\n#### `fixture` interface\n\n```ts\ninterface fixture<t extends htmlelement = htmlelement> {\n [symbol.dispose](): void; // delegates to dispose() — enables `using` declarations\n element: t;\n readonly disposed: boolean; // true after dispose() has been called\n readonly shadow: shadowroot | null;\n get<e extends element>(selector: string): e;\n query<e extends element>(selector: string): e | null;\n queryall<e extends element>(selector: string): e[];\n getbytext<e extends element>(text: string, selector?: string): e;\n querybytext<e extends element>(text: string, selector?: string): e | null;\n queryallbytext<e extends element>(text: string, selector?: string): e[];\n getbytestid<e extends element>(testid: string): e;\n querybytestid<e extends element>(testid: string): e | null;\n queryallbytestid<e extends element>(testid: string): e[];\n attr(name: string, value: string | number | boolean): promise<void>;\n attrs(record: record<string, string | number | boolean>): promise<void>;\n flush(options?: flushoptions): promise<void>;\n act(fn: () => unknown): promise<void>;\n dispose(): void; // removes the component from the dom — idempotent\n}\n```\n\n#### `renderhook`\n\nuseful for testing composable lifecycle hooks (`onmounted`, `watcheffect`, `inject`, etc.) without a template. `onmounted`/`oncleanup`/`watcheffect`/... work exactly as inside a real `setup()`, since they resolve the same implicit current component context:\n\n```ts\n// without props\nconst { result, flush, dispose } = await renderhook(() => {\n const count = signal(0);\n onmounted(() => {\n count.value = 1;\n });\n return count;\n});\nexpect(result.value).tobe(1);\n\n// with typed props (prop defs overload)\nconst { result } = await renderhook({ label: prop.string('hello'), count: prop.number(0) }, (props) => props.label);\nexpect(result.value).tobe('hello');\n```\n\n## ripple primitives\n\nore does **not** re export reactive primitives. import them directly from `@vielzeug/ripple`:\n\n```ts\nimport { batch, computed, signal, watch } from '@vielzeug/ripple';\n```\n\nsee the [ripple documentation](/ripple/) for the full api.\n\n## lifecycle events\n\n| event | when |\n| | |\n| `ore:connect` | after every `connectedcallback` (including reconnects) |\n| `ore:disconnect` | after `disconnectedcallback`, before component state is reset |\n| `ore:error` | when a lifecycle callback fails — bubbles, composed; detail is `orelifecycleerror` |\n\n## types\n\n```ts\ntype propdef<t> = {\n readonly default: t;\n readonly parse: (value: string | null) => t;\n reflect?: boolean;\n};\n\ntype propsdef<t extends record<string, unknown>> = {\n [k in keyof required<t>]: propdef<t[k & keyof t]>;\n};\n\ntype propinputdefs = record<string, propdef<unknown>>;\n\n/**\n * infer reactive props type from a propinputdefs map.\n * each entry becomes readable<t> keyed by prop name.\n */\ntype inferprops<d extends propinputdefs> = {\n readonly [k in keyof d] ?: readable<inferpropvalue<d[k]>>;\n};\n\n// runtime hooks — all plain functions imported from '@vielzeug/ore', not fields on an object.\ntype onmountedcallback = () => cleanup | undefined;\ntype onformresetcallback = () => void;\n\ndeclare function onmounted(fn: onmountedcallback): void; // dom ready callback; runs after each connection's render\ndeclare function oncleanup(fn: cleanup): void; // register teardown; called on disconnect\ndeclare function onelement<t extends htmlelement>(\n ref: readable<t | null>,\n callback: (el: t) => cleanup | undefined,\n): () => void;\ndeclare function onevent<k extends keyof htmlelementeventmap>(\n target: eventtarget | null | undefined,\n event: k,\n listener: (e: htmlelementeventmap[k]) => void,\n options?: addeventlisteneroptions,\n): void;\ndeclare function onevent(\n target: eventtarget | null | undefined,\n event: string,\n listener: eventlistener,\n options?: addeventlisteneroptions,\n): void;\ndeclare function onformreset(fn: onformresetcallback): void; // runs on every ancestor <form> reset; formassociated only\ndeclare function watcheffect(fn: () => cleanup | undefined): () => void; // scoped reactive effect; auto cleaned on disconnect\ndeclare function bind(config: hostbindconfig, options?: bindoptions): () => void; // bindings for host or any target element\ndeclare function provide<t>(key: injectionkey<t>, value: t): void; // register a context value on the host element\ndeclare function inject<t>(key: injectionkey<t>): t | undefined;\ndeclare function inject<t>(key: injectionkey<t>, fallback: t): t;\ndeclare function gethost(): htmlelement; // the current component's host element\ndeclare function useemit<emits extends record<string, unknown> = record<string, never>>(): emitfn<emits>;\ndeclare function useslots<slotnames extends string = string>(): componentslots<slotnames>;\n\ntype componentdefinition<props extends record<string, unknown> = record<never, never>> = {\n formassociated?: boolean;\n props?: propsdef<props>;\n setup: (props: inferprops<propsdef<props>>) => htmlresult | null;\n shadow?: partial<shadowrootinit> | false; // false = light dom\n styles?: (string | cssstylesheet | cssresult)[];\n};\n\ntype hostbindingvalue =\n | (() => string | number | boolean | null | undefined)\n | readable<string | number | boolean | null | undefined>\n | string\n | number\n | boolean\n | null\n | undefined;\n\ntype reflectconfig = record<string, hostbindingvalue>;\n\ntype hostbindconfig = {\n aria?: reflectconfig;\n attr?: reflectconfig;\n class?: (() => record<string, boolean>) | record<string, readable<boolean> | (() => boolean) | boolean>;\n on?: record<string, ((event: event) => void) | undefined>;\n style?: record<string, hostbindingvalue>;\n};\n\ntype bindoptions = addeventlisteneroptions & {\n target?: element;\n};\n\ntype hostbindfn = (config: hostbindconfig, options?: bindoptions) => () => void;\n\ntype componentslots<s extends string = string> = {\n elements(name?: s): readable<element[]>;\n has(name?: s): readable<boolean>;\n};\n\ntype ref<t extends element> = signal<t | null>;\n\ntype refcallback<t extends element> = (el: t | null) => void;\n\ntype injectionkey<t> = symbol & { readonly __ore_injection_key?: t };\n\ninterface htmlresult {\n mount(\n parent: parentnode,\n anchor: node | null,\n registercleanup: (fn: () => void) => void,\n ): node[];\n}\n\ntype cssresult = {\n content: string;\n tostring(): string;\n};\n\ntype livebinding<t> = { readonly source: readable<t> };\n\ntype emitfn<t extends record<string, unknown>> = {\n <k extends keyswithoutdetail<t>>(event: k): boolean;\n <k extends exclude<keyof t, keyswithoutdetail<t>>>(event: k, detail: t[k]): boolean;\n};\n// keyswithoutdetail is an internal helper type, not exported.\n\ntype formfieldoptions<t = unknown> = {\n disabled?: readable<boolean>;\n el?: htmlelement;\n emptystringfornull?: boolean;\n onreset?: () => void;\n toformvalue?: (value: t) => file | formdata | string | null;\n validationmessage?: readable<string>;\n validity?: readable<validitystateflags | null>;\n value: signal<t> | readable<t>;\n};\n\ntype formfieldhandle = {\n checkvalidity: () => boolean;\n readonly internals: elementinternals;\n reportvalidity: () => boolean;\n setcustomvalidity: (message: string) => void;\n};\n\ntype mutationobservervalue = {\n entries: mutationrecord[];\n latest: mutationrecord | null;\n};\n\n/** phase in which a oreerror occurred. */\ntype oreerrorphase = 'each reconcile' | 'form reset' | 'mounted' | 'setup';\n```\n\n## errors\n\n`oreerror` is the base class for every ore error class — `err instanceof oreerror` catches all of them.\n`oreerror.is(err)` is the equivalent static type guard.\n\n **`oreapierror`** — thrown when the `ore` api itself is misused: calling `define()` with a duplicate tag, calling a lifecycle hook (`inject`, `onmounted`, `oncleanup`, `onevent`, …) outside of `setup()`, or passing an invalid prop definition to `define()`.\n **`oreinternalerror`** — thrown when an ore invariant fails, indicating a package bug rather than invalid application code.\n **`orelifecycleerror`** — reported in the `ore:error` event when component `setup()`, a mounted callback, a form reset callback, or `each()` reconciliation fails. extends `oreerror` with:\n `component: string` — the element's local name\n `phase: oreerrorphase` — `'setup'` | `'mounted'` | `'form reset'` | `'each reconcile'`\n `cause: error` — the original error thrown by `setup()`\n **`oretimeouterror`** — thrown by `flush()` (from `@vielzeug/ore/testing`) when pending ore work does not settle before its timeout.\n\nlifecycle failures dispatch a bubbling, composed `ore:error` event whose `detail` is the `orelifecycleerror`. setup\nfailures still rethrow their original error; mounted and form reset callback failures are reported through the same\nevent so their remaining callbacks can continue.\n",
|
|
898
|
+
"usage": " \ntitle: ore — usage guide\ndescription: practical ore usage patterns for components, props, templates, slots, context, forms, sentinel integration, and tests.\n \n\n[[toc]]\n\n## basic usage\n\n`define(tag, definition)` registers a custom element.\n\nyour `setup()` function receives typed prop signals and returns an `htmlresult` directly. its state belongs to the\ncurrent connection: disconnect disposes it, and reconnecting the same element runs setup again.\n\n```ts\nimport { signal } from '@vielzeug/ripple';\nimport { define, html } from '@vielzeug/ore';\n\ndefine('status chip', {\n setup() {\n const online = signal(true);\n\n return html`\n <button @click=${() => (online.value = !online.value)}>${() => (online.value ? 'online' : 'offline')}</button>\n `;\n },\n});\n```\n\neverything besides `props` — lifecycle hooks, host bindings, context, slots, emit — is a plain function imported from `@vielzeug/ore`, called directly from `setup()` (or a composable it calls):\n\n```ts\nimport { define, gethost, html, bind, useemit, useslots } from '@vielzeug/ore';\n\ndefine('my widget', {\n setup(_props) {\n const el = gethost(); // the host htmlelement\n const emit = useemit<{ close: undefined }>(); // typed event emitter\n const slots = useslots<'header'>(); // reactive slot observation\n\n bind({ attr: { role: 'group' } }); // host binding helper (attr, class, style, on)\n\n return html`<slot></slot>`;\n },\n});\n```\n\n## signals and effects\n\nore does not re export ripple primitives — import them directly from `@vielzeug/ripple`.\n\n```ts\nimport { batch, computed, effect, signal, watch } from '@vielzeug/ripple';\n\nconst count = signal(0);\nconst doubled = computed(() => count.value * 2);\n\neffect(() => {\n console.log('doubled =', doubled.value);\n});\n\nwatch(count, (next, prev) => {\n console.log('count changed', prev, ' >', next);\n});\n\nbatch(() => {\n count.value = 1;\n count.value = 2;\n});\n```\n\n## onmounted and lifecycle\n\nuse `onmounted()` for dom dependent initialization that must run after the template is mounted. use `onelement(ref, cb)` for work tied to a specific dom node. `onevent()` attaches a listener that is automatically removed on disconnect.\n\n```ts\nimport { signal } from '@vielzeug/ripple';\nimport { define, html, onelement, onevent, onmounted, ref, useslots } from '@vielzeug/ore';\n\ndefine('deferred init', {\n setup(_props) {\n const tabindex = signal(0);\n const inputref = ref<htmlinputelement>();\n const slots = useslots<'items'>();\n\n onmounted(() => {\n const items = slots.elements('items').value;\n console.log('found', items.length, 'items');\n });\n\n onelement(inputref, (input) => {\n input.focus();\n });\n\n onevent(window, 'keydown', (e: keyboardevent) => {\n if (e.key === 'escape') tabindex.value = 0;\n });\n\n return html`<div><slot name=\"items\"></slot><input ref=${inputref} /></div>`;\n },\n});\n```\n\n## prop definitions\n\nuse `prop.*` helpers for common cases, or raw `propdef` objects for custom parsing or `reflect: false`.\n\n```ts\nimport { define, html, prop } from '@vielzeug/ore';\n\ndefine('x button', {\n props: {\n label: prop.string('button'),\n disabled: prop.bool(false),\n variant: prop.oneof(['primary', 'secondary'] as const, 'primary'),\n count: prop.number(0),\n },\n setup(props) {\n return html`\n <button ?disabled=${props.disabled} data variant=${props.variant}>${props.label} (${props.count})</button>\n `;\n },\n});\n```\n\n## template bindings\n\n`html` supports text, attributes, booleans, properties, events, refs, and nested templates.\n\n```ts\nimport { computed, signal } from '@vielzeug/ripple';\nimport { define, html, ref } from '@vielzeug/ore';\n\ndefine('profile name', {\n setup() {\n const name = signal('alice');\n const inputref = ref<htmlinputelement>();\n\n return html`\n <label title=${computed(() => 'current: ' + name.value)}>name</label>\n <input\n ref=${inputref}\n value=${name}\n aria label=${() => 'current name ' + name.value}\n @input=${(event: event) => {\n name.value = (event.target as htmlinputelement).value;\n }} />\n <p>hello ${name}</p>\n `;\n },\n});\n```\n\n## directives\n\nore exports `each`, `classmap`, `stylemap`, `when`, `live`, and `unsafehtml` from `@vielzeug/ore`. use ordinary\nattribute bindings plus native event handlers for two way input state; no special model directive is required.\n\n```ts\nimport { signal } from '@vielzeug/ripple';\nimport { classmap, define, each, html, stylemap, when } from '@vielzeug/ore';\n\ndefine('task list', {\n setup() {\n const tasks = signal([{ id: 1, text: 'write tests' }]);\n const active = signal(true);\n\n return html`\n <ul\n class=\"${classmap({ ready: () => tasks.value.length > 0 })}\"\n style=${stylemap({ opacity: () => (active.value ? 1 : 0.5) })}>\n ${when(\n () => active.value,\n () => html`<li>active</li>`,\n () => html`<li>paused</li>`,\n )}\n ${each(\n tasks,\n (task) => task.id,\n (task) => html`<li>${() => task.value.text}</li>`,\n )}\n </ul>\n `;\n },\n});\n```\n\n### each() api\n\n`each(source, key, render, fallback?)` takes positional arguments:\n\n **source** — signal, getter, or plain array\n **key** — function returning a unique key per item\n **render** — receives reactive `item` and `index` signals\n **fallback** — optional, rendered when the list is empty\n\n```ts\neach(\n items,\n (item) => item.id,\n (item, index) => html`<li>#${index}: ${() => item.value.label}</li>`,\n () => html`<li>no items</li>`,\n);\n```\n\n## live form bindings\n\nuse `live(signal)` for inputs that should preserve in progress user edits instead of overwriting the dom on stale writes.\n\n```ts\nimport { signal } from '@vielzeug/ripple';\nimport { define, html, live } from '@vielzeug/ore';\n\ndefine('live search', {\n setup() {\n const query = signal('');\n\n return html`\n <input value=${live(query)} @input=${(e: event) => (query.value = (e.target as htmlinputelement).value)} />\n `;\n },\n});\n```\n\n## host bindings\n\n`bind()` wires reactive attrs, classes, styles, and events to the host element.\n\n```ts\nimport { signal } from '@vielzeug/ripple';\nimport { bind, define, html } from '@vielzeug/ore';\n\ndefine('x toggle', {\n setup(_props) {\n const open = signal(false);\n\n bind({\n attr: { 'aria expanded': () => string(open.value), role: 'button', tabindex: 0 },\n class: { 'is open': open },\n on: { click: () => (open.value = !open.value) },\n });\n\n return html`<slot></slot>`;\n },\n});\n```\n\nthe `bind` config supports `attr`, `class`, `style`, and `on` sections.\n\n## aria bindings\n\nuse `bind({ aria: config }, { target })` to reactively sync aria attributes to any element. shorthand keys are normalised to `aria *` automatically — `expanded` becomes `aria expanded`, `role` is set verbatim.\n\n```ts\nimport { signal } from '@vielzeug/ripple';\nimport { bind, define, html, onmounted } from '@vielzeug/ore';\n\ndefine('x disclosure', {\n setup(_props) {\n const open = signal(false);\n const panelid = 'disclosure panel';\n\n bind({\n attr: { role: 'button', tabindex: 0 },\n on: { click: () => (open.value = !open.value) },\n });\n\n onmounted(() => {\n const trigger = document.queryselector('#trigger') as htmlelement;\n if (trigger) {\n // bind() registers cleanup automatically when called inside setup\n bind(\n {\n aria: {\n controls: panelid,\n expanded: () => string(open.value),\n haspopup: 'region',\n },\n },\n { target: trigger },\n );\n }\n });\n\n return html`<slot></slot>`;\n },\n});\n```\n\nstatic values are applied once. getter functions create reactive effects. setting a value to `null`, `undefined`, or `false` removes the attribute.\n\n`bind()` always returns a cleanup function. use it to stop syncing early when a trigger element can be swapped out:\n\n```ts\nonmounted(() => {\n const trigger = document.queryselector('#trigger') as htmlelement;\n const stoparia = bind({ aria: { expanded: () => string(open.value) } }, { target: trigger });\n\n // stop syncing when the trigger is replaced\n oncleanup(stoparia);\n});\n```\n\n### binding a non host element with `bind()`\n\npass `{ target: el }` as a second argument to bind attributes, classes, styles, or events to any element:\n\n```ts\nimport { signal } from '@vielzeug/ripple';\nimport { bind, define, html, onmounted, ref } from '@vielzeug/ore';\n\ndefine('button wrapper', {\n setup(_props) {\n const visible = signal(false);\n const btnref = ref<htmlbuttonelement>();\n\n onmounted(() => {\n const btn = btnref.value;\n if (!btn) return;\n\n bind(\n {\n attr: { 'aria pressed': () => string(visible.value) },\n on: { click: () => (visible.value = !visible.value) },\n },\n { target: btn },\n );\n });\n\n return html`<button ref=${btnref}>toggle</button>`;\n },\n});\n```\n\n## slots and emits\n\n```ts\nimport { define, html, useemit, useslots, when } from '@vielzeug/ore';\n\ndefine('card with footer', {\n setup(_props) {\n const slots = useslots<'header' | 'footer'>();\n const emit = useemit<{ action: undefined }>();\n\n return html`\n <div class=\"card\">\n <slot name=\"header\"></slot>\n <slot></slot>\n ${when(slots.has('footer'), () => html`<footer><slot name=\"footer\"></slot></footer>`)}\n </div>\n <button @click=${() => emit('action')}>go</button>\n `;\n },\n});\n```\n\npass a `slotnames` type parameter to `useslots<slotnames>()` to get typed `slots.has()` and `slots.elements()` calls.\n\n## context provide/inject\n\n```ts\nimport { signal } from '@vielzeug/ripple';\nimport { createcontext, define, html, injectstrict, provide } from '@vielzeug/ore';\n\nconst count_ctx = createcontext<returntype<typeof signal<number>>>('count');\n\ndefine('count provider', {\n setup(_props) {\n const count = signal(0);\n provide(count_ctx, count);\n\n return html`<button @click=${() => count.value++}><slot></slot></button>`;\n },\n});\n\ndefine('count consumer', {\n setup() {\n const count = injectstrict(count_ctx);\n\n return html`<p>count: ${count}</p>`;\n },\n});\n```\n\n`provide()` registers cleanup automatically — context keys are removed from the registry when the providing component disconnects. on reconnect, `setup()` runs fresh and `provide()` re registers without spurious \"overwriting\" warnings. provide a `readable` (signal/computed) rather than a raw value if descendants need to observe later changes — `inject()` resolves and caches the value once per consumer connection.\n\n## form associated elements\n\n```ts\nimport { signal } from '@vielzeug/ripple';\nimport { define, html, prop } from '@vielzeug/ore';\nimport { usefield } from '@vielzeug/ore';\n\ndefine('rating input', {\n formassociated: true,\n setup() {\n const value = signal(0);\n const field = usefield({ value });\n\n return html`\n <button @click=${() => (value.value = 1)}>1</button>\n <button @click=${() => (value.value = 2)}>2</button>\n <button @click=${() => (value.value = 3)}>3</button>\n <button @click=${() => field.reportvalidity()}>validate</button>\n <p>current: ${value}</p>\n `;\n },\n});\n```\n\n## sentinel observers\n\nuse `@vielzeug/sentinel` for reactive browser and dom observations. create element dependent sentinels inside `onmounted()` and dispose them with the component.\n\n```ts\nimport { define, html, oncleanup, onmounted, ref, watcheffect } from '@vielzeug/ore';\nimport { createelementsize, sentinelunavailableerror } from '@vielzeug/sentinel';\n\ndefine('x observed', {\n setup(_props) {\n const boxref = ref<htmldivelement>();\n\n onmounted(() => {\n const element = boxref.value;\n if (!element) return;\n\n try {\n const size = createelementsize(element);\n\n watcheffect(() => {\n console.log(size.value?.width);\n });\n\n oncleanup(() => size.dispose());\n } catch (error) {\n if (!(error instanceof sentinelunavailableerror)) throw error;\n }\n });\n\n return html`<div ref=${boxref}>observe me</div>`;\n },\n});\n```\n\n## testing utilities\n\nimport from `@vielzeug/ore/testing`.\n\n```ts\nimport { aftereach, describe, expect, it } from 'vitest';\nimport { signal } from '@vielzeug/ripple';\nimport { fireclick } from '@vielzeug/assay';\nimport { html } from '@vielzeug/ore';\nimport { cleanup, mount } from '@vielzeug/ore/testing';\n\ndescribe('my counter', () => {\n aftereach(cleanup);\n\n it('increments on click', async () => {\n let count!: returntype<typeof signal<number>>;\n const { query, act } = await mount(() => {\n count = signal(0);\n return html`<button @click=${() => count.value++}>${count}</button>`;\n });\n\n expect(query('button')?.textcontent).tobe('0');\n\n await act(() => fireclick(query('button')!));\n\n expect(query('button')?.textcontent).tobe('1');\n });\n});\n```\n\n## framework integration\n\nore components are standard custom elements and work natively in any framework.\n\n::: code group\n\n```tsx [react]\n// react 19+ supports custom elements natively.\nimport './x toggle'; // wherever define('x toggle', { ... }) is called\n\nfunction app() {\n return <x toggle aria label=\"open menu\" />;\n}\n```\n\n```ts [vue 3]\n<script setup lang=\"ts\">\nimport './x toggle'; // wherever define('x toggle', { ... }) is called\nimport { ref } from 'vue';\n\nconst open = ref(false);\n</script>\n\n<template>\n <x toggle :aria label=\"'open menu'\" @click=\"open = !open\" />\n</template>\n```\n\n```svelte [svelte]\n<script>\n import './x toggle'; // wherever define('x toggle', { ... }) is called\n\n function handleclick() {\n console.log('toggled');\n }\n</script>\n\n<x toggle aria label=\"open menu\" on:click={handleclick} />\n```\n\n:::\n\n## working with other vielzeug libraries\n\n### with ripple\n\nimport ripple primitives directly from `@vielzeug/ripple` for standalone reactive state outside components.\n\n```ts\nimport { signal, computed } from '@vielzeug/ripple';\nimport { define, html } from '@vielzeug/ore';\n\n// shared state created outside any component\nconst theme = signal<'light' | 'dark'>('light');\nconst isdark = computed(() => theme.value === 'dark');\n\ndefine('theme toggle', {\n setup() {\n return html`\n <button @click=${() => (theme.value = isdark.value ? 'light' : 'dark')}>\n ${() =>\n isdark.value ? '<ore icon name=\"sun\" size=\"16\"></ore icon>' : '<ore icon name=\"moon\" size=\"16\"></ore icon>'}\n </button>\n `;\n },\n});\n```\n\n### with forge\n\nuse `@vielzeug/forge` for typed form state. `usefield()` remains intentionally narrow: it connects a form associated\ncustom element to native `elementinternals` without imposing submission, validation, or dirty state policy.\n\n```ts\nimport { createform } from '@vielzeug/forge';\nimport { define, html } from '@vielzeug/ore';\n\ndefine('signup form', {\n setup(_props) {\n const form = createform({ initialvalues: { email: '' } });\n\n return html`\n <form\n @submit=${(event: submitevent) => {\n event.preventdefault();\n void form.submit(async (values) => {\n console.log(values);\n });\n }}>\n <slot></slot>\n </form>\n `;\n },\n});\n```\n\n## best practices\n\n setup returns `html\\`...\\`` directly — not a function wrapping the template.\n use `watcheffect()` for reactive subscriptions tied to component lifetime — it auto registers cleanup on disconnect.\n use `onelement(ref, cb)` instead of `onmounted` when the work is tied to a single dom node.\n bind host attributes and classes via `bind()` rather than mutating the element directly.\n provide context at the nearest ancestor — avoid global context singletons.\n call `oncleanup()` for every resource allocated in `setup()` (websockets, intervals, external subscriptions).\n use `live(signal)` for form inputs to prevent clobbering user in progress edits.\n extract composable helper functions freely — `onmounted`/`oncleanup`/`bind`/... resolve the active component through implicit context, so they work from any function called (transitively) during `setup()`, with no need to pass them in as parameters.\n test component mounting and lifecycle with `@vielzeug/ore/testing`; import generic dom events, queries, and waits\n from `@vielzeug/assay`.\n",
|
|
797
899
|
"examples": " \ntitle: ore — examples\ndescription: practical examples and recipes for ore.\n \n\n## examples\n\n [counter component](./examples/counter component.md)\n [typed props and emits](./examples/typed props and emits.md)\n [observers in onmounted()](./examples/observers in onmount.md)\n [search list with directives](./examples/search list with directives.md)\n [context provider and consumer](./examples/context provider and consumer.md)\n [prop helpers and raw propdef](./examples/propsof builder api.md)\n [form associated rating input](./examples/form associated rating input.md)\n [test example with @vielzeug/ore/testing](./examples/test example at vielzeug ore testing.md)\n"
|
|
798
900
|
},
|
|
799
901
|
"examples": [],
|
|
800
|
-
"exports": "define prop html css ref createcontext inject injectstrict provide onmounted oncleanup onevent onelement onformreset watcheffect useemit useslots gethost bind each when classmap stylemap live unsafehtml usefield
|
|
902
|
+
"exports": "define prop html css ref createcontext inject injectstrict provide onmounted oncleanup onevent onelement onformreset watcheffect useemit useslots gethost bind each when classmap stylemap live unsafehtml usefield createid createstableid resetstableidcounter oreerror oreapierror oreinternalerror orelifecycleerror bindoptions",
|
|
801
903
|
"keywords": "web components custom elements reactive templates signals lifecycle",
|
|
802
904
|
"name": "@vielzeug/ore",
|
|
803
905
|
"related": "ripple refine orbit",
|
|
804
906
|
"slug": "ore",
|
|
805
|
-
"source": "export type { componentdefinition } from './component types';\nexport { createcontext, type injectionkey, inject, injectstrict, provide } from './context';\nexport { define, prop } from './define';\n// near universal template directives — used in most non trivial components (lists,\n// conditionals, and class/style maps. kept in the main entry alongside\n// `html`/`define` rather than a separate sub path: tree shaking already means an unused export\n// costs nothing in a bundled consumer, so splitting these off only adds an extra import line\n// for functionality most components need on day one. `unsafehtml()` and `live()` remain here\n// too: their explicit names make their specialized behavior clear without a second import path.\nexport { classmap } from './directives/classmap';\nexport { each } from './directives/each';\nexport { type livebinding, live } from './directives/live';\nexport { stylemap } from './directives/stylemap';\nexport { unsafehtml } from './directives/unsafe html';\nexport { when } from './directives/when';\nexport { oreapierror, oreerror, type oreerrorphase, oreinternalerror, orelifecycleerror } from './errors';\nexport { type formfieldhandle, type formfieldoptions, usefield } from './forms/field';\nexport {\n type bindoptions,\n bind,\n type hostbindconfig,\n type hostbindfn,\n type hostbindingvalue,\n type reflectconfig,\n} from './host bind';\nexport
|
|
907
|
+
"source": "export type { componentdefinition } from './component types';\nexport { createcontext, type injectionkey, inject, injectstrict, provide } from './context';\nexport { define, prop } from './define';\n// near universal template directives — used in most non trivial components (lists,\n// conditionals, and class/style maps. kept in the main entry alongside\n// `html`/`define` rather than a separate sub path: tree shaking already means an unused export\n// costs nothing in a bundled consumer, so splitting these off only adds an extra import line\n// for functionality most components need on day one. `unsafehtml()` and `live()` remain here\n// too: their explicit names make their specialized behavior clear without a second import path.\nexport { classmap } from './directives/classmap';\nexport { each } from './directives/each';\nexport { type livebinding, live } from './directives/live';\nexport { stylemap } from './directives/stylemap';\nexport { unsafehtml } from './directives/unsafe html';\nexport { when } from './directives/when';\nexport { oreapierror, oreerror, type oreerrorphase, oreinternalerror, orelifecycleerror } from './errors';\nexport { type formfieldhandle, type formfieldoptions, usefield } from './forms/field';\nexport {\n type bindoptions,\n bind,\n type hostbindconfig,\n type hostbindfn,\n type hostbindingvalue,\n type reflectconfig,\n} from './host bind';\nexport type { inferprops, propdef, propinputdefs, propsdef } from './props';\n// lifecycle hooks — plain functions, called during setup() or a composable it invokes.\nexport {\n gethost,\n type onformresetcallback,\n type onmountedcallback,\n oncleanup,\n onelement,\n onevent,\n onformreset,\n onmounted,\n watcheffect,\n} from './runtime';\nexport { type componentslots, useslots } from './slots';\nexport { html } from './template/instantiator';\nexport { type htmlresult, type ref, type refcallback, ref } from './template/result';\nexport { type cssresult, css } from './utils/css';\nexport { type emitfn, useemit } from './utils/emit';\n\nexport { createid, createstableid, resetstableidcounter } from './utils/id';\n"
|
|
806
908
|
},
|
|
807
909
|
{
|
|
808
910
|
"category": "ui",
|
|
@@ -881,7 +983,7 @@
|
|
|
881
983
|
"docs": {
|
|
882
984
|
"index": " \ntitle: ripple — reactive graphs\ndescription: framework agnostic signals, derived values, effects, scopes, watchers, and async resources.\npackage: ripple\ncategory: state\nkeywords: [reactive, signals, computed, effects, graph, scope, batch, watch, resource, async]\nrelated: [ore, clockwork, ledger]\nexports: [createripple, signal, computed, effect, batch, createscope, untrack, watch, resource, isreactive]\nenvironments: [browser, node, ssr, deno]\n \n\n<! markdownlint disable md025 md033 md060 >\n\n<packagehero package=\"ripple\" />\n\n## why ripple?\n\nhand rolled reactive state spreads subscription, cleanup, and derived value rules across application code. ripple gives you one graph boundary with explicit disposal and fine grained dependencies while keeping rendering and routing outside the runtime.\n\n```ts\n// before\nlet count = 0;\nconst listeners = new set<() => void>();\n\nfunction setcount(next: number) {\n count = next;\n for (const listener of listeners) listener();\n}\n\n// after\nimport { createripple } from '@vielzeug/ripple';\n\nconst ripple = createripple();\nconst count = ripple.signal(0);\nconst doubled = ripple.computed(() => count.value * 2);\nconst stop = ripple.effect(() => console.log(doubled.value));\n\ncount.value = 1;\nstop.dispose();\nripple.dispose();\n```\n\n| feature | ripple | zustand | jotai |\n| | | | |\n| bundle size | <packageinfo package=\"ripple\" type=\"size\" /> | ~3.5 kb | ~7 kb |\n| zero dependencies | <ore icon name=\"check\" size=\"16\"></ore icon> | <ore icon name=\"x\" size=\"16\"></ore icon> | <ore icon name=\"x\" size=\"16\"></ore icon> |\n| framework agnostic | <ore icon name=\"check\" size=\"16\"></ore icon> | <ore icon name=\"check\" size=\"16\"></ore icon> | react first |\n| explicit graph lifetime | <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| fine grained derived values | <ore icon name=\"check\" size=\"16\"></ore icon> | selectors | atoms |\n\n<div class=\"decision callout\">\n\n**use ripple when** you need framework independent state with explicit graph lifetime and small composable primitives.\n\n**consider a framework store when** component bindings, server cache, or framework specific tooling matter more than portable reactive state.\n\n</div>\n\n## installation\n\n::: code group\n\n```sh [pnpm]\npnpm add @vielzeug/ripple\n```\n\n```sh [npm]\nnpm install @vielzeug/ripple\n```\n\n```sh [yarn]\nyarn add @vielzeug/ripple\n```\n\n:::\n\n## quick start\n\ncreate one graph, derive a value, observe it, then dispose resources when the graph lifetime ends.\n\n```ts\nimport { createripple } from '@vielzeug/ripple';\n\nconst ripple = createripple();\nconst count = ripple.signal(0);\nconst doubled = ripple.computed(() => count.value * 2);\nconst stop = ripple.effect(() => console.log(doubled.value));\n\nripple.batch(() => {\n count.value = 1;\n count.value = 2;\n});\n\nstop.dispose();\nripple.dispose();\n```\n\n## features\n\n<div class=\"features grid\">\n\n `createripple()` creates an isolated graph and lifetime boundary.\n `signal()` stores writable values with configurable equality.\n `computed()` derives lazy read only values.\n `effect()` reacts to dependency changes with cleanup support.\n `batch()` coalesces synchronous writes and notifications.\n `createscope()` groups owned reactive work.\n `watch()` observes one selected source transition.\n `resource()` loads async values with stale work cancellation.\n\n</div>\n\n## documentation\n\n<div class=\"doc links\">\n\n [usage guide](./usage.md)\n [api reference](./api.md)\n [examples](./examples.md)\n [migration guide](./migration.md)\n\n</div>\n\n## see also\n\n<div class=\"see also\">\n\n [ore](/ore/) — uses ripple signals and effects for web component reactivity.\n [clockwork](/clockwork/) — exposes machine state through reactive ripple values.\n [ledger](/ledger/) — adds command based undo and redo beside ripple state.\n\n</div>\n\n<! markdownlint enable >\n",
|
|
883
985
|
"api": " \ntitle: ripple — api reference\ndescription: complete reference for reactive graphs, signals, effects, scopes, watchers, and resources.\n \n\n[[toc]]\n\n## api overview\n\n| symbol | purpose | execution mode | common gotcha |\n| | | | |\n| `createripple()` | create isolated graph | sync | disposal is terminal; create a new graph instead of reusing it |\n| `signal()` | create writable value | sync | default graph is process wide |\n| `computed()` | create lazy derived value | sync | keep derivation pure |\n| `effect()` | react to dependency reads | sync | dispose handle or return cleanup |\n| `batch()` | coalesce synchronous writes | sync | does not roll back writes |\n| `createscope()` | group owned reactive work | sync | call `run()` to activate it |\n| `untrack()` | read without tracking | sync | read still happens immediately |\n| `watch()` | observe selected output | sync | use `effect()` for broad reads |\n| `resource()` | load async source | async | read dependencies in source callback |\n| `isreactive()` | test `readable` identity | sync | does not test arbitrary objects |\n\n## package entry point\n\n| import | purpose |\n| | |\n| `@vielzeug/ripple` | all primitives, types, and errors — signals, computed, effects, scopes, watch, resource, and the isolated graph factory |\n\n## graph creation\n\n### `createripple(options?)`\n\n```ts\nfunction createripple(options?: rippleoptions): ripple;\n```\n\ncreates one isolated reactive graph. factories on the returned object share scheduling, ownership, observer, and error boundaries. `dispose()` is terminal: `ripple.disposed` becomes `true`, existing owned work is disposed, and creating more graph work throws `rippledisposedruntimeerror`. create a new graph for a new lifetime.\n\n| parameter | type | description |\n| | | |\n| `options.onerror` | `(error, context) => void` | receives effect, cleanup, listener, or observer failures. |\n| `options.observer` | `reactiveobserver` | receives graph events. |\n\n**returns:** `ripple`.\n\n**example:**\n\n```ts\nimport { createripple } from '@vielzeug/ripple';\n\nconst ripple = createripple();\nconst count = ripple.signal(0);\nconst stop = ripple.effect(() => console.log(count.value));\n\nstop.dispose();\nripple.dispose();\n```\n\n \n\n### `isreactive(value)`\n\n```ts\nfunction isreactive<t>(value: t | readable<t>): value is readable<t>;\n```\n\ntests whether a value is a ripple created readable node, including `resource`. recognition works across duplicated ripple module graphs.\n\n**returns:** `true` for a ripple `signal`, computed value, or `resource`; otherwise `false`.\n\n**example:**\n\n```ts\nimport { isreactive, signal } from '@vielzeug/ripple';\n\nconsole.log(isreactive(signal(0)));\n```\n\n## default graph functions\n\n### `signal(initial, options?)`\n\n```ts\nfunction signal<t>(initial: t, options?: signaloptions<t>): signal<t>;\n```\n\ncreates writable state on the default graph. use `update()` for immutable replacement patterns.\n\n**returns:** `signal<t>`.\n\n**example:**\n\n```ts\nimport { signal } from '@vielzeug/ripple';\n\nconst count = signal(0);\ncount.value += 1;\n\nconst cart = signal({ items: 0 });\ncart.update((state) => ({ ...state, items: state.items + 1 }));\n```\n\n \n\n### `computed(derive, options?)`\n\n```ts\nfunction computed<t>(derive: () => t, options?: computedoptions<t>): readable<t>;\n```\n\ncreates a lazy read only value from reactive reads in `derive`.\n\n**returns:** `readable<t>`.\n\n**example:**\n\n```ts\nimport { computed, signal } from '@vielzeug/ripple';\n\nconst count = signal(2);\nconst doubled = computed(() => count.value * 2);\nconsole.log(doubled.value);\n```\n\n \n\n### `effect(callback, options?)`\n\n```ts\nfunction effect(callback: () => cleanup | undefined, options?: effectoptions): effecthandle;\n```\n\nruns immediately and reruns when its tracked reads change. a returned cleanup runs before the next callback or disposal.\n\n**returns:** `effecthandle`.\n\n**example:**\n\n```ts\nimport { effect, signal } from '@vielzeug/ripple';\n\nconst connected = signal(false);\nconst stop = effect(() => {\n if (!connected.value) return;\n\n return () => console.log('disconnect');\n});\n\nstop.dispose();\n```\n\n \n\n### `batch(fn)` and `untrack(fn)`\n\n```ts\nfunction batch<t>(fn: () => t): t;\nfunction untrack<t>(fn: () => t): t;\n```\n\n`batch()` defers effects and listeners until its callback returns. `untrack()` reads current state without adding dependencies to an enclosing effect.\n\n**returns:** the callback result.\n\n**example:**\n\n```ts\nimport { batch, signal, untrack } from '@vielzeug/ripple';\n\nconst first = signal('ada');\nconst last = signal('lovelace');\nconst locale = signal('en us');\n\nbatch(() => {\n first.value = 'grace';\n last.value = 'hopper';\n});\n\nconsole.log(untrack(() => locale.value));\n```\n\n \n\n### `createscope(name?)`\n\n```ts\nfunction createscope(name?: string): scope;\n```\n\ncreates a disposable ownership boundary. work created inside `scope.run()` belongs to that scope.\n\n**returns:** `scope`.\n\n**example:**\n\n```ts\nimport { createscope, effect, signal } from '@vielzeug/ripple';\n\nconst scope = createscope('panel');\nconst count = signal(0);\n\nscope.run(() => effect(() => console.log(count.value)));\nscope.dispose();\n```\n\n## watch and resources\n\n### `watch(source, callback, options?)`\n\n```ts\nfunction watch<t>(\n source: readable<t> | (() => t),\n callback: (value: t, previous: t | undefined) => void,\n options?: watchoptions<t>,\n): effecthandle;\n```\n\nobserves selected output changes using the default graph or a `ripple.watch()` method.\n\n**returns:** `effecthandle`.\n\n**example:**\n\n```ts\nimport { signal, watch } from '@vielzeug/ripple';\n\nconst count = signal(0);\nconst stop = watch(count, (value, previous) => console.log(previous, value), { immediate: true });\nstop.dispose();\n```\n\n \n\n### `resource(source, loader, options?)`\n\n```ts\nfunction resource<source, value>(\n source: () => source,\n loader: (source: source, context: { readonly signal: abortsignal }) => promise<value>,\n options?: resourceoptions,\n): resource<value>;\n```\n\ntracks `source`, aborts stale loader work, and exposes `asyncstate<value>`. source and loader failures become `status: 'error'` state; handle them from `resource.value` rather than `rippleoptions.onerror`, which is reserved for runtime callback, cleanup, listener, and observer failures.\n\n**returns:** `resource<value>`.\n\n**example:**\n\n```ts\nimport { resource, signal } from '@vielzeug/ripple';\n\nconst userid = signal('42');\nconst user = resource(() => userid.value, async (id) => ({ id }));\n\nif (user.value.status === 'error') console.error(user.value.error);\nuser.dispose();\n```\n\n## types\n\n```ts\ntype cleanup = () => void;\ntype equality<t> = (previous: t, next: t) => boolean;\ntype unsubscribe = () => void;\n\ntype signaloptions<t> = { equals?: equality<t>; name?: string };\ntype computedoptions<t> = { equals?: equality<t>; name?: string };\ntype effectoptions = { name?: string; scheduler?: 'microtask' | 'sync' };\ntype watchoptions<t> = { equals?: equality<t>; immediate?: boolean; name?: string; once?: boolean };\ntype resourceoptions = { name?: string };\n\ntype reactiveevent =\n | { readonly kind: 'compute'; readonly name?: string }\n | { readonly kind: 'effect'; readonly name?: string }\n | { readonly kind: 'write'; readonly name?: string; readonly next: unknown; readonly previous: unknown }\n | { readonly kind: 'dispose'; readonly name?: string; readonly node: 'effect' | 'scope' };\n\ntype reactiveobserver = (event: reactiveevent) => void;\ntype reactiveerrorcontext = { readonly kind: 'cleanup' | 'effect' | 'listener' | 'observer'; readonly name?: string };\ntype rippleoptions = { observer?: reactiveobserver; onerror?: (error: unknown, context: reactiveerrorcontext) => void };\n\ntype asyncstate<t> =\n | { readonly previous?: t; readonly status: 'pending' }\n | { readonly status: 'success'; readonly value: t }\n | { readonly error: unknown; readonly previous?: t; readonly status: 'error' };\n\ninterface readable<t> {\n readonly name?: string;\n peek(): t;\n subscribe(listener: () => void): unsubscribe;\n readonly value: t;\n}\n\ninterface signal<t> extends readable<t> { update(updater: (prev: t) => t): void; value: t }\ninterface disposable { dispose(): void; readonly disposed: boolean; readonly disposalsignal: abortsignal; [symbol.dispose](): void }\ntype effecthandle = disposable;\ninterface scope extends disposable { run<t>(fn: () => t): t }\n\ninterface resource<t> extends readable<asyncstate<t>>, disposable { reload(): void }\n\ninterface ripple {\n batch<t>(fn: () => t): t;\n computed<t>(derive: () => t, options?: computedoptions<t>): readable<t>;\n createscope(name?: string): scope;\n dispose(): void;\n readonly disposed: boolean;\n effect(callback: () => cleanup | undefined, options?: effectoptions): effecthandle;\n resource<source, value>(source: () => source, loader: (source: source, context: { readonly signal: abortsignal }) => promise<value>, options?: resourceoptions): resource<value>;\n signal<t>(initial: t, options?: signaloptions<t>): signal<t>;\n untrack<t>(fn: () => t): t;\n watch<t>(source: readable<t> | (() => t), callback: (value: t, previous: t | undefined) => void, options?: watchoptions<t>): effecthandle;\n}\n```\n\n## errors\n\n| error | trigger | notable properties |\n| | | |\n| `rippleerror` | base ripple error | use `instanceof rippleerror` to narrow unknown values. |\n| `ripplecomputedcycleerror` | computed dependency reads itself through a cycle | extends `rippleerror`. |\n| `rippledisposedruntimeerror` | factory or execution api used after `ripple.dispose()` | extends `rippleerror`. |\n| `rippledisposedscopeerror` | `scope.run()` after scope disposal | extends `rippleerror`. |\n| `rippleinfinitelooperror` | effect flush exceeds graph iteration limit | extends `rippleerror`. |\n",
|
|
884
|
-
"usage": " \ntitle: ripple — usage guide\ndescription: build reactive state with one explicit graph boundary.\n \n\n[[toc]]\n\n## basic usage\n\nuse top level functions when one application lifetime graph is sufficient. read a signal inside an effect to make that read reactive.\n\n```ts\nimport { computed, effect, signal } from '@vielzeug/ripple';\n\nconst count = signal(0);\nconst label = computed(() => `count: ${count.value}`);\nconst stop = effect(() => console.log(label.value));\n\ncount.value = 1;\nstop.dispose();\n```\n\n## isolated graphs\n\nuse `createripple()` for tests, ssr requests, embedded applications, or independently disposable features. never mix reactive values from separate graphs.\n\n```ts\nimport { createripple } from '@vielzeug/ripple';\n\nconst ripple = createripple({\n onerror(error, context) {\n console.log(context.kind, error);\n },\n});\n\nconst count = ripple.signal(0);\nconst stop = ripple.effect(() => console.log(count.value));\n\nstop.dispose();\nripple.dispose();\n```\n\n## derived values and batches\n\nuse `computed()` for pure derivation. use `untrack()` when a current read must not become an effect dependency. use `batch()` for related synchronous writes.\n\n```ts\nconst first = ripple.signal('ada');\nconst last = ripple.signal('lovelace');\nconst locale = ripple.signal('en us');\nconst name = ripple.computed(() => `${first.value} ${last.value}`);\n\nripple.effect(() => {\n console.log({ locale: ripple.untrack(() => locale.value), name: name.value });\n});\n\nripple.batch(() => {\n first.value = 'grace';\n last.value = 'hopper';\n});\n```\n\n## scheduling and subscriptions\n\nripple propagates every synchronous write before flushing effects. each flush pass runs effects queued at its\nstart before direct `subscribe()` listeners queued at its start. work queued by either runs in a later pass.\neffects using `scheduler: 'microtask'` join a later microtask and coalesce writes made before that task runs.\n\n```ts\nconst count = ripple.signal(0);\nconst log: string[] = [];\n\nripple.effect(() => log.push(`effect: ${count.value}`));\ncount.subscribe(() => log.push(`listener: ${count.value}`));\nripple.effect(() => log.push(`deferred: ${count.value}`), { scheduler: 'microtask' });\n\nlog.length = 0; // ignore synchronous creation runs.\ncount.value = 1;\nconsole.log(log); // ['effect: 1', 'listener: 1']\n\nawait promise.resolve();\nconsole.log(log); // ['effect: 1', 'listener: 1', 'deferred: 1']\n```\n\n## ownership with scopes\n\ncreate a scope when a group of effects or derived values shares one lifetime. dispose the scope when its feature ends.\n\n```ts\nconst scope = ripple.createscope('panel');\nconst count = ripple.signal(0);\n\nscope.run(() => {\n ripple.effect(() => console.log(`panel count: ${count.value}`));\n});\n\ncount.value = 1;\nscope.dispose();\n```\n\n## watch selected values\n\nuse `watch()` for one selected output. use `effect()` when every reactive read in the callback should be a dependency.\n\n```ts\nconst stopwatch = ripple.watch(\n () => `${first.value} ${last.value}`,\n (value, previous) => console.log({ previous, value }),\n { immediate: true },\n);\n\nstopwatch.dispose();\n```\n\n## async data\n\n`resource()` captures source dependencies synchronously and passes a cancellation signal to the loader.\n\n```ts\nconst userid = ripple.signal('42');\nconst user = ripple.resource(\n () => userid.value,\n async (id, { signal }) => {\n const response = await fetch(`/users/${id}`, { signal });\n if (!response.ok) throw new error(`request failed: ${response.status}`);\n\n return response.json() as promise<{ id: string; name: string }>;\n },\n);\n\nif (user.value.status === 'success') console.log(user.value.value.name);\nif (user.value.status === 'error') console.error(user.value.error);\nuser.dispose();\n```\n\n## object state\n\n`signal()` with `update()` holds one value and supports immutable replacement patterns. return replacement objects from `update()` when object consumers depend on immutable updates.\n\n```ts\nconst cart = ripple.signal({ items: 0, label: 'empty' });\nconst items = ripple.computed(() => cart.value.items);\n\ncart.update((state) => ({ ...state, items: state.items + 1 }));\ncart.value = { items: 3, label: 'ready' };\n\nconsole.log(items.value);\n```\n\n## testing\n\ncreate an isolated graph per test. disposal prevents effects and resource work from leaking into later tests.\n\n```ts\nimport { expect, test } from 'vitest';\nimport { createripple } from '@vielzeug/ripple';\n\ntest('derives a doubled count', () => {\n const ripple = createripple();\n const count = ripple.signal(2);\n const doubled = ripple.computed(() => count.value * 2);\n\n expect(doubled.value).tobe(4);\n ripple.dispose();\n});\n```\n\n## framework integration\n\nuse signals and effects with any renderer. dispose component owned effects when the component unmounts.\n\n::: code group\n\n```ts [react]\nimport { useeffect, usestate } from 'react';\nimport { createripple } from '@vielzeug/ripple';\n\nconst ripple = createripple();\nconst count = ripple.signal(0);\n\nexport function counter() {\n const [, rerender] = usestate(0);\n\n useeffect(() => {\n const stop = ripple.effect(() => {\n void count.value;\n rerender((revision) => revision + 1);\n });\n\n return () => stop.dispose();\n }, []);\n\n return <button onclick={() => (count.value += 1)}>{count.value}</button>;\n}\n```\n\n```ts [vue 3]\nimport { onunmounted, ref } from 'vue';\nimport { createripple } from '@vielzeug/ripple';\n\nconst ripple = createripple();\nconst count = ripple.signal(0);\nconst revision = ref(0);\nconst stop = ripple.effect(() => {\n void count.value;\n revision.value++;\n});\n\nonunmounted(() => stop.dispose());\n```\n\n```ts [svelte]\n<script lang=\"ts\">\n import { ondestroy } from 'svelte';\n import { createripple } from '@vielzeug/ripple';\n\n const ripple = createripple();\n const count = ripple.signal(0);\n let revision = 0;\n const stop = ripple.effect(() => {\n void count.value;\n revision++;\n });\n\n ondestroy(() => stop.dispose());\n</script>\n\n<button on:click={() => (count.value += 1)}>{count.value}</button>\n```\n\n:::\n\n## working with other vielzeug libraries\n\nore uses ripple for component reactivity. clockwork actors expose framework neutral snapshots; bridge actor subscriptions into a ripple signal. ledger adds undo/redo commands around state changes without replacing graph.\n\n```ts\nimport { createripple } from '@vielzeug/ripple';\nimport { definemachine } from '@vielzeug/clockwork';\n\nconst ripple = createripple();\nconst actor = definemachine<record<string, never>, { type: 'start' }>()({\n initial: 'idle',\n states: { active: {}, idle: { on: { start: { target: 'active' } } } },\n}).createactor();\n\nconst snapshot = ripple.signal(actor.snapshot);\nconst stop = actor.subscribe((next) => (snapshot.value = next));\nconst status = ripple.computed(() => snapshot.value.state);\nconsole.log(status.value);\n\nstop();\nactor.dispose();\nripple.dispose();\n```\n\n## best practices\n\n create one graph per ownership boundary.\n keep computed callbacks pure.\n return cleanup from effects.\n dispose request, test, and feature graphs.\n batch related synchronous writes.\n use `watch()` only for selected source transitions.\n read dependencies in a resource source, not its loader.\n use `onerror` for runtime callback, cleanup, listener, and observer failures; handle resource source and loader failures through `resource.value.status === 'error'`.\n",
|
|
986
|
+
"usage": " \ntitle: ripple — usage guide\ndescription: build reactive state with one explicit graph boundary.\n \n\n[[toc]]\n\n## basic usage\n\nuse top level functions when one application lifetime graph is sufficient. read a signal inside an effect to make that read reactive.\n\n```ts\nimport { computed, effect, signal } from '@vielzeug/ripple';\n\nconst count = signal(0);\nconst label = computed(() => `count: ${count.value}`);\nconst stop = effect(() => console.log(label.value));\n\ncount.value = 1;\nstop.dispose();\n```\n\n## isolated graphs\n\nuse `createripple()` for tests, ssr requests, embedded applications, or independently disposable features. never mix reactive values from separate graphs.\n\n```ts\nimport { createripple } from '@vielzeug/ripple';\n\nconst ripple = createripple({\n onerror(error, context) {\n console.log(context.kind, error);\n },\n});\n\nconst count = ripple.signal(0);\nconst stop = ripple.effect(() => console.log(count.value));\n\nstop.dispose();\nripple.dispose();\n```\n\n## derived values and batches\n\nuse `computed()` for pure derivation. use `untrack()` when a current read must not become an effect dependency. use `batch()` for related synchronous writes.\n\n```ts\nconst first = ripple.signal('ada');\nconst last = ripple.signal('lovelace');\nconst locale = ripple.signal('en us');\nconst name = ripple.computed(() => `${first.value} ${last.value}`);\n\nripple.effect(() => {\n console.log({ locale: ripple.untrack(() => locale.value), name: name.value });\n});\n\nripple.batch(() => {\n first.value = 'grace';\n last.value = 'hopper';\n});\n```\n\n## scheduling and subscriptions\n\nripple propagates every synchronous write before flushing effects. each flush pass runs effects queued at its\nstart before direct `subscribe()` listeners queued at its start. work queued by either runs in a later pass.\neffects using `scheduler: 'microtask'` join a later microtask and coalesce writes made before that task runs.\n\n```ts\nconst count = ripple.signal(0);\nconst log: string[] = [];\n\nripple.effect(() => log.push(`effect: ${count.value}`));\ncount.subscribe(() => log.push(`listener: ${count.value}`));\nripple.effect(() => log.push(`deferred: ${count.value}`), { scheduler: 'microtask' });\n\nlog.length = 0; // ignore synchronous creation runs.\ncount.value = 1;\nconsole.log(log); // ['effect: 1', 'listener: 1']\n\nawait promise.resolve();\nconsole.log(log); // ['effect: 1', 'listener: 1', 'deferred: 1']\n```\n\n## ownership with scopes\n\ncreate a scope when a group of effects or derived values shares one lifetime. dispose the scope when its feature ends.\n\n```ts\nconst scope = ripple.createscope('panel');\nconst count = ripple.signal(0);\n\nscope.run(() => {\n ripple.effect(() => console.log(`panel count: ${count.value}`));\n});\n\ncount.value = 1;\nscope.dispose();\n```\n\n## watch selected values\n\nuse `watch()` for one selected output. use `effect()` when every reactive read in the callback should be a dependency.\n\n```ts\nconst stopwatch = ripple.watch(\n () => `${first.value} ${last.value}`,\n (value, previous) => console.log({ previous, value }),\n { immediate: true },\n);\n\nstopwatch.dispose();\n```\n\n## async data\n\n`resource()` captures source dependencies synchronously and passes a cancellation signal to the loader.\n\n```ts\nconst userid = ripple.signal('42');\nconst user = ripple.resource(\n () => userid.value,\n async (id, { signal }) => {\n const response = await fetch(`/users/${id}`, { signal });\n if (!response.ok) throw new error(`request failed: ${response.status}`);\n\n return response.json() as promise<{ id: string; name: string }>;\n },\n);\n\nif (user.value.status === 'success') console.log(user.value.value.name);\nif (user.value.status === 'error') console.error(user.value.error);\nuser.dispose();\n```\n\n## object state\n\n`signal()` with `update()` holds one value and supports immutable replacement patterns. return replacement objects from `update()` when object consumers depend on immutable updates.\n\n```ts\nconst cart = ripple.signal({ items: 0, label: 'empty' });\nconst items = ripple.computed(() => cart.value.items);\n\ncart.update((state) => ({ ...state, items: state.items + 1 }));\ncart.value = { items: 3, label: 'ready' };\n\nconsole.log(items.value);\n```\n\n## testing\n\ncreate an isolated graph per test. disposal prevents effects and resource work from leaking into later tests.\n\n```ts\nimport { expect, test } from 'vitest';\nimport { createripple } from '@vielzeug/ripple';\n\ntest('derives a doubled count', () => {\n const ripple = createripple();\n const count = ripple.signal(2);\n const doubled = ripple.computed(() => count.value * 2);\n\n expect(doubled.value).tobe(4);\n ripple.dispose();\n});\n```\n\n## framework integration\n\nuse signals and effects with any renderer. dispose component owned effects when the component unmounts.\n\n::: code group\n\n```ts [react]\nimport { useeffect, usestate } from 'react';\nimport { createripple } from '@vielzeug/ripple';\n\nconst ripple = createripple();\nconst count = ripple.signal(0);\n\nexport function counter() {\n const [, rerender] = usestate(0);\n\n useeffect(() => {\n const stop = ripple.effect(() => {\n void count.value;\n rerender((revision) => revision + 1);\n });\n\n return () => stop.dispose();\n }, []);\n\n return <button onclick={() => (count.value += 1)}>{count.value}</button>;\n}\n```\n\n```ts [vue 3]\nimport { onunmounted, ref } from 'vue';\nimport { createripple } from '@vielzeug/ripple';\n\nconst ripple = createripple();\nconst count = ripple.signal(0);\nconst revision = ref(0);\nconst stop = ripple.effect(() => {\n void count.value;\n revision.value++;\n});\n\nonunmounted(() => stop.dispose());\n```\n\n```ts [svelte]\n<script lang=\"ts\">\n import { ondestroy } from 'svelte';\n import { createripple } from '@vielzeug/ripple';\n\n const ripple = createripple();\n const count = ripple.signal(0);\n let revision = 0;\n const stop = ripple.effect(() => {\n void count.value;\n revision++;\n });\n\n ondestroy(() => stop.dispose());\n</script>\n\n<button on:click={() => (count.value += 1)}>{count.value}</button>\n```\n\n:::\n\n## working with other vielzeug libraries\n\nore uses ripple for component reactivity. clockwork actors expose framework neutral snapshots; bridge actor subscriptions into a ripple signal. ledger adds undo/redo commands around state changes without replacing graph.\n\n```ts\nimport { createripple } from '@vielzeug/ripple';\nimport { definemachine } from '@vielzeug/clockwork';\n\nconst ripple = createripple();\nconst actor = definemachine<record<string, never>, { type: 'start' }>()({\n initial: 'idle',\n states: { active: {}, idle: { on: { start: { target: 'active' } } } },\n}).createactor();\n\nconst snapshot = ripple.signal(actor.snapshot);\nconst stop = actor.subscribe((next) => (snapshot.value = next));\nconst status = ripple.computed(() => snapshot.value.state);\nconsole.log(status.value);\n\nstop();\nactor.dispose();\nripple.dispose();\n```\n\n## gotchas\n\n### `subscribe()` forces computed evaluation\n\n`readable.subscribe()` calls `peek()` before registering the listener. for signals this is a no op, but for computeds it forces `refresh()` — the derivation runs immediately even if no one reads `.value`. this ensures `equals` comparison works on the first dependency change. avoid subscribing to expensive computeds unless you need their value.\n\n### computed first run failure is recoverable\n\nif a computed's `derive` throws on its first run (e.g., a source is `null`), the computed commits the partial dependencies it tracked before the throw. when a dependency changes and the derivation can succeed, the computed refreshes and notifies its dependents. effects that read a failing computed report the error through `onerror` and re run when the computed recovers.\n\n## best practices\n\n create one graph per ownership boundary.\n keep computed callbacks pure.\n return cleanup from effects.\n dispose request, test, and feature graphs.\n batch related synchronous writes.\n use `watch()` only for selected source transitions.\n read dependencies in a resource source, not its loader.\n use `onerror` for runtime callback, cleanup, listener, and observer failures; handle resource source and loader failures through `resource.value.status === 'error'`.\n",
|
|
885
987
|
"examples": " \ntitle: ripple — examples\ndescription: practical ripple recipes.\n \n\n## examples\n\n [reactive counter](./examples/reactive counter.md)\n [batch and untrack](./examples/batch and untrack.md)\n [scope ownership](./examples/scope ownership.md)\n [watch selected value](./examples/watch selected value.md)\n [immutable state](./examples/immutable store.md)\n [isolated graph](./examples/isolated runtime.md)\n [async resource](./examples/async resource.md)\n"
|
|
886
988
|
},
|
|
887
989
|
"examples": [
|
|
@@ -1093,13 +1195,35 @@
|
|
|
1093
1195
|
"slug": "scroll",
|
|
1094
1196
|
"source": "export type {\n domvirtuallistcontroller,\n domvirtuallistoptions,\n domvirtuallistrenderargs,\n recyclefn,\n sticktobottomoptions,\n virtualrenderitem,\n virtualscrolleroptions,\n} from './dom virtual list';\nexport { createdomvirtuallist, createvirtualscroller } from './dom virtual list';\nexport { scrollconfigurationerror, scrollerror, scrollrangeerror } from './errors';\nexport type {\n gridrangechangeevent,\n gridvirtualizer,\n gridvirtualizeroptions,\n gridvirtualizerstate,\n gridvirtualizerupdateoptions,\n scrolltocelloptions,\n} from './grid virtualizer';\nexport { creategridvirtualizer } from './grid virtualizer';\nexport type {\n groupsection,\n groupvirtualheader,\n groupvirtualitem,\n groupvirtualizer,\n groupvirtualizeroptions,\n groupvirtualizerstate,\n groupvirtualizerupdateoptions,\n} from './grouped virtualizer';\nexport { creategroupedvirtualizer } from './grouped virtualizer';\nexport type {\n measurementcache,\n overscan,\n scrolltarget,\n scrolltoindexoptions,\n virtualitem,\n virtualizer,\n virtualizeroptions,\n virtualizerstate,\n virtualizerupdateoptions,\n virtualkey,\n} from './virtualizer';\nexport { createmeasurementcache, createvirtualizer, default_estimate_size, default_overscan } from './virtualizer';\n"
|
|
1095
1197
|
},
|
|
1198
|
+
{
|
|
1199
|
+
"category": "environment",
|
|
1200
|
+
"description": "reactive browser and dom observations for viewport, network, media query, element size, and intersection state.",
|
|
1201
|
+
"docs": {
|
|
1202
|
+
"index": " \ntitle: sentinel — reactive environment state\ndescription: reactive browser and dom observations for viewport, network, media query, element size, and intersection state.\npackage: sentinel\ncategory: environment\nkeywords: [reactive, browser, viewport, network, media query, resize observer, intersection observer]\nrelated: [ripple, ore, focus, gesture]\nexports: [createviewport, createnetwork, createmediaquery, createelementsize, createintersection, sentinelerror, sentinelunavailableerror, sentinel]\nenvironments: [browser]\n \n\n<! markdownlint disable md025 md033 md060 >\n\n<packagehero package=\"sentinel\" />\n\n## why sentinel?\n\nbrowser environment apis use different events, observer callbacks, initial states, and cleanup methods. sentinel gives them one explicit handle shape and exposes current values as ripple `readable<t>` signals.\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.value?.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=\"x\" size=\"16\"></ore icon> | <ore icon name=\"check\" size=\"16\"></ore icon> | <ore icon name=\"check\" size=\"16\"></ore icon> |\n| reactive current state | <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 reactive state, consistent ownership, and composition with ripple.\n\n**consider native apis when** one isolated observer is sufficient and adding ripple as a peer dependency is not justified.\n\n</div>\n\n## installation\n\n::: code group\n\n```sh [pnpm]\npnpm add @vielzeug/sentinel @vielzeug/ripple\n```\n\n```sh [npm]\nnpm install @vielzeug/sentinel @vielzeug/ripple\n```\n\n```sh [yarn]\nyarn add @vielzeug/sentinel @vielzeug/ripple\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.value;\n console.log(`${width}×${height} at ${dpr}dpr`);\n };\n\n render();\n const unsubscribe = viewport.subscribe(render);\n\n return () => {\n unsubscribe();\n viewport.dispose();\n };\n}\n\nconst stopobserving = observeviewport();\n// call stopobserving() when the owning view unmounts.\n```\n\n<div class=\"features grid\">\n\n## features\n\n `createviewport()` — observe viewport dimensions and device pixel ratio.\n `createnetwork()` — track online status and optional connection details.\n `createmediaquery()` — observe one media query.\n `createelementsize()` — read content box dimensions from `resizeobserver`.\n `createintersection()` — track normalized intersection state.\n `dispose()` — release owned browser observers and listeners.\n `sentineloptions.signal` — abort several sentinels through one external lifetime.\n\n</div>\n\n<div class=\"doc links\">\n\n## documentation\n\n [**usage guide**](./usage.md) — apply sentinel lifecycles, framework bindings, and ripple composition.\n [**api reference**](./api.md) — review factory signatures, options, state types, and errors.\n [**examples**](./examples.md) — follow focused browser observation examples.\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",
|
|
1203
|
+
"api": " \ntitle: sentinel — api reference\ndescription: factory signatures, options, state types, lifecycle handles, and errors for sentinel.\n \n\n[[toc]]\n\n## api overview\n\n| symbol | purpose | execution mode | common gotcha |\n| | | | |\n| `createviewport()` | observe layout viewport dimensions and device pixel ratio | sync | requires a browser window |\n| `createnetwork()` | observe online status and optional connection information | sync | `connection` is often `null` |\n| `createmediaquery()` | observe one media query | sync | throws when `matchmedia` is unavailable |\n| `createelementsize()` | observe element content box dimensions | sync | value is `null` before the first delivery |\n| `createintersection()` | observe element intersection state | sync | value is `null` before the first delivery |\n| `sentinel<t>` | combine a ripple readable with explicit browser resource ownership | sync | subscriptions and the sentinel have separate cleanup |\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.runtime` | `pick<ripple, 'signal'>` | ripple runtime that owns the internal signal |\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.value.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.runtime` | `pick<ripple, 'signal'>` | ripple runtime that owns the internal signal |\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.value.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.runtime` | `pick<ripple, 'signal'>` | ripple runtime that owns the internal signal |\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.value.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.runtime` | `pick<ripple, 'signal'>` | ripple runtime that owns the internal signal |\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.value?.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.runtime` | `pick<ripple, 'signal'>` | ripple runtime that owns the internal signal |\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.value?.isintersecting);\n});\n\nunsubscribe();\nintersection.dispose();\n```\n\n## types\n\n### `sentinel<t>`\n\n```ts\ninterface sentinel<t> extends readable<t> {\n readonly disposalsignal: abortsignal;\n dispose(): void;\n readonly disposed: boolean;\n [symbol.dispose](): void;\n}\n```\n\n`value`, `peek()`, and `subscribe()` follow ripple's `readable<t>` contract. `dispose()` stops the underlying browser observation. a subscription's returned function remains independently owned by the subscriber.\n\n| member | type | description |\n| | | |\n| `value` | `t` | current reactive snapshot |\n| `peek()` | `() => t` | read the snapshot without reactive tracking |\n| `subscribe(listener)` | `(listener: () => void) => () => void` | subscribe to invalidations and return an independent unsubscribe function |\n| `disposed` | `boolean` | whether observation has ended |\n| `disposalsignal` | `abortsignal` | aborts when observation ends |\n| `dispose()` | `() => void` | stop observation and release owned browser resources |\n| `[symbol.dispose]()` | `() => void` | dispose through the explicit resource management protocol |\n\n \n\n### `sentineloptions`\n\n```ts\ninterface sentineloptions {\n readonly runtime?: pick<ripple, 'signal'>;\n readonly signal?: abortsignal;\n}\n```\n\n \n\n### `windowsentineloptions`\n\n```ts\ninterface windowsentineloptions extends sentineloptions {\n readonly target?: window;\n}\n```\n\n \n\n### `createintersectionoptions`\n\n```ts\ninterface createintersectionoptions extends sentineloptions {\n readonly root?: element | document | null;\n readonly rootmargin?: string;\n readonly scrollmargin?: string;\n readonly threshold?: number | number[];\n}\n```\n\n \n\n### `viewportstate`\n\n```ts\ninterface viewportstate {\n readonly dpr: number;\n readonly height: number;\n readonly width: number;\n}\n```\n\n \n\n### `networkconnectionsnapshot`\n\n```ts\ninterface networkconnectionsnapshot {\n readonly downlink?: number;\n readonly effectivetype?: 'slow 2g' | '2g' | '3g' | '4g';\n readonly rtt?: number;\n readonly savedata?: boolean;\n}\n```\n\n \n\n### `networkstate`\n\n```ts\ninterface networkstate {\n readonly connection: networkconnectionsnapshot | null;\n readonly online: boolean;\n}\n```\n\n \n\n### `mediaquerystate`\n\n```ts\ninterface mediaquerystate {\n readonly matches: boolean;\n}\n```\n\n \n\n### `elementsizestate`\n\n```ts\ninterface elementsizestate {\n readonly height: number;\n readonly width: number;\n}\n```\n\n \n\n### `intersectionstate`\n\n```ts\ninterface intersectionstate {\n readonly intersectionratio: number;\n readonly isintersecting: boolean;\n}\n```\n\n## errors\n\n### `sentinelerror`\n\n```ts\nclass sentinelerror extends error {\n constructor(message: string, options?: erroroptions);\n}\n```\n\nbase class for package defined errors.\n\n \n\n### `sentinelunavailableerror`\n\n```ts\nclass sentinelunavailableerror extends sentinelerror {}\n```\n\nthrown when a required browser api or window is unavailable:\n\n `createviewport()` and `createnetwork()` when no browser window is available.\n `createmediaquery()` when `matchmedia` is unavailable.\n `createelementsize()` when the element has no window or `resizeobserver` is unavailable.\n `createintersection()` when the element has no window or `intersectionobserver` is unavailable.\n\nnative setup errors remain unchanged, including invalid observer options or targets.\n",
|
|
1204
|
+
"usage": " \ntitle: sentinel — usage guide\ndescription: observe browser and dom state with explicit reactive 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.value;\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 value changed; read the new snapshot from `.value` inside the listener. disposing a sentinel stops its browser observer or event listeners. it does not unsubscribe consumers from the ripple readable.\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.value.width);\nconsole.log(viewport.value.height);\nconsole.log(viewport.value.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.value.online);\nconsole.log(network.value.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.value.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.value;\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.value?.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\nan injected ripple runtime creates the state signal. runtime disposal and sentinel disposal remain separate responsibilities.\n\n```ts\nimport { createripple } from '@vielzeug/ripple';\nimport { createviewport } from '@vielzeug/sentinel';\n\nconst ripple = createripple();\nconst viewport = createviewport({ runtime: ripple });\n\nviewport.dispose();\nripple.dispose();\n```\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\ncreate the sentinel after the component mounts, mirror its current value into framework state, and unsubscribe and dispose on unmount.\n\n::: code group\n\n```tsx [react]\nimport { createviewport, type viewportstate } from '@vielzeug/sentinel';\nimport { useeffect, usestate } from 'react';\n\nexport function viewportsize() {\n const [viewportstate, setviewportstate] = usestate<viewportstate | null>(null);\n\n useeffect(() => {\n const viewport = createviewport();\n const update = () => setviewportstate(viewport.value);\n\n update();\n const unsubscribe = viewport.subscribe(update);\n\n return () => {\n unsubscribe();\n viewport.dispose();\n };\n }, []);\n\n return <output>{viewportstate ? `${viewportstate.width}×${viewportstate.height}` : 'measuring…'}</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?.value ?? 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.value;\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\nuse ripple to derive values from one or more sentinel states. dispose the watcher separately from the sentinels.\n\n```ts\nimport { computed, watch } from '@vielzeug/ripple';\nimport { createmediaquery, createviewport } from '@vielzeug/sentinel';\n\nconst viewport = createviewport();\nconst mobilequery = createmediaquery('(max width: 768px)');\nconst compact = computed(() => mobilequery.value.matches || viewport.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.value?.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 from `.value` 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",
|
|
1205
|
+
"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"
|
|
1206
|
+
},
|
|
1207
|
+
"examples": [
|
|
1208
|
+
{
|
|
1209
|
+
"id": "viewport-basic",
|
|
1210
|
+
"text": "createviewport basic import { createviewport } from '@vielzeug/sentinel'\n\nconst viewport = createviewport()\nconst logviewport = () => {\n const { dpr, height, width } = viewport.value\n console.log(`${width}x${height} at ${dpr}dpr`)\n}\n\nlogviewport()\nconst unsubscribe = viewport.subscribe(logviewport)\nwindow.dispatchevent(new event('resize'))\n\nunsubscribe()\nviewport.dispose()\nconsole.log('disposed:', viewport.disposed)"
|
|
1211
|
+
}
|
|
1212
|
+
],
|
|
1213
|
+
"exports": "createviewport createnetwork createmediaquery createelementsize createintersection sentinelerror sentinelunavailableerror sentinel",
|
|
1214
|
+
"keywords": "reactive browser viewport network media query resize observer intersection observer",
|
|
1215
|
+
"name": "@vielzeug/sentinel",
|
|
1216
|
+
"related": "ripple ore focus gesture",
|
|
1217
|
+
"slug": "sentinel",
|
|
1218
|
+
"source": "export { createelementsize } from './element size.ts';\nexport { sentinelerror, sentinelunavailableerror } from './errors.ts';\nexport type { createintersectionoptions } from './intersection.ts';\nexport { createintersection } from './intersection.ts';\nexport { createmediaquery } from './media query.ts';\nexport { createnetwork } from './network.ts';\nexport type {\n elementsizestate,\n intersectionstate,\n mediaquerystate,\n networkconnectionsnapshot,\n networkstate,\n sentinel,\n sentineloptions,\n viewportstate,\n windowsentineloptions,\n} from './types.ts';\nexport { createviewport } from './viewport.ts';\n"
|
|
1219
|
+
},
|
|
1096
1220
|
{
|
|
1097
1221
|
"category": "data",
|
|
1098
1222
|
"description": "framework agnostic collection sources for local, page, cursor, and infinite pagination.",
|
|
1099
1223
|
"docs": {
|
|
1100
|
-
"index": " \ntitle: sourcerer — reactive query sources\ndescription: framework agnostic collection sources for local, page, cursor, and infinite pagination.\npackage: sourcerer\ncategory: data\nkeywords: [pagination, data source, cursor, infinite scroll, search]\nrelated: [courier, ripple, scout, wayfinder]\nexports:\n [\n createcursorsource,\n createinfinitesource,\n createlocalsource,\n createpagesource,\n anypagination,\n cursorpagination,\n cursorquery,\n cursorquerypatch,\n cursorresult,\n cursorsource,\n cursorsourceconfig,\n infinitepagination,\n infinitequery,\n infinitequerypatch,\n infinitesource,\n infinitesourceconfig,\n localquery,\n localquerypatch,\n localsource,\n localsourceconfig,\n loadcontext,\n pagepagination,\n pagequery,\n pagequerypatch,\n pageresult,\n pagesource,\n pagesourceconfig,\n source,\n sourcesnapshot,\n ]\nenvironments: [browser, node, ssr, deno]\n \n\n<! markdownlint disable md025 md033 md060 >\n\n<packagehero package=\"sourcerer\" />\n\n## why sourcerer?\n\nlists often combine pagination, search, request cancellation, and render state. sourcerer gives local arrays and remote loaders one snapshot contract while leaving caching, retries, and transport policy to your application.\n\n```ts\nimport { createpagesource } from '@vielzeug/sourcerer';\n\ntype user = { id: number; name: string };\n\n// before: query changes can mix old items with new loading and page state.\nlet items: user[] = [];\nlet page = 1;\nlet isloading = false;\n\n// after: one source publishes internally consistent loaded state.\nconst source = createpagesource<user>({\n autostart: false,\n load: async () => ({ data: [{ id: 1, name: 'ada' }], total: 1 }),\n});\nsource.subscribe((snapshot) => console.log(snapshot.data));\nsource.dispose();\n```\n\n| feature | sourcerer | manual list state | courier query cache |\n| | | | |\n| bundle size | <packageinfo package=\"sourcerer\" type=\"size\" /> | application defined | <packageinfo package=\"courier\" type=\"size\" /> |\n| zero runtime dependencies | <ore icon name=\"check\" size=\"16\"></ore icon> | <ore icon name=\"check\" size=\"16\"></ore icon> | <ore icon name=\"x\" size=\"16\"></ore icon> |\n| local and remote collections | <ore icon name=\"check\" size=\"16\"></ore icon> | application defined | <ore icon name=\"triangle alert\" size=\"16\"></ore icon> |\n| cursor and infinite pagination | <ore icon name=\"check\" size=\"16\"></ore icon> | application defined | <ore icon name=\"triangle alert\" size=\"16\"></ore icon> |\n| latest request cancellation | <ore icon name=\"check\" size=\"16\"></ore icon> | application defined | transport level |\n\n<div class=\"decision callout\">\n\n**use sourcerer when** one ui collection needs local or remote pagination with an explicit, framework independent snapshot contract.\n\n**consider courier alone when** you only need cached http queries and pagination state belongs elsewhere.\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, then dispose it with its owner.\n\n```ts\nimport { createpagesource } from '@vielzeug/sourcerer';\n\ntype user = { id: number; name: string };\n\nconst source = createpagesource<user>({\n autostart: false,\n load: async ({ query }) => {\n const users = [\n { id: 1, name: 'ada' },\n { id: 2, name: 'grace' },\n { id: 3, name: 'linus' },\n ];\n const start = (query.page 1) * query.pagesize;\n\n return { data: users.slice(start, start + query.pagesize), total: users.length };\n },\n});\n\ntry {\n await source.reload();\n console.log(source.snapshot.data);\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()` — synchronous search and numbered pagination over an array\n `createpagesource()` — numbered remote pages with latest request cancellation\n `createcursorsource()` — sequential opaque cursor navigation\n `createinfinitesource()` — append only page loading\n `sourcesnapshot` — loaded `query`, `data`, and `pagination` plus optional `pendingquery`\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/) — use as transport, caching, and retry policy inside a page loader\n [scout](/scout/) — adapt an indexed search matcher for local sources\n [ripple](/ripple/) — project source snapshots into reactive application state\n [wayfinder](/wayfinder/) — validate and synchronize page query fields with route state\n\n</div>\n\n<! markdownlint enable md025 md033 md060 >\n",
|
|
1101
|
-
"api": " \ntitle: sourcerer — api reference\ndescription: public api for @vielzeug/sourcerer.\n \n\n[[toc]]\n\n## api overview\n\n| symbol | purpose | execution | common gotcha |\n| | | | |\n| `createlocalsource()` | in memory search and pagination | sync | prepare filtering and ranking before `setdata()` |\n| `createpagesource()` | numbered async pages | async | `query` remains loaded state while `pendingquery` is active |\n| `createcursorsource()` | cursor based async pages | async | `after` and `before` cannot coexist |\n| `createinfinitesource()` | appended async pages | async | `loadmore()` does nothing while fetching or exhausted |\n| `sourcesnapshot` | atomic loaded state plus pending request | type | read `pendingquery` for newer in flight state |\n\n## package entry point\n\n| import | purpose |\n| | |\n| `@vielzeug/sourcerer` | factories and public types |\n\n## factories\n\n### `createlocalsource()`\n\n```ts\nfunction createlocalsource<t>(data: readonly t[], config?: localsourceconfig<t>): localsource<t>\n```\n\ncreates a synchronous source over an in memory collection.\n\n| option | type | description |\n| | | |\n| `initialquery` | `localquerypatch` | initial page, page size, or search value |\n| `match` | `(item, search) => boolean` | explicit search predicate |\n\n**returns:** `localsource<t>`.\n\n```ts\nimport { createlocalsource } from '@vielzeug/sourcerer';\n\nconst users = createlocalsource(\n [{ id: 1, name: 'ada' }],\n {\n initialquery: { pagesize: 20 },\n match: (user, search) => user.name.tolowercase().includes(search.tolowercase()),\n },\n);\n\nusers.setquery({ search: 'ada' });\n```\n\n \n\n### `createpagesource()`\n\n```ts\nfunction createpagesource<t, tfilter = unknown, tsort = unknown>(\n config: pagesourceconfig<t, tfilter, tsort>,\n): pagesource<t, tfilter, tsort>\n```\n\ncreates a numbered source. new queries abort older work. loaded state stays in `snapshot`; newer work appears in `snapshot.pendingquery`.\n\n| option | type | description |\n| | | |\n| `autostart` | `boolean` | start initial request; default `true` |\n| `initialquery` | `pagequerypatch<tfilter, tsort>` | initial query values |\n| `load` | `(context) => promise<pageresult<t>>` | transport callback |\n\n**returns:** `pagesource<t, tfilter, tsort>`.\n\n```ts\nimport { createpagesource } from '@vielzeug/sourcerer';\n\nconst users = createpagesource({\n autostart: false,\n load: async () => ({ data: [{ id: 1, name: 'ada' }], total: 1 }),\n});\n\nawait users.setquery({ page: 1 });\nusers.dispose();\n```\n\n \n\n### `createcursorsource()`\n\n```ts\nfunction createcursorsource<t, tcursor = string>(\n config: cursorsourceconfig<t, tcursor>,\n): cursorsource<t, tcursor>\n```\n\ncreates a sequential cursor source. search and page size changes reset cursors.\n\n**returns:** `cursorsource<t, tcursor>`.\n\n```ts\nimport { createcursorsource } from '@vielzeug/sourcerer';\n\nconst orders = createcursorsource({\n autostart: false,\n load: async () => ({ data: ['order 1'] }),\n});\n\nawait orders.reload();\nawait orders.page.next();\norders.dispose();\n```\n\n \n\n### `createinfinitesource()`\n\n```ts\nfunction createinfinitesource<t>(config: infinitesourceconfig<t>): infinitesource<t>\n```\n\ncreates an append only source. query changes replace loaded collection after successful first page load.\n\n**returns:** `infinitesource<t>`.\n\n```ts\nimport { createinfinitesource } from '@vielzeug/sourcerer';\n\nconst feed = createinfinitesource({\n autostart: false,\n load: async () => ({ data: ['post 1'], total: 1 }),\n});\n\nawait feed.loadmore();\nfeed.dispose();\n```\n\n## types\n\n### source primitives\n\n```ts\ntype disposable = {\n [symbol.dispose](): void;\n readonly disposalsignal: abortsignal;\n dispose(): void;\n readonly disposed: boolean;\n};\n\ntype sourcesnapshot<t, tquery, tpagination extends anypagination = anypagination> = readonly<{\n data: readonly t[];\n error: error | null;\n isfetching: boolean;\n pagination: tpagination;\n pendingquery?: tquery;\n query: tquery;\n}>;\n\ntype source<t, tquery, tpagination extends anypagination = anypagination> = disposable & {\n readonly snapshot: sourcesnapshot<t, tquery, tpagination>;\n subscribe(listener: (snapshot: sourcesnapshot<t, tquery, tpagination>) => void): () => void;\n};\n```\n\n### numbered pages\n\n```ts\ntype pagepagination = readonly<{\n count: number;\n hasnext: boolean;\n hasprevious: boolean;\n index: number;\n kind: 'page';\n size: number;\n total: number;\n}>;\n\ntype pagequery<tfilter = unknown, tsort = unknown> = readonly<{\n filter?: tfilter;\n page: number;\n pagesize: number;\n search: string;\n sort?: tsort;\n}>;\n\ntype pagequerypatch<tfilter = unknown, tsort = unknown> = readonly<{\n filter?: tfilter | undefined;\n page?: number;\n pagesize?: number;\n search?: string;\n sort?: tsort | undefined;\n}>;\n\ntype pageresult<t> = readonly<{ data: readonly t[]; total: number }>;\ntype loadcontext<tquery> = readonly<{ query: tquery; signal: abortsignal }>;\n\ntype pagesourceconfig<t, tfilter = unknown, tsort = unknown> = readonly<{\n autostart?: boolean;\n initialquery?: pagequerypatch<tfilter, tsort>;\n load(context: loadcontext<pagequery<tfilter, tsort>>): promise<pageresult<t>>;\n}>;\n\ntype pagesource<t, tfilter = unknown, tsort = unknown> = source<t, pagequery<tfilter, tsort>, pagepagination> & {\n readonly page: readonly<{\n go(index: number): promise<void>;\n last(): promise<void>;\n next(): promise<void>;\n previous(): promise<void>;\n }>;\n reload(): promise<void>;\n setquery(changes: pagequerypatch<tfilter, tsort>): promise<void>;\n};\n```\n\n### local sources\n\n```ts\ntype localquery = readonly<{ page: number; pagesize: number; search: string }>;\ntype localquerypatch = readonly<{ page?: number; pagesize?: number; search?: string }>;\ntype localsourceconfig<t> = readonly<{\n initialquery?: localquerypatch;\n match?: (item: t, search: string) => boolean;\n}>;\n\ntype localsource<t> = source<t, localquery, pagepagination> & {\n readonly page: readonly<{\n go(index: number): void;\n last(): void;\n next(): void;\n previous(): void;\n }>;\n setdata(data: readonly t[]): void;\n setquery(changes: localquerypatch): void;\n};\n```\n\n### cursor and infinite sources\n\n```ts\ntype cursorpagination<tcursor = string> = readonly<{\n hasnext: boolean;\n hasprevious: boolean;\n kind: 'cursor';\n nextcursor?: tcursor;\n previouscursor?: tcursor;\n total?: number;\n}>;\n\ntype cursorquery<tcursor = string> = readonly<{\n after?: tcursor;\n before?: tcursor;\n pagesize: number;\n search: string;\n}>;\n\ntype cursorquerypatch<tcursor = string> = readonly<{\n after?: tcursor | undefined;\n before?: tcursor | undefined;\n pagesize?: number;\n search?: string;\n}>;\n\ntype cursorresult<t, tcursor = string> = readonly<{\n data: readonly t[];\n nextcursor?: tcursor;\n previouscursor?: tcursor;\n total?: number;\n}>;\n\ntype cursorsourceconfig<t, tcursor = string> = readonly<{\n autostart?: boolean;\n initialquery?: cursorquerypatch<tcursor>;\n load(context: loadcontext<cursorquery<tcursor>>): promise<cursorresult<t, tcursor>>;\n}>;\n\ntype cursorsource<t, tcursor = string> = source<t, cursorquery<tcursor>, cursorpagination<tcursor>> & {\n readonly page: readonly<{ next(): promise<void>; previous(): promise<void> }>;\n reload(): promise<void>;\n setquery(changes: cursorquerypatch<tcursor>): promise<void>;\n};\n\ntype infinitepagination = readonly<{\n hasmore: boolean;\n
|
|
1102
|
-
"usage": " \ntitle: sourcerer — usage guide\ndescription: build local, page, cursor, and infinite collection sources.\n \n\n[[toc]]\n\n## basic usage\n\nuse a local source when data already exists in memory.\n\n```ts\nimport { createlocalsource } from '@vielzeug/sourcerer';\n\nconst source = createlocalsource(\n [\n { id: 1, name: 'ada' },\n { id: 2, name: 'grace' },\n { id: 3, name: 'linus' },\n ],\n {\n initialquery: { pagesize: 2 },\n match: (user, search) => user.name.tolowercase().includes(search.tolowercase()),\n },\n);\n\nsource.setquery({ search: 'a' });\nconsole.log(source.snapshot.data);\nsource.dispose();\n```\n\nread `snapshot.query`, `snapshot.data`, and `snapshot.pagination` together. they always describe one loaded result.\n\n## handle pending remote queries\n\nuse `pendingquery` to distinguish loaded data from newer work.\n\n```ts\nimport { createpagesource } from '@vielzeug/sourcerer';\n\nconst source = createpagesource<string>({\n autostart: false,\n load: async ({ query }) => {\n const data = ['ada', 'grace', 'linus'];\n const start = (query.page 1) * query.pagesize;\n\n return { data: data.slice(start, start + query.pagesize), total: data.length };\n },\n});\n\nsource.subscribe((snapshot) => {\n if (snapshot.pendingquery) console.log('loading:', snapshot.pendingquery);\n console.log('loaded:', snapshot.query, snapshot.data);\n});\n\nawait source.setquery({ page: 2 });\nsource.dispose();\n```\n\nnew `setquery()` calls abort older requests. a failed current request preserves prior loaded data, records `snapshot.error`, and rejects the returned promise.\n\n## use cursor pagination\n\nuse cursors when an api cannot provide stable page numbers.\n\n```ts\nimport { createcursorsource } from '@vielzeug/sourcerer';\n\nconst rows = ['a', 'b', 'c', 'd'];\nconst source = createcursorsource<string, number>({\n autostart: false,\n initialquery: { pagesize: 2 },\n load: async ({ query }) => {\n const start = query.after ?? 0;\n const data = rows.slice(start, start + query.pagesize);\n const nextcursor = start + data.length;\n\n return {\n data,\n nextcursor: nextcursor < rows.length ? nextcursor : undefined,\n previouscursor: start > 0 ? math.max(0, start query.pagesize) : undefined,\n };\n },\n});\n\nawait source.reload();\nawait source.page.next();\nconsole.log(source.snapshot.data);\nsource.dispose();\n```\n\n`after` and `before` cannot coexist. search or page size changes reset cursor state.\n\n## build an infinite feed\n\nuse an infinite source when each page should append.\n\n```ts\nimport { createinfinitesource } from '@vielzeug/sourcerer';\n\nconst source = createinfinitesource<number>({\n autostart: false,\n initialquery: { pagesize: 2 },\n load: async ({ query }) => {\n const values = [1, 2, 3, 4, 5];\n const start = (query.page 1) * query.pagesize;\n\n return { data: values.slice(start, start + query.pagesize), total: values.length };\n },\n});\n\nawait source.loadmore();\nawait source.loadmore();\nconsole.log(source.snapshot.data);\nsource.dispose();\n```\n\n`loadmore()` is a no op while fetching or after `pagination.hasmore` becomes false.\n\n## testing and debugging\n\ninject deterministic loaders in unit tests. await source commands before reading final state.\n\n```ts\nimport { expect, it } from 'vitest';\nimport { createpagesource } from '@vielzeug/sourcerer';\n\nit('loads first page', async () => {\n const source = createpagesource({\n autostart: false,\n load: async () => ({ data: ['ada'], total: 1 }),\n });\n\n await source.reload();\n expect(source.snapshot.data).toequal(['ada']);\n source.dispose();\n});\n```\n\n## framework integration\n\nsubscribe through each framework’s lifecycle. keep source creation stable across renders.\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 () => ({ data: [{ id: 1, name: 'ada' }], total: 1 }) }),\n [],\n );\n const snapshot = usesyncexternalstore(source.subscribe, () => source.snapshot);\n\n useeffect(() => () => source.dispose(), [source]);\n\n return <p>{snapshot.isfetching ? 'loading' : snapshot.data.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 () => ({ data: [{ id: 1, name: 'ada' }], total: 1 }) });\nconst snapshot = shallowref(source.snapshot);\nconst stop = source.subscribe((next) => (snapshot.value = next));\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 () => ({ data: [{ id: 1, name: 'ada' }], total: 1 }) });\n let snapshot = source.snapshot;\n const stop = source.subscribe((next) => (snapshot = next));\n\n ondestroy(() => {\n stop();\n source.dispose();\n });\n</script>\n\n{#if snapshot.isfetching}loading{/if}\n{#each snapshot.data as user}{user.name}{/each}\n```\n\n:::\n\n## working with other vielzeug libraries\n\nuse courier for transport policy. sourcerer owns request succession; courier owns http behavior.\n\n```ts\nimport { createcourier } from '@vielzeug/courier';\nimport { createpagesource } from '@vielzeug/sourcerer';\n\nconst courier = createcourier({ baseurl: '/api' });\nconst source = createpagesource({\n load: ({ query, signal }) => courier.get('/users', { query, signal }),\n});\n```\n\nuse scout’s matcher when local search needs an index.\n\n```ts\nimport { createindex, tosearchmatcher } from '@vielzeug/scout';\nimport { createlocalsource } from '@vielzeug/sourcerer';\n\nconst users = [{ name: 'ada' }, { name: 'grace' }];\nconst index = createindex(users, { fields: ['name'] });\nconst source = createlocalsource(users, { match: tosearchmatcher(index) });\n```\n\n## best practices\n\n dispose each source with its owning view, request, or scope.\n read one snapshot object per render instead of mixing source fields across updates.\n inspect `pendingquery` before rendering controls for in flight work.\n validate url query values before passing them to `setquery()`.\n keep caching, retries, polling, and optimistic writes in your transport layer.\n use `setdata()` with prepared local collections; keep ranking and filtering explicit.\n debounce text inputs before updating remote source queries.\n",
|
|
1224
|
+
"index": " \ntitle: sourcerer — reactive query sources\ndescription: framework agnostic collection sources for local, page, cursor, and infinite pagination.\npackage: sourcerer\ncategory: data\nkeywords: [pagination, data source, cursor, infinite scroll, search]\nrelated: [courier, ripple, scout, wayfinder]\nexports:\n [\n createcursorsource,\n createinfinitesource,\n createlocalsource,\n createpagesource,\n anypagination,\n cursorpagination,\n cursorquery,\n cursorquerypatch,\n cursorresult,\n cursorsource,\n cursorsourceconfig,\n infinitepagination,\n infiniteloadquery,\n infinitequery,\n infinitequerypatch,\n infinitesource,\n infinitesourceconfig,\n localquery,\n localquerypatch,\n localsource,\n localsourceconfig,\n loadcontext,\n pagepagination,\n pagequery,\n pagequerypatch,\n pageresult,\n pagesource,\n pagesourceconfig,\n source,\n sourcesnapshot,\n ]\nenvironments: [browser, node, ssr, deno]\n \n\n<! markdownlint disable md025 md033 md060 >\n\n<packagehero package=\"sourcerer\" />\n\n## why sourcerer?\n\nlists often combine pagination, search, request cancellation, and render state. sourcerer gives local arrays and remote loaders one snapshot contract while leaving caching, retries, and transport policy to your application.\n\n```ts\nimport { createpagesource } from '@vielzeug/sourcerer';\n\ntype user = { id: number; name: string };\n\n// before: query changes can mix old items with new loading and page state.\nlet items: user[] = [];\nlet page = 1;\nlet isloading = false;\n\n// after: one source publishes internally consistent loaded state.\nconst source = createpagesource<user>({\n autostart: false,\n load: async () => ({ data: [{ id: 1, name: 'ada' }], total: 1 }),\n});\nsource.subscribe((snapshot) => console.log(snapshot.data));\nsource.dispose();\n```\n\n| feature | sourcerer | manual list state | courier query cache |\n| | | | |\n| bundle size | <packageinfo package=\"sourcerer\" type=\"size\" /> | application defined | <packageinfo package=\"courier\" type=\"size\" /> |\n| zero runtime dependencies | <ore icon name=\"check\" size=\"16\"></ore icon> | <ore icon name=\"check\" size=\"16\"></ore icon> | <ore icon name=\"x\" size=\"16\"></ore icon> |\n| local and remote collections | <ore icon name=\"check\" size=\"16\"></ore icon> | application defined | <ore icon name=\"triangle alert\" size=\"16\"></ore icon> |\n| cursor and infinite pagination | <ore icon name=\"check\" size=\"16\"></ore icon> | application defined | <ore icon name=\"triangle alert\" size=\"16\"></ore icon> |\n| latest request cancellation | <ore icon name=\"check\" size=\"16\"></ore icon> | application defined | transport level |\n\n<div class=\"decision callout\">\n\n**use sourcerer when** one ui collection needs local or remote pagination with an explicit, framework independent snapshot contract.\n\n**consider courier alone when** you only need cached http queries and pagination state belongs elsewhere.\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, then dispose it with its owner.\n\n```ts\nimport { createpagesource } from '@vielzeug/sourcerer';\n\ntype user = { id: number; name: string };\n\nconst source = createpagesource<user>({\n autostart: false,\n load: async ({ query }) => {\n const users = [\n { id: 1, name: 'ada' },\n { id: 2, name: 'grace' },\n { id: 3, name: 'linus' },\n ];\n const start = (query.page 1) * query.pagesize;\n\n return { data: users.slice(start, start + query.pagesize), total: users.length };\n },\n});\n\ntry {\n await source.reload();\n console.log(source.snapshot.data);\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()` — synchronous search and numbered pagination over an array\n `createpagesource()` — numbered remote pages with latest request cancellation\n `createcursorsource()` — sequential opaque cursor navigation\n `createinfinitesource()` — append only page loading\n `sourcesnapshot` — loaded `query`, `data`, and `pagination` plus optional `pendingquery`\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/) — use as transport, caching, and retry policy inside a page loader\n [scout](/scout/) — adapt an indexed search matcher for local sources\n [ripple](/ripple/) — project source snapshots into reactive application state\n [wayfinder](/wayfinder/) — validate and synchronize page query fields with route state\n\n</div>\n\n<! markdownlint enable md025 md033 md060 >\n",
|
|
1225
|
+
"api": " \ntitle: sourcerer — api reference\ndescription: public api for @vielzeug/sourcerer.\n \n\n[[toc]]\n\n## api overview\n\n| symbol | purpose | execution | common gotcha |\n| | | | |\n| `createlocalsource()` | in memory search and pagination | sync | prepare filtering and ranking before `setdata()` |\n| `createpagesource()` | numbered async pages | async | `query` remains loaded state while `pendingquery` is active |\n| `createcursorsource()` | cursor based async pages | async | `after` and `before` cannot coexist |\n| `createinfinitesource()` | appended async pages | async | `loadmore()` does nothing while fetching or exhausted; `pendingquery` set only on query replace, not append |\n| `sourcesnapshot` | atomic loaded state plus pending request | type | read `pendingquery` for newer in flight state |\n\n## package entry point\n\n| import | purpose |\n| | |\n| `@vielzeug/sourcerer` | factories and public types |\n\n## factories\n\n### `createlocalsource()`\n\n```ts\nfunction createlocalsource<t>(data: readonly t[], config?: localsourceconfig<t>): localsource<t>\n```\n\ncreates a synchronous source over an in memory collection.\n\n| option | type | description |\n| | | |\n| `initialquery` | `localquerypatch` | initial page, page size, or search value |\n| `match` | `(item, search) => boolean` | explicit search predicate |\n\n**returns:** `localsource<t>`.\n\n```ts\nimport { createlocalsource } from '@vielzeug/sourcerer';\n\nconst users = createlocalsource(\n [{ id: 1, name: 'ada' }],\n {\n initialquery: { pagesize: 20 },\n match: (user, search) => user.name.tolowercase().includes(search.tolowercase()),\n },\n);\n\nusers.setquery({ search: 'ada' });\n```\n\n \n\n### `createpagesource()`\n\n```ts\nfunction createpagesource<t, tfilter = unknown, tsort = unknown>(\n config: pagesourceconfig<t, tfilter, tsort>,\n): pagesource<t, tfilter, tsort>\n```\n\ncreates a numbered source. new queries abort older work. loaded state stays in `snapshot`; newer work appears in `snapshot.pendingquery`.\n\n| option | type | description |\n| | | |\n| `autostart` | `boolean` | start initial request; default `true` |\n| `initialquery` | `pagequerypatch<tfilter, tsort>` | initial query values |\n| `load` | `(context) => promise<pageresult<t>>` | transport callback |\n\n**returns:** `pagesource<t, tfilter, tsort>`.\n\n```ts\nimport { createpagesource } from '@vielzeug/sourcerer';\n\nconst users = createpagesource({\n autostart: false,\n load: async () => ({ data: [{ id: 1, name: 'ada' }], total: 1 }),\n});\n\nawait users.setquery({ page: 1 });\nusers.dispose();\n```\n\n \n\n### `createcursorsource()`\n\n```ts\nfunction createcursorsource<t, tcursor = string>(\n config: cursorsourceconfig<t, tcursor>,\n): cursorsource<t, tcursor>\n```\n\ncreates a sequential cursor source. search and page size changes reset cursors.\n\n**returns:** `cursorsource<t, tcursor>`.\n\n```ts\nimport { createcursorsource } from '@vielzeug/sourcerer';\n\nconst orders = createcursorsource({\n autostart: false,\n load: async () => ({ data: ['order 1'] }),\n});\n\nawait orders.reload();\nawait orders.page.next();\norders.dispose();\n```\n\n \n\n### `createinfinitesource()`\n\n```ts\nfunction createinfinitesource<t>(config: infinitesourceconfig<t>): infinitesource<t>\n```\n\ncreates an append only source. query changes replace loaded collection after successful first page load.\n\n**returns:** `infinitesource<t>`.\n\n```ts\nimport { createinfinitesource } from '@vielzeug/sourcerer';\n\nconst feed = createinfinitesource({\n autostart: false,\n load: async () => ({ data: ['post 1'], total: 1 }),\n});\n\nawait feed.loadmore();\nfeed.dispose();\n```\n\n## types\n\n### source primitives\n\n```ts\ntype disposable = {\n [symbol.dispose](): void;\n readonly disposalsignal: abortsignal;\n dispose(): void;\n readonly disposed: boolean;\n};\n\ntype sourcesnapshot<t, tquery, tpagination extends anypagination = anypagination> = readonly<{\n data: readonly t[];\n error: error | null;\n isfetching: boolean;\n pagination: tpagination;\n pendingquery?: tquery;\n query: tquery;\n}>;\n\ntype source<t, tquery, tpagination extends anypagination = anypagination> = disposable & {\n readonly snapshot: sourcesnapshot<t, tquery, tpagination>;\n subscribe(listener: (snapshot: sourcesnapshot<t, tquery, tpagination>) => void): () => void;\n};\n```\n\n### numbered pages\n\n```ts\ntype pagepagination = readonly<{\n count: number;\n hasnext: boolean;\n hasprevious: boolean;\n index: number;\n kind: 'page';\n size: number;\n total: number;\n}>;\n\ntype pagequery<tfilter = unknown, tsort = unknown> = readonly<{\n filter?: tfilter;\n page: number;\n pagesize: number;\n search: string;\n sort?: tsort;\n}>;\n\ntype pagequerypatch<tfilter = unknown, tsort = unknown> = readonly<{\n filter?: tfilter | undefined;\n page?: number;\n pagesize?: number;\n search?: string;\n sort?: tsort | undefined;\n}>;\n\ntype pageresult<t> = readonly<{ data: readonly t[]; total: number }>;\ntype loadcontext<tquery> = readonly<{ query: tquery; signal: abortsignal }>;\n\ntype pagesourceconfig<t, tfilter = unknown, tsort = unknown> = readonly<{\n autostart?: boolean;\n initialquery?: pagequerypatch<tfilter, tsort>;\n load(context: loadcontext<pagequery<tfilter, tsort>>): promise<pageresult<t>>;\n}>;\n\ntype pagesource<t, tfilter = unknown, tsort = unknown> = source<t, pagequery<tfilter, tsort>, pagepagination> & {\n readonly page: readonly<{\n go(index: number): promise<void>;\n last(): promise<void>;\n next(): promise<void>;\n previous(): promise<void>;\n }>;\n reload(): promise<void>;\n setquery(changes: pagequerypatch<tfilter, tsort>): promise<void>;\n};\n```\n\n### local sources\n\n```ts\ntype localquery = readonly<{ page: number; pagesize: number; search: string }>;\ntype localquerypatch = readonly<{ page?: number; pagesize?: number; search?: string }>;\ntype localsourceconfig<t> = readonly<{\n initialquery?: localquerypatch;\n match?: (item: t, search: string) => boolean;\n}>;\n\ntype localsource<t> = source<t, localquery, pagepagination> & {\n readonly page: readonly<{\n go(index: number): void;\n last(): void;\n next(): void;\n previous(): void;\n }>;\n setdata(data: readonly t[]): void;\n setquery(changes: localquerypatch): void;\n};\n```\n\n### cursor and infinite sources\n\n```ts\ntype cursorpagination<tcursor = string> = readonly<{\n hasnext: boolean;\n hasprevious: boolean;\n kind: 'cursor';\n nextcursor?: tcursor;\n previouscursor?: tcursor;\n total?: number;\n}>;\n\ntype cursorquery<tcursor = string> = readonly<{\n after?: tcursor;\n before?: tcursor;\n pagesize: number;\n search: string;\n}>;\n\ntype cursorquerypatch<tcursor = string> = readonly<{\n after?: tcursor | undefined;\n before?: tcursor | undefined;\n pagesize?: number;\n search?: string;\n}>;\n\ntype cursorresult<t, tcursor = string> = readonly<{\n data: readonly t[];\n nextcursor?: tcursor;\n previouscursor?: tcursor;\n total?: number;\n}>;\n\ntype cursorsourceconfig<t, tcursor = string> = readonly<{\n autostart?: boolean;\n initialquery?: cursorquerypatch<tcursor>;\n load(context: loadcontext<cursorquery<tcursor>>): promise<cursorresult<t, tcursor>>;\n}>;\n\ntype cursorsource<t, tcursor = string> = source<t, cursorquery<tcursor>, cursorpagination<tcursor>> & {\n readonly page: readonly<{ next(): promise<void>; previous(): promise<void> }>;\n reload(): promise<void>;\n setquery(changes: cursorquerypatch<tcursor>): promise<void>;\n};\n\ntype infinitepagination = readonly<{\n hasmore: boolean;\n kind: 'infinite';\n loaded: number;\n total: number;\n}>;\n\ntype infinitequery = readonly<{ pagesize: number; search: string }>;\ntype infinitequerypatch = readonly<{ pagesize?: number; search?: string }>;\ntype infiniteloadquery = readonly<{ page: number; pagesize: number; search: string }>;\n\ntype infinitesourceconfig<t> = readonly<{\n autostart?: boolean;\n initialquery?: infinitequerypatch;\n load(context: loadcontext<infiniteloadquery>): promise<pageresult<t>>;\n}>;\n\ntype infinitesource<t> = source<t, infinitequery, infinitepagination> & {\n loadmore(): promise<void>;\n reload(): promise<void>;\n setquery(changes: infinitequerypatch): promise<void>;\n};\n```\n\n### shared helpers\n\n```ts\ntype anypagination = cursorpagination<unknown> | infinitepagination | pagepagination;\n```\n",
|
|
1226
|
+
"usage": " \ntitle: sourcerer — usage guide\ndescription: build local, page, cursor, and infinite collection sources.\n \n\n[[toc]]\n\n## basic usage\n\nuse a local source when data already exists in memory.\n\n```ts\nimport { createlocalsource } from '@vielzeug/sourcerer';\n\nconst source = createlocalsource(\n [\n { id: 1, name: 'ada' },\n { id: 2, name: 'grace' },\n { id: 3, name: 'linus' },\n ],\n {\n initialquery: { pagesize: 2 },\n match: (user, search) => user.name.tolowercase().includes(search.tolowercase()),\n },\n);\n\nsource.setquery({ search: 'a' });\nconsole.log(source.snapshot.data);\nsource.dispose();\n```\n\nread `snapshot.query`, `snapshot.data`, and `snapshot.pagination` together. they always describe one loaded result.\n\n## handle pending remote queries\n\nuse `pendingquery` to distinguish loaded data from newer work.\n\n```ts\nimport { createpagesource } from '@vielzeug/sourcerer';\n\nconst source = createpagesource<string>({\n autostart: false,\n load: async ({ query }) => {\n const data = ['ada', 'grace', 'linus'];\n const start = (query.page 1) * query.pagesize;\n\n return { data: data.slice(start, start + query.pagesize), total: data.length };\n },\n});\n\nsource.subscribe((snapshot) => {\n if (snapshot.pendingquery) console.log('loading:', snapshot.pendingquery);\n console.log('loaded:', snapshot.query, snapshot.data);\n});\n\nawait source.setquery({ page: 2 });\nsource.dispose();\n```\n\nnew `setquery()` calls abort older requests. a failed current request preserves prior loaded data, records `snapshot.error`, and rejects the returned promise.\n\n## use cursor pagination\n\nuse cursors when an api cannot provide stable page numbers.\n\n```ts\nimport { createcursorsource } from '@vielzeug/sourcerer';\n\nconst rows = ['a', 'b', 'c', 'd'];\nconst source = createcursorsource<string, number>({\n autostart: false,\n initialquery: { pagesize: 2 },\n load: async ({ query }) => {\n const start = query.after ?? 0;\n const data = rows.slice(start, start + query.pagesize);\n const nextcursor = start + data.length;\n\n return {\n data,\n nextcursor: nextcursor < rows.length ? nextcursor : undefined,\n previouscursor: start > 0 ? math.max(0, start query.pagesize) : undefined,\n };\n },\n});\n\nawait source.reload();\nawait source.page.next();\nconsole.log(source.snapshot.data);\nsource.dispose();\n```\n\n`after` and `before` cannot coexist. search or page size changes reset cursor state.\n\n## build an infinite feed\n\nuse an infinite source when each page should append.\n\n```ts\nimport { createinfinitesource } from '@vielzeug/sourcerer';\n\nconst source = createinfinitesource<number>({\n autostart: false,\n initialquery: { pagesize: 2 },\n load: async ({ query }) => {\n const values = [1, 2, 3, 4, 5];\n const start = (query.page 1) * query.pagesize;\n\n return { data: values.slice(start, start + query.pagesize), total: values.length };\n },\n});\n\nawait source.loadmore();\nawait source.loadmore();\nconsole.log(source.snapshot.data);\nsource.dispose();\n```\n\n`loadmore()` is a no op while fetching or after `pagination.hasmore` becomes false.\n\n## testing and debugging\n\ninject deterministic loaders in unit tests. await source commands before reading final state.\n\n```ts\nimport { expect, it } from 'vitest';\nimport { createpagesource } from '@vielzeug/sourcerer';\n\nit('loads first page', async () => {\n const source = createpagesource({\n autostart: false,\n load: async () => ({ data: ['ada'], total: 1 }),\n });\n\n await source.reload();\n expect(source.snapshot.data).toequal(['ada']);\n source.dispose();\n});\n```\n\n## framework integration\n\nsubscribe through each framework’s lifecycle. keep source creation stable across renders.\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 () => ({ data: [{ id: 1, name: 'ada' }], total: 1 }) }),\n [],\n );\n const snapshot = usesyncexternalstore(source.subscribe, () => source.snapshot);\n\n useeffect(() => () => source.dispose(), [source]);\n\n return <p>{snapshot.isfetching ? 'loading' : snapshot.data.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 () => ({ data: [{ id: 1, name: 'ada' }], total: 1 }) });\nconst snapshot = shallowref(source.snapshot);\nconst stop = source.subscribe((next) => (snapshot.value = next));\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 () => ({ data: [{ id: 1, name: 'ada' }], total: 1 }) });\n let snapshot = source.snapshot;\n const stop = source.subscribe((next) => (snapshot = next));\n\n ondestroy(() => {\n stop();\n source.dispose();\n });\n</script>\n\n{#if snapshot.isfetching}loading{/if}\n{#each snapshot.data as user}{user.name}{/each}\n```\n\n:::\n\n## working with other vielzeug libraries\n\nuse courier for transport policy. sourcerer owns request succession; courier owns http behavior.\n\n```ts\nimport { createcourier } from '@vielzeug/courier';\nimport { createpagesource } from '@vielzeug/sourcerer';\n\nconst courier = createcourier({ baseurl: '/api' });\nconst source = createpagesource({\n load: ({ query, signal }) => courier.get('/users', { query, signal }),\n});\n```\n\nuse scout’s matcher when local search needs an index.\n\n```ts\nimport { createindex, tosearchmatcher } from '@vielzeug/scout';\nimport { createlocalsource } from '@vielzeug/sourcerer';\n\nconst users = [{ name: 'ada' }, { name: 'grace' }];\nconst index = createindex(users, { fields: ['name'] });\nconst source = createlocalsource(users, { match: tosearchmatcher(index) });\n```\n\n## gotchas\n\n### navigation methods are no ops while fetching\n\n`page.go()`, `page.next()`, `page.previous()`, `page.last()` (page and cursor sources) and `loadmore()` (infinite source) return a resolved promise and change nothing while a request is in flight. this prevents queued navigation from racing with abort logic. if a user clicks \"next\" during a fetch, the click is lost — debounce or disable navigation controls while `snapshot.isfetching` is true.\n\n### `pendingquery` means a different query is in flight\n\nfor page and cursor sources, `pendingquery` is always set when a new query is loading. for infinite sources, `pendingquery` is set only when `setquery()` or `reload()` replaces the query — not during `loadmore()` append fetches. to detect an append in progress on an infinite source, read `snapshot.isfetching` with `pendingquery` absent.\n\n### `samequery` uses reference equality\n\n`setquery()` compares the new query against the current one using `object.is` per field. for `filter` and `sort` (opaque `tfilter`/`tsort` types), a new object with the same content triggers a refetch. memoize filter/sort objects in your application layer if you want to avoid redundant requests.\n\n### clear optional query fields by passing `undefined` explicitly\n\nin `pagequerypatch`, omitting `filter` preserves the current value; passing `filter: undefined` clears it. the same applies to `sort`. in `cursorquerypatch`, omitting `after`/`before` preserves the current cursor; passing `after: undefined` or `before: undefined` clears it. this distinction is runtime only — typescript's optional field syntax does not distinguish \"absent\" from \"explicitly `undefined`.\"\n\n```ts\n// page source: keep current filter, change page:\nsource.setquery({ page: 2 });\n\n// page source: clear filter, reset to page 1:\nsource.setquery({ filter: undefined });\n\n// cursor source: clear after cursor:\nsource.setquery({ after: undefined });\n```\n\n## best practices\n\n dispose each source with its owning view, request, or scope.\n read one snapshot object per render instead of mixing source fields across updates.\n inspect `pendingquery` before rendering controls for in flight work.\n validate url query values before passing them to `setquery()`.\n keep caching, retries, polling, and optimistic writes in your transport layer.\n use `setdata()` with prepared local collections; keep ranking and filtering explicit.\n debounce text inputs before updating remote source queries.\n",
|
|
1103
1227
|
"examples": " \ntitle: sourcerer — examples\ndescription: recipes for local, page, cursor, infinite, and framework source usage.\n \n\n## examples\n\n [local pagination and search](./examples/local pagination and filtering.md)\n [page query 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"
|
|
1104
1228
|
},
|
|
1105
1229
|
"examples": [
|
|
@@ -1128,12 +1252,12 @@
|
|
|
1128
1252
|
"text": "page source import { createpagesource } from '@vielzeug/sourcerer'\n\nconst allitems = array.from({ length: 47 }, (_, index) => ({ id: index + 1, name: `item ${index + 1}` }))\n\nconst source = createpagesource({\n initialquery: { pagesize: 10 },\n load: async ({ query }) => {\n const filtered = query.search ? allitems.filter((item) => item.name.includes(query.search)) : allitems\n const start = (query.page 1) * query.pagesize\n return { data: filtered.slice(start, start + query.pagesize), total: filtered.length }\n },\n})\n\nawait source.reload()\nawait source.setquery({ search: 'item 4' })\nconsole.log(source.snapshot.data.map((item) => item.name))\nconsole.log(source.snapshot.pagination)\n\nsource.dispose()"
|
|
1129
1253
|
}
|
|
1130
1254
|
],
|
|
1131
|
-
"exports": "createcursorsource createinfinitesource createlocalsource createpagesource anypagination cursorpagination cursorquery cursorquerypatch cursorresult cursorsource cursorsourceconfig infinitepagination infinitequery infinitequerypatch infinitesource infinitesourceconfig localquery localquerypatch localsource localsourceconfig loadcontext pagepagination pagequery pagequerypatch pageresult pagesource pagesourceconfig source sourcesnapshot",
|
|
1255
|
+
"exports": "createcursorsource createinfinitesource createlocalsource createpagesource anypagination cursorpagination cursorquery cursorquerypatch cursorresult cursorsource cursorsourceconfig infinitepagination infiniteloadquery infinitequery infinitequerypatch infinitesource infinitesourceconfig localquery localquerypatch localsource localsourceconfig loadcontext pagepagination pagequery pagequerypatch pageresult pagesource pagesourceconfig source sourcesnapshot",
|
|
1132
1256
|
"keywords": "pagination data source cursor infinite scroll search",
|
|
1133
1257
|
"name": "@vielzeug/sourcerer",
|
|
1134
1258
|
"related": "courier ripple scout wayfinder",
|
|
1135
1259
|
"slug": "sourcerer",
|
|
1136
|
-
"source": "export { createcursorsource } from './cursorsource';\nexport { createinfinitesource } from './infinitesource';\nexport { createlocalsource } from './localsource';\nexport { createpagesource } from './pagesource';\nexport type {\n anypagination,\n cursorpagination,\n cursorquery,\n cursorquerypatch,\n cursorresult,\n cursorsource,\n cursorsourceconfig,\n infinitepagination,\n infinitequery,\n infinitequerypatch,\n infinitesource,\n infinitesourceconfig,\n loadcontext,\n localquery,\n localquerypatch,\n localsource,\n localsourceconfig,\n pagepagination,\n pagequery,\n pagequerypatch,\n pageresult,\n pagesource,\n pagesourceconfig,\n source,\n sourcesnapshot,\n} from './types';\n"
|
|
1260
|
+
"source": "export { createcursorsource } from './cursorsource';\nexport { createinfinitesource } from './infinitesource';\nexport { createlocalsource } from './localsource';\nexport { createpagesource } from './pagesource';\nexport type {\n anypagination,\n cursorpagination,\n cursorquery,\n cursorquerypatch,\n cursorresult,\n cursorsource,\n cursorsourceconfig,\n infiniteloadquery,\n infinitepagination,\n infinitequery,\n infinitequerypatch,\n infinitesource,\n infinitesourceconfig,\n loadcontext,\n localquery,\n localquerypatch,\n localsource,\n localsourceconfig,\n pagepagination,\n pagequery,\n pagequerypatch,\n pageresult,\n pagesource,\n pagesourceconfig,\n source,\n sourcesnapshot,\n} from './types';\n"
|
|
1137
1261
|
},
|
|
1138
1262
|
{
|
|
1139
1263
|
"category": "validation",
|