Element CRUD: add, remove, reorder, and nest elements with placement rules #6

Closed
opened 2026-07-11 12:59:34 +02:00 by dominik.polakovics · 3 comments

What to build

Structural editing of a page's element list (docs/DECISIONS.md D4, D6): a "new element" picker grouped by schema group, honoring placement.allowedIn/allowedAt and the page type's allowed elements; delete with confirm; reorder via drag & drop; nesting into container elements (grid columns) with the same rule enforcement. All mutations go through the element tree and canonical serializer.

Acceptance criteria

  • Picker offers only elements allowed at the chosen position (page type + placement rules); disallowed drops are rejected visually during drag
  • Add inserts with schema defaults; delete removes the element and its children after confirmation
  • Drag & drop reorders top-level elements and moves elements into/out of grid columns when rules allow
  • Every mutation persists through the canonical serializer and shows in the preview
  • Undo of the last structural mutation (single-step) works in the session

Blocked by

## What to build Structural editing of a page's element list (docs/DECISIONS.md D4, D6): a "new element" picker grouped by schema `group`, honoring `placement.allowedIn`/`allowedAt` and the page type's allowed elements; delete with confirm; reorder via drag & drop; nesting into container elements (grid columns) with the same rule enforcement. All mutations go through the element tree and canonical serializer. ## Acceptance criteria - [ ] Picker offers only elements allowed at the chosen position (page type + placement rules); disallowed drops are rejected visually during drag - [ ] Add inserts with schema defaults; delete removes the element and its children after confirmation - [ ] Drag & drop reorders top-level elements and moves elements into/out of grid columns when rules allow - [ ] Every mutation persists through the canonical serializer and shows in the preview - [ ] Undo of the last structural mutation (single-step) works in the session ## Blocked by - #3
Author
Owner

This was generated by AI during triage.

Agent Brief

Category: enhancement
Summary: Structural editing of a page's element tree: a "new element" picker grouped by sidecar group, delete with confirmation, drag & drop reordering, and nesting into container elements — all governed by placement rules (placement.allowedIn/allowedAt from sidecars plus the page type's allowed elements), all persisted through the element tree and canonical serializer, with single-step undo.

Dependency note: this issue is hard-blocked by #3 (schema-generated element form and the canonical save path); the scheduler handles ordering — do not start until #3 is merged. #3 provides the sidecar schema loader (label, icon, group, params, and a placement block that #3 parses and serves but explicitly does not enforce), the harness API that serves schemas and a page's element tree, and the save path through the round-trip engine from #2. This issue is where placement enforcement lands. The issue body's docs/DECISIONS.md D4, D6 references point at a file that is not committed — this brief is self-contained.

Current behavior:
After #3, the tool can edit the params of an element that already exists on a page and save the file canonically, with the preview updating via LiveReload. Sidecars declare a placement block, but it is parsed and served without semantics. There is no way to add an element to a page, delete one, change element order, or move an element into or out of a container — the page's structure can only be changed by editing the markdown file by hand. There is no undo of any kind.

Desired behavior:

