@expressivecss/expressive 0.8.0 → 0.9.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 (50) hide show
  1. package/CHANGELOG.md +48 -1
  2. package/README.md +69 -26
  3. package/dist/css/expressive.css +657 -85
  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/fonts/LICENSE-material-symbols +93 -0
  8. package/dist/fonts/LICENSE-noto-sans +93 -0
  9. package/dist/fonts/LICENSE-roboto +93 -0
  10. package/dist/fonts/material-symbols-outlined.woff2 +0 -0
  11. package/dist/fonts/material-symbols-rounded.woff2 +0 -0
  12. package/dist/fonts/material-symbols-sharp.woff2 +0 -0
  13. package/dist/fonts/noto-sans-latin-400.woff2 +0 -0
  14. package/dist/fonts/noto-sans-latin-500.woff2 +0 -0
  15. package/dist/fonts/roboto-latin-400.woff2 +0 -0
  16. package/dist/fonts/roboto-latin-500.woff2 +0 -0
  17. package/dist/js/expressive.cjs +501 -144
  18. package/dist/js/expressive.cjs.map +4 -4
  19. package/dist/js/expressive.js +501 -144
  20. package/dist/js/expressive.js.map +4 -4
  21. package/dist/js/expressive.min.js +10 -10
  22. package/dist/js/expressive.min.js.map +4 -4
  23. package/dist/js/expressive.mjs +501 -144
  24. package/dist/js/expressive.mjs.map +4 -4
  25. package/dist/types/components/appBar.d.ts +26 -0
  26. package/dist/types/components/buttonGroup.d.ts +39 -0
  27. package/dist/types/components/cards.d.ts +11 -13
  28. package/dist/types/components/expandingCard.d.ts +1 -1
  29. package/dist/types/components/index.d.ts +2 -0
  30. package/dist/types/components/menu.d.ts +4 -0
  31. package/dist/types/components/registry.d.ts +10 -0
  32. package/dist/types/components/timepicker.d.ts +0 -1
  33. package/dist/types/components/tooltip.d.ts +1 -0
  34. package/dist/types/index.d.ts +1 -1
  35. package/package.json +25 -3
  36. package/src/sass/abstracts/_mixins.scss +52 -0
  37. package/src/sass/abstracts/_variables.scss +19 -0
  38. package/src/sass/base/_fonts.scss +63 -0
  39. package/src/sass/components/_badges.scss +1 -2
  40. package/src/sass/components/_button-groups.scss +252 -80
  41. package/src/sass/components/_cards.scss +108 -93
  42. package/src/sass/components/_expanding-card.scss +1 -35
  43. package/src/sass/components/_icon-buttons.scss +2 -24
  44. package/src/sass/components/_icons-material-design.scss +6 -6
  45. package/src/sass/components/_navbar.scss +180 -27
  46. package/src/sass/components/_navigation-bar.scss +9 -1
  47. package/src/sass/components/_navigation-rail.scss +9 -1
  48. package/src/sass/components/_transitions.scss +8 -1
  49. package/src/sass/components/forms/_select.scss +38 -0
  50. package/src/sass/expressive.scss +5 -0
@@ -1,52 +1,26 @@
1
1
  @use "sass:map";
2
2
  @use "../abstracts" as *;
3
3
 
4
- // Button groups.
4
+ // Material 3 Expressive button groups: standard and connected, each across
5
+ // xsmall through xlarge. Direct children can be common buttons or icon
6
+ // buttons. A size on the group propagates to children unless a child has an
7
+ // explicit size of its own.
5
8
  //
6
- // Anatomy is the HTML. A <div class="button-group"> holding two or more
7
- // buttons - related actions that read as one control without becoming one.
8
- // The group is a container of independent buttons: each is its own Tab stop,
9
- // each does its own thing, and nothing here is scripted.
9
+ // ButtonGroup supplies standard-group width redistribution for every group.
10
+ // Toggle groups opt into managed selection with `data-selection="single|multiple"`;
11
+ // groups without it retain native action-button semantics. Native buttons keep
12
+ // their normal Tab, Space, and Enter behavior. The container may use
13
+ // `role="group"` and an accessible name, but never a composite role such as
14
+ // `toolbar` because this component has no roving-focus or arrow-key contract.
10
15
  //
