@aceshooting/lyra-ui 0.1.0 → 0.1.2

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 (125) hide show
  1. package/README.md +77 -6
  2. package/custom-elements.json +12015 -0
  3. package/dist/components/chart/bar-chart.d.ts +14 -0
  4. package/dist/components/chart/bar-chart.js +14 -0
  5. package/dist/components/chart/box-plot.d.ts +46 -0
  6. package/dist/components/chart/box-plot.js +145 -0
  7. package/dist/components/chart/box-plot.styles.d.ts +1 -0
  8. package/dist/components/chart/box-plot.styles.js +22 -0
  9. package/dist/components/chart/bubble-chart.d.ts +19 -0
  10. package/dist/components/chart/bubble-chart.js +18 -0
  11. package/dist/components/chart/chart-loader.d.ts +8 -0
  12. package/dist/components/chart/chart-loader.js +26 -0
  13. package/dist/components/chart/chart.d.ts +91 -0
  14. package/dist/components/chart/chart.js +275 -0
  15. package/dist/components/chart/chart.styles.d.ts +1 -0
  16. package/dist/components/chart/chart.styles.js +33 -0
  17. package/dist/components/chart/doughnut-chart.d.ts +15 -0
  18. package/dist/components/chart/doughnut-chart.js +14 -0
  19. package/dist/components/chart/histogram-bin.d.ts +6 -0
  20. package/dist/components/chart/histogram-bin.js +19 -0
  21. package/dist/components/chart/histogram.d.ts +20 -0
  22. package/dist/components/chart/histogram.js +70 -0
  23. package/dist/components/chart/histogram.styles.d.ts +1 -0
  24. package/dist/components/chart/histogram.styles.js +1 -0
  25. package/dist/components/chart/line-chart.d.ts +14 -0
  26. package/dist/components/chart/line-chart.js +14 -0
  27. package/dist/components/chart/pie-chart.d.ts +16 -0
  28. package/dist/components/chart/pie-chart.js +15 -0
  29. package/dist/components/chart/polar-area-chart.d.ts +15 -0
  30. package/dist/components/chart/polar-area-chart.js +14 -0
  31. package/dist/components/chart/radar-chart.d.ts +15 -0
  32. package/dist/components/chart/radar-chart.js +14 -0
  33. package/dist/components/chart/scatter-chart.d.ts +15 -0
  34. package/dist/components/chart/scatter-chart.js +15 -0
  35. package/dist/components/combobox/combobox.d.ts +16 -0
  36. package/dist/components/combobox/combobox.js +113 -6
  37. package/dist/components/combobox/combobox.styles.js +68 -7
  38. package/dist/components/combobox/option.d.ts +2 -2
  39. package/dist/components/combobox/option.js +11 -7
  40. package/dist/components/date-picker/date-input.d.ts +12 -1
  41. package/dist/components/date-picker/date-input.js +68 -6
  42. package/dist/components/date-picker/date-input.styles.js +58 -6
  43. package/dist/components/date-picker/date-picker.js +5 -2
  44. package/dist/components/date-picker/date-picker.styles.js +9 -2
  45. package/dist/components/empty/empty.d.ts +32 -0
  46. package/dist/components/empty/empty.js +90 -0
  47. package/dist/components/empty/empty.styles.d.ts +1 -0
  48. package/dist/components/empty/empty.styles.js +42 -0
  49. package/dist/components/export-button/csv.d.ts +10 -0
  50. package/dist/components/export-button/csv.js +28 -0
  51. package/dist/components/export-button/export-button.d.ts +41 -0
  52. package/dist/components/export-button/export-button.js +151 -0
  53. package/dist/components/export-button/export-button.styles.d.ts +1 -0
  54. package/dist/components/export-button/export-button.styles.js +79 -0
  55. package/dist/components/file-input/accept.d.ts +14 -0
  56. package/dist/components/file-input/accept.js +30 -0
  57. package/dist/components/file-input/file-input.d.ts +47 -0
  58. package/dist/components/file-input/file-input.js +182 -0
  59. package/dist/components/file-input/file-input.styles.d.ts +1 -0
  60. package/dist/components/file-input/file-input.styles.js +44 -0
  61. package/dist/components/flag/flag.d.ts +4 -0
  62. package/dist/components/flag/flag.js +24 -1
  63. package/dist/components/flag/flag.styles.js +1 -1
  64. package/dist/components/gauge/gauge.d.ts +29 -0
  65. package/dist/components/gauge/gauge.js +133 -0
  66. package/dist/components/gauge/gauge.styles.d.ts +1 -0
  67. package/dist/components/gauge/gauge.styles.js +60 -0
  68. package/dist/components/graph/graph.d.ts +68 -0
  69. package/dist/components/graph/graph.js +276 -0
  70. package/dist/components/graph/graph.styles.d.ts +1 -0
  71. package/dist/components/graph/graph.styles.js +34 -0
  72. package/dist/components/heatmap/heatmap-scale.d.ts +8 -0
  73. package/dist/components/heatmap/heatmap-scale.js +17 -0
  74. package/dist/components/heatmap/heatmap.d.ts +66 -0
  75. package/dist/components/heatmap/heatmap.js +252 -0
  76. package/dist/components/heatmap/heatmap.styles.d.ts +1 -0
  77. package/dist/components/heatmap/heatmap.styles.js +39 -0
  78. package/dist/components/map/map-loader.d.ts +8 -0
  79. package/dist/components/map/map-loader.js +17 -0
  80. package/dist/components/map/map.d.ts +49 -0
  81. package/dist/components/map/map.js +189 -0
  82. package/dist/components/map/map.styles.d.ts +1 -0
  83. package/dist/components/map/map.styles.js +48 -0
  84. package/dist/components/playback/playback.d.ts +51 -0
  85. package/dist/components/playback/playback.js +146 -0
  86. package/dist/components/playback/playback.styles.d.ts +1 -0
  87. package/dist/components/playback/playback.styles.js +40 -0
  88. package/dist/components/skeleton/skeleton.d.ts +26 -0
  89. package/dist/components/skeleton/skeleton.js +65 -0
  90. package/dist/components/skeleton/skeleton.styles.d.ts +1 -0
  91. package/dist/components/skeleton/skeleton.styles.js +57 -0
  92. package/dist/components/split/split.d.ts +41 -0
  93. package/dist/components/split/split.js +171 -0
  94. package/dist/components/split/split.styles.d.ts +1 -0
  95. package/dist/components/split/split.styles.js +51 -0
  96. package/dist/components/stat/stat.d.ts +36 -0
  97. package/dist/components/stat/stat.js +117 -0
  98. package/dist/components/stat/stat.styles.d.ts +1 -0
  99. package/dist/components/stat/stat.styles.js +86 -0
  100. package/dist/components/table/table.d.ts +54 -0
  101. package/dist/components/table/table.js +201 -0
  102. package/dist/components/table/table.styles.d.ts +1 -0
  103. package/dist/components/table/table.styles.js +98 -0
  104. package/dist/components/time-range/time-range.d.ts +40 -0
  105. package/dist/components/time-range/time-range.js +219 -0
  106. package/dist/components/time-range/time-range.styles.d.ts +1 -0
  107. package/dist/components/time-range/time-range.styles.js +78 -0
  108. package/dist/components/toast/toast-item.js +10 -2
  109. package/dist/components/toast/toast-item.styles.js +13 -4
  110. package/dist/components/toast/toast.styles.js +0 -1
  111. package/dist/components/tree/tree-node.d.ts +50 -0
  112. package/dist/components/tree/tree-node.js +196 -0
  113. package/dist/components/tree/tree.d.ts +65 -0
  114. package/dist/components/tree/tree.js +227 -0
  115. package/dist/components/tree/tree.styles.d.ts +1 -0
  116. package/dist/components/tree/tree.styles.js +13 -0
  117. package/dist/internal/form-associated.js +49 -1
  118. package/dist/internal/icons.d.ts +11 -0
  119. package/dist/internal/icons.js +64 -0
  120. package/dist/internal/tokens.styles.js +41 -1
  121. package/dist/lyra.d.ts +67 -0
  122. package/dist/lyra.js +55 -0
  123. package/llms-full.txt +1425 -0
  124. package/llms.txt +47 -0
  125. package/package.json +60 -11
