@lmvz-ds/components 0.36.0 → 0.36.1

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 (139) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/cjs/aria.constants-DPK5mQEL.js +65 -0
  3. package/cjs/{stencil.util-TjS8_DoU.js → async.util-Dnl6c82X.js} +1 -13
  4. package/cjs/{directional-focus-controller-3uDoin45.js → directional-focus-controller-8Xd_7MHT.js} +1 -1
  5. package/cjs/{element-activation-controller-BWtUKzu2.js → element-activation-controller-DzTUocEO.js} +1 -1
  6. package/cjs/{element.util-CctmBvUY.js → element.util-BlhcwmfR.js} +2 -62
  7. package/cjs/lmvz-action-list.cjs.entry.js +3 -2
  8. package/cjs/lmvz-button-group.cjs.entry.js +2 -1
  9. package/cjs/lmvz-button_2.cjs.entry.js +3 -2
  10. package/cjs/lmvz-checkbox.cjs.entry.js +38 -4
  11. package/cjs/lmvz-components.cjs.js +1 -1
  12. package/cjs/lmvz-datepicker.cjs.entry.js +10 -3
  13. package/cjs/lmvz-header_2.cjs.entry.js +4 -3
  14. package/cjs/lmvz-input.cjs.entry.js +242 -0
  15. package/cjs/lmvz-menuitem.cjs.entry.js +3 -2
  16. package/cjs/lmvz-modal.cjs.entry.js +2 -1
  17. package/cjs/{lmvz-input_2.cjs.entry.js → lmvz-popover.cjs.entry.js} +24 -228
  18. package/cjs/lmvz-radio.cjs.entry.js +61 -3
  19. package/cjs/lmvz-select.cjs.entry.js +423 -16
  20. package/cjs/lmvz-snackbar.cjs.entry.js +1 -1
  21. package/cjs/lmvz-spinner.cjs.entry.js +1 -1
  22. package/cjs/lmvz-tab.cjs.entry.js +1 -1
  23. package/cjs/lmvz-tabs.cjs.entry.js +4 -3
  24. package/cjs/lmvz-toggle.cjs.entry.js +7 -3
  25. package/cjs/loader.cjs.js +1 -1
  26. package/cjs/stencil.util-CBrPLd-F.js +17 -0
  27. package/collection/components/lmvz-checkbox/lmvz-checkbox.css +4 -2
  28. package/collection/components/lmvz-checkbox/lmvz-checkbox.js +68 -8
  29. package/collection/components/lmvz-datepicker/lmvz-datepicker.css +0 -5
  30. package/collection/components/lmvz-datepicker/lmvz-datepicker.js +17 -3
  31. package/collection/components/lmvz-input/lmvz-input.css +1 -1
  32. package/collection/components/lmvz-input/lmvz-input.js +47 -12
  33. package/collection/components/lmvz-popover/lmvz-popover.css +1 -1
  34. package/collection/components/lmvz-popover/lmvz-popover.js +45 -5
  35. package/collection/components/lmvz-radio/lmvz-radio.js +77 -9
  36. package/collection/components/lmvz-select/lmvz-select.css +96 -34
  37. package/collection/components/lmvz-select/lmvz-select.js +412 -21
  38. package/collection/components/lmvz-snackbar/lmvz-snackbar.js +1 -1
  39. package/collection/components/lmvz-spinner/lmvz-spinner.js +1 -1
  40. package/collection/components/lmvz-tab/lmvz-tab.js +1 -1
  41. package/collection/components/lmvz-tabs/lmvz-tabs.js +1 -1
  42. package/collection/components/lmvz-toggle/lmvz-toggle.js +21 -1
  43. package/collection/integration/header-integration/header-integration.js +1 -1
  44. package/collection/styles/fragments/_focus-within.css +2 -2
  45. package/collection/utils/aria/listbox-controller.js +154 -0
  46. package/collection/utils/radio/radio-group-controller.js +17 -0
  47. package/components/lmvz-checkbox.js +1 -1
  48. package/components/lmvz-datepicker.js +1 -1
  49. package/components/lmvz-input.js +1 -1
  50. package/components/lmvz-popover.js +1 -1
  51. package/components/lmvz-radio.js +1 -1
  52. package/components/lmvz-select.js +1 -1
  53. package/components/lmvz-snackbar.js +1 -1
  54. package/components/lmvz-spinner.js +1 -1
  55. package/components/lmvz-tab.js +1 -1
  56. package/components/lmvz-tabs.js +1 -1
  57. package/components/lmvz-toggle.js +1 -1
  58. package/components/p-BApwCp0R.js +1 -0
  59. package/components/p-C5gRiLRR.js +1 -0
  60. package/esm/aria.constants-DIyiticz.js +62 -0
  61. package/esm/{stencil.util-C3QOYC8E.js → async.util-DZj20TV1.js} +1 -13
  62. package/esm/{directional-focus-controller-Bhu7RqYE.js → directional-focus-controller-QukTTWBE.js} +1 -1
  63. package/esm/{element-activation-controller-BuwyVq4A.js → element-activation-controller-dxJZKMag.js} +1 -1
  64. package/esm/{element.util-Bd9JyKJZ.js → element.util-3ydVsxUW.js} +2 -61
  65. package/esm/lmvz-action-list.entry.js +3 -2
  66. package/esm/lmvz-button-group.entry.js +2 -1
  67. package/esm/lmvz-button_2.entry.js +3 -2
  68. package/esm/lmvz-checkbox.entry.js +38 -4
  69. package/esm/lmvz-components.js +1 -1
  70. package/esm/lmvz-datepicker.entry.js +10 -3
  71. package/esm/lmvz-header_2.entry.js +4 -3
  72. package/esm/lmvz-input.entry.js +240 -0
  73. package/esm/lmvz-menuitem.entry.js +3 -2
  74. package/esm/lmvz-modal.entry.js +2 -1
  75. package/esm/{lmvz-input_2.entry.js → lmvz-popover.entry.js} +23 -226
  76. package/esm/lmvz-radio.entry.js +61 -3
  77. package/esm/lmvz-select.entry.js +423 -16
  78. package/esm/lmvz-snackbar.entry.js +1 -1
  79. package/esm/lmvz-spinner.entry.js +1 -1
  80. package/esm/lmvz-tab.entry.js +1 -1
  81. package/esm/lmvz-tabs.entry.js +4 -3
  82. package/esm/lmvz-toggle.entry.js +7 -3
  83. package/esm/loader.js +1 -1
  84. package/esm/stencil.util-D2-5rsN3.js +15 -0
  85. package/hydrate/index.js +625 -68
  86. package/hydrate/index.mjs +625 -68
  87. package/lmvz-components/lmvz-components.esm.js +1 -1
  88. package/lmvz-components/{p-544e3ccb.entry.js → p-0f65d001.entry.js} +1 -1
  89. package/lmvz-components/p-137b1fe1.entry.js +1 -0
  90. package/lmvz-components/p-26dd9684.entry.js +1 -0
  91. package/lmvz-components/p-36f44108.entry.js +1 -0
  92. package/lmvz-components/p-398b5cf4.entry.js +1 -0
  93. package/lmvz-components/p-3a396fc8.entry.js +1 -0
  94. package/lmvz-components/p-40716828.entry.js +1 -0
  95. package/lmvz-components/p-70f1e31a.entry.js +1 -0
  96. package/lmvz-components/p-71d06d1d.entry.js +1 -0
  97. package/lmvz-components/{p-d280d21a.entry.js → p-88661c50.entry.js} +1 -1
  98. package/lmvz-components/p-91eb71bb.entry.js +1 -0
  99. package/lmvz-components/p-BA3wizqg.js +1 -0
  100. package/lmvz-components/{p-GGkwaiT6.js → p-CHxiS3ja.js} +1 -1
  101. package/lmvz-components/{p-UhNgD4L3.js → p-CPSyM_e-.js} +1 -1
  102. package/lmvz-components/p-DIyiticz.js +1 -0
  103. package/lmvz-components/p-DZj20TV1.js +1 -0
  104. package/lmvz-components/p-a680a7b2.entry.js +1 -0
  105. package/lmvz-components/{p-d48203f9.entry.js → p-a6fe9b61.entry.js} +1 -1
  106. package/lmvz-components/{p-dbfbcb74.entry.js → p-a99acb02.entry.js} +1 -1
  107. package/lmvz-components/p-bh9cItsP.js +1 -0
  108. package/lmvz-components/p-f16a19fc.entry.js +1 -0
  109. package/lmvz-components/p-f3f0ed49.entry.js +1 -0
  110. package/lmvz-components/p-f516a949.entry.js +1 -0
  111. package/manifest.json +366 -30
  112. package/package.json +1 -1
  113. package/types/api/ds.types.d.ts +1 -0
  114. package/types/components/lmvz-checkbox/lmvz-checkbox.d.ts +9 -1
  115. package/types/components/lmvz-datepicker/lmvz-datepicker.d.ts +2 -0
  116. package/types/components/lmvz-input/lmvz-input.d.ts +3 -0
  117. package/types/components/lmvz-popover/lmvz-popover.d.ts +2 -0
  118. package/types/components/lmvz-radio/lmvz-radio.d.ts +12 -2
  119. package/types/components/lmvz-select/lmvz-select.d.ts +53 -4
  120. package/types/components/lmvz-toggle/lmvz-toggle.d.ts +1 -0
  121. package/types/components.d.ts +96 -12
  122. package/types/utils/aria/listbox-controller.d.ts +42 -0
  123. package/types/utils/radio/radio-group-controller.d.ts +3 -0
  124. package/components/p-CPuI0_r_.js +0 -1
  125. package/components/p-CWpl0WaZ.js +0 -1
  126. package/lmvz-components/p-63f6a596.entry.js +0 -1
  127. package/lmvz-components/p-8022c6dc.entry.js +0 -1
  128. package/lmvz-components/p-Bd9JyKJZ.js +0 -1
  129. package/lmvz-components/p-C3QOYC8E.js +0 -1
  130. package/lmvz-components/p-a611c110.entry.js +0 -1
  131. package/lmvz-components/p-b1bb4393.entry.js +0 -1
  132. package/lmvz-components/p-b50b1cb5.entry.js +0 -1
  133. package/lmvz-components/p-b932cf91.entry.js +0 -1
  134. package/lmvz-components/p-bff115d4.entry.js +0 -1
  135. package/lmvz-components/p-d9aede81.entry.js +0 -1
  136. package/lmvz-components/p-f0e28f3a.entry.js +0 -1
  137. package/lmvz-components/p-f853faae.entry.js +0 -1
  138. package/lmvz-components/p-fa2efc42.entry.js +0 -1
  139. package/lmvz-components/p-fe6f5bdc.entry.js +0 -1