11
- // No composite role is declared. `role="toolbar"` and its nine siblings
12
- // promise arrow-key navigation this component does not implement and does not
13
- // want to - SEMANTICS rule 2. A group that needs a name takes `role="group"`
14
- // with an `aria-label`, which promises no keyboard model at all.
15
- //
16
- // Two variants and five sizes, which M3 states as ten token families
17
- // (md.comp.button-group.{standard,connected}.{xsmall…xlarge}, DSP 34.0.21).
18
- // They collapse to one family here the way the button's own ladder does: the
19
- // size class rewrites the tokens, and only the numbers that actually differ
20
- // are named twice.
21
- //
22
- // standard items keep their own round shape; the gap closes as the
23
- // buttons grow (18 / 12 / 8 / 8 / 8dp), and pressing one widens
24
- // it by 15% of its width
25
- // connected items sit 2dp apart at every size, the ends of the row are
26
- // round and the joins are square-ish, and pressing an item
27
- // squares its inner corners further
28
- //
29
- // Selection is deliberately absent. M3's connected group has a selected state
30
- // with fully round corners, and holding it takes either script or a checked
31
- // control - which is `.segmented-button`, a <fieldset> of radios that already
32
- // does exactly that. A group of <button>s has no state to read.
33
-
34
- // The items: common buttons, and `.circle` for an icon-only one - which is
35
- // the shape M3 draws a group with most often, and which the group sizes like
36
- // any other item.
37
- //
38
- // `.icon-button` is deliberately *not* an item. It is its own component with
39
- // its own size ladder, its own insets and its own pressed shape, all set on
40
- // the element by its own rule - so a group could not size it by handing its
41
- // items tokens, and the corner morph here would fight the one it already
42
- // draws. Write `<button class="button circle">` for an icon-only group.
43
- //
44
- // This is also not `_buttons.scss`'s `$_btn`, which excludes the widget chrome
45
- // that happens to be a <button> (a datepicker day, a chip). Chrome cannot
46
- // reach a group anyway: `button-group-items-are-controls` in semantics.json
47
- // admits only a <button> or an <a href> as a direct child, so the buttons
48
- // here are the ones an author put here.
49
- $_item: ":is(button:not(.icon-button), a.button)";
16
+ // Standard groups keep separate items and grow/reshape the pressed one.
17
+ // Connected groups fill their available width, divide it evenly, keep a 2dp
18
+ // seam, and morph the shared corners. Toggle selection changes color, shape,
19
+ // and icon fill so color is never the only cue.
20
+ $_item: ":is(button, a.button)";
21
+ $_label-item: ":is(button:not(.icon-button), a.button)";
22
+ $_filled-item: ":where(.filled, :not(.elevated, .filled, .tonal, .outlined, .text))";
23
+ $_toggle-item: ":is(button:not(.icon-button):not(.text), a.button:not(.text), .icon-button:is(.filled, .tonal, .outlined))";
50
24
 
51
25
  // The size classes, as one `:not()` argument list - which takes the weight of
52
26
  // a single class, not five. An item carrying its own size class sets the
@@ -65,19 +39,52 @@ $_standard-space: (
65
39
  "xlarge": 8px,
66
40
  );
67
41
 
42
+ // md.comp.button.<size>.{container-shape-square, pressed-container-shape}.
43
+ // Common buttons and icon buttons share these numbers, so the group can morph
44
+ // either kind of child through one pair of properties.
45
+ $_item-shapes: (
46
+ "xsmall": (square: 12px, pressed: 8px),
47
+ "small": (square: 12px, pressed: 8px),
48
+ "medium": (square: 16px, pressed: 12px),
49
+ "large": (square: 28px, pressed: 16px),
50
+ "xlarge": (square: 28px, pressed: 16px),
51
+ );
52
+
68
53
  // md.comp.button-group.connected.<size>.{inner-corner, pressed.inner-corner}.
