maverick-wave 5.38.0 → 6.0.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.
Files changed (69) hide show
  1. package/README.md +63 -47
  2. package/maverick-wave.min.css +94 -68
  3. package/maverick-wave.min.js +1 -1
  4. package/package.json +7 -7
  5. package/src/js/main.js +382 -57
  6. package/src/scss/abstracts/_mixins.scss +35 -24
  7. package/src/scss/abstracts/_variables.scss +15 -0
  8. package/src/scss/base/_base.scss +107 -64
  9. package/src/scss/base/_index.scss +1 -0
  10. package/src/scss/base/_page-transitions.scss +29 -0
  11. package/src/scss/base/_reset.scss +0 -8
  12. package/src/scss/components/_accordions.scss +23 -18
  13. package/src/scss/components/_action-bar.scss +143 -0
  14. package/src/scss/components/_alerts.scss +13 -1
  15. package/src/scss/components/_avatars.scss +17 -0
  16. package/src/scss/components/_badge.scss +12 -0
  17. package/src/scss/components/_breadcrumbs.scss +10 -2
  18. package/src/scss/components/_chat.scss +250 -0
  19. package/src/scss/components/_code.scss +3 -1
  20. package/src/scss/components/_compare.scss +135 -0
  21. package/src/scss/components/_deck.scss +8 -14
  22. package/src/scss/components/_devices.scss +323 -0
  23. package/src/scss/components/_dropdown.scss +43 -2
  24. package/src/scss/components/_empty-state.scss +43 -0
  25. package/src/scss/components/_flag.scss +16 -0
  26. package/src/scss/components/_index.scss +6 -0
  27. package/src/scss/components/_lightbox.scss +1 -1
  28. package/src/scss/components/_marquee.scss +132 -0
  29. package/src/scss/components/_modals.scss +112 -83
  30. package/src/scss/components/_occasions.scss +62 -11
  31. package/src/scss/components/_pagination.scss +91 -0
  32. package/src/scss/components/_portrait-gallery.scss +1 -1
  33. package/src/scss/components/_progress.scss +122 -2
  34. package/src/scss/components/_segmented.scss +65 -2
  35. package/src/scss/components/_skeleton.scss +8 -0
  36. package/src/scss/components/_spinners.scss +38 -0
  37. package/src/scss/components/_stat.scss +232 -0
  38. package/src/scss/components/_stepper.scss +42 -2
  39. package/src/scss/components/_stories.scss +36 -22
  40. package/src/scss/components/_tables.scss +160 -17
  41. package/src/scss/components/_tabs.scss +42 -4
  42. package/src/scss/components/_tags.scss +39 -4
  43. package/src/scss/components/_tiles.scss +1 -1
  44. package/src/scss/components/_timelines.scss +138 -1
  45. package/src/scss/components/_toasts.scss +5 -1
  46. package/src/scss/components/_tooltip.scss +13 -0
  47. package/src/scss/form-elements/_checkbox.scss +40 -7
  48. package/src/scss/form-elements/_form.scss +2 -4
  49. package/src/scss/form-elements/_input.scss +9 -6
  50. package/src/scss/form-elements/_radio.scss +32 -3
  51. package/src/scss/form-elements/_select.scss +44 -1
  52. package/src/scss/form-elements/_slider.scss +18 -0
  53. package/src/scss/form-elements/_textarea.scss +9 -6
  54. package/src/scss/form-elements/_toggle.scss +37 -4
  55. package/src/scss/layout/_app-shell.scss +661 -0
  56. package/src/scss/layout/_bento.scss +43 -0
  57. package/src/scss/layout/_footer.scss +114 -2
  58. package/src/scss/layout/_grid.scss +7 -2
  59. package/src/scss/layout/_header-reveal.scss +30 -17
  60. package/src/scss/layout/_header.scss +21 -4
  61. package/src/scss/layout/_index.scss +2 -0
  62. package/src/scss/layout/_parallax.scss +13 -11
  63. package/src/scss/utilities/_accessibility.scss +4 -19
  64. package/src/scss/utilities/_index.scss +1 -0
  65. package/src/scss/utilities/_print.scss +7 -3
  66. package/src/scss/utilities/_reveal.scss +25 -17
  67. package/src/scss/utilities/_shape.scss +24 -0
  68. package/src/scss/utilities/_touch-targets.scss +23 -0
  69. package/src/scss/utilities/_variants.scss +25 -6
