@slotted/styles 0.1.0-beta.10 → 0.1.0-beta.11

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@slotted/styles",
3
- "version": "0.1.0-beta.10",
3
+ "version": "0.1.0-beta.11",
4
4
  "license": "MIT",
5
5
  "repository": {
6
6
  "type": "git",
@@ -380,6 +380,11 @@
380
380
  * breakpoint is. While it is raised the collapse does not run: a column has
381
381
  * room for every action, and a surface holding what did not fit would be a
382
382
  * menu for nothing.
383
+ *
384
+ * A bar that holds an overflow control does not stack: it keeps one row, the
385
+ * actions without a counterpart stay and the rest fold into the menu. It
386
+ * stacked once, hid its control, and three actions with counterparts stood
387
+ * in a bar 164 pixels tall on a 358 pixel phone.
383
388
  */
384
389
  @container slotted-action-bar (width < 24rem) {
385
390
  /**
@@ -388,7 +393,7 @@
388
393
  * the order a form is read in, and only the paint is reversed. `stretch`
389
394
  * fills the column, because a column of natural widths is a ragged edge.
390
395
  */
391
- [data-ui-action-bar] [data-part-actions] {
396
+ [data-ui-action-bar]:not(:has([data-part-overflow])) [data-part-actions] {
392
397
  --_stacked: 1;
393
398
 
394
399
  align-items: stretch;
@@ -397,26 +402,15 @@
397
402
  margin-inline: 0;
398
403
  }
399
404
 
400
- [data-ui-action-bar] [data-part-action] > * {
405
+ [data-ui-action-bar]:not(:has([data-part-overflow])) [data-part-action] > * {
401
406
  inline-size: 100%;
402
407
  }
403
408
 
404
- /**
405
- * While the bar is a column there is no overflow, because a column has room
406
- * for every action — the binding stops measuring, and the rule says the same
407
- * thing on the side a reader is looking at. Without it the trigger is
408
- * painted at the top of the column by the reversal, which is where the
409
- * action that confirms belongs.
410
- */
411
- [data-ui-action-bar] [data-part-overflow] {
412
- display: none;
413
- }
414
-
415
409
  /**
416
410
  * A rule that divided two controls in a row divides two rows here, so it
417
411
  * turns with them.
418
412
  */
419
- [data-ui-action-bar] [data-part-separator] {
413
+ [data-ui-action-bar]:not(:has([data-part-overflow])) [data-part-separator] {
420
414
  block-size: var(--slotted-border-width-thin, 1px);
421
415
  inline-size: auto;
422
416
  margin-block: calc(var(--_gap) / 2);
@@ -424,14 +418,14 @@
424
418
  min-block-size: 0;
425
419
  }
426
420
 
427
- [data-ui-action-bar] [data-part-status] {
421
+ [data-ui-action-bar]:not(:has([data-part-overflow])) [data-part-status] {
428
422
  flex: 1 1 100%;
429
423
  }
430
424
 
431
425
  /* What the consumer put in the status, addressed as the part's child and
432
426
  never by a class of its own: while the bar is a column the status fills
433
427
  the row, as an action does two rules above. */
434
- [data-ui-action-bar] [data-part-status] > * {
428
+ [data-ui-action-bar]:not(:has([data-part-overflow])) [data-part-status] > * {
435
429
  align-content: space-between;
436
430
  inline-size: 100%;
437
431
  }
@@ -676,14 +676,20 @@
676
676
  }
677
677
 
678
678
  /**
679
- * The label carries a wrapping line height of its own: the control's line
680
- * height is 1, which centres a single line inside the box and crushes the
681
- * second one. A token longer than the container breaks instead of escaping.
679
+ * The label is one line at its own width. It broke anywhere once, which made
680
+ * a button in a row as narrow as one letter: squeezed, "Corrigir" became
681
+ * three lines and broke inside the word. A row of buttons now overflows, or
682
+ * a toolbar folds what does not fit; a control held narrower than its label
683
+ * by its container — `max-inline-size: 100%` — cuts the label with an
684
+ * ellipsis rather than spilling it past the border. The line height is the
685
+ * text step's, which the trim below reads.
682
686
  */
683
687
  [data-ui-button] [data-part-label] {
684
688
  line-height: var(--slotted-line-height-sm, 1.45);
685
689
  min-inline-size: 0;
686
- overflow-wrap: anywhere;
690
+ overflow: hidden;
691
+ text-overflow: ellipsis;
692
+ white-space: nowrap;
687
693
  }
688
694
 
689
695
  /**
@@ -15,8 +15,9 @@
15
15
  * the ends of the bar and nowhere else — and states no distance between its
16
16
  * controls. A joined bar does not wrap and does not stack: its shape is the
17
17
  * whole point of it, and no surveyed implementation flips that shape at a
18
- * width the consumer cannot see coming. A label that outgrows the bar wraps
19
- * inside its own control, which is `button`'s rule and not this family's.
18
+ * width the consumer cannot see coming. A label that outgrows the bar ends
19
+ * in an ellipsis inside its own control, which is `button`'s rule and not
20
+ * this family's.
20
21
  *
21
22
  * The children are the consumer's own elements, selected as direct children.
22
23
  * Primer wraps each child in a div of its own and pays for it twice: its
@@ -135,6 +135,25 @@
135
135
  overflow: clip;
136
136
  }
137
137
 
138
+ /**
139
+ * A theme draws the panel as a material with this filter beside a surface
140
+ * with alpha: `--slotted-command-palette-backdrop-filter`, `none` unless set.
141
+ * The root is the one layer that paints a ground under either presentation.
142
+ * A reader who asked for less transparency gets no filter, and under forced
143
+ * colours the ground is the system's.
144
+ */
145
+ [data-ui-command-palette] {
146
+ -webkit-backdrop-filter: var(--slotted-command-palette-backdrop-filter, none);
147
+ backdrop-filter: var(--slotted-command-palette-backdrop-filter, none);
148
+ }
149
+
150
+ @media (prefers-reduced-transparency: reduce), (forced-colors: active) {
151
+ [data-ui-command-palette] {
152
+ -webkit-backdrop-filter: none;
153
+ backdrop-filter: none;
154
+ }
155
+ }
156
+
138
157
  /* ── Size ─────────────────────────────────────────────────────────────── */
139
158
 
140
159
  [data-ui-command-palette][data-size='sm'] {
@@ -2,6 +2,7 @@
2
2
  "--slotted-border-subtle",
3
3
  "--slotted-border-width-thin",
4
4
  "--slotted-command-palette-active",
5
+ "--slotted-command-palette-backdrop-filter",
5
6
  "--slotted-command-palette-border-color",
6
7
  "--slotted-command-palette-footer-surface",
7
8
  "--slotted-command-palette-highlight",
@@ -812,10 +812,11 @@
812
812
 
813
813
  /* The last column's handle stays inside the row: half a target hanging
814
814
  past the last cell was twelve pixels of overflow on a row nothing could
815
- scroll to. */
815
+ scroll to. The last column header, not the row's last child: the pop-in
816
+ header follows it when columns yield to the frame. */
816
817
  [data-ui-data-grid]
817
818
  [data-part-header-row]
818
- > [data-part-column-header]:last-child
819
+ > [data-part-column-header]:not(:has(~ [data-part-column-header]))
819
820
  > [data-part-resizer] {
820
821
  inline-size: calc(var(--slotted-target-min, 24px) / 2);
821
822
  inset-inline-end: 0;
@@ -840,6 +841,30 @@
840
841
  background-color: var(--_cursor);
841
842
  }
842
843
 
844
+ /**
845
+ * Under `resizeMode="deferred"` the drag moves this line and nothing else:
846
+ * it stands on the column's edge, spans the scroller's visible height and
847
+ * slides by `translate`, which lays nothing out; the width is applied once,
848
+ * on release.
849
+ */
850
+ [data-ui-data-grid] [data-part-resizer] > [data-part-resize-guide] {
851
+ background-color: var(
852
+ --slotted-data-grid-resize-guide-color,
853
+ var(--slotted-focus-ring-color, Highlight)
854
+ );
855
+ inline-size: var(
856
+ --slotted-data-grid-resize-guide-width,
857
+ var(--slotted-border-width-thick, 2px)
858
+ );
859
+ inset-block-start: 0;
860
+ inset-inline-start: calc(
861
+ 50% - var(--slotted-data-grid-resize-guide-width, var(--slotted-border-width-thick, 2px)) / 2
862
+ );
863
+ pointer-events: none;
864
+ position: absolute;
865
+ z-index: 2;
866
+ }
867
+
843
868
  /**
844
869
  * ── Reordering ─────────────────────────────────────────────────────────
845
870
  *
@@ -2414,6 +2439,11 @@
2414
2439
  * ── Scheme and platform ────────────────────────────────────────────────
2415
2440
  */
2416
2441
  @media (forced-colors: active) {
2442
+ [data-ui-data-grid] [data-part-resizer] > [data-part-resize-guide] {
2443
+ background-color: Highlight;
2444
+ forced-color-adjust: none;
2445
+ }
2446
+
2417
2447
  [data-ui-data-grid] [role='gridcell'][data-current],
2418
2448
  [data-ui-data-grid] [role='columnheader'][data-current] {
2419
2449
  box-shadow: inset 0 0 0 var(--_ring-width) Highlight;
@@ -45,6 +45,8 @@
45
45
  "--slotted-data-grid-pin-shadow-size",
46
46
  "--slotted-data-grid-radius",
47
47
  "--slotted-data-grid-range-surface",
48
+ "--slotted-data-grid-resize-guide-color",
49
+ "--slotted-data-grid-resize-guide-width",
48
50
  "--slotted-data-grid-row-block-size",
49
51
  "--slotted-data-grid-row-color-selected",
50
52
  "--slotted-data-grid-row-max-block-size",
@@ -318,7 +318,9 @@
318
318
  * column above.
319
319
  */
320
320
  [data-ui-detail-panel] > [data-part-footer] {
321
- background-color: var(--_ground);
321
+ -webkit-backdrop-filter: var(--slotted-detail-panel-footer-backdrop-filter, none);
322
+ backdrop-filter: var(--slotted-detail-panel-footer-backdrop-filter, none);
323
+ background-color: var(--slotted-detail-panel-footer-background, var(--_ground));
322
324
  border-block-start: var(--_rule-size) solid var(--_rule);
323
325
  inset-block-end: var(--slotted-page-sticky-bottom, 0px);
324
326
  margin-block-start: auto;
@@ -326,4 +328,17 @@
326
328
  position: sticky;
327
329
  z-index: var(--slotted-z-index-sticky, 100);
328
330
  }
331
+
332
+ /**
333
+ * A theme draws the footer as a material with its own ground and filter,
334
+ * apart from the ground a sticky section header reads. A reader who asked
335
+ * for less transparency gets no filter, and under forced colours the ground
336
+ * is the system's.
337
+ */
338
+ @media (prefers-reduced-transparency: reduce), (forced-colors: active) {
339
+ [data-ui-detail-panel] > [data-part-footer] {
340
+ -webkit-backdrop-filter: none;
341
+ backdrop-filter: none;
342
+ }
343
+ }
329
344
  }
@@ -6,6 +6,8 @@
6
6
  "--slotted-detail-panel-aside-size-md",
7
7
  "--slotted-detail-panel-aside-size-sm",
8
8
  "--slotted-detail-panel-border",
9
+ "--slotted-detail-panel-footer-backdrop-filter",
10
+ "--slotted-detail-panel-footer-background",
9
11
  "--slotted-detail-panel-gap-lg",
10
12
  "--slotted-detail-panel-gap-md",
11
13
  "--slotted-detail-panel-gap-sm",
@@ -356,6 +356,21 @@
356
356
  */
357
357
  opacity: 0;
358
358
  transition: opacity var(--slotted-duration-short) var(--slotted-easing-exit);
359
+
360
+ /**
361
+ * A theme draws the panel as a material with a surface that has alpha and
362
+ * this filter, `none` unless set. The panel's own fade keeps the filter
363
+ * drawn; an ancestor's opacity below one is what removes it.
364
+ */
365
+ -webkit-backdrop-filter: var(--slotted-dialog-backdrop-filter, none);
366
+ backdrop-filter: var(--slotted-dialog-backdrop-filter, none);
367
+ }
368
+
369
+ @media (prefers-reduced-transparency: reduce), (forced-colors: active) {
370
+ [data-ui-dialog] > [data-part-viewport] > [data-part-content] {
371
+ -webkit-backdrop-filter: none;
372
+ backdrop-filter: none;
373
+ }
359
374
  }
360
375
 
361
376
  /**
@@ -1045,6 +1060,29 @@
1045
1060
  overflow: visible;
1046
1061
  }
1047
1062
 
1063
+ /**
1064
+ * The children fade in the panel's place, at the panel's durations. A plain
1065
+ * panel paints nothing, so the picture is the same; what changes is that no
1066
+ * ancestor of a child's material is below full opacity, which in Chromium
1067
+ * would draw no backdrop filter until the fade ended — a command palette
1068
+ * drawn as a material showed the page sharp behind it for the whole fade.
1069
+ */
1070
+ [data-ui-dialog][data-surface='plain'] > [data-part-viewport] > [data-part-content] {
1071
+ opacity: 1;
1072
+ transition: none;
1073
+ }
1074
+ [data-ui-dialog][data-surface='plain'] > [data-part-viewport] > [data-part-content] > * {
1075
+ opacity: 0;
1076
+ transition: opacity var(--slotted-duration-short) var(--slotted-easing-exit);
1077
+ }
1078
+ [data-ui-dialog][data-surface='plain'][data-open]
1079
+ > [data-part-viewport]
1080
+ > [data-part-content]
1081
+ > * {
1082
+ opacity: 1;
1083
+ transition: opacity var(--slotted-duration-long) var(--slotted-easing-standard);
1084
+ }
1085
+
1048
1086
  [data-ui-dialog][data-surface='plain']
1049
1087
  > [data-part-viewport]
1050
1088
  > [data-part-content]
@@ -8,6 +8,7 @@
8
8
  "--slotted-control-radius",
9
9
  "--slotted-control-transition-duration",
10
10
  "--slotted-control-transition-easing",
11
+ "--slotted-dialog-backdrop-filter",
11
12
  "--slotted-dialog-body-color",
12
13
  "--slotted-dialog-border-color",
13
14
  "--slotted-dialog-close-active-background",
@@ -412,7 +412,9 @@
412
412
  transition:
413
413
  translate var(--_slide),
414
414
  inline-size var(--_move),
415
- block-size var(--_move);
415
+ block-size var(--_move),
416
+ border-start-start-radius var(--_move),
417
+ border-start-end-radius var(--_move);
416
418
  }
417
419
 
418
420
  [data-ui-drawer][data-mode='overlay']:not([data-open]) > [data-part-panel] {
@@ -525,6 +527,22 @@
525
527
  border-start-end-radius: var(--slotted-drawer-radius, var(--_radius));
526
528
  }
527
529
 
530
+ /**
531
+ * A sheet that reaches the top of the layer is the screen, Material 3's
532
+ * expanded bottom sheet: its top corners square — a theme keeps a radius
533
+ * with `--slotted-drawer-radius-expanded` — and its header clears the safe
534
+ * area the status bar takes. The binding writes `data-expanded` while the
535
+ * extent is the whole layer, and under `size="full"`.
536
+ */
537
+ [data-ui-drawer][data-side='bottom'][data-expanded] > [data-part-panel] {
538
+ border-start-start-radius: var(--slotted-drawer-radius-expanded, 0px);
539
+ border-start-end-radius: var(--slotted-drawer-radius-expanded, 0px);
540
+ }
541
+
542
+ [data-ui-drawer][data-side='bottom'][data-expanded] > [data-part-panel] > [data-part-header] {
543
+ padding-block-start: calc(var(--_padding-block) + env(safe-area-inset-top, 0px));
544
+ }
545
+
528
546
  /**
529
547
  * A sheet's separator takes the first row, so the three that follow move down
530
548
  * one. `top` needs no such rule: its separator is the fourth row and the
@@ -23,6 +23,7 @@
23
23
  "--slotted-drawer-extent-xl",
24
24
  "--slotted-drawer-footer-surface",
25
25
  "--slotted-drawer-radius",
26
+ "--slotted-drawer-radius-expanded",
26
27
  "--slotted-drawer-resizer-line-active-color",
27
28
  "--slotted-drawer-resizer-line-color",
28
29
  "--slotted-drawer-rule-color",
package/src/page/page.css CHANGED
@@ -147,6 +147,22 @@
147
147
  translate: 0 100%;
148
148
  }
149
149
 
150
+ /**
151
+ * A theme draws the footer as a material with a ground that has alpha and
152
+ * `--slotted-page-footer-backdrop-filter`, `none` unless set. The footer's
153
+ * own fade keeps the filter drawn: only an ancestor's opacity removes it.
154
+ */
155
+ [data-ui-page] > [data-part-footer] {
156
+ -webkit-backdrop-filter: var(--slotted-page-footer-backdrop-filter, none);
157
+ backdrop-filter: var(--slotted-page-footer-backdrop-filter, none);
158
+ }
159
+ @media (prefers-reduced-transparency: reduce), (forced-colors: active) {
160
+ [data-ui-page] > [data-part-footer] {
161
+ -webkit-backdrop-filter: none;
162
+ backdrop-filter: none;
163
+ }
164
+ }
165
+
150
166
  [data-ui-page] > [data-part-footer] > * > [data-part-actions] {
151
167
  align-items: center;
152
168
  display: flex;
@@ -7,6 +7,7 @@
7
7
  "--slotted-font-family-sans",
8
8
  "--slotted-page-background-sunken",
9
9
  "--slotted-page-background-surface",
10
+ "--slotted-page-footer-backdrop-filter",
10
11
  "--slotted-page-footer-background",
11
12
  "--slotted-page-footer-border",
12
13
  "--slotted-page-footer-padding-block",
@@ -77,6 +77,14 @@
77
77
  --_rule-size: var(--slotted-border-width-thin, 1px);
78
78
  --_duration: var(--slotted-duration-medium);
79
79
  --_easing: var(--slotted-easing-standard, cubic-bezier(0.2, 0, 0, 1));
80
+ /**
81
+ * How the ground changes when `plain` takes it back, or a material takes
82
+ * over, as content passes under the chrome. A duration of nought makes the
83
+ * change instant.
84
+ */
85
+ --_ground-transition: background-color
86
+ var(--slotted-page-header-ground-duration, var(--_duration))
87
+ var(--slotted-page-header-ground-easing, var(--_easing));
80
88
 
81
89
  background-color: var(--_ground);
82
90
  color: var(--_text);
@@ -98,7 +106,9 @@
98
106
  * is the slide of the header area under the title and never a fight with
99
107
  * the scroll, during which the offset is constant.
100
108
  */
101
- transition: inset-block-start var(--_duration) var(--_easing);
109
+ transition:
110
+ inset-block-start var(--_duration) var(--_easing),
111
+ var(--_ground-transition);
102
112
  /**
103
113
  * Above every other sticky thing on the page — a section's header, a
104
114
  * table's column header, a pinned corner — so what a short section pushes
@@ -189,7 +199,9 @@
189
199
  [data-ui-page-header][data-variant='floating'] > * > [data-part-title-area] {
190
200
  border-start-end-radius: calc(var(--_radius) - var(--_rule-size));
191
201
  border-start-start-radius: calc(var(--_radius) - var(--_rule-size));
192
- transition: inset-block-start var(--_duration) var(--_easing);
202
+ transition:
203
+ inset-block-start var(--_duration) var(--_easing),
204
+ var(--_ground-transition);
193
205
  }
194
206
  [data-ui-page-header][data-variant='floating'][data-stuck] {
195
207
  --_sticky-inset: var(--slotted-page-header-inset, var(--slotted-space-4, 0.5rem));
@@ -231,6 +243,7 @@
231
243
  overflow-anchor: none;
232
244
  padding-block: var(--_padding-block) var(--slotted-space-4, 0.5rem);
233
245
  position: sticky;
246
+ transition: var(--_ground-transition);
234
247
  z-index: 2;
235
248
  }
236
249
 
@@ -254,8 +267,24 @@
254
267
  * transparent at rest, filters nothing at rest.
255
268
  */
256
269
  [data-ui-page-header][data-stuck] {
257
- -webkit-backdrop-filter: var(--slotted-page-header-backdrop-filter, none);
258
- backdrop-filter: var(--slotted-page-header-backdrop-filter, none);
270
+ -webkit-backdrop-filter: var(--slotted-page-header-backdrop-filter, var(--_backdrop, none));
271
+ backdrop-filter: var(--slotted-page-header-backdrop-filter, var(--_backdrop, none));
272
+ }
273
+
274
+ /**
275
+ * `material`: stuck, the chrome is the theme's material — the ground
276
+ * `--slotted-surface-material` and the filter `--slotted-material-backdrop`
277
+ * — in every variant. At rest each variant keeps its own ground. The family
278
+ * tokens still win, and the pill keeps the opaque surface, because half of
279
+ * it hangs below the root over content the root's filter does not reach.
280
+ * The theme makes both material tokens opaque under reduced transparency.
281
+ */
282
+ [data-ui-page-header][data-material][data-stuck] {
283
+ --_ground: var(
284
+ --slotted-page-header-background,
285
+ var(--slotted-surface-material, var(--_surface))
286
+ );
287
+ --_backdrop: var(--slotted-material-backdrop, none);
259
288
  }
260
289
 
261
290
  /**
@@ -1138,6 +1167,30 @@
1138
1167
  > [data-part-marks] {
1139
1168
  grid-column: 1 / -1;
1140
1169
  }
1170
+ /* A rich title — a visual, marks or a subheading — leaves a whole row free under it, and a toolbar
1171
+ that folds takes that row: beside the title it had the 40 per cent the title left, and
1172
+ folded every action on a 390 pixel phone. Beside a plain title it stays on the title row. */
1173
+ [data-ui-page-header]
1174
+ > *
1175
+ > [data-part-title-area]
1176
+ > [data-part-title-row]:has(
1177
+ > [data-part-actions] > [data-ui-toolbar][data-overflow='menu']
1178
+ ):has(
1179
+ > [data-part-visual],
1180
+ > [data-part-title] > :is([data-part-subheading], [data-part-marks]:not(:empty))
1181
+ ) {
1182
+ flex-wrap: wrap;
1183
+ }
1184
+ [data-ui-page-header]
1185
+ > *
1186
+ > [data-part-title-area]
1187
+ > [data-part-title-row]:has(
1188
+ > [data-part-visual],
1189
+ > [data-part-title] > :is([data-part-subheading], [data-part-marks]:not(:empty))
1190
+ )
1191
+ > [data-part-actions]:has(> [data-ui-toolbar][data-overflow='menu']) {
1192
+ flex: 1 1 100%;
1193
+ }
1141
1194
  /* One line, as snapped: the phone has no second one to give it. */
1142
1195
  [data-ui-page-header]
1143
1196
  > *
@@ -31,12 +31,15 @@
31
31
  "--slotted-line-height-md",
32
32
  "--slotted-line-height-sm",
33
33
  "--slotted-line-height-xl",
34
+ "--slotted-material-backdrop",
34
35
  "--slotted-page-header-backdrop-filter",
35
36
  "--slotted-page-header-background",
36
37
  "--slotted-page-header-border",
37
38
  "--slotted-page-header-control-size",
38
39
  "--slotted-page-header-description-measure",
39
40
  "--slotted-page-header-gap",
41
+ "--slotted-page-header-ground-duration",
42
+ "--slotted-page-header-ground-easing",
40
43
  "--slotted-page-header-heading-size-lg",
41
44
  "--slotted-page-header-heading-size-md",
42
45
  "--slotted-page-header-heading-size-sm",
@@ -58,6 +61,7 @@
58
61
  "--slotted-space-5",
59
62
  "--slotted-space-6",
60
63
  "--slotted-surface",
64
+ "--slotted-surface-material",
61
65
  "--slotted-target-comfortable",
62
66
  "--slotted-target-min",
63
67
  "--slotted-text",
@@ -8,19 +8,25 @@
8
8
  * chases itself around a faint track, growing from its head and closing
9
9
  * from its tail, so the eye reads work rather than a wheel.
10
10
  *
11
- * The drawing is vector. Two SVG images mask the host's ink: the track, the
12
- * whole circle at a fifth of the ink's strength, and the arc, a stroke with
13
- * round caps whose dash grows and travels by its own animation. The images
14
- * are rasterised again on every frame at the box's size, so the stroke is
15
- * as sharp at twelve pixels as at twenty-four, its ends are round, and
16
- * nothing is cut at a fractional edge.
11
+ * The drawing is vector and inline: the host writes an `svg` of two circles
12
+ * inside the box, the track and the arc, stroked in the host's ink. No
13
+ * image is fetched. The ring was two SVG images used as masks, written as
14
+ * `data:` URLs, and a page whose policy allows images from its own origin
15
+ * alone refused both: the ring was absent.
17
16
  *
18
- * It was gradients before this — a conic arc, two radial caps and a radial
19
- * mask, three angles animated as custom properties. A conic gradient's hard
20
- * stop has no antialiasing, so the arc stepped from frame to frame like a
21
- * GIF; the half-pixel stops of the caps and the mask blurred a ring of
22
- * thirteen pixels; and a ring of five sixths of the box sat on a fractional
23
- * inset the mask clipped.
17
+ * The turn is the box's `rotate`, which the compositor runs: the arc used to
18
+ * turn inside the mask image, which is rasterised again on the main thread
19
+ * for every frame, and with the main thread busy for two seconds the ring
20
+ * drew no frame at all (`tools/harness/scenarios/spinner-busy.mjs`). The
21
+ * dash still opens and travels on the main thread; under load the arc keeps
22
+ * turning at the length it had.
23
+ *
24
+ * It was gradients before the masks — a conic arc, two radial caps and a
25
+ * radial mask, three angles animated as custom properties. A conic
26
+ * gradient's hard stop has no antialiasing, so the arc stepped from frame to
27
+ * frame like a GIF, and the half-pixel stops of the caps and the mask
28
+ * blurred a ring of thirteen pixels. A stroke has round caps and is
29
+ * antialiased at every size.
24
30
  *
25
31
  * The geometry is the icon set's: in a box of twenty-four units the ring is
26
32
  * twenty across and the stroke is two and a quarter, so the swap with a
@@ -28,31 +34,20 @@
28
34
  */
29
35
  [data-ui-spinner] {
30
36
  --_size: var(--slotted-spinner-size, var(--slotted-control-icon-size-md, 16px));
31
- --_track: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Ccircle cx='12' cy='12' r='8.875' fill='none' stroke='%23000' stroke-opacity='0.2' stroke-width='2.25'/%3E%3C/svg%3E");
32
- /*
33
- * The arc: a dash of one to seventy hundredths of the circle that opens
34
- * in the first half of the cycle and travels on in the second, while the
35
- * whole circle turns under it. Each half eases on its own.
36
- */
37
- --_arc: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cg%3E%3CanimateTransform attributeName='transform' type='rotate' from='0 12 12' to='360 12 12' dur='2s' repeatCount='indefinite'/%3E%3Ccircle cx='12' cy='12' r='8.875' fill='none' stroke='%23000' stroke-width='2.25' stroke-linecap='round' pathLength='100' stroke-dasharray='1 150' transform='rotate(-90 12 12)'%3E%3Canimate attributeName='stroke-dasharray' values='1 150;70 150;70 150' keyTimes='0;0.5;1' calcMode='spline' keySplines='0.42 0 0.58 1;0.42 0 0.58 1' dur='1.5s' repeatCount='indefinite'/%3E%3Canimate attributeName='stroke-dashoffset' values='0;-28;-99' keyTimes='0;0.5;1' calcMode='spline' keySplines='0.42 0 0.58 1;0.42 0 0.58 1' dur='1.5s' repeatCount='indefinite'/%3E%3C/circle%3E%3C/g%3E%3C/svg%3E");
38
37
 
39
- background-color: currentColor;
38
+ /* A turn and a dash out of step, so the arc never comes back to the same place. */
39
+ animation: slotted-spinner-turn calc(var(--slotted-duration-loop-slow) * 1.25) linear infinite;
40
40
  block-size: var(--_size);
41
+ color: inherit;
41
42
  display: inline-block;
42
43
  /* A ring that shrinks is not a ring: it keeps its size in a flex row. */
43
44
  flex: none;
44
45
  /*
45
- * The ring is painted with a background, and forced colours replace a
46
- * background that is not one of the system's own: the ring would go and
47
- * the box stay. Not adjusting keeps `currentcolor`, the host's ink, which
48
- * forced colours have already set.
46
+ * Not adjusting keeps `currentcolor`, the host's ink, which forced colours
47
+ * have already set, on a stroke the system would otherwise repaint.
49
48
  */
50
49
  forced-color-adjust: none;
51
50
  inline-size: var(--_size);
52
- mask-image: var(--_arc), var(--_track);
53
- mask-position: center;
54
- mask-repeat: no-repeat;
55
- mask-size: 100% 100%;
56
51
  opacity: 1;
57
52
  scale: 1;
58
53
  transition:
@@ -61,6 +56,56 @@
61
56
  vertical-align: middle;
62
57
  }
63
58
 
59
+ [data-ui-spinner] > svg {
60
+ block-size: 100%;
61
+ display: block;
62
+ fill: none;
63
+ inline-size: 100%;
64
+ stroke: currentColor;
65
+ stroke-width: 2.25;
66
+ }
67
+
68
+ /* The track: the whole circle at a fifth of the ink's strength. */
69
+ [data-ui-spinner] > svg > circle:first-child {
70
+ stroke-opacity: 0.2;
71
+ }
72
+
73
+ /*
74
+ * The arc: a dash of one to seventy hundredths of the circle that opens in
75
+ * the first half of the cycle and travels on in the second, from the top.
76
+ * Each half eases on its own.
77
+ */
78
+ [data-ui-spinner] > svg > circle:last-child {
79
+ animation: slotted-spinner-arc var(--slotted-duration-loop-slow) infinite;
80
+ rotate: -90deg;
81
+ stroke-dasharray: 1 150;
82
+ stroke-linecap: round;
83
+ transform-origin: center;
84
+ }
85
+
86
+ @keyframes slotted-spinner-turn {
87
+ to {
88
+ rotate: 360deg;
89
+ }
90
+ }
91
+
92
+ @keyframes slotted-spinner-arc {
93
+ 0% {
94
+ animation-timing-function: cubic-bezier(0.42, 0, 0.58, 1);
95
+ stroke-dasharray: 1 150;
96
+ stroke-dashoffset: 0;
97
+ }
98
+ 50% {
99
+ animation-timing-function: cubic-bezier(0.42, 0, 0.58, 1);
100
+ stroke-dasharray: 70 150;
101
+ stroke-dashoffset: -28;
102
+ }
103
+ 100% {
104
+ stroke-dasharray: 70 150;
105
+ stroke-dashoffset: -99;
106
+ }
107
+ }
108
+
64
109
  @starting-style {
65
110
  [data-ui-spinner] {
66
111
  opacity: 0;
@@ -68,14 +113,15 @@
68
113
  }
69
114
  }
70
115
 
71
- /**
72
- * A frozen ring still communicates waiting: the track, and an arc of three
73
- * quarters resting on it. Stopping it entirely is less distracting than
74
- * replacing one continuous motion with a slower one.
75
- */
116
+ /* Still: a track with an arc of three quarters resting on it. */
76
117
  @media (prefers-reduced-motion: reduce) {
77
- [data-ui-spinner] {
78
- --_arc: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Ccircle cx='12' cy='12' r='8.875' fill='none' stroke='%23000' stroke-width='2.25' stroke-linecap='round' pathLength='100' stroke-dasharray='75 150' transform='rotate(-90 12 12)'/%3E%3C/svg%3E");
118
+ [data-ui-spinner],
119
+ [data-ui-spinner] > svg > circle:last-child {
120
+ animation: none;
121
+ }
122
+
123
+ [data-ui-spinner] > svg > circle:last-child {
124
+ stroke-dasharray: 75 150;
79
125
  }
80
126
  }
81
127
  }
@@ -2,5 +2,6 @@
2
2
  "--slotted-control-icon-size-md",
3
3
  "--slotted-control-transition-duration",
4
4
  "--slotted-control-transition-easing",
5
+ "--slotted-duration-loop-slow",
5
6
  "--slotted-spinner-size"
6
7
  ]
package/src/tabs/tabs.css CHANGED
@@ -464,10 +464,12 @@
464
464
  * label it keeps a minimum length, centred on the content. It stands
465
465
  * `--slotted-tabs-marker-offset` above the list's edge, over the track, and the axes are physical in this one rule
466
466
  * because the measurement is: the position is from the list's left edge.
467
- * Position and length are both layout properties, so one thread advances
468
- * both edges in the same frame: a composited `translate` beside a
469
- * main-thread `inline-size` drifted apart while the panel rendered, and the
470
- * bar grew and shrank on its way between tabs of unequal widths.
467
+ * At rest the bar is laid out by `left` and `inline-size`, so its ends keep
468
+ * their radius; it slides by `translate` and `scale` together, both on the
469
+ * compositor, so both edges advance in the same frame. A composited
470
+ * `translate` beside a main-thread `inline-size` drifted apart while the
471
+ * panel rendered, and a slide of `left` and `inline-size` lost every frame
472
+ * to a panel rendering on the same thread.
471
473
  */
472
474
  [data-ui-tabs] > :where([data-part-list]) > [data-part-marker] {
473
475
  --_start-inset: var(--_marker-inset);
@@ -503,28 +505,33 @@
503
505
  }
504
506
 
505
507
  /**
506
- * It slides only when the selection moves: `data-moving` arrives with a
507
- * position whose tab changed, and leaves with a re-measurement of the same
508
- * tab, so the first position, a font arriving and a resized list place the
509
- * bar without motion. The slide takes the medium duration on a curve that
510
- * starts and ends slowly, `--slotted-tabs-marker-easing`: the control
511
- * duration on the theme's decelerating curve covered a third of the
512
- * distance in its first frame and read as a jump. Under reduced motion it
513
- * jumps.
508
+ * It slides only when the selection moves: `createIndicator` places the bar
509
+ * on the new tab, writes the shift and the scale from the box it was drawn
510
+ * in, and raises `data-moving`, which starts the slide from there back to
511
+ * nought; a re-measurement of the same tab drops it, so the first position,
512
+ * a font arriving and a resized list place the bar without motion. The slide
513
+ * takes the medium duration on a curve that starts and ends slowly,
514
+ * `--slotted-tabs-marker-easing`: the control duration on the theme's
515
+ * decelerating curve covered a third of the distance in its first frame and
516
+ * read as a jump. Under reduced motion it jumps.
514
517
  */
515
518
  [data-ui-tabs] > :where([data-part-list]) > [data-part-marker][data-moving] {
516
519
  --_marker-easing: var(--slotted-tabs-marker-easing, cubic-bezier(0.4, 0, 0.2, 1));
517
520
 
518
- transition:
519
- left var(--slotted-duration-medium) var(--_marker-easing),
520
- top var(--slotted-duration-medium) var(--_marker-easing),
521
- inline-size var(--slotted-duration-medium) var(--_marker-easing),
522
- block-size var(--slotted-duration-medium) var(--_marker-easing);
521
+ animation: slotted-tabs-marker-slide var(--slotted-duration-medium) var(--_marker-easing);
522
+ transform-origin: 0 0;
523
+ }
524
+
525
+ @keyframes slotted-tabs-marker-slide {
526
+ from {
527
+ translate: var(--_indicator-shift, 0px 0px);
528
+ scale: var(--_indicator-scale, 1 1);
529
+ }
523
530
  }
524
531
 
525
532
  @media (prefers-reduced-motion: reduce) {
526
533
  [data-ui-tabs] > :where([data-part-list]) > [data-part-marker][data-moving] {
527
- transition: none;
534
+ animation: none;
528
535
  }
529
536
  }
530
537
 
@@ -381,6 +381,25 @@
381
381
  * tone is in the mark and in nothing else, which is enough — it is the
382
382
  * silhouette that survives forced colours anyway.
383
383
  */
384
+ /**
385
+ * A theme that paints the raised message with a surface that has alpha blurs
386
+ * what passes under it: `--slotted-toast-backdrop-filter`, `none` unless set.
387
+ * Only `raised` floats over content it does not own; `soft` and `solid` are
388
+ * tone grounds. A reader who asked for less transparency gets no filter, and
389
+ * under forced colours the ground is the system's.
390
+ */
391
+ [data-ui-toast] > [data-part-list] > [data-part-item][data-variant='raised'] {
392
+ -webkit-backdrop-filter: var(--slotted-toast-backdrop-filter, none);
393
+ backdrop-filter: var(--slotted-toast-backdrop-filter, none);
394
+ }
395
+
396
+ @media (prefers-reduced-transparency: reduce), (forced-colors: active) {
397
+ [data-ui-toast] > [data-part-list] > [data-part-item][data-variant='raised'] {
398
+ -webkit-backdrop-filter: none;
399
+ backdrop-filter: none;
400
+ }
401
+ }
402
+
384
403
  [data-ui-toast] > [data-part-list] > [data-part-item][data-variant='soft'] {
385
404
  --_ground: var(--_tone-surface);
386
405
  --_edge: var(--_tone-border);
@@ -36,6 +36,7 @@
36
36
  "--slotted-text",
37
37
  "--slotted-text-muted",
38
38
  "--slotted-toast-actions-gap",
39
+ "--slotted-toast-backdrop-filter",
39
40
  "--slotted-toast-border-color",
40
41
  "--slotted-toast-border-width",
41
42
  "--slotted-toast-close-radius",