- Fix PATCH /api/projects/:id/shares/:userId: query SELECT 1 instead of
SELECT id (project_shares has a composite PK, no id column)
- Add audit logging to existing share grant/revoke routes
- Add POST /api/projects/:id/transfer for ownership handoff
- Remove inline admin user/audit routes from routes.ts (now in admin.ts)
- Remove unused requireAdmin import and userPatch schema from routes.ts
Offline spellcheck via hunspell compiled to WASM (runs fully in-browser),
plus optional self-hostable LanguageTool grammar/style checking.
- hunspell-wasm spell engine with bundled en_US dictionary (base64-decoded
browser-safe via atob + TextDecoder)
- LanguageTool engine (self-hostable, docker compose snippet under
docker/languagetool/)
- single decoration pipeline: spell + grammar hits merge into one squiggle
layer (red=spelling, blue=grammar) with a side panel to jump/apply/ignore
and add-to-dictionary
- useCheckService hook wired into WritingView; check.css loaded in main.tsx
- spell + grammar unit tests (30 editor tests green)
PLAN.md and CHUNK#7.md updated to reflect completion.
The bundled en_US Hunspell dictionary was decoded with Node's Buffer,
which is undefined in the browser and crashed React on app load
(Blank UI). Switch to atob() + TextDecoder, available in both the
browser and Node 18+, so the dictionary imports cleanly in Vite.
Fixes the blank UI reported after dev server startup.
Wire real spell-checking into the editor via hunspell-wasm (bundled en_US
dictionary) and an optional LanguageTool grammar client.
- spell/: HunspellSpellEngine wrapper, pure spell logic, base64 en_US dict
- grammar/: LanguageToolEngine grammar client and pure grammar logic
- decorations/: collectBlock text assembly, offset-mapped CheckService,
buildCheckDecorations (flat DecorationSet, inline spell/grammar marks)
- export engines and types from editor index; add hunspell-wasm dep
Verified: typecheck (6 pkgs), tests (all pass), lint (6 pkgs), and web
build all green.
Introduce a full-screen brand splash shown while the project store is
loading (status !== ready). Uses a new vertical logo lockup (P tile /
PubScript wordmark / S tile) matching the existing typewriter-tile
design, centered on the active theme background with a subtle
fade/appear animation and a graceful fade-out once the app is ready.
- apps/web/public/logo-lockup.svg: vertical lockup variant of the mark
- SplashScreen.tsx: overlay driven by the store's load status
- splash.css: full-screen overlay + keyframe animations
- wire into App.tsx and import splash.css in main.tsx
Recreate the gold-rimmed 'PS' typewriter-tile emblem as scalable SVGs:
- public/logo.svg — dark-ground emblem used as the app favicon
- public/logo-mark.svg — transparent mark placed in the Topbar beside
the page title, with a subtle hover scale
Add <link rel=icon> to index.html and a .ps-topbar-logo rule in
AppShell.css.
Add the @pubscript/exporters package exporting a flattened ProseMirror
project to Markdown, HTML, EPUB, DOCX, ODT and PDF, plus a deterministic
generatedAt timestamp threaded through every format (ZIP entry dates,
PDF /ID, DOCX dc:created/modified, EPUB id, HTML/XML footers) so golden
files are byte-stable.
Wire an ExportDialog into the web app (Topbar trigger, App route) with
format selection, theme pick, and download helpers.
Add round-trip and golden-file determinism tests (22) covering
non-empty output, byte-identical re-exports, Markdown round-trip through
the editor, and EPUB/ODT ZIP + mimetype + XML validation.
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.
- Consistency tab in EntitiesView surfacing findInconsistencies() results
- ContextRail + MentionPopover wired into WritingView for entity context/hover
- CommandPalette indexes entities and chapters for cross-entity search
- link field type for entity references in character fields
- findDeadCharacterMentions consistency check + tests
- Expand seed with a location, note, and a natural Elias Vell mention so the
dead-character-after-death case is actually exercised
- Author docs/DATA_MODEL.md documenting core schemas, types, storage contract
- Rename CharacterMention id attr -> entityId to match core backlinks/consistency
The App.tsx wrapped Topbar + main in a flex column div with height:100%,
which the .ps-app CSS grid auto-placed into the 48px topbar cell, leaving
the main grid area with zero height and a blank write view. Removed the
wrapper so Topbar (grid-area: topbar) and main (grid-area: main) are direct
grid children and receive their proper grid cells.
WritingView passed chapter.doc.content (a bare ProseMirror node array)
as the editor content, which made EditorState.create build a doc node
with an undefined type, throwing 'Cannot read properties of undefined
(reading schema)' and unmounting the write view. Pass the full doc
object ({ type: 'doc', content }) on init and on setContent sync.
Add editor regression tests covering schema instantiation, node
registration, and doc-content round-trip.
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.)
The entry point defined and exported start() but never called it, so
running 'node --import tsx src/index.ts' imported the module and exited
0 without ever calling app.listen. Add a top-level start().catch() so the
server binds and logs its listen addresses.
The TipTap node definitions imported their node-name constants from
schema.ts, while schema.ts imported the nodes to build the schema. This
circular dependency evaluated the nodes before the constants were
assigned, leaving sceneBreak/footnote/footnoteArea/characterMention with
name 'undefined' — so they never registered in PubScriptSchema. As a
result <hr class="scene-break"> parsed as StarterKit's horizontalRule
instead of a scene break.
- Move node-name constants, groups, MentionAttrs, and colorAttribute into
a new constants.ts module that both schema.ts and the node files import
from, breaking the cycle.
- Remove the redundant separate History extension (already in StarterKit),
which also silenced a TipTap 'Duplicate extension names: undoRedo' warn.
- Drop unused TipTap extension and prosemirror-* deps (all access goes
through @tiptap/pm).