@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,47 +7,12 @@ 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, collectFocusableElements, 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 { nextId } from '../../internal/a11y.js';
13
14
  import { chevronIcon, closeIcon, expandIcon } from '../../internal/icons.js';
14
15
  import { styles } from './widget.styles.js';
15
- const FOCUSABLE_SELECTOR = 'a[href], button:not([disabled]), input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])';
16
- // Shadow-piercing so a slotted custom element's real focusable target (e.g.
17
- // an <input> inside its own shadow root) is found even though the host tag
18
- // itself doesn't match FOCUSABLE_SELECTOR.
19
- function collectFocusable(el) {
20
- const result = [];
21
- if (el.matches(FOCUSABLE_SELECTOR)) {
22
- result.push(el);
23
- }
24
- if (el instanceof HTMLSlotElement) {
25
- for (const assigned of el.assignedElements({ flatten: true })) {
26
- result.push(...collectFocusable(assigned));
27
- }
28
- return result;
29
- }
30
- const container = el.shadowRoot ?? el;
31
- for (const child of Array.from(container.children)) {
32
- result.push(...collectFocusable(child));
33
- }
34
- return result;
35
- }
36
- /**
37
- * Whether `el` is actually laid out/paintable — used to drop e.g. a
38
- * `[collapsed]` body's slotted content from the fullscreen tab order.
39
- * `el.offsetParent !== null` is the usual shorthand for this, but it's
40
- * unreliable here: it resolves `null` for an element whose nearest
41
- * *positioned* ancestor (fullscreen's `[part="base"] { position: fixed }`)
42
- * lives across a slot-projection boundary from the element itself (true for
43
- * every element this scans, since they're all slotted content), even though
44
- * the element is genuinely rendered. `checkVisibility()` (falling back to
45
- * `getClientRects().length` on engines without it) correctly follows
46
- * flattened-tree/slot assignment instead.
47
- */
48
- function isRendered(el) {
49
- return el.checkVisibility ? el.checkVisibility() : el.getClientRects().length > 0;
50
- }
51
16
  /**
52
17
  * `<lyra-widget>` — a titled panel shell with an optional collapse toggle and
53
18
  * an optional fullscreen-expand toggle. Fullscreen promotes the same host
@@ -99,48 +64,19 @@ export class LyraWidget extends LyraElement {
99
64
  this.emit('lyra-collapse-change', this.collapsed);
100
65
  };
101
66
  this.toggleFullscreen = (e) => {
102
- this.lastTrigger = e.currentTarget;
67
+ if (!this.fullscreen)
68
+ this.explicitTrigger = e.currentTarget;
103
69
  this.fullscreen = !this.fullscreen;
104
70
  this.emit('lyra-fullscreen-change', this.fullscreen);
105
- if (!this.fullscreen) {
106
- this.lastTrigger?.focus();
107
- }
108
71
  };
109
- this.onDocKeyDown = (e) => {
110
- if (!this.fullscreen)
111
- return;
112
- if (e.key === 'Escape') {
113
- e.preventDefault();
114
- this.fullscreen = false;
115
- this.emit('lyra-fullscreen-change', false);
116
- this.lastTrigger?.focus();
117
- return;
118
- }
119
- if (e.key !== 'Tab')
120
- return;
121
- const focusable = this.getFocusableElements();
122
- if (focusable.length === 0) {
123
- e.preventDefault();
124
- return;
125
- }
126
- const first = focusable[0];
127
- const last = focusable[focusable.length - 1];
128
- const active = this.getActiveElement();
129
- if (e.shiftKey && active === first) {
130
- e.preventDefault();
131
- last.focus();
132
- }
133
- else if (!e.shiftKey && active === last) {
134
- e.preventDefault();
135
- first.focus();
136
- }
137
- };
138
- this.onBackdropClick = () => {
72
+ this.dismissFullscreen = () => {
139
73
  if (!this.fullscreen)
140
74
  return;
141
75
  this.fullscreen = false;
142
76
  this.emit('lyra-fullscreen-change', false);
143
- this.lastTrigger?.focus();
77
+ };
78
+ this.onBackdropClick = () => {
79
+ this.overlayHandle?.dismissBackdrop();
144
80
  };
145
81
  }
146
82
  static { this.styles = [LyraElement.styles, styles]; }
@@ -150,13 +86,10 @@ export class LyraWidget extends LyraElement {
150
86
  }
151
87
  if (changed.has('fullscreen')) {
152
88
  if (this.fullscreen) {
153
- this.releaseScrollLock = lockScroll();
154
- document.addEventListener('keydown', this.onDocKeyDown);
89
+ this.activateFullscreenOverlay();
155
90
  }
156
91
  else {
157
- this.releaseScrollLock?.();
158
- this.releaseScrollLock = undefined;
159
- document.removeEventListener('keydown', this.onDocKeyDown);
92
+ this.deactivateFullscreenOverlay();
160
93
  }
161
94
  }
162
95
  }
@@ -167,14 +100,23 @@ export class LyraWidget extends LyraElement {
167
100
  // because a mouse click natively focuses the button that triggered it --
168
101
  // not true for a directly-set `fullscreen` property, and not guaranteed
169
102
  // for every input method/browser even in the click case.
103
+ //
104
+ // The `collapsed` branch covers the same modal-focus-trap requirement for
105
+ // a second case: collapsing the body while fullscreen hides (display:none)
106
+ // whatever was focused inside it, which the browser resolves by silently
107
+ // moving focus outside the panel. The shared manager then reclaims it while
108
+ // preserving focus that is still on one of the visible header controls.
170
109
  updated(changed) {
171
110
  if (changed.has('fullscreen') && this.fullscreen) {
172
- const first = this.getFocusableElements()[0];
173
- if (first) {
174
- first.focus();
175
- }
176
- else {
177
- this.shadowRoot?.querySelector('[part="base"]')?.focus();
111
+ this.overlayHandle?.focusInitial();
112
+ }
113
+ else if (changed.has('collapsed') && this.fullscreen) {
114
+ const panel = this.shadowRoot?.querySelector('[part="base"]');
115
+ const active = deepActiveElement(this.ownerDocument);
116
+ if (panel && !collectFocusableElements(panel).includes(active)) {
117
+ if (active && typeof active.blur === 'function')
118
+ active.blur();
119
+ this.overlayHandle?.focusInitial();
178
120
  }
179
121
  }
180
122
  }
@@ -183,49 +125,42 @@ export class LyraWidget extends LyraElement {
183
125
  // A reconnect (e.g. a drag-and-drop reparent that keeps this same
184
126
  // element instance) fires disconnectedCallback then connectedCallback
185
127
  // synchronously with no update in between, so willUpdate never reruns
186
- // to notice `fullscreen` is still true. Restore the scroll lock/trap it
187
- // dropped. `hasUpdated` excludes the initial mount, where willUpdate's
188
- // first pass already establishes them from the starting property value.
189
- if (this.hasUpdated && this.fullscreen && !this.releaseScrollLock) {
190
- this.releaseScrollLock = lockScroll();
191
- document.addEventListener('keydown', this.onDocKeyDown);
128
+ // to notice `fullscreen` is still true. Restore the shared overlay
129
+ // registration and scroll lock it dropped.
130
+ if (this.hasUpdated && this.fullscreen) {
131
+ if (this.overlayHandle?.isActive()) {
132
+ this.overlayHandle.resume();
133
+ }
134
+ else {
135
+ this.activateFullscreenOverlay();
136
+ }
137
+ if (!this.releaseScrollLock)
138
+ this.releaseScrollLock = lockScroll(this.ownerDocument);
139
+ queueMicrotask(() => this.overlayHandle?.focusInitial());
192
140
  }
193
141
  }
194
142
  disconnectedCallback() {
195
143
  super.disconnectedCallback();
196
144
  this.releaseScrollLock?.();
197
145
  this.releaseScrollLock = undefined;
198
- document.removeEventListener('keydown', this.onDocKeyDown);
146
+ this.overlayHandle?.suspend();
199
147
  }
200
- // Bounds Tab/Shift+Tab to the panel while fullscreen (a modal presentation)
201
- // so keyboard focus can't escape to page content hidden behind the
202
- // backdrop. Order follows the header's actions slot, then the collapse/
203
- // fullscreen buttons, then the body slot -- the same order the flattened
204
- // tree already tabs through.
205
- getFocusableElements() {
206
- const root = this.shadowRoot;
207
- if (!root)
208
- return [];
209
- const fromSlot = (selector) => {
210
- const slot = root.querySelector(selector);
211
- return slot ? slot.assignedElements({ flatten: true }).flatMap(collectFocusable) : [];
212
- };
213
- const shadowButtons = Array.from(root.querySelectorAll('[part="collapse-button"], [part="fullscreen-button"]'));
214
- return [
215
- ...fromSlot('slot[name="actions"]'),
216
- ...shadowButtons,
217
- ...fromSlot('[part="body"] slot:not([name])'),
218
- ].filter(isRendered);
148
+ activateFullscreenOverlay() {
149
+ this.releaseScrollLock ??= lockScroll(this.ownerDocument);
150
+ this.overlayHandle = activateOverlay({
151
+ host: this,
152
+ panel: () => this.shadowRoot?.querySelector('[part="base"]') ?? null,
153
+ onEscape: this.dismissFullscreen,
154
+ onBackdrop: this.dismissFullscreen,
155
+ restoreFocusTo: this.explicitTrigger,
156
+ });
157
+ this.explicitTrigger = undefined;
219
158
  }
220
- getActiveElement() {
221
- let active = document.activeElement;
222
- while (active) {
223
- const inner = active.shadowRoot?.activeElement ?? null;
224
- if (!inner)
225
- break;
226
- active = inner;
227
- }
228
- return active;
159
+ deactivateFullscreenOverlay() {
160
+ this.releaseScrollLock?.();
161
+ this.releaseScrollLock = undefined;
162
+ this.overlayHandle?.deactivate();
163
+ this.overlayHandle = undefined;
229
164
  }
230
165
  render() {
231
166
  const hasLabel = this.label.length > 0;
@@ -261,11 +196,10 @@ export class LyraWidget extends LyraElement {
261
196
  aria-expanded=${this.collapsed ? 'false' : 'true'}
262
197
  aria-label=${this.collapsed ? 'Expand panel' : 'Collapse panel'}
263
198
  aria-controls=${this.bodyId}
199
+ style="transform:rotate(${this.collapsed ? '0deg' : '90deg'})"
264
200
  @click=${this.toggleCollapsed}
265
201
  >
266
- <span style="display:inline-flex;transform:rotate(${this.collapsed ? '0deg' : '90deg'})"
267
- >${chevronIcon()}</span
268
- >
202
+ ${chevronIcon()}
269
203
  </button>`
270
204
  : nothing}
271
205
  ${this.expandable
@@ -29,6 +29,12 @@ export const styles = css `
29
29
  flex: 1 1 auto;
30
30
  min-inline-size: 0;
31
31
  }
32
+ [part='label'],
33
+ [part='sublabel'] {
34
+ overflow: hidden;
35
+ text-overflow: ellipsis;
36
+ white-space: nowrap;
37
+ }
32
38
  [part='label'] {
33
39
  font-weight: 600;
34
40
  }
@@ -80,12 +86,12 @@ export const styles = css `
80
86
  position: fixed;
81
87
  inset: var(--lyra-widget-fullscreen-inset, 0);
82
88
  background: var(--lyra-widget-overlay-color);
83
- z-index: 999;
89
+ z-index: var(--lyra-overlay-stack-index, 1000);
84
90
  }
85
91
  :host([fullscreen]) [part='base'] {
86
92
  position: fixed;
87
93
  inset: var(--lyra-widget-fullscreen-inset);
88
- z-index: 1000;
94
+ z-index: calc(var(--lyra-overlay-stack-index, 1000) + 1);
89
95
  box-shadow: var(--lyra-shadow);
90
96
  }
91
97
  :host([fullscreen]) [part='body'] {
@@ -52,6 +52,13 @@ export interface WordCloudLayoutResult {
52
52
  }
53
53
  /** DOM-node/compute-time safety cap — mirrors lyra-sparkline's `MAX_BARS`. */
