mtrl 0.7.0 → 0.8.0-next.4

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 (48) hide show
  1. package/dist/components/button/api.d.ts +4 -0
  2. package/dist/components/button/features/toggle.d.ts +42 -0
  3. package/dist/components/button/types.d.ts +30 -0
  4. package/dist/components/button-group/button-group.d.ts +6 -4
  5. package/dist/components/button-group/config.d.ts +15 -7
  6. package/dist/components/button-group/constants.d.ts +85 -17
  7. package/dist/components/button-group/index.d.ts +1 -1
  8. package/dist/components/button-group/types.d.ts +68 -4
  9. package/dist/components/carousel/api.d.ts +11 -30
  10. package/dist/components/carousel/arrangement.d.ts +23 -0
  11. package/dist/components/carousel/carousel.d.ts +13 -63
  12. package/dist/components/carousel/config.d.ts +2 -31
  13. package/dist/components/carousel/constants.d.ts +25 -151
  14. package/dist/components/carousel/features/index.d.ts +2 -5
  15. package/dist/components/carousel/features/scroll.d.ts +19 -0
  16. package/dist/components/carousel/features/slides.d.ts +18 -8
  17. package/dist/components/carousel/index.d.ts +3 -37
  18. package/dist/components/carousel/keylines.d.ts +65 -0
  19. package/dist/components/carousel/strategy.d.ts +36 -0
  20. package/dist/components/carousel/types.d.ts +36 -242
  21. package/dist/components/dialog/api.d.ts +1 -1
  22. package/dist/components/index.d.ts +2 -0
  23. package/dist/components/slider/constants.d.ts +8 -8
  24. package/dist/components/slider/types.d.ts +4 -1
  25. package/dist/components/snackbar/api.d.ts +1 -1
  26. package/dist/components/snackbar/config.d.ts +2 -1
  27. package/dist/components/snackbar/constants.d.ts +18 -0
  28. package/dist/components/snackbar/index.d.ts +2 -1
  29. package/dist/components/snackbar/queue.d.ts +10 -2
  30. package/dist/components/snackbar/types.d.ts +37 -1
  31. package/dist/index.cjs +20 -10
  32. package/dist/index.cjs.map +54 -38
  33. package/dist/index.js +20 -10
  34. package/dist/index.js.map +54 -38
  35. package/dist/package.json +1 -1
  36. package/dist/styles.css +2 -2
  37. package/package.json +3 -6
  38. package/src/styles/abstract/_theme.scss +4 -2
  39. package/src/styles/abstract/_variables.scss +59 -12
  40. package/src/styles/components/_button-group.scss +122 -349
  41. package/src/styles/components/_button.scss +333 -591
  42. package/src/styles/components/_carousel.scss +131 -611
  43. package/src/styles/components/_icon-button.scss +23 -23
  44. package/src/styles/components/_select.scss +9 -1
  45. package/src/styles/components/_slider.scss +10 -9
  46. package/src/styles/themes/_index.scss +1 -0
  47. package/src/styles/themes/_legacy.scss +73 -0
  48. package/dist/components/carousel/features/drag.d.ts +0 -8
@@ -1,4 +1,6 @@
1
- // src/components/button/_styles.scss
1
+ // src/styles/components/_button.scss
2
+ @use 'sass:map';
3
+ @use 'sass:list';
2
4
  @use '../../styles/abstract/base' as base;
3
5
  @use '../../styles/abstract/variables' as v;
4
6
  @use '../../styles/abstract/functions' as f;
@@ -10,27 +12,59 @@ $component: '#{base.$prefix}-button';
10
12
  // =============================================================================
11
13
  // BUTTON COMPONENT STYLES
12
14
  // =============================================================================
15
+ // Material Design 3 common button.
16
+ //
17
+ // Measurements, shapes and colour roles follow the Compose Material 3 token
18
+ // files (ButtonXSmallTokens.kt … ButtonXLargeTokens.kt, FilledButtonTokens.kt,
19
+ // ElevatedButtonTokens.kt, TonalButtonTokens.kt, OutlinedButtonTokens.kt,
20
+ // TextButtonTokens.kt) and ButtonDefaults in Button.kt. Values live in the
21
+ // $button map in abstract/_variables.scss.
22
+ //
13
23
  // Supports:
14
- // - Multiple variants (filled, elevated, tonal, outlined, text)
15
- // - Icon integration
16
- // - Progress indicator integration
17
- // - Circular/icon-only buttons
18
- // - Ripple effects
19
- // - State layers
20
- // - Accessibility focus indicators
24
+ // - Five colour styles (filled, elevated, tonal, outlined, text)
25
+ // - Five sizes (xs, s, m, l, xl) and two shapes (round, square)
26
+ // - Leading icon, integrated progress indicator
27
+ // - State layers, focus ring, pressed shape morph
28
+ // - 48dp touch target on the xs and s sizes
29
+ //
30
+ // Shape hooks: a container (button group, split button) can override the
31
+ // resting, pressed and selected corner shapes through the custom properties
32
+ // --<prefix>-button-shape, --<prefix>-button-shape-pressed and
33
+ // --<prefix>-button-shape-selected, without fighting the rules below.
21
34
  // =============================================================================
