@expressivecss/expressive 0.8.0 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/CHANGELOG.md +48 -1
  2. package/README.md +69 -26
  3. package/dist/css/expressive.css +657 -85
  4. package/dist/css/expressive.css.map +1 -1
  5. package/dist/css/expressive.min.css +1 -1
  6. package/dist/css/expressive.min.css.map +1 -1
  7. package/dist/fonts/LICENSE-material-symbols +93 -0
  8. package/dist/fonts/LICENSE-noto-sans +93 -0
  9. package/dist/fonts/LICENSE-roboto +93 -0
  10. package/dist/fonts/material-symbols-outlined.woff2 +0 -0
  11. package/dist/fonts/material-symbols-rounded.woff2 +0 -0
  12. package/dist/fonts/material-symbols-sharp.woff2 +0 -0
  13. package/dist/fonts/noto-sans-latin-400.woff2 +0 -0
  14. package/dist/fonts/noto-sans-latin-500.woff2 +0 -0
  15. package/dist/fonts/roboto-latin-400.woff2 +0 -0
  16. package/dist/fonts/roboto-latin-500.woff2 +0 -0
  17. package/dist/js/expressive.cjs +501 -144
  18. package/dist/js/expressive.cjs.map +4 -4
  19. package/dist/js/expressive.js +501 -144
  20. package/dist/js/expressive.js.map +4 -4
  21. package/dist/js/expressive.min.js +10 -10
  22. package/dist/js/expressive.min.js.map +4 -4
  23. package/dist/js/expressive.mjs +501 -144
  24. package/dist/js/expressive.mjs.map +4 -4
  25. package/dist/types/components/appBar.d.ts +26 -0
  26. package/dist/types/components/buttonGroup.d.ts +39 -0
  27. package/dist/types/components/cards.d.ts +11 -13
  28. package/dist/types/components/expandingCard.d.ts +1 -1
  29. package/dist/types/components/index.d.ts +2 -0
  30. package/dist/types/components/menu.d.ts +4 -0
  31. package/dist/types/components/registry.d.ts +10 -0
  32. package/dist/types/components/timepicker.d.ts +0 -1
  33. package/dist/types/components/tooltip.d.ts +1 -0
  34. package/dist/types/index.d.ts +1 -1
  35. package/package.json +25 -3
  36. package/src/sass/abstracts/_mixins.scss +52 -0
  37. package/src/sass/abstracts/_variables.scss +19 -0
  38. package/src/sass/base/_fonts.scss +63 -0
  39. package/src/sass/components/_badges.scss +1 -2
  40. package/src/sass/components/_button-groups.scss +252 -80
  41. package/src/sass/components/_cards.scss +108 -93
  42. package/src/sass/components/_expanding-card.scss +1 -35
  43. package/src/sass/components/_icon-buttons.scss +2 -24
  44. package/src/sass/components/_icons-material-design.scss +6 -6
  45. package/src/sass/components/_navbar.scss +180 -27
  46. package/src/sass/components/_navigation-bar.scss +9 -1
  47. package/src/sass/components/_navigation-rail.scss +9 -1
  48. package/src/sass/components/_transitions.scss +8 -1
  49. package/src/sass/components/forms/_select.scss +38 -0
  50. package/src/sass/expressive.scss +5 -0
@@ -6,19 +6,22 @@
6
6
  // Headings are the headline, icon-only links/buttons are the leading and
7
7
  // trailing actions, <menu> / <ul> hold text destinations. Helpers are only
8
8
  // for variants the markup cannot express: .center, .medium, .large, .fixed.
9
+ // `.medium` and `.large` are the Expressive flexible bars.
9
10
  // Destinations at the bottom of the screen are nav.navigation-bar, not this.
10
11
  //
11
- // Tokens follow the M3 top-app-bar spec
12
- // (https://m3.material.io/components/app-bars/specs):
13
- // container surface, 64dp small / 112 medium / 152 large
14
- // headline on-surface, title-large (small), headline-small/medium
12
+ // Tokens follow the M3 Expressive app bar spec
13
+ // (https://m3.material.io/components/app-bars/specs, Material Web v29.0.16):
14
+ // container surface, 64dp small / 112 medium-flexible /
15
+ // 120 large-flexible (136 / 152 with a subtitle)
16
+ // headline on-surface, title-large (small), headline-medium
17
+ // (medium-flexible), display-small (large-flexible)
18
+ // subtitle label-medium / label-large / title-medium in an <hgroup>
15
19
  // icons 24dp in a 48dp target, 4dp inset. The spec mutes the
