From 90488a95b1c9f31c37526c426d6d1ab4aa0436f2 Mon Sep 17 00:00:00 2001 From: Philip Okugbe <16838612+Philipinho@users.noreply.github.com> Date: Tue, 15 Jul 2025 06:27:48 +0100 Subject: [PATCH] feat: table background color, cell header and align (#1352) * feat: add toggle header cell button to table cell menu Added ability to toggle header cells directly from the table cell menu. This enhancement includes: - New toggle header cell button with IconTableRow icon - Consistent UI/UX with existing table menu patterns - Proper internationalization support * fix: typo in aria-label for toggle header cell button * feat: add table cell background color picker - Extended TableCell and TableHeader to support backgroundColor attribute - Created TableBackgroundColor component with 21 color options - Integrated color picker into table cell menu using Mantine UI - Added support for both regular cells and header cells - Updated imports to use custom TableHeader from @docmost/editor-ext * feat: add text alignment to table cell menu - Created TableTextAlignment component with left, center, and right alignment options - Integrated alignment selector into table cell menu - Shows current alignment icon in the button - Displays checkmark next to active alignment in dropdown * background colors * table background color in dark mode * add bg color name * rename color attribute * increase minimum table width --- .../table/table-background-color.tsx | 145 ++++++++++++++++++ .../components/table/table-cell-menu.tsx | 21 +++ .../components/table/table-text-alignment.tsx | 109 +++++++++++++ .../features/editor/extensions/extensions.ts | 2 +- .../src/features/editor/styles/table.css | 53 ++++++- .../src/collaboration/collaboration.util.ts | 12 +- packages/editor-ext/src/lib/table/cell.ts | 31 ++++ packages/editor-ext/src/lib/table/header.ts | 37 +++++ packages/editor-ext/src/lib/table/index.ts | 1 + 9 files changed, 401 insertions(+), 10 deletions(-) create mode 100644 apps/client/src/features/editor/components/table/table-background-color.tsx create mode 100644 apps/client/src/features/editor/components/table/table-text-alignment.tsx create mode 100644 packages/editor-ext/src/lib/table/header.ts diff --git a/apps/client/src/features/editor/components/table/table-background-color.tsx b/apps/client/src/features/editor/components/table/table-background-color.tsx new file mode 100644 index 00000000..204f0b02 --- /dev/null +++ b/apps/client/src/features/editor/components/table/table-background-color.tsx @@ -0,0 +1,145 @@ +import React, { FC } from "react"; +import { IconCheck, IconPalette } from "@tabler/icons-react"; +import { + ActionIcon, + ColorSwatch, + Popover, + Stack, + Text, + Tooltip, + UnstyledButton, +} from "@mantine/core"; +import { useEditor } from "@tiptap/react"; +import { useTranslation } from "react-i18next"; + +export interface TableColorItem { + name: string; + color: string; +} + +interface TableBackgroundColorProps { + editor: ReturnType; +} + +const TABLE_COLORS: TableColorItem[] = [ + { name: "Default", color: "" }, + { name: "Blue", color: "#b4d5ff" }, + { name: "Green", color: "#acf5d2" }, + { name: "Yellow", color: "#fef1b4" }, + { name: "Red", color: "#ffbead" }, + { name: "Pink", color: "#ffc7fe" }, + { name: "Gray", color: "#eaecef" }, + { name: "Purple", color: "#c1b7f2" }, +]; + +export const TableBackgroundColor: FC = ({ + editor, +}) => { + const { t } = useTranslation(); + const [opened, setOpened] = React.useState(false); + + const setTableCellBackground = (color: string, colorName: string) => { + editor + .chain() + .focus() + .updateAttributes("tableCell", { + backgroundColor: color || null, + backgroundColorName: color ? colorName : null + }) + .updateAttributes("tableHeader", { + backgroundColor: color || null, + backgroundColorName: color ? colorName : null + }) + .run(); + setOpened(false); + }; + + // Get current cell's background color + const getCurrentColor = () => { + if (editor.isActive("tableCell")) { + const attrs = editor.getAttributes("tableCell"); + return attrs.backgroundColor || ""; + } + if (editor.isActive("tableHeader")) { + const attrs = editor.getAttributes("tableHeader"); + return attrs.backgroundColor || ""; + } + return ""; + }; + + const currentColor = getCurrentColor(); + + return ( + + + + setOpened(!opened)} + > + + + + + + + + + {t("Background color")} + + +
+ {TABLE_COLORS.map((item, index) => ( + setTableCellBackground(item.color, item.name)} + style={{ + position: "relative", + width: "24px", + height: "24px", + }} + title={t(item.name)} + > + + {currentColor === item.color && ( + + )} + + + ))} +
+
+
+
+ ); +}; diff --git a/apps/client/src/features/editor/components/table/table-cell-menu.tsx b/apps/client/src/features/editor/components/table/table-cell-menu.tsx index e348ea6e..2ea2e8dd 100644 --- a/apps/client/src/features/editor/components/table/table-cell-menu.tsx +++ b/apps/client/src/features/editor/components/table/table-cell-menu.tsx @@ -12,8 +12,11 @@ import { IconColumnRemove, IconRowRemove, IconSquareToggle, + IconTableRow, } from "@tabler/icons-react"; import { useTranslation } from "react-i18next"; +import { TableBackgroundColor } from "./table-background-color"; +import { TableTextAlignment } from "./table-text-alignment"; export const TableCellMenu = React.memo( ({ editor, appendTo }: EditorMenuProps): JSX.Element => { @@ -45,6 +48,10 @@ export const TableCellMenu = React.memo( editor.chain().focus().deleteRow().run(); }, [editor]); + const toggleHeaderCell = useCallback(() => { + editor.chain().focus().toggleHeaderCell().run(); + }, [editor]); + return ( + + + + + + + + + ); diff --git a/apps/client/src/features/editor/components/table/table-text-alignment.tsx b/apps/client/src/features/editor/components/table/table-text-alignment.tsx new file mode 100644 index 00000000..0cde7f44 --- /dev/null +++ b/apps/client/src/features/editor/components/table/table-text-alignment.tsx @@ -0,0 +1,109 @@ +import React, { FC } from "react"; +import { + IconAlignCenter, + IconAlignLeft, + IconAlignRight, + IconCheck, +} from "@tabler/icons-react"; +import { + ActionIcon, + Button, + Popover, + rem, + ScrollArea, + Tooltip, +} from "@mantine/core"; +import { useEditor } from "@tiptap/react"; +import { useTranslation } from "react-i18next"; + +interface TableTextAlignmentProps { + editor: ReturnType; +} + +interface AlignmentItem { + name: string; + icon: React.ElementType; + command: () => void; + isActive: () => boolean; + value: string; +} + +export const TableTextAlignment: FC = ({ editor }) => { + const { t } = useTranslation(); + const [opened, setOpened] = React.useState(false); + + const items: AlignmentItem[] = [ + { + name: "Align left", + value: "left", + isActive: () => editor.isActive({ textAlign: "left" }), + command: () => editor.chain().focus().setTextAlign("left").run(), + icon: IconAlignLeft, + }, + { + name: "Align center", + value: "center", + isActive: () => editor.isActive({ textAlign: "center" }), + command: () => editor.chain().focus().setTextAlign("center").run(), + icon: IconAlignCenter, + }, + { + name: "Align right", + value: "right", + isActive: () => editor.isActive({ textAlign: "right" }), + command: () => editor.chain().focus().setTextAlign("right").run(), + icon: IconAlignRight, + }, + ]; + + const activeItem = items.find((item) => item.isActive()) || items[0]; + + return ( + + + + setOpened(!opened)} + > + + + + + + + + + {items.map((item, index) => ( + + ))} + + + + + ); +}; \ No newline at end of file diff --git a/apps/client/src/features/editor/extensions/extensions.ts b/apps/client/src/features/editor/extensions/extensions.ts index 7b83fd31..4558151d 100644 --- a/apps/client/src/features/editor/extensions/extensions.ts +++ b/apps/client/src/features/editor/extensions/extensions.ts @@ -11,7 +11,6 @@ import { Typography } from "@tiptap/extension-typography"; import { TextStyle } from "@tiptap/extension-text-style"; import { Color } from "@tiptap/extension-color"; import Table from "@tiptap/extension-table"; -import TableHeader from "@tiptap/extension-table-header"; import SlashCommand from "@/features/editor/extensions/slash-command"; import { Collaboration } from "@tiptap/extension-collaboration"; import { CollaborationCursor } from "@tiptap/extension-collaboration-cursor"; @@ -25,6 +24,7 @@ import { MathInline, TableCell, TableRow, + TableHeader, TrailingNode, TiptapImage, Callout, diff --git a/apps/client/src/features/editor/styles/table.css b/apps/client/src/features/editor/styles/table.css index 7d02ef03..d60a299c 100644 --- a/apps/client/src/features/editor/styles/table.css +++ b/apps/client/src/features/editor/styles/table.css @@ -4,6 +4,7 @@ overflow-x: auto; & table { overflow-x: hidden; + min-width: 700px !important; } } @@ -38,8 +39,8 @@ th { background-color: light-dark( - var(--mantine-color-gray-1), - var(--mantine-color-dark-5) + var(--mantine-color-gray-1), + var(--mantine-color-dark-5) ); font-weight: bold; text-align: left; @@ -66,8 +67,54 @@ position: absolute; z-index: 2; } - } } +/* Table cell background colors with dark mode support */ +.ProseMirror { + table { + @mixin dark { + /* Blue */ + td[data-background-color="#b4d5ff"], + th[data-background-color="#b4d5ff"] { + background-color: #1a3a5c !important; + } + /* Green */ + td[data-background-color="#acf5d2"], + th[data-background-color="#acf5d2"] { + background-color: #1a4d3a !important; + } + + /* Yellow */ + td[data-background-color="#fef1b4"], + th[data-background-color="#fef1b4"] { + background-color: #7c5014 !important; + } + + /* Red */ + td[data-background-color="#ffbead"], + th[data-background-color="#ffbead"] { + background-color: #5c2a23 !important; + } + + /* Pink */ + td[data-background-color="#ffc7fe"], + th[data-background-color="#ffc7fe"] { + background-color: #4d2a4d !important; + } + + /* Gray */ + td[data-background-color="#eaecef"], + th[data-background-color="#eaecef"] { + background-color: #2a2e33 !important; + } + + /* Purple */ + td[data-background-color="#c1b7f2"], + th[data-background-color="#c1b7f2"] { + background-color: #3a2f5c !important; + } + } + } +} diff --git a/apps/server/src/collaboration/collaboration.util.ts b/apps/server/src/collaboration/collaboration.util.ts index 8a41d79d..a766ec91 100644 --- a/apps/server/src/collaboration/collaboration.util.ts +++ b/apps/server/src/collaboration/collaboration.util.ts @@ -11,7 +11,6 @@ import { TextStyle } from '@tiptap/extension-text-style'; import { Color } from '@tiptap/extension-color'; import { Youtube } from '@tiptap/extension-youtube'; import Table from '@tiptap/extension-table'; -import TableHeader from '@tiptap/extension-table-header'; import { Callout, Comment, @@ -22,6 +21,7 @@ import { LinkExtension, MathBlock, MathInline, + TableHeader, TableCell, TableRow, TiptapImage, @@ -31,7 +31,7 @@ import { Drawio, Excalidraw, Embed, - Mention + Mention, } from '@docmost/editor-ext'; import { generateText, getSchema, JSONContent } from '@tiptap/core'; import { generateHTML } from '../common/helpers/prosemirror/html'; @@ -46,7 +46,7 @@ export const tiptapExtensions = [ codeBlock: false, }), Comment, - TextAlign.configure({ types: ["heading", "paragraph"] }), + TextAlign.configure({ types: ['heading', 'paragraph'] }), TaskList, TaskItem.configure({ nested: true, @@ -66,9 +66,9 @@ export const tiptapExtensions = [ DetailsContent, DetailsSummary, Table, - TableHeader, - TableRow, TableCell, + TableRow, + TableHeader, Youtube, TiptapImage, TiptapVideo, @@ -78,7 +78,7 @@ export const tiptapExtensions = [ Drawio, Excalidraw, Embed, - Mention + Mention, ] as any; export function jsonToHtml(tiptapJson: any) { diff --git a/packages/editor-ext/src/lib/table/cell.ts b/packages/editor-ext/src/lib/table/cell.ts index 17ab1e29..0714d69a 100644 --- a/packages/editor-ext/src/lib/table/cell.ts +++ b/packages/editor-ext/src/lib/table/cell.ts @@ -3,4 +3,35 @@ import { TableCell as TiptapTableCell } from "@tiptap/extension-table-cell"; export const TableCell = TiptapTableCell.extend({ name: "tableCell", content: "paragraph+", + + addAttributes() { + return { + ...this.parent?.(), + backgroundColor: { + default: null, + parseHTML: (element) => element.style.backgroundColor || null, + renderHTML: (attributes) => { + if (!attributes.backgroundColor) { + return {}; + } + return { + style: `background-color: ${attributes.backgroundColor}`, + 'data-background-color': attributes.backgroundColor, + }; + }, + }, + backgroundColorName: { + default: null, + parseHTML: (element) => element.getAttribute('data-background-color-name') || null, + renderHTML: (attributes) => { + if (!attributes.backgroundColorName) { + return {}; + } + return { + 'data-background-color-name': attributes.backgroundColorName.toLowerCase(), + }; + }, + }, + }; + }, }); diff --git a/packages/editor-ext/src/lib/table/header.ts b/packages/editor-ext/src/lib/table/header.ts new file mode 100644 index 00000000..46b1efaf --- /dev/null +++ b/packages/editor-ext/src/lib/table/header.ts @@ -0,0 +1,37 @@ +import { TableHeader as TiptapTableHeader } from "@tiptap/extension-table-header"; + +export const TableHeader = TiptapTableHeader.extend({ + name: "tableHeader", + content: "paragraph+", + + addAttributes() { + return { + ...this.parent?.(), + backgroundColor: { + default: null, + parseHTML: (element) => element.style.backgroundColor || null, + renderHTML: (attributes) => { + if (!attributes.backgroundColor) { + return {}; + } + return { + style: `background-color: ${attributes.backgroundColor}`, + 'data-background-color': attributes.backgroundColor, + }; + }, + }, + backgroundColorName: { + default: null, + parseHTML: (element) => element.getAttribute('data-background-color-name') || null, + renderHTML: (attributes) => { + if (!attributes.backgroundColorName) { + return {}; + } + return { + 'data-background-color-name': attributes.backgroundColorName.toLowerCase(), + }; + }, + }, + }; + }, +}); \ No newline at end of file diff --git a/packages/editor-ext/src/lib/table/index.ts b/packages/editor-ext/src/lib/table/index.ts index 5661ef84..656c1825 100644 --- a/packages/editor-ext/src/lib/table/index.ts +++ b/packages/editor-ext/src/lib/table/index.ts @@ -1,2 +1,3 @@ export * from "./row"; export * from "./cell"; +export * from "./header";