✏️

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 EditorSPEC-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)

#typecontent JSON shapeRenders as
1paragraph{ text: RichText[] }Inline rich text.
2heading_1{ text: RichText[] }H1.
3heading_2{ text: RichText[] }H2.
4heading_3{ text: RichText[] }H3.
5bullet_list_item{ text: RichText[] }Bulleted list item; nesting via parent_block_id.
6numbered_list_item{ text: RichText[] }Numbered list item.
7checklist_item{ text: RichText[], checked: bool }Task checkbox.
8quote{ text: RichText[] }Blockquote.
9callout{ text: RichText[], icon: string, color: enum }Container; children allowed.
10divider{}Horizontal rule.
11code{ text: string, language: string, caption?: string, wrap: bool }Monaco-style block.
12image{ file_uuid: string, caption?: RichText[], width_pct?: number, align?: enum }Signed URL fetched at render.
13video_embed{ url: string, provider: "youtube|vimeo|loom|other", caption?: RichText[] }oEmbed.
14file_attachment{ file_uuid: string, display: "card|inline" }Download via signed URL.
15pdf{ file_uuid: string, page?: number }Inline PDF.js viewer.
16table{ rows: number, cols: number, cells: [[RichText[]]] }Static table (≠ database view).
17database_view{ database_id: number, view_id: number, embed: "inline|linked" }Embeds a documentation_database.
18kanban_view{ database_id, view_id }Sugar for database_view of type board.
19calendar_view{ database_id, view_id }Sugar for database_view of type calendar.
20gallery_view{ database_id, view_id }Sugar for database_view of type gallery.
21toggle{ text: RichText[], expanded: bool }Container; children allowed.
22synced_block{ source_block_id: number }Renders source content; edits propagate.
23ai_generated{ prompt: string, output_text: string, model: string, accepted_at: datetime }Marked block; never auto-edits.
24button{ label: string, action: "new_subpage|run_template|copy_link|external_url", config: object }Configurable action button.
25link_preview{ url: string, oembed: object }Unfurled preview.
26breadcrumb{}Renders page path.
27table_of_contents{ depth_max: number }Renders headings on this page.
28columns{ 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_at timestamp updates and a discreet indicator shows. On 4xx/5xx: retry with backoff up to 3 times; persist to localStorage as a draft.
  • A Reverb broadcast on private-page.{id} echoes block.updated to 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 type against config('documentation.block_types').
  • Validate content JSON against per-type schema.
  • Enforce content size ≤ 256 KiB per block.