@vielzeug/codex 3.0.4 → 3.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -0
- package/data/catalog.json +93 -8
- package/data/llms-full.txt +10269 -8939
- package/data/llms.txt +3 -1
- package/data/manifest.json +1 -1
- package/data/packages/codex.json +1 -1
- package/data/packages/forge.json +6 -5
- package/data/packages/mesh.json +54 -0
- package/data/packages/refine.json +3 -3
- package/data/packages/sigil.json +49 -0
- package/data/packages/spell.json +30 -26
- package/data/refine.json +653 -5
- package/data/search.json +63 -15
- package/dist/cli.js +32 -0
- package/dist/cli.js.map +1 -1
- package/dist/skills.d.ts +26 -0
- package/dist/skills.js +46 -0
- package/dist/skills.js.map +1 -0
- package/package.json +4 -3
- package/skills/vielzeug/SKILL.md +153 -0
package/data/search.json
CHANGED
|
@@ -202,7 +202,7 @@
|
|
|
202
202
|
"docs": {
|
|
203
203
|
"index": " \ntitle: codex\ndescription: local mcp access to vielzeug documentation and package metadata.\npackage: codex\ncategory: ai\nkeywords: [mcp, docs, ai]\nrelated: [refine]\nexports: [loadsnapshot, snapshotcatalog, createmcpserver, starthttphost, stdioservertransport, catalogerror, codexerror]\nenvironments: [node]\n \n\n<packagehero package=\"codex\" />\n\n## why codex?\n\ncodex exposes current vielzeug catalog data through mcp without scanning source at request time.\n\n## installation\n\n```sh\npnpm add @vielzeug/codex\n```\n\n## quick start\n\n```sh\nnpx y @vielzeug/codex\n```\n\n## features\n\n `loadsnapshot` validates chunked snapshot metadata.\n `snapshotcatalog` loads package content only when requested.\n `createmcpserver` registers generic package tools; `/refine` adds component tools explicitly.\n root, `/advanced`, and `/refine` entry points publish typescript declarations.\n `starthttphost` enforces loopback binding plus localhost host and origin validation.\n\n## documentation\n\n [usage](./usage.md)\n [api](./api.md)\n [examples](./examples.md)\n [migration guide](./migration.md)\n\n## see also\n\n [refine](../refine/) provides component metadata bundled by codex.\n",
|
|
204
204
|
"api": " \ntitle: codex api\ndescription: snapshot, catalog, mcp server, and local http host apis.\n \n\n[[toc]]\n\n## api overview\n\n| symbol | purpose | execution mode | common gotcha |\n| | | | |\n| `loadsnapshot` | read validated snapshot metadata | sync | content chunks load lazily |\n| `snapshotcatalog` | query package corpus | sync | construct from loaded snapshot |\n| `createmcpserver` | mcp adapter factory (generic tools) | sync | requires catalog and version |\n| `starthttphost` | loopback streamable http host | async | http remains local only |\n| `registerrefinetools` | opt in refine tool registration | sync | import from `@vielzeug/codex/refine` |\n| `parsepointer` / `parsemanifest` / `parsecatalog` / `parsecontent` / `parsesearch` | pure snapshot parsers | sync | import from `@vielzeug/codex/advanced`; throw `codexerror` on malformed input |\n\n## package entry points\n\n| import | purpose |\n| | |\n| `@vielzeug/codex` | `catalog`, `snapshotcatalog`, `loadsnapshot`, mcp server, and http host |\n| `@vielzeug/codex/advanced` | snapshot parser internals and raw snapshot types |\n| `@vielzeug/codex/refine` | opt in refine component tools (`registerrefinetools`) |\n\n## snapshot\n\n### `loadsnapshot`\n\n```ts\nloadsnapshot(snapshotroot?: string, options?: { validatecontents?: boolean }): loadedsnapshot;\n```\n\nloads catalog/search metadata and refine metadata. `snapshotroot` defaults to bundled `data/`. package content chunks stay lazy unless `validatecontents: true`; `validatesnapshot()` from `/advanced` is the full validation shortcut.\n\n### `snapshotcatalog`\n\n```ts\nnew snapshotcatalog(snapshot: loadedsnapshot)\n```\n\nprovides generic package lookup, docs/source/example/signature access, and deterministic search. use `snapshotrefinecatalog` from `/refine` when component methods are needed.\n\n \n\n### snapshot parsers (`@vielzeug/codex/advanced`)\n\n```ts\nvalidatesnapshot(snapshotroot?: string): void;\nloadsnapshotdirectory(directory: string, options?: { validatecontents?: boolean }): loadedsnapshot;\nparsepointer(value: unknown): snapshotpointer;\nparsemanifest(value: unknown): snapshotmanifest;\nparsecatalog(value: unknown): catalogfile;\nparsecontent(value: unknown, slug: string): packagecontent;\nparsesearch(value: unknown, catalog: catalogfile): searchrecord[];\nparserefine(value: unknown): cemdeclaration[];\n```\n\npure validation parsers used by `loadsnapshot`. each throws `codexerror` on malformed input. `validatesnapshot` loads and validates every package content chunk — use during generation, integration tests, or explicit artifact verification.\n\n## mcp\n\n### `createmcpserver`\n\n```ts\ncreatemcpserver(catalog: catalog, options: { version: string; debug?: boolean }): server;\n```\n\nregisters generic package tools over `catalog`. refine tools are opt in through `/refine`. the root also re exports `stdioservertransport` for connecting the server without a second package import.\n\n## refine tools (`@vielzeug/codex/refine`)\n\n### `snapshotrefinecatalog`\n\n```ts\nnew snapshotrefinecatalog(snapshot: loadedsnapshot)\n```\n\nextends `snapshotcatalog` with `getcomponent()` and `listcomponents()`.\n\n### `registerrefinetools`\n\n```ts\nregisterrefinetools(server: server, catalog: refinecatalog, debug?: boolean): void;\n```\n\nupgrades a server created by `createmcpserver()` with the combined generic and `refine *` tool set. `refinetools` is exported as a readonly registry.\n\n## http\n\n### `starthttphost`\n\n```ts\nstarthttphost(options: httphostoptions): promise<httphost>;\n```\n\nstarts streamable http on `127.0.0.1` by default. runtime validation accepts only `127.0.0.1` or `::1`; every request must also pass localhost host and origin checks. `configureserver` runs once for each lazily created mcp request server and is not called during host startup.\n\n## types\n\n```ts\ninterface snapshotpointer {\n directory: string;\n}\n\ninterface snapshotmanifest {\n catalog: 'catalog.json';\n contentdirectory: 'packages';\n refine: 'refine.json' | null;\n schemaversion: typeof snapshot_schema_version;\n search: 'search.json';\n version: string;\n}\n```\n\ndev snapshots use `snapshotpointer` (via `current.json`); published snapshots are static directories.\n\n```ts\ninterface loadedsnapshot {\n catalog: catalogfile;\n contentdirectory: string;\n manifest: snapshotmanifest;\n refinecomponents: cemdeclaration[];\n search: searchrecord[];\n}\n```\n\n```ts\ninterface catalogfile {\n packages: packagemeta[];\n version: string;\n}\n\ninterface searchrecord {\n category: string;\n description: string;\n docs: partial<record<docpage, string>>;\n examples: array<{ id: string; text: string }>;\n exports: string;\n keywords: string;\n name: string;\n related: string;\n slug: string;\n source: string | null;\n}\n\ninterface packagemeta {\n availabledocpages: docpage[];\n category: string;\n description: string;\n exampleids: string[];\n exports: string[];\n hassource: boolean;\n keywords: string[];\n name: string;\n related: string[];\n slug: string;\n version: string;\n}\n\ninterface packagecontent {\n apisource: string | null;\n docs: partial<record<docpage, string>>;\n examples: example[];\n typesignatures: record<string, string>;\n}\n\ninterface example {\n code: string;\n id: string;\n name: string;\n}\n```\n\n```ts\ninterface catalog {\n getcontent(slug: string): packagecontent;\n getdocs(slug: string, page: docpage): string;\n getexample(slug: string, exampleid: string): example;\n getpackage(slug: string): packagemeta;\n getsource(slug: string): string;\n gettypesignature(slug: string, symbol: string): string;\n listexamples(slug: string): array<pick<example, 'id' | 'name'>>;\n listpackages(): packagemeta[];\n search(query: string): searchhit[];\n}\n\ninterface refinecatalog extends catalog {\n getcomponent(tagname: string): cemdeclaration;\n listcomponents(): cemdeclaration[];\n}\n\ninterface searchhit {\n matchedexamples?: string[];\n matchedin: array<'docs' | 'examples' | 'exports' | 'keywords' | 'metadata' | 'related' | 'source'>;\n matchedpages?: docpage[];\n name: string;\n slug: string;\n}\n```\n\n```ts\ninterface httphost {\n readonly disposalsignal: abortsignal;\n dispose(): promise<void>;\n readonly disposed: boolean;\n readonly host: string;\n readonly port: number;\n [symbol.asyncdispose](): promise<void>;\n}\n\ninterface httphostoptions {\n catalog: catalog;\n configureserver?: (server: server) => void;\n debug?: boolean;\n host?: '127.0.0.1' | '::1';\n port: number;\n version: string;\n}\n```\n\n```ts\nconst doc_pages = ['index', 'api', 'usage', 'examples'] as const;\ntype docpage = (typeof doc_pages)[number];\n\nconst snapshot_schema_version = 1 as const;\n```\n\n`doc_pages`, `snapshot_schema_version`, and raw snapshot types are exported from `/advanced`. cem types and `refinecatalog` are exported from `/refine`.\n\n```ts\ninterface cemdeclaration {\n attributes?: cemattribute[];\n cssparts?: cemcsspart[];\n cssproperties?: cemcssproperty[];\n description?: string;\n events?: cemevent[];\n members?: cemmember[];\n name?: string;\n slots?: cemslot[];\n superclass?: { name: string; package?: string };\n tagname?: string;\n [key: string]: unknown;\n}\n\ninterface cemattribute {\n default?: string;\n description?: string;\n fieldname?: string;\n name: string;\n type?: { text: string };\n}\n\ninterface cemcsspart {\n description?: string;\n name: string;\n}\n\ninterface cemcssproperty {\n default?: string;\n description?: string;\n name: string;\n}\n\ninterface cemevent {\n description?: string;\n name: string;\n type?: { text: string };\n}\n\ninterface cemmember {\n description?: string;\n kind?: 'field' | 'method';\n name: string;\n type?: { text: string };\n}\n\ninterface cemslot {\n description?: string;\n name: string;\n}\n```\n\n## errors\n\n`codexerror` signals malformed snapshots or host failures. `catalogerror` adds `invalid_arg`, `not_found`, or `unavailable` for expected tool failures.\n",
|
|
205
|
-
"usage": " \ntitle: codex — usage guide\ndescription: install, connect, develop, and debug the vielzeug mcp server.\n \n\n[[toc]]\n\n## basic usage\n\nrun local stdio server:\n\n```sh\nnpx y @vielzeug/codex\n```\n\nuse shipped `mcp setup.json` for machine readable generic configuration. client specific configuration must use its documented mcp format.\n\n## http mode\n\nhttp uses streamable http and binds loopback only:\n\n```sh\nnpx y @vielzeug/codex port=3100\ncurl http://127.0.0.1:3100/health\n```\n\nresponse includes snapshot version. runtime bind validation and host/origin allowlists restrict access to localhost; no remote host mode exists. programmatic `configureserver` hooks run once per mcp request server, not at host startup. dispose the host asynchronously and observe `disposed` or `disposalsignal` when coordinating shutdown.\n\n## local development\n\nrequires node 22+ and root setup:\n\n```sh\npnpm setup\ncd packages/codex\npnpm test:unit\npnpm test:integration\npnpm dev\n```\n\n`test:unit` uses fixtures only. `test:integration` regenerates a current snapshot then checks real monorepo inputs.\n\n`pnpm dev` watches documentation and package inputs, atomically publishes snapshots, then restarts server when snapshot changes.\n\n## debugging\n\n```sh\npnpm dev\nnode src/cli.ts port=3100 debug\ncurl http://127.0.0.1:3100/health\n```\n\n` debug` logs tool durations and expected catalog errors to stderr. build `@vielzeug/refine` before generating snapshot when component metadata changes.\n\n## programmatic usage\n\n```ts\nimport { snapshotcatalog, stdioservertransport, createmcpserver, loadsnapshot } from '@vielzeug/codex';\n\nconst snapshot = loadsnapshot();\nconst catalog = new snapshotcatalog(snapshot);\nawait createmcpserver(catalog, { version: snapshot.manifest.version }).connect(new stdioservertransport());\n```\n\nto include refine component tools, upgrade the server with the `@vielzeug/codex/refine` subpath:\n\n```ts\nimport { stdioservertransport, createmcpserver, loadsnapshot } from '@vielzeug/codex';\nimport { snapshotrefinecatalog, registerrefinetools } from '@vielzeug/codex/refine';\n\nconst snapshot = loadsnapshot();\nconst catalog = new snapshotrefinecatalog(snapshot);\nconst server = createmcpserver(catalog, { version: snapshot.manifest.version });\nregisterrefinetools(server, catalog);\nawait server.connect(new stdioservertransport());\n```\n\n## best practices\n\n use `search packages` for capability discovery before loading broad source.\n use `get type signature` before loading full source.\n published package snapshots are static directories; local dev snapshots are immutable generations selected by `.dev/current.json`.\n import `validatesnapshot()` from `/advanced` for artifact verification; normal startup keeps package chunks lazy.\n treat `configureserver` as a per request factory hook and avoid process global side effects.\n keep http local. use stdio for normal client integration.\n run `pnpm test:unit` before `pnpm test:integration`.\n",
|
|
205
|
+
"usage": " \ntitle: codex — usage guide\ndescription: install, connect, develop, and debug the vielzeug mcp server.\n \n\n[[toc]]\n\n## basic usage\n\nrun local stdio server:\n\n```sh\nnpx y @vielzeug/codex\n```\n\nuse shipped `mcp setup.json` for machine readable generic configuration. client specific configuration must use its documented mcp format.\n\n## http mode\n\nhttp uses streamable http and binds loopback only:\n\n```sh\nnpx y @vielzeug/codex port=3100\ncurl http://127.0.0.1:3100/health\n```\n\nresponse includes snapshot version. runtime bind validation and host/origin allowlists restrict access to localhost; no remote host mode exists. programmatic `configureserver` hooks run once per mcp request server, not at host startup. dispose the host asynchronously and observe `disposed` or `disposalsignal` when coordinating shutdown.\n\n## agent skill\n\ncodex ships a `vielzeug` agent skill — a `skill.md` that tells an ai agent working in *your* project to discover packages through the mcp tools instead of guessing, to use the owning `@vielzeug/*` package instead of hand rolling, and to build ui from refine components, tokens, and theme whenever `@vielzeug/refine` is installed. install it into the project:\n\n```sh\nnpx y @vielzeug/codex skills install\nnpx y @vielzeug/codex skills install target=.claude/skills\nnpx y @vielzeug/codex skills install force # overwrite an existing copy\n```\n\nwithout ` target`, the first existing directory among `.agents/skills`, `.github/skills`, and `.claude/skills` is used; when none exists, `.agents/skills` is created. the command refuses to overwrite an installed skill unless ` force` is passed. rerun after upgrading codex to pick up the current package list, which is generated from the published manifests. the skill pairs with the mcp server: its lookups (`get docs`, `refine get component`, `refine validate usage`, …) assume codex is connected, and it documents `node_modules` fallbacks for when it is not.\n\n## local development\n\nrequires node 22+ and root setup:\n\n```sh\npnpm setup\ncd packages/codex\npnpm test:unit\npnpm test:integration\npnpm dev\n```\n\n`test:unit` uses fixtures only. `test:integration` regenerates a current snapshot then checks real monorepo inputs.\n\n`pnpm dev` watches documentation and package inputs, atomically publishes snapshots, then restarts server when snapshot changes.\n\n## debugging\n\n```sh\npnpm dev\nnode src/cli.ts port=3100 debug\ncurl http://127.0.0.1:3100/health\n```\n\n` debug` logs tool durations and expected catalog errors to stderr. build `@vielzeug/refine` before generating snapshot when component metadata changes.\n\n## programmatic usage\n\n```ts\nimport { snapshotcatalog, stdioservertransport, createmcpserver, loadsnapshot } from '@vielzeug/codex';\n\nconst snapshot = loadsnapshot();\nconst catalog = new snapshotcatalog(snapshot);\nawait createmcpserver(catalog, { version: snapshot.manifest.version }).connect(new stdioservertransport());\n```\n\nto include refine component tools, upgrade the server with the `@vielzeug/codex/refine` subpath:\n\n```ts\nimport { stdioservertransport, createmcpserver, loadsnapshot } from '@vielzeug/codex';\nimport { snapshotrefinecatalog, registerrefinetools } from '@vielzeug/codex/refine';\n\nconst snapshot = loadsnapshot();\nconst catalog = new snapshotrefinecatalog(snapshot);\nconst server = createmcpserver(catalog, { version: snapshot.manifest.version });\nregisterrefinetools(server, catalog);\nawait server.connect(new stdioservertransport());\n```\n\n## best practices\n\n use `search packages` for capability discovery before loading broad source.\n use `get type signature` before loading full source.\n published package snapshots are static directories; local dev snapshots are immutable generations selected by `.dev/current.json`.\n import `validatesnapshot()` from `/advanced` for artifact verification; normal startup keeps package chunks lazy.\n treat `configureserver` as a per request factory hook and avoid process global side effects.\n keep http local. use stdio for normal client integration.\n run `pnpm test:unit` before `pnpm test:integration`.\n",
|
|
206
206
|
"examples": " \ntitle: codex — examples\ndescription: practical mcp tool call examples for package discovery, docs lookup, and refine component queries.\n \n\n## examples\n\n [listing packages](./examples/listing packages.md)\n [searching packages](./examples/searching packages.md)\n [package metadata](./examples/package metadata.md)\n [reading docs](./examples/reading docs.md)\n [running repl examples](./examples/running repl examples.md)\n [looking up components](./examples/looking up components.md)\n [inspector](./examples/inspector.md)\n"
|
|
207
207
|
},
|
|
208
208
|
"examples": [],
|
|
@@ -491,9 +491,9 @@
|
|
|
491
491
|
"category": "state",
|
|
492
492
|
"description": "framework neutral form and field state with nested handles, flat validation issues, and explicit integration helpers.",
|
|
493
493
|
"docs": {
|
|
494
|
-
"index": " \ntitle: forge — immutable typed form state\ndescription: framework neutral form and field state with nested handles, flat validation issues, and explicit integration helpers.\npackage: forge\ncategory: state\nkeywords: [forms, validation, fields, immutable, standard schema, formdata]\nexports: [createform]\nrelated: [spell, vault, assay]\nenvironments: [browser, node, ssr, deno]\n \n\n<! markdownlint disable md025 md033 md060 >\n\n<packagehero package=\"forge\" />\n\n## why forge?\n\nforge owns form state without owning rendering or validation timing. nested field handles keep access typed, flat validation issues compose across arrays and unions, and forge owned validation and persistence operations settle on cancellation.\n\n## quick start\n\n```ts\nimport { createform } from '@vielzeug/forge';\n\nconst form = createform({\n initialvalues: { email: '', profile: { name: '' } },\n validate: (values) =>\n values.email.includes('@') ? undefined : [{ path: ['email'], message: 'invalid email' }],\n});\n\nform.field('profile').field('name').set('ada');\nconst result = await form.validate();\n```\n\n| feature | forge | local component state |\n| | | |\n| bundle size | <packageinfo package=\"forge\" type=\"size\" /> | n/a |\n| nested typed fields | <ore icon name=\"check\" size=\"16\"></ore icon> | manual |\n| flat validation issues | <ore icon name=\"check\" size=\"16\"></ore icon> | manual |\n| abortable validation | <ore icon name=\"check\" size=\"16\"></ore icon> | manual |\n| framework neutral | <ore icon name=\"check\" size=\"16\"></ore icon> | varies |\n| zero third party dependencies | <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 forge when** several fields need coordinated validation, submission, dirty/touched state, nested updates, or explicit persistence.\n\n**use local state when** a form has only one or two fields and no shared validation lifecycle.\n\n</div>\n\n## installation\n\n```sh\npnpm add @vielzeug/forge\n```\n\n## features\n\n<div class=\"features grid\">\n\n `createform()` — deeply readonly form state with structural sharing and cloned dates\n `field().field()` — typed nested field handles\n flat `{ path, message }` validation issues\n explicit `validate()` and `submit()` timing\n `/dom` — optional element binding\n `/schema` — validator agnostic standard schema adapter\n `/persist` — explicit structural store integration\n `/form data` — browser submission serialization\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 [spell](/spell/) — standard schema compatible validation.\n [vault](/vault/) — durable typed storage for drafts.\n [assay](/assay/) — dom test helpers for form interaction.\n\n<! markdownlint enable md025 md033 md060 >\n",
|
|
495
|
-
"api": " \ntitle: forge — api reference\ndescription: immutable typed form state, flat validation issues, and optional integration helpers.\n \n\n[[toc]]\n\n## api overview\n\n| symbol | purpose | execution mode | common gotcha |\n| | | | |\n| `createform()` | create immutable form and field state | sync | dispose with its owner |\n| `validate()` | run the configured full form validator | async | superseded work returns `aborted` |\n| `submit()` | validate and submit one captured snapshot | async | concurrent submission rejects |\n| `bindfield()` | bind a field to an html element | sync | caller owns returned cleanup |\n| `schemavalidator()` | adapt standard schema validation | async | transform output is not applied |\n| `loadform()` / `saveform()` | explicit structural draft persistence | async | loading will not overwrite newer edits |\n| `toformdata()` | serialize supported values | sync | dotted keys and object arrays reject |\n\n## package entry point\n\n| import | purpose |\n| | |\n| `@vielzeug/forge` | form and field state |\n| `@vielzeug/forge/dom` | `bindfield()` dom binding |\n| `@vielzeug/forge/form data` | `toformdata()` serialization |\n| `@vielzeug/forge/schema` | standard schema validation adapter |\n| `@vielzeug/forge/persist` | explicit draft save/load helpers |\n\n## createform\n\n```ts\nfunction createform<tvalues extends record<string, unknown>>(\n options: formoptions<tvalues>,\n): form<tvalues>\n```\n\n`initialvalues` is required. validation is explicit and returns flat issues.\n\n```ts\nconst form = createform({\n initialvalues: { email: '' },\n validate: (values) =>\n values.email.includes('@') ? undefined : [{ path: ['email'], message: 'invalid email' }],\n});\n```\n\n## form\n\n```ts\ninterface form<tvalues extends record<string, unknown>> {\n readonly value: readonlydeep<tvalues>;\n readonly state: formstate;\n readonly disposed: boolean;\n readonly disposalsignal: abortsignal;\n field<k extends keyof tvalues & string>(key: k): field<tvalues[k]>;\n set(next: tvalues | ((previous: readonlydeep<tvalues>) => tvalues)): void;\n reset(next?: tvalues): void;\n validate(signal?: abortsignal): promise<validationresult>;\n submit<tresult>(handler, signal?: abortsignal): promise<submitresult<tresult>>;\n subscribe(listener, options?: { immediate?: boolean }): () => void;\n dispose(): void;\n [symbol.dispose](): void;\n}\n```\n\n`validate()` and `submit()` return `{ status: 'valid' }`, `{ status: 'invalid', issues }`, or `{ status: 'aborted' }`. successful submission returns `{ status: 'ok', value }`. submission handlers receive the exact snapshot that passed validation. `form.state` retains one stable reference until the next transition for framework external store adapters.\n\n```ts\ntype formstate = readonly<{\n formerror: string | undefined;\n haserrors: boolean;\n issues: readonly validationissue[] | undefined;\n submitcount: number;\n submitting: boolean;\n touched: boolean;\n validity: 'invalid' | 'unknown' | 'valid';\n validating: boolean;\n}>;\n```\n\n## field\n\n```ts\ninterface field<value> {\n readonly value: readonlydeep<value>;\n readonly state: fieldstate<value>;\n readonly dirty: boolean;\n readonly touched: boolean;\n readonly error: string | undefined;\n field(keyorindex): field<unknown>;\n set(next: value | ((previous: value) => value)): void;\n reset(): void;\n touch(): void;\n subscribe(listener, options?: { immediate?: boolean }): () => void;\n}\n```\n\nnested `field().field()` handles avoid string paths while preserving type inference.\n\n## value types\n\n```ts\ntype atomic = date | file | blob;\ntype readonlydate = omit<date, extract<keyof date, `set${string}`>>;\ntype readonlydeep<t> = t extends date\n ? readonlydate\n : t extends file | blob\n ? t\n : t extends readonly (infer item)[]\n ? readonly readonlydeep<item>[]\n : t extends record<string, unknown>\n ? { readonly [k in keyof t]: readonlydeep<t[k]> }\n : t;\n```\n\nforge clones valid dates, preserves binary identity, freezes plain containers, and rejects unsupported, circular, sparse, or non finite values.\n\n## validationissue\n\n```ts\ntype validationissue = readonly<{\n path: readonly (string | number)[];\n message: string;\n}>;\n```\n\n`path: []` denotes a form level issue. a field derives its first matching error from the flat issue list.\n\n## bindfield\n\n```ts\nimport { bindfield } from '@vielzeug/forge/dom';\n\nconst stop = bindfield(input, form.field('email'), {\n read: (element) => element.value,\n write: (element, value) => { element.value = value; },\n});\n```\n\nthe binding reads the configured event (`input` by default), marks the field touched on blur, writes external state changes, and returns cleanup.\n\n## schemavalidator\n\n```ts\nimport { schemavalidator } from '@vielzeug/forge/schema';\n\nconst form = createform({\n initialvalues: { email: '' },\n validate: schemavalidator(userschema),\n});\n```\n\naccepts any standard schema compatible validator. portable issues are copied into forge owned immutable `{ path, message }` values. cancellation settles promptly, although the underlying schema operation may continue. successful transformed output is intentionally not written into the form.\n\n## draft persistence\n\n```ts\nimport { loadform, saveform } from '@vielzeug/forge/persist';\n\nawait saveform(form, store, 'drafts', codec);\nawait loadform(form, store, 'drafts', 'profile', codec, { signal });\n```\n\nthe store is structural: it only needs typed `get(table, key)` and `put(table, value)` methods. the codec owns record conversion. the optional signal and form disposal cancel pending work; a load returns `false` when values changed before the record arrived.\n\n## formdata\n\n```ts\nimport { toformdata } from '@vielzeug/forge/form data';\n\nconst body = toformdata(form.value);\n```\n\nflattens nested plain objects to dotted keys and repeats scalar or binary array entries. dots in object keys and nested objects or arrays inside arrays throw `forgeconfigerror` to prevent ambiguous or lossy output.\n\n## errors\n\n `forgeerror` — base package error.\n `forgeconfigerror` — invalid value, path, issue, or serialization configuration.\n `forgedisposederror` — operation attempted after disposal.\n `forgesubmiterror` — concurrent submission attempt.\n `forgevalidationerror` — unexpected validator execution failure, with the original `cause`.\n",
|
|
496
|
-
"usage": " \ntitle: forge — usage guide\ndescription: build, validate, submit, bind, and persist typed forms with forge.\n \n\n[[toc]]\n\n## basic usage\n\n```ts\nimport { createform } from '@vielzeug/forge';\n\nconst form = createform({\n initialvalues: {\n email: '',\n profile: { name: '' },\n },\n});\n```\n\nread `form.value` for the current deeply readonly snapshot. use `set()` for whole form replacement and field handles for focused updates.\n\n```ts\nform.field('email').set('ada@example.com');\nform.field('profile').field('name').set('ada');\n```\n\n## validate explicitly\n\nvalidators return a flat issue list. forge does not choose blur/change/submit policy for the application.\n\n```ts\nconst form = createform({\n initialvalues: { email: '', password: '' },\n validate: (values) => {\n const issues = [];\n if (!values.email.includes('@')) issues.push({ path: ['email'], message: 'invalid email' });\n if (values.password.length < 12) issues.push({ path: ['password'], message: 'use at least 12 characters' });\n return issues.length ? issues : undefined;\n },\n});\n\nconst result = await form.validate();\nif (result.status === 'invalid') console.log(result.issues);\n```\n\na field's `error` is derived from the first issue whose path equals that field path. use `path: []` for form level failures.\n\n## submit\n\n```ts\nconst result = await form.submit(async (values, signal) => {\n const response = await fetch('/profile', {\n body: json.stringify(values),\n method: 'post',\n signal,\n });\n return response.json();\n});\n\nif (result.status === 'ok') console.log(result.value);\n```\n\n`submit()` validates first, passes the exact validated snapshot to the handler, prevents duplicate active submissions, and reports aborts explicitly.\n\n## bind an element\n\n```ts\nimport { bindfield } from '@vielzeug/forge/dom';\n\nconst stop = bindfield(input, form.field('email'), {\n read: (element) => element.value,\n write: (element, value) => { element.value = value; },\n});\n```\n\ncall `stop()` when the element owner unmounts.\n\n## use a standard schema validator\n\n```ts\nimport { schemavalidator } from '@vielzeug/forge/schema';\n\nconst form = createform({\n initialvalues: { email: '' },\n validate: schemavalidator(userschema),\n});\n```\n\nspell schemas implement standard schema directly. forge uses the schema result only for success and issues; transformations do not replace form values. cancellation settles forge promptly even when underlying validator work cannot be stopped.\n\n## persist a draft\n\n```ts\nimport { loadform, saveform } from '@vielzeug/forge/persist';\n\nconst codec = {\n fromrecord: (record) => record.value,\n torecord: (values) => ({ id: 'profile', value: values }),\n};\n\nawait saveform(form, store, 'drafts', codec);\nawait loadform(form, store, 'drafts', 'profile', codec, { signal });\n```\n\npersistence remains explicit. the store owns durability and validation; the codec owns record shape. loading returns `false` rather than overwriting edits made while storage was pending.\n\n## serialize formdata\n\n```ts\nimport { toformdata } from '@vielzeug/forge/form data';\n\nawait fetch('/profile', { body: toformdata(form.value), method: 'post' });\n```\n\nobject keys containing dots and arrays containing nested objects reject instead of producing ambiguous or lossy entries.\n\n## dispose\n\n```ts\nform.dispose();\n```\n\ndisposal settles active forge validation and signals active submission/persistence work and clears subscribers. it does not dispose borrowed validators, stores, or dom elements.\n\n## framework integration\n\ninject an owner scoped form and subscribe to its stable `formstate` snapshot.\n\n::: code group\n\n```tsx [react]\nimport { usesyncexternalstore } from 'react';\nimport type { form } from '@vielzeug/forge';\n\nexport function useformstate<t extends record<string, unknown>>(form: form<t>) {\n return usesyncexternalstore(form.subscribe, () => form.state, () => form.state);\n}\n```\n\n```ts [vue]\nimport { onunmounted, shallowref } from 'vue';\nimport type { form } from '@vielzeug/forge';\n\nexport function useformstate<t extends record<string, unknown>>(form: form<t>) {\n const state = shallowref(form.state);\n const stop = form.subscribe((next) => { state.value = next; }, { immediate: true });\n onunmounted(stop);\n return state;\n}\n```\n\n```ts [svelte]\nimport type { form } from '@vielzeug/forge';\n\nexport const formstate = <t extends record<string, unknown>>(form: form<t>) => ({\n subscribe(run: (state: form<t>['state']) => void) {\n return form.subscribe(run, { immediate: true });\n },\n});\n```\n\n:::\n\nthe owner that creates the form disposes it. framework subscriptions own only their unsubscribe callbacks.\n\n## working with other vielzeug libraries\n\n pass spell schemas to `schemavalidator()` through standard schema.\n pass vault stores directly to `loadform()` and `saveform()`.\n bind ore or native controls with `bindfield()` and owner cleanup.\n\n## best practices\n\n keep validation timing in the ui layer.\n return flat issues rather than shape coupled error trees.\n use field handles instead of string paths.\n decode persisted/network data at its boundary.\n dispose forms with their owner.\n",
|
|
494
|
+
"index": " \ntitle: forge — immutable typed form state\ndescription: framework neutral form and field state with nested handles, flat validation issues, and explicit integration helpers.\npackage: forge\ncategory: state\nkeywords: [forms, validation, fields, immutable, standard schema, formdata]\nexports: [createform, toplainvalues]\nrelated: [spell, vault, assay]\nenvironments: [browser, node, ssr, deno]\n \n\n<! markdownlint disable md025 md033 md060 >\n\n<packagehero package=\"forge\" />\n\n## why forge?\n\nforge owns form state without owning rendering or validation timing. nested field handles keep access typed, flat validation issues compose across arrays and unions, and forge owned validation and persistence operations settle on cancellation.\n\n## quick start\n\n```ts\nimport { createform } from '@vielzeug/forge';\n\nconst form = createform({\n initialvalues: { email: '', profile: { name: '' } },\n validate: (values) =>\n values.email.includes('@') ? undefined : [{ path: ['email'], message: 'invalid email' }],\n});\n\nform.field('profile').field('name').set('ada');\nconst result = await form.validate();\n```\n\n| feature | forge | local component state |\n| | | |\n| bundle size | <packageinfo package=\"forge\" type=\"size\" /> | n/a |\n| nested typed fields | <ore icon name=\"check\" size=\"16\"></ore icon> | manual |\n| flat validation issues | <ore icon name=\"check\" size=\"16\"></ore icon> | manual |\n| abortable validation | <ore icon name=\"check\" size=\"16\"></ore icon> | manual |\n| framework neutral | <ore icon name=\"check\" size=\"16\"></ore icon> | varies |\n| zero third party dependencies | <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 forge when** several fields need coordinated validation, submission, dirty/touched state, nested updates, or explicit persistence.\n\n**use local state when** a form has only one or two fields and no shared validation lifecycle.\n\n</div>\n\n## installation\n\n```sh\npnpm add @vielzeug/forge\n```\n\n## features\n\n<div class=\"features grid\">\n\n `createform()` — deeply readonly form state with structural sharing and cloned dates\n `field().field()` — typed nested field handles\n `patch()` — shallow top level merge without spread casts\n `normalize` + `toplainvalues()` — hold domain model classes in form state\n flat `{ path, message }` validation issues\n explicit `validate()` and `submit()` timing\n `/dom` — optional element binding\n `/schema` — validator agnostic standard schema adapter\n `/persist` — explicit structural store integration\n `/form data` — browser submission serialization\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 [spell](/spell/) — standard schema compatible validation.\n [vault](/vault/) — durable typed storage for drafts.\n [assay](/assay/) — dom test helpers for form interaction.\n\n<! markdownlint enable md025 md033 md060 >\n",
|
|
495
|
+
"api": " \ntitle: forge — api reference\ndescription: immutable typed form state, flat validation issues, and optional integration helpers.\n \n\n[[toc]]\n\n## api overview\n\n| symbol | purpose | execution mode | common gotcha |\n| | | | |\n| `createform()` | create immutable form and field state | sync | dispose with its owner |\n| `toplainvalues()` | flatten class instances to the plain value shape | sync | drops circular branches |\n| `validate()` | run the configured full form validator | async | superseded work returns `aborted` |\n| `submit()` | validate and submit one captured snapshot | async | concurrent submission rejects |\n| `bindfield()` | bind a field to an html element | sync | caller owns returned cleanup |\n| `schemavalidator()` | adapt standard schema validation | async | transform output is not applied |\n| `loadform()` / `saveform()` | explicit structural draft persistence | async | loading will not overwrite newer edits |\n| `toformdata()` | serialize supported values | sync | dotted keys and object arrays reject |\n\n## package entry point\n\n| import | purpose |\n| | |\n| `@vielzeug/forge` | form and field state |\n| `@vielzeug/forge/dom` | `bindfield()` dom binding |\n| `@vielzeug/forge/form data` | `toformdata()` serialization |\n| `@vielzeug/forge/schema` | standard schema validation adapter |\n| `@vielzeug/forge/persist` | explicit draft save/load helpers |\n\n## createform\n\n```ts\nfunction createform<tvalues extends record<string, unknown>>(\n options: formoptions<tvalues>,\n): form<tvalues>\n```\n\n`initialvalues` is required. validation is explicit and returns flat issues.\n\n```ts\nconst form = createform({\n initialvalues: { email: '' },\n validate: (values) =>\n values.email.includes('@') ? undefined : [{ path: ['email'], message: 'invalid email' }],\n});\n```\n\n`normalize` runs before value validation at every write boundary (`initialvalues`, `set`, `patch`, `reset`, `field().set`), so a caller that normalizes once at `createform` stays correct everywhere. pair it with `toplainvalues()` to hold domain model classes in form state.\n\n```ts\nimport { createform, toplainvalues } from '@vielzeug/forge';\n\nconst form = createform({ initialvalues: { user: new usermodel() }, normalize: toplainvalues });\n\nform.reset({ user: new usermodel() }); // same normalization as init\n```\n\n## toplainvalues\n\n```ts\nfunction toplainvalues<t>(value: t): t;\n```\n\ndeeply converts a value into the plain shape forge accepts: primitives pass through, `date` is cloned, `file`/`blob` keep identity, arrays map, and class instances flatten to their own enumerable entries. unsafe keys (`__proto__`, `constructor`, `prototype`) are dropped, so the output always passes value validation. the returned type parameter is a convenience cast — the flattened shape differs from the input type whenever class instances are present.\n\nlossy conversions replace the error an un normalized write would throw: `map`, `set`, and other keyless built ins flatten to `{}`; sparse array slots are dropped, shifting later indexes; `nan`, `infinity`, functions, and circular branches become `undefined`. normalize model classes, not arbitrary graphs.\n\n```ts\nimport { createform, toplainvalues } from '@vielzeug/forge';\n\nclass usermodel {\n email = '';\n name = '';\n}\n\ntoplainvalues(new usermodel()); // { email: '', name: '' }\nconst form = createform({ initialvalues: { user: new usermodel() }, normalize: toplainvalues });\n```\n\n## form\n\n```ts\ninterface form<tvalues extends record<string, unknown>> {\n readonly value: readonlydeep<tvalues>;\n readonly state: formstate;\n readonly disposed: boolean;\n readonly disposalsignal: abortsignal;\n field<k extends keyof tvalues & string>(key: k): field<tvalues[k]>;\n set(next: tvalues | ((previous: readonlydeep<tvalues>) => tvalues)): void;\n patch(next: partial<tvalues>): void;\n patch(updater: (previous: readonlydeep<tvalues>) => partial<tvalues>): void;\n reset(next?: tvalues): void;\n validate(signal?: abortsignal): promise<validationresult>;\n submit<tresult>(handler, signal?: abortsignal): promise<submitresult<tresult>>;\n subscribe(listener, options?: { immediate?: boolean }): () => void;\n dispose(): void;\n [symbol.dispose](): void;\n}\n```\n\n`validate()` and `submit()` return `{ status: 'valid' }`, `{ status: 'invalid', issues }`, or `{ status: 'aborted' }`. successful submission returns `{ status: 'ok', value }`. submission handlers receive the exact snapshot that passed validation. `form.state` retains one stable reference until the next transition for framework external store adapters.\n\n`patch()` shallow merges top level keys — `form.patch({ count: 1 })` equals `form.set((prev) => ({ ...prev, count: 1 }))` without the `as t` cast a spread requires. nested values replace by identity; there is no deep merge.\n\n```ts\ntype formstate = readonly<{\n formerror: string | undefined;\n haserrors: boolean;\n issues: readonly validationissue[] | undefined;\n submitcount: number;\n submitting: boolean;\n touched: boolean;\n validity: 'invalid' | 'unknown' | 'valid';\n validating: boolean;\n}>;\n```\n\n## field\n\n```ts\ninterface field<value> {\n readonly value: readonlydeep<value>;\n readonly state: fieldstate<value>;\n readonly dirty: boolean;\n readonly touched: boolean;\n readonly error: string | undefined;\n field(keyorindex): field<unknown>;\n set(next: value | ((previous: value) => value)): void;\n reset(): void;\n touch(): void;\n subscribe(listener, options?: { immediate?: boolean }): () => void;\n}\n```\n\nnested `field().field()` handles avoid string paths while preserving type inference.\n\n## value types\n\n```ts\ntype atomic = date | file | blob;\ntype readonlydate = omit<date, extract<keyof date, `set${string}`>>;\ntype readonlydeep<t> = t extends date\n ? readonlydate\n : t extends file | blob\n ? t\n : t extends readonly (infer item)[]\n ? readonly readonlydeep<item>[]\n : t extends record<string, unknown>\n ? { readonly [k in keyof t]: readonlydeep<t[k]> }\n : t;\n```\n\nforge clones valid dates, preserves binary identity, freezes plain containers, and rejects unsupported, circular, sparse, or non finite values.\n\n## validationissue\n\n```ts\ntype validationissue = readonly<{\n path: readonly (string | number)[];\n message: string;\n}>;\n```\n\n`path: []` denotes a form level issue. a field derives its first matching error from the flat issue list.\n\n## bindfield\n\n```ts\nimport { bindfield } from '@vielzeug/forge/dom';\n\nconst stop = bindfield(input, form.field('email'), {\n read: (element) => element.value,\n write: (element, value) => { element.value = value; },\n});\n```\n\nthe binding reads the configured event (`input` by default), marks the field touched on blur, writes external state changes, and returns cleanup.\n\n## schemavalidator\n\n```ts\nimport { schemavalidator } from '@vielzeug/forge/schema';\n\nconst form = createform({\n initialvalues: { email: '' },\n validate: schemavalidator(userschema),\n});\n```\n\naccepts any standard schema compatible validator. portable issues are copied into forge owned immutable `{ path, message }` values. cancellation settles promptly, although the underlying schema operation may continue. successful transformed output is intentionally not written into the form.\n\n## draft persistence\n\n```ts\nimport { loadform, saveform } from '@vielzeug/forge/persist';\n\nawait saveform(form, store, 'drafts', codec);\nawait loadform(form, store, 'drafts', 'profile', codec, { signal });\n```\n\nthe store is structural: it only needs typed `get(table, key)` and `put(table, value)` methods. the codec owns record conversion. the optional signal and form disposal cancel pending work; a load returns `false` when values changed before the record arrived.\n\n## formdata\n\n```ts\nimport { toformdata } from '@vielzeug/forge/form data';\n\nconst body = toformdata(form.value);\n```\n\nflattens nested plain objects to dotted keys and repeats scalar or binary array entries. dots in object keys and nested objects or arrays inside arrays throw `forgeconfigerror` to prevent ambiguous or lossy output.\n\n## errors\n\n `forgeerror` — base package error.\n `forgeconfigerror` — invalid value, path, issue, or serialization configuration.\n `forgedisposederror` — operation attempted after disposal.\n `forgesubmiterror` — concurrent submission attempt.\n `forgevalidationerror` — unexpected validator execution failure, with the original `cause`.\n",
|
|
496
|
+
"usage": " \ntitle: forge — usage guide\ndescription: build, validate, submit, bind, and persist typed forms with forge.\n \n\n[[toc]]\n\n## basic usage\n\n```ts\nimport { createform } from '@vielzeug/forge';\n\nconst form = createform({\n initialvalues: {\n email: '',\n profile: { name: '' },\n },\n});\n```\n\nread `form.value` for the current deeply readonly snapshot. use `set()` for whole form replacement and field handles for focused updates.\n\n```ts\nform.field('email').set('ada@example.com');\nform.field('profile').field('name').set('ada');\n```\n\n`patch()` shallow merges top level keys without the cast a spread `set()` needs:\n\n```ts\nform.patch({ email: 'ada@example.com' }); // siblings keep their values\n```\n\n## hold domain model classes\n\nforge values must be plain json safe data. when your app stores class instances (models, dtos with behavior), pass `normalize: toplainvalues` once at `createform`; it runs before validation at every write boundary — init, `set`, `patch`, `reset`, and nested `field().set` — so a reset with fresh models behaves like init.\n\n```ts\nimport { createform, toplainvalues } from '@vielzeug/forge';\n\nclass providerusermodel {\n id: number;\n email = '';\n constructor(id: number) { this.id = id; }\n}\n\nconst form = createform<{ users: providerusermodel[] }>({\n initialvalues: { users: [new providerusermodel(1)] },\n normalize: toplainvalues,\n});\n\nform.field('users').field(0).set(new providerusermodel(2)); // flattened like init\nform.reset({ users: [new providerusermodel(3)] }); // flattened like init\n// rebuild models at submit:\nawait form.submit((values) => send(values.users.map((u) => new providerusermodel(u.id))));\n```\n\nwithout `normalize`, a rejected write throws `forgeconfigerror` naming the offending path (for example `list[0]`).\n\n## validate explicitly\n\nvalidators return a flat issue list. forge does not choose blur/change/submit policy for the application.\n\n```ts\nconst form = createform({\n initialvalues: { email: '', password: '' },\n validate: (values) => {\n const issues = [];\n if (!values.email.includes('@')) issues.push({ path: ['email'], message: 'invalid email' });\n if (values.password.length < 12) issues.push({ path: ['password'], message: 'use at least 12 characters' });\n return issues.length ? issues : undefined;\n },\n});\n\nconst result = await form.validate();\nif (result.status === 'invalid') console.log(result.issues);\n```\n\na field's `error` is derived from the first issue whose path equals that field path. use `path: []` for form level failures.\n\n## submit\n\n```ts\nconst result = await form.submit(async (values, signal) => {\n const response = await fetch('/profile', {\n body: json.stringify(values),\n method: 'post',\n signal,\n });\n return response.json();\n});\n\nif (result.status === 'ok') console.log(result.value);\n```\n\n`submit()` validates first, passes the exact validated snapshot to the handler, prevents duplicate active submissions, and reports aborts explicitly.\n\n## bind an element\n\n```ts\nimport { bindfield } from '@vielzeug/forge/dom';\n\nconst stop = bindfield(input, form.field('email'), {\n read: (element) => element.value,\n write: (element, value) => { element.value = value; },\n});\n```\n\ncall `stop()` when the element owner unmounts.\n\n## use a standard schema validator\n\n```ts\nimport { schemavalidator } from '@vielzeug/forge/schema';\n\nconst form = createform({\n initialvalues: { email: '' },\n validate: schemavalidator(userschema),\n});\n```\n\nspell schemas implement standard schema directly. forge uses the schema result only for success and issues; transformations do not replace form values. cancellation settles forge promptly even when underlying validator work cannot be stopped.\n\n## persist a draft\n\n```ts\nimport { loadform, saveform } from '@vielzeug/forge/persist';\n\nconst codec = {\n fromrecord: (record) => record.value,\n torecord: (values) => ({ id: 'profile', value: values }),\n};\n\nawait saveform(form, store, 'drafts', codec);\nawait loadform(form, store, 'drafts', 'profile', codec, { signal });\n```\n\npersistence remains explicit. the store owns durability and validation; the codec owns record shape. loading returns `false` rather than overwriting edits made while storage was pending.\n\n## serialize formdata\n\n```ts\nimport { toformdata } from '@vielzeug/forge/form data';\n\nawait fetch('/profile', { body: toformdata(form.value), method: 'post' });\n```\n\nobject keys containing dots and arrays containing nested objects reject instead of producing ambiguous or lossy entries.\n\n## dispose\n\n```ts\nform.dispose();\n```\n\ndisposal settles active forge validation and signals active submission/persistence work and clears subscribers. it does not dispose borrowed validators, stores, or dom elements.\n\n## framework integration\n\ninject an owner scoped form and subscribe to its stable `formstate` snapshot.\n\n::: code group\n\n```tsx [react]\nimport { usesyncexternalstore } from 'react';\nimport type { form } from '@vielzeug/forge';\n\nexport function useformstate<t extends record<string, unknown>>(form: form<t>) {\n return usesyncexternalstore(form.subscribe, () => form.state, () => form.state);\n}\n```\n\nwhen the component is the form's sole owner, create it in a ref and skip explicit disposal — writes on a disposed form throw `forgedisposederror`, which is what a strictmode double mount hits when effect cleanup disposes a ref held form that survives the remount. garbage collection reclaims a sole subscriber form; dispose explicitly only when external subscribers or in flight work need teardown.\n\n```ts [vue]\nimport { onunmounted, shallowref } from 'vue';\nimport type { form } from '@vielzeug/forge';\n\nexport function useformstate<t extends record<string, unknown>>(form: form<t>) {\n const state = shallowref(form.state);\n const stop = form.subscribe((next) => { state.value = next; }, { immediate: true });\n onunmounted(stop);\n return state;\n}\n```\n\n```ts [svelte]\nimport type { form } from '@vielzeug/forge';\n\nexport const formstate = <t extends record<string, unknown>>(form: form<t>) => ({\n subscribe(run: (state: form<t>['state']) => void) {\n return form.subscribe(run, { immediate: true });\n },\n});\n```\n\n:::\n\nthe owner that creates the form disposes it. framework subscriptions own only their unsubscribe callbacks.\n\n## working with other vielzeug libraries\n\n pass spell schemas to `schemavalidator()` through standard schema.\n pass vault stores directly to `loadform()` and `saveform()`.\n bind ore or native controls with `bindfield()` and owner cleanup.\n\n## best practices\n\n keep validation timing in the ui layer.\n return flat issues rather than shape coupled error trees.\n use field handles instead of string paths.\n decode persisted/network data at its boundary.\n dispose forms with their owner.\n",
|
|
497
497
|
"examples": " \ntitle: forge — examples\ndescription: practical immutable form recipes.\n \n\n## examples\n\n [login form](./examples/login form.md)\n [conditional values](./examples/form with conditional fields.md)\n [dynamic arrays](./examples/dynamic form fields.md)\n [contact form with file upload](./examples/contact form with file upload.md)\n [registration form](./examples/registration form.md)\n [multi step wizard](./examples/multi step wizard.md)\n [search form with debounce](./examples/search form with debounce.md)\n"
|
|
498
498
|
},
|
|
499
499
|
"examples": [
|
|
@@ -530,12 +530,12 @@
|
|
|
530
530
|
"text": "nested field handles import { createform } from '@vielzeug/forge'\n\nconst form = createform({ initialvalues: { shipping: { city: '', street: '' } } })\nconst shipping = form.field('shipping')\n\nshipping.field('street').set('123 main street')\nshipping.field('city').set('portland')\nconsole.log(shipping.value)\nconsole.log(form.value.shipping)"
|
|
531
531
|
}
|
|
532
532
|
],
|
|
533
|
-
"exports": "createform",
|
|
533
|
+
"exports": "createform toplainvalues",
|
|
534
534
|
"keywords": "forms validation fields immutable standard schema formdata",
|
|
535
535
|
"name": "@vielzeug/forge",
|
|
536
536
|
"related": "spell vault assay",
|
|
537
537
|
"slug": "forge",
|
|
538
|
-
"source": "export { forgeconfigerror, forgedisposederror, forgeerror, forgesubmiterror, forgevalidationerror } from './errors.js';\nexport { createform } from './form.js';\nexport * from './types.js';\n"
|
|
538
|
+
"source": "export { forgeconfigerror, forgedisposederror, forgeerror, forgesubmiterror, forgevalidationerror } from './errors.js';\nexport { createform } from './form.js';\nexport { toplainvalues } from './plain.js';\nexport * from './types.js';\n"
|
|
539
539
|
},
|
|
540
540
|
{
|
|
541
541
|
"category": "input",
|
|
@@ -793,6 +793,28 @@
|
|
|
793
793
|
"slug": "lingua",
|
|
794
794
|
"source": "export { catalogkeys } from './catalog';\nexport {\n linguadisposederror,\n linguaerror,\n linguainvalidcatalogerror,\n linguainvalidlocaleerror,\n linguainvalidpluralcounterror,\n linguainvalidstateerror,\n linguamissingcatalogerror,\n linguamissingkeyerror,\n linguamissingvalueerror,\n} from './errors';\nexport { createi18n, type i18n, type i18nsnapshot } from './i18n';\nexport { createtranslator, type translator } from './translator';\nexport type {\n catalog,\n catalognode,\n catalogs,\n i18noptions,\n locale,\n messagekey,\n missinghandler,\n missinginfo,\n missingstrategy,\n part,\n pluralcategory,\n pluralkey,\n pluralmessage,\n pluraloptions,\n subscribeoptions,\n textkey,\n textpart,\n translateoptions,\n translationstate,\n translatoroptions,\n valuepart,\n values,\n} from './types';\n"
|
|
795
795
|
},
|
|
796
|
+
{
|
|
797
|
+
"category": "webrtc",
|
|
798
|
+
"description": "backendless peer to peer session transport over webrtc data channels with manual pairing and host authoritative star topology",
|
|
799
|
+
"docs": {
|
|
800
|
+
"index": " \ntitle: mesh — backendless peer to peer sessions\ndescription: \"backendless peer to peer session transport over webrtc data channels with manual pairing and host authoritative star topology\"\npackage: mesh\ncategory: webrtc\nkeywords: [webrtc, p2p, peer to peer, datachannel, lan, pairing, backendless, signaling free]\nexports: [createmeshhost, createmeshguest, meshcodec, meshprotocol]\nrelated: [pulse, arsenal, herald]\nenvironments: [browser, node]\n \n\n<! markdownlint disable md025 md033 md060 >\n\n<packagehero package=\"mesh\" />\n\n## why mesh?\n\nbrowsers cannot discover peers on a lan, so sharing live state between nearby devices has always meant running a server. mesh pairs devices out of band — copy/paste, `navigator.share`, or qr via `meshqrcodec` — and then moves typed messages directly over a webrtc data channel with no backend involved.\n\n```ts\n// before\nconst pc = new rtcpeerconnection();\nconst dc = pc.createdatachannel('sync');\nawait pc.setlocaldescription(await pc.createoffer());\n// …wait for ice, hand the sdp to the other device, wire expiry, timeouts,\n// message framing, and dedupe yourself — per guest\n\n// after\nconst host = createmeshhost<appprotocol>();\nconst invitation = await host.createinvitation();\nsharetext(meshcodec.encode(invitation)); // copy/paste or navigator.share\nconst peer = await host.acceptanswer(meshcodec.decode(answertext));\nhost.send(peer.id, 'snapshot', { rev: 1 });\n```\n\n| feature | mesh | `pulse` (websocket) | raw `rtcpeerconnection` |\n| | | | |\n| bundle size | <packageinfo package=\"mesh\" type=\"size\" /> | <packageinfo package=\"pulse\" type=\"size\" /> | 0 b |\n| runtime dependencies | `@vielzeug/arsenal` | <ore icon name=\"check\" size=\"16\"></ore icon> none | <ore icon name=\"check\" size=\"16\"></ore icon> none |\n| works without a server | <ore icon name=\"check\" size=\"16\"></ore icon> | <ore icon name=\"x\" size=\"16\"></ore icon> | <ore icon name=\"check\" size=\"16\"></ore icon> |\n| typed message protocol | <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| multi peer sessions | star, host authoritative | server rooms | manual per peer wiring |\n| pairing safety | proof + ttl + approval hook | server auth | <ore icon name=\"x\" size=\"16\"></ore icon> |\n\n<div class=\"decision callout\">\n\n**use mesh when** a small group of devices on one network must exchange live state and you cannot — or do not want to — run a server.\n\n**consider `pulse` when** you have a backend anyway, need internet wide reach, or want server managed rooms and presence.\n\n</div>\n\n## limitations\n\nmesh is deliberately small. it does **not** provide:\n\n automatic lan discovery — every pairing is an explicit out of band exchange.\n guest to guest relay — guests only ever talk to the host.\n reconnection across page reloads — a reload destroys the `rtcpeerconnection`; the consumer re pairs and re associates the stable `peerid`.\n crdts or conflict resolution — payload semantics are yours.\n turn — there is no guarantee peers connect off lan.\n persistence, or any ui.\n\ntwo operational caveats matter in practice: the host page must stay open for the session to live, and client isolated wi fi, vpns, or strict firewalls can block even same lan pairing.\n\n## installation\n\n::: code group\n\n```sh [pnpm]\npnpm add @vielzeug/mesh\n```\n\n```sh [npm]\nnpm install @vielzeug/mesh\n```\n\n```sh [yarn]\nyarn add @vielzeug/mesh\n```\n\n:::\n\n## quick start\n\npair one host and one guest, then exchange typed messages. pairing requires a secure context (`https:` or `localhost`) for `rtcpeerconnection`.\n\n```ts\nimport { createmeshguest, createmeshhost, meshcodec, type meshprotocol } from '@vielzeug/mesh';\n\n// the consumer declares both directions of the conversation.\ninterface appprotocol extends meshprotocol {\n tohost: { note: string };\n toguest: { ack: { ok: boolean } };\n}\n\n// host side\nconst host = createmeshhost<appprotocol>();\nhost.on('note', (message) => host.send(message.peerid, 'ack', { ok: true }));\n\nconst invitation = await host.createinvitation();\nconst invitationtext = meshcodec.encode(invitation); // hand to the guest\n\n// guest side (other device)\nconst guest = createmeshguest<appprotocol>();\nconst answer = await guest.acceptinvitation(meshcodec.decode(invitationtext));\nconst answertext = meshcodec.encode(answer); // hand back to the host\n\n// host accepts the answer; resolves once the channel is open\ntry {\n await host.acceptanswer(meshcodec.decode(answertext));\n guest.send('note', 'hello over webrtc');\n} finally {\n host.dispose();\n guest.dispose();\n}\n```\n\n## features\n\n<div class=\"features grid\">\n\n **`createmeshhost`** — host authoritative star sessions; one `rtcpeerconnection` per guest.\n **`createmeshguest`** — single host guest node with its own lifecycle.\n **`meshcodec`** — compact base64url encoding for pairing payloads; pure and replaceable.\n **typed protocol** — declare `tohost`/`toguest` maps once; `send`, `on`, and `broadcast` stay type checked.\n **manual pairing** — invitation/answer exchange over copy/paste or `navigator.share`; proof of possession, ttl, and an `approvepeer` hook.\n **`tap` observability** — status transitions, ice state, byte counts, and rejections without affecting behavior.\n **`peers`, `onpeer`, `kick`** — host side peer inventory and lifecycle.\n **injectable `rtc`** — every webrtc object comes from a factory, so tests run fully in memory.\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 [pulse](/pulse/) — typed websocket sessions when a server is available.\n [arsenal](/arsenal/) — the utilities mesh builds on for ids, randomness, and hashing.\n [herald](/herald/) — in process event bus for wiring mesh messages into your app.\n\n</div>\n\n<! markdownlint enable md025 md033 md060 >\n",
|
|
801
|
+
"api": " \ntitle: mesh — api reference\ndescription: public api of @vielzeug/mesh.\n \n\n[[toc]]\n\n## api overview\n\n| symbol | purpose | execution | common gotcha |\n| | | | |\n| `createmeshhost` | host authoritative session node; pairs one `rtcpeerconnection` per guest | sync | `createinvitation`/`acceptanswer` are async; calls after `dispose()` throw `meshdisposederror` |\n| `createmeshguest` | guest node that pairs with one host | sync | second `acceptinvitation` throws `meshpairingerror` |\n| `meshcodec` | base64url encode/decode for pairing payloads | sync | `decode` throws `meshpairingerror` on malformed text or wrong `v` |\n| `meshqrcodec` | deflate raw + base64url codec for qr sized payloads | async | `encode` throws `meshunsupportederror` without `compressionstream` |\n| `meshprotocol` | declares the `tohost`/`toguest` message maps | — | extend it; the maps themselves stay plain records |\n| `mesherror` | base class for all mesh errors | — | `instanceof mesherror` catches every mesh originated error |\n\n## package entry point\n\n| import | purpose |\n| | |\n| `@vielzeug/mesh` | complete public mesh api |\n\n## factory functions\n\n### createmeshhost()\n\n```ts\nfunction createmeshhost<p extends meshprotocol>(options?: meshhostoptions): meshhost<p>;\n```\n\nreturns a host node in status `'idle'`. creating it never touches webrtc — the first `createinvitation()` does.\n\n#### options (`meshhostoptions`)\n\n| option | type | default | purpose |\n| | | | |\n| `iceservers` | `readonly rtciceserver[]` | `[]` | passed to the peer connection; mesh ships no stun/turn. |\n| `channel` | `meshchanneloptions` | `{ ordered: true, label: 'mesh' }` | data channel init mapped to `rtcdatachannelinit`. |\n| `maxmessagebytes` | `number` | `65_536` | serialized message cap, both directions. |\n| `invitationttlms` | `number` | `300_000` | invitation validity window. |\n| `icegatheringtimeoutms` | `number` | `5_000` | ice gathering cap; also bounds the channel open wait. |\n| `serialize` / `deserialize` | functions | `json.stringify` / `json.parse` | message envelope serialization. |\n| `rtc` | `meshrtcfactory` | `globalthis.rtcpeerconnection` | injection point for tests and non browser runtimes. |\n| `clock` | `() => number` | `date.now` | ttls and message timestamps. |\n| `random` | `randomsource` | `crypto.getrandomvalues` | ids and secrets. |\n| `signal` | `abortsignal` | — | disposes the node on abort. |\n| `approvepeer` | `(peer: meshpeerinfo) => boolean \\| promise<boolean>` | approve | runs after proof verification; `false` rejects with `meshpairingerror`. |\n\n#### example\n\n```ts\nimport { createmeshhost, meshcodec } from '@vielzeug/mesh';\n\nconst host = createmeshhost<appprotocol>({ approvepeer: (peer) => peer.name !== undefined });\nconst invitation = await host.createinvitation();\n```\n\n \n\n### createmeshguest()\n\n```ts\nfunction createmeshguest<p extends meshprotocol>(options?: meshguestoptions): meshguest<p>;\n```\n\nreturns a guest node in status `'idle'`. `meshguestoptions` is `meshoptions` — the shared option table above without `approvepeer`.\n\n#### example\n\n```ts\nimport { createmeshguest, meshcodec } from '@vielzeug/mesh';\n\nconst guest = createmeshguest<appprotocol>();\nconst answer = await guest.acceptinvitation(meshcodec.decode(invitationtext), { name: 'sam' });\n```\n\n## host surface\n\n`meshhost<p>` extends `meshnode`.\n\n| member | signature | purpose |\n| | | |\n| `createinvitation` | `(meta?: { name?: string }) => promise<meshinvitation>` | single use invitation for one guest; repeatable — one per guest. |\n| `acceptanswer` | `(answer: meshanswer) => promise<meshpeer>` | verifies proof + approval, then resolves once the channel opens. rejects `meshpairingerror` (unknown/expired/duplicate session, proof mismatch, refusal) or `meshtimeouterror`. |\n| `peers` | `readonlymap<string, meshpeer>` | live peer inventory keyed by peer id. |\n| `send` | `<k extends keyof p['toguest'] & string>(peerid: string, type: k, payload: p['toguest'][k]) => void` | typed unicast; throws `meshconnectionerror` for unknown/unconnected peers, `meshpayloaderror` over the cap. |\n| `broadcast` | `<k extends keyof p['toguest'] & string>(type: k, payload: p['toguest'][k], options?: { except?: readonly string[] }) => void` | sends to every connected peer not in `except`. |\n| `on` | `<k extends keyof p['tohost'] & string>(type: k, handler: (message: meshinbound<p['tohost'][k]>) => void) => unsubscribe` | typed inbound subscription from any guest. |\n| `onpeer` | `(handler: (event: meshpeerevent) => void) => unsubscribe` | `'joined'` / `'left'` / `'status change'` per peer. |\n| `kick` | `(peerid: string, reason?: string) => void` | disconnects a peer; the guest observes `'disconnected'`. |\n\n## guest surface\n\n`meshguest<p>` extends `meshnode`.\n\n| member | signature | purpose |\n| | | |\n| `acceptinvitation` | `(invitation: meshinvitation, meta?: { name?: string }) => promise<meshanswer>` | consumes an invitation; the returned answer goes back to the host out of band. throws `meshpairingerror` on expired/malformed input or when already paired. |\n| `host` | `meshpeer \\| null` | the host peer once pairing started; its `id` is the invitation's `sessionid`. |\n| `send` | `<k extends keyof p['tohost'] & string>(type: k, payload: p['tohost'][k]) => void` | typed send to the host; throws `meshconnectionerror` when unpaired. |\n| `on` | `<k extends keyof p['toguest'] & string>(type: k, handler: (message: meshinbound<p['toguest'][k]>) => void) => unsubscribe` | typed inbound subscription from the host. |\n\n## shared surface\n\n`meshnode` members common to host and guest.\n\n| member | signature | purpose |\n| | | |\n| `id` | `string` | this node's id. |\n| `status` | `meshstatus` | current lifecycle state. |\n| `disposed` | `boolean` | whether the node is permanently disposed. |\n| `disposalsignal` | `abortsignal` | aborted by `dispose()`. |\n| `dispose` | `() => void` | idempotent teardown: closes every connection, aborts `disposalsignal`, emits `'dispose'`, detaches tappers. |\n| `[symbol.dispose]` | `() => void` | enables `using` declarations. |\n| `tap` | `(handler: (event: meshevent) => void, options?: { signal?: abortsignal }) => unsubscribe` | observability stream; handler errors are swallowed. |\n\n## codec\n\n```ts\nconst meshcodec: {\n encode(payload: meshinvitation | meshanswer): string;\n decode(text: string): meshinvitation | meshanswer;\n};\n```\n\ncompact json → base64url for pairing payloads, kept pure so other encodings can slot in later. `decode` validates shape and `v`, throwing `meshpairingerror` on malformed text, wrong version, or unrecognized payload.\n\n```ts\nconst text = meshcodec.encode(await host.createinvitation());\nconst answer = await guest.acceptinvitation(meshcodec.decode(text));\nawait host.acceptanswer(meshcodec.decode(meshcodec.encode(answer)));\n```\n\n### `meshqrcodec`\n\n```ts\nconst meshqrcodec: {\n encode(payload: meshinvitation | meshanswer): promise<string>;\n decode(text: string): promise<meshinvitation | meshanswer>;\n};\n```\n\nqr oriented variant: deflate raw compresses the json payload, then base64url encodes it with an `mq1.` prefix. `decode` accepts both `mq1.*` and plain `meshcodec` output, so camera scans and paste fallbacks share one path.\n\n```ts\nconst invitationtext = await meshqrcodec.encode(await host.createinvitation()); // \"mq1.…\"\nconst answer = await host.acceptanswer(await meshqrcodec.decode(scannedtext));\n```\n\n`encode` throws `meshunsupportederror` where `compressionstream` is missing; `decode` throws `meshpairingerror` on corrupt base64/deflate/json and `meshunsupportederror` where `decompressionstream` is missing.\n\n## types\n\n### protocol\n\n```ts\ninterface meshprotocol {\n readonly tohost: meshmessagemap; // messages a guest may send\n readonly toguest: meshmessagemap; // messages the host may send\n}\n\ntype meshmessagemap = record<string, unknown>;\n```\n\n### pairing payloads\n\n```ts\ninterface meshinvitation {\n readonly v: 1;\n readonly sessionid: string; // 128 bit, base64url\n readonly secret: string; // 256 bit, base64url\n readonly expiresat: number; // epoch ms\n readonly sdp: string; // host offer, candidates inlined\n}\n\ninterface meshanswer {\n readonly v: 1;\n readonly sessionid: string;\n readonly proof: string; // hmac sha 256 of the answer sdp keyed by secret\n readonly peer: { readonly id: string; readonly name?: string };\n readonly sdp: string;\n}\n```\n\n### peers, status, messages\n\n```ts\ntype meshstatus = 'idle' | 'pairing' | 'connecting' | 'connected' | 'disconnected' | 'failed' | 'disposed';\n\ninterface meshpeerinfo {\n readonly id: string;\n readonly name?: string;\n}\n\ninterface meshpeer extends meshpeerinfo {\n readonly status: meshstatus;\n readonly role: 'host' | 'guest';\n}\n\ntype meshpeerevent =\n | { readonly type: 'joined'; readonly peer: meshpeer }\n | { readonly type: 'left'; readonly peer: meshpeer; readonly reason?: string }\n | { readonly type: 'status change'; readonly peer: meshpeer };\n\ninterface meshinbound<t> {\n readonly peerid: string;\n readonly messageid: string;\n readonly sentat: number;\n readonly payload: t;\n}\n\ntype unsubscribe = () => void;\n```\n\n### events\n\n```ts\ntype meshevent =\n | { readonly type: 'status change'; readonly peerid: string | null; readonly status: meshstatus }\n | { readonly type: 'invitation created' | 'invitation expired'; readonly sessionid: string }\n | { readonly type: 'peer approved' | 'peer rejected' | 'peer joined' | 'peer left'; readonly peerid: string; readonly reason?: string }\n | { readonly type: 'message sent' | 'message received'; readonly peerid: string; readonly messagetype: string; readonly bytes: number }\n | { readonly type: 'message rejected'; readonly peerid: string; readonly reason: 'too large' | 'duplicate' | 'malformed' | 'unknown type' }\n | { readonly type: 'ice state'; readonly peerid: string; readonly state: string }\n | { readonly type: 'security downgrade'; readonly reason: string }\n | { readonly type: 'error'; readonly error: mesherror }\n | { readonly type: 'dispose' };\n```\n\n`peerid: null` on `'status change'` marks a node level transition; a string marks a per peer one.\n\n### options\n\n```ts\ninterface meshchanneloptions {\n readonly ordered?: boolean;\n readonly maxretransmits?: number;\n readonly label?: string;\n}\n\ninterface meshoptions {\n readonly iceservers?: readonly rtciceserver[];\n readonly channel?: meshchanneloptions;\n readonly maxmessagebytes?: number;\n readonly invitationttlms?: number;\n readonly icegatheringtimeoutms?: number;\n readonly serialize?: (value: unknown) => string;\n readonly deserialize?: (text: string) => unknown;\n readonly rtc?: meshrtcfactory;\n readonly clock?: () => number;\n readonly random?: randomsource;\n readonly signal?: abortsignal;\n}\n\ninterface meshhostoptions extends meshoptions {\n readonly approvepeer?: (peer: meshpeerinfo) => boolean | promise<boolean>;\n}\n\ntype meshguestoptions = meshoptions;\n```\n\n### webrtc injection\n\nminimal structural subsets — the surface mesh actually calls, no more. implement them over `wrtc` or a test double to run outside a browser.\n\n```ts\ninterface meshrtcfactory {\n createpeerconnection(config: rtcconfiguration): rtcpeerconnectionlike;\n}\n\ninterface rtcpeerconnectionlike {\n readonly connectionstate: string;\n readonly iceconnectionstate: string;\n readonly icegatheringstate: string;\n readonly localdescription: rtcsessiondescriptionlike | null;\n createdatachannel(label: string, init?: { ordered?: boolean; maxretransmits?: number }): rtcdatachannellike;\n createoffer(): promise<rtcsessiondescriptionlike>;\n createanswer(): promise<rtcsessiondescriptionlike>;\n setlocaldescription(description: rtcsessiondescriptionlike): promise<void>;\n setremotedescription(description: rtcsessiondescriptionlike): promise<void>;\n close(): void;\n addeventlistener(type: 'datachannel', listener: (event: { readonly channel: rtcdatachannellike }) => void): void;\n addeventlistener(type: 'icegatheringstatechange' | 'iceconnectionstatechange' | 'connectionstatechange', listener: () => void): void;\n addeventlistener(type: string, listener: (event: meshrtcevent) => void): void;\n removeeventlistener(type: string, listener: (event: meshrtcevent) => void): void;\n}\n\ninterface rtcdatachannellike {\n readonly readystate: string;\n send(data: string): void;\n close(): void;\n addeventlistener(type: 'open' | 'close' | 'error', listener: () => void): void;\n addeventlistener(type: 'message', listener: (event: { readonly data: string }) => void): void;\n addeventlistener(type: string, listener: (event: meshrtcevent) => void): void;\n removeeventlistener(type: string, listener: (event: meshrtcevent) => void): void;\n}\n\ninterface rtcsessiondescriptionlike {\n readonly type?: 'offer' | 'answer' | 'pranswer' | 'rollback';\n readonly sdp?: string;\n}\n\ninterface meshrtcevent {\n readonly data?: unknown;\n readonly channel?: rtcdatachannellike;\n}\n```\n\n## errors\n\n| class | thrown when | notable properties |\n| | | |\n| `mesherror` | base class — never thrown directly | `instanceof mesherror` catches all mesh errors |\n| `meshpairingerror` | malformed/expired/unknown invitation or answer, proof mismatch, duplicate answer, `approvepeer` refusal, codec `decode` failure | — |\n| `meshconnectionerror` | ice/dtls failure, channel closed, send to unknown/unconnected peer, offer/answer creation failure | `peerid: string \\| null` |\n| `meshpayloaderror` | outbound message over `maxmessagebytes` or unserializable | — |\n| `meshtimeouterror` | channel open wait exceeded | — |\n| `meshdisposederror` | any method called after `dispose()` | — |\n| `meshunsupportederror` | no `rtcpeerconnection` in the environment and no `rtc` injected — raised at first use, never at import | — |\n",
|
|
802
|
+
"usage": " \ntitle: mesh — usage guide\ndescription: how to use @vielzeug/mesh.\n \n\n[[toc]]\n\n## basic usage\n\ndeclare the protocol once, create a host and a guest, and exchange the pairing payloads out of band. `rtcpeerconnection` requires a secure context (`https:` or `localhost`).\n\n```ts\nimport { createmeshguest, createmeshhost, meshcodec, type meshprotocol } from '@vielzeug/mesh';\n\ninterface appprotocol extends meshprotocol {\n tohost: { note: string };\n toguest: { ack: { ok: boolean } };\n}\n\nconst host = createmeshhost<appprotocol>();\nconst guest = createmeshguest<appprotocol>();\n\n// host produces an invitation; the guest turns it into an answer.\nconst invitationtext = meshcodec.encode(await host.createinvitation());\nconst answertext = meshcodec.encode(await guest.acceptinvitation(meshcodec.decode(invitationtext)));\n\n// the host resolves once the data channel is open.\nawait host.acceptanswer(meshcodec.decode(answertext));\n\nguest.send('note', 'hello');\nhost.on('note', (m) => host.send(m.peerid, 'ack', { ok: true }));\n```\n\n## pairing flow\n\npairing is a two leg manual exchange. each leg is a base64url string produced by `meshcodec.encode` and consumed by `meshcodec.decode`.\n\n```ts\n// host device — one invitation per prospective guest\nconst invitation = await host.createinvitation();\nconst text = meshcodec.encode(invitation);\n```\n\ndeliver `text` any way you can: a textarea the user copies, or `navigator.share` on mobile:\n\n```ts\nif (navigator.share) {\n await navigator.share({ text, title: 'join my session' });\n}\n```\n\nthe guest decodes it and returns an answer the same way:\n\n```ts\nconst answer = await guest.acceptinvitation(meshcodec.decode(text), { name: 'sam' });\nconst answertext = meshcodec.encode(answer); // back to the host\nconst peer = await host.acceptanswer(meshcodec.decode(answertext));\n```\n\nrules that shape the flow:\n\n an invitation is **single use** — call `createinvitation()` once per guest.\n it **expires** at `expiresat` (default `invitationttlms: 300_000`). expired invitations are refused on both sides and cleaned up on the host.\n the answer carries a **proof** derived from the invitation's secret, so a swapped or forged answer is rejected before any connection attempt.\n `approvepeer` runs after proof verification — return `false` to refuse a guest with `meshpairingerror` and a `'peer rejected'` tap.\n\n```ts\nconst host = createmeshhost<appprotocol>({\n approvepeer: (peer) => allowlist.has(peer.id),\n});\n```\n\non the guest, the host's `peerid` is the invitation's `sessionid` — the invitation carries no other host identity. on the host, a guest's `peerid` is the id the guest generated for its answer.\n\n### pairing over qr\n\n`meshqrcodec` is an async codec that compresses pairing payloads (deflate raw + base64url, `mq1.` prefix) so they fit a qr code comfortably. `decode` accepts both the compressed `mq1.*` form and plain `meshcodec` output — paste fallback and camera scanning work through one code path.\n\n```ts\nimport { meshqrcodec } from '@vielzeug/mesh';\nimport { encodeqr, tosvg } from '@vielzeug/sigil';\n\n// async — compressionstream based. throws meshunsupportederror where streams are missing.\nconst invitationtext = await meshqrcodec.encode(await host.createinvitation());\ndocument.queryselector('#qr').innerhtml = tosvg(encodeqr(invitationtext));\n\nconst answer = await host.acceptanswer(await meshqrcodec.decode(scannedanswertext));\n```\n\n`encode` throws `meshunsupportederror` where `compressionstream`/`decompressionstream` are unavailable — keep the plain `meshcodec` paste flow as a fallback. the [sigil pairing recipe](../sigil/examples/pair two devices.md) shows the full two device flow, and `ore qr code`/`ore qr scanner` (refine) wrap the rendering and camera sides.\n\n## sending and receiving\n\n`send`/`on`/`broadcast` are typed from the protocol. outbound messages serialize through `serialize` (default `json.stringify`) into a versioned envelope with id, type, timestamp, and payload.\n\n```ts\nhost.send(peerid, 'ack', { ok: true }); // one guest\nhost.broadcast('ack', { ok: true }, { except: [peerid] }); // all but one\nguest.send('note', 'from the guest');\n```\n\ninbound messages arrive as `meshinbound<t>` — `{ peerid, messageid, sentat, payload }`. rules:\n\n payloads over `maxmessagebytes` (default 65 536) throw `meshpayloaderror` outbound; oversized inbound frames are dropped and tapped as `'message rejected'` with reason `'too large'`.\n duplicate message ids (e.g. after a retry) are dropped and tapped as `'duplicate'`.\n a type with no registered `on` handler is tapped as `'unknown type'` — never thrown.\n `send` to an unknown or unconnected peer throws `meshconnectionerror`.\n\n## peer lifecycle\n\nnode `status` moves through `idle → pairing → connecting → connected`, then `disconnected`, `failed`, or `disposed`. watch transitions — and everything else — through `tap`:\n\n```ts\nhost.tap((event) => {\n if (event.type === 'status change' && event.peerid === null) console.log('node:', event.status);\n if (event.type === 'peer left') console.log('left:', event.peerid, event.reason);\n});\n```\n\nthe host gets structured peer events through `onpeer` and the live inventory through `peers`:\n\n```ts\nhost.onpeer(({ type, peer }) => console.log(type, peer.id, peer.status));\nhost.kick(peerid, 'bye'); // guest observes 'disconnected'\n```\n\n`peerid` on a `status change` event distinguishes node level (`null`) from per peer transitions. per peer events also flow through `tap` so a single subscription observes the whole session.\n\ndispose nodes deterministically — `dispose()` is idempotent, aborts `disposalsignal`, emits a final `'dispose'` event, and detaches all tappers. passing an `abortsignal` option disposes on abort; `using` works too.\n\n## timeouts and limits\n\n| option | default | meaning |\n| | | |\n| `invitationttlms` | `300_000` | how long an unanswered invitation stays valid. |\n| `icegatheringtimeoutms` | `5_000` | caps ice gathering — on timeout pairing proceeds with the candidates gathered so far. also bounds the channel open wait in `acceptanswer` and the guest's never opened channel guard. |\n| `maxmessagebytes` | `65_536` | serialized message cap, enforced in both directions. |\n\n`acceptanswer` rejects with `meshtimeouterror` when the channel never opens; the guest independently marks itself `'failed'` and taps an `'error'` event. inject `clock` to drive ttls deterministically in tests.\n\n## security model\n\nmesh reduces the manual pairing attack surface but does not remove it:\n\n every invitation carries a random 256 bit **secret**; the answer must present a proof derived from it (hmac sha 256 via `subtlecrypto`, falling back to a non cryptographic hash with a `'security downgrade'` tap).\n invitations are **single use and time boxed**, so a leaked invite is only useful briefly.\n `approvepeer` is the host's admission hook — use it for a name check, a shared passphrase confirmed out of band, or an allowlist.\n `maxmessagebytes` caps every inbound frame; malformed frames are dropped, never dispatched.\n **roles are app level.** a guest can send any declared `tohost` type with any payload — validate guest state on the host as if it came from a form. never trust `peer.name` as an identity; it is self declared.\n\n## testing\n\nevery webrtc object comes from the injectable `rtc` factory — tests pair nodes fully in memory, with no browser. model a fake on the package's own `src/__tests__/_fixtures.ts`: two peer connections wired through token sdp, with knobs for latency, drops, ice failure, and stalled open.\n\n```ts\nconst host = createmeshhost<appprotocol>({ rtc: fake.rtc, clock: () => now });\nconst guest = createmeshguest<appprotocol>({ rtc: fake.rtc });\n```\n\nthe [repl examples](/repl) include a runnable in memory pairing you can copy as a starting point.\n\n## framework integration\n\na node is a plain object — own it in a composable/store and dispose on teardown.\n\n::: code group\n\n```ts [react]\nimport { useeffect, useref } from 'react';\nimport { createmeshhost, type meshprotocol } from '@vielzeug/mesh';\n\nexport function usemeshhost<p extends meshprotocol>() {\n const ref = useref<returntype<typeof createmeshhost<p>>>(null);\n if (!ref.current) ref.current = createmeshhost<p>();\n useeffect(() => () => ref.current?.dispose(), []);\n return ref.current;\n}\n```\n\n```ts [vue]\nimport { onunmounted } from 'vue';\nimport { createmeshhost, type meshprotocol } from '@vielzeug/mesh';\n\nexport function usemeshhost<p extends meshprotocol>() {\n const host = createmeshhost<p>();\n onunmounted(() => host.dispose());\n return host;\n}\n```\n\n```ts [svelte]\nimport { ondestroy } from 'svelte';\nimport { createmeshhost, type meshprotocol } from '@vielzeug/mesh';\n\nexport function createhost<p extends meshprotocol>() {\n const host = createmeshhost<p>();\n ondestroy(() => host.dispose());\n return host;\n}\n```\n\n:::\n\n## working with other vielzeug libraries\n\nmesh builds on `@vielzeug/arsenal` for random ids, secrets, and the fallback proof hash — no extra wiring needed. pair it with `herald` when you want mesh messages to feed an in process event bus:\n\n```ts\nimport { createbus } from '@vielzeug/herald';\n\nconst bus = createbus<{ 'guest note': string }>();\nhost.on('note', (m) => bus.emit('guest note', m.payload));\n```\n\n## best practices\n\n pair over `meshcodec` strings, never raw sdp — the transport restores the crlf line endings that copy/paste mangles.\n create **one invitation per guest**; never reuse or forward an invitation.\n keep `invitationttlms` short for physical proximity pairing and always gate unknown guests through `approvepeer`.\n subscribe `on` handlers **before** `acceptanswer` resolves so early messages are not tapped as `'unknown type'`.\n validate every guest payload on the host — treat `tohost` messages as untrusted input.\n `dispose()` both nodes on teardown; pass `signal` to bind disposal to a controller you already own.\n inject `rtc` and `clock` in tests instead of mocking globals.\n plan re pairing, not reconnection: after a reload, create a fresh invitation and let the app re associate the stable `peerid`.\n",
|
|
803
|
+
"examples": " \ntitle: mesh — examples\ndescription: practical examples and recipes for @vielzeug/mesh.\n \n\n## examples\n\n [getting started](./examples/getting started.md)\n [host authoritative command loop](./examples/host authoritative command loop.md)\n"
|
|
804
|
+
},
|
|
805
|
+
"examples": [
|
|
806
|
+
{
|
|
807
|
+
"id": "pairing",
|
|
808
|
+
"text": "pairing & messaging import { createmeshguest, createmeshhost, meshcodec } from '@vielzeug/mesh'\n\n// in memory webrtc fake — the same 'rtc' injection point the package's own\n// tests use. in a real app, omit 'rtc' and globalthis.rtcpeerconnection is used.\nconst fakertc = (() => {\n const links = new map()\n const bag = () => {\n const ls = new map()\n return {\n addeventlistener: (t, f) => ls.set(t, [...(ls.get(t) ?? []), f]),\n removeeventlistener: (t, f) => ls.set(t, (ls.get(t) ?? []).filter((x) => x !== f)),\n emit: (t, e = {}) => (ls.get(t) ?? []).foreach((f) => f(e)),\n }\n }\n const channel = () =>\n object.assign(bag(), {\n readystate: 'connecting',\n remote: null,\n send(data) {\n if (this.remote?.readystate === 'open') queuemicrotask(() => this.remote.emit('message', { data }))\n },\n open() {\n this.readystate = 'open'\n this.emit('open')\n },\n close() {\n if (this.readystate === 'closed') return\n this.readystate = 'closed'\n this.emit('close')\n this.remote?.close()\n },\n })\n const pc = (token) =>\n object.assign(bag(), {\n connectionstate: 'new',\n iceconnectionstate: 'new',\n icegatheringstate: 'new',\n localdescription: null,\n createdatachannel() {\n this.dc = channel()\n return this.dc\n },\n createoffer() {\n links.set(token, { host: this })\n return promise.resolve({ sdp: 'offer:' + token, type: 'offer' })\n },\n createanswer() {\n const link = [...links.values()].find((l) => l.guest === this)\n return promise.resolve({ sdp: 'answer:' + link.hosttoken + ':' + token, type: 'answer' })\n },\n setlocaldescription(d) {\n this.localdescription = d\n this.icegatheringstate = 'complete'\n return promise.resolve()\n },\n setremotedescription(d) {\n if (d.type === 'offer') {\n // sdp is crlf normalized like a real description — trim the token\n const hosttoken = d.sdp.split(':')[1].trim()\n const link = links.get(hosttoken)\n link.guest = this\n link.hosttoken = hosttoken\n return promise.resolve()\n }\n const hosttoken = d.sdp.replace('answer:', '').split(':')[0].trim()\n const link = links.get(hosttoken)\n const guestchannel = channel()\n const hostchannel = link.host.dc\n hostchannel.remote = guestchannel\n guestchannel.remote = hostchannel\n link.guest.emit('datachannel', { channel: guestchannel })\n queuemicrotask(() => {\n hostchannel.open()\n guestchannel.open()\n })\n return promise.resolve()\n },\n close() {\n this.dc?.close()\n },\n })\n let n = 0\n return { createpeerconnection: () => pc('pc' + ++n) }\n})()\n\nconst host = createmeshhost({ rtc: fakertc })\nconst guest = createmeshguest({ rtc: fakertc })\n\nguest.on('ack', (m) => console.log('guest got ack:', m.payload.ok, 'from', m.peerid))\nhost.on('note', (m) => {\n console.log('host got note:', m.payload)\n host.send(m.peerid, 'ack', { ok: true })\n})\n\n// the out of band legs are plain strings — copy/paste or navigator.share\nconst invitationtext = meshcodec.encode(await host.createinvitation())\nconsole.log('invitation bytes:', invitationtext.length)\n\nconst answertext = meshcodec.encode(await guest.acceptinvitation(meshcodec.decode(invitationtext)))\nconst peer = await host.acceptanswer(meshcodec.decode(answertext))\n\nconsole.log('paired — host sees peer', peer.id, '| status:', host.status, '/', guest.status)\n\nguest.send('note', 'hello over the fake channel')\nawait new promise((r) => settimeout(r, 0))\n\nhost.dispose()\nguest.dispose()\nconsole.log('disposed:', host.status, '/', guest.status)"
|
|
809
|
+
}
|
|
810
|
+
],
|
|
811
|
+
"exports": "createmeshhost createmeshguest meshcodec meshprotocol",
|
|
812
|
+
"keywords": "webrtc p2p peer to peer datachannel lan pairing backendless signaling free",
|
|
813
|
+
"name": "@vielzeug/mesh",
|
|
814
|
+
"related": "pulse arsenal herald",
|
|
815
|
+
"slug": "mesh",
|
|
816
|
+
"source": "// public root surface of @vielzeug/mesh. every public export goes through this file.\nexport { meshcodec } from './codec';\nexport {\n meshconnectionerror,\n meshdisposederror,\n mesherror,\n meshpairingerror,\n meshpayloaderror,\n meshtimeouterror,\n meshunsupportederror,\n} from './errors';\nexport { createmeshguest } from './guest';\nexport { createmeshhost } from './host';\nexport { type meshasynccodec, meshqrcodec } from './qr codec';\nexport type {\n meshanswer,\n meshchanneloptions,\n meshcodec,\n meshevent,\n meshguest,\n meshguestoptions,\n meshhost,\n meshhostoptions,\n meshinbound,\n meshinvitation,\n meshmessagemap,\n meshnode,\n meshoptions,\n meshpeer,\n meshpeerevent,\n meshpeerinfo,\n meshprotocol,\n meshrtcevent,\n meshrtcfactory,\n meshstatus,\n rtcdatachannellike,\n rtcpeerconnectionlike,\n rtcsessiondescriptionlike,\n unsubscribe,\n} from './types';\n"
|
|
817
|
+
},
|
|
796
818
|
{
|
|
797
819
|
"category": "ui",
|
|
798
820
|
"description": "lifecycle owned web animations api primitives for native playback, groups, and additive flip transitions.",
|
|
@@ -971,12 +993,12 @@
|
|
|
971
993
|
"category": "ui components",
|
|
972
994
|
"description": "accessible, themeable web components built with ore for framework and vanilla dom apps.",
|
|
973
995
|
"docs": {
|
|
974
|
-
"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 copy command,\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 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 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 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 list`, `ore list item`, `ore marquee`, `ore pagination`, `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 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 datagrid`, `ore date picker`, `ore file input`, `ore input`, `ore message composer`, `ore number input`, `ore otp input`, `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",
|
|
975
|
-
"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| `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` without registering components |\n| `@vielzeug/refine/<component>` | register one component and export its tag constant, props, events, and related 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`, `list`, `list item`, `marquee`, `pagination`, `separator`, `stats`, `step`, `stepper`, `table`, `text` |\n| disclosure | `accordion`, `accordion item`, `tabs`, `tab item`, `tab panel` |\n| feedback | `alert`, `async`, `badge`, `chip`, `password strength`, `progress`, `skeleton`, `toast`, `typing indicator` |\n| inputs | `button`, `button group`, `calendar`, `checkbox`, `checkbox group`, `combobox`, `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## 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",
|
|
976
|
-
"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"
|
|
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 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 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 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| `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` without registering components |\n| `@vielzeug/refine/<component>` | register one component and export its tag constant, props, events, and related 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`, `stats`, `step`, `stepper`, `table`, `text` |\n| disclosure | `accordion`, `accordion item`, `tabs`, `tab item`, `tab panel` |\n| feedback | `alert`, `async`, `badge`, `chip`, `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## 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
|
+
"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"
|
|
977
999
|
},
|
|
978
1000
|
"examples": [],
|
|
979
|
-
"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 copy command ore datagrid ore date picker ore dialog ore drawer ore file input ore grid ore grid item ore icon ore input 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 radio ore radio group ore rating ore select ore separator ore sidebar ore sidebar group ore sidebar item ore skeleton ore slider 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",
|
|
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 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 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",
|
|
980
1002
|
"keywords": "web components accessible themeable ui components design system",
|
|
981
1003
|
"name": "@vielzeug/refine",
|
|
982
1004
|
"related": "ore orbit forge keymap",
|
|
@@ -1215,6 +1237,32 @@
|
|
|
1215
1237
|
"slug": "sentinel",
|
|
1216
1238
|
"source": "export { createelementsize } from './element size.ts';\nexport { sentinelerror, sentinelunavailableerror } from './errors.ts';\nexport type { createintersectionoptions } from './intersection.ts';\nexport { createintersection } from './intersection.ts';\nexport { createmediaquery } from './media query.ts';\nexport { createnetwork } from './network.ts';\nexport type {\n elementsizestate,\n intersectionstate,\n mediaquerystate,\n networkconnectionsnapshot,\n networkstate,\n sentinel,\n sentineloptions,\n viewportstate,\n windowsentineloptions,\n} from './types.ts';\nexport { createviewport } from './viewport.ts';\n"
|
|
1217
1239
|
},
|
|
1240
|
+
{
|
|
1241
|
+
"category": "utilities",
|
|
1242
|
+
"description": "qr code generation and scanning — pure matrix encoder, svg and canvas renderers, native barcodedetector scanning",
|
|
1243
|
+
"docs": {
|
|
1244
|
+
"index": " \ntitle: sigil — qr code generation and scanning\ndescription: \"qr code generation and scanning — pure matrix encoder, svg and canvas renderers, native barcodedetector scanning\"\npackage: sigil\ncategory: utilities\nkeywords: [qr, qrcode, barcode, encoder, scanner, svg, canvas, barcodedetector, pairing]\nexports: [encodeqr, tosvg, createqrscanner, qrcapacity, detectqr]\nrelated: [mesh, refine]\nenvironments: [browser, node]\n \n\n<! markdownlint disable md025 md033 md060 >\n\n<packagehero package=\"sigil\" />\n\n## why sigil?\n\nqr workflows usually pull in a general purpose barcode library that renders to canvas and guesses at colors, or a from scratch decoder you do not need. sigil is a pure, zero dependency encoder (numeric, alphanumeric, and byte modes; versions 1–40; all four error correction levels; automatic mask selection) with renderers that stay out of the way — an svg string that themes with css, and a canvas painter for print/bitmap flows. scanning is a thin lifecycle wrapper over the platform's native `barcodedetector`, so there is no decoder weight and no polyfill to ship.\n\n```ts\n// before — a qr library per concern, canvas only output, hardcoded colors\nimport qrcode from 'qrcode';\nconst dataurl = await qrcode.todataurl(text, { width: 256, color: { dark: '#000' } });\nimg.src = dataurl; // raster, fixed colors, no theme support\n\n// after — encode once, render anywhere\nimport { encodeqr, tosvg } from '@vielzeug/sigil';\nconst matrix = encodeqr(text); // smallest version that fits, auto mask\nelement.innerhtml = tosvg(matrix); // currentcolor modules — themes with css\n```\n\n| feature | sigil | `qrcode` (node style) | `jsqr` |\n| | | | |\n| bundle size | <packageinfo package=\"sigil\" type=\"size\" /> | ~100 kb+ | decoder only |\n| runtime dependencies | <ore icon name=\"check\" size=\"16\"></ore icon> none | <ore icon name=\"x\" size=\"16\"></ore icon> | <ore icon name=\"check\" size=\"16\"></ore icon> none |\n| output | matrix, svg string, canvas | canvas/dataurl/utf 8 | — |\n| themeable output | `currentcolor` by default | hardcoded color strings | — |\n| scanning | native `barcodedetector` wrapper | <ore icon name=\"x\" size=\"16\"></ore icon> | pure js decoder |\n| ssr safe import | <ore icon name=\"check\" size=\"16\"></ore icon> | <ore icon name=\"x\" size=\"16\"></ore icon> | <ore icon name=\"check\" size=\"16\"></ore icon> |\n\n<div class=\"decision callout\">\n\n**use sigil when** you need qr output that themes with your ui (svg/`currentcolor`), a minimal encoder with no runtime dependencies, or a scanner that uses the platform decoder instead of shipping one.\n\n**consider `jsqr` style decoding when** you must scan in browsers without `barcodedetector` — sigil reports `sigilunsupportederror` rather than bundling a fallback decoder.\n\n</div>\n\n## installation\n\n::: code group\n\n```sh [pnpm]\npnpm add @vielzeug/sigil\n```\n\n```sh [npm]\nnpm install @vielzeug/sigil\n```\n\n```sh [yarn]\nyarn add @vielzeug/sigil\n```\n\n:::\n\n## quick start\n\nencode to an svg string — works in node and the browser:\n\n```ts\nimport { encodeqr, qrcapacity, tosvg } from '@vielzeug/sigil';\n\nconst payload = 'https://vielzeug.dev';\nconst matrix = encodeqr(payload, { errorcorrection: 'm' });\ndocument.queryselector('#qr').innerhtml = tosvg(matrix, { label: 'vielzeug link' });\n```\n\nscan with the camera (browser only — see support notes in the usage guide):\n\n```ts\nimport { createqrscanner } from '@vielzeug/sigil';\n\nconst video = document.queryselector('video');\nconst scanner = createqrscanner({ video });\nscanner.onresult((result) => {\n console.log('scanned:', result.value); // once: true stops after first decode\n});\nawait scanner.start(); // throws sigilunsupportederror / sigilpermissionerror\n```\n\n## features\n\n<div class=\"features grid\">\n\n `encodeqr` — pure matrix encoder: numeric/alphanumeric/byte modes, versions 1–40, ec levels l/m/q/h, automatic or forced mask\n `qrcapacity` — byte capacity per version/mode/level, for pre flight validation\n `tosvg` — compact path merged svg string; `currentcolor` modules, `<title>` + `role=\"img\"` built in\n `drawtocanvas` — bitmap rendering with dpr aware scaling for print and download flows\n `detectqr` — one shot detection on any `imagebitmapsource`\n `createqrscanner` — camera scan loop with `once` mode, status events, `tap()` observability, and strict disposal\n `isqrscansupported`/`qrscansupport` — sync and async feature detection that never assumes\n\n</div>\n\n## documentation\n\n<div class=\"doc links\">\n\n [usage guide](./usage.md) — encoding modes, capacity, rendering, scanning, and testing\n [api reference](./api.md) — every export, option, type, and error\n [examples](./examples.md) — copy paste recipes, including pairing two devices with `meshqrcodec`\n\n</div>\n\n## see also\n\n<div class=\"see also\">\n\n [mesh](../mesh/) — backendless p2p sessions; `meshqrcodec` produces compact qr friendly pairing payloads\n [refine](../refine/) — `ore qr code` and `ore qr scanner` wrap sigil in ready made, themeable components\n\n</div>\n\n<! markdownlint enable >\n",
|
|
1245
|
+
"api": " \ntitle: sigil — api reference\ndescription: public api of @vielzeug/sigil — encoder, renderers, scanner, types, and errors.\n \n\n[[toc]]\n\n## api overview\n\n| symbol | purpose | execution | common gotcha |\n| | | | |\n| `encodeqr` | encode a payload into a `qrmatrix` | sync | throws `sigilcapacityerror` over capacity |\n| `qrcapacity` | payload limit for a version/level/mode | sync | returns *characters* for numeric/alphanumeric, *bytes* for byte mode |\n| `tosvg` | render a matrix as an svg string | sync | escapes `dark`/`light` as raw css — pass values, not markup |\n| `drawtocanvas` | paint a matrix onto a canvas | sync | browser only; resizes the canvas for dpr |\n| `detectqr` | one shot detection on an image source | async | throws `sigilunsupportederror` without `barcodedetector` |\n| `createqrscanner` | camera scan loop | async (start) | must `dispose()`; camera tracks persist otherwise |\n| `isqrscansupported` | sync feature check | sync | constructor exists ≠ `qr_code` supported — prefer `qrscansupport()` |\n| `qrscansupport` | async feature check incl. formats | async | never assume support per browser/version |\n\n## package entry point\n\n| import | purpose |\n| | |\n| `@vielzeug/sigil` | complete public sigil api |\n\n## encoding\n\n### `encodeqr(data, options?)`\n\n```ts\nfunction encodeqr(data: string | uint8array, options?: qrencodeoptions): qrmatrix;\n```\n\nencodes `data` into a qr symbol. picks the most compact mode covering the whole input, the smallest fitting version, and the lowest penalty mask.\n\n| parameter | type | default | description |\n| | | | |\n| `data` | `string \\| uint8array` | — | payload; `uint8array` implies byte mode |\n| `options.errorcorrection` | `'l' \\| 'm' \\| 'q' \\| 'h'` | `'m'` | error correction level |\n| `options.version` | `1–40` | auto | pin the version; throws if payload can't fit |\n| `options.minversion` | `1–40` | `1` | smallest acceptable version |\n| `options.mode` | `qrmode` | auto | force a segment mode |\n| `options.mask` | `0–7` | auto | force the mask (test vectors only) |\n\n**returns** a frozen `qrmatrix` with `size`, `version`, `mode`, `errorcorrection`, `mask`, `modules`, and `get(x, y)`.\n\n**throws** `sigilcapacityerror` (payload exceeds capacity), `sigiloptionerror` (invalid mode/version/mask combination).\n\n```ts\nimport { encodeqr } from '@vielzeug/sigil';\n\nconst matrix = encodeqr('hello world', { errorcorrection: 'q' });\nmatrix.size; // 21 (v1)\nmatrix.get(0, 0); // finder pattern → true\n```\n\n \n\n### `qrcapacity(version, errorcorrection, mode)`\n\n```ts\nfunction qrcapacity(version: number, errorcorrection: qrerrorcorrection, mode: qrmode): number;\n```\n\nreturns the payload limit: characters for `numeric`/`alphanumeric`, bytes for `byte`.\n\n```ts\nqrcapacity(1, 'm', 'byte'); // → 14\nqrcapacity(40, 'l', 'numeric'); // → 7089\n```\n\n## rendering\n\n### `tosvg(matrix, options?)`\n\n```ts\nfunction tosvg(matrix: qrmatrix, options?: qrsvgoptions): string;\n```\n\nreturns a complete `<svg>` string. runs in any environment — no dom required.\n\n| parameter | type | default | description |\n| | | | |\n| `options.dark` | `string` | `'currentcolor'` | dark module color |\n| `options.light` | `string` | `'transparent'` | background color |\n| `options.margin` | `number` | `4` | quiet zone modules |\n| `options.scale` | `number` | `1` | user units per module |\n| `options.optimizepath` | `boolean` | `true` | merge dark runs into one `<path>` |\n| `options.label` | `string` | — | `<title>` + `aria label`; enables `role=\"img\"` |\n\n```ts\nconst svg = tosvg(matrix, { label: 'pairing code', dark: 'var( qr dark)' });\n```\n\n \n\n### `drawtocanvas(matrix, canvas, options?)` — browser only\n\n```ts\nfunction drawtocanvas(matrix: qrmatrix, canvas: htmlcanvaselement, options?: qrcanvasoptions): number;\n```\n\npaints the matrix and returns the css pixel edge length. sizes the backing store for `devicepixelratio` so output stays crisp on retina displays.\n\n| parameter | type | default | description |\n| | | | |\n| `options.scale` | `number` | `1` | css px per module |\n| `options.margin` | `number` | `4` | quiet zone modules |\n| `options.dark` / `options.light` | `string` | `'#000'` / `'#fff'` | fill colors |\n\n## scanning (browser only)\n\n### `createqrscanner(options)`\n\n```ts\nfunction createqrscanner(options: qrscanneroptions): qrscanner;\n```\n\ncreates a camera scan loop around the native `barcodedetector`.\n\n| parameter | type | default | description |\n| | | | |\n| `options.video` | `htmlvideoelement` | — | element the stream attaches to (needs `autoplay playsinline muted`) |\n| `options.constraints` | `mediatrackconstraints` | `{ facingmode: 'environment' }` | `getusermedia` video constraints |\n| `options.intervalms` | `number` | `200` | minimum ms between detect passes |\n| `options.once` | `boolean` | `true` | stop after the first result |\n| `options.signal` | `abortsignal` | — | abort → `stop()` |\n| `options.detector` | `qrdetector` | native | injected detector for tests |\n| `options.mediadevices` | `pick<mediadevices, 'getusermedia'>` | `navigator.mediadevices` | injected media for tests |\n\n**returns** a `qrscanner`:\n\n| member | signature | description |\n| | | |\n| `start()` | `() => promise<void>` | request camera, attach stream, run the detect loop. rejects with `sigilunsupportederror` / `sigilpermissionerror` |\n| `stop()` | `() => void` | stop the loop and tracks; instance stays usable |\n| `onresult(handler)` | `(handler) => () => void` | decode results; returns unsubscribe |\n| `tap(handler, options?)` | `(handler, { signal? }) => () => void` | observe `sigilevent`s; handler errors are swallowed |\n| `status` | `qrscannerstatus` | `'idle' \\| 'starting' \\| 'scanning' \\| 'stopped' \\| 'disposed'` |\n| `dispose()` / `[symbol.dispose]` | `() => void` | terminal teardown; `disposalsignal` aborts |\n| `disposed` | `boolean` | whether `dispose()` ran |\n\n```ts\nconst scanner = createqrscanner({ video });\nconst off = scanner.onresult(({ value }) => console.log(value));\nawait scanner.start();\n```\n\n \n\n### `detectqr(source, options?)`\n\n```ts\nfunction detectqr(\n source: imagebitmapsource,\n options?: { signal?: abortsignal; detector?: qrdetector },\n): promise<qrscanresult | null>;\n```\n\nsingle pass detection on an image source. returns the first result's `{ value, cornerpoints }`, or `null` when nothing decodes. throws `sigilunsupportederror` when no detector is available.\n\n \n\n### `isqrscansupported()` / `qrscansupport()`\n\n```ts\nfunction isqrscansupported(): boolean;\nfunction qrscansupport(): promise<boolean>;\n```\n\n`isqrscansupported` checks the `barcodedetector` constructor synchronously. `qrscansupport` additionally asks `getsupportedformats()` for `'qr_code'` — use it before showing scan affordances.\n\n## types\n\n```ts\ntype qrerrorcorrection = 'l' | 'm' | 'q' | 'h';\ntype qrmode = 'numeric' | 'alphanumeric' | 'byte';\ntype qrscannerstatus = 'idle' | 'starting' | 'scanning' | 'stopped' | 'disposed';\n\ninterface qrmatrix {\n readonly errorcorrection: qrerrorcorrection;\n readonly mask: number;\n readonly mode: qrmode;\n readonly modules: readonlyarray<readonlyarray<boolean>>; // frozen, row major\n readonly size: number; // 17 + 4 * version\n readonly version: number; // 1–40\n get(x: number, y: number): boolean; // false outside bounds\n}\n\ninterface qrscanresult {\n readonly cornerpoints?: readonlyarray<{ readonly x: number; readonly y: number }>;\n readonly value: string;\n}\n\ninterface qrdetector {\n detect(source: imagebitmapsource): promise<\n readonlyarray<{ rawvalue: string; cornerpoints?: readonlyarray<{ x: number; y: number }> }>\n >;\n}\n\ntype sigilevent =\n | { readonly type: 'status change'; readonly status: qrscannerstatus }\n | { readonly type: 'detect'; readonly value: string; readonly elapsedms: number }\n | { readonly type: 'frame skipped'; readonly reason: 'not ready' | 'busy' }\n | { readonly type: 'error'; readonly error: sigilerror }\n | { readonly type: 'dispose' };\n```\n\n`qrencodeoptions`, `qrsvgoptions`, `qrcanvasoptions`, `qrscanneroptions`, and `qrscanner` are documented under their factories above.\n\n## errors\n\n| class | trigger | notable members |\n| | | |\n| `sigilerror` | base class for all sigil errors | — |\n| `sigilcapacityerror` | payload exceeds the version/mode limit | `bytes`, `maxbytes`, `version` |\n| `sigiloptionerror` | invalid option combination (forced mode can't represent input, bad mask/version) | — |\n| `sigilunsupportederror` | `barcodedetector`/`getusermedia` missing | — |\n| `sigilpermissionerror` | camera `notallowederror` | — |\n| `sigildisposederror` | `start()` on a disposed scanner | — |\n\nall carry `cause` when wrapping a platform error.\n",
|
|
1246
|
+
"usage": " \ntitle: sigil — usage guide\ndescription: encoding modes, capacity checks, svg/canvas rendering, camera scanning, and testing for @vielzeug/sigil.\n \n\n[[toc]]\n\n## basic usage\n\n`encodeqr` produces a `qrmatrix` — a frozen square of dark/light modules. render it with `tosvg` (string, works anywhere) or `drawtocanvas` (browser).\n\n```ts\nimport { encodeqr, qrcapacity, tosvg } from '@vielzeug/sigil';\n\nconst matrix = encodeqr('https://vielzeug.dev');\n// pick the smallest version automatically, or pin one:\nconst pinned = encodeqr('https://vielzeug.dev', { version: 5, errorcorrection: 'q' });\n\nconst svg = tosvg(matrix); // inline <svg> string — inject or serialize\n```\n\nthe encoder chooses the most compact mode for the whole input and the smallest version that fits. `mask` is chosen by the lowest iso penalty score — override it only for test vectors.\n\n## encoding modes and capacity\n\nthree segment modes cover every payload:\n\n| mode | alphabet | max payload (v40 l) |\n| | | |\n| `numeric` | `0–9` | 7,089 digits |\n| `alphanumeric` | `0–9 a z space $%*+ ./:` | 4,296 chars |\n| `byte` | utf 8 (no eci header) | 2,953 bytes |\n\nmode selection is automatic — `encodeqr('01234567')` uses `numeric`, `'hello world'` uses `alphanumeric`, anything else uses `byte`. force a mode with `mode`, but a forced mode that cannot represent the input throws `sigiloptionerror`.\n\ncheck capacity before encoding with `qrcapacity(version, errorcorrection, mode)` — it returns the maximum *bytes* (for `byte` mode) or *characters* the symbol holds:\n\n```ts\nimport { qrcapacity, sigilcapacityerror } from '@vielzeug/sigil';\n\nqrcapacity(10, 'm', 'byte'); // → 213 (data codewords available at v10 m)\n\ntry {\n encodeqr(hugepayload, { version: 1 });\n} catch (error) {\n if (error instanceof sigilcapacityerror) {\n console.log(`${error.bytes} > ${error.maxbytes} at v${error.version}`);\n }\n}\n```\n\nuse `minversion` to keep a stable symbol size while a payload changes — useful when a layout must not reflow between renders.\n\n## error correction\n\n| level | recovery | trade off |\n| | | |\n| `l` | ~7% | smallest matrix |\n| `m` | ~15% | default — good balance |\n| `q` | ~25% | print, wear |\n| `h` | ~30% | logos/overlays, dense codes |\n\nhigher levels shrink capacity at the same version. for an overlaid logo, prefer `h` *and* keep the logo inside the quiet zone's margin of safety.\n\n## rendering\n\n### svg — `tosvg`\n\n```ts\nconst svg = tosvg(matrix, {\n dark: 'currentcolor', // default — inherits text color\n light: 'transparent', // default\n margin: 4, // quiet zone in modules (spec minimum is 4)\n scale: 1, // user units per module — prefer scaling via css\n optimizepath: true, // merge adjacent modules into one path (default)\n label: 'pairing code', // <title> + role=\"img\"\n});\n```\n\n`optimizepath` merges each row's dark runs into a single `<path>` — orders of magnitude smaller markup. set `dark`/`light` to css variables to theme the code (`dark: 'var( qr dark)'`).\n\n### canvas — `drawtocanvas` (browser only)\n\n```ts\nconst sizepx = drawtocanvas(matrix, canvas, { scale: 8, margin: 4 });\n// canvas is resized for devicepixelratio; returns the css pixel edge length\n```\n\nuse canvas for print, download (`canvas.toblob`), or compositing. svg is the better default for display.\n\n## scanning (browser only)\n\nscanning wraps the native `barcodedetector`. it is **not** universally available — feature detect, never assume:\n\n| engine | `barcodedetector` | notes |\n| | | |\n| chrome / edge / android webview | yes | `qr_code` format supported |\n| firefox | no (behind flag in some builds) | `isqrscansupported()` → `false` |\n| safari | varies by version | check at runtime; do not rely on version sniffing |\n\n```ts\nimport { createqrscanner, isqrscansupported, qrscansupport } from '@vielzeug/sigil';\n\nisqrscansupported(); // sync — barcodedetector constructor exists\nawait qrscansupport(); // async — also verifies 'qr_code' is a supported format\n\nconst scanner = createqrscanner({\n video, // <video autoplay playsinline muted>\n constraints: { facingmode: 'environment' },\n intervalms: 200, // min ms between detect passes\n once: true, // stop after first result\n});\n\nscanner.onresult((result) => showpairing(result.value));\nscanner.tap((event) => console.debug(event.type)); // status change, detect, frame skipped…\n\nawait scanner.start(); // requests camera, attaches stream, starts the loop\nscanner.stop(); // pauses; start() resumes\nscanner.dispose(); // terminal — releases everything\n```\n\n`start()` rejects with `sigilunsupportederror` (no detector/getusermedia) or `sigilpermissionerror` (`notallowederror`). detection failures mid scan surface through `tap` `error` events and stop the scanner.\n\n### one shot detection\n\n`detectqr(source)` runs a single pass over any `imagebitmapsource` — `imagedata`, `blob`, `videoframe`, an `<img>`, or a `<canvas>`:\n\n```ts\nimport { detectqr } from '@vielzeug/sigil';\n\nconst result = await detectqr(imagebitmap); // → { value, cornerpoints } | null\n```\n\n## testing\n\nboth seams are injectable — never touch globals in tests:\n\n```ts\nimport { createqrscanner } from '@vielzeug/sigil';\n\nconst fakescanner = createqrscanner({\n video: document.createelement('video'),\n detector: { detect: async () => [{ rawvalue: 'payload' }] }, // fake barcodedetector\n mediadevices: { getusermedia: async () => fakestream }, // fake camera\n});\n```\n\nfor the web components, `ore qr scanner` exposes a `scannerfactory` js property so tests inject a fake `qrscanner` without stubbing `barcodedetector` or `getusermedia`.\n\n## framework integration\n\nencoding is synchronous and ssr safe — render `tosvg` output as part of server markup:\n\n::: code group\n\n```tsx [react]\nfunction qr({ value }: { value: string }) {\n const svg = usememo(() => tosvg(encodeqr(value)), [value]);\n return <span dangerouslysetinnerhtml={{ __html: svg }} />;\n}\n```\n\n```vue [vue]\n<script setup>\nimport { computed } from 'vue';\nimport { encodeqr, tosvg } from '@vielzeug/sigil';\nconst props = defineprops({ value: string });\nconst svg = computed(() => tosvg(encodeqr(props.value ?? '')));\n</script>\n<template><span v html=\"svg\" /></template>\n```\n\n```html [svelte]\n<script>\n import { encodeqr, tosvg } from '@vielzeug/sigil';\n export let value;\n $: svg = tosvg(encodeqr(value));\n</script>\n<span>{@html svg}</span>\n```\n\n:::\n\nscanning needs lifecycle — start in `onmounted`/`onmount`, always `dispose()` in cleanup.\n\n## working with other vielzeug libraries\n\n **mesh** — `meshqrcodec` compresses webrtc pairing payloads (offer/answer json) to qr friendly strings. the [pair two devices with qr](./examples/pair two devices.md) recipe shows the full flow.\n **refine** — prefer `ore qr code` / `ore qr scanner` over hand rolled wrappers: they compose sigil with themeable markup, a11y labels, and state handling.\n\n## best practices\n\n pre validate payloads with `qrcapacity` before surfacing an encode control.\n keep the quiet zone — `margin` below 4 risks unreadable codes; never overlay content on it.\n prefer `tosvg` + `currentcolor` for display; reserve `drawtocanvas` for print/download.\n feature detect scanning with `qrscansupport()` before showing a scan affordance.\n always `dispose()` scanners — camera tracks outlive route changes otherwise.\n use `once: false` only when the ux genuinely expects repeated decodes.\n treat scanned strings as untrusted input — validate before navigation or pairing.\n pin `minversion` in animated/live payloads so the symbol doesn't resize mid flow.\n",
|
|
1247
|
+
"examples": " \ntitle: sigil — examples\ndescription: practical examples and recipes for @vielzeug/sigil.\n \n\n## examples\n\n [getting started](./examples/getting started.md)\n [pair two devices with qr](./examples/pair two devices.md)\n"
|
|
1248
|
+
},
|
|
1249
|
+
"examples": [
|
|
1250
|
+
{
|
|
1251
|
+
"id": "capacity-check",
|
|
1252
|
+
"text": "capacity check import { encodeqr, qrcapacity, sigilcapacityerror } from '@vielzeug/sigil'\n\n// qrcapacity returns the payload limit per version/level/mode —\n// characters for numeric/alphanumeric, bytes for byte mode.\nfor (const [v, ec, mode] of [\n [1, 'l', 'byte'],\n [1, 'm', 'byte'],\n [10, 'm', 'byte'],\n [40, 'l', 'byte'],\n [40, 'l', 'numeric'],\n]) {\n console.log(`v${v} ${ec} ${mode}: ${qrcapacity(v, ec, mode)}`)\n}\n\n// encoding beyond capacity throws sigilcapacityerror with the numbers.\nconst payload = 'x'.repeat(qrcapacity(1, 'm', 'byte') + 1)\ntry {\n encodeqr(payload, { version: 1 })\n} catch (e) {\n if (e instanceof sigilcapacityerror) {\n console.log('capacity exceeded:', e.bytes, '>', e.maxbytes, 'at v' + e.version)\n }\n}\n\n// without a pinned version the encoder just grows to the next fitting one\nconst auto = encodeqr(payload)\nconsole.log('auto picked version:', auto.version)"
|
|
1253
|
+
},
|
|
1254
|
+
{
|
|
1255
|
+
"id": "encode-svg",
|
|
1256
|
+
"text": "encode to svg import { encodeqr, tosvg } from '@vielzeug/sigil'\n\n// the encoder picks the most compact mode, smallest version, and best mask.\nconst matrix = encodeqr('https://vielzeug.dev', { errorcorrection: 'm' })\nconsole.log('version:', matrix.version, '| size:', matrix.size, 'modules | mode:', matrix.mode)\n\n// tosvg returns a complete <svg> string — works in node (no dom needed).\n// dark defaults to currentcolor so the code themes with css.\nconst svg = tosvg(matrix, { label: 'vielzeug link' })\nconsole.log(svg.slice(0, 120) + '…')\nconsole.log('svg bytes:', svg.length)\n\n// longer payloads bump the version automatically\nconst bigger = encodeqr('https://vielzeug.dev/sigil/examples/pair two devices')\nconsole.log('longer payload → version:', bigger.version, '| size:', bigger.size)"
|
|
1257
|
+
}
|
|
1258
|
+
],
|
|
1259
|
+
"exports": "encodeqr tosvg createqrscanner qrcapacity detectqr",
|
|
1260
|
+
"keywords": "qr qrcode barcode encoder scanner svg canvas barcodedetector pairing",
|
|
1261
|
+
"name": "@vielzeug/sigil",
|
|
1262
|
+
"related": "mesh refine",
|
|
1263
|
+
"slug": "sigil",
|
|
1264
|
+
"source": "// public root surface of @vielzeug/sigil. every public export goes through this file.\nexport { drawtocanvas } from './canvas';\nexport { encodeqr, qrcapacity } from './encode';\nexport {\n sigilcapacityerror,\n sigildisposederror,\n sigilerror,\n sigiloptionerror,\n sigilpermissionerror,\n sigilunsupportederror,\n} from './errors';\nexport { createqrscanner, detectqr, isqrscansupported, qrscansupport } from './scan';\nexport { tosvg } from './svg';\nexport type {\n qrcanvasoptions,\n qrdetector,\n qrencodeoptions,\n qrerrorcorrection,\n qrmatrix,\n qrmode,\n qrscanner,\n qrscanneroptions,\n qrscannerstatus,\n qrscanresult,\n qrsvgoptions,\n sigilevent,\n} from './types';\n"
|
|
1265
|
+
},
|
|
1218
1266
|
{
|
|
1219
1267
|
"category": "data",
|
|
1220
1268
|
"description": "framework agnostic local and remote collection state with page, cursor, and infinite pagination.",
|
|
@@ -1261,9 +1309,9 @@
|
|
|
1261
1309
|
"category": "validation",
|
|
1262
1310
|
"description": "zero dependency schema validation with explicit async checks, standard schema interoperability, and json schema export.",
|
|
1263
1311
|
"docs": {
|
|
1264
|
-
"index": " \ntitle: spell — schema validation for typescript\ndescription: zero dependency schema validation with explicit async checks, standard schema interoperability, and json schema export.\npackage: spell\ncategory: validation\nkeywords: [schema, validation, parsing, standard schema, json schema, locale, typescript, descriptors]\nrelated: [forge, courier, vault]\nexports:\n [s, createparsecontext, spellvalidationerror, spelldefinitionerror, standardschemav1, errorcode, './json', './predicates']\nenvironments: [browser, node, ssr, deno]\n \n\n<! markdownlint disable md025 md033 md060 >\n\n<packagehero package=\"spell\" />\n\n## why spell?\n\nspell keeps runtime validation, static inference, and portable definitions in one api. use `s` for schema construction; import json conversion and predicates from dedicated subpaths.\n\nthis example shows the difference between manual branching and a single reusable schema.\n\n```ts\n// before\nfunction parseuserbefore(value: unknown) {\n if (typeof value !== 'object' || value === null) throw new error('expected object');\n\n const candidate = value as record<string, unknown>;\n\n if (typeof candidate.email !== 'string' || !candidate.email.includes('@')) {\n throw new error('expected valid email');\n }\n\n if (typeof candidate.role !== 'string' || !['admin', 'editor', 'viewer'].includes(candidate.role)) {\n throw new error('expected valid role');\n }\n\n return {\n email: candidate.email,\n role: candidate.role,\n };\n}\n\n// after\nimport { s } from '@vielzeug/spell';\n\nconst user = s.object({\n email: s.string().email(),\n role: s.enum(['admin', 'editor', 'viewer'] as const),\n});\n\nconst user = user.parse({ email: 'ada@example.com', role: 'admin' });\n```\n\n| feature | spell | zod | yup |\n| | | | |\n| bundle size | <packageinfo package=\"spell\" type=\"size\" /> | ~62 kb | ~14 kb |\n| type inference | <ore icon name=\"check\" size=\"16\"></ore icon> `infer<t>` | <ore icon name=\"check\" size=\"16\"></ore icon> | partial |\n| coercion api | <ore icon name=\"check\" size=\"16\"></ore icon> `s.coerce.*` | <ore icon name=\"check\" size=\"16\"></ore icon> | <ore icon name=\"check\" size=\"16\"></ore icon> |\n| async validation | <ore icon name=\"check\" size=\"16\"></ore icon> `.checkasync()` | <ore icon name=\"check\" size=\"16\"></ore icon> | <ore icon name=\"check\" size=\"16\"></ore icon> |\n| error flattening | <ore icon name=\"check\" size=\"16\"></ore icon> `flatten()` + `flattenfirst()` | <ore icon name=\"check\" size=\"16\"></ore icon> | partial |\n| zero dependencies | <ore icon name=\"check\" size=\"16\"></ore icon> | <ore icon name=\"check\" size=\"16\"></ore icon> | <ore icon name=\"x\" size=\"16\"></ore icon> |\n\n<div class=\"decision callout\">\n\n**use spell when** you want a fluent schema api with strong typescript inference, structured errors, and no third party runtime dependencies.\n\n**consider alternatives when** you are already standardized on another validator ecosystem and migration cost outweighs the api benefits.\n\n</div>\n\n## installation\n\nuse your workspace package manager to add spell.\n\n::: code group\n\n```sh [pnpm]\npnpm add @vielzeug/spell\n```\n\n```sh [npm]\nnpm install @vielzeug/spell\n```\n\n```sh [yarn]\nyarn add @vielzeug/spell\n```\n\n:::\n\n## quick start\n\nstart with a schema, then parse unknown input and use the inferred output type everywhere else.\n\n```ts\nimport { s, type infer } from '@vielzeug/spell';\n\nconst user = s\n .object({\n email: s.string().email(),\n name: s.string().min(1),\n role: s.enum(['admin', 'editor', 'viewer'] as const),\n })\n .relaxed(); // allow extra keys — omit for strict mode (default)\n\ntype user = infer<typeof user>;\n\nconst payload: unknown = {\n email: 'ada@example.com',\n name: 'ada',\n role: 'admin',\n team: 'platform',\n};\n\nconst result = user.safeparse(payload);\n\nif (!result.success) throw result.error;\nconst user = result.data;\n```\n\n## features\n\n<div class=\"features grid\">\n\n namespace and tree shakeable schema builders.\n sync and async parsing with `parse()`, `safeparse()`, `parseasync()`, and `safeparseasync()`.\n dependency free standard schema v1 interoperability through `schema['~standard']`.\n explicit `check()` and `checkasync()` rules; sync parsing never skips an async check.\n wrapper modes for `optional`, `nullable`, `nullish`, `default`, `catch`, and `required`.\n frozen declarative definitions through `definition()` and json schema export via `fromdefinition()` from `@vielzeug/spell/json`.\n request local message customization through `createparsecontext()`.\n ordered union parsing produces the same selected branch in sync and async modes.\n structured errors with direct path lookup and flattened views.\n object parsing is hardened against prototype pollution style keys.\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 [forge](/forge/) — typed form state that uses spell schemas as its validation layer\n [courier](/courier/) — http client for validating request and response payloads at service boundaries\n [vault](/vault/) — unified storage api that accepts spell schemas to type gate persisted data\n\n</div>\n\n<! markdownlint enable md025 md033 md060 >\n",
|
|
1265
|
-
"api": " \ntitle: spell — api reference\ndescription: reference for spell schema builders, parsing, errors, context, and tooling exports.\n \n\n[[toc]]\n\n## api overview\n\n| symbol | purpose | execution mode | common gotcha |\n| | | | |\n| `s` | creates schemas | sync or async, depending on checks | `checkasync()` requires async parsing |\n| `createparsecontext()` | creates request local messages | sync | pass the context to each parse call |\n| `spellvalidationerror` | provides validation issues | sync/async parse failures | use `safeparse()` for expected invalid data |\n\n## package entry point\n\n| import | purpose |\n| | |\n| `@vielzeug/spell` | schema builders, errors, types, and parse contexts |\n| `@vielzeug/spell/json` | convert portable definitions to json schema |\n| `@vielzeug/spell/predicates` | standalone format and type predicates |\n\n```ts\nimport { createparsecontext, s, type infer } from '@vielzeug/spell';\nimport { fromdefinition } from '@vielzeug/spell/json';\nimport { isemail } from '@vielzeug/spell/predicates';\n```\n\n## `s`\n\nall builders live under `s`.\n\n| builder | purpose |\n| | |\n| `string`, `number`, `boolean`, `bigint`, `date` | primitive values |\n| `literal`, `enum`, `null`, `undefined`, `unknown`, `never` | exact and universal values |\n| `array`, `tuple`, `set`, `map`, `record`, `object` | collections |\n| `union`, `intersect`, `discriminatedunion`, `lazy` | composition |\n| `coerce.*` | coercing primitive schemas |\n\n```ts\nconst user = s.object({\n email: s.string().email(),\n id: s.string().uuid(),\n role: s.enum(['admin', 'member'] as const),\n});\n\ntype user = infer<typeof user>;\n```\n\nobject schemas reject unknown keys. use `.relaxed()` to retain extras.\n\n## standard schema interoperability\n\nevery schema implements the dependency free standard schema v1 structural protocol through `schema['~standard']`. consumers that accept standard schema validators can use spell schemas directly without an adapter.\n\n```ts\nconst user = s.object({ name: s.string() });\nconst result = await user['~standard'].validate({ name: 'ada' });\n```\n\nsuccessful results contain `value`; failures contain portable `{ message, path }` issues. failed unions remain one stable union issue instead of selecting a branch heuristically.\n\nthe exported `standardschemav1` interface mirrors the full v1 protocol, including `validate(value, options?)` and the `inferinput` and `inferoutput` namespace helpers.\n\n## parsing\n\nevery schema provides:\n\n```ts\nschema.parse(value, context?); // output or spellvalidationerror\nschema.safeparse(value, context?); // parseresult<output>\nschema.parseasync(value, context?); // promise<output>\nschema.safeparseasync(value, context?); // promise<parseresult<output>>\nschema.is(value); // value is output\nschema.assert(value, label?); // assertion\n```\n\n`parse()`, `safeparse()`, `is()`, and `assert()` are available on synchronous schemas. calling `checkasync()` returns an async only schema, where typescript exposes only `parseasync()` and `safeparseasync()`. that async only mode propagates through compositional schemas when a child is asynchronous.\n\n## custom checks\n\n`check()` is synchronous. `checkasync()` is asynchronous. do not return a promise from `check()`.\n\n```ts\nconst signup = s.object({ confirm: s.string(), password: s.string() }).check((value, context) => {\n if (value.password !== value.confirm) {\n context.addissue({ code: 'custom', message: 'passwords must match', path: ['confirm'] });\n }\n});\n\nconst availableemail = s\n .string()\n .email()\n .checkasync(async (value) => {\n return (await emailavailable(value)) || 'email is already registered';\n });\n```\n\n`checkcontext.addissue()` takes `{ code, message, params?, path? }`. paths are relative to current schema.\n\n## modifiers and transforms\n\n```ts\ns.string().optional();\ns.string().nullable();\ns.string().nullish();\ns.string().required();\ns.string().default('guest');\ns.string().catch('guest');\ns.string()\n .trim()\n .transform((value) => value.tolowercase());\ns.string().pipe(s.string().slug());\ns.string().label('user name');\n```\n\n`default()`, `catch()`, preprocessors, transforms, and checks are runtime behavior. they cannot become portable definitions.\n\n## definitions and json schema\n\n`definition()` is only for schemas containing declarative structure. it returns frozen data and throws `spelldefinitionerror` when runtime behavior is present.\n\n```ts\nimport { s } from '@vielzeug/spell';\nimport { fromdefinition } from '@vielzeug/spell/json';\n\nconst product = s.object({\n id: s.string().uuid(),\n name: s.string().min(1),\n});\n\nconst definition = product.definition();\nconst jsonschema = fromdefinition(definition);\n```\n\nno implicit schema to json conversion exists. make definition boundary explicit.\n\n## parse contexts\n\n`createparsecontext()` creates immutable request local validation messages.\n\n```ts\nfunction createparsecontext(messages?: deeppartial<messages>): parsecontext;\n```\n\n**returns:** a parse context with built in messages merged with the supplied overrides.\n\n```ts\nimport { createparsecontext, s } from '@vielzeug/spell';\n\nconst context = createparsecontext({\n object: { invalidkeys: () => 'unsupported field' },\n});\n\nconst result = s.object({ email: s.string().email() }).safeparse({ email: 'ada@example.com', extra: true }, context);\n\nif (!result.success) {\n const messages = result.error.messagesat('email');\n console.log(messages);\n}\n```\n\n## types\n\n### inference types\n\n| type | resolves to |\n| | |\n| `infer<tschema>` | parsed output; alias of `inferoutput<tschema>` |\n| `inferinput<tschema>` | input accepted before parsing or transformation |\n| `inferoutput<tschema>` | value returned after parsing and transformation |\n\ncomposite schemas preserve child input types. object input types make fields optional when their field schema accepts `undefined`.\n\n### standard schema\n\n```ts\ninterface standardschemav1<input = unknown, output = input> {\n readonly '~standard': standardschemav1.props<input, output>;\n}\n\nnamespace standardschemav1 {\n interface props<input = unknown, output = input> {\n readonly version: 1;\n readonly vendor: string;\n readonly validate: (value: unknown, options?: options) => result<output> | promise<result<output>>;\n readonly types?: types<input, output>;\n }\n\n type result<output> = { readonly value: output; readonly issues?: undefined } | { readonly issues: readonly issue[] };\n interface options { readonly libraryoptions?: record<string, unknown> }\n interface issue { readonly message: string; readonly path?: readonly (propertykey | pathsegment)[] }\n interface pathsegment { readonly key: propertykey }\n interface types<input = unknown, output = input> { readonly input: input; readonly output: output }\n type inferinput<schema extends standardschemav1> = nonnullable<schema['~standard']['types']>['input'];\n type inferoutput<schema extends standardschemav1> = nonnullable<schema['~standard']['types']>['output'];\n}\n```\n\n### parse result and issues\n\n```ts\ntype parseresult<t> = { data: t; success: true } | { error: spellvalidationerror; success: false };\n\ntype issue =\n | { code: 'custom'; message: string; params?: record<string, unknown>; path: (string | number)[] }\n | { code: 'invalid_base64'; message: string; params: { format: string }; path: (string | number)[] }\n | { code: 'invalid_date'; message: string; params?: undefined; path: (string | number)[] }\n | { code: 'invalid_duration'; message: string; params: { format: string }; path: (string | number)[] }\n | { code: 'invalid_enum'; message: string; params: { values: readonly unknown[] }; path: (string | number)[] }\n | { code: 'invalid_finite'; message: string; params?: undefined; path: (string | number)[] }\n | { code: 'invalid_integer'; message: string; params?: undefined; path: (string | number)[] }\n | { code: 'invalid_keys'; message: string; params: { keys: string[] }; path: (string | number)[] }\n | { code: 'invalid_length'; message: string; params: { exact: number }; path: (string | number)[] }\n | { code: 'invalid_literal'; message: string; params: { expected: unknown }; path: (string | number)[] }\n | { code: 'invalid_multiple_of'; message: string; params: { step: number | bigint }; path: (string | number)[] }\n | { code: 'invalid_safe'; message: string; params?: undefined; path: (string | number)[] }\n | {\n code: 'invalid_string';\n message: string;\n params: { format?: string; includes?: string; pattern?: string; prefix?: string; suffix?: string };\n path: (string | number)[];\n }\n | { code: 'invalid_type'; message: string; params?: undefined; path: (string | number)[] }\n | { code: 'invalid_union'; message: string; params: { errors: issue[][] }; path: (string | number)[] }\n | { code: 'invalid_unique'; message: string; params: { unique: true }; path: (string | number)[] }\n | { code: 'invalid_url'; message: string; params: { format: string }; path: (string | number)[] }\n | {\n code: 'invalid_variant';\n message: string;\n params: { discriminator: string; expected: string[] };\n path: (string | number)[];\n }\n | {\n code: 'too_big';\n message: string;\n params: { exclusive?: boolean; max: number | bigint | date };\n path: (string | number)[];\n }\n | {\n code: 'too_small';\n message: string;\n params: { exclusive?: boolean; min: number | bigint | date };\n path: (string | number)[];\n }\n | { code: string & {}; message: string; params?: record<string, unknown>; path: (string | number)[] };\n```\n\n`errorcode` is a const object mapping each issue code to its string literal.\n\n### validation contracts\n\n```ts\ntype parsecontext = { messages: messages };\n\ntype checkcontext = {\n addissue: (issue: {\n code: string;\n message: string;\n params?: record<string, unknown>;\n path?: (string | number)[];\n }) => void;\n};\n\ntype validateresult = boolean | null | undefined | string;\n```\n\n### messages\n\n```ts\ntype messagefn<ctx extends record<string, unknown> = record<string, unknown>> = string | ((ctx: ctx) => string);\n\ntype messages = {\n array: { length: (ctx: { exact: number; value: unknown[] }) => string; max: (ctx: { max: number; value: unknown[] }) => string; min: (ctx: { min: number; value: unknown[] }) => string; nonempty: () => string; type: () => string; unique: () => string };\n bigint: { max: (ctx: { max: bigint; value: bigint }) => string; min: (ctx: { min: bigint; value: bigint }) => string; multipleof: (ctx: { step: bigint; value: bigint }) => string; negative: () => string; nonnegative: () => string; nonpositive: () => string; positive: () => string; type: () => string };\n boolean: { type: () => string };\n check: { default: () => string };\n date: { max: (ctx: { max: date; value: date }) => string; min: (ctx: { min: date; value: date }) => string; type: () => string };\n enum: { invalid: (ctx: { values: readonly unknown[] }) => string };\n instanceof: { type: (ctx: { classname: string }) => string };\n literal: { expected: (ctx: { expected: unknown }) => string };\n map: { max: (ctx: { max: number; value: map<unknown, unknown> }) => string; min: (ctx: { min: number; value: map<unknown, unknown> }) => string; nonempty: () => string; size: (ctx: { exact: number; value: map<unknown, unknown> }) => string; type: () => string };\n never: { invalid: () => string };\n number: { finite: () => string; int: () => string; max: (ctx: { max: number; value: number }) => string; min: (ctx: { min: number; value: number }) => string; multipleof: (ctx: { step: number; value: number }) => string; negative: () => string; nonnegative: () => string; nonpositive: () => string; positive: () => string; safe: () => string; type: () => string };\n object: { invalidkeys: (ctx: { keys: string[] }) => string; type: () => string };\n set: { max: (ctx: { max: number; value: set<unknown> }) => string; min: (ctx: { min: number; value: set<unknown> }) => string; nonempty: () => string; size: (ctx: { exact: number; value: set<unknown> }) => string; type: () => string };\n string: { base64: () => string; base64url: () => string; cuid: () => string; cuid2: () => string; date: () => string; datetime: () => string; duration: () => string; email: () => string; emoji: () => string; endswith: (ctx: { suffix: string; value: string }) => string; hex: () => string; hexcolor: () => string; includes: (ctx: { substr: string; value: string }) => string; ip: () => string; jwt: () => string; length: (ctx: { exact: number; value: string }) => string; max: (ctx: { max: number; value: string }) => string; min: (ctx: { min: number; value: string }) => string; nanoid: () => string; nonempty: () => string; numeric: () => string; regex: (ctx: { value: string }) => string; semver: () => string; slug: () => string; startswith: (ctx: { prefix: string; value: string }) => string; time: () => string; type: () => string; ulid: () => string; url: () => string; uuid: () => string };\n tuple: { length: (ctx: { exact: number }) => string; min: (ctx: { min: number }) => string; type: () => string };\n union: { invalid: () => string };\n variant: { invaliddiscriminator: (ctx: { discriminator: string; expected: string[] }) => string; type: () => string };\n};\n\ntype deeppartial<t> = {\n [k in keyof t]?: t[k] extends record<string, unknown> ? deeppartial<t[k]> : t[k];\n};\n```\n\n### descriptor and json schema\n\n```ts\ntype schemadescriptor = basedescriptor &\n (\n | { kind: 'unknown' | 'never' | 'boolean' | 'bigint' | 'date' | 'lazy' }\n | { classname: string; kind: 'instanceof' }\n | { contentencoding?: string; format?: string; kind: 'string'; maxlength?: number; minlength?: number; pattern?: string | null }\n | { exclusivemaximum?: number; exclusiveminimum?: number; kind: 'number'; maximum?: number; minimum?: number; multipleof?: number; typehint?: 'integer' }\n | { kind: 'literal'; value: string | number | boolean | null | undefined }\n | { kind: 'enum'; values: readonly (string | number)[] }\n | { items: schemadescriptor; kind: 'array'; maxitems?: number; minitems?: number }\n | { items: schemadescriptor[]; kind: 'tuple'; rest: schemadescriptor | null }\n | { fields: record<string, schemadescriptor>; kind: 'object'; strict: boolean }\n | { key: schemadescriptor; kind: 'record'; value: schemadescriptor }\n | { items: schemadescriptor; kind: 'set' }\n | { key: schemadescriptor; kind: 'map'; value: schemadescriptor }\n | { branches: schemadescriptor[]; kind: 'union' | 'intersect' }\n | { branches: record<string, schemadescriptor>; discriminator: string; kind: 'variant' }\n | { from: schemadescriptor; kind: 'pipe'; to: schemadescriptor }\n );\n\ntype jsonschema = record<string, unknown>;\n```\n\n### schema traversal\n\n`schemawalker<r>` is the visitor type accepted by `schema.walk()`. handlers are optional and correspond to schema kinds; composite handlers also receive their walked children. use `unknown` as the fallback handler.\n\n### error helpers\n\n```ts\ntype flaterror = { messages: string[]; path: (string | number)[] };\ntype flaterrorfirst = { message: string; path: (string | number)[] };\n```\n\n## errors\n\n `spellerror` — base class. use `instanceof spellerror` for cross boundary narrowing.\n `spellvalidationerror` — validation failure with `issues`, `messagesat()`, `flatten()`, and `flattenfirst()`.\n `spelldefinitionerror` — schema cannot create portable definition.\n\n```ts\nconst result = s.object({ email: s.string().email() }).safeparse({ email: 'invalid' });\n\nif (!result.success) {\n const { fielderrors, formerrors } = result.error.flatten();\n console.log(fielderrors, formerrors);\n}\n```\n",
|
|
1266
|
-
"usage": " \ntitle: spell — usage guide\ndescription: learn how to build schemas, compose wrappers, customize locales, and integrate spell with other vielzeug packages.\n \n\n[[toc]]\n\n## basic usage\n\nstart with `safeparse()` when you want explicit success and failure branches.\n\n```ts\nimport { s } from '@vielzeug/spell';\n\nconst signup = s.object({\n email: s.string().email(),\n password: s.string().min(12),\n referralcode: s.string().optional(),\n});\n\nconst result = signup.safeparse({\n email: 'ada@example.com',\n password: 'horse battery staple',\n});\n\nif (!result.success) {\n console.error(result.error.issues);\n} else {\n console.log(result.data.email);\n}\n```\n\nuse `parse()` when invalid input should throw immediately. use `safeparse()` when invalid input is part of normal control flow.\n\n## building schemas\n\nuse the namespace form when readability matters more than bundle trimming.\n\n```ts\nimport { s } from '@vielzeug/spell';\n\nconst article = s.object({\n id: s.string().uuid(),\n title: s.string().trim().min(1).max(120),\n slug: s.string().slug(),\n tags: s.array(s.string().min(1)).default(() => []),\n meta: s\n .object({\n published: s.boolean(),\n publishedat: s.date().nullable(),\n })\n .relaxed(),\n});\n```\n\n```ts\nimport { s } from '@vielzeug/spell';\n\nconst todo = s.object({\n done: s.boolean(),\n tags: s.array(s.string().min(1)).default(() => []),\n title: s.string().min(1),\n});\n```\n\nobject schemas reject unknown keys by default. call `.relaxed()` when you need to preserve extra properties.\n\ncall `.defaults()` to get a fully default filled object without providing any input. every required field must have a `.default()` set, or a `spellvalidationerror` is thrown. call `.partialdefaults()` when only some fields have defaults — fields without a default are silently omitted instead of throwing.\n\n```ts\nconst config = s.object({\n host: s.string().default('localhost'),\n port: s.number().default(3000),\n});\n\nconfig.defaults(); // { host: 'localhost', port: 3000 }\n\nconst form = s.object({ name: s.string(), role: s.string().default('viewer') });\nform.partialdefaults(); // { role: 'viewer' }\n```\n\n## wrapper modes, defaults, and fallbacks\n\nchain wrappers to describe missing values and recovery rules without losing schema metadata.\n\n```ts\nimport { s } from '@vielzeug/spell';\n\nconst displayname = s.string().trim().min(2).label('display name').optional().default('guest').nullable();\n\ndisplayname.parse(undefined); // 'guest'\ndisplayname.parse(null); // null\ndisplayname.description; // 'display name'\n```\n\ncall `.required()` to remove `undefined` without removing `null`.\n\n```ts\nimport { s } from '@vielzeug/spell';\n\nconst nullablebutrequired = s.string().optional().nullable().required();\n\nnullablebutrequired.parse('ada');\nnullablebutrequired.parse(null);\n// nullablebutrequired.parse(undefined); // throws\n```\n\nuse `.catch()` when you want a fallback output after validation fails.\n\n```ts\nimport { s } from '@vielzeug/spell';\n\nconst port = s.number().int().min(1).max(65535).catch(3000);\n\nport.parse('not a number'); // 3000\n```\n\n## custom validation\n\nuse `check()` for synchronous domain rules and `checkasync()` for asynchronous rules. sync parsing rejects schemas with asynchronous checks.\n\n```ts\nimport { s } from '@vielzeug/spell';\n\n// boolean shorthand: return false to fail with default message\nconst evennumber = s.number().check((n) => n % 2 === 0);\n\n// string shorthand: return the message as a string\nconst username = s\n .string()\n .min(3)\n .check((v) => !v.startswith('_') || 'cannot start with underscore');\n\n// multiple issues via ctx.addissue()\nconst signup = s.object({ confirm: s.string(), password: s.string() }).check((v, ctx) => {\n if (v.password !== v.confirm) {\n ctx.addissue({ code: 'custom', message: 'passwords must match', path: ['confirm'] });\n }\n});\n```\n\n`checkasync()` returns an async only schema: typescript exposes `parseasync()` and `safeparseasync()` but not `parse()` or `safeparse()`. this mode survives fluent modifiers and propagates through nested arrays, objects, unions, intersections, tuples, maps, records, sets, lazy schemas, pipelines, and `s.discriminatedunion(...)` branches. sync parsing also fails at runtime instead of accepting an unchecked value.\n\n```ts\nimport { s } from '@vielzeug/spell';\n\nconst takenemails = new set(['ada@example.com']);\n\nconst accountemail = s\n .string()\n .email()\n .checkasync(async (value, ctx) => {\n if (takenemails.has(value)) {\n ctx.addissue({ code: 'custom', message: 'email is already taken', path: [] });\n }\n });\n\n// async checks require parseasync\nawait accountemail.parseasync('grace@example.com');\n```\n\nuse `check()` for predicate only rules too. return `true` on success or message on failure.\n\n```ts\nimport { s } from '@vielzeug/spell';\n\nconst positiveprice = s.number().check((value) => value > 0 || 'must be positive');\npositiveprice.parse(9.99);\n```\n\n## strings, numbers, and safe regex usage\n\nuse schema helpers for common string and number constraints instead of hand written predicates.\n\n```ts\nimport { s } from '@vielzeug/spell';\n\nconst password = s.string().min(12).regex(/[a z]/).regex(/[0 9]/);\nconst price = s.number().nonnegative().multipleof(0.01);\nconst launchwindow = s.date().min(new date('2025 01 01t00:00:00.000z'));\n```\n\nspell strips stateful `/g` and `/y` flags from `regex()` patterns before validation. repeated parses stay deterministic even when the original regular expression is reused.\n\n## coercion and transforms\n\nuse coercion when input arrives as strings, query parameters, or form values.\n\n```ts\nimport { s } from '@vielzeug/spell';\n\nconst query = s.object({\n draft: s.coerce.boolean().default(false),\n limit: s.coerce.number().int().positive().default(20),\n publishedat: s.coerce.date().nullable(),\n search: s.coerce.string().trim().min(1).optional(),\n});\n\nconst parsed = query.parse({\n draft: 'true',\n limit: '50',\n publishedat: '2025 04 01t12:00:00.000z',\n search: ' vielzeug ',\n});\n```\n\nuse `transform()` or `pipe()` after validation when downstream code needs a different output shape.\n\n```ts\nimport { s } from '@vielzeug/spell';\n\nconst trimmedtags = s.array(s.string().trim().min(1)).transform((tags) => tags.map((tag) => tag.tolowercase()));\nconst slug = s.string().trim().min(1).pipe(s.string().slug());\n```\n\n## introspection, round trips, and json schema\n\nuse declarative definitions when schemas need to cross process boundaries or feed tooling.\n\n```ts\nimport { s } from '@vielzeug/spell';\nimport { fromdefinition } from '@vielzeug/spell/json';\n\nconst product = s\n .object({\n id: s.string().uuid(),\n name: s.string().min(1),\n price: s.number().positive().multipleof(0.01),\n })\n .label('product');\n\nconst definition = product.definition();\nconst jsonschema = fromdefinition(definition);\n\nproduct.parse({ id: '550e8400 e29b 41d4 a716 446655440000', name: 'keyboard', price: 129.99 });\nconsole.log(jsonschema.title);\n```\n\ndefinitions are frozen serializable snapshots of declarative schema structure. use `definition()` and `fromdefinition()` for external tooling. schemas with runtime checks, transforms, defaults, catches, or preprocessors intentionally have no definition.\n\n## messages\n\nspell has no mutable process wide configuration. build one parse context per request, locale, or form, then pass it explicitly.\n\n```ts\nimport { createparsecontext, s } from '@vielzeug/spell';\n\nconst user = s.object({ email: s.string().email() });\nconst german = createparsecontext({\n object: { invalidkeys: () => 'keine unbekannten felder erlaubt' },\n});\n\nuser.safeparse({ email: 'ada@example.com', extra: true }, german);\n```\n\ninternal development warnings always use `console.warn` in development builds. route application diagnostics in application code instead of mutating library wide logger state.\n\n## working with validation errors\n\nuse `spellvalidationerror` helpers when you need ui ready error structures.\n\n```ts\nimport { s, spellvalidationerror } from '@vielzeug/spell';\n\nconst user = s.object({\n email: s.string().email(),\n profile: s.object({\n name: s.string().min(2),\n }),\n});\n\nconst result = user.safeparse({ email: 'nope', profile: { name: '' } });\n\nif (!result.success && result.error instanceof spellvalidationerror) {\n const profileerrors = result.error.messagesat('profile', 'name');\n console.log(profileerrors);\n}\n```\n\nunion failures produce one stable `invalid_union` issue. use `s.discriminatedunion()` when a discriminator can identify the intended object branch and provide field specific errors.\n\n## schema traversal with walk()\n\nuse `walk()` to inspect or transform a schema tree without importing internal implementation classes.\n\n```ts\nimport { s, type schemawalker } from '@vielzeug/spell';\n\nconst fields: string[] = [];\n\nconst collectfields: schemawalker<void> = {\n object(schema) {\n for (const [key, child] of object.entries(schema.shape)) {\n fields.push(key);\n child.walk(collectfields);\n }\n },\n unknown() {},\n};\n\nconst user = s.object({\n email: s.string().email(),\n profile: s.object({ name: s.string() }),\n});\n\nuser.walk(collectfields);\nconsole.log(fields); // ['email', 'profile', 'name']\n```\n\n`walk()` dispatches by `schema.kind`. if no handler matches and no `unknown` fallback is provided, `walk()` returns `null`. add an `unknown` handler to capture any kind not explicitly listed in your visitor.\n\n## framework integration\n\nspell works anywhere you can call a function before state enters your app.\n\n::: code group\n\n```tsx [react]\nimport { s } from '@vielzeug/spell';\n\nconst searchparams = s\n .object({\n page: s.coerce.number().int().positive().default(1),\n q: s.string().trim().optional(),\n })\n .relaxed();\n\nexport function searchpage({ rawparams }: { rawparams: unknown }) {\n const params = searchparams.parse(rawparams);\n\n return (\n <div>\n {params.q ?? 'all results'} — page {params.page}\n </div>\n );\n}\n```\n\n```ts [vue]\nimport { computed, ref } from 'vue';\nimport { s } from '@vielzeug/spell';\n\nconst settings = s.object({\n locale: s.string().min(2),\n compact: s.coerce.boolean().default(false),\n});\n\nconst raw = ref<unknown>({ locale: 'en', compact: 'true' });\nconst settings = computed(() => settings.parse(raw.value));\n```\n\n:::\n\nuse `safeparse()` at event boundaries and `parse()` inside trusted data flows.\n\n## working with other vielzeug libraries\n\nuse spell as the validation layer and let other packages focus on transport, forms, or storage.\n\n```ts\nimport { createform } from '@vielzeug/forge';\nimport { schemavalidator } from '@vielzeug/forge/schema';\nimport { createcourier } from '@vielzeug/courier';\nimport { s } from '@vielzeug/spell';\n\nconst profile = s.object({\n displayname: s.string().min(2),\n newsletter: s.boolean(),\n});\n\nconst form = createform({\n initialvalues: {\n displayname: '',\n newsletter: false,\n },\n validate: schemavalidator(profile),\n});\n\nconst courier = createcourier({ baseurl: '/api' });\nconst profile = profile.parse(await courier.get('/profile'));\n```\n\nuse spell definitions with `@vielzeug/codex` or other tooling when you need generated docs or external schema consumers.\n\n## best practices\n\n keep schemas close to the boundary where unknown data enters your app.\n use `s` consistently for construction; use explicit `/json` and `/predicates` subpaths for tooling.\n use `.default(() => value)` for mutable defaults such as arrays, objects, `map`, and `set`.\n call `.required()` when you want to remove `undefined` but keep `null` semantics intact.\n use `check()` with a `ctx` argument when you need `ctx.addissue()`; return a message for simple predicate failures.\n use `checkasync()` and `parseasync()` for every asynchronous domain rule.\n build a parse context per request or test; never rely on mutable process wide configuration.\n use `definition()` with `fromdefinition()` from `@vielzeug/spell/json` for external tooling.\n",
|
|
1312
|
+
"index": " \ntitle: spell — schema validation for typescript\ndescription: zero dependency schema validation with explicit async checks, standard schema interoperability, and json schema export.\npackage: spell\ncategory: validation\nkeywords: [schema, validation, parsing, standard schema, json schema, locale, typescript, descriptors]\nrelated: [forge, courier, vault]\nexports:\n [s, createparsecontext, joinissuepath, spellvalidationerror, spelldefinitionerror, standardschemav1, errorcode, './json', './predicates']\nenvironments: [browser, node, ssr, deno]\n \n\n<! markdownlint disable md025 md033 md060 >\n\n<packagehero package=\"spell\" />\n\n## why spell?\n\nspell keeps runtime validation, static inference, and portable definitions in one api. use `s` for schema construction; import json conversion and predicates from dedicated subpaths.\n\nthis example shows the difference between manual branching and a single reusable schema.\n\n```ts\n// before\nfunction parseuserbefore(value: unknown) {\n if (typeof value !== 'object' || value === null) throw new error('expected object');\n\n const candidate = value as record<string, unknown>;\n\n if (typeof candidate.email !== 'string' || !candidate.email.includes('@')) {\n throw new error('expected valid email');\n }\n\n if (typeof candidate.role !== 'string' || !['admin', 'editor', 'viewer'].includes(candidate.role)) {\n throw new error('expected valid role');\n }\n\n return {\n email: candidate.email,\n role: candidate.role,\n };\n}\n\n// after\nimport { s } from '@vielzeug/spell';\n\nconst user = s.object({\n email: s.string().email(),\n role: s.enum(['admin', 'editor', 'viewer'] as const),\n});\n\nconst user = user.parse({ email: 'ada@example.com', role: 'admin' });\n```\n\n| feature | spell | zod | yup |\n| | | | |\n| bundle size | <packageinfo package=\"spell\" type=\"size\" /> | ~62 kb | ~14 kb |\n| type inference | <ore icon name=\"check\" size=\"16\"></ore icon> `infer<t>` | <ore icon name=\"check\" size=\"16\"></ore icon> | partial |\n| coercion api | <ore icon name=\"check\" size=\"16\"></ore icon> `s.coerce.*` | <ore icon name=\"check\" size=\"16\"></ore icon> | <ore icon name=\"check\" size=\"16\"></ore icon> |\n| async validation | <ore icon name=\"check\" size=\"16\"></ore icon> `.checkasync()` | <ore icon name=\"check\" size=\"16\"></ore icon> | <ore icon name=\"check\" size=\"16\"></ore icon> |\n| error flattening | <ore icon name=\"check\" size=\"16\"></ore icon> `flatten()` + `flattenfirst()` | <ore icon name=\"check\" size=\"16\"></ore icon> | partial |\n| zero dependencies | <ore icon name=\"check\" size=\"16\"></ore icon> | <ore icon name=\"check\" size=\"16\"></ore icon> | <ore icon name=\"x\" size=\"16\"></ore icon> |\n\n<div class=\"decision callout\">\n\n**use spell when** you want a fluent schema api with strong typescript inference, structured errors, and no third party runtime dependencies.\n\n**consider alternatives when** you are already standardized on another validator ecosystem and migration cost outweighs the api benefits.\n\n</div>\n\n## installation\n\nuse your workspace package manager to add spell.\n\n::: code group\n\n```sh [pnpm]\npnpm add @vielzeug/spell\n```\n\n```sh [npm]\nnpm install @vielzeug/spell\n```\n\n```sh [yarn]\nyarn add @vielzeug/spell\n```\n\n:::\n\n## quick start\n\nstart with a schema, then parse unknown input and use the inferred output type everywhere else.\n\n```ts\nimport { s, type infer } from '@vielzeug/spell';\n\nconst user = s\n .object({\n email: s.string().email(),\n name: s.string().min(1),\n role: s.enum(['admin', 'editor', 'viewer'] as const),\n })\n .relaxed(); // allow extra keys — omit for strict mode (default)\n\ntype user = infer<typeof user>;\n\nconst payload: unknown = {\n email: 'ada@example.com',\n name: 'ada',\n role: 'admin',\n team: 'platform',\n};\n\nconst result = user.safeparse(payload);\n\nif (!result.success) throw result.error;\nconst user = result.data;\n```\n\n## features\n\n<div class=\"features grid\">\n\n namespace and tree shakeable schema builders.\n sync and async parsing with `parse()`, `safeparse()`, `parseasync()`, and `safeparseasync()`.\n dependency free standard schema v1 interoperability through `schema['~standard']`.\n explicit `check()` and `checkasync()` rules; sync parsing never skips an async check.\n wrapper modes for `optional`, `nullable`, `nullish`, `default`, `catch`, and `required`.\n frozen declarative definitions through `definition()` and json schema export via `fromdefinition()` from `@vielzeug/spell/json`.\n request local message customization through `createparsecontext()`.\n ordered union parsing produces the same selected branch in sync and async modes.\n structured errors with direct path lookup and flattened views.\n object parsing is hardened against prototype pollution style keys.\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 [forge](/forge/) — typed form state that uses spell schemas as its validation layer\n [courier](/courier/) — http client for validating request and response payloads at service boundaries\n [vault](/vault/) — unified storage api that accepts spell schemas to type gate persisted data\n\n</div>\n\n<! markdownlint enable md025 md033 md060 >\n",
|
|
1313
|
+
"api": " \ntitle: spell — api reference\ndescription: reference for spell schema builders, parsing, errors, context, and tooling exports.\n \n\n[[toc]]\n\n## api overview\n\n| symbol | purpose | execution mode | common gotcha |\n| | | | |\n| `s` | creates schemas | sync or async, depending on checks | `checkasync()` requires async parsing |\n| `createparsecontext()` | creates request local messages | sync | pass the context to each parse call |\n| `joinissuepath()` | flattens issue paths to ui keys | sync | empty or undefined paths join to `''` |\n| `spellvalidationerror` | provides validation issues | sync/async parse failures | use `safeparse()` for expected invalid data |\n\n## package entry point\n\n| import | purpose |\n| | |\n| `@vielzeug/spell` | schema builders, errors, types, and parse contexts |\n| `@vielzeug/spell/json` | convert portable definitions to json schema |\n| `@vielzeug/spell/predicates` | standalone format and type predicates |\n\n```ts\nimport { createparsecontext, s, type infer } from '@vielzeug/spell';\nimport { fromdefinition } from '@vielzeug/spell/json';\nimport { isemail } from '@vielzeug/spell/predicates';\n```\n\n## `s`\n\nall builders live under `s`.\n\n| builder | purpose |\n| | |\n| `string`, `number`, `boolean`, `bigint`, `date` | primitive values |\n| `literal`, `enum`, `null`, `undefined`, `unknown`, `never` | exact and universal values |\n| `array`, `tuple`, `set`, `map`, `record`, `object` | collections |\n| `union`, `intersect`, `discriminatedunion`, `lazy` | composition |\n| `coerce.*` | coercing primitive schemas |\n\n```ts\nconst user = s.object({\n email: s.string().email(),\n id: s.string().uuid(),\n role: s.enum(['admin', 'member'] as const),\n});\n\ntype user = infer<typeof user>;\n```\n\nobject schemas reject unknown keys. use `.relaxed()` to retain extras.\n\n## standard schema interoperability\n\nevery schema implements the dependency free standard schema v1 structural protocol through `schema['~standard']`. consumers that accept standard schema validators can use spell schemas directly without an adapter.\n\n```ts\nconst user = s.object({ name: s.string() });\nconst result = await user['~standard'].validate({ name: 'ada' });\n```\n\nsuccessful results contain `value`; failures contain portable `{ message, path }` issues. failed unions remain one stable union issue instead of selecting a branch heuristically.\n\nthe exported `standardschemav1` interface mirrors the full v1 protocol, including `validate(value, options?)` and the `inferinput` and `inferoutput` namespace helpers.\n\n## parsing\n\nevery schema provides:\n\n```ts\nschema.parse(value, context?); // output or spellvalidationerror\nschema.safeparse(value, context?); // parseresult<output>\nschema.parseasync(value, context?); // promise<output>\nschema.safeparseasync(value, context?); // promise<parseresult<output>>\nschema.is(value); // value is output\nschema.assert(value, label?); // assertion\n```\n\n`parse()`, `safeparse()`, `is()`, and `assert()` are available on synchronous schemas. calling `checkasync()` returns an async only schema, where typescript exposes only `parseasync()` and `safeparseasync()`. that async only mode propagates through compositional schemas when a child is asynchronous.\n\nui adapters that accept `anyschema` cannot call `safeparse()` on it — the structural surface type omits it. type adapter parameters against `syncparsable` instead:\n\n```ts\nimport type { syncparsable } from '@vielzeug/spell';\n\nfunction spellvalidator(schema: syncparsable<unknown>) {\n const result = schema.safeparse(value); // types cleanly, no shim or cast\n}\n```\n\nthe shape is structural, so an async only schema is assignable to `syncparsable`; calling `safeparse()` on one returns a failed result (\"parse() cannot evaluate async checks\"). when the mode is not statically known, narrow with `inferschemamode` and use `safeparseasync()` for async members.\n\n## custom checks\n\n`check()` is synchronous. `checkasync()` is asynchronous. do not return a promise from `check()`.\n\n```ts\nconst signup = s.object({ confirm: s.string(), password: s.string() }).check((value, context) => {\n if (value.password !== value.confirm) {\n context.addissue({ code: 'custom', message: 'passwords must match', path: ['confirm'] });\n }\n});\n\nconst availableemail = s\n .string()\n .email()\n .checkasync(async (value) => {\n return (await emailavailable(value)) || 'email is already registered';\n });\n```\n\n`checkcontext.addissue()` takes `{ code, message, params?, path? }`. paths are relative to current schema.\n\n## modifiers and transforms\n\n```ts\ns.string().optional();\ns.string().nullable();\ns.string().nullish();\ns.string().required();\ns.string().default('guest');\ns.string().catch('guest');\ns.string()\n .trim()\n .transform((value) => value.tolowercase());\ns.string().pipe(s.string().slug());\ns.string().label('user name');\n```\n\n`default()`, `catch()`, preprocessors, transforms, and checks are runtime behavior. they cannot become portable definitions.\n\n## definitions and json schema\n\n`definition()` is only for schemas containing declarative structure. it returns frozen data and throws `spelldefinitionerror` when runtime behavior is present.\n\n```ts\nimport { s } from '@vielzeug/spell';\nimport { fromdefinition } from '@vielzeug/spell/json';\n\nconst product = s.object({\n id: s.string().uuid(),\n name: s.string().min(1),\n});\n\nconst definition = product.definition();\nconst jsonschema = fromdefinition(definition);\n```\n\nno implicit schema to json conversion exists. make definition boundary explicit.\n\n## parse contexts\n\n`createparsecontext()` creates immutable request local validation messages.\n\n```ts\nfunction createparsecontext(messages?: deeppartial<messages>): parsecontext;\n```\n\n**returns:** a parse context with built in messages merged with the supplied overrides.\n\n```ts\nimport { createparsecontext, s } from '@vielzeug/spell';\n\nconst context = createparsecontext({\n object: { invalidkeys: () => 'unsupported field' },\n});\n\nconst result = s.object({ email: s.string().email() }).safeparse({ email: 'ada@example.com', extra: true }, context);\n\nif (!result.success) {\n const messages = result.error.messagesat('email');\n console.log(messages);\n}\n```\n\n## types\n\n### inference types\n\n| type | resolves to |\n| | |\n| `infer<tschema>` | parsed output; alias of `inferoutput<tschema>` |\n| `inferinput<tschema>` | input accepted before parsing or transformation |\n| `inferoutput<tschema>` | value returned after parsing and transformation |\n\ncomposite schemas preserve child input types. object input types make fields optional when their field schema accepts `undefined`.\n\n### standard schema\n\n```ts\ninterface standardschemav1<input = unknown, output = input> {\n readonly '~standard': standardschemav1.props<input, output>;\n}\n\nnamespace standardschemav1 {\n interface props<input = unknown, output = input> {\n readonly version: 1;\n readonly vendor: string;\n readonly validate: (value: unknown, options?: options) => result<output> | promise<result<output>>;\n readonly types?: types<input, output>;\n }\n\n type result<output> = { readonly value: output; readonly issues?: undefined } | { readonly issues: readonly issue[] };\n interface options { readonly libraryoptions?: record<string, unknown> }\n interface issue { readonly message: string; readonly path?: readonly (propertykey | pathsegment)[] }\n interface pathsegment { readonly key: propertykey }\n interface types<input = unknown, output = input> { readonly input: input; readonly output: output }\n type inferinput<schema extends standardschemav1> = nonnullable<schema['~standard']['types']>['input'];\n type inferoutput<schema extends standardschemav1> = nonnullable<schema['~standard']['types']>['output'];\n}\n```\n\n### parse result and issues\n\n```ts\ntype parseresult<t> = { data: t; success: true } | { error: spellvalidationerror; success: false };\n\ntype issue =\n | { code: 'custom'; message: string; params?: record<string, unknown>; path: (string | number)[] }\n | { code: 'invalid_base64'; message: string; params: { format: string }; path: (string | number)[] }\n | { code: 'invalid_date'; message: string; params?: undefined; path: (string | number)[] }\n | { code: 'invalid_duration'; message: string; params: { format: string }; path: (string | number)[] }\n | { code: 'invalid_enum'; message: string; params: { values: readonly unknown[] }; path: (string | number)[] }\n | { code: 'invalid_finite'; message: string; params?: undefined; path: (string | number)[] }\n | { code: 'invalid_integer'; message: string; params?: undefined; path: (string | number)[] }\n | { code: 'invalid_keys'; message: string; params: { keys: string[] }; path: (string | number)[] }\n | { code: 'invalid_length'; message: string; params: { exact: number }; path: (string | number)[] }\n | { code: 'invalid_literal'; message: string; params: { expected: unknown }; path: (string | number)[] }\n | { code: 'invalid_multiple_of'; message: string; params: { step: number | bigint }; path: (string | number)[] }\n | { code: 'invalid_safe'; message: string; params?: undefined; path: (string | number)[] }\n | {\n code: 'invalid_string';\n message: string;\n params: { format?: string; includes?: string; pattern?: string; prefix?: string; suffix?: string };\n path: (string | number)[];\n }\n | { code: 'invalid_type'; message: string; params?: undefined; path: (string | number)[] }\n | { code: 'invalid_union'; message: string; params: { errors: issue[][] }; path: (string | number)[] }\n | { code: 'invalid_unique'; message: string; params: { unique: true }; path: (string | number)[] }\n | { code: 'invalid_url'; message: string; params: { format: string }; path: (string | number)[] }\n | {\n code: 'invalid_variant';\n message: string;\n params: { discriminator: string; expected: string[] };\n path: (string | number)[];\n }\n | {\n code: 'too_big';\n message: string;\n params: { exclusive?: boolean; max: number | bigint | date };\n path: (string | number)[];\n }\n | {\n code: 'too_small';\n message: string;\n params: { exclusive?: boolean; min: number | bigint | date };\n path: (string | number)[];\n }\n | { code: string & {}; message: string; params?: record<string, unknown>; path: (string | number)[] };\n```\n\n`errorcode` is a const object mapping each issue code to its string literal.\n\n### validation contracts\n\n```ts\ntype parsecontext = { messages: messages };\n\ntype checkcontext = {\n addissue: (issue: {\n code: string;\n message: string;\n params?: record<string, unknown>;\n path?: (string | number)[];\n }) => void;\n};\n\ntype validateresult = boolean | null | undefined | string;\n```\n\n### messages\n\n```ts\ntype messagefn<ctx extends record<string, unknown> = record<string, unknown>> = string | ((ctx: ctx) => string);\n\ntype messages = {\n array: { length: (ctx: { exact: number; value: unknown[] }) => string; max: (ctx: { max: number; value: unknown[] }) => string; min: (ctx: { min: number; value: unknown[] }) => string; nonempty: () => string; type: () => string; unique: () => string };\n bigint: { max: (ctx: { max: bigint; value: bigint }) => string; min: (ctx: { min: bigint; value: bigint }) => string; multipleof: (ctx: { step: bigint; value: bigint }) => string; negative: () => string; nonnegative: () => string; nonpositive: () => string; positive: () => string; type: () => string };\n boolean: { type: () => string };\n check: { default: () => string };\n date: { max: (ctx: { max: date; value: date }) => string; min: (ctx: { min: date; value: date }) => string; type: () => string };\n enum: { invalid: (ctx: { values: readonly unknown[] }) => string };\n instanceof: { type: (ctx: { classname: string }) => string };\n literal: { expected: (ctx: { expected: unknown }) => string };\n map: { max: (ctx: { max: number; value: map<unknown, unknown> }) => string; min: (ctx: { min: number; value: map<unknown, unknown> }) => string; nonempty: () => string; size: (ctx: { exact: number; value: map<unknown, unknown> }) => string; type: () => string };\n never: { invalid: () => string };\n number: { finite: () => string; int: () => string; max: (ctx: { max: number; value: number }) => string; min: (ctx: { min: number; value: number }) => string; multipleof: (ctx: { step: number; value: number }) => string; negative: () => string; nonnegative: () => string; nonpositive: () => string; positive: () => string; safe: () => string; type: () => string };\n object: { invalidkeys: (ctx: { keys: string[] }) => string; type: () => string };\n set: { max: (ctx: { max: number; value: set<unknown> }) => string; min: (ctx: { min: number; value: set<unknown> }) => string; nonempty: () => string; size: (ctx: { exact: number; value: set<unknown> }) => string; type: () => string };\n string: { base64: () => string; base64url: () => string; cuid: () => string; cuid2: () => string; date: () => string; datetime: () => string; duration: () => string; email: () => string; emoji: () => string; endswith: (ctx: { suffix: string; value: string }) => string; hex: () => string; hexcolor: () => string; includes: (ctx: { substr: string; value: string }) => string; ip: () => string; jwt: () => string; length: (ctx: { exact: number; value: string }) => string; max: (ctx: { max: number; value: string }) => string; min: (ctx: { min: number; value: string }) => string; nanoid: () => string; nonempty: () => string; numeric: () => string; regex: (ctx: { value: string }) => string; semver: () => string; slug: () => string; startswith: (ctx: { prefix: string; value: string }) => string; time: () => string; type: () => string; ulid: () => string; url: () => string; uuid: () => string };\n tuple: { length: (ctx: { exact: number }) => string; min: (ctx: { min: number }) => string; type: () => string };\n union: { invalid: () => string };\n variant: { invaliddiscriminator: (ctx: { discriminator: string; expected: string[] }) => string; type: () => string };\n};\n\ntype deeppartial<t> = {\n [k in keyof t]?: t[k] extends record<string, unknown> ? deeppartial<t[k]> : t[k];\n};\n```\n\n### descriptor and json schema\n\n```ts\ntype schemadescriptor = basedescriptor &\n (\n | { kind: 'unknown' | 'never' | 'boolean' | 'bigint' | 'date' | 'lazy' }\n | { classname: string; kind: 'instanceof' }\n | { contentencoding?: string; format?: string; kind: 'string'; maxlength?: number; minlength?: number; pattern?: string | null }\n | { exclusivemaximum?: number; exclusiveminimum?: number; kind: 'number'; maximum?: number; minimum?: number; multipleof?: number; typehint?: 'integer' }\n | { kind: 'literal'; value: string | number | boolean | null | undefined }\n | { kind: 'enum'; values: readonly (string | number)[] }\n | { items: schemadescriptor; kind: 'array'; maxitems?: number; minitems?: number }\n | { items: schemadescriptor[]; kind: 'tuple'; rest: schemadescriptor | null }\n | { fields: record<string, schemadescriptor>; kind: 'object'; strict: boolean }\n | { key: schemadescriptor; kind: 'record'; value: schemadescriptor }\n | { items: schemadescriptor; kind: 'set' }\n | { key: schemadescriptor; kind: 'map'; value: schemadescriptor }\n | { branches: schemadescriptor[]; kind: 'union' | 'intersect' }\n | { branches: record<string, schemadescriptor>; discriminator: string; kind: 'variant' }\n | { from: schemadescriptor; kind: 'pipe'; to: schemadescriptor }\n );\n\ntype jsonschema = record<string, unknown>;\n```\n\n### schema traversal\n\n`schemawalker<r>` is the visitor type accepted by `schema.walk()`. handlers are optional and correspond to schema kinds; composite handlers also receive their walked children. use `unknown` as the fallback handler.\n\n### error helpers\n\n```ts\ntype flaterror = { messages: string[]; path: (string | number)[] };\ntype flaterrorfirst = { message: string; path: (string | number)[] };\n\n/** an issue path in either shape: spell native segments or standard schema `{ key }` segments. */\ntype issuepathsegment = propertykey | { readonly key: propertykey };\ntype issuepath = readonly issuepathsegment[];\n\n/** the synchronous parse surface; `anyschema` omits `safeparse`, adapter parameters use this. */\ntype syncparsable<t = unknown> = { safeparse(value: unknown): parseresult<t> };\n\nfunction joinissuepath(path: issuepath | undefined, separator?: string): string;\n```\n\n`joinissuepath()` flattens an issue path into a ui key such as `'permissions.0.slug'`. it accepts spell native `(string | number)[]` paths and standard schema paths with `{ key }` segments, and returns `''` for empty or undefined paths.\n\n## errors\n\n `spellerror` — base class. use `instanceof spellerror` for cross boundary narrowing.\n `spellvalidationerror` — validation failure with `issues`, `messagesat()`, `flatten()`, and `flattenfirst()`.\n `spelldefinitionerror` — schema cannot create portable definition.\n\n```ts\nconst result = s.object({ email: s.string().email() }).safeparse({ email: 'invalid' });\n\nif (!result.success) {\n const { fielderrors, formerrors } = result.error.flatten();\n console.log(fielderrors, formerrors);\n}\n```\n",
|
|
1314
|
+
"usage": " \ntitle: spell — usage guide\ndescription: learn how to build schemas, compose wrappers, customize locales, and integrate spell with other vielzeug packages.\n \n\n[[toc]]\n\n## basic usage\n\nstart with `safeparse()` when you want explicit success and failure branches.\n\n```ts\nimport { s } from '@vielzeug/spell';\n\nconst signup = s.object({\n email: s.string().email(),\n password: s.string().min(12),\n referralcode: s.string().optional(),\n});\n\nconst result = signup.safeparse({\n email: 'ada@example.com',\n password: 'horse battery staple',\n});\n\nif (!result.success) {\n console.error(result.error.issues);\n} else {\n console.log(result.data.email);\n}\n```\n\nuse `parse()` when invalid input should throw immediately. use `safeparse()` when invalid input is part of normal control flow.\n\n## building schemas\n\nuse the namespace form when readability matters more than bundle trimming.\n\n```ts\nimport { s } from '@vielzeug/spell';\n\nconst article = s.object({\n id: s.string().uuid(),\n title: s.string().trim().min(1).max(120),\n slug: s.string().slug(),\n tags: s.array(s.string().min(1)).default(() => []),\n meta: s\n .object({\n published: s.boolean(),\n publishedat: s.date().nullable(),\n })\n .relaxed(),\n});\n```\n\n```ts\nimport { s } from '@vielzeug/spell';\n\nconst todo = s.object({\n done: s.boolean(),\n tags: s.array(s.string().min(1)).default(() => []),\n title: s.string().min(1),\n});\n```\n\nobject schemas reject unknown keys by default. call `.relaxed()` when you need to preserve extra properties.\n\ncall `.defaults()` to get a fully default filled object without providing any input. every required field must have a `.default()` set, or a `spellvalidationerror` is thrown. call `.partialdefaults()` when only some fields have defaults — fields without a default are silently omitted instead of throwing.\n\n```ts\nconst config = s.object({\n host: s.string().default('localhost'),\n port: s.number().default(3000),\n});\n\nconfig.defaults(); // { host: 'localhost', port: 3000 }\n\nconst form = s.object({ name: s.string(), role: s.string().default('viewer') });\nform.partialdefaults(); // { role: 'viewer' }\n```\n\n## wrapper modes, defaults, and fallbacks\n\nchain wrappers to describe missing values and recovery rules without losing schema metadata.\n\n```ts\nimport { s } from '@vielzeug/spell';\n\nconst displayname = s.string().trim().min(2).label('display name').optional().default('guest').nullable();\n\ndisplayname.parse(undefined); // 'guest'\ndisplayname.parse(null); // null\ndisplayname.description; // 'display name'\n```\n\ncall `.required()` to remove `undefined` without removing `null`.\n\n```ts\nimport { s } from '@vielzeug/spell';\n\nconst nullablebutrequired = s.string().optional().nullable().required();\n\nnullablebutrequired.parse('ada');\nnullablebutrequired.parse(null);\n// nullablebutrequired.parse(undefined); // throws\n```\n\nuse `.catch()` when you want a fallback output after validation fails.\n\n```ts\nimport { s } from '@vielzeug/spell';\n\nconst port = s.number().int().min(1).max(65535).catch(3000);\n\nport.parse('not a number'); // 3000\n```\n\n## custom validation\n\nuse `check()` for synchronous domain rules and `checkasync()` for asynchronous rules. sync parsing rejects schemas with asynchronous checks.\n\n```ts\nimport { s } from '@vielzeug/spell';\n\n// boolean shorthand: return false to fail with default message\nconst evennumber = s.number().check((n) => n % 2 === 0);\n\n// string shorthand: return the message as a string\nconst username = s\n .string()\n .min(3)\n .check((v) => !v.startswith('_') || 'cannot start with underscore');\n\n// multiple issues via ctx.addissue()\nconst signup = s.object({ confirm: s.string(), password: s.string() }).check((v, ctx) => {\n if (v.password !== v.confirm) {\n ctx.addissue({ code: 'custom', message: 'passwords must match', path: ['confirm'] });\n }\n});\n```\n\n`checkasync()` returns an async only schema: typescript exposes `parseasync()` and `safeparseasync()` but not `parse()` or `safeparse()`. this mode survives fluent modifiers and propagates through nested arrays, objects, unions, intersections, tuples, maps, records, sets, lazy schemas, pipelines, and `s.discriminatedunion(...)` branches. sync parsing also fails at runtime instead of accepting an unchecked value.\n\n```ts\nimport { s } from '@vielzeug/spell';\n\nconst takenemails = new set(['ada@example.com']);\n\nconst accountemail = s\n .string()\n .email()\n .checkasync(async (value, ctx) => {\n if (takenemails.has(value)) {\n ctx.addissue({ code: 'custom', message: 'email is already taken', path: [] });\n }\n });\n\n// async checks require parseasync\nawait accountemail.parseasync('grace@example.com');\n```\n\nuse `check()` for predicate only rules too. return `true` on success or message on failure.\n\n```ts\nimport { s } from '@vielzeug/spell';\n\nconst positiveprice = s.number().check((value) => value > 0 || 'must be positive');\npositiveprice.parse(9.99);\n```\n\n## strings, numbers, and safe regex usage\n\nuse schema helpers for common string and number constraints instead of hand written predicates.\n\n```ts\nimport { s } from '@vielzeug/spell';\n\nconst password = s.string().min(12).regex(/[a z]/).regex(/[0 9]/);\nconst price = s.number().nonnegative().multipleof(0.01);\nconst launchwindow = s.date().min(new date('2025 01 01t00:00:00.000z'));\n```\n\nspell strips stateful `/g` and `/y` flags from `regex()` patterns before validation. repeated parses stay deterministic even when the original regular expression is reused.\n\n## coercion and transforms\n\nuse coercion when input arrives as strings, query parameters, or form values.\n\n```ts\nimport { s } from '@vielzeug/spell';\n\nconst query = s.object({\n draft: s.coerce.boolean().default(false),\n limit: s.coerce.number().int().positive().default(20),\n publishedat: s.coerce.date().nullable(),\n search: s.coerce.string().trim().min(1).optional(),\n});\n\nconst parsed = query.parse({\n draft: 'true',\n limit: '50',\n publishedat: '2025 04 01t12:00:00.000z',\n search: ' vielzeug ',\n});\n```\n\nuse `transform()` or `pipe()` after validation when downstream code needs a different output shape.\n\n```ts\nimport { s } from '@vielzeug/spell';\n\nconst trimmedtags = s.array(s.string().trim().min(1)).transform((tags) => tags.map((tag) => tag.tolowercase()));\nconst slug = s.string().trim().min(1).pipe(s.string().slug());\n```\n\n## introspection, round trips, and json schema\n\nuse declarative definitions when schemas need to cross process boundaries or feed tooling.\n\n```ts\nimport { s } from '@vielzeug/spell';\nimport { fromdefinition } from '@vielzeug/spell/json';\n\nconst product = s\n .object({\n id: s.string().uuid(),\n name: s.string().min(1),\n price: s.number().positive().multipleof(0.01),\n })\n .label('product');\n\nconst definition = product.definition();\nconst jsonschema = fromdefinition(definition);\n\nproduct.parse({ id: '550e8400 e29b 41d4 a716 446655440000', name: 'keyboard', price: 129.99 });\nconsole.log(jsonschema.title);\n```\n\ndefinitions are frozen serializable snapshots of declarative schema structure. use `definition()` and `fromdefinition()` for external tooling. schemas with runtime checks, transforms, defaults, catches, or preprocessors intentionally have no definition.\n\n## messages\n\nspell has no mutable process wide configuration. build one parse context per request, locale, or form, then pass it explicitly.\n\n```ts\nimport { createparsecontext, s } from '@vielzeug/spell';\n\nconst user = s.object({ email: s.string().email() });\nconst german = createparsecontext({\n object: { invalidkeys: () => 'keine unbekannten felder erlaubt' },\n});\n\nuser.safeparse({ email: 'ada@example.com', extra: true }, german);\n```\n\ninternal development warnings always use `console.warn` in development builds. route application diagnostics in application code instead of mutating library wide logger state.\n\n## working with validation errors\n\nuse `spellvalidationerror` helpers when you need ui ready error structures.\n\n```ts\nimport { s, spellvalidationerror } from '@vielzeug/spell';\n\nconst user = s.object({\n email: s.string().email(),\n profile: s.object({\n name: s.string().min(2),\n }),\n});\n\nconst result = user.safeparse({ email: 'nope', profile: { name: '' } });\n\nif (!result.success && result.error instanceof spellvalidationerror) {\n const profileerrors = result.error.messagesat('profile', 'name');\n console.log(profileerrors);\n}\n```\n\nkey inline errors by dot path with `joinissuepath()`, which also unwraps standard schema `{ key }` segments:\n\n```ts\nimport { joinissuepath } from '@vielzeug/spell';\n\nfor (const issue of result.error.issues) {\n console.log(joinissuepath(issue.path), issue.message); // 'profile.name: ...'\n}\n```\n\nunion failures produce one stable `invalid_union` issue. use `s.discriminatedunion()` when a discriminator can identify the intended object branch and provide field specific errors.\n\n## validating synchronously in react\n\nadapters that validate inside a render or `validate` callback need the sync parse surface. `anyschema` does not expose `safeparse`; type the adapter parameter against `syncparsable` instead:\n\n```ts\nimport { type syncparsable } from '@vielzeug/spell';\n\nfunction spellvalidator(schema: syncparsable<unknown>) {\n return (values: unknown) => {\n const result = schema.safeparse(values);\n if (result.success) return undefined;\n\n return result.error.issues.map((issue) => ({\n message: issue.message,\n path: issue.path,\n }));\n };\n}\n\nspellvalidator(s.object({ email: s.string() })); // no shim or cast\n```\n\n## schema traversal with walk()\n\nuse `walk()` to inspect or transform a schema tree without importing internal implementation classes.\n\n```ts\nimport { s, type schemawalker } from '@vielzeug/spell';\n\nconst fields: string[] = [];\n\nconst collectfields: schemawalker<void> = {\n object(schema) {\n for (const [key, child] of object.entries(schema.shape)) {\n fields.push(key);\n child.walk(collectfields);\n }\n },\n unknown() {},\n};\n\nconst user = s.object({\n email: s.string().email(),\n profile: s.object({ name: s.string() }),\n});\n\nuser.walk(collectfields);\nconsole.log(fields); // ['email', 'profile', 'name']\n```\n\n`walk()` dispatches by `schema.kind`. if no handler matches and no `unknown` fallback is provided, `walk()` returns `null`. add an `unknown` handler to capture any kind not explicitly listed in your visitor.\n\n## framework integration\n\nspell works anywhere you can call a function before state enters your app.\n\n::: code group\n\n```tsx [react]\nimport { s } from '@vielzeug/spell';\n\nconst searchparams = s\n .object({\n page: s.coerce.number().int().positive().default(1),\n q: s.string().trim().optional(),\n })\n .relaxed();\n\nexport function searchpage({ rawparams }: { rawparams: unknown }) {\n const params = searchparams.parse(rawparams);\n\n return (\n <div>\n {params.q ?? 'all results'} — page {params.page}\n </div>\n );\n}\n```\n\n```ts [vue]\nimport { computed, ref } from 'vue';\nimport { s } from '@vielzeug/spell';\n\nconst settings = s.object({\n locale: s.string().min(2),\n compact: s.coerce.boolean().default(false),\n});\n\nconst raw = ref<unknown>({ locale: 'en', compact: 'true' });\nconst settings = computed(() => settings.parse(raw.value));\n```\n\n:::\n\nuse `safeparse()` at event boundaries and `parse()` inside trusted data flows.\n\n## working with other vielzeug libraries\n\nuse spell as the validation layer and let other packages focus on transport, forms, or storage.\n\n```ts\nimport { createform } from '@vielzeug/forge';\nimport { schemavalidator } from '@vielzeug/forge/schema';\nimport { createcourier } from '@vielzeug/courier';\nimport { s } from '@vielzeug/spell';\n\nconst profile = s.object({\n displayname: s.string().min(2),\n newsletter: s.boolean(),\n});\n\nconst form = createform({\n initialvalues: {\n displayname: '',\n newsletter: false,\n },\n validate: schemavalidator(profile),\n});\n\nconst courier = createcourier({ baseurl: '/api' });\nconst profile = profile.parse(await courier.get('/profile'));\n```\n\nuse spell definitions with `@vielzeug/codex` or other tooling when you need generated docs or external schema consumers.\n\n## best practices\n\n keep schemas close to the boundary where unknown data enters your app.\n use `s` consistently for construction; use explicit `/json` and `/predicates` subpaths for tooling.\n use `.default(() => value)` for mutable defaults such as arrays, objects, `map`, and `set`.\n call `.required()` when you want to remove `undefined` but keep `null` semantics intact.\n use `check()` with a `ctx` argument when you need `ctx.addissue()`; return a message for simple predicate failures.\n use `checkasync()` and `parseasync()` for every asynchronous domain rule.\n build a parse context per request or test; never rely on mutable process wide configuration.\n use `definition()` with `fromdefinition()` from `@vielzeug/spell/json` for external tooling.\n",
|
|
1267
1315
|
"examples": " \ntitle: spell — examples\ndescription: practical examples and recipes for spell.\n \n\n## examples\n\n [validating api payloads](./examples/api.md)\n [form safe parsing](./examples/forms.md)\n [async business rules](./examples/async.md)\n [schema introspection and round trips](./examples/introspection.md)\n [unions, intersections, and variants](./examples/unions.md)\n [schema traversal with walk()](./examples/walk.md)\n"
|
|
1268
1316
|
},
|
|
1269
1317
|
"examples": [
|
|
@@ -1340,12 +1388,12 @@
|
|
|
1340
1388
|
"text": "wrappers & defaults // optional(), nullable(), default(), catch() — missing value semantics\nimport { s } from '@vielzeug/spell'\n\n// optional: accepts undefined, passes through validation otherwise\nconst nickname = s.string().min(2).optional().default('guest')\n\nconsole.log(nickname.parse(undefined)) // 'guest'\nconsole.log(nickname.parse('ada')) // 'ada'\n\n// nullable: accepts null explicitly\nconst bio = s.string().max(200).nullable()\n\nconsole.log(bio.parse(null)) // null\nconsole.log(bio.parse('loves types')) // 'loves types'\n\n// nullish: accepts both null and undefined\nconst avatar = s.string().url().nullish()\n\nconsole.log(avatar.parse(null)) // null\nconsole.log(avatar.parse(undefined)) // undefined\n\n// required(): strips undefined without removing null\nconst nullablebutrequired = s.string().optional().nullable().required()\nconsole.log(nullablebutrequired.parse(null)) // null\nconsole.log(nullablebutrequired.safeparse(undefined).success) // false\n\n// catch(): returns a fallback when validation fails — never throws\nconst port = s.number().int().min(1).max(65535).catch(3000)\nconsole.log(port.parse(8080)) // 8080\nconsole.log(port.parse('not a port')) // 3000"
|
|
1341
1389
|
}
|
|
1342
1390
|
],
|
|
1343
|
-
"exports": "s createparsecontext spellvalidationerror spelldefinitionerror standardschemav1 errorcode ./json ./predicates",
|
|
1391
|
+
"exports": "s createparsecontext joinissuepath spellvalidationerror spelldefinitionerror standardschemav1 errorcode ./json ./predicates",
|
|
1344
1392
|
"keywords": "schema validation parsing standard schema json schema locale typescript descriptors",
|
|
1345
1393
|
"name": "@vielzeug/spell",
|
|
1346
1394
|
"related": "forge courier vault",
|
|
1347
1395
|
"slug": "spell",
|
|
1348
|
-
"source": "export type {\n anyschema,\n checkcontext,\n flaterror,\n flaterrorfirst,\n infer,\n inferinput,\n inferoutput,\n issue,\n jsonschema,\n messagefn,\n messages,\n parsecontext,\n parseresult,\n schemadescriptor,\n schemawalker,\n standardschemav1,\n validateresult,\n} from './core';\nexport { errorcode, schema, spelldefinitionerror, spellerror, spellvalidationerror } from './core';\nexport { createparsecontext, type deeppartial } from './messages';\nexport { s } from './s';\n"
|
|
1396
|
+
"source": "export type {\n anyschema,\n checkcontext,\n flaterror,\n flaterrorfirst,\n infer,\n inferinput,\n inferoutput,\n issue,\n issuepath,\n issuepathsegment,\n jsonschema,\n messagefn,\n messages,\n parsecontext,\n parseresult,\n schemadescriptor,\n schemawalker,\n standardschemav1,\n syncparsable,\n validateresult,\n} from './core';\nexport { errorcode, joinissuepath, schema, spelldefinitionerror, spellerror, spellvalidationerror } from './core';\nexport { createparsecontext, type deeppartial } from './messages';\nexport { s } from './s';\n"
|
|
1349
1397
|
},
|
|
1350
1398
|
{
|
|
1351
1399
|
"category": "time",
|