54
54
  export declare const MAX_WORDS = 150;
55
+ /** Largest accepted font size in CSS pixels. This keeps measurement and
56
+ * placement geometry finite enough for an interactive render even when a
57
+ * caller supplies an otherwise-valid but impractically large number. */
58
+ export declare const MAX_FONT_SIZE_PX = 512;
59
+ /** Maximum number of candidate positions tested for any one word. Together
60
+ * with `MAX_WORDS`, this puts a hard upper bound on spiral-search work. */
61
+ export declare const MAX_SPIRAL_ITERATIONS = 4096;
55
62
  /** Minimum gap enforced both between placed words and around the returned bounding box. */
56
63
  export declare const GAP = 3;
57
64
  /**
@@ -1,5 +1,12 @@
1
1
  /** DOM-node/compute-time safety cap — mirrors lyra-sparkline's `MAX_BARS`. */
2
2
  export const MAX_WORDS = 150;
3
+ /** Largest accepted font size in CSS pixels. This keeps measurement and
4
+ * placement geometry finite enough for an interactive render even when a
5
+ * caller supplies an otherwise-valid but impractically large number. */
6
+ export const MAX_FONT_SIZE_PX = 512;
7
+ /** Maximum number of candidate positions tested for any one word. Together
8
+ * with `MAX_WORDS`, this puts a hard upper bound on spiral-search work. */
9
+ export const MAX_SPIRAL_ITERATIONS = 4096;
3
10
  /** Minimum gap enforced both between placed words and around the returned bounding box. */
