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,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;
|
||||
}
|
||||
Reference in New Issue
Block a user