22
35
 
36
+ $sizes: xs, s, m, l, xl;
37
+
38
+ // Label typescale per size (ButtonDefaults.textStyleFor)
39
+ $label-typescale: (
40
+ 'xs': 'label-large',
41
+ 's': 'label-large',
42
+ 'm': 'title-medium',
43
+ 'l': 'headline-small',
44
+ 'xl': 'headline-large'
45
+ );
46
+
47
+ // Colour of the icon, label and state layer per colour style
48
+ $content-color: (
49
+ 'filled': 'on-primary',
50
+ 'elevated': 'primary',
51
+ 'tonal': 'on-secondary-container',
52
+ 'outlined': 'on-surface-variant',
53
+ 'text': 'primary'
54
+ );
55
+
23
56
  .#{$component} {
24
- // Base styles
57
+ // Base styles (the s size is the default)
25
58
  position: relative;
26
59
  display: inline-flex;
27
60
  align-items: center;
28
61
  justify-content: center;
62
+ gap: v.button('icon-gap-s');
29
63
  min-width: v.button('min-width');
30
- height: v.button('height');
31
- padding: 0 v.button('padding-horizontal');
64
+ height: v.button('height-s');
65
+ padding: 0 v.button('padding-s');
32
66
  border: none;
33
- border-radius: v.button('border-radius');
67
+ border-radius: var(--#{base.$prefix}-button-shape, v.button('border-radius'));
34
68
  background-color: transparent;
35
69
  color: inherit;
36
70
  font: inherit;
@@ -39,718 +73,426 @@ $component: '#{base.$prefix}-button';
39
73
  user-select: none;
40
74
  vertical-align: middle;
41
75
  appearance: none;
42
- overflow: hidden;
43
- will-change: border-radius, transform;
44
-
76
+ will-change: border-radius;
77
+
45
78
  // Typography
46
79
  @include m.typography('label-large');
47
-
48
- // Default to round shape (pill-shaped)
49
- border-radius: v.button('border-radius');
50
-
51
- // Explicitly apply fast transition to border-radius
52
- transition:
53
- background-color v.motion('duration-short2') v.motion('easing-standard'),
54
- color v.motion('duration-short2') v.motion('easing-standard'),
55
- box-shadow v.motion('duration-short2') v.motion('easing-standard'),
56
- border-radius v.motion('duration-short1') v.motion('easing-standard-accelerate'),
57
- transform v.motion('duration-short1') v.motion('easing-standard-accelerate');
58
-
59
- // Focus styles
80
+
81
+ // Colours follow the default effects spring, the shape morph the fast
82
+ // spatial spring (Compose Button and ToggleButton animation specs)
83
+ transition:
84
+ background-color v.motion('spring-default-effects-duration') v.motion('spring-default-effects-easing'),
85
+ color v.motion('spring-default-effects-duration') v.motion('spring-default-effects-easing'),
86
+ box-shadow v.motion('spring-default-effects-duration') v.motion('spring-default-effects-easing'),
87
+ border-color v.motion('spring-default-effects-duration') v.motion('spring-default-effects-easing'),
88
+ border-radius v.motion('spring-fast-spatial-duration') v.motion('spring-fast-spatial-easing');
89
+
90
+ // Focus indicator: 3dp secondary ring, 2dp outside the container
91
+ // (material-web tokens/_md-comp-focus-ring.scss)
60
92
  &:focus {
61
93
  outline: none;
62
94
  }
63
-
95
+
64
96
  &:focus-visible {
65
- outline: 2px solid t.color('outline');
97
+ outline: 3px solid t.color('secondary');
66
98
  outline-offset: 2px;
67
99
  }
68
-
69
- // Interactive states
100
+
101
+ // Disabled: container on-surface 10%, label and icon on-surface-variant 38%
102
+ // (FilledButtonTokens.Disabled*)
70
103
  &:disabled {
71
104
  pointer-events: none;
72
- color: t.alpha('surface', 0.38);
73
- background-color: t.alpha('surface', 0.12);
74
-
105
+ color: t.alpha('on-surface-variant', 0.38);
106
+ background-color: t.alpha('on-surface', 0.1);
107
+ box-shadow: none;
108
+
75
109
  .#{$component}-icon {
76
- color: t.alpha('surface', 0.38);
110
+ color: t.alpha('on-surface-variant', 0.38);
77
111
  }
78
112
  }
79
-
80
- // State layer for ripple effect
113
+
114
+ // State layer
81
115
  &::before {
82
116
  content: '';
83
117
  position: absolute;
84
- top: 0;
85
- left: 0;
86
- right: 0;
87
- bottom: 0;
118
+ inset: 0;
88
119
  background-color: currentColor;
89
120
  opacity: 0;
90
121
  pointer-events: none;
91
- transition: opacity 0.2s cubic-bezier(0.4, 0, 0.2, 1);
122
+ border-radius: inherit;
123
+ transition: opacity v.motion('duration-short2') v.motion('easing-standard');
92
124
  }
