@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,83 @@
1
+ import { css } from 'lit';
2
+ export const styles = css `
3
+ :host {
4
+ display: block;
5
+ position: relative;
6
+ block-size: var(--lyra-chart-height, 280px);
7
+ /* Same token names/fallback chain as chart.ts's --lyra-chart-* — a host
8
+ already theming lyra-chart gets lyra-lite-chart themed for free, and
9
+ vice versa. Unlike chart.ts (canvas-rendered, can't consume var()
10
+ directly), this component is plain SVG/DOM, so these are read natively
11
+ by the CSS below — no getComputedStyle()/JS-side resolution needed. */
12
+ --lyra-chart-grid-color: var(--lyra-color-border);
13
+ --lyra-chart-tick-color: var(--lyra-color-text-quiet);
14
+ --lyra-chart-legend-color: var(--lyra-color-text);
15
+ --lyra-chart-tooltip-bg: var(--lyra-color-surface);
16
+ --lyra-chart-tooltip-text: var(--lyra-color-text);
17
+ }
18
+ [part='base'] {
19
+ position: relative;
20
+ inline-size: 100%;
21
+ block-size: 100%;
22
+ display: flex;
23
+ flex-direction: column;
24
+ gap: var(--lyra-space-xs);
25
+ }
26
+ svg {
27
+ flex: 1 1 auto;
28
+ inline-size: 100%;
29
+ block-size: 100%;
30
+ min-block-size: 0;
31
+ overflow: visible;
32
+ }
33
+ [part='grid-line'] {
34
+ stroke: var(--lyra-chart-grid-color);
35
+ stroke-width: 1;
36
+ }
37
+ [part='axis-label'] {
38
+ fill: var(--lyra-chart-tick-color);
39
+ font-size: 10px;
40
+ font-family: var(--lyra-font-family, inherit);
41
+ }
42
+ [part='axis-title'] {
43
+ fill: var(--lyra-chart-tick-color);
44
+ font-size: 11px;
45
+ font-weight: 600;
46
+ font-family: var(--lyra-font-family, inherit);
47
+ }
48
+ [part='bar'] {
49
+ cursor: pointer;
50
+ }
51
+ [part='bar']:focus-visible,
52
+ [part='point']:focus-visible {
53
+ outline: var(--lyra-focus-ring-width) solid var(--lyra-focus-ring-color);
54
+ outline-offset: 1px;
55
+ }
56
+ [part='line'] {
57
+ fill: none;
58
+ stroke-width: 2;
59
+ }
60
+ [part='point'] {
61
+ cursor: pointer;
62
+ }
63
+ [part='legend'] {
64
+ display: flex;
65
+ flex-wrap: wrap;
66
+ gap: var(--lyra-space-s);
67
+ justify-content: center;
68
+ flex: 0 0 auto;
69
+ }
70
+ [part='legend-item'] {
71
+ display: inline-flex;
72
+ align-items: center;
73
+ gap: 0.35em;
74
+ font-size: 0.8rem;
75
+ color: var(--lyra-chart-legend-color);
76
+ }
77
+ [part='legend-swatch'] {
78
+ inline-size: 0.7em;
79
+ block-size: 0.7em;
80
+ border-radius: 2px;
81
+ flex: 0 0 auto;
82
+ }
83
+ `;
@@ -29,15 +29,23 @@ export type ComboboxSource = (query: string) => Promise<ComboboxSourceRow[]>;
29
29
  * @event lyra-show - The listbox opened.
30
30
  * @event lyra-hide - The listbox closed.
31
31
  * @event lyra-clear - The value was cleared.
32
+ * @csspart form-control - The outer wrapper around label, combobox, listbox, error and hint.
33
+ * @csspart form-control-label - The `<label>` element.
32
34
  * @csspart combobox - The input container (positioning anchor).
33
35
  * @csspart combobox-input - The text input.
34
36
  * @csspart listbox - The options popover.
35
37
  * @csspart option - An option row.
38
+ * @csspart option-dot - An option row's leading status dot (when `dot-color` is set).
39
+ * @csspart option-label - An option row's label/sub wrapper.
40
+ * @csspart option-sub - An option row's secondary line (when `sub` is set).
41
+ * @csspart option-overflow - The "+N more" indicator shown when rows are capped by `maxRender`.
36
42
  * @csspart tags - The multi-select tag container.
