Merge branch 'worktree-agent-a6b15d4e17dad5970'

This commit is contained in:
Andreas Reinhold / reini
2026-09-14 06:17:26 +02:00
33 changed files with 603 additions and 165 deletions
+86
View File
@@ -0,0 +1,86 @@
/*
* The transitions an action runs between its states — buttons, connected segments, menu items,
* the FAB menu's trigger.
*
* M3 has two spring styles and they are not interchangeable: spatial moves things (position,
* size, rotation, rounded corners) and overshoots by 9% on the way, effects changes how things
* look (colour, opacity, elevation) and is critically damped, because a colour must never
* overshoot — tokens/motion.css says so in the library's own words. An element whose corners and
* colours both change on a press therefore needs two springs at once, which Tailwind cannot write
* (`duration-*` and `ease-*` each emit one value); CSS can, by giving `transition-property` a
* list and matching it position for position.
*
* `state-transition-fast` is the press/hover/select transition of a control the finger lands on:
* shape, size and the place it sits on the fast spatial spring (a `fab` button nudges up when a
* snackbar arrives under it), colour and elevation on the fast effects spring.
* `state-transition-default` is the same pair one step slower, for the bigger morph of a FAB
* turning into a close button.
*
* `popover-transition` is the enter and exit of a menu: M3 asks for a transition that ties the
* list to its trigger, so it scales up from its `transform-origin` on the spatial spring while it
* fades in on the effects one, and `allow-discrete` keeps `display` and `overlay` alive long
* enough for the exit to be seen.
*
* These are `@utility` declarations, so they compile into Tailwind's utilities layer like any
* other class: a `transition-*` utility written beside one on the call site will fight it, and
* whichever Tailwind emits last wins. Pass neither.
*/
@utility state-transition-fast {
transition-property: border-radius, padding, margin, bottom, background-color, color, box-shadow;
transition-duration:
var(--md-sys-motion-spatial-fast-duration), var(--md-sys-motion-spatial-fast-duration),
var(--md-sys-motion-spatial-fast-duration), var(--md-sys-motion-spatial-fast-duration),
var(--md-sys-motion-effects-fast-duration), var(--md-sys-motion-effects-fast-duration), var(--md-sys-motion-effects-fast-duration);
transition-timing-function:
var(--md-sys-motion-spatial-fast), var(--md-sys-motion-spatial-fast),
var(--md-sys-motion-spatial-fast), var(--md-sys-motion-spatial-fast),
var(--md-sys-motion-effects-fast), var(--md-sys-motion-effects-fast), var(--md-sys-motion-effects-fast);
}
@utility state-transition-default {
transition-property: border-radius, padding, margin, background-color, color, box-shadow;
transition-duration:
var(--md-sys-motion-spatial-default-duration), var(--md-sys-motion-spatial-default-duration), var(--md-sys-motion-spatial-default-duration),
var(--md-sys-motion-effects-default-duration), var(--md-sys-motion-effects-default-duration), var(--md-sys-motion-effects-default-duration);
transition-timing-function:
var(--md-sys-motion-spatial-default), var(--md-sys-motion-spatial-default), var(--md-sys-motion-spatial-default),
var(--md-sys-motion-effects-default), var(--md-sys-motion-effects-default), var(--md-sys-motion-effects-default);
}
@utility popover-transition {
opacity: 0;
scale: 0.95;
transition-property: opacity, scale, display, overlay;
transition-duration:
var(--md-sys-motion-effects-fast-duration), var(--md-sys-motion-spatial-fast-duration),
var(--md-sys-motion-spatial-fast-duration), var(--md-sys-motion-spatial-fast-duration);
transition-timing-function:
var(--md-sys-motion-effects-fast), var(--md-sys-motion-spatial-fast), linear, linear;
transition-behavior: allow-discrete;
&:popover-open {
opacity: 1;
scale: 1;
}
@starting-style {
&:popover-open {
opacity: 0;
scale: 0.95;
}
}
}
/*
* A FAB menu's items rise into place as the list opens and sink back as it closes. The entry is
* `@starting-style` on the item itself; the exit needs the state to be readable while the popover
* is on its way out, which is what `:not(:popover-open)` is — the popover keeps `display: flex`
* for the length of its own discrete transition, so the items have somewhere to animate to.
*
* Unlayered (a plain rule, not an `@utility`), so it beats the item's own utilities.
*/
[data-fab-menu]:not(:popover-open) > * {
translate: 0 0.5rem;
opacity: 0;
}
+51 -7
View File
@@ -13,8 +13,15 @@
* full, the inner corners small, smaller still while pressed, and a selected segment fully round
* (ConnectedButtonGroupSmallTokens and the M3 Expressive connected button group spec).
*
* Split button (`<x-split-button>`): the same idea for two halves; the trailing half turns
* round and its chevron turns over while its menu is open (SplitButton*Tokens).
* Split button (`<x-split-button>`): the same idea for two halves, on its own corner scale —
* SplitButton*Tokens gives the inner corner 4/4/4/8/12px at rest and *grows* it to 8/12/12/20/20px
* under the finger, the opposite of a connected group, so `--split-inner*` is separate from
* `--group-inner*`. The trailing half turns round and its chevron turns over while its menu is
* open, on M3's standard scheme rather than the expressive springs (the site: "the trailing
* button uses the standard motion scheme, not expressive, when rotating"), and the chevron sits
* a little towards the leading half so the pair reads as one control (`--split-nudge`:
* 1/1/2/3/6px). `--split-icon` is the trailing icon's own size, 22/22/26/38/50px, which the
* icon-button table does not know about.
*
* "Full" here is half the size's height (`--group-full`), never `--md-sys-shape-corner-full`'s
* 9999px. One element mixes full outer corners with small inner ones, and when a box's radii
@@ -29,7 +36,7 @@
--group-full: 1.25rem;
}
[data-button-group][data-size='xs'] { --group-pad: 0.75rem; --group-grow: 4px; --group-inner: var(--md-sys-shape-corner-xs); --group-inner-pressed: 2px; --group-full: 1rem; }
[data-button-group][data-size='xs'] { --group-pad: 1rem; --group-grow: 4px; --group-inner: var(--md-sys-shape-corner-xs); --group-inner-pressed: 2px; --group-full: 1rem; }
[data-button-group][data-size='sm'] { --group-pad: 1rem; --group-grow: 6px; --group-full: 1.25rem; }
[data-button-group][data-size='md'] { --group-pad: 1.5rem; --group-grow: 8px; --group-full: 1.75rem; }
[data-button-group][data-size='lg'] { --group-pad: 3rem; --group-grow: 16px; --group-inner: var(--md-sys-shape-corner-lg); --group-inner-pressed: var(--md-sys-shape-corner-md); --group-full: 3rem; }
@@ -47,24 +54,52 @@
padding-inline-start: calc(var(--group-pad) - var(--group-grow));
}
[data-button-group='split'] { --split-inner: var(--md-sys-shape-corner-xs); --split-inner-pressed: var(--md-sys-shape-corner-md); --split-icon: 22px; --split-nudge: 1px; }
[data-button-group='split'][data-size='xs'] { --split-inner-pressed: var(--md-sys-shape-corner-sm); }
[data-button-group='split'][data-size='md'] { --split-icon: 26px; --split-nudge: 2px; }
[data-button-group='split'][data-size='lg'] { --split-inner: var(--md-sys-shape-corner-sm); --split-inner-pressed: var(--md-sys-shape-corner-lg-increased); --split-icon: 38px; --split-nudge: 3px; }
[data-button-group='split'][data-size='xl'] { --split-inner: var(--md-sys-shape-corner-md); --split-inner-pressed: var(--md-sys-shape-corner-lg-increased); --split-icon: 50px; --split-nudge: 6px; }
/*
* A connected segment at the two smallest sizes is 32px or 40px tall, and an icon-only one is as
* narrow as it is tall. M3 names both numbers and tells you not to reduce them: "XS and S
* connected button groups have a 48dp target area and a 48dp minimum width". The target is the
* `touch-target` utility on the segment itself; the width is here, where a group can reach the
* `<x-button>`s a caller put inside it.
*/
[data-button-group='connected'][data-size='xs'] > *,
[data-button-group='connected'][data-size='sm'] > * {
min-inline-size: 3rem;
}
/*
* Connected segments and split halves take their inner corner from `--group-corner`, so pressing
* or selecting changes one variable and the rounded outer corners stay put.
*/
[data-button-group='connected'] > *,
[data-split] {
--group-corner: var(--group-inner);
border-start-start-radius: var(--group-corner);
border-end-start-radius: var(--group-corner);
border-start-end-radius: var(--group-corner);
border-end-end-radius: var(--group-corner);
}
[data-button-group='connected'] > :active,
[data-split]:active {
[data-button-group='connected'] > * {
--group-corner: var(--group-inner);
}
[data-button-group='connected'] > :active {
--group-corner: var(--group-inner-pressed);
}
[data-split] {
--group-corner: var(--split-inner);
}
[data-split]:active {
--group-corner: var(--split-inner-pressed);
}
[data-button-group='connected'] > :is([aria-pressed='true'], :has(:checked)),
[data-split='trailing'][aria-expanded='true'] {
--group-corner: var(--group-full);
@@ -82,8 +117,17 @@
border-end-end-radius: var(--group-full);
}
/*
* The trailing chevron: its own size, and `--split-nudge` off centre towards the leading half —
* equal and opposite margins move it by exactly that much inside a centred flex row, in whichever
* direction the writing mode runs.
*/
[data-split='trailing'] svg {
transition: rotate var(--md-sys-motion-spatial-fast-duration) var(--md-sys-motion-spatial-fast);
width: var(--split-icon);
height: var(--split-icon);
margin-inline-start: calc(-1 * var(--split-nudge));
margin-inline-end: var(--split-nudge);
transition: rotate var(--md-sys-motion-duration-short) var(--md-sys-motion-easing-standard);
}
[data-split='trailing'][aria-expanded='true'] svg {
+1
View File
@@ -20,6 +20,7 @@
@import './tokens/font.css';
@import './tokens/theme.css';
@import './tokens/state.css';
@import './components/actions.css';
@import './components/groups.css';
@import './components/list.css';
@import './components/field.css';