@aceshooting/lyra-ui 2.6.0 → 2.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (176) hide show
  1. package/README.md +13 -8
  2. package/custom-elements.json +3128 -31
  3. package/dist/components/app-rail/app-rail.class.d.ts +8 -1
  4. package/dist/components/app-rail/app-rail.class.d.ts.map +1 -1
  5. package/dist/components/app-rail/app-rail.class.js +12 -2
  6. package/dist/components/app-rail/app-rail.class.js.map +1 -1
  7. package/dist/components/chat-composer/chat-composer.class.d.ts +27 -0
  8. package/dist/components/chat-composer/chat-composer.class.d.ts.map +1 -1
  9. package/dist/components/chat-composer/chat-composer.class.js +64 -1
  10. package/dist/components/chat-composer/chat-composer.class.js.map +1 -1
  11. package/dist/components/checkbox/checkbox.class.d.ts +7 -0
  12. package/dist/components/checkbox/checkbox.class.d.ts.map +1 -1
  13. package/dist/components/checkbox/checkbox.class.js +7 -0
  14. package/dist/components/checkbox/checkbox.class.js.map +1 -1
  15. package/dist/components/chip/chip.class.d.ts +2 -0
  16. package/dist/components/chip/chip.class.d.ts.map +1 -1
  17. package/dist/components/chip/chip.class.js +3 -1
  18. package/dist/components/chip/chip.class.js.map +1 -1
  19. package/dist/components/chip/chip.styles.d.ts.map +1 -1
  20. package/dist/components/chip/chip.styles.js +4 -1
  21. package/dist/components/chip/chip.styles.js.map +1 -1
  22. package/dist/components/combobox/combobox.class.d.ts +16 -0
  23. package/dist/components/combobox/combobox.class.d.ts.map +1 -1
  24. package/dist/components/combobox/combobox.class.js +47 -2
  25. package/dist/components/combobox/combobox.class.js.map +1 -1
  26. package/dist/components/combobox/combobox.styles.d.ts.map +1 -1
  27. package/dist/components/combobox/combobox.styles.js +56 -6
  28. package/dist/components/combobox/combobox.styles.js.map +1 -1
  29. package/dist/components/conversation-item/conversation-item.class.d.ts +14 -0
  30. package/dist/components/conversation-item/conversation-item.class.d.ts.map +1 -1
  31. package/dist/components/conversation-item/conversation-item.class.js +38 -0
  32. package/dist/components/conversation-item/conversation-item.class.js.map +1 -1
  33. package/dist/components/date-picker/date-input.class.d.ts +25 -0
  34. package/dist/components/date-picker/date-input.class.d.ts.map +1 -1
  35. package/dist/components/date-picker/date-input.class.js +61 -0
  36. package/dist/components/date-picker/date-input.class.js.map +1 -1
  37. package/dist/components/dialog/dialog.class.d.ts +13 -1
  38. package/dist/components/dialog/dialog.class.d.ts.map +1 -1
  39. package/dist/components/dialog/dialog.class.js +24 -8
  40. package/dist/components/dialog/dialog.class.js.map +1 -1
  41. package/dist/components/empty/empty.class.d.ts +2 -0
  42. package/dist/components/empty/empty.class.d.ts.map +1 -1
  43. package/dist/components/empty/empty.class.js +2 -0
  44. package/dist/components/empty/empty.class.js.map +1 -1
  45. package/dist/components/empty/empty.styles.d.ts.map +1 -1
  46. package/dist/components/empty/empty.styles.js +5 -2
  47. package/dist/components/empty/empty.styles.js.map +1 -1
  48. package/dist/components/export-button/export-button.class.d.ts +19 -2
  49. package/dist/components/export-button/export-button.class.d.ts.map +1 -1
  50. package/dist/components/export-button/export-button.class.js +32 -8
  51. package/dist/components/export-button/export-button.class.js.map +1 -1
  52. package/dist/components/export-button/export-button.styles.d.ts.map +1 -1
  53. package/dist/components/export-button/export-button.styles.js +8 -1
  54. package/dist/components/export-button/export-button.styles.js.map +1 -1
  55. package/dist/components/gauge/gauge.class.d.ts +6 -2
  56. package/dist/components/gauge/gauge.class.d.ts.map +1 -1
  57. package/dist/components/gauge/gauge.class.js +40 -4
  58. package/dist/components/gauge/gauge.class.js.map +1 -1
  59. package/dist/components/gauge/gauge.styles.js +1 -1
  60. package/dist/components/graph/graph.class.d.ts +28 -1
  61. package/dist/components/graph/graph.class.d.ts.map +1 -1
  62. package/dist/components/graph/graph.class.js +94 -42
  63. package/dist/components/graph/graph.class.js.map +1 -1
  64. package/dist/components/graph/graph.styles.d.ts.map +1 -1
  65. package/dist/components/graph/graph.styles.js +4 -1
  66. package/dist/components/graph/graph.styles.js.map +1 -1
  67. package/dist/components/map/map.class.d.ts +4 -2
  68. package/dist/components/map/map.class.d.ts.map +1 -1
  69. package/dist/components/map/map.class.js +5 -3
  70. package/dist/components/map/map.class.js.map +1 -1
  71. package/dist/components/mention-popover/mention-popover.class.d.ts +9 -4
  72. package/dist/components/mention-popover/mention-popover.class.d.ts.map +1 -1
  73. package/dist/components/mention-popover/mention-popover.class.js +11 -5
  74. package/dist/components/mention-popover/mention-popover.class.js.map +1 -1
  75. package/dist/components/menu/menu.class.d.ts +10 -4
  76. package/dist/components/menu/menu.class.d.ts.map +1 -1
  77. package/dist/components/menu/menu.class.js +11 -5
  78. package/dist/components/menu/menu.class.js.map +1 -1
  79. package/dist/components/model-select/model-select.class.d.ts +44 -0
  80. package/dist/components/model-select/model-select.class.d.ts.map +1 -1
  81. package/dist/components/model-select/model-select.class.js +125 -0
  82. package/dist/components/model-select/model-select.class.js.map +1 -1
  83. package/dist/components/model-select/model-select.styles.d.ts.map +1 -1
  84. package/dist/components/model-select/model-select.styles.js +21 -0
  85. package/dist/components/model-select/model-select.styles.js.map +1 -1
  86. package/dist/components/pagination/pagination.class.d.ts +81 -0
  87. package/dist/components/pagination/pagination.class.d.ts.map +1 -0
  88. package/dist/components/pagination/pagination.class.js +278 -0
  89. package/dist/components/pagination/pagination.class.js.map +1 -0
  90. package/dist/components/pagination/pagination.d.ts +2 -0
  91. package/dist/components/pagination/pagination.d.ts.map +1 -0
  92. package/dist/components/pagination/pagination.js +5 -0
  93. package/dist/components/pagination/pagination.js.map +1 -0
  94. package/dist/components/pagination/pagination.styles.d.ts +2 -0
  95. package/dist/components/pagination/pagination.styles.d.ts.map +1 -0
  96. package/dist/components/pagination/pagination.styles.js +142 -0
  97. package/dist/components/pagination/pagination.styles.js.map +1 -0
  98. package/dist/components/phone-input/phone-input.class.d.ts +191 -0
  99. package/dist/components/phone-input/phone-input.class.d.ts.map +1 -0
  100. package/dist/components/phone-input/phone-input.class.js +493 -0
  101. package/dist/components/phone-input/phone-input.class.js.map +1 -0
  102. package/dist/components/phone-input/phone-input.d.ts +2 -0
  103. package/dist/components/phone-input/phone-input.d.ts.map +1 -0
  104. package/dist/components/phone-input/phone-input.js +5 -0
  105. package/dist/components/phone-input/phone-input.js.map +1 -0
  106. package/dist/components/phone-input/phone-input.styles.d.ts +2 -0
  107. package/dist/components/phone-input/phone-input.styles.d.ts.map +1 -0
  108. package/dist/components/phone-input/phone-input.styles.js +123 -0
  109. package/dist/components/phone-input/phone-input.styles.js.map +1 -0
  110. package/dist/components/segmented/segmented.class.d.ts +1 -0
  111. package/dist/components/segmented/segmented.class.d.ts.map +1 -1
  112. package/dist/components/segmented/segmented.class.js +1 -0
  113. package/dist/components/segmented/segmented.class.js.map +1 -1
  114. package/dist/components/slider/slider.class.d.ts +3 -0
  115. package/dist/components/slider/slider.class.d.ts.map +1 -1
  116. package/dist/components/slider/slider.class.js +3 -0
  117. package/dist/components/slider/slider.class.js.map +1 -1
  118. package/dist/components/switch/switch.class.d.ts +20 -0
  119. package/dist/components/switch/switch.class.d.ts.map +1 -1
  120. package/dist/components/switch/switch.class.js +83 -23
  121. package/dist/components/switch/switch.class.js.map +1 -1
  122. package/dist/components/switch/switch.styles.d.ts.map +1 -1
  123. package/dist/components/switch/switch.styles.js +18 -0
  124. package/dist/components/switch/switch.styles.js.map +1 -1
  125. package/dist/components/table/table.class.d.ts +22 -6
  126. package/dist/components/table/table.class.d.ts.map +1 -1
  127. package/dist/components/table/table.class.js +20 -6
  128. package/dist/components/table/table.class.js.map +1 -1
  129. package/dist/components/textarea/textarea.class.d.ts +86 -7
  130. package/dist/components/textarea/textarea.class.d.ts.map +1 -1
  131. package/dist/components/textarea/textarea.class.js +305 -21
  132. package/dist/components/textarea/textarea.class.js.map +1 -1
  133. package/dist/components/textarea/textarea.styles.d.ts.map +1 -1
  134. package/dist/components/textarea/textarea.styles.js +36 -0
  135. package/dist/components/textarea/textarea.styles.js.map +1 -1
  136. package/dist/components/time-range/time-range.class.d.ts +4 -0
  137. package/dist/components/time-range/time-range.class.d.ts.map +1 -1
  138. package/dist/components/time-range/time-range.class.js +4 -0
  139. package/dist/components/time-range/time-range.class.js.map +1 -1
  140. package/dist/components/tool-approval-dialog/tool-approval-dialog.class.d.ts +6 -0
  141. package/dist/components/tool-approval-dialog/tool-approval-dialog.class.d.ts.map +1 -1
  142. package/dist/components/tool-approval-dialog/tool-approval-dialog.class.js +8 -0
  143. package/dist/components/tool-approval-dialog/tool-approval-dialog.class.js.map +1 -1
  144. package/dist/components/tree/tree-node.class.d.ts +3 -0
  145. package/dist/components/tree/tree-node.class.d.ts.map +1 -1
  146. package/dist/components/tree/tree-node.class.js +16 -1
  147. package/dist/components/tree/tree-node.class.js.map +1 -1
  148. package/dist/components/tree/tree-node.styles.d.ts.map +1 -1
  149. package/dist/components/tree/tree-node.styles.js +27 -1
  150. package/dist/components/tree/tree-node.styles.js.map +1 -1
  151. package/dist/components/tree/tree.class.d.ts +11 -1
  152. package/dist/components/tree/tree.class.d.ts.map +1 -1
  153. package/dist/components/tree/tree.class.js +6 -2
  154. package/dist/components/tree/tree.class.js.map +1 -1
  155. package/dist/components/typing-indicator/typing-indicator.class.d.ts +4 -0
  156. package/dist/components/typing-indicator/typing-indicator.class.d.ts.map +1 -1
  157. package/dist/components/typing-indicator/typing-indicator.class.js +4 -0
  158. package/dist/components/typing-indicator/typing-indicator.class.js.map +1 -1
  159. package/dist/components/typing-indicator/typing-indicator.styles.d.ts.map +1 -1
  160. package/dist/components/typing-indicator/typing-indicator.styles.js +9 -7
  161. package/dist/components/typing-indicator/typing-indicator.styles.js.map +1 -1
  162. package/dist/internal/localization.d.ts +1 -1
  163. package/dist/internal/localization.d.ts.map +1 -1
  164. package/dist/internal/localization.js +5 -0
  165. package/dist/internal/localization.js.map +1 -1
  166. package/dist/internal/root-registration-allowlist.d.ts +1 -1
  167. package/dist/internal/root-registration-allowlist.d.ts.map +1 -1
  168. package/dist/internal/root-registration-allowlist.js +2 -0
  169. package/dist/internal/root-registration-allowlist.js.map +1 -1
  170. package/dist/lyra.d.ts +11 -3
  171. package/dist/lyra.d.ts.map +1 -1
  172. package/dist/lyra.js +4 -0
  173. package/dist/lyra.js.map +1 -1
  174. package/llms-full.txt +573 -94
  175. package/llms.txt +5 -3
  176. package/package.json +9 -1
