From bb68d9d5d2803b78c2f826c9b3d7730d33e27e63 Mon Sep 17 00:00:00 2001 From: Schwitzz Date: Sun, 25 Oct 2026 13:57:07 -0500 Subject: [PATCH] =?UTF-8?q?CHUNK=20#3:=20Writing=20Surface=20=E2=80=94=20e?= =?UTF-8?q?ditor=20wiring,=20storage=20view,=20polish?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Complete the Writing Surface chunk: wire the web writing view to the offline-first storage adapter, finish the editor package exports, and apply Step 4 polish (SVG monogram logo, real project in the sidebar, font settings → CSS variables). Editor package - Re-export EditorContent + Editor type from @pubscript/editor so the web app never imports TipTap directly. Web writing view (apps/web) - project-store (zustand) over storageLocal: project, chapters, active chapter, autosave via createAutosave, idle detection via createIdleDetector. - Client-side seed mirroring the server seed ("The Glass Harbor"). - WritingView component: editor + autosave label + word/scene count + chapter nav; wire the /write route inside AppShell. - Import editor.css in main.tsx. Core storage contract - Extend StorageAdapter with createBook/listBooks/listChapters; implement in local + server adapters; update contract test mock. Polish - Sidebar: SVG monogram logo + live project title/word count instead of the placeholder "The Silent Cartographer". - Fonts: applyFontSettings() in the theme engine writes --ps-font-* variables; .ps-content reads --ps-font-size/leading/measure; settings store applies fontSize on boot and change. Docs - PLAN.md: CHUNK #3 → in progress. - CHUNK#3_PROGRESS.md: steps 1–3 done, step 4 polish in progress. Verified: full workspace typecheck clean, lint clean, core 24 / ui 5 / editor 7 tests pass, web build clean. (server vitest hits EADDRINUSE on 8080 — pre-existing Docker WordPress port conflict, unrelated.) --- CHUNK#3_PROGRESS.md | 31 ++++ PLAN.md | 2 +- apps/web/package.json | 1 + apps/web/src/App.tsx | 2 +- apps/web/src/components/Sidebar.tsx | 41 ++++- apps/web/src/components/WritingView.tsx | 139 +++++++++++++++++ apps/web/src/main.tsx | 2 + apps/web/src/routes/Write.tsx | 71 ++------- apps/web/src/store/project-store.ts | 104 +++++++++++++ apps/web/src/store/seed.ts | 199 ++++++++++++++++++++++++ apps/web/src/store/settings.ts | 7 +- apps/web/src/styles/writing.css | 161 +++++++++++++++++++ packages/core/src/autosave.test.ts | 9 ++ packages/core/src/storage-local.ts | 20 +++ packages/core/src/storage-server.ts | 16 ++ packages/core/src/storage.ts | 6 + packages/editor/src/Editor.tsx | 10 +- packages/editor/src/index.ts | 5 + packages/ui/src/AppShell.css | 13 +- packages/ui/src/theme.ts | 36 +++++ pnpm-lock.yaml | 3 + 21 files changed, 804 insertions(+), 74 deletions(-) create mode 100644 CHUNK#3_PROGRESS.md create mode 100644 apps/web/src/components/WritingView.tsx create mode 100644 apps/web/src/store/project-store.ts create mode 100644 apps/web/src/store/seed.ts create mode 100644 apps/web/src/styles/writing.css diff --git a/CHUNK#3_PROGRESS.md b/CHUNK#3_PROGRESS.md new file mode 100644 index 0000000..feb7bff --- /dev/null +++ b/CHUNK#3_PROGRESS.md @@ -0,0 +1,31 @@ +# CHUNK #3 — Editor & Writing View + +**Status:** In progress. Steps 1–3 committed. Step 4 (polish) in progress. + +## Progress +- [x] Step 1: Editor package (TipTap 3 schema, wrapper, markdown, key bindings) — committed +- [x] Step 2: Writing view in apps/web wired to storage adapter + - [x] Add @pubscript/editor to web deps + - [x] project-store (zustand) over storageLocal + - [x] client-side seed for local adapter + - [x] WritingView component (editor + autosave + word/scene count + chapter nav) + - [x] wire Write route +- [x] Step 3: Performance (autosave via createAutosave, idle detection via createIdleDetector) +- [ ] Step 4: Polish (editor.css import ✅, fonts 🟡, SVG monogram logo ✅) + - [x] editor.css import in main.tsx + - [x] SVG monogram logo in Sidebar + - [x] Sidebar reflects the real seeded project ("The Glass Harbor") + live word count + - [ ] font family/size/leading/measure controls wired to CSS variables (size only so far) + +## Key facts +- Editor package exports: usePubScriptEditor, createEditorInstance, emptyDecorationSource, throttle, createIdleDetector, serializeToMarkdown, parseMarkdown, node types + constants. +- PubScriptEditor React component re-exports EditorContent + Editor type from @pubscript/editor (web never imports TipTap directly). +- storageLocal (offline-first, IndexedDB) chosen for web wiring (server routes require auth). +- Demo prose lives in server/src/seed.ts ("The Glass Harbor", 3 chapters, 4 chars, 1 world, 1 board). +- Web app uses @pubscript/core/local (storageLocal) — no server needed to run offline. +- Server runs on PORT (default 8080; set PORT=8081 when 8080 is taken by Docker WordPress). + +## Remaining (finish Step 4) +- Wire font family/leading/measure settings to CSS variables (fontSize already wired via applyFontSettings). +- Add a settings UI control for font size (or confirm existing one applies). +- Final polish pass: focus mode visual, empty-state copy, a11y labels. diff --git a/PLAN.md b/PLAN.md index ec0a1f1..93fb61b 100644 --- a/PLAN.md +++ b/PLAN.md @@ -269,7 +269,7 @@ Each chunk is a 1–3 day work order in `CHUNK#n.md`, written so a human *or* a |---|---|---|---|---| | 1 | **Scaffold & Foundations** | 1 | ✅ Done | Monorepo, strict TS, tooling, app shell, theme system, CI, settings store | | 2 | **Data Model, Storage & Auth** | 3 | ⬜ Not started | Full domain types, StorageAdapter, Local + Server adapters, Fastify auth/multi-tenant, autosave, contract tests | -| 3 | **Writing Surface** | 3 | ⬜ Not started | TipTap editor, document tree, fonts, focus mode, Markdown I/O, performance | +| 3 | **Writing Surface** | 3 | 🟡 In progress | TipTap editor ✅, document tree ✅, focus mode ✅, Markdown I/O ✅, autosave ✅, fonts/logo polish 🟡 | | 4 | **Characters & World-Building** | 2 | ⬜ Not started | Entity schema, character sheets, world items, search, mentions, backlinks | | 5 | **Plot & Whiteboard** | 3 | ⬜ Not started | Beat board + templates, timeline, React Flow whiteboard (200-node perf) | | 6 | **Export Pipeline** | 3 | ⬜ Not started | MD/HTML/EPUB/DOCX/ODT + PDF, export dialog, CI validation | diff --git a/apps/web/package.json b/apps/web/package.json index 0a4a138..cddcf21 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -13,6 +13,7 @@ }, "dependencies": { "@pubscript/core": "workspace:*", + "@pubscript/editor": "workspace:*", "@pubscript/ui": "workspace:*", "react": "^19.0.0", "react-dom": "^19.0.0", diff --git a/apps/web/src/App.tsx b/apps/web/src/App.tsx index f34f316..393803a 100644 --- a/apps/web/src/App.tsx +++ b/apps/web/src/App.tsx @@ -2,7 +2,7 @@ import { useState, useEffect } from "react"; import { Routes, Route } from "react-router-dom"; import { Sidebar, Topbar, CommandPalette } from "./components/index.js"; import { ToastViewport } from "@pubscript/ui"; -import { Write } from "./routes/Write.js"; +import Write from "./routes/Write.js"; import { Characters } from "./routes/Characters.js"; import { Plot } from "./routes/Plot.js"; import { Whiteboard } from "./routes/Whiteboard.js"; diff --git a/apps/web/src/components/Sidebar.tsx b/apps/web/src/components/Sidebar.tsx index 0ad9836..ce6fad9 100644 --- a/apps/web/src/components/Sidebar.tsx +++ b/apps/web/src/components/Sidebar.tsx @@ -1,4 +1,5 @@ import { NavLink } from "react-router-dom"; +import { useProjectStore } from "../store/project-store.js"; export interface NavItem { to: string; @@ -11,19 +12,51 @@ export interface SidebarProps { nav: NavItem[]; } +/** A minimal view of a chapter document for word counting. */ +interface ChapterDoc { + doc: { content?: Array<{ text?: string }> }; +} + +/** Total word count across every chapter document in the active project. */ +function totalWords(chapters: ChapterDoc[]): number { + let total = 0; + for (const chapter of chapters) { + for (const node of chapter.doc.content ?? []) { + if (node.text) total += node.text.trim().split(/\s+/).filter(Boolean).length; + } + } + return total; +} + export function Sidebar({ nav }: SidebarProps) { + const project = useProjectStore((s) => s.project); + const chapters = useProjectStore((s) => s.chapters); + const title = project?.title ?? "Untitled project"; + const wordCount = totalWords(chapters); + return (