@expressivecss/expressive 0.2.0 → 0.4.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 (80) hide show
  1. package/README.md +8 -8
  2. package/dist/css/expressive.css +1915 -1566
  3. package/dist/css/expressive.css.map +1 -1
  4. package/dist/css/expressive.min.css +1 -1
  5. package/dist/css/expressive.min.css.map +1 -1
  6. package/dist/js/expressive.cjs +1338 -1528
  7. package/dist/js/expressive.cjs.map +4 -4
  8. package/dist/js/expressive.js +1338 -1528
  9. package/dist/js/expressive.js.map +4 -4
  10. package/dist/js/expressive.min.js +10 -20
  11. package/dist/js/expressive.min.js.map +4 -4
  12. package/dist/js/expressive.mjs +1338 -1529
  13. package/dist/js/expressive.mjs.map +4 -4
  14. package/dist/types/components/autocomplete.d.ts +2 -9
  15. package/dist/types/components/buttons.d.ts +19 -43
  16. package/dist/types/components/cards.d.ts +3 -6
  17. package/dist/types/components/carousel.d.ts +42 -48
  18. package/dist/types/components/characterCounter.d.ts +3 -5
  19. package/dist/types/components/chips.d.ts +4 -4
  20. package/dist/types/components/collapsible.d.ts +28 -23
  21. package/dist/types/components/datepicker.d.ts +2 -8
  22. package/dist/types/components/dropdown.d.ts +11 -2
  23. package/dist/types/components/index.d.ts +0 -1
  24. package/dist/types/components/lightbox.d.ts +2 -2
  25. package/dist/types/components/parallax.d.ts +7 -37
  26. package/dist/types/components/pushpin.d.ts +2 -2
  27. package/dist/types/components/range.d.ts +2 -2
  28. package/dist/types/components/registry.d.ts +0 -4
  29. package/dist/types/components/scrollspy.d.ts +26 -50
  30. package/dist/types/components/select.d.ts +20 -6
  31. package/dist/types/components/sidenav.d.ts +40 -55
  32. package/dist/types/components/slider.d.ts +2 -2
  33. package/dist/types/components/tabs.d.ts +2 -2
  34. package/dist/types/components/tapTarget.d.ts +25 -26
  35. package/dist/types/components/timepicker.d.ts +2 -4
  36. package/dist/types/components/tooltip.d.ts +2 -2
  37. package/dist/types/core/component.d.ts +4 -4
  38. package/dist/types/index.d.ts +1 -1
  39. package/package.json +2 -2
  40. package/src/sass/README.md +3 -6
  41. package/src/sass/abstracts/_breakpoints.scss +0 -16
  42. package/src/sass/abstracts/_variables.scss +0 -21
  43. package/src/sass/base/_grid.scss +0 -19
  44. package/src/sass/components/_badges.scss +6 -5
  45. package/src/sass/components/_breadcrumb.scss +93 -16
  46. package/src/sass/components/_buttons.scss +212 -145
  47. package/src/sass/components/_cards.scss +50 -123
  48. package/src/sass/components/_carousel.scss +128 -63
  49. package/src/sass/components/_chips.scss +1 -1
  50. package/src/sass/components/_collapsible.scss +158 -60
  51. package/src/sass/components/_collection.scss +146 -80
  52. package/src/sass/components/_datepicker.scss +1 -15
  53. package/src/sass/components/_dropdown.scss +110 -14
  54. package/src/sass/components/_icons-material-design.scss +77 -4
  55. package/src/sass/components/_index.scss +1 -0
  56. package/src/sass/components/_lightbox.scss +4 -0
  57. package/src/sass/components/_modal.scss +13 -31
  58. package/src/sass/components/_navbar.scss +24 -6
  59. package/src/sass/components/_page-footer.scss +134 -15
  60. package/src/sass/components/_pagination.scss +131 -25
  61. package/src/sass/components/_parallax.scss +96 -21
  62. package/src/sass/components/_preloader.scss +145 -300
  63. package/src/sass/components/_scrollspy.scss +18 -0
  64. package/src/sass/components/_sidenav.scss +230 -119
  65. package/src/sass/components/_slider.scss +8 -0
  66. package/src/sass/components/_table_of_contents.scss +30 -14
  67. package/src/sass/components/_tabs.scss +54 -35
  68. package/src/sass/components/_tapTarget.scss +128 -45
  69. package/src/sass/components/_toast.scss +10 -11
  70. package/src/sass/components/_toolbar.scss +2 -2
  71. package/src/sass/components/_tooltip.scss +4 -9
  72. package/src/sass/components/_transitions.scss +0 -5
  73. package/src/sass/components/forms/_index.scss +0 -5
  74. package/src/sass/components/forms/_input-fields.scss +49 -52
  75. package/src/sass/components/forms/_select.scss +175 -60
  76. package/src/sass/components/forms/_switches.scss +21 -8
  77. package/src/sass/utilities/_helpers.scss +1 -3
  78. package/src/sass/utilities/_typescale.scss +0 -5
  79. package/src/sass/utilities/_visibility.scss +2 -2
  80. package/dist/types/components/modal.d.ts +0 -45