package/llms-full.txt CHANGED
@@ -22,8 +22,8 @@ composition without registration; for example, `components/empty/empty.class.js`
22
22
  ## Contents
23
23
 
24
24
  - **Shared foundation** — `internal/` (base class, form-association mixin, positioning helper, design tokens)
25
- - **Form controls, toasts, sparkline, and flags** — `lyra-combobox`/`lyra-option`, `lyra-select`, `lyra-date-picker`/`lyra-date-input`, `lyra-toast` family, `lyra-sparkline`, `lyra-flag`
26
- - **Dashboard atoms** — `lyra-empty`, `lyra-skeleton`, `lyra-stat`, `lyra-table`, `lyra-gauge`, `lyra-export-button`, `lyra-copy-button`, `lyra-split`, `lyra-widget`, `lyra-word-cloud`
25
+ - **Form controls, toasts, sparkline, and flags** — `lyra-combobox`/`lyra-option`, `lyra-select`, `lyra-date-picker`/`lyra-date-input`, `lyra-phone-input`, `lyra-toast` family, `lyra-sparkline`, `lyra-flag`
26
+ - **Dashboard atoms** — `lyra-empty`, `lyra-skeleton`, `lyra-stat`, `lyra-table`, `lyra-pagination`, `lyra-gauge`, `lyra-export-button`, `lyra-copy-button`, `lyra-split`, `lyra-widget`, `lyra-word-cloud`
27
27
  - **Temporal & graph** — `lyra-time-range`, `lyra-playback`, `lyra-heatmap`, `lyra-graph`, `lyra-tree`/`lyra-tree-node`
28
28
  - **Chart family** — `lyra-chart` (core), typed subclasses, `lyra-histogram`, `lyra-box-plot`,
29
29
  `lyra-lite-chart` (dependency-free bar/line alternative, no peer deps)
@@ -193,6 +193,8 @@ shared `FormAssociated` mixin — see gotchas).
193
193
 
194
194
  **Properties:**
195
195
  - `multiple: boolean = false` (reflected)
196
+ - `size: 'xs' | 's' | 'm' | 'l' | 'xl' = 'm'` (reflected — same scale as `lyra-select`'s `size`;
197
+ also scales the "+N" overflow tag's padding/font-size to match)
196
198
  - `placeholder: string = ''`
197
199
  - `disabled: boolean = false` (reflected)
198
200
  - `required: boolean = false` (reflected — enforced via `internals.setValidity()`; also reflected as
@@ -224,10 +226,17 @@ shared `FormAssociated` mixin — see gotchas).
224
226
  `overflowText` row instead of being dropped silently)
225
227
  - `value: string | string[]` — a getter/setter: plain `string` in single mode, `string[]` in
226
228
  `multiple` mode
227
-
228
- `ComboboxSourceRow = { value: string; label: string; sub?: string; dotColor?: string; group?:
229
- string; disabled?: boolean }` — the row shape both the async `source` and the light-DOM
230
- `<lyra-option>` path normalize to internally.
229
+ - `selectedRows: ComboboxSourceRow[]` (read-only getter) — structured rows for the current
230
+ selection, including any opaque `data` payload supplied by an async source. Selected async rows
231
+ remain available after the query changes or a later source result no longer contains them
232
+
233
+ `ComboboxSourceRow = { value: string; label: string; sub?: string; icon?: unknown; badge?: string |
234
+ number; accessibleLabel?: string; data?: unknown; dotColor?: string; group?: string; disabled?:
235
+ boolean }` — the row shape used by the async `source` path. `icon` renders as a decorative leading
236
+ visual, `badge` as trailing metadata, `accessibleLabel` can provide richer spoken text than the
237
+ visible label, and `data` is retained without being rendered for retrieval through `selectedRows`.
238
+ The light-DOM `<lyra-option>` path normalizes its supported label/sub/dot/group fields to the same
239
+ internal row model.
231
240
 
232
241
  **Events:** typing in the filter exposes the original bubbling/composed, non-cancelable `InputEvent`
233
242
  as exactly one host `input` event and does not fire `change`. An actual user selection mutation —
@@ -242,9 +251,10 @@ attribute when provided)
242
251
 
243
252
  **CSS parts:** `form-control`, `form-control-label`, `combobox`, `tags`, `tag`,
244
253
  `tag__remove-button`, `combobox-input`, `clear-button`, `expand-icon`, `listbox`, `option`,
245
- `option-dot` (the leading status dot, when a row's `dotColor` is set), `option-label`, `option-sub`
246
- (a row's secondary line, when `sub` is set), `option-overflow` (the "+N more" indicator from
247
- `maxRender`), `error`, `hint`
254
+ `option-dot` (the leading status dot, when a row's `dotColor` is set), `option-icon` (the decorative
255
+ leading visual for an async row), `option-label`, `option-sub` (a row's secondary line, when `sub`
256
+ is set), `option-badge` (an async row's trailing metadata), `option-overflow` (the "+N more"
257
+ indicator from `maxRender`), `error`, `hint`
248
258
 
249
259
  **Themeable custom properties:** shared tokens only.
250
260
 
@@ -281,7 +291,14 @@ attribute when provided)
281
291
  <script type="module">
282
292
  document.getElementById('cb2').source = async (query) => {
283
293
  const rows = await fetchFruit(query); // your own lookup
284
- return rows.map((r) => ({ value: r.id, label: r.name }));
294
+ return rows.map((r) => ({
295
+ value: r.id,
296
+ label: r.name,
297
+ icon: renderFruitIcon(r), // decorative; hidden from assistive technology
298
+ badge: r.category,
299
+ accessibleLabel: `${r.name}, ${r.category}`,
300
+ data: r, // retained in cb2.selectedRows after selection
301
+ }));
285
302
  };
286
303
  </script>
287
304
  ```
@@ -304,7 +321,6 @@ synchronous and fires no `input`/`change` event.
304
321
  - a host-level `aria-label` attribute on `<lyra-combobox>` now takes priority over `label`/
305
322
  `placeholder`/`"Combobox"` when resolving the accessible name on `[part="combobox-input"]` —
306
323
  previously it was silently ignored. Matches the same fallback on `<lyra-select>`.
307
- - no `size` variant (small/medium/large) unlike sibling `lyra-toast-item`.
308
324
  - `aria-required` reflects `required` immediately; `aria-invalid`, by contrast, only reflects
309
325
  **after the field has been touched** (first `blur`) — a `required` field with a validity error
310
326
  doesn't look invalid to assistive tech before that, by design (avoids flashing invalid styling on
@@ -313,6 +329,10 @@ synchronous and fires no `input`/`change` event.
313
329
  - `dotColor`/`sub`/`group` are read from light-DOM `<lyra-option>` children as before, but are also
314
330
  first-class fields on `ComboboxSourceRow` for the async `source` path — an async lookup can drive
315
331
  the same grouped/dot/sub-text rendering a static option list can.
332
+ - `icon`, `badge`, `accessibleLabel`, and `data` are async-source row features rather than
333
+ `<lyra-option>` properties. Icons are decorative (`aria-hidden`); use `accessibleLabel` when the
334
+ visible label/sub/badge combination needs a fuller spoken name. `data` is deliberately opaque and
335
+ is available only through the read-only `selectedRows` getter.
316
336
  - Full ARIA 1.2 combobox pattern (`role=combobox`, roving `aria-activedescendant`, real DOM focus
317
337
  kept on the input) is implemented correctly — a genuine strength, safe to build on. Declaratively-
318
338
  selected options (`<lyra-option value="b" selected>`) are seeded into the selection (mirroring
@@ -507,10 +527,18 @@ Text field + calendar popover, **form-associated** via the shared `FormAssociate
507
527
  calendar-toggle button)
508
528
  - `dialogLabel: string = 'Choose date'` (attribute `dialog-label` — accessible name for the
509
529
  `role="dialog"` calendar popover)
530
+ - `spellcheck: boolean = true` — forwarded to the internal `<input>`
531
+ - `autocapitalize: string = ''` — forwarded to the internal `<input>`; empty omits the attribute
532
+ - `autoCorrect: string = ''` (attribute `autocorrect`, Safari/WebKit-specific) — forwarded to the
533
+ internal `<input>`; empty omits the attribute. Named `autoCorrect` in JS/TS (not `autocorrect`)
534
+ to dodge a `lib.dom.d.ts` collision with `HTMLElement`'s own boolean `autocorrect` IDL member;
535
+ the wire attribute is still the plain `autocorrect` name
510
536
 
511
537
  **Methods:** `show()`, `hide()`, `clear()`
512
538
 