69
- // The three small sizes share corner.value.small / corner.value.extra-small,
70
- // which is the group's default, so only the two that differ are listed.
54
+ // The live M3 measurement is authoritative for XS (4px). Material Web 34.0.21
55
+ // still emits 8px there; S and M agree at 8px.
71
56
  $_connected-corners: (
57
+ "xsmall": (inner: 4px, pressed: 4px),
58
+ "small": (inner: 8px, pressed: 4px),
59
+ "medium": (inner: 8px, pressed: 4px),
72
60
  "large": (inner: 16px, pressed: 12px),
73
61
  "xlarge": (inner: 20px, pressed: 16px),
74
62
  );
75
63
 
64
+ @mixin _disabled-toggle {
65
+ @include z-depth("0");
66
+ color: color-mix(in oklab, var(--md-sys-color-on-surface) 38%, transparent);
67
+ pointer-events: none;
68
+ box-shadow: none;
69
+ cursor: default;
70
+ }
71
+
76
72
  .button-group {
77
73
  --md-comp-button-group-between-space: 12px;
74
+ --md-comp-button-group-item-shape-round: 9999px;
75
+ --md-comp-button-group-item-shape-square: 12px;
76
+ --md-comp-button-group-pressed-item-shape: 8px;
78
77
  // M3 states this as a percentage of the item's own width. CSS cannot
79
78
  // multiply a length by one, so it is the same figure as a ratio.
80
79
  --md-comp-button-group-pressed-item-width-multiplier: .15;
80
+ // Material's fast-spatial spring (damping .9, stiffness 1400), sampled into
81
+ // a CSS linear easing so width and corner motion use the published dynamics.
82
+ --md-comp-button-group-motion: linear(
83
+ 0, .056 5%, .1796 10%, .3252 15%, .4675 20%, .5936 25%,
84
+ .6988 30%, .7828 35%, .8475 40%, .8959 45%, .931 50%,
85
+ .9559 55%, .973 60%, .9845 65%, .9919 70%, .9965 75%,
86
+ .9992 80%, 1.0006 85%, 1.0013 90%, 1.0015 95%, 1.0015
87
+ );
81
88
 
82
89
  display: flex;
83
90
  align-items: center;
@@ -93,48 +100,195 @@ $_connected-corners: (
93
100
  @include button-size($name);
94
101
 
95
102
  --md-comp-button-group-between-space: #{map.get($_standard-space, $name)};
103
+ --md-comp-button-group-item-shape-square: #{map.get(map.get($_item-shapes, $name), square)};
104
+ --md-comp-button-group-pressed-item-shape: #{map.get(map.get($_item-shapes, $name), pressed)};
96
105
 
97
- > #{$_item}:not(#{$_sized}) {
106
+ > #{$_label-item}:not(#{$_sized}) {
98
107
  @include typescale(map.get($s, label));
99
108
  }
109
+
110
+ > .icon-button:not(#{$_sized}) {
111
+ @include icon-button-size($name);
112
+ }
113
+
114
+ > .icon-button.narrow:not(#{$_sized}) {
115
+ --md-comp-icon-button-leading-space: #{map.get(map.get($icon-button-sizes, $name), narrow)};
116
+ --md-comp-icon-button-trailing-space: #{map.get(map.get($icon-button-sizes, $name), narrow)};
117
+ }
118
+
119
+ > .icon-button.wide:not(#{$_sized}) {
120
+ --md-comp-icon-button-leading-space: #{map.get(map.get($icon-button-sizes, $name), wide)};
121
+ --md-comp-icon-button-trailing-space: #{map.get(map.get($icon-button-sizes, $name), wide)};
122
+ }
123
+ }
124
+
125
+ // Group shape tokens inherit, so an explicitly sized item must rewrite
126
+ // them on itself just as its button geometry tokens do.
127
+ > #{$_item}.#{$name} {
128
+ --md-comp-button-group-item-shape-square: #{map.get(map.get($_item-shapes, $name), square)};
129
+ --md-comp-button-group-pressed-item-shape: #{map.get(map.get($_item-shapes, $name), pressed)};
130
+ --md-comp-button-group-inner-corner-corner-size: #{map.get(map.get($_connected-corners, $name), inner)};
131
+ --md-comp-button-group-pressed-inner-corner-corner-size: #{map.get(map.get($_connected-corners, $name), pressed)};
100
132
  }
