Layout System
Status: Implemented Depends On: Page System, Block Content Types
Overview
Section titled “Overview”The Layout System enables CSS Grid-based spatial arrangement of page content. Instead of the default single-column flow, pages can display blocks in multi-column and multi-row grids using named areas. Each area maps to a block, and each block gets its own independent TipTap/Loro editor instance (per-cell CRDT isolation).
Layouts are defined as CSS Grid grid-template-areas strings. The system ships with 8 builtin layout definitions and supports user-created custom layouts. When a page type has a default layout, newly created pages of that type receive the layout automatically.
Architecture Overview
Section titled “Architecture Overview”Architecture
Section titled “Architecture”Framework (Tauri) └── Tauri commands apps/desktop/src-tauri/src/commands/ apply_layout, remove_layout, assign_block_area, create_block_in_area, check_layout_compatibility, create_layout, update_layout, delete_layout, list_layouts
Application ├── Layout definition CRUD crates/application/src/layout/ │ CreateLayoutUseCase, UpdateLayoutUseCase, │ DeleteLayoutUseCase, ListLayoutsUseCase ├── LayoutRepository (trait) crates/application/src/layout/services.rs └── Page-layout operations crates/application/src/page/ ApplyLayoutUseCase, RemoveLayoutUseCase, AssignBlockToAreaUseCase, CreateBlockInAreaUseCase, CheckLayoutCompatibilityUseCase
Domain ├── Layout (value object) crates/domain/src/layout.rs │ CSS Grid template parsing, rectangle validation, area extraction └── LayoutDefinition (entity) crates/domain/src/layout_definition.rs Named wrapper with metadata, builtin definitions
Infrastructure └── SqliteLayoutRepository crates/infrastructure/sqlite/src/workspace/ layouts table, pages.layout + blocks.area columns, types.default_layout_id (all in V001 baseline)
Frontend ├── GridLayout.tsx apps/desktop/src-react/components/editor/ │ Multi-editor grid, one InklingsEditor per cell ├── LayoutPicker.tsx Layout selection dropdown ├── LayoutPreview.tsx Visual preview of grid areas ├── LayoutCreator.tsx Custom layout creation UI ├── LayoutConflictDialog.tsx Displaced block resolution dialog └── EmptyCell.tsx Click-to-create for unoccupied areasDependencies flow inward: Framework -> Infrastructure -> Application -> Domain.
Key Design Decisions
Section titled “Key Design Decisions”1. CSS Grid Template Areas as the Layout Primitive
Section titled “1. CSS Grid Template Areas as the Layout Primitive”Layouts use the CSS grid-template-areas syntax directly. Each row is newline-separated, each column is space-separated. Area names must form contiguous rectangles (validated at construction time). This aligns the data model with the rendering engine — the template string maps 1:1 to a CSS property.
portrait stats statsportrait bio bionotes notes notesThis defines four areas: portrait (spans rows 1-2, column 1), stats (row 1, cols 2-3), bio (row 2, cols 2-3), and notes (row 3, all columns).
2. Contiguous Rectangle Validation
Section titled “2. Contiguous Rectangle Validation”Every named area in a template must form a contiguous rectangle. This is enforced at domain construction via bounding box check:
let expected_count = (max_row - min_row + 1) * (max_col - min_col + 1);if cells.len() != expected_count { return Err(DomainError::Validation(...));}L-shapes, T-shapes, and other non-rectangular arrangements are rejected. This guarantees valid CSS Grid output without runtime surprises.
Complexity bounds are enforced: max 20 rows, max 20 columns, max 50 distinct area names.
3. Layout as a Value Object, LayoutDefinition as Entity
Section titled “3. Layout as a Value Object, LayoutDefinition as Entity”Layout is a value object (no ID, no timestamps) containing only the grid specification: version, grid_type, template,
and areas. It is embedded directly in the pages.layout JSON column.
LayoutDefinition wraps a Layout with an ID, name, description, icon, and is_builtin flag. Definitions live in the
layouts table and serve as the catalog from which users pick layouts.
When a layout is applied to a page, the Layout value object is snapshotted onto the page. Subsequent changes to
the LayoutDefinition do not retroactively affect pages already using it.
4. Per-Cell CRDT Isolation
Section titled “4. Per-Cell CRDT Isolation”Multi-editor grids require one LoroDoc per block — blocks in different grid areas never share a CRDT document. Each
BlockCell in GridLayout.tsx creates its own InklingsEditor instance that loads/saves independently via
get_block_content_snapshot_by_id / save_block_content_by_id. This prevents cross-cell interference and keeps
undo/redo scoped to individual cells.
5. Builtin Layouts with Deterministic IDs
Section titled “5. Builtin Layouts with Deterministic IDs”The 8 builtin layout definitions use UUID v5 (derived from Uuid::NAMESPACE_DNS + layout name), making IDs stable
across restarts and workspace re-seeds. Builtins are marked is_builtin = true and cannot be modified or deleted. They
are seeded via INSERT OR IGNORE on workspace open.
| Name | Template | Areas |
|---|---|---|
| Single Column | content | 1 |
| Two Equal Columns | left right | 2 |
| Sidebar + Main | sidebar main main | 2 |
| Main + Sidebar | main main sidebar | 2 |
| Two-by-Two Grid | tl tr\nbl br | 4 |
| Header + Two Columns + Footer | header header\nleft right\nfooter footer | 4 |
| Character Sheet | portrait stats stats\nportrait bio bio\nnotes notes notes | 4 |
| Dashboard | metric1 metric2 metric3\ndetail-left detail-left detail-right | 5 |
6. Layout Compatibility and Conflict Resolution
Section titled “6. Layout Compatibility and Conflict Resolution”When switching a page to a new layout, blocks with area assignments that don’t exist in the new layout are “displaced.” The system provides a two-step workflow:
- Check (
CheckLayoutCompatibilityUseCase): Read-only query returning aLayoutCompatibilityResultwith displaced block IDs and content previews. - Resolve (
LayoutConflictDialog): Frontend presents displaced blocks to the user. They can cancel or force-apply (clearing displaced areas).
ApplyLayoutUseCase rejects the operation if displaced blocks exist — the caller must resolve conflicts first or use
RemoveLayoutUseCase to clear all areas.
7. Type-Layout Default Association
Section titled “7. Type-Layout Default Association”Page types can have a default_layout_id (V001 baseline). When a page of that type is created, the default layout is
applied automatically. Deleting a layout definition clears default_layout_id on any types that reference it (via
clear_default_layout_references in TypeRepository).
Storage Schema
Section titled “Storage Schema”pages.layout (V001 baseline)
Section titled “pages.layout (V001 baseline)”JSON column storing the snapshotted Layout value object:
{ "version": 1, "grid_type": "css_grid", "template": "portrait stats stats\nportrait bio bio\nnotes notes notes", "areas": ["portrait", "stats", "bio", "notes"]}NULL when no layout is applied (default single-column rendering).
blocks.area (V001 baseline)
Section titled “blocks.area (V001 baseline)”TEXT column storing the area name this block is assigned to. NULL for blocks without area assignment.
layouts table (V001 baseline)
Section titled “layouts table (V001 baseline)”CREATE TABLE layouts ( id TEXT PRIMARY KEY, name TEXT NOT NULL, description TEXT, icon TEXT, layout TEXT NOT NULL, -- JSON Layout value object is_builtin INTEGER NOT NULL DEFAULT 0, created_at TEXT NOT NULL, updated_at TEXT NOT NULL);CREATE INDEX idx_layouts_name ON layouts(name);UNIQUE constraint on name prevents duplicate layout definition names.
types.default_layout_id (V001 baseline)
Section titled “types.default_layout_id (V001 baseline)”TEXT column on the types table referencing layouts(id). Applied automatically when pages of that type are created.
Key Code Paths
Section titled “Key Code Paths”| Scenario | Entry Point |
|---|---|
| Apply layout to page | ApplyLayoutUseCase::execute() in crates/application/src/page/apply_layout.rs |
| Remove layout from page | RemoveLayoutUseCase::execute() in crates/application/src/page/remove_layout.rs |
| Check compatibility before switch | CheckLayoutCompatibilityUseCase::execute() in crates/application/src/page/check_layout_compatibility.rs |
| Assign block to area | AssignBlockToAreaUseCase::execute() in crates/application/src/page/assign_block_area.rs |
| Create block in empty cell | CreateBlockInAreaUseCase::execute() in crates/application/src/page/create_block_in_area.rs |
| CRUD layout definitions | crates/application/src/layout/ (create, update, delete, list) |
| Render multi-editor grid | GridLayout.tsx in apps/desktop/src-react/components/editor/ |
| Template validation | Layout::validate_template() in crates/domain/src/layout.rs |
Related
Section titled “Related”- Page System (
page-system): Layout is a property of a page; blocks belong to pages - Block Content Types (
block-content-system):CreateBlockInAreaUseCasesupports both Markdown and Image blocks - ADR-006: Per-cell CRDT isolation builds on Loro architecture
Enables spatial page layouts. See also: Tag System, Wiki-Link System.
Was this page helpful?
Thanks for your feedback!