93
-
94
- // Ensure proper layout with icons
125
+
126
+ // Icon
95
127
  &-icon {
96
128
  display: inline-flex;
97
129
  align-items: center;
98
130
  justify-content: center;
99
- width: 22px;
100
- height: 22px;
101
-
131
+ flex: none;
132
+ width: v.button('icon-size-s');
133
+ height: v.button('icon-size-s');
134
+
102
135
  svg {
103
- width: 22px;
104
- height: 22px;
105
- }
106
-
107
- + .#{$component}-text {
108
- margin-left: 8px;
136
+ width: 100%;
137
+ height: 100%;
109
138
  }
110
139
  }
111
140
 
112
- // Progress element styling
141
+ // Progress indicator: takes the icon slot
113
142
  &-progress {
114
143
  display: inline-flex;
115
144
  align-items: center;
116
145
  justify-content: center;
117
- margin-inline-end: 8px;
118
- width: 20px;
119
- height: 20px;
120
-
121
- // When it's the mtrl-progress element itself
146
+ flex: none;
147
+ width: v.button('icon-size-s');
148
+ height: v.button('icon-size-s');
149
+
122
150
  &.#{base.$prefix}-progress {
123
- // Reset progress default size since we're setting it here
124
- width: 20px;
125
- height: 20px;
126
-
127
- &-canvas {
128
- color: currentColor;
129
- }
130
- }
131
-
132
- // In text-only buttons (no icon), progress acts like an icon
133
- &:first-child {
134
- margin-inline-start: 0;
151
+ width: v.button('icon-size-s');
152
+ height: v.button('icon-size-s');
153
+ transition: opacity v.motion('duration-short2') v.motion('easing-standard');
135
154
  }
136
-
137
- // When following a visible icon, add left margin too
138
- .#{$component}-icon:not([style*="display: none"]) + & {
139
- margin-inline-start: 4px;
155
+
156
+ .#{base.$prefix}-progress-canvas {
157
+ color: currentColor;
140
158
  }
141
159
  }
142
-
160
+
143
161
  &-text {
144
- // Text truncation for long button labels
145
162
  @include m.truncate;
146
-
163
+
147
164
  .#{$component}--icon-only & {
148
165
  @include m.visually-hidden;
149
166
  }
150
167
  }
151
-
152
- // Circular buttons
153
- &--circular {
154
- flex: none !important;
155
- border-radius: 50%;
156
- padding: 8px;
157
- min-width: unset;
158
- width: 40px !important ;
159
- height: 40px;
160
- display: inline-flex;
161
- align-items: center;
162
- justify-content: center;
163
-
164
- // Material Design 3 Accessibility Requirement:
165
- // Icon buttons must have a minimum touch target of 48x48dp
166
- // even when their visual size appears smaller
167
-
168
- // Circular buttons should always remain circular when pressed
169
- &:active,
170
- &.#{$component}--active {
171
- border-radius: 50%;
172
- &.#{$component}--square {
173
- border-radius: 12px !important;
174
- }
175
- }
176
168
 
177
- &.#{$component}--square {
178
- border-radius: 12px;
179
- }
169
+ // ---------------------------------------------------------------------------
170
+ // Sizes
171
+ // ---------------------------------------------------------------------------
172
+ @each $size in $sizes {
173
+ &--#{$size} {
174
+ height: v.button('height-#{$size}');
175
+ padding: 0 v.button('padding-#{$size}');
176
+ gap: v.button('icon-gap-#{$size}');
177
+ @include m.typography(map.get($label-typescale, $size));
180
178
 
181
- // Size-specific circular button dimensions
182
- &.#{$component}--xs {
183
- // Ensure minimum 48dp touch target for accessibility
184
- width: v.button('height-xs');
185
- height: v.button('height-xs');
186
- padding: 8px;
187
-
188
-
189
- // Adjust icon size to maintain visual proportions
190
- .#{$component}-icon {
191
- width: v.button('icon-size-xs');
192
- height: v.button('icon-size-xs');
193
-
194
- svg {
195
- width: v.button('icon-size-xs');
196
- height: v.button('icon-size-xs');
197
- }
179
+ .#{$component}-icon,
180
+ .#{$component}-progress,
181
+ .#{$component}-progress.#{base.$prefix}-progress {
182
+ width: v.button('icon-size-#{$size}');
183
+ height: v.button('icon-size-#{$size}');
198
184
  }
199
185
 
200
-
201
- }
202
-
203
- &.#{$component}--s {
204
- // Ensure minimum 48dp touch target for accessibility
205
- width: v.button('height-s');
206
- height: v.button('height-s');
207
- padding: 8px;
208
-
209
- // Standard icon size for S
210
- .#{$component}-icon {
211
- width: v.button('icon-size-s');
212
- height: v.button('icon-size-s');
213
-
214
- svg {
215
- width: v.button('icon-size-s');
216
- height: v.button('icon-size-s');
217
- }
186
+ // Square shape at rest
187
+ &.#{$component}--square {
188
+ border-radius: var(--#{base.$prefix}-button-shape, v.button('shape-square-#{$size}'));
218
189
  }