16
20
  // trailing ones (on-surface-variant) and leaves the leading
17
21
  // one on-surface; both inherit here instead, so a fill +
18
22
  // on-* pair on the header stays readable. The two tokens
19
23
  // below are the opt-in to the spec colors.
20
- // medium/large headline 16dp from the inline edges, 20dp (medium) /
21
- // 28dp (large) above the container bottom
24
+ // medium/large headline 16dp from the inline edges; wrap to two lines
22
25
  // on scroll surface-container fill (not elevation - the spec's
23
26
  // level2 elevation carries a transparent shadow) so the bar
24
27
  // separates from the body. A color utility on the
@@ -46,6 +49,9 @@
46
49
  // the page as an app bar. Same trap as $_icon in _icons-material-design.
47
50
  // :is() also keeps the specificity where nav:not(...) had it.
48
51
  $_bar: ":is(nav:not(.tabs, .navigation-bar, .navigation-rail), .bar)";
52
+ $_heading: ":is(h1, h2, h3, h4, h5, h6)";
53
+ $_title: ":is(#{$_heading}, hgroup)";
54
+ $_search: ":is(.search-bar, .searchbar)";
49
55
 
50
56
  header:has(> :is(nav:not(.navigation-bar, .navigation-rail), .bar)) {
51
57
  --md-comp-top-app-bar-container-color: var(--md-sys-color-surface);
@@ -81,7 +87,7 @@ header:has(> :is(nav:not(.navigation-bar, .navigation-rail), .bar)) {
81
87
  margin: 0;
82
88
  }
83
89
 
84
- > #{$_bar} > :is(h1, h2, h3, h4, h5, h6) {
90
+ > #{$_bar} > #{$_title} {
85
91
  flex: 1 1 auto;
86
92
  min-width: 0;
87
93
  margin: 0;
@@ -95,6 +101,12 @@ header:has(> :is(nav:not(.navigation-bar, .navigation-rail), .bar)) {
95
101
  text-overflow: ellipsis;
96
102
  white-space: nowrap;
97
103
  color: inherit;
104
+ }
105
+
106
+ > #{$_bar} > #{$_heading},
107
+ > #{$_bar} > hgroup > #{$_heading} {
108
+ margin: 0;
109
+ color: inherit;
98
110
  font-family: var(--md-sys-typescale-title-large-font-family-name);
99
111
  font-size: var(--md-sys-typescale-title-large-font-size);
100
112
  font-weight: var(--md-sys-typescale-title-large-font-weight);
@@ -102,6 +114,25 @@ header:has(> :is(nav:not(.navigation-bar, .navigation-rail), .bar)) {
102
114
  letter-spacing: var(--md-sys-typescale-title-large-letter-spacing);
103
115
  }
104
116
 
117
+ > #{$_bar} > hgroup {
118
+ display: flex;
119
+ flex-direction: column;
120
+ justify-content: center;
121
+ gap: 0;
122
+ white-space: normal;
123
+ text-overflow: unset;
124
+ }
125
+
126
+ > #{$_bar} > hgroup > p {
127
+ margin: 0;
128
+ color: inherit;
129
+ font-family: var(--md-sys-typescale-label-medium-font-family-name);
130
+ font-size: var(--md-sys-typescale-label-medium-font-size);
131
+ font-weight: var(--md-sys-typescale-label-medium-font-weight);
132
+ line-height: var(--md-sys-typescale-label-medium-line-height);
133
+ letter-spacing: var(--md-sys-typescale-label-medium-letter-spacing);
134
+ }
135
+
105
136
  // Icon-only actions — 48dp target, 24dp glyph, 4dp from the container edge.
106
137
  // $icon, not `i`: the canonical icon is a <span class="material-symbols">, and
107
138
  // keying on the element meant every bar written the documented way lost this
@@ -150,7 +181,7 @@ header:has(> :is(nav:not(.navigation-bar, .navigation-rail), .bar)) {
150
181
  // anatomy, here as everywhere else in this file. Without this the trailing
151
182
  // token was declared and consumed by nothing, so the documented way to opt
152
183
  // into the spec's muted trailing icons did nothing at all.
153
- > #{$_bar} > :is(h1, h2, h3, h4, h5, h6) ~ :is(a, button):not(.button, .icon-button):not(.circle):not(.extra):has(> #{$icon}:only-child) {
184
+ > #{$_bar} > :is(#{$_title}, #{$_search}) ~ :is(a, button):not(.button, .icon-button):not(.circle):not(.extra):has(> #{$icon}:only-child) {
154
185
  color: var(--md-comp-top-app-bar-trailing-icon-color);
155
186
  }
