ultisuite-client/lib/email-preview-dark-styles.ts
R3D347HR4Y ad1370ea7e
Some checks are pending
E2E / Playwright e2e (push) Waiting to run
feat: enhance configuration and add new demo layouts
- Introduced turbopack alias for canvas in next.config.mjs.
- Updated package.json scripts for development and branding tasks.
- Added new dependencies for Tiptap extensions.
- Implemented new demo layouts for agenda, contacts, drive, and mail applications.
- Enhanced globals.css for improved theming and splash screen animations.
- Added OAuth callback handling for drive mounts.
- Updated layout components to integrate new demo shells and improve structure.
2026-06-12 19:10:24 +02:00

332 lines
11 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/** CSS injecté dans les iframes daperçu mail (sujet + corps). */
import { stripHiddenEmailHtml } from "@/lib/strip-hidden-email-html"
const DARK_TEXT = "#e8eaed"
const DARK_LINK = "#8ab4f8"
/** Texte par défaut sans style expéditeur — aligné sur --mail-text. */
const DEFAULT_BODY_TEXT_LIGHT = "#3c4043"
const LIGHT_TEXT = "#202124"
const LIGHT_LINK = "#1a73e8"
/** Même pile que lapp (Geist + fallbacks système). */
export const EMAIL_PREVIEW_FONT_FAMILY =
"'Geist', 'Geist Fallback', ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif"
/** Évite boucle iframe ↔ hauteur quand le HTML expéditeur utilise vh / min-height:100%. */
export const EMAIL_PREVIEW_IFRAME_SHELL_CSS = `
html, body {
height: auto !important;
min-height: 0 !important;
max-height: none !important;
}
[data-ultimail-measure-root] {
display: flow-root;
height: auto !important;
min-height: 0 !important;
}
`
export function emailPreviewBaseCss(isDark: boolean): string {
return `
* { margin: 0; padding: 0; box-sizing: border-box; }
html {
color-scheme: ${isDark ? "dark" : "light"};
background: transparent !important;
}
${EMAIL_PREVIEW_IFRAME_SHELL_CSS}
html, body {
background: transparent !important;
overflow: visible;
word-wrap: break-word;
overflow-wrap: break-word;
}
body {
font-family: ${EMAIL_PREVIEW_FONT_FAMILY};
font-size: 14px;
line-height: 1.6;
color: ${isDark ? DARK_TEXT : DEFAULT_BODY_TEXT_LIGHT} !important;
padding: 0;
}
a, a * { color: ${isDark ? DARK_LINK : LIGHT_LINK} !important; }
img { max-width: 100%; height: auto; }
blockquote {
border-left: 3px solid ${isDark ? "#5f6368" : "#dadce0"};
padding-left: 12px;
margin: 8px 0;
color: ${isDark ? "#9aa0a6" : "#5f6368"} !important;
}
pre, code {
background: ${isDark ? "#3c4043" : "#f6f8fa"} !important;
color: ${isDark ? DARK_TEXT : LIGHT_TEXT} !important;
border-radius: 3px;
font-size: 13px;
}
pre { padding: 12px; overflow-x: auto; }
code { padding: 2px 6px; }
`
}
export function emailPreviewSubjectCss(isDark: boolean): string {
return `
* { margin: 0; padding: 0; box-sizing: border-box; }
html {
color-scheme: ${isDark ? "dark" : "light"};
background: transparent !important;
}
html, body {
background: transparent !important;
overflow: hidden;
white-space: normal;
word-wrap: break-word;
}
body {
font-family: ${EMAIL_PREVIEW_FONT_FAMILY};
font-size: 22px;
line-height: 1.3;
color: ${isDark ? DARK_TEXT : DEFAULT_BODY_TEXT_LIGHT} !important;
padding: 0;
}
`
}
/** CSS minimal quand le mail conserve sa mise en forme d'origine (contenu distant autorisé). */
export function emailPreviewWrapperCss(isDark = false): string {
const textColor = isDark ? DARK_TEXT : DEFAULT_BODY_TEXT_LIGHT
const linkColor = isDark ? DARK_LINK : LIGHT_LINK
return `
html {
color-scheme: ${isDark ? "dark" : "light"};
background: transparent !important;
}
${EMAIL_PREVIEW_IFRAME_SHELL_CSS}
html, body {
margin: 0;
padding: 0;
background: transparent !important;
}
body {
overflow: visible;
word-wrap: break-word;
overflow-wrap: break-word;
font-family: ${EMAIL_PREVIEW_FONT_FAMILY};
font-size: 14px;
line-height: 1.6;
color: ${textColor};
}
a, a * {
color: ${linkColor};
}
img {
max-width: 100%;
height: auto;
}
`
}
/** Force le texte clair et fonds transparents sur le HTML d'e-mail en mode sombre. */
export function emailPreviewDarkOverrideCss(): string {
return `
:root { color-scheme: dark; }
body,
body div, body p, body span, body td, body th, body li, body font,
body h1, body h2, body h3, body h4, body h5, body h6,
body label, body strong, body b, body em, body i, body u,
body center, body table, body tbody, body thead, body tfoot, body tr {
color: ${DARK_TEXT} !important;
}
body a, body a * {
color: ${DARK_LINK} !important;
}
[bgcolor="#ffffff"], [bgcolor="#FFFFFF"], [bgcolor="white"],
[bgcolor="#f8f9fa"], [bgcolor="#F8F9FA"], [bgcolor="#f1f3f4"], [bgcolor="#F1F3F4"],
[bgcolor="#e8eaed"], [bgcolor="#E8EAED"], [bgcolor="#f6f8fc"], [bgcolor="#F6F8FC"],
[bgcolor="#fafafa"], [bgcolor="#FAFAFA"], [bgcolor="#eeeeee"], [bgcolor="#EEEEEE"],
[bgcolor="#fcfcfc"], [bgcolor="#FCFCFC"], [bgcolor="#fff"], [bgcolor="#FFF"] {
background-color: transparent !important;
background: transparent !important;
}
[color="#000000"], [color="#000"], [color="#111111"], [color="#202124"],
[color="#3c4043"], [color="#5f6368"], [color="#444746"], [color="#1f1f1f"],
[color="#333333"], [color="#333"], [color="#666666"], [color="#666"],
[color="#757575"], [color="#80868b"], [color="#9aa0a6"] {
color: ${DARK_TEXT} !important;
}
font[color] {
color: ${DARK_TEXT} !important;
}
[bgcolor="#000000"], [bgcolor="#000"], [bgcolor="#202124"], [bgcolor="#3c4043"],
[bgcolor="#1a1a1a"], [bgcolor="#2d2d2d"] {
background-color: #3c4043 !important;
}
div, td, th, p, span, li, h1, h2, h3, h4, h5, h6, table {
border-color: color-mix(in srgb, ${DARK_TEXT} 25%, transparent) !important;
}
`
}
/**
* En fin de <body> (après les <style> expéditeur) quand le contenu distant est bloqué.
* Gagne la cascade sur p { color: #333 !important } dans le corps du mail.
*/
export function emailPreviewDarkTailOverrideCss(): string {
return `
body p, body span, body div, body td, body th, body li, body font,
body h1, body h2, body h3, body h4, body h5, body h6,
body label, body strong, body b, body em, body i, body u,
body center, body blockquote, body pre {
color: ${DARK_TEXT} !important;
}
body a, body a * {
color: ${DARK_LINK} !important;
}
[color], font[color] {
color: ${DARK_TEXT} !important;
}
`
}
const LIGHT_SURFACE_TEXT_TAGS =
"p,span,div,td,th,li,h1,h2,h3,h4,h5,h6,font,label,strong,b,em,i,u,center,blockquote,pre"
/** Fin de body + contenu distant : texte clair par défaut, zones claires marquées au runtime. */
export function emailPreviewDarkRemoteBodyTailCss(): string {
const lightSurfaceChildren = LIGHT_SURFACE_TEXT_TAGS.split(",")
.map((t) => `[data-ultimail-light-surface] ${t}`)
.join(", ")
return `
${emailPreviewDarkTailOverrideCss()}
${lightSurfaceChildren},
[data-ultimail-light-surface] {
color: ${LIGHT_TEXT} !important;
}
[data-ultimail-light-surface] a,
[data-ultimail-light-surface] a * {
color: ${LIGHT_LINK} !important;
}
`
}
/** Adoucit les fonds très sombres en mode clair (e-mails « dark »). */
export function emailPreviewLightOverrideCss(): string {
return `
[bgcolor="#000000"], [bgcolor="#000"], [bgcolor="#202124"], [bgcolor="#3c4043"],
[bgcolor="#1a1a1a"], [bgcolor="#2d2d2d"] {
background-color: #f1f3f4 !important;
}
[color="#ffffff"], [color="#FFFFFF"], [color="#e8eaed"], [color="#f8f9fa"],
[color="#dadce0"] {
color: ${LIGHT_TEXT} !important;
}
font[color="#ffffff"], font[color="#FFFFFF"], font[color="#e8eaed"] {
color: ${LIGHT_TEXT} !important;
}
`
}
const LIGHT_BG_STYLE =
/background(?:-color)?\s*:\s*(?:#(?:fff(?:fff)?|fefefe|f[ef][ef][ef](?:ff)?)|white|rgb\(\s*255\s*,\s*255\s*,\s*255\s*\)|rgba\(\s*255\s*,\s*255\s*,\s*255\s*,[^)]+\))/gi
const DARK_BG_STYLE =
/background(?:-color)?\s*:\s*(?:#(?:000(?:000)?|202124|3c4043|1a1a1a|2d2d2d)|black|rgb\(\s*0\s*,\s*0\s*,\s*0\s*\))/gi
/** Remplace ou supprime les couleurs de texte inline (pas background-color). */
const INLINE_COLOR_STYLE =
/(?<!background-)(?<!border-)color\s*:\s*(?:#[0-9a-f]{3,8}\b|rgb\(\s*[\d.,\s%]+\s*\)|rgba\(\s*[\d.,\s%]+\s*\)|[a-z]{3,20})\b/gi
const INLINE_BG_STYLE =
/background(?:-color)?\s*:\s*(?:#[0-9a-f]{3,8}\b|rgb\(\s*[\d.,\s%]+\s*\)|rgba\(\s*[\d.,\s%]+\s*\)|[a-z]{3,20})\b/gi
function rewriteStyleAttribute(styles: string, isDark: boolean): string {
let next = styles
if (isDark) {
next = next
.replace(INLINE_BG_STYLE, "background:transparent")
.replace(INLINE_COLOR_STYLE, `color:${DARK_TEXT}`)
} else {
next = next
.replace(DARK_BG_STYLE, "background:#f1f3f4")
.replace(
/(?<!background-)(?<!border-)color\s*:\s*(?:#(?:fff(?:fff)?|e8eaed|f8f9fa)|white|rgb\(\s*255\s*,)/gi,
`color:${LIGHT_TEXT}`
)
}
return next.replace(/;\s*;/g, ";").replace(/^;|;$/g, "").trim()
}
function rewriteInlineStyles(html: string, isDark: boolean): string {
return html.replace(
/\sstyle=(["'])([\s\S]*?)\1/gi,
(_match, quote: string, styles: string) => {
const rewritten = rewriteStyleAttribute(styles, isDark)
if (!rewritten) return ""
return ` style=${quote}${rewritten}${quote}`
}
)
}
function normalizeAttrColorValue(raw: string): string {
const v = raw.trim()
if (/^#?[0-9a-f]{3,8}$/i.test(v)) {
return v.startsWith("#") ? v : `#${v}`
}
return v
}
function isDarkColorAttrValue(raw: string): boolean {
const normalized = normalizeAttrColorValue(raw).toLowerCase()
if (normalized === "black") return true
const hex = normalized.match(/^#([0-9a-f]{3,8})$/i)
if (hex) {
let h = hex[1]
if (h.length === 3) h = h.split("").map((c) => c + c).join("")
if (h.length >= 6) {
const r = parseInt(h.slice(0, 2), 16)
const g = parseInt(h.slice(2, 4), 16)
const b = parseInt(h.slice(4, 6), 16)
return 0.2126 * r + 0.7152 * g + 0.0722 * b < 120
}
}
const rgb = normalized.match(/^rgba?\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)/)
if (rgb) {
const r = Number(rgb[1])
const g = Number(rgb[2])
const b = Number(rgb[3])
return 0.2126 * r + 0.7152 * g + 0.0722 * b < 120
}
return false
}
function rewriteHtmlColorAttributes(html: string, isDark: boolean): string {
if (!isDark) return html
return html.replace(
/\s(color)=(["'])([^"']*)\2/gi,
(match, attr: string, quote: string, value: string) => {
if (attr.toLowerCase() !== "color") return match
if (!isDarkColorAttrValue(value)) return match
return ` color=${quote}${DARK_TEXT}${quote}`
}
)
}
export function preprocessEmailHtmlForTheme(html: string, isDark: boolean): string {
let next = stripHiddenEmailHtml(html)
next = rewriteInlineStyles(next, isDark)
next = rewriteHtmlColorAttributes(next, isDark)
if (isDark) {
next = next.replace(LIGHT_BG_STYLE, "background:transparent")
next = next.replace(/\sbgcolor=(["'])(?:#?(?:fff(?:fff)?|ffffff|white)|#f[0-9a-f]{5})\1/gi, "")
}
return next
}