# @aceshooting/lyra-ui — LLM API Reference `@aceshooting/lyra-ui` is a free, independent, MIT-licensed [Lit](https://lit.dev) web-component library and an open-source alternative to Shoelace and Web Awesome. It combines accessible form controls, layout and overlay primitives, dashboards, charts, data visualization, and Conversation & Agent UI. Selected components retain documented Web Awesome-compatible public names under a `lr-` prefix to make migration easier, but Lyra has its own implementation, design tokens, localization runtime, RTL behavior, and release surface. It has no runtime dependency on either project. **This file is the whole catalog concatenated — several hundred thousand tokens.** Read it end to end only if you genuinely need everything. Otherwise: | To… | Read | |---|---| | use one component | `llms/components/.md` (path derived from the tag; self-contained) | | find the right component | `llms/index.md` (every tag, its import path, one-line purpose) | | get library-wide behavior right | `llms/shared.md` (status, imports/autoloading, events, forms, theming/styles, i18n/RTL, TS/frameworks, SSR, utilities, AI types) | | theme it | `llms/tokens.md` | | know what to `npm install` | `llms/peers.md` | | port `wa-*`/`sl-*` markup | `llms/migration.md` (per-tag classification, safe rewrites, and warnings) | Prefer stable tag-shaped registration imports such as `@aceshooting/lyra-ui/components/lr-input.js`; each registers its tag as a side effect without coupling application code to Lyra's source-family layout. Existing family-shaped registration paths remain supported. Class-only and helper modules continue to use their owning family paths; a sibling `.class.js` exports a class without registering it. ## Contents This file is the concatenation of the per-family sources in `llms/`. When only one component is in question, read `llms/components/.md` instead — it is the same text plus its import path, and costs a few hundred tokens instead of the whole catalog. `llms/index.md` maps every tag to its family and entry point. - **Foundation** — base class, form association, events, i18n, theming, TypeScript, frameworks, SSR, packaging (`llms/shared.md`) - **Form controls & inputs** — 29 tags in `components/forms/` - **Data display, dashboards & flow canvas** — 26 tags in `components/data/` - **Layout, navigation & structure** — 36 tags in `components/layout/` - **Overlays, status & feedback** — 21 tags in `components/overlays/` - **Utilities, formatting & observers** — 20 tags in `components/utility/` - **Media, files & maps** — 22 tags in `components/media/` - **Charts** — 12 tags in `components/charts/` - **Document & file viewers** — 23 tags in `components/viewers/` - **Conversation & chat UI** — 34 tags in `components/conversation/` - **Agent tooling, tool calls & evaluation** — 35 tags in `components/agent-tools/` - **Retrieval, knowledge graph & grounding** — 30 tags in `components/retrieval/` - **Reference tables** — design tokens (`llms/tokens.md`), optional peers (`llms/peers.md`), `wa-*`/`sl-*` migration (`llms/migration.md`) --- # Foundation ## Component status, versioning, and deprecation Every public `lr-*` component has an explicit status and `since` version in the package metadata. `since` is the earliest published Lyra UI release manifest that contained the tag; for a component introduced by the current release, it is the current package version. - **Stable** components are supported for production use. An incompatible public API or behavior change requires a semver-major release. - **Experimental** components are still open to design review, but they are not exempt from compatibility promises: once published, their public APIs receive the same full-semver protection as stable components until they are formally deprecated and removed. Deprecation is explicit metadata, not an implication from status. Each deprecated component or member names a replacement, a deprecation version, a rationale, and the earliest permitted removal version. If an API is deprecated in major version M, it remains available for the complete M+1 release line and cannot be removed before M+2. This policy applies equally to stable and experimental public APIs. **9.0.0 took a one-time exception to that policy, and says so rather than quietly breaking it.** Three members whose recorded removal window had genuinely opened were removed normally (`lr-tool-call-chip`/`lr-message-parts`' `lr-tool-chip-select`, and `lr-flow-canvas`' `--lr-flow-canvas-node-current-outline-color`). Alongside them, a small set of members that had _never_ been deprecated were renamed and their old spellings removed in the same release, without the customary M+1 warning period — `lr-usage-badge`'s `compact`, `lr-chart`'s `horizontal`, `lr-rag-answer`/`lr-retrieval-results`' `error`, `lr-ingestion-queue`'s `virtualizeThreshold` → `virtualizeAt`, `lr-knowledge-base`'s `lr-kb-*` events, `lr-data-grid`'s `columns`/`filename` option fields, and `lr-test-results`' two legacy detail-slot spellings, plus the component-specific `LyraModelCatalog`/`LyraVoiceCatalog` aliases in favor of the shared `LyraCatalog`. Every one has a mechanical migration listed in the 9.0.0 changelog entry and in `migration.md`. From 9.0.0 onward the M+2 rule applies as written; treat the above as a documented exception, not a precedent. ### Release history and upgrade notes `since` records when a tag first appeared; it is not a history of later additions, fixes, or breaking changes. For every release after 9.0.0 — including minor and patch releases — read the package's shipped [CHANGELOG.md](./CHANGELOG.md) before upgrading. Family-wide breaking-change summaries sit at the start of the applicable authored `llms/.md` file; each generated `llms/components/.md` header links that family summary when one exists. Component-specific version notes remain in the component's own section. `llms/migration.md` is narrower: it covers `wa-*`/`sl-*` renames and compatibility decisions, not Lyra release history. The major-version landmarks after 9.0.0 are: - **10.0.0:** removed eligible members deprecated during 9.x and made the public-contract corrections listed in its changelog entry; follow that entry's per-member migration guidance. - **11.0.0:** carried no known consumer-breaking change; consumers upgrading from 10.x were not expected to change code. - **12.0.0:** removed the inherited static `LyraElement.getPropertyDescriptor()` surface after Lit deprecated that finalization hook. Ordinary component consumers need no migration; only subclasses that themselves overrode the hook are affected. Collection snapshot enforcement moved to a decorator-agnostic accessor seam instead. ### The support window Compatibility promises are bounded by a published support window, not by "evergreen browsers": Chromium 120+, Gecko 121+, WebKit 16.4+, and Node 20+ (ESM only; there is no CommonJS entry point). Those floors are derived from platform features the source actually uses — `:dir()`, `:has()`, `@container`, `color-mix()`, `ElementInternals` form association — because the package ships untranspiled ES2022 modules with no polyfills and no build-time downleveling. There is deliberately no `browserslist` field: it would describe a build step this package does not have. The version floors are derived from those platform features rather than individually verified version-by-version. Raising any floor is a semver-major change. Full policy, including the known WebKit cross-shadow-selection gap and the rule for when a `@supports` fallback may be dropped: . ## Importing and registering components Every component has a stable, tag-shaped side-effect entry point that registers its own tag. Use `components/.js`; this public boundary stays unchanged if the internal family folders move: ```js import "@aceshooting/lyra-ui/components/lr-combobox.js"; // registers import "@aceshooting/lyra-ui/components/lr-table.js"; // registers ``` The older family-shaped registration paths remain supported for compatibility. Class-only `.class.js` entries still use their owning family path because they intentionally expose source organization and do not register a tag. Principal v8 and compatibility registrations use the same exact shape: ```js import "@aceshooting/lyra-ui/components/lr-page.js"; import "@aceshooting/lyra-ui/components/lr-video.js"; import "@aceshooting/lyra-ui/components/lr-video-playlist.js"; import "@aceshooting/lyra-ui/components/lr-native-time-input.js"; import "@aceshooting/lyra-ui/components/lr-pan-zoom.js"; import "@aceshooting/lyra-ui/components/lr-split-panel.js"; import "@aceshooting/lyra-ui/components/lr-alert.js"; ``` `llms/index.md` lists every tag and its owning implementation module. The stable tag-shaped alias and the family-shaped compatibility path both resolve through the package's `./components/*` export. **Breaking in 8.0.0 — the package root no longer registers anything.** Through 7.x, importing the bare `@aceshooting/lyra-ui` root had the side effect of defining every non-optional-peer tag, so a project that only wanted a type or a helper from it silently pulled the then-current 268 component definitions into its eager bundle. The root is now a pure, side-effect-free export surface, and the registrations moved to an explicit entry: ```js import "@aceshooting/lyra-ui/all.js"; // exactly the pre-8 root behaviour, opted into by name ``` _Nothing was removed from the root's named surface._ Every class, helper, and type it exported in 7.x is still exported, from the same specifier; `all.js` re-exports that identical surface, so `import { LyraTable } from '@aceshooting/lyra-ui';` and the `@aceshooting/lyra-ui/all.js` form of it both keep working. Only the registration side effect changed. _Migrating._ If a bare `import '@aceshooting/lyra-ui';` (or a bundler entry that relied on it) was how your tags got defined, add the `/all.js` specifier — a one-line change with identical behaviour. If instead the root was only ever imported for values or types, delete nothing: those imports now cost what they should. The symptom of a missed migration is an unupgraded element — the tag renders as an empty inert box with its light DOM visible — not a module error, because the specifier still resolves and still hands back everything it used to. _Granular imports remain the recommendation._ `all.js` is a compatibility and prototyping convenience, not the intended production shape: it is side-effectful by definition and cannot be tree-shaken down to the handful of elements a page actually renders. The entry points, then: - **Class without registration.** Each entry has a `.class.js` sibling exporting the class (and the `HTMLElementTagNameMap` augmentation) without touching `customElements`: `import { LyraTable } from '@aceshooting/lyra-ui/components/data/table/table.class.js';`. Use it for subclassing, `instanceof` checks, or type-only imports. - **Duplicate package copies.** Re-registering the same constructor is silent and idempotent. If a different Lyra constructor already owns a tag, the first definition remains active and Lyra emits one warning for that exact conflict with the existing/incoming package versions, constructor names, and both constructor references. An existing non-Lyra definition is reported with an `unknown` existing version rather than guessed provenance. - **Root barrel.** `import '@aceshooting/lyra-ui';` registers **nothing** (see the 8.0.0 note above). It re-exports a broad compatibility surface of commonly used classes, helpers, and types, but it is not an exhaustive promise that every component-owned type or future export is present. Prefer the owning component entry in application code, both for the smallest bundle and the complete contract of that component. - **`all.js` compatibility entry.** `import '@aceshooting/lyra-ui/all.js';` registers the 272 root-included tags — everything **except** the 16 inventory-designated optional-peer-family tags: `lr-chart` and its 8 typed subclasses (`lr-line-chart`, `lr-bar-chart`, `lr-pie-chart`, `lr-doughnut-chart`, `lr-radar-chart`, `lr-polar-area-chart`, `lr-bubble-chart`, `lr-scatter-chart`), `lr-box-plot`, `lr-histogram`, `lr-map`, `lr-graph`, `lr-knowledge-graph-explorer`, `lr-geojson-view`, and `lr-geojson-viewer`. Those always need their own subpath import, from `all.js` exactly as from the root — the entry deliberately preserves the optional-peer isolation contract rather than putting `chart.js`, `maplibre-gl`, or the `d3-*` set on the critical path of every install. It is the one import that defeats tree-shaking. (Server-side, `@aceshooting/lyra-ui/ssr/all.js` is the counterpart that _does_ register the complete inventory, optional-peer families included; see "SSR and declarative shadow DOM".) - **Document anchor/highlight types.** The granular document-viewer entry owns and exports `LyraAnchor`, `LyraAnchorKind`, `LyraHighlight`, `LyraHighlightTone`, `AnchorTargetCapabilities`, `HighlightActivateDetail`, `TextSelectDetail`, and `AnchorResultDetail`. The registration-free root now intentionally re-exports these contracts, together with `LyraAnchorTarget` and `LyraAnchorTargetEventMap`, and all are semver-covered: ```ts import type { LyraAnchor, LyraHighlight, AnchorTargetCapabilities, } from "@aceshooting/lyra-ui/components/viewers/document-viewer/document-viewer.js"; ``` Prefer the granular entry for component-local imports; use the root export when an application deliberately shares the contracts across several viewer integrations. - **`lr-flag`** registers from the barrel, but resolving a flag by `country`/`language` (rather than a pre-resolved `src`) additionally needs `import '@aceshooting/lyra-ui/components/media/flag/flag-peer.js';` once. - **Other subpaths.** `@aceshooting/lyra-ui/theme.css` (ready-made light/dark theme), `@aceshooting/lyra-ui/tokens-root.css` (opt-in: the curated resolved `--lr-*` tokens at `:root`, so your own components can read them), `@aceshooting/lyra-ui/native.css` (opt-in native-element styles inside `.lr-native`), `@aceshooting/lyra-ui/utilities.css` (opt-in light-DOM layout/text utilities), `@aceshooting/lyra-ui/theme.js` (the zero-dependency mode/accent runtime), `@aceshooting/lyra-ui/localization.js` (side-effect-free locale runtime), `@aceshooting/lyra-ui/autoloader.js` (side-effect-free on-demand tag loading), `@aceshooting/lyra-ui/autoloader-cdn.js` (browser-guarded auto-start side effect), `@aceshooting/lyra-ui/translations/.js` (the eleven shipped message catalogs), `@aceshooting/lyra-ui/events` (the global typed-event map — types only, no runtime), `@aceshooting/lyra-ui/ai` (provider-neutral data types), `@aceshooting/lyra-ui/testing` (happy-dom shims, `createLyraEvent()` for building a validated test event, a small set of interaction drivers that go through a component's own real activation path, and `waitForLyraElement()`/`waitForToast()` for awaiting a lazily registered mount), `@aceshooting/lyra-ui/utilities/*` (the curated shared helpers, all documented below). ### Registration-free component helpers The following focused helpers are public, side-effect-free modules. They do not define a custom element, so they are suitable for server code, workers, or applications that need the same data normalization as a component without registering it: ```ts import { DEFAULT_INTERNAL_PATTERNS, parseStackTrace, STACK_TRACE_LIMITS, } from '@aceshooting/lyra-ui/components/agent-tools/stack-trace/stack-trace-parse.js'; import { MAX_RENDERED_LYRA_SPANS, normalizeLyraSpans, } from '@aceshooting/lyra-ui/components/agent-tools/trace-tree/span.js'; import { agentStatusMessage, agentStatusVariant, } from '@aceshooting/lyra-ui/components/agent-tools/agent-status-presentation.js'; import { approvalAction, approvalDecision, } from '@aceshooting/lyra-ui/components/agent-tools/approval-state.js'; import { DEFAULT_WIDGET_TYPE_REGISTRY } from '@aceshooting/lyra-ui/components/conversation/widget-renderer/default-registry.js'; ``` Their companion type exports remain on those same paths: stack parsing exposes `StackFrame`, `StackGroup`, `StackTraceParseOptions`, and `StackTraceParseResult`; span projection exposes `LyraSpan`, `LyraSpanKind`, `LyraSpanStatus`, and `LyraSpanProjection`; status and approval helpers expose their respective presentation and decision types. The status helpers are `agentStatusKind`, `agentStatusLabel`, `agentStatusMessage`, `agentStatusVariant`, `isAgentStatusTerminal`, and `isAgentStatusActive`; approval also exports `ApprovalAction` and `ApprovalDecision`; span also exports `normalizeLyraSpanKind` and `normalizeLyraSpanStatus`; the widget registry module exports only `DEFAULT_WIDGET_TYPE_REGISTRY`. Import a component registration entry separately when the page also renders that component. ### Optional autoloader `@aceshooting/lyra-ui/autoloader.js` exports `discover(root?, options?)`, `start(root?, options?)`, and `stop()`. It can load each known Lyra tag independently, allowing bundlers to split components while granular imports remain independent. Importing this entry alone has no side effect and registers nothing. - `discover()` scans once. `start()` performs the same initial scan, then observes dynamic and Turbo-style replacement subtrees; a later `start()` stops the previous watcher. `stop()` is idempotent, disconnects it, invalidates pending definitions, and removes loader-owned markers. - The optional root is a `Document`, `DocumentFragment`/open `ShadowRoot`, or `Element`; the default is `document`. Caller-owned open shadow roots are traversed iteratively. Each element resolves against its owning/scoped custom-element registry rather than an unrelated global registry. - `maxElements` (default `10_000`), `maxRoots` (`2_000`), `maxDepth` (`256`), and `maxWork` (`100_000`) bound one complete discovery operation. `maxConcurrency` (`16`) bounds concurrent definition and first-update tasks. Invalid limits throw `RangeError`; an initial `discover()` or `start()` preflights its currently rendered tree and rejects a traversal-limit failure before loading it. Newly rendered shadow content remains under the same cumulative ceilings, but a failure found there necessarily follows the parent definition that rendered it. - A discovered element carries `data-lr-autoload-pending` until its class is defined and its first `updateComplete` settles. The exported `AUTOLOADER_PENDING_ATTRIBUTE` is that exact string. A pre-existing consumer-owned marker is never removed by the loader. - `{ events: true }` emits bubbling/composed `lr-autoload-preload`, `lr-autoload-loaded`, `lr-autoload-error`, and `lr-autoload-traversal-error` events on the supplied root. Detail is `{ tag, optionalPeers }`, plus the caught `error` for the error event. `loaded` means the registry definition exists; the pending marker remains authoritative until first render finishes. A watched insertion that exceeds a traversal ceiling emits the traversal event with `{ limit, maximum, error }`. A statically over-limit insertion launches no definitions from that insertion; a later failure in its first-update shadow content leaves the already loaded parent intact. Later work can still retry. - Optional-peer tags are skipped by default. `optionalPeers: ['dompurify', 'postal-mime']` enables a tag only when the allowlist contains **all** packages recorded for it; `optionalPeers: 'all'` is for an installation that deliberately provides the entire peer set. A failed import clears its marker and in-flight cache, so a later scan or insertion can retry it. ```ts import { start, stop } from "@aceshooting/lyra-ui/autoloader.js"; await start(document, { optionalPeers: ["dompurify", "postal-mime"], events: true, }); // Later, when this application no longer owns the rendered subtree: stop(); ``` `@aceshooting/lyra-ui/autoloader-cdn.js` is the separate side-effect entry. It auto-starts only when `document` exists and reads `data-lyra-optional-peers="peer-a,peer-b"` plus the boolean `data-lyra-autoload-events` from its own ` ``` Both variants read a stored `{ mode, accent, surface }` record, resolve `auto`, and derive the same per-role ramp(s) from the same math — the bootstrap re-implements it inline (self-contained, so it can run before any module loads) rather than importing the runtime, but the two are tested to never drift. A missing or malformed record receives the runtime's `{ mode: 'auto', accent: null, surface: null }` default; blocked `localStorage` leaves the document untouched rather than throwing before your app loads. **External-file delivery for a strict CSP.** `@aceshooting/lyra-ui/theme-bootstrap.js` is a third, non-module way to ship the same bootstrap: a static script asset published alongside the package, containing exactly `lyraThemeBootstrap`'s bytes (both are produced from the same build step, so they can never drift apart). Reference it with a plain ` ``` This exists for a Content-Security-Policy that forbids `unsafe-inline` and cannot mint a per-response nonce — a static HTML entry, for example — where the documented inline-script nonce/hash guidance above does not apply. Serving it same-origin (copy it into your build output, or configure your bundler/static host to do so) needs no hash at all; hashing it for an even stricter policy uses the same CSP `script-src` hash mechanism browsers already apply to any external script resource. **Configuring the static asset from its own ` ``` - `data-lr-theme-storage-key` — the `localStorage` key to read, in place of the default `'lyra-theme'`. Equivalent to `createLyraThemeBootstrap({ storageKey })`'s argument, but resolved by the static file itself at parse time rather than baked in ahead of time. This is what lets an application with its own pre-existing storage key use the static asset instead of inlining a per-app copy. - `data-lr-theme-attributes` — a space-separated list of attribute names to set on `` in place of the default `data-lr-theme data-theme` pair, replacing that list entirely rather than adding to it. Both attributes are optional and independently validated; an absent, empty, oversized, or malformed value falls back to the built-in default rather than throwing, so a ` ``` **Which locale a component ends up using.** Four sources, first answer wins: 1. **The component's own `locale`, then its own `lang`.** 2. **The nearest ancestor declaring `locale` or `lang`** (crossing shadow boundaries), except that `lang` on `` is not read here — see 4. A `locale` attribute on `` _is_, since that attribute is this library's own and can only be a deliberate opt-in. 3. **`setLyraLocale(tag)`**, the page-level selection. 4. **``**, the document default. 5. **`'en'`.** **Breaking in 9.0.0:** steps 3 and 4 were the other way round, which made `setLyraLocale()` inert on any page that declares `` — i.e. essentially every well-formed page. `setLyraLocale('fr')` under `` silently kept rendering English. It now wins. Two consequences: an application that switched locale by _rewriting_ `` still works only if it never also called `setLyraLocale()` (the explicit call now pins the locale until changed or cleared with `setLyraLocale('')`), and a per-subtree `lang`/`locale` override is unaffected — it still beats both. To keep ``/`dir` in step with `setLyraLocale()` — which everything _outside_ this library reads, from `:lang()` rules to spellcheck to a screen reader's pronunciation — use `bridgeLyraLocale()` from `@aceshooting/lyra-ui/utilities/localization.js` (see [Shared helpers](#shared-helpers-utilities)). The side-effect-free `@aceshooting/lyra-ui/localization.js` entry exports `registerLyraLocale`, `setLyraLocale`, `getLyraLocale`, `getLyraLocaleDirection`, `getRegisteredLyraLocales`, `getRegisteredLyraLocaleKeys`, `subscribeLyraLocaleRegistry`, `resolveLyraLocale`, `resolveLyraDirection`, `resolveLyraString`, `LYRA_DEFAULT_STRINGS`, and the types `LyraLocaleStrings`, `LyraLocaleMeta`, `LyraLocaleDirection`, `LyraMessageKey`, `LyraMessage`, `LyraPluralMessage` and `LyraPluralCategory`. The package root continues to re-export the same surface for compatibility and remains registration-free in v8; use the dedicated entry when the application only needs locale setup and the narrower import graph. **`LYRA_DEFAULT_STRINGS` is the authoritative key list** (matching the `LyraMessageKey` union) — read it to find the key to override rather than guessing one. Date, number, byte, relative-time and calendar output goes through `Intl`. **Locale-tag identity.** Registration, active selection, `getLyraLocale()`, registry enumeration, component resolution and `bridgeLyraLocale()` all use the same public spelling. `_` remains an input alias, and structurally valid BCP-47 tags go through the platform canonicalizer, so `PT_BR` becomes `pt-BR` and deprecated aliases collapse onto their current tag. Private-use-only and short legacy application tags that the platform canonicalizer rejects remain supported as bounded lowercase identities. `registerLyraLocale()` and `setLyraLocale()` reject values that are neither valid BCP-47 nor bounded alphanumeric custom tags; an inherited invalid over-complex `locale`/`lang` safely resolves to English. Structurally valid long BCP-47 tags remain accepted. **Lookup order for a tag.** Every message resolves through one chain, and `Intl.PluralRules` category selection walks the same chain, so the two can never disagree: 1. **The BCP-47 truncation walk, most specific first** — `zh-Hans-CN` → `zh-Hans` → `zh`. Casing and `_` separators are normalized, so `pt_BR` and `pt-br` are the same key. Lookup chains are memoized and capped at 64 candidates. A structurally valid tag beyond the normal complexity ceiling retains its exact tag, base language and English fallback without constructing an unbounded prefix ladder; malformed over-complex inherited input goes directly to English. 2. **Then any registered, well-formed BCP-47 catalog sharing the base language**, which is how a _regional-only_ catalog is reached from a less specific tag: `lang="zh"` and `lang="zh-Hans"` both find the shipped `zh-CN` catalog, and `lang="pt"`/`lang="pt-PT"` both find `pt-BR`. Order within this step is deterministic and independent of import order — most shared subtags first (`zh-Hant-TW` prefers a registered `zh-TW` over `zh-CN`), then alphabetically as the tie-break (bare `zh` with both registered picks `zh-CN`). Register the regional tag you actually mean if the tie-break isn't the answer you want. Legacy/custom tags are exact/truncation-addressable only and do not become regional siblings accidentally. 3. **Then `en`**, always available through the built-in English defaults. Step 1 always beats step 2: with both `zh` and `zh-CN` registered, `zh-Hans-CN` resolves to `zh`. ```ts import { getLyraLocaleDirection } from "@aceshooting/lyra-ui/localization.js"; getLyraLocaleDirection("ar-EG"); // 'rtl' — declared by the shipped `ar` catalog, inherited by the region getLyraLocaleDirection("de"); // 'ltr' ``` `getLyraLocaleDirection(tag): 'ltr' | 'rtl'` answers "does this locale need `dir="rtl"`?" without an application keeping its own tag table. It reads a `dir` declared by `registerLyraLocale()`'s optional third argument first (walked through the same chain above, so a region inherits its base language's declaration), then `Intl.Locale`'s text-info surface where the engine has it, and finally `'ltr'`. It only _reports_ a direction — nothing in the library applies one; see [RTL and direction](#rtl-and-direction). ```ts registerLyraLocale("ar", { close: "إغلاق" }, { dir: "rtl", name: "العربية" }); ``` `registerLyraLocale(tag, strings, meta?)`'s third argument is optional catalog metadata — `{ dir?: 'ltr' | 'rtl'; name?: string }` (`LyraLocaleMeta`). Nothing in it is ever rendered: `dir` feeds `getLyraLocaleDirection()`, `name` is the locale's endonym for an application's own locale list. It merges the same way `strings` does, so a later two-argument call adding messages never drops metadata, and the two-argument call remains the normal way to register a catalog. Registration snapshots at most 4,096 own enumerable catalog properties before publishing them. Each accepted value is either a string or a plain/null-prototype CLDR plural record whose own data properties are string categories and which includes `other`. Nested plural records are cloned and frozen. Arrays, `null`, inherited properties, accessors and malformed records are ignored per key: they never execute and never replace that key's last valid translation, while valid sibling keys still merge. Empty strings remain intentional translations. Per-instance `.strings` overrides use the same validation, so `resolveLyraString()` always returns a string and malformed overrides fall through to the next valid tier. `getRegisteredLyraLocales(): readonly string[]` returns a fresh frozen list of every locale with strings registered via `registerLyraLocale()`, plus `'en'` (always available through the built-in English fallback), sorted, deduped and canonically spelled. `subscribeLyraLocaleRegistry(listener: () => void): () => void` fires when registry membership grows, including for a newly registered locale that is not active; extending an existing catalog does not change membership and does not fire it. `subscribeLyraLocale()` (on `@aceshooting/lyra-ui/utilities/localization.js`) instead fires when the active selection changes or a registration can alter the active locale's messages/direction, and filters unrelated registrations. Both return an idempotent unsubscribe. Delivery uses the eligible starting listener snapshots: one callback failure cannot prevent later active, component or registry listeners from running. State commits first, then the mutator throws one `AggregateError` containing all callback failures after delivery completes. `` is the built-in consumer of the registry subscription; see `llms/components/lr-locale-picker.md`. `getRegisteredLyraLocaleKeys(locale: string): readonly string[]` returns a frozen snapshot of exactly the keys `locale`'s own catalog carries — no BCP-47 fallback-chain widening and no merge with the built-in English defaults. This answers "what has this locale actually been given", distinct from `getRegisteredLyraLocales()`, which reports registry *membership* (which tags exist) rather than catalog *content*. Diff the result's length against `Object.keys(LYRA_DEFAULT_STRINGS).length` to measure a locale's own translation coverage without a silent English-fallback merge making a partial catalog look complete. A locale nothing ever registered — including `'en'` itself, unless it was explicitly passed to `registerLyraLocale()` — returns an empty snapshot. In development (Lit's own dev-mode signal; silent in production), resolving a message for a resolved locale other than English that has no override, no fallback, and no registered catalog entry for that key warns once per (locale, key) to `console.warn` before falling through to the English default — the same silent-fallback `resolveLyraString()`, `localize()`, and `resolveLyraScopedString()` all shared with no way to detect it before. It never fires for an English-resolved locale, since falling through to `defaults` is simply how English itself resolves. Gotcha: `localize()`'s optional second argument is a fallback string. Passing a defined literal there silently defeats a registered catalog — omit it, or pass `undefined`. ### Ready-made catalogs: `@aceshooting/lyra-ui/translations/.js` Twelve full catalogs ship with the package — **`ar`, `de`, `es`, `fa`, `fr`, `he`, `it`, `ja`, `pt-BR`, `ro`, `ru`, `zh-CN`** — each covering every key in `LYRA_DEFAULT_STRINGS`. They are **side-effect-only modules**: import one bare, read nothing from it, and it calls `registerLyraLocale()` for you. ```ts import "@aceshooting/lyra-ui/translations/de.js"; import "@aceshooting/lyra-ui/translations/ar.js"; // declares dir: 'rtl'; direction still comes from dir import "@aceshooting/lyra-ui/translations/fa.js"; // fa-IR falls back to this base catalog import "@aceshooting/lyra-ui/translations/he.js"; // he-IL falls back to this base catalog import "@aceshooting/lyra-ui/translations/it.js"; // Italian import "@aceshooting/lyra-ui/translations/pt-BR.js"; // also serves pt and pt-PT import "@aceshooting/lyra-ui/translations/ro.js"; // Romanian import "@aceshooting/lyra-ui/translations/zh-CN.js"; // also serves zh, zh-Hans and zh-Hans-CN ``` Persian and Hebrew use CLDR plural categories (`fa`: `one`/`other`; `he`: `one`/`two`/`other`); Italian uses a non-default set too (`it`: `one`/`many`/`other`), and so does Romanian (`ro`: `one`/`few`/`other`, where `few` covers `0` and `2`-`19` and `other` is the `de`-requiring form from `20` upward). `ar`, `fa` and `he` declare `dir: 'rtl'`, so `getLyraLocaleDirection()` answers for them (and for `ar-EG`, `fa-IR`, `he-IL`) — but locale selection still does not _force_ writing direction: set `dir="rtl"` on the page or an ancestor yourself. `pt-BR` and `zh-CN` are the only Portuguese and Chinese catalogs, and they are regional tags. Step 2 of the lookup order above is what makes them reachable from `lang="pt"`, `lang="pt-PT"`, `lang="zh"`, `lang="zh-Hans"` and `lang="zh-Hans-CN"` — no separate `pt`/`zh` alias registration is needed. They are still listed under their real tags in `getRegisteredLyraLocales()`. Import only the locales the application can actually offer — each is a separate module, so unimported ones cost nothing. A catalog registered this way is merged like any other, so a later `registerLyraLocale('de', { close: '…' })` still overrides individual keys, and a per-instance `.strings` still wins over both. Importing a catalog registers it; it does not _select_ it — `setLyraLocale()` or `` still chooses. What the import does do is make the locale show up in `getRegisteredLyraLocales()`, and therefore in ``, so the set you import is the set a user can switch between. ### Smaller catalogs: `@aceshooting/lyra-ui/translations//.js` Each locale above is also published as twelve smaller, side-effect-only **family slices** — one per component family (`agent-tools`, `charts`, `conversation`, `data`, `forms`, `layout`, `media`, `overlays`, `retrieval`, `utility`, `viewers`), plus `shared` for the handful of messages more than one family reaches (roving-focus/overlay/a11y strings like `collapse`, `open`, `search`). Import only the families the application actually renders instead of the whole-locale aggregate above: ```ts import "@aceshooting/lyra-ui/translations/fr/forms.js"; // lr-input, lr-select, lr-combobox, ... import "@aceshooting/lyra-ui/translations/fr/data.js"; // lr-table, lr-tree, lr-data-grid, ... import "@aceshooting/lyra-ui/translations/fr/shared.js"; // cross-cutting strings both families reach ``` `@aceshooting/lyra-ui/translations/fr.js` is unchanged: it is now a thin aggregate that imports every slice above, so the plain whole-locale import from the previous section keeps working exactly as before — this is a purely additive, opt-in way to shrink a non-English bundle, mirroring the per-component tree-shaking English defaults already get for free. A component's family is the directory it ships under (`src/components///`); when in doubt, import the aggregate and measure, or import `shared` alongside whichever family slices you do import so a cross-cutting string is never silently missing. ### Pluralized messages A message may be a plain string or a **`LyraPluralMessage`** — an object keyed by CLDR plural category, one string per category the language needs: ```ts import { registerLyraLocale } from "@aceshooting/lyra-ui/localization.js"; registerLyraLocale("en", { viewerSearchMatchCount: { one: "{count} match", other: "{count} matches" }, }); registerLyraLocale("ru", { viewerSearchMatchCount: { one: "{count} совпадение", few: "{count} совпадения", many: "{count} совпадений", other: "{count} совпадения", }, }); ``` - **The categories are `zero | one | two | few | many | other`** — the values `Intl.PluralRules.prototype.select()` can return. A language uses only the subset its grammar needs: English and German `one`/`other`, Russian `one`/`few`/`many`/`other`, Arabic all six, Japanese and Chinese only `other`. - **`other` is required.** It is the terminal step of the category fallback chain, so every selection is guaranteed to land on a real string. TypeScript enforces it; the remaining five keys are optional. A missing intermediate category widens to a grammatical neighbour before falling back to `other`. - **Selection is driven by `values.count`**, run through `Intl.PluralRules` at the component's effective locale — never at the locale the catalog was authored in, so an unregistered locale still pluralizes correctly against whatever strings it does have. - **`pluralCount` is the escape hatch for a pre-formatted count.** When `{count}` must render as locale-grouped text (`Intl.NumberFormat` output is a string, and `'1,024'` cannot select a category), pass the display string as `count` and the raw number as `pluralCount`. A non-finite or absent value selects `other`. **Breaking in 8.0.0:** a catalog that paired a singular key with a separate `Plural` key must be rewritten as one object-valued entry. The old spelling is not read, and nothing warns — the plural key becomes an unused entry and the singular renders for every count. Fold the pair into `{ one: …, other: … }` under the singular key's name. ## RTL and direction Direction is inherited from the platform `dir` cascade; locale/`lang` selection does not change it, and no component forces its own. Pair an RTL locale with `dir="rtl"` — ask `getLyraLocaleDirection(tag)` rather than hard-coding a list of RTL tags, and note that ``'s `lr-change` detail already carries the picked locale's `direction`, so applying it is `document.documentElement.dir = event.detail.direction`. Layout mirrors through CSS logical properties. Where physical math is unavoidable — drag ratios, arrow-key direction, anchored placement — components share one internal direction helper: `isRtl(el)` (used by `lr-multi-split`, `lr-time-range`, `lr-dock-panel`), plus `rtlAwareSide(side, el)` and `rtlAwarePlacement(placement, el)`, which swap the `left`/`right` component of a value under RTL and pass it through unchanged under LTR (`lr-menu` resolves its `placement` this way). These are implementation detail, not a published subpath — resolve direction in your own code with `getComputedStyle(el).direction`, which is the same answer through the same inheritance. Test both directions for anything with horizontal order, start/end placement, drag deltas, or previous/next navigation. ## Provider-neutral AI types: `@aceshooting/lyra-ui/ai` The agentic components share one vocabulary, exported as types from a dedicated subpath. Use these instead of hand-rolling state shapes — they bind field-for-field onto the components, with no adapter layer: ```ts import { createAgentStreamState, reduceAgentStream, adaptAiSdkMessage, AgUiStreamAdapter, adaptA2UiSurface, type AgentRun, type ChatMessage, type MessagePart, type RetrievalChunk, } from "@aceshooting/lyra-ui/ai"; ``` - **Run/step state** — `AgentStatusKind`, `AgentStatus`, `AgentStep`, `AgentRun` - **Conversation** — `ChatMessage`, ordered `MessagePart` variants, `ToolInvocation` - **Documents & grounding** — `DocumentRef`, `Citation`, `RetrievalQuery`, `RetrievalChunk`, `RetrievalScoreBreakdown`, `GroundedClaim`, `GroundingAssessment`, `DocumentLocator` - **Streaming runtime** — `AgentStreamEvent`, `AgentStreamState`, `createAgentStreamState()`, `reduceAgentStream()`, `reduceAgentStreamEvents()`; every event carries a monotonic `generation` and strictly increasing `sequence`. A newer generation atomically retires the previous run, while an older or replayed cursor is ignored. JSON Patch input is structurally validated and rejects prototype-mutating paths. - **Protocol adapters** — `adaptAiSdkMessage()`, `AgUiStreamAdapter`, and `adaptA2UiSurface()` map structural provider messages, events, and documents onto the neutral runtime without pulling vendor SDKs into the package. Feed AG-UI events through `new AgUiStreamAdapter().push(event)`. Adapter inputs are treated as untrusted data, recursively snapshotted, and rejected without throwing when malformed, non-serializable, or over their configured budgets. - **Resource limits** — pass a partial `AgentStreamLimits` to `createAgentStreamState()`, or adapter limit options to the relevant adapter. The exported `DEFAULT_AGENT_STREAM_LIMITS`, `DEFAULT_AI_SDK_ADAPTER_LIMITS`, `DEFAULT_AG_UI_ADAPTER_LIMITS`, and `DEFAULT_A2UI_ADAPTER_LIMITS` constants document the defaults; invalid limit values fall back to them. Runtime breaches become an explicit error state/event, while snapshot/A2UI rejection returns `null` or an empty adapter result as documented by that boundary. - **Message-part invariants** — `MessagePartState` is transport-only (`'streaming' | 'complete'`): failures use an `ErrorMessagePart` or the owning domain error. Tool results are a success/error union, and a `type: 'data'` part contains exactly one of `data` or `widget`. - **Event payloads** — `RunLifecycleEventDetail`, `RetrievalProgressEventDetail`, `CitationSelectEventDetail`, `ToolApprovalEventDetail`, `CancelEventDetail`, `RetryEventDetail`, `ExportEventDetail` These structural, provider-agnostic types correspond to the component inputs above; map a vendor payload onto them at the edge. ### Task-first AI composition guide - **Render one model response:** `lr-message-parts`; use `lr-chat-message` only when the message shell (avatar, author, actions) is also needed. - **Build the main prompt affordance:** `lr-prompt-input`; it already composes attachments, model/voice/source controls, mentions/commands, and `lr-prompt-queue`. - **Run an agent workspace:** `lr-agent-workspace` + `reduceAgentStream()`; add `lr-subagent-panel` for nested runs and `lr-mcp-app` only for executable MCP App resources. - **Show grounded output:** `lr-rag-answer`; pass claim records for `lr-claim-evidence`, use `lr-retrieval-compare` for retrieval tuning and `lr-rag-eval-dashboard` for run metrics. - **Develop prompts/tools:** `lr-prompt-studio` and `lr-json-schema-viewer`. - **Build voice sessions:** `lr-realtime-session`; it composes the existing audio visualizer, push-to-talk control, and transcript feed while leaving transport ownership with the host. Family registration entry points are additive: importing `@aceshooting/lyra-ui/components/conversation`, `/agent-tools`, or `/retrieval` registers and exports that complete family. Granular component entry points remain the smallest bundles and are preferred in production. ## Optional peer dependencies All 29 peers are optional, in two groups. The 26 component-facing peers remain outside the default install; components load them on demand where applicable. React, Svelte, and Vue are compile-time-only peers for their matching opt-in declaration entries (`custom-elements-jsx`, `svelte`, and `vue`): those entries emit empty JavaScript, no component loads a framework, and Lyra ships no runtime wrapper. `llms/peers.md` is the generated peer-role/component table. Loading and failure UI for component peers is component-specific: viewer sections document their localized loading/error/notice states, while `lr-include` preserves its light-DOM fallback/live region and emits `lr-include-error` when its sanitizer is unavailable. Some components additionally issue a deduped warning. Consult the owning component section instead of assuming every peer user renders an `` or the same degraded state. `lr-phone-input` is the exception to dynamic peer import: it takes a consumer-built adapter (`loadLibphonenumberAdapter()`) rather than importing `libphonenumber-js` itself. ## Framework integration Plain custom elements, so they work anywhere — with the usual two caveats. React 19/JSX, Vue 3, and Svelte 5 projects can opt into the generated declarations shown under "TypeScript" without installing or shipping a wrapper; import the normal granular registration entry separately. - **Complex values must be property-bound, not attribute-bound.** An attribute stringifies: `rows="[object Object]"`. Use the framework's property syntax for anything that isn't a string, number, or boolean: Lit `.rows=${rows}`, Vue `:rows.prop="rows"` (or `.rows="rows"`), Angular `[rows]="rows"`, Svelte `bind:this` + assignment, React 19+ passes objects to custom-element properties natively (earlier React needs a ref). - **Events are dashed custom events.** Lit `@lr-change=${…}`, React 19 `onlr-change={…}`, Vue `@lr-change="…"`, Angular `(lr-change)="…"`, and Svelte 5 `onlr-change={…}` (or the legacy `on:lr-change={…}`). Earlier React versions use `ref.addEventListener('lr-change', …)`. - **Angular** additionally needs `CUSTOM_ELEMENTS_SCHEMA` in the module/component that uses the tags. - In-DOM templates lower-case attribute names; camelCase property names only survive in framework templates and JS, never in hand-written HTML attributes. - **Dev-mode unknown-attribute diagnostics ignore framework-owned scoping/debug attributes.** Angular's default emulated view encapsulation writes `_ngcontent-*`/`_nghost-*` scoping markers onto every element it manages, its dev builds add `ng-reflect-*` input reflections and `ng-version`, and Vue's scoped styles add `data-v-*` — none of these trigger the unknown-attribute warning. A genuinely misspelled or unsupported attribute still warns. ## SSR and declarative shadow DOM Root, `all.js`, and granular component imports are server-safe under Node 20+. `@aceshooting/lyra-ui/ssr/all.js` is the **server-only** convenience entry: unlike the browser `all.js`, it registers the complete inventory including the optional-peer families — defining those tags never imports their peers (each component loads its own lazily, client-side), and the browser-bundle argument for excluding them does not apply to a server render. Use the public `@aceshooting/lyra-ui/ssr-loader.js` entry for the tested Lit SSR contract. Its exported `LYRA_SSR_SUPPORT_MATRIX`, `LYRA_SSR_RENDER_AND_HYDRATE_TAGS`, and `LYRA_SSR_CLIENT_RENDER_TAGS` classify every inventory tag exactly once: - `render-and-hydrate`: `@lit-labs/ssr` emits Declarative Shadow DOM, and the client reuses the existing shadow root and nodes. - `client-render`: the server emits the host's serializable attributes and light DOM with no shadow template; the component renders when its definition upgrades in the browser. Use this for initial renders that require light-DOM traversal, layout, canvas, observers, media, or other browser APIs. Server setup (the fallback must precede Lit's renderer): ```ts import { lyraSsrElementRenderers } from "@aceshooting/lyra-ui/ssr-loader.js"; import { render, LitElementRenderer } from "@lit-labs/ssr"; import { html } from "lit"; const result = render(html`
Dashboard
`, { elementRenderers: lyraSsrElementRenderers(LitElementRenderer), }); ``` In the browser, import `@aceshooting/lyra-ui/ssr-loader.js` before any other module that can import Lit. This installs `@lit-labs/ssr-client/lit-element-hydrate-support.js` before component registration. `getLyraSsrMode(tagName)` reads one tag's tier, and `diagnoseLyraHydration(document)` inspects current Lyra hosts in the supplied tree and every reachable open shadow root, awaits registered hosts' current updates, and reports `ready`, `unregistered`, `missing-shadow-root`, or `update-failed`. `ready` means the current update finished and a shadow root exists; it does not claim that server markup was observed or hydrated. Read each diagnostic's separate `mode` to distinguish the declared `render-and-hydrate` and `client-render` tiers. The loader preserves optional-peer isolation: import a root-excluded component's granular registration after the loader. A fallback cannot serialize JS property bindings, so put initial server state in attributes/light DOM or assign it client-side. A `render-and-hydrate` component whose rendering depends on something only a browser can answer — its own light-DOM children, or a browser global such as `EyeDropper` — reproduces the server's answer on the hydrating render and corrects itself on the next update, so a slotted override lands one frame after hydration; a browser-only mount is unaffected and renders the final result the first time. Layout/observer/canvas/media work begins after hydration, and remote content is client-only. **If hydration never runs at all** — a page that intentionally ships no script bundle, a reader that fetches the raw HTML, or a crawler — a `render-and-hydrate` tag's declarative-shadow-DOM output is either permanently complete and correct on its own, or only ever promised once hydration executes; nothing in between is supported. `getLyraSsrStaticSafety(tagName)` (also `LYRA_SSR_SUPPORT_MATRIX.declarativeShadowDom.staticSafety[tagName]`) returns which: `'static-safe'` is the first guarantee, `'hydration-required'` is the second. Every `render-and-hydrate` tag has one of the two; `pnpm test:ssr` fails closed on any tag that does not, so there is no silent third case. Do not infer `'static-safe'` from a component "looking static" — check the classification, because two components that look equally static can differ: - `lr-details` is `'static-safe'`. Its native `
`/`` toggle is bound with a Lit *property* binding (`.open=${this.open}`), which the SSR renderer reflects to a real `open` attribute in the server markup, and the click listener that vetoes a user toggle is registered only by the component's own (unexecuted, without hydration) lifecycle code. With hydration JS never running, both facts together mean the browser's native disclosure behavior is what answers a click — so `lr-details` renders in the correct open/closed state and keeps expanding and collapsing indefinitely, with zero JavaScript. - A canvas-painted component such as `lr-chart` is `'hydration-required'`: a `` element has no pixels until script draws to it, so without hydration the server markup is present but visibly empty forever, not merely stale for one frame. `lr-lite-chart` is also `'hydration-required'`, but for a different reason: it renders real SVG (not canvas), and its default `layout="fit"` mode needs a ResizeObserver-measured host size before it can draw coordinate-based geometry, so without hydration it stays on its structurally-identical pre-measurement fallback rather than ever reaching final content. ## Testing form-associated components: `@aceshooting/lyra-ui/testing` `@aceshooting/lyra-ui/testing` exports `installHappyDomFormAssociatedShims(): void` for test environments without usable `ElementInternals`. In that environment, form participation is inert while components retain local validity and state behavior. Install the helper once before assertions that need form-value or validity behavior: it supplies `setFormValue()`, `setValidity()`, `checkValidity()`, `reportValidity()`, and readonly `form`/`labels`/`validity`/`validationMessage`/`willValidate`. The shim is a no-op where the platform already provides internals, so it is safe in a shared setup file. ## Constructing a validated test event: `createLyraEvent()` `@aceshooting/lyra-ui/testing` also exports `createLyraEvent(tag, name, detail?): CustomEvent`, for a downstream suite that wants to dispatch one specific `lr-*` component's documented event — at a listener under test, without rendering the real component — instead of hand-rolling a `CustomEvent` and guessing its shape and flags: ```ts import { createLyraEvent } from '@aceshooting/lyra-ui/testing'; const event = createLyraEvent('lr-confirm-bar', 'lr-approve', { args: null, waitUntil: () => {} }); event.cancelable; // true — lr-confirm-bar's own lr-approve call site is cancelable target.dispatchEvent(event); ``` `tag` and `name` are checked against that component's own generated event map: an event name the tag does not document, an unregistered tag, or a `detail` of the wrong shape are all compile errors. `bubbles` and `composed` are always `true` (every `lr-*` event is), and `cancelable` is looked up per tag and event from the component's own documented contract, so a listener that calls `preventDefault()` on a genuinely cancelable event is actually exercised — the exact case a hand-built event with a guessed `cancelable` value silently skips. `detail` is always optional, even where the real component's own type makes it required, so a test that only cares about the dispatched event's flags does not have to fabricate a realistic one; an omitted or `undefined` detail becomes `null`, matching `LyraElement.emit()`'s own normalization. `createLyraEvent()` builds the event only — dispatch it yourself with `target.dispatchEvent(event)`. Scope: covers every `lr-*`-named event a component documents. A component's native-named re-emits (`input`, `change`, `blur`, `focus`, ...) already have real DOM event types and dispatch semantics of their own that this factory does not model. ## Driving a component's real activation path: interaction drivers For the exact gap `createLyraEvent()` leaves open — choosing an option, submitting a confirm decision, toggling a switch, activating a step — `@aceshooting/lyra-ui/testing` exports a small set of typed interaction drivers, one per interaction, that go through the real component's own activation path (its own shadow-part lookup and `.click()`, the same as its own tests) instead of a downstream suite reverse-engineering internal detail shapes or shadow-part selectors itself: ```ts import { chooseOption, submitConfirmDecision, toggleSwitch, activateStep, } from '@aceshooting/lyra-ui/testing'; await chooseOption(combobox, 'banana'); // opens the listbox, clicks the matching [part="option"] row await submitConfirmDecision(confirmBar, 'approved'); // clicks [part="approve-button"] await toggleSwitch(switchEl); // calls switchEl.click(), lr-switch's own activation path await activateStep(stepper, 'review'); // clicks the [part="step"] button for that stepId (or pass an index) ``` `chooseOption()` accepts any of ``, ``, ``, `` and `` — every component that independently implements the same `[part="option"]` row plus `data-value` delegated-click pattern for its own listbox/popup. `submitConfirmDecision()` accepts either `` or ``, which render the identical `[part="approve-button"]`/`[part="deny-button"]` pair for the same `lr-approve`/`lr-deny` contract. Each driver is `async` and awaits `updateComplete` before returning, so assertions written immediately after it see fully-settled DOM/state. Each one also throws a plain `Error` — never a silent no-op — when the requested interaction cannot actually happen: the target is disabled, the option owner/stepper is read-only or has no matching row/step currently rendered, or the confirm bar is already decided. That is the exact failure mode a hand-rolled event misses (for example dispatching a confirm event without `cancelable`, which makes a `preventDefault()`-based pending-state handler a silent no-op that still looks tested) — these drivers exercise the component for real, so a precondition that blocks the interaction is surfaced as a thrown error instead of quietly doing nothing. Pure DOM operations only (`Element.click()`, shadow-part queries, public properties) — no `@web/test-runner`/CDP-only helper, so they also run under a downstream suite's own happy-dom/jsdom environment, not only a real browser. Scope: one driver per interaction named above. Not a general "drive any component" toolkit — render the real component and interact with it directly for anything else. ## Awaiting a lazily registered mount: `waitForLyraElement()` and `waitForToast()` An imperative API can register its elements lazily -- `toast()` dynamically `import()`s ``/`` on first call (a deliberate bundle-size trade: importing the package root, or even `toast()` itself, never pulls the element classes into an eagerly loaded bundle). A fire-and-forget `toast(...)` call -- the normal application pattern, since a component should not block its own flow on a toast -- therefore leaves the document empty for at least one microtask after the call returns. `@aceshooting/lyra-ui/testing` exports `waitForLyraElement()` for this shape in general, plus `waitForToast()` as the named convenience for `toast()` specifically: ```ts import { waitForToast } from '@aceshooting/lyra-ui/testing'; toast('Saved'); // fire-and-forget; toast.class.js/toast-item.class.js may still be importing const item = await waitForToast('Saved'); // resolves once a matching mounts expect(item.textContent?.trim()).to.equal('Saved'); ``` `waitForToast(match?, options?)` resolves once a `` is connected and upgraded. A string `match` compares against the item's trimmed `textContent` (what `toast('Saved')` sets verbatim); pass a predicate — `(item: LyraToastItem) => boolean` — for anything else (a substring, an icon/action check, a specific variant); omitting `match` resolves the first toast item to mount. The underlying `waitForLyraElement(selector, options?)` is generic over any element reachable from `options.root` (`document` by default): it resolves once an element matching `selector` is both connected and upgraded — registered with a constructor the element is actually an instance of — filtered further by an optional `options.match: (element: T) => boolean`. An element already present in markup before its class registers (the SSR/hydration case) is not a match until it upgrades. Both resolve via `MutationObserver` (new elements arriving) and `customElements.whenDefined()` (an already-connected-but-undefined element finishing registration) rather than polling on a timer, and both reject with an Error describing the selector, the timeout, and how many non-matching candidates were found — after a bounded `options.timeoutMs` (2000ms default). Every underlying API is standard DOM/HTML with no `@web/test-runner`/CDP dependency, so both also run under a downstream suite's own happy-dom environment, not only a real browser. Scope: awaiting a lazy mount reachable from a root you already have a handle to. Not a replacement for `updateComplete` (a mounted element may still have a pending render) or for the interaction drivers above (already-mounted components' own activation paths). `toast()` is currently the only imperative `lyra-ui` API that registers its elements through a dynamic `import()`; `confirm()` registers `` synchronously (a static import plus an idempotent `defineElement()` call) and mounts its transient dialog before returning, so it has no equivalent gap. ## happy-dom's custom-property resolver and host-to-part token forwarding In 16.0.0, seven built-in controls that each compose a real `` for their icon-only action — ``, `` (whose close button is inherited by ``), ``, ``, ``, `` (shared by ``), and `` — captured the composed control's public `--lr-icon-button-*` tokens on their own `:host` and forwarded that private token back onto the SAME public token name on the `[part]` rendering the composed control, so that an ancestor theme override still reached the composed child instead of being shadowed by the component's own default. That was legal under the CSS Custom Properties spec — `:host` and `[part]` resolve on different elements, so a real browser resolves the host declaration to a concrete value first and the part substitutes that, and per-element cycle detection never fired — but **happy-dom does not model that element boundary**. Its `CSSComputedStyle` merges ancestor and own-element custom properties into a single flat map with no notion of which element declared what, and (at least through 20.14.5, the newest release at time of writing) `CSSVariableFormatter.resolveVariables` substitutes into that map recursively with no visited set and no depth cap — so the capture-and-forward pair resolved into each other forever, throwing an unhandled `RangeError: Maximum call stack size exceeded` from `CSSVariableFormatter.resolveVariables` on every render of any of the seven components. Every test still reported as passing — there was no failing assertion to point at — but the runner counted the unhandled errors and exited non-zero anyway, which read as unrelated flakiness rather than a CSS issue. **Current versions are unaffected.** `` now carries a private `--_lr-icon-button--default` fallback tier for every paint token (background, color, border, and their hover/active variants — the same shape its corner radius already used via `--_lr-icon-button-radius-default`), and each composing component sets its own default directly on that private tier rather than re-declaring the public token name. ``'s own stylesheet still checks the public token first, so an ancestor override reaches a composed control exactly as before, but no descendant declares a public `--lr-icon-button-*` token from a private token that was itself derived from that same public token — so no resolver, scoped or flattened, ever sees a cycle. A project still hitting the `RangeError` above should upgrade `@aceshooting/lyra-ui`; the workarounds that version range needed (patching or upgrading the DOM implementation to cycle-aware/depth-limited custom-property resolution, or running the affected suites against a real browser engine) are no longer necessary once it does. ## Accessibility contract Semantic roles live on the shadow-DOM element that owns them, with explicit false states for toggle/selection/expansion ARIA attributes and deliberate host-name forwarding. Form-associated controls preserve `ElementInternals`, reset, validity, focus, and native editing behavior. Reusable layouts respond to their allocated container rather than the viewport, and decorative or infinite motion simplifies under `prefers-reduced-motion: reduce`. Keyboard model: a composite widget (menu, tab group, tree, table, calendar, carousel, segmented control) is a single tab stop using a roving `tabindex`; arrow keys move within it and skip disabled, hidden, `aria-hidden` and `inert` items; `Home`/`End` jump to the ends; `Enter` and `Space` both activate; `Escape` dismisses the topmost dismissible overlay and returns focus to whatever opened it. `ArrowLeft`/`ArrowRight` mean previous/next and swap under `dir="rtl"`. **`accessibleLabel` binds to one of two different attributes depending on the component, and the two are not interchangeable.** Most components (e.g. `lr-card`, `lr-stat`) expose it as a direct alias of the native `aria-label` attribute — `accessible-label="…"` does nothing on those; write `aria-label="…"` instead. A minority (e.g. `lr-callout`, `lr-table`) separately compute an internal accessible name and expose `accessibleLabel` through the bespoke `accessible-label` attribute instead, specifically so the host's own native `aria-label` can still independently override that computed name. Check the component's own reference section (or its class JSDoc) for which convention it uses before writing markup — an `accessible-label` attribute on an `aria-label`-only component is a silent no-op, not an error. Lyra UI does not make a formal assistive-technology conformance claim or provide a VPAT. For the documented accessibility scope and how to report an accessibility issue, see . ## Editor and tooling integration The published package ships machine-readable metadata for editors: `custom-elements.json` (Custom Elements Manifest), `web-types.json` (JetBrains, zero-config), and `vscode-html-data.json` / `vscode-css-data.json` (point `html.customData` / `css.customData` at them in `.vscode/settings.json`). For an agent, `llms/components/.md` is the cheaper source; these files matter when scaffolding a project's editor configuration. Build tools can import the manifest through the explicit `@aceshooting/lyra-ui/custom-elements.json` package export; native Node ESM uses `with { type: 'json' }` on that import. **Inherited members in `custom-elements.json`.** A subclass declaration's `cssParts` array is always the complete flattened set, including everything inherited from a superclass — `::part()` has no type-level inheritance chain a consumer could otherwise walk. `attributes`, `members`, `events`, `slots`, and `cssProperties` are the opposite: a subclass declaration lists only its own entries plus any inherited entry it explicitly overrides, pruning an inherited-and-unmodified entry of those four kinds. This is intentional, not a gap — a TypeScript/JS consumer already gets the full set for free from the generated `.d.ts` `extends` chain, the same resolution `web-types.json` and `vscode-html-data.json` perform ahead of time. A consumer reading `custom-elements.json` directly and needing the complete `attributes`/`members`/`events`/`slots`/`cssProperties` set for those four kinds must walk the declaration's own `superclass.name`/`superclass.module` across `modules[].declarations[]` itself, or read `web-types.json`/`vscode-html-data.json` instead, which are already fully resolved. **Which tags an entry registers, and which message keys it can reach (`registrations.json`).** A stable per-tag entry (`@aceshooting/lyra-ui/components/lr-.js`) can, at import time, define more than one custom element: importing `lr-table.js` also registers ``, ``, `` and ``, because `lr-table`'s registration entry imports those composed children's own registration entries before defining `` itself. `custom-elements.json` declares one custom element per family source module, with no field for a stable per-tag entry specifier and none for the extra tags importing it registers as a side effect. For that, read the generated `@aceshooting/lyra-ui/registrations.json` instead (`schemaVersion: 1`): `{ entries: [{ tag, entry, registrationModule, distModule, registers, localeKeys }], integrations: [{ entry, registrationModule, distModule, registers, localeKeys }] }`. Every `entries` row describes the stable per-tag alias above and always carries `tag`. `integrations` lists the published integration-bridge specifiers, which install an integration — an optional-peer resolver, a lazy document-format registrar — without being any single component's own alias, so they carry no `tag`: `components/media/flag/flag-peer.js`, `components/viewers/archive-viewer/archive-viewer-register.js` and `components/viewers/ebook-viewer/ebook-viewer-register.js`, the three imports a per-tag alias cannot stand in for. They are a separate array rather than tag-less rows mixed into `entries`, so a reader that keys `entries` by `tag` keeps working; `integrations`, `distModule` and `localeKeys` are all additive, which is why the schema version is unchanged. `registrationModule` is the `src/` path used internally; `distModule` is that same module's own published deep specifier (e.g. `./components/data/table/table.js`) so a caller holding either a per-tag alias or a deep import can resolve the other without reading `src/` or walking `dist/` — for an integration bridge, `distModule` equals `entry`, since there is no separate alias. `registers` is every `lr-*` tag importing `entry` defines, direct or transitive, derived from the same transitive-import analysis `scripts/check-component-dependencies.mjs` already performs against the real registration graph (not a second hand-maintained list). `localeKeys` is every `LyraMessageKey` the registered tags can reach — including a key reached only through an indirect lookup table (e.g. `lr-attachment-trigger`'s `{ triggerKey: 'attachmentTriggerFiles' }`-shaped map), because it reuses `generate-default-string-slices.mjs`'s own reachability walk rather than a literal-`localize()`-only scan. All of it is regenerated by `pnpm run registration-graph`. ## Independence and migration Lyra has no runtime, theme, or design-token dependency on Shoelace or Web Awesome. Documented `wa-*` comparisons are migration references only; Lyra's own tokens, events, localization runtime, and implementation are the source of truth. `llms/migration.md` holds the generated per-tag `exact`/`rewritten`/`warning-required`/`conceptual-only`/`unsupported` decision and every declared member/default/import rewrite. Only the first two classifications are automatic; use the codemod's location-aware report rather than treating a README relationship as a rename allowlist. Security-motivated differences remain explicit. `lr-include` sanitizes every fragment, omits a script-executing mode, and defaults to same-origin fetches. Its post-sanitization transclusion is network-silent and non-interactive: anchors remain only for resolvable same-document `#fragment` links (rebased per include instance); every other navigation or resource attribute, including `href`, `src`, `srcset`, `action`, `ping`, and `poster`, is stripped, so images do not load. Form-control and custom-element wrappers are unwrapped to safe ordinary text or children, while controls with no passive content are removed. Link-like controls strip `opener` and force-add `noopener noreferrer` whenever `target` is set while preserving other settable author tokens where the component exposes `rel`; iframe/media/viewer inputs keep their URL validation, sandbox, size caps, and generation guards. A use that depends on weaker behavior is left unchanged with a warning. For a staged theme migration, map existing values onto `--lr-theme-*` explicitly in application CSS rather than expecting an implicit compatibility layer. ## Family barrels Each of the eleven component families has an entry point that registers every element in it: `@aceshooting/lyra-ui/components/forms`, `.../components/overlays`, and so on for `agent-tools`, `charts`, `conversation`, `data`, `layout`, `media`, `retrieval`, `utility` and `viewers`. They existed for three families before 8.0.0, were in the exports map, and were documented nowhere — so they were effectively unreachable. All eleven now exist and are listed here. A family barrel is **side-effectful by design**: importing it registers every tag in that family, the same way `all.js` registers all of them (the package root itself registers nothing as of 8.0.0 — see "Importing and registering components"). Reach for one when you genuinely use most of a family and want a single import; reach for the granular `@aceshooting/lyra-ui/components/.js` path when you do not, because a barrel cannot be tree-shaken down to the two elements you actually render. The tag-shaped path remains stable if a component moves between Lyra's internal family folders; existing family-shaped registration paths remain supported. ```js import "@aceshooting/lyra-ui/components/forms"; // every form control import "@aceshooting/lyra-ui/components/lr-input.js"; // just ``` --- ## Shared helpers: `utilities/` Not custom elements — infrastructure the components compose, curated into a supported public surface. Importable one module per helper, e.g. `@aceshooting/lyra-ui/utilities/positioner.js` — the `.js` is required — or as a whole from the extensionless `@aceshooting/lyra-ui/utilities`. **This replaced `@aceshooting/lyra-ui/internal/*` in 8.0.0. `@aceshooting/lyra-ui/internal/*` is no longer a published subpath** — the import fails to resolve rather than degrading. The whole `internal/` tree used to be exported with no stability statement, which made every internal refactor potentially breaking for someone. The helpers below are the supported surface and are covered by semver. Each subpath and the extensionless barrel use explicit named exports, so an implementation helper added to `internal/` does not silently become public. Rewriting an existing supported import is a path swap and nothing more: ```ts // before 8.0.0 import { place } from "@aceshooting/lyra-ui/internal/positioner.js"; // 8.0.0 import { place } from "@aceshooting/lyra-ui/utilities/positioner.js"; ``` If you were importing something from `internal/` that is not listed below, it was never a supported entry point. After getting the user's explicit agreement, submit a capability request through the feature-request API described in "When no component fits" so it can be promoted deliberately. - **`LyraElement`** — the base class. `static styles = [tokens]`; subclasses prepend `LyraElement.styles` to their own `static styles`. Supplies `emit()` (see "Events"), the typed `addEventListener` overload (see "TypeScript"), `locale`/`strings` (see "Localization"), and protected `localize()` / `effectiveLocale` / `effectiveDirection`, all memoized once per update cycle. `LyraEmitOptions { cancelable?: boolean }` is the public options object accepted by `emit()`; set `cancelable` only for a real, branch-on-veto operation. - **`catalog` → `LyraCatalogEntry` and `LyraCatalog`** — type-only shared vocabulary for model, voice, and future catalog-backed controls. Import it from `@aceshooting/lyra-ui/utilities/catalog.js`; both types are also available from the package root and the model-select/voice-picker granular entries. The exact contracts are `LyraCatalogEntry { id: string; label: string }` and `LyraCatalog = readonly string[] | readonly T[]`. A catalog is homogeneous: use string shorthand (the same string becomes both id and label) or typed object rows, never a mixed array. Readonly tuples and arrays are accepted, and object catalogs retain an extended row type such as `LyraModelCatalogEntry` or `LyraVoiceCatalogEntry`. - **`anchor-target` → `LyraAnchorTarget` and `LyraAnchorTargetEventMap`** — type-only structural contracts for viewers that expose Lyra's shared document-anchor surface. Import them from `@aceshooting/lyra-ui/utilities/anchor-target.js` when a host, adapter, or external viewer needs to accept or implement `highlights`, `activeHighlightId`, `anchor`, `anchorKinds`, and `scrollToAnchor(target)` without importing the internal mixin. The event map types the shared `lr-highlight-activate`, `lr-text-select`, and `lr-anchor-result` listener vocabulary; a concrete viewer can support only the events it actually emits, as documented on that component. The exact structural contract is `LyraAnchorTarget { highlights: readonly LyraHighlight[]; activeHighlightId: string | null; anchor: LyraAnchor | string | null; readonly anchorKinds: readonly LyraAnchorKind[]; scrollToAnchor(target: LyraAnchor | string): Promise }`. Assigning `highlights` synchronously creates a frozen array of copied, frozen highlight records; malformed records without a non-empty string `anchor.kind` are omitted, while each accepted record's otherwise-opaque `anchor` retains caller identity so reference-based anchor jumps still work. - **`positioner` → `place(anchor, popup, opts?): () => void`, `trackRect(target, onUpdate(rect)): () => void`, and `virtualAnchorFromRect()`** — thin wrapper over `@floating-ui/dom`'s `computePosition` + `autoUpdate`. Forces `strategy: 'fixed'` (matching the popup's own `position: fixed` CSS — otherwise it lands offset by the page scroll), middleware `offset(opts.offset ?? 4)`, `flip()`, `shift({ padding: 8 })`, default `placement: 'bottom-start'`. Returns a cleanup function that stops the `autoUpdate` loop — call it in `disconnectedCallback()`. `trackRect()` reports the target's initial viewport rect exactly once before returning, follows later layout/viewport changes, and returns the same cleanup shape. `virtualAnchorFromRect()` adapts a live rectangle provider to the exported `VirtualAnchor` contract; `PlaceOptions` and its closed-set placement/sizing types are exported for typed wrappers. The structural result is `PlacementResult { placement: Placement; arrow?: { x?: number; y?: number } }`, where `arrow` contains its resolved coordinates when supplied. `VirtualAnchor` exposes `getBoundingClientRect()` and optional `contextElement`; `virtualAnchorFromRect({ x, y, width?, height?, contextElement? })` builds one. `PlaceOptions` exposes `placement`, `strategy`, `offset`, `skidding`, `boundary`, `flip`, `flipFallbackPlacements`, `flipFallbackStrategy`, `flipBoundary`, `flipPadding`, `shift`, `shiftBoundary`, `shiftPadding`, `padding`, `autoSize`, `autoSizeBoundary`, `autoSizePadding`, `sync`, `arrow`, `arrowPadding`, `hoverBridge`, and `onPlaced(result)`. Exact authoring contracts are `VirtualAnchor { getBoundingClientRect(): DOMRect; contextElement?: Element }`, `virtualAnchorFromRect(rect: { x: number; y: number; width?: number; height?: number; contextElement?: Element }): VirtualAnchor`, and `PlaceOptions { placement?: Placement; strategy?: PlaceStrategy; offset?: number; skidding?: number; boundary?: PlaceBoundary; flip?: boolean; flipFallbackPlacements?: Placement[]; flipFallbackStrategy?: PlaceFlipFallbackStrategy; flipBoundary?: PlaceBoundary; flipPadding?: number; shift?: boolean; shiftBoundary?: PlaceBoundary; shiftPadding?: number; padding?: number; autoSize?: PlaceAutoSize; autoSizeBoundary?: PlaceBoundary; autoSizePadding?: number; sync?: PlaceSync; arrow?: HTMLElement; arrowPadding?: number; hoverBridge?: HTMLElement; onPlaced?: (result: PlacementResult) => void }`. `place()` and `virtualAnchorFromRect()` throw `RangeError` before registering observers, invoking callbacks, or writing styles when coordinates/options are non-finite, dimensions or padding are negative; finite signed `offset`/`skidding` stay valid. If a previously valid live anchor later returns invalid geometry, positioning stops and rolls back without publishing partial state or a placement callback. Used by `lr-combobox`, `lr-select`, `lr-date-input`, `lr-export-button`, `lr-model-select`, `lr-mention-popover`, `lr-tool-call-chip`, `lr-citation-badge`, and `lr-menu`. - **`prefix`** — `LYRA_PREFIX = 'lr'`; `tag(name)` → `` `lr-${name}` ``; `defineElement(name, ctor)`, an idempotent `customElements.define` that is safe if a module is evaluated twice. Exact callable contracts are `tag(name: string): string` and `defineElement(name: string, ctor: CustomElementConstructor): void`. - **`a11y`** — `nextId(scope)`, a monotonic id generator (`nextId('combobox-list')` → `"lr-combobox-list-3"`); `srOnly`, a visually-hidden-but-AT-visible class. The callable contract is `nextId(scope: string): string`. - **`icons`** — the shared inline-SVG set (`calendarIcon`, `chevronIcon`, `closeIcon`, `expandIcon`, `eyeIcon`, `eyeOffIcon`, `fileIcon`, `folderIcon`, `pauseIcon`, `playIcon`, and `spinnerIcon`). One 24×24 viewBox per icon, rendered at `1em` so each inherits the caller's font size; none bakes in a direction — callers rotate the wrapping `part` via CSS. Each is a zero-argument template factory: `calendarIcon(): SVGTemplateResult`, `chevronIcon(): SVGTemplateResult`, `closeIcon(): SVGTemplateResult`, `expandIcon(): SVGTemplateResult`, `eyeIcon(): SVGTemplateResult`, `eyeOffIcon(): SVGTemplateResult`, `fileIcon(): SVGTemplateResult`, `folderIcon(): SVGTemplateResult`, `pauseIcon(): SVGTemplateResult`, `playIcon(): SVGTemplateResult`, and `spinnerIcon(): SVGTemplateResult`. - **`scroll-lock` → `lockScroll(doc = document): () => void`** — ref-counted `doc.documentElement` scroll lock (used by `lr-widget`'s fullscreen mode); safe to acquire/release concurrently, restores the original `overflow` only when the last lock releases. - **`form-associated` → `FormAssociated(Base)`**, plus `attachInternalsSafely()` and `createFallbackInternals()` — the mixin documented under "Form association" above, exposed so an application can build its **own** form-associated control alongside Lyra's and have it participate in a form, restore on reset, and report validity the same way every `lr-` control does. Reach for it instead of hand-rolling `attachInternals()` when a bespoke control has to sit in the same `
` as these. `attachInternalsSafely()` inspects own and inherited data descriptors without reading accessors, calls an eligible implementation once, and returns the fallback internals for a missing, accessor-backed, non-callable, or throwing capability. The free-function signatures are `attachInternalsSafely(host)`, `createStringArrayFormDataState(name, values)`, `readStringArrayFormDataState(state)`, and `FormAssociated(Base, valueAdapter?)`. For a non-string value, pass a typed `FormValueAdapter` as that `valueAdapter`; `FormSubmissionValue`, `createStringArrayFormDataState()`, `readStringArrayFormDataState()`, `isEmptyFormValue()`, and `stringFormValueAdapter` are the retained adapter-building seams. Form-owner mutation, validity barring, anchor installation, and other mixin implementation helpers are deliberately not exported. `FormValueAdapter` has readonly `empty`, required `toFormValue(value)`, and optional `toFormState(value)`, `isEmpty(value)`, `fromAttribute(attribute)`, `toAttribute(value)`, and `fromFormState(state)`. `FormAssociatedInterface` exposes `internals`; the `name` getter/setter (`next`); `value`; `defaultValue`; `customError`; `disabled`; `required`; readonly `effectiveDisabled`; the `form` getter/setter (`owner`); readonly `labels`, `validity`, `validationMessage`, and `willValidate`; `setFormValue(next)`; `getForm()`; `checkValidity()`; `reportValidity()`; `setCustomValidity(message)`; `resetValidity()`; `formResetCallback()`; and `formStateRestoreCallback(state, reason)`. The exact callable signatures are `attachInternalsSafely(host: HTMLElement): ElementInternals`, `createFallbackInternals(): ElementInternals`, `createStringArrayFormDataState(name: string, values: readonly string[]): FormData`, `readStringArrayFormDataState(state: string | File | FormData | null): string[]`, `isEmptyFormValue(value: unknown): boolean`, and `FormAssociated(Base: T, valueAdapter?: FormValueAdapter): T & Constructor & FormAssociatedSubclassInterface>`. The exact adapter records are `FormValueAdapter { readonly empty: TValue; toFormValue(value: TValue): FormSubmissionValue; toFormState?(value: TValue): FormSubmissionValue; isEmpty?(value: TValue): boolean; fromAttribute?(attribute: string): TValue; toAttribute?(value: TValue): string | null; fromFormState?(state: FormSubmissionValue): TValue }` and `FormAssociatedInterface { internals: ElementInternals; get name(): string; set name(next: string | null); value: TValue; defaultValue: TValue; customError: string | null; disabled: boolean; required: boolean; readonly effectiveDisabled: boolean; get form(): HTMLFormElement | null; set form(owner: FormOwnerValue); readonly labels: NodeList; readonly validity: ValidityState; readonly validationMessage: string; readonly willValidate: boolean; setFormValue(next: TValue): void; getForm(): HTMLFormElement | null; checkValidity(): boolean; reportValidity(): boolean; setCustomValidity(message: string): void; resetValidity(): void; formResetCallback(): void; formStateRestoreCallback(state: FormSubmissionValue, reason: 'autocomplete' | 'restore'): void }`. The exported subclass seam is `FormAssociatedSubclassInterface { protected captureLiveValueCheckpoint(): { readonly value: TValue; readonly dirty: boolean }; protected restoreLiveValueCheckpoint(checkpoint: { readonly value: TValue; readonly dirty: boolean }): void }`. - **`group-by-recency` → `groupByRecency(items, options?)`** — buckets dated items into Today / Yesterday / Previous 7 Days / Older, on **local calendar-day boundaries** ("yesterday" is the previous calendar date, not 24–48 hours ago). Plain data in, plain data out — no DOM. `getTimestamp` extracts the date (default: the item _is_ a `Date`; a returned number is epoch **milliseconds**), `now` fixes the reference instant for deterministic tests or an "as of" report, and `labels` overrides any of the four English defaults — the strings are yours, so localize them through your own catalog. Empty buckets are omitted, order within a bucket is the input's, a future timestamp lands in Today and an unparseable one in Older. Exposed because an application rendering its own list beside `lr-thread-list` needs bucketing that agrees with the component's; reimplementing "this week" is how two lists on one page start disagreeing about what day it is. The typed records are `RecencyLabels { today?; yesterday?; previousWeek?; older? }`, `GroupByRecencyOptions { getTimestamp?(item); now?; labels? }`, and `RecencyBucket { label; items }`. The callable signature is `groupByRecency(items: T[], options?: GroupByRecencyOptions): RecencyBucket[]`. - **`defined` → `allDefined(root?, options?): Promise`** — iteratively waits for every currently rendered, inventory-known Lyra tag below a `Document`, `DocumentFragment`/open `ShadowRoot`, or `Element` to be defined in its owning/scoped registry. It also waits for each available `updateComplete`, then repeats so tags created by that first render are included. Open shadow roots are traversed without recursive calls; unknown `lr-*` names are ignored instead of hanging. `AllDefinedOptions` bounds the complete operation with `maxElements` (default `10_000`), `maxRoots` (`2_000`), `maxDepth` (`256`), `maxWork` (`100_000`), and `maxPasses` (`100`). Invalid limits throw `RangeError`; exceeding a limit rejects instead of resolving with a partial readiness result. Consumer-owned elements with throwing registry or update-completion accessors are treated as unavailable while valid siblings continue. With no browser document or registry it resolves immediately. It **does not import or define components**: pair it with explicit registration imports, `discover()`, or `start()` when bootstrap/tests need a readiness barrier. The exact options record is `AllDefinedOptions { readonly maxPasses?: number; readonly maxElements?: number; readonly maxRoots?: number; readonly maxDepth?: number; readonly maxWork?: number }`. - **`css-length` → `resolveCssLength(value, options?)`** — resolves finite numbers and CSS `px`/`rem`/`em`/`%`/`vw`/`vh` lengths to pixels without allocating DOM. Supply `host` for live font and owner-realm context, `percentBase` for percentages, and an optional `viewportBasis` for deterministic viewport-unit resolution: either a `Window` or `{ inlineSize, blockSize }`. Unsupported expressions and unavailable context return `undefined`; range policy remains the caller's responsibility. Its exact signatures are `resolveCssLength(value: number | string | undefined, options?: ResolveCssLengthOptions): number | undefined` and `ResolveCssLengthOptions { readonly host?: Element; readonly percentBase?: number; readonly viewportBasis?: Window | Readonly<{ inlineSize: number; blockSize: number }> }`. - **`format` → `formatNumber(value, locale?, options?)`, `formatDate(value, locale?, options?)`, `formatRelativeTime(value, locale?, options?)`, and `formatBytes(value, locale?, options?)`** — pure, string-returning wrappers over the same memoized `Intl` formatter cache and locale resolution ``, ``, ``, and `` render through. Reach for these when you need a formatted **string** rather than a rendered element: interpolating into a message template, populating a text-only property on another component (a stat tile's value, a chart tick label, a badge's cost text), building a search predicate, or composing an accessibility announcement. An omitted `locale` (or the explicit `'auto'` sentinel) on any of the four resolves to the page's active `setLyraLocale()` locale, exactly like a rendered `` component with no closer `locale`/`lang` override — not a hardcoded `'en'`. It falls back to `'en'` only once no active locale has ever been set, so an app that never calls `setLyraLocale()` sees no change. An explicit BCP-47 tag always stays authoritative over the active locale. `formatNumber()` and `formatBytes()` accept a `bigint` or a decimal/integer string, not just a `number`, for exact-precision input (large ids, monetary amounts, exact byte counts) — a plain `number` is a float64 and cannot exactly represent an integer beyond `Number.MAX_SAFE_INTEGER` or most decimal fractions. `formatBytes()` still selects its unit (`byte`..`petabyte`/`bit`.. `petabit`) from an approximate magnitude, since which unit is only ever a display choice, but computes the displayed amount itself with exact `bigint` division, so the digits a `bigint`/string input carries are never rounded away. `formatDate()` and `formatRelativeTime()` accept the same date sources as ``/``'s `date` property (an ISO/date string, epoch milliseconds, or a `Date`) and return `undefined` for an unresolvable source instead of throwing; `formatBytes()` likewise returns `undefined` for a non-finite or unparseable `value`. `formatNumber()` and invalid `options` on any of the four throw the same error `Intl.NumberFormat`/`Intl.DateTimeFormat`/`Intl.RelativeTimeFormat`'s own constructor would. `formatRelativeTime()`'s `options.unit` accepts an explicit unit or `'auto'` (default) to pick the largest unit the magnitude clears, matching ``'s heuristic; `options.now` fixes the reference instant for a deterministic test or an "as of" report. It is a one-shot computation — pair it with your own timer, or use ``, for text that must stay current while displayed. The exact signatures are `formatNumber(value: number | bigint | string, locale?: string, options?: Intl.NumberFormatOptions): string`, `formatDate(value: string | number | Date, locale?: string, options?: Intl.DateTimeFormatOptions): string | undefined`, `formatRelativeTime(value: string | number | Date, locale?: string, options?: LyraFormatRelativeTimeOptions): string | undefined`, and `formatBytes(value: number | bigint | string, locale?: string, options?: LyraFormatBytesOptions): string | undefined`. `LyraFormatRelativeTimeOptions { readonly unit?: LyraRelativeTimeUnit | 'auto'; readonly format?: LyraFormatDisplay; readonly numeric?: LyraRelativeTimeNumeric; readonly now?: number }` and `LyraFormatBytesOptions { readonly unit?: LyraFormatBytesUnit; readonly display?: LyraFormatDisplay; readonly unitStep?: number; readonly decimals?: number }`. ```ts import { formatBytes, formatRelativeTime } from "@aceshooting/lyra-ui/utilities/format.js"; const size = formatBytes(12_345_678_901_234_567_890n, "en-US"); // exact, no float rounding const updated = formatRelativeTime(item.updatedAt, "en-US"); // "3 days ago" ``` - **`layered-layout` → `layeredLayout()`** — the deterministic, dependency-free layered-DAG ("Sugiyama-lite") layout `lr-flow-canvas` draws with: cycle handling, longest-path layering, barycenter crossing reduction, and coordinates assigned along the block axis so the result is RTL-neutral. `fixedPositions` entries keep their given coordinates: their combined inline extent is reserved before computed boxes, and a fixed layer's block extent advances every later layer, so fixed and computed boxes retain `gapX`/`gapY` separation without moving the anchors. Two conflicting caller-fixed boxes are deliberately kept verbatim. Node dimensions, gaps, and fixed coordinates must be finite, non-negative values no greater than `Number.MAX_SAFE_INTEGER`; bad geometry throws `RangeError` before graph traversal. Traversal is iterative, and `maxVirtualWaypoints` sets the nonnegative integer routing budget (default 10,000; invalid values use the default, negative values clamp to zero, and fractions truncate). The returned `LayeredLayoutResult` contains readonly `positions`, `virtualWaypointCount`, and `truncated`; when truncated, long edges influence ordering through their real endpoints instead of allocating every intermediate layer. Positions are raw box centers; the first computed layer starts at `y = 0`, so its centers are offset by half that layer's height. Centering the drawing in your own canvas is yours. Its exact data contracts are `LayeredLayoutNode { id; width; height }`, `LayeredLayoutEdge { source; target }`, `LayeredLayoutOptions { fixedPositions?: ReadonlyMap>; gapX?: number; gapY?: number; maxVirtualWaypoints?: number }`, and `LayeredLayoutResult { readonly positions: ReadonlyMap>; readonly truncated: boolean; readonly virtualWaypointCount: number }`. The callable contract is `layeredLayout(input: { nodes: readonly LayeredLayoutNode[]; edges: readonly LayeredLayoutEdge[]; options?: LayeredLayoutOptions }): LayeredLayoutResult`. Call `layeredLayout(input)` with `input.nodes`, `input.edges`, and optional `input.options`; the result's `positions` map contains readonly `{ x, y }` coordinates. - **`animation-registry` → `setDefaultAnimation(animationName, animation)`, `setAnimation(element, animationName, animation)`, and `getAnimation(element, animationName, options?)`** — public motion overrides in native Web Animations API vocabulary. Resolution is per-element first, page-wide default second, then the component's token-derived fallback. `rtlKeyframes` supplies a logical-direction alternative; `getAnimation()` infers computed direction unless `options.dir` is explicit. Passing `null` disables visible motion without skipping the owning component's events or promise lifecycle. Each setter returns an idempotent cleanup that restores the previous stacked registration; element registrations live in a `WeakMap`, so neither the registry nor a retained cleanup keeps a detached element alive. Each registration takes a bounded shallow frozen snapshot of its readonly keyframe arrays, keyframe records, and options, so later caller mutation cannot alter another component's motion; `getAnimation()` returns a fresh readonly frozen snapshot. Each logical direction retains at most 512 keyframes, a keyframe may carry at most 256 enumerable own fields, and the options record may carry at most 64. An oversized or getter-throwing JavaScript record is retained as an inert override: resolution uses the caller's valid bounded fallback, or the zero-duration disabled result when no valid fallback exists. Reduced motion is respected by default by flattening delay/duration/end-delay to zero and iterations to one while preserving the resolved end frame; only a caller with a stronger policy should pass `respectReducedMotion: false`. The exact records are `LyraElementAnimation { readonly keyframes: readonly Readonly[]; readonly rtlKeyframes?: readonly Readonly[]; readonly options?: Readonly }`, `LyraResolvedElementAnimation { readonly keyframes: readonly Readonly[]; readonly options: Readonly }`, and `LyraGetAnimationOptions { readonly dir?: 'ltr' | 'rtl'; readonly fallback?: LyraElementAnimation | null; readonly respectReducedMotion?: boolean }`. The callable contracts are `setDefaultAnimation(animationName: string, animation: LyraElementAnimation | null): LyraAnimationCleanup`, `setAnimation(element: Element, animationName: string, animation: LyraElementAnimation | null): LyraAnimationCleanup`, and `getAnimation(element: Element, animationName: string, options?: LyraGetAnimationOptions): LyraResolvedElementAnimation`. ```ts import { setAnimation, type LyraElementAnimation, } from "@aceshooting/lyra-ui/utilities/animation-registry.js"; const dialog = document.querySelector("lr-dialog"); const enter: LyraElementAnimation = { keyframes: [{ opacity: 0 }, { opacity: 1 }], options: { duration: 180 }, }; const release = setAnimation(dialog, "dialog.show", enter); // release() restores the previous registration. ``` - **`overlay-manager` → `activateOverlay(options): OverlayHandle` and `suspendLyraModalsFor(externalModal): () => void`** — per-`Document` coordination used by Lyra's modal and focus-returning overlay surfaces, including dialogs/drawers, Page/app navigation, command/tool surfaces, lightbox/tour, and responsive/floating/fullscreen panels. All entries share one topmost stack: only the top entry handles Escape, Tab trapping, and backdrop dismissal. Content outside the active modal's composed path is inert, including lower overlays and page content added while it is open. Focus traversal crosses slots and open shadow roots; activation preserves focus already inside but pulls outside focus in, and closing restores the still-connected opener. Nested closes restore into the surviving overlay before returning to the original trigger. `OverlayActivationOptions.lockScroll` gives the manager document-scoped, ref-counted ownership of scroll locking for the entry's registered lifetime; it releases that ownership during disconnect or rendered suspension. `suspendWhenUnrendered` defaults to `false`. When enabled, an active entry whose resolved panel generates no CSS layout box — including because `display: none` is set on the host or an ancestor — releases inerting, focus-trap/stack ownership, and manager-owned scroll lock without changing the component's logical open state. It resumes in its original stack order when rendered again. `deactivate`'s `deferScrollLockRelease` option (default `false`) skips releasing the entry's scroll lock as part of deactivation and instead returns the release function, so a component with a visible exit animation can hold the lock until that animation actually finishes rather than the instant it starts closing; it is ignored (returns `undefined`) when the entry never requested `lockScroll`. `OverlayActivationOptions` exposes `host`, `panel`, optional `modalRoot`, `modal`, `lockScroll`, `suspendWhenUnrendered`, `onEscape`, `onBackdrop`, `preferredInitialFocus`, `beforeInitialFocus`, `restoreFocusTo`, `trapFocus`, and `onTab`. `OverlayHandle` exposes `focusInitial()`, `focusAutofocus()`, `updateRestoreFocusTo(target)`, `deactivate({ restoreFocus?, deferScrollLockRelease? }?)`, `suspend()`, `resume()`, `isTopmost()`, `isActive()`, and `dismissBackdrop()`; the deactivate argument is the exported `OverlayDeactivateOptions` record, and its return value is the deferred scroll-lock release function (or `undefined`). Exact records are `OverlayActivationOptions { host: HTMLElement; panel: () => HTMLElement | null; modalRoot?: () => HTMLElement | null; onEscape: () => void; onBackdrop?: () => void; preferredInitialFocus?: () => HTMLElement | null; beforeInitialFocus?: () => boolean; restoreFocusTo?: OverlayRestoreFocusTarget; modal?: boolean; trapFocus?: boolean; onTab?: () => void; suspendWhenUnrendered?: boolean; lockScroll?: boolean }`, `OverlayDeactivateOptions { restoreFocus?: boolean; deferScrollLockRelease?: boolean }`, and `OverlayHandle { focusInitial(): void; focusAutofocus(): boolean; updateRestoreFocusTo(target: OverlayRestoreFocusTarget): void; deactivate(options?: OverlayDeactivateOptions): (() => void) | undefined; suspend(): void; resume(): void; isTopmost(): boolean; isActive(): boolean; dismissBackdrop(): boolean }`. When a third-party modal must open above a Lyra modal, call the public helper after its root is connected, then release it when that modal closes: ```ts import { suspendLyraModalsFor } from "@aceshooting/lyra-ui/utilities/overlay-manager.js"; const externalModal = document.querySelector("#vendor-modal")!; const release = suspendLyraModalsFor(externalModal); release(); // idempotent ``` The handle is document-scoped and nestable. While any such handle is active, Lyra yields Escape/Tab ownership and keeps only the external modal paths non-inert; disconnecting or adopting the external root releases its handle automatically. - **`announcer` → `Announcer` and `acquireAnnouncementSink()`** — throttled live-region announcements, paired with `lr-live-region`. `Announcer` is the DOM-free coalescing engine; `acquireAnnouncementSink(politeness, options?)` hands back the ref-counted, visually hidden region in the **host document's light DOM** that every Lyra announcement lands in (a live region inside a shadow root is not reliably announced). The module also exports `ANNOUNCEMENT_SINK_ATTRIBUTE` — the `data-lr-live-region` marker those regions carry — so a consumer's DOM diffing, snapshot testing, or `MutationObserver` can recognize and ignore them. Both are documented in full in `llms/components/lr-live-region.md`. The constructor accepts `AnnouncerOptions { throttleMs?; onFlush(text); timerHost? }`; each call accepts `AnnounceOptions { force? }`; and `AnnouncerTimerHost` provides `setTimeout(handler, timeout)` plus `clearTimeout(handle)`. `AnnouncementSinkOptions` exposes `document`, `source`, and `messageTtlMs`; its `AnnouncementSink` handle exposes readonly `element` and `politeness`, mutable `messageTtlMs`, `announce(text)`, and `release()`. Exact sink contracts are `acquireAnnouncementSink(politeness: AnnouncementPoliteness, options?: AnnouncementSinkOptions): AnnouncementSink`, `AnnouncementSinkOptions { document?: Document; source?: Element; messageTtlMs?: number }`, `AnnouncementSink { readonly element: HTMLElement; readonly politeness: AnnouncementPoliteness; messageTtlMs: number; announce(text: string): void; release(): void }`, and `AnnouncerTimerHost { setTimeout(handler: () => void, timeout: number): number; clearTimeout(handle: number): void }`. - **`localization` → `subscribeLyraLocale(listener): () => void` and `bridgeLyraLocale(options?): () => void`** — the _active-locale_ half of the locale runtime, which the side-effect-free `@aceshooting/lyra-ui/localization.js` entry does not carry (that one has `subscribeLyraLocaleRegistry()`, which answers a different question — see "Localization"). `subscribeLyraLocale()` fires whenever the active selection changes, and when a newly registered or extended catalog can alter that selection's messages or direction. Registrations unrelated to the active lookup chain are filtered. An application can therefore re-render its **own** locale-dependent output in step with the components without reacting to every lazy catalog. `bridgeLyraLocale()` mirrors the active locale's canonical public tag onto an element's `lang` and — unless `direction: false` — its `dir`, resolved through `getLyraLocaleDirection()`. `setLyraLocale()` only tells _this library_ which locale is in force; `:lang()` selectors, hyphenation and quote marks, spelling dictionaries, a screen reader's pronunciation of untranslated prose and every third-party widget all read the platform `lang`/`dir` cascade instead, so an application that switches locale at runtime has to write those attributes itself. This is that glue, in one supported place. `target` defaults to `document.documentElement`; pass an application root to scope it to a subtree. It is strictly opt-in — importing the module does nothing, and the library never calls it for you. While no locale is active it leaves the target's authored `lang`/`dir` alone rather than blanking them. Multiple bridges on the same target share one subscription and authored-state snapshot; cleanup handles can release in any order, and the last release restores exactly what the target carried before the first bridge, including an attribute that was absent. Direction remains mirrored while any active handle leaves `direction` enabled. Its exact options record is `LyraLocaleBridgeOptions { target?: Element; direction?: boolean }`. ```ts import { bridgeLyraLocale } from "@aceshooting/lyra-ui/utilities/localization.js"; import { setLyraLocale } from "@aceshooting/lyra-ui/localization.js"; import "@aceshooting/lyra-ui/translations/ar.js"; const stop = bridgeLyraLocale(); // mirrors onto setLyraLocale("ar"); // stop(); // restores whatever carried before ``` `resolveLyraScopedString(/* public names: host, key, defaults, overrides, fallback, values */): unknown` is a scoped variant of `resolveLyraString()` (see "Localization"): the same override → fallback → registered-locale-catalog resolution chain, but against a caller-supplied `defaults` record instead of the complete `LYRA_DEFAULT_STRINGS` catalog. It lives on this tree-shakable entry rather than the side-effect-free `@aceshooting/lyra-ui/localization.js` one so that resolving a handful of an application's own messages never pulls in the full compatibility catalog to do it — pass a small `defaults` object of just the keys used. ```ts import { resolveLyraScopedString } from "@aceshooting/lyra-ui/utilities/localization.js"; const label = resolveLyraScopedString(host, "save", { save: "Save" }); ``` **Known gotchas:** - `formResetCallback()` restores the _content attribute_ default, so `el.value = 'x'` never redefines what `form.reset()` restores to (native `defaultValue`/`defaultSelected` semantics). ## Reference artifacts The package includes authored family and shared references plus derived index, component, token, peer, migration, and concatenated references. ## When no component fits, file it First rule out a naming mismatch: check `llms/index.md` for a component covering the same job under a different name. Most apparent gaps are discovery failures, not missing components. Then check the live catalog — one read-only request, no side effects, nothing filed: ```bash curl -sS 'https://www.lyra-ui.com/api/v1/components/search?q=kanban+board+swimlane' ``` It returns the closest components with doc links. Search as many phrasings as you like; this endpoint is meant to be used freely, and it is the cheapest way to discover that a "missing" component already exists under a name you did not guess. If nothing fits, report the gap so it can be built — but **ask the user and get their explicit agreement first.** Filing sends the user's description to an external service; show them what you intend to submit, and never file as a silent side effect of noticing a gap. If the user has not said yes, do not send it. ```bash curl -sS -X POST https://www.lyra-ui.com/api/v1/feature-requests \ -H 'Content-Type: application/json' \ -d '{ "title": "Kanban board", "description": "Needed a drag-and-drop board with swimlanes for a task view; nothing in the catalog covers it.", "searched_for": ["kanban", "board", "swimlane", "drag drop"], "settled_for": "a hand-rolled div grid with HTML5 drag events", "agent": "claude-code" }' ``` Accepted fields: `title` (required, ≤120 chars), `description` (required, ≤4000 chars), `use_case`, `searched_for` (array of terms tried — the most valuable field, since it records which name was expected and that's exactly what makes a component undiscoverable), `settled_for` (what was used instead), `agent`, and the optional contact fields `name` (≤120) and `email` (≤200). Anonymous submission is the default and is fine — `name`/`email` only add value if the maintainer might follow up. Ask the user whether they want to be reachable before adding either one; never invent, guess, or reuse an address from context you happen to have (git config, an earlier message, the environment). All submissions, including any name/email, are stored privately and shown only to the maintainer — never published. The response includes `matches` (the closest existing components, with doc links — read it, since it often answers the gap outright) and an `id`; status is readable later at `https://www.lyra-ui.com/api/v1/feature-requests/{id}`. The full schema is at `https://www.lyra-ui.com/api/v1/openapi.json`. **Never include private material.** Submissions leave the user's machine. Describe the component generically — no source code, no client or product names, no file paths, no credentials. If the need cannot be described without such details, do not file it. Use the API even when you are working inside the lyra-ui repo itself. It is the only supported automated intake path for an assistant acting on a user's behalf — do not write the request into a local file instead, where nothing will pick it up, and do not silently open a GitHub issue. A person filing their own report can use the human-facing routes in `SUPPORT.md`. Keep the report short and concrete: - **Name the component you wanted**, in library style (`lr-kanban-board`), so the gap is searchable. - **Say what it had to do** in a sentence or two — the behaviour, not your implementation. - **List the `lr-*` components you actually checked** and why each fell short. This is what separates a real gap from a naming mismatch, and it is the part only you can supply. ## Exported TypeScript contracts These named interfaces and helper signatures are available to typed integrations. They are grouped by capability so the component sections above can stay focused. - **`ai-adapters-a2ui-contracts`** — AI adapter and runtime contracts. `A2UiAdapterLimits { maxComponents: unknown; maxDepth: unknown; maxOutputNodes: unknown; maxChildrenPerComponent: unknown; maxNodes: unknown; maxBytes: unknown; maxStringCharacters: unknown; }` `A2UiLikeAction { id: unknown; payload: unknown; }` `A2UiLikeComponent { id: unknown; type: unknown; props: unknown; text: unknown; children: unknown; action: unknown; }` `A2UiLikeSurface { surfaceId: unknown; rootId: unknown; components: unknown; data: unknown; }` `adaptA2UiSurface(/* public names: surface, typeMap, limits */): unknown` - **`ai-adapters-ag-ui-contracts`** — AI adapter and runtime contracts. `AgUiAdapterLimits { maxBufferedTools: unknown; maxToolArgumentBytes: unknown; maxTextDeltaCharacters: unknown; maxDepth: unknown; maxNodes: unknown; maxBytes: unknown; maxStringCharacters: unknown; }` `AgUiLikeEvent { type: unknown; eventId: unknown; runId: unknown; messageId: unknown; role: unknown; delta: unknown; toolCallId: unknown; toolCallName: unknown; result: unknown; message: unknown; code: unknown; snapshot: unknown; messages: unknown; }` - **`ai-adapters-ai-sdk-contracts`** — AI adapter and runtime contracts. `adaptAiSdkMessage(/* public names: message, limits */): unknown` `AiSdkAdapterLimits { maxParts: unknown; maxDepth: unknown; maxNodes: unknown; maxBytes: unknown; maxStringCharacters: unknown; }` `AiSdkLikeMessage { id: unknown; role: unknown; parts: unknown; metadata: unknown; }` - **`ai-runtime-contracts`** — AI adapter and runtime contracts. `AgentStreamLimits { maxMessages: unknown; maxPartsPerMessage: unknown; maxTools: unknown; maxDeltaCharacters: unknown; maxTextCharactersPerPart: unknown; maxIdentifierCharacters: unknown; maxStatusMessageCharacters: unknown; maxPatchOperations: unknown; maxSnapshotDepth: unknown; maxSnapshotNodes: unknown; maxSnapshotBytes: unknown; maxRetainedBytes: unknown; }` `AgentStreamState { generation: unknown; cursor: unknown; limits: unknown; runId: unknown; status: unknown; messages: unknown; tools: unknown; sharedState: unknown; error: unknown; message: unknown; code: unknown; }` `applySharedStatePatch(/* public names: value, patch */): unknown` `createAgentStreamState(/* public names: limits */): unknown` `parseJsonPatch(/* public names: value, limits */): unknown` `reduceAgentStreamEvents(/* public names: state, events */): unknown` `reduceAgentStream(/* public names: state, event */): unknown` - **`ai-snapshot-contracts`** — AI adapter and runtime contracts. `createProviderSnapshotBudget(/* public names: limits */): unknown` `ProviderSnapshotBudget { limits: unknown; bytes: unknown; nodes: unknown; }` `ProviderSnapshotLimits { maxDepth: unknown; maxNodes: unknown; maxBytes: unknown; maxStringCharacters: unknown; }` `resolveProviderSnapshotLimits(/* public names: limits */): unknown` `snapshotProviderValue(/* public names: value, budget */): unknown` - **`ai-types-contracts`** — AI adapter and runtime contracts. `AgentRun { id: unknown; status: unknown; startedAt: unknown; endedAt: unknown; model: unknown; costEstimate: unknown; steps: unknown; }` `AgentStatus { kind: unknown; message: unknown; }` `AgentStep { id: unknown; kind: unknown; label: unknown; status: unknown; startedAt: unknown; endedAt: unknown; }` `AttachmentMessagePart { type: unknown; document: unknown; id: unknown; state: unknown; metadata: unknown; }` `AudioMessagePart { type: unknown; src: unknown; transcript: unknown; mimeType: unknown; id: unknown; state: unknown; metadata: unknown; }` `CancelEventDetail { reason: unknown; }` `ChatMessage { id: unknown; role: unknown; status: unknown; timestamp: unknown; text: unknown; attachments: unknown; parts: unknown; metadata: unknown; }` `Citation { id: unknown; chunkId: unknown; sourceId: unknown; span: unknown; start: unknown; end: unknown; label: unknown; locator: unknown; answerRange: unknown; quote: unknown; metadata: unknown; }` `CitationMessagePart { type: unknown; citation: unknown; id: unknown; state: unknown; metadata: unknown; }` `CitationSelectEventDetail { citation: unknown; }` `DataMessagePart { type: unknown; name: unknown; data: unknown; widget: unknown; id: unknown; state: unknown; metadata: unknown; }` `DocumentRef { id: unknown; name: unknown; mimeType: unknown; uri: unknown; version: unknown; }` `ErrorMessagePart { type: unknown; message: unknown; code: unknown; retryable: unknown; id: unknown; state: unknown; metadata: unknown; }` `ExportEventDetail { format: unknown; }` `GroundedClaim { id: unknown; text: unknown; status: unknown; citationIds: unknown; answerRange: unknown; start: unknown; end: unknown; confidence: unknown; explanation: unknown; }` `GroundingAssessment { supportedClaims: unknown; unsupportedClaims: unknown; coverage: unknown; confidence: unknown; warnings: unknown; claims: unknown; }` `MessagePartBase { id: unknown; state: unknown; metadata: unknown; }` `ReasoningMessagePart { type: unknown; text: unknown; collapsed: unknown; id: unknown; state: unknown; metadata: unknown; }` `RetrievalChunk { id: unknown; text: unknown; score: unknown; source: unknown; metadata: unknown; rank: unknown; locator: unknown; queryId: unknown; stage: unknown; traceId: unknown; scores: unknown; }` `RetrievalProgressEventDetail { queryId: unknown; stage: unknown; progress: unknown; }` `RetrievalQuery { text: unknown; filters: unknown; mode: unknown; scope: unknown; }` `RetrievalScoreBreakdown { dense: unknown; sparse: unknown; rerank: unknown; final: unknown; }` `RetryEventDetail { attempt: unknown; messageId: unknown; }` `RunLifecycleEventDetail { runId: unknown; status: unknown; }` `TextMessagePart { type: unknown; text: unknown; id: unknown; state: unknown; metadata: unknown; }` `ToolApprovalEventDetail { invocationId: unknown; approved: unknown; }` `ToolCallMessagePart { type: unknown; invocation: unknown; id: unknown; state: unknown; metadata: unknown; }` `ToolInvocation { id: unknown; name: unknown; args: unknown; status: unknown; result: unknown; error: unknown; }` `ToolResultErrorMessagePart { error: unknown; result: unknown; type: unknown; invocationId: unknown; name: unknown; id: unknown; state: unknown; metadata: unknown; }` `ToolResultSuccessMessagePart { result: unknown; error: unknown; type: unknown; invocationId: unknown; name: unknown; id: unknown; state: unknown; metadata: unknown; }` `WidgetMessagePart { type: unknown; name: unknown; data: unknown; widget: unknown; id: unknown; state: unknown; metadata: unknown; }` - **`autoloader-contracts`** — Shared utility contracts. `AutoloaderErrorEventDetail { error: unknown; tag: unknown; optionalPeers: unknown; }` `AutoloaderEventDetail { tag: unknown; optionalPeers: unknown; }` `AutoloaderOptions { optionalPeers: unknown; events: unknown; maxConcurrency: unknown; maxElements: unknown; maxRoots: unknown; maxDepth: unknown; maxWork: unknown; }` `AutoloaderTraversalErrorEventDetail { limit: unknown; maximum: unknown; error: unknown; }` `discover(root?: LyraDefinitionRoot, options?: AutoloaderOptions): Promise` `start(root?: LyraDefinitionRoot, options?: AutoloaderOptions): Promise` `stop(): unknown` - **`custom-elements-jsx-contracts`** — Framework integration type contracts. `LyraReactIntrinsicElements { "lr-accordion": unknown; "lr-accordion-item": unknown; "lr-activity-feed": unknown; "lr-agent-eval-dashboard": unknown; "lr-agent-run": unknown; "lr-agent-trace": unknown; "lr-agent-workspace": unknown; "lr-alert": unknown; "lr-animated-image": unknown; "lr-animation": unknown; "lr-app-rail": unknown; "lr-app-rail-group": unknown; "lr-app-rail-item": unknown; "lr-approval-queue": unknown; "lr-archive-viewer": unknown; "lr-artifact-panel": unknown; "lr-attachment-chip": unknown; "lr-attachment-trigger": unknown; "lr-audio-visualizer": unknown; "lr-av-player": unknown; "lr-avatar": unknown; "lr-avatar-group": unknown; "lr-badge": unknown; "lr-bar-chart": unknown; "lr-box-plot": unknown; "lr-branch-picker": unknown; "lr-breadcrumb": unknown; "lr-breadcrumb-item": unknown; "lr-browser-frame": unknown; "lr-bubble-chart": unknown; "lr-button": unknown; "lr-button-group": unknown; "lr-calendar": unknown; "lr-calendar-viewer": unknown; "lr-callout": unknown; "lr-card": unknown; "lr-carousel": unknown; "lr-carousel-item": unknown; "lr-chart": unknown; "lr-chat-composer": unknown; "lr-chat-message": unknown; "lr-chat-viewport": unknown; "lr-checkbox": unknown; "lr-checkbox-group": unknown; "lr-checkpoint": unknown; "lr-chip": unknown; "lr-chip-group": unknown; "lr-chunk-inspector": unknown; "lr-citation-badge": unknown; "lr-claim-evidence": unknown; "lr-code-block": unknown; "lr-code-block-core": unknown; "lr-code-editor": unknown; "lr-color-picker": unknown; "lr-combobox": unknown; "lr-command-palette": unknown; "lr-commit-card": unknown; "lr-community-card": unknown; "lr-compare-panel": unknown; "lr-condition-builder": unknown; "lr-confirm-bar": unknown; "lr-contact-viewer": unknown; "lr-context-inspector": unknown; "lr-context-meter": unknown; "lr-control-group": unknown; "lr-conversation-item": unknown; "lr-copy-button": unknown; "lr-csv-viewer": unknown; "lr-dashboard-grid": unknown; "lr-data-grid": unknown; "lr-dataset-viewer": unknown; "lr-date-input": unknown; "lr-date-picker": unknown; "lr-details": unknown; "lr-dialog": unknown; "lr-diff-view": unknown; "lr-divider": unknown; "lr-dock-panel": unknown; "lr-document-compare": unknown; "lr-document-library": unknown; "lr-document-preview": unknown; "lr-document-viewer": unknown; "lr-docx-viewer": unknown; "lr-doughnut-chart": unknown; "lr-drawer": unknown; "lr-drilldown-panel": unknown; "lr-drop-zone": unknown; "lr-dropdown": unknown; "lr-dropdown-item": unknown; "lr-ebook-viewer": unknown; "lr-email-viewer": unknown; "lr-embedding-explorer": unknown; "lr-emoji-picker": unknown; "lr-empty": unknown; "lr-entity-card": unknown; "lr-entity-chip": unknown; "lr-entity-dossier": unknown; "lr-env-list": unknown; "lr-eval-dataset": unknown; "lr-eval-result": unknown; "lr-eval-run": unknown; "lr-export-button": unknown; "lr-file-icon": unknown; "lr-file-input": unknown; "lr-file-tree": unknown; "lr-filter-bar": unknown; "lr-flag": unknown; "lr-flow-canvas": unknown; "lr-flow-controls": unknown; "lr-flow-minimap": unknown; "lr-flow-node": unknown; "lr-flow-run-status": unknown; "lr-format-bytes": unknown; "lr-format-date": unknown; "lr-format-number": unknown; "lr-funnel": unknown; "lr-gauge": unknown; "lr-generation-metrics": unknown; "lr-geojson-view": unknown; "lr-geojson-viewer": unknown; "lr-graph": unknown; "lr-graph-legend": unknown; "lr-graph-query-builder": unknown; "lr-grounding-summary": unknown; "lr-handoff-divider": unknown; "lr-heatmap": unknown; "lr-highlight-layer": unknown; "lr-histogram": unknown; "lr-html-viewer": unknown; "lr-icon": unknown; "lr-icon-button": unknown; "lr-image-comparer": unknown; "lr-image-viewer": unknown; "lr-include": unknown; "lr-ingestion-queue": unknown; "lr-input": unknown; "lr-intersection-observer": unknown; "lr-json-viewer": unknown; "lr-kbd": unknown; "lr-knowledge-base": unknown; "lr-knowledge-base-admin": unknown; "lr-knowledge-graph-explorer": unknown; "lr-known-date": unknown; "lr-lightbox": unknown; "lr-line-chart": unknown; "lr-lite-chart": unknown; "lr-live-region": unknown; "lr-locale-picker": unknown; "lr-map": unknown; "lr-markdown": unknown; "lr-markdown-core": unknown; "lr-mcp-app": unknown; "lr-media-card": unknown; "lr-memory-panel": unknown; "lr-mention-popover": unknown; "lr-menu": unknown; "lr-menu-item": unknown; "lr-menu-label": unknown; "lr-message-actions": unknown; "lr-message-feedback": unknown; "lr-message-parts": unknown; "lr-mind-map": unknown; "lr-model-select": unknown; "lr-model-settings-panel": unknown; "lr-multi-split": unknown; "lr-mutation-observer": unknown; "lr-native-time-input": unknown; "lr-neighbor-list": unknown; "lr-node-palette": unknown; "lr-notebook-viewer": unknown; "lr-number-input": unknown; "lr-option": unknown; "lr-otp-input": unknown; "lr-page": unknown; "lr-page-rail": unknown; "lr-pagination": unknown; "lr-pan-zoom": unknown; "lr-path-strip": unknown; "lr-pdf-viewer": unknown; "lr-phone-input": unknown; "lr-pie-chart": unknown; "lr-polar-area-chart": unknown; "lr-policy-summary": unknown; "lr-poll-status": unknown; "lr-popover": unknown; "lr-popup": unknown; "lr-pptx-viewer": unknown; "lr-progress-bar": unknown; "lr-progress-ring": unknown; "lr-prompt-input": unknown; "lr-prompt-queue": unknown; "lr-prompt-studio": unknown; "lr-provenance-panel": unknown; "lr-push-to-talk": unknown; "lr-qr-code": unknown; "lr-radar-chart": unknown; "lr-radio": unknown; "lr-radio-button": unknown; "lr-radio-group": unknown; "lr-rag-answer": unknown; "lr-rag-eval-dashboard": unknown; "lr-random-content": unknown; "lr-rating": unknown; "lr-realtime-session": unknown; "lr-relative-time": unknown; "lr-reorder-item": unknown; "lr-reorder-list": unknown; "lr-resize-observer": unknown; "lr-responsive-panel": unknown; "lr-result-card": unknown; "lr-result-field": unknown; "lr-retrieval-compare": unknown; "lr-retrieval-results": unknown; "lr-retrieval-search": unknown; "lr-retrieval-trace": unknown; "lr-rubric-form": unknown; "lr-scatter-chart": unknown; "lr-json-schema-viewer": unknown; "lr-scroller": unknown; "lr-segmented": unknown; "lr-select": unknown; "lr-selection-toolbar": unknown; "lr-sequence-playback": unknown; "lr-sequence-strip": unknown; "lr-skeleton": unknown; "lr-slider": unknown; "lr-source-card": unknown; "lr-source-list": unknown; "lr-source-picker": unknown; "lr-span-waterfall": unknown; "lr-sparkline": unknown; "lr-spinner": unknown; "lr-split-panel": unknown; "lr-spreadsheet-viewer": unknown; "lr-stack-trace": unknown; "lr-stat": unknown; "lr-stepper": unknown; "lr-stream-status": unknown; "lr-streaming-text": unknown; "lr-streaming-text-core": unknown; "lr-subagent-panel": unknown; "lr-suggestion-chips": unknown; "lr-svg-viewer": unknown; "lr-swatch-picker": unknown; "lr-switch": unknown; "lr-tab": unknown; "lr-tab-group": unknown; "lr-tab-panel": unknown; "lr-table": unknown; "lr-tag": unknown; "lr-task-list": unknown; "lr-terminal": unknown; "lr-test-results": unknown; "lr-textarea": unknown; "lr-thinking-panel": unknown; "lr-thread-list": unknown; "lr-time-input": unknown; "lr-time-range": unknown; "lr-timeline": unknown; "lr-timeline-item": unknown; "lr-toast": unknown; "lr-toast-item": unknown; "lr-token-input": unknown; "lr-tool-approval-dialog": unknown; "lr-tool-call-chip": unknown; "lr-tool-param-form": unknown; "lr-tool-result-dialog": unknown; "lr-tool-result-view": unknown; "lr-tool-select-dialog": unknown; "lr-tool-timeline": unknown; "lr-tooltip": unknown; "lr-tour": unknown; "lr-trace-tree": unknown; "lr-transcript-feed": unknown; "lr-tree": unknown; "lr-tree-item": unknown; "lr-typing-indicator": unknown; "lr-usage-badge": unknown; "lr-video": unknown; "lr-video-playlist": unknown; "lr-virtual-list": unknown; "lr-visually-hidden": unknown; "lr-voice-picker": unknown; "lr-widget": unknown; "lr-widget-renderer": unknown; "lr-word-cloud": unknown; "lr-xml-viewer": unknown; "lr-zoomable-frame": unknown; }` - **`internal-ansi-contracts`** — Shared utility contracts. `AnsiStyles { bold: unknown; dim: unknown; italic: unknown; underline: unknown; inverse: unknown; fg: unknown; bg: unknown; }` - **`internal-canvas-color-contracts`** — Shared utility contracts. `resolveCanvasColor(/* public names: scope, color, fallback */): unknown` `resolveCanvasColors(/* public names: scope, colors, fallback */): unknown` - **`internal-localization-runtime-contracts`** — Shared utility contracts. `getLyraLocaleDirection(/* public names: locale */): unknown` `getLyraLocale(): unknown` `getRegisteredLyraLocaleKeys(/* public names: locale */): unknown` `getRegisteredLyraLocales(): unknown` `registerLyraLocale(/* public names: locale, strings, meta */): unknown` `resolveLyraDirection(/* public names: host */): unknown` `resolveLyraLocale(/* public names: host */): unknown` `setLyraLocale(/* public names: locale */): unknown` `subscribeLyraLocaleRegistry(/* public names: listener */): unknown` - **`internal-localization-types-contracts`** — Shared utility contracts. `LyraLocaleMeta { dir: unknown; name: unknown; }` - **`internal-localization-contracts`** — Shared utility contracts. `resolveLyraString(/* public names: host, key, overrides, fallback, values */): unknown` - **`internal-node-type-style-contracts`** — Shared utility contracts. `LyraNodeTypeStyle { id: unknown; label: unknown; color: unknown; shape: unknown; }` - **`internal-registered-animation-contracts`** — Shared utility contracts. `RegisteredAnimationSpec { keyframes: unknown; rtlKeyframes: unknown; durationProperties: unknown; easingProperties: unknown; fallbackDuration: unknown; fallbackEasing: unknown; options: unknown; }` - **`internal-text-viewer-target-contracts`** — Shared utility contracts. `LyraSearchChangeDetail { query: unknown; matchCount: unknown; matchCountExact: unknown; activeIndex: unknown; }` `LyraTextViewerTarget { search: unknown; query: unknown; searchNext: unknown; searchPrevious: unknown; clearSearch: unknown; highlights: unknown; activeHighlightId: unknown; anchor: unknown; anchorKinds: unknown; scrollToAnchor: unknown; target: unknown; }` - **`ssr-contracts`** — Shared utility contracts. `buildLyraSsrStaticSafety(/* public names: tags, tagCapabilities, auditedStaticSafeTags */): unknown` Fail-closed classifier behind `LYRA_SSR_STATIC_SAFETY`/`getLyraSsrStaticSafety`: a tag present in neither a non-empty `tagCapabilities` entry nor `auditedStaticSafeTags` is reported in the result's `unaudited` list instead of silently defaulting to `'static-safe'`. Pure and fixture-friendly -- `scripts/check-ssr.mjs` calls it with the real inventory, tests call it with small fixture lists. `deriveLyraSsrStaticSafety(/* public names: capabilities */): unknown` Derives static safety from one tag's capability record: any `'after-hydration'`/`'client-only'` entry makes it `'hydration-required'`; an empty record defaults to `'static-safe'`. That empty- record default is only valid for a record already known to reflect a completed review (as `LYRA_SSR_TAG_CAPABILITIES` entries do) -- `buildLyraSsrStaticSafety()` does not rely on it for a tag missing from that record entirely, which it instead routes through `auditedStaticSafeTags`. `diagnoseLyraHydration(/* public names: root */): unknown` `getLyraSsrMode(/* public names: tagName */): unknown` `getLyraSsrStaticSafety(/* public names: tagName */): unknown` Returns the static-safety classification for a `render-and-hydrate` Lyra tag, or `undefined` for a tag outside that tier. See "SSR and declarative shadow DOM" above for the full write-up. `LyraHydrationDiagnostic { element: unknown; tag: unknown; mode: unknown; status: unknown; error: unknown; }` `LyraLitElementRendererConstructor { prototype: unknown; matchesClass: unknown; constructor: unknown; tagName: unknown; attributes: unknown; }` `LyraSsrClientRenderReason { code: unknown; detail: unknown; }` `LyraSsrStaticSafetyResult { classification: unknown; unaudited: unknown; }` Return shape of `buildLyraSsrStaticSafety()`: `classification` holds one entry per classified tag (a tag absent from it was neither capability-bearing nor audited), and a non-empty `unaudited` means the check must fail closed. `lyraSsrElementRenderers(/* public names: litElementRenderer */): unknown` - **`svelte-contracts`** — Framework integration type contracts. `LyraElementTagNameMap { "lr-accordion": unknown; "lr-accordion-item": unknown; "lr-activity-feed": unknown; "lr-agent-eval-dashboard": unknown; "lr-agent-run": unknown; "lr-agent-trace": unknown; "lr-agent-workspace": unknown; "lr-alert": unknown; "lr-animated-image": unknown; "lr-animation": unknown; "lr-app-rail": unknown; "lr-app-rail-group": unknown; "lr-app-rail-item": unknown; "lr-approval-queue": unknown; "lr-archive-viewer": unknown; "lr-artifact-panel": unknown; "lr-attachment-chip": unknown; "lr-attachment-trigger": unknown; "lr-audio-visualizer": unknown; "lr-av-player": unknown; "lr-avatar": unknown; "lr-avatar-group": unknown; "lr-badge": unknown; "lr-bar-chart": unknown; "lr-box-plot": unknown; "lr-branch-picker": unknown; "lr-breadcrumb": unknown; "lr-breadcrumb-item": unknown; "lr-browser-frame": unknown; "lr-bubble-chart": unknown; "lr-button": unknown; "lr-button-group": unknown; "lr-calendar": unknown; "lr-calendar-viewer": unknown; "lr-callout": unknown; "lr-card": unknown; "lr-carousel": unknown; "lr-carousel-item": unknown; "lr-chart": unknown; "lr-chat-composer": unknown; "lr-chat-message": unknown; "lr-chat-viewport": unknown; "lr-checkbox": unknown; "lr-checkbox-group": unknown; "lr-checkpoint": unknown; "lr-chip": unknown; "lr-chip-group": unknown; "lr-chunk-inspector": unknown; "lr-citation-badge": unknown; "lr-claim-evidence": unknown; "lr-code-block": unknown; "lr-code-block-core": unknown; "lr-code-editor": unknown; "lr-color-picker": unknown; "lr-combobox": unknown; "lr-command-palette": unknown; "lr-commit-card": unknown; "lr-community-card": unknown; "lr-compare-panel": unknown; "lr-condition-builder": unknown; "lr-confirm-bar": unknown; "lr-contact-viewer": unknown; "lr-context-inspector": unknown; "lr-context-meter": unknown; "lr-control-group": unknown; "lr-conversation-item": unknown; "lr-copy-button": unknown; "lr-csv-viewer": unknown; "lr-dashboard-grid": unknown; "lr-data-grid": unknown; "lr-dataset-viewer": unknown; "lr-date-input": unknown; "lr-date-picker": unknown; "lr-details": unknown; "lr-dialog": unknown; "lr-diff-view": unknown; "lr-divider": unknown; "lr-dock-panel": unknown; "lr-document-compare": unknown; "lr-document-library": unknown; "lr-document-preview": unknown; "lr-document-viewer": unknown; "lr-docx-viewer": unknown; "lr-doughnut-chart": unknown; "lr-drawer": unknown; "lr-drilldown-panel": unknown; "lr-drop-zone": unknown; "lr-dropdown": unknown; "lr-dropdown-item": unknown; "lr-ebook-viewer": unknown; "lr-email-viewer": unknown; "lr-embedding-explorer": unknown; "lr-emoji-picker": unknown; "lr-empty": unknown; "lr-entity-card": unknown; "lr-entity-chip": unknown; "lr-entity-dossier": unknown; "lr-env-list": unknown; "lr-eval-dataset": unknown; "lr-eval-result": unknown; "lr-eval-run": unknown; "lr-export-button": unknown; "lr-file-icon": unknown; "lr-file-input": unknown; "lr-file-tree": unknown; "lr-filter-bar": unknown; "lr-flag": unknown; "lr-flow-canvas": unknown; "lr-flow-controls": unknown; "lr-flow-minimap": unknown; "lr-flow-node": unknown; "lr-flow-run-status": unknown; "lr-format-bytes": unknown; "lr-format-date": unknown; "lr-format-number": unknown; "lr-funnel": unknown; "lr-gauge": unknown; "lr-generation-metrics": unknown; "lr-geojson-view": unknown; "lr-geojson-viewer": unknown; "lr-graph": unknown; "lr-graph-legend": unknown; "lr-graph-query-builder": unknown; "lr-grounding-summary": unknown; "lr-handoff-divider": unknown; "lr-heatmap": unknown; "lr-highlight-layer": unknown; "lr-histogram": unknown; "lr-html-viewer": unknown; "lr-icon": unknown; "lr-icon-button": unknown; "lr-image-comparer": unknown; "lr-image-viewer": unknown; "lr-include": unknown; "lr-ingestion-queue": unknown; "lr-input": unknown; "lr-intersection-observer": unknown; "lr-json-viewer": unknown; "lr-kbd": unknown; "lr-knowledge-base": unknown; "lr-knowledge-base-admin": unknown; "lr-knowledge-graph-explorer": unknown; "lr-known-date": unknown; "lr-lightbox": unknown; "lr-line-chart": unknown; "lr-lite-chart": unknown; "lr-live-region": unknown; "lr-locale-picker": unknown; "lr-map": unknown; "lr-markdown": unknown; "lr-markdown-core": unknown; "lr-mcp-app": unknown; "lr-media-card": unknown; "lr-memory-panel": unknown; "lr-mention-popover": unknown; "lr-menu": unknown; "lr-menu-item": unknown; "lr-menu-label": unknown; "lr-message-actions": unknown; "lr-message-feedback": unknown; "lr-message-parts": unknown; "lr-mind-map": unknown; "lr-model-select": unknown; "lr-model-settings-panel": unknown; "lr-multi-split": unknown; "lr-mutation-observer": unknown; "lr-native-time-input": unknown; "lr-neighbor-list": unknown; "lr-node-palette": unknown; "lr-notebook-viewer": unknown; "lr-number-input": unknown; "lr-option": unknown; "lr-otp-input": unknown; "lr-page": unknown; "lr-page-rail": unknown; "lr-pagination": unknown; "lr-pan-zoom": unknown; "lr-path-strip": unknown; "lr-pdf-viewer": unknown; "lr-phone-input": unknown; "lr-pie-chart": unknown; "lr-polar-area-chart": unknown; "lr-policy-summary": unknown; "lr-poll-status": unknown; "lr-popover": unknown; "lr-popup": unknown; "lr-pptx-viewer": unknown; "lr-progress-bar": unknown; "lr-progress-ring": unknown; "lr-prompt-input": unknown; "lr-prompt-queue": unknown; "lr-prompt-studio": unknown; "lr-provenance-panel": unknown; "lr-push-to-talk": unknown; "lr-qr-code": unknown; "lr-radar-chart": unknown; "lr-radio": unknown; "lr-radio-button": unknown; "lr-radio-group": unknown; "lr-rag-answer": unknown; "lr-rag-eval-dashboard": unknown; "lr-random-content": unknown; "lr-rating": unknown; "lr-realtime-session": unknown; "lr-relative-time": unknown; "lr-reorder-item": unknown; "lr-reorder-list": unknown; "lr-resize-observer": unknown; "lr-responsive-panel": unknown; "lr-result-card": unknown; "lr-result-field": unknown; "lr-retrieval-compare": unknown; "lr-retrieval-results": unknown; "lr-retrieval-search": unknown; "lr-retrieval-trace": unknown; "lr-rubric-form": unknown; "lr-scatter-chart": unknown; "lr-json-schema-viewer": unknown; "lr-scroller": unknown; "lr-segmented": unknown; "lr-select": unknown; "lr-selection-toolbar": unknown; "lr-sequence-playback": unknown; "lr-sequence-strip": unknown; "lr-skeleton": unknown; "lr-slider": unknown; "lr-source-card": unknown; "lr-source-list": unknown; "lr-source-picker": unknown; "lr-span-waterfall": unknown; "lr-sparkline": unknown; "lr-spinner": unknown; "lr-split-panel": unknown; "lr-spreadsheet-viewer": unknown; "lr-stack-trace": unknown; "lr-stat": unknown; "lr-stepper": unknown; "lr-stream-status": unknown; "lr-streaming-text": unknown; "lr-streaming-text-core": unknown; "lr-subagent-panel": unknown; "lr-suggestion-chips": unknown; "lr-svg-viewer": unknown; "lr-swatch-picker": unknown; "lr-switch": unknown; "lr-tab": unknown; "lr-tab-group": unknown; "lr-tab-panel": unknown; "lr-table": unknown; "lr-tag": unknown; "lr-task-list": unknown; "lr-terminal": unknown; "lr-test-results": unknown; "lr-textarea": unknown; "lr-thinking-panel": unknown; "lr-thread-list": unknown; "lr-time-input": unknown; "lr-time-range": unknown; "lr-timeline": unknown; "lr-timeline-item": unknown; "lr-toast": unknown; "lr-toast-item": unknown; "lr-token-input": unknown; "lr-tool-approval-dialog": unknown; "lr-tool-call-chip": unknown; "lr-tool-param-form": unknown; "lr-tool-result-dialog": unknown; "lr-tool-result-view": unknown; "lr-tool-select-dialog": unknown; "lr-tool-timeline": unknown; "lr-tooltip": unknown; "lr-tour": unknown; "lr-trace-tree": unknown; "lr-transcript-feed": unknown; "lr-tree": unknown; "lr-tree-item": unknown; "lr-typing-indicator": unknown; "lr-usage-badge": unknown; "lr-video": unknown; "lr-video-playlist": unknown; "lr-virtual-list": unknown; "lr-visually-hidden": unknown; "lr-voice-picker": unknown; "lr-widget": unknown; "lr-widget-renderer": unknown; "lr-word-cloud": unknown; "lr-xml-viewer": unknown; "lr-zoomable-frame": unknown; }` `LyraSvelteElements { "lr-accordion": unknown; "lr-accordion-item": unknown; "lr-activity-feed": unknown; "lr-agent-eval-dashboard": unknown; "lr-agent-run": unknown; "lr-agent-trace": unknown; "lr-agent-workspace": unknown; "lr-alert": unknown; "lr-animated-image": unknown; "lr-animation": unknown; "lr-app-rail": unknown; "lr-app-rail-group": unknown; "lr-app-rail-item": unknown; "lr-approval-queue": unknown; "lr-archive-viewer": unknown; "lr-artifact-panel": unknown; "lr-attachment-chip": unknown; "lr-attachment-trigger": unknown; "lr-audio-visualizer": unknown; "lr-av-player": unknown; "lr-avatar": unknown; "lr-avatar-group": unknown; "lr-badge": unknown; "lr-bar-chart": unknown; "lr-box-plot": unknown; "lr-branch-picker": unknown; "lr-breadcrumb": unknown; "lr-breadcrumb-item": unknown; "lr-browser-frame": unknown; "lr-bubble-chart": unknown; "lr-button": unknown; "lr-button-group": unknown; "lr-calendar": unknown; "lr-calendar-viewer": unknown; "lr-callout": unknown; "lr-card": unknown; "lr-carousel": unknown; "lr-carousel-item": unknown; "lr-chart": unknown; "lr-chat-composer": unknown; "lr-chat-message": unknown; "lr-chat-viewport": unknown; "lr-checkbox": unknown; "lr-checkbox-group": unknown; "lr-checkpoint": unknown; "lr-chip": unknown; "lr-chip-group": unknown; "lr-chunk-inspector": unknown; "lr-citation-badge": unknown; "lr-claim-evidence": unknown; "lr-code-block": unknown; "lr-code-block-core": unknown; "lr-code-editor": unknown; "lr-color-picker": unknown; "lr-combobox": unknown; "lr-command-palette": unknown; "lr-commit-card": unknown; "lr-community-card": unknown; "lr-compare-panel": unknown; "lr-condition-builder": unknown; "lr-confirm-bar": unknown; "lr-contact-viewer": unknown; "lr-context-inspector": unknown; "lr-context-meter": unknown; "lr-control-group": unknown; "lr-conversation-item": unknown; "lr-copy-button": unknown; "lr-csv-viewer": unknown; "lr-dashboard-grid": unknown; "lr-data-grid": unknown; "lr-dataset-viewer": unknown; "lr-date-input": unknown; "lr-date-picker": unknown; "lr-details": unknown; "lr-dialog": unknown; "lr-diff-view": unknown; "lr-divider": unknown; "lr-dock-panel": unknown; "lr-document-compare": unknown; "lr-document-library": unknown; "lr-document-preview": unknown; "lr-document-viewer": unknown; "lr-docx-viewer": unknown; "lr-doughnut-chart": unknown; "lr-drawer": unknown; "lr-drilldown-panel": unknown; "lr-drop-zone": unknown; "lr-dropdown": unknown; "lr-dropdown-item": unknown; "lr-ebook-viewer": unknown; "lr-email-viewer": unknown; "lr-embedding-explorer": unknown; "lr-emoji-picker": unknown; "lr-empty": unknown; "lr-entity-card": unknown; "lr-entity-chip": unknown; "lr-entity-dossier": unknown; "lr-env-list": unknown; "lr-eval-dataset": unknown; "lr-eval-result": unknown; "lr-eval-run": unknown; "lr-export-button": unknown; "lr-file-icon": unknown; "lr-file-input": unknown; "lr-file-tree": unknown; "lr-filter-bar": unknown; "lr-flag": unknown; "lr-flow-canvas": unknown; "lr-flow-controls": unknown; "lr-flow-minimap": unknown; "lr-flow-node": unknown; "lr-flow-run-status": unknown; "lr-format-bytes": unknown; "lr-format-date": unknown; "lr-format-number": unknown; "lr-funnel": unknown; "lr-gauge": unknown; "lr-generation-metrics": unknown; "lr-geojson-view": unknown; "lr-geojson-viewer": unknown; "lr-graph": unknown; "lr-graph-legend": unknown; "lr-graph-query-builder": unknown; "lr-grounding-summary": unknown; "lr-handoff-divider": unknown; "lr-heatmap": unknown; "lr-highlight-layer": unknown; "lr-histogram": unknown; "lr-html-viewer": unknown; "lr-icon": unknown; "lr-icon-button": unknown; "lr-image-comparer": unknown; "lr-image-viewer": unknown; "lr-include": unknown; "lr-ingestion-queue": unknown; "lr-input": unknown; "lr-intersection-observer": unknown; "lr-json-viewer": unknown; "lr-kbd": unknown; "lr-knowledge-base": unknown; "lr-knowledge-base-admin": unknown; "lr-knowledge-graph-explorer": unknown; "lr-known-date": unknown; "lr-lightbox": unknown; "lr-line-chart": unknown; "lr-lite-chart": unknown; "lr-live-region": unknown; "lr-locale-picker": unknown; "lr-map": unknown; "lr-markdown": unknown; "lr-markdown-core": unknown; "lr-mcp-app": unknown; "lr-media-card": unknown; "lr-memory-panel": unknown; "lr-mention-popover": unknown; "lr-menu": unknown; "lr-menu-item": unknown; "lr-menu-label": unknown; "lr-message-actions": unknown; "lr-message-feedback": unknown; "lr-message-parts": unknown; "lr-mind-map": unknown; "lr-model-select": unknown; "lr-model-settings-panel": unknown; "lr-multi-split": unknown; "lr-mutation-observer": unknown; "lr-native-time-input": unknown; "lr-neighbor-list": unknown; "lr-node-palette": unknown; "lr-notebook-viewer": unknown; "lr-number-input": unknown; "lr-option": unknown; "lr-otp-input": unknown; "lr-page": unknown; "lr-page-rail": unknown; "lr-pagination": unknown; "lr-pan-zoom": unknown; "lr-path-strip": unknown; "lr-pdf-viewer": unknown; "lr-phone-input": unknown; "lr-pie-chart": unknown; "lr-polar-area-chart": unknown; "lr-policy-summary": unknown; "lr-poll-status": unknown; "lr-popover": unknown; "lr-popup": unknown; "lr-pptx-viewer": unknown; "lr-progress-bar": unknown; "lr-progress-ring": unknown; "lr-prompt-input": unknown; "lr-prompt-queue": unknown; "lr-prompt-studio": unknown; "lr-provenance-panel": unknown; "lr-push-to-talk": unknown; "lr-qr-code": unknown; "lr-radar-chart": unknown; "lr-radio": unknown; "lr-radio-button": unknown; "lr-radio-group": unknown; "lr-rag-answer": unknown; "lr-rag-eval-dashboard": unknown; "lr-random-content": unknown; "lr-rating": unknown; "lr-realtime-session": unknown; "lr-relative-time": unknown; "lr-reorder-item": unknown; "lr-reorder-list": unknown; "lr-resize-observer": unknown; "lr-responsive-panel": unknown; "lr-result-card": unknown; "lr-result-field": unknown; "lr-retrieval-compare": unknown; "lr-retrieval-results": unknown; "lr-retrieval-search": unknown; "lr-retrieval-trace": unknown; "lr-rubric-form": unknown; "lr-scatter-chart": unknown; "lr-json-schema-viewer": unknown; "lr-scroller": unknown; "lr-segmented": unknown; "lr-select": unknown; "lr-selection-toolbar": unknown; "lr-sequence-playback": unknown; "lr-sequence-strip": unknown; "lr-skeleton": unknown; "lr-slider": unknown; "lr-source-card": unknown; "lr-source-list": unknown; "lr-source-picker": unknown; "lr-span-waterfall": unknown; "lr-sparkline": unknown; "lr-spinner": unknown; "lr-split-panel": unknown; "lr-spreadsheet-viewer": unknown; "lr-stack-trace": unknown; "lr-stat": unknown; "lr-stepper": unknown; "lr-stream-status": unknown; "lr-streaming-text": unknown; "lr-streaming-text-core": unknown; "lr-subagent-panel": unknown; "lr-suggestion-chips": unknown; "lr-svg-viewer": unknown; "lr-swatch-picker": unknown; "lr-switch": unknown; "lr-tab": unknown; "lr-tab-group": unknown; "lr-tab-panel": unknown; "lr-table": unknown; "lr-tag": unknown; "lr-task-list": unknown; "lr-terminal": unknown; "lr-test-results": unknown; "lr-textarea": unknown; "lr-thinking-panel": unknown; "lr-thread-list": unknown; "lr-time-input": unknown; "lr-time-range": unknown; "lr-timeline": unknown; "lr-timeline-item": unknown; "lr-toast": unknown; "lr-toast-item": unknown; "lr-token-input": unknown; "lr-tool-approval-dialog": unknown; "lr-tool-call-chip": unknown; "lr-tool-param-form": unknown; "lr-tool-result-dialog": unknown; "lr-tool-result-view": unknown; "lr-tool-select-dialog": unknown; "lr-tool-timeline": unknown; "lr-tooltip": unknown; "lr-tour": unknown; "lr-trace-tree": unknown; "lr-transcript-feed": unknown; "lr-tree": unknown; "lr-tree-item": unknown; "lr-typing-indicator": unknown; "lr-usage-badge": unknown; "lr-video": unknown; "lr-video-playlist": unknown; "lr-virtual-list": unknown; "lr-visually-hidden": unknown; "lr-voice-picker": unknown; "lr-widget": unknown; "lr-widget-renderer": unknown; "lr-word-cloud": unknown; "lr-xml-viewer": unknown; "lr-zoomable-frame": unknown; }` - **`testing-event-factory-contracts`** — Shared utility contracts. `createLyraEvent(/* public names: tag, name, detail */): unknown` See "Constructing a validated test event: `createLyraEvent()`" above for the full contract. - **`testing-happy-dom-shims-contracts`** — Shared utility contracts. `installHappyDomFormAssociatedShims(): unknown` `installStubInternalsForTest(/* public names: host */): unknown` - **`testing-interaction-drivers-contracts`** — Shared utility contracts. `chooseOption(/* public names: owner, value */): unknown` `submitConfirmDecision(/* public names: owner, decision */): unknown` `toggleSwitch(/* public names: switchEl */): unknown` `activateStep(/* public names: stepper, target */): unknown` See "Driving a component's real activation path: interaction drivers" above for the full contract. - **`testing-wait-for-mount-contracts`** — Shared utility contracts. `waitForLyraElement(/* public names: selector, options */): unknown` `WaitForLyraElementOptions { root: unknown; match: (element: unknown) => unknown; timeoutMs: unknown; }` `waitForToast(/* public names: match, options */): unknown` See "Awaiting a lazily registered mount: `waitForLyraElement()` and `waitForToast()`" above for the full contract. - **`theme-gemstones-data-contracts`** — Shared utility contracts. `GemstoneAccent { key: unknown; fill: unknown; deep: unknown; }` - **`theme-gemstones-contracts`** — Shared utility contracts. `gemstoneGlyph(/* public names: color */): unknown` `gemstoneSelectedGlyphStyles: CSSResult` — the shared "selected" halo/shine presentation for a rendered `gemstoneGlyph()`, applied via the `data-lr-gemstone-selected` boolean attribute on the element wrapping the glyph. `lr-swatch-picker mode="gemstone"` includes this exact stylesheet in its own `static styles` for its checked swatch's automatic glyph rather than keeping a private copy, so a glyph rendered anywhere else on the page can match it exactly by consuming the same export and attribute. - **`theme-presets-contracts`** — Shared utility contracts. `applyLyraThemePreset(/* public names: presetOrName */): unknown` `defineLyraThemePreset(/* public names: preset */): unknown` `LyraThemePresetChangeDetail { id: unknown; theme: unknown; }` `LyraThemePreset { id: unknown; theme: unknown; }` - **`theme-theme-contracts`** — Shared utility contracts. `createLyraThemeBootstrap(/* public names: options */): unknown` `getLyraTheme(): unknown` `LyraThemeBootstrapOptions { storageKey: unknown; }` `LyraTheme { mode: unknown; accent: unknown; surface: unknown; }` `setLyraTheme(/* public names: theme */): unknown` - **`vue-contracts`** — Framework integration type contracts. `LyraVueGlobalComponents { "lr-accordion": unknown; "lr-accordion-item": unknown; "lr-activity-feed": unknown; "lr-agent-eval-dashboard": unknown; "lr-agent-run": unknown; "lr-agent-trace": unknown; "lr-agent-workspace": unknown; "lr-alert": unknown; "lr-animated-image": unknown; "lr-animation": unknown; "lr-app-rail": unknown; "lr-app-rail-group": unknown; "lr-app-rail-item": unknown; "lr-approval-queue": unknown; "lr-archive-viewer": unknown; "lr-artifact-panel": unknown; "lr-attachment-chip": unknown; "lr-attachment-trigger": unknown; "lr-audio-visualizer": unknown; "lr-av-player": unknown; "lr-avatar": unknown; "lr-avatar-group": unknown; "lr-badge": unknown; "lr-bar-chart": unknown; "lr-box-plot": unknown; "lr-branch-picker": unknown; "lr-breadcrumb": unknown; "lr-breadcrumb-item": unknown; "lr-browser-frame": unknown; "lr-bubble-chart": unknown; "lr-button": unknown; "lr-button-group": unknown; "lr-calendar": unknown; "lr-calendar-viewer": unknown; "lr-callout": unknown; "lr-card": unknown; "lr-carousel": unknown; "lr-carousel-item": unknown; "lr-chart": unknown; "lr-chat-composer": unknown; "lr-chat-message": unknown; "lr-chat-viewport": unknown; "lr-checkbox": unknown; "lr-checkbox-group": unknown; "lr-checkpoint": unknown; "lr-chip": unknown; "lr-chip-group": unknown; "lr-chunk-inspector": unknown; "lr-citation-badge": unknown; "lr-claim-evidence": unknown; "lr-code-block": unknown; "lr-code-block-core": unknown; "lr-code-editor": unknown; "lr-color-picker": unknown; "lr-combobox": unknown; "lr-command-palette": unknown; "lr-commit-card": unknown; "lr-community-card": unknown; "lr-compare-panel": unknown; "lr-condition-builder": unknown; "lr-confirm-bar": unknown; "lr-contact-viewer": unknown; "lr-context-inspector": unknown; "lr-context-meter": unknown; "lr-control-group": unknown; "lr-conversation-item": unknown; "lr-copy-button": unknown; "lr-csv-viewer": unknown; "lr-dashboard-grid": unknown; "lr-data-grid": unknown; "lr-dataset-viewer": unknown; "lr-date-input": unknown; "lr-date-picker": unknown; "lr-details": unknown; "lr-dialog": unknown; "lr-diff-view": unknown; "lr-divider": unknown; "lr-dock-panel": unknown; "lr-document-compare": unknown; "lr-document-library": unknown; "lr-document-preview": unknown; "lr-document-viewer": unknown; "lr-docx-viewer": unknown; "lr-doughnut-chart": unknown; "lr-drawer": unknown; "lr-drilldown-panel": unknown; "lr-drop-zone": unknown; "lr-dropdown": unknown; "lr-dropdown-item": unknown; "lr-ebook-viewer": unknown; "lr-email-viewer": unknown; "lr-embedding-explorer": unknown; "lr-emoji-picker": unknown; "lr-empty": unknown; "lr-entity-card": unknown; "lr-entity-chip": unknown; "lr-entity-dossier": unknown; "lr-env-list": unknown; "lr-eval-dataset": unknown; "lr-eval-result": unknown; "lr-eval-run": unknown; "lr-export-button": unknown; "lr-file-icon": unknown; "lr-file-input": unknown; "lr-file-tree": unknown; "lr-filter-bar": unknown; "lr-flag": unknown; "lr-flow-canvas": unknown; "lr-flow-controls": unknown; "lr-flow-minimap": unknown; "lr-flow-node": unknown; "lr-flow-run-status": unknown; "lr-format-bytes": unknown; "lr-format-date": unknown; "lr-format-number": unknown; "lr-funnel": unknown; "lr-gauge": unknown; "lr-generation-metrics": unknown; "lr-geojson-view": unknown; "lr-geojson-viewer": unknown; "lr-graph": unknown; "lr-graph-legend": unknown; "lr-graph-query-builder": unknown; "lr-grounding-summary": unknown; "lr-handoff-divider": unknown; "lr-heatmap": unknown; "lr-highlight-layer": unknown; "lr-histogram": unknown; "lr-html-viewer": unknown; "lr-icon": unknown; "lr-icon-button": unknown; "lr-image-comparer": unknown; "lr-image-viewer": unknown; "lr-include": unknown; "lr-ingestion-queue": unknown; "lr-input": unknown; "lr-intersection-observer": unknown; "lr-json-viewer": unknown; "lr-kbd": unknown; "lr-knowledge-base": unknown; "lr-knowledge-base-admin": unknown; "lr-knowledge-graph-explorer": unknown; "lr-known-date": unknown; "lr-lightbox": unknown; "lr-line-chart": unknown; "lr-lite-chart": unknown; "lr-live-region": unknown; "lr-locale-picker": unknown; "lr-map": unknown; "lr-markdown": unknown; "lr-markdown-core": unknown; "lr-mcp-app": unknown; "lr-media-card": unknown; "lr-memory-panel": unknown; "lr-mention-popover": unknown; "lr-menu": unknown; "lr-menu-item": unknown; "lr-menu-label": unknown; "lr-message-actions": unknown; "lr-message-feedback": unknown; "lr-message-parts": unknown; "lr-mind-map": unknown; "lr-model-select": unknown; "lr-model-settings-panel": unknown; "lr-multi-split": unknown; "lr-mutation-observer": unknown; "lr-native-time-input": unknown; "lr-neighbor-list": unknown; "lr-node-palette": unknown; "lr-notebook-viewer": unknown; "lr-number-input": unknown; "lr-option": unknown; "lr-otp-input": unknown; "lr-page": unknown; "lr-page-rail": unknown; "lr-pagination": unknown; "lr-pan-zoom": unknown; "lr-path-strip": unknown; "lr-pdf-viewer": unknown; "lr-phone-input": unknown; "lr-pie-chart": unknown; "lr-polar-area-chart": unknown; "lr-policy-summary": unknown; "lr-poll-status": unknown; "lr-popover": unknown; "lr-popup": unknown; "lr-pptx-viewer": unknown; "lr-progress-bar": unknown; "lr-progress-ring": unknown; "lr-prompt-input": unknown; "lr-prompt-queue": unknown; "lr-prompt-studio": unknown; "lr-provenance-panel": unknown; "lr-push-to-talk": unknown; "lr-qr-code": unknown; "lr-radar-chart": unknown; "lr-radio": unknown; "lr-radio-button": unknown; "lr-radio-group": unknown; "lr-rag-answer": unknown; "lr-rag-eval-dashboard": unknown; "lr-random-content": unknown; "lr-rating": unknown; "lr-realtime-session": unknown; "lr-relative-time": unknown; "lr-reorder-item": unknown; "lr-reorder-list": unknown; "lr-resize-observer": unknown; "lr-responsive-panel": unknown; "lr-result-card": unknown; "lr-result-field": unknown; "lr-retrieval-compare": unknown; "lr-retrieval-results": unknown; "lr-retrieval-search": unknown; "lr-retrieval-trace": unknown; "lr-rubric-form": unknown; "lr-scatter-chart": unknown; "lr-json-schema-viewer": unknown; "lr-scroller": unknown; "lr-segmented": unknown; "lr-select": unknown; "lr-selection-toolbar": unknown; "lr-sequence-playback": unknown; "lr-sequence-strip": unknown; "lr-skeleton": unknown; "lr-slider": unknown; "lr-source-card": unknown; "lr-source-list": unknown; "lr-source-picker": unknown; "lr-span-waterfall": unknown; "lr-sparkline": unknown; "lr-spinner": unknown; "lr-split-panel": unknown; "lr-spreadsheet-viewer": unknown; "lr-stack-trace": unknown; "lr-stat": unknown; "lr-stepper": unknown; "lr-stream-status": unknown; "lr-streaming-text": unknown; "lr-streaming-text-core": unknown; "lr-subagent-panel": unknown; "lr-suggestion-chips": unknown; "lr-svg-viewer": unknown; "lr-swatch-picker": unknown; "lr-switch": unknown; "lr-tab": unknown; "lr-tab-group": unknown; "lr-tab-panel": unknown; "lr-table": unknown; "lr-tag": unknown; "lr-task-list": unknown; "lr-terminal": unknown; "lr-test-results": unknown; "lr-textarea": unknown; "lr-thinking-panel": unknown; "lr-thread-list": unknown; "lr-time-input": unknown; "lr-time-range": unknown; "lr-timeline": unknown; "lr-timeline-item": unknown; "lr-toast": unknown; "lr-toast-item": unknown; "lr-token-input": unknown; "lr-tool-approval-dialog": unknown; "lr-tool-call-chip": unknown; "lr-tool-param-form": unknown; "lr-tool-result-dialog": unknown; "lr-tool-result-view": unknown; "lr-tool-select-dialog": unknown; "lr-tool-timeline": unknown; "lr-tooltip": unknown; "lr-tour": unknown; "lr-trace-tree": unknown; "lr-transcript-feed": unknown; "lr-tree": unknown; "lr-tree-item": unknown; "lr-typing-indicator": unknown; "lr-usage-badge": unknown; "lr-video": unknown; "lr-video-playlist": unknown; "lr-virtual-list": unknown; "lr-visually-hidden": unknown; "lr-voice-picker": unknown; "lr-widget": unknown; "lr-widget-renderer": unknown; "lr-word-cloud": unknown; "lr-xml-viewer": unknown; "lr-zoomable-frame": unknown; }` # Form controls & inputs (`components/forms/`) ## Breaking changes in 10.0.0 `` drops the three members it carried through 9.x as documented one-major back-compat aliases, each with a like-for-like replacement that has shipped since. The `options` property is now `items` — same frozen owned-snapshot contract, still `attribute: false`, so no markup changes. The `label` property/attribute is now `accessibleLabel` (attribute `aria-label`), or the host `aria-label` directly; this is the one worth grepping for, because an un-updated `` leaves the internal `role="radiogroup"` with no accessible name at all rather than failing loudly. And the exported `SwatchOption` type is now `SwatchPickerItem`. The item shape itself is unchanged, including its own per-item `label` field, which is a different member and stays. Also corrected in 10.0.0 — not breaking, but visible. A specificity sweep found rules that were meant to win yet were losing to another rule in the same shadow stylesheet, so their declarations never applied at all. In this family: arrow-keying onto the already-selected option in `` or `` produces a visible keyboard highlight again — `[aria-selected="true"]` was written after the active-descendant rule at equal specificity and swallowed it, so the highlight was absent exactly on the row a user is most likely to arrow onto first. `appearance="filled"` has a focus indicator again on `` and ``, both of which previously had none: the appearance rule out-ranked `:focus-within`, and the only `outline` in the focus rule was `solid transparent`. Both now express appearance as private custom properties, so no `[part]` rule can out-rank another. `` and ``'s active preset regain their pointer feedback, and `` can now veto all three of its mutations (`lr-add` and `lr-token-edit` became cancelable alongside `lr-remove`, which already was — additive; see that section). ## Setter-only `null` clearing in 8.0.0 Several mapped string IDLs accept `null` on assignment without widening their read type. This is a JavaScript/TypeScript compatibility surface: after the write, every getter below still returns its canonical string. Name attributes are removed; live-value writes follow each control's existing live/default-value reflection contract. HTML authors remove an attribute instead of writing the literal text `"null"`. - `.name = null` clears to `''` on `lr-button`, `lr-checkbox`, `lr-color-picker`, `lr-combobox`, `lr-date-input`, `lr-input`, `lr-number-input`, `lr-otp-input`, `lr-radio`, `lr-radio-group`, `lr-select`, `lr-switch`, `lr-textarea`, and `lr-time-input`. - `.value = null` clears to `''` on `lr-color-picker`, `lr-input`, `lr-number-input`, `lr-otp-input`, and `lr-radio-group`. - `.value = null` restores the native checkbox default `'on'` on `lr-checkbox` and `lr-switch` while removing the `value` attribute. An explicit non-null `.value = 'on'` instead reflects `value="on"`, preserving the distinction between an absent native default and an authored value. These are setter-only input types, not nullable states: code that reads any member above continues to receive a `string`. ## The validity alias is cancelable in 8.0.0 Every form-associated control emits `lr-invalid` (no detail, bubbling, composed) as the alias of the native, non-bubbling `invalid` event. In 8.0.0 that alias is **cancelable**, and its cancellation is forwarded to the native event that produced it: ```ts form.addEventListener("lr-invalid", (event) => { event.preventDefault(); // suppresses the browser's own validation bubble, showMyOwnErrorSummary(event.target); // and reportValidity()'s focus/scroll of this control }); ``` `preventDefault()` suppresses only the platform's _default UI_ — the validation bubble, and the focus/scroll `reportValidity()` performs on the first invalid control. The control stays invalid, still fails `checkValidity()`, and still blocks submission. Cancelling a copy of a platform event could only ever mean cancelling the original, which is why the forwarding exists at all: before it, an app wiring `lr-invalid` to its own error banner had no way to stop the native UI appearing alongside it. Nothing changes for code that ignores the event, and the listener has to be attached before the validity check runs. Every per-control mention of `lr-invalid` below inherits this contract; the per-control sections repeat only what is specific to that control. ## Mirrored static validator catalogs The mirrored form-control constructors expose a public `static validators` catalog with the same callable shape as Web Awesome's `Validator[]`: `observedAttributes`, `checkValidity(element)`, and an optional message. The catalog is available on `LyraButton`, `LyraCheckbox`, `LyraColorPicker`, `LyraInput`, `LyraNumberInput`, `LyraOtpInput`, `LyraRadio`, `LyraRadioGroup`, `LyraSelect`, `LyraSlider`, `LyraSwitch`, `LyraTextarea`, and `LyraTimeInput`. Each call returns a fresh array; calling an entry projects the control's current `ValidityState` into `{ isValid, message, invalidKeys }` without mutating the control. ```ts import { LyraInput } from "@aceshooting/lyra-ui/components/forms/input/input.js"; const input = document.querySelector("lr-input")!; const result = LyraInput.validators[0].checkValidity(input); ``` ## The required-field marker A labelled control with `required` set paints ` *` after its label text. It is one shared rule on the `form-control-label` part, so it looks and sits identically on every control that renders that part — here, and on `lr-file-input`, `lr-model-select`, `lr-voice-picker` and `lr-tool-param-form` in the other families. `lr-checkbox`, `lr-switch` and `lr-radio` have no label box of their own and paint none; a control with the part but no label text set paints none either, so no stray glyph is orphaned. Three consumer-settable properties replace it, retune its colour, or suppress it entirely — `--lr-form-control-required-content` (a quoted CSS `content` string; `''` suppresses the marker), `--lr-form-control-required-color` (default `var(--lr-color-danger)`), and `--lr-form-control-required-offset` (default `0`). Each is an inline `var()` fallback rather than a `:host` declaration, so setting one on any ancestor — `:root` included — reaches every marker at once. Full description, worked examples, and why the content string is never localized by the library: `llms/shared.md` → "The required-field marker". ## Disabled and readonly controls publish no invalid state A control **barred from constraint validation** — its own `disabled`, an ancestor `
`, `readonly` on the controls that have it, or anything else that makes `willValidate` false — matches neither `:state(invalid)` nor `:state(user-invalid)`, and reports no violation from `checkValidity()`. That is the native rule: `` and `` both match neither `:valid` nor `:invalid`. It matters because the idiomatic stylesheet rule keys off the tag — `lr-input:state(user-invalid) { border-color: … }` — so a disabled required field that still published `invalid` painted every greyed-out control in the form red. `required`/`optional` are unaffected: they describe the attribute, not the outcome, so a disabled required field still matches `:state(required)`, exactly like native `:required`. Style the barred case through `:state(disabled)`/`:disabled` and `:state(readonly)` instead. Full description in `llms/shared.md` → "CSS custom states". ## `lr-combobox` / `lr-option` Filterable single/multi-select combining a text input with a listbox. Mirrors the core `` API under the `lr-` prefix. **Form-associated** (hand-rolled internals, not the shared `FormAssociated` mixin — see gotchas). Consumer writes on mounted options immediately update the owning picker and its submission, without changing the reset default or emitting picker input/change events. Owner synchronization does not echo as a consumer selected write. A mounted `option.selected` assignment updates the live picker value and form submission immediately, including deselection and equal-value writes; it emits no user input/change event. When `multiple` becomes false, the public value, live option flags, and popup `aria-selected` expose one selected occurrence. The retained multiple-selection history returns if multiple is enabled again without another selection write. Composing keyboard events (`isComposing` or legacy key code 229) remain with filter editing; they do not navigate, select/create a value, or dismiss the popup. Source options with `inert`, including inherited inertness, are unavailable through the popup. Live inert changes refresh row availability. Text, insertion, replacement, removal, slot reassignment/removal, and relevant attributes of `start`/`end`/`prefix`/`suffix` adornments refresh the corresponding cloned presentation; unchanged presentation retains its clone identity while filtering. Host `aria-describedby` resolves external descriptions onto the native combobox filter before local error/hint guidance. References track missing targets, replacement, removal/reinsertion, reconnect, and adoption. Removing `label`, `hint`, or `error-text` safely omits the content while leaving removed string properties at their native `null` readback; explicit empty strings remain supplied empty strings. **First-interaction registration.** Where initial-route weight is stricter than a static combobox registration allows, keep a labelled native `` as the working pre-JavaScript control and import only the granular combobox registration on its first focus. Copy the native value after the import resolves so typing that happens while the chunk is in flight is not lost, then transfer focus explicitly — the browser does not replay the focus event after custom-element upgrade: ```html
``` Leave the native control in place if registration fails. This pattern preserves the initial shell; the full form-label, option, overlay, and first-open positioning contracts arrive in deferred chunks instead of being weakened in a separate partial combobox implementation. An `lr-option` row remains bounded by its owning listbox: the default label ellipsizes and each `start`/`end` (or `prefix`/`suffix`) adornment is capped at 40% of the row. Unbroken metadata therefore cannot widen a 320px LTR or RTL picker. **Adornments in the popup (fixed in 11.0.0).** Before 11.0.0 this paragraph described behavior the code did not have: `lr-combobox` builds its popup from normalized row *data* rather than from the light-DOM nodes, so a slotted `start`/`end`/`prefix`/`suffix` adornment had nowhere to land and simply never rendered — the documented slots and their documented parts were both dead inside the one component `lr-option` exists to feed. They now render. The nodes are **cloned** into the row (`option-start` / `option-end` parts, inert and `aria-hidden`, so they never join the option's accessible name), which means the author's own `` subtree is left exactly where they put it rather than being moved into a shadow root as a side effect of opening a dropdown: ```html France Malta ``` An async `source` row can carry the same two fields (`start`, `end`) alongside its existing `icon`. ### `lr-combobox` **Properties:** - `multiple: boolean = false` (reflected) - `size: LyraSize = 'm'` (reflected — the shared control ladder, so both `2xs`/`xs`/`s`/`m`/`l`/`xl` and the `small`/`medium`/`large` spellings are accepted; also scales the "+N" overflow tag and decorative expand icon; `size="s"` shares its outer control height with `lr-input`, `lr-select`, and `lr-segmented` without part overrides) - `pill: boolean = false` (reflected) — rounds the trigger row's corners to a full pill, mirroring `lr-input`'s own `pill`. It changes the private radius default to `--lr-radius-pill`, so an inherited or direct `--lr-combobox-radius` remains authoritative - `placeholder: string = ''` - `disabled: boolean = false` (reflected) - `required: boolean = false` (reflected — enforced via `internals.setValidity()`; also reflected as `aria-required` on `` immediately. That semantic input exposes `aria-invalid="true"` whenever visible error chrome is present, or after interaction while intrinsic/custom validity fails; it explicitly returns to `"false"` when neither applies) - `name: string = ''` - `label: string = ''` - `hint: string = ''` - `errorText: string = ''` (attribute `error-text` — static error copy shown below the hint; overridden by slotted `error` content when provided) - `open: boolean = false` (reflected) - `allowCreate: boolean = false` (attribute `allow-create`) — a nonmatching query renders a localized create row. Activating it emits cancelable `lr-create`; unless vetoed, the component appends a real `` and selects it (also supported in `multiple` mode) - `allowCustomValue: boolean = false` (attribute `allow-custom-value`) — single-select only; commits arbitrary text on Enter without creating an option - `showUnknownOption: boolean = false` (attribute `show-unknown-option`, reflected) — appends every committed value that no option or async row claims to the end of the listbox as a synthetic, badged, keyboard-reachable, re-selectable row. Off by default. The synthetic row is filtered by the active query exactly like the `allow-create` row is, so a query it does not match neither shows it nor suppresses the "no matches" copy; re-picking it re-commits the same value and deliberately does **not** reclassify it as known — the badge and the row both survive, and the row never appears in `selectedRows` - `getUnknownLabel?: (value: string) => string` (attribute: false) — renders the label for a committed value that matches no option or async row, everywhere it appears (trigger, `multiple` tag, synthetic row). `getTag` cannot serve this case: it is handed a matched option and there is none. A blank return falls back to the raw value. Not consulted while an async `source` fetch has never yet resolved for this element — see "Unknown committed values" below; `loadingText` covers that window instead, since the value is not yet known to be unmatched at all - `appearance: 'filled' | 'outlined' | 'filled-outlined' = 'outlined'` (reflected) - `placement: 'top' | 'bottom' = 'bottom'` (reflected; flip/shift can still keep the listbox in view) - `positioningStrategy: PlaceStrategy = 'fixed'` (attribute `positioning-strategy`, reflected) — the CSS positioning scheme the listbox is laid out with, spelled the same as on `lr-select`, `lr-popover`, `lr-dropdown`, `lr-tooltip` and `lr-color-picker`. `fixed` is this control's default and what it has always rendered: it positions against the viewport and escapes most clipping ancestors, which suits a typeahead list that usually sits inside a scrollable region. `absolute` positions against the nearest containing block and scrolls with it. An unsupported value resolves to the default. Like `placement`, a change takes effect the next time the listbox opens. When the instance sets nothing, the cascading `--lr-positioning-strategy` custom property (`absolute`/`fixed`, set on `:root`, a theme, or one clipping ancestor) is honoured ahead of the default; an explicit instance value always wins. There is deliberately no `hoist` alias here: unlike on `lr-select`, where it is Shoelace's established spelling, it would be a boolean defaulting to `true`, so its attribute could only ever express the value the control already has - `sync?: PlaceSync` (reflected) — copies the trigger's width, height, or both onto the listbox, spelled the same as on `lr-dropdown`/`lr-popup`/`lr-popover`. Unset (the default), the listbox sizes to its own content, clamped between `--lr-size-12rem` and `--lr-size-28rem`, exactly as before. `sync="width"` drops that content-based clamp so a full-width trigger with short option labels gets a listbox that aligns to its own edges instead of floating narrower in the middle. A synced listbox is capped on `--lr-positioner-available-inline-size` **alone** — the space the positioner actually measured beside the anchor, which still keeps an over-wide trigger from pushing it off-screen. `--lr-popover-viewport-clamp` does **not** apply to a synced listbox — its 92vw default used to shorten the listbox against its own trigger in exactly the full-width case `sync` exists for, and `lr-popup` never applied it either. It still applies with `sync` unset. Like `placement`, a change takes effect the next time the listbox opens - `clearable: boolean = false` (reflected) — displays the clear button while there is something to clear on **either** axis this control owns: a committed selection, or _visible_ filter text. See "the clear button covers two axes" below - `withClear: boolean = false` (attribute `with-clear`) — Web Awesome's spelling of `clearable`; either one enables the same clear button. Not deprecated: Web Awesome names this attribute `with-clear` and Shoelace names it `clearable`, so honouring both is what keeps a mechanical tag rename from silently dropping the control - `withLabel: boolean = false` and `withHint: boolean = false` (attributes `with-label` and `with-hint`) — SSR slot-presence hints - `getTag: ((option: LyraOption, index: number) => unknown) | undefined` (attribute: false) — replaces a built-in multiple-selection tag with consumer Lit/DOM/text output; strings render as text, never as HTML - `validators: LyraComboboxValidator[] = []` (attribute: false) — extra JavaScript validators run after the intrinsic `required` constraint, the same contract `lr-date-input` implements. Each entry may be a `(value, input) => void | boolean | string | ValidityStateFlags` function (`value` is the live `string | string[]`), an object with `validate(value, input)` returning that same vocabulary, or a Web Awesome-compatible object with `checkValidity(input)` returning `{ isValid, message, invalidKeys }`, where `invalidKeys` names `ValidityState` flags; that object may also expose `observedAttributes` and a string or callback `message`. Changing any listed host attribute revalidates automatically. `isValid: true` (or `true`/`undefined` from a function) passes; otherwise the listed flags are set (`customError` when the list maps to nothing) and the returned message wins over the validator-level fallback. A throwing validator fails closed with the localized generic message. `disabled` bars them exactly as it bars `required` - `validationTarget: HTMLElement | undefined` — writable native-validity focus anchor. After the first render it defaults to the internal filter input; assign another element to override it, or assign `undefined` to restore that input. It is `undefined` before the input exists - `autocomplete: string = 'off'`, `inputMode: string = ''` (attribute `inputmode`), `enterKeyHint: string = ''` (attribute `enterkeyhint`), `spellcheck: boolean = false`, `autocapitalize: string = ''`, and `autocorrect: boolean = true` (attribute values `on`/`off`) — native editing-assistance properties forwarded to the internal filter input. Removing a `spellcheck` attribute after an override restores this component's declared `false` default. The lowercase mapped IDLs `inputmode` and `enterkeyhint` delegate to the corresponding camel-case native properties - `inputValue: string` — the live filter input text; programmatic writes are event-silent - `maxOptionsVisible: number = 3` (attribute `max-options-visible` — caps how many selected **tags** show before collapsing to `+N`; nothing to do with the suggestion list, see the three-caps note below) - `visibleOptions?: number` (attribute `visible-options`, new in 11.0.0) — bounds the popup to about this many suggestion rows, leaving the rest reachable by scrolling. Purely presentational: every row is still rendered. Measured from where row N actually starts rather than computed from a token, because a row's height varies with `sub` lines, adornments and group labels. Unset imposes no bound of its own and the listbox keeps exactly its previous max-height behavior; zero, negative, and non-finite values normalize to unset rather than collapsing the popup **The three caps, which are easy to confuse.** `visibleOptions` caps how many suggestion rows are *visible* (presentation; the rest scroll). `maxRender` caps how many suggestion rows are *rendered at all* (performance; the rest do not exist and are summarized by `option-overflow`). `maxOptionsVisible` caps how many *selected tags* show in multi-select and never touches the suggestion list. - `emptyText?: string` (attribute `empty-text`) — omission displays localized `noMatches` (`"No matches"` in the built-in English locale); any supplied string, including `''`, renders verbatim - `loadingText?: string` (attribute `loading-text`) — shown while a `source` fetch is in flight, and in place of the raw value on the trigger/tag for a committed value the fetch has never yet resolved (see "Unknown committed values" below); omission displays localized `loading` (`"Loading…"` in English), while any supplied string, including `''`, renders verbatim - `overflowText?: string` (attribute `overflow-text`) — shown when `maxRender` caps the rows; omission displays localized `comboboxOverflow` (`"+{n} more — refine your search"` in English). A supplied template wins verbatim over `.strings`, including when it equals that English template or is empty; `{n}` is still replaced with the locale-formatted hidden count - `filter: OptionFilter | null = null` (attribute: false — `(option, query) => boolean`; default matches `label`/`searchText` case-insensitively; ignored while `source` is set) - `source: ComboboxSource | null = null` (attribute: false — `(query: string, options: { signal: AbortSignal; limit: number }) => Promise`; when set, replaces the light-DOM `` list with an async lookup, debounced by `sourceDelay` ms after each keystroke and re-run on clear/pick. Forward `options.signal` to `fetch(url, { signal })` to cancel the request when a newer query supersedes it or the element disconnects. `loadingText` is shown while a call is in flight; a stale in-flight call that resolves after a newer one (or after disconnect) is dropped via a monotonic token. The exported type requires the `options` parameter; an existing one-parameter `(query) => …` function remains assignable under TypeScript's ordinary function parameter compatibility, but consumers that need cancellation should accept and forward `options.signal`; honor `options.limit` when practical and return `{ rows, total }` to report the provider-side match count. The component independently clone-normalizes the result, retains at most 2,000 rows/250,000 aggregate text units, skips malformed or hostile rows, and never trusts a provider to enforce the ceiling. A current rejection clears stale rows and renders a localized disabled listbox row; that visible row is not a shadow live region. The same localized message is appended to `[data-lr-live-region="assertive"]` in the document for each fresh post-mount rejection, including an identical retry, while raw caught error text stays out of the UI.) - `sourceDelay: number = 200` (attribute `source-delay` — debounce in ms between the last keystroke and the `source` call; `0` fires on every keystroke. Sanitized to a finite non-negative duration, falling back to `200` for a non-finite value) - `maxRender: number = 200` (attribute `max-render` — caps how many rows render at once, always keeping the current selection visible even if it's outside the cap; the excess renders as one `overflowText` row instead of being dropped silently. See "Large option lists" below for how to size it, and when `source` is the better answer. Runtime writes are capped at 1,000) - `sourceTotal: number` (read-only) — provider-side total for the latest accepted response - `sourceTruncated: boolean` (read-only) — whether the response reported or contained more rows than the bounded retained snapshot; the overflow row includes that hidden count - `value: string | string[]` — a getter/setter: plain `string` in single mode, `string[]` in `multiple` mode. Assigning `undefined`/`null` clears the selection; every string, including `''`, is instead a candidate value resolved against the current local options/async rows — an `` (or a matching async row) is legitimate and round-trips like any other value. A value matching no current option/row still commits rather than being dropped or treated as a clear — see "Unknown committed values" below - `customError: string | null` (attribute `custom-error`) — reflected consumer validation message - `selectedRows` (read: `ComboboxSourceRow[]`; write: `readonly ComboboxSourceRow[]`) — structured rows for the current selection, including any opaque `data` payload supplied by an async source row or a light-DOM ``, reached by reference and never deep-cloned. Reads return detached row snapshots. Writes select stable row values resolved against the current or deferred source, dropping duplicate and detached values without emitting selection events. Selected async rows remain available after the query changes or a later source result no longer contains them - `selectionStart`, `selectionEnd`, and `selectionDirection` — selection getters/setters forwarded to the internal input **Unknown committed values.** A committed value matching no current option/row (a stale value from a removed option, or a programmatic assignment with a typo) still commits — the raw string stays fully reachable through `value`/`selectedRows` — but renders a dashed/italic `[part='unknown-value']` badge next to the closed single-select input, or on the relevant `multiple`-mode tag, instead of an unexplained bare label, mirroring ``'s synthetic "not in catalog" stale-value row — see `--lr-combobox-unknown-value-border-style`/`-color` below. The badge is suppressed while an async `source` fetch has never yet resolved for this element — from mount through the debounce delay and the in-flight call itself — and never shown for an `allowCustomValue` commit, which is a sanctioned unmatched value, not a stale one. Over that same unresolved window the raw value itself is also withheld: the trigger (and any `multiple`-mode tag for the same value) shows the `loadingText` placeholder instead, so a value seeded before its async catalogue has ever answered never flashes an unexplained, unbadged machine key. Once the fetch settles — success or failure — the raw value returns, badged if it still matches nothing. **Methods:** `focus(options?)`, `blur()`, `select()`, `setSelectionRange()`, and `setRangeText()` forward to the internal input. `setRangeText()` synchronizes the filter query and visible options. `show(): Promise` and `hide(): Promise` settle after `lr-after-show` and `lr-after-hide`, respectively. `refresh(): void` re-runs the current `source` query without changing the source's identity, its debounce controller, or its delay. Reassigning `source` is a provider change and clears the fetched rows and pending-selection cache; `refresh()` does not. It queues for the next open while the listbox is closed, and is a no-op without a `source`. `resetValidity()` clears consumer custom validity and restores the current intrinsic constraints. `getForm()` returns the owning form, including an external owner selected by the `form` attribute. `setCustomValidity(message)` carries a rejection no client-side constraint can express ("that option is no longer available"): a non-empty message raises `customError`, becomes `validationMessage`, and blocks submission; `''` clears it and restores the control's own computed validity, so a `required` combobox with nothing chosen goes back to `valueMissing` rather than to valid. The message survives every selection change and a `form.reset()` — like a native control, only another `setCustomValidity('')` or `resetValidity()` clears it — and is used verbatim, never localized. **8.0 migration:** the former camel-case string property `autoCorrect` is not retained as a public alias. Set the boolean `autocorrect` IDL, or use `autocorrect="on"` / `autocorrect="off"` in markup. `ComboboxSourceRow = { readonly value: string; readonly label: string; readonly sub?: string; readonly icon?: unknown; readonly start?: unknown; readonly end?: unknown; readonly badge?: string | number; accessibleLabel?: string; data?: unknown; dotColor?: string; group?: string; disabled?: boolean }` — the row shape used by the async `source` path. `start` and `end` (new in 11.0.0) are the async counterparts of ``'s `start`/`end` adornment slots and render as the `option-start` / `option-end` parts, inert and aria-hidden exactly like `icon`. `icon` renders as a decorative leading visual whose rendered subtree stays visible but is inert and hidden from assistive technology; put independent actions outside it. `badge` renders as trailing metadata, `accessibleLabel` can provide richer spoken text than the visible label, and `data` is retained without being rendered for retrieval through `selectedRows`. `dotColor` accepts a valid CSS `color`; invalid values, declaration-breaking input, and `url()` render a transparent dot. The light-DOM `` path normalizes its supported label/sub/dot/group/data fields to the same internal row model — `` is the light-DOM counterpart of an async row's own `data` field, reached by reference through `selectedRows` exactly the same way. When a local option is removed or becomes disabled, or an async response shrinks, an existing keyboard-active row clamps to the nearest enabled survivor. If every row is disabled or removed, `aria-activedescendant` clears; an untouched list with no active row remains untouched. **Events:** typing in the filter exposes the original bubbling/composed, non-cancelable `InputEvent` as exactly one host `input` event (no `value` detail) and does not fire `change`. An actual user selection mutation — pointer or keyboard selection, multiple-value toggle, tag/Backspace removal, or clear — emits exactly one bubbling/composed, non-cancelable `input` `CustomEvent`, immediately followed by the same shape of `change`, then a prefixed `lr-change` alias. All three carry `detail: { value; data: readonly unknown[] }` — `value` is the new committed selection (a string in single mode, a `string[]` in `multiple` mode); `data` is index-aligned with `value`: `data[i]` describes `value[i]` — the opaque `data` payload of a light-DOM `` or an async source row's own `data`, reached by reference and never deep-cloned — or `undefined` in that value's own slot when it currently matches no live row/option (see "Unknown committed values" above; unlike `selectedRows`, which drops that entry instead). `lr-change` mirrors ``'s namespaced alias; subscribe to it when you want a `lr-`-prefixed event, or to the native-style `input`/`change` for parity with a native control. Re-picking the current single value and programmatic/default/reset/restore writes are silent (including on `lr-change`). The clear button emits one `lr-clear` after its `input`/`change`/`lr-change` triple. `lr-activate` (`detail: { value: string }`, bubbling/composed, non-cancelable) fires on **every** activation of an available listbox row — a click, or Enter on the active row — whether or not the selection actually moved. Its `value` is the activated option's own value, **always a single string**, even in `multiple` mode, where the `input`/`change`/`lr-change` triple carries the whole `string[]` instead. It reports that the user picked a row and gates nothing. Use it for the single-select repeat pick that `change`/`lr-change` deliberately stay silent for — "re-run that filter" is a real intent — which is otherwise unobservable, because the rows live in this shadow root, so a retargeted `click` names no option and a keyboard commit produces no click at all. When an activation _does_ move the selection, `input`/`change`/`lr-change` are emitted first, so either listener reads the settled selection. Not fired for typing, for a committed custom value matching no row, for the clear button, or for a programmatic `value` assignment. `lr-filter` (`detail: { value: string }`) reports the in-progress filter text on every user-driven keystroke — the live as-you-typed search string, deliberately _not_ `value`, which is the committed selection. It is the supported way to read that text; reaching into the shadow root for `[part="combobox-input"]`'s value is not. Named `lr-filter` rather than `lr-input` precisely because `lr-input`'s detail on `` is the committed value, and the two must not share a name while carrying different strings. It fires for user edits only. Picking a row, `form.reset()`, dismissing the listbox, and a programmatic `value` write blank the filter silently. `setRangeText()` silently replaces the requested or selected native text range, synchronizes the resulting query and visible options, and refreshes an async source when present; it preserves the committed selection. Activating the clear button is a user edit: when a query existed it emits `lr-filter` with `value: ''` before the clear transaction finishes. `lr-show` and `lr-hide` report the start of listbox visibility transitions. `lr-show` is a cancelable veto point; `lr-hide` is cancelable while connected, but the disconnect-driven close is non-cancelable because an already-removed control cannot honour a veto. Vetoing a connected close is atomic: the filter query, active option, async result rows, reflected `open` state, and overlay ownership remain unchanged, so the host can defer dismissal without reconstructing the search. `lr-after-show` and `lr-after-hide` fire when the corresponding transition settles. `lr-create` carries `detail: { inputValue }` and is also cancelable: preventing it suppresses the default append/select action so the host can normalize and commit its own option. The internal input's `focus` and `blur` are relayed exactly once from the host as owner-realm native `FocusEvent`s. Both bubble, cross the shadow boundary, and preserve `relatedTarget`. `lr-invalid` (no detail) is emitted once as a bubbling/composed, **cancelable** alias when native validity fails — see "The validity alias is cancelable in 8.0.0" above. `lr-source-error` is non-cancelable, `detail: { error, query }` carrying the raw rejection from an async `source` call plus the exact query string that call was made with (the rejected call's own query, not necessarily the live `query`/`inputValue`, which may have moved on — or been cleared by closing the listbox — by the time the rejection settles). The rendered copy stays localized and never shows the raw error. `lr-retry` is cancelable; the built-in failed-load action calls `refresh()`, and `preventDefault()` leaves the failure on screen. While the failure state is the only popup content, the popup swaps `role="listbox"` for `role="dialog"` (the input gains the matching `aria-haspopup="dialog"` and drops `aria-activedescendant`, and the popup carries the localized failure heading as its accessible name). `dialog` is one of the four popup roles WAI-ARIA lets a `role="combobox"` own, so the still expanded `aria-controls` target keeps a valid owner while holding a retry `button` that is not a legal listbox child. A successful retry restores `role="listbox"`. **The clear button covers two axes, and announces only the one that moved.** A combobox owns both a committed selection and an in-progress filter query, so the button renders whenever either has something to clear, and one press clears both: - Clearing a selection emits `input`, then `change`, then `lr-change`, then `lr-clear` — and, if the query was also non-empty, `lr-filter` with an empty `value`. - A **query-only** clear (nothing selected, just typed text) emits `lr-filter` with an empty `value` and deliberately **no** `change` and **no** `lr-clear`. There was no selection transition to report, so announcing one would be a lie. Don't listen for `lr-clear` to detect "the user emptied the field" — listen for `lr-filter` when you care about the query. - The query half of the render gate is scoped to states where the query is actually _visible_: an open listbox in single-select, or any time in `multiple` mode. A closed single-select shows the selected label rather than the query, so a stale query alone never surfaces a button offering to clear text the user cannot see. **Large option lists: sizing `maxRender`, and why the listbox is not windowed.** Every row that survives the filter and fits under `maxRender` is a real DOM element — the listbox renders its rows in full rather than recycling a small window of them across a scroll. That is a deliberate accessibility constraint, not an omission: the filter input carries `aria-activedescendant`, which is an **IDREF**, and an IDREF only resolves within its own tree scope. Moving the rows into the library's windowing primitive (``) would place them one shadow root deeper than the input that has to point at them, where neither the attribute nor its `ariaActiveDescendantElement` element-reflection replacement can reach — element reflection resolves same-root or upward only. So `` stays `role="list"`/`role="listitem"` and is the right tool for feeds and viewers, not for a listbox whose active row must stay addressable. What that means in practice: - **Up to a few hundred rows, raise `maxRender` and move on.** A few hundred flex rows is an unremarkable amount of DOM; a country, currency, or time-zone list (typically 200–450 entries) renders comfortably with `max-render` set to cover it. The cap exists to bound pathological cases, not to make lists of that size expensive. - **Past roughly a thousand rows, reach for `source` instead of a larger cap.** An async source narrows the candidate set before it ever becomes DOM, which is a categorically better trade than rendering everything and asking the browser to lay it out. Pair it with `sourceDelay` to debounce. - **Leave the overflow row doing its job.** When the cap does bite, the excess collapses into one `overflowText` row (default `"+{n} more — refine your search"`) rather than disappearing silently, and the current selection is always kept visible even when it falls outside the cap. Suppressing that row by setting `maxRender` far above the real list size trades a useful "keep typing" affordance for layout work no user asked for. **Slots:** default (`` children), `label`, `hint`, `error` (overrides the `errorText` attribute when provided), plus two adornment slots: - `start` — content at the inline-start of the trigger row, before the selected-value tags and the filter input. It is decorative chrome, **not** an option: only `` elements in the default slot are ever collected into the option list. - `end` — content after the filter input and the built-in clear action, and before the expand icon, so consumer content never sits outboard of the dropdown chevron. - `clear-icon` and `expand-icon` replace the corresponding built-in glyphs. - `source-error` — replaces the built-in failed-`source` state, retry control included. Deliberately named apart from the form-control `error` slot: they are different failures and a field has to be able to show both. **CSS parts:** `form-control`, `form-control-label`, `label`, `form-control-input`, `combobox`, `start` and `end` (the two adornment-slot wrappers, each `hidden` while nothing is slotted into it), `tags`, `tag`, `tag-label`, `tag__content`, `tag__remove-button`, `tag__remove-button__base`, `combobox-input`, `clear-button`, `unknown-value` (the dashed/italic badge shown next to the closed single-select input, or on a `multiple`-mode tag, when the committed value matches no current option/row), `expand-icon`, `listbox`, `group-label` (the heading of an option group — rows sharing a `group` — named as on `lr-select` and `lr-emoji-picker` so one rule styles every grouped list; it labels the `role="group"` wrapper here), `option`, `option-dot` (the leading status dot, when a row's `dotColor` is set), `option-icon` (the inert, aria-hidden decorative leading visual for an async row), `option-start` and `option-end` (the inert, aria-hidden adornments cloned from the source option's `start`/`prefix` and `end`/`suffix` slots, or from an async row's `start`/`end`), `option-label`, `option-sub` (a row's secondary line, when `sub` is set), `option-badge` (an async row's trailing metadata), `option-overflow` (the "+N more" indicator from `maxRender`), `source-error-row` (the listbox row holding the failed-`source` state), `source-error` (the shared failed-load state itself, with `source-error-base`, `source-error-icon`, `source-error-heading`, `source-error-description` and `source-error-actions` forwarded from the composed ``), `retry-button`, `error`, `hint` **TypeScript:** `LyraCombobox` — `value`/`defaultValue` and the `lr-change`/`lr-input` detail `value` narrow to `string` when `Multiple` is `false` and `string[]` (`readonly string[]` in a detail) when `true`. Types only; the runtime and the mirrored surface are unchanged, and an untyped `` keeps `string | string[]`. **The required marker.** `required` with a non-empty `label` paints the library's shared marker on `[part="form-control-label"]` — the one `::after` rule described above, not a copy of it, so `--lr-form-control-required-content`, `--lr-form-control-required-color` and `--lr-form-control-required-offset` retune or suppress it here exactly as they do on `lr-input`. With no label text the part is hidden and no glyph is painted. **Themeable custom properties:** `--lr-combobox-trigger-padding`, `--lr-combobox-trigger-min-height`, `--lr-combobox-font-size`, `--lr-combobox-tag-padding`, `--lr-combobox-tag-font-size`, `--tag-max-size` (default `var(--lr-size-5rem)`), `--show-duration`, `--hide-duration`, and `--lr-combobox-expand-size` (the decorative icon box; each standard size supplies an aligned default), plus shared tokens. `--lr-combobox-gap` (default `--lr-space-xs`, the gap inside `[part='combobox']`) and `--lr-combobox-radius` (default `--lr-radius`, its corner radius) are both retunable without a `::part(combobox)` rule but, unlike the properties above, do not vary by `size` — the same `--lr-button-gap`/`-radius` pattern. The trigger row's own surface became public in 16.0.0, having been a private pair until then: `--lr-combobox-fill` (default `var(--lr-color-surface)`) and `--lr-combobox-border-color` (default `var(--lr-color-border)`). The `filled`/`filled-outlined` treatments default the fill to `var(--lr-color-surface-raised)` and `filled` defaults the border to `transparent`; a value set here wins over every treatment. `--lr-combobox-open-border-color` (default `var(--lr-color-brand)`) recolors the row's edge while it holds focus, which was a hardcoded brand border before. Read the name as the state the listbox opens in rather than as a synonym for `open`: it is bound to `:focus-within`, so it also paints on a focused row whose listbox is closed — after an Escape dismissal, say. It is named for symmetry with `lr-select`'s `--lr-select-open-border-color`, which really is gated on `open`. The shared field halo `--lr-form-control-focus-shadow` (default `none`) paints a `box-shadow` while this control is focused — one name for every field-shaped control in the library, so a halo is configured once instead of per component. It is additive: the focus outline and border cue are the accessibility answer to focus and are never replaced by it. `--lr-combobox-option-active-bg` (default `var(--lr-color-brand-quiet)`) recolors the background of a hovered or keyboard-active `[part='option']` row — the same per-component indirection `lr-select`'s identical `--lr-select-option-active-bg` uses, so a consumer can retheme just this row state without hijacking the shared `--lr-color-brand-quiet` token library-wide. The currently-**selected** row (`[part='option'][aria-selected='true']`) has its own matching set: `--lr-combobox-option-selected-bg` (default `transparent`), `--lr-combobox-option-selected-border` and `--lr-combobox-option-selected-color` (both default `var(--lr-color-brand)`), and `--lr-combobox-option-selected-font-weight` (default `var(--lr-font-weight-semibold)`) — the same four-token indirection `lr-select`/`lr-model-select` already provide for their own selected row. Like the active-bg knob these are inline `var()` fallbacks, not declared on `:host`, so a consumer can retheme the selected row without hijacking `--lr-color-brand` library-wide. The listbox popup itself is a floating surface and paints from the **shared overlay-surface family** (16.0.0), not from the page surface every card and input reads: `--lr-overlay-surface` (default `var(--lr-color-surface-overlay)`), `--lr-overlay-border` (default `var(--lr-color-border)`), `--lr-overlay-radius` (default `var(--lr-radius)`) and `--lr-overlay-shadow-anchored` (default `var(--lr-shadow-m)`). None is declared on `:host`, so one declaration on `:root` — or on any ancestor, to scope it — retints this popup together with every other floating surface, and none of it touches the trigger row the popup drops from. `--lr-combobox-unknown-value-border-style` (default `dashed`) and `--lr-combobox-unknown-value-border-color` (default `var(--lr-color-border)`) retheme the `[part='unknown-value']` badge described above under "Unknown committed values". `--lr-combobox-option-badge-bg` (default `var(--lr-color-brand-quiet)`) retints the `[part='option-badge']` trailing metadata badge on an async row, and the "not in catalog" badge `show-unknown-option` renders on the synthetic unmatched-value listbox row — the same per-component indirection `lr-select`'s `--lr-select-option-badge-bg` uses. `--lr-combobox-trigger-height` pins an **exact** input-container height (both floors and caps it), for pixel-matching an `` or `` in the same toolbar row. It is **undeclared by default**, leaving `--lr-combobox-trigger-min-height` as a floor only and the row free to grow — see "exact-height hatches" under `lr-input` for why `auto` is not a way to opt back out. Intended for a single-row combobox: in `multiple` mode, a tag row long enough to wrap overflows the pinned box visibly (nothing is clipped or made unreachable), so leave it unset there. **CSS custom states:** `blank`, `disabled`, `required`, `optional`, `valid`, `invalid`, `user-valid`, and `user-invalid`. **Optional peer deps:** none. ### `lr-option` **Properties:** - `value: string = ''` - `disabled: boolean = false` - `defaultSelected: boolean = false` (attribute `selected`; property writes do not reflect) — the declarative and `form.reset()` default. Changing it after mount updates the parent's reset baseline without overwriting a dirty live selection - `selected: boolean = false` (property only) — live selectedness. The parent combobox/select updates it as the current value changes; those live writes never rewrite the `selected` attribute or `defaultSelected` - `group: string = ''` (section header) - `searchText: string = ''` (attribute `search-text` — extra text the filter matches beyond the visible label) - `sub: string = ''` (optional secondary line rendered under the label, e.g. a status/date summary) - `dotColor: string = ''` (attribute `dot-color` — optional CSS color for a small leading status dot; invalid values, declaration-breaking input, and `url()` render the dot transparently) - `data?: unknown` (attribute: false) — opaque application payload, e.g. the backend record this option represents. Never read or rendered by this component; retained by reference, never deep-cloned, through the owning `lr-combobox`'s `selectedRows` and the owning `lr-select`'s `selectedData`, and in both controls' `lr-input`/`lr-change`/`input`/`change` event details. Assigning it notifies the owning picker with `lr-option-change`, like `sub`/`dotColor`/`group` - `label: string` — settable WA-compatible plain-text label. A non-empty property/attribute wins; otherwise it resolves to `defaultLabel`. Property writes stay property-only (no reflection) - `defaultLabel: string` (read-only) — normalized accessibility-visible text generated from the flattened default slot. Hidden subtrees are excluded, visible nested `aria-label` values replace their descendants, and `start`/`end`/`prefix`/`suffix` adornments are excluded. Direct and forwarding-slot mutations update the value and notify the owning picker **Method:** `getTextLabel(): string` returns `defaultLabel`, preserving Shoelace's content-derived plain-text contract even when a separate WA `label` override is present. **Events:** `lr-option-change` — bubbles when the option's label or selectable data changes so its parent `lr-combobox` or `lr-select` can refresh its normalized option rows. It is a private child-to-parent refresh signal, not a picker event: the owning `lr-combobox`/`lr-select` consumes it and stops it, so it never reaches a listener on the picker host (whose own contract is `lr-change`/`lr-input`/`change`/`input`). Listen on the `` itself to observe it. **Slots:** default (visible label), `start`/`end` (WA adornments), and `prefix`/`suffix` (Shoelace aliases). `start` and `prefix` project into one leading wrapper; `end` and `suffix` project into one trailing wrapper. **CSS parts:** `base`, `checked-icon`, `label`, `start`/`prefix` (same node), and `end`/`suffix` (same node). **CSS custom property:** `--current-text-color` (default `var(--lr-color-text)`) colors the keyboard-current row. **CSS custom states:** `current` (the host is the roving-focus target), `selected`, `disabled`, and `hover` (pointer presence, including drag sessions). Own-anatomy state hooks are `--lr-option-hover-bg`, `--lr-option-active-bg`, `--lr-option-current-bg`, `--lr-option-current-color`, `--lr-option-selected-font-weight`, and `--lr-option-checked-icon-color`. Their defaults preserve brand-quiet hover/current paint, the shared active mix, semibold selected text, and the brand checkmark. The current-color hook falls back through upstream `--current-text-color`, so existing themes keep working. The stock `lr-combobox` and `lr-select` intentionally treat each option as light-DOM data and render normalized rows in their own shadow roots. Style those rows through the parent's `option`, `option-label`, and related parts; the parts above style an option's own anatomy when it is rendered by a custom owner. ```html France Germany ``` ```html ``` Multi-select submits as **repeated `FormData` entries** (not a joined string) — `new FormData(form).getAll(name)` behaves like a native multi-value control. An unnamed multi-select (`multiple` with no `name`) contributes nothing to the form at all, matching a nameless native ``'s popup, not just a click outside or Escape. - `dotColor`/`sub`/`group` are read from light-DOM `` children as before, but are also first-class fields on `ComboboxSourceRow` for the async `source` path — an async lookup can drive the same grouped/dot/sub-text rendering a static option list can. - `icon`, `badge`, and `accessibleLabel` are async-source row features rather than `` properties. Icons are decorative (`aria-hidden`); use `accessibleLabel` when the visible label/sub/badge combination needs a fuller spoken name. `data`, by contrast, is a first-class `` property too (the light-DOM counterpart of the async row's own `data`): it is deliberately opaque, never rendered, and reachable through the read-only `selectedRows` getter (combobox), the read-only `selectedData` getter (select), and both controls' `lr-input`/ `lr-change`/`input`/`change` event details. - Full ARIA 1.2 combobox pattern (`role=combobox`, roving `aria-activedescendant`, real DOM focus kept on the input) is implemented correctly — a genuine strength, safe to build on. `` sets that option's `defaultSelected`, seeds the live selection, and supplies the `form.reset()` baseline, mirroring native ``. Session-history/autofill state is a JSON string array in `multiple` mode and the plain submitted string in single mode; malformed state restores an empty selection. The listbox renders `aria-multiselectable` in **both** states (`"true"` and `"false"`), never omitting it. **Breaking in 8.0.0:** `value` is now typed `string | string[]` even in single mode. A TypeScript consumer that read it as a plain string needs a narrowing step — `const v = el.value; const single = typeof v === 'string' ? v : (v[0] ?? '');` **Single-option auto-commit.** Opt-in via `autoCommitSingleOption` (default `false` — a select always renders the normal combobox/listbox/chevron trigger unless enabled, matching pre-1.3.0 behavior). When set and exactly one option is available (neither disabled nor inert, including through an inert ancestor), the popup never opens at all: a click, Enter, Space, ArrowDown, or ArrowUp on the trigger commits that sole option directly, and the trigger renders as a plain `role="button"` with no chevron/`aria-haspopup`/`aria-expanded`/`aria-controls`/`aria-activedescendant` rather than a combobox with a permanently inert popup state — opening a one-row list to pick the only available choice is pure friction with no real decision behind it. It never changes `value`/validity defaults on its own: an unselected single-option select stays unselected (and a `required` one stays invalid) exactly like the multi-option case, until the trigger is actually activated. **Properties:** - `placeholder: string = ''` — text shown on the trigger while nothing is selected, and the trigger's accessible name when neither a host `aria-label` nor a `label` supplies one. One exception to "an empty selection always shows this": while `loading` is `true` the trigger shows the localized `loading` text instead — see `loading` below. The accessible name is unaffected either way - `disabled: boolean = false` (reflected) - `required: boolean = false` (reflected — enforced via `internals.setValidity()`) - `name: string = ''` - `label: string = ''` - `hint: string = ''` - `errorText: string = ''` (attribute `error-text` — static error copy shown below the hint; overridden by slotted `error` content when provided) - `open: boolean = false` (reflected). Direct or fieldset-cascaded disablement synchronously forces it closed; every later property or attribute attempt to open remains normalized to `false` until the control is enabled again - `size: LyraSize = 'm'` (reflected — the shared control ladder, same scale as `lr-input`/`lr-combobox`/`lr-button`, for compact toolbar placements that don't fit the default trigger height. Both spellings of every tier are accepted: `2xs`/`xs`/`s`/`m`/`l`/`xl` and `small`/`medium`/`large`) - `appearance: 'accent' | 'filled' | 'outlined' | 'filled-outlined' | 'plain' = 'outlined'` (reflected) — the library's shared field-surface vocabulary. `outlined` (the default) is a bordered surface; `filled` swaps the border for a raised fill; `filled-outlined` keeps both; `plain` drops both; `accent` paints the loud brand fill with on-brand text (the placeholder, expand icon, adornments and chips all ride that on-brand color rather than the quiet-text tokens). Every value keeps the same box, border width and radius, and each restates its own `:hover` feedback. `lr-input` and `lr-textarea` use the same mapped `outlined` default. - `pill: boolean = false` (reflected) — fully-rounded trigger corners. It changes the private radius default to `--lr-radius-pill`, so an inherited or direct `--lr-select-radius` remains authoritative - `placement: Placement = 'bottom'` (reflected) — preferred listbox placement, from the Floating UI vocabulary (`'top'`, `'bottom-end'`, …). `flip`/`shift` may still move the popup to keep it in view, and the `left`/`right` component is swapped under RTL. Assignment while open refreshes positioning in place without closing, firing lifecycle events, or changing stack order - `hoist: boolean = false` (reflected) — switches Floating UI from its mapped absolute strategy to fixed positioning, escaping clipping containers. It also switches live while open; an effective direction change refreshes logical left/right placement by the same path - `positioningStrategy: PlaceStrategy = 'absolute'` (attribute `positioning-strategy`, reflected) — see `` (`llms/components/lr-popover.md`). `hoist: boolean = false` is its retained exact alias; writing either spelling updates the other. This property always reports the instance's own authored value (or the mirrored `absolute` default); when neither spelling is authored, the listbox is actually placed with the cascading `--lr-positioning-strategy` custom property honored ahead of that default — see the listbox's own **Themeable custom properties** below - `sync?: PlaceSync` (reflected) — copies the trigger's width, height, or both onto the listbox, spelled and typed the same as on `lr-popup`/`lr-popover`/`lr-dropdown`/`lr-combobox` (`'width' | 'height' | 'both'`). Unset (the default), the listbox sizes to its own content, clamped between `--lr-size-12rem` and `min(--lr-popover-viewport-clamp, --lr-size-28rem)`, exactly as before. `sync="width"` drops that content-based clamp so a full-width trigger with short option labels gets a listbox that aligns to its own edges instead of floating narrower in the middle. A synced listbox is capped on `--lr-positioner-available-inline-size` **alone** — the space the positioner actually measured beside the trigger, which still keeps an over-wide trigger from pushing it off-screen; `--lr-popover-viewport-clamp` does **not** apply to a synced listbox, and still does with `sync` unset. Assignment while open repositions in place without closing; unsetting it releases the inline width the positioner wrote - `showUnknownOption: boolean = false` (attribute `show-unknown-option`, reflected) — appends every committed value that no `` claims to the end of the listbox as a synthetic, badged, keyboard-reachable, re-selectable row. Off by default - `getUnknownLabel?: (value: string) => string` (attribute: false) — renders the label for a committed value that matches no option, everywhere it appears (trigger, `multiple` tag, synthetic row). `getTag` cannot serve this case: it is handed a matched option and there is none. A blank return falls back to the raw value. Not consulted while `loading` suppresses that same value — see below - `loading: boolean = false` (reflected) — whether a committed value's real label may still be pending because its `` catalog hasn't arrived yet (e.g. an async fetch still in flight). While `true`, a committed value that currently matches no option renders the localized `loading` placeholder in the trigger label or the relevant `multiple` tag instead of the raw value, with no `notInCatalog`/`[part='unknown-value']` badge and no synthetic `showUnknownOption` listbox row — "not yet resolved" is a different state from "known to be missing". It covers an **empty** selection too: with nothing selected — a create form whose catalogue is still being fetched, or an edit form whose saved selection is legitimately empty — the trigger renders that same localized text in place of `placeholder`, from the same `loading` message key, so both halves of a pending state read the same words and one `registerLyraLocale()` translation reaches both. Nothing to re-localize in the consuming app. With `loading` false an empty selection renders `placeholder` exactly as before. The trigger's accessible name never changes for this: a host `aria-label` wins, then `label`, then `placeholder`, then the localized `select` fallback. A value already matching a live option is unaffected. Never mutates `value`/`selectedOptions` itself, and does not itself disable the trigger — pair it with `disabled` when the control should also be non-interactive while pending - `filled: boolean = false` (reflected) — Shoelace alias for the filled trigger treatment - `autofocus: boolean = false` / `title: string = ''` — forwarded to the internal trigger - `multiple: boolean = false` (reflected) — several options selectable at once; see "Multi-select" above. Flipping it re-shapes `value` and the submitted form entry, so it is normally set once declaratively - `maxOptionsVisible: number = 3` (attribute `max-options-visible`) — how many chips render in `multiple` mode before the rest collapse behind a localized "+N" chip. `0` removes the cap entirely. Sanitized to a finite, non-negative integer: a fractional value truncates, a negative one clamps to `0` (i.e. uncapped), and a non-finite one falls back to `3` - `withClear: boolean = false` (attribute `with-clear`, reflected) — renders a clear button while anything is selected (and nothing at all while the selection is empty). It sits in the trigger's inline-end band as a **sibling** of the trigger rather than a child of it — same nesting reason as the chips — so pressing it clears the selection without opening the listbox - `clearable: boolean = false` — Shoelace's spelling of `withClear`; either one renders the same button. Present so a mechanical `sl-select` → `lr-select` rename keeps the clear control - `helpText: string = ''` (attribute `help-text`) — Shoelace alias for `hint`; `hint` wins if both are present. `withLabel`/`withHint` (`with-label`/`with-hint`) are SSR slot-presence hints - `getTag?: LyraSelectTagRenderer` (attribute: false) — `(option: LyraOption, index: number) => unknown`, exported under that name from the component's own module, renders one selected option's chip in `multiple` mode. Whatever it returns replaces the whole built-in `[part="tag"]` element, so re-declare `part="tag"` on your own root node to keep the default styling hooks. A returned **string renders as text, never as markup** (it lands in an ordinary Lit child position). A custom tag replaces the built-in remove control too, so it owns any custom removal affordance. Overflow past `max-options-visible` still collapses into the built-in "+N" chip - `autoCommitSingleOption: boolean = false` (attribute `auto-commit-single-option`) — opts in to the single-option auto-commit behavior described above - `value: string | string[]` — a getter/setter: a plain `string` in single mode (empty when nothing is selected), a `string[]` in `multiple` mode - `defaultValue: string | string[]` (attribute `default-value` accepts the single string form) — reset selection; changing it updates the live value only while the control is pristine. Assigning `undefined`/`null` to either clears the selection; every string, including `''`, is a candidate value resolved against the current ``s instead — an `` is a legitimate row and round-trips like any other value. A value matching no current option still commits — see "Unknown committed values" below - `selectedOptions: LyraOption[]` — a writable, fresh snapshot of the live selected occurrences. Assigning live child options commits their exact occurrences through the same event-silent path as `value`; foreign/detached options are ignored, and single mode keeps only the first. Mutating an array returned by the getter never mutates the control - `selectedData: readonly unknown[]` (read-only) — the opaque `data` payload of each committed value, index-aligned with `value`: `selectedData[i]` describes `value[i]` (or `value` itself in single mode), and stays that value's own slot — `undefined`, never shifted or dropped — when that value currently matches no live option (see "Unknown committed values" below). Always an array the same length as `value`, in both single and `multiple` mode. Reached by reference, never deep-cloned - `customError: string | null` (attribute `custom-error`) — reflected consumer validation message **Unknown committed values.** A committed value matching no current `` (a stale value from a removed option, or a programmatic assignment with a typo) still commits — the raw string stays fully reachable through `value`/`selectedOptions` — but renders a dashed/italic `[part='unknown-value']` badge next to the trigger label, or on the relevant `multiple`-mode tag, instead of an unexplained bare label, mirroring ``'s synthetic "not in catalog" stale-value row — see `--lr-select-unknown-value-border-style`/`-color` below. Set `loading` while that same value's own `` catalog simply hasn't arrived yet (unlike ``, `` has no async `source` of its own, so this is consumer-driven rather than automatic): a still-unmatched value then renders the localized `loading` placeholder instead of the raw value, with no `unknown-value` badge and no synthetic `showUnknownOption` row, since it is not yet known to be missing. Once the matching option mounts, the real label renders on the next render with no `value`/`selectedOptions` re-assignment needed, whether or not `loading` is also flipped back to `false`. The same flag covers the other half of that state: with **nothing** selected the trigger renders the same localized `loading` text in place of `placeholder`, so a consumer never has to hand-write a conditional placeholder bound to the same flag and re-localize, in its own catalogue, the string this control already owns. **Methods:** `focus(options?)`, `blur()`, and `click()` forward to the internal trigger button. `show()` and `hide()` return `Promise` and resolve after `lr-after-show`/`lr-after-hide` once the matching transition settles. `getForm()` returns the browser-resolved form owner, including an external owner selected by `form`. `setCustomValidity(message)` carries a rejection no client-side constraint can express ("that option is no longer available"): a non-empty message raises `customError`, becomes `validationMessage`, and blocks submission; `''` clears it and restores the control's own computed validity, so a `required` select with nothing chosen goes back to `valueMissing` rather than to valid. The message survives every selection change and a `form.reset()` — like a native control, only another `setCustomValidity('')` or `resetValidity()` clears it — and is used verbatim, never localized. `resetValidity()` changes only that consumer error layer: it restores the current intrinsic required/selection validity without changing the selection/default or clearing prior interaction state. **Events:** each real selection change emits, in order, a native `InputEvent` named `input`, `lr-input`, a native `Event` named `change`, then `lr-change`. The native events carry no detail; read `event.target.value`. Both prefixed aliases carry `detail: { value: string | string[]; data: readonly unknown[] }` — `value` is the new committed selection, a string in single mode and a `string[]` in `multiple` mode; `data` is index-aligned with `value` exactly like `selectedData` above (the same reference, `undefined` for a value matching no live option), reached by reference and never deep-cloned. The complete sequence is silent for a programmatic `value` write, `form.reset()`, or session-state restoration. Plus `lr-clear` (no detail; emitted by the `with-clear` button _after_ its `input`/`lr-input`/`change`/`lr-change` run, and never when there was nothing to clear, so it never announces a no-op), `lr-show`, `lr-hide`, and bubbling, composed `focus`/`blur` events re-dispatched from the internal trigger. `lr-show` is cancelable; `lr-hide` is cancelable while connected and non-cancelable only for the disconnect-driven close, where a veto cannot be honoured. A direct or fieldset-cascaded disablement is a policy closure rather than a user-requested transition: it synchronously closes without the vetoable `lr-hide` or settled `lr-after-hide` lifecycle, and a listener cannot hold a disabled popup open. `lr-after-show` and `lr-after-hide` fire after the corresponding listbox transition has settled; an interrupted transition drops its stale after-event. `lr-invalid` (no detail, cancelable) fires when a validity check finds the control invalid. `lr-activate` (`detail: { value: string }`, bubbling, composed, non-cancelable) fires on **every** activation of an available listbox row — a click, or Enter/Space on the active row — whether or not the selection actually moved. Its `value` is the activated option's own value, **always a single string**, even in `multiple` mode, where `lr-input`/`lr-change` carry the whole `string[]` instead. It reports that the user picked a row and gates nothing. Use it for the single-select repeat pick that `change`/`lr-change` deliberately stay silent for (matching a native ``'s own type-ahead; the buffer resets ~500ms after the last keystroke. In closed `multiple` mode the bounded search skips already-selected option occurrences and continues to a later unselected match, including a distinct row that carries the same public string value. - `` sets that option's `defaultSelected`, seeds the live selection, and supplies the `form.reset()` baseline, mirroring native ``'s popup, without restoring focus and undoing the browser's native Tab/Shift+Tab destination. - The trigger's accessible name now checks a host-level `aria-label` attribute first, before falling back to `label`/`placeholder`/`"Select"` — a plain `aria-label` on `` is no longer silently ignored. Precedence is presence-based: `aria-label=""` remains an explicit empty override rather than restoring any fallback. - With `autoCommitSingleOption` set, a select with exactly one available option never exposes `role="combobox"`/opens a listbox at all — see "Single-option auto-commit" above. Testing/automation code that always expects a `role="combobox"` trigger, or that opens the listbox before asserting on a row, either needs at least two available options or should leave `autoCommitSingleOption` unset to observe the normal dropdown chrome. --- ## `lr-date-picker` / `lr-date-input` (+ `calendar-core.ts`) Mirrors the ``/`` 3.11 public API under `lr-`. Both components are **experimental since 3.8**. Values use ISO 8601: `YYYY-MM-DD` (single) or `YYYY-MM-DD/YYYY-MM-DD` (range). For `lr-date-input`, host `aria-describedby` targets resolve in the host's root and describe the native combobox input through element references, before its existing error and hint descriptions. Replacement, removal, reinsertion, newly resolved IDs, reconnect, and adoption update those relationships. Removing `label`, `hint`, or `error-text` safely omits that copy while preserving `null` property readback; later supplied values render normally. `lr-date-input`'s `clearLabel`, `openLabel`, and `dialogLabel` retain their initial defaults (`''`, `''`, and `'Choose date'`). Omitted labels localize; explicitly supplied text, built-in English labels, and empty strings win over `.strings`. Attribute removal restores localization while preserving `null` readback. The composed calendar applies the same range endpoint and inclusive-length admission rules as the standalone picker. An authored host `aria-label` names an enclosing calendar group. Individual month and selection grids retain their distinct generated period names; changing or removing the purpose label updates the group without changing a caller-authored host role. Removing the `value` attribute renders an empty selection safely while preserving `null` readback; a subsequent valid value works normally. Live constraints repair roving state without moving focus from an unrelated control. If a focused cell becomes unavailable, focus recovers onto an enabled cell. Explicit distant bounds seed the bounded automatic search within the permitted domain; a genuinely empty domain has no enabled roving stop. Selected day and range-endpoint buttons retain their foreground/background pairing during hover and press; author state-token overrides remain available. Month/year/decade state buttons retain the common typography, padding, border reset, and minimum action size. The `calendar-core.ts` helper `formatISO()` returns an empty string for invalid dates or years outside `0000`–`9999`, so generated ISO anchors never advertise an unsupported signed or five-digit year. The ISO model is proleptic Gregorian in every locale and supports years `0000`–`9999`, including `0000`–`0099` without JavaScript's `Date` 1900 remap. Navigation anchors remain within that domain; moving past either boundary leaves a valid roving stop and does not change the selected value. Month/day names and visible day/week digits follow the effective locale while formatters explicitly select the Gregorian calendar. `lr-date-input` uses locale `formatRange()` for range presentation and normalizes locale digits plus bidi marks before parsing, so its own Arabic/Persian display round-trips to the same ISO value. ### `lr-date-picker` Inline month-grid calendar, not form-associated (used standalone or embedded inside `lr-date-input`'s popover). **Properties (28):** - `dayContent` (JS only): `LyraDatePickerDayContent | undefined` - `presets: LyraDateRangePreset[] = []` (JS only, new in 11.0.0) — `LyraDateRangePreset { label: string; start?: string; end?: string; id?: string }`, where `start`/`end` are ISO `YYYY-MM-DD` and `id` is an optional caller-owned correlation key, echoed verbatim on `appliedPreset` and never read by the picker itself. **Either bound may be omitted (new in 11.1.0)** to mean an OPEN bound, resolving to the picker's `min` / `max` respectively — that is how an "All time" preset is expressed. When the corresponding `min`/`max` is unset there is nothing to resolve to (a `value` of `YYYY-MM-DD/YYYY-MM-DD` has no unbounded spelling), so that preset's button renders **disabled** rather than looking live and doing nothing when pressed. Renders a `[part="presets"]` quick-range button row above the calendar, for the dashboard time-filter shape (Today / Last 7 days / Last 30 days / This month / All time). **Range mode only** — a preset names two dates, so it is ignored for a single-date picker rather than rendering a row that cannot do anything; unset renders nothing at all. Applying one commits through the same path a two-click selection uses, so the ISO serialization, the `min`/`max` clamping and the `input`-then-`change` pair are identical and a consumer's change handler cannot tell them apart. A reversed preset normalizes; a malformed one is ignored rather than clearing the current value, so a bad entry in a config-driven list never reads as "the user picked nothing". Non-array runtime assignments normalize to the empty collection, and null/non-object entries in an otherwise valid array are omitted from the rendered row rather than aborting the calendar. Empty and whitespace-only labels are also omitted, preserving named siblings. Explicit endpoints clamp to `min`/`max` before admission. Both endpoints must be selectable, and the inclusive length must satisfy `minRange`/`maxRange`; invalid outcomes render disabled and do not emit value events. Interior dates need not all be enabled. A same-day manual completion obeys those same inclusive length limits. Long preset labels wrap in narrow allocations, including unbroken text and RTL. The active button carries `aria-pressed="true"` and `data-active`. Deliberately the same `label`/`start`/`end`/`id` shape as ``'s `TimeRangePreset`, so the library has one preset vocabulary rather than two — the only difference is the unit (ISO dates, not numbers) - `appliedPreset: LyraDateRangePreset | undefined` (read-only, new in 11.1.0) — the preset whose button produced the current `value`, or `undefined` when the range was picked by hand, cleared, or changed externally. `clear()` removes identity before synchronous `input`/`change` listeners run; an external value change removes identity silently. An accepted preset retains its exact caller-owned source identity through its own update. Read it inside your own `change`/`input` handler. It exists because a dashboard filter has to persist *which* preset is active rather than the pair it froze to: "Last 7 days" must still mean the last 7 days after tomorrow's reload. That fact is not recoverable from `value` — re-deriving it by string-matching is the mapping table `presets` exists to delete, and it is ambiguous anyway (Today and This month coincide on the 1st of a month, and a hand-picked range can equal a preset's pair by construction). A property rather than an event detail because `input`/`change` here are **native** events, deliberately indistinguishable from a manual selection so existing handlers need no special case, and a native `Event` cannot carry a detail without changing type - `disabled: boolean = false` (reflected) - `disabledDates: string | string[] | Date[] = ''` (attribute `disabled-dates`) - `disabledDaysOfWeek: string = ''` (attribute `disabled-days-of-week`) - `disableFuture: boolean = false` and `disablePast: boolean = false` (reflected) - `firstDayOfWeek: LyraDatePickerFirstDayOfWeek = 'auto'` (`'auto'|'sun'|'mon'|'tue'|'wed'| 'thu'|'fri'|'sat'`; attribute `first-day-of-week`, reflected) - `focusedDate: string = ''` (attribute `focused-date`, reflected) - `isDateDisabled?: (date: Date) => boolean` (JS only) - `locale: string = ''` (reflected; malformed tags fall back to the platform locale) - `max: string = ''` and `min: string = ''` (reflected ISO bounds) - `maxRange: number = 0` and `minRange: number = 0` (attributes `max-range`/`min-range`, reflected; positive values count both range endpoints) - `mode: 'single'|'range' = 'single'` (reflected; unknown values normalize to `single`) - `months: 1|2 = 1` (reflected; finite values are truncated and clamped to `1..2`) - `pageBy: 'months'|'single' = 'months'` (attribute `page-by`, reflected) - `readonly: boolean = false` (reflected) - `size: LyraSize = 'm'` (reflected; the shared `2xs`–`xl` ladder plus `small`/`medium`/`large` aliases) - `today: string = ''` (reflected ISO override for deterministic today styling/constraints) - `value: string = ''` (reflected) - `valueAsDate: Date | null` and `valueAsRange: { from: Date|null; to: Date|null }` (JS-only accessors; setters are silent and normalize reversed ranges) - `view: 'days'|'months'|'years'|'decades' = 'days'` (reflected) - `weekdayFormat: 'narrow'|'short'|'long' = 'short'` (attribute `weekday-format`, reflected) - `withOutsideDays: boolean = false` and `withWeekNumbers: boolean = false` (reflected) Lyra retains the additive `previousLabel`/`nextLabel` accessible-label overrides and the `selection` range getter. Their initial readback remains `'Previous month'` and `'Next month'`; omitted labels localize, while explicit text, the built-in English labels, and empty strings win over locale and `.strings` copy. Removing either label attribute restores localized omission while preserving `null` property readback. **Methods:** `clear()`, `focus(options?)`, `goToToday()`, and `goToDate(date: string | Date)`. Valid navigation dates are clamped to `min`/`max`; invalid values are ignored. **Keyboard:** The day grid uses one roving Tab stop. Month, year, and decade selection views do the same: Arrow keys move through their four-column visual grid (with horizontal movement mirrored in RTL), Home/End move to the first/last enabled period in the current page, and Enter/Space drills into the focused period. Moving beyond a selection-grid edge opens the adjacent period page; disabled periods never receive the roving focus. A period is enabled only when it contains at least one date selectable under the active bounds, past/future limits, disabled dates/weekdays, predicate, and pending-range limits; activating an unavailable period is a no-op. **Events:** all are non-cancelable. `input` is a bubbling/composed native `InputEvent` (including the first endpoint of a range); `change` is a bubbling/composed native `Event` for committed values. `lr-focus-day` carries `{ date: Date }`, and `lr-view-change` carries `{ view, date }`. **Slots:** `header`, `previous-icon`, `next-icon`, and `footer`. A dynamic `day-YYYY-MM-DD` slot is also accepted as a Lyra extension and takes precedence over `dayContent`. **Custom states:** `disabled`, `range`, and `readonly`. **CSS parts (37):** `date-picker` / permanent compatibility name `base` (tokens on the same visible shell; both names remain supported), `day`, `day-disabled`, `day-label`, `day-outside`, `day-placeholder`, `day-range-end`, `day-range-inner`, `day-range-preview`, `day-range-start`, `day-selected`, `day-today`, `day-weekend`, `footer`, `grid`, `header`, `month`, `month-label`, `months`, `nav`, `next`, `previous`, `title`, `view-cell`, `view-grid`, `view-item`, `presets` (the quick-range row), `preset-button` (one quick-range button; carries `data-active` while its range is the current value), `view-item-disabled`, `view-item-selected`, `view-item-today`, `view-row`, `weekday`, `weekdays`, `weeknumber`, and `weeknumbers`. Lyra additionally retains the existing `week` part. **Themeable custom properties:** `--lr-cell-size` (default `2.25rem`, controls day-cell/grid-column size; its private default follows the `size` tier — `2xs`/`xs`/`s`/`l`/`xl`; `m` keeps the default). An inherited or direct public value remains authoritative in every tier. **Optional peer deps:** none. ### `lr-date-input` Text field + calendar popover, **form-associated** via the shared `FormAssociated` mixin (`name`, `value`, `disabled`, `required` all inherited). **Properties (44):** - `appearance: 'filled'|'outlined'|'filled-outlined' = 'outlined'` (reflected) - `appliedPreset: LyraDateRangePreset | undefined` (read-only, new in 12.0.0) — the `presets` entry whose button produced the current `value`, or `undefined` when the value was picked on the calendar, typed into the field, cleared, or reset. Read it inside your own `change`/`input` handler; it is updated before those events are relayed, so a handler observes the preset that caused the very commit it is handling, and it is `undefined` while the popover has never been opened. Mirrors the nested `lr-date-picker`'s identically-named property across this component's shadow boundary, which is where the readback is actually needed: the compact text-field-plus-popover shape is the one a dashboard filter uses, the nested picker instance is unreachable from outside (a CSS part cannot yield it), and the fact is not recoverable from `value` — re-deriving it by string-matching is the mapping table `presets` exists to delete and is ambiguous anyway. A property rather than an event detail because `input`/`change` here are **native** events that cannot carry one - `assumeInteractionOn: string[] = ['input']` (JS only) - `autocomplete: string = ''` - `dayContent?: LyraDatePickerDayContent` (JS only) - `defaultValue: string = ''` (reset value; reflected through the `value` content attribute) - `disabled: boolean = false` - `disabledDates: string | string[] | Date[] = ''` and `disabledDaysOfWeek: string = ''` - `disableFuture: boolean = false` and `disablePast: boolean = false` (reflected) - `distance: number = 0` (reflected; finite offset from the anchor) - `firstDayOfWeek: LyraDateInputFirstDayOfWeek = 'auto'` (reflected) - `form: HTMLFormElement | null` (JS-only FACE owner) - `hint: string = ''` - `isDateDisabled?: (date: Date) => boolean` (JS only) - `label: string = ''` - `max: string = ''` and `min: string = ''` (reflected ISO bounds) - `maxRange: number = 0` and `minRange: number = 0` (reflected; positive values include both endpoints) - `mode: 'single'|'range' = 'single'` (reflected) - `months: 1|2 = 1` (reflected; finite values are truncated and clamped) - `name: string = ''` (reflected) - `open: boolean = false` (reflected) - `pageBy: 'months'|'single' = 'months'` (reflected) - `pill: boolean = false` (reflected) - `placement: LyraDateInputPlacement = 'bottom-start'` (reflected; all 12 side/alignment placements are accepted) - `presets: LyraDateRangePreset[] = []` (JS only, new in 11.1.0) — forwarded verbatim to the nested `lr-date-picker`, whose own `presets` documents the semantics (range mode only, open bounds resolving to `min`/`max`, unset renders nothing). Forwarded rather than reimplemented because the picker lives in this component's shadow root, so a consumer has no route to it. The row's parts are re-exported as `presets`/`preset-button`, and `appliedPreset` above reports which entry produced the current value - `readonly: boolean = false` and `required: boolean = false` (reflected) - `size: LyraSize = 'm'` (reflected; `2xs`–`xl` and aliases) - `today: string = ''` (reflected ISO override) - `validationTarget: HTMLElement | undefined` (JS only) — writable native-validity focus anchor. It defaults to the internal input after first render; assign another element to override it, or assign `undefined` to restore the internal input - `validators: LyraDateInputValidator[] = []` (JS only) — each entry may be a `(value, input) => void | boolean | string | ValidityStateFlags` function, an object with `validate(value, input)` returning that same result vocabulary, or a Web Awesome-compatible object with `checkValidity(input)`. The mapped object returns `{ isValid, message, invalidKeys }`, where `invalidKeys` names `ValidityState` flags; it may also expose `observedAttributes` and a string or callback `message`. Changing any listed host attribute automatically runs validity again. `isValid: true` passes. Otherwise the listed flags are set (`customError` is used when the list is empty) and the returned message wins over the validator-level fallback. - `value: string = ''` (JS property) - `valueAsDate: Date | null` and `valueAsRange: { from: Date|null; to: Date|null }` (JS-only accessors; setters are silent and normalize reversed ranges) - `weekdayFormat: 'narrow'|'short'|'long' = 'short'` (reflected) - `withClear: boolean = false`, `withHint: boolean = false`, and `withLabel: boolean = false` - `withOutsideDays: boolean = false` and `withWeekNumbers: boolean = false` (reflected) Lyra retains additive native-wrapper and form-chrome properties: `placeholder`, `locale`, `errorText`, `accessibleLabel` (attribute `aria-label`), `clearLabel`, `openLabel`, `dialogLabel`, `spellcheck`, `autocapitalize`, `autoCorrect` (attribute `autocorrect`), `inputMode: string = ''` (attribute `inputmode`), `enterKeyHint: string = ''` (attribute `enterkeyhint`), and the reflected `customError: string | null` (attribute `custom-error`). `withLabel` and `withHint` are SSR hints: they force those slot wrappers into the first render so server output and hydration have the same structure even before assigned-slot state is observable. The shared Lyra FACE contract also reflects `disabled` and accepts a `form` content-attribute owner ID in addition to the element-valued `form` IDL. **Methods:** `blur()`, `clear()`, `focus(options?)`, `formStateRestoreCallback(state)`, `hide()`, `resetValidity()`, `setCustomValidity(message)`, and `show()`. The shared form contract additionally exposes `getForm()`, `checkValidity()`, and `reportValidity()`; Lyra's native wrapper also exposes `click()`. `show()` and `hide()` return promises that settle after their corresponding transition; they do nothing when already settled, and respect cancellation of their request event. `clear()` is a no-op while blank, disabled, or readonly; otherwise it emits `lr-clear`, then `input`, then `change`. Lyra also retains native-wrapper `select()`, `setSelectionRange()`, and `setRangeText()`. The text input is itself the popup-opening combobox owner: it exposes `role="combobox"`, `aria-haspopup="dialog"`, and explicit `aria-controls`/`aria-expanded` alongside the expand button. Host focus/click/show/clear calls are synchronous no-ops as soon as direct or fieldset disablement starts, including before Lit has updated the inner native controls. **Getters:** `input: HTMLInputElement | undefined` — the internal native ``, for direct DOM access. **Selection properties:** `selectionStart`, `selectionEnd`, and `selectionDirection` mirror the internal native date input. **Events:** `input` is an `InputEvent`, `change` is an `Event`, and `focus`/`blur` are `FocusEvent`s preserving `relatedTarget`; each is dispatched exactly once from the host and is bubbling, composed, and non-cancelable. `lr-show`/`lr-hide` are cancelable requests emitted before state changes; `lr-after-show`/`lr-after-hide` are non-cancelable and fire after rendering and popup animations settle. `lr-clear` is non-cancelable. `lr-invalid` **is** cancelable: `preventDefault()` on it suppresses the browser's native validation bubble and `reportValidity()`'s focus/scroll of this control, without making the control valid — see "The validity alias is cancelable in 8.0.0" above. **Slots (10):** `clear-icon`, dynamic `day-YYYY-MM-DD`, `end`, `expand-icon`, `footer`, `hint`, `label`, `next-icon`, `previous-icon`, and `start`. Lyra additionally retains `error`, which overrides `errorText`. The editable input shrinks first in a constrained row; `start` and `end` adornments are each capped at 40% and ellipsize unbroken content. Clear and calendar actions retain their fixed target. **Custom states:** `blank`, `disabled`, `open`, and `range`; the shared form-associated mixin also exposes its validity states. **CSS parts (21):** `clear-button`, `date-input`, `date-picker`, `presets` and `preset-button` (forwarded from the nested `lr-date-picker` via `exportparts`, so the quick-range row is styleable from outside — new in 11.1.0), `end`, `expand-button`, `expand-icon`, `form-control`, `form-control-input`, `form-control-label`, `hint`, `input`, `input-wrapper`, `popup`, `range-separator`, `segment`, `segment-literal`, `start`, permanent compatibility name `base` (a nested wrapper inside `date-input`), and permanent compatibility name `label` (the inner label-content wrapper inside `form-control-label`). Lyra additionally retains `error`. **Form value and validation:** a complete range submits `YYYY-MM-DD/YYYY-MM-DD`. A first range endpoint remains visible in `value` but contributes the empty string to `FormData` until the second endpoint is selected. `min`/`max`, past/future limits, disabled dates/weekdays, the predicate, range length, `required`, and configured validators all feed FACE validity. Reset and state restore use the same normalization path as direct property writes. **The required marker.** `required` with a non-empty `label` paints the library's shared marker on `[part="form-control-label"]` — the one `::after` rule described above, not a copy of it, so `--lr-form-control-required-content`, `--lr-form-control-required-color` and `--lr-form-control-required-offset` retune or suppress it here exactly as they do on `lr-input`. With no label text the part is hidden and no glyph is painted. **Themeable custom properties:** `--lr-date-input-padding-block` (default `--lr-form-control-padding-block`) pads the native text input; `--lr-date-input-padding-inline` (default `--lr-form-control-padding-inline`) pads the row. `--lr-date-input-font-size` defaults to `--lr-form-control-font-size`. The row floor `--lr-date-input-control-min-height` defaults to `--lr-form-control-height`. All four defaults follow the shared size ladder, including `small`/`medium`/`large` aliases and inherited `--lr-theme-form-control-height-*` overrides. Inherited or direct public values win. `pill` changes the private radius default to `--lr-radius-pill`; a public `--lr-date-input-radius` still wins. Calendar and clear actions fit inside the selected row height while retaining at least 24×24 CSS-pixel targets. At the default root size, rows measure 30/40/48/56px for s/m/l/xl; 2xs and xs grow to 26px to accommodate the 24px action plus the row borders. Custom content or font metrics can grow an unconstrained row. A theme setting the small tier to 36px produces a 36px date row. `--lr-date-input-control-height` remains undeclared by default and pins an exact row height when set. If it forces a row below its target size, the action overflows instead of shrinking. The mapped `--show-duration` and `--hide-duration` hooks independently retime the popup's enter and exit transitions; both default to `var(--lr-transition-fast)`. Clear and calendar action state hooks are `--lr-date-input-action-hover-color`, `--lr-date-input-action-hover-bg`, and `--lr-date-input-action-hover-radius` (defaults: text, transparent, and the input radius), plus `--lr-date-input-action-active-color`, `--lr-date-input-action-active-bg`, and `--lr-date-input-action-active-radius` for the pressed state. They inherit from theme ancestors; direct values on `lr-date-input` win without retuning library-wide tokens. `--lr-positioning-strategy` (16.0.0) — the calendar popup reads this same cascading `absolute`/`fixed` override documented on `` when it is (re)positioned, falling back to its own `fixed` default when nothing is set. There is no per-instance `positioning-strategy` property on ``; set the custom property on `:root`, a theme, or one clipping ancestor to change every unset date input beneath it. **Optional peer deps:** none. ```html ``` `calendar-core.ts` exports the pure date math both components share (**internal — not re-exported from the package root**, but worth knowing when reasoning about behavior): `parseISO`, `formatISO`, `isSameDay`, `addMonths`, `addMonthsClampingDay(date, n)` (like `addMonths` but clamps the result to the target month's last day instead of overflowing into the month after — e.g. Jan 31 + 1 month lands on Feb 28/29, not Mar 3; backs `lr-date-picker`'s PageUp/PageDown), `clampDate`, `monthMatrix(year, month, firstDayOfWeek)`, `weekdayLabels(firstDayOfWeek, format, locale)`, `monthTitle(year, month, locale)`, `resolveFirstDayOfWeek(value, locale?)`, `normalizeCalendarMode(value)`, `normalizeCalendarMonths(value)`, `normalizeWeekdayFormat(value)`, and `dateTimeFormat(locale, options)`. **Known gotchas:** - `first-day-of-week="auto"` now derives from `locale` when the runtime's `Intl.Locale` exposes week-info (`weekInfo`/`getWeekInfo()`, still shifting between engines) — `resolveFirstDayOfWeek()` only hardcodes Sunday as the fallback when that isn't available or `locale` is unset. A French/German-locale user with a supporting runtime now gets the Monday-first grid their OS would show; on an older runtime it still falls back to Sunday-first silently. **If you need a guaranteed locale-correct week start regardless of runtime**, pass an explicit `first-day-of-week="mon"` (accepted values: `sun`/`mon`/`tue`/`wed`/`thu`/`fri`/`sat`). - Runtime attribute/property inputs are normalized before calendar math or `Intl` formatting: unsupported modes and weekday formats use their documented defaults, month counts cannot exceed the two-grid API, malformed locale tags use the platform locale, and invalid `Date` objects do not replace the current view. - (date-input only) — opening the calendar does not move focus into it: `Alt+ArrowDown` leaves focus on the text input, and the expand button keeps focus when clicked. The focused opener is remembered. Escape and a finalized calendar selection close the popup and return focus to that same connected element; a direct `.open = false` also restores it when focus would otherwise remain inside the now-hidden popup. Outside-pointer dismissal deliberately leaves focus at the clicked target. - (date-input only) — typing an unparseable string and blurring/committing sets `badInput` via `internals.setValidity()` and reverts the displayed text to the last valid commit. The visible `` mirrors `required` through native `required`/`aria-required` and exposes touched outer validity through `aria-invalid`; this includes required, bad-input, and range failures owned by the form-associated host. A host `aria-label` is reactively forwarded to the input, including later changes/removal, and takes precedence over the component's label/placeholder fallback. Native validation attempts reveal `aria-invalid`; `form.reset()` clears that touched presentation. A parseable typed date outside an active bound is committed instead and reports the precise `rangeUnderflow`/`rangeOverflow` state. - (date-input only) — declarative, IDL, reset, and restored values are sanitized to exact ISO dates: calendar-invalid or malformed strings become `''` (and therefore `valueMissing` when required), while valid dates outside `min`/`max` or `disable-past`/`disable-future` remain submitted and expose `rangeUnderflow`/`rangeOverflow`. Changes to these constraints, `mode`, and `readonly` recompute validity synchronously; range mode validates both endpoints. - The grid keyboard pattern (Arrow/PageUp/PageDown/Home/End navigation with correct focus sequencing) is implemented correctly and safe to rely on, as is the selected/range-day text color (`--lr-date-picker-selected-color`, defaulting to `--lr-color-on-brand`, not a hardcoded literal). Override selected background and foreground together to preserve contrast. **Additional API surface:** - `--lr-date-picker-month-gap` — Gap between visible months. Default: `var(--lr-space-l)`. - `--lr-date-picker-header-gap` — Month-header child gap. Default: `var(--lr-space-s)`. - `--lr-date-picker-radius` — Calendar and control corner radius. Default: `var(--lr-radius)`. - `--lr-date-picker-nav-hover-bg` — Hover background of the previous/next month-navigation buttons. Default: `var(--lr-color-brand-quiet)`. An inline `var()` fallback rather than a `:host` declaration, and the rule wraps its selector in `:where()` so a consumer's own `::part(previous):hover` still wins without `!important`. - `--lr-date-picker-nav-active-bg` — Pressed navigation background; defaults to the hover color mixed by `--lr-color-mix-active`. - `--lr-date-picker-preset-hover-bg`, `--lr-date-picker-preset-active-bg`, and `--lr-date-picker-preset-selected-bg` (new in 11.0.0) — hover, pressed, and currently-selected paint for a `presets` quick-range button. Defaults are `var(--lr-color-brand-quiet)`, that hover colour mixed by `--lr-color-mix-active`, and `var(--lr-color-brand)` respectively. - `--lr-date-picker-preset-selected-border` (default `var(--lr-color-brand)`) and `--lr-date-picker-preset-selected-color` (default `var(--lr-color-on-brand)`) independently theme a selected preset's border and foreground; the selected background token controls only its background. - `--lr-date-picker-title-hover-color`, `--lr-date-picker-title-active-color`, `--lr-date-picker-title-active-bg`, and `--lr-date-picker-title-active-radius` — Month-title hover/press paint and pressed shape; defaults to brand, brand, brand-quiet, and `var(--lr-date-picker-radius)` respectively. - `--lr-date-picker-day-hover-bg` and `--lr-date-picker-day-active-bg` — Day hover/press backgrounds; the pressed default mixes the hover hook by `--lr-color-mix-active`. - `--lr-date-picker-day-outside-color`, `--lr-date-picker-today-outline`, `--lr-date-picker-disabled-color`, and `--lr-date-picker-disabled-opacity` — adjacent-month, today, and disabled-day paint; defaults preserve the quiet-text, brand, and shared disabled tokens. - `--lr-date-picker-range-bg`, `--lr-date-picker-range-preview-bg`, and `--lr-date-picker-range-color` — range-interior, pending-preview, and adjacent-month range text paint. The preview defaults to the range background hook. - `--lr-date-picker-selected-bg` and `--lr-date-picker-selected-color` — selected day and range endpoint paint; defaults to brand/on-brand. - `--lr-date-picker-view-hover-bg`, `--lr-date-picker-view-active-bg`, `--lr-date-picker-view-selected-bg`, `--lr-date-picker-view-selected-color`, `--lr-date-picker-view-today-outline`, and `--lr-date-picker-view-disabled-opacity` — the corresponding month/year/decade selection-view states, independently themeable from day cells. - `--lr-date-input-placeholder-color` — Placeholder text color. Default: `var(--lr-color-text-quiet)`. - `--lr-date-input-gap` — Gap between input-row children. Default: `var(--lr-space-xs)`. - `--lr-date-input-radius` — Input-row corner radius. Default: `var(--lr-radius)`. - `--lr-date-input-focus-border-color` — Focused row border color. Default: `var(--lr-color-brand)`. - `--lr-date-input-fill` — Resting input-row background, public since 16.0.0 (the radius beside it always was). Default: `var(--lr-color-surface)`, or `var(--lr-color-surface-raised)` under the `filled`/`filled-outlined` treatments; a value set here wins over every treatment. - `--lr-date-input-border-color` — Resting input-row border color. Default: `var(--lr-color-border)`, or `transparent` under `filled`. - `--lr-form-control-focus-shadow` — The shared field halo, painted as a `box-shadow` while the row holds focus. Default: `none`. Additive — the focused brand border is never replaced by it. --- ## `lr-textarea` A multiline plain-text input primitive, form-associated (participates in native `` submission/validation/reset via `name`/`value`/`disabled`/`required`/`checkValidity()`/ `reportValidity()`). Ships an opt-in `label`/`hint`/`errorText` form-control chrome mirroring `lr-select` -- left unset, none of it renders. Removing `label`, `hint`, `help-text`, or `error-text` safely omits the corresponding content while preserving native `null` property readback. Explicit empty strings stay empty; later supplied text renders normally. Changing own `disabled` from true to false in the same task that disables an ancestor fieldset keeps the native editing control effectively disabled. The enabled first-legend exception and explicit own-disabled state retain their native meaning; validity and form submission follow the effective disabled state. ```html ``` ### Properties | Property | Attribute | Type | Default | Description | | ------------------------ | -------------------------- | -------------------------------------------------------------------- | ------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `value` | `value` | `string` | `''` | The current text value. | | `defaultValue` | `value` / `default-value` | `string` | `''` | Reset value. The Shoelace attribute alias and the native-style `value` content attribute share the same reset engine. | | `rows` | `rows` | `number` | `4` | Visible text rows (mapped default). | | `resize` | `resize` | `'none' \| 'vertical' \| 'horizontal' \| 'both' \| 'auto'` | `'vertical'` | Native CSS `resize` behavior, plus `'auto'` (`ResizeObserver`-driven grow-to-content, no manual handle). An invalid runtime value falls back to `'vertical'`; `'auto'` maps native CSS resize to `none`. | | `size` | `size` | `LyraSize` | `'m'` | Visual size on the shared control ladder — the same scale as `lr-input`/`lr-select`/`lr-button`, and both spellings of every tier are accepted (`2xs`/`xs`/`s`/`m`/`l`/`xl` and `small`/`medium`/`large`). Governs the field's padding, font size and corner radius. Reflected. | | `appearance` | `appearance` | `'accent' \| 'filled' \| 'outlined' \| 'filled-outlined' \| 'plain'` | `'outlined'` | Visual treatment of the field. The mapped default draws a border without a fill; the other values share `lr-input`'s vocabulary. Reflected. | | `filled` | `filled` | `boolean` | `false` | Shoelace alias for the filled treatment. | | `pill` | `pill` | `boolean` | `false` | Fully rounded field corners, matching `lr-input`'s/`lr-select`'s own `pill` — both upstreams ship it on their textarea, so a mechanical tag rename must not drop it. It changes the private radius default to `--lr-radius-pill`, so an inherited or direct `--lr-textarea-radius` stays authoritative. Most useful on a one- or two-row field: a tall multi-line surface with fully rounded ends wastes its first and last line's inline space, which is why it is opt-in rather than tied to `size`. Reflected. | | `withCount` | `with-count` | `boolean` | `false` | Renders a character count below the field, inside `[part="footer"]`. With `maxlength` set it counts _down_ the remaining characters instead of up from zero. Reflected. | | `placeholder` | `placeholder` | `string` | `''` | Placeholder text. | | `readonly` | `readonly` | `boolean` | `false` | Native read-only behavior: prevents user edits while preserving focus, selection/copy, form submission, and silent programmatic editing methods. Reflected. | | `label` | `label` | `string` | `''` | Visible label text. Unset: no label chrome renders. | | `hint` | `hint` | `string` | `''` | Hint text below the field. | | `helpText` | `help-text` | `string` | `''` | Shoelace alias for `hint`; `hint` wins when both are set. | | `withLabel` / `withHint` | `with-label` / `with-hint` | `boolean` | `false` | SSR slot-presence hints; neither is required for hydrated client-side slot detection. | | `errorText` | `error-text` | `string` | `''` | Error text below the field (overridden by slotted `error` content). | | `customError` | `custom-error` | `string \| null` | `null` | Reflected consumer-supplied validation message. A non-empty value blocks submission until `setCustomValidity('')` clears it. | | `accessibleLabel` | `aria-label` | `string \| null` | `null` | Accessible-name override forwarded to the internal ` ``` Integration is entirely the host's responsibility: detect a mention/command trigger in the host's own `input` handling, set `anchor`/`items`/`query` and flip `open = true`, and forward every `keydown` through `handleKeyDown()` while open. Anchor relationship and active-descendant syncing are automatic; if an explicit `syncActiveDescendant()` check returns `false`, call `focusActiveOption()` after the first consumed ArrowUp/ArrowDown so the fallback owns navigation from then on. Setting `open = false` whenever the query stops looking like an active mention context (a space typed, the trigger deleted, the input blurred, …) is also the host's job — `lr-mention-close` fires automatically from that. Positioning measures exactly where the caret currently paints via a hidden-mirror-element technique (`caretClientRect()`) and positions against that single point with `internal/positioner.js`'s `place()`, so the popup tracks the caret rather than sitting under the whole textarea. Re-measures automatically only on an `anchor` or `query` change while open (a keystroke moves the caret, so a fresh `query` is the proxy for "the caret may have moved"). **Known gotchas:** - a host-level `aria-label` attribute on `` now takes priority over `label` (and its localized default) when resolving `[part="listbox"]`'s accessible name — previously it was silently ignored. Matches the same fallback on ``/``. - Never copy `activeDescendantId` or `listboxId` onto a document-owned control as a string ARIA IDREF; shadow-root IDs are outside that control's tree scope. Use `syncActiveDescendant()` and its `focusActiveOption()` fallback. - The popover opens pre-highlighted on the top match (index 0), unlike ``'s own listbox which opens with nothing highlighted (`-1`) — a bare Enter right after opening commits immediately. - Caret-precise positioning only applies to a plain `