feat(plot): Chunk #5 story whiteboard, timeline & plot beats

Story whiteboard built on React Flow v11: free-form canvas of typed
nodes (character, world, chapter, note, image, group) with draggable
leads-to/contrasts/causes edges, cross-link selectors (chapter/entity),
linked-node badges, and an "Add beats to board" import from the plot.

Plot beats feature set:
- Status/locking (draft/revised/locked) with a progress bar
- Beat templates (three-act, save-the-cat, seven-point, custom)
- Timeline route with search/chapter/character filters
- Entity cross-links (entityIds) on beats and board nodes
- getBoard added to the StorageAdapter contract (local + server) and
  REST endpoints; loadBoard now restores persisted board state

Storage & server:
- PlotBeat type (status, entityIds) across core types, Dexie schema,
  and server mapping/routes/migration (002_plot_beats.sql)
- SQLite-safe column names (beat_order, entity_ids)
- Server seed data for plot beats

UI: accent Button variant, Badge variants, plot/timeline/whiteboard
stylesheets. Tests: templates module, plot-beat store round-trip,
board getBoard. typecheck, lint, test, and build all green.
This commit is contained in:
Schwitzz
2026-10-26 19:00:07 -05:00
parent 301e842c5c
commit 45f5c505c9
28 changed files with 3488 additions and 52 deletions
+19
View File
@@ -0,0 +1,19 @@
-- 002_plot_beats.sql — plot beats for the Plot board and Timeline (CHUNK #5).
CREATE TABLE IF NOT EXISTS plot_beats (
id TEXT NOT NULL,
project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
title TEXT NOT NULL DEFAULT '',
body TEXT NOT NULL DEFAULT '',
act INTEGER NOT NULL DEFAULT 1,
type TEXT NOT NULL DEFAULT 'note',
beat_order INTEGER NOT NULL DEFAULT 0,
status TEXT NOT NULL DEFAULT 'draft',
chapter_id TEXT,
entity_ids TEXT NOT NULL DEFAULT '[]',
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL,
PRIMARY KEY (project_id, id)
);
CREATE INDEX IF NOT EXISTS idx_plot_beats_project ON plot_beats(project_id);
CREATE INDEX IF NOT EXISTS idx_plot_beats_act ON plot_beats(project_id, act, beat_order);