diff --git a/docs/phase-1-tiptap-strategy.md b/docs/phase-1-tiptap-strategy.md new file mode 100644 index 000000000..a788930e6 --- /dev/null +++ b/docs/phase-1-tiptap-strategy.md @@ -0,0 +1,142 @@ +# Phase 1 — Tiptap Extension Audit & Paragraph Node Strategy + +**Status:** Decision (no production code) +**Tracking issue:** #267 +**Consumed by:** #272 (paragraph + heading + toolbar) +**Supersedes:** Phase 0 finding #4 in `PHASE-0-FINDINGS.md` + +## Purpose + +Phase 0 shipped the editor spike with `@tiptap/react` + `@tiptap/starter-kit` + `@tiptap/extension-link` because it was the fastest path to a working paragraph. `starter-kit` is a meta-package that drags in ~15 extensions, most of which conflict with our block-aware architecture: headings, lists, code blocks, and blockquotes are **blocks** in our system, not ProseMirror nodes living inside a paragraph. Shipping `starter-kit` unchanged means every paragraph's editor can produce document structure our block store cannot represent. + +This document decides (1) which extensions each block-level Tiptap editor actually needs, and (2) whether paragraph's Tiptap instance uses Tiptap's stock `Paragraph` node or a custom `BlockParagraph` node that talks to the Zustand store when the user splits or merges. + +## 1. Starter-Kit Extension Audit + +`@tiptap/starter-kit` bundles the following extensions (v3.22.x). Each row is decided against our architecture: **one Tiptap editor per block instance, containing only inline content for that single block**. Block-level structure (new paragraphs, headings, lists, quotes, code) is owned by the Zustand block store, not by ProseMirror. + +| # | Extension | Kind | Decision | Reasoning | +|---|---|---|---|---| +| 1 | `Document` | node | **Keep** | Required root. Every ProseMirror doc needs it. | +| 2 | `Paragraph` | node | **Replace** (see §2) | Stock node's Enter/Backspace behavior operates on the PM doc; we need it to operate on the block store. Replaced with `BlockParagraph`. | +| 3 | `Text` | node | **Keep** | Required for any text content. | +| 4 | `Heading` | node | **Drop** | Headings are their own block type in our system. A paragraph's editor must never be able to produce an `
` inside the same editor. Pressing **Backspace** at the start of a block must merge this block's content into the previous block. Both actions operate on the **block tree**, not on a single editor's PM doc.
+
+Option A makes this awkward: we'd have to listen for `onUpdate`, diff the resulting HTML, detect "oh, a second paragraph appeared, let me split this into two blocks," and then forcibly revert the PM doc back to a single paragraph. That is fragile — the PM doc transiently contains structure the block store never agreed to, and reverting it fights Tiptap's transaction pipeline. Selection placement after the split becomes a second fight.
+
+Option B inverts it. `BlockParagraph` installs keymap handlers for `Enter` and `Backspace` that:
+
+- **`Enter` at block boundary:** prevent PM's default, split the current text at the caret, dispatch `insertBlock({ type: 'paragraph', content: `–`
`) based on the store attribute, and the toolbar's level switcher dispatches a store update, not a PM transaction.
+- This keeps heading level in one place (the store) and avoids the "is the source of truth PM or the store?" ambiguity.
+
+## 4. Final Dependency List for #272
+
+Direct dependencies to add / keep in `package.json`:
+
+```json
+{
+ "dependencies": {
+ "@tiptap/react": "^3.22.3",
+ "@tiptap/pm": "^3.22.3",
+ "@tiptap/core": "^3.22.3",
+ "@tiptap/extension-document": "^3.22.3",
+ "@tiptap/extension-text": "^3.22.3",
+ "@tiptap/extension-paragraph": "^3.22.3",
+ "@tiptap/extension-heading": "^3.22.3",
+ "@tiptap/extension-hard-break": "^3.22.3",
+ "@tiptap/extension-bold": "^3.22.3",
+ "@tiptap/extension-italic": "^3.22.3",
+ "@tiptap/extension-link": "^3.22.3"
+ }
+}
+```
+
+Dependencies to **remove**:
+
+- `@tiptap/starter-kit` — replaced by the explicit list above.
+
+Notes:
+
+- `@tiptap/core` and `@tiptap/pm` are transitive today but become direct because `BlockParagraph` imports from them (`Node.create`, `Plugin`, `TextSelection`). Declaring them directly avoids relying on hoisting.
+- `@tiptap/extension-paragraph` and `@tiptap/extension-heading` are the **base** nodes that `BlockParagraph` / `BlockHeading` extend. They are not registered as-is in the editor — our custom nodes are registered in their place.
+- `history` is intentionally absent. Store-level undo/redo replaces it.
+- `strike`, `code`, `bullet-list`, `ordered-list`, `list-item`, `blockquote`, `code-block`, `horizontal-rule`, `dropcursor`, `gapcursor` are intentionally absent. They come back if and only if they return as explicit block types in a later phase.
+
+## 5. Open Follow-ups (not part of #267)
+
+- **Store-level undo/redo** — replaces disabled Tiptap `History`. Already landed under **#266** (Phase 1.4: Undo/redo history layer on editor store); implemented in `editorStore.ts` and covered by `editorStoreHistory.test.ts`. No follow-up work remains — #272 just needs to pass `history: false` when constructing each Tiptap instance.
+- **Dropcursor revisit** — if we later allow dropping images or other media inside a paragraph (rather than as sibling blocks), dropcursor comes back.
+- **Inline `code` and `strike` marks** — Phase 2 typography expansion.
+- **Collaborative editing extensions** (`@tiptap/extension-collaboration`) — not in scope anywhere in Phase 1; flag for Phase 5+.
+
+## Acceptance (against #267)
+
+1. ✅ `docs/phase-1-tiptap-strategy.md` covers the starter-kit audit (§1), the paragraph node decision (§2), and the final dependency list for #272 (§4).
+2. ✅ No production code changes — this commit touches only documentation.