maverick-wave 5.27.0 → 5.29.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 +4 -2
- package/.claude/skills/mw-maverick-wave/references/components.md +212 -0
- package/.claude/skills/mw-maverick-wave/references/javascript.md +6 -1
- package/.claude/skills/mw-maverick-wave/references/theming.md +53 -51
- package/CHANGELOG.md +17 -0
- package/CLAUDE.md +4 -4
- package/README.md +2 -2
- package/index.html +65 -0
- package/maverick-wave.min.css +4 -4
- package/maverick-wave.min.js +1 -1
- package/package.json +2 -2
- package/scripts/verify.js +2 -1
- package/src/assets/images/photo-balloon.svg +35 -0
- package/src/assets/images/photo-forest-path.svg +31 -0
- package/src/assets/images/photo-lighthouse.svg +42 -0
- package/src/assets/images/photo-palm.svg +27 -0
- package/src/assets/images/photo-peak.svg +27 -0
- package/src/assets/images/photo-tower.svg +34 -0
- package/src/assets/images/photo-waterfall.svg +32 -0
- package/src/assets/images/story-aurora.svg +36 -0
- package/src/assets/images/story-beach.svg +34 -0
- package/src/assets/images/story-stars.svg +35 -0
- package/src/assets/media/story-sunrise.jpg +0 -0
- package/src/assets/media/story-sunrise.mp4 +0 -0
- package/src/js/main.js +411 -8
- package/src/partials/avatars-container.html +50 -0
- package/src/partials/calendar-container.html +36 -38
- package/src/partials/feed-container.html +506 -0
- package/src/partials/item-lists-container.html +80 -130
- package/src/partials/kanban-container.html +17 -294
- package/src/partials/mosaic-container.html +173 -0
- package/src/partials/panels-container.html +113 -162
- package/src/partials/portrait-gallery-container.html +175 -0
- package/src/partials/stories-container.html +182 -0
- package/src/partials/tables-container.html +21 -40
- package/src/scss/abstracts/_mixins.scss +2 -0
- package/src/scss/abstracts/_variables.scss +9 -0
- package/src/scss/components/_avatars.scss +52 -0
- package/src/scss/components/_cards.scss +7 -3
- package/src/scss/components/_feed.scss +203 -0
- package/src/scss/components/_hero-badge.scss +48 -0
- package/src/scss/components/_index.scss +8 -0
- package/src/scss/components/_leader-row.scss +24 -18
- package/src/scss/components/_lightbox.scss +178 -0
- package/src/scss/components/_mosaic.scss +83 -0
- package/src/scss/components/_overlay-btn.scss +47 -0
- package/src/scss/components/_portrait-gallery.scss +180 -0
- package/src/scss/components/_stories.scss +443 -0
- package/src/scss/components/_tiles.scss +4 -1
- package/src/scss/components/_tooltip.scss +193 -0
- package/src/scss/layout/_footer.scss +46 -84
- package/src/scss/layout/_grid.scss +48 -45
- package/src/scss/layout/_header-reveal.scss +1 -1
- package/src/scss/layout/_header.scss +49 -66
- package/src/scss/layout/_home.scss +5 -2
- package/src/scss/layout/_main.scss +100 -337
- package/src/scss/layout/_parallax.scss +1 -1
- package/src/scss/layout/_section.scss +24 -17
- package/src/scss/utilities/_fit.scss +29 -0
- package/src/scss/utilities/_index.scss +1 -0
|
@@ -0,0 +1,443 @@
|
|
|
1
|
+
@use '../abstracts' as *;
|
|
2
|
+
@use 'overlay-btn';
|
|
3
|
+
|
|
4
|
+
@layer mw.components {
|
|
5
|
+
$frame-height: min(92dvh, 56rem);
|
|
6
|
+
$frame-time: 5s;
|
|
7
|
+
// Frames per story the timing is written out for
|
|
8
|
+
$max-frames: 12;
|
|
9
|
+
|
|
10
|
+
// A bar cut into as many segments as the story has frames
|
|
11
|
+
@function segments($color, $count) {
|
|
12
|
+
@return repeating-linear-gradient(
|
|
13
|
+
to right,
|
|
14
|
+
$color 0 calc(100% / #{$count} - 3px),
|
|
15
|
+
transparent 0 calc(100% / #{$count})
|
|
16
|
+
);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
.mw-stories {
|
|
20
|
+
display: flex;
|
|
21
|
+
gap: spacing('5');
|
|
22
|
+
overflow-x: auto;
|
|
23
|
+
overscroll-behavior-x: contain;
|
|
24
|
+
padding-block: spacing('2');
|
|
25
|
+
scroll-snap-type: x proximity;
|
|
26
|
+
scrollbar-width: none;
|
|
27
|
+
@include scroll-hint;
|
|
28
|
+
|
|
29
|
+
&::-webkit-scrollbar {
|
|
30
|
+
display: none;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// A finger swipes the row, a mouse wheel cannot - it gets the bar
|
|
34
|
+
@media (hover: hover) and (pointer: fine) {
|
|
35
|
+
scrollbar-width: thin;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
.mw-story {
|
|
40
|
+
flex: none;
|
|
41
|
+
display: inline-flex;
|
|
42
|
+
flex-direction: column;
|
|
43
|
+
align-items: center;
|
|
44
|
+
gap: spacing('2');
|
|
45
|
+
width: 4.75rem;
|
|
46
|
+
padding: spacing('0');
|
|
47
|
+
border: none;
|
|
48
|
+
border-radius: radius('md');
|
|
49
|
+
background: none;
|
|
50
|
+
color: inherit;
|
|
51
|
+
font: inherit;
|
|
52
|
+
cursor: pointer;
|
|
53
|
+
scroll-snap-align: start;
|
|
54
|
+
@include tappable;
|
|
55
|
+
@include focus-ring;
|
|
56
|
+
|
|
57
|
+
&:active .mw-story-cover {
|
|
58
|
+
transform: scale(0.93);
|
|
59
|
+
transition-duration: var(--mw-duration-instant);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
.mw-story-cover {
|
|
64
|
+
display: block;
|
|
65
|
+
width: 4.25rem;
|
|
66
|
+
height: 4.25rem;
|
|
67
|
+
padding: 3px;
|
|
68
|
+
border-radius: radius('full');
|
|
69
|
+
// The ink tones, not the fills: the ring has to carry on a dark page too
|
|
70
|
+
background: linear-gradient(
|
|
71
|
+
45deg,
|
|
72
|
+
var(--mw-secondary-text-color),
|
|
73
|
+
var(--mw-primary-text-color)
|
|
74
|
+
);
|
|
75
|
+
transition: transform var(--mw-duration-fast) var(--mw-ease-out);
|
|
76
|
+
|
|
77
|
+
img {
|
|
78
|
+
display: block;
|
|
79
|
+
width: 100%;
|
|
80
|
+
height: 100%;
|
|
81
|
+
// The token that follows the surface - the page, or a card around the row
|
|
82
|
+
border: 2px solid var(--mw-scroll-hint-cover, var(--mw-page-background));
|
|
83
|
+
border-radius: radius('full');
|
|
84
|
+
object-fit: cover;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
.mw-story-label {
|
|
89
|
+
max-width: 100%;
|
|
90
|
+
font-size: font-size('xs');
|
|
91
|
+
@include truncate;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// A vertical snap strip: swipe, wheel and arrow keys page through it natively
|
|
95
|
+
.mw-story-reel {
|
|
96
|
+
// Not only the UA rule, or a browser without popovers shows it in the row
|
|
97
|
+
display: none;
|
|
98
|
+
width: 100%;
|
|
99
|
+
height: 100%;
|
|
100
|
+
max-width: none;
|
|
101
|
+
max-height: none;
|
|
102
|
+
margin: 0;
|
|
103
|
+
padding: spacing('0');
|
|
104
|
+
border: none;
|
|
105
|
+
overflow-y: auto;
|
|
106
|
+
overscroll-behavior: contain;
|
|
107
|
+
scroll-snap-type: y mandatory;
|
|
108
|
+
scrollbar-width: none;
|
|
109
|
+
background: #000;
|
|
110
|
+
color: $fixed-surface-text;
|
|
111
|
+
--mw-focus-ring-color: #{$fixed-surface-text};
|
|
112
|
+
// A long press pauses, so it must not open the image menu as well
|
|
113
|
+
user-select: none;
|
|
114
|
+
-webkit-touch-callout: none;
|
|
115
|
+
opacity: 0;
|
|
116
|
+
transition:
|
|
117
|
+
opacity var(--mw-duration-base) var(--mw-ease-out),
|
|
118
|
+
overlay var(--mw-duration-base) allow-discrete,
|
|
119
|
+
display var(--mw-duration-base) allow-discrete;
|
|
120
|
+
|
|
121
|
+
&::-webkit-scrollbar {
|
|
122
|
+
display: none;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
&:popover-open {
|
|
126
|
+
display: block;
|
|
127
|
+
opacity: 1;
|
|
128
|
+
|
|
129
|
+
@starting-style {
|
|
130
|
+
opacity: 0;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
&::backdrop {
|
|
135
|
+
background: rgb(0 0 0 / 88%);
|
|
136
|
+
opacity: 0;
|
|
137
|
+
transition:
|
|
138
|
+
opacity var(--mw-duration-base) var(--mw-ease-out),
|
|
139
|
+
overlay var(--mw-duration-base) allow-discrete,
|
|
140
|
+
display var(--mw-duration-base) allow-discrete;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
&:popover-open::backdrop {
|
|
144
|
+
opacity: 1;
|
|
145
|
+
|
|
146
|
+
@starting-style {
|
|
147
|
+
opacity: 0;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
> img,
|
|
152
|
+
> video {
|
|
153
|
+
display: block;
|
|
154
|
+
width: 100%;
|
|
155
|
+
height: 100%;
|
|
156
|
+
object-fit: cover;
|
|
157
|
+
scroll-snap-align: start;
|
|
158
|
+
scroll-snap-stop: always;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Wider than a phone held upright: the story keeps its phone shape
|
|
162
|
+
@include media-up('sm') {
|
|
163
|
+
width: calc(#{$frame-height} * 9 / 16);
|
|
164
|
+
height: $frame-height;
|
|
165
|
+
margin: auto;
|
|
166
|
+
border-radius: radius('lg');
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
.mw-story-reel-head {
|
|
171
|
+
position: sticky;
|
|
172
|
+
top: 0;
|
|
173
|
+
z-index: 2;
|
|
174
|
+
display: flex;
|
|
175
|
+
align-items: flex-start;
|
|
176
|
+
gap: spacing('1');
|
|
177
|
+
height: 5rem;
|
|
178
|
+
// Laid over the first frame instead of pushing it down
|
|
179
|
+
margin-bottom: -5rem;
|
|
180
|
+
padding: calc(max(#{spacing('3')}, env(safe-area-inset-top, 0px)) + 0.75rem)
|
|
181
|
+
spacing('3') 0 spacing('5');
|
|
182
|
+
background: linear-gradient(rgb(0 0 0 / 55%), transparent);
|
|
183
|
+
font-size: font-size('sm');
|
|
184
|
+
font-weight: font-weight('bold');
|
|
185
|
+
text-shadow: 0 1px 3px rgb(0 0 0 / 60%);
|
|
186
|
+
|
|
187
|
+
> span {
|
|
188
|
+
flex: 1;
|
|
189
|
+
min-width: 0;
|
|
190
|
+
@include truncate;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
.mw-overlay-btn {
|
|
194
|
+
margin-top: -#{spacing('3')};
|
|
195
|
+
background: transparent;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// The progress bar: the track and the fill drawn over it
|
|
199
|
+
&::before,
|
|
200
|
+
&::after {
|
|
201
|
+
content: '';
|
|
202
|
+
position: absolute;
|
|
203
|
+
top: max(#{spacing('3')}, env(safe-area-inset-top, 0px));
|
|
204
|
+
left: spacing('3');
|
|
205
|
+
right: spacing('3');
|
|
206
|
+
height: 2px;
|
|
207
|
+
border-radius: 2px;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
&::before {
|
|
211
|
+
background: rgb(255 255 255 / 35%);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
&::after {
|
|
215
|
+
background: $fixed-surface-text;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// A checkbox, so pausing needs no script: two bars, a triangle once paused
|
|
220
|
+
.mw-story-reel-pause {
|
|
221
|
+
position: relative;
|
|
222
|
+
|
|
223
|
+
input {
|
|
224
|
+
position: absolute;
|
|
225
|
+
inset: 0;
|
|
226
|
+
margin: 0;
|
|
227
|
+
opacity: 0;
|
|
228
|
+
cursor: pointer;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
&:has(:focus-visible) {
|
|
232
|
+
outline: var(--mw-focus-ring-width) solid var(--mw-focus-ring-color);
|
|
233
|
+
outline-offset: var(--mw-focus-ring-offset);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
&::before {
|
|
237
|
+
content: '';
|
|
238
|
+
width: 10px;
|
|
239
|
+
height: 13px;
|
|
240
|
+
border-inline: 3px solid currentColor;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
&:has(:checked)::before {
|
|
244
|
+
width: 0;
|
|
245
|
+
height: 0;
|
|
246
|
+
margin-left: 3px;
|
|
247
|
+
border-inline: 0;
|
|
248
|
+
border-left: 11px solid currentColor;
|
|
249
|
+
border-block: 7px solid transparent;
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// Built by the script: left a third back, the rest on
|
|
254
|
+
.mw-story-reel-prev,
|
|
255
|
+
.mw-story-reel-next {
|
|
256
|
+
position: absolute;
|
|
257
|
+
top: 0;
|
|
258
|
+
bottom: 0;
|
|
259
|
+
z-index: 1;
|
|
260
|
+
display: flex;
|
|
261
|
+
align-items: center;
|
|
262
|
+
padding: spacing('0') spacing('3');
|
|
263
|
+
border: none;
|
|
264
|
+
background: none;
|
|
265
|
+
color: $fixed-surface-text;
|
|
266
|
+
cursor: pointer;
|
|
267
|
+
@include tappable;
|
|
268
|
+
@include focus-ring-inset(-4px);
|
|
269
|
+
|
|
270
|
+
svg {
|
|
271
|
+
width: 1.75rem;
|
|
272
|
+
height: 1.75rem;
|
|
273
|
+
fill: none;
|
|
274
|
+
stroke: currentColor;
|
|
275
|
+
stroke-width: 2;
|
|
276
|
+
stroke-linecap: round;
|
|
277
|
+
stroke-linejoin: round;
|
|
278
|
+
opacity: 0;
|
|
279
|
+
filter: drop-shadow(0 1px 2px rgb(0 0 0 / 60%));
|
|
280
|
+
transition: opacity var(--mw-duration-fast) var(--mw-ease-out);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
&:focus-visible svg {
|
|
284
|
+
opacity: 1;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// A finger taps the halves blind; a mouse gets the arrows to aim at
|
|
288
|
+
@media (hover: hover) and (pointer: fine) {
|
|
289
|
+
svg {
|
|
290
|
+
opacity: 0.7;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
&:hover svg {
|
|
294
|
+
opacity: 1;
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
.mw-story-reel-prev {
|
|
300
|
+
left: 0;
|
|
301
|
+
width: 30%;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
.mw-story-reel-next {
|
|
305
|
+
right: 0;
|
|
306
|
+
width: 70%;
|
|
307
|
+
justify-content: flex-end;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// Longhands only in this file: the shorthand would reset the scroll timeline
|
|
311
|
+
@media (prefers-reduced-motion: no-preference) {
|
|
312
|
+
.mw-story-reel {
|
|
313
|
+
overflow: hidden;
|
|
314
|
+
scroll-snap-type: none;
|
|
315
|
+
|
|
316
|
+
> img,
|
|
317
|
+
> video {
|
|
318
|
+
position: absolute;
|
|
319
|
+
inset: 0;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
> :nth-child(n + 2 of img, video) {
|
|
323
|
+
visibility: hidden;
|
|
324
|
+
animation-name: mw-story-reel-frame;
|
|
325
|
+
animation-duration: 1ms;
|
|
326
|
+
animation-timing-function: step-end;
|
|
327
|
+
animation-fill-mode: forwards;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
@for $frame from 2 through $max-frames {
|
|
331
|
+
> :nth-child(#{$frame} of img, video) {
|
|
332
|
+
animation-delay: ($frame - 1) * $frame-time;
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
// Untimed, they would show at once on top of the first
|
|
337
|
+
> :nth-child(n + #{$max-frames + 1} of img, video) {
|
|
338
|
+
display: none;
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
.mw-story-reel-head::after {
|
|
343
|
+
clip-path: inset(0 100% 0 0);
|
|
344
|
+
animation-name: mw-story-reel-fill;
|
|
345
|
+
animation-duration: $frame-time;
|
|
346
|
+
animation-timing-function: linear;
|
|
347
|
+
animation-fill-mode: forwards;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
// One segment per frame, and the fill takes as long as all of them
|
|
351
|
+
@for $frames from 2 through $max-frames {
|
|
352
|
+
.mw-story-reel:has(
|
|
353
|
+
> :nth-child(#{$frames} of img, video):nth-last-child(1 of img, video)
|
|
354
|
+
)
|
|
355
|
+
.mw-story-reel-head {
|
|
356
|
+
&::before {
|
|
357
|
+
background: segments(rgb(255 255 255 / 35%), $frames);
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
&::after {
|
|
361
|
+
background: segments($fixed-surface-text, $frames);
|
|
362
|
+
animation-duration: $frames * $frame-time;
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
// A longer story ends at the last timed frame
|
|
368
|
+
.mw-story-reel:has(> :nth-child(#{$max-frames + 1} of img, video))
|
|
369
|
+
.mw-story-reel-head {
|
|
370
|
+
&::before {
|
|
371
|
+
background: segments(rgb(255 255 255 / 35%), $max-frames);
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
&::after {
|
|
375
|
+
background: segments($fixed-surface-text, $max-frames);
|
|
376
|
+
animation-duration: $max-frames * $frame-time;
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
.mw-story-reel:is(:active, :has(.mw-story-reel-pause :checked)) {
|
|
381
|
+
> *,
|
|
382
|
+
.mw-story-reel-head::after {
|
|
383
|
+
animation-play-state: paused;
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
// No autoplay with reduced motion: the frames stay a strip to page through
|
|
389
|
+
@media (prefers-reduced-motion: reduce) {
|
|
390
|
+
.mw-story-reel-pause,
|
|
391
|
+
.mw-story-reel-prev,
|
|
392
|
+
.mw-story-reel-next {
|
|
393
|
+
display: none;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
.mw-story-reel-head::after {
|
|
397
|
+
transform-origin: left;
|
|
398
|
+
transform: scaleX(0);
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
@supports (animation-timeline: scroll()) {
|
|
402
|
+
.mw-story-reel-head::after {
|
|
403
|
+
animation-name: mw-story-reel-scroll;
|
|
404
|
+
animation-timing-function: linear;
|
|
405
|
+
animation-fill-mode: both;
|
|
406
|
+
animation-timeline: scroll();
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
@supports not (animation-timeline: scroll()) {
|
|
411
|
+
.mw-story-reel-head {
|
|
412
|
+
&::before,
|
|
413
|
+
&::after {
|
|
414
|
+
display: none;
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
@keyframes mw-story-reel-frame {
|
|
421
|
+
to {
|
|
422
|
+
visibility: visible;
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
@keyframes mw-story-reel-fill {
|
|
427
|
+
to {
|
|
428
|
+
clip-path: inset(0);
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
@keyframes mw-story-reel-scroll {
|
|
433
|
+
to {
|
|
434
|
+
transform: scaleX(1);
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
// A popover does not lock the page scroll behind it
|
|
439
|
+
:root:has(.mw-story-reel:popover-open) {
|
|
440
|
+
overflow: hidden;
|
|
441
|
+
scrollbar-gutter: stable; // Or the page shifts by the hidden scrollbar
|
|
442
|
+
}
|
|
443
|
+
}
|
|
@@ -41,7 +41,10 @@
|
|
|
41
41
|
&:hover {
|
|
42
42
|
box-shadow: var(--mw-elevation-3);
|
|
43
43
|
border-color: var(--mw-border-accent);
|
|
44
|
-
|
|
44
|
+
|
|
45
|
+
&::after {
|
|
46
|
+
--mw-corner-accent: var(--mw-corner-accent-hover);
|
|
47
|
+
}
|
|
45
48
|
}
|
|
46
49
|
|
|
47
50
|
@include hover-move {
|
|
@@ -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
|
+
}
|