maverick-wave 6.6.0 → 6.8.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/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "maverick-wave",
3
- "version": "6.6.0",
3
+ "version": "6.8.0",
4
4
  "config": {
5
- "version_short": "6.6"
5
+ "version_short": "6.8"
6
6
  },
7
7
  "description": "A lightweight, modern CSS framework for building responsive websites with elegance and speed.",
8
8
  "main": "src/js/main.js",
@@ -114,12 +114,21 @@
114
114
  // rather than off the end.
115
115
  $_radius-floor: map.get(v.$radius, 'xs');
116
116
 
117
+ // The sharp half of the signature on a value of its own, so the scale can round
118
+ // controls up without softening the one corner meant to read as sharp
119
+ $_radius-sharp-base: 5px;
120
+
117
121
  // Emitted through --mw-radius-scale, so one number retunes every rounded box in
118
122
  // the framework at once - 0 squares them off, above 1 rounds them further, and
119
123
  // the corner arc follows. 'none' and 'full' pass through: an explicit 0 stays
120
- // 0, and a circle has to stay a circle.
124
+ // 0, and a circle has to stay a circle. A length instead of a key takes the
125
+ // same scale and floor, for the few values that sit off the scale.
121
126
  @function radius($key) {
122
- $value: map.get(v.$radius, $key);
127
+ $value: $key;
128
+
129
+ @if meta.type-of($key) != 'number' {
130
+ $value: map.get(v.$radius, $key);
131
+ }
123
132
 
124
133
  @if $key == 'none' or $key == 'full' {
125
134
  @return $value;
@@ -137,7 +146,21 @@ $_radius-floor: map.get(v.$radius, 'xs');
137
146
 
138
147
  @return max(
139
148
  #{$_radius-floor},
140
- calc(#{map.get(v.$radius, 'sm')} * #{$scale} * #{$scale})
149
+ calc(#{$_radius-sharp-base} * #{$scale} * #{$scale})
150
+ );
151
+ }
152
+
153
+ // Where a floating island sits: percentages on a fixed box are the viewport's,
154
+ // so this puts the header's content box on the page container,
155
+ // $header-island-reach wider on either side - and the bar at the foot on the
156
+ // same edges. Logo and links stay put while the header docks around them.
157
+ @function island-edge() {
158
+ @return max(
159
+ calc(var(--mw-header-inset) - #{v.$header-island-reach}),
160
+ calc(
161
+ (100% - var(--mw-container-width)) / 2 - var(--mw-header-pad) -
162
+ #{v.$header-island-reach}
163
+ )
141
164
  );
142
165
  }
143
166
 
@@ -276,6 +276,15 @@
276
276
  }
277
277
  }
278
278
 
279
+ // The floating chrome, shared by the header island and the action bar island
280
+ @mixin island-surface {
281
+ background: var(--mw-header-glass);
282
+ backdrop-filter: blur(20px) saturate(1.6);
283
+ border: 1px solid var(--mw-header-island-border);
284
+ border-radius: radius('xl');
285
+ box-shadow: var(--mw-elevation-4);
286
+ }
287
+
279
288
  // The MaverickWave silhouette: sharp corners on one diagonal, round on the other,
280
289
  // with an accent arc on the round pair - the asymmetry alone reads as an accident.
281
290
  //
@@ -352,14 +361,20 @@ $_corner-opaque: 55%;
352
361
  );
353
362
  }
354
363
 