101
133
  }
102
134
 
103
135
  > #{$_item} {
104
- transition: padding .2s ease-out, width .2s ease-out,
105
- border-radius .2s ease-out, background-color .2s ease-out;
106
- }
107
-
108
- // Pressing an item in a standard group widens it. M3 compresses the two
109
- // neighbours by the same amount so the group's own width never changes;
110
- // that needs each item's measured width, which is script.
111
- //
112
- // ponytail: the grow is 15% of the item's *height* - exact for a `.circle`,
113
- // whose width is its height, and close for a label - and the items after it
114
- // slide rather than compress. Measure and compress in JS if a group ever
115
- // sits in a fixed-width row where the slide is visible.
116
- &:not(.connected) > #{$_item}:active {
117
- --_grow: calc(
118
- var(--md-comp-filled-button-container-height) *
119
- var(--md-comp-button-group-pressed-item-width-multiplier) / 2
136
+ position: relative;
137
+ transition:
138
+ width 200ms var(--md-comp-button-group-motion),
139
+ border-radius 200ms var(--md-comp-button-group-motion),
140
+ background-color 200ms ease-out;
141
+ }
142
+
143
+ // Every item owns a centered 48dp minimum hit target. For M/L/XL this is a
144
+ // no-op because the visible container already exceeds it; the unconditional
145
+ // rule also covers a default-small group and explicit XS/S child overrides.
146
+ > #{$_item}::before {
147
+ content: "";
148
+ position: absolute;
149
+ top: 50%;
150
+ left: 50%;
151
+ width: 100%;
152
+ height: 100%;
153
+ min-width: 48px;
154
+ min-height: 48px;
155
+ translate: -50% -50%;
156
+ }
157
+
158
+ &:not(.connected) > #{$_item} {
159
+ --_corner: var(--md-comp-button-group-item-shape-round);
160
+ border-radius: var(--_corner);
161
+ }
162
+
163
+ &.square:not(.connected) > #{$_item} {
164
+ --_corner: var(--md-comp-button-group-item-shape-square);
165
+ }
166
+
167
+ &:not(.connected) > #{$_item}[aria-pressed=true] {
168
+ --_corner: var(--md-comp-button-group-item-shape-square);
169
+ }
170
+
171
+ &.square:not(.connected) > #{$_item}[aria-pressed=true] {
172
+ --_corner: var(--md-comp-button-group-item-shape-round);
173
+ }
174
+
175
+ // Toggle-button colors come from md.comp.button.<style>.{unselected,selected}.
176
+ // Shape and the filled icon below provide two non-color selection cues.
177
+ > #{$_label-item}#{$_filled-item}[aria-pressed=false],
178
+ > .icon-button.filled[aria-pressed=false] {
179
+ --_toggle-container-color: var(--md-sys-color-surface-container);
180
+ --_toggle-state-color: var(--md-sys-color-on-surface-variant);
181
+ color: var(--md-sys-color-on-surface-variant);
182
+ background-color: var(--_toggle-container-color);
183
+ }
184
+
185
+ > #{$_label-item}#{$_filled-item}[aria-pressed=true],
186
+ > .icon-button.filled[aria-pressed=true] {
187
+ --_toggle-container-color: var(--md-sys-color-primary);
188
+ --_toggle-state-color: var(--md-sys-color-on-primary);
189
+ color: var(--md-sys-color-on-primary);
190
+ background-color: var(--_toggle-container-color);
191
+ }
192
+
193
+ > #{$_label-item}.tonal[aria-pressed=false],
194
+ > .icon-button.tonal[aria-pressed=false] {
195
+ --_toggle-container-color: var(--md-sys-color-secondary-container);
196
+ --_toggle-state-color: var(--md-sys-color-on-secondary-container);
197
+ color: var(--md-sys-color-on-secondary-container);
198
+ background-color: var(--_toggle-container-color);
199
+ }
200
+
201
+ > #{$_label-item}.tonal[aria-pressed=true],
202
+ > .icon-button.tonal[aria-pressed=true] {
203
+ --_toggle-container-color: var(--md-sys-color-secondary);
204
+ --_toggle-state-color: var(--md-sys-color-on-secondary);
205
+ color: var(--md-sys-color-on-secondary);
206
+ background-color: var(--_toggle-container-color);
207
+ }
208
+
209
+ > #{$_label-item}.outlined[aria-pressed=false],
210
+ > .icon-button.outlined[aria-pressed=false] {
211
+ --_toggle-container-color: transparent;
212
+ --_toggle-state-color: var(--md-sys-color-on-surface-variant);
213
+ color: var(--md-sys-color-on-surface-variant);
214
+ background-color: var(--_toggle-container-color);
215
+ }
216
+
217
+ > #{$_label-item}.outlined[aria-pressed=true],
218
+ > .icon-button.outlined[aria-pressed=true] {
219
+ --_toggle-container-color: var(--md-sys-color-inverse-surface);
220
+ --_toggle-state-color: var(--md-sys-color-inverse-on-surface);
221
+ color: var(--md-sys-color-inverse-on-surface);
222
+ background-color: var(--_toggle-container-color);
223
+ }
224
+
225
+ > #{$_label-item}.elevated[aria-pressed=false] {
226
+ --_toggle-container-color: var(--md-sys-color-surface-container-low);
227
+ --_toggle-state-color: var(--md-sys-color-primary);
228
+ color: var(--md-sys-color-primary);
229
+ background-color: var(--_toggle-container-color);
230
+ }
231
+
232
+ > #{$_label-item}.elevated[aria-pressed=true] {
233
+ --_toggle-container-color: var(--md-sys-color-primary);
234
+ --_toggle-state-color: var(--md-sys-color-on-primary);
235
+ color: var(--md-sys-color-on-primary);
236
+ background-color: var(--_toggle-container-color);
237
+ }
238
+
239
+ > #{$_toggle-item}[aria-pressed]:hover {
240
+ background-color: color-mix(
241
+ in oklab,
242
+ var(--_toggle-state-color) state-opacity("hover"),
243
+ var(--_toggle-container-color)
120
244
  );