156
187
 
@@ -226,6 +257,22 @@ header:has(> :is(nav:not(.navigation-bar, .navigation-rail), .bar)) {
226
257
  }
227
258
  }
228
259
  }
260
+
261
+ // Search app bar: the search component sits in the title slot. Icons
262
+ // beside it (siblings of the search) stay outside; icons inside it stay
263
+ // on the search-bar rules. Centered supporting text is the Expressive
264
+ // search-app-bar configuration.
265
+ > #{$_bar} > #{$_search} {
266
+ flex: 1 1 auto;
267
+ width: auto;
268
+ min-width: 0;
269
+ align-self: center;
270
+ margin-block: 4px;
271
+
272
+ input {
273
+ text-align: center;
274
+ }
275
+ }
229
276
  }
230
277
 
231
278
  %app-bar-destination {
@@ -251,12 +298,14 @@ header:has(> :is(nav:not(.navigation-bar, .navigation-rail), .bar)) {
251
298
  }
252
299
 
253
300
  // Center-aligned small bar. The headline is taken out of flow so leading and
254
- // trailing actions can sit on the edges without shifting the title.
255
- header.center:has(> #{$_bar}) > #{$_bar} {
301
+ // trailing actions can sit on the edges without shifting the title. Medium
302
+ // and large keep the title on the second row and center it there instead.
303
+ header.center:not(.medium, .large):has(> #{$_bar}) > #{$_bar},
304
+ header.center.collapsed:has(> #{$_bar}) > #{$_bar} {
256
305
  position: relative;
257
306
  justify-content: space-between;
258
307
 
259
- > :is(h1, h2, h3, h4, h5, h6) {
308
+ > #{$_title} {
260
309
  position: absolute;
261
310
  left: 50%;
262
311
  transform: translateX(-50%);
@@ -293,18 +342,28 @@ header.large:has(> #{$_bar}) > #{$_bar} {
293
342
  height: 48px;
294
343
  }
295
344
 
296
- > :is(h1, h2, h3, h4, h5, h6) {
345
+ > #{$_title} {
297
346
  order: 1;
298
347
  // Wider than the line, so it always breaks onto its own one - a basis of
299
348
  // exactly 100% still fits beside the zero-width spacer above, which put
300
349
  // the headline back on the icon row. It shrinks to fit once there.
301
350
  flex: 1 1 calc(100% + 1px);
302
351
  white-space: normal;
303
- overflow: visible;
352
+ overflow: hidden;
304
353
  text-overflow: unset;
305
354
  }
306
355
 
307
- > :is(h1, h2, h3, h4, h5, h6) + * {
356
+ > #{$_heading},
357
+ > hgroup > #{$_heading} {
358
+ display: -webkit-box;
359
+ -webkit-box-orient: vertical;
360
+ -webkit-line-clamp: 2;
361
+ line-clamp: 2;
362
+ overflow: hidden;
363
+ white-space: normal;
364
+ }
365
+
366
+ > #{$_title} + * {
308
367
  margin-inline-start: auto;
309
368
  }
310
369
  }
@@ -313,28 +372,122 @@ header.medium:has(> #{$_bar}) > #{$_bar} {
313
372
  min-height: 112px;
314
373
  padding-bottom: 20px;
315
374
 
316
- > :is(h1, h2, h3, h4, h5, h6) {
375
+ > #{$_title} {
317
376
  padding: 0 12px;
318
- font-family: var(--md-sys-typescale-headline-small-font-family-name);
319
- font-size: var(--md-sys-typescale-headline-small-font-size);
320
- font-weight: var(--md-sys-typescale-headline-small-font-weight);
321
- line-height: var(--md-sys-typescale-headline-small-line-height);
322
- letter-spacing: var(--md-sys-typescale-headline-small-letter-spacing);
323
377
  }
324
- }
325
-
326
- header.large:has(> #{$_bar}) > #{$_bar} {
327
- min-height: 152px;
328
- padding-bottom: 28px;
329
378
 
330
- > :is(h1, h2, h3, h4, h5, h6) {
331
- padding: 0 12px;
379
+ > #{$_heading},
380
+ > hgroup > #{$_heading} {
332
381
  font-family: var(--md-sys-typescale-headline-medium-font-family-name);
333
382
  font-size: var(--md-sys-typescale-headline-medium-font-size);
334
383
  font-weight: var(--md-sys-typescale-headline-medium-font-weight);
335
384
  line-height: var(--md-sys-typescale-headline-medium-line-height);
336
385
  letter-spacing: var(--md-sys-typescale-headline-medium-letter-spacing);
337
386
  }
387
+
388
+ > hgroup > p {
389
+ font-family: var(--md-sys-typescale-label-large-font-family-name);
390
+ font-size: var(--md-sys-typescale-label-large-font-size);
391
+ font-weight: var(--md-sys-typescale-label-large-font-weight);
392
+ line-height: var(--md-sys-typescale-label-large-line-height);
393
+ letter-spacing: var(--md-sys-typescale-label-large-letter-spacing);
394
+ }
395
+ }
396
+
397
+ header.medium:has(> #{$_bar} > hgroup > p) > #{$_bar} {
398
+ min-height: 136px;
399
+ }
400
+
401
+ header.large:has(> #{$_bar}) > #{$_bar} {
402
+ min-height: 120px;
403
+ padding-bottom: 20px;
404
+
405
+ > #{$_title} {
406
+ padding: 0 12px;
407
+ }
408
+
409
+ > #{$_heading},
410
+ > hgroup > #{$_heading} {
411
+ font-family: var(--md-sys-typescale-display-small-font-family-name);
412
+ font-size: var(--md-sys-typescale-display-small-font-size);
413
+ font-weight: var(--md-sys-typescale-display-small-font-weight);
414
+ line-height: var(--md-sys-typescale-display-small-line-height);
415
+ letter-spacing: var(--md-sys-typescale-display-small-letter-spacing);
416
+ }
417
+
418
+ > hgroup > p {
419
+ font-family: var(--md-sys-typescale-title-medium-font-family-name);
420
+ font-size: var(--md-sys-typescale-title-medium-font-size);
421
+ font-weight: var(--md-sys-typescale-title-medium-font-weight);
422
+ line-height: var(--md-sys-typescale-title-medium-line-height);
423
+ letter-spacing: var(--md-sys-typescale-title-medium-letter-spacing);
424
+ }
425
+ }
426
+
427
+ header.large:has(> #{$_bar} > hgroup > p) > #{$_bar} {
428
+ min-height: 152px;
429
+ padding-bottom: 28px;
430
+ }
431
+
432
+ header.center.medium:not(.collapsed):has(> #{$_bar}) > #{$_bar} > #{$_title},
433
+ header.center.large:not(.collapsed):has(> #{$_bar}) > #{$_bar} > #{$_title} {
434
+ text-align: center;
435
+ align-items: center;
436
+ }
437
+
438
+ // Collapse the flexible bars into the small 64dp row. AppBar toggles
439
+ // `.collapsed` when the page has scrolled; the class is also legal to set
440
+ // by hand. The two-row layout, the spacer, and the expanded type roles
441
+ // all have to go, or the 64dp height just clips the second row.
442
+ header.medium.collapsed:has(> #{$_bar}) > #{$_bar},
443
+ header.large.collapsed:has(> #{$_bar}) > #{$_bar} {
444
+ flex-wrap: nowrap;
445
+ align-content: center;
446
+ align-items: center;
447
+ min-height: 64px;
448
+ padding-top: 0;
449
+ padding-bottom: 0;
450
+
451
+ &::before {
452
+ display: none;
453
+ }
454
+
455
+ > #{$_title} {
456
+ order: 0;
457
+ flex: 1 1 auto;
458
+ white-space: nowrap;
459
+ overflow: hidden;
460
+ text-overflow: ellipsis;
461
+ }
462
+
463
+ > #{$_title} + * {
464
+ margin-inline-start: 0;
465
+ }
466
+
467
+ > #{$_heading},
468
+ > hgroup > #{$_heading} {
469
+ display: block;
470
+ padding: 0;
471
+ -webkit-line-clamp: unset;
472
+ line-clamp: unset;
473
+ white-space: nowrap;
474
+ overflow: hidden;
475
+ text-overflow: ellipsis;
476
+ font-family: var(--md-sys-typescale-title-large-font-family-name);
477
+ font-size: var(--md-sys-typescale-title-large-font-size);
478
+ font-weight: var(--md-sys-typescale-title-large-font-weight);
479
+ line-height: var(--md-sys-typescale-title-large-line-height);
480
+ letter-spacing: var(--md-sys-typescale-title-large-letter-spacing);
481
+ }
482
+
483
+ > hgroup > p {
484
+ padding: 0;
485
+ font-family: var(--md-sys-typescale-label-medium-font-family-name);
486
+ font-size: var(--md-sys-typescale-label-medium-font-size);
487
+ font-weight: var(--md-sys-typescale-label-medium-font-weight);
488
+ line-height: var(--md-sys-typescale-label-medium-line-height);
489
+ letter-spacing: var(--md-sys-typescale-label-medium-letter-spacing);
490
+ }
338
491
  }
