@expressivecss/expressive 0.7.0-rc.0 → 0.8.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 (105) hide show
  1. package/CHANGELOG.md +1311 -1
  2. package/README.md +14 -11
  3. package/dist/css/expressive.css +4215 -1668
  4. package/dist/css/expressive.css.map +1 -1
  5. package/dist/css/expressive.min.css +1 -1
  6. package/dist/css/expressive.min.css.map +1 -1
  7. package/dist/js/expressive.cjs +1231 -1253
  8. package/dist/js/expressive.cjs.map +4 -4
  9. package/dist/js/expressive.js +1231 -1253
  10. package/dist/js/expressive.js.map +4 -4
  11. package/dist/js/expressive.min.js +12 -12
  12. package/dist/js/expressive.min.js.map +4 -4
  13. package/dist/js/expressive.mjs +1231 -1253
  14. package/dist/js/expressive.mjs.map +4 -4
  15. package/dist/types/behaviors/bottomSheets.d.ts +19 -11
  16. package/dist/types/behaviors/sheetDrag.d.ts +40 -0
  17. package/dist/types/behaviors/sideSheets.d.ts +2 -9
  18. package/dist/types/components/autocomplete.d.ts +15 -3
  19. package/dist/types/components/buttons.d.ts +5 -1
  20. package/dist/types/components/carousel.d.ts +87 -53
  21. package/dist/types/components/chips.d.ts +21 -1
  22. package/dist/types/components/expandingCard.d.ts +31 -0
  23. package/dist/types/components/index.d.ts +21 -20
  24. package/dist/types/components/lightbox.d.ts +1 -0
  25. package/dist/types/components/menu.d.ts +7 -1
  26. package/dist/types/components/{sidenav.d.ts → navigationDrawer.d.ts} +16 -16
  27. package/dist/types/components/registry.d.ts +8 -8
  28. package/dist/types/components/slider.d.ts +26 -85
  29. package/dist/types/components/snackbar.d.ts +20 -5
  30. package/dist/types/components/tabs.d.ts +12 -0
  31. package/dist/types/components/timepicker.d.ts +12 -9
  32. package/dist/types/core/utils.d.ts +64 -53
  33. package/dist/types/index.d.ts +8 -9
  34. package/package.json +15 -1
  35. package/src/sass/README.md +61 -11
  36. package/src/sass/abstracts/_breakpoints.scss +17 -19
  37. package/src/sass/abstracts/_mixins.scss +103 -21
  38. package/src/sass/abstracts/_variables.scss +50 -5
  39. package/src/sass/base/_global.scss +6 -24
  40. package/src/sass/base/_grid.scss +15 -12
  41. package/src/sass/base/_normalize.scss +34 -38
  42. package/src/sass/base/_typography.scss +4 -18
  43. package/src/sass/components/_badges.scss +2 -3
  44. package/src/sass/components/_banners.scss +298 -0
  45. package/src/sass/components/_bottom-app-bar.scss +180 -0
  46. package/src/sass/components/_bottom-sheet.scss +12 -4
  47. package/src/sass/components/_button-groups.scss +186 -0
  48. package/src/sass/components/_buttons.scss +245 -92
  49. package/src/sass/components/_cards.scss +494 -73
  50. package/src/sass/components/_carousel.scss +562 -68
  51. package/src/sass/components/_chips.scss +64 -5
  52. package/src/sass/components/_datepicker.scss +24 -24
  53. package/src/sass/components/_dialog.scss +17 -11
  54. package/src/sass/components/_drag-handle.scss +130 -0
  55. package/src/sass/components/_expanding-card.scss +260 -0
  56. package/src/sass/components/_fab-menu.scss +232 -0
  57. package/src/sass/components/_floating-sheet.scss +86 -0
  58. package/src/sass/components/_icon-buttons.scss +194 -0
  59. package/src/sass/components/_icons-material-design.scss +16 -4
  60. package/src/sass/components/_index.scss +22 -6
  61. package/src/sass/components/_list.scss +16 -13
  62. package/src/sass/components/_loading-indicator.scss +106 -0
  63. package/src/sass/components/_menu.scss +335 -61
  64. package/src/sass/components/_navbar.scss +71 -24
  65. package/src/sass/components/_navigation-bar.scss +13 -10
  66. package/src/sass/components/{_sidenav.scss → _navigation-drawer.scss} +123 -33
  67. package/src/sass/components/_navigation-rail.scss +22 -22
  68. package/src/sass/components/_page-footer.scss +19 -16
  69. package/src/sass/components/_pagination.scss +24 -6
  70. package/src/sass/components/_panes.scss +52 -11
  71. package/src/sass/components/{_preloader.scss → _progress.scss} +11 -3
  72. package/src/sass/components/_scrim.scss +37 -0
  73. package/src/sass/components/_search.scss +259 -0
  74. package/src/sass/components/_segmented-buttons.scss +188 -0
  75. package/src/sass/components/_side-sheet.scss +2 -2
  76. package/src/sass/components/_snackbar.scss +20 -15
  77. package/src/sass/components/_split-button.scss +196 -0
  78. package/src/sass/components/_tabs.scss +7 -8
  79. package/src/sass/components/_timepicker.scss +67 -58
  80. package/src/sass/components/_toolbar.scss +206 -80
  81. package/src/sass/components/forms/_checkboxes.scss +49 -12
  82. package/src/sass/components/forms/_fieldset.scss +4 -4
  83. package/src/sass/components/forms/_file-input.scss +8 -1
  84. package/src/sass/components/forms/_index.scss +1 -1
  85. package/src/sass/components/forms/_input-fields.scss +50 -44
  86. package/src/sass/components/forms/_radio-buttons.scss +42 -10
  87. package/src/sass/components/forms/_select.scss +0 -1
  88. package/src/sass/components/forms/{_range.scss → _slider.scss} +8 -9
  89. package/src/sass/components/forms/_switches.scss +6 -7
  90. package/src/sass/tokens/_index.scss +4 -1
  91. package/src/sass/tokens/_reference.scss +70 -61
  92. package/src/sass/tokens/_state.scss +24 -0
  93. package/src/sass/tokens/_theme.scss +6 -3
  94. package/src/sass/tokens/_vibrant.scss +76 -0
  95. package/src/sass/utilities/_colors.scss +14 -1
  96. package/src/sass/utilities/_helpers.scss +5 -5
  97. package/src/sass/utilities/_visibility.scss +48 -26
  98. package/dist/types/behaviors/waves.d.ts +0 -15
  99. package/dist/types/components/parallax.d.ts +0 -16
  100. package/dist/types/components/range.d.ts +0 -43
  101. package/dist/types/core/bounding.d.ts +0 -6
  102. package/dist/types/core/edges.d.ts +0 -6
  103. package/src/sass/components/_parallax.scss +0 -104
  104. package/src/sass/components/_pulse.scss +0 -37
  105. package/src/sass/components/_slider.scss +0 -129
