Take the icon off the snackbar and lift the FAB clear of it

M3 says to avoid an icon in a snackbar and gives the supporting text no fourth
colour, so the state glyph and the 80% description are gone: a type now only
picks the announcement role. The 40px action and close buttons take the shared
touch-target, Escape dismisses a snackbar that holds the focus, and the host
publishes --material-snackbar-height on <html> so a `fab` button sits above the
snackbar instead of under it. Plan step 18, actions.md ACT-17, ACT-18, ACT-20,
ACT-34, ACT-35.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qwx5USif3wFFmxtHg5U1g9
This commit is contained in:
Andreas Reinhold
2026-09-14 06:09:47 +02:00
co-authored by Claude Opus 5
parent eac7f6a1de
commit 7bc4f740e7
7 changed files with 76 additions and 33 deletions
+14 -21
View File
@@ -28,12 +28,19 @@
`@persist` keeps the host across wire:navigate, so a toast dispatched with `redirectTo` is
still on screen when the next page arrives.
There is no state icon: M3 says to avoid one in a snackbar ("use a dialog instead if an icon
feels necessary"), and both lines of the message are plain inverse-on-surface — M3 gives the
supporting text no fourth colour and tells the two lines apart by position. The 40px action
and close buttons carry `touch-target`, which reaches M3's 48px without growing the container.
Escape dismisses a snackbar that holds the focus.
M3's snackbar (SnackbarTokens, androidx Compose Material 3, Apache-2.0): inverse surface,
body-medium text, a label-large action in inverse-primary, extra-small corners, elevation 3,
48px for one line. A type draws its state icon in the inverse state colour. `position`:
`bottom` (centred, the default) or `bottom-start`. It lifts above a bottom bar through
`--material-bottom-bar`. A compact window (below `medium`, 600px) gets the full-width snackbar;
from `medium` it hugs its line length instead, as M3 asks. --}}
48px for one line. `position`: `bottom` (centred, the default) or `bottom-start`. It lifts
above a bottom bar through `--material-bottom-bar`, and publishes its own height as
`--material-snackbar-height` so a FAB can lift clear of it — M3: a snackbar appears above a
FAB, never in front of or behind one. A compact window (below `medium`, 600px) gets the
full-width snackbar; from `medium` it hugs its line length instead, as M3 asks. --}}
@props(['position' => 'bottom'])
@@ -60,31 +67,17 @@
x-on:focusout="resume()"
class="pointer-events-auto flex min-h-12 w-full max-w-[min(100%,36rem)] items-center gap-3 rounded-corner-xs bg-inverse-surface py-1.5 ps-4 pe-2 text-inverse-on-surface shadow-elevation-3 transition-[translate,opacity] duration-(--md-sys-motion-spatial-fast-duration) ease-spatial-fast starting:translate-y-4 starting:opacity-0 medium:w-auto medium:min-w-86"
>
<template x-if="icon(current.type)">
<span class="flex shrink-0" x-bind:class="{
'text-inverse-success': current.type === 'success',
'text-inverse-error': current.type === 'error',
'text-inverse-warning': current.type === 'warning',
'text-inverse-info': current.type === 'info',
}">
<span x-show="current.type === 'success'"><x-livewire-material::icon name="check_circle" filled class="size-6" /></span>
<span x-show="current.type === 'error'"><x-livewire-material::icon name="error" filled class="size-6" /></span>
<span x-show="current.type === 'warning'"><x-livewire-material::icon name="warning" filled class="size-6" /></span>
<span x-show="current.type === 'info'"><x-livewire-material::icon name="info" filled class="size-6" /></span>
</span>
</template>
<div class="min-w-0 flex-1 py-1.5">
<p class="type-body-md" x-text="current.title"></p>
<p class="type-body-md opacity-80" x-show="current.description" x-text="current.description"></p>
<p class="type-body-md" x-show="current.description" x-text="current.description"></p>
</div>
<template x-if="current.action">
<button type="button" data-toast-action class="state-layer focus-ring h-10 shrink-0 rounded-corner-full px-3 type-label-lg text-inverse-primary" x-text="current.action.label" x-on:click="act()"></button>
<button type="button" data-toast-action class="state-layer focus-ring touch-target h-10 shrink-0 rounded-corner-full px-3 type-label-lg text-inverse-primary" x-text="current.action.label" x-on:click="act()"></button>
</template>
<template x-if="current.action || ! current.timeout">
<button type="button" class="state-layer focus-ring inline-flex size-10 shrink-0 items-center justify-center rounded-corner-full" aria-label="{{ __('Dismiss') }}" x-on:click="dismiss()">
<button type="button" class="state-layer focus-ring touch-target inline-flex size-10 shrink-0 items-center justify-center rounded-corner-full" aria-label="{{ __('Dismiss') }}" x-on:click="dismiss()">
<x-livewire-material::icon name="close" optical="20" class="size-5" />
</button>
</template>