219
- }
220
-
221
- &.#{$component}--m {
222
- width: v.button('height-m');
223
- height: v.button('height-m');
224
- padding: 12px;
225
- }
226
-
227
- &.#{$component}--l {
228
- width: v.button('height-l');
229
- height: v.button('height-l');
230
- padding: 20px;
231
- }
232
-
233
- &.#{$component}--xl {
234
- width: v.button('height-xl');
235
- height: v.button('height-xl');
236
- padding: 32px;
237
- }
238
190
 
239
- .#{$component}-icon {
240
- margin: 0;
241
- }
242
-
243
- // Hide text in circular buttons
244
- .#{$component}-text {
245
- display: none;
246
- }
247
-
248
- // Progress in circular buttons
249
- .#{$component}-progress {
250
- margin: 0;
251
-
252
- &.#{base.$prefix}-progress {
253
- width: 24px;
254
- height: 24px;
255
- }
256
- }
257
-
258
- // Override padding adjustments for circular buttons
259
- &.#{$component}--progress {
260
- padding: 0 !important;
261
- }
262
- }
263
-
264
- &--icon {
265
- padding: 0 v.button('padding-horizontal') 0 calc(v.button('padding-horizontal') / 2 + 4px);
266
- }
267
-
268
- // Shape variants
269
- &--round {
270
- // Default round shape - already applied in base styles
271
- // Sizes M, L, XL get special pill-shaped radius
272
- &.#{$component}--m {
273
- border-radius: calc(v.button('height-m') / 2);
274
- }
275
-
276
- &.#{$component}--l {
277
- border-radius: calc(v.button('height-l') / 2);
278
- }
279
-
280
- &.#{$component}--xl {
281
- border-radius: calc(v.button('height-xl') / 2);
282
- }
283
- }
284
-
285
- &--square {
286
- // Square shape with size-specific border radius
287
- &.#{$component}--xs,
288
- &.#{$component}--s {
289
- border-radius: f.get-shape('medium'); // 12dp
290
- }
291
-
292
- &.#{$component}--m {
293
- border-radius: f.get-shape('large'); // 16dp
294
- }
295
-
296
- &.#{$component}--l,
297
- &.#{$component}--xl {
298
- border-radius: f.get-shape('extra-large'); // 28dp
299
- }
300
-
301
- // Pressed state for square buttons - no morphing needed
302
- &:active,
303
- &.#{$component}--active {
304
- // Square buttons maintain their border radius when pressed
305
- }
306
- }
307
-
308
- // Size variants
309
- &--xs {
310
- height: v.button('height-xs');
311
- padding: 0 v.button('padding-xs');
312
- min-width: 48px;
313
-
314
- &.#{$component}--icon {
315
- padding: 0 v.button('padding-xs') 0 calc(v.button('padding-xs') / 2 + 4px);
316
- }
317
-
318
- &.#{$component}--progress {
319
- padding: 0 v.button('padding-xs') 0 calc(v.button('padding-xs') / 2 + 4px);
320
- }
321
-
322
- &.#{$component}--text {
323
- padding: 0 calc(v.button('padding-xs') - 4px);
324
- }
325
-
326
- .#{$component}-icon {
327
- width: v.button('icon-size-xs');
328
- height: v.button('icon-size-xs');
329
-
330
- svg {
331
- width: v.button('icon-size-xs');
332
- height: v.button('icon-size-xs');
333
- }
334
- }
335
-
336
- .#{$component}-progress {
337
- width: v.button('icon-size-xs');
338
- height: v.button('icon-size-xs');
339
-
340
- &.#{base.$prefix}-progress {
341
- width: v.button('icon-size-xs');
342
- height: v.button('icon-size-xs');
191
+ // Outlined outline width
192
+ &.#{$component}--outlined {
193
+ border-width: v.button('outline-width-#{$size}');
343
194
  }
344
195
  }
345
196
  }
346
197
 
347
- &--s {
348
- // S is the default size, already defined in base styles
349
- // Explicitly setting them here for clarity
350
- height: v.button('height-s');
351
- padding: 0 v.button('padding-s');
352
-
353
- &.#{$component}--icon {
354
- padding: 0 v.button('padding-s') 0 calc(v.button('padding-s') / 2 + 4px);
355
- }
356
-
357
- &.#{$component}--progress {
358
- padding: 0 v.button('padding-s') 0 calc(v.button('padding-s') / 2 + 4px);
359
- }
360
-
361
- &.#{$component}--text {
362
- padding: 0 v.button('padding-horizontal-small');
363
- }
364
- }
198
+ // Small button next to an icon: 12dp on the icon side on pointer devices
199
+ // (ButtonDefaults.getSmallContentPadding)
200
+ &--s.#{$component}--icon {
201
+ padding-left: v.button('padding-icon');
365
202
 