339
492
 
340
493
  // Pin the bar. On scroll, fill with surface-container so it separates
@@ -109,8 +109,12 @@
109
109
  position: absolute;
110
110
  z-index: 1;
111
111
  top: calc(4px + (var(--md-comp-nav-bar-indicator-height) - 24px) / 2);
112
- left: calc(50% + 12px);
112
+ inset-inline-start: calc(50% + 12px);
113
113
  transform: translate(-50%, -50%);
114
+
115
+ &:dir(rtl) {
116
+ transform: translate(50%, -50%);
117
+ }
114
118
  }
115
119
  }
116
120
 
@@ -137,6 +141,10 @@
137
141
  left: auto;
138
142
  inset-inline-start: calc(16px + 24px);
139
143
  transform: translate(-50%, -50%);
144
+
145
+ &:dir(rtl) {
146
+ transform: translate(50%, -50%);
147
+ }
140
148
  }
141
149
 
142
150
  &:hover:not(.active, [aria-current]) {
@@ -173,8 +173,12 @@
173
173
  position: absolute;
174
174
  z-index: 1;
175
175
  top: calc(6px + (var(--md-comp-nav-rail-indicator-height) - 24px) / 2);
176
- left: calc(50% + 12px);
176
+ inset-inline-start: calc(50% + 12px);
177
177
  transform: translate(-50%, -50%);
178
+
179
+ &:dir(rtl) {
180
+ transform: translate(50%, -50%);
181
+ }
178
182
  }
