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 14:58:15 +02:00
committed by GitHub
co-authored by Amruth Pillai
parent 1507d869c7
commit e96a51f31c
63 changed files with 565 additions and 179 deletions
@@ -1,5 +1,6 @@
import type { HTMLElement, Node } from "node-html-parser";
import { NodeType, parse } from "node-html-parser";
import { isDarkColor } from "@reactive-resume/utils/color";
export const richTextMarkClassName = "rr-pdf-mark";
@@ -45,8 +46,19 @@ const mergeClassNames = (...classNames: (string | undefined)[]): string => {
const normalizeMarkElements = (root: ReturnType<typeof parse>) => {
for (const mark of root.querySelectorAll("mark")) {
const dataColor = mark.getAttribute("data-color");
mark.tagName = "span";
mark.setAttribute("class", mergeClassNames(mark.getAttribute("class"), richTextMarkClassName));
// Preserve custom highlight color as inline background-color for react-pdf-html.
// Legacy marks without data-color fall back to the .rr-pdf-mark stylesheet (yellow).
if (dataColor) {
const existingStyle = mark.getAttribute("style") ?? "";
let inlineStyle = `background-color: ${dataColor}`;
if (isDarkColor(dataColor)) inlineStyle += "; color: #ffffff";
mark.setAttribute("style", existingStyle ? `${existingStyle}; ${inlineStyle}` : inlineStyle);
}
}
};