diff --git a/components/email/__tests__/font-size.test.ts b/components/email/__tests__/font-size.test.ts index 46804c9ee..613942edb 100644 --- a/components/email/__tests__/font-size.test.ts +++ b/components/email/__tests__/font-size.test.ts @@ -1,7 +1,7 @@ import { describe, expect, it } from 'vitest'; import { Editor } from '@tiptap/core'; import StarterKit from '@tiptap/starter-kit'; -import { TextStyle } from '@tiptap/extension-text-style'; +import { TextStyle, BackgroundColor } from '@tiptap/extension-text-style'; import Color from '@tiptap/extension-color'; import { FontSize, FONT_SIZES } from '../font-size'; @@ -11,7 +11,7 @@ import { FontSize, FONT_SIZES } from '../font-size'; function makeEditor(content = '

Hello world

') { return new Editor({ element: document.createElement('div'), - extensions: [StarterKit, TextStyle, Color, FontSize], + extensions: [StarterKit, TextStyle, Color, BackgroundColor, FontSize], content, }); } @@ -72,3 +72,61 @@ describe('font-size extension', () => { expect([...FONT_SIZES]).toEqual(['14px', '16px', '18px', '20px', '24px', '32px']); }); }); + +describe('background-color extension', () => { + it('applies background-color as an inline style on the selection', () => { + const editor = makeEditor(); + editor.commands.selectAll(); + editor.commands.setBackgroundColor('#fef3c7'); + const html = editor.getHTML(); + // jsdom normalizes #fef3c7 to rgb() form. + expect(html).toContain('background-color: rgb(254, 243, 199)'); + editor.destroy(); + }); + + it('reports the active tint via the textStyle attributes', () => { + const editor = makeEditor(); + editor.commands.selectAll(); + editor.commands.setBackgroundColor('#dcfce7'); + // getAttributes returns the stored attribute verbatim (no jsdom + // round-trip), unlike getHTML which normalizes to rgb() form. + expect(editor.getAttributes('textStyle').backgroundColor).toBe('#dcfce7'); + editor.destroy(); + }); + + it('unsetBackgroundColor removes the attribute and the empty textStyle mark', () => { + const editor = makeEditor(); + editor.commands.selectAll(); + editor.commands.setBackgroundColor('#dbeafe'); + editor.commands.unsetBackgroundColor(); + expect(editor.getHTML()).not.toContain('background-color'); + editor.destroy(); + }); + + it('parses background-color from pasted inline styles', () => { + const editor = makeEditor('

Marked rest

'); + editor.commands.setTextSelection({ from: 1, to: 5 }); + // parseHTML reads the raw style attribute, so the hex form survives. + expect(editor.getAttributes('textStyle').backgroundColor).toBe('#fde68a'); + editor.destroy(); + }); + + it('keeps color, background-color and font-size on the same span', () => { + const editor = makeEditor(); + editor.commands.selectAll(); + editor.commands.setFontSize('18px'); + editor.commands.setColor('#c5221f'); + editor.commands.setBackgroundColor('#fef3c7'); + const html = editor.getHTML(); + expect(html).toContain('font-size: 18px'); + expect(html).toContain('color: rgb(197, 34, 31)'); + expect(html).toContain('background-color: rgb(254, 243, 199)'); + // Removing the tint must keep the other two attributes. + editor.commands.unsetBackgroundColor(); + const after = editor.getHTML(); + expect(after).not.toContain('background-color'); + expect(after).toContain('font-size: 18px'); + expect(after).toContain('color: rgb(197, 34, 31)'); + editor.destroy(); + }); +}); diff --git a/components/email/rich-text-editor.tsx b/components/email/rich-text-editor.tsx index ba0922666..661cbb066 100644 --- a/components/email/rich-text-editor.tsx +++ b/components/email/rich-text-editor.tsx @@ -9,7 +9,7 @@ import Underline from "@tiptap/extension-underline"; import Link from "@tiptap/extension-link"; import TextAlign from "@tiptap/extension-text-align"; import { TextDirection } from "@/components/email/text-direction"; -import { TextStyle } from "@tiptap/extension-text-style"; +import { TextStyle, BackgroundColor } from "@tiptap/extension-text-style"; import Color from "@tiptap/extension-color"; import { FontSize, FONT_SIZES } from "@/components/email/font-size"; import { ResizableImage } from "@/components/email/resizable-image"; @@ -45,6 +45,8 @@ import { Heading2, Table as TableIcon, Baseline, + Highlighter, + ALargeSmall, Trash2, Rows3, Columns3, @@ -128,6 +130,15 @@ const TEXT_COLORS = [ "#7627bb", "#c2185b", "#795548", "#fa5252", "#fd7e14", "#40c057", "#4dabf7", "#e64980", ]; +// Highlighter presets (2 x 8) - soft, text-legible tints rather than the +// fully-saturated TEXT_COLORS, so highlighted text stays readable. +// Inline `style="background-color: …"` survives email round-trips the same +// way `color` does; the BackgroundColor extension preserves pasted ones. +const BACKGROUND_COLORS = [ + "#fef3c7", "#fee2e2", "#dcfce7", "#dbeafe", "#fae8ff", "#ffe4e6", "#ecfccb", "#f1f5f9", + "#fde68a", "#fecaca", "#bbf7d0", "#bfdbfe", "#f5d0fe", "#ffd6e7", "#e7f9c4", "#e2e8f0", +]; + function TableSizePicker({ onPick }: { onPick: (rows: number, cols: number) => void }) { const t = useTranslations("email_composer.toolbar"); const [hover, setHover] = useState<{ r: number; c: number } | null>(null); @@ -199,6 +210,7 @@ export function RichTextEditor({ }), TextStyle, Color, + BackgroundColor, FontSize, ResizableImage, Placeholder.configure({ @@ -328,6 +340,8 @@ export function RichTextEditor({ const tableWrapperRef = useRef(null); const [colorMenuOpen, setColorMenuOpen] = useState(false); const colorWrapperRef = useRef(null); + const [bgColorMenuOpen, setBgColorMenuOpen] = useState(false); + const bgColorWrapperRef = useRef(null); const [fontSizeMenuOpen, setFontSizeMenuOpen] = useState(false); const fontSizeWrapperRef = useRef(null); @@ -353,6 +367,17 @@ export function RichTextEditor({ return () => document.removeEventListener("mousedown", handler); }, [colorMenuOpen]); + useEffect(() => { + if (!bgColorMenuOpen) return; + const handler = (e: MouseEvent) => { + if (bgColorWrapperRef.current && !bgColorWrapperRef.current.contains(e.target as Node)) { + setBgColorMenuOpen(false); + } + }; + document.addEventListener("mousedown", handler); + return () => document.removeEventListener("mousedown", handler); + }, [bgColorMenuOpen]); + useEffect(() => { if (!tableMenuOpen) return; const handler = (e: MouseEvent) => { @@ -371,6 +396,7 @@ export function RichTextEditor({ } const currentFontSize: string | null = editor.getAttributes("textStyle").fontSize ?? null; + const currentBgColor: string | null = editor.getAttributes("textStyle").backgroundColor ?? null; return (
@@ -382,15 +408,21 @@ export function RichTextEditor({ extra opaque overlay (an always-on stronger overlay would read darker than main even at rest). */}
- {/* Font size */} + {/* Font size - big-A/small-A glyph reads as "size" at a glance, + with the active size as a small live badge. */}
setFontSizeMenuOpen((v) => !v)} title={tToolbar("font_size")} > - - {currentFontSize ? currentFontSize.replace("px", "") : "Aa"} + + + {currentFontSize && ( + + {currentFontSize.replace("px", "")} + + )} {fontSizeMenuOpen && ( @@ -500,6 +532,57 @@ export function RichTextEditor({
)}
+ {/* Background color - same shape as the text-colour control: the + highlighter glyph previews the active tint on its little bar. */} +
+ setBgColorMenuOpen((v) => !v)} + title={tToolbar("background_color")} + > + + + + + + {bgColorMenuOpen && ( +
+
+ {BACKGROUND_COLORS.map((color) => ( +
+
+ +
+ )} +
diff --git a/locales/ar/common.json b/locales/ar/common.json index ec8461c3f..9f0ae9ee9 100644 --- a/locales/ar/common.json +++ b/locales/ar/common.json @@ -730,7 +730,9 @@ "delete_table": "حذف الجدول", "pick_size": "اختيار الحجم", "font_size": "حجم الخط", - "font_size_default": "افتراضي" + "font_size_default": "افتراضي", + "background_color": "لون الخلفية", + "remove_background_color": "إزالة لون الخلفية" }, "send_filing_warning": "تم الإرسال - لكن التنظيف بعد الإرسال فشل، وقد تبقى مسودة قديمة." }, diff --git a/locales/ca/common.json b/locales/ca/common.json index 7ed69225f..a5ba6c6b8 100644 --- a/locales/ca/common.json +++ b/locales/ca/common.json @@ -730,7 +730,9 @@ "delete_table": "Elimina la taula", "pick_size": "Tria la mida", "font_size": "Mida de la lletra", - "font_size_default": "Per defecte" + "font_size_default": "Per defecte", + "background_color": "Color de fons", + "remove_background_color": "Elimina el color de fons" }, "send_filing_warning": "Enviat, però la neteja posterior a l'enviament ha fallat; és possible que quedi un esborrany obsolet." }, diff --git a/locales/cs/common.json b/locales/cs/common.json index 04989780a..2e305ceb5 100644 --- a/locales/cs/common.json +++ b/locales/cs/common.json @@ -730,7 +730,9 @@ "delete_table": "Odstranit tabulku", "pick_size": "Vybrat velikost", "font_size": "Velikost písma", - "font_size_default": "Výchozí" + "font_size_default": "Výchozí", + "background_color": "Barva pozadí", + "remove_background_color": "Odebrat barvu pozadí" }, "send_filing_warning": "Odesláno - ale následný úklid selhal, může zůstat zastaralý koncept." }, diff --git a/locales/da/common.json b/locales/da/common.json index a68c329c4..cdd9bdfdd 100644 --- a/locales/da/common.json +++ b/locales/da/common.json @@ -730,7 +730,9 @@ "delete_table": "Slet tabel", "pick_size": "Vælg størrelse", "font_size": "Skriftstørrelse", - "font_size_default": "Standard" + "font_size_default": "Standard", + "background_color": "Baggrundsfarve", + "remove_background_color": "Fjern baggrundsfarve" }, "send_filing_warning": "Sendt - men oprydningen bagefter mislykkedes, en forældet kladde kan blive stående." }, diff --git a/locales/de/common.json b/locales/de/common.json index 82db397a9..dbf925f6e 100644 --- a/locales/de/common.json +++ b/locales/de/common.json @@ -730,7 +730,9 @@ "delete_table": "Tabelle löschen", "pick_size": "Größe wählen", "font_size": "Schriftgröße", - "font_size_default": "Standard" + "font_size_default": "Standard", + "background_color": "Hintergrundfarbe", + "remove_background_color": "Hintergrundfarbe entfernen" }, "send_filing_warning": "Gesendet - aber das Aufräumen danach schlug fehl, evtl. bleibt ein alter Entwurf sichtbar." }, diff --git a/locales/en/common.json b/locales/en/common.json index 72494a037..e59ad18b0 100644 --- a/locales/en/common.json +++ b/locales/en/common.json @@ -730,7 +730,9 @@ "delete_table": "Delete table", "pick_size": "Pick size", "font_size": "Font size", - "font_size_default": "Default" + "font_size_default": "Default", + "background_color": "Background color", + "remove_background_color": "Remove background color" }, "send_filing_warning": "Sent - but the post-send cleanup failed, a stale draft may remain." }, diff --git a/locales/es/common.json b/locales/es/common.json index f662bbf04..97ab1052a 100644 --- a/locales/es/common.json +++ b/locales/es/common.json @@ -730,7 +730,9 @@ "delete_table": "Eliminar tabla", "pick_size": "Elegir tamaño", "font_size": "Tamaño de fuente", - "font_size_default": "Predeterminado" + "font_size_default": "Predeterminado", + "background_color": "Color de fondo", + "remove_background_color": "Quitar color de fondo" }, "send_filing_warning": "Enviado - pero la limpieza posterior falló, puede quedar un borrador obsoleto." }, diff --git a/locales/fa/common.json b/locales/fa/common.json index 60910ddce..3ef14c7a6 100644 --- a/locales/fa/common.json +++ b/locales/fa/common.json @@ -730,7 +730,9 @@ "delete_table": "حذف جدول", "pick_size": "انتخاب اندازه", "font_size": "اندازه فونت", - "font_size_default": "پیش‌فرض" + "font_size_default": "پیش‌فرض", + "background_color": "رنگ پس‌زمینه", + "remove_background_color": "حذف رنگ پس‌زمینه" }, "send_filing_warning": "ارسال شد - اما پاک‌سازی پس از ارسال ناموفق بود، ممکن است پیش‌نویس قدیمی باقی بماند." }, diff --git a/locales/fr/common.json b/locales/fr/common.json index 0746c6da0..dc4116756 100644 --- a/locales/fr/common.json +++ b/locales/fr/common.json @@ -730,7 +730,9 @@ "delete_table": "Supprimer le tableau", "pick_size": "Choisir la taille", "font_size": "Taille de police", - "font_size_default": "Par défaut" + "font_size_default": "Par défaut", + "background_color": "Couleur d'arrière-plan", + "remove_background_color": "Supprimer la couleur d'arrière-plan" }, "send_filing_warning": "Envoyé - mais le nettoyage après envoi a échoué, un ancien brouillon peut subsister." }, diff --git a/locales/he/common.json b/locales/he/common.json index fd446bdef..41c6fda09 100644 --- a/locales/he/common.json +++ b/locales/he/common.json @@ -695,7 +695,9 @@ "delete_table": "מחיקת טבלה", "pick_size": "בחירת גודל", "font_size": "גודל גופן", - "font_size_default": "ברירת מחדל" + "font_size_default": "ברירת מחדל", + "background_color": "צבע רקע", + "remove_background_color": "הסר צבע רקע" }, "send_filing_warning": "נשלח - אך הניקוי שלאחר השליחה נכשל, ייתכן שתישאר טיוטה ישנה." }, diff --git a/locales/hu/common.json b/locales/hu/common.json index d58582a7a..7091ba8c5 100644 --- a/locales/hu/common.json +++ b/locales/hu/common.json @@ -730,7 +730,9 @@ "delete_table": "Táblázat törlése", "pick_size": "Méret kiválasztása", "font_size": "Betűméret", - "font_size_default": "Alapértelmezett" + "font_size_default": "Alapértelmezett", + "background_color": "Háttérszín", + "remove_background_color": "Háttérszín eltávolítása" }, "send_filing_warning": "Elküldve - de az utólagos rendrakás nem sikerült, egy elavult piszkozat megmaradhat." }, diff --git a/locales/it/common.json b/locales/it/common.json index 79dfc6cf2..d59335b14 100644 --- a/locales/it/common.json +++ b/locales/it/common.json @@ -730,7 +730,9 @@ "delete_table": "Elimina tabella", "pick_size": "Scegli dimensione", "font_size": "Dimensione carattere", - "font_size_default": "Predefinita" + "font_size_default": "Predefinita", + "background_color": "Colore di sfondo", + "remove_background_color": "Rimuovi colore di sfondo" }, "send_filing_warning": "Inviato - ma la pulizia successiva non è riuscita, potrebbe restare una bozza obsoleta." }, diff --git a/locales/ja/common.json b/locales/ja/common.json index 76317b533..80f154e58 100644 --- a/locales/ja/common.json +++ b/locales/ja/common.json @@ -730,7 +730,9 @@ "delete_table": "表を削除", "pick_size": "サイズを選択", "font_size": "文字サイズ", - "font_size_default": "デフォルト" + "font_size_default": "デフォルト", + "background_color": "背景色", + "remove_background_color": "背景色を削除" }, "send_filing_warning": "送信されましたが、送信後の整理に失敗しました。古い下書きが残る場合があります。" }, diff --git a/locales/ko/common.json b/locales/ko/common.json index 28127de73..c29c8a570 100644 --- a/locales/ko/common.json +++ b/locales/ko/common.json @@ -730,7 +730,9 @@ "delete_table": "표 삭제", "pick_size": "크기 선택", "font_size": "글자 크기", - "font_size_default": "기본값" + "font_size_default": "기본값", + "background_color": "배경색", + "remove_background_color": "배경색 제거" }, "send_filing_warning": "보냈지만 전송 후 정리에 실패했습니다. 오래된 임시 보관 메일이 남아 있을 수 있습니다." }, diff --git a/locales/lv/common.json b/locales/lv/common.json index 7355e6f86..a51796997 100644 --- a/locales/lv/common.json +++ b/locales/lv/common.json @@ -730,7 +730,9 @@ "delete_table": "Dzēst tabulu", "pick_size": "Izvēlēties izmēru", "font_size": "Fonta izmērs", - "font_size_default": "Noklusējums" + "font_size_default": "Noklusējums", + "background_color": "Fona krāsa", + "remove_background_color": "Noņemt fona krāsu" }, "send_filing_warning": "Nosūtīts - bet pēcapstrāde neizdevās, var palikt novecojis melnraksts." }, diff --git a/locales/mn/common.json b/locales/mn/common.json index 86ebbdacc..638cd1555 100644 --- a/locales/mn/common.json +++ b/locales/mn/common.json @@ -730,7 +730,9 @@ "delete_table": "Хүснэгт устгах", "pick_size": "Хэмжээ сонгох", "font_size": "Фонтын хэмжээ", - "font_size_default": "Үндсэн" + "font_size_default": "Үндсэн", + "background_color": "Дэвсгэр өнгө", + "remove_background_color": "Дэвсгэр өнгийг арилгах" }, "send_filing_warning": "Илгээсэн - гэхдээ илгээсний дараах цэвэрлэгээ амжилтгүй болсон тул хуучирсан ноорог үлдэж болзошгүй." }, diff --git a/locales/nl/common.json b/locales/nl/common.json index 230449e81..2c78d5afa 100644 --- a/locales/nl/common.json +++ b/locales/nl/common.json @@ -730,7 +730,9 @@ "delete_table": "Tabel verwijderen", "pick_size": "Grootte kiezen", "font_size": "Lettergrootte", - "font_size_default": "Standaard" + "font_size_default": "Standaard", + "background_color": "Achtergrondkleur", + "remove_background_color": "Achtergrondkleur verwijderen" }, "send_filing_warning": "Verzonden - maar het opruimen daarna is mislukt, mogelijk blijft een oud concept staan." }, diff --git a/locales/pl/common.json b/locales/pl/common.json index d70b9316f..805c56ff2 100644 --- a/locales/pl/common.json +++ b/locales/pl/common.json @@ -730,7 +730,9 @@ "delete_table": "Usuń tabelę", "pick_size": "Wybierz rozmiar", "font_size": "Rozmiar czcionki", - "font_size_default": "Domyślny" + "font_size_default": "Domyślny", + "background_color": "Kolor tła", + "remove_background_color": "Usuń kolor tła" }, "send_filing_warning": "Wysłano - ale późniejsze porządkowanie nie powiodło się, może pozostać nieaktualna wersja robocza." }, diff --git a/locales/pt/common.json b/locales/pt/common.json index e0351df3c..3d5208c45 100644 --- a/locales/pt/common.json +++ b/locales/pt/common.json @@ -730,7 +730,9 @@ "delete_table": "Excluir tabela", "pick_size": "Escolher tamanho", "font_size": "Tamanho da fonte", - "font_size_default": "Padrão" + "font_size_default": "Padrão", + "background_color": "Cor de fundo", + "remove_background_color": "Remover cor de fundo" }, "send_filing_warning": "Enviado - mas a limpeza posterior falhou, um rascunho antigo pode permanecer." }, diff --git a/locales/ro/common.json b/locales/ro/common.json index 81cae002f..d76697c6e 100644 --- a/locales/ro/common.json +++ b/locales/ro/common.json @@ -730,7 +730,9 @@ "delete_table": "Șterge tabelul", "pick_size": "Alege dimensiunea", "font_size": "Dimensiune font", - "font_size_default": "Implicit" + "font_size_default": "Implicit", + "background_color": "Culoarea fundalului", + "remove_background_color": "Elimină culoarea fundalului" }, "send_filing_warning": "Trimis - dar curățarea ulterioară a eșuat, poate rămâne o ciornă veche." }, diff --git a/locales/ru/common.json b/locales/ru/common.json index 33cbe3067..6493a1b4a 100644 --- a/locales/ru/common.json +++ b/locales/ru/common.json @@ -730,7 +730,9 @@ "delete_table": "Удалить таблицу", "pick_size": "Выбрать размер", "font_size": "Размер шрифта", - "font_size_default": "По умолчанию" + "font_size_default": "По умолчанию", + "background_color": "Цвет фона", + "remove_background_color": "Убрать цвет фона" }, "send_filing_warning": "Отправлено - но последующая очистка не удалась, может остаться устаревший черновик." }, diff --git a/locales/sk/common.json b/locales/sk/common.json index 77e461746..9e13b0b46 100644 --- a/locales/sk/common.json +++ b/locales/sk/common.json @@ -730,7 +730,9 @@ "delete_table": "Odstrániť tabuľku", "pick_size": "Vybrať veľkosť", "font_size": "Veľkosť písma", - "font_size_default": "Predvolené" + "font_size_default": "Predvolené", + "background_color": "Farba pozadia", + "remove_background_color": "Odstrániť farbu pozadia" }, "send_filing_warning": "Odoslané - ale následné upratovanie zlyhalo, môže zostať zastaraný koncept." }, diff --git a/locales/tr/common.json b/locales/tr/common.json index 81d9a7794..655cdedfa 100644 --- a/locales/tr/common.json +++ b/locales/tr/common.json @@ -730,7 +730,9 @@ "delete_table": "Tabloyu sil", "pick_size": "Boyut seç", "font_size": "Yazı boyutu", - "font_size_default": "Varsayılan" + "font_size_default": "Varsayılan", + "background_color": "Arka plan rengi", + "remove_background_color": "Arka plan rengini kaldır" }, "send_filing_warning": "Gönderildi - ancak sonrasındaki temizleme başarısız oldu, eski bir taslak kalabilir." }, diff --git a/locales/uk/common.json b/locales/uk/common.json index a9585bdc6..b64bea073 100644 --- a/locales/uk/common.json +++ b/locales/uk/common.json @@ -730,7 +730,9 @@ "delete_table": "Видалити таблицю", "pick_size": "Вибрати розмір", "font_size": "Розмір шрифту", - "font_size_default": "За замовчуванням" + "font_size_default": "За замовчуванням", + "background_color": "Колір фону", + "remove_background_color": "Прибрати колір фону" }, "send_filing_warning": "Надіслано - але подальше очищення не вдалося, може залишитися застаріла чернетка." }, diff --git a/locales/zh/common.json b/locales/zh/common.json index 47c88cf07..78c3391d9 100644 --- a/locales/zh/common.json +++ b/locales/zh/common.json @@ -730,7 +730,9 @@ "delete_table": "删除表格", "pick_size": "选择大小", "font_size": "字号", - "font_size_default": "默认" + "font_size_default": "默认", + "background_color": "背景颜色", + "remove_background_color": "移除背景颜色" }, "send_filing_warning": "已发送,但发送后的清理失败,可能会残留旧草稿。" },