@@ -14,6 +14,7 @@ export type OptionFilter = (option: LyraOption, query: string) => boolean;
14
14
  * @slot - `<lyra-option>` elements.
15
15
  * @slot label - Custom label content.
16
16
  * @slot hint - Custom hint content.
17
+ * @slot error - Custom error content.
17
18
  * @event change - The selection changed.
18
19
  * @event input - The user typed in the filter or changed the selection.
19
20
  * @event lyra-show - The listbox opened.
@@ -27,6 +28,7 @@ export type OptionFilter = (option: LyraOption, query: string) => boolean;
27
28
  * @csspart tag - An individual selected tag.
28
29
  * @csspart clear-button - The clear button.
29
30
  * @csspart expand-icon - The dropdown indicator.
31
+ * @csspart error - The error message.
30
32
  */
31
33
  export declare class LyraCombobox extends LyraElement {
32
34
  static formAssociated: boolean;
@@ -43,6 +45,7 @@ export declare class LyraCombobox extends LyraElement {
43
45
  name: string;
44
46
  label: string;
45
47
  hint: string;
48
+ errorText: string;
46
49
  open: boolean;
47
50
  withClear: boolean;
48
51
  maxOptionsVisible: number;
@@ -51,14 +54,23 @@ export declare class LyraCombobox extends LyraElement {
51
54
  private query;
52
55
  private activeIndex;
53
56
  private options;
57
+ private touched;
58
+ private hasHintSlot;
59
+ private hasErrorSlot;
60
+ private hasLabelSlot;
54
61
  private internals;
55
62
  private listId;
56
63
  private cleanup?;
57
64
  private _selected;
65
+ private _defaultSelected;
66
+ private _defaultCaptured;
58
67
  constructor();
68
+ connectedCallback(): void;
69
+ protected willUpdate(): void;
59
70
  /** The selected value(s): a string in single mode, a string[] in `multiple` mode. */
60
71
  get value(): string | string[];
61
72
  set value(next: string | string[]);
73
+ private updateValidity;
62
74
  private syncFormValue;
63
75
  formResetCallback(): void;
64
76
  formDisabledCallback(disabled: boolean): void;
@@ -78,6 +90,10 @@ export declare class LyraCombobox extends LyraElement {
78
90
  private removeValue;
79
91
  private clear;
80
92
  private onInput;
93
+ private onInputBlur;
94
+ private onHintSlotChange;
95
+ private onErrorSlotChange;
96
+ private onLabelSlotChange;
81
97
  private onKeyDown;
82
98
  private onComboMouseDown;
83
99
  private renderRows;
@@ -10,6 +10,7 @@ import { LyraElement } from '../../internal/lyra-element.js';
10
10
  import { defineElement } from '../../internal/prefix.js';
11
11
  import { place } from '../../internal/positioner.js';
12
12
  import { nextId } from '../../internal/a11y.js';
13
+ import { chevronIcon, closeIcon } from '../../internal/icons.js';
13
14
  import { styles } from './combobox.styles.js';
14
15
  import { LyraOption } from './option.js';
15
16
  import './option.js';
@@ -24,6 +25,7 @@ import './option.js';
24
25
  * @slot - `<lyra-option>` elements.
25
26
  * @slot label - Custom label content.
26
27
  * @slot hint - Custom hint content.
28
+ * @slot error - Custom error content.
27
29
  * @event change - The selection changed.
28
30
  * @event input - The user typed in the filter or changed the selection.
29
31
  * @event lyra-show - The listbox opened.
@@ -37,6 +39,7 @@ import './option.js';
37
39
  * @csspart tag - An individual selected tag.
38
40
  * @csspart clear-button - The clear button.
39
41
  * @csspart expand-icon - The dropdown indicator.
42
+ * @csspart error - The error message.
40
43
  */
41
44
  export class LyraCombobox extends LyraElement {
42
45
  static { this.formAssociated = true; }
@@ -53,6 +56,7 @@ export class LyraCombobox extends LyraElement {
53
56
  this.name = '';
54
57
  this.label = '';
55
58
  this.hint = '';
59
+ this.errorText = '';
56
60
  this.open = false;
57
61
  this.withClear = false;
58
62
  this.maxOptionsVisible = 3;
@@ -61,13 +65,53 @@ export class LyraCombobox extends LyraElement {
61
65
  this.query = '';
62
66
  this.activeIndex = -1;
63
67
  this.options = [];
68
+ // Set on the combobox input's first `blur`; gates the `data-invalid`
69
+ // reflection below so validity styling never flashes on first render.
70
+ this.touched = false;
71
+ // `[part]:empty` never matches — the part always contains a literal
72
+ // `<slot>` child element regardless of assigned content — so real
73
+ // emptiness is tracked in JS instead (same fix as lyra-stat's
74
+ // icon/caption, commit 6c1004c) and reflected via `hidden`. Applies to
75
+ // `form-control-label` too: the required-asterisk `::after` attaches to
76
+ // that box, so leaving it always-visible orphans a stray ' *' when no
77
+ // `label` is set.
78
+ this.hasHintSlot = false;
79
+ this.hasErrorSlot = false;
80
+ this.hasLabelSlot = false;
64
81
  this.listId = nextId('combobox-list');
65
82
  this._selected = [];
83
+ // What `form.reset()` restores to. Captured exactly once, from whatever
84
+ // `<lyra-option selected>` markup was present the first time slotted
85
+ // options are collected (mirrors native `<select><option selected>`) —
86
+ // never from the `value` setter, so a user picking an option (even the
87
+ // very first pick on an initially-unselected combobox) can't itself
88
+ // become the reset default (native `defaultValue`/`defaultSelected`
89
+ // semantics: only declarative/attribute state feeds the default, plain
90
+ // property assignment never does).
91
+ this._defaultSelected = [];
92
+ this._defaultCaptured = false;
66
93
  this.collectOptions = (e) => {
67
94
  const slot = e.target;
68
95
  this.options = slot
69
96
  .assignedElements({ flatten: true })
70
97
  .filter((el) => el instanceof LyraOption);
98
+ if (!this._defaultCaptured) {
99
+ this._defaultCaptured = true;
100
+ // Seed the initial selection — and the reset default — from
101
+ // declarative `<lyra-option selected>` markup — mirrors native
102
+ // `<select><option selected>` — the exact case the audit found
103
+ // silently ignored (forms-core §combobox, Medium). This is the only
104
+ // place `_defaultSelected` is set; picking an option later (the
105
+ // `value` setter) never redefines the reset default, so an
106
+ // initially-unselected combobox always resets back to empty even
107
+ // after the user has picked something.
108
+ const declared = this.options.filter((o) => o.selected).map((o) => o.value);
109
+ this._defaultSelected = [...declared];
110
+ if (declared.length) {
111
+ this.value = this.multiple ? declared : declared[0];
112
+ return; // `value=`'s setter already called reflectSelected()
113
+ }
114
+ }
71
115
  this.reflectSelected();
72
116
  };
73
117
  this.onDocPointer = (e) => {
@@ -80,6 +124,18 @@ export class LyraCombobox extends LyraElement {
80
124
  this.show();
81
125
  this.emit('input');
82
126
  };
127
+ this.onInputBlur = () => {
128
+ this.touched = true;
129
+ };
130
+ this.onHintSlotChange = (e) => {
131
+ this.hasHintSlot = e.target.assignedElements({ flatten: true }).length > 0;
132
+ };
133
+ this.onErrorSlotChange = (e) => {
134
+ this.hasErrorSlot = e.target.assignedElements({ flatten: true }).length > 0;
135
+ };
136
+ this.onLabelSlotChange = (e) => {
137
+ this.hasLabelSlot = e.target.assignedElements({ flatten: true }).length > 0;
138
+ };
83
139
  this.onKeyDown = (e) => {
84
140
  const opts = this.filtered.filter((o) => !o.disabled);
85
141
  switch (e.key) {
@@ -135,6 +191,17 @@ export class LyraCombobox extends LyraElement {
135
191
  };
136
192
  this.internals = this.attachInternals();
137
193
  }
194
+ connectedCallback() {
195
+ super.connectedCallback();
196
+ this.updateValidity();
197
+ }
198
+ willUpdate() {
199
+ if (!this.hasUpdated) {
200
+ this.hasHintSlot = Array.from(this.children).some((el) => el.getAttribute('slot') === 'hint');
201
+ this.hasErrorSlot = Array.from(this.children).some((el) => el.getAttribute('slot') === 'error');
202
+ this.hasLabelSlot = Array.from(this.children).some((el) => el.getAttribute('slot') === 'label');
203
+ }
204
+ }
138
205
  /** The selected value(s): a string in single mode, a string[] in `multiple` mode. */
139
206
  get value() {
140
207
  return this.multiple ? [...this._selected] : (this._selected[0] ?? '');
@@ -144,8 +211,17 @@ export class LyraCombobox extends LyraElement {
144
211
  this._selected = Array.isArray(next) ? [...next] : next ? [next] : [];
145
212
  this.syncFormValue();
146
213
  this.reflectSelected();
214
+ this.updateValidity();
147
215
  this.requestUpdate('value', old);
148
216
  }
217
+ updateValidity() {
218
+ if (this.required && this._selected.length === 0) {
219
+ this.internals.setValidity({ valueMissing: true }, 'Please select an option.');
220
+ }
221
+ else {
222
+ this.internals.setValidity({});
223
+ }
224
+ }
149
225
  syncFormValue() {
150
226
  if (this.multiple) {
151
227
  const fd = new FormData();
@@ -159,7 +235,7 @@ export class LyraCombobox extends LyraElement {
159
235
  }
160
236
  }
161
237
  formResetCallback() {
162
- this.value = [];
238
+ this.value = [...this._defaultSelected];
163
239
  this.query = '';
164
240
  }
165
241
  formDisabledCallback(disabled) {
@@ -225,6 +301,11 @@ export class LyraCombobox extends LyraElement {
225
301
  this.cleanup = place(anchor, listbox);
226
302
  }
227
303
  }
304
+ if (changed.has('required'))
305
+ this.updateValidity();
306
+ if (changed.has('touched') || changed.has('required') || changed.has('value')) {
307
+ this.toggleAttribute('data-invalid', this.touched && !this.internals.validity.valid);
308
+ }
228
309
  }
229
310
  pick(option) {
230
311
  if (option.disabled)
@@ -288,9 +369,14 @@ export class LyraCombobox extends LyraElement {
288
369
  const shownTags = this.multiple ? this._selected.slice(0, this.maxOptionsVisible) : [];
289
370
  const extra = this.multiple ? this._selected.length - shownTags.length : 0;
290
371
  const hasValue = this._selected.length > 0;
372
+ const hasHint = this.hasHintSlot || this.hint.length > 0;
373
+ const hasError = this.hasErrorSlot || this.errorText.length > 0;
374
+ const hasLabel = this.hasLabelSlot || this.label.length > 0;
291
375
  return html `
292
376
  <div part="form-control">
293
- <label part="form-control-label">${this.label}<slot name="label"></slot></label>
377
+ <label part="form-control-label" ?hidden=${!hasLabel}>
378
+ ${this.label}<slot name="label" @slotchange=${this.onLabelSlotChange}></slot>
379
+ </label>
294
380
  <div part="combobox" @mousedown=${this.onComboMouseDown}>
295
381
  <div part="tags" style="display:contents">
296
382
  ${shownTags.map((v) => html `<span part="tag"
@@ -303,7 +389,7 @@ export class LyraCombobox extends LyraElement {
303
389
  this.removeValue(v);
304
390
  }}
305
391
  >
306
- &times;</button
392
+ ${closeIcon()}</button
307
393
  ></span
308
394
  >`)}
309
395
  ${extra > 0 ? html `<span part="tag">+${extra}</span>` : ''}
@@ -323,6 +409,7 @@ export class LyraCombobox extends LyraElement {
323
409
  @input=${this.onInput}
324
410
  @keydown=${this.onKeyDown}
325
411
  @focus=${() => this.show()}
412
+ @blur=${this.onInputBlur}
326
413
  />
327
414
  ${this.withClear && hasValue
328
415
  ? html `<button
@@ -334,17 +421,22 @@ export class LyraCombobox extends LyraElement {
334
421
  this.clear();
335
422
  }}
336
423
  >
337
- &times;
424
+ ${closeIcon()}
338
425
  </button>`
339
426
  : ''}
340
- <span part="expand-icon" aria-hidden="true">▾</span>
427
+ <span part="expand-icon" aria-hidden="true">${chevronIcon()}</span>
341
428
  </div>
342
429
  <div part="listbox" id=${this.listId} role="listbox" aria-multiselectable=${this.multiple ? 'true' : 'false'}>
343
430
  ${filtered.length === 0
344
431
  ? html `<div class="empty">${this.emptyText}</div>`
345
432
  : this.renderRows(filtered, activeId)}
346
433
  </div>
347
- <div part="hint">${this.hint}<slot name="hint"></slot></div>
434
+ <div part="error" ?hidden=${!hasError}>
435
+ ${this.errorText}<slot name="error" @slotchange=${this.onErrorSlotChange}></slot>
436
+ </div>
437
+ <div part="hint" ?hidden=${!hasHint}>
438
+ ${this.hint}<slot name="hint" @slotchange=${this.onHintSlotChange}></slot>
439
+ </div>
348
440
  </div>
349
441
  <slot @slotchange=${this.collectOptions} hidden></slot>
350
442
  `;
@@ -371,6 +463,9 @@ __decorate([
371
463
  __decorate([
372
464
  property()
373
465
  ], LyraCombobox.prototype, "hint", void 0);
466
+ __decorate([
467
+ property({ attribute: 'error-text' })
468
+ ], LyraCombobox.prototype, "errorText", void 0);
374
469
  __decorate([
375
470
  property({ type: Boolean, reflect: true })
376
471
  ], LyraCombobox.prototype, "open", void 0);
@@ -395,4 +490,16 @@ __decorate([
395
490
  __decorate([
396
491
  state()
397
492
  ], LyraCombobox.prototype, "options", void 0);
493
+ __decorate([
494
+ state()
495
+ ], LyraCombobox.prototype, "touched", void 0);
496
+ __decorate([
497
+ state()
498
+ ], LyraCombobox.prototype, "hasHintSlot", void 0);
499
+ __decorate([
500
+ state()
501
+ ], LyraCombobox.prototype, "hasErrorSlot", void 0);
502
+ __decorate([
503
+ state()
504
+ ], LyraCombobox.prototype, "hasLabelSlot", void 0);
398
505
  defineElement('combobox', LyraCombobox);
@@ -9,9 +9,19 @@ export const styles = css `
9
9
  font-size: 0.875rem;
10
10
  font-weight: 600;
11
11
  }
12
- [part='form-control-label']:empty {
12
+ /* :empty never matches here -- the part always contains a literal slot
13
+ child element regardless of assigned/text content -- so real emptiness
14
+ is tracked in JS (hasLabelSlot) and reflected via the hidden attribute
15
+ instead (same fix as [part='hint']/[part='error'] below). Without this,
16
+ the required-asterisk ::after below (which attaches to this box)
17
+ renders a stray ' *' with nothing before it whenever label is unset. */
18
+ [part='form-control-label'][hidden] {
13
19
  display: none;
14
20
  }
21
+ :host([required]) [part='form-control-label']::after {
22
+ content: ' *';
23
+ color: var(--lyra-color-danger);
24
+ }
15
25
 
16
26
  [part='combobox'] {
17
27
  display: flex;
@@ -31,7 +41,9 @@ export const styles = css `
31
41
  outline: 2px solid transparent;
32
42
  }
33
43
  :host([disabled]) [part='combobox'] {
34
- opacity: 0.5;
44
+ /* was a literal 0.5; now the shared library-wide disabled-state token
45
+ (still 0.5 by default fallback, so no visual change here). */
46
+ opacity: var(--lyra-opacity-disabled);
35
47
  cursor: not-allowed;
36
48
  }
37
49
 
@@ -68,16 +80,34 @@ export const styles = css `
68
80
  [part='clear-button'],
69
81
  [part='expand-icon'] {
70
82
  flex: 0 0 auto;
83
+ display: inline-flex;
84
+ align-items: center;
85
+ justify-content: center;
71
86
  border: none;
72
87
  background: none;
73
88
  cursor: pointer;
74
89
  color: var(--lyra-color-text-quiet);
75
- padding: 0 var(--lyra-space-xs);
90
+ padding: var(--lyra-space-xs);
91
+ /* Real touch target in *both* dimensions (WCAG 2.2 SC 2.5.8 needs
92
+ 24x24 CSS px, not just height — a design-review fix that only set
93
+ min-block-size left these buttons ~21px wide). icon-button-size is
94
+ the ceiling, but never grow past what the [part=combobox] row's own
95
+ 2.5rem min-block-size has room for once its own block padding is
96
+ subtracted. */
97
+ min-inline-size: min(var(--lyra-icon-button-size), 1.75rem);
98
+ min-block-size: min(var(--lyra-icon-button-size), 1.75rem);
76
99
  line-height: 1;
77
100
  }
101
+ [part='expand-icon'] svg {
102
+ transform: rotate(90deg);
103
+ }
104
+ [part='clear-button']:focus-visible,
105
+ [part='tag__remove-button']:focus-visible {
106
+ outline: var(--lyra-focus-ring-width) solid var(--lyra-focus-ring-color);
107
+ outline-offset: var(--lyra-focus-ring-offset);
108
+ }
78
109
 
79
110
  [part='listbox'] {
80
- display: none;
81
111
  position: fixed;
82
112
  z-index: 900;
83
113
  box-sizing: border-box;
@@ -91,9 +121,26 @@ export const styles = css `
91
121
  border: 1px solid var(--lyra-color-border);
92
122
  border-radius: var(--lyra-radius);
93
123
  box-shadow: var(--lyra-shadow);
124
+ /* Closed state: invisible + slightly raised. visibility (not
125
+ display:none) so opacity/transform can actually transition; hit-testing
126
+ and a11y exposure stay off since this part is already position:fixed. */
127
+ visibility: hidden;
128
+ opacity: 0;
129
+ transform: translateY(-0.25rem);
130
+ transition:
131
+ opacity var(--lyra-transition-fast),
132
+ transform var(--lyra-transition-fast),
133
+ visibility var(--lyra-transition-fast);
94
134
  }
95
135
  :host([open]) [part='listbox'] {
96
- display: block;
136
+ visibility: visible;
137
+ opacity: 1;
138
+ transform: translateY(0);
139
+ }
140
+ @media (prefers-reduced-motion: reduce) {
141
+ [part='listbox'] {
142
+ transition: none !important;
143
+ }
97
144
  }
98
145
 
99
146
  [part='option'] {
@@ -120,7 +167,9 @@ export const styles = css `
120
167
  font-weight: 600;
121
168
  }
122
169
  [part='option'][aria-disabled='true'] {
123
- opacity: 0.4;
170
+ /* was a literal 0.4; unified with the rest of the library's single
171
+ disabled-state opacity token (intentionally changes 0.4 -> 0.5). */
172
+ opacity: var(--lyra-opacity-disabled);
124
173
  cursor: not-allowed;
125
174
  }
126
175
 
@@ -142,7 +191,19 @@ export const styles = css `
142
191
  font-size: 0.8125rem;
143
192
  color: var(--lyra-color-text-quiet);
144
193
  }
145
- [part='hint']:empty {
194
+ /* :empty never matches here -- the part always contains a literal
195
+ slot child element regardless of assigned/text content -- so real
196
+ emptiness is tracked in JS (hasHintSlot/hasErrorSlot) and reflected via
197
+ the hidden attribute instead (same fix as lyra-stat's icon/caption). */
198
+ [part='hint'][hidden] {
199
+ display: none;
200
+ }
201
+ [part='error'] {
202
+ margin-block-start: var(--lyra-space-xs);
203
+ font-size: 0.8125rem;
204
+ color: var(--lyra-color-danger);
205
+ }
206
+ [part='error'][hidden] {
146
207
  display: none;
147
208
  }
148
209
  `;
@@ -10,7 +10,7 @@ import { LyraElement } from '../../internal/lyra-element.js';
10
10
  * @customElement lyra-option
11
11
  */
12
12
  export declare class LyraOption extends LyraElement {
13
- static styles: import("lit").CSSResult;
13
+ static styles: import("lit").CSSResultGroup[];
14
14
  /** The selection key submitted with the form. */
15
15
  value: string;
16
16
  /** Disable selecting this option. */
@@ -21,7 +21,7 @@ export declare class LyraOption extends LyraElement {
21
21
  group: string;
22
22
  /** Extra text the filter should match beyond the label. */
23
23
  searchText: string;
24
- /** Resolved label: explicit `label` attribute wins, else trimmed text content. */
24
+ /** Resolved label: explicit non-empty `label` attribute wins, else trimmed text content. */
25
25
  get label(): string;
26
26
  render(): TemplateResult;
27
27
  }
@@ -31,14 +31,18 @@ export class LyraOption extends LyraElement {
31
31
  /** Extra text the filter should match beyond the label. */
32
32
  this.searchText = '';
33
33
  }
34
- static { this.styles = css `
35
- :host {
36
- display: none;
37
- }
38
- `; }
39
- /** Resolved label: explicit `label` attribute wins, else trimmed text content. */
34
+ static { this.styles = [
35
+ LyraElement.styles,
36
+ css `
37
+ :host {
38
+ display: none;
39
+ }
40
+ `,
41
+ ]; }
42
+ /** Resolved label: explicit non-empty `label` attribute wins, else trimmed text content. */
40
43
  get label() {
41
- return (this.getAttribute('label') ?? this.textContent ?? '').trim();
44
+ const attr = this.getAttribute('label');
45
+ return (attr ? attr : (this.textContent ?? '')).trim();
42
46
  }
43
47
  render() {
44
48
  return html `<slot></slot>`;
@@ -16,7 +16,8 @@ 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
19
+ * @csspart form-control, form-control-label, input-wrapper, input, clear-button, expand-button, popup, date-picker, hint, error
20
+ * @slot error - Custom error content.
20
21
  */
21
22
  export declare class LyraDateInput extends LyraDateInput_base {
22
23
  static styles: import("lit").CSSResultGroup[];
@@ -28,13 +29,19 @@ export declare class LyraDateInput extends LyraDateInput_base {
28
29
  withClear: boolean;
29
30
  label: string;
30
31
  hint: string;
32
+ errorText: string;
31
33
  placeholder: string;
32
34
  locale: string;
33
35
  months: 1 | 2;
34
36
  firstDayOfWeek: string;
35
37
  weekdayFormat: WeekdayFormat;
36
38
  private cleanupFn?;
39
+ private touched;
40
+ private hasHintSlot;
41
+ private hasErrorSlot;
42
+ private hasLabelSlot;
37
43
  private get displayText();
44
+ protected willUpdate(): void;
38
45
  /** Open the calendar popover. */
39
46
  show(): void;
40
47
  /** Close the calendar popover. */
@@ -46,6 +53,10 @@ export declare class LyraDateInput extends LyraDateInput_base {
46
53
  protected updated(changed: PropertyValues): void;
47
54
  private onInputChange;
48
55
  private onInputKey;
56
+ private onInputBlur;
57
+ private onHintSlotChange;
58
+ private onErrorSlotChange;
59
+ private onLabelSlotChange;
49
60
  private onPickerChange;
50
61
  render(): TemplateResult;
51
62
  }
@@ -10,6 +10,7 @@ import { LyraElement } from '../../internal/lyra-element.js';
10
10
  import { FormAssociated } from '../../internal/form-associated.js';
11
11
  import { defineElement } from '../../internal/prefix.js';
12
12
  import { place } from '../../internal/positioner.js';
13
+ import { closeIcon, calendarIcon } from '../../internal/icons.js';
13
14
  import { parseISO, formatISO } from './calendar-core.js';
14
15
  import { styles } from './date-input.styles.js';
15
16
  import { LyraDatePicker } from './date-picker.js';
@@ -27,7 +28,8 @@ import './date-picker.js';
27
28
  * @event change - Fired on committed date transitions.
28
29
  * @event lyra-show / lyra-hide - Calendar popover lifecycle.
29
30
  * @event lyra-clear - The clear button was used.
30
- * @csspart form-control, form-control-label, input-wrapper, input, clear-button, expand-button, popup, date-picker, hint
31
+ * @csspart form-control, form-control-label, input-wrapper, input, clear-button, expand-button, popup, date-picker, hint, error
32
+ * @slot error - Custom error content.
31
33
  */
32
34
  export class LyraDateInput extends FormAssociated(LyraElement) {
33
35
  constructor() {
@@ -40,11 +42,25 @@ export class LyraDateInput extends FormAssociated(LyraElement) {
40
42
  this.withClear = false;
41
43
  this.label = '';
42
44
  this.hint = '';
45
+ this.errorText = '';
43
46
  this.placeholder = '';
44
47
  this.locale = '';
45
48
  this.months = 1;
46
49
  this.firstDayOfWeek = 'auto';
47
50
  this.weekdayFormat = 'short';
51
+ // Set on the date input's first `blur`; gates the `data-invalid`
52
+ // reflection below so validity styling never flashes on first render.
53
+ this.touched = false;
54
+ // `[part]:empty` never matches — the part always contains a literal
55
+ // `<slot>` child element regardless of assigned content — so real
56
+ // emptiness is tracked in JS instead (same fix as lyra-stat's
57
+ // icon/caption, commit 6c1004c) and reflected via `hidden`. Applies to
58
+ // `form-control-label` too: the required-asterisk `::after` attaches to
59
+ // that box, so leaving it always-visible orphans a stray ' *' when no
60
+ // `label` is set.
61
+ this.hasHintSlot = false;
62
+ this.hasErrorSlot = false;
63
+ this.hasLabelSlot = false;
48
64
  this.onDocPointer = (e) => {
49
65
  if (!e.composedPath().includes(this))
50
66
  this.hide();
@@ -70,6 +86,18 @@ export class LyraDateInput extends FormAssociated(LyraElement) {
70
86
  this.show();
71
87
  }
72
88
  };
89
+ this.onInputBlur = () => {
90
+ this.touched = true;
91
+ };
92
+ this.onHintSlotChange = (e) => {
93
+ this.hasHintSlot = e.target.assignedElements({ flatten: true }).length > 0;
94
+ };
95
+ this.onErrorSlotChange = (e) => {
96
+ this.hasErrorSlot = e.target.assignedElements({ flatten: true }).length > 0;
97
+ };
98
+ this.onLabelSlotChange = (e) => {
99
+ this.hasLabelSlot = e.target.assignedElements({ flatten: true }).length > 0;
100
+ };
73
101
  this.onPickerChange = (e) => {
74
102
  e.stopPropagation();
75
103
  const picker = e.target;
@@ -90,6 +118,13 @@ export class LyraDateInput extends FormAssociated(LyraElement) {
90
118
  const fmt = (d) => d.toLocaleDateString(this.locale || undefined);
91
119
  return this.mode === 'range' && to ? `${fmt(from)} – ${fmt(to)}` : fmt(from);
92
120
  }
121
+ willUpdate() {
122
+ if (!this.hasUpdated) {
123
+ this.hasHintSlot = Array.from(this.children).some((el) => el.getAttribute('slot') === 'hint');
124
+ this.hasErrorSlot = Array.from(this.children).some((el) => el.getAttribute('slot') === 'error');
125
+ this.hasLabelSlot = Array.from(this.children).some((el) => el.getAttribute('slot') === 'label');
126
+ }
127
+ }
93
128
  /** Open the calendar popover. */
94
129
  show() {
95
130
  if (this.open || this.disabled || this.readonly)
@@ -119,6 +154,7 @@ export class LyraDateInput extends FormAssociated(LyraElement) {
119
154
  document.removeEventListener('pointerdown', this.onDocPointer);
120
155
  }
121
156
  updated(changed) {
157
+ super.updated(changed);
122
158
  if (changed.has('open')) {
123
159
  this.cleanupFn?.();
124
160
  this.cleanupFn = undefined;
@@ -129,12 +165,20 @@ export class LyraDateInput extends FormAssociated(LyraElement) {
129
165
  this.cleanupFn = place(anchor, popup);
130
166
  }
131
167
  }
168
+ if (changed.has('touched') || changed.has('required') || changed.has('value')) {
169
+ this.toggleAttribute('data-invalid', this.touched && !this.internals.validity.valid);
170
+ }
132
171
  }
133
172
  render() {
134
173
  const hasValue = this.value.length > 0;
174
+ const hasHint = this.hasHintSlot || this.hint.length > 0;
175
+ const hasError = this.hasErrorSlot || this.errorText.length > 0;
176
+ const hasLabel = this.hasLabelSlot || this.label.length > 0;
135
177
  return html `
136
178
  <div part="form-control">
137
- <label part="form-control-label">${this.label}<slot name="label"></slot></label>
179
+ <label part="form-control-label" ?hidden=${!hasLabel}>
180
+ ${this.label}<slot name="label" @slotchange=${this.onLabelSlotChange}></slot>
181
+ </label>
138
182
  <div part="input-wrapper">
139
183
  <input
140
184
  part="input"
@@ -146,6 +190,7 @@ export class LyraDateInput extends FormAssociated(LyraElement) {
146
190
  ?readonly=${this.readonly}
147
191
  @change=${this.onInputChange}
148
192
  @keydown=${this.onInputKey}
193
+ @blur=${this.onInputBlur}
149
194
  />
150
195
  ${this.withClear && hasValue
151
196
  ? html `<button
@@ -154,7 +199,7 @@ export class LyraDateInput extends FormAssociated(LyraElement) {
154
199
  aria-label="Clear"
155
200
  @click=${() => this.clear()}
156
201
  >
157
- &times;
202
+ ${closeIcon()}
158
203
  </button>`
159
204
  : ''}
160
205
  <button
@@ -166,7 +211,7 @@ export class LyraDateInput extends FormAssociated(LyraElement) {
166
211
  ?disabled=${this.disabled || this.readonly}
167
212
  @click=${() => (this.open ? this.hide() : this.show())}
168
213
  >
169
- <span part="expand-icon" aria-hidden="true">📅</span>
214
+ <span part="expand-icon" aria-hidden="true">${calendarIcon()}</span>
170
215
  </button>
171
216
  </div>
172
217
  <div part="popup" role="dialog" aria-label="Choose date">
@@ -183,7 +228,12 @@ export class LyraDateInput extends FormAssociated(LyraElement) {
183
228
  @change=${this.onPickerChange}
184
229
  ></lyra-date-picker>
185
230
  </div>
186
- <div part="hint">${this.hint}<slot name="hint"></slot></div>
231
+ <div part="error" ?hidden=${!hasError}>
232
+ ${this.errorText}<slot name="error" @slotchange=${this.onErrorSlotChange}></slot>
233
+ </div>
234
+ <div part="hint" ?hidden=${!hasHint}>
235
+ ${this.hint}<slot name="hint" @slotchange=${this.onHintSlotChange}></slot>
236
+ </div>
187
237
  </div>
188
238
  `;
189
239
  }
@@ -212,6 +262,9 @@ __decorate([
212
262
  __decorate([
213
263
  property()
214
264
  ], LyraDateInput.prototype, "hint", void 0);
265
+ __decorate([
266
+ property({ attribute: 'error-text' })
267
+ ], LyraDateInput.prototype, "errorText", void 0);
215
268
  __decorate([
216
269
  property()
217
270
  ], LyraDateInput.prototype, "placeholder", void 0);
@@ -229,5 +282,14 @@ __decorate([
229
282
  ], LyraDateInput.prototype, "weekdayFormat", void 0);
230
283
  __decorate([
231
284
  state()
232
- ], LyraDateInput.prototype, "cleanupFn", void 0);
285
+ ], LyraDateInput.prototype, "touched", void 0);
286
+ __decorate([
287
+ state()
288
+ ], LyraDateInput.prototype, "hasHintSlot", void 0);
289
+ __decorate([
290
+ state()
291
+ ], LyraDateInput.prototype, "hasErrorSlot", void 0);
292
+ __decorate([
293
+ state()
294
+ ], LyraDateInput.prototype, "hasLabelSlot", void 0);
233
295
  defineElement('date-input', LyraDateInput);