add ability to include custom css & js in web interface (webmail, webaccount, webadmin), and use css variables in webmail for easier customization

if files {webmail,webaccount,webadmin}.{css,js} exist in the configdir (where
the mox.conf file lives), their contents are included in the web apps.

the webmail now uses css variables, mostly for colors. so you can write a
custom webmail.css that changes the variables, e.g.:

	:root {
		--color: blue
	}

you can also look at css class names and override their styles.

in the future, we may want to make some css variables configurable in the
per-user settings in the webmail. should reduce the number of variables first.

any custom javascript is loaded first. if it defines a global function
"moxBeforeDisplay", that is called each time a page loads (after
authentication) with the DOM element of the page content as parameter. the
webmail is a single persistent page. this can be used to make some changes to
the DOM, e.g. inserting some elements. we'll have to see how well this works in
practice. perhaps some patterns emerge (e.g. adding a logo), and we can make
those use-cases easier to achieve.

helps partially with issue #114, and based on questions from laura-lilly on
matrix.
This commit is contained in:
Mechiel Lukkien
2024-11-29 10:17:07 +01:00
parent 9e8c8ca583
commit 96d86ad6f1
20 changed files with 838 additions and 418 deletions

View File

@ -9,11 +9,11 @@
// We keep the default/regular styles and dark-mode styles in separate stylesheets.
const cssStyle = dom.style(attr.type('text/css'))
document.head.appendChild(cssStyle)
document.head.prepend(cssStyle)
const styleSheet = cssStyle.sheet!
const cssStyleDark = dom.style(attr.type('text/css'))
document.head.appendChild(cssStyleDark)
document.head.prepend(cssStyleDark)
const styleSheetDark = cssStyleDark.sheet!
styleSheetDark.insertRule('@media (prefers-color-scheme: dark) {}')
const darkModeRule = styleSheetDark.cssRules[0] as CSSMediaRule
@ -42,8 +42,11 @@ const ensureCSS = (selector: string, styles: { [prop: string]: string | number |
let darkst: CSSStyleDeclaration | undefined
for (let [k, v] of Object.entries(styles)) {
// We've kept the camel-case in our code which we had from when we did "st[prop] =
// value". It is more convenient as object keys. So convert to kebab-case.
k = k.replace(/[A-Z]/g, s => '-'+s.toLowerCase())
// value". It is more convenient as object keys. So convert to kebab-case, but only
// if this is not a css property.
if (!k.startsWith('--')) {
k = k.replace(/[A-Z]/g, s => '-'+s.toLowerCase())
}
if (Array.isArray(v)) {
if (v.length !== 2) {
throw new Error('2 elements required for light/dark mode style, got '+v.length)
@ -70,64 +73,132 @@ const css = (className: string, styles: { [prop: string]: string | number | stri
// todo: reduce number of colors. hopefully we can derive some colors from a few base colors (making them brighter/darker, or shifting hue, etc). then make them configurable through settings.
// todo: add the standard padding and border-radius, perhaps more.
// todo: could make some of these {prop: value} objects and pass them directly to css()
const styles = {
color: ['black', '#ddd'],
colorMild: ['#555', '#bbb'],
colorMilder: ['#666', '#aaa'],
backgroundColor: ['white', '#222'],
backgroundColorMild: ['#f8f8f8', '#080808'],
backgroundColorMilder: ['#999', '#777'],
borderColor: ['#ccc', '#333'],
mailboxesTopBackgroundColor: ['#fdfdf1', 'rgb(26, 18, 0)'],
msglistBackgroundColor: ['#f5ffff', 'rgb(4, 19, 13)'],
boxShadow: ['0 0 20px rgba(0, 0, 0, 0.1)', '0px 0px 20px #000'],
// We define css variables, making them easy to override.
ensureCSS(':root', {
'--color': ['black', '#ddd'],
'--colorMild': ['#555', '#bbb'],
'--colorMilder': ['#666', '#aaa'],
'--backgroundColor': ['white', '#222'],
'--backgroundColorMild': ['#f8f8f8', '#080808'],
'--backgroundColorMilder': ['#999', '#777'],
'--borderColor': ['#ccc', '#333'],
'--mailboxesTopBackgroundColor': ['#fdfdf1', '#1a1200'],
'--msglistBackgroundColor': ['#f5ffff', '#04130d'],
'--boxShadow': ['0 0 20px rgba(0, 0, 0, 0.1)', '0px 0px 20px #000'],
buttonBackground: ['#eee', '#222'],
buttonBorderColor: ['#888', '#666'],
buttonHoverBackground: ['#ddd', '#333'],
'--buttonBackground': ['#eee', '#222'],
'--buttonBorderColor': ['#888', '#666'],
'--buttonHoverBackground': ['#ddd', '#333'],
overlayOpaqueBackgroundColor: ['#eee', '#011'],
overlayBackgroundColor: ['rgba(0, 0, 0, 0.2)', 'rgba(0, 0, 0, 0.5)'],
'--overlayOpaqueBackgroundColor': ['#eee', '#011'],
'--overlayBackgroundColor': ['rgba(0, 0, 0, 0.2)', 'rgba(0, 0, 0, 0.5)'],
popupColor: ['black', 'white'],
popupBackgroundColor: ['white', 'rgb(49, 50, 51)'],
popupBorderColor: ['#ccc', '#555'],
'--popupColor': ['black', 'white'],
'--popupBackgroundColor': ['white', '#313233'],
'--popupBorderColor': ['#ccc', '#555'],
highlightBackground: ['gold', '#a70167'],
highlightBorderColor: ['#8c7600', 'rgb(253, 31, 167)'],
highlightBackgroundHover: ['#ffbd21', 'rgb(113, 4, 71)'],
'--highlightBackground': ['gold', '#a70167'],
'--highlightBorderColor': ['#8c7600', '#fd1fa7'],
'--highlightBackgroundHover': ['#ffbd21', '#710447'],
mailboxActiveBackground: ['linear-gradient(135deg, #ffc7ab 0%, #ffdeab 100%)', 'linear-gradient(135deg, rgb(182, 61, 0) 0%, rgb(140, 90, 13) 100%)'],
mailboxHoverBackgroundColor: ['#eee', 'rgb(66, 31, 21)'],
'--mailboxActiveBackground': ['linear-gradient(135deg, #ffc7ab 0%, #ffdeab 100%)', 'linear-gradient(135deg, #b63d00 0%, #8c5a0d 100%)'],
'--mailboxHoverBackgroundColor': ['#eee', '#421f15'],
msgItemActiveBackground: ['linear-gradient(135deg, #8bc8ff 0%, #8ee5ff 100%)', 'linear-gradient(135deg, rgb(4, 92, 172) 0%, rgb(2, 123, 160) 100%)'],
msgItemHoverBackgroundColor: ['#eee', 'rgb(7, 51, 72)'],
msgItemFocusBorderColor: ['#2685ff', '#2685ff'],
'--msgItemActiveBackground': ['linear-gradient(135deg, #8bc8ff 0%, #8ee5ff 100%)', 'linear-gradient(135deg, #045cac 0%, #027ba0 100%)'],
'--msgItemHoverBackgroundColor': ['#eee', '#073348'],
'--msgItemFocusBorderColor': ['#2685ff', '#2685ff'],
buttonTristateOnBackground: ['#c4ffa9', 'rgb(39, 126, 0)'],
buttonTristateOffBackground: ['#ffb192', 'rgb(191, 65, 15)'],
'--buttonTristateOnBackground': ['#c4ffa9', '#277e00'],
'--buttonTristateOffBackground': ['#ffb192', '#bf410f'],
warningBackgroundColor: ['#ffca91', 'rgb(168, 87, 0)'],
'--warningBackgroundColor': ['#ffca91', '#a85700'],
successBackground: ['#d2f791', '#1fa204'],
emphasisBackground: ['#666', '#aaa'],
'--successBackground': ['#d2f791', '#1fa204'],
'--emphasisBackground': ['#666', '#aaa'],
// For authentication/security results.
underlineGreen: '#50c40f',
underlineRed: '#e15d1c',
underlineBlue: '#09f',
underlineGrey: '#888',
'--underlineGreen': '#50c40f',
'--underlineRed': '#e15d1c',
'--underlineBlue': '#09f',
'--underlineGrey': '#888',
'--quoted1Color': ['#03828f', '#71f2ff'], // red
'--quoted2Color': ['#c7445c', '#ec4c4c'], // green
'--quoted3Color': ['#417c10', '#73e614'], // blue
'--scriptSwitchUnderlineColor': ['#dca053', '#e88f1e'],
'--linkColor': ['#096bc2', '#63b6ff'],
'--linkVisitedColor': ['#0704c1', '#c763ff'],
})
// Typed way to reference a css variables. Kept from before used variables.
const styles = {
color: 'var(--color)',
colorMild: 'var(--colorMild)',
colorMilder: 'var(--colorMilder)',
backgroundColor: 'var(--backgroundColor)',
backgroundColorMild: 'var(--backgroundColorMild)',
backgroundColorMilder: 'var(--backgroundColorMilder)',
borderColor: 'var(--borderColor)',
mailboxesTopBackgroundColor: 'var(--mailboxesTopBackgroundColor)',
msglistBackgroundColor: 'var(--msglistBackgroundColor)',
boxShadow: 'var(--boxShadow)',
buttonBackground: 'var(--buttonBackground)',
buttonBorderColor: 'var(--buttonBorderColor)',
buttonHoverBackground: 'var(--buttonHoverBackground)',
overlayOpaqueBackgroundColor: 'var(--overlayOpaqueBackgroundColor)',
overlayBackgroundColor: 'var(--overlayBackgroundColor)',
popupColor: 'var(--popupColor)',
popupBackgroundColor: 'var(--popupBackgroundColor)',
popupBorderColor: 'var(--popupBorderColor)',
highlightBackground: 'var(--highlightBackground)',
highlightBorderColor: 'var(--highlightBorderColor)',
highlightBackgroundHover: 'var(--highlightBackgroundHover)',
mailboxActiveBackground: 'var(--mailboxActiveBackground)',
mailboxHoverBackgroundColor: 'var(--mailboxHoverBackgroundColor)',
msgItemActiveBackground: 'var(--msgItemActiveBackground)',
msgItemHoverBackgroundColor: 'var(--msgItemHoverBackgroundColor)',
msgItemFocusBorderColor: 'var(--msgItemFocusBorderColor)',
buttonTristateOnBackground: 'var(--buttonTristateOnBackground)',
buttonTristateOffBackground: 'var(--buttonTristateOffBackground)',
warningBackgroundColor: 'var(--warningBackgroundColor)',
successBackground: 'var(--successBackground)',
emphasisBackground: 'var(--emphasisBackground)',
// For authentication/security results.
underlineGreen: 'var(--underlineGreen)',
underlineRed: 'var(--underlineRed)',
underlineBlue: 'var(--underlineBlue)',
underlineGrey: 'var(--underlineGrey)',
quoted1Color: 'var(--quoted1Color)',
quoted2Color: 'var(--quoted2Color)',
quoted3Color: 'var(--quoted3Color)',
scriptSwitchUnderlineColor: 'var(--scriptSwitchUnderlineColor)',
linkColor: 'var(--linkColor)',
linkVisitedColor: 'var(--linkVisitedColor)',
}
const styleClasses = {
// For quoted text, with multiple levels of indentations.
quoted: [
css('quoted1', {color: ['#03828f', '#71f2ff']}), // red
css('quoted2', {color: ['#c7445c', 'rgb(236, 76, 76)']}), // green
css('quoted3', {color: ['#417c10', 'rgb(115, 230, 20)']}), // blue
css('quoted1', {color: styles.quoted1Color}),
css('quoted2', {color: styles.quoted2Color}),
css('quoted3', {color: styles.quoted3Color}),
],
// When text switches between unicode scripts.
scriptswitch: css('scriptswitch', {textDecoration: 'underline 2px', textDecorationColor: ['#dca053', 'rgb(232, 143, 30)']}),
scriptswitch: css('scriptswitch', {textDecoration: 'underline 2px', textDecorationColor: styles.scriptSwitchUnderlineColor}),
textMild: css('textMild', {color: styles.colorMild}),
// For keywords (also known as flags/labels/tags) on messages.
keyword: css('keyword', {padding: '0 .15em', borderRadius: '.15em', fontWeight: 'normal', fontSize: '.9em', margin: '0 .15em', whiteSpace: 'nowrap', background: styles.highlightBackground, color: styles.color, border: '1px solid', borderColor: styles.highlightBorderColor}),
@ -138,15 +209,15 @@ ensureCSS('.msgHeaders td', {wordBreak: 'break-word'}) // Prevent horizontal scr
ensureCSS('.keyword.keywordCollapsed', {opacity: .75}),
// Generic styling.
ensureCSS('html', {backgroundColor: 'var(--backgroundColor)', color: 'var(--color)'})
ensureCSS('*', {fontSize: 'inherit', fontFamily: "'ubuntu', 'lato', sans-serif", margin: 0, padding: 0, boxSizing: 'border-box'})
ensureCSS('.mono, .mono *', {fontFamily: "'ubuntu mono', monospace"})
ensureCSS('table td, table th', {padding: '.15em .25em'})
ensureCSS('.pad', {padding: '.5em'})
ensureCSS('iframe', {border: 0})
ensureCSS('img, embed, video, iframe', {backgroundColor: 'white', color: 'black'})
ensureCSS(':root', {backgroundColor: styles.backgroundColor, color: styles.color})
ensureCSS('a', {color: ['rgb(9, 107, 194)', 'rgb(99, 182, 255)']})
ensureCSS('a:visited', {color: ['rgb(7, 4, 193)', 'rgb(199, 99, 255)']})
ensureCSS('a', {color: styles.linkColor})
ensureCSS('a:visited', {color: styles.linkVisitedColor})
// For message view with multiple inline elements (often a single text and multiple messages).
ensureCSS('.textmulti > *:nth-child(even)', {backgroundColor: ['#f4f4f4', '#141414']})

View File

@ -4,10 +4,17 @@
<title>Message</title>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<style>
/* css placeholder */
</style>
</head>
<body>
<div id="page"><div style="padding: 1em">Loading...</div></div>
<script>
/* js placeholder */
</script>
<!-- Load message data synchronously like in text.html, which needs it to generate a meaningful 'loaded' event, used for updating the iframe height. -->
<script src="parsedmessage.js"></script>

View File

@ -1055,10 +1055,10 @@ var api;
// instances of a class.
// We keep the default/regular styles and dark-mode styles in separate stylesheets.
const cssStyle = dom.style(attr.type('text/css'));
document.head.appendChild(cssStyle);
document.head.prepend(cssStyle);
const styleSheet = cssStyle.sheet;
const cssStyleDark = dom.style(attr.type('text/css'));
document.head.appendChild(cssStyleDark);
document.head.prepend(cssStyleDark);
const styleSheetDark = cssStyleDark.sheet;
styleSheetDark.insertRule('@media (prefers-color-scheme: dark) {}');
const darkModeRule = styleSheetDark.cssRules[0];
@ -1085,8 +1085,11 @@ const ensureCSS = (selector, styles, important) => {
let darkst;
for (let [k, v] of Object.entries(styles)) {
// We've kept the camel-case in our code which we had from when we did "st[prop] =
// value". It is more convenient as object keys. So convert to kebab-case.
k = k.replace(/[A-Z]/g, s => '-' + s.toLowerCase());
// value". It is more convenient as object keys. So convert to kebab-case, but only
// if this is not a css property.
if (!k.startsWith('--')) {
k = k.replace(/[A-Z]/g, s => '-' + s.toLowerCase());
}
if (Array.isArray(v)) {
if (v.length !== 2) {
throw new Error('2 elements required for light/dark mode style, got ' + v.length);
@ -1112,54 +1115,105 @@ const css = (className, styles, important) => {
};
// todo: reduce number of colors. hopefully we can derive some colors from a few base colors (making them brighter/darker, or shifting hue, etc). then make them configurable through settings.
// todo: add the standard padding and border-radius, perhaps more.
// todo: could make some of these {prop: value} objects and pass them directly to css()
const styles = {
color: ['black', '#ddd'],
colorMild: ['#555', '#bbb'],
colorMilder: ['#666', '#aaa'],
backgroundColor: ['white', '#222'],
backgroundColorMild: ['#f8f8f8', '#080808'],
backgroundColorMilder: ['#999', '#777'],
borderColor: ['#ccc', '#333'],
mailboxesTopBackgroundColor: ['#fdfdf1', 'rgb(26, 18, 0)'],
msglistBackgroundColor: ['#f5ffff', 'rgb(4, 19, 13)'],
boxShadow: ['0 0 20px rgba(0, 0, 0, 0.1)', '0px 0px 20px #000'],
buttonBackground: ['#eee', '#222'],
buttonBorderColor: ['#888', '#666'],
buttonHoverBackground: ['#ddd', '#333'],
overlayOpaqueBackgroundColor: ['#eee', '#011'],
overlayBackgroundColor: ['rgba(0, 0, 0, 0.2)', 'rgba(0, 0, 0, 0.5)'],
popupColor: ['black', 'white'],
popupBackgroundColor: ['white', 'rgb(49, 50, 51)'],
popupBorderColor: ['#ccc', '#555'],
highlightBackground: ['gold', '#a70167'],
highlightBorderColor: ['#8c7600', 'rgb(253, 31, 167)'],
highlightBackgroundHover: ['#ffbd21', 'rgb(113, 4, 71)'],
mailboxActiveBackground: ['linear-gradient(135deg, #ffc7ab 0%, #ffdeab 100%)', 'linear-gradient(135deg, rgb(182, 61, 0) 0%, rgb(140, 90, 13) 100%)'],
mailboxHoverBackgroundColor: ['#eee', 'rgb(66, 31, 21)'],
msgItemActiveBackground: ['linear-gradient(135deg, #8bc8ff 0%, #8ee5ff 100%)', 'linear-gradient(135deg, rgb(4, 92, 172) 0%, rgb(2, 123, 160) 100%)'],
msgItemHoverBackgroundColor: ['#eee', 'rgb(7, 51, 72)'],
msgItemFocusBorderColor: ['#2685ff', '#2685ff'],
buttonTristateOnBackground: ['#c4ffa9', 'rgb(39, 126, 0)'],
buttonTristateOffBackground: ['#ffb192', 'rgb(191, 65, 15)'],
warningBackgroundColor: ['#ffca91', 'rgb(168, 87, 0)'],
successBackground: ['#d2f791', '#1fa204'],
emphasisBackground: ['#666', '#aaa'],
// We define css variables, making them easy to override.
ensureCSS(':root', {
'--color': ['black', '#ddd'],
'--colorMild': ['#555', '#bbb'],
'--colorMilder': ['#666', '#aaa'],
'--backgroundColor': ['white', '#222'],
'--backgroundColorMild': ['#f8f8f8', '#080808'],
'--backgroundColorMilder': ['#999', '#777'],
'--borderColor': ['#ccc', '#333'],
'--mailboxesTopBackgroundColor': ['#fdfdf1', '#1a1200'],
'--msglistBackgroundColor': ['#f5ffff', '#04130d'],
'--boxShadow': ['0 0 20px rgba(0, 0, 0, 0.1)', '0px 0px 20px #000'],
'--buttonBackground': ['#eee', '#222'],
'--buttonBorderColor': ['#888', '#666'],
'--buttonHoverBackground': ['#ddd', '#333'],
'--overlayOpaqueBackgroundColor': ['#eee', '#011'],
'--overlayBackgroundColor': ['rgba(0, 0, 0, 0.2)', 'rgba(0, 0, 0, 0.5)'],
'--popupColor': ['black', 'white'],
'--popupBackgroundColor': ['white', '#313233'],
'--popupBorderColor': ['#ccc', '#555'],
'--highlightBackground': ['gold', '#a70167'],
'--highlightBorderColor': ['#8c7600', '#fd1fa7'],
'--highlightBackgroundHover': ['#ffbd21', '#710447'],
'--mailboxActiveBackground': ['linear-gradient(135deg, #ffc7ab 0%, #ffdeab 100%)', 'linear-gradient(135deg, #b63d00 0%, #8c5a0d 100%)'],
'--mailboxHoverBackgroundColor': ['#eee', '#421f15'],
'--msgItemActiveBackground': ['linear-gradient(135deg, #8bc8ff 0%, #8ee5ff 100%)', 'linear-gradient(135deg, #045cac 0%, #027ba0 100%)'],
'--msgItemHoverBackgroundColor': ['#eee', '#073348'],
'--msgItemFocusBorderColor': ['#2685ff', '#2685ff'],
'--buttonTristateOnBackground': ['#c4ffa9', '#277e00'],
'--buttonTristateOffBackground': ['#ffb192', '#bf410f'],
'--warningBackgroundColor': ['#ffca91', '#a85700'],
'--successBackground': ['#d2f791', '#1fa204'],
'--emphasisBackground': ['#666', '#aaa'],
// For authentication/security results.
underlineGreen: '#50c40f',
underlineRed: '#e15d1c',
underlineBlue: '#09f',
underlineGrey: '#888',
'--underlineGreen': '#50c40f',
'--underlineRed': '#e15d1c',
'--underlineBlue': '#09f',
'--underlineGrey': '#888',
'--quoted1Color': ['#03828f', '#71f2ff'],
'--quoted2Color': ['#c7445c', '#ec4c4c'],
'--quoted3Color': ['#417c10', '#73e614'],
'--scriptSwitchUnderlineColor': ['#dca053', '#e88f1e'],
'--linkColor': ['#096bc2', '#63b6ff'],
'--linkVisitedColor': ['#0704c1', '#c763ff'],
});
// Typed way to reference a css variables. Kept from before used variables.
const styles = {
color: 'var(--color)',
colorMild: 'var(--colorMild)',
colorMilder: 'var(--colorMilder)',
backgroundColor: 'var(--backgroundColor)',
backgroundColorMild: 'var(--backgroundColorMild)',
backgroundColorMilder: 'var(--backgroundColorMilder)',
borderColor: 'var(--borderColor)',
mailboxesTopBackgroundColor: 'var(--mailboxesTopBackgroundColor)',
msglistBackgroundColor: 'var(--msglistBackgroundColor)',
boxShadow: 'var(--boxShadow)',
buttonBackground: 'var(--buttonBackground)',
buttonBorderColor: 'var(--buttonBorderColor)',
buttonHoverBackground: 'var(--buttonHoverBackground)',
overlayOpaqueBackgroundColor: 'var(--overlayOpaqueBackgroundColor)',
overlayBackgroundColor: 'var(--overlayBackgroundColor)',
popupColor: 'var(--popupColor)',
popupBackgroundColor: 'var(--popupBackgroundColor)',
popupBorderColor: 'var(--popupBorderColor)',
highlightBackground: 'var(--highlightBackground)',
highlightBorderColor: 'var(--highlightBorderColor)',
highlightBackgroundHover: 'var(--highlightBackgroundHover)',
mailboxActiveBackground: 'var(--mailboxActiveBackground)',
mailboxHoverBackgroundColor: 'var(--mailboxHoverBackgroundColor)',
msgItemActiveBackground: 'var(--msgItemActiveBackground)',
msgItemHoverBackgroundColor: 'var(--msgItemHoverBackgroundColor)',
msgItemFocusBorderColor: 'var(--msgItemFocusBorderColor)',
buttonTristateOnBackground: 'var(--buttonTristateOnBackground)',
buttonTristateOffBackground: 'var(--buttonTristateOffBackground)',
warningBackgroundColor: 'var(--warningBackgroundColor)',
successBackground: 'var(--successBackground)',
emphasisBackground: 'var(--emphasisBackground)',
// For authentication/security results.
underlineGreen: 'var(--underlineGreen)',
underlineRed: 'var(--underlineRed)',
underlineBlue: 'var(--underlineBlue)',
underlineGrey: 'var(--underlineGrey)',
quoted1Color: 'var(--quoted1Color)',
quoted2Color: 'var(--quoted2Color)',
quoted3Color: 'var(--quoted3Color)',
scriptSwitchUnderlineColor: 'var(--scriptSwitchUnderlineColor)',
linkColor: 'var(--linkColor)',
linkVisitedColor: 'var(--linkVisitedColor)',
};
const styleClasses = {
// For quoted text, with multiple levels of indentations.
quoted: [
css('quoted1', { color: ['#03828f', '#71f2ff'] }),
css('quoted2', { color: ['#c7445c', 'rgb(236, 76, 76)'] }),
css('quoted3', { color: ['#417c10', 'rgb(115, 230, 20)'] }), // blue
css('quoted1', { color: styles.quoted1Color }),
css('quoted2', { color: styles.quoted2Color }),
css('quoted3', { color: styles.quoted3Color }),
],
// When text switches between unicode scripts.
scriptswitch: css('scriptswitch', { textDecoration: 'underline 2px', textDecorationColor: ['#dca053', 'rgb(232, 143, 30)'] }),
scriptswitch: css('scriptswitch', { textDecoration: 'underline 2px', textDecorationColor: styles.scriptSwitchUnderlineColor }),
textMild: css('textMild', { color: styles.colorMild }),
// For keywords (also known as flags/labels/tags) on messages.
keyword: css('keyword', { padding: '0 .15em', borderRadius: '.15em', fontWeight: 'normal', fontSize: '.9em', margin: '0 .15em', whiteSpace: 'nowrap', background: styles.highlightBackground, color: styles.color, border: '1px solid', borderColor: styles.highlightBorderColor }),
@ -1168,15 +1222,15 @@ const styleClasses = {
ensureCSS('.msgHeaders td', { wordBreak: 'break-word' }); // Prevent horizontal scroll bar for long header values.
ensureCSS('.keyword.keywordCollapsed', { opacity: .75 }),
// Generic styling.
ensureCSS('*', { fontSize: 'inherit', fontFamily: "'ubuntu', 'lato', sans-serif", margin: 0, padding: 0, boxSizing: 'border-box' });
ensureCSS('html', { backgroundColor: 'var(--backgroundColor)', color: 'var(--color)' });
ensureCSS('*', { fontSize: 'inherit', fontFamily: "'ubuntu', 'lato', sans-serif", margin: 0, padding: 0, boxSizing: 'border-box' });
ensureCSS('.mono, .mono *', { fontFamily: "'ubuntu mono', monospace" });
ensureCSS('table td, table th', { padding: '.15em .25em' });
ensureCSS('.pad', { padding: '.5em' });
ensureCSS('iframe', { border: 0 });
ensureCSS('img, embed, video, iframe', { backgroundColor: 'white', color: 'black' });
ensureCSS(':root', { backgroundColor: styles.backgroundColor, color: styles.color });
ensureCSS('a', { color: ['rgb(9, 107, 194)', 'rgb(99, 182, 255)'] });
ensureCSS('a:visited', { color: ['rgb(7, 4, 193)', 'rgb(199, 99, 255)'] });
ensureCSS('a', { color: styles.linkColor });
ensureCSS('a:visited', { color: styles.linkVisitedColor });
// For message view with multiple inline elements (often a single text and multiple messages).
ensureCSS('.textmulti > *:nth-child(even)', { backgroundColor: ['#f4f4f4', '#141414'] });
ensureCSS('.textmulti > *', { padding: '2ex .5em', margin: '-.5em' /* compensate pad */ });
@ -1417,13 +1471,17 @@ const init = () => {
iframepath += '?sameorigin=true';
let iframe;
const page = document.getElementById('page');
dom._kids(page, dom.div(css('msgMeta', { backgroundColor: styles.backgroundColorMild, borderBottom: '1px solid', borderBottomColor: styles.borderColor }), msgheaderview, msgattachmentview), iframe = dom.iframe(attr.title('Message body.'), attr.src(iframepath), css('msgIframe', { width: '100%', height: '100%' }), function load() {
const root = dom.div(dom.div(css('msgMeta', { backgroundColor: styles.backgroundColorMild, borderBottom: '1px solid', borderBottomColor: styles.borderColor }), msgheaderview, msgattachmentview), iframe = dom.iframe(attr.title('Message body.'), attr.src(iframepath), css('msgIframe', { width: '100%', height: '100%' }), function load() {
// Note: we load the iframe content specifically in a way that fires the load event only when the content is fully rendered.
iframe.style.height = iframe.contentDocument.documentElement.scrollHeight + 'px';
if (window.location.hash === '#print') {
window.print();
}
}));
if (typeof moxBeforeDisplay !== 'undefined') {
moxBeforeDisplay(root);
}
dom._kids(page, root);
};
try {
init();

View File

@ -2,6 +2,8 @@
// Loaded from synchronous javascript.
declare let messageItem: api.MessageItem
// From customization script.
declare let moxBeforeDisplay: (root: HTMLElement) => void
const init = () => {
const mi = api.parser.MessageItem(messageItem)
@ -40,7 +42,7 @@ const init = () => {
let iframe: HTMLIFrameElement
const page = document.getElementById('page')!
dom._kids(page,
const root = dom.div(
dom.div(
css('msgMeta', {backgroundColor: styles.backgroundColorMild, borderBottom: '1px solid', borderBottomColor: styles.borderColor}),
msgheaderview,
@ -59,6 +61,10 @@ const init = () => {
},
)
)
if (typeof moxBeforeDisplay !== 'undefined') {
moxBeforeDisplay(root)
}
dom._kids(page, root)
}
try {

View File

@ -3,10 +3,17 @@
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<style>
/* css placeholder */
</style>
</head>
<body>
<div id="page" style="opacity: .1">Loading...</div>
<script>
/* js placeholder */
</script>
<!-- Load message data synchronously to generate a meaningful 'loaded' event, used by webmailmsg.html for updating the iframe height . -->
<script src="parsedmessage.js"></script>

View File

@ -1055,10 +1055,10 @@ var api;
// instances of a class.
// We keep the default/regular styles and dark-mode styles in separate stylesheets.
const cssStyle = dom.style(attr.type('text/css'));
document.head.appendChild(cssStyle);
document.head.prepend(cssStyle);
const styleSheet = cssStyle.sheet;
const cssStyleDark = dom.style(attr.type('text/css'));
document.head.appendChild(cssStyleDark);
document.head.prepend(cssStyleDark);
const styleSheetDark = cssStyleDark.sheet;
styleSheetDark.insertRule('@media (prefers-color-scheme: dark) {}');
const darkModeRule = styleSheetDark.cssRules[0];
@ -1085,8 +1085,11 @@ const ensureCSS = (selector, styles, important) => {
let darkst;
for (let [k, v] of Object.entries(styles)) {
// We've kept the camel-case in our code which we had from when we did "st[prop] =
// value". It is more convenient as object keys. So convert to kebab-case.
k = k.replace(/[A-Z]/g, s => '-' + s.toLowerCase());
// value". It is more convenient as object keys. So convert to kebab-case, but only
// if this is not a css property.
if (!k.startsWith('--')) {
k = k.replace(/[A-Z]/g, s => '-' + s.toLowerCase());
}
if (Array.isArray(v)) {
if (v.length !== 2) {
throw new Error('2 elements required for light/dark mode style, got ' + v.length);
@ -1112,54 +1115,105 @@ const css = (className, styles, important) => {
};
// todo: reduce number of colors. hopefully we can derive some colors from a few base colors (making them brighter/darker, or shifting hue, etc). then make them configurable through settings.
// todo: add the standard padding and border-radius, perhaps more.
// todo: could make some of these {prop: value} objects and pass them directly to css()
const styles = {
color: ['black', '#ddd'],
colorMild: ['#555', '#bbb'],
colorMilder: ['#666', '#aaa'],
backgroundColor: ['white', '#222'],
backgroundColorMild: ['#f8f8f8', '#080808'],
backgroundColorMilder: ['#999', '#777'],
borderColor: ['#ccc', '#333'],
mailboxesTopBackgroundColor: ['#fdfdf1', 'rgb(26, 18, 0)'],
msglistBackgroundColor: ['#f5ffff', 'rgb(4, 19, 13)'],
boxShadow: ['0 0 20px rgba(0, 0, 0, 0.1)', '0px 0px 20px #000'],
buttonBackground: ['#eee', '#222'],
buttonBorderColor: ['#888', '#666'],
buttonHoverBackground: ['#ddd', '#333'],
overlayOpaqueBackgroundColor: ['#eee', '#011'],
overlayBackgroundColor: ['rgba(0, 0, 0, 0.2)', 'rgba(0, 0, 0, 0.5)'],
popupColor: ['black', 'white'],
popupBackgroundColor: ['white', 'rgb(49, 50, 51)'],
popupBorderColor: ['#ccc', '#555'],
highlightBackground: ['gold', '#a70167'],
highlightBorderColor: ['#8c7600', 'rgb(253, 31, 167)'],
highlightBackgroundHover: ['#ffbd21', 'rgb(113, 4, 71)'],
mailboxActiveBackground: ['linear-gradient(135deg, #ffc7ab 0%, #ffdeab 100%)', 'linear-gradient(135deg, rgb(182, 61, 0) 0%, rgb(140, 90, 13) 100%)'],
mailboxHoverBackgroundColor: ['#eee', 'rgb(66, 31, 21)'],
msgItemActiveBackground: ['linear-gradient(135deg, #8bc8ff 0%, #8ee5ff 100%)', 'linear-gradient(135deg, rgb(4, 92, 172) 0%, rgb(2, 123, 160) 100%)'],
msgItemHoverBackgroundColor: ['#eee', 'rgb(7, 51, 72)'],
msgItemFocusBorderColor: ['#2685ff', '#2685ff'],
buttonTristateOnBackground: ['#c4ffa9', 'rgb(39, 126, 0)'],
buttonTristateOffBackground: ['#ffb192', 'rgb(191, 65, 15)'],
warningBackgroundColor: ['#ffca91', 'rgb(168, 87, 0)'],
successBackground: ['#d2f791', '#1fa204'],
emphasisBackground: ['#666', '#aaa'],
// We define css variables, making them easy to override.
ensureCSS(':root', {
'--color': ['black', '#ddd'],
'--colorMild': ['#555', '#bbb'],
'--colorMilder': ['#666', '#aaa'],
'--backgroundColor': ['white', '#222'],
'--backgroundColorMild': ['#f8f8f8', '#080808'],
'--backgroundColorMilder': ['#999', '#777'],
'--borderColor': ['#ccc', '#333'],
'--mailboxesTopBackgroundColor': ['#fdfdf1', '#1a1200'],
'--msglistBackgroundColor': ['#f5ffff', '#04130d'],
'--boxShadow': ['0 0 20px rgba(0, 0, 0, 0.1)', '0px 0px 20px #000'],
'--buttonBackground': ['#eee', '#222'],
'--buttonBorderColor': ['#888', '#666'],
'--buttonHoverBackground': ['#ddd', '#333'],
'--overlayOpaqueBackgroundColor': ['#eee', '#011'],
'--overlayBackgroundColor': ['rgba(0, 0, 0, 0.2)', 'rgba(0, 0, 0, 0.5)'],
'--popupColor': ['black', 'white'],
'--popupBackgroundColor': ['white', '#313233'],
'--popupBorderColor': ['#ccc', '#555'],
'--highlightBackground': ['gold', '#a70167'],
'--highlightBorderColor': ['#8c7600', '#fd1fa7'],
'--highlightBackgroundHover': ['#ffbd21', '#710447'],
'--mailboxActiveBackground': ['linear-gradient(135deg, #ffc7ab 0%, #ffdeab 100%)', 'linear-gradient(135deg, #b63d00 0%, #8c5a0d 100%)'],
'--mailboxHoverBackgroundColor': ['#eee', '#421f15'],
'--msgItemActiveBackground': ['linear-gradient(135deg, #8bc8ff 0%, #8ee5ff 100%)', 'linear-gradient(135deg, #045cac 0%, #027ba0 100%)'],
'--msgItemHoverBackgroundColor': ['#eee', '#073348'],
'--msgItemFocusBorderColor': ['#2685ff', '#2685ff'],
'--buttonTristateOnBackground': ['#c4ffa9', '#277e00'],
'--buttonTristateOffBackground': ['#ffb192', '#bf410f'],
'--warningBackgroundColor': ['#ffca91', '#a85700'],
'--successBackground': ['#d2f791', '#1fa204'],
'--emphasisBackground': ['#666', '#aaa'],
// For authentication/security results.
underlineGreen: '#50c40f',
underlineRed: '#e15d1c',
underlineBlue: '#09f',
underlineGrey: '#888',
'--underlineGreen': '#50c40f',
'--underlineRed': '#e15d1c',
'--underlineBlue': '#09f',
'--underlineGrey': '#888',
'--quoted1Color': ['#03828f', '#71f2ff'],
'--quoted2Color': ['#c7445c', '#ec4c4c'],
'--quoted3Color': ['#417c10', '#73e614'],
'--scriptSwitchUnderlineColor': ['#dca053', '#e88f1e'],
'--linkColor': ['#096bc2', '#63b6ff'],
'--linkVisitedColor': ['#0704c1', '#c763ff'],
});
// Typed way to reference a css variables. Kept from before used variables.
const styles = {
color: 'var(--color)',
colorMild: 'var(--colorMild)',
colorMilder: 'var(--colorMilder)',
backgroundColor: 'var(--backgroundColor)',
backgroundColorMild: 'var(--backgroundColorMild)',
backgroundColorMilder: 'var(--backgroundColorMilder)',
borderColor: 'var(--borderColor)',
mailboxesTopBackgroundColor: 'var(--mailboxesTopBackgroundColor)',
msglistBackgroundColor: 'var(--msglistBackgroundColor)',
boxShadow: 'var(--boxShadow)',
buttonBackground: 'var(--buttonBackground)',
buttonBorderColor: 'var(--buttonBorderColor)',
buttonHoverBackground: 'var(--buttonHoverBackground)',
overlayOpaqueBackgroundColor: 'var(--overlayOpaqueBackgroundColor)',
overlayBackgroundColor: 'var(--overlayBackgroundColor)',
popupColor: 'var(--popupColor)',
popupBackgroundColor: 'var(--popupBackgroundColor)',
popupBorderColor: 'var(--popupBorderColor)',
highlightBackground: 'var(--highlightBackground)',
highlightBorderColor: 'var(--highlightBorderColor)',
highlightBackgroundHover: 'var(--highlightBackgroundHover)',
mailboxActiveBackground: 'var(--mailboxActiveBackground)',
mailboxHoverBackgroundColor: 'var(--mailboxHoverBackgroundColor)',
msgItemActiveBackground: 'var(--msgItemActiveBackground)',
msgItemHoverBackgroundColor: 'var(--msgItemHoverBackgroundColor)',
msgItemFocusBorderColor: 'var(--msgItemFocusBorderColor)',
buttonTristateOnBackground: 'var(--buttonTristateOnBackground)',
buttonTristateOffBackground: 'var(--buttonTristateOffBackground)',
warningBackgroundColor: 'var(--warningBackgroundColor)',
successBackground: 'var(--successBackground)',
emphasisBackground: 'var(--emphasisBackground)',
// For authentication/security results.
underlineGreen: 'var(--underlineGreen)',
underlineRed: 'var(--underlineRed)',
underlineBlue: 'var(--underlineBlue)',
underlineGrey: 'var(--underlineGrey)',
quoted1Color: 'var(--quoted1Color)',
quoted2Color: 'var(--quoted2Color)',
quoted3Color: 'var(--quoted3Color)',
scriptSwitchUnderlineColor: 'var(--scriptSwitchUnderlineColor)',
linkColor: 'var(--linkColor)',
linkVisitedColor: 'var(--linkVisitedColor)',
};
const styleClasses = {
// For quoted text, with multiple levels of indentations.
quoted: [
css('quoted1', { color: ['#03828f', '#71f2ff'] }),
css('quoted2', { color: ['#c7445c', 'rgb(236, 76, 76)'] }),
css('quoted3', { color: ['#417c10', 'rgb(115, 230, 20)'] }), // blue
css('quoted1', { color: styles.quoted1Color }),
css('quoted2', { color: styles.quoted2Color }),
css('quoted3', { color: styles.quoted3Color }),
],
// When text switches between unicode scripts.
scriptswitch: css('scriptswitch', { textDecoration: 'underline 2px', textDecorationColor: ['#dca053', 'rgb(232, 143, 30)'] }),
scriptswitch: css('scriptswitch', { textDecoration: 'underline 2px', textDecorationColor: styles.scriptSwitchUnderlineColor }),
textMild: css('textMild', { color: styles.colorMild }),
// For keywords (also known as flags/labels/tags) on messages.
keyword: css('keyword', { padding: '0 .15em', borderRadius: '.15em', fontWeight: 'normal', fontSize: '.9em', margin: '0 .15em', whiteSpace: 'nowrap', background: styles.highlightBackground, color: styles.color, border: '1px solid', borderColor: styles.highlightBorderColor }),
@ -1168,15 +1222,15 @@ const styleClasses = {
ensureCSS('.msgHeaders td', { wordBreak: 'break-word' }); // Prevent horizontal scroll bar for long header values.
ensureCSS('.keyword.keywordCollapsed', { opacity: .75 }),
// Generic styling.
ensureCSS('*', { fontSize: 'inherit', fontFamily: "'ubuntu', 'lato', sans-serif", margin: 0, padding: 0, boxSizing: 'border-box' });
ensureCSS('html', { backgroundColor: 'var(--backgroundColor)', color: 'var(--color)' });
ensureCSS('*', { fontSize: 'inherit', fontFamily: "'ubuntu', 'lato', sans-serif", margin: 0, padding: 0, boxSizing: 'border-box' });
ensureCSS('.mono, .mono *', { fontFamily: "'ubuntu mono', monospace" });
ensureCSS('table td, table th', { padding: '.15em .25em' });
ensureCSS('.pad', { padding: '.5em' });
ensureCSS('iframe', { border: 0 });
ensureCSS('img, embed, video, iframe', { backgroundColor: 'white', color: 'black' });
ensureCSS(':root', { backgroundColor: styles.backgroundColor, color: styles.color });
ensureCSS('a', { color: ['rgb(9, 107, 194)', 'rgb(99, 182, 255)'] });
ensureCSS('a:visited', { color: ['rgb(7, 4, 193)', 'rgb(199, 99, 255)'] });
ensureCSS('a', { color: styles.linkColor });
ensureCSS('a:visited', { color: styles.linkVisitedColor });
// For message view with multiple inline elements (often a single text and multiple messages).
ensureCSS('.textmulti > *:nth-child(even)', { backgroundColor: ['#f4f4f4', '#141414'] });
ensureCSS('.textmulti > *', { padding: '2ex .5em', margin: '-.5em' /* compensate pad */ });
@ -1392,10 +1446,14 @@ const loadMsgheaderView = (msgheaderelem, mi, moreHeaders, refineKeyword, allAdd
const init = async () => {
const pm = api.parser.ParsedMessage(parsedMessage);
const mi = api.parser.MessageItem(messageItem);
dom._kids(document.body, dom.div(dom._class('pad', 'mono', 'textmulti'), css('msgTextPreformatted', { whiteSpace: 'pre-wrap' }), (pm.Texts || []).map(t => renderText(t.replace(/\r\n/g, '\n'))), (mi.Attachments || []).filter(f => isImage(f)).map(f => {
const root = dom.div(dom.div(dom._class('pad', 'mono', 'textmulti'), css('msgTextPreformatted', { whiteSpace: 'pre-wrap' }), (pm.Texts || []).map(t => renderText(t.replace(/\r\n/g, '\n'))), (mi.Attachments || []).filter(f => isImage(f)).map(f => {
const pathStr = [0].concat(f.Path || []).join('.');
return dom.div(dom.div(css('msgAttachment', { flexGrow: 1, display: 'flex', alignItems: 'center', justifyContent: 'center', maxHeight: 'calc(100% - 50px)' }), dom.img(attr.src('view/' + pathStr), attr.title(f.Filename), css('msgAttachmentImage', { maxWidth: '100%', maxHeight: '100%', boxShadow: styles.boxShadow }))));
})));
if (typeof moxBeforeDisplay !== 'undefined') {
moxBeforeDisplay(root);
}
dom._kids(document.body, root);
};
init()
.catch((err) => {

View File

@ -3,11 +3,13 @@
// Loaded from synchronous javascript.
declare let messageItem: api.MessageItem
declare let parsedMessage: api.ParsedMessage
// From customization script.
declare let moxBeforeDisplay: (root: HTMLElement) => void
const init = async () => {
const pm = api.parser.ParsedMessage(parsedMessage)
const mi = api.parser.MessageItem(messageItem)
dom._kids(document.body,
const root = dom.div(
dom.div(dom._class('pad', 'mono', 'textmulti'),
css('msgTextPreformatted', {whiteSpace: 'pre-wrap'}),
(pm.Texts || []).map(t => renderText(t.replace(/\r\n/g, '\n'))),
@ -26,6 +28,10 @@ const init = async () => {
}),
)
)
if (typeof moxBeforeDisplay !== 'undefined') {
moxBeforeDisplay(root)
}
dom._kids(document.body, root)
}
init()

View File

@ -12,6 +12,7 @@ import (
"errors"
"fmt"
"io"
"io/fs"
"log/slog"
"mime"
"net/http"
@ -21,6 +22,7 @@ import (
"runtime/debug"
"strconv"
"strings"
"time"
_ "embed"
@ -147,27 +149,62 @@ func xdbread(ctx context.Context, acc *store.Account, fn func(tx *bstore.Tx)) {
}
var webmailFile = &mox.WebappFile{
HTML: webmailHTML,
JS: webmailJS,
HTMLPath: filepath.FromSlash("webmail/webmail.html"),
JSPath: filepath.FromSlash("webmail/webmail.js"),
HTML: webmailHTML,
JS: webmailJS,
HTMLPath: filepath.FromSlash("webmail/webmail.html"),
JSPath: filepath.FromSlash("webmail/webmail.js"),
CustomStem: "webmail",
}
// Serve content, either from a file, or return the fallback data. Caller
// should already have set the content-type. We use this to return a file from
// the local file system (during development), or embedded in the binary (when
// deployed).
func serveContentFallback(log mlog.Log, w http.ResponseWriter, r *http.Request, path string, fallback []byte) {
func customization() (css, js []byte, err error) {
if css, err = os.ReadFile(mox.ConfigDirPath("webmail.css")); err != nil && !errors.Is(err, fs.ErrNotExist) {
return nil, nil, err
}
if js, err = os.ReadFile(mox.ConfigDirPath("webmail.js")); err != nil && !errors.Is(err, fs.ErrNotExist) {
return nil, nil, err
}
css = append([]byte("/* Custom CSS by admin from $configdir/webmail.css: */\n"), css...)
js = append([]byte("// Custom JS by admin from $configdir/webmail.js:\n"), js...)
js = append(js, '\n')
return css, js, nil
}
// Serve HTML content, either from a file, or return the fallback data. If
// customize is set, css/js is inserted if configured. Caller should already have
// set the content-type. We use this to return a file from the local file system
// (during development), or embedded in the binary (when deployed).
func serveContentFallback(log mlog.Log, w http.ResponseWriter, r *http.Request, path string, fallback []byte, customize bool) {
serve := func(mtime time.Time, rd io.ReadSeeker) {
if customize {
buf, err := io.ReadAll(rd)
if err != nil {
log.Errorx("reading content to customize", err)
http.Error(w, "500 - internal server error - reading content to customize", http.StatusInternalServerError)
return
}
customCSS, customJS, err := customization()
if err != nil {
log.Errorx("reading customizations", err)
http.Error(w, "500 - internal server error - reading customizations", http.StatusInternalServerError)
return
}
buf = bytes.Replace(buf, []byte("/* css placeholder */"), customCSS, 1)
buf = bytes.Replace(buf, []byte("/* js placeholder */"), customJS, 1)
rd = bytes.NewReader(buf)
}
http.ServeContent(w, r, "", mtime, rd)
}
f, err := os.Open(path)
if err == nil {
defer f.Close()
st, err := f.Stat()
if err == nil {
http.ServeContent(w, r, "", st.ModTime(), f)
serve(st.ModTime(), f)
return
}
}
http.ServeContent(w, r, "", mox.FallbackMtime(log), bytes.NewReader(fallback))
serve(mox.FallbackMtime(log), bytes.NewReader(fallback))
}
func init() {
@ -261,7 +298,7 @@ func handle(apiHandler http.Handler, isForwarded bool, accountPath string, w htt
}
w.Header().Set("Content-Type", "application/javascript; charset=utf-8")
serveContentFallback(log, w, r, path, fallback)
serveContentFallback(log, w, r, path, fallback, false)
return
}
@ -621,7 +658,7 @@ func handle(apiHandler http.Handler, isForwarded bool, accountPath string, w htt
path := filepath.FromSlash("webmail/msg.html")
fallback := webmailmsgHTML
serveContentFallback(log, w, r, path, fallback)
serveContentFallback(log, w, r, path, fallback, true)
case len(t) == 2 && t[1] == "parsedmessage.js":
// Used by msg.html, for the msg* endpoints, for the data needed to show all data
@ -689,7 +726,7 @@ func handle(apiHandler http.Handler, isForwarded bool, accountPath string, w htt
// from disk.
path := filepath.FromSlash("webmail/text.html")
fallback := webmailtextHTML
serveContentFallback(log, w, r, path, fallback)
serveContentFallback(log, w, r, path, fallback, true)
case len(t) == 2 && (t[1] == "html" || t[1] == "htmlexternal"):
// Returns the first HTML part, with "cid:" URIs replaced with an inlined datauri

View File

@ -14,10 +14,14 @@ fieldset { border: 0; }
@keyframes fadein { 0% { opacity: 0 } 100% { opacity: 1 } }
@keyframes fadeout { 0% { opacity: 1 } 100% { opacity: 0.1 } }
.invert { filter: invert(100%); }
/* css placeholder */
</style>
</head>
<body>
<div id="page"><div style="padding: 1em; text-align: center">Loading...</div></div>
<script>/* placeholder */</script>
<script>
/* js placeholder */
</script>
</body>
</html>

View File

@ -1055,10 +1055,10 @@ var api;
// instances of a class.
// We keep the default/regular styles and dark-mode styles in separate stylesheets.
const cssStyle = dom.style(attr.type('text/css'));
document.head.appendChild(cssStyle);
document.head.prepend(cssStyle);
const styleSheet = cssStyle.sheet;
const cssStyleDark = dom.style(attr.type('text/css'));
document.head.appendChild(cssStyleDark);
document.head.prepend(cssStyleDark);
const styleSheetDark = cssStyleDark.sheet;
styleSheetDark.insertRule('@media (prefers-color-scheme: dark) {}');
const darkModeRule = styleSheetDark.cssRules[0];
@ -1085,8 +1085,11 @@ const ensureCSS = (selector, styles, important) => {
let darkst;
for (let [k, v] of Object.entries(styles)) {
// We've kept the camel-case in our code which we had from when we did "st[prop] =
// value". It is more convenient as object keys. So convert to kebab-case.
k = k.replace(/[A-Z]/g, s => '-' + s.toLowerCase());
// value". It is more convenient as object keys. So convert to kebab-case, but only
// if this is not a css property.
if (!k.startsWith('--')) {
k = k.replace(/[A-Z]/g, s => '-' + s.toLowerCase());
}
if (Array.isArray(v)) {
if (v.length !== 2) {
throw new Error('2 elements required for light/dark mode style, got ' + v.length);
@ -1112,54 +1115,105 @@ const css = (className, styles, important) => {
};
// todo: reduce number of colors. hopefully we can derive some colors from a few base colors (making them brighter/darker, or shifting hue, etc). then make them configurable through settings.
// todo: add the standard padding and border-radius, perhaps more.
// todo: could make some of these {prop: value} objects and pass them directly to css()
const styles = {
color: ['black', '#ddd'],
colorMild: ['#555', '#bbb'],
colorMilder: ['#666', '#aaa'],
backgroundColor: ['white', '#222'],
backgroundColorMild: ['#f8f8f8', '#080808'],
backgroundColorMilder: ['#999', '#777'],
borderColor: ['#ccc', '#333'],
mailboxesTopBackgroundColor: ['#fdfdf1', 'rgb(26, 18, 0)'],
msglistBackgroundColor: ['#f5ffff', 'rgb(4, 19, 13)'],
boxShadow: ['0 0 20px rgba(0, 0, 0, 0.1)', '0px 0px 20px #000'],
buttonBackground: ['#eee', '#222'],
buttonBorderColor: ['#888', '#666'],
buttonHoverBackground: ['#ddd', '#333'],
overlayOpaqueBackgroundColor: ['#eee', '#011'],
overlayBackgroundColor: ['rgba(0, 0, 0, 0.2)', 'rgba(0, 0, 0, 0.5)'],
popupColor: ['black', 'white'],
popupBackgroundColor: ['white', 'rgb(49, 50, 51)'],
popupBorderColor: ['#ccc', '#555'],
highlightBackground: ['gold', '#a70167'],
highlightBorderColor: ['#8c7600', 'rgb(253, 31, 167)'],
highlightBackgroundHover: ['#ffbd21', 'rgb(113, 4, 71)'],
mailboxActiveBackground: ['linear-gradient(135deg, #ffc7ab 0%, #ffdeab 100%)', 'linear-gradient(135deg, rgb(182, 61, 0) 0%, rgb(140, 90, 13) 100%)'],
mailboxHoverBackgroundColor: ['#eee', 'rgb(66, 31, 21)'],
msgItemActiveBackground: ['linear-gradient(135deg, #8bc8ff 0%, #8ee5ff 100%)', 'linear-gradient(135deg, rgb(4, 92, 172) 0%, rgb(2, 123, 160) 100%)'],
msgItemHoverBackgroundColor: ['#eee', 'rgb(7, 51, 72)'],
msgItemFocusBorderColor: ['#2685ff', '#2685ff'],
buttonTristateOnBackground: ['#c4ffa9', 'rgb(39, 126, 0)'],
buttonTristateOffBackground: ['#ffb192', 'rgb(191, 65, 15)'],
warningBackgroundColor: ['#ffca91', 'rgb(168, 87, 0)'],
successBackground: ['#d2f791', '#1fa204'],
emphasisBackground: ['#666', '#aaa'],
// We define css variables, making them easy to override.
ensureCSS(':root', {
'--color': ['black', '#ddd'],
'--colorMild': ['#555', '#bbb'],
'--colorMilder': ['#666', '#aaa'],
'--backgroundColor': ['white', '#222'],
'--backgroundColorMild': ['#f8f8f8', '#080808'],
'--backgroundColorMilder': ['#999', '#777'],
'--borderColor': ['#ccc', '#333'],
'--mailboxesTopBackgroundColor': ['#fdfdf1', '#1a1200'],
'--msglistBackgroundColor': ['#f5ffff', '#04130d'],
'--boxShadow': ['0 0 20px rgba(0, 0, 0, 0.1)', '0px 0px 20px #000'],
'--buttonBackground': ['#eee', '#222'],
'--buttonBorderColor': ['#888', '#666'],
'--buttonHoverBackground': ['#ddd', '#333'],
'--overlayOpaqueBackgroundColor': ['#eee', '#011'],
'--overlayBackgroundColor': ['rgba(0, 0, 0, 0.2)', 'rgba(0, 0, 0, 0.5)'],
'--popupColor': ['black', 'white'],
'--popupBackgroundColor': ['white', '#313233'],
'--popupBorderColor': ['#ccc', '#555'],
'--highlightBackground': ['gold', '#a70167'],
'--highlightBorderColor': ['#8c7600', '#fd1fa7'],
'--highlightBackgroundHover': ['#ffbd21', '#710447'],
'--mailboxActiveBackground': ['linear-gradient(135deg, #ffc7ab 0%, #ffdeab 100%)', 'linear-gradient(135deg, #b63d00 0%, #8c5a0d 100%)'],
'--mailboxHoverBackgroundColor': ['#eee', '#421f15'],
'--msgItemActiveBackground': ['linear-gradient(135deg, #8bc8ff 0%, #8ee5ff 100%)', 'linear-gradient(135deg, #045cac 0%, #027ba0 100%)'],
'--msgItemHoverBackgroundColor': ['#eee', '#073348'],
'--msgItemFocusBorderColor': ['#2685ff', '#2685ff'],
'--buttonTristateOnBackground': ['#c4ffa9', '#277e00'],
'--buttonTristateOffBackground': ['#ffb192', '#bf410f'],
'--warningBackgroundColor': ['#ffca91', '#a85700'],
'--successBackground': ['#d2f791', '#1fa204'],
'--emphasisBackground': ['#666', '#aaa'],
// For authentication/security results.
underlineGreen: '#50c40f',
underlineRed: '#e15d1c',
underlineBlue: '#09f',
underlineGrey: '#888',
'--underlineGreen': '#50c40f',
'--underlineRed': '#e15d1c',
'--underlineBlue': '#09f',
'--underlineGrey': '#888',
'--quoted1Color': ['#03828f', '#71f2ff'],
'--quoted2Color': ['#c7445c', '#ec4c4c'],
'--quoted3Color': ['#417c10', '#73e614'],
'--scriptSwitchUnderlineColor': ['#dca053', '#e88f1e'],
'--linkColor': ['#096bc2', '#63b6ff'],
'--linkVisitedColor': ['#0704c1', '#c763ff'],
});
// Typed way to reference a css variables. Kept from before used variables.
const styles = {
color: 'var(--color)',
colorMild: 'var(--colorMild)',
colorMilder: 'var(--colorMilder)',
backgroundColor: 'var(--backgroundColor)',
backgroundColorMild: 'var(--backgroundColorMild)',
backgroundColorMilder: 'var(--backgroundColorMilder)',
borderColor: 'var(--borderColor)',
mailboxesTopBackgroundColor: 'var(--mailboxesTopBackgroundColor)',
msglistBackgroundColor: 'var(--msglistBackgroundColor)',
boxShadow: 'var(--boxShadow)',
buttonBackground: 'var(--buttonBackground)',
buttonBorderColor: 'var(--buttonBorderColor)',
buttonHoverBackground: 'var(--buttonHoverBackground)',
overlayOpaqueBackgroundColor: 'var(--overlayOpaqueBackgroundColor)',
overlayBackgroundColor: 'var(--overlayBackgroundColor)',
popupColor: 'var(--popupColor)',
popupBackgroundColor: 'var(--popupBackgroundColor)',
popupBorderColor: 'var(--popupBorderColor)',
highlightBackground: 'var(--highlightBackground)',
highlightBorderColor: 'var(--highlightBorderColor)',
highlightBackgroundHover: 'var(--highlightBackgroundHover)',
mailboxActiveBackground: 'var(--mailboxActiveBackground)',
mailboxHoverBackgroundColor: 'var(--mailboxHoverBackgroundColor)',
msgItemActiveBackground: 'var(--msgItemActiveBackground)',
msgItemHoverBackgroundColor: 'var(--msgItemHoverBackgroundColor)',
msgItemFocusBorderColor: 'var(--msgItemFocusBorderColor)',
buttonTristateOnBackground: 'var(--buttonTristateOnBackground)',
buttonTristateOffBackground: 'var(--buttonTristateOffBackground)',
warningBackgroundColor: 'var(--warningBackgroundColor)',
successBackground: 'var(--successBackground)',
emphasisBackground: 'var(--emphasisBackground)',
// For authentication/security results.
underlineGreen: 'var(--underlineGreen)',
underlineRed: 'var(--underlineRed)',
underlineBlue: 'var(--underlineBlue)',
underlineGrey: 'var(--underlineGrey)',
quoted1Color: 'var(--quoted1Color)',
quoted2Color: 'var(--quoted2Color)',
quoted3Color: 'var(--quoted3Color)',
scriptSwitchUnderlineColor: 'var(--scriptSwitchUnderlineColor)',
linkColor: 'var(--linkColor)',
linkVisitedColor: 'var(--linkVisitedColor)',
};
const styleClasses = {
// For quoted text, with multiple levels of indentations.
quoted: [
css('quoted1', { color: ['#03828f', '#71f2ff'] }),
css('quoted2', { color: ['#c7445c', 'rgb(236, 76, 76)'] }),
css('quoted3', { color: ['#417c10', 'rgb(115, 230, 20)'] }), // blue
css('quoted1', { color: styles.quoted1Color }),
css('quoted2', { color: styles.quoted2Color }),
css('quoted3', { color: styles.quoted3Color }),
],
// When text switches between unicode scripts.
scriptswitch: css('scriptswitch', { textDecoration: 'underline 2px', textDecorationColor: ['#dca053', 'rgb(232, 143, 30)'] }),
scriptswitch: css('scriptswitch', { textDecoration: 'underline 2px', textDecorationColor: styles.scriptSwitchUnderlineColor }),
textMild: css('textMild', { color: styles.colorMild }),
// For keywords (also known as flags/labels/tags) on messages.
keyword: css('keyword', { padding: '0 .15em', borderRadius: '.15em', fontWeight: 'normal', fontSize: '.9em', margin: '0 .15em', whiteSpace: 'nowrap', background: styles.highlightBackground, color: styles.color, border: '1px solid', borderColor: styles.highlightBorderColor }),
@ -1168,15 +1222,15 @@ const styleClasses = {
ensureCSS('.msgHeaders td', { wordBreak: 'break-word' }); // Prevent horizontal scroll bar for long header values.
ensureCSS('.keyword.keywordCollapsed', { opacity: .75 }),
// Generic styling.
ensureCSS('*', { fontSize: 'inherit', fontFamily: "'ubuntu', 'lato', sans-serif", margin: 0, padding: 0, boxSizing: 'border-box' });
ensureCSS('html', { backgroundColor: 'var(--backgroundColor)', color: 'var(--color)' });
ensureCSS('*', { fontSize: 'inherit', fontFamily: "'ubuntu', 'lato', sans-serif", margin: 0, padding: 0, boxSizing: 'border-box' });
ensureCSS('.mono, .mono *', { fontFamily: "'ubuntu mono', monospace" });
ensureCSS('table td, table th', { padding: '.15em .25em' });
ensureCSS('.pad', { padding: '.5em' });
ensureCSS('iframe', { border: 0 });
ensureCSS('img, embed, video, iframe', { backgroundColor: 'white', color: 'black' });
ensureCSS(':root', { backgroundColor: styles.backgroundColor, color: styles.color });
ensureCSS('a', { color: ['rgb(9, 107, 194)', 'rgb(99, 182, 255)'] });
ensureCSS('a:visited', { color: ['rgb(7, 4, 193)', 'rgb(199, 99, 255)'] });
ensureCSS('a', { color: styles.linkColor });
ensureCSS('a:visited', { color: styles.linkVisitedColor });
// For message view with multiple inline elements (often a single text and multiple messages).
ensureCSS('.textmulti > *:nth-child(even)', { backgroundColor: ['#f4f4f4', '#141414'] });
ensureCSS('.textmulti > *', { padding: '2ex .5em', margin: '-.5em' /* compensate pad */ });
@ -6781,6 +6835,9 @@ const init = async () => {
else {
selectLayout(layoutElem.value);
}
if (window.moxBeforeDisplay) {
moxBeforeDisplay(webmailroot);
}
dom._kids(page, webmailroot);
checkMsglistWidth();
window.addEventListener('resize', function () {

View File

@ -133,6 +133,8 @@ declare let page: HTMLElement
declare let moxversion: string
declare let moxgoos: string
declare let moxgoarch: string
// From customization script.
declare let moxBeforeDisplay: (root: HTMLElement) => void
// All logging goes through log() instead of console.log, except "should not happen" logging.
let log: (...args: any[]) => void = () => {}
@ -7057,6 +7059,9 @@ const init = async () => {
} else {
selectLayout(layoutElem.value)
}
if ((window as any).moxBeforeDisplay) {
moxBeforeDisplay(webmailroot)
}
dom._kids(page, webmailroot)
checkMsglistWidth()