@aceshooting/lyra-ui 2.5.0 → 2.7.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 (214) hide show
  1. package/README.md +13 -7
  2. package/custom-elements.json +4560 -1025
  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/chat-message/chat-message.class.d.ts +7 -1
  12. package/dist/components/chat-message/chat-message.class.d.ts.map +1 -1
  13. package/dist/components/chat-message/chat-message.class.js +15 -4
  14. package/dist/components/chat-message/chat-message.class.js.map +1 -1
  15. package/dist/components/checkbox/checkbox.class.d.ts +7 -0
  16. package/dist/components/checkbox/checkbox.class.d.ts.map +1 -1
  17. package/dist/components/checkbox/checkbox.class.js +7 -0
  18. package/dist/components/checkbox/checkbox.class.js.map +1 -1
  19. package/dist/components/chip/chip.class.d.ts +2 -0
  20. package/dist/components/chip/chip.class.d.ts.map +1 -1
  21. package/dist/components/chip/chip.class.js +3 -1
  22. package/dist/components/chip/chip.class.js.map +1 -1
  23. package/dist/components/chip/chip.styles.d.ts.map +1 -1
  24. package/dist/components/chip/chip.styles.js +10 -2
  25. package/dist/components/chip/chip.styles.js.map +1 -1
  26. package/dist/components/combobox/combobox.class.d.ts +16 -0
  27. package/dist/components/combobox/combobox.class.d.ts.map +1 -1
  28. package/dist/components/combobox/combobox.class.js +47 -2
  29. package/dist/components/combobox/combobox.class.js.map +1 -1
  30. package/dist/components/combobox/combobox.styles.d.ts.map +1 -1
  31. package/dist/components/combobox/combobox.styles.js +56 -6
  32. package/dist/components/combobox/combobox.styles.js.map +1 -1
  33. package/dist/components/conversation-item/conversation-item.class.d.ts +14 -0
  34. package/dist/components/conversation-item/conversation-item.class.d.ts.map +1 -1
  35. package/dist/components/conversation-item/conversation-item.class.js +38 -0
  36. package/dist/components/conversation-item/conversation-item.class.js.map +1 -1
  37. package/dist/components/date-picker/date-input.class.d.ts +25 -0
  38. package/dist/components/date-picker/date-input.class.d.ts.map +1 -1
  39. package/dist/components/date-picker/date-input.class.js +61 -0
  40. package/dist/components/date-picker/date-input.class.js.map +1 -1
  41. package/dist/components/dialog/dialog.class.d.ts +13 -1
  42. package/dist/components/dialog/dialog.class.d.ts.map +1 -1
  43. package/dist/components/dialog/dialog.class.js +24 -8
  44. package/dist/components/dialog/dialog.class.js.map +1 -1
  45. package/dist/components/empty/empty.class.d.ts +2 -0
  46. package/dist/components/empty/empty.class.d.ts.map +1 -1
  47. package/dist/components/empty/empty.class.js +2 -0
  48. package/dist/components/empty/empty.class.js.map +1 -1
  49. package/dist/components/empty/empty.styles.d.ts.map +1 -1
  50. package/dist/components/empty/empty.styles.js +6 -3
  51. package/dist/components/empty/empty.styles.js.map +1 -1
  52. package/dist/components/export-button/export-button.class.d.ts +19 -2
  53. package/dist/components/export-button/export-button.class.d.ts.map +1 -1
  54. package/dist/components/export-button/export-button.class.js +32 -8
  55. package/dist/components/export-button/export-button.class.js.map +1 -1
  56. package/dist/components/export-button/export-button.styles.d.ts.map +1 -1
  57. package/dist/components/export-button/export-button.styles.js +8 -1
  58. package/dist/components/export-button/export-button.styles.js.map +1 -1
  59. package/dist/components/gauge/gauge.class.d.ts +6 -2
  60. package/dist/components/gauge/gauge.class.d.ts.map +1 -1
  61. package/dist/components/gauge/gauge.class.js +40 -4
  62. package/dist/components/gauge/gauge.class.js.map +1 -1
  63. package/dist/components/gauge/gauge.styles.js +1 -1
  64. package/dist/components/graph/graph.class.d.ts +28 -1
  65. package/dist/components/graph/graph.class.d.ts.map +1 -1
  66. package/dist/components/graph/graph.class.js +94 -42
  67. package/dist/components/graph/graph.class.js.map +1 -1
  68. package/dist/components/graph/graph.styles.d.ts.map +1 -1
  69. package/dist/components/graph/graph.styles.js +4 -1
  70. package/dist/components/graph/graph.styles.js.map +1 -1
  71. package/dist/components/heatmap/heatmap.class.d.ts +22 -1
  72. package/dist/components/heatmap/heatmap.class.d.ts.map +1 -1
  73. package/dist/components/heatmap/heatmap.class.js +38 -3
  74. package/dist/components/heatmap/heatmap.class.js.map +1 -1
  75. package/dist/components/map/map.class.d.ts +4 -2
  76. package/dist/components/map/map.class.d.ts.map +1 -1
  77. package/dist/components/map/map.class.js +5 -3
  78. package/dist/components/map/map.class.js.map +1 -1
  79. package/dist/components/mention-popover/mention-popover.class.d.ts +9 -4
  80. package/dist/components/mention-popover/mention-popover.class.d.ts.map +1 -1
  81. package/dist/components/mention-popover/mention-popover.class.js +11 -5
  82. package/dist/components/mention-popover/mention-popover.class.js.map +1 -1
  83. package/dist/components/menu/menu.class.d.ts +10 -4
  84. package/dist/components/menu/menu.class.d.ts.map +1 -1
  85. package/dist/components/menu/menu.class.js +11 -5
  86. package/dist/components/menu/menu.class.js.map +1 -1
  87. package/dist/components/model-select/model-select.class.d.ts +44 -0
  88. package/dist/components/model-select/model-select.class.d.ts.map +1 -1
  89. package/dist/components/model-select/model-select.class.js +125 -0
  90. package/dist/components/model-select/model-select.class.js.map +1 -1
  91. package/dist/components/model-select/model-select.styles.d.ts.map +1 -1
  92. package/dist/components/model-select/model-select.styles.js +21 -0
  93. package/dist/components/model-select/model-select.styles.js.map +1 -1
  94. package/dist/components/pagination/pagination.class.d.ts +81 -0
  95. package/dist/components/pagination/pagination.class.d.ts.map +1 -0
  96. package/dist/components/pagination/pagination.class.js +278 -0
  97. package/dist/components/pagination/pagination.class.js.map +1 -0
  98. package/dist/components/pagination/pagination.d.ts +2 -0
  99. package/dist/components/pagination/pagination.d.ts.map +1 -0
  100. package/dist/components/pagination/pagination.js +5 -0
  101. package/dist/components/pagination/pagination.js.map +1 -0
  102. package/dist/components/pagination/pagination.styles.d.ts +2 -0
  103. package/dist/components/pagination/pagination.styles.d.ts.map +1 -0
  104. package/dist/components/pagination/pagination.styles.js +142 -0
  105. package/dist/components/pagination/pagination.styles.js.map +1 -0
  106. package/dist/components/phone-input/phone-input.class.d.ts +191 -0
  107. package/dist/components/phone-input/phone-input.class.d.ts.map +1 -0
  108. package/dist/components/phone-input/phone-input.class.js +493 -0
  109. package/dist/components/phone-input/phone-input.class.js.map +1 -0
  110. package/dist/components/phone-input/phone-input.d.ts +2 -0
  111. package/dist/components/phone-input/phone-input.d.ts.map +1 -0
  112. package/dist/components/phone-input/phone-input.js +5 -0
  113. package/dist/components/phone-input/phone-input.js.map +1 -0
  114. package/dist/components/phone-input/phone-input.styles.d.ts +2 -0
  115. package/dist/components/phone-input/phone-input.styles.d.ts.map +1 -0
  116. package/dist/components/phone-input/phone-input.styles.js +123 -0
  117. package/dist/components/phone-input/phone-input.styles.js.map +1 -0
  118. package/dist/components/poll-status/poll-status.styles.d.ts.map +1 -1
  119. package/dist/components/poll-status/poll-status.styles.js +5 -6
  120. package/dist/components/poll-status/poll-status.styles.js.map +1 -1
  121. package/dist/components/segmented/segmented.class.d.ts +4 -0
  122. package/dist/components/segmented/segmented.class.d.ts.map +1 -1
  123. package/dist/components/segmented/segmented.class.js +2 -1
  124. package/dist/components/segmented/segmented.class.js.map +1 -1
  125. package/dist/components/segmented/segmented.styles.d.ts.map +1 -1
  126. package/dist/components/segmented/segmented.styles.js +7 -0
  127. package/dist/components/segmented/segmented.styles.js.map +1 -1
  128. package/dist/components/skeleton/skeleton.styles.d.ts.map +1 -1
  129. package/dist/components/skeleton/skeleton.styles.js +1 -0
  130. package/dist/components/skeleton/skeleton.styles.js.map +1 -1
  131. package/dist/components/slider/slider.class.d.ts +3 -0
  132. package/dist/components/slider/slider.class.d.ts.map +1 -1
  133. package/dist/components/slider/slider.class.js +3 -0
  134. package/dist/components/slider/slider.class.js.map +1 -1
  135. package/dist/components/stream-status/stream-status.styles.d.ts.map +1 -1
  136. package/dist/components/stream-status/stream-status.styles.js +4 -5
  137. package/dist/components/stream-status/stream-status.styles.js.map +1 -1
  138. package/dist/components/switch/switch.class.d.ts +20 -0
  139. package/dist/components/switch/switch.class.d.ts.map +1 -1
  140. package/dist/components/switch/switch.class.js +83 -23
  141. package/dist/components/switch/switch.class.js.map +1 -1
  142. package/dist/components/switch/switch.styles.d.ts.map +1 -1
  143. package/dist/components/switch/switch.styles.js +18 -0
  144. package/dist/components/switch/switch.styles.js.map +1 -1
  145. package/dist/components/table/table.class.d.ts +12 -0
  146. package/dist/components/table/table.class.d.ts.map +1 -1
  147. package/dist/components/table/table.class.js +6 -1
  148. package/dist/components/table/table.class.js.map +1 -1
  149. package/dist/components/table/table.styles.d.ts.map +1 -1
  150. package/dist/components/table/table.styles.js +3 -0
  151. package/dist/components/table/table.styles.js.map +1 -1
  152. package/dist/components/textarea/textarea.class.d.ts +127 -0
  153. package/dist/components/textarea/textarea.class.d.ts.map +1 -0
  154. package/dist/components/textarea/textarea.class.js +364 -0
  155. package/dist/components/textarea/textarea.class.js.map +1 -0
  156. package/dist/components/textarea/textarea.d.ts +2 -0
  157. package/dist/components/textarea/textarea.d.ts.map +1 -0
  158. package/dist/components/textarea/textarea.js +5 -0
  159. package/dist/components/textarea/textarea.js.map +1 -0
  160. package/dist/components/textarea/textarea.styles.d.ts +2 -0
  161. package/dist/components/textarea/textarea.styles.d.ts.map +1 -0
  162. package/dist/components/textarea/textarea.styles.js +67 -0
  163. package/dist/components/textarea/textarea.styles.js.map +1 -0
  164. package/dist/components/time-range/time-range.class.d.ts +4 -0
  165. package/dist/components/time-range/time-range.class.d.ts.map +1 -1
  166. package/dist/components/time-range/time-range.class.js +4 -0
  167. package/dist/components/time-range/time-range.class.js.map +1 -1
  168. package/dist/components/tool-approval-dialog/tool-approval-dialog.class.d.ts +6 -0
  169. package/dist/components/tool-approval-dialog/tool-approval-dialog.class.d.ts.map +1 -1
  170. package/dist/components/tool-approval-dialog/tool-approval-dialog.class.js +8 -0
  171. package/dist/components/tool-approval-dialog/tool-approval-dialog.class.js.map +1 -1
  172. package/dist/components/tree/tree-node.class.d.ts +3 -0
  173. package/dist/components/tree/tree-node.class.d.ts.map +1 -1
  174. package/dist/components/tree/tree-node.class.js +16 -1
  175. package/dist/components/tree/tree-node.class.js.map +1 -1
  176. package/dist/components/tree/tree-node.styles.d.ts.map +1 -1
  177. package/dist/components/tree/tree-node.styles.js +27 -1
  178. package/dist/components/tree/tree-node.styles.js.map +1 -1
  179. package/dist/components/tree/tree.class.d.ts +11 -1
  180. package/dist/components/tree/tree.class.d.ts.map +1 -1
  181. package/dist/components/tree/tree.class.js +6 -2
  182. package/dist/components/tree/tree.class.js.map +1 -1
  183. package/dist/components/typing-indicator/typing-indicator.class.d.ts +4 -0
  184. package/dist/components/typing-indicator/typing-indicator.class.d.ts.map +1 -1
  185. package/dist/components/typing-indicator/typing-indicator.class.js +4 -0
  186. package/dist/components/typing-indicator/typing-indicator.class.js.map +1 -1
  187. package/dist/components/typing-indicator/typing-indicator.styles.d.ts.map +1 -1
  188. package/dist/components/typing-indicator/typing-indicator.styles.js +24 -22
  189. package/dist/components/typing-indicator/typing-indicator.styles.js.map +1 -1
  190. package/dist/components/widget/widget.class.d.ts +5 -0
  191. package/dist/components/widget/widget.class.d.ts.map +1 -1
  192. package/dist/components/widget/widget.class.js +22 -1
  193. package/dist/components/widget/widget.class.js.map +1 -1
  194. package/dist/components/widget/widget.styles.d.ts.map +1 -1
  195. package/dist/components/widget/widget.styles.js +3 -2
  196. package/dist/components/widget/widget.styles.js.map +1 -1
  197. package/dist/internal/localization.d.ts +1 -1
  198. package/dist/internal/localization.d.ts.map +1 -1
  199. package/dist/internal/localization.js +6 -0
  200. package/dist/internal/localization.js.map +1 -1
  201. package/dist/internal/root-registration-allowlist.d.ts +1 -1
  202. package/dist/internal/root-registration-allowlist.d.ts.map +1 -1
  203. package/dist/internal/root-registration-allowlist.js +3 -0
  204. package/dist/internal/root-registration-allowlist.js.map +1 -1
  205. package/dist/internal/tokens.styles.d.ts.map +1 -1
  206. package/dist/internal/tokens.styles.js +6 -2
  207. package/dist/internal/tokens.styles.js.map +1 -1
  208. package/dist/lyra.d.ts +14 -2
  209. package/dist/lyra.d.ts.map +1 -1
  210. package/dist/lyra.js +6 -0
  211. package/dist/lyra.js.map +1 -1
  212. package/llms-full.txt +615 -92
  213. package/llms.txt +5 -3
  214. package/package.json +12 -2
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
 
