diff --git a/resources/css/components.css b/resources/css/components.css index f965e9d3..ca6f3e08 100644 --- a/resources/css/components.css +++ b/resources/css/components.css @@ -15,6 +15,7 @@ @import './components/loading.css'; @import './components/tooltip.css'; @import './components/badge.css'; +@import './components/button.css'; /* Inputs, selection and data */ diff --git a/resources/css/components/button.css b/resources/css/components/button.css new file mode 100644 index 00000000..8fdbc7b7 --- /dev/null +++ b/resources/css/components/button.css @@ -0,0 +1,452 @@ +/* + * : M3 Expressive's buttons — label button, icon button and toggle — and the page's + * create action that is a FAB on a compact window. + * + * Geometry from androidx Compose Material 3's tokens (Apache-2.0), per `data-md-size`: + * + * size height label padding gap type icon (label / icon-only) outline square pressed + * xs 32px 16px 8px label-lg 20 / 20px 1px md sm + * sm 40px 16px 8px label-lg 20 / 24px 1px md sm + * md 56px 24px 8px title-md 24 / 24px 1px lg md + * lg 96px 48px 12px headline-sm 32 / 32px 2px xl lg + * xl 136px 64px 16px headline-lg 40 / 40px 3px xl lg + * + * (ButtonXSmall…XLargeTokens; the xs padding is the 16px Phase D settled on, ACT-09.) An icon button + * (`data-md-icon-button`) is as tall and `narrow`, `default` or `wide` across by + * XSmall…XLargeIconButtonTokens: 28/32/40, 32/40/52, 48/56/72, 64/96/128 and 104/136/184px. The + * views size the glyphs through ``; `--md-button-icon` carries the same size for the + * loading indicator that stands in for the glyph while the button's action runs. + * + * Shape: round (`--md-sys-shape-corner-full`) by default; `data-md-shape="square"` and a selected + * button take the size's square corner, a selected square icon button turns round, and every one + * squares off to the pressed corner under the finger. Those corner rules are wrapped in `:where()`, + * so they carry no specificity at all: a group's stylesheet (button-group.css, split-button.css) + * reshapes the buttons inside it with any selector, without a specificity fight. + * + * Colour: `data-md-color` picks the roles a variant draws with; the variants set + * `--md-button-container`, `--md-button-label`, `--md-button-outline` and `--md-button-elevation`, + * which one rule paints, so the disabled treatment (on-surface at 10% behind 38% text, + * FilledButtonTokens.DisabledContainerOpacity and DisabledLabelTextOpacity) replaces them with a + * plain declaration. Primary's tonal button is secondary-container and its standard icon button + * on-surface-variant, as M3 draws them; the text button's label is primary, as Compose draws it, + * not the token's on-surface-variant, which its own source marks as wrong; a selected text button + * takes the tonal container (the library's deliberate deviations, docs/audits/m3-alignment). + * + * Motion: corners, padding, margins and `bottom` on the fast spatial spring, colour and shadow on + * the fast effects spring beside it — a colour must never overshoot (tokens/motion.css). + * + * The state layer, focus ring and 48px target (xs and sm) are the declarations of the foundation's + * `md-state-layer`, `md-focus-ring` and `md-touch-target` (foundation/interaction.css), keyed on + * the button itself. + * + * `data-md-compact-fab`: below `medium` (600px) the button is an extended FAB in the thumb zone — + * fixed 16px from the end, 56px tall with 16px padding, the large corner, `title-md`, + * primary-container at elevation 3 (ExtendedFabPrimaryTokens) — lifted clear of a bottom bar and + * of a snackbar on screen (`--material-bottom-bar`, `--material-snackbar-height`), because M3 puts a + * snackbar above a FAB and never over one. From `medium` it is the button it was written as. + * `data-md-responsive` on the label hides it below `expanded` (840px). + */ + +@layer material.reset, material.tokens, material.base, material.layout, material.components, material.text, material.visibility; + +@import './icon.css'; +@import './loading.css'; +@import './tooltip.css'; + +@layer material.components { + [data-md-button] { + --md-button-color: var(--md-sys-color-primary); + --md-button-on-color: var(--md-sys-color-on-primary); + --md-button-tone: var(--md-sys-color-secondary-container); + --md-button-on-tone: var(--md-sys-color-on-secondary-container); + --md-button-tone-selected: var(--md-sys-color-secondary); + --md-button-on-tone-selected: var(--md-sys-color-on-secondary); + --md-button-quiet: var(--md-sys-color-on-surface-variant); + + --md-button-container: transparent; + --md-button-label: var(--md-button-color); + --md-button-outline: transparent; + --md-button-elevation: none; + + position: relative; + isolation: isolate; + display: inline-flex; + flex-shrink: 0; + align-items: center; + justify-content: center; + block-size: var(--md-button-height); + background-color: var(--md-button-container); + color: var(--md-button-label); + box-shadow: var(--md-button-elevation); + white-space: nowrap; + cursor: pointer; + -webkit-user-select: none; + user-select: none; + outline: none; + 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); + + &::before { + content: ''; + position: absolute; + inset: 0; + z-index: -1; + border-radius: inherit; + background-color: currentColor; + opacity: 0; + pointer-events: none; + transition: opacity var(--md-sys-motion-effects-fast-duration) var(--md-sys-motion-effects-fast); + } + + @media (hover: hover) { + &:hover::before { + opacity: var(--md-sys-state-hover-state-layer-opacity); + } + } + + &:focus-visible { + outline: 3px solid var(--md-sys-color-secondary); + outline-offset: 2px; + } + + &:focus-visible::before { + opacity: var(--md-sys-state-focus-state-layer-opacity); + } + + &:active::before { + opacity: var(--md-sys-state-pressed-state-layer-opacity); + } + } + + /* Sizes. */ + [data-md-button][data-md-size='xs'] { + --md-button-height: 32px; + --md-button-padding: var(--md-sys-measurement-space200); + --md-button-gap: var(--md-sys-measurement-space100); + --md-button-icon: 20px; + --md-button-outline-width: 1px; + --md-button-square: var(--md-sys-shape-corner-md); + --md-button-pressed: var(--md-sys-shape-corner-sm); + --md-button-narrow: 28px; + --md-button-default: 32px; + --md-button-wide: 40px; + } + + [data-md-button][data-md-size='sm'] { + --md-button-height: 40px; + --md-button-padding: var(--md-sys-measurement-space200); + --md-button-gap: var(--md-sys-measurement-space100); + --md-button-icon: 20px; + --md-button-outline-width: 1px; + --md-button-square: var(--md-sys-shape-corner-md); + --md-button-pressed: var(--md-sys-shape-corner-sm); + --md-button-narrow: 32px; + --md-button-default: 40px; + --md-button-wide: 52px; + } + + [data-md-button][data-md-size='sm'][data-md-icon-button] { + --md-button-icon: 24px; + } + + [data-md-button][data-md-size='md'] { + --md-button-height: 56px; + --md-button-padding: var(--md-sys-measurement-space300); + --md-button-gap: var(--md-sys-measurement-space100); + --md-button-icon: 24px; + --md-button-outline-width: 1px; + --md-button-square: var(--md-sys-shape-corner-lg); + --md-button-pressed: var(--md-sys-shape-corner-md); + --md-button-narrow: 48px; + --md-button-default: 56px; + --md-button-wide: 72px; + } + + [data-md-button][data-md-size='lg'] { + --md-button-height: 96px; + --md-button-padding: var(--md-sys-measurement-space600); + --md-button-gap: 12px; + --md-button-icon: 32px; + --md-button-outline-width: 2px; + --md-button-square: var(--md-sys-shape-corner-xl); + --md-button-pressed: var(--md-sys-shape-corner-lg); + --md-button-narrow: 64px; + --md-button-default: 96px; + --md-button-wide: 128px; + } + + [data-md-button][data-md-size='xl'] { + --md-button-height: 136px; + --md-button-padding: var(--md-sys-measurement-space800); + --md-button-gap: var(--md-sys-measurement-space200); + --md-button-icon: 40px; + --md-button-outline-width: 3px; + --md-button-square: var(--md-sys-shape-corner-xl); + --md-button-pressed: var(--md-sys-shape-corner-lg); + --md-button-narrow: 104px; + --md-button-default: 136px; + --md-button-wide: 184px; + } + + [data-md-button]:not([data-md-icon-button]) { + gap: var(--md-button-gap); + padding-inline: var(--md-button-padding); + } + + [data-md-button]:not([data-md-icon-button]):is([data-md-size='xs'], [data-md-size='sm']) { + font: var(--md-sys-typescale-label-lg); + letter-spacing: var(--md-sys-typescale-label-lg-tracking); + font-variation-settings: normal; + } + + [data-md-button]:not([data-md-icon-button])[data-md-size='md'] { + font: var(--md-sys-typescale-title-md); + letter-spacing: var(--md-sys-typescale-title-md-tracking); + font-variation-settings: normal; + } + + [data-md-button]:not([data-md-icon-button])[data-md-size='lg'] { + font: var(--md-sys-typescale-headline-sm); + letter-spacing: var(--md-sys-typescale-headline-sm-tracking); + font-variation-settings: normal; + } + + [data-md-button]:not([data-md-icon-button])[data-md-size='xl'] { + font: var(--md-sys-typescale-headline-lg); + letter-spacing: var(--md-sys-typescale-headline-lg-tracking); + font-variation-settings: normal; + } + + [data-md-button][data-md-icon-button] { + --md-button-width: var(--md-button-default); + + inline-size: var(--md-button-width); + + &[data-md-width='narrow'] { + --md-button-width: var(--md-button-narrow); + } + + &[data-md-width='wide'] { + --md-button-width: var(--md-button-wide); + } + } + + /* M3's minimum target, past a button drawn smaller than 48px. */ + [data-md-button]:is([data-md-size='xs'], [data-md-size='sm'])::after { + content: ''; + position: absolute; + top: 50%; + left: 50%; + width: 100%; + height: 100%; + min-width: var(--md-sys-measurement-space600); + min-height: var(--md-sys-measurement-space600); + transform: translate(-50%, -50%); + } + + /* Shape, without specificity: a group reshapes the buttons in it. */ + :where([data-md-button]) { + border-radius: var(--md-sys-shape-corner-full); + } + + :where([data-md-button]):where([data-md-shape='square'], [data-md-selected='true']) { + border-radius: var(--md-button-square); + } + + :where([data-md-button]):where([data-md-icon-button][data-md-shape='square'][data-md-selected='true']) { + border-radius: var(--md-sys-shape-corner-full); + } + + :where([data-md-button]):where(:active) { + border-radius: var(--md-button-pressed); + } + + /* Colour roles. */ + [data-md-button][data-md-color='secondary'] { + --md-button-color: var(--md-sys-color-secondary); + --md-button-on-color: var(--md-sys-color-on-secondary); + --md-button-tone: var(--md-sys-color-secondary-container); + --md-button-on-tone: var(--md-sys-color-on-secondary-container); + } + + [data-md-button][data-md-color='tertiary'] { + --md-button-color: var(--md-sys-color-tertiary); + --md-button-on-color: var(--md-sys-color-on-tertiary); + --md-button-tone: var(--md-sys-color-tertiary-container); + --md-button-on-tone: var(--md-sys-color-on-tertiary-container); + } + + [data-md-button][data-md-color='error'] { + --md-button-color: var(--md-sys-color-error); + --md-button-on-color: var(--md-sys-color-on-error); + --md-button-tone: var(--md-sys-color-error-container); + --md-button-on-tone: var(--md-sys-color-on-error-container); + } + + [data-md-button][data-md-color='success'] { + --md-button-color: var(--md-sys-color-success); + --md-button-on-color: var(--md-sys-color-on-success); + --md-button-tone: var(--md-sys-color-success-container); + --md-button-on-tone: var(--md-sys-color-on-success-container); + } + + [data-md-button][data-md-color='warning'] { + --md-button-color: var(--md-sys-color-warning); + --md-button-on-color: var(--md-sys-color-on-warning); + --md-button-tone: var(--md-sys-color-warning-container); + --md-button-on-tone: var(--md-sys-color-on-warning-container); + } + + [data-md-button][data-md-color='info'] { + --md-button-color: var(--md-sys-color-info); + --md-button-on-color: var(--md-sys-color-on-info); + --md-button-tone: var(--md-sys-color-info-container); + --md-button-on-tone: var(--md-sys-color-on-info-container); + } + + /* Every colour but primary selects into the colour itself and is quiet in it. */ + [data-md-button]:not([data-md-color='primary']) { + --md-button-tone-selected: var(--md-button-color); + --md-button-on-tone-selected: var(--md-button-on-color); + --md-button-quiet: var(--md-button-color); + } + + /* Variants. */ + [data-md-button][data-md-variant='filled'] { + --md-button-container: var(--md-button-color); + --md-button-label: var(--md-button-on-color); + + &[data-md-selected='false'] { + --md-button-container: var(--md-sys-color-surface-container); + --md-button-label: var(--md-sys-color-on-surface-variant); + } + } + + [data-md-button][data-md-variant='tonal'] { + --md-button-container: var(--md-button-tone); + --md-button-label: var(--md-button-on-tone); + + &[data-md-selected='true'] { + --md-button-container: var(--md-button-tone-selected); + --md-button-label: var(--md-button-on-tone-selected); + } + } + + [data-md-button][data-md-variant='outlined'] { + --md-button-label: var(--md-button-quiet); + --md-button-outline: var(--md-sys-color-outline-variant); + + border: var(--md-button-outline-width) solid var(--md-button-outline); + + &[data-md-selected='true'] { + --md-button-container: var(--md-sys-color-inverse-surface); + --md-button-label: var(--md-sys-color-inverse-on-surface); + --md-button-outline: transparent; + } + } + + [data-md-button][data-md-variant='elevated'] { + --md-button-container: var(--md-sys-color-surface-container-low); + --md-button-elevation: var(--md-sys-elevation-1); + + &[data-md-selected='true'] { + --md-button-container: var(--md-button-color); + --md-button-label: var(--md-button-on-color); + } + } + + [data-md-button][data-md-variant='text'][data-md-icon-button]:not([data-md-selected='true']) { + --md-button-label: var(--md-button-quiet); + } + + [data-md-button][data-md-variant='text']:not([data-md-icon-button])[data-md-selected='true'] { + --md-button-container: var(--md-button-tone); + --md-button-label: var(--md-button-on-tone); + } + + @media (hover: hover) { + [data-md-button]:is([data-md-variant='filled'], [data-md-variant='tonal']):hover { + --md-button-elevation: var(--md-sys-elevation-1); + } + + [data-md-button][data-md-variant='elevated']:hover { + --md-button-elevation: var(--md-sys-elevation-2); + } + } + + /* Disabled: no state layer, no shadow, M3's disabled colours over whatever the variant set. */ + [data-md-button]:is(:disabled, [aria-disabled='true']) { + box-shadow: none; + color: color-mix(in srgb, var(--md-sys-color-on-surface) calc(var(--md-sys-state-disabled-content-opacity) * 100%), transparent); + + &::before { + display: none; + } + } + + [data-md-button]:is([data-md-variant='filled'], [data-md-variant='tonal'], [data-md-variant='elevated'], [data-md-variant='outlined'][data-md-selected='true']):is(:disabled, [aria-disabled='true']) { + background-color: color-mix(in srgb, var(--md-sys-color-on-surface) 10%, transparent); + } + + [data-md-button]:disabled { + cursor: not-allowed; + } + + [data-md-button][aria-disabled='true'] { + pointer-events: none; + } + + /* The glyph's place, and the loading indicator that stands in for it. */ + [data-md-button-icon] { + display: contents; + } + + [data-md-button-spinner] { + align-items: center; + justify-content: center; + } + + [data-md-button-spinner] > [data-md-loading] { + inline-size: var(--md-button-icon); + block-size: var(--md-button-icon); + color: currentColor; + } + + @media (width < 840px) { + [data-md-button] > [data-md-responsive] { + display: none; + } + } + + @media (width < 600px) { + [data-md-button][data-md-compact-fab] { + --md-button-height: 56px; + --md-button-padding: var(--md-sys-measurement-space200); + --md-button-gap: var(--md-sys-measurement-space100); + + position: fixed; + inset-inline-end: var(--md-sys-measurement-space200); + bottom: calc(var(--material-bottom-bar, 0px) + var(--material-snackbar-height, 0px) + var(--md-sys-measurement-space200)); + z-index: 30; + border-radius: var(--md-sys-shape-corner-lg); + background-color: var(--md-sys-color-primary-container); + color: var(--md-sys-color-on-primary-container); + box-shadow: var(--md-sys-elevation-3); + font: var(--md-sys-typescale-title-md); + letter-spacing: var(--md-sys-typescale-title-md-tracking); + font-variation-settings: normal; + } + + [data-md-button][data-md-compact-fab] [data-md-icon] { + inline-size: 24px; + block-size: 24px; + } + } +} diff --git a/resources/css/components/groups.css b/resources/css/components/groups.css index de21ac79..33a595bf 100644 --- a/resources/css/components/groups.css +++ b/resources/css/components/groups.css @@ -71,15 +71,15 @@ border-radius: var(--group-full); } -[data-button-group='standard'] > :not([data-icon-button]):active:not(:disabled, [aria-disabled='true']) { +[data-button-group='standard'] > :not([data-md-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'])) { +[data-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(--group-pad) - var(--group-grow)); } -[data-button-group='standard'] > :not([data-icon-button]):active:not(:disabled, [aria-disabled='true']) + :not([data-icon-button]) { +[data-button-group='standard'] > :not([data-md-icon-button]):active:not(:disabled, [aria-disabled='true']) + :not([data-md-icon-button]) { padding-inline-start: calc(var(--group-pad) - var(--group-grow)); } diff --git a/resources/css/components/toolbar.css b/resources/css/components/toolbar.css index 14e95ab2..48af9a08 100644 --- a/resources/css/components/toolbar.css +++ b/resources/css/components/toolbar.css @@ -60,11 +60,11 @@ color: var(--md-sys-color-on-primary-container); } -[data-toolbar][data-vibrant] [data-icon-button]:not([aria-pressed="true"]) { +[data-toolbar][data-vibrant] [data-md-icon-button]:not([aria-pressed="true"]) { color: var(--md-sys-color-on-primary-container); } -[data-toolbar][data-vibrant] [data-icon-button][aria-pressed="true"] { +[data-toolbar][data-vibrant] [data-md-icon-button][aria-pressed="true"] { background-color: var(--md-sys-color-surface-container); color: var(--md-sys-color-on-surface); } @@ -72,7 +72,7 @@ /* M3's colour list for a standard toolbar ends "Standard button (Primary)", as the vibrant list ends "Standard button (On primary container)" — the row above. An icon button carries its own ink as a utility, so the container's `color` never reaches it and this rule has to. */ -[data-toolbar]:not([data-vibrant]) [data-icon-button]:not([aria-pressed="true"]) { +[data-toolbar]:not([data-vibrant]) [data-md-icon-button]:not([aria-pressed="true"]) { color: var(--md-sys-color-primary); } diff --git a/resources/views/components/button.blade.php b/resources/views/components/button.blade.php index 4e8fab32..8a0f8c4a 100644 --- a/resources/views/components/button.blade.php +++ b/resources/views/components/button.blade.php @@ -9,9 +9,8 @@ `size` is Expressive's scale — `xs` 32px, `sm` 40px (the default), `md` 56px, `lg` 96px, `xl` 136px — each with its own padding, type style and icon size. `shape` is `round` (the default) or `square`; either one squares off towards a smaller corner while pressed, on the - fast spatial spring, while the colours cross on the fast effects spring beside it - (`state-transition-fast`, resources/css/components/actions.css: a colour must never - overshoot). + fast spatial spring, while the colours cross on the fast effects spring beside it (a colour + must never overshoot). With an `icon` and no label it is an icon button: `width` is `narrow`, `default` or `wide`, `variant="text"` is M3's standard icon button, and the tooltip or label names it for screen @@ -39,8 +38,9 @@ (`--material-bottom-bar`, `--material-snackbar-height`), because M3 puts a snackbar above a FAB and never over one. - Never pass `hidden`, a display or a position class: the button is `inline-flex` and - `relative`, and whichever Tailwind emits last wins. Wrap it instead. --}} + The view renders the props as `data-md-*` attributes and resources/css/components/button.css + draws them, in the package's components layer: a caller's class or unlayered CSS outranks + every rule there, `hidden` included. --}} @props([ 'label' => null, @@ -68,7 +68,6 @@ 'fab' => false, 'disabled' => false, 'type' => 'button', - 'corners' => null, ]) @php @@ -86,7 +85,7 @@ }; $size = in_array($size, ['xs', 'sm', 'md', 'lg', 'xl'], true) ? $size : 'sm'; - $square = $shape === 'square'; + $shape = $shape === 'square' ? 'square' : 'round'; $width = in_array($width, ['narrow', 'default', 'wide'], true) ? $width : 'default'; $iconOnly = filled($icon) && blank($label) && $slot->isEmpty(); $isLink = filled($link); @@ -107,134 +106,61 @@ default => null, }; - // Every colour class written out whole, so Tailwind compiles it. - $ink = [ - 'primary' => 'text-primary', 'secondary' => 'text-secondary', 'tertiary' => 'text-tertiary', - 'error' => 'text-error', 'success' => 'text-success', 'warning' => 'text-warning', 'info' => 'text-info', - ]; - $fill = [ - 'primary' => 'bg-primary text-on-primary', 'secondary' => 'bg-secondary text-on-secondary', - 'tertiary' => 'bg-tertiary text-on-tertiary', 'error' => 'bg-error text-on-error', - 'success' => 'bg-success text-on-success', 'warning' => 'bg-warning text-on-warning', 'info' => 'bg-info text-on-info', - ]; - $container = [ - 'primary' => 'bg-secondary-container text-on-secondary-container', 'secondary' => 'bg-secondary-container text-on-secondary-container', - 'tertiary' => 'bg-tertiary-container text-on-tertiary-container', 'error' => 'bg-error-container text-on-error-container', - 'success' => 'bg-success-container text-on-success-container', 'warning' => 'bg-warning-container text-on-warning-container', - 'info' => 'bg-info-container text-on-info-container', - ]; - $containerSelected = ['primary' => 'bg-secondary text-on-secondary'] + $fill; - $quietInk = ['primary' => 'text-on-surface-variant'] + $ink; - - $colours = match ($variant) { - 'filled' => $selected === false ? 'bg-surface-container text-on-surface-variant' : $fill[$color], - 'tonal' => $selected === true ? $containerSelected[$color] : $container[$color], - 'outlined' => $selected === true ? 'bg-inverse-surface text-inverse-on-surface border-transparent' : 'border-outline-variant '.$quietInk[$color], - 'elevated' => $selected === true ? $fill[$color] : 'bg-surface-container-low '.$ink[$color], - 'text' => match (true) { - $iconOnly => $selected === true ? $ink[$color] : $quietInk[$color], - default => $selected === true ? $container[$color] : $ink[$color], - }, - }; - - $squareCorners = ['xs' => 'rounded-corner-md', 'sm' => 'rounded-corner-md', 'md' => 'rounded-corner-lg', 'lg' => 'rounded-corner-xl', 'xl' => 'rounded-corner-xl']; - $pressed = ['xs' => 'active:rounded-corner-sm', 'sm' => 'active:rounded-corner-sm', 'md' => 'active:rounded-corner-md', 'lg' => 'active:rounded-corner-lg', 'xl' => 'active:rounded-corner-lg']; - - $corner = match (true) { - $iconOnly && $selected === true => $square ? 'rounded-corner-full' : $squareCorners[$size], - $selected === true, $square => $squareCorners[$size], - default => 'rounded-corner-full', - }; - - $dimensions = $iconOnly - ? [ - 'xs' => ['narrow' => 'h-8 w-7', 'default' => 'size-8', 'wide' => 'h-8 w-10'], - 'sm' => ['narrow' => 'h-10 w-8', 'default' => 'size-10', 'wide' => 'h-10 w-13'], - 'md' => ['narrow' => 'h-14 w-12', 'default' => 'size-14', 'wide' => 'h-14 w-18'], - 'lg' => ['narrow' => 'h-24 w-16', 'default' => 'size-24', 'wide' => 'h-24 w-32'], - 'xl' => ['narrow' => 'h-34 w-26', 'default' => 'size-34', 'wide' => 'h-34 w-46'], - ][$size][$width] - : [ - 'xs' => 'h-8 gap-2 px-4 type-label-lg', - 'sm' => 'h-10 gap-2 px-4 type-label-lg', - 'md' => 'h-14 gap-2 px-6 type-title-md', - 'lg' => 'h-24 gap-3 px-12 type-headline-sm', - 'xl' => 'h-34 gap-4 px-16 type-headline-lg', - ][$size]; - + // The glyph's size in px (button.css draws the loading indicator at the same size). M3 draws a + // symbol from its 20px cut below 24px, which the icon component picks from the size. $iconSize = $iconOnly - ? ['xs' => 'size-5', 'sm' => 'size-6', 'md' => 'size-6', 'lg' => 'size-8', 'xl' => 'size-10'][$size] - : ['xs' => 'size-5', 'sm' => 'size-5', 'md' => 'size-6', 'lg' => 'size-8', 'xl' => 'size-10'][$size]; - - // M3 draws a Material Symbol from a 20px cut below 24px: heavier strokes, wider counters, so a - // small glyph does not thin out. The cut follows the size the icon is drawn at, never the button's. - $optical = $iconSize === 'size-5' ? '20' : '24'; - - $outline = ['xs' => 'border', 'sm' => 'border', 'md' => 'border', 'lg' => 'border-2', 'xl' => 'border-3'][$size]; - $contained = in_array($variant, ['filled', 'tonal', 'elevated'], true) || ($variant === 'outlined' && $selected === true); - - $classes = [ - 'group/button state-layer focus-ring inline-flex shrink-0 cursor-pointer select-none items-center justify-center whitespace-nowrap', - 'state-transition-fast', - $dimensions, - // A composite component (a split button's halves) passes the corners its place needs. - $corners ?? $corner.' '.$pressed[$size], - $colours, - $outline => $variant === 'outlined', - 'hover:shadow-elevation-1' => in_array($variant, ['filled', 'tonal'], true), - 'shadow-elevation-1 hover:shadow-elevation-2' => $variant === 'elevated', - // Below 48px the touch target reaches past the button, as M3 requires (tokens/state.css). - 'touch-target' => in_array($size, ['xs', 'sm'], true), - 'disabled:cursor-not-allowed disabled:shadow-none aria-disabled:pointer-events-none aria-disabled:shadow-none', - 'disabled:bg-on-surface/10 disabled:text-on-surface/38 aria-disabled:bg-on-surface/10 aria-disabled:text-on-surface/38' => $contained, - 'disabled:text-on-surface/38 aria-disabled:text-on-surface/38' => ! $contained, - // The FAB, on a compact window only: an extended FAB in the thumb zone, clear of a bottom bar the layout declares. - 'max-medium:fixed max-medium:end-4 max-medium:bottom-[calc(var(--material-bottom-bar,0px)+var(--material-snackbar-height,0px)+1rem)] max-medium:z-30 max-medium:h-14 max-medium:gap-2 max-medium:px-4 max-medium:rounded-corner-lg max-medium:type-title-md max-medium:bg-primary-container max-medium:text-on-primary-container max-medium:shadow-elevation-3' => $fab, - ]; + ? ['xs' => 20, 'sm' => 24, 'md' => 24, 'lg' => 32, 'xl' => 40][$size] + : ['xs' => 20, 'sm' => 20, 'md' => 24, 'lg' => 32, 'xl' => 40][$size]; $tag = $isLink ? 'a' : 'button'; - $attributes = $attributes - ->class($classes) - ->merge(array_filter([ - 'href' => $isLink ? $link : null, - 'target' => $isLink && $external ? '_blank' : null, - 'rel' => $isLink && $external ? 'noopener' : null, - 'wire:navigate' => $isLink && ! $external && ! $noWireNavigate && ! $attributes->has('wire:navigate') ? true : null, - 'aria-disabled' => $inert ? 'true' : null, - 'tabindex' => $inert ? '-1' : null, - 'type' => $isLink ? null : $type, - 'disabled' => ! $isLink && $disabled ? true : null, - 'aria-label' => $iconOnly && ! $attributes->has('aria-label') ? ($label ?? $tip) : null, - // A link is not a toggle: ARIA defines aria-pressed for buttons only. A selected link keeps - // the selected look; `aria-current` is the caller's to set (`:aria-current="'page'"`). - 'aria-pressed' => $selected === null || $isLink ? null : ($selected ? 'true' : 'false'), - 'data-icon-button' => $iconOnly ? true : null, - 'wire:loading.attr' => $spinnerTarget ? 'disabled' : null, - 'wire:target' => $spinnerTarget, - 'style' => $anchor ? "anchor-name: {$anchor}" : null, - ], fn ($value): bool => $value !== null)); + $attributes = $attributes->merge(array_filter([ + 'data-md-button' => true, + 'data-md-variant' => $variant, + 'data-md-color' => $color, + 'data-md-size' => $size, + 'data-md-shape' => $shape, + 'data-md-icon-button' => $iconOnly ? true : null, + 'data-md-width' => $iconOnly ? $width : null, + 'data-md-selected' => $selected === null ? null : ($selected ? 'true' : 'false'), + 'data-md-compact-fab' => $fab ? true : null, + 'href' => $isLink ? $link : null, + 'target' => $isLink && $external ? '_blank' : null, + 'rel' => $isLink && $external ? 'noopener' : null, + 'wire:navigate' => $isLink && ! $external && ! $noWireNavigate && ! $attributes->has('wire:navigate') ? true : null, + 'aria-disabled' => $inert ? 'true' : null, + 'tabindex' => $inert ? '-1' : null, + 'type' => $isLink ? null : $type, + 'disabled' => ! $isLink && $disabled ? true : null, + 'aria-label' => $iconOnly && ! $attributes->has('aria-label') ? ($label ?? $tip) : null, + // A link is not a toggle: ARIA defines aria-pressed for buttons only. A selected link keeps + // the selected look; `aria-current` is the caller's to set (`:aria-current="'page'"`). + 'aria-pressed' => $selected === null || $isLink ? null : ($selected ? 'true' : 'false'), + 'wire:loading.attr' => $spinnerTarget ? 'disabled' : null, + 'wire:target' => $spinnerTarget, + 'style' => $anchor ? "anchor-name: {$anchor}" : null, + ], fn ($value): bool => $value !== null)); @endphp <{{ $tag }} {{ $attributes }}> @if ($spinnerTarget) - - + + @endif @if ($icon) - - + + @endif @unless ($iconOnly) - $responsive])>{{ $label ?? $slot }} + {{ $label ?? $slot }} @endunless @if ($iconRight) - + @endif @if ($tip !== null) diff --git a/resources/views/components/split-button.blade.php b/resources/views/components/split-button.blade.php index ff430b88..d5c3f6c2 100644 --- a/resources/views/components/split-button.blade.php +++ b/resources/views/components/split-button.blade.php @@ -50,7 +50,6 @@ :size="$size" :disabled="$disabled" :spinner="$spinner" - corners="" data-split="leading" :class="$leadingPadding" /> @@ -64,7 +63,6 @@ :color="$color" :size="$size" :disabled="$disabled" - corners="" data-split="trailing" :class="$trailingWidth" /> diff --git a/resources/views/components/theme-toggle.blade.php b/resources/views/components/theme-toggle.blade.php index 206762a8..5ae93846 100644 --- a/resources/views/components/theme-toggle.blade.php +++ b/resources/views/components/theme-toggle.blade.php @@ -84,7 +84,7 @@