maverick-wave 6.5.0 → 6.7.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.
@@ -3,23 +3,71 @@
3
3
  // Shared by the three controls in .mw-header-actions
4
4
  $_control-size: 40px;
5
5
 
6
+ // The surface signature at link size - the surface's 20px arc would close into a
7
+ // frame on a box this short
8
+ $_mark-round: radius('lg');
9
+
10
+ // Where the island sits: percentages on a fixed box are the viewport's, so this
11
+ // puts its content box on the page container, $header-island-reach wider on
12
+ // either side. Logo and links stay put while the bar docks around them.
13
+ $_island-edge: max(
14
+ calc(var(--mw-header-inset) - #{$header-island-reach}),
15
+ calc(
16
+ (100% - var(--mw-container-width)) / 2 - var(--mw-header-pad) -
17
+ #{$header-island-reach}
18
+ )
19
+ );
20
+
21
+ @mixin navbar-mark-shape {
22
+ border-radius: radius-sharp() $_mark-round radius-sharp() $_mark-round;
23
+
24
+ :root.mw-corners-even & {
25
+ border-radius: radius('md');
26
+ }
27
+ }
28
+
29
+ // The arc pair on the mark. Set on the pseudo-element itself and not on the
30
+ // header, or every surface inside the bar would take the bar's accent.
31
+ @mixin navbar-mark-arcs {
32
+ @include corner-accent(0, 'both', radius-sharp(), $_mark-round);
33
+ --mw-corner-accent: var(--mw-header-navbar-list-active-color);
34
+
35
+ :root.mw-corners-even & {
36
+ @include corner-accent(0, 'top', radius('md'), radius('md'));
37
+ }
38
+ }
39
+
6
40
  @layer mw.layout {
41
+ // The island's side padding. On the root and not on the header, so the ribbon
42
+ // hanging under the island can find the same edges.
43
+ :root {
44
+ --mw-header-pad: #{spacing('4')};
45
+
46
+ @include media-up('md') {
47
+ --mw-header-pad: #{spacing('5')};
48
+ }
49
+ }
50
+
7
51
  .mw-header {
8
52
  position: fixed;
9
- top: spacing('0');
10
- left: spacing('0');
11
- width: 100%;
53
+ top: var(--mw-header-inset);
54
+ left: $_island-edge;
55
+ right: $_island-edge;
12
56
  z-index: z-index('header');
13
57
  // Dark in both themes, fixed light included, where no light-dark() flips
14
58
  --mw-focus-ring-color: var(--mw-header-navbar-list-active-color);
15
- background: var(--mw-header-background);
16
- backdrop-filter: blur(7px);
17
- border-bottom: 1px solid var(--mw-header-border);
18
- padding: spacing('3') spacing('0');
59
+ background: var(--mw-header-glass);
60
+ backdrop-filter: blur(20px) saturate(1.6);
61
+ border: 1px solid var(--mw-header-island-border);
62
+ border-radius: radius('xl');
63
+ box-shadow: var(--mw-elevation-4);
64
+ // The height comes from min-height; the padding only has to let a control
65
+ // grown to its 2.75rem touch floor fit inside it
66
+ padding: spacing('2') var(--mw-header-pad);
19
67
  transition: var(--mw-transition);
20
68
  display: flex;
21
69
  align-items: center;
22
- min-height: var(--mw-header-height);
70
+ min-height: calc(var(--mw-header-height) - var(--mw-header-inset));
23
71
 
24
72
  // A component host (Angular, React) sitting between the header and its
25
73
  // container would become the flex item and shrink to content width, leaving
@@ -31,7 +79,9 @@ $_control-size: 40px;
31
79
  min-width: 0;
32
80
  }
33
81
 
82
+ // The island already sits on the page container, see $_island-edge
34
83
  .mw-container {
84
+ width: 100%;
35
85
  display: flex;
36
86
  justify-content: space-between;
37
87
  align-items: center;
@@ -213,41 +263,37 @@ $_control-size: 40px;
213
263
  }
214
264
  }
215
265
 
216
- // The panel rides under the bar, not beside it. A drawer pinned to the right
217
- // edge runs into the logo: 250px of panel against a 180px logo leaves 60px of
218
- // overlap on a phone, and .mw-logo sits higher in the header's stacking
219
- // context, so the logo prints on top of the menu. Under the bar there is
220
- // nothing to collide with, at any logo width.
266
+ // The panel hangs under the island as a card of its own, not beside it. A
267
+ // drawer pinned to the right edge runs into the logo: 250px of panel against a
268
+ // 180px logo leaves 60px of overlap on a phone, and .mw-logo sits higher in the
269
+ // header's stacking context, so the logo prints on top of the menu. Under the
270
+ // island there is nothing to collide with, at any logo width.
221
271
  //
222
272
  // position: absolute against the header and not fixed against the viewport:
223
273
  // the header carries a backdrop-filter, which makes it the containing block
224
274
  // for a fixed child anyway - at that point the anchoring is only nominally to
225
- // the viewport, and every place the bar moves has to be undone by hand. Bound
226
- // to the bar it simply travels with it.
275
+ // the viewport, and every place the island moves has to be undone by hand.
276
+ // Bound to it, the card simply travels with it.
227
277
  @mixin navbar-top-sheet {
228
278
  position: absolute;
229
- top: 100%;
230
- left: spacing('0');
231
- right: spacing('0');
279
+ top: calc(100% + #{spacing('3')});
280
+ // Out to the island's outer edge, past its border
281
+ left: -1px;
282
+ right: -1px;
232
283
  width: auto;
233
284
  height: auto;
285
+ // Opaque: a backdrop-filter nested in the header's own samples only the
286
+ // header, so a translucent card would show the page through it unblurred
234
287
  background: var(--mw-header-background);
235
- backdrop-filter: blur(7px);
236
- // Both edges: the panel covers the bar's own bottom border - top: 100%
237
- // lands on the padding box, which is above it - so without one of its own
238
- // the bar and the panel read as a single block of colour.
239
- border-top: 1px solid var(--mw-header-border);
240
- border-bottom: 1px solid var(--mw-header-border);
241
- box-shadow:
242
- 0 6px 12px var(--mw-shadow-near),
243
- 0 18px 40px var(--mw-shadow-far);
288
+ border: 1px solid var(--mw-header-island-border);
289
+ border-radius: radius('xl');
244
290
  z-index: z-index('header-panel');
245
291
 
246
- // Rolled out from under the bar rather than slid in. A slide needs a place
247
- // to start, and the only direction with room is straight up through the
248
- // header - which would take the panel behind its own stacking context root,
249
- // where a child cannot go. The clip does the same thing visually and needs
250
- // no z-index trick.
292
+ // Rolled out from under the island rather than slid in. A slide needs a
293
+ // place to start, and the only direction with room is straight up through
294
+ // the header - which would take the panel behind its own stacking context
295
+ // root, where a child cannot go. The clip does the same thing visually and
296
+ // needs no z-index trick.
251
297
  clip-path: inset(0 0 100% 0);
252
298
  visibility: hidden;
253
299
  transition:
@@ -258,29 +304,36 @@ $_control-size: 40px;
258
304
  display: flex;
259
305
  flex-direction: column;
260
306
  align-items: stretch;
261
- gap: spacing('0');
307
+ gap: spacing('1');
262
308
  // The list is a flex item of the panel, so without this it shrinks to the
263
309
  // width of its longest label and the rows end a third of the way across
264
310
  width: 100%;
265
- // Tall menus stay reachable without the panel growing past the screen
266
- max-height: calc(100dvh - var(--mw-header-height));
311
+ // Tall menus stay reachable without the card growing past the screen: the
312
+ // room under the island, less the gap above the card and the inset below
313
+ max-height: calc(
314
+ 100dvh - var(--mw-header-height) -
315
+ #{spacing('3')} - var(--mw-header-inset)
316
+ );
267
317
  overflow-y: auto;
268
318
  // Keeps a rubber-band scroll inside the panel from moving the page
269
319
  overscroll-behavior: contain;
270
- // The gutter sits on the link, not here, so a row's hover and its tap
271
- // target run the full width of the panel instead of stopping at the text
272
- padding-bottom: env(safe-area-inset-bottom, 0px);
320
+ padding: spacing('3') spacing('3')
321
+ calc(#{spacing('3')} + env(safe-area-inset-bottom, 0px));
322
+
323
+ // The card is the surface here - no gliding mark
324
+ &::before,
325
+ &::after {
326
+ content: none;
327
+ }
273
328
 
274
- // From here a row fits, and a full-width band with four links stacked in
275
- // its left corner is mostly empty band. Wrapping, because the count is
329
+ // From here a row fits, and a full-width card with four links stacked in
330
+ // its left corner is mostly empty card. Wrapping, because the count is
276
331
  // the page's business, not the framework's.
277
332
  @include media-up('sm') {
278
333
  flex-direction: row;
279
334
  flex-wrap: wrap;
280
335
  align-items: center;
281
336
  gap: spacing('2');
282
- padding: spacing('3') var(--mw-container-gutter)
283
- calc(#{spacing('3')} + env(safe-area-inset-bottom, 0px));
284
337
  }
285
338
  }
286
339
 
@@ -290,19 +343,8 @@ $_control-size: 40px;
290
343
  opacity: 0;
291
344
  transform: translateY(-8px);
292
345
 
293
- // Rules between the rows, not around the block: a line above the first
294
- // one would double the panel's own top border, one below the last would
295
- // double its bottom.
296
- + .mw-navbar-item .mw-navbar-link {
297
- border-top: 1px solid var(--mw-header-border);
298
- }
299
-
300
346
  @include media-up('sm') {
301
347
  width: auto;
302
-
303
- + .mw-navbar-item .mw-navbar-link {
304
- border-top: none;
305
- }
306
348
  }
307
349
  }
308
350
 
@@ -311,45 +353,34 @@ $_control-size: 40px;
311
353
  align-items: center;
312
354
  min-height: 52px;
313
355
  width: 100%;
314
- padding: spacing('3') var(--mw-container-gutter);
315
- border-radius: radius('none');
356
+ padding: spacing('3') spacing('5');
316
357
  font-size: font-size('lg');
317
358
  text-align: left;
318
- color: var(--mw-header-text-color);
359
+ color: var(--mw-header-navbar-list-color);
319
360
 
320
361
  @include media-up('sm') {
321
362
  min-height: 44px;
322
363
  padding: spacing('3') spacing('4');
323
- border-radius: radius('md');
324
364
  font-size: font-size('md');
325
365
  }
326
366
 
327
- // The sliding underline is a hover affordance and has no place on touch
328
- &::after {
329
- display: none;
330
- }
331
-
332
367
  // Restated here: the plain color above already outranks the base
333
368
  &:hover {
334
- color: var(--mw-header-navbar-list-active-color);
335
- background: var(--mw-primary-background);
369
+ color: var(--mw-header-text-color);
370
+ background: var(--mw-header-navbar-mark);
336
371
  }
337
372
 
338
373
  &:active {
339
- background: var(--mw-primary-background-hover);
374
+ background: #{fade('header-navbar-list-active-color', 26%)};
340
375
  }
341
376
 
342
377
  // Last, so the current page keeps its look while being hovered
343
378
  &:is(.mw-active, .active, [aria-current='page']) {
344
- color: var(--mw-header-navbar-list-active-color);
345
- background: var(--mw-primary-background);
346
- // Marks the left edge of a stacked list. In a row the links are pills
347
- // and the same rule would draw a stray tick inside each of them.
348
- box-shadow: inset 3px 0 0 var(--mw-header-navbar-list-active-color);
349
- font-weight: font-weight('medium');
350
-
351
- @include media-up('sm') {
352
- box-shadow: none;
379
+ color: var(--mw-header-text-color);
380
+ background: var(--mw-header-navbar-mark);
381
+
382
+ &::after {
383
+ @include navbar-mark-arcs;
353
384
  }
354
385
  }
355
386
  }
@@ -430,14 +461,10 @@ $_control-size: 40px;
430
461
  list-style: none;
431
462
  margin: spacing('0');
432
463
  padding: spacing('0');
433
- gap: spacing('2');
464
+ gap: spacing('0');
434
465
  align-items: center;
435
466
  flex-wrap: nowrap;
436
467
  overflow: visible;
437
-
438
- @include media-up('lg') {
439
- gap: spacing('4');
440
- }
441
468
  }
442
469
 
443
470
  &-item {
@@ -448,41 +475,32 @@ $_control-size: 40px;
448
475
 
449
476
  &-link {
450
477
  position: relative;
478
+ display: block;
479
+ padding: spacing('2') spacing('3');
480
+ @include navbar-mark-shape;
451
481
  font-weight: font-weight('medium');
452
- font-size: inherit;
482
+ // A step under the body text, which is what pays for the padding the mark
483
+ // needs - the row takes no more room than the bare labels used to
484
+ font-size: font-size('sm');
453
485
  transition: var(--mw-transition);
454
486
  color: var(--mw-header-navbar-list-color);
455
- padding-bottom: 3px;
456
487
 
457
- &::after {
458
- content: '';
459
- position: absolute;
460
- left: 10%;
461
- bottom: 0;
462
- width: 80%;
463
- height: 2px;
464
- background: currentColor;
465
- transform: scaleX(0);
466
- transform-origin: center;
467
- transition:
468
- transform var(--mw-duration-fast),
469
- opacity var(--mw-duration-fast);
470
- opacity: 0;
488
+ &:hover {
489
+ color: var(--mw-header-text-color);
490
+ background: var(--mw-header-navbar-mark);
471
491
  }
472
492
 
473
- &:hover,
474
- &.mw-active,
475
- &.active,
476
- &[aria-current='page'] {
477
- color: var(--mw-header-navbar-list-active-color);
493
+ &:active {
494
+ background: #{fade('header-navbar-list-active-color', 26%)};
478
495
  }
479
496
 
480
- &:hover::after,
481
- &.mw-active::after,
482
- &.active::after,
483
- &[aria-current='page']::after {
484
- transform: scaleX(1);
485
- opacity: 1;
497
+ &:is(.mw-active, .active, [aria-current='page']) {
498
+ color: var(--mw-header-text-color);
499
+ background: var(--mw-header-navbar-mark);
500
+
501
+ &::after {
502
+ @include navbar-mark-arcs;
503
+ }
486
504
  }
487
505
  }
488
506
 
@@ -502,6 +520,157 @@ $_control-size: 40px;
502
520
  }
503
521
  }
504
522
 
523
+ // The mark follows the pointer and the keyboard and comes back to rest on the
524
+ // current page: the link under the pointer takes the anchor name, the current
525
+ // one holds it only while no other does. Chromium animates the move, WebKit
526
+ // and Gecko jump - both land in the right place. Without anchor positioning
527
+ // each link paints its own mark, see above.
528
+ @supports (anchor-scope: all) and (position-anchor: --mw-a) {
529
+ .mw-navbar-list {
530
+ position: relative;
531
+ anchor-scope: --mw-navbar-mark;
532
+
533
+ &::before {
534
+ content: '';
535
+ position: absolute;
536
+ pointer-events: none;
537
+ @include navbar-mark-shape;
538
+ background: var(--mw-header-navbar-mark);
539
+ }
540
+
541
+ &::after {
542
+ @include navbar-mark-arcs;
543
+ }
544
+
545
+ // After the arcs: corner-accent() sets `inset: 0` for the box it frames
546
+ &::before,
547
+ &::after,
548
+ :root.mw-corners-even &::after {
549
+ position-anchor: --mw-navbar-mark;
550
+ inset: anchor(top) anchor(right) anchor(bottom) anchor(left);
551
+ opacity: 0;
552
+ transition:
553
+ left var(--mw-duration-base) var(--mw-ease-out),
554
+ right var(--mw-duration-base) var(--mw-ease-out),
555
+ opacity var(--mw-duration-fast) var(--mw-ease-out);
556
+ }
557
+
558
+ &:has(
559
+ .mw-navbar-link:is(
560
+ :hover,
561
+ :focus-visible,
562
+ .mw-active,
563
+ .active,
564
+ [aria-current='page']
565
+ )
566
+ ) {
567
+ &::before,
568
+ &::after {
569
+ opacity: 1;
570
+ }
571
+ }
572
+ }
573
+
574
+ .mw-navbar-link {
575
+ &:is(:hover, :focus-visible) {
576
+ anchor-name: --mw-navbar-mark;
577
+ }
578
+
579
+ &:hover,
580
+ &:is(.mw-active, .active, [aria-current='page']) {
581
+ background: transparent;
582
+ }
583
+
584
+ &:is(.mw-active, .active, [aria-current='page'])::after,
585
+ :root.mw-corners-even
586
+ &:is(.mw-active, .active, [aria-current='page'])::after {
587
+ content: none;
588
+ }
589
+ }
590
+
591
+ .mw-navbar-list:not(:has(.mw-navbar-link:is(:hover, :focus-visible)))
592
+ .mw-navbar-link:is(.mw-active, .active, [aria-current='page']) {
593
+ anchor-name: --mw-navbar-mark;
594
+ }
595
+ }
596
+
597
+ // An announcement hangs from the island as its lower half: the same edges, the
598
+ // island squared off where the two meet and its shadow handed down to the
599
+ // ribbon, so they float as one piece. 1px up, under the island's border, so no
600
+ // rounding seam opens between them.
601
+ .mw-announcement:not(.mw-announcement-static) {
602
+ top: calc(var(--mw-header-height) - 1px);
603
+ left: $_island-edge;
604
+ right: $_island-edge;
605
+ border: 1px solid var(--mw-header-island-border);
606
+ border-top: none;
607
+ border-radius: 0 0 radius('xl') radius('xl');
608
+ box-shadow: var(--mw-elevation-4);
609
+ // The link's hover fill would square off the rounded corners
610
+ overflow: hidden;
611
+ }
612
+
613
+ html:has(.mw-announcement:not(.mw-announcement-static, [hidden])) .mw-header {
614
+ border-bottom-left-radius: 0;
615
+ border-bottom-right-radius: 0;
616
+ box-shadow: none;
617
+
618
+ // The open menu covers the ribbon instead of hanging below it - a phone
619
+ // would otherwise lose the height of both before the first link
620
+ .mw-navbar {
621
+ top: 100%;
622
+ border-top-left-radius: 0;
623
+ border-top-right-radius: 0;
624
+ }
625
+ }
626
+
627
+ // The open menu covers it, but through the scrim's blur its edge would still
628
+ // bleed out around the card
629
+ body.mw-nav-open .mw-announcement:not(.mw-announcement-static) {
630
+ visibility: hidden;
631
+ }
632
+
633
+ // The flat bar, for a site that wants the header to just stand there: full
634
+ // width, fixed at the top, no island and no reveal (_header-reveal.scss is off
635
+ // under the same class). On <html> like the other site-wide variants.
636
+ :root.mw-header-bar {
637
+ --mw-header-height: #{$header-bar-height};
638
+
639
+ .mw-header {
640
+ top: 0;
641
+ left: 0;
642
+ right: 0;
643
+ min-height: var(--mw-header-height);
644
+ // Percentages of the viewport, so the content box is the page container
645
+ padding-inline: calc((100% - var(--mw-container-width)) / 2);
646
+ border-width: 0 0 1px;
647
+ border-color: var(--mw-header-border);
648
+ border-radius: 0;
649
+ background: var(--mw-header-background);
650
+ box-shadow: none;
651
+ }
652
+
653
+ // The menu hangs straight from the bar, edge to edge. In the row the nav is
654
+ // static and has no border style, so none of this reaches it.
655
+ .mw-navbar {
656
+ top: 100%;
657
+ left: 0;
658
+ right: 0;
659
+ border-width: 1px 0;
660
+ border-radius: 0;
661
+ }
662
+
663
+ // The ribbon under the bar, as it always was
664
+ .mw-announcement:not(.mw-announcement-static) {
665
+ top: var(--mw-header-height);
666
+ left: 0;
667
+ right: 0;
668
+ border: none;
669
+ border-radius: 0;
670
+ box-shadow: var(--mw-elevation-2);
671
+ }
672
+ }
673
+
505
674
  // The profile button and the round icon control beside it share one silhouette
506
675
  .mw-profile-btn,
507
676
  .mw-header-btn {
@@ -601,7 +770,7 @@ $_control-size: 40px;
601
770
  --mw-focus-ring-color: var(--mw-primary-text-color);
602
771
  }
603
772
 
604
- // Forced colours paint the drawn line away; the current page gets a real one
773
+ // Forced colours paint the mark's fill away; the current page gets a real line
605
774
  @media (forced-colors: active) {
606
775
  .mw-navbar-link:is(.mw-active, .active, [aria-current='page']) {
607
776
  text-decoration: underline 2px;