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