Files
livewire-material/resources/css/components/button-group.css
T
Andreas Reinhold / reiniandClaude Opus 5 247c596c3a Cut duplicated and speculative code across the package
An over-engineering audit of the whole tree, applied in five reviewed
batches. Behaviour stays the same except where UPGRADE.md says otherwise.

PHP: the showcase and error-page stylesheets are prebuilt into
resources/dist by bin/stylesheets.mjs, through Vite's own postcss-import
(first occurrence kept, the order an application's build gives), instead
of Stylesheets::bundle() inlining imports on every request; only the
import walk DesignGuard needs stays. SchemeStylesheet::withProfiles()
replaces three copies of the scheme-plus-profiles loop, material:scheme
leaves spec and contrast checks to the node script that already made
them, and the error page's scheme cache, the hashed view namespace, the
translations path with no lang/ folder and DesignGuard's 1.x-name hints
are gone.

JS: the androidx shape port progress.js and both bin scripts each carried
lives once in resources/js/shapes.js (the generated SVGs are unchanged);
util.js holds ringIndex(), ms(), reopenGuard() and remember(), which
were written out several times; listeners are released through
AbortController; tooltip.js's hoverPopover() serves the rich tooltip too.

CSS: every rule for an element inside the navigation rail queries
`--md-navigation-rail-value` instead of repeating the seven collapsed
conditions under five media branches; badge, alert, progress, slider and
button read one non-inheriting colour-role table (components/color.css);
the dialog chrome, the submenu's popover chrome, the chip's state layer
and touch target, and the visually-hidden inputs use the shared rules
they copied; foundation/tokens.css is folded into foundation.css.

Views: Support\Field and Support\Link replace the error-key, bound-value
and link-attribute blocks copied into the fields and link components;
the timepicker period group, the menu filter and the showcase head are
partials; the datepicker's steppers and entry fields are loops; component
docblocks no longer restate SKILL.md.

Tests and tooling: one dataset-driven ComponentStylesheetsTest replaces
four per-group files, DesignGuardTest and the layout-component tests use
datasets, browser tests share one ready() helper, CSS parsing lives in
ComponentStylesheet alone. docs/audits and the finding IDs citing it are
removed, as are pestphp/pest-plugin-laravel, the unused composer scripts
and check:font; the lint job runs in the feature job, which now installs
node packages so the prebuilt-stylesheet staleness test runs in CI.

Feature suite 1177 passed, Chrome browser suite 299 passed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-17 19:29:21 +02:00

167 lines
7.8 KiB
CSS

/*
* <x-button-group>: related buttons in a row, and the shapes a button takes from its place among
* its siblings. The connected form is also what `<x-group>` draws its segments with, which is why
* its stylesheet imports this one. A group never wraps: M3's rule, and the press expansion only
* reaches a neighbour on the same line anyway.
*
* Standard (`data-md-button-group="standard"`): the buttons stand apart — 18/12/8/8/8px by size
* (ButtonGroupXSmall…XLargeTokens.BetweenSpace) — and 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, 4/6/8/16/
* 20px, on the button's own padding of 16/16/24/48/64px). Icon buttons keep their width. A square
* standard group has no inner edges, so it hands its buttons the square corner scale
* `<x-button shape="square">` draws (12/12/16/28/28px, 8/8/12/16/16px under the finger), and a
* selected button in it turns round.
*
* Connected (`data-md-button-group="connected"`): 2px apart, the outer corners full, the inner
* corners small, smaller still while pressed, and a selected segment (`aria-pressed`, or a checked
* input inside it) fully round — ConnectedButtonGroupXSmall…XLargeTokens and the M3 Expressive
* connected button group spec: inner corners 4/8/8/16/20px, pressed 2/4/4/12/16px. At the two
* smallest sizes a segment is 32px or 40px tall, and M3 names both numbers it must not go below:
* "XS and S connected button groups have a 48dp target area and a 48dp minimum width".
*
* Shape (`data-md-shape="square"`, M3's "Default shape | Round, square" configuration):
* `--md-button-group-outer` is what the two ends of a group round to — half the height for a round
* group, and for a square one the corner its inner edges take, which is the square table M3
* publishes for connected groups (4/8/8/16/20dp by size). Selection morphs the other way: M3 has a
* toggle inside a group "swap shape square/round on selection", so a selected segment in a square
* group turns round, which is why the selected rule keeps `--md-button-group-full`.
*
* "Full" is half the size's height (16/20/28/48/68px), 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.
*
* <x-button>'s own corner rules carry no specificity, so these reshape the buttons in a group
* whatever selector they use; a pressed or selected segment changes one custom property and the
* outer corners stay put.
*/
@layer material.reset, material.tokens, material.base, material.layout, material.components, material.text, material.visibility;
@layer material.components {
[data-md-button-group] {
--md-button-group-inner: var(--md-sys-shape-corner-sm);
--md-button-group-inner-pressed: var(--md-sys-shape-corner-xs);
--md-button-group-full: 20px;
--md-button-group-square: var(--md-sys-shape-corner-md);
--md-button-group-square-pressed: var(--md-sys-shape-corner-sm);
--md-button-group-outer: var(--md-button-group-full);
--md-button-group-pad: var(--md-sys-measurement-space200);
--md-button-group-grow: 6px;
display: inline-flex;
align-items: center;
}
[data-md-button-group][data-md-size='xs'] {
--md-button-group-inner: var(--md-sys-shape-corner-xs);
--md-button-group-inner-pressed: 2px;
--md-button-group-full: 16px;
--md-button-group-grow: 4px;
}
[data-md-button-group][data-md-size='md'] {
--md-button-group-full: 28px;
--md-button-group-square: var(--md-sys-shape-corner-lg);
--md-button-group-square-pressed: var(--md-sys-shape-corner-md);
--md-button-group-pad: var(--md-sys-measurement-space300);
--md-button-group-grow: 8px;
}
[data-md-button-group][data-md-size='lg'] {
--md-button-group-inner: var(--md-sys-shape-corner-lg);
--md-button-group-inner-pressed: var(--md-sys-shape-corner-md);
--md-button-group-full: 48px;
--md-button-group-square: var(--md-sys-shape-corner-xl);
--md-button-group-square-pressed: var(--md-sys-shape-corner-lg);
--md-button-group-pad: var(--md-sys-measurement-space600);
--md-button-group-grow: 16px;
}
[data-md-button-group][data-md-size='xl'] {
--md-button-group-inner: var(--md-sys-shape-corner-lg-increased);
--md-button-group-inner-pressed: var(--md-sys-shape-corner-lg);
--md-button-group-full: 68px;
--md-button-group-square: var(--md-sys-shape-corner-xl);
--md-button-group-square-pressed: var(--md-sys-shape-corner-lg);
--md-button-group-pad: var(--md-sys-measurement-space800);
--md-button-group-grow: 20px;
}
[data-md-button-group='standard'] {
gap: var(--md-sys-measurement-space100);
}
[data-md-button-group='standard'][data-md-size='xs'] {
gap: 18px;
}
[data-md-button-group='standard'][data-md-size='sm'] {
gap: 12px;
}
/* A square standard group hands its buttons the square corner scale; a selected one rounds. */
[data-md-button-group='standard'][data-md-shape='square'] > * {
border-radius: var(--md-button-group-square);
}
[data-md-button-group='standard'][data-md-shape='square'] > :active:not(:disabled, [aria-disabled='true']) {
border-radius: var(--md-button-group-square-pressed);
}
[data-md-button-group='standard'][data-md-shape='square'] > [aria-pressed='true'] {
border-radius: var(--md-button-group-full);
}
/* Press expansion: the pressed label button grows, the label buttons beside it give way. */
[data-md-button-group='standard'] > :not([data-md-icon-button]):active:not(:disabled, [aria-disabled='true']) {
padding-inline: calc(var(--md-button-group-pad) + var(--md-button-group-grow));
}
[data-md-button-group='standard'] > :not([data-md-icon-button]):has(+ :not([data-md-icon-button]):active:not(:disabled, [aria-disabled='true'])) {
padding-inline-end: calc(var(--md-button-group-pad) - var(--md-button-group-grow));
}
[data-md-button-group='standard'] > :not([data-md-icon-button]):active:not(:disabled, [aria-disabled='true']) + :not([data-md-icon-button]) {
padding-inline-start: calc(var(--md-button-group-pad) - var(--md-button-group-grow));
}
/* A square group's ends take the corner its inner edges take. */
[data-md-button-group][data-md-shape='square'] {
--md-button-group-outer: var(--md-button-group-inner);
}
[data-md-button-group='connected'] {
gap: var(--md-sys-measurement-space25);
}
[data-md-button-group='connected']:is([data-md-size='xs'], [data-md-size='sm']) > * {
min-inline-size: var(--md-sys-measurement-space600);
}
[data-md-button-group='connected'] > * {
--md-button-group-corner: var(--md-button-group-inner);
border-radius: var(--md-button-group-corner);
}
[data-md-button-group='connected'] > :active {
--md-button-group-corner: var(--md-button-group-inner-pressed);
}
[data-md-button-group='connected'] > :is([aria-pressed='true'], :has(:checked)) {
--md-button-group-corner: var(--md-button-group-full);
}
[data-md-button-group='connected'] > :first-child {
border-start-start-radius: var(--md-button-group-outer);
border-end-start-radius: var(--md-button-group-outer);
}
[data-md-button-group='connected'] > :last-child {
border-start-end-radius: var(--md-button-group-outer);
border-end-end-radius: var(--md-button-group-outer);
}
}