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
@@ -234,9 +234,7 @@
"Anyone with this link can join this workspace.": "Jeder mit diesem Link kann dem Arbeitsbereich beitreten.",
"Invite link": "Einladungslink",
"Copy": "Kopieren",
"Copy to space": "In Raum kopieren",
"Copied": "Kopiert",
"Duplicate": "Duplizieren",
"Select a user": "Benutzer auswählen",
"Select a group": "Gruppe auswählen",
"Export all pages and attachments in this space.": "Alle Seiten und Anhänge in diesem Bereich exportieren.",
@@ -554,5 +554,8 @@
"Select expiration date": "Select expiration date",
"This action cannot be undone. Any applications using this API key will stop working.": "This action cannot be undone. Any applications using this API key will stop working.",
"Update API key": "Update API key",
"Manage API keys for all users in the workspace": "Manage API keys for all users in the workspace"
"Manage API keys for all users in the workspace": "Manage API keys for all users in the workspace",
"Background color": "Background color",
"Highlight color": "Highlight color",
"Remove color": "Remove color"
}
@@ -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>
@@ -6,7 +6,6 @@ import { TaskItem } from "@tiptap/extension-task-item";
import { Underline } from "@tiptap/extension-underline";
import { Superscript } from "@tiptap/extension-superscript";
import SubScript from "@tiptap/extension-subscript";
import { Highlight } from "@tiptap/extension-highlight";
import { Typography } from "@tiptap/extension-typography";
import { TextStyle } from "@tiptap/extension-text-style";
import { Color } from "@tiptap/extension-color";
@@ -40,6 +39,7 @@ import {
Mention,
Subpages,
TableDndExtension,
Highlight
} from "@docmost/editor-ext";
import {
randomElement,
@@ -5,7 +5,7 @@
);
color: light-dark(
var(--mantine-color-default-color),
var(--mantine-color-dark-0)
var(--mantine-color-white)
);
font-size: var(--mantine-font-size-md);
line-height: var(--mantine-line-height-xl);
@@ -115,7 +115,7 @@
}
& > .react-renderer {
margin-top: var(--mantine-spacing-sm);
margin-top: var(--mantine-spacing-sm);
margin-bottom: var(--mantine-spacing-sm);
&:first-child {
@@ -141,7 +141,7 @@
.selection,
*::selection {
background-color: Highlight;
background-color: light-dark(Highlight, var(--mantine-color-gray-7));
}
.comment-mark {
@@ -209,4 +209,3 @@
.actionIconGroup {
background: var(--mantine-color-body);
}
@@ -0,0 +1,177 @@
/* Highlight colors with dark mode support */
.ProseMirror {
/* Blue */
mark[data-color="#98d8f2"] {
background-color: light-dark(
rgb(224 242 254),
rgba(37, 99, 235, 0.35)
) !important;
}
/* Green */
mark[data-color="#7edb6c"] {
background-color: light-dark(
rgb(220 252 231),
rgba(0, 138, 0, 0.35)
) !important;
}
/* Purple */
mark[data-color="#e0d6ed"] {
background-color: light-dark(
rgb(243 232 255),
rgba(147, 51, 234, 0.35)
) !important;
}
/* Red */
mark[data-color="#ffc6c2"] {
background-color: light-dark(
rgb(255 228 230),
rgba(224, 0, 0, 0.35)
) !important;
}
/* Yellow */
mark[data-color="#faf594"] {
background-color: light-dark(
rgb(254 249 195),
rgba(234, 179, 8, 0.35)
) !important;
}
/* Orange */
mark[data-color="#f5c8a9"] {
background-color: light-dark(
rgb(251, 236, 221),
rgba(255, 165, 0, 0.45)
) !important;
}
/* Pink */
mark[data-color="#f5cfe0"] {
background-color: light-dark(
rgb(252, 241, 246),
rgba(186, 64, 129, 0.35)
) !important;
}
/* Gray */
mark[data-color="#dfdfd7"] {
background-color: light-dark(
rgb(238 238 235),
rgba(168, 162, 158, 0.35)
) !important;
}
/* Brown */
mark[data-color="#d7c4b7"] {
background-color: light-dark(
rgb(215 196 183),
rgba(146, 64, 14, 0.35)
) !important;
}
}
/* Color selector trigger button styles */
.color-selector-trigger[data-text-color="#2563EB"] {
color: #2563EB !important;
}
.color-selector-trigger[data-text-color="#008A00"] {
color: #008A00 !important;
}
.color-selector-trigger[data-text-color="#9333EA"] {
color: #9333EA !important;
}
.color-selector-trigger[data-text-color="#E00000"] {
color: #E00000 !important;
}
.color-selector-trigger[data-text-color="#EAB308"] {
color: #EAB308 !important;
}
.color-selector-trigger[data-text-color="#FFA500"] {
color: #FFA500 !important;
}
.color-selector-trigger[data-text-color="#BA4081"] {
color: #BA4081 !important;
}
.color-selector-trigger[data-text-color="#A8A29E"] {
color: #A8A29E !important;
}
.color-selector-trigger[data-text-color="#92400E"] {
color: #92400E !important;
}
/* Highlight background colors with light-dark support - solid colors for trigger button */
.color-selector-trigger[data-highlight-color="#98d8f2"] {
background-color: light-dark(
rgb(224 242 254),
rgb(30 64 175)
) !important;
}
.color-selector-trigger[data-highlight-color="#7edb6c"] {
background-color: light-dark(
rgb(220 252 231),
rgb(21 128 61)
) !important;
}
.color-selector-trigger[data-highlight-color="#e0d6ed"] {
background-color: light-dark(
rgb(243 232 255),
rgb(107 33 168)
) !important;
}
.color-selector-trigger[data-highlight-color="#ffc6c2"] {
background-color: light-dark(
rgb(255 228 230),
rgb(185 28 28)
) !important;
}
.color-selector-trigger[data-highlight-color="#faf594"] {
background-color: light-dark(
rgb(254 249 195),
rgb(161 98 7)
) !important;
}
.color-selector-trigger[data-highlight-color="#f5c8a9"] {
background-color: light-dark(
rgb(251 236 221),
rgb(194 65 12)
) !important;
}
.color-selector-trigger[data-highlight-color="#f5cfe0"] {
background-color: light-dark(
rgb(252 241 246),
rgb(157 23 77)
) !important;
}
.color-selector-trigger[data-highlight-color="#dfdfd7"] {
background-color: light-dark(
rgb(238 238 235),
rgb(115 115 115)
) !important;
}
.color-selector-trigger[data-highlight-color="#d7c4b7"] {
background-color: light-dark(
rgb(215 196 183),
rgb(120 53 15)
) !important;
}
@@ -12,3 +12,4 @@
@import "./find.css";
@import "./mention.css";
@import "./ordered-list.css";
@import "./highlight.css";