4
11
  export const GAP = 3;
5
12
  const ROTATE_PROBABILITY = 0.25;
@@ -13,12 +20,17 @@ const MAX_RADIUS_AREA_FACTOR = 6;
13
20
  * `minFontSize`/`maxFontSize` input. */
14
21
  const MIN_SANE_FONT_SIZE = 1;
15
22
  /** Normalizes possibly-invalid minFontSize/maxFontSize into a finite,
16
- * positive, correctly-ordered pair — non-finite/negative inputs fall back
17
- * to MIN_SANE_FONT_SIZE, and a reversed pair (max < min) is swapped rather
18
- * than left to invert the weight-to-size mapping. */
23
+ * positive, bounded, correctly-ordered pair — non-finite/negative inputs
24
+ * fall back to MIN_SANE_FONT_SIZE, huge finite values are capped, and a
25
+ * reversed pair (max < min) is swapped rather than left to invert the
26
+ * weight-to-size mapping. */
19
27
  function resolveFontSizeBounds(minFontSize, maxFontSize) {
20
- const min = Number.isFinite(minFontSize) && minFontSize > 0 ? minFontSize : MIN_SANE_FONT_SIZE;
21
- const max = Number.isFinite(maxFontSize) && maxFontSize > 0 ? maxFontSize : MIN_SANE_FONT_SIZE;
28
+ const min = Number.isFinite(minFontSize) && minFontSize > 0
29
+ ? Math.min(minFontSize, MAX_FONT_SIZE_PX)
30
+ : MIN_SANE_FONT_SIZE;
31
+ const max = Number.isFinite(maxFontSize) && maxFontSize > 0
32
+ ? Math.min(maxFontSize, MAX_FONT_SIZE_PX)
33
+ : MIN_SANE_FONT_SIZE;
22
34
  return min <= max ? [min, max] : [max, min];
23
35
  }
