Add M3 error pages and the Markdown mail theme
tests / feature (8.5) (push) Successful in 1m8s
tests / browser (safari, webkit) (push) Has been cancelled
tests / browser (firefox, firefox) (push) Has been cancelled
tests / lint (push) Successful in 1m5s
tests / feature (8.4) (push) Successful in 1m11s
tests / browser (chrome, chromium) (push) Failing after 6m9s

Error pages for 401 to 503 in an error-view root appended to view.paths,
with an inline fallback when the Vite build is missing, and a mail theme
rendered from the application's scheme JSON, with opt-in header and
message components. Completes Phase 9.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01V9NnLxnPp8vaaurb3Z1MFy
This commit is contained in:
Andreas Reinhold / reini
2026-09-13 08:55:58 +02:00
co-authored by Claude Opus 5
parent ab692b66bb
commit 8f174520eb
29 changed files with 1595 additions and 2 deletions
@@ -0,0 +1,10 @@
{{-- Forbidden. A message the app passed along (`abort(403, '…')`, a policy's `Response::deny('')`)
is the sentence, as in the framework's own page; otherwise a general one. --}}
@extends('errors::minimal')
@section('title', __('Forbidden'))
@section('code', '403')
@section('shape', 'gem')
@section('headline', __('You dont have access'))
@section('message', isset($exception) && $exception->getMessage() !== '' ? __($exception->getMessage()) : __('Your account isnt allowed to open this page.'))
@@ -0,0 +1,9 @@
{{-- Not found. --}}
@extends('errors::minimal')
@section('title', __('Not Found'))
@section('code', '404')
@section('shape', 'cookie-9')
@section('headline', __('Page not found'))
@section('message', __('The page youre looking for doesnt exist or has moved.'))
@@ -0,0 +1,16 @@
{{-- Page expired: the form's CSRF token outlived the session. Reloading the page the form was on
issues a new one, so that is the action — a link to it, never a reload of this response,
which would post the stale form again. --}}
@extends('errors::minimal')
@section('title', __('Page Expired'))
@section('code', '419')
@section('shape', 'clover-4')
@section('headline', __('This page has expired'))
@section('message', __('It was open for a while. Refresh it, then try again.'))
@section('actions')
<x-button :link="url()->previous()" :label="__('Refresh the page')" variant="filled" size="md" no-wire-navigate />
<x-button :link="url('/')" :label="__('Go home')" variant="text" size="md" no-wire-navigate />
@endsection
@@ -0,0 +1,9 @@
{{-- Too many requests. --}}
@extends('errors::minimal')
@section('title', __('Too Many Requests'))
@section('code', '429')
@section('shape', 'cookie-12')
@section('headline', __('Slow down a little'))
@section('message', __('There were too many requests in a short time. Wait a moment, then try again.'))
@@ -0,0 +1,9 @@
{{-- Server error. Nothing from the exception is shown: its message is for the log, not the visitor. --}}
@extends('errors::minimal')
@section('title', __('Server Error'))
@section('code', '500')
@section('shape', 'soft-burst')
@section('headline', __('Something went wrong'))
@section('message', __('An error on our side stopped this page from loading. Please try again in a moment.'))
@@ -0,0 +1,22 @@
{{-- Service unavailable, usually maintenance mode. A message the app passed (`abort(503, '…')`) is
the sentence; maintenance mode's own "Service Unavailable" is not a message, so it is not.
`php artisan down --render="errors::503"` renders this once, in the console, with no
`$exception` and no request — so the one action reloads whatever address the visitor is
on, in the browser, rather than a URL decided here. --}}
@extends('errors::minimal')
@php
$reason = isset($exception) ? $exception->getMessage() : '';
@endphp
@section('title', __('Service Unavailable'))
@section('code', '503')
@section('shape', 'puffy')
@section('headline', __('Well be right back'))
@section('message', $reason !== '' && $reason !== 'Service Unavailable' ? __($reason) : __('Were making some improvements. Please check back soon.'))
@section('actions')
<x-button :label="__('Try again')" variant="filled" size="md" onclick="location.reload()" />
@endsection
@@ -0,0 +1,83 @@
{{-- The layout every error page extends: this package's 403, 404, 419, 429, 500 and 503, and the
framework's own 401 and 402, which extend `errors::minimal` and find this file before the
framework's (the provider appends this folder's parent to `view.paths`).
Sections the framework layout's `title`, `code` and `message`, plus three of its own, so
either layout renders the other's pages:
`title` the browser tab, followed by the app name;
`code` the status, drawn in display type over the shape;
`headline` what happened, in a few words (without it, `message` is the headline);
`message` one sentence on what to do about it;
`shape` an M3 Expressive shape name (`cookie-7` by default), in primary-container;
`actions` the buttons; by default a filled Home and, when the visitor came from a page on
the way here, a text Back.
The app's own Vite entries (`livewire-material.showcase.vite`) bring its scheme, font and
utilities. But an error page is also what shows while a deploy has no build yet, so when
those tags cannot be made the page brings a small stylesheet of its own: the app's scheme
from its scheme data, drawn onto the `data-error-*` hooks. Keep the hooks when changing
the markup. The shape turns once a minute, unless the visitor asks for reduced motion. --}}
@php
$assets = \NoNameWeb\LivewireMaterial\Support\ErrorPage::assets();
$back = \NoNameWeb\LivewireMaterial\Support\ErrorPage::backUrl();
@endphp
<!DOCTYPE html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="robots" content="noindex" />
<title>@yield('title') · {{ config('app.name') }}</title>
<x-theme-script />
@if ($assets !== null)
{{ $assets }}
@else
<style data-error-fallback>{{ \NoNameWeb\LivewireMaterial\Support\ErrorPage::fallbackStyles() }}</style>
@endif
<style>
@keyframes material-error-turn { to { transform: rotate(1turn); } }
@media (prefers-reduced-motion: no-preference) { [data-error-shape] { animation: material-error-turn 60s linear infinite; } }
</style>
</head>
<body class="min-h-dvh bg-surface font-sans text-on-surface antialiased">
<main data-error-page class="mx-auto flex min-h-dvh max-w-xl flex-col items-center justify-center px-6 py-12 text-center">
<div data-error-art class="relative grid size-48 shrink-0 place-items-center sm:size-60">
<div data-error-shape class="absolute inset-0">
<x-shape :name="trim($__env->yieldContent('shape', 'cookie-7'))" class="size-full text-primary-container" />
</div>
<p data-error-code class="relative type-emphasized-display-lg text-on-primary-container tabular-nums">@yield('code')</p>
</div>
<h1 data-error-headline class="mt-10 type-headline-md text-balance sm:type-headline-lg">
@hasSection('headline')
@yield('headline')
@else
@yield('message')
@endif
</h1>
@hasSection('headline')
<p data-error-message class="mt-3 max-w-md type-body-lg text-balance text-on-surface-variant">@yield('message')</p>
@endif
<div data-error-actions class="mt-10 flex flex-wrap items-center justify-center gap-3">
@hasSection('actions')
@yield('actions')
@else
<x-button :link="url('/')" :label="__('Go home')" variant="filled" size="md" no-wire-navigate />
@if ($back !== null)
<x-button :link="$back" :label="__('Go back')" variant="text" size="md" no-wire-navigate />
@endif
@endif
</div>
</main>
</body>
</html>
@@ -0,0 +1,24 @@
{{-- The mail's masthead: the app name in title-lg, or the logo from
`livewire-material.mail.logo` (`src`, `width`, `height`).
Replaces the framework's header, which swaps the name for Laravel's own logo whenever the
app is still called "Laravel". A logo keeps the name as its alt text, for readers with
images off; its width and height are repeated as attributes because Outlook sizes an image
from those and ignores the stylesheet. Serve the file at twice those dimensions to stay
crisp, from an absolute URL — a queued mail has no request to resolve a relative one. --}}
@props(['url', 'logo' => config('livewire-material.mail.logo')])
@php
$name = trim(strip_tags((string) $slot));
$src = is_array($logo) ? ($logo['src'] ?? null) : null;
@endphp
<tr>
<td class="header">
<a href="{{ $url }}" style="display: inline-block;">
@if (filled($src))
<img src="{{ $src }}" class="logo" alt="{{ $name }}" @if (filled($logo['width'] ?? null)) width="{{ $logo['width'] }}" @endif @if (filled($logo['height'] ?? null)) height="{{ $logo['height'] }}" @endif style="border: 0;">
@else
{{ $name }}
@endif
</a>
</td>
</tr>
@@ -0,0 +1,35 @@
{{-- Every Markdown mail passes through here. The framework's message, with the footer open to
the mail: a `<x-slot:footer>` inside `<x-mail::message>` replaces the default line — for the
sender's address, a privacy link, or an unsubscribe link on mail that is not transactional.
Keep the Markdown below unindented: four spaces make a code block. --}}
<x-mail::layout>
{{-- Header --}}
<x-slot:header>
<x-mail::header :url="config('app.url')">
{{ config('app.name') }}
</x-mail::header>
</x-slot:header>
{{-- Body --}}
{!! $slot !!}
{{-- Subcopy --}}
@isset($subcopy)
<x-slot:subcopy>
<x-mail::subcopy>
{!! $subcopy !!}
</x-mail::subcopy>
</x-slot:subcopy>
@endisset
{{-- Footer --}}
<x-slot:footer>
<x-mail::footer>
@isset($footer)
{!! $footer !!}
@else
© {{ date('Y') }} {{ config('app.name') }}. {{ __('All rights reserved.') }}
@endisset
</x-mail::footer>
</x-slot:footer>
</x-mail::layout>
@@ -0,0 +1,32 @@
{{-- The plain-text twin of html/message.blade.php: the same footer slot, no markup. --}}
<x-mail::layout>
{{-- Header --}}
<x-slot:header>
<x-mail::header :url="config('app.url')">
{{ config('app.name') }}
</x-mail::header>
</x-slot:header>
{{-- Body --}}
{{ $slot }}
{{-- Subcopy --}}
@isset($subcopy)
<x-slot:subcopy>
<x-mail::subcopy>
{{ $subcopy }}
</x-mail::subcopy>
</x-slot:subcopy>
@endisset
{{-- Footer --}}
<x-slot:footer>
<x-mail::footer>
@isset($footer)
{{ $footer }}
@else
© {{ date('Y') }} {{ config('app.name') }}. @lang('All rights reserved.')
@endisset
</x-mail::footer>
</x-slot:footer>
</x-mail::layout>
+426
View File
@@ -0,0 +1,426 @@
{{-- The Markdown mail theme, as CSS rendered from the application's colour scheme.
Select it with `mail.markdown.theme` = `livewire-material::mail.theme` (MAIL_MARKDOWN_THEME),
or `$theme` / `->theme()` on one mailable or MailMessage. Laravel renders a namespaced theme
as a view (Illuminate\Mail\Markdown::render) and hands the result to CssToInlineStyles, which
writes it onto every element's `style` so the colours are the app's light scheme, read
from `livewire-material.scheme` (Support\Scheme, falling back to the package's default), and
a regenerated scheme reaches the next mail without a copy to keep in step.
What a mail client can take, and so what this is:
- Hexes only. No custom properties, no `color-mix()`, no alpha: a client resolves none of
them, and Outlook drops an alpha channel.
- Light only. `Css\Processor::doCleanup()` strips every `@media` block from the theme before
inlining, so a `prefers-color-scheme` rule here would be deleted silently; it could only
live in a `<style>` in a published layout, and the framework layout declares
`color-scheme: light` in two meta tags besides. Clients that force dark invert the light
tones, which are mid-tone enough to survive it.
- M3's typescale on the bare tags CommonMark writes (`h1` headline-sm, `p` body-lg, a table
head title-sm), in px, with a system font stack: web fonts and
`font-variation-settings` reach too few clients to be worth an asset.
- No elevation. The card separates from the page by tone, as M3 surfaces do.
- The button is M3's filled button at the `md` size (56px, title-md label, full corners),
written as borders because padding on an `<a>` is not honoured everywhere. Outlook
Classic squares the corners; it is still a button. `x-mail::button`'s `color` takes any
role with a filled pair — primary, secondary, tertiary, error, success, warning, info —
and the framework's blue, green and red. --}}
@php
$role = \NoNameWeb\LivewireMaterial\Support\Scheme::light();
$page = $role['surface-container'];
$card = $role['surface-container-lowest'];
$tint = $role['surface-container'];
$buttons = [
'primary' => ['primary', 'on-primary'],
'secondary' => ['secondary', 'on-secondary'],
'tertiary' => ['tertiary', 'on-tertiary'],
'error' => ['error', 'on-error'],
'success' => ['success', 'on-success'],
'warning' => ['warning', 'on-warning'],
'info' => ['info', 'on-info'],
'blue' => ['primary', 'on-primary'],
'green' => ['success', 'on-success'],
'red' => ['error', 'on-error'],
];
@endphp
/* Base */
body,
body *:not(html):not(style):not(br):not(tr):not(code) {
box-sizing: border-box;
font-family: -apple-system, BlinkMacSystemFont, Roboto, 'Segoe UI', Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol';
position: relative;
}
body {
-webkit-text-size-adjust: none;
background-color: {{ $page }};
color: {{ $role['on-surface-variant'] }};
height: 100%;
line-height: 1.5;
margin: 0;
padding: 0;
width: 100% !important;
}
p,
ul,
ol,
blockquote {
line-height: 1.5;
text-align: start;
}
a {
color: {{ $role['primary'] }};
}
a img {
border: none;
}
/* Typography: M3's typescale */
h1 {
color: {{ $role['on-surface'] }};
font-size: 24px;
font-weight: 400;
letter-spacing: 0;
line-height: 32px;
margin-top: 0;
text-align: start;
}
h2 {
color: {{ $role['on-surface'] }};
font-size: 22px;
font-weight: 400;
letter-spacing: 0;
line-height: 28px;
margin-top: 0;
text-align: start;
}
h3 {
color: {{ $role['on-surface'] }};
font-size: 16px;
font-weight: 500;
letter-spacing: 0.15px;
line-height: 24px;
margin-top: 0;
text-align: start;
}
p {
color: {{ $role['on-surface-variant'] }};
font-size: 16px;
font-weight: 400;
letter-spacing: 0.5px;
line-height: 24px;
margin-top: 0;
text-align: start;
}
ul,
ol {
color: {{ $role['on-surface-variant'] }};
font-size: 16px;
font-weight: 400;
letter-spacing: 0.5px;
line-height: 24px;
margin: 0 0 16px;
padding-left: 24px;
}
li {
margin: 0 0 4px;
}
/* A `->line()` per item makes a loose list, each item wrapped in a paragraph with a
paragraph's margin; without this it reads as numbered paragraphs, not one list. */
li p {
margin: 0;
}
/* Bold stays bold: a 500 would fall to 400 in the many system fonts without a medium weight. */
strong {
color: {{ $role['on-surface'] }};
}
blockquote {
border-left: 4px solid {{ $role['outline-variant'] }};
margin: 0 0 16px;
padding-left: 16px;
}
code {
background-color: {{ $tint }};
border-radius: 4px;
color: {{ $role['on-surface'] }};
font-size: 14px;
padding: 2px 4px;
}
hr {
border: 0;
border-top: 1px solid {{ $role['outline-variant'] }};
margin: 24px 0;
}
p.sub {
font-size: 14px;
letter-spacing: 0.25px;
line-height: 20px;
}
img {
max-width: 100%;
}
/* Layout */
.wrapper {
-premailer-cellpadding: 0;
-premailer-cellspacing: 0;
-premailer-width: 100%;
background-color: {{ $page }};
margin: 0;
padding: 0;
width: 100%;
}
.content {
-premailer-cellpadding: 0;
-premailer-cellspacing: 0;
-premailer-width: 100%;
margin: 0;
padding: 0;
width: 100%;
}
/* Header: the app name in title-lg, or the configured logo */
.header {
padding: 32px 0 24px;
text-align: center;
}
.header a {
color: {{ $role['on-surface'] }};
font-size: 22px;
font-weight: 500;
letter-spacing: 0;
line-height: 28px;
text-decoration: none;
}
.logo {
border: 0;
display: block;
height: auto;
margin: 0 auto;
}
/* Body: the card, separated from the page by tone alone */
.body {
-premailer-cellpadding: 0;
-premailer-cellspacing: 0;
-premailer-width: 100%;
background-color: {{ $page }};
border: 0;
margin: 0;
padding: 0;
width: 100%;
}
.inner-body {
-premailer-cellpadding: 0;
-premailer-cellspacing: 0;
-premailer-width: 570px;
background-color: {{ $card }};
border: 0;
border-radius: 24px;
margin: 0 auto;
padding: 0;
width: 570px;
}
/* A long URL breaks where it has to; `break-all`, the framework's value, breaks ordinary words too. */
.inner-body a {
overflow-wrap: anywhere;
}
.content-cell {
max-width: 100vw;
padding: 32px;
}
/* Subcopy */
.subcopy {
border-top: 1px solid {{ $role['outline-variant'] }};
margin-top: 24px;
padding-top: 24px;
}
.subcopy p {
font-size: 14px;
letter-spacing: 0.25px;
line-height: 20px;
}
/* Footer: body-sm */
.footer {
-premailer-cellpadding: 0;
-premailer-cellspacing: 0;
-premailer-width: 570px;
margin: 0 auto;
padding: 0;
text-align: center;
width: 570px;
}
.footer p {
color: {{ $role['on-surface-variant'] }};
font-size: 12px;
font-weight: 400;
letter-spacing: 0.4px;
line-height: 16px;
text-align: center;
}
.footer a {
color: {{ $role['on-surface-variant'] }};
text-decoration: underline;
}
/* Tables: a title-sm head over an outline-variant rule, body-md cells, a rule between rows */
.table table {
-premailer-cellpadding: 0;
-premailer-cellspacing: 0;
-premailer-width: 100%;
border-collapse: collapse;
margin: 24px auto;
width: 100%;
}
.table th {
border-bottom: 1px solid {{ $role['outline-variant'] }};
color: {{ $role['on-surface-variant'] }};
font-size: 14px;
font-weight: 500;
letter-spacing: 0.1px;
line-height: 20px;
margin: 0;
padding: 0 12px 12px;
text-align: start;
}
/* A Markdown table's column alignment arrives as an `align` attribute, which an inline
`text-align` would override; the head follows it here as the cells do by themselves. */
.table th[align="center"] {
text-align: center;
}
.table th[align="right"] {
text-align: right;
}
.table td {
border-bottom: 1px solid {{ $role['outline-variant'] }};
color: {{ $role['on-surface'] }};
font-size: 14px;
font-weight: 400;
font-variant-numeric: tabular-nums;
letter-spacing: 0.25px;
line-height: 20px;
margin: 0;
padding: 12px;
}
.table tr:last-child td {
border-bottom: 0;
}
/* Buttons: M3's filled button, md */
.action {
-premailer-cellpadding: 0;
-premailer-cellspacing: 0;
-premailer-width: 100%;
float: unset;
margin: 32px auto;
padding: 0;
text-align: center;
width: 100%;
}
.button {
-webkit-text-size-adjust: none;
border-radius: 9999px;
display: inline-block;
font-size: 16px;
font-weight: 500;
letter-spacing: 0.15px;
line-height: 24px;
overflow: hidden;
text-decoration: none;
}
@foreach ($buttons as $name => [$container, $content])
.button-{{ $name }} {
background-color: {{ $role[$container] }};
border-bottom: 16px solid {{ $role[$container] }};
border-left: 24px solid {{ $role[$container] }};
border-right: 24px solid {{ $role[$container] }};
border-top: 16px solid {{ $role[$container] }};
color: {{ $role[$content] }};
}
@endforeach
/* Panel: a tinted container, no rule down its edge */
.panel {
-premailer-cellpadding: 0;
-premailer-cellspacing: 0;
-premailer-width: 100%;
border-collapse: separate;
margin: 24px 0;
width: 100%;
}
.panel-content {
background-color: {{ $tint }};
border-radius: 16px;
color: {{ $role['on-surface'] }};
padding: 16px 20px;
}
.panel-content p {
color: {{ $role['on-surface'] }};
}
.panel-item {
padding: 0;
}
.panel-item p:last-of-type {
margin-bottom: 0;
padding-bottom: 0;
}
/* Utilities */
.break-all {
overflow-wrap: anywhere;
}
+1
View File
@@ -26,5 +26,6 @@
@include('livewire-material::showcase.sections.bars')
@include('livewire-material::showcase.sections.navigation')
@include('livewire-material::showcase.sections.data')
@include('livewire-material::showcase.sections.pages')
</main>
@endsection
+1 -1
View File
@@ -18,7 +18,7 @@
<a href="{{ route('livewire-material.showcase') }}" class="shrink-0 type-title-lg max-sm:hidden">Livewire Material</a>
<nav class="-my-2 flex min-w-0 flex-1 gap-x-4 overflow-x-auto py-2 whitespace-nowrap type-label-lg text-on-surface-variant [scrollbar-width:none]" aria-label="Sections">
@foreach (['colour' => 'Colour', 'type' => 'Type', 'shape' => 'Shape', 'elevation' => 'Elevation', 'motion' => 'Motion', 'icons' => 'Icons', 'buttons' => 'Buttons', 'menus' => 'Menus', 'communication' => 'Communication', 'progress' => 'Progress', 'containment' => 'Containment', 'carousel' => 'Carousel', 'fields' => 'Fields', 'chips' => 'Chips', 'sliders' => 'Sliders', 'pickers' => 'Date pickers', 'timepickers' => 'Time pickers', 'bars' => 'Bars', 'navigation' => 'Navigation', 'data' => 'Data'] as $anchor => $section)
@foreach (['colour' => 'Colour', 'type' => 'Type', 'shape' => 'Shape', 'elevation' => 'Elevation', 'motion' => 'Motion', 'icons' => 'Icons', 'buttons' => 'Buttons', 'menus' => 'Menus', 'communication' => 'Communication', 'progress' => 'Progress', 'containment' => 'Containment', 'carousel' => 'Carousel', 'fields' => 'Fields', 'chips' => 'Chips', 'sliders' => 'Sliders', 'pickers' => 'Date pickers', 'timepickers' => 'Time pickers', 'bars' => 'Bars', 'navigation' => 'Navigation', 'data' => 'Data', 'pages' => 'Pages'] as $anchor => $section)
<a href="#{{ $anchor }}" class="rounded-corner-xs hover:text-on-surface focus-ring">{{ $section }}</a>
@endforeach
</nav>
+40
View File
@@ -0,0 +1,40 @@
{{-- The showcase's sample Markdown mail: every part the theme styles — heading, prose, bold,
a list, the button, a panel, a table, subcopy and the footer slot. Markdown, so it stays
unindented. --}}
<x-mail::message>
# Your export is ready
Hello Anna, the report you asked for on **Monday** has finished. It holds three files and stays available for seven days.
<x-mail::button :url="url('/')">
Download the export
</x-mail::button>
<x-mail::panel>
Files are deleted automatically once they expire. Download them before **20 September** to keep a copy.
</x-mail::panel>
<x-mail::table>
| File | Rows | Size |
|:-----|-----:|-----:|
| customers.csv | 1,204 | 184 KB |
| orders.csv | 8,930 | 1.2 MB |
| summary.pdf | — | 96 KB |
</x-mail::table>
What happens next:
1. Open the export from the button above.
2. Check the summary before you share it.
Thanks,<br>
{{ config('app.name') }}
<x-slot:subcopy>
If the button does not work, paste this address into your browser: [{{ url('/') }}]({{ url('/') }})
</x-slot:subcopy>
<x-slot:footer>
© {{ date('Y') }} {{ config('app.name') }} · [Privacy]({{ url('/') }}) · [Unsubscribe]({{ url('/') }})
</x-slot:footer>
</x-mail::message>
@@ -0,0 +1,16 @@
<section id="pages" class="scroll-mt-24 space-y-6">
<h2 class="type-headline-md">Pages</h2>
<p class="max-w-3xl type-body-md text-on-surface-variant">
The error pages, raised for real so an app's own <code>resources/views/errors</code> still wins, and the Markdown mail theme
(<code>livewire-material::mail.theme</code>) on a sample mail, both in this application's scheme.
</p>
<div class="flex flex-wrap items-center gap-2">
@foreach (['401', '402', '403', '404', '419', '429', '500', '503'] as $code)
<x-button :link="route('livewire-material.error', $code)" :label="$code" variant="tonal" no-wire-navigate />
@endforeach
<x-button :link="route('livewire-material.mail')" label="Sample mail" icon="mail" variant="outlined" no-wire-navigate />
</div>
</section>