@@ -5,26 +5,27 @@
5
5
  // Anatomy is the HTML. A <menu id> is the surface; each <li> is an item,
6
6
  // an <hr> is a divider, and an <i> inside the item is the leading icon.
7
7
  // The trigger is still .dropdown-trigger[data-target] — that is the JS
8
- // contract. .dropdown-content is an alias (FormSelect and Autocomplete
9
- // still generate it).
8
+ // contract.
10
9
  //
11
10
  // Tokens follow https://m3.material.io/components/menus/specs
12
- // container surface, 4dp corners, elevation 2, 112–280dp
13
- // item 48dp, label-large / on-surface, 12dp inset
11
+ // plus the 1rem surface / stadium items used by fields and select.
12
+ // container surface, 1rem corners, elevation 2, 112–280dp
13
+ // item 48dp, 2rem corners, label-large / on-surface, 12dp inset
14
14
  // leading icon 24dp, on-surface-variant
15
15
  // divider 1dp outline-variant
16
16
  // state layer 8% hover / 10% selected
17
17
 
18
+ $_menu-chevron: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath fill='black' d='M10 6l6 6-6 6'/%3E%3C/svg%3E");
19
+
18
20
  [popover] {
19
21
  outline: none;
20
22
  padding: 0;
21
23
  border: none;
22
24
  }
23
25
 