@@ -0,0 +1,194 @@
1
+ @use "sass:map";
2
+ @use "../abstracts" as *;
3
+
4
+ // Icon buttons.
5
+ //
6
+ // Its own component, not a modifier on the common button: M3 gives it nine
7
+ // token families of its own — md.comp.icon-button.{xsmall…xlarge} for the size
8
+ // ladder and md.comp.icon-button-{standard,filled,tonal,outlined} for the
9
+ // colours — and neither the sizes nor the colours line up with a common
10
+ // button's. `.icon-button` is listed in `$_not-btn` in `_buttons.scss` for
11
+ // exactly that reason, so none of the common-button geometry reaches it.
12
+ //
13
+ // The 40dp `.button.circle` icon button is untouched. It stays what it always
14
+ // was: a common button wearing a round shape.
15
+ //
16
+ // Hosts step aside for it. A component that styles its own child controls
17
+ // already lets a control that styles itself opt out - that is what `.button` is
18
+ // doing in those `:not()` lists - and `.icon-button` is now in every one of
19
+ // them (toolbar, app bar, list row, dialog, snackbar, rail, panes, footer,
20
+ // side sheet). `tests/icon-buttons.test.js` keeps the lists in step; the member
21
+ // goes inside the existing `:not()` argument list, never chained after it, so
22
+ // the host rule stays exactly as specific as it was.
23
+ //
24
+ // The navigation drawer is the deliberate exception. Its `$_row` geometry has
25
+ // no opt-out for `.button` either: every row in a drawer is one shape, and
26
+ // carving an icon button out of that is the drawer's decision to make, not
27
+ // this component's.
28
+ //
29
+ // Anatomy is the HTML — one control, one icon, and the name on the control.
30
+ // The icon is decoration (`aria-hidden`), so `aria-label` is the only name the
31
+ // control has; `semantics.json` enforces both halves of that.
32
+ //
33
+ // Tokens (M3 md.comp.icon-button.*, DSP 34.0.21):
34
+ //
35
+ // size height icon space narrow wide square pressed outline
36
+ // xsmall 32 20 6 4 10 12 8 1
37
+ // small 40 24 8 4 14 12 8 1 <- default
38
+ // medium 56 24 16 12 24 16 12 1
39
+ // large 96 32 32 16 48 28 16 2
40
+ // xlarge 136 40 48 32 72 28 16 3
41
+ //
42
+ // Width is not a token: it is leading + icon + trailing. That is why every
43
+ // size's default width equals its height, and why `.narrow` / `.wide` need
44
+ // nothing but a different inset.
45
+ $_sizes: (
46
+ "xsmall": (height: 32px, icon: 20px, space: 6px, narrow: 4px, wide: 10px, square: 12px, pressed: 8px, outline: 1px),
47
+ "small": (height: 40px, icon: 24px, space: 8px, narrow: 4px, wide: 14px, square: 12px, pressed: 8px, outline: 1px),
48
+ "medium": (height: 56px, icon: 24px, space: 16px, narrow: 12px, wide: 24px, square: 16px, pressed: 12px, outline: 1px),
49
+ "large": (height: 96px, icon: 32px, space: 32px, narrow: 16px, wide: 48px, square: 28px, pressed: 16px, outline: 2px),
50
+ "xlarge": (height: 136px, icon: 40px, space: 48px, narrow: 32px, wide: 72px, square: 28px, pressed: 16px, outline: 3px),
51
+ );
52
+
53
+ @mixin _size($name) {
54
+ $s: map.get($_sizes, $name);
55
+
56
+ --md-comp-icon-button-container-height: #{map.get($s, height)};
57
+ --md-comp-icon-button-icon-size: #{map.get($s, icon)};
58
+ --md-comp-icon-button-leading-space: #{map.get($s, space)};
59
+ --md-comp-icon-button-trailing-space: #{map.get($s, space)};
60
+ --md-comp-icon-button-narrow-space: #{map.get($s, narrow)};
61
+ --md-comp-icon-button-wide-space: #{map.get($s, wide)};
62
+ --md-comp-icon-button-container-shape-square: #{map.get($s, square)};
63
+ --md-comp-icon-button-pressed-container-shape: #{map.get($s, pressed)};
64
+ --md-comp-icon-button-outline-width: #{map.get($s, outline)};
65
+ }
66
+
67
+ // Standard is the default style: no container, `on-surface-variant` icon. Every
68
+ // other style is two tokens.
69
+ .icon-button {
70
+ @include _size("small");
71
+
72
+ --md-comp-icon-button-color: var(--md-sys-color-on-surface-variant);
73
+ --md-comp-icon-button-container-color: transparent;
74
+ --md-comp-icon-button-container-shape: 9999px;
75
+
76
+ box-sizing: border-box;
77
+ display: inline-flex;
78
+ flex: 0 0 auto;
79
+ align-items: center;
80
+ justify-content: center;
81
+ height: var(--md-comp-icon-button-container-height);
82
+ padding-block: 0;
83
+ padding-inline: var(--md-comp-icon-button-leading-space)
84
+ var(--md-comp-icon-button-trailing-space);
85
+ border: none;
86
+ border-radius: var(--md-comp-icon-button-container-shape);
87
+ color: var(--md-comp-icon-button-color);
88
+ background-color: var(--md-comp-icon-button-container-color);
89
+ text-decoration: none;
90
+ cursor: pointer;
91
+ outline: 0;
92
+ user-select: none;
93
+ appearance: none;
94
+ vertical-align: middle;
95
+ -webkit-tap-highlight-color: transparent;
96
+ transition: background-color .2s ease-out, border-radius .2s ease-out;
97
+
98
+ > #{$icon} {
99
+ flex: 0 0 auto;
100
+ font-size: var(--md-comp-icon-button-icon-size);
101
+ width: var(--md-comp-icon-button-icon-size);
102
+ height: var(--md-comp-icon-button-icon-size);
103
+ line-height: var(--md-comp-icon-button-icon-size);
104
+ color: inherit;
105
+ overflow: hidden;
106
+ }
107
+
108
+ // The state layer is the icon colour mixed into the container, whatever the
109
+ // style set those two to - `transparent` included, which is how a standard
110
+ // icon button gets a translucent tint instead of an opaque one.
111
+ @each $state, $selector in ("hover": "&:hover", "focus": "&:focus", "pressed": "&:active") {
112
+ #{$selector} {
113
+ background-color: color-mix(
114
+ in oklab,
115
+ var(--md-comp-icon-button-color) state-opacity($state),
116
+ var(--md-comp-icon-button-container-color)
117
+ );
118
+ }
119
+ }
120
+
121
+ @include focus-visible;
122
+ }
123
+
124
+ // Styles. Each is the icon colour and the container it sits on; the state
125
+ // layers above follow from those two.
126
+ .icon-button.filled {
127
+ --md-comp-icon-button-color: var(--md-sys-color-on-primary);
128
+ --md-comp-icon-button-container-color: var(--md-sys-color-primary);
129
+ }
130
+
131
+ .icon-button.tonal {
132
+ --md-comp-icon-button-color: var(--md-sys-color-on-secondary-container);
133
+ --md-comp-icon-button-container-color: var(--md-sys-color-secondary-container);
134
+ }
135
+
136
+ // The outline is a border rather than an inset box-shadow so it survives
137
+ // forced-colors mode, and the inset gives the width back — otherwise a 3dp
138
+ // xlarge outline makes a round button 6px wider than it is tall.
139
+ .icon-button.outlined {
140
+ border: var(--md-comp-icon-button-outline-width) solid var(--md-sys-color-outline-variant);
141
+ padding-inline: calc(var(--md-comp-icon-button-leading-space) - var(--md-comp-icon-button-outline-width))
142
+ calc(var(--md-comp-icon-button-trailing-space) - var(--md-comp-icon-button-outline-width));
143
+ }
144
+
145
+ // Sizes. Default is small (40dp), which is M3's default icon button.
146
+ @each $name in ("xsmall", "medium", "large", "xlarge") {
147
+ .icon-button.#{$name} {
148
+ @include _size($name);
149
+ }
150
+ }
151
+
152
+ // Width axis. Narrow and wide only move the inset, so they compose with every
153
+ // size and with the outlined padding above.
154
+ .icon-button.narrow {
155
+ --md-comp-icon-button-leading-space: var(--md-comp-icon-button-narrow-space);
156
+ --md-comp-icon-button-trailing-space: var(--md-comp-icon-button-narrow-space);
157
+ }
158
+
159
+ .icon-button.wide {
160
+ --md-comp-icon-button-leading-space: var(--md-comp-icon-button-wide-space);
161
+ --md-comp-icon-button-trailing-space: var(--md-comp-icon-button-wide-space);
162
+ }
163
+
164
+ // Shape. Round by default, `.square` for the squared container. The pressed
165
+ // shape is the M3 Expressive corner morph, and it has to be written after
166
+ // `.square`: both are one class plus the root, so a tie is decided by order,
167
+ // and a squared button pressed would otherwise keep its resting corner.
168
+ .icon-button.square {
169
+ --md-comp-icon-button-container-shape: var(--md-comp-icon-button-container-shape-square);
170
+ }
171
+
172
+ .icon-button:active {
173
+ border-radius: var(--md-comp-icon-button-pressed-container-shape);
174
+ }
175
+
176
+ // Disabled. The icon fades to 38% on-surface; a container that exists fades to
177
+ // 10%. Standard and outlined have none, so theirs stays transparent.
178
+ //
179
+ // `aria-disabled="true"` is in the list because an <a> cannot take `disabled`,
180
+ // and it is the attribute that actually tells a screen reader. Note what CSS
181
+ // cannot do for a link: `pointer-events: none` stops the pointer and nothing
182
+ // else, so a disabled-looking <a href> is still reachable by Tab and still
183
+ // navigates on Enter. Drop the `href` (or the link) rather than painting a
184
+ // control that lies about being inert - the docs page says so too.
185
+ .icon-button:is(:disabled, [disabled], [aria-disabled="true"], .disabled) {
186
+ --md-comp-icon-button-color: color-mix(in oklab, var(--md-sys-color-on-surface) 38%, transparent);
187
+
188
+ pointer-events: none;
189
+ cursor: default;
190
+ }
191
+
192
+ .icon-button:is(.filled, .tonal):is(:disabled, [disabled], [aria-disabled="true"], .disabled) {
193
+ --md-comp-icon-button-container-color: color-mix(in oklab, var(--md-sys-color-on-surface) 10%, transparent);
194
+ }
@@ -12,7 +12,7 @@
12
12
  // or by setting --md-icon-font. Fill, weight, grade, and optical size
