A collaborative knowledge workspace powered by Lexical and tldraw.
- Rich document editing via Lexical projected from a canonical
PageContentModel - Realtime document collaboration via Yjs + Hocuspocus with
y-indexeddboffline cache - Freeform canvas mode via tldraw with local runtime plus canonical snapshot checkpoints
- Generic sidebar collections with stored category metadata and page membership
- Editable workspace branding for the shell title/subtitle plus document/canvas labels and badges
- Seeded generic workspace collections:
- Notes · Research · People · Spaces · Projects
- Create, rename, and remove collections plus create new pages in any collection
- Sync-aware document status UI for connection, sync, offline cache, and participant count
- Two built-in themes with an instant switcher in the sidebar footer
- Lucide icons throughout — no emoji
This repo is partway through the broader "general knowledge workspace" plan.
- Implemented today: generic shell vocabulary, seeded non-TTRPG collections, workspace/mode branding, category metadata persistence, a normalized shared page-content schema, stable document block identities, canonical document reconciliation, and active Lexical collaboration over Yjs/Hocuspocus.
- Transitional today: canvas mode still uses tldraw's own local persistence keyed by page id, but now mirrors canonical snapshot checkpoints into
PageContentModel; workspace shell metadata remains local-only in browser storage. - Not implemented yet: asset library UI/storage flows, PDF rendering/embed modes, pinned block surfaces, mechanics-aware blocks, faceted retrieval UX, and a fully shared canvas runtime over the same block graph.
| Theme | Description |
|---|---|
| LANCER (default) | Sci-fi mecha aesthetic — amber/cyan accents, monospace terminal fonts, deep blue-black backgrounds |
| Dark Fantasy | High-fantasy aesthetic — gold accents, serif brand font, deep purple-black backgrounds |
The theme contract now has two layers:
- Flavor tokens inside each
[data-theme="..."]block insrc/themes/themes.css - Semantic tokens defined once in
:rootand consumed everywhere else
The semantic contract is the source of truth for custom UI:
--theme-app-background--theme-panel--theme-surface-elevated--theme-border--theme-text--theme-text-muted--theme-accent--theme-accent-muted--theme-danger--theme-focus-ring--theme-radius--theme-shadow-elevated
Those tokens are used by:
- the app shell styles in
src/App.css - the shared primitives in
src/primitives/primitives.css - Lexical document surfaces in
src/components/LexicalDocumentEditor.tsx/src/App.css - tldraw theme overrides in
src/components/Editor.tsx/src/App.css
Adding a new theme requires only two steps:
- Add a
[data-theme="my-theme"]block tosrc/themes/themes.cssmapping the flavor tokens - Add a
{ id, label, shortLabel, appearance }entry to theTHEMESarray insrc/themes/index.ts
Custom UI behavior and accessibility now go through a small internal primitives layer in src/primitives/:
Button/IconButtonToggleGroupCollapsibleDropdownMenuAlertDialogInputTooltip
Feature components should prefer composing these primitives rather than using Radix directly. This keeps focus, hover, active, disabled, and overlay states aligned with the shared semantic theme tokens.
The app defines PageContentModel as the canonical shared page-content schema for page identity, page metadata, blocks, entities, relations, and assets.
- Page metadata: stable page identity, title, mode, category membership, ordering metadata, tags, pinning, grouping, custom fields, and asset references
- Blocks: meaningful document/content units that drag-and-drop editing can reorder, annotate, pin, and attach asset/mechanics metadata to
- Entities: named records referenced by blocks or canvas objects
- Relations: stable references between blocks and/or entities
- Assets: renderer-agnostic file/image/PDF records that can be referenced from pages and blocks
This shared model lives in src/lib/document-pages.ts, is migrated through src/lib/page-content-migrations.ts, and is projected into Lexical by src/lib/document-editor.ts.
- Lexical uses Yjs/Hocuspocus as the collaborative runtime, but
PageContentModelremains the canonical persisted representation for document pages. - Top-level document blocks now keep stable non-positional ids, and matched blocks preserve metadata, entity ids, and surviving relations across normal edits.
- Canvas still uses tldraw's own persisted state keyed by page id, but canvas pages now mirror a canonical tldraw snapshot block into
PageContentModelas a checkpoint bridge. - Categories remain one organizational projection, and their presentation metadata is persisted on each category record instead of being inferred from category ids in generic UI helpers.
src/lib/block-registry.tsdefines the initial renderer-agnostic block registry seam for Lexical nodes and markdown behavior, with future hooks for canvas projection and richer block metadata.
See also:
docs/architecture-sync.mdfor the sync contract, authority ladder, and migration policydocs/development-sync.mdfor operational workflows and extension guidance
npm install
npm run hocuspocus:dev
npm run devOpen http://localhost:5173 in your browser.
The editor connects to ws://127.0.0.1:1234 by default.
Environment variables:
HOCUSPOCUS_HOSTandHOCUSPOCUS_PORTconfigure the local collaboration serverHOCUSPOCUS_DB_PATHoverrides the SQLite file used by the local collaboration serverHOCUSPOCUS_TOKENenables simple token auth on the serverVITE_HOCUSPOCUS_URLpoints the web app at a different Hocuspocus endpointVITE_HOCUSPOCUS_TOKENsends the matching client token when auth is enabled
This repository is initialized for OpenSpec with GitHub Copilot prompt/skill files in .github/ and baseline specs in openspec/specs/.
Recommended local setup:
npm install -g @fission-ai/openspec@latest
openspec updateOnce installed, restart your IDE so the generated GitHub Copilot slash commands are available:
/opsx:propose— create a change proposal and planning artifacts/opsx:explore— investigate ideas before opening a change/opsx:apply— implement the tasks for an existing change/opsx:archive— merge/archive a completed change
Project spec artifacts live in:
openspec/specs/— current source-of-truth behavior specsopenspec/changes/— proposed changes and implementation plans
| Layer | Technology |
|---|---|
| Document editor | Lexical + Markdown + Yjs collaboration |
| Canvas | tldraw |
| Data / CRDT | Yjs + Hocuspocus + y-indexeddb |
| Icons | lucide-react |
| UI framework | React 18 + TypeScript |
| Build tool | Vite 5 |
The document editor uses Yjs as the CRDT model, synchronizes through
Hocuspocus, persists shared state in a local SQLite database, and keeps a local y-indexeddb cache for
offline continuity.
Authority for collaborative document content is:
- active remote room state from Hocuspocus
- local Yjs IndexedDB cache when offline
- canonical
PageContentModelsnapshots for first-open seeding and recovery - local browser storage is never allowed to silently overwrite newer collaborative room state
Room seeding rules:
- if a room already has Yjs content, the editor uses that content
- if a room is empty and canonical page content exists, Lexical seeds the room once
- a room seed marker is stored in the Yjs document so reopening the same empty room does not reseed repeatedly
- remote updates flowing through Yjs update the canonical
PageContentModelthrough the Lexical projection
Metadata semantics today:
- Collaborative/shared in realtime: document body blocks for document-mode pages
- Eventually consistent local snapshot: canonical
PageContentModelpersisted in the docs store - Local-only today: workspace title/subtitle, workspace mode labels, category presentation metadata, and the live tldraw runtime state
- Canonical checkpoint today: canvas pages mirror a tldraw snapshot into
PageContentModel, which is used for seeding and mode-switch preservation but is not yet realtime collaborative
For local development, run the collaboration server and Vite app in separate terminals:
npm run hocuspocus:dev
npm run devEach document page uses its page id as the Hocuspocus room name when a collaboration session is created.
The default local SQLite database is stored at .data/hocuspocus.sqlite.