@expressivecss/expressive 0.3.0 → 0.4.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 (74) hide show
  1. package/README.md +1 -1
  2. package/dist/css/expressive.css +1011 -1339
  3. package/dist/css/expressive.css.map +1 -1
  4. package/dist/css/expressive.min.css +1 -1
  5. package/dist/css/expressive.min.css.map +1 -1
  6. package/dist/js/expressive.cjs +701 -853
  7. package/dist/js/expressive.cjs.map +4 -4
  8. package/dist/js/expressive.js +701 -853
  9. package/dist/js/expressive.js.map +4 -4
  10. package/dist/js/expressive.min.js +10 -20
  11. package/dist/js/expressive.min.js.map +4 -4
  12. package/dist/js/expressive.mjs +701 -854
  13. package/dist/js/expressive.mjs.map +4 -4
  14. package/dist/types/components/autocomplete.d.ts +2 -9
  15. package/dist/types/components/buttons.d.ts +2 -2
  16. package/dist/types/components/cards.d.ts +3 -6
  17. package/dist/types/components/carousel.d.ts +3 -3
  18. package/dist/types/components/characterCounter.d.ts +3 -5
  19. package/dist/types/components/chips.d.ts +2 -2
  20. package/dist/types/components/collapsible.d.ts +6 -23
  21. package/dist/types/components/datepicker.d.ts +2 -8
  22. package/dist/types/components/dropdown.d.ts +11 -2
  23. package/dist/types/components/index.d.ts +0 -1
  24. package/dist/types/components/lightbox.d.ts +2 -2
  25. package/dist/types/components/parallax.d.ts +2 -8
  26. package/dist/types/components/pushpin.d.ts +2 -2
  27. package/dist/types/components/range.d.ts +2 -2
  28. package/dist/types/components/registry.d.ts +0 -4
  29. package/dist/types/components/scrollspy.d.ts +26 -50
  30. package/dist/types/components/select.d.ts +20 -6
  31. package/dist/types/components/sidenav.d.ts +40 -55
  32. package/dist/types/components/slider.d.ts +2 -2
  33. package/dist/types/components/tabs.d.ts +2 -2
  34. package/dist/types/components/tapTarget.d.ts +2 -2
  35. package/dist/types/components/timepicker.d.ts +2 -4
  36. package/dist/types/components/tooltip.d.ts +2 -2
  37. package/dist/types/core/component.d.ts +4 -4
  38. package/dist/types/index.d.ts +1 -1
  39. package/package.json +2 -2
  40. package/src/sass/README.md +2 -6
  41. package/src/sass/abstracts/_breakpoints.scss +0 -16
  42. package/src/sass/abstracts/_variables.scss +0 -21
  43. package/src/sass/base/_grid.scss +0 -19
  44. package/src/sass/components/_badges.scss +1 -1
  45. package/src/sass/components/_buttons.scss +22 -54
  46. package/src/sass/components/_cards.scss +50 -123
  47. package/src/sass/components/_carousel.scss +4 -6
  48. package/src/sass/components/_collapsible.scss +13 -15
  49. package/src/sass/components/_collection.scss +9 -27
  50. package/src/sass/components/_datepicker.scss +1 -15
  51. package/src/sass/components/_dropdown.scss +110 -14
  52. package/src/sass/components/_index.scss +1 -0
  53. package/src/sass/components/_modal.scss +13 -31
  54. package/src/sass/components/_navbar.scss +9 -6
  55. package/src/sass/components/_page-footer.scss +134 -15
  56. package/src/sass/components/_parallax.scss +7 -28
  57. package/src/sass/components/_preloader.scss +145 -300
  58. package/src/sass/components/_scrollspy.scss +18 -0
  59. package/src/sass/components/_sidenav.scss +226 -120
  60. package/src/sass/components/_table_of_contents.scss +30 -14
  61. package/src/sass/components/_tabs.scss +54 -35
  62. package/src/sass/components/_tapTarget.scss +2 -6
  63. package/src/sass/components/_toast.scss +10 -11
  64. package/src/sass/components/_toolbar.scss +2 -2
  65. package/src/sass/components/_tooltip.scss +4 -9
  66. package/src/sass/components/_transitions.scss +0 -5
  67. package/src/sass/components/forms/_index.scss +0 -5
  68. package/src/sass/components/forms/_input-fields.scss +49 -52
  69. package/src/sass/components/forms/_select.scss +175 -60
  70. package/src/sass/components/forms/_switches.scss +21 -8
  71. package/src/sass/utilities/_helpers.scss +1 -3
  72. package/src/sass/utilities/_typescale.scss +0 -5
  73. package/src/sass/utilities/_visibility.scss +2 -2
  74. package/dist/types/components/modal.d.ts +0 -45
