diff --git a/bun.lockb b/bun.lockb index 2f225b1..f91f653 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/package.json b/package.json index 5cc19a7..59062f8 100644 --- a/package.json +++ b/package.json @@ -22,6 +22,10 @@ "@tiptap/extension-image": "^3.14.0", "@tiptap/extension-link": "^3.14.0", "@tiptap/extension-list-item": "^3.14.0", + "@tiptap/extension-table": "^3.14.0", + "@tiptap/extension-table-cell": "^3.14.0", + "@tiptap/extension-table-header": "^3.14.0", + "@tiptap/extension-table-row": "^3.14.0", "@tiptap/extension-text-style": "^3.14.0", "@tiptap/pm": "^3.14.0", "@tiptap/starter-kit": "^3.14.0", diff --git a/src/app.css b/src/app.css index 51c505b..df5de34 100644 --- a/src/app.css +++ b/src/app.css @@ -726,3 +726,86 @@ a.hover-underline-animation:hover::after { } } } + +/* Table styles for TipTap editor */ +.tiptap-table { + border-collapse: collapse; + table-layout: fixed; + width: 100%; + margin: 1rem 0; + overflow: hidden; +} + +.tiptap-table td, +.tiptap-table th { + min-width: 1em; + border: 2px solid var(--color-surface2); + padding: 0.5rem; + vertical-align: top; + box-sizing: border-box; + position: relative; +} + +.tiptap-table th { + font-weight: bold; + text-align: left; + background-color: var(--color-surface0); +} + +.tiptap-table .selectedCell { + background-color: var(--color-surface1); +} + +.tiptap-table p { + margin: 0; +} + +/* Additional table styles for ProseMirror */ +.ProseMirror table { + border-collapse: collapse; + table-layout: fixed; + width: 100%; + margin: 1rem 0; + overflow: hidden; +} + +.ProseMirror table td, +.ProseMirror table th { + min-width: 3em; + border: 2px solid var(--color-text); + padding: 0.5rem; + vertical-align: top; + box-sizing: border-box; + position: relative; + background-color: var(--color-mantle); +} + +.ProseMirror table th { + font-weight: bold; + text-align: left; + background-color: var(--color-surface0); +} + +.ProseMirror table .selectedCell:after { + z-index: 2; + position: absolute; + content: ""; + left: 0; + right: 0; + top: 0; + bottom: 0; + background-color: var(--color-blue); + opacity: 0.2; + pointer-events: none; +} + +.ProseMirror table .column-resize-handle { + position: absolute; + right: -2px; + top: 0; + bottom: 0; + width: 4px; + z-index: 20; + background-color: var(--color-blue); + pointer-events: none; +} diff --git a/src/components/blog/TextEditor.tsx b/src/components/blog/TextEditor.tsx index 3e168ba..5ae8d1b 100644 --- a/src/components/blog/TextEditor.tsx +++ b/src/components/blog/TextEditor.tsx @@ -4,6 +4,10 @@ import StarterKit from "@tiptap/starter-kit"; import Link from "@tiptap/extension-link"; import CodeBlockLowlight from "@tiptap/extension-code-block-lowlight"; import Image from "@tiptap/extension-image"; +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 { Node } from "@tiptap/core"; import { createLowlight, common } from "lowlight"; import css from "highlight.js/lib/languages/css"; @@ -196,6 +200,12 @@ export default function TextEditor(props: TextEditorProps) { left: 0 }); + const [showTableMenu, setShowTableMenu] = createSignal(false); + const [tableMenuPosition, setTableMenuPosition] = createSignal({ + top: 0, + left: 0 + }); + const editor = createTiptapEditor(() => ({ element: editorRef, extensions: [ @@ -205,7 +215,28 @@ export default function TextEditor(props: TextEditorProps) { openOnClick: true }), Image, - IframeEmbed + IframeEmbed, + Table.configure({ + resizable: true, + HTMLAttributes: { + class: "tiptap-table" + } + }), + TableRow.configure({ + HTMLAttributes: { + class: "tiptap-table-row" + } + }), + TableHeader.configure({ + HTMLAttributes: { + class: "tiptap-table-header" + } + }), + TableCell.configure({ + HTMLAttributes: { + class: "tiptap-table-cell" + } + }) ], content: props.preSet || `

Hello! World

`, editorProps: { @@ -319,6 +350,31 @@ export default function TextEditor(props: TextEditorProps) { setShowLanguageSelector(!showLanguageSelector()); }; + const insertTable = (rows: number, cols: number) => { + const instance = editor(); + if (!instance) return; + + console.log("Inserting table:", rows, "x", cols); + + instance + .chain() + .focus() + .insertTable({ rows, cols, withHeaderRow: true }) + .run(); + + console.log("Table inserted, isActive:", instance.isActive("table")); + setShowTableMenu(false); + }; + + const showTableInserter = (e: MouseEvent) => { + const buttonRect = (e.currentTarget as HTMLElement).getBoundingClientRect(); + setTableMenuPosition({ + top: buttonRect.bottom + 5, + left: buttonRect.left + }); + setShowTableMenu(!showTableMenu()); + }; + // Close language selector on outside click createEffect(() => { if (showLanguageSelector()) { @@ -340,6 +396,78 @@ export default function TextEditor(props: TextEditorProps) { } }); + // Close table menu on outside click + createEffect(() => { + if (showTableMenu()) { + const handleClickOutside = (e: MouseEvent) => { + const target = e.target as HTMLElement; + if ( + !target.closest(".table-menu") && + !target.closest("[data-table-trigger]") + ) { + setShowTableMenu(false); + } + }; + + setTimeout(() => { + document.addEventListener("click", handleClickOutside); + }, 0); + + return () => document.removeEventListener("click", handleClickOutside); + } + }); + + // Table Grid Selector Component + const TableGridSelector = () => { + const [hoverCell, setHoverCell] = createSignal({ row: 0, col: 0 }); + const maxRows = 10; + const maxCols = 10; + + return ( +
+
+ Insert Table: {hoverCell().row + 1} × {hoverCell().col + 1} +
+
+ + {(_, idx) => { + const row = Math.floor(idx() / maxCols); + const col = idx() % maxCols; + + return ( +
setHoverCell({ row, col })} + onClick={() => insertTable(row + 1, col + 1)} + /> + ); + }} + +
+
+ +
+
+ ); + }; + return (
@@ -499,6 +627,19 @@ export default function TextEditor(props: TextEditorProps) {
+ {/* Table Grid Selector */} + +
+ +
+
+
+
+ + {/* Table controls - shown when cursor is in a table */} + +
+ + + + + + + +
+ + + + + + + +
+ + + + + + + + +
)}