@aceshooting/lyra-ui 2.0.0 → 2.1.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 (224) hide show
  1. package/README.md +87 -14
  2. package/custom-elements.json +11555 -5539
  3. package/dist/components/app-rail/app-rail-item.d.ts +30 -0
  4. package/dist/components/app-rail/app-rail-item.js +68 -0
  5. package/dist/components/app-rail/app-rail-item.styles.d.ts +1 -0
  6. package/dist/components/app-rail/app-rail-item.styles.js +65 -0
  7. package/dist/components/app-rail/app-rail.d.ts +21 -24
  8. package/dist/components/app-rail/app-rail.js +68 -132
  9. package/dist/components/app-rail/app-rail.styles.js +5 -2
  10. package/dist/components/attachment-chip/attachment-chip.d.ts +38 -11
  11. package/dist/components/attachment-chip/attachment-chip.js +73 -29
  12. package/dist/components/attachment-trigger/attachment-trigger.d.ts +13 -0
  13. package/dist/components/attachment-trigger/attachment-trigger.js +13 -1
  14. package/dist/components/attachment-trigger/attachment-trigger.styles.js +28 -6
  15. package/dist/components/chart/bar-chart.js +2 -18
  16. package/dist/components/chart/box-plot.d.ts +25 -1
  17. package/dist/components/chart/box-plot.js +136 -6
  18. package/dist/components/chart/box-plot.styles.js +13 -0
  19. package/dist/components/chart/bubble-chart.js +2 -18
  20. package/dist/components/chart/chart.d.ts +54 -10
  21. package/dist/components/chart/chart.js +182 -39
  22. package/dist/components/chart/doughnut-chart.js +2 -18
  23. package/dist/components/chart/histogram-bin.d.ts +5 -2
  24. package/dist/components/chart/histogram-bin.js +12 -4
  25. package/dist/components/chart/histogram.js +8 -16
  26. package/dist/components/chart/line-chart.js +2 -18
  27. package/dist/components/chart/lite-chart.d.ts +80 -3
  28. package/dist/components/chart/lite-chart.js +250 -53
  29. package/dist/components/chart/pie-chart.js +2 -18
  30. package/dist/components/chart/polar-area-chart.js +2 -18
  31. package/dist/components/chart/radar-chart.js +2 -18
  32. package/dist/components/chart/scatter-chart.js +2 -18
  33. package/dist/components/chat-composer/chat-composer.d.ts +42 -1
  34. package/dist/components/chat-composer/chat-composer.js +84 -3
  35. package/dist/components/chat-composer/chat-composer.styles.js +11 -1
  36. package/dist/components/chat-message/chat-message.d.ts +32 -4
  37. package/dist/components/chat-message/chat-message.js +57 -17
  38. package/dist/components/checkbox/checkbox.d.ts +66 -8
  39. package/dist/components/checkbox/checkbox.js +173 -50
  40. package/dist/components/checkbox/checkbox.styles.js +9 -2
  41. package/dist/components/chip/chip-group.js +8 -2
  42. package/dist/components/chip/chip.js +9 -2
  43. package/dist/components/citation-badge/citation-badge.js +21 -3
  44. package/dist/components/code-block/code-block.d.ts +38 -0
  45. package/dist/components/code-block/code-block.js +72 -7
  46. package/dist/components/code-block/code-loader.d.ts +44 -1
  47. package/dist/components/code-block/code-loader.js +63 -0
  48. package/dist/components/combobox/combobox.d.ts +46 -5
  49. package/dist/components/combobox/combobox.js +175 -35
  50. package/dist/components/combobox/combobox.styles.js +3 -3
  51. package/dist/components/context-meter/context-meter.js +17 -8
  52. package/dist/components/context-meter/context-meter.styles.js +0 -6
  53. package/dist/components/conversation-item/conversation-item.d.ts +13 -16
  54. package/dist/components/conversation-item/conversation-item.js +29 -20
  55. package/dist/components/date-picker/calendar-core.d.ts +24 -0
  56. package/dist/components/date-picker/calendar-core.js +51 -4
  57. package/dist/components/date-picker/date-input.d.ts +77 -10
  58. package/dist/components/date-picker/date-input.js +278 -73
  59. package/dist/components/date-picker/date-picker.d.ts +28 -4
  60. package/dist/components/date-picker/date-picker.js +95 -37
  61. package/dist/components/dialog/confirm.js +3 -6
  62. package/dist/components/dialog/dialog.d.ts +75 -31
  63. package/dist/components/dialog/dialog.js +147 -131
  64. package/dist/components/dialog/dialog.styles.js +46 -2
  65. package/dist/components/dock-panel/dock-panel.d.ts +8 -0
  66. package/dist/components/dock-panel/dock-panel.js +15 -1
  67. package/dist/components/dock-panel/dock-panel.styles.js +15 -1
  68. package/dist/components/document-preview/document-preview.d.ts +15 -7
  69. package/dist/components/document-preview/document-preview.js +29 -13
  70. package/dist/components/empty/empty.d.ts +1 -2
  71. package/dist/components/empty/empty.js +18 -12
  72. package/dist/components/empty/empty.styles.js +0 -3
  73. package/dist/components/export-button/export-button.d.ts +12 -0
  74. package/dist/components/export-button/export-button.js +33 -5
  75. package/dist/components/export-button/export-button.styles.js +5 -1
  76. package/dist/components/file-input/accept.js +2 -0
  77. package/dist/components/file-input/file-input.d.ts +10 -1
  78. package/dist/components/file-input/file-input.js +36 -5
  79. package/dist/components/flag/flag.d.ts +1 -1
  80. package/dist/components/flag/flag.js +15 -1
  81. package/dist/components/flag/flag.styles.js +1 -1
  82. package/dist/components/flag/language-map.d.ts +4 -1
  83. package/dist/components/flag/language-map.js +6 -3
  84. package/dist/components/gauge/gauge.d.ts +10 -1
  85. package/dist/components/gauge/gauge.js +28 -9
  86. package/dist/components/generation-status/generation-status.d.ts +13 -5
  87. package/dist/components/generation-status/generation-status.js +59 -5
  88. package/dist/components/graph/graph.d.ts +37 -3
  89. package/dist/components/graph/graph.js +132 -20
  90. package/dist/components/heatmap/calendar-grid.d.ts +15 -6
  91. package/dist/components/heatmap/calendar-grid.js +17 -6
  92. package/dist/components/heatmap/heatmap-scale.d.ts +5 -1
  93. package/dist/components/heatmap/heatmap-scale.js +8 -2
  94. package/dist/components/heatmap/heatmap.d.ts +166 -25
  95. package/dist/components/heatmap/heatmap.js +372 -89
  96. package/dist/components/json-viewer/json-viewer.d.ts +22 -5
  97. package/dist/components/json-viewer/json-viewer.js +115 -37
  98. package/dist/components/json-viewer/json-viewer.styles.js +2 -1
  99. package/dist/components/kbd/kbd.js +24 -7
  100. package/dist/components/live-region/live-region.d.ts +10 -1
  101. package/dist/components/live-region/live-region.js +42 -6
  102. package/dist/components/map/map.d.ts +24 -1
  103. package/dist/components/map/map.js +89 -24
  104. package/dist/components/markdown/markdown-loader.d.ts +24 -0
  105. package/dist/components/markdown/markdown-loader.js +35 -1
  106. package/dist/components/markdown/markdown.d.ts +46 -8
  107. package/dist/components/markdown/markdown.js +136 -31
  108. package/dist/components/markdown/markdown.styles.js +7 -1
  109. package/dist/components/media-card/media-card.d.ts +25 -17
  110. package/dist/components/media-card/media-card.js +43 -65
  111. package/dist/components/mention-popover/mention-popover.d.ts +4 -1
  112. package/dist/components/mention-popover/mention-popover.js +23 -5
  113. package/dist/components/menu/menu-item.d.ts +32 -3
  114. package/dist/components/menu/menu-item.js +94 -7
  115. package/dist/components/menu/menu-item.styles.js +8 -0
  116. package/dist/components/menu/menu.d.ts +25 -1
  117. package/dist/components/menu/menu.js +99 -3
  118. package/dist/components/menu/menu.styles.js +3 -2
  119. package/dist/components/model-select/model-select.d.ts +48 -3
  120. package/dist/components/model-select/model-select.js +120 -22
  121. package/dist/components/model-select/model-select.styles.js +17 -3
  122. package/dist/components/model-settings-panel/model-settings-panel.d.ts +25 -1
  123. package/dist/components/model-settings-panel/model-settings-panel.js +65 -1
  124. package/dist/components/playback/playback.d.ts +6 -2
  125. package/dist/components/playback/playback.js +72 -29
  126. package/dist/components/playback/playback.styles.js +6 -1
  127. package/dist/components/responsive-panel/responsive-panel.d.ts +29 -16
  128. package/dist/components/responsive-panel/responsive-panel.js +130 -132
  129. package/dist/components/responsive-panel/responsive-panel.styles.js +1 -1
  130. package/dist/components/result-card/result-card.d.ts +8 -2
  131. package/dist/components/result-card/result-card.js +9 -3
  132. package/dist/components/result-card/result-card.styles.js +0 -1
  133. package/dist/components/select/select.d.ts +59 -20
  134. package/dist/components/select/select.js +125 -43
  135. package/dist/components/skeleton/skeleton.d.ts +3 -0
  136. package/dist/components/skeleton/skeleton.js +7 -1
  137. package/dist/components/skeleton/skeleton.styles.js +5 -4
  138. package/dist/components/slider/slider.d.ts +57 -5
  139. package/dist/components/slider/slider.js +195 -80
  140. package/dist/components/source-card/source-card.d.ts +17 -5
  141. package/dist/components/source-card/source-card.js +55 -8
  142. package/dist/components/source-card/source-card.styles.js +1 -1
  143. package/dist/components/source-list/source-list.js +9 -1
  144. package/dist/components/sparkline/sparkline.js +34 -10
  145. package/dist/components/split/split.d.ts +141 -14
  146. package/dist/components/split/split.js +357 -52
  147. package/dist/components/split/split.styles.js +18 -1
  148. package/dist/components/stat/stat.d.ts +10 -4
  149. package/dist/components/stat/stat.js +46 -11
  150. package/dist/components/stream-status/stream-status.d.ts +27 -11
  151. package/dist/components/stream-status/stream-status.js +95 -13
  152. package/dist/components/streaming-text/streaming-text.d.ts +9 -3
  153. package/dist/components/streaming-text/streaming-text.js +25 -10
  154. package/dist/components/streaming-text/streaming-text.styles.js +3 -2
  155. package/dist/components/switch/switch.d.ts +52 -7
  156. package/dist/components/switch/switch.js +111 -40
  157. package/dist/components/switch/switch.styles.js +1 -1
  158. package/dist/components/table/table.d.ts +18 -3
  159. package/dist/components/table/table.js +88 -20
  160. package/dist/components/tabs/tabs.d.ts +23 -0
  161. package/dist/components/tabs/tabs.js +76 -12
  162. package/dist/components/tabs/tabs.styles.js +11 -0
  163. package/dist/components/thinking-panel/thinking-panel.d.ts +3 -1
  164. package/dist/components/thinking-panel/thinking-panel.js +34 -9
  165. package/dist/components/thinking-panel/thinking-panel.styles.js +6 -0
  166. package/dist/components/time-range/time-range.d.ts +59 -2
  167. package/dist/components/time-range/time-range.js +163 -67
  168. package/dist/components/toast/toast-item.d.ts +12 -3
  169. package/dist/components/toast/toast-item.js +90 -53
  170. package/dist/components/toast/toast-item.styles.js +59 -28
  171. package/dist/components/toast/toast.js +17 -5
  172. package/dist/components/toast/toaster.js +10 -1
  173. package/dist/components/tool-approval-dialog/tool-approval-dialog.d.ts +11 -10
  174. package/dist/components/tool-approval-dialog/tool-approval-dialog.js +50 -124
  175. package/dist/components/tool-approval-dialog/tool-approval-dialog.styles.js +1 -1
  176. package/dist/components/tool-call-chip/tool-call-chip.d.ts +18 -2
  177. package/dist/components/tool-call-chip/tool-call-chip.js +86 -25
  178. package/dist/components/tool-param-form/tool-param-form.d.ts +89 -28
  179. package/dist/components/tool-param-form/tool-param-form.js +350 -106
  180. package/dist/components/tool-result-dialog/tool-result-dialog.d.ts +27 -7
  181. package/dist/components/tool-result-dialog/tool-result-dialog.js +80 -115
  182. package/dist/components/tool-result-dialog/tool-result-dialog.styles.js +1 -1
  183. package/dist/components/tool-result-view/registry.d.ts +0 -8
  184. package/dist/components/tool-result-view/registry.js +7 -2
  185. package/dist/components/tool-result-view/tool-result-view.d.ts +6 -3
  186. package/dist/components/tool-result-view/tool-result-view.js +18 -4
  187. package/dist/components/tool-select-dialog/tool-select-dialog.d.ts +11 -11
  188. package/dist/components/tool-select-dialog/tool-select-dialog.js +44 -133
  189. package/dist/components/tool-select-dialog/tool-select-dialog.styles.js +13 -4
  190. package/dist/components/tree/tree-node.d.ts +4 -1
  191. package/dist/components/tree/tree-node.js +4 -1
  192. package/dist/components/tree/tree.d.ts +17 -8
  193. package/dist/components/tree/tree.js +21 -20
  194. package/dist/components/typing-indicator/typing-indicator.d.ts +12 -2
  195. package/dist/components/typing-indicator/typing-indicator.js +16 -4
  196. package/dist/components/typing-indicator/typing-indicator.styles.js +2 -2
  197. package/dist/components/virtual-list/virtual-list.d.ts +56 -19
  198. package/dist/components/virtual-list/virtual-list.js +184 -38
  199. package/dist/components/virtual-list/virtual-list.styles.js +12 -0
  200. package/dist/components/widget/widget.d.ts +5 -4
  201. package/dist/components/widget/widget.js +54 -120
  202. package/dist/components/widget/widget.styles.js +8 -2
  203. package/dist/components/word-cloud/word-cloud-layout.d.ts +7 -0
  204. package/dist/components/word-cloud/word-cloud-layout.js +20 -6
  205. package/dist/components/word-cloud/word-cloud.d.ts +22 -2
  206. package/dist/components/word-cloud/word-cloud.js +45 -6
  207. package/dist/components/word-cloud/word-cloud.styles.js +13 -1
  208. package/dist/internal/anchored-validity.d.ts +1 -0
  209. package/dist/internal/anchored-validity.js +86 -0
  210. package/dist/internal/form-associated.d.ts +7 -0
  211. package/dist/internal/form-associated.js +43 -4
  212. package/dist/internal/numbers.d.ts +12 -0
  213. package/dist/internal/numbers.js +26 -0
  214. package/dist/internal/overlay-manager.d.ts +51 -0
  215. package/dist/internal/overlay-manager.js +523 -0
  216. package/dist/internal/positioner.js +27 -3
  217. package/dist/internal/safe-url.d.ts +12 -0
  218. package/dist/internal/safe-url.js +41 -0
  219. package/dist/internal/tokens.styles.js +17 -11
  220. package/dist/lyra.d.ts +5 -2
  221. package/dist/lyra.js +3 -1
  222. package/llms-full.txt +970 -426
  223. package/llms.txt +5 -4
  224. package/package.json +19 -2
