diff --git a/apps/desktop/src/renderer/components/MarkdownEditor/MarkdownEditor.tsx b/apps/desktop/src/renderer/components/MarkdownEditor/MarkdownEditor.tsx index 36dc056dfca..ee5ea3d6cbf 100644 --- a/apps/desktop/src/renderer/components/MarkdownEditor/MarkdownEditor.tsx +++ b/apps/desktop/src/renderer/components/MarkdownEditor/MarkdownEditor.tsx @@ -21,6 +21,7 @@ import { OrderedList } from "@tiptap/extension-ordered-list"; import { Paragraph } from "@tiptap/extension-paragraph"; import Placeholder from "@tiptap/extension-placeholder"; import { Strike } from "@tiptap/extension-strike"; +import { TableKit } from "@tiptap/extension-table"; import TaskItem from "@tiptap/extension-task-item"; import TaskList from "@tiptap/extension-task-list"; import { Text } from "@tiptap/extension-text"; @@ -150,6 +151,20 @@ function getMarkdown(editor: Editor | null): string { return storage?.markdown?.getMarkdown?.() ?? ""; } +function isMarkdownTable(text: string): boolean { + const lines = text + .trim() + .split(/\r?\n/) + .map((line) => line.trim()) + .filter(Boolean); + + if (lines.length < 2 || !lines[0]?.includes("|")) { + return false; + } + + return /^\|?\s*:?-{3,}:?\s*(\|\s*:?-{3,}:?\s*)+\|?$/.test(lines[1]); +} + export function MarkdownEditor({ content, onSave, @@ -166,6 +181,7 @@ export function MarkdownEditor({ // Thread through a ref so the extension reads the live callback each fire. const searchFilesRef = useRef(searchFiles); searchFilesRef.current = searchFiles; + const editorRef = useRef(null); const { getUrlAction } = useInlineLinkActions(); @@ -241,6 +257,26 @@ export function MarkdownEditor({ LinearImage.configure({ HTMLAttributes: { class: "max-w-full h-auto rounded-md my-3" }, }), + TableKit.configure({ + table: { + resizable: false, + cellMinWidth: 192, + HTMLAttributes: { + class: "markdown-table my-4 min-w-full border-collapse", + }, + }, + tableHeader: { + HTMLAttributes: { + class: + "bg-muted px-4 py-2 text-left text-sm font-semibold align-top", + }, + }, + tableCell: { + HTMLAttributes: { + class: "border-t border-border px-4 py-2 text-sm align-top", + }, + }, + }), Placeholder.configure({ placeholder: ({ node }) => { if (node.type.name === "paragraph") { @@ -278,6 +314,22 @@ export function MarkdownEditor({ } return false; }, + handlePaste: (_, event) => { + const text = event.clipboardData?.getData("text/plain") ?? ""; + const currentEditor = editorRef.current; + if (!currentEditor || !isMarkdownTable(text)) { + return false; + } + + event.preventDefault(); + return currentEditor.commands.insertContentAt( + { + from: currentEditor.state.selection.from, + to: currentEditor.state.selection.to, + }, + text, + ); + }, handleClickOn: (_view, _pos, _node, _nodePos, event) => { const target = event.target as HTMLElement | null; const anchor = target?.closest?.("a") as HTMLAnchorElement | null; @@ -302,6 +354,7 @@ export function MarkdownEditor({ onSave?.(getMarkdown(editor)); }, }); + editorRef.current = editor; useEffect(() => { if (!editor || editor.isFocused) return; diff --git a/apps/desktop/src/renderer/components/MarkdownEditor/markdown-editor.css b/apps/desktop/src/renderer/components/MarkdownEditor/markdown-editor.css index bdb50063efe..d22e8d50486 100644 --- a/apps/desktop/src/renderer/components/MarkdownEditor/markdown-editor.css +++ b/apps/desktop/src/renderer/components/MarkdownEditor/markdown-editor.css @@ -85,3 +85,27 @@ ul[data-type="taskList"] 0 0 0 2px hsl(var(--background)), 0 0 0 4px hsl(var(--ring)); } + +.markdown-table { + width: max-content; + min-width: 100%; + overflow: hidden; + border: 1px solid hsl(var(--border)); + border-radius: 0.375rem; +} + +.markdown-table th, +.markdown-table td { + border: 1px solid hsl(var(--border)); + min-width: 12rem; +} + +.markdown-table th > *, +.markdown-table td > * { + margin-top: 0; + margin-bottom: 0; +} + +.markdown-table .selectedCell { + background-color: hsl(var(--accent) / 0.6); +} diff --git a/apps/desktop/src/renderer/components/MarkdownRenderer/components/TipTapMarkdownRenderer/createMarkdownExtensions.ts b/apps/desktop/src/renderer/components/MarkdownRenderer/components/TipTapMarkdownRenderer/createMarkdownExtensions.ts index 0f04ccd4735..71212c6228e 100644 --- a/apps/desktop/src/renderer/components/MarkdownRenderer/components/TipTapMarkdownRenderer/createMarkdownExtensions.ts +++ b/apps/desktop/src/renderer/components/MarkdownRenderer/components/TipTapMarkdownRenderer/createMarkdownExtensions.ts @@ -16,10 +16,7 @@ import { ListItem } from "@tiptap/extension-list-item"; import { OrderedList } from "@tiptap/extension-ordered-list"; import { Paragraph } from "@tiptap/extension-paragraph"; import { Strike } from "@tiptap/extension-strike"; -import { Table } from "@tiptap/extension-table"; -import TableCell from "@tiptap/extension-table-cell"; -import TableHeader from "@tiptap/extension-table-header"; -import TableRow from "@tiptap/extension-table-row"; +import { TableKit } from "@tiptap/extension-table"; import TaskItem from "@tiptap/extension-task-item"; import TaskList from "@tiptap/extension-task-list"; import { Text } from "@tiptap/extension-text"; @@ -158,21 +155,23 @@ export function createMarkdownExtensions({ }, }), SafeImage, - Table.configure({ - resizable: false, - HTMLAttributes: { - class: "markdown-table my-4 min-w-full border-collapse", + TableKit.configure({ + table: { + resizable: false, + cellMinWidth: 192, + HTMLAttributes: { + class: "markdown-table my-4 min-w-full border-collapse", + }, }, - }), - TableRow, - TableHeader.configure({ - HTMLAttributes: { - class: "bg-muted px-4 py-2 text-left text-sm font-semibold align-top", + tableHeader: { + HTMLAttributes: { + class: "bg-muted px-4 py-2 text-left text-sm font-semibold align-top", + }, }, - }), - TableCell.configure({ - HTMLAttributes: { - class: "border-t border-border px-4 py-2 text-sm align-top", + tableCell: { + HTMLAttributes: { + class: "border-t border-border px-4 py-2 text-sm align-top", + }, }, }), Markdown.configure({