37
43
  * @csspart tag - An individual selected tag.
44
+ * @csspart tag__remove-button - A tag's remove button.
38
45
  * @csspart clear-button - The clear button.
39
46
  * @csspart expand-icon - The dropdown indicator.
40
47
  * @csspart error - The error message.
48
+ * @csspart hint - The hint message.
41
49
  */
42
50
  export declare class LyraCombobox extends LyraElement {
43
51
  static formAssociated: boolean;
@@ -79,6 +87,7 @@ export declare class LyraCombobox extends LyraElement {
79
87
  private listId;
80
88
  private inputId;
81
89
  private cleanup?;
90
+ private _isFirstUpdate;
82
91
  private _selected;
83
92
  private _defaultSelected;
84
93
  private _defaultCaptured;
@@ -31,15 +31,23 @@ import './option.js';
31
31
  * @event lyra-show - The listbox opened.
32
32
  * @event lyra-hide - The listbox closed.
33
33
  * @event lyra-clear - The value was cleared.
34
+ * @csspart form-control - The outer wrapper around label, combobox, listbox, error and hint.
35
+ * @csspart form-control-label - The `<label>` element.
34
36
  * @csspart combobox - The input container (positioning anchor).
35
37
  * @csspart combobox-input - The text input.
36
38
  * @csspart listbox - The options popover.
37
39
  * @csspart option - An option row.
40
+ * @csspart option-dot - An option row's leading status dot (when `dot-color` is set).
41
+ * @csspart option-label - An option row's label/sub wrapper.
42
+ * @csspart option-sub - An option row's secondary line (when `sub` is set).
43
+ * @csspart option-overflow - The "+N more" indicator shown when rows are capped by `maxRender`.
38
44
  * @csspart tags - The multi-select tag container.
39
45
  * @csspart tag - An individual selected tag.
46
+ * @csspart tag__remove-button - A tag's remove button.
40
47
  * @csspart clear-button - The clear button.
41
48
  * @csspart expand-icon - The dropdown indicator.
42
49
  * @csspart error - The error message.
50
+ * @csspart hint - The hint message.
43
51
  */
44
52
  export class LyraCombobox extends LyraElement {
45
53
  static { this.formAssociated = true; }
@@ -91,6 +99,7 @@ export class LyraCombobox extends LyraElement {
91
99
  this._rowsByValue = new Map();
92
100
  this.listId = nextId('combobox-list');
93
101
  this.inputId = nextId('combobox-input');
102
+ this._isFirstUpdate = true;
94
103
  this._selected = [];
95
104
  // What `form.reset()` restores to. Captured exactly once, from whatever
96
105
  // `<lyra-option selected>` markup was present the first time slotted
@@ -104,6 +113,7 @@ export class LyraCombobox extends LyraElement {
104
113
  this._defaultCaptured = false;
105
114
  this.collectOptions = (e) => {
106
115
  const slot = e.target;
116
+ const previous = new Set(this.options);
107
117
  this.options = slot
108
118
  .assignedElements({ flatten: true })
109
119
  .filter((el) => el instanceof LyraOption);
@@ -123,6 +133,20 @@ export class LyraCombobox extends LyraElement {
123
133
  return; // `value=`'s setter already called reflectSelected()
124
134
  }
125
135
  }
136
+ else {
137
+ // Options slotted in after the first pass (e.g. a lazily-populated
138
+ // list appended post-connect) still declare selection the same way a
139
+ // native `<select><option selected>` would -- seed newly-arrived ones
140
+ // into the live selection instead of letting reflectSelected() below
141
+ // strip their `selected` attribute back off.
142
+ const newlySelected = this.options.filter((o) => !previous.has(o) && o.selected).map((o) => o.value);
143
+ if (newlySelected.length) {
144
+ this.value = this.multiple
145
+ ? [...new Set([...this._selected, ...newlySelected])]
146
+ : newlySelected[newlySelected.length - 1];
147
+ return; // `value=`'s setter already called reflectSelected()
148
+ }
149
+ }
126
150
  this.reflectSelected();
127
151
  };
128
152
  this.onDocPointer = (e) => {
@@ -139,6 +163,12 @@ export class LyraCombobox extends LyraElement {
139
163
  };
140
164
  this.onInputBlur = () => {
141
165
  this.touched = true;
166
+ // A mouse click outside the element is already handled by
167
+ // onDocPointer/hide(), but that leaves keyboard users with no way to
168
+ // dismiss the listbox short of Escape -- tabbing focus away from the
169
+ // input should close it too, the same as it would for a native
170
+ // `<select>`'s popup.
171
+ this.hide();
142
172
  };
143
173
  this.onHintSlotChange = (e) => {
144
174
  this.hasHintSlot = e.target.assignedElements({ flatten: true }).length > 0;
@@ -227,6 +257,11 @@ export class LyraCombobox extends LyraElement {
227
257
  this.updateValidity();
228
258
  }
229
259
  willUpdate() {
260
+ // `hasUpdated` flips to `true` before `updated()` even sees its first
261
+ // call, so it can't distinguish "just mounted" from "just changed" there
262
+ // -- capture that distinction here, while it's still reliable, for
263
+ // `updated()`'s `open`-handling below to consult.
264
+ this._isFirstUpdate = !this.hasUpdated;
230
265
  if (!this.hasUpdated) {
231
266
  this.hasHintSlot = Array.from(this.children).some((el) => el.getAttribute('slot') === 'hint');
232
267
  this.hasErrorSlot = Array.from(this.children).some((el) => el.getAttribute('slot') === 'error');
@@ -255,10 +290,19 @@ export class LyraCombobox extends LyraElement {
255
290
  }
256
291
  syncFormValue() {
257
292
  if (this.multiple) {
293
+ // A FormData form value submits under the keys baked into the FormData
294
+ // itself, bypassing the element's own `name` the way a plain string
295
+ // value would use it -- so an unnamed multi-select must contribute
296
+ // nothing (matching a nameless native `<select multiple>`) rather than
297
+ // inventing a shared key that would merge with any other unnamed
298
+ // combobox in the same form.
299
+ if (!this.name) {
300
+ this.internals.setFormValue(null);
301
+ return;
302
+ }
258
303
  const fd = new FormData();
259
- const key = this.name || 'value';
260
304
  for (const v of this._selected)
261
- fd.append(key, v);
305
+ fd.append(this.name, v);
262
306
  this.internals.setFormValue(fd);
263
307
  }
264
308
  else {
@@ -281,6 +325,7 @@ export class LyraCombobox extends LyraElement {
281
325
  disconnectedCallback() {
282
326
  super.disconnectedCallback();
283
327
  this.cleanup?.();
328
+ clearTimeout(this.sourceTimer);
284
329
  document.removeEventListener('pointerdown', this.onDocPointer);
285
330
  }
286
331
  reflectSelected() {
@@ -339,33 +384,41 @@ export class LyraCombobox extends LyraElement {
339
384
  if (this.open || this.disabled)
340
385
  return;
341
386
  this.open = true;
342
- this.emit('lyra-show');
343
- document.addEventListener('pointerdown', this.onDocPointer);
344
387
  }
345
388
  hide() {
346
389
  if (!this.open)
347
390
  return;
348
391
  this.open = false;
349
392
  this.activeIndex = -1;
350
- this.emit('lyra-hide');
351
- document.removeEventListener('pointerdown', this.onDocPointer);
352
393
  }
353
394
  updated(changed) {
354
395
  if (changed.has('open')) {
355
396
  this.cleanup?.();
356
397
  this.cleanup = undefined;
398
+ // All `open`-driven side effects (positioning, the click-outside
399
+ // listener, and the lyra-show/lyra-hide events) live here rather than
400
+ // in show()/hide() so they fire however `open` became true -- via
401
+ // show()/hide()'s own user-interaction paths, or a consumer/test
402
+ // setting `el.open` directly, which bypasses both entirely.
357
403
  if (this.open) {
404
+ document.addEventListener('pointerdown', this.onDocPointer);
405
+ // Don't announce a "show" transition for markup that's simply
406
+ // rendering open for the first time (e.g. `<lyra-combobox open>`) --
407
+ // only for an actual closed-to-open transition.
408
+ if (!this._isFirstUpdate)
409
+ this.emit('lyra-show');
358
410
  const anchor = this.renderRoot.querySelector('[part="combobox"]');
359
411
  const listbox = this.renderRoot.querySelector('[part="listbox"]');
360
412
  if (anchor && listbox)
361
413
  this.cleanup = place(anchor, listbox);
362
- // Reacting to the `open` property itself (not just inside show())
363
- // means this fires however `open` became true — via show()'s own
364
- // user-interaction paths, or a consumer/test setting `el.open = true`
365
- // directly, which bypasses show() entirely.
366
414
  if (this.source && this.asyncRows.length === 0)
367
415
  this.runSource(this.query);
368
416
  }
417
+ else {
418
+ document.removeEventListener('pointerdown', this.onDocPointer);
419
+ if (!this._isFirstUpdate)
420
+ this.emit('lyra-hide');
421
+ }
369
422
  }
370
423
  if (changed.has('required'))
371
424
  this.updateValidity();
@@ -388,6 +441,12 @@ export class LyraCombobox extends LyraElement {
388
441
  this.query = '';
389
442
  this.hide();
390
443
  }
444
+ // The query resets to '' above but `asyncRows` doesn't refresh on its
445
+ // own -- re-run `source` so the listbox (still open in multiple mode,
446
+ // and whatever `asyncRows` holds for the next time it reopens in single
447
+ // mode) matches the now-empty input instead of the stale prior query.
448
+ if (this.source)
449
+ this.runSource(this.query);
391
450
  this.emit('input');
392
451
  this.emit('change');
393
452
  }
@@ -398,6 +457,8 @@ export class LyraCombobox extends LyraElement {
398
457
  clear() {
399
458
  this.value = [];
400
459
  this.query = '';
460
+ if (this.source)
461
+ this.runSource(this.query);
401
462
  this.emit('lyra-clear');
402
463
  this.emit('change');
403
464
  }
@@ -493,6 +554,8 @@ export class LyraCombobox extends LyraElement {
493
554
  aria-controls=${this.listId}
494
555
  aria-activedescendant=${activeId}
495
556
  aria-autocomplete="list"
557
+ aria-required=${this.required ? 'true' : 'false'}
558
+ aria-invalid=${this.touched && !this.internals.validity.valid ? 'true' : 'false'}
496
559
  autocomplete="off"
497
560
  .value=${this.displayValue}
498
561
  placeholder=${hasValue && !this.multiple ? '' : this.placeholder}
@@ -526,9 +589,9 @@ export class LyraCombobox extends LyraElement {
526
589
  @click=${this.onListboxClick}
527
590
  >
528
591
  ${this.loading
529
- ? html `<div class="loading">Loading…</div>`
592
+ ? html `<div class="loading" role="option" aria-selected="false" aria-disabled="true">Loading…</div>`
530
593
  : rows.length === 0
531
- ? html `<div class="empty">${this.emptyText}</div>`
594
+ ? html `<div class="empty" role="option" aria-selected="false" aria-disabled="true">${this.emptyText}</div>`
532
595
  : html `${this.renderRows(rows, activeId)}
533
596
  ${overflow > 0
534
597
  ? html `<div part="option-overflow">+${overflow} more — refine your search</div>`
@@ -4,3 +4,21 @@ export default meta;
4
4
  type Story = StoryObj;
5
5
  export declare const Default: Story;
6
6
  export declare const Multiple: Story;
7
+ /**
8
+ * Rows come from `<lyra-option>` children plus `group` (section headers),
9
+ * `sub` (a secondary line), and `dot-color` (a leading status dot) — useful
10
+ * for richer pickers like a device or status list.
11
+ */
12
+ export declare const RichRows: Story;
13
+ /**
14
+ * `source` replaces the light-DOM `<lyra-option>` list with an async
15
+ * `(query) => Promise<ComboboxSourceRow[]>` lookup, debounced ~200ms after
16
+ * each keystroke. A "Loading…" row is shown while a call is in flight.
17
+ */
18
+ export declare const AsyncSource: Story;
19
+ /**
20
+ * `filter` overrides the default label/searchText matcher entirely, e.g. to
21
+ * match only from the start of the label instead of anywhere within it.
22
+ */
23
+ export declare const CustomFilter: Story;
24
+ export declare const States: Story;
@@ -25,3 +25,78 @@ export const Multiple = {
25
25
  </lyra-combobox>
26
26
  `,
27
27
  };
28
+ /**
29
+ * Rows come from `<lyra-option>` children plus `group` (section headers),
30
+ * `sub` (a secondary line), and `dot-color` (a leading status dot) — useful
31
+ * for richer pickers like a device or status list.
32
+ */
33
+ export const RichRows = {
34
+ render: () => html `
35
+ <lyra-combobox label="Inverter" placeholder="Pick one…" style="max-width: 22rem">
36
+ <lyra-option value="inv-1" group="Building A" sub="Running" dot-color="var(--lyra-color-success)">
37
+ Inverter 1
38
+ </lyra-option>
39
+ <lyra-option value="inv-2" group="Building A" sub="Idle" dot-color="var(--lyra-color-text-quiet)">
40
+ Inverter 2
41
+ </lyra-option>
42
+ <lyra-option value="inv-3" group="Building B" sub="Fault" dot-color="var(--lyra-color-danger)">
43
+ Inverter 3
44
+ </lyra-option>
45
+ </lyra-combobox>
46
+ `,
47
+ };
48
+ /**
49
+ * `source` replaces the light-DOM `<lyra-option>` list with an async
50
+ * `(query) => Promise<ComboboxSourceRow[]>` lookup, debounced ~200ms after
51
+ * each keystroke. A "Loading…" row is shown while a call is in flight.
52
+ */
53
+ export const AsyncSource = {
54
+ render: () => {
55
+ const all = ['Apple', 'Banana', 'Cherry', 'Date', 'Elderberry', 'Fig', 'Grape'];
56
+ const source = async (query) => {
57
+ await new Promise((r) => setTimeout(r, 400));
58
+ return all
59
+ .filter((label) => label.toLowerCase().includes(query.toLowerCase()))
60
+ .map((label) => ({ value: label.toLowerCase(), label }));
61
+ };
62
+ return html `
63
+ <lyra-combobox label="Fruit (async)" placeholder="Type to search…" with-clear
64
+ style="max-width: 22rem" .source=${source}
65
+ ></lyra-combobox>
66
+ `;
67
+ },
68
+ };
69
+ /**
70
+ * `filter` overrides the default label/searchText matcher entirely, e.g. to
71
+ * match only from the start of the label instead of anywhere within it.
72
+ */
73
+ export const CustomFilter = {
74
+ render: () => {
75
+ const startsWith = (option, query) => option.label.toLowerCase().startsWith(query.toLowerCase());
76
+ return html `
77
+ <lyra-combobox label="Fruit (starts with…)" placeholder="Try “an”…" style="max-width: 22rem"
78
+ .filter=${startsWith}
79
+ >
80
+ <lyra-option value="a">Apple</lyra-option>
81
+ <lyra-option value="b">Banana</lyra-option>
82
+ <lyra-option value="c">Cherry</lyra-option>
83
+ <lyra-option value="d">Date</lyra-option>
84
+ </lyra-combobox>
85
+ `;
86
+ },
87
+ };
88
+ export const States = {
89
+ render: () => html `
90
+ <div style="display: flex; flex-direction: column; gap: 1.5rem; max-width: 22rem">
91
+ <lyra-combobox label="Disabled" disabled placeholder="Can't touch this">
92
+ <lyra-option value="a">Apple</lyra-option>
93
+ </lyra-combobox>
94
+ <lyra-combobox label="Required" required hint="Pick your favorite">
95
+ <lyra-option value="a">Apple</lyra-option>
96
+ </lyra-combobox>
97
+ <lyra-combobox label="Invalid" required error-text="Selection required">
98
+ <lyra-option value="a">Apple</lyra-option>
99
+ </lyra-combobox>
100
+ </div>
101
+ `,
102
+ };
@@ -5,8 +5,19 @@ export function parseISO(s) {
5
5
  const m = /^(\d{4})-(\d{2})-(\d{2})$/.exec(s.trim());
6
6
  if (!m)
7
7
  return null;
8
- const d = new Date(Number(m[1]), Number(m[2]) - 1, Number(m[3]));
9
- return isNaN(d.getTime()) ? null : d;
8
+ const year = Number(m[1]);
9
+ const month = Number(m[2]);
10
+ const day = Number(m[3]);
11
+ const d = new Date(year, month - 1, day);
12
+ if (isNaN(d.getTime()))
13
+ return null;
14
+ // The Date constructor silently rolls invalid components over into the
15
+ // next month/year (e.g. month 13 or Feb 30) instead of producing an
16
+ // invalid date, so a calendar-invalid input must be caught by comparing
17
+ // the constructed date back against what was actually typed.
18
+ if (d.getFullYear() !== year || d.getMonth() !== month - 1 || d.getDate() !== day)
19
+ return null;
20
+ return d;
10
21
  }
11
22
  /** Format a Date as local `YYYY-MM-DD`. */
12
23
  export function formatISO(d) {
@@ -16,8 +16,10 @@ declare const LyraDateInput_base: typeof LyraElement & (new (...args: any[]) =>
16
16
  * @event change - Fired on committed date transitions.
17
17
  * @event lyra-show / lyra-hide - Calendar popover lifecycle.
18
18
  * @event lyra-clear - The clear button was used.
19
- * @csspart form-control, form-control-label, input-wrapper, input, clear-button, expand-button, popup, date-picker, hint, error
19
+ * @csspart form-control, form-control-label, input-wrapper, input, clear-button, expand-button, expand-icon, popup, date-picker, hint, error
20
+ * @slot label - Custom label content.
20
21
  * @slot error - Custom error content.
22
+ * @slot hint - Custom hint content.
21
23
  */
22
24
  export declare class LyraDateInput extends LyraDateInput_base {
23
25
  static styles: import("lit").CSSResultGroup[];
@@ -35,8 +37,12 @@ export declare class LyraDateInput extends LyraDateInput_base {
35
37
  months: 1 | 2;
36
38
  firstDayOfWeek: string;
37
39
  weekdayFormat: WeekdayFormat;
40
+ disablePast: boolean;
41
+ disableFuture: boolean;
42
+ withOutsideDays: boolean;
38
43
  private cleanupFn?;
39
44
  private inputId;
45
+ private popupId;
40
46
  private touched;
41
47
  private hasHintSlot;
42
48
  private hasErrorSlot;
@@ -54,10 +60,12 @@ export declare class LyraDateInput extends LyraDateInput_base {
54
60
  protected updated(changed: PropertyValues): void;
55
61
  private onInputChange;
56
62
  private onInputKey;
63
+ private onFormControlKey;
57
64
  private onInputBlur;
58
65
  private onHintSlotChange;
59
66
  private onErrorSlotChange;
60
67
  private onLabelSlotChange;
68
+ private onPickerInput;
61
69
  private onPickerChange;
62
70
  render(): TemplateResult;
63
71
  }
@@ -29,8 +29,10 @@ import './date-picker.js';
29
29
  * @event change - Fired on committed date transitions.
30
30
  * @event lyra-show / lyra-hide - Calendar popover lifecycle.
31
31
  * @event lyra-clear - The clear button was used.
32
- * @csspart form-control, form-control-label, input-wrapper, input, clear-button, expand-button, popup, date-picker, hint, error
32
+ * @csspart form-control, form-control-label, input-wrapper, input, clear-button, expand-button, expand-icon, popup, date-picker, hint, error
33
+ * @slot label - Custom label content.
33
34
  * @slot error - Custom error content.
35
+ * @slot hint - Custom hint content.
34
36
  */
35
37
  export class LyraDateInput extends FormAssociated(LyraElement) {
36
38
  constructor() {
@@ -49,7 +51,11 @@ export class LyraDateInput extends FormAssociated(LyraElement) {
49
51
  this.months = 1;
50
52
  this.firstDayOfWeek = 'auto';
51
53
  this.weekdayFormat = 'short';
54
+ this.disablePast = false;
55
+ this.disableFuture = false;
56
+ this.withOutsideDays = false;
52
57
  this.inputId = nextId('date-input');
58
+ this.popupId = nextId('date-popup');
53
59
  // Set on the date input's first `blur`; gates the `data-invalid`
54
60
  // reflection below so validity styling never flashes on first render.
55
61
  this.touched = false;
@@ -78,7 +84,13 @@ export class LyraDateInput extends FormAssociated(LyraElement) {
78
84
  this.emit('change');
79
85
  return;
80
86
  }
81
- const parsed = parseISO(raw) ?? (isNaN(Date.parse(raw)) ? null : new Date(Date.parse(raw)));
87
+ // A string that already looks like strict ISO (YYYY-MM-DD) must be judged
88
+ // by parseISO alone: it validates calendar correctness (rejects e.g.
89
+ // "2026-02-30"), but Date.parse() doesn't -- falling through to it for an
90
+ // ISO-shaped string would silently resurrect the same rollover parseISO
91
+ // guards against (Date.parse('2026-02-30') also normalizes to March 2).
92
+ const looksLikeISO = /^\d{4}-\d{2}-\d{2}$/.test(raw);
93
+ const parsed = looksLikeISO ? parseISO(raw) : isNaN(Date.parse(raw)) ? null : new Date(Date.parse(raw));
82
94
  if (parsed) {
83
95
  this.value = formatISO(parsed);
84
96
  this.emit('input');
@@ -100,6 +112,17 @@ export class LyraDateInput extends FormAssociated(LyraElement) {
100
112
  this.show();
101
113
  }
102
114
  };
115
+ // Attached to the whole form-control, not just the text input, so Escape
116
+ // closes the popover from anywhere inside it -- including the nested
117
+ // picker's own day/nav buttons, which take real DOM focus (roving
118
+ // tabindex) rather than the input keeping focus throughout.
119
+ this.onFormControlKey = (e) => {
120
+ if (e.key === 'Escape' && this.open) {
121
+ e.preventDefault();
122
+ this.hide();
123
+ this.renderRoot.querySelector('[part="expand-button"]')?.focus();
124
+ }
125
+ };
103
126
  this.onInputBlur = () => {
104
127
  this.touched = true;
105
128
  };
@@ -112,14 +135,25 @@ export class LyraDateInput extends FormAssociated(LyraElement) {
112
135
  this.onLabelSlotChange = (e) => {
113
136
  this.hasLabelSlot = e.target.assignedElements({ flatten: true }).length > 0;
114
137
  };
115
- this.onPickerChange = (e) => {
138
+ // The nested picker's `input` event isn't wired anywhere else, so without
139
+ // this listener it bubbles+composes straight through this shadow boundary
140
+ // (LyraElement.emit always dispatches bubbles:true, composed:true) and
141
+ // fires on this host a *second* time, on top of the explicit emit below.
142
+ this.onPickerInput = (e) => {
116
143
  e.stopPropagation();
117
144
  const picker = e.target;
118
145
  this.value = picker.value;
119
146
  this.emit('input');
147
+ };
148
+ this.onPickerChange = (e) => {
149
+ e.stopPropagation();
150
+ const picker = e.target;
151
+ this.value = picker.value;
120
152
  this.emit('change');
121
- if (this.mode === 'single')
122
- this.hide();
153
+ // The picker only fires `change` once a selection is finalized (a single
154
+ // pick, or the second click of a range), so this is always the right
155
+ // moment to close, in either mode.
156
+ this.hide();
123
157
  };
124
158
  }
125
159
  static { this.styles = [LyraElement.styles, styles]; }
@@ -189,7 +223,7 @@ export class LyraDateInput extends FormAssociated(LyraElement) {
189
223
  const hasError = this.hasErrorSlot || this.errorText.length > 0;
190
224
  const hasLabel = this.hasLabelSlot || this.label.length > 0;
191
225
  return html `
192
- <div part="form-control">
226
+ <div part="form-control" @keydown=${this.onFormControlKey}>
193
227
  <label part="form-control-label" for=${this.inputId} ?hidden=${!hasLabel}>
194
228
  ${this.label}<slot name="label" @slotchange=${this.onLabelSlotChange}></slot>
195
229
  </label>
@@ -223,13 +257,14 @@ export class LyraDateInput extends FormAssociated(LyraElement) {
223
257
  aria-label="Open calendar"
224
258
  aria-haspopup="dialog"
225
259
  aria-expanded=${this.open ? 'true' : 'false'}
260
+ aria-controls=${this.popupId}
226
261
  ?disabled=${this.disabled || this.readonly}
227
262
  @click=${() => (this.open ? this.hide() : this.show())}
228
263
  >
229
264
  <span part="expand-icon" aria-hidden="true">${calendarIcon()}</span>
230
265
  </button>
231
266
  </div>
232
- <div part="popup" role="dialog" aria-label="Choose date">
267
+ <div id=${this.popupId} part="popup" role="dialog" aria-label="Choose date">
233
268
  <lyra-date-picker
234
269
  part="date-picker"
235
270
  .value=${this.value}
@@ -240,8 +275,12 @@ export class LyraDateInput extends FormAssociated(LyraElement) {
240
275
  .locale=${this.locale}
241
276
  .disabled=${this.disabled}
242
277
  .readonly=${this.readonly}
278
+ .disablePast=${this.disablePast}
279
+ .disableFuture=${this.disableFuture}
280
+ .withOutsideDays=${this.withOutsideDays}
243
281
  first-day-of-week=${this.firstDayOfWeek}
244
282
  weekday-format=${this.weekdayFormat}
283
+ @input=${this.onPickerInput}
245
284
  @change=${this.onPickerChange}
246
285
  ></lyra-date-picker>
247
286
  </div>
@@ -297,6 +336,15 @@ __decorate([
297
336
  __decorate([
298
337
  property({ attribute: 'weekday-format' })
299
338
  ], LyraDateInput.prototype, "weekdayFormat", void 0);
339
+ __decorate([
340
+ property({ type: Boolean, attribute: 'disable-past' })
341
+ ], LyraDateInput.prototype, "disablePast", void 0);
342
+ __decorate([
343
+ property({ type: Boolean, attribute: 'disable-future' })
344
+ ], LyraDateInput.prototype, "disableFuture", void 0);
345
+ __decorate([
346
+ property({ type: Boolean, attribute: 'with-outside-days' })
347
+ ], LyraDateInput.prototype, "withOutsideDays", void 0);
300
348
  __decorate([
301
349
  state()
302
350
  ], LyraDateInput.prototype, "touched", void 0);
@@ -3,3 +3,8 @@ declare const meta: Meta;
3
3
  export default meta;
4
4
  type Story = StoryObj;
5
5
  export declare const Default: Story;
6
+ export declare const MinMax: Story;
7
+ export declare const DisablePast: Story;
8
+ export declare const DisableFuture: Story;
9
+ export declare const Range: Story;
10
+ export declare const WithOutsideDays: Story;
@@ -10,3 +10,39 @@ export const Default = {
10
10
  <lyra-date-input label="Start date" with-clear style="max-width: 16rem"></lyra-date-input>
11
11
  `,
12
12
  };
13
+ export const MinMax = {
14
+ render: () => html `
15
+ <lyra-date-input
16
+ label="Appointment date"
17
+ min="2026-07-10"
18
+ max="2026-07-20"
19
+ value="2026-07-15"
20
+ style="max-width: 16rem"
21
+ ></lyra-date-input>
22
+ `,
23
+ };
24
+ export const DisablePast = {
25
+ render: () => html `
26
+ <lyra-date-input label="Upcoming date" disable-past style="max-width: 16rem"></lyra-date-input>
27
+ `,
28
+ };
29
+ export const DisableFuture = {
30
+ render: () => html `
31
+ <lyra-date-input label="Historical date" disable-future style="max-width: 16rem"></lyra-date-input>
32
+ `,
33
+ };
34
+ export const Range = {
35
+ render: () => html `
36
+ <lyra-date-input label="Trip dates" mode="range" months="2" style="max-width: 20rem"></lyra-date-input>
37
+ `,
38
+ };
39
+ export const WithOutsideDays = {
40
+ render: () => html `
41
+ <lyra-date-input
42
+ label="Meeting date"
43
+ with-outside-days
44
+ value="2026-07-15"
45
+ style="max-width: 16rem"
46
+ ></lyra-date-input>
47
+ `,
48
+ };