@@ -7,43 +7,11 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
7
7
  import { html, nothing } from 'lit';
8
8
  import { property, state } from 'lit/decorators.js';
9
9
  import { LyraElement } from '../../internal/lyra-element.js';
10
+ import { activateOverlay, deepActiveElement } from '../../internal/overlay-manager.js';
10
11
  import { defineElement } from '../../internal/prefix.js';
11
12
  import { lockScroll } from '../../internal/scroll-lock.js';
12
13
  import { styles } from './responsive-panel.styles.js';
13
- const FOCUSABLE_SELECTOR = 'a[href], button:not([disabled]), input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])';
14
- // Shadow-piercing so a slotted custom element's real focusable target (e.g.
15
- // an <input> inside its own shadow root) is found even though the host tag
16
- // itself doesn't match FOCUSABLE_SELECTOR. Deliberately duplicated from
17
- // lyra-dialog's identical helper rather than imported/shared -- every
18
- // overlay-shaped component in this family (lyra-dialog, lyra-widget,
19
- // lyra-tool-result-dialog, lyra-tool-select-dialog) re-implements this
20
- // locally rather than nesting a previous overlay component inside its own
21
- // shadow DOM, because slot-forwarding would break the inner component's own
22
- // light-DOM scanning.
23
- function collectFocusable(el) {
24
- const result = [];
25
- if (el.matches(FOCUSABLE_SELECTOR)) {
26
- result.push(el);
27
- }
28
- if (el instanceof HTMLSlotElement) {
29
- for (const assigned of el.assignedElements({ flatten: true })) {
30
- result.push(...collectFocusable(assigned));
31
- }
32
- return result;
33
- }
34
- const container = el.shadowRoot ?? el;
35
- for (const child of Array.from(container.children)) {
36
- result.push(...collectFocusable(child));
37
- }
38
- return result;
39
- }
40
- /** Whether `el` is actually laid out/paintable -- `checkVisibility()` (falling
41
- * back to `getClientRects().length` where unsupported) correctly follows
42
- * flattened-tree/slot assignment, unlike `offsetParent`. Mirrors
43
- * lyra-dialog's identical helper. */
44
- function isRendered(el) {
45
- return el.checkVisibility ? el.checkVisibility() : el.getClientRects().length > 0;
46
- }
14
+ const HEADING_SELECTOR = 'h1, h2, h3, h4, h5, h6, [role="heading"]';
47
15
  /**
48
16
  * Pure resolution of the `mode` prop + current viewport into the actual
49
17
  * presentation. Kept separate from the `matchMedia` listener wiring below so
@@ -73,18 +41,26 @@ export function resolveEffectiveMode(mode, belowBreakpoint) {
73
41
  * attributes and backdrop element, differ), so lit-html's diffing keeps
74
42
  * `[part="body"]` and its `<slot>` as the same DOM node across the
75
43
  * transition -- scroll position and focus inside the slotted content survive
76
- * it for free, with no extra bookkeeping required. The one exception is
77
- * focus: crossing the breakpoint while already open does *not* forcibly
78
- * move focus into the panel (unlike an `open` transition, which does) --
79
- * doing so on a resize could yank focus away from an input the user is
80
- * actively typing into inside the now-newly-modal panel, which is worse UX
81
- * than a modal that's very briefly missing its initial focus placement.
44
+ * it for free. When an open inline panel becomes modal, focus already inside
45
+ * is preserved and outside focus moves to the first available target. Closing
46
+ * still returns to the opener captured by the original inline open.
47
+ *
48
+ * The overlay presentation uses the library's shared overlay coordinator for
49
+ * focus trapping, Escape/backdrop dismissal, inerting, and stack ordering,
50
+ * while retaining this component's own responsive rendering and close event.
82
51
  *
83
- * The overlay presentation duplicates lyra-dialog's `role="dialog"` +
84
- * focus-trap + Escape/backdrop-dismiss + scroll-lock mechanics locally
85
- * rather than nesting a `<lyra-dialog>` inside this component's shadow DOM,
86
- * per the established precedent for every overlay-shaped component in this
87
- * family (see lyra-dialog's own module doc).
52
+ * Accessible name (overlay presentation only -- the inline presentation has
53
+ * no dialog semantics to name): `label`, when set, is used verbatim. When
54
+ * `label` is empty, this falls back to the `header` slot's content -- a
55
+ * heading element (`h1`–`h6` or `[role="heading"]`) among the slotted header
56
+ * content wins if present, otherwise the header slot's full text content is
57
+ * used, mirroring lyra-dialog's `detectHeading()`/`headingText` fallback (see
58
+ * dialog.ts's module doc for why this uses `aria-label`, a copied string,
59
+ * rather than `aria-labelledby`: the header content is light DOM while
60
+ * `[part="panel"]` lives in this element's shadow tree, and an ID-reference
61
+ * attribute can't resolve across that boundary). A panel opened without
62
+ * either `label` or header content still renders `role="dialog"` with no
63
+ * accessible name -- set one of the two to avoid that.
88
64
  *
89
65
  * @customElement lyra-responsive-panel
90
66
  * @slot - The panel body.
@@ -126,7 +102,8 @@ export class LyraResponsivePanel extends LyraElement {
126
102
  * height. */
