From 7e0a86a6600bc9d6095ddf02dca31fd865a28522 Mon Sep 17 00:00:00 2001 From: ibayue <11729209+ibayue@users.noreply.github.com> Date: Wed, 26 Aug 2026 16:24:31 +0800 Subject: [PATCH 1/8] feat(composer): sticky formatting toolbar and font-size picker - Pin the rich-text toolbar to the top of the composer's scroll container (sticky) so it stays visible while editing long bodies; the previously translucent background becomes opaque so body text doesn't bleed through while pinned. - Add a font-size picker to the toolbar (14/16/18/20/24/32px plus "Default") backed by a new TipTap FontSize extension that stores the size as an inline style on the TextStyle mark, mirroring how @tiptap/extension-color handles colour - inline styles survive email round-trips, and unsetting the size leaves other text-style attributes (e.g. colour) intact. - Add font_size / font_size_default toolbar strings to all 25 locales. Co-Authored-By: Claude --- components/email/__tests__/font-size.test.ts | 74 ++++++++++++++++++ components/email/font-size.ts | 79 ++++++++++++++++++++ components/email/rich-text-editor.tsx | 71 +++++++++++++++++- locales/ar/common.json | 4 +- locales/ca/common.json | 4 +- locales/cs/common.json | 4 +- locales/da/common.json | 4 +- locales/de/common.json | 4 +- locales/en/common.json | 4 +- locales/es/common.json | 4 +- locales/fa/common.json | 4 +- locales/fr/common.json | 4 +- locales/he/common.json | 4 +- locales/hu/common.json | 4 +- locales/it/common.json | 4 +- locales/ja/common.json | 4 +- locales/ko/common.json | 4 +- locales/lv/common.json | 4 +- locales/mn/common.json | 4 +- locales/nl/common.json | 4 +- locales/pl/common.json | 4 +- locales/pt/common.json | 4 +- locales/ro/common.json | 4 +- locales/ru/common.json | 4 +- locales/sk/common.json | 4 +- locales/tr/common.json | 4 +- locales/uk/common.json | 4 +- locales/zh/common.json | 4 +- 28 files changed, 297 insertions(+), 27 deletions(-) create mode 100644 components/email/__tests__/font-size.test.ts create mode 100644 components/email/font-size.ts diff --git a/components/email/__tests__/font-size.test.ts b/components/email/__tests__/font-size.test.ts new file mode 100644 index 000000000..46804c9ee --- /dev/null +++ b/components/email/__tests__/font-size.test.ts @@ -0,0 +1,74 @@ +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 Color from '@tiptap/extension-color'; + +import { FontSize, FONT_SIZES } from '../font-size'; + +// Mirrors the composer's registration order for TextStyle-based marks +// (rich-text-editor.tsx): TextStyle first, then the attribute extensions. +function makeEditor(content = '

Hello world

') { + return new Editor({ + element: document.createElement('div'), + extensions: [StarterKit, TextStyle, Color, FontSize], + content, + }); +} + +describe('font-size extension', () => { + it('applies font-size as an inline style on the selection', () => { + const editor = makeEditor(); + editor.commands.selectAll(); + editor.commands.setFontSize('18px'); + expect(editor.getHTML()).toContain('font-size: 18px'); + editor.destroy(); + }); + + it('reports the active size via the textStyle attributes', () => { + const editor = makeEditor(); + editor.commands.selectAll(); + editor.commands.setFontSize('24px'); + expect(editor.getAttributes('textStyle').fontSize).toBe('24px'); + editor.destroy(); + }); + + it('unsetFontSize removes the attribute and the empty textStyle mark', () => { + const editor = makeEditor(); + editor.commands.selectAll(); + editor.commands.setFontSize('18px'); + editor.commands.unsetFontSize(); + expect(editor.getAttributes('textStyle').fontSize).toBeUndefined(); + expect(editor.getHTML()).not.toContain('font-size'); + editor.destroy(); + }); + + it('parses font-size from pasted inline styles', () => { + const editor = makeEditor('

Big rest