179
183
  }
180
184
 
@@ -260,6 +264,10 @@
260
264
  left: auto;
261
265
  inset-inline-start: calc(16px + 24px);
262
266
  transform: translate(-50%, -50%);
267
+
268
+ &:dir(rtl) {
269
+ transform: translate(50%, -50%);
270
+ }
263
271
  }
264
272
  }
265
273
 
@@ -11,4 +11,11 @@
11
11
  &.scale-in {
12
12
  transform: scale(1);
13
13
  }
14
- }
14
+ }
15
+
16
+ @media (prefers-reduced-motion: reduce) {
17
+ .scale-transition,
18
+ .scale-transition.scale-out {
19
+ transition: none !important;
20
+ }
21
+ }
@@ -132,6 +132,44 @@ select.browser-default {
132
132
  }
133
133
  }
134
134
 
135
+ // Filled selects always have a floated label. Put that label in a real row
136
+ // so translated wrapping and enlarged text reserve space above the value.
137
+ .field:has(> .hide-select):has(> label):not(.outlined) {
138
+ display: grid;
139
+
140
+ > label {
141
+ grid-row: 1;
142
+ background-color: var(--md-comp-filled-text-field-container-color);
143
+ border-start-start-radius: inherit;
144
+ border-start-end-radius: inherit;
145
+ position: static;
146
+ transform: none;
147
+ margin: 0;
148
+ padding: 8px 16px 0;
149
+ padding-inline-end: 52px;
150
+ overflow-wrap: anywhere;
151
+ pointer-events: auto;
152
+ @include typescale("body-small");
153
+ }
154
+
155
+ &:has(> i:first-of-type:not(.suffix):not(:last-child), > i.prefix, > .prefix) > label {
156
+ padding-left: 52px;
157
+ }
158
+
159
+ &.disabled > label {
160
+ background-color: color-mix(in oklab, var(--md-sys-color-on-surface) 4%, transparent);
161
+ }
162
+
163
+ > input.menu-trigger {
164
+ grid-row: 2;
165
+ height: auto;
166
+ min-height: calc(var(--md-sys-typescale-body-large-line-height) + 8px);
167
+ padding-block: 0 7px;
168
+ border-start-start-radius: 0;
169
+ border-start-end-radius: 0;
170
+ }
171
+ }
172
+
135
173
  .field:has(> .hide-select).disabled {
136
174
  .caret {
137
175
  background-color: color-mix(in oklab, var(--md-sys-color-on-surface) 38%, transparent);
@@ -41,6 +41,11 @@
41
41
 
42
42
  @forward "abstracts";
43
43
 
44
+ // @font-face must stay unlayered. A face declared inside @layer base can
45
+ // lose to any unlayered consumer face, or be ignored by engines that still
46
+ // treat layered faces as experimental.
47
+ @include meta.load-css("base/fonts");
48
+
44
49
  @layer tokens, base, components, utilities;
45
50
 
46
51
  @layer tokens {