domma-cms 1.6.58 → 1.6.59
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json
CHANGED
|
@@ -76,6 +76,7 @@ import {applyTemplate, expandTemplate} from './templates.js';
|
|
|
76
76
|
import {makeResizable} from './resizable.js';
|
|
77
77
|
import {whenClosed} from './when-closed.js';
|
|
78
78
|
import {esc} from './reader-kit.js';
|
|
79
|
+
import {installFontControls} from './font-controls.js';
|
|
79
80
|
import {
|
|
80
81
|
collectGroups,
|
|
81
82
|
currentFragment,
|
|
@@ -568,10 +569,12 @@ export function openCompose({
|
|
|
568
569
|
bodyInput.value = draft.body ?? '';
|
|
569
570
|
|
|
570
571
|
let editor = null;
|
|
572
|
+
let fontControls = null;
|
|
571
573
|
if (typeof E?.editor === 'function') {
|
|
572
574
|
try {
|
|
573
575
|
editor = E.editor(editorHost, {mode: 'rich', height: '34vh'});
|
|
574
576
|
editor.setValue(draft.html || textToHtml(draft.body ?? ''));
|
|
577
|
+
try { fontControls = installFontControls(editor); } catch { fontControls = null; }
|
|
575
578
|
} catch {
|
|
576
579
|
editor = null;
|
|
577
580
|
}
|
|
@@ -930,6 +933,9 @@ export function openCompose({
|
|
|
930
933
|
// The editor is a live component with its own listeners and autosave.
|
|
931
934
|
whenClosed(slideover, () => {
|
|
932
935
|
closed = true;
|
|
936
|
+
// Before the editor: it closes an open menu (left on <body> by a
|
|
937
|
+
// Ctrl+Enter send) and drops its dropdowns and window listener.
|
|
938
|
+
fontControls?.destroy();
|
|
933
939
|
editor?.destroy?.();
|
|
934
940
|
stopResizing();
|
|
935
941
|
for (const suggester of suggesters) suggester.destroy?.();
|
|
@@ -0,0 +1,375 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Font and size menus for the mail editor (Domma's `E.editor`, rich mode).
|
|
3
|
+
*
|
|
4
|
+
* Domma's editor has no font controls of its own, so these are added to its
|
|
5
|
+
* toolbar here - the same way Domma's own editor extensions do, through the
|
|
6
|
+
* instance's `_toolbarEl` / `_editorEl`. The lists are `fonts.js`.
|
|
7
|
+
*
|
|
8
|
+
* The style is applied by hand, not with `execCommand('fontName')`: that
|
|
9
|
+
* command writes `<font>` tags, and re-styling part of an already-styled run
|
|
10
|
+
* makes the browser push the parent's style down as more `<font>` tags. The
|
|
11
|
+
* sanitiser drops `<font>` (so signatures and templates lost their faces) and
|
|
12
|
+
* mail carried them out. Instead every text node in the selection gets the
|
|
13
|
+
* property on its own span - an existing span it alone fills, or a new one -
|
|
14
|
+
* written as a plain string so the stack keeps its single quotes. With only a
|
|
15
|
+
* caret, an empty span (holding a zero-width space until something is typed)
|
|
16
|
+
* takes the style for what comes next. Any `<font>` that arrives anyway, by
|
|
17
|
+
* paste, is turned into a span on the next input.
|
|
18
|
+
*/
|
|
19
|
+
import {FONT_SIZES, findFont, fontTagStyle, groupedFonts, setStyleProperty, splitStyle} from './fonts.js';
|
|
20
|
+
import {esc} from './reader-kit.js';
|
|
21
|
+
|
|
22
|
+
const ZWSP = '';
|
|
23
|
+
const CARET_ATTR = 'data-dm-font-caret';
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Add the Font and Size menus to a rich editor's toolbar.
|
|
27
|
+
*
|
|
28
|
+
* @param {object} editor - an `E.editor(..., {mode: 'rich'})` instance
|
|
29
|
+
* @returns {{destroy: Function}|null} null where the editor or E.dropdown is absent
|
|
30
|
+
*/
|
|
31
|
+
export function installFontControls(editor) {
|
|
32
|
+
const content = editor?._editorEl;
|
|
33
|
+
const toolbar = editor?._toolbarEl;
|
|
34
|
+
if (!content || !toolbar || typeof E?.dropdown !== 'function') return null;
|
|
35
|
+
|
|
36
|
+
// The selection as it was when a menu was opened - taken from the live
|
|
37
|
+
// selection at that moment, so nothing an earlier apply moved is stale.
|
|
38
|
+
let saved = null;
|
|
39
|
+
const capture = () => {
|
|
40
|
+
const selection = document.getSelection();
|
|
41
|
+
if (!selection?.rangeCount) return;
|
|
42
|
+
const range = selection.getRangeAt(0);
|
|
43
|
+
if (content.contains(range.commonAncestorContainer)) saved = range.cloneRange();
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const restore = () => {
|
|
47
|
+
content.focus({preventScroll: true});
|
|
48
|
+
const selection = document.getSelection();
|
|
49
|
+
const range = saved && content.contains(saved.startContainer) && content.contains(saved.endContainer)
|
|
50
|
+
? saved
|
|
51
|
+
: endOf(content);
|
|
52
|
+
selection.removeAllRanges();
|
|
53
|
+
selection.addRange(range);
|
|
54
|
+
return range;
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
const setStyle = (el, prop, value) => {
|
|
58
|
+
const style = setStyleProperty(el.getAttribute('style') ?? '', prop, value);
|
|
59
|
+
if (style) el.setAttribute('style', style); else el.removeAttribute('style');
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
/** Style every text node in a (non-collapsed) range. */
|
|
63
|
+
const styleRange = (range, prop, value) => {
|
|
64
|
+
// Split the boundary text nodes; a live range follows the split.
|
|
65
|
+
if (range.endContainer.nodeType === Node.TEXT_NODE && range.endOffset < range.endContainer.length) {
|
|
66
|
+
range.endContainer.splitText(range.endOffset);
|
|
67
|
+
}
|
|
68
|
+
if (range.startContainer.nodeType === Node.TEXT_NODE && range.startOffset > 0) {
|
|
69
|
+
range.startContainer.splitText(range.startOffset);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const nodes = [];
|
|
73
|
+
const root = range.commonAncestorContainer;
|
|
74
|
+
if (root.nodeType === Node.TEXT_NODE) {
|
|
75
|
+
nodes.push(root);
|
|
76
|
+
} else {
|
|
77
|
+
const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT);
|
|
78
|
+
let node;
|
|
79
|
+
while ((node = walker.nextNode())) {
|
|
80
|
+
if (!node.length || !range.intersectsNode(node)) continue;
|
|
81
|
+
if (node === range.endContainer && range.endOffset === 0) continue;
|
|
82
|
+
if (node === range.startContainer && range.startOffset >= node.length) continue;
|
|
83
|
+
nodes.push(node);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
if (!nodes.length) return;
|
|
87
|
+
|
|
88
|
+
for (const node of nodes) {
|
|
89
|
+
const parent = node.parentNode;
|
|
90
|
+
if (parent.nodeName === 'SPAN' && parent !== content && parent.childNodes.length === 1) {
|
|
91
|
+
setStyle(parent, prop, value);
|
|
92
|
+
} else {
|
|
93
|
+
const span = document.createElement('span');
|
|
94
|
+
span.setAttribute('style', `${prop}: ${value}`);
|
|
95
|
+
parent.insertBefore(span, node);
|
|
96
|
+
span.appendChild(node);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
range.setStart(nodes[0], 0);
|
|
101
|
+
range.setEnd(nodes.at(-1), nodes.at(-1).length);
|
|
102
|
+
const selection = document.getSelection();
|
|
103
|
+
selection.removeAllRanges();
|
|
104
|
+
selection.addRange(range);
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
/** A caret: the style goes on a placeholder span for what is typed next. */
|
|
108
|
+
const styleCaret = (range, prop, value) => {
|
|
109
|
+
const holder = range.startContainer.parentElement?.closest?.(`[${CARET_ATTR}]`);
|
|
110
|
+
if (holder && content.contains(holder)) {
|
|
111
|
+
setStyle(holder, prop, value);
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
const span = document.createElement('span');
|
|
115
|
+
span.setAttribute(CARET_ATTR, '');
|
|
116
|
+
span.setAttribute('style', `${prop}: ${value}`);
|
|
117
|
+
const text = document.createTextNode(ZWSP);
|
|
118
|
+
span.appendChild(text);
|
|
119
|
+
range.insertNode(span);
|
|
120
|
+
const selection = document.getSelection();
|
|
121
|
+
selection.removeAllRanges();
|
|
122
|
+
selection.collapse(text, 1);
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Keep the editor free of <font> and of placeholder clutter. Runs after
|
|
127
|
+
* every apply and on every input, before Domma's own input listener
|
|
128
|
+
* records the content (capture on the parent).
|
|
129
|
+
*/
|
|
130
|
+
const tidy = () => {
|
|
131
|
+
const selection = document.getSelection();
|
|
132
|
+
const caret = selection?.rangeCount
|
|
133
|
+
? [selection.anchorNode, selection.anchorOffset, selection.focusNode, selection.focusOffset]
|
|
134
|
+
: null;
|
|
135
|
+
const swapped = new Map();
|
|
136
|
+
|
|
137
|
+
for (const el of [...content.querySelectorAll('font')]) {
|
|
138
|
+
const style = splitStyle(el.getAttribute('style') ?? '')
|
|
139
|
+
.reduce((acc, [p, v]) => setStyleProperty(acc, p, v), fontTagStyle({
|
|
140
|
+
face: el.getAttribute('face'),
|
|
141
|
+
size: el.getAttribute('size'),
|
|
142
|
+
color: el.getAttribute('color')
|
|
143
|
+
}));
|
|
144
|
+
const span = document.createElement('span');
|
|
145
|
+
if (style) span.setAttribute('style', style);
|
|
146
|
+
while (el.firstChild) span.appendChild(el.firstChild);
|
|
147
|
+
el.replaceWith(span);
|
|
148
|
+
swapped.set(el, span);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Moving the text out of a <font> collapses a caret inside it; the
|
|
152
|
+
// nodes survive the move, so the caret goes back onto them.
|
|
153
|
+
if (caret && swapped.size) {
|
|
154
|
+
const node = n => swapped.get(n) ?? n;
|
|
155
|
+
const [anchor, anchorOffset, focus, focusOffset] = caret;
|
|
156
|
+
if (content.contains(node(anchor)) && content.contains(node(focus))) {
|
|
157
|
+
selection.setBaseAndExtent(node(anchor), anchorOffset, node(focus), focusOffset);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const live = selection?.rangeCount ? selection.getRangeAt(0).startContainer : null;
|
|
162
|
+
for (const holder of [...content.querySelectorAll(`[${CARET_ATTR}]`)]) {
|
|
163
|
+
const typed = holder.textContent.replaceAll(ZWSP, '');
|
|
164
|
+
if (typed) {
|
|
165
|
+
// Something was typed: drop the placeholder character. A live
|
|
166
|
+
// selection follows deleteData, so the caret stays put.
|
|
167
|
+
const walker = document.createTreeWalker(holder, NodeFilter.SHOW_TEXT);
|
|
168
|
+
let text;
|
|
169
|
+
while ((text = walker.nextNode())) {
|
|
170
|
+
let at;
|
|
171
|
+
while ((at = text.data.indexOf(ZWSP)) >= 0) text.deleteData(at, 1);
|
|
172
|
+
}
|
|
173
|
+
holder.removeAttribute(CARET_ATTR);
|
|
174
|
+
} else if (!holder.contains(live)) {
|
|
175
|
+
holder.remove();
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
const scope = content.parentNode ?? content;
|
|
181
|
+
scope.addEventListener('input', tidy, true);
|
|
182
|
+
|
|
183
|
+
const apply = (prop, value) => {
|
|
184
|
+
const range = restore();
|
|
185
|
+
if (range.collapsed) styleCaret(range, prop, value);
|
|
186
|
+
else styleRange(range, prop, value);
|
|
187
|
+
tidy();
|
|
188
|
+
// Domma's editor keeps its undo history and counters from input.
|
|
189
|
+
content.dispatchEvent(new Event('input', {bubbles: true}));
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
// ---- The menus ----------------------------------------------------------
|
|
193
|
+
|
|
194
|
+
const group = document.createElement('div');
|
|
195
|
+
group.className = 'dm-editor-toolbar-group mail-font-controls';
|
|
196
|
+
|
|
197
|
+
const fontButton = menuButton('Font', 'Font - the reader sees the first face in its list they have');
|
|
198
|
+
const sizeButton = menuButton('Size', 'Font size');
|
|
199
|
+
group.append(fontButton, sizeButton);
|
|
200
|
+
toolbar.appendChild(group);
|
|
201
|
+
|
|
202
|
+
let currentFont = null;
|
|
203
|
+
let currentSize = null;
|
|
204
|
+
|
|
205
|
+
const fontItems = groupedFonts().flatMap(g => [
|
|
206
|
+
{header: g.label},
|
|
207
|
+
...g.fonts.map(font => ({label: font.label, value: font.id, stack: font.stack, webFont: font.webFont}))
|
|
208
|
+
]);
|
|
209
|
+
|
|
210
|
+
const fontMenu = E.dropdown(fontButton, {
|
|
211
|
+
items: fontItems,
|
|
212
|
+
itemTemplate: item => `<span class="mail-font-item" data-mail-menu="font" style="font-family: ${esc(item.stack)}">${esc(item.label)}</span>`
|
|
213
|
+
+ (item.webFont ? '<span class="mail-font-web" title="Not loaded into the message - readers without it see the fallback">web</span>' : ''),
|
|
214
|
+
onSelect: ({value}) => {
|
|
215
|
+
const font = findFont(value);
|
|
216
|
+
if (!font) return;
|
|
217
|
+
apply('font-family', font.stack);
|
|
218
|
+
currentFont = font.id;
|
|
219
|
+
fontButton.textContent = font.label;
|
|
220
|
+
fontButton.style.fontFamily = font.stack;
|
|
221
|
+
}
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
const sizeMenu = E.dropdown(sizeButton, {
|
|
225
|
+
items: FONT_SIZES.map(px => ({label: `${px}px`, value: String(px)})),
|
|
226
|
+
itemTemplate: item => `<span class="mail-size-item" data-mail-menu="size">${esc(item.label)}</span>`,
|
|
227
|
+
onSelect: ({value}) => {
|
|
228
|
+
const px = Number(value);
|
|
229
|
+
if (!FONT_SIZES.includes(px)) return;
|
|
230
|
+
apply('font-size', `${px}px`);
|
|
231
|
+
currentSize = String(px);
|
|
232
|
+
sizeButton.textContent = `${px}px`;
|
|
233
|
+
}
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
const menus = [
|
|
237
|
+
{kind: 'font', dropdown: fontMenu, button: fontButton, current: () => currentFont},
|
|
238
|
+
{kind: 'size', dropdown: sizeMenu, button: sizeButton, current: () => currentSize}
|
|
239
|
+
];
|
|
240
|
+
|
|
241
|
+
const menuElement = kind => document
|
|
242
|
+
.querySelector(`.domma-dropdown-menu.show [data-mail-menu="${kind}"]`)
|
|
243
|
+
?.closest('.domma-dropdown-menu') ?? null;
|
|
244
|
+
|
|
245
|
+
const closeMenu = (menu, focusButton) => {
|
|
246
|
+
menu.dropdown.close();
|
|
247
|
+
if (focusButton) menu.button.focus();
|
|
248
|
+
};
|
|
249
|
+
|
|
250
|
+
// Keyboard inside an open menu. Every key handled here stops here, so an
|
|
251
|
+
// Enter that picks a font can never reach the compose window's send keys.
|
|
252
|
+
const onMenuKey = (menu, event) => {
|
|
253
|
+
const items = [...event.currentTarget.querySelectorAll('.domma-dropdown-item')];
|
|
254
|
+
const index = items.indexOf(document.activeElement);
|
|
255
|
+
const move = to => items[(to + items.length) % items.length]?.focus();
|
|
256
|
+
const keys = {
|
|
257
|
+
ArrowDown: () => move(index + 1),
|
|
258
|
+
ArrowUp: () => move(index < 0 ? items.length - 1 : index - 1),
|
|
259
|
+
Home: () => move(0),
|
|
260
|
+
End: () => move(items.length - 1),
|
|
261
|
+
Enter: () => (items[index] ?? items[0])?.click(),
|
|
262
|
+
' ': () => (items[index] ?? items[0])?.click(),
|
|
263
|
+
Tab: () => closeMenu(menu, true),
|
|
264
|
+
Escape: () => closeMenu(menu, true)
|
|
265
|
+
};
|
|
266
|
+
const act = keys[event.key];
|
|
267
|
+
if (!act) return;
|
|
268
|
+
event.preventDefault();
|
|
269
|
+
event.stopPropagation();
|
|
270
|
+
act();
|
|
271
|
+
};
|
|
272
|
+
|
|
273
|
+
const bound = new WeakSet();
|
|
274
|
+
const focusMenu = menu => {
|
|
275
|
+
const el = menuElement(menu.kind);
|
|
276
|
+
if (!el) return;
|
|
277
|
+
const items = [...el.querySelectorAll('.domma-dropdown-item')];
|
|
278
|
+
for (const item of items) item.tabIndex = -1;
|
|
279
|
+
if (!bound.has(el)) {
|
|
280
|
+
bound.add(el);
|
|
281
|
+
el.addEventListener('keydown', event => onMenuKey(menu, event));
|
|
282
|
+
}
|
|
283
|
+
const current = items.find(item => item.dataset.value === menu.current()) ?? items[0];
|
|
284
|
+
if (!current) return;
|
|
285
|
+
current.scrollIntoView?.({block: 'nearest'});
|
|
286
|
+
// The menu fades in from `visibility: hidden`, and an invisible item
|
|
287
|
+
// refuses focus - so keep trying for the length of the fade.
|
|
288
|
+
let tries = 0;
|
|
289
|
+
const attempt = () => {
|
|
290
|
+
if (!el.isConnected || !el.classList.contains('show')) return;
|
|
291
|
+
current.focus({preventScroll: true});
|
|
292
|
+
if (document.activeElement !== current && ++tries < 30) requestAnimationFrame(attempt);
|
|
293
|
+
};
|
|
294
|
+
attempt();
|
|
295
|
+
};
|
|
296
|
+
|
|
297
|
+
for (const menu of menus) {
|
|
298
|
+
// The button keeps the editor's selection (mousedown is cancelled), so
|
|
299
|
+
// it is still live here; capture only takes one inside the editor.
|
|
300
|
+
menu.button.addEventListener('click', () => {
|
|
301
|
+
capture();
|
|
302
|
+
queueMicrotask(() => { if (menu.dropdown.isOpen?.()) focusMenu(menu); });
|
|
303
|
+
});
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// Escape with a menu open closes the MENU. Domma's dropdown and the
|
|
307
|
+
// compose slideover both listen on document, so without this one Escape
|
|
308
|
+
// shuts the menu and the whole compose window. Window capture runs first.
|
|
309
|
+
const onEscape = event => {
|
|
310
|
+
if (event.key !== 'Escape') return;
|
|
311
|
+
if (!content.isConnected) {
|
|
312
|
+
window.removeEventListener('keydown', onEscape, true);
|
|
313
|
+
return;
|
|
314
|
+
}
|
|
315
|
+
const open = menus.find(menu => menu.dropdown?.isOpen?.());
|
|
316
|
+
if (!open) return;
|
|
317
|
+
event.preventDefault();
|
|
318
|
+
event.stopPropagation();
|
|
319
|
+
closeMenu(open, true);
|
|
320
|
+
};
|
|
321
|
+
window.addEventListener('keydown', onEscape, true);
|
|
322
|
+
|
|
323
|
+
return {
|
|
324
|
+
destroy() {
|
|
325
|
+
window.removeEventListener('keydown', onEscape, true);
|
|
326
|
+
scope.removeEventListener('input', tidy, true);
|
|
327
|
+
for (const menu of menus) {
|
|
328
|
+
try { menu.dropdown?.close?.(); } catch { /* gone */ }
|
|
329
|
+
try { menu.dropdown?.destroy?.(); } catch { /* gone */ }
|
|
330
|
+
}
|
|
331
|
+
group.remove();
|
|
332
|
+
}
|
|
333
|
+
};
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/**
|
|
337
|
+
* A collapsed range at the end of an element.
|
|
338
|
+
*
|
|
339
|
+
* @param {HTMLElement} el
|
|
340
|
+
* @returns {Range}
|
|
341
|
+
*/
|
|
342
|
+
function endOf(el) {
|
|
343
|
+
const range = document.createRange();
|
|
344
|
+
range.selectNodeContents(el);
|
|
345
|
+
range.collapse(false);
|
|
346
|
+
return range;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
/**
|
|
350
|
+
* A toolbar button that opens a menu without taking the editor's focus.
|
|
351
|
+
*
|
|
352
|
+
* @param {string} label
|
|
353
|
+
* @param {string} title
|
|
354
|
+
* @returns {HTMLButtonElement}
|
|
355
|
+
*/
|
|
356
|
+
function menuButton(label, title) {
|
|
357
|
+
const button = document.createElement('button');
|
|
358
|
+
button.type = 'button';
|
|
359
|
+
button.className = 'dm-editor-toolbar-btn mail-font-btn';
|
|
360
|
+
button.title = title;
|
|
361
|
+
button.setAttribute('aria-haspopup', 'menu');
|
|
362
|
+
button.textContent = label;
|
|
363
|
+
button.addEventListener('mousedown', event => event.preventDefault());
|
|
364
|
+
// The mail reader's single-key shortcuts listen on document and claim
|
|
365
|
+
// Enter (open message) - which would cancel the click Enter makes here.
|
|
366
|
+
button.addEventListener('keydown', event => {
|
|
367
|
+
if (event.key === 'Enter' || event.key === ' ') event.stopPropagation();
|
|
368
|
+
if (event.key === 'ArrowDown') {
|
|
369
|
+
event.preventDefault();
|
|
370
|
+
event.stopPropagation();
|
|
371
|
+
button.click();
|
|
372
|
+
}
|
|
373
|
+
});
|
|
374
|
+
return button;
|
|
375
|
+
}
|
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The fonts and sizes the mail editor offers - pure, so both the compose
|
|
3
|
+
* window and the settings editors (signatures, templates, away message) use
|
|
4
|
+
* one list, and it can be checked without a browser.
|
|
5
|
+
*
|
|
6
|
+
* Mail is not a web page: there is no font loading worth relying on. Outlook
|
|
7
|
+
* on Windows ignores web fonts outright, Gmail only honours its own Google
|
|
8
|
+
* faces, and a font the reader has not got is replaced by whatever the client
|
|
9
|
+
* likes. So every entry is a STACK - the face asked for, then the nearest
|
|
10
|
+
* faces commonly installed, then a generic family - and the stack is what
|
|
11
|
+
* goes into the message as inline `font-family`. A reader without Calibri
|
|
12
|
+
* still gets a clean sans-serif, never Times by accident.
|
|
13
|
+
*
|
|
14
|
+
* Stacks quote multi-word names with SINGLE quotes, and the editor writes
|
|
15
|
+
* them into `style` as plain strings (`setStyleProperty`) rather than through
|
|
16
|
+
* the CSSOM, so they stay that way. Where the browser does rewrite a style it
|
|
17
|
+
* uses `"`, serialised as `"` - legal HTML that the sanitiser and mail
|
|
18
|
+
* clients read the same; the single quotes are only the tidier form.
|
|
19
|
+
*
|
|
20
|
+
* Web fonts (Roboto, Open Sans, ...) are offered because people ask for them
|
|
21
|
+
* by name, but are flagged `webFont`: nothing loads them into the message, so
|
|
22
|
+
* the mail shows the face only where the reader has it installed (or the
|
|
23
|
+
* client supplies it) and the fallback everywhere else.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
/** Group ids in menu order, with their headings. */
|
|
27
|
+
export const FONT_GROUPS = Object.freeze([
|
|
28
|
+
Object.freeze({id: 'sans', label: 'Sans-serif'}),
|
|
29
|
+
Object.freeze({id: 'serif', label: 'Serif'}),
|
|
30
|
+
Object.freeze({id: 'mono', label: 'Monospace'}),
|
|
31
|
+
Object.freeze({id: 'display', label: 'Display & handwriting'}),
|
|
32
|
+
Object.freeze({id: 'web', label: 'Web fonts (need a fallback)'}),
|
|
33
|
+
Object.freeze({id: 'generic', label: 'Generic families'})
|
|
34
|
+
]);
|
|
35
|
+
|
|
36
|
+
const SANS = "Arial, 'Helvetica Neue', Helvetica, sans-serif";
|
|
37
|
+
const SERIF = "Georgia, 'Times New Roman', Times, serif";
|
|
38
|
+
const MONO = "'Courier New', Courier, monospace";
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* [id, label, group, stack, webFont?]. The id is stable (it may be stored);
|
|
42
|
+
* the label is what the menu says.
|
|
43
|
+
*/
|
|
44
|
+
const TABLE = [
|
|
45
|
+
// ---- Sans-serif -------------------------------------------------------
|
|
46
|
+
['arial', 'Arial', 'sans', "Arial, 'Helvetica Neue', Helvetica, sans-serif"],
|
|
47
|
+
['arial-black', 'Arial Black', 'sans', "'Arial Black', 'Arial Bold', Gadget, Arial, sans-serif"],
|
|
48
|
+
['helvetica', 'Helvetica', 'sans', "'Helvetica Neue', Helvetica, Arial, sans-serif"],
|
|
49
|
+
['verdana', 'Verdana', 'sans', 'Verdana, Geneva, Tahoma, sans-serif'],
|
|
50
|
+
['tahoma', 'Tahoma', 'sans', 'Tahoma, Verdana, Geneva, Segoe, sans-serif'],
|
|
51
|
+
['trebuchet', 'Trebuchet MS', 'sans', "'Trebuchet MS', 'Lucida Grande', 'Lucida Sans Unicode', 'Lucida Sans', Tahoma, sans-serif"],
|
|
52
|
+
['segoe', 'Segoe UI', 'sans', "'Segoe UI', Segoe, Tahoma, Geneva, Verdana, sans-serif"],
|
|
53
|
+
['calibri', 'Calibri', 'sans', "Calibri, Candara, Segoe, 'Segoe UI', Optima, Arial, sans-serif"],
|
|
54
|
+
['candara', 'Candara', 'sans', "Candara, Calibri, Segoe, 'Segoe UI', Optima, Arial, sans-serif"],
|
|
55
|
+
['century-gothic', 'Century Gothic', 'sans', "'Century Gothic', CenturyGothic, AppleGothic, Futura, Arial, sans-serif"],
|
|
56
|
+
['franklin-gothic', 'Franklin Gothic', 'sans', "'Franklin Gothic Medium', 'Franklin Gothic', 'ITC Franklin Gothic', 'Arial Narrow', Arial, sans-serif"],
|
|
57
|
+
['gill-sans', 'Gill Sans', 'sans', "'Gill Sans', 'Gill Sans MT', Calibri, 'Trebuchet MS', sans-serif"],
|
|
58
|
+
['lucida-sans', 'Lucida Sans', 'sans', "'Lucida Sans', 'Lucida Grande', 'Lucida Sans Unicode', Geneva, Verdana, sans-serif"],
|
|
59
|
+
['optima', 'Optima', 'sans', "Optima, Segoe, 'Segoe UI', Candara, Calibri, Arial, sans-serif"],
|
|
60
|
+
['futura', 'Futura', 'sans', "Futura, 'Trebuchet MS', 'Century Gothic', Arial, sans-serif"],
|
|
61
|
+
|
|
62
|
+
// ---- Serif ------------------------------------------------------------
|
|
63
|
+
['georgia', 'Georgia', 'serif', "Georgia, 'Times New Roman', Times, serif"],
|
|
64
|
+
['times', 'Times New Roman', 'serif', "'Times New Roman', Times, Georgia, serif"],
|
|
65
|
+
['cambria', 'Cambria', 'serif', "Cambria, Georgia, 'Times New Roman', Times, serif"],
|
|
66
|
+
['garamond', 'Garamond', 'serif', "Garamond, 'EB Garamond', Baskerville, 'Baskerville Old Face', 'Hoefler Text', 'Times New Roman', serif"],
|
|
67
|
+
['palatino', 'Palatino', 'serif', "Palatino, 'Palatino Linotype', 'Palatino LT STD', 'Book Antiqua', Georgia, serif"],
|
|
68
|
+
['book-antiqua', 'Book Antiqua', 'serif', "'Book Antiqua', Palatino, 'Palatino Linotype', 'Palatino LT STD', Georgia, serif"],
|
|
69
|
+
['baskerville', 'Baskerville', 'serif', "Baskerville, 'Baskerville Old Face', 'Hoefler Text', Garamond, 'Times New Roman', serif"],
|
|
70
|
+
['didot', 'Didot', 'serif', "Didot, 'Didot LT STD', 'Hoefler Text', Garamond, 'Times New Roman', serif"],
|
|
71
|
+
['rockwell', 'Rockwell', 'serif', "Rockwell, 'Courier Bold', Courier, Georgia, Times, 'Times New Roman', serif"],
|
|
72
|
+
|
|
73
|
+
// ---- Monospace --------------------------------------------------------
|
|
74
|
+
['courier-new', 'Courier New', 'mono', "'Courier New', Courier, 'Lucida Sans Typewriter', 'Lucida Typewriter', monospace"],
|
|
75
|
+
['lucida-console', 'Lucida Console', 'mono', "'Lucida Console', 'Lucida Sans Typewriter', Monaco, 'Bitstream Vera Sans Mono', monospace"],
|
|
76
|
+
['consolas', 'Consolas', 'mono', "Consolas, Monaco, 'Lucida Console', 'Liberation Mono', 'Courier New', monospace"],
|
|
77
|
+
['monaco', 'Monaco', 'mono', "Monaco, Consolas, 'Lucida Console', 'Courier New', monospace"],
|
|
78
|
+
|
|
79
|
+
// ---- Display & handwriting -------------------------------------------
|
|
80
|
+
['comic-sans', 'Comic Sans MS', 'display', "'Comic Sans MS', 'Comic Sans', 'Chalkboard SE', 'Comic Neue', cursive"],
|
|
81
|
+
['impact', 'Impact', 'display', "Impact, Haettenschweiler, 'Franklin Gothic Bold', Charcoal, 'Helvetica Inserat', 'Arial Black', sans-serif"],
|
|
82
|
+
['brush-script', 'Brush Script MT', 'display', "'Brush Script MT', 'Brush Script Std', 'Lucida Calligraphy', 'Lucida Handwriting', 'Apple Chancery', cursive"],
|
|
83
|
+
|
|
84
|
+
// ---- Web fonts: offered, never loaded - the fallback is what most see --
|
|
85
|
+
['roboto', 'Roboto', 'web', `Roboto, 'Helvetica Neue', ${SANS}`, true],
|
|
86
|
+
['open-sans', 'Open Sans', 'web', `'Open Sans', 'Segoe UI', ${SANS}`, true],
|
|
87
|
+
['lato', 'Lato', 'web', `Lato, 'Segoe UI', ${SANS}`, true],
|
|
88
|
+
['montserrat', 'Montserrat', 'web', `Montserrat, 'Century Gothic', 'Trebuchet MS', ${SANS}`, true],
|
|
89
|
+
['poppins', 'Poppins', 'web', `Poppins, 'Century Gothic', 'Segoe UI', ${SANS}`, true],
|
|
90
|
+
['source-sans', 'Source Sans 3', 'web', `'Source Sans 3', 'Source Sans Pro', 'Segoe UI', ${SANS}`, true],
|
|
91
|
+
['inter', 'Inter', 'web', `Inter, 'Segoe UI', ${SANS}`, true],
|
|
92
|
+
['nunito', 'Nunito', 'web', `Nunito, 'Segoe UI', ${SANS}`, true],
|
|
93
|
+
['raleway', 'Raleway', 'web', `Raleway, 'Century Gothic', ${SANS}`, true],
|
|
94
|
+
['oswald', 'Oswald', 'web', `Oswald, 'Arial Narrow', Impact, ${SANS}`, true],
|
|
95
|
+
['merriweather', 'Merriweather', 'web', `Merriweather, ${SERIF}`, true],
|
|
96
|
+
['playfair', 'Playfair Display', 'web', `'Playfair Display', Didot, ${SERIF}`, true],
|
|
97
|
+
['lora', 'Lora', 'web', `Lora, ${SERIF}`, true],
|
|
98
|
+
['roboto-mono', 'Roboto Mono', 'web', `'Roboto Mono', Consolas, Monaco, ${MONO}`, true],
|
|
99
|
+
|
|
100
|
+
// ---- Generic families: the reader's own choice -------------------------
|
|
101
|
+
['sans-serif', 'Sans-serif (reader’s default)', 'generic', 'sans-serif'],
|
|
102
|
+
['serif', 'Serif (reader’s default)', 'generic', 'serif'],
|
|
103
|
+
['monospace', 'Monospace (reader’s default)', 'generic', 'monospace'],
|
|
104
|
+
['cursive', 'Cursive (reader’s default)', 'generic', 'cursive']
|
|
105
|
+
];
|
|
106
|
+
|
|
107
|
+
/** Every font, in menu order. Stacks are de-duplicated (the shared tails overlap). */
|
|
108
|
+
export const FONTS = Object.freeze(TABLE.map(([id, label, group, stack, webFont = false]) =>
|
|
109
|
+
Object.freeze({id, label, group, stack: joinStack(stackFamilies(stack)), webFont})));
|
|
110
|
+
|
|
111
|
+
/** Font sizes in px - the common 8-72 ladder. */
|
|
112
|
+
export const FONT_SIZES = Object.freeze([8, 9, 10, 11, 12, 13, 14, 15, 16, 18, 20, 22, 24, 26, 28, 32, 36, 40, 48, 56, 64, 72]);
|
|
113
|
+
|
|
114
|
+
/** The CSS generic families a stack may end on. */
|
|
115
|
+
export const GENERIC_FAMILIES = Object.freeze(['serif', 'sans-serif', 'monospace', 'cursive', 'fantasy', 'system-ui']);
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Look a font up by id.
|
|
119
|
+
*
|
|
120
|
+
* @param {string} id
|
|
121
|
+
* @returns {object|null}
|
|
122
|
+
*/
|
|
123
|
+
export function findFont(id) {
|
|
124
|
+
return FONTS.find(font => font.id === id) ?? null;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* The fonts grouped for a menu: `[{id, label, fonts: [...]}]`, empty groups
|
|
129
|
+
* left out.
|
|
130
|
+
*
|
|
131
|
+
* @returns {{id: string, label: string, fonts: object[]}[]}
|
|
132
|
+
*/
|
|
133
|
+
export function groupedFonts() {
|
|
134
|
+
return FONT_GROUPS
|
|
135
|
+
.map(group => ({...group, fonts: FONTS.filter(font => font.group === group.id)}))
|
|
136
|
+
.filter(group => group.fonts.length);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* The families of a stack, unquoted: "Calibri, 'Segoe UI', sans-serif" →
|
|
141
|
+
* ['Calibri', 'Segoe UI', 'sans-serif'].
|
|
142
|
+
*
|
|
143
|
+
* @param {string} stack
|
|
144
|
+
* @returns {string[]}
|
|
145
|
+
*/
|
|
146
|
+
export function stackFamilies(stack) {
|
|
147
|
+
return String(stack ?? '')
|
|
148
|
+
.split(',')
|
|
149
|
+
.map(part => part.trim().replace(/^(['"])(.*)\1$/, '$2').trim())
|
|
150
|
+
.filter(Boolean);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Join families back into a stack: each once, first mention wins, names
|
|
155
|
+
* that are not a single identifier quoted with single quotes.
|
|
156
|
+
*
|
|
157
|
+
* @param {string[]} families
|
|
158
|
+
* @returns {string}
|
|
159
|
+
*/
|
|
160
|
+
export function joinStack(families) {
|
|
161
|
+
const seen = new Set();
|
|
162
|
+
const out = [];
|
|
163
|
+
for (const family of families) {
|
|
164
|
+
const key = family.toLowerCase();
|
|
165
|
+
if (!family || seen.has(key)) continue;
|
|
166
|
+
seen.add(key);
|
|
167
|
+
out.push(/^[A-Za-z][A-Za-z0-9-]*$/.test(family) ? family : `'${family.replace(/'/g, '')}'`);
|
|
168
|
+
}
|
|
169
|
+
return out.join(', ');
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Whether a font size (px) is one the menu offers.
|
|
174
|
+
*
|
|
175
|
+
* @param {number|string} size
|
|
176
|
+
* @returns {boolean}
|
|
177
|
+
*/
|
|
178
|
+
export function isFontSize(size) {
|
|
179
|
+
return FONT_SIZES.includes(Number(size));
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* The inline style a chosen face or size becomes. Values outside the lists
|
|
184
|
+
* give '' - nothing free-form reaches the message.
|
|
185
|
+
*
|
|
186
|
+
* @param {{font?: string, size?: number|string}} choice - font id and/or px size
|
|
187
|
+
* @returns {string}
|
|
188
|
+
*/
|
|
189
|
+
export function inlineStyle({font, size} = {}) {
|
|
190
|
+
const parts = [];
|
|
191
|
+
const entry = font ? findFont(font) : null;
|
|
192
|
+
if (entry) parts.push(`font-family: ${entry.stack}`);
|
|
193
|
+
if (size !== undefined && size !== null && isFontSize(size)) parts.push(`font-size: ${Number(size)}px`);
|
|
194
|
+
return parts.join('; ');
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/** What the legacy `<font size="1..7">` values mean, in px. */
|
|
198
|
+
export const LEGACY_FONT_SIZES = Object.freeze({1: 10, 2: 13, 3: 16, 4: 18, 5: 24, 6: 32, 7: 48});
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Split a `style` attribute into [property, value] pairs. Semicolons inside
|
|
202
|
+
* quotes or parentheses (a data: URI, a quoted family) do not split.
|
|
203
|
+
*
|
|
204
|
+
* @param {string} style
|
|
205
|
+
* @returns {[string, string][]}
|
|
206
|
+
*/
|
|
207
|
+
export function splitStyle(style) {
|
|
208
|
+
const out = [];
|
|
209
|
+
let depth = 0;
|
|
210
|
+
let quote = '';
|
|
211
|
+
let start = 0;
|
|
212
|
+
const text = String(style ?? '');
|
|
213
|
+
const push = end => {
|
|
214
|
+
const chunk = text.slice(start, end);
|
|
215
|
+
const colon = chunk.indexOf(':');
|
|
216
|
+
if (colon > 0) {
|
|
217
|
+
const prop = chunk.slice(0, colon).trim().toLowerCase();
|
|
218
|
+
const value = chunk.slice(colon + 1).trim();
|
|
219
|
+
if (prop && value) out.push([prop, value]);
|
|
220
|
+
}
|
|
221
|
+
};
|
|
222
|
+
for (let i = 0; i < text.length; i++) {
|
|
223
|
+
const ch = text[i];
|
|
224
|
+
if (quote) {
|
|
225
|
+
if (ch === quote) quote = '';
|
|
226
|
+
} else if (ch === '"' || ch === "'") {
|
|
227
|
+
quote = ch;
|
|
228
|
+
} else if (ch === '(') {
|
|
229
|
+
depth++;
|
|
230
|
+
} else if (ch === ')') {
|
|
231
|
+
depth = Math.max(0, depth - 1);
|
|
232
|
+
} else if (ch === ';' && !depth) {
|
|
233
|
+
push(i);
|
|
234
|
+
start = i + 1;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
push(text.length);
|
|
238
|
+
return out;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Set (or with a null value, remove) one property in a `style` string,
|
|
243
|
+
* leaving the rest as written - so a stack keeps its single quotes rather
|
|
244
|
+
* than going through the browser's CSSOM, which re-quotes with `"`.
|
|
245
|
+
*
|
|
246
|
+
* @param {string} style
|
|
247
|
+
* @param {string} prop
|
|
248
|
+
* @param {string|null} value
|
|
249
|
+
* @returns {string}
|
|
250
|
+
*/
|
|
251
|
+
export function setStyleProperty(style, prop, value) {
|
|
252
|
+
const name = String(prop).toLowerCase();
|
|
253
|
+
const pairs = splitStyle(style).filter(([p]) => p !== name);
|
|
254
|
+
if (value !== null && value !== undefined && value !== '') pairs.push([name, String(value)]);
|
|
255
|
+
return pairs.map(([p, v]) => `${p}: ${v}`).join('; ');
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* The inline style that replaces a `<font>` tag - one the browser wrote, or
|
|
260
|
+
* one pasted in. `<font>` is not kept by the sanitiser (signatures,
|
|
261
|
+
* templates) and is unreliable in mail, so none is left in the editor.
|
|
262
|
+
*
|
|
263
|
+
* @param {{face?: string|null, size?: string|null, color?: string|null}} attrs
|
|
264
|
+
* @returns {string}
|
|
265
|
+
*/
|
|
266
|
+
export function fontTagStyle({face, size, color} = {}) {
|
|
267
|
+
let style = '';
|
|
268
|
+
const families = stackFamilies(face);
|
|
269
|
+
if (families.length) style = setStyleProperty(style, 'font-family', joinStack(families));
|
|
270
|
+
const raw = String(size ?? '').trim();
|
|
271
|
+
if (/^[+-]?[1-7]$/.test(raw)) {
|
|
272
|
+
const level = /^[+-]/.test(raw) ? 3 + Number(raw) : Number(raw);
|
|
273
|
+
const px = LEGACY_FONT_SIZES[Math.min(7, Math.max(1, level))];
|
|
274
|
+
style = setStyleProperty(style, 'font-size', `${px}px`);
|
|
275
|
+
}
|
|
276
|
+
const tint = String(color ?? '').trim();
|
|
277
|
+
if (/^(#[0-9a-f]{3,8}|[a-z]+|rgba?\([0-9.,%\s]+\))$/i.test(tint)) style = setStyleProperty(style, 'color', tint);
|
|
278
|
+
return style;
|
|
279
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
#view-container.mail-fullbleed{max-width:none}.mail-layout [hidden],.dm-slideover [hidden]{display:none!important}.mail-reader-summary{display:flex;align-items:baseline;gap:.5rem;flex-wrap:wrap}.mail-header-toggle,.mail-extras-toggle{padding:0!important;font-size:.78rem;opacity:.75;text-decoration:underline}.mail-header-toggle:hover,.mail-extras-toggle:hover{opacity:1}.mail-header-details{display:grid;grid-template-columns:max-content 1fr;gap:.15rem .75rem;margin:.5rem 0 0;font-size:.8rem}.mail-header-details dt{opacity:.65}.mail-header-details dd{margin:0;overflow-wrap:anywhere}.mail-layout{display:grid;grid-template-columns:var(--mail-col-1, 190px) auto var(--mail-col-2, 340px) auto 1fr;height:calc(100vh - 140px);min-height:320px;border:1px solid var(--dm-border);border-radius:var(--dm-radius-md, 8px);overflow:hidden;background:var(--dm-surface)}.mail-folders-pane,.mail-list-pane{display:flex;flex-direction:column;border-right:1px solid var(--dm-border);min-width:0;min-height:0}.mail-reader-pane{display:flex;flex-direction:column;min-width:0;min-height:0}.mail-pane-head{display:flex;align-items:center;justify-content:space-between;gap:.5rem;padding:.5rem .75rem;font-size:.8rem;font-weight:600;text-transform:uppercase;letter-spacing:.04em;color:var(--dm-text-muted);border-bottom:1px solid var(--dm-border);flex-shrink:0}.mail-folders-list,.mail-list{overflow-y:auto;flex:1;min-height:0}.mail-search{display:flex;align-items:center;gap:.4rem;padding:.4rem .6rem;border-bottom:1px solid var(--dm-border);color:var(--dm-text-muted);flex-shrink:0}.mail-search input{flex:1;min-width:0;font-size:.85rem;padding:.25rem .4rem}.mail-search-note{padding:.3rem .6rem;font-size:.74rem;color:var(--dm-text-muted);border-bottom:1px solid var(--dm-border);flex-shrink:0}.mail-slideover-resizer{padding:0;position:absolute;left:0;top:0;bottom:0;width:6px;cursor:col-resize;z-index:2;background:transparent}.mail-slideover-resizer:hover,.is-resizing>.mail-slideover-resizer{background:var(--dm-primary);opacity:.7}.is-resizing{user-select:none;transition:none!important}.mail-selection{display:flex;align-items:center;gap:.35rem;flex-wrap:wrap;padding:.35rem .6rem;font-size:.78rem;background:var(--dm-primary);color:var(--dm-primary-text);flex-shrink:0}.mail-selection span{flex:1;min-width:0}.mail-selection .btn{color:var(--dm-primary-text);border:1px solid currentColor;background:transparent}.mail-selection .btn:hover{background:color-mix(in srgb,var(--dm-primary-text) 15%,transparent)}.mail-item.is-selected{background:var(--dm-active-bg);box-shadow:inset 3px 0 0 var(--dm-primary)}.mail-undo{display:flex;align-items:center;gap:.5rem;padding:.4rem .6rem;font-size:.78rem;background:var(--dm-active-bg);border-bottom:1px solid var(--dm-border);flex-shrink:0}.mail-undo span{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.mail-splitter{width:5px;cursor:col-resize;background:var(--dm-border);flex-shrink:0;background-clip:content-box;border-left:2px solid transparent;border-right:2px solid transparent;box-sizing:content-box}.mail-splitter:hover,.mail-layout.is-resizing .mail-splitter{background-color:var(--dm-primary)}.mail-layout.is-resizing{user-select:none}.mail-layout.is-resizing .mail-folders-pane,.mail-layout.is-resizing .mail-list-pane,.mail-layout.is-resizing .mail-reader-pane,.mail-layout.is-resizing iframe{pointer-events:none}.mail-unread-filter{display:flex;align-items:center;gap:.3rem;font-size:.7rem;text-transform:none;letter-spacing:0;cursor:pointer;margin-left:auto;margin-right:.25rem}.mail-unread-pill{flex-shrink:0;min-width:1.15rem;padding:0 .3rem;border-radius:999px;background:var(--dm-primary);color:var(--dm-primary-text);font-size:.66rem;font-weight:700;line-height:1.35;text-align:center}.mail-folder.is-context .mail-folder-name{opacity:.6}.mail-account{display:flex;align-items:center;gap:.4rem;width:100%;padding:.45rem .6rem;border:none;background:var(--dm-surface);cursor:pointer;text-align:left;font-size:.78rem;font-weight:700;text-transform:uppercase;letter-spacing:.03em;color:var(--dm-text-muted)}.mail-account:hover{background:var(--dm-hover-bg)}.mail-account.is-active{color:var(--dm-text);position:sticky;top:0;z-index:1;background:var(--dm-surface)}.mail-account-name{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.mail-folder{display:flex;align-items:center;gap:.5rem;padding:.45rem .75rem;font-size:.875rem;cursor:pointer;border:none;background:none;width:100%;text-align:left;color:var(--dm-text)}.mail-folder:hover{background:var(--dm-hover-bg)}.mail-folder.is-active{background:var(--dm-active-bg);color:var(--dm-primary);font-weight:600}.mail-folder.is-unselectable{opacity:.6;cursor:default}.mail-folder.is-unselectable:hover{background:none}.mail-folder-name{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.mail-item{display:block;width:100%;text-align:left;padding:.55rem .75rem;border:none;border-bottom:1px solid var(--dm-border);background:none;cursor:pointer;color:var(--dm-text)}.mail-item:hover{background:var(--dm-hover-bg)}.mail-item.is-active{background:var(--dm-active-bg)}.mail-item-top{display:flex;justify-content:space-between;gap:.5rem;align-items:baseline}.mail-item-from{font-size:.85rem;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.mail-item.is-unread .mail-item-from,.mail-item.is-unread .mail-item-subject{font-weight:700}.mail-item-date{font-size:.72rem;color:var(--dm-text-muted);flex-shrink:0}.mail-item-subject{font-size:.82rem;margin-top:.1rem;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.mail-item-meta{display:flex;align-items:center;gap:.35rem;margin-top:.15rem;color:var(--dm-text-muted)}.mail-pager{display:flex;align-items:center;justify-content:space-between;gap:.5rem;padding:.4rem .6rem;border-top:1px solid var(--dm-border);font-size:.75rem;color:var(--dm-text-muted);flex-shrink:0}.mail-reader-head{padding:.75rem 1rem;border-bottom:1px solid var(--dm-border);flex-shrink:0}.mail-reader-head:empty{display:none}.mail-reader-subject{font-size:1.05rem;font-weight:600;margin:0 0 .3rem;color:var(--dm-text)}.mail-reader-actions{display:flex;gap:.25rem;margin:.1rem 0 .35rem;flex-wrap:wrap}.mail-reader-meta{font-size:.8rem;color:var(--dm-text-muted)}.mail-reader-body{flex:1;position:relative;overflow:hidden;display:flex;flex-direction:column;min-height:0;background:var(--dm-surface)}.mail-reader-body iframe{width:100%;flex:1;border:none;display:block;background:#fff}.mail-notice{display:flex;align-items:center;gap:.5rem;flex-wrap:wrap;padding:.4rem .75rem;font-size:.78rem;background:var(--dm-warning);color:var(--dm-warning-text);border-bottom:1px solid var(--dm-border)}.mail-notice .btn{color:var(--dm-warning-text);border:1px solid currentColor;background:transparent;opacity:.95}.mail-notice .btn:hover{background:color-mix(in srgb,var(--dm-warning-text) 15%,transparent);opacity:1}.mail-attachments{display:flex;flex-wrap:wrap;gap:.4rem;padding:.5rem .75rem;border-bottom:1px solid var(--dm-border);background:var(--dm-background-alt)}.mail-empty{display:flex;flex-direction:column;align-items:center;justify-content:center;gap:.5rem;height:100%;color:var(--dm-text-muted);text-align:center;padding:1rem}.mail-empty p{margin:0;font-size:.875rem}@media(max-width:900px){.mail-layout{grid-template-columns:1fr;height:auto!important}.mail-splitter{display:none}.mail-folders-pane,.mail-list-pane{border-right:none;border-bottom:1px solid var(--dm-border)}.mail-folders-list,.mail-list{max-height:240px}.mail-reader-body{min-height:60vh}}.mail-box-row{display:flex;align-items:center;gap:.5rem;padding:.5rem .6rem;border:1px solid var(--dm-border);border-radius:var(--dm-radius-md, 6px)}.mail-default-badge{margin-left:.4rem;font-size:.7rem;font-weight:500;vertical-align:middle}.mail-item.is-dragging{opacity:.45}.mail-folder.is-drop-target{background:var(--dm-primary);color:var(--dm-primary-text);outline:2px solid var(--dm-primary);outline-offset:-2px}.mail-folder.is-drop-target .mail-folder-name,.mail-folder.is-drop-target .mail-unread-pill{color:inherit}.mail-item.is-dragging *{pointer-events:none}.mail-item{position:relative}.mail-item-tick{position:absolute;left:.45rem;top:.55rem;display:inline-flex;opacity:0;color:var(--dm-text-muted);transition:opacity .12s ease;padding:2px;margin:-2px;border-radius:var(--dm-radius-sm, 3px)}.mail-item:hover .mail-item-tick,.mail-item:focus-visible .mail-item-tick,.mail-list.is-selecting .mail-item-tick,.mail-item-tick[aria-checked=true]{opacity:1}.mail-item-tick[aria-checked=true]{color:var(--dm-primary)}.mail-item-tick:hover{color:var(--dm-primary);background:var(--dm-hover-bg)}.mail-item-top,.mail-item-subject,.mail-item-meta{padding-left:1.5rem}.mail-select-all{display:inline-flex;align-items:center;margin-right:.1rem;cursor:pointer}.mail-select-all input{cursor:pointer;margin:0}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}.mail-item-meta>span{display:inline-flex}.mail-item-meta .is-replied{color:var(--dm-success)}.mail-item-meta .is-forwarded{color:var(--dm-info, var(--dm-primary))}.mail-item-meta .is-high{color:var(--dm-danger)}.mail-item-meta .is-low{color:var(--dm-text-muted);opacity:.7}.mail-folder.is-hidden-folder .mail-folder-name{opacity:.5;font-style:italic}.mail-thread-chip{display:inline-flex;align-items:center;margin-left:.4rem;padding:0 .35rem;border-radius:999px;font-size:.68rem;font-weight:700;line-height:1.5;color:var(--dm-text-muted);background:var(--dm-active-bg);cursor:pointer}.mail-thread-chip:hover{color:var(--dm-primary)}.mail-thread-chip.is-unread{background:var(--dm-primary);color:var(--dm-primary-text)}.mail-item.is-in-thread{padding-left:1.6rem;box-shadow:inset 3px 0 0 var(--dm-border)}.mail-item.is-in-thread .mail-item-from{font-size:.8rem}.mail-pane-head .btn.is-on{color:var(--dm-primary);background:var(--dm-active-bg)}#mail-outbox-btn{font-size:.72rem;font-weight:700}#mail-outbox-btn.is-failed{color:var(--dm-danger)}.mail-attachment{display:inline-flex;align-items:center;border:1px solid var(--dm-border);border-radius:var(--dm-radius-md, 6px);overflow:hidden}.mail-attachment .btn{border:none;border-radius:0}.mail-attachment-save{padding-left:.4rem!important;padding-right:.4rem!important;border-left:1px solid var(--dm-border)!important;opacity:.7}.mail-attachment-save:hover{opacity:1;color:var(--dm-primary)}.mail-section-heading{margin:1rem 0 .1rem;font-size:.8rem;text-transform:uppercase;letter-spacing:.04em;color:var(--dm-text-muted)}.mail-section-heading:first-child{margin-top:.25rem}.mail-section-form .domma-form{margin:0}.mail-section-form .domma-form-field:last-child{margin-bottom:0}.mail-item-card{margin:0}.mail-item-card .card-header{padding:.5rem .6rem}.mail-item-card .card-body{padding:.6rem}.mail-ellipsis{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;min-width:0}.mail-status{min-height:1.2em}.mail-empty-line{margin:.25rem 0}.mail-editor-fallback{min-height:8rem;font-family:inherit;resize:vertical}.mail-clause{display:flex;gap:.3rem;align-items:center;flex-wrap:wrap}.mail-clause .form-select,.mail-clause .form-input{width:auto;flex:0 1 auto}.mail-clause .mail-clause-value{flex:1 1 8rem;min-width:8rem}.mail-section-list{display:flex;flex-direction:column;gap:.5rem}.mail-scroll-sm{max-height:20vh;overflow-y:auto}.mail-scroll-md{max-height:46vh;overflow-y:auto}.mail-section-form .domma-form-field:has(select) .form-label .text-danger{display:none}.mail-compose-form{padding:.25rem 0 .5rem;display:flex;flex-direction:column;gap:.6rem;height:100%}.mail-compose-body{display:flex;flex-direction:column;flex:1;min-height:0}.mail-compose-editor{flex:1;min-height:0}.mail-compose-textarea{flex:1;min-height:34vh;font-family:inherit;resize:vertical}.mail-attach-list{display:flex;flex-direction:column;gap:.2rem}.mail-template-row{margin:-.15rem 0 .35rem}.mail-template-picker{max-width:16rem}.mail-suggest-row{display:flex;flex-direction:column;line-height:1.25}.mail-suggest-detail{opacity:.7}.mail-rule-off{opacity:.55}.mail-panel{padding:.25rem 0 .5rem}.mail-panel-stack{padding:.25rem 0 .5rem;display:flex;flex-direction:column;gap:1rem}.mail-form-stack{padding:.25rem 0 .5rem;display:flex;flex-direction:column;gap:.75rem}.mail-dialog-stack{display:flex;flex-direction:column;gap:.5rem;padding:.25rem 0}.mail-hint{margin-top:.2rem}.mail-box-sub{overflow:hidden;text-overflow:ellipsis}.mail-add-btn{align-self:flex-start;margin-top:.5rem}.mail-none{margin:0;padding:.5rem .6rem}.mail-kv{padding:.5rem 0;display:flex;flex-direction:column;gap:.35rem}.mail-kv-row{display:flex;gap:.75rem}.mail-kv-key{min-width:8.5rem;flex-shrink:0}.mail-toggle-row{display:flex;align-items:center;gap:.5rem;margin-bottom:.75rem}.mail-sender-list{display:flex;flex-direction:column;gap:.2rem;max-height:40vh;overflow-y:auto}.mail-folder-pick{display:flex;align-items:center;gap:.5rem;padding:.3rem .4rem;border-radius:var(--dm-radius-md, 6px)}.mail-diag-log{width:100%;height:30vh;margin-top:.5rem;font-family:monospace;font-size:.72rem}.mail-facts{display:flex;flex-direction:column;gap:.2rem;margin-bottom:.75rem}.mail-item-more{position:absolute;right:.4rem;bottom:.35rem;display:inline-flex;align-items:center;justify-content:center;min-width:1.4rem;height:1.4rem;border-radius:var(--dm-radius-sm, 3px);font-size:1rem;line-height:1;color:var(--dm-text-muted);background:inherit;opacity:0;transition:opacity .12s ease}.mail-item:hover .mail-item-more,.mail-item:focus-visible .mail-item-more,.mail-item.is-active .mail-item-more{opacity:1}.mail-item-more:hover{color:var(--dm-primary)}.mail-section-intro{display:flex;align-items:center;gap:.35rem;margin:0 0 .75rem;padding-bottom:.5rem;border-bottom:1px solid var(--dm-border)}.mail-pager-middle{display:flex;align-items:center;gap:.5rem;min-width:0}.mail-page-size{font:inherit;color:inherit;background:transparent;border:1px solid var(--dm-border);border-radius:4px;padding:.1rem .25rem}.mail-auth-choice{display:flex;flex-wrap:wrap;align-items:center;gap:.4rem;padding-bottom:.5rem;border-bottom:1px solid var(--dm-border)}.mail-oauth{display:flex;flex-direction:column;gap:.75rem}.mail-oauth-warn{margin:0;padding:.5rem .7rem;border:1px solid var(--dm-warning);border-radius:var(--dm-radius-md);color:var(--dm-warning-text)}.mail-oauth-state{display:flex;align-items:center;gap:.5rem;flex-wrap:wrap}.mail-oauth-chip{margin-left:.4rem;border:1px solid var(--dm-border);background:var(--dm-surface);color:var(--dm-text-muted)}.mail-oauth-state .mail-oauth-chip{margin-left:0}.mail-oauth-chip--ok{border-color:var(--dm-success);color:var(--dm-success)}.mail-oauth-chip--bad{border-color:var(--dm-danger);color:var(--dm-danger)}.mail-oauth-guide{border:1px solid var(--dm-border);border-radius:var(--dm-radius-md);padding:.5rem .75rem;background:var(--dm-background-alt)}.mail-oauth-guide>summary{cursor:pointer;font-weight:600}.mail-oauth-guide-body ol{padding-left:1.2rem;margin:.5rem 0 0}.mail-oauth-guide-body li{margin-bottom:.45rem}.mail-oauth-code{word-break:break-all;user-select:all}.mail-loading-slot{display:block;min-height:3rem;padding:.5rem .6rem}.mail-row-more{flex-shrink:0;margin-left:auto;display:inline-flex;align-items:center;justify-content:center;min-width:1.3rem;height:1.3rem;border-radius:var(--dm-radius-sm);font-size:.95rem;line-height:1;color:var(--dm-text-muted);opacity:0;transition:opacity .12s ease}.mail-unread-pill+.mail-row-more{margin-left:0}.mail-folder:hover .mail-row-more,.mail-folder:focus-visible .mail-row-more,.mail-folder.is-active .mail-row-more,.mail-account:hover .mail-row-more,.mail-account:focus-visible .mail-row-more,.mail-account.is-active .mail-row-more{opacity:1}.mail-row-more:hover{color:var(--dm-primary)}.mail-box-row .mail-row-more{opacity:1;margin-left:0}.mail-box-row:focus-visible{outline:2px solid var(--dm-primary);outline-offset:-2px}.mail-restored-note{margin:0;padding:.4rem .6rem;border-left:3px solid var(--dm-warning);background:var(--dm-background-alt)}
|
|
1
|
+
#view-container.mail-fullbleed{max-width:none}.mail-layout [hidden],.dm-slideover [hidden]{display:none!important}.mail-reader-summary{display:flex;align-items:baseline;gap:.5rem;flex-wrap:wrap}.mail-header-toggle,.mail-extras-toggle{padding:0!important;font-size:.78rem;opacity:.75;text-decoration:underline}.mail-header-toggle:hover,.mail-extras-toggle:hover{opacity:1}.mail-header-details{display:grid;grid-template-columns:max-content 1fr;gap:.15rem .75rem;margin:.5rem 0 0;font-size:.8rem}.mail-header-details dt{opacity:.65}.mail-header-details dd{margin:0;overflow-wrap:anywhere}.mail-layout{display:grid;grid-template-columns:var(--mail-col-1, 190px) auto var(--mail-col-2, 340px) auto 1fr;height:calc(100vh - 140px);min-height:320px;border:1px solid var(--dm-border);border-radius:var(--dm-radius-md, 8px);overflow:hidden;background:var(--dm-surface)}.mail-folders-pane,.mail-list-pane{display:flex;flex-direction:column;border-right:1px solid var(--dm-border);min-width:0;min-height:0}.mail-reader-pane{display:flex;flex-direction:column;min-width:0;min-height:0}.mail-pane-head{display:flex;align-items:center;justify-content:space-between;gap:.5rem;padding:.5rem .75rem;font-size:.8rem;font-weight:600;text-transform:uppercase;letter-spacing:.04em;color:var(--dm-text-muted);border-bottom:1px solid var(--dm-border);flex-shrink:0}.mail-folders-list,.mail-list{overflow-y:auto;flex:1;min-height:0}.mail-search{display:flex;align-items:center;gap:.4rem;padding:.4rem .6rem;border-bottom:1px solid var(--dm-border);color:var(--dm-text-muted);flex-shrink:0}.mail-search input{flex:1;min-width:0;font-size:.85rem;padding:.25rem .4rem}.mail-search-note{padding:.3rem .6rem;font-size:.74rem;color:var(--dm-text-muted);border-bottom:1px solid var(--dm-border);flex-shrink:0}.mail-slideover-resizer{padding:0;position:absolute;left:0;top:0;bottom:0;width:6px;cursor:col-resize;z-index:2;background:transparent}.mail-slideover-resizer:hover,.is-resizing>.mail-slideover-resizer{background:var(--dm-primary);opacity:.7}.is-resizing{user-select:none;transition:none!important}.mail-selection{display:flex;align-items:center;gap:.35rem;flex-wrap:wrap;padding:.35rem .6rem;font-size:.78rem;background:var(--dm-primary);color:var(--dm-primary-text);flex-shrink:0}.mail-selection span{flex:1;min-width:0}.mail-selection .btn{color:var(--dm-primary-text);border:1px solid currentColor;background:transparent}.mail-selection .btn:hover{background:color-mix(in srgb,var(--dm-primary-text) 15%,transparent)}.mail-item.is-selected{background:var(--dm-active-bg);box-shadow:inset 3px 0 0 var(--dm-primary)}.mail-undo{display:flex;align-items:center;gap:.5rem;padding:.4rem .6rem;font-size:.78rem;background:var(--dm-active-bg);border-bottom:1px solid var(--dm-border);flex-shrink:0}.mail-undo span{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.mail-splitter{width:5px;cursor:col-resize;background:var(--dm-border);flex-shrink:0;background-clip:content-box;border-left:2px solid transparent;border-right:2px solid transparent;box-sizing:content-box}.mail-splitter:hover,.mail-layout.is-resizing .mail-splitter{background-color:var(--dm-primary)}.mail-layout.is-resizing{user-select:none}.mail-layout.is-resizing .mail-folders-pane,.mail-layout.is-resizing .mail-list-pane,.mail-layout.is-resizing .mail-reader-pane,.mail-layout.is-resizing iframe{pointer-events:none}.mail-unread-filter{display:flex;align-items:center;gap:.3rem;font-size:.7rem;text-transform:none;letter-spacing:0;cursor:pointer;margin-left:auto;margin-right:.25rem}.mail-unread-pill{flex-shrink:0;min-width:1.15rem;padding:0 .3rem;border-radius:999px;background:var(--dm-primary);color:var(--dm-primary-text);font-size:.66rem;font-weight:700;line-height:1.35;text-align:center}.mail-folder.is-context .mail-folder-name{opacity:.6}.mail-account{display:flex;align-items:center;gap:.4rem;width:100%;padding:.45rem .6rem;border:none;background:var(--dm-surface);cursor:pointer;text-align:left;font-size:.78rem;font-weight:700;text-transform:uppercase;letter-spacing:.03em;color:var(--dm-text-muted)}.mail-account:hover{background:var(--dm-hover-bg)}.mail-account.is-active{color:var(--dm-text);position:sticky;top:0;z-index:1;background:var(--dm-surface)}.mail-account-name{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.mail-folder{display:flex;align-items:center;gap:.5rem;padding:.45rem .75rem;font-size:.875rem;cursor:pointer;border:none;background:none;width:100%;text-align:left;color:var(--dm-text)}.mail-folder:hover{background:var(--dm-hover-bg)}.mail-folder.is-active{background:var(--dm-active-bg);color:var(--dm-primary);font-weight:600}.mail-folder.is-unselectable{opacity:.6;cursor:default}.mail-folder.is-unselectable:hover{background:none}.mail-folder-name{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.mail-item{display:block;width:100%;text-align:left;padding:.55rem .75rem;border:none;border-bottom:1px solid var(--dm-border);background:none;cursor:pointer;color:var(--dm-text)}.mail-item:hover{background:var(--dm-hover-bg)}.mail-item.is-active{background:var(--dm-active-bg)}.mail-item-top{display:flex;justify-content:space-between;gap:.5rem;align-items:baseline}.mail-item-from{font-size:.85rem;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.mail-item.is-unread .mail-item-from,.mail-item.is-unread .mail-item-subject{font-weight:700}.mail-item-date{font-size:.72rem;color:var(--dm-text-muted);flex-shrink:0}.mail-item-subject{font-size:.82rem;margin-top:.1rem;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.mail-item-meta{display:flex;align-items:center;gap:.35rem;margin-top:.15rem;color:var(--dm-text-muted)}.mail-pager{display:flex;align-items:center;justify-content:space-between;gap:.5rem;padding:.4rem .6rem;border-top:1px solid var(--dm-border);font-size:.75rem;color:var(--dm-text-muted);flex-shrink:0}.mail-reader-head{padding:.75rem 1rem;border-bottom:1px solid var(--dm-border);flex-shrink:0}.mail-reader-head:empty{display:none}.mail-reader-subject{font-size:1.05rem;font-weight:600;margin:0 0 .3rem;color:var(--dm-text)}.mail-reader-actions{display:flex;gap:.25rem;margin:.1rem 0 .35rem;flex-wrap:wrap}.mail-reader-meta{font-size:.8rem;color:var(--dm-text-muted)}.mail-reader-body{flex:1;position:relative;overflow:hidden;display:flex;flex-direction:column;min-height:0;background:var(--dm-surface)}.mail-reader-body iframe{width:100%;flex:1;border:none;display:block;background:#fff}.mail-notice{display:flex;align-items:center;gap:.5rem;flex-wrap:wrap;padding:.4rem .75rem;font-size:.78rem;background:var(--dm-warning);color:var(--dm-warning-text);border-bottom:1px solid var(--dm-border)}.mail-notice .btn{color:var(--dm-warning-text);border:1px solid currentColor;background:transparent;opacity:.95}.mail-notice .btn:hover{background:color-mix(in srgb,var(--dm-warning-text) 15%,transparent);opacity:1}.mail-attachments{display:flex;flex-wrap:wrap;gap:.4rem;padding:.5rem .75rem;border-bottom:1px solid var(--dm-border);background:var(--dm-background-alt)}.mail-empty{display:flex;flex-direction:column;align-items:center;justify-content:center;gap:.5rem;height:100%;color:var(--dm-text-muted);text-align:center;padding:1rem}.mail-empty p{margin:0;font-size:.875rem}@media(max-width:900px){.mail-layout{grid-template-columns:1fr;height:auto!important}.mail-splitter{display:none}.mail-folders-pane,.mail-list-pane{border-right:none;border-bottom:1px solid var(--dm-border)}.mail-folders-list,.mail-list{max-height:240px}.mail-reader-body{min-height:60vh}}.mail-box-row{display:flex;align-items:center;gap:.5rem;padding:.5rem .6rem;border:1px solid var(--dm-border);border-radius:var(--dm-radius-md, 6px)}.mail-default-badge{margin-left:.4rem;font-size:.7rem;font-weight:500;vertical-align:middle}.mail-item.is-dragging{opacity:.45}.mail-folder.is-drop-target{background:var(--dm-primary);color:var(--dm-primary-text);outline:2px solid var(--dm-primary);outline-offset:-2px}.mail-folder.is-drop-target .mail-folder-name,.mail-folder.is-drop-target .mail-unread-pill{color:inherit}.mail-item.is-dragging *{pointer-events:none}.mail-item{position:relative}.mail-item-tick{position:absolute;left:.45rem;top:.55rem;display:inline-flex;opacity:0;color:var(--dm-text-muted);transition:opacity .12s ease;padding:2px;margin:-2px;border-radius:var(--dm-radius-sm, 3px)}.mail-item:hover .mail-item-tick,.mail-item:focus-visible .mail-item-tick,.mail-list.is-selecting .mail-item-tick,.mail-item-tick[aria-checked=true]{opacity:1}.mail-item-tick[aria-checked=true]{color:var(--dm-primary)}.mail-item-tick:hover{color:var(--dm-primary);background:var(--dm-hover-bg)}.mail-item-top,.mail-item-subject,.mail-item-meta{padding-left:1.5rem}.mail-select-all{display:inline-flex;align-items:center;margin-right:.1rem;cursor:pointer}.mail-select-all input{cursor:pointer;margin:0}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}.mail-item-meta>span{display:inline-flex}.mail-item-meta .is-replied{color:var(--dm-success)}.mail-item-meta .is-forwarded{color:var(--dm-info, var(--dm-primary))}.mail-item-meta .is-high{color:var(--dm-danger)}.mail-item-meta .is-low{color:var(--dm-text-muted);opacity:.7}.mail-folder.is-hidden-folder .mail-folder-name{opacity:.5;font-style:italic}.mail-thread-chip{display:inline-flex;align-items:center;margin-left:.4rem;padding:0 .35rem;border-radius:999px;font-size:.68rem;font-weight:700;line-height:1.5;color:var(--dm-text-muted);background:var(--dm-active-bg);cursor:pointer}.mail-thread-chip:hover{color:var(--dm-primary)}.mail-thread-chip.is-unread{background:var(--dm-primary);color:var(--dm-primary-text)}.mail-item.is-in-thread{padding-left:1.6rem;box-shadow:inset 3px 0 0 var(--dm-border)}.mail-item.is-in-thread .mail-item-from{font-size:.8rem}.mail-pane-head .btn.is-on{color:var(--dm-primary);background:var(--dm-active-bg)}#mail-outbox-btn{font-size:.72rem;font-weight:700}#mail-outbox-btn.is-failed{color:var(--dm-danger)}.mail-attachment{display:inline-flex;align-items:center;border:1px solid var(--dm-border);border-radius:var(--dm-radius-md, 6px);overflow:hidden}.mail-attachment .btn{border:none;border-radius:0}.mail-attachment-save{padding-left:.4rem!important;padding-right:.4rem!important;border-left:1px solid var(--dm-border)!important;opacity:.7}.mail-attachment-save:hover{opacity:1;color:var(--dm-primary)}.mail-section-heading{margin:1rem 0 .1rem;font-size:.8rem;text-transform:uppercase;letter-spacing:.04em;color:var(--dm-text-muted)}.mail-section-heading:first-child{margin-top:.25rem}.mail-section-form .domma-form{margin:0}.mail-section-form .domma-form-field:last-child{margin-bottom:0}.mail-item-card{margin:0}.mail-item-card .card-header{padding:.5rem .6rem}.mail-item-card .card-body{padding:.6rem}.mail-ellipsis{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;min-width:0}.mail-status{min-height:1.2em}.mail-empty-line{margin:.25rem 0}.mail-editor-fallback{min-height:8rem;font-family:inherit;resize:vertical}.mail-clause{display:flex;gap:.3rem;align-items:center;flex-wrap:wrap}.mail-clause .form-select,.mail-clause .form-input{width:auto;flex:0 1 auto}.mail-clause .mail-clause-value{flex:1 1 8rem;min-width:8rem}.mail-section-list{display:flex;flex-direction:column;gap:.5rem}.mail-scroll-sm{max-height:20vh;overflow-y:auto}.mail-scroll-md{max-height:46vh;overflow-y:auto}.mail-section-form .domma-form-field:has(select) .form-label .text-danger{display:none}.mail-compose-form{padding:.25rem 0 .5rem;display:flex;flex-direction:column;gap:.6rem;height:100%}.mail-compose-body{display:flex;flex-direction:column;flex:1;min-height:0}.mail-compose-editor{flex:1;min-height:0}.mail-compose-textarea{flex:1;min-height:34vh;font-family:inherit;resize:vertical}.mail-attach-list{display:flex;flex-direction:column;gap:.2rem}.mail-template-row{margin:-.15rem 0 .35rem}.mail-template-picker{max-width:16rem}.mail-suggest-row{display:flex;flex-direction:column;line-height:1.25}.mail-suggest-detail{opacity:.7}.mail-rule-off{opacity:.55}.mail-panel{padding:.25rem 0 .5rem}.mail-panel-stack{padding:.25rem 0 .5rem;display:flex;flex-direction:column;gap:1rem}.mail-form-stack{padding:.25rem 0 .5rem;display:flex;flex-direction:column;gap:.75rem}.mail-dialog-stack{display:flex;flex-direction:column;gap:.5rem;padding:.25rem 0}.mail-hint{margin-top:.2rem}.mail-box-sub{overflow:hidden;text-overflow:ellipsis}.mail-add-btn{align-self:flex-start;margin-top:.5rem}.mail-none{margin:0;padding:.5rem .6rem}.mail-kv{padding:.5rem 0;display:flex;flex-direction:column;gap:.35rem}.mail-kv-row{display:flex;gap:.75rem}.mail-kv-key{min-width:8.5rem;flex-shrink:0}.mail-toggle-row{display:flex;align-items:center;gap:.5rem;margin-bottom:.75rem}.mail-sender-list{display:flex;flex-direction:column;gap:.2rem;max-height:40vh;overflow-y:auto}.mail-folder-pick{display:flex;align-items:center;gap:.5rem;padding:.3rem .4rem;border-radius:var(--dm-radius-md, 6px)}.mail-diag-log{width:100%;height:30vh;margin-top:.5rem;font-family:monospace;font-size:.72rem}.mail-facts{display:flex;flex-direction:column;gap:.2rem;margin-bottom:.75rem}.mail-item-more{position:absolute;right:.4rem;bottom:.35rem;display:inline-flex;align-items:center;justify-content:center;min-width:1.4rem;height:1.4rem;border-radius:var(--dm-radius-sm, 3px);font-size:1rem;line-height:1;color:var(--dm-text-muted);background:inherit;opacity:0;transition:opacity .12s ease}.mail-item:hover .mail-item-more,.mail-item:focus-visible .mail-item-more,.mail-item.is-active .mail-item-more{opacity:1}.mail-item-more:hover{color:var(--dm-primary)}.mail-section-intro{display:flex;align-items:center;gap:.35rem;margin:0 0 .75rem;padding-bottom:.5rem;border-bottom:1px solid var(--dm-border)}.mail-pager-middle{display:flex;align-items:center;gap:.5rem;min-width:0}.mail-page-size{font:inherit;color:inherit;background:transparent;border:1px solid var(--dm-border);border-radius:4px;padding:.1rem .25rem}.mail-auth-choice{display:flex;flex-wrap:wrap;align-items:center;gap:.4rem;padding-bottom:.5rem;border-bottom:1px solid var(--dm-border)}.mail-oauth{display:flex;flex-direction:column;gap:.75rem}.mail-oauth-warn{margin:0;padding:.5rem .7rem;border:1px solid var(--dm-warning);border-radius:var(--dm-radius-md);color:var(--dm-warning-text)}.mail-oauth-state{display:flex;align-items:center;gap:.5rem;flex-wrap:wrap}.mail-oauth-chip{margin-left:.4rem;border:1px solid var(--dm-border);background:var(--dm-surface);color:var(--dm-text-muted)}.mail-oauth-state .mail-oauth-chip{margin-left:0}.mail-oauth-chip--ok{border-color:var(--dm-success);color:var(--dm-success)}.mail-oauth-chip--bad{border-color:var(--dm-danger);color:var(--dm-danger)}.mail-oauth-guide{border:1px solid var(--dm-border);border-radius:var(--dm-radius-md);padding:.5rem .75rem;background:var(--dm-background-alt)}.mail-oauth-guide>summary{cursor:pointer;font-weight:600}.mail-oauth-guide-body ol{padding-left:1.2rem;margin:.5rem 0 0}.mail-oauth-guide-body li{margin-bottom:.45rem}.mail-oauth-code{word-break:break-all;user-select:all}.mail-loading-slot{display:block;min-height:3rem;padding:.5rem .6rem}.mail-row-more{flex-shrink:0;margin-left:auto;display:inline-flex;align-items:center;justify-content:center;min-width:1.3rem;height:1.3rem;border-radius:var(--dm-radius-sm);font-size:.95rem;line-height:1;color:var(--dm-text-muted);opacity:0;transition:opacity .12s ease}.mail-unread-pill+.mail-row-more{margin-left:0}.mail-folder:hover .mail-row-more,.mail-folder:focus-visible .mail-row-more,.mail-folder.is-active .mail-row-more,.mail-account:hover .mail-row-more,.mail-account:focus-visible .mail-row-more,.mail-account.is-active .mail-row-more{opacity:1}.mail-row-more:hover{color:var(--dm-primary)}.mail-box-row .mail-row-more{opacity:1;margin-left:0}.mail-box-row:focus-visible{outline:2px solid var(--dm-primary);outline-offset:-2px}.mail-restored-note{margin:0;padding:.4rem .6rem;border-left:3px solid var(--dm-warning);background:var(--dm-background-alt)}.mail-font-controls .mail-font-btn{width:auto;min-width:3.5rem;max-width:10rem;padding:0 .5rem;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:.8125rem}.domma-dropdown-menu:has(.mail-font-item),.domma-dropdown-menu:has(.mail-size-item){max-height:min(60vh,26rem);overflow-y:auto;z-index:10050}.domma-dropdown-item:has(.mail-font-item){display:flex;align-items:center;justify-content:space-between;gap:.75rem}.mail-font-item{font-size:.9375rem}.mail-font-web{font-size:.6875rem;color:var(--dm-text-muted);text-transform:uppercase;letter-spacing:.04em}
|
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
*
|
|
23
23
|
* @module _lib/admin/mail/section-kit
|
|
24
24
|
*/
|
|
25
|
+
import {installFontControls} from './font-controls.js';
|
|
25
26
|
|
|
26
27
|
/**
|
|
27
28
|
* A `<div>` with Domma classes on it, appended to a parent.
|
|
@@ -215,12 +216,14 @@ export function richEditor(parent, html, {height = '10rem', text = ''} = {}) {
|
|
|
215
216
|
try {
|
|
216
217
|
const editor = E.editor(host, {mode: 'rich', height});
|
|
217
218
|
editor.setValue(html ?? '');
|
|
219
|
+
let fonts = null;
|
|
220
|
+
try { fonts = installFontControls(editor); } catch { /* no menus, still an editor */ }
|
|
218
221
|
return {
|
|
219
222
|
rich: true,
|
|
220
223
|
get: () => editor.getValue(),
|
|
221
224
|
getText: () => editor.getText(),
|
|
222
225
|
set: value => editor.setValue(value ?? ''),
|
|
223
|
-
destroy: () => editor.destroy?.()
|
|
226
|
+
destroy: () => { fonts?.destroy(); editor.destroy?.(); }
|
|
224
227
|
};
|
|
225
228
|
} catch {
|
|
226
229
|
host.textContent = '';
|