@@ -579,6 +607,247 @@ and `dateTimeFormat(locale, options)`.
579
607
 
580
608
  ---
581
609
 
610
+ ## lyra-textarea
611
+
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.
616
+
617
+ ```html
618
+ <lyra-textarea placeholder="Notes" rows="4"></lyra-textarea>
619
+ ```
620
+
621
+ ### Properties
622
+
623
+ | Property | Attribute | Type | Default | Description |
624
+ | --- | --- | --- | --- | --- |
625
+ | `value` | `value` | `string` | `''` | The current text value. |
626
+ | `rows` | `rows` | `number` | `3` | Visible text rows. |
627
+ | `resize` | `resize` | `'none' \| 'vertical' \| 'both' \| 'auto'` | `'vertical'` | Native CSS `resize` behavior, plus `'auto'` (`ResizeObserver`-driven grow-to-content, no manual handle). |
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>`. |
640
+ | `name` | `name` | `string` | `''` | Form field name. |
641
+ | `disabled` | `disabled` | `boolean` | `false` | Disables the control. |
642
+ | `required` | `required` | `boolean` | `false` | Participates in native constraint validation. |
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
+
670
+ ### Events
671
+
672
+ | Event | Detail | Description |
673
+ | --- | --- | --- |
674
+ | `lyra-input` | `{ value: string }` | Fired on every user-driven edit. |
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. |
691
+
692
+ ### CSS Parts
693
+
694
+ | Part | Description |
695
+ | --- | --- |
696
+ | `form-control` | The outer wrapper around label, textarea, error and hint. |
697
+ | `form-control-label` | The `<label>` element. |
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.
848
+
849
+ ---
850
+
582
851
  ## `lyra-toast` / `lyra-toast-item` / `toast()`
583
852
 
584
853
  Stacking toast/notification region. Mirrors `<wa-toast>`/`<wa-toast-item>` under `lyra-`.
@@ -866,8 +1135,9 @@ attribute), `description` (rich description content, overrides the `description`
866
1135
 
867
1136
  **CSS parts:** `base`, `icon`, `heading`, `description`, `actions`
868
1137
 
869
- **Themeable custom properties:** shared tokens only (`--lyra-space-xs/-s/-l`,
870
- `--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`).
871
1141
 
872
1142
  **Optional peer deps:** none.
873
1143
 
@@ -876,6 +1146,8 @@ attribute), `description` (rich description content, overrides the `description`
876
1146
  <svg slot="" ...></svg> <!-- default slot: any icon/illustration -->
877
1147
  <div slot="actions"><button>Clear filters</button></div>
878
1148
  </lyra-empty>
1149
+ <lyra-empty compact heading="No results" description="Try a different search."
1150
+ style="--lyra-empty-compact-align: center"></lyra-empty>
879
1151
  ```