366
- &--m {
367
- height: v.button('height-m');
368
- padding: 0 v.button('padding-m');
369
- min-width: 80px;
370
-
371
- &.#{$component}--icon {
372
- padding: 0 v.button('padding-m') 0 calc(v.button('padding-m') / 2 + 8px);
373
- }
374
-
375
- &.#{$component}--progress {
376
- padding: 0 v.button('padding-m') 0 calc(v.button('padding-m') / 2 + 8px);
377
- }
378
-
379
- &.#{$component}--text {
380
- padding: 0 calc(v.button('padding-m') - 4px);
381
- }
382
-
383
- .#{$component}-icon {
384
- width: v.button('icon-size-m');
385
- height: v.button('icon-size-m');
386
-
387
- svg {
388
- width: v.button('icon-size-m');
389
- height: v.button('icon-size-m');
390
- }
391
- }
392
-
393
- .#{$component}-progress {
394
- width: v.button('icon-size-m');
395
- height: v.button('icon-size-m');
396
-
397
- &.#{base.$prefix}-progress {
398
- width: v.button('icon-size-m');
399
- height: v.button('icon-size-m');
400
- }
203
+ &:has(.#{$component}-icon--end) {
204
+ padding-left: v.button('padding-s');
205
+ padding-right: v.button('padding-icon');
401
206
  }
402
207
  }
403
208
 
404
- &--l {
405
- height: v.button('height-l');
406
- padding: 0 v.button('padding-l');
407
- min-width: 120px;
408
-
409
- // Use larger typography for L and XL sizes
410
- @include m.typography('title-large');
411
-
412
- &.#{$component}--icon {
413
- padding: 0 v.button('padding-l') 0 calc(v.button('padding-l') / 2 + 12px);
414
- }
415
-
416
- &.#{$component}--progress {
417
- padding: 0 v.button('padding-l') 0 calc(v.button('padding-l') / 2 + 12px);
418
- }
419
-
420
- &.#{$component}--text {
421
- padding: 0 calc(v.button('padding-l') - 8px);
422
- }
423
-
424
- .#{$component}-icon {
425
- width: v.button('icon-size-l');
426
- height: v.button('icon-size-l');
427
-
428
- svg {
429
- width: v.button('icon-size-l');
430
- height: v.button('icon-size-l');
431
- }
432
-
433
- + .#{$component}-text {
434
- margin-left: 16px;
435
- }
436
- }
437
-
438
- .#{$component}-progress {
439
- width: v.button('icon-size-l');
440
- height: v.button('icon-size-l');
441
- margin-inline-end: 16px;
442
-
443
- &.#{base.$prefix}-progress {
444
- width: v.button('icon-size-l');
445
- height: v.button('icon-size-l');
446
- }
209
+ // The xs and s sizes keep a 48dp touch target (m3.material.io button specs,
210
+ // target areas): the hit area extends above and below the container.
211
+ &--xs,
212
+ &--s {
213
+ &::after {
214
+ content: '';
215
+ position: absolute;
216
+ left: 0;
217
+ right: 0;
218
+ top: 50%;
219
+ height: v.button('touch-target');
220
+ transform: translateY(-50%);
447
221
  }
448
222
  }
449
223
 
450
- &--xl {
451
- height: v.button('height-xl');
452
- padding: 0 v.button('padding-xl');
453
- min-width: 160px;
454
-
455
- // Use larger typography for L and XL sizes
456
- @include m.typography('headline-medium');
457
-
458
- &.#{$component}--icon {
459
- padding: 0 v.button('padding-xl') 0 calc(v.button('padding-xl') / 2 + 16px);
460
- }
461
-
462
- &.#{$component}--progress {
463
- padding: 0 v.button('padding-xl') 0 calc(v.button('padding-xl') / 2 + 16px);
464
- }
465
-
466
- &.#{$component}--text {
467
- padding: 0 calc(v.button('padding-xl') - 12px);
468
- }
469
-
470
- .#{$component}-icon {
471
- width: v.button('icon-size-xl');
472
- height: v.button('icon-size-xl');
473
-
474
- svg {
475
- width: v.button('icon-size-xl');
476
- height: v.button('icon-size-xl');
477
- }
478
-
479
- + .#{$component}-text {
480
- margin-left: 24px;
481
- }
482
- }
483
-
484
- .#{$component}-progress {
485
- width: v.button('icon-size-xl');
486
- height: v.button('icon-size-xl');
487
- margin-inline-end: 24px;
488
-
489
- &.#{base.$prefix}-progress {
490
- width: v.button('icon-size-xl');
491
- height: v.button('icon-size-xl');
492
- }
493
- }
494
- }
495
-
224
+ // ---------------------------------------------------------------------------
225
+ // Pressed shape: the container squares off while pressed, for both shapes
226
+ // (Button*Tokens.PressedContainerShape). Declared after the size and shape
227
+ // rules so it wins at equal specificity.
228
+ // ---------------------------------------------------------------------------
229
+ &:active,
496
230
  &--active {
497
- // Common active state styling for all variants
231
+ border-radius: var(--#{base.$prefix}-button-shape-pressed, v.button('shape-pressed-s'));
498
232
  box-shadow: none;
499
-
500
- // Per MD3, buttons have a significantly more rectangular appearance when pressed
501
- // Default to small shape
502
- border-radius: f.get-shape('small');
503
-
504
- // ARIA attribute to indicate active state for accessibility
505
- &[aria-expanded="true"] {
506
- // For compatibility with ARIA
507
- }
508
233
  }
509
234
 
510
- // Size-specific active state border radius based on MD3 specs
511
- // Only applies to round shape buttons - square buttons maintain their radius
512
- &--round {
513
- &.#{$component}--xs,
514
- &.#{$component}--s {
235
+ @each $size in $sizes {
236
+ &--#{$size} {
515
237
  &:active,
516
238
  &.#{$component}--active {
517
- // XS and S sizes use 12dp radius when pressed
518
- border-radius: f.get-shape('medium');
239
+ border-radius: var(--#{base.$prefix}-button-shape-pressed, v.button('shape-pressed-#{$size}'));
519
240
  }
520
241
  }
521
-
522
- &.#{$component}--m {
523
- &:active,
524
- &.#{$component}--active {
525
- // M size uses 16dp radius when pressed
526
- border-radius: f.get-shape('large');
242
+ }
243
+
244
+ // ---------------------------------------------------------------------------
245
+ // Colour styles
246
+ // ---------------------------------------------------------------------------
247
+ @each $variant, $role in $content-color {
248
+ &--#{$variant} {
249
+ color: t.color($role);
250
+
251
+ &:hover {
252
+ @include m.state-layer(t.color($role), 'hover');
527
253
  }
528
- }
529
-
530
- &.#{$component}--l,
531
- &.#{$component}--xl {
254
+
255
+ &:focus-visible {
256
+ @include m.state-layer(t.color($role), 'focus');
257
+ }
258
+
532
259
  &:active,
533
260
  &.#{$component}--active {
534
- // L and XL sizes use 28dp radius when pressed
535
- border-radius: f.get-shape('extra-large');
261
+ @include m.state-layer(t.color($role), 'pressed');
536
262
  }