513
- **Events:** `input`, `change`, `lyra-show`, `lyra-hide`, `lyra-clear`
539
+ **Events:** `input`, `change`, `lyra-show`, `lyra-hide`, `lyra-clear`, `blur` (re-dispatched from
540
+ the internal `<input>`'s own `blur`, bubbling and composed unlike the native event), `focus`
541
+ (re-dispatched from the internal `<input>`'s own `focus`, for the same reason as `blur`)
514
542
 
515
543
  **Slots:** `label`, `error` (overrides `errorText`), `hint`
516
544
 
@@ -581,10 +609,10 @@ and `dateTimeFormat(locale, options)`.
581
609
 
582
610
  ## lyra-textarea
583
611
 
584
- A bare, multiline plain-text input primitive -- the thin `lyra-*` equivalent of `wa-textarea`.
585
- Form-associated (participates in native `<form>` submission/validation/reset via
586
- `name`/`value`/`disabled`/`required`/`checkValidity()`/`reportValidity()`). No auto-resize and no
587
- label/hint/error chrome -- wrap it in your own form-field layout for those.
612
+ A multiline plain-text input primitive, form-associated (participates in native `<form>`
613
+ submission/validation/reset via `name`/`value`/`disabled`/`required`/`checkValidity()`/
614
+ `reportValidity()`). Ships an opt-in `label`/`hint`/`errorText` form-control chrome mirroring
615
+ `lyra-select` -- left unset, none of it renders.
588
616
 
589
617
  ```html
590
618
  <lyra-textarea placeholder="Notes" rows="4"></lyra-textarea>
@@ -596,24 +624,227 @@ label/hint/error chrome -- wrap it in your own form-field layout for those.
596
624
  | --- | --- | --- | --- | --- |
597
625
  | `value` | `value` | `string` | `''` | The current text value. |
598
626
  | `rows` | `rows` | `number` | `3` | Visible text rows. |
599
- | `resize` | `resize` | `'none' \| 'vertical' \| 'both'` | `'vertical'` | Native CSS `resize` behavior. |
627
+ | `resize` | `resize` | `'none' \| 'vertical' \| 'both' \| 'auto'` | `'vertical'` | Native CSS `resize` behavior, plus `'auto'` (`ResizeObserver`-driven grow-to-content, no manual handle). |
600
628
  | `placeholder` | `placeholder` | `string` | `''` | Placeholder text. |
629
+ | `label` | `label` | `string` | `''` | Visible label text. Unset: no label chrome renders. |
630
+ | `hint` | `hint` | `string` | `''` | Hint text below the field. |
631
+ | `errorText` | `error-text` | `string` | `''` | Error text below the field (overridden by slotted `error` content). |
632
+ | `accessibleLabel` | `aria-label` | `string \| null` | `null` | Accessible-name override forwarded to the internal `<textarea>`; wins over `label`, `placeholder`, and the localized default. |
633
+ | `spellcheck` | `spellcheck` | `boolean` | `true` | Forwarded to the native `<textarea>`. |
634
+ | `autocapitalize` | `autocapitalize` | `string` | `''` | Forwarded to the native `<textarea>`; empty omits the attribute. |
635
+ | `autoCorrect` | `autocorrect` | `string` | `''` | Forwarded to the native `<textarea>` (Safari/WebKit-specific); empty omits the attribute. |
636
+ | `wrap` | `wrap` | `'hard' \| 'soft' \| 'off'` | `'soft'` | Native line-wrapping/submission behavior. |
637
+ | `autocomplete` | `autocomplete` | `string` | `''` | Forwarded to the native `<textarea>`; empty omits the attribute. |
638
+ | `inputMode` | `inputmode` | `string` | `''` | Virtual-keyboard input hint forwarded to the native `<textarea>`. |
639
+ | `enterKeyHint` | `enterkeyhint` | `string` | `''` | Virtual-keyboard Enter-key hint forwarded to the native `<textarea>`. |
601
640
  | `name` | `name` | `string` | `''` | Form field name. |
602
641
  | `disabled` | `disabled` | `boolean` | `false` | Disables the control. |
603
642
  | `required` | `required` | `boolean` | `false` | Participates in native constraint validation. |
604
643
 
644
+ The visible label, hint, and error live in the same shadow tree as the native control, so their
645
+ generated ids safely drive the native `<label>`/`aria-describedby` relationships. Name precedence
646
+ is `accessibleLabel`/host `aria-label`, visible `label`, `placeholder`, then the localized
647
+ `textareaLabel` message. External `aria-labelledby`/`aria-describedby` idrefs are not copied across
648
+ the shadow boundary.
649
+
650
+ ### Getters and selection properties
651
+
652
+ - `input: HTMLTextAreaElement | null` — the internal native control.
653
+ - `selectionStart: number | null`, `selectionEnd: number | null`, and
654
+ `selectionDirection: 'forward'|'backward'|'none'|null` — readable and writable passthroughs to
655
+ the native selection state.
656
+ - The shared form-associated getters `form`, `labels`, `validity`, `validationMessage`,
657
+ `willValidate`, and `effectiveDisabled` are also available.
658
+
659
+ ### Methods
660
+
661
+ | Method | Description |
662
+ | --- | --- |
663
+ | `focus(options?)` / `blur()` | Focus or blur the internal native control. |
664
+ | `select()` | Select all text. |
665
+ | `setSelectionRange(start, end, direction?)` | Set the native selection range and optional direction. |
666
+ | `setRangeText(replacement, start?, end?, selectMode?)` | Apply a native range edit, then synchronize the component `value`, form value, validity, and auto-grown size without emitting a user event. |
667
+ | `setFormValue(value)` | Set the reactive and submitted value synchronously. |
668
+ | `checkValidity()` / `reportValidity()` | Run native constraint validation through `ElementInternals`. |
669
+
605
670
  ### Events
606
671
 
607
672
  | Event | Detail | Description |
608
673
  | --- | --- | --- |
609
674
  | `lyra-input` | `{ value: string }` | Fired on every user-driven edit. |
610
675
  | `lyra-change` | `{ value: string }` | Fired on native `change` timing (blur after a committed edit). |
676
+ | `blur` | none | Re-dispatched from the internal native `<textarea>`'s own `blur` -- bubbling and composed, unlike the native event. |
677
+ | `focus` | none | Re-dispatched from the internal native `<textarea>`'s own `focus`, for the same reason as `blur`. |
678
+
679
+ Programmatic property assignments, selection changes, `setRangeText()`, form reset, and form-state
680
+ restoration are silent. User edits update `value`, submitted form data, and required validity before
681
+ the corresponding `lyra-input`/`lyra-change` event is dispatched. `form.reset()` restores the
682
+ original declarative `value`, matching native `defaultValue` behavior.
683
+
684
+ ### Slots
685
+
686
+ | Slot | Description |
687
+ | --- | --- |
688
+ | `label` | Custom label content. |
689
+ | `hint` | Custom hint content. |
690
+ | `error` | Custom error content. |
611
691
 
612
692
  ### CSS Parts
613
693
 
614
694
  | Part | Description |
615
695
  | --- | --- |
696
+ | `form-control` | The outer wrapper around label, textarea, error and hint. |
697
+ | `form-control-label` | The `<label>` element. |
616
698
  | `textarea` | The native `<textarea>` element. |
699
+ | `hint` | The hint message. |
700
+ | `error` | The error message. |
701
+
702
+ ### Themeable custom properties
703
+
704
+ - `--lyra-textarea-max-block-size` (default `none`) — bounds `resize="auto"`; content beyond the
705
+ bound scrolls inside the native textarea. Auto-resize remeasures after user edits, programmatic
706
+ `value`/`rows` changes, range edits, and container-width changes.
707
+
708
+ ---
709
+
710
+ ## `lyra-phone-input`
711
+
712
+ A form-associated, country-aware telephone field. The submitted `value` is either canonical E.164
713
+ (for example `+352621123456`) or `''` while the editable input is empty, incomplete, or invalid.
714
+ Numbering-plan metadata and national formatting stay outside Lyra's base bundle: supply a
715
+ synchronous `PhoneNumberAdapter`, or lazily create one from a `libphonenumber-js`-compatible module
716
+ with `loadLibphonenumberAdapter()`. Without an adapter, already-international E.164 input still
717
+ normalizes and validates; national input remains editable with `incomplete` validity.
718
+
719
+ **Types:**
720
+
721
+ ```ts
722
+ type PhoneNumberStatus = 'empty' | 'incomplete' | 'invalid' | 'valid';
723
+
724
+ interface PhoneCountry {
725
+ code: string; // ISO 3166-1 alpha-2
726
+ callingCode: string; // no leading "+"
727
+ label?: string; // overrides Intl.DisplayNames
728
+ }
729
+
730
+ interface PhoneNumberParseResult {
731
+ status: PhoneNumberStatus;
732
+ e164?: string; // required for status: 'valid'
733
+ formatted?: string; // best-effort editable display text
734
+ country?: string; // detected ISO alpha-2 code
735
+ }
736
+
737
+ interface PhoneNumberAdapter {
738
+ readonly countries?: readonly PhoneCountry[];
739
+ parse(input: string, country?: string): PhoneNumberParseResult;
740
+ }
741
+ ```
742
+
743
+ **Properties:**
744
+
745
+ - `value: string = ''` — canonical E.164 form/submission value. A programmatic assignment is parsed
746
+ and normalized synchronously but emits no user event.
747
+ - `name: string = ''`, `disabled: boolean = false`, `required: boolean = false` — native-like
748
+ form-control properties supplied by `FormAssociated`; inherited disabled fieldsets are included
749
+ through `effectiveDisabled`.
750
+ - `adapter?: PhoneNumberAdapter` (attribute: false) — synchronous numbering-plan parser/formatter.
751
+ No metadata implementation is imported by the component itself.
752
+ - `countries: readonly PhoneCountry[] = []` (attribute: false) — explicit selector rows; takes
753
+ precedence over `adapter.countries`.
754
+ - `defaultCountry: string = ''` (attribute `default-country`) — selected when `country` has not been
755
+ set explicitly.
756
+ - `country: string` — current uppercase ISO alpha-2 selection; falls back to `defaultCountry`, then
757
+ the first explicit/adapter country. Changing the country reparses the editable number.
758
+ - `label: string = ''`, `hint: string = ''`, `errorText: string = ''` (attribute `error-text`) —
759
+ visible form-field chrome; each has a matching named slot.
760
+ - `placeholder: string = ''` — forwarded to the native telephone input.
761
+ - host `aria-label` — forwarded to the internal telephone input. Name precedence is `phoneLabel`,
762
+ host `aria-label`, visible `label`, then `placeholder`.
763
+ - `phoneLabel: string = ''` (attribute `phone-label`) — explicit accessible-name override for the
764
+ native telephone input.
765
+ - `countryLabel: string = 'Select'` (attribute `country-label`) — country-selector accessible name;
766
+ the untouched default routes through the shared localized `select` message.
767
+ - `incompleteText: string = 'The value is invalid.'` (attribute `incomplete-text`) — validation
768
+ message for dial-like input that can still become valid with more digits. The untouched default
769
+ routes through the shared localized `valueInvalid` message.
770
+ - `invalidText: string = 'The value is invalid.'` (attribute `invalid-text`) — completed-invalid
771
+ message, localized through the same shared key while left at its default.
772
+ - `autocomplete: string = 'tel'`, `inputmode: 'tel'|'numeric'|'text' = 'tel'`,
773
+ `enterkeyhint: string = ''` — forwarded to the internal `<input type="tel">`.
774
+ - readonly `input: HTMLInputElement | undefined` — the internal native telephone input.
775
+ - readonly `inputValue: string` — editable formatted/partial text, which remains available even when
776
+ canonical `value` is `''`.
777
+ - readonly `phoneStatus: PhoneNumberStatus` — current parse state. The host also reflects it through
778
+ `data-phone-status`.
779
+ - readonly `form`, `labels`, `validity`, `validationMessage`, `willValidate`, and
780
+ `effectiveDisabled` — the shared form-associated native-like getters.
781
+
782
+ **Validity:** empty + `required` sets `valueMissing`; incomplete dial-like input sets `badInput`;
783
+ completed-invalid input sets `typeMismatch`; valid E.164 input clears all three. Partial or invalid
784
+ text remains in `inputValue`/the native input so validation never makes a number impossible to edit,
785
+ but its canonical submitted `value` is blank. Native validation feedback is anchored to the
786
+ telephone input, not the adjacent country selector.
787
+
788
+ **Methods:** `focus(options?)`, `blur()`, and `select()` forward to the native telephone input.
789
+ `setFormValue(value)`, `checkValidity()`, and `reportValidity()` come from `FormAssociated`.
790
+ `form.reset()` restores the original declarative `value` and the default country.
791
+
792
+ **Events:**
793
+
794
+ - `input` — every user edit and country change.
795
+ - `change` — native telephone-input commit timing and every country change.
796
+ - `focus` / `blur` — bubbling, composed bridges for the internal native input's non-crossing focus
797
+ events.
798
+
799
+ `input`/`change` detail is
800
+ `{ value: string; inputValue: string; country: string; valid: boolean; status: PhoneNumberStatus }`.
801
+ Programmatic property assignments and form reset/state restoration are silent.
802
+
803
+ **Slots:** `label`, `hint`, `error`, `country-prefix` (optional visual before the country selector,
804
+ such as a consumer-owned `<lyra-flag>`; no flag package is imported automatically).
805
+
806
+ **CSS parts:** `form-control`, `form-control-label`, `input-wrapper`, `country-prefix`,
807
+ `country-select`, `calling-code`, `input`, `hint`, `error`.
808
+
809
+ **Themeable custom properties:** shared tokens only. The phone-number input and calling code are
810
+ deliberately `dir="ltr"`/isolated because telephone numbers are algorithmic content; surrounding
811
+ form chrome and the country selector inherit LTR/RTL and use logical spacing/borders.
812
+
813
+ **Optional peer deps:** `libphonenumber-js` is declared optional but never imported by Lyra itself.
814
+ For full national parsing/formatting, install it in the consuming app and pass it through the
815
+ consumer-supplied lazy loader below. Because the import expression lives in consumer code, no
816
+ numbering metadata enters a bundle that does not opt in.
817
+
818
+ ```ts
819
+ import '@aceshooting/lyra-ui/components/phone-input/phone-input.js';
820
+ import { loadLibphonenumberAdapter } from
821
+ '@aceshooting/lyra-ui/components/phone-input/phone-input.class.js';
822
+
823
+ const phone = document.querySelector('lyra-phone-input');
824
+ phone.adapter = await loadLibphonenumberAdapter(() => import('libphonenumber-js/min'));
825
+ ```
826
+
827
+ ```html
828
+ <lyra-phone-input
829
+ name="mobile"
830
+ label="Mobile number"
831
+ hint="Used only for account security"
832
+ default-country="LU"
833
+ required
834
+ ></lyra-phone-input>
835
+ ```
836
+
837
+ **Known gotchas:**
838
+
839
+ - An adapter's `parse()` method is synchronous because it runs on every keystroke. Load any optional
840
+ module first, then assign the resolved adapter. Adapter exceptions degrade to the E.164-only
841
+ fallback rather than breaking editing.
842
+ - A valid adapter result must include an E.164-shaped `e164`; a malformed "valid" result is treated
843
+ as invalid instead of entering form submission.
844
+ - Country names use `Intl.DisplayNames` and fall back to the ISO code; set `PhoneCountry.label` for
845
+ a product-specific name. Calling codes are data, not derived by the component.
846
+ - The component does not import `@aceshooting/lyra-flags`. Compose an optional flag through
847
+ `country-prefix` and keep it decorative when the adjacent select already announces the country.
617
848
 
618
849
  ---
619
850
 
@@ -904,8 +1135,9 @@ attribute), `description` (rich description content, overrides the `description`
904
1135
 
905
1136
  **CSS parts:** `base`, `icon`, `heading`, `description`, `actions`
906
1137
 
907
- **Themeable custom properties:** shared tokens only (`--lyra-space-xs/-s/-l`,
908
- `--lyra-color-text-quiet/-border/-text`).
1138
+ **Themeable custom properties:** `--lyra-empty-compact-align` (compact mode only; defaults preserve
1139
+ the existing `flex-start` cross-axis and `start` text alignment, and `center` centers both), plus
1140
+ shared tokens (`--lyra-space-xs/-s/-l`, `--lyra-color-text-quiet/-border/-text`).
909
1141
 
910
1142
  **Optional peer deps:** none.
911
1143
 
@@ -914,6 +1146,8 @@ attribute), `description` (rich description content, overrides the `description`
914
1146
  <svg slot="" ...></svg> <!-- default slot: any icon/illustration -->
915
1147
  <div slot="actions"><button>Clear filters</button></div>
916
1148
  </lyra-empty>
1149
+ <lyra-empty compact heading="No results" description="Try a different search."
1150
+ style="--lyra-empty-compact-align: center"></lyra-empty>
917
1151
  ```
918
1152
 
919
1153
  **Known gotchas:**
@@ -1146,18 +1380,94 @@ body's `max-block-size`).
1146
1380
 
1147
1381
  ---
1148
1382
 
1383
+ ## `lyra-pagination`
1384
+
1385
+ Controlled page navigation for server-side or client-side data sets: previous/next buttons, a
1386
+ validated numeric page jump, a localized item-range summary, and a polite announcement after the
1387
+ host applies a requested page. The component owns no data fetching and never mutates `page`.
1388
+
1389
+ **Properties and getters:**
1390
+ - `page: number = 1` (reflected) — the currently applied page. Runtime values are presented within
1391
+ the valid `1..pageCount` range, but the public property itself remains controlled and is not
1392
+ rewritten by the component
1393
+ - `pageSize: number = 20` (attribute `page-size`) — items per page; finite values are truncated to
1394
+ a non-negative integer for the derived calculations, and zero produces no pages
1395
+ - `totalItems: number = 0` (attribute `total-items`) — total item count; finite values are truncated
1396
+ to a non-negative integer for display and page-count calculations
1397
+ - `pageCount: number` (readonly getter) — `ceil(totalItems / pageSize)` after the normalization
1398
+ above, or `0` when either normalized input is zero
1399
+ - `disabled: boolean = false` (reflected)
1400
+ - `loading: boolean = false` (reflected) — disables all controls and sets `aria-busy="true"` on the
1401
+ internal navigation landmark
1402
+ - `hideSummary: boolean = false` (attribute `hide-summary`, reflected) — omits the built-in range
1403
+ summary while retaining the controls
1404
+ - `size: 'xs'|'s'|'m'|'l'|'xl' = 'm'` (reflected) — changes control height and type size
1405
+ - `itemLabel: string = ''` (attribute `item-label`) — custom item noun used in the summary; empty
1406
+ selects the localized singular `item` or plural `items` key
1407
+ - `accessibleLabel: string | null = null` (attribute `aria-label`) — host accessible-name override
1408
+ forwarded to the internal `<nav>` landmark; takes precedence over `label`
1409
+ - `label: string = 'Pagination'` — fallback accessible name for the internal `<nav>` landmark
1410
+ - `pageLabel: string = 'Page'` (attribute `page-label`) — accessible name for the page-jump input
1411
+ - `previousLabel: string = 'Previous'` (attribute `previous-label`), `nextLabel: string = 'Next'`
1412
+ (attribute `next-label`) — accessible names for the icon-only directional buttons
1413
+
1414
+ Built-in property defaults resolve through the locale registry. A property value customized away
1415
+ from its built-in default is treated as an explicit per-instance wording override.
1416
+
1417
+ **Events:** `lyra-page-change` (`detail: { page: number }`, bubbles and composes, non-cancelable) —
1418
+ emitted for a valid, different requested page. The host applies `event.detail.page` back to `page`
1419
+ after routing, fetching, or any other policy decision.
1420
+
1421
+ **Slots:** none.
1422
+
1423
+ **CSS parts:** `base`, `summary`, `controls`, `previous-button`, `previous-icon`, `page-field`,
1424
+ `page-input`, `page-count`, `next-button`, `next-icon`, `live-region`.
1425
+
1426
+ **Themeable custom properties:** `--lyra-pagination-control-size` and
1427
+ `--lyra-pagination-font-size` (both default from `size`), plus shared color, spacing, border, radius,
1428
+ disabled-opacity, and focus-ring tokens.
1429
+
1430
+ **Optional peer deps:** none.
1431
+
1432
+ ```html
1433
+ <lyra-pagination total-items="237" page-size="20"></lyra-pagination>
1434
+ <script>
1435
+ const pagination = document.querySelector('lyra-pagination');
1436
+ pagination.addEventListener('lyra-page-change', async (event) => {
1437
+ await loadPage(event.detail.page);
1438
+ pagination.page = event.detail.page;
1439
+ });
1440
+ </script>
1441
+ ```
1442
+
1443
+ **Known gotchas:**
1444
+ - user activation only emits an intent. Until the host applies a new `page`, the numeric input
1445
+ returns to the currently controlled value; assigning the page triggers the localized
1446
+ `role="status"` announcement
1447
+ - the jump input accepts only whole pages in `1..pageCount`; empty, fractional, and out-of-range
1448
+ drafts expose `aria-invalid="true"` and emit nothing
1449
+ - zero items, zero page size, `disabled`, and `loading` all disable the navigation controls. The
1450
+ empty summary is still rendered via the localized `paginationEmptySummary` message
1451
+ (`'{total} {itemLabel}'`, producing `0 items` in the default locale) unless `hide-summary` is set
1452
+ - below a 20rem container allocation the summary and controls stack; the breakpoint responds to
1453
+ the component's own inline size, not the viewport. Previous/next icons also mirror under RTL
1454
+
1455
+ ---
1456
+
1149
1457
  ## `lyra-gauge`
1150
1458
 
1151
- Dependency-free SVG radial or linear meter (270°-sweep arc math, no charting library).
1459
+ Dependency-free SVG radial, full-circle ring, or linear meter (no charting library).
1152
1460
 
1153
1461
  **Properties:**
1154
1462
  - `value: number = 0`
1155
1463
  - `min: number = 0`
1156
1464
  - `max: number = 100`
1157
- - `type: 'radial'|'linear' = 'radial'` (reflected)
1465
+ - `type: 'radial'|'ring'|'linear' = 'radial'` (reflected — `radial` is a 270° sweep; `ring` is a
1466
+ full circle that begins at 12 o'clock)
1158
1467
  - `label: string = ''`
1159
- - `valueLabel?: string` (attribute: false — overrides only the *visible* text, see gotchas; an empty
1160
- string is treated the same as unset and falls back to the numeric `value`)
1468
+ - `valueLabel?: string` (attribute: false — overrides both the visible text and the host's
1469
+ `aria-valuetext`; an empty string is treated the same as unset and falls back to the numeric
1470
+ `value` while removing `aria-valuetext`)
1161
1471
 
1162
1472
  **Events:** none.
1163
1473
 
@@ -1165,16 +1475,18 @@ Dependency-free SVG radial or linear meter (270°-sweep arc math, no charting li
1165
1475
 
1166
1476
  **CSS parts:** `base` (the `<svg>`), `track`, `fill`, `value`, `label`
1167
1477
 
1168
- **Themeable custom properties:** `--lyra-color-brand` (fill color, shared token no per-instance
1169
- gauge-specific custom property exists).
1478
+ **Themeable custom properties:** `--lyra-gauge-fill` (fill stroke, falling back to the shared
1479
+ `--lyra-color-brand` token).
1170
1480
 
1171
1481
  **Optional peer deps:** none.
1172
1482
 
1173
1483
  ```html
1174
1484
  <lyra-gauge value="72" min="0" max="100" label="CPU"></lyra-gauge>
1485
+ <lyra-gauge type="ring" value="84" label="Coverage"
1486
+ style="--lyra-gauge-fill: var(--lyra-color-success)"></lyra-gauge>
1175
1487
  <lyra-gauge type="linear" value="0.4" max="1"></lyra-gauge>
1176
1488
  <script type="module">
1177
- document.querySelector('lyra-gauge').valueLabel = '72°F'; // changes displayed text only, see gotchas
1489
+ document.querySelector('lyra-gauge').valueLabel = '72°F'; // visible text and announced value
1178
1490
  </script>
1179
1491
  ```
1180
1492
 
@@ -1183,8 +1495,8 @@ gauge-specific custom property exists).
1183
1495
  changing the visible SVG text), so a screen reader announces your formatted string instead of the
1184
1496
  raw `aria-valuenow` number; the SVG `<text part="value">`/`<text part="label">` elements are
1185
1497
  `aria-hidden="true"` so they're no longer separately exposed inside the same `role="meter"` host.
1186
- - no color-threshold/variant support: a single fixed `--lyra-color-brand` fill regardless
1187
- of value (no amber/red-near-capacity idiom built in).
1498
+ - no automatic color-threshold/variant logic is built in. Set `--lyra-gauge-fill` per instance (or
1499
+ reactively from application state) when the value should select a success/warning/danger color.
1188
1500
  - no documented component-specific sizing custom property; host size is fixed em values
1189
1501
  (`8em`/`12em` radial, `12em`/`1.5em` linear) — resize via plain CSS `width`/`height` on the
1190
1502
  element instead.
@@ -1198,8 +1510,8 @@ gauge-specific custom property exists).
1198
1510
 
1199
1511
  ## `lyra-export-button`
1200
1512
 
1201
- CSV/JSON download button — either single-format (click exports immediately) or multi-format (click
1202
- opens a small menu).
1513
+ CSV/JSON download button with extensible event-driven formats — either single-format (click exports
1514
+ immediately) or multi-format (click opens a small menu).
1203
1515
 
1204
1516
  **Properties:**
1205
1517
  - `rows: Record<string, unknown>[] = []` (attribute: false)
@@ -1209,21 +1521,27 @@ opens a small menu).
1209
1521
  of CSV degrading to a header-less/blank file while only JSON had a fallback — so an unconfigured