880
1152
 
881
1153
  **Known gotchas:**
@@ -1108,18 +1380,94 @@ body's `max-block-size`).
1108
1380
 
1109
1381
  ---
1110
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
+
1111
1457
  ## `lyra-gauge`
1112
1458
 
1113
- 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).
1114
1460
 
1115
1461
  **Properties:**
1116
1462
  - `value: number = 0`
1117
1463
  - `min: number = 0`
1118
1464
  - `max: number = 100`
1119
- - `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)
1120
1467
  - `label: string = ''`
1121
- - `valueLabel?: string` (attribute: false — overrides only the *visible* text, see gotchas; an empty
1122
- 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`)
1123
1471
 
1124
1472
  **Events:** none.
1125
1473
 
@@ -1127,16 +1475,18 @@ Dependency-free SVG radial or linear meter (270°-sweep arc math, no charting li
1127
1475
 
1128
1476
  **CSS parts:** `base` (the `<svg>`), `track`, `fill`, `value`, `label`
1129
1477
 
1130
- **Themeable custom properties:** `--lyra-color-brand` (fill color, shared token no per-instance
1131
- gauge-specific custom property exists).
1478
+ **Themeable custom properties:** `--lyra-gauge-fill` (fill stroke, falling back to the shared
1479
+ `--lyra-color-brand` token).
1132
1480
 
1133
1481
  **Optional peer deps:** none.
1134
1482
 
1135
1483
  ```html
1136
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>
1137
1487
  <lyra-gauge type="linear" value="0.4" max="1"></lyra-gauge>
1138
1488
  <script type="module">
1139
- 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
1140
1490
  </script>
1141
1491
  ```
1142
1492
 
@@ -1145,8 +1495,8 @@ gauge-specific custom property exists).
1145
1495
  changing the visible SVG text), so a screen reader announces your formatted string instead of the