537
263
  }
538
264
  }
539
265
 
540
- // FILLED BUTTON
266
+ // Filled: primary container, level 1 on hover (FilledButtonTokens)
541
267
  &--filled {
542
268
  background-color: t.color('primary');
543
- color: t.color('on-primary');
544
-
545
- // Progress inherits the on-primary color
546
- .#{base.$prefix}-progress-canvas {
547
- color: t.color('on-primary');
548
- }
549
-
269
+
550
270
  &:hover {
551
- @include m.state-layer(t.color('on-primary'), 'hover');
552
- // According to MD3, filled buttons DO have elevation on hover
553
271
  @include m.elevation(1);
554
272
  }
555
-
273
+
556
274
  &:active,
557
- &.#{$component}--active {
558
- @include m.state-layer(t.color('on-primary'), 'pressed');
559
- // MD3 reduces elevation on press
560
- @include m.elevation(0);
561
- }
275
+ &.#{$component}--active,
562
276
  &:disabled {
563
277
  @include m.elevation(0);
564
278
  }
565
279
  }
566
-
567
- // ELEVATED BUTTON
280
+
281
+ // Elevated: surface-container-low, level 1 at rest, 2 on hover, 0 disabled
282
+ // (ElevatedButtonTokens)
568
283
  &--elevated {
569
284
  background-color: t.color('surface-container-low');
570
- color: t.color('primary');
571
285
  @include m.elevation(1);
572
-
573
- // Progress uses primary color
574
- .#{base.$prefix}-progress-canvas {
575
- color: t.color('primary');
576
- }
577
-
286
+
578
287
  &:hover {
579
- @include m.state-layer(t.color('primary'), 'hover');
580
- // MD3 increases elevation on hover
581
288
  @include m.elevation(2);
582
289
  }
583
-
290
+
584
291
  &:active,
585
292
  &.#{$component}--active {
586
- @include m.state-layer(t.color('primary'), 'pressed');
587
- // MD3 reduces elevation on press, but not completely removed
588
293
  @include m.elevation(1);
589
294
  }
590
-
295
+
591
296
  &:disabled {
592
- @include m.elevation(0);
593
297
  box-shadow: none;
594
298
  }
595
-
596
- // Handle icon color in elevated button
597
- .#{$component}-icon {
598
- color: t.color('primary');
599
- }
600
299
  }
601
-
602
- // TONAL BUTTON
300
+
301
+ // Tonal: secondary container, level 1 on hover (TonalButtonTokens)
603
302
  &--tonal {
604
303
  background-color: t.color('secondary-container');
605
- color: t.color('on-secondary-container');
606
- // MD3 tonal buttons have a subtle initial elevation
607
- @include m.elevation(0);
608
-
609
- // Progress uses on-secondary-container color
610
- .#{base.$prefix}-progress-canvas {
611
- color: t.color('on-secondary-container');
612
- }
613
-
304
+
614
305
  &:hover {
615
- @include m.state-layer(t.color('on-secondary-container'), 'hover');
616
- // MD3 tonal buttons have elevation on hover
617
306
  @include m.elevation(1);
618
307
  }
619
-
308
+
620
309
  &:active,
621
- &.#{$component}--active {
622
- @include m.state-layer(t.color('on-secondary-container'), 'pressed');
623
- // MD3 reduces elevation on press
624
- @include m.elevation(0);
625
- }
626
-
310
+ &.#{$component}--active,
627
311
  &:disabled {
628
- box-shadow: none;
312
+ @include m.elevation(0);
629
313
  }
