Layout Abstraction
Summary
Section titled “Summary”A Layout is an embedded Reducer that implements Component. It arranges child components using an AutoGrid instance as its spatial engine. Layout holds Loro container handles (like Prose holds LoroText), so mutations write-through to the CRDT — children and grid configuration are synced and collaborative.
Children are stored in an in-memory Vec<LayoutChild> (primary read path) with Loro write-through (LoroList + LoroMap). AutoGrid determines position from list order — children have no coordinates in Loro. Layout persists order and spans; AutoGrid computes pixel positions on demand via Taffy (CSS Grid).
Layouts support two kinds: homogeneous (N instances of the same component, UI can add/remove) and heterogeneous (fixed slots with specific component types per slot, defined by a LayoutType).
On a Whiteboard, layouts are positioned elements (structured islands within the free canvas) mixed freely with regular placements. On a Worksheet, the modality embeds layouts for structured arrangement. On a Slide, each slide contains one layout. Documents have no layouts — they are vertical component lists by nature.
Layouts support lock/unlock (unlocking explodes a layout into individual free elements) and nesting (layouts containing layouts via flat map). Coercion (switching component types while preserving property values) is designed in Component Coercion.
Depends on Typed Element Components for per-element-type property schemas. Depends on AutoGrid Reducer for the spatial engine. Interacts with Document Modality — Documents deliberately omit layouts. Interacts with Selection Trait — layout children participate in selection.
Motivation
Section titled “Motivation”-
No structured arrangement primitive — Whiteboards have free positioning (
WhiteboardPosition { x, y, w, h, rotation, z_index }) and worksheets have a flat AutoGrid. There’s no abstraction for “arrange these N items in a column” or “wrap these cards into rows.” Every layout decision is either manual (whiteboard) or hardcoded (worksheet grid). -
Templates can’t express layout intent — A template can declare component placements with property bindings, but it can’t say “put these three items in a row” or “stack these vertically with 16px gap.” Layout algorithms are implicit in the modality, not declarative in the template.
-
No structured-to-freeform bridge — Users want both structure (auto-arranged content) and freedom (manual positioning). There’s no way to start with a structured arrangement and break out of it, or to select freeform elements and group them into a layout.
-
AI can’t generate spatial structure — AI generates component properties but has no vocabulary for arranging them. Without a layout primitive, AI output is either unstyled (worksheet) or requires manual positioning (whiteboard).
-
Worksheet-to-whiteboard gap — Worksheets and whiteboards feel like different universes. A layout abstraction shared across modalities would let the same content flow between structured (worksheet) and freeform (whiteboard) contexts.
Design
Section titled “Design”Layout — embedded Reducer + Component
Section titled “Layout — embedded Reducer + Component”Layout follows the Prose pattern: an embedded Reducer that holds Loro container handles into the parent’s LoroDoc. It also implements Component so it plugs into the standard compilation pipeline.
pub struct Layout { rt: Runtime<Self>,}LayoutState — dual state with Loro write-through
Section titled “LayoutState — dual state with Loro write-through”pub struct LayoutState { /// Ordered children — primary read path (in-memory Vec). pub children: Vec<LayoutChild>, /// Layout kind (homogeneous or heterogeneous). pub kind: LayoutKind, /// Whether the layout is locked. pub locked: bool, /// Spatial engine — ephemeral, rebuilt from Loro on init. pub grid: AutoGrid, /// Loro list handle for persisting children. pub loro_children: LoroList, /// Loro map handle for persisting config. pub loro_config: LoroMap,}The Vec<LayoutChild> is the primary read path for fast access. Loro handles are always required (matching the Prose pattern — ProseState always holds a LoroText). All mutations write-through to both the Vec and Loro. Tests create a LoroDoc and pass the handles — it’s trivial setup.
AutoGrid is always ephemeral, rebuilt from the children Vec on init. No coordinates in Loro — AutoGrid computes pixel positions from list order + spans via Taffy.
LayoutChild
Section titled “LayoutChild”pub struct LayoutChild { pub id: String, pub component_id: String, pub bindings: HashMap<String, PropertyValue>, pub col_span: i32, pub row_span: i32,}LayoutChild implements LoroMapItem for bidirectional Loro mapping. Bindings are stored as a JSON string in Loro (simpler than per-field CRDT for property bags).
LayoutKind
Section titled “LayoutKind”Controls whether the UI can add/remove children:
pub enum LayoutKind { /// All children use the same component (e.g. all "card" or all "question"). Homogeneous { component_id: String }, /// Fixed child slots defined by a LayoutType. UI cannot add/remove. Heterogeneous { layout_type_id: String },}With homogeneous, each child’s component_id is stored per-child (allows coercion to change it). With heterogeneous, each child’s component_id comes from the LayoutType’s slot definitions.
Loro container structure
Section titled “Loro container structure”Each layout creates containers in the parent’s LoroDoc:
// In parent Modality::new(), for each layout placement:let children_list = doc.get_list(&format!("layout:{id}"));let config_map = doc.get_map(&format!("layout_config:{id}"));let layout = Layout::new(kind, options, width, children_list, config_map);Child LoroMap schema (via LoroMapItem):
{ "id": String, "component_id": String, "bindings": String (JSON), "col_span": i64, "row_span": i64}No x/y/w/h — position is determined by index in the LoroList. AutoGrid computes pixel positions via Taffy CSS Grid auto-placement.
Config LoroMap schema:
{ "kind": String (JSON of LayoutKind), "grid_options": String (JSON of GridOptions), "locked": bool}Reducer implementation
Section titled “Reducer implementation”impl Reducer for Layout { type State = LayoutState; type Intent = LayoutIntent; type Feedback = LayoutFeedback; type Error = LayoutError; type Services = LayoutServices;
fn reduce( cmd: Command<LayoutIntent, LayoutFeedback>, state: &mut LayoutState, ) -> Effect<LayoutServices, LayoutIntent, LayoutFeedback> { match cmd { Command::Intent(intent) => { if let Err(e) = reduce_intent(intent, state) { return Effect::feedback(LayoutFeedback::Error(e)); } Effect::none() } Command::Feedback(_) => Effect::none(), } }}LayoutIntent
Section titled “LayoutIntent”pub enum LayoutIntent { /// Add a child at the given index (None = append). AddChild { id: String, component_id: String, bindings: HashMap<String, PropertyValue>, col_span: i32, row_span: i32, index: Option<i32>, }, /// Remove a child by ID. RemoveChild { id: String }, /// Update property bindings on a child. UpdateBindings { id: String, bindings: HashMap<String, PropertyValue> }, /// Change the layout kind (homogeneous/heterogeneous). SetKind { kind: LayoutKind }, /// Lock/unlock the entire layout. SetLocked { locked: bool }, /// Forward an intent to the embedded AutoGrid. Grid(AutoGridIntent),}Intent reduce — how Layout and AutoGrid interact
Section titled “Intent reduce — how Layout and AutoGrid interact”The key insight: Vec order IS AutoGrid Vec order. Layout keeps them in sync. Each mutation:
- Updates
Vec<LayoutChild>(primary state) - Dispatches to AutoGrid (ephemeral positions)
- Writes through to Loro (persistence)
LayoutIntent::AddChild { id, component_id, bindings, col_span, row_span, index } => { let child = LayoutChild { id, component_id, bindings, col_span, row_span }; let node = LayoutNode::with_spans(&child.id, col_span, row_span);
// 1. Insert into Vec let idx = match index { Some(idx) => { state.children.insert(idx, child.clone()); idx } None => { state.children.push(child.clone()); state.children.len() - 1 } };
// 2. Add to AutoGrid state.grid.dispatch(Command::Intent(AutoGridIntent::Add { node, index }));
// 3. Write-through to Loro if index.is_some() { insert_child_to_loro(&state.loro_children, idx, &child); } else { push_child_to_loro(&state.loro_children, &child); }}After Grid reorder/resize intents, sync_from_grid() reads the new order and spans from the AutoGrid snapshot back into the children Vec, then writes through to Loro.
Component implementation — compile flow
Section titled “Component implementation — compile flow”Layout provides children_data() so the parent knows what to compile, and compile_children() to position pre-compiled outputs:
impl Layout { /// Get data about all children so the parent can compile them. pub fn children_data(&self) -> Vec<LayoutChildData> { ... }
/// Compile children into a LayoutElement. /// Takes pre-compiled children as (id, output) pairs and positions them /// using the AutoGrid snapshot. Generic over output type O. pub fn compile_children<O: Clone + PartialEq>( &self, id: String, compiled: Vec<(String, O)>, ) -> LayoutElement<O> { let snapshot = self.grid().snapshot(); let children = compiled.into_iter().filter_map(|(child_id, output)| { snapshot.nodes.iter() .find(|sn| sn.id == child_id) .map(|sn| PositionedChild { id: child_id, x: sn.x, y: sn.y, w: sn.w, h: sn.h, output }) }).collect();
LayoutElement { id, grid_options: snapshot.options, children, container_width: snapshot.container_width, ... } }}LayoutElement<E> is generic — each modality provides its own concrete type (e.g. WhiteboardLayoutElement wraps LayoutElement<Vec<WhiteboardElement>>).
Parent compile_child flow
Section titled “Parent compile_child flow”The parent modality (e.g. Whiteboard) orchestrates the full compile:
fn compile_layout_recursive( layout_reducer: &Layout, layout_id: &str, all_layouts: &HashMap<String, Layout>,) -> Vec<WhiteboardElement> { let children_data = layout_reducer.children_data(); let mut compiled = vec![];
for child in &children_data { if child.component_id == "layout" { // Recurse: look up nested layout in the flat map let nested = all_layouts.get(&child.id)?; let elements = compile_layout_recursive(nested, &child.id, all_layouts); compiled.push((child.id.clone(), elements)); } else { // Regular component: compile through component registry let elements = compile_component(&child.component_id, &child.bindings); compiled.push((child.id.clone(), elements)); } }
let generic = layout_reducer.compile_children(layout_id.to_string(), compiled); let layout_element = WhiteboardLayoutElement::from_generic(generic); vec![WhiteboardElement::Layout(layout_element)]}Layout handles where (AutoGrid pixel positions), parent handles what (component compilation, recursion, caching).
Container bounds
Section titled “Container bounds”The parent determines the AutoGrid container width from the placement’s dimensions:
- Whiteboard:
placement.position.w(the layout element’s width on the canvas) - Worksheet: page width
- Slide: slide width
The parent passes container_width when creating the Layout and updates it via LayoutIntent::Grid(AutoGridIntent::SetContainerWidth { width }) when the placement is resized.
// When the layout placement is resized on the whiteboard:WhiteboardIntent::ResizeElement { id, .. } => { // ...update placement position... if p.component_key == "layout" { if let Some(layout) = state.ephemeral.layouts.get_mut(&id) { layout.dispatch(Command::Intent(LayoutIntent::Grid( AutoGridIntent::SetContainerWidth { width } ))); } }}Embedding pattern — flat map
Section titled “Embedding pattern — flat map”Layout instances live in the parent’s ephemeral state as a flat map:
pub struct WhiteboardEphemeral { pub prose_editors: HashMap<String, Prose>, pub layouts: HashMap<String, Layout>, // ALL layouts: top-level + nested // ...}Both top-level layouts (tied to placements with component_key == "layout") and nested layouts (children of other layouts with component_id == "layout") live in the same flat HashMap. No tree structure — nesting is resolved at compile time by recursion through the flat map.
This matches the Prose pattern: a flat registry keyed by ID, with creation/destruction managed by the parent.
Intent forwarding
Section titled “Intent forwarding”// In parent intent enum:pub enum WhiteboardIntent { // ...existing... Layout { id: String, intent: LayoutIntent }, UnlockLayout { layout_id: String }, CreateLayoutFromSelection { element_ids: Vec<String>, options: GridOptions, kind: LayoutKind },}
// In parent reduce:WhiteboardIntent::Layout { id, intent } => { // Check if adding/removing a nested layout child let adding_nested = matches!(&intent, LayoutIntent::AddChild { component_id, .. } if component_id == "layout" ).then(|| child_id.clone()); let removing_nested = matches!(&intent, LayoutIntent::RemoveChild { id } if /* child is a layout */ ).then(|| child_id.clone());
// Forward to the layout if let Some(layout) = state.ephemeral.layouts.get_mut(&id) { layout.dispatch(Command::Intent(intent)); }
// Create/remove nested layout in the flat map if let Some(child_id) = adding_nested { let children_list = doc.get_list(&format!("layout:{child_id}")); let config_map = doc.get_map(&format!("layout_config:{child_id}")); state.ephemeral.layouts.insert(child_id, Layout::new(...)); } if let Some(child_id) = removing_nested { state.ephemeral.layouts.remove(&child_id); }}Init flow
Section titled “Init flow”fn init_layouts(doc: &LoroDoc, placements: &[ComponentPlacement]) -> HashMap<String, Layout> { let mut layouts = HashMap::new();
// 1. Create top-level layouts from placements for p in placements.iter().filter(|p| p.component_key == "layout") { let children_list = doc.get_list(&format!("layout:{}", p.id)); let config_map = doc.get_map(&format!("layout_config:{}", p.id)); let kind = LayoutKind::Homogeneous { component_id: /* from placement bindings */ }; layouts.insert(p.id.clone(), Layout::new(kind, options, width, children_list, config_map)); }
// 2. Walk layout children and create entries for nested layouts loop { let needed: Vec<String> = layouts.values() .flat_map(|l| l.state().children.iter()) .filter(|c| c.component_id == "layout" && !layouts.contains_key(&c.id)) .map(|c| c.id.clone()) .collect(); if needed.is_empty() { break; } for child_id in needed { let children_list = doc.get_list(&format!("layout:{child_id}")); let config_map = doc.get_map(&format!("layout_config:{child_id}")); layouts.insert(child_id, Layout::new(...)); } }
layouts}Nesting
Section titled “Nesting”Layouts can contain other layouts. A child with component_id == "layout" creates an entry in the parent’s flat layouts map. Nesting is:
- Flat, not tree — all layouts (top-level and nested) live in one
HashMap<String, Layout>. NoLayoutState::nested. - Resolved at compile time —
compile_layout_recursive()walks the flat map, recursing when it encounters a layout child. - Managed by parent — the whiteboard creates/destroys nested Layout instances in
Layouthandler when children withcomponent_id == "layout"are added/removed. - Undo/redo —
reconcile_layouts_from_loro()rebuilds all layouts from Loro, then adds missing nested layouts and (optionally) removes stale ones.
Undo/redo — rebuild from Loro
Section titled “Undo/redo — rebuild from Loro”fn reconcile_layouts_from_loro(ephemeral: &mut WhiteboardEphemeral) { // 1. Rebuild each existing layout from Loro for layout in ephemeral.layouts.values_mut() { layout.rebuild_from_loro(); }
// 2. Add new nested layouts that appeared after undo/redo let needed: Vec<String> = ephemeral.layouts.values() .flat_map(|l| l.state().children.iter()) .filter(|c| c.component_id == "layout" && !ephemeral.layouts.contains_key(&c.id)) .map(|c| c.id.clone()) .collect(); for child_id in needed { ephemeral.layouts.insert(child_id, Layout::new(...)); }}Called from on_undo() and on_redo() hooks on the Whiteboard modality.
Lock / unlock
Section titled “Lock / unlock”A layout starts locked — children are auto-arranged and cannot be individually repositioned.
Unlocking a layout (Whiteboard only):
- Gets pixel positions from
grid.snapshot()for all children - Converts each child into an independent
ComponentPlacement<WhiteboardPosition>using the snapshot’s pixel coordinates (offset by the layout placement’s own position) - Removes the layout’s Loro containers from the doc
- Removes the layout placement from
placements - Adds the new independent placements
This is a one-way operation. Grouping freeform elements back into a layout requires explicit CreateLayoutFromSelection.
LayoutType — creation template for heterogeneous layouts
Section titled “LayoutType — creation template for heterogeneous layouts”LayoutType defines what child slots a heterogeneous layout has. Only used for creation and heterogeneous coercion — not used at compile time.
pub struct LayoutType { pub id: String, pub name: String, pub slots: Vec<LayoutSlot>, pub grid_columns: Option<i32>,}
pub struct LayoutSlot { pub component_id: String, pub col_span: i32, pub row_span: i32,}Note: Component Coercion proposes redesigning LayoutType as shape-only (slots without component references) and replacing LayoutKind with a LayoutConfig struct. This RFC documents the current implementation; the Nested Toolbar Context RFC describes the planned evolution.
Empty state — placeholders
Section titled “Empty state — placeholders”When a layout child has no bindings populated, it renders as a placeholder. Placeholder rendering is a Dart concern — the compiled output includes an is_empty: bool flag per element so Dart can overlay contextual placeholders:
- Image component -> “Generate with AI / Upload / Paste URL”
- Text component -> “Type or generate with AI”
- Question component -> “Add question”
Compile child hash
Section titled “Compile child hash”Layout placements participate in the compile cache. The parent includes layout children data in the hash so recompilation triggers when children change:
// In compile_child_hash for layout placements:if placement.component_key == "layout" { if let Some(layout) = self.rt.state.ephemeral.layouts.get(&placement.id) { for child in &layout.state().children { child.id.hash(&mut hasher); child.component_id.hash(&mut hasher); child.col_span.hash(&mut hasher); child.row_span.hash(&mut hasher); } }}Implementation Plan
Section titled “Implementation Plan”-
Layout types —
LayoutKind,LayoutChild,LayoutType,LayoutSlotincrates/modality/layout/src/types.rs.LayoutIntent,LayoutFeedback,LayoutErrorincommands.rs. Done. -
Layout Reducer —
Layout { rt: Runtime<Self> }withLayoutState(Vec + optional Loro + AutoGrid),reduce_intent(). Done. 21 tests (14 in-memory + 7 Loro persistence). -
Loro persistence —
LayoutChildimplementsLoroMapItem.loro_sync.rsprovides init, CRUD, and sync helpers. Write-through on every mutation.rebuild_from_loro()for undo/redo. Done. -
Layout Component — Generic
LayoutElement<E>,PositionedChild<E>,compile_children()positions pre-compiled outputs using AutoGrid snapshot.children_data()for parent to know what to compile. Done. -
Whiteboard integration —
layouts: HashMap<String, Layout>onWhiteboardEphemeral.Layoutintent forwarding.compile_layout_recursive()for nesting.reconcile_layouts_from_loro()for undo/redo. Resize ->SetContainerWidth. Compile child hash includes layout children. Done. -
Nesting — Flat map design. Whiteboard creates nested Layout entries when
component_id == "layout"children are added. Recursive compile. Reconcile on undo/redo. Done. -
Coercion — Designed in Component Coercion. Not yet implemented.
-
Lock/Unlock + CreateLayoutFromSelection — Designed but not yet implemented. UnlockLayout explodes layout into independent placements. CreateLayoutFromSelection groups selected elements into a new layout.
-
Worksheet / Slide integration — Not yet implemented. Same pattern as whiteboard (embed Layout instances, pass container width).
-
FRB + Dart — Layout types exposed via FRB (
layout::commands,layout::typesinrust_input). ConcreteWhiteboardLayoutElementin whiteboard elements. FRB codegen includes layout Dart bindings. Dart rendering not yet implemented. -
AI vocabulary — Layout-aware tools (
add_layout,add_layout_child, etc.). Not yet implemented.
Alternatives Considered
Section titled “Alternatives Considered”Layout as a Modality
Section titled “Layout as a Modality”Making Layout a full Modality with its own Session, Slot, HasChildren support. Layout would get free undo/redo, clipboard, AI per layout. Rejected because:
- Whiteboard wants mixed content (free elements + layouts in the same placement list) — HasChildren only supports one child type
- Per-layout Session is heavyweight for what is essentially “arrange these children”
- Undo, clipboard, AI all operate at the parent Session level (same as Prose)
- Embedded Reducer is simpler and consistent with Prose pattern
Layout as pure data (not a Reducer)
Section titled “Layout as pure data (not a Reducer)”A Layout struct in synced state with LayoutAlgorithm::arrange() pure function. No Reducer, no state. Rejected because:
- Layout needs interactive behavior: drag, resize, reorder (via AutoGrid)
- Pure functions can’t handle drag lifecycle (save/restore, hysteresis)
- The Reducer pattern gives standard dispatch, snapshot emission, and service access
Loro containers as primary state
Section titled “Loro containers as primary state”Layout state holds LoroList/LoroMap directly as the primary read path, no in-memory Vec. Rejected because:
- Reading from Loro on every access is slower than Vec
- Vec is the natural fast read path; Loro is the persistence/sync layer
Optional Loro handles
Section titled “Optional Loro handles”Make Loro handles Option<LoroList> / Option<LoroMap> so tests can run without creating a LoroDoc. Rejected because:
- Prose doesn’t do this —
ProseStatealways holds aLoroText - Every mutation needs
if let Some(ref list)guards, adding noise - Creating a
LoroDocin tests is trivial (2 lines) - One code path is simpler than two
Coordinate-based persistence
Section titled “Coordinate-based persistence”Store x/y/w/h per child in Loro (original the Layout Abstraction RFC design, based on original the AutoGrid RFC coordinate model). Rejected because:
- AutoGrid is order-based — nodes have no coordinates, position = Vec index
- LoroList has natural ordering semantics that map directly to AutoGrid’s model
- Persisting order + spans is simpler and merges better via CRDT (no coordinate conflicts)
- Pixel positions are computed on demand by Taffy — they’re derived, not stored
Nested layouts inside LayoutState
Section titled “Nested layouts inside LayoutState”Store nested HashMap<String, Layout> inside LayoutState::nested. Rejected because:
- Creates a tree structure that’s hard to manage for undo/redo reconciliation
- Flat map (same as
prose_editors) is simpler — all layouts are siblings in one HashMap - Nesting is a compile-time concern, not a state concern
- Intent forwarding (
Layout { id }) works for any depth with the flat map
Resolved Questions
Section titled “Resolved Questions”Q: Is Layout a Modality or an embedded Reducer? A: Embedded Reducer (like Prose). Lives in the parent’s ephemeral state. No own Session — undo, clipboard, AI all flow through the parent. Mixed with free elements on Whiteboard without HasChildren.
Q: Does Layout implement Component?
A: Yes, via compile_children(). Parent pre-compiles children and passes results. Layout positions them using grid.snapshot() pixel coordinates. Parent handles what (component compilation), Layout handles where (AutoGrid positions).
Q: How does Layout interact with AutoGrid? A: Layout embeds an AutoGrid instance. Vec order = AutoGrid Vec order. On init, AutoGrid is rebuilt from Vec. On mutations: Layout updates Vec, dispatches to AutoGrid, then writes through to Loro. AutoGrid has no knowledge of Loro.
Q: What does Layout persist to Loro?
A: Order (LoroList index) and spans (col_span, row_span). Plus child ID, component_id, and bindings (as JSON). No coordinates. Pixel positions are computed by Taffy at snapshot/compile time.
Q: Where does container width come from?
A: The parent. On Whiteboard, it’s the layout placement’s position.w. On Worksheet, page width. On Slide, slide width. Parent passes it at init and updates via SetContainerWidth when the placement is resized.
Q: How does the compile pipeline work?
A: (1) Parent calls layout.children_data() to get child info. (2) For each child, parent resolves the component and compiles it (or recurses for nested layouts). (3) Parent calls layout.compile_children(id, compiled_pairs). (4) Layout calls grid.snapshot(), maps each child’s output to its pixel rect. (5) Returns LayoutElement<O> which the modality wraps in its concrete type.
Q: Can a layout contain other layouts (nesting)?
A: Yes. A child with component_id == "layout" creates an entry in the parent’s flat layouts HashMap. No tree structure in Layout itself. Compile recurses through the flat map. Same pattern as prose_editors.
Q: Are layout children homogeneous or heterogeneous?
A: Both, controlled by LayoutKind. Homogeneous: all children share one component_id, UI can add/remove. Heterogeneous: fixed slots defined by a LayoutType, UI cannot add/remove.
Q: How does undo/redo work?
A: rebuild_from_loro() on each Layout reads children+config from Loro handles and rebuilds the in-memory Vec + AutoGrid. reconcile_layouts_from_loro() adds/removes nested layout entries in the flat map. Called from on_undo()/on_redo() hooks.
Q: How does unlock work?
A: Whiteboard only. Read pixel positions from grid.snapshot(), offset by the layout placement’s own canvas position, create independent ComponentPlacement<WhiteboardPosition> for each child. Remove layout placement + Loro containers. One-way — re-grouping requires CreateLayoutFromSelection.
Q: Can users drag children between layouts? A: No. Cross-layout drag is not supported. Children belong to their layout. Moving content between layouts is done via remove + add, not drag gesture.
Q: What about animation on unlock? A: Pure Dart concern. Dart interpolates from layout positions to freeform positions. The unlock intent atomically converts children to independent placements — Dart handles visual transition.
Q: What is the slide-layout relationship? A: Slides are just whiteboards (child Whiteboard sessions in LessonPlan). Whiteboards can contain layouts. No special slide-layout treatment.
Unresolved Questions
Section titled “Unresolved Questions”-
Coercion implementation — Component Coercion RFC designs a general component coercion system. Implementation is pending.
-
CreateLayoutFromSelection — Not yet implemented. Should selection be by individual elements or by bounding box?
-
UnlockLayout — Not yet implemented. Should unlocked children inherit z-index from the layout placement?
-
write_grid_options_to_loro— Defined inloro_sync.rsbut not currently called. Grid option changes don’t persist through Loro yet (only through the config map on init).