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:
@@ -7,6 +7,7 @@ import type {
|
||||
Character,
|
||||
Entity,
|
||||
Note,
|
||||
PlotBeat,
|
||||
Project,
|
||||
ProjectMeta,
|
||||
User,
|
||||
@@ -69,6 +70,21 @@ export interface BoardRow {
|
||||
edges: string;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
export interface PlotBeatRow {
|
||||
id: string;
|
||||
project_id: string;
|
||||
title: string;
|
||||
body: string;
|
||||
act: number;
|
||||
type: string;
|
||||
beat_order: number;
|
||||
status: string;
|
||||
chapter_id: string | null;
|
||||
entity_ids: string;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
export interface AISessionRow {
|
||||
id: string;
|
||||
project_id: string;
|
||||
@@ -160,6 +176,34 @@ export function toBoard(r: BoardRow): Board {
|
||||
};
|
||||
}
|
||||
|
||||
export function toPlotBeat(r: PlotBeatRow): PlotBeat {
|
||||
return {
|
||||
id: r.id,
|
||||
projectId: r.project_id,
|
||||
title: r.title,
|
||||
body: r.body,
|
||||
act: r.act,
|
||||
type: r.type as PlotBeat["type"],
|
||||
order: r.beat_order,
|
||||
status: (r.status as PlotBeat["status"]) ?? "draft",
|
||||
chapterId: r.chapter_id ?? undefined,
|
||||
entityIds: parseEntityIds(r.entity_ids),
|
||||
createdAt: r.created_at,
|
||||
updatedAt: r.updated_at,
|
||||
};
|
||||
}
|
||||
|
||||
/** Parse the JSON `entity_ids` column into a string[] (empty when blank). */
|
||||
function parseEntityIds(raw: string): string[] {
|
||||
if (!raw) return [];
|
||||
try {
|
||||
const parsed = JSON.parse(raw);
|
||||
return Array.isArray(parsed) ? parsed.filter((id): id is string => typeof id === "string") : [];
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
export function toAISession(r: AISessionRow): AISession {
|
||||
return {
|
||||
id: r.id,
|
||||
|
||||
Reference in New Issue
Block a user