@expressivecss/expressive 0.7.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.
- package/CHANGELOG.md +1313 -1
- package/README.md +76 -30
- package/dist/css/expressive.css +4896 -1777
- package/dist/css/expressive.css.map +1 -1
- package/dist/css/expressive.min.css +1 -1
- package/dist/css/expressive.min.css.map +1 -1
- package/dist/fonts/LICENSE-material-symbols +93 -0
- package/dist/fonts/LICENSE-noto-sans +93 -0
- package/dist/fonts/LICENSE-roboto +93 -0
- package/dist/fonts/material-symbols-outlined.woff2 +0 -0
- package/dist/fonts/material-symbols-rounded.woff2 +0 -0
- package/dist/fonts/material-symbols-sharp.woff2 +0 -0
- package/dist/fonts/noto-sans-latin-400.woff2 +0 -0
- package/dist/fonts/noto-sans-latin-500.woff2 +0 -0
- package/dist/fonts/roboto-latin-400.woff2 +0 -0
- package/dist/fonts/roboto-latin-500.woff2 +0 -0
- package/dist/js/expressive.cjs +1673 -1338
- package/dist/js/expressive.cjs.map +4 -4
- package/dist/js/expressive.js +1673 -1338
- package/dist/js/expressive.js.map +4 -4
- package/dist/js/expressive.min.js +13 -13
- package/dist/js/expressive.min.js.map +4 -4
- package/dist/js/expressive.mjs +1673 -1338
- package/dist/js/expressive.mjs.map +4 -4
- package/dist/types/behaviors/bottomSheets.d.ts +19 -11
- package/dist/types/behaviors/sheetDrag.d.ts +40 -0
- package/dist/types/behaviors/sideSheets.d.ts +2 -9
- package/dist/types/components/appBar.d.ts +26 -0
- package/dist/types/components/autocomplete.d.ts +15 -3
- package/dist/types/components/buttonGroup.d.ts +39 -0
- package/dist/types/components/buttons.d.ts +5 -1
- package/dist/types/components/cards.d.ts +11 -13
- package/dist/types/components/carousel.d.ts +87 -53
- package/dist/types/components/chips.d.ts +21 -1
- package/dist/types/components/expandingCard.d.ts +31 -0
- package/dist/types/components/index.d.ts +23 -20
- package/dist/types/components/lightbox.d.ts +1 -0
- package/dist/types/components/menu.d.ts +11 -1
- package/dist/types/components/{sidenav.d.ts → navigationDrawer.d.ts} +16 -16
- package/dist/types/components/registry.d.ts +18 -8
- package/dist/types/components/slider.d.ts +26 -85
- package/dist/types/components/snackbar.d.ts +20 -5
- package/dist/types/components/tabs.d.ts +12 -0
- package/dist/types/components/timepicker.d.ts +12 -10
- package/dist/types/components/tooltip.d.ts +1 -0
- package/dist/types/core/utils.d.ts +64 -53
- package/dist/types/index.d.ts +8 -9
- package/package.json +39 -3
- package/src/sass/README.md +61 -11
- package/src/sass/abstracts/_breakpoints.scss +17 -19
- package/src/sass/abstracts/_mixins.scss +155 -21
- package/src/sass/abstracts/_variables.scss +69 -5
- package/src/sass/base/_fonts.scss +63 -0
- package/src/sass/base/_global.scss +6 -24
- package/src/sass/base/_grid.scss +15 -12
- package/src/sass/base/_normalize.scss +34 -38
- package/src/sass/base/_typography.scss +4 -18
- package/src/sass/components/_badges.scss +3 -5
- package/src/sass/components/_banners.scss +298 -0
- package/src/sass/components/_bottom-app-bar.scss +180 -0
- package/src/sass/components/_bottom-sheet.scss +12 -4
- package/src/sass/components/_button-groups.scss +358 -0
- package/src/sass/components/_buttons.scss +245 -92
- package/src/sass/components/_cards.scss +514 -78
- package/src/sass/components/_carousel.scss +562 -68
- package/src/sass/components/_chips.scss +64 -5
- package/src/sass/components/_datepicker.scss +24 -24
- package/src/sass/components/_dialog.scss +17 -11
- package/src/sass/components/_drag-handle.scss +130 -0
- package/src/sass/components/_expanding-card.scss +226 -0
- package/src/sass/components/_fab-menu.scss +232 -0
- package/src/sass/components/_floating-sheet.scss +86 -0
- package/src/sass/components/_icon-buttons.scss +172 -0
- package/src/sass/components/_icons-material-design.scss +22 -10
- package/src/sass/components/_index.scss +22 -6
- package/src/sass/components/_list.scss +16 -13
- package/src/sass/components/_loading-indicator.scss +106 -0
- package/src/sass/components/_menu.scss +335 -61
- package/src/sass/components/_navbar.scss +245 -45
- package/src/sass/components/_navigation-bar.scss +22 -11
- package/src/sass/components/{_sidenav.scss → _navigation-drawer.scss} +123 -33
- package/src/sass/components/_navigation-rail.scss +31 -23
- package/src/sass/components/_page-footer.scss +19 -16
- package/src/sass/components/_pagination.scss +24 -6
- package/src/sass/components/_panes.scss +52 -11
- package/src/sass/components/{_preloader.scss → _progress.scss} +11 -3
- package/src/sass/components/_scrim.scss +37 -0
- package/src/sass/components/_search.scss +259 -0
- package/src/sass/components/_segmented-buttons.scss +188 -0
- package/src/sass/components/_side-sheet.scss +2 -2
- package/src/sass/components/_snackbar.scss +20 -15
- package/src/sass/components/_split-button.scss +196 -0
- package/src/sass/components/_tabs.scss +7 -8
- package/src/sass/components/_timepicker.scss +67 -58
- package/src/sass/components/_toolbar.scss +206 -80
- package/src/sass/components/_transitions.scss +8 -1
- package/src/sass/components/forms/_checkboxes.scss +49 -12
- package/src/sass/components/forms/_fieldset.scss +4 -4
- package/src/sass/components/forms/_file-input.scss +8 -1
- package/src/sass/components/forms/_index.scss +1 -1
- package/src/sass/components/forms/_input-fields.scss +50 -44
- package/src/sass/components/forms/_radio-buttons.scss +42 -10
- package/src/sass/components/forms/_select.scss +38 -1
- package/src/sass/components/forms/{_range.scss → _slider.scss} +8 -9
- package/src/sass/components/forms/_switches.scss +6 -7
- package/src/sass/expressive.scss +5 -0
- package/src/sass/tokens/_index.scss +4 -1
- package/src/sass/tokens/_reference.scss +70 -61
- package/src/sass/tokens/_state.scss +24 -0
- package/src/sass/tokens/_theme.scss +6 -3
- package/src/sass/tokens/_vibrant.scss +76 -0
- package/src/sass/utilities/_colors.scss +14 -1
- package/src/sass/utilities/_helpers.scss +5 -5
- package/src/sass/utilities/_visibility.scss +48 -26
- package/dist/types/behaviors/waves.d.ts +0 -15
- package/dist/types/components/parallax.d.ts +0 -16
- package/dist/types/components/range.d.ts +0 -43
- package/dist/types/core/bounding.d.ts +0 -6
- package/dist/types/core/edges.d.ts +0 -6
- package/src/sass/components/_parallax.scss +0 -104
- package/src/sass/components/_pulse.scss +0 -37
- package/src/sass/components/_slider.scss +0 -129
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
@use "../abstracts" as *;
|
|
2
|
+
|
|
3
|
+
// Bottom app bar. A row of *commands* at the bottom edge, with an optional
|
|
4
|
+
// FAB — the counterpart of the top app bar, not of the navigation bar.
|
|
5
|
+
//
|
|
6
|
+
// It is not `nav.navigation-bar`, and the difference is the whole reason this
|
|
7
|
+
// component exists twice over. A navigation bar holds 3–5 destinations that do
|
|
8
|
+
// not change per screen; a bottom app bar holds the actions of the screen you
|
|
9
|
+
// are already on. So this is not a <nav> and takes no landmark (rule 4), and
|
|
10
|
+
// M3 says never show the two at once. The retired version of this component
|
|
11
|
+
// keyed on `footer:has(> nav:only-child)` — an element, which both claimed the
|
|
12
|
+
// landmark and put the two one markup slip apart. The host is a class now, and
|
|
13
|
+
// `semantics.json` forbids writing it on a <nav>.
|
|
14
|
+
//
|
|
15
|
+
// Anatomy is the HTML. Direct <a> / <button> children with a single icon are
|
|
16
|
+
// the actions; a `.button.extra.circle` child is the FAB and is pushed to the
|
|
17
|
+
// end. `.icon-button` styles itself and opts out, as it does in every host.
|
|
18
|
+
//
|
|
19
|
+
// M3 Expressive's toolbar (`.toolbar.docked`) is the newer answer to the same
|
|
20
|
+
// problem and holds the same commands in a 64dp bar. Both ship; this one is
|
|
21
|
+
// the app-bar-shaped 80dp version the spec still publishes tokens for.
|
|
22
|
+
//
|
|
23
|
+
// Tokens (md.comp.bottom-app-bar.*, DSP 34.0.21 — the container is all of it;
|
|
24
|
+
// the actions are icon buttons and the FAB is a FAB):
|
|
25
|
+
// --md-comp-bottom-app-bar-container-color surface-container
|
|
26
|
+
// --md-comp-bottom-app-bar-container-height 80dp, one height with or
|
|
27
|
+
// without a FAB (with-fab's 72dp
|
|
28
|
+
// is deprecated), content centred
|
|
29
|
+
// --md-comp-bottom-app-bar-container-shape 0
|
|
30
|
+
// --md-comp-bottom-app-bar-icon-color on-surface-variant
|
|
31
|
+
// --md-comp-bottom-app-bar-icon-size 24dp
|
|
32
|
+
// --md-comp-bottom-app-bar-action-size 48dp target
|
|
33
|
+
// --md-comp-bottom-app-bar-leading-space 4dp — 16dp to the glyph, less
|
|
34
|
+
// --md-comp-bottom-app-bar-trailing-space the 12dp the target insets
|
|
35
|
+
// --md-comp-bottom-app-bar-fab-container-color secondary-container, per
|
|
36
|
+
// BottomAppBarDefaults, not the
|
|
37
|
+
// free-standing FAB's primary
|
|
38
|
+
// --md-comp-bottom-app-bar-fab-trailing-space 16dp from the container edge,
|
|
39
|
+
// measured from the edge and not
|
|
40
|
+
// added to the container's own
|
|
41
|
+
// trailing space
|
|
42
|
+
// --md-comp-bottom-app-bar-z 997, the navigation bar's rung
|
|
43
|
+
.bottom-app-bar {
|
|
44
|
+
--md-comp-bottom-app-bar-container-color: var(--md-sys-color-surface-container);
|
|
45
|
+
--md-comp-bottom-app-bar-container-height: 80px;
|
|
46
|
+
--md-comp-bottom-app-bar-container-shape: 0;
|
|
47
|
+
--md-comp-bottom-app-bar-icon-color: var(--md-sys-color-on-surface-variant);
|
|
48
|
+
--md-comp-bottom-app-bar-icon-size: 24px;
|
|
49
|
+
--md-comp-bottom-app-bar-action-size: 48px;
|
|
50
|
+
--md-comp-bottom-app-bar-leading-space: 4px;
|
|
51
|
+
--md-comp-bottom-app-bar-trailing-space: 4px;
|
|
52
|
+
--md-comp-bottom-app-bar-fab-container-color: var(--md-sys-color-secondary-container);
|
|
53
|
+
--md-comp-bottom-app-bar-fab-trailing-space: 16px;
|
|
54
|
+
--md-comp-bottom-app-bar-z: 997;
|
|
55
|
+
|
|
56
|
+
box-sizing: border-box;
|
|
57
|
+
display: flex;
|
|
58
|
+
// No gap: the actions are 48dp targets and abut, which is Compose's
|
|
59
|
+
// `Arrangement.Start` and puts their glyph centres 48dp apart. The top app
|
|
60
|
+
// bar spaces its actions the same way.
|
|
61
|
+
align-items: center;
|
|
62
|
+
width: 100%;
|
|
63
|
+
min-height: var(--md-comp-bottom-app-bar-container-height);
|
|
64
|
+
margin: 0;
|
|
65
|
+
padding-inline: var(--md-comp-bottom-app-bar-leading-space)
|
|
66
|
+
var(--md-comp-bottom-app-bar-trailing-space);
|
|
67
|
+
border-radius: var(--md-comp-bottom-app-bar-container-shape);
|
|
68
|
+
background-color: var(--md-comp-bottom-app-bar-container-color);
|
|
69
|
+
color: var(--md-sys-color-on-surface);
|
|
70
|
+
@include z-depth("2");
|
|
71
|
+
|
|
72
|
+
// Icon-only actions. Same shape as the top app bar's: a bare control with a
|
|
73
|
+
// single icon becomes a 48dp target. Anything that styles itself — the
|
|
74
|
+
// common button, the icon button, the FAB — is left alone.
|
|
75
|
+
> :is(a, button):not(.button, .icon-button):not(.circle):not(.extra):has(> #{$icon}:only-child) {
|
|
76
|
+
display: inline-flex;
|
|
77
|
+
flex: 0 0 auto;
|
|
78
|
+
align-items: center;
|
|
79
|
+
justify-content: center;
|
|
80
|
+
width: var(--md-comp-bottom-app-bar-action-size);
|
|
81
|
+
height: var(--md-comp-bottom-app-bar-action-size);
|
|
82
|
+
margin: 0;
|
|
83
|
+
padding: 0;
|
|
84
|
+
border: 0;
|
|
85
|
+
border-radius: 50%;
|
|
86
|
+
background: transparent;
|
|
87
|
+
color: var(--md-comp-bottom-app-bar-icon-color);
|
|
88
|
+
cursor: pointer;
|
|
89
|
+
appearance: none;
|
|
90
|
+
font: inherit;
|
|
91
|
+
text-decoration: none;
|
|
92
|
+
@include focus-visible;
|
|
93
|
+
|
|
94
|
+
> #{$icon} {
|
|
95
|
+
display: block;
|
|
96
|
+
width: var(--md-comp-bottom-app-bar-icon-size);
|
|
97
|
+
height: var(--md-comp-bottom-app-bar-icon-size);
|
|
98
|
+
font-size: var(--md-comp-bottom-app-bar-icon-size);
|
|
99
|
+
line-height: var(--md-comp-bottom-app-bar-icon-size);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
&:hover {
|
|
103
|
+
background-color: color-mix(in oklab, currentColor state-opacity("hover"), transparent);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
&:active {
|
|
107
|
+
background-color: color-mix(in oklab, currentColor state-opacity("pressed"), transparent);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
&:disabled,
|
|
111
|
+
&.disabled {
|
|
112
|
+
pointer-events: none;
|
|
113
|
+
color: color-mix(in oklab, var(--md-sys-color-on-surface) 38%, transparent);
|
|
114
|
+
background: transparent;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// The FAB. `margin-inline-start: auto` is the spacer, so the bar needs no
|
|
119
|
+
// filler element between the actions and it. Flat and secondary-container:
|
|
120
|
+
// the bar is already at level 2, and a second shadow inside it reads as a
|
|
121
|
+
// sticker. Specificity beats the FAB's own `:where()`-scoped block.
|
|
122
|
+
> :is(a, button).extra.circle,
|
|
123
|
+
> :is(a, button).large.circle {
|
|
124
|
+
margin-inline-start: auto;
|
|
125
|
+
// 16dp from the container edge. The bar's own trailing space is already
|
|
126
|
+
// spent, so this is the remainder - Compose's `16.dp - HorizontalPadding`.
|
|
127
|
+
margin-inline-end: calc(
|
|
128
|
+
var(--md-comp-bottom-app-bar-fab-trailing-space) -
|
|
129
|
+
var(--md-comp-bottom-app-bar-trailing-space)
|
|
130
|
+
);
|
|
131
|
+
background-color: var(--md-comp-bottom-app-bar-fab-container-color);
|
|
132
|
+
color: var(--md-sys-color-on-secondary-container);
|
|
133
|
+
box-shadow: none;
|
|
134
|
+
|
|
135
|
+
&:hover {
|
|
136
|
+
box-shadow: none;
|
|
137
|
+
background-color: color-mix(
|
|
138
|
+
in oklab,
|
|
139
|
+
var(--md-sys-color-on-secondary-container) state-opacity("hover"),
|
|
140
|
+
var(--md-comp-bottom-app-bar-fab-container-color)
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Focus and pressed are separate states with separate opacities, the way
|
|
145
|
+
// `abstracts/_mixins` keeps them. `:focus`, not `:focus-visible`: the FAB's
|
|
146
|
+
// own block in `_buttons.scss` paints a `primary-container` tint on plain
|
|
147
|
+
// `:focus`, and leaving that uncovered would flash the wrong colour family
|
|
148
|
+
// at a mouse click. The ring is `focus-visible`'s job and the base rule
|
|
149
|
+
// already carries it.
|
|
150
|
+
&:focus {
|
|
151
|
+
background-color: color-mix(
|
|
152
|
+
in oklab,
|
|
153
|
+
var(--md-sys-color-on-secondary-container) state-opacity("focus"),
|
|
154
|
+
var(--md-comp-bottom-app-bar-fab-container-color)
|
|
155
|
+
);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
&:active {
|
|
159
|
+
background-color: color-mix(
|
|
160
|
+
in oklab,
|
|
161
|
+
var(--md-sys-color-on-secondary-container) state-opacity("pressed"),
|
|
162
|
+
var(--md-comp-bottom-app-bar-fab-container-color)
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Pinned to the bottom edge. Same z rung as nav.navigation-bar.fixed: the two
|
|
169
|
+
// are alternatives, never neighbours, so neither needs to sit above the other.
|
|
170
|
+
.bottom-app-bar.fixed {
|
|
171
|
+
position: fixed;
|
|
172
|
+
inset: auto 0 0 0;
|
|
173
|
+
z-index: var(--md-comp-bottom-app-bar-z);
|
|
174
|
+
// The bar grows by the inset rather than spending its own height on it.
|
|
175
|
+
// Under `border-box` the padding would otherwise come out of the 80dp the
|
|
176
|
+
// token block and the docs both promise, and the actions would sit in
|
|
177
|
+
// whatever was left.
|
|
178
|
+
min-height: calc(var(--md-comp-bottom-app-bar-container-height) + env(safe-area-inset-bottom, 0px));
|
|
179
|
+
padding-bottom: env(safe-area-inset-bottom, 0px);
|
|
180
|
+
}
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
//
|
|
11
11
|
// Tokens follow https://m3.material.io/components/bottom-sheets
|
|
12
12
|
// container surface-container-low, 28dp top corners, elevation 1
|
|
13
|
-
// max width 640dp; 56dp inset from the
|
|
13
|
+
// max width 640dp; 56dp inset from the Medium breakpoint
|
|
14
14
|
// max height viewport minus 72dp
|
|
15
15
|
// handle 32×4dp bar in a 48dp hit target, on-surface-variant
|
|
16
16
|
// scrim modal only (UA ::backdrop + the dialog scrim token)
|
|
@@ -60,7 +60,7 @@ dialog.bottom-sheet {
|
|
|
60
60
|
z-index: 1100;
|
|
61
61
|
}
|
|
62
62
|
|
|
63
|
-
@include bp-up("
|
|
63
|
+
@include bp-up("medium") {
|
|
64
64
|
width: min(
|
|
65
65
|
var(--md-comp-bottom-sheet-container-max-width),
|
|
66
66
|
calc(100% - 2 * var(--md-comp-bottom-sheet-margin-inline))
|
|
@@ -90,12 +90,12 @@ dialog.bottom-sheet {
|
|
|
90
90
|
pointer-events: none;
|
|
91
91
|
}
|
|
92
92
|
|
|
93
|
-
&:has(> .handle)::before {
|
|
93
|
+
&:has(> :is(.handle, .drag-handle))::before {
|
|
94
94
|
content: none;
|
|
95
95
|
display: none;
|
|
96
96
|
}
|
|
97
97
|
|
|
98
|
-
> .handle {
|
|
98
|
+
> :is(.handle, .drag-handle) {
|
|
99
99
|
box-sizing: border-box;
|
|
100
100
|
flex: 0 0 48px;
|
|
101
101
|
align-self: center;
|
|
@@ -111,6 +111,14 @@ dialog.bottom-sheet {
|
|
|
111
111
|
appearance: none;
|
|
112
112
|
}
|
|
113
113
|
|
|
114
|
+
// The sheet paints its own bar with a mask on the element, so the vertical
|
|
115
|
+
// bar _drag-handle.scss draws in ::before would print across it. This is the
|
|
116
|
+
// whole cost of one class serving both hosts, and it is cheaper than a
|
|
117
|
+
// `horizontal` modifier whose values M3 does not publish.
|
|
118
|
+
> .drag-handle::before {
|
|
119
|
+
content: none;
|
|
120
|
+
}
|
|
121
|
+
|
|
114
122
|
> :is(h1, h2, h3, h4, h5, h6) {
|
|
115
123
|
padding-top: 8px;
|
|
116
124
|
}
|
|
@@ -0,0 +1,358 @@
|
|
|
1
|
+
@use "sass:map";
|
|
2
|
+
@use "../abstracts" as *;
|
|
3
|
+
|
|
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.
|
|
8
|
+
//
|
|
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.
|
|
15
|
+
//
|
|
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))";
|
|
24
|
+
|
|
25
|
+
// The size classes, as one `:not()` argument list - which takes the weight of
|
|
26
|
+
// a single class, not five. An item carrying its own size class sets the
|
|
27
|
+
// geometry tokens on itself and beats the group by inheritance; the type role
|
|
28
|
+
// has no such mechanism, so the group's rule has to step aside by selector or
|
|
29
|
+
// an `xlarge` item inside a `medium` group gets xlarge geometry and medium
|
|
30
|
+
// type.
|
|
31
|
+
$_sized: ".xsmall, .small, .medium, .large, .xlarge";
|
|
32
|
+
|
|
33
|
+
// md.comp.button-group.standard.<size>.between-space
|
|
34
|
+
$_standard-space: (
|
|
35
|
+
"xsmall": 18px,
|
|
36
|
+
"small": 12px,
|
|
37
|
+
"medium": 8px,
|
|
38
|
+
"large": 8px,
|
|
39
|
+
"xlarge": 8px,
|
|
40
|
+
);
|
|
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
|
+
|
|
53
|
+
// md.comp.button-group.connected.<size>.{inner-corner, pressed.inner-corner}.
|
|
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.
|
|
56
|
+
$_connected-corners: (
|
|
57
|
+
"xsmall": (inner: 4px, pressed: 4px),
|
|
58
|
+
"small": (inner: 8px, pressed: 4px),
|
|
59
|
+
"medium": (inner: 8px, pressed: 4px),
|
|
60
|
+
"large": (inner: 16px, pressed: 12px),
|
|
61
|
+
"xlarge": (inner: 20px, pressed: 16px),
|
|
62
|
+
);
|
|
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
|
+
|
|
72
|
+
.button-group {
|
|
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;
|
|
77
|
+
// M3 states this as a percentage of the item's own width. CSS cannot
|
|
78
|
+
// multiply a length by one, so it is the same figure as a ratio.
|
|
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
|
+
);
|
|
88
|
+
|
|
89
|
+
display: flex;
|
|
90
|
+
align-items: center;
|
|
91
|
+
gap: var(--md-comp-button-group-between-space);
|
|
92
|
+
|
|
93
|
+
// The size axis. A group sizes its own items by rewriting the button
|
|
94
|
+
// tokens, which the items inherit - so a size is written once, on the
|
|
95
|
+
// group, and a button that wants to differ still overrides it with its own
|
|
96
|
+
// size class. The type role cannot travel that way: `btn` states a
|
|
97
|
+
// font-size of its own, so it is set on the items.
|
|
98
|
+
@each $name, $s in $button-sizes {
|
|
99
|
+
&.#{$name} {
|
|
100
|
+
@include button-size($name);
|
|
101
|
+
|
|
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)};
|
|
105
|
+
|
|
106
|
+
> #{$_label-item}:not(#{$_sized}) {
|
|
107
|
+
@include typescale(map.get($s, label));
|
|
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)};
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
> #{$_item} {
|
|
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)
|
|
244
|
+
);
|
|
245
|
+
}
|
|
246
|
+
|
|
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
|
+
);
|
|
253
|
+
}
|
|
254
|
+
|
|
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)
|
|
260
|
+
);
|
|
261
|
+
}
|
|
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
|
+
|
|
292
|
+
// Connected. Emitted after the size rules on purpose: `.button-group.medium`
|
|
293
|
+
// and `.button-group.connected` are both two classes, so source order is the
|
|
294
|
+
// only thing that keeps a connected group at 2dp.
|
|
295
|
+
&.connected {
|
|
296
|
+
--md-comp-button-group-between-space: 2px;
|
|
297
|
+
--md-comp-button-group-container-shape: 9999px;
|
|
298
|
+
--md-comp-button-group-inner-corner-corner-size: 8px;
|
|
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%;
|
|
303
|
+
|
|
304
|
+
@each $name, $c in $_connected-corners {
|
|
305
|
+
&.#{$name} {
|
|
306
|
+
--md-comp-button-group-inner-corner-corner-size: #{map.get($c, inner)};
|
|
307
|
+
--md-comp-button-group-pressed-inner-corner-corner-size: #{map.get($c, pressed)};
|
|
308
|
+
}
|
|
309
|
+
}
|
|
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
|
+
|
|
319
|
+
// The corner goes through a variable so that pressing an item changes one
|
|
320
|
+
// value rather than restating `border-radius`. A shorthand for the pressed
|
|
321
|
+
// state would take the end items' round outer corners with it.
|
|
322
|
+
> #{$_item} {
|
|
323
|
+
--_corner: var(--md-comp-button-group-inner-corner-corner-size);
|
|
324
|
+
--_outer-corner: var(--md-comp-button-group-container-shape);
|
|
325
|
+
|
|
326
|
+
flex: 1 1 0;
|
|
327
|
+
border-radius: var(--_corner);
|
|
328
|
+
|
|
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) {
|
|
335
|
+
--_corner: var(--md-comp-button-group-pressed-inner-corner-corner-size);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// Logical, so the ends of the row are the ends of the row in RTL too.
|
|
339
|
+
&:first-child {
|
|
340
|
+
border-start-start-radius: var(--_outer-corner);
|
|
341
|
+
border-end-start-radius: var(--_outer-corner);
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
&:last-child {
|
|
345
|
+
border-start-end-radius: var(--_outer-corner);
|
|
346
|
+
border-end-end-radius: var(--_outer-corner);
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
// The press grow and the corner morph are both motion, and the only motion
|
|
353
|
+
// this component has.
|
|
354
|
+
@media (prefers-reduced-motion: reduce) {
|
|
355
|
+
.button-group > #{$_item} {
|
|
356
|
+
transition: none;
|
|
357
|
+
}
|
|
358
|
+
}
|