630
314
  }
631
-
632
- // OUTLINED BUTTON
315
+
316
+ // Outlined: outline-variant outline, on-surface-variant content
317
+ // (OutlinedButtonTokens, ButtonDefaults.outlinedButtonBorder)
633
318
  &--outlined {
634
- border: 1px solid rgba(var(--mtrl-sys-color-outline-rgb), 0.38);
635
- color: t.color('primary');
636
-
637
- // Progress uses primary color
638
- .#{base.$prefix}-progress-canvas {
639
- color: t.color('primary');
640
- }
641
-
642
- &:hover {
643
- @include m.state-layer(t.color('primary'), 'hover');
644
- }
645
-
646
- &:active,
647
- &.#{$component}--active {
648
- @include m.state-layer(t.color('primary'), 'pressed');
649
- }
650
-
319
+ border: v.button('outline-width-s') solid t.color('outline-variant');
320
+
651
321
  &:disabled {
652
- border: 1px solid t.alpha('outline', 0.12);
322
+ background-color: transparent;
323
+ border-color: t.alpha('outline-variant', 0.1);
653
324
  }
654
325
  }
655
-
656
- // TEXT BUTTON
326
+
327
+ // Text: no container, 12dp horizontal padding, 16dp beside the label when
328
+ // there is an icon (ButtonDefaults.TextButtonContentPadding,
329
+ // TextButtonWithIconContentPadding). Larger sizes use the size's spacing.
657
330
  &--text {
658
- min-width: v.button('min-width');
659
331
  padding: 0 v.button('padding-horizontal-small');
660
- color: t.color('primary');
661
-
662
- // Progress uses primary color
663
- .#{base.$prefix}-progress-canvas {
664
- color: t.color('primary');
665
- }
666
-
667
- &:hover {
668
- @include m.state-layer(t.color('primary'), 'hover');
332
+
333
+ &.#{$component}--icon {
334
+ padding: 0 v.button('padding-s') 0 v.button('padding-horizontal-small');
335
+
336
+ &:has(.#{$component}-icon--end) {
337
+ padding: 0 v.button('padding-horizontal-small') 0 v.button('padding-s');
338
+ }
669
339
  }
670
-
671
- &:active,
672
- &.#{$component}--active {
673
- @include m.state-layer(t.color('primary'), 'pressed');
340
+
341
+ @each $size in m, l, xl {
342
+ &.#{$component}--#{$size} {
343
+ padding: 0 v.button('padding-#{$size}');
344
+ }
674
345
  }
675
-
346
+
676
347
  &:disabled {
677
- background-color: transparent;
348
+ background-color: transparent;
678
349
  }
679
350
  }
680
-
681
- // Special case for icon-only buttons
682
- &--icon-only {
683
- min-width: v.button('height');
684
- width: v.button('height');
685
- padding: 0;
686
- border-radius: 50%;
687
- }
688
351
 
689
- // When button has progress showing
690
- &--progress {
691
- // Reduce left padding when progress is showing, similar to icon buttons
692
- padding: 0 v.button('padding-horizontal') 0 calc(v.button('padding-horizontal') / 2 + 4px);
693
-
694
- // Per MD3, round buttons have a significantly more rectangular appearance when progress (active) is showing
695
- // Square buttons maintain their standard border radius
696
- &.#{$component}--round {
697
- &.#{$component}--xs,
698
- &.#{$component}--s {
699
- border-radius: f.get-shape('medium') !important;
352
+ // ---------------------------------------------------------------------------
353
+ // Toggle buttons (ToggleButtonDefaults in ToggleButton.kt, *ButtonTokens
354
+ // Unselected*/Selected* roles). Unselected filled buttons drop to
355
+ // surface-container; selected buttons take the selected roles and swap
356
+ // the resting shape: round becomes square, square becomes round
357
+ // (m3.material.io button specs, "When selected").
358
+ // ---------------------------------------------------------------------------
359
+ &--toggle {
360
+ &.#{$component}--filled {
361
+ background-color: t.color('surface-container');
362
+ color: t.color('on-surface-variant');
363
+
364
+ &:hover {
365
+ @include m.state-layer(t.color('on-surface-variant'), 'hover');
700
366
  }
701
-
702
- &.#{$component}--m {
703
- border-radius: f.get-shape('large') !important;
367
+
368
+ &:focus-visible {
369
+ @include m.state-layer(t.color('on-surface-variant'), 'focus');
704
370
  }
705
-
706
- &.#{$component}--l,
707
- &.#{$component}--xl {
708
- border-radius: f.get-shape('extra-large') !important;
371
+
372
+ &:active,
373
+ &.#{$component}--active {
374
+ @include m.state-layer(t.color('on-surface-variant'), 'pressed');
709
375
  }
710
376
  }
711
377
 
