Skip to content

Commit dd055b0

Browse files
committed
Add dark mode support for email templates and extract media queries
1 parent 675f71c commit dd055b0

File tree

4 files changed

+296
-20
lines changed

4 files changed

+296
-20
lines changed

src/Illuminate/Mail/Markdown.php

Lines changed: 77 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,13 @@ class Markdown
4242
*/
4343
protected static $withSecuredEncoding = false;
4444

45+
/**
46+
* The extracted head styles (media queries) from the theme.
47+
*
48+
* @var string
49+
*/
50+
protected static $headStyles = '';
51+
4552
/**
4653
* Create a new Markdown renderer instance.
4754
*
@@ -67,6 +74,20 @@ public function render($view, array $data = [], $inliner = null)
6774
{
6875
$this->view->flushFinderCache();
6976

77+
$this->view->replaceNamespace('mail', $this->htmlComponentPaths());
78+
79+
if ($this->view->exists($customTheme = Str::start($this->theme, 'mail.'))) {
80+
$theme = $customTheme;
81+
} else {
82+
$theme = str_contains($this->theme, '::')
83+
? $this->theme
84+
: 'mail::themes.'.$this->theme;
85+
}
86+
87+
$themeCss = $this->view->make($theme, $data)->render();
88+
89+
[$inlineCss, static::$headStyles] = $this->extractMediaQueries($themeCss);
90+
7091
$bladeCompiler = $this->view
7192
->getEngineResolver()
7293
->resolve('blade')
@@ -88,9 +109,7 @@ function () use ($view, $data) {
88109
}
89110

90111
try {
91-
$contents = $this->view->replaceNamespace(
92-
'mail', $this->htmlComponentPaths()
93-
)->make($view, $data)->render();
112+
$contents = $this->view->make($view, $data)->render();
94113
} finally {
95114
EncodedHtmlString::flushState();
96115
}
@@ -99,16 +118,8 @@ function () use ($view, $data) {
99118
}
100119
);
101120

102-
if ($this->view->exists($customTheme = Str::start($this->theme, 'mail.'))) {
103-
$theme = $customTheme;
104-
} else {
105-
$theme = str_contains($this->theme, '::')
106-
? $this->theme
107-
: 'mail::themes.'.$this->theme;
108-
}
109-
110121
return new HtmlString(($inliner ?: new CssToInlineStyles)->convert(
111-
str_replace('\[', '[', $contents), $this->view->make($theme, $data)->render()
122+
str_replace('\[', '[', $contents), $inlineCss
112123
));
113124
}
114125

@@ -289,5 +300,59 @@ public static function withoutSecuredEncoding()
289300
public static function flushState()
290301
{
291302
static::$withSecuredEncoding = false;
303+
static::$headStyles = '';
304+
}
305+
306+
/**
307+
* Extract media queries from CSS that cannot be inlined.
308+
*
309+
* @param string $css
310+
* @return array{0: string, 1: string}
311+
*/
312+
protected function extractMediaQueries($css)
313+
{
314+
$mediaBlocks = '';
315+
$inlineCss = '';
316+
$offset = 0;
317+
$length = strlen($css);
318+
319+
while (($pos = strpos($css, '@media', $offset)) !== false) {
320+
$inlineCss .= substr($css, $offset, $pos - $offset);
321+
322+
$open = strpos($css, '{', $pos);
323+
324+
if ($open === false) {
325+
break;
326+
}
327+
328+
$braceCount = 1;
329+
$i = $open + 1;
330+
331+
while ($i < $length && $braceCount > 0) {
332+
if ($css[$i] === '{') {
333+
$braceCount++;
334+
} elseif ($css[$i] === '}') {
335+
$braceCount--;
336+
}
337+
$i++;
338+
}
339+
340+
$mediaBlocks .= substr($css, $pos, $i - $pos)."\n";
341+
$offset = $i;
342+
}
343+
344+
$inlineCss .= substr($css, $offset);
345+
346+
return [$inlineCss, $mediaBlocks];
347+
}
348+
349+
/**
350+
* Get the extracted head styles (media queries) from the theme.
351+
*
352+
* @return string
353+
*/
354+
public static function getHeadStyles()
355+
{
356+
return static::$headStyles;
292357
}
293358
}

src/Illuminate/Mail/resources/views/html/layout.blade.php

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@
44
<title>{{ config('app.name') }}</title>
55
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
66
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
7-
<meta name="color-scheme" content="light">
8-
<meta name="supported-color-schemes" content="light">
7+
<meta name="color-scheme" content="light dark">
8+
<meta name="supported-color-schemes" content="light dark">
99
<style>
1010
@media only screen and (max-width: 600px) {
1111
.inner-body {
@@ -22,6 +22,8 @@
2222
width: 100% !important;
2323
}
2424
}
25+
26+
{!! \Illuminate\Mail\Markdown::getHeadStyles() !!}
2527
</style>
2628
{!! $head ?? '' !!}
2729
</head>

src/Illuminate/Mail/resources/views/html/themes/default.css

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -295,3 +295,94 @@ img {
295295
.break-all {
296296
word-break: break-all;
297297
}
298+
299+
/* Dark Mode */
300+
301+
@media (prefers-color-scheme: dark) {
302+
body,
303+
.wrapper,
304+
.body {
305+
background-color: #18181b !important;
306+
}
307+
308+
.inner-body {
309+
background-color: #27272a !important;
310+
border-color: #3f3f46 !important;
311+
}
312+
313+
p,
314+
ul,
315+
ol,
316+
blockquote,
317+
span,
318+
td {
319+
color: #e4e4e7 !important;
320+
}
321+
322+
a {
323+
color: #a5b4fc !important;
324+
}
325+
326+
h1,
327+
h2,
328+
h3,
329+
.header a {
330+
color: #fafafa !important;
331+
}
332+
333+
.logo {
334+
filter: invert(23%) sepia(5%) saturate(531%) hue-rotate(202deg) brightness(96%) contrast(91%) !important;
335+
}
336+
337+
.button-primary,
338+
.button-blue {
339+
background-color: #fafafa !important;
340+
border-color: #fafafa !important;
341+
color: #18181b !important;
342+
}
343+
344+
.button-secondary {
345+
background-color: #3f3f46 !important;
346+
border-color: #3f3f46 !important;
347+
color: #fafafa !important;
348+
}
349+
350+
.button-success,
351+
.button-green {
352+
background-color: #22c55e !important;
353+
border-color: #22c55e !important;
354+
color: #fff !important;
355+
}
356+
357+
.button-error,
358+
.button-red {
359+
background-color: #ef4444 !important;
360+
border-color: #ef4444 !important;
361+
color: #fff !important;
362+
}
363+
364+
.footer p,
365+
.footer a {
366+
color: #71717a !important;
367+
}
368+
369+
.panel {
370+
border-left-color: #d4d4d8 !important;
371+
}
372+
373+
.panel-content {
374+
background-color: #3f3f46 !important;
375+
}
376+
377+
.panel-content p {
378+
color: #e4e4e7 !important;
379+
}
380+
381+
.subcopy {
382+
border-top-color: #3f3f46 !important;
383+
}
384+
385+
.table th {
386+
border-bottom-color: #3f3f46 !important;
387+
}
388+
}

0 commit comments

Comments
 (0)