1210
1522
  export still produces a proper header + data file in either format
1211
1523
  - `filename: string = 'export'`
1212
- - `formats: ExportFormat[] = ['csv']` (attribute: false; `ExportFormat = 'csv' | 'json'` **only**
1213
- no XLSX despite the family sometimes being described as covering it)
1524
+ - `formats: ExportFormatOption[] = ['csv']` (attribute: false), where `ExportFormatOption` is the
1525
+ built-in `ExportFormat = 'csv' | 'json'` or an `ExportFormatDescriptor = { id: string; label:
1526
+ string; description?: string; extension?: string }`. Descriptor labels/descriptions are
1527
+ consumer-supplied, already-localized copy. Custom ids are event-only; no custom encoder is bundled
1214
1528
  - `disabled: boolean = false` (reflected) — also disables every `[part="menu-item"]` button, not just
1215
1529
  the trigger
1530
+ - `loading: boolean = false` (reflected) — controlled busy state for an async or server-generated
1531
+ export; sets host/trigger `aria-busy` and disables the trigger and menu items. The component does
1532
+ not toggle it automatically
1216
1533
  - `label: string = 'Export'` — trigger button text; also feeds the format-choice menu's `aria-label`
1217
1534
  as `` `${label} format` `` so assistive tech gets an accessible name for the menu
