5.9 KiB
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— controlledvalue/onChange(HTML string), optionaleditable, toolbar configRichTextContent— read-only renderer with sanitization and overflow-safe CSSRichTextChecklistbehavior — 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.bodyremains atextcolumn; 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/notescontinues to accept/returnbodyas 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/:idthat updates only the task-itemcheckedstate inside the HTML (parse → flipdata-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.