@@ -29,18 +29,6 @@
29
29
  background: var(--mw-gray-background);
30
30
  }
31
31
 
32
- // The tinted surface at rest, not the hovered one. A row starts out
33
- // transparent, so it takes the whole step in one go - and at the hover tone
34
- // it lands close enough to the solid header above it to read as selected
35
- // rather than as pointed at. A badge or a tag is already sitting on the
36
- // resting tint, which is why the louder one is right there and not here.
37
- tr:hover {
38
- td {
39
- background: var(--mw-primary-background);
40
- color: var(--mw-text-color);
41
- }
42
- }
43
-
44
32
  @include media-up('sm') {
45
33
  th,
46
34
  td {
@@ -145,11 +133,10 @@
145
133
  }
146
134
  }
147
135
 
148
- // Hoverable rows
149
- .mw-table-hover {
150
- tbody tr:hover {
151
- background: var(--mw-primary-background);
152
- }
136
+ // The resting tint: the hover tone next to the solid header reads as selected
137
+ .mw-table-hover tbody tr:hover > td {
138
+ background: var(--mw-primary-background);
139
+ color: var(--mw-text-color);
153
140
  }
154
141
 
155
142
  // Quiet header for data-heavy views
@@ -192,4 +179,160 @@
192
179
  --mw-table-scroll-height: 400px;
193
180
  }
194
181
  }
