@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.
Files changed (122) hide show
  1. package/CHANGELOG.md +1313 -1
  2. package/README.md +76 -30
  3. package/dist/css/expressive.css +4896 -1777
  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 +1673 -1338
  18. package/dist/js/expressive.cjs.map +4 -4
  19. package/dist/js/expressive.js +1673 -1338
  20. package/dist/js/expressive.js.map +4 -4
  21. package/dist/js/expressive.min.js +13 -13
  22. package/dist/js/expressive.min.js.map +4 -4
  23. package/dist/js/expressive.mjs +1673 -1338
  24. package/dist/js/expressive.mjs.map +4 -4
  25. package/dist/types/behaviors/bottomSheets.d.ts +19 -11
  26. package/dist/types/behaviors/sheetDrag.d.ts +40 -0
  27. package/dist/types/behaviors/sideSheets.d.ts +2 -9
  28. package/dist/types/components/appBar.d.ts +26 -0
  29. package/dist/types/components/autocomplete.d.ts +15 -3
  30. package/dist/types/components/buttonGroup.d.ts +39 -0
  31. package/dist/types/components/buttons.d.ts +5 -1
  32. package/dist/types/components/cards.d.ts +11 -13
  33. package/dist/types/components/carousel.d.ts +87 -53
  34. package/dist/types/components/chips.d.ts +21 -1
  35. package/dist/types/components/expandingCard.d.ts +31 -0
  36. package/dist/types/components/index.d.ts +23 -20
  37. package/dist/types/components/lightbox.d.ts +1 -0
  38. package/dist/types/components/menu.d.ts +11 -1
  39. package/dist/types/components/{sidenav.d.ts → navigationDrawer.d.ts} +16 -16
  40. package/dist/types/components/registry.d.ts +18 -8
  41. package/dist/types/components/slider.d.ts +26 -85
  42. package/dist/types/components/snackbar.d.ts +20 -5
  43. package/dist/types/components/tabs.d.ts +12 -0
  44. package/dist/types/components/timepicker.d.ts +12 -10
  45. package/dist/types/components/tooltip.d.ts +1 -0
  46. package/dist/types/core/utils.d.ts +64 -53
  47. package/dist/types/index.d.ts +8 -9
  48. package/package.json +39 -3
  49. package/src/sass/README.md +61 -11
  50. package/src/sass/abstracts/_breakpoints.scss +17 -19
  51. package/src/sass/abstracts/_mixins.scss +155 -21
  52. package/src/sass/abstracts/_variables.scss +69 -5
  53. package/src/sass/base/_fonts.scss +63 -0
  54. package/src/sass/base/_global.scss +6 -24
  55. package/src/sass/base/_grid.scss +15 -12
  56. package/src/sass/base/_normalize.scss +34 -38
  57. package/src/sass/base/_typography.scss +4 -18
  58. package/src/sass/components/_badges.scss +3 -5
  59. package/src/sass/components/_banners.scss +298 -0
  60. package/src/sass/components/_bottom-app-bar.scss +180 -0
  61. package/src/sass/components/_bottom-sheet.scss +12 -4
  62. package/src/sass/components/_button-groups.scss +358 -0
  63. package/src/sass/components/_buttons.scss +245 -92
  64. package/src/sass/components/_cards.scss +514 -78
  65. package/src/sass/components/_carousel.scss +562 -68
  66. package/src/sass/components/_chips.scss +64 -5
  67. package/src/sass/components/_datepicker.scss +24 -24
  68. package/src/sass/components/_dialog.scss +17 -11
  69. package/src/sass/components/_drag-handle.scss +130 -0
  70. package/src/sass/components/_expanding-card.scss +226 -0
  71. package/src/sass/components/_fab-menu.scss +232 -0
  72. package/src/sass/components/_floating-sheet.scss +86 -0
  73. package/src/sass/components/_icon-buttons.scss +172 -0
  74. package/src/sass/components/_icons-material-design.scss +22 -10
  75. package/src/sass/components/_index.scss +22 -6
  76. package/src/sass/components/_list.scss +16 -13
  77. package/src/sass/components/_loading-indicator.scss +106 -0
  78. package/src/sass/components/_menu.scss +335 -61
  79. package/src/sass/components/_navbar.scss +245 -45
  80. package/src/sass/components/_navigation-bar.scss +22 -11
  81. package/src/sass/components/{_sidenav.scss → _navigation-drawer.scss} +123 -33
  82. package/src/sass/components/_navigation-rail.scss +31 -23
  83. package/src/sass/components/_page-footer.scss +19 -16
  84. package/src/sass/components/_pagination.scss +24 -6
  85. package/src/sass/components/_panes.scss +52 -11
  86. package/src/sass/components/{_preloader.scss → _progress.scss} +11 -3
  87. package/src/sass/components/_scrim.scss +37 -0
  88. package/src/sass/components/_search.scss +259 -0
  89. package/src/sass/components/_segmented-buttons.scss +188 -0
  90. package/src/sass/components/_side-sheet.scss +2 -2
  91. package/src/sass/components/_snackbar.scss +20 -15
  92. package/src/sass/components/_split-button.scss +196 -0
  93. package/src/sass/components/_tabs.scss +7 -8
  94. package/src/sass/components/_timepicker.scss +67 -58
  95. package/src/sass/components/_toolbar.scss +206 -80
  96. package/src/sass/components/_transitions.scss +8 -1
  97. package/src/sass/components/forms/_checkboxes.scss +49 -12
  98. package/src/sass/components/forms/_fieldset.scss +4 -4
  99. package/src/sass/components/forms/_file-input.scss +8 -1
  100. package/src/sass/components/forms/_index.scss +1 -1
  101. package/src/sass/components/forms/_input-fields.scss +50 -44
  102. package/src/sass/components/forms/_radio-buttons.scss +42 -10
  103. package/src/sass/components/forms/_select.scss +38 -1
  104. package/src/sass/components/forms/{_range.scss → _slider.scss} +8 -9
  105. package/src/sass/components/forms/_switches.scss +6 -7
  106. package/src/sass/expressive.scss +5 -0
  107. package/src/sass/tokens/_index.scss +4 -1
  108. package/src/sass/tokens/_reference.scss +70 -61
  109. package/src/sass/tokens/_state.scss +24 -0
  110. package/src/sass/tokens/_theme.scss +6 -3
  111. package/src/sass/tokens/_vibrant.scss +76 -0
  112. package/src/sass/utilities/_colors.scss +14 -1
  113. package/src/sass/utilities/_helpers.scss +5 -5
  114. package/src/sass/utilities/_visibility.scss +48 -26
  115. package/dist/types/behaviors/waves.d.ts +0 -15
  116. package/dist/types/components/parallax.d.ts +0 -16
  117. package/dist/types/components/range.d.ts +0 -43
  118. package/dist/types/core/bounding.d.ts +0 -6
  119. package/dist/types/core/edges.d.ts +0 -6
  120. package/src/sass/components/_parallax.scss +0 -104
  121. package/src/sass/components/_pulse.scss +0 -37
  122. 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 / < 601px): Single pane visible at a time.
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 (Single-Pane on compact screens < 840px / < 601px)
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
- @media (width >= 840px) {
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
- @media (width >= 840px) {
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
- @media (width >= 840px) {
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
- @media (width >= 840px) {
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
- @media (width >= 1200px) {
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
- @media (width >= 840px) {
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
- > :is(button, a.button):has(> i:only-child) {
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
- > i {
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) 8%, transparent);
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
- .progress:not(:is(progress)):not(:has(> .determinate, > .indeterminate)) {
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
- .preloader {
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
+ }