@novacraft-engineering/mailbox 0.2.0 → 0.3.0
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/app/api/mail/emails/route.ts +3 -2
- package/app/api/mail/inbox/route.ts +1 -1
- package/app/mail/MailSelect.tsx +21 -13
- package/app/mail/RichEditor.tsx +11 -13
- package/app/mail/Ticker.tsx +34 -2
- package/app/mail/page.module.css +106 -12
- package/app/mail/page.tsx +259 -82
- package/app/mail/quoted.ts +162 -0
- package/app/mail/search.ts +50 -0
- package/lib/mailbox.ts +4 -0
- package/package.json +1 -2
- package/tools/__pycache__/import-mbox.cpython-314.pyc +0 -0
- package/tools/import-mbox.py +36 -11
- package/app/api/mail/upload/route.ts +0 -43
|
@@ -12,7 +12,7 @@ function fromAddressOf(email: SentEmail): string {
|
|
|
12
12
|
|
|
13
13
|
export const runtime = 'nodejs'
|
|
14
14
|
|
|
15
|
-
type SentEmail = { id: string; from: string; to: string[]; subject: string; createdAt: string; scheduledAt: string | null; lastEvent: string }
|
|
15
|
+
type SentEmail = { id: string; from: string; to: string[]; subject: string; createdAt: string; scheduledAt: string | null; lastEvent: string; attachmentCount: number }
|
|
16
16
|
|
|
17
17
|
const KNOWN_AUTO_SUBJECTS = new Set([`Reset your ${BRAND.name} Mail password`])
|
|
18
18
|
|
|
@@ -65,6 +65,7 @@ export async function GET(req: Request) {
|
|
|
65
65
|
createdAt: item.createdAt,
|
|
66
66
|
scheduledAt: null,
|
|
67
67
|
lastEvent: item.lastEvent ?? '',
|
|
68
|
+
attachmentCount: item.attachmentCount ?? 0,
|
|
68
69
|
}))
|
|
69
70
|
|
|
70
71
|
// Sent mail is scoped the same way the inbox is: to the signed-in mailbox, whatever
|
|
@@ -106,7 +107,7 @@ export async function GET(req: Request) {
|
|
|
106
107
|
size: null,
|
|
107
108
|
read: true,
|
|
108
109
|
starred: Boolean(flagOf[email.id]?.starred),
|
|
109
|
-
hasAttachment:
|
|
110
|
+
hasAttachment: (email.attachmentCount ?? 0) > 0,
|
|
110
111
|
labels: [],
|
|
111
112
|
folder: flagOf[email.id]?.trashed ? 'trash' : flagOf[email.id]?.archived ? 'archive' : 'sent',
|
|
112
113
|
}),
|
|
@@ -37,7 +37,7 @@ export async function GET(req: Request) {
|
|
|
37
37
|
// Counting scans the whole match. Unfiltered, the sidebar already has this number
|
|
38
38
|
// from countFolders, so only a search — whose match nothing else knows — pays for it.
|
|
39
39
|
threadId,
|
|
40
|
-
withTotal: !cursor && Boolean(text),
|
|
40
|
+
withTotal: !cursor && Boolean(text || params.get('from') || params.get('to') || params.get('label') || params.get('unread') || params.get('starred') || params.get('attachment')),
|
|
41
41
|
unread: params.get('unread') === '1' ? true : undefined,
|
|
42
42
|
starred: params.get('starred') === '1' ? true : undefined,
|
|
43
43
|
hasAttachment: params.get('attachment') === '1' || undefined,
|
package/app/mail/MailSelect.tsx
CHANGED
|
@@ -12,6 +12,16 @@ export function anchoredMenuStyle(anchor: HTMLElement): React.CSSProperties {
|
|
|
12
12
|
: { ...base, top: rect.bottom + 6 }
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
+
export const GLYPH = {
|
|
16
|
+
font: <span className={styles.selectGlyphSerif}>A</span>,
|
|
17
|
+
size: 'tT',
|
|
18
|
+
spacing: (
|
|
19
|
+
<svg viewBox="0 0 12 12" fill="none" aria-hidden>
|
|
20
|
+
<path d="M3 2v8M1.6 3.6L3 2l1.4 1.6M1.6 8.4L3 10l1.4-1.6M6.5 4.2h4M6.5 7.8h4" stroke="currentColor" strokeWidth="1.3" strokeLinecap="round" strokeLinejoin="round" />
|
|
21
|
+
</svg>
|
|
22
|
+
),
|
|
23
|
+
}
|
|
24
|
+
|
|
15
25
|
export const CHEVRON = (
|
|
16
26
|
<svg className={styles.selectChevron} viewBox="0 0 12 8" fill="none" aria-hidden>
|
|
17
27
|
<path d="M1 1l5 5 5-5" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round" strokeLinejoin="round" />
|
|
@@ -28,11 +38,10 @@ export default function MailSelect({
|
|
|
28
38
|
editable = false,
|
|
29
39
|
optionStyle,
|
|
30
40
|
prefix,
|
|
31
|
-
|
|
32
|
-
title,
|
|
41
|
+
label,
|
|
33
42
|
}: {
|
|
34
43
|
value: string
|
|
35
|
-
options: Array<{ value: string; label: string }>
|
|
44
|
+
options: Array<{ value: string; label: string; closed?: string }>
|
|
36
45
|
onChange: (value: string) => void
|
|
37
46
|
ariaLabel: string
|
|
38
47
|
buttonClassName?: string
|
|
@@ -40,11 +49,8 @@ export default function MailSelect({
|
|
|
40
49
|
/** Typed values are accepted too: committed on Enter or when the field loses focus. */
|
|
41
50
|
editable?: boolean
|
|
42
51
|
optionStyle?: (value: string) => React.CSSProperties
|
|
43
|
-
/** A quiet glyph before the value, so three selects in a row can be told apart. */
|
|
44
52
|
prefix?: React.ReactNode
|
|
45
|
-
|
|
46
|
-
compact?: boolean
|
|
47
|
-
title?: string
|
|
53
|
+
label?: string
|
|
48
54
|
}) {
|
|
49
55
|
const [open, setOpen] = useState(false)
|
|
50
56
|
const [menuStyle, setMenuStyle] = useState<React.CSSProperties>({})
|
|
@@ -72,24 +78,26 @@ export default function MailSelect({
|
|
|
72
78
|
}, [open])
|
|
73
79
|
|
|
74
80
|
const current = options.find(option => option.value === value)
|
|
81
|
+
const shown = current?.closed ?? current?.label
|
|
75
82
|
const toggle = () => {
|
|
76
83
|
if (!open && anchorRef.current) setMenuStyle(anchoredMenuStyle(anchorRef.current))
|
|
77
84
|
setOpen(prev => !prev)
|
|
78
85
|
}
|
|
79
86
|
const commitTyped = () => {
|
|
80
|
-
if (typed !== null && typed !== (
|
|
87
|
+
if (typed !== null && typed !== (shown ?? value)) onChange(typed.trim())
|
|
81
88
|
setTyped(null)
|
|
82
89
|
}
|
|
83
90
|
|
|
84
91
|
return (
|
|
85
92
|
<span className={styles.select} ref={wrapRef}>
|
|
86
93
|
{editable ? (
|
|
87
|
-
<span ref={anchorRef as React.RefObject<HTMLSpanElement>} className={`${styles.selectBtn} ${styles.selectEditable} ${buttonClassName ?? ''}`}
|
|
94
|
+
<span ref={anchorRef as React.RefObject<HTMLSpanElement>} className={`${styles.selectBtn} ${styles.selectEditable} ${buttonClassName ?? ''}`}>
|
|
88
95
|
{prefix && <span className={styles.selectGlyph} aria-hidden>{prefix}</span>}
|
|
96
|
+
{label && <span className={styles.selectLabel}>{label}</span>}
|
|
89
97
|
<input
|
|
90
98
|
className={styles.selectInput}
|
|
91
99
|
aria-label={ariaLabel}
|
|
92
|
-
value={typed ??
|
|
100
|
+
value={typed ?? shown ?? value}
|
|
93
101
|
placeholder={placeholder}
|
|
94
102
|
onFocus={() => { if (!open) toggle() }}
|
|
95
103
|
onChange={event => setTyped(event.target.value)}
|
|
@@ -113,12 +121,12 @@ export default function MailSelect({
|
|
|
113
121
|
className={`${styles.selectBtn} ${buttonClassName ?? ''}`}
|
|
114
122
|
aria-haspopup="listbox"
|
|
115
123
|
aria-expanded={open}
|
|
116
|
-
aria-label={
|
|
117
|
-
title={title ?? (compact ? `${ariaLabel}: ${current?.label ?? placeholder}` : undefined)}
|
|
124
|
+
aria-label={ariaLabel}
|
|
118
125
|
onClick={toggle}
|
|
119
126
|
>
|
|
120
127
|
{prefix && <span className={styles.selectGlyph} aria-hidden>{prefix}</span>}
|
|
121
|
-
{
|
|
128
|
+
{label && <span className={styles.selectLabel}>{label}</span>}
|
|
129
|
+
<span style={optionStyle && current?.value ? optionStyle(current.value) : undefined}>{shown ?? placeholder}</span>
|
|
122
130
|
{CHEVRON}
|
|
123
131
|
</button>
|
|
124
132
|
)}
|
package/app/mail/RichEditor.tsx
CHANGED
|
@@ -10,7 +10,7 @@ import { CLIENT_BRAND } from '@/lib/brand.client'
|
|
|
10
10
|
import TextAlign from '@tiptap/extension-text-align'
|
|
11
11
|
import { FontFamily, FontSize, TextStyle } from '@tiptap/extension-text-style'
|
|
12
12
|
import { LINE_SPACINGS, BUILTIN_FONTS, FONT_SIZES, fontStack, lineSpacingOf, paragraphGap, type BaseFont } from '@/lib/fonts'
|
|
13
|
-
import MailSelect from './MailSelect'
|
|
13
|
+
import MailSelect, { GLYPH } from './MailSelect'
|
|
14
14
|
import { Color } from '@tiptap/extension-color'
|
|
15
15
|
import Highlight from '@tiptap/extension-highlight'
|
|
16
16
|
import { Table, TableCell, TableHeader, TableRow } from '@tiptap/extension-table'
|
|
@@ -631,12 +631,10 @@ function Toolbar({ editor, uploadImage, fonts = [], baseFont }: { editor: Editor
|
|
|
631
631
|
<MailSelect
|
|
632
632
|
buttonClassName={styles.rteSelect}
|
|
633
633
|
ariaLabel="Font"
|
|
634
|
-
|
|
635
|
-
prefix={
|
|
636
|
-
compact
|
|
637
|
-
title={`Font: ${currentFamily || `${defaultFamily} (default)`}`}
|
|
634
|
+
label="Font"
|
|
635
|
+
prefix={GLYPH.font}
|
|
638
636
|
value={currentFamily}
|
|
639
|
-
options={[{ value: '', label: `${defaultFamily} (default)
|
|
637
|
+
options={[{ value: '', label: `${defaultFamily} (default)`, closed: defaultFamily }, ...[...BUILTIN_FONTS, ...fonts.filter(font => !BUILTIN_FONTS.includes(font))].map(font => ({ value: font, label: font }))]}
|
|
640
638
|
optionStyle={font => (font ? { fontFamily: `'${font}', Arial, sans-serif` } : {})}
|
|
641
639
|
onChange={family => {
|
|
642
640
|
if (family) editor.chain().focus().setFontFamily(family).run()
|
|
@@ -646,12 +644,12 @@ function Toolbar({ editor, uploadImage, fonts = [], baseFont }: { editor: Editor
|
|
|
646
644
|
<MailSelect
|
|
647
645
|
buttonClassName={`${styles.rteSelect} ${styles.rteSizeSelect}`}
|
|
648
646
|
ariaLabel="Font size"
|
|
647
|
+
label="Size"
|
|
649
648
|
placeholder={defaultSize}
|
|
650
|
-
prefix=
|
|
651
|
-
title={`Size — default ${defaultSize}`}
|
|
649
|
+
prefix={GLYPH.size}
|
|
652
650
|
editable
|
|
653
651
|
value={(editor.getAttributes('textStyle').fontSize as string | undefined) ?? ''}
|
|
654
|
-
options={[{ value: '', label: `${defaultSize} (default)
|
|
652
|
+
options={[{ value: '', label: `${defaultSize} (default)`, closed: defaultSize }, ...FONT_SIZES.map(size => ({ value: size, label: size.replace('px', '') }))]}
|
|
655
653
|
onChange={raw => {
|
|
656
654
|
const size = /^\d+(\.\d+)?$/.test(raw) ? `${raw}px` : raw
|
|
657
655
|
if (!size || /default/i.test(size)) editor.chain().focus().unsetFontSize().run()
|
|
@@ -660,13 +658,13 @@ function Toolbar({ editor, uploadImage, fonts = [], baseFont }: { editor: Editor
|
|
|
660
658
|
/>
|
|
661
659
|
<MailSelect
|
|
662
660
|
buttonClassName={`${styles.rteSelect} ${styles.rteSizeSelect}`}
|
|
663
|
-
ariaLabel="Line
|
|
661
|
+
ariaLabel="Line height"
|
|
662
|
+
label="Line height"
|
|
664
663
|
placeholder={defaultSpacing}
|
|
665
|
-
prefix=
|
|
666
|
-
title={`Line spacing — default ${defaultSpacing}`}
|
|
664
|
+
prefix={GLYPH.spacing}
|
|
667
665
|
editable
|
|
668
666
|
value={editor.getAttributes('paragraph').lineSpacing ? String(editor.getAttributes('paragraph').lineSpacing) : ''}
|
|
669
|
-
options={[{ value: '', label: `${defaultSpacing} (default)
|
|
667
|
+
options={[{ value: '', label: `${defaultSpacing} (default)`, closed: defaultSpacing }, ...LINE_SPACINGS.map(spacing => ({ value: String(spacing), label: String(spacing) }))]}
|
|
670
668
|
onChange={raw => {
|
|
671
669
|
const spacing = parseFloat(raw)
|
|
672
670
|
if (!raw.trim() || /default/i.test(raw)) editor.chain().focus().updateAttributes('paragraph', { lineSpacing: null }).run()
|
package/app/mail/Ticker.tsx
CHANGED
|
@@ -3,17 +3,49 @@
|
|
|
3
3
|
import { useEffect, useRef, type ReactNode } from 'react'
|
|
4
4
|
import styles from './page.module.css'
|
|
5
5
|
|
|
6
|
+
export const TICKER_SPOTS = [
|
|
7
|
+
{ group: 'Mail list', on: false, spots: [['listFrom', 'Sender'], ['listSubject', 'Subject'], ['listSnippet', 'Preview']] },
|
|
8
|
+
{ group: 'Conversation', on: true, spots: [['threadFrom', 'Sender'], ['threadSnippet', 'Preview']] },
|
|
9
|
+
{ group: 'Reader', on: true, spots: [['readerFrom', 'From'], ['readerTo', 'To and Cc']] },
|
|
10
|
+
{ group: 'Attachments', on: true, spots: [['attachName', 'File names in the row'], ['attachChip', 'File names in the composer']] },
|
|
11
|
+
{ group: 'Reply bar', on: true, spots: [['replyRecipients', 'Recipients']] },
|
|
12
|
+
] as const
|
|
13
|
+
|
|
14
|
+
export type TickerSpot = (typeof TICKER_SPOTS)[number]['spots'][number][0]
|
|
15
|
+
export type TickerSettings = Partial<Record<TickerSpot, boolean>>
|
|
16
|
+
|
|
17
|
+
const DEFAULT_ON = new Map<string, boolean>(TICKER_SPOTS.flatMap(group => group.spots.map(([spot]) => [spot, group.on])))
|
|
18
|
+
|
|
19
|
+
export function tickerDefault(spot: TickerSpot): boolean {
|
|
20
|
+
return DEFAULT_ON.get(spot) ?? true
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function tickerSettingsFrom(raw: unknown): TickerSettings {
|
|
24
|
+
const kept: TickerSettings = {}
|
|
25
|
+
if (raw && typeof raw === 'object') {
|
|
26
|
+
for (const [spot, value] of Object.entries(raw)) {
|
|
27
|
+
if (DEFAULT_ON.has(spot) && typeof value === 'boolean') kept[spot as TickerSpot] = value
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return kept
|
|
31
|
+
}
|
|
32
|
+
|
|
6
33
|
/**
|
|
7
34
|
* One line of text that, when it does not fit, glides back and forth instead of ending in
|
|
8
35
|
* an ellipsis. Measured on mount and whenever the host or its text changes size.
|
|
9
36
|
*/
|
|
10
|
-
export default function Ticker({ className, title, children }: { className?: string; title?: string; children: ReactNode }) {
|
|
37
|
+
export default function Ticker({ className, title, enabled = true, children }: { className?: string; title?: string; enabled?: boolean; children: ReactNode }) {
|
|
11
38
|
const host = useRef<HTMLSpanElement>(null)
|
|
12
39
|
const text = useRef<HTMLSpanElement>(null)
|
|
13
40
|
useEffect(() => {
|
|
14
41
|
const outer = host.current
|
|
15
42
|
const inner = text.current
|
|
16
43
|
if (!outer || !inner) return
|
|
44
|
+
if (!enabled) {
|
|
45
|
+
delete outer.dataset.ticker
|
|
46
|
+
outer.style.removeProperty('--ticker-shift')
|
|
47
|
+
return
|
|
48
|
+
}
|
|
17
49
|
const measure = () => {
|
|
18
50
|
const shift = inner.scrollWidth - outer.clientWidth
|
|
19
51
|
if (shift > 4) {
|
|
@@ -31,7 +63,7 @@ export default function Ticker({ className, title, children }: { className?: str
|
|
|
31
63
|
observer.observe(outer)
|
|
32
64
|
observer.observe(inner)
|
|
33
65
|
return () => observer.disconnect()
|
|
34
|
-
}, [children])
|
|
66
|
+
}, [children, enabled])
|
|
35
67
|
return (
|
|
36
68
|
<span ref={host} className={`${className ?? ''} ${styles.tickerHost}`} title={title}>
|
|
37
69
|
<span ref={text} className={styles.tickerText}>{children}</span>
|
package/app/mail/page.module.css
CHANGED
|
@@ -196,6 +196,7 @@
|
|
|
196
196
|
.folderActive {
|
|
197
197
|
background: var(--nc-ink-3);
|
|
198
198
|
color: var(--nc-fg-1);
|
|
199
|
+
box-shadow: inset 0 0 0 1.5px var(--nc-violet-500);
|
|
199
200
|
}
|
|
200
201
|
.folder svg { width: 16px; height: 16px; flex-shrink: 0; }
|
|
201
202
|
/* Total for the folder — plain and muted. */
|
|
@@ -527,6 +528,12 @@
|
|
|
527
528
|
cursor: pointer;
|
|
528
529
|
}
|
|
529
530
|
.crossFolderHint:hover { border-color: var(--nc-violet-500); }
|
|
531
|
+
.emptyNudge {
|
|
532
|
+
align-self: center;
|
|
533
|
+
margin: 2px 0 0;
|
|
534
|
+
padding: 8px 14px;
|
|
535
|
+
font-size: 13px;
|
|
536
|
+
}
|
|
530
537
|
.refreshBtn svg { width: 14px; height: 14px; }
|
|
531
538
|
.filterChip {
|
|
532
539
|
margin-left: auto;
|
|
@@ -1681,7 +1688,7 @@
|
|
|
1681
1688
|
border-radius: 6px;
|
|
1682
1689
|
background: var(--nc-ink-1);
|
|
1683
1690
|
color: var(--nc-fg-1);
|
|
1684
|
-
font: 500
|
|
1691
|
+
font: 500 11px/1 var(--nc-font-sans);
|
|
1685
1692
|
}
|
|
1686
1693
|
.rteSwatches { display: flex; gap: 3px; }
|
|
1687
1694
|
.rteSwatch {
|
|
@@ -2221,13 +2228,22 @@
|
|
|
2221
2228
|
.selectBtn:hover { border-color: var(--nc-line-2); color: var(--nc-fg-1); }
|
|
2222
2229
|
.selectGlyph {
|
|
2223
2230
|
flex-shrink: 0;
|
|
2224
|
-
|
|
2225
|
-
|
|
2226
|
-
|
|
2231
|
+
display: inline-flex;
|
|
2232
|
+
align-items: center;
|
|
2233
|
+
justify-content: center;
|
|
2234
|
+
width: 16px;
|
|
2235
|
+
height: 16px;
|
|
2236
|
+
font: 700 11px/1 var(--nc-font-sans);
|
|
2237
|
+
letter-spacing: .01em;
|
|
2238
|
+
color: var(--nc-fg-3);
|
|
2239
|
+
}
|
|
2240
|
+
.selectGlyph svg { width: 13px; height: 13px; }
|
|
2241
|
+
.selectGlyphSerif { font: 700 13px/1 Georgia, 'Times New Roman', serif; }
|
|
2242
|
+
.selectLabel {
|
|
2243
|
+
flex-shrink: 0;
|
|
2244
|
+
color: var(--nc-fg-3);
|
|
2245
|
+
white-space: nowrap;
|
|
2227
2246
|
}
|
|
2228
|
-
.selectGlyphSerif { font: 600 14px/1 Georgia, 'Times New Roman', serif; color: var(--nc-fg-3); }
|
|
2229
|
-
.selectEditable .selectGlyph { margin-left: 9px; }
|
|
2230
|
-
.selectEditable .selectGlyph + .selectInput { padding-left: 6px; }
|
|
2231
2247
|
.selectBtn > span {
|
|
2232
2248
|
overflow: hidden;
|
|
2233
2249
|
text-overflow: ellipsis;
|
|
@@ -2270,6 +2286,7 @@
|
|
|
2270
2286
|
.selectOptionOn { background: var(--nc-ink-4); color: var(--nc-violet-200); }
|
|
2271
2287
|
|
|
2272
2288
|
.toolbar .selectBtn { padding: 7px 10px; font-size: 12px; }
|
|
2289
|
+
.selectBtn.rteSelect { font-size: 11.5px; }
|
|
2273
2290
|
.campaignSelectBtn {
|
|
2274
2291
|
width: 100%;
|
|
2275
2292
|
justify-content: space-between;
|
|
@@ -2550,6 +2567,40 @@
|
|
|
2550
2567
|
overflow: hidden;
|
|
2551
2568
|
background: transparent;
|
|
2552
2569
|
}
|
|
2570
|
+
.quoteLine {
|
|
2571
|
+
display: flex;
|
|
2572
|
+
align-items: center;
|
|
2573
|
+
gap: 10px;
|
|
2574
|
+
width: 100%;
|
|
2575
|
+
margin: 2px 0 14px;
|
|
2576
|
+
padding: 0;
|
|
2577
|
+
border: 0;
|
|
2578
|
+
background: transparent;
|
|
2579
|
+
cursor: pointer;
|
|
2580
|
+
}
|
|
2581
|
+
.quoteLineRule {
|
|
2582
|
+
flex: 1;
|
|
2583
|
+
height: 1px;
|
|
2584
|
+
background: var(--nc-line-1);
|
|
2585
|
+
}
|
|
2586
|
+
.quoteLineLabel {
|
|
2587
|
+
display: inline-flex;
|
|
2588
|
+
align-items: center;
|
|
2589
|
+
gap: 5px;
|
|
2590
|
+
flex-shrink: 0;
|
|
2591
|
+
padding: 4px 11px;
|
|
2592
|
+
border: 1px solid var(--nc-line-1);
|
|
2593
|
+
border-radius: 999px;
|
|
2594
|
+
background: var(--nc-ink-2);
|
|
2595
|
+
color: var(--nc-fg-3);
|
|
2596
|
+
font: 500 11.5px/1 var(--nc-font-sans);
|
|
2597
|
+
transition: color var(--dur-fast) var(--nc-ease-out), border-color var(--dur-fast) var(--nc-ease-out);
|
|
2598
|
+
}
|
|
2599
|
+
.quoteLineLabel svg { width: 11px; height: 8px; }
|
|
2600
|
+
.quoteLine:hover .quoteLineLabel { color: var(--nc-fg-1); border-color: var(--nc-line-2); }
|
|
2601
|
+
.quoteLine[aria-expanded='true'] .quoteLineLabel svg { transform: rotate(180deg); }
|
|
2602
|
+
|
|
2603
|
+
.threadMsgStatic .threadMsgHead { cursor: default; }
|
|
2553
2604
|
.threadMsg + .threadMsg { border-top: 1px solid var(--nc-line-1); }
|
|
2554
2605
|
.threadMsgOpen .threadMsgHead { border-bottom: 1px solid var(--nc-line-1); }
|
|
2555
2606
|
.threadMsgHead {
|
|
@@ -2591,6 +2642,34 @@
|
|
|
2591
2642
|
text-overflow: ellipsis;
|
|
2592
2643
|
white-space: nowrap;
|
|
2593
2644
|
}
|
|
2645
|
+
.threadMsgRecips {
|
|
2646
|
+
display: flex;
|
|
2647
|
+
align-items: baseline;
|
|
2648
|
+
justify-content: flex-start;
|
|
2649
|
+
gap: 8px;
|
|
2650
|
+
width: 100%;
|
|
2651
|
+
min-width: 0;
|
|
2652
|
+
margin-top: 3px;
|
|
2653
|
+
overflow: hidden;
|
|
2654
|
+
}
|
|
2655
|
+
.threadMsgRecipsText {
|
|
2656
|
+
/* Hugs the addresses so Details sits beside them rather than out at the far margin. */
|
|
2657
|
+
flex: 0 1 auto;
|
|
2658
|
+
min-width: 0;
|
|
2659
|
+
font: 400 11.5px/1.35 var(--nc-font-sans);
|
|
2660
|
+
color: var(--nc-fg-3);
|
|
2661
|
+
}
|
|
2662
|
+
.threadMsgDetails {
|
|
2663
|
+
flex-shrink: 0;
|
|
2664
|
+
padding: 0;
|
|
2665
|
+
border: 0;
|
|
2666
|
+
background: transparent;
|
|
2667
|
+
color: var(--nc-violet-500);
|
|
2668
|
+
font: 500 11px/1.35 var(--nc-font-sans);
|
|
2669
|
+
cursor: pointer;
|
|
2670
|
+
}
|
|
2671
|
+
.threadMsgDetails:hover { text-decoration: underline; }
|
|
2672
|
+
|
|
2594
2673
|
.threadMsgDate {
|
|
2595
2674
|
font: 400 11px/1 var(--nc-font-mono);
|
|
2596
2675
|
color: var(--nc-fg-3);
|
|
@@ -3935,6 +4014,18 @@
|
|
|
3935
4014
|
font: 500 14px/1.4 var(--nc-font-sans);
|
|
3936
4015
|
color: var(--nc-fg-1);
|
|
3937
4016
|
}
|
|
4017
|
+
.tickerGroup {
|
|
4018
|
+
display: flex;
|
|
4019
|
+
flex-direction: column;
|
|
4020
|
+
gap: 6px;
|
|
4021
|
+
margin-top: 12px;
|
|
4022
|
+
}
|
|
4023
|
+
.tickerGroupName {
|
|
4024
|
+
font: 600 11px/1 var(--nc-font-mono);
|
|
4025
|
+
letter-spacing: .04em;
|
|
4026
|
+
text-transform: uppercase;
|
|
4027
|
+
color: var(--nc-fg-3);
|
|
4028
|
+
}
|
|
3938
4029
|
.settingsToggle input {
|
|
3939
4030
|
width: 16px;
|
|
3940
4031
|
height: 16px;
|
|
@@ -6592,20 +6683,23 @@
|
|
|
6592
6683
|
.fontInput { flex: 1 1 160px; height: 34px; padding: 0 10px; border: 1px solid var(--nc-line-1); border-radius: 8px; background: var(--nc-ink-1); color: var(--nc-fg-1); font: 500 13px/1 var(--nc-font-sans); }
|
|
6593
6684
|
.fontUpload { height: 34px; display: inline-flex; align-items: center; padding: 0 12px; border: 1px solid var(--nc-line-1); border-radius: 8px; font: 500 13px/1 var(--nc-font-sans); color: var(--nc-fg-1); cursor: pointer; }
|
|
6594
6685
|
|
|
6595
|
-
.selectEditable { padding: 0 6px 0
|
|
6596
|
-
.selectInput {
|
|
6686
|
+
.selectEditable { padding: 0 6px 0 8px; gap: 6px; }
|
|
6687
|
+
.selectBtn .selectInput {
|
|
6597
6688
|
width: 3.5em;
|
|
6598
6689
|
min-width: 0;
|
|
6599
6690
|
height: 100%;
|
|
6600
|
-
padding: 0
|
|
6691
|
+
padding: 0;
|
|
6601
6692
|
border: 0;
|
|
6693
|
+
border-radius: 0;
|
|
6602
6694
|
background: transparent;
|
|
6695
|
+
box-shadow: none;
|
|
6696
|
+
appearance: none;
|
|
6603
6697
|
color: inherit;
|
|
6604
6698
|
font: inherit;
|
|
6605
6699
|
outline: none;
|
|
6606
6700
|
}
|
|
6607
|
-
|
|
6608
|
-
.rteSizeSelect .selectInput {
|
|
6701
|
+
.selectBtn .selectInput:focus { border: 0; box-shadow: none; }
|
|
6702
|
+
.rteSizeSelect .selectInput { width: 3.2em; }
|
|
6609
6703
|
.selectChevronBtn { display: inline-flex; align-items: center; padding: 0 4px; border: 0; background: transparent; color: inherit; cursor: pointer; }
|
|
6610
6704
|
.mailboxDomainSelect.selectBtn { padding: 4px 6px; background: transparent; border: 0; }
|
|
6611
6705
|
|