1146
1496
  raw `aria-valuenow` number; the SVG `<text part="value">`/`<text part="label">` elements are
1147
1497
  `aria-hidden="true"` so they're no longer separately exposed inside the same `role="meter"` host.
1148
- - no color-threshold/variant support: a single fixed `--lyra-color-brand` fill regardless
1149
- 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.
1150
1500
  - no documented component-specific sizing custom property; host size is fixed em values
1151
1501
  (`8em`/`12em` radial, `12em`/`1.5em` linear) — resize via plain CSS `width`/`height` on the
1152
1502
  element instead.
@@ -1160,8 +1510,8 @@ gauge-specific custom property exists).
1160
1510
 
1161
1511
  ## `lyra-export-button`
1162
1512
 
1163
- CSV/JSON download button — either single-format (click exports immediately) or multi-format (click
1164
- 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).
1165
1515
 
1166
1516
  **Properties:**
1167
1517
  - `rows: Record<string, unknown>[] = []` (attribute: false)
@@ -1171,21 +1521,27 @@ opens a small menu).
1171
1521
  of CSV degrading to a header-less/blank file while only JSON had a fallback — so an unconfigured
1172
1522
  export still produces a proper header + data file in either format
1173
1523
  - `filename: string = 'export'`
1174
- - `formats: ExportFormat[] = ['csv']` (attribute: false; `ExportFormat = 'csv' | 'json'` **only**
1175
- 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
1176
1528
  - `disabled: boolean = false` (reflected) — also disables every `[part="menu-item"]` button, not just
1177
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
1178
1533
  - `label: string = 'Export'` — trigger button text; also feeds the format-choice menu's `aria-label`
1179
1534
  as `` `${label} format` `` so assistive tech gets an accessible name for the menu
1180
1535
  - `open: boolean = false` (reflected)
1181
1536
 
1182
- **Events:** `lyra-export` (`detail: { format }`, **cancelable** — call `preventDefault()` to
1537
+ **Events:** `lyra-export` (`detail: { format: string }`, **cancelable** — call `preventDefault()` to
1183
1538
  substitute your own server-generated download instead of the built-in client-side one),
1184
- `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)
1185
1541
 
1186
1542
  **Slots:** none.
1187
1543
 
1188
- **CSS parts:** `trigger`, `menu`, `menu-item`
1544
+ **CSS parts:** `trigger`, `menu`, `menu-item`, `format-label`, `format-description`
1189
1545
 
1190
1546
  **Themeable custom properties:** shared tokens only.
1191
1547
 
@@ -1199,6 +1555,19 @@ substitute your own server-generated download instead of the built-in client-sid
1199
1555
  exp.columns = [{ key: 'name', label: 'Name' }, { key: 'value', label: 'Value' }];
1200
1556
  exp.formats = ['csv', 'json']; // shows a format-choice menu instead of exporting immediately
1201
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
+ });
1202
1571
  </script>
1203
1572
  ```
1204
1573
 
@@ -1211,8 +1580,10 @@ downloadBlob(content: string, filename: string, mime: string): void // trig
1211
1580
  ```
1212
1581
 
1213
1582
  **Known gotchas:**
1214
- - XLSX isn't implemented (`ExportFormat` really is `'csv' | 'json'`) don't pass
1215
- `'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.
1216
1587
  - CSV formula-injection guarding and the deferred (5s) `URL.revokeObjectURL` (works around Safari
1217
1588
  cancelling in-flight downloads on immediate revoke) are genuine, safe-to-rely-on strengths.
1218
1589
  - the multi-format menu (`role="menu"`) supports full arrow-key navigation — ArrowUp/ArrowDown move
@@ -1846,36 +2217,48 @@ same color as `--lyra-heatmap-focus-ring-color`).
1846
2217
  A force-directed node-link diagram with pan/zoom/drag, built on `d3-force`.
1847
2218
 
1848
2219
  **Properties:**
1849
- - `nodes: GraphNode[] = []` (attribute: false) — `GraphNode { id: string; label?: string; radius?:
1850
- number; color?: string }`; `radius` is clamped to `[6, 24]` (an unset/non-finite value falls back
1851
- to the midpoint, `15`) so a node can never render invisibly small or absurdly large
1852
- - `links: GraphLink[] = []` (attribute: false) `GraphLink { source: string; target: string;
1853
- width?: number }` (source/target are node ids). A link whose `source` id doesn't resolve to a real
1854
- node is still dropped entirely (there's no position to draw a stub from). A link whose `target` id
1855
- doesn't resolve instead renders as a short, dashed, non-interactive stub off `source`'s own
1856
- position (`[part='link'][data-dangling]`, `aria-hidden="true"`) rather than being silently dropped
1857
- e.g. for a wiki-style `[[link]]` reference to a not-yet-created node. A dangling stub is excluded
1858
- 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.
1859
2237
  - `width: number = 800`
1860
2238
  - `height: number = 600`
1861
2239
  - `chargeStrength: number = -300` (attribute `charge-strength` — live-reactive, see gotchas)
1862
2240
  - `linkDistance: number = 100` (attribute `link-distance` — live-reactive, see gotchas)
1863
2241
  - `minZoom: number = 0.1` (attribute `min-zoom`)
1864
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
1865
2245
  - `seed?: number` — when set, seeds each node's initial x/y deterministically
1866
2246
  (keyed by node **id**, not array index/order) instead of `forceSimulation()`'s own random start,
1867
2247
  and settles the simulation synchronously instead of animating the settle (same effect
1868
2248
  `prefers-reduced-motion` has, see gotchas)
1869
2249
 
1870
- **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)
1871
2252
 
1872
2253
  **Slots:** none.
1873
2254
 
1874
- **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`
1875
2257
 
1876
2258
  **Themeable custom properties:** `--lyra-node-fill` (set inline per-node from `GraphNode.color`,
1877
- falls back to `--lyra-color-brand`); also uses `--lyra-color-border` (link stroke), `--lyra-color-text`
1878
- + `--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).
1879
2262
 
