maverick-wave 5.26.0 → 5.28.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/.claude/skills/mw-maverick-wave/SKILL.md +9 -1
- package/.claude/skills/mw-maverick-wave/references/components.md +13 -6
- package/.claude/skills/mw-maverick-wave/references/layout.md +2 -2
- package/.claude/skills/mw-maverick-wave/references/theming.md +2 -1
- package/CHANGELOG.md +13 -0
- package/README.md +2 -2
- package/maverick-wave.min.css +7 -7
- package/maverick-wave.min.js +1 -1
- package/package.json +2 -2
- package/scripts/verify.js +10 -1
- package/src/js/main.js +8 -8
- package/src/partials/tiles-container.html +38 -38
- package/src/scss/abstracts/_variables.scss +6 -0
- package/src/scss/base/_base.scss +17 -7
- package/src/scss/base/_reset.scss +7 -1
- package/src/scss/components/_announcement.scss +1 -3
- package/src/scss/components/_blog-post.scss +5 -4
- package/src/scss/components/_cards.scss +2 -2
- package/src/scss/components/_contact.scss +5 -3
- package/src/scss/components/_hero-badge.scss +48 -0
- package/src/scss/components/_index.scss +2 -0
- package/src/scss/components/_leader-row.scss +24 -18
- package/src/scss/components/_localhost-indicator.scss +2 -2
- package/src/scss/components/_panels.scss +18 -52
- package/src/scss/components/_tabs.scss +2 -2
- package/src/scss/components/_tiles.scss +43 -49
- package/src/scss/components/_tooltip.scss +193 -0
- package/src/scss/layout/_footer.scss +46 -84
- package/src/scss/layout/_grid.scss +67 -42
- package/src/scss/layout/_header-reveal.scss +1 -1
- package/src/scss/layout/_header.scss +49 -77
- package/src/scss/layout/_home.scss +7 -4
- package/src/scss/layout/_main.scss +100 -337
- package/src/scss/layout/_parallax.scss +1 -1
- package/src/scss/layout/_section.scss +37 -21
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
@use '../abstracts' as *;
|
|
2
|
+
|
|
3
|
+
@layer mw.components {
|
|
4
|
+
// Glass rather than a fill: on a hero image a solid badge is the loudest thing
|
|
5
|
+
// on the screen and plain text sinks into the picture.
|
|
6
|
+
.mw-hero-badge {
|
|
7
|
+
display: inline-flex;
|
|
8
|
+
align-items: center;
|
|
9
|
+
justify-content: center;
|
|
10
|
+
gap: spacing('3');
|
|
11
|
+
max-width: 100%;
|
|
12
|
+
margin-bottom: spacing('6');
|
|
13
|
+
padding: spacing('2') spacing('5');
|
|
14
|
+
border: 1px solid #{fade('hero-text-color', 22%)};
|
|
15
|
+
border-radius: 999px;
|
|
16
|
+
// A vertical ramp and not a flat tint: glass is lit from above, and the
|
|
17
|
+
// gradient is what gives the pill a thickness instead of a fogged panel
|
|
18
|
+
background: linear-gradient(
|
|
19
|
+
to bottom,
|
|
20
|
+
#{fade('hero-text-color', 16%)},
|
|
21
|
+
#{fade('hero-text-color', 5%)}
|
|
22
|
+
);
|
|
23
|
+
// saturate() is what separates glass from frosted plastic: the blur alone
|
|
24
|
+
// greys out what shows through, and pushing the chroma back up is how a
|
|
25
|
+
// real lens reads. The two inset lines are the specular highlight - light
|
|
26
|
+
// catching the top edge, a fainter bounce off the bottom - and the outer
|
|
27
|
+
// shadow is what lifts the pill off the photo instead of pasting it on.
|
|
28
|
+
backdrop-filter: blur(14px) saturate(180%);
|
|
29
|
+
box-shadow:
|
|
30
|
+
inset 0 1px 0 #{fade('hero-text-color', 40%)},
|
|
31
|
+
inset 0 -1px 0 #{fade('hero-text-color', 12%)},
|
|
32
|
+
0 2px 12px rgb(0 0 0 / 18%);
|
|
33
|
+
color: fade('hero-text-color', 88%);
|
|
34
|
+
font-size: font-size('sm');
|
|
35
|
+
font-weight: font-weight('medium');
|
|
36
|
+
letter-spacing: 0.02em;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Carries the secondary colour, because the badge is a state, not a label -
|
|
40
|
+
// override `background` for a different one
|
|
41
|
+
.mw-hero-badge-dot {
|
|
42
|
+
width: 8px;
|
|
43
|
+
height: 8px;
|
|
44
|
+
flex-shrink: 0;
|
|
45
|
+
border-radius: radius('full');
|
|
46
|
+
background: var(--mw-secondary-color);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
@forward 'empty-state';
|
|
20
20
|
@forward 'flag';
|
|
21
21
|
@forward 'gallery';
|
|
22
|
+
@forward 'hero-badge';
|
|
22
23
|
@forward 'info';
|
|
23
24
|
@forward 'kanban';
|
|
24
25
|
@forward 'kbd';
|
|
@@ -49,3 +50,4 @@
|
|
|
49
50
|
@forward 'tiles';
|
|
50
51
|
@forward 'timelines';
|
|
51
52
|
@forward 'toasts';
|
|
53
|
+
@forward 'tooltip';
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
// and invoice draws one.
|
|
11
11
|
//
|
|
12
12
|
// The leader is the row's own ::before rather than an element in the markup.
|
|
13
|
-
// A generated box on a
|
|
13
|
+
// A generated box on a grid container is a grid item, so "order" can place it
|
|
14
14
|
// between the two spans - and generated content stays out of the
|
|
15
15
|
// accessibility tree, where a filler element would need aria-hidden.
|
|
16
16
|
.mw-leader-row {
|
|
@@ -18,20 +18,27 @@
|
|
|
18
18
|
--mw-leader-row-width: 2px;
|
|
19
19
|
--mw-leader-row-color: var(--mw-border);
|
|
20
20
|
|
|
21
|
-
|
|
22
|
-
//
|
|
23
|
-
//
|
|
24
|
-
|
|
25
|
-
|
|
21
|
+
// Grid, not a wrapping flex row. Flex fills a line greedily and moves the
|
|
22
|
+
// next item down before it shrinks anything, so a label that still fit on
|
|
23
|
+
// its own line kept it and pushed the value underneath - on a phone the
|
|
24
|
+
// row then read as text, dashes, and a number stranded on the line below.
|
|
25
|
+
// A grid column gives instead: the label breaks between its words and the
|
|
26
|
+
// value stays where it belongs.
|
|
27
|
+
//
|
|
28
|
+
// The leader column holds a stub of line when label and value nearly fill
|
|
29
|
+
// the row. Anything past the two declared columns - a second value - takes
|
|
30
|
+
// a column of its own.
|
|
31
|
+
display: grid;
|
|
32
|
+
grid-template-columns: minmax(0, auto) minmax(spacing('6'), 1fr);
|
|
33
|
+
grid-auto-flow: column;
|
|
34
|
+
grid-auto-columns: max-content;
|
|
35
|
+
align-items: end;
|
|
26
36
|
gap: spacing('3');
|
|
27
37
|
margin: 0;
|
|
28
38
|
|
|
29
39
|
&::before {
|
|
30
40
|
content: '';
|
|
31
41
|
order: 1;
|
|
32
|
-
flex: 1 1 auto;
|
|
33
|
-
// Keeps a stub of line visible when label and value nearly fill the row
|
|
34
|
-
min-width: spacing('6');
|
|
35
42
|
border-bottom: var(--mw-leader-row-width) var(--mw-leader-row-style)
|
|
36
43
|
var(--mw-leader-row-color);
|
|
37
44
|
margin-bottom: $_mw-leader-baseline-lift;
|
|
@@ -39,17 +46,16 @@
|
|
|
39
46
|
|
|
40
47
|
&-label {
|
|
41
48
|
order: 0;
|
|
42
|
-
//
|
|
43
|
-
//
|
|
44
|
-
//
|
|
45
|
-
|
|
49
|
+
// Words first, and the column floor is zero, so a single word wider than
|
|
50
|
+
// what is left of the row breaks inside itself rather than running past
|
|
51
|
+
// the edge - a German compound noun reaches that point on a phone.
|
|
52
|
+
overflow-wrap: break-word;
|
|
46
53
|
// The row also works as a dl group, and a dd carries 40px of browser margin
|
|
47
54
|
margin: 0;
|
|
48
55
|
}
|
|
49
56
|
|
|
50
57
|
&-value {
|
|
51
58
|
order: 2;
|
|
52
|
-
flex-shrink: 0;
|
|
53
59
|
margin: 0;
|
|
54
60
|
// A price or a page number must not break, and a column of them only
|
|
55
61
|
// lines up with tabular figures.
|
|
@@ -59,8 +65,8 @@
|
|
|
59
65
|
|
|
60
66
|
// Two values in one row read as a single long range with only a gap between
|
|
61
67
|
// them. The mark takes the leader's colour, so retuning a row moves both.
|
|
62
|
-
// It hangs off the first of the pair, not the second
|
|
63
|
-
//
|
|
68
|
+
// It hangs off the first of the pair, not the second, where a trailing mark
|
|
69
|
+
// would read as "and there is more" at the end of the row.
|
|
64
70
|
&-value:has(+ &-value)::after {
|
|
65
71
|
content: '|';
|
|
66
72
|
margin-left: spacing('3');
|
|
@@ -77,10 +83,10 @@
|
|
|
77
83
|
}
|
|
78
84
|
|
|
79
85
|
&-top {
|
|
80
|
-
align-items:
|
|
86
|
+
align-items: start;
|
|
81
87
|
|
|
82
88
|
&::before {
|
|
83
|
-
//
|
|
89
|
+
// start pins the leader to the top edge, so it drops to the first
|
|
84
90
|
// line's baseline itself instead of being lifted off the last one
|
|
85
91
|
margin: calc(1lh - #{$_mw-leader-baseline-lift}) 0 0;
|
|
86
92
|
}
|
|
@@ -10,11 +10,11 @@
|
|
|
10
10
|
height: 4px;
|
|
11
11
|
background: var(--mw-warning-color);
|
|
12
12
|
z-index: z-index('modal')-10;
|
|
13
|
-
animation: pulse-indicator motion(4s) infinite;
|
|
13
|
+
animation: mw-pulse-indicator motion(4s) infinite;
|
|
14
14
|
}
|
|
15
15
|
|
|
16
16
|
// Keyframes for the pulsing indicator
|
|
17
|
-
@keyframes pulse-indicator {
|
|
17
|
+
@keyframes mw-pulse-indicator {
|
|
18
18
|
0% {
|
|
19
19
|
opacity: 0.5;
|
|
20
20
|
box-shadow: 0 0 3px var(--mw-warning-color);
|
|
@@ -1,6 +1,14 @@
|
|
|
1
1
|
@use '../abstracts' as *;
|
|
2
2
|
|
|
3
3
|
@layer mw.components {
|
|
4
|
+
// -scrollable is the older spelling of -max-height-md
|
|
5
|
+
$_mw-panel-heights: (
|
|
6
|
+
'max-height-sm': 300px,
|
|
7
|
+
'max-height-md': 500px,
|
|
8
|
+
'max-height-lg': 700px,
|
|
9
|
+
'scrollable': 500px,
|
|
10
|
+
);
|
|
11
|
+
|
|
4
12
|
.mw-panel {
|
|
5
13
|
background: var(--mw-card-background);
|
|
6
14
|
@include surface(2px);
|
|
@@ -8,13 +16,15 @@
|
|
|
8
16
|
overflow: hidden;
|
|
9
17
|
display: flex;
|
|
10
18
|
flex-direction: column;
|
|
11
|
-
min-height: 100%;
|
|
12
|
-
margin: spacing('1');
|
|
13
19
|
|
|
14
20
|
&-header {
|
|
15
21
|
padding: spacing('4') spacing('5');
|
|
16
22
|
border-bottom: 1px solid var(--mw-border-accent);
|
|
17
23
|
display: flex;
|
|
24
|
+
// A title with an action beside it runs out of room on a phone long
|
|
25
|
+
// before the title runs out of words
|
|
26
|
+
flex-wrap: wrap;
|
|
27
|
+
gap: spacing('3');
|
|
18
28
|
justify-content: space-between;
|
|
19
29
|
align-items: center;
|
|
20
30
|
margin: spacing('0');
|
|
@@ -48,7 +58,6 @@
|
|
|
48
58
|
gap: spacing('3');
|
|
49
59
|
}
|
|
50
60
|
|
|
51
|
-
// Panel variants
|
|
52
61
|
&-primary {
|
|
53
62
|
.mw-panel-header {
|
|
54
63
|
background: var(--mw-primary-color);
|
|
@@ -72,56 +81,13 @@
|
|
|
72
81
|
@include corner-accent(2px, 'bottom');
|
|
73
82
|
}
|
|
74
83
|
|
|
75
|
-
//
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
max-height: calc(500px - #{2 * spacing('4')} - 2px);
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
// Scrollable variants for max height
|
|
86
|
-
&-max-height {
|
|
87
|
-
&-sm {
|
|
88
|
-
max-height: 300px;
|
|
89
|
-
min-height: 0;
|
|
90
|
-
align-self: start;
|
|
91
|
-
.mw-panel-body {
|
|
92
|
-
max-height: calc(300px - #{2 * spacing('4')} - 2px);
|
|
93
|
-
overflow-y: auto;
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
&-md {
|
|
98
|
-
max-height: 500px;
|
|
99
|
-
min-height: 0;
|
|
100
|
-
align-self: start;
|
|
101
|
-
.mw-panel-body {
|
|
102
|
-
max-height: calc(500px - #{2 * spacing('4')} - 2px);
|
|
103
|
-
overflow-y: auto;
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
&-lg {
|
|
108
|
-
max-height: 700px;
|
|
109
|
-
min-height: 0;
|
|
84
|
+
// Only the cap - the body already scrolls and already takes what the header
|
|
85
|
+
// and footer leave it. `align-self` because a grid row stretches its items
|
|
86
|
+
// first, and a stretched panel reaches the cap from above whatever it says.
|
|
87
|
+
@each $name, $height in $_mw-panel-heights {
|
|
88
|
+
&-#{$name} {
|
|
89
|
+
max-height: $height;
|
|
110
90
|
align-self: start;
|
|
111
|
-
.mw-panel-body {
|
|
112
|
-
max-height: calc(700px - #{2 * spacing('4')} - 2px);
|
|
113
|
-
overflow-y: auto;
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
@include media-down('sm') {
|
|
119
|
-
font-size: font-size('sm');
|
|
120
|
-
|
|
121
|
-
&-header {
|
|
122
|
-
h3 {
|
|
123
|
-
font-size: font-size('sm');
|
|
124
|
-
}
|
|
125
91
|
}
|
|
126
92
|
}
|
|
127
93
|
}
|
|
@@ -132,7 +132,7 @@
|
|
|
132
132
|
display: block;
|
|
133
133
|
// 1.1s to reveal a tab panel means a second of blank space after every
|
|
134
134
|
// click. A panel switch is a state change, not an entrance.
|
|
135
|
-
animation:
|
|
135
|
+
animation: mw-tabs-fade var(--mw-duration-base) var(--mw-ease-out);
|
|
136
136
|
}
|
|
137
137
|
}
|
|
138
138
|
|
|
@@ -281,7 +281,7 @@
|
|
|
281
281
|
}
|
|
282
282
|
}
|
|
283
283
|
|
|
284
|
-
@keyframes
|
|
284
|
+
@keyframes mw-tabs-fade {
|
|
285
285
|
from {
|
|
286
286
|
opacity: 0;
|
|
287
287
|
}
|
|
@@ -1,51 +1,8 @@
|
|
|
1
1
|
@use '../abstracts' as *;
|
|
2
2
|
|
|
3
3
|
@layer mw.components {
|
|
4
|
-
.mw-tiles {
|
|
5
|
-
width: 100%;
|
|
6
|
-
margin: spacing('0') auto;
|
|
7
|
-
padding: spacing('0');
|
|
8
|
-
display: grid;
|
|
9
|
-
gap: spacing('4');
|
|
10
|
-
grid-template-columns: 1fr;
|
|
11
|
-
|
|
12
|
-
@include media-up('sm') {
|
|
13
|
-
grid-template-columns: repeat(2, 1fr);
|
|
14
|
-
width: 80%;
|
|
15
|
-
padding: spacing('3');
|
|
16
|
-
gap: spacing('6');
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
@include media-up('lg') {
|
|
20
|
-
grid-template-columns: repeat(3, 1fr);
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
// The page gutter is the container's job, a second inset only narrows the tile
|
|
24
|
-
&-4col {
|
|
25
|
-
@include media-up('sm') {
|
|
26
|
-
width: 85%;
|
|
27
|
-
grid-template-columns: repeat(2, 1fr);
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
@include media-up('md') {
|
|
31
|
-
grid-template-columns: repeat(3, 1fr);
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
@include media-up('lg') {
|
|
35
|
-
grid-template-columns: repeat(4, 1fr);
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
&-2col {
|
|
40
|
-
@include media-up('sm') {
|
|
41
|
-
width: 75%;
|
|
42
|
-
grid-template-columns: repeat(2, 1fr);
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
|
|
47
4
|
// The tile sizes its type off its own width, not the window's. In
|
|
48
|
-
// `.mw-tiles-4col` on a wide desktop a tile is a quarter of
|
|
5
|
+
// `.mw-tiles-4col` on a wide desktop a tile is a quarter of the container -
|
|
49
6
|
// narrower than the same tile alone on a phone, where it used to be the one
|
|
50
7
|
// getting the small type.
|
|
51
8
|
//
|
|
@@ -69,7 +26,6 @@
|
|
|
69
26
|
overflow: hidden;
|
|
70
27
|
display: flex;
|
|
71
28
|
flex-direction: column;
|
|
72
|
-
cursor: pointer;
|
|
73
29
|
|
|
74
30
|
// Ahead of the size modifiers below, which take over from 'sm'
|
|
75
31
|
@include media-up('sm') {
|
|
@@ -90,13 +46,22 @@
|
|
|
90
46
|
|
|
91
47
|
@include hover-move {
|
|
92
48
|
transform: translateY(-2px);
|
|
49
|
+
|
|
50
|
+
// The whole tile is the target, unlike a card where the image frame
|
|
51
|
+
// owns its own hover - a tile is one face, not a stack of regions.
|
|
52
|
+
.mw-tile-img img {
|
|
53
|
+
transform: scale(1.05);
|
|
54
|
+
}
|
|
93
55
|
}
|
|
94
56
|
|
|
95
57
|
// A tile is tapped, so it needs a press of its own - the lift above never
|
|
96
|
-
// runs on a finger.
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
58
|
+
// runs on a finger. Guarded like every other travel in the framework; what
|
|
59
|
+
// is left without it is the latched hover, which is feedback enough.
|
|
60
|
+
@media (prefers-reduced-motion: no-preference) {
|
|
61
|
+
&:active {
|
|
62
|
+
transform: scale(0.985);
|
|
63
|
+
transition-duration: var(--mw-duration-instant);
|
|
64
|
+
}
|
|
100
65
|
}
|
|
101
66
|
|
|
102
67
|
&-img {
|
|
@@ -145,6 +110,13 @@
|
|
|
145
110
|
font-size: font-size('sm');
|
|
146
111
|
line-height: 1.3;
|
|
147
112
|
|
|
113
|
+
// The ramp the header takes: a tile filling a phone is a wide
|
|
114
|
+
// container and reads at full size, one squeezed into a desktop
|
|
115
|
+
// column stays small.
|
|
116
|
+
@container mw-tile (width >= #{$_mw-tile-narrow}) {
|
|
117
|
+
font-size: font-size('base');
|
|
118
|
+
}
|
|
119
|
+
|
|
148
120
|
&:last-child {
|
|
149
121
|
margin-bottom: spacing('0');
|
|
150
122
|
}
|
|
@@ -186,6 +158,10 @@
|
|
|
186
158
|
|
|
187
159
|
p {
|
|
188
160
|
font-size: font-size('xs');
|
|
161
|
+
|
|
162
|
+
@container mw-tile (width >= #{$_mw-tile-narrow}) {
|
|
163
|
+
font-size: font-size('sm');
|
|
164
|
+
}
|
|
189
165
|
}
|
|
190
166
|
}
|
|
191
167
|
}
|
|
@@ -225,6 +201,24 @@
|
|
|
225
201
|
}
|
|
226
202
|
}
|
|
227
203
|
|
|
204
|
+
// A tile is only a control when the markup makes it one. The look above is a
|
|
205
|
+
// surface and stays on a div; everything a finger or a keyboard needs sits
|
|
206
|
+
// here, where the element itself answers for it - a hand cursor on a div
|
|
207
|
+
// promises a click nothing handles, and a ring on it would never be reached.
|
|
208
|
+
a.mw-tile,
|
|
209
|
+
button.mw-tile {
|
|
210
|
+
cursor: pointer;
|
|
211
|
+
// A button brings its own font, alignment and colour, and the tile's own
|
|
212
|
+
// children set none of the three
|
|
213
|
+
font: inherit;
|
|
214
|
+
color: inherit;
|
|
215
|
+
text-align: inherit;
|
|
216
|
+
text-decoration: none;
|
|
217
|
+
|
|
218
|
+
@include focus-ring;
|
|
219
|
+
@include tappable;
|
|
220
|
+
}
|
|
221
|
+
|
|
228
222
|
// A cramped tile reads better ranged left whatever the window is doing - a
|
|
229
223
|
// centred line inside 200px is two words and a gap. Every selector here is a
|
|
230
224
|
// child: a container cannot answer a query about itself, and the tile *is* the
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
@use 'sass:math';
|
|
2
|
+
@use '../abstracts' as *;
|
|
3
|
+
|
|
4
|
+
@layer mw.components {
|
|
5
|
+
// A pseudo element on the trigger, so an ancestor with `overflow: hidden` cuts
|
|
6
|
+
// it off. Hidden means `display: none`: a laid-out box still counts towards the
|
|
7
|
+
// page's overflow, and a 280px bubble near the right edge added 50px of sideways
|
|
8
|
+
// scroll on a phone. Hence `allow-discrete` and the starting style.
|
|
9
|
+
$_mw-tooltip-arrow: 6px;
|
|
10
|
+
$_mw-tooltip-gap: 8px;
|
|
11
|
+
// A square turned 45deg rather than a border triangle: it reads the same from
|
|
12
|
+
// above and from below, so the second placement costs no second arrow and a
|
|
13
|
+
// flip never has to repaint anything. The diagonal carries the visible width,
|
|
14
|
+
// so the side is the visible height doubled over root two.
|
|
15
|
+
$_mw-tooltip-arrow-side: math.div($_mw-tooltip-arrow * 2, math.sqrt(2));
|
|
16
|
+
// Half a side short of where the triangle's edge sat, so the turned square is
|
|
17
|
+
// centred on the bubble's edge and only its outer half shows. The inner half
|
|
18
|
+
// lands on the bubble in the same colour, which is what makes it read as one
|
|
19
|
+
// shape rather than a diamond stuck to a box.
|
|
20
|
+
$_mw-tooltip-arrow-inset: $_mw-tooltip-gap +
|
|
21
|
+
$_mw-tooltip-arrow - math.div($_mw-tooltip-arrow-side, 2);
|
|
22
|
+
|
|
23
|
+
[data-tooltip] {
|
|
24
|
+
position: relative;
|
|
25
|
+
|
|
26
|
+
&::before {
|
|
27
|
+
content: attr(data-tooltip);
|
|
28
|
+
position: absolute;
|
|
29
|
+
bottom: 100%;
|
|
30
|
+
left: 50%;
|
|
31
|
+
transform: translateX(-50%);
|
|
32
|
+
margin-bottom: $_mw-tooltip-gap + $_mw-tooltip-arrow;
|
|
33
|
+
max-width: min(280px, 90vw);
|
|
34
|
+
width: max-content;
|
|
35
|
+
padding: spacing('1') spacing('2');
|
|
36
|
+
background: var(--mw-gray-color);
|
|
37
|
+
color: $fixed-surface-text;
|
|
38
|
+
font-size: font-size('xs');
|
|
39
|
+
border-radius: radius('xs');
|
|
40
|
+
box-shadow: var(--mw-elevation-2);
|
|
41
|
+
pointer-events: none;
|
|
42
|
+
opacity: 0;
|
|
43
|
+
display: none;
|
|
44
|
+
transition:
|
|
45
|
+
opacity var(--mw-duration-fast) var(--mw-ease-out),
|
|
46
|
+
display var(--mw-duration-fast) allow-discrete;
|
|
47
|
+
z-index: z-index('tooltip');
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
&::after {
|
|
51
|
+
content: '';
|
|
52
|
+
position: absolute;
|
|
53
|
+
bottom: 100%;
|
|
54
|
+
left: 50%;
|
|
55
|
+
transform: translateX(-50%) rotate(45deg);
|
|
56
|
+
margin-bottom: $_mw-tooltip-arrow-inset;
|
|
57
|
+
width: $_mw-tooltip-arrow-side;
|
|
58
|
+
height: $_mw-tooltip-arrow-side;
|
|
59
|
+
background: var(--mw-gray-color);
|
|
60
|
+
opacity: 0;
|
|
61
|
+
display: none;
|
|
62
|
+
transition:
|
|
63
|
+
opacity var(--mw-duration-fast) var(--mw-ease-out),
|
|
64
|
+
display var(--mw-duration-fast) allow-discrete;
|
|
65
|
+
z-index: z-index('tooltip');
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// The placement API: the text is the attribute, the side is a class. The
|
|
69
|
+
// three steer different properties - block axis, inline axis - so `below`
|
|
70
|
+
// and `end` compose without a rule for the pairing.
|
|
71
|
+
//
|
|
72
|
+
// They place the bubble against the trigger rather than joining the anchored
|
|
73
|
+
// path below, and that is the point, not a shortcut. The reason to want them
|
|
74
|
+
// is a control in a fixed header, and that header carries a backdrop-filter,
|
|
75
|
+
// which makes it the containing block for anything `fixed` inside it.
|
|
76
|
+
// `block-end` is then the few pixels between the trigger and the header's own
|
|
77
|
+
// bottom edge, the bubble does not fit, and it gets pushed back over the
|
|
78
|
+
// trigger it belongs to. What this gives up is the clipping immunity the
|
|
79
|
+
// anchored path buys - a modifier inside a scroll container is cut off.
|
|
80
|
+
//
|
|
81
|
+
// Opting out has to restore the placement, not just the positioning scheme:
|
|
82
|
+
// the anchored path below is the later rule and clears `bottom`, `top`,
|
|
83
|
+
// `left` and the centring transform so `position-area` can resolve them
|
|
84
|
+
// itself. Leave that cleared and the box falls to its static position, which
|
|
85
|
+
// for a pseudo element is inside the trigger it is supposed to sit above.
|
|
86
|
+
// The three rules under this one then move the one edge each of them owns.
|
|
87
|
+
&:is(.mw-tooltip-below, .mw-tooltip-end, .mw-tooltip-start) {
|
|
88
|
+
&::before,
|
|
89
|
+
&::after {
|
|
90
|
+
position: absolute;
|
|
91
|
+
position-area: none;
|
|
92
|
+
bottom: 100%;
|
|
93
|
+
top: auto;
|
|
94
|
+
left: 50%;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
&::before {
|
|
98
|
+
transform: translateX(-50%);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
&::after {
|
|
102
|
+
transform: translateX(-50%) rotate(45deg);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
&.mw-tooltip-below {
|
|
107
|
+
&::before {
|
|
108
|
+
top: 100%;
|
|
109
|
+
bottom: auto;
|
|
110
|
+
margin-top: $_mw-tooltip-gap + $_mw-tooltip-arrow;
|
|
111
|
+
margin-bottom: 0;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
&::after {
|
|
115
|
+
top: 100%;
|
|
116
|
+
bottom: auto;
|
|
117
|
+
margin-top: $_mw-tooltip-arrow-inset;
|
|
118
|
+
margin-bottom: 0;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Only the bubble moves - the arrow stays centred on the trigger it points at.
|
|
123
|
+
&.mw-tooltip-end::before {
|
|
124
|
+
left: auto;
|
|
125
|
+
right: 0;
|
|
126
|
+
transform: none;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
&.mw-tooltip-start::before {
|
|
130
|
+
left: 0;
|
|
131
|
+
right: auto;
|
|
132
|
+
transform: none;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
&:hover::before,
|
|
136
|
+
&:hover::after,
|
|
137
|
+
&:focus-visible::before,
|
|
138
|
+
&:focus-visible::after {
|
|
139
|
+
opacity: 1;
|
|
140
|
+
display: block;
|
|
141
|
+
|
|
142
|
+
@starting-style {
|
|
143
|
+
opacity: 0;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// A press usually scales the trigger, and a transform makes it a containing
|
|
148
|
+
// block for the fixed-position bubble above - which would drop the label
|
|
149
|
+
// into the element being pressed. So it steps aside for the click instead;
|
|
150
|
+
// by then it has been read anyway. After the hover rule, same weight.
|
|
151
|
+
&:active::before,
|
|
152
|
+
&:active::after {
|
|
153
|
+
opacity: 0;
|
|
154
|
+
display: none;
|
|
155
|
+
// Not transitioned: `allow-discrete` holds `display: block` until the fade
|
|
156
|
+
// ends, and the bubble would spend those 180ms visibly sitting in the
|
|
157
|
+
// element. Going back out on release keeps the fade - that one is wanted.
|
|
158
|
+
transition: none;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// The clipping caveat above, lifted where the browser can do it: `fixed` takes
|
|
162
|
+
// the bubble out of the trigger's containing block, anchor positioning keeps
|
|
163
|
+
// it attached. No `anchor-name` needed - a pseudo element is implicitly
|
|
164
|
+
// anchored to the element it comes from.
|
|
165
|
+
//
|
|
166
|
+
// Still no `position-try-fallbacks: flip-block`, and no longer because of the
|
|
167
|
+
// arrow - the turned square survives a flip. Bubble and arrow are two
|
|
168
|
+
// separately anchored boxes, so each would decide on its own whether it fits,
|
|
169
|
+
// and the tiny one nearly always does: the label flips, the arrow stays.
|
|
170
|
+
@supports (position-area: block-start) {
|
|
171
|
+
&::before,
|
|
172
|
+
&::after {
|
|
173
|
+
position: fixed;
|
|
174
|
+
// The absolute placement these replace - left as-is they fight the
|
|
175
|
+
// position-area, which resolves the inset properties itself.
|
|
176
|
+
bottom: auto;
|
|
177
|
+
left: auto;
|
|
178
|
+
top: auto;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
&::before {
|
|
182
|
+
position-area: block-start;
|
|
183
|
+
transform: none;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// The turn stays - it is the arrow's shape, not its placement.
|
|
187
|
+
&::after {
|
|
188
|
+
position-area: block-start;
|
|
189
|
+
transform: rotate(45deg);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|