- Updated .env.example to include configuration for OnlyOffice Document Server. - Modified the workspace configuration to remove the drive-suite path. - Adjusted TypeScript environment imports for consistency. - Enhanced Next.js configuration to disable canvas in Webpack. - Updated package.json to include new dependencies for OnlyOffice and PDF.js. - Added global styles for OnlyOffice theme integration in the CSS. - Created new layout and page components for the Drive feature, including public sharing and editing functionalities. - Updated metadata handling across various layouts to reflect the new app structure.
316 lines
10 KiB
TypeScript
316 lines
10 KiB
TypeScript
/** CSS injecté dans les iframes d’aperç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 l’app (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"
|
||
|
||
export function emailPreviewBaseCss(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: 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;
|
||
}
|
||
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
|
||
}
|