@@ -11,8 +11,6 @@
11
11
  // --md-comp-tap-target-supporting-color
12
12
  // --md-comp-tap-target-duration 300ms
13
13
  // --md-comp-tap-target-easing soft ease-in-out
14
- //
15
- // .feature-discovery is an alias of .tap-target.
16
14
 
17
15
  .tap-target-wrapper {
18
16
  --md-comp-tap-target-size: 480px;
@@ -53,8 +51,7 @@
53
51
  pointer-events: none;
54
52
  transition-delay: 0s;
55
53
 
56
- .tap-target,
57
- .feature-discovery {
54
+ .tap-target {
58
55
  transform: scale(1);
59
56
  opacity: 1;
60
57
  }
@@ -69,8 +66,7 @@
69
66
  }
70
67
  }
71
68
 
72
- .tap-target,
73
- .feature-discovery {
69
+ .tap-target {
74
70
  position: absolute;
75
71
  inset: 0;
76
72
  display: grid;
@@ -2,10 +2,10 @@
2
2
 
3
3
  // Snackbars (toasts).
4
4
  //
5
- // Anatomy is the HTML. A .snackbar / .toast is the container. A <p> (or
6
- // <span>, or loose text) is the supporting text. A trailing <button> is
7
- // the action; a .circle button is the optional close. An optional leading
8
- // <i> is an icon. .toast-action stays as an alias on the action control.
5
+ // Anatomy is the HTML. A .snackbar is the container. A <p> (or <span>,
6
+ // or loose text) is the supporting text. A trailing <button> is the
7
+ // action; a .circle button is the optional close. An optional leading
8
+ // <i> is an icon.
9
9
  //
10
10
  // Tokens follow https://m3.material.io/components/snackbar/specs
11
11
  // container inverse-surface, 4dp corners, elevation 3, min 48dp
@@ -15,11 +15,10 @@
15
15
  // close 24dp icon, inverse-on-surface
16
16
  // inset 8dp compact, 24dp from start+bottom on larger screens
17
17
  //
18
- // The constructor always adds both .toast and .snackbar. A static
19
- // .snackbar is in-flow (docs previews). .active pins it to the viewport.
20
- // There is no AutoInit and no toast() helper.
18
+ // The constructor adds .snackbar. A static .snackbar is in-flow (docs
19
+ // previews). .active pins it to the viewport. There is no AutoInit.
21
20
 
22
- $_bar: ":is(.toast, .snackbar)";
21
+ $_bar: ".snackbar";
23
22
 
24
23
  #toast-container {
25
24
  --md-comp-snackbar-container-margin-leading: 8px;
@@ -128,7 +127,7 @@ $_bar: ":is(.toast, .snackbar)";
128
127
  }
129
128
 
130
129
  // Inline links in the supporting text use the action color.
131
- a:not(.button, .toast-action) {
130
+ a:not(.button) {
132
131
  color: var(--md-comp-snackbar-action-label-text-color);
133
132
  }
134
133
 
@@ -147,8 +146,8 @@ $_bar: ":is(.toast, .snackbar)";
147
146
  }
148
147
 
149
148
  // Text action — inverse-primary, not the filled common-button default.
150
- > :is(button, a.button, a.toast-action, .toast-action):not(.circle),
151
- > nav > :is(button, a.button, a.toast-action, .toast-action):not(.circle) {
149
+ > :is(button, a.button):not(.circle),
150
+ > nav > :is(button, a.button):not(.circle) {
152
151
  flex: 0 0 auto;
153
152
  height: 40px;
154
153
  min-width: 48px;
@@ -49,7 +49,7 @@ nav.toolbar {
49
49
 
50
50
  // Actions — transparent at rest so they sit on the container, not filled.
51
51
  // .extra is the 56dp FAB; leave it alone so a docked bar can embed one.
52
- > :is(a, button):not(.btn-floating, .extra) {
52
+ > :is(a, button):not(.extra) {
53
53
  box-sizing: border-box;
54
54
  display: inline-flex;
55
55
  flex: 0 0 auto;
@@ -103,7 +103,7 @@ nav.toolbar {
103
103
  }
104
104
 
105
105
  // Icon-only — .circle, or an <i> with no <span> sibling.
106
- > :is(a, button):not(.btn-floating, .extra):is(.circle, :has(> i):not(:has(> span))) {
106
+ > :is(a, button):not(.extra):is(.circle, :has(> i):not(:has(> span))) {
107
107
  width: var(--md-comp-toolbar-action-size);
108
108
  padding: 0;
109
109
  border-radius: 50%;
@@ -14,11 +14,9 @@
14
14
  // gap 4dp from the activator
15
15
  //
16
16
  // The JS plugin still exists: .tooltipped + data-tooltip, AutoInit,
17
- // class .tooltip.material-tooltip appended to <body>. .material-tooltip
18
- // is an alias. There is no data-html / unsafeHTML.
17
+ // class .tooltip appended to <body>. There is no data-html / unsafeHTML.
19
18
 
20
- .tooltip,
21
- .material-tooltip {
19
+ .tooltip {
22
20
  --md-comp-plain-tooltip-container-color: var(--md-sys-color-inverse-surface);
23
21
  --md-comp-plain-tooltip-supporting-text-color: var(--md-sys-color-inverse-on-surface);
24
22
  --md-comp-plain-tooltip-container-shape: 4px;
@@ -109,8 +107,7 @@
109
107
 
110
108
  // JS plugin: appended to <body>, top/left set in script. Do not use the
111
109
  // child-positioning rules; they would fight the inline transform.
112
- body > .tooltip,
113
- .material-tooltip {
110
+ body > .tooltip {
114
111
  position: absolute;
115
112
  left: 0;
116
113
  top: 0;
@@ -121,9 +118,7 @@ body > .tooltip,
121
118
 
122
119
  // Rich tooltip — title, supporting text, optional action.
123
120
  .tooltip.rich,
124
- .tooltip.max,
125
- .material-tooltip.rich,
126
- .material-tooltip.max {
121
+ .tooltip.max {
127
122
  --md-comp-rich-tooltip-container-color: var(--md-sys-color-surface);
128
123
  --md-comp-rich-tooltip-container-shape: 12px;
129
124
 
@@ -1,10 +1,5 @@
1
1
  @use "../abstracts" as *;
2
2
 
3
- // Starting states for JS-driven animations: Sidenav staggers its list items in,
4
- // and .fade-in is applied before an element is animated into view.
5
- ul.staggered-list li { opacity: 0; }
6
- .fade-in { opacity: 0; transform-origin: 0 50%; }
7
-
8
3
  .scale-transition {
9
4
  transition: transform .3s cubic-bezier(0.53, 0.01, 0.36, 1.63) !important;
10
5
 
@@ -7,11 +7,6 @@
7
7
  @forward 'range';
8
8
  @forward 'fieldset';
9
9
 
10
- // Remove Focus Boxes
11
- select:focus {
12
- outline: 1px solid var(--md-ref-palette-primary80);
13
- }
14
-
15
10
  label {
16
11
  font-size: .8rem;
17
12
  color: var(--md-sys-color-on-surface-variant);
@@ -2,14 +2,17 @@
2
2
 
3
3
  // Text fields.
4
4
  //
5
- // Anatomy is the HTML. .field (or .input-field) is the container. The
6
- // <label> after the control is the floating label. A leading <i> is the
7
- // leading icon. <small> is supporting text. .outlined / .border is the
8
- // outlined variant; the default is filled.
5
+ // Anatomy is the HTML. .field is the container. The <label> after the
6
+ // control is the floating label. A leading <i> (or .prefix) is the
7
+ // leading icon; .suffix (or a second / trailing <i>) is the trailing
8
+ // icon. Icons come before the control in the documented markup, so
9
+ // trailing cannot be selected with :last-child — that left .suffix
10
+ // in flow above the well. <small> is supporting text. .outlined is
11
+ // the outlined variant; the default is filled.
9
12
  //
10
13
  // Tokens follow https://m3.material.io/components/text-fields/specs
11
- // filled surface-variant well, 4dp top corners, 1dp/2dp bottom indicator
12
- // outlined 1dp/2dp outline, 4dp corners
14
+ // filled surface-variant well, 1rem corners, 1dp/2dp bottom indicator
15
+ // outlined 1dp/2dp outline, 1rem corners
13
16
  // height 56dp
14
17
  // label body-large rest, body-small floated
15
18
  // input body-large / on-surface
@@ -34,21 +37,23 @@ $_text-inputs: "input:not([type]):not(.browser-default), input[type=text]:not(.b
34
37
  font-family: $font-stack;
35
38
  }
36
39
 
37
- .field,
38
- .input-field {
40
+ .field {
39
41
  --md-comp-filled-text-field-container-color: var(--md-sys-color-surface-variant);
42
+ --md-comp-filled-text-field-container-shape: 1rem;
40
43
  --md-comp-filled-text-field-caret-color: var(--md-sys-color-primary);
41
44
  --input-color: var(--md-comp-filled-text-field-caret-color);
42
45
 
43
46
  position: relative;
44
47
  clear: both;
45
48
 
46
- input:not([type="range"]),
49
+ // Checkboxes / radios inside a field (multi-select menu) are not text
50
+ // fields — do not give them the 56dp well.
51
+ input:not([type="range"], [type="checkbox"], [type="radio"], [type="file"]),
47
52
  textarea {
48
53
  box-sizing: border-box;
49
54
  padding: 20px 16px 8px;
50
55
  border: none;
51
- border-radius: 4px 4px 0 0;
56
+ border-radius: var(--md-comp-filled-text-field-container-shape);
52
57
  border-bottom: 1px solid var(--md-sys-color-on-surface-variant);
53
58
  background-color: var(--md-comp-filled-text-field-container-color);
54
59
  caret-color: var(--input-color);
@@ -67,16 +72,16 @@ $_text-inputs: "input:not([type]):not(.browser-default), input[type=text]:not(.b
67
72
  }
68
73
 
69
74
  &:focus:not([readonly]) + label,
70
- &:focus:not([readonly]) + span:not(.supporting-text, .character-counter) {
75
+ &:focus:not([readonly]) + span:not(.supporting-text, .character-counter, .caret) {
71
76
  color: var(--input-color);
72
77
  }
73
78
 
74
79
  &:focus:not([readonly]) + label,
75
80
  &:not([placeholder=" "]) + label,
76
81
  &:not(:placeholder-shown) + label,
77
- &:focus:not([readonly]) + span:not(.supporting-text, .character-counter),
78
- &:not([placeholder=" "]) + span:not(.supporting-text, .character-counter),
79
- &:not(:placeholder-shown) + span:not(.supporting-text, .character-counter) {
82
+ &:focus:not([readonly]) + span:not(.supporting-text, .character-counter, .caret),
83
+ &:not([placeholder=" "]) + span:not(.supporting-text, .character-counter, .caret),
84
+ &:not(:placeholder-shown) + span:not(.supporting-text, .character-counter, .caret) {
80
85
  transform: scale(calc(12 / 16));
81
86
  top: 8px;
82
87
  }
@@ -119,7 +124,7 @@ $_text-inputs: "input:not([type]):not(.browser-default), input[type=text]:not(.b
119
124
  }
120
125
 
121
126
  > label,
122
- > span:not(.supporting-text, .character-counter, .file-path) {
127
+ > span:not(.supporting-text, .character-counter, .file-path, .caret) {
123
128
  color: var(--md-sys-color-on-surface-variant);
124
129
  user-select: none;
125
130
  position: absolute;
@@ -152,10 +157,11 @@ $_text-inputs: "input:not([type]):not(.browser-default), input[type=text]:not(.b
152
157
  font-family: $font-stack;
153
158
  }
154
159
 
155
- > i:first-child:not(.suffix),
156
- > .prefix {
160
+ // Shared icon box. Position is set on the leading / trailing rules;
161
+ // a lone <i> matches several of the :is() items and must not pick
162
+ // a side here.
163
+ > i:is(.prefix, .suffix, :first-of-type, :last-of-type, :last-child) {
157
164
  position: absolute;
158
- left: 12px;
159
165
  top: 16px;
160
166
  display: flex;
161
167
  align-items: center;
@@ -165,53 +171,44 @@ $_text-inputs: "input:not([type]):not(.browser-default), input[type=text]:not(.b
165
171
  line-height: 24px;
166
172
  color: var(--md-sys-color-on-surface-variant);
167
173
  user-select: none;
174
+ }
175
+
176
+ > i:first-of-type:not(.suffix):not(:last-child),
177
+ > i.prefix {
178
+ left: 12px;
168
179
  pointer-events: none;
169
180
  }
170
181
 
171
- > .suffix,
172
- > i.suffix {
173
- position: absolute;
182
+ > i.suffix,
183
+ > i:last-of-type:not(:first-of-type),
184
+ > i:last-child:not(:first-child):not(.prefix) {
174
185
  right: 12px;
175
- top: 16px;
176
- display: flex;
177
- align-items: center;
178
- font-size: 24px;
179
- width: 24px;
180
- height: 24px;
181
- line-height: 24px;
182
- color: var(--md-sys-color-on-surface-variant);
183
- user-select: none;
184
186
  }
185
187
 
186
- > i:first-child:not(.suffix) ~ input,
187
- > i:first-child:not(.suffix) ~ textarea,
188
- > .prefix ~ input,
189
- > .prefix ~ textarea {
190
- padding-left: 52px;
191
- }
188
+ &:has(> i:first-of-type:not(.suffix):not(:last-child), > i.prefix) {
189
+ > :is(input, textarea) {
190
+ padding-left: 52px;
191
+ }
192
192
 
193
- > .suffix ~ input,
194
- > .suffix ~ textarea,
195
- > i.suffix ~ input,
196
- > i.suffix ~ textarea {
197
- padding-right: 52px;
193
+ > :is(label, span:not(.supporting-text, .character-counter, .file-path, .caret)) {
194
+ left: 52px;
195
+ }
198
196
  }
199
197
 
200
- > i:first-child:not(.suffix) ~ label,
201
- > i:first-child:not(.suffix) ~ span:not(.supporting-text, .character-counter),
202
- > .prefix ~ label {
203
- left: 52px;
198
+ &:has(> i.suffix, > i:last-of-type:not(:first-of-type), > i:last-child:not(:first-child):not(.prefix)) {
199
+ > :is(input, textarea) {
200
+ padding-right: 52px;
201
+ }
204
202
  }
205
203
 
206
- &.outlined,
207
- &.border {
204
+ &.outlined {
208
205
  > input,
209
206
  > textarea {
210
207
  padding-top: 8px;
211
208
  padding-bottom: 8px;
212
209
  background-color: transparent;
213
210
  border: 1px solid var(--md-sys-color-outline);
214
- border-radius: 4px;
211
+ border-radius: var(--md-comp-filled-text-field-container-shape);
215
212
 
216
213
  &:focus:not([readonly]) {
217
214
  border-width: 2px;
@@ -223,9 +220,9 @@ $_text-inputs: "input:not([type]):not(.browser-default), input[type=text]:not(.b
223
220
  &:focus:not([readonly]) + label,
224
221
  &:not([placeholder=" "]) + label,
225
222
  &:not(:placeholder-shown) + label,
226
- &:focus:not([readonly]) + span:not(.supporting-text, .character-counter),
227
- &:not([placeholder=" "]) + span:not(.supporting-text, .character-counter),
228
- &:not(:placeholder-shown) + span:not(.supporting-text, .character-counter) {
223
+ &:focus:not([readonly]) + span:not(.supporting-text, .character-counter, .caret),
224
+ &:not([placeholder=" "]) + span:not(.supporting-text, .character-counter, .caret),
225
+ &:not(:placeholder-shown) + span:not(.supporting-text, .character-counter, .caret) {
229
226
  top: -8px;
230
227
  left: 16px;
231
228
  margin-left: -4px;
@@ -253,7 +250,7 @@ $_text-inputs: "input:not([type]):not(.browser-default), input[type=text]:not(.b
253
250
  }
254
251
 
255
252
  .searchbar {
256
- .prefix {
253
+ > i:first-child {
257
254
  position: absolute;
258
255
  padding-left: 1rem;
259
256
  top: 0;
@@ -1,103 +1,218 @@
1
1
  @use "../../abstracts" as *;
2
2
 
3
+ // Select (exposed dropdown).
4
+ //
5
+ // Anatomy is the HTML. A <select> is the control. FormSelect wraps it
6
+ // in .field (or reuses an existing one) and paints a text field + menu.
7
+ // .browser-default keeps the native control. .caret is the trailing
8
+ // expand icon (CSS, not an SVG node).
9
+ //
10
+ // Tokens follow https://m3.material.io/components/menus/overview
11
+ // (exposed dropdown = text field + menu) and the filled/outlined field
12
+ // tokens in _input-fields.scss.
13
+ // field 56dp, same chrome as .field
14
+ // trailing icon 24dp, 12dp from the end, rotates when open
15
+ // selected item 10% on-surface + trailing check
16
+ // optgroup on-surface-variant, outline-variant rule
17
+ //
18
+ $_select-chevron: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath fill='black' d='M7 10l5 5 5-5z'/%3E%3C/svg%3E");
19
+ $_select-check: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath fill='black' d='M9 16.17 4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z'/%3E%3C/svg%3E");
20
+
21
+ // Native / no-js / .browser-default: a 56dp field, not a 3rem box.
22
+ select {
23
+ --md-comp-filled-select-container-color: var(--md-sys-color-surface-variant);
24
+ --md-comp-filled-select-container-shape: 1rem;
25
+ --md-comp-filled-select-input-text-color: var(--md-sys-color-on-surface);
26
+
27
+ box-sizing: border-box;
28
+ width: 100%;
29
+ height: 56px;
30
+ margin: 0;
31
+ padding: 16px 12px;
32
+ border: 1px solid var(--md-sys-color-outline);
33
+ border-radius: var(--md-comp-filled-select-container-shape);
34
+ background-color: var(--md-comp-filled-select-container-color);
35
+ color: var(--md-comp-filled-select-input-text-color);
36
+ @include typescale("body-large");
37
+ font-family: $font-stack;
38
+
39
+ &:focus {
40
+ outline: 2px solid var(--md-sys-color-primary);
41
+ outline-offset: 2px;
42
+ border-color: var(--md-sys-color-primary);
43
+ }
44
+
45
+ &:disabled {
46
+ color: color-mix(in oklab, var(--md-sys-color-on-surface) 38%, transparent);
47
+ border-color: color-mix(in oklab, var(--md-sys-color-on-surface) 12%, transparent);
48
+ background-color: color-mix(in oklab, var(--md-sys-color-on-surface) 4%, transparent);
49
+ }
50
+ }
51
+
3
52
  select.browser-default {
4
53
  opacity: 1;
5
- color: var(--md-sys-color-on-background);
6
54
  }
7
55
 
8
- // TODO: Change to ".select" and not use tag name directly
9
- // Keep selects useable without javascript
56
+ @supports (appearance: base-select) {
57
+ select.browser-default,
58
+ select.browser-default::picker(select) {
59
+ appearance: base-select;
60
+ }
10
61
 
11
- select {
12
- background-color: var(--md-sys-color-surface);
13
- width: 100%;
14
- padding: 5px;
15
- border: 1px solid var(--md-sys-color-outline-variant);
16
- border-radius: 2px;
17
- height: 3rem;
62
+ select.browser-default::picker(select) {
63
+ border: none;
64
+ border-radius: var(--md-comp-menu-container-shape, 1rem);
65
+ background-color: var(--md-sys-color-surface);
66
+ color: var(--md-sys-color-on-surface);
67
+ @include z-depth("2");
68
+ }
18
69
  }
19
70
 
20
- .select-wrapper:focus-within {
21
- outline: 2px solid var(--md-sys-color-primary);
22
- border-radius: 4px;
23
- }
71
+ .field:has(> .hide-select) {
72
+ // BeerCSS menu: 1rem surface, stadium items. Same radius on the
73
+ // field so the closed control and the open list read as one shape.
74
+ --md-comp-filled-select-container-shape: 1rem;
75
+ --md-comp-menu-container-shape: 1rem;
76
+ --md-comp-filled-select-trailing-icon-size: 24px;
77
+ --md-comp-filled-select-trailing-icon-color: var(--md-sys-color-on-surface-variant);
78
+ --md-comp-menu-item-leading-image-size: 40px;
24
79
 
25
- .select-wrapper {
26
80
  position: relative;
81
+ border-radius: var(--md-comp-filled-select-container-shape);
27
82
 
83
+ > input.dropdown-trigger {
84
+ border-radius: inherit;
85
+ }
86
+
87
+ > input.dropdown-trigger {
88
+ padding-inline-end: 52px;
89
+ }
90
+
91
+ // Trailing expand icon. Empty .caret is the hook; the glyph is a mask.
92
+ // `left: auto` beats `.field > span { left: 16px }` if that rule ever
93
+ // matches again — left + width would otherwise ignore inset-inline-end
94
+ // and pin the chevron over the value.
28
95
  .caret {
29
96
  position: absolute;
30
- right: 0;
31
- top: 0;
32
- bottom: 0;
33
- margin: auto 0;
34
- z-index: 0;
35
- fill: var(--md-sys-color-on-background);
97
+ inset-block-start: 16px;
98
+ inset-inline-start: auto;
99
+ left: auto;
100
+ inset-inline-end: 12px;
101
+ z-index: 1;
102
+ width: var(--md-comp-filled-select-trailing-icon-size);
103
+ height: var(--md-comp-filled-select-trailing-icon-size);
104
+ margin: 0;
36
105
  pointer-events: none;
106
+ background-color: var(--md-comp-filled-select-trailing-icon-color);
107
+ mask: $_select-chevron center / 24px no-repeat;
108
+ transition: transform 0.2s ease;
109
+ }
110
+
111
+ &:has(> input[aria-expanded="true"]) .caret,
112
+ &:has(> menu:popover-open) .caret {
113
+ transform: rotate(180deg);
37
114
  }
38
115
 
39
- // Hide select with overflow hidden instead of using display none
40
- // (this prevents form validation errors with hidden form elements)
116
+ // Keep the native select in the field's box so constraint-validation
117
+ // bubbles land on the control, not at (0, 0).
41
118
  .hide-select {
42
- width: 0;
43
- height: 0;
44
- overflow: hidden;
45
119
  position: absolute;
46
- top: 0;
120
+ inset: 0;
121
+ overflow: hidden;
122
+ opacity: 0;
123
+ pointer-events: none;
47
124
  z-index: -1;
48
- }
49
- }
50
125
 
51
- select:disabled {
52
- color: var(--md-sys-color-on-surface);
126
+ select {
127
+ width: 100%;
128
+ height: 100%;
129
+ margin: 0;
130
+ padding: 0;
131
+ border: 0;
132
+ }
133
+ }
53
134
  }
54
135
 
55
- .select-wrapper.disabled {
56
- + label {
57
- color: var(--md-sys-color-on-surface);
58
- }
136
+ .field:has(> .hide-select).disabled {
59
137
  .caret {
60
- fill: var(--md-sys-color-on-surface);
138
+ background-color: color-mix(in oklab, var(--md-sys-color-on-surface) 38%, transparent);
139
+ }
140
+
141
+ + label,
142
+ > label {
143
+ color: color-mix(in oklab, var(--md-sys-color-on-surface) 38%, transparent);
61
144
  }
62
145
  }
63
146
 
64
- .select-wrapper input.select-dropdown:disabled {
65
- color: var(--md-sys-color-on-surface);
147
+ .field:has(> .hide-select) > input.dropdown-trigger:disabled {
66
148
  cursor: default;
67
149
  user-select: none;
68
150
  }
69
151
 
70
- .select-wrapper i {
71
- color: var(--md-sys-color-on-surface);
152
+ menu[role="listbox"] > li.disabled,
153
+ menu[role="listbox"] > li.disabled > span,
154
+ menu[role="listbox"] > li.optgroup {
155
+ color: color-mix(in oklab, var(--md-sys-color-on-surface) 38%, transparent);
156
+ }
157
+
158
+ menu[role="listbox"] > li:not(.optgroup, .divider) {
159
+ display: flex;
160
+ align-items: center;
72
161
  }
73
162
 
74
- .select-dropdown li.disabled,
75
- .select-dropdown li.disabled > span,
76
- .select-dropdown li.optgroup {
77
- color: var(--md-sys-color-on-surface);
78
- //background-color: transparent;
163
+ menu[role="listbox"] > li.selected:not(.disabled, .optgroup) {
164
+ background-color: color-mix(in oklab, var(--md-sys-color-on-surface) 10%, transparent);
79
165
  }
80
166
 
81
- // Icons
82
- .select-dropdown li {
83
- img {
84
- height: $dropdown-item-height - 10;
85
- width: $dropdown-item-height - 10;
86
- margin: 5px 15px;
87
- float: right;
167
+ // Single-select: trailing check. Multi-select already has a leading checkbox.
168
+ menu[role="listbox"]:not(.multiple-select-dropdown) > li.selected:not(.disabled, .optgroup)::after {
169
+ content: "";
170
+ flex: 0 0 24px;
171
+ width: 24px;
172
+ height: 24px;
173
+ margin-inline-start: auto;
174
+ margin-inline-end: 12px;
175
+ background-color: var(--md-sys-color-on-surface);
176
+ mask: $_select-check center / 24px no-repeat;
177
+ }
178
+
179
+ menu[role="listbox"] li img {
180
+ box-sizing: border-box;
181
+ flex: 0 0 var(--md-comp-menu-item-leading-image-size);
182
+ width: var(--md-comp-menu-item-leading-image-size);
183
+ height: var(--md-comp-menu-item-leading-image-size);
184
+ margin: 4px 12px;
185
+ object-fit: cover;
186
+ border-radius: 4px;
187
+ order: 1;
188
+ margin-inline-start: auto;
189
+
190
+ &.left {
191
+ order: 0;
192
+ margin-inline-start: 12px;
193
+ margin-inline-end: 0;
88
194
  }
89
195
  }
90
196
 
91
- // Optgroup styles
92
- .select-dropdown li.optgroup {
93
- border-top: 1px solid rgba(0, 0, 0, 0.04);
94
- &.selected > span {
95
- color: var(--md-sys-color-on-background);
197
+ menu[role="listbox"] li.optgroup {
198
+ cursor: default;
199
+ pointer-events: none;
200
+
201
+ &:not(:first-child) {
202
+ border-top: 1px solid var(--md-sys-color-outline-variant);
96
203
  }
97
- & > span {
204
+
205
+ > span {
98
206
  color: var(--md-sys-color-on-surface-variant);
99
207
  }
100
- & ~ li.optgroup-option {
101
- padding-left: 1rem;
208
+ }
209
+
210
+ menu[role="listbox"] li.optgroup-option {
211
+ padding-inline-start: 1rem;
212
+ }
213
+
214
+ @media (prefers-reduced-motion: reduce) {
215
+ .field:has(> .hide-select) .caret {
216
+ transition: none;
102
217
  }
103
218
  }
@@ -68,13 +68,21 @@
68
68
  border: 2px solid var(--md-sys-color-outline);
69
69
  border-radius: 16px;
70
70
  background-color: var(--md-sys-color-surface-variant);
71
+ // Size the image to the handle. A 100% gradient keeps the circle in
72
+ // the middle of the track, so background-position cannot put off on
73
+ // the start and on on the end.
71
74
  background-image: radial-gradient(
72
- circle,
73
- var(--md-sys-color-outline) 0 8px,
74
- transparent 8.5px
75
+ circle closest-side,
76
+ var(--md-sys-color-outline) 100%,
77
+ transparent 101%
75
78
  );
76
79
  background-repeat: no-repeat;
77
- background-position: 8px center;
80
+ background-origin: padding-box;
81
+ background-size: var(--md-comp-switch-handle-unselected) var(--md-comp-switch-handle-unselected);
82
+ // Lengths only. A % in background-position is “align this point of the
83
+ // image with that point of the box”, so calc(100% - handle) leaves the
84
+ // thumb on the start and it only grows.
85
+ background-position: 6px 50%;
78
86
  cursor: pointer;
79
87
  transition: background-color .2s ease, background-position .2s ease,
80
88
  background-size .2s ease, border-color .2s ease, box-shadow .2s ease;
@@ -83,11 +91,16 @@
83
91
  border-color: var(--md-sys-color-primary);
84
92
  background-color: var(--md-sys-color-primary);
85
93
  background-image: radial-gradient(
86
- circle,
87
- var(--md-sys-color-on-primary) 0 12px,
88
- transparent 12.5px
94
+ circle closest-side,
95
+ var(--md-sys-color-on-primary) 100%,
96
+ transparent 101%
89
97
  );
90
- background-position: calc(100% - 14px) center;
98
+ background-size: var(--md-comp-switch-handle-selected) var(--md-comp-switch-handle-selected);
99
+ // padding-box is track minus the 2px border on each side; 2px inset.
100
+ background-position: calc(
101
+ var(--md-comp-switch-track-width) - 4px - 2px - var(--md-comp-switch-handle-selected)
102
+ )
103
+ 50%;
91
104
  }
92
105
 
93
106
  &:hover {