feat: Text background highlight (#1754)

* #1196/feat: add text background highlight

* unify text color

* dark mode support
* unify text color and highlight

* dark mode support for color selector trigger

* fix see through in color selector dark mode

* fix selection highlight in dark mode

* brown color

* clean up

---------

Co-authored-by: sanua356 <sanek.pankratov356@gmail.com>
This commit is contained in:
Philip Okugbe
2025-12-01 11:34:35 +00:00
committed by GitHub
parent ec3a04f7c7
commit 8014ba3ab7
11 changed files with 389 additions and 71 deletions
@@ -144,16 +144,16 @@ export const EditorBubbleMenu: FC<EditorBubbleMenuProps> = (props) => {
onHide: () => {
setIsNodeSelectorOpen(false);
setIsTextAlignmentOpen(false);
setIsColorSelectorOpen(false);
setIsLinkSelectorOpen(false);
setIsColorSelectorOpen(false);
},
},
};
const [isNodeSelectorOpen, setIsNodeSelectorOpen] = useState(false);
const [isTextAlignmentSelectorOpen, setIsTextAlignmentOpen] = useState(false);
const [isColorSelectorOpen, setIsColorSelectorOpen] = useState(false);
const [isLinkSelectorOpen, setIsLinkSelectorOpen] = useState(false);
const [isColorSelectorOpen, setIsColorSelectorOpen] = useState(false);
return (
<BubbleMenu {...bubbleMenuProps}>
@@ -164,8 +164,8 @@ export const EditorBubbleMenu: FC<EditorBubbleMenuProps> = (props) => {
setIsOpen={() => {
setIsNodeSelectorOpen(!isNodeSelectorOpen);
setIsTextAlignmentOpen(false);
setIsColorSelectorOpen(false);
setIsLinkSelectorOpen(false);
setIsColorSelectorOpen(false);
}}
/>
@@ -175,8 +175,8 @@ export const EditorBubbleMenu: FC<EditorBubbleMenuProps> = (props) => {
setIsOpen={() => {
setIsTextAlignmentOpen(!isTextAlignmentSelectorOpen);
setIsNodeSelectorOpen(false);
setIsColorSelectorOpen(false);
setIsLinkSelectorOpen(false);
setIsColorSelectorOpen(false);
}}
/>
@@ -1,5 +1,5 @@
import { Dispatch, FC, SetStateAction } from "react";
import { IconCheck, IconPalette } from "@tabler/icons-react";
import React, { Dispatch, FC, SetStateAction } from "react";
import { IconCheck, IconChevronDown, IconPalette } from "@tabler/icons-react";
import {
ActionIcon,
Button,
@@ -8,6 +8,9 @@ import {
ScrollArea,
Text,
Tooltip,
SimpleGrid,
Box,
Stack,
} from "@mantine/core";
import type { Editor } from "@tiptap/react";
import { useEditorState } from "@tiptap/react";
@@ -61,9 +64,12 @@ const TEXT_COLORS: BubbleColorMenuItem[] = [
name: "Gray",
color: "#A8A29E",
},
{
name: "Brown",
color: "#92400E",
},
];
// TODO: handle dark mode
const HIGHLIGHT_COLORS: BubbleColorMenuItem[] = [
{
name: "Default",
@@ -71,35 +77,39 @@ const HIGHLIGHT_COLORS: BubbleColorMenuItem[] = [
},
{
name: "Blue",
color: "#c1ecf9",
color: "#98d8f2",
},
{
name: "Green",
color: "#acf79f",
color: "#7edb6c",
},
{
name: "Purple",
color: "#f6f3f8",
color: "#e0d6ed",
},
{
name: "Red",
color: "#fdebeb",
color: "#ffc6c2",
},
{
name: "Yellow",
color: "#fbf4a2",
color: "#faf594",
},
{
name: "Orange",
color: "#faebdd",
color: "#f5c8a9",
},
{
name: "Pink",
color: "#faf1f5",
color: "#f5cfe0",
},
{
name: "Gray",
color: "#f1f1ef",
color: "#dfdfd7",
},
{
name: "Brown",
color: "#d7c4b7",
},
];
@@ -112,17 +122,21 @@ export const ColorSelector: FC<ColorSelectorProps> = ({
const editorState = useEditorState({
editor,
selector: ctx => {
selector: (ctx) => {
if (!ctx.editor) {
return null;
}
const activeColors: Record<string, boolean> = {};
TEXT_COLORS.forEach(({ color }) => {
activeColors[`text_${color}`] = ctx.editor.isActive("textStyle", { color });
activeColors[`text_${color}`] = ctx.editor.isActive("textStyle", {
color,
});
});
HIGHLIGHT_COLORS.forEach(({ color }) => {
activeColors[`highlight_${color}`] = ctx.editor.isActive("highlight", { color });
activeColors[`highlight_${color}`] = ctx.editor.isActive("highlight", {
color,
});
});
return activeColors;
@@ -133,67 +147,152 @@ export const ColorSelector: FC<ColorSelectorProps> = ({
return null;
}
const activeColorItem = TEXT_COLORS.find(({ color }) =>
editorState[`text_${color}`]
const activeColorItem = TEXT_COLORS.find(
({ color }) => editorState[`text_${color}`],
);
const activeHighlightItem = HIGHLIGHT_COLORS.find(({ color }) =>
editorState[`highlight_${color}`]
const activeHighlightItem = HIGHLIGHT_COLORS.find(
({ color }) => editorState[`highlight_${color}`],
);
return (
<Popover width={200} opened={isOpen} withArrow>
<Popover width={220} opened={isOpen} withArrow>
<Popover.Target>
<Tooltip label={t("Text color")} withArrow>
<ActionIcon
<Button
variant="default"
size="lg"
radius="0"
style={{
border: "none",
color: activeColorItem?.color,
}}
rightSection={<IconChevronDown size={16} />}
onClick={() => setIsOpen(!isOpen)}
data-text-color={activeColorItem?.color || ""}
data-highlight-color={activeHighlightItem?.color || ""}
className="color-selector-trigger"
style={{
height: "34px",
border: "none",
fontWeight: 500,
fontSize: rem(16),
paddingLeft: rem(8),
paddingRight: rem(4),
}}
>
<IconPalette size={16} stroke={2} />
</ActionIcon>
A
</Button>
</Tooltip>
</Popover.Target>
<Popover.Dropdown>
{/* make mah responsive */}
<ScrollArea.Autosize type="scroll" mah="400">
<Text span c="dimmed" tt="uppercase" inherit>
{t("Color")}
</Text>
<Stack gap="md">
<Box>
<Text size="sm" fw={600} mb="xs">
{t("Text color")}
</Text>
<SimpleGrid cols={5} spacing="xs">
{TEXT_COLORS.map(({ name, color }, index) => (
<Tooltip key={index} label={t(name)} withArrow>
<Box
onClick={() => {
if (name === "Default") {
editor.commands.unsetColor();
} else {
editor
.chain()
.focus()
.setColor(color || "")
.run();
}
setIsOpen(false);
}}
style={{
width: rem(28),
height: rem(28),
borderRadius: rem(6),
border: editorState[`text_${color}`]
? "2px solid var(--mantine-color-gray-8)"
: "1px solid var(--mantine-color-gray-4)",
cursor: "pointer",
position: "relative",
display: "flex",
alignItems: "center",
justifyContent: "center",
fontSize: rem(16),
fontWeight: 600,
color: color || "var(--mantine-color-gray-8)",
}}
>
A
</Box>
</Tooltip>
))}
</SimpleGrid>
</Box>
<Button.Group orientation="vertical">
{TEXT_COLORS.map(({ name, color }, index) => (
<Button
key={index}
variant="default"
leftSection={<span style={{ color }}>A</span>}
justify="left"
fullWidth
rightSection={
editorState[`text_${color}`] && (
<IconCheck style={{ width: rem(16) }} />
)
}
onClick={() => {
if (name === "Default") {
editor.commands.unsetColor();
} else {
editor.chain().focus().setColor(color || "").run();
}
setIsOpen(false);
}}
style={{ border: "none" }}
>
{t(name)}
</Button>
))}
</Button.Group>
<Box>
<Text size="sm" fw={600} mb="xs">
{t("Highlight color")}
</Text>
<SimpleGrid cols={5} spacing="xs">
{HIGHLIGHT_COLORS.map(({ name, color }, index) => (
<Tooltip key={index} label={t(name)} withArrow>
<Box
onClick={() => {
if (name === "Default") {
editor.commands.unsetHighlight();
} else {
editor
.chain()
.focus()
.toggleMark("highlight", {
color: color || "",
colorName: name.toLowerCase() || "",
})
.run();
}
setIsOpen(false);
}}
style={{
width: rem(28),
height: rem(28),
borderRadius: rem(4),
backgroundColor: color || "var(--mantine-color-gray-2)",
border: "1px solid var(--mantine-color-gray-4)",
cursor: "pointer",
position: "relative",
display: "flex",
alignItems: "center",
justifyContent: "center",
fontSize: rem(16),
fontWeight: 600,
color: "var(--mantine-color-gray-8)",
}}
>
{editorState[`highlight_${color}`] ? (
<IconCheck
size={16}
color="var(--mantine-color-green-7)"
/>
) : (
"A"
)}
</Box>
</Tooltip>
))}
</SimpleGrid>
</Box>
<Button
variant="default"
fullWidth
onClick={() => {
editor.commands.unsetColor();
editor.commands.unsetHighlight();
setIsOpen(false);
}}
>
{t("Remove color")}
</Button>
</Stack>
</ScrollArea.Autosize>
</Popover.Dropdown>
</Popover>