@vielzeug/codex 26.10.5 → 26.10.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/data/catalog.json +81 -42
- package/data/llms-full.txt +1472 -18
- package/data/llms.txt +2 -1
- package/data/manifest.json +1 -1
- package/data/packages/archive.json +76 -0
- package/data/packages/arsenal.json +18 -4
- package/data/packages/courier.json +3 -3
- package/data/packages/prism.json +55 -52
- package/data/packages/refine.json +2 -2
- package/data/packages/tandem.json +1 -1
- package/data/refine.json +3872 -3722
- package/data/search.json +35 -14
- package/package.json +2 -2
- package/skills/vielzeug/SKILL.md +2 -1
package/data/search.json
CHANGED
|
@@ -1,12 +1,29 @@
|
|
|
1
1
|
[
|
|
2
|
+
{
|
|
3
|
+
"category": "utilities",
|
|
4
|
+
"description": "community content publishing for cloudflare workers + d1: a hardened worker core over per collection payload schemas",
|
|
5
|
+
"docs": {
|
|
6
|
+
"index": " \ntitle: archive\ndescription: \"community content publishing for cloudflare workers + d1: a hardened worker core over per collection payload schemas\"\npackage: archive\ncategory: utilities\nkeywords: [cloudflare, worker, d1, community, publishing, content, catalog, gdpr]\nrelated: [spell, courier, rune]\nexports: [definearchivecollection, createarchiveworker, createauthworker, archiveauthhealthschema, archivederivationerror]\nenvironments: [browser, node]\n \n\n<! markdownlint disable md025 md033 md060 >\n\n<packagehero package=\"archive\" />\n\n## why archive?\n\nyou can share content entries with ranked browsing, filters, likes and account\nerasure on cloudflare workers + d1. `definearchivecollection` supplies your json\npayload schema and indexing rules; the worker supplies sessions, guards,\nidempotent publishing, cursors and rate limits.\n\n```ts\n// before: each deployment repeats validation, indexing and route plumbing.\n\n// after: the deployment is one collection definition plus one factory call.\nimport { definearchivecollection } from '@vielzeug/archive';\nimport { createarchiveworker } from '@vielzeug/archive/worker';\nimport { s } from '@vielzeug/spell';\n\nconst notes = definearchivecollection({\n name: 'notes', contentversion: 1,\n payloadschema: s.object({ title: s.string().min(1).max(128) }),\n index: ({ title }) => ({ title, filter: null, tags: [] }),\n});\nexport default createarchiveworker([notes]);\n```\n\n| feature | archive | hand rolled worker | baas (firebase/supabase) |\n| | | | |\n| bundle size (core) | <packageinfo package=\"archive\" type=\"size\" /> | depends on deployment code | depends on selected sdk |\n| zero dependencies | no — spell and jose | depends on implementation | no — client sdk |\n| content knowledge in the core | <ore icon name=\"check\" size=\"16\"></ore icon> none — one collection seam | <ore icon name=\"x\" size=\"16\"></ore icon> baked in | <ore icon name=\"x\" size=\"16\"></ore icon> rules everywhere |\n| canonical fresh database d1 schema | <ore icon name=\"check\" size=\"16\"></ore icon> in the package | deployment owned | not a d1 backend |\n| idempotent publish w/ tombstones | <ore icon name=\"check\" size=\"16\"></ore icon> built in | <ore icon name=\"x\" size=\"16\"></ore icon> rebuild it | <ore icon name=\"x\" size=\"16\"></ore icon> rebuild it |\n| gdpr erasure / export endpoints | <ore icon name=\"check\" size=\"16\"></ore icon> built in | <ore icon name=\"x\" size=\"16\"></ore icon> rebuild it | <ore icon name=\"triangle alert\" size=\"16\"></ore icon> partial |\n| strict dto re validation on read | <ore icon name=\"check\" size=\"16\"></ore icon> every entry | <ore icon name=\"x\" size=\"16\"></ore icon> optional | <ore icon name=\"x\" size=\"16\"></ore icon> optional |\n\n<div class=\"decision callout\">\n\n**use archive when** an app needs a shareable, ranked, liked community space for\ncontent entries on cloudflare workers + d1, and can express its content as one strict\npayload schema.\n**consider alternatives when** you need rich queries beyond filter/tag/search, live\ncollaboration, or a backend you do not control — archive is deliberately one narrow,\nhardened surface.\n\n</div>\n\n## installation\n\n::: code group\n\n```sh [pnpm]\npnpm add @vielzeug/archive @vielzeug/spell\n```\n\n```sh [npm]\nnpm install @vielzeug/archive @vielzeug/spell\n```\n\n```sh [yarn]\nyarn add @vielzeug/archive @vielzeug/spell\n```\n\n:::\n\n## quick start\n\na deployment is one collection definition plus one factory call. the definition\nbrings the payload schema and what to index; everything else — the emitted entry\nenvelope, the content hash, the wire contract, the routes — is composed for you.\n\n```ts\nimport { definearchivecollection } from '@vielzeug/archive';\nimport { createarchiveworker } from '@vielzeug/archive/worker';\nimport { s } from '@vielzeug/spell';\n\n// the collection's payload: one strict schema is the whole content contract.\nconst payloadschema = s.object({\n cuisine: s.enum(['italian', 'japanese']),\n ingredients: s.array(s.string().trim().min(1).max(64)).min(1).max(20),\n name: s.string().trim().min(1).max(32),\n});\n\nconst recipescollection = definearchivecollection({\n canonicalize: (payload) => ({ ...payload, ingredients: [...payload.ingredients].sort() }),\n contentversion: 1,\n filters: ['italian', 'japanese'],\n index: (payload) => ({\n filter: payload.cuisine,\n tags: [payload.ingredients.length <= 5 ? 'quick' : 'involved'],\n title: payload.name,\n }),\n name: 'recipes',\n payloadschema,\n tags: ['quick', 'involved'],\n});\n\n// the whole worker: routing, auth, guards, limits, sql and dto disciplines included.\nexport default createarchiveworker([recipescollection]);\n```\n\nthe browser side never imports worker machinery. the same collection's composed\nschemas parse what the routes emit:\n\n```ts\nimport { definearchivecollection } from '@vielzeug/archive';\nimport { s } from '@vielzeug/spell';\n\nconst notes = definearchivecollection({\n name: 'notes', contentversion: 1,\n payloadschema: s.object({ title: s.string().min(1).max(128) }),\n index: ({ title }) => ({ title, filter: null, tags: [] }),\n});\nconst response = await fetch('/api/v1/collections/notes/entries?sort=liked');\nif (!response.ok) throw new error('archive unavailable');\nconst parsed = notes.contract.page.parse(await response.json());\nconsole.log(parsed.entries);\n```\n\n## features\n\n<div class=\"features grid\">\n\n `definearchivecollection` — the one content seam: payload schema, canonicalization, filter/tag/title derivation, union typed vocabularies\n `createarchiveworker` — the whole routes/auth/limits/sql stack from registered collections\n `collection.contract` — entry, page, publish/update input schemas shared by client and worker\n `archive_limits` + `archivehealthschema` — the application bounds, advertised by health and validated on both sides\n `archiveauthhealthschema` + `auth_schema_version` — typed issuer readiness checked against the issuer ddl stamp\n `archiveerrorresponseschema` — the one error envelope, code typed across the wire\n `createauthworker` — scanner safe confirmation, live session version enforcement, durable budgets, metadata export and coordinated erasure\n `auth_issuer_service` — required uncached issuer check after local catalog jwt verification\n `archivederivationerror` — distinguishes faulty collection code from invalid submitted content\n moderation — reader reports, the operator soft hide and the report queue, keyed on a verified `roles` claim\n `worker.tap()` / `issuer.tap()` — typed sanitized runtime events; neither core logs by default\n `migrations/` — canonical fresh database ddl with the storage stamp health reads back; no upgrade framework\n\n</div>\n\n## documentation\n\n<div class=\"doc links\">\n\n [usage guide](./usage.md) — stand up a collection, deploy the worker, wire the client.\n [api reference](./api.md) — every export, all three entry points, the full route table.\n [examples](./examples.md) — recipes from first deployment to gdpr erasure.\n [release status](./migration.md) — unreleased package and fresh database scope.\n\n</div>\n\n## see also\n\n<div class=\"see also\">\n\n [spell](/spell/) — every payload schema here is a spell schema; the core composes envelopes from it.\n [courier](/courier/) — the http client shape an app's frontend mirrors to consume these routes.\n [rune](/rune/) — the structured logger a deployment points `worker.tap()` events at.\n\n</div>\n\n<! markdownlint enable md060 md033 md025 >\n",
|
|
7
|
+
"api": " \ntitle: \"archive: api reference\"\ndescription: \"collection definitions, browser safe wire schemas, worker deployment, and identity issuance.\"\n \n\n[[toc]]\n\n## api overview\n\n| symbol | purpose | execution mode | common gotcha |\n| | | | |\n| `definearchivecollection` | compose validation, indexing, identity and wire schemas | sync | invalid content derives to `null`; callback faults throw |\n| `createarchiveworker` | build the catalog worker | sync | collection names must be unique |\n| `createauthworker` | build the passwordless issuer | sync | `auth_archive` is required |\n| `worker.tap`, `issuer.tap` | observe sanitized runtime outcomes | sync | neither core logs by default; observer exceptions are swallowed |\n| `archivederivationerror` | identify a collection implementation/runtime fault | sync | faults become sanitized 500s, not content validation 422s |\n| `archive_limits` | shared application bounds | sync | page bounds do not cap total exports |\n| `archiveerrorresponseschema`, `archiveauthhealthschema` | validate error and issuer health responses | sync | unknown error codes and health states reject |\n| `archiveerror`, `archivedefinitionerror` | public base and invalid definition errors | sync | thrown locally, not transmitted as error objects |\n| `sessioncookievalue` | compose shared session cookie attributes | sync | deployments must choose `secure` from their environment |\n| `iswellformedtext` | check control characters and lone surrogates | sync | does not trim or impose a length bound |\n\n## package entry point\n\n| import | purpose |\n| | |\n| `@vielzeug/archive` | browser safe collections, constants, schemas and errors |\n| `@vielzeug/archive/worker` | catalog factory, issuer session service binding and event types |\n| `@vielzeug/archive/auth` | issuer factory, operator lifetime and binding types |\n\n## collections\n\n### `definearchivecollection`\n\ncreates a frozen collection and its contract; malformed names, versions or vocabularies throw `archivedefinitionerror`.\n\n```ts\nfunction definearchivecollection<\n p extends object, const c extends number, const f extends string = never, const t extends string = never,\n>(definition: archivecollectiondefinition<p, c, f, t>): archivecollection<p, c>;\n```\n\n| parameter | contract |\n| | |\n| `name` | lowercase `[a z0 9 ]{1,32}` |\n| `contentversion` | positive safe integer |\n| `payloadschema` | spell schema whose output is a stable json object |\n| `filters`, `tags` | optional readonly arrays; omitted means empty vocabulary |\n| `canonicalize` | optional pure, idempotent, schema valid normalization |\n| `index` | derive `filter`, `tags` and searchable `title` without mutating the payload |\n\n**returns:** `archivecollection<p, c>`.\n\n**example:** define and derive one collection without worker bindings.\n\n```ts\nimport { definearchivecollection } from '@vielzeug/archive';\nimport { s } from '@vielzeug/spell';\n\nconst recipes = definearchivecollection({\n name: 'recipes',\n contentversion: 1,\n payloadschema: s.object({ name: s.string().min(1).max(32) }),\n index: (payload) => ({ filter: null, tags: [], title: payload.name }),\n});\nconst derived = await recipes.derive({ name: 'rice' });\nif (derived === null) throw new error('invalid recipe');\nconsole.log(derived.contenthash);\n```\n\n| method / property | result |\n| | |\n| `derive(raw)` | `promise<archivederivation \\| null>` |\n| `contract.entry` | public entry schema |\n| `contract.ownedentry`, `ownedentries` | owner view with `clientkey` and `hidden` |\n| `contract.page` | entries and forward/backward cursors |\n| `contract.publishinput`, `updateinput` | publish and revision guarded update request schemas |\n| `filters`, `tags` | frozen vocabulary snapshots |\n\n`derive` returns `null` for invalid json objects, schema invalid input, invalid titles, too many tags and oversized canonical payloads. unexpected schema, callback or hashing failures throw `archivederivationerror`. non idempotent or schema invalid canonicalization, unstable schema transformations, index mutation and out of vocabulary projections are also derivation faults. routes map `null` to 422 `invalid payload` and faults to a sanitized 500 `internal`.\nschema output that is lossy or no longer json is a derivation fault, not invalid\nsubmitted content. invalid index result shapes also throw.\n\nconst generics preserve the content version literal and infer filter/tag unions from vocabulary arrays without `as const`. `noinfer` prevents `index` from widening those unions; omitted arrays infer `never`, permitting only `filter: null` and empty tags.\n\nobjects serialize with recursively sorted keys; arrays preserve order unless your canonicalizer changes it. sha 256 hashes the core owned `{contentversion, payload}` envelope. response schemas require stored payloads to remain stable under parsing; a response validation fault has stage `response`.\n\n`filter`, tags and searchable title are persisted projections, not computed during reads. changing `index` or its vocabulary does not update stored rows. the active definition and persisted dataset must remain consistent; archive has no projection update or historical schema api.\n\n## worker and maintenance\n\n### `createarchiveworker`\n\nreturns the catalog request/scheduled handler and its observation side channel.\n\n```ts\nfunction createarchiveworker<e extends archiveworkerenv>(\n collections: readonly archivecollection[],\n): archiveworkerhandler<e>;\n```\n\n**returns:** `archiveworkerhandler<e>`; duplicate names throw `archivedefinitionerror` at registration.\n\n**example:** export a deployable worker using the package's structural bindings.\n\n```ts\nimport { definearchivecollection } from '@vielzeug/archive';\nimport { createarchiveworker, type archiveworkerenv } from '@vielzeug/archive/worker';\nimport { s } from '@vielzeug/spell';\n\nconst notes = definearchivecollection({\n name: 'notes', contentversion: 1,\n payloadschema: s.object({ title: s.string().min(1).max(128) }),\n index: ({ title }) => ({ title, filter: null, tags: [] }),\n});\nexport default createarchiveworker<archiveworkerenv>([notes]);\n```\n\n| method | contract |\n| | |\n| `fetch(request, env, ctx)` | returns `promise<response>` |\n| `scheduled(controller, env, ctx)` | sweeps expired retry keys and revocation tombstones; emits failures and rethrows |\n| `tap(handler, { signal? }?)` | returns unsubscribe; a pre aborted signal registers nothing |\n\n`auth_issuer_service` is a required `fetch` shaped service binding to the issuer.\nthe catalog verifies the signed jwt locally, then asks the issuer to validate its\ncurrent account session version. it does not cache successful issuer verdicts.\nif the service exchange fails or exceeds its fixed 5000 ms timeout (body decoding\nincluded), the catalog returns `503 auth unavailable`; the timeout does not cancel\nwork already started by the issuer.\n\nonly anonymous entry lists can use the 30 second edge cache. any `cookie` or `authorization` header disables caching, even if invalid or empty. detail, metadata, private and error responses are `no store`. public reads do not verify sessions.\n\n \n\n### `worker.tap`\n\nobserves typed request, scheduled and cache failure events without affecting request behavior.\n\n```ts\ntap(handler: (event: archiveevent) => void, options?: { readonly signal?: abortsignal }): () => void;\n```\n\n**returns:** an unsubscribe function; abort detaches the observer. diagnostics contain categories and trusted configuration field names, never raw errors, sql, subjects, aliases, tokens or urls.\n\n**example:** observe a worker and detach explicitly.\n\n```ts\nimport { createarchiveworker, type archiveworkerenv } from '@vielzeug/archive/worker';\n\nconst worker = createarchiveworker<archiveworkerenv>([]);\nconst stop = worker.tap((event) => console.log(json.stringify(event)));\nstop();\n```\n\n \n\n## identity issuer\n\n### `createauthworker`\n\nreturns the passwordless email issuer with account export and coordinated erasure.\n\n```ts\nfunction createauthworker<e extends authworkerenv>(): authworkerhandler<e>;\n```\n\n**returns:** `authworkerhandler<e>`.\n\n**example:** export an issuer configured through deployment bindings.\n\n```ts\nimport { createauthworker, type authworkerenv } from '@vielzeug/archive/auth';\n\nexport default createauthworker<authworkerenv>();\n```\n\n| method | contract |\n| | |\n| `fetch(request, env, ctx?)` | returns `promise<response>` |\n| `scheduled(controller, env, ctx?)` | removes expired links, closed send windows and unverified unreferenced accounts |\n| `tap(handler, { signal? }?)` | observes typed `authevent`; returns unsubscribe; pre aborted signals are inert |\n\nmagic links expire after 15 minutes and work once. the durable per email budget allows three sends per hour; consumption, expiration and send failure do not refund it. ordinary sessions last at most 30 days. operator sessions use `operator_session_max_age_seconds` (900 seconds). `operator_emails` controls newly minted role claims.\n\nthe issuer global admission budget is durable d1 state in `auth_sign_in_budget`:\n20 requests per 60 second window before account creation or email delivery.\nthis supplements, rather than replaces, the durable per email budget. no external\nrate limiter binding is required.\nadmission spends are not refunded after later database or email failures.\nrequest emails are trimmed and lowercased before validation. `sender_address`\nmust already be canonical; `operator_emails` entries are trimmed and lowercased\nbefore validation.\n\nsign in requests create pending account rows with `lastsigninat: null`. successful\nredemption atomically claims the one use link and stamps the account's sign in\ntime. the sweep deletes expired links and closed per user send windows, then\nunverified accounts with neither remaining links nor send budget references.\nverified accounts are retained until explicit account deletion.\n\n`@vielzeug/archive/auth` exports `operator_session_max_age_seconds = 900` alongside\nthe factory and issuer binding types.\n\n`auth_jwt_previous_public_jwks` optionally publishes retired public rs256 keys and verifies sessions minted before rotation; private members reject configuration. the package has no demo sign in routes.\n\nopening a magic link performs a safe get that displays a click confirmation page,\nnot redemption. only a marked post from an exact allowed origin or\n`public_origin` consumes `{ token }` and returns 204 with the session cookie. the\nconfirmation page navigates to `/signed in` after success; the post is not a\nredirect. confirmation responses are `no store` with no referrer and restrictive\ncsp; the page removes the token from the address bar and loads no external assets.\nget displays confirmation for any syntactically valid token, including a used\ntoken; only post establishes whether the link is still redeemable. post accepts\nexactly `{ token }` and rejects query parameters.\n\nthe issuer never logs directly. `tap` emits sanitized request and scheduled\noutcomes; observer failures are swallowed, abort detaches the observer and failed\nscheduled work emits then rethrows. `authdiagnostic` reports only a category.\n\n## http routes\n\nevery route below is mounted under `/api/v1`. mutation guards require an exact allowed `origin` and `x archive request: 1`. registered publishers pin `x archive publisher` to the identity captured when preparing private requests. the header is a freshness precondition, not authority.\n\n| route | contract |\n| | |\n| `get /health` | public catalog readiness; 200 `ok` or 503 `degraded` |\n| `post /sign out` | catalog local sign out; `{ ok: true }` and expired session cookie, without issuer access |\n| `get /collections/:collection` | collection metadata |\n| `get /collections/:collection/entries` | `author`, `filter`, `tag`, `search`, `sort`, `cursor`, `limit`; bidirectional entry page |\n| `post /collections/:collection/entries` | `{ clientkey, payload }`; 201 create or 200 replay |\n| `get /collections/:collection/entries/:id` | public entry; always `no store` |\n| `put /collections/:collection/entries/:id` | owner only `{ expectedrevision, payload }` |\n| `delete /collections/:collection/entries/:id` | owner or operator removal; 204 |\n| `put` / `delete /collections/:collection/entries/:id/like` | like / unlike visible entries only; guarded write and response; like state response |\n| `post /collections/:collection/entries/:id/report` | `{ reason }`; idempotent per entry/reporter; 204 |\n| `put` / `delete /collections/:collection/entries/:id/hidden` | operator hide / unhide; 204 |\n| `get /collections/:collection/reports` | operator page; `status=unresolved` (default) or `resolved`, `cursor`, `limit` |\n| `put /collections/:collection/reports/:id/resolved` | operator resolves report by its unique report id; idempotent 204 |\n| `delete /publishers/:id` | operator erases archive profile; not issuer account |\n| `get /moderation log` | operator forward page; `cursor`, `limit` |\n| `get` / `post` / `put /me` | identity, register with optional `{ name }`, rename with required `{ name }` |\n| `delete /me` | profile only cascade; 204; does not clear cookie |\n| `delete /me/account` | revoke issuer/subject and atomically erase profile; 204; coordinated issuer erasure seam |\n| `get /me/publications?collection=…` | owner entries including hidden state and retry keys |\n| `get /me/likes` | forward page `{ ids, cursor }`; `cursor`, `limit` |\n| `get /me/likes?ids=…` | subset lookup `{ ids }`; at most `likestateids` input ids |\n| `get /me/reports` | own forward report page across collections; `cursor`, `limit` |\n| `get /auth/health` | public issuer readiness; typed `authhealth`, 200 `ok` or 503 `degraded`; no email or peer probe |\n| `get /auth/session` | internal issuer session check for the catalog service binding; exact `{ ok: true }`, `no store`, no pii; 401 for an invalid, stale or absent account session |\n| `post /auth/sign out all` | requires marker, exact origin, session and captured `account_header`; atomically increments `session_version`, deletes outstanding links, preserves account and catalog data; 200 `{ ok: true }` with an expired cookie only on durable success |\n| `get /auth/jwks.json` | active and configured retired public verification keys |\n| `post /auth/sign in` | `{ email }` (trimmed and lowercased); 202 `{ ok: true }` |\n| `get /auth/verify?token=…` | safe no store click confirmation page; no token consumption or session change |\n| `post /auth/verify` | json `{ token }`, marker and exact allowed origin or `public_origin`; consume once; 204 with session cookie |\n| `get /auth/account` | `{ id, email, createdat, lastsigninat }`; no tokens or budget data |\n| `delete /auth/account` | requires `account_header` with the captured issuer account uuid; catalog `/me/account` via `auth_archive`, require exact 204, then issuer user and cookie clear; 204 |\n\nforward pages are newest first with opaque context bound keyset cursors. keep the same query context when following a cursor; end at `cursor: null`. `maxpagesize` bounds each page, not the total number of liked ids, reports or audit actions you can export. the subset like query is not a page and cannot combine `ids` with `cursor` or `limit`.\n\nfull erasure creates an issuer/subject revocation tombstone for 30 days plus ten\nseconds and deletes the profile in the same transaction. catalog authentication\nrejects that subject with 401 on ordinary private routes; registration also checks\nrevocation in sql to prevent resurrection races. only `delete /me/account` may\nbypass the issuer session lookup, and only to retry erasure with the original\nsigned session of the same principal while its existing revocation tombstone is\nvalid; jwt signature and claims are still verified. it is not a general\nstale session exception: initial erasure validates the issuer session. the\nissuer facing `delete /auth/account` always checks the current session version\nbefore calling the catalog, so retrying it after completed erasure returns 401\nbecause the account is absent.\ndirect profile only `delete /me` allows later registration using the same session.\nthe sweep removes expired tombstones. re enrollment through the reference issuer\ncreates a fresh account uuid subject; an external issuer must issue a new subject\nto re enroll during the revocation window.\n\nthe issuer stores `auth_users.session_version` as a safe non negative integer,\ndefaulting to `0`. every session jwt carries the current value as the required\n`sessionversion` claim. each ordinary issuer authenticated route compares that\nclaim to the current account row; a mismatch is unauthenticated. `post\n/api/v1/auth/sign out all` checks the captured issuer account uuid against the\nverified subject before atomically incrementing the version and deleting pending\nlinks. a changed account answers 409 `account changed`; a failed request does not\nclear the cookie. this revokes all sessions, including the caller's, after a\nsuccessful response. requests authorized before revocation may finish; there is\nno cross database transactional or in flight cancellation guarantee.\n\nthe catalog requires `auth_issuer_service`, a service binding to the issuer. after\nlocal jwt signature/claims verification, it requests\n`get /api/v1/auth/session` through that binding on every private request. only a\nstrictly validated 401 `unauthenticated` remains a 401. malformed, unexpected,\nprovider and 5xx responses become 503 `auth unavailable`; there is no authority\ncache. both service directions use a fixed 5000 ms timeout that includes body\ndecoding. the issuer's `/api/v1/auth/session` is an internal\nservice contract, not a frontend session api.\n\nissuer readiness is independent of catalog readiness. `get\n/api/v1/auth/health` returns `archiveauthhealthschema` as `authhealth`; its\nread only probe checks configuration, a signing and verification key proof,\nbinding shapes, the singleton `auth_schema_meta` stamp against\n`auth_schema_version`, and readability of expected tables, columns and triggers.\nit does not write, send email or probe the catalog service. responses are\nsanitized.\nconfiguration failure, database failure and schema mismatch return http 503\nwith `status: 'degraded'` and the corresponding reason.\n\neffective hide, unhide, operator removal and publisher erasure append audit rows atomically with the state transition. concurrent idempotent no ops append nothing. audit rows contain opaque ids, not names or content, and have no foreign keys so they outlive erasure. resolving a report sets `resolvedat`; it is not one of the four audit action kinds.\n\n## constants and schemas\n\n| export | value or parsed output |\n| | |\n| `archive_contract_version`, `archive_schema_version` | both debut at `1`; wire and storage versions are independent |\n| `auth_schema_version` | `1`; issuer ddl stamp checked by `/auth/health` |\n| `archive_collection_name_pattern` | `/^[a z0 9 ]{1,32}$/` |\n| `archive_operator_role` | `'archive operator'` |\n| `session_cookie_name`, `session_cookie_path` | `'archive_session'`, `'/api/v1'` |\n| `session_max_age_seconds` | `2592000` |\n| `operator_session_max_age_seconds` | `900`; shared root contract, also re exported by `/auth` |\n| `account_header` | `'x archive account'`; required issuer account uuid on coordinated erasure |\n| `publisher_header`, `request_marker_header` | `'x archive publisher'`, `'x archive request'` |\n| `ill_formed_text`, `iswellformedtext(value: string): boolean` | reject control characters and lone utf 16 surrogates |\n| `uuidschema`, `isotimestampschema`, `hex64schema` | uuid, iso timestamp, lowercase sha 256 hex strings |\n| `archivealiasschema` | trimmed, well formed alias of at most 24 characters |\n| `archiveauthorschema` | `{ id: string; name: string }` |\n| `archiveidentityschema` | `archiveidentity` |\n| `archiveaccountschema` | `archiveaccount`; issuer metadata, excluding private fields |\n| `archivelimitsschema` | all numeric `archive_limits` fields |\n| `archivehealthschema` | `archivehealth` |\n| `archiveauthhealthschema` | `authhealth` |\n| `archiveerrorresponseschema` | `archiveerrorresponse` |\n| `archiveokschema` | `{ ok: true }` |\n| `archivecollectionmetaschema` | `archivecollectionmeta` |\n| `archivelikedidsschema` | `{ ids: string[] }` subset response |\n| `archivelikespageschema` | `archivelikespage` |\n| `archivelikestateschema` | `{ id: string; likecount: number; liked: boolean }` |\n| `archivereportsschema`, `archiveownreportsschema` | `archivereports`, `archiveownreports` |\n| `archivemoderationlogschema` | `archivemoderationlog` |\n| `archivesortschema` | `archivesort` |\n| `archive_error_codes` | values of `archiveerrorcode` below |\n| `archive_moderation_actions` | `['hide', 'unhide', 'remove', 'erase publisher']` |\n\n### `sessioncookievalue`\n\ncomposes a session cookie using the shared security attributes.\n\n```ts\nfunction sessioncookievalue(token: string, options: { maxageseconds: number; secure: boolean }): string;\n```\n\n| parameter | contract |\n| | |\n| `token` | session token, or empty string when clearing |\n| `options.maxageseconds` | cookie lifetime; `0` clears it |\n| `options.secure` | include `secure` for production https |\n\n**returns:** the `set cookie` value with `httponly`, `samesite=strict`, the shared path and optional `secure`.\n\n**example:** clear the production cookie.\n\n```ts\nimport { sessioncookievalue } from '@vielzeug/archive';\n\nconst expiredcookie = sessioncookievalue('', { maxageseconds: 0, secure: true });\nconsole.log(expiredcookie);\n```\n\n \n\n### `iswellformedtext`\n\nchecks text for control characters and lone utf 16 surrogates.\n\n```ts\nfunction iswellformedtext(value: string): boolean;\n```\n\n**returns:** `true` when `ill_formed_text` does not match; it does not trim or impose bounds.\n\n**example:** validate text independently of the alias schema.\n\n```ts\nimport { iswellformedtext } from '@vielzeug/archive';\n\nconsole.log(iswellformedtext('rice'), iswellformedtext('\\u0000'));\n```\n\n### `archive_limits`\n\n`archive_limits` is frozen with these values:\n\n```ts\nconst limits = {\n aliaschars: 24, cursorchars: 1024, idempotencyretentionms: 172800000,\n likestateids: 50, maxpagesize: 50, maxpublications: 50, pagesize: 10,\n payloadbytes: 8192, publiccacheseconds: 30, reportreasonchars: 256,\n requestbytes: 12288, searchchars: 64, searchterms: 6, tagsperentry: 16,\n titlechars: 128, vocabchars: 64,\n};\n```\n\n## types\n\nthe following definitions use `schema` from `@vielzeug/spell`. schema derived aliases show their complete output shape.\n\n```ts\ninterface archiveindexing<f extends string = string, t extends string = string> {\n filter: f | null;\n tags: readonly t[];\n title: string;\n}\ninterface archivecollectiondefinition<\n p extends object, c extends number, f extends string = never, t extends string = never,\n> {\n canonicalize?(payload: p): p;\n contentversion: c;\n filters?: readonly f[];\n index(payload: p): archiveindexing<noinfer<f>, noinfer<t>>;\n name: string;\n payloadschema: schema<p, unknown>;\n tags?: readonly t[];\n}\ninterface archivederivation {\n canonicaljson: string;\n contenthash: string;\n contentversion: number;\n filter: string | null;\n tags: readonly string[];\n title: string;\n}\ninterface archiveentry<p = unknown, c extends number = number> {\n author: { id: string; name: string };\n contenthash: string;\n contentversion: c;\n id: string;\n likecount: number;\n payload: p;\n publishedat: string;\n revision: number;\n schemaversion: typeof archive_contract_version;\n updatedat: string;\n}\ninterface archiveownedentry<p = unknown, c extends number = number> extends archiveentry<p, c> {\n clientkey: string;\n hidden: boolean;\n}\ninterface archivecontract<p = unknown, c extends number = number> {\n entry: schema<archiveentry<p, c>>;\n ownedentries: schema<{ entries: archiveownedentry<p, c>[] }>;\n ownedentry: schema<archiveownedentry<p, c>>;\n page: schema<archivepage<archiveentry<p, c>>>;\n publishinput: schema<{ clientkey: string; payload: p }, unknown>;\n updateinput: schema<{ expectedrevision: number; payload: p }, unknown>;\n}\ninterface archivecollection<p extends object = object, c extends number = number> {\n readonly contentversion: c;\n readonly contract: archivecontract<p, c>;\n readonly derive: (raw: unknown) => promise<archivederivation | null>;\n readonly filters: readonly string[];\n readonly name: string;\n readonly tags: readonly string[];\n}\ninterface archivepage<t> {\n cursor: string | null;\n entries: readonly t[];\n previous: string | null;\n}\ntype archivesort = 'liked' | 'recent';\ntype archivederivationstage = 'schema' | 'canonicalize' | 'index' | 'hash' | 'response';\ntype archiveidentity = { id: string; name: string; operator: boolean } | null;\ntype archiveaccount = {\n id: string; email: string; createdat: string; lastsigninat: string | null;\n};\ntype archivecollectionmeta = { filters: string[]; name: string; tags: string[] };\ntype archivelikespage = { cursor: string | null; ids: string[] };\ntype archiveerrorcode =\n | 'account changed' | 'auth unavailable' | 'bad request' | 'conflict' | 'forbidden' | 'gone'\n | 'internal' | 'invalid payload' | 'likes disabled' | 'method not allowed'\n | 'not found' | 'publisher changed' | 'publishing disabled' | 'quota'\n | 'rate limited' | 'record unavailable' | 'timeout' | 'too large'\n | 'unauthenticated' | 'unavailable' | 'unsupported media';\ntype archiveerrorresponse = {\n error: { code: archiveerrorcode; message: string; requestid: string };\n};\ntype archivehealth = {\n dbschemaversion: number | null;\n features: { likes: boolean; publishing: boolean };\n limits: {\n aliaschars: number; cursorchars: number; idempotencyretentionms: number;\n likestateids: number; maxpagesize: number; maxpublications: number;\n pagesize: number; payloadbytes: number; publiccacheseconds: number;\n reportreasonchars: number; requestbytes: number; searchchars: number;\n searchterms: number; tagsperentry: number; titlechars: number; vocabchars: number;\n };\n schemaversion: 1;\n status: 'ok' | 'degraded';\n reason: 'database unavailable' | 'schema mismatch' | null;\n};\ntype authhealth = {\n dbschemaversion: number | null;\n schemaversion: 1;\n status: 'ok' | 'degraded';\n reason: 'configuration unavailable' | 'database unavailable' | 'schema mismatch' | null;\n};\ntype archivereports = {\n cursor: string | null;\n reports: {\n id: string; createdat: string; entryid: string; entrytitle: string;\n reason: string; reporter: { id: string; name: string }; resolvedat: string | null;\n }[];\n};\ntype archiveownreports = {\n cursor: string | null;\n reports: {\n collection: string; createdat: string; entryid: string; id: string;\n reason: string; resolvedat: string | null;\n }[];\n};\ntype archivemoderationaction = 'hide' | 'unhide' | 'remove' | 'erase publisher';\ntype archivemoderationlog = {\n cursor: string | null;\n actions: {\n id: string; action: archivemoderationaction; collection: string | null;\n createdat: string; operatorid: string; targetid: string;\n }[];\n};\n```\n\nworker subpath types model only the binding methods the catalog calls.\n\n```ts\ninterface d1statement {\n all<t>(): promise<{ results?: t[] }>;\n bind(...params: (string | number | null)[]): d1statement;\n first<t>(): promise<t | null>;\n run(): promise<{ meta?: { changes?: number } }>;\n}\ninterface d1like {\n batch(statements: d1statement[]): promise<{ meta?: { changes?: number } }[]>;\n prepare(sql: string): d1statement;\n}\ninterface ratelimiterbinding {\n limit(input: { key: string }): promise<{ success: boolean }>;\n}\ninterface archiveworkerenv {\n auth_issuer_service: authissuerservicebinding;\n allowed_origins: string;\n app_env: string;\n db: d1like;\n jwt_audience: string;\n jwt_issuer: string;\n jwt_jwks_url: string;\n likes_enabled: string;\n publishing_enabled: string;\n rate_limit_key_secret: string;\n read_limit: ratelimiterbinding;\n write_limit: ratelimiterbinding;\n}\ninterface authissuerservicebinding {\n fetch(request: request): promise<response>;\n}\ninterface archiveexecutioncontext {\n waituntil(promise: promise<unknown>): void;\n}\ntype archivediagnostic =\n | { readonly kind: 'configuration'; readonly field: string }\n | { readonly kind: 'database' | 'record' | 'schema' | 'internal' | 'cache' | 'collection' };\ninterface archiverequestevent {\n code: archiveerrorcode | 'ok';\n diagnostic?: archivediagnostic;\n ms: number;\n requestid: string;\n route: string;\n status: number;\n type: 'request';\n}\ntype archivescheduledevent =\n | { removed: number; type: 'scheduled'; status: 'ok' }\n | { type: 'scheduled'; status: 'failed'; diagnostic: archivediagnostic };\ntype archiveevent =\n | archiverequestevent\n | archivescheduledevent\n | { readonly type: 'cache error'; readonly diagnostic: archivediagnostic };\ninterface archiveworkerhandler<e extends archiveworkerenv = archiveworkerenv> {\n fetch(request: request, env: e, ctx: archiveexecutioncontext): promise<response>;\n scheduled?(controller: unknown, env: e, ctx: archiveexecutioncontext): void | promise<void>;\n tap(handler: (event: archiveevent) => void, options?: { readonly signal?: abortsignal }): () => void;\n}\n```\n\nscheduled `removed` counts the combined expired retry key and revocation rows.\n\nauth subpath types describe the sibling issuer and its required service binding.\n\n```ts\ninterface sendemail {\n send(message: { from: string; html?: string; subject: string; text: string; to: string }): promise<unknown>;\n}\ninterface autharchivebinding {\n fetch(request: request): promise<response>;\n}\ninterface authworkerenv {\n allowed_origins: string;\n app_env: string;\n app_name: string;\n audience: string;\n auth_archive: autharchivebinding;\n auth_db: d1like;\n auth_email: sendemail;\n auth_issuer: string;\n auth_jwt_previous_public_jwks?: string;\n auth_jwt_private_jwk: string;\n operator_emails: string;\n public_origin: string;\n sender_address: string;\n}\ninterface authworkerhandler<e extends authworkerenv = authworkerenv> {\n fetch(request: request, env: e, ctx?: archiveexecutioncontext): promise<response>;\n scheduled(controller: unknown, env: e, ctx?: archiveexecutioncontext): promise<void>;\n tap(handler: (event: authevent) => void, options?: { readonly signal?: abortsignal }): () => void;\n}\ntype authdiagnostic = {\n readonly kind: 'configuration' | 'database' | 'email' | 'catalog' | 'internal';\n};\ntype authevent =\n | {\n readonly type: 'request';\n readonly requestid: string;\n readonly route:\n | '/auth/account'\n | '/auth/health'\n | '/auth/jwks.json'\n | '/auth/session'\n | '/auth/sign in'\n | '/auth/sign out all'\n | '/auth/verify'\n | 'unmatched';\n readonly status: number;\n readonly code: archiveerrorcode | 'ok';\n readonly ms: number;\n readonly diagnostic?: authdiagnostic;\n }\n | { readonly type: 'scheduled'; readonly status: 'ok'; readonly removed: number }\n | { readonly type: 'scheduled'; readonly status: 'failed'; readonly diagnostic: authdiagnostic };\n```\n\nissuer scheduled `removed` counts expired token rows, closed per user send windows\nand deleted unverified accounts. the singleton admission row is retained and its\nwindow resets atomically on a later sign in request.\n\n## errors\n\n| exported class | trigger | properties |\n| | | |\n| `archiveerror` | base for public package errors | `name`, `message`, optional `cause` |\n| `archivedefinitionerror` | invalid collection definition or duplicate registered collection names | extends `archiveerror` |\n| `archivederivationerror` | unexpected schema/canonicalization/index/hash/response fault or violated collection invariant | extends `archiveerror`; readonly `stage: archivederivationstage`; optional `cause` |\n\n`archiveerror` and `archivedefinitionerror` accept `(message: string, opts?: erroroptions)`.\n`archivederivationerror` accepts `(stage: archivederivationstage, message: string, opts?: erroroptions)`.\nits message and cause are local diagnostics, not response content. worker request\nfailures use the wire envelope, not these thrown classes. internal `apierror` and\n`authoperationerror` are not exported.\n\nstored entries that fail the current wire schema return 503 `record unavailable`.\nunexpected exceptions in the collection's response payload callbacks instead\nthrow `archivederivationerror` with stage `response` and become sanitized 500s.\n\nthe complete public error signatures are:\n\n```ts\nclass archiveerror extends error {\n constructor(message: string, opts?: erroroptions);\n}\nclass archivedefinitionerror extends archiveerror {}\nclass archivederivationerror extends archiveerror {\n readonly stage: archivederivationstage;\n constructor(stage: archivederivationstage, message: string, opts?: erroroptions);\n}\n```\n\ncatalog rate limit responses use `retry after: 60`; configure catalog limiter periods to 60 seconds. issuer admission/send budget responses instead carry seconds until their durable window reopens. health's degraded response is the health dto, not an error envelope: schema mismatch reports the observed version; database unavailability reports `dbschemaversion: null`.\n",
|
|
8
|
+
"usage": " \ntitle: \"archive: usage guide\"\ndescription: \"define a collection, initialize fresh d1 databases, deploy the workers, and wire your client and observation policy.\"\n \n\n[[toc]]\n\n## basic usage\n\na deployment is a collection definition plus one factory call. put the definition in a\nmodule both sides import — the browser needs the composed schemas, the worker needs\nthe collection itself.\n\n```ts\n// recipes collection.ts — the one content contract, shared by both sides.\nimport { definearchivecollection } from '@vielzeug/archive';\nimport { s } from '@vielzeug/spell';\n\nconst payloadschema = s.object({\n cuisine: s.enum(['italian', 'japanese']),\n ingredients: s.array(s.string().trim().min(1).max(64)).min(1).max(20),\n name: s.string().trim().min(1).max(32),\n});\n\nexport const recipescollection = definearchivecollection({\n // ingredient order is not identity: the canonical form sorts, so reordered\n // content hashes identically and updates as a no op.\n canonicalize: (payload) => ({ ...payload, ingredients: [...payload.ingredients].sort() }),\n contentversion: 1,\n filters: ['italian', 'japanese'],\n index: (payload) => ({\n filter: payload.cuisine,\n tags: [payload.ingredients.length <= 5 ? 'quick' : 'involved'],\n title: payload.name,\n }),\n name: 'recipes',\n payloadschema,\n tags: ['quick', 'involved'],\n});\n```\n\nthe worker entry imports the collection and exports the built handler:\n\n```ts\n// backend/src/index.ts — the whole deployment.\nimport { createarchiveworker } from '@vielzeug/archive/worker';\nimport { recipescollection } from './recipes collection';\n\nconst worker = createarchiveworker([recipescollection]);\n\n// one sanitized log line per request: ids, codes and route classes only.\nworker.tap((event) => console.log(json.stringify(event)));\n\nexport default worker;\n```\n\nthe browser imports the same collection for its schemas — never the worker subpath:\n\n```ts\nimport { recipescollection } from './recipes collection';\nimport { archiveidentityschema, publisher_header } from '@vielzeug/archive';\n\nconst response = await fetch('/api/v1/collections/recipes/entries');\nif (!response.ok) throw new error('archive unavailable');\nconst page = recipescollection.contract.page.parse(await response.json());\nconst meresponse = await fetch('/api/v1/me', { credentials: 'same origin' });\nif (!meresponse.ok) throw new error('identity unavailable');\nconst me = archiveidentityschema.parse(await meresponse.json());\nif (me === null) throw new error('register a publisher first');\nconst mineresponse = await fetch('/api/v1/me/publications?collection=recipes', {\n credentials: 'same origin', headers: { [publisher_header]: me.id },\n});\nif (!mineresponse.ok) throw new error('own publications unavailable');\nconst owned = recipescollection.contract.ownedentries.parse(await mineresponse.json());\nconsole.log(page.entries, owned.entries);\n```\n\n## the collection contract\n\n`definearchivecollection` is the only place content knowledge lives. the definition is\nvalidated at registration — a bad `name`, `contentversion`, or any vocabulary value\n(well formed text, 1..`vocabchars` chars) throws there, never mid request — and every\nordinary invalid content returns `null` → `422 invalid payload`. collection\nimplementation faults instead throw `archivederivationerror` → sanitized\n`500 internal`:\n\n **parse** — accept stable json objects only; schema outputs must remain json.\n **canonicalize** — optional; a result that leaves the payload schema throws, so a\n broken normalizer can never store invalid content.\n **index** — the returned `filter` must be declared in `filters`, every `tag` in\n `tags` (duplicates collapse), and the `title` is trimmed and bounded\n (`titlechars`, well formed text). a derivation outside the declared vocabulary\n would store a row no list query can reach — it throws instead.\n **size** — the canonical json must fit `payloadbytes`.\n **identity** — the content hash is sha 256 over `{contentversion, payload}`:\n identical content hashes identically, a real edit always re identifies, and the\n core owns the envelope so collections cannot drift it.\n\ndeclare vocabularies with readonly arrays (`['fire', 'ice']`) for automatic\nliteral union inference. const generics retain the version literal and vocabulary\nunions; `noinfer` prevents the index callback from widening them. omitted arrays\nmean empty vocabularies: return `null` for\nthe filter and no tags. object keys are recursively sorted by the core; schema\ntransformations must stabilize and `canonicalize` must be pure and idempotent.\nnon json input returns `null`. unexpected callback, schema or hashing failures,\nunstable transformations, non idempotent canonicalization and index mutation throw\n`archivederivationerror`; its `stage` identifies the failing phase.\nlossy or non json schema output and invalid index result shapes also throw.\n\n`contentversion` participates in content identity. request handling validates\nonly the current collection definition; it does not interpret historical schemas.\narchive is unreleased and provides no content upgrade apis.\n\nfilters, tags and searchable titles are persisted data derived at publish/update\ntime. editing `index` or its vocabularies does not recompute existing projections.\nkeep the active definition and dataset consistent. for an unreleased development\ndeployment, deliberately reset and reseed a fresh database when you change payload\nor projection semantics; the package does not supply a migration framework.\nresetting destroys existing data. perform it only as an explicit, deliberate\ndevelopment operation, not automatically during startup or deployment.\n\n**payloads carry decisions, not content.** the payload schema should accept\nuser authored choices expressed as ids plus user written text — never the content's\nown copyrighted expression (card text, artwork, rules), never more than the game's\nown identifiers. for games with paid or dlc content this is the whole answer:\nownership resolves on each viewer's device against their own library, the service\nnever learns what anyone owns, and the strict schema is the enforcement seam —\nuse a strict schema that rejects unknown fields, so a collection that only models ids cannot\naccidentally start shipping paid content. for example, published game builds can\nreference cards, equipment and potions by id plus a user written name. the client\nrenders pieces the viewer does not own as unavailable. keep the browser safe\npublication contract in a shared domain module consumed by both the frontend and\nits backend, without importing frontend application code into the worker.\n\n## the worker and its environment\n\n`createarchiveworker(collections)` returns the full handler: anonymous public reads,\ncookie authenticated mutations behind exact origin and request marker guards, local\nrs256/jwks verification followed by an uncached issuer session version check,\nper ip read limits and per subject write limits keyed by peppered hmacs, the\n30 second anonymous cache on public entry lists only, operator moderation, and a\nscheduled expiry sweep for retired publish retry keys and revocation tombstones.\nhealth sits outside the rate limit and session pipelines: probes never burn the\nanonymous read budget or call the issuer.\n\nthe worker reads its environment fail closed — every binding must be present and\nwell formed, or the route answers 503 without naming what is missing:\n\n| variable | meaning |\n| | |\n| `app_env` | `local` or `production` (transport rules and the pepper policy) |\n| `allowed_origins` | exact canonical origins; never wildcards |\n| `jwt_issuer` / `jwt_audience` / `jwt_jwks_url` | your identity provider |\n| `auth_issuer_service` | required issuer service binding for live session version checks |\n| `likes_enabled` / `publishing_enabled` | feature flags |\n| `rate_limit_key_secret` | hmac pepper. required ≥ 32 chars in production; local may fall back |\n| `db` / `read_limit` / `write_limit` | d1 + rate limit bindings |\n\nboth limiter bindings must run 60 second windows (`simple` limiters:\n`period: 60`): the platform binding does not expose its period at runtime, so the\nworker's 429 `retry after` header is fixed at 60 seconds — a longer window would\nmake the header lie.\n\ngenerated env types (wrangler's `worker configuration.d.ts`) satisfy the package's\nstructural `archiveworkerenv` — no `@cloudflare/workers types` dependency, no casts.\n\n## the auth issuer\n\n`createauthworker()` from `@vielzeug/archive/auth` is the reference passwordless\nidentity issuer a deployment runs beside the catalog worker: one post with an email\nsends a single use magic link, and explicitly confirming it sets the `archive_session`\ncookie. the catalog verifies the jwt locally, then calls the issuer through\n`auth_issuer_service` to check the account's current session version. requests create\npending account rows; explicit link redemption atomically stamps the account's\nsign in time and claims the token. the issuer's d1 schema ships at\n`node_modules/@vielzeug/archive/migrations/auth/`; point the issuer's\n`migrations_dir` there. the catalog's `jwt_issuer` and `jwt_jwks_url` must identify\nthe issuer, and its audience must match the issuer's `audience`.\n\n| variable | meaning |\n| | |\n| `app_name` | the app's display name: the magic link email's subject and copy name it |\n| `allowed_origins` | exact origins allowed to trigger unsafe routes (the app origin(s)) |\n| `app_env` | `local` or `production` — production requires https and sets secure cookies |\n| `audience` / `auth_issuer` | the claims every minted session carries; the catalog verifies the same pair |\n| `auth_db` / `auth_email` | the issuer's d1 and send email bindings |\n| `auth_archive` | required service binding to the catalog worker; deletion calls its profile erasure route |\n| `auth_jwt_private_jwk` | the rs256 private key as a jwk json string: a wrangler secret in production |\n| `auth_jwt_previous_public_jwks` | optional retired public rs256 jwks; publish during rotation until old sessions expire |\n| `operator_emails` | comma separated emails whose newly minted sessions carry the operator role |\n| `public_origin` | app origin for links, confirmation post origin checks and client navigation after sign in |\n| `sender_address` | the address magic links come from: a domain onboarded to cloudflare email service |\n\nunder `wrangler dev` the send email binding is simulated — the link prints to the\nissuer's console, so that console is the inbox. demo only alice/bob routes are\ndeployment wrappers, not package apis.\n\nthe issuer first admits sign in requests through its singleton durable\n`auth_sign_in_budget` table: 20 requests per 60 seconds before creating accounts\nor delivering email. no rate limiter binding is required. it then\npermits three sign in emails per email in an hourly durable window.\nlater database or delivery failures do not refund global admission.\nusing or expiring a link never refunds sends; failed delivery also spends the\nallowance. links expire after 15 minutes. operator sessions expire after 900\nseconds, ordinary sessions after at most 30 days. retain retired public keys during\nrotation; the active private key signs every new session. sign in input trims and\nlowercases email before validation. `sender_address` must already be canonical;\n`operator_emails` entries are trimmed and lowercased before validation.\n\nthe scheduled sweep deletes expired links and closed per user send windows, then\npending accounts only when neither a link nor send budget row still references\nthem. failed delivery deletes the unusable token but retains the spent allowance;\nit does not immediately remove the pending account. verified accounts stay until\nexplicit erasure. the singleton admission row remains and resets its window on a\nlater admitted request.\n\nopening `get /auth/verify?token=…` renders a no store confirmation page and does\nnot consume the link or change the session, so email scanners cannot sign in by\nfetching it. a deliberate click sends `post /auth/verify` with json `{ token }`,\nthe request marker and an exact allowed origin or `public_origin`.\nsuccess is 204 with set cookie; the page then navigates to `/signed in`. the\nredemption update and account sign in stamp are trigger backed and atomic.\nthe page removes the token from the address bar, loads no external assets, and\nsets a restrictive csp and `referrer policy: no referrer`.\n\n`get /auth/account` exports account metadata only. for full erasure, call\n`delete /auth/account` with `account_header` (`x archive account`) set to the\nissuer account uuid captured from `get /auth/account` before confirmation.\nthe issuer rejects missing or invalid uuids with 400 and a different signed in\naccount with 409 `account changed`, before any erasure. this guard applies even\nbefore publisher registration and must stay pinned across retries.\nthe issuer forwards the caller's session and publisher\nprecondition through `auth_archive` to `delete /me/account`, requires exactly 204,\nthen deletes its\nown user and clears the cookie. failure preserves the session for retry. direct\n`delete /me` removes only the archive profile and leaves the cookie intact.\n\nseveral game frontends can share one issuer and catalog. keep the cookie\nhost only on the central api host, use `credentials: 'include'` from same site\nhttps game subdomains, and allow each exact frontend origin in both workers.\ndo not share cookies through a parent domain `domain` attribute. the\n[multi collection recipe](./examples/multi collection deployment.md) describes\nthe topology, session revalidation and platform wide account boundary.\n\n`delete /me/account` atomically revokes the issuer/subject and cascades profile\ndeletion. its tombstone lasts 30 days plus ten seconds; old sessions cannot read\nprivate data or register again, including a racing registration. the dedicated\naccount erasure route alone may bypass the issuer lookup, and only to retry with\nthe same principal's original signed session while its existing tombstone is\nvalid; signature and claims are still verified, and initial erasure validates the\nissuer. the issuer's `delete /auth/account` checks the current version before\nstarting erasure; after completed erasure a retry there returns 401. a fresh\nissuer account gets a new uuid subject. external issuers must use a new subject to\nre enroll during the revocation window. profile only deletion does not revoke the\nsubject and permits re registration with existing credentials.\n\neach jwt also carries the issuer account's required safe integer `sessionversion`.\nthe issuer compares it with `auth_users.session_version` on every ordinary\nauthenticated route. a jwt with an old version is unauthenticated even before its\nexpiry; the catalog asks the issuer on every private request and does not cache a\nsuccessful verdict. a valid local jwt with an unavailable, malformed, 5xx or\ntimed out (fixed 5000 ms, including body decoding) issuer response becomes\n`503 auth unavailable`, not anonymous identity.\n\n## the database\n\nthe package ships the canonical catalog migration:\n`node_modules/@vielzeug/archive/migrations/0001_community_archive.sql` — the whole schema,\nmoderation included, as one designed file. the runtime sql in the package is\nwritten against exactly this schema, and the package's own integration suite runs\nagainst the same file. point wrangler at the directory:\n\n```jsonc\n// wrangler.jsonc — the deployment owns its d1 database, not its ddl.\n{\n \"d1_databases\": [\n {\n \"binding\": \"db\",\n \"database_name\": \"my app\",\n \"migrations_dir\": \"./node_modules/@vielzeug/archive/migrations\"\n }\n ]\n}\n```\n\nthe issuer points to\n`node_modules/@vielzeug/archive/migrations/auth/0001_auth.sql`. both files are\nfresh database initialization definitions, not upgrades. the issuer ddl initializes\n`auth_users.session_version` to `0` and stamps `auth_schema_meta.applied` as\n`auth_schema_version` (`1`). neither file automatically deletes or resets data.\nwhile archive is unreleased, its\ncanonical initial schemas are edited directly. reset and reseed an unreleased\ndevelopment database deliberately when its schema or collection semantics no\nlonger match; reapplying the initial file does not update an existing dataset.\nthe initialization file also stamps the storage schema\n(`archive_schema_meta`) at `archive_schema_version`, and health reads it back: a\ndatabase with a mismatched stamp answers `status: 'degraded'` with the\nobserved `dbschemaversion` and http 503 (`reason: 'schema mismatch'`).\nan unavailable database produces http 503 with `dbschemaversion: null` and\n`reason: 'database unavailable'`. the debut storage stamp is `1`. a unit\ntest pins the shipped ddl's `check` bounds to `archive_limits`, so the two can never\ndrift silently. the stamp does not detect changes in your collection's payload\nschema, vocabulary or indexing function; you own that dataset consistency.\n\n## routes and wire names\n\n```\nget /api/v1/health\npost /api/v1/sign out\nget /api/v1/auth/health\nget /api/v1/auth/session (internal issuer check)\npost /api/v1/auth/sign out all (revoke every session)\nget /api/v1/collections/:collection (self description)\nget /api/v1/collections/:collection/entries (author, filter, tag, search, sort, cursor, limit)\npost /api/v1/collections/:collection/entries\nget /api/v1/collections/:collection/entries/:id\nput /api/v1/collections/:collection/entries/:id (expectedrevision)\ndelete /api/v1/collections/:collection/entries/:id\nput /api/v1/collections/:collection/entries/:id/like\ndelete /api/v1/collections/:collection/entries/:id/like\npost /api/v1/collections/:collection/entries/:id/report (reason)\nput /api/v1/collections/:collection/entries/:id/hidden (operator)\ndelete /api/v1/collections/:collection/entries/:id/hidden (operator)\nget /api/v1/collections/:collection/reports (operator: status, cursor, limit)\nput /api/v1/collections/:collection/reports/:id/resolved (operator: report id)\ndelete /api/v1/publishers/:id (operator: erase any publisher)\nget /api/v1/moderation log (operator: cursor, limit)\nget /api/v1/me post /me put /me delete /me\nget /api/v1/me/publications?collection=:collection get /api/v1/me/likes[?ids=…]\nget /api/v1/me/reports (cursor, limit)\nget /api/v1/auth/account\nget /api/v1/auth/jwks.json\npost /api/v1/auth/sign in (email)\ndelete /api/v1/auth/account (coordinated full erasure)\ndelete /api/v1/me/account (issuer erasure seam; revokes subject)\nget /api/v1/auth/verify?token=… (confirmation only)\npost /api/v1/auth/verify (guarded {token} redemption)\n```\n\npublishing is bounded by `maxpublications` live entries per `(collection, owner)`.\nthe quota check runs inside the guarded insert.\n\n`/me/likes` without `ids`, `/me/reports`, operator reports and `/moderation log`\nuse forward keyset pages: follow `cursor` until `null`, preserving query context.\n`limit` defaults to `pagesize` and is at most `maxpagesize`; there is no total\nexport bound. `/me/likes?ids=…` remains a subset lookup returning `{ ids }`, not a\npage, and cannot combine `ids` with `cursor` or `limit`.\n\nshared wire names live in the package — `archive_session` cookie,\n`x archive publisher` and `x archive request` headers, `archive_error_codes`, and\n`archive_limits` — so a client and the worker cannot drift. parse failures with the\ncollection's composed schemas; parse errors with `archiveerrorresponseschema`, the\nsign out body with `archiveokschema`, a collection's self description with\n`archivecollectionmetaschema` and the operator queue with `archivereportsschema`.\nevery entry's `schemaversion` is the archive contract's version, not the\ncollection's; the collection's own format version lives in its hash envelope and in\neach entry's `contentversion`.\n\nonly anonymous entry lists carry a 30 second edge cache contract\n(`publiccacheseconds`). detail is always `no store`. any cookie or authorization\nheader disables caching, regardless of validity; metadata, private and error\nresponses also use `no store`.\n\nimplement ui ownership checks in your application by comparing the current\npublisher id to the entry's `author.id`; there is no package action policy helper.\nserver mutations remain authoritative.\n\n`post /api/v1/sign out` is deliberately local: it clears only this browser's\ncookie and does not contact the issuer or revoke copied sessions. to revoke every\nsession, post to `/api/v1/auth/sign out all` with the mutation marker, an exact\nallowed `origin`, and the issuer account uuid captured from\n`get /api/v1/auth/account` in `x archive account`. the issuer returns 409\n`account changed` if the active subject differs. on success it atomically increments\n`session_version`, deletes outstanding links, preserves account and catalog data,\nand returns `{ ok: true }` with an expired cookie. requests authorized before\nrevocation may finish; there is no cross database cancellation guarantee.\n\n`get /api/v1/auth/health` is public and read only. it validates issuer\nconfiguration, key signing/verification, binding shapes, the `auth_schema_meta`\nstamp and expected tables, columns and triggers, returns `authhealth` through\n`archiveauthhealthschema`, and answers 503 `degraded` for configuration,\ndatabase or schema failures. it never writes, sends email or probes the catalog.\n\n## moderation\n\noperator authority is the session token's optional `roles` claim carrying\n`archive_operator_role` (`'archive operator'`) — minted by your issuer, verified with\nthe same signature, audience and issuer checks as any session, and never a separate\ncredential. moderation routes additionally require a registered publisher row: one\naccountable identity per action.\n\n **report** — any registered publisher reports a visible entry with a bounded\n free text reason (`reportreasonchars`). idempotent per (entry, reporter): the same\n report twice is the same 204. missing or hidden entries answer 404 — there is\n nothing left to report.\n **hide** — an operator removes an entry from every public surface (list, detail,\n likes) with a soft hide: the row, its likes and its retry key stay intact. the\n owner still sees the entry in `/me/publications` with `hidden: true`, edits and\n key replay are frozen (403), removal stays allowed, and unhiding restores\n everything. already hidden or already visible answer the same 204.\n **queue** — the operator pages newest reports with entry title and reporter\n identity; `status` is `unresolved` (default) or `resolved`. each report has its\n own `id` and nullable `resolvedat`. resolve it with\n `put /collections/:collection/reports/:id/resolved`; repeating is a 204 no op.\n **log** — every effective moderation action (hide, unhide, removal, publisher\n erasure) appends one accountability row in the same transaction that applies it,\n and `get /api/v1/moderation log` pages the newest actions to\n operators. rows carry only ids, the action kind and the time — no names (they\n change), no content — and no foreign keys: the audit must outlive the erasures it\n records. idempotent repeats (hiding an already hidden entry, erasing an\n already erased publisher) answer the same 204 with no new row, including\n concurrent repeats: audit insertion tests the effective state inside the batch.\n\nlike and unlike both require a visible entry. their sql guards visibility during\nthe mutation, and the response checks it again; hiding or deleting an entry\ncannot turn a racing unlike into a successful response for unavailable content.\n\n## observability\n\nthe core never logs. `worker.tap(handler, { signal? })` emits one `archiveevent` per\nrequest — `{ type: 'request', code, ms, requestid, route, status, diagnostic? }` —\nand successful sweep — `{ type: 'scheduled', status: 'ok', removed }`. failed\nsweeps emit `{ type: 'scheduled', status: 'failed', diagnostic }` and rethrow so\nthe runtime sees failure. cache failures emit `{ type: 'cache error', diagnostic }`.\nthe worker swallows observer errors so\nobservability cannot affect the worker. with no handlers registered there is zero\nwork. route is a finite class like `/collections/:collection/entries`, never a\nraw path; no event carries a url, cookie, subject, alias or token. diagnostics are\ntyped sanitized categories, not raw exceptions. a pre aborted tap signal is inert.\nthe sweep's `removed` counts expired retry keys and revocation tombstones together.\n\nthe issuer also exposes `tap()` with `authevent` from `@vielzeug/archive/auth`.\nit never logs directly. events contain finite route labels, codes, counts,\nduration and sanitized `authdiagnostic` categories, never emails, tokens or raw\nexceptions. scheduled failures emit then rethrow; pre aborted taps are inert.\nissuer sweep counts include expired tokens, closed per user send windows and\nremoved pending accounts; the singleton admission row is not removed.\n\n```ts\nimport { createlogger } from '@vielzeug/rune';\nimport { createarchiveworker } from '@vielzeug/archive/worker';\nimport { recipescollection } from './recipes collection';\n\nconst log = createlogger({ namespace: 'recipes' });\nconst worker = createarchiveworker([recipescollection]);\nworker.tap((event) => log.debug(event, `archive:${event.type}`));\nexport default worker;\n```\n\n## testing\n\nthe package proves itself against real workerd + local d1:\n\n```sh\npnpm vitest run packages/archive/src/__tests__/ # fast unit suite\npnpm filter @vielzeug/archive test:integration # real workerd + local d1 + issuer coverage\npnpm filter @vielzeug/archive test:types # strict types over src and the integration suite\n```\n\nprove your own collection the way the reference deployment does: a slim deployment\nsuite that publishes over your real vocabulary and asserts the derivation, plus the\npackage's generic suite for the core behavior. a collection with an `index` that can\ndrift out of vocabulary should unit test `derive` directly: ordinary invalid input\nreturns `null`, while implementation faults throw `archivederivationerror`.\ndirect ddl tests exercise the shipped schema rather than a duplicate schema.\n\n## framework integration\n\nthe root entry is plain typescript with no framework coupling; vue, react and svelte\napps all consume the composed schemas the same way. a vue 3 composable that pages a\ncollection:\n\n```ts\n// userecipes.ts — one collection's typed client in any component tree.\nimport { recipescollection } from './recipes collection';\nimport { ref, shallowref } from 'vue';\n\nexport function userecipes() {\n const entries = shallowref<returntype<typeof recipescollection.contract.entry.parse>[]>([]);\n const cursor = ref<string | null>(null);\n async function load(sort: 'liked' | 'recent') {\n const params = new urlsearchparams({ sort });\n if (cursor.value !== null) params.set('cursor', cursor.value);\n const response = await fetch(`/api/v1/collections/recipes/entries?${params}`);\n if (!response.ok) throw new error('the archive is unavailable.');\n const page = recipescollection.contract.page.parse(await response.json());\n entries.value = [...entries.value, ...page.entries];\n cursor.value = page.cursor;\n }\n return { cursor, entries, load };\n}\n```\n\n## working with other vielzeug libraries\n\n [spell](/spell/) — the payload schema a collection declares; the core composes the\n envelope schemas from it, so the wire and the types cannot drift.\n [courier](/courier/) — the http client shape an app's frontend mirrors to consume\n these routes: strict response schemas over one typed port.\n [rune](/rune/) — the structured logger a deployment points `tap()` events at.\n\n## best practices\n\n compose client schemas from `collection.contract`; never hand write the envelope.\n declare exactly the vocabulary your `index` derives, with union types where you\n have them; a value outside it rejects loudly.\n keep `canonicalize` pure and schema true: reordered lists in, canonical form out.\n initialize fresh databases with the shipped ddl and deliberately reset/reseed\n unreleased development data when collection or storage semantics change.\n treat `archive_error_codes` and `archiveerrorresponseschema` as one tested contract.\n pin the expected publisher header from the identity you captured at sign in, never from storage alone.\n grant the operator role claim only through your issuer, and only to accounts that\n are also registered publishers.\n wire a sanitized `tap()` sink and register the rate limit pepper as a production secret.\n",
|
|
9
|
+
"examples": " \ntitle: \"archive: examples\"\ndescription: \"task oriented recipes for @vielzeug/archive, from first deployment to erasure.\"\n \n\n [stand up a content archive](./examples/stand up a content archive.md)\n [content identity and versions](./examples/content identity and versions.md)\n [multi collection deployment](./examples/multi collection deployment.md)\n [moderate content](./examples/moderate content.md)\n [client error mapping](./examples/client error mapping.md)\n [gdpr erasure and export](./examples/gdpr erasure and export.md)\n"
|
|
10
|
+
},
|
|
11
|
+
"examples": [],
|
|
12
|
+
"exports": "definearchivecollection createarchiveworker createauthworker archiveauthhealthschema archivederivationerror",
|
|
13
|
+
"keywords": "cloudflare worker d1 community publishing content catalog gdpr",
|
|
14
|
+
"name": "@vielzeug/archive",
|
|
15
|
+
"related": "spell courier rune",
|
|
16
|
+
"slug": "archive",
|
|
17
|
+
"source": "// public root surface of @vielzeug/archive: the browser safe contract — the wire\n// schemas and the collection seam. the cloudflare worker itself\n// lives behind the `@vielzeug/archive/worker` subpath so client bundles never\n// resolve worker machinery.\nexport * from './collection';\nexport * from './contract';\nexport * from './errors';\n"
|
|
18
|
+
},
|
|
2
19
|
{
|
|
3
20
|
"category": "utilities",
|
|
4
21
|
"description": "tree shakeable typescript utilities with focused category entry points for arrays, async work, caching, objects, strings, math, and guards.",
|
|
5
22
|
"docs": {
|
|
6
23
|
"index": " \ntitle: 'arsenal: utility library for typescript'\ndescription: tree shakeable typescript utilities with focused category entry points for arrays, async work, caching, objects, strings, math, and guards.\npackage: arsenal\ncategory: utilities\nkeywords: [utility, array, string, object, math, async, debounce, throttle, cache]\nexports: [groupby, retry, debounce, isequal, taskpool, cache, fuzzyfilter, tryparsejson, allocate, hash]\nrelated: [tempo, sourcerer, spell, coins]\nenvironments: [browser, node, ssr, deno]\n \n\n<! markdownlint disable md025 md033 md060 >\n\n<packagehero package=\"arsenal\" />\n\n## why arsenal?\n\narsenal provides typed, zero dependency utilities that reduce repeated application code: collection transforms, numeric operations, retry and cancellation, caching, safe path traversal, and deterministic serialization. the root stays curated while category entry points expose the broader toolkit. exact platform aliases remain excluded.\n\n```ts\n// before\nconst users = json.parse(raw).filter((user) => user.name.includes(query));\n\n// after\nimport { fuzzyfilter } from '@vielzeug/arsenal/array';\nimport { tryparsejson } from '@vielzeug/arsenal/object';\n\nconst parsed = tryparsejson(raw);\nconst users = parsed.ok ? fuzzyfilter(parsed.value as user[], query, { select: (user) => user.name }) : [];\n```\n\n| feature | arsenal | lodash es | remeda |\n| | | | |\n| bundle size | <packageinfo package=\"arsenal\" type=\"size\" /> | ~72 kb | ~18 kb |\n| typed root utilities | <ore icon name=\"check\" size=\"16\"></ore icon> | partial | <ore icon name=\"check\" size=\"16\"></ore icon> |\n| category entry points | <ore icon name=\"check\" size=\"16\"></ore icon> | partial | partial |\n| async task pool and cache | <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| zero dependencies | <ore icon name=\"check\" size=\"16\"></ore icon> | <ore icon name=\"check\" size=\"16\"></ore icon> | <ore icon name=\"check\" size=\"16\"></ore icon> |\n\n<div class=\"decision callout\">\n\n**use arsenal when** you need one typed utility dependency with focused subpaths for specialized behavior.\n\n**consider narrower alternatives when** you need only platform apis or a small functional subset.\n\n</div>\n\n## installation\n\n::: code group\n\n```sh [pnpm]\npnpm add @vielzeug/arsenal\n```\n\n```sh [npm]\nnpm install @vielzeug/arsenal\n```\n\n```sh [yarn]\nyarn add @vielzeug/arsenal\n```\n\n:::\n\n## quick start\n\n```ts\nimport { groupby, retry } from '@vielzeug/arsenal';\nimport { cache } from '@vielzeug/arsenal/cache';\nimport { taskpool } from '@vielzeug/arsenal/async';\n\nconst byrole = groupby([{ role: 'admin' }, { role: 'user' }], (user) => user.role);\n\nconst pool = taskpool({ concurrency: 2 });\nconst health = await pool.run((signal) => retry(() => fetch('/health', { signal }).then((response) => response.json())));\n\nconst responses = cache<string, unknown>({ ttlms: 60_000 });\nconst profile = await responses.getorload('/profile', () => fetch('/profile').then((response) => response.json()));\n\npool.dispose();\nconsole.log(byrole, health, profile);\n```\n\n## features\n\n<div class=\"features grid\">\n\n **`retry`**: retry async work with cancellation support from package root\n **`taskpool`**: bounded, disposable concurrent work from `/async`\n **`cache`**: identity keyed ttl cache with async load deduplication from `/cache`\n **`fuzzyfilter`**: explicit field fuzzy filtering from `/array`\n **`tryparsejson`**: preserve json syntax failures from `/object`\n **`allocate`**: lossless proportional distribution from `/math`\n **`chunk`, `zip`, `compact`**: typed collection transforms from `/array`\n **`range`, `average`, `median`**: common numeric operations from `/math`\n **`assert`, `tap`**: typed control flow helpers from `/function`\n **`isequal`**: structural equality from package root\n **`hash`**: deterministic serialization for stable cache keys from `/object`\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/): validate `unknown` json data after `tryparsejson`.\n [vault](/vault/): persistent storage; arsenal cache is in memory only.\n [tempo](/tempo/): date/time utilities kept outside arsenal.\n [coins](/coins/): money formatting and currency conversion.\n\n</div>\n\n<! markdownlint enable md025 md033 md060 >\n",
|
|
7
|
-
"api": " \ntitle: 'arsenal: api reference'\ndescription: reference for arsenal root utilities and category entry points.\n \n\n[[toc]]\n\n## api overview\n\n| symbol | purpose | execution | common gotcha |\n| | | | |\n| `groupby` | group values by key (prototype pollution guarded) | sync | root export |\n| `chunk` / `zip` / `compact` | typed collection transforms | sync | available from `/array` |\n| `range` / `average` / `median` | numeric generation and aggregation | sync | available from `/math` |\n| `assert` | assert and narrow a condition | sync | available from `/function` |\n| `retry` | retry async work | async | rethrows final error |\n| `taskpool` | bound concurrent tasks | async | available from `/async` |\n| `cache` | in memory identity keyed cache | async | available from `/cache` |\n| `fuzzyfilter` | filter string or selected object fields | sync | object collections require `select` |\n| `fuzzyscore` | rank string or selected object fields | sync | object collections require `select` |\n| `tryparsejson` | preserve json syntax result | sync | returns `unknown` on success |\n| `getpath` | optional object lookup (safe path guarded) | sync | available from `/object` |\n| `allocate` | lossless proportional distribution | sync | available from `/math` |\n| `isequal` | structural equality | sync | root export |\n| `hash` | deterministic serialization for cache keys | sync | available from `/object` |\n\n## package entry points\n\n| import | purpose |\n| | |\n| `@vielzeug/arsenal` | curated common utilities |\n| `@vielzeug/arsenal/array` | immutable transforms, set operations, sorting, fuzzy search |\n| `@vielzeug/arsenal/async` | retry, cancellation, task pool, timing |\n| `@vielzeug/arsenal/cache` | in memory cache and memoization |\n| `@vielzeug/arsenal/function` | assertions, composition, timing, teardown |\n| `@vielzeug/arsenal/guards` | type guards, predicate combinators, equality guards |\n| `@vielzeug/arsenal/math` | ranges, aggregation, statistics, interpolation, exact allocation |\n| `@vielzeug/arsenal/object` | paths, transforms, hash, json parse result |\n| `@vielzeug/arsenal/random` | cryptographic entropy selection and shuffle |\n| `@vielzeug/arsenal/string` | unicode aware case transforms and similarity |\n\n## array\n\n### typed transforms\n\n```ts\nchunk<t>(input: readonly t[], size?: number): t[][]\ncompact<t>(array: readonly t[]): array<exclude<t, false | '' | 0 | 0n | null | undefined>>\nfirst<t>(array: readonly t[], fallback?: t): t | undefined\nlast<t>(array: readonly t[], fallback?: t): t | undefined\nreplace<t>(array: readonly t[], predicate: (value: t) => boolean, value: t): t[]\nrotate<t>(array: readonly t[], positions: number, options?: { wrap?: boolean }): t[]\nzip(...arrays): array<tuple>\nunzip(rows): tupleofarrays\n```\n\n`take`, `takelast`, `drop`, and `droplast` return new arrays and normalize negative counts to zero. set operations accept optional selectors for object identity.\n\n### fuzzyfilter / fuzzyscore\n\n```ts\nfuzzyfilter(strings: readonly string[], query: string, options?: fuzzyoptions): string[]\nfuzzyfilter<t>(items: readonly t[], query: string, options: fuzzyselection<t>): t[]\nfuzzyscore(strings: readonly string[], query: string, options?: fuzzyoptions): scoredresult<string>[]\nfuzzyscore<t>(items: readonly t[], query: string, options: fuzzyselection<t>): scoredresult<t>[]\n```\n\n`fuzzyfilter` preserves input order. `fuzzyscore` orders results by descending score.\n\n```ts\nimport { fuzzyfilter } from '@vielzeug/arsenal/array';\n\nconst users = [{ email: 'alice@example.com', name: 'alice' }];\nconst matches = fuzzyfilter(users, 'alice', { select: (user) => [user.name, user.email] });\n```\n\n \n\n## async\n\n### taskpool\n\n```ts\ninterface taskpool {\n run<t>(task: (signal: abortsignal) => promise<t>): promise<t>;\n idle(): promise<void>;\n dispose(reason?: unknown): void;\n readonly active: number;\n readonly pending: number;\n readonly disposed: boolean;\n readonly disposalsignal: abortsignal;\n}\n\ntaskpool(options?: { concurrency?: number }): taskpool\n```\n\n`dispose()` aborts running cooperative tasks and rejects pending tasks.\n\n```ts\nimport { taskpool } from '@vielzeug/arsenal/async';\n\nconst pool = taskpool({ concurrency: 2 });\nconst user = await pool.run((signal) => fetch('/user', { signal }).then((response) => response.json()));\npool.dispose();\n```\n\n \n\n## cache\n\n### cache\n\n```ts\ninterface cache<k, t> {\n entries(): readonlyarray<readonly [k, t]>;\n get(key: k): t | undefined;\n set(key: k, value: t, options?: { ttlms?: number }): void;\n getorload(key: k, load: () => promise<t>): promise<t>;\n delete(key: k): boolean;\n clear(): void;\n readonly size: number;\n}\n\ncache<k, t>(options?: cacheoptions): cache<k, t>\n```\n\nkeys use native `map` identity. expiry is lazy, evaluated by `get`, `getorload`, `set`, and `entries`. `entries()` trims expired values and returns a new insertion ordered snapshot without exposing internal storage. the `size` getter does not evict expired entries.\n\n```ts\nimport { cache } from '@vielzeug/arsenal/cache';\n\nconst profiles = cache<string, profile>({ ttlms: 60_000 });\nconst profile = await profiles.getorload('me', loadprofile);\n```\n\n \n\n## encoding\n\nurl safe base64 (`base64url`) codecs without `btoa`/`buffer`, so the same code runs in browsers, workers, and node.\n\n```ts\nimport { base64urltobytes, base64urltotext, bytestobase64url, texttobase64url, utf8bytes } from '@vielzeug/arsenal';\n\ntexttobase64url('{\"build\":\"hero\"}'); // never contains '+', '/', or '='\nbase64urltotext(texttobase64url('league of legends')); // round trips any utf 8 text\nbytestobase64url(uint8array.from([0, 1, 250])); // byte level codec for binary payloads\nbase64urltobytes(code); // throws on characters outside the alphabet\nutf8bytes('hunter äöü'); // 11: byte length of a utf 8 string\n```\n\n## object\n\n### tryparsejson\n\n```ts\ntype jsonparseresult = { ok: true; value: unknown } | { error: syntaxerror; ok: false };\n\ntryparsejson(text: string): jsonparseresult\n```\n\nuse a schema validator after success to refine `unknown` data.\n\n```ts\nimport { tryparsejson } from '@vielzeug/arsenal/object';\n\nconst result = tryparsejson(raw);\nif (!result.ok) throw result.error;\n```\n\n### getpath\n\n```ts\ngetpath<t extends record<string, unknown>, p extends string>(item: t, path: p): pathvalue<t, p> | undefined\ngetpathor<t extends record<string, unknown>, p extends string, f>(item: t, path: p, fallback: f): pathvalue<t, p> | f\nrequirepath<t extends record<string, unknown>, p extends string>(item: t, path: p): exclude<pathvalue<t, p>, undefined>\n```\n\n### hash\n\n```ts\nhash(value: unknown, options?: hashoptions): string\n```\n\nproduces a deterministic, order independent serialization. handles `date`, `regexp`, `set`, `map`, and `bigint`. circular references produce a sentinel. useful for stable cache keys.\n\n \n\n## math\n\n### numeric toolkit\n\n```ts\nrange(stop: number): number[]\nrange(start: number, stop: number, step?: number): number[]\nclamp(value: number, min?: number, max?: number): number\nsum<t>(values: readonly t[], select?: (value: t) => number): number\naverage<t>(values: readonly t[], select?: (value: t) => number): number | undefined\nmedian<t>(values: readonly t[], select?: (value: t) => number): number | undefined\nvariance<t>(values: readonly t[], select?: (value: t) => number): number\nstandarddeviation<t>(values: readonly t[], select?: (value: t) => number): number\n```\n\n`sum` and `average` reject non finite values. `gcd` and `lcm` require safe integers. `linspace` requires finite bounds and a positive integer point count. `variance` and `standarddeviation` compute population statistics.\n\n### allocate\n\n```ts\nallocate(amount: number, ratios: number[] | number): number[]\nallocate(amount: bigint, ratios: number[] | number): bigint[]\n```\n\ndistributes an amount proportionally across ratios. the indivisible remainder is applied to the last bucket so the sum equals the original amount exactly: critical for financial operations.\n\n## types\n\n```ts\ntype fuzzyoptions = {\n normalize?: boolean;\n threshold?: number;\n};\n\ntype fuzzyselection<t> = fuzzyoptions & {\n select: (item: t) => string | readonly string[];\n};\n\ntype scoredresult<t> = { item: t; score: number };\n\ntype cacheoptions = {\n capacity?: number;\n now?: () => number;\n ttlms?: number;\n};\n```\n\n## errors\n\n `rangeerror`: invalid numeric bounds, capacity, concurrency, or retry count.\n `typeerror`: invalid value types, unsupported comparison, or required path missing.\n `arsenalserializationerror`: memo or hash cannot serialize supplied input.\n",
|
|
8
|
-
"usage": " \ntitle: 'arsenal: usage guide'\ndescription: use arsenal root utilities for common work and category entry points for specialized collection, async, cache, object, and string behavior.\n \n\n[[toc]]\n\n## basic usage\n\nstart at package root for common utilities. move to a category entry point for the broader typed toolkit. arsenal keeps helpers that improve readability, narrowing, immutable updates, or edge case consistency; exact platform aliases stay out.\n\n```ts\nimport { groupby, retry } from '@vielzeug/arsenal';\n\nconst users = [\n { id: 'a1', role: 'admin' },\n { id: 'u1', role: 'user' },\n { id: 'u2', role: 'user' },\n];\n\nconst byrole = groupby(users, (user) => user.role);\nconst health = await retry(() => fetch('/health').then((response) => response.json()));\n\nconsole.log(byrole, health);\n```\n\nuse category imports for apis absent from root:\n\n```ts\nimport { fuzzyfilter } from '@vielzeug/arsenal/array';\nimport { taskpool } from '@vielzeug/arsenal/async';\nimport { cache } from '@vielzeug/arsenal/cache';\nimport { tryparsejson } from '@vielzeug/arsenal/object';\n```\n\n## transform collections\n\nuse `/array` for transforms that preserve input immutability. `filtermap` combines mapping and omission; `indexby` and `groupby` build lookup structures without mutation.\n\n```ts\nimport { filtermap, indexby, sort } from '@vielzeug/arsenal/array';\n\nconst products = [\n { id: 'p1', price: 20, published: true },\n { id: 'p2', price: 10, published: false },\n { id: 'p3', price: 15, published: true },\n];\n\nconst publishedlabels = filtermap(products, (product) =>\n product.published ? `${product.id}: ${product.price}` : undefined,\n);\nconst byid = indexby(products, (product) => product.id);\nconst byprice = sort(products, (product) => product.price);\n\nconsole.log(publishedlabels, byid, byprice);\n```\n\n## search explicit fields\n\nsearch string arrays directly. object collections require `select`, so callers define exactly what can match.\n\n```ts\nimport { fuzzyfilter, fuzzyscore } from '@vielzeug/arsenal/array';\n\nconst users = [\n { email: 'alice@example.com', name: 'alice' },\n { email: 'bob@example.com', name: 'bob' },\n];\n\nconst matches = fuzzyfilter(users, 'alice', { select: (user) => [user.name, user.email] });\nconst ranked = fuzzyscore(users, 'ali', { select: (user) => user.name });\n```\n\n## work with object data\n\nuse `/object` for paths, key selection, stable cache keys, and object transforms.\n\n```ts\nimport { getpathor, hash, omit, pick } from '@vielzeug/arsenal/object';\n\nconst config = { api: { host: 'localhost', port: 3000 }, debug: true };\nconst port = getpathor(config, 'api.port', 8080);\nconst publicconfig = pick(config, ['api']);\nconst productionconfig = omit(config, ['debug']);\nconst key = hash({ port, productionconfig });\n\nconsole.log(publicconfig, key);\n```\n\n## parse and validate json\n\n`tryparsejson` distinguishes syntax failure from schema failure. treat successful values as `unknown`, then validate with spell or application code.\n\n```ts\nimport { tryparsejson } from '@vielzeug/arsenal/object';\nimport { s } from '@vielzeug/spell';\n\nconst user = s.object({ id: s.string(), name: s.string() });\nconst parsed = tryparsejson(raw);\n\nif (!parsed.ok) throw parsed.error;\n\nconst user = user.parse(parsed.value);\n```\n\n## bound concurrent work\n\nuse `parallel` for one finite collection. use `taskpool` when tasks arrive over time or need disposal.\n\n```ts\nimport { parallel, taskpool } from '@vielzeug/arsenal/async';\n\nconst metadata = await parallel(urls, (url) => fetch(url).then((response) => response.json()), { limit: 4 });\n\nconst pool = taskpool({ concurrency: 2 });\nconst profile = await pool.run((signal) => fetch('/profile', { signal }).then((response) => response.json()));\n\nawait pool.idle();\npool.dispose();\n\nconsole.log(metadata, profile);\n```\n\n## cache loaded values\n\nuse `cache` for process local values. keys retain native `map` identity. `getorload` deduplicates concurrent loads for one key.\n\n```ts\nimport { cache } from '@vielzeug/arsenal/cache';\n\ntype profile = { id: string; name: string };\n\nconst profiles = cache<string, profile>({ capacity: 100, ttlms: 60_000 });\nconst profile = await profiles.getorload('me', () => fetch('/profile').then((response) => response.json()));\n\nconsole.log(profiles.entries());\nprofiles.delete('me');\nconst freshprofile = await profiles.getorload('me', () => fetch('/profile').then((response) => response.json()));\n\nconsole.log(profile, freshprofile);\n```\n\n## test deterministic randomness\n\nrandom helpers use cryptographic entropy by default. pass `randomsource` in tests when output must be deterministic.\n\n```ts\nimport { random, randomfloat, type randomsource } from '@vielzeug/arsenal/random';\n\nconst source: randomsource = { next: () => 0.5 };\n\nrandomfloat(source); // 0.5\nrandom(1, 4, source); // 3\n```\n\n## encode text for urls and qr codes\n\nshare codes ride in urls and qr payloads, where `+`, `/`, and padding `=` break consumers. the base64url codecs are environment independent: no `btoa`, no `buffer`, so the same encode/decode pair works in the browser, in workers, and in node.\n\n```ts\nimport { base64urltotext, texttobase64url } from '@vielzeug/arsenal';\n\nconst code = texttobase64url(json.stringify({ seat: 4, version: 2 }));\n// eyjzzwf0ijo0lcj2zxjzaw9uijoyfq: safe in a query string or qr\n\nconst payload = json.parse(base64urltotext(code));\n```\n\ndecoding throws on characters outside the base64url alphabet, so corrupted input fails loudly instead of decoding to garbage.\n\n## working with other vielzeug libraries\n\nuse spell after `tryparsejson` for typed external data. use vault instead of `cache` when data must survive reloads or process restart.\n\n```ts\nimport { tryparsejson } from '@vielzeug/arsenal/object';\nimport { s } from '@vielzeug/spell';\n\nconst settings = s.object({ theme: s.string() });\nconst parsed = tryparsejson(rawsettings);\nconst settings = parsed.ok ? settings.parse(parsed.value) : { theme: 'system' };\n```\n\n## best practices\n\n import common utilities from package root.\n import the broader toolkit from category entry points.\n prefer arsenal when a helper improves typing, intent, immutable updates, or edge case behavior.\n use the platform directly for exact aliases such as `crypto.randomuuid()` and `array.prototype.flat()`.\n pass `select` for every fuzzy search over objects.\n validate parsed json before using it as application data.\n use `parallel` for finite batches and `taskpool` for ongoing work.\n dispose task pools when their owner ends.\n use `cache` only for in memory data.\n inject `randomsource` in deterministic tests.\n",
|
|
9
|
-
"examples": " \ntitle: 'arsenal: examples'\ndescription: practical examples and recipes for arsenal.\n \n\n## examples\n\n [array utilities](./examples/array.md)\n [async utilities](./examples/async.md)\n [cache utilities](./examples/cache.md)\n [function utilities](./examples/function.md)\n [guards / typed predicates](./examples/typed.md)\n [math utilities](./examples/math.md)\n [object utilities](./examples/object.md): includes `getpath`, `tryparsejson`, `hash`, `diff`, `deepmerge`\n [random utilities](./examples/random.md)\n [string utilities](./examples/string.md)\n"
|
|
24
|
+
"api": " \ntitle: 'arsenal: api reference'\ndescription: reference for arsenal root utilities and category entry points.\n \n\n[[toc]]\n\n## api overview\n\n| symbol | purpose | execution | common gotcha |\n| | | | |\n| `groupby` | group values by key (prototype pollution guarded) | sync | root export |\n| `chunk` / `zip` / `compact` | typed collection transforms | sync | available from `/array` |\n| `range` / `average` / `median` | numeric generation and aggregation | sync | available from `/math` |\n| `assert` | assert and narrow a condition | sync | available from `/function` |\n| `retry` | retry async work | async | rethrows final error |\n| `taskpool` | bound concurrent tasks | async | available from `/async` |\n| `cache` | in memory identity keyed cache | async | available from `/cache` |\n| `fuzzyfilter` | filter string or selected object fields | sync | object collections require `select` |\n| `fuzzyscore` | rank string or selected object fields | sync | object collections require `select` |\n| `tryparsejson` | preserve json syntax result | sync | returns `unknown` on success |\n| `getpath` | optional object lookup (safe path guarded) | sync | available from `/object` |\n| `allocate` | lossless proportional distribution | sync | available from `/math` |\n| `isequal` | structural equality | sync | root export |\n| `hash` | deterministic serialization for cache keys | sync | available from `/object` |\n| `compressbytes` / `decompressbytes` | native byte compression with bounded decompression | async | requires native compression streams; decoding requires an output limit |\n| `pack` / `unpack` | versioned, compressed, url safe payload envelope | async | both sides bound the payload with `maxbytes`; compression falls back to raw |\n\n## package entry points\n\n| import | purpose |\n| | |\n| `@vielzeug/arsenal` | curated common utilities |\n| `@vielzeug/arsenal/array` | immutable transforms, set operations, sorting, fuzzy search |\n| `@vielzeug/arsenal/async` | retry, cancellation, task pool, timing |\n| `@vielzeug/arsenal/cache` | in memory cache and memoization |\n| `@vielzeug/arsenal/encoding` | base64url and native byte compression |\n| `@vielzeug/arsenal/function` | assertions, composition, timing, teardown |\n| `@vielzeug/arsenal/guards` | type guards, predicate combinators, equality guards |\n| `@vielzeug/arsenal/math` | ranges, aggregation, statistics, interpolation, exact allocation |\n| `@vielzeug/arsenal/object` | paths, transforms, hash, json parse result |\n| `@vielzeug/arsenal/random` | cryptographic entropy selection and shuffle |\n| `@vielzeug/arsenal/string` | unicode aware case transforms and similarity |\n\n## array\n\n### typed transforms\n\n```ts\nchunk<t>(input: readonly t[], size?: number): t[][]\ncompact<t>(array: readonly t[]): array<exclude<t, false | '' | 0 | 0n | null | undefined>>\nfirst<t>(array: readonly t[], fallback?: t): t | undefined\nlast<t>(array: readonly t[], fallback?: t): t | undefined\nreplace<t>(array: readonly t[], predicate: (value: t) => boolean, value: t): t[]\nrotate<t>(array: readonly t[], positions: number, options?: { wrap?: boolean }): t[]\nzip(...arrays): array<tuple>\nunzip(rows): tupleofarrays\n```\n\n`take`, `takelast`, `drop`, and `droplast` return new arrays and normalize negative counts to zero. set operations accept optional selectors for object identity.\n\n### fuzzyfilter / fuzzyscore\n\n```ts\nfuzzyfilter(strings: readonly string[], query: string, options?: fuzzyoptions): string[]\nfuzzyfilter<t>(items: readonly t[], query: string, options: fuzzyselection<t>): t[]\nfuzzyscore(strings: readonly string[], query: string, options?: fuzzyoptions): scoredresult<string>[]\nfuzzyscore<t>(items: readonly t[], query: string, options: fuzzyselection<t>): scoredresult<t>[]\n```\n\n`fuzzyfilter` preserves input order. `fuzzyscore` orders results by descending score.\n\n```ts\nimport { fuzzyfilter } from '@vielzeug/arsenal/array';\n\nconst users = [{ email: 'alice@example.com', name: 'alice' }];\nconst matches = fuzzyfilter(users, 'alice', { select: (user) => [user.name, user.email] });\n```\n\n \n\n## async\n\n### taskpool\n\n```ts\ninterface taskpool {\n run<t>(task: (signal: abortsignal) => promise<t>): promise<t>;\n idle(): promise<void>;\n dispose(reason?: unknown): void;\n readonly active: number;\n readonly pending: number;\n readonly disposed: boolean;\n readonly disposalsignal: abortsignal;\n}\n\ntaskpool(options?: { concurrency?: number }): taskpool\n```\n\n`dispose()` aborts running cooperative tasks and rejects pending tasks.\n\n```ts\nimport { taskpool } from '@vielzeug/arsenal/async';\n\nconst pool = taskpool({ concurrency: 2 });\nconst user = await pool.run((signal) => fetch('/user', { signal }).then((response) => response.json()));\npool.dispose();\n```\n\n \n\n## cache\n\n### cache\n\n```ts\ninterface cache<k, t> {\n entries(): readonlyarray<readonly [k, t]>;\n get(key: k): t | undefined;\n set(key: k, value: t, options?: { ttlms?: number }): void;\n getorload(key: k, load: () => promise<t>): promise<t>;\n delete(key: k): boolean;\n clear(): void;\n readonly size: number;\n}\n\ncache<k, t>(options?: cacheoptions): cache<k, t>\n```\n\nkeys use native `map` identity. expiry is lazy, evaluated by `get`, `getorload`, `set`, and `entries`. `entries()` trims expired values and returns a new insertion ordered snapshot without exposing internal storage. the `size` getter does not evict expired entries.\n\n```ts\nimport { cache } from '@vielzeug/arsenal/cache';\n\nconst profiles = cache<string, profile>({ ttlms: 60_000 });\nconst profile = await profiles.getorload('me', loadprofile);\n```\n\n \n\n## encoding\n\nurl safe base64 (`base64url`) codecs without `btoa`/`buffer`, so the same code runs in browsers, workers, and node.\n\n```ts\nimport { base64urltobytes, base64urltotext, bytestobase64url, texttobase64url, utf8bytes } from '@vielzeug/arsenal';\n\ntexttobase64url('{\"build\":\"hero\"}'); // never contains '+', '/', or '='\nbase64urltotext(texttobase64url('league of legends')); // round trips any utf 8 text\nbytestobase64url(uint8array.from([0, 1, 250])); // byte level codec for binary payloads\nbase64urltobytes(code); // throws on characters outside the alphabet\nutf8bytes('hunter äöü'); // 11: byte length of a utf 8 string\n```\n\n### compressbytes\n\n```ts\ncompressbytes(bytes: uint8array, options?: compressionoptions): promise<uint8array>\n```\n\ncompresses bytes using the native `compressionstream` api without modifying the input.\n\n| option | type | default | meaning |\n| | | | |\n| `format` | `compressionformat` | `'deflate raw'` | compression format; the receiver must use the same format |\n\n**returns:** compressed bytes. rejects with `arsenalserializationerror` when the format or capability is unavailable, or stream processing fails. does not fall back to another format.\n\n**example:** encode compressed bytes for a url.\n\n```ts\nimport { bytestobase64url, compressbytes } from '@vielzeug/arsenal';\n\nconst bytes = new textencoder().encode(json.stringify({ name: 'hunters' }));\nconst code = bytestobase64url(await compressbytes(bytes));\n```\n\n \n\n### decompressbytes\n\n```ts\ndecompressbytes(bytes: uint8array, options: decompressionoptions): promise<uint8array>\n```\n\ndecompresses bytes using the native `decompressionstream` api, enforcing the output limit while reading chunks.\n\n| option | type | default | meaning |\n| | | | |\n| `format` | `compressionformat` | `'deflate raw'` | format used by the sender |\n| `maxoutputbytes` | `number` | required | non negative safe integer; maximum decoded byte length |\n\n**returns:** decompressed bytes. rejects with `arsenalserializationerror` for invalid limits, corrupt or truncated data, unavailable formats/capabilities, or output exceeding the limit. a failed stream is canceled and its reader lock released. bound encoded input separately before calling this function.\n\n**example:** decode text within an application defined byte limit.\n\n```ts\nimport { compressbytes, decompressbytes } from '@vielzeug/arsenal';\n\nconst compressed = await compressbytes(new textencoder().encode('hunters'));\nconst bytes = await decompressbytes(compressed, { maxoutputbytes: 8192 });\nconst text = new textdecoder('utf 8', { fatal: true }).decode(bytes);\n```\n\n \n\n### pack\n\n```ts\npack(payload: unknown, options: sharecodeoptions): promise<string>\n```\n\nserializes `payload` as json and encodes it into a compact share code: `prefix` + mode marker + base64url bytes, compressed with raw deflate when the compressed form is shorter and the runtime supports the streams.\n\n| option | type | meaning |\n| | | |\n| `maxbytes` | `number` | positive safe integer; maximum serialized payload size |\n| `prefix` | `string` | 1–8 alphanumeric characters identifying the code family and version |\n\n**returns:** the shorter of the raw (`prefix` + `r`) and compressed (`prefix` + `d`) representations. throws `sharecodeerror` (`too large`, `invalid`) for oversized payloads and misconfigured options.\n\n**example:** encode a shareable build as a code small enough for a url.\n\n```ts\nimport { pack } from '@vielzeug/arsenal';\n\nconst code = await pack({ cards: ['slash', 'guard'] }, { maxbytes: 8192, prefix: 'ab1' });\n```\n\n \n\n### unpack\n\n```ts\nunpack(code: string, options: sharecodeoptions): promise<unknown>\n```\n\ndecodes a share code produced by `pack` with the same options. both sides are bounded: input longer than `maxbytes` could carry is refused up front, streamed decompression stops at `maxbytes`, and non canonical base64url is rejected by round trip.\n\n**returns:** the parsed json payload. throws `sharecodeerror` (`invalid` for foreign or malformed codes, `too large` for budget overruns, `compression unavailable` when a compressed code arrives on a runtime without `decompressionstream`). underlying compression failures surface as `arsenalserializationerror`.\n\n**example:** decode untrusted input within the byte budget.\n\n```ts\nimport { unpack } from '@vielzeug/arsenal';\n\nconst payload = await unpack(code, { maxbytes: 8192, prefix: 'ab1' });\n```\n\n## object\n\n### tryparsejson\n\n```ts\ntype jsonparseresult = { ok: true; value: unknown } | { error: syntaxerror; ok: false };\n\ntryparsejson(text: string): jsonparseresult\n```\n\nuse a schema validator after success to refine `unknown` data.\n\n```ts\nimport { tryparsejson } from '@vielzeug/arsenal/object';\n\nconst result = tryparsejson(raw);\nif (!result.ok) throw result.error;\n```\n\n### getpath\n\n```ts\ngetpath<t extends record<string, unknown>, p extends string>(item: t, path: p): pathvalue<t, p> | undefined\ngetpathor<t extends record<string, unknown>, p extends string, f>(item: t, path: p, fallback: f): pathvalue<t, p> | f\nrequirepath<t extends record<string, unknown>, p extends string>(item: t, path: p): exclude<pathvalue<t, p>, undefined>\n```\n\n### hash\n\n```ts\nhash(value: unknown, options?: hashoptions): string\n```\n\nproduces a deterministic, order independent serialization. handles `date`, `regexp`, `set`, `map`, and `bigint`. circular references produce a sentinel. useful for stable cache keys.\n\n \n\n## math\n\n### numeric toolkit\n\n```ts\nrange(stop: number): number[]\nrange(start: number, stop: number, step?: number): number[]\nclamp(value: number, min?: number, max?: number): number\nsum<t>(values: readonly t[], select?: (value: t) => number): number\naverage<t>(values: readonly t[], select?: (value: t) => number): number | undefined\nmedian<t>(values: readonly t[], select?: (value: t) => number): number | undefined\nvariance<t>(values: readonly t[], select?: (value: t) => number): number\nstandarddeviation<t>(values: readonly t[], select?: (value: t) => number): number\n```\n\n`sum` and `average` reject non finite values. `gcd` and `lcm` require safe integers. `linspace` requires finite bounds and a positive integer point count. `variance` and `standarddeviation` compute population statistics.\n\n### allocate\n\n```ts\nallocate(amount: number, ratios: number[] | number): number[]\nallocate(amount: bigint, ratios: number[] | number): bigint[]\n```\n\ndistributes an amount proportionally across ratios. the indivisible remainder is applied to the last bucket so the sum equals the original amount exactly: critical for financial operations.\n\n## types\n\n```ts\ntype fuzzyoptions = {\n normalize?: boolean;\n threshold?: number;\n};\n\ntype fuzzyselection<t> = fuzzyoptions & {\n select: (item: t) => string | readonly string[];\n};\n\ntype scoredresult<t> = { item: t; score: number };\n\ntype cacheoptions = {\n capacity?: number;\n now?: () => number;\n ttlms?: number;\n};\n\ninterface compressionoptions {\n format?: compressionformat;\n}\n\ninterface decompressionoptions extends compressionoptions {\n maxoutputbytes: number;\n}\n```\n\n## errors\n\n `rangeerror`: invalid numeric bounds, capacity, concurrency, or retry count.\n `typeerror`: invalid value types, unsupported comparison, or required path missing.\n `arsenalserializationerror`: memo or hash cannot serialize supplied input; compression fails, is unavailable, receives an invalid output limit, or exceeds that limit. stream failures retain the original error as `cause`.\n `sharecodeerror`: a share code cannot be encoded or decoded; the `reason` field is `invalid`, `too large`, or `compression unavailable`.\n",
|
|
25
|
+
"usage": " \ntitle: 'arsenal: usage guide'\ndescription: use arsenal root utilities for common work and category entry points for specialized collection, async, cache, object, and string behavior.\n \n\n[[toc]]\n\n## basic usage\n\nstart at package root for common utilities. move to a category entry point for the broader typed toolkit. arsenal keeps helpers that improve readability, narrowing, immutable updates, or edge case consistency; exact platform aliases stay out.\n\n```ts\nimport { groupby, retry } from '@vielzeug/arsenal';\n\nconst users = [\n { id: 'a1', role: 'admin' },\n { id: 'u1', role: 'user' },\n { id: 'u2', role: 'user' },\n];\n\nconst byrole = groupby(users, (user) => user.role);\nconst health = await retry(() => fetch('/health').then((response) => response.json()));\n\nconsole.log(byrole, health);\n```\n\nuse category imports for apis absent from root:\n\n```ts\nimport { fuzzyfilter } from '@vielzeug/arsenal/array';\nimport { taskpool } from '@vielzeug/arsenal/async';\nimport { cache } from '@vielzeug/arsenal/cache';\nimport { tryparsejson } from '@vielzeug/arsenal/object';\n```\n\n## transform collections\n\nuse `/array` for transforms that preserve input immutability. `filtermap` combines mapping and omission; `indexby` and `groupby` build lookup structures without mutation.\n\n```ts\nimport { filtermap, indexby, sort } from '@vielzeug/arsenal/array';\n\nconst products = [\n { id: 'p1', price: 20, published: true },\n { id: 'p2', price: 10, published: false },\n { id: 'p3', price: 15, published: true },\n];\n\nconst publishedlabels = filtermap(products, (product) =>\n product.published ? `${product.id}: ${product.price}` : undefined,\n);\nconst byid = indexby(products, (product) => product.id);\nconst byprice = sort(products, (product) => product.price);\n\nconsole.log(publishedlabels, byid, byprice);\n```\n\n## search explicit fields\n\nsearch string arrays directly. object collections require `select`, so callers define exactly what can match.\n\n```ts\nimport { fuzzyfilter, fuzzyscore } from '@vielzeug/arsenal/array';\n\nconst users = [\n { email: 'alice@example.com', name: 'alice' },\n { email: 'bob@example.com', name: 'bob' },\n];\n\nconst matches = fuzzyfilter(users, 'alice', { select: (user) => [user.name, user.email] });\nconst ranked = fuzzyscore(users, 'ali', { select: (user) => user.name });\n```\n\n## work with object data\n\nuse `/object` for paths, key selection, stable cache keys, and object transforms.\n\n```ts\nimport { getpathor, hash, omit, pick } from '@vielzeug/arsenal/object';\n\nconst config = { api: { host: 'localhost', port: 3000 }, debug: true };\nconst port = getpathor(config, 'api.port', 8080);\nconst publicconfig = pick(config, ['api']);\nconst productionconfig = omit(config, ['debug']);\nconst key = hash({ port, productionconfig });\n\nconsole.log(publicconfig, key);\n```\n\n## parse and validate json\n\n`tryparsejson` distinguishes syntax failure from schema failure. treat successful values as `unknown`, then validate with spell or application code.\n\n```ts\nimport { tryparsejson } from '@vielzeug/arsenal/object';\nimport { s } from '@vielzeug/spell';\n\nconst user = s.object({ id: s.string(), name: s.string() });\nconst parsed = tryparsejson(raw);\n\nif (!parsed.ok) throw parsed.error;\n\nconst user = user.parse(parsed.value);\n```\n\n## bound concurrent work\n\nuse `parallel` for one finite collection. use `taskpool` when tasks arrive over time or need disposal.\n\n```ts\nimport { parallel, taskpool } from '@vielzeug/arsenal/async';\n\nconst metadata = await parallel(urls, (url) => fetch(url).then((response) => response.json()), { limit: 4 });\n\nconst pool = taskpool({ concurrency: 2 });\nconst profile = await pool.run((signal) => fetch('/profile', { signal }).then((response) => response.json()));\n\nawait pool.idle();\npool.dispose();\n\nconsole.log(metadata, profile);\n```\n\n## cache loaded values\n\nuse `cache` for process local values. keys retain native `map` identity. `getorload` deduplicates concurrent loads for one key.\n\n```ts\nimport { cache } from '@vielzeug/arsenal/cache';\n\ntype profile = { id: string; name: string };\n\nconst profiles = cache<string, profile>({ capacity: 100, ttlms: 60_000 });\nconst profile = await profiles.getorload('me', () => fetch('/profile').then((response) => response.json()));\n\nconsole.log(profiles.entries());\nprofiles.delete('me');\nconst freshprofile = await profiles.getorload('me', () => fetch('/profile').then((response) => response.json()));\n\nconsole.log(profile, freshprofile);\n```\n\n## test deterministic randomness\n\nrandom helpers use cryptographic entropy by default. pass `randomsource` in tests when output must be deterministic.\n\n```ts\nimport { random, randomfloat, type randomsource } from '@vielzeug/arsenal/random';\n\nconst source: randomsource = { next: () => 0.5 };\n\nrandomfloat(source); // 0.5\nrandom(1, 4, source); // 3\n```\n\n## encode text for urls and qr codes\n\nshare codes ride in urls and qr payloads, where `+`, `/`, and padding `=` break consumers. the base64url codecs are environment independent: no `btoa`, no `buffer`, so the same encode/decode pair works in the browser, in workers, and in node.\n\n```ts\nimport { base64urltotext, texttobase64url } from '@vielzeug/arsenal';\n\nconst code = texttobase64url(json.stringify({ seat: 4, version: 2 }));\n// eyjzzwf0ijo0lcj2zxjzaw9uijoyfq: safe in a query string or qr\n\nconst payload = json.parse(base64urltotext(code));\n```\n\ndecoding throws on characters outside the base64url alphabet, so corrupted input fails loudly instead of decoding to garbage.\n\n## compress share payloads\n\ncompress utf 8 bytes before base64url encoding. set a decoded byte limit before accepting compressed input from another device.\n\n```ts\nimport { base64urltobytes, bytestobase64url, compressbytes, decompressbytes } from '@vielzeug/arsenal';\n\nconst payload = json.stringify({ name: 'hunters', notes: 'an expedition through alborea. '.repeat(20) });\nconst compressed = await compressbytes(new textencoder().encode(payload));\nconst code = bytestobase64url(compressed);\nconst bytes = await decompressbytes(base64urltobytes(code), { maxoutputbytes: 8192 });\nconst restored = new textdecoder('utf 8', { fatal: true }).decode(bytes);\nconsole.log(restored === payload);\n```\n\nboth helpers default to `deflate raw`. you can explicitly select `gzip` or `deflate`; use the same format at both ends. the helpers reject when native streams do not support the selected format. they do not choose a fallback or define a wire protocol.\n\nfor a complete versioned envelope over these primitives — prefix, compression mode marker, byte budgets on both sides — see [`pack` / `unpack`](./api.md#pack).\n\ncompression can enlarge small inputs. compare the final encoded strings, including any format marker, before choosing a representation. bound input size before decoding base64url and validate decompressed json against your schema. the decompression limit bounds retained output, not native stream working memory.\n\n## working with other vielzeug libraries\n\nuse spell after `tryparsejson` for typed external data. use vault instead of `cache` when data must survive reloads or process restart.\n\n```ts\nimport { tryparsejson } from '@vielzeug/arsenal/object';\nimport { s } from '@vielzeug/spell';\n\nconst settings = s.object({ theme: s.string() });\nconst parsed = tryparsejson(rawsettings);\nconst settings = parsed.ok ? settings.parse(parsed.value) : { theme: 'system' };\n```\n\n## best practices\n\n import common utilities from package root.\n import the broader toolkit from category entry points.\n prefer arsenal when a helper improves typing, intent, immutable updates, or edge case behavior.\n use the platform directly for exact aliases such as `crypto.randomuuid()` and `array.prototype.flat()`.\n pass `select` for every fuzzy search over objects.\n validate parsed json before using it as application data.\n use `parallel` for finite batches and `taskpool` for ongoing work.\n dispose task pools when their owner ends.\n use `cache` only for in memory data.\n inject `randomsource` in deterministic tests.\n",
|
|
26
|
+
"examples": " \ntitle: 'arsenal: examples'\ndescription: practical examples and recipes for arsenal.\n \n\n## examples\n\n [array utilities](./examples/array.md)\n [async utilities](./examples/async.md)\n [cache utilities](./examples/cache.md)\n [encoding utilities](./examples/encoding.md)\n [function utilities](./examples/function.md)\n [guards / typed predicates](./examples/typed.md)\n [math utilities](./examples/math.md)\n [object utilities](./examples/object.md): includes `getpath`, `tryparsejson`, `hash`, `diff`, `deepmerge`\n [random utilities](./examples/random.md)\n [string utilities](./examples/string.md)\n"
|
|
10
27
|
},
|
|
11
28
|
"examples": [
|
|
12
29
|
{
|
|
@@ -61,6 +78,10 @@
|
|
|
61
78
|
"id": "async-waitFor",
|
|
62
79
|
"text": "waitfor poll until condition is true or timeout/abort fires import { waitfor } from '@vielzeug/arsenal/async'\n\n// simulate a value that becomes ready after a short delay\nlet ready = false\nsettimeout(() => { ready = true }, 200)\n\nconsole.log('waiting for ready...')\nawait waitfor(() => ready, { interval: 50, timeout: 2000 })\nconsole.log('ready!')\n\n// abort early with an external signal\nconst ac = new abortcontroller()\nsettimeout(() => ac.abort(new error('user cancelled')), 100)\n\ntry {\n await waitfor(() => false, {\n interval: 50,\n signal: ac.signal,\n timeout: 5000,\n })\n} catch (err) {\n console.log('aborted:', err.message) // 'user cancelled'\n}"
|
|
63
80
|
},
|
|
81
|
+
{
|
|
82
|
+
"id": "encoding-compression",
|
|
83
|
+
"text": "compressbytes bounded share payload compression import { base64urltobytes, bytestobase64url, compressbytes, decompressbytes } from '@vielzeug/arsenal'\n\nconst text = json.stringify({ notes: 'an expedition through alborea. '.repeat(20) })\nconst original = new textencoder().encode(text)\nconst code = bytestobase64url(await compressbytes(original))\nconst restored = await decompressbytes(base64urltobytes(code), { maxoutputbytes: 8192 })\n\nconsole.log('original bytes:', original.length)\nconsole.log('compressed code characters:', code.length)\nconsole.log('matches original:', new textdecoder('utf 8', { fatal: true }).decode(restored) === text)"
|
|
84
|
+
},
|
|
64
85
|
{
|
|
65
86
|
"id": "function-debounce",
|
|
66
87
|
"text": "debounce trailing (default) and leading edge options import { debounce } from '@vielzeug/arsenal/function'\n\n// trailing (default) \nlet trailingcount = 0\nconst onsearch = debounce((q) => {\n trailingcount++\n console.log(`trailing #${trailingcount}: \"${q}\"`)\n}, 200)\n\nonsearch('c')\nonsearch('ca')\nonsearch('cat') // only this fires after 200ms\n\n// leading only \nlet leadingcount = 0\nconst onsubmit = debounce((q) => {\n leadingcount++\n console.log(`leading #${leadingcount}: \"${q}\"`)\n}, 200, { leading: true, trailing: false })\n\nonsubmit('first') // fires immediately\nonsubmit('second') // silenced (within 200ms window)\nonsubmit('third') // silenced\n\n// leading + trailing: fires on both edges \nlet bothcount = 0\nconst onboth = debounce(() => {\n bothcount++\n console.log(`both edge #${bothcount}`)\n}, 200, { leading: true, trailing: true })\n\nonboth() // fires immediately (leading)\n// trailing edge fires after 200ms (bothcount becomes 2)\n\nsettimeout(() => {\n console.log('trailing fires:', trailingcount, '| leading fires:', leadingcount, '| both fires:', bothcount)\n // trailing: 1 | leading: 1 | both: 2\n}, 400)"
|
|
@@ -135,7 +156,7 @@
|
|
|
135
156
|
"name": "@vielzeug/arsenal",
|
|
136
157
|
"related": "tempo sourcerer spell coins",
|
|
137
158
|
"slug": "arsenal",
|
|
138
|
-
"source": "export * from './array/chunk';\nexport * from './array/filtermap';\nexport * from './array/groupby';\nexport * from './array/indexby';\nexport * from './array/partition';\nexport * from './array/sort';\nexport * from './array/uniq';\nexport * from './async/abortable';\nexport * from './async/aborterror';\nexport * from './async/attempt';\nexport * from './async/backoff';\nexport * from './async/parallel';\nexport * from './async/retry';\nexport * from './async/sleep';\nexport * from './async/taskpool';\nexport * from './async/waitfor';\nexport * from './cache/cache';\nexport * from './cache/memo';\nexport * from './encoding/base64url';\nexport * from './errors';\nexport * from './function/debounce';\nexport * from './function/once';\nexport * from './function/pipe';\nexport * from './function/tapper';\nexport * from './function/throttle';\nexport * from './guards/combinators';\nexport * from './guards/isdefined';\nexport * from './guards/isequal';\nexport * from './guards/isnil';\nexport * from './guards/isplainobject';\nexport * from './math/allocate';\nexport * from './math/clamp';\nexport * from './math/range';\nexport * from './object/getpath';\nexport * from './object/hash';\nexport * from './object/omit';\nexport * from './object/pick';\nexport * from './random/random';\nexport * from './random/source';\nexport * from './string/camelcase';\nexport * from './types';\n"
|
|
159
|
+
"source": "export * from './array/chunk';\nexport * from './array/filtermap';\nexport * from './array/groupby';\nexport * from './array/indexby';\nexport * from './array/partition';\nexport * from './array/sort';\nexport * from './array/uniq';\nexport * from './async/abortable';\nexport * from './async/aborterror';\nexport * from './async/attempt';\nexport * from './async/backoff';\nexport * from './async/parallel';\nexport * from './async/retry';\nexport * from './async/sleep';\nexport * from './async/taskpool';\nexport * from './async/waitfor';\nexport * from './cache/cache';\nexport * from './cache/memo';\nexport * from './encoding/base64url';\nexport * from './encoding/compression';\nexport * from './encoding/share code';\nexport * from './errors';\nexport * from './function/debounce';\nexport * from './function/once';\nexport * from './function/pipe';\nexport * from './function/tapper';\nexport * from './function/throttle';\nexport * from './guards/combinators';\nexport * from './guards/isdefined';\nexport * from './guards/isequal';\nexport * from './guards/isnil';\nexport * from './guards/isplainobject';\nexport * from './math/allocate';\nexport * from './math/clamp';\nexport * from './math/range';\nexport * from './object/getpath';\nexport * from './object/hash';\nexport * from './object/omit';\nexport * from './object/pick';\nexport * from './random/random';\nexport * from './random/source';\nexport * from './string/camelcase';\nexport * from './types';\n"
|
|
139
160
|
},
|
|
140
161
|
{
|
|
141
162
|
"category": "testing",
|
|
@@ -314,9 +335,9 @@
|
|
|
314
335
|
"description": "a framework neutral http client with explicit cached reads, prefetching, immutable middleware, typed paths, and structured errors.",
|
|
315
336
|
"docs": {
|
|
316
337
|
"index": " \ntitle: 'courier: http client'\ndescription: a framework neutral http client with explicit cached reads, prefetching, immutable middleware, typed paths, and structured errors.\npackage: courier\ncategory: http\nkeywords: [http client, fetch, cache, prefetch, middleware, interceptors, errors]\nrelated: [flux, spell, postmaster]\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, error classification, middleware composition, and repeated read coordination to each application. courier keeps those concerns in one client, including explicit cached gets and warm cache prefetching, while observable query and mutation state remain consumer owned.\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\nconst user = await courier.get<user>('/users/{id}', { params: { id: userid } });\n```\n\n| feature | courier | ky | ofetch |\n| | | | |\n| bundle size | <packageinfo package=\"courier\" type=\"size\" /> | separate package | separate package |\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| native fetch transport | <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| explicit parsed value cache and prefetch | <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| immutable middleware at construction | <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| structured error taxonomy | <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\n<div class=\"decision callout\">\n\n**use courier when** one client should own typed http, explicit cached gets and prefetching, immutable middleware, and structured errors, while a separate state layer owns observable queries and mutations.\n\n**consider ky when** you only need a compact fetch wrapper. **consider ofetch when** you want a bundled fetch utility with its own retry and caching conventions.\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 transport client for an application or request scope, then use method conveniences for standard http methods and `request()` for custom methods.\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' });\n\ntry {\n const user = await courier.get<user>('/users/{id}', { params: { id: 42 } });\n console.log(user.name);\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, immutable middleware pipeline, header defaults, and cancellation boundary.\n **`request()`**: one contract for every http method; pass `method` in the config.\n **cached `get()`**: opt in with a structured key and optional per read ttl; successful parsed values share bounded storage and in flight work.\n **`prefetch()`**: await cache warming while request failures remain observable through `tap()`.\n **`invalidatecache()` / `clearcache()`**: invalidate structured key prefixes without query state.\n **`get()` / `post()` / `put()` / `patch()` / `delete()`**: conveniences for standard methods.\n **`withbearerauth()` / `withrequestid()` / `withlogging()`**: composable middleware configured at construction.\n **`tap()`**: signal owned structured transport observation.\n **error taxonomy**: http, network, timeout, abort, parse, and schema failures are distinct, actionable classes.\n **`cancelall()` / `dispose()`**: abort active requests and tear down the transport.\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/): composes async streams; pair with courier transport calls in the owning state layer.\n [spell](/spell/): validates parsed http payloads through courier's `schema` option.\n [postmaster](/postmaster/): coordinates durable delivery of courier requests through a job outbox.\n\n</div>\n\n<! markdownlint enable md025 md033 md060 >\n",
|
|
317
|
-
"api": " \ntitle: 'courier: api reference'\ndescription: reference for courier http, cached read, prefetch, middleware, and error apis.\n \n\n[[toc]]\n\n## api overview\n\n| symbol | purpose | execution mode | common gotcha |\n| | | | |\n| `createcourier()` | creates transport client | sync | dispose only when whole scope ends |\n| `request()` | sends one http request of any method | async | direct calls never deduplicate |\n| `get()` | sends a get, optionally through the explicit cache | async | one key must identify one representation |\n| `prefetch()` | warms a cached get | async | request failures resolve after surfacing through `tap()` |\n| `invalidatecache()` / `clearcache()` | removes cached values | sync | invalidation matches structured key prefixes |\n| `post()` / `put()` / `patch()` / `delete()` | common write method conveniences | async | use `request()` for custom methods |\n| `withbearerauth()` | adds authorization middleware | sync | token provider runs per request |\n| `withrequestid()` | adds request identifier middleware | sync | default generator uses `crypto.randomuuid()` |\n| `withlogging()` | logs request result metadata | sync | requires explicit logger; urls may contain sensitive query values |\n| `tap()` | observe structured transport events | sync | observation only; optional signal owns lifetime |\n\n## package entry point\n\n| import | purpose |\n| | |\n| `@vielzeug/courier` | client factory, errors, middleware helpers, and public types |\n\n## client\n\n### `createcourier()`\n\n```ts\ncreatecourier(options?: courieroptions): courier;\n```\n\nreturns an http client sharing base url, default headers, immutable middleware, optional parsed value cache, timeout, and cancellation lifecycle.\n\n| `courieroptions` field | type | default | description |\n| | | | |\n| `baseurl` | `string` | `''` | prefix for relative request paths |\n| `cache.capacity` | `number` | `100` | maximum cached entries; positive integer or `infinity` |\n| `cache.ttlms` | `number` | `30_000` | default cache freshness; non negative milliseconds or `infinity` |\n| `fetch` | `typeof globalthis.fetch` | `globalthis.fetch` | fetch implementation |\n| `headers` | `headersinit` | `{}` | default request headers (immutable; merged lowercase) |\n| `middleware` | `readonly middleware[]` | `[]` | immutable middleware chain configured at construction |\n| `timeout` | `number` | `30_000` | integer milliseconds from 1 to 2,147,483,647, or `infinity` |\n\n**returns:** `courier`.\n\n```ts\nimport { createcourier, withbearerauth } from '@vielzeug/courier';\n\nconst courier = createcourier({\n baseurl: 'https://api.example.com',\n middleware: [withbearerauth('token')],\n});\n```\n\n| `courier` member | signature | description |\n| | | |\n| `request` | `<t, p>(path, config?) => promise<t>` | sends one http request; `config.method` defaults to `get` |\n| `get` | `<t, p>(path, config?) => promise<t>` | sends get; `cache` opts parsed responses into bounded caching |\n| `prefetch` | `<t, p>(path, config) => promise<void>` | warms one required structured cache key; failures are tap observed |\n| `invalidatecache` | `(prefix: couriercachekey) => void` | invalidates matching settled and pending key prefixes |\n| `clearcache` | `() => void` | removes every cached and pending entry |\n| `post` | `<t, p>(path, config?) => promise<t>` | sends post |\n| `put` | `<t, p>(path, config?) => promise<t>` | sends put |\n| `patch` | `<t, p>(path, config?) => promise<t>` | sends patch |\n| `delete` | `<t, p>(path, config?) => promise<t>` | sends delete |\n| `tap` | `(handler, options?: { signal?: abortsignal }) => () => void` | observes structured request and disposal events |\n| `cancelall` | `() => void` | aborts active requests; client remains usable |\n| `dispose` | `() => void` | final teardown; aborts work and marks client unusable |\n| `disposed` | `boolean` | whether final disposal occurred |\n| `disposalsignal` | `abortsignal` | aborts on final disposal |\n\n \n\n## requests\n\n### `request()`\n\n```ts\nrequest<t, p extends string>(path: p, config?: requestconfig<p, t> & { method?: string }): promise<t>;\n```\n\nsends one http request. `config.method` defaults to `get`; pass any method (including custom verbs) to support it consistently.\n\n| `requestconfig<p, t>` field | type | description |\n| | | |\n| `method` | `string` | http method; defaults to `get` |\n| `params` | `record<string, string \\| number \\| boolean>` | path parameters for `{param}` placeholders |\n| `query` | `params` | query string parameters |\n| `body` | `unknown` | bodyinit values pass through; streams use node duplex; get/head reject bodies; other values encode as json |\n| `headers` | `headersinit` | per request headers merged with (and overriding) defaults |\n| `responsetype` | `responsetype` | response parsing strategy |\n| `schema` | `{ parse(data: unknown): t }` | optional parsed response validator; incompatible with `responsetype: 'raw'` |\n| `signal` | `abortsignal` | external abort signal merged with internal cancellation |\n| `timeout` | `number` | integer milliseconds from 1 to 2,147,483,647, or `infinity`; overrides default |\n| `fetchinit` | `omit<requestinit, 'body' \\| 'headers' \\| 'method' \\| 'signal'>` | raw fetch options for advanced use |\n\n**returns:** parsed response body (or `undefined` for empty bodies).\n\n```ts\nconst created = await courier.request<user>('/users', {\n method: 'post',\n body: { name: 'ada' },\n});\n```\n\n### `get()`\n\n```ts\nget<t, p extends string>(path: p, config?: getrequestconfig<p, t>): promise<t>;\n```\n\nconvenience for `request(path, { ...config, method: 'get' })`. a `cache` descriptor opts a parsed response into the bounded cache. it requires a structured key and may override the default ttl. raw responses cannot be cached.\n\n```ts\nconst user = await courier.get<user>('/users/{id}', {\n cache: { key: ['users', 1], ttlms: 10_000 },\n params: { id: 1 },\n});\n```\n\n## cache\n\n### `prefetch()`\n\n```ts\nprefetch<t, p extends string>(path: p, config: prefetchconfig<p, t>): promise<void>;\n```\n\nstarts the same cached get used by `get()`. `config.cache` is required; `signal`, per call `timeout`, and `responsetype: 'raw'` are unavailable. fresh values and pending loads are reused. the promise settles when the attempt finishes. request failures emit `request error` through `tap()` and resolve without caching data.\n\n### `invalidatecache()` and `clearcache()`\n\n```ts\ninvalidatecache(prefix: couriercachekey): void;\nclearcache(): void;\n```\n\n`invalidatecache()` removes every settled entry whose structured key starts with `prefix` and prevents matching pending loads from being stored. `clearcache()` applies that behavior to all entries. existing waiters may still receive an invalidated in flight result; `cancelall()` or `dispose()` aborts physical work.\n\n \n\n## middleware\n\nmiddleware is an immutable chain configured at construction. each middleware receives an immutable `fetchcontext` and a `next` function.\n\n```ts\ntype middleware = (ctx: fetchcontext, next: (ctx: fetchcontext) => promise<response>) => promise<response>;\n```\n\n### middleware helpers\n\n```ts\nwithbearerauth(token: string | (() => string | null | undefined | promise<string | null | undefined>)): middleware;\nwithrequestid(options?: { generate?: () => string; header?: string }): middleware;\nwithlogging(options: {\n logger: (message: string, meta: { duration: number; method: string; status: number; url: string }) => void;\n}): middleware;\n```\n\neach helper returns a `middleware` passed in the `middleware` option. `withlogging` requires an explicit `logger` function, isolates logger failures from requests, and has no default console output.\n\n```ts\nconst courier = createcourier({\n middleware: [\n withbearerauth(() => tokenstore.getaccesstoken()),\n withrequestid(),\n withlogging({ logger: (msg) => console.log(msg) }),\n ],\n});\n```\n\n## types\n\n```ts\ntype transportoptions = {\n baseurl?: string;\n fetch?: typeof globalthis.fetch;\n headers?: headersinit;\n middleware?: readonly middleware[];\n timeout?: number;\n};\n\ntype couriercachekeyatom = string | number | boolean | null;\ntype couriercachekey = readonly [couriercachekeyatom, ...couriercachekeyatom[]];\n\ntype couriercacheoptions = {\n capacity?: number;\n ttlms?: number;\n};\n\ntype courierreadcache = {\n key: couriercachekey;\n ttlms?: number;\n};\n\ntype courieroptions = transportoptions & {\n cache?: couriercacheoptions;\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 | undefined>): fetchcontext;\n withinit(updates: partial<omit<requestinit, 'body' | 'headers' | 'method' | 'signal'>>): fetchcontext;\n};\n\ntype middleware = (ctx: fetchcontext, next: (ctx: fetchcontext) => promise<response>) => promise<response>;\n\ntype courierevent =\n | { type: 'request start'; method: string; url: string }\n | { type: 'request success'; method: string; url: string; status: number; duration: number }\n | { type: 'request error'; method: string; url: string; error: unknown }\n | { type: 'dispose' };\n```\n\n```ts\ntype paramvalue = string | number | boolean | null | readonly (string | number | boolean | null)[] | undefined;\ntype params = record<string, paramvalue>;\ntype responsetype = 'auto' | 'json' | 'text' | 'blob' | 'arraybuffer' | 'raw';\ntype requestconfig<p extends string = string, t = unknown> = {\n body?: unknown;\n method?: string;\n fetchinit?: omit<requestinit, 'body' | 'headers' | 'method' | 'signal'>;\n headers?: headersinit;\n params?: record<string, string | number | boolean>;\n query?: params;\n responsetype?: responsetype;\n schema?: { parse(data: unknown): t };\n signal?: abortsignal;\n timeout?: number;\n};\n\ntype getrequestconfig<p extends string = string, t = unknown> = omit<\n requestconfig<p, t>,\n 'body' | 'method' | 'responsetype'\n> &\n ({ cache?: courierreadcache; responsetype?: exclude<responsetype, 'raw'> } | { cache?: never; responsetype: 'raw' }) & {\n body?: never;\n };\n\ntype prefetchconfig<p extends string = string, t = unknown> = omit<\n getrequestconfig<p, t>,\n 'cache' | 'responsetype' | 'signal' | 'timeout'\n> & {\n cache: courierreadcache;\n responsetype?: exclude<responsetype, 'raw'>;\n signal?: never;\n timeout?: never;\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 or client cancellation | `method`, `url`, `cause` |\n| `courierschemavalidationerror` | response schema fails | `data`, `cause` |\n| `courierparseerror` | response body cannot parse | n/a |\n| `courierconfigerror` | invalid request config (get/head body, raw + schema) | n/a |\n| `courierdisposederror` | work starts after disposal | n/a |\n\nerrors thrown directly by custom middleware are preserved rather than reclassified.\n",
|
|
318
|
-
"usage": " \ntitle: 'courier: usage guide'\ndescription: use one courier client for typed http, explicit cached reads, prefetching, immutable middleware, and structured errors.\n \n\n[[toc]]\n\n## basic usage\n\ncreate one courier client for an application or request scope. its transport policy, middleware, and disposal lifecycle apply to every request.\n\n```ts\nimport { createcourier } from '@vielzeug/courier';\n\ntype user = { id: number; name: string };\n\nconst courier = createcourier({ baseurl: 'https://api.example.com' });\nconst user = await courier.get<user>('/users/{id}', { params: { id: 1 } });\nconsole.log(user.name);\n```\n\n## http requests\n\nuse `get()`, `post()`, `put()`, `patch()`, and `delete()` for standard methods. use `request()` when the method is custom or selected dynamically. courier preserves root relative paths, encodes path parameters, serializes json compatible bodies, and parses successful responses.\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\n`get()`, `post()`, `put()`, `patch()`, and `delete()` cover common methods. use `request()` for custom verbs or when a method is selected dynamically.\n\n## default headers\n\nset default headers at construction. they are merged (lowercased) into every request and cannot be mutated at runtime: middleware handles dynamic headers.\n\n```ts\nconst courier = createcourier({\n baseurl: 'https://api.example.com',\n headers: { 'x client': 'web' },\n});\n```\n\n## middleware\n\nmiddleware is an immutable chain configured once at construction. each middleware receives an immutable `fetchcontext` and a `next` function. use `ctx.withheaders()` or `ctx.withinit()` to derive a new context: never mutate `ctx` directly.\n\n```ts\nimport { withbearerauth, withrequestid, createcourier } from '@vielzeug/courier';\n\nconst courier = createcourier({\n baseurl: 'https://api.example.com',\n middleware: [\n withbearerauth(() => sessionstorage.getitem('access token') ?? ''),\n withrequestid(),\n ],\n});\n```\n\na bearer token provider may return `null`, `undefined`, or an empty string to omit a stale authorization header. use `withlogging()` with an explicit `logger` function to log requests during local development. `withlogging()` includes full urls, so sanitize query values before persistent logging.\n\n```ts\nimport { withlogging, createcourier } from '@vielzeug/courier';\n\nconst courier = createcourier({\n baseurl: 'https://api.example.com',\n middleware: [withlogging({ logger: (msg) => console.log(msg) })],\n});\n```\n\nuse `tap()` for multiple structured transport observers. it emits request start, request success, request error, and dispose events; observer failures cannot alter requests. pass `{ signal }` to own listener lifetime.\n\n```ts\nconst stop = courier.tap((event) => metrics.record(event), { signal: routesignal });\n```\n\nbecause middleware is immutable, a client's behavior is locally understandable from its construction call. to change policy, create a new client.\n\n## cached reads and prefetching\n\nopt parsed gets into caching with a structured key. courier uses a finite capacity of 100 entries and a 30 second ttl by default; client options change those defaults, and each read may override ttl. successful parsed and schema validated values are cached, while errors are not.\n\n```ts\nconst courier = createcourier({\n baseurl: 'https://api.example.com',\n cache: { capacity: 200, ttlms: 60_000 },\n});\nconst cache = { key: ['accounts', accountid, 'users', userid] as const, ttlms: 10_000 };\n\nawait courier.prefetch<user>('/users/{id}', { cache, params: { id: userid } });\nconst user = await courier.get<user>('/users/{id}', { cache, params: { id: userid } });\n```\n\n`prefetch()` returns a promise that settles when warming finishes. request failures are not cached or rejected from that promise; the physical request emits `request error` through `tap()`. cache, key, and url configuration errors still throw before prefetch starts.\n\ninvalidate a structured prefix after writes. every matching settled or pending key becomes ineligible for caching, while unrelated entries remain available.\n\n```ts\nawait courier.patch('/users/{id}', { body: update, params: { id: userid } });\ncourier.invalidatecache(['accounts', accountid, 'users']);\n```\n\na cache key must identify one response representation, including a stable non secret principal or tenant identifier, schema, and relevant request variants. never place credentials in a key; call `clearcache()` when identity changes. cached objects are shared by reference.\n\nconcurrent callers share physical work but own their own signal and timeout. one cancelled caller does not affect the others. when every caller cancels and no prefetch owns the load, courier aborts the physical request. `cancelall()` always aborts physical work; `dispose()` also clears settled cache data.\n\n## response parsing\n\ncourier parses successful 2xx response bodies automatically based on `content type`, unless you set `responsetype` explicitly (`'json'`, `'text'`, `'blob'`, `'arraybuffer'`, or `'raw'` for the `response` itself). empty json bodies return `undefined` instead of throwing. raw response bodies remain owned by courier until consumed or cancelled, so `cancelall()` and `dispose()` still abort them. raw responses cannot be combined with `schema`. always consume or cancel the body to release request ownership, and set `timeout: infinity` for intentionally long lived streams. courier preserves url, redirect, and type metadata on tracked responses and their clones.\n\n```ts\nconst buffer = await courier.request('/files/{id}', {\n method: 'get',\n params: { id: 1 },\n responsetype: 'arraybuffer',\n});\n```\n\n## schema validation\n\npass a `schema` with a `parse(data)` method to validate the parsed body. any validator works: a `@vielzeug/spell` schema or a plain function wrapper. a failed parse throws `courierschemavalidationerror`.\n\n```ts\nimport { s } from '@vielzeug/spell';\n\nconst user = await courier.get('/users/1', {\n schema: s.object({ id: s.number(), name: s.string() }),\n});\n```\n\n## cancellation and disposal\n\npass a `signal` to cancel one request. `cancelall()` aborts every active request without disposing the client. `dispose()` aborts active work and marks the client unusable: call it only at the final application or request boundary.\n\n```ts\nconst controller = new abortcontroller();\nvoid courier.get('/slow', { signal: controller.signal });\ncontroller.abort(); // cancels that request only\n\ncourier.cancelall(); // cancels all active requests\ncourier.dispose(); // final teardown\n```\n\n## timeouts\n\nset a client wide `timeout` (default 30,000 ms) or a per request timeout. finite values must be integer milliseconds from 1 through 2,147,483,647. a timed out request rejects with `couriertimeouterror`; use `infinity` for intentionally long lived work.\n\n```ts\nconst courier = createcourier({ timeout: 10_000 });\nawait courier.get('/health', { timeout: 2_000 });\n```\n\n## error handling\n\ncourier classifies url, request construction, fetch, cancellation, http, response parsing, and schema failures into distinct error classes. errors thrown directly by custom middleware are preserved.\n\n```ts\nimport {\n courieraborterror,\n courierhttperror,\n couriernetworkerror,\n courierparseerror,\n couriertimeouterror,\n createcourier,\n} from '@vielzeug/courier';\n\nconst courier = createcourier({ baseurl: 'https://api.example.com' });\n\nasync function loaduser(): promise<string> {\n try {\n return (await courier.get<{ name: string }>('/users/1', { timeout: 2_000 })).name;\n } catch (error) {\n if (error instanceof courieraborterror) return 'cancelled';\n if (error instanceof couriertimeouterror) return 'timed out; retry.';\n if (courierhttperror.is(error, 404)) return 'user not found.';\n if (error instanceof couriernetworkerror) return 'check your connection.';\n if (error instanceof courierparseerror) return 'the server returned an unreadable response.';\n throw error;\n }\n}\n```\n\n a `courierhttperror` has a response (`status`, `data`, `headers`); a `couriernetworkerror` does not.\n do not display cancellation as an application error during navigation.\n `courierschemavalidationerror` carries the raw pre validation `data`.\n\n## composing with a state layer\n\ncourier owns http transport and optional parsed get caching. observable query state, optimistic mutations, collection navigation, and stream state belong to the owning state layer (e.g. sourcerer, tanstack query, or consumer code). compose courier calls into that layer instead of nesting state machines.\n\n```ts\nimport { createpagesource } from '@vielzeug/sourcerer';\n\nconst users = createpagesource<user>({\n load: async ({ page, pagesize, signal }) => {\n const result = await courier.get<{ data: user[]; total: number }>('/users', {\n query: { page, pagesize },\n signal,\n });\n return { items: result.data, totalitems: result.total };\n },\n});\n\nawait users.reload();\n```\n\n## framework integration\n\ncreate courier at an application or route boundary. inject the client where requests are issued; let the framework own rendering and cache state.\n\n::: code group\n\n```tsx [react]\nimport { createcontext, usecontext } from 'react';\nimport { createcourier } from '@vielzeug/courier';\n\ntype user = { id: number; name: string };\n\ntype courierclient = returntype<typeof createcourier>;\nconst couriercontext = createcontext<courierclient | null>(null);\n\nexport function courierprovider({ children, courier }: { children: react.reactnode; courier: courierclient }) {\n return <couriercontext.provider value={courier}>{children}</couriercontext.provider>;\n}\n\nexport function usecourier() {\n const courier = usecontext(couriercontext);\n if (!courier) throw new error('courierprovider missing');\n return courier;\n}\n\n// in a component:\n// const user = await usecourier().get<user>('/users/1');\n```\n\n```ts [vue 3]\nimport { createcourier } from '@vielzeug/courier';\n\nconst courier = createcourier({ baseurl: '/api' });\n// provide via app.provide('courier', courier) and inject in setup().\n```\n\n```svelte [svelte]\n<script lang=\"ts\">\n import { createcourier } from '@vielzeug/courier';\n export let courier: returntype<typeof createcourier>;\n</script>\n```\n\n:::\n\ncourier exposes no loading, error, or cache store. construct the client at the application or ssr request boundary, pass it into framework providers, and dispose it at that same boundary. render state in the framework that owns the view.\n\n## gotchas\n\n### root relative paths stay root relative\n\nwith no `baseurl`, `'/api/users'` is passed to fetch unchanged while `'api/users'` remains document relative. with a `baseurl`, courier appends either spelling to that base.\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### query parameters precede fragments\n\nwhen a path already contains `#fragment`, courier inserts `config.query` before the fragment so values remain part of the http query.\n\n### get and head bodies are rejected\n\nfetch does not support bodies for get or head. courier rejects them as request parse errors before dispatch.\n\n### middleware is immutable\n\nthere is no `use()` method. to change middleware, create a new client. this keeps each client's behavior locally understandable and avoids runtime mutation of the request chain.\n\n### keep credentials out of urls when using logging middleware\n\n`withlogging()` logs the full url, including query parameters. if urls may contain sensitive data, provide a custom `logger` that sanitizes the url first.\n\n## best practices\n\n create one courier client per application or ssr request scope.\n configure middleware once at construction; never mutate it at runtime.\n use method conveniences for get/post/put/patch/delete and `request()` for custom verbs.\n use explicit cache keys only for parsed gets that should share data and in flight work.\n clear cached data when the authenticated principal or tenant changes.\n let the owning state layer own observable queries and mutations.\n dispose only at the final application or request boundary.\n keep credentials out of urls when using logging middleware.\n",
|
|
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 [sse events](./examples/sse events.md)\n"
|
|
338
|
+
"api": " \ntitle: 'courier: api reference'\ndescription: reference for courier http, cached read, prefetch, middleware, and error apis.\n \n\n[[toc]]\n\n## api overview\n\n| symbol | purpose | execution mode | common gotcha |\n| | | | |\n| `createcourier()` | creates transport client | sync | dispose only when whole scope ends |\n| `request()` | sends one http request of any method | async | direct calls never deduplicate |\n| `get()` | sends a get, optionally through the explicit cache | async | one key must identify one representation |\n| `prefetch()` | warms a cached get | async | request failures resolve after surfacing through `tap()` |\n| `invalidatecache()` / `clearcache()` | removes cached values | sync | invalidation matches structured key prefixes |\n| `post()` / `put()` / `patch()` / `delete()` | common write method conveniences | async | use `request()` for custom methods |\n| `withbearerauth()` | adds authorization middleware | sync | token provider runs per request |\n| `withrequestid()` | adds request identifier middleware | sync | default generator uses `crypto.randomuuid()` |\n| `withlogging()` | logs request result metadata | sync | requires explicit logger; urls may contain sensitive query values |\n| `tap()` | observe structured transport events | sync | observation only; optional signal owns lifetime |\n\n## package entry point\n\n| import | purpose |\n| | |\n| `@vielzeug/courier` | client factory, errors, middleware helpers, and public types |\n\n## client\n\n### `createcourier()`\n\n```ts\ncreatecourier(options?: courieroptions): courier;\n```\n\nreturns an http client sharing base url, default headers, immutable middleware, optional parsed value cache, timeout, and cancellation lifecycle.\n\n| `courieroptions` field | type | default | description |\n| | | | |\n| `baseurl` | `string` | `''` | prefix for relative request paths |\n| `cache.capacity` | `number` | `100` | maximum cached entries; positive integer or `infinity` |\n| `cache.ttlms` | `number` | `30_000` | default cache freshness; non negative milliseconds or `infinity` |\n| `fetch` | `typeof globalthis.fetch` | `globalthis.fetch` | fetch implementation |\n| `headers` | `headersinit` | `{}` | default request headers (immutable; merged lowercase) |\n| `middleware` | `readonly middleware[]` | `[]` | immutable middleware chain configured at construction |\n| `timeout` | `number` | `30_000` | integer milliseconds from 1 to 2,147,483,647, or `infinity` |\n\n**returns:** `courier`.\n\n```ts\nimport { createcourier, withbearerauth } from '@vielzeug/courier';\n\nconst courier = createcourier({\n baseurl: 'https://api.example.com',\n middleware: [withbearerauth('token')],\n});\n```\n\n| `courier` member | signature | description |\n| | | |\n| `request` | `<t, p>(path, config?) => promise<t>` | sends one http request; `config.method` defaults to `get` |\n| `get` | `<t, p>(path, config?) => promise<t>` | sends get; `cache` opts parsed responses into bounded caching |\n| `prefetch` | `<t, p>(path, config) => promise<void>` | warms one required structured cache key; failures are tap observed |\n| `invalidatecache` | `(prefix: couriercachekey) => void` | invalidates matching settled and pending key prefixes |\n| `clearcache` | `() => void` | removes every cached and pending entry |\n| `post` | `<t, p>(path, config?) => promise<t>` | sends post |\n| `put` | `<t, p>(path, config?) => promise<t>` | sends put |\n| `patch` | `<t, p>(path, config?) => promise<t>` | sends patch |\n| `delete` | `<t, p>(path, config?) => promise<t>` | sends delete |\n| `tap` | `(handler, options?: { signal?: abortsignal }) => () => void` | observes structured request and disposal events |\n| `cancelall` | `() => void` | aborts active requests; client remains usable |\n| `dispose` | `() => void` | final teardown; aborts work and marks client unusable |\n| `disposed` | `boolean` | whether final disposal occurred |\n| `disposalsignal` | `abortsignal` | aborts on final disposal |\n\n \n\n## requests\n\n### `request()`\n\n```ts\nrequest<t, p extends string>(path: p, config?: requestconfig<p, t> & { method?: string }): promise<t>;\n```\n\nsends one http request. `config.method` defaults to `get`; pass any method (including custom verbs) to support it consistently.\n\n| `requestconfig<p, t>` field | type | description |\n| | | |\n| `method` | `string` | http method; defaults to `get` |\n| `params` | `record<string, string \\| number \\| boolean>` | path parameters for `{param}` placeholders |\n| `query` | `params` | query string parameters |\n| `body` | `unknown` | bodyinit values pass through; streams use node duplex; get/head reject bodies; other values encode as json |\n| `headers` | `headersinit` | per request headers merged with (and overriding) defaults |\n| `responsetype` | `responsetype` | response parsing strategy |\n| `schema` | `{ parse(data: unknown): t }` | optional parsed response validator; incompatible with `responsetype: 'raw'` |\n| `signal` | `abortsignal` | external abort signal merged with internal cancellation |\n| `timeout` | `number` | integer milliseconds from 1 to 2,147,483,647, or `infinity`; overrides default |\n| `fetchinit` | `omit<requestinit, 'body' \\| 'headers' \\| 'method' \\| 'signal'>` | raw fetch options for advanced use |\n\n**returns:** parsed response body (or `undefined` for empty bodies). with\n`responsetype: 'raw'` the resolved value is the `response` itself; a `204`/`205`\nanswer whose transport carried an unexpected empty body stream is normalized to\n`body: null` — the stray stream is cancelled and headers, status, statustext,\nurl, redirect and type metadata are preserved (on the response and its clones).\na null body raw response owns no pending resource, so no consumption is required\nto release the request.\n\n```ts\nconst created = await courier.request<user>('/users', {\n method: 'post',\n body: { name: 'ada' },\n});\n```\n\n### `get()`\n\n```ts\nget<t, p extends string>(path: p, config?: getrequestconfig<p, t>): promise<t>;\n```\n\nconvenience for `request(path, { ...config, method: 'get' })`. a `cache` descriptor opts a parsed response into the bounded cache. it requires a structured key and may override the default ttl. raw responses cannot be cached.\n\n```ts\nconst user = await courier.get<user>('/users/{id}', {\n cache: { key: ['users', 1], ttlms: 10_000 },\n params: { id: 1 },\n});\n```\n\n## cache\n\n### `prefetch()`\n\n```ts\nprefetch<t, p extends string>(path: p, config: prefetchconfig<p, t>): promise<void>;\n```\n\nstarts the same cached get used by `get()`. `config.cache` is required; `signal`, per call `timeout`, and `responsetype: 'raw'` are unavailable. fresh values and pending loads are reused. the promise settles when the attempt finishes. request failures emit `request error` through `tap()` and resolve without caching data.\n\n### `invalidatecache()` and `clearcache()`\n\n```ts\ninvalidatecache(prefix: couriercachekey): void;\nclearcache(): void;\n```\n\n`invalidatecache()` removes every settled entry whose structured key starts with `prefix` and prevents matching pending loads from being stored. `clearcache()` applies that behavior to all entries. existing waiters may still receive an invalidated in flight result; `cancelall()` or `dispose()` aborts physical work.\n\n \n\n## middleware\n\nmiddleware is an immutable chain configured at construction. each middleware receives an immutable `fetchcontext` and a `next` function.\n\n```ts\ntype middleware = (ctx: fetchcontext, next: (ctx: fetchcontext) => promise<response>) => promise<response>;\n```\n\n### middleware helpers\n\n```ts\nwithbearerauth(token: string | (() => string | null | undefined | promise<string | null | undefined>)): middleware;\nwithrequestid(options?: { generate?: () => string; header?: string }): middleware;\nwithlogging(options: {\n logger: (message: string, meta: { duration: number; method: string; status: number; url: string }) => void;\n}): middleware;\n```\n\neach helper returns a `middleware` passed in the `middleware` option. `withlogging` requires an explicit `logger` function, isolates logger failures from requests, and has no default console output.\n\n```ts\nconst courier = createcourier({\n middleware: [\n withbearerauth(() => tokenstore.getaccesstoken()),\n withrequestid(),\n withlogging({ logger: (msg) => console.log(msg) }),\n ],\n});\n```\n\n## types\n\n```ts\ntype transportoptions = {\n baseurl?: string;\n fetch?: typeof globalthis.fetch;\n headers?: headersinit;\n middleware?: readonly middleware[];\n timeout?: number;\n};\n\ntype couriercachekeyatom = string | number | boolean | null;\ntype couriercachekey = readonly [couriercachekeyatom, ...couriercachekeyatom[]];\n\ntype couriercacheoptions = {\n capacity?: number;\n ttlms?: number;\n};\n\ntype courierreadcache = {\n key: couriercachekey;\n ttlms?: number;\n};\n\ntype courieroptions = transportoptions & {\n cache?: couriercacheoptions;\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 | undefined>): fetchcontext;\n withinit(updates: partial<omit<requestinit, 'body' | 'headers' | 'method' | 'signal'>>): fetchcontext;\n};\n\ntype middleware = (ctx: fetchcontext, next: (ctx: fetchcontext) => promise<response>) => promise<response>;\n\ntype courierevent =\n | { type: 'request start'; method: string; url: string }\n | { type: 'request success'; method: string; url: string; status: number; duration: number }\n | { type: 'request error'; method: string; url: string; error: unknown }\n | { type: 'dispose' };\n```\n\n```ts\ntype paramvalue = string | number | boolean | null | readonly (string | number | boolean | null)[] | undefined;\ntype params = record<string, paramvalue>;\ntype responsetype = 'auto' | 'json' | 'text' | 'blob' | 'arraybuffer' | 'raw';\ntype requestconfig<p extends string = string, t = unknown> = {\n body?: unknown;\n method?: string;\n fetchinit?: omit<requestinit, 'body' | 'headers' | 'method' | 'signal'>;\n headers?: headersinit;\n params?: record<string, string | number | boolean>;\n query?: params;\n responsetype?: responsetype;\n schema?: { parse(data: unknown): t };\n signal?: abortsignal;\n timeout?: number;\n};\n\ntype getrequestconfig<p extends string = string, t = unknown> = omit<\n requestconfig<p, t>,\n 'body' | 'method' | 'responsetype'\n> &\n ({ cache?: courierreadcache; responsetype?: exclude<responsetype, 'raw'> } | { cache?: never; responsetype: 'raw' }) & {\n body?: never;\n };\n\ntype prefetchconfig<p extends string = string, t = unknown> = omit<\n getrequestconfig<p, t>,\n 'cache' | 'responsetype' | 'signal' | 'timeout'\n> & {\n cache: courierreadcache;\n responsetype?: exclude<responsetype, 'raw'>;\n signal?: never;\n timeout?: never;\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 or client cancellation | `method`, `url`, `cause` |\n| `courierschemavalidationerror` | response schema fails | `data`, `cause` |\n| `courierparseerror` | response body cannot parse | n/a |\n| `courierconfigerror` | invalid request config (get/head body, raw + schema) | n/a |\n| `courierdisposederror` | work starts after disposal | n/a |\n\nerrors thrown directly by custom middleware are preserved rather than reclassified.\n",
|
|
339
|
+
"usage": " \ntitle: 'courier: usage guide'\ndescription: use one courier client for typed http, explicit cached reads, prefetching, immutable middleware, and structured errors.\n \n\n[[toc]]\n\n## basic usage\n\ncreate one courier client for an application or request scope. its transport policy, middleware, and disposal lifecycle apply to every request.\n\n```ts\nimport { createcourier } from '@vielzeug/courier';\n\ntype user = { id: number; name: string };\n\nconst courier = createcourier({ baseurl: 'https://api.example.com' });\nconst user = await courier.get<user>('/users/{id}', { params: { id: 1 } });\nconsole.log(user.name);\n```\n\n## http requests\n\nuse `get()`, `post()`, `put()`, `patch()`, and `delete()` for standard methods. use `request()` when the method is custom or selected dynamically. courier preserves root relative paths, encodes path parameters, serializes json compatible bodies, and parses successful responses.\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\n`get()`, `post()`, `put()`, `patch()`, and `delete()` cover common methods. use `request()` for custom verbs or when a method is selected dynamically.\n\n## default headers\n\nset default headers at construction. they are merged (lowercased) into every request and cannot be mutated at runtime: middleware handles dynamic headers.\n\n```ts\nconst courier = createcourier({\n baseurl: 'https://api.example.com',\n headers: { 'x client': 'web' },\n});\n```\n\n## middleware\n\nmiddleware is an immutable chain configured once at construction. each middleware receives an immutable `fetchcontext` and a `next` function. use `ctx.withheaders()` or `ctx.withinit()` to derive a new context: never mutate `ctx` directly.\n\n```ts\nimport { withbearerauth, withrequestid, createcourier } from '@vielzeug/courier';\n\nconst courier = createcourier({\n baseurl: 'https://api.example.com',\n middleware: [\n withbearerauth(() => sessionstorage.getitem('access token') ?? ''),\n withrequestid(),\n ],\n});\n```\n\na bearer token provider may return `null`, `undefined`, or an empty string to omit a stale authorization header. use `withlogging()` with an explicit `logger` function to log requests during local development. `withlogging()` includes full urls, so sanitize query values before persistent logging.\n\n```ts\nimport { withlogging, createcourier } from '@vielzeug/courier';\n\nconst courier = createcourier({\n baseurl: 'https://api.example.com',\n middleware: [withlogging({ logger: (msg) => console.log(msg) })],\n});\n```\n\nuse `tap()` for multiple structured transport observers. it emits request start, request success, request error, and dispose events; observer failures cannot alter requests. pass `{ signal }` to own listener lifetime.\n\n```ts\nconst stop = courier.tap((event) => metrics.record(event), { signal: routesignal });\n```\n\nbecause middleware is immutable, a client's behavior is locally understandable from its construction call. to change policy, create a new client.\n\n## cached reads and prefetching\n\nopt parsed gets into caching with a structured key. courier uses a finite capacity of 100 entries and a 30 second ttl by default; client options change those defaults, and each read may override ttl. successful parsed and schema validated values are cached, while errors are not.\n\n```ts\nconst courier = createcourier({\n baseurl: 'https://api.example.com',\n cache: { capacity: 200, ttlms: 60_000 },\n});\nconst cache = { key: ['accounts', accountid, 'users', userid] as const, ttlms: 10_000 };\n\nawait courier.prefetch<user>('/users/{id}', { cache, params: { id: userid } });\nconst user = await courier.get<user>('/users/{id}', { cache, params: { id: userid } });\n```\n\n`prefetch()` returns a promise that settles when warming finishes. request failures are not cached or rejected from that promise; the physical request emits `request error` through `tap()`. cache, key, and url configuration errors still throw before prefetch starts.\n\ninvalidate a structured prefix after writes. every matching settled or pending key becomes ineligible for caching, while unrelated entries remain available.\n\n```ts\nawait courier.patch('/users/{id}', { body: update, params: { id: userid } });\ncourier.invalidatecache(['accounts', accountid, 'users']);\n```\n\na cache key must identify one response representation, including a stable non secret principal or tenant identifier, schema, and relevant request variants. never place credentials in a key; call `clearcache()` when identity changes. cached objects are shared by reference.\n\nconcurrent callers share physical work but own their own signal and timeout. one cancelled caller does not affect the others. when every caller cancels and no prefetch owns the load, courier aborts the physical request. `cancelall()` always aborts physical work; `dispose()` also clears settled cache data.\n\n## response parsing\n\ncourier parses successful 2xx response bodies automatically based on `content type`, unless you set `responsetype` explicitly (`'json'`, `'text'`, `'blob'`, `'arraybuffer'`, or `'raw'` for the `response` itself). empty json bodies return `undefined` instead of throwing. raw response bodies remain owned by courier until consumed or cancelled, so `cancelall()` and `dispose()` still abort them. raw responses cannot be combined with `schema`. always consume or cancel the body to release request ownership, and set `timeout: infinity` for intentionally long lived streams. courier preserves url, redirect, and type metadata on tracked responses and their clones. a `204`/`205` raw answer is normalized to `body: null` — a stray empty transport stream is cancelled for you — so a null body raw response never carries pending body ownership and needs no consumption.\n\n```ts\nconst buffer = await courier.request('/files/{id}', {\n method: 'get',\n params: { id: 1 },\n responsetype: 'arraybuffer',\n});\n```\n\n## schema validation\n\npass a `schema` with a `parse(data)` method to validate the parsed body. any validator works: a `@vielzeug/spell` schema or a plain function wrapper. a failed parse throws `courierschemavalidationerror`.\n\n```ts\nimport { s } from '@vielzeug/spell';\n\nconst user = await courier.get('/users/1', {\n schema: s.object({ id: s.number(), name: s.string() }),\n});\n```\n\n## cancellation and disposal\n\npass a `signal` to cancel one request. `cancelall()` aborts every active request without disposing the client. `dispose()` aborts active work and marks the client unusable: call it only at the final application or request boundary.\n\n```ts\nconst controller = new abortcontroller();\nvoid courier.get('/slow', { signal: controller.signal });\ncontroller.abort(); // cancels that request only\n\ncourier.cancelall(); // cancels all active requests\ncourier.dispose(); // final teardown\n```\n\n## timeouts\n\nset a client wide `timeout` (default 30,000 ms) or a per request timeout. finite values must be integer milliseconds from 1 through 2,147,483,647. a timed out request rejects with `couriertimeouterror`; use `infinity` for intentionally long lived work.\n\n```ts\nconst courier = createcourier({ timeout: 10_000 });\nawait courier.get('/health', { timeout: 2_000 });\n```\n\n## error handling\n\ncourier classifies url, request construction, fetch, cancellation, http, response parsing, and schema failures into distinct error classes. errors thrown directly by custom middleware are preserved.\n\n```ts\nimport {\n courieraborterror,\n courierhttperror,\n couriernetworkerror,\n courierparseerror,\n couriertimeouterror,\n createcourier,\n} from '@vielzeug/courier';\n\nconst courier = createcourier({ baseurl: 'https://api.example.com' });\n\nasync function loaduser(): promise<string> {\n try {\n return (await courier.get<{ name: string }>('/users/1', { timeout: 2_000 })).name;\n } catch (error) {\n if (error instanceof courieraborterror) return 'cancelled';\n if (error instanceof couriertimeouterror) return 'timed out; retry.';\n if (courierhttperror.is(error, 404)) return 'user not found.';\n if (error instanceof couriernetworkerror) return 'check your connection.';\n if (error instanceof courierparseerror) return 'the server returned an unreadable response.';\n throw error;\n }\n}\n```\n\n a `courierhttperror` has a response (`status`, `data`, `headers`); a `couriernetworkerror` does not.\n do not display cancellation as an application error during navigation.\n `courierschemavalidationerror` carries the raw pre validation `data`.\n\n## composing with a state layer\n\ncourier owns http transport and optional parsed get caching. observable query state, optimistic mutations, collection navigation, and stream state belong to the owning state layer (e.g. sourcerer, tanstack query, or consumer code). compose courier calls into that layer instead of nesting state machines.\n\n```ts\nimport { createpagesource } from '@vielzeug/sourcerer';\n\nconst users = createpagesource<user>({\n load: async ({ page, pagesize, signal }) => {\n const result = await courier.get<{ data: user[]; total: number }>('/users', {\n query: { page, pagesize },\n signal,\n });\n return { items: result.data, totalitems: result.total };\n },\n});\n\nawait users.reload();\n```\n\n## framework integration\n\ncreate courier at an application or route boundary. inject the client where requests are issued; let the framework own rendering and cache state.\n\n::: code group\n\n```tsx [react]\nimport { createcontext, usecontext } from 'react';\nimport { createcourier } from '@vielzeug/courier';\n\ntype user = { id: number; name: string };\n\ntype courierclient = returntype<typeof createcourier>;\nconst couriercontext = createcontext<courierclient | null>(null);\n\nexport function courierprovider({ children, courier }: { children: react.reactnode; courier: courierclient }) {\n return <couriercontext.provider value={courier}>{children}</couriercontext.provider>;\n}\n\nexport function usecourier() {\n const courier = usecontext(couriercontext);\n if (!courier) throw new error('courierprovider missing');\n return courier;\n}\n\n// in a component:\n// const user = await usecourier().get<user>('/users/1');\n```\n\n```ts [vue 3]\nimport { createcourier } from '@vielzeug/courier';\n\nconst courier = createcourier({ baseurl: '/api' });\n// provide via app.provide('courier', courier) and inject in setup().\n```\n\n```svelte [svelte]\n<script lang=\"ts\">\n import { createcourier } from '@vielzeug/courier';\n export let courier: returntype<typeof createcourier>;\n</script>\n```\n\n:::\n\ncourier exposes no loading, error, or cache store. construct the client at the application or ssr request boundary, pass it into framework providers, and dispose it at that same boundary. render state in the framework that owns the view.\n\n## gotchas\n\n### root relative paths stay root relative\n\nwith no `baseurl`, `'/api/users'` is passed to fetch unchanged while `'api/users'` remains document relative. with a `baseurl`, courier appends either spelling to that base.\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### query parameters precede fragments\n\nwhen a path already contains `#fragment`, courier inserts `config.query` before the fragment so values remain part of the http query.\n\n### get and head bodies are rejected\n\nfetch does not support bodies for get or head. courier rejects them as request parse errors before dispatch.\n\n### middleware is immutable\n\nthere is no `use()` method. to change middleware, create a new client. this keeps each client's behavior locally understandable and avoids runtime mutation of the request chain.\n\n### keep credentials out of urls when using logging middleware\n\n`withlogging()` logs the full url, including query parameters. if urls may contain sensitive data, provide a custom `logger` that sanitizes the url first.\n\n## best practices\n\n create one courier client per application or ssr request scope.\n configure middleware once at construction; never mutate it at runtime.\n use method conveniences for get/post/put/patch/delete and `request()` for custom verbs.\n use explicit cache keys only for parsed gets that should share data and in flight work.\n clear cached data when the authenticated principal or tenant changes.\n let the owning state layer own observable queries and mutations.\n dispose only at the final application or request boundary.\n keep credentials out of urls when using logging middleware.\n",
|
|
340
|
+
"examples": " \ntitle: 'courier: examples'\ndescription: practical examples and recipes for courier.\n \n\n## examples\n\n [authentication](./examples/authentication.md)\n [service discovery and custom fetch](./examples/service discovery and custom fetch.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 [sse events](./examples/sse events.md)\n"
|
|
320
341
|
},
|
|
321
342
|
"examples": [
|
|
322
343
|
{
|
|
@@ -939,8 +960,8 @@
|
|
|
939
960
|
"description": "responsive svg charts with explicit updates: line, bar, area, scatter, pie, radar, sparkline",
|
|
940
961
|
"docs": {
|
|
941
962
|
"index": " \ntitle: 'prism: responsive svg data visualization'\ndescription: 'responsive svg charts with explicit updates: line, bar, area, scatter, pie, radar, sparkline'\npackage: prism\ncategory: ui\nkeywords: [chart, svg, visualization, responsive, line chart, bar chart, area chart, scatter chart, pie chart, radar chart, sparkline, accessibility, typescript]\nrelated: [refine, orbit]\nexports: [createlinechart, createbarchart, createpiechart, charthandle]\nenvironments: [browser]\n \n\n<! markdownlint disable md025 md033 md060 >\n\n<packagehero package=\"prism\" />\n\n## why prism?\n\nprism renders charts as svg in a host element and keeps application state outside the chart. you can replace chart data explicitly, respond to container resizing, and style the output with css custom properties.\n\n```ts\nimport { createlinechart } from '@vielzeug/prism';\n\nconst host = document.createelement('div');\nhost.style.csstext = 'width:640px;height:320px';\ndocument.body.append(host);\n\n// before: replace a hand drawn svg each time values change.\nfunction drawline(values: number[]) {\n const path = document.createelementns('http://www.w3.org/2000/svg', 'path');\n const svg = document.createelementns('http://www.w3.org/2000/svg', 'svg');\n const low = math.min(...values);\n const span = math.max(...values) low || 1;\n const points = values.map((value, index) => {\n const x = (index / math.max(1, values.length 1)) * 640;\n const y = 320 ((value low) / span) * 320;\n return `${index === 0 ? 'm' : 'l'}${x},${y}`;\n });\n svg.setattribute('viewbox', '0 0 640 320');\n path.setattribute('d', points.join(' '));\n path.setattribute('fill', 'none');\n path.setattribute('stroke', 'currentcolor');\n svg.append(path);\n host.replacechildren(svg);\n}\ndrawline([10, 25, 18]);\ndrawline([10, 25, 18, 32]);\n\n// after: keep the chart instance and replace its series data.\nconst chart = createlinechart(host, {\n a11y: { arialabel: 'revenue by month' },\n series: [{ data: [{ key: 1, value: 10 }, { key: 2, value: 25 }, { key: 3, value: 18 }], name: 'revenue' }],\n});\nchart.update([{\n data: [{ key: 1, value: 10 }, { key: 2, value: 25 }, { key: 3, value: 18 }, { key: 4, value: 32 }],\n name: 'revenue',\n}]);\nchart.dispose();\n```\n\n| feature | prism | hand written svg |\n| | | |\n| bundle size | <packageinfo package=\"prism\" type=\"size\" /> | no chart package |\n| zero dependencies | <ore icon name=\"x\" size=\"16\"></ore icon> (uses orbit) | <ore icon name=\"check\" size=\"16\"></ore icon> |\n| data updates | `charthandle.update()` replaces chart data | you implement the update path |\n| responsive sizing | `resizeobserver` recalculates the chart layout | you implement the resize behavior |\n| chart interactions | built in pointer and keyboard interactions on supported charts | you implement them |\n| svg accessibility | informative charts support an accessible name and keyboard focus | you implement the accessible behavior |\n\n<div class=\"decision callout\">\n\n**use prism when** you need chart factories, responsive svg output, and an explicit data update boundary without adopting a charting framework.\n\n**consider hand written svg when** you need a single specialized graphic and prefer to own its scales, resizing, labels, and interactions directly.\n\n</div>\n\n## installation\n\n::: code group\n\n```sh [pnpm]\npnpm add @vielzeug/prism\n```\n\n```sh [npm]\nnpm install @vielzeug/prism\n```\n\n```sh [yarn]\nyarn add @vielzeug/prism\n```\n\n:::\n\n## quick start\n\ncreate and size a host element before creating the chart. import `theme.css` once to load prism's default styles and css custom properties.\n\n```ts\nimport { createlinechart } from '@vielzeug/prism';\nimport '@vielzeug/prism/theme.css';\n\nconst host = document.createelement('div');\nhost.style.csstext = 'width:640px;height:320px';\ndocument.body.append(host);\n\nconst chart = createlinechart(host, {\n a11y: { arialabel: 'revenue by month' },\n series: [{\n data: [{ key: 1, value: 10 }, { key: 2, value: 25 }, { key: 3, value: 18 }],\n name: 'revenue',\n }],\n tooltip: true,\n});\n\nchart.update([{\n data: [{ key: 1, value: 10 }, { key: 2, value: 25 }, { key: 3, value: 18 }, { key: 4, value: 32 }],\n name: 'revenue',\n}]);\n\nchart.dispose();\n```\n\n## features\n\n<div class=\"features grid\">\n\n **`createlinechart()` / `createareachart()`**: plot continuous numeric or date keyed series\n **`createbarchart()`**: compare categories with grouped, stacked, or horizontal bars\n **`createscatterchart()`**: plot independent numeric `(x, y)` points\n **`createpiechart()`**: render pie, donut, or semi circle slices\n **`createradarchart()`**: compare series across fixed, labelled axes\n **`createsparkline()`**: render compact line, area, bar, or stacked values without axes\n **`charthandle.update(data)`**: replace chart data without handing state ownership to prism\n **`charthandle.dispose()` / `symbol.dispose`**: release the svg, observer, overlays, and chart owned animation\n **`charthandle.tap()`**: observe resize and disposal events\n **`settheme()` / `resettheme()`**: set supported palette, typography, axis, grid, and tooltip tokens globally or on one element\n **css custom properties**: style the chart palette, marks, axes, labels, tooltip, legend, and motion\n **resize observation**: recalculate svg layout when the host element changes size\n **accessible interaction**: labelled interactive charts expose keyboard navigation and polite value announcements; sparklines are pointer only without value announcements\n\n</div>\n\n## documentation\n\n<div class=\"doc links\">\n\n [usage guide](./usage.md)\n [api reference](./api.md)\n [examples](./examples.md)\n [migration guide](./migration.md)\n\n</div>\n\n## see also\n\n<div class=\"see also\">\n\n [refine](/refine/): web components and theme tokens for building controls and layouts around chart hosts\n [orbit](/orbit/): floating element positioning used internally by prism's built in tooltip\n\n</div>\n\n<! markdownlint enable md025 md033 md060 >\n",
|
|
942
|
-
"api": " \ntitle: 'prism: api reference'\ndescription: complete reference for prism chart factories, scales, themes, types, and errors.\n \n\n[[toc]]\n\n## api overview\n\nall chart factories, scale utilities, theme helpers, and error classes are synchronous. charts render into an html element and return a `charthandle`; they do not return a promise.\n\n| symbol | purpose | execution mode | common gotcha |\n| | | | |\n| `createlinechart()` | render one or more continuous line series | sync | `key` values must be numbers or dates |\n| `createareachart()` | render continuous series with filled areas | sync | fills use a gradient by default |\n| `createscatterchart()` | plot independent `(x, y)` points | sync | `key` is the x value; the y domain is fitted to data |\n| `createbarchart()` | render categorical bars | sync | negative values in stacked variants are clamped to zero |\n| `createpiechart()` | render pie, donut, or semi circle slices | sync | `variant` is fixed at creation; `update()` replaces slices |\n| `createradarchart()` | compare series across fixed axes | sync | requires at least three axes to draw |\n| `createsparkline()` | render a compact line, area, bar, or stack | sync | has no axes, legend, or keyboard navigation |\n| `linearscale()` | create a numeric scale | sync | `nice` defaults to `true`; set `nice: false` to preserve the domain |\n| `timescale()` | create a date scale | sync | `nice` defaults to `true` |\n| `bandscale()` | create an ordered categorical scale | sync | unknown categories map to `0` and warn in development |\n| `settheme()` | set prism css custom properties | sync | writes inline styles to `document.documentelement` by default |\n| `resettheme()` | remove theme overrides set by `settheme()` | sync | only clears the selected target |\n| `seriescolor()` | resolve a palette css value | sync | palette positions wrap after eight |\n| `prismerror` | base class for prism errors | sync | use `instanceof` to catch any prism error |\n| `prismrendererror` | report an invalid chart container or failed chart operation | sync | also thrown by `update()` after disposal |\n\n## package entry point\n\n| import | purpose |\n| | |\n| `@vielzeug/prism` | chart factories, scale and theme functions, error classes, and public types |\n| `@vielzeug/prism/theme.css` | default chart styles, css custom properties, dark mode values, and reduced motion rules |\n\n## chart factories\n\neach factory appends an svg to `container`. a chart with no measurable container size uses a temporary `600 × 300` svg size and emits a development warning; `resizeobserver` applies later size changes. unless `a11y` is informative, the svg is hidden from the accessibility tree.\n\n### `createlinechart()`\n\n```ts\nfunction createlinechart(\n container: htmlelement,\n config: linechartconfig,\n): charthandle<lineseriesconfig[]>;\n```\n\nrenders line series and returns a handle whose `update()` argument replaces the complete series array. axes render by default; the value axis has grid lines by default. pass `false` to `xaxis` or `yaxis` to suppress that axis.\n\nseries use linear interpolation by default, omit point markers by default, and use a point radius of 3 when markers are enabled. `yaxis` defaults to `'left'`; crosshair, tooltip, and legend are disabled unless configured.\n\n| parameter | type | description |\n| | | |\n| `container` | `htmlelement` | host that receives the chart svg and optional overlays |\n| `config` | `linechartconfig` | series, axes, interaction, accessibility, and motion settings |\n\n**returns:** `charthandle<lineseriesconfig[]>`.\n\n```ts\nimport { createlinechart } from '@vielzeug/prism';\n\nconst host = document.createelement('div');\nhost.style.csstext = 'width:640px;height:320px';\ndocument.body.append(host);\n\nconst chart = createlinechart(host, {\n a11y: { arialabel: 'revenue by month' },\n series: [{ data: [{ key: 1, value: 10 }, { key: 2, value: 15 }], name: 'revenue' }],\n});\nchart.update([{ data: [{ key: 1, value: 10 }, { key: 2, value: 15 }, { key: 3, value: 18 }], name: 'revenue' }]);\nchart.dispose();\n```\n\n### `createareachart()`\n\n```ts\nfunction createareachart(\n container: htmlelement,\n config: areachartconfig,\n): charthandle<areaseriesconfig[]>;\n```\n\nrenders continuous area series with a gradient fill by default. `update()` replaces the complete series array.\n\narea series use linear interpolation by default and show their top line unless `showline: false` is set. axes render by default, with grid lines on the value axis.\n\n| parameter | type | description |\n| | | |\n| `container` | `htmlelement` | host that receives the chart svg and optional overlays |\n| `config` | `areachartconfig` | series, axes, interaction, accessibility, and motion settings |\n\n**returns:** `charthandle<areaseriesconfig[]>`.\n\n```ts\nimport { createareachart } from '@vielzeug/prism';\n\nconst host = document.createelement('div');\nhost.style.csstext = 'width:640px;height:320px';\ndocument.body.append(host);\n\nconst chart = createareachart(host, {\n a11y: { arialabel: 'visitors over time' },\n series: [{ data: [{ key: 1, value: 10 }, { key: 2, value: 18 }], name: 'visitors' }],\n});\nchart.dispose();\n```\n\n### `createscatterchart()`\n\n```ts\nfunction createscatterchart(\n container: htmlelement,\n config: scatterchartconfig,\n): charthandle<scatterseriesconfig[]>;\n```\n\nplots each datum independently: `key` is the numeric or date x value and `value` is the y value. points are not joined. `update()` replaces the complete series array.\n\naxes render by default, with grid lines on the value axis. the value scale is fitted to the observed data rather than forced to include zero. tooltip, crosshair, and legend are disabled unless configured.\n\n| parameter | type | description |\n| | | |\n| `container` | `htmlelement` | host that receives the chart svg and optional overlays |\n| `config` | `scatterchartconfig` | independent points, axes, interaction, accessibility, and motion settings |\n\n**returns:** `charthandle<scatterseriesconfig[]>`.\n\n```ts\nimport { createscatterchart } from '@vielzeug/prism';\n\nconst host = document.createelement('div');\nhost.style.csstext = 'width:640px;height:320px';\ndocument.body.append(host);\n\nconst chart = createscatterchart(host, {\n a11y: { arialabel: 'height against weight' },\n series: [{ data: [{ key: 160, value: 55 }, { key: 175, value: 70 }], name: 'cohort a' }],\n tooltip: true,\n});\nchart.dispose();\n```\n\nscatter interaction reports one datum and series rather than a `values` array. keyboard navigation visits points across series in x order.\n\n### `createbarchart()`\n\n```ts\nfunction createbarchart(\n container: htmlelement,\n config: barchartconfig,\n): charthandle<barseriesconfig[]>;\n```\n\nrenders grouped, stacked, grouped horizontal, or stacked horizontal categories. `update()` replaces the complete series array; the variant is fixed at creation. axes render by default, with grid lines on the value axis.\n\nthe default variant is `grouped`; tooltip and legend are disabled unless configured.\n\n| parameter | type | description |\n| | | |\n| `container` | `htmlelement` | host that receives the chart svg and optional overlays |\n| `config` | `barchartconfig` | series, layout variant, axes, interaction, accessibility, and motion settings |\n\n**returns:** `charthandle<barseriesconfig[]>`.\n\n```ts\nimport { createbarchart } from '@vielzeug/prism';\n\nconst host = document.createelement('div');\nhost.style.csstext = 'width:640px;height:320px';\ndocument.body.append(host);\n\nconst chart = createbarchart(host, {\n a11y: { arialabel: 'tasks by status' },\n series: [{ data: [{ key: 'open', value: 12 }, { key: 'done', value: 8 }], name: 'tasks' }],\n});\nchart.dispose();\n```\n\nfor stacked variants, negative values are treated as zero and produce a development warning.\n\n### `createpiechart()`\n\n```ts\nfunction createpiechart(\n container: htmlelement,\n config: piechartconfig,\n): charthandle<piesliceconfig[]>;\n```\n\nrenders pie, donut, or semi circle slices. the default variant is `pie`; `update()` replaces the slice array. the default inner radius is zero for pies and 55% of the outer radius for donut and semi variants. pie slices default to no padding or rounded corners; donut and semi slices default to 8px padding and corner radius.\n\n| parameter | type | description |\n| | | |\n| `container` | `htmlelement` | host that receives the chart svg and optional overlays |\n| `config` | `piechartconfig` | slices, shape, geometry, legend, tooltip, and interaction settings |\n\n**returns:** `charthandle<piesliceconfig[]>`.\n\n```ts\nimport { createpiechart } from '@vielzeug/prism';\n\nconst host = document.createelement('div');\nhost.style.csstext = 'width:320px;height:240px';\ndocument.body.append(host);\n\nconst chart = createpiechart(host, {\n a11y: { arialabel: 'orders by channel' },\n data: [{ label: 'direct', value: 72 }, { label: 'referral', value: 28 }],\n variant: 'donut',\n});\nchart.dispose();\n```\n\n### `createradarchart()`\n\n```ts\nfunction createradarchart(\n container: htmlelement,\n config: radarchartconfig,\n): charthandle<radarseriesconfig[]>;\n```\n\nrenders one shape per series across the configured axes. `update()` replaces series data but not axes or other configuration. fewer than three axes warn in development and draw no chart. tooltip and legend are disabled unless configured.\n\n| parameter | type | description |\n| | | |\n| `container` | `htmlelement` | host that receives the chart svg and optional overlays |\n| `config` | `radarchartconfig` | fixed axes, series, scale, grid, appearance, and interaction settings |\n\n**returns:** `charthandle<radarseriesconfig[]>`.\n\n```ts\nimport { createradarchart } from '@vielzeug/prism';\n\nconst host = document.createelement('div');\nhost.style.csstext = 'width:400px;height:320px';\ndocument.body.append(host);\n\nconst chart = createradarchart(host, {\n a11y: { arialabel: 'hero stats' },\n axes: [{ key: 'str', label: 'strength' }, { key: 'agi', label: 'agility' }, { key: 'end', label: 'endurance' }],\n domain: [0, 10],\n series: [{ data: [{ key: 'str', value: 8 }, { key: 'agi', value: 6 }, { key: 'end', value: 9 }], name: 'adam' }],\n});\nchart.dispose();\n```\n\nradar defaults are a solid fill, linear curves, visible points, a four level polygon grid with bands, no grid labels, and a first axis angle of zero degrees. grid labels render only when all axes share a domain.\n\n### `createsparkline()`\n\n```ts\nfunction createsparkline(\n container: htmlelement,\n config: sparklineconfig,\n): charthandle<number[] | stacksegment[]>;\n```\n\nrenders compact data without axes, chart margins, or a legend. `update()` replaces the number or stack segment array. the default variant is `line`; only line and area variants draw the endpoint marker by default.\n\n| parameter | type | description |\n| | | |\n| `container` | `htmlelement` | host that receives the chart svg |\n| `config` | `sparklineconfig` | values, variant, appearance, accessibility, and pointer callbacks |\n\n**returns:** `charthandle<number[] | stacksegment[]>`.\n\n```ts\nimport { createsparkline } from '@vielzeug/prism';\n\nconst host = document.createelement('div');\nhost.style.csstext = 'width:240px;height:48px';\ndocument.body.append(host);\n\nconst chart = createsparkline(host, { data: [2, 5, 3, 8], variant: 'area' });\nchart.update([2, 5, 3, 8, 13]);\nchart.dispose();\n```\n\n`stacksegment[]` is for `variant: 'stack'`. pointer callbacks are available for numeric line, area, and bar sparklines; sparklines do not support keyboard navigation.\n\n## scale factories\n\n### `linearscale()`\n\n```ts\nfunction linearscale(config: linearscaleconfig): scale<number>;\n```\n\nmaps a numeric domain to a numeric range. `nice` defaults to `true`; `clamp` defaults to `false` and applies to `map()`, not `invert()`.\n\n| parameter | type | default | description |\n| | | | |\n| `config.domain` | `[number, number]` | required | input extent |\n| `config.range` | `[number, number]` | required | output extent |\n| `config.clamp` | `boolean` | `false` | clamp mapped values to the range |\n| `config.nice` | `boolean` | `true` | expand the domain to rounded boundaries |\n\n**returns:** `scale<number>`, with `map`, `invert`, `ticks`, `domain`, and `range`.\n\n```ts\nimport { linearscale } from '@vielzeug/prism';\n\nconst scale = linearscale({ domain: [0, 100], range: [0, 500] });\nscale.map(50); // 250\n```\n\n### `timescale()`\n\n```ts\nfunction timescale(config: timescaleconfig): scale<date>;\n```\n\nmaps date values to a numeric range. `nice` defaults to `true` and rounds the domain to a time interval.\n\n| parameter | type | default | description |\n| | | | |\n| `config.domain` | `[date, date]` | required | input date extent |\n| `config.range` | `[number, number]` | required | output extent |\n| `config.nice` | `boolean` | `true` | expand the domain to time interval boundaries |\n\n**returns:** `scale<date>`, with `map`, `invert`, `ticks`, `domain`, and `range`.\n\n```ts\nimport { timescale } from '@vielzeug/prism';\n\nconst scale = timescale({\n domain: [new date('2026 01 01'), new date('2026 12 31')],\n range: [0, 500],\n});\nscale.map(new date('2026 07 02'));\n```\n\n### `bandscale()`\n\n```ts\nfunction bandscale(config: bandscaleconfig): bandscale;\n```\n\nmaps ordered string categories to the start of each band. `padding` defaults to `0.1`; `paddingouter` defaults to the same value. mapping an unknown category returns `0` and warns in development.\n\n| parameter | type | default | description |\n| | | | |\n| `config.domain` | `string[]` | required | ordered categories |\n| `config.range` | `[number, number]` | required | output extent |\n| `config.padding` | `number` | `0.1` | inner gap as a ratio of bandwidth |\n| `config.paddingouter` | `number` | `padding` | outer gap as a ratio of bandwidth |\n\n**returns:** `bandscale`, with `map`, `ticks`, `bandwidth`, `gap`, `domain`, and `range`.\n\n```ts\nimport { bandscale } from '@vielzeug/prism';\n\nconst scale = bandscale({ domain: ['a', 'b'], range: [0, 200] });\nscale.bandwidth();\n```\n\n## theme utilities\n\n`theme.css` supplies the base css values and styles. `settheme()` writes inline custom properties, which take precedence over stylesheet declarations. for other visual controls, set css custom properties directly.\n\n### `settheme()`\n\n```ts\nfunction settheme(theme: prismtheme, options?: themescope): void;\n```\n\nsets the provided theme values on `options.scope`, or on `document.documentelement` when omitted. omitted scalar keys remain unchanged. providing `colors` replaces all eight palette slots, removing unused slots.\n\n| parameter | type | description |\n| | | |\n| `theme` | `prismtheme` | palette and supported axis, grid, text, font, and tooltip properties |\n| `options.scope` | `htmlelement` | element that scopes the inline custom properties |\n\n**returns:** `void`.\n\n```ts\nimport { settheme } from '@vielzeug/prism';\n\nconst host = document.createelement('div');\ndocument.body.append(host);\nsettheme({ colors: ['#2563eb', '#16a34a'], textcolor: '#111827' }, { scope: host });\n```\n\n### `resettheme()`\n\n```ts\nfunction resettheme(options?: themescope): void;\n```\n\nremoves every custom property managed by `settheme()` from the given scope or the document root. it does not remove css rules or change any other custom properties.\n\n| parameter | type | description |\n| | | |\n| `options` | `themescope` | optional target; defaults to `document.documentelement` |\n\n**returns:** `void`.\n\n```ts\nimport { resettheme, settheme } from '@vielzeug/prism';\n\nconst scope = document.createelement('div');\nsettheme({ textcolor: '#111827' }, { scope });\nresettheme({ scope });\n```\n\n### `seriescolor()`\n\n```ts\nfunction seriescolor(index: number, override?: string): string;\n```\n\nreturns `override` when it is provided; otherwise returns a `var( prism color n)` reference. the palette has eight positions and the index wraps around after the eighth.\n\n| parameter | type | description |\n| | | |\n| `index` | `number` | zero based palette position |\n| `override` | `string` | optional color returned instead of a css variable |\n\n**returns:** `string`.\n\n```ts\nimport { seriescolor } from '@vielzeug/prism';\n\nseriescolor(0); // 'var( prism color 1)'\nseriescolor(1, '#2563eb'); // '#2563eb'\n```\n\n## chart handle\n\n`charthandle<tdata>` is returned by every chart factory. `update()` synchronously rebuilds the chart from its new data; enabled motion may animate the resulting svg. `dispose()` is idempotent, removes the chart svg, observers, tooltip and legend, cancels chart owned animation, aborts `disposalsignal`, and emits one `dispose` tap event. `update()` after disposal throws `prismrendererror`.\n\n`tap()` observes `resize` and `dispose` events outside the render path. it returns an unsubscribe function; an optional abort signal removes the subscription. observer handler exceptions are swallowed so they cannot affect chart behavior. a tap added after disposal returns a no op unsubscribe function.\n\n`[symbol.dispose]()` delegates to `dispose()`, allowing explicit resource management where supported.\n\nall chart handles expose the same lifecycle members; interactive cartesian and pie/radar charts use a `.prism live` status region when no tooltip is enabled. a tooltip is itself a polite status region. sparklines have neither a tooltip nor a live value announcer.\n\n| method or property | contract |\n| | |\n| `el` | the chart's root `svgsvgelement` |\n| `update(data)` | replace chart data and render; throws `prismrendererror` after disposal |\n| `dispose()` | idempotently release chart owned dom, observation, and motion |\n| `disposed` | whether the handle has been disposed |\n| `disposalsignal` | `abortsignal` aborted during disposal |\n| `tap(handler, options?)` | observe resize/dispose events; returns an unsubscribe function |\n| `[symbol.dispose]()` | calls `dispose()` |\n\n`prismevent` reports a changed svg content size as `{ type: 'resize', width, height }`; the `dispose` event is emitted once before teardown completes.\n\n## types\n\nthe following are all types and interfaces exported from `@vielzeug/prism`. definitions mirror the package entry point contracts.\n\n### animation types\n\n```ts\nexport type easingfn = (t: number) => number;\nexport type easingname = 'back out' | 'ease in' | 'ease in out' | 'ease out' | 'expo out' | 'linear';\n\nexport interface transitionconfig {\n /** milliseconds per animated lane. defaults to 300 (420 for a line entrance, 400 for radar). */\n duration?: number;\n easing?: easingname | ((t: number) => number);\n /** `system` skips animation when reduced motion is preferred. */\n preference?: 'always' | 'never' | 'system';\n /** per bar delay on bar charts or per series delay on radar charts. */\n stagger?: number;\n}\n\nexport type transitionoption = transitionconfig | boolean;\n```\n\n### theme types\n\n```ts\nexport interface prismtheme {\n axiscolor?: string;\n colors?: string[];\n fontfamily?: string;\n gridcolor?: string;\n gridopacity?: number;\n textcolor?: string;\n textcolorsecondary?: string;\n tooltipbg?: string;\n tooltipborder?: string;\n tooltipcolor?: string;\n}\n\nexport interface themescope {\n readonly scope?: htmlelement;\n}\n```\n\n`prismtheme.colors` writes up to eight palette slots. the scalar fields map as follows:\n\n| field | css custom property |\n| | |\n| `axiscolor` | ` prism axis color` |\n| `fontfamily` | ` prism font family` |\n| `gridcolor` | ` prism grid color` |\n| `gridopacity` | ` prism grid opacity` |\n| `textcolor` | ` prism text color` |\n| `textcolorsecondary` | ` prism text color secondary` |\n| `tooltipbg` | ` prism tooltip bg` |\n| `tooltipborder` | ` prism tooltip border` |\n| `tooltipcolor` | ` prism tooltip color` |\n\n### core and data types\n\n```ts\nexport interface chartmargin {\n bottom: number;\n left: number;\n right: number;\n top: number;\n}\n\nexport interface chartdimensions {\n height: number;\n margin: chartmargin;\n width: number;\n}\n\nexport type prismevent =\n | { readonly height: number; readonly type: 'resize'; readonly width: number }\n | { readonly type: 'dispose' };\n\nexport interface charthandle<tdata = unknown> {\n readonly disposalsignal: abortsignal;\n dispose(): void;\n readonly disposed: boolean;\n readonly el: svgsvgelement;\n tap(handler: (event: prismevent) => void, options?: { readonly signal?: abortsignal }): () => void;\n update(data: tdata): void;\n [symbol.dispose](): void;\n}\n\nexport type charta11y =\n | { readonly decorative: true }\n | { readonly arialabel: string; readonly decorative?: false };\n\nexport interface datum<tkey extends date | number | string = date | number | string> {\n dash?: string;\n key: tkey;\n meta?: record<string, unknown>;\n opacity?: number;\n value: number;\n}\n\nexport type continuousdatum = datum<date | number>;\n\nexport interface series<tdatum extends datum = datum> {\n color?: string;\n data: tdatum[];\n id?: string;\n name: string;\n}\n\nexport interface chartevent {\n datum: datum;\n originalevent: event;\n series: series;\n values?: seriesvalue[];\n}\n\nexport interface seriesvalue {\n datum: datum | undefined;\n series: series;\n}\n```\n\n`datum.dash` and `datum.opacity` affect rendered marks only on the chart types that support them. on bars, `dash` adds a 1px series color stroke so the dash is visible against the fill. `series.id` becomes a `data series id` attribute on a series group; characters outside `[a za z0 9_ ]` are replaced with ` `. when omitted, the id is based on the series index.\n\n| chart | `dash` | `opacity` |\n| | | |\n| bar | strokes that datum's bar | applies to that bar |\n| line | applies to the segment from this datum to the next | applies to that segment and its point marker |\n| area | applies to the top line segment from this datum to the next | applies to that segment |\n| scatter | ignored | applies to that point |\n| pie, radar, sparkline | ignored | ignored |\n\nfor line and area charts, the last datum has no following segment, so its `dash` value is ignored. the chart factories group adjacent segments with the same presentation values into path runs.\n\nfor line, area, and bar charts, `chartevent.values` lists each series at the active key in series order. its `datum` is `undefined` when that series has no matching key. `originalevent` is a pointer event or a keyboard event for keyboard activation. scatter events identify only the nearest independent point and omit `values`.\n\n### scale types\n\n```ts\nexport interface scale<t> {\n readonly domain: readonly [t, t];\n invert(pixel: number): t;\n map(value: t): number;\n readonly range: readonly [number, number];\n ticks(count?: number): t[];\n}\n\nexport interface bandscale {\n bandwidth(): number;\n readonly domain: readonly string[];\n gap(): number;\n map(value: string): number;\n readonly range: readonly [number, number];\n ticks(count?: number): string[];\n}\n\nexport interface linearscaleconfig {\n clamp?: boolean;\n domain: [number, number];\n nice?: boolean;\n range: [number, number];\n}\n\nexport interface timescaleconfig {\n domain: [date, date];\n nice?: boolean;\n range: [number, number];\n}\n\nexport interface bandscaleconfig {\n domain: string[];\n padding?: number;\n paddingouter?: number;\n range: [number, number];\n}\n```\n\n### axis and interaction types\n\n```ts\nexport type horizontalaxisposition = 'bottom' | 'top';\nexport type verticalaxisposition = 'left' | 'right';\nexport type axisposition = horizontalaxisposition | verticalaxisposition;\n\nexport interface gridconfig {\n color?: string;\n dash?: string;\n}\n\nexport interface axisconfig<tposition extends axisposition = axisposition> {\n grid?: gridconfig | boolean;\n label?: string;\n position?: tposition;\n tickcount?: number;\n tickformat?: (value: date | number | string) => string;\n}\n\nexport type xaxisconfig = axisconfig<horizontalaxisposition>;\nexport type yaxisconfig = axisconfig<verticalaxisposition>;\n\nexport interface tooltipconfig {\n offset?: number;\n render?: (datum: datum, series: series) => node | string;\n}\n\nexport interface crosshairconfig {\n horizontal?: boolean;\n snap?: boolean;\n vertical?: boolean;\n}\n\nexport type legendposition = 'bottom' | 'left' | 'right' | 'top';\n\nexport interface legendconfig {\n position?: legendposition;\n}\n```\n\nthe crosshair defaults to a vertical line that snaps to the nearest datum; the horizontal line is hidden by default. with `snap: false`, only the guide follows the pointer—tooltip and callback data still refer to the nearest datum.\n\ntooltip strings are assigned as text, not parsed as html. for a radar chart, `tooltip.render` receives a datum whose `meta` contains the active axis, all series values, and the default announcement text.\n\n### chart configuration types\n\n```ts\nexport interface basechartconfig {\n a11y?: charta11y;\n legend?: legendconfig | boolean;\n margin?: partial<chartmargin>;\n onclick?: (event: chartevent) => void;\n onhover?: (event: chartevent | null) => void;\n tooltip?: tooltipconfig | boolean;\n transition?: transitionoption;\n xaxis?: xaxisconfig | false;\n yaxis?: yaxisconfig | false;\n}\n\nexport interface lineseriesconfig extends series<continuousdatum> {\n curve?: 'linear' | 'monotone' | 'step';\n pointradius?: number;\n showpoints?: boolean;\n strokewidth?: number;\n yaxis?: 'left' | 'right';\n}\n\nexport interface linechartconfig extends basechartconfig {\n crosshair?: crosshairconfig | boolean;\n rightyaxis?: omit<yaxisconfig, 'position' | 'grid'>;\n series: lineseriesconfig[];\n}\n\nexport interface areaseriesconfig extends series<continuousdatum> {\n curve?: 'linear' | 'monotone' | 'step';\n fill?: 'gradient' | 'solid';\n fillopacity?: number;\n showline?: boolean;\n}\n\nexport interface areachartconfig extends basechartconfig {\n crosshair?: crosshairconfig | boolean;\n series: areaseriesconfig[];\n}\n\nexport interface scatterseriesconfig extends series<continuousdatum> {\n pointradius?: number;\n}\n\nexport interface scatterchartconfig extends basechartconfig {\n crosshair?: crosshairconfig | boolean;\n series: scatterseriesconfig[];\n}\n\nexport interface barseriesconfig extends series {\n borderradius?: number;\n}\n\nexport type barvariant = 'grouped' | 'grouped horizontal' | 'stacked' | 'stacked horizontal';\n\nexport interface barchartconfig extends basechartconfig {\n series: barseriesconfig[];\n variant?: barvariant;\n}\n\nexport type pievariant = 'donut' | 'pie' | 'semi';\n\nexport interface piesliceconfig {\n color?: string;\n label?: string;\n value: number;\n}\n\nexport interface pieevent {\n index: number;\n originalevent: event;\n slice: piesliceconfig;\n}\n\nexport interface piechartconfig extends omit<basechartconfig, 'margin' | 'onclick' | 'onhover' | 'xaxis' | 'yaxis'> {\n cornerradius?: number;\n data: piesliceconfig[];\n innerradius?: number;\n onclick?: (event: pieevent) => void;\n onhover?: (event: pieevent | null) => void;\n padpixels?: number;\n variant?: pievariant;\n}\n\nexport interface radaraxisconfig {\n format?: (value: number) => string;\n key: string;\n label: string;\n max?: number;\n min?: number;\n}\n\nexport type radarseriesconfig = series<datum<string>>;\n\nexport interface radargridconfig {\n bands?: boolean;\n labels?: boolean;\n levels?: number;\n shape?: 'circle' | 'polygon';\n}\n\nexport interface radaraxisvalue {\n datum: datum<string> | undefined;\n series: radarseriesconfig;\n}\n\nexport interface radarevent {\n axis: radaraxisconfig;\n index: number;\n originalevent: event;\n values: radaraxisvalue[];\n}\n\nexport interface radarchartconfig extends omit<basechartconfig, 'margin' | 'onclick' | 'onhover' | 'xaxis' | 'yaxis'> {\n axes: radaraxisconfig[];\n curve?: 'linear' | 'rounded';\n domain?: [number, number];\n fill?: 'gradient' | 'none' | 'solid';\n grid?: radargridconfig | boolean;\n onclick?: (event: radarevent) => void;\n onhover?: (event: radarevent | null) => void;\n series: radarseriesconfig[];\n showpoints?: boolean;\n showvalues?: boolean;\n startangle?: number;\n}\n\nexport type sparklinevariant = 'area' | 'bar' | 'line' | 'stack';\n\nexport interface stacksegment {\n color?: string;\n label?: string;\n value: number;\n}\n\nexport interface sparklineevent {\n index: number;\n originalevent: event;\n value: number;\n}\n\nexport interface sparklineconfig {\n a11y?: charta11y;\n color?: string;\n cornerradius?: number;\n curve?: 'linear' | 'monotone' | 'step';\n data: number[] | stacksegment[];\n fillopacity?: number;\n onclick?: (event: sparklineevent) => void;\n onhover?: (event: sparklineevent | null) => void;\n padpixels?: number;\n showendpoint?: boolean;\n strokewidth?: number;\n transition?: transitionoption;\n variant?: sparklinevariant;\n}\n```\n\n## errors\n\n### `prismerror`\n\n```ts\nclass prismerror extends error {\n constructor(message: string, opts?: erroroptions);\n}\n```\n\nbase class for prism originated errors. its `name` is set to the concrete error class name, and it supports the standard `erroroptions` including `cause`.\n\n### `prismrendererror`\n\n```ts\nclass prismrendererror extends prismerror {}\n```\n\nthrown when a chart cannot be rendered because its container is not a dom element, when initial rendering fails (the original error is available as `cause`), or when `update()` is called after disposal. empty or malformed chart data may instead produce a development warning or an empty rendering, depending on the chart.\n",
|
|
943
|
-
"usage": " \ntitle: 'prism: usage guide'\ndescription: create responsive svg charts, update their data, handle interaction, and theme @vielzeug/prism.\n \n\n[[toc]]\n\n## basic usage\n\ncreate a host element with non zero dimensions, then pass it to a chart factory. this example creates the host in javascript so the snippet can run as written.\n\n```ts\nimport { createlinechart } from '@vielzeug/prism';\nimport '@vielzeug/prism/theme.css';\n\nconst container = document.createelement('div');\ncontainer.style.csstext = 'width:640px;height:320px';\ndocument.body.append(container);\n\nconst chart = createlinechart(container, {\n a11y: { arialabel: 'revenue by month' },\n series: [\n {\n name: 'revenue',\n data: [\n { key: 1, value: 10 },\n { key: 2, value: 16 },\n ],\n },\n ],\n tooltip: true,\n});\n\nchart.dispose();\n```\n\neach factory appends an svg to its container and returns a `charthandle`. prism observes the container with `resizeobserver`; if it has zero dimensions on creation, prism uses a temporary `600 × 300` size and warns in development. give the host its layout before creating the chart.\n\n## updating data\n\nkeep application state outside prism and pass the next complete data value to `update()`. the update method replaces chart data synchronously; the default transition may animate the resulting svg.\n\n```ts\nimport { createlinechart } from '@vielzeug/prism';\n\nconst container = document.createelement('div');\ncontainer.style.csstext = 'width:640px;height:320px';\ndocument.body.append(container);\n\nlet series = [{ data: [{ key: 1, value: 10 }], name: 'live' }];\nconst chart = createlinechart(container, { series });\n\nseries = [{ data: [{ key: 1, value: 10 }, { key: 2, value: 20 }], name: 'live' }];\nchart.update(series);\nchart.dispose();\n```\n\n`update()` accepts the type shown by that factory's handle: a complete series array for line, area, scatter, bar, and radar charts; a slice array for pie charts; and a number or stack segment array for sparklines. chart options such as axes, variant, callbacks, and accessibility are set at creation. for example, radar axes and a pie chart's variant do not change through `update()`.\n\n## line charts\n\nline and area series use number or `date` keys. all series in one chart must use the same key kind. use `curve` to choose interpolation, and enable markers with `showpoints`.\n\n```ts\nimport { createlinechart } from '@vielzeug/prism';\n\nconst container = document.createelement('div');\ncontainer.style.csstext = 'width:640px;height:320px';\ndocument.body.append(container);\n\nconst chart = createlinechart(container, {\n a11y: { arialabel: 'revenue and expenses' },\n series: [\n {\n curve: 'monotone',\n data: [{ key: 1, value: 100 }, { key: 2, value: 150 }, { key: 3, value: 130 }],\n name: 'revenue',\n showpoints: true,\n },\n {\n color: '#c2410c',\n data: [{ key: 1, value: 70 }, { key: 2, value: 95 }, { key: 3, value: 110 }],\n name: 'expenses',\n },\n ],\n tooltip: true,\n xaxis: { label: 'month' },\n yaxis: { grid: true, label: 'dollars' },\n});\n\nchart.dispose();\n```\n\n### independent value axes\n\nassign a line series to the right axis when it uses a different value range. both axes share the x scale; `rightyaxis` configures the second axis and cannot configure its position or grid.\n\n```ts\nimport { createlinechart } from '@vielzeug/prism';\n\nconst container = document.createelement('div');\ncontainer.style.csstext = 'width:640px;height:320px';\ndocument.body.append(container);\n\nconst chart = createlinechart(container, {\n a11y: { arialabel: 'health and damage by level' },\n rightyaxis: { label: 'damage' },\n series: [\n { data: [{ key: 1, value: 30 }, { key: 2, value: 20 }], name: 'health' },\n { data: [{ key: 1, value: 100 }, { key: 2, value: 300 }], name: 'damage', yaxis: 'right' },\n ],\n yaxis: { grid: true, label: 'health' },\n});\n\nchart.dispose();\n```\n\nwhen any series uses the right axis, the primary value axis is placed on the left. grid lines belong to the primary `yaxis`. the independent numeric scales mean that curve crossings do not imply equal values.\n\n### time based x axis\n\nuse `date` values for every key when plotting time. line and area charts use a time scale whose domain fits the data instead of rounding outward to tick boundaries.\n\n```ts\nimport { createlinechart } from '@vielzeug/prism';\n\nconst container = document.createelement('div');\ncontainer.style.csstext = 'width:640px;height:320px';\ndocument.body.append(container);\n\nconst formatter = new intl.datetimeformat(undefined, { month: 'short', day: 'numeric' });\nconst chart = createlinechart(container, {\n a11y: { arialabel: 'signups over time' },\n series: [{\n data: [\n { key: new date('2026 09 01'), value: 50 },\n { key: new date('2026 09 08'), value: 80 },\n { key: new date('2026 09 15'), value: 120 },\n ],\n name: 'signups',\n }],\n xaxis: { tickformat: (value) => value instanceof date ? formatter.format(value) : string(value) },\n});\n\nchart.dispose();\n```\n\n`tickformat` formats axis labels only. tooltip content and accessibility announcements use the underlying keys and values.\n\n## bar charts\n\nbar chart keys are string categories. the default `grouped` variant puts series side by side; the horizontal variants put categories on the y axis and values on the x axis.\n\n```ts\nimport { createbarchart } from '@vielzeug/prism';\n\nconst container = document.createelement('div');\ncontainer.style.csstext = 'width:640px;height:320px';\ndocument.body.append(container);\n\nconst chart = createbarchart(container, {\n a11y: { arialabel: 'sales by quarter' },\n series: [{\n data: [{ key: 'q1', value: 200 }, { key: 'q2', value: 350 }, { key: 'q3', value: 280 }],\n name: 'sales',\n }],\n tooltip: true,\n});\n\nchart.dispose();\n```\n\nchoose `grouped`, `stacked`, `grouped horizontal`, or `stacked horizontal` in `variant`. negative values in a stacked chart are clamped to zero and produce a development warning. bars in stacked variants remain square; `borderradius` applies to grouped bars.\n\naxes render by default, with grid lines on the value axis. for horizontal variants, `xaxis` is the value axis and `yaxis` is the category axis. configure `grid` on the value axis.\n\n## area charts\n\narea series use a gradient fill by default. set `fill: 'solid'` for a flat fill, or set `fillopacity` to override the theme opacity for that series.\n\n```ts\nimport { createareachart } from '@vielzeug/prism';\n\nconst container = document.createelement('div');\ncontainer.style.csstext = 'width:640px;height:320px';\ndocument.body.append(container);\n\nconst chart = createareachart(container, {\n a11y: { arialabel: 'visitors over time' },\n crosshair: true,\n series: [{\n curve: 'monotone',\n data: [{ key: 1, value: 12 }, { key: 2, value: 18 }, { key: 3, value: 15 }],\n fill: 'solid',\n fillopacity: 0.2,\n name: 'visitors',\n showline: true,\n }],\n});\n\nchart.dispose();\n```\n\n## scatter charts\n\nscatter charts treat every datum as an independent point: `key` is the numeric x value and `value` is the y value. the scales fit the observed values rather than forcing the y domain to include zero. set `pointradius` on a series to change its marker size.\n\n```ts\nimport { createscatterchart } from '@vielzeug/prism';\n\nconst container = document.createelement('div');\ncontainer.style.csstext = 'width:640px;height:320px';\ndocument.body.append(container);\n\nconst chart = createscatterchart(container, {\n a11y: { arialabel: 'height against weight' },\n series: [{\n data: [{ key: 160, value: 55 }, { key: 175, value: 70 }, { key: 190, value: 88 }],\n name: 'cohort a',\n pointradius: 5,\n }],\n tooltip: true,\n});\n\nchart.dispose();\n```\n\nscatter callbacks identify one nearest point and do not provide cross series `values`, because points do not share a category key.\n\n## forecast and emphasis styling\n\nadd `dash` or `opacity` to individual `datum` objects when one part of a series needs distinct styling. these fields do not change interaction, tooltip, or accessibility behavior.\n\n```ts\nimport { createlinechart } from '@vielzeug/prism';\n\nconst container = document.createelement('div');\ncontainer.style.csstext = 'width:640px;height:320px';\ndocument.body.append(container);\n\nconst values = [10, 13, 15, 18];\nconst chart = createlinechart(container, {\n series: [{\n data: values.map((value, index) => ({\n key: index + 1,\n value,\n ...(index >= 2 ? { dash: '5 5', opacity: 0.4 } : {}),\n })),\n name: 'forecast',\n showpoints: true,\n }],\n});\n\nchart.dispose();\n```\n\nfor line and area charts, each datum's `dash` styles the segment from that datum to the next; the final datum has no outgoing segment. line opacity also fades the datum's marker. area styling affects the top line, not the fill. bar opacity and dashes apply to the individual bar; scatter opacity applies to its point and scatter dashes are ignored. pie, radar, and sparkline ignore these two fields.\n\n## pie, donut, and semi charts\n\nuse `createpiechart()` for all three shapes. `variant` is fixed when the chart is created; `update()` replaces the slice data.\n\n```ts\nimport { createpiechart } from '@vielzeug/prism';\n\nconst container = document.createelement('div');\ncontainer.style.csstext = 'width:320px;height:240px';\ndocument.body.append(container);\n\nconst chart = createpiechart(container, {\n a11y: { arialabel: 'orders by channel' },\n data: [\n { label: 'direct', value: 42 },\n { label: 'organic', value: 28 },\n { label: 'referral', value: 18 },\n { label: 'social', value: 12 },\n ],\n legend: true,\n tooltip: true,\n variant: 'donut',\n});\n\nchart.dispose();\n```\n\nthe `pie` default has no inner radius. `donut` and `semi` default to an inner radius of 55% of the outer radius. `innerradius` overrides that size in pixels. slice labels are omitted when they do not fit; keep a tooltip or legend when small slices need identifying.\n\n## radar charts\n\ndefine at least three axes when creating a radar chart. series data uses string keys that match `radaraxisconfig.key`; `update()` replaces series but keeps axes and other configuration fixed.\n\n```ts\nimport { createradarchart } from '@vielzeug/prism';\n\nconst container = document.createelement('div');\ncontainer.style.csstext = 'width:400px;height:320px';\ndocument.body.append(container);\n\nconst axes = [\n { key: 'strength', label: 'strength' },\n { key: 'agility', label: 'agility' },\n { key: 'endurance', label: 'endurance' },\n];\nconst chart = createradarchart(container, {\n a11y: { arialabel: 'hero stats' },\n axes,\n domain: [0, 10],\n series: [{\n data: [{ key: 'strength', value: 8 }, { key: 'agility', value: 6 }, { key: 'endurance', value: 9 }],\n name: 'adam',\n }],\n tooltip: true,\n});\n\nchart.dispose();\n```\n\nthe default shared domain starts at zero and uses a nice maximum; set `domain` to compare series on a fixed range. per axis `min` and `max` override the chart domain for that axis. grid rings, bands, curves, fills, vertex dots, and values are configurable; see `radarchartconfig` and `radargridconfig` in the api reference.\n\n## sparklines\n\nsparklines omit axes, margins, and legends. choose `line`, `area`, or `bar` for numeric arrays, and `stack` for an array of `stacksegment` objects.\n\n```ts\nimport { createsparkline } from '@vielzeug/prism';\n\nconst container = document.createelement('div');\ncontainer.style.csstext = 'width:240px;height:48px';\ndocument.body.append(container);\n\nconst chart = createsparkline(container, {\n a11y: { arialabel: 'weekly orders' },\n data: [10, 14, 12, 18, 22],\n variant: 'area',\n});\n\nchart.update([10, 14, 12, 18, 22, 25]);\nchart.dispose();\n```\n\nfor a stacked sparkline, each segment can set `value`, `color`, and `label`. sparklines do not offer keyboard navigation, a tooltip, or a live value announcer. add a visible value or summary when the trend conveys important information.\n\n## interaction\n\nset `tooltip: true` to show prism's default tooltip, or pass a `tooltipconfig` to format its content. a `render` callback receives the active `datum` and `series`. returned strings are inserted as text; return a dom `node` for structured content.\n\n```ts\nimport { createlinechart } from '@vielzeug/prism';\n\nconst container = document.createelement('div');\ncontainer.style.csstext = 'width:640px;height:320px';\ndocument.body.append(container);\n\nconst chart = createlinechart(container, {\n series: [{ data: [{ key: 1, value: 4200 }, { key: 2, value: 5100 }], name: 'revenue' }],\n tooltip: {\n render: (datum, series) => `${series.name}: ${new intl.numberformat().format(datum.value)}`,\n },\n});\n\nchart.dispose();\n```\n\n`onhover` receives an event while a point, category, slice, or radar axis is active, then `null` when the interaction clears. `onclick` receives the selected event. for line, area, and bar charts, `chartevent.values` contains each series' datum at that shared key; a missing datum is `undefined`. scatter, pie, radar, and sparkline use their chart specific event types.\n\n`crosshair` is available on line, area, and scatter charts. by default it shows a vertical guide and snaps the line to the nearest datum. set `horizontal: true` to add a horizontal guide or `vertical: false` to hide the vertical one. set `snap: false` to make the guide follow the pointer; tooltip and callback data still come from the nearest datum.\n\n### keyboard and announcements\n\nset `a11y: { arialabel }` on an informative chart. prism gives the svg `role=\"img\"`, an accessible name, and keyboard focus. arrow keys move among chart data; `home` and `end` move to the first and last item where supported; `enter` or `space` activates an item when an `onclick` callback is configured; `escape` clears the active selection. sparklines do not support keyboard navigation.\n\nwhen `a11y` is omitted, or set to `{ decorative: true }`, the svg is marked `aria hidden=\"true\"` and does not enter the keyboard tab order. only mark charts decorative when their information is available elsewhere. interactive cartesian and pie/radar charts use the tooltip as a polite live region when enabled; otherwise they announce active values in `.prism live`. sparklines have no live value announcements, so provide an equivalent visible value or summary.\n\n## animation\n\nchart motion is enabled by default, with a 300 ms duration per animated lane (radar uses 400 ms). pass `transition: false` to render updates synchronously, or pass `true` to state the default explicitly.\n\n```ts\nimport { createbarchart } from '@vielzeug/prism';\n\nconst container = document.createelement('div');\ncontainer.style.csstext = 'width:640px;height:320px';\ndocument.body.append(container);\n\nconst chart = createbarchart(container, {\n series: [{ data: [{ key: 'a', value: 4 }, { key: 'b', value: 7 }], name: 'count' }],\n transition: { duration: 500, easing: 'ease out', preference: 'system' },\n});\n\nchart.dispose();\n```\n\ncolumns grow together from their baseline by default over 300 ms. lines trace their final curve over 420 ms, uncovering point markers along the way without fading or shifting the series. line data updates retain the default 300 ms geometry transition; an explicit `duration` overrides both timings.\n\n`preference: 'system'` (the default) disables javascript motion when the user prefers reduced motion. use `'always'` to ignore that preference or `'never'` to disable motion. `stagger` sets an optional delay between bars or radar series; bar charts cap the effective delay so long category axes do not add unbounded time. named easing values are `ease in`, `ease in out`, `ease out`, `back out`, `expo out`, and `linear`; you can also provide an easing function.\n\n## responsive layout and lifecycle\n\nsize the host with css. prism observes its content box and updates the svg dimensions and chart layout when it changes. bar and line resize redraws apply immediately rather than animating between layout sizes; entrance animations and explicit data update transitions remain enabled. the legend is rendered as container chrome outside the svg and included in resize calculations.\n\ncall `dispose()` when the view no longer owns the chart. disposal is idempotent: it removes the svg, tooltip, legend, and resize observation, stops in flight chart motion, and aborts `disposalsignal`. `chart[symbol.dispose]()` calls the same cleanup method.\n\nuse `tap()` when external code needs resize or dispose notifications rather than observing the chart dom itself:\n\n```ts\nimport { createlinechart } from '@vielzeug/prism';\n\nconst container = document.createelement('div');\ncontainer.style.csstext = 'width:640px;height:320px';\ndocument.body.append(container);\n\nconst chart = createlinechart(container, {\n series: [{ data: [{ key: 1, value: 10 }], name: 'revenue' }],\n});\nconst stopobserving = chart.tap((event) => {\n if (event.type === 'resize') console.log(event.width, event.height);\n if (event.type === 'dispose') console.log('chart disposed');\n});\n\nstopobserving();\nchart.dispose();\n```\n\n`tap()` returns an unsubscribe function and accepts `{ signal }` for automatic unsubscription. a listener exception is swallowed by the observer so it cannot interrupt chart rendering.\n\n## theming\n\nimport the stylesheet once to load prism's structural styles, default tokens, dark mode rules, and reduced motion css. you can override any css custom property on an ancestor of the chart.\n\n```css\n.dashboard chart {\n prism color 1: #2563eb;\n prism color 2: #16a34a;\n prism grid color: #94a3b8;\n prism line width: 3;\n prism text color: #172033;\n prism tooltip bg: #111827;\n}\n```\n\n`settheme()` provides a typed subset of theme properties. a scoped call writes inline values on the provided element; omit `scope` to write them to `document.documentelement`. inline values override stylesheet tokens until reset.\n\n```ts\nimport { resettheme, settheme } from '@vielzeug/prism';\n\nconst container = document.createelement('div');\ncontainer.style.csstext = 'width:640px;height:320px';\ndocument.body.append(container);\n\nsettheme({ colors: ['#2563eb', '#16a34a'], fontfamily: 'inter, sans serif' }, { scope: container });\n// call resettheme({ scope: container }) when this scoped theme is no longer needed.\nresettheme({ scope: container });\n```\n\n### available tokens\n\nthe stylesheet defines defaults for the following token groups. pie label color and size are read with fallback values by the renderer but are not declared in the base token block. set a token on the chart container or an ancestor to scope an override.\n\n| group | css custom properties |\n| | |\n| palette and surfaces | ` prism color 1` through ` prism color 8`, ` prism bg`, ` prism border radius` |\n| axes and grid | ` prism axis color`, ` prism axis width`, ` prism grid color`, ` prism grid width`, ` prism grid dash`, ` prism grid opacity` |\n| text | ` prism font family`, ` prism font size`, ` prism font size label`, ` prism font size title`, ` prism font weight`, ` prism font weight title`, ` prism text color`, ` prism text color secondary` |\n| tooltip | ` prism tooltip bg`, ` prism tooltip color`, ` prism tooltip border`, ` prism tooltip radius`, ` prism tooltip padding`, ` prism tooltip shadow`, ` prism tooltip font size` |\n| crosshair | ` prism crosshair color`, ` prism crosshair width`, ` prism crosshair dash`, ` prism crosshair opacity` |\n| points | ` prism point radius`, ` prism point radius hover`, ` prism point stroke width`, ` prism point stroke` |\n| bars | ` prism bar radius`, ` prism bar hover opacity`, ` prism bar gap`, ` prism bar band fill`, ` prism bar band opacity`, ` prism bar dim opacity` |\n| areas and lines | ` prism area opacity`, ` prism area stroke width`, ` prism area gradient start`, ` prism area gradient end`, ` prism line width`, ` prism line width hover` |\n| interaction | ` prism dim opacity`, ` prism active point radius`, ` prism point ring`, ` prism focus color` |\n| sparklines | ` prism spark fill opacity`, ` prism spark gradient start`, ` prism spark gradient end` |\n| radar | ` prism radar grid color`, ` prism radar grid opacity`, ` prism radar band fill`, ` prism radar band opacity`, ` prism radar spoke color`, ` prism radar fill opacity`, ` prism radar stroke width`, ` prism radar point radius`, ` prism radar point radius active`, ` prism radar label color`, ` prism radar label size`, ` prism radar value color`, ` prism radar value size`, ` prism radar dim opacity` |\n| motion | ` prism duration fast`, ` prism duration`, ` prism duration slow`, ` prism ease`, ` prism ease spring`, ` prism transition`, ` prism transition fast` |\n| legend and pie labels | ` prism legend gap`, ` prism legend dot size`, ` prism legend font size`, ` prism pie label color`, ` prism pie label size` |\n\nthe default palette and structural colors adapt to `prefers color scheme: dark`, `html.dark`, or `[data prism theme='dark']` on an ancestor. scoped inline values set with `settheme()` take precedence in either mode. the stylesheet also provides `prefers reduced motion` overrides for css transitions.\n\n## errors and diagnostics\n\nprism throws `prismrendererror` when the container is not a dom element, when initial rendering fails, or when `update()` is called on a disposed chart. a wrapped initial render error exposes its cause. use `prismerror` to catch all prism originated errors.\n\nempty or malformed data is not always an exception. depending on the chart, prism may draw nothing or emit a development only `console.warn`; examples include an unknown `bandscale` category, negative values in stacked bars, and radar axes with fewer than three entries or duplicate keys. correct the input rather than relying on a warning for validation.\n\n## framework integration\n\nprism has no react, vue, or svelte adapter. in any framework, give prism a host element after it is mounted, call `update()` when data changes, and call `dispose()` before the host is removed. this plain dom pattern shows the ownership boundary without depending on a framework specific lifecycle api:\n\n```ts\nimport { createlinechart, type lineseriesconfig } from '@vielzeug/prism';\n\nfunction mountchart(host: htmlelement, series: lineseriesconfig[]) {\n const chart = createlinechart(host, { a11y: { arialabel: 'revenue' }, series });\n\n return {\n update(next: lineseriesconfig[]) {\n chart.update(next);\n },\n dispose() {\n chart.dispose();\n },\n };\n}\n\nconst host = document.createelement('div');\nhost.style.csstext = 'width:640px;height:320px';\ndocument.body.append(host);\nconst view = mountchart(host, [{ data: [{ key: 1, value: 10 }], name: 'revenue' }]);\nview.update([{ data: [{ key: 1, value: 10 }, { key: 2, value: 15 }], name: 'revenue' }]);\nview.dispose();\n```\n\nthe application owns its state and host; prism owns only the svg, observer, and optional overlay elements it creates.\n\n## working with other vielzeug libraries\n\nuse refine components for dashboard controls and keep prism in a sized host element. map refine theme tokens to prism css custom properties or call `settheme()` with a scoped element; prism does not require a refine adapter. prism uses orbit internally to place tooltips, so chart users do not need to create orbit floating elements for the built in tooltip.\n\n## best practices\n\n give the host a non zero size before creating a chart.\n set `a11y.arialabel` when a chart communicates information.\n use `date` or number keys consistently across each line or area chart.\n pass the complete new data value to `update()`.\n keep chart options fixed after creation; recreate the chart to change them.\n dispose the handle when its host leaves the owning view.\n use `transition: false` for deterministic synchronous rendering.\n prefer css custom properties for reusable theme overrides.\n",
|
|
963
|
+
"api": " \ntitle: 'prism: api reference'\ndescription: complete reference for prism chart factories, scales, themes, types, and errors.\n \n\n[[toc]]\n\n## api overview\n\nall chart factories, scale utilities, theme helpers, and error classes are synchronous. charts render into an html element and return a `charthandle`; they do not return a promise.\n\n| symbol | purpose | execution mode | common gotcha |\n| | | | |\n| `createlinechart()` | render one or more continuous line series | sync | `key` values must be numbers or dates |\n| `createareachart()` | render continuous series with filled areas | sync | fills use a gradient by default |\n| `createscatterchart()` | plot independent `(x, y)` points | sync | `key` is the x value; the y domain is fitted to data |\n| `createbarchart()` | render categorical bars | sync | negative values in stacked variants are clamped to zero |\n| `createpiechart()` | render pie, donut, or semi circle slices | sync | `variant` is fixed at creation; `update()` replaces slices |\n| `createradarchart()` | compare series across fixed axes | sync | requires at least three axes to draw |\n| `createsparkline()` | render a compact line, area, bar, or stack | sync | has no axes, legend, or keyboard navigation |\n| `linearscale()` | create a numeric scale | sync | `nice` defaults to `true`; set `nice: false` to preserve the domain |\n| `timescale()` | create a date scale | sync | `nice` defaults to `true` |\n| `bandscale()` | create an ordered categorical scale | sync | unknown categories map to `0` and warn in development |\n| `settheme()` | set prism css custom properties | sync | writes inline styles to `document.documentelement` by default |\n| `resettheme()` | remove theme overrides set by `settheme()` | sync | only clears the selected target |\n| `seriescolor()` | resolve a palette css value | sync | palette positions wrap after eight |\n| `prismerror` | base class for prism errors | sync | use `instanceof` to catch any prism error |\n| `prismrendererror` | report an invalid chart container or failed chart operation | sync | also thrown by `update()` after disposal |\n\n## package entry point\n\n| import | purpose |\n| | |\n| `@vielzeug/prism` | chart factories, scale and theme functions, error classes, and public types |\n| `@vielzeug/prism/theme.css` | default chart styles, css custom properties, dark mode values, and reduced motion rules |\n\n## chart factories\n\neach factory appends an svg to `container`. a chart with no measurable container size uses a temporary `600 × 300` svg size and emits a development warning; `resizeobserver` applies later size changes. unless `a11y` is informative, the svg is hidden from the accessibility tree.\n\n### `createlinechart()`\n\n```ts\nfunction createlinechart(\n container: htmlelement,\n config: linechartconfig,\n): charthandle<lineseriesconfig[]>;\n```\n\nrenders line series and returns a handle whose `update()` argument replaces the complete series array. axes render by default; the value axis has grid lines by default. pass `false` to `xaxis` or `yaxis` to suppress that axis.\n\nseries use linear interpolation by default, omit point markers by default, and use a point radius of 3 when markers are enabled. `yaxis` defaults to `'left'`; crosshair, tooltip, and legend are disabled unless configured.\n\n| parameter | type | description |\n| | | |\n| `container` | `htmlelement` | host that receives the chart svg and optional overlays |\n| `config` | `linechartconfig` | series, axes, interaction, accessibility, and motion settings |\n\n**returns:** `charthandle<lineseriesconfig[]>`.\n\n```ts\nimport { createlinechart } from '@vielzeug/prism';\n\nconst host = document.createelement('div');\nhost.style.csstext = 'width:640px;height:320px';\ndocument.body.append(host);\n\nconst chart = createlinechart(host, {\n a11y: { arialabel: 'revenue by month' },\n series: [{ data: [{ key: 1, value: 10 }, { key: 2, value: 15 }], name: 'revenue' }],\n});\nchart.update([{ data: [{ key: 1, value: 10 }, { key: 2, value: 15 }, { key: 3, value: 18 }], name: 'revenue' }]);\nchart.dispose();\n```\n\n### `createareachart()`\n\n```ts\nfunction createareachart(\n container: htmlelement,\n config: areachartconfig,\n): charthandle<areaseriesconfig[]>;\n```\n\nrenders continuous area series with a gradient fill by default. `update()` replaces the complete series array.\n\narea series use linear interpolation by default and show their top line unless `showline: false` is set. axes render by default, with grid lines on the value axis.\n\n| parameter | type | description |\n| | | |\n| `container` | `htmlelement` | host that receives the chart svg and optional overlays |\n| `config` | `areachartconfig` | series, axes, interaction, accessibility, and motion settings |\n\n**returns:** `charthandle<areaseriesconfig[]>`.\n\n```ts\nimport { createareachart } from '@vielzeug/prism';\n\nconst host = document.createelement('div');\nhost.style.csstext = 'width:640px;height:320px';\ndocument.body.append(host);\n\nconst chart = createareachart(host, {\n a11y: { arialabel: 'visitors over time' },\n series: [{ data: [{ key: 1, value: 10 }, { key: 2, value: 18 }], name: 'visitors' }],\n});\nchart.dispose();\n```\n\n### `createscatterchart()`\n\n```ts\nfunction createscatterchart(\n container: htmlelement,\n config: scatterchartconfig,\n): charthandle<scatterseriesconfig[]>;\n```\n\nplots each datum independently: `key` is the numeric or date x value and `value` is the y value. points are not joined. `update()` replaces the complete series array.\n\naxes render by default, with grid lines on the value axis. the value scale is fitted to the observed data rather than forced to include zero. tooltip, crosshair, and legend are disabled unless configured.\n\n| parameter | type | description |\n| | | |\n| `container` | `htmlelement` | host that receives the chart svg and optional overlays |\n| `config` | `scatterchartconfig` | independent points, axes, interaction, accessibility, and motion settings |\n\n**returns:** `charthandle<scatterseriesconfig[]>`.\n\n```ts\nimport { createscatterchart } from '@vielzeug/prism';\n\nconst host = document.createelement('div');\nhost.style.csstext = 'width:640px;height:320px';\ndocument.body.append(host);\n\nconst chart = createscatterchart(host, {\n a11y: { arialabel: 'height against weight' },\n series: [{ data: [{ key: 160, value: 55 }, { key: 175, value: 70 }], name: 'cohort a' }],\n tooltip: true,\n});\nchart.dispose();\n```\n\nscatter interaction reports the datum and series of the point nearest the pointer in two dimensions, without a `values` array. keyboard navigation visits points across series in x order. tooltip `titleformat` receives the x value and `valueformat` the y value. on mount, points grow from radius zero at their final positions; updates slide existing points and grow new ones.\n\n### `createbarchart()`\n\n```ts\nfunction createbarchart(\n container: htmlelement,\n config: barchartconfig,\n): charthandle<barseriesconfig[]>;\n```\n\nrenders grouped, stacked, grouped horizontal, or stacked horizontal categories. `update()` replaces the complete series array; the variant is fixed at creation. axes render by default, with grid lines on the value axis.\n\nthe default variant is `grouped`; tooltip and legend are disabled unless configured.\n\n| parameter | type | description |\n| | | |\n| `container` | `htmlelement` | host that receives the chart svg and optional overlays |\n| `config` | `barchartconfig` | series, layout variant, axes, interaction, accessibility, and motion settings |\n\n**returns:** `charthandle<barseriesconfig[]>`.\n\n```ts\nimport { createbarchart } from '@vielzeug/prism';\n\nconst host = document.createelement('div');\nhost.style.csstext = 'width:640px;height:320px';\ndocument.body.append(host);\n\nconst chart = createbarchart(host, {\n a11y: { arialabel: 'tasks by status' },\n series: [{ data: [{ key: 'open', value: 12 }, { key: 'done', value: 8 }], name: 'tasks' }],\n});\nchart.dispose();\n```\n\nfor stacked variants, negative values are treated as zero and produce a development warning.\n\n### `createpiechart()`\n\n```ts\nfunction createpiechart(\n container: htmlelement,\n config: piechartconfig,\n): charthandle<piesliceconfig[]>;\n```\n\nrenders pie, donut, or semi circle slices. the default variant is `pie`; `update()` replaces the slice array. the default inner radius is zero for pies and 55% of the outer radius for donut and semi variants. pie slices default to no padding or rounded corners; donut and semi slices default to 8px padding and corner radius.\n\n| parameter | type | description |\n| | | |\n| `container` | `htmlelement` | host that receives the chart svg and optional overlays |\n| `config` | `piechartconfig` | slices, shape, geometry, legend, tooltip, and interaction settings |\n\n**returns:** `charthandle<piesliceconfig[]>`.\n\n```ts\nimport { createpiechart } from '@vielzeug/prism';\n\nconst host = document.createelement('div');\nhost.style.csstext = 'width:320px;height:240px';\ndocument.body.append(host);\n\nconst chart = createpiechart(host, {\n a11y: { arialabel: 'orders by channel' },\n data: [{ label: 'direct', value: 72 }, { label: 'referral', value: 28 }],\n variant: 'donut',\n});\nchart.dispose();\n```\n\n### `createradarchart()`\n\n```ts\nfunction createradarchart(\n container: htmlelement,\n config: radarchartconfig,\n): charthandle<radarseriesconfig[]>;\n```\n\nrenders one shape per series across the configured axes. `update()` replaces series data but not axes or other configuration. fewer than three axes warn in development and draw no chart. tooltip and legend are disabled unless configured.\n\n| parameter | type | description |\n| | | |\n| `container` | `htmlelement` | host that receives the chart svg and optional overlays |\n| `config` | `radarchartconfig` | fixed axes, series, scale, grid, appearance, and interaction settings |\n\n**returns:** `charthandle<radarseriesconfig[]>`.\n\n```ts\nimport { createradarchart } from '@vielzeug/prism';\n\nconst host = document.createelement('div');\nhost.style.csstext = 'width:400px;height:320px';\ndocument.body.append(host);\n\nconst chart = createradarchart(host, {\n a11y: { arialabel: 'hero stats' },\n axes: [{ key: 'str', label: 'strength' }, { key: 'agi', label: 'agility' }, { key: 'end', label: 'endurance' }],\n domain: [0, 10],\n series: [{ data: [{ key: 'str', value: 8 }, { key: 'agi', value: 6 }, { key: 'end', value: 9 }], name: 'adam' }],\n});\nchart.dispose();\n```\n\nradar defaults are a solid fill, linear curves, visible points, a four level polygon grid with bands, no grid labels, and a first axis angle of zero degrees. grid labels render only when all axes share a domain.\n\n### `createsparkline()`\n\n```ts\nfunction createsparkline(\n container: htmlelement,\n config: sparklineconfig,\n): charthandle<number[] | stacksegment[]>;\n```\n\nrenders compact data without axes, chart margins, or a legend. `update()` replaces the number or stack segment array. the default variant is `line`; only line and area variants draw the endpoint marker by default.\n\n| parameter | type | description |\n| | | |\n| `container` | `htmlelement` | host that receives the chart svg |\n| `config` | `sparklineconfig` | values, variant, appearance, accessibility, and pointer callbacks |\n\n**returns:** `charthandle<number[] | stacksegment[]>`.\n\n```ts\nimport { createsparkline } from '@vielzeug/prism';\n\nconst host = document.createelement('div');\nhost.style.csstext = 'width:240px;height:48px';\ndocument.body.append(host);\n\nconst chart = createsparkline(host, { data: [2, 5, 3, 8], variant: 'area' });\nchart.update([2, 5, 3, 8, 13]);\nchart.dispose();\n```\n\n`stacksegment[]` is for `variant: 'stack'`. pointer callbacks are available for numeric line, area, and bar sparklines; sparklines do not support keyboard navigation.\n\n## scale factories\n\n### `linearscale()`\n\n```ts\nfunction linearscale(config: linearscaleconfig): scale<number>;\n```\n\nmaps a numeric domain to a numeric range. `nice` defaults to `true`; `clamp` defaults to `false` and applies to `map()`, not `invert()`.\n\n| parameter | type | default | description |\n| | | | |\n| `config.domain` | `[number, number]` | required | input extent |\n| `config.range` | `[number, number]` | required | output extent |\n| `config.clamp` | `boolean` | `false` | clamp mapped values to the range |\n| `config.nice` | `boolean` | `true` | expand the domain to rounded boundaries |\n\n**returns:** `scale<number>`, with `map`, `invert`, `ticks`, `domain`, and `range`.\n\n```ts\nimport { linearscale } from '@vielzeug/prism';\n\nconst scale = linearscale({ domain: [0, 100], range: [0, 500] });\nscale.map(50); // 250\n```\n\n### `timescale()`\n\n```ts\nfunction timescale(config: timescaleconfig): scale<date>;\n```\n\nmaps date values to a numeric range. `nice` defaults to `true` and rounds the domain to a time interval.\n\n| parameter | type | default | description |\n| | | | |\n| `config.domain` | `[date, date]` | required | input date extent |\n| `config.range` | `[number, number]` | required | output extent |\n| `config.nice` | `boolean` | `true` | expand the domain to time interval boundaries |\n\n**returns:** `scale<date>`, with `map`, `invert`, `ticks`, `domain`, and `range`.\n\n```ts\nimport { timescale } from '@vielzeug/prism';\n\nconst scale = timescale({\n domain: [new date('2026 01 01'), new date('2026 12 31')],\n range: [0, 500],\n});\nscale.map(new date('2026 07 02'));\n```\n\n### `bandscale()`\n\n```ts\nfunction bandscale(config: bandscaleconfig): bandscale;\n```\n\nmaps ordered string categories to the start of each band. `padding` defaults to `0.1`; `paddingouter` defaults to the same value. mapping an unknown category returns `0` and warns in development.\n\n| parameter | type | default | description |\n| | | | |\n| `config.domain` | `string[]` | required | ordered categories |\n| `config.range` | `[number, number]` | required | output extent |\n| `config.padding` | `number` | `0.1` | inner gap as a ratio of bandwidth |\n| `config.paddingouter` | `number` | `padding` | outer gap as a ratio of bandwidth |\n\n**returns:** `bandscale`, with `map`, `ticks`, `bandwidth`, `gap`, `domain`, and `range`.\n\n```ts\nimport { bandscale } from '@vielzeug/prism';\n\nconst scale = bandscale({ domain: ['a', 'b'], range: [0, 200] });\nscale.bandwidth();\n```\n\n## theme utilities\n\n`theme.css` supplies the base css values and styles. `settheme()` writes inline custom properties, which take precedence over stylesheet declarations. for other visual controls, set css custom properties directly.\n\n### `settheme()`\n\n```ts\nfunction settheme(theme: prismtheme, options?: themescope): void;\n```\n\nsets the provided theme values on `options.scope`, or on `document.documentelement` when omitted. omitted scalar keys remain unchanged. providing `colors` replaces all eight palette slots, removing unused slots.\n\n| parameter | type | description |\n| | | |\n| `theme` | `prismtheme` | palette and supported axis, grid, text, font, and tooltip properties |\n| `options.scope` | `htmlelement` | element that scopes the inline custom properties |\n\n**returns:** `void`.\n\n```ts\nimport { settheme } from '@vielzeug/prism';\n\nconst host = document.createelement('div');\ndocument.body.append(host);\nsettheme({ colors: ['#2563eb', '#16a34a'], textcolor: '#111827' }, { scope: host });\n```\n\n### `resettheme()`\n\n```ts\nfunction resettheme(options?: themescope): void;\n```\n\nremoves every custom property managed by `settheme()` from the given scope or the document root. it does not remove css rules or change any other custom properties.\n\n| parameter | type | description |\n| | | |\n| `options` | `themescope` | optional target; defaults to `document.documentelement` |\n\n**returns:** `void`.\n\n```ts\nimport { resettheme, settheme } from '@vielzeug/prism';\n\nconst scope = document.createelement('div');\nsettheme({ textcolor: '#111827' }, { scope });\nresettheme({ scope });\n```\n\n### `seriescolor()`\n\n```ts\nfunction seriescolor(index: number, override?: string): string;\n```\n\nreturns `override` when it is provided; otherwise returns a `var( prism color n)` reference. the palette has eight positions and the index wraps around after the eighth.\n\n| parameter | type | description |\n| | | |\n| `index` | `number` | zero based palette position |\n| `override` | `string` | optional color returned instead of a css variable |\n\n**returns:** `string`.\n\n```ts\nimport { seriescolor } from '@vielzeug/prism';\n\nseriescolor(0); // 'var( prism color 1)'\nseriescolor(1, '#2563eb'); // '#2563eb'\n```\n\n## chart handle\n\n`charthandle<tdata>` is returned by every chart factory. `update()` synchronously rebuilds the chart from its new data; enabled motion may animate the resulting svg. `dispose()` is idempotent, removes the chart svg, observers, tooltip and legend, cancels chart owned animation, aborts `disposalsignal`, and emits one `dispose` tap event. `update()` after disposal throws `prismrendererror`.\n\n`tap()` observes `resize` and `dispose` events outside the render path. it returns an unsubscribe function; an optional abort signal removes the subscription. observer handler exceptions are swallowed so they cannot affect chart behavior. a tap added after disposal returns a no op unsubscribe function.\n\n`[symbol.dispose]()` delegates to `dispose()`, allowing explicit resource management where supported.\n\nall chart handles expose the same lifecycle members; interactive cartesian and pie/radar charts use a `.prism live` status region when no tooltip is enabled. a tooltip is itself a polite status region. sparklines have neither a tooltip nor a live value announcer.\n\n| method or property | contract |\n| | |\n| `el` | the chart's root `svgsvgelement` |\n| `update(data)` | replace chart data and render; throws `prismrendererror` after disposal |\n| `dispose()` | idempotently release chart owned dom, observation, and motion |\n| `disposed` | whether the handle has been disposed |\n| `disposalsignal` | `abortsignal` aborted during disposal |\n| `tap(handler, options?)` | observe resize/dispose events; returns an unsubscribe function |\n| `[symbol.dispose]()` | calls `dispose()` |\n\n`prismevent` reports a changed svg content size as `{ type: 'resize', width, height }`; the `dispose` event is emitted once before teardown completes.\n\n## types\n\nthe following are all types and interfaces exported from `@vielzeug/prism`. definitions mirror the package entry point contracts.\n\n### animation types\n\n```ts\nexport type easingfn = (t: number) => number;\nexport type easingname = 'back out' | 'ease in' | 'ease in out' | 'ease out' | 'expo out' | 'linear';\n\nexport interface transitionconfig {\n /** milliseconds per animated lane. defaults to 300 (420 for a line entrance, 400 for radar). */\n duration?: number;\n easing?: easingname | ((t: number) => number);\n /** `system` skips animation when reduced motion is preferred. */\n preference?: 'always' | 'never' | 'system';\n /** per bar delay on bar charts, per entering point delay (x order) on scatter charts, or per series delay on radar charts. bar and scatter charts cap the total added delay at about 400 ms. */\n stagger?: number;\n}\n\nexport type transitionoption = transitionconfig | boolean;\n```\n\n### theme types\n\n```ts\nexport interface prismtheme {\n axiscolor?: string;\n colors?: string[];\n fontfamily?: string;\n gridcolor?: string;\n gridopacity?: number;\n textcolor?: string;\n textcolorsecondary?: string;\n tooltipbg?: string;\n tooltipborder?: string;\n tooltipcolor?: string;\n}\n\nexport interface themescope {\n readonly scope?: htmlelement;\n}\n```\n\n`prismtheme.colors` writes up to eight palette slots. the scalar fields map as follows:\n\n| field | css custom property |\n| | |\n| `axiscolor` | ` prism axis color` |\n| `fontfamily` | ` prism font family` |\n| `gridcolor` | ` prism grid color` |\n| `gridopacity` | ` prism grid opacity` |\n| `textcolor` | ` prism text color` |\n| `textcolorsecondary` | ` prism text color secondary` |\n| `tooltipbg` | ` prism tooltip bg` |\n| `tooltipborder` | ` prism tooltip border` |\n| `tooltipcolor` | ` prism tooltip color` |\n\n### core and data types\n\n```ts\nexport interface chartmargin {\n bottom: number;\n left: number;\n right: number;\n top: number;\n}\n\nexport interface chartdimensions {\n height: number;\n margin: chartmargin;\n width: number;\n}\n\nexport type prismevent =\n | { readonly height: number; readonly type: 'resize'; readonly width: number }\n | { readonly type: 'dispose' };\n\nexport interface charthandle<tdata = unknown> {\n readonly disposalsignal: abortsignal;\n dispose(): void;\n readonly disposed: boolean;\n readonly el: svgsvgelement;\n tap(handler: (event: prismevent) => void, options?: { readonly signal?: abortsignal }): () => void;\n update(data: tdata): void;\n [symbol.dispose](): void;\n}\n\nexport type charta11y =\n | { readonly decorative: true }\n | { readonly arialabel: string; readonly decorative?: false };\n\nexport interface datum<tkey extends date | number | string = date | number | string> {\n dash?: string;\n key: tkey;\n meta?: record<string, unknown>;\n opacity?: number;\n value: number;\n}\n\nexport type continuousdatum = datum<date | number>;\n\nexport interface series<tdatum extends datum = datum> {\n color?: string;\n data: tdatum[];\n id?: string;\n name: string;\n}\n\nexport interface chartevent {\n datum: datum;\n originalevent: event;\n series: series;\n values?: seriesvalue[];\n}\n\nexport interface seriesvalue {\n datum: datum | undefined;\n series: series;\n}\n```\n\n`datum.dash` and `datum.opacity` affect rendered marks only on the chart types that support them. on bars, `dash` adds a 1px series color stroke so the dash is visible against the fill. `series.id` becomes a `data series id` attribute on a series group; characters outside `[a za z0 9_ ]` are replaced with ` `. when omitted, the id is based on the series index.\n\n| chart | `dash` | `opacity` |\n| | | |\n| bar | strokes that datum's bar | applies to that bar |\n| line | applies to the segment from this datum to the next | applies to that segment and its point marker |\n| area | applies to the top line segment from this datum to the next | applies to that segment |\n| scatter | ignored | applies to that point |\n| pie, radar, sparkline | ignored | ignored |\n\nfor line and area charts, the last datum has no following segment, so its `dash` value is ignored. the chart factories group adjacent segments with the same presentation values into path runs.\n\nfor line, area, and bar charts, `chartevent.values` lists each series at the active key in series order. its `datum` is `undefined` when that series has no matching key. `originalevent` is a pointer event or a keyboard event for keyboard activation. scatter events identify only the nearest independent point and omit `values`.\n\n### scale types\n\n```ts\nexport interface scale<t> {\n readonly domain: readonly [t, t];\n invert(pixel: number): t;\n map(value: t): number;\n readonly range: readonly [number, number];\n ticks(count?: number): t[];\n}\n\nexport interface bandscale {\n bandwidth(): number;\n readonly domain: readonly string[];\n gap(): number;\n map(value: string): number;\n readonly range: readonly [number, number];\n ticks(count?: number): string[];\n}\n\nexport interface linearscaleconfig {\n clamp?: boolean;\n domain: [number, number];\n nice?: boolean;\n range: [number, number];\n}\n\nexport interface timescaleconfig {\n domain: [date, date];\n nice?: boolean;\n range: [number, number];\n}\n\nexport interface bandscaleconfig {\n domain: string[];\n padding?: number;\n paddingouter?: number;\n range: [number, number];\n}\n```\n\n### axis and interaction types\n\n```ts\nexport type horizontalaxisposition = 'bottom' | 'top';\nexport type verticalaxisposition = 'left' | 'right';\nexport type axisposition = horizontalaxisposition | verticalaxisposition;\n\nexport interface gridconfig {\n color?: string;\n dash?: string;\n}\n\nexport interface axisconfig<tposition extends axisposition = axisposition> {\n grid?: gridconfig | boolean;\n label?: string;\n position?: tposition;\n tickcount?: number;\n tickformat?: (value: date | number | string) => string;\n tickvalues?: readonlyarray<date | number | string>;\n}\n\nexport type xaxisconfig = axisconfig<horizontalaxisposition>;\nexport type yaxisconfig = axisconfig<verticalaxisposition>;\n\nexport interface tooltipconfig {\n offset?: number;\n render?: (datum: datum, series: series) => node | string;\n titleformat?: (key: date | number | string) => string;\n valueformat?: (value: number, datum: datum, series: series) => string;\n}\n\nexport interface crosshairconfig {\n horizontal?: boolean;\n snap?: boolean;\n vertical?: boolean;\n}\n\nexport type legendposition = 'bottom' | 'left' | 'right' | 'top';\n\nexport interface legendconfig {\n position?: legendposition;\n}\n```\n\nthe crosshair defaults to a vertical line that snaps to the nearest datum; the horizontal line is hidden by default. with `snap: false`, only the guide follows the pointer—tooltip and callback data still refer to the nearest datum.\n\n`tickvalues` renders exactly the given ticks instead of scale generated ones, and gridlines follow the same list. values must match the axis's scale kind (numbers on a linear axis, `date`s on a time axis, category strings on a band axis); an off scale value warns in development. without it, ticks come from the scale as before.\n\nnumeric tick labels at a magnitude of 1e5 or above default to compact notation (`123k`, `1.23m`) so long numbers fit the default value axis margin; pass `tickformat: string` for raw labels. supplying a partial `yaxis` (or the value axis of a horizontal bar chart) merges over the default, so `{ tickformat }` alone keeps the default grid lines.\n\n`titleformat` and `valueformat` shape the default comparison tooltip on line, area, and bar charts—the title from the focused key and each row's value—and the same formatting is applied to the tooltip's spoken summary. `render` still replaces the whole tooltip body and takes precedence. tooltip strings are assigned as text, not parsed as html. for a radar chart, `tooltip.render` receives a datum whose `meta` contains the active axis, all series values, and the default announcement text.\n\n### chart configuration types\n\n```ts\nexport interface basechartconfig {\n a11y?: charta11y;\n legend?: legendconfig | boolean;\n margin?: partial<chartmargin>;\n onclick?: (event: chartevent) => void;\n onhover?: (event: chartevent | null) => void;\n tooltip?: tooltipconfig | boolean;\n transition?: transitionoption;\n xaxis?: xaxisconfig | false;\n yaxis?: yaxisconfig | false;\n}\n\nexport interface lineseriesconfig extends series<continuousdatum> {\n curve?: 'linear' | 'monotone' | 'step';\n pointradius?: number;\n showpoints?: boolean;\n strokewidth?: number;\n yaxis?: 'left' | 'right';\n}\n\nexport interface linechartconfig extends basechartconfig {\n crosshair?: crosshairconfig | boolean;\n rightyaxis?: omit<yaxisconfig, 'position' | 'grid'>;\n series: lineseriesconfig[];\n}\n\nexport interface areaseriesconfig extends series<continuousdatum> {\n curve?: 'linear' | 'monotone' | 'step';\n fill?: 'gradient' | 'solid';\n fillopacity?: number;\n showline?: boolean;\n}\n\nexport interface areachartconfig extends basechartconfig {\n crosshair?: crosshairconfig | boolean;\n series: areaseriesconfig[];\n}\n\nexport interface scatterseriesconfig extends series<continuousdatum> {\n pointradius?: number;\n}\n\nexport interface scatterchartconfig extends basechartconfig {\n crosshair?: crosshairconfig | boolean;\n series: scatterseriesconfig[];\n}\n\nexport interface barseriesconfig extends series {\n borderradius?: number;\n}\n\nexport type barvariant = 'grouped' | 'grouped horizontal' | 'stacked' | 'stacked horizontal';\n\nexport interface barchartconfig extends basechartconfig {\n series: barseriesconfig[];\n variant?: barvariant;\n}\n\nexport type pievariant = 'donut' | 'pie' | 'semi';\n\nexport interface piesliceconfig {\n color?: string;\n label?: string;\n value: number;\n}\n\nexport interface pieevent {\n index: number;\n originalevent: event;\n slice: piesliceconfig;\n}\n\nexport interface piechartconfig extends omit<basechartconfig, 'margin' | 'onclick' | 'onhover' | 'xaxis' | 'yaxis'> {\n cornerradius?: number;\n data: piesliceconfig[];\n innerradius?: number;\n onclick?: (event: pieevent) => void;\n onhover?: (event: pieevent | null) => void;\n padpixels?: number;\n variant?: pievariant;\n}\n\nexport interface radaraxisconfig {\n format?: (value: number) => string;\n key: string;\n label: string;\n max?: number;\n min?: number;\n}\n\nexport type radarseriesconfig = series<datum<string>>;\n\nexport interface radargridconfig {\n bands?: boolean;\n labels?: boolean;\n levels?: number;\n shape?: 'circle' | 'polygon';\n}\n\nexport interface radaraxisvalue {\n datum: datum<string> | undefined;\n series: radarseriesconfig;\n}\n\nexport interface radarevent {\n axis: radaraxisconfig;\n index: number;\n originalevent: event;\n values: radaraxisvalue[];\n}\n\nexport interface radarchartconfig extends omit<basechartconfig, 'margin' | 'onclick' | 'onhover' | 'xaxis' | 'yaxis'> {\n axes: radaraxisconfig[];\n curve?: 'linear' | 'rounded';\n domain?: [number, number];\n fill?: 'gradient' | 'none' | 'solid';\n grid?: radargridconfig | boolean;\n onclick?: (event: radarevent) => void;\n onhover?: (event: radarevent | null) => void;\n series: radarseriesconfig[];\n showpoints?: boolean;\n showvalues?: boolean;\n startangle?: number;\n}\n\nexport type sparklinevariant = 'area' | 'bar' | 'line' | 'stack';\n\nexport interface stacksegment {\n color?: string;\n label?: string;\n value: number;\n}\n\nexport interface sparklineevent {\n index: number;\n originalevent: event;\n value: number;\n}\n\nexport interface sparklineconfig {\n a11y?: charta11y;\n color?: string;\n cornerradius?: number;\n curve?: 'linear' | 'monotone' | 'step';\n data: number[] | stacksegment[];\n fillopacity?: number;\n onclick?: (event: sparklineevent) => void;\n onhover?: (event: sparklineevent | null) => void;\n padpixels?: number;\n showendpoint?: boolean;\n strokewidth?: number;\n transition?: transitionoption;\n variant?: sparklinevariant;\n}\n```\n\n## errors\n\n### `prismerror`\n\n```ts\nclass prismerror extends error {\n constructor(message: string, opts?: erroroptions);\n}\n```\n\nbase class for prism originated errors. its `name` is set to the concrete error class name, and it supports the standard `erroroptions` including `cause`.\n\n### `prismrendererror`\n\n```ts\nclass prismrendererror extends prismerror {}\n```\n\nthrown when a chart cannot be rendered because its container is not a dom element, when initial rendering fails (the original error is available as `cause`), or when `update()` is called after disposal. empty or malformed chart data may instead produce a development warning or an empty rendering, depending on the chart.\n",
|
|
964
|
+
"usage": " \ntitle: 'prism: usage guide'\ndescription: create responsive svg charts, update their data, handle interaction, and theme @vielzeug/prism.\n \n\n[[toc]]\n\n## basic usage\n\ncreate a host element with non zero dimensions, then pass it to a chart factory. this example creates the host in javascript so the snippet can run as written.\n\n```ts\nimport { createlinechart } from '@vielzeug/prism';\nimport '@vielzeug/prism/theme.css';\n\nconst container = document.createelement('div');\ncontainer.style.csstext = 'width:640px;height:320px';\ndocument.body.append(container);\n\nconst chart = createlinechart(container, {\n a11y: { arialabel: 'revenue by month' },\n series: [\n {\n name: 'revenue',\n data: [\n { key: 1, value: 10 },\n { key: 2, value: 16 },\n ],\n },\n ],\n tooltip: true,\n});\n\nchart.dispose();\n```\n\neach factory appends an svg to its container and returns a `charthandle`. prism observes the container with `resizeobserver`; if it has zero dimensions on creation, prism uses a temporary `600 × 300` size and warns in development. give the host its layout before creating the chart.\n\n## updating data\n\nkeep application state outside prism and pass the next complete data value to `update()`. the update method replaces chart data synchronously; the default transition may animate the resulting svg.\n\n```ts\nimport { createlinechart } from '@vielzeug/prism';\n\nconst container = document.createelement('div');\ncontainer.style.csstext = 'width:640px;height:320px';\ndocument.body.append(container);\n\nlet series = [{ data: [{ key: 1, value: 10 }], name: 'live' }];\nconst chart = createlinechart(container, { series });\n\nseries = [{ data: [{ key: 1, value: 10 }, { key: 2, value: 20 }], name: 'live' }];\nchart.update(series);\nchart.dispose();\n```\n\n`update()` accepts the type shown by that factory's handle: a complete series array for line, area, scatter, bar, and radar charts; a slice array for pie charts; and a number or stack segment array for sparklines. chart options such as axes, variant, callbacks, and accessibility are set at creation. for example, radar axes and a pie chart's variant do not change through `update()`.\n\n## line charts\n\nline and area series use number or `date` keys. all series in one chart must use the same key kind. use `curve` to choose interpolation, and enable markers with `showpoints`.\n\n```ts\nimport { createlinechart } from '@vielzeug/prism';\n\nconst container = document.createelement('div');\ncontainer.style.csstext = 'width:640px;height:320px';\ndocument.body.append(container);\n\nconst chart = createlinechart(container, {\n a11y: { arialabel: 'revenue and expenses' },\n series: [\n {\n curve: 'monotone',\n data: [{ key: 1, value: 100 }, { key: 2, value: 150 }, { key: 3, value: 130 }],\n name: 'revenue',\n showpoints: true,\n },\n {\n color: '#c2410c',\n data: [{ key: 1, value: 70 }, { key: 2, value: 95 }, { key: 3, value: 110 }],\n name: 'expenses',\n },\n ],\n tooltip: true,\n xaxis: { label: 'month' },\n yaxis: { grid: true, label: 'dollars' },\n});\n\nchart.dispose();\n```\n\n### independent value axes\n\nassign a line series to the right axis when it uses a different value range. both axes share the x scale; `rightyaxis` configures the second axis and cannot configure its position or grid.\n\n```ts\nimport { createlinechart } from '@vielzeug/prism';\n\nconst container = document.createelement('div');\ncontainer.style.csstext = 'width:640px;height:320px';\ndocument.body.append(container);\n\nconst chart = createlinechart(container, {\n a11y: { arialabel: 'health and damage by level' },\n rightyaxis: { label: 'damage' },\n series: [\n { data: [{ key: 1, value: 30 }, { key: 2, value: 20 }], name: 'health' },\n { data: [{ key: 1, value: 100 }, { key: 2, value: 300 }], name: 'damage', yaxis: 'right' },\n ],\n yaxis: { grid: true, label: 'health' },\n});\n\nchart.dispose();\n```\n\nwhen any series uses the right axis, the primary value axis is placed on the left. grid lines belong to the primary `yaxis`. the independent numeric scales mean that curve crossings do not imply equal values.\n\n### time based x axis\n\nuse `date` values for every key when plotting time. line and area charts use a time scale whose domain fits the data instead of rounding outward to tick boundaries.\n\n```ts\nimport { createlinechart } from '@vielzeug/prism';\n\nconst container = document.createelement('div');\ncontainer.style.csstext = 'width:640px;height:320px';\ndocument.body.append(container);\n\nconst formatter = new intl.datetimeformat(undefined, { month: 'short', day: 'numeric' });\nconst chart = createlinechart(container, {\n a11y: { arialabel: 'signups over time' },\n series: [{\n data: [\n { key: new date('2026 09 01'), value: 50 },\n { key: new date('2026 09 08'), value: 80 },\n { key: new date('2026 09 15'), value: 120 },\n ],\n name: 'signups',\n }],\n xaxis: { tickformat: (value) => value instanceof date ? formatter.format(value) : string(value) },\n});\n\nchart.dispose();\n```\n\n`tickformat` formats axis labels only. tooltip content and accessibility announcements use the underlying keys and values. to render an exact set of ticks instead of scale generated ones, pass `tickvalues`; gridlines follow the same list. values must match the axis's scale kind, and an off scale value warns in development:\n\n```ts\n// force a tick on the last month, which nice stepped ticks can skip.\nconst chart = createlinechart(container, {\n series: [{ data: [{ key: 0, value: 90 }, { key: 11, value: 128 }], name: 'costs' }],\n xaxis: { tickvalues: [0, 6, 11] },\n});\n```\n\nnumeric tick labels at a magnitude of `1e5` or above default to compact notation (`123k`, `1.23m`) so long numbers fit the default value axis margin; pass `tickformat: string` for raw labels. supplying a partial value axis config merges over its default, so `yaxis: { tickformat }` alone keeps the default grid lines.\n\n## bar charts\n\nbar chart keys are string categories. the default `grouped` variant puts series side by side; the horizontal variants put categories on the y axis and values on the x axis.\n\n```ts\nimport { createbarchart } from '@vielzeug/prism';\n\nconst container = document.createelement('div');\ncontainer.style.csstext = 'width:640px;height:320px';\ndocument.body.append(container);\n\nconst chart = createbarchart(container, {\n a11y: { arialabel: 'sales by quarter' },\n series: [{\n data: [{ key: 'q1', value: 200 }, { key: 'q2', value: 350 }, { key: 'q3', value: 280 }],\n name: 'sales',\n }],\n tooltip: true,\n});\n\nchart.dispose();\n```\n\nchoose `grouped`, `stacked`, `grouped horizontal`, or `stacked horizontal` in `variant`. negative values in a stacked chart are clamped to zero and produce a development warning. bars in stacked variants remain square; `borderradius` applies to grouped bars.\n\naxes render by default, with grid lines on the value axis. for horizontal variants, `xaxis` is the value axis and `yaxis` is the category axis. configure `grid` on the value axis.\n\n## area charts\n\narea series use a gradient fill by default. set `fill: 'solid'` for a flat fill, or set `fillopacity` to override the theme opacity for that series.\n\n```ts\nimport { createareachart } from '@vielzeug/prism';\n\nconst container = document.createelement('div');\ncontainer.style.csstext = 'width:640px;height:320px';\ndocument.body.append(container);\n\nconst chart = createareachart(container, {\n a11y: { arialabel: 'visitors over time' },\n crosshair: true,\n series: [{\n curve: 'monotone',\n data: [{ key: 1, value: 12 }, { key: 2, value: 18 }, { key: 3, value: 15 }],\n fill: 'solid',\n fillopacity: 0.2,\n name: 'visitors',\n showline: true,\n }],\n});\n\nchart.dispose();\n```\n\n## scatter charts\n\nscatter charts treat every datum as an independent point: `key` is the numeric or `date` x value and `value` is the y value. the scales fit the observed values rather than forcing the y domain to include zero. set `pointradius` on a series to change its marker size (default `3`).\n\n```ts\nimport { createscatterchart } from '@vielzeug/prism';\n\nconst container = document.createelement('div');\ncontainer.style.csstext = 'width:640px;height:320px';\ndocument.body.append(container);\n\nconst chart = createscatterchart(container, {\n a11y: { arialabel: 'height against weight' },\n series: [{\n data: [{ key: 160, value: 55 }, { key: 175, value: 70 }, { key: 190, value: 88 }],\n name: 'cohort a',\n pointradius: 5,\n }],\n tooltip: true,\n});\n\nchart.dispose();\n```\n\nscatter callbacks identify the nearest point in two dimensions and do not provide cross series `values`, because points do not share a category key. keyboard navigation visits every point across series in x order. with a tooltip, `titleformat` formats the x value and `valueformat` the y value, in both the visible row and the spoken summary.\n\n## forecast and emphasis styling\n\nadd `dash` or `opacity` to individual `datum` objects when one part of a series needs distinct styling. these fields do not change interaction, tooltip, or accessibility behavior.\n\n```ts\nimport { createlinechart } from '@vielzeug/prism';\n\nconst container = document.createelement('div');\ncontainer.style.csstext = 'width:640px;height:320px';\ndocument.body.append(container);\n\nconst values = [10, 13, 15, 18];\nconst chart = createlinechart(container, {\n series: [{\n data: values.map((value, index) => ({\n key: index + 1,\n value,\n ...(index >= 2 ? { dash: '5 5', opacity: 0.4 } : {}),\n })),\n name: 'forecast',\n showpoints: true,\n }],\n});\n\nchart.dispose();\n```\n\nfor line and area charts, each datum's `dash` styles the segment from that datum to the next; the final datum has no outgoing segment. line opacity also fades the datum's marker. area styling affects the top line, not the fill. bar opacity and dashes apply to the individual bar; scatter opacity applies to its point and scatter dashes are ignored. pie, radar, and sparkline ignore these two fields.\n\n## pie, donut, and semi charts\n\nuse `createpiechart()` for all three shapes. `variant` is fixed when the chart is created; `update()` replaces the slice data.\n\n```ts\nimport { createpiechart } from '@vielzeug/prism';\n\nconst container = document.createelement('div');\ncontainer.style.csstext = 'width:320px;height:240px';\ndocument.body.append(container);\n\nconst chart = createpiechart(container, {\n a11y: { arialabel: 'orders by channel' },\n data: [\n { label: 'direct', value: 42 },\n { label: 'organic', value: 28 },\n { label: 'referral', value: 18 },\n { label: 'social', value: 12 },\n ],\n legend: true,\n tooltip: true,\n variant: 'donut',\n});\n\nchart.dispose();\n```\n\nthe `pie` default has no inner radius. `donut` and `semi` default to an inner radius of 55% of the outer radius. `innerradius` overrides that size in pixels. slice labels are omitted when they do not fit; keep a tooltip or legend when small slices need identifying.\n\n## radar charts\n\ndefine at least three axes when creating a radar chart. series data uses string keys that match `radaraxisconfig.key`; `update()` replaces series but keeps axes and other configuration fixed.\n\n```ts\nimport { createradarchart } from '@vielzeug/prism';\n\nconst container = document.createelement('div');\ncontainer.style.csstext = 'width:400px;height:320px';\ndocument.body.append(container);\n\nconst axes = [\n { key: 'strength', label: 'strength' },\n { key: 'agility', label: 'agility' },\n { key: 'endurance', label: 'endurance' },\n];\nconst chart = createradarchart(container, {\n a11y: { arialabel: 'hero stats' },\n axes,\n domain: [0, 10],\n series: [{\n data: [{ key: 'strength', value: 8 }, { key: 'agility', value: 6 }, { key: 'endurance', value: 9 }],\n name: 'adam',\n }],\n tooltip: true,\n});\n\nchart.dispose();\n```\n\nthe default shared domain starts at zero and uses a nice maximum; set `domain` to compare series on a fixed range. per axis `min` and `max` override the chart domain for that axis. grid rings, bands, curves, fills, vertex dots, and values are configurable; see `radarchartconfig` and `radargridconfig` in the api reference.\n\n## sparklines\n\nsparklines omit axes, margins, and legends. choose `line`, `area`, or `bar` for numeric arrays, and `stack` for an array of `stacksegment` objects.\n\n```ts\nimport { createsparkline } from '@vielzeug/prism';\n\nconst container = document.createelement('div');\ncontainer.style.csstext = 'width:240px;height:48px';\ndocument.body.append(container);\n\nconst chart = createsparkline(container, {\n a11y: { arialabel: 'weekly orders' },\n data: [10, 14, 12, 18, 22],\n variant: 'area',\n});\n\nchart.update([10, 14, 12, 18, 22, 25]);\nchart.dispose();\n```\n\nfor a stacked sparkline, each segment can set `value`, `color`, and `label`. sparklines do not offer keyboard navigation, a tooltip, or a live value announcer. add a visible value or summary when the trend conveys important information.\n\n## interaction\n\nset `tooltip: true` to show prism's default tooltip, or pass a `tooltipconfig` to format its content. `titleformat` and `valueformat` shape the default comparison tooltip on line, area, and bar charts—the title from the focused key and each row's value—without rebuilding its markup, and the same formatting is applied to the tooltip's spoken summary. a `render` callback receives the active `datum` and `series` and replaces the whole body, taking precedence over the formatters. returned strings are inserted as text; return a dom `node` for structured content.\n\n```ts\nimport { createlinechart } from '@vielzeug/prism';\n\nconst container = document.createelement('div');\ncontainer.style.csstext = 'width:640px;height:320px';\ndocument.body.append(container);\n\nconst compact = new intl.numberformat(undefined, { notation: 'compact' });\nconst months = ['jan', 'feb', 'mar', 'apr', 'may', 'jun', 'jul', 'aug', 'sep', 'oct', 'nov', 'dec'];\n\nconst chart = createlinechart(container, {\n series: [{ data: [{ key: 11, value: 128000 }], name: 'ec2' }],\n tooltip: {\n titleformat: (key) => months[number(key)] ?? string(key),\n valueformat: (value) => compact.format(value),\n },\n});\n\nchart.dispose();\n```\n\n`onhover` receives an event while a point, category, slice, or radar axis is active, then `null` when the interaction clears. `onclick` receives the selected event. for line, area, and bar charts, `chartevent.values` contains each series' datum at that shared key; a missing datum is `undefined`. scatter events are `chartevent` objects without `values`; pie, radar, and sparkline use their chart specific event types.\n\n`crosshair` is available on line, area, and scatter charts. by default it shows a vertical guide and snaps the line to the nearest datum. set `horizontal: true` to add a horizontal guide or `vertical: false` to hide the vertical one. set `snap: false` to make the guide follow the pointer; tooltip and callback data still come from the nearest datum.\n\n### keyboard and announcements\n\nset `a11y: { arialabel }` on an informative chart. prism gives the svg `role=\"img\"`, an accessible name, and keyboard focus. arrow keys move among chart data; `home` and `end` move to the first and last item where supported; `enter` or `space` activates an item when an `onclick` callback is configured; `escape` clears the active selection. sparklines do not support keyboard navigation.\n\nwhen `a11y` is omitted, or set to `{ decorative: true }`, the svg is marked `aria hidden=\"true\"` and does not enter the keyboard tab order. only mark charts decorative when their information is available elsewhere. interactive cartesian and pie/radar charts use the tooltip as a polite live region when enabled; otherwise they announce active values in `.prism live`. sparklines have no live value announcements, so provide an equivalent visible value or summary.\n\n## animation\n\nchart motion is enabled by default, with a 300 ms duration per animated lane (radar uses 400 ms). pass `transition: false` to render updates synchronously, or pass `true` to state the default explicitly.\n\n```ts\nimport { createbarchart } from '@vielzeug/prism';\n\nconst container = document.createelement('div');\ncontainer.style.csstext = 'width:640px;height:320px';\ndocument.body.append(container);\n\nconst chart = createbarchart(container, {\n series: [{ data: [{ key: 'a', value: 4 }, { key: 'b', value: 7 }], name: 'count' }],\n transition: { duration: 500, easing: 'ease out', preference: 'system' },\n});\n\nchart.dispose();\n```\n\ncolumns grow together from their baseline by default over 300 ms. lines trace their final curve over 420 ms, uncovering point markers along the way without fading or shifting the series. line data updates retain the default 300 ms geometry transition; an explicit `duration` overrides both timings.\n\n`preference: 'system'` (the default) disables javascript motion when the user prefers reduced motion. use `'always'` to ignore that preference or `'never'` to disable motion. `stagger` sets an optional delay between bars, entering scatter points (in x order), or radar series; bar and scatter charts cap the effective delay so many marks do not add unbounded time. named easing values are `ease in`, `ease in out`, `ease out`, `back out`, `expo out`, and `linear`; you can also provide an easing function.\n\n## responsive layout and lifecycle\n\nsize the host with css. prism observes its content box and updates the svg dimensions and chart layout when it changes. bar, line, and scatter resize redraws apply immediately rather than animating between layout sizes; entrance animations and explicit data update transitions remain enabled. the legend is rendered as container chrome outside the svg and included in resize calculations.\n\ncall `dispose()` when the view no longer owns the chart. disposal is idempotent: it removes the svg, tooltip, legend, and resize observation, stops in flight chart motion, and aborts `disposalsignal`. `chart[symbol.dispose]()` calls the same cleanup method.\n\nuse `tap()` when external code needs resize or dispose notifications rather than observing the chart dom itself:\n\n```ts\nimport { createlinechart } from '@vielzeug/prism';\n\nconst container = document.createelement('div');\ncontainer.style.csstext = 'width:640px;height:320px';\ndocument.body.append(container);\n\nconst chart = createlinechart(container, {\n series: [{ data: [{ key: 1, value: 10 }], name: 'revenue' }],\n});\nconst stopobserving = chart.tap((event) => {\n if (event.type === 'resize') console.log(event.width, event.height);\n if (event.type === 'dispose') console.log('chart disposed');\n});\n\nstopobserving();\nchart.dispose();\n```\n\n`tap()` returns an unsubscribe function and accepts `{ signal }` for automatic unsubscription. a listener exception is swallowed by the observer so it cannot interrupt chart rendering.\n\n## theming\n\nimport the stylesheet once to load prism's structural styles, default tokens, dark mode rules, and reduced motion css. you can override any css custom property on an ancestor of the chart.\n\n```css\n.dashboard chart {\n prism color 1: #2563eb;\n prism color 2: #16a34a;\n prism grid color: #94a3b8;\n prism line width: 3;\n prism text color: #172033;\n prism tooltip bg: #111827;\n}\n```\n\n`settheme()` provides a typed subset of theme properties. a scoped call writes inline values on the provided element; omit `scope` to write them to `document.documentelement`. inline values override stylesheet tokens until reset.\n\n```ts\nimport { resettheme, settheme } from '@vielzeug/prism';\n\nconst container = document.createelement('div');\ncontainer.style.csstext = 'width:640px;height:320px';\ndocument.body.append(container);\n\nsettheme({ colors: ['#2563eb', '#16a34a'], fontfamily: 'inter, sans serif' }, { scope: container });\n// call resettheme({ scope: container }) when this scoped theme is no longer needed.\nresettheme({ scope: container });\n```\n\n### available tokens\n\nthe stylesheet defines defaults for the following token groups. pie label color and size are read with fallback values by the renderer but are not declared in the base token block. set a token on the chart container or an ancestor to scope an override.\n\n| group | css custom properties |\n| | |\n| palette and surfaces | ` prism color 1` through ` prism color 8`, ` prism bg`, ` prism border radius` |\n| axes and grid | ` prism axis color`, ` prism axis width`, ` prism grid color`, ` prism grid width`, ` prism grid dash`, ` prism grid opacity` |\n| text | ` prism font family`, ` prism font size`, ` prism font size label`, ` prism font size title`, ` prism font weight`, ` prism font weight title`, ` prism text color`, ` prism text color secondary` |\n| tooltip | ` prism tooltip bg`, ` prism tooltip color`, ` prism tooltip border`, ` prism tooltip radius`, ` prism tooltip padding`, ` prism tooltip shadow`, ` prism tooltip font size` |\n| crosshair | ` prism crosshair color`, ` prism crosshair width`, ` prism crosshair dash`, ` prism crosshair opacity` |\n| points | ` prism point radius`, ` prism point radius hover`, ` prism point stroke width`, ` prism point stroke` |\n| bars | ` prism bar radius`, ` prism bar hover opacity`, ` prism bar gap`, ` prism bar band fill`, ` prism bar band opacity`, ` prism bar dim opacity` |\n| areas and lines | ` prism area opacity`, ` prism area stroke width`, ` prism area gradient start`, ` prism area gradient end`, ` prism line width`, ` prism line width hover` |\n| interaction | ` prism dim opacity`, ` prism active point radius`, ` prism point ring`, ` prism focus color` |\n| sparklines | ` prism spark fill opacity`, ` prism spark gradient start`, ` prism spark gradient end` |\n| radar | ` prism radar grid color`, ` prism radar grid opacity`, ` prism radar band fill`, ` prism radar band opacity`, ` prism radar spoke color`, ` prism radar fill opacity`, ` prism radar stroke width`, ` prism radar point radius`, ` prism radar point radius active`, ` prism radar label color`, ` prism radar label size`, ` prism radar value color`, ` prism radar value size`, ` prism radar dim opacity` |\n| motion | ` prism duration fast`, ` prism duration`, ` prism duration slow`, ` prism ease`, ` prism ease spring`, ` prism transition`, ` prism transition fast` |\n| legend and pie labels | ` prism legend gap`, ` prism legend dot size`, ` prism legend font size`, ` prism pie label color`, ` prism pie label size` |\n\nthe default palette and structural colors adapt to `prefers color scheme: dark`, `html.dark`, or `[data prism theme='dark']` on an ancestor. scoped inline values set with `settheme()` take precedence in either mode. the stylesheet also provides `prefers reduced motion` overrides for css transitions.\n\n## errors and diagnostics\n\nprism throws `prismrendererror` when the container is not a dom element, when initial rendering fails, or when `update()` is called on a disposed chart. a wrapped initial render error exposes its cause. use `prismerror` to catch all prism originated errors.\n\nempty or malformed data is not always an exception. depending on the chart, prism may draw nothing or emit a development only `console.warn`; examples include an unknown `bandscale` category, negative values in stacked bars, and radar axes with fewer than three entries or duplicate keys. correct the input rather than relying on a warning for validation.\n\n## framework integration\n\nprism has no react, vue, or svelte adapter. in any framework, give prism a host element after it is mounted, call `update()` when data changes, and call `dispose()` before the host is removed. this plain dom pattern shows the ownership boundary without depending on a framework specific lifecycle api:\n\n```ts\nimport { createlinechart, type lineseriesconfig } from '@vielzeug/prism';\n\nfunction mountchart(host: htmlelement, series: lineseriesconfig[]) {\n const chart = createlinechart(host, { a11y: { arialabel: 'revenue' }, series });\n\n return {\n update(next: lineseriesconfig[]) {\n chart.update(next);\n },\n dispose() {\n chart.dispose();\n },\n };\n}\n\nconst host = document.createelement('div');\nhost.style.csstext = 'width:640px;height:320px';\ndocument.body.append(host);\nconst view = mountchart(host, [{ data: [{ key: 1, value: 10 }], name: 'revenue' }]);\nview.update([{ data: [{ key: 1, value: 10 }, { key: 2, value: 15 }], name: 'revenue' }]);\nview.dispose();\n```\n\nthe application owns its state and host; prism owns only the svg, observer, and optional overlay elements it creates.\n\n## working with other vielzeug libraries\n\nuse refine components for dashboard controls and keep prism in a sized host element. map refine theme tokens to prism css custom properties or call `settheme()` with a scoped element; prism does not require a refine adapter. prism uses orbit internally to place tooltips, so chart users do not need to create orbit floating elements for the built in tooltip.\n\n## best practices\n\n give the host a non zero size before creating a chart.\n set `a11y.arialabel` when a chart communicates information.\n use `date` or number keys consistently across each line or area chart.\n pass the complete new data value to `update()`.\n keep chart options fixed after creation; recreate the chart to change them.\n dispose the handle when its host leaves the owning view.\n use `transition: false` for deterministic synchronous rendering.\n prefer css custom properties for reusable theme overrides.\n",
|
|
944
965
|
"examples": " \ntitle: 'prism: examples'\ndescription: focused recipes for creating, updating, interacting with, and theming prism charts.\n \n\n [create a line chart](./examples/line chart.md)\n [plot a time series](./examples/time series.md)\n [plot a scatter chart](./examples/scatter chart.md)\n [compare categories with a bar chart](./examples/bar chart.md)\n [render compact charts](./examples/compact charts.md)\n [compare hero stats with a radar chart](./examples/radar chart.md)\n [update chart data](./examples/update data.md)\n [sync a details panel with hover](./examples/hover details.md)\n [render a custom tooltip](./examples/custom tooltip.md)\n [match charts to your design system](./examples/theming.md)\n"
|
|
945
966
|
},
|
|
946
967
|
"examples": [],
|
|
@@ -949,7 +970,7 @@
|
|
|
949
970
|
"name": "@vielzeug/prism",
|
|
950
971
|
"related": "refine orbit",
|
|
951
972
|
"slug": "prism",
|
|
952
|
-
"source": "// public api: all exports for @vielzeug/prism\n\nexport type { easingfn, easingname } from './animation/easing';\n// chart factories\nexport { createareachart } from './charts/area';\nexport { createbarchart } from './charts/bar';\nexport { createlinechart } from './charts/line';\nexport { createpiechart } from './charts/pie';\nexport { createradarchart } from './charts/radar';\nexport { createsparkline } from './charts/sparkline';\n// error classes\nexport { prismerror, prismrendererror } from './errors';\n// scale factories\nexport { bandscale } from './scales/band';\nexport { linearscale } from './scales/linear';\nexport { timescale } from './scales/time';\nexport type { bandscaleconfig, linearscaleconfig, timescaleconfig } from './scales/types';\n// theme utilities\nexport { resettheme, seriescolor, settheme } from './theme';\nexport type {\n areachartconfig,\n areaseriesconfig,\n axisconfig,\n axisposition,\n bandscale,\n barchartconfig,\n barseriesconfig,\n barvariant,\n basechartconfig,\n charta11y,\n chartdimensions,\n chartevent,\n charthandle,\n chartmargin,\n continuousdatum,\n crosshairconfig,\n datum,\n gridconfig,\n horizontalaxisposition,\n legendconfig,\n legendposition,\n linechartconfig,\n lineseriesconfig,\n piechartconfig,\n pieevent,\n piesliceconfig,\n pievariant,\n prismevent,\n prismtheme,\n radaraxisconfig,\n radaraxisvalue,\n radarchartconfig,\n radarevent,\n radargridconfig,\n radarseriesconfig,\n scale,\n series,\n seriesvalue,\n sparklineconfig,\n sparklineevent,\n sparklinevariant,\n stacksegment,\n themescope,\n tooltipconfig,\n transitionconfig,\n transitionoption,\n verticalaxisposition,\n xaxisconfig,\n yaxisconfig,\n} from './types';\n"
|
|
973
|
+
"source": "// public api: all exports for @vielzeug/prism\n\nexport type { easingfn, easingname } from './animation/easing';\n// chart factories\nexport { createareachart } from './charts/area';\nexport { createbarchart } from './charts/bar';\nexport { createlinechart } from './charts/line';\nexport { createpiechart } from './charts/pie';\nexport { createradarchart } from './charts/radar';\nexport { createscatterchart } from './charts/scatter';\nexport { createsparkline } from './charts/sparkline';\n// error classes\nexport { prismerror, prismrendererror } from './errors';\n// scale factories\nexport { bandscale } from './scales/band';\nexport { linearscale } from './scales/linear';\nexport { timescale } from './scales/time';\nexport type { bandscaleconfig, linearscaleconfig, timescaleconfig } from './scales/types';\n// theme utilities\nexport { resettheme, seriescolor, settheme } from './theme';\nexport type {\n areachartconfig,\n areaseriesconfig,\n axisconfig,\n axisposition,\n bandscale,\n barchartconfig,\n barseriesconfig,\n barvariant,\n basechartconfig,\n charta11y,\n chartdimensions,\n chartevent,\n charthandle,\n chartmargin,\n continuousdatum,\n crosshairconfig,\n datum,\n gridconfig,\n horizontalaxisposition,\n legendconfig,\n legendposition,\n linechartconfig,\n lineseriesconfig,\n piechartconfig,\n pieevent,\n piesliceconfig,\n pievariant,\n prismevent,\n prismtheme,\n radaraxisconfig,\n radaraxisvalue,\n radarchartconfig,\n radarevent,\n radargridconfig,\n radarseriesconfig,\n scale,\n scatterchartconfig,\n scatterseriesconfig,\n series,\n seriesvalue,\n sparklineconfig,\n sparklineevent,\n sparklinevariant,\n stacksegment,\n themescope,\n tooltipconfig,\n transitionconfig,\n transitionoption,\n verticalaxisposition,\n xaxisconfig,\n yaxisconfig,\n} from './types';\n"
|
|
953
974
|
},
|
|
954
975
|
{
|
|
955
976
|
"category": "websockets",
|
|
@@ -993,12 +1014,12 @@
|
|
|
993
1014
|
"category": "ui components",
|
|
994
1015
|
"description": "accessible, themeable web components built with ore for framework and vanilla dom apps.",
|
|
995
1016
|
"docs": {
|
|
996
|
-
"index": " \ntitle: 'refine: web component library'\ndescription: accessible, themeable web components built with ore for framework and vanilla dom apps.\npackage: refine\ncategory: ui components\nkeywords: [web components, accessible, themeable, ui, components, design system]\nrelated: [ore, orbit, forge, keymap]\nexports:\n [\n ore accordion,\n ore accordion item,\n ore alert,\n ore async,\n ore avatar,\n ore avatar group,\n ore badge,\n ore box,\n ore breadcrumb,\n ore breadcrumb item,\n ore button,\n ore button group,\n ore calendar,\n ore card,\n ore carousel,\n ore carousel slide,\n ore chat message,\n ore checkbox,\n ore checkbox group,\n ore chip,\n ore code window,\n ore column,\n ore combobox,\n ore combobox option,\n ore command palette,\n ore command palette item,\n ore cookie banner,\n ore copy command,\n ore counter,\n ore datagrid,\n ore date picker,\n ore dialog,\n ore drawer,\n ore file input,\n ore grid,\n ore grid item,\n ore icon,\n ore input,\n ore keyboard key,\n ore keyboard shortcut,\n ore list,\n ore list item,\n ore marquee,\n ore menu,\n ore menu item,\n ore menu separator,\n ore message composer,\n ore navbar,\n ore navbar item,\n ore navigation menu,\n ore navigation menu item,\n ore navigation menu panel,\n ore number input,\n ore otp input,\n ore pagination,\n ore password strength,\n ore popover,\n ore progress,\n ore qr code,\n ore qr scanner,\n ore radio,\n ore radio group,\n ore rating,\n ore select,\n ore separator,\n ore sidebar,\n ore sidebar group,\n ore sidebar item,\n ore skeleton,\n ore slider,\n ore speech player,\n ore stats,\n ore step,\n ore stepper,\n ore switch,\n ore tab item,\n ore tab panel,\n ore table,\n ore tabs,\n ore td,\n ore text,\n ore th,\n ore tr,\n ore textarea,\n ore time picker,\n ore toast,\n ore tooltip,\n ore typing indicator,\n ]\nenvironments: [browser]\n \n\n<! markdownlint disable md025 md033 md060 >\n\n<packagehero package=\"refine\" />\n\n## why refine?\n\nevery project needs ui primitives. refine provides accessible web components that work natively anywhere html is rendered, no framework required.\n\n```html\n<! before: roll your own button with aria >\n<button class=\"btn btn primary\" role=\"button\" aria pressed=\"false\" tabindex=\"0\">\n <span class=\"btn spinner\" aria hidden=\"true\"></span>\n save\n</button>\n\n<! after: refine >\n<ore button variant=\"solid\" color=\"primary\" loading>save</ore button>\n```\n\n| feature | refine | shoelace | material web |\n| | | | |\n| bundle size | <packageinfo package=\"refine\" type=\"size\" /> | ~145 kb | ~200 kb |\n| built with | ore | lit | lit |\n| runtime dependencies | vielzeug ui primitives + lucide | lit + utilities | lit + material foundations |\n| accessible | wcag aa | wcag aa | wcag aa |\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\n<div class=\"decision callout\">\n\n**use refine when** you want accessible web components that match the vielzeug design system without a heavy framework dependency.\n\n**consider shoelace or material web** if your team is already standardized on those ecosystems and you need their established component catalogs.\n\n</div>\n\n## installation\n\n::: code group\n\n```sh [pnpm]\npnpm add @vielzeug/refine\n```\n\n```sh [npm]\nnpm install @vielzeug/refine\n```\n\n```sh [yarn]\nyarn add @vielzeug/refine\n```\n\n:::\n\n## quick start\n\n```ts\n// 1. import global styles once\nimport '@vielzeug/refine/fouc.css'; // hide unupgraded custom elements until first paint\nimport '@vielzeug/refine/tokens.css'; // tokens, animations, cascade layers\n\n// 2. register only the elements you need\nimport '@vielzeug/refine/button';\nimport '@vielzeug/refine/input';\nimport '@vielzeug/refine/card';\n```\n\n```html\n<ore button variant=\"solid\" color=\"primary\">save</ore button>\n<ore input label=\"email\" type=\"email\" required></ore input>\n<ore card padding=\"lg\">\n <span slot=\"header\">account</span>\n <p>card content goes here.</p>\n</ore card>\n```\n\n```ts\n```\n\n### cdn / vanilla html\n\nuse the self contained iife bundle to load refine directly from a cdn in any html page: no build step required:\n\n```html\n<! 1. styles >\n<link rel=\"stylesheet\" href=\"https://unpkg.com/@vielzeug/refine/dist/styles/fouc.css\" />\n<link rel=\"stylesheet\" href=\"https://unpkg.com/@vielzeug/refine/dist/styles/tokens.css\" />\n\n<! 2. all components (iife: registers global refine namespace) >\n<script src=\"https://unpkg.com/@vielzeug/refine/dist/refine.iife.js\"></script>\n```\n\nfor bundler based projects that still want a cdn url, use the esm bundle via an import map:\n\n```html\n<script type=\"importmap\">\n {\n \"imports\": {\n \"@vielzeug/refine\": \"https://esm.sh/@vielzeug/refine\",\n \"@vielzeug/refine/button\": \"https://esm.sh/@vielzeug/refine/button\",\n \"@vielzeug/refine/input\": \"https://esm.sh/@vielzeug/refine/input\"\n }\n }\n</script>\n\n<script type=\"module\">\n import '@vielzeug/refine/button';\n import '@vielzeug/refine/input';\n</script>\n```\n\n### package entry points\n\n| import | purpose |\n| | |\n| `@vielzeug/refine/fouc.css` | fouc suppression for unupgraded custom elements |\n| `@vielzeug/refine/tokens.css` | global design tokens and cascade layers |\n| `@vielzeug/refine/styles/preflight.css` | optional browser default reset (includes fouc suppression) |\n\ncomponent registration happens through side effect imports such as `@vielzeug/refine/button` and `@vielzeug/refine/dialog`.\n\n### components\n\n**content:** `ore avatar`, `ore avatar group`, `ore breadcrumb`, `ore card`, `ore carousel`, `ore carousel slide`, `ore chat message`, `ore icon`, `ore keyboard key`, `ore keyboard shortcut`, `ore list`, `ore list item`, `ore marquee`, `ore pagination`, `ore qr code`, `ore separator`, `ore stats`, `ore step`, `ore stepper`, `ore table`, `ore text`\n\n**disclosure:** `ore accordion`, `ore accordion item`, `ore tabs`, `ore tab item`, `ore tab panel`\n\n**feedback:** `ore alert`, `ore async`, `ore badge`, `ore chip`, `ore cookie banner`, `ore password strength`, `ore progress`, `ore skeleton`, `ore toast`, `ore typing indicator`\n\n**inputs:** `ore button`, `ore button group`, `ore calendar`, `ore checkbox`, `ore checkbox group`, `ore column`, `ore combobox`, `ore counter`, `ore datagrid`, `ore date picker`, `ore file input`, `ore input`, `ore message composer`, `ore number input`, `ore otp input`, `ore qr scanner`, `ore radio`, `ore radio group`, `ore rating`, `ore select`, `ore slider`, `ore switch`, `ore textarea`, `ore time picker`\n\n**layout:** `ore box`, `ore grid`, `ore grid item`, `ore navbar`, `ore sidebar`\n\n**overlay:** `ore command palette`, `ore command palette item`, `ore dialog`, `ore drawer`, `ore menu`, `ore popover`, `ore tooltip`\n\n## component portfolio\n\nrefine keeps one public package and classifies components by composition level rather than stability. all listed components remain supported and tested.\n\n **foundations**: buttons, inputs, selection controls, disclosure, dialogs, menus, tooltips, layout primitives, and feedback indicators. start here when building an application specific interface.\n **composed patterns**: data grids, command palettes, navigation menus, steppers, calendars, date/time pickers, file inputs, and message composers. these package recurring accessibility and interaction behavior that is costly to rebuild correctly.\n **presentation patterns**: cards, chat messages, code windows, stats, marquees, avatars, badges, and skeletons. use these when their product vocabulary matches the application; prefer foundations when it does not.\n\nclassification guides discovery and bundle selection. it is not a reason to remove working components or move them into unstable packages; per component subpath imports remain the tree shaking boundary.\n\n## features\n\n<div class=\"features grid\">\n\n **accessible**: keyboard navigation, aria wiring, and focus management across interactive components\n **themeable**: global tokens plus component level css custom properties\n **framework agnostic**: works anywhere html can be rendered\n **tree shakeable**: import only the component entry points you register\n **comprehensive surface**: inputs, content, disclosure, feedback, layout, and overlay primitives\n **typed framework declarations**: dom, react, and vue tags derive from the component property contracts\n\n</div>\n\n### prerequisites\n\n browser runtime with custom elements support.\n import `@vielzeug/refine/fouc.css` and `@vielzeug/refine/tokens.css` before rendering components.\n for ssr, render placeholders server side and hydrate components only on the client.\n\n## documentation\n\n<div class=\"doc links\">\n\n [usage guide](./usage.md)\n [api reference](./api.md)\n [migration guide](./migration.md)\n\n</div>\n\n## see also\n\n<div class=\"see also\">\n\n [ore](/ore/): web component runtime that powers refine\n [orbit](/orbit/): floating ui positioning used in refine's overlays\n [forge](/forge/): form state management for use with refine inputs\n [keymap](/keymap/): keyboard shortcut manager that powers the command palette's global trigger\n\n</div>\n\n<! markdownlint enable md025 md033 md060 >\n",
|
|
997
|
-
"api": " \ntitle: 'refine: api reference'\ndescription: published component, stylesheet, framework type, and error entry points for @vielzeug/refine.\n \n\n[[toc]]\n\n## api overview\n\n| symbol | purpose | execution mode | common gotcha |\n| | | | |\n| `@vielzeug/refine/<component>` | register one custom element and export its public types | sync | importing a type alone does not register the element |\n| `@vielzeug/refine/tokens.css` | load required theme tokens, animations, and cascade layers | css | import once before components render |\n| `@vielzeug/refine/fouc.css` | hide unregistered `ore *` elements until upgrade | css | load before first paint |\n| `@vielzeug/refine/styles/preflight.css` | apply the optional browser reset | css | the reset affects global elements |\n| `@vielzeug/refine/frameworks/elements` | register typed dom tag mappings | types only | import for side effects in typescript |\n| `@vielzeug/refine/frameworks/react` | register typed react jsx elements | types only | does not provide runtime wrappers |\n| `@vielzeug/refine/frameworks/vue` | register typed vue global components | types only | does not install a vue plugin |\n| `@vielzeug/refine/theme` | `createthemecontroller()`: light/dark/system document theme | sync | does not persist the preference or set an accent color |\n| `eventfieldvalue()` / `eventfieldchecked()` | read a form control's value/checked from `event.currenttarget` | sync | exported from the package root; guards non field targets |\n| `refineerror` | base class for package defined public errors | sync | component configuration warnings do not throw this error |\n\n## package entry point\n\n| import | purpose |\n| | |\n| `@vielzeug/refine` | export `refineerror` and the `eventfieldvalue`/`eventfieldchecked` field readers without registering components |\n| `@vielzeug/refine/<component>` | register one component and export its tag constant, props, events, and related types |\n| `@vielzeug/refine/theme` | `createthemecontroller()` and its theme types |\n| `@vielzeug/refine/tokens.css` | required global design contract |\n| `@vielzeug/refine/fouc.css` | optional pre upgrade visibility rule |\n| `@vielzeug/refine/styles/*` | focused theme, animation, layer, and preflight stylesheets |\n| `@vielzeug/refine/frameworks/*` | typescript augmentations for dom, react, and vue |\n\n## styles\n\n```ts\nimport '@vielzeug/refine/fouc.css';\nimport '@vielzeug/refine/tokens.css';\nimport '@vielzeug/refine/styles/preflight.css';\n```\n\n`tokens.css` is required. it defines tokens, animations, and cascade layer order without resetting global elements. `preflight.css` is optional and includes fouc suppression.\n\n| import path | purpose |\n| | |\n| `@vielzeug/refine/fouc.css` | fouc suppression for unupgraded custom elements |\n| `@vielzeug/refine/tokens.css` | tokens, animation helpers, and cascade layers |\n| `@vielzeug/refine/styles/theme.css` | theme token declarations |\n| `@vielzeug/refine/styles/animation.css` | animation helpers |\n| `@vielzeug/refine/styles/layers.css` | cascade layer declarations |\n| `@vielzeug/refine/styles/preflight.css` | optional browser reset and fouc suppression |\n\n## components\n\neach component uses one registration and type entry point:\n\n```ts\nimport '@vielzeug/refine/button';\nimport type { orebuttonprops } from '@vielzeug/refine/button';\n```\n\n| area | components |\n| | |\n| content | `avatar`, `avatar group`, `breadcrumb`, `card`, `carousel`, `chat message`, `code window`, `copy command`, `icon`, `keyboard key`, `list`, `list item`, `marquee`, `pagination`, `separator`, `speech player`, `stats`, `step`, `stepper`, `table`, `text` |\n| disclosure | `accordion`, `accordion item`, `tabs`, `tab item`, `tab panel` |\n| feedback | `alert`, `async`, `badge`, `chip`, `cookie banner`, `password strength`, `progress`, `skeleton`, `toast`, `typing indicator` |\n| inputs | `button`, `button group`, `calendar`, `checkbox`, `checkbox group`, `combobox`, `counter`, `datagrid`, `date picker`, `file input`, `input`, `message composer`, `number input`, `otp input`, `radio`, `radio group`, `rating`, `select`, `slider`, `switch`, `textarea`, `time picker` |\n| layout | `box`, `grid`, `grid item`, `navbar`, `sidebar` |\n| overlays | `command palette`, `dialog`, `drawer`, `menu`, `navigation menu`, `popover`, `tooltip` |\n\neach component page lists its attributes, javascript properties, events, slots, parts, and css custom properties.\n\n## framework types\n\n```ts\nimport type {} from '@vielzeug/refine/frameworks/elements';\nimport type {} from '@vielzeug/refine/frameworks/react';\nimport type {} from '@vielzeug/refine/frameworks/vue';\n```\n\nthe declarations cover every supported tag and derive component properties from refine's authoritative `htmlelementtagnamemap`. they provide types only; component registration still uses component subpaths.\n\n## events and form controls\n\nform controls expose `.value` or `.checked` and dispatch standard `input` and `change` events. read the property from `event.currenttarget`.\n\nstateful overlays expose controlled `open`, optional `default open`, and an `open change` custom event with `{ open, reason }` detail. component pages define additional detail fields and reasons.\n\n## utilities\n\n### `createthemecontroller(options?)`\n\n```ts\nimport { createthemecontroller } from '@vielzeug/refine/theme';\n\nconst theme = createthemecontroller(); // starts in 'system' mode\ntheme.setpreference('dark');\ntheme.dispose();\n```\n\nowns the light/dark/system theme for the document: a reactive `preference` signal, the effective `resolved` mode (`'dark' | 'light'` after resolving `system` against the os), and the dom application `styles/theme.css` expects: the `.dark` class plus the `color scheme` property on the root element, so every `light dark()` token resolves to the right branch. while in `system` mode the controller tracks the os `prefers color scheme` via sentinel's `createmediaquery`.\n\nthe controller does **not** persist the preference or expose an accent color: those are consumer concerns (wire `watch(controller.preference, save)` for persistence; set ` color primary hue` yourself). options: `initial` (starting preference, default `'system'`), `root` (element carrying the classes, default `document.documentelement`), `target` (window whose `matchmedia` backs `system`). the handle is disposable: `dispose()`, `disposed`, `disposalsignal`, `[symbol.dispose]`.\n\n### `eventfieldvalue(event)` / `eventfieldchecked(event)`\n\n```ts\nimport { eventfieldchecked, eventfieldvalue } from '@vielzeug/refine';\n\ninput.addeventlistener('input', (event) => {\n const value = eventfieldvalue(event); // string | undefined: never a blind cast\n});\n```\n\nguarded readers for the standard `input`/`change` event pattern above: they read `value`/`checked` from `event.currenttarget` only when the target actually carries it, so a handler wired to the wrong element yields `undefined`/`false` instead of a crash or a lie. exported from the package root, which registers no components.\n\n## types\n\ncomponent props follow the `ore<component>props` naming pattern. event maps follow `ore<component>events`. import both from the component subpath that owns them.\n\n`refineelementmap`, `refinereactintrinsicelements`, and `refinevueglobalcomponents` are exported from their respective `frameworks/*` type entry points.\n\n## errors\n\n### `refineerror`\n\n```ts\nclass refineerror extends error {\n constructor(message: string, opts?: erroroptions);\n}\n```\n\nbase class for public refine errors. it preserves `cause` through `erroroptions`.\n",
|
|
1017
|
+
"index": " \ntitle: 'refine: web component library'\ndescription: accessible, themeable web components built with ore for framework and vanilla dom apps.\npackage: refine\ncategory: ui components\nkeywords: [web components, accessible, themeable, ui, components, design system]\nrelated: [ore, orbit, forge, keymap]\nexports:\n [\n ore accordion,\n ore accordion item,\n ore alert,\n ore async,\n ore avatar,\n ore avatar group,\n ore badge,\n ore box,\n ore breadcrumb,\n ore breadcrumb item,\n ore button,\n ore button group,\n ore calendar,\n ore card,\n ore carousel,\n ore carousel slide,\n ore chat message,\n ore checkbox,\n ore checkbox group,\n ore chip,\n ore code window,\n ore column,\n ore combobox,\n ore combobox option,\n ore command palette,\n ore command palette item,\n ore cookie banner,\n ore copy command,\n ore counter,\n ore datagrid,\n ore date picker,\n ore dialog,\n ore drawer,\n ore file input,\n ore grid,\n ore grid item,\n ore icon,\n ore input,\n ore keyboard key,\n ore keyboard shortcut,\n ore list,\n ore list item,\n ore marquee,\n ore menu,\n ore menu item,\n ore menu separator,\n ore message composer,\n ore navbar,\n ore navbar item,\n ore navigation menu,\n ore navigation menu item,\n ore navigation menu panel,\n ore number input,\n ore otp input,\n ore pagination,\n ore password strength,\n ore popover,\n ore progress,\n ore qr code,\n ore qr scanner,\n ore query bar,\n ore radio,\n ore radio group,\n ore rating,\n ore select,\n ore separator,\n ore sidebar,\n ore sidebar group,\n ore sidebar item,\n ore skeleton,\n ore slider,\n ore speech player,\n ore stats,\n ore step,\n ore stepper,\n ore switch,\n ore tab item,\n ore tab panel,\n ore table,\n ore tabs,\n ore td,\n ore text,\n ore th,\n ore tr,\n ore textarea,\n ore time picker,\n ore toast,\n ore tooltip,\n ore typing indicator,\n ]\nenvironments: [browser]\n \n\n<! markdownlint disable md025 md033 md060 >\n\n<packagehero package=\"refine\" />\n\n## why refine?\n\nevery project needs ui primitives. refine provides accessible web components that work natively anywhere html is rendered, no framework required.\n\n```html\n<! before: roll your own button with aria >\n<button class=\"btn btn primary\" role=\"button\" aria pressed=\"false\" tabindex=\"0\">\n <span class=\"btn spinner\" aria hidden=\"true\"></span>\n save\n</button>\n\n<! after: refine >\n<ore button variant=\"solid\" color=\"primary\" loading>save</ore button>\n```\n\n| feature | refine | shoelace | material web |\n| | | | |\n| bundle size | <packageinfo package=\"refine\" type=\"size\" /> | ~145 kb | ~200 kb |\n| built with | ore | lit | lit |\n| runtime dependencies | vielzeug ui primitives + lucide | lit + utilities | lit + material foundations |\n| accessible | wcag aa | wcag aa | wcag aa |\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\n<div class=\"decision callout\">\n\n**use refine when** you want accessible web components that match the vielzeug design system without a heavy framework dependency.\n\n**consider shoelace or material web** if your team is already standardized on those ecosystems and you need their established component catalogs.\n\n</div>\n\n## installation\n\n::: code group\n\n```sh [pnpm]\npnpm add @vielzeug/refine\n```\n\n```sh [npm]\nnpm install @vielzeug/refine\n```\n\n```sh [yarn]\nyarn add @vielzeug/refine\n```\n\n:::\n\n## quick start\n\n```ts\n// 1. import global styles once\nimport '@vielzeug/refine/fouc.css'; // hide unupgraded custom elements until first paint\nimport '@vielzeug/refine/tokens.css'; // tokens, animations, cascade layers\n\n// 2. register only the elements you need\nimport '@vielzeug/refine/button';\nimport '@vielzeug/refine/input';\nimport '@vielzeug/refine/card';\n```\n\n```html\n<ore button variant=\"solid\" color=\"primary\">save</ore button>\n<ore input label=\"email\" type=\"email\" required></ore input>\n<ore card padding=\"lg\">\n <span slot=\"header\">account</span>\n <p>card content goes here.</p>\n</ore card>\n```\n\n```ts\n```\n\n### cdn / vanilla html\n\nuse the self contained iife bundle to load refine directly from a cdn in any html page: no build step required:\n\n```html\n<! 1. styles >\n<link rel=\"stylesheet\" href=\"https://unpkg.com/@vielzeug/refine/dist/styles/fouc.css\" />\n<link rel=\"stylesheet\" href=\"https://unpkg.com/@vielzeug/refine/dist/styles/tokens.css\" />\n\n<! 2. all components (iife: registers global refine namespace) >\n<script src=\"https://unpkg.com/@vielzeug/refine/dist/refine.iife.js\"></script>\n```\n\nfor bundler based projects that still want a cdn url, use the esm bundle via an import map:\n\n```html\n<script type=\"importmap\">\n {\n \"imports\": {\n \"@vielzeug/refine\": \"https://esm.sh/@vielzeug/refine\",\n \"@vielzeug/refine/button\": \"https://esm.sh/@vielzeug/refine/button\",\n \"@vielzeug/refine/input\": \"https://esm.sh/@vielzeug/refine/input\"\n }\n }\n</script>\n\n<script type=\"module\">\n import '@vielzeug/refine/button';\n import '@vielzeug/refine/input';\n</script>\n```\n\n### package entry points\n\n| import | purpose |\n| | |\n| `@vielzeug/refine/fouc.css` | fouc suppression for unupgraded custom elements |\n| `@vielzeug/refine/tokens.css` | global design tokens and cascade layers |\n| `@vielzeug/refine/styles/preflight.css` | optional browser default reset (includes fouc suppression) |\n\ncomponent registration happens through side effect imports such as `@vielzeug/refine/button` and `@vielzeug/refine/dialog`.\n\n### components\n\n**content:** `ore avatar`, `ore avatar group`, `ore breadcrumb`, `ore card`, `ore carousel`, `ore carousel slide`, `ore chat message`, `ore icon`, `ore keyboard key`, `ore keyboard shortcut`, `ore list`, `ore list item`, `ore marquee`, `ore pagination`, `ore qr code`, `ore separator`, `ore stats`, `ore step`, `ore stepper`, `ore table`, `ore text`\n\n**disclosure:** `ore accordion`, `ore accordion item`, `ore tabs`, `ore tab item`, `ore tab panel`\n\n**feedback:** `ore alert`, `ore async`, `ore badge`, `ore chip`, `ore cookie banner`, `ore password strength`, `ore progress`, `ore skeleton`, `ore toast`, `ore typing indicator`\n\n**inputs:** `ore button`, `ore button group`, `ore calendar`, `ore checkbox`, `ore checkbox group`, `ore column`, `ore combobox`, `ore counter`, `ore datagrid`, `ore date picker`, `ore file input`, `ore input`, `ore message composer`, `ore number input`, `ore otp input`, `ore qr scanner`, `ore query bar`, `ore radio`, `ore radio group`, `ore rating`, `ore select`, `ore slider`, `ore switch`, `ore textarea`, `ore time picker`\n\n**layout:** `ore box`, `ore grid`, `ore grid item`, `ore navbar`, `ore sidebar`\n\n**overlay:** `ore command palette`, `ore command palette item`, `ore dialog`, `ore drawer`, `ore menu`, `ore popover`, `ore tooltip`\n\n## component portfolio\n\nrefine keeps one public package and classifies components by composition level rather than stability. all listed components remain supported and tested.\n\n **foundations**: buttons, inputs, selection controls, disclosure, dialogs, menus, tooltips, layout primitives, and feedback indicators. start here when building an application specific interface.\n **composed patterns**: data grids, command palettes, navigation menus, steppers, calendars, date/time pickers, file inputs, and message composers. these package recurring accessibility and interaction behavior that is costly to rebuild correctly.\n **presentation patterns**: cards, chat messages, code windows, stats, marquees, avatars, badges, and skeletons. use these when their product vocabulary matches the application; prefer foundations when it does not.\n\nclassification guides discovery and bundle selection. it is not a reason to remove working components or move them into unstable packages; per component subpath imports remain the tree shaking boundary.\n\n## features\n\n<div class=\"features grid\">\n\n **accessible**: keyboard navigation, aria wiring, and focus management across interactive components\n **themeable**: global tokens plus component level css custom properties\n **framework agnostic**: works anywhere html can be rendered\n **tree shakeable**: import only the component entry points you register\n **comprehensive surface**: inputs, content, disclosure, feedback, layout, and overlay primitives\n **typed framework declarations**: dom, react, and vue tags derive from the component property contracts\n\n</div>\n\n### prerequisites\n\n browser runtime with custom elements support.\n import `@vielzeug/refine/fouc.css` and `@vielzeug/refine/tokens.css` before rendering components.\n for ssr, render placeholders server side and hydrate components only on the client.\n\n## documentation\n\n<div class=\"doc links\">\n\n [usage guide](./usage.md)\n [api reference](./api.md)\n [migration guide](./migration.md)\n\n</div>\n\n## see also\n\n<div class=\"see also\">\n\n [ore](/ore/): web component runtime that powers refine\n [orbit](/orbit/): floating ui positioning used in refine's overlays\n [forge](/forge/): form state management for use with refine inputs\n [keymap](/keymap/): keyboard shortcut manager that powers the command palette's global trigger\n\n</div>\n\n<! markdownlint enable md025 md033 md060 >\n",
|
|
1018
|
+
"api": " \ntitle: 'refine: api reference'\ndescription: published component, stylesheet, framework type, and error entry points for @vielzeug/refine.\n \n\n[[toc]]\n\n## api overview\n\n| symbol | purpose | execution mode | common gotcha |\n| | | | |\n| `@vielzeug/refine/<component>` | register one custom element and export its public types | sync | importing a type alone does not register the element |\n| `@vielzeug/refine/tokens.css` | load required theme tokens, animations, and cascade layers | css | import once before components render |\n| `@vielzeug/refine/fouc.css` | hide unregistered `ore *` elements until upgrade | css | load before first paint |\n| `@vielzeug/refine/styles/preflight.css` | apply the optional browser reset | css | the reset affects global elements |\n| `@vielzeug/refine/frameworks/elements` | register typed dom tag mappings | types only | import for side effects in typescript |\n| `@vielzeug/refine/frameworks/react` | register typed react jsx elements | types only | does not provide runtime wrappers |\n| `@vielzeug/refine/frameworks/vue` | register typed vue global components | types only | does not install a vue plugin |\n| `@vielzeug/refine/theme` | `createthemecontroller()`: light/dark/system document theme | sync | does not persist the preference or set an accent color |\n| `eventfieldvalue()` / `eventfieldchecked()` | read a form control's value/checked from `event.currenttarget` | sync | exported from the package root; guards non field targets |\n| `refineerror` | base class for package defined public errors | sync | component configuration warnings do not throw this error |\n\n## package entry point\n\n| import | purpose |\n| | |\n| `@vielzeug/refine` | export `refineerror` and the `eventfieldvalue`/`eventfieldchecked` field readers without registering components |\n| `@vielzeug/refine/<component>` | register one component and export its tag constant, props, events, and related types |\n| `@vielzeug/refine/theme` | `createthemecontroller()` and its theme types |\n| `@vielzeug/refine/tokens.css` | required global design contract |\n| `@vielzeug/refine/fouc.css` | optional pre upgrade visibility rule |\n| `@vielzeug/refine/styles/*` | focused theme, animation, layer, and preflight stylesheets |\n| `@vielzeug/refine/frameworks/*` | typescript augmentations for dom, react, and vue |\n\n## styles\n\n```ts\nimport '@vielzeug/refine/fouc.css';\nimport '@vielzeug/refine/tokens.css';\nimport '@vielzeug/refine/styles/preflight.css';\n```\n\n`tokens.css` is required. it defines tokens, animations, and cascade layer order without resetting global elements. `preflight.css` is optional and includes fouc suppression.\n\n| import path | purpose |\n| | |\n| `@vielzeug/refine/fouc.css` | fouc suppression for unupgraded custom elements |\n| `@vielzeug/refine/tokens.css` | tokens, animation helpers, and cascade layers |\n| `@vielzeug/refine/styles/theme.css` | theme token declarations |\n| `@vielzeug/refine/styles/animation.css` | animation helpers |\n| `@vielzeug/refine/styles/layers.css` | cascade layer declarations |\n| `@vielzeug/refine/styles/preflight.css` | optional browser reset and fouc suppression |\n\n## components\n\neach component uses one registration and type entry point:\n\n```ts\nimport '@vielzeug/refine/button';\nimport type { orebuttonprops } from '@vielzeug/refine/button';\n```\n\n| area | components |\n| | |\n| content | `avatar`, `avatar group`, `breadcrumb`, `card`, `carousel`, `chat message`, `code window`, `copy command`, `icon`, `keyboard key`, `list`, `list item`, `marquee`, `pagination`, `separator`, `speech player`, `stats`, `step`, `stepper`, `table`, `text` |\n| disclosure | `accordion`, `accordion item`, `tabs`, `tab item`, `tab panel` |\n| feedback | `alert`, `async`, `badge`, `chip`, `cookie banner`, `password strength`, `progress`, `skeleton`, `toast`, `typing indicator` |\n| inputs | `button`, `button group`, `calendar`, `checkbox`, `checkbox group`, `combobox`, `counter`, `datagrid`, `date picker`, `file input`, `input`, `message composer`, `number input`, `otp input`, `query bar`, `radio`, `radio group`, `rating`, `select`, `slider`, `switch`, `textarea`, `time picker` |\n| layout | `box`, `grid`, `grid item`, `navbar`, `sidebar` |\n| overlays | `command palette`, `dialog`, `drawer`, `menu`, `navigation menu`, `popover`, `tooltip` |\n\neach component page lists its attributes, javascript properties, events, slots, parts, and css custom properties.\n\n## framework types\n\n```ts\nimport type {} from '@vielzeug/refine/frameworks/elements';\nimport type {} from '@vielzeug/refine/frameworks/react';\nimport type {} from '@vielzeug/refine/frameworks/vue';\n```\n\nthe declarations cover every supported tag and derive component properties from refine's authoritative `htmlelementtagnamemap`. they provide types only; component registration still uses component subpaths.\n\n## events and form controls\n\nform controls expose `.value` or `.checked` and dispatch standard `input` and `change` events. read the property from `event.currenttarget`.\n\nstateful overlays expose controlled `open`, optional `default open`, and an `open change` custom event with `{ open, reason }` detail. component pages define additional detail fields and reasons.\n\n## utilities\n\n### `createthemecontroller(options?)`\n\n```ts\nimport { createthemecontroller } from '@vielzeug/refine/theme';\n\nconst theme = createthemecontroller(); // starts in 'system' mode\ntheme.setpreference('dark');\ntheme.dispose();\n```\n\nowns the light/dark/system theme for the document: a reactive `preference` signal, the effective `resolved` mode (`'dark' | 'light'` after resolving `system` against the os), and the dom application `styles/theme.css` expects: the `.dark` class plus the `color scheme` property on the root element, so every `light dark()` token resolves to the right branch. while in `system` mode the controller tracks the os `prefers color scheme` via sentinel's `createmediaquery`.\n\nthe controller does **not** persist the preference or expose an accent color: those are consumer concerns (wire `watch(controller.preference, save)` for persistence; set ` color primary hue` yourself). options: `initial` (starting preference, default `'system'`), `root` (element carrying the classes, default `document.documentelement`), `target` (window whose `matchmedia` backs `system`). the handle is disposable: `dispose()`, `disposed`, `disposalsignal`, `[symbol.dispose]`.\n\n### `eventfieldvalue(event)` / `eventfieldchecked(event)`\n\n```ts\nimport { eventfieldchecked, eventfieldvalue } from '@vielzeug/refine';\n\ninput.addeventlistener('input', (event) => {\n const value = eventfieldvalue(event); // string | undefined: never a blind cast\n});\n```\n\nguarded readers for the standard `input`/`change` event pattern above: they read `value`/`checked` from `event.currenttarget` only when the target actually carries it, so a handler wired to the wrong element yields `undefined`/`false` instead of a crash or a lie. exported from the package root, which registers no components.\n\n## types\n\ncomponent props follow the `ore<component>props` naming pattern. event maps follow `ore<component>events`. import both from the component subpath that owns them.\n\n`refineelementmap`, `refinereactintrinsicelements`, and `refinevueglobalcomponents` are exported from their respective `frameworks/*` type entry points.\n\n## errors\n\n### `refineerror`\n\n```ts\nclass refineerror extends error {\n constructor(message: string, opts?: erroroptions);\n}\n```\n\nbase class for public refine errors. it preserves `cause` through `erroroptions`.\n",
|
|
998
1019
|
"usage": " \ntitle: 'refine: usage guide'\ndescription: register refine components, configure properties, handle events, compose slots, and integrate frameworks.\n \n\n[[toc]]\n\n## basic usage\n\nload the global design contract once, then register only the components used by the page.\n\n```ts\nimport '@vielzeug/refine/tokens.css';\nimport '@vielzeug/refine/button';\nimport '@vielzeug/refine/input';\nimport '@vielzeug/refine/dialog';\n\nconst input = document.queryselector('ore input');\nconst save = document.queryselector('ore button');\n\nif (!(input instanceof htmlelement) || !(save instanceof htmlelement)) {\n throw new error('expected refine controls');\n}\n\nsave.addeventlistener('click', () => {\n console.log((input as htmlelement & { value: string }).value);\n});\n```\n\nadd the browser reset only when the application owns global element styling:\n\n```ts\nimport '@vielzeug/refine/styles/preflight.css';\n```\n\n## set attributes and properties\n\nuse attributes for serializable html state.\n\n```html\n<ore button variant=\"outline\" color=\"secondary\" size=\"lg\" disabled>\n save changes\n</ore button>\n```\n\nuse javascript properties for arrays, objects, callbacks, and controlled state.\n\n```ts\nimport '@vielzeug/refine/datagrid';\nimport type { oredatagridprops } from '@vielzeug/refine/datagrid';\n\nconst grid = document.queryselector('ore datagrid') as htmlelement & oredatagridprops;\ngrid.columns = [\n { key: 'name', label: 'name', sortable: true },\n { key: 'role', label: 'role' },\n];\ngrid.rows = [\n { id: '1', name: 'ada', role: 'admin' },\n { id: '2', name: 'linus', role: 'editor' },\n];\n```\n\n## handle events\n\nform controls dispatch standard `input` and `change` events. read their current property from `event.currenttarget`.\n\n```ts\nconst input = document.queryselector('ore input');\ninput?.addeventlistener('input', (event) => {\n const target = event.currenttarget as htmlelement & { value: string };\n console.log(target.value);\n});\n```\n\ncustom events use kebab case names and typed detail objects.\n\n```ts\nimport type { oredialogevents } from '@vielzeug/refine/dialog';\n\nconst dialog = document.queryselector('ore dialog');\ndialog?.addeventlistener('open change', (event: customevent<oredialogevents['open change']>) => {\n console.log(event.detail.open, event.detail.reason);\n});\n```\n\n## compose slots\n\ndefault slots hold primary content. named slots identify component owned regions.\n\n```html\n<ore card>\n <span slot=\"header\">account</span>\n <p>profile settings</p>\n <div slot=\"footer\">\n <ore button variant=\"ghost\">cancel</ore button>\n <ore button>save</ore button>\n </div>\n</ore card>\n```\n\ninput components commonly expose `prefix`, `suffix`, `label`, `helper`, and `error` slots. each component reference lists its supported slots.\n\n## load typescript framework declarations\n\nimport the declaration matching the framework once in an application type entry point.\n\n::: code group\n\n```ts [dom]\nimport type {} from '@vielzeug/refine/frameworks/elements';\n```\n\n```ts [react]\nimport type {} from '@vielzeug/refine/frameworks/react';\n```\n\n```ts [vue]\nimport type {} from '@vielzeug/refine/frameworks/vue';\n```\n\n:::\n\nthese entry points provide typed tags and component properties. they do not register components or install runtime adapters.\n\n## framework integration\n\nrefine elements use native dom properties, events, and slots. frameworks differ only in how they forward custom element properties and events.\n\n::: code group\n\n```tsx [react 19]\nimport '@vielzeug/refine/button';\n\nexport function savebutton() {\n return <ore button variant=\"solid\">save</ore button>;\n}\n```\n\n```vue [vue 3]\n<script setup lang=\"ts\">\nimport '@vielzeug/refine/button';\n</script>\n\n<template>\n <ore button variant=\"solid\">save</ore button>\n</template>\n```\n\n```svelte [svelte]\n<script lang=\"ts\">\n import '@vielzeug/refine/button';\n</script>\n\n<ore button variant=\"solid\">save</ore button>\n```\n\n:::\n\nsee [framework integration](./frameworks.md) for custom events, controlled properties, ssr guards, and framework specific configuration.\n\n## working with other vielzeug libraries\n\n### ore\n\nuse ore when authoring a custom element that composes refine components.\n\n```ts\nimport '@vielzeug/refine/button';\nimport { define, html } from '@vielzeug/ore';\n\ndefine('save panel', {\n setup() {\n return html`<ore button @click=${() => console.log('save')}>save</ore button>`;\n },\n});\n```\n\n### ripple\n\nuse ripple to drive component properties from application state.\n\n```ts\nimport '@vielzeug/refine/button';\nimport { effect, signal } from '@vielzeug/ripple';\n\nconst loading = signal(false);\nconst button = document.queryselector('ore button') as htmlelement & { loading?: boolean };\nconst loadingeffect = effect(() => {\n button.loading = loading.value;\n});\n\nloadingeffect.dispose();\n```\n\n## best practices\n\n **import** `tokens.css` once before rendering components.\n **register** components through explicit subpaths.\n **set** structured data and callbacks as javascript properties.\n **read** form state from the event target instead of custom event payloads.\n **provide** accessible labels for icon only controls.\n **use** component reference pages for supported slots, parts, and css properties.\n **dispose** application owned ripple effects and service handles.\n **guard** dom dependent component registration during ssr.\n **working with ai agents?** install the `vielzeug` skill (`npx y @vielzeug/codex skills install`) so agents reach for refine components and tokens instead of custom widgets: see the [codex usage guide](../codex/usage.md#agent skill).\n"
|
|
999
1020
|
},
|
|
1000
1021
|
"examples": [],
|
|
1001
|
-
"exports": "ore accordion ore accordion item ore alert ore async ore avatar ore avatar group ore badge ore box ore breadcrumb ore breadcrumb item ore button ore button group ore calendar ore card ore carousel ore carousel slide ore chat message ore checkbox ore checkbox group ore chip ore code window ore column ore combobox ore combobox option ore command palette ore command palette item ore cookie banner ore copy command ore counter ore datagrid ore date picker ore dialog ore drawer ore file input ore grid ore grid item ore icon ore input ore keyboard key ore keyboard shortcut ore list ore list item ore marquee ore menu ore menu item ore menu separator ore message composer ore navbar ore navbar item ore navigation menu ore navigation menu item ore navigation menu panel ore number input ore otp input ore pagination ore password strength ore popover ore progress ore qr code ore qr scanner ore radio ore radio group ore rating ore select ore separator ore sidebar ore sidebar group ore sidebar item ore skeleton ore slider ore speech player ore stats ore step ore stepper ore switch ore tab item ore tab panel ore table ore tabs ore td ore text ore th ore tr ore textarea ore time picker ore toast ore tooltip ore typing indicator",
|
|
1022
|
+
"exports": "ore accordion ore accordion item ore alert ore async ore avatar ore avatar group ore badge ore box ore breadcrumb ore breadcrumb item ore button ore button group ore calendar ore card ore carousel ore carousel slide ore chat message ore checkbox ore checkbox group ore chip ore code window ore column ore combobox ore combobox option ore command palette ore command palette item ore cookie banner ore copy command ore counter ore datagrid ore date picker ore dialog ore drawer ore file input ore grid ore grid item ore icon ore input ore keyboard key ore keyboard shortcut ore list ore list item ore marquee ore menu ore menu item ore menu separator ore message composer ore navbar ore navbar item ore navigation menu ore navigation menu item ore navigation menu panel ore number input ore otp input ore pagination ore password strength ore popover ore progress ore qr code ore qr scanner ore query bar ore radio ore radio group ore rating ore select ore separator ore sidebar ore sidebar group ore sidebar item ore skeleton ore slider ore speech player ore stats ore step ore stepper ore switch ore tab item ore tab panel ore table ore tabs ore td ore text ore th ore tr ore textarea ore time picker ore toast ore tooltip ore typing indicator",
|
|
1002
1023
|
"keywords": "web components accessible themeable ui components design system",
|
|
1003
1024
|
"name": "@vielzeug/refine",
|
|
1004
1025
|
"related": "ore orbit forge keymap",
|
|
@@ -1398,7 +1419,7 @@
|
|
|
1398
1419
|
"index": " \ntitle: 'tandem: offline first sync engine'\ndescription: offline first sync engine with rev baselines, tombstoned deletions, and idle batched pushes for browser applications.\npackage: tandem\ncategory: sync\nkeywords: [sync, offline, conflict, tombstone, rev, replication, last write wins]\nrelated: [vault, postmaster, sentinel, ripple]\nexports: [createsync, syncport, syncgateway, synchandle, tandemevent, tandemerror, tandemdisposederror]\nenvironments: [browser, node]\n \n\n<! markdownlint disable md025 md033 md060 >\n\n<packagehero package=\"tandem\" />\n\n## why tandem?\n\na local first app keeps its records in indexeddb but must still reach a server: batch bursts of edits into one upload, propagate deletions that a plain pull would resurrect, and reconcile two devices that edited the same record while offline. tandem drives that round trip against any backend, tracking a per record write counter so a push carries only what actually changed.\n\n```ts\n// before\n// hand rolled: debounce each save, post the whole table, hope deletions survive.\nlet timer;\nfunction onsave(record) {\n cleartimeout(timer);\n timer = settimeout(() => fetch('/sync', { method: 'post', body: json.stringify(allrecords()) }), 3000);\n}\n\n// after\nimport { createsync } from '@vielzeug/tandem';\n\nconst sync = createsync({ gateway, port, idledelayms: 3000 });\nsync.changed(); // a local edit happened: the engine batches and pushes dirty records\n```\n\n| feature | tandem | rxsync | powersync |\n| | | | |\n| bundle size | <packageinfo package=\"tandem\" type=\"size\" /> | <ore icon name=\"x\" size=\"16\" /> | <ore icon name=\"x\" size=\"16\" /> |\n| zero runtime dependencies | <ore icon name=\"check\" size=\"16\" /> | <ore icon name=\"x\" size=\"16\" /> | <ore icon name=\"x\" size=\"16\" /> |\n| backend agnostic (bring your `port`) | <ore icon name=\"check\" size=\"16\" /> | <ore icon name=\"x\" size=\"16\" /> | <ore icon name=\"x\" size=\"16\" /> |\n| storage agnostic (bring your `gateway`) | <ore icon name=\"check\" size=\"16\" /> | <ore icon name=\"x\" size=\"16\" /> | <ore icon name=\"check\" size=\"16\" /> |\n| tombstoned deletions + rev conflicts | <ore icon name=\"check\" size=\"16\" /> | <ore icon name=\"check\" size=\"16\" /> | <ore icon name=\"check\" size=\"16\" /> |\n\n<div class=\"decision callout\">\n\n**use `tandem` when** you already own a local store and a backend and want the sync scheduler: dirty tracking, batching, hide flows, and conflict reconcile: without adopting a database or a server.\n\n**consider `powersync` when** you want a managed sync service with its own sqlite client and server component rather than a scheduler over your existing storage.\n\n</div>\n\n## installation\n\n::: code group\n\n```sh [pnpm]\npnpm add @vielzeug/tandem\n```\n\n```sh [npm]\nnpm install @vielzeug/tandem\n```\n\n```sh [yarn]\nyarn add @vielzeug/tandem\n```\n\n:::\n\n## quick start\n\nimplement the two seams: a `syncport` for the server and a `syncgateway` for local storage: then start the scheduler. records are opaque to tandem; it reads only `id` and `rev`, and every rev comparison is the engine's: the gateway upserts, validates, and tombstones.\n\n```ts\nimport { createsync, type syncgateway, type syncport } from '@vielzeug/tandem';\n\nconst port: syncport = {\n pull: (since) => fetch(`/sync?since=${since ?? ''}`).then((r) => r.json()),\n push: async (records, deletions) => {\n const response = await fetch('/sync', { method: 'post', body: json.stringify({ records, deletions }) });\n if (!response.ok) throw new error(await response.text()); // divergence → the engine pulls to reconcile\n },\n};\n\nconst gateway: syncgateway = {\n records: () => localrecords.map((record) => ({ entity: 'docs', record })),\n pendingdeletions: () => readtombstones(),\n applyrecords: (pulled) => upsert(pulled), // the engine pre filters; you validate untrusted records\n applydeletions: (deletions) => removelocal(deletions),\n cleardeletions: (deletions) => droptombstones(deletions),\n loadstate: () => readsyncstate(),\n savestate: (state) => writesyncstate(state),\n};\n\nconst sync = createsync({ gateway, port });\nsync.tap((event) => console.debug('sync', event)); // observe pushes, pulls, warnings\nmystore.subscribe(() => sync.changed()); // a local write happened\n\n// on account switch or teardown: after flushing this account's work:\nawait sync.flush();\nsync.dispose();\n```\n\n## features\n\n<div class=\"features grid\">\n\n **`createsync()`**: drives a port against a gateway with a serialized push queue.\n **`syncport`**: the server seam: `pull(since)` and `push(records, deletions)`.\n **`syncgateway`**: the storage seam: upserts, tombstones, and the persisted baseline: the engine owns every rev comparison.\n **`rev` baselines**: a record is dirty exactly when its rev is above the last synced one, across reloads.\n **`changed()`**: wire it to your write path; the engine debounces a burst into one push.\n **`flush()`**: a full cycle on demand: pull remote changes, then push dirty records; rejects on failure.\n **hide flows**: flushes with `keepalive` on `pagehide`/`hidden`, runs a full cycle on return to the foreground.\n **`tap()`**: typed `tandemevent`s for pushes, pulls, invalid records, and warnings.\n **lifecycle owned**: `dispose()`, `disposed`, `disposalsignal`, and `[symbol.dispose]`; post dispose `changed()`/`flush()` fail with `tandemdisposederror`.\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 [vault](/vault/): the typed storage core most gateways wrap; its indexeddb adapter is a natural `records()`/`applyrecords()` backend.\n [postmaster](/postmaster/): the durable outbox half of offline sync; pair it with tandem for at least once mutations alongside state sync.\n [sentinel](/sentinel/): subscribable browser state; its network signal can gate when you call `flush()`.\n [ripple](/ripple/): signals and effects; `store.subscribe(() => sync.changed())` is the usual wiring.\n\n</div>\n\n<! markdownlint enable md025 md033 md060 >\n",
|
|
1399
1420
|
"api": " \ntitle: 'tandem: api reference'\ndescription: factory, port and gateway contracts, events, and types for tandem.\n \n\n[[toc]]\n\n## api overview\n\n| symbol | purpose | execution mode | common gotcha |\n| | | | |\n| `createsync()` | scheduler driving a port against a gateway | sync (queues async work) | boot runs pull then push; dispose before switching accounts |\n\n## package entry point\n\n| import | purpose |\n| | |\n| `@vielzeug/tandem` | `createsync`, `tandemerror`, `tandemdisposederror`, port/gateway contracts, events, and types |\n\n## factories\n\n### `createsync()`\n\n```ts\nfunction createsync<trecord extends syncrecordbase = syncrecordbase>(\n options: syncoptions<trecord>,\n): synchandle;\n```\n\nstarts a sync scheduler: it loads the persisted baseline, pulls remote changes, pushes anything dirty, then flushes on `changed()`, on tab hide, and on foreground return.\n\n| parameter | type | description |\n| | | |\n| `options.gateway` | `syncgateway<trecord>` | the device's storage seam |\n| `options.idledelayms` | `number \\| undefined` | quiet period before a flush (default `3000`) |\n| `options.port` | `syncport<trecord>` | the server seam |\n\n**returns** a `synchandle`. ambient flushes never reject: they surface failures as `warning` events through `tap()`. an explicit `flush()` rejects when its cycle fails.\n\n**example**\n\n```ts\nimport { createsync } from '@vielzeug/tandem';\n\nconst sync = createsync({ gateway, idledelayms: 1500, port });\nsync.changed();\nawait sync.flush();\nsync.dispose();\n```\n\n#### `synchandle` methods\n\n| member | description |\n| | |\n| `changed()` | notes a local change and (re)starts the idle flush timer; throws `tandemdisposederror` after `dispose` |\n| `dispose()` | stops the scheduler, clears the timer, removes document/window listeners |\n| `disposed` | `true` once disposed |\n| `disposalsignal` | `abortsignal` aborted on dispose |\n| `flush()` | runs a full cycle: pull, then push; resolves when it completes, rejects when it fails, and rejects with `tandemdisposederror` after `dispose` |\n| `tap(handler, options?)` | observes `tandemevent`s; returns an unsubscribe function |\n| `[symbol.dispose]()` | calls `dispose()` for `using` scopes |\n\n## contracts\n\n### `syncport`\n\nthe server seam. `pull(since)` returns remote records, remote deletions, and the next cursor. `push(records, deletions, options)` sends local changes; reject it when the server diverged so the engine pulls to reconcile. `options.keepalive` marks a flush that must survive the tab closing.\n\n### `syncgateway`\n\nthe storage seam. the engine owns every rev comparison: it calls your gateway with the decisions already made.\n\n| member | contract |\n| | |\n| `records()` | this device's own records, remote mirrors excluded; sync or async, so a disk backed gateway can read from storage instead of holding a mirror |\n| `pendingdeletions()` | tombstones the server has not acknowledged |\n| `applyrecords(records)` | upsert the given records: already filtered to what the server is ahead on; return the ids that failed validation |\n| `applydeletions(deletions)` | remove records deleted remotely |\n| `cleardeletions(deletions)` | drop the given tombstones a successful push carried |\n| `loadstate()` | the persisted `syncstate`, or `null` on a fresh device |\n| `savestate(state)` | persist the baseline after a completed pull or push |\n\n## events\n\n### `tandemevent`\n\n```ts\ntype tandemevent =\n | { readonly records: number; readonly type: 'pull' }\n | { readonly records: number; readonly type: 'push' }\n | { readonly message: string; readonly skipped: readonly string[]; readonly type: 'invalid' }\n | { readonly error: unknown; readonly message: string; readonly type: 'warning' }\n | { readonly type: 'dispose' };\n```\n\n`invalid` reports record ids `applyrecords` refused; `warning` reports a rejected push or transport failure and carries the original cause in `error` beside its `message`; `pull` and `push` report the record counts applied and pushed. `dispose` fires once, before tappers are cleared.\n\n## types\n\n### `syncrecordbase`\n\n```ts\ninterface syncrecordbase {\n readonly id: string;\n readonly rev: number;\n}\n```\n\nthe only shape tandem reads from a record: identity and a write counter. `rev` is required: the dirty model is a rev comparison, so every synced record carries one from birth.\n\n### `syncenvelope`\n\n```ts\ninterface syncenvelope<trecord extends syncrecordbase = syncrecordbase> {\n readonly entity: string;\n readonly record: trecord;\n}\n```\n\na record paired with the entity (table or kind) it belongs to.\n\n### `syncdeletion`\n\n```ts\ninterface syncdeletion {\n readonly deletedat?: string;\n readonly entity: string;\n readonly id: string;\n}\n```\n\na tombstone: a deletion that travels to the server on the next push. `deletedat` is set by whoever recorded the deletion and the engine never reads it: include it when your server wants the timestamp, omit it otherwise.\n\n### `syncstate`\n\n```ts\ninterface syncstate {\n cursor: string | null;\n revs: record<string, number>;\n}\n```\n\nthe persisted baseline. `revs` maps `\"${entity}:${id}\"` to the newest rev the server has seen.\n\n### `syncpullresult`\n\n```ts\ninterface syncpullresult<trecord extends syncrecordbase = syncrecordbase> {\n readonly cursor: string | null;\n readonly deletions: readonly syncdeletion[];\n readonly records: readonly syncenvelope<trecord>[];\n}\n```\n\n### `syncoptions`\n\n```ts\ninterface syncoptions<trecord extends syncrecordbase = syncrecordbase> {\n gateway: syncgateway<trecord>;\n idledelayms?: number;\n port: syncport<trecord>;\n}\n```\n\n## errors\n\n### `tandemerror`\n\n```ts\nclass tandemerror extends error {}\n```\n\nbase class for every error tandem throws; `instanceof tandemerror` catches them all in one branch.\n\n### `tandemdisposederror`\n\n```ts\nclass tandemdisposederror extends tandemerror {}\n```\n\nthrown by `changed()` and rejected by `flush()` after `dispose()`. a scheduler that stops silently is a sync engine's worst failure: the device keeps editing while nothing uploads, so a leaked handle fails loudly instead.\n\nbeyond disposal, tandem defines no error classes. a rejected `push` is expected input, not a failure: the engine pulls to reconcile and reports it as a `warning` event. `flush()` additionally rejects with the underlying port or gateway error; dirty records survive the failure and retry on the next cycle.\n",
|
|
1400
1421
|
"usage": " \ntitle: 'tandem: usage guide'\ndescription: 'how to drive offline sync with @vielzeug/tandem: ports, gateways, rev baselines, tombstones, and reconciliation.'\n \n\n[[toc]]\n\n## basic usage\n\ntandem sits between your storage and your server. you implement two interfaces and call `createsync`; the engine handles batching, dirty tracking, hide flows, and conflict reconcile.\n\n```ts\nimport { createsync, type syncgateway, type syncport } from '@vielzeug/tandem';\n\ninterface doc {\n id: string;\n rev: number;\n title: string;\n}\n\nconst port: syncport<doc> = {\n async pull(since) {\n return fetch(`/sync/pull?since=${since ?? ''}`).then((response) => response.json());\n },\n async push(records, deletions, options) {\n const response = await fetch('/sync/push', {\n body: json.stringify({ records, deletions }),\n headers: { 'content type': 'application/json' },\n keepalive: options?.keepalive,\n method: 'post',\n });\n if (!response.ok) throw new error(`server diverged: ${await response.text()}`);\n },\n};\n\nconst gateway: syncgateway<doc> = {\n records: () => docs.map((record) => ({ entity: 'docs', record })),\n pendingdeletions: () => readtombstones(),\n async applyrecords(pulled) {\n // the engine already filtered these to records the server is ahead on;\n // validate the untrusted remotes and upsert the rest.\n const skipped: string[] = [];\n for (const { record } of pulled) {\n if (!isvalid(record)) skipped.push(record.id);\n else upsert(record);\n }\n return skipped;\n },\n applydeletions: (deletions) => promise.all(deletions.map((d) => remove(d.id))),\n cleardeletions: (deletions) => promise.all(deletions.map((d) => droptombstone(d.id))),\n loadstate: () => readrow('syncstate'),\n savestate: (state) => writerow('syncstate', state),\n};\n\nconst sync = createsync<doc>({ gateway, port });\n```\n\n## track dirty records with revs\n\ntandem never diffs record contents. each record carries a `rev` counting its committed writes, and the gateway persists a baseline of the newest rev the server has seen. a record is dirty exactly when its current rev is above that baseline.\n\n```ts\n// your write path bumps the rev on every committed change.\nfunction savedoc(id: string, patch: partial<doc>): void {\n const current = docs.find((doc) => doc.id === id)!;\n upsert({ ...current, ...patch, rev: current.rev + 1 });\n sync.changed(); // schedule a flush\n}\n```\n\nbecause the baseline is persisted, dirtiness survives a reload: a record edited while offline is still above the baseline when the app restarts, and the boot sequence pushes it. the engine never asks your gateway to compare revs: ahead, dirty, and deletion precedence are all its decisions.\n\n## propagate deletions with tombstones\n\na plain pull cannot carry a deletion: the record is gone. record a tombstone when you delete locally; tandem pushes it and clears it only once the server acknowledges.\n\n```ts\nfunction deletedoc(id: string): void {\n remove(id);\n writetombstone({ deletedat: new date().toisostring(), entity: 'docs', id });\n sync.changed();\n}\n```\n\na remote deletion loses to a local record with unpushed changes: if your local rev is above the baseline, tandem keeps your edit and pushes it instead of applying the deletion. the guard runs both ways: a pulled record whose id is locally tombstoned, or deleted in the same pull, never applies, so a backend without an incremental cursor cannot resurrect your deletions.\n\n## batch changes and flush\n\ncall `changed()` on every local write. the engine debounces a burst into one push after `idledelayms` (default 3000), and serializes pushes so two never overlap.\n\n```ts\nconst sync = createsync({ gateway, idledelayms: 1500, port });\n\n// force a full cycle now: on a save button, or before navigating away.\ntry {\n await sync.flush();\n} catch {\n // the cycle failed: edits stay local and dirty; the next cycle retries them\n}\n```\n\n`flush()` pulls remote changes and then pushes dirty records, so it is also the wiring point for a returning network connection. it rejects when either half fails; the records stay dirty and the next cycle retries them. after `dispose()`, `flush()` rejects with `tandemdisposederror` and `changed()` throws it: a leaked scheduler fails loudly instead of silently stopping. if your reactive wiring can fire after disposal, unsubscribe from your store before disposing the handle, or guard with `sync.disposed`.\n\ntandem also flushes on its own: it pushes with `keepalive: true` when the tab hides or closes, and runs a full cycle when the tab returns to the foreground. serve the keepalive push with `navigator.sendbeacon` or a `keepalive` fetch so it survives the tab closing.\n\n## reconcile a rejected push\n\nwhen the server is ahead of the lineage you pushed, reject the push. tandem immediately pulls to reconcile, then the next flush retries with the merged state. ambient flushes (debounce, hide) report the failure as a `warning` event; `flush()` additionally rejects with the port's error.\n\n```ts\nasync push(records, deletions) {\n const response = await fetch('/sync/push', { body: json.stringify({ records, deletions }), method: 'post' });\n if (response.status === 409) throw new error('server ahead'); // triggers a reconcile pull\n}\n```\n\n## observe with tap()\n\n`tap()` is the observation seam: pushes, pulls, refused records, and warnings. handler errors are swallowed, so an observer never breaks sync.\n\n```ts\nconst unsubscribe = sync.tap((event) => {\n switch (event.type) {\n case 'push':\n case 'pull':\n console.debug(`sync ${event.type}: ${event.records} records`);\n break;\n case 'invalid':\n console.warn(event.message, event.skipped);\n break;\n case 'warning':\n console.warn(event.message, event.error); // push rejected, network failure, etc.\n break;\n }\n});\n```\n\n## lifecycle and disposal\n\ndispose before switching accounts: the baseline is per account state and a running scheduler would push into the wrong port.\n\n```ts\nconst sync = createsync({ gateway, port });\n// ...\nsync.dispose(); // stops the scheduler, clears the timer, removes document/window listeners\n```\n\n`disposed` and `disposalsignal` report the state; `[symbol.dispose]` lets you scope it with `using`.\n\n```ts\n{\n using sync = createsync({ gateway, port });\n sync.changed();\n} // disposed here\n```\n\n## testing\n\ndrive the engine against in memory doubles. a `syncport` double records pushes and returns scripted pulls; a `syncgateway` double is a record array plus a tombstone list plus a state row.\n\n```ts\nimport { createsync } from '@vielzeug/tandem';\n\nconst pushes: unknown[][] = [];\nconst port: syncport = {\n pull: async () => ({ cursor: null, deletions: [], records: [] }),\n push: async (records, deletions) => void pushes.push([records, deletions]),\n};\n\nconst sync = createsync({ gateway: fakegateway, idledelayms: 10, port });\nawait vi.waitfor(() => expect(pushes.length).tobe(1)); // startup push\nsync.dispose();\n```\n\n## framework integration\n\ntandem is framework neutral. wire `changed()` to your reactive layer and `dispose()` to the component or scope lifecycle.\n\n::: code group\n\n```ts [vue 3]\nimport { onscopedispose, watch } from 'vue';\n\nconst sync = createsync({ gateway, port });\nwatch(store.records, () => sync.changed(), { deep: true });\nonscopedispose(() => sync.dispose());\n```\n\n```tsx [react]\nimport { useeffect, useref } from 'react';\n\nconst syncref = useref(createsync({ gateway, port }));\nuseeffect(() => {\n const sync = syncref.current;\n return () => sync.dispose();\n}, []);\n```\n\n```svelte [svelte]\n<script lang=\"ts\">\n import { ondestroy } from 'svelte';\n const sync = createsync({ gateway, port });\n $effect(() => { $records; sync.changed(); });\n ondestroy(() => sync.dispose());\n</script>\n```\n\n:::\n\n## working with other vielzeug libraries\n\n **vault**: the usual `gateway` backend. its indexeddb adapter stores records and the sync state row; `records()` reads them from storage (it may be async), `applyrecords()` writes through the codec.\n **postmaster**: complementary, not overlapping. postmaster guarantees at least once delivery of discrete mutations; tandem keeps whole record state in step. apps that need both run them side by side.\n **sentinel**: subscribe to `createnetwork()` and call `sync.flush()` when the connection returns; the cycle pulls and pushes, so remote changes land without waiting for the next foreground return.\n **ripple**: `store.subscribe(() => sync.changed())` is the idiomatic wiring for a signal backed store.\n\n## best practices\n\n bump `rev` on every committed write: the whole dirty model depends on it.\n validate remote records in `applyrecords()` and return the ids you refuse; never trust a pulled record.\n keep `records()` free of remote session mirrors: it must return only this device's own records.\n dispose on account switch; a stale scheduler pushes into the wrong port.\n serve `keepalive` pushes with `sendbeacon` or a `keepalive` fetch so they survive tab close.\n reject a push on divergence instead of silently overwriting; let the reconcile pull merge.\n persist the sync state row per account, alongside the records it describes.\n call `flush()` before actions that assume both sides are current, such as a share or export, and handle its rejection.\n",
|
|
1401
|
-
"examples": " \ntitle: 'tandem: examples'\ndescription: offline sync recipes for two device editing, deletions, and account switching.\n \n\n## examples\n\n [sync a vault store](./examples/sync a vault store.md)\n [keep deletions from resurrecting](./examples/keep deletions from resurrecting.md)\n [switch accounts safely](./examples/switch accounts safely.md)\n"
|
|
1422
|
+
"examples": " \ntitle: 'tandem: examples'\ndescription: offline sync recipes for two device editing, deletions, and account switching.\n \n\n## examples\n\n [sync a vault store](./examples/sync a vault store.md)\n [make store commands undoable](./examples/make store commands undoable.md)\n [keep deletions from resurrecting](./examples/keep deletions from resurrecting.md)\n [switch accounts safely](./examples/switch accounts safely.md)\n"
|
|
1402
1423
|
},
|
|
1403
1424
|
"examples": [
|
|
1404
1425
|
{
|