90 lines
5.9 KiB
Markdown
90 lines
5.9 KiB
Markdown
# 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](https://gitea.ginnoir.com/ginnoir/famapp/issues/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](https://gitea.ginnoir.com/ginnoir/famapp/issues/7) epic, [#8](https://gitea.ginnoir.com/ginnoir/famapp/issues/8) research child
|
|
- Current notes editor: `src/modules/notes/components/note-editor.tsx`
|
|
- Uploads: `src/app/api/uploads/route.ts`
|