Files
webmail/components/email/font-size.ts
T
ibayueandClaude 7e0a86a660 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 <noreply@anthropic.com>
2026-09-07 16:58:50 +08:00

80 lines
2.4 KiB
TypeScript

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<ReturnType> {
fontSize: {
setFontSize: (fontSize: string) => ReturnType;
unsetFontSize: () => ReturnType;
};
}
}