1880
2263
  **Optional peer deps:** `d3-force`, `d3-drag`, `d3-zoom`, `d3-selection` (all four required
1881
2264
  together; lazy-`import()`ed once per page, `console.warn` once and renders empty if missing —
@@ -1885,9 +2268,17 @@ install with `pnpm add d3-force d3-drag d3-zoom d3-selection`).
1885
2268
  <lyra-graph style="display:block;height:500px"></lyra-graph>
1886
2269
  <script>
1887
2270
  const g = document.querySelector('lyra-graph');
1888
- g.nodes = [{ id: 'a', label: 'A' }, { id: 'b', label: 'B' }];
1889
- 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
+ }];
1890
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));
1891
2282
  </script>
1892
2283
  ```
1893
2284
 
@@ -1916,6 +2307,9 @@ install with `pnpm add d3-force d3-drag d3-zoom d3-selection`).
1916
2307
  - `GraphNode.color` is sanitized (rejects `;`/`{`/`}`) before being written into the
1917
2308
  `--lyra-node-fill` inline custom property, so an untrusted color string can't break out of that
1918
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.
1919
2313
  - a structural `nodes`/`links` change now carries over each already-settled node's position (and any
1920
2314
  in-progress drag) by id when rebuilding the simulation, instead of discarding every node's (x, y)
1921
2315
  and re-running the whole ~300-tick random-start settle from scratch — only genuinely new ids get a
@@ -1946,10 +2340,13 @@ deeply-nested node's own shadow root still reaches it).
1946
2340
 
1947
2341
  **Properties:**
1948
2342
  - `data: TreeItem[] = []` (attribute: false) — `TreeItem { id: string; label: string; children?:
1949
- 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
1950
2346
  - `label: string = ''` — accessible name for the tree; `role="tree"` lives on an internal
1951
- `[part="base"]` element, not the host, so this is the only way to label it (`aria-label`/
1952
- `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.
1953
2350
 
1954
2351
  **Keyboard:** ArrowDown/ArrowUp move the roving focus to the next/previous *visible* node.
1955
2352
  ArrowRight expands a collapsed node (focus stays put; a second ArrowRight then steps into the first
@@ -1965,7 +2362,7 @@ observed internally to keep the roving `activeId` in sync with clicks).
1965
2362
 
1966
2363
  **Slots:** default (holds the `<lyra-tree-node>` elements it manages).
1967
2364
 
1968
- **CSS parts:** `base`
2365
+ **CSS parts:** `base`, `empty` (the empty-state message shown when `data` is empty)
1969
2366
 
1970
2367
  **Themeable custom properties:** shared tokens `--lyra-space-xs`/`-s`, `--lyra-color-brand-quiet`,
1971
2368
  `--lyra-color-text-quiet`, `--lyra-color-border`, `--lyra-color-text`, `--lyra-radius`,
@@ -1997,12 +2394,15 @@ descendants of the treeitem, matching the WAI-ARIA treeitem pattern's containmen
1997
2394
 
1998
2395
  **Events:** `lyra-node-toggle` (`detail: { id, expanded }`, fired by `expand()`/`collapse()` — via
1999
2396
  the toggle button or ArrowRight/ArrowLeft), `lyra-node-select` (`detail: { id }`, fired by `select()`
2000
- — 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
2001
2399
  through `lyra-tree`'s light DOM.
2002
2400
 
2003
2401
  **Slots:** none.
2004
2402
 
2005
- **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.
2006
2406
 
2007
2407
  **Themeable custom properties:** `--lyra-tree-depth` (internal, set inline per row for
2008
2408
  indentation), plus the shared tokens listed above.
@@ -2013,7 +2413,14 @@ indentation), plus the shared tokens listed above.
2013
2413
  <lyra-tree></lyra-tree>
2014
2414
  <script>
2015
2415
  document.querySelector('lyra-tree').data = [
2016
- { 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
+ },
2017
2424
  ];
2018
2425
  </script>
2019
2426
  ```
@@ -2030,6 +2437,9 @@ indentation), plus the shared tokens listed above.
2030
2437
  tree's `updateComplete` (e.g. after `focusNode()`) doesn't run before an arbitrarily-nested node has
2031
2438
  actually finished rendering its pushed-down `activeId`/`tabIndex` — one more pending update per
2032
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.
2033
2443
 
2034
2444
  ---
2035
2445
 
@@ -2409,6 +2819,10 @@ raw `map` escape hatch for anything unexposed.
2409
2819
  escaped) when the content is plain text. A marker whose `color` changes for a persisting `id`
2410
2820
  can't be recolored in place (no `Marker.setColor()`) and is torn down/reconstructed instead — see
2411
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.
2412
2826
 
2413
2827
  **Getters:** `map` → the raw `maplibregl.Map` instance.
2414
2828
 
@@ -2576,6 +2990,11 @@ hand-building that chrome isn't worth it.
2576
2990
  header row (creating one, with no heading text, if `heading` is unset), wired to the same
2577
2991
  `close()` path Escape/backdrop-dismiss already use, with reason `'close-button'`
2578
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
+
2579
2998
  **Methods:** `close(reason: DialogCloseReason = 'api'): void` — closes the dialog, emits
2580
2999
  `lyra-dialog-close` with `reason`, and returns focus to whatever had it right before the dialog
2581
3000
  opened. `DialogCloseReason = 'escape' | 'backdrop' | 'close-button' | 'api' | 'unmount' | string` —
@@ -2635,7 +3054,12 @@ clipped; the viewport is still a hard limit either way), plus shared tokens `--l
2635
3054
  </script>
2636
3055
  ```
2637
3056
 
