Files
famapp/docs/decisions/0004-rich-text-editor.md
T

5.9 KiB

0004 — Rich-text editor library and storage format

Date: 2026-07-03 Status: accepted (2026-07-04)

Context

Notes (task 85) and journal (task 86) need a shared rich-text editor with emoji and modern formatting. Notes today use a plain <textarea> and a minimal custom markdown preview (# headings, - lists only). Body is stored as plain text in notes.body. Mobile horizontal overflow is a known bug (Gitea #5). Gitea epic #7.

What we decided (2026-07-04)

# Topic Choice
1 Editor library TipTap (ProseMirror family) — WYSIWYG, not home-grown
2 Storage format HTML in existing notes.body text column; sanitize on every render
3 v1 formatting Full-featured: headings, bold/italic, bullet + numbered lists, links, emoji, blockquotes, code blocks, tables, checklists, images, file attachments, undo/redo
4 Images & attachments Upload to famapp via existing /api/uploads; embed returned URLs in note HTML
5 Shared component location src/components/rich-text/ — imported by notes now, journal later
6 Existing plain-text notes Lazy migration: on read, non-HTML bodies wrapped as paragraphs; persisted as HTML on next save. No schema change.
7 Checklists on read surfaces Interactive everywhere — index, dashboard widget, and share view can toggle checklist items (updates note via API)

Decision

Editor: TipTap

  • Use TipTap with React bindings and official extensions for the v1 feature set.
  • Export contract for the shared component:
    • RichTextEditor — controlled value / onChange (HTML string), optional editable, toolbar config
    • RichTextContent — read-only renderer with sanitization and overflow-safe CSS
    • RichTextChecklist behavior — interactive task items on read surfaces (see below)
  • Journal (task 86) imports from src/components/rich-text/ without forking.

Storage: HTML in notes.body

  • notes.body remains a text column; content is HTML after first save from the new editor.
  • Sanitize all HTML before dangerouslySetInnerHTML (DOMPurify or equivalent) on every read surface: editor preview, index cards, dashboard widget, share viewer.
  • Allowlist tags/attributes matching TipTap output (headings, lists, task lists, tables, links, images, code, blockquote, etc.).
  • API /api/v1/notes continues to accept/return body as a string (now HTML). Document in OpenAPI.

Lazy migration for legacy notes

  • Helper normalizeNoteBody(body: string): string — if body does not start with < (heuristic) or fails HTML detection, convert plain text to <p> paragraphs (preserve line breaks).
  • Run on read paths (editor load, index excerpt, widget, share) until user saves, which persists HTML.
  • No one-shot migration script required for v1.

Images and attachments

  • TipTap image + file extensions call existing upload route (POST /api/uploads).
  • Stored URLs embedded in HTML (<img src="…">, attachment links as <a href="…">).
  • Upload auth: session or household bearer token (same as other API calls).

Interactive checklists (read surfaces)

  • TipTap TaskList extension for authoring.
  • On index, widget, and share surfaces: render checklists with tappable checkboxes.
  • Toggle calls a server action or PATCH /api/v1/notes/:id that updates only the task-item checked state inside the HTML (parse → flip data-checked / class → save). Activity log optional.
  • Share links with write capability may allow toggle if share token grants write (reuse existing share write path where applicable).

Mobile overflow

  • Fix as part of RichTextContent: overflow-wrap: anywhere, max-width: 100%, table horizontal scroll container, no fixed-width embeds. Closes Gitea #5.

Consequences

Positive

  • One editor stack for notes + journal; wife-friendly WYSIWYG.
  • HTML storage is simple to render and matches TipTap's native export.
  • Lazy migration avoids a risky bulk conversion; existing notes keep working.
  • Upload reuse avoids new storage infrastructure.

Trade-offs

  • Full TipTap extension set increases bundle size — acceptable for an editor route; lazy-load editor chunk on notes/journal pages.
  • HTML in DB is less readable than markdown for debugging; mitigated by Drizzle Studio and API still returning strings.
  • Interactive checklists on read surfaces require HTML-aware toggle logic (not trivial CRUD) — budget extra implementation time in task 85.
  • Sanitization is security-critical; must test XSS vectors and keep allowlist in sync with TipTap extensions.

Out of scope (ADR)

  • Journal module UI (task 86) — consumes shared component after 85 lands.
  • Collaborative editing, comments, version history.

References

  • Task brief: docs/tasks/85-rich-text-notes.md
  • Gitea: #7 epic, #8 research child
  • Current notes editor: src/modules/notes/components/note-editor.tsx
  • Uploads: src/app/api/uploads/route.ts