@aceshooting/lyra-ui 1.0.1 → 1.2.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 (150) hide show
  1. package/README.md +9 -10
  2. package/custom-elements.json +15118 -8342
  3. package/dist/components/chart/chart.d.ts +57 -1
  4. package/dist/components/chart/chart.js +202 -12
  5. package/dist/components/chart/chart.stories.d.ts +32 -0
  6. package/dist/components/chart/chart.stories.js +141 -0
  7. package/dist/components/chart/chart.styles.js +16 -0
  8. package/dist/components/chart/histogram.d.ts +2 -0
  9. package/dist/components/chart/histogram.js +18 -2
  10. package/dist/components/chart/line-chart.stories.d.ts +17 -0
  11. package/dist/components/chart/line-chart.stories.js +75 -0
  12. package/dist/components/chart/lite-chart.d.ts +83 -0
  13. package/dist/components/chart/lite-chart.js +424 -0
  14. package/dist/components/chart/lite-chart.stories.d.ts +14 -0
  15. package/dist/components/chart/lite-chart.stories.js +114 -0
  16. package/dist/components/chart/lite-chart.styles.d.ts +1 -0
  17. package/dist/components/chart/lite-chart.styles.js +83 -0
  18. package/dist/components/combobox/combobox.d.ts +9 -0
  19. package/dist/components/combobox/combobox.js +76 -13
  20. package/dist/components/combobox/combobox.stories.d.ts +18 -0
  21. package/dist/components/combobox/combobox.stories.js +75 -0
  22. package/dist/components/date-picker/calendar-core.js +13 -2
  23. package/dist/components/date-picker/date-input.d.ts +9 -1
  24. package/dist/components/date-picker/date-input.js +55 -7
  25. package/dist/components/date-picker/date-input.stories.d.ts +5 -0
  26. package/dist/components/date-picker/date-input.stories.js +36 -0
  27. package/dist/components/date-picker/date-picker.d.ts +11 -3
  28. package/dist/components/date-picker/date-picker.js +87 -37
  29. package/dist/components/date-picker/date-picker.stories.d.ts +4 -0
  30. package/dist/components/date-picker/date-picker.stories.js +12 -0
  31. package/dist/components/date-picker/date-picker.styles.js +1 -1
  32. package/dist/components/empty/empty.d.ts +12 -1
  33. package/dist/components/empty/empty.js +20 -3
  34. package/dist/components/empty/empty.stories.d.ts +2 -0
  35. package/dist/components/empty/empty.stories.js +20 -0
  36. package/dist/components/empty/empty.styles.js +11 -0
  37. package/dist/components/export-button/csv.js +6 -2
  38. package/dist/components/export-button/export-button.d.ts +15 -1
  39. package/dist/components/export-button/export-button.js +89 -12
  40. package/dist/components/export-button/export-button.stories.d.ts +1 -0
  41. package/dist/components/export-button/export-button.stories.js +5 -0
  42. package/dist/components/export-button/export-button.styles.js +11 -3
  43. package/dist/components/file-input/accept.d.ts +8 -2
  44. package/dist/components/file-input/accept.js +9 -3
  45. package/dist/components/file-input/file-input.d.ts +5 -2
  46. package/dist/components/file-input/file-input.js +23 -8
  47. package/dist/components/file-input/file-input.stories.d.ts +2 -0
  48. package/dist/components/file-input/file-input.stories.js +9 -0
  49. package/dist/components/flag/flag.d.ts +69 -3
  50. package/dist/components/flag/flag.js +107 -30
  51. package/dist/components/flag/flag.stories.d.ts +1 -0
  52. package/dist/components/flag/flag.stories.js +14 -0
  53. package/dist/components/flag/flag.styles.js +1 -1
  54. package/dist/components/flag/language-map.d.ts +11 -0
  55. package/dist/components/flag/language-map.js +12 -1
  56. package/dist/components/gauge/gauge.d.ts +2 -0
  57. package/dist/components/gauge/gauge.js +19 -5
  58. package/dist/components/gauge/gauge.stories.d.ts +2 -0
  59. package/dist/components/gauge/gauge.stories.js +6 -0
  60. package/dist/components/graph/graph-loader.d.ts +28 -0
  61. package/dist/components/graph/graph-loader.js +44 -0
  62. package/dist/components/graph/graph.d.ts +41 -15
  63. package/dist/components/graph/graph.js +146 -71
  64. package/dist/components/graph/graph.stories.d.ts +3 -0
  65. package/dist/components/graph/graph.stories.js +38 -0
  66. package/dist/components/graph/graph.styles.js +6 -1
  67. package/dist/components/heatmap/calendar-grid.d.ts +7 -0
  68. package/dist/components/heatmap/calendar-grid.js +27 -10
  69. package/dist/components/heatmap/heatmap.d.ts +149 -12
  70. package/dist/components/heatmap/heatmap.js +445 -28
  71. package/dist/components/heatmap/heatmap.stories.d.ts +22 -0
  72. package/dist/components/heatmap/heatmap.stories.js +155 -0
  73. package/dist/components/heatmap/heatmap.styles.js +60 -2
  74. package/dist/components/map/map.d.ts +17 -1
  75. package/dist/components/map/map.js +68 -21
  76. package/dist/components/map/map.stories.d.ts +10 -0
  77. package/dist/components/map/map.stories.js +107 -12
  78. package/dist/components/map/map.styles.js +2 -2
  79. package/dist/components/playback/playback.d.ts +6 -2
  80. package/dist/components/playback/playback.js +36 -12
  81. package/dist/components/playback/playback.stories.d.ts +2 -0
  82. package/dist/components/playback/playback.stories.js +6 -0
  83. package/dist/components/playback/playback.styles.js +10 -2
  84. package/dist/components/select/select.d.ts +125 -0
  85. package/dist/components/select/select.js +480 -0
  86. package/dist/components/select/select.stories.d.ts +21 -0
  87. package/dist/components/select/select.stories.js +91 -0
  88. package/dist/components/select/select.styles.d.ts +1 -0
  89. package/dist/components/select/select.styles.js +212 -0
  90. package/dist/components/skeleton/skeleton.js +4 -7
  91. package/dist/components/sparkline/sparkline.d.ts +1 -1
  92. package/dist/components/sparkline/sparkline.js +40 -13
  93. package/dist/components/sparkline/sparkline.stories.d.ts +4 -0
  94. package/dist/components/sparkline/sparkline.stories.js +22 -0
  95. package/dist/components/split/split.d.ts +24 -3
  96. package/dist/components/split/split.js +155 -31
  97. package/dist/components/split/split.stories.d.ts +2 -0
  98. package/dist/components/split/split.stories.js +20 -0
  99. package/dist/components/stat/stat.d.ts +43 -1
  100. package/dist/components/stat/stat.js +97 -4
  101. package/dist/components/stat/stat.stories.d.ts +4 -0
  102. package/dist/components/stat/stat.stories.js +66 -1
  103. package/dist/components/stat/stat.styles.js +64 -0
  104. package/dist/components/table/table.d.ts +64 -2
  105. package/dist/components/table/table.js +213 -17
  106. package/dist/components/table/table.stories.d.ts +6 -0
  107. package/dist/components/table/table.stories.js +45 -0
  108. package/dist/components/table/table.styles.js +51 -4
  109. package/dist/components/time-range/time-range.d.ts +34 -5
  110. package/dist/components/time-range/time-range.js +217 -36
  111. package/dist/components/time-range/time-range.stories.d.ts +2 -0
  112. package/dist/components/time-range/time-range.stories.js +13 -0
  113. package/dist/components/time-range/time-range.styles.js +44 -2
  114. package/dist/components/toast/toast-item.d.ts +14 -2
  115. package/dist/components/toast/toast-item.js +134 -17
  116. package/dist/components/toast/toast-item.styles.js +28 -2
  117. package/dist/components/toast/toast.stories.d.ts +3 -0
  118. package/dist/components/toast/toast.stories.js +41 -1
  119. package/dist/components/toast/toast.styles.js +11 -11
  120. package/dist/components/toast/toaster.d.ts +1 -1
  121. package/dist/components/toast/toaster.js +13 -6
  122. package/dist/components/tree/tree-node.d.ts +13 -7
  123. package/dist/components/tree/tree-node.js +24 -10
  124. package/dist/components/tree/tree.d.ts +64 -15
  125. package/dist/components/tree/tree.js +113 -22
  126. package/dist/components/tree/tree.stories.d.ts +4 -0
  127. package/dist/components/tree/tree.stories.js +31 -1
  128. package/dist/components/tree/update-cascade.d.ts +13 -0
  129. package/dist/components/tree/update-cascade.js +13 -0
  130. package/dist/components/widget/widget.d.ts +25 -1
  131. package/dist/components/widget/widget.js +164 -6
  132. package/dist/components/widget/widget.stories.d.ts +3 -0
  133. package/dist/components/widget/widget.stories.js +43 -0
  134. package/dist/components/widget/widget.styles.js +20 -3
  135. package/dist/components/word-cloud/word-cloud-layout.d.ts +63 -0
  136. package/dist/components/word-cloud/word-cloud-layout.js +145 -0
  137. package/dist/components/word-cloud/word-cloud.d.ts +65 -0
  138. package/dist/components/word-cloud/word-cloud.js +255 -0
  139. package/dist/components/word-cloud/word-cloud.stories.d.ts +10 -0
  140. package/dist/components/word-cloud/word-cloud.stories.js +55 -0
  141. package/dist/components/word-cloud/word-cloud.styles.d.ts +1 -0
  142. package/dist/components/word-cloud/word-cloud.styles.js +55 -0
  143. package/dist/internal/a11y.js +2 -1
  144. package/dist/internal/rtl.d.ts +7 -0
  145. package/dist/internal/rtl.js +9 -0
  146. package/dist/lyra.d.ts +9 -1
  147. package/dist/lyra.js +6 -0
  148. package/llms-full.txt +962 -276
  149. package/llms.txt +12 -9
  150. package/package.json +6 -4
