Merge branch 'worktree-agent-a9b4c522a39c6ce62'
# Conflicts: # tests/Feature/StylesheetsTest.php # workbench/resources/css/app.css
This commit is contained in:
@@ -7,10 +7,10 @@
|
||||
Both are `error` by default, as M3 draws them, and an `outline` status label draws its edge in
|
||||
`outline`, not `outline-variant`: M3 asks a badge for 3:1 against what is behind it, and
|
||||
`outline-variant` is the decorative role dividers use. `floating` pins one to the top-end
|
||||
corner of a `relative` parent, on M3's anchor geometry — the dot flush in the corner, the
|
||||
count 2px above it — an icon or an icon button:
|
||||
corner of a `position: relative` parent, on M3's anchor geometry — the dot flush in the corner,
|
||||
the count 2px above it — an icon or an icon button:
|
||||
|
||||
<span class="relative inline-flex"><x-icon name="notifications" /><x-badge value="4" floating /></span>
|
||||
<span style="position: relative; display: inline-flex"><x-icon name="notifications" /><x-badge value="4" floating /></span>
|
||||
|
||||
The status label is not an M3 badge but every app needs one: `tonal` draws the value in the
|
||||
colour's container ("Expired" in error-container), `solid` in the colour itself (a label that
|
||||
@@ -21,13 +21,17 @@
|
||||
count (the ink an outline badge already writes in), surface-container-high with
|
||||
on-surface-variant text when `tonal`, the outline edge when `outline`.
|
||||
- `plain` — no background, text or border colour at all, only shape, size and type, so the
|
||||
caller's classes paint it: `<x-badge value="Run" tonal color="plain" class="bg-tertiary-container text-on-tertiary-container" />`.
|
||||
caller's CSS paints it.
|
||||
|
||||
The value is `value`, or the slot, which renders as HTML — an icon beside the word:
|
||||
`<x-badge tonal><x-icon name="bolt" optical="20" class="size-3" /> Pro</x-badge>`. With neither it is a dot.
|
||||
`<x-badge tonal><x-icon name="bolt" size="12" /> Pro</x-badge>`. With neither it is a dot.
|
||||
|
||||
A count or dot says nothing to a screen reader on its own: give the icon's control a label
|
||||
that includes it ("Notifications, 4 new"), or pass `label` here. --}}
|
||||
that includes it ("Notifications, 4 new"), or pass `label` here.
|
||||
|
||||
Drawn by resources/css/components/badge.css from `data-md-dot` (the small badge),
|
||||
`data-md-variant` (a status label: `solid`, `tonal` or `outline`, the first of them that is
|
||||
set, in that order), `data-md-color` and `data-md-floating`. --}}
|
||||
|
||||
@props([
|
||||
'value' => null,
|
||||
@@ -54,38 +58,23 @@
|
||||
$markup = false;
|
||||
}
|
||||
|
||||
$filled = [
|
||||
'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',
|
||||
'neutral' => 'bg-on-surface-variant text-surface',
|
||||
];
|
||||
$container = [
|
||||
'primary' => 'bg-primary-container text-on-primary-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',
|
||||
'neutral' => 'bg-surface-container-high text-on-surface-variant',
|
||||
];
|
||||
$paint = match (true) {
|
||||
$color === 'plain' => '',
|
||||
! $status, $solid => $filled[$color],
|
||||
$tonal => $container[$color],
|
||||
default => 'border-outline text-on-surface-variant',
|
||||
// `solid` wins over `tonal`, and either over `outline`, as the three have always resolved.
|
||||
$variant = match (true) {
|
||||
! $status => null,
|
||||
(bool) $solid => 'solid',
|
||||
(bool) $tonal => 'tonal',
|
||||
default => 'outline',
|
||||
};
|
||||
|
||||
$attributes = $attributes
|
||||
->class([
|
||||
'inline-flex shrink-0 items-center justify-center whitespace-nowrap',
|
||||
'size-1.5 rounded-corner-full' => $dot,
|
||||
'h-4 min-w-4 rounded-corner-full px-1 type-label-sm tabular-nums' => ! $dot && ! $status,
|
||||
'h-6 gap-1 rounded-corner-sm px-2 type-label-md' => $status,
|
||||
'border' => $outline && ! $tonal && ! $solid && ! $dot,
|
||||
$paint => $paint !== '',
|
||||
'absolute top-0 end-0' => $floating && $dot,
|
||||
'absolute -top-0.5 start-[calc(100%-0.75rem)]' => $floating && ! $dot,
|
||||
])
|
||||
->merge(array_filter([
|
||||
'aria-label' => $label,
|
||||
'aria-hidden' => $label === null && ! $status ? 'true' : null,
|
||||
]));
|
||||
$attributes = $attributes->merge(array_filter([
|
||||
'data-md-badge' => true,
|
||||
'data-md-dot' => $dot ? true : null,
|
||||
'data-md-variant' => $variant,
|
||||
'data-md-color' => $color,
|
||||
'data-md-floating' => $floating ? true : null,
|
||||
'aria-label' => $label,
|
||||
'aria-hidden' => $label === null && ! $status ? 'true' : null,
|
||||
], fn ($value): bool => $value !== null));
|
||||
@endphp
|
||||
|
||||
<span {{ $attributes }}>@unless ($dot)@if ($markup){{ $slot }}@else{{ $text }}@endif@endunless</span>
|
||||
|
||||
@@ -45,7 +45,7 @@
|
||||
smaller `size` or fewer buttons, or two groups stacked.
|
||||
|
||||
Spacing from the M3 Expressive spec (ButtonGroupSmallTokens: 12px at the small size). The
|
||||
shapes are resources/css/components/groups.css. --}}
|
||||
spacing, shapes and press expansion are resources/css/components/button-group.css. --}}
|
||||
|
||||
@props([
|
||||
'connected' => false,
|
||||
@@ -71,12 +71,12 @@
|
||||
<div
|
||||
role="group"
|
||||
@if ($label) aria-label="{{ $label }}" @endif
|
||||
data-button-group="{{ $connected ? 'connected' : 'standard' }}"
|
||||
data-size="{{ $size }}"
|
||||
data-shape="{{ $shape }}"
|
||||
data-md-button-group="{{ $connected ? 'connected' : 'standard' }}"
|
||||
data-md-size="{{ $size }}"
|
||||
data-md-shape="{{ $shape }}"
|
||||
@if ($selection !== null)
|
||||
data-selection="{{ $selection }}"
|
||||
@if ($required) data-selection-required @endif
|
||||
data-md-selection="{{ $selection }}"
|
||||
@if ($required) data-md-selection-required @endif
|
||||
x-data="{
|
||||
multiple: {{ $multiple ? 'true' : 'false' }},
|
||||
required: {{ $required ? 'true' : 'false' }},
|
||||
@@ -129,13 +129,7 @@
|
||||
x-on:click="press($event)"
|
||||
@unless ($model) x-modelable="value" @endunless
|
||||
@endif
|
||||
{{ $attributes->class([
|
||||
'inline-flex items-center',
|
||||
'gap-0.5' => $connected,
|
||||
'gap-[18px]' => ! $connected && $size === 'xs',
|
||||
'gap-3' => ! $connected && $size === 'sm',
|
||||
'gap-2' => ! $connected && in_array($size, ['md', 'lg', 'xl'], true),
|
||||
]) }}
|
||||
{{ $attributes }}
|
||||
>
|
||||
{{ $slot }}
|
||||
</div>
|
||||
|
||||
@@ -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)
|
||||
<span wire:loading.flex wire:target="{{ $spinnerTarget }}" class="items-center justify-center">
|
||||
<x-livewire-material::loading :class="$iconSize.' text-current'" :label="false" />
|
||||
<span wire:loading.flex wire:target="{{ $spinnerTarget }}" data-md-button-spinner>
|
||||
<x-livewire-material::loading :label="false" />
|
||||
</span>
|
||||
@endif
|
||||
|
||||
@if ($icon)
|
||||
<span class="contents" @if ($spinnerTarget) wire:loading.remove wire:target="{{ $spinnerTarget }}" @endif>
|
||||
<x-livewire-material::icon :name="$icon" :optical="$optical" :filled="$selected === true || ($iconOnly && $selected === null)" :class="\Illuminate\Support\Arr::toCssClasses([$iconSize, 'max-medium:size-6' => $fab])" />
|
||||
<span data-md-button-icon @if ($spinnerTarget) wire:loading.remove wire:target="{{ $spinnerTarget }}" @endif>
|
||||
<x-livewire-material::icon :name="$icon" :size="$iconSize" :filled="$selected === true || ($iconOnly && $selected === null)" />
|
||||
</span>
|
||||
@endif
|
||||
|
||||
@unless ($iconOnly)
|
||||
<span @class(['max-expanded:hidden' => $responsive])>{{ $label ?? $slot }}</span>
|
||||
<span data-md-button-label @if ($responsive) data-md-responsive @endif>{{ $label ?? $slot }}</span>
|
||||
@endunless
|
||||
|
||||
@if ($iconRight)
|
||||
<x-livewire-material::icon :name="$iconRight" :optical="$optical" :class="$iconSize" />
|
||||
<x-livewire-material::icon :name="$iconRight" :size="$iconSize" />
|
||||
@endif
|
||||
|
||||
@if ($tip !== null)
|
||||
|
||||
@@ -6,8 +6,8 @@
|
||||
drawn in its container, or in the colour itself with `variant="filled"`.
|
||||
|
||||
It does not place itself: put it where the layout wants it, e.g.
|
||||
`<div class="fixed end-4 bottom-4"><x-fab icon="add" tooltip="New share" /></div>`. For a
|
||||
create action that is a FAB on a phone and a header button above, use `<x-button fab>`.
|
||||
`<div style="position: fixed; inset-inline-end: 16px; bottom: 16px"><x-fab icon="add" tooltip="New share" /></div>`.
|
||||
For a create action that is a FAB on a phone and a header button above, use `<x-button fab>`.
|
||||
|
||||
The glyph is filled, not outlined: M3 says so twice, in the FAB's anatomy and again in its
|
||||
guidelines ("icon should be filled (not outlined) and unambiguous").
|
||||
@@ -17,19 +17,19 @@
|
||||
than greying it out. A form-submit FAB that must not be pressed twice takes the caller's own
|
||||
`wire:loading.attr="disabled"`.
|
||||
|
||||
`data-fab` marks the root, so a place a FAB sits in can draw it its own way: a navigation
|
||||
`data-md-fab` marks the root, so a place a FAB sits in can draw it its own way: a navigation
|
||||
rail flattens a nested FAB to elevation 0.
|
||||
|
||||
`collapse-on-scroll` is M3's extended FAB that "can collapse to a FAB on scroll-down and
|
||||
re-expand to extended on scroll-up": while the window scrolls down it shrinks to the FAB of
|
||||
its size, and it extends again when the page scrolls back up or reaches the top. The label
|
||||
closes and fades while the width follows it on the spatial spring (resources/css/components/
|
||||
actions.css); under reduced motion the two swap outright. The label stays in the page,
|
||||
clipped rather than removed, so the collapsed FAB keeps its accessible name. It needs an
|
||||
`icon` — a FAB with no glyph is no FAB — and does nothing on a plain FAB.
|
||||
closes and fades while the width follows it on the spatial spring; under reduced motion the
|
||||
two swap outright. The label stays in the page, clipped rather than removed, so the collapsed
|
||||
FAB keeps its accessible name. It needs an `icon` — a FAB with no glyph is no FAB — and does
|
||||
nothing on a plain FAB.
|
||||
|
||||
Sizes, corners and elevation from FabBaseline/Medium/LargeTokens and ExtendedFab*Tokens
|
||||
(androidx Compose Material 3, Apache-2.0). --}}
|
||||
(androidx Compose Material 3, Apache-2.0), drawn by resources/css/components/fab.css. --}}
|
||||
|
||||
@props([
|
||||
'icon' => null,
|
||||
@@ -47,53 +47,42 @@
|
||||
@php
|
||||
$size = in_array($size, ['sm', 'md', 'lg'], true) ? $size : 'sm';
|
||||
$color = in_array($color, ['primary', 'secondary', 'tertiary'], true) ? $color : 'primary';
|
||||
$variant = $variant === 'filled' ? 'filled' : 'container';
|
||||
$extended = filled($label) || $slot->isNotEmpty();
|
||||
$collapsing = $collapseOnScroll && $extended && filled($icon);
|
||||
$isLink = filled($link);
|
||||
|
||||
$colours = $variant === 'filled'
|
||||
? ['primary' => 'bg-primary text-on-primary', 'secondary' => 'bg-secondary text-on-secondary', 'tertiary' => 'bg-tertiary text-on-tertiary'][$color]
|
||||
: ['primary' => 'bg-primary-container text-on-primary-container', 'secondary' => 'bg-secondary-container text-on-secondary-container', 'tertiary' => 'bg-tertiary-container text-on-tertiary-container'][$color];
|
||||
|
||||
$dimensions = $extended
|
||||
? ['sm' => 'h-14 min-w-20 gap-2 px-4 rounded-corner-lg type-title-md', 'md' => 'h-20 min-w-20 gap-4 px-[26px] rounded-corner-lg-increased type-title-lg', 'lg' => 'h-24 min-w-24 gap-5 px-7 rounded-corner-xl type-headline-sm'][$size]
|
||||
: ['sm' => 'size-14 rounded-corner-lg', 'md' => 'size-20 rounded-corner-lg-increased', 'lg' => 'size-24 rounded-corner-xl'][$size];
|
||||
|
||||
$iconSize = ['sm' => 'size-6', 'md' => 'size-7', 'lg' => 'size-8'][$size];
|
||||
$iconSize = ['sm' => 24, 'md' => 28, 'lg' => 32][$size];
|
||||
$anchor = $tooltip !== null ? '--material-fab-'.\Illuminate\Support\Str::lower(\Illuminate\Support\Str::random(10)) : null;
|
||||
$tag = $isLink ? 'a' : 'button';
|
||||
|
||||
$attributes = $attributes
|
||||
->class([
|
||||
'state-layer focus-ring inline-flex shrink-0 cursor-pointer select-none items-center justify-center whitespace-nowrap',
|
||||
'shadow-elevation-3 hover:shadow-elevation-4 transition-[box-shadow,background-color,color] duration-(--md-sys-motion-effects-fast-duration) ease-effects-fast',
|
||||
$dimensions,
|
||||
$colours,
|
||||
])
|
||||
->merge(array_filter([
|
||||
'href' => $isLink ? $link : null,
|
||||
'target' => $isLink && $external ? '_blank' : null,
|
||||
'rel' => $isLink && $external ? 'noopener' : null,
|
||||
'wire:navigate' => $isLink && ! $external && ! $attributes->has('wire:navigate') ? true : null,
|
||||
'type' => $isLink ? null : $type,
|
||||
// The hook a place uses to draw a nested FAB its own way: a rail flattens it to 0dp.
|
||||
'data-fab' => true,
|
||||
'x-data' => $collapsing ? 'materialFab' : null,
|
||||
'x-bind:data-collapsed' => $collapsing ? "collapsed ? '' : null" : null,
|
||||
'data-fab-collapsible' => $collapsing ? true : null,
|
||||
'data-fab-size' => $collapsing ? $size : null,
|
||||
'aria-label' => ! $extended && ! $attributes->has('aria-label') ? $tooltip : null,
|
||||
'style' => $anchor ? "anchor-name: {$anchor}" : null,
|
||||
], fn ($value): bool => $value !== null));
|
||||
$attributes = $attributes->merge(array_filter([
|
||||
// The hook a place uses to draw a nested FAB its own way: a rail flattens it to 0dp.
|
||||
'data-md-fab' => true,
|
||||
'data-md-size' => $size,
|
||||
'data-md-color' => $color,
|
||||
'data-md-variant' => $variant,
|
||||
'data-md-extended' => $extended ? true : null,
|
||||
'data-md-collapse-on-scroll' => $collapsing ? true : null,
|
||||
'href' => $isLink ? $link : null,
|
||||
'target' => $isLink && $external ? '_blank' : null,
|
||||
'rel' => $isLink && $external ? 'noopener' : null,
|
||||
'wire:navigate' => $isLink && ! $external && ! $attributes->has('wire:navigate') ? true : null,
|
||||
'type' => $isLink ? null : $type,
|
||||
'x-data' => $collapsing ? 'materialFab' : null,
|
||||
'x-bind:data-md-collapsed' => $collapsing ? "collapsed ? '' : null" : null,
|
||||
'aria-label' => ! $extended && ! $attributes->has('aria-label') ? $tooltip : null,
|
||||
'style' => $anchor ? "anchor-name: {$anchor}" : null,
|
||||
], fn ($value): bool => $value !== null));
|
||||
@endphp
|
||||
|
||||
<{{ $tag }} {{ $attributes }}>
|
||||
@if ($icon)
|
||||
<x-livewire-material::icon :name="$icon" filled :class="$iconSize" />
|
||||
<x-livewire-material::icon :name="$icon" filled :size="$iconSize" />
|
||||
@endif
|
||||
|
||||
@if ($collapsing)
|
||||
<span data-fab-label><span>{{ $label ?? $slot }}</span></span>
|
||||
<span data-md-fab-label><span>{{ $label ?? $slot }}</span></span>
|
||||
@elseif ($extended)
|
||||
<span>{{ $label ?? $slot }}</span>
|
||||
@endif
|
||||
|
||||
@@ -11,9 +11,8 @@
|
||||
The chosen segment rounds fully and takes the selected colour; `variant` is `tonal` (the
|
||||
default), `filled` or `outlined`, as for toggle buttons. The segments share the row unless
|
||||
`inline`. An option with `'disabled' => true` greys its own segment. At `xs` and `sm` a
|
||||
segment carries `touch-target` and a 48px minimum width, which M3 asks for by name and tells
|
||||
you never to reduce. Corners move on the spatial spring and colours on the effects one
|
||||
(`state-transition-fast`).
|
||||
segment keeps a 48px target and a 48px minimum width, which M3 asks for by name and tells
|
||||
you never to reduce. Corners move on the spatial spring and colours on the effects one.
|
||||
|
||||
`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),
|
||||
@@ -25,9 +24,11 @@
|
||||
the hint.
|
||||
|
||||
`hint-class` adds classes to the hint, as on `<x-field>`: a colour there paints it
|
||||
(`hint-class="text-warning"` for a hint that warns). The hint's own colour then carries no
|
||||
specificity, as the field's does in the components layer, because which of two colour
|
||||
utilities wins depends on the order Tailwind emits them. --}}
|
||||
(`hint-class="md-ink-warning"` for a hint that warns), because a caller's class outranks the
|
||||
package's layer.
|
||||
|
||||
Drawn by resources/css/components/group.css, with the connected shapes of
|
||||
resources/css/components/button-group.css. --}}
|
||||
|
||||
@props([
|
||||
'label' => null,
|
||||
@@ -52,76 +53,54 @@
|
||||
$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';
|
||||
$variant = in_array($variant, ['tonal', 'filled', 'outlined'], true) ? $variant : 'tonal';
|
||||
// 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);
|
||||
|
||||
$segment = [
|
||||
'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];
|
||||
// A 20px glyph comes from M3's 20px cut, which the icon component picks from the size.
|
||||
$iconSize = ['xs' => 20, 'sm' => 20, 'md' => 24, 'lg' => 32, 'xl' => 40][$size];
|
||||
|
||||
$hintClasses = filled($hintClass)
|
||||
? \Illuminate\Support\Arr::toCssClasses(['mt-1 type-body-sm [:where(&)]:text-on-surface-variant', $hintClass])
|
||||
: 'mt-1 type-body-sm text-on-surface-variant';
|
||||
|
||||
$iconSize = ['xs' => 'size-5', 'sm' => 'size-5', 'md' => 'size-6', 'lg' => 'size-8', 'xl' => 'size-10'][$size];
|
||||
// A 20px glyph comes from M3's 20px cut, which is drawn for that size rather than scaled to it.
|
||||
$optical = $iconSize === 'size-5' ? '20' : '24';
|
||||
|
||||
$colours = match ($variant) {
|
||||
'filled' => 'bg-surface-container text-on-surface-variant has-checked:bg-primary has-checked:text-on-primary',
|
||||
'outlined' => 'border border-outline-variant text-on-surface-variant has-checked:border-transparent has-checked:bg-inverse-surface has-checked:text-inverse-on-surface',
|
||||
default => 'bg-secondary-container text-on-secondary-container has-checked:bg-secondary has-checked:text-on-secondary',
|
||||
};
|
||||
$root = $attributes->only(['class', 'wire:key'])->merge(array_filter([
|
||||
'data-md-group' => true,
|
||||
'data-md-size' => $size,
|
||||
'data-md-variant' => $variant,
|
||||
'data-md-shape' => $shape,
|
||||
'data-md-multiple' => $multiple ? true : null,
|
||||
'data-md-inline' => $inline ? true : null,
|
||||
], fn ($value): bool => $value !== null));
|
||||
@endphp
|
||||
|
||||
<fieldset {{ $attributes->only(['class', 'wire:key'])->class('min-w-0') }}>
|
||||
<fieldset {{ $root }}>
|
||||
@if (filled($label))
|
||||
<legend class="mb-2 type-label-lg text-on-surface-variant">{{ $label }}</legend>
|
||||
<legend data-md-group-legend>{{ $label }}</legend>
|
||||
@endif
|
||||
|
||||
<div data-button-group="connected" data-size="{{ $size }}" data-shape="{{ $shape }}" @class(['flex gap-0.5', 'w-full' => ! $inline, 'w-fit' => $inline])>
|
||||
<div data-md-button-group="connected" data-md-size="{{ $size }}" data-md-shape="{{ $shape }}">
|
||||
@foreach ($options as $option)
|
||||
<label @class([
|
||||
'state-layer relative flex cursor-pointer select-none items-center justify-center whitespace-nowrap',
|
||||
'state-transition-fast',
|
||||
'flex-1' => ! $inline,
|
||||
$small ? 'touch-target min-w-12' : 'min-w-0',
|
||||
$segment,
|
||||
$colours,
|
||||
'has-focus-visible:outline-3 has-focus-visible:outline-offset-2 has-focus-visible:outline-secondary',
|
||||
'has-disabled:cursor-not-allowed has-disabled:bg-on-surface/10 has-disabled:text-on-surface/38',
|
||||
])>
|
||||
<label data-md-group-segment>
|
||||
<input
|
||||
{{ $attributes->whereStartsWith(['wire:model', 'x-model']) }}
|
||||
type="{{ $multiple ? 'checkbox' : 'radio' }}"
|
||||
name="{{ $multiple ? $name.'[]' : $name }}"
|
||||
value="{{ data_get($option, $optionValue) }}"
|
||||
@disabled(data_get($option, 'disabled'))
|
||||
class="peer sr-only"
|
||||
/>
|
||||
|
||||
@if (filled(data_get($option, $optionIcon)))
|
||||
<x-livewire-material::icon :name="data_get($option, $optionIcon)" :optical="$optical" :class="$iconSize" />
|
||||
<x-livewire-material::icon :name="data_get($option, $optionIcon)" :size="$iconSize" />
|
||||
@endif
|
||||
|
||||
<span class="truncate">{{ data_get($option, $optionLabel) }}</span>
|
||||
<span data-md-group-label>{{ data_get($option, $optionLabel) }}</span>
|
||||
</label>
|
||||
@endforeach
|
||||
</div>
|
||||
|
||||
@if ($messages !== [])
|
||||
@foreach ($messages as $message)
|
||||
<p class="mt-1 type-body-sm text-error">{{ $message }}</p>
|
||||
<p data-md-group-error>{{ $message }}</p>
|
||||
@endforeach
|
||||
@elseif (filled($hint))
|
||||
<p class="{{ $hintClasses }}">{{ $hint }}</p>
|
||||
<p data-md-group-hint @if (filled($hintClass)) class="{{ $hintClass }}" @endif>{{ $hint }}</p>
|
||||
@endif
|
||||
</fieldset>
|
||||
|
||||
@@ -1,16 +1,17 @@
|
||||
{{-- M3 Expressive's loading indicator: a shape that morphs through seven Expressive shapes while
|
||||
it turns, for a wait that has no known length.
|
||||
|
||||
In the text colour — primary unless the caller colours it — and 48px unless sized. `contained`
|
||||
puts it on a primary-container circle, for a spinner over content. It is a `progressbar`
|
||||
named `label` ("Loading" by default); pass `:label="false"` where something else already
|
||||
says what is happening, as a button's own spinner does.
|
||||
In the text colour — primary unless the caller colours it — and 48px unless the caller's CSS
|
||||
sizes it. `contained` puts it on a primary-container circle, for a spinner over content. It
|
||||
is a `progressbar` named `label` ("Loading" by default); pass `:label="false"` where something
|
||||
else already says what is happening, as a button's own spinner does.
|
||||
|
||||
Pure SVG with SMIL, so it animates in every engine without script. Under
|
||||
`prefers-reduced-motion` the shape rests. The geometry is androidx Compose Material 3's
|
||||
LoadingIndicator and Morph, ported in bin/loading-indicator.mjs (Apache-2.0):
|
||||
LoadingIndicatorTokens' 38px indicator in a 48px container, a morph every 650ms on a spring,
|
||||
a quarter turn per morph and a full turn every 4666ms. --}}
|
||||
a quarter turn per morph and a full turn every 4666ms. Drawn by
|
||||
resources/css/components/loading.css. --}}
|
||||
|
||||
@props([
|
||||
'contained' => false,
|
||||
@@ -18,26 +19,19 @@
|
||||
])
|
||||
|
||||
@php
|
||||
$sized = preg_match('/(^|\s)(size|w|h)-/', (string) $attributes->get('class')) === 1;
|
||||
$coloured = preg_match('/(^|\s)text-(?!(xs|sm|base|lg|xl|[2-9]xl|left|center|right|start|end)(\s|$))/', (string) $attributes->get('class')) === 1;
|
||||
$decorative = $label === false;
|
||||
$label = $decorative ? null : ($label ?? __('Loading'));
|
||||
|
||||
$attributes = $attributes
|
||||
->class([
|
||||
'inline-flex shrink-0 items-center justify-center',
|
||||
'size-12' => ! $sized,
|
||||
'rounded-corner-full bg-primary-container text-on-primary-container' => $contained,
|
||||
'text-primary' => ! $contained && ! $coloured,
|
||||
])
|
||||
->merge(array_filter([
|
||||
'role' => $decorative ? null : 'progressbar',
|
||||
'aria-label' => $label,
|
||||
'aria-hidden' => $decorative ? 'true' : null,
|
||||
]));
|
||||
$attributes = $attributes->merge(array_filter([
|
||||
'data-md-loading' => true,
|
||||
'data-md-contained' => $contained ? true : null,
|
||||
'role' => $decorative ? null : 'progressbar',
|
||||
'aria-label' => $label,
|
||||
'aria-hidden' => $decorative ? 'true' : null,
|
||||
], fn ($value): bool => $value !== null));
|
||||
@endphp
|
||||
|
||||
<span {{ $attributes }}>
|
||||
{{ \NoNameWeb\LivewireMaterial\Support\SvgFile::loadingIndicator(true, new \Illuminate\View\ComponentAttributeBag(['class' => 'size-full motion-reduce:hidden', 'aria-hidden' => 'true', 'focusable' => 'false'])) }}
|
||||
{{ \NoNameWeb\LivewireMaterial\Support\SvgFile::loadingIndicator(false, new \Illuminate\View\ComponentAttributeBag(['class' => 'hidden size-full motion-reduce:block', 'aria-hidden' => 'true', 'focusable' => 'false'])) }}
|
||||
{{ \NoNameWeb\LivewireMaterial\Support\SvgFile::loadingIndicator(true, new \Illuminate\View\ComponentAttributeBag(['data-md-loading-animated' => true, 'aria-hidden' => 'true', 'focusable' => 'false'])) }}
|
||||
{{ \NoNameWeb\LivewireMaterial\Support\SvgFile::loadingIndicator(false, new \Illuminate\View\ComponentAttributeBag(['data-md-loading-still' => true, 'aria-hidden' => 'true', 'focusable' => 'false'])) }}
|
||||
</span>
|
||||
|
||||
@@ -5,16 +5,16 @@
|
||||
<x-menu-item label="Download files one by one" wire:click="downloadEach" />
|
||||
</x-split-button>
|
||||
|
||||
Attributes (wire:click, spinner…) go to the leading button; the slot is the menu. `variant` is
|
||||
`filled` (the default), `tonal`, `outlined` or `elevated` — a text split button does not exist
|
||||
in M3 — with `color` and `size` as on `<x-button>`. The halves sit 2px apart; the trailing
|
||||
one rounds fully and turns its chevron over while the menu is open. `menu-label` names the
|
||||
trailing button and the menu for screen readers.
|
||||
Attributes (wire:click, spinner…) go to the leading button, a `class` to the pair; the slot is
|
||||
the menu. `variant` is `filled` (the default), `tonal`, `outlined` or `elevated` — a text split
|
||||
button does not exist in M3 — with `color` and `size` as on `<x-button>`. The halves sit 2px
|
||||
apart; the trailing one rounds fully and turns its chevron over while the menu is open.
|
||||
`menu-label` names the trailing button and the menu for screen readers.
|
||||
|
||||
Padding from SplitButton*Tokens (androidx Compose Material 3, Apache-2.0): the leading button
|
||||
keeps less room on its inner side at the two smallest sizes, and the trailing button is 48px
|
||||
wide there. resources/css/components/groups.css carries the rest of the spec, which is all
|
||||
geometry: the inner corners (4/4/4/8/12px, growing under the finger, not shrinking as a
|
||||
wide there. resources/css/components/split-button.css carries the rest of the spec, which is
|
||||
all geometry: the inner corners (4/4/4/8/12px, growing under the finger, not shrinking as a
|
||||
connected group's do), the trailing icon's own size (22/22/26/38/50px, which the icon-button
|
||||
table has no entry for), the 1/1/2/3/6px nudge that sits the chevron towards the leading half,
|
||||
and the standard motion scheme M3 asks for while it turns over. --}}
|
||||
@@ -35,12 +35,9 @@
|
||||
$variant = in_array($variant, ['filled', 'tonal', 'outlined', 'elevated'], true) ? $variant : 'filled';
|
||||
$size = in_array($size, ['xs', 'sm', 'md', 'lg', 'xl'], true) ? $size : 'sm';
|
||||
$menuLabel ??= __('More options');
|
||||
|
||||
$leadingPadding = ['xs' => 'pe-2.5', 'sm' => 'pe-3', 'md' => '', 'lg' => '', 'xl' => ''][$size];
|
||||
$trailingWidth = ['xs' => 'w-12', 'sm' => 'w-12', 'md' => '', 'lg' => '', 'xl' => ''][$size];
|
||||
@endphp
|
||||
|
||||
<div data-button-group="split" data-size="{{ $size }}" {{ $attributes->only('class')->class('inline-flex items-center gap-0.5') }}>
|
||||
<div data-md-split-button data-md-size="{{ $size }}" {{ $attributes->only('class') }}>
|
||||
<x-livewire-material::button
|
||||
{{ $attributes->except('class') }}
|
||||
:label="$label"
|
||||
@@ -50,9 +47,7 @@
|
||||
:size="$size"
|
||||
:disabled="$disabled"
|
||||
:spinner="$spinner"
|
||||
corners=""
|
||||
data-split="leading"
|
||||
:class="$leadingPadding"
|
||||
data-md-split="leading"
|
||||
/>
|
||||
|
||||
<x-livewire-material::menu :position="$position" :label="$menuLabel">
|
||||
@@ -64,9 +59,7 @@
|
||||
:color="$color"
|
||||
:size="$size"
|
||||
:disabled="$disabled"
|
||||
corners=""
|
||||
data-split="trailing"
|
||||
:class="$trailingWidth"
|
||||
data-md-split="trailing"
|
||||
/>
|
||||
</x-slot:trigger>
|
||||
|
||||
|
||||
@@ -84,7 +84,7 @@
|
||||
<button
|
||||
type="button"
|
||||
x-data
|
||||
data-icon-button
|
||||
data-md-icon-button
|
||||
data-theme-toggle="{{ $mode }}"
|
||||
@if ($mode === 'cycle')
|
||||
aria-label="{{ __('Theme') }}"
|
||||
|
||||
@@ -9,8 +9,9 @@
|
||||
at once on a press or Escape; leaving or blurring lets it stand for M3's 1.5s. Only one
|
||||
tooltip is on screen at a time, as M3 asks. A phone has no hover, so a control there carries its
|
||||
words. The bubble is a `popover="manual"` in the top layer — never clipped by an
|
||||
`overflow-hidden` parent, never widening a scroll container — placed by CSS anchor
|
||||
`overflow: hidden` parent, never widening a scroll container — placed by CSS anchor
|
||||
positioning on `side` (`top`, `bottom`, `left`, `right`), flipping when there is no room.
|
||||
Drawn by resources/css/components/tooltip.css.
|
||||
|
||||
It is aria-hidden: an icon button takes the same words as its aria-label, and a labelled
|
||||
control would otherwise read them twice. --}}
|
||||
@@ -28,7 +29,7 @@
|
||||
@endphp
|
||||
|
||||
@if ($standalone)
|
||||
<span class="inline-flex" style="anchor-name: {{ $anchor }}">
|
||||
<span data-md-tooltip-anchor style="anchor-name: {{ $anchor }}">
|
||||
{{ $slot }}
|
||||
@endif
|
||||
|
||||
@@ -36,15 +37,9 @@
|
||||
popover="manual"
|
||||
aria-hidden="true"
|
||||
x-data="materialTooltip"
|
||||
data-md-tooltip
|
||||
data-md-side="{{ $side }}"
|
||||
style="position-anchor: {{ $anchor }}"
|
||||
@class([
|
||||
'pointer-events-none m-0 max-w-50 overflow-visible border-0 rounded-corner-xs bg-inverse-surface px-2 py-1 text-center whitespace-normal type-body-sm text-inverse-on-surface [inset:auto]',
|
||||
'opacity-0 transition-[opacity,display,overlay] transition-discrete duration-(--md-sys-motion-effects-fast-duration) ease-effects-fast open:opacity-100 starting:open:opacity-0',
|
||||
'my-1 [position-area:top] [position-try-fallbacks:flip-block]' => $side === 'top',
|
||||
'my-1 [position-area:bottom] [position-try-fallbacks:flip-block]' => $side === 'bottom',
|
||||
'mx-1 [position-area:left] [position-try-fallbacks:flip-inline]' => $side === 'left',
|
||||
'mx-1 [position-area:right] [position-try-fallbacks:flip-inline]' => $side === 'right',
|
||||
])
|
||||
>{{ $text }}</span>
|
||||
|
||||
@if ($standalone)
|
||||
|
||||
Reference in New Issue
Block a user