From 25de7d996c2fbc67015a25b3ef3c8169ce300226 Mon Sep 17 00:00:00 2001 From: Linus Rath <139418639+rathlinus@users.noreply.github.com> Date: Thu, 30 Apr 2026 15:16:51 +0200 Subject: [PATCH] feat: add tables to composer #236 --- app/globals.css | 51 ++++++++ components/email/email-composer.tsx | 28 +++-- components/email/rich-text-editor.tsx | 165 +++++++++++++++++++++++++- package-lock.json | 57 +++++++++ package.json | 4 + 5 files changed, 294 insertions(+), 11 deletions(-) diff --git a/app/globals.css b/app/globals.css index 795a05a9..4ee13ebd 100644 --- a/app/globals.css +++ b/app/globals.css @@ -705,3 +705,54 @@ body { .tiptap .ProseMirror-selectednode img { outline: none; } + +.tiptap table { + border-collapse: collapse; + margin: 0.5rem 0; + table-layout: fixed; + width: 100%; + overflow: hidden; +} + +.tiptap table td, +.tiptap table th { + border: 1px solid var(--color-border); + padding: 0.375rem 0.5rem; + vertical-align: top; + position: relative; + min-width: 1em; +} + +.tiptap table th { + background-color: var(--color-muted) !important; + color: var(--color-foreground) !important; + font-weight: 600; + text-align: left; +} + +.tiptap table p { + margin: 0; +} + +.tiptap table .selectedCell::after { + background: rgba(99, 102, 241, 0.15); + content: ""; + inset: 0; + pointer-events: none; + position: absolute; + z-index: 2; +} + +.tiptap table .column-resize-handle { + background-color: var(--color-primary); + bottom: -2px; + pointer-events: none; + position: absolute; + right: -2px; + top: 0; + width: 4px; +} + +.tiptap.resize-cursor { + cursor: col-resize; +} diff --git a/components/email/email-composer.tsx b/components/email/email-composer.tsx index bacb966a..ec6afbe5 100644 --- a/components/email/email-composer.tsx +++ b/components/email/email-composer.tsx @@ -820,19 +820,27 @@ export function EmailComposer({ attachments: Array<{ blobId: string; name: string; type: string; size: number; disposition: 'inline'; cid: string }>; } => { const known = inlineImagesRef.current; - if (known.length === 0) return { html, attachments: [] }; - const doc = new DOMParser().parseFromString(`
${html}`, 'text/html'); const used = new Mapmargins inside table cells, + // inflating row height. Tiptap wraps cell text in
, so force margin:0 + // to match the composer's tight rows. + doc.querySelectorAll('td > p, th > p').forEach((p) => { + const existing = p.getAttribute('style') || ''; + p.setAttribute('style', `margin:0;${existing}`); }); return { diff --git a/components/email/rich-text-editor.tsx b/components/email/rich-text-editor.tsx index a30a7c82..fba66c01 100644 --- a/components/email/rich-text-editor.tsx +++ b/components/email/rich-text-editor.tsx @@ -1,6 +1,6 @@ "use client"; -import React, { useEffect, useCallback } from "react"; +import React, { useEffect, useCallback, useState, useRef } from "react"; import { useEditor, EditorContent } from "@tiptap/react"; import StarterKit from "@tiptap/starter-kit"; import Underline from "@tiptap/extension-underline"; @@ -10,6 +10,10 @@ import { TextStyle } from "@tiptap/extension-text-style"; import Color from "@tiptap/extension-color"; import { ResizableImage } from "@/components/email/resizable-image"; import Placeholder from "@tiptap/extension-placeholder"; +import { Table } from "@tiptap/extension-table"; +import { TableRow } from "@tiptap/extension-table-row"; +import { TableHeader } from "@tiptap/extension-table-header"; +import { TableCell } from "@tiptap/extension-table-cell"; import { cn } from "@/lib/utils"; import { Bold, @@ -29,6 +33,10 @@ import { RemoveFormatting, Heading1, Heading2, + Table as TableIcon, + Trash2, + Rows3, + Columns3, } from "lucide-react"; export interface InlineImageUpload { @@ -79,6 +87,43 @@ function ToolbarSeparator() { return
; } +const TABLE_PICKER_ROWS = 6; +const TABLE_PICKER_COLS = 8; + +function TableSizePicker({ onPick }: { onPick: (rows: number, cols: number) => void }) { + const [hover, setHover] = useState<{ r: number; c: number } | null>(null); + return ( +