24
36
  /** Clamps a (possibly negative/non-finite) input weight for scale math only —
@@ -89,7 +101,9 @@ export function layoutWordCloud(words, options) {
89
101
  let x = 0;
90
102
  let y = 0;
91
103
  let foundSpot = placed.length === 0;
92
- while (!foundSpot && radius < maxRadius) {
104
+ let iterations = 0;
105
+ while (!foundSpot && radius < maxRadius && iterations < MAX_SPIRAL_ITERATIONS) {
106
+ iterations++;
93
107
  x = radius * Math.cos(theta);
94
108
  y = radius * Math.sin(theta);
95
109
  let collides = false;
@@ -16,12 +16,19 @@ export type { WordCloudWord };
16
16
  * Instead, like `lyra-heatmap`'s cells, the whole `[part="svg"]` is one tab
17
17
  * stop with roving arrow-key focus (Home/End jump to the first/last word,
18
18
  * Enter/Space activates the focused one), a drawn `[part="focus-ring"]`, and
19
- * a visually-hidden `aria-live="polite"` status announcement.
19
+ * a visually-hidden `aria-live="polite"` status announcement. The SVG focus
20
+ * target carries the group role and accessible name so the semantics follow
21
+ * the element a keyboard user actually focuses.
20
22
  *
21
23
  * @customElement lyra-word-cloud
22
24
  * @event lyra-word-click - Fired on click, or Enter/Space on the focused word.
23
25
  * `detail: { text, weight, group }`.
24
- * @csspart base, svg, word, focus-ring, live-region, empty
26
+ * @csspart base - The word-cloud wrapper.
27
+ * @csspart svg - The word-cloud SVG.
28
+ * @csspart word - A rendered word.
29
+ * @csspart focus-ring - The keyboard focus ring.
30
+ * @csspart live-region - The visually hidden announcement region.
31
+ * @csspart empty - The empty-state message.
25
32
  */