712
- // If button also has an icon, use even less left padding
713
- &.#{$component}--icon {
714
- padding: 0 v.button('padding-horizontal') 0 calc(v.button('padding-horizontal') / 2);
378
+ &.#{$component}--selected {
379
+ $selected: (
380
+ 'filled': ('primary', 'on-primary'),
381
+ 'elevated': ('primary', 'on-primary'),
382
+ 'tonal': ('secondary', 'on-secondary'),
383
+ 'outlined': ('inverse-surface', 'inverse-on-surface')
384
+ );
385
+
386
+ @each $variant, $roles in $selected {
387
+ $container: list.nth($roles, 1);
388
+ $content: list.nth($roles, 2);
389
+
390
+ &.#{$component}--#{$variant} {
391
+ background-color: t.color($container);
392
+ color: t.color($content);
393
+
394
+ &:hover {
395
+ @include m.state-layer(t.color($content), 'hover');
396
+ }
397
+
398
+ &:focus-visible {
399
+ @include m.state-layer(t.color($content), 'focus');
400
+ }
401
+
402
+ &:active,
403
+ &.#{$component}--active {
404
+ @include m.state-layer(t.color($content), 'pressed');
405
+ }
406
+ }
407
+ }
408
+
409
+ &.#{$component}--outlined {
410
+ border-color: transparent;
411
+ }
412
+
413
+ // Shape swap: a round toggle squares off, a square one rounds
414
+ @each $size in $sizes {
415
+ &.#{$component}--#{$size} {
416
+ border-radius: var(--#{base.$prefix}-button-shape-selected, v.button('shape-square-#{$size}'));
417
+ }
418
+ }
419
+
420
+ &.#{$component}--square {
421
+ border-radius: var(--#{base.$prefix}-button-shape-selected, v.button('border-radius'));
422
+ }
423
+
424
+ @each $size in $sizes {
425
+ &.#{$component}--#{$size}:active,
426
+ &.#{$component}--#{$size}.#{$component}--active {
427
+ border-radius: var(--#{base.$prefix}-button-shape-pressed, v.button('shape-pressed-#{$size}'));
428
+ }
429
+ }
430
+ }
431
+
432
+ // Disabled keeps the common disabled roles whatever the selection
433
+ &:disabled,
434
+ &.#{$component}--selected:disabled {
435
+ color: t.alpha('on-surface-variant', 0.38);
436
+ background-color: t.alpha('on-surface', 0.1);
437
+ border-color: t.alpha('outline-variant', 0.1);
438
+ box-shadow: none;
439
+ }
440
+
441
+ &.#{$component}--outlined:disabled {
442
+ background-color: transparent;
715
443
  }
716
444
  }
717
- }
718
445
 
719
- // FIX: Disabled on dark theme (recommandations do not work as expected)
720
- html[data-theme-mode=dark] {
721
- // Interactive states
722
- .#{$component} {
723
- &:disabled {
724
- color: t.alpha('on-surface-dim', 0.38);
725
- background-color: t.alpha('surface-dim', 0.12);
446
+ // ---------------------------------------------------------------------------
447
+ // Icon-only and circular buttons (kept for the icon-button family)
448
+ // ---------------------------------------------------------------------------
449
+ &--icon-only {
450
+ min-width: v.button('height-s');
451
+ width: v.button('height-s');
452
+ padding: 0;
453
+ border-radius: 50%;
454
+ }
455
+
456
+ &--circular {
457
+ flex: none !important;
458
+ border-radius: 50%;
459
+ padding: 8px;
460
+ min-width: unset;
461
+ width: v.button('height-s') !important;
462
+ height: v.button('height-s');
463
+
464
+ &:active,
465
+ &.#{$component}--active {
466
+ border-radius: 50%;
726
467
 
727
- .#{$component}-text {
728
- opacity: 0.38;
468
+ &.#{$component}--square {
469
+ border-radius: 12px !important;
729
470
  }
730
- .#{$component}-icon {
731
- opacity: 0.38;
732
- color: t.alpha('on-surface-dim', 0.12);
471
+ }
472
+
473
+ &.#{$component}--square {
474
+ border-radius: 12px;
475
+ }
476
+
477
+ @each $size in $sizes {
478
+ &.#{$component}--#{$size} {
479
+ width: v.button('height-#{$size}');
480
+ height: v.button('height-#{$size}');
481
+ padding: 0;
733
482
  }
734
483
  }
735
- }
736
- }
737
484
 
738
- // Add loading state styles
739
- .#{$component} {
740
- // Loading state animations
741
- &[disabled] {
485
+ .#{$component}-text {
486
+ display: none;
487
+ }
488
+
742
489
  &.#{$component}--progress {
743
- .#{$component}-progress {
744
- // Ensure progress is visible in disabled state
745
- opacity: 0.7;
746
- }
490
+ padding: 0 !important;
747
491
  }
748
492
  }
749
-
750
- // Smooth transitions when showing/hiding progress
751
- .#{$component}-progress {
752
- &.#{base.$prefix}-progress {
753
- transition: opacity v.motion('duration-short2') v.motion('easing-standard');
754
- }
493
+
494
+ // Progress showing while disabled (loading state)
495
+ &[disabled].#{$component}--progress .#{$component}-progress {
496
+ opacity: 0.7;
755
497
  }
756
- }
498
+ }