Placement model. Since the decisions doc is absent, this brief fixes the semantics. Two rule sources combine, and an operation is allowed only when every declared constraint passes:

  1. Sidecar placement (per element type): allowedIn lists the parents that may contain the element — names of container element types, plus a designated token for the page top level (e.g. page). allowedAt lists the page types on which the element may appear. An absent or empty list on either axis means "unrestricted on that axis" (consistent with #3's permissive placement: {}).
  2. Page-type allowed elements: the tool knows, per page, which element types are allowed on it. A page's type follows Hugo convention (front matter type if set, otherwise its top-level section, otherwise a default), and a minimal editor-side declaration maps page type → allowed elements, styled after the existing editor/ sidecar convention. A page type with no declaration allows all elements. This is the minimal mechanism only — full page-type management is #9.

Exact key naming and config location are the implementer's choice as long as both axes (allowed parents, allowed page types) plus the page-type allowlist are representable, enforced, and covered by tests.

Picker. A "new element" affordance at a chosen insertion position (top level between siblings, or inside a container) opens a picker of element types grouped by the sidecar group, showing each type's schema label and icon. The picker offers only the types allowed at that position under the combined rules; nothing disallowed is listed (or it is visibly disabled — but never insertable). Choosing a type inserts a new element at that position with all params set to their schema defaults (required params without defaults left for the form to demand) and an empty body where the body policy allows one, then persists and shows the element form for the new element.

Delete. Deleting an element asks for confirmation, naming the element (schema label), and warning when it has children. Confirmation removes the element and its entire subtree; cancel is a no-op.

Drag & drop. Elements can be dragged to reorder among top-level siblings and to move into or out of container elements (e.g. grid columns), including into an empty container. While dragging, valid drop positions show an insertion indicator; positions the rules disallow are visually rejected (no-drop affordance) and dropping there changes nothing. A drop commits the move as one mutation.

Nesting. Container semantics come from the rules above: a container accepts a child only if the child's allowedIn admits that container (and the page-type allowlist admits the child). Enforcement applies identically to picker insertion and drag/drop moves.

Persistence. Every mutation — insert, delete, reorder, move — goes through the element tree and the canonical serializer from #2 and rewrites only that page's file. Text nodes and untouched elements survive unchanged (modulo canonicalization). Nothing mutates markdown as strings. After each successful mutation the preview reflects the change via LiveReload without manual reload. The server is authoritative: a mutation request that violates placement or page-type rules is rejected server-side with an actionable error and leaves the file untouched, regardless of what the client UI allowed.

Undo. The session keeps the pre-mutation state of the last structural mutation. A single undo action reverts that mutation — the file is rewritten (canonically) to its prior structure and the preview updates. One step only: undoing twice does not walk further back, and redo is not required. Undo is scoped to structural mutations; param-edit undo is not required.

Key interfaces:

  • The placement shape in the sidecar schema type from #3allowedIn (allowed parents; container names plus a top-level token) and allowedAt (allowed page types), both optional, absent meaning unrestricted. The validation logic that answers "may element type X sit at position P on page Y?" should live where both the server (authoritative) and any future callers can use it, and be unit-testable without HTTP.
  • A minimal page-type declaration: page type → allowed element types, resolved per page via front matter type / section / default.
  • Mutation API contract: page identifier + a structural operation (insert type at position, delete node, move node to position) in; validated, persisted through the canonical serializer; rule violations returned as errors the UI can display. Since tree nodes carry no IDs, positions need a stable addressing scheme (e.g. child-index paths into the parsed tree) that the client and server share.
  • Undo contract: the last structural mutation per session can be reverted through the same save path.

Acceptance criteria:

  • The picker at a given position offers exactly the element types allowed there by the combination of page-type allowlist and placement.allowedIn/allowedAt; rules are enforced server-side too, and a rule-violating mutation request is rejected without touching the file
  • Adding an element inserts it at the chosen position with schema defaults and persists canonically
  • Deleting an element requires confirmation and removes the element with its whole subtree
  • Drag & drop reorders top-level elements and moves elements into/out of a container (including an empty one); disallowed drop targets are visually rejected during the drag and a drop there is a no-op
  • Every mutation persists through the canonical serializer, rewrites only that page's file, and shows in the preview without manual reload; surrounding text nodes and untouched elements are preserved
  • A single-step undo reverts the last structural mutation in the session and the preview reflects it; a second undo does not walk further back
  • The sample site includes a container element type (template + sidecar, e.g. a grid with columns) and placement declarations sufficient to exercise allowed and disallowed cases; the flows above are covered by tests

Out of scope:

  • Page tree browsing and the real element-list navigation (#4) — reuse whatever minimal element list #3 left behind
  • Editing element params or bodies beyond what insertion touches — the form itself is #3, rich text is #7, inline chips are #8
  • Page-level operations and full page-type management — create/rename/move/delete/reorder pages is #9; only the minimal page type → allowed elements mapping lands here
  • The full v1 element library (#14) and theme styling (#15) — only what the sample site needs to exercise nesting
  • Multi-step undo history or redo
  • Git commits on save (#10), media handling (#12), locks/identity/CSRF (#13), click-to-select from the preview (#16)
> *This was generated by AI during triage.* ## Agent Brief **Category:** enhancement **Summary:** Structural editing of a page's element tree: a "new element" picker grouped by sidecar `group`, delete with confirmation, drag & drop reordering, and nesting into container elements — all governed by placement rules (`placement.allowedIn`/`allowedAt` from sidecars plus the page type's allowed elements), all persisted through the element tree and canonical serializer, with single-step undo. **Dependency note:** this issue is **hard-blocked by #3** (schema-generated element form and the canonical save path); the scheduler handles ordering — do not start until #3 is merged. #3 provides the sidecar schema loader (`label`, `icon`, `group`, `params`, and a `placement` block that #3 parses and serves but explicitly does **not** enforce), the harness API that serves schemas and a page's element tree, and the save path through the round-trip engine from #2. This issue is where placement enforcement lands. The issue body's `docs/DECISIONS.md D4, D6` references point at a file that is not committed — this brief is self-contained. **Current behavior:** After #3, the tool can edit the params of an element that already exists on a page and save the file canonically, with the preview updating via LiveReload. Sidecars declare a `placement` block, but it is parsed and served without semantics. There is no way to add an element to a page, delete one, change element order, or move an element into or out of a container — the page's structure can only be changed by editing the markdown file by hand. There is no undo of any kind. **Desired behavior:** *Placement model.* Since the decisions doc is absent, this brief fixes the semantics. Two rule sources combine, and an operation is allowed only when every declared constraint passes: 1. **Sidecar `placement`** (per element type): `allowedIn` lists the parents that may contain the element — names of container element types, plus a designated token for the page top level (e.g. `page`). `allowedAt` lists the page types on which the element may appear. An absent or empty list on either axis means "unrestricted on that axis" (consistent with #3's permissive `placement: {}`). 2. **Page-type allowed elements**: the tool knows, per page, which element types are allowed on it. A page's type follows Hugo convention (front matter `type` if set, otherwise its top-level section, otherwise a default), and a minimal editor-side declaration maps page type → allowed elements, styled after the existing `editor/` sidecar convention. A page type with no declaration allows all elements. This is the minimal mechanism only — full page-type management is #9. Exact key naming and config location are the implementer's choice as long as both axes (allowed parents, allowed page types) plus the page-type allowlist are representable, enforced, and covered by tests. *Picker.* A "new element" affordance at a chosen insertion position (top level between siblings, or inside a container) opens a picker of element types grouped by the sidecar `group`, showing each type's schema `label` and `icon`. The picker offers **only** the types allowed at that position under the combined rules; nothing disallowed is listed (or it is visibly disabled — but never insertable). Choosing a type inserts a new element at that position with all params set to their schema defaults (required params without defaults left for the form to demand) and an empty body where the body policy allows one, then persists and shows the element form for the new element. *Delete.* Deleting an element asks for confirmation, naming the element (schema label), and warning when it has children. Confirmation removes the element and its entire subtree; cancel is a no-op. *Drag & drop.* Elements can be dragged to reorder among top-level siblings and to move into or out of container elements (e.g. grid columns), including into an empty container. While dragging, valid drop positions show an insertion indicator; positions the rules disallow are visually rejected (no-drop affordance) and dropping there changes nothing. A drop commits the move as one mutation. *Nesting.* Container semantics come from the rules above: a container accepts a child only if the child's `allowedIn` admits that container (and the page-type allowlist admits the child). Enforcement applies identically to picker insertion and drag/drop moves. *Persistence.* Every mutation — insert, delete, reorder, move — goes through the element tree and the canonical serializer from #2 and rewrites only that page's file. Text nodes and untouched elements survive unchanged (modulo canonicalization). Nothing mutates markdown as strings. After each successful mutation the preview reflects the change via LiveReload without manual reload. The server is authoritative: a mutation request that violates placement or page-type rules is rejected server-side with an actionable error and leaves the file untouched, regardless of what the client UI allowed. *Undo.* The session keeps the pre-mutation state of the last structural mutation. A single undo action reverts that mutation — the file is rewritten (canonically) to its prior structure and the preview updates. One step only: undoing twice does not walk further back, and redo is not required. Undo is scoped to structural mutations; param-edit undo is not required. **Key interfaces:** - The `placement` shape in the sidecar schema type from #3 — `allowedIn` (allowed parents; container names plus a top-level token) and `allowedAt` (allowed page types), both optional, absent meaning unrestricted. The validation logic that answers "may element type X sit at position P on page Y?" should live where both the server (authoritative) and any future callers can use it, and be unit-testable without HTTP. - A minimal page-type declaration: page type → allowed element types, resolved per page via front matter `type` / section / default. - Mutation API contract: page identifier + a structural operation (insert type at position, delete node, move node to position) in; validated, persisted through the canonical serializer; rule violations returned as errors the UI can display. Since tree nodes carry no IDs, positions need a stable addressing scheme (e.g. child-index paths into the parsed tree) that the client and server share. - Undo contract: the last structural mutation per session can be reverted through the same save path. **Acceptance criteria:** - [ ] The picker at a given position offers exactly the element types allowed there by the combination of page-type allowlist and `placement.allowedIn`/`allowedAt`; rules are enforced server-side too, and a rule-violating mutation request is rejected without touching the file - [ ] Adding an element inserts it at the chosen position with schema defaults and persists canonically - [ ] Deleting an element requires confirmation and removes the element with its whole subtree - [ ] Drag & drop reorders top-level elements and moves elements into/out of a container (including an empty one); disallowed drop targets are visually rejected during the drag and a drop there is a no-op - [ ] Every mutation persists through the canonical serializer, rewrites only that page's file, and shows in the preview without manual reload; surrounding text nodes and untouched elements are preserved - [ ] A single-step undo reverts the last structural mutation in the session and the preview reflects it; a second undo does not walk further back - [ ] The sample site includes a container element type (template + sidecar, e.g. a grid with columns) and placement declarations sufficient to exercise allowed and disallowed cases; the flows above are covered by tests **Out of scope:** - Page tree browsing and the real element-list navigation (#4) — reuse whatever minimal element list #3 left behind - Editing element params or bodies beyond what insertion touches — the form itself is #3, rich text is #7, inline chips are #8 - Page-level operations and full page-type management — create/rename/move/delete/reorder pages is #9; only the minimal page type → allowed elements mapping lands here - The full v1 element library (#14) and theme styling (#15) — only what the sample site needs to exercise nesting - Multi-step undo history or redo - Git commits on save (#10), media handling (#12), locks/identity/CSRF (#13), click-to-select from the preview (#16)
Author
Owner

This was generated by AI while landing a PR.

Re-run brief — this issue must be rebuilt against the current main

The first AFK run on this issue (branch afk/6, PR #23, head 7cb4e5d) is being abandoned, not merged. It is correct work built on a floor that has since been replaced. Read this before starting; it supersedes the original Agent Brief above where they conflict.

Why the first run could not land

The original brief told this issue to "reuse whatever minimal element list #3 left behind" and put #4 (page tree browsing) out of scope. The run did exactly that — it built element CRUD on top of #3's web/src/components/ElementsPane.tsx.

Since then #4 landed (PR #21) and deleted ElementsPane.tsx, replacing the editor with a multi-pane architecture. #6 and #4 both reshape the element-list surface, but the tracker never recorded a #6 → #4 dependency, so they were built concurrently against divergent bases. Result: a modify/delete conflict on the central component plus deep, independent rewrites of web/src/api.ts and internal/server/server.go. Re-integration by hand would mean re-authoring the feature's UI, so the work is being regenerated on the current floor instead.

Those two instructions in the original brief are now void: #4 is merged, and the element list you must build on is #4's — there is no "minimal element list from #3" any more.

The floor you are building on (current main)

Frontend (web/src/):

  • Panes: components/TreePane.tsx (page navigation, left) · components/PagePane.tsx (the element list, center — FormPane mounts inside it) · components/FormPane.tsx (schema-generated param form) · components/PreviewPane.tsx · components/StatusPill.tsx. ElementsPane.tsx does not exist. Do not recreate it.
  • selection.ts — the shared selection seam: PageId = {path, lang}, ElementSelection = {page, elementPath}, createSelection(){selectedPage, selectPage, selectedElement, selectElement}.
  • schemas.ts, and test/mockApi.ts (installMockApi()) — the shared test mock. Tests select a page from the tree first; the element list is empty until then.
  • Document model is a discriminated union: ElementNode = BlockNode | InlineNode | TextNode; children exist only on BlockNode (narrow with node.kind === "block" before touching .children).

Backend (internal/):

  • Page identity is the pair (path, lang), never a bare path string. GET /api/page?path=&lang={page: {path, lang}, tree}; POST /api/page/save body {page, lang, path, name, values}. GET /api/tree?lang= is the site nav tree (internal/pagetree).
  • internal/pagetree.ContentRoot(contentDir, lang) is the single place a language resolves to a content root — handlers never join contentDir themselves.
  • server.New(provider, dist, siteDir, hugo hugobin.Resolved) — 4 args (Hugo pinning, #5).

Where this feature belongs now

Element CRUD operates on the element list, and the element list lives in PagePane. Build the picker / insert / delete / drag-&-drop / undo affordances into PagePane (extract a sub-component if it grows large), with TreePane remaining pure page navigation. Selection changes go through selection.ts.

What to salvage from the abandoned run (PR #23, 7cb4e5d)

That run's backend and pure-logic modules are good and largely portable — reuse them rather than reinventing:

  • internal/placement/Rules (CanInsert/CanMove/AllowedOnPage), PageType(), allowlist loading. Ports verbatim.
  • internal/server/tree.go — tree mechanics (shortcodeAt, pathOf, spliceInto, removeAt, normalizeText, …). Pure, no page-identity coupling. Ports verbatim.
  • internal/server/mutate.go / undo.go — the insert/delete/move + single-step undo logic. Sound; needs (path, lang) threading (below).
  • web/src/rules.ts — picker filtering + computeDrop/canMoveTo drag logic, and its unit tests (drop.test.ts, rules.test.ts). Ports with ~2 edits (narrow on kind === "block" before .children; type placement properly).

Backend integration constraints (verified — these all compile)

  1. element.Schema.Placement must be a typed struct, not map[string]any:
    type Placement struct { AllowedIn []string; AllowedAt []string }. Nothing in placement/ compiles otherwise. This is the linchpin — do it first (elementplacementserver).
  2. Extend the page response rather than adding a parallel endpoint — one union type serves /api/page, /save, /mutate, /undo:
    type pageResponse struct {
        Page            pageIdentity      `json:"page"`             // {path, lang}
        Tree            *content.Document `json:"tree"`
        PageType        string            `json:"pageType"`
        AllowedElements []string          `json:"allowedElements"`
        Path            []int             `json:"path,omitempty"`   // inserted/moved node
    }
    
  3. Thread lang through mutate/undo. mutateRequest needs a Lang field; the undo snapshot must carry lang and the resolved absolute path (so undo neither re-resolves the language nor re-derives the content root).
  4. Handler gains pagetypesDir, mu sync.Mutex, undo *undoSnapshotkeep the existing hugo hugobin.Resolved field and the 4-arg New.
  5. placement.PageType splits on / — pass it the slash form of the path.

Decide these coherently (the first run had no page tree, so it never faced them)

  • Undo scope across pages. The server undo slot is a single global step. With a page tree you can navigate away — decide and document whether the Undo control is page-scoped (disabled when you're not on the mutated page) or reflects the global last step. Pick one and make the UI honest about it.
  • Where the Undo and "add element" controls live now that the left pane is the page tree.
  • Whether FormPane.onSaved keeps passing the tree or the full page response.

Acceptance criteria

Unchanged from the original brief (picker honoring placement rules, add with schema defaults, delete-with-confirm incl. subtree, drag-&-drop reorder/nest with invalid drops visually rejected and server-side enforced, canonical-serializer persistence visible in preview, single-step undo). Additionally: the feature must integrate with #4's tree navigation and (path, lang) identity, and all existing tests on main must stay green.

> *This was generated by AI while landing a PR.* ## Re-run brief — this issue must be rebuilt against the current `main` The first AFK run on this issue (branch `afk/6`, PR #23, head `7cb4e5d`) is **being abandoned, not merged.** It is correct work built on a floor that has since been replaced. **Read this before starting; it supersedes the original Agent Brief above where they conflict.** ### Why the first run could not land The original brief told this issue to *"reuse whatever minimal element list #3 left behind"* and put **#4 (page tree browsing) out of scope**. The run did exactly that — it built element CRUD on top of #3's `web/src/components/ElementsPane.tsx`. Since then **#4 landed (PR #21)** and **deleted `ElementsPane.tsx`**, replacing the editor with a multi-pane architecture. #6 and #4 both reshape the element-list surface, but the tracker never recorded a `#6 → #4` dependency, so they were built concurrently against divergent bases. Result: a `modify/delete` conflict on the central component plus deep, independent rewrites of `web/src/api.ts` and `internal/server/server.go`. Re-integration by hand would mean re-authoring the feature's UI, so the work is being regenerated on the current floor instead. **Those two instructions in the original brief are now void:** #4 is *merged*, and the element list you must build on is #4's — there is no "minimal element list from #3" any more. ### The floor you are building on (current `main`) **Frontend** (`web/src/`): - Panes: `components/TreePane.tsx` (page navigation, left) · `components/PagePane.tsx` (**the element list**, center — `FormPane` mounts *inside* it) · `components/FormPane.tsx` (schema-generated param form) · `components/PreviewPane.tsx` · `components/StatusPill.tsx`. **`ElementsPane.tsx` does not exist. Do not recreate it.** - `selection.ts` — the shared selection seam: `PageId = {path, lang}`, `ElementSelection = {page, elementPath}`, `createSelection()` → `{selectedPage, selectPage, selectedElement, selectElement}`. - `schemas.ts`, and `test/mockApi.ts` (`installMockApi()`) — the shared test mock. Tests select a page from the tree first; the element list is empty until then. - Document model is a **discriminated union**: `ElementNode = BlockNode | InlineNode | TextNode`; children exist **only on `BlockNode`** (narrow with `node.kind === "block"` before touching `.children`). **Backend** (`internal/`): - Page identity is the pair **`(path, lang)`**, never a bare path string. `GET /api/page?path=&lang=` → `{page: {path, lang}, tree}`; `POST /api/page/save` body `{page, lang, path, name, values}`. `GET /api/tree?lang=` is the site nav tree (`internal/pagetree`). - `internal/pagetree.ContentRoot(contentDir, lang)` is the **single** place a language resolves to a content root — handlers never join `contentDir` themselves. - `server.New(provider, dist, siteDir, hugo hugobin.Resolved)` — 4 args (Hugo pinning, #5). ### Where this feature belongs now Element CRUD operates on the element list, and the element list lives in **`PagePane`**. Build the picker / insert / delete / drag-&-drop / undo affordances **into `PagePane`** (extract a sub-component if it grows large), with `TreePane` remaining pure page navigation. Selection changes go through `selection.ts`. ### What to salvage from the abandoned run (PR #23, `7cb4e5d`) That run's **backend and pure-logic modules are good and largely portable** — reuse them rather than reinventing: - `internal/placement/` — `Rules` (`CanInsert`/`CanMove`/`AllowedOnPage`), `PageType()`, allowlist loading. Ports **verbatim**. - `internal/server/tree.go` — tree mechanics (`shortcodeAt`, `pathOf`, `spliceInto`, `removeAt`, `normalizeText`, …). Pure, no page-identity coupling. Ports **verbatim**. - `internal/server/mutate.go` / `undo.go` — the insert/delete/move + single-step undo logic. Sound; needs `(path, lang)` threading (below). - `web/src/rules.ts` — picker filtering + `computeDrop`/`canMoveTo` drag logic, and its unit tests (`drop.test.ts`, `rules.test.ts`). Ports with ~2 edits (narrow on `kind === "block"` before `.children`; type `placement` properly). ### Backend integration constraints (verified — these all compile) 1. **`element.Schema.Placement` must be a typed struct**, not `map[string]any`: `type Placement struct { AllowedIn []string; AllowedAt []string }`. Nothing in `placement/` compiles otherwise. This is the linchpin — do it first (`element` → `placement` → `server`). 2. **Extend the page response** rather than adding a parallel endpoint — one union type serves `/api/page`, `/save`, `/mutate`, `/undo`: ```go type pageResponse struct { Page pageIdentity `json:"page"` // {path, lang} Tree *content.Document `json:"tree"` PageType string `json:"pageType"` AllowedElements []string `json:"allowedElements"` Path []int `json:"path,omitempty"` // inserted/moved node } ``` 3. **Thread `lang` through mutate/undo.** `mutateRequest` needs a `Lang` field; the undo snapshot must carry `lang` **and** the resolved absolute path (so undo neither re-resolves the language nor re-derives the content root). 4. `Handler` gains `pagetypesDir`, `mu sync.Mutex`, `undo *undoSnapshot` — **keep** the existing `hugo hugobin.Resolved` field and the 4-arg `New`. 5. `placement.PageType` splits on `/` — pass it the **slash** form of the path. ### Decide these coherently (the first run had no page tree, so it never faced them) - **Undo scope across pages.** The server undo slot is a single global step. With a page tree you can navigate away — decide and document whether the Undo control is page-scoped (disabled when you're not on the mutated page) or reflects the global last step. Pick one and make the UI honest about it. - Where the Undo and "add element" controls live now that the left pane is the page tree. - Whether `FormPane.onSaved` keeps passing the tree or the full page response. ### Acceptance criteria Unchanged from the original brief (picker honoring placement rules, add with schema defaults, delete-with-confirm incl. subtree, drag-&-drop reorder/nest with invalid drops visually rejected and server-side enforced, canonical-serializer persistence visible in preview, single-step undo). **Additionally:** the feature must integrate with #4's tree navigation and `(path, lang)` identity, and all existing tests on `main` must stay green.
Author
Owner

Closing as superseded: the maintainer has decided on a clean-slate reset of the project (issue #29, PR #30). The v1 implementation this issue builds on is being removed and the plan will be re-drawn from scratch; this spec is retired rather than actioned.

Closing as superseded: the maintainer has decided on a clean-slate reset of the project (issue #29, PR #30). The v1 implementation this issue builds on is being removed and the plan will be re-drawn from scratch; this spec is retired rather than actioned.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
Cloonar/deckle#6
No description provided.