26
33
  export declare class LyraWordCloud extends LyraElement {
27
34
  static styles: import("lit").CSSResultGroup[];
@@ -43,6 +50,19 @@ export declare class LyraWordCloud extends LyraElement {
43
50
  private focusedIndex;
44
51
  /** Text of the visually-hidden `aria-live="polite"` status announcement. */
45
52
  private liveText;
53
+ /**
54
+ * Whether `role`/`aria-label` were present on the host *before* this
55
+ * element's own code ever wrote to them -- snapshotted once, in the very
56
+ * first `willUpdate()` (guarded by `hasUpdated`, which Lit only flips to
57
+ * `true` after that first update commits). Re-checking `hasAttribute()` on
58
+ * every update instead would self-poison: once this element writes its own
59
+ * default `aria-label` on the first render, `hasAttribute('aria-label')` is
60
+ * permanently `true` afterwards, which would wrongly look like an
61
+ * author-supplied value and freeze the label instead of refreshing it as
62
+ * `words` changes on later updates.
63
+ */
64
+ private authorSuppliedRole;
65
+ private authorSuppliedAriaLabel;
46
66
  private measureText;
47
67
  private fontFamily;
48
68
  private paletteColors;
@@ -53,12 +53,19 @@ function warnSkippedWords(count) {
53
53
  * Instead, like `lyra-heatmap`'s cells, the whole `[part="svg"]` is one tab
54
54
  * stop with roving arrow-key focus (Home/End jump to the first/last word,
55
55
  * Enter/Space activates the focused one), a drawn `[part="focus-ring"]`, and
56
- * a visually-hidden `aria-live="polite"` status announcement.
56
+ * a visually-hidden `aria-live="polite"` status announcement. The SVG focus
57
+ * target carries the group role and accessible name so the semantics follow
58
+ * the element a keyboard user actually focuses.
57
59
  *
58
60
  * @customElement lyra-word-cloud
59
61
  * @event lyra-word-click - Fired on click, or Enter/Space on the focused word.
60
62
  * `detail: { text, weight, group }`.
61
- * @csspart base, svg, word, focus-ring, live-region, empty
63
+ * @csspart base - The word-cloud wrapper.
64
+ * @csspart svg - The word-cloud SVG.
65
+ * @csspart word - A rendered word.
66
+ * @csspart focus-ring - The keyboard focus ring.
67
+ * @csspart live-region - The visually hidden announcement region.
68
+ * @csspart empty - The empty-state message.
62
69
  */
63
70
  export class LyraWordCloud extends LyraElement {
64
71
  constructor() {
@@ -78,6 +85,19 @@ export class LyraWordCloud extends LyraElement {
78
85
  this.focusedIndex = null;
79
86
  /** Text of the visually-hidden `aria-live="polite"` status announcement. */
80
87
  this.liveText = '';
88
+ /**
89
+ * Whether `role`/`aria-label` were present on the host *before* this
90
+ * element's own code ever wrote to them -- snapshotted once, in the very
91
+ * first `willUpdate()` (guarded by `hasUpdated`, which Lit only flips to
92
+ * `true` after that first update commits). Re-checking `hasAttribute()` on
93
+ * every update instead would self-poison: once this element writes its own
94
+ * default `aria-label` on the first render, `hasAttribute('aria-label')` is
95
+ * permanently `true` afterwards, which would wrongly look like an
96
+ * author-supplied value and freeze the label instead of refreshing it as
97
+ * `words` changes on later updates.
98
+ */
99
+ this.authorSuppliedRole = false;
100
+ this.authorSuppliedAriaLabel = false;
81
101
  this.measureText = (text, fontSize) => {
82
102
  const ctx = getScratchCtx();
83
103
  if (!ctx)
@@ -152,8 +172,19 @@ export class LyraWordCloud extends LyraElement {
152
172
  changed.has('orientations')) {
153
173
  this.relayout();
154
174
  }
155
- this.setAttribute('role', 'group');
156
- this.setAttribute('aria-label', `Word cloud of ${this.cachedLayout.placed.length} word${this.cachedLayout.placed.length === 1 ? '' : 's'}`);
175
+ if (!this.hasUpdated) {
176
+ this.authorSuppliedRole = this.hasAttribute('role');
177
+ this.authorSuppliedAriaLabel = this.hasAttribute('aria-label');
178
+ }
179
+ // Only default role/aria-label when the author hasn't already supplied
180
+ // one -- unconditionally overwriting them on every update (including
181
+ // ones triggered purely by a `words` re-layout) would silently clobber a
182
+ // host's own accessible name/role on the very next data refresh.
183
+ if (!this.authorSuppliedRole)
184
+ this.setAttribute('role', 'group');
185
+ if (!this.authorSuppliedAriaLabel) {
186
+ this.setAttribute('aria-label', `Word cloud of ${this.cachedLayout.placed.length} word${this.cachedLayout.placed.length === 1 ? '' : 's'}`);
187
+ }
157
188
  }
158
189
  relayout() {
159
190
  this.cachedLayout = layoutWordCloud(this.words, {
@@ -217,7 +248,15 @@ export class LyraWordCloud extends LyraElement {
217
248
  const ring = focused ? this.focusRingRect(focused) : undefined;
218
249
  return html `
219
250
  <div part="base">
220
- <svg part="svg" tabindex="0" viewBox="0 0 ${layout.width} ${layout.height}" @keydown=${this.onKeyDown}>
251
+ <svg
252
+ part="svg"
253
+ role="group"
254
+ aria-label=${this.getAttribute('aria-label') ?? nothing}
255
+ aria-describedby="live-region"
256
+ tabindex="0"
257
+ viewBox="0 0 ${layout.width} ${layout.height}"
258
+ @keydown=${this.onKeyDown}
259
+ >
221
260
  ${layout.placed.map((w) => svg `<text
222
261
  part="word"
223
262
  x=${w.x}
@@ -231,7 +270,7 @@ export class LyraWordCloud extends LyraElement {
231
270
  ? svg `<rect part="focus-ring" x=${ring.x} y=${ring.y} width=${ring.width} height=${ring.height}></rect>`
232
271
  : ''}
233
272
  </svg>
234
- <div part="live-region" class="sr-only" role="status" aria-live="polite">${this.liveText}</div>
273
+ <div id="live-region" part="live-region" class="sr-only" role="status" aria-live="polite">${this.liveText}</div>
235
274
  </div>
236
275
  `;
237
276
  }
@@ -16,6 +16,18 @@ export const styles = css `
16
16
  --lyra-word-cloud-color-7: #0a7d91;
17
17
  --lyra-word-cloud-color-8: #57606a;
18
18
  }
19
+ @media (prefers-color-scheme: dark) {
20
+ :host {
21
+ --lyra-word-cloud-color-1: var(--wa-color-brand-fill-loud, #4ea0f0);
22
+ --lyra-word-cloud-color-2: var(--wa-color-success-fill-loud, #3fb950);
23
+ --lyra-word-cloud-color-3: var(--wa-color-warning-fill-loud, #d29922);
24
+ --lyra-word-cloud-color-4: var(--wa-color-danger-fill-loud, #fa524a);
25
+ --lyra-word-cloud-color-5: #b083f5;
26
+ --lyra-word-cloud-color-6: #f470b8;
27
+ --lyra-word-cloud-color-7: #52d6e8;
28
+ --lyra-word-cloud-color-8: #c9d1d9;
29
+ }
30
+ }
19
31
  [part='base'] {
20
32
  display: flex;
21
33
  flex-direction: column;
@@ -46,7 +58,7 @@ export const styles = css `
46
58
  [part='focus-ring'] {
47
59
  fill: none;
48
60
  stroke: var(--lyra-focus-ring-color);
49
- stroke-width: 2px;
61
+ stroke-width: var(--lyra-focus-ring-width);
50
62
  pointer-events: none;
51
63
  }
52
64
  [part='empty'] {
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,86 @@
1
+ /** @internal */
2
+ export const VALIDITY_ANCHOR = Symbol('lyra-validity-anchor');
3
+ /** @internal */
4
+ export const SET_ANCHORED_VALIDITY = Symbol('lyra-set-anchored-validity');
5
+ /** @internal */
6
+ export function resolveValidityAnchor(provider) {
7
+ if (!provider || (typeof provider !== 'object' && typeof provider !== 'function'))
8
+ return undefined;
9
+ const resolver = provider[VALIDITY_ANCHOR];
10
+ if (typeof resolver !== 'function')
11
+ return undefined;
12
+ return resolver.call(provider) ?? undefined;
13
+ }
14
+ /**
15
+ * Keeps ElementInternals validity synchronous while refreshing its focus
16
+ * anchor after every render. The refresh is required because validity can be
17
+ * computed before the first shadow render and because conditional templates
18
+ * can replace a previously registered anchor.
19
+ *
20
+ * @internal
21
+ */
22
+ export class AnchoredValidityController {
23
+ constructor(host, internals, getAnchor) {
24
+ this.host = host;
25
+ this.internals = internals;
26
+ this.getAnchor = getAnchor;
27
+ this.flags = {};
28
+ this.message = '';
29
+ this.revision = 0;
30
+ this.refreshToken = 0;
31
+ host.addController(this);
32
+ }
33
+ setValidity(flags = {}, message = '') {
34
+ this.flags = { ...flags };
35
+ this.message = message;
36
+ this.revision += 1;
37
+ this.apply();
38
+ }
39
+ refreshAnchor() {
40
+ if (this.isInvalid())
41
+ this.apply();
42
+ }
43
+ hostUpdated() {
44
+ if (!this.isInvalid())
45
+ return;
46
+ const revision = this.revision;
47
+ const token = ++this.refreshToken;
48
+ // Rebind just after the host's update so focus side effects from an
49
+ // already-visible validation UI cannot schedule a new Lit update from
50
+ // inside hostUpdated(). This microtask runs before updateComplete settles.
51
+ queueMicrotask(() => {
52
+ if (token !== this.refreshToken || revision !== this.revision)
53
+ return;
54
+ if (this.apply())
55
+ return;
56
+ // Nested custom controls may render one microtask after their parent.
57
+ // Retry once so an outer form can anchor inside that nested shadow tree.
58
+ queueMicrotask(() => {
59
+ if (token === this.refreshToken && revision === this.revision)
60
+ this.apply();
61
+ });
62
+ });
63
+ }
64
+ isInvalid() {
65
+ return Object.values(this.flags).some(Boolean);
66
+ }
67
+ /** Returns true when an invalid state was installed with a real anchor. */
68
+ apply() {
69
+ const anchor = this.isInvalid() ? this.getAnchor() : undefined;
70
+ if (anchor?.isConnected) {
71
+ try {
72
+ this.internals.setValidity(this.flags, this.message, anchor);
73
+ return true;
74
+ }
75
+ catch (error) {
76
+ // A stale or incorrectly provided non-descendant anchor must not
77
+ // break the host's update. Preserve validity and fall back to the
78
+ // host until the next fresh render resolves a legal descendant.
79
+ if (!(error instanceof DOMException) || error.name !== 'NotFoundError')
80
+ throw error;
81
+ }
82
+ }
83
+ this.internals.setValidity(this.flags, this.message);
84
+ return false;
85
+ }
86
+ }
@@ -8,9 +8,16 @@ export interface FormAssociatedInterface {
8
8
  disabled: boolean;
9
9
  required: boolean;
10
10
  readonly effectiveDisabled: boolean;
11
+ readonly form: HTMLFormElement | null;
12
+ readonly labels: NodeList;
13
+ readonly validity: ValidityState;
14
+ readonly validationMessage: string;
15
+ readonly willValidate: boolean;
11
16
  setFormValue(next: string): void;
12
17
  checkValidity(): boolean;
13
18
  reportValidity(): boolean;
19
+ formResetCallback(): void;
20
+ formStateRestoreCallback(state: string | File | FormData | null, mode?: 'restore' | 'autocomplete'): void;
14
21
  }
15
22
  /**
16
23
  * Mixin that turns a Lit component into a form-associated custom element via