@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
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
//
|
|
8
8
|
// Canonical Layouts:
|
|
9
9
|
// 1. List-Detail (.list-detail, .panes.list-detail):
|
|
10
|
-
// - Compact (< 840px
|
|
10
|
+
// - Compact and Medium (< 840px): Single pane visible at a time.
|
|
11
11
|
// - Expanded (>= 840px): 360dp list pane + 1fr detail pane.
|
|
12
12
|
// 2. Supporting Pane (.supporting-pane-layout, .panes.supporting):
|
|
13
13
|
// - 1fr primary pane + 360-400dp supporting pane (on trailing or leading side).
|
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
// with surface-container-low and a 24dp gap.
|
|
21
21
|
//
|
|
22
22
|
// Tokens follow https://m3.material.io/foundations/adaptive-design/canonical-layouts
|
|
23
|
+
// --md-comp-pane-margin 16px Compact / 24px Medium+
|
|
23
24
|
// --md-comp-pane-gap 24px
|
|
24
25
|
// --md-comp-pane-divider-color outline-variant
|
|
25
26
|
// --md-comp-pane-container-color surface
|
|
@@ -51,10 +52,25 @@
|
|
|
51
52
|
background-color: var(--md-comp-pane-container-color);
|
|
52
53
|
color: var(--md-sys-color-on-surface);
|
|
53
54
|
|
|
54
|
-
// Default
|
|
55
|
+
// Default: single pane on Compact and Medium windows (< 840px).
|
|
55
56
|
grid-template-columns: 1fr;
|
|
56
57
|
gap: 0;
|
|
57
58
|
|
|
59
|
+
// M3 layout margins are the inline space between content and the window
|
|
60
|
+
// edges. Keep the full-height pane model and inset layouts below Large.
|
|
61
|
+
@include bp-down("medium") {
|
|
62
|
+
--md-comp-pane-margin: 16px;
|
|
63
|
+
width: calc(100% - (2 * var(--md-comp-pane-margin)));
|
|
64
|
+
margin-inline: var(--md-comp-pane-margin);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
@include bp-between("medium", "expanded") {
|
|
68
|
+
--md-comp-pane-margin: 24px;
|
|
69
|
+
width: calc(100% - (2 * var(--md-comp-pane-margin)));
|
|
70
|
+
margin-inline: var(--md-comp-pane-margin);
|
|
71
|
+
gap: var(--md-comp-pane-gap);
|
|
72
|
+
}
|
|
73
|
+
|
|
58
74
|
// Panes in compact mode: list/primary visible by default, detail/supporting hidden
|
|
59
75
|
// unless marked .active
|
|
60
76
|
> :is(.pane, .list-pane, .primary-pane, .detail-pane, .supporting-pane) {
|
|
@@ -75,7 +91,7 @@
|
|
|
75
91
|
}
|
|
76
92
|
|
|
77
93
|
// Expanded Viewports: Dual-pane side-by-side (via container query or breakpoint)
|
|
78
|
-
@
|
|
94
|
+
@include bp-up("expanded") {
|
|
79
95
|
grid-template-columns: var(--md-comp-pane-list-width) 1fr;
|
|
80
96
|
gap: 0;
|
|
81
97
|
|
|
@@ -92,6 +108,29 @@
|
|
|
92
108
|
display: flex !important;
|
|
93
109
|
}
|
|
94
110
|
}
|
|
111
|
+
|
|
112
|
+
// This follows both two-pane rules so their base gap cannot cancel the M3
|
|
113
|
+
// Expanded spacer when the viewport and container queries both match.
|
|
114
|
+
@include bp-between("expanded", "large") {
|
|
115
|
+
--md-comp-pane-margin: 24px;
|
|
116
|
+
width: calc(100% - (2 * var(--md-comp-pane-margin)));
|
|
117
|
+
margin-inline: var(--md-comp-pane-margin);
|
|
118
|
+
gap: var(--md-comp-pane-gap);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
@include bp-between("large", "extra-large") {
|
|
122
|
+
--md-comp-pane-margin: 24px;
|
|
123
|
+
width: calc(100% - (2 * var(--md-comp-pane-margin)));
|
|
124
|
+
margin-inline: var(--md-comp-pane-margin);
|
|
125
|
+
gap: var(--md-comp-pane-gap);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
@include bp-up("extra-large") {
|
|
129
|
+
--md-comp-pane-margin: 24px;
|
|
130
|
+
width: calc(100% - (2 * var(--md-comp-pane-margin)));
|
|
131
|
+
margin-inline: var(--md-comp-pane-margin);
|
|
132
|
+
gap: var(--md-comp-pane-gap);
|
|
133
|
+
}
|
|
95
134
|
}
|
|
96
135
|
|
|
97
136
|
// ---------------------------------------------------------------------------
|
|
@@ -101,7 +140,7 @@
|
|
|
101
140
|
// 1. Supporting Pane Layout
|
|
102
141
|
.supporting-pane-layout,
|
|
103
142
|
.panes.supporting {
|
|
104
|
-
@
|
|
143
|
+
@include bp-up("expanded") {
|
|
105
144
|
grid-template-columns: 1fr var(--md-comp-pane-supporting-width);
|
|
106
145
|
}
|
|
107
146
|
|
|
@@ -112,7 +151,7 @@
|
|
|
112
151
|
// Leading supporting pane (.start / .left)
|
|
113
152
|
&.start,
|
|
114
153
|
&.left {
|
|
115
|
-
@
|
|
154
|
+
@include bp-up("expanded") {
|
|
116
155
|
grid-template-columns: var(--md-comp-pane-supporting-width) 1fr;
|
|
117
156
|
}
|
|
118
157
|
|
|
@@ -125,7 +164,7 @@
|
|
|
125
164
|
// 2. Equal / Proportional Pane Layout
|
|
126
165
|
.panes.equal,
|
|
127
166
|
.pane-layout.equal {
|
|
128
|
-
@
|
|
167
|
+
@include bp-up("expanded") {
|
|
129
168
|
grid-template-columns: 1fr 1fr;
|
|
130
169
|
}
|
|
131
170
|
|
|
@@ -137,7 +176,7 @@
|
|
|
137
176
|
// 3. Three-Pane Layout (List + Primary Focus + Supporting)
|
|
138
177
|
.panes.three-pane,
|
|
139
178
|
.pane-layout.three-pane {
|
|
140
|
-
@
|
|
179
|
+
@include bp-up("large") {
|
|
141
180
|
grid-template-columns: var(--md-comp-pane-list-width) 1fr var(--md-comp-pane-supporting-width);
|
|
142
181
|
}
|
|
143
182
|
|
|
@@ -181,7 +220,7 @@
|
|
|
181
220
|
// ---------------------------------------------------------------------------
|
|
182
221
|
:is(.panes, .pane-layout, .list-detail, .supporting-pane-layout):not(.separated, .floating) {
|
|
183
222
|
> :is(.pane, .list-pane, .primary-pane, .detail-pane, .supporting-pane):not(:last-child) {
|
|
184
|
-
@
|
|
223
|
+
@include bp-up("expanded") {
|
|
185
224
|
border-inline-end: 1px solid var(--md-comp-pane-divider-color);
|
|
186
225
|
}
|
|
187
226
|
|
|
@@ -231,7 +270,9 @@
|
|
|
231
270
|
text-overflow: ellipsis;
|
|
232
271
|
}
|
|
233
272
|
|
|
234
|
-
|
|
273
|
+
// See _dialog: `:only-child` cannot see a text-node label, so `.button`
|
|
274
|
+
// is the opt-out for an icon action that also carries one.
|
|
275
|
+
> :is(button:not(.button, .icon-button), a.button):has(> #{$icon}:only-child) {
|
|
235
276
|
flex: 0 0 48px;
|
|
236
277
|
box-sizing: border-box;
|
|
237
278
|
width: 48px;
|
|
@@ -244,7 +285,7 @@
|
|
|
244
285
|
color: var(--md-sys-color-on-surface-variant);
|
|
245
286
|
box-shadow: none;
|
|
246
287
|
|
|
247
|
-
>
|
|
288
|
+
> #{$icon} {
|
|
248
289
|
font-size: 24px;
|
|
249
290
|
width: 24px;
|
|
250
291
|
height: 24px;
|
|
@@ -252,7 +293,7 @@
|
|
|
252
293
|
}
|
|
253
294
|
|
|
254
295
|
&:hover {
|
|
255
|
-
background-color: color-mix(in oklab, var(--md-sys-color-on-surface)
|
|
296
|
+
background-color: color-mix(in oklab, var(--md-sys-color-on-surface) state-opacity("hover"), transparent);
|
|
256
297
|
}
|
|
257
298
|
|
|
258
299
|
&:focus-visible {
|
|
@@ -17,6 +17,9 @@
|
|
|
17
17
|
// --md-comp-progress-circular-size 48px
|
|
18
18
|
// --md-comp-progress-circular-thickness 4px
|
|
19
19
|
|
|
20
|
+
// M3 names this component "Progress indicators", linear and circular. The
|
|
21
|
+
// class is `.progress`; `.preloader` is the Materialize name for the circular
|
|
22
|
+
// one and stays as an alias, as does `.progress.circular`.
|
|
20
23
|
.progress,
|
|
21
24
|
.preloader {
|
|
22
25
|
--md-comp-progress-track: var(--md-sys-color-secondary-container);
|
|
@@ -78,8 +81,11 @@ progress.progress {
|
|
|
78
81
|
transition: width 0.3s linear;
|
|
79
82
|
}
|
|
80
83
|
|
|
81
|
-
// Bare div.progress with a value token, no child.
|
|
82
|
-
|
|
84
|
+
// Bare div.progress with a value token, no child. :not(.circular) because a
|
|
85
|
+
// `<span class="progress circular">` is not a <progress>, has no determinate
|
|
86
|
+
// child, and so matched this too - drawing a linear fill on top of the conic
|
|
87
|
+
// circular indicator.
|
|
88
|
+
.progress:not(:is(progress)):not(.circular):not(:has(> .determinate, > .indeterminate)) {
|
|
83
89
|
&::after {
|
|
84
90
|
content: "";
|
|
85
91
|
position: absolute;
|
|
@@ -133,7 +139,9 @@ progress.progress:indeterminate {
|
|
|
133
139
|
// Circular — one element
|
|
134
140
|
// ---------------------------------------------------------------------------
|
|
135
141
|
|
|
136
|
-
.
|
|
142
|
+
// The circular indicator. `.progress.circular` is the M3-aligned name;
|
|
143
|
+
// `.preloader` is the Materialize one and still works.
|
|
144
|
+
:is(.preloader, .progress.circular) {
|
|
137
145
|
box-sizing: border-box;
|
|
138
146
|
display: inline-block;
|
|
139
147
|
width: var(--md-comp-progress-circular-size);
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
@use "../abstracts" as *;
|
|
2
|
+
|
|
3
|
+
// Scrim (M3 Expressive). A foundation, not a component: there is no markup an
|
|
4
|
+
// author writes to get one. It is the wash a modal surface puts behind itself
|
|
5
|
+
// to dim what it covers, and this is the one place its value is decided -
|
|
6
|
+
// dialogs, both sheets, the navigation drawer and the modal navigation rail
|
|
7
|
+
// all consume the same custom property - the sheets through the
|
|
8
|
+
// `dialog::backdrop` rule they already share, so they declare nothing.
|
|
9
|
+
//
|
|
10
|
+
// https://m3.material.io/styles/color/roles - the `scrim` role, at 32%.
|
|
11
|
+
//
|
|
12
|
+
// Blur is deliberately not part of it. Only the basic dialog frosts what is
|
|
13
|
+
// behind it (`--md-comp-basic-dialog-scrim-blur`); the drawer and the rail
|
|
14
|
+
// slide over the page and would smear it.
|
|
15
|
+
//
|
|
16
|
+
// Declared on :root rather than per surface because four of the five consumers
|
|
17
|
+
// paint into `::backdrop`, which has no box of its own to hang a token on -
|
|
18
|
+
// only the rail uses a `::before`. `::backdrop` inherits from the element it
|
|
19
|
+
// belongs to, so setting this on one `<dialog>` still dims only that one.
|
|
20
|
+
//
|
|
21
|
+
// Resolving the mix here narrows one thing, deliberately: `--md-sys-color-scrim`
|
|
22
|
+
// is substituted at :root, so overriding *that* on a subtree no longer reaches a
|
|
23
|
+
// scrim below it. Override it at the root to move every scrim, or set
|
|
24
|
+
// `--md-comp-scrim-color` on a surface to move one. A scrim is painted by a
|
|
25
|
+
// top-layer or fixed box covering the viewport, so it has no meaningful subtree
|
|
26
|
+
// to be rethemed by in the first place.
|
|
27
|
+
|
|
28
|
+
// `:host` alongside `:root` for the reason tokens/_theme.scss and
|
|
29
|
+
// tokens/_state.scss carry it: a sheet loaded inside a shadow root cannot reach
|
|
30
|
+
// its own host with `:root`, and the token would be undefined there - taking
|
|
31
|
+
// `background-color` down with it, so the surface would show no scrim at all.
|
|
32
|
+
// The rules this replaced read `--md-sys-color-scrim`, which is declared on
|
|
33
|
+
// both, so :root alone would have narrowed what already worked.
|
|
34
|
+
:root,
|
|
35
|
+
:host {
|
|
36
|
+
--md-comp-scrim-color: color-mix(in oklab, var(--md-sys-color-scrim) 32%, transparent);
|
|
37
|
+
}
|
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
@use "../abstracts" as *;
|
|
2
|
+
|
|
3
|
+
// Search: the M3 search bar and the view it expands into.
|
|
4
|
+
//
|
|
5
|
+
// Anatomy is the HTML. A `<search class="search-bar">` is the bar - an
|
|
6
|
+
// optional leading icon or icon button, an `<input type="search">`, and
|
|
7
|
+
// optional trailing actions. `.searchbar` is the pre-1.0 alias.
|
|
8
|
+
//
|
|
9
|
+
// `.search-view` is the surface the bar expands into, and it is two different
|
|
10
|
+
// native elements because the two variants want two different behaviours:
|
|
11
|
+
//
|
|
12
|
+
// docked a plain element inside the bar, shown by dropping `hidden`.
|
|
13
|
+
// The bar keeps focus, which is the whole point - a modeless
|
|
14
|
+
// `<dialog>.show()` moves focus into the dialog and would take
|
|
15
|
+
// the caret out of the input the user is typing in.
|
|
16
|
+
// full-screen a `<dialog class="search-view full-screen">` opened with
|
|
17
|
+
// `showModal()`, so Escape and light dismiss come from the
|
|
18
|
+
// platform and `behaviors/dialogs.ts` rather than from here.
|
|
19
|
+
//
|
|
20
|
+
// Suggestions are Autocomplete's, not this component's. Put `autocomplete` on
|
|
21
|
+
// the bar's input and the combobox, its listbox and the keyboard contract
|
|
22
|
+
// behind them are the ones already implemented and tested. The search view
|
|
23
|
+
// holds content - recent searches, results, filters - and takes no composite
|
|
24
|
+
// role of its own; see SEMANTICS.md.
|
|
25
|
+
//
|
|
26
|
+
// Tokens follow md.comp.search-bar.* / md.comp.search-view.* at DSP 34.0.21
|
|
27
|
+
// bar 56dp, corner-full, surface-container-high, elevation 3
|
|
28
|
+
// input body-large / on-surface; the placeholder is M3's supporting
|
|
29
|
+
// text, hence on-surface-variant
|
|
30
|
+
// icons 24dp; leading on-surface, trailing on-surface-variant
|
|
31
|
+
// avatar 30dp, corner-full
|
|
32
|
+
// spacing 16dp at an end with no action, 4dp at one with an icon
|
|
33
|
+
// button, which carries its own 12dp inset
|
|
34
|
+
// view surface-container-high, corner-medium, elevation 3, 2dp
|
|
35
|
+
// below the bar
|
|
36
|
+
// full-screen fills the viewport, no corners, 72dp header
|
|
37
|
+
// divider outline
|
|
38
|
+
//
|
|
39
|
+
// The bar and the docked view are both the *contained* (Expressive) variant,
|
|
40
|
+
// which is what the 4dp/16dp spacing pair and the 2dp gap belong to; taking
|
|
41
|
+
// the shape from the divided/baseline set instead would mix two anatomies.
|
|
42
|
+
// `--md-comp-search-view-max-height` is a framework knob, not a DSP token -
|
|
43
|
+
// M3 sizes the docked view by its content and leaves the cap to the platform.
|
|
44
|
+
//
|
|
45
|
+
// Nest descendants inside these blocks, and keep `$_bar` wrapped in `:is()` -
|
|
46
|
+
// a bare list interpolated into `.search-view > #{$_bar}` breaks out of its
|
|
47
|
+
// context and emits a top-level rule.
|
|
48
|
+
|
|
49
|
+
$_bar: ":is(.search-bar, .searchbar)";
|
|
50
|
+
|
|
51
|
+
#{$_bar} {
|
|
52
|
+
--md-comp-search-bar-container-color: var(--md-sys-color-surface-container-high);
|
|
53
|
+
--md-comp-search-bar-container-height: 56px;
|
|
54
|
+
--md-comp-search-bar-container-shape: 9999px;
|
|
55
|
+
--md-comp-search-bar-leading-space: 16px;
|
|
56
|
+
--md-comp-search-bar-trailing-space: 16px;
|
|
57
|
+
--md-comp-search-bar-icon-label-space: 16px;
|
|
58
|
+
--md-comp-search-bar-icon-size: 24px;
|
|
59
|
+
--md-comp-search-bar-avatar-size: 30px;
|
|
60
|
+
|
|
61
|
+
position: relative;
|
|
62
|
+
box-sizing: border-box;
|
|
63
|
+
display: flex;
|
|
64
|
+
align-items: center;
|
|
65
|
+
gap: var(--md-comp-search-bar-icon-label-space);
|
|
66
|
+
width: 100%;
|
|
67
|
+
min-height: var(--md-comp-search-bar-container-height);
|
|
68
|
+
padding-inline: var(--md-comp-search-bar-leading-space)
|
|
69
|
+
var(--md-comp-search-bar-trailing-space);
|
|
70
|
+
border-radius: var(--md-comp-search-bar-container-shape);
|
|
71
|
+
background-color: var(--md-comp-search-bar-container-color);
|
|
72
|
+
color: var(--md-sys-color-on-surface);
|
|
73
|
+
@include z-depth("3");
|
|
74
|
+
|
|
75
|
+
// An icon *button* already insets its glyph by 12dp, so M3 pulls the end
|
|
76
|
+
// space in to 4dp beside one. A bare glyph keeps the full 16dp.
|
|
77
|
+
// A `form[method="dialog"]` wrapper is spelled out rather than folded into
|
|
78
|
+
// the compound: `:has()` may not be nested inside `:has()`, and because the
|
|
79
|
+
// argument list is forgiving, `form:has(> .icon-button)` in there would be
|
|
80
|
+
// dropped silently rather than reported.
|
|
81
|
+
&:has(> .icon-button:first-child),
|
|
82
|
+
&:has(> form:first-child > .icon-button) {
|
|
83
|
+
--md-comp-search-bar-leading-space: 4px;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// The docked view is a child of the bar but out of flow, so the last
|
|
87
|
+
// *control* is the last child that is not it - `:last-child` alone reads the
|
|
88
|
+
// view as the trailing element and leaves the icon button on 16dp.
|
|
89
|
+
&:has(> .icon-button:nth-last-child(1 of :not(.search-view))),
|
|
90
|
+
&:has(> form:nth-last-child(1 of :not(.search-view)) > .icon-button) {
|
|
91
|
+
--md-comp-search-bar-trailing-space: 4px;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// A leading or trailing action wrapped in `form[method="dialog"]` - how the
|
|
95
|
+
// full-screen view closes itself without script.
|
|
96
|
+
> form {
|
|
97
|
+
display: flex;
|
|
98
|
+
flex: 0 0 auto;
|
|
99
|
+
align-items: center;
|
|
100
|
+
margin: 0;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
> #{$icon} {
|
|
104
|
+
flex: 0 0 auto;
|
|
105
|
+
font-size: var(--md-comp-search-bar-icon-size);
|
|
106
|
+
width: var(--md-comp-search-bar-icon-size);
|
|
107
|
+
height: var(--md-comp-search-bar-icon-size);
|
|
108
|
+
line-height: var(--md-comp-search-bar-icon-size);
|
|
109
|
+
color: var(--md-sys-color-on-surface);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
> #{$icon}:nth-last-child(1 of :not(.search-view)) {
|
|
113
|
+
color: var(--md-sys-color-on-surface-variant);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// An account avatar, the one non-icon element M3 puts in the bar. A tappable
|
|
117
|
+
// one goes inside a button, which brings its own 48dp target.
|
|
118
|
+
> img {
|
|
119
|
+
flex: 0 0 auto;
|
|
120
|
+
width: var(--md-comp-search-bar-avatar-size);
|
|
121
|
+
height: var(--md-comp-search-bar-avatar-size);
|
|
122
|
+
border-radius: 50%;
|
|
123
|
+
object-fit: cover;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// The input carries none of the text-field chrome: no underline, no floating
|
|
127
|
+
// label, no 56dp block of its own. It is the bar that is the container.
|
|
128
|
+
> input {
|
|
129
|
+
flex: 1 1 auto;
|
|
130
|
+
min-width: 0;
|
|
131
|
+
height: auto;
|
|
132
|
+
margin: 0;
|
|
133
|
+
padding: 0;
|
|
134
|
+
border: none;
|
|
135
|
+
background-color: transparent;
|
|
136
|
+
box-shadow: none;
|
|
137
|
+
color: var(--md-sys-color-on-surface);
|
|
138
|
+
@include typescale("body-large");
|
|
139
|
+
|
|
140
|
+
&:focus {
|
|
141
|
+
outline: none;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
&::placeholder {
|
|
145
|
+
color: var(--md-sys-color-on-surface-variant);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// The UA's own clear affordance duplicates the trailing icon button M3
|
|
149
|
+
// asks for, and cannot be styled to match it.
|
|
150
|
+
&::-webkit-search-cancel-button {
|
|
151
|
+
appearance: none;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Pre-1.0 `.searchbar` sat in a navbar beside a fixed drawer.
|
|
156
|
+
&.has-sidebar {
|
|
157
|
+
margin-inline-start: 0;
|
|
158
|
+
|
|
159
|
+
@include bp-up("expanded") {
|
|
160
|
+
// `width: auto`, because the margin sits *outside* the base bar's
|
|
161
|
+
// `width: 100%`: the two together make the bar 100% + 300px wide and
|
|
162
|
+
// push its trailing edge past the viewport, on exactly the fixed-drawer
|
|
163
|
+
// layout this rule exists to preserve. The legacy bar had no width of
|
|
164
|
+
// its own, so the margin shrank it.
|
|
165
|
+
width: auto;
|
|
166
|
+
margin-inline-start: 300px;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
.search-view {
|
|
172
|
+
--md-comp-search-view-container-color: var(--md-sys-color-surface-container-high);
|
|
173
|
+
--md-comp-search-view-container-shape: 12px;
|
|
174
|
+
--md-comp-search-view-header-height: 56px;
|
|
175
|
+
--md-comp-search-view-divider-color: var(--md-sys-color-outline);
|
|
176
|
+
--md-comp-search-view-bar-results-gap: 2px;
|
|
177
|
+
--md-comp-search-view-max-height: 60vh;
|
|
178
|
+
|
|
179
|
+
box-sizing: border-box;
|
|
180
|
+
position: absolute;
|
|
181
|
+
z-index: 1;
|
|
182
|
+
inset-inline: 0;
|
|
183
|
+
top: calc(100% + var(--md-comp-search-view-bar-results-gap));
|
|
184
|
+
display: flex;
|
|
185
|
+
flex-direction: column;
|
|
186
|
+
max-height: var(--md-comp-search-view-max-height);
|
|
187
|
+
// The surface scrolls when its content is loose (a <div> of results); when
|
|
188
|
+
// the content is a `.list` the list scrolls instead, so a header and its
|
|
189
|
+
// divider stay put. Which of the two moves depends on the composition, so
|
|
190
|
+
// both declare it.
|
|
191
|
+
overflow-y: auto;
|
|
192
|
+
overscroll-behavior: contain;
|
|
193
|
+
border-radius: var(--md-comp-search-view-container-shape);
|
|
194
|
+
background-color: var(--md-comp-search-view-container-color);
|
|
195
|
+
color: var(--md-sys-color-on-surface);
|
|
196
|
+
@include z-depth("3");
|
|
197
|
+
|
|
198
|
+
// Two ways this surface is closed, and an author `display` beats both of the
|
|
199
|
+
// UA rules that do the hiding - `[hidden] { display: none }` for the docked
|
|
200
|
+
// element and `dialog:not([open]) { display: none }` for the full-screen
|
|
201
|
+
// one. The second is the one that bites: without it a closed <dialog> paints
|
|
202
|
+
// an opaque panel over the whole viewport, and no visual baseline exists to
|
|
203
|
+
// catch it on the change that introduces the page.
|
|
204
|
+
&[hidden],
|
|
205
|
+
&:not([open]):is(dialog) {
|
|
206
|
+
display: none;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// The view's own header is a search bar with the pill taken off - the same
|
|
210
|
+
// markup, so nothing new to learn for the full-screen variant.
|
|
211
|
+
> #{$_bar} {
|
|
212
|
+
flex: 0 0 auto;
|
|
213
|
+
min-height: var(--md-comp-search-view-header-height);
|
|
214
|
+
border-radius: 0;
|
|
215
|
+
background-color: transparent;
|
|
216
|
+
@include z-depth("0");
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
> hr {
|
|
220
|
+
flex: 0 0 auto;
|
|
221
|
+
box-sizing: border-box;
|
|
222
|
+
width: 100%;
|
|
223
|
+
height: 1px;
|
|
224
|
+
margin: 0;
|
|
225
|
+
padding: 0;
|
|
226
|
+
border: 0;
|
|
227
|
+
background-color: var(--md-comp-search-view-divider-color);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
> .list {
|
|
231
|
+
flex: 1 1 auto;
|
|
232
|
+
min-height: 0;
|
|
233
|
+
overflow-y: auto;
|
|
234
|
+
margin: 0;
|
|
235
|
+
padding-block: 8px;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// Full-screen. The UA caps a modal dialog at `calc(100% - 2em - 6px)`, so
|
|
240
|
+
// width: 100% alone leaves a strip of scrim down the side - the same clearing
|
|
241
|
+
// dialog.max and the bottom sheet already do.
|
|
242
|
+
.search-view.full-screen {
|
|
243
|
+
--md-comp-search-view-container-shape: 0;
|
|
244
|
+
--md-comp-search-view-header-height: 72px;
|
|
245
|
+
--md-comp-search-view-max-height: none;
|
|
246
|
+
|
|
247
|
+
position: fixed;
|
|
248
|
+
inset: 0;
|
|
249
|
+
width: 100%;
|
|
250
|
+
min-width: 0;
|
|
251
|
+
max-width: none;
|
|
252
|
+
height: 100%;
|
|
253
|
+
margin: 0;
|
|
254
|
+
|
|
255
|
+
&::backdrop {
|
|
256
|
+
background: none;
|
|
257
|
+
backdrop-filter: none;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
@use "../abstracts" as *;
|
|
2
|
+
|
|
3
|
+
// Segmented buttons.
|
|
4
|
+
//
|
|
5
|
+
// Anatomy is the HTML. A <fieldset class="segmented-button"> is the group and
|
|
6
|
+
// its <legend> names it. Each segment is an <input> and the
|
|
7
|
+
// <label class="segment"> beside it, tied by id: radios make the group
|
|
8
|
+
// single-select, checkboxes make it multi-select, and the input type is the
|
|
9
|
+
// whole difference.
|
|
10
|
+
//
|
|
11
|
+
// The control is the label's *sibling*, not its child, which is the same
|
|
12
|
+
// arrangement filter chips use and for the same reason: to
|
|
13
|
+
// forms/_radio-buttons and forms/_checkboxes, a <label> wrapping one of those
|
|
14
|
+
// inputs *is* a radio or a checkbox. They would paint a 20dp ring on the
|
|
15
|
+
// input, stretch the label to a 48dp row and, for a checkbox, mask a
|
|
16
|
+
// checkmark onto its ::before - none of which a segment wants, and all of
|
|
17
|
+
// which outweigh a segment rule that only names two classes.
|
|
18
|
+
//
|
|
19
|
+
// Nothing here is scripted, and that is the point. The browser owns the
|
|
20
|
+
// checked state and, for radios, the arrow-key model a single-select group
|
|
21
|
+
// promises: SEMANTICS rule 2 asks that a composite keyboard contract be
|
|
22
|
+
// implemented before the role is claimed, and the native one is implemented
|
|
23
|
+
// by the platform. No ARIA role is declared, so none is withheld.
|
|
24
|
+
//
|
|
25
|
+
// Tokens follow M3, md.comp.outlined-segmented-button.* (DSP 34.0.21):
|
|
26
|
+
// container 40dp, corner.full, 1dp outline, dividers between segments
|
|
27
|
+
// segment 12dp insets, 18dp icon, 8dp icon-label space, label-large
|
|
28
|
+
// unselected on-surface on a transparent container
|
|
29
|
+
// selected on-secondary-container on secondary-container
|
|
30
|
+
// state layer 8% hover, 10% press (focus is the ring, not a layer)
|
|
31
|
+
// disabled label and icon at 38%, outline at 12%
|
|
32
|
+
.segmented-button {
|
|
33
|
+
--md-comp-outlined-segmented-button-container-height: 40px;
|
|
34
|
+
--md-comp-outlined-segmented-button-container-shape: 9999px;
|
|
35
|
+
--md-comp-outlined-segmented-button-outline-width: 1px;
|
|
36
|
+
--md-comp-outlined-segmented-button-outline-color: var(--md-sys-color-outline);
|
|
37
|
+
--md-comp-outlined-segmented-button-leading-space: 12px;
|
|
38
|
+
--md-comp-outlined-segmented-button-trailing-space: 12px;
|
|
39
|
+
--md-comp-outlined-segmented-button-icon-size: 18px;
|
|
40
|
+
--md-comp-outlined-segmented-button-icon-label-space: 8px;
|
|
41
|
+
--md-comp-outlined-segmented-button-label-text-color: var(--md-sys-color-on-surface);
|
|
42
|
+
--md-comp-outlined-segmented-button-selected-container-color: var(--md-sys-color-secondary-container);
|
|
43
|
+
--md-comp-outlined-segmented-button-selected-label-text-color: var(--md-sys-color-on-secondary-container);
|
|
44
|
+
|
|
45
|
+
// A <fieldset> arrives with a border, 16dp of padding and a min-width; the
|
|
46
|
+
// group's own outline replaces all three.
|
|
47
|
+
box-sizing: border-box;
|
|
48
|
+
// Equal columns across whatever inline size the group is given, which is
|
|
49
|
+
// what M3 specifies - material-web's own set is `grid-auto-columns: 1fr` on
|
|
50
|
+
// a full-width grid, not a shrink-wrapped row. Constrain the group with a
|
|
51
|
+
// width or a wrapper when it should be narrower than its container.
|
|
52
|
+
display: grid;
|
|
53
|
+
grid-auto-flow: column;
|
|
54
|
+
grid-auto-columns: 1fr;
|
|
55
|
+
min-inline-size: 0;
|
|
56
|
+
margin: 0;
|
|
57
|
+
padding: 0;
|
|
58
|
+
background-color: transparent;
|
|
59
|
+
border: var(--md-comp-outlined-segmented-button-outline-width) solid
|
|
60
|
+
var(--md-comp-outlined-segmented-button-outline-color);
|
|
61
|
+
border-radius: var(--md-comp-outlined-segmented-button-container-shape);
|
|
62
|
+
// Squares the segments off against the pill, so the end segments' fill and
|
|
63
|
+
// their focus ring both stop at the outline.
|
|
64
|
+
overflow: hidden;
|
|
65
|
+
|
|
66
|
+
& + & {
|
|
67
|
+
margin-block-start: 24px;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// M3's anatomy has no visible group label. A set of radios is only a set
|
|
71
|
+
// because a <fieldset> says so, and a <fieldset> is only named because of
|
|
72
|
+
// its <legend> — so the legend is the accessible name and nothing else.
|
|
73
|
+
//
|
|
74
|
+
// There is deliberately no modifier that shows it. The root is both the
|
|
75
|
+
// fieldset and the grid of segments, so a legend back in flow is another
|
|
76
|
+
// column beside them; putting it above the pill would take an inner
|
|
77
|
+
// wrapper the markup does not have. A group that needs a visible label
|
|
78
|
+
// writes one before the fieldset and leaves the legend as the name.
|
|
79
|
+
> legend {
|
|
80
|
+
@include visually-hidden();
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// The control carries the state and the form value; the label is the
|
|
84
|
+
// segment. Clipped rather than `display: none` so it stays focusable — and
|
|
85
|
+
// so the arrow keys still move between the radios.
|
|
86
|
+
> input {
|
|
87
|
+
@include visually-hidden();
|
|
88
|
+
border: 0;
|
|
89
|
+
appearance: none;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
> .segment {
|
|
93
|
+
// Read back by the state layer below, so hover and press are one rule
|
|
94
|
+
// each rather than one per selected/unselected pair.
|
|
95
|
+
--_label-color: var(--md-comp-outlined-segmented-button-label-text-color);
|
|
96
|
+
--_container-color: transparent;
|
|
97
|
+
|
|
98
|
+
display: inline-flex;
|
|
99
|
+
align-items: center;
|
|
100
|
+
justify-content: center;
|
|
101
|
+
gap: var(--md-comp-outlined-segmented-button-icon-label-space);
|
|
102
|
+
margin: 0;
|
|
103
|
+
min-block-size: calc(
|
|
104
|
+
var(--md-comp-outlined-segmented-button-container-height) - 2 *
|
|
105
|
+
var(--md-comp-outlined-segmented-button-outline-width)
|
|
106
|
+
);
|
|
107
|
+
padding-inline: var(--md-comp-outlined-segmented-button-leading-space)
|
|
108
|
+
var(--md-comp-outlined-segmented-button-trailing-space);
|
|
109
|
+
white-space: nowrap;
|
|
110
|
+
cursor: pointer;
|
|
111
|
+
user-select: none;
|
|
112
|
+
color: var(--_label-color);
|
|
113
|
+
background-color: var(--_container-color);
|
|
114
|
+
transition: background-color .2s ease, color .2s ease;
|
|
115
|
+
@include typescale("label-large");
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Divider, not a border on every segment: the group's own outline is the
|
|
119
|
+
// outer edge, so only the joins are drawn. `:first-of-type` counts labels,
|
|
120
|
+
// and every segment is one — the legend and the inputs are other types.
|
|
121
|
+
> .segment:not(:first-of-type) {
|
|
122
|
+
border-inline-start: var(--md-comp-outlined-segmented-button-outline-width) solid
|
|
123
|
+
var(--md-comp-outlined-segmented-button-outline-color);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Selected is the input's business. The adjacency is what lets the label
|
|
127
|
+
// read a state only the control has, with no class for the page to sync.
|
|
128
|
+
> input:checked + .segment {
|
|
129
|
+
--_label-color: var(--md-comp-outlined-segmented-button-selected-label-text-color);
|
|
130
|
+
--_container-color: var(--md-comp-outlined-segmented-button-selected-container-color);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
> .segment > #{$icon} {
|
|
134
|
+
font-size: var(--md-comp-outlined-segmented-button-icon-size);
|
|
135
|
+
width: var(--md-comp-outlined-segmented-button-icon-size);
|
|
136
|
+
height: var(--md-comp-outlined-segmented-button-icon-size);
|
|
137
|
+
line-height: var(--md-comp-outlined-segmented-button-icon-size);
|
|
138
|
+
color: inherit;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
> input:not(:disabled) + .segment {
|
|
142
|
+
&:hover {
|
|
143
|
+
background-color: color-mix(
|
|
144
|
+
in oklab,
|
|
145
|
+
var(--_label-color) state-opacity("hover"),
|
|
146
|
+
var(--_container-color)
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
&:active {
|
|
151
|
+
background-color: color-mix(
|
|
152
|
+
in oklab,
|
|
153
|
+
var(--_label-color) state-opacity("pressed"),
|
|
154
|
+
var(--_container-color)
|
|
155
|
+
);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// The ring is drawn inside the segment: `overflow: hidden` above would clip
|
|
160
|
+
// an outline sitting outside it, and the end segments would lose half of it
|
|
161
|
+
// to the pill's own corner.
|
|
162
|
+
> input:focus-visible + .segment {
|
|
163
|
+
@include focus-ring();
|
|
164
|
+
outline-offset: -3px;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
> input:disabled + .segment {
|
|
168
|
+
--_label-color: color-mix(in oklab, var(--md-sys-color-on-surface) 38%, transparent);
|
|
169
|
+
cursor: default;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
.segmented-button:disabled {
|
|
174
|
+
--md-comp-outlined-segmented-button-outline-color: color-mix(
|
|
175
|
+
in oklab,
|
|
176
|
+
var(--md-sys-color-on-surface) 12%,
|
|
177
|
+
transparent
|
|
178
|
+
);
|
|
179
|
+
--md-comp-outlined-segmented-button-label-text-color: color-mix(
|
|
180
|
+
in oklab,
|
|
181
|
+
var(--md-sys-color-on-surface) 38%,
|
|
182
|
+
transparent
|
|
183
|
+
);
|
|
184
|
+
|
|
185
|
+
> .segment {
|
|
186
|
+
cursor: default;
|
|
187
|
+
}
|
|
188
|
+
}
|