SPEC-STV-04-Editor-System
SPEC-STV-04 · Spec header. Spec ID: SPEC-STV-04 · Title: Editor System & Block Types · Version: 1.0.0 · Status: Planned · Authority: Specification · Priority: P0 · Owner role: Frontend lead · Reviewers: Backend architect, Product architect · Last reviewed: 2026-05-11 · Sync targets: resources/js/editor/**, app/Services/Blocks/**, docs/EDITOR_SYSTEM.md, docs/BLOCK_TYPES.md · Depends on: SPEC-STV-HUB, SPEC-STV-02 · Consumed by: SPEC-STV-05, -06, -08, -10 · Conflict rule: Hub wins. · Change policy: Frontend lead + Backend architect; Registry bump on type-set change.
Mobile editor behavior → see SPEC-STV-13. This spec (SPEC-STV-04) is canonical for the block model, the 28 types, RichText, slash menu, floating toolbar, drag handle, autosave protocol, version history events, desktop keyboard shortcuts, and sanitization. The detailed mobile editor surface — mobile gestures, text-selection toolbar, AI select-to-edit, AI preview bottom sheet, mobile slash drawer, mobile toolbar, page-tree drawer, haptics, accessibility, and gesture conflict rules — lives in SPEC-STV-13 · Mobile Gestures & Text Selection Editor → SPEC-STV-13. When SPEC-STV-04 §9 (mobile mode) and SPEC-STV-13 overlap, SPEC-STV-13 wins on mobile-only concerns.
1 · Block model
Every block lives in page_blocks with { page_id, parent_block_id?, type, content JSON, metadata JSON?, position decimal(20,10) }. Children blocks point at a parent_block_id (used by toggle, callout, columns). position uses fractional ordering so an insert between A and B is one UPDATE.
2 · The 28 block types (canonical)
| # | type | content JSON shape | Renders as |
|---|---|---|---|
| 1 | paragraph | { text: RichText[] } | Inline rich text. |
| 2 | heading_1 | { text: RichText[] } | H1. |
| 3 | heading_2 | { text: RichText[] } | H2. |
| 4 | heading_3 | { text: RichText[] } | H3. |
| 5 | bullet_list_item | { text: RichText[] } | Bulleted list item; nesting via parent_block_id. |
| 6 | numbered_list_item | { text: RichText[] } | Numbered list item. |
| 7 | checklist_item | { text: RichText[], checked: bool } | Task checkbox. |
| 8 | quote | { text: RichText[] } | Blockquote. |
| 9 | callout | { text: RichText[], icon: string, color: enum } | Container; children allowed. |
| 10 | divider | {} | Horizontal rule. |
| 11 | code | { text: string, language: string, caption?: string, wrap: bool } | Monaco-style block. |
| 12 | image | { file_uuid: string, caption?: RichText[], width_pct?: number, align?: enum } | Signed URL fetched at render. |
| 13 | video_embed | { url: string, provider: "youtube|vimeo|loom|other", caption?: RichText[] } | oEmbed. |
| 14 | file_attachment | { file_uuid: string, display: "card|inline" } | Download via signed URL. |
| 15 | pdf | { file_uuid: string, page?: number } | Inline PDF.js viewer. |
| 16 | table | { rows: number, cols: number, cells: [[RichText[]]] } | Static table (≠ database view). |
| 17 | database_view | { database_id: number, view_id: number, embed: "inline|linked" } | Embeds a documentation_database. |
| 18 | kanban_view | { database_id, view_id } | Sugar for database_view of type board. |
| 19 | calendar_view | { database_id, view_id } | Sugar for database_view of type calendar. |
| 20 | gallery_view | { database_id, view_id } | Sugar for database_view of type gallery. |
| 21 | toggle | { text: RichText[], expanded: bool } | Container; children allowed. |
| 22 | synced_block | { source_block_id: number } | Renders source content; edits propagate. |
| 23 | ai_generated | { prompt: string, output_text: string, model: string, accepted_at: datetime } | Marked block; never auto-edits. |
| 24 | button | { label: string, action: "new_subpage|run_template|copy_link|external_url", config: object } | Configurable action button. |
| 25 | link_preview | { url: string, oembed: object } | Unfurled preview. |
| 26 | breadcrumb | {} | Renders page path. |
| 27 | table_of_contents | { depth_max: number } | Renders headings on this page. |
| 28 | columns | { widths: number[] } | Container with N child columns; each column groups children. |
3 · RichText spec
RichText[] is an array of runs: { text: string, marks?: ["bold"|"italic"|"underline"|"strike"|"code"|"link"], link?: { url, title? }, mention?: { type: "user|page|date", id_or_value } }.
4 · Slash menu
Opens on / at the start or inside a paragraph. Filterable. Categories: Basic (paragraph, headings, lists, quote, divider, code), Media (image, video, file, PDF), Containers (callout, toggle, columns), Database (table, database_view + sugars), Advanced (synced, AI, button, link preview, breadcrumb, TOC). Recently used shown first. Mobile: long-press the + handle.
5 · Floating toolbar
Appears on text selection. Buttons: B, I, U, S, code, link, color, mention, AI (opens selection editor — SPEC-STV-08), convert-to (paragraph → heading → list → quote → callout). Toolbar hides on touch-drag.
6 · Drag handle
⋮⋮ handle appears on hover (desktop) and on long-press (mobile). Drop targets show a hairline. Drop emits one POST /blocks/{id}/move with the new fractional position. Cross-page drag = move + page version event on both pages.
7 · Autosave protocol
- Every change in the editor queues a delta.
- A debounced flush (500 ms idle, 5 s max) calls
POST /blocks/batch.
- On success:
last_saved_attimestamp updates and a discreet indicator shows. On 4xx/5xx: retry with backoff up to 3 times; persist tolocalStorageas a draft.
- A Reverb broadcast on
private-page.{id}echoesblock.updatedto other open clients so they invalidate caches (no live cursors v1).
8 · Version history protocol (event-based)
Every accepted write emits a page_versions row with op and a compact payload:
block_add→{ block_id, type, content, position, parent_block_id }
block_update→{ block_id, before: { content? metadata? }, after: {...} }
block_delete→{ block_id, before }
move→{ block_id, before: { position, parent_block_id }, after: {...} }
title→{ before, after }
restore→{ from_version_number }
A full snapshot_hash is taken every N events (configurable, default 50) and on demand. Restoring rebuilds the document by replaying events to the target version.
9 · Mobile vs desktop editor modes
- Desktop — slash menu, drag handle, floating toolbar, multi-column layout, keyboard shortcuts (see §10).
- Mobile — bottom action bar (toolbar collapsed to icons),
+button to insert below, swipe-to-indent for list items, no columns (columns collapse to stacked blocks).
10 · Keyboard shortcuts (desktop)
Cmd/Ctrl + B/I/U formatting · Cmd/Ctrl + K global search · / slash menu · Tab / Shift+Tab indent/outdent list · Cmd/Ctrl + Enter toggle checkbox · Cmd/Ctrl + D duplicate block · Cmd/Ctrl + Shift + ↑/↓ move block · Cmd/Ctrl + Z / Shift+Z undo / redo (client-side stack; persisted as version events).
11 · Validation
Sanitizer::block($content) runs server-side before save:
- Strip all HTML except a whitelist of inline tags allowed inside
code-like blocks (none — code text is plain).
- Validate
typeagainstconfig('documentation.block_types').
- Validate
contentJSON against per-type schema.
- Enforce
contentsize ≤ 256 KiB per block.