@novacraft-engineering/mailbox 0.2.0 → 0.2.1
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/mail/MailSelect.tsx +21 -13
- package/app/mail/RichEditor.tsx +11 -13
- package/app/mail/page.module.css +25 -12
- package/app/mail/page.tsx +7 -7
- package/package.json +1 -1
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/page.module.css
CHANGED
|
@@ -1681,7 +1681,7 @@
|
|
|
1681
1681
|
border-radius: 6px;
|
|
1682
1682
|
background: var(--nc-ink-1);
|
|
1683
1683
|
color: var(--nc-fg-1);
|
|
1684
|
-
font: 500
|
|
1684
|
+
font: 500 11px/1 var(--nc-font-sans);
|
|
1685
1685
|
}
|
|
1686
1686
|
.rteSwatches { display: flex; gap: 3px; }
|
|
1687
1687
|
.rteSwatch {
|
|
@@ -2221,13 +2221,22 @@
|
|
|
2221
2221
|
.selectBtn:hover { border-color: var(--nc-line-2); color: var(--nc-fg-1); }
|
|
2222
2222
|
.selectGlyph {
|
|
2223
2223
|
flex-shrink: 0;
|
|
2224
|
-
|
|
2225
|
-
|
|
2226
|
-
|
|
2224
|
+
display: inline-flex;
|
|
2225
|
+
align-items: center;
|
|
2226
|
+
justify-content: center;
|
|
2227
|
+
width: 16px;
|
|
2228
|
+
height: 16px;
|
|
2229
|
+
font: 700 11px/1 var(--nc-font-sans);
|
|
2230
|
+
letter-spacing: .01em;
|
|
2231
|
+
color: var(--nc-fg-3);
|
|
2232
|
+
}
|
|
2233
|
+
.selectGlyph svg { width: 13px; height: 13px; }
|
|
2234
|
+
.selectGlyphSerif { font: 700 13px/1 Georgia, 'Times New Roman', serif; }
|
|
2235
|
+
.selectLabel {
|
|
2236
|
+
flex-shrink: 0;
|
|
2237
|
+
color: var(--nc-fg-3);
|
|
2238
|
+
white-space: nowrap;
|
|
2227
2239
|
}
|
|
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
2240
|
.selectBtn > span {
|
|
2232
2241
|
overflow: hidden;
|
|
2233
2242
|
text-overflow: ellipsis;
|
|
@@ -2270,6 +2279,7 @@
|
|
|
2270
2279
|
.selectOptionOn { background: var(--nc-ink-4); color: var(--nc-violet-200); }
|
|
2271
2280
|
|
|
2272
2281
|
.toolbar .selectBtn { padding: 7px 10px; font-size: 12px; }
|
|
2282
|
+
.selectBtn.rteSelect { font-size: 11.5px; }
|
|
2273
2283
|
.campaignSelectBtn {
|
|
2274
2284
|
width: 100%;
|
|
2275
2285
|
justify-content: space-between;
|
|
@@ -6592,20 +6602,23 @@
|
|
|
6592
6602
|
.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
6603
|
.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
6604
|
|
|
6595
|
-
.selectEditable { padding: 0 6px 0
|
|
6596
|
-
.selectInput {
|
|
6605
|
+
.selectEditable { padding: 0 6px 0 8px; gap: 6px; }
|
|
6606
|
+
.selectBtn .selectInput {
|
|
6597
6607
|
width: 3.5em;
|
|
6598
6608
|
min-width: 0;
|
|
6599
6609
|
height: 100%;
|
|
6600
|
-
padding: 0
|
|
6610
|
+
padding: 0;
|
|
6601
6611
|
border: 0;
|
|
6612
|
+
border-radius: 0;
|
|
6602
6613
|
background: transparent;
|
|
6614
|
+
box-shadow: none;
|
|
6615
|
+
appearance: none;
|
|
6603
6616
|
color: inherit;
|
|
6604
6617
|
font: inherit;
|
|
6605
6618
|
outline: none;
|
|
6606
6619
|
}
|
|
6607
|
-
|
|
6608
|
-
.rteSizeSelect .selectInput {
|
|
6620
|
+
.selectBtn .selectInput:focus { border: 0; box-shadow: none; }
|
|
6621
|
+
.rteSizeSelect .selectInput { width: 3.2em; }
|
|
6609
6622
|
.selectChevronBtn { display: inline-flex; align-items: center; padding: 0 4px; border: 0; background: transparent; color: inherit; cursor: pointer; }
|
|
6610
6623
|
.mailboxDomainSelect.selectBtn { padding: 4px 6px; background: transparent; border: 0; }
|
|
6611
6624
|
|
package/app/mail/page.tsx
CHANGED
|
@@ -10,7 +10,7 @@ import { subscribePush, unsubscribePush, useInstall, useNotifications } from './
|
|
|
10
10
|
import RichEditor from './RichEditor'
|
|
11
11
|
import { inlineEmailStyles, htmlToPlainText, dropUnreachableImages, outlookSafeImages, stripOwnPixel } from '@/lib/email-html'
|
|
12
12
|
import { BUILTIN_FONTS, DEFAULT_LINE_SPACING, EMPTY_FONT, FONT_SIZES, LINE_SPACINGS, fontFaceCss, fontStack, lineSpacingOf, type BaseFont, type CustomFont, paragraphGap } from '@/lib/fonts'
|
|
13
|
-
import MailSelect from './MailSelect'
|
|
13
|
+
import MailSelect, { GLYPH } from './MailSelect'
|
|
14
14
|
import Ticker from './Ticker'
|
|
15
15
|
import { applyThreadFlagDeltas, normalizeSubject } from '@/lib/threads'
|
|
16
16
|
import { defaultSignature, fillSignature } from '@/lib/default-signature'
|
|
@@ -8221,19 +8221,19 @@ export default function DevMailPage() {
|
|
|
8221
8221
|
<div className={styles.fontAdd}>
|
|
8222
8222
|
<MailSelect
|
|
8223
8223
|
ariaLabel="Default font"
|
|
8224
|
-
prefix={
|
|
8224
|
+
prefix={GLYPH.font}
|
|
8225
8225
|
value={settings.defaultFont?.family ?? ''}
|
|
8226
|
-
options={[{ value: '', label: 'Arial (standard)' }, ...[...BUILTIN_FONTS, ...(settings.fonts ?? []).map(font => font.name)].filter((font, index, all) => all.indexOf(font) === index).map(font => ({ value: font, label: font }))]}
|
|
8226
|
+
options={[{ value: '', label: 'Arial (standard)', closed: 'Arial' }, ...[...BUILTIN_FONTS, ...(settings.fonts ?? []).map(font => font.name)].filter((font, index, all) => all.indexOf(font) === index).map(font => ({ value: font, label: font }))]}
|
|
8227
8227
|
optionStyle={font => (font ? { fontFamily: `'${font}', Arial, sans-serif` } : {})}
|
|
8228
8228
|
onChange={family => setMailSettings(current => ({ ...current, defaultFont: { ...(current.defaultFont ?? EMPTY_FONT), family } }))}
|
|
8229
8229
|
/>
|
|
8230
8230
|
<MailSelect
|
|
8231
8231
|
ariaLabel="Default size"
|
|
8232
|
-
prefix=
|
|
8232
|
+
prefix={GLYPH.size}
|
|
8233
8233
|
editable
|
|
8234
8234
|
placeholder="15"
|
|
8235
8235
|
value={settings.defaultFont?.size ?? ''}
|
|
8236
|
-
options={[{ value: '', label: '15 (standard)' }, ...FONT_SIZES.map(size => ({ value: size, label: size.replace('px', '') }))]}
|
|
8236
|
+
options={[{ value: '', label: '15 (standard)', closed: '15' }, ...FONT_SIZES.map(size => ({ value: size, label: size.replace('px', '') }))]}
|
|
8237
8237
|
onChange={raw => {
|
|
8238
8238
|
const size = /^\d+(\.\d+)?$/.test(raw) ? `${raw}px` : raw
|
|
8239
8239
|
if (size && !/^\d+(\.\d+)?(px|pt|em|rem|%)$/.test(size)) return
|
|
@@ -8248,12 +8248,12 @@ export default function DevMailPage() {
|
|
|
8248
8248
|
<div className={styles.fontAdd}>
|
|
8249
8249
|
<MailSelect
|
|
8250
8250
|
ariaLabel="Line spacing"
|
|
8251
|
-
prefix=
|
|
8251
|
+
prefix={GLYPH.spacing}
|
|
8252
8252
|
editable
|
|
8253
8253
|
placeholder={String(DEFAULT_LINE_SPACING)}
|
|
8254
8254
|
value={settings.defaultFont?.lineSpacing ? String(settings.defaultFont.lineSpacing) : ''}
|
|
8255
8255
|
options={[
|
|
8256
|
-
{ value: '', label: `${DEFAULT_LINE_SPACING} (standard)
|
|
8256
|
+
{ value: '', label: `${DEFAULT_LINE_SPACING} (standard)`, closed: String(DEFAULT_LINE_SPACING) },
|
|
8257
8257
|
...LINE_SPACINGS.filter(spacing => spacing !== DEFAULT_LINE_SPACING).map(spacing => ({ value: String(spacing), label: String(spacing) })),
|
|
8258
8258
|
]}
|
|
8259
8259
|
onChange={raw => {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@novacraft-engineering/mailbox",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"description": "A shared webmail app: one codebase, one deployment per mailbox. Threads, a rich composer with signatures, attachments on S3-compatible storage, sharing links, full-text search, web push and PWA install.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"webmail",
|