From 6f278845f3cd834f58a9087cdf25e9ce1f865439 Mon Sep 17 00:00:00 2001 From: dealerweb Date: Fri, 10 Jul 2026 12:40:05 +0200 Subject: [PATCH] Feature: text color picker in the composer toolbar The rich-text editor already registers the TextStyle and Color extensions so that colored text pasted or quoted from incoming mail survives editing - but there was no way to set a color yourself. Adds a "Text color" toolbar button next to the strikethrough control, wired to the already-loaded extensions: a 2x8 preset swatch grid plus a "Remove color" entry, following the table button's dropdown pattern (wrapper ref, outside-click close, same popover styling) and the table size picker's swatch grid. The button's baseline icon renders in the currently active color, so the selection is visible without any extra indicator element. No new dependencies and no locale changes; the toolbar titles in this file are plain English throughout, and Clear Formatting already removes colors via unsetAllMarks. --- components/email/rich-text-editor.tsx | 65 +++++++++++++++++++++++++++ 1 file changed, 65 insertions(+) diff --git a/components/email/rich-text-editor.tsx b/components/email/rich-text-editor.tsx index 28da13a6..f59704d4 100644 --- a/components/email/rich-text-editor.tsx +++ b/components/email/rich-text-editor.tsx @@ -41,6 +41,7 @@ import { Heading1, Heading2, Table as TableIcon, + Baseline, Trash2, Rows3, Columns3, @@ -143,6 +144,14 @@ function ToolbarSeparator() { const TABLE_PICKER_ROWS = 6; const TABLE_PICKER_COLS = 8; +// Preset text colours (2 x 8). Inline `style="color: …"` survives email +// round-trips; the TextStyle/Color extensions are already registered to +// preserve pasted colours - this palette just adds a UI to set them. +const TEXT_COLORS = [ + "#000000", "#5f6368", "#9aa0a6", "#c5221f", "#e8710a", "#f9ab00", "#188038", "#1967d2", + "#7627bb", "#c2185b", "#795548", "#fa5252", "#fd7e14", "#40c057", "#4dabf7", "#e64980", +]; + function TableSizePicker({ onPick }: { onPick: (rows: number, cols: number) => void }) { const [hover, setHover] = useState<{ r: number; c: number } | null>(null); return ( @@ -336,6 +345,19 @@ export function RichTextEditor({ const [tableMenuOpen, setTableMenuOpen] = useState(false); const tableWrapperRef = useRef(null); + const [colorMenuOpen, setColorMenuOpen] = useState(false); + const colorWrapperRef = useRef(null); + + useEffect(() => { + if (!colorMenuOpen) return; + const handler = (e: MouseEvent) => { + if (colorWrapperRef.current && !colorWrapperRef.current.contains(e.target as Node)) { + setColorMenuOpen(false); + } + }; + document.addEventListener("mousedown", handler); + return () => document.removeEventListener("mousedown", handler); + }, [colorMenuOpen]); useEffect(() => { if (!tableMenuOpen) return; @@ -386,6 +408,49 @@ export function RichTextEditor({ > +
+ setColorMenuOpen((v) => !v)} + title="Text color" + > + {/* The icon itself previews the active colour - no layout shift. */} + + + {colorMenuOpen && ( +
+
+ {TEXT_COLORS.map((color) => ( +
+
+ +
+ )} +