feat(tiptap): add native table support with round-trip MDC conversion#444
feat(tiptap): add native table support with round-trip MDC conversion#444nvdai2401 wants to merge 20 commits into
Conversation
|
@nvdai2401 is attempting to deploy a commit to the Nuxt Team on Vercel. A member of the Team first needs to authorize it. |
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds integration test for table → MDC → TipTap → MDC → markdown round-trip. Fails at expect(tableNode).toBeDefined() because mdcToTiptapMap has no table handler yet (TDD RED phase). Also adds vitest infrastructure fixes needed to run tests without a full dev:prepare build: resolve alias for self-referential nuxt-studio/app/utils import, and a src/module/tsconfig.json so vite's esbuild transform doesn't need the gitignored .nuxt/tsconfig.json. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds table/tableRow/tableHeader/tableCell entries to tiptapToMDCMap with createTableElement and createTableCellElement factory functions. Also fixes the round-trip test assertions to account for GFM column padding (the serializer pads columns to the longest cell value). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Inserting a table threw "Duplicate use of selection JSON ID cell"
because prosemirror-tables was being loaded twice in the studio app's
Vite bundle: once inlined inside the @tiptap/extension-table optimizeDeps
pre-bundle, and once as a direct unbundled import via @tiptap/pm/tables.
Both registered the cell selection on the same shared Selection class.
Why: import all four extensions (Table/Row/Cell/Header) from a single
package and let Vite control which copy of prosemirror-tables runs.
How to apply: exclude @tiptap/extension-table from optimizeDeps and
include @tiptap/pm/tables instead so the table module is the single
shared instance the unbundled extension-table imports.
Also removes the redundant @tiptap/extension-table-{cell,header,row}
devDependencies — they are thin re-export shims of @tiptap/extension-table
and adding them increased the chance of multi-path resolution.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The customHandlers object spread the dynamic component handlers AFTER the explicit image/video/table handlers. When a project has a component named "table" (e.g. Nuxt UI's UTable), its componentHandler was overwriting tableHandler and the slash command inserted a collapsed Vue component block instead of the native TipTap table. Move the spread before the explicit handlers so image/video/table win even when a same-named component exists in the meta catalog. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Without these, tables rendered as bare HTML in the editor — no borders, no padding, no header background — even though the live preview styled them correctly via the site's typography CSS. Cells inside the studio editor now show visible borders, padded content, a highlighted header row, and a selected-cell indicator. Includes dark-mode variants. Targets the .studio-table class set on the Table extension's HTMLAttributes. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
ProseMirror lets editor.can().setParagraph() return true on a table selection — choosing it would silently replace the table with a single paragraph and drop every cell value. Match the UX of custom block components like Card / Element where these options are greyed out by mapping the Turn into children to disabled items when the selected node is a table. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…is active A project's MDC components catalog can contain a 'table' entry (e.g. Nuxt UI's <UTable>). The slash menu was showing two "Table" entries — one from getStandardSuggestionItems (our native extension) and one from componentItems — which is confusing UX, especially since both now route to the same handler but originate from different code paths. Filter components named 'table' out of componentItems so only the native entry remains. Implemented as a NATIVE_OVERRIDE_COMPONENTS set so future native overrides can opt in the same way. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds a contextual bubble menu that appears when the cursor is inside
a table cell, exposing TipTap's built-in table commands as visible
buttons:
- Add row above / below
- Add column left / right
- Toggle header row
- Delete row / column / entire table
Implementation uses @tiptap/vue-3's BubbleMenu with shouldShow gated
on editor.isActive('table') so it only renders inside tables and
doesn't interfere with the regular text-selection bubble toolbar.
Buttons follow the existing UButton + i-lucide-* pattern; delete
table is colored error to signal destructiveness.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
ad3b249 to
7486e13
Compare
A 3x3 table feels heavyweight as a starter — 1 header + 2 data rows × 3 columns is more cells than most users want before they start filling things in. Drop the default to 1 header + 1 data row × 2 columns; users can grow the table from the bubble menu. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
Hey @larbish @TotomInc, I created this PR to add native table support to the Studio editor. It’s been a long-standing gap and I tried to keep the change as minimal and idiomatic as possible. What’s in: /table slash command inserts a 2x2 GFM table; cells are editable with Tab navigation; existing markdown tables render as editable tables; full round-trip through MDC ↔ markdown (4 integration tests). A floating bubble menu inside tables exposes add/delete row/column and toggle-header actions. Drag-handle “Turn into” is force-disabled for tables (otherwise ProseMirror would silently swap the table for a paragraph). What I’d appreciate eyes on:
Out of scope for v1: column resizing, cell merging/splitting. Happy to tackle those in a follow-up. Branch is up to date with main, all 247 tests pass. |
|
Hello @nvdai2401, thanks a lot for this PR! That's a great feature. I'm currently finishing this PR about migrating the nuxt studio parser to comark.dev. This has a big impact on the transformation between TipTap AST and Comark AST (and no more legacy MDC AST). I should merge it really soon and I'll provide a skill I've maintained during the all migration so you can easily adapt your PR once this branch is merged. I'll keep you posted! |
|
Round-trip mdc conversion is a big upgrade to studio, can actually enable bigger patterns (e.g.: kanban boards and other components) |
Summary
Adds native TipTap table editing to the Studio editor. Users can:
/tableto insert a 2x2 table with a header rowApproach
Uses upstream
@tiptap/extension-tabledirectly with native ProseMirror rendering — no custom NodeView, so cell selection and keyboard navigation behave as users expect from a standard TipTap editor.The implementation followed a TDD cycle: failing round-trip test first, then bidirectional MDC conversion (
mdcToTiptapfortable/thead/tbody/tr/th/td,tiptapToMdcfor the inverse). Cell content is wrapped in a paragraph in TipTap (its schema requires block content in cells) and unwrapped on export.A floating
BubbleMenu(gated oneditor.isActive('table')) exposes TipTap's built-in table commands as buttons: add row above/below, add column left/right, toggle header, delete row/column/table.What's in scope (v1)
/tableslash commandNot in scope (deferred)
resizable: falsefor v1)Test plan
/tableinsertion in dev playgroundoptimizeDepsconfig insrc/app/vite.config.tsdoesn't break other Vite consumers@tiptap/extension-table@^3.22.2is acceptableNotable changes & gotchas
@tiptap/extension-tableis excluded from optimizeDeps and@tiptap/pm/tablesis included instead. Without this, prosemirror-tables loaded twice (once inlined inside the @tiptap/extension-table pre-bundle, once as a direct ES module fromprosemirror-tables/dist/index.js), causingDuplicate use of selection JSON ID cell.useTiptapEditor'scustomHandlersnow spreads dynamic component handlers before the explicitimage/video/tablehandlers so a project-defined component namedtable(e.g. Nuxt UI's<UTable>) doesn't shadow our native handler.useTiptapEditorfilterscomponentItemsto skip names listed inNATIVE_OVERRIDE_COMPONENTS(currently['table']), so the slash menu shows one Table entry instead of two.vitest.config.tsalias fornuxt-studio/appand a minimalsrc/module/tsconfig.jsonwere added so tests run without requiringpnpm dev:prepare. These are benign in CI on Node 22, but happy to drop them if reviewers prefer.Testing recording
https://www.loom.com/share/9d81adf0698c4e5bbb20d8f474dda2b0
🤖 Generated with Claude Code