182
+
183
+ // The button sorts, aria-sort says which way - the arrow only repeats it
184
+ .mw-table th[aria-sort] > button {
185
+ appearance: none;
186
+ padding: 0;
187
+ border: 0;
188
+ background: none;
189
+ color: inherit;
190
+ font: inherit;
191
+ cursor: pointer;
192
+ // The header's fill is the ring's usual tone
193
+ @include focus-ring($color: currentColor);
194
+ }
195
+
196
+ .mw-table th[aria-sort]::after {
197
+ content: '';
198
+ display: inline-block;
199
+ width: 0.55em;
200
+ height: 0.55em;
201
+ margin-left: spacing('2');
202
+ vertical-align: middle;
203
+ background: currentColor;
204
+ clip-path: polygon(50% 0, 100% 50%, 50% 100%, 0 50%);
205
+ opacity: 0.45;
206
+ }
207
+
208
+ .mw-table th[aria-sort='ascending']::after {
209
+ clip-path: polygon(50% 10%, 100% 90%, 0 90%);
210
+ opacity: 1;
211
+ }
212
+
213
+ .mw-table th[aria-sort='descending']::after {
214
+ clip-path: polygon(0 10%, 100% 10%, 50% 90%);
215
+ opacity: 1;
216
+ }
217
+
218
+ // Set by the app or by a checked first cell; an edge too, not colour alone
219
+ $_selected-row: ':is([aria-selected="true"], .mw-selected, :has(> :first-child input[type="checkbox"]:checked))';
220
+
221
+ .mw-table tr#{$_selected-row} > td {
222
+ background: var(--mw-primary-background-hover);
223
+ color: var(--mw-text-color);
224
+
225
+ &:first-child {
226
+ box-shadow: inset 3px 0 0 var(--mw-primary-text-color);
227
+ }
228
+ }
229
+
230
+ // After the table in the markup, so the counter has seen every row first
231
+ .mw-table-bulk {
232
+ counter-reset: mw-table-selected;
233
+ }
234
+
235
+ .mw-table-bulk tbody tr > :first-child input[type='checkbox']:checked {
236
+ counter-increment: mw-table-selected;
237
+ }
238
+
239
+ .mw-table-bulk-bar {
240
+ position: sticky;
241
+ bottom: calc(#{spacing('4')} + var(--mw-action-bar-reserve, 0px));
242
+ z-index: z-index('menu');
243
+ display: none;
244
+ align-items: center;
245
+ gap: spacing('3');
246
+ width: fit-content;
247
+ max-width: 100%;
248
+ margin: spacing('4') auto 0;
249
+ padding: spacing('2') spacing('2') spacing('2') spacing('5');
250
+ border-radius: radius-sharp() radius('lg') radius-sharp() radius('lg');
251
+ background: var(--mw-header-background);
252
+ color: var(--mw-header-text-color);
253
+ box-shadow: var(--mw-elevation-4);
254
+ // Dark in both themes like the header bar, so what sits in it reads dark too
255
+ color-scheme: dark;
256
+ --mw-focus-ring-color: var(--mw-header-navbar-list-active-color);
257
+ }
258
+
259
+ .mw-table-bulk:has(tbody tr > :first-child input[type='checkbox']:checked)
260
+ .mw-table-bulk-bar {
261
+ display: flex;
262
+ }
263
+
264
+ .mw-table-bulk-count {
265
+ font-weight: font-weight('medium');
266
+ font-variant-numeric: tabular-nums;
267
+ white-space: nowrap;
268
+
269
+ // Empty, the bar counts for itself; an app that knows the number writes it
270
+ &:empty::before {
271
+ content: counter(mw-table-selected);
272
+ }
273
+ }
274
+
275
+ // Sticky cells need an opaque tone of their own, or the row slides through
276
+ .mw-table-sticky-col :is(th, td):first-child {
277
+ position: sticky;
278
+ left: 0;
279
+ z-index: z-index('lift');
280
+ // A collapsed border stays behind when the cell sticks, so it draws its own
281
+ box-shadow: inset -1px 0 0 var(--mw-border);
282
+ }
283
+
284
+ .mw-table-sticky-col th:first-child {
285
+ z-index: z-index('lift') + 1;
286
+ }
287
+
288
+ .mw-table-sticky-col td:first-child {
289
+ background: var(--mw-card-background);
290
+ }
291
+
292
+ .mw-table-sticky-col tr:nth-child(even) > td:first-child {
293
+ background: color-mix(
294
+ in srgb,
295
+ var(--mw-gray-color) 20%,
296
+ var(--mw-card-background)
297
+ );
298
+ }
299
+
300
+ .mw-table-sticky-col.mw-table-hover tr:hover > td:first-child {
301
+ background: color-mix(
302
+ in srgb,
303
+ var(--mw-primary-color) 30%,
304
+ var(--mw-card-background)
305
+ );
306
+ }
307
+
308
+ .mw-table-sticky-col tr#{$_selected-row} > td:first-child {
309
+ background: color-mix(
310
+ in srgb,
311
+ var(--mw-primary-color) 50%,
312
+ var(--mw-card-background)
313
+ );
314
+ box-shadow:
315
+ inset 3px 0 0 var(--mw-primary-text-color),
316
+ inset -1px 0 0 var(--mw-border);
317
+ }
318
+
319
+ // Forced colours drop fills: selection in Highlight, the arrow in text colour
320
+ @media (forced-colors: active) {
321
+ .mw-table tr#{$_selected-row} > td,
322
+ .mw-table-sticky-col tr#{$_selected-row} > td:first-child {
323
+ forced-color-adjust: none;
324
+ background: Highlight;
325
+ color: HighlightText;
326
+ box-shadow: none;
327
+
328
+ * {
329
+ color: inherit;
330
+ }
331
+ }
332
+
333
+ .mw-table th[aria-sort]::after {
334
+ forced-color-adjust: none;
335
+ background: CanvasText;
336
+ }
337
+ }
195
338
  }
@@ -1,6 +1,17 @@
1
1
  @use '../abstracts' as *;
2
2
 
3
3
  @layer mw.components {
4
+ // aria-selected carries the same state, for a tablist an application renders
5
+ @mixin _selected {
6
+ @include active {
7
+ @content;
8
+ }
9
+
10
+ &[aria-selected='true'] {
11
+ @content;
12
+ }
13
+ }
14
+
4
15
  // Registered, so a half-written value from the script cannot invalidate the
5
16
  // whole `transform` and leave the marker somewhere other than at rest.
6
17
  // `inherits: true` because the bar is the nav's ::after, and a pseudo element
@@ -85,10 +96,16 @@
85
96
  transition-duration: var(--mw-duration-instant);
86
97
  }
87
98
 
88
- @include active() {
99
+ @include _selected {
89
100
  color: var(--mw-primary-text-color);
90
101
  border-bottom-color: var(--mw-primary-text-color);
91
102
  }
103
+
104
+ &:is(:disabled, [aria-disabled='true']) {
105
+ opacity: 0.5;
106
+ cursor: not-allowed;
107
+ pointer-events: none;
108
+ }
92
109
  }
93
110
 
94
111
  // The active tab is marked by a bar that travels to it rather than appearing