1218
1535
  - `open: boolean = false` (reflected)
1219
1536
 
1220
- **Events:** `lyra-export` (`detail: { format }`, **cancelable** — call `preventDefault()` to
1537
+ **Events:** `lyra-export` (`detail: { format: string }`, **cancelable** — call `preventDefault()` to
1221
1538
  substitute your own server-generated download instead of the built-in client-side one),
1222
- `lyra-export-complete` (`detail: { format }`, fires after a non-cancelled download completes)
1539
+ `lyra-export-complete` (`detail: { format: 'csv' | 'json' }`, fires only after a non-cancelled
1540
+ built-in download completes), `lyra-show`, `lyra-hide` (format-menu visibility transitions)
1223
1541
 
1224
1542
  **Slots:** none.
1225
1543
 
1226
- **CSS parts:** `trigger`, `menu`, `menu-item`
1544
+ **CSS parts:** `trigger`, `menu`, `menu-item`, `format-label`, `format-description`
1227
1545
 
1228
1546
  **Themeable custom properties:** shared tokens only.
1229
1547
 
@@ -1237,6 +1555,19 @@ substitute your own server-generated download instead of the built-in client-sid
1237
1555
  exp.columns = [{ key: 'name', label: 'Name' }, { key: 'value', label: 'Value' }];
1238
1556
  exp.formats = ['csv', 'json']; // shows a format-choice menu instead of exporting immediately
1239
1557
  exp.addEventListener('lyra-export', (e) => console.log('exporting', e.detail.format));
1558
+
1559
+ // Custom formats supply menu copy but remain application-handled.
1560
+ exp.formats = [
1561
+ 'csv',
1562
+ { id: 'xlsx', label: 'Excel workbook', description: 'Preserves spreadsheet data types', extension: 'xlsx' },
1563
+ ];
1564
+ exp.addEventListener('lyra-export', async (e) => {
1565
+ if (e.detail.format !== 'xlsx') return;
1566
+ e.preventDefault();
1567
+ exp.loading = true;
1568
+ try { await exportWorkbook(exp.rows); }
1569
+ finally { exp.loading = false; }
1570
+ });
1240
1571
  </script>
1241
1572
  ```
1242
1573
 
@@ -1249,8 +1580,10 @@ downloadBlob(content: string, filename: string, mime: string): void // trig
1249
1580
  ```
1250
1581
 
1251
1582
  **Known gotchas:**
1252
- - XLSX isn't implemented (`ExportFormat` really is `'csv' | 'json'`) don't pass
1253
- `'xlsx'` expecting it to work.
1583
+ - CSV and JSON are the only built-in encoders. To offer XLSX/PDF/etc., pass an
1584
+ `ExportFormatDescriptor` and handle its id from `lyra-export`; custom formats never trigger a
1585
+ download or `lyra-export-complete` on their own. A descriptor's optional `extension` is metadata
1586
+ for that handler, not automatic filename handling.
1254
1587
  - CSV formula-injection guarding and the deferred (5s) `URL.revokeObjectURL` (works around Safari
1255
1588
  cancelling in-flight downloads on immediate revoke) are genuine, safe-to-rely-on strengths.
1256
1589
  - the multi-format menu (`role="menu"`) supports full arrow-key navigation — ArrowUp/ArrowDown move
@@ -1884,36 +2217,48 @@ same color as `--lyra-heatmap-focus-ring-color`).
1884
2217
  A force-directed node-link diagram with pan/zoom/drag, built on `d3-force`.
1885
2218
 
1886
2219
  **Properties:**
1887
- - `nodes: GraphNode[] = []` (attribute: false) — `GraphNode { id: string; label?: string; radius?:
1888
- number; color?: string }`; `radius` is clamped to `[6, 24]` (an unset/non-finite value falls back
1889
- to the midpoint, `15`) so a node can never render invisibly small or absurdly large
1890
- - `links: GraphLink[] = []` (attribute: false) `GraphLink { source: string; target: string;
1891
- width?: number }` (source/target are node ids). A link whose `source` id doesn't resolve to a real
1892
- node is still dropped entirely (there's no position to draw a stub from). A link whose `target` id
1893
- doesn't resolve instead renders as a short, dashed, non-interactive stub off `source`'s own
1894
- position (`[part='link'][data-dangling]`, `aria-hidden="true"`) rather than being silently dropped
1895
- e.g. for a wiki-style `[[link]]` reference to a not-yet-created node. A dangling stub is excluded
1896
- from `d3-force`'s own simulation input and from click/keyboard interaction.
2220
+ - `nodes: GraphNode[] = []` (attribute: false) — `GraphNode { id: string; label?: string;
2221
+ accessibleLabel?: string; description?: string; radius?: number; color?: string }`;
2222
+ `accessibleLabel` supplies richer spoken text than the visible label, while `description` renders
2223
+ as native SVG `<title>` tooltip text. `radius` is clamped to `[6, 24]` (an unset/non-finite value
2224
+ falls back to the midpoint, `15`) so a node can never render invisibly small or absurdly large
2225
+ - `links: GraphLink[] = []` (attribute: false) `GraphLink { id?: string; source: string; target:
2226
+ string; width?: number; label?: string; accessibleLabel?: string; description?: string; directed?:
2227
+ boolean; color?: string; dash?: number[] }` (source/target are node ids). `directed` adds an
2228
+ arrowhead; `color` and `dash` style the individual stroke; `label` provides a spoken-name and SVG
2229
+ tooltip fallback but is not rendered as visible edge text; `accessibleLabel` and `description`
2230
+ can override the spoken name and tooltip independently. A link whose `source` id doesn't resolve
2231
+ to a real node is still dropped entirely
2232
+ (there's no position to draw a stub from). A link whose `target` id doesn't resolve instead renders
2233
+ as a short, dashed, non-interactive stub off `source`'s own position
2234
+ (`[part='link'][data-dangling]`, `aria-hidden="true"`) rather than being silently dropped — e.g. for
2235
+ a wiki-style `[[link]]` reference to a not-yet-created node. A dangling stub is excluded from
2236
+ `d3-force`'s own simulation input and from click/keyboard interaction.
1897
2237
  - `width: number = 800`
1898
2238
  - `height: number = 600`
1899
2239
  - `chargeStrength: number = -300` (attribute `charge-strength` — live-reactive, see gotchas)
1900
2240
  - `linkDistance: number = 100` (attribute `link-distance` — live-reactive, see gotchas)
1901
2241
  - `minZoom: number = 0.1` (attribute `min-zoom`)
1902
2242
  - `maxZoom: number = 8` (attribute `max-zoom`)
2243
+ - `accessibleLabel: string | null = null` (attribute `aria-label`) — host accessible name forwarded
2244
+ to the internal semantic SVG; when unset, the SVG uses the localized node/link-count summary
1903
2245
  - `seed?: number` — when set, seeds each node's initial x/y deterministically
1904
2246
  (keyed by node **id**, not array index/order) instead of `forceSimulation()`'s own random start,
1905
2247
  and settles the simulation synchronously instead of animating the settle (same effect
1906
2248
  `prefers-reduced-motion` has, see gotchas)
1907
2249
 
1908
- **Events:** `lyra-node-click` (`detail: { id }`), `lyra-link-click` (`detail: { source, target }`)
2250
+ **Events:** `lyra-node-click` (`detail: { id }`), `lyra-link-click` (`detail: { source, target,
2251
+ id? }`; the optional `id` is the stable `GraphLink.id` supplied by the caller)
1909
2252
 
1910
2253
  **Slots:** none.
1911
2254
 
1912
- **CSS parts:** `base`, `svg`, `node`, `link`, `label`
2255
+ **CSS parts:** `base`, `svg`, `node`, `link`, `arrowhead` (the marker path shared by directed links),
2256
+ `label`, `live-region`, `data-list`, `empty`
1913
2257
 
1914
2258
  **Themeable custom properties:** `--lyra-node-fill` (set inline per-node from `GraphNode.color`,
1915
- falls back to `--lyra-color-brand`); also uses `--lyra-color-border` (link stroke), `--lyra-color-text`
1916
- + `--lyra-font` (label text), `--lyra-focus-ring-*` (node/link `:focus-visible` outline).
2259
+ falls back to `--lyra-color-brand`) and `--lyra-link-color` (set inline per-link from
2260
+ `GraphLink.color`, falling back to `--lyra-color-border`); also uses `--lyra-color-text` +
2261
+ `--lyra-font` (label text), `--lyra-focus-ring-*` (node/link `:focus-visible` outline).
1917
2262
 
1918
2263
  **Optional peer deps:** `d3-force`, `d3-drag`, `d3-zoom`, `d3-selection` (all four required
1919
2264
  together; lazy-`import()`ed once per page, `console.warn` once and renders empty if missing —
@@ -1923,9 +2268,17 @@ install with `pnpm add d3-force d3-drag d3-zoom d3-selection`).
1923
2268
  <lyra-graph style="display:block;height:500px"></lyra-graph>
1924
2269
  <script>
1925
2270
  const g = document.querySelector('lyra-graph');
1926
- g.nodes = [{ id: 'a', label: 'A' }, { id: 'b', label: 'B' }];
1927
- g.links = [{ source: 'a', target: 'b' }];
2271
+ g.nodes = [
2272
+ { id: 'a', label: 'A', accessibleLabel: 'Source document A', description: 'The source document' },
2273
+ { id: 'b', label: 'B', description: 'The cited document' },
2274
+ ];
2275
+ g.links = [{
2276
+ id: 'citation-a-b', source: 'a', target: 'b', label: 'cites',
2277
+ accessibleLabel: 'Document A cites document B', description: 'Citation relationship',
2278
+ directed: true, color: 'var(--lyra-color-brand)', dash: [6, 3],
2279
+ }];
1928
2280
  g.addEventListener('lyra-node-click', (e) => console.log(e.detail.id));
2281
+ g.addEventListener('lyra-link-click', (e) => console.log(e.detail.id, e.detail.source, e.detail.target));
1929
2282
  </script>
1930
2283
  ```
1931
2284
 
@@ -1954,6 +2307,9 @@ install with `pnpm add d3-force d3-drag d3-zoom d3-selection`).
1954
2307
  - `GraphNode.color` is sanitized (rejects `;`/`{`/`}`) before being written into the
1955
2308
  `--lyra-node-fill` inline custom property, so an untrusted color string can't break out of that
1956
2309
  CSS declaration.
2310
+ - `GraphLink.color` applies the same declaration-delimiter sanitization. `GraphLink.dash` is used
2311
+ only when every entry is finite and non-negative; an empty or invalid array falls back to a solid
2312
+ line rather than partially applying malformed SVG stroke data.
1957
2313
  - a structural `nodes`/`links` change now carries over each already-settled node's position (and any
1958
2314
  in-progress drag) by id when rebuilding the simulation, instead of discarding every node's (x, y)
1959
2315
  and re-running the whole ~300-tick random-start settle from scratch — only genuinely new ids get a
@@ -1984,10 +2340,13 @@ deeply-nested node's own shadow root still reaches it).
1984
2340
 
1985
2341
  **Properties:**
1986
2342
  - `data: TreeItem[] = []` (attribute: false) — `TreeItem { id: string; label: string; children?:
1987
- TreeItem[]; badge?: string | number }`
2343
+ TreeItem[]; badge?: string | number; icon?: unknown; description?: string; accessibleLabel?:
2344
+ string }`; `icon` renders as a decorative leading visual, `description` as secondary visible row
2345
+ text, and `accessibleLabel` names the `role="treeitem"` host without changing its visible label
1988
2346
  - `label: string = ''` — accessible name for the tree; `role="tree"` lives on an internal
1989
- `[part="base"]` element, not the host, so this is the only way to label it (`aria-label`/
1990
- `aria-labelledby` set directly on `<lyra-tree>` wouldn't reach it)
2347
+ `[part="base"]` element. The component forwards a host `aria-label` to that semantic element when
2348
+ `label` is empty; `label` takes precedence when both are set. External `aria-labelledby` idrefs
2349
+ are not forwarded across the shadow boundary.
1991
2350
 
1992
2351
  **Keyboard:** ArrowDown/ArrowUp move the roving focus to the next/previous *visible* node.
1993
2352
  ArrowRight expands a collapsed node (focus stays put; a second ArrowRight then steps into the first
@@ -2003,7 +2362,7 @@ observed internally to keep the roving `activeId` in sync with clicks).
2003
2362
 
2004
2363
  **Slots:** default (holds the `<lyra-tree-node>` elements it manages).
2005
2364
 
2006
- **CSS parts:** `base`
2365
+ **CSS parts:** `base`, `empty` (the empty-state message shown when `data` is empty)
2007
2366
 
2008
2367
  **Themeable custom properties:** shared tokens `--lyra-space-xs`/`-s`, `--lyra-color-brand-quiet`,
2009
2368
  `--lyra-color-text-quiet`, `--lyra-color-border`, `--lyra-color-text`, `--lyra-radius`,
@@ -2035,12 +2394,15 @@ descendants of the treeitem, matching the WAI-ARIA treeitem pattern's containmen
2035
2394
 
2036
2395
  **Events:** `lyra-node-toggle` (`detail: { id, expanded }`, fired by `expand()`/`collapse()` — via
2037
2396
  the toggle button or ArrowRight/ArrowLeft), `lyra-node-select` (`detail: { id }`, fired by `select()`
2038
- — via clicking the label or Enter/Space) — dispatched from `lyra-tree-node`, bubble/compose up
2397
+ — via clicking anywhere in the row or Enter/Space) — dispatched from `lyra-tree-node`,
2398
+ bubble/compose up
2039
2399
  through `lyra-tree`'s light DOM.
2040
2400
 
2041
2401
  **Slots:** none.
2042
2402
 
2043
- **CSS parts:** `row`, `toggle`, `label`, `badge`
2403
+ **CSS parts:** `row`, `toggle`, `icon`, `content`, `label`, `description`, `badge`, `group`. `icon` is
2404
+ `aria-hidden="true"`; `content` groups the primary label and optional wrapping secondary
2405
+ description while preserving one interactive treeitem per row.
2044
2406
 
2045
2407
  **Themeable custom properties:** `--lyra-tree-depth` (internal, set inline per row for
2046
2408
  indentation), plus the shared tokens listed above.
@@ -2051,7 +2413,14 @@ indentation), plus the shared tokens listed above.
2051
2413
  <lyra-tree></lyra-tree>