127
103
  this.variant = 'fullscreen';
128
104
  /** Accessible name for the overlay presentation's `role="dialog"`. Unused in the inline
129
- * presentation, which has no dialog semantics to name. */
105
+ * presentation, which has no dialog semantics to name. When empty, falls back to the `header`
106
+ * slot's content -- see the class doc for the full fallback order. */
130
107
  this.label = '';
131
108
  /** CSS length passed to `matchMedia` as `(max-width: <this>)` to decide, in `mode="auto"`,
132
109
  * whether the effective presentation is `'overlay'` (below/at this width) or `'inline'`
@@ -136,50 +113,25 @@ export class LyraResponsivePanel extends LyraElement {
136
113
  this.effectiveMode = 'inline';
137
114
  this.hasHeaderSlot = false;
138
115
  this.hasFooterSlot = false;
139
- /** Whether the overlay-only mechanics (scroll-lock, document Escape/Tab trap) are currently
116
+ /** Whether the overlay-only mechanics (scroll-lock and shared overlay registration) are currently
140
117
  * engaged -- derived from `effectiveMode === 'overlay' && open`, tracked separately from those
141
118
  * two properties so willUpdate can diff "was engaged" vs "should be engaged now" regardless of
142
119
  * which of the two changed. */
143
120
  this.overlayChromeActive = false;
