{{-- A Material 3 Expressive button — label button, icon button or toggle — in one component. `variant` is M3's style: `filled`, `tonal`, `outlined`, `elevated` or `text` (the default). `color` is the role it is drawn in: `primary` (the default), `secondary`, `tertiary`, `error`, `success`, `warning`, `info`; `tone` is the same prop under ReStride's name. The shorthands `primary`, `danger` and `caution` mean filled primary, filled error and filled warning. Unknown values fall back to the defaults rather than rendering nothing. `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. 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 readers. `selected` makes it a toggle: `true` or `false` sets `aria-pressed` and M3's selected colours, and a selected round button turns square (a selected square icon button turns round). Text buttons are not toggles in M3; a selected one takes the tonal container. Values from androidx Compose Material 3's tokens (Button*Tokens, *IconButtonTokens, Apache-2.0); 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. Behaviour kept from maryUI: `link` renders an anchor with `wire:navigate` (unless `external` or `no-wire-navigate`); `disabled` works on a link too, as `aria-disabled`; `spinner` shows the loading indicator while the button's own `wire:click` runs (or the action named by a string); `responsive` hides the label below `lg`; `tooltip`, `tooltip-left`, `tooltip-right` and `tooltip-bottom` attach a plain tooltip. `fab` is a page's create action: an extended FAB pinned in the thumb zone below `sm`, a filled button above it — one element either way. 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. --}} @props([ 'label' => null, 'icon' => null, 'iconRight' => null, 'link' => null, 'external' => false, 'noWireNavigate' => false, 'variant' => null, 'color' => null, 'tone' => null, 'primary' => false, 'danger' => false, 'caution' => false, 'size' => 'sm', 'shape' => 'round', 'width' => 'default', 'selected' => null, 'spinner' => null, 'responsive' => false, 'tooltip' => null, 'tooltipLeft' => null, 'tooltipRight' => null, 'tooltipBottom' => null, 'fab' => false, 'disabled' => false, 'type' => 'button', 'corners' => null, ]) @php $variant = match (true) { in_array($variant, ['filled', 'tonal', 'outlined', 'text', 'elevated'], true) => $variant, $primary || $danger || $caution || $fab => 'filled', default => 'text', }; $color = match (true) { $danger => 'error', $caution => 'warning', in_array($color ?? $tone, ['primary', 'secondary', 'tertiary', 'error', 'success', 'warning', 'info'], true) => $color ?? $tone, default => 'primary', }; $size = in_array($size, ['xs', 'sm', 'md', 'lg', 'xl'], true) ? $size : 'sm'; $square = $shape === 'square'; $width = in_array($width, ['narrow', 'default', 'wide'], true) ? $width : 'default'; $iconOnly = filled($icon) && blank($label) && $slot->isEmpty(); $isLink = filled($link); $inert = $disabled && $isLink; $tip = $tooltip ?? $tooltipLeft ?? $tooltipRight ?? $tooltipBottom; $tipSide = match (true) { $tooltipLeft !== null => 'left', $tooltipRight !== null => 'right', $tooltipBottom !== null => 'bottom', default => 'top', }; $anchor = $tip !== null ? '--material-button-'.\Illuminate\Support\Str::lower(\Illuminate\Support\Str::random(10)) : null; $spinnerTarget = match (true) { $spinner === true, $spinner === 1, $spinner === '1' => $attributes->whereStartsWith('wire:click')->first(), is_string($spinner) && $spinner !== '' => $spinner, 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-3 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]; $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]; $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', 'transition-[border-radius,background-color,color,box-shadow,padding,margin] duration-(--md-sys-motion-spatial-fast-duration) ease-spatial-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. 'after:absolute after:top-1/2 after:left-1/2 after:size-full after:min-h-12 after:min-w-12 after:-translate-x-1/2 after:-translate-y-1/2' => 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, below sm only: an extended FAB in the thumb zone, clear of a bottom bar the layout declares. 'max-sm:fixed max-sm:end-4 max-sm:bottom-[calc(var(--material-bottom-bar,0px)+1rem)] max-sm:z-30 max-sm:h-14 max-sm:gap-2 max-sm:px-4 max-sm:rounded-corner-lg max-sm:type-title-md max-sm:bg-primary-container max-sm:text-on-primary-container max-sm:shadow-elevation-3' => $fab, ]; $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, 'aria-pressed' => $selected === null ? 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)); @endphp <{{ $tag }} {{ $attributes }}> @if ($spinnerTarget) @endif @if ($icon) @endif @unless ($iconOnly) $responsive])>{{ $label ?? $slot }} @endunless @if ($iconRight) @endif @if ($tip !== null) @endif