diff --git a/apps/client/public/locales/de-DE/translation.json b/apps/client/public/locales/de-DE/translation.json index bc1bd1f2..57c8cfba 100644 --- a/apps/client/public/locales/de-DE/translation.json +++ b/apps/client/public/locales/de-DE/translation.json @@ -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.", diff --git a/apps/client/public/locales/en-US/translation.json b/apps/client/public/locales/en-US/translation.json index 36985540..38f5433a 100644 --- a/apps/client/public/locales/en-US/translation.json +++ b/apps/client/public/locales/en-US/translation.json @@ -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" } diff --git a/apps/client/src/features/editor/components/bubble-menu/bubble-menu.tsx b/apps/client/src/features/editor/components/bubble-menu/bubble-menu.tsx index d28eae98..e8085ca6 100644 --- a/apps/client/src/features/editor/components/bubble-menu/bubble-menu.tsx +++ b/apps/client/src/features/editor/components/bubble-menu/bubble-menu.tsx @@ -144,16 +144,16 @@ export const EditorBubbleMenu: FC = (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 ( @@ -164,8 +164,8 @@ export const EditorBubbleMenu: FC = (props) => { setIsOpen={() => { setIsNodeSelectorOpen(!isNodeSelectorOpen); setIsTextAlignmentOpen(false); - setIsColorSelectorOpen(false); setIsLinkSelectorOpen(false); + setIsColorSelectorOpen(false); }} /> @@ -175,8 +175,8 @@ export const EditorBubbleMenu: FC = (props) => { setIsOpen={() => { setIsTextAlignmentOpen(!isTextAlignmentSelectorOpen); setIsNodeSelectorOpen(false); - setIsColorSelectorOpen(false); setIsLinkSelectorOpen(false); + setIsColorSelectorOpen(false); }} /> diff --git a/apps/client/src/features/editor/components/bubble-menu/color-selector.tsx b/apps/client/src/features/editor/components/bubble-menu/color-selector.tsx index a59eb8e4..d0907b81 100644 --- a/apps/client/src/features/editor/components/bubble-menu/color-selector.tsx +++ b/apps/client/src/features/editor/components/bubble-menu/color-selector.tsx @@ -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 = ({ const editorState = useEditorState({ editor, - selector: ctx => { + selector: (ctx) => { if (!ctx.editor) { return null; } const activeColors: Record = {}; 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 = ({ 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 ( - + - } 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), + }} > - - + A + - {/* make mah responsive */} - - {t("Color")} - + + + + {t("Text color")} + + + {TEXT_COLORS.map(({ name, color }, index) => ( + + { + 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 + + + ))} + + - - {TEXT_COLORS.map(({ name, color }, index) => ( - - ))} - + + + {t("Highlight color")} + + + {HIGHLIGHT_COLORS.map(({ name, color }, index) => ( + + { + 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}`] ? ( + + ) : ( + "A" + )} + + + ))} + + + + + diff --git a/apps/client/src/features/editor/extensions/extensions.ts b/apps/client/src/features/editor/extensions/extensions.ts index 92ed8c29..c654824e 100644 --- a/apps/client/src/features/editor/extensions/extensions.ts +++ b/apps/client/src/features/editor/extensions/extensions.ts @@ -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, diff --git a/apps/client/src/features/editor/styles/core.css b/apps/client/src/features/editor/styles/core.css index 479d000d..10480b8e 100644 --- a/apps/client/src/features/editor/styles/core.css +++ b/apps/client/src/features/editor/styles/core.css @@ -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); } - diff --git a/apps/client/src/features/editor/styles/highlight.css b/apps/client/src/features/editor/styles/highlight.css new file mode 100644 index 00000000..332f48e5 --- /dev/null +++ b/apps/client/src/features/editor/styles/highlight.css @@ -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; +} diff --git a/apps/client/src/features/editor/styles/index.css b/apps/client/src/features/editor/styles/index.css index e426e0ba..e32a606f 100644 --- a/apps/client/src/features/editor/styles/index.css +++ b/apps/client/src/features/editor/styles/index.css @@ -12,3 +12,4 @@ @import "./find.css"; @import "./mention.css"; @import "./ordered-list.css"; +@import "./highlight.css"; diff --git a/apps/server/src/collaboration/collaboration.util.ts b/apps/server/src/collaboration/collaboration.util.ts index 008bfa31..355b4bd7 100644 --- a/apps/server/src/collaboration/collaboration.util.ts +++ b/apps/server/src/collaboration/collaboration.util.ts @@ -5,7 +5,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'; @@ -33,6 +32,7 @@ import { Embed, Mention, Subpages, + Highlight } from '@docmost/editor-ext'; import { generateText, getSchema, JSONContent } from '@tiptap/core'; import { generateHTML, generateJSON } from '../common/helpers/prosemirror/html'; diff --git a/packages/editor-ext/src/index.ts b/packages/editor-ext/src/index.ts index a0efaa1b..04b99566 100644 --- a/packages/editor-ext/src/index.ts +++ b/packages/editor-ext/src/index.ts @@ -20,3 +20,4 @@ export * from "./lib/markdown"; export * from "./lib/search-and-replace"; export * from "./lib/embed-provider"; export * from "./lib/subpages"; +export * from "./lib/highlight"; diff --git a/packages/editor-ext/src/lib/highlight.ts b/packages/editor-ext/src/lib/highlight.ts new file mode 100644 index 00000000..becef994 --- /dev/null +++ b/packages/editor-ext/src/lib/highlight.ts @@ -0,0 +1,40 @@ +import { + Highlight as TiptapHighlight, + type HighlightOptions, +} from "@tiptap/extension-highlight"; + +export const Highlight = TiptapHighlight.extend({ + addAttributes() { + return { + ...this.parent?.(), + color: { + default: null, + parseHTML: (element) => + element.getAttribute("data-color") || element.style.backgroundColor, + renderHTML: (attributes) => { + if (!attributes.color) { + return {}; + } + + return { + "data-color": attributes.color, + style: `background-color: ${attributes.color}; color: inherit`, + }; + }, + }, + colorName: { + default: null, + parseHTML: (element) => + element.getAttribute("data-highlight-color-name") || null, + renderHTML: (attributes) => { + if (!attributes.colorName) { + return {}; + } + return { + "data-highlight-color-name": attributes.colorName.toLowerCase(), + }; + }, + }, + }; + }, +});