121
+ this.focusOverlayAfterUpdate = false;
144
122
  this.isFirstUpdate = true;
145
123
  this.onMediaChange = (e) => {
146
124
  this.handleBreakpointChange(e.matches);
147
125
  };
148
126
  this.onHeaderSlotChange = (e) => {
149
127
  this.hasHeaderSlot = e.target.assignedElements({ flatten: true }).length > 0;
128
+ this.headingText = this.detectHeadingText();
150
129
  };
151
130
  this.onFooterSlotChange = (e) => {
152
131
  this.hasFooterSlot = e.target.assignedElements({ flatten: true }).length > 0;
153
132
  };
154
133
  this.onBackdropClick = () => {
155
- this.close('backdrop');
156
- };
157
- this.onDocKeyDown = (e) => {
158
- if (!this.overlayChromeActive)
159
- return;
160
- if (e.key === 'Escape') {
161
- e.preventDefault();
162
- this.close('escape');
163
- return;
164
- }
165
- if (e.key !== 'Tab')
166
- return;
167
- const focusable = this.getFocusableElements();
168
- if (focusable.length === 0) {
169
- e.preventDefault();
170
- return;
171
- }
172
- const first = focusable[0];
173
- const last = focusable[focusable.length - 1];
174
- const active = this.getActiveElement();
175
- if (e.shiftKey && active === first) {
176
- e.preventDefault();
177
- last.focus();
178
- }
179
- else if (!e.shiftKey && active === last) {
180
- e.preventDefault();
181
- first.focus();
182
- }
134
+ this.overlayHandle?.dismissBackdrop();
183
135
  };
184
136
  }
185
137
  static { this.styles = [LyraElement.styles, styles]; }