@@ -22,7 +22,14 @@ export class LmvzRadio extends ReactiveControllerHost {
22
22
  name;
23
23
  disabled = false;
24
24
  required = false;
25
- error = false;
25
+ nativeError = false;
26
+ errorFromProp = false;
27
+ get error() {
28
+ return this.errorFromProp || this.nativeError;
29
+ }
30
+ set error(value) {
31
+ this.errorFromProp = value;
32
+ }
26
33
  helperText;
27
34
  form;
28
35
  autofocus = false;
@@ -30,6 +37,7 @@ export class LmvzRadio extends ReactiveControllerHost {
30
37
  _handlingFormReset = false;
31
38
  handleCheckedChange(newVal, oldVal) {
32
39
  this.internals.setFormValue?.(newVal ? this.value : null);
40
+ this.updateValidity();
33
41
  if (!newVal && oldVal && !this._handlingNativeChange && !this._handlingFormReset) {
34
42
  this.lmvzChange.emit(false);
35
43
  }
@@ -37,6 +45,9 @@ export class LmvzRadio extends ReactiveControllerHost {
37
45
  this.lmvzChange.emit(true);
38
46
  }
39
47
  }
48
+ handleRequiredChange() {
49
+ this.updateValidity();
50
+ }
40
51
  handleValueChange(newVal) {
41
52
  if (this.checked) {
42
53
  this.internals.setFormValue?.(newVal);
@@ -58,6 +69,9 @@ export class LmvzRadio extends ReactiveControllerHost {
58
69
  async reportValidity() {
59
70
  return this.internals?.reportValidity() ?? true;
60
71
  }
72
+ async getValidationMessage() {
73
+ return this.internals?.validationMessage ?? '';
74
+ }
61
75
  constructor() {
62
76
  super();
63
77
  this.addController(new AriaValidationController(this, {
@@ -69,8 +83,13 @@ export class LmvzRadio extends ReactiveControllerHost {
69
83
  componentWillLoad() {
70
84
  this.initialChecked = this.checked;
71
85
  this.internals.setFormValue?.(this.checked ? this.value : null);
86
+ this.updateValidity();
72
87
  super.componentWillLoad();
73
88
  }
89
+ componentDidLoad() {
90
+ this.updateValidity();
91
+ super.componentDidLoad();
92
+ }
74
93
  formAssociatedCallback() {
75
94
  this.internals.setFormValue?.(this.checked ? this.value : null);
76
95
  }
@@ -82,6 +101,8 @@ export class LmvzRadio extends ReactiveControllerHost {
82
101
  finally {
83
102
  this._handlingFormReset = false;
84
103
  }
104
+ this.updateValidity();
105
+ this.radioGroupController.refreshValidity();
85
106
  }
86
107
  formStateRestoreCallback(state) {
87
108
  if (typeof state !== 'string')
@@ -94,6 +115,20 @@ export class LmvzRadio extends ReactiveControllerHost {
94
115
  this._handlingFormReset = false;
95
116
  }
96
117
  }
118
+ isAnyInGroupChecked() {
119
+ return this.radioGroupController.isAnyChecked();
120
+ }
121
+ updateValidity() {
122
+ if (this.required && !this.isAnyInGroupChecked()) {
123
+ this.internals.setValidity?.({ valueMissing: true }, 'Please select an option.', this.nativeInput);
124
+ return;
125
+ }
126
+ this.internals.setValidity?.({});
127
+ this.nativeError = false;
128
+ }
129
+ handleInvalid() {
130
+ this.nativeError = true;
131
+ }
97
132
  handleChange = (event) => {
98
133
  this._handlingNativeChange = true;
99
134
  try {
@@ -136,8 +171,11 @@ export class LmvzRadio extends ReactiveControllerHost {
136
171
  return;
137
172
  this.radioGroupController.setFocused(target);
138
173
  }
174
+ retargetInnerClicks() {
175
+ this.nativeInput?.click();
176
+ }
139
177
  render() {
140
- return (h(Host, { key: 'cb2db48956ca781f226fbbddcf1d8ea819f9192a' }, h("input", { key: 'ec208dbaa714ae015415370b0d38ef916955b023', type: "radio", id: this.radioId, name: this.name ?? undefined, value: this.value, checked: this.checked, disabled: this.disabled, required: this.required, autoFocus: this.autofocus, "aria-invalid": this.error ? 'true' : undefined, "aria-describedby": this.helperId, ref: (el) => (this.nativeInput = el), onChange: this.handleChange }), h("span", { key: 'ef8221077d410d59248ce30678989f6bf7fbbebb', class: "circle", "aria-hidden": "true" }, this.checked && h("span", { key: '7faa45b58d39f05f64a7782329046fe07b0c2a98', class: "dot" })), h("span", { key: 'e508b4bf061ab0e038df7c169a80285bb7930dc3', class: "content" }, h("label", { key: '8117be47e7b0ae9cc17bab21bb7cc5d9300d2146', htmlFor: this.radioId }, this.label), h("span", { key: '44ab845ee18415b54fb5d23e533f85644f4e5e50', "aria-live": "polite", class: "helper-text", id: this.helperId }, this.helperText))));
178
+ return (h(Host, { key: 'f1fdc1167c4e59c2524e1e545c3382ca32853788' }, h("input", { key: 'c1b711444475ed401766cc5827eeaec0c46b62cf', type: "radio", id: this.radioId, value: this.value, checked: this.checked, disabled: this.disabled, autoFocus: this.autofocus, "aria-required": this.required ? 'true' : 'false', "aria-invalid": this.error ? 'true' : undefined, "aria-describedby": this.helperId, ref: (el) => (this.nativeInput = el), onChange: this.handleChange }), h("span", { key: 'b25e623269df05bdf8159e72da4df2e0bfb5d7bc', class: "circle", "aria-hidden": "true" }, this.checked && h("span", { key: '93d12890c7ba0abe203c63953ea1115aee37d7a0', class: "dot" })), h("span", { key: '44dcfdc4e7ee3820624b0d02a438139f45bb8733', class: "content" }, h("label", { key: 'd0547cd5e79642b298da155c9937715224b52cae', htmlFor: this.radioId }, this.label), h("span", { key: 'a9df5dd8a775a2f7422d5bac296869f88553e33f', onClick: this.retargetInnerClicks.bind(this), "aria-live": "polite", class: "helper-text", id: this.helperId }, this.helperText))));
141
179
  }
142
180
  static get is() { return "lmvz-radio"; }
143
181
  static get encapsulation() { return "scoped"; }
@@ -232,13 +270,13 @@ export class LmvzRadio extends ReactiveControllerHost {
232
270
  "docs": {
233
271
  "tags": [{
234
272
  "name": "remarks",
235
- "text": "A value of `\"\"` (empty string) is forwarded to the native `<input>`, but\n`RadioGroupController` treats it as unnamed \u2014 no group coordination occurs between\nradios with `name=\"\"`."
273
+ "text": "A value of `\"\"` (empty string) is treated as unnamed by `RadioGroupController` \u2014\nno group coordination occurs between radios with `name=\"\"`."
236
274
  }],
237
- "text": "Name attribute for form submission via ElementInternals.\n\nWhen multiple `lmvz-radio` elements share the same `name` and form scope,\nthe `name` is forwarded directly to the native `<input type=\"radio\">`,\nenabling browser-native mutual exclusion: selecting one radio automatically\ndeselects all others with the same name in that form. If a radio is outside\nany form, its scope defaults to the document level \u2014 same-name radios\noutside forms form a document-scoped group. Radios with the same `name` in\ndifferent forms are independent groups.\n\nThe `RadioGroupController` automatically manages roving tabindex (only the\nchecked radio has `tabindex=\"0\"`) and keyboard navigation (Arrow keys, Home, End)\nwithin the form-scoped group."
275
+ "text": "Name attribute for form submission via ElementInternals.\n\nThe `name` is NOT forwarded to the native `<input type=\"radio\">` \u2014 this component\nrenders in `scoped` (light DOM) mode, so a native `name` attribute would make the\ninner `<input>` an independent, genuinely form-associated element of the outer\n`<form>`, submitting a duplicate value alongside the host's own `ElementInternals`\nparticipation. Instead, `name` is used purely as a JS-level grouping key: the\n`RadioGroupController` tracks all `lmvz-radio` hosts sharing the same `name` and form\nscope in an internal registry and enforces mutual exclusion itself (via\n`RadioGroup.select()`, which sets `checked = false` on every other host in the group).\nIf a radio is outside any form, its scope defaults to the document level \u2014 same-name\nradios outside forms form a document-scoped group. Radios with the same `name` in\ndifferent forms are independent groups.\n\nThe `RadioGroupController` also manages roving tabindex (only the checked radio has\n`tabindex=\"0\"`) and keyboard navigation (Arrow keys, Home, End) within the group."
238
276
  },
239
277
  "getter": false,
240
278
  "setter": false,
241
- "reflect": false,
279
+ "reflect": true,
242
280
  "attribute": "name"
243
281
  },
244
282
  "disabled": {
@@ -304,11 +342,10 @@ export class LmvzRadio extends ReactiveControllerHost {
304
342
  }],
305
343
  "text": "Whether the radio is in an error state."
306
344
  },
307
- "getter": false,
308
- "setter": false,
345
+ "getter": true,
346
+ "setter": true,
309
347
  "reflect": true,
310
- "attribute": "error",
311
- "defaultValue": "false"
348
+ "attribute": "error"
312
349
  },
313
350
  "helperText": {
314
351
  "type": "string",
@@ -373,6 +410,11 @@ export class LmvzRadio extends ReactiveControllerHost {
373
410
  }
374
411
  };
375
412
  }
413
+ static get states() {
414
+ return {
415
+ "nativeError": {}
416
+ };
417
+ }
376
418
  static get events() {
377
419
  return [{
378
420
  "method": "lmvzChange",
@@ -458,6 +500,23 @@ export class LmvzRadio extends ReactiveControllerHost {
458
500
  "text": "Reports validation errors to the user.",
459
501
  "tags": []
460
502
  }
503
+ },
504
+ "getValidationMessage": {
505
+ "complexType": {
506
+ "signature": "() => Promise<string>",
507
+ "parameters": [],
508
+ "references": {
509
+ "Promise": {
510
+ "location": "global",
511
+ "id": "global::Promise"
512
+ }
513
+ },
514
+ "return": "Promise<string>"
515
+ },
516
+ "docs": {
517
+ "text": "Returns the current validation message, if any.",
518
+ "tags": []
519
+ }
461
520
  }
462
521
  };
463
522
  }
@@ -466,6 +525,9 @@ export class LmvzRadio extends ReactiveControllerHost {
466
525
  return [{
467
526
  "propName": "checked",
468
527
  "methodName": "handleCheckedChange"
528
+ }, {
529
+ "propName": "required",
530
+ "methodName": "handleRequiredChange"
469
531
  }, {
470
532
  "propName": "value",
471
533
  "methodName": "handleValueChange"
@@ -476,6 +538,12 @@ export class LmvzRadio extends ReactiveControllerHost {
476
538
  }
477
539
  static get listeners() {
478
540
  return [{
541
+ "name": "invalid",
542
+ "method": "handleInvalid",
543
+ "target": undefined,
544
+ "capture": false,
545
+ "passive": false
546
+ }, {
479
547
  "name": "keydown",
480
548
  "method": "handleKeydown",
481
549
  "target": undefined,
@@ -43,12 +43,9 @@
43
43
  /* Extra top space to accommodate the floating label */
44
44
 
45
45
  --lmvz-internal-select-floating-label-top-offset: 0.375rem;
46
-
47
- padding-top: var(--lmvz-internal-select-floating-label-top-offset);
48
-
49
46
  --lmvz-select-radius: var(--lmvz-component-input-radius-default, 999px);
50
47
  --lmvz-select-bg: var(--lmvz-semantic-color-surface-input-primary, #ffffff);
51
- --lmvz-select-border-color: var(--lmvz-semantic-color-border-default, #e0e0e0);
48
+ --lmvz-select-border-color: var(--lmvz-semantic-color-border-alternate, #c7c7c7);
52
49
  --lmvz-select-border-color-hover: var(--lmvz-semantic-color-border-hover, #a1a1a1);
53
50
  --lmvz-select-border-width: var(--lmvz-semantic-border-width-default, 1px);
54
51
  --lmvz-select-text-color: var(--lmvz-semantic-color-on-surface-input-primary, #000000);
@@ -63,6 +60,8 @@
63
60
  --lmvz-select-helper-color: var(--lmvz-semantic-color-on-surface-input-secondary, #545454);
64
61
  --lmvz-select-focus-color: var(--lmvz-semantic-color-status-on-active, #0e7ab4);
65
62
  --lmvz-select-error-color: var(--lmvz-semantic-color-status-on-danger, #a31419);
63
+
64
+ padding-top: var(--lmvz-internal-select-floating-label-top-offset);
66
65
  }
67
66
  .select-wrapper {
68
67
  position: relative;
@@ -101,19 +100,20 @@ label.floating-label {
101
100
  font-weight: 500;
102
101
  }
103
102
  label > span[aria-hidden='true'],
104
- div[aria-hidden='true'] > span:first-child > span[aria-hidden='true'] {
103
+ .select-trigger > span:first-child > span[aria-hidden='true'] {
105
104
  color: var(--lmvz-select-error-color);
106
105
  margin-left: 2px;
107
106
  display: inline-block;
108
107
  line-height: 0;
109
108
  vertical-align: middle;
110
109
  }
111
- /* Visual pill trigger */
112
- div[aria-hidden] {
110
+ /* Combobox trigger — the accessible, interactive control (role="combobox") */
111
+ .select-trigger {
113
112
  display: flex;
114
113
  align-items: center;
115
114
  gap: var(--lmvz-select-gap);
116
115
  height: 100%;
116
+ width: 100%;
117
117
  padding: 0 var(--lmvz-select-padding-x);
118
118
  border-radius: var(--lmvz-select-radius);
119
119
  background-color: var(--lmvz-select-bg);
@@ -124,26 +124,29 @@ div[aria-hidden] {
124
124
  z-index: 1;
125
125
  transition: border-color 0.15s ease;
126
126
  }
127
- :host(:not([disabled], [highlighted])) .select-wrapper:hover div[aria-hidden] {
127
+ :host(:not([disabled], [highlighted], [error])) .select-wrapper:hover .select-trigger {
128
128
  border-color: var(--lmvz-select-border-color-hover);
129
129
  }
130
- :host([highlighted]) div[aria-hidden] {
130
+ :host([error]) .select-trigger {
131
+ border-color: var(--lmvz-select-error-color);
132
+ }
133
+ :host([highlighted]) .select-trigger {
131
134
  background-color: var(--lmvz-semantic-color-status-active-alternate, #0f8acc);
132
135
  border-color: var(--lmvz-semantic-color-border-active, #0f8acc);
133
136
  color: var(--lmvz-semantic-color-status-on-active-alternate, #ffffff);
134
137
  }
135
- :host([highlighted]) div[aria-hidden] > span:first-child {
138
+ :host([highlighted]) .select-trigger > span:first-child {
136
139
  color: var(--lmvz-semantic-color-status-on-active-alternate, #ffffff);
137
140
  }
138
141
  :host([highlighted]) label.floating-label {
139
142
  background-color: var(--lmvz-semantic-color-status-active-alternate, #0f8acc);
140
143
  color: var(--lmvz-semantic-color-status-on-active-alternate, #ffffff);
141
144
  }
142
- .select-wrapper:focus-within div[aria-hidden] {
145
+ :host:has(:focus-visible) .select-trigger {
143
146
  outline: var(--lmvz-ds-outline, 1px solid #0e7ab4);
144
147
  outline-offset: var(--lmvz-ds-outline-offset, clamp(0.25rem, 0.19rem + 0.26vw, 0.5rem));
145
148
  }
146
- div[aria-hidden] > span:first-child {
149
+ .select-trigger > span:first-child {
147
150
  flex: 1 0 0;
148
151
  font: var(--lmvz-typography-body-md-strong, 500 clamp(0.88rem, 0.84rem + 0.13vw, 1rem) / 1.5
149
152
  Router);
@@ -151,36 +154,26 @@ div[aria-hidden] > span:first-child {
151
154
  overflow: hidden;
152
155
  text-overflow: ellipsis;
153
156
  white-space: nowrap;
157
+ text-align: left;
154
158
  }
155
159
  /* Active-selected text color: when a value is selected (not applicable in highlighted state) */
156
- :host(:not([highlighted])) .select-wrapper.has-value div[aria-hidden] > span:first-child {
160
+ :host(:not([highlighted])) .select-wrapper.has-value .select-trigger > span:first-child {
157
161
  color: var(--lmvz-semantic-color-int-on-secondary-active, #000000);
158
162
  }
159
- div[aria-hidden] > span:last-child {
163
+ .select-trigger > span:last-child {
160
164
  flex-shrink: 0;
161
165
  display: flex;
162
166
  align-items: center;
163
167
  justify-content: center;
164
168
  }
165
- div[aria-hidden] svg {
169
+ .select-trigger svg {
166
170
  width: var(--lmvz-component-icon-size-2xs, clamp(0.63rem, 0.59rem + 0.13vw, 0.75rem));
167
171
  height: var(--lmvz-component-icon-size-2xs, clamp(0.63rem, 0.59rem + 0.13vw, 0.75rem));
168
172
  flex-shrink: 0;
169
173
  }
170
- /* Native select absolutely covers the trigger, invisible, intercepts clicks */
171
- select {
172
- appearance: none;
173
- position: absolute;
174
- inset: 0;
175
- width: 100%;
176
- height: 100%;
177
- opacity: 0;
178
- cursor: pointer;
179
- z-index: 2;
180
- border: none;
181
- background: transparent;
182
- margin: 0;
183
- padding: 0;
174
+ /* Native <option>/<optgroup> markup is only a data source for the internal listbox; never shown. */
175
+ .lmvz-select-native-options {
176
+ display: none;
184
177
  }
185
178
  :host([disabled]) .select-wrapper {
186
179
  opacity: var(--lmvz-select-disabled-opacity);
@@ -188,22 +181,29 @@ select {
188
181
  cursor: not-allowed;
189
182
  }
190
183
  [role='status'] {
191
- margin-top: 4px;
184
+ padding-top: var(--lmvz-dimension-8-6, clamp(0.38rem, 0.34rem + 0.13vw, 0.5rem));
192
185
  padding-left: var(--lmvz-select-padding-x);
193
186
  font-size: var(--lmvz-select-label-font-size);
194
187
  color: var(--lmvz-select-helper-color);
195
188
  line-height: normal;
196
189
  }
190
+ [role='alert'] {
191
+ padding-top: var(--lmvz-dimension-8-6, clamp(0.38rem, 0.34rem + 0.13vw, 0.5rem));
192
+ padding-left: var(--lmvz-select-padding-x);
193
+ font-size: var(--lmvz-component-body-sm-font-size, clamp(0.75rem, 0.72rem + 0.13vw, 0.88rem));
194
+ color: var(--lmvz-select-error-color);
195
+ line-height: normal;
196
+ }
197
197
  :host([size='small']) {
198
198
  --lmvz-select-height: var(--lmvz-component-input-size-sm, clamp(2rem, 1.94rem + 0.26vw, 2.25rem));
199
199
  --lmvz-select-padding-x: var(--lmvz-component-input-padding-sm, clamp(0.75rem, 0.72rem + 0.13vw, 0.88rem));
200
200
  --lmvz-select-gap: var(--lmvz-component-input-gap-sm, clamp(0.38rem, 0.31rem + 0.26vw, 0.63rem));
201
201
  --lmvz-select-label-font-size: var(--lmvz-component-body-2xs-font-size, clamp(0.5rem, 0.47rem + 0.13vw, 0.63rem));
202
202
  }
203
- :host([size='small']) div[aria-hidden] > span:first-child {
203
+ :host([size='small']) .select-trigger > span:first-child {
204
204
  font-size: var(--lmvz-component-body-sm-font-size, clamp(0.75rem, 0.72rem + 0.13vw, 0.88rem));
205
205
  }
206
- :host([size='small']) div[aria-hidden] svg {
206
+ :host([size='small']) .select-trigger svg {
207
207
  width: var(--lmvz-component-icon-size-4xs, clamp(0.5rem, 0.47rem + 0.13vw, 0.63rem));
208
208
  height: var(--lmvz-component-icon-size-4xs, clamp(0.5rem, 0.47rem + 0.13vw, 0.63rem));
209
209
  }
@@ -213,10 +213,72 @@ select {
213
213
  --lmvz-select-gap: var(--lmvz-component-input-gap-lg, clamp(0.75rem, 0.69rem + 0.26vw, 1rem));
214
214
  --lmvz-select-label-font-size: var(--lmvz-component-body-sm-font-size, clamp(0.75rem, 0.72rem + 0.13vw, 0.88rem));
215
215
  }
216
- :host([size='large']) div[aria-hidden] > span:first-child {
216
+ :host([size='large']) .select-trigger > span:first-child {
217
217
  font-size: var(--lmvz-component-body-lg-font-size, clamp(1rem, 0.97rem + 0.13vw, 1.13rem));
218
218
  }
219
- :host([size='large']) div[aria-hidden] svg {
219
+ :host([size='large']) .select-trigger svg {
220
220
  width: var(--lmvz-component-icon-size-xs, clamp(0.75rem, 0.72rem + 0.13vw, 0.88rem));
221
221
  height: var(--lmvz-component-icon-size-xs, clamp(0.75rem, 0.72rem + 0.13vw, 0.88rem));
222
222
  }
223
+ /*
224
+ * Option-list item styling for the `role="option"` elements rendered inside the
225
+ * `lmvz-popover` (containerRole="listbox") popup. The token set mirrors the states
226
+ * `lmvz-action-list` applies to its `<lmvz-button>`/`<button>` items (tertiary variant),
227
+ * so the listbox looks visually identical to an action-list item:
228
+ * - default: --lmvz-semantic-color-int-tertiary / --lmvz-semantic-color-int-on-tertiary
229
+ * - hover: --lmvz-semantic-color-int-tertiary-hover / --lmvz-semantic-color-int-on-tertiary-hover
230
+ * - active: --lmvz-semantic-color-int-tertiary-active / --lmvz-semantic-color-int-on-tertiary-active
231
+ * - disabled: --lmvz-component-input-disabled-opacity (opacity, same token as lib-styles buttons fragment)
232
+ * - focus: --lmvz-ds-outline / --lmvz-ds-outline-offset (same as lib-styles buttons fragment focus-visible)
233
+ * "selected" has no action-list equivalent (action-list is a menu, not a listbox), so it
234
+ * reuses this component's own highlighted-state tokens for consistency with the trigger.
235
+ * "active" (`data-active="true"`) is this component's own addition: since the combobox uses
236
+ * the activedescendant model, the highlighted option never receives real DOM focus, so
237
+ * `:hover`/`:focus-visible` cannot indicate it to sighted keyboard users — `data-active` is
238
+ * set by the host from `ListboxController`'s active id and styled the same as hover/focus.
239
+ */
240
+ .lmvz-select-option {
241
+ --lmvz-select-option-background: var(--lmvz-semantic-color-int-tertiary, #ffffff);
242
+ --lmvz-select-option-color: var(--lmvz-semantic-color-int-on-tertiary, #000000);
243
+
244
+ display: flex;
245
+ align-items: center;
246
+ gap: var(--lmvz-select-gap);
247
+ padding: var(--lmvz-component-input-padding-sm, clamp(0.75rem, 0.72rem + 0.13vw, 0.88rem));
248
+ border-radius: var(--lmvz-semantic-border-radius-md, 6px);
249
+ background-color: var(--lmvz-select-option-background);
250
+ color: var(--lmvz-select-option-color);
251
+ cursor: pointer;
252
+ font: var(--lmvz-typography-body-md-strong, 500 clamp(0.88rem, 0.84rem + 0.13vw, 1rem) / 1.5
253
+ Router);
254
+ transition:
255
+ background-color 0.15s ease,
256
+ color 0.15s ease;
257
+ }
258
+ .lmvz-select-option:not([aria-disabled='true']):hover {
259
+ --lmvz-select-option-background: var(--lmvz-semantic-color-int-tertiary-hover, #f0f0f0);
260
+ --lmvz-select-option-color: var(--lmvz-semantic-color-int-on-tertiary-hover, #000000);
261
+ }
262
+ .lmvz-select-option:not([aria-disabled='true']):active {
263
+ --lmvz-select-option-background: var(--lmvz-semantic-color-int-tertiary-active, #e0e0e0);
264
+ --lmvz-select-option-color: var(--lmvz-semantic-color-int-on-tertiary-active, #000000);
265
+ }
266
+ .lmvz-select-option:focus-visible {
267
+ outline: var(--lmvz-ds-outline, 1px solid #0e7ab4);
268
+ outline-offset: var(--lmvz-ds-outline-offset, clamp(0.25rem, 0.19rem + 0.26vw, 0.5rem));
269
+ position: relative;
270
+ z-index: 1;
271
+ }
272
+ .lmvz-select-option[data-active='true']:not([aria-disabled='true']) {
273
+ --lmvz-select-option-background: var(--lmvz-semantic-color-int-tertiary-hover, #f0f0f0);
274
+ --lmvz-select-option-color: var(--lmvz-semantic-color-int-on-tertiary-hover, #000000);
275
+ }
276
+ .lmvz-select-option[aria-selected='true'] {
277
+ --lmvz-select-option-background: var(--lmvz-semantic-color-status-active-alternate, #0f8acc);
278
+ --lmvz-select-option-color: var(--lmvz-semantic-color-status-on-active-alternate, #ffffff);
279
+ }
280
+ .lmvz-select-option[aria-disabled='true'] {
281
+ cursor: not-allowed;
282
+ pointer-events: none;
283
+ opacity: var(--lmvz-component-input-disabled-opacity, 40%);
284
+ }