@@ -177,7 +194,7 @@
177
194
  border-bottom: none;
178
195
  border-right: 2px solid transparent;
179
196
 
180
- @include active() {
197
+ @include _selected {
181
198
  border-right-color: var(--mw-primary-text-color);
182
199
  }
183
200
 
@@ -186,7 +203,7 @@
186
203
  border-bottom: 2px solid transparent;
187
204
  flex-shrink: 0;
188
205
 
189
- @include active() {
206
+ @include _selected {
190
207
  border-right-color: transparent;
191
208
  border-bottom-color: var(--mw-primary-text-color);
192
209
  }
@@ -273,7 +290,7 @@
273
290
  border: 1px solid var(--mw-border-accent);
274
291
  background: var(--mw-card-background);
275
292
 
276
- @include active() {
293
+ @include _selected {
277
294
  background: var(--mw-primary-color);
278
295
  color: var(--mw-primary-accent-text-color);
279
296
  }
@@ -289,4 +306,25 @@
289
306
  opacity: 1;
290
307
  }
291
308
  }
309
+
310
+ // Forced colours drop the travelling bar and the pill's fill
311
+ @media (forced-colors: active) {
312
+ .mw-tabs-nav[data-mw-tabs-marker]::after {
313
+ forced-color-adjust: none;
314
+ background: Highlight;
315
+ }
316
+
317
+ .mw-tabs-nav-item:is(.mw-active, .active, [aria-selected='true']) {
318
+ text-decoration: underline 2px;
319
+ text-underline-offset: 0.35em;
320
+ }
321
+
322
+ .mw-tabs-pills
323
+ .mw-tabs-nav-item:is(.mw-active, .active, [aria-selected='true']) {
324
+ forced-color-adjust: none;
325
+ background: Highlight;
326
+ color: HighlightText;
327
+ text-decoration: none;
328
+ }
329
+ }
292
330
  }
@@ -32,7 +32,7 @@
32
32
  transition: var(--mw-transition-fast);
33
33
  user-select: none;
34
34
 
35
- &:hover {
35
+ &:is(a, button):hover {
36
36
  background: var(--mw-primary-background-hover);
37
37
  }
38
38
  }
