Skip to content

Ethics03/primordial

Repository files navigation

Crosmos is a Tauri-based knowledge-base application with a custom block-based editor. It features a workspace system ("spaces"), file management, and an immutable editor state architecture.

Tech Stack

  • Frontend: React 19 + TypeScript + Vite
  • Backend: Tauri v2 (Rust)
  • State Management: Zustand
  • Styling: Tailwind CSS v4 + custom CSS
  • Editor: Custom block-based contentEditable implementation

Development Commands

# Start development server (frontend + Tauri)
bun run dev

# Build the application
bun run build

# Preview production build
bun run preview

# Run Tauri CLI commands
bun run tauri <command>

Core Architecture

Editor Architecture (Immutable State Pattern)

The editor follows a strict immutability pattern (inspired from the implementation of Lexical from facebook) with three core layers:

  1. EditorState (src/lib/editor/core/editorState.ts): Top-level immutable state container
  • Contains NoteModel, selection state, and undo/redo stacks
  • Every operation returns a new EditorState instance
  • Methods: withContent(), withSelection(), forceSelect(), push(), undo(), redo()
  1. NoteModel (src/lib/editor/core/noteModel.ts): Immutable document model
  • Contains array of blocks with O(1) lookup via internal Map
  • Every operation returns a new NoteModel instance
  • Key methods: updateBlock(), insertTextAtOffset(), mergeBlocks(), handleEnter()
  • Converts to/from JSON for persistence
  1. Block System: Plugin-based block types registered in src/lib/editor/registry.ts
  • Each block type has a config with: className, render, handleEnter, shortcuts
  • Block types: paragraph, heading1/2/3, codeBlock, bulletList, lineBreak
  • New block types can be added by implementing BlockTypeConfig and registering

Selection Management

  • Selection capture: getCurrentSelection() in src/lib/editor/core/selection.ts
  • Calculates offset within block content from DOM selection
  • Uses data-block-id attributes to identify blocks
  • Selection restoration: restoreSelection()
  • Uses TreeWalker to find correct text node and offset
  • Handles empty blocks and edge cases
  • Called after state updates via useSelection hook
  • Force selection: Used when operations require immediate caret repositioning (e.g., Enter key splits block and moves caret to new block)

Key Handlers (src/hooks/useKeyHandlers.ts)

  • Enter: Splits block at cursor, creates new block below, moves cursor to start of new block
  • Shift+Enter: Inserts newline within block (browser default)
  • Backspace at offset 0: Merges current block with previous block, positions cursor at merge point

Store Architecture (Zustand)

  • editorStore (src/stores/editorStore.ts): Manages editor state, tabs, file I/O
  • Tab system: Each tab tracks its own EditorState
  • File operations: openFile(), saveFile(), closeTab(), setActiveTab()
  • Auto-saves on tab switches and dirty state changes
  • spaceStore (src/stores/spaceStore.ts): Manages workspaces
  • "Spaces" are directories containing notes
  • Communicates with Tauri backend via invoke()

Auto-save System (src/hooks/useAutoSave.ts)

  • Debounced auto-save (1 second delay)
  • Manual save shortcut: Cmd/Ctrl+S
  • Clears pending saves when file becomes clean or switches

Tauri Integration

  • File I/O commands: read_file_cmd, write_file_cmd
  • Space management: get_spaces, add_spaces, set_last_opened, remove_space
  • Uses Tauri plugins: fs, dialog, opener, store
  • Rust backend in src-tauri/ with commands in src-tauri/src/commands/

Important Patterns

Immutability Flow

When updating content:

const updatedModel = noteModel.updateBlock(id, content);  // Returns new NoteModel
const newState = editorState.withContent(updatedModel);   // Returns new EditorState
setEditorState(newState);                                 // Triggers re-render

Selection Preservation

Operations that modify structure (Enter, Backspace merge) use forceSelect():

const newState = editorState
  .withContent(newNote)
  .forceSelect({ blockId, offset, isCollapsed: true });

The useSelection hook watches for forceSelection flag and restores cursor position.

Block Rendering

Blocks are rendered via BlockView component which:

  1. Looks up block config from registry
  2. Wraps block with data-block-id and data-block-type attributes
  3. Renders block-specific component from config
  4. Each block handles its own contentEditable div with onInput and onKeyDown callbacks

File Structure

src/
├── lib/editor/
│   ├── core/              # Core editor logic (EditorState, NoteModel, selection)
│   ├── blocks/            # Block type implementations
│   ├── types.ts           # Type definitions
│   └── registry.ts        # Block type registry
├── components/
│   ├── editor/            # Editor UI (Editor, BlockView, Tabs)
│   ├── block/             # Block component implementations
│   └── [other UI]
├── stores/                # Zustand stores
├── hooks/                 # React hooks
└── styles/                # CSS files

src-tauri/
├── src/
│   ├── main.rs           # Entry point
│   ├── commands/         # Tauri commands
│   └── models/           # Rust data models
└── Cargo.toml            # Rust dependencies

Data Format

Notes are stored as JSON with structure:

{
  id: string,
  title: string,
  version: number,
  blocks: Block[],
  createdAt: number,
  updatedAt: number
}

Each block:

{
  id: string,
  type: BlockType,
  content: string,
  metadata?: Record<string, any>,
  children?: Block[]
}

Common Development Tasks

Adding a New Block Type

  1. Create block component in src/components/block/ implementing BlockProps
  2. Export BlockTypeConfig from component file
  3. Register in src/lib/editor/registry.ts
  4. Add type to BlockType union in src/lib/editor/types.ts

Debugging Selection Issues

  • Check data-block-id attributes are present on block wrappers
  • Verify contentEditable element exists within block
  • Use browser dev tools to inspect Selection and Range objects
  • Check console for selection restoration errors

Working with Immutable State

  • Never mutate blocks array or Block objects directly
  • Always use NoteModel methods that return new instances
  • Chain operations on EditorState: state.withContent(...).forceSelect(...)
  • Use push() to add to undo stack for undoable operations
  • remember we gotta implement richText with consistent behaviour

About

Primordial - A Tauri-based note-taking app with a custom block editor, rich text support, immutable state architecture. (I was crazy)

Topics

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors