@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
@@ -10,6 +10,7 @@ import { LyraElement } from '../../internal/lyra-element.js';
10
10
  import { FormAssociated } from '../../internal/form-associated.js';
11
11
  import { defineElement } from '../../internal/prefix.js';
12
12
  import { isRtl } from '../../internal/rtl.js';
13
+ import { finiteNumber, finiteRange } from '../../internal/numbers.js';
13
14
  import { styles } from './slider.styles.js';
14
15
  /** PageUp/PageDown move by a larger increment than a single Arrow step,
15
16
  * matching the WAI-ARIA APG slider pattern's expected keyboard interactions
@@ -23,15 +24,16 @@ function isArrowKey(key) {
23
24
  function isSliderKey(key) {
24
25
  return isArrowKey(key) || key === 'Home' || key === 'End' || key === 'PageUp' || key === 'PageDown';
25
26
  }
26
- /** Number of decimal digits in `n`'s shortest string representation, e.g.
27
- * `0.1` -> 1, `5` -> 0. Used to round a stepped value back to the precision
28
- * `step` itself implies, instead of leaving binary floating-point noise in
29
- * place (e.g. `20.200000000000003`). Mirrors lyra-time-range's identical
30
- * helper. */
27
+ /** Number of decimal places implied by `n`, including exponent notation.
28
+ * `0.1` -> 1, `5` -> 0, and `1e-7` -> 7. Used to round a stepped value back
29
+ * to the precision `step` itself implies, instead of leaving binary
30
+ * floating-point noise in place. */
31
31
  function decimalPlaces(n) {
32
- const str = n.toString();
33
- const dot = str.indexOf('.');
34
- return dot === -1 ? 0 : str.length - dot - 1;
32
+ if (!Number.isFinite(n) || n === 0)
33
+ return 0;
34
+ const [mantissa, exponentText] = n.toExponential().split('e');
35
+ const mantissaPlaces = mantissa.includes('.') ? mantissa.length - mantissa.indexOf('.') - 1 : 0;
36
+ return Math.max(0, mantissaPlaces - Number(exponentText));
35
37
  }
36
38
  /**
37
39
  * `<lyra-slider>` — a numeric range control (e.g. an LLM "temperature"
@@ -49,6 +51,10 @@ function decimalPlaces(n) {
49
51
  * to block submission before that default lands, matching how `required`
50
52
  * isn't a meaningful constraint on a native range input either.
51
53
  *
54
+ * Clicking anywhere on the track (not just the 16px thumb) jumps the thumb
55
+ * to that point and continues the same gesture as a drag, matching native
56
+ * `<input type=range>` click-to-seek.
57
+ *
52
58
  * @customElement lyra-slider
53
59
  * @event lyra-input - Fired continuously during an active drag or a
54
60
  * keyboard step (including OS key-repeat while a key is held), mirroring
@@ -67,9 +73,9 @@ function decimalPlaces(n) {
67
73
  export class LyraSlider extends FormAssociated(LyraElement) {
68
74
  constructor() {
69
75
  super(...arguments);
70
- this.min = 0;
71
- this.max = 100;
72
- this.step = 1;
76
+ this._min = 0;
77
+ this._max = 100;
78
+ this._step = 1;
73
79
  /** Accessible name for the slider, used when no visible label context
74
80
  * exists around it (e.g. no wrapping `<label>` or adjacent heading). Set
75
81
  * as `aria-label` on the interactive `role="slider"` element. A plain
@@ -87,6 +93,8 @@ export class LyraSlider extends FormAssociated(LyraElement) {
87
93
  // multi-touch/robustness parity with lyra-split and lyra-time-range, even
88
94
  // though a single thumb has no per-pointer state of its own to track.
89
95
  this.activePointers = new Set();
96
+ this.changedPointers = new Set();
97
+ this.keyboardChanged = false;
90
98
  this.onKeyDown = (e) => {
91
99
  if (this.effectiveDisabled)
92
100
  return;
@@ -101,27 +109,29 @@ export class LyraSlider extends FormAssociated(LyraElement) {
101
109
  const backwardKey = rtl ? 'ArrowRight' : 'ArrowLeft';
102
110
  if (e.key === forwardKey || e.key === 'ArrowUp') {
103
111
  e.preventDefault();
104
- this.setValue(current + this.step, false);
112
+ this.keyboardChanged = this.setValue(current + this.step, false) || this.keyboardChanged;
105
113
  }
106
114
  else if (e.key === backwardKey || e.key === 'ArrowDown') {
107
115
  e.preventDefault();
108
- this.setValue(current - this.step, false);
116
+ this.keyboardChanged = this.setValue(current - this.step, false) || this.keyboardChanged;
109
117
  }
110
118
  else if (e.key === 'PageUp') {
111
119
  e.preventDefault();
112
- this.setValue(current + this.step * PAGE_STEP_MULTIPLIER, false);
120
+ this.keyboardChanged =
121
+ this.setValue(current + this.step * PAGE_STEP_MULTIPLIER, false) || this.keyboardChanged;
113
122
  }
114
123
  else if (e.key === 'PageDown') {
115
124
  e.preventDefault();
116
- this.setValue(current - this.step * PAGE_STEP_MULTIPLIER, false);
125
+ this.keyboardChanged =
126
+ this.setValue(current - this.step * PAGE_STEP_MULTIPLIER, false) || this.keyboardChanged;
117
127
  }
118
128
  else if (e.key === 'Home') {
119
129
  e.preventDefault();
120
- this.setValue(this.domain().lo, false);
130
+ this.keyboardChanged = this.setValue(this.domain().lo, false) || this.keyboardChanged;
121
131
  }
122
132
  else if (e.key === 'End') {
123
133
  e.preventDefault();
124
- this.setValue(this.domain().hi, false);
134
+ this.keyboardChanged = this.setValue(this.domain().hi, false) || this.keyboardChanged;
125
135
  }
126
136
  };
127
137
  this.onKeyUp = (e) => {
@@ -135,20 +145,46 @@ export class LyraSlider extends FormAssociated(LyraElement) {
135
145
  // shape a pointer drag has.
136
146
  if (this.effectiveDisabled || !isSliderKey(e.key))
137
147
  return;
138
- this.emit('lyra-change', { value: this.valueAsNumber });
148
+ if (this.keyboardChanged) {
149
+ this.emit('lyra-change', { value: this.valueAsNumber });
150
+ this.keyboardChanged = false;
151
+ }
139
152
  };
140
153
  this.onPointerDown = (e) => {
141
154
  if (this.effectiveDisabled)
142
155
  return;
143
- this.activePointers.add(e.pointerId);
144
- e.target.setPointerCapture(e.pointerId);
145
- window.addEventListener('pointermove', this.onPointerMove);
146
- window.addEventListener('pointerup', this.onPointerUp);
147
- // A drag can end without a pointerup: a system gesture / palm rejection
148
- // can fire `pointercancel`, and losing capture (e.g. element removed)
149
- // fires `lostpointercapture` both need the same teardown as pointerup.
150
- window.addEventListener('pointercancel', this.onPointerUp);
151
- window.addEventListener('lostpointercapture', this.onPointerUp);
156
+ this.beginDrag(e.pointerId, e.target);
157
+ };
158
+ /** A pointerdown anywhere on `[part="base"]` other than the thumb itself
159
+ * (the vast majority of the control's clickable area) jumps the thumb to
160
+ * that point and continues the same gesture as a drag, mirroring native
161
+ * `<input type=range>` click-to-seek. A pointerdown that started on the
162
+ * thumb bubbles up to this same listener too already fully handled by
163
+ * `onPointerDown` above, so it's ignored here via the `e.target === thumb`
164
+ * check. Unlike the two-handle `lyra-time-range` (where a track click is
165
+ * ambiguous about which handle should move), a single thumb has no such
166
+ * ambiguity. */
167
+ this.onBasePointerDown = (e) => {
168
+ if (this.effectiveDisabled)
169
+ return;
170
+ const thumb = this.renderRoot.querySelector('[part="thumb"]');
171
+ if (!thumb || e.target === thumb)
172
+ return;
173
+ const track = this.renderRoot.querySelector('[part="track"]');
174
+ if (!track)
175
+ return;
176
+ const rect = track.getBoundingClientRect();
177
+ const raw = rect.width === 0 ? 0 : (e.clientX - rect.left) / rect.width;
178
+ // Mirrors onPointerMove's own RTL handling below.
179
+ const ratio = Math.min(1, Math.max(0, isRtl(this) ? 1 - raw : raw));
180
+ const { lo, hi } = this.domain();
181
+ if (this.setValue(lo + ratio * (hi - lo), false))
182
+ this.changedPointers.add(e.pointerId);
183
+ this.beginDrag(e.pointerId, thumb);
184
+ // Keyboard interaction (arrow keys, Home/End, ...) can continue
185
+ // seamlessly right after the click, exactly as if the user had tabbed to
186
+ // the thumb and started dragging it directly.
187
+ thumb.focus();
152
188
  };
153
189
  this.onPointerMove = (e) => {
154
190
  if (!this.activePointers.has(e.pointerId))
@@ -172,16 +208,63 @@ export class LyraSlider extends FormAssociated(LyraElement) {
172
208
  // rightward drag would move the thumb the wrong way.
173
209
  const ratio = Math.min(1, Math.max(0, isRtl(this) ? 1 - raw : raw));
174
210
  const { lo, hi } = this.domain();
175
- this.setValue(lo + ratio * (hi - lo), false);
211
+ if (this.setValue(lo + ratio * (hi - lo), false))
212
+ this.changedPointers.add(e.pointerId);
176
213
  };
177
214
  this.onPointerUp = (e) => {
178
215
  this.endDrag(e.pointerId, true);
179
216
  };
180
217
  }
181
218
  static { this.styles = [LyraElement.styles, styles]; }
219
+ // These accessors sanitize the live value synchronously when a range
220
+ // setting changes. Keeping the properties `noAccessor` prevents Lit's
221
+ // default async field setter from leaving `.value`, `.valueAsNumber`, and
222
+ // ElementInternals' form value disagreeing until the next update flush.
223
+ static { this.properties = {
224
+ min: { type: Number, noAccessor: true },
225
+ max: { type: Number, noAccessor: true },
226
+ step: { type: Number, noAccessor: true },
227
+ }; }
228
+ get min() {
229
+ return this._min;
230
+ }
231
+ set min(next) {
232
+ const old = this._min;
233
+ this._min = finiteNumber(next, 0);
234
+ this.requestUpdate('min', old);
235
+ this.sanitizeCurrentValue();
236
+ }
237
+ get max() {
238
+ return this._max;
239
+ }
240
+ set max(next) {
241
+ const old = this._max;
242
+ this._max = finiteNumber(next, 100);
243
+ this.requestUpdate('max', old);
244
+ this.sanitizeCurrentValue();
245
+ }
246
+ get step() {
247
+ return this._step;
248
+ }
249
+ set step(next) {
250
+ const old = this._step;
251
+ // A zero/negative step is retained as an explicit "unstepped" mode;
252
+ // invalid/non-finite input follows the same safe path without poisoning
253
+ // the current value with NaN.
254
+ this._step = finiteRange(next, 0, 0);
255
+ this.requestUpdate('step', old);
256
+ this.sanitizeCurrentValue();
257
+ }
182
258
  connectedCallback() {
183
259
  super.connectedCallback();
184
- this.ensureValue();
260
+ if (this.pendingValue !== undefined) {
261
+ const pending = this.pendingValue;
262
+ this.pendingValue = undefined;
263
+ this.value = pending;
264
+ }
265
+ else {
266
+ this.ensureValue();
267
+ }
185
268
  }
186
269
  disconnectedCallback() {
187
270
  super.disconnectedCallback();
@@ -189,6 +272,8 @@ export class LyraSlider extends FormAssociated(LyraElement) {
189
272
  // mid-drag (or a pointercancel/alt-tab means pointerup never reaches
190
273
  // `window`), these window-level listeners would otherwise leak.
191
274
  this.activePointers.clear();
275
+ this.changedPointers.clear();
276
+ this.keyboardChanged = false;
192
277
  window.removeEventListener('pointermove', this.onPointerMove);
193
278
  window.removeEventListener('pointerup', this.onPointerUp);
194
279
  window.removeEventListener('pointercancel', this.onPointerUp);
@@ -201,13 +286,36 @@ export class LyraSlider extends FormAssociated(LyraElement) {
201
286
  * to the midpoint of `[min, max]`. Writing stringifies the clamped result
202
287
  * back into `value`. */
203
288
  get valueAsNumber() {
204
- if (this.value === '')
289
+ if (super.value === '')
205
290
  return this.defaultNumericValue();
206
- const n = Number(this.value);
291
+ const n = Number(super.value);
207
292
  return Number.isFinite(n) ? this.clampValue(n) : this.defaultNumericValue();
208
293
  }
209
294
  set valueAsNumber(next) {
210
- this.value = String(this.clampValue(next));
295
+ this.value = String(Number.isFinite(next) ? this.clampValue(next) : this.defaultNumericValue());
296
+ }
297
+ /**
298
+ * `FormAssociated` provides the form plumbing; this override adds the
299
+ * slider's native-range sanitization at the IDL boundary so invalid direct
300
+ * assignments cannot briefly submit a literal `NaN`/`Infinity`.
301
+ */
302
+ get value() {
303
+ return super.value;
304
+ }
305
+ set value(next) {
306
+ const raw = next ?? '';
307
+ if (!this.isConnected) {
308
+ this.pendingValue = raw;
309
+ super.value = raw;
310
+ return;
311
+ }
312
+ const numeric = Number(raw);
313
+ const sanitized = raw === '' || !Number.isFinite(numeric) ? this.defaultNumericValue() : this.clampValue(numeric);
314
+ super.value = String(sanitized);
315
+ }
316
+ formResetCallback() {
317
+ super.formResetCallback();
318
+ this.value = String(this.valueAsNumber);
211
319
  }
212
320
  /** If `value` is still unset, seed it with the sanitized default — the
213
321
  * midpoint of `[min, max]`, snapped to `step` — so `value`/`valueAsNumber`
@@ -216,12 +324,26 @@ export class LyraSlider extends FormAssociated(LyraElement) {
216
324
  if (this.value === '')
217
325
  this.value = String(this.defaultNumericValue());
218
326
  }
327
+ /** Re-sanitize an already assigned value immediately after range settings change. */
328
+ sanitizeCurrentValue() {
329
+ if (!this.isConnected)
330
+ return;
331
+ const current = super.value;
332
+ if (current === '')
333
+ return;
334
+ const sanitized = String(this.clampValue(Number(current)));
335
+ if (sanitized !== current)
336
+ this.value = sanitized;
337
+ }
219
338
  domain() {
220
339
  // A caller-supplied min/max that fails Number attribute conversion
221
- // arrives here as NaN; fall back to this property's own default rather
222
- // than propagating NaN into every clampValue()/percentOf() caller.
223
- const min = isNaN(this.min) ? 0 : this.min;
224
- const max = isNaN(this.max) ? 100 : this.max;
340
+ // arrives here as NaN, and a literal `min="Infinity"`/`max="Infinity"`
341
+ // arrives as +-Infinity; `isNaN(...)` alone only catches the former, so
342
+ // test finiteness instead -- otherwise Infinity propagates into every
343
+ // clampValue()/percentOf() caller (e.g. the midpoint default computing
344
+ // `0 + Infinity / 2`).
345
+ const min = finiteNumber(this.min, 0);
346
+ const max = finiteNumber(this.max, 100);
225
347
  return { lo: Math.min(min, max), hi: Math.max(min, max) };
226
348
  }
227
349
  defaultNumericValue() {
@@ -229,26 +351,32 @@ export class LyraSlider extends FormAssociated(LyraElement) {
229
351
  return this.clampValue(lo + (hi - lo) / 2);
230
352
  }
231
353
  percentOf(value) {
232
- if (isNaN(value))
233
- return 0;
234
354
  const { lo, hi } = this.domain();
355
+ const safeValue = finiteRange(value, lo, lo, hi);
235
356
  const span = hi - lo || 1;
236
- return ((value - lo) / span) * 100;
357
+ return ((safeValue - lo) / span) * 100;
237
358
  }
238
359
  clampValue(raw) {
239
360
  const { lo, hi } = this.domain();
361
+ // A NaN/Infinity `raw` (e.g. `valueAsNumber = NaN`, or a `value` string
362
+ // that fails Number conversion) would otherwise propagate straight
363
+ // through the Math.round/Math.max/Math.min calls below and poison the
364
+ // submitted FormAssociated value with the literal "NaN"/"Infinity" —
365
+ // resolve it to a real, finite, in-domain number instead.
366
+ raw = finiteNumber(raw, lo);
240
367
  // A non-positive or non-finite step would otherwise divide by zero/NaN
241
368
  // below; treat it as "unstepped" instead of propagating NaN.
242
- const hasStep = Number.isFinite(this.step) && this.step > 0;
369
+ const step = finiteRange(this.step, 0, 0);
370
+ const hasStep = step > 0;
243
371
  let stepped = raw;
244
372
  if (hasStep) {
245
373
  // Anchor the step grid at the domain's own `lo` (matching native
246
374
  // `<input type=range>`) instead of absolute 0, and round back to
247
375
  // `step`'s own decimal precision so repeated steps land on exact
248
376
  // values like 0.7 instead of 0.7000000000000001.
249
- const stepsFromLo = Math.round((raw - lo) / this.step);
250
- const factor = 10 ** decimalPlaces(this.step);
251
- stepped = Math.round((lo + stepsFromLo * this.step) * factor) / factor;
377
+ const stepsFromLo = Math.round((raw - lo) / step);
378
+ const factor = 10 ** decimalPlaces(step);
379
+ stepped = Math.round((lo + stepsFromLo * step) * factor) / factor;
252
380
  }
253
381
  return Math.min(hi, Math.max(lo, stepped));
254
382
  }
@@ -256,18 +384,40 @@ export class LyraSlider extends FormAssociated(LyraElement) {
256
384
  return String(v);
257
385
  }
258
386
  setValue(raw, commit) {
387
+ const previous = this.valueAsNumber;
259
388
  const clamped = this.clampValue(raw);
389
+ if (clamped === previous)
390
+ return false;
260
391
  this.value = String(clamped);
261
392
  this.emit('lyra-input', { value: clamped });
262
393
  if (commit)
263
394
  this.emit('lyra-change', { value: clamped });
395
+ return true;
396
+ }
397
+ /** Start tracking `pointerId` as an active drag, transferring pointer
398
+ * capture to `captureTarget` and wiring the shared window-level move/end
399
+ * listeners. Shared by a pointerdown that starts on the thumb itself and
400
+ * one that starts elsewhere on the track (see `onBasePointerDown`), so
401
+ * both gestures continue identically from here on. */
402
+ beginDrag(pointerId, captureTarget) {
403
+ this.activePointers.add(pointerId);
404
+ this.changedPointers.delete(pointerId);
405
+ captureTarget.setPointerCapture(pointerId);
406
+ window.addEventListener('pointermove', this.onPointerMove);
407
+ window.addEventListener('pointerup', this.onPointerUp);
408
+ // A drag can end without a pointerup: a system gesture / palm rejection
409
+ // can fire `pointercancel`, and losing capture (e.g. element removed)
410
+ // fires `lostpointercapture` — both need the same teardown as pointerup.
411
+ window.addEventListener('pointercancel', this.onPointerUp);
412
+ window.addEventListener('lostpointercapture', this.onPointerUp);
264
413
  }
265
414
  /** Stop the drag owned by `pointerId`, optionally committing a final lyra-change. */
266
415
  endDrag(pointerId, commit) {
267
416
  if (!this.activePointers.has(pointerId))
268
417
  return;
269
418
  this.activePointers.delete(pointerId);
270
- if (commit)
419
+ const changed = this.changedPointers.delete(pointerId);
420
+ if (commit && changed)
271
421
  this.emit('lyra-change', { value: this.valueAsNumber });
272
422
  // Only the last concurrent drag to end tears down the shared window
273
423
  // listeners — an overlapping second pointer may still be down.
@@ -278,32 +428,6 @@ export class LyraSlider extends FormAssociated(LyraElement) {
278
428
  window.removeEventListener('lostpointercapture', this.onPointerUp);
279
429
  }
280
430
  }
281
- willUpdate(changed) {
282
- // `formResetCallback` (inherited from `FormAssociated`, restoring
283
- // `value` to whatever default the mixin captured — "" when no `value`
284
- // attribute was ever declared) isn't part of the mixin's exported
285
- // `FormAssociatedInterface` type, so it can't be overridden here with a
286
- // `super` call the way `connectedCallback`/`disconnectedCallback` above
287
- // are. Catching the resulting empty `value` here instead, in the same
288
- // pre-render pass that already re-clamps a changed min/max/step below,
289
- // covers `form.reset()` the same way `connectedCallback` covers first
290
- // mount — and doing it in `willUpdate` (not `updated`) folds the fix-up
291
- // into this same update cycle instead of scheduling a second one.
292
- if (changed.has('value') && this.value === '') {
293
- this.ensureValue();
294
- return;
295
- }
296
- if ((changed.has('min') || changed.has('max') || changed.has('step')) && this.value !== '') {
297
- // A narrowed/shifted domain (or a changed step grid) after mount must
298
- // not leave `value` — and the rendered thumb position it drives —
299
- // outside the current bounds or off the step grid. `min`/`max`/`step`
300
- // already hold their new values by this point, so the getter itself
301
- // (which clamps against the live domain) computes the corrected value.
302
- const next = String(this.valueAsNumber);
303
- if (next !== this.value)
304
- this.value = next;
305
- }
306
- }
307
431
  render() {
308
432
  const num = this.valueAsNumber;
309
433
  const pct = this.percentOf(num);
@@ -311,7 +435,7 @@ export class LyraSlider extends FormAssociated(LyraElement) {
311
435
  const { lo, hi } = this.domain();
312
436
  const ariaLabel = this.label || this.getAttribute('aria-label') || nothing;
313
437
  return html `
314
- <div part="base">
438
+ <div part="base" @pointerdown=${this.onBasePointerDown}>
315
439
  <div part="track"></div>
316
440
  <div part="fill" style=${`inline-size:${pct}%`}></div>
317
441
  <div
@@ -334,15 +458,6 @@ export class LyraSlider extends FormAssociated(LyraElement) {
334
458
  `;
335
459
  }
336
460
  }
337
- __decorate([
338
- property({ type: Number })
339
- ], LyraSlider.prototype, "min", void 0);
340
- __decorate([
341
- property({ type: Number })
342
- ], LyraSlider.prototype, "max", void 0);
343
- __decorate([
344
- property({ type: Number })
345
- ], LyraSlider.prototype, "step", void 0);
346
461
  __decorate([
347
462
  property()
348
463
  ], LyraSlider.prototype, "label", void 0);
@@ -1,4 +1,4 @@
1
- import { type TemplateResult } from 'lit';
1
+ import { type PropertyValues, type TemplateResult } from 'lit';
2
2
  import { LyraElement } from '../../internal/lyra-element.js';
3
3
  export interface SourceCardExpandDetail {
4
4
  sourceId: string;
@@ -24,10 +24,12 @@ export interface SourceCardOpenDetail {
24
24
  * card's `id`/`source-id`. See the `@example` below.
25
25
  *
26
26
  * @customElement lyra-source-card
27
- * @slot excerpt - A short preview, always visible.
27
+ * @slot excerpt - A short preview. When left empty, the `excerpt` part
28
+ * collapses away entirely rather than leaving an empty gap in the card.
28
29
  * @slot full - The complete source text/chunk, hidden behind the "Show
29
30
  * more"/"Show less" toggle. When left empty, no toggle renders at all — a
30
- * card with no `full` content simply has no expand affordance.
31
+ * card with no `full` content simply has no expand affordance. Removing all
32
+ * `full`-slotted content while expanded automatically collapses it back.
31
33
  * @event lyra-expand - The per-card "Show more"/"Show less" toggle was
32
34
  * activated. `detail: { sourceId, expanded }`.
33
35
  * @event lyra-open - The title was activated. `detail: { sourceId, href }` —
@@ -38,7 +40,7 @@ export interface SourceCardOpenDetail {
38
40
  * viewer, etc).
39
41
  * @csspart base - The outer container.
40
42
  * @csspart title - The clickable title/page heading (`<button>`).
41
- * @csspart excerpt - The wrapper around the `excerpt` slot.
43
+ * @csspart excerpt - The wrapper around the `excerpt` slot, `hidden` when the slot has no assigned content.
42
44
  * @csspart full - The wrapper around the `full` slot, `hidden` while collapsed.
43
45
  * @csspart toggle - The "Show more"/"Show less" button. Only rendered when the `full` slot has content.
44
46
  *
@@ -67,7 +69,12 @@ export declare class LyraSourceCard extends LyraElement {
67
69
  static styles: import("lit").CSSResultGroup[];
68
70
  /** Stable identifier matching a `<lyra-citation-badge>` elsewhere on the page. */
69
71
  sourceId: string;
70
- /** The source's display title, e.g. a filename. */
72
+ /** The source's display title, e.g. a filename. Rendered only as the
73
+ * title button's own text -- a bare host-level `title` attribute (the
74
+ * browser's global tooltip attribute) is actively stripped once Lit has
75
+ * synced it into this property, so the card never grows an unsolicited
76
+ * native tooltip repeating the same text. See `attributeChangedCallback`
77
+ * and `updated` below. */
71
78
  title: string;
72
79
  /** Optional page reference, e.g. `12` or `"iv"` — rendered as-is (never
73
80
  * parsed/validated as a number), so a non-numeric page label works too. */
@@ -75,11 +82,16 @@ export declare class LyraSourceCard extends LyraElement {
75
82
  /** Optional URL, echoed back (unopened) in `lyra-open`'s detail. */
76
83
  href?: string;
77
84
  private hasFullSlot;
85
+ private hasExcerptSlot;
78
86
  private fullExpanded;
79
87
  private readonly fullId;
88
+ private stripHostTitleAttr;
80
89
  protected willUpdate(): void;
81
90
  firstUpdated(): void;
91
+ attributeChangedCallback(name: string, old: string | null, value: string | null): void;
92
+ protected updated(changed: PropertyValues<this>): void;
82
93
  private onFullSlotChange;
94
+ private onExcerptSlotChange;
83
95
  private get titleText();
84
96
  private onTitleClick;
85
97
  private toggleFull;
@@ -26,10 +26,12 @@ import { styles } from './source-card.styles.js';
26
26
  * card's `id`/`source-id`. See the `@example` below.
27
27
  *
28
28
  * @customElement lyra-source-card
29
- * @slot excerpt - A short preview, always visible.
29
+ * @slot excerpt - A short preview. When left empty, the `excerpt` part
30
+ * collapses away entirely rather than leaving an empty gap in the card.
30
31
  * @slot full - The complete source text/chunk, hidden behind the "Show
31
32
  * more"/"Show less" toggle. When left empty, no toggle renders at all — a
32
- * card with no `full` content simply has no expand affordance.
33
+ * card with no `full` content simply has no expand affordance. Removing all
34
+ * `full`-slotted content while expanded automatically collapses it back.
33
35
  * @event lyra-expand - The per-card "Show more"/"Show less" toggle was
34
36
  * activated. `detail: { sourceId, expanded }`.
35
37
  * @event lyra-open - The title was activated. `detail: { sourceId, href }` —
@@ -40,7 +42,7 @@ import { styles } from './source-card.styles.js';
40
42
  * viewer, etc).
41
43
  * @csspart base - The outer container.
42
44
  * @csspart title - The clickable title/page heading (`<button>`).
43
- * @csspart excerpt - The wrapper around the `excerpt` slot.
45
+ * @csspart excerpt - The wrapper around the `excerpt` slot, `hidden` when the slot has no assigned content.
44
46
  * @csspart full - The wrapper around the `full` slot, `hidden` while collapsed.
45
47
  * @csspart toggle - The "Show more"/"Show less" button. Only rendered when the `full` slot has content.
46
48
  *
@@ -70,17 +72,35 @@ export class LyraSourceCard extends LyraElement {
70
72
  super(...arguments);
71
73
  /** Stable identifier matching a `<lyra-citation-badge>` elsewhere on the page. */
72
74
  this.sourceId = '';
73
- /** The source's display title, e.g. a filename. */
75
+ /** The source's display title, e.g. a filename. Rendered only as the
76
+ * title button's own text -- a bare host-level `title` attribute (the
77
+ * browser's global tooltip attribute) is actively stripped once Lit has
78
+ * synced it into this property, so the card never grows an unsolicited
79
+ * native tooltip repeating the same text. See `attributeChangedCallback`
80
+ * and `updated` below. */
74
81
  this.title = '';
75
82
  // See `<lyra-widget>`'s `hasActionsSlot` for the identical
76
83
  // presence-tracking convention -- a `[part]` always contains a literal
77
84
  // `<slot>` child regardless of assigned content, so CSS `:empty` never
78
85
  // matches; real emptiness is tracked in JS instead.
79
86
  this.hasFullSlot = false;
87
+ this.hasExcerptSlot = false;
80
88
  this.fullExpanded = false;
81
89
  this.fullId = nextId('source-card-full');
90
+ // Guards the `removeAttribute('title')` call in `updated()` below: removing
91
+ // an observed attribute fires `attributeChangedCallback` synchronously just
92
+ // like setting one does, and without this flag Lit would treat the removal
93
+ // as a fresh (empty) attribute value and reset the `title` property right
94
+ // back to `null`, losing the value it just finished syncing in.
95
+ this.stripHostTitleAttr = false;
82
96
  this.onFullSlotChange = (e) => {
83
- this.hasFullSlot = e.target.assignedElements({ flatten: true }).length > 0;
97
+ const count = e.target.assignedElements({ flatten: true }).length;
98
+ this.hasFullSlot = count > 0;
99
+ if (count === 0)
100
+ this.fullExpanded = false;
101
+ };
102
+ this.onExcerptSlotChange = (e) => {
103
+ this.hasExcerptSlot = e.target.assignedElements({ flatten: true }).length > 0;
84
104
  };
85
105
  this.onTitleClick = () => {
86
106
  this.emit('lyra-open', { sourceId: this.sourceId, href: this.href });
@@ -94,11 +114,33 @@ export class LyraSourceCard extends LyraElement {
94
114
  willUpdate() {
95
115
  if (!this.hasUpdated) {
96
116
  this.hasFullSlot = Array.from(this.children).some((el) => el.getAttribute('slot') === 'full');
117
+ this.hasExcerptSlot = Array.from(this.children).some((el) => el.getAttribute('slot') === 'excerpt');
97
118
  }
98
119
  }
99
120
  firstUpdated() {
100
- const slot = this.shadowRoot.querySelector('slot[name="full"]');
101
- this.hasFullSlot = slot.assignedElements({ flatten: true }).length > 0;
121
+ const fullSlot = this.shadowRoot.querySelector('slot[name="full"]');
122
+ const fullCount = fullSlot.assignedElements({ flatten: true }).length;
123
+ this.hasFullSlot = fullCount > 0;
124
+ if (fullCount === 0)
125
+ this.fullExpanded = false;
126
+ const excerptSlot = this.shadowRoot.querySelector('slot[name="excerpt"]');
127
+ this.hasExcerptSlot = excerptSlot.assignedElements({ flatten: true }).length > 0;
128
+ }
129
+ attributeChangedCallback(name, old, value) {
130
+ if (name === 'title' && this.stripHostTitleAttr)
131
+ return;
132
+ super.attributeChangedCallback(name, old, value);
133
+ }
134
+ updated(changed) {
135
+ if (changed.has('title') && this.hasAttribute('title')) {
136
+ // The attribute has already been converted into the `title` property
137
+ // by this point (that's how it got here), so the DOM attribute itself
138
+ // is now redundant -- and, left in place, would make the whole card
139
+ // show a native tooltip repeating the title text on hover.
140
+ this.stripHostTitleAttr = true;
141
+ this.removeAttribute('title');
142
+ this.stripHostTitleAttr = false;
143
+ }
102
144
  }
103
145
  get titleText() {
104
146
  const base = this.title || 'Untitled source';
@@ -108,7 +150,9 @@ export class LyraSourceCard extends LyraElement {
108
150
  return html `
109
151
  <div part="base">
110
152
  <button part="title" type="button" @click=${this.onTitleClick}>${this.titleText}</button>
111
- <div part="excerpt"><slot name="excerpt"></slot></div>
153
+ <div part="excerpt" ?hidden=${!this.hasExcerptSlot}>
154
+ <slot name="excerpt" @slotchange=${this.onExcerptSlotChange}></slot>
155
+ </div>
112
156
  ${this.hasFullSlot
113
157
  ? html `<button
114
158
  part="toggle"
@@ -142,6 +186,9 @@ __decorate([
142
186
  __decorate([
143
187
  state()
144
188
  ], LyraSourceCard.prototype, "hasFullSlot", void 0);
189
+ __decorate([
190
+ state()
191
+ ], LyraSourceCard.prototype, "hasExcerptSlot", void 0);
145
192
  __decorate([
146
193
  state()
147
194
  ], LyraSourceCard.prototype, "fullExpanded", void 0);
@@ -38,7 +38,7 @@ export const styles = css `
38
38
  font-size: 0.8125rem;
39
39
  line-height: 1.4;
40
40
  }
41
- [part='excerpt']:empty {
41
+ [part='excerpt'][hidden] {
42
42
  display: none;
43
43
  }
44
44
  [part='toggle'] {
@@ -69,7 +69,15 @@ export class LyraSourceList extends LyraElement {
69
69
  static { this.styles = [LyraElement.styles, styles]; }
70
70
  willUpdate() {
71
71
  if (!this.hasUpdated) {
72
- this.slottedCount = this.children.length;
72
+ // Must count only children assigned to the *default* slot -- same rule
73
+ // `firstUpdated`/`onSlotChange` apply via `assignedElements()` below --
74
+ // otherwise a direct child carrying a foreign `slot` attribute makes
75
+ // this pre-count disagree with the authoritative one, which schedules
76
+ // a wasted second update (and Lit's dev-mode change-in-update warning).
77
+ // An explicit `slot=""` still assigns to the default slot per the HTML
78
+ // slot algorithm, so check the attribute's value rather than its mere
79
+ // presence.
80
+ this.slottedCount = Array.from(this.children).filter((el) => !el.getAttribute('slot')).length;
73
81
  }
74
82
  }
75
83
  firstUpdated() {