@vielzeug/codex 2.2.8 → 2.2.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/data/catalog.json +171 -22
- package/data/llms-full.txt +14178 -11054
- package/data/llms.txt +6 -2
- package/data/manifest.json +1 -1
- package/data/packages/courier.json +2 -2
- package/data/packages/dnd.json +2 -2
- package/data/packages/focus.json +37 -0
- package/data/packages/forge.json +9 -10
- package/data/packages/gesture.json +25 -0
- package/data/packages/illusionist.json +132 -0
- package/data/packages/lingua.json +4 -3
- package/data/packages/ore.json +4 -9
- package/data/packages/ripple.json +1 -1
- package/data/packages/sentinel.json +35 -0
- package/data/packages/sourcerer.json +30 -29
- package/data/refine.json +3921 -3960
- package/data/search.json +148 -24
- package/package.json +1 -1
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
{
|
|
2
|
-
"apiSource": "export {\n LinguaDisposedError,\n LinguaError,\n LinguaInvalidCatalogError,\n LinguaInvalidLocaleError,\n LinguaInvalidPluralCountError,\n LinguaInvalidStateError,\n LinguaMissingCatalogError,\n} from './errors';\nexport {\n createTranslationStore,\n hydrateTranslationStore,\n type TranslationSnapshot,\n type TranslationStore,\n} from './i18n';\nexport { createCatalogTranslator, createTranslator, type Translator } from './translator';\nexport type {\n Catalog,\n CatalogLoader,\n CatalogNode,\n CatalogSource,\n CatalogSources,\n Catalogs,\n CatalogTranslatorOptions,\n Locale,\n MessageKey,\n PluralCategory,\n PluralKey,\n PluralMessage,\n PluralOptions,\n SubscribeOptions,\n TextKey,\n TranslateOptions,\n TranslationState,\n TranslationStoreOptions,\n TranslatorOptions,\n Values,\n} from './types';\n",
|
|
2
|
+
"apiSource": "export { catalogKeys } from './catalog';\nexport {\n LinguaDisposedError,\n LinguaError,\n LinguaInvalidCatalogError,\n LinguaInvalidLocaleError,\n LinguaInvalidPluralCountError,\n LinguaInvalidStateError,\n LinguaMissingCatalogError,\n} from './errors';\nexport {\n createTranslationStore,\n hydrateTranslationStore,\n type TranslationSnapshot,\n type TranslationStore,\n} from './i18n';\nexport { createCatalogTranslator, createTranslator, type Translator } from './translator';\nexport type {\n Catalog,\n CatalogLoader,\n CatalogNode,\n CatalogSource,\n CatalogSources,\n Catalogs,\n CatalogTranslatorOptions,\n Locale,\n MessageKey,\n PluralCategory,\n PluralKey,\n PluralMessage,\n PluralOptions,\n SubscribeOptions,\n TextKey,\n TranslateOptions,\n TranslationState,\n TranslationStoreOptions,\n TranslatorOptions,\n Values,\n} from './types';\n",
|
|
3
3
|
"docs": {
|
|
4
4
|
"index": "---\ntitle: Lingua — Explicit localization for TypeScript\ndescription: Framework-neutral locale catalogs, typed translations, and explicit plural messages.\npackage: lingua\ncategory: i18n\nkeywords: [internationalization, translations, pluralization, locale, i18n, catalog-loading]\nrelated: [ripple, wayfinder, courier]\nexports: [createCatalogTranslator, createTranslationStore, createTranslator, hydrateTranslationStore, LinguaError, LinguaDisposedError, LinguaInvalidCatalogError, LinguaInvalidLocaleError, LinguaInvalidPluralCountError, LinguaInvalidStateError, LinguaMissingCatalogError]\nenvironments: [browser, node, ssr, deno]\n---\n\n<!-- markdownlint-disable MD025 MD033 MD060 -->\n\n<PackageHero package=\"lingua\" />\n\n## Why Lingua?\n\nLingua separates immutable translation from mutable locale state. Use one catalog per locale, then select static or stateful API from whether locale can change.\n\n```ts\n// Before\nconst message = catalogs[locale]?.inbox?.[count === 1 ? 'one' : 'other'] ?? 'inbox';\n\n// After\nconst output = i18n.translate('inbox', { count });\n```\n\n| Feature | Lingua | i18next | FormatJS |\n| --- | --- | --- | --- |\n| Bundle size | <PackageInfo package=\"lingua\" type=\"size\" /> | Varies by selected modules | Varies by selected modules |\n| Zero runtime dependencies | <ore-icon name=\"check\" size=\"16\"></ore-icon> | <ore-icon name=\"triangle-alert\" size=\"16\"></ore-icon> | <ore-icon name=\"triangle-alert\" size=\"16\"></ore-icon> |\n| Explicit plural catalog nodes | <ore-icon name=\"check\" size=\"16\"></ore-icon> | Convention/config dependent | ICU-message dependent |\n| Declared lazy locale catalogs | <ore-icon name=\"check\" size=\"16\"></ore-icon> | Plugin/config dependent | Application-defined |\n| Immutable locale snapshots | <ore-icon name=\"check\" size=\"16\"></ore-icon> | Application-defined | Application-defined |\n\n<div class=\"decision-callout\">\n\n**Use Lingua when** you need a compact TypeScript runtime with explicit catalog structure, deterministic fallback, and framework-neutral subscriptions.\n\n**Consider i18next or FormatJS when** you need their plugin ecosystems, message extraction pipelines, or framework-specific integrations.\n\n</div>\n\n## Installation\n\n::: code-group\n\n```sh [pnpm]\npnpm add @vielzeug/lingua\n```\n\n```sh [npm]\nnpm install @vielzeug/lingua\n```\n\n```sh [yarn]\nyarn add @vielzeug/lingua\n```\n\n:::\n\n## Quick Start\n\nCreate locale store with static catalogs, then dispose it when owner ends.\n\n```ts\nimport { createTranslationStore } from '@vielzeug/lingua';\n\nconst i18n = createTranslationStore({\n catalogs: {\n de: { inbox: { plural: { one: 'Eine Nachricht', other: '{count} Nachrichten' } } },\n en: { inbox: { plural: { one: 'One message', other: '{count} messages' } } },\n },\n locale: 'en',\n});\n\ntry {\n console.log(i18n.translate('inbox', { count: 3 }));\n await i18n.setLocale('de');\n console.log(i18n.translate('inbox', { count: 1 }));\n} finally {\n i18n.dispose();\n}\n```\n\n## Features\n\n<div class=\"features-grid\">\n\n- `createCatalogTranslator()` compiles one immutable fixed-locale catalog.\n- `createTranslator()` compiles immutable locale-keyed catalogs.\n- `createTranslationStore()` manages locale changes and declared catalogs.\n- `translate()` renders text and plural messages through explicit catalog nodes.\n- `translateDynamic()` makes runtime-key lookup explicit.\n- `load()` deduplicates lazy catalog loading per locale.\n- `getSnapshot()` and `subscribe()` expose immutable translator revisions.\n- `serialize()` and `hydrateTranslationStore()` transfer resolved SSR catalogs.\n- `createFormatter()` and `validateCatalog()` remain isolated subpath tools.\n\n</div>\n\n## Documentation\n\n<div class=\"doc-links\">\n\n- [Usage Guide](./usage.md)\n- [API Reference](./api.md)\n- [Examples](./examples.md)\n- [Migration Guide](./migration.md)\n\n</div>\n\n## See Also\n\n<div class=\"see-also\">\n\n- [Ripple](../ripple/index.md) adapts Lingua snapshots into reactive application state.\n- [Courier](../courier/index.md) can fetch locale catalogs before passing them to Lingua loaders.\n- [Wayfinder](../wayfinder/index.md) can drive locale selection from route state.\n\n</div>\n\n<!-- markdownlint-enable MD025 MD033 MD060 -->\n",
|
|
5
|
-
"api": "---\ntitle: Lingua — API Reference\ndescription: Complete API reference for @vielzeug/lingua.\n---\n\n[[toc]]\n\n## API Overview\n\n| Symbol | Purpose | Execution mode | Common gotcha |\n| --- | --- | --- | --- |\n| `createCatalogTranslator()` | Compile one immutable locale catalog | Sync | No fallback locales |\n| `createTranslator()` | Compile immutable locale catalogs | Sync | Locale is fixed for translator lifetime |\n| `createTranslationStore()` | Create mutable locale and catalog store | Sync | Load lazy locale explicitly |\n| `hydrateTranslationStore()` | Create store from serialized loaded catalogs | Sync | Serialized state never includes loaders |\n| `createFormatter()` | Format Intl values from `/format` | Sync | Import from subpath |\n| `validateCatalog()` | Check explicit plural forms from `/validate` | Sync | Import from subpath |\n| `LinguaError` | Base class for Lingua errors | Sync | Use `LinguaError.is()` for broad narrowing |\n\n## Package Entry Point\n\n| Import | Purpose |\n| --- | --- |\n| `@vielzeug/lingua` | Translation factories, state types, and Lingua errors |\n| `@vielzeug/lingua/format` | `createFormatter()` and formatter types |\n| `@vielzeug/lingua/validate` | `validateCatalog()` and `ValidationIssue` |\n\n## Translation Factories\n\n### createCatalogTranslator\n\n```ts\nfunction createCatalogTranslator<C extends Catalog>(\n catalog: C,\n options?: CatalogTranslatorOptions,\n): Translator<C>;\n```\n\nCompiles one catalog and returns an immutable fixed-locale translator. Locale defaults to `en` and controls plural selection and diagnostics.\n\n| Parameter | Type | Description |\n| --- | --- | --- |\n| `catalog` | `C` | One catalog containing only messages and grouping objects |\n| `options` | `CatalogTranslatorOptions` | Locale and missing-message handlers; fallback is unavailable |\n\n**Returns:** `Translator<C>`.\n\n**Example:**\n\n```ts\nimport { createCatalogTranslator } from '@vielzeug/lingua';\n\nconst translator = createCatalogTranslator(\n { save: 'Enregistrer' },\n { locale: 'fr' },\n);\n\ntranslator.translate('save');\n```\n\n---\n\n### createTranslator\n\n```ts\nfunction createTranslator<C extends Catalog>(catalogs: Catalogs<C>, options?: TranslatorOptions): Translator<C>;\n```\n\nCompiles locale catalogs and returns immutable translator.\n\n| Parameter | Type | Description |\n| --- | --- | --- |\n| `catalogs` | `Catalogs<C>` | Locale-keyed catalog objects |\n| `options` | `TranslatorOptions` | Locale, fallback chain, and missing-message handlers |\n\n**Returns:** `Translator<C>`.\n\n**Example:**\n\n```ts\nimport { createTranslator } from '@vielzeug/lingua';\n\nconst translator = createTranslator(\n { en: { save: 'Save' }, fr: { save: 'Enregistrer' } },\n { locale: 'fr' },\n);\n\ntranslator.translate('save');\n```\n\n| Method | Signature | Returns |\n| --- | --- | --- |\n| `translate` | `(textKey, options?)` or `(pluralKey, { count, ordinal?, values? })` | Rendered string |\n| `translateDynamic` | `(key, options?)` | Rendered string for runtime key |\n| `segments` | `(textKey, { values })` or `(pluralKey, { count, ordinal?, values? })` | String and typed-value segments |\n| `segmentsDynamic` | `(key, options)` | Segments for runtime key |\n| `locale` | `Locale` | Resolved active locale |\n\n---\n\n### createTranslationStore\n\n```ts\nfunction createTranslationStore<C extends Catalog>(options: TranslationStoreOptions<C>): TranslationStore<C>;\n```\n\nCreates catalog store, current locale state, and immutable translator snapshots.\n\n| Parameter | Type | Description |\n| --- | --- | --- |\n| `options.catalogs` | `CatalogSources<C>` | Static catalogs or lazy locale loaders |\n| `options.locale` | `Locale` | Initial locale; defaults to `en` |\n| `options.fallback` | `Locale \\| readonly Locale[]` | Fallback locale chain |\n| `options.onMissingKey` | `(key, locale) => string` | Missing-message handler |\n| `options.onMissingValue` | `(name, key, locale) => string` | Missing-interpolation handler |\n\n**Returns:** `TranslationStore<C>`, with every `Translator<C>` method plus lifecycle methods.\n\n**Example:**\n\n```ts\nimport { createTranslationStore } from '@vielzeug/lingua';\n\nconst translations = createTranslationStore({\n catalogs: { en: { title: 'Home' }, fr: { title: 'Accueil' } },\n locale: 'en',\n});\n\nawait translations.setLocale('fr');\ntranslations.translate('title');\n```\n\n| Method or property | Signature | Returns |\n| --- | --- | --- |\n| `translate` | Translator method | Rendered string |\n| `segments` | Translator method | String and typed-value segments |\n| `load` | `({ locale? })` | `Promise<void>` after catalog resolution |\n| `setLocale` | `(locale)` | `Promise<void>` after locale commit; never loads implicitly |\n| `isLoaded` | `({ locale? })` | `boolean` |\n| `getSnapshot` | `()` | `TranslationSnapshot<C>` |\n| `subscribe` | `(listener, { immediate?, signal? })` | Unsubscribe function |\n| `serialize` | `()` | Loader-free `TranslationState<C>` |\n| `dispose` | `()` | `void` |\n| `locale` | `Locale` | Current canonical locale |\n| `disposed` | `boolean` | Disposal state |\n| `disposalSignal` | `AbortSignal` | Aborts on disposal |\n| `[Symbol.dispose]` | `()` | Delegates to `dispose()` |\n\n---\n\n### hydrateTranslationStore\n\n```ts\nfunction hydrateTranslationStore<C extends Catalog>(\n state: TranslationState<C>,\n options?: Omit<TranslationStoreOptions<C>, 'locale' | 'catalogs'>,\n): TranslationStore<C>;\n```\n\nCreates translation store from SSR state payload containing resolved raw catalogs.\n\n| Parameter | Type | Description |\n| --- | --- | --- |\n| `state` | `TranslationState<C>` | Version `3`, active locale, and loader-free catalogs |\n| `options` | `Omit<TranslationStoreOptions<C>, 'locale' \\| 'catalogs'>` | Fallback and missing-message handlers |\n\n**Returns:** `TranslationStore<C>`.\n\n**Example:**\n\n```ts\nimport { createTranslationStore, hydrateTranslationStore } from '@vielzeug/lingua';\n\nconst server = createTranslationStore({ catalogs: { en: { title: 'Home' } }, locale: 'en' });\nconst client = hydrateTranslationStore(server.serialize());\n\nclient.translate('title');\n```\n\n## Formatting and Validation\n\n### createFormatter\n\n```ts\nfunction createFormatter(source: string | (() => string)): Formatter;\n```\n\nCreates cached Intl formatters using static locale or locale getter.\n\n| Parameter | Type | Description |\n| --- | --- | --- |\n| `source` | `string \\| (() => string)` | Static locale or locale getter |\n\n**Returns:** `Formatter`.\n\n**Example:**\n\n```ts\nimport { createFormatter } from '@vielzeug/lingua/format';\n\nconst formatter = createFormatter('en-US');\nformatter.currency(19.99, 'USD');\n```\n\n| Method | Signature | Returns |\n| --- | --- | --- |\n| `number` | `(value, options?)` | `string` |\n| `currency` | `(value, currency, options?)` | `string` |\n| `date` | `(value, options?)` | `string` |\n| `relative` | `(value, unit, options?)` | `string` |\n| `list` | `(value, options?)` | `string` |\n| `duration` | `(value, options?)` | `string` |\n\n### validateCatalog\n\n```ts\nfunction validateCatalog(catalog: Catalog, locale: Locale): ValidationIssue[];\n```\n\nValidates explicit plural messages against locale plural categories after catalog structural validation.\n\n| Parameter | Type | Description |\n| --- | --- | --- |\n| `catalog` | `Catalog` | Explicit catalog to validate |\n| `locale` | `Locale` | BCP 47 locale tag |\n\n**Returns:** `ValidationIssue[]`.\n\n**Example:**\n\n```ts\nimport { validateCatalog } from '@vielzeug/lingua/validate';\n\nvalidateCatalog({ inbox: { plural: { one: 'One message' } } }, 'en');\n```\n\n## Types\n\n```ts\ntype Locale = string;\ntype PluralCategory = Intl.LDMLPluralRule;\ntype PluralMessage = { readonly plural: Partial<Record<PluralCategory, string>> };\ntype CatalogNode = Catalog | PluralMessage | string;\ntype Catalog = { readonly [key: string]: CatalogNode };\ntype Catalogs<C extends Catalog = Catalog> = Record<Locale, C>;\ntype CatalogTranslatorOptions = Omit<TranslatorOptions, 'fallback'>;\ntype CatalogLoader<C extends Catalog = Catalog> = () => Promise<C>;\ntype CatalogSource<C extends Catalog = Catalog> = C | CatalogLoader<C>;\ntype CatalogSources<C extends Catalog = Catalog> = Record<Locale, CatalogSource<C>>;\n\ntype TranslationStoreOptions<C extends Catalog = Catalog> = TranslatorOptions & {\n catalogs: CatalogSources<C>;\n};\n\ntype TranslationState<C extends Catalog = Catalog> = {\n readonly catalogs: Catalogs<C>;\n readonly locale: Locale;\n readonly version: 3;\n};\n\ntype TranslationSnapshot<C extends Catalog = Catalog> = {\n readonly locale: Locale;\n readonly revision: number;\n readonly translator: Translator<C>;\n};\n\ntype TranslationStore<C extends Catalog = Catalog> = Translator<C> & {\n readonly disposalSignal: AbortSignal;\n dispose(): void;\n readonly disposed: boolean;\n getSnapshot(): TranslationSnapshot<C>;\n isLoaded(options?: { locale?: Locale }): boolean;\n load(options?: { locale?: Locale }): Promise<void>;\n serialize(): TranslationState<C>;\n setLocale(locale: Locale): Promise<void>;\n subscribe(listener: (snapshot: TranslationSnapshot<C>) => void, options?: SubscribeOptions): () => void;\n [Symbol.dispose](): void;\n};\n\ntype Translator<C extends Catalog = Catalog> = {\n readonly locale: Locale;\n segments<V>(key: TextKey<C>, options: TranslateOptions & { values: Record<string, V> }): Array<string | V>;\n segments<V>(key: PluralKey<C>, options: PluralOptions & { values?: Record<string, V> }): Array<string | number | V>;\n segmentsDynamic<V>(\n key: string,\n options: (TranslateOptions | PluralOptions) & { values?: Record<string, V> },\n ): Array<string | number | V>;\n translate(key: TextKey<C>, options?: TranslateOptions): string;\n translate(key: PluralKey<C>, options: PluralOptions): string;\n translateDynamic(key: string, options?: TranslateOptions | PluralOptions): string;\n};\n```\n\n```ts\ntype Values = Record<string, unknown>;\ntype TranslateOptions = { values?: Values };\ntype PluralOptions = TranslateOptions & { count: number; ordinal?: boolean };\ntype TranslatorOptions = {\n fallback?: Locale | readonly Locale[];\n locale?: Locale;\n onMissingKey?: (key: string, locale: Locale) => string;\n onMissingValue?: (name: string, key: string, locale: Locale) => string;\n};\ntype SubscribeOptions = { immediate?: boolean; signal?: AbortSignal };\n\ntype MessageKey<\n C,\n Prefix extends string = '',\n Depth extends readonly unknown[] = readonly [1, 1, 1, 1, 1, 1],\n> = Depth extends readonly [unknown, ...infer Rest]\n ? C extends string | PluralMessage\n ? Prefix\n : C extends Catalog\n ? {\n [K in string & keyof C]: MessageKey<C[K], Prefix extends '' ? K : `${Prefix}.${K}`, Rest>;\n }[string & keyof C]\n : never\n : never;\n\ntype TextKey<\n C,\n Prefix extends string = '',\n Depth extends readonly unknown[] = readonly [1, 1, 1, 1, 1, 1],\n> = Depth extends readonly [unknown, ...infer Rest]\n ? C extends string\n ? Prefix\n : C extends Catalog\n ? {\n [K in string & keyof C]: TextKey<C[K], Prefix extends '' ? K : `${Prefix}.${K}`, Rest>;\n }[string & keyof C]\n : never\n : never;\n\ntype PluralKey<\n C,\n Prefix extends string = '',\n Depth extends readonly unknown[] = readonly [1, 1, 1, 1, 1, 1],\n> = Depth extends readonly [unknown, ...infer Rest]\n ? C extends PluralMessage\n ? Prefix\n : C extends Catalog\n ? {\n [K in string & keyof C]: PluralKey<C[K], Prefix extends '' ? K : `${Prefix}.${K}`, Rest>;\n }[string & keyof C]\n : never\n : never;\n\ntype DurationValue = Partial<Record<\n 'days' | 'hours' | 'microseconds' | 'milliseconds' | 'minutes' | 'months' | 'nanoseconds' | 'seconds' | 'weeks' | 'years',\n number\n>>;\n\ntype DurationFormatOptions = {\n hours?: '2-digit' | 'numeric';\n microseconds?: 'numeric';\n milliseconds?: 'numeric';\n minutes?: '2-digit' | 'numeric';\n nanoseconds?: 'numeric';\n seconds?: '2-digit' | 'numeric';\n style?: 'digital' | 'long' | 'narrow' | 'short';\n};\n\ntype ListFormatOptions = { style?: 'long' | 'narrow' | 'short'; type?: 'and' | 'or' };\n\ntype Formatter = {\n currency(value: number, currency: string, options?: Omit<Intl.NumberFormatOptions, 'currency' | 'style'>): string;\n date(value: Date | number, options?: Intl.DateTimeFormatOptions): string;\n duration(value: DurationValue, options?: DurationFormatOptions): string;\n list(value: Array<string | number>, options?: ListFormatOptions): string;\n number(value: number, options?: Intl.NumberFormatOptions): string;\n relative(value: number, unit: Intl.RelativeTimeFormatUnit, options?: Intl.RelativeTimeFormatOptions): string;\n};\n\ntype ValidationIssue = { key: string; locale: Locale; missing: Intl.LDMLPluralRule };\n```\n\n## Errors\n\n| Error | Trigger |\n| --- | --- |\n| `LinguaDisposedError` | State mutation or subscription after `dispose()` |\n| `LinguaInvalidCatalogError` | Invalid catalog node or reserved key |\n| `LinguaInvalidLocaleError` | Invalid BCP 47 locale tag |\n| `LinguaInvalidPluralCountError` | Non-finite plural count |\n| `LinguaInvalidStateError` | Unsupported serialized state version |\n| `LinguaMissingCatalogError` | Catalog has no source for requested locale |\n",
|
|
6
|
-
"usage": "---\ntitle: Lingua — Usage Guide\ndescription: Translate explicit catalogs, load lazy locales, and connect locale snapshots to UI state.\n---\n\n[[toc]]\n\n## Basic Usage\n\nCreate i18n store from locale-keyed catalogs. Strings are text messages; plural messages use `{ plural: ... }`.\n\n```ts\nimport { createTranslationStore } from '@vielzeug/lingua';\n\nconst i18n = createTranslationStore({\n catalogs: {\n en: {\n greeting: 'Hello, {name}!',\n inbox: { plural: { one: 'One message', other: '{count} messages' } },\n },\n },\n locale: 'en',\n});\n\nconsole.log(i18n.translate('greeting', { values: { name: 'Ada' } }));\nconsole.log(i18n.translate('inbox', { count: 3 }));\n```\n\nCall `dispose()` when store belongs to temporary request, test, or route owner.\n\n## Define Explicit Catalogs\n\nUse nested objects only to group keys. A plural message always has `plural`, so regular objects containing `one` or `other` remain groups.\n\n```ts\nconst catalog = {\n account: {\n greeting: 'Hello, {name}!',\n unread: { plural: { one: 'One unread message', other: '{count} unread messages' } },\n },\n};\n```\n\nUse `{ values }` for text replacements. Pass `count` at top level for plural selection; Lingua injects it into selected template. Absent replacements render as `{name}` by default. `segments()` preserves an own `undefined` or `null` value; omit property to receive `{name}`.\n\nCatalogs contain strings, grouping objects, and explicit `{ plural: ... }` messages only. Keep application data outside catalog, then translate display labels while constructing it.\n\n```ts\nimport { createCatalogTranslator } from '@vielzeug/lingua';\n\nconst messages = {\n status: { blocked: 'Blocked', done: 'Done', inProgress: 'In progress' },\n};\nconst statusDefinitions = [\n { labelKey: 'status.inProgress', value: 'in-progress' },\n { labelKey: 'status.blocked', value: 'blocked' },\n { labelKey: 'status.done', value: 'done' },\n] as const;\nconst translator = createCatalogTranslator(messages);\nconst statusOptions = statusDefinitions.map(({ labelKey, value }) => ({ label: translator.translate(labelKey), value }));\n```\n\n## Render Framework Content\n\nUse `segments()` when replacements are framework nodes, links, or other values that must not be stringified.\n\n```ts\nimport { createCatalogTranslator } from '@vielzeug/lingua';\n\nconst translator = createCatalogTranslator({ error: 'Try {retry} or {support}.' });\n\nconst retry = { href: '/retry', label: 'retry' };\nconst support = { href: '/support', label: 'support' };\n\nconsole.log(translator.segments('error', { values: { retry, support } }));\n```\n\nRender returned array with framework fragment or list primitive. Give UI values consumer-owned keys before passing them to `segments()`; Lingua preserves value identity and never clones or mutates them.\n\n## Use Static Catalogs\n\nUse `createCatalogTranslator()` when one catalog and locale stay fixed for translator lifetime. It defaults locale to `en`; pass `locale` when plural rules or diagnostics need another locale. Lingua snapshots catalog messages during construction. Do not mutate source catalog objects afterward.\n\n```ts\nimport { createCatalogTranslator } from '@vielzeug/lingua';\n\nconst translator = createCatalogTranslator(\n { save: 'Enregistrer' },\n { locale: 'fr' },\n);\n\nconsole.log(translator.translate('save'));\n```\n\nUse `createTranslator()` when fixed translation requires locale-keyed catalogs and fallback resolution.\n\n```ts\nimport { createTranslator } from '@vielzeug/lingua';\n\nconst translator = createTranslator(\n { en: { save: 'Save' }, fr: { save: 'Enregistrer' } },\n { locale: 'fr' },\n);\n\nconsole.log(translator.translate('save'));\n```\n\n## Load Catalogs and Switch Locales\n\nDeclare one static catalog or lazy loader per locale. Switch locale, then load it explicitly when source is lazy.\n\n```ts\nimport { createTranslationStore } from '@vielzeug/lingua';\n\nconst i18n = createTranslationStore({\n catalogs: {\n en: { navigation: { settings: 'Settings' } },\n fr: async () => ({ navigation: { settings: 'Réglages' } }),\n },\n locale: 'en',\n});\n\nawait i18n.setLocale('fr');\nawait i18n.load();\nconsole.log(i18n.translate('navigation.settings'));\n```\n\nConcurrent loads for same locale share work. `setLocale()` never triggers hidden loads.\n\n## Subscribe to Immutable Snapshots\n\nSubscribe when UI state must change with locale or loaded active/fallback catalog. Every callback receives snapshot containing translator for that revision.\n\n```ts\nconst unsubscribe = i18n.subscribe(\n ({ locale, translator }) => {\n console.log(locale, translator.translate('navigation.settings'));\n },\n { immediate: true },\n);\n\nunsubscribe();\n```\n\nPass `{ signal }` when an `AbortController` owns subscription lifetime.\n\n## SSR State\n\nSerialize resolved catalogs on server, then hydrate client store from same payload. `getSnapshot()` stays referentially stable until store revision changes, so use same hydrated store throughout initial client render.\n\n```ts\nimport { createTranslationStore, hydrateTranslationStore } from '@vielzeug/lingua';\n\nconst serverTranslationStore = createTranslationStore({\n catalogs: { en: { title: 'Server title' } },\n locale: 'en',\n});\n\nconst state = serverTranslationStore.serialize();\nconst clientTranslationStore = hydrateTranslationStore(state, { fallback: 'en' });\n\nconsole.log(clientTranslationStore.translate('title'));\nserverTranslationStore.dispose();\nclientTranslationStore.dispose();\n```\n\nState contains raw loaded catalogs. It never contains loader functions.\n\n## Formatting and Validation\n\nImport formatting and catalog validation from dedicated subpaths to keep translation state focused.\n\n```ts\nimport { createFormatter } from '@vielzeug/lingua/format';\nimport { validateCatalog } from '@vielzeug/lingua/validate';\n\nconst formatter = createFormatter('en-US');\nconst catalog = { inbox: { plural: { one: 'One message', other: '{count} messages' } } };\n\nconsole.log(formatter.currency(19.99, 'USD'));\nconsole.log(validateCatalog(catalog, 'en'));\n```\n\n## Framework Integration\n\nPass stable `getSnapshot()` and `subscribe()` methods to framework state primitives. For SSR, create client store from same serialized state used by server before calling `useSyncExternalStore`.\n\n::: code-group\n\n```ts [React]\nimport { useSyncExternalStore } from 'react';\n\nimport type { TranslationStore } from '@vielzeug/lingua';\n\nexport function useTranslator(i18n: TranslationStore) {\n const snapshot = useSyncExternalStore(i18n.subscribe, i18n.getSnapshot, i18n.getSnapshot);\n\n return snapshot.translator;\n}\n```\n\n```ts [Vue 3]\nimport { onUnmounted, shallowRef } from 'vue';\n\nimport type { TranslationStore } from '@vielzeug/lingua';\n\nexport function useTranslator(i18n: TranslationStore) {\n const snapshot = shallowRef(i18n.getSnapshot());\n const unsubscribe = i18n.subscribe((next) => {\n snapshot.value = next;\n });\n\n onUnmounted(unsubscribe);\n return snapshot;\n}\n```\n\n```ts [Svelte]\nimport { readable } from 'svelte/store';\n\nimport type { TranslationStore } from '@vielzeug/lingua';\n\nexport function translatorStore(i18n: TranslationStore) {\n return readable(i18n.getSnapshot().translator, (set) => i18n.subscribe(({ translator }) => set(translator)));\n}\n```\n\n:::\n\n## Working with Other Vielzeug Libraries\n\nBridge Lingua subscriptions into Ripple through Flux when templates need reactive locale reads.\n\n```ts\nimport { stream } from '@vielzeug/flux';\nimport { toSignal } from '@vielzeug/flux/ripple';\nimport { computed } from '@vielzeug/ripple';\n\nconst localeBinding = toSignal(\n stream<string>((observer) => {\n observer.next(i18n.locale);\n return i18n.subscribe(({ locale }) => observer.next(locale));\n }),\n { initial: i18n.locale },\n);\n\nexport const locale = computed(() => localeBinding.value);\n```\n\nUse Courier loaders when locale catalogs come from HTTP rather than bundled modules; pass each loader to `catalogs`.\n\n## Best Practices\n\n- Define plural messages with `{ plural: ... }` and no sibling metadata.\n- Keep arrays and application metadata outside catalogs.\n- Treat source catalog objects as immutable after construction.\n- Use `translateDynamic()` only for runtime-generated keys.\n- Load a lazy catalog before rendering it.\n- Give UI values keys before passing them to `segments()`.\n- Keep loader functions out of SSR payloads.\n- Dispose temporary stores after requests, tests, and route lifetimes.\n",
|
|
5
|
+
"api": "---\ntitle: Lingua — API Reference\ndescription: Complete API reference for @vielzeug/lingua.\n---\n\n[[toc]]\n\n## API Overview\n\n| Symbol | Purpose | Execution mode | Common gotcha |\n| --- | --- | --- | --- |\n| `createCatalogTranslator()` | Compile one immutable locale catalog | Sync | No fallback locales |\n| `createTranslator()` | Compile immutable locale catalogs | Sync | Locale is fixed for translator lifetime |\n| `createTranslationStore()` | Create mutable locale and catalog store | Sync | Load lazy locale explicitly |\n| `hydrateTranslationStore()` | Create store from serialized loaded catalogs | Sync | Serialized state never includes loaders |\n| `catalogKeys()` | Enumerate message keys as dotted paths | Sync | Accepts store (current locale) or raw catalog; traverse subtrees for group-scoped keys |\n| `createFormatter()` | Format Intl values from `/format` | Sync | Import from subpath |\n| `validateCatalog()` | Check explicit plural forms from `/validate` | Sync | Import from subpath |\n| `compareCatalogs()` | Compare key parity across locales from `/validate` | Sync | First locale is the base; import from subpath |\n| `LinguaError` | Base class for Lingua errors | Sync | Use `LinguaError.is()` for broad narrowing |\n\n## Package Entry Point\n\n| Import | Purpose |\n| --- | --- |\n| `@vielzeug/lingua` | Translation factories, state types, and Lingua errors |\n| `@vielzeug/lingua/format` | `createFormatter()` and formatter types |\n| `@vielzeug/lingua/validate` | `validateCatalog()`, `compareCatalogs()`, and `ValidationIssue` |\n\n## Translation Factories\n\n### createCatalogTranslator\n\n```ts\nfunction createCatalogTranslator<C extends Catalog>(\n catalog: C,\n options?: CatalogTranslatorOptions,\n): Translator<C>;\n```\n\nCompiles one catalog and returns an immutable fixed-locale translator. Locale defaults to `en` and controls plural selection and diagnostics.\n\n| Parameter | Type | Description |\n| --- | --- | --- |\n| `catalog` | `C` | One catalog containing only messages and grouping objects |\n| `options` | `CatalogTranslatorOptions` | Locale and missing-message handlers; fallback is unavailable |\n\n**Returns:** `Translator<C>`.\n\n**Example:**\n\n```ts\nimport { createCatalogTranslator } from '@vielzeug/lingua';\n\nconst translator = createCatalogTranslator(\n { save: 'Enregistrer' },\n { locale: 'fr' },\n);\n\ntranslator.translate('save');\n```\n\n---\n\n### createTranslator\n\n```ts\nfunction createTranslator<C extends Catalog>(catalogs: Catalogs<C>, options?: TranslatorOptions): Translator<C>;\n```\n\nCompiles locale catalogs and returns immutable translator.\n\n| Parameter | Type | Description |\n| --- | --- | --- |\n| `catalogs` | `Catalogs<C>` | Locale-keyed catalog objects |\n| `options` | `TranslatorOptions` | Locale, fallback chain, and missing-message handlers |\n\n**Returns:** `Translator<C>`.\n\n**Example:**\n\n```ts\nimport { createTranslator } from '@vielzeug/lingua';\n\nconst translator = createTranslator(\n { en: { save: 'Save' }, fr: { save: 'Enregistrer' } },\n { locale: 'fr' },\n);\n\ntranslator.translate('save');\n```\n\n| Method | Signature | Returns |\n| --- | --- | --- |\n| `translate` | `(textKey, options?)` or `(pluralKey, { count, ordinal?, values? })` | Rendered string |\n| `translateDynamic` | `(key, options?)` | Rendered string for runtime key |\n| `segments` | `(textKey, { values })` or `(pluralKey, { count, ordinal?, values? })` | String and typed-value segments |\n| `segmentsDynamic` | `(key, options)` | Segments for runtime key |\n| `locale` | `Locale` | Resolved active locale |\n\n---\n\n### createTranslationStore\n\n```ts\nfunction createTranslationStore<C extends Catalog>(options: TranslationStoreOptions<C>): TranslationStore<C>;\n```\n\nCreates catalog store, current locale state, and immutable translator snapshots.\n\n| Parameter | Type | Description |\n| --- | --- | --- |\n| `options.catalogs` | `CatalogSources<C>` | Static catalogs or lazy locale loaders |\n| `options.locale` | `Locale` | Initial locale; defaults to `en` |\n| `options.fallback` | `Locale \\| readonly Locale[]` | Fallback locale chain |\n| `options.onMissingKey` | `(key, locale) => string` | Missing-message handler |\n| `options.onMissingValue` | `(name, key, locale) => string` | Missing-interpolation handler |\n\n**Returns:** `TranslationStore<C>`, with every `Translator<C>` method plus lifecycle methods.\n\n**Example:**\n\n```ts\nimport { createTranslationStore } from '@vielzeug/lingua';\n\nconst translations = createTranslationStore({\n catalogs: { en: { title: 'Home' }, fr: { title: 'Accueil' } },\n locale: 'en',\n});\n\nawait translations.setLocale('fr');\ntranslations.translate('title');\n```\n\n| Method or property | Signature | Returns |\n| --- | --- | --- |\n| `translate` | Translator method | Rendered string |\n| `segments` | Translator method | String and typed-value segments |\n| `load` | `({ locale? })` | `Promise<void>` after catalog resolution |\n| `setLocale` | `(locale)` | `Promise<void>` after locale commit; never loads implicitly |\n| `isLoaded` | `({ locale? })` | `boolean` |\n| `getSnapshot` | `()` | `TranslationSnapshot<C>` |\n| `subscribe` | `(listener, { immediate?, signal? })` | Unsubscribe function |\n| `serialize` | `()` | Loader-free `TranslationState<C>` |\n| `dispose` | `()` | `void` |\n| `locale` | `Locale` | Current canonical locale |\n| `disposed` | `boolean` | Disposal state |\n| `disposalSignal` | `AbortSignal` | Aborts on disposal |\n| `[Symbol.dispose]` | `()` | Delegates to `dispose()` |\n\n---\n\n### hydrateTranslationStore\n\n```ts\nfunction hydrateTranslationStore<C extends Catalog>(\n state: TranslationState<C>,\n options?: Omit<TranslationStoreOptions<C>, 'locale' | 'catalogs'>,\n): TranslationStore<C>;\n```\n\nCreates translation store from SSR state payload containing resolved raw catalogs.\n\n| Parameter | Type | Description |\n| --- | --- | --- |\n| `state` | `TranslationState<C>` | Version `3`, active locale, and loader-free catalogs |\n| `options` | `Omit<TranslationStoreOptions<C>, 'locale' \\| 'catalogs'>` | Fallback and missing-message handlers |\n\n**Returns:** `TranslationStore<C>`.\n\n**Example:**\n\n```ts\nimport { createTranslationStore, hydrateTranslationStore } from '@vielzeug/lingua';\n\nconst server = createTranslationStore({ catalogs: { en: { title: 'Home' } }, locale: 'en' });\nconst client = hydrateTranslationStore(server.serialize());\n\nclient.translate('title');\n```\n\n---\n\n## Catalog Utilities\n\n### catalogKeys\n\n```ts\nfunction catalogKeys<C extends Catalog>(source: TranslationStore<C> | C): ReadonlyArray<TextKey<C>>;\n```\n\nEnumerates every message key as a dotted path. Traverses nested grouping objects and explicit `{ plural: ... }` messages, producing the same paths that `TextKey<C>` represents at the type level. Pass a `TranslationStore` to read from its current locale catalog; pass a raw catalog object to enumerate directly.\n\n| Parameter | Type | Description |\n| --- | --- | --- |\n| `source` | `TranslationStore<C> \\| C` | Store (uses current locale) or raw catalog object |\n\n**Returns:** `ReadonlyArray<TextKey<C>>` — dotted paths to every text and plural message.\n\n```ts\nimport { catalogKeys, createTranslationStore } from '@vielzeug/lingua';\n\nconst i18n = createTranslationStore({\n catalogs: { en: { nav: { home: 'Home', settings: 'Settings' } } },\n locale: 'en',\n});\n\nconst allKeys = catalogKeys(i18n); // ['nav.home', 'nav.settings']\nconst navKeys = catalogKeys(i18n.serialize().catalogs.en.nav); // ['home', 'settings']\n```\n\n---\n\n## Formatting and Validation\n\n### createFormatter\n\n```ts\nfunction createFormatter(source: string | (() => string)): Formatter;\n```\n\nCreates cached Intl formatters using static locale or locale getter.\n\n| Parameter | Type | Description |\n| --- | --- | --- |\n| `source` | `string \\| (() => string)` | Static locale or locale getter |\n\n**Returns:** `Formatter`.\n\n**Example:**\n\n```ts\nimport { createFormatter } from '@vielzeug/lingua/format';\n\nconst formatter = createFormatter('en-US');\nformatter.currency(19.99, 'USD');\n```\n\n| Method | Signature | Returns |\n| --- | --- | --- |\n| `number` | `(value, options?)` | `string` |\n| `currency` | `(value, currency, options?)` | `string` |\n| `date` | `(value, options?)` | `string` |\n| `relative` | `(value, unit, options?)` | `string` |\n| `list` | `(value, options?)` | `string` |\n| `duration` | `(value, options?)` | `string` |\n\n### validateCatalog\n\n```ts\nfunction validateCatalog(catalog: Catalog, locale: Locale): ValidationIssue[];\n```\n\nValidates explicit plural messages against locale plural categories after catalog structural validation.\n\n| Parameter | Type | Description |\n| --- | --- | --- |\n| `catalog` | `Catalog` | Explicit catalog to validate |\n| `locale` | `Locale` | BCP 47 locale tag |\n\n**Returns:** `ValidationIssue[]`.\n\n**Example:**\n\n```ts\nimport { validateCatalog } from '@vielzeug/lingua/validate';\n\nvalidateCatalog({ inbox: { plural: { one: 'One message' } } }, 'en');\n```\n\n### compareCatalogs\n\n```ts\nfunction compareCatalogs<C extends Catalog>(catalogs: Catalogs<C>): CatalogComparison;\n```\n\nCompares key sets across locales. First locale is the base — reports keys missing in each target and keys present in targets but absent from base. Validates each catalog structurally.\n\n| Parameter | Type | Description |\n| --- | --- | --- |\n| `catalogs` | `Catalogs<C>` | Locale-keyed catalogs to compare |\n\n**Returns:** `CatalogComparison` with `missing` and `extra` arrays.\n\n```ts\nimport { compareCatalogs } from '@vielzeug/lingua/validate';\n\nconst result = compareCatalogs({\n en: { greeting: 'Hello', farewell: 'Goodbye' },\n de: { greeting: 'Hallo' },\n});\n// { missing: [{ key: 'farewell', locale: 'de' }], extra: [] }\n```\n\n## Types\n\n```ts\ntype Locale = string;\ntype PluralCategory = Intl.LDMLPluralRule;\ntype PluralMessage = { readonly plural: Partial<Record<PluralCategory, string>> };\ntype CatalogNode = Catalog | PluralMessage | string;\ntype Catalog = { readonly [key: string]: CatalogNode };\ntype Catalogs<C extends Catalog = Catalog> = Record<Locale, C>;\ntype CatalogTranslatorOptions = Omit<TranslatorOptions, 'fallback'>;\ntype CatalogLoader<C extends Catalog = Catalog> = () => Promise<C>;\ntype CatalogSource<C extends Catalog = Catalog> = C | CatalogLoader<C>;\ntype CatalogSources<C extends Catalog = Catalog> = Record<Locale, CatalogSource<C>>;\n\ntype TranslationStoreOptions<C extends Catalog = Catalog> = TranslatorOptions & {\n catalogs: CatalogSources<C>;\n};\n\ntype TranslationState<C extends Catalog = Catalog> = {\n readonly catalogs: Catalogs<C>;\n readonly locale: Locale;\n readonly version: 3;\n};\n\ntype TranslationSnapshot<C extends Catalog = Catalog> = {\n readonly locale: Locale;\n readonly revision: number;\n readonly translator: Translator<C>;\n};\n\ntype TranslationStore<C extends Catalog = Catalog> = Translator<C> & {\n readonly disposalSignal: AbortSignal;\n dispose(): void;\n readonly disposed: boolean;\n getSnapshot(): TranslationSnapshot<C>;\n isLoaded(options?: { locale?: Locale }): boolean;\n load(options?: { locale?: Locale }): Promise<void>;\n serialize(): TranslationState<C>;\n setLocale(locale: Locale): Promise<void>;\n subscribe(listener: (snapshot: TranslationSnapshot<C>) => void, options?: SubscribeOptions): () => void;\n [Symbol.dispose](): void;\n};\n\ntype Translator<C extends Catalog = Catalog> = {\n readonly locale: Locale;\n segments<V>(key: TextKey<C>, options: TranslateOptions & { values: Record<string, V> }): Array<string | V>;\n segments<V>(key: PluralKey<C>, options: PluralOptions & { values?: Record<string, V> }): Array<string | number | V>;\n segmentsDynamic<V>(\n key: string,\n options: (TranslateOptions | PluralOptions) & { values?: Record<string, V> },\n ): Array<string | number | V>;\n translate(key: TextKey<C>, options?: TranslateOptions): string;\n translate(key: PluralKey<C>, options: PluralOptions): string;\n translateDynamic(key: string, options?: TranslateOptions | PluralOptions): string;\n};\n```\n\n```ts\ntype Values = Record<string, unknown>;\ntype TranslateOptions = { values?: Values };\ntype PluralOptions = TranslateOptions & { count: number; ordinal?: boolean };\ntype TranslatorOptions = {\n fallback?: Locale | readonly Locale[];\n locale?: Locale;\n onMissingKey?: (key: string, locale: Locale) => string;\n onMissingValue?: (name: string, key: string, locale: Locale) => string;\n};\ntype SubscribeOptions = { immediate?: boolean; signal?: AbortSignal };\n\ntype MessageKey<\n C,\n Prefix extends string = '',\n Depth extends readonly unknown[] = readonly [1, 1, 1, 1, 1, 1],\n> = Depth extends readonly [unknown, ...infer Rest]\n ? C extends string | PluralMessage\n ? Prefix\n : C extends Catalog\n ? {\n [K in string & keyof C]: MessageKey<C[K], Prefix extends '' ? K : `${Prefix}.${K}`, Rest>;\n }[string & keyof C]\n : never\n : never;\n\ntype TextKey<\n C,\n Prefix extends string = '',\n Depth extends readonly unknown[] = readonly [1, 1, 1, 1, 1, 1],\n> = Depth extends readonly [unknown, ...infer Rest]\n ? C extends string\n ? Prefix\n : C extends Catalog\n ? {\n [K in string & keyof C]: TextKey<C[K], Prefix extends '' ? K : `${Prefix}.${K}`, Rest>;\n }[string & keyof C]\n : never\n : never;\n\ntype PluralKey<\n C,\n Prefix extends string = '',\n Depth extends readonly unknown[] = readonly [1, 1, 1, 1, 1, 1],\n> = Depth extends readonly [unknown, ...infer Rest]\n ? C extends PluralMessage\n ? Prefix\n : C extends Catalog\n ? {\n [K in string & keyof C]: PluralKey<C[K], Prefix extends '' ? K : `${Prefix}.${K}`, Rest>;\n }[string & keyof C]\n : never\n : never;\n\ntype DurationValue = Partial<Record<\n 'days' | 'hours' | 'microseconds' | 'milliseconds' | 'minutes' | 'months' | 'nanoseconds' | 'seconds' | 'weeks' | 'years',\n number\n>>;\n\ntype DurationFormatOptions = {\n hours?: '2-digit' | 'numeric';\n microseconds?: 'numeric';\n milliseconds?: 'numeric';\n minutes?: '2-digit' | 'numeric';\n nanoseconds?: 'numeric';\n seconds?: '2-digit' | 'numeric';\n style?: 'digital' | 'long' | 'narrow' | 'short';\n};\n\ntype ListFormatOptions = { style?: 'long' | 'narrow' | 'short'; type?: 'and' | 'or' };\n\ntype Formatter = {\n currency(value: number, currency: string, options?: Omit<Intl.NumberFormatOptions, 'currency' | 'style'>): string;\n date(value: Date | number, options?: Intl.DateTimeFormatOptions): string;\n duration(value: DurationValue, options?: DurationFormatOptions): string;\n list(value: Array<string | number>, options?: ListFormatOptions): string;\n number(value: number, options?: Intl.NumberFormatOptions): string;\n relative(value: number, unit: Intl.RelativeTimeFormatUnit, options?: Intl.RelativeTimeFormatOptions): string;\n};\n\ntype ValidationIssue = { key: string; locale: Locale; missing: Intl.LDMLPluralRule };\ntype CatalogComparison = {\n readonly missing: ReadonlyArray<{ key: string; locale: Locale }>;\n readonly extra: ReadonlyArray<{ key: string; locale: Locale }>;\n};\n```\n\n## Errors\n\n| Error | Trigger |\n| --- | --- |\n| `LinguaDisposedError` | State mutation or subscription after `dispose()` |\n| `LinguaInvalidCatalogError` | Invalid catalog node or reserved key |\n| `LinguaInvalidLocaleError` | Invalid BCP 47 locale tag |\n| `LinguaInvalidPluralCountError` | Non-finite plural count |\n| `LinguaInvalidStateError` | Unsupported serialized state version |\n| `LinguaMissingCatalogError` | Catalog has no source for requested locale |\n",
|
|
6
|
+
"usage": "---\ntitle: Lingua — Usage Guide\ndescription: Translate explicit catalogs, load lazy locales, and connect locale snapshots to UI state.\n---\n\n[[toc]]\n\n## Basic Usage\n\nCreate i18n store from locale-keyed catalogs. Strings are text messages; plural messages use `{ plural: ... }`.\n\n```ts\nimport { createTranslationStore } from '@vielzeug/lingua';\n\nconst i18n = createTranslationStore({\n catalogs: {\n en: {\n greeting: 'Hello, {name}!',\n inbox: { plural: { one: 'One message', other: '{count} messages' } },\n },\n },\n locale: 'en',\n});\n\nconsole.log(i18n.translate('greeting', { values: { name: 'Ada' } }));\nconsole.log(i18n.translate('inbox', { count: 3 }));\n```\n\nCall `dispose()` when store belongs to temporary request, test, or route owner.\n\n## Define Explicit Catalogs\n\nUse nested objects only to group keys. A plural message always has `plural`, so regular objects containing `one` or `other` remain groups.\n\n```ts\nconst catalog = {\n account: {\n greeting: 'Hello, {name}!',\n unread: { plural: { one: 'One unread message', other: '{count} unread messages' } },\n },\n};\n```\n\nUse `{ values }` for text replacements. Pass `count` at top level for plural selection; Lingua injects it into selected template. Absent replacements render as `{name}` by default. `segments()` preserves an own `undefined` or `null` value; omit property to receive `{name}`.\n\nCatalogs contain strings, grouping objects, and explicit `{ plural: ... }` messages only. Keep application data outside catalog, then translate display labels while constructing it.\n\n```ts\nimport { createCatalogTranslator } from '@vielzeug/lingua';\n\nconst messages = {\n status: { blocked: 'Blocked', done: 'Done', inProgress: 'In progress' },\n};\nconst statusDefinitions = [\n { labelKey: 'status.inProgress', value: 'in-progress' },\n { labelKey: 'status.blocked', value: 'blocked' },\n { labelKey: 'status.done', value: 'done' },\n] as const;\nconst translator = createCatalogTranslator(messages);\nconst statusOptions = statusDefinitions.map(({ labelKey, value }) => ({ label: translator.translate(labelKey), value }));\n```\n\n## Enumerate Catalog Keys\n\nUse `catalogKeys()` to derive key arrays from the catalog itself instead of maintaining a parallel list that can go stale. It traverses nested grouping objects and explicit `{ plural: ... }` messages, returning the same dotted paths that `TextKey<C>` represents at the type level.\n\nPass a `TranslationStore` to enumerate keys from its current locale catalog without specifying a locale explicitly.\n\n```ts\nimport { catalogKeys, createTranslationStore } from '@vielzeug/lingua';\n\nconst i18n = createTranslationStore({\n catalogs: {\n en: {\n greeting: 'Hello, {name}!',\n inbox: { plural: { one: 'One message', other: '{count} messages' } },\n nav: { home: 'Home', settings: 'Settings' },\n },\n },\n locale: 'en',\n});\n\nconst allKeys = catalogKeys(i18n);\n// ['greeting', 'inbox', 'nav.home', 'nav.settings']\n```\n\nPass a raw catalog object to enumerate keys directly. Call `catalogKeys()` on a nested subtree to get exactly the keys in that group — no filtering, no casts.\n\n```ts\nimport { catalogKeys } from '@vielzeug/lingua';\n\nconst messages = {\n nav: { home: 'Home', settings: 'Settings' },\n} as const;\n\nconst allKeys = catalogKeys(messages);\n// ['nav.home', 'nav.settings']\n\nconst navKeys = catalogKeys(messages.nav);\n// ['home', 'settings']\n```\n\nUse this for random message selection, cycling, or validation without a stale parallel array.\n\n## Render Framework Content\n\nUse `segments()` when replacements are framework nodes, links, or other values that must not be stringified.\n\n```ts\nimport { createCatalogTranslator } from '@vielzeug/lingua';\n\nconst translator = createCatalogTranslator({ error: 'Try {retry} or {support}.' });\n\nconst retry = { href: '/retry', label: 'retry' };\nconst support = { href: '/support', label: 'support' };\n\nconsole.log(translator.segments('error', { values: { retry, support } }));\n```\n\nRender returned array with framework fragment or list primitive. Give UI values consumer-owned keys before passing them to `segments()`; Lingua preserves value identity and never clones or mutates them.\n\n## Use Static Catalogs\n\nUse `createCatalogTranslator()` when one catalog and locale stay fixed for translator lifetime. It defaults locale to `en`; pass `locale` when plural rules or diagnostics need another locale. Lingua snapshots catalog messages during construction. Do not mutate source catalog objects afterward.\n\n```ts\nimport { createCatalogTranslator } from '@vielzeug/lingua';\n\nconst translator = createCatalogTranslator(\n { save: 'Enregistrer' },\n { locale: 'fr' },\n);\n\nconsole.log(translator.translate('save'));\n```\n\nUse `createTranslator()` when fixed translation requires locale-keyed catalogs and fallback resolution.\n\n```ts\nimport { createTranslator } from '@vielzeug/lingua';\n\nconst translator = createTranslator(\n { en: { save: 'Save' }, fr: { save: 'Enregistrer' } },\n { locale: 'fr' },\n);\n\nconsole.log(translator.translate('save'));\n```\n\n## Load Catalogs and Switch Locales\n\nDeclare one static catalog or lazy loader per locale. Switch locale, then load it explicitly when source is lazy.\n\n```ts\nimport { createTranslationStore } from '@vielzeug/lingua';\n\nconst i18n = createTranslationStore({\n catalogs: {\n en: { navigation: { settings: 'Settings' } },\n fr: async () => ({ navigation: { settings: 'Réglages' } }),\n },\n locale: 'en',\n});\n\nawait i18n.setLocale('fr');\nawait i18n.load();\nconsole.log(i18n.translate('navigation.settings'));\n```\n\nConcurrent loads for same locale share work. `setLocale()` never triggers hidden loads.\n\n## Subscribe to Immutable Snapshots\n\nSubscribe when UI state must change with locale or loaded active/fallback catalog. Every callback receives snapshot containing translator for that revision.\n\n```ts\nconst unsubscribe = i18n.subscribe(\n ({ locale, translator }) => {\n console.log(locale, translator.translate('navigation.settings'));\n },\n { immediate: true },\n);\n\nunsubscribe();\n```\n\nPass `{ signal }` when an `AbortController` owns subscription lifetime.\n\n## SSR State\n\nSerialize resolved catalogs on server, then hydrate client store from same payload. `getSnapshot()` stays referentially stable until store revision changes, so use same hydrated store throughout initial client render.\n\n```ts\nimport { createTranslationStore, hydrateTranslationStore } from '@vielzeug/lingua';\n\nconst serverTranslationStore = createTranslationStore({\n catalogs: { en: { title: 'Server title' } },\n locale: 'en',\n});\n\nconst state = serverTranslationStore.serialize();\nconst clientTranslationStore = hydrateTranslationStore(state, { fallback: 'en' });\n\nconsole.log(clientTranslationStore.translate('title'));\nserverTranslationStore.dispose();\nclientTranslationStore.dispose();\n```\n\nState contains raw loaded catalogs. It never contains loader functions.\n\n## Formatting and Validation\n\nImport formatting and catalog validation from dedicated subpaths to keep translation state focused.\n\n```ts\nimport { createFormatter } from '@vielzeug/lingua/format';\nimport { compareCatalogs, validateCatalog } from '@vielzeug/lingua/validate';\n\nconst formatter = createFormatter('en-US');\nconst catalog = { inbox: { plural: { one: 'One message', other: '{count} messages' } } };\n\nconsole.log(formatter.currency(19.99, 'USD'));\nconsole.log(validateCatalog(catalog, 'en'));\n```\n\nUse `compareCatalogs()` to catch missing or extra keys across locales — the most common i18n defect. First locale is the base.\n\n```ts\nimport { compareCatalogs } from '@vielzeug/lingua/validate';\n\nconst result = compareCatalogs({\n en: { greeting: 'Hello', farewell: 'Goodbye' },\n de: { greeting: 'Hallo' },\n});\n// { missing: [{ key: 'farewell', locale: 'de' }], extra: [] }\n```\n\n## Framework Integration\n\nPass stable `getSnapshot()` and `subscribe()` methods to framework state primitives. For SSR, create client store from same serialized state used by server before calling `useSyncExternalStore`.\n\n::: code-group\n\n```ts [React]\nimport { useSyncExternalStore } from 'react';\n\nimport type { TranslationStore } from '@vielzeug/lingua';\n\nexport function useTranslator(i18n: TranslationStore) {\n const snapshot = useSyncExternalStore(i18n.subscribe, i18n.getSnapshot, i18n.getSnapshot);\n\n return snapshot.translator;\n}\n```\n\n```ts [Vue 3]\nimport { onUnmounted, shallowRef } from 'vue';\n\nimport type { TranslationStore } from '@vielzeug/lingua';\n\nexport function useTranslator(i18n: TranslationStore) {\n const snapshot = shallowRef(i18n.getSnapshot());\n const unsubscribe = i18n.subscribe((next) => {\n snapshot.value = next;\n });\n\n onUnmounted(unsubscribe);\n return snapshot;\n}\n```\n\n```ts [Svelte]\nimport { readable } from 'svelte/store';\n\nimport type { TranslationStore } from '@vielzeug/lingua';\n\nexport function translatorStore(i18n: TranslationStore) {\n return readable(i18n.getSnapshot().translator, (set) => i18n.subscribe(({ translator }) => set(translator)));\n}\n```\n\n:::\n\n## Working with Other Vielzeug Libraries\n\nBridge Lingua subscriptions into Ripple through Flux when templates need reactive locale reads.\n\n```ts\nimport { stream } from '@vielzeug/flux';\nimport { toSignal } from '@vielzeug/flux/ripple';\nimport { computed } from '@vielzeug/ripple';\n\nconst localeBinding = toSignal(\n stream<string>((observer) => {\n observer.next(i18n.locale);\n return i18n.subscribe(({ locale }) => observer.next(locale));\n }),\n { initial: i18n.locale },\n);\n\nexport const locale = computed(() => localeBinding.value);\n```\n\nUse Courier loaders when locale catalogs come from HTTP rather than bundled modules; pass each loader to `catalogs`.\n\n## Best Practices\n\n- Define plural messages with `{ plural: ... }` and no sibling metadata.\n- Keep arrays and application metadata outside catalogs.\n- Treat source catalog objects as immutable after construction.\n- Use `translateDynamic()` only for runtime-generated keys.\n- Load a lazy catalog before rendering it.\n- Give UI values keys before passing them to `segments()`.\n- Keep loader functions out of SSR payloads.\n- Dispose temporary stores after requests, tests, and route lifetimes.\n",
|
|
7
7
|
"examples": "---\ntitle: Lingua — Examples\ndescription: Focused examples for explicit catalogs and locale resources.\n---\n\n- [Static Translator](./examples/static-translator.md)\n- [Lazy Locale Catalog](./examples/feature-resources.md)\n- [SSR Hydration](./examples/ssr-hydration.md)\n"
|
|
8
8
|
},
|
|
9
9
|
"examples": [
|
|
@@ -29,6 +29,7 @@
|
|
|
29
29
|
}
|
|
30
30
|
],
|
|
31
31
|
"typeSignatures": {
|
|
32
|
+
"catalogKeys": "export { catalogKeys } from './catalog';",
|
|
32
33
|
"LinguaDisposedError": "export {\n LinguaDisposedError,\n LinguaError,\n LinguaInvalidCatalogError,\n LinguaInvalidLocaleError,\n LinguaInvalidPluralCountError,\n LinguaInvalidStateError,\n LinguaMissingCatalogError,\n} from './errors';",
|
|
33
34
|
"LinguaError": "export {\n LinguaDisposedError,\n LinguaError,\n LinguaInvalidCatalogError,\n LinguaInvalidLocaleError,\n LinguaInvalidPluralCountError,\n LinguaInvalidStateError,\n LinguaMissingCatalogError,\n} from './errors';",
|
|
34
35
|
"LinguaInvalidCatalogError": "export {\n LinguaDisposedError,\n LinguaError,\n LinguaInvalidCatalogError,\n LinguaInvalidLocaleError,\n LinguaInvalidPluralCountError,\n LinguaInvalidStateError,\n LinguaMissingCatalogError,\n} from './errors';",
|
package/data/packages/ore.json
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
{
|
|
2
|
-
"apiSource": "export type { ComponentDefinition } from './component-types';\nexport { createContext, type InjectionKey, inject, injectStrict, provide } from './context';\nexport { define, prop } from './define';\n// Near-universal template directives — used in most non-trivial components (lists,\n// conditionals, and class/style maps. Kept in the main entry alongside\n// `html`/`define` rather than a separate sub-path: tree-shaking already means an unused export\n// costs nothing in a bundled consumer, so splitting these off only adds an extra import line\n// for functionality most components need on day one. `unsafeHtml()` and `live()` remain here\n// too: their explicit names make their specialized behavior clear without a second import path.\nexport { classMap } from './directives/classMap';\nexport { each } from './directives/each';\nexport { type LiveBinding, live } from './directives/live';\nexport { styleMap } from './directives/styleMap';\nexport { unsafeHtml } from './directives/unsafe-html';\nexport { when } from './directives/when';\nexport { OreApiError, OreError, type OreErrorPhase, OreInternalError, OreLifecycleError } from './errors';\nexport { type FormFieldHandle, type FormFieldOptions, useField } from './forms/field';\nexport {\n type BindOptions,\n bind,\n type HostBindConfig,\n type HostBindFn,\n type HostBindingValue,\n type ReflectConfig,\n} from './host-bind';\nexport
|
|
2
|
+
"apiSource": "export type { ComponentDefinition } from './component-types';\nexport { createContext, type InjectionKey, inject, injectStrict, provide } from './context';\nexport { define, prop } from './define';\n// Near-universal template directives — used in most non-trivial components (lists,\n// conditionals, and class/style maps. Kept in the main entry alongside\n// `html`/`define` rather than a separate sub-path: tree-shaking already means an unused export\n// costs nothing in a bundled consumer, so splitting these off only adds an extra import line\n// for functionality most components need on day one. `unsafeHtml()` and `live()` remain here\n// too: their explicit names make their specialized behavior clear without a second import path.\nexport { classMap } from './directives/classMap';\nexport { each } from './directives/each';\nexport { type LiveBinding, live } from './directives/live';\nexport { styleMap } from './directives/styleMap';\nexport { unsafeHtml } from './directives/unsafe-html';\nexport { when } from './directives/when';\nexport { OreApiError, OreError, type OreErrorPhase, OreInternalError, OreLifecycleError } from './errors';\nexport { type FormFieldHandle, type FormFieldOptions, useField } from './forms/field';\nexport {\n type BindOptions,\n bind,\n type HostBindConfig,\n type HostBindFn,\n type HostBindingValue,\n type ReflectConfig,\n} from './host-bind';\nexport type { InferProps, PropDef, PropInputDefs, PropsDef } from './props';\n// Lifecycle hooks — plain functions, called during setup() or a composable it invokes.\nexport {\n getHost,\n type OnFormResetCallback,\n type OnMountedCallback,\n onCleanup,\n onElement,\n onEvent,\n onFormReset,\n onMounted,\n watchEffect,\n} from './runtime';\nexport { type ComponentSlots, useSlots } from './slots';\nexport { html } from './template/instantiator';\nexport { type HTMLResult, type Ref, type RefCallback, ref } from './template/result';\nexport { type CSSResult, css } from './utils/css';\nexport { type EmitFn, useEmit } from './utils/emit';\n\nexport { createId, createStableId, resetStableIdCounter } from './utils/id';\n",
|
|
3
3
|
"docs": {
|
|
4
|
-
"index": "---\ntitle: Ore — Web component authoring with signals\ndescription: Functional custom-element authoring with typed props, reactive templates, lifecycle helpers,
|
|
5
|
-
"api": "---\ntitle: Ore — API Reference\ndescription: Complete API reference for @vielzeug/ore and @vielzeug/ore/testing.\n---\n\n[[toc]]\n\n## API Overview\n\nAll browser-runtime symbols below are imported from `@vielzeug/ore`. Lifecycle/context/binding functions (`onMounted`, `onCleanup`, `onEvent`, `onElement`, `watchEffect`, `bind`, `provide`, `useEmit`, `useSlots`, `getHost`) resolve the active component through an implicit \"current component\" context — they work when called synchronously during `setup()`, or from any composable function `setup()` calls (transitively), but throw if called outside that window.\n\n> `watchEffect` is not named `watch` — `@vielzeug/ripple` already exports a `watch(source, callback)` with different semantics (explicit source + old/new value pair), and the two are frequently imported in the same file.\n\n| Symbol | Purpose | Execution mode | Common gotcha |\n| ---------------------- | ----------------------------------------------------- | -------------- | -------------------------------------------------------------------------- |\n| `define()` | Register a custom element with reactive setup | Sync | Tag must contain a hyphen; call before first use |\n| `html` | Tagged template literal returning HTMLResult | Sync | Expressions must be signals, functions, or primitives |\n| `prop.*` | Typed prop helpers (string, bool, number, …) | Sync | Prop values are signals — read `.value` |\n| `provide()`/`inject()` | Context API for parent-to-descendant sharing | Setup only | Must be called synchronously during `setup()` |\n| `ref()` | Reactive reference to a DOM element | Sync | Value is null until after first mount |\n| `createContext()` | Create a typed injection key | Sync | Context is scoped to the component tree |\n| `each()` | Keyed list rendering with DOM diffing | Sync | Duplicate keys report `ore:error`; plain `T[]` is a one-time static render |\n| `when()` | Conditional branch rendering | Sync | Getter-fn computed disposed on cleanup; static bool skips subscription |\n| `live(signal)` | One-way binding that skips stale writes during input | Sync | Use for controlled inputs alongside a manual `@input` handler |\n| `onMounted(fn)` | DOM-ready callback | Setup only | Must be called synchronously during `setup()` |\n| `onCleanup(fn)` | Register teardown | Setup only | Called on component disconnect |\n| `onEvent(target, …)` | Scoped event listener with auto-cleanup | Setup only | No-ops on null target; removed on disconnect |\n| `useField(options)` | Wire signal to form `ElementInternals` | Setup only | Requires `formAssociated: true` on the component definition |\n| `onFormReset(fn)` | Run work when the ancestor `<form>` resets | Setup only | Fires every reset (not one-shot); only for `formAssociated: true` components |\n| `useEmit<Emits>()` | Typed `emit()` bound to the current host | Setup only | Call once per component; returns `dispatchEvent`'s boolean (`false` if a listener called `preventDefault()`) |\n| `useSlots<SlotNames>()`| Reactive slot presence/element signals | Setup only | Safe to call more than once — the underlying registry is created once |\n| `getHost()` | The current component's host element | Setup only | Prefer a higher-level helper (`bind`, …) when one exists |\n\n## Package Entry Points\n\n| Import | Purpose |\n| ------------------------- | ------------------------------------------------------------------ |\n| `@vielzeug/ore` | All browser runtime APIs, including directives, fields, and observers |\n| `@vielzeug/ore/testing` | Ore-specific mounting, lifecycle, hook, cleanup, and form test support |\n| `@vielzeug/assay` | Generic DOM events, scoped queries, and async waiting |\n\n## Core Component API\n\n### `define(tag, definition)`\n\n```ts\ndefine<Props>(tag: string, definition: ComponentDefinition<Props>): void;\n```\n\nThe `setup()` function receives only typed prop signals:\n\n```ts\nsetup(props) {\n return html`<div>${props.label}</div>`;\n}\n```\n\nEverything else — lifecycle hooks, host bindings, context, slots, emit — is a plain function imported from `@vielzeug/ore`, called directly from `setup()` (or a composable it calls):\n\n```ts\nimport { define, html, onMounted, useEmit, useSlots } from '@vielzeug/ore';\n\ndefine('my-card', {\n setup(_props) {\n const emit = useEmit<{ close: undefined }>();\n const slots = useSlots<'header' | 'footer'>();\n\n onMounted(() => console.log('mounted'));\n\n // emit() returns dispatchEvent's boolean — false if a listener called preventDefault()\n const notCancelled = emit('close');\n\n return html`${when(slots.has('header'), () => html`<slot name=\"header\"></slot>`)}`;\n },\n});\n```\n\n`useEmit<Emits>()` and `useSlots<SlotNames>()` are factory hooks — call them once per setup run to get a typed\n`emit`/`slots` bound to the current host. `useSlots()` is safe to call more than once within that setup run.\n\n### ComponentDefinition\n\n```ts\ntype ComponentDefinition<Props> = {\n formAssociated?: boolean;\n props?: PropsDef<Props>;\n setup: (props: InferProps<PropsDef<Props>>) => HTMLResult | null;\n shadow?: Partial<ShadowRootInit> | false; // false = light DOM (no shadow root)\n styles?: (string | CSSStyleSheet | CSSResult)[];\n};\n```\n\n## Runtime Helpers\n\n`onMounted`, `onCleanup`, `onEvent`, `onElement`, and `watchEffect` are plain functions imported from `@vielzeug/ore`. Call them directly during `setup()`.\n\n```ts\nimport { html, onCleanup, onEvent, onMounted } from '@vielzeug/ore';\n\nsetup(props) {\n onMounted(() => {\n // DOM is ready; return a function for mount-scoped cleanup\n return () => { /* cleanup on unmount */ };\n });\n\n onCleanup(() => { /* called on disconnect */ });\n\n onEvent(window, 'keydown', (e) => { /* auto-removed on disconnect */ });\n\n return html`...`;\n}\n```\n\nBecause these resolve the active component through an implicit context (rather than a value threaded through parameters), composable helper functions can call them directly too — no need to pass hooks in as options:\n\n```ts\nimport { onCleanup } from '@vielzeug/ore';\n\nfunction useMyHelper() {\n onCleanup(() => { /* teardown */ });\n}\n\n// In setup:\nsetup(_props) {\n useMyHelper();\n return html`...`;\n}\n```\n\n## Props API\n\n| Helper | Signature | Notes |\n| ----------------------------------- | ------------------ | ------------------------------------------------------------------------------------------------------------ |\n| `prop.string(defaultValue?)` | `PropDef<string>` | Reflects by default |\n| `prop.bool(defaultValue?)` | `PropDef<boolean>` | Any non-null attribute value other than `\"false\"` parses as `true`; `\"false\"` or absent attribute is `false` |\n| `prop.number(defaultValue?)` | `PropDef<number>` | Returns default (not NaN) and warns in dev when attribute is not a valid number |\n| `prop.oneOf(allowed, defaultValue)` | `PropDef<T>` | Restricts to provided string union |\n| `prop.json(defaultValue)` | `PropDef<T>` | JSON.parse from attribute; `reflect: false` |\n| `prop.data<T>(defaultValue?)` | `PropDef<T>` | JS-only — never reads/writes an attribute; use for objects, arrays, callbacks, or any non-serialisable value |\n\n> **Choosing the right prop helper:**\n>\n> - **`prop.json`** — value can be declared in HTML (`<my-el config='{\"x\":1}'>`); attribute string is `JSON.parse`d.\n> - **`prop.data`** — value is always set from JavaScript (objects, arrays, callbacks, class instances); the attribute is never read. Use this for both data and function props.\n\nWhen you need custom parsing or `reflect: false`, use a raw `PropDef` object:\n\n```ts\nprops: {\n items: { default: [], parse: () => [], reflect: false },\n}\n```\n\nUse `prop.data` for props that hold JS-only values (including callbacks) that cannot be serialised through an HTML attribute:\n\n```ts\ndefine('data-grid', {\n props: {\n getRowKey: prop.data<(row: unknown) => string>(),\n columns: prop.data<DataGridColumn[]>([]),\n onSort: prop.data<(key: string) => void>(),\n },\n setup(props) {\n // Set from JS: grid.getRowKey = (row) => row.id\n return html`...`;\n },\n});\n```\n\n## Template and Directives\n\n### `html`\n\nTagged template literal that returns an `HTMLResult`. Supports text interpolation, ordinary attributes (`attr=`),\nboolean attributes (`?attr=`), events (`@event=`), refs (`ref=`), and nested templates.\n\n### `css`\n\nTagged template literal that returns a `CSSResult` for use in `styles`.\n\n### Directives\n\n| Directive | Purpose |\n| -------------------------------------- | ----------------------------------------------------------------------------------------------------- |\n| `each(source, key, render, fallback?)` | Keyed reactive list; render receives `Readable<T>` and `Readable<number>`; plain `T[]` is a one-time static snapshot |\n| `when(condition, truthy, falsy?)` | Conditional rendering |\n| `classMap(record)` | Reactive class string from object map |\n| `styleMap(record)` | Reactive inline style string from object map |\n| `live(signal)` | One-way binding that skips stale writes during active user input; use with `@input` handler |\n| `unsafeHtml(value)` | HTML rendering sink; sanitize untrusted values before calling |\n\n### `unsafeHtml`\n\n`unsafeHtml()` is an explicit HTML injection sink. It has no global sanitizer: sanitize untrusted\ncontent before passing it to the directive, so the trust boundary remains at the call site.\n\n```ts\nimport { unsafeHtml } from '@vielzeug/ore';\n\nconst safeArticle = sanitize(userSuppliedArticle);\n\nreturn html`<article>${unsafeHtml(safeArticle)}</article>`;\n```\n\n## Host Bindings\n\n`bind(config, options?)` is a plain function imported from `@vielzeug/ore`:\n\n```ts\nbind({\n attr: { role: 'button', 'aria-expanded': () => String(open.value) },\n class: { 'is-open': open },\n style: { '--height': () => height.value + 'px' },\n on: { click: handleClick },\n});\n```\n\n`bind()` auto-registers cleanup with the component scope — no manual `onCleanup` needed. Returns a cleanup function for early teardown.\n\n### Off-host bindings\n\nPass `{ target: el }` as a second argument to bind to any element other than the host:\n\n```ts\nbind(\n { attr: { 'aria-expanded': () => String(isOpen.value) } },\n { target: triggerEl },\n);\n```\n\nEvent listener options (`once`, `capture`, `passive`) are also accepted in the second argument. Cleanup is auto-registered with the component scope when called during setup.\n\n### Reactive ARIA attributes\n\nFor reactive ARIA attribute syncing, use `bind({ aria: config }, { target })`. Shorthand keys are normalised to `aria-*` automatically (`expanded` → `aria-expanded`; `role` is passed verbatim):\n\n```ts\n// Inside setup — cleanup auto-registered\nbind(\n {\n aria: {\n expanded: () => isOpen.value,\n controls: panelId,\n haspopup: 'listbox',\n },\n },\n { target: triggerEl },\n);\n\n// Manage cleanup manually — bind() always returns a cleanup fn\nconst stopAria = bind({ aria: { expanded: () => isOpen.value } }, { target: triggerEl });\n// Call stopAria() when the trigger is swapped out\n```\n\nStatic values (strings, numbers, booleans) are applied once. Getter functions and signals create reactive effects. Setting a value to `null`, `undefined`, or `false` removes the attribute.\n\n## Slots\n\n- `slots.has(name?)` — `Readable<boolean>` — whether the named (or default) slot has assigned content\n- `slots.elements(name?)` — `Readable<Element[]>` — the assigned elements for the slot\n\nSlot signals update reactively when assigned content changes, including when slots are inserted dynamically (via `when()` or `each()`) after mount.\n\n## Context API\n\n- `createContext<T>(description?)` — Create a typed injection key\n- `provide(key, value)` — Provide a value to descendants\n- `inject(key)` — Resolve from nearest ancestor; returns `undefined` if not found\n- `inject(key, fallback)` — Resolve with a fallback value\n- `injectStrict(key)` — Resolve or throw if absent\n\n`provide()` and `inject()` must be called synchronously during `setup()`. Calling them outside a setup context throws\n`'Lifecycle hooks must be called during component setup'`. Context resolution walks the ancestor chain including shadow\nDOM boundaries. `inject()` resolves and caches its result once per consumer — provide a `Readable` (signal/computed)\nrather than a raw value if descendants need to observe later changes; re-calling `provide()` with a new raw value\nafterward is not seen by consumers that already resolved it (a dev-mode warning fires when a key is provided twice on\nthe same element).\n\n## Utilities\n\n- `ref<T>()` — Create a `Signal<T | null>` element reference. Set to the element via `ref=` in templates.\n- `createId(prefix = 'id')` — Generate a unique incremental string ID (e.g. `'id-1'`, `'id-2'`). Each call returns a new ID — it does not deduplicate by prefix.\n- `createStableId(prefix = 'id')` — Generate a unique ID that also embeds a short random tag shared across all IDs generated in the session (e.g. `'field-a3k21'`), reducing collision risk when multiple app instances run on the same page. Like `createId()`, every call returns a new ID.\n- `resetStableIdCounter()` — Reset the `createStableId()` counter to 0. Call in test `beforeEach` for deterministic IDs. Scoped to `createStableId()` only — `createId()` has no public reset (it's for uniqueness, not cross-test determinism).\n\n## Form-Associated API\n\nImport from `@vielzeug/ore`.\n\n### `useField(options)`\n\nWire a form-associated element to `ElementInternals`. Requires `formAssociated: true` on the component definition. The `disabled` state tracking via `internals.states` (CustomStateSet) is skipped with a dev warning if the API is unavailable in the current environment.\n\n```ts\ntype FormFieldOptions<T> = {\n disabled?: Readable<boolean>;\n /** Defaults to the host element active during setup. */\n el?: HTMLElement;\n /**\n * When true, a null/undefined value is submitted as '' instead of null,\n * keeping the field's key present in FormData even when the value is absent.\n * Only applies to the default toFormValue; ignored if toFormValue is provided.\n * @default false\n */\n emptyStringForNull?: boolean;\n /** Called when the ancestor <form> resets (see onFormReset) — restore local field state here. */\n onReset?: () => void;\n toFormValue?: (value: T) => File | FormData | string | null;\n /** Recomputed reactively and passed straight to internals.setValidity(). null = always valid. */\n validationMessage?: Readable<string>;\n validity?: Readable<ValidityStateFlags | null>;\n value: Signal<T> | Readable<T>;\n};\n\ntype FormFieldHandle = {\n checkValidity(): boolean;\n readonly internals: ElementInternals;\n reportValidity(): boolean;\n /** Set (non-empty message) or clear (empty string) a custom validity error. */\n setCustomValidity(message: string): void;\n};\n```\n\nPass `validity`/`validationMessage` to make `required`-style constraints participate in native constraint validation\nthrough `checkValidity()` and `reportValidity()`:\n\n```ts\nconst isBlank = (v: string) => v.trim() === '';\n\nuseField({\n validationMessage: computed(() => (required.value && isBlank(value.value) ? 'This field is required.' : '')),\n validity: computed(() => (required.value && isBlank(value.value) ? { valueMissing: true } : null)),\n value,\n});\n```\n\n## Observer APIs\n\nImport from `@vielzeug/ore`.\n\n- `resizeObserver(element)` — Returns `Readable<{ height: number; width: number }>`, initialised to `{ height: 0, width: 0 }`\n- `intersectionObserver(element, options?)` — Returns `Readable<IntersectionObserverEntry | null>`, initialised to `null`\n- `mutationObserver(element, options?)` — Returns `Readable<{ entries: MutationRecord[]; latest: MutationRecord | null }>`, initialised to `{ entries: [], latest: null }`\n- `mediaObserver(query)` — Returns `Readable<boolean>`, initialised to the query's current `matches` state\n\n## Testing APIs\n\nImport from `@vielzeug/ore/testing`.\n\n| API | Purpose |\n| ------------------------ | ------------------------------------------------------------------------------------------ |\n| `mount(setup, options?)` | Mount a component and return a test fixture |\n| `cleanup()` | Remove all mounted elements and reset test state |\n| `install(afterEach, options?)` | Register auto-cleanup; pass `{ formInternals: true }` to also install the `ElementInternals`/`FormData`/`<form>.reset()` jsdom polyfill (see below) |\n| `installFormInternalsPolyfill()` | Installs the form-internals polyfill directly (returns an `uninstall()` that restores every patched global). Usually called via `install(afterEach, { formInternals: true })` |\n| `walkFlatTree(root, visit)` | Walks the flat tree (expanding `<slot>` via `assignedElements()`) — for finding slotted content across a shadow boundary that `querySelectorAll()` can't cross |\n| `flush(options?)` | Drain reactive updates and animation frames |\n| `debugFlush()` | Run `flush()` with `console.debug` diagnostics |\n| `mock(tag, template?)` | Register a no-op stub custom element |\n| `renderHook(setup)` | Run lifecycle hooks in isolation; overload accepts `propDefs` as first arg for typed props |\n| `resetOreForTests()` | Reset styles and ID counters when mounting is managed manually |\n| `OreTimeoutError` | Error thrown when `flush()` cannot settle tracked Ore work |\n\n> **Test isolation:** `cleanup()` removes mounted elements and resets all cross-test Ore state (the stylesheet cache and ID counters) via `resetOreForTests()`. Call it in `afterEach` (or use `install()`) to prevent state leaking between tests.\n\nImport `within`, named dispatchers such as `fireClick`, and waits such as `waitUntil` or `waitForEvent` from\n`@vielzeug/assay`.\n\n> **Form-associated component testing:** jsdom implements none of the `ElementInternals` form-association API — `install(afterEach, { formInternals: true })` polyfills `setFormValue`/`setValidity`/`checkValidity`/`reportValidity`/`validationMessage`/`validity`/`states`, mixes `checkValidity`/`reportValidity`/`validity`/`validationMessage` onto the host element itself (real browsers do this for any `formAssociated: true` element), makes `FormData` collect a form-associated element's set value, and makes `<form>.reset()` invoke `formResetCallback()`. Every patch is a guarded no-op when its target already exists, and `installFormInternalsPolyfill()` returns an `uninstall()` that restores every patched global. The polyfill is opt-in (`{ formInternals: true }`) because the patches are global — suites without form-associated components shouldn't carry them. A downstream package (e.g. a component library built on `ore`) should rely on this instead of hand-rolling its own copy.\n\n#### `Fixture` interface\n\n```ts\ninterface Fixture<T extends HTMLElement = HTMLElement> {\n [Symbol.dispose](): void; // Delegates to dispose() — enables `using` declarations\n element: T;\n readonly disposed: boolean; // true after dispose() has been called\n readonly shadow: ShadowRoot | null;\n get<E extends Element>(selector: string): E;\n query<E extends Element>(selector: string): E | null;\n queryAll<E extends Element>(selector: string): E[];\n getByText<E extends Element>(text: string, selector?: string): E;\n queryByText<E extends Element>(text: string, selector?: string): E | null;\n queryAllByText<E extends Element>(text: string, selector?: string): E[];\n getByTestId<E extends Element>(testId: string): E;\n queryByTestId<E extends Element>(testId: string): E | null;\n queryAllByTestId<E extends Element>(testId: string): E[];\n attr(name: string, value: string | number | boolean): Promise<void>;\n attrs(record: Record<string, string | number | boolean>): Promise<void>;\n flush(options?: FlushOptions): Promise<void>;\n act(fn: () => unknown): Promise<void>;\n dispose(): void; // Removes the component from the DOM — idempotent\n}\n```\n\n#### `renderHook`\n\nUseful for testing composable lifecycle hooks (`onMounted`, `watchEffect`, `inject`, etc.) without a template. `onMounted`/`onCleanup`/`watchEffect`/... work exactly as inside a real `setup()`, since they resolve the same implicit current-component context:\n\n```ts\n// Without props\nconst { result, flush, dispose } = await renderHook(() => {\n const count = signal(0);\n onMounted(() => {\n count.value = 1;\n });\n return count;\n});\nexpect(result.value).toBe(1);\n\n// With typed props (prop-defs overload)\nconst { result } = await renderHook({ label: prop.string('hello'), count: prop.number(0) }, (props) => props.label);\nexpect(result.value).toBe('hello');\n```\n\n## Ripple Primitives\n\nOre does **not** re-export reactive primitives. Import them directly from `@vielzeug/ripple`:\n\n```ts\nimport { batch, computed, signal, watch } from '@vielzeug/ripple';\n```\n\nSee the [Ripple documentation](/ripple/) for the full API.\n\n## Lifecycle Events\n\n| Event | When |\n| ------------------ | ------------------------------------------------------------- |\n| `ore:connect` | After every `connectedCallback` (including reconnects) |\n| `ore:disconnect` | After `disconnectedCallback`, before component state is reset |\n| `ore:error` | When a lifecycle callback fails — bubbles, composed; detail is `OreLifecycleError` |\n\n## Types\n\n```ts\ntype PropDef<T> = {\n readonly default: T;\n readonly parse: (value: string | null) => T;\n reflect?: boolean;\n};\n\ntype PropsDef<T extends Record<string, unknown>> = {\n [K in keyof Required<T>]: PropDef<T[K & keyof T]>;\n};\n\ntype PropInputDefs = Record<string, PropDef<unknown>>;\n\n/**\n * Infer reactive props type from a PropInputDefs map.\n * Each entry becomes Readable<T> keyed by prop name.\n */\ntype InferProps<D extends PropInputDefs> = {\n readonly [K in keyof D]-?: Readable<InferPropValue<D[K]>>;\n};\n\n// Runtime hooks — all plain functions imported from '@vielzeug/ore', not fields on an object.\ntype OnMountedCallback = () => Cleanup | undefined;\ntype OnFormResetCallback = () => void;\n\ndeclare function onMounted(fn: OnMountedCallback): void; // DOM-ready callback; runs after each connection's render\ndeclare function onCleanup(fn: Cleanup): void; // Register teardown; called on disconnect\ndeclare function onElement<T extends HTMLElement>(\n ref: Readable<T | null>,\n callback: (el: T) => Cleanup | undefined,\n): () => void;\ndeclare function onEvent<K extends keyof HTMLElementEventMap>(\n target: EventTarget | null | undefined,\n event: K,\n listener: (e: HTMLElementEventMap[K]) => void,\n options?: AddEventListenerOptions,\n): void;\ndeclare function onEvent(\n target: EventTarget | null | undefined,\n event: string,\n listener: EventListener,\n options?: AddEventListenerOptions,\n): void;\ndeclare function onFormReset(fn: OnFormResetCallback): void; // Runs on every ancestor <form> reset; formAssociated only\ndeclare function watchEffect(fn: () => Cleanup | undefined): () => void; // Scoped reactive effect; auto-cleaned on disconnect\ndeclare function bind(config: HostBindConfig, options?: BindOptions): () => void; // Bindings for host or any target element\ndeclare function provide<T>(key: InjectionKey<T>, value: T): void; // Register a context value on the host element\ndeclare function inject<T>(key: InjectionKey<T>): T | undefined;\ndeclare function inject<T>(key: InjectionKey<T>, fallback: T): T;\ndeclare function getHost(): HTMLElement; // The current component's host element\ndeclare function useEmit<Emits extends Record<string, unknown> = Record<string, never>>(): EmitFn<Emits>;\ndeclare function useSlots<SlotNames extends string = string>(): ComponentSlots<SlotNames>;\n\ntype ComponentDefinition<Props extends Record<string, unknown> = Record<never, never>> = {\n formAssociated?: boolean;\n props?: PropsDef<Props>;\n setup: (props: InferProps<PropsDef<Props>>) => HTMLResult | null;\n shadow?: Partial<ShadowRootInit> | false; // false = light DOM\n styles?: (string | CSSStyleSheet | CSSResult)[];\n};\n\ntype HostBindingValue =\n | (() => string | number | boolean | null | undefined)\n | Readable<string | number | boolean | null | undefined>\n | string\n | number\n | boolean\n | null\n | undefined;\n\ntype ReflectConfig = Record<string, HostBindingValue>;\n\ntype HostBindConfig = {\n aria?: ReflectConfig;\n attr?: ReflectConfig;\n class?: (() => Record<string, boolean>) | Record<string, Readable<boolean> | (() => boolean) | boolean>;\n on?: Record<string, ((event: Event) => void) | undefined>;\n style?: Record<string, HostBindingValue>;\n};\n\ntype BindOptions = AddEventListenerOptions & {\n target?: Element;\n};\n\ntype HostBindFn = (config: HostBindConfig, options?: BindOptions) => () => void;\n\ntype ComponentSlots<S extends string = string> = {\n elements(name?: S): Readable<Element[]>;\n has(name?: S): Readable<boolean>;\n};\n\ntype Ref<T extends Element> = Signal<T | null>;\n\ntype RefCallback<T extends Element> = (el: T | null) => void;\n\ntype InjectionKey<T> = symbol & { readonly __ore_injection_key?: T };\n\ninterface HTMLResult {\n mount(\n parent: ParentNode,\n anchor: Node | null,\n registerCleanup: (fn: () => void) => void,\n ): Node[];\n}\n\ntype CSSResult = {\n content: string;\n toString(): string;\n};\n\ntype LiveBinding<T> = { readonly source: Readable<T> };\n\ntype EmitFn<T extends Record<string, unknown>> = {\n <K extends KeysWithoutDetail<T>>(event: K): boolean;\n <K extends Exclude<keyof T, KeysWithoutDetail<T>>>(event: K, detail: T[K]): boolean;\n};\n// KeysWithoutDetail is an internal helper type, not exported.\n\ntype FormFieldOptions<T = unknown> = {\n disabled?: Readable<boolean>;\n el?: HTMLElement;\n emptyStringForNull?: boolean;\n onReset?: () => void;\n toFormValue?: (value: T) => File | FormData | string | null;\n validationMessage?: Readable<string>;\n validity?: Readable<ValidityStateFlags | null>;\n value: Signal<T> | Readable<T>;\n};\n\ntype FormFieldHandle = {\n checkValidity: () => boolean;\n readonly internals: ElementInternals;\n reportValidity: () => boolean;\n setCustomValidity: (message: string) => void;\n};\n\ntype MutationObserverValue = {\n entries: MutationRecord[];\n latest: MutationRecord | null;\n};\n\n/** Phase in which a OreError occurred. */\ntype OreErrorPhase = 'each-reconcile' | 'form-reset' | 'mounted' | 'setup';\n```\n\n## Errors\n\n`OreError` is the base class for every Ore error class — `err instanceof OreError` catches all of them.\n`OreError.is(err)` is the equivalent static type-guard.\n\n- **`OreApiError`** — thrown when the `ore` API itself is misused: calling `define()` with a duplicate tag, calling a lifecycle hook (`inject`, `onMounted`, `onCleanup`, `onEvent`, …) outside of `setup()`, or passing an invalid prop definition to `define()`.\n- **`OreInternalError`** — thrown when an Ore invariant fails, indicating a package bug rather than invalid application code.\n- **`OreLifecycleError`** — reported in the `ore:error` event when component `setup()`, a mounted callback, a form-reset callback, or `each()` reconciliation fails. Extends `OreError` with:\n - `component: string` — the element's local name\n - `phase: OreErrorPhase` — `'setup'` | `'mounted'` | `'form-reset'` | `'each-reconcile'`\n - `cause: Error` — the original error thrown by `setup()`\n- **`OreTimeoutError`** — thrown by `flush()` (from `@vielzeug/ore/testing`) when pending Ore work does not settle before its timeout.\n\nLifecycle failures dispatch a bubbling, composed `ore:error` event whose `detail` is the `OreLifecycleError`. Setup\nfailures still rethrow their original error; mounted and form-reset callback failures are reported through the same\nevent so their remaining callbacks can continue.\n",
|
|
6
|
-
"usage": "---\ntitle: Ore — Usage Guide\ndescription: Practical Ore usage patterns for components, props, templates, slots, context, forms, observers, and tests.\n---\n\n[[toc]]\n\n## Basic Usage\n\n`define(tag, definition)` registers a custom element.\n\nYour `setup()` function receives typed prop signals and returns an `HTMLResult` directly. Its state belongs to the\ncurrent connection: disconnect disposes it, and reconnecting the same element runs setup again.\n\n```ts\nimport { signal } from '@vielzeug/ripple';\nimport { define, html } from '@vielzeug/ore';\n\ndefine('status-chip', {\n setup() {\n const online = signal(true);\n\n return html`\n <button @click=${() => (online.value = !online.value)}>${() => (online.value ? 'Online' : 'Offline')}</button>\n `;\n },\n});\n```\n\nEverything besides `props` — lifecycle hooks, host bindings, context, slots, emit — is a plain function imported from `@vielzeug/ore`, called directly from `setup()` (or a composable it calls):\n\n```ts\nimport { define, getHost, html, bind, useEmit, useSlots } from '@vielzeug/ore';\n\ndefine('my-widget', {\n setup(_props) {\n const el = getHost(); // the host HTMLElement\n const emit = useEmit<{ close: undefined }>(); // typed event emitter\n const slots = useSlots<'header'>(); // reactive slot observation\n\n bind({ attr: { role: 'group' } }); // host binding helper (attr, class, style, on)\n\n return html`<slot></slot>`;\n },\n});\n```\n\n## signals and effects\n\nOre does not re-export ripple primitives — import them directly from `@vielzeug/ripple`.\n\n```ts\nimport { batch, computed, effect, signal, watch } from '@vielzeug/ripple';\n\nconst count = signal(0);\nconst doubled = computed(() => count.value * 2);\n\neffect(() => {\n console.log('doubled =', doubled.value);\n});\n\nwatch(count, (next, prev) => {\n console.log('count changed', prev, '->', next);\n});\n\nbatch(() => {\n count.value = 1;\n count.value = 2;\n});\n```\n\n## onMounted and lifecycle\n\nUse `onMounted()` for DOM-dependent initialization that must run after the template is mounted. Use `onElement(ref, cb)` for work tied to a specific DOM node. `onEvent()` attaches a listener that is automatically removed on disconnect.\n\n```ts\nimport { signal } from '@vielzeug/ripple';\nimport { define, html, onElement, onEvent, onMounted, ref, useSlots } from '@vielzeug/ore';\n\ndefine('deferred-init', {\n setup(_props) {\n const tabIndex = signal(0);\n const inputRef = ref<HTMLInputElement>();\n const slots = useSlots<'items'>();\n\n onMounted(() => {\n const items = slots.elements('items').value;\n console.log('Found', items.length, 'items');\n });\n\n onElement(inputRef, (input) => {\n input.focus();\n });\n\n onEvent(window, 'keydown', (e: KeyboardEvent) => {\n if (e.key === 'Escape') tabIndex.value = 0;\n });\n\n return html`<div><slot name=\"items\"></slot><input ref=${inputRef} /></div>`;\n },\n});\n```\n\n## prop definitions\n\nUse `prop.*` helpers for common cases, or raw `PropDef` objects for custom parsing or `reflect: false`.\n\n```ts\nimport { define, html, prop } from '@vielzeug/ore';\n\ndefine('x-button', {\n props: {\n label: prop.string('Button'),\n disabled: prop.bool(false),\n variant: prop.oneOf(['primary', 'secondary'] as const, 'primary'),\n count: prop.number(0),\n },\n setup(props) {\n return html`\n <button ?disabled=${props.disabled} data-variant=${props.variant}>${props.label} (${props.count})</button>\n `;\n },\n});\n```\n\n## template bindings\n\n`html` supports text, attributes, booleans, properties, events, refs, and nested templates.\n\n```ts\nimport { computed, signal } from '@vielzeug/ripple';\nimport { define, html, ref } from '@vielzeug/ore';\n\ndefine('profile-name', {\n setup() {\n const name = signal('Alice');\n const inputRef = ref<HTMLInputElement>();\n\n return html`\n <label title=${computed(() => 'Current: ' + name.value)}>Name</label>\n <input\n ref=${inputRef}\n value=${name}\n aria-label=${() => 'Current name ' + name.value}\n @input=${(event: Event) => {\n name.value = (event.target as HTMLInputElement).value;\n }} />\n <p>Hello ${name}</p>\n `;\n },\n});\n```\n\n## directives\n\nOre exports `each`, `classMap`, `styleMap`, `when`, `live`, and `unsafeHtml` from `@vielzeug/ore`. Use ordinary\nattribute bindings plus native event handlers for two-way input state; no special model directive is required.\n\n```ts\nimport { signal } from '@vielzeug/ripple';\nimport { classMap, define, each, html, styleMap, when } from '@vielzeug/ore';\n\ndefine('task-list', {\n setup() {\n const tasks = signal([{ id: 1, text: 'Write tests' }]);\n const active = signal(true);\n\n return html`\n <ul\n class=\"${classMap({ ready: () => tasks.value.length > 0 })}\"\n style=${styleMap({ opacity: () => (active.value ? 1 : 0.5) })}>\n ${when(\n () => active.value,\n () => html`<li>Active</li>`,\n () => html`<li>Paused</li>`,\n )}\n ${each(\n tasks,\n (task) => task.id,\n (task) => html`<li>${() => task.value.text}</li>`,\n )}\n </ul>\n `;\n },\n});\n```\n\n### each() API\n\n`each(source, key, render, fallback?)` takes positional arguments:\n\n- **source** — signal, getter, or plain array\n- **key** — function returning a unique key per item\n- **render** — receives reactive `item` and `index` signals\n- **fallback** — optional, rendered when the list is empty\n\n```ts\neach(\n items,\n (item) => item.id,\n (item, index) => html`<li>#${index}: ${() => item.value.label}</li>`,\n () => html`<li>No items</li>`,\n);\n```\n\n## live form bindings\n\nUse `live(signal)` for inputs that should preserve in-progress user edits instead of overwriting the DOM on stale writes.\n\n```ts\nimport { signal } from '@vielzeug/ripple';\nimport { define, html, live } from '@vielzeug/ore';\n\ndefine('live-search', {\n setup() {\n const query = signal('');\n\n return html`\n <input value=${live(query)} @input=${(e: Event) => (query.value = (e.target as HTMLInputElement).value)} />\n `;\n },\n});\n```\n\n## host bindings\n\n`bind()` wires reactive attrs, classes, styles, and events to the host element.\n\n```ts\nimport { signal } from '@vielzeug/ripple';\nimport { bind, define, html } from '@vielzeug/ore';\n\ndefine('x-toggle', {\n setup(_props) {\n const open = signal(false);\n\n bind({\n attr: { 'aria-expanded': () => String(open.value), role: 'button', tabindex: 0 },\n class: { 'is-open': open },\n on: { click: () => (open.value = !open.value) },\n });\n\n return html`<slot></slot>`;\n },\n});\n```\n\nThe `bind` config supports `attr`, `class`, `style`, and `on` sections.\n\n## ARIA bindings\n\nUse `bind({ aria: config }, { target })` to reactively sync ARIA attributes to any element. Shorthand keys are normalised to `aria-*` automatically — `expanded` becomes `aria-expanded`, `role` is set verbatim.\n\n```ts\nimport { signal } from '@vielzeug/ripple';\nimport { bind, define, html, onMounted } from '@vielzeug/ore';\n\ndefine('x-disclosure', {\n setup(_props) {\n const open = signal(false);\n const panelId = 'disclosure-panel';\n\n bind({\n attr: { role: 'button', tabindex: 0 },\n on: { click: () => (open.value = !open.value) },\n });\n\n onMounted(() => {\n const trigger = document.querySelector('#trigger') as HTMLElement;\n if (trigger) {\n // bind() registers cleanup automatically when called inside setup\n bind(\n {\n aria: {\n controls: panelId,\n expanded: () => String(open.value),\n haspopup: 'region',\n },\n },\n { target: trigger },\n );\n }\n });\n\n return html`<slot></slot>`;\n },\n});\n```\n\nStatic values are applied once. Getter functions create reactive effects. Setting a value to `null`, `undefined`, or `false` removes the attribute.\n\n`bind()` always returns a cleanup function. Use it to stop syncing early when a trigger element can be swapped out:\n\n```ts\nonMounted(() => {\n const trigger = document.querySelector('#trigger') as HTMLElement;\n const stopAria = bind({ aria: { expanded: () => String(open.value) } }, { target: trigger });\n\n // Stop syncing when the trigger is replaced\n onCleanup(stopAria);\n});\n```\n\n### Binding a non-host element with `bind()`\n\nPass `{ target: el }` as a second argument to bind attributes, classes, styles, or events to any element:\n\n```ts\nimport { signal } from '@vielzeug/ripple';\nimport { bind, define, html, onMounted, ref } from '@vielzeug/ore';\n\ndefine('button-wrapper', {\n setup(_props) {\n const visible = signal(false);\n const btnRef = ref<HTMLButtonElement>();\n\n onMounted(() => {\n const btn = btnRef.value;\n if (!btn) return;\n\n bind(\n {\n attr: { 'aria-pressed': () => String(visible.value) },\n on: { click: () => (visible.value = !visible.value) },\n },\n { target: btn },\n );\n });\n\n return html`<button ref=${btnRef}>Toggle</button>`;\n },\n});\n```\n\n## slots and emits\n\n```ts\nimport { define, html, useEmit, useSlots, when } from '@vielzeug/ore';\n\ndefine('card-with-footer', {\n setup(_props) {\n const slots = useSlots<'header' | 'footer'>();\n const emit = useEmit<{ action: undefined }>();\n\n return html`\n <div class=\"card\">\n <slot name=\"header\"></slot>\n <slot></slot>\n ${when(slots.has('footer'), () => html`<footer><slot name=\"footer\"></slot></footer>`)}\n </div>\n <button @click=${() => emit('action')}>Go</button>\n `;\n },\n});\n```\n\nPass a `SlotNames` type parameter to `useSlots<SlotNames>()` to get typed `slots.has()` and `slots.elements()` calls.\n\n## context provide/inject\n\n```ts\nimport { signal } from '@vielzeug/ripple';\nimport { createContext, define, html, injectStrict, provide } from '@vielzeug/ore';\n\nconst COUNT_CTX = createContext<ReturnType<typeof signal<number>>>('count');\n\ndefine('count-provider', {\n setup(_props) {\n const count = signal(0);\n provide(COUNT_CTX, count);\n\n return html`<button @click=${() => count.value++}><slot></slot></button>`;\n },\n});\n\ndefine('count-consumer', {\n setup() {\n const count = injectStrict(COUNT_CTX);\n\n return html`<p>Count: ${count}</p>`;\n },\n});\n```\n\n## form-associated elements\n\n```ts\nimport { signal } from '@vielzeug/ripple';\nimport { define, html, prop } from '@vielzeug/ore';\nimport { useField } from '@vielzeug/ore';\n\ndefine('rating-input', {\n formAssociated: true,\n setup() {\n const value = signal(0);\n const field = useField({ value });\n\n return html`\n <button @click=${() => (value.value = 1)}>1</button>\n <button @click=${() => (value.value = 2)}>2</button>\n <button @click=${() => (value.value = 3)}>3</button>\n <button @click=${() => field.reportValidity()}>Validate</button>\n <p>Current: ${value}</p>\n `;\n },\n});\n```\n\n## platform observers\n\nObserver helpers from `@vielzeug/ore` require real DOM nodes, so call them inside `onMounted()`.\n\n```ts\nimport { effect } from '@vielzeug/ripple';\nimport { define, html, intersectionObserver, mediaObserver, onMounted, ref, resizeObserver } from '@vielzeug/ore';\n\ndefine('x-observed', {\n setup(_props) {\n const boxRef = ref<HTMLDivElement>();\n\n onMounted(() => {\n const element = boxRef.value;\n if (!element) return;\n\n const size = resizeObserver(element);\n const visible = intersectionObserver(element, { threshold: 0.5 });\n const dark = mediaObserver('(prefers-color-scheme: dark)');\n\n // effect() auto-tracks every signal read inside — re-runs when any of the three change.\n effect(() => {\n console.log(size.value.width, visible.value?.isIntersecting, dark.value);\n });\n });\n\n return html`<div ref=${boxRef}>Observe me</div>`;\n },\n});\n```\n\n## testing utilities\n\nImport from `@vielzeug/ore/testing`.\n\n```ts\nimport { afterEach, describe, expect, it } from 'vitest';\nimport { signal } from '@vielzeug/ripple';\nimport { fireClick } from '@vielzeug/assay';\nimport { html } from '@vielzeug/ore';\nimport { cleanup, mount } from '@vielzeug/ore/testing';\n\ndescribe('my-counter', () => {\n afterEach(cleanup);\n\n it('increments on click', async () => {\n let count!: ReturnType<typeof signal<number>>;\n const { query, act } = await mount(() => {\n count = signal(0);\n return html`<button @click=${() => count.value++}>${count}</button>`;\n });\n\n expect(query('button')?.textContent).toBe('0');\n\n await act(() => fireClick(query('button')!));\n\n expect(query('button')?.textContent).toBe('1');\n });\n});\n```\n\n## Framework Integration\n\nOre components are standard custom elements and work natively in any framework.\n\n::: code-group\n\n```tsx [React]\n// React 19+ supports custom elements natively.\nimport './x-toggle'; // wherever define('x-toggle', { ... }) is called\n\nfunction App() {\n return <x-toggle aria-label=\"Open menu\" />;\n}\n```\n\n```ts [Vue 3]\n<script setup lang=\"ts\">\nimport './x-toggle'; // wherever define('x-toggle', { ... }) is called\nimport { ref } from 'vue';\n\nconst open = ref(false);\n</script>\n\n<template>\n <x-toggle :aria-label=\"'Open menu'\" @click=\"open = !open\" />\n</template>\n```\n\n```svelte [Svelte]\n<script>\n import './x-toggle'; // wherever define('x-toggle', { ... }) is called\n\n function handleClick() {\n console.log('toggled');\n }\n</script>\n\n<x-toggle aria-label=\"Open menu\" on:click={handleClick} />\n```\n\n:::\n\n## Working with Other Vielzeug Libraries\n\n### With Ripple\n\nImport ripple primitives directly from `@vielzeug/ripple` for standalone reactive state outside components.\n\n```ts\nimport { signal, computed } from '@vielzeug/ripple';\nimport { define, html } from '@vielzeug/ore';\n\n// Shared state created outside any component\nconst theme = signal<'light' | 'dark'>('light');\nconst isDark = computed(() => theme.value === 'dark');\n\ndefine('theme-toggle', {\n setup() {\n return html`\n <button @click=${() => (theme.value = isDark.value ? 'light' : 'dark')}>\n ${() =>\n isDark.value ? '<ore-icon name=\"sun\" size=\"16\"></ore-icon>' : '<ore-icon name=\"moon\" size=\"16\"></ore-icon>'}\n </button>\n `;\n },\n});\n```\n\n### With Forge\n\nUse `@vielzeug/forge` for typed form state. `useField()` remains intentionally narrow: it connects a form-associated\ncustom element to native `ElementInternals` without imposing submission, validation, or dirty-state policy.\n\n```ts\nimport { createForm } from '@vielzeug/forge';\nimport { define, html } from '@vielzeug/ore';\n\ndefine('signup-form', {\n setup(_props) {\n const form = createForm({ initialValues: { email: '' } });\n\n return html`\n <form\n @submit=${(event: SubmitEvent) => {\n event.preventDefault();\n void form.submit(async (values) => {\n console.log(values);\n });\n }}>\n <slot></slot>\n </form>\n `;\n },\n});\n```\n\n## Best Practices\n\n- Setup returns `html\\`...\\`` directly — not a function wrapping the template.\n- Use `watchEffect()` for reactive subscriptions tied to component lifetime — it auto-registers cleanup on disconnect.\n- Use `onElement(ref, cb)` instead of `onMounted` when the work is tied to a single DOM node.\n- Bind host attributes and classes via `bind()` rather than mutating the element directly.\n- Provide context at the nearest ancestor — avoid global context singletons.\n- Call `onCleanup()` for every resource allocated in `setup()` (WebSockets, intervals, external subscriptions).\n- Use `live(signal)` for form inputs to prevent clobbering user-in-progress edits.\n- Extract composable helper functions freely — `onMounted`/`onCleanup`/`bind`/... resolve the active component through implicit context, so they work from any function called (transitively) during `setup()`, with no need to pass them in as parameters.\n- Test component mounting and lifecycle with `@vielzeug/ore/testing`; import generic DOM events, queries, and waits\n from `@vielzeug/assay`.\n",
|
|
4
|
+
"index": "---\ntitle: Ore — Web component authoring with signals\ndescription: Functional custom-element authoring with typed props, reactive templates, lifecycle helpers, and testing utilities.\npackage: ore\ncategory: ui-primitives\nkeywords: [web-components, custom-elements, reactive, templates, signals, lifecycle]\nrelated: [ripple, refine, orbit]\nexports: [define, prop, html, css, ref, createContext, inject, injectStrict, provide, onMounted, onCleanup, onEvent, onElement, onFormReset, watchEffect, useEmit, useSlots, getHost, bind, each, when, classMap, styleMap, live, unsafeHtml, useField, createId, createStableId, resetStableIdCounter, OreError, OreApiError, OreInternalError, OreLifecycleError, BindOptions]\nenvironments: [browser]\n---\n\n<!-- markdownlint-disable MD025 MD033 MD060 -->\n\n<PackageHero package=\"ore\" />\n\n## Why Ore?\n\nOre keeps custom elements functional and signal-driven while giving you direct control over templates, lifecycle hooks, host bindings, and form-associated behavior.\n\n```ts\n// Before — vanilla custom element boilerplate\nclass MyCounter extends HTMLElement {\n #count = 0;\n connectedCallback() {\n this.attachShadow({ mode: 'open' });\n this.#render();\n }\n #render() {\n this.shadowRoot!.innerHTML = `<button>${this.#count}</button>`;\n this.shadowRoot!.querySelector('button')!.onclick = () => {\n this.#count++;\n this.#render();\n };\n }\n}\ncustomElements.define('my-counter', MyCounter);\n\n// After — Ore\nimport { signal } from '@vielzeug/ripple';\nimport { define, html } from '@vielzeug/ore';\n\ndefine('my-counter', {\n setup() {\n const count = signal(0);\n return html`<button @click=${() => count.value++}>${count}</button>`;\n },\n});\n```\n\n| Feature | Ore | Lit | Stencil |\n| -------------------------- | ------------------------------------------- | ----------------------------------------------------------------- | ------------------------------------------ |\n| Bundle size | <PackageInfo package=\"ore\" type=\"size\" /> | ~12 kB | ~60 kB+ toolchain |\n| Signal-first runtime | <ore-icon name=\"check\" size=\"16\"></ore-icon> | <ore-icon name=\"x\" size=\"16\"></ore-icon> (separate signals package) | <ore-icon name=\"x\" size=\"16\"></ore-icon> |\n| Functional component setup | <ore-icon name=\"check\" size=\"16\"></ore-icon> | Partial | <ore-icon name=\"x\" size=\"16\"></ore-icon> |\n| Typed prop helpers | <ore-icon name=\"check\" size=\"16\"></ore-icon> | Partial | <ore-icon name=\"check\" size=\"16\"></ore-icon> |\n| Host binding helpers | <ore-icon name=\"check\" size=\"16\"></ore-icon> | Partial | Partial |\n| Form-associated helpers | <ore-icon name=\"check\" size=\"16\"></ore-icon> | Manual | Partial |\n| Zero dependencies | <ore-icon name=\"check\" size=\"16\"></ore-icon> | <ore-icon name=\"check\" size=\"16\"></ore-icon> | <ore-icon name=\"x\" size=\"16\"></ore-icon> |\n\n<div class=\"decision-callout\">\n\n**Use Ore when** you want typed, signal-driven custom elements with minimal runtime overhead and no framework lock-in.\n\n**Consider Lit when** you need a mature ecosystem with wide community adoption and don't need signal-based reactivity.\n\n</div>\n\n## Installation\n\n::: code-group\n\n```sh [pnpm]\npnpm add @vielzeug/ore @vielzeug/ripple\n```\n\n```sh [npm]\nnpm install @vielzeug/ore @vielzeug/ripple\n```\n\n```sh [yarn]\nyarn add @vielzeug/ore @vielzeug/ripple\n```\n\n:::\n\n## Quick Start\n\n```ts\nimport { computed, signal } from '@vielzeug/ripple';\nimport { bind, css, define, html, onMounted, prop } from '@vielzeug/ore';\n\ndefine('my-counter', {\n props: {\n label: prop.string('Count'),\n step: prop.number(1),\n },\n styles: [\n css`\n :host {\n display: inline-grid;\n gap: 0.5rem;\n }\n `,\n ],\n setup(props) {\n const count = signal(0);\n const doubled = computed(() => count.value * 2);\n\n bind({ class: { 'is-positive': () => count.value > 0 } });\n\n onMounted(() => console.log('mounted'));\n\n return html`\n <button @click=${() => (count.value += props.step.value)}>${props.label}: ${count}</button>\n <p>Doubled: ${doubled}</p>\n `;\n },\n});\n```\n\n## Features\n\n<div class=\"features-grid\">\n\n- Signal-first runtime with `signal`, `computed`, `watch`, `batch` from `@vielzeug/ripple` — import them directly\n- Functional component authoring via `define(tag, { props, setup, styles, formAssociated })`\n- Props via `prop.*` helpers (`prop.string`, `prop.number`, `prop.bool`, `prop.oneOf`, `prop.json`, `prop.data`) or raw `PropDef` objects\n- `setup(props)` takes only props and returns an `HTMLResult` directly: `return html\\`...\\``\n- Lifecycle hooks — `onMounted`, `onCleanup`, `onEvent`, `onElement`, `watchEffect` — plain functions imported from `@vielzeug/ore`, called directly from `setup()` or any composable it calls\n- Directives: `each` (keyed reactive list rendering), `classMap`, `styleMap`, `when`, `live`, `unsafeHtml`\n- Host bindings via `bind({ attr, class, style, on })` — pass `{ target: el }` to bind any off-host element\n- Reactive ARIA sync via `bind({ aria }, { target })` — applies `aria-*` attributes reactively to any element, auto-cleanup on disconnect\n- Context via `provide(key, value)` / `inject(key)`; typed emit/slots via `useEmit<Emits>()` / `useSlots<SlotNames>()`\n- Form-associated `useField()` and observer helpers are root exports\n- Testing utilities (`@vielzeug/ore/testing`) — `mount`, `renderHook`, `flush`, `cleanup`\n- Generic testing utilities (scoped queries, named event dispatchers, and async waits) are exported by `@vielzeug/assay`\n- Debug utilities (`@vielzeug/ore/testing`) — `debugFlush()` for diagnosing update timing\n\n</div>\n\n## Package Entry Points\n\n| Import | Purpose |\n| --------------------------- | ----------------------------------------------------------------------------- |\n| `@vielzeug/ore` | All browser runtime APIs: components, directives, `useField`, and observers |\n| `@vielzeug/ore/testing` | Ore-specific mounting, lifecycle flushing, hooks, cleanup, and form internals |\n| `@vielzeug/assay` | Generic DOM events, scoped queries, and async waiting |\n\n## Documentation\n\n<div class=\"doc-links\">\n\n- [Usage Guide](./usage.md)\n- [API Reference](./api.md)\n- [Examples](./examples.md)\n- [Migration Guide](./migration.md)\n\n</div>\n\n## See Also\n\n<div class=\"see-also\">\n\n- [Refine](../refine/index.md) for prebuilt accessible components powered by Ore.\n- [Ripple](../ripple/index.md) for reactive state used inside Ore components.\n- [Forge](../forge/index.md) for typed form state that integrates with Ore.\n\n</div>\n\n<!-- markdownlint-enable MD025 MD033 MD060 -->\n",
|
|
5
|
+
"api": "---\ntitle: Ore — API Reference\ndescription: Complete API reference for @vielzeug/ore and @vielzeug/ore/testing.\n---\n\n[[toc]]\n\n## API Overview\n\nAll browser-runtime symbols below are imported from `@vielzeug/ore`. Lifecycle/context/binding functions (`onMounted`, `onCleanup`, `onEvent`, `onElement`, `watchEffect`, `bind`, `provide`, `useEmit`, `useSlots`, `getHost`) resolve the active component through an implicit \"current component\" context — they work when called synchronously during `setup()`, or from any composable function `setup()` calls (transitively), but throw if called outside that window.\n\n> `watchEffect` is not named `watch` — `@vielzeug/ripple` already exports a `watch(source, callback)` with different semantics (explicit source + old/new value pair), and the two are frequently imported in the same file.\n\n| Symbol | Purpose | Execution mode | Common gotcha |\n| ---------------------- | ----------------------------------------------------- | -------------- | -------------------------------------------------------------------------- |\n| `define()` | Register a custom element with reactive setup | Sync | Tag must contain a hyphen; call before first use |\n| `html` | Tagged template literal returning HTMLResult | Sync | Expressions must be signals, functions, or primitives |\n| `prop.*` | Typed prop helpers (string, bool, number, …) | Sync | Prop values are signals — read `.value` |\n| `provide()`/`inject()` | Context API for parent-to-descendant sharing | Setup only | Must be called synchronously during `setup()` |\n| `ref()` | Reactive reference to a DOM element | Sync | Value is null until after first mount |\n| `createContext()` | Create a typed injection key | Sync | Context is scoped to the component tree |\n| `each()` | Keyed list rendering with DOM diffing | Sync | Duplicate keys report `ore:error`; plain `T[]` is a one-time static render |\n| `when()` | Conditional branch rendering | Sync | Getter-fn computed disposed on cleanup; static bool skips subscription |\n| `live(signal)` | One-way binding that skips stale writes during input | Sync | Use for controlled inputs alongside a manual `@input` handler |\n| `onMounted(fn)` | DOM-ready callback | Setup only | Must be called synchronously during `setup()` |\n| `onCleanup(fn)` | Register teardown | Setup only | Called on component disconnect |\n| `onEvent(target, …)` | Scoped event listener with auto-cleanup | Setup only | No-ops on null target; removed on disconnect |\n| `useField(options)` | Wire signal to form `ElementInternals` | Setup only | Requires `formAssociated: true` on the component definition |\n| `onFormReset(fn)` | Run work when the ancestor `<form>` resets | Setup only | Fires every reset (not one-shot); only for `formAssociated: true` components |\n| `useEmit<Emits>()` | Typed `emit()` bound to the current host | Setup only | Call once per component; returns `dispatchEvent`'s boolean (`false` if a listener called `preventDefault()`) |\n| `useSlots<SlotNames>()`| Reactive slot presence/element signals | Setup only | Safe to call more than once — the underlying registry is created once |\n| `getHost()` | The current component's host element | Setup only | Prefer a higher-level helper (`bind`, …) when one exists |\n\n## Package Entry Points\n\n| Import | Purpose |\n| ------------------------- | ------------------------------------------------------------------ |\n| `@vielzeug/ore` | All browser runtime APIs, including directives, fields, and lifecycle helpers |\n| `@vielzeug/ore/testing` | Ore-specific mounting, lifecycle, hook, cleanup, and form test support |\n| `@vielzeug/assay` | Generic DOM events, scoped queries, and async waiting |\n\n## Core Component API\n\n### `define(tag, definition)`\n\n```ts\ndefine<Props>(tag: string, definition: ComponentDefinition<Props>): void;\n```\n\nThe `setup()` function receives only typed prop signals:\n\n```ts\nsetup(props) {\n return html`<div>${props.label}</div>`;\n}\n```\n\nEverything else — lifecycle hooks, host bindings, context, slots, emit — is a plain function imported from `@vielzeug/ore`, called directly from `setup()` (or a composable it calls):\n\n```ts\nimport { define, html, onMounted, useEmit, useSlots } from '@vielzeug/ore';\n\ndefine('my-card', {\n setup(_props) {\n const emit = useEmit<{ close: undefined }>();\n const slots = useSlots<'header' | 'footer'>();\n\n onMounted(() => console.log('mounted'));\n\n // emit() returns dispatchEvent's boolean — false if a listener called preventDefault()\n const notCancelled = emit('close');\n\n return html`${when(slots.has('header'), () => html`<slot name=\"header\"></slot>`)}`;\n },\n});\n```\n\n`useEmit<Emits>()` and `useSlots<SlotNames>()` are factory hooks — call them once per setup run to get a typed\n`emit`/`slots` bound to the current host. `useSlots()` is safe to call more than once within that setup run.\n\n### ComponentDefinition\n\n```ts\ntype ComponentDefinition<Props> = {\n formAssociated?: boolean;\n props?: PropsDef<Props>;\n setup: (props: InferProps<PropsDef<Props>>) => HTMLResult | null;\n shadow?: Partial<ShadowRootInit> | false; // false = light DOM (no shadow root)\n styles?: (string | CSSStyleSheet | CSSResult)[];\n};\n```\n\n## Runtime Helpers\n\n`onMounted`, `onCleanup`, `onEvent`, `onElement`, and `watchEffect` are plain functions imported from `@vielzeug/ore`. Call them directly during `setup()`.\n\n```ts\nimport { html, onCleanup, onEvent, onMounted } from '@vielzeug/ore';\n\nsetup(props) {\n onMounted(() => {\n // DOM is ready; return a function for mount-scoped cleanup\n return () => { /* cleanup on unmount */ };\n });\n\n onCleanup(() => { /* called on disconnect */ });\n\n onEvent(window, 'keydown', (e) => { /* auto-removed on disconnect */ });\n\n return html`...`;\n}\n```\n\nBecause these resolve the active component through an implicit context (rather than a value threaded through parameters), composable helper functions can call them directly too — no need to pass hooks in as options:\n\n```ts\nimport { onCleanup } from '@vielzeug/ore';\n\nfunction useMyHelper() {\n onCleanup(() => { /* teardown */ });\n}\n\n// In setup:\nsetup(_props) {\n useMyHelper();\n return html`...`;\n}\n```\n\n## Props API\n\n| Helper | Signature | Notes |\n| ----------------------------------- | ------------------ | ------------------------------------------------------------------------------------------------------------ |\n| `prop.string(defaultValue?)` | `PropDef<string>` | Reflects by default |\n| `prop.bool(defaultValue?)` | `PropDef<boolean>` | Any non-null attribute value other than `\"false\"` parses as `true`; `\"false\"` or absent attribute is `false` |\n| `prop.number(defaultValue?)` | `PropDef<number>` | Returns default (not NaN) and warns in dev when attribute is not a valid number |\n| `prop.oneOf(allowed, defaultValue)` | `PropDef<T>` | Restricts to provided string union |\n| `prop.json(defaultValue)` | `PropDef<T>` | JSON.parse from attribute; `reflect: false` |\n| `prop.data<T>(defaultValue?)` | `PropDef<T>` | JS-only — never reads/writes an attribute; use for objects, arrays, callbacks, or any non-serialisable value |\n\n> **Choosing the right prop helper:**\n>\n> - **`prop.json`** — value can be declared in HTML (`<my-el config='{\"x\":1}'>`); attribute string is `JSON.parse`d.\n> - **`prop.data`** — value is always set from JavaScript (objects, arrays, callbacks, class instances); the attribute is never read. Use this for both data and function props.\n\nWhen you need custom parsing or `reflect: false`, use a raw `PropDef` object:\n\n```ts\nprops: {\n items: { default: [], parse: () => [], reflect: false },\n}\n```\n\nUse `prop.data` for props that hold JS-only values (including callbacks) that cannot be serialised through an HTML attribute:\n\n```ts\ndefine('data-grid', {\n props: {\n getRowKey: prop.data<(row: unknown) => string>(),\n columns: prop.data<DataGridColumn[]>([]),\n onSort: prop.data<(key: string) => void>(),\n },\n setup(props) {\n // Set from JS: grid.getRowKey = (row) => row.id\n return html`...`;\n },\n});\n```\n\n## Template and Directives\n\n### `html`\n\nTagged template literal that returns an `HTMLResult`. Supports text interpolation, ordinary attributes (`attr=`),\nboolean attributes (`?attr=`), events (`@event=`), refs (`ref=`), and nested templates.\n\n### `css`\n\nTagged template literal that returns a `CSSResult` for use in `styles`.\n\n### Directives\n\n| Directive | Purpose |\n| -------------------------------------- | ----------------------------------------------------------------------------------------------------- |\n| `each(source, key, render, fallback?)` | Keyed reactive list; render receives `Readable<T>` and `Readable<number>`; plain `T[]` is a one-time static snapshot |\n| `when(condition, truthy, falsy?)` | Conditional rendering |\n| `classMap(record)` | Reactive class string from object map |\n| `styleMap(record)` | Reactive inline style string from object map |\n| `live(signal)` | One-way binding that skips stale writes during active user input; use with `@input` handler |\n| `unsafeHtml(value)` | HTML rendering sink; sanitize untrusted values before calling |\n\n### `unsafeHtml`\n\n`unsafeHtml()` is an explicit HTML injection sink. It has no global sanitizer: sanitize untrusted\ncontent before passing it to the directive, so the trust boundary remains at the call site.\n\n```ts\nimport { unsafeHtml } from '@vielzeug/ore';\n\nconst safeArticle = sanitize(userSuppliedArticle);\n\nreturn html`<article>${unsafeHtml(safeArticle)}</article>`;\n```\n\n## Host Bindings\n\n`bind(config, options?)` is a plain function imported from `@vielzeug/ore`:\n\n```ts\nbind({\n attr: { role: 'button', 'aria-expanded': () => String(open.value) },\n class: { 'is-open': open },\n style: { '--height': () => height.value + 'px' },\n on: { click: handleClick },\n});\n```\n\n`bind()` auto-registers cleanup with the component scope — no manual `onCleanup` needed. Returns a cleanup function for early teardown.\n\n### Off-host bindings\n\nPass `{ target: el }` as a second argument to bind to any element other than the host:\n\n```ts\nbind(\n { attr: { 'aria-expanded': () => String(isOpen.value) } },\n { target: triggerEl },\n);\n```\n\nEvent listener options (`once`, `capture`, `passive`) are also accepted in the second argument. Cleanup is auto-registered with the component scope when called during setup.\n\n### Reactive ARIA attributes\n\nFor reactive ARIA attribute syncing, use `bind({ aria: config }, { target })`. Shorthand keys are normalised to `aria-*` automatically (`expanded` → `aria-expanded`; `role` is passed verbatim):\n\n```ts\n// Inside setup — cleanup auto-registered\nbind(\n {\n aria: {\n expanded: () => isOpen.value,\n controls: panelId,\n haspopup: 'listbox',\n },\n },\n { target: triggerEl },\n);\n\n// Manage cleanup manually — bind() always returns a cleanup fn\nconst stopAria = bind({ aria: { expanded: () => isOpen.value } }, { target: triggerEl });\n// Call stopAria() when the trigger is swapped out\n```\n\nStatic values (strings, numbers, booleans) are applied once. Getter functions and signals create reactive effects. Setting a value to `null`, `undefined`, or `false` removes the attribute.\n\n## Slots\n\n- `slots.has(name?)` — `Readable<boolean>` — whether the named (or default) slot has assigned content\n- `slots.elements(name?)` — `Readable<Element[]>` — the assigned elements for the slot\n\nSlot signals update reactively when assigned content changes, including when slots are inserted dynamically (via `when()` or `each()`) after mount.\n\n## Context API\n\n- `createContext<T>(description?)` — Create a typed injection key\n- `provide(key, value)` — Provide a value to descendants\n- `inject(key)` — Resolve from nearest ancestor; returns `undefined` if not found\n- `inject(key, fallback)` — Resolve with a fallback value\n- `injectStrict(key)` — Resolve or throw if absent\n\n`provide()` and `inject()` must be called synchronously during `setup()`. Calling them outside a setup context throws\n`'Lifecycle hooks must be called during component setup'`. Context resolution walks the ancestor chain including shadow\nDOM boundaries. `inject()` resolves and caches its result once per consumer — provide a `Readable` (signal/computed)\nrather than a raw value if descendants need to observe later changes; re-calling `provide()` with a new raw value\nafterward is not seen by consumers that already resolved it (a dev-mode warning fires when a key is provided twice on\nthe same element). `provide()` registers cleanup automatically — context keys are removed from the registry when the\nproviding component disconnects, so reconnecting the same element runs `setup()` fresh without spurious \"overwriting\"\nwarnings or stale keys leaking to descendants.\n\n## Utilities\n\n- `ref<T>()` — Create a `Signal<T | null>` element reference. Set to the element via `ref=` in templates.\n- `createId(prefix = 'id')` — Generate a unique incremental string ID (e.g. `'id-1'`, `'id-2'`). Each call returns a new ID — it does not deduplicate by prefix.\n- `createStableId(prefix = 'id')` — Generate a unique ID that also embeds a short random tag shared across all IDs generated in the session (e.g. `'field-a3k21'`), reducing collision risk when multiple app instances run on the same page. Like `createId()`, every call returns a new ID.\n- `resetStableIdCounter()` — Reset the `createStableId()` counter to 0. Call in test `beforeEach` for deterministic IDs. Scoped to `createStableId()` only — `createId()` has no public reset (it's for uniqueness, not cross-test determinism).\n\n## Form-Associated API\n\nImport from `@vielzeug/ore`.\n\n### `useField(options)`\n\nWire a form-associated element to `ElementInternals`. Requires `formAssociated: true` on the component definition. The `disabled` state tracking via `internals.states` (CustomStateSet) is skipped with a dev warning if the API is unavailable in the current environment.\n\n```ts\ntype FormFieldOptions<T> = {\n disabled?: Readable<boolean>;\n /** Defaults to the host element active during setup. */\n el?: HTMLElement;\n /**\n * When true, a null/undefined value is submitted as '' instead of null,\n * keeping the field's key present in FormData even when the value is absent.\n * Only applies to the default toFormValue; ignored if toFormValue is provided.\n * @default false\n */\n emptyStringForNull?: boolean;\n /** Called when the ancestor <form> resets (see onFormReset) — restore local field state here. */\n onReset?: () => void;\n toFormValue?: (value: T) => File | FormData | string | null;\n /** Recomputed reactively and passed straight to internals.setValidity(). null = always valid. */\n validationMessage?: Readable<string>;\n validity?: Readable<ValidityStateFlags | null>;\n value: Signal<T> | Readable<T>;\n};\n\ntype FormFieldHandle = {\n checkValidity(): boolean;\n readonly internals: ElementInternals;\n reportValidity(): boolean;\n /** Set (non-empty message) or clear (empty string) a custom validity error. */\n setCustomValidity(message: string): void;\n};\n```\n\nPass `validity`/`validationMessage` to make `required`-style constraints participate in native constraint validation\nthrough `checkValidity()` and `reportValidity()`:\n\n```ts\nconst isBlank = (v: string) => v.trim() === '';\n\nuseField({\n validationMessage: computed(() => (required.value && isBlank(value.value) ? 'This field is required.' : '')),\n validity: computed(() => (required.value && isBlank(value.value) ? { valueMissing: true } : null)),\n value,\n});\n```\n\n## Testing APIs\n\nImport from `@vielzeug/ore/testing`.\n\n| API | Purpose |\n| ------------------------ | ------------------------------------------------------------------------------------------ |\n| `mount(setup, options?)` | Mount a component and return a test fixture |\n| `cleanup()` | Remove all mounted elements and reset test state |\n| `install(afterEach, options?)` | Register auto-cleanup; pass `{ formInternals: true }` to also install the `ElementInternals`/`FormData`/`<form>.reset()` jsdom polyfill (see below) |\n| `installFormInternalsPolyfill()` | Installs the form-internals polyfill directly (returns an `uninstall()` that restores every patched global). Usually called via `install(afterEach, { formInternals: true })` |\n| `walkFlatTree(root, visit)` | Walks the flat tree (expanding `<slot>` via `assignedElements()`) — for finding slotted content across a shadow boundary that `querySelectorAll()` can't cross |\n| `flush(options?)` | Drain reactive updates and animation frames |\n| `debugFlush()` | Run `flush()` with `console.debug` diagnostics |\n| `mock(tag, template?)` | Register a no-op stub custom element |\n| `renderHook(setup)` | Run lifecycle hooks in isolation; overload accepts `propDefs` as first arg for typed props |\n| `resetOreForTests()` | Reset styles and ID counters when mounting is managed manually |\n| `OreTimeoutError` | Error thrown when `flush()` cannot settle tracked Ore work |\n\n> **Test isolation:** `cleanup()` removes mounted elements and resets all cross-test Ore state (the stylesheet cache and ID counters) via `resetOreForTests()`. Call it in `afterEach` (or use `install()`) to prevent state leaking between tests.\n\nImport `within`, named dispatchers such as `fireClick`, and waits such as `waitUntil` or `waitForEvent` from\n`@vielzeug/assay`.\n\n> **Form-associated component testing:** jsdom implements none of the `ElementInternals` form-association API — `install(afterEach, { formInternals: true })` polyfills `setFormValue`/`setValidity`/`checkValidity`/`reportValidity`/`validationMessage`/`validity`/`states`, mixes `checkValidity`/`reportValidity`/`validity`/`validationMessage` onto the host element itself (real browsers do this for any `formAssociated: true` element), makes `FormData` collect a form-associated element's set value, and makes `<form>.reset()` invoke `formResetCallback()`. Every patch is a guarded no-op when its target already exists, and `installFormInternalsPolyfill()` returns an `uninstall()` that restores every patched global. The polyfill is opt-in (`{ formInternals: true }`) because the patches are global — suites without form-associated components shouldn't carry them. A downstream package (e.g. a component library built on `ore`) should rely on this instead of hand-rolling its own copy.\n\n#### `Fixture` interface\n\n```ts\ninterface Fixture<T extends HTMLElement = HTMLElement> {\n [Symbol.dispose](): void; // Delegates to dispose() — enables `using` declarations\n element: T;\n readonly disposed: boolean; // true after dispose() has been called\n readonly shadow: ShadowRoot | null;\n get<E extends Element>(selector: string): E;\n query<E extends Element>(selector: string): E | null;\n queryAll<E extends Element>(selector: string): E[];\n getByText<E extends Element>(text: string, selector?: string): E;\n queryByText<E extends Element>(text: string, selector?: string): E | null;\n queryAllByText<E extends Element>(text: string, selector?: string): E[];\n getByTestId<E extends Element>(testId: string): E;\n queryByTestId<E extends Element>(testId: string): E | null;\n queryAllByTestId<E extends Element>(testId: string): E[];\n attr(name: string, value: string | number | boolean): Promise<void>;\n attrs(record: Record<string, string | number | boolean>): Promise<void>;\n flush(options?: FlushOptions): Promise<void>;\n act(fn: () => unknown): Promise<void>;\n dispose(): void; // Removes the component from the DOM — idempotent\n}\n```\n\n#### `renderHook`\n\nUseful for testing composable lifecycle hooks (`onMounted`, `watchEffect`, `inject`, etc.) without a template. `onMounted`/`onCleanup`/`watchEffect`/... work exactly as inside a real `setup()`, since they resolve the same implicit current-component context:\n\n```ts\n// Without props\nconst { result, flush, dispose } = await renderHook(() => {\n const count = signal(0);\n onMounted(() => {\n count.value = 1;\n });\n return count;\n});\nexpect(result.value).toBe(1);\n\n// With typed props (prop-defs overload)\nconst { result } = await renderHook({ label: prop.string('hello'), count: prop.number(0) }, (props) => props.label);\nexpect(result.value).toBe('hello');\n```\n\n## Ripple Primitives\n\nOre does **not** re-export reactive primitives. Import them directly from `@vielzeug/ripple`:\n\n```ts\nimport { batch, computed, signal, watch } from '@vielzeug/ripple';\n```\n\nSee the [Ripple documentation](/ripple/) for the full API.\n\n## Lifecycle Events\n\n| Event | When |\n| ------------------ | ------------------------------------------------------------- |\n| `ore:connect` | After every `connectedCallback` (including reconnects) |\n| `ore:disconnect` | After `disconnectedCallback`, before component state is reset |\n| `ore:error` | When a lifecycle callback fails — bubbles, composed; detail is `OreLifecycleError` |\n\n## Types\n\n```ts\ntype PropDef<T> = {\n readonly default: T;\n readonly parse: (value: string | null) => T;\n reflect?: boolean;\n};\n\ntype PropsDef<T extends Record<string, unknown>> = {\n [K in keyof Required<T>]: PropDef<T[K & keyof T]>;\n};\n\ntype PropInputDefs = Record<string, PropDef<unknown>>;\n\n/**\n * Infer reactive props type from a PropInputDefs map.\n * Each entry becomes Readable<T> keyed by prop name.\n */\ntype InferProps<D extends PropInputDefs> = {\n readonly [K in keyof D]-?: Readable<InferPropValue<D[K]>>;\n};\n\n// Runtime hooks — all plain functions imported from '@vielzeug/ore', not fields on an object.\ntype OnMountedCallback = () => Cleanup | undefined;\ntype OnFormResetCallback = () => void;\n\ndeclare function onMounted(fn: OnMountedCallback): void; // DOM-ready callback; runs after each connection's render\ndeclare function onCleanup(fn: Cleanup): void; // Register teardown; called on disconnect\ndeclare function onElement<T extends HTMLElement>(\n ref: Readable<T | null>,\n callback: (el: T) => Cleanup | undefined,\n): () => void;\ndeclare function onEvent<K extends keyof HTMLElementEventMap>(\n target: EventTarget | null | undefined,\n event: K,\n listener: (e: HTMLElementEventMap[K]) => void,\n options?: AddEventListenerOptions,\n): void;\ndeclare function onEvent(\n target: EventTarget | null | undefined,\n event: string,\n listener: EventListener,\n options?: AddEventListenerOptions,\n): void;\ndeclare function onFormReset(fn: OnFormResetCallback): void; // Runs on every ancestor <form> reset; formAssociated only\ndeclare function watchEffect(fn: () => Cleanup | undefined): () => void; // Scoped reactive effect; auto-cleaned on disconnect\ndeclare function bind(config: HostBindConfig, options?: BindOptions): () => void; // Bindings for host or any target element\ndeclare function provide<T>(key: InjectionKey<T>, value: T): void; // Register a context value on the host element\ndeclare function inject<T>(key: InjectionKey<T>): T | undefined;\ndeclare function inject<T>(key: InjectionKey<T>, fallback: T): T;\ndeclare function getHost(): HTMLElement; // The current component's host element\ndeclare function useEmit<Emits extends Record<string, unknown> = Record<string, never>>(): EmitFn<Emits>;\ndeclare function useSlots<SlotNames extends string = string>(): ComponentSlots<SlotNames>;\n\ntype ComponentDefinition<Props extends Record<string, unknown> = Record<never, never>> = {\n formAssociated?: boolean;\n props?: PropsDef<Props>;\n setup: (props: InferProps<PropsDef<Props>>) => HTMLResult | null;\n shadow?: Partial<ShadowRootInit> | false; // false = light DOM\n styles?: (string | CSSStyleSheet | CSSResult)[];\n};\n\ntype HostBindingValue =\n | (() => string | number | boolean | null | undefined)\n | Readable<string | number | boolean | null | undefined>\n | string\n | number\n | boolean\n | null\n | undefined;\n\ntype ReflectConfig = Record<string, HostBindingValue>;\n\ntype HostBindConfig = {\n aria?: ReflectConfig;\n attr?: ReflectConfig;\n class?: (() => Record<string, boolean>) | Record<string, Readable<boolean> | (() => boolean) | boolean>;\n on?: Record<string, ((event: Event) => void) | undefined>;\n style?: Record<string, HostBindingValue>;\n};\n\ntype BindOptions = AddEventListenerOptions & {\n target?: Element;\n};\n\ntype HostBindFn = (config: HostBindConfig, options?: BindOptions) => () => void;\n\ntype ComponentSlots<S extends string = string> = {\n elements(name?: S): Readable<Element[]>;\n has(name?: S): Readable<boolean>;\n};\n\ntype Ref<T extends Element> = Signal<T | null>;\n\ntype RefCallback<T extends Element> = (el: T | null) => void;\n\ntype InjectionKey<T> = symbol & { readonly __ore_injection_key?: T };\n\ninterface HTMLResult {\n mount(\n parent: ParentNode,\n anchor: Node | null,\n registerCleanup: (fn: () => void) => void,\n ): Node[];\n}\n\ntype CSSResult = {\n content: string;\n toString(): string;\n};\n\ntype LiveBinding<T> = { readonly source: Readable<T> };\n\ntype EmitFn<T extends Record<string, unknown>> = {\n <K extends KeysWithoutDetail<T>>(event: K): boolean;\n <K extends Exclude<keyof T, KeysWithoutDetail<T>>>(event: K, detail: T[K]): boolean;\n};\n// KeysWithoutDetail is an internal helper type, not exported.\n\ntype FormFieldOptions<T = unknown> = {\n disabled?: Readable<boolean>;\n el?: HTMLElement;\n emptyStringForNull?: boolean;\n onReset?: () => void;\n toFormValue?: (value: T) => File | FormData | string | null;\n validationMessage?: Readable<string>;\n validity?: Readable<ValidityStateFlags | null>;\n value: Signal<T> | Readable<T>;\n};\n\ntype FormFieldHandle = {\n checkValidity: () => boolean;\n readonly internals: ElementInternals;\n reportValidity: () => boolean;\n setCustomValidity: (message: string) => void;\n};\n\ntype MutationObserverValue = {\n entries: MutationRecord[];\n latest: MutationRecord | null;\n};\n\n/** Phase in which a OreError occurred. */\ntype OreErrorPhase = 'each-reconcile' | 'form-reset' | 'mounted' | 'setup';\n```\n\n## Errors\n\n`OreError` is the base class for every Ore error class — `err instanceof OreError` catches all of them.\n`OreError.is(err)` is the equivalent static type-guard.\n\n- **`OreApiError`** — thrown when the `ore` API itself is misused: calling `define()` with a duplicate tag, calling a lifecycle hook (`inject`, `onMounted`, `onCleanup`, `onEvent`, …) outside of `setup()`, or passing an invalid prop definition to `define()`.\n- **`OreInternalError`** — thrown when an Ore invariant fails, indicating a package bug rather than invalid application code.\n- **`OreLifecycleError`** — reported in the `ore:error` event when component `setup()`, a mounted callback, a form-reset callback, or `each()` reconciliation fails. Extends `OreError` with:\n - `component: string` — the element's local name\n - `phase: OreErrorPhase` — `'setup'` | `'mounted'` | `'form-reset'` | `'each-reconcile'`\n - `cause: Error` — the original error thrown by `setup()`\n- **`OreTimeoutError`** — thrown by `flush()` (from `@vielzeug/ore/testing`) when pending Ore work does not settle before its timeout.\n\nLifecycle failures dispatch a bubbling, composed `ore:error` event whose `detail` is the `OreLifecycleError`. Setup\nfailures still rethrow their original error; mounted and form-reset callback failures are reported through the same\nevent so their remaining callbacks can continue.\n",
|
|
6
|
+
"usage": "---\ntitle: Ore — Usage Guide\ndescription: Practical Ore usage patterns for components, props, templates, slots, context, forms, Sentinel integration, and tests.\n---\n\n[[toc]]\n\n## Basic Usage\n\n`define(tag, definition)` registers a custom element.\n\nYour `setup()` function receives typed prop signals and returns an `HTMLResult` directly. Its state belongs to the\ncurrent connection: disconnect disposes it, and reconnecting the same element runs setup again.\n\n```ts\nimport { signal } from '@vielzeug/ripple';\nimport { define, html } from '@vielzeug/ore';\n\ndefine('status-chip', {\n setup() {\n const online = signal(true);\n\n return html`\n <button @click=${() => (online.value = !online.value)}>${() => (online.value ? 'Online' : 'Offline')}</button>\n `;\n },\n});\n```\n\nEverything besides `props` — lifecycle hooks, host bindings, context, slots, emit — is a plain function imported from `@vielzeug/ore`, called directly from `setup()` (or a composable it calls):\n\n```ts\nimport { define, getHost, html, bind, useEmit, useSlots } from '@vielzeug/ore';\n\ndefine('my-widget', {\n setup(_props) {\n const el = getHost(); // the host HTMLElement\n const emit = useEmit<{ close: undefined }>(); // typed event emitter\n const slots = useSlots<'header'>(); // reactive slot observation\n\n bind({ attr: { role: 'group' } }); // host binding helper (attr, class, style, on)\n\n return html`<slot></slot>`;\n },\n});\n```\n\n## signals and effects\n\nOre does not re-export ripple primitives — import them directly from `@vielzeug/ripple`.\n\n```ts\nimport { batch, computed, effect, signal, watch } from '@vielzeug/ripple';\n\nconst count = signal(0);\nconst doubled = computed(() => count.value * 2);\n\neffect(() => {\n console.log('doubled =', doubled.value);\n});\n\nwatch(count, (next, prev) => {\n console.log('count changed', prev, '->', next);\n});\n\nbatch(() => {\n count.value = 1;\n count.value = 2;\n});\n```\n\n## onMounted and lifecycle\n\nUse `onMounted()` for DOM-dependent initialization that must run after the template is mounted. Use `onElement(ref, cb)` for work tied to a specific DOM node. `onEvent()` attaches a listener that is automatically removed on disconnect.\n\n```ts\nimport { signal } from '@vielzeug/ripple';\nimport { define, html, onElement, onEvent, onMounted, ref, useSlots } from '@vielzeug/ore';\n\ndefine('deferred-init', {\n setup(_props) {\n const tabIndex = signal(0);\n const inputRef = ref<HTMLInputElement>();\n const slots = useSlots<'items'>();\n\n onMounted(() => {\n const items = slots.elements('items').value;\n console.log('Found', items.length, 'items');\n });\n\n onElement(inputRef, (input) => {\n input.focus();\n });\n\n onEvent(window, 'keydown', (e: KeyboardEvent) => {\n if (e.key === 'Escape') tabIndex.value = 0;\n });\n\n return html`<div><slot name=\"items\"></slot><input ref=${inputRef} /></div>`;\n },\n});\n```\n\n## prop definitions\n\nUse `prop.*` helpers for common cases, or raw `PropDef` objects for custom parsing or `reflect: false`.\n\n```ts\nimport { define, html, prop } from '@vielzeug/ore';\n\ndefine('x-button', {\n props: {\n label: prop.string('Button'),\n disabled: prop.bool(false),\n variant: prop.oneOf(['primary', 'secondary'] as const, 'primary'),\n count: prop.number(0),\n },\n setup(props) {\n return html`\n <button ?disabled=${props.disabled} data-variant=${props.variant}>${props.label} (${props.count})</button>\n `;\n },\n});\n```\n\n## template bindings\n\n`html` supports text, attributes, booleans, properties, events, refs, and nested templates.\n\n```ts\nimport { computed, signal } from '@vielzeug/ripple';\nimport { define, html, ref } from '@vielzeug/ore';\n\ndefine('profile-name', {\n setup() {\n const name = signal('Alice');\n const inputRef = ref<HTMLInputElement>();\n\n return html`\n <label title=${computed(() => 'Current: ' + name.value)}>Name</label>\n <input\n ref=${inputRef}\n value=${name}\n aria-label=${() => 'Current name ' + name.value}\n @input=${(event: Event) => {\n name.value = (event.target as HTMLInputElement).value;\n }} />\n <p>Hello ${name}</p>\n `;\n },\n});\n```\n\n## directives\n\nOre exports `each`, `classMap`, `styleMap`, `when`, `live`, and `unsafeHtml` from `@vielzeug/ore`. Use ordinary\nattribute bindings plus native event handlers for two-way input state; no special model directive is required.\n\n```ts\nimport { signal } from '@vielzeug/ripple';\nimport { classMap, define, each, html, styleMap, when } from '@vielzeug/ore';\n\ndefine('task-list', {\n setup() {\n const tasks = signal([{ id: 1, text: 'Write tests' }]);\n const active = signal(true);\n\n return html`\n <ul\n class=\"${classMap({ ready: () => tasks.value.length > 0 })}\"\n style=${styleMap({ opacity: () => (active.value ? 1 : 0.5) })}>\n ${when(\n () => active.value,\n () => html`<li>Active</li>`,\n () => html`<li>Paused</li>`,\n )}\n ${each(\n tasks,\n (task) => task.id,\n (task) => html`<li>${() => task.value.text}</li>`,\n )}\n </ul>\n `;\n },\n});\n```\n\n### each() API\n\n`each(source, key, render, fallback?)` takes positional arguments:\n\n- **source** — signal, getter, or plain array\n- **key** — function returning a unique key per item\n- **render** — receives reactive `item` and `index` signals\n- **fallback** — optional, rendered when the list is empty\n\n```ts\neach(\n items,\n (item) => item.id,\n (item, index) => html`<li>#${index}: ${() => item.value.label}</li>`,\n () => html`<li>No items</li>`,\n);\n```\n\n## live form bindings\n\nUse `live(signal)` for inputs that should preserve in-progress user edits instead of overwriting the DOM on stale writes.\n\n```ts\nimport { signal } from '@vielzeug/ripple';\nimport { define, html, live } from '@vielzeug/ore';\n\ndefine('live-search', {\n setup() {\n const query = signal('');\n\n return html`\n <input value=${live(query)} @input=${(e: Event) => (query.value = (e.target as HTMLInputElement).value)} />\n `;\n },\n});\n```\n\n## host bindings\n\n`bind()` wires reactive attrs, classes, styles, and events to the host element.\n\n```ts\nimport { signal } from '@vielzeug/ripple';\nimport { bind, define, html } from '@vielzeug/ore';\n\ndefine('x-toggle', {\n setup(_props) {\n const open = signal(false);\n\n bind({\n attr: { 'aria-expanded': () => String(open.value), role: 'button', tabindex: 0 },\n class: { 'is-open': open },\n on: { click: () => (open.value = !open.value) },\n });\n\n return html`<slot></slot>`;\n },\n});\n```\n\nThe `bind` config supports `attr`, `class`, `style`, and `on` sections.\n\n## ARIA bindings\n\nUse `bind({ aria: config }, { target })` to reactively sync ARIA attributes to any element. Shorthand keys are normalised to `aria-*` automatically — `expanded` becomes `aria-expanded`, `role` is set verbatim.\n\n```ts\nimport { signal } from '@vielzeug/ripple';\nimport { bind, define, html, onMounted } from '@vielzeug/ore';\n\ndefine('x-disclosure', {\n setup(_props) {\n const open = signal(false);\n const panelId = 'disclosure-panel';\n\n bind({\n attr: { role: 'button', tabindex: 0 },\n on: { click: () => (open.value = !open.value) },\n });\n\n onMounted(() => {\n const trigger = document.querySelector('#trigger') as HTMLElement;\n if (trigger) {\n // bind() registers cleanup automatically when called inside setup\n bind(\n {\n aria: {\n controls: panelId,\n expanded: () => String(open.value),\n haspopup: 'region',\n },\n },\n { target: trigger },\n );\n }\n });\n\n return html`<slot></slot>`;\n },\n});\n```\n\nStatic values are applied once. Getter functions create reactive effects. Setting a value to `null`, `undefined`, or `false` removes the attribute.\n\n`bind()` always returns a cleanup function. Use it to stop syncing early when a trigger element can be swapped out:\n\n```ts\nonMounted(() => {\n const trigger = document.querySelector('#trigger') as HTMLElement;\n const stopAria = bind({ aria: { expanded: () => String(open.value) } }, { target: trigger });\n\n // Stop syncing when the trigger is replaced\n onCleanup(stopAria);\n});\n```\n\n### Binding a non-host element with `bind()`\n\nPass `{ target: el }` as a second argument to bind attributes, classes, styles, or events to any element:\n\n```ts\nimport { signal } from '@vielzeug/ripple';\nimport { bind, define, html, onMounted, ref } from '@vielzeug/ore';\n\ndefine('button-wrapper', {\n setup(_props) {\n const visible = signal(false);\n const btnRef = ref<HTMLButtonElement>();\n\n onMounted(() => {\n const btn = btnRef.value;\n if (!btn) return;\n\n bind(\n {\n attr: { 'aria-pressed': () => String(visible.value) },\n on: { click: () => (visible.value = !visible.value) },\n },\n { target: btn },\n );\n });\n\n return html`<button ref=${btnRef}>Toggle</button>`;\n },\n});\n```\n\n## slots and emits\n\n```ts\nimport { define, html, useEmit, useSlots, when } from '@vielzeug/ore';\n\ndefine('card-with-footer', {\n setup(_props) {\n const slots = useSlots<'header' | 'footer'>();\n const emit = useEmit<{ action: undefined }>();\n\n return html`\n <div class=\"card\">\n <slot name=\"header\"></slot>\n <slot></slot>\n ${when(slots.has('footer'), () => html`<footer><slot name=\"footer\"></slot></footer>`)}\n </div>\n <button @click=${() => emit('action')}>Go</button>\n `;\n },\n});\n```\n\nPass a `SlotNames` type parameter to `useSlots<SlotNames>()` to get typed `slots.has()` and `slots.elements()` calls.\n\n## context provide/inject\n\n```ts\nimport { signal } from '@vielzeug/ripple';\nimport { createContext, define, html, injectStrict, provide } from '@vielzeug/ore';\n\nconst COUNT_CTX = createContext<ReturnType<typeof signal<number>>>('count');\n\ndefine('count-provider', {\n setup(_props) {\n const count = signal(0);\n provide(COUNT_CTX, count);\n\n return html`<button @click=${() => count.value++}><slot></slot></button>`;\n },\n});\n\ndefine('count-consumer', {\n setup() {\n const count = injectStrict(COUNT_CTX);\n\n return html`<p>Count: ${count}</p>`;\n },\n});\n```\n\n`provide()` registers cleanup automatically — context keys are removed from the registry when the providing component disconnects. On reconnect, `setup()` runs fresh and `provide()` re-registers without spurious \"overwriting\" warnings. Provide a `Readable` (signal/computed) rather than a raw value if descendants need to observe later changes — `inject()` resolves and caches the value once per consumer connection.\n\n## form-associated elements\n\n```ts\nimport { signal } from '@vielzeug/ripple';\nimport { define, html, prop } from '@vielzeug/ore';\nimport { useField } from '@vielzeug/ore';\n\ndefine('rating-input', {\n formAssociated: true,\n setup() {\n const value = signal(0);\n const field = useField({ value });\n\n return html`\n <button @click=${() => (value.value = 1)}>1</button>\n <button @click=${() => (value.value = 2)}>2</button>\n <button @click=${() => (value.value = 3)}>3</button>\n <button @click=${() => field.reportValidity()}>Validate</button>\n <p>Current: ${value}</p>\n `;\n },\n});\n```\n\n## Sentinel Observers\n\nUse `@vielzeug/sentinel` for reactive browser and DOM observations. Create element-dependent Sentinels inside `onMounted()` and dispose them with the component.\n\n```ts\nimport { define, html, onCleanup, onMounted, ref, watchEffect } from '@vielzeug/ore';\nimport { createElementSize, SentinelUnavailableError } from '@vielzeug/sentinel';\n\ndefine('x-observed', {\n setup(_props) {\n const boxRef = ref<HTMLDivElement>();\n\n onMounted(() => {\n const element = boxRef.value;\n if (!element) return;\n\n try {\n const size = createElementSize(element);\n\n watchEffect(() => {\n console.log(size.value?.width);\n });\n\n onCleanup(() => size.dispose());\n } catch (error) {\n if (!(error instanceof SentinelUnavailableError)) throw error;\n }\n });\n\n return html`<div ref=${boxRef}>Observe me</div>`;\n },\n});\n```\n\n## testing utilities\n\nImport from `@vielzeug/ore/testing`.\n\n```ts\nimport { afterEach, describe, expect, it } from 'vitest';\nimport { signal } from '@vielzeug/ripple';\nimport { fireClick } from '@vielzeug/assay';\nimport { html } from '@vielzeug/ore';\nimport { cleanup, mount } from '@vielzeug/ore/testing';\n\ndescribe('my-counter', () => {\n afterEach(cleanup);\n\n it('increments on click', async () => {\n let count!: ReturnType<typeof signal<number>>;\n const { query, act } = await mount(() => {\n count = signal(0);\n return html`<button @click=${() => count.value++}>${count}</button>`;\n });\n\n expect(query('button')?.textContent).toBe('0');\n\n await act(() => fireClick(query('button')!));\n\n expect(query('button')?.textContent).toBe('1');\n });\n});\n```\n\n## Framework Integration\n\nOre components are standard custom elements and work natively in any framework.\n\n::: code-group\n\n```tsx [React]\n// React 19+ supports custom elements natively.\nimport './x-toggle'; // wherever define('x-toggle', { ... }) is called\n\nfunction App() {\n return <x-toggle aria-label=\"Open menu\" />;\n}\n```\n\n```ts [Vue 3]\n<script setup lang=\"ts\">\nimport './x-toggle'; // wherever define('x-toggle', { ... }) is called\nimport { ref } from 'vue';\n\nconst open = ref(false);\n</script>\n\n<template>\n <x-toggle :aria-label=\"'Open menu'\" @click=\"open = !open\" />\n</template>\n```\n\n```svelte [Svelte]\n<script>\n import './x-toggle'; // wherever define('x-toggle', { ... }) is called\n\n function handleClick() {\n console.log('toggled');\n }\n</script>\n\n<x-toggle aria-label=\"Open menu\" on:click={handleClick} />\n```\n\n:::\n\n## Working with Other Vielzeug Libraries\n\n### With Ripple\n\nImport ripple primitives directly from `@vielzeug/ripple` for standalone reactive state outside components.\n\n```ts\nimport { signal, computed } from '@vielzeug/ripple';\nimport { define, html } from '@vielzeug/ore';\n\n// Shared state created outside any component\nconst theme = signal<'light' | 'dark'>('light');\nconst isDark = computed(() => theme.value === 'dark');\n\ndefine('theme-toggle', {\n setup() {\n return html`\n <button @click=${() => (theme.value = isDark.value ? 'light' : 'dark')}>\n ${() =>\n isDark.value ? '<ore-icon name=\"sun\" size=\"16\"></ore-icon>' : '<ore-icon name=\"moon\" size=\"16\"></ore-icon>'}\n </button>\n `;\n },\n});\n```\n\n### With Forge\n\nUse `@vielzeug/forge` for typed form state. `useField()` remains intentionally narrow: it connects a form-associated\ncustom element to native `ElementInternals` without imposing submission, validation, or dirty-state policy.\n\n```ts\nimport { createForm } from '@vielzeug/forge';\nimport { define, html } from '@vielzeug/ore';\n\ndefine('signup-form', {\n setup(_props) {\n const form = createForm({ initialValues: { email: '' } });\n\n return html`\n <form\n @submit=${(event: SubmitEvent) => {\n event.preventDefault();\n void form.submit(async (values) => {\n console.log(values);\n });\n }}>\n <slot></slot>\n </form>\n `;\n },\n});\n```\n\n## Best Practices\n\n- Setup returns `html\\`...\\`` directly — not a function wrapping the template.\n- Use `watchEffect()` for reactive subscriptions tied to component lifetime — it auto-registers cleanup on disconnect.\n- Use `onElement(ref, cb)` instead of `onMounted` when the work is tied to a single DOM node.\n- Bind host attributes and classes via `bind()` rather than mutating the element directly.\n- Provide context at the nearest ancestor — avoid global context singletons.\n- Call `onCleanup()` for every resource allocated in `setup()` (WebSockets, intervals, external subscriptions).\n- Use `live(signal)` for form inputs to prevent clobbering user-in-progress edits.\n- Extract composable helper functions freely — `onMounted`/`onCleanup`/`bind`/... resolve the active component through implicit context, so they work from any function called (transitively) during `setup()`, with no need to pass them in as parameters.\n- Test component mounting and lifecycle with `@vielzeug/ore/testing`; import generic DOM events, queries, and waits\n from `@vielzeug/assay`.\n",
|
|
7
7
|
"examples": "---\ntitle: Ore — Examples\ndescription: Practical examples and recipes for ore.\n---\n\n## Examples\n\n- [Counter Component](./examples/counter-component.md)\n- [Typed Props And Emits](./examples/typed-props-and-emits.md)\n- [Observers In onMounted()](./examples/observers-in-onmount.md)\n- [Search List With Directives](./examples/search-list-with-directives.md)\n- [Context Provider And Consumer](./examples/context-provider-and-consumer.md)\n- [Prop Helpers And Raw PropDef](./examples/propsof-builder-api.md)\n- [Form Associated Rating Input](./examples/form-associated-rating-input.md)\n- [Test Example With @vielzeug/ore/testing](./examples/test-example-at-vielzeug-ore-testing.md)\n"
|
|
8
8
|
},
|
|
9
9
|
"examples": [],
|
|
@@ -37,11 +37,6 @@
|
|
|
37
37
|
"HostBindFn": "export {\n type BindOptions,\n bind,\n type HostBindConfig,\n type HostBindFn,\n type HostBindingValue,\n type ReflectConfig,\n} from './host-bind';",
|
|
38
38
|
"HostBindingValue": "export {\n type BindOptions,\n bind,\n type HostBindConfig,\n type HostBindFn,\n type HostBindingValue,\n type ReflectConfig,\n} from './host-bind';",
|
|
39
39
|
"ReflectConfig": "export {\n type BindOptions,\n bind,\n type HostBindConfig,\n type HostBindFn,\n type HostBindingValue,\n type ReflectConfig,\n} from './host-bind';",
|
|
40
|
-
"intersectionObserver": "export { intersectionObserver } from './observers/intersection-observe';",
|
|
41
|
-
"mediaObserver": "export { mediaObserver } from './observers/media-observe';",
|
|
42
|
-
"MutationObserverValue": "export { type MutationObserverValue, mutationObserver } from './observers/mutation-observe';",
|
|
43
|
-
"mutationObserver": "export { type MutationObserverValue, mutationObserver } from './observers/mutation-observe';",
|
|
44
|
-
"resizeObserver": "export { resizeObserver } from './observers/resize-observe';",
|
|
45
40
|
"InferProps": "export type { InferProps, PropDef, PropInputDefs, PropsDef } from './props';",
|
|
46
41
|
"PropDef": "export type { InferProps, PropDef, PropInputDefs, PropsDef } from './props';",
|
|
47
42
|
"PropInputDefs": "export type { InferProps, PropDef, PropInputDefs, PropsDef } from './props';",
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"docs": {
|
|
4
4
|
"index": "---\ntitle: Ripple — Reactive graphs\ndescription: Framework-agnostic signals, derived values, effects, scopes, watchers, and async resources.\npackage: ripple\ncategory: state\nkeywords: [reactive, signals, computed, effects, graph, scope, batch, watch, resource, async]\nrelated: [ore, clockwork, ledger]\nexports: [createRipple, signal, computed, effect, batch, createScope, untrack, watch, resource, isReactive]\nenvironments: [browser, node, ssr, deno]\n---\n\n<!-- markdownlint-disable MD025 MD033 MD060 -->\n\n<PackageHero package=\"ripple\" />\n\n## Why Ripple?\n\nHand-rolled reactive state spreads subscription, cleanup, and derived-value rules across application code. Ripple gives you one graph boundary with explicit disposal and fine-grained dependencies while keeping rendering and routing outside the runtime.\n\n```ts\n// Before\nlet count = 0;\nconst listeners = new Set<() => void>();\n\nfunction setCount(next: number) {\n count = next;\n for (const listener of listeners) listener();\n}\n\n// After\nimport { createRipple } from '@vielzeug/ripple';\n\nconst ripple = createRipple();\nconst count = ripple.signal(0);\nconst doubled = ripple.computed(() => count.value * 2);\nconst stop = ripple.effect(() => console.log(doubled.value));\n\ncount.value = 1;\nstop.dispose();\nripple.dispose();\n```\n\n| Feature | Ripple | Zustand | Jotai |\n| --- | --- | --- | --- |\n| Bundle size | <PackageInfo package=\"ripple\" type=\"size\" /> | ~3.5 kB | ~7 kB |\n| Zero dependencies | <ore-icon name=\"check\" size=\"16\"></ore-icon> | <ore-icon name=\"x\" size=\"16\"></ore-icon> | <ore-icon name=\"x\" size=\"16\"></ore-icon> |\n| Framework-agnostic | <ore-icon name=\"check\" size=\"16\"></ore-icon> | <ore-icon name=\"check\" size=\"16\"></ore-icon> | React-first |\n| Explicit graph lifetime | <ore-icon name=\"check\" size=\"16\"></ore-icon> | <ore-icon name=\"x\" size=\"16\"></ore-icon> | <ore-icon name=\"x\" size=\"16\"></ore-icon> |\n| Fine-grained derived values | <ore-icon name=\"check\" size=\"16\"></ore-icon> | Selectors | Atoms |\n\n<div class=\"decision-callout\">\n\n**Use Ripple when** you need framework-independent state with explicit graph lifetime and small composable primitives.\n\n**Consider a framework store when** component bindings, server cache, or framework-specific tooling matter more than portable reactive state.\n\n</div>\n\n## Installation\n\n::: code-group\n\n```sh [pnpm]\npnpm add @vielzeug/ripple\n```\n\n```sh [npm]\nnpm install @vielzeug/ripple\n```\n\n```sh [yarn]\nyarn add @vielzeug/ripple\n```\n\n:::\n\n## Quick Start\n\nCreate one graph, derive a value, observe it, then dispose resources when the graph lifetime ends.\n\n```ts\nimport { createRipple } from '@vielzeug/ripple';\n\nconst ripple = createRipple();\nconst count = ripple.signal(0);\nconst doubled = ripple.computed(() => count.value * 2);\nconst stop = ripple.effect(() => console.log(doubled.value));\n\nripple.batch(() => {\n count.value = 1;\n count.value = 2;\n});\n\nstop.dispose();\nripple.dispose();\n```\n\n## Features\n\n<div class=\"features-grid\">\n\n- `createRipple()` creates an isolated graph and lifetime boundary.\n- `signal()` stores writable values with configurable equality.\n- `computed()` derives lazy read-only values.\n- `effect()` reacts to dependency changes with cleanup support.\n- `batch()` coalesces synchronous writes and notifications.\n- `createScope()` groups owned reactive work.\n- `watch()` observes one selected source transition.\n- `resource()` loads async values with stale-work cancellation.\n\n</div>\n\n## Documentation\n\n<div class=\"doc-links\">\n\n- [Usage Guide](./usage.md)\n- [API Reference](./api.md)\n- [Examples](./examples.md)\n- [Migration Guide](./migration.md)\n\n</div>\n\n## See Also\n\n<div class=\"see-also\">\n\n- [Ore](/ore/) — uses Ripple signals and effects for web-component reactivity.\n- [Clockwork](/clockwork/) — exposes machine state through reactive Ripple values.\n- [Ledger](/ledger/) — adds command-based undo and redo beside Ripple state.\n\n</div>\n\n<!-- markdownlint-enable -->\n",
|
|
5
5
|
"api": "---\ntitle: Ripple — API Reference\ndescription: Complete reference for reactive graphs, signals, effects, scopes, watchers, and resources.\n---\n\n[[toc]]\n\n## API Overview\n\n| Symbol | Purpose | Execution mode | Common gotcha |\n| --- | --- | --- | --- |\n| `createRipple()` | Create isolated graph | Sync | Disposal is terminal; create a new graph instead of reusing it |\n| `signal()` | Create writable value | Sync | Default graph is process-wide |\n| `computed()` | Create lazy derived value | Sync | Keep derivation pure |\n| `effect()` | React to dependency reads | Sync | Dispose handle or return cleanup |\n| `batch()` | Coalesce synchronous writes | Sync | Does not roll back writes |\n| `createScope()` | Group owned reactive work | Sync | Call `run()` to activate it |\n| `untrack()` | Read without tracking | Sync | Read still happens immediately |\n| `watch()` | Observe selected output | Sync | Use `effect()` for broad reads |\n| `resource()` | Load async source | Async | Read dependencies in source callback |\n| `isReactive()` | Test `Readable` identity | Sync | Does not test arbitrary objects |\n\n## Package Entry Point\n\n| Import | Purpose |\n| --- | --- |\n| `@vielzeug/ripple` | All primitives, types, and errors — signals, computed, effects, scopes, watch, resource, and the isolated graph factory |\n\n## Graph Creation\n\n### `createRipple(options?)`\n\n```ts\nfunction createRipple(options?: RippleOptions): Ripple;\n```\n\nCreates one isolated reactive graph. Factories on the returned object share scheduling, ownership, observer, and error boundaries. `dispose()` is terminal: `ripple.disposed` becomes `true`, existing owned work is disposed, and creating more graph work throws `RippleDisposedRuntimeError`. Create a new graph for a new lifetime.\n\n| Parameter | Type | Description |\n| --- | --- | --- |\n| `options.onError` | `(error, context) => void` | Receives effect, cleanup, listener, or observer failures. |\n| `options.observer` | `ReactiveObserver` | Receives graph events. |\n\n**Returns:** `Ripple`.\n\n**Example:**\n\n```ts\nimport { createRipple } from '@vielzeug/ripple';\n\nconst ripple = createRipple();\nconst count = ripple.signal(0);\nconst stop = ripple.effect(() => console.log(count.value));\n\nstop.dispose();\nripple.dispose();\n```\n\n---\n\n### `isReactive(value)`\n\n```ts\nfunction isReactive<T>(value: T | Readable<T>): value is Readable<T>;\n```\n\nTests whether a value is a Ripple-created readable node, including `Resource`. Recognition works across duplicated Ripple module graphs.\n\n**Returns:** `true` for a Ripple `Signal`, computed value, or `Resource`; otherwise `false`.\n\n**Example:**\n\n```ts\nimport { isReactive, signal } from '@vielzeug/ripple';\n\nconsole.log(isReactive(signal(0)));\n```\n\n## Default Graph Functions\n\n### `signal(initial, options?)`\n\n```ts\nfunction signal<T>(initial: T, options?: SignalOptions<T>): Signal<T>;\n```\n\nCreates writable state on the default graph. Use `update()` for immutable replacement patterns.\n\n**Returns:** `Signal<T>`.\n\n**Example:**\n\n```ts\nimport { signal } from '@vielzeug/ripple';\n\nconst count = signal(0);\ncount.value += 1;\n\nconst cart = signal({ items: 0 });\ncart.update((state) => ({ ...state, items: state.items + 1 }));\n```\n\n---\n\n### `computed(derive, options?)`\n\n```ts\nfunction computed<T>(derive: () => T, options?: ComputedOptions<T>): Readable<T>;\n```\n\nCreates a lazy read-only value from reactive reads in `derive`.\n\n**Returns:** `Readable<T>`.\n\n**Example:**\n\n```ts\nimport { computed, signal } from '@vielzeug/ripple';\n\nconst count = signal(2);\nconst doubled = computed(() => count.value * 2);\nconsole.log(doubled.value);\n```\n\n---\n\n### `effect(callback, options?)`\n\n```ts\nfunction effect(callback: () => Cleanup | undefined, options?: EffectOptions): EffectHandle;\n```\n\nRuns immediately and reruns when its tracked reads change. A returned cleanup runs before the next callback or disposal.\n\n**Returns:** `EffectHandle`.\n\n**Example:**\n\n```ts\nimport { effect, signal } from '@vielzeug/ripple';\n\nconst connected = signal(false);\nconst stop = effect(() => {\n if (!connected.value) return;\n\n return () => console.log('disconnect');\n});\n\nstop.dispose();\n```\n\n---\n\n### `batch(fn)` and `untrack(fn)`\n\n```ts\nfunction batch<T>(fn: () => T): T;\nfunction untrack<T>(fn: () => T): T;\n```\n\n`batch()` defers effects and listeners until its callback returns. `untrack()` reads current state without adding dependencies to an enclosing effect.\n\n**Returns:** the callback result.\n\n**Example:**\n\n```ts\nimport { batch, signal, untrack } from '@vielzeug/ripple';\n\nconst first = signal('Ada');\nconst last = signal('Lovelace');\nconst locale = signal('en-US');\n\nbatch(() => {\n first.value = 'Grace';\n last.value = 'Hopper';\n});\n\nconsole.log(untrack(() => locale.value));\n```\n\n---\n\n### `createScope(name?)`\n\n```ts\nfunction createScope(name?: string): Scope;\n```\n\nCreates a disposable ownership boundary. Work created inside `scope.run()` belongs to that scope.\n\n**Returns:** `Scope`.\n\n**Example:**\n\n```ts\nimport { createScope, effect, signal } from '@vielzeug/ripple';\n\nconst scope = createScope('panel');\nconst count = signal(0);\n\nscope.run(() => effect(() => console.log(count.value)));\nscope.dispose();\n```\n\n## Watch and Resources\n\n### `watch(source, callback, options?)`\n\n```ts\nfunction watch<T>(\n source: Readable<T> | (() => T),\n callback: (value: T, previous: T | undefined) => void,\n options?: WatchOptions<T>,\n): EffectHandle;\n```\n\nObserves selected output changes using the default graph or a `Ripple.watch()` method.\n\n**Returns:** `EffectHandle`.\n\n**Example:**\n\n```ts\nimport { signal, watch } from '@vielzeug/ripple';\n\nconst count = signal(0);\nconst stop = watch(count, (value, previous) => console.log(previous, value), { immediate: true });\nstop.dispose();\n```\n\n---\n\n### `resource(source, loader, options?)`\n\n```ts\nfunction resource<Source, Value>(\n source: () => Source,\n loader: (source: Source, context: { readonly signal: AbortSignal }) => Promise<Value>,\n options?: ResourceOptions,\n): Resource<Value>;\n```\n\nTracks `source`, aborts stale loader work, and exposes `AsyncState<Value>`. Source and loader failures become `status: 'error'` state; handle them from `resource.value` rather than `RippleOptions.onError`, which is reserved for runtime callback, cleanup, listener, and observer failures.\n\n**Returns:** `Resource<Value>`.\n\n**Example:**\n\n```ts\nimport { resource, signal } from '@vielzeug/ripple';\n\nconst userId = signal('42');\nconst user = resource(() => userId.value, async (id) => ({ id }));\n\nif (user.value.status === 'error') console.error(user.value.error);\nuser.dispose();\n```\n\n## Types\n\n```ts\ntype Cleanup = () => void;\ntype Equality<T> = (previous: T, next: T) => boolean;\ntype Unsubscribe = () => void;\n\ntype SignalOptions<T> = { equals?: Equality<T>; name?: string };\ntype ComputedOptions<T> = { equals?: Equality<T>; name?: string };\ntype EffectOptions = { name?: string; scheduler?: 'microtask' | 'sync' };\ntype WatchOptions<T> = { equals?: Equality<T>; immediate?: boolean; name?: string; once?: boolean };\ntype ResourceOptions = { name?: string };\n\ntype ReactiveEvent =\n | { readonly kind: 'compute'; readonly name?: string }\n | { readonly kind: 'effect'; readonly name?: string }\n | { readonly kind: 'write'; readonly name?: string; readonly next: unknown; readonly previous: unknown }\n | { readonly kind: 'dispose'; readonly name?: string; readonly node: 'effect' | 'scope' };\n\ntype ReactiveObserver = (event: ReactiveEvent) => void;\ntype ReactiveErrorContext = { readonly kind: 'cleanup' | 'effect' | 'listener' | 'observer'; readonly name?: string };\ntype RippleOptions = { observer?: ReactiveObserver; onError?: (error: unknown, context: ReactiveErrorContext) => void };\n\ntype AsyncState<T> =\n | { readonly previous?: T; readonly status: 'pending' }\n | { readonly status: 'success'; readonly value: T }\n | { readonly error: unknown; readonly previous?: T; readonly status: 'error' };\n\ninterface Readable<T> {\n readonly name?: string;\n peek(): T;\n subscribe(listener: () => void): Unsubscribe;\n readonly value: T;\n}\n\ninterface Signal<T> extends Readable<T> { update(updater: (prev: T) => T): void; value: T }\ninterface Disposable { dispose(): void; readonly disposed: boolean; readonly disposalSignal: AbortSignal; [Symbol.dispose](): void }\ntype EffectHandle = Disposable;\ninterface Scope extends Disposable { run<T>(fn: () => T): T }\n\ninterface Resource<T> extends Readable<AsyncState<T>>, Disposable { reload(): void }\n\ninterface Ripple {\n batch<T>(fn: () => T): T;\n computed<T>(derive: () => T, options?: ComputedOptions<T>): Readable<T>;\n createScope(name?: string): Scope;\n dispose(): void;\n readonly disposed: boolean;\n effect(callback: () => Cleanup | undefined, options?: EffectOptions): EffectHandle;\n resource<Source, Value>(source: () => Source, loader: (source: Source, context: { readonly signal: AbortSignal }) => Promise<Value>, options?: ResourceOptions): Resource<Value>;\n signal<T>(initial: T, options?: SignalOptions<T>): Signal<T>;\n untrack<T>(fn: () => T): T;\n watch<T>(source: Readable<T> | (() => T), callback: (value: T, previous: T | undefined) => void, options?: WatchOptions<T>): EffectHandle;\n}\n```\n\n## Errors\n\n| Error | Trigger | Notable properties |\n| --- | --- | --- |\n| `RippleError` | Base Ripple error | Use `instanceof RippleError` to narrow unknown values. |\n| `RippleComputedCycleError` | Computed dependency reads itself through a cycle | Extends `RippleError`. |\n| `RippleDisposedRuntimeError` | Factory or execution API used after `ripple.dispose()` | Extends `RippleError`. |\n| `RippleDisposedScopeError` | `scope.run()` after scope disposal | Extends `RippleError`. |\n| `RippleInfiniteLoopError` | Effect flush exceeds graph iteration limit | Extends `RippleError`. |\n",
|
|
6
|
-
"usage": "---\ntitle: Ripple — Usage Guide\ndescription: Build reactive state with one explicit graph boundary.\n---\n\n[[toc]]\n\n## Basic Usage\n\nUse top-level functions when one application-lifetime graph is sufficient. Read a signal inside an effect to make that read reactive.\n\n```ts\nimport { computed, effect, signal } from '@vielzeug/ripple';\n\nconst count = signal(0);\nconst label = computed(() => `Count: ${count.value}`);\nconst stop = effect(() => console.log(label.value));\n\ncount.value = 1;\nstop.dispose();\n```\n\n## Isolated Graphs\n\nUse `createRipple()` for tests, SSR requests, embedded applications, or independently disposable features. Never mix reactive values from separate graphs.\n\n```ts\nimport { createRipple } from '@vielzeug/ripple';\n\nconst ripple = createRipple({\n onError(error, context) {\n console.log(context.kind, error);\n },\n});\n\nconst count = ripple.signal(0);\nconst stop = ripple.effect(() => console.log(count.value));\n\nstop.dispose();\nripple.dispose();\n```\n\n## Derived Values and Batches\n\nUse `computed()` for pure derivation. Use `untrack()` when a current read must not become an effect dependency. Use `batch()` for related synchronous writes.\n\n```ts\nconst first = ripple.signal('Ada');\nconst last = ripple.signal('Lovelace');\nconst locale = ripple.signal('en-US');\nconst name = ripple.computed(() => `${first.value} ${last.value}`);\n\nripple.effect(() => {\n console.log({ locale: ripple.untrack(() => locale.value), name: name.value });\n});\n\nripple.batch(() => {\n first.value = 'Grace';\n last.value = 'Hopper';\n});\n```\n\n## Scheduling and Subscriptions\n\nRipple propagates every synchronous write before flushing effects. Each flush pass runs effects queued at its\nstart before direct `subscribe()` listeners queued at its start. Work queued by either runs in a later pass.\nEffects using `scheduler: 'microtask'` join a later microtask and coalesce writes made before that task runs.\n\n```ts\nconst count = ripple.signal(0);\nconst log: string[] = [];\n\nripple.effect(() => log.push(`effect: ${count.value}`));\ncount.subscribe(() => log.push(`listener: ${count.value}`));\nripple.effect(() => log.push(`deferred: ${count.value}`), { scheduler: 'microtask' });\n\nlog.length = 0; // Ignore synchronous creation runs.\ncount.value = 1;\nconsole.log(log); // ['effect: 1', 'listener: 1']\n\nawait Promise.resolve();\nconsole.log(log); // ['effect: 1', 'listener: 1', 'deferred: 1']\n```\n\n## Ownership with Scopes\n\nCreate a scope when a group of effects or derived values shares one lifetime. Dispose the scope when its feature ends.\n\n```ts\nconst scope = ripple.createScope('panel');\nconst count = ripple.signal(0);\n\nscope.run(() => {\n ripple.effect(() => console.log(`Panel count: ${count.value}`));\n});\n\ncount.value = 1;\nscope.dispose();\n```\n\n## Watch Selected Values\n\nUse `watch()` for one selected output. Use `effect()` when every reactive read in the callback should be a dependency.\n\n```ts\nconst stopWatch = ripple.watch(\n () => `${first.value} ${last.value}`,\n (value, previous) => console.log({ previous, value }),\n { immediate: true },\n);\n\nstopWatch.dispose();\n```\n\n## Async Data\n\n`resource()` captures source dependencies synchronously and passes a cancellation signal to the loader.\n\n```ts\nconst userId = ripple.signal('42');\nconst user = ripple.resource(\n () => userId.value,\n async (id, { signal }) => {\n const response = await fetch(`/users/${id}`, { signal });\n if (!response.ok) throw new Error(`Request failed: ${response.status}`);\n\n return response.json() as Promise<{ id: string; name: string }>;\n },\n);\n\nif (user.value.status === 'success') console.log(user.value.value.name);\nif (user.value.status === 'error') console.error(user.value.error);\nuser.dispose();\n```\n\n## Object State\n\n`signal()` with `update()` holds one value and supports immutable replacement patterns. Return replacement objects from `update()` when object consumers depend on immutable updates.\n\n```ts\nconst cart = ripple.signal({ items: 0, label: 'empty' });\nconst items = ripple.computed(() => cart.value.items);\n\ncart.update((state) => ({ ...state, items: state.items + 1 }));\ncart.value = { items: 3, label: 'ready' };\n\nconsole.log(items.value);\n```\n\n## Testing\n\nCreate an isolated graph per test. Disposal prevents effects and resource work from leaking into later tests.\n\n```ts\nimport { expect, test } from 'vitest';\nimport { createRipple } from '@vielzeug/ripple';\n\ntest('derives a doubled count', () => {\n const ripple = createRipple();\n const count = ripple.signal(2);\n const doubled = ripple.computed(() => count.value * 2);\n\n expect(doubled.value).toBe(4);\n ripple.dispose();\n});\n```\n\n## Framework Integration\n\nUse signals and effects with any renderer. Dispose component-owned effects when the component unmounts.\n\n::: code-group\n\n```ts [React]\nimport { useEffect, useState } from 'react';\nimport { createRipple } from '@vielzeug/ripple';\n\nconst ripple = createRipple();\nconst count = ripple.signal(0);\n\nexport function Counter() {\n const [, rerender] = useState(0);\n\n useEffect(() => {\n const stop = ripple.effect(() => {\n void count.value;\n rerender((revision) => revision + 1);\n });\n\n return () => stop.dispose();\n }, []);\n\n return <button onClick={() => (count.value += 1)}>{count.value}</button>;\n}\n```\n\n```ts [Vue 3]\nimport { onUnmounted, ref } from 'vue';\nimport { createRipple } from '@vielzeug/ripple';\n\nconst ripple = createRipple();\nconst count = ripple.signal(0);\nconst revision = ref(0);\nconst stop = ripple.effect(() => {\n void count.value;\n revision.value++;\n});\n\nonUnmounted(() => stop.dispose());\n```\n\n```ts [Svelte]\n<script lang=\"ts\">\n import { onDestroy } from 'svelte';\n import { createRipple } from '@vielzeug/ripple';\n\n const ripple = createRipple();\n const count = ripple.signal(0);\n let revision = 0;\n const stop = ripple.effect(() => {\n void count.value;\n revision++;\n });\n\n onDestroy(() => stop.dispose());\n</script>\n\n<button on:click={() => (count.value += 1)}>{count.value}</button>\n```\n\n:::\n\n## Working with Other Vielzeug Libraries\n\nOre uses Ripple for component reactivity. Clockwork actors expose framework-neutral snapshots; bridge actor subscriptions into a Ripple signal. Ledger adds undo/redo commands around state changes without replacing graph.\n\n```ts\nimport { createRipple } from '@vielzeug/ripple';\nimport { defineMachine } from '@vielzeug/clockwork';\n\nconst ripple = createRipple();\nconst actor = defineMachine<Record<string, never>, { type: 'START' }>()({\n initial: 'idle',\n states: { active: {}, idle: { on: { START: { target: 'active' } } } },\n}).createActor();\n\nconst snapshot = ripple.signal(actor.snapshot);\nconst stop = actor.subscribe((next) => (snapshot.value = next));\nconst status = ripple.computed(() => snapshot.value.state);\nconsole.log(status.value);\n\nstop();\nactor.dispose();\nripple.dispose();\n```\n\n## Best Practices\n\n- Create one graph per ownership boundary.\n- Keep computed callbacks pure.\n- Return cleanup from effects.\n- Dispose request, test, and feature graphs.\n- Batch related synchronous writes.\n- Use `watch()` only for selected source transitions.\n- Read dependencies in a resource source, not its loader.\n- Use `onError` for runtime callback, cleanup, listener, and observer failures; handle resource source and loader failures through `resource.value.status === 'error'`.\n",
|
|
6
|
+
"usage": "---\ntitle: Ripple — Usage Guide\ndescription: Build reactive state with one explicit graph boundary.\n---\n\n[[toc]]\n\n## Basic Usage\n\nUse top-level functions when one application-lifetime graph is sufficient. Read a signal inside an effect to make that read reactive.\n\n```ts\nimport { computed, effect, signal } from '@vielzeug/ripple';\n\nconst count = signal(0);\nconst label = computed(() => `Count: ${count.value}`);\nconst stop = effect(() => console.log(label.value));\n\ncount.value = 1;\nstop.dispose();\n```\n\n## Isolated Graphs\n\nUse `createRipple()` for tests, SSR requests, embedded applications, or independently disposable features. Never mix reactive values from separate graphs.\n\n```ts\nimport { createRipple } from '@vielzeug/ripple';\n\nconst ripple = createRipple({\n onError(error, context) {\n console.log(context.kind, error);\n },\n});\n\nconst count = ripple.signal(0);\nconst stop = ripple.effect(() => console.log(count.value));\n\nstop.dispose();\nripple.dispose();\n```\n\n## Derived Values and Batches\n\nUse `computed()` for pure derivation. Use `untrack()` when a current read must not become an effect dependency. Use `batch()` for related synchronous writes.\n\n```ts\nconst first = ripple.signal('Ada');\nconst last = ripple.signal('Lovelace');\nconst locale = ripple.signal('en-US');\nconst name = ripple.computed(() => `${first.value} ${last.value}`);\n\nripple.effect(() => {\n console.log({ locale: ripple.untrack(() => locale.value), name: name.value });\n});\n\nripple.batch(() => {\n first.value = 'Grace';\n last.value = 'Hopper';\n});\n```\n\n## Scheduling and Subscriptions\n\nRipple propagates every synchronous write before flushing effects. Each flush pass runs effects queued at its\nstart before direct `subscribe()` listeners queued at its start. Work queued by either runs in a later pass.\nEffects using `scheduler: 'microtask'` join a later microtask and coalesce writes made before that task runs.\n\n```ts\nconst count = ripple.signal(0);\nconst log: string[] = [];\n\nripple.effect(() => log.push(`effect: ${count.value}`));\ncount.subscribe(() => log.push(`listener: ${count.value}`));\nripple.effect(() => log.push(`deferred: ${count.value}`), { scheduler: 'microtask' });\n\nlog.length = 0; // Ignore synchronous creation runs.\ncount.value = 1;\nconsole.log(log); // ['effect: 1', 'listener: 1']\n\nawait Promise.resolve();\nconsole.log(log); // ['effect: 1', 'listener: 1', 'deferred: 1']\n```\n\n## Ownership with Scopes\n\nCreate a scope when a group of effects or derived values shares one lifetime. Dispose the scope when its feature ends.\n\n```ts\nconst scope = ripple.createScope('panel');\nconst count = ripple.signal(0);\n\nscope.run(() => {\n ripple.effect(() => console.log(`Panel count: ${count.value}`));\n});\n\ncount.value = 1;\nscope.dispose();\n```\n\n## Watch Selected Values\n\nUse `watch()` for one selected output. Use `effect()` when every reactive read in the callback should be a dependency.\n\n```ts\nconst stopWatch = ripple.watch(\n () => `${first.value} ${last.value}`,\n (value, previous) => console.log({ previous, value }),\n { immediate: true },\n);\n\nstopWatch.dispose();\n```\n\n## Async Data\n\n`resource()` captures source dependencies synchronously and passes a cancellation signal to the loader.\n\n```ts\nconst userId = ripple.signal('42');\nconst user = ripple.resource(\n () => userId.value,\n async (id, { signal }) => {\n const response = await fetch(`/users/${id}`, { signal });\n if (!response.ok) throw new Error(`Request failed: ${response.status}`);\n\n return response.json() as Promise<{ id: string; name: string }>;\n },\n);\n\nif (user.value.status === 'success') console.log(user.value.value.name);\nif (user.value.status === 'error') console.error(user.value.error);\nuser.dispose();\n```\n\n## Object State\n\n`signal()` with `update()` holds one value and supports immutable replacement patterns. Return replacement objects from `update()` when object consumers depend on immutable updates.\n\n```ts\nconst cart = ripple.signal({ items: 0, label: 'empty' });\nconst items = ripple.computed(() => cart.value.items);\n\ncart.update((state) => ({ ...state, items: state.items + 1 }));\ncart.value = { items: 3, label: 'ready' };\n\nconsole.log(items.value);\n```\n\n## Testing\n\nCreate an isolated graph per test. Disposal prevents effects and resource work from leaking into later tests.\n\n```ts\nimport { expect, test } from 'vitest';\nimport { createRipple } from '@vielzeug/ripple';\n\ntest('derives a doubled count', () => {\n const ripple = createRipple();\n const count = ripple.signal(2);\n const doubled = ripple.computed(() => count.value * 2);\n\n expect(doubled.value).toBe(4);\n ripple.dispose();\n});\n```\n\n## Framework Integration\n\nUse signals and effects with any renderer. Dispose component-owned effects when the component unmounts.\n\n::: code-group\n\n```ts [React]\nimport { useEffect, useState } from 'react';\nimport { createRipple } from '@vielzeug/ripple';\n\nconst ripple = createRipple();\nconst count = ripple.signal(0);\n\nexport function Counter() {\n const [, rerender] = useState(0);\n\n useEffect(() => {\n const stop = ripple.effect(() => {\n void count.value;\n rerender((revision) => revision + 1);\n });\n\n return () => stop.dispose();\n }, []);\n\n return <button onClick={() => (count.value += 1)}>{count.value}</button>;\n}\n```\n\n```ts [Vue 3]\nimport { onUnmounted, ref } from 'vue';\nimport { createRipple } from '@vielzeug/ripple';\n\nconst ripple = createRipple();\nconst count = ripple.signal(0);\nconst revision = ref(0);\nconst stop = ripple.effect(() => {\n void count.value;\n revision.value++;\n});\n\nonUnmounted(() => stop.dispose());\n```\n\n```ts [Svelte]\n<script lang=\"ts\">\n import { onDestroy } from 'svelte';\n import { createRipple } from '@vielzeug/ripple';\n\n const ripple = createRipple();\n const count = ripple.signal(0);\n let revision = 0;\n const stop = ripple.effect(() => {\n void count.value;\n revision++;\n });\n\n onDestroy(() => stop.dispose());\n</script>\n\n<button on:click={() => (count.value += 1)}>{count.value}</button>\n```\n\n:::\n\n## Working with Other Vielzeug Libraries\n\nOre uses Ripple for component reactivity. Clockwork actors expose framework-neutral snapshots; bridge actor subscriptions into a Ripple signal. Ledger adds undo/redo commands around state changes without replacing graph.\n\n```ts\nimport { createRipple } from '@vielzeug/ripple';\nimport { defineMachine } from '@vielzeug/clockwork';\n\nconst ripple = createRipple();\nconst actor = defineMachine<Record<string, never>, { type: 'START' }>()({\n initial: 'idle',\n states: { active: {}, idle: { on: { START: { target: 'active' } } } },\n}).createActor();\n\nconst snapshot = ripple.signal(actor.snapshot);\nconst stop = actor.subscribe((next) => (snapshot.value = next));\nconst status = ripple.computed(() => snapshot.value.state);\nconsole.log(status.value);\n\nstop();\nactor.dispose();\nripple.dispose();\n```\n\n## Gotchas\n\n### `subscribe()` forces computed evaluation\n\n`Readable.subscribe()` calls `peek()` before registering the listener. For signals this is a no-op, but for computeds it forces `refresh()` — the derivation runs immediately even if no one reads `.value`. This ensures `equals` comparison works on the first dependency change. Avoid subscribing to expensive computeds unless you need their value.\n\n### Computed first-run failure is recoverable\n\nIf a computed's `derive` throws on its first run (e.g., a source is `null`), the computed commits the partial dependencies it tracked before the throw. When a dependency changes and the derivation can succeed, the computed refreshes and notifies its dependents. Effects that read a failing computed report the error through `onError` and re-run when the computed recovers.\n\n## Best Practices\n\n- Create one graph per ownership boundary.\n- Keep computed callbacks pure.\n- Return cleanup from effects.\n- Dispose request, test, and feature graphs.\n- Batch related synchronous writes.\n- Use `watch()` only for selected source transitions.\n- Read dependencies in a resource source, not its loader.\n- Use `onError` for runtime callback, cleanup, listener, and observer failures; handle resource source and loader failures through `resource.value.status === 'error'`.\n",
|
|
7
7
|
"examples": "---\ntitle: Ripple — Examples\ndescription: Practical Ripple recipes.\n---\n\n## Examples\n\n- [Reactive Counter](./examples/reactive-counter.md)\n- [Batch and Untrack](./examples/batch-and-untrack.md)\n- [Scope Ownership](./examples/scope-ownership.md)\n- [Watch Selected Value](./examples/watch-selected-value.md)\n- [Immutable State](./examples/immutable-store.md)\n- [Isolated Graph](./examples/isolated-runtime.md)\n- [Async Resource](./examples/async-resource.md)\n"
|
|
8
8
|
},
|
|
9
9
|
"examples": [
|