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
@@ -7,4 +7,5 @@ This application uses `nonameweb/livewire-material`: Material 3 Expressive compo
- Never write maryUI tags (`<x-mary-*>`) or daisyUI classes (`btn`, `card`, `badge`, `bg-base-200`, `text-base-content`). They compile to nothing and fail silently.
- Every layout includes `<x-theme-script />` in `<head>` before `@vite`. The colour scheme is generated with `php artisan material:scheme` never edit `resources/css/material-scheme.css` by hand.
- While the application runs locally, every token and component renders in the application's own scheme at `/material` (the showcase).
- HTTP error pages and the Markdown mail theme come from the package. Change error wording by publishing `--tag=livewire-material-errors`; select the mail theme with `MAIL_MARKDOWN_THEME=livewire-material::mail.theme`.
@endverbatim
@@ -85,6 +85,59 @@ class Settings extends Component
The methods are protected. They dispatch a `toast` browser event (`assertDispatched('toast', type: 'success', title: 'Settings saved')` in tests).
## Error pages
Laravel's HTTP error pages — 403, 404, 419, 429, 500, 503, and the framework's own 401 and 402 — render in M3 without setup. The provider appends the package's error views to `view.paths` after the application's, so a file in `resources/views/errors/` always wins.
- The pages load `config('livewire-material.showcase.vite')` and `<x-theme-script />`, so they use the app's scheme, font and theme. While the build is missing (a deploy in progress) they fall back to an inline stylesheet coloured from `resources/css/material-scheme.json`.
- `abort(403, 'Only the owner can open this share.')` and `abort(503, '…')` show the message as the sentence. Every other string goes through `__()`; translate them in `lang/{locale}.json`.
- To change wording or design, run `php artisan vendor:publish --tag=livewire-material-errors`, which copies the layout and pages to `resources/views/errors`. A page extends `errors::minimal` and sets `title`, `code`, `headline`, `message`, `shape` (an `<x-shape>` name) and optionally `actions`:
```blade
@extends('errors::minimal')
@section('title', __('Payment Required'))
@section('code', '402')
@section('shape', 'cookie-4')
@section('headline', __('Your plan has ended'))
@section('message', __('Choose a plan to keep using the app.'))
@section('actions')
<x-button :link="route('billing')" :label="__('Choose a plan')" variant="filled" size="md" no-wire-navigate />
@endsection
```
- Maintenance mode: `php artisan down --render="errors::503"`.
- The showcase previews each page at `/material/errors/{code}`.
## Mail
Markdown mail (notifications and `markdown:` mailables) wears M3 once the application selects the theme:
```dotenv
MAIL_MARKDOWN_THEME=livewire-material::mail.theme
```
or per mail: `(new MailMessage)->theme('livewire-material::mail.theme')`, or `public $theme = 'livewire-material::mail.theme';` on a mailable.
- Colours are the light scheme from `resources/css/material-scheme.json` (`livewire-material.scheme`), inlined as hexes; regenerate the scheme and mail follows. Without the file, the package's default scheme applies.
- Write the body as Markdown; the theme styles the bare tags (`#` headings, prose, lists, tables) with M3's typescale. `<x-mail::button :url="…">` is a filled pill in `primary`; `color` also takes `secondary`, `tertiary`, `error`, `success`, `warning` and `info`. `<x-mail::panel>` is a tinted container.
- There is no dark mail. Never put `@media` rules, CSS variables or `color-mix()` in mail CSS: the inliner strips media queries and mail clients resolve no variables.
- The package's mail header (the app name, or a logo) and message (with a replaceable footer) are opt-in: set `MATERIAL_MAIL_COMPONENTS=true`, or `php artisan vendor:publish --tag=livewire-material-mail` to copy them into `resources/views/vendor/mail`. For a logo set `livewire-material.mail.logo` to `['src' => 'https://example.com/logo.png', 'width' => 160, 'height' => 40]`: an absolute URL, with the image at twice those dimensions.
- With those components, a mail can replace the footer:
```blade
<x-mail::message>
Your export is ready.
<x-slot:footer>
© {{ date('Y') }} {{ config('app.name') }} · [Unsubscribe]({{ $unsubscribeUrl }})
</x-slot:footer>
</x-mail::message>
```
- The showcase renders a sample mail at `/material/mail`.
## Components
### `<x-icon>`
@@ -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>