Editor: interactive composition wireframe — drag to reorder and restructure

Drag a view's box (or Alt+up/down) to reorder it within its container; drag onto another view's edge to pair the two in a new row/column, move across containers, or insert. Each leaf shows a glyph of its mark type.

Core: spec-restructure.wrapViews (wrap, with flatten-to-insert, collapse, and data-pin invariants) and spec-insert.moveViewTo; the editor applies every drag as one undoable edit via AppStore.composeRequest, keeping the editor the single text source.
This commit is contained in:
2026-06-29 14:25:31 +03:00
parent 57604a80a6
commit 216797ff9b
16 changed files with 1160 additions and 56 deletions
+23 -5
View File
@@ -297,13 +297,24 @@ thin app-layer services.
- `spec-inline-data` — the rows a specific `data` binding carries for profiling
(`rowsForDataBinding`: inline `values`, or a self-defined `datasets` entry).
- `spec-insert` — the composition the cursor is in (`compositionTargetAt`), inserting a view
at an index (`insertView`) and reordering siblings (`moveView`), plus `elementOffset` to
re-find a view after the edit.
at an index (`insertView`) and reordering siblings (`moveView` swaps a neighbour, `moveViewTo`
slides to any index), plus `elementOffset` to re-find a view after the edit. It also owns the
shared `SpecPath` walkers (`valueAtPath`/`arrayAtPath`/`isPrefixPath`) — a module navigating a
path reuses these rather than re-inlining the array/object descent.
- `spec-view-tree` — the whole composition as a recursive tree (`viewTree`), each node carrying
its operator, orientation and byte range. Read at once (vs. `spec-insert`'s one-array edits) to
drive the composition wireframe — a read-only schematic of the multi-view structure in a preview-
toolbar disclosure (`CompositionWireframe`); clicking a box reveals that view's range in the
editor via `AppStore.requestRevealView`. Interaction contract in [arch 10](10-interaction-and-feedback.md).
drive the composition wireframe — a schematic of the multi-view structure in a preview-toolbar
disclosure (`CompositionWireframe`); clicking a box reveals that view's range in the editor via
`AppStore.requestRevealView`, and on the draft it is **drag-editable** (reorder + restructure).
Interaction contract in [arch 10](10-interaction-and-feedback.md).
- `spec-restructure` — the path-targeted cross-container moves behind the wireframe's drag.
`wrapViews(target, source, axis, side)` pairs the dragged source beside the drop target in a new
concat (placed where the target was, the source removed), enforcing three invariants:
**flatten** a bare same-orientation concat nested directly in a concat (so a with-axis drop reads
as a plain _insert_, not redundant nesting), **collapse** the source's emptied container (unwrap a
one-child, drop a zero-child, recursing up the chain), and **data-pin** a source's inherited
`data` before it changes ancestor (`dataBindingAtPath`, so it never silently rebinds). Degenerate
drops — onto itself, its own ancestor/descendant, or the root — return null.
**Services (app, store-aware via `getState`):** `spec-transform-actions` (the
wrap/simplify/add-view operations and their surfaces), `spec-dataset-hints` (completion,
@@ -325,6 +336,13 @@ Decision rules:
- **One edit path.** The lightbulb returns a `WorkspaceEdit` (no editor handle); the toolbar
and palette use `executeEdits`. Both build the replacement through the same
serialize-and-reindent step, bracketed by `pushUndoStop`, so ⌘Z restores the prior text.
- **Wireframe restructuring is one core op + the editor's undo.** Every drag resolves to either
`moveViewTo` (reorder within one container) or `wrapViews` (everything else — wrap, cross-container
move, insert); the wireframe only _requests_ it (`AppStore.requestComposeMove`/`requestComposeWrap`)
and `SpecEditor` applies it through the same `executeEdits` + `pushUndoStop` path as the other
transforms, so a drag is one ⌘Z and the editor stays the single text source. `wrapViews` covers
wrap and insert with one operation because it flattens bare same-orientation nesting afterward; the
drag's edge-zone interaction model lives in [arch 10](10-interaction-and-feedback.md).
- **Composition CodeLens is cursor-scoped.** It follows the view the cursor sits in —
` Add view above/below` at the view's edges and `↑/↓ Move` to reorder among its siblings —
rather than one fixed button per composition array; an empty composition shows a single ` Add view`,