'); + editor.commands.setTextSelection({ from: 1, to: 5 }); + expect(editor.getAttributes('textStyle').fontSize).toBe('32px'); + editor.destroy(); + }); + + it('keeps color and font-size on the same span', () => { + const editor = makeEditor(); + editor.commands.selectAll(); + editor.commands.setFontSize('18px'); + editor.commands.setColor('#c5221f'); + const html = editor.getHTML(); + expect(html).toContain('font-size: 18px'); + // jsdom's DOM serializer normalizes #c5221f to rgb() form; either way the + // colour declaration must be on the same span as the size. + expect(html).toContain('color: rgb(197, 34, 31)'); + // Removing the size must keep the colour, not the whole mark. + editor.commands.unsetFontSize(); + const after = editor.getHTML(); + expect(after).not.toContain('font-size'); + expect(after).toContain('color: rgb(197, 34, 31)'); + editor.destroy(); + }); + + it('exposes the toolbar size presets the UI renders', () => { + expect([...FONT_SIZES]).toEqual(['14px', '16px', '18px', '20px', '24px', '32px']); + }); +}); diff --git a/components/email/font-size.ts b/components/email/font-size.ts new file mode 100644 index 000000000..e733d9445 --- /dev/null +++ b/components/email/font-size.ts @@ -0,0 +1,79 @@ +import { Extension } from "@tiptap/core"; + +// Font-size as an attribute on the TextStyle mark, mirroring how +// @tiptap/extension-color registers `color`: one global attribute on +// "textStyle", a setter and an unsetter. Emitted as an inline +// `style="font-size: …"`, which email clients preserve on round-trip - +// the same property the composer's StyledParagraph/StyledHeading keep +// for pasted content. +export const FontSize = Extension.create({ + name: "fontSize", + + addOptions() { + return { + types: ["textStyle"], + }; + }, + + addGlobalAttributes() { + return [ + { + types: this.options.types, + attributes: { + fontSize: { + default: null, + parseHTML: (element) => { + const styleAttr = element.getAttribute("style"); + if (styleAttr) { + const decls = styleAttr.split(";").map((s) => s.trim()).filter(Boolean); + for (let i = decls.length - 1; i >= 0; i -= 1) { + const parts = decls[i].split(":"); + if (parts.length >= 2 && parts[0].trim().toLowerCase() === "font-size") { + return parts.slice(1).join(":").trim().replace(/['"]+/g, ""); + } + } + } + return element.style.fontSize?.replace(/['"]+/g, "") || null; + }, + renderHTML: (attributes) => { + if (!attributes.fontSize) { + return {}; + } + return { + style: `font-size: ${attributes.fontSize}`, + }; + }, + }, + }, + }, + ]; + }, + + addCommands() { + return { + setFontSize: + (fontSize: string) => + ({ chain }) => { + return chain().setMark("textStyle", { fontSize }).run(); + }, + unsetFontSize: + () => + ({ chain }) => { + return chain().setMark("textStyle", { fontSize: null }).removeEmptyTextStyle().run(); + }, + }; + }, +}); + +// The sizes the toolbar offers. 14/16px read as body text, the rest as +// emphasis - the composer's base font is 14px (text-sm on .tiptap). +export const FONT_SIZES = ["14px", "16px", "18px", "20px", "24px", "32px"] as const; + +declare module "@tiptap/core" { + interface Commands { + fontSize: { + setFontSize: (fontSize: string) => ReturnType; + unsetFontSize: () => ReturnType; + }; + } +} diff --git a/components/email/rich-text-editor.tsx b/components/email/rich-text-editor.tsx index dd63e9d14..1af370512 100644 --- a/components/email/rich-text-editor.tsx +++ b/components/email/rich-text-editor.tsx @@ -11,6 +11,7 @@ import TextAlign from "@tiptap/extension-text-align"; import { TextDirection } from "@/components/email/text-direction"; import { TextStyle } 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"; import Placeholder from "@tiptap/extension-placeholder"; import { Table } from "@tiptap/extension-table"; @@ -198,6 +199,7 @@ export function RichTextEditor({ }), TextStyle, Color, + FontSize, ResizableImage, Placeholder.configure({ placeholder, @@ -326,6 +328,19 @@ export function RichTextEditor({ const tableWrapperRef = useRef(null); const [colorMenuOpen, setColorMenuOpen] = useState(false); const colorWrapperRef = useRef(null); + const [fontSizeMenuOpen, setFontSizeMenuOpen] = useState(false); + const fontSizeWrapperRef = useRef(null); + + useEffect(() => { + if (!fontSizeMenuOpen) return; + const handler = (e: MouseEvent) => { + if (fontSizeWrapperRef.current && !fontSizeWrapperRef.current.contains(e.target as Node)) { + setFontSizeMenuOpen(false); + } + }; + document.addEventListener("mousedown", handler); + return () => document.removeEventListener("mousedown", handler); + }, [fontSizeMenuOpen]); useEffect(() => { if (!colorMenuOpen) return; @@ -355,10 +370,62 @@ export function RichTextEditor({ ); } + const currentFontSize: string | null = editor.getAttributes("textStyle").fontSize ?? null; + return (
- {/* Toolbar */} -
+ {/* Toolbar - sticky within the composer's scroll container so it stays + visible while editing long bodies (the fields section scrolls away + above it; the opaque background keeps body text from bleeding + through while pinned). */} +
+ {/* Font size */} +
+ setFontSizeMenuOpen((v) => !v)} + title={tToolbar("font_size")} + > + + {currentFontSize ? currentFontSize.replace("px", "") : "Aa"} + + + {fontSizeMenuOpen && ( +
+ {FONT_SIZES.map((size) => ( + + ))} +
+ +
+ )} +
+ + + editor.chain().focus().toggleBold().run()} diff --git a/locales/ar/common.json b/locales/ar/common.json index ac7018dc0..ec8461c3f 100644 --- a/locales/ar/common.json +++ b/locales/ar/common.json @@ -728,7 +728,9 @@ "delete_column": "حذف العمود", "toggle_header_row": "تبديل صف الرأس", "delete_table": "حذف الجدول", - "pick_size": "اختيار الحجم" + "pick_size": "اختيار الحجم", + "font_size": "حجم الخط", + "font_size_default": "افتراضي" }, "send_filing_warning": "تم الإرسال - لكن التنظيف بعد الإرسال فشل، وقد تبقى مسودة قديمة." }, diff --git a/locales/ca/common.json b/locales/ca/common.json index bb0ce968d..7ed69225f 100644 --- a/locales/ca/common.json +++ b/locales/ca/common.json @@ -728,7 +728,9 @@ "delete_column": "Elimina la columna", "toggle_header_row": "Commuta la fila de capçalera", "delete_table": "Elimina la taula", - "pick_size": "Tria la mida" + "pick_size": "Tria la mida", + "font_size": "Mida de la lletra", + "font_size_default": "Per defecte" }, "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 a8d69da24..04989780a 100644 --- a/locales/cs/common.json +++ b/locales/cs/common.json @@ -728,7 +728,9 @@ "delete_column": "Odstranit sloupec", "toggle_header_row": "Přepnout řádek záhlaví", "delete_table": "Odstranit tabulku", - "pick_size": "Vybrat velikost" + "pick_size": "Vybrat velikost", + "font_size": "Velikost písma", + "font_size_default": "Výchozí" }, "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 5d1eff6fb..a68c329c4 100644 --- a/locales/da/common.json +++ b/locales/da/common.json @@ -728,7 +728,9 @@ "delete_column": "Slet kolonne", "toggle_header_row": "Slå overskriftsrække til/fra", "delete_table": "Slet tabel", - "pick_size": "Vælg størrelse" + "pick_size": "Vælg størrelse", + "font_size": "Skriftstørrelse", + "font_size_default": "Standard" }, "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 70331f3ab..82db397a9 100644 --- a/locales/de/common.json +++ b/locales/de/common.json @@ -728,7 +728,9 @@ "delete_column": "Spalte löschen", "toggle_header_row": "Kopfzeile umschalten", "delete_table": "Tabelle löschen", - "pick_size": "Größe wählen" + "pick_size": "Größe wählen", + "font_size": "Schriftgröße", + "font_size_default": "Standard" }, "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 68874b945..72494a037 100644 --- a/locales/en/common.json +++ b/locales/en/common.json @@ -728,7 +728,9 @@ "delete_column": "Delete column", "toggle_header_row": "Toggle header row", "delete_table": "Delete table", - "pick_size": "Pick size" + "pick_size": "Pick size", + "font_size": "Font size", + "font_size_default": "Default" }, "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 c4172288e..f662bbf04 100644 --- a/locales/es/common.json +++ b/locales/es/common.json @@ -728,7 +728,9 @@ "delete_column": "Eliminar columna", "toggle_header_row": "Alternar fila de encabezado", "delete_table": "Eliminar tabla", - "pick_size": "Elegir tamaño" + "pick_size": "Elegir tamaño", + "font_size": "Tamaño de fuente", + "font_size_default": "Predeterminado" }, "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 9dc030aac..60910ddce 100644 --- a/locales/fa/common.json +++ b/locales/fa/common.json @@ -728,7 +728,9 @@ "delete_column": "حذف ستون", "toggle_header_row": "تغییر وضعیت ردیف سرصفحه", "delete_table": "حذف جدول", - "pick_size": "انتخاب اندازه" + "pick_size": "انتخاب اندازه", + "font_size": "اندازه فونت", + "font_size_default": "پیش‌فرض" }, "send_filing_warning": "ارسال شد - اما پاک‌سازی پس از ارسال ناموفق بود، ممکن است پیش‌نویس قدیمی باقی بماند." }, diff --git a/locales/fr/common.json b/locales/fr/common.json index b844debfc..0746c6da0 100644 --- a/locales/fr/common.json +++ b/locales/fr/common.json @@ -728,7 +728,9 @@ "delete_column": "Supprimer la colonne", "toggle_header_row": "Basculer la ligne d'en-tête", "delete_table": "Supprimer le tableau", - "pick_size": "Choisir la taille" + "pick_size": "Choisir la taille", + "font_size": "Taille de police", + "font_size_default": "Par défaut" }, "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 e5e7d311d..fd446bdef 100644 --- a/locales/he/common.json +++ b/locales/he/common.json @@ -693,7 +693,9 @@ "delete_column": "מחיקת עמודה", "toggle_header_row": "החלפת שורת כותרת", "delete_table": "מחיקת טבלה", - "pick_size": "בחירת גודל" + "pick_size": "בחירת גודל", + "font_size": "גודל גופן", + "font_size_default": "ברירת מחדל" }, "send_filing_warning": "נשלח - אך הניקוי שלאחר השליחה נכשל, ייתכן שתישאר טיוטה ישנה." }, diff --git a/locales/hu/common.json b/locales/hu/common.json index 0e07afb34..d58582a7a 100644 --- a/locales/hu/common.json +++ b/locales/hu/common.json @@ -728,7 +728,9 @@ "delete_column": "Oszlop törlése", "toggle_header_row": "Fejlécsor váltása", "delete_table": "Táblázat törlése", - "pick_size": "Méret kiválasztása" + "pick_size": "Méret kiválasztása", + "font_size": "Betűméret", + "font_size_default": "Alapértelmezett" }, "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 48a3ae4f8..79dfc6cf2 100644 --- a/locales/it/common.json +++ b/locales/it/common.json @@ -728,7 +728,9 @@ "delete_column": "Elimina colonna", "toggle_header_row": "Attiva/disattiva riga di intestazione", "delete_table": "Elimina tabella", - "pick_size": "Scegli dimensione" + "pick_size": "Scegli dimensione", + "font_size": "Dimensione carattere", + "font_size_default": "Predefinita" }, "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 de7e40c0e..76317b533 100644 --- a/locales/ja/common.json +++ b/locales/ja/common.json @@ -728,7 +728,9 @@ "delete_column": "列を削除", "toggle_header_row": "ヘッダー行の切り替え", "delete_table": "表を削除", - "pick_size": "サイズを選択" + "pick_size": "サイズを選択", + "font_size": "文字サイズ", + "font_size_default": "デフォルト" }, "send_filing_warning": "送信されましたが、送信後の整理に失敗しました。古い下書きが残る場合があります。" }, diff --git a/locales/ko/common.json b/locales/ko/common.json index 8cb8e3d75..28127de73 100644 --- a/locales/ko/common.json +++ b/locales/ko/common.json @@ -728,7 +728,9 @@ "delete_column": "열 삭제", "toggle_header_row": "머리글 행 전환", "delete_table": "표 삭제", - "pick_size": "크기 선택" + "pick_size": "크기 선택", + "font_size": "글자 크기", + "font_size_default": "기본값" }, "send_filing_warning": "보냈지만 전송 후 정리에 실패했습니다. 오래된 임시 보관 메일이 남아 있을 수 있습니다." }, diff --git a/locales/lv/common.json b/locales/lv/common.json index f38757c6e..7355e6f86 100644 --- a/locales/lv/common.json +++ b/locales/lv/common.json @@ -728,7 +728,9 @@ "delete_column": "Dzēst kolonnu", "toggle_header_row": "Pārslēgt galvenes rindu", "delete_table": "Dzēst tabulu", - "pick_size": "Izvēlēties izmēru" + "pick_size": "Izvēlēties izmēru", + "font_size": "Fonta izmērs", + "font_size_default": "Noklusējums" }, "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 fdfa0ca40..86ebbdacc 100644 --- a/locales/mn/common.json +++ b/locales/mn/common.json @@ -728,7 +728,9 @@ "delete_column": "Баганыг устгах", "toggle_header_row": "Толгойн мөрийг сэлгэх", "delete_table": "Хүснэгт устгах", - "pick_size": "Хэмжээ сонгох" + "pick_size": "Хэмжээ сонгох", + "font_size": "Фонтын хэмжээ", + "font_size_default": "Үндсэн" }, "send_filing_warning": "Илгээсэн - гэхдээ илгээсний дараах цэвэрлэгээ амжилтгүй болсон тул хуучирсан ноорог үлдэж болзошгүй." }, diff --git a/locales/nl/common.json b/locales/nl/common.json index 14dd7093d..230449e81 100644 --- a/locales/nl/common.json +++ b/locales/nl/common.json @@ -728,7 +728,9 @@ "delete_column": "Kolom verwijderen", "toggle_header_row": "Koprij aan/uit", "delete_table": "Tabel verwijderen", - "pick_size": "Grootte kiezen" + "pick_size": "Grootte kiezen", + "font_size": "Lettergrootte", + "font_size_default": "Standaard" }, "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 57380fb89..d70b9316f 100644 --- a/locales/pl/common.json +++ b/locales/pl/common.json @@ -728,7 +728,9 @@ "delete_column": "Usuń kolumnę", "toggle_header_row": "Przełącz wiersz nagłówka", "delete_table": "Usuń tabelę", - "pick_size": "Wybierz rozmiar" + "pick_size": "Wybierz rozmiar", + "font_size": "Rozmiar czcionki", + "font_size_default": "Domyślny" }, "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 2c3f35014..e0351df3c 100644 --- a/locales/pt/common.json +++ b/locales/pt/common.json @@ -728,7 +728,9 @@ "delete_column": "Excluir coluna", "toggle_header_row": "Alternar linha de cabeçalho", "delete_table": "Excluir tabela", - "pick_size": "Escolher tamanho" + "pick_size": "Escolher tamanho", + "font_size": "Tamanho da fonte", + "font_size_default": "Padrão" }, "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 1f6fd1896..81cae002f 100644 --- a/locales/ro/common.json +++ b/locales/ro/common.json @@ -728,7 +728,9 @@ "delete_column": "Șterge coloana", "toggle_header_row": "Comută rândul de antet", "delete_table": "Șterge tabelul", - "pick_size": "Alege dimensiunea" + "pick_size": "Alege dimensiunea", + "font_size": "Dimensiune font", + "font_size_default": "Implicit" }, "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 13814a0cc..33cbe3067 100644 --- a/locales/ru/common.json +++ b/locales/ru/common.json @@ -728,7 +728,9 @@ "delete_column": "Удалить столбец", "toggle_header_row": "Переключить строку заголовка", "delete_table": "Удалить таблицу", - "pick_size": "Выбрать размер" + "pick_size": "Выбрать размер", + "font_size": "Размер шрифта", + "font_size_default": "По умолчанию" }, "send_filing_warning": "Отправлено - но последующая очистка не удалась, может остаться устаревший черновик." }, diff --git a/locales/sk/common.json b/locales/sk/common.json index 56f3e76ec..77e461746 100644 --- a/locales/sk/common.json +++ b/locales/sk/common.json @@ -728,7 +728,9 @@ "delete_column": "Odstrániť stĺpec", "toggle_header_row": "Prepnúť riadok záhlavia", "delete_table": "Odstrániť tabuľku", - "pick_size": "Vybrať veľkosť" + "pick_size": "Vybrať veľkosť", + "font_size": "Veľkosť písma", + "font_size_default": "Predvolené" }, "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 99c5227fd..81d9a7794 100644 --- a/locales/tr/common.json +++ b/locales/tr/common.json @@ -728,7 +728,9 @@ "delete_column": "Sütunu sil", "toggle_header_row": "Başlık satırını aç/kapat", "delete_table": "Tabloyu sil", - "pick_size": "Boyut seç" + "pick_size": "Boyut seç", + "font_size": "Yazı boyutu", + "font_size_default": "Varsayılan" }, "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 723af5fc2..a9585bdc6 100644 --- a/locales/uk/common.json +++ b/locales/uk/common.json @@ -728,7 +728,9 @@ "delete_column": "Видалити стовпець", "toggle_header_row": "Переключити рядок заголовка", "delete_table": "Видалити таблицю", - "pick_size": "Вибрати розмір" + "pick_size": "Вибрати розмір", + "font_size": "Розмір шрифту", + "font_size_default": "За замовчуванням" }, "send_filing_warning": "Надіслано - але подальше очищення не вдалося, може залишитися застаріла чернетка." }, diff --git a/locales/zh/common.json b/locales/zh/common.json index 0d804d91c..47c88cf07 100644 --- a/locales/zh/common.json +++ b/locales/zh/common.json @@ -728,7 +728,9 @@ "delete_column": "删除列", "toggle_header_row": "切换标题行", "delete_table": "删除表格", - "pick_size": "选择大小" + "pick_size": "选择大小", + "font_size": "字号", + "font_size_default": "默认" }, "send_filing_warning": "已发送,但发送后的清理失败,可能会残留旧草稿。" }, From 0ead019c5c9907236a2c7b4f9683d59267101313 Mon Sep 17 00:00:00 2001 From: ibayue <11729209+ibayue@users.noreply.github.com> Date: Wed, 26 Aug 2026 17:50:27 +0800 Subject: [PATCH 2/8] fix(composer): keep From/To/Cc/Bcc/Subject fields pinned above the body Move the fields section out of the composer's scroll container so it stays visible while scrolling long bodies, matching the pinned formatting toolbar. Since the fields were the first child of the scroll container, hoisting them above it is visually identical to making them sticky - but needs no dynamic offset for the toolbar's own sticky top (the fields' height varies with Cc/Bcc toggling and recipient-chip wrapping). The scroll container now wraps only the body and the signature preview. Co-Authored-By: Claude --- components/email/email-composer.tsx | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/components/email/email-composer.tsx b/components/email/email-composer.tsx index b19bb55e0..7e6125436 100644 --- a/components/email/email-composer.tsx +++ b/components/email/email-composer.tsx @@ -2598,9 +2598,10 @@ export function EmailComposer({ )}
-
- {/* Fields section */} -
+ {/* Fields section - outside the scroll container so From/To/Cc/Bcc/ + Subject stay reachable while scrolling long bodies, matching the + pinned formatting toolbar (see rich-text-editor.tsx). */} +
{/* From field */}
{t('from')}: @@ -2879,8 +2880,9 @@ export function EmailComposer({ className="flex-1 border-0 focus-visible:ring-0 h-8 px-0 text-sm" />
-
+
+
{/* Body */} {plainTextMode ? (