2052
2414
  <script>
2053
2415
  document.querySelector('lyra-tree').data = [
2054
- { id: '1', label: 'Root', children: [{ id: '1a', label: 'Child A' }, { id: '1b', label: 'Child B', badge: 3 }] },
2416
+ {
2417
+ id: '1',
2418
+ label: 'Root',
2419
+ description: 'Two child documents',
2420
+ accessibleLabel: 'Root, two child documents',
2421
+ icon: document.createTextNode('◇'),
2422
+ children: [{ id: '1a', label: 'Child A' }, { id: '1b', label: 'Child B', badge: 3 }],
2423
+ },
2055
2424
  ];
2056
2425
  </script>
2057
2426
  ```
@@ -2068,6 +2437,9 @@ indentation), plus the shared tokens listed above.
2068
2437
  tree's `updateComplete` (e.g. after `focusNode()`) doesn't run before an arbitrarily-nested node has
2069
2438
  actually finished rendering its pushed-down `activeId`/`tabIndex` — one more pending update per
2070
2439
  depth level, otherwise.
2440
+ - row enrichment is intentionally structured rather than an unrestricted renderer: use `icon`,
2441
+ `label`, `description`, `badge`, and `accessibleLabel`. This keeps the host as the single
2442
+ `role="treeitem"` interaction target and preserves the APG keyboard model.
2071
2443
 
2072
2444
  ---
2073
2445
 
@@ -2447,6 +2819,10 @@ raw `map` escape hatch for anything unexposed.
2447
2819
  escaped) when the content is plain text. A marker whose `color` changes for a persisting `id`
2448
2820
  can't be recolored in place (no `Marker.setColor()`) and is torn down/reconstructed instead — see
2449
2821
  gotchas.
2822
+ - `label: string = ''` — accessible name for the map region, applied as `[part="base"]`'s
2823
+ `aria-label`. A plain `aria-label` attribute on the host itself is honored as a fallback when
2824
+ `label` is left unset, matching `lyra-slider`/`lyra-checkbox`/`lyra-switch`; with neither set, it
2825
+ falls back to the localized `'map'` message.
2450
2826
 
2451
2827
  **Getters:** `map` → the raw `maplibregl.Map` instance.
2452
2828
 
@@ -2614,6 +2990,11 @@ hand-building that chrome isn't worth it.
2614
2990
  header row (creating one, with no heading text, if `heading` is unset), wired to the same
2615
2991
  `close()` path Escape/backdrop-dismiss already use, with reason `'close-button'`
2616
2992
 
2993
+ Also settable as a plain `aria-label` attribute (not a public JS property): overrides the panel's
2994
+ computed accessible name outright, winning over every other source below (a slotted heading,
2995
+ `heading`, `label`) — matching `<lyra-date-input>`'s `accessibleLabel` pattern. Left unset, the
2996
+ existing three-tier fallback below is unchanged.
2997
+
2617
2998
  **Methods:** `close(reason: DialogCloseReason = 'api'): void` — closes the dialog, emits
2618
2999
  `lyra-dialog-close` with `reason`, and returns focus to whatever had it right before the dialog
2619
3000
  opened. `DialogCloseReason = 'escape' | 'backdrop' | 'close-button' | 'api' | 'unmount' | string` —
@@ -2673,7 +3054,12 @@ clipped; the viewport is still a hard limit either way), plus shared tokens `--l
2673
3054
  </script>
2674
3055
  ```
2675
3056
 
2676
- Accessible name / visible header, in priority order: (1) if a heading element (`h1`–`h6` or
3057
+ Accessible name / visible header, in priority order: (0) if the host element itself has an
3058
+ `aria-label` attribute set, its value becomes `aria-label` on the panel outright, overriding every
3059
+ source below (including a slotted heading) and suppressing the visible header/`heading` row and the
3060
+ sr-only `label` element from rendering at all — the standard ARIA convention for a consumer that
3061
+ wants full control over the announced name regardless of whatever `heading`/`label` props are also
3062
+ set; (1) otherwise, if a heading element (`h1`–`h6` or
2677
3063
  `[role="heading"]`) is a *direct child* (not inside `slot="footer"`), its text content becomes
2678
3064
  `aria-label` on the panel — takes priority over `heading` below so an existing consumer that already
2679
3065
  slots its own heading keeps rendering it exactly as before; (2) otherwise, when `heading` is set, a
@@ -2888,8 +3274,11 @@ labeled options, rendered as a button row" is ubiquitous settings/filter-panel U
2888
3274
 
2889
3275
  **Properties:**
2890
3276
  - `items: SegmentedItem[] = []` (attribute: false) — `SegmentedItem { value: string; label: string;
2891
- disabled?: boolean }`.
3277
+ icon?: unknown; disabled?: boolean }`; `icon` renders as a decorative leading visual inside
3278
+ `segment-icon` and does not replace the required text label.
2892
3279
  - `value: string = ''` — the currently selected item's `value`.
3280
+ - `label: string = ''` — accessible name copied to the internal `role="radiogroup"`; when empty, a
3281
+ host-level `aria-label` is used as a fallback.
2893
3282
 
2894
3283
  **Events:** `lyra-change` (`detail: { value }`) — fired when the selected value changes via click or
2895
3284
  keyboard.
@@ -2897,7 +3286,7 @@ keyboard.
2897
3286
  **Slots:** none.
2898
3287
 
2899
3288
  **CSS parts:** `base` (the `role="radiogroup"` root), `segment` (a single `role="radio"` button),