13
13
  // are axes and inherit as custom properties.
14
14
 
15
- :root {
15
+ :root, :host {
16
16
  --md-icon-font-outlined: "Material Symbols Outlined";
17
17
  --md-icon-font-rounded: "Material Symbols Rounded";
18
18
  --md-icon-font-sharp: "Material Symbols Sharp";
@@ -23,9 +23,12 @@
23
23
  --md-icon-optical-size: 24;
24
24
  }
25
25
 
26
- [icon-style="outlined"] { --md-icon-font: var(--md-icon-font-outlined); }
27
- [icon-style="rounded"] { --md-icon-font: var(--md-icon-font-rounded); }
28
- [icon-style="sharp"] { --md-icon-font: var(--md-icon-font-sharp); }
26
+ [icon-style="outlined"],
27
+ :host([icon-style="outlined"]) { --md-icon-font: var(--md-icon-font-outlined); }
28
+ [icon-style="rounded"],
29
+ :host([icon-style="rounded"]) { --md-icon-font: var(--md-icon-font-rounded); }
30
+ [icon-style="sharp"],
31
+ :host([icon-style="sharp"]) { --md-icon-font: var(--md-icon-font-sharp); }
29
32
 
30
33
  $_icon: ".material-symbols, .material-symbols-outlined, .material-symbols-rounded, .material-symbols-sharp, .material-icons";
31
34
 
@@ -54,6 +57,15 @@ $_icon: ".material-symbols, .material-symbols-outlined, .material-symbols-rounde
54
57
  // Nest so each class in $_icon gets the suffix. `#{$_icon}.tiny` would
55
58
  // only attach `.tiny` to the last item and leave every other icon
56
59
  // matching the bare `.material-symbols` selector — including `.large`.
60
+ // Float helpers, moved off the `i` element selector in base/_global so they
61
+ // survive the canonical <span class="material-symbols"> form.
62
+ &.left {
63
+ float: left;
64
+ margin-left: -8px;
65
+ }
66
+ &.right {
67
+ float: right;
68
+ }
57
69
  &.tiny {
58
70
  font-size: 1rem;
59
71
  --md-icon-optical-size: 20;
@@ -1,10 +1,19 @@
1
1
  // One line per component. Order is cascade order, so keep related widgets
2
2
  // together and append new components at the end unless order matters.
3
3
 
4
+ // Icons first: it is the base glyph default (font-size, line-height) that a
5
+ // component's own icon rule is meant to override. `$icon` interpolates to an
6
+ // :is() carrying classes, so a component rule ties with the base rule on
7
+ // specificity and the winner is decided by source order alone - with icons
8
+ // loaded 4th, `buttons`, `list` and `badges` silently lost that tie. That is
9
+ // why a button icon rendered as an 18px box around a 24px glyph.
10
+ @forward "icons-material-design";
4
11
  @forward "buttons";
12
+ @forward "button-groups";
13
+ @forward "split-button";
14
+ @forward "icon-buttons";
5
15
  @forward "list";
6
16
  @forward "badges";
7
- @forward "icons-material-design";
8
17
  @forward "navbar";
9
18
  @forward "navigation-bar";
10
19
  @forward "navigation-rail";
@@ -16,23 +25,30 @@
16
25
  @forward "tooltip";
17
26
  @forward "menu";
18
27
  @forward "dialog";
28
+ @forward "expanding-card";
19
29
  @forward "bottom-sheet";
20
30
  @forward "side-sheet";
31
+ @forward "floating-sheet";
21
32
  @forward "chips";
22
33
  @forward "lightbox";
23
- @forward "parallax";
24
34
  @forward "docked-display";
25
35
  @forward "page-footer";
26
36
  @forward "forms";
27
37
  @forward "table_of_contents";
28
38
  @forward "scrollspy";
29
- @forward "sidenav";
30
- @forward "preloader";
31
- @forward "slider";
39
+ @forward "navigation-drawer";
40
+ @forward "progress";
32
41
  @forward "carousel";
33
- @forward "pulse";
34
42
  @forward "datepicker";
35
43
  @forward "timepicker";
36
44
  @forward "breadcrumb";
37
45
  @forward "pagination";
38
46
  @forward "panes";
47
+ @forward "fab-menu";
48
+ @forward "loading-indicator";
49
+ @forward "search";
50
+ @forward "segmented-buttons";
51
+ @forward "banners";
52
+ @forward "drag-handle";
53
+ @forward "bottom-app-bar";
54
+ @forward "scrim";
@@ -67,14 +67,14 @@
67
67
  padding: 8px var(--md-comp-list-item-padding);
68
68
  @include typescale("body-large");
69
69
 
70
- > :is(i, img, input):first-child {
70
+ > :is(#{$icon}, img, input):first-child {
71
71
  grid-column: 1;
72
72
  grid-row: 1 / -1;
73
73
  align-self: center;
74
74
  flex-shrink: 0;
75
75
  }
76
76
 
77
- > i:first-child:not(.circle) {
77
+ > #{$icon}:first-child:not(.circle) {
78
78
  font-size: var(--md-comp-list-leading-icon-size);
79
79
  width: var(--md-comp-list-leading-icon-size);
80
80
  height: var(--md-comp-list-leading-icon-size);
@@ -88,14 +88,14 @@
88
88
  border-radius: 8px;
89
89
  }
90
90
 
91
- > :is(img, i).circle {
91
+ > :is(img, #{$icon}).circle {
92
92
  width: var(--md-comp-list-avatar-size);
93
93
  height: var(--md-comp-list-avatar-size);
94
94
  border-radius: 50%;
95
95
  object-fit: cover;
96
96
  }
97
97
 
98
- > i.circle {
98
+ > #{$icon}.circle {
99
99
  display: inline-flex;
100
100
  align-items: center;
101
101
  justify-content: center;
@@ -105,8 +105,11 @@
105
105
  color: var(--md-sys-color-on-primary-container);
106
106
  }
107
107
 
108
+ // $icon-label, not a bare `span`: the canonical icon is a <span> too, so
109
+ // this rule used to pull the leading icon out of column 1 and into the
110
+ // text column - the icon rules above went dead and this one over-matched.
108
111
  > div:not(.meta),
109
- > span {
112
+ > #{$icon-label} {
110
113
  grid-column: 2;
111
114
  min-width: 0;
112
115
  }
@@ -120,14 +123,14 @@
120
123
  @include typescale("body-medium");
121
124
  }
122
125
 
123
- > :is(i, kbd, button, time, .meta, .badge, .button, a):last-child:not(:first-child) {
126
+ > :is(#{$icon}, kbd, button, time, .meta, .badge, .button, a):last-child:not(:first-child) {
124
127
  grid-column: 3;
125
128
  grid-row: 1 / -1;
126
129
  justify-self: end;
127
130
  align-self: center;
128
131
  }
129
132
 
130
- > i:last-child:not(:first-child) {
133
+ > #{$icon}:last-child:not(:first-child) {
131
134
  font-size: var(--md-comp-list-trailing-icon-size);
132
135
  width: var(--md-comp-list-trailing-icon-size);
133
136
  height: var(--md-comp-list-trailing-icon-size);
@@ -146,8 +149,8 @@
146
149
  padding: 0;
147
150
  }
148
151
 
149
- > button:last-child:not(:first-child):not(.button),
150
- > a:last-child:not(:first-child):not(.button):has(> i:only-child) {
152
+ > button:last-child:not(:first-child):not(.button, .icon-button),
153
+ > a:last-child:not(:first-child):not(.button, .icon-button):has(> #{$icon}:only-child) {
151
154
  display: inline-flex;
152
155
  align-items: center;
153
156
  justify-content: center;
@@ -181,7 +184,7 @@
181
184
  > li:not(.active):not([aria-selected="true"]):not([aria-current]):not(:has(> [aria-current])):not(:has([aria-selected="true"])):not(:has(:checked)):hover {
182
185
  background-color: color-mix(
183
186
  in oklab,
184
- var(--md-sys-color-on-surface) 8%,
187
+ var(--md-sys-color-on-surface) state-opacity("hover"),
185
188
  var(--md-comp-list-item-container-color)
186
189
  );
187
190
  }
@@ -195,7 +198,7 @@
195
198
  background-color: var(--md-comp-list-selected-container-color);
196
199
  color: var(--md-comp-list-selected-color);
197
200
 
198
- i,
201
+ #{$icon},
199
202
  p,
200
203
  kbd,
201
204
  time,
@@ -203,8 +206,8 @@
203
206
  color: inherit;
204
207
  }
205
208
 
206
- > i:first-child,
207
- > :is(a, label):only-child > i:first-child {
209
+ > #{$icon}:first-child,
210
+ > :is(a, label):only-child > #{$icon}:first-child {
208
211
  --md-icon-fill: 1;
209
212
  }
210
213
  }
@@ -0,0 +1,106 @@
1
+ @use "sass:list";
2
+ @use "sass:math";
3
+ @use "../abstracts" as *;
4
+
5
+ // Loading indicator. M3 Expressive's indicator for short waits (under ~5s):
6
+ // a shape that morphs while it rotates. One element, CSS-only.
7
+ //
8
+ // <span class="loading-indicator" role="status" aria-label="Loading">
9
+ // <span class="loading-indicator contained" role="status" aria-label="Loading">
10
+ //
11
+ // It supersedes the indeterminate *circular* case of `.progress` — see the
12
+ // docs page. `.progress` keeps every determinate case and both linear bars.
13
+ //
14
+ // Tokens (M3 md.comp.loading-indicator.*):
15
+ // --md-comp-loading-indicator-active-indicator-size 38px
16
+ // --md-comp-loading-indicator-container-size 48px
17
+ // --md-comp-loading-indicator-active-indicator-color primary
18
+ // --md-comp-loading-indicator-container-color transparent
19
+ //
20
+ // `--md-comp-loading-indicator-duration` is ours, not M3's - the spec states
21
+ // the motion, not a duration token.
22
+
23
+ // The morph shapes. M3 cycles seven named shapes (soft burst, four cookies, a
24
+ // pentagon, a pill); these are four cookies plus a circle, which is the part
25
+ // of the vocabulary one formula can generate. M3 draws each shape as a rounded
26
+ // polygon and interpolates vertex by vertex; `clip-path` interpolates the same
27
+ // way, but only between polygons with the same vertex count. So every shape
28
+ // here is 24 points on a polar curve
29
+ // `r = 50% * (1 - depth * (1 - cos(petals * a)) / 2)`: depth 0 is a circle,
30
+ // and 24 divides 4, 6, 8 and 12 so every cusp lands on a vertex.
31
+ // Corners are unrounded — a 24-gon reads as the shape, not as a polygon. The
32
+ // docs call these cookies, which is M3's word for them; `depth: 0` is the
33
+ // circle the cycle opens on, so the function is named for the curve instead.
34
+ $_points: 24;
35
+
36
+ // Two decimals is 0.005% of a 38px box - a fiftieth of a pixel. Full float
37
+ // precision costs ~2KB across the five shapes and buys nothing visible.
38
+ @function _pct($v) {
39
+ @return math.div(math.round($v * 100), 100);
40
+ }
41
+
42
+ @function _petal-shape($petals, $depth) {
43
+ $pts: ();
44
+ @for $i from 0 through $_points - 1 {
45
+ $a: math.div($i * 2 * math.$pi, $_points);
46
+ $r: 50% * (1 - math.div($depth * (1 - math.cos($petals * $a)), 2));
47
+ $pts: list.append($pts, _pct(50% + $r * math.cos($a)) _pct(50% + $r * math.sin($a)), comma);
48
+ }
49
+ @return polygon($pts);
50
+ }
51
+
52
+ .loading-indicator {
53
+ --md-comp-loading-indicator-active-indicator-size: 38px;
54
+ --md-comp-loading-indicator-container-size: 48px;
55
+ --md-comp-loading-indicator-active-indicator-color: var(--md-sys-color-primary);
56
+ --md-comp-loading-indicator-container-color: transparent;
57
+ --md-comp-loading-indicator-duration: 3000ms;
58
+
59
+ box-sizing: border-box;
60
+ display: inline-flex;
61
+ align-items: center;
62
+ justify-content: center;
63
+ width: var(--md-comp-loading-indicator-container-size);
64
+ height: var(--md-comp-loading-indicator-container-size);
65
+ border-radius: 50%;
66
+ background-color: var(--md-comp-loading-indicator-container-color);
67
+ vertical-align: middle;
68
+
69
+ &::before {
70
+ content: "";
71
+ display: block;
72
+ width: var(--md-comp-loading-indicator-active-indicator-size);
73
+ height: var(--md-comp-loading-indicator-active-indicator-size);
74
+ background-color: var(--md-comp-loading-indicator-active-indicator-color);
75
+ clip-path: _petal-shape(0, 0);
76
+ animation:
77
+ loading-indicator-morph var(--md-comp-loading-indicator-duration) ease-in-out infinite,
78
+ loading-indicator-spin var(--md-comp-loading-indicator-duration) linear infinite;
79
+ }
80
+
81
+ // Contained. M3 pairs the container with `on-secondary-container` for the
82
+ // indicator, not `primary` — the container is the louder surface.
83
+ &.contained {
84
+ --md-comp-loading-indicator-container-color: var(--md-sys-color-secondary-container);
85
+ --md-comp-loading-indicator-active-indicator-color: var(--md-sys-color-on-secondary-container);
86
+ }
87
+ }
88
+
89
+ @keyframes loading-indicator-morph {
90
+ 0%,
91
+ 100% { clip-path: _petal-shape(0, 0); }
92
+ 20% { clip-path: _petal-shape(4, 0.32); }
93
+ 40% { clip-path: _petal-shape(6, 0.26); }
94
+ 60% { clip-path: _petal-shape(8, 0.2); }
95
+ 80% { clip-path: _petal-shape(12, 0.14); }
96
+ }
97
+
98
+ @keyframes loading-indicator-spin {
99
+ to { transform: rotate(1turn); }
100
+ }
101
+
102
+ @media (prefers-reduced-motion: reduce) {
103
+ .loading-indicator::before {
104
+ animation: none;
105
+ }
106
+ }