feat(editor): add multicolor highlight with auto-contrast text (#3110)

* feat(editor): add multicolor highlight with auto-contrast text

Enable the Tiptap Highlight extension in multicolor mode, replacing the
single-color yellow toggle with a full color picker (16 presets + custom).
When the chosen highlight color is perceptually dark, text inside the mark
automatically renders white for readability.

Changes span the full pipeline:
- Editor: ColorPicker UI, extended renderHTML for contrast detection
- PDF: normalizeMarkElements preserves data-color as inline style
- DOCX: mergeStyle reads actual background-color from <mark>
- Utils: new isDarkColor() luminance helper

Backward-compatible: legacy <mark> without data-color still renders yellow.

Resolves #3109

* fix: handle multicolor highlight edge cases

---------

Co-authored-by: Amruth Pillai <im.amruth@gmail.com>
This commit is contained in:
JamesGoslings
2026-06-01 20:58:15 +08:00
committed by GitHub
parent 1507d869c7
commit e96a51f31c
63 changed files with 565 additions and 179 deletions
+13
View File
@@ -55,4 +55,17 @@ describe("htmlToParagraphs", () => {
const result = htmlToParagraphs("<!--c--><script>x</script><p>Hello</p>");
expect(result.length).toBeGreaterThanOrEqual(1);
});
it("applies default yellow shading for <mark> without background-color", () => {
const result = htmlToParagraphs("<p><mark>highlighted</mark></p>");
const json = JSON.stringify(result[0]);
// TextRun with shading should contain "FFFF00" in serialized output
expect(json).toContain("FFFF00");
});
it("reads custom background-color from <mark> style for shading fill", () => {
const result = htmlToParagraphs('<p><mark style="background-color: rgba(204, 255, 204, 1)">green</mark></p>');
const json = JSON.stringify(result[0]);
expect(json).toContain("CCFFCC");
});
});
+7 -3
View File
@@ -1,6 +1,6 @@
import type { IShadingAttributesProperties, ISpacingProperties } from "docx";
import { ExternalHyperlink, HeadingLevel, Paragraph, TextRun } from "docx";
import { parseColorString } from "@reactive-resume/utils/color";
import { isDarkColor, parseColorString } from "@reactive-resume/utils/color";
import { toSafeDocxLink } from "./link-utils";
export interface HtmlStyleConfig {
@@ -65,9 +65,13 @@ function mergeStyle(parent: InlineStyle, tag: string, element?: HTMLElement): In
case "CODE":
next.font = "Courier New";
break;
case "MARK":
next.shading = { fill: "FFFF00" };
case "MARK": {
const bgColor = (element as HTMLElement | undefined)?.style.backgroundColor;
const fill = bgColor ? toDocxColorValue(bgColor) : null;
next.shading = { fill: fill ?? "FFFF00" };
if (bgColor && isDarkColor(bgColor)) next.color = "FFFFFF";
break;
}
}
const colorValue = (element as HTMLElement | undefined)?.style.color;