2638
- 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
2639
3063
  `[role="heading"]`) is a *direct child* (not inside `slot="footer"`), its text content becomes
2640
3064
  `aria-label` on the panel — takes priority over `heading` below so an existing consumer that already
2641
3065
  slots its own heading keeps rendering it exactly as before; (2) otherwise, when `heading` is set, a
@@ -2850,8 +3274,11 @@ labeled options, rendered as a button row" is ubiquitous settings/filter-panel U
2850
3274
 
2851
3275
  **Properties:**
2852
3276
  - `items: SegmentedItem[] = []` (attribute: false) — `SegmentedItem { value: string; label: string;
2853
- 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.
2854
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.
2855
3282
 
2856
3283
  **Events:** `lyra-change` (`detail: { value }`) — fired when the selected value changes via click or
2857
3284
  keyboard.
@@ -2859,7 +3286,7 @@ keyboard.
2859
3286
  **Slots:** none.
2860
3287
 
2861
3288
  **CSS parts:** `base` (the `role="radiogroup"` root), `segment` (a single `role="radio"` button),
2862
- `segment-label` (the segment's label text).
3289
+ `segment-icon` (an optional decorative leading icon), `segment-label` (the segment's label text).
2863
3290
 
2864
3291
  **Themeable custom properties:** shared tokens only — `--lyra-color-border`/`-surface`/`-text`/
2865
3292
  `-text-quiet`, `--lyra-radius`, `--lyra-font-size-sm`, `--lyra-font-weight-semibold`,
@@ -2872,8 +3299,8 @@ keyboard.
2872
3299
  <script type="module">
2873
3300
  const seg = document.querySelector('lyra-segmented');
2874
3301
  seg.items = [
2875
- { value: 'day', label: 'Day' },
2876
- { value: 'week', label: 'Week' },
3302
+ { value: 'day', label: 'Day', icon: '☀' },
3303
+ { value: 'week', label: 'Week', icon: '▦' },
2877
3304
  { value: 'month', label: 'Month' },
2878
3305
  ];
2879
3306
  seg.value = 'week';
@@ -2887,6 +3314,8 @@ keyboard.
2887
3314
  - this component self-selects on navigation: clicking or arrow-navigating to an item immediately
2888
3315
  updates `value` and fires `lyra-change` — there's no separate "commit" step the way, e.g.,
2889
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.
2890
3319
 
2891
3320
  ---
2892
3321
 
@@ -2960,6 +3389,10 @@ empty string. Restoration updates state, form data, and validity synchronously w
2960
3389
  A boolean toggle-switch form control. `role="switch"` with `aria-checked` read as an on/off state
2961
3390
  rather than checked/unchecked, and no indeterminate state. Structurally the same idea as
2962
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>`).
2963
3396
 
2964
3397
  **Properties:**
2965
3398
  - `checked: boolean = false` (reflected)
@@ -2967,17 +3400,24 @@ rather than checked/unchecked, and no indeterminate state. Structurally the same
2967
3400
  - `required: boolean = false` (reflected — enforced via `internals.setValidity()`)
2968
3401
  - `name: string = ''`
2969
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.
2970
3406
 
2971
3407
  **Events:** `lyra-change` (`detail: { checked: boolean }`) — fired on a user toggle (click or
2972
3408
  Space/Enter); not fired for a programmatic `.checked` assignment.
2973
3409
 
2974
- **Slots:** default — label text, rendered next to the track. Clicking it toggles the switch, the
2975
- same as clicking a checkbox's associated `<label>`. If left empty, set `aria-label` on the host so
2976
- 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.
2977
3416
 
2978
- **CSS parts:** `base` (the whole interactive control, `role="switch"`), `track` (the pill-shaped
2979
- background), `thumb` (the circular knob that slides across the track), `label` (wrapper around the
2980
- 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)
2981
3421
 
2982
3422
  **Themeable custom properties:** `--lyra-switch-track-inline-size` (default `2.25rem`),
2983
3423
  `--lyra-switch-track-block-size` (default `1.25rem`), `--lyra-switch-thumb-offset` (default
@@ -3346,6 +3786,10 @@ inline size; component-specific, no shared width token exists), plus shared toke
3346
3786
  `--lyra-color-brand`, `--lyra-color-text-quiet`, `--lyra-color-danger`, `--lyra-color-danger-quiet`,
3347
3787
  `--lyra-radius`, `--lyra-icon-button-size`, `--lyra-focus-ring-*`, `--lyra-transition-fast`.
3348
3788
 
3789
+ `[part=bubble]`'s background resolves through `--lyra-color-surface`. If your own panel/container
3790
+ background already maps to that same token, override `[part=bubble]`'s background explicitly (e.g.
3791
+ via `::part(bubble)`) so message bubbles stay visually distinct from the surrounding panel.
3792
+
3349
3793
  **Optional peer deps:** none. Internally renders a `<lyra-live-region>` (a first-party sibling
3350
3794
  component, auto-imported alongside this one, not an npm peer) for the status-transition
3351
3795
  announcements described below.
@@ -3418,10 +3862,11 @@ variant), `cursor` (the blinking bar in the `cursor` variant)
3418
3862
  **Themeable custom properties:** `--lyra-typing-dot-size` (default `0.5rem`, `0.375rem` at
3419
3863
  `size="sm"`), `--lyra-typing-gap` (default `0.25rem`, `0.1875rem` at `size="sm"`),
3420
3864
  `--lyra-typing-cursor-width` (default `0.125rem`, `0.09375rem` at `size="sm"`),
3421
- `--lyra-typing-cursor-height` (default `1em`, unaffected by `size`) — all component-specific; plus
3422
- the shared `--lyra-transition-base` token, used as each variant's full `animation:` shorthand
3423
- (duration + timing-function together) since it drives a continuous ambient loop rather than a
3424
- 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.
3425
3870
 
3426
3871
  **Optional peer deps:** none.
3427
3872
 
@@ -3429,6 +3874,9 @@ discrete state flip (which would instead use `--lyra-transition-fast`).
3429
3874
  <lyra-typing-indicator label="Assistant is responding…"></lyra-typing-indicator>
3430
3875
  <lyra-typing-indicator variant="pulse" size="sm"></lyra-typing-indicator>
