A full-stack-less collaborative editing playground built with React, TypeScript, shadcn/ui, and Tailwind CSS.
This project demonstrates how a character-level CRDT can keep two editor replicas consistent while supporting:
- automatic live merge (Google Docs style) when connected
- simulated network partitions (isolate one editor)
- manual one-way sync/push between replicas
- transparent internals (version vector, CRDT item table, action timeline)
- Two independent editor replicas (
Editor A,Editor B) powered byCRDTDocument - Automatic merge when both replicas are connected
- Per-editor isolate/reconnect controls
- Pending operation counters (
A -> B,B -> A) - Manual controls:
- push
A -> B - push
B -> A - two-way sync
- reset demo
- push
- Under-the-hood panel per editor:
- version vector
- ordered CRDT item list with tombstones
- Action timeline with local/network/sync event types
- React 19
- TypeScript 6
- Vite 8
- Tailwind CSS v4
- shadcn/ui (Radix Vega style)
- Radix primitives (via
radix-uipackage) - Lucide icons
.
├── components.json
├── src
│ ├── App.tsx
│ ├── index.css
│ ├── main.tsx
│ ├── components
│ │ ├── CRDTDemo.tsx
│ │ └── ui
│ │ ├── accordion.tsx
│ │ ├── badge.tsx
│ │ ├── button.tsx
│ │ ├── card.tsx
│ │ ├── scroll-area.tsx
│ │ ├── separator.tsx
│ │ ├── table.tsx
│ │ └── textarea.tsx
│ ├── lib
│ │ └── utils.ts
│ └── utils
│ └── crdt.ts
├── tsconfig.json
├── tsconfig.app.json
├── tsconfig.node.json
└── vite.config.ts
- Node.js 20+ (recommended)
- npm (or bun)
npm installnpm run devOpen the local URL shown by Vite.
npm run buildnpm run lintnpm run preview| Command | Description |
|---|---|
npm run dev |
Start Vite dev server |
npm run build |
Type-check (tsc -b) + production build |
npm run lint |
Run ESLint |
npm run preview |
Preview built app |
This repository is already configured, but if you need to reproduce setup in a fresh Vite app, run:
# 1) Initialize shadcn in a Vite + Radix project
npx shadcn@latest init -t vite -b radix -p vega -y
# 2) Add components used by this demo
npx shadcn@latest add card badge textarea table scroll-area separator accordionNotes:
- Tailwind v4 is loaded in
src/index.css. - The
@/*alias is configured in TS configs andvite.config.ts. components.jsondefines shadcn aliases and styling mode (radix-vega).
- Both editors start connected.
- Typing in one editor creates local CRDT operations.
- If both are connected, operations merge automatically into the peer replica.
- If one editor is isolated, operations remain local and pending counters increase.
- Use push/sync controls (or reconnect) to deliver queued changes.
The CRDT engine lives in src/utils/crdt.ts.
class CRDTDocument {
constructor(initialDoc?: Doc);
static fromDoc(doc: Doc): CRDTDocument;
getText(): string;
insert(agent: string, pos: number, text: string): void;
insertOne(agent: string, pos: number, char: string): void;
delete(pos: number, length: number): void;
mergeFrom(source: CRDTDocument | Doc): void;
applyRemoteInsert(item: Item): void;
toDoc(): Doc;
getItems(): Item[];
getVersion(): Version;
}ID = [agent, seq]Itemcontains content, unique id, left/right origins, anddeletedtombstone stateVersiontracks the highest sequence applied per agentDoccontains ordered content items and version map
- Inserts are causal and ordered by:
- origin references (
originLeft,originRight) - deterministic conflict resolution by agent id when origins collide
- origin references (
- Deletes mark tombstones (
deleted = true) instead of removing items - Replica merge (
mergeInto) applies missing operations only when dependencies exist, then propagates delete flags
- In-memory simulation only (no real server transport layer)
- No cursor-presence or selection synchronization
- Text diffing in the UI assumes a single contiguous change per input event (works well for typical typing/editing)
Before committing changes, run:
npm run lint
npm run build