24
- .dropdown-content,
25
26
  menu[id] {
26
27
  --md-comp-menu-container-color: var(--md-sys-color-surface);
27
- --md-comp-menu-container-shape: 4px;
28
+ --md-comp-menu-container-shape: 1rem;
28
29
  --md-comp-menu-item-container-height: 48px;
29
30
 
30
31
  box-sizing: border-box;
@@ -47,6 +48,7 @@ menu[id] {
47
48
  @include z-depth("2");
48
49
 
49
50
  > li {
51
+ box-sizing: border-box;
50
52
  margin: 0;
51
53
  padding: 0;
52
54
  width: 100%;
@@ -70,6 +72,11 @@ menu[id] {
70
72
  color: var(--md-sys-color-on-surface);
71
73
  text-decoration: none;
72
74
  cursor: pointer;
75
+
76
+ &:hover,
77
+ &:focus {
78
+ background: none;
79
+ }
73
80
  @include typescale("label-large");
74
81
  font-family: $font-stack;
75
82
  }
@@ -86,6 +93,12 @@ menu[id] {
86
93
  }
87
94
  }
88
95
 
96
+ > li:not(.disabled, .divider, .optgroup) {
97
+ width: calc(100% - 16px);
98
+ margin-inline: 8px;
99
+ border-radius: 2rem;
100
+ }
101
+
89
102
  > li:not(.disabled, .divider):hover,
90
103
  > li.active {
91
104
  background-color: color-mix(in oklab, var(--md-sys-color-on-surface) 8%, transparent);
@@ -108,22 +121,105 @@ menu[id] {
108
121
  background-color: var(--md-sys-color-outline-variant);
109
122
  cursor: default;
110
123
  }
124
+
125
+ // Nested flyouts sit outside the list box; do not clip them.
126
+ &:has(> li > menu) {
127
+ overflow: visible;
128
+ }
129
+
130
+ li:has(> menu) {
131
+ position: relative;
132
+ }
133
+
134
+ li:has(> menu) > :is(a, button, span)::after {
135
+ content: "";
136
+ flex: 0 0 24px;
137
+ width: 24px;
138
+ height: 24px;
139
+ margin-inline-start: auto;
140
+ background-color: var(--md-sys-color-on-surface-variant);
141
+ mask: $_menu-chevron center / 24px no-repeat;
142
+ }
143
+ }
144
+
145
+ // Nested menu. Optional, desktop-first. No second Dropdown instance —
146
+ // a <menu> inside an <li> is a flyout. Hover / :focus-within on fine
147
+ // pointers; .open is the tap / keyboard switch.
148
+ menu[id] li > menu {
149
+ --md-comp-menu-container-color: var(--md-sys-color-surface);
150
+ --md-comp-menu-container-shape: 1rem;
151
+ --md-comp-menu-item-container-height: 48px;
152
+
153
+ box-sizing: border-box;
154
+ display: none;
155
+ position: absolute;
156
+ inset-block-start: 0;
157
+ inset-inline-start: 100%;
158
+ z-index: 1;
159
+ min-width: 112px;
160
+ margin: 0;
161
+ padding: 8px 0;
162
+ list-style: none;
163
+ overflow: visible;
164
+ opacity: 1;
165
+ transform: none;
166
+ background-color: var(--md-comp-menu-container-color);
167
+ border-radius: var(--md-comp-menu-container-shape);
168
+ color: var(--md-sys-color-on-surface);
169
+ @include z-depth("2");
170
+
171
+ > li {
172
+ box-sizing: border-box;
173
+ min-height: var(--md-comp-menu-item-container-height);
174
+ }
175
+
176
+ > li:not(.disabled, .divider, .optgroup) {
177
+ width: calc(100% - 16px);
178
+ margin-inline: 8px;
179
+ border-radius: 2rem;
180
+ }
181
+
182
+ > li:not(.disabled, .divider):hover,
183
+ > li.active {
184
+ background-color: color-mix(in oklab, var(--md-sys-color-on-surface) 8%, transparent);
185
+ }
186
+
187
+ > li > :is(a, button, span) {
188
+ display: flex;
189
+ align-items: center;
190
+ gap: 12px;
191
+ box-sizing: border-box;
192
+ width: 100%;
193
+ min-height: var(--md-comp-menu-item-container-height);
194
+ padding: 0 12px;
195
+ color: var(--md-sys-color-on-surface);
196
+ text-decoration: none;
197
+ @include typescale("label-large");
198
+ font-family: $font-stack;
199
+ }
200
+ }
201
+
202
+ @media (hover: hover) and (pointer: fine) {
203
+ menu[id] li:is(:hover, :focus-within) > menu {
204
+ display: block;
205
+ }
206
+ }
207
+
208
+ menu[id] li.open > menu {
209
+ display: block;
210
+ }
211
+
212
+ menu[id] li.submenu-start > menu {
213
+ inset-inline-start: auto;
214
+ inset-inline-end: 100%;
111
215
  }
112
216
 
113
217
  body.keyboard-focused {
114
- .dropdown-content > li:focus,
115
218
  menu[id] > li:focus {
116
219
  background-color: color-mix(in oklab, var(--md-sys-color-on-surface) 10%, transparent);
117
220
  }
118
221
  }
119
222
 
120
- .input-field.col .dropdown-content [type="checkbox"] + label {
121
- top: 1px;
122
- left: 0;
123
- height: 18px;
124
- transform: none;
125
- }
126
-
127
223
  .dropdown-trigger {
128
224
  cursor: pointer;
129
225
  }
@@ -1,8 +1,81 @@
1
1
  @use "../abstracts" as *;
2
2
 
3
- /* This is needed for some mobile phones to display the Google Icon font properly */
4
- .material-icons, .material-symbols-outlined,
5
- .material-symbols-rounded, .material-symbols-sharp {
3
+ // Material Symbols. The framework does not ship the font files — outlined,
4
+ // rounded, and sharp are three separate variable fonts, and pulling all
5
+ // three into the main sheet would dwarf the rest of the CSS. Load the
6
+ // families you use (see the Icons docs). Outlined with every axis:
7
+ //
8
+ // fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@20..48,100..700,0..1,-50..200
9
+ //
10
+ // Style (outlined / rounded / sharp) is the font family, not a variation
11
+ // axis. Switch it with the `icon-style` attribute (same idea as `theme`)
12
+ // or by setting --md-icon-font. Fill, weight, grade, and optical size
13
+ // are axes and inherit as custom properties.
14
+
15
+ :root {
16
+ --md-icon-font-outlined: "Material Symbols Outlined";
17
+ --md-icon-font-rounded: "Material Symbols Rounded";
18
+ --md-icon-font-sharp: "Material Symbols Sharp";
19
+ --md-icon-font: var(--md-icon-font-outlined);
20
+ --md-icon-fill: 0;
21
+ --md-icon-weight: 400;
22
+ --md-icon-grade: 0;
23
+ --md-icon-optical-size: 24;
24
+ }
25
+
26
+ [icon-style="outlined"] { --md-icon-font: var(--md-icon-font-outlined); }
27
+ [icon-style="rounded"] { --md-icon-font: var(--md-icon-font-rounded); }
28
+ [icon-style="sharp"] { --md-icon-font: var(--md-icon-font-sharp); }
29
+
30
+ $_icon: ".material-symbols, .material-symbols-outlined, .material-symbols-rounded, .material-symbols-sharp, .material-icons";
31
+
32
+ #{$_icon} {
33
+ display: inline-block;
34
+ font-family: var(--md-icon-font);
35
+ font-weight: normal;
36
+ font-style: normal;
37
+ font-size: 24px;
38
+ line-height: 1;
39
+ letter-spacing: normal;
40
+ text-transform: none;
41
+ white-space: nowrap;
42
+ word-wrap: normal;
43
+ direction: ltr;
44
+ user-select: none;
6
45
  text-rendering: optimizeLegibility;
7
- font-feature-settings: 'liga';
46
+ font-feature-settings: "liga";
47
+ -webkit-font-smoothing: antialiased;
48
+ font-variation-settings:
49
+ "FILL" var(--md-icon-fill),
50
+ "wght" var(--md-icon-weight),
51
+ "GRAD" var(--md-icon-grade),
52
+ "opsz" var(--md-icon-optical-size);
53
+
54
+ // Nest so each class in $_icon gets the suffix. `#{$_icon}.tiny` would
55
+ // only attach `.tiny` to the last item and leave every other icon
56
+ // matching the bare `.material-symbols` selector — including `.large`.
57
+ &.tiny {
58
+ font-size: 1rem;
59
+ --md-icon-optical-size: 20;
60
+ }
61
+ &.small {
62
+ font-size: 2rem;
63
+ --md-icon-optical-size: 24;
64
+ }
65
+ &.medium {
66
+ font-size: 4rem;
67
+ --md-icon-optical-size: 40;
68
+ }
69
+ &.large {
70
+ font-size: 6rem;
71
+ --md-icon-optical-size: 48;
72
+ }
8
73
  }
74
+
75
+ // One-off family overrides. These only change --md-icon-font so the
76
+ // inherited fill / weight / grade / opsz still apply.
77
+ .material-symbols-outlined { --md-icon-font: var(--md-icon-font-outlined); }
78
+ .material-symbols-rounded { --md-icon-font: var(--md-icon-font-rounded); }
79
+ .material-symbols-sharp { --md-icon-font: var(--md-icon-font-sharp); }
80
+
81
+ .icon-filled { --md-icon-fill: 1; }
@@ -23,6 +23,7 @@
23
23
  @forward "page-footer";
24
24
  @forward "forms";
25
25
  @forward "table_of_contents";
26
+ @forward "scrollspy";
26
27
  @forward "sidenav";
27
28
  @forward "preloader";
28
29
  @forward "slider";
@@ -6,6 +6,10 @@
6
6
  position: relative;
7
7
  transition: opacity .4s;
8
8
  backface-visibility: hidden;
9
+ // Same 12px as .responsive-img. Exclude .circle so a disc stays a disc.
10
+ &:not(.circle) {
11
+ border-radius: 12px;
12
+ }
9
13
 
10
14
  &:hover {
11
15
  &:not(.active) {
@@ -4,8 +4,7 @@
4
4
  //
5
5
  // Anatomy is the HTML. A <dialog> is a basic dialog. A heading is the
6
6
  // headline, a <p> (or a wrapping <div>) is supporting text, and the last
7
- // child <form method="dialog"> or <nav> is the action row. .modal and
8
- // .modal-header / .modal-content / .modal-footer stay as aliases.
7
+ // child <form method="dialog"> or <nav> is the action row.
9
8
  //
10
9
  // Tokens follow https://m3.material.io/components/dialogs/specs
11
10
  // container surface, 28dp corners, 280–560dp, elevation 3
@@ -16,8 +15,7 @@
16
15
  // scrim 32% + 12px blur
17
16
  // padding 24dp
18
17
 
19
- dialog,
20
- .modal {
18
+ dialog {
21
19
  --md-comp-basic-dialog-container-color: var(--md-sys-color-surface);
22
20
  --md-comp-basic-dialog-container-shape: 28px;
23
21
  --md-comp-basic-dialog-container-min-width: 280px;
@@ -51,8 +49,7 @@ html:has(dialog:modal) {
51
49
  overflow: hidden;
52
50
  }
53
51
 
54
- dialog[open],
55
- .modal[open] {
52
+ dialog[open] {
56
53
  display: flex;
57
54
  flex-direction: column;
58
55
  overflow: hidden;
@@ -71,8 +68,7 @@ dialog > i:first-child {
71
68
  color: var(--md-sys-color-secondary);
72
69
  }
73
70
 
74
- dialog > :is(h1, h2, h3, h4, h5, h6),
75
- .modal-header {
71
+ dialog > :is(h1, h2, h3, h4, h5, h6) {
76
72
  flex: 0 0 auto;
77
73
  margin: 0;
78
74
  padding: 24px 24px 16px;
@@ -90,8 +86,7 @@ dialog > i:first-child + :is(h1, h2, h3, h4, h5, h6) {
90
86
  }
91
87
 
92
88
  dialog > p,
93
- dialog > div,
94
- .modal-content {
89
+ dialog > div {
95
90
  flex: 1 1 auto;
96
91
  min-height: 0;
97
92
  overflow: auto;
@@ -106,14 +101,12 @@ dialog > div,
106
101
  color: color-mix(in oklab, currentColor 72%, transparent);
107
102
  }
108
103
 
109
- dialog > :is(h1, h2, h3, h4, h5, h6) + p,
110
- .modal-header + .modal-content {
104
+ dialog > :is(h1, h2, h3, h4, h5, h6) + p {
111
105
  padding-top: 0;
112
106
  }
113
107
 
114
108
  // Action row. form[method=dialog] is the native close mechanism.
115
- dialog > :is(form, nav):last-child,
116
- .modal-footer {
109
+ dialog > :is(form, nav):last-child {
117
110
  display: flex;
118
111
  flex-wrap: wrap;
119
112
  justify-content: flex-end;
@@ -126,16 +119,11 @@ dialog > :is(form, nav):last-child,
126
119
  background: none;
127
120
  }
128
121
 
129
- .modal-close {
130
- cursor: pointer;
131
- }
132
-
133
122
  // Full-screen (M3). Takes the viewport; no corners.
134
123
  // UA `dialog:modal` sets max-width: calc(100% - 2em - 6px). Without
135
124
  // clearing it, width: 100% is capped and a strip of scrim shows on the
136
125
  // right. Bottom sheet already overrides this.
137
- dialog.max,
138
- .modal.max {
126
+ dialog.max {
139
127
  width: 100%;
140
128
  max-width: none;
141
129
  height: 100%;
@@ -149,8 +137,7 @@ dialog.max,
149
137
  // the edge against the viewport is square. `left` / `right` also float
150
138
  // via the helper utilities (`!important`), so sheets reset that.
151
139
  dialog.bottom,
152
- dialog.bottom-sheet,
153
- .modal.bottom-sheet {
140
+ dialog.bottom-sheet {
154
141
  width: 100%;
155
142
  min-width: 0;
156
143
  max-width: none;
@@ -161,8 +148,7 @@ dialog.bottom-sheet,
161
148
  }
162
149
 
163
150
  dialog.top,
164
- dialog.top-sheet,
165
- .modal.top-sheet {
151
+ dialog.top-sheet {
166
152
  width: 100%;
167
153
  min-width: 0;
168
154
  max-width: none;
@@ -174,10 +160,8 @@ dialog.top-sheet,
174
160
 
175
161
  dialog.left,
176
162
  dialog.left-sheet,
177
- .modal.left-sheet,
178
163
  dialog.right,
179
- dialog.right-sheet,
180
- .modal.right-sheet {
164
+ dialog.right-sheet {
181
165
  float: none !important;
182
166
  width: min(var(--md-comp-basic-dialog-sheet-width), 85%);
183
167
  max-width: none;
@@ -190,8 +174,7 @@ dialog.right-sheet,
190
174
  }
191
175
 
192
176
  dialog.left,
193
- dialog.left-sheet,
194
- .modal.left-sheet {
177
+ dialog.left-sheet {
195
178
  inset-inline-start: 0;
196
179
  inset-inline-end: auto;
197
180
  border-radius: 0 var(--md-comp-basic-dialog-container-shape)
@@ -199,8 +182,7 @@ dialog.left-sheet,
199
182
  }
200
183
 
201
184
  dialog.right,
202
- dialog.right-sheet,
203
- .modal.right-sheet {
185
+ dialog.right-sheet {
204
186
  inset-inline-start: auto;
205
187
  inset-inline-end: 0;
206
188
  border-radius: var(--md-comp-basic-dialog-container-shape) 0 0
@@ -63,7 +63,7 @@ header:has(> nav) {
63
63
  }
64
64
 
65
65
  // Icon-only actions — 48dp target, 24dp glyph, 4dp from the container edge.
66
- > nav:not(.tabs) > :is(a, button):not(.btn):not(.button):not(.btn-floating):not(.btn-large):not(.btn-small):has(> i:only-child) {
66
+ > nav:not(.tabs) > :is(a, button):not(.button):not(.circle):not(.extra):has(> i:only-child) {
67
67
  display: inline-flex;
68
68
  flex: 0 0 auto;
69
69
  align-items: center;
@@ -100,7 +100,7 @@ header:has(> nav) {
100
100
  }
101
101
 
102
102
  // Text destinations, either loose or grouped in a menu/list.
103
- > nav:not(.tabs) > a:not(:has(> i:only-child)):not(.btn):not(.button):not(.btn-floating):not(.breadcrumb) {
103
+ > nav:not(.tabs) > a:not(:has(> i:only-child)):not(.button):not(.circle):not(.breadcrumb) {
104
104
  @extend %app-bar-destination;
105
105
  }
106
106
 
@@ -117,11 +117,29 @@ header:has(> nav) {
117
117
  align-items: stretch;
118
118
  }
119
119
 
120
- a {
120
+ // Direct destinations only. Dropdown JS parks the menu next to the
121
+ // trigger, so a descendant `a` would also paint a square hover on
122
+ // top of the menu's pill.
123
+ > li > a {
121
124
  @extend %app-bar-destination;
122
125
  }
123
126
  }
124
127
 
128
+ // Breadcrumb trail. Layout only — colors stay on the breadcrumb tokens
129
+ // (which this header remaps to inherit). Do not treat the crumbs as
130
+ // destinations.
131
+ > nav:not(.tabs) > ol {
132
+ display: flex;
133
+ flex-wrap: wrap;
134
+ align-items: center;
135
+ align-self: center;
136
+ flex: 1 1 auto;
137
+ min-width: 0;
138
+ list-style: none;
139
+ margin: 0;
140
+ padding-inline: 12px;
141
+ }
142
+
125
143
  > nav:not(.tabs) > form {
126
144
  display: flex;
127
145
  flex: 1 1 auto;
@@ -278,7 +296,7 @@ header.fixed:has(> nav) {
278
296
  }
279
297
 
280
298
  // Bottom app bar. 80dp row, icons on the start, a FAB (any .btn) pushed to the end.
281
- footer:not(.page-footer):has(> nav) {
299
+ footer:has(> nav:only-child) {
282
300
  --md-comp-bottom-app-bar-container-color: var(--md-sys-color-surface);
283
301
  --md-comp-bottom-app-bar-container-height: 80px;
284
302
 
@@ -297,7 +315,7 @@ footer:not(.page-footer):has(> nav) {
297
315
  margin: 0;
298
316
  }
299
317
 
300
- > nav:not(.tabs) > :is(a, button):not(.btn):not(.button):not(.btn-floating):not(.btn-large):not(.btn-small):has(> i:only-child) {
318
+ > nav:not(.tabs) > :is(a, button):not(.button):not(.circle):not(.extra):has(> i:only-child) {
301
319
  display: inline-flex;
302
320
  flex: 0 0 auto;
303
321
  align-items: center;
@@ -333,7 +351,7 @@ footer:not(.page-footer):has(> nav) {
333
351
  }
334
352
  }
335
353
 
336
- > nav:not(.tabs) > :is(.btn, .btn-floating, .btn-large) {
354
+ > nav:not(.tabs) > :is(.button, .circle, .extra) {
337
355
  margin-inline-start: auto;
338
356
  }
339
357
 
@@ -1,27 +1,146 @@
1
1
  @use "../abstracts" as *;
2
2
 
3
- .page-footer {
3
+ // Site footer.
4
+ //
5
+ // Anatomy is the HTML. A <footer> is the landmark. A heading is a column
6
+ // title. A <nav> (or ul) is a link list. The last child <small> is the
7
+ // legal bar. A footer whose only child is a <nav> is the bottom app bar,
8
+ // not this component.
9
+ //
10
+ // Tokens:
11
+ // --md-comp-footer-padding-block 3rem
12
+ // --md-comp-footer-padding-inline 1rem
13
+ // --md-comp-footer-gap 2rem
14
+ // --md-comp-footer-legal-height 48px
15
+ //
16
+ // Unfilled by default (dashed hairline). .filled uses surface-container.
17
+ // There is no JavaScript.
18
+
19
+ footer:not(:has(> nav:only-child)) {
20
+ --md-comp-footer-padding-block: 3rem;
21
+ --md-comp-footer-padding-inline: 1rem;
22
+ --md-comp-footer-gap: 2rem;
23
+ --md-comp-footer-legal-height: 48px;
24
+
25
+ box-sizing: border-box;
4
26
  margin-top: 5rem;
5
- padding-top: 3rem;
27
+ padding: var(--md-comp-footer-padding-block) var(--md-comp-footer-padding-inline) 1rem;
6
28
  border-top: 1px dashed var(--md-sys-color-outline-variant);
29
+ color: var(--md-sys-color-on-surface);
30
+
7
31
  p {
8
- color: var(--md-sys-color-outline);
32
+ margin: 0 0 8px;
33
+ color: var(--md-sys-color-on-surface-variant);
34
+ @include typescale("body-medium");
35
+ font-family: $font-stack;
9
36
  }
10
- a {
37
+
38
+ a:not(.button, .circle, .extra) {
11
39
  color: var(--md-sys-color-primary);
40
+ text-decoration: none;
41
+ @include focus-visible;
42
+ }
43
+
44
+ :is(h1, h2, h3, h4, h5, h6) {
45
+ margin: 0 0 8px;
46
+ color: var(--md-sys-color-on-surface);
47
+ @include typescale("title-small");
48
+ font-family: $font-stack;
49
+ }
50
+
51
+ ul {
52
+ margin: 0;
53
+ padding: 0;
54
+ list-style: none;
55
+ }
56
+ }
57
+
58
+ footer:not(:has(> nav:only-child)).filled,
59
+ footer:not(:has(> nav:only-child)).surface {
60
+ background-color: var(--md-sys-color-surface-container);
61
+ border-top: 1px solid var(--md-sys-color-outline-variant);
62
+ }
63
+
64
+ // Columns: direct <nav> children. A wrapping .container is the grid
65
+ // when the author uses one. Legacy .row still spans the full measure.
66
+ footer:not(:has(> nav:only-child)) > .container,
67
+ footer:not(:has(> nav:only-child)):not(:has(> .container)) {
68
+ display: grid;
69
+ gap: var(--md-comp-footer-gap);
70
+ grid-template-columns: 1fr;
71
+
72
+ @include bp-up("small") {
73
+ grid-template-columns: repeat(2, 1fr);
74
+ }
75
+
76
+ @include bp-up("large") {
77
+ grid-template-columns: repeat(4, 1fr);
12
78
  }
13
- .footer-copyright,
14
- .footer-copyright a {
15
- overflow: hidden;
16
- min-height: 50px;
17
- display: flex;
18
- align-items: center;
19
- justify-content: space-between;
20
- padding: 10px 0px;
79
+
80
+ @include bp-up("xlarge") {
81
+ grid-template-columns: repeat(5, 1fr);
82
+ }
83
+ }
84
+
85
+ footer:not(:has(> nav:only-child)) > .container > .row,
86
+ footer:not(:has(> nav:only-child)) > .row {
87
+ grid-column: 1 / -1;
88
+ }
89
+
90
+ footer:not(:has(> nav:only-child)) nav {
91
+ display: flex;
92
+ flex-direction: column;
93
+ align-items: flex-start;
94
+ gap: 0;
95
+ min-width: 0;
96
+ }
97
+
98
+ footer:not(:has(> nav:only-child)) nav > a:not(.button, .circle, .extra),
99
+ footer:not(:has(> nav:only-child)) ul a:not(.button, .circle, .extra) {
100
+ display: inline-flex;
101
+ align-items: center;
102
+ box-sizing: border-box;
103
+ min-height: 48px;
104
+ padding: 0 12px;
105
+ margin-inline-start: -12px;
106
+ border-radius: 2rem;
107
+ @include typescale("label-large");
108
+ font-family: $font-stack;
109
+
110
+ &:hover {
111
+ background-color: color-mix(in oklab, var(--md-sys-color-on-surface) 8%, transparent);
112
+ }
113
+ }
114
+
115
+ footer:not(:has(> nav:only-child)) > small:last-child {
116
+ display: flex;
117
+ flex-wrap: wrap;
118
+ align-items: center;
119
+ justify-content: space-between;
120
+ gap: 8px 16px;
121
+ box-sizing: border-box;
122
+ min-height: var(--md-comp-footer-legal-height);
123
+ margin: 0;
124
+ padding-block: 12px;
125
+ grid-column: 1 / -1;
126
+ border-top: 1px solid var(--md-sys-color-outline-variant);
127
+ color: var(--md-sys-color-on-surface-variant);
128
+ @include typescale("body-small");
129
+ font-family: $font-stack;
130
+
131
+ a:not(.button) {
132
+ color: var(--md-sys-color-primary);
21
133
  }
22
134
  }
23
135
 
24
- .page-footer ul {
25
- padding-left: 0;
26
- list-style-type: none;
136
+ // Sticky: pin the footer to the bottom of a short page. Only when the
137
+ // landmark is a direct child of body — not a footer nested in a demo.
138
+ body:has(> footer:not(:has(> nav:only-child))) {
139
+ min-height: 100dvh;
140
+ display: flex;
141
+ flex-direction: column;
142
+ }
143
+
144
+ body:has(> footer:not(:has(> nav:only-child))) > main {
145
+ flex: 1 0 auto;
27
146
  }