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