121
245
  }
122
246
 
123
- // A label button is as wide as its insets make it, so it grows by them.
124
- &:not(.connected) > #{$_item}:not(.circle):active {
125
- padding-inline:
126
- calc(var(--md-comp-filled-button-leading-space) + var(--_grow))
127
- calc(var(--md-comp-filled-button-trailing-space) + var(--_grow));
247
+ > #{$_toggle-item}[aria-pressed]:focus {
248
+ background-color: color-mix(
249
+ in oklab,
250
+ var(--_toggle-state-color) state-opacity("focus"),
251
+ var(--_toggle-container-color)
252
+ );
128
253
  }
129
254
 
130
- // A `.circle` states its own width and has no insets to grow - padding would
131
- // squeeze the icon inside the same box rather than widen it.
132
- &:not(.connected) > #{$_item}.circle:active {
133
- width: calc(
134
- var(--md-comp-filled-button-container-height) + 2 * var(--_grow)
255
+ > #{$_toggle-item}[aria-pressed]:is(:active, .button-group-pressed) {
256
+ background-color: color-mix(
257
+ in oklab,
258
+ var(--_toggle-state-color) state-opacity("pressed"),
259
+ var(--_toggle-container-color)
135
260
  );
136
261
  }
137
262
 
263
+ > #{$_label-item}#{$_filled-item}[aria-pressed]:is(:disabled, [disabled], .disabled),
264
+ > #{$_label-item}:is(.tonal, .elevated)[aria-pressed]:is(:disabled, [disabled], .disabled),
265
+ > .icon-button:is(.filled, .tonal)[aria-pressed]:is(:disabled, [disabled], .disabled),
266
+ > #{$_label-item}.outlined[aria-pressed=true]:is(:disabled, [disabled], .disabled),
267
+ > .icon-button.outlined[aria-pressed=true]:is(:disabled, [disabled], .disabled) {
268
+ @include _disabled-toggle;
269
+ background-color: color-mix(in oklab, var(--md-sys-color-on-surface) 10%, transparent);
270
+ border-color: transparent;
271
+ }
272
+
273
+ > #{$_label-item}.outlined[aria-pressed=false]:is(:disabled, [disabled], .disabled),
274
+ > .icon-button.outlined[aria-pressed=false]:is(:disabled, [disabled], .disabled) {
275
+ @include _disabled-toggle;
276
+ background-color: transparent;
277
+ border-color: var(--md-sys-color-outline-variant);
278
+ }
279
+
280
+ > #{$_item}[aria-pressed=true] > #{$icon} {
281
+ --md-icon-fill: 1;
282
+ }
283
+
284
+ // ButtonGroup measures the pressed item and takes its 15% growth from the
285
+ // immediate neighbors, keeping the standard group's total width stable.
286
+ // CSS owns only the simultaneous shape change.
287
+ &:not(.connected) > #{$_item}:is(:active, .button-group-pressed),
288
+ &.square:not(.connected) > #{$_item}[aria-pressed=true]:is(:active, .button-group-pressed) {
289
+ --_corner: var(--md-comp-button-group-pressed-item-shape);
290
+ }
291
+
138
292
  // Connected. Emitted after the size rules on purpose: `.button-group.medium`
