Files
livewire-material/resources/css/components/groups.css
T
Andreas ReinholdandClaude Opus 5 d560e7449c Put the split button on its own geometry
It borrowed the connected group's corner variables, so its inner corners were
wrong at four of five sizes and shrank under the finger where SplitButton*Tokens
grows them; its chevron took the icon-button table's size at every step, sat dead
centre, and turned over on the expressive spring. groups.css now carries
--split-inner, --split-icon and --split-nudge per size, and the rotation uses
M3's standard scheme. Plan step 18, actions.md ACT-06, ACT-07, ACT-29, ACT-30.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qwx5USif3wFFmxtHg5U1g9
2026-09-14 05:58:13 +02:00

136 lines
6.6 KiB
CSS

/*
* Button groups and split buttons: shapes that depend on a button's place among its siblings.
*
* Unlayered on purpose. Each <x-button> draws its own corners and padding as utilities, and a
* rule in any @layer loses to a utility whatever its specificity; the group has to win.
*
* Standard group (`<x-button-group>`): pressing a label button widens it and narrows its
* neighbours by the same amount, so the row keeps its length — Expressive's press expansion
* (ButtonGroupDefaults.ExpandedRatio is 15%; this uses a fixed step per size). Icon buttons keep
* their width.
*
* Connected group (`<x-button-group connected>`, `<x-group>`): 2px apart, the outer corners
* 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, 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
* add up to more than its side, CSS scales every radius by the same factor: a 9999px corner
* beside an 8px one shrank the 8px one to a hundredth of a pixel, so the inner corners drew
* square.
*/
[data-button-group] {
--group-inner: var(--md-sys-shape-corner-sm);
--group-inner-pressed: var(--md-sys-shape-corner-xs);
--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='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; }
[data-button-group][data-size='xl'] { --group-pad: 4rem; --group-grow: 20px; --group-inner: var(--md-sys-shape-corner-lg-increased); --group-inner-pressed: var(--md-sys-shape-corner-lg); --group-full: 4.25rem; }
[data-button-group='standard'] > :not([data-icon-button]):active:not(:disabled, [aria-disabled='true']) {
padding-inline: calc(var(--group-pad) + var(--group-grow));
}
[data-button-group='standard'] > :not([data-icon-button]):has(+ :not([data-icon-button]):active:not(:disabled, [aria-disabled='true'])) {
padding-inline-end: calc(var(--group-pad) - var(--group-grow));
}
[data-button-group='standard'] > :not([data-icon-button]):active:not(:disabled, [aria-disabled='true']) + :not([data-icon-button]) {
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] {
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'] > * {
--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);
}
[data-button-group='connected'] > :first-child,
[data-split='leading'] {
border-start-start-radius: var(--group-full);
border-end-start-radius: var(--group-full);
}
[data-button-group='connected'] > :last-child,
[data-split='trailing'] {
border-start-end-radius: var(--group-full);
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 {
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 {
rotate: 180deg;
}