|
| 1 | +# Editor / Layout Workspace Split |
| 2 | + |
| 3 | +**Date:** 2026-03-27 |
| 4 | +**Status:** Draft |
| 5 | +**Scope:** `packages/formspec-studio/` only — no external package changes |
| 6 | + |
| 7 | +## Problem |
| 8 | + |
| 9 | +The Studio Editor tab mixes Tier 1 (Definition) and Tier 3 (Component) concerns. It renders items by walking the component tree, shows layout containers (Card, Grid, Page) as LayoutBlocks, includes page navigation tabs for wizard/tabs mode, and supports wrap-in-Card/Grid operations. This creates bugs (Page nodes appearing as layout blocks on the Editor canvas) and confuses two distinct authoring intents: shaping the data structure vs arranging the visual layout. |
| 10 | + |
| 11 | +The Layout tab (currently `PagesTab`) already manages page structure — mode selection, page cards, grid region editing, field assignment — but lacks the visual canvas and drag-and-drop that the Editor provides. |
| 12 | + |
| 13 | +## Design Principles |
| 14 | + |
| 15 | +The split follows the Formspec specification's three-tier architecture (Core S2.3): |
| 16 | + |
| 17 | +- **Editor tab = Tier 1 Definition** — Structure Layer (what data is collected) + Behavior Layer (how data behaves). Answers: "what items exist, what types are they, how do they validate?" |
| 18 | +- **Layout tab = Tier 2 Theme + Tier 3 Component** — Presentation Layer (how data is displayed). Answers: "where do items go, what do they look like, how is the form paginated?" |
| 19 | + |
| 20 | +The spec mandates this separation as an "architectural invariant" (Core S2.3, line 627). Definition item array order is data-structural. Component tree children order is visual. Binding is by key, not position (Component S4.1). These are independent by design. |
| 21 | + |
| 22 | +## The New Editor Tab — Definition Tree Editor |
| 23 | + |
| 24 | +### Purpose |
| 25 | + |
| 26 | +Pure Tier 1 data-structure editor. Shows what data the form collects and how it behaves. Derives its view entirely from `definition.items` — never reads the component tree. |
| 27 | + |
| 28 | +### What It Shows |
| 29 | + |
| 30 | +A tree view of `definition.items`: |
| 31 | + |
| 32 | +- **Fields** — compact rows showing key, label, dataType badge |
| 33 | +- **Groups** — collapsible tree nodes with indented children |
| 34 | +- **Display items** — rows showing key, label, widgetHint badge (advisory) |
| 35 | +- **Bind indicators** — small pills/icons for required, calculated, conditional, readonly, constrained |
| 36 | +- No pages, no layout containers, no component tree structure |
| 37 | + |
| 38 | +### Interactions |
| 39 | + |
| 40 | +- Click item → select, open definition properties panel |
| 41 | +- `+ Add Item` → palette (fields, groups, display — no layout category) |
| 42 | +- Drag-to-reorder within tree → reorders `definition.items` array (data structure order) |
| 43 | +- Right-click → delete, duplicate, move into/out of group |
| 44 | +- Multi-select with Cmd+click, Shift+click (same patterns as current Editor) |
| 45 | + |
| 46 | +### Properties Panel (Right Sidebar) |
| 47 | + |
| 48 | +Definition and behavior properties only: |
| 49 | + |
| 50 | +| Section | Properties | |
| 51 | +|---------|-----------| |
| 52 | +| Identity | key, label, description, hint, type, dataType | |
| 53 | +| Field config | initialValue, prePopulate, semanticType, currency, precision, prefix, suffix | |
| 54 | +| Options | options (inline), optionSet, choicesFrom | |
| 55 | +| Group config | repeatable, minRepeat, maxRepeat | |
| 56 | +| Display content | label (body text), advisory `presentation.widgetHint` | |
| 57 | +| Binds | required, calculate, relevant, readonly, constraint, constraintMessage, default, whitespace, excludedValue | |
| 58 | +| Advisory hints | `presentation.layout.page` (shown as annotation — "assigned to page X") | |
| 59 | + |
| 60 | +Properties NOT shown (moved to Layout): widget override, style, CSS classes, grid span/start, responsive breakpoints, appearance/label-position overrides, component `when`. |
| 61 | + |
| 62 | +### What Gets Deleted from Editor |
| 63 | + |
| 64 | +- `render-tree-nodes.tsx` — component tree rendering |
| 65 | +- `LayoutBlock.tsx` — layout container blocks |
| 66 | +- `GroupTabs.tsx` — page navigation tabs |
| 67 | +- `FieldBlock.tsx`, `GroupBlock.tsx`, `DisplayBlock.tsx` — visual card blocks (replaced with compact tree rows) |
| 68 | +- `dnd/` directory — component-tree-order DnD (replaced with definition-order DnD) |
| 69 | +- `DragHandle.tsx` — extracted to `components/ui/` first (shared with Layout) |
| 70 | +- `canvas-operations.ts`, `block-utils.ts` — canvas utilities |
| 71 | +- `flattenStructural` and page-aware filtering — no longer needed |
| 72 | +- `properties/AppearanceSection.tsx` — moves to Layout |
| 73 | +- `properties/LayoutProperties.tsx` — moves to Layout |
| 74 | +- `properties/WidgetHintSection.tsx` — moves to Layout (queries component tree) |
| 75 | + |
| 76 | +## The New Layout Tab — Visual Form Builder |
| 77 | + |
| 78 | +### Purpose |
| 79 | + |
| 80 | +Tier 2+3 visual builder. Shows how the form looks and is organized. Built from scratch — not migrated from the current Editor canvas. Absorbs all functionality from the current `PagesTab`. |
| 81 | + |
| 82 | +### What It Shows |
| 83 | + |
| 84 | +The component tree rendered as a visual canvas: |
| 85 | + |
| 86 | +- **Page nodes** — first-class visible sections with titles, rendered as large section blocks |
| 87 | +- **Layout containers** — Card, Grid, Panel, Columns as dashed-border wrappers with children |
| 88 | +- **Field blocks** — bound items showing label and widget type |
| 89 | +- **Display blocks** — display items showing label and component type (Heading, Text, Divider) |
| 90 | +- **Page navigation** — wizard step bar or tab strip depending on `pageMode` |
| 91 | +- **Mode selector** — single / wizard / tabs (absorbed from current PagesTab) |
| 92 | +- **Unassigned items tray** — definition items not yet placed in the component tree |
| 93 | + |
| 94 | +### Unassigned Items Tray |
| 95 | + |
| 96 | +The tray shows definition items that exist but are not bound in the component tree. Per Component S4.5, required unbound items MUST be rendered at runtime; non-required MAY be omitted. The tray distinguishes between these: |
| 97 | + |
| 98 | +- **Required unassigned** — highlighted, since the rendered form will auto-append them as fallback inputs |
| 99 | +- **Non-required unassigned** — dimmed, since they may not appear in the rendered form |
| 100 | + |
| 101 | +Dragging an item from the tray into the canvas creates a component node with `bind` set to the item's key. |
| 102 | + |
| 103 | +### Interactions |
| 104 | + |
| 105 | +- Drag-and-drop reorder within component tree (visual order, not data order) |
| 106 | +- Drag from unassigned tray onto pages/containers |
| 107 | +- Right-click → wrap in Card/Grid/Panel, unwrap, move to page |
| 108 | +- Add Page button, page reordering, page deletion |
| 109 | +- Grid region editing (span/start within pages) |
| 110 | + |
| 111 | +### Properties Panel (Right Sidebar) |
| 112 | + |
| 113 | +Presentation properties only: |
| 114 | + |
| 115 | +| Section | Properties | |
| 116 | +|---------|-----------| |
| 117 | +| Widget | component type selector (actual override, not advisory hint) | |
| 118 | +| Appearance | style overrides, CSS classes, labelPosition override | |
| 119 | +| Layout | grid span, start, responsive breakpoint overrides | |
| 120 | +| Container | direction, gap, columns (for Grid/Stack/Columns containers) | |
| 121 | +| Conditional | component `when` expression (visual-only — does NOT clear data) | |
| 122 | +| Accessibility | component-level a11y overrides | |
| 123 | + |
| 124 | +Properties NOT shown (stay in Editor): key, label, dataType, options, binds, repeat config. |
| 125 | + |
| 126 | +### Cross-Tier Write: pageMode |
| 127 | + |
| 128 | +The Layout tab's mode selector writes `definition.formPresentation.pageMode` to the Definition document. This is a cross-tier write — a presentation control modifying a Tier 1 property. This is architecturally defensible because `pageMode` is explicitly advisory (Core S4.1.1) and exists solely to control pagination behavior. The Layout tab also writes `formPresentation.showProgress`, `allowSkip`, `defaultTab`, `tabPosition` — all mode-dependent behavioral settings that are only meaningful when `pageMode` is non-`single`. |
| 129 | + |
| 130 | +### Component `when` vs Bind `relevant` |
| 131 | + |
| 132 | +The Layout tab exposes component `when` expressions. The Editor tab exposes bind `relevant` expressions. Per Component S8.2, these have fundamentally different data-model semantics: |
| 133 | + |
| 134 | +- **`when` (Layout)** — visual only. Hidden component's bound data is preserved in the instance. |
| 135 | +- **`relevant` (Editor)** — affects data model. Non-relevant items MAY be removed from the instance per `nonRelevantBehavior`. |
| 136 | + |
| 137 | +The Layout properties panel should clearly label `when` as "Visual Condition (data preserved)" to prevent confusion with `relevant`. |
| 138 | + |
| 139 | +## Shell & Routing Changes |
| 140 | + |
| 141 | +### Tab Bar |
| 142 | + |
| 143 | +No tab additions or removals. Same seven tabs: Editor, Logic, Data, Layout, Theme, Mapping, Preview. |
| 144 | + |
| 145 | +### WORKSPACES Map |
| 146 | + |
| 147 | +```typescript |
| 148 | +const WORKSPACES: Record<string, React.FC> = { |
| 149 | + Editor: DefinitionTreeEditor, // NEW — replaces EditorCanvas |
| 150 | + Logic: LogicTab, |
| 151 | + Data: DataTab, |
| 152 | + Layout: LayoutCanvas, // NEW — replaces PagesTab |
| 153 | + Theme: ThemeTab, |
| 154 | + Mapping: MappingTab, |
| 155 | + Preview: PreviewTab, |
| 156 | +}; |
| 157 | +``` |
| 158 | + |
| 159 | +### Shared State |
| 160 | + |
| 161 | +| Hook | Editor | Layout | Notes | |
| 162 | +|------|--------|--------|-------| |
| 163 | +| `useDefinition` | Yes | Yes | Shared — both read definition | |
| 164 | +| `useComponent` | No | Yes | Layout only — Editor never reads component tree | |
| 165 | +| `useSelection` | Yes | Yes | Shared SelectionProvider; both tabs select by path | |
| 166 | +| `useActiveGroup` | No | Yes | Layout only — page navigation is a Layout concern | |
| 167 | +| `useProject` | Yes | Yes | Shared — both dispatch commands | |
| 168 | +| `useCanvasTargets` | Yes | Yes | Shared — scroll-to-target in both | |
| 169 | +| `useTheme` | No | Yes | Layout only — theme cascade resolution | |
| 170 | +| `usePageStructure` | No | Yes | Layout only — from current PagesTab | |
| 171 | + |
| 172 | +### Properties Panel in Shell |
| 173 | + |
| 174 | +The right sidebar currently renders `ItemProperties` for all workspaces. In the new design: |
| 175 | + |
| 176 | +- When **Editor** is active → render `DefinitionProperties` (Tier 1 props) |
| 177 | +- When **Layout** is active → render `ComponentProperties` (Tier 2/3 props) |
| 178 | +- When other tabs are active → keep current behavior (or hide panel) |
| 179 | + |
| 180 | +The Shell routes based on `activeTab`, not item selection. |
| 181 | + |
| 182 | +### Item Addition from Both Tabs |
| 183 | + |
| 184 | +Items can be created from either tab: |
| 185 | + |
| 186 | +- **Editor** `+ Add Item` → creates a definition item. Appears as unassigned in Layout until placed. |
| 187 | +- **Layout** add-from-palette → creates a definition item AND places it in the component tree in one operation (dispatches `definition.addItem` + `pages.assignItem` atomically, same as `project.addPage` does today). |
| 188 | + |
| 189 | +## File Structure |
| 190 | + |
| 191 | +``` |
| 192 | +workspaces/ |
| 193 | + editor/ # REWRITTEN — definition tree editor |
| 194 | + DefinitionTreeEditor.tsx # Main — tree view of definition.items |
| 195 | + ItemRow.tsx # Compact tree row for field/group/display |
| 196 | + GroupNode.tsx # Collapsible group with children |
| 197 | + AddItemPalette.tsx # Reuse existing, minus layout category |
| 198 | + EditorContextMenu.tsx # Simplified — no wrap/unwrap, no layout actions |
| 199 | + dnd/ # Definition-order DnD (reorders items array) |
| 200 | + properties/ |
| 201 | + DefinitionProperties.tsx # Properties panel root |
| 202 | + FieldConfigSection.tsx # Stays — strip any presentation props |
| 203 | + OptionsSection.tsx # Stays as-is |
| 204 | + GroupConfigSection.tsx # Stays as-is |
| 205 | + ContentSection.tsx # Stays — strip widget override |
| 206 | + BindsInlineSection.tsx # New — inline bind editing for selected item |
| 207 | + MultiSelectSummary.tsx # Stays — batch delete/duplicate |
| 208 | + shared.tsx # Stays — PropInput, AddPlaceholder |
| 209 | +
|
| 210 | + layout/ # NEW — replaces pages/ |
| 211 | + LayoutCanvas.tsx # Main — component tree visual builder |
| 212 | + CanvasBlock.tsx # Renders any component tree node |
| 213 | + PageSection.tsx # Page node as a titled section |
| 214 | + LayoutContainer.tsx # Card/Grid/Panel/Columns wrapper |
| 215 | + FieldBlock.tsx # Bound field as a visual card |
| 216 | + DisplayBlock.tsx # Display item visual block |
| 217 | + UnassignedTray.tsx # Items not placed in component tree |
| 218 | + ModeSelector.tsx # Single/wizard/tabs (from PagesTab) |
| 219 | + PageNav.tsx # Wizard step bar or tab bar |
| 220 | + dnd/ # Component-tree-order DnD |
| 221 | + properties/ |
| 222 | + ComponentProperties.tsx # Properties panel root |
| 223 | + WidgetSection.tsx # Component type selector |
| 224 | + AppearanceSection.tsx # Style, CSS classes (migrated from editor) |
| 225 | + LayoutSection.tsx # Grid span, responsive |
| 226 | + ContainerSection.tsx # Direction, gap, columns |
| 227 | +``` |
| 228 | + |
| 229 | +### Deleted Entirely |
| 230 | + |
| 231 | +- `workspaces/editor/FieldBlock.tsx` |
| 232 | +- `workspaces/editor/GroupBlock.tsx` |
| 233 | +- `workspaces/editor/DisplayBlock.tsx` |
| 234 | +- `workspaces/editor/LayoutBlock.tsx` |
| 235 | +- `workspaces/editor/render-tree-nodes.tsx` |
| 236 | +- `workspaces/editor/GroupTabs.tsx` |
| 237 | +- `workspaces/editor/DragHandle.tsx` (extracted to `components/ui/`) |
| 238 | +- `workspaces/editor/canvas-operations.ts` |
| 239 | +- `workspaces/editor/block-utils.ts` |
| 240 | +- `workspaces/editor/dnd/` (old component-tree DnD) |
| 241 | +- `workspaces/editor/properties/AppearanceSection.tsx` (moves to layout) |
| 242 | +- `workspaces/editor/properties/LayoutProperties.tsx` (moves to layout) |
| 243 | +- `workspaces/editor/properties/WidgetHintSection.tsx` (moves to layout) |
| 244 | +- `workspaces/pages/` (entire directory) |
| 245 | + |
| 246 | +### Extracted to `components/ui/` |
| 247 | + |
| 248 | +- `DragHandle.tsx` — shared between Editor (definition reorder) and Layout (component tree reorder) |
| 249 | + |
| 250 | +## Concerns Not Affected by This Split |
| 251 | + |
| 252 | +These Tier 1 behavioral constructs already live in dedicated tabs and are not part of the Editor/Layout split: |
| 253 | + |
| 254 | +- **Variables** — Logic tab (`VariablesSection`) |
| 255 | +- **Shapes** — Logic tab (`ShapesSection`) |
| 256 | +- **Instances / Data Sources** — Data tab (`DataSources`) |
| 257 | +- **Option Sets** — Data tab (`OptionSets`) |
| 258 | +- **Screener** — Blueprint sidebar (`ScreenerSection`) |
| 259 | +- **Mappings** — Mapping tab |
| 260 | +- **Migrations** — not yet implemented; separate concern |
| 261 | + |
| 262 | +## Testing Strategy |
| 263 | + |
| 264 | +### Preserve Test IDs |
| 265 | + |
| 266 | +The new Editor tree rows MUST emit the same `data-testid` patterns as the current blocks: |
| 267 | + |
| 268 | +- `data-testid="field-{key}"` on field rows |
| 269 | +- `data-testid="group-{key}"` on group nodes |
| 270 | +- `data-testid="display-{key}"` on display rows |
| 271 | +- `data-testid="add-item"` on the Add Item button |
| 272 | + |
| 273 | +This minimizes E2E test breakage. Layout-specific test IDs (`layout-{nodeId}`, wrap/unwrap actions) move to Layout tab tests. |
| 274 | + |
| 275 | +### Unit Tests |
| 276 | + |
| 277 | +- `DefinitionTreeEditor` — renders items tree from definition, add/delete/reorder changes definition array order, no component tree awareness |
| 278 | +- `LayoutCanvas` — renders component tree, Page nodes visible as sections, DnD reorders component tree children, unassigned tray shows unbound items |
| 279 | +- `DefinitionProperties` — shows only Tier 1 properties for selected item |
| 280 | +- `ComponentProperties` — shows only Tier 2/3 properties for selected component node |
| 281 | + |
| 282 | +### Integration Tests |
| 283 | + |
| 284 | +- Adding item in Editor → appears as unassigned in Layout |
| 285 | +- Placing unassigned item on a page in Layout → does not change definition order |
| 286 | +- Reordering in Editor → changes `definition.items` order, does not affect component tree |
| 287 | +- Reordering in Layout → changes component tree order, does not affect definition items |
| 288 | +- Properties split: selecting item in Editor shows definition props; selecting in Layout shows component props |
| 289 | + |
| 290 | +### E2E Tests |
| 291 | + |
| 292 | +- Full authoring workflow: create form in Editor → switch to Layout → arrange on pages → Preview renders correctly |
| 293 | +- Existing E2E tests updated: layout-specific tests (`layout-components.spec.ts`, wrap/unwrap) navigate to Layout tab instead of Editor |
| 294 | +- Wizard/tabs mode tests navigate to Layout tab for page management |
| 295 | + |
| 296 | +### Test Files Affected |
| 297 | + |
| 298 | +**Editor unit tests (rewrite):** 13 files in `tests/workspaces/editor/` |
| 299 | +**Pages unit tests (rewrite):** 7 files in `tests/workspaces/pages/` |
| 300 | +**Integration tests (update):** `editor-workflow`, `import-export`, `undo-redo` |
| 301 | +**E2E tests (update selectively):** ~13 files — primarily `editor-authoring`, `layout-components`, `interaction-patterns`, `wizard-mode` |
| 302 | + |
| 303 | +## Migration |
| 304 | + |
| 305 | +No data migration required. The definition and component documents are unchanged — only the UI surface that edits each part is relocated. Existing projects load identically. |
| 306 | + |
| 307 | +## Spec References |
| 308 | + |
| 309 | +- Core S2.3 — Three-layer separation of concerns (architectural invariant) |
| 310 | +- Core S4.1.1 — `formPresentation` (advisory, does not participate in processing model) |
| 311 | +- Core S4.2.5 — `presentation` hints (advisory, MUST NOT affect data semantics) |
| 312 | +- Core S4.3 — Binds (behavioral layer) |
| 313 | +- Component S3.3 — Children array ordering (renderers MUST preserve) |
| 314 | +- Component S4.1 — Slot binding by key (not position) |
| 315 | +- Component S4.5 — Unbound required items (MUST render as fallback) |
| 316 | +- Component S8.2 — `when` vs `relevant` (visual-only vs data-affecting) |
| 317 | +- Component S11.3 — Cross-tier cascade precedence |
| 318 | +- Theme S5.1 — Cascade levels (Tier 1 < Tier 2 < Tier 3) |
| 319 | +- Theme S6.3 — Unassigned items rendering policy |
0 commit comments