139
293
  // and `.button-group.connected` are both two classes, so source order is the
140
294
  // only thing that keeps a connected group at 2dp.
@@ -143,6 +297,9 @@ $_connected-corners: (
143
297
  --md-comp-button-group-container-shape: 9999px;
144
298
  --md-comp-button-group-inner-corner-corner-size: 8px;
145
299
  --md-comp-button-group-pressed-inner-corner-corner-size: 4px;
300
+ --md-comp-button-group-selected-inner-corner-corner-size: 50%;
301
+
302
+ width: 100%;
146
303
 
147
304
  @each $name, $c in $_connected-corners {
148
305
  &.#{$name} {
@@ -151,27 +308,42 @@ $_connected-corners: (
151
308
  }
152
309
  }
153
310
 
311
+ &.square {
312
+ --md-comp-button-group-container-shape: var(--md-comp-button-group-inner-corner-corner-size);
313
+ }
314
+
315
+ > #{$_item} {
316
+ min-width: 48px;
317
+ }
318
+
154
319
  // The corner goes through a variable so that pressing an item changes one
155
- // value rather than restating `border-radius`. A shorthand under `:active`
156
- // would take the end items' round outer corners with it.
320
+ // value rather than restating `border-radius`. A shorthand for the pressed
321
+ // state would take the end items' round outer corners with it.
157
322
  > #{$_item} {
158
323
  --_corner: var(--md-comp-button-group-inner-corner-corner-size);
324
+ --_outer-corner: var(--md-comp-button-group-container-shape);
159
325
 
326
+ flex: 1 1 0;
160
327
  border-radius: var(--_corner);
161
328
 
162
- &:active {
329
+ &[aria-pressed=true] {
330
+ --_corner: var(--md-comp-button-group-selected-inner-corner-corner-size);
331
+ --_outer-corner: var(--md-comp-button-group-selected-inner-corner-corner-size);
332
+ }
333
+
334
+ &:is(:active, .button-group-pressed) {
163
335
  --_corner: var(--md-comp-button-group-pressed-inner-corner-corner-size);
164
336
  }
165
337
 
166
338
  // Logical, so the ends of the row are the ends of the row in RTL too.
167
339
  &:first-child {
168
- border-start-start-radius: var(--md-comp-button-group-container-shape);
169
- border-end-start-radius: var(--md-comp-button-group-container-shape);
340
+ border-start-start-radius: var(--_outer-corner);
341
+ border-end-start-radius: var(--_outer-corner);
170
342
  }
171
343
 
172
344
  &:last-child {
173
- border-start-end-radius: var(--md-comp-button-group-container-shape);
174
- border-end-end-radius: var(--md-comp-button-group-container-shape);
345
+ border-start-end-radius: var(--_outer-corner);
346
+ border-end-end-radius: var(--_outer-corner);
175
347
  }
176
348
  }
177
349
  }