3431
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>
3432
3880
  ```
3433
3881
 
3434
3882
  Accessibility: since this indicator typically mounts and unmounts around a real generation lifecycle
@@ -3446,11 +3894,9 @@ decorative; `label` is the entire accessible content, nothing narrates individua
3446
3894
  state (`opacity: 1`, no transform, `animation: none !important`) rather than freezing on whatever
3447
3895
  frame the animation happened to be on — notably relevant for `cursor`, which would otherwise risk
3448
3896
  freezing on its invisible ("off") blink half.
3449
- - the two dot-bounce stagger delays (`60ms`, `120ms`) are fixed literals, not `calc()`-derived
3450
- fractions of `--lyra-transition-base` that token is a *compound* `duration timing-function` value
3451
- (like every `transition:`/`animation:` shorthand token in this library), so it can't be decomposed
3452
- arithmetically; the two delays just approximate a third and two-thirds of the token's own default
3453
- 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.
3454
3900
  - `size="sm"` shrinks the dot size, gap, and cursor width, but **not** `--lyra-typing-cursor-height`
3455
3901
  (still `1em` at any size) — the cursor bar's height is meant to track surrounding text size via
3456
3902
  `1em`, not the component's own `size` property.
@@ -3775,7 +4221,9 @@ state, so no additional scroll-lock/focus-trap bookkeeping is needed for that tr
3775
4221
  ## `lyra-chat-composer`
3776
4222
 
3777
4223
  The message input for a chat/agent conversation surface: an auto-resizing `<textarea>` plus a
3778
- 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
3779
4227
  `<lyra-date-input>`) — `name: string = ''`, `value: string = ''`, `disabled: boolean = false`
3780
4228
  (reflected), `required: boolean = false` (reflected) are all inherited, along with
3781
4229
  `checkValidity()`/`reportValidity()`, so it participates in native `<form>` submission/validation/
@@ -3794,6 +4242,12 @@ reveals the invalid state, and `form.reset()` clears the touched presentation.
3794
4242
  built-in button's icon/label (send vs. stop) and whether Enter still submits
3795
4243
  - `submitOnEnter: boolean = true` (reflected, attribute `submit-on-enter`) — when `false`, Enter
3796
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`.
3797
4251
 
3798
4252
  **Methods (own):** none beyond the inherited `checkValidity()`/`reportValidity()`.
3799
4253
 
@@ -3805,6 +4259,10 @@ reveals the invalid state, and `form.reset()` clears the touched presentation.
3805
4259
  trimming is left to the consumer. Submitting does **not** clear `value`
3806
4260
  - `lyra-stop` (no detail) — fired by the built-in button while `status` is `"sending"` or
3807
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`
3808
4266
 
3809
4267
  **Slots:** `leading` (content before the textarea, e.g. an attach-file trigger button), `chips` (an
3810
4268
  attachment tray rendered above the input row), `trailing` (overrides the built-in send/stop button
@@ -4295,6 +4753,12 @@ First-party invention (no Web Awesome equivalent).
4295
4753
  rename button never renders and the row can never enter its editing state; flipping it to `false`
4296
4754
  while a rename is already in progress cancels that edit (discards the draft, like Escape) rather
4297
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.
4298
4762
 
4299
4763
  **Events:** `lyra-select` (no detail payload — identify the row via the platform `id` attribute on the
4300
4764
  event's `target`/`currentTarget`, the same convention `<lyra-attachment-chip>` uses; fires on a click on
@@ -4302,7 +4766,9 @@ event's `target`/`currentTarget`, the same convention `<lyra-attachment-chip>` u
4302
4766
  while not currently renaming), `lyra-rename` (`detail: { title: string }` — an in-place rename was
4303
4767
  committed via Enter or blur-while-editing; does not mutate `title` itself, this is a controlled
4304
4768
  component — not fired when the trimmed draft is empty or unchanged from the original `title`, treated
4305
- 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`)
4306
4772
 
4307
4773
  **Slots:** `actions` — overflow/icon-button controls rendered at the trailing edge of the row (e.g. a
4308
4774
  pin/delete control); only visually shown once it actually has assigned elements.
@@ -4385,7 +4851,20 @@ emitting `lyra-change`.
4385
4851
  id, and once non-empty it takes over as the accessible-name source (an `aria-label` override is
4386
4852
  then only consulted as the fallback). Empty (the default) keeps the original