2900
- `segment-label` (the segment's label text).
3289
+ `segment-icon` (an optional decorative leading icon), `segment-label` (the segment's label text).
2901
3290
 
2902
3291
  **Themeable custom properties:** shared tokens only — `--lyra-color-border`/`-surface`/`-text`/
2903
3292
  `-text-quiet`, `--lyra-radius`, `--lyra-font-size-sm`, `--lyra-font-weight-semibold`,
@@ -2910,8 +3299,8 @@ keyboard.
2910
3299
  <script type="module">
2911
3300
  const seg = document.querySelector('lyra-segmented');
2912
3301
  seg.items = [
2913
- { value: 'day', label: 'Day' },
2914
- { value: 'week', label: 'Week' },
3302
+ { value: 'day', label: 'Day', icon: '☀' },
3303
+ { value: 'week', label: 'Week', icon: '▦' },
2915
3304
  { value: 'month', label: 'Month' },
2916
3305
  ];
2917
3306
  seg.value = 'week';
@@ -2925,6 +3314,8 @@ keyboard.
2925
3314
  - this component self-selects on navigation: clicking or arrow-navigating to an item immediately
2926
3315
  updates `value` and fires `lyra-change` — there's no separate "commit" step the way, e.g.,
2927
3316
  `lyra-select`'s popup has.
3317
+ - the semantic `radiogroup` lives inside shadow DOM. Set `label` (preferred for reactive code) or a
3318
+ host `aria-label`; the component deliberately forwards the resulting name to that internal role.
2928
3319
 
2929
3320
  ---
2930
3321
 
@@ -2998,6 +3389,10 @@ empty string. Restoration updates state, form data, and validity synchronously w
2998
3389
  A boolean toggle-switch form control. `role="switch"` with `aria-checked` read as an on/off state
2999
3390
  rather than checked/unchecked, and no indeterminate state. Structurally the same idea as
3000
3391
  `<lyra-checkbox>` (form-associated via `ElementInternals`, click and Space/Enter both toggle).
3392
+ Ships an opt-in `hint`/`errorText` form-control chrome (props + matching named slots + `hint`/`error`
3393
+ CSS parts), mirroring `<lyra-select>`'s pattern for those two pieces — left unset, neither renders.
3394
+ Deliberately no separate top-of-field `label` prop/slot/part: the default slot already is this
3395
+ control's visible, clickable label (same as `<lyra-checkbox>`).
3001
3396
 
3002
3397
  **Properties:**
3003
3398
  - `checked: boolean = false` (reflected)
@@ -3005,17 +3400,24 @@ rather than checked/unchecked, and no indeterminate state. Structurally the same
3005
3400
  - `required: boolean = false` (reflected — enforced via `internals.setValidity()`)
3006
3401
  - `name: string = ''`
3007
3402
  - `value: string = 'on'` — only contributed to form submission while `checked`
3403
+ - `hint: string = ''` — hint text below the switch. Unset: no hint chrome renders.
3404
+ - `errorText: string = ''` (attribute `error-text`) — error text below the switch (overridden by
3405
+ slotted `error` content). Unset: no error chrome renders.
3008
3406
 
3009
3407
  **Events:** `lyra-change` (`detail: { checked: boolean }`) — fired on a user toggle (click or
3010
3408
  Space/Enter); not fired for a programmatic `.checked` assignment.
3011
3409
 
3012
- **Slots:** default — label text, rendered next to the track. Clicking it toggles the switch, the
3013
- same as clicking a checkbox's associated `<label>`. If left empty, set `aria-label` on the host so
3014
- the control still has an accessible name.
3410
+ **Slots:**
3411
+ - default label text, rendered next to the track. Clicking it toggles the switch, the same as
3412
+ clicking a checkbox's associated `<label>`. If left empty, set `aria-label` on the host so the
3413
+ control still has an accessible name.
3414
+ - `hint` — custom hint content.
3415
+ - `error` — custom error content.
3015
3416
 
3016
- **CSS parts:** `base` (the whole interactive control, `role="switch"`), `track` (the pill-shaped
3017
- background), `thumb` (the circular knob that slides across the track), `label` (wrapper around the
3018
- default slot)
3417
+ **CSS parts:** `form-control` (the outer wrapper around the switch, error and hint), `base` (the
3418
+ whole interactive control, `role="switch"`), `track` (the pill-shaped background), `thumb` (the
3419
+ circular knob that slides across the track), `label` (wrapper around the default slot), `hint` (the
3420
+ hint message), `error` (the error message)
3019
3421
 
3020
3422
  **Themeable custom properties:** `--lyra-switch-track-inline-size` (default `2.25rem`),
3021
3423
  `--lyra-switch-track-block-size` (default `1.25rem`), `--lyra-switch-thumb-offset` (default
@@ -3460,10 +3862,11 @@ variant), `cursor` (the blinking bar in the `cursor` variant)
3460
3862
  **Themeable custom properties:** `--lyra-typing-dot-size` (default `0.5rem`, `0.375rem` at
3461
3863
  `size="sm"`), `--lyra-typing-gap` (default `0.25rem`, `0.1875rem` at `size="sm"`),
3462
3864
  `--lyra-typing-cursor-width` (default `0.125rem`, `0.09375rem` at `size="sm"`),
3463
- `--lyra-typing-cursor-height` (default `1em`, unaffected by `size`) — all component-specific; plus
3464
- the shared `--lyra-transition-base` token, used as each variant's full `animation:` shorthand
3465
- (duration + timing-function together) since it drives a continuous ambient loop rather than a
3466
- discrete state flip (which would instead use `--lyra-transition-fast`).
3865
+ `--lyra-typing-cursor-height` (default `1em`, unaffected by `size`),
3866
+ `--lyra-typing-dot-stagger-1` (default `600ms`, second dot), and
3867
+ `--lyra-typing-dot-stagger-2` (default `1200ms`, third dot). Every variant uses the shared
3868
+ `--lyra-transition-ambient` compound duration/timing token (default `1.8s ease-in-out`) as its
3869
+ animation cycle.
3467
3870
 
3468
3871
  **Optional peer deps:** none.
3469
3872
 
@@ -3471,6 +3874,9 @@ discrete state flip (which would instead use `--lyra-transition-fast`).
3471
3874
  <lyra-typing-indicator label="Assistant is responding…"></lyra-typing-indicator>
3472
3875
  <lyra-typing-indicator variant="pulse" size="sm"></lyra-typing-indicator>
3473
3876
  <lyra-typing-indicator variant="cursor"></lyra-typing-indicator>
3877
+ <lyra-typing-indicator
3878
+ style="--lyra-transition-ambient: 900ms ease-in-out; --lyra-typing-dot-stagger-1: 300ms; --lyra-typing-dot-stagger-2: 600ms"
3879
+ ></lyra-typing-indicator>
3474
3880
  ```
3475
3881
 
3476
3882
  Accessibility: since this indicator typically mounts and unmounts around a real generation lifecycle
@@ -3488,11 +3894,9 @@ decorative; `label` is the entire accessible content, nothing narrates individua
3488
3894
  state (`opacity: 1`, no transform, `animation: none !important`) rather than freezing on whatever
3489
3895
  frame the animation happened to be on — notably relevant for `cursor`, which would otherwise risk
3490
3896
  freezing on its invisible ("off") blink half.
3491
- - the two dot-bounce stagger delays (`60ms`, `120ms`) are fixed literals, not `calc()`-derived
3492
- fractions of `--lyra-transition-base` that token is a *compound* `duration timing-function` value
3493
- (like every `transition:`/`animation:` shorthand token in this library), so it can't be decomposed
3494
- arithmetically; the two delays just approximate a third and two-thirds of the token's own default
3495
- duration.
3897
+ - `--lyra-transition-ambient` is a compound `duration timing-function` value and cannot be divided
3898
+ with `calc()`. When retiming it, override both stagger properties alongside it to preserve the
3899
+ default one-third/two-thirds dot phasing, as shown above.
3496
3900
  - `size="sm"` shrinks the dot size, gap, and cursor width, but **not** `--lyra-typing-cursor-height`
3497
3901
  (still `1em` at any size) — the cursor bar's height is meant to track surrounding text size via
3498
3902
  `1em`, not the component's own `size` property.
@@ -3817,7 +4221,9 @@ state, so no additional scroll-lock/focus-trap bookkeeping is needed for that tr
3817
4221
  ## `lyra-chat-composer`
3818
4222
 
3819
4223
  The message input for a chat/agent conversation surface: an auto-resizing `<textarea>` plus a
3820
- built-in send/stop button. **Form-associated** via the shared `FormAssociated` mixin (same shape as
4224
+ built-in send/stop button. Deliberately no label/hint/error chrome a composite chat-input
4225
+ control, not a labeled form field; wrap it in your own layout for that context. **Form-associated**
4226
+ via the shared `FormAssociated` mixin (same shape as
3821
4227
  `<lyra-date-input>`) — `name: string = ''`, `value: string = ''`, `disabled: boolean = false`
3822
4228
  (reflected), `required: boolean = false` (reflected) are all inherited, along with
3823
4229
  `checkValidity()`/`reportValidity()`, so it participates in native `<form>` submission/validation/
@@ -3836,6 +4242,12 @@ reveals the invalid state, and `form.reset()` clears the touched presentation.
3836
4242
  built-in button's icon/label (send vs. stop) and whether Enter still submits
3837
4243
  - `submitOnEnter: boolean = true` (reflected, attribute `submit-on-enter`) — when `false`, Enter
3838
4244
  always inserts a newline instead of submitting
4245
+ - `spellcheck: boolean = true` — forwarded to the internal `<textarea>`
4246
+ - `autocapitalize: string = ''` — forwarded to the internal `<textarea>`; empty omits the attribute
4247
+ - `autoCorrect: string = ''` (attribute `autocorrect`) — forwarded to the internal `<textarea>`
4248
+ (Safari/WebKit-specific); empty omits the attribute. Named `autoCorrect`, not `autocorrect`, only
4249
+ to dodge a `lib.dom.d.ts` collision with `HTMLElement`'s own `boolean`-typed `autocorrect` IDL
4250
+ member; the host attribute is explicitly mapped to plain `autocorrect`.
3839
4251
 
3840
4252
  **Methods (own):** none beyond the inherited `checkValidity()`/`reportValidity()`.
3841
4253
 
@@ -3847,6 +4259,10 @@ reveals the invalid state, and `form.reset()` clears the touched presentation.
3847
4259
  trimming is left to the consumer. Submitting does **not** clear `value`
3848
4260
  - `lyra-stop` (no detail) — fired by the built-in button while `status` is `"sending"` or
3849
4261
  `"streaming"`
4262
+ - `blur` (no detail) — re-dispatched from the internal `<textarea>`'s own `blur`, bubbling and
4263
+ composed unlike the native event
4264
+ - `focus` (no detail) — re-dispatched from the internal `<textarea>`'s own `focus`, for the same
4265
+ reason as `blur`
3850
4266
 
3851
4267
  **Slots:** `leading` (content before the textarea, e.g. an attach-file trigger button), `chips` (an
3852
4268
  attachment tray rendered above the input row), `trailing` (overrides the built-in send/stop button
@@ -4337,6 +4753,12 @@ First-party invention (no Web Awesome equivalent).
4337
4753
  rename button never renders and the row can never enter its editing state; flipping it to `false`
4338
4754
  while a rename is already in progress cancels that edit (discards the draft, like Escape) rather
4339
4755
  than leaving it stranded and still committable.
4756
+ - `spellcheck: boolean = true` — forwarded to the in-place rename `<input>`; `spellcheck="false"` is
4757
+ parsed as false (not Lit's default boolean-attribute behavior)
4758
+ - `autocapitalize: string = ''` — forwarded to the in-place rename `<input>`; empty omits the attribute
4759
+ - `autoCorrect: string = ''` (attribute `autocorrect`) — forwarded to the in-place rename `<input>`
4760
+ (Safari/WebKit-specific); empty omits the attribute. Named `autoCorrect` to avoid
4761
+ `HTMLElement.autocorrect`'s incompatible DOM typing.
4340
4762
 
4341
4763
  **Events:** `lyra-select` (no detail payload — identify the row via the platform `id` attribute on the
4342
4764
  event's `target`/`currentTarget`, the same convention `<lyra-attachment-chip>` uses; fires on a click on
@@ -4344,7 +4766,9 @@ event's `target`/`currentTarget`, the same convention `<lyra-attachment-chip>` u
4344
4766
  while not currently renaming), `lyra-rename` (`detail: { title: string }` — an in-place rename was
4345
4767
  committed via Enter or blur-while-editing; does not mutate `title` itself, this is a controlled
4346
4768
  component — not fired when the trimmed draft is empty or unchanged from the original `title`, treated
4347
- as an implicit cancel)
4769
+ as an implicit cancel), `blur` (no detail — re-dispatched from the in-place rename `<input>`'s own
4770
+ `blur`, bubbling and composed unlike the native event), `focus` (no detail — re-dispatched from the
4771
+ in-place rename `<input>`'s own `focus`, for the same reason as `blur`)
4348
4772
 
4349
4773
  **Slots:** `actions` — overflow/icon-button controls rendered at the trailing edge of the row (e.g. a
4350
4774
  pin/delete control); only visually shown once it actually has assigned elements.
@@ -4427,7 +4851,20 @@ emitting `lyra-change`.
4427
4851
  id, and once non-empty it takes over as the accessible-name source (an `aria-label` override is
4428
4852
  then only consulted as the fallback). Empty (the default) keeps the original
4429
4853
  `aria-label || placeholder || 'Model'` accessible-name chain untouched.
4854
+ - `hint: string = ''` — hint text below the field. Unset (the default): no hint chrome renders.
4855
+ - `errorText: string = ''` (attribute `error-text`) — error text below the field (overridden by
4856
+ slotted `error` content). Unset (the default): no error chrome renders.
4430
4857
  - `placeholder: string = ''`
4858
+ - `spellcheck: boolean = true` — forwarded to the free-text mode's native `<input>`; no effect in
4859
+ closed-dropdown mode (no native text input there). `spellcheck="false"` is parsed as `false` (a
4860
+ custom converter, not Lit's presence-based `type: Boolean`, so the literal attribute string is
4861
+ honored — matches `<lyra-textarea>`/`<lyra-date-input>`).
4862
+ - `autocapitalize: string = ''` — forwarded to the free-text mode's native `<input>`; empty omits
4863
+ the attribute.
4864
+ - `autoCorrect: string = ''` (attribute `autocorrect`) — forwarded to the free-text mode's native
4865
+ `<input>`'s own `autocorrect` (Safari/WebKit-specific); empty omits the attribute. Named
4866
+ `autoCorrect` in JS purely to dodge a `lib.dom.d.ts` typing collision — the wire attribute is
4867
+ still plain `autocorrect`.
4431
4868
  - `name: string = ''` (reflected)
4432
4869
  - `disabled: boolean = false` (reflected)
4433
4870
  - `required: boolean = false` (reflected — enforced via `internals.setValidity()`)
@@ -4450,12 +4887,18 @@ since changed) is never silently dropped: it's appended to the rendered option l
4450
4887
  visually-distinct row (dashed border, italic label, "not in catalog" badge) computed fresh from
4451
4888
  `catalog` + `value` on every access, without ever mutating the `catalog` property itself.
4452
4889
 
4453
- **Events:** `lyra-change` (`detail: { value: string; inCatalog: boolean }` — fired when a value is
4454
- selected from the listbox or committed in free-text mode; `inCatalog` reflects whether that value was
4455
- actually present in `normalizedCatalog`, so a consumer can flag a freshly-typed custom value distinctly
4456
- from a real catalog pick)
4457
-
4458
- **Slots:** none.
4890
+ **Events:**
4891
+ - `lyra-change` (`detail: { value: string; inCatalog: boolean }` fired when a value is selected
4892
+ from the listbox or committed in free-text mode; `inCatalog` reflects whether that value was
4893
+ actually present in `normalizedCatalog`, so a consumer can flag a freshly-typed custom value
4894
+ distinctly from a real catalog pick)
4895
+ - `blur` (no detail) — re-dispatched from the free-text mode's internal `<input>`'s own `blur`,
4896
+ bubbling and composed unlike the native event. Closed-dropdown mode's trigger `<button>` has no
4897
+ equivalent re-dispatch, matching `<lyra-select>`'s own trigger.
4898
+ - `focus` (no detail) — re-dispatched from the free-text mode's internal `<input>`'s own `focus`,
4899
+ for the same reason as `blur`.
4900
+
4901
+ **Slots:** `hint` (custom hint content), `error` (custom error content).
4459
4902
 
4460
4903
  **CSS parts:** `form-control-label` (the `<label>` element — only rendered, and only contributes to
4461
4904
  the accessible name, once `label` is non-empty), `trigger` (closed-dropdown mode's
@@ -4463,7 +4906,8 @@ the accessible name, once `label` is non-empty), `trigger` (closed-dropdown mode
4463
4906
  container, also its positioning anchor), `combobox-input` (the free-text `<input>`),
4464
4907
  `provider-badge` (the optional leading `provider` label), `listbox` (the options popover, shared by
4465
4908
  both modes), `option`, `option-label`, `option-badge` (the "not in catalog" badge on a synthetic
4466
- stale-value row), `expand-icon` (the dropdown chevron, present in both modes)
4909
+ stale-value row), `expand-icon` (the dropdown chevron, present in both modes), `hint` (the hint
4910
+ message), `error` (the error message)
4467
4911
 
4468
4912
  **Themeable custom properties:** shared tokens only — `--lyra-space-xs/-s`,
4469
4913
  `--lyra-color-border/-surface/-brand/-brand-quiet/-text-quiet`, `--lyra-radius`, `--lyra-shadow`,
@@ -4500,6 +4944,9 @@ stale-value row), `expand-icon` (the dropdown chevron, present in both modes)
4500
4944
  - In free-text mode, the input's displayed text is `query` only while `open`; while closed it shows the
4501
4945
  committed value's label — so setting `.value` programmatically doesn't require also touching the live
4502
4946
  typed text.
4947
+ - `hint`/`errorText` mirror `<lyra-select>`'s identical chrome, including the `aria-describedby` wiring
4948
+ to the rendered `hint`/`error` ids — set on whichever element (`trigger` or `combobox-input`) owns
4949
+ `role="combobox"` for the active mode.
4503
4950
 
4504
4951
  ---
4505
4952
 
@@ -4939,7 +5386,8 @@ accessor itself, which still takes full priority.
4939
5386
  no overlay chrome renders until `mode` is `'mobile'` again. Set this directly, or use the built-in
4940
5387
  toggle button — there is no separate `show()`/`hide()` pair.
4941
5388
  - `label: string = 'Navigation'` — accessible name for the rail's navigation landmark, and for its
4942
- dialog role while the mobile overlay is open.
5389
+ dialog role while the mobile overlay is open. A host-level `aria-label` attribute (see below)
5390
+ takes precedence over this when both are set.
4943
5391
  - `preferredMode?: 'full' | 'icon-only' | null` (attribute `preferred-mode`) — manually prefers
4944
5392
  `'full'` or `'icon-only'` for the non-mobile breakpoint axis, while `mobile-breakpoint` continues to
4945
5393
  be tracked automatically regardless — e.g. a user's manual collapse toggle that should still yield
@@ -4968,6 +5416,10 @@ accessor itself, which still takes full priority.
4968
5416
  "chases" the pointer instead of tracking it 1:1. Effectively read-only (this component owns the
4969
5417
  transitions), but a plain reflected property like every other boolean here.
4970
5418
 
5419
+ Also settable as a plain `aria-label` attribute (not a reactive property): overrides the computed
5420
+ `label`/localized-default accessible name on both the navigation landmark and the mobile dialog
5421
+ role, matching `<lyra-date-input>`'s `accessibleLabel`.
5422
+
4971
5423
  **Events:** `lyra-mode-change` (`detail: AppRailModeChangeDetail` = `{ mode: AppRailMode }`; the
4972
5424
  effective mode changed, whether from a breakpoint crossing or an explicit `mode` assignment — not
4973
5425
  fired for a redundant reassignment to the mode already in effect), `lyra-toggle`
@@ -5219,7 +5671,10 @@ own listbox.
5219
5671
  - `filter: MentionFilter | null = null` (attribute: false) — overrides the built-in
5220
5672
  case-insensitive `label`/`description` substring match entirely.
5221
5673
  - `emptyText: string = 'No matches'` (attribute `empty-text`)
5222
- - `label: string = 'Suggestions'` — accessible name for the `role="listbox"` popup.
5674
+ - `label: string = 'Suggestions'` — accessible name for the `role="listbox"` popup. A host-level
5675
+ plain `aria-label` attribute on `<lyra-mention-popover>` itself takes priority over this property
5676
+ when present (checked via a plain `getAttribute()` read, not a reactive property) — matches the
5677
+ same fallback on `<lyra-combobox>`/`<lyra-table>`.
5223
5678
  - `filteredItems: MentionItem[]` — read-only getter; `items` filtered by `query` via `filter` (or
5224
5679
  the built-in default). Empty `query` returns `items` unfiltered.
5225
5680
  - `activeDescendantId: string | null` — read-only getter; the `id` of the currently-highlighted
@@ -5302,6 +5757,9 @@ automatically only on an `anchor` or `query` change while open (a keystroke move
5302
5757
  fresh `query` is the proxy for "the caret may have moved").
5303
5758
 
5304
5759
  **Known gotchas:**
5760
+ - a host-level `aria-label` attribute on `<lyra-mention-popover>` now takes priority over `label`
5761
+ (and its localized default) when resolving `[part="listbox"]`'s accessible name — previously it
5762
+ was silently ignored. Matches the same fallback on `<lyra-combobox>`/`<lyra-table>`.
5305
5763
  - The popover opens pre-highlighted on the top match (index 0), unlike `<lyra-combobox>`'s own
5306
5764
  listbox which opens with nothing highlighted (`-1`) — a bare Enter right after opening commits
5307
5765
  immediately.
@@ -5922,6 +6380,12 @@ shared composed-tree focus traversal used by the other modal families.
5922
6380
  disconnect/reconnect fire back-to-back with no update in between.
5923
6381
  - the Approve button's native `disabled` attribute (while the draft is invalid JSON) automatically
5924
6382
  excludes it from the shared Tab trap, whose focusable-set computation skips disabled controls.
6383
+ - the raw-JSON `args-editor` textarea always hardcodes `spellcheck="false"`, `autocapitalize="off"`,
6384
+ and `autocorrect="off"` — not exposed as configurable properties, unlike `<lyra-textarea>`'s/
6385
+ `<lyra-chat-composer>`'s passthrough props — since its content is always JSON, never prose; without
6386
+ this a mobile browser (notably iOS Safari, which defaults textarea `autocapitalize` to
6387
+ `'sentences'`) could auto-capitalize or auto-correct key/value text as the user edits, silently
6388
+ corrupting the JSON.
5925
6389
 
5926
6390
  ---
5927
6391
 
@@ -6081,7 +6545,10 @@ menu, an avatar menu, or a history row's overflow menu. Uses the WAI-ARIA "menu
6081
6545
  side is mirrored under `dir="rtl"`, so e.g. `placement="left-start"` still anchors to the menu's
6082
6546
  trailing edge instead of pinning to the physical left)
6083
6547
  - `label: string = 'Menu'` — accessible name for the `role="menu"` popup; override with something
6084
- specific (e.g. "Row actions") when a page has more than one menu
6548
+ specific (e.g. "Row actions") when a page has more than one menu. A host-level `aria-label`
6549
+ attribute takes precedence over both this prop and the localized default (unset by default, so
6550
+ a no-op for existing consumers), matching `lyra-select`/`lyra-model-select`'s
6551
+ `this.getAttribute('aria-label') || <computed default>` precedence
6085
6552
 
6086
6553
  **Events:** `lyra-show` (no detail — fires only when `open` transitions to `true`, not for markup
6087
6554
  that renders `open` true from the start), `lyra-hide` (same first-render guard, opposite
@@ -6217,7 +6684,8 @@ already follow.
6217
6684
  - `removable: boolean = false` (reflected — shows the remove (×) button)
6218
6685
  - `selected: boolean = false` (reflected) — opt-in toggle/pressed mode: when set, `[part='base']`
6219
6686
  itself becomes focusable and keyboard-activatable (Enter/Space, mirroring native `<button>`
6220
- behavior), reflects `aria-pressed`, and toggles on click/activation, emitting `lyra-chip-select`.
6687
+ behavior), reflects `aria-pressed` (explicitly `"false"`, not omitted, whenever toggle mode is
6688
+ active but unpressed), and toggles on click/activation, emitting `lyra-chip-select`.
6221
6689
  Has no effect (no interactive semantics added to `[part='base']`) when combined with `removable`,
6222
6690
  since the remove button already nests inside `[part='base']` — a focusable descendant of a
6223
6691
  `role="button"` ancestor isn't allowed there, and this component's two real use cases (a
@@ -6245,7 +6713,8 @@ affordance, only rendered while `removable`)
6245
6713
  tone; default `var(--lyra-color-text)` / `var(--lyra-color-surface)` / `var(--lyra-color-border)` —
6246
6714
  mirrors the same accent/bg/border vocabulary `<lyra-tool-call-chip>`/`<lyra-attachment-chip>` use),
6247
6715
  `--lyra-chip-pressed-border` (border color while pressed/selected — falls back to
6248
- `--lyra-chip-accent`), plus shared tokens (`--lyra-space-xs`, `--lyra-space-s`,
6716
+ `--lyra-chip-accent`), `--lyra-chip-pressed-bg` (background color while pressed/selected — falls
6717
+ back to `--lyra-chip-bg`), plus shared tokens (`--lyra-space-xs`, `--lyra-space-s`,
6249
6718
  `--lyra-color-brand`/`-brand-quiet`, `--lyra-color-success`/`-success-quiet`,
6250
6719
  `--lyra-color-warning`/`-warning-quiet`, `--lyra-color-danger`/`-danger-quiet`,
6251
6720
  `--lyra-icon-button-size`, `--lyra-focus-ring-width`, `--lyra-focus-ring-color`,
@@ -6253,6 +6722,16 @@ mirrors the same accent/bg/border vocabulary `<lyra-tool-call-chip>`/`<lyra-atta
6253
6722
 
6254
6723
  **Optional peer deps:** none.
6255
6724
 
6725
+ ```html
6726
+ <lyra-chip
6727
+ toggleable
6728
+ selected
6729
+ style="--lyra-chip-bg: var(--lyra-color-surface); --lyra-chip-pressed-bg: var(--lyra-color-warning-quiet)"
6730
+ >
6731
+ Priority filter
6732
+ </lyra-chip>
6733
+ ```
6734
+
6256
6735
  ### `lyra-chip-group`
6257
6736
 
6258
6737
  A flex-wrap container for a set of `<lyra-chip>` children — plain light-DOM composition, direct