From 5d0e9bc12fdbdd48e48c50a4d1c141d26bbc5516 Mon Sep 17 00:00:00 2001 From: Andreas Reinhold / reini Date: Mon, 14 Sep 2026 06:40:14 +0200 Subject: [PATCH] Square a button group and the choice drawn as one MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Plan step 22, actions.md § Missing (Square-by-default button groups): M3 lists "Default shape | Round, square" as a button-group configuration and neither `` nor `` had a `shape` prop, so a group of square buttons still drew round outer corners. `shape="square"` passes the square corner scale to every child through `data-shape` and two new variables in groups.css: `--group-outer` is what the two ends of a group round to (half the height when round, the corner its inner edges take when square — M3's square table, 4/8/8/16/20dp), and `--group-square`/`--group-square-pressed` give a standard group's buttons the scale `` draws. The selected rules keep `--group-full`, so a selected button in a square group rounds: M3 has a toggle inside a group swap shape on selection either way. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01Qwx5USif3wFFmxtHg5U1g9 --- .../livewire-material-development/SKILL.md | 4 +- resources/css/components/groups.css | 43 ++++++++++++++++--- .../views/components/button-group.blade.php | 10 +++++ resources/views/components/group.blade.php | 13 +++++- .../views/showcase/sections/buttons.blade.php | 21 +++++++++ tests/Feature/Components/ButtonGroupTest.php | 16 +++++++ 6 files changed, 96 insertions(+), 11 deletions(-) diff --git a/resources/boost/skills/livewire-material-development/SKILL.md b/resources/boost/skills/livewire-material-development/SKILL.md index 145cc8e0..359d50a8 100644 --- a/resources/boost/skills/livewire-material-development/SKILL.md +++ b/resources/boost/skills/livewire-material-development/SKILL.md @@ -280,7 +280,7 @@ Clusters: `` draws M3's line, `` M3 Expres ### `` -A row of ``s: ``. `connected` sets them 2px apart with small inner corners (a selected toggle rounds fully). Pass the `size` of the buttons inside. +A row of ``s: ``. `connected` sets them 2px apart with small inner corners (a selected toggle rounds fully). Pass the `size` of the buttons inside. `shape="square"` is M3's square group and covers every button in it, so do not write `shape` on each one: a connected group's ends square to the corner its inner edges take, a standard group's buttons take the square corner scale, and a selected button still rounds — M3 has the toggle morph the other way. ### `` @@ -294,7 +294,7 @@ A choice between a few options as a connected button group of native radios (che ]" hint="Recipients lose access after that" /> ``` -Props: `label`, `hint`, `hint-class` (classes for the hint, as on the fields; a colour there paints it), `name` (required with `x-model`), `options`, `option-value` (`id`), `option-label` (`name`), `option-icon` (`icon`), `size`, `variant` (`tonal`, `filled`, `outlined`), `multiple`, `inline` (intrinsic width instead of sharing the row). A validation error for the bound property replaces the hint. +Props: `label`, `hint`, `hint-class` (classes for the hint, as on the fields; a colour there paints it), `name` (required with `x-model`), `options`, `option-value` (`id`), `option-label` (`name`), `option-icon` (`icon`), `size`, `variant` (`tonal`, `filled`, `outlined`), `shape` (`round`, `square`), `multiple`, `inline` (intrinsic width instead of sharing the row). A validation error for the bound property replaces the hint. ### `` diff --git a/resources/css/components/groups.css b/resources/css/components/groups.css index 6058a39a..de21ac79 100644 --- a/resources/css/components/groups.css +++ b/resources/css/components/groups.css @@ -13,6 +13,16 @@ * full, the inner corners small, smaller still while pressed, and a selected segment fully round * (ConnectedButtonGroupSmallTokens and the M3 Expressive connected button group spec). * + * Shape (`shape="square"`, M3's "Default shape | Round, square" configuration): `--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 same corner its inner edges take, which is the square table M3 publishes for connected + * groups (4/8/8/16/20dp by size). A square *standard* group has no inner edges, so it hands its + * buttons the square corner scale `` draws (`--group-square`, and + * `--group-square-pressed` under the finger) instead of asking the caller to write it on each + * one. Selection morphs the other way in both: M3 has a toggle inside a group "swap shape + * square/round on selection", so a selected button in a square group turns round, which is why + * the selected rules keep `--group-full` rather than `--group-outer`. + * * 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 @@ -34,13 +44,32 @@ --group-inner: var(--md-sys-shape-corner-sm); --group-inner-pressed: var(--md-sys-shape-corner-xs); --group-full: 1.25rem; + --group-square: var(--md-sys-shape-corner-md); + --group-square-pressed: var(--md-sys-shape-corner-sm); + --group-outer: var(--group-full); } [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; } -[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][data-size='md'] { --group-pad: 1.5rem; --group-grow: 8px; --group-full: 1.75rem; --group-square: var(--md-sys-shape-corner-lg); --group-square-pressed: var(--md-sys-shape-corner-md); } +[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; --group-square: var(--md-sys-shape-corner-xl); --group-square-pressed: var(--md-sys-shape-corner-lg); } +[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; --group-square: var(--md-sys-shape-corner-xl); --group-square-pressed: var(--md-sys-shape-corner-lg); } + +/* A square group's ends take the corner its inner edges take — M3's square table for connected + groups — and a square standard group hands its buttons the button's own square scale. */ +[data-button-group][data-shape='square'] { --group-outer: var(--group-inner); } + +[data-button-group='standard'][data-shape='square'] > * { + border-radius: var(--group-square); +} + +[data-button-group='standard'][data-shape='square'] > :active:not(:disabled, [aria-disabled='true']) { + border-radius: var(--group-square-pressed); +} + +[data-button-group='standard'][data-shape='square'] > [aria-pressed='true'] { + border-radius: var(--group-full); +} [data-button-group='standard'] > :not([data-icon-button]):active:not(:disabled, [aria-disabled='true']) { padding-inline: calc(var(--group-pad) + var(--group-grow)); @@ -107,14 +136,14 @@ [data-button-group='connected'] > :first-child, [data-split='leading'] { - border-start-start-radius: var(--group-full); - border-end-start-radius: var(--group-full); + border-start-start-radius: var(--group-outer); + border-end-start-radius: var(--group-outer); } [data-button-group='connected'] > :last-child, [data-split='trailing'] { - border-start-end-radius: var(--group-full); - border-end-end-radius: var(--group-full); + border-start-end-radius: var(--group-outer); + border-end-end-radius: var(--group-outer); } /* diff --git a/resources/views/components/button-group.blade.php b/resources/views/components/button-group.blade.php index 8a670aa5..f8935067 100644 --- a/resources/views/components/button-group.blade.php +++ b/resources/views/components/button-group.blade.php @@ -11,6 +11,13 @@ the size of the buttons inside, so the spacing and corners match. For a choice bound to a property, `` draws a connected group of radios. + `shape` is M3's "Default shape | Round, square" configuration, and covers every button in the + group so it need not be written on each one: `square` squares a connected group's two ends to + the corner its inner edges take (M3's square table, 4/8/8/16/20dp by size) and gives a + standard group's buttons the square corner scale `` draws. Selection + still morphs the other way — M3 has a toggle inside a group "swap shape square/round on + selection" — so a selected button in a square group rounds. + A group never wraps to a second line — M3's rule, and the press expansion only reaches a neighbour on the same line anyway. Where a row is too long for its window the answer is a smaller `size` or fewer buttons, or two groups stacked. @@ -22,10 +29,12 @@ 'connected' => false, 'size' => 'sm', 'label' => null, + 'shape' => 'round', ]) @php $size = in_array($size, ['xs', 'sm', 'md', 'lg', 'xl'], true) ? $size : 'sm'; + $shape = $shape === 'square' ? 'square' : 'round'; @endphp
class([ 'inline-flex items-center', 'gap-0.5' => $connected, diff --git a/resources/views/components/group.blade.php b/resources/views/components/group.blade.php index d5bbef1e..c64e453e 100644 --- a/resources/views/components/group.blade.php +++ b/resources/views/components/group.blade.php @@ -15,9 +15,14 @@ you never to reduce. Corners move on the spatial spring and colours on the effects one (`state-transition-fast`). + `shape` is M3's "Default shape | Round, square" configuration: `square` squares the two ends + of the group to the corner its inner edges take (M3's square table, 4/8/8/16/20dp by size), + and the chosen segment still rounds fully, so selection reads the same either way. + ReStride's props, kept: `label`, `hint`, `hint-class`, `name` (needed with `x-model`, which names no property), `options`, `option-value`, `option-label`; plus `option-icon`, `size`, - `variant`, `multiple`, `inline`. A validation message for the bound property replaces the hint. + `variant`, `shape`, `multiple`, `inline`. A validation message for the bound property replaces + the hint. `hint-class` adds classes to the hint, as on ``: a colour there paints it (`hint-class="text-warning"` for a hint that warns). The hint's own colour then carries no @@ -35,6 +40,7 @@ 'optionIcon' => 'icon', 'size' => 'sm', 'variant' => 'tonal', + 'shape' => 'round', 'multiple' => false, 'inline' => false, ]) @@ -46,6 +52,9 @@ $errorKey = $model ?? (filled($attributes->get('name')) ? str_replace(['[]', '[', ']'], ['', '.', ''], (string) $attributes->get('name')) : null); $messages = $errorKey !== null && isset($errors) ? \Illuminate\Support\Arr::flatten($errors->get($errorKey)) : []; $size = in_array($size, ['xs', 'sm', 'md', 'lg', 'xl'], true) ? $size : 'sm'; + // M3's "Default shape | Round, square": a square group's ends take the corner its inner edges + // take; the chosen segment still rounds fully, which is the cue selection has always carried. + $shape = $shape === 'square' ? 'square' : 'round'; // M3: an xs or sm connected segment keeps a 48px target and a 48px minimum width, whatever // its 32px/40px container measures. From md the segment is wider than that on its own. $small = in_array($size, ['xs', 'sm'], true); @@ -78,7 +87,7 @@ {{ $label }} @endif -
! $inline, 'w-fit' => $inline])> +
! $inline, 'w-fit' => $inline])> @foreach ($options as $option)