355
- // The arc alone, for a variant that redraws it without restating the shape
356
- @mixin corner-accent($border-width: 1px, $corners: 'both') {
364
+ // The arc alone, for a variant that redraws it without restating the shape.
365
+ // The radii are only for a box too small to carry the surface's 20px.
366
+ @mixin corner-accent(
367
+ $border-width: 1px,
368
+ $corners: 'both',
369
+ $sharp-radius: radius-sharp(),
370
+ $round-radius: radius('xl')
371
+ ) {
357
372
  // max(), not a bare subtraction: --mw-radius-scale can take the radius to 0,
358
373
  // and while a negative border-radius is clamped for us, a negative circle in
359
374
  // the mask below would throw the whole gradient away and lose the arc.
360
375
  $width: _accent-width($border-width);
361
- $sharp: max(0px, calc(#{radius-sharp()} - #{$width}));
362
- $round: max(0px, calc(#{radius('xl')} - #{$width}));
376
+ $sharp: max(0px, calc(#{$sharp-radius} - #{$width}));
377
+ $round: max(0px, calc(#{$round-radius} - #{$width}));
363
378
  // Opaque to where the curve ends, fading from there, so the arc runs into the
364
379
  // straight edge instead of stopping at the tangent. 1.8 is bounded by the
365
380
  // shortest surface: past about 2 the two arcs meet on a 50px box and read as
@@ -426,10 +426,25 @@ $header-colors: (
426
426
  'burgerbutton-color': token('primary-accent-text-color'),
427
427
  'burgerbutton-open-color': token('secondary-accent-text-color'),
428
428
  'border': fade('header-navbar-list-active-color', 40%),
429
+ // Translucent enough for the blur to show, dense enough that the light ink
430
+ // still reads over a white page
431
+ 'glass': fade('header-background', 88%),
432
+ 'island-border': fade('header-text-color', 13%),
433
+ 'navbar-mark': fade('header-navbar-list-active-color', 16%),
429
434
  );
430
- // Drives bar height, anchor scroll offset and drawer padding. Intrinsic height is
431
- // 59px: 42px logo + 2 × spacing('3') + 1px border.
432
- $header-height: 62px;
435
+ // The room the header takes from the top of the viewport - island plus the gap
436
+ // above it - so every offset that reads it clears the island without knowing it
437
+ // floats. The island itself is 63px: a 44px touch control + 2 × spacing('2') +
438
+ // 2px border, with room to spare around the 42px logo.
439
+ $header-height: 77px;
440
+ // Between the island and the viewport edge
441
+ $header-inset: 14px;
442
+ // How far the island reaches past the page container on either side, and in
443
+ // from the inset where that is closer: room for the links, at the price of the
444
+ // logo standing that much left of the text below it
445
+ $header-island-reach: 5px;
446
+ // The flat bar under mw-header-bar, at the height it had before the island
447
+ $header-bar-height: 62px;
433
448
 
434
449
  // Here and not next to the typography rules, because this is the file
435
450
  // `@use 'main' with (...)` configures. No font is bundled.
@@ -522,14 +537,20 @@ $breakpoints: (
522
537
  $radius: (
523
538
  'none': 0,
524
539
  'xs': 2px,
525
- 'sm': 5px,
526
- 'md': 10px,
540
+ 'sm': 7px,
541
+ 'md': 12px,
527
542
  'lg': 15px,
528
543
  'xl': 20px,
529
544
  '2xl': 30px,
530
545
  'full': 50%,
531
546
  );
532
547
 
548
+ // Off the scale on purpose: a chip next to a tag stays tighter than the buttons
549
+ // and fields it would otherwise be mistaken for. Outside the map, or every key
550
+ // would turn into a mw-radius-* utility.
551
+ $chip-radius: 10px;
552
+ $chip-radius-sm: 5px;
553
+
533
554
  // Z-index
534
555
  $z-index: (
535
556
  'lift': 3,
@@ -131,6 +131,7 @@
131
131
 
132
132
  // header height
133
133
  --mw-header-height: #{$header-height};
134
+ --mw-header-inset: #{$header-inset};
134
135
 
135
136
  // Read by the anchor offset of anything the sticky section nav can cover.
136
137
  // The strip at its tallest: a 2.75rem button under a thumb plus its two
@@ -1,5 +1,11 @@
1
1
  @use '../abstracts' as *;
2
2
 
3
+ // Clear of the home indicator where there is one, the header's gap where not
4
+ $_island-bottom: max(var(--mw-header-inset), env(safe-area-inset-bottom, 0px));
5
+ // Lighter than the flat bar, and as low as it goes: the items keep their
6
+ // 2.75rem touch floor, so the rest is only the padding around them
7
+ $_island-height: calc(var(--mw-action-bar-height) - 4px);
8
+
3
9
  @layer mw.components {
4
10
  :root {
5
11
  --mw-action-bar-height: 3.75rem;
@@ -18,6 +24,10 @@
18
24
  }
19
25
  }
20
26
 
27
+ :root:has(.mw-action-bar-island:not(.mw-action-bar-embedded)) {
28
+ --mw-action-bar-reserve: calc(#{$_island-height} + #{$_island-bottom});
29
+ }
30
+
21
31
  // Chrome like the header, so it takes the header's colours in both themes
22
32
  .mw-action-bar {
23
33
  --mw-focus-ring-color: var(--mw-header-navbar-list-active-color);
@@ -44,6 +54,19 @@
44
54
  position: absolute;
45
55
  }
46
56
 
57
+ &-island {
58
+ inset-inline: island-edge();
59
+ bottom: $_island-bottom;
60
+ min-height: $_island-height;
61
+ padding: spacing('1');
62
+ @include island-surface;
63
+
64
+ // Its own frame has no page container to line up with
65
+ &.mw-action-bar-embedded {
66
+ inset-inline: calc(var(--mw-header-inset) - #{$header-island-reach});
67
+ }
68
+ }
69
+
47
70
  &-item {
48
71
  display: flex;
49
72
  flex-direction: column;
@@ -129,6 +152,12 @@
129
152
  justify-content: center;
130
153
  grid-auto-columns: minmax(0, 10rem);
131
154
  }
155
+
156
+ // A dock: a container-wide island would float the targets in empty glass
157
+ .mw-action-bar-island {
158
+ width: fit-content;
159
+ margin-inline: auto;
160
+ }
132
161
  }
133
162
 
134
163
  @media print {
@@ -54,6 +54,8 @@
54
54
  .mw-announcement-static {
55
55
  // Relative for the close button, without a z-index lifting it over nearby menus
56
56
  position: relative;
57
+ // The fixed ribbon's offsets would shift a relative one by the header height
58
+ inset: auto;
57
59
  z-index: auto;
58
60
  }
59
61
 
@@ -95,7 +95,7 @@
95
95
  &-language {
96
96
  font-size: font-size('2xs');
97
97
  padding: spacing('1');
98
- border-radius: radius('sm');
98
+ border-radius: radius($chip-radius-sm);
99
99
 
100
100
  @include media-up('sm') {
101
101
  font-size: font-size('xs');
@@ -11,6 +11,12 @@
11
11
  background: var(--mw-warning-color);
12
12
  z-index: z-index('modal')-10;
13
13
  animation: mw-pulse-indicator motion(4s) infinite;
14
+
15
+ // Clear of the island's rounded corners
16
+ .mw-header > & {
17
+ inset-inline: radius('xl');
18
+ width: auto;
19
+ }
14
20
  }
15
21
 
16
22
  // Keyframes for the pulsing indicator
@@ -13,7 +13,7 @@
13
13
  text-decoration: none;
14
14
  background: var(--mw-card-background);
15
15
  border: 1px solid var(--mw-border);
16
- border-radius: radius('md');
16
+ border-radius: radius($chip-radius);
17
17
  padding: spacing('2');
18
18
  transition: var(--mw-transition-fast);
19
19
  color: var(--mw-text-color);
@@ -55,7 +55,7 @@
55
55
  padding: spacing('1') spacing('2');
56
56
  min-width: 32px;
57
57
  min-height: 32px;
58
- border-radius: radius('sm');
58
+ border-radius: radius($chip-radius-sm);
59
59
 
60
60
  .mw-techstack-logo {
61
61
  width: 18px;
@@ -78,7 +78,7 @@
78
78
 
79
79
  &-logo {
80
80
  object-fit: contain;
81
- border-radius: radius('sm');
81
+ border-radius: radius($chip-radius-sm);
82
82
  transition: transform var(--mw-duration-fast) var(--mw-ease-out);
83
83
  flex-shrink: 0;
84
84
  }
@@ -1,11 +1,57 @@
1
1
  @use '../abstracts' as *;
2
2
 
3
+ // Short enough that the bar is the island by the time the hero's headline is gone
4
+ $dock-range: 160px;
5
+
3
6
  // Carried by the float and not by the class, or the ring would still be there in
4
7
  // the arrived bar. Fixed values and not the elevation ramp: this lands on a
5
8
  // picture, not on a theme surface, and the light theme cuts its shadows for paper.
6
9
  $float-edge: (0 0 0 1px rgb(255 255 255 / 0.22), 0 4px 14px rgb(0 0 0 / 0.4));
7
10
 
8
11
  @layer mw.layout {
12
+ :root:not(.mw-header-bar) {
13
+ // A header that stays put starts as the full bar and docks into the island over
14
+ // the first stretch of scroll. A revealing one is off screen up there anyway and
15
+ // arrives as the island directly. Without scroll timelines it is the island
16
+ // from the start. The card under the island docks with it, so a menu opened at
17
+ // the top hangs from the bar edge to edge like the bar itself.
18
+ @supports (animation-timeline: scroll()) {
19
+ .mw-header:not(.mw-header-reveal) {
20
+ @include motion-safe {
21
+ animation-name: mw-header-dock;
22
+ animation-timing-function: var(--mw-ease-out);
23
+ animation-fill-mode: both;
24
+ animation-timeline: scroll(root);
25
+ animation-range: 0 $dock-range;
26
+ }
27
+
28
+ // In the row the nav is static and has no radius, so the same keyframes
29
+ // pass over it without a trace
30
+ .mw-navbar {
31
+ @include motion-safe {
32
+ animation-name: mw-header-dock-sheet;
33
+ animation-timing-function: var(--mw-ease-out);
34
+ animation-fill-mode: both;
35
+ animation-timeline: scroll(root);
36
+ animation-range: 0 $dock-range;
37
+ }
38
+ }
39
+ }
40
+
41
+ // The ribbon hangs from the island, so it docks with it
42
+ body:not(:has(.mw-header-reveal))
43
+ .mw-announcement:not(.mw-announcement-static) {
44
+ @include motion-safe {
45
+ animation-name: mw-announcement-dock;
46
+ animation-timing-function: var(--mw-ease-out);
47
+ animation-fill-mode: both;
48
+ animation-timeline: scroll(root);
49
+ animation-range: 0 $dock-range;
50
+ }
51
+ }
52
+ }
53
+ }
54
+
9
55
  // The bar starts above the screen and rides in over the first stretch of scroll.
10
56
  // Its own file, and not next to .mw-header: that one uses the `animation`
11
57
  // shorthand, which a consumer bundler folds together with a timeline into a
@@ -77,7 +123,7 @@ $float-edge: (0 0 0 1px rgb(255 255 255 / 0.22), 0 4px 14px rgb(0 0 0 / 0.4));
77
123
  .mw-announcement:focus-within
78
124
  ) {
79
125
  .mw-header-reveal {
80
- top: 0 !important;
126
+ top: var(--mw-header-inset) !important;
81
127
  opacity: 1 !important;
82
128
  }
83
129
 
@@ -124,11 +170,13 @@ $float-edge: (0 0 0 1px rgb(255 255 255 / 0.22), 0 4px 14px rgb(0 0 0 / 0.4));
124
170
  // earlier - together the bar would slide away in front of the reader.
125
171
  .mw-header-reveal.mw-header-away {
126
172
  @include motion-safe {
127
- top: calc(-1 * var(--mw-header-reveal-offset));
173
+ top: calc(var(--mw-header-inset) - var(--mw-header-reveal-offset));
128
174
  opacity: 0;
129
175
 
176
+ // Visible through the slide, so its shadow would hang into the hero
130
177
  &:has(.mw-header-keep) {
131
178
  opacity: 1;
179
+ box-shadow: none;
132
180
  }
133
181
 
134
182
  .mw-header-keep {
@@ -177,25 +225,63 @@ $float-edge: (0 0 0 1px rgb(255 255 255 / 0.22), 0 4px 14px rgb(0 0 0 / 0.4));
177
225
  @keyframes mw-header-reveal {
178
226
  0%,
179
227
  30% {
180
- top: calc(-1 * var(--mw-header-reveal-offset));
228
+ top: calc(var(--mw-header-inset) - var(--mw-header-reveal-offset));
181
229
  opacity: 0;
182
230
  }
183
231
 
184
232
  100% {
185
- top: 0;
233
+ top: var(--mw-header-inset);
186
234
  opacity: 1;
187
235
  }
188
236
  }
189
237
 
190
- // The same ride without the fade, for a bar something stays visible through
238
+ // The same ride without the fade, for a bar something stays visible through -
239
+ // and without the shadow while it is up there, or it hangs into the hero
191
240
  @keyframes mw-header-slide {
192
241
  0%,
193
242
  30% {
194
- top: calc(-1 * var(--mw-header-reveal-offset));
243
+ top: calc(var(--mw-header-inset) - var(--mw-header-reveal-offset));
244
+ box-shadow: none;
195
245
  }
196
246
 
197
247
  100% {
248
+ top: var(--mw-header-inset);
249
+ }
250
+ }
251
+
252
+ // Only the start: the end is the island the header already is
253
+ @keyframes mw-header-dock {
254
+ from {
198
255
  top: 0;
256
+ left: 0;
257
+ right: 0;
258
+ min-height: var(--mw-header-height);
259
+ // Percentages of the viewport here, so the content box is the page
260
+ // container in both states and the logo does not move sideways
261
+ padding-inline: calc(
262
+ (100% - var(--mw-container-width)) / 2 - #{$header-island-reach}
263
+ );
264
+ border-radius: 0;
265
+ border-color: transparent transparent var(--mw-header-border);
266
+ background-color: var(--mw-header-background);
267
+ box-shadow: none;
268
+ }
269
+ }
270
+
271
+ @keyframes mw-header-dock-sheet {
272
+ from {
273
+ top: 100%;
274
+ border-radius: 0;
275
+ }
276
+ }
277
+
278
+ @keyframes mw-announcement-dock {
279
+ from {
280
+ left: 0;
281
+ right: 0;
282
+ border-radius: 0;
283
+ border-color: transparent;
284
+ box-shadow: none;
199
285
  }
200
286
  }
201
287