Skip to content
Documentation GitHub
Content

Layout System

Status: Implemented Depends On: Page System, Block Content Types


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.


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 areas

Dependencies flow inward: Framework -> Infrastructure -> Application -> Domain.


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 stats
portrait bio bio
notes notes notes

This 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).

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.

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.

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.

NameTemplateAreas
Single Columncontent1
Two Equal Columnsleft right2
Sidebar + Mainsidebar main main2
Main + Sidebarmain main sidebar2
Two-by-Two Gridtl tr\nbl br4
Header + Two Columns + Footerheader header\nleft right\nfooter footer4
Character Sheetportrait stats stats\nportrait bio bio\nnotes notes notes4
Dashboardmetric1 metric2 metric3\ndetail-left detail-left detail-right5

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:

  1. Check (CheckLayoutCompatibilityUseCase): Read-only query returning a LayoutCompatibilityResult with displaced block IDs and content previews.
  2. 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.

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).


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).

TEXT column storing the area name this block is assigned to. NULL for blocks without area assignment.

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.

TEXT column on the types table referencing layouts(id). Applied automatically when pages of that type are created.


ScenarioEntry Point
Apply layout to pageApplyLayoutUseCase::execute() in crates/application/src/page/apply_layout.rs
Remove layout from pageRemoveLayoutUseCase::execute() in crates/application/src/page/remove_layout.rs
Check compatibility before switchCheckLayoutCompatibilityUseCase::execute() in crates/application/src/page/check_layout_compatibility.rs
Assign block to areaAssignBlockToAreaUseCase::execute() in crates/application/src/page/assign_block_area.rs
Create block in empty cellCreateBlockInAreaUseCase::execute() in crates/application/src/page/create_block_in_area.rs
CRUD layout definitionscrates/application/src/layout/ (create, update, delete, list)
Render multi-editor gridGridLayout.tsx in apps/desktop/src-react/components/editor/
Template validationLayout::validate_template() in crates/domain/src/layout.rs

  • Page System (page-system): Layout is a property of a page; blocks belong to pages
  • Block Content Types (block-content-system): CreateBlockInAreaUseCase supports 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?