@@ -188,46 +140,69 @@ export class LyraResponsivePanel extends LyraElement {
188
140
  if (this.isFirstUpdate) {
189
141
  this.hasHeaderSlot = Array.from(this.children).some((el) => el.getAttribute('slot') === 'header');
190
142
  this.hasFooterSlot = Array.from(this.children).some((el) => el.getAttribute('slot') === 'footer');
143
+ this.headingText = this.detectHeadingText();
144
+ }
145
+ // Must run before the effectiveMode computation below reads
146
+ // belowBreakpoint: setupMediaQuery() -> handleBreakpointChange() mutates
147
+ // belowBreakpoint synchronously, and a runtime mobileBreakpoint change is
148
+ // otherwise the only kind of change in this method that doesn't itself
149
+ // touch mode/belowBreakpoint, so computing effectiveMode first would read
150
+ // the not-yet-updated belowBreakpoint and never get another chance to
151
+ // react -- a property write from inside willUpdate doesn't schedule a
152
+ // fresh update, it only folds into the one already in progress.
153
+ // mobileBreakpoint is present on the very first willUpdate too (Lit's
154
+ // changed-properties map includes every class-field default from
155
+ // construction), but connectedCallback() already called setupMediaQuery()
156
+ // once before this first update runs, so repeating it here would be
157
+ // redundant -- only a live post-mount change should re-run it.
158
+ if (!this.isFirstUpdate && changed.has('mobileBreakpoint') && this.isConnected) {
159
+ this.setupMediaQuery();
191
160
  }
192
161
  if (this.isFirstUpdate || changed.has('mode') || changed.has('belowBreakpoint')) {
193
162
  this.effectiveMode = resolveEffectiveMode(this.mode, this.belowBreakpoint);
194
163
  }
195
- if (changed.has('mobileBreakpoint') && this.isConnected) {
196
- this.setupMediaQuery();
164
+ // Captured on a genuine open transition only, independent of
165
+ // effectiveMode at that moment -- so it reflects whatever triggered the
166
+ // open even if a later breakpoint crossing (while still open) switches
167
+ // the presentation to overlay before close() runs. Deliberately not
168
+ // folded into the overlayOpen-engage branch below: that branch also
169
+ // fires when only effectiveMode (not open) changes, which would
170
+ // re-capture whatever currently has focus (e.g. something inside the
171
+ // panel) instead of the original external trigger.
172
+ if (changed.has('open') && this.open) {
173
+ const active = deepActiveElement(this.ownerDocument);
174
+ this.lastTrigger = active && typeof active.focus === 'function' ? active : undefined;
197
175
  }
198
176
  const overlayOpen = this.effectiveMode === 'overlay' && this.open;
199
177
  if (overlayOpen !== this.overlayChromeActive) {
200
178
  this.overlayChromeActive = overlayOpen;
201
179
  if (overlayOpen) {
202
- const active = this.getActiveElement();
203
- this.lastTrigger = active instanceof HTMLElement ? active : undefined;
204
- this.releaseScrollLock = lockScroll();
205
- document.addEventListener('keydown', this.onDocKeyDown);
180
+ this.activateOverlayChrome();
181
+ // Once the presentation becomes modal, focus cannot remain behind
182
+ // it. The manager preserves focus that is already inside the panel
183
+ // and otherwise moves it to the first composed focus target.
184
+ this.focusOverlayAfterUpdate = true;
206
185
  }
207
186
  else {
208
- this.releaseScrollLock?.();
209
- this.releaseScrollLock = undefined;
210
- document.removeEventListener('keydown', this.onDocKeyDown);
187
+ // Removing modal chrome while the still-open panel becomes inline
188
+ // must preserve focus in that same panel. Only a real open -> closed
189
+ // transition restores the opener.
190
+ this.deactivateOverlayChrome(changed.has('open') && !this.open);
211
191
  }
212
192
  }
193
+ if (changed.has('open') && !this.open)
194
+ this.lastTrigger = undefined;
213
195
  }
214
196
  // Runs after render (not willUpdate) so [part="panel"] has already landed
215
197
  // in the DOM before the fallback .focus() call below can rely on it --
216
- // mirrors lyra-dialog's identical ordering rationale. Only a genuine `open`
217
- // transition steals focus (see the class doc for why a breakpoint-crossing
218
- // while already open deliberately does not).
198
+ // mirrors lyra-dialog's identical ordering rationale.
219
199
  updated(changed) {
220
200
  if (!this.isFirstUpdate && changed.has('effectiveMode')) {
221
201
  this.emit('lyra-mode-change', { mode: this.effectiveMode });
222
202
  }
223
- if (changed.has('open') && this.open && this.effectiveMode === 'overlay') {
224
- const first = this.getFocusableElements()[0];
225
- if (first) {
226
- first.focus();
227
- }
228
- else {
229
- this.shadowRoot?.querySelector('[part="panel"]')?.focus();
230
- }
203
+ if (this.focusOverlayAfterUpdate) {
204
+ this.focusOverlayAfterUpdate = false;
205
+ this.overlayHandle?.focusInitial();
231
206
  }
232
207
  }
233
208
  connectedCallback() {
@@ -237,10 +212,17 @@ export class LyraResponsivePanel extends LyraElement {
237
212
  // instance) fires disconnectedCallback then connectedCallback
238
213
  // synchronously with no update in between, so willUpdate never reruns to
239
214
  // notice overlay chrome is still supposed to be active -- restore it.
240
- // Mirrors lyra-dialog's identical reconnect handling.
241
- if (this.hasUpdated && this.overlayChromeActive && !this.releaseScrollLock) {
242
- this.releaseScrollLock = lockScroll();
243
- document.addEventListener('keydown', this.onDocKeyDown);
215
+ const overlayOpen = resolveEffectiveMode(this.mode, this.belowBreakpoint) === 'overlay' && this.open;
216
+ if (this.hasUpdated && overlayOpen) {
217
+ if (this.overlayHandle?.isActive()) {
218
+ this.overlayHandle.resume();
219
+ }
220
+ else {
221
+ this.activateOverlayChrome();
222
+ }
223
+ if (!this.releaseScrollLock)
224
+ this.releaseScrollLock = lockScroll(this.ownerDocument);
225
+ queueMicrotask(() => this.overlayHandle?.focusInitial());
244
226
  }
245
227
  }
246
228
  disconnectedCallback() {
@@ -249,11 +231,32 @@ export class LyraResponsivePanel extends LyraElement {
249
231
  this.mediaQuery = undefined;
250
232
  this.releaseScrollLock?.();
251
233
  this.releaseScrollLock = undefined;
252
- document.removeEventListener('keydown', this.onDocKeyDown);
234
+ this.overlayHandle?.suspend();
235
+ }
236
+ activateOverlayChrome() {
237
+ this.releaseScrollLock ??= lockScroll(this.ownerDocument);
238
+ this.overlayHandle = activateOverlay({
239
+ host: this,
240
+ panel: () => this.shadowRoot?.querySelector('[part="panel"]') ?? null,
241
+ onEscape: () => this.close('escape'),
242
+ onBackdrop: () => this.close('backdrop'),
243
+ restoreFocusTo: this.lastTrigger ?? null,
244
+ });
245
+ }
246
+ deactivateOverlayChrome(restoreFocus = true) {
247
+ this.releaseScrollLock?.();
248
+ this.releaseScrollLock = undefined;
249
+ this.overlayHandle?.deactivate({ restoreFocus });
250
+ this.overlayHandle = undefined;
253
251
  }
254
252
  setupMediaQuery() {
255
253
  this.mediaQuery?.removeEventListener('change', this.onMediaChange);
256
- this.mediaQuery = window.matchMedia(`(max-width: ${this.mobileBreakpoint})`);
254
+ const view = this.ownerDocument.defaultView;
255
+ if (!view?.matchMedia) {
256
+ this.mediaQuery = undefined;
257
+ return;
258
+ }
259
+ this.mediaQuery = view.matchMedia(`(max-width: ${this.mobileBreakpoint})`);
257
260
  this.mediaQuery.addEventListener('change', this.onMediaChange);
258
261
  this.handleBreakpointChange(this.mediaQuery.matches);
259
262
  }
@@ -263,6 +266,27 @@ export class LyraResponsivePanel extends LyraElement {
263
266
  handleBreakpointChange(matches) {
264
267
  this.belowBreakpoint = matches;
265
268
  }
269
+ // Only direct children slotted into `header` are scanned -- same depth
270
+ // limit lyra-dialog's own detectHeading() applies to its direct children.
271
+ // A heading tag among them wins (its own text only, so a header slot mixing
272
+ // a heading with e.g. a trailing icon button doesn't pull the button's
273
+ // label into the accessible name); otherwise the header slot's combined
274
+ // text stands in for it, since this component (unlike lyra-dialog) has a
275
+ // dedicated header slot that's already the conventional place a heading
276
+ // would go. Recomputed only on slot assignment changes, not on every
277
+ // render -- same rationale as lyra-dialog's detectHeading().
278
+ detectHeadingText() {
279
+ const headerChildren = Array.from(this.children).filter((el) => el.getAttribute('slot') === 'header');
280
+ if (headerChildren.length === 0)
281
+ return undefined;
282
+ const heading = headerChildren.find((el) => el.matches(HEADING_SELECTOR));
283
+ if (heading)
284
+ return heading.textContent?.trim() || undefined;
285
+ return (headerChildren
286
+ .map((el) => el.textContent?.trim())
287
+ .filter(Boolean)
288
+ .join(' ') || undefined);
289
+ }
266
290
  /**
267
291
  * Close the panel. `reason` is forwarded as the `lyra-close` detail --
268
292
  * built-in overlay triggers pass `'escape'`/`'backdrop'`; a consumer's own
@@ -278,39 +302,10 @@ export class LyraResponsivePanel extends LyraElement {
278
302
  return;
279
303
  this.open = false;
280
304
  this.emit('lyra-close', reason);
281
- if (this.effectiveMode === 'overlay') {
282
- this.lastTrigger?.focus();
283
- }
284
- }
285
- // Bounds Tab/Shift+Tab to the panel while overlay chrome is active. Order
286
- // follows the header slot, then the body slot, then the footer slot -- the
287
- // same order the flattened tree already tabs through.
288
- getFocusableElements() {
289
- const root = this.shadowRoot;
290
- if (!root)
291
- return [];
292
- const fromSlot = (selector) => {
293
- const slot = root.querySelector(selector);
294
- return slot ? slot.assignedElements({ flatten: true }).flatMap(collectFocusable) : [];
295
- };
296
- return [
297
- ...fromSlot('slot[name="header"]'),
298
- ...fromSlot('slot:not([name])'),
299
- ...fromSlot('slot[name="footer"]'),
300
- ].filter(isRendered);
301
- }
302
- getActiveElement() {
303
- let active = document.activeElement;
304
- while (active) {
305
- const inner = active.shadowRoot?.activeElement ?? null;
306
- if (!inner)
307
- break;
308
- active = inner;
309
- }
310
- return active;
311
305
  }
312
306
  render() {
313
307
  const overlay = this.effectiveMode === 'overlay';
308
+ const accessibleName = this.label || this.headingText;
314
309
  return html `
315
310
  <div part="base" class=${overlay ? 'overlay' : 'inline'}>
316
311
  ${overlay ? html `<div part="backdrop" @click=${this.onBackdropClick}></div>` : nothing}
@@ -318,7 +313,7 @@ export class LyraResponsivePanel extends LyraElement {
318
313
  part="panel"
319
314
  role=${overlay ? 'dialog' : nothing}
320
315
  aria-modal=${overlay ? 'true' : nothing}
321
- aria-label=${overlay && this.label ? this.label : nothing}
316
+ aria-label=${overlay && accessibleName ? accessibleName : nothing}
322
317
  tabindex=${overlay ? '-1' : nothing}
323
318
  >
324
319
  <div part="header" ?hidden=${!this.hasHeaderSlot}>
@@ -362,4 +357,7 @@ __decorate([
362
357
  __decorate([
363
358
  state()
364
359
  ], LyraResponsivePanel.prototype, "hasFooterSlot", void 0);
360
+ __decorate([
361
+ state()
362
+ ], LyraResponsivePanel.prototype, "headingText", void 0);
365
363
  defineElement('responsive-panel', LyraResponsivePanel);
@@ -24,7 +24,7 @@ export const styles = css `
24
24
  display: flex;
25
25
  position: fixed;
26
26
  inset: 0;
27
- z-index: 1000;
27
+ z-index: var(--lyra-overlay-stack-index, 1000);
28
28
  }
29
29
  /* Bottom-sheet anchors its panel to the block-end edge instead of
30
30
  stretching it full-height like fullscreen does. */
@@ -22,7 +22,10 @@ import { LyraElement } from '../../internal/lyra-element.js';
22
22
  * `actions` is still observed) but `hidden` whenever there is no `title`
23
23
  * and no `actions` content — an untitled, action-less card has no visible
24
24
  * header bar at all.
25
- * @csspart title - The title text.
25
+ * @csspart title - The title text. Truncates with an ellipsis when it
26
+ * overflows; carries its own native `title` attribute (the full string) so
27
+ * hovering the truncated text reveals it via the browser's default tooltip,
28
+ * scoped to just this element rather than the whole card.
26
29
  * @csspart actions - The wrapper around the `actions` slot. `hidden`
27
30
  * whenever the slot has no assigned content.
28
31
  * @csspart body - The wrapper around the default slot.
@@ -30,7 +33,10 @@ import { LyraElement } from '../../internal/lyra-element.js';
30
33
  export declare class LyraResultCard extends LyraElement {
31
34
  static styles: import("lit").CSSResultGroup[];
32
35
  /** Small heading for the card. Leave unset for an untitled card (e.g. a
33
- * bare block of `<lyra-result-field>` rows with no natural heading). */
36
+ * bare block of `<lyra-result-field>` rows with no natural heading).
37
+ * Rendered into the truncating `[part="title"]` span, which also carries
38
+ * this value as its own `title` attribute so the disclosure tooltip is
39
+ * scoped to that element rather than the whole host. */
34
40
  title: string;
35
41
  private hasActionsSlot;
36
42
  protected willUpdate(): void;
@@ -31,7 +31,10 @@ import { styles } from './result-card.styles.js';
31
31
  * `actions` is still observed) but `hidden` whenever there is no `title`
32
32
  * and no `actions` content — an untitled, action-less card has no visible
33
33
  * header bar at all.
34
- * @csspart title - The title text.
34
+ * @csspart title - The title text. Truncates with an ellipsis when it
35
+ * overflows; carries its own native `title` attribute (the full string) so
36
+ * hovering the truncated text reveals it via the browser's default tooltip,
37
+ * scoped to just this element rather than the whole card.
35
38
  * @csspart actions - The wrapper around the `actions` slot. `hidden`
36
39
  * whenever the slot has no assigned content.
37
40
  * @csspart body - The wrapper around the default slot.
@@ -40,7 +43,10 @@ export class LyraResultCard extends LyraElement {
40
43
  constructor() {
41
44
  super(...arguments);
42
45
  /** Small heading for the card. Leave unset for an untitled card (e.g. a
43
- * bare block of `<lyra-result-field>` rows with no natural heading). */
46
+ * bare block of `<lyra-result-field>` rows with no natural heading).
47
+ * Rendered into the truncating `[part="title"]` span, which also carries
48
+ * this value as its own `title` attribute so the disclosure tooltip is
49
+ * scoped to that element rather than the whole host. */
44
50
  this.title = '';
45
51
  // See `<lyra-widget>`'s identical `hasActionsSlot` -- a `[part]` wrapper
46
52
  // always contains a literal `<slot>` child regardless of assigned
@@ -64,7 +70,7 @@ export class LyraResultCard extends LyraElement {
64
70
  return html `
65
71
  <div part="base">
66
72
  <div part="header" ?hidden=${!hasHeader}>
67
- ${hasTitle ? html `<span part="title">${this.title}</span>` : nothing}
73
+ ${hasTitle ? html `<span part="title" title=${this.title}>${this.title}</span>` : nothing}
68
74
  <div part="actions" ?hidden=${!this.hasActionsSlot}>
69
75
  <slot name="actions" @slotchange=${this.onActionsSlotChange}></slot>
70
76
  </div>
@@ -27,7 +27,6 @@ export const styles = css `
27
27
  min-inline-size: 0;
28
28
  color: var(--lyra-color-text);
29
29
  font-weight: 600;
30
- font-size: 0.8125rem;
31
30
  overflow: hidden;
32
31
  text-overflow: ellipsis;
33
32
  white-space: nowrap;
@@ -21,16 +21,21 @@ export type LyraSelectSize = 'xs' | 's' | 'm' | 'l' | 'xl';
21
21
  * "active" row is conveyed via `aria-activedescendant`, never actual focus),
22
22
  * matching the WAI-ARIA "select-only combobox" pattern.
23
23
  *
24
- * When exactly one option is enabled (regardless of how many disabled ones
25
- * exist alongside it), the popup never opens at all: a click, Enter, Space,
26
- * ArrowDown, or ArrowUp on the trigger commits that sole option directly,
27
- * and the trigger renders as a plain `role="button"` with no chevron/
28
- * `aria-haspopup`/`aria-expanded` rather than a combobox with a permanently
29
- * inert popup state — opening a one-row list to pick the only available
30
- * choice is pure friction with no real decision behind it. This never
31
- * changes `value`/validity defaults on its own — an unselected single-option
32
- * select stays unselected (and a `required` one stays invalid) exactly like
33
- * the multi-option case, until the trigger is actually activated.
24
+ * When `autoCommitSingleOption` is set and exactly one option is enabled
25
+ * (regardless of how many disabled ones exist alongside it), the popup never
26
+ * opens at all: a click, Enter, Space, ArrowDown, or ArrowUp on the trigger
27
+ * commits that sole option directly, and the trigger renders as a plain
28
+ * `role="button"` with no chevron/`aria-haspopup`/`aria-expanded` rather than
29
+ * a combobox with a permanently inert popup state — opening a one-row list to
30
+ * pick the only available choice is pure friction with no real decision
31
+ * behind it. This never changes `value`/validity defaults on its own — an
32
+ * unselected single-option select stays unselected (and a `required` one
33
+ * stays invalid) exactly like the multi-option case, until the trigger is
34
+ * actually activated. `autoCommitSingleOption` defaults to `false`: by
35
+ * default a select always renders the normal combobox/listbox/chevron
36
+ * trigger no matter how many options are enabled, matching pre-1.3.0
37
+ * behavior — opt in explicitly if a narrowing-to-one option list should
38
+ * auto-commit.
34
39
  *
35
40
  * @customElement lyra-select
36
41
  * @slot - `<lyra-option>` elements.
@@ -58,6 +63,16 @@ export declare class LyraSelect extends LyraElement {
58
63
  static formAssociated: boolean;
59
64
  static styles: import("lit").CSSResultGroup[];
60
65
  static properties: {
66
+ disabled: {
67
+ type: BooleanConstructor;
68
+ reflect: boolean;
69
+ noAccessor: boolean;
70
+ };
71
+ required: {
72
+ type: BooleanConstructor;
73
+ reflect: boolean;
74
+ noAccessor: boolean;
75
+ };
61
76
  value: {
62
77
  noAccessor: boolean;
63
78
  };
@@ -67,14 +82,22 @@ export declare class LyraSelect extends LyraElement {
67
82
  };
68
83
  };
69
84
  placeholder: string;
70
- disabled: boolean;
71
- required: boolean;
72
85
  label: string;
73
86
  hint: string;
74
87
  errorText: string;
75
88
  open: boolean;
76
89
  /** Visual size — same `xs`–`xl` scale as `lyra-toast-item`'s `size`. */
77
90
  size: LyraSelectSize;
91
+ /**
92
+ * Opt-in: when `true` and exactly one `<lyra-option>` is enabled, the
93
+ * trigger commits that option directly (click/Enter/Space/ArrowDown/
94
+ * ArrowUp) instead of opening the listbox, and renders as a plain
95
+ * `role="button"` with no chevron. Defaults to `false`, which always
96
+ * renders the normal combobox/listbox/chevron trigger regardless of how
97
+ * many options are enabled — the pre-1.3.0 behavior. See the class doc
98
+ * above and `onlyOption` below for the full rationale.
99
+ */
100
+ autoCommitSingleOption: boolean;
78
101
  private activeIndex;
79
102
  private options;
80
103
  private touched;
@@ -82,22 +105,35 @@ export declare class LyraSelect extends LyraElement {
82
105
  private hasErrorSlot;
83
106
  private hasLabelSlot;
84
107
  private internals;
108
+ private validityController;
85
109
  private _fieldsetDisabled;
86
110
  private listId;
87
111
  private triggerId;
88
112
  private cleanup?;
89
113
  private _isFirstUpdate;
90
114
  private _selected;
115
+ private _disabled;
116
+ private _required;
91
117
  private _defaultSelected;
92
118
  private _defaultCaptured;
119
+ private _restoredStateActive;
93
120
  private typeAheadBuffer;
94
121
  private typeAheadTimer?;
95
122
  private _name;
96
123
  constructor();
124
+ get form(): HTMLFormElement | null;
125
+ get labels(): NodeList;
126
+ get validity(): ValidityState;
127
+ get validationMessage(): string;
128
+ get willValidate(): boolean;
97
129
  connectedCallback(): void;
98
130
  protected willUpdate(): void;
99
131
  get name(): string;
100
132
  set name(next: string);
133
+ get disabled(): boolean;
134
+ set disabled(next: boolean);
135
+ get required(): boolean;
136
+ set required(next: boolean);
101
137
  /** The selected value: a single string (empty when nothing is selected). */
102
138
  get value(): string;
103
139
  set value(next: string);
@@ -108,6 +144,7 @@ export declare class LyraSelect extends LyraElement {
108
144
  * own `disabled` IDL property/attribute is never mutated by a fieldset. */
109
145
  get effectiveDisabled(): boolean;
110
146
  formResetCallback(): void;
147
+ formStateRestoreCallback(state: string | File | FormData | null, _mode?: 'restore' | 'autocomplete'): void;
111
148
  /**
112
149
  * Called by the browser when an ancestor `<fieldset disabled>` toggles.
113
150
  * Tracked separately from the consumer's own `disabled` (see
@@ -121,14 +158,16 @@ export declare class LyraSelect extends LyraElement {
121
158
  private collectOptions;
122
159
  private reflectSelected;
123
160
  /**
124
- * The sole navigable (non-disabled) option, when there's exactly one. Opening a one-row
125
- * popup to pick the only available choice is pure friction with no real decision behind
126
- * it, so a single-option select skips the popup entirely -- see `onTriggerClick`/`onKeyDown`,
127
- * which commit this option directly on activation instead of opening the listbox, and
128
- * `render()`, which drops the popup-trigger ARIA semantics (`role="combobox"`,
129
- * `aria-haspopup`, `aria-expanded`, the chevron) in favor of a plain button's, since no
130
- * popup can ever appear while this getter returns a value. Does not affect `value`/
131
- * validity defaults in any way -- an unselected single-option select stays unselected
161
+ * The sole navigable (non-disabled) option, when `autoCommitSingleOption` is set and
162
+ * there's exactly one. Opening a one-row popup to pick the only available choice is pure
163
+ * friction with no real decision behind it, so an opted-in single-option select skips the
164
+ * popup entirely -- see `onTriggerClick`/`onKeyDown`, which commit this option directly on
165
+ * activation instead of opening the listbox, and `render()`, which drops the popup-trigger
166
+ * ARIA semantics (`role="combobox"`, `aria-haspopup`, `aria-expanded`, the chevron) in favor
167
+ * of a plain button's, since no popup can ever appear while this getter returns a value.
168
+ * Returns `undefined` whenever `autoCommitSingleOption` is `false` (the default), regardless
169
+ * of option count, so the normal combobox trigger renders unconditionally. Does not affect
170
+ * `value`/validity defaults in any way -- an unselected single-option select stays unselected
132
171
  * until the trigger is actually activated, exactly like the multi-option case.
133
172
  */
134
173
  private get onlyOption();