@@ -77,7 +77,7 @@
77
77
  background-color: var(--mw-#{$name}-#{$infix}background);
78
78
  border-color: var(--mw-#{$name}-color);
79
79
 
80
- &:hover {
80
+ &:is(a, button):hover {
81
81
  background: var(--mw-#{$name}-#{$infix}background-hover);
82
82
  }
83
83
  }
@@ -90,7 +90,7 @@
90
90
  border-color: var(--mw-border);
91
91
  color: var(--mw-text-muted-color);
92
92
 
93
- &:hover {
93
+ &:is(a, button):hover {
94
94
  background-color: var(--mw-gray-background);
95
95
  border-color: var(--mw-text-muted-color);
96
96
  color: var(--mw-text-color);
@@ -107,7 +107,7 @@
107
107
  border-color: var(--mw-border);
108
108
  color: var(--mw-text-color);
109
109
 
110
- &:hover {
110
+ &:is(a, button):hover {
111
111
  background-color: var(--mw-surface-muted);
112
112
  border-color: var(--mw-border-accent);
113
113
  }
@@ -135,4 +135,39 @@
135
135
  }
136
136
  }
137
137
  }
138
+
139
+ // A tag that does something - a filter, a link - is a control: ring and floor
140
+ :is(a, button).mw-tag {
141
+ font-family: inherit;
142
+ cursor: pointer;
143
+ text-decoration: none;
144
+ @include tappable;
145
+ @include focus-ring;
146
+ @include touch-floor(2.25rem);
147
+ }
148
+
149
+ // Pressed fills and ticks, so the state never rests on colour alone
150
+ :is(a, button).mw-tag:is([aria-pressed='true'], .mw-active) {
151
+ background: var(--mw-primary-color);
152
+ border-color: var(--mw-primary-color);
153
+ color: var(--mw-primary-accent-text-color);
154
+
155
+ &::before {
156
+ content: '';
157
+ width: 0.3em;
158
+ height: 0.6em;
159
+ margin: 0 0.55em 0.15em 0.1em;
160
+ border: solid currentColor;
161
+ border-width: 0 0.15em 0.15em 0;
162
+ rotate: 45deg;
163
+ }
164
+ }
165
+
166
+ @each $name, $infix in $_mw-tag-colors {
167
+ :is(a, button).mw-tag-#{$name}:is([aria-pressed='true'], .mw-active) {
168
+ background: var(--mw-#{$name}-color);
169
+ border-color: var(--mw-#{$name}-color);
170
+ color: var(--mw-#{$name}-accent-text-color);
171
+ }
172
+ }
138
173
  }
@@ -60,7 +60,7 @@
60
60
  // A tile is tapped, so it needs a press of its own - the lift above never
61
61
  // runs on a finger. Guarded like every other travel in the framework; what
62
62
  // is left without it is the latched hover, which is feedback enough.
63
- @media (prefers-reduced-motion: no-preference) {
63
+ @include motion-safe {
64
64
  &:active {
65
65
  transform: scale(0.985);
66
66
  transition-duration: var(--mw-duration-instant);
@@ -16,10 +16,13 @@
16
16
  }
17
17
  }
18
18
 
19
+ // Never further than the page gutter, or the slide scrolls the page sideways
19
20
  @keyframes mw-slide-in-right {
20
21
  from {
21
22
  opacity: 0;
22
- transform: translateX(60px);
23
+ transform: translateX(
24
+ min(60px, calc(var(--mw-container-gutter) - #{spacing('3')}))
25
+ );
23
26
  }
24
27
  to {
25
28
  opacity: 1;
@@ -27,6 +30,18 @@
27
30
  }
28
31
  }
29
32
 
33
+ // From the side, a full-width column runs past the screen edge while it plays
34
+ @keyframes mw-timeline-rise {
35
+ from {
36
+ opacity: 0;
37
+ transform: translateY(24px);
38
+ }
39
+ to {
40
+ opacity: 1;
41
+ transform: translateY(0);
42
+ }
43
+ }
44
+
30
45
  @keyframes mw-pulse-ring-big {
31
46
  0% {
32
47
  box-shadow: 0 0 0 0 var(--mw-primary-text-color);
@@ -223,6 +238,7 @@
223
238
  // Override odd/even: content always full-width below the date
224
239
  &-step:nth-child(odd) &-content,
225
240
  &-step:nth-child(even) &-content {
241
+ animation-name: mw-timeline-rise;
226
242
  align-self: stretch;
227
243
  width: 100%;
228
244
  padding-left: 0;
@@ -305,6 +321,14 @@
305
321
  opacity: 0;
306
322
  }
307
323
 
324
+ // Every step: the stagger covers ten, an eleventh stayed at opacity 0
325
+ &-step &-content {
326
+ animation-name: mw-slide-in-right;
327
+ animation-duration: var(--mw-duration-slow);
328
+ animation-timing-function: var(--mw-ease-out);
329
+ animation-fill-mode: forwards;
330
+ }
331
+
308
332
  // Staggered animation
309
333
  @for $i from 1 through 10 {
310
334
  &-step:nth-child(#{$i}) &-content {
@@ -316,6 +340,10 @@
316
340
  }
317
341
  }
318
342
 
343
+ &-step:nth-child(n + 11) &-content {
344
+ animation-delay: 2.2s;
345
+ }
346
+
319
347
  // Active step
320
348
  &-step.mw-active &-date,
321
349
  &-step.active &-date {
@@ -351,6 +379,115 @@
351
379
  align-self: stretch;
352
380
  padding-left: spacing('3');
353
381
  }
382
+
383
+ &-step:nth-child(n) &-content {
384
+ animation-name: mw-timeline-rise;
385
+ }
386
+ }
387
+ }
388
+
389
+ // An activity feed or audit log: many lines, no entrance to wait for
390
+ .mw-timeline-compact {
391
+ position: relative;
392
+ margin: 0;
393
+ padding: 0;
394
+ list-style: none;
395
+
396
+ &::before {
397
+ content: '';
398
+ position: absolute;
399
+ top: spacing('2');
400
+ bottom: spacing('2');
401
+ left: calc(#{spacing('2')} - 1px);
402
+ width: 2px;
403
+ background: var(--mw-border);
404
+ }
405
+ }
406
+
407
+ .mw-timeline-compact-item {
408
+ --mw-timeline-dot: var(--mw-primary-text-color);
409
+
410
+ position: relative;
411
+ display: grid;
412
+ grid-template-columns: minmax(0, 1fr);
413
+ gap: 0 spacing('3');
414
+ padding: 0 0 spacing('4') spacing('6');
415
+ font-size: font-size('sm');
416
+
417
+ &:last-child {
418
+ padding-bottom: 0;
419
+ }
420
+
421
+ // The dot sits on the line, ringed in the page colour so the line breaks
422
+ &::before {
423
+ content: '';
424
+ position: absolute;
425
+ top: 0.4em;
426
+ left: 0;
427
+ width: spacing('4');
428
+ height: spacing('4');
429
+ border-radius: radius('full');
430
+ background: var(--mw-timeline-dot);
431
+ box-shadow: 0 0 0 3px var(--mw-page-background);
432
+ }
433
+
434
+ @include media-up('sm') {
435
+ grid-template-columns: 6.5rem minmax(0, 1fr);
436
+ }
437
+ }
438
+
439
+ .mw-timeline-compact-time {
440
+ color: var(--mw-text-muted-color);
441
+ font-size: font-size('xs');
442
+ font-variant-numeric: tabular-nums;
443
+ white-space: nowrap;
444
+
445
+ @include media-up('sm') {
446
+ padding-top: 0.15em;
447
+ }
448
+ }
449
+
450
+ .mw-timeline-compact-body {
451
+ min-width: 0;
452
+ color: var(--mw-text-color);
453
+ overflow-wrap: anywhere;
454
+ }
455
+
456
+ @each $name, $col in $semantic-colors {
457
+ .mw-timeline-compact-#{$name} {
458
+ --mw-timeline-dot: var(--mw-#{$name}-text-color);
459
+ }
460
+ }
461
+
462
+ // On a surface the ring has to match the surface, not the page
463
+ :is(.mw-card, .mw-panel, .mw-modal, .mw-app-aside)
464
+ .mw-timeline-compact-item::before {
465
+ box-shadow: 0 0 0 3px var(--mw-card-background);
466
+ }
467
+
468
+ // Forced colours drop the lines and dots (fills) and the active date's colour
469
+ @media (forced-colors: active) {
470
+ .mw-timeline-big::before,
471
+ .mw-timeline-simple::before,
472
+ .mw-timeline-compact::before,
473
+ .mw-timeline-compact-item::before {
474
+ forced-color-adjust: none;
475
+ background: CanvasText;
476
+ }
477
+
478
+ .mw-timeline-big-step:is(.mw-active, .active)
479
+ .mw-timeline-big-date-container,
480
+ .mw-timeline-simple-step:is(.mw-active, .active) .mw-timeline-simple-date {
481
+ forced-color-adjust: none;
482
+ background: Highlight;
483
+ color: HighlightText;
484
+
485
+ // The date classes named too: they outweigh a bare `*`
486
+ *,
487
+ .mw-timeline-big-date-main,
488
+ .mw-timeline-big-date-sub {
489
+ color: inherit;
490
+ }
354
491
  }
355
492
  }
356
493
  }
@@ -37,11 +37,15 @@
37
37
  transform: translateX(-50%);
38
38
  }
39
39
 
40
+ // The reserve only exists while an action bar holds the foot of the screen
40
41
  &-bottom-right {
41
- bottom: spacing('5');
42
+ bottom: calc(#{spacing('5')} + var(--mw-action-bar-reserve, 0px));
42
43
  right: spacing('5');
43
44
  top: auto;
44
45
  left: auto;
46
+ max-height: calc(
47
+ 100dvh - #{2 * spacing('5')} - var(--mw-action-bar-reserve, 0px)
48
+ );
45
49
  flex-direction: column-reverse;
46
50
 
47
51
  > * {
@@ -188,6 +188,19 @@
188
188
  position-area: block-start;
189
189
  transform: rotate(45deg);
190
190
  }
191
+
192
+ // Beside the trigger, for an icon rail - anchored only, or a scroller cuts it
193
+ &.mw-tooltip-inline-end {
194
+ &::before {
195
+ position-area: inline-end;
196
+ margin: 0 0 0 ($_mw-tooltip-gap + $_mw-tooltip-arrow);
197
+ }
198
+
199
+ &::after {
200
+ position-area: inline-end;
201
+ margin: 0 0 0 $_mw-tooltip-arrow-inset;
202
+ }
203
+ }
191
204
  }
192
205
  }
193
206
  }
@@ -37,13 +37,18 @@
37
37
  cursor: pointer;
38
38
  user-select: none;
39
39
  position: relative;
40
+ isolation: isolate;
40
41
 
42
+ // Row-sized for touch exploration, behind the content so a label link works
41
43
  input[type='checkbox'] {
42
44
  position: absolute;
43
- opacity: 0;
44
- width: 0;
45
- height: 0;
45
+ inset: 0;
46
+ z-index: -1;
47
+ width: 100%;
48
+ height: 100%;
46
49
  margin: spacing('0');
50
+ opacity: 0;
51
+ cursor: inherit;
47
52
  }
48
53
 
49
54
  .mw-checkbox-box {
@@ -76,13 +81,30 @@
76
81
  border-color: var(--mw-primary-color);
77
82
  }
78
83
 
79
- input[type='checkbox']:checked + .mw-checkbox-box {
84
+ &:hover
85
+ input[type='checkbox']:not(:checked, :indeterminate, :disabled)
86
+ + .mw-checkbox-box {
87
+ border-color: var(--mw-gray-color);
88
+ }
89
+
90
+ input[type='checkbox']:is(:checked, :indeterminate) + .mw-checkbox-box {
80
91
  border-color: var(--mw-primary-color);
81
92
  }
82
93
  input[type='checkbox']:checked + .mw-checkbox-box::after {
83
94
  transform: translate(-50%, -60%) rotate(-45deg) scale(1);
84
95
  }
85
96
 
97
+ // A bar instead of the tick - the "some of them" state of a select-all box
98
+ input[type='checkbox']:indeterminate + .mw-checkbox-box::after {
99
+ height: 0;
100
+ border-left-width: 0;
101
+ transform: translate(-50%, -50%) scale(1);
102
+ }
103
+
104
+ input[type='checkbox']:is(#{$invalid-states}) + .mw-checkbox-box {
105
+ border-color: var(--mw-danger-color);
106
+ }
107
+
86
108
  .mw-checkbox-label {
87
109
  font-size: font-size('base');
88
110
  color: var(--mw-text-color);
@@ -119,16 +141,19 @@
119
141
 
120
142
  @each $name, $col in $semantic-colors {
121
143
  &-#{$name} {
122
- input[type='checkbox']:checked + .mw-checkbox-box {
144
+ input[type='checkbox']:is(:checked, :indeterminate) + .mw-checkbox-box {
123
145
  border-color: #{$col};
124
146
  }
125
- input[type='checkbox']:checked + .mw-checkbox-box::after {
147
+ input[type='checkbox']:is(:checked, :indeterminate)
148
+ + .mw-checkbox-box::after {
126
149
  border-color: #{$col};
127
150
  }
128
151
  }
129
152
  }
130
153
 
131
- &-disabled {
154
+ // The attribute too: a reactive form disables the input, not the label
155
+ &-disabled,
156
+ &:has(input[type='checkbox']:disabled) {
132
157
  cursor: not-allowed;
133
158
 
134
159
  input[type='checkbox'] {
@@ -142,4 +167,12 @@
142
167
  }
143
168
  }
144
169
  }
170
+
171
+ // Forced colours drop the halo, which is the whole focus indicator here
172
+ @media (forced-colors: active) {
173
+ .mw-checkbox input[type='checkbox']:focus-visible + .mw-checkbox-box {
174
+ outline: 2px solid;
175
+ outline-offset: 2px;
176
+ }
177
+ }
145
178
  }
@@ -169,14 +169,12 @@
169
169
  // mw-field-has-error. `:user-invalid` and never `:invalid`, which is true from
170
170
  // page load and would mark an empty required field before anyone typed.
171
171
  // The class stays authoritative for a validator that lives in the application.
172
- .mw-input:user-invalid,
173
- .mw-select:user-invalid,
174
- .mw-textarea:user-invalid {
172
+ :is(.mw-input, .mw-select, .mw-textarea):is(#{$invalid-states}) {
175
173
  border-color: var(--mw-danger-color);
176
174
  @include focus-halo('danger-text-color');
177
175
  }
178
176
 
179
- .mw-field:has(> :user-invalid) > .mw-field-hint {
177
+ .mw-field:has(> :is(#{$invalid-states})) > .mw-field-hint {
180
178
  color: var(--mw-danger-text-color);
181
179
  }
182
180
  }