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
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:
co-authored by
Claude Opus 5
parent
ab692b66bb
commit
8f174520eb
@@ -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 don’t have access'))
|
||||
@section('message', isset($exception) && $exception->getMessage() !== '' ? __($exception->getMessage()) : __('Your account isn’t 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 you’re looking for doesn’t 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', __('We’ll be right back'))
|
||||
@section('message', $reason !== '' && $reason !== 'Service Unavailable' ? __($reason) : __('We’re 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>
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
Reference in New Issue
Block a user