@@ -0,0 +1,480 @@
1
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
2
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
3
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
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
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
6
+ };
7
+ import { html } from 'lit';
8
+ import { property, state } from 'lit/decorators.js';
9
+ import { LyraElement } from '../../internal/lyra-element.js';
10
+ import { defineElement } from '../../internal/prefix.js';
11
+ import { place } from '../../internal/positioner.js';
12
+ import { nextId } from '../../internal/a11y.js';
13
+ import { chevronIcon } from '../../internal/icons.js';
14
+ import { styles } from './select.styles.js';
15
+ import { LyraOption } from '../combobox/option.js';
16
+ import '../combobox/option.js';
17
+ export class LyraSelect extends LyraElement {
18
+ static { this.formAssociated = true; }
19
+ static { this.styles = [LyraElement.styles, styles]; }
20
+ static { this.properties = {
21
+ value: { noAccessor: true },
22
+ }; }
23
+ constructor() {
24
+ super();
25
+ this.placeholder = '';
26
+ this.disabled = false;
27
+ this.required = false;
28
+ this.name = '';
29
+ this.label = '';
30
+ this.hint = '';
31
+ this.errorText = '';
32
+ this.open = false;
33
+ /** Visual size — same `xs`–`xl` scale as `lyra-toast-item`'s `size`. */
34
+ this.size = 'm';
35
+ this.activeIndex = -1;
36
+ this.options = [];
37
+ // Set on the trigger button's first `blur`; gates the `data-invalid`
38
+ // reflection below so validity styling never flashes on first render.
39
+ this.touched = false;
40
+ // `[part]:empty` never matches -- the part always contains a literal
41
+ // `<slot>` child element regardless of assigned content -- so real
42
+ // emptiness is tracked in JS instead (same fix as lyra-combobox's
43
+ // hasHintSlot/hasErrorSlot/hasLabelSlot) and reflected via `hidden`.
44
+ this.hasHintSlot = false;
45
+ this.hasErrorSlot = false;
46
+ this.hasLabelSlot = false;
47
+ this.listId = nextId('select-list');
48
+ this.triggerId = nextId('select-trigger');
49
+ this._isFirstUpdate = true;
50
+ this._selected = '';
51
+ // What `form.reset()` restores to. Captured exactly once, from whatever
52
+ // `<lyra-option selected>` markup was present the first time slotted
53
+ // options are collected (mirrors native `<select><option selected>`) --
54
+ // never from the `value` setter, so a user picking an option (even the
55
+ // very first pick on an initially-unselected select) can't itself become
56
+ // the reset default. See lyra-combobox's identical `_defaultSelected`.
57
+ this._defaultSelected = '';
58
+ this._defaultCaptured = false;
59
+ // Standard listbox type-ahead: printable keystrokes accumulate into this
60
+ // buffer and reset ~500ms after the last one, so "b" then "a" narrows to
61
+ // "ba" instead of restarting the search on every keystroke.
62
+ this.typeAheadBuffer = '';
63
+ this.collectOptions = (e) => {
64
+ const slot = e.target;
65
+ const previous = new Set(this.options);
66
+ this.options = slot
67
+ .assignedElements({ flatten: true })
68
+ .filter((el) => el instanceof LyraOption);
69
+ if (!this._defaultCaptured) {
70
+ this._defaultCaptured = true;
71
+ // Seed the initial selection -- and the reset default -- from
72
+ // declarative `<lyra-option selected>` markup, mirroring native
73
+ // `<select><option selected>`. Only the *first* declared-selected
74
+ // option matters when several declare it, mirroring lyra-combobox's
75
+ // single-mode behavior. This is the only place `_defaultSelected` is
76
+ // set; picking an option later (the `value` setter) never redefines
77
+ // the reset default.
78
+ const declared = this.options.filter((o) => o.selected).map((o) => o.value);
79
+ this._defaultSelected = declared[0] ?? '';
80
+ if (declared.length) {
81
+ this.value = declared[0];
82
+ return; // `value=`'s setter already called reflectSelected()
83
+ }
84
+ }
85
+ else {
86
+ // Options slotted in after the first pass (e.g. a lazily-populated
87
+ // list appended post-connect) still declare selection the same way a
88
+ // native `<select><option selected>` would -- seed the newest one
89
+ // into the live selection instead of letting reflectSelected() below
90
+ // strip its `selected` attribute back off.
91
+ const newlySelected = this.options.filter((o) => !previous.has(o) && o.selected).map((o) => o.value);
92
+ if (newlySelected.length) {
93
+ this.value = newlySelected[newlySelected.length - 1];
94
+ return; // `value=`'s setter already called reflectSelected()
95
+ }
96
+ }
97
+ this.reflectSelected();
98
+ };
99
+ this.onDocPointer = (e) => {
100
+ if (!e.composedPath().includes(this))
101
+ this.hide();
102
+ };
103
+ this.onTriggerClick = () => {
104
+ if (this.disabled)
105
+ return;
106
+ this.open ? this.hide() : this.show();
107
+ };
108
+ this.onTriggerBlur = () => {
109
+ this.touched = true;
110
+ // A mouse click outside the element is already handled by
111
+ // onDocPointer/hide(), but that leaves keyboard users with no way to
112
+ // dismiss the listbox short of Escape -- tabbing focus away from the
113
+ // trigger should close it too, the same as lyra-combobox's input blur.
114
+ this.hide();
115
+ };
116
+ this.onHintSlotChange = (e) => {
117
+ this.hasHintSlot = e.target.assignedElements({ flatten: true }).length > 0;
118
+ };
119
+ this.onErrorSlotChange = (e) => {
120
+ this.hasErrorSlot = e.target.assignedElements({ flatten: true }).length > 0;
121
+ };
122
+ this.onLabelSlotChange = (e) => {
123
+ this.hasLabelSlot = e.target.assignedElements({ flatten: true }).length > 0;
124
+ };
125
+ this.onKeyDown = (e) => {
126
+ const navigable = this.options.filter((o) => !o.disabled);
127
+ switch (e.key) {
128
+ case 'ArrowDown':
129
+ e.preventDefault();
130
+ if (!this.open)
131
+ return this.show();
132
+ this.activeIndex = Math.min(navigable.length - 1, this.activeIndex + 1);
133
+ break;
134
+ case 'ArrowUp':
135
+ e.preventDefault();
136
+ if (!this.open)
137
+ return this.show();
138
+ this.activeIndex = Math.max(0, this.activeIndex - 1);
139
+ break;
140
+ case 'Enter':
141
+ case ' ':
142
+ // When closed, let the button's native Enter/Space activation fire
143
+ // its own `click` handler (onTriggerClick) to open -- only intercept
144
+ // here to commit/dismiss while already open, so that synthesized
145
+ // click doesn't also re-toggle it shut.
146
+ if (this.open) {
147
+ e.preventDefault();
148
+ if (this.activeIndex >= 0 && navigable[this.activeIndex]) {
149
+ this.selectOption(navigable[this.activeIndex]);
150
+ }
151
+ else {
152
+ this.hide();
153
+ }
154
+ }
155
+ break;
156
+ case 'Escape':
157
+ if (this.open) {
158
+ e.preventDefault();
159
+ this.hide();
160
+ }
161
+ break;
162
+ case 'Home':
163
+ if (this.open) {
164
+ e.preventDefault();
165
+ this.activeIndex = 0;
166
+ }
167
+ break;
168
+ case 'End':
169
+ if (this.open) {
170
+ e.preventDefault();
171
+ this.activeIndex = navigable.length - 1;
172
+ }
173
+ break;
174
+ default:
175
+ if (e.key.length === 1 && !e.altKey && !e.ctrlKey && !e.metaKey) {
176
+ this.typeAhead(e.key);
177
+ }
178
+ break;
179
+ }
180
+ };
181
+ // Delegated onto [part="listbox"] (see render()) rather than one closure
182
+ // pair allocated per option per render -- resolves the target row via
183
+ // closest('[part="option"]') + a data-value lookup, mirroring lyra-combobox.
184
+ this.onListboxMouseDown = (e) => {
185
+ if (e.target.closest('[part="option"]'))
186
+ e.preventDefault();
187
+ };
188
+ this.onListboxClick = (e) => {
189
+ const optionEl = e.target.closest('[part="option"]');
190
+ const value = optionEl?.dataset.value;
191
+ if (value === undefined)
192
+ return;
193
+ const option = this.options.find((o) => o.value === value);
194
+ if (option)
195
+ this.selectOption(option);
196
+ };
197
+ this.internals = this.attachInternals();
198
+ }
199
+ connectedCallback() {
200
+ super.connectedCallback();
201
+ this.updateValidity();
202
+ }
203
+ willUpdate() {
204
+ // `hasUpdated` flips to `true` before `updated()` even sees its first
205
+ // call, so it can't distinguish "just mounted" from "just changed" there
206
+ // -- capture that distinction here, while it's still reliable, for
207
+ // `updated()`'s `open`-handling below to consult.
208
+ this._isFirstUpdate = !this.hasUpdated;
209
+ if (!this.hasUpdated) {
210
+ this.hasHintSlot = Array.from(this.children).some((el) => el.getAttribute('slot') === 'hint');
211
+ this.hasErrorSlot = Array.from(this.children).some((el) => el.getAttribute('slot') === 'error');
212
+ this.hasLabelSlot = Array.from(this.children).some((el) => el.getAttribute('slot') === 'label');
213
+ }
214
+ }
215
+ /** The selected value: a single string (empty when nothing is selected). */
216
+ get value() {
217
+ return this._selected;
218
+ }
219
+ set value(next) {
220
+ const old = this._selected;
221
+ this._selected = next ?? '';
222
+ this.syncFormValue();
223
+ this.reflectSelected();
224
+ this.updateValidity();
225
+ this.requestUpdate('value', old);
226
+ }
227
+ updateValidity() {
228
+ if (this.required && !this._selected) {
229
+ this.internals.setValidity({ valueMissing: true }, 'Please select an option.');
230
+ }
231
+ else {
232
+ this.internals.setValidity({});
233
+ }
234
+ }
235
+ syncFormValue() {
236
+ this.internals.setFormValue(this._selected);
237
+ }
238
+ formResetCallback() {
239
+ this.value = this._defaultSelected;
240
+ }
241
+ formDisabledCallback(disabled) {
242
+ this.disabled = disabled;
243
+ }
244
+ checkValidity() {
245
+ return this.internals.checkValidity();
246
+ }
247
+ reportValidity() {
248
+ return this.internals.reportValidity();
249
+ }
250
+ disconnectedCallback() {
251
+ super.disconnectedCallback();
252
+ this.cleanup?.();
253
+ clearTimeout(this.typeAheadTimer);
254
+ document.removeEventListener('pointerdown', this.onDocPointer);
255
+ }
256
+ reflectSelected() {
257
+ for (const o of this.options)
258
+ o.selected = o.value === this._selected;
259
+ }
260
+ show() {
261
+ if (this.open || this.disabled)
262
+ return;
263
+ this.open = true;
264
+ }
265
+ hide() {
266
+ if (!this.open)
267
+ return;
268
+ this.open = false;
269
+ this.activeIndex = -1;
270
+ }
271
+ updated(changed) {
272
+ if (changed.has('open')) {
273
+ this.cleanup?.();
274
+ this.cleanup = undefined;
275
+ // All `open`-driven side effects (positioning, the click-outside
276
+ // listener, and the lyra-show/lyra-hide events) live here rather than
277
+ // in show()/hide() so they fire however `open` became true -- via
278
+ // show()/hide()'s own user-interaction paths, or a consumer/test
279
+ // setting `el.open` directly, which bypasses both entirely.
280
+ if (this.open) {
281
+ document.addEventListener('pointerdown', this.onDocPointer);
282
+ // Don't announce a "show" transition for markup that's simply
283
+ // rendering open for the first time (e.g. `<lyra-select open>`) --
284
+ // only for an actual closed-to-open transition.
285
+ if (!this._isFirstUpdate)
286
+ this.emit('lyra-show');
287
+ const anchor = this.renderRoot.querySelector('[part="trigger"]');
288
+ const listbox = this.renderRoot.querySelector('[part="listbox"]');
289
+ if (anchor && listbox)
290
+ this.cleanup = place(anchor, listbox);
291
+ }
292
+ else {
293
+ document.removeEventListener('pointerdown', this.onDocPointer);
294
+ if (!this._isFirstUpdate)
295
+ this.emit('lyra-hide');
296
+ }
297
+ }
298
+ if (changed.has('required'))
299
+ this.updateValidity();
300
+ if (changed.has('touched') || changed.has('required') || changed.has('value')) {
301
+ this.toggleAttribute('data-invalid', this.touched && !this.internals.validity.valid);
302
+ }
303
+ }
304
+ selectOption(option) {
305
+ if (option.disabled)
306
+ return;
307
+ this.value = option.value;
308
+ this.hide();
309
+ this.emit('input');
310
+ this.emit('change');
311
+ }
312
+ /**
313
+ * Standard listbox type-ahead: moves to the next non-disabled option whose
314
+ * label starts with the accumulated buffer, cycling from just after the
315
+ * "current" option (the active row while open, the selected value while
316
+ * closed). While open this only moves `activeIndex` (a highlight, matching
317
+ * Arrow-key nav -- Enter/click still commits it); while closed there's no
318
+ * highlight to show, so it commits immediately, matching a native
319
+ * `<select>`'s closed-state type-ahead.
320
+ */
321
+ typeAhead(char) {
322
+ clearTimeout(this.typeAheadTimer);
323
+ this.typeAheadBuffer += char.toLowerCase();
324
+ this.typeAheadTimer = setTimeout(() => {
325
+ this.typeAheadBuffer = '';
326
+ }, 500);
327
+ const navigable = this.options.filter((o) => !o.disabled);
328
+ if (!navigable.length)
329
+ return;
330
+ const currentValue = this.open ? navigable[this.activeIndex]?.value : this._selected;
331
+ const currentIndex = navigable.findIndex((o) => o.value === currentValue);
332
+ const n = navigable.length;
333
+ for (let step = 1; step <= n; step++) {
334
+ const idx = (currentIndex + step + n) % n;
335
+ const candidate = navigable[idx];
336
+ if (candidate.label.toLowerCase().startsWith(this.typeAheadBuffer)) {
337
+ if (this.open) {
338
+ this.activeIndex = idx;
339
+ }
340
+ else {
341
+ this.selectOption(candidate);
342
+ }
343
+ return;
344
+ }
345
+ }
346
+ }
347
+ renderRows(options, activeId) {
348
+ const out = [];
349
+ let currentGroup;
350
+ options.forEach((o, i) => {
351
+ if (o.group !== currentGroup) {
352
+ currentGroup = o.group;
353
+ if (currentGroup)
354
+ out.push(html `<div class="group-label">${currentGroup}</div>`);
355
+ }
356
+ const id = `${this.listId}-opt-${i}`;
357
+ const selected = o.value === this._selected;
358
+ out.push(html `<div
359
+ part="option"
360
+ id=${id}
361
+ role="option"
362
+ data-value=${o.value}
363
+ aria-selected=${selected ? 'true' : 'false'}
364
+ aria-disabled=${o.disabled ? 'true' : 'false'}
365
+ ?data-active=${id === activeId}
366
+ >
367
+ ${o.dotColor ? html `<span part="option-dot" style=${`background:${o.dotColor}`}></span>` : ''}
368
+ <span part="option-label">
369
+ <span>${o.label}</span>
370
+ ${o.sub ? html `<span part="option-sub">${o.sub}</span>` : ''}
371
+ </span>
372
+ </div>`);
373
+ });
374
+ return out;
375
+ }
376
+ render() {
377
+ const options = this.options;
378
+ const navigable = options.filter((o) => !o.disabled);
379
+ const active = this.activeIndex >= 0 ? navigable[this.activeIndex] : undefined;
380
+ const activeId = active ? `${this.listId}-opt-${options.indexOf(active)}` : '';
381
+ const selectedLabel = this._selected
382
+ ? (options.find((o) => o.value === this._selected)?.label ?? this._selected)
383
+ : '';
384
+ const hasValue = this._selected.length > 0;
385
+ const hasHint = this.hasHintSlot || this.hint.length > 0;
386
+ const hasError = this.hasErrorSlot || this.errorText.length > 0;
387
+ const hasLabel = this.hasLabelSlot || this.label.length > 0;
388
+ return html `
389
+ <div part="form-control">
390
+ <label part="form-control-label" for=${this.triggerId} ?hidden=${!hasLabel}>
391
+ ${this.label}<slot name="label" @slotchange=${this.onLabelSlotChange}></slot>
392
+ </label>
393
+ <button
394
+ id=${this.triggerId}
395
+ part="trigger"
396
+ type="button"
397
+ role="combobox"
398
+ aria-haspopup="listbox"
399
+ aria-expanded=${this.open ? 'true' : 'false'}
400
+ aria-controls=${this.listId}
401
+ aria-activedescendant=${activeId}
402
+ aria-label=${this.getAttribute('aria-label') || this.label || this.placeholder || 'Select'}
403
+ aria-required=${this.required ? 'true' : 'false'}
404
+ aria-invalid=${this.touched && !this.internals.validity.valid ? 'true' : 'false'}
405
+ ?disabled=${this.disabled}
406
+ @click=${this.onTriggerClick}
407
+ @keydown=${this.onKeyDown}
408
+ @blur=${this.onTriggerBlur}
409
+ >
410
+ <span class="trigger-label" ?data-placeholder=${!hasValue}
411
+ >${hasValue ? selectedLabel : this.placeholder}</span
412
+ >
413
+ <span part="expand-icon" aria-hidden="true">${chevronIcon()}</span>
414
+ </button>
415
+ <div
416
+ part="listbox"
417
+ id=${this.listId}
418
+ role="listbox"
419
+ @mousedown=${this.onListboxMouseDown}
420
+ @click=${this.onListboxClick}
421
+ >
422
+ ${this.renderRows(options, activeId)}
423
+ </div>
424
+ <div part="error" ?hidden=${!hasError}>
425
+ ${this.errorText}<slot name="error" @slotchange=${this.onErrorSlotChange}></slot>
426
+ </div>
427
+ <div part="hint" ?hidden=${!hasHint}>
428
+ ${this.hint}<slot name="hint" @slotchange=${this.onHintSlotChange}></slot>
429
+ </div>
430
+ </div>
431
+ <slot @slotchange=${this.collectOptions} hidden></slot>
432
+ `;
433
+ }
434
+ }
435
+ __decorate([
436
+ property()
437
+ ], LyraSelect.prototype, "placeholder", void 0);
438
+ __decorate([
439
+ property({ type: Boolean, reflect: true })
440
+ ], LyraSelect.prototype, "disabled", void 0);
441
+ __decorate([
442
+ property({ type: Boolean, reflect: true })
443
+ ], LyraSelect.prototype, "required", void 0);
444
+ __decorate([
445
+ property()
446
+ ], LyraSelect.prototype, "name", void 0);
447
+ __decorate([
448
+ property()
449
+ ], LyraSelect.prototype, "label", void 0);
450
+ __decorate([
451
+ property()
452
+ ], LyraSelect.prototype, "hint", void 0);
453
+ __decorate([
454
+ property({ attribute: 'error-text' })
455
+ ], LyraSelect.prototype, "errorText", void 0);
456
+ __decorate([
457
+ property({ type: Boolean, reflect: true })
458
+ ], LyraSelect.prototype, "open", void 0);
459
+ __decorate([
460
+ property({ reflect: true })
461
+ ], LyraSelect.prototype, "size", void 0);
462
+ __decorate([
463
+ state()
464
+ ], LyraSelect.prototype, "activeIndex", void 0);
465
+ __decorate([
466
+ state()
467
+ ], LyraSelect.prototype, "options", void 0);
468
+ __decorate([
469
+ state()
470
+ ], LyraSelect.prototype, "touched", void 0);
471
+ __decorate([
472
+ state()
473
+ ], LyraSelect.prototype, "hasHintSlot", void 0);
474
+ __decorate([
475
+ state()
476
+ ], LyraSelect.prototype, "hasErrorSlot", void 0);
477
+ __decorate([
478
+ state()
479
+ ], LyraSelect.prototype, "hasLabelSlot", void 0);
480
+ defineElement('select', LyraSelect);
@@ -0,0 +1,21 @@
1
+ import type { Meta, StoryObj } from '@storybook/web-components-vite';
2
+ declare const meta: Meta;
3
+ export default meta;
4
+ type Story = StoryObj;
5
+ export declare const Default: Story;
6
+ /** `<lyra-option selected>` seeds the initial value, mirroring `<option selected>`. */
7
+ export declare const PreSelectedValue: Story;
8
+ /**
9
+ * Rows come from `<lyra-option>` children plus `group` (section headers),
10
+ * `sub` (a secondary line), and `dot-color` (a leading status dot) — same
11
+ * rich-row support as `<lyra-combobox>`.
12
+ */
13
+ export declare const RichRows: Story;
14
+ /** `size` spans the same `xs`–`xl` scale as `lyra-toast-item`, default `m`. */
15
+ export declare const Sizes: Story;
16
+ export declare const Disabled: Story;
17
+ /**
18
+ * `required` blocks form submission while empty; `error-text` renders a
19
+ * validation message alongside the shared `hint`.
20
+ */
21
+ export declare const RequiredWithValidation: Story;
@@ -0,0 +1,91 @@
1
+ import { html } from 'lit';
2
+ const meta = {
3
+ title: 'Select',
4
+ component: 'lyra-select',
5
+ tags: ['autodocs'],
6
+ };
7
+ export default meta;
8
+ export const Default = {
9
+ render: () => html `
10
+ <lyra-select label="Fruit" placeholder="Pick one…" style="max-width: 20rem">
11
+ <lyra-option value="a">Apple</lyra-option>
12
+ <lyra-option value="b">Banana</lyra-option>
13
+ <lyra-option value="c">Cherry</lyra-option>
14
+ <lyra-option value="d">Date</lyra-option>
15
+ </lyra-select>
16
+ `,
17
+ };
18
+ /** `<lyra-option selected>` seeds the initial value, mirroring `<option selected>`. */
19
+ export const PreSelectedValue = {
20
+ render: () => html `
21
+ <lyra-select label="Fruit" style="max-width: 20rem">
22
+ <lyra-option value="a">Apple</lyra-option>
23
+ <lyra-option value="b" selected>Banana</lyra-option>
24
+ <lyra-option value="c">Cherry</lyra-option>
25
+ <lyra-option value="d">Date</lyra-option>
26
+ </lyra-select>
27
+ `,
28
+ };
29
+ /**
30
+ * Rows come from `<lyra-option>` children plus `group` (section headers),
31
+ * `sub` (a secondary line), and `dot-color` (a leading status dot) — same
32
+ * rich-row support as `<lyra-combobox>`.
33
+ */
34
+ export const RichRows = {
35
+ render: () => html `
36
+ <lyra-select label="Inverter" placeholder="Pick one…" style="max-width: 22rem">
37
+ <lyra-option value="inv-1" group="Building A" sub="Running" dot-color="var(--lyra-color-success)">
38
+ Inverter 1
39
+ </lyra-option>
40
+ <lyra-option value="inv-2" group="Building A" sub="Idle" dot-color="var(--lyra-color-text-quiet)">
41
+ Inverter 2
42
+ </lyra-option>
43
+ <lyra-option value="inv-3" group="Building B" sub="Fault" dot-color="var(--lyra-color-danger)">
44
+ Inverter 3
45
+ </lyra-option>
46
+ </lyra-select>
47
+ `,
48
+ };
49
+ /** `size` spans the same `xs`–`xl` scale as `lyra-toast-item`, default `m`. */
50
+ export const Sizes = {
51
+ render: () => {
52
+ const sizes = ['xs', 's', 'm', 'l', 'xl'];
53
+ return html `
54
+ <div style="display: flex; flex-direction: column; gap: 1rem; max-width: 20rem">
55
+ ${sizes.map((size) => html `
56
+ <lyra-select size=${size} placeholder=${`Size "${size}"`}>
57
+ <lyra-option value="a">Apple</lyra-option>
58
+ <lyra-option value="b">Banana</lyra-option>
59
+ </lyra-select>
60
+ `)}
61
+ </div>
62
+ `;
63
+ },
64
+ };
65
+ export const Disabled = {
66
+ render: () => html `
67
+ <lyra-select label="Disabled" disabled placeholder="Can't touch this" style="max-width: 20rem">
68
+ <lyra-option value="a">Apple</lyra-option>
69
+ <lyra-option value="b">Banana</lyra-option>
70
+ </lyra-select>
71
+ `,
72
+ };
73
+ /**
74
+ * `required` blocks form submission while empty; `error-text` renders a
75
+ * validation message alongside the shared `hint`.
76
+ */
77
+ export const RequiredWithValidation = {
78
+ render: () => html `
79
+ <form style="display: flex; flex-direction: column; gap: 1.5rem; max-width: 22rem">
80
+ <lyra-select label="Required" required hint="Pick your favorite">
81
+ <lyra-option value="a">Apple</lyra-option>
82
+ <lyra-option value="b">Banana</lyra-option>
83
+ </lyra-select>
84
+ <lyra-select label="Invalid" required error-text="Selection required">
85
+ <lyra-option value="a">Apple</lyra-option>
86
+ <lyra-option value="b">Banana</lyra-option>
87
+ </lyra-select>
88
+ <button type="submit">Submit</button>
89
+ </form>
90
+ `,
91
+ };
@@ -0,0 +1 @@
1
+ export declare const styles: import("lit").CSSResult;