4387
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.
4388
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`.
4389
4868
  - `name: string = ''` (reflected)
4390
4869
  - `disabled: boolean = false` (reflected)
4391
4870
  - `required: boolean = false` (reflected — enforced via `internals.setValidity()`)
@@ -4408,12 +4887,18 @@ since changed) is never silently dropped: it's appended to the rendered option l
4408
4887
  visually-distinct row (dashed border, italic label, "not in catalog" badge) computed fresh from
4409
4888
  `catalog` + `value` on every access, without ever mutating the `catalog` property itself.
4410
4889
 
4411
- **Events:** `lyra-change` (`detail: { value: string; inCatalog: boolean }` — fired when a value is
4412
- selected from the listbox or committed in free-text mode; `inCatalog` reflects whether that value was
4413
- actually present in `normalizedCatalog`, so a consumer can flag a freshly-typed custom value distinctly
4414
- from a real catalog pick)
4415
-
4416
- **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).
4417
4902
 
4418
4903
  **CSS parts:** `form-control-label` (the `<label>` element — only rendered, and only contributes to
4419
4904
  the accessible name, once `label` is non-empty), `trigger` (closed-dropdown mode's
@@ -4421,7 +4906,8 @@ the accessible name, once `label` is non-empty), `trigger` (closed-dropdown mode
4421
4906
  container, also its positioning anchor), `combobox-input` (the free-text `<input>`),
4422
4907
  `provider-badge` (the optional leading `provider` label), `listbox` (the options popover, shared by
4423
4908
  both modes), `option`, `option-label`, `option-badge` (the "not in catalog" badge on a synthetic
4424
- 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)
4425
4911
 
4426
4912
  **Themeable custom properties:** shared tokens only — `--lyra-space-xs/-s`,
4427
4913
  `--lyra-color-border/-surface/-brand/-brand-quiet/-text-quiet`, `--lyra-radius`, `--lyra-shadow`,
@@ -4458,6 +4944,9 @@ stale-value row), `expand-icon` (the dropdown chevron, present in both modes)
4458
4944
  - In free-text mode, the input's displayed text is `query` only while `open`; while closed it shows the
4459
4945
  committed value's label — so setting `.value` programmatically doesn't require also touching the live
4460
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.
4461
4950
 
4462
4951
  ---
4463
4952
 
@@ -4897,7 +5386,8 @@ accessor itself, which still takes full priority.
4897
5386
  no overlay chrome renders until `mode` is `'mobile'` again. Set this directly, or use the built-in
4898
5387
  toggle button — there is no separate `show()`/`hide()` pair.
4899
5388
  - `label: string = 'Navigation'` — accessible name for the rail's navigation landmark, and for its
4900
- 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.
4901
5391
  - `preferredMode?: 'full' | 'icon-only' | null` (attribute `preferred-mode`) — manually prefers
4902
5392
  `'full'` or `'icon-only'` for the non-mobile breakpoint axis, while `mobile-breakpoint` continues to
4903
5393
  be tracked automatically regardless — e.g. a user's manual collapse toggle that should still yield
@@ -4926,6 +5416,10 @@ accessor itself, which still takes full priority.
4926
5416
  "chases" the pointer instead of tracking it 1:1. Effectively read-only (this component owns the
4927
5417
  transitions), but a plain reflected property like every other boolean here.
4928
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
+
4929
5423
  **Events:** `lyra-mode-change` (`detail: AppRailModeChangeDetail` = `{ mode: AppRailMode }`; the
4930
5424
  effective mode changed, whether from a breakpoint crossing or an explicit `mode` assignment — not
4931
5425
  fired for a redundant reassignment to the mode already in effect), `lyra-toggle`
@@ -5177,7 +5671,10 @@ own listbox.
5177
5671
  - `filter: MentionFilter | null = null` (attribute: false) — overrides the built-in
5178
5672
  case-insensitive `label`/`description` substring match entirely.
5179
5673
  - `emptyText: string = 'No matches'` (attribute `empty-text`)
5180
- - `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>`.
5181
5678
  - `filteredItems: MentionItem[]` — read-only getter; `items` filtered by `query` via `filter` (or
5182
5679
  the built-in default). Empty `query` returns `items` unfiltered.
5183
5680
  - `activeDescendantId: string | null` — read-only getter; the `id` of the currently-highlighted
@@ -5260,6 +5757,9 @@ automatically only on an `anchor` or `query` change while open (a keystroke move
5260
5757
  fresh `query` is the proxy for "the caret may have moved").
5261
5758
 
5262
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>`.
5263
5763
  - The popover opens pre-highlighted on the top match (index 0), unlike `<lyra-combobox>`'s own
5264
5764
  listbox which opens with nothing highlighted (`-1`) — a bare Enter right after opening commits
5265
5765
  immediately.
@@ -5880,6 +6380,12 @@ shared composed-tree focus traversal used by the other modal families.
5880
6380
  disconnect/reconnect fire back-to-back with no update in between.
5881
6381
  - the Approve button's native `disabled` attribute (while the draft is invalid JSON) automatically
5882
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.
5883
6389
 
5884
6390
  ---
5885
6391
 
@@ -6039,7 +6545,10 @@ menu, an avatar menu, or a history row's overflow menu. Uses the WAI-ARIA "menu
6039
6545
  side is mirrored under `dir="rtl"`, so e.g. `placement="left-start"` still anchors to the menu's
6040
6546
  trailing edge instead of pinning to the physical left)
6041
6547
  - `label: string = 'Menu'` — accessible name for the `role="menu"` popup; override with something
6042
- 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
6043
6552
 
6044
6553
  **Events:** `lyra-show` (no detail — fires only when `open` transitions to `true`, not for markup
6045
6554
  that renders `open` true from the start), `lyra-hide` (same first-render guard, opposite
@@ -6175,7 +6684,8 @@ already follow.
6175
6684
  - `removable: boolean = false` (reflected — shows the remove (×) button)
6176
6685
  - `selected: boolean = false` (reflected) — opt-in toggle/pressed mode: when set, `[part='base']`
6177
6686
  itself becomes focusable and keyboard-activatable (Enter/Space, mirroring native `<button>`
6178
- 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`.
6179
6689
  Has no effect (no interactive semantics added to `[part='base']`) when combined with `removable`,
6180
6690
  since the remove button already nests inside `[part='base']` — a focusable descendant of a
6181
6691
  `role="button"` ancestor isn't allowed there, and this component's two real use cases (a
@@ -6202,13 +6712,26 @@ affordance, only rendered while `removable`)
6202
6712
  (component-local trio swapped per `tone` rather than repeating background/color/border per part per
6203
6713
  tone; default `var(--lyra-color-text)` / `var(--lyra-color-surface)` / `var(--lyra-color-border)` —
6204
6714
  mirrors the same accent/bg/border vocabulary `<lyra-tool-call-chip>`/`<lyra-attachment-chip>` use),
6205
- plus shared tokens (`--lyra-space-xs`, `--lyra-space-s`, `--lyra-color-brand`/`-brand-quiet`,
6206
- `--lyra-color-success`/`-success-quiet`, `--lyra-color-warning`/`-warning-quiet`,
6207
- `--lyra-color-danger`/`-danger-quiet`, `--lyra-icon-button-size`, `--lyra-focus-ring-width`,
6208
- `--lyra-focus-ring-color`, `--lyra-focus-ring-offset`, `--lyra-transition-fast`).
6715
+ `--lyra-chip-pressed-border` (border color while pressed/selected — falls back to
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`,
6718
+ `--lyra-color-brand`/`-brand-quiet`, `--lyra-color-success`/`-success-quiet`,
6719
+ `--lyra-color-warning`/`-warning-quiet`, `--lyra-color-danger`/`-danger-quiet`,
6720
+ `--lyra-icon-button-size`, `--lyra-focus-ring-width`, `--lyra-focus-ring-color`,
6721
+ `--lyra-focus-ring-offset`, `--lyra-transition-fast`).
6209
6722
 
6210
6723
  **Optional peer deps:** none.
6211
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
+
6212
6735
  ### `lyra-chip-group`
6213
6736
 
6214
6737
  A flex-wrap container for a set of `<lyra-chip>` children — plain light-DOM composition, direct