Plan step 22, actions.md § Missing (FAB to extended FAB scroll collapse): M3 has an extended FAB collapse to a FAB on scroll-down and re-extend on scroll-up, and `<x-button fab>` only swapped on window width. `<x-fab collapse-on-scroll>` (extended, with an icon) takes a `materialFab` flag from the new resources/js/fab.js, which reads the window's scroll once a frame, ignores moves under 8px and extends again near the top. The morph is CSS in actions.css: the label sits in a grid track that closes to zero, the gap and minimum width follow on the default spatial spring, and the label fades on the effects one; reduced motion swaps outright through the motion tokens. The label is clipped, not removed, so the collapsed FAB keeps its accessible name. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Qwx5USif3wFFmxtHg5U1g9
149 lines
7.5 KiB
CSS
149 lines
7.5 KiB
CSS
/*
|
|
* The transitions an action runs between its states — buttons, connected segments, menu items,
|
|
* the FAB menu's trigger, and an extended FAB collapsing to a FAB on scroll.
|
|
*
|
|
* 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;
|
|
}
|
|
|
|
/*
|
|
* An extended FAB that collapses to a FAB while the page scrolls down (`<x-fab
|
|
* collapse-on-scroll>`, resources/js/fab.js sets `data-collapsed`). M3's extended FAB page: it
|
|
* "can collapse to a FAB on scroll-down and re-expand to extended on scroll-up — when switching
|
|
* between FAB↔extended FAB, shape changes, the icon moves left, and the text label fades in/out".
|
|
*
|
|
* `width: auto` cannot transition, so the label sits in a one-column grid whose track closes to
|
|
* zero; that track is what animates, and the button lays itself out around it every frame. The
|
|
* gap and the minimum width go with it, all on the default spatial spring — the size of morph
|
|
* `state-transition-default` is for — while the label fades on the effects spring. The collapsed
|
|
* sizes are the FAB's own (FabBaseline/Medium/LargeTokens: 56/80/96px). The extended FAB's
|
|
* padding already centres the glyph at `sm` (16px + 24px + 16px) and `md` (26 + 28 + 26); at
|
|
* `lg` its 28px is 4px short of the 32px that centres a 32px glyph in 96px, so that one moves.
|
|
* Corners do not change: each size's FAB and extended FAB share one.
|
|
*
|
|
* The transition list repeats the FAB's own shadow and colour transitions, which this rule
|
|
* replaces. Under reduced motion tokens/motion.css takes every duration to zero, so it swaps.
|
|
*/
|
|
[data-fab-collapsible] {
|
|
--fab-collapsed-size: 3.5rem;
|
|
|
|
transition-property: min-inline-size, padding-inline, gap, box-shadow, background-color, color;
|
|
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-fast-duration), var(--md-sys-motion-effects-fast-duration), var(--md-sys-motion-effects-fast-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-fast), var(--md-sys-motion-effects-fast), var(--md-sys-motion-effects-fast);
|
|
}
|
|
|
|
[data-fab-collapsible][data-fab-size='md'] { --fab-collapsed-size: 5rem; }
|
|
[data-fab-collapsible][data-fab-size='lg'] { --fab-collapsed-size: 6rem; }
|
|
|
|
[data-fab-label] {
|
|
display: grid;
|
|
grid-template-columns: 1fr;
|
|
transition-property: grid-template-columns, opacity;
|
|
transition-duration: var(--md-sys-motion-spatial-default-duration), var(--md-sys-motion-effects-default-duration);
|
|
transition-timing-function: var(--md-sys-motion-spatial-default), var(--md-sys-motion-effects-default);
|
|
}
|
|
|
|
[data-fab-label] > span {
|
|
overflow: hidden;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
[data-fab-collapsible][data-collapsed] {
|
|
min-inline-size: var(--fab-collapsed-size);
|
|
gap: 0;
|
|
}
|
|
|
|
[data-fab-collapsible][data-fab-size='lg'][data-collapsed] {
|
|
padding-inline: 2rem;
|
|
}
|
|
|
|
/* The label stays in the accessibility tree — clipped and transparent, never `display: none` — so
|
|
the collapsed FAB keeps the name the extended one had. */
|
|
[data-fab-collapsible][data-collapsed] [data-fab-label] {
|
|
grid-template-columns: 0fr;
|
|
opacity: 0;
|
|
}
|