@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
@@ -51,7 +51,10 @@ import './menu-item.js';
51
51
  * Home/End jump to the first/last non-disabled item. Enter/Space activate
52
52
  * the focused item. Escape closes and returns focus to the trigger. Tab
53
53
  * closes the menu without trapping focus (the browser's own default Tab
54
- * behavior proceeds untouched).
54
+ * behavior proceeds untouched). A printable keypress runs type-ahead:
55
+ * roving focus jumps to the next non-disabled item whose text starts with
56
+ * the accumulated buffer, cycling from just after the active item (mirrors
57
+ * `<lyra-select>`'s identical listbox type-ahead).
55
58
  * - A click outside both the trigger and the open popup closes it (mirrors
56
59
  * `<lyra-select>`'s `onDocPointer` exactly) — this does *not* refocus the
57
60
  * trigger, since the outside click itself already moved focus somewhere
@@ -112,6 +115,11 @@ export class LyraMenu extends LyraElement {
112
115
  this._isFirstUpdate = true;
113
116
  this.pendingFocus = 'first';
114
117
  this.listId = nextId('menu-list');
118
+ // Standard menu type-ahead, mirroring lyra-select's identical listbox
119
+ // trio: printable keystrokes accumulate into this buffer and reset ~500ms
120
+ // after the last one, so "d" then "e" narrows to "de" instead of
121
+ // restarting the search on every keystroke.
122
+ this.typeAheadBuffer = '';
115
123
  this.onDocPointer = (e) => {
116
124
  if (!e.composedPath().includes(this))
117
125
  this.hide();
@@ -161,9 +169,19 @@ export class LyraMenu extends LyraElement {
161
169
  this.reposition();
162
170
  };
163
171
  this.onItemsSlotChange = (e) => {
172
+ this.itemStateObserver?.disconnect();
164
173
  this.items = e.target
165
174
  .assignedElements({ flatten: true })
166
175
  .filter((el) => el instanceof LyraMenuItem);
176
+ if (typeof MutationObserver !== 'undefined') {
177
+ this.itemStateObserver = new MutationObserver(() => this.onItemStateChange());
178
+ for (const item of this.items) {
179
+ this.itemStateObserver.observe(item, {
180
+ attributes: true,
181
+ attributeFilter: ['disabled', 'hidden', 'aria-hidden'],
182
+ });
183
+ }
184
+ }
167
185
  if (this.activeIndex >= this.items.length)
168
186
  this.activeIndex = -1;
169
187
  this.applyRovingTabIndex();
@@ -175,6 +193,24 @@ export class LyraMenu extends LyraElement {
175
193
  if (this.open && this.activeIndex === -1)
176
194
  this.focusRoving(this.pendingFocus);
177
195
  };
196
+ /** Rehomes roving focus immediately when an active item becomes disabled or hidden. */
197
+ this.onItemStateChange = () => {
198
+ const navigable = this.items.filter((item) => this.isNavigable(item));
199
+ if (this.activeIndex >= 0 && !this.isNavigable(this.items[this.activeIndex])) {
200
+ const current = this.activeIndex;
201
+ const next = navigable.find((item) => this.items.indexOf(item) > current) ??
202
+ navigable.find((item) => this.items.indexOf(item) < current);
203
+ if (next) {
204
+ this.setActiveItem(next);
205
+ }
206
+ else {
207
+ this.activeIndex = -1;
208
+ this.applyRovingTabIndex();
209
+ }
210
+ return;
211
+ }
212
+ this.applyRovingTabIndex();
213
+ };
178
214
  this.onItemSelect = (e) => {
179
215
  const item = e.target;
180
216
  if (!(item instanceof LyraMenuItem))
@@ -182,8 +218,27 @@ export class LyraMenu extends LyraElement {
182
218
  this.emit('lyra-menu-select', { value: item.value });
183
219
  this.hide(true);
184
220
  };
221
+ /** Resyncs `activeIndex` (and the roving `tabindex`) to wherever real DOM
222
+ * focus actually lands, for any path that doesn't go through
223
+ * `setActiveItem()` -- e.g. a real mousedown on an item, which focuses it
224
+ * even while `disabled` (`tabIndex="-1"` remains mouse-focusable per
225
+ * spec). Without this, `activeIndex` goes stale the moment focus moves any
226
+ * other way, and subsequent Arrow-key navigation computes its next item
227
+ * from that stale position instead of from where focus actually is. A
228
+ * no-op for `setActiveItem()`'s own `.focus()` call, since `activeIndex`
229
+ * there is already set to match before focus moves. */
230
+ this.onListFocusIn = (e) => {
231
+ const target = e.target;
232
+ if (!(target instanceof LyraMenuItem))
233
+ return;
234
+ const index = this.items.indexOf(target);
235
+ if (index === -1 || index === this.activeIndex)
236
+ return;
237
+ this.activeIndex = index;
238
+ this.applyRovingTabIndex();
239
+ };
185
240
  this.onListKeyDown = (e) => {
186
- const navigable = this.items.filter((i) => !i.disabled);
241
+ const navigable = this.items.filter((i) => this.isNavigable(i));
187
242
  const current = this.activeIndex >= 0 ? this.items[this.activeIndex] : undefined;
188
243
  const currentNavIndex = current ? navigable.indexOf(current) : -1;
189
244
  switch (e.key) {
@@ -227,6 +282,9 @@ export class LyraMenu extends LyraElement {
227
282
  this.hide();
228
283
  break;
229
284
  default:
285
+ if (e.key.length === 1 && !e.altKey && !e.ctrlKey && !e.metaKey) {
286
+ this.typeAhead(e.key);
287
+ }
230
288
  return;
231
289
  }
232
290
  };
@@ -280,7 +338,16 @@ export class LyraMenu extends LyraElement {
280
338
  super.disconnectedCallback();
281
339
  this.cleanup?.();
282
340
  this.cleanup = undefined;
341
+ clearTimeout(this.typeAheadTimer);
342
+ this.itemStateObserver?.disconnect();
343
+ this.itemStateObserver = undefined;
283
344
  document.removeEventListener('pointerdown', this.onDocPointer);
345
+ // Reset so a reconnect (e.g. a drag-drop reparent) re-triggers
346
+ // `updated()`'s `open`-driven branch -- without this, `open` stays
347
+ // `true` across the disconnect/reconnect and `changed.has('open')` never
348
+ // fires again, leaving the menu rendered open with no positioning and
349
+ // no outside-click listener.
350
+ this.open = false;
284
351
  }
285
352
  show(focus = 'first') {
286
353
  if (this.open)
@@ -310,6 +377,9 @@ export class LyraMenu extends LyraElement {
310
377
  this.triggerEl.setAttribute('aria-expanded', this.open ? 'true' : 'false');
311
378
  this.triggerEl.setAttribute('aria-controls', this.listId);
312
379
  }
380
+ isNavigable(item) {
381
+ return !item.disabled && !item.hidden && item.getAttribute('aria-hidden') !== 'true';
382
+ }
313
383
  /** Flips exactly one non-disabled item's `tabIndex` to `0` (the roving
314
384
  * target) and every other item's to `-1` -- see `<lyra-menu-item>`'s doc
315
385
  * for why this is the sole authority over that property. */
@@ -322,7 +392,7 @@ export class LyraMenu extends LyraElement {
322
392
  * non-disabled item. A no-op when there are none -- focus then simply
323
393
  * stays on the trigger (see onTriggerKeyDown's Escape safety net). */
324
394
  focusRoving(which) {
325
- const navigable = this.items.filter((i) => !i.disabled);
395
+ const navigable = this.items.filter((i) => this.isNavigable(i));
326
396
  if (!navigable.length)
327
397
  return;
328
398
  const item = which === 'first' ? navigable[0] : navigable[navigable.length - 1];
@@ -333,6 +403,30 @@ export class LyraMenu extends LyraElement {
333
403
  this.applyRovingTabIndex();
334
404
  item.focus();
335
405
  }
406
+ /** Standard WAI-ARIA APG menu-button type-ahead: moves the roving focus to
407
+ * the next non-disabled item whose text content starts with the
408
+ * accumulated buffer, cycling from just after the currently active item
409
+ * -- mirrors `<lyra-select>`'s identical listbox type-ahead. */
410
+ typeAhead(char) {
411
+ clearTimeout(this.typeAheadTimer);
412
+ this.typeAheadBuffer += char.toLowerCase();
413
+ this.typeAheadTimer = setTimeout(() => {
414
+ this.typeAheadBuffer = '';
415
+ }, 500);
416
+ const navigable = this.items.filter((i) => !i.disabled);
417
+ if (!navigable.length)
418
+ return;
419
+ const current = this.activeIndex >= 0 ? this.items[this.activeIndex] : undefined;
420
+ const currentIndex = current ? navigable.indexOf(current) : -1;
421
+ const n = navigable.length;
422
+ for (let step = 1; step <= n; step++) {
423
+ const candidate = navigable[(currentIndex + step + n) % n];
424
+ if ((candidate.textContent ?? '').trim().toLowerCase().startsWith(this.typeAheadBuffer)) {
425
+ this.setActiveItem(candidate);
426
+ return;
427
+ }
428
+ }
429
+ }
336
430
  render() {
337
431
  return html `
338
432
  <div part="trigger" @click=${this.onTriggerClick} @keydown=${this.onTriggerKeyDown}>
@@ -345,7 +439,9 @@ export class LyraMenu extends LyraElement {
345
439
  role="menu"
346
440
  aria-label=${this.label}
347
441
  @keydown=${this.onListKeyDown}
442
+ @focusin=${this.onListFocusIn}
348
443
  @lyra-menu-item-select=${this.onItemSelect}
444
+ @lyra-menu-item-state-change=${this.onItemStateChange}
349
445
  >
350
446
  <slot @slotchange=${this.onItemsSlotChange}></slot>
351
447
  </div>
@@ -18,7 +18,8 @@ export const styles = css `
18
18
  z-index: 900;
19
19
  box-sizing: border-box;
20
20
  min-inline-size: 10rem;
21
- max-inline-size: min(92vw, 20rem);
21
+ max-inline-size: min(92vw, 20rem, var(--lyra-positioner-available-inline-size, 100vw));
22
+ max-block-size: min(20rem, var(--lyra-positioner-available-block-size, 20rem));
22
23
  background: var(--lyra-color-surface);
23
24
  border: 1px solid var(--lyra-color-border);
24
25
  border-radius: var(--lyra-radius);
@@ -50,7 +51,7 @@ export const styles = css `
50
51
  [part='list'] {
51
52
  display: flex;
52
53
  flex-direction: column;
53
- max-block-size: 20rem;
54
+ max-block-size: min(20rem, var(--lyra-positioner-available-block-size, 20rem));
54
55
  overflow-y: auto;
55
56
  padding: var(--lyra-space-xs);
56
57
  outline: none;
@@ -30,6 +30,8 @@ export type LyraModelCatalog = string[] | LyraModelCatalogEntry[];
30
30
  *
31
31
  * @customElement lyra-model-select
32
32
  * @event lyra-change - The selected/typed value changed. `detail: { value: string; inCatalog: boolean }`.
33
+ * @csspart form-control-label - The `<label>` element (only rendered — and only contributes to the
34
+ * accessible name — once `label` is non-empty).
33
35
  * @csspart trigger - The trigger button (closed-dropdown mode's positioning anchor).
34
36
  * @csspart combobox - The text-input container (free-text mode's positioning anchor).
35
37
  * @csspart combobox-input - The free-text mode's text input.
@@ -44,9 +46,23 @@ export declare class LyraModelSelect extends LyraElement {
44
46
  static formAssociated: boolean;
45
47
  static styles: import("lit").CSSResultGroup[];
46
48
  static properties: {
49
+ disabled: {
50
+ type: BooleanConstructor;
51
+ reflect: boolean;
52
+ noAccessor: boolean;
53
+ };
54
+ required: {
55
+ type: BooleanConstructor;
56
+ reflect: boolean;
57
+ noAccessor: boolean;
58
+ };
47
59
  value: {
48
60
  noAccessor: boolean;
49
61
  };
62
+ name: {
63
+ reflect: boolean;
64
+ noAccessor: boolean;
65
+ };
50
66
  };
51
67
  /** Informational only — e.g. `'ollama'`. Rendered as a small leading badge for display grouping. */
52
68
  provider: string;
@@ -54,29 +70,56 @@ export declare class LyraModelSelect extends LyraElement {
54
70
  catalog?: LyraModelCatalog;
55
71
  /** Let the user type/commit a value that isn't in `catalog`, even when `catalog` is non-empty. */
56
72
  allowCustom: boolean;
73
+ /**
74
+ * Optional visible title above the control, mirroring `lyra-select`'s
75
+ * `label` exactly: rendered via a `part="form-control-label"` `<label>`
76
+ * paired with the control's id, and — once non-empty — it takes over as
77
+ * the accessible-name source (the `aria-label` fallback below is then only
78
+ * emitted when the host has an explicit `aria-label` override, same
79
+ * precedence as `lyra-select`). Leaving it empty (the default) keeps
80
+ * today's `aria-label || placeholder || 'Model'` chain untouched.
81
+ */
82
+ label: string;
57
83
  placeholder: string;
58
- name: string;
59
- disabled: boolean;
60
- required: boolean;
61
84
  open: boolean;
62
85
  private activeIndex;
63
86
  private query;
64
87
  private touched;
65
88
  private internals;
89
+ private validityController;
66
90
  private listId;
67
91
  private controlId;
68
92
  private cleanup?;
69
93
  private _value;
94
+ private _fieldsetDisabled;
95
+ private _name;
96
+ private _disabled;
97
+ private _required;
70
98
  private _defaultValue;
71
99
  constructor();
100
+ get form(): HTMLFormElement | null;
101
+ get labels(): NodeList;
102
+ get validity(): ValidityState;
103
+ get validationMessage(): string;
104
+ get willValidate(): boolean;
72
105
  connectedCallback(): void;
73
106
  disconnectedCallback(): void;
74
107
  attributeChangedCallback(name: string, old: string | null, val: string | null): void;
75
108
  /** The current model id (empty string when nothing is selected). */
76
109
  get value(): string;
77
110
  set value(next: string);
111
+ /** The form submission key, reflected synchronously for native form APIs. */
112
+ get name(): string;
113
+ set name(next: string);
114
+ get disabled(): boolean;
115
+ set disabled(next: boolean);
116
+ get required(): boolean;
117
+ set required(next: boolean);
118
+ /** Whether the control is disabled explicitly or by an ancestor fieldset. */
119
+ get effectiveDisabled(): boolean;
78
120
  private updateValidity;
79
121
  formResetCallback(): void;
122
+ formStateRestoreCallback(state: string | File | FormData | null, _mode?: 'restore' | 'autocomplete'): void;
80
123
  formDisabledCallback(disabled: boolean): void;
81
124
  checkValidity(): boolean;
82
125
  reportValidity(): boolean;
@@ -114,6 +157,8 @@ export declare class LyraModelSelect extends LyraElement {
114
157
  private onListboxClick;
115
158
  private renderRows;
116
159
  private renderListbox;
160
+ /** `part="form-control-label"` — see `label`'s doc comment for its precedence over `aria-label`. */
161
+ private renderLabel;
117
162
  private renderClosed;
118
163
  private renderFreeText;
119
164
  render(): TemplateResult;
@@ -4,13 +4,14 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
4
4
  else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
5
5
  return c > 3 && r && Object.defineProperty(target, key, r), r;
6
6
  };
7
- import { html } from 'lit';
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
10
  import { defineElement } from '../../internal/prefix.js';
11
11
  import { place } from '../../internal/positioner.js';
12
12
  import { nextId } from '../../internal/a11y.js';
13
13
  import { chevronIcon } from '../../internal/icons.js';
14
+ import { AnchoredValidityController, VALIDITY_ANCHOR } from '../../internal/anchored-validity.js';
14
15
  import { styles } from './model-select.styles.js';
15
16
  /**
16
17
  * `<lyra-model-select>` — a provider/model picker that renders as a closed
@@ -32,6 +33,8 @@ import { styles } from './model-select.styles.js';
32
33
  *
33
34
  * @customElement lyra-model-select
34
35
  * @event lyra-change - The selected/typed value changed. `detail: { value: string; inCatalog: boolean }`.
36
+ * @csspart form-control-label - The `<label>` element (only rendered — and only contributes to the
37
+ * accessible name — once `label` is non-empty).
35
38
  * @csspart trigger - The trigger button (closed-dropdown mode's positioning anchor).
36
39
  * @csspart combobox - The text-input container (free-text mode's positioning anchor).
37
40
  * @csspart combobox-input - The free-text mode's text input.
@@ -46,7 +49,10 @@ export class LyraModelSelect extends LyraElement {
46
49
  static { this.formAssociated = true; }
47
50
  static { this.styles = [LyraElement.styles, styles]; }
48
51
  static { this.properties = {
52
+ disabled: { type: Boolean, reflect: true, noAccessor: true },
53
+ required: { type: Boolean, reflect: true, noAccessor: true },
49
54
  value: { noAccessor: true },
55
+ name: { reflect: true, noAccessor: true },
50
56
  }; }
51
57
  constructor() {
52
58
  super();
@@ -54,10 +60,17 @@ export class LyraModelSelect extends LyraElement {
54
60
  this.provider = '';
55
61
  /** Let the user type/commit a value that isn't in `catalog`, even when `catalog` is non-empty. */
56
62
  this.allowCustom = false;
63
+ /**
64
+ * Optional visible title above the control, mirroring `lyra-select`'s
65
+ * `label` exactly: rendered via a `part="form-control-label"` `<label>`
66
+ * paired with the control's id, and — once non-empty — it takes over as
67
+ * the accessible-name source (the `aria-label` fallback below is then only
68
+ * emitted when the host has an explicit `aria-label` override, same
69
+ * precedence as `lyra-select`). Leaving it empty (the default) keeps
70
+ * today's `aria-label || placeholder || 'Model'` chain untouched.
71
+ */
72
+ this.label = '';
57
73
  this.placeholder = '';
58
- this.name = '';
59
- this.disabled = false;
60
- this.required = false;
61
74
  this.open = false;
62
75
  this.activeIndex = -1;
63
76
  // Free-text mode's live input text. Only meaningful while `open` — the
@@ -70,6 +83,10 @@ export class LyraModelSelect extends LyraElement {
70
83
  this.listId = nextId('model-select-list');
71
84
  this.controlId = nextId('model-select-control');
72
85
  this._value = '';
86
+ this._fieldsetDisabled = false;
87
+ this._name = '';
88
+ this._disabled = false;
89
+ this._required = false;
73
90
  // What `form.reset()` restores to — captured from the `value` *content
74
91
  // attribute* only, mirroring native `<input>`/`FormAssociated`'s
75
92
  // `_defaultValue` (see internal/form-associated.ts). There's no child
@@ -82,7 +99,7 @@ export class LyraModelSelect extends LyraElement {
82
99
  };
83
100
  // -- Closed-dropdown mode (trigger button) --------------------------------
84
101
  this.onTriggerClick = () => {
85
- if (this.disabled)
102
+ if (this.effectiveDisabled)
86
103
  return;
87
104
  this.open ? this.hide() : this.show();
88
105
  };
@@ -139,7 +156,7 @@ export class LyraModelSelect extends LyraElement {
139
156
  };
140
157
  // -- Free-text mode (text input) ------------------------------------------
141
158
  this.onComboMouseDown = (e) => {
142
- if (this.disabled)
159
+ if (this.effectiveDisabled)
143
160
  return;
144
161
  e.preventDefault();
145
162
  this.renderRoot.querySelector('[part="combobox-input"]')?.focus();
@@ -214,6 +231,8 @@ export class LyraModelSelect extends LyraElement {
214
231
  e.preventDefault();
215
232
  };
216
233
  this.onListboxClick = (e) => {
234
+ if (this.effectiveDisabled)
235
+ return;
217
236
  const optionEl = e.target.closest('[part="option"]');
218
237
  const value = optionEl?.dataset.value;
219
238
  if (value === undefined)
@@ -223,6 +242,30 @@ export class LyraModelSelect extends LyraElement {
223
242
  this.selectEntry(entry);
224
243
  };
225
244
  this.internals = this.attachInternals();
245
+ this.validityController = new AnchoredValidityController(this, this.internals, () => this[VALIDITY_ANCHOR]());
246
+ // Native <input> always has a submission value ("") from construction —
247
+ // without this, a control whose `value` is never touched is entirely
248
+ // absent from FormData instead of present as "" (see form-associated.ts).
249
+ this.internals.setFormValue('');
250
+ }
251
+ get form() {
252
+ return this.internals.form;
253
+ }
254
+ get labels() {
255
+ return this.internals.labels;
256
+ }
257
+ get validity() {
258
+ return this.internals.validity;
259
+ }
260
+ get validationMessage() {
261
+ return this.internals.validationMessage;
262
+ }
263
+ get willValidate() {
264
+ return this.internals.willValidate;
265
+ }
266
+ /** @internal */
267
+ [VALIDITY_ANCHOR]() {
268
+ return this.renderRoot?.querySelector('[part="trigger"], [part="combobox-input"]') ?? null;
226
269
  }
227
270
  connectedCallback() {
228
271
  super.connectedCallback();
@@ -231,7 +274,14 @@ export class LyraModelSelect extends LyraElement {
231
274
  disconnectedCallback() {
232
275
  super.disconnectedCallback();
233
276
  this.cleanup?.();
277
+ this.cleanup = undefined;
234
278
  document.removeEventListener('pointerdown', this.onDocPointer);
279
+ // Reset so a reconnect (e.g. a drag-drop reparent) re-triggers
280
+ // `updated()`'s `open`-driven branch -- without this, `open` stays
281
+ // `true` across the disconnect/reconnect and `changed.has('open')` never
282
+ // fires again, leaving the listbox rendered open with no positioning and
283
+ // no outside-click listener.
284
+ this.open = false;
235
285
  }
236
286
  attributeChangedCallback(name, old, val) {
237
287
  super.attributeChangedCallback(name, old, val);
@@ -249,19 +299,65 @@ export class LyraModelSelect extends LyraElement {
249
299
  this.updateValidity();
250
300
  this.requestUpdate('value', old);
251
301
  }
302
+ /** The form submission key, reflected synchronously for native form APIs. */
303
+ get name() {
304
+ return this._name;
305
+ }
306
+ set name(next) {
307
+ const old = this._name;
308
+ this._name = next ?? '';
309
+ if (this._name) {
310
+ this.setAttribute('name', this._name);
311
+ }
312
+ else {
313
+ this.removeAttribute('name');
314
+ }
315
+ this.requestUpdate('name', old);
316
+ }
317
+ get disabled() {
318
+ return this._disabled;
319
+ }
320
+ set disabled(next) {
321
+ const old = this._disabled;
322
+ this._disabled = Boolean(next);
323
+ this.toggleAttribute('disabled', this._disabled);
324
+ if (this._disabled)
325
+ this.hide();
326
+ this.requestUpdate('disabled', old);
327
+ }
328
+ get required() {
329
+ return this._required;
330
+ }
331
+ set required(next) {
332
+ const old = this._required;
333
+ this._required = Boolean(next);
334
+ this.toggleAttribute('required', this._required);
335
+ this.updateValidity();
336
+ this.requestUpdate('required', old);
337
+ }
338
+ /** Whether the control is disabled explicitly or by an ancestor fieldset. */
339
+ get effectiveDisabled() {
340
+ return this.disabled || this._fieldsetDisabled;
341
+ }
252
342
  updateValidity() {
253
343
  if (this.required && !this._value) {
254
- this.internals.setValidity({ valueMissing: true }, 'Please choose a model.');
344
+ this.validityController.setValidity({ valueMissing: true }, 'Please choose a model.');
255
345
  }
256
346
  else {
257
- this.internals.setValidity({});
347
+ this.validityController.setValidity({});
258
348
  }
259
349
  }
260
350
  formResetCallback() {
261
351
  this.value = this._defaultValue;
262
352
  }
353
+ formStateRestoreCallback(state, _mode) {
354
+ this.value = typeof state === 'string' ? state : '';
355
+ }
263
356
  formDisabledCallback(disabled) {
264
- this.disabled = disabled;
357
+ this._fieldsetDisabled = disabled;
358
+ if (disabled)
359
+ this.hide();
360
+ this.requestUpdate();
265
361
  }
266
362
  checkValidity() {
267
363
  return this.internals.checkValidity();
@@ -307,7 +403,7 @@ export class LyraModelSelect extends LyraElement {
307
403
  return this.effectiveEntries.find((e) => e.id === id)?.label ?? id;
308
404
  }
309
405
  show() {
310
- if (this.open || this.disabled)
406
+ if (this.open || this.effectiveDisabled)
311
407
  return;
312
408
  this.open = true;
313
409
  }
@@ -388,11 +484,17 @@ export class LyraModelSelect extends LyraElement {
388
484
  </div>
389
485
  `;
390
486
  }
487
+ /** `part="form-control-label"` — see `label`'s doc comment for its precedence over `aria-label`. */
488
+ renderLabel() {
489
+ return html `<label part="form-control-label" for=${this.controlId} ?hidden=${!this.label}>${this.label}</label>`;
490
+ }
391
491
  renderClosed() {
392
492
  const rows = this.effectiveEntries;
393
493
  const activeId = this.activeIndex >= 0 && rows[this.activeIndex] ? `${this.listId}-opt-${this.activeIndex}` : '';
394
494
  const hasValue = this._value.length > 0;
495
+ const hasLabel = this.label.length > 0;
395
496
  return html `
497
+ ${this.renderLabel()}
396
498
  <button
397
499
  id=${this.controlId}
398
500
  part="trigger"
@@ -402,10 +504,10 @@ export class LyraModelSelect extends LyraElement {
402
504
  aria-expanded=${this.open ? 'true' : 'false'}
403
505
  aria-controls=${this.listId}
404
506
  aria-activedescendant=${activeId}
405
- aria-label=${this.getAttribute('aria-label') || this.placeholder || 'Model'}
507
+ aria-label=${this.getAttribute('aria-label') || (hasLabel ? nothing : this.placeholder || 'Model')}
406
508
  aria-required=${this.required ? 'true' : 'false'}
407
509
  aria-invalid=${this.touched && !this.internals.validity.valid ? 'true' : 'false'}
408
- ?disabled=${this.disabled}
510
+ ?disabled=${this.effectiveDisabled}
409
511
  @click=${this.onTriggerClick}
410
512
  @keydown=${this.onTriggerKeyDown}
411
513
  @blur=${this.onTriggerBlur}
@@ -422,14 +524,16 @@ export class LyraModelSelect extends LyraElement {
422
524
  renderFreeText() {
423
525
  const rows = this.filteredEntries;
424
526
  const activeId = this.activeIndex >= 0 && rows[this.activeIndex] ? `${this.listId}-opt-${this.activeIndex}` : '';
527
+ const hasLabel = this.label.length > 0;
425
528
  return html `
529
+ ${this.renderLabel()}
426
530
  <div part="combobox" @mousedown=${this.onComboMouseDown}>
427
531
  ${this.provider ? html `<span part="provider-badge">${this.provider}</span>` : ''}
428
532
  <input
429
533
  id=${this.controlId}
430
534
  part="combobox-input"
431
535
  role="combobox"
432
- aria-label=${this.getAttribute('aria-label') || this.placeholder || 'Model'}
536
+ aria-label=${this.getAttribute('aria-label') || (hasLabel ? nothing : this.placeholder || 'Model')}
433
537
  aria-expanded=${this.open ? 'true' : 'false'}
434
538
  aria-controls=${this.listId}
435
539
  aria-activedescendant=${activeId}
@@ -439,7 +543,7 @@ export class LyraModelSelect extends LyraElement {
439
543
  autocomplete="off"
440
544
  .value=${this.open ? this.query : this.labelFor(this._value)}
441
545
  placeholder=${this.placeholder}
442
- ?disabled=${this.disabled}
546
+ ?disabled=${this.effectiveDisabled}
443
547
  @input=${this.onInput}
444
548
  @keydown=${this.onInputKeyDown}
445
549
  @focus=${this.onInputFocus}
@@ -465,16 +569,10 @@ __decorate([
465
569
  ], LyraModelSelect.prototype, "allowCustom", void 0);
466
570
  __decorate([
467
571
  property()
468
- ], LyraModelSelect.prototype, "placeholder", void 0);
572
+ ], LyraModelSelect.prototype, "label", void 0);
469
573
  __decorate([
470
574
  property()
471
- ], LyraModelSelect.prototype, "name", void 0);
472
- __decorate([
473
- property({ type: Boolean, reflect: true })
474
- ], LyraModelSelect.prototype, "disabled", void 0);
475
- __decorate([
476
- property({ type: Boolean, reflect: true })
477
- ], LyraModelSelect.prototype, "required", void 0);
575
+ ], LyraModelSelect.prototype, "placeholder", void 0);
478
576
  __decorate([
479
577
  property({ type: Boolean, reflect: true })
480
578
  ], LyraModelSelect.prototype, "open", void 0);
@@ -6,10 +6,24 @@ export const styles = css `
6
6
  inline-size: 100%;
7
7
  max-inline-size: 24rem;
8
8
  }
9
- :host([disabled]) {
9
+ :host(:disabled) {
10
10
  cursor: not-allowed;
11
11
  }
12
12
 
13
+ [part='form-control-label'] {
14
+ display: block;
15
+ margin-block-end: var(--lyra-space-xs);
16
+ font-size: 0.875rem;
17
+ font-weight: 600;
18
+ }
19
+ /* :empty never matches here -- the part always renders a literal label
20
+ element regardless of label content -- so real emptiness is tracked via
21
+ the label property length and reflected through the hidden attribute
22
+ instead (same fix as lyra-select's own form-control-label). */
23
+ [part='form-control-label'][hidden] {
24
+ display: none;
25
+ }
26
+
13
27
  [part='trigger'],
14
28
  [part='combobox'] {
15
29
  display: flex;
@@ -40,8 +54,8 @@ export const styles = css `
40
54
  :host([open]) [part='trigger'] {
41
55
  border-color: var(--lyra-color-brand);
42
56
  }
43
- :host([disabled]) [part='trigger'],
44
- :host([disabled]) [part='combobox'] {
57
+ :host(:disabled) [part='trigger'],
58
+ :host(:disabled) [part='combobox'] {
45
59
  opacity: var(--lyra-opacity-disabled);
46
60
  cursor: not-allowed;
47
61
  }