feat(editor): form screen with General · Appearance · Access tabs, validation at rest, one unsaved-changes guard #175

Merged
dominik.polakovics merged 3 commits from afk/172 into main 2026-10-09 10:41:29 +02:00

Closes #172

What changes

Server / scaffold

  • New optional sidecar param key tab: general (default) | appearance; page-type fields may also use access (navigation flags such as hidden). tab: access on an element param, or an unknown value, is a load error.
  • Described fields carry tab; the Enabled field is on Access (it keeps group: access), and the backend layout pickers are on Appearance.
  • Validation at rest: POST /api/form returns errors for element and properties forms. These are the messages a save of the loaded values would report, keyed like the 422 (name, items[i].name), and the existing save checks produce them.
  • Scaffold sidecars: side, cols, lightbox, variant and size are on Appearance, and hidden/footer are on Access. The testdata site's sidecars follow the same split.
  • scaffold/site/AGENTS.md documents tab. The drift test now requires tab, general, appearance and access. The README's /api/form contract is updated too.
  • The theme bridge (ce-bridge.js) scrolls a highlight into view when asked (scroll: true).

Editor

  • EditPane is replaced by FormScreen (Docheader + ElementForm).
    • Row 1 of the docheader: Close, Save, Save & close, Preview, then Enable/Disable, Duplicate and a ⋯ menu (Cut, Copy, Move up/down, Delete…).
    • Row 2: crumbs ending in the slot name, then icon, label, #uid, and the status ("Unsaved changes" badge or "Saved just now").
  • ElementForm replaces FormPane; FormPane.tsx and FormItems.test.tsx are gone, and the tests moved to ElementForm.test.tsx.
    • Tabs: General (the Content group, then one group per items collection), Appearance (up to 3 columns, or a note when the element has none), Access (Enabled and Anchor).
    • Item rows: the first row is open, an open row shows its fields in two columns, and a collapsed row with problems shows a chip such as "Alt text missing".
  • PageForm replaces PageSettings, with the same tabs:
    • General: the page fields.
    • Appearance: the Layout pickers and appearance fields.
    • Access: Enabled, the navigation flags, and a read-only Path.
  • Shared layout pieces: FieldGroup, FieldFrame, FormTabs and problemTabs (the red dot per tab), in FormLayout.tsx.
  • One exit path, leaveForm() in guard.ts. The shell's useBeforeLeave and the host links both call it, so Close, Esc, the Content/Properties tabs, the tree, the module rail, browser back, host navigation and the docheader's structural actions all show one dialog: Stay / Discard changes / Save & continue, titled "in ".
  • useFormShortcuts: ⌘/Ctrl+S saves. Esc closes the form only when no menu or dialog layer is open (#140 rule).
  • Preview opens /view/<lang>/<page>?element=<uid>. The View module resolves the uid and highlights the element, and the preview scrolls it into view.
  • "Save & close" everywhere. The remaining text glyphs (×, ▾/▸, ⚠, ↺/↻) are now lucide icons.

Notes and decisions

  • No items limit exists in the schema. The "N of up to M" hint is not shown, and adding a limit is out of scope.
  • Enable/Disable in the docheader flips the form's Enabled switch. It is a normal edit that Save writes, so it never races the open form.
  • Structural actions on a dirty form ask first. Duplicate, Move and Cut go through leaveForm, so Save & continue saves before they run. Delete… asks only its own confirmation (the edits go with the element) and then closes unguarded.
  • Page Access holds navigation flags through tab: access. It is allowed on page-type fields only; element sidecars still choose only between general and appearance.

Verification

  • make test: 80 vitest files, 2322 tests, plus go test ./..., all green. tsc --noEmit, go vet and make build are clean.
  • New tests cover:
    • tab assignment for element and page forms;
    • validation at rest (field message, row chip, tab dot, clearing);
    • the guard on Close, Esc, the tree, the rail, browser back and the Properties → Content tab;
    • Save & continue saving and then proceeding;
    • ⌘S/Ctrl+S, and Esc with the ⋯ menu open closing only the menu;
    • Preview's route with ?element=;
    • the docheader actions;
    • the tab load rules and /api/form errors.
  • Smoke test against a copy of testdata/site: with the hero's required title removed, POST /api/form returns errors: {title: "this field is required"}, and align comes back with tab: appearance. The UI was not exercised in a real browser.

🤖 Generated with Claude Code

Closes #172 ## What changes **Server / scaffold** - New optional sidecar param key `tab`: `general` (default) | `appearance`; page-type fields may also use `access` (navigation flags such as `hidden`). `tab: access` on an element param, or an unknown value, is a load error. - Described fields carry `tab`; the Enabled field is on Access (it keeps `group: access`), and the backend layout pickers are on Appearance. - Validation at rest: `POST /api/form` returns `errors` for element and properties forms. These are the messages a save of the loaded values would report, keyed like the 422 (`name`, `items[i].name`), and the existing save checks produce them. - Scaffold sidecars: `side`, `cols`, `lightbox`, `variant` and `size` are on Appearance, and `hidden`/`footer` are on Access. The testdata site's sidecars follow the same split. - `scaffold/site/AGENTS.md` documents `tab`. The drift test now requires `tab`, `general`, `appearance` and `access`. The README's `/api/form` contract is updated too. - The theme bridge (`ce-bridge.js`) scrolls a highlight into view when asked (`scroll: true`). **Editor** - `EditPane` is replaced by `FormScreen` (Docheader + `ElementForm`). - Row 1 of the docheader: Close, **Save**, Save & close, Preview, then Enable/Disable, Duplicate and a ⋯ menu (Cut, Copy, Move up/down, Delete…). - Row 2: crumbs ending in the slot name, then icon, label, `#uid`, and the status ("Unsaved changes" badge or "Saved just now"). - `ElementForm` replaces `FormPane`; `FormPane.tsx` and `FormItems.test.tsx` are gone, and the tests moved to `ElementForm.test.tsx`. - Tabs: General (the Content group, then one group per items collection), Appearance (up to 3 columns, or a note when the element has none), Access (Enabled and Anchor). - Item rows: the first row is open, an open row shows its fields in two columns, and a collapsed row with problems shows a chip such as "Alt text missing". - `PageForm` replaces `PageSettings`, with the same tabs: - General: the page fields. - Appearance: the Layout pickers and appearance fields. - Access: Enabled, the navigation flags, and a read-only Path. - Shared layout pieces: `FieldGroup`, `FieldFrame`, `FormTabs` and `problemTabs` (the red dot per tab), in `FormLayout.tsx`. - One exit path, `leaveForm()` in `guard.ts`. The shell's `useBeforeLeave` and the host links both call it, so Close, Esc, the Content/Properties tabs, the tree, the module rail, browser back, host navigation and the docheader's structural actions all show one dialog: **Stay / Discard changes / Save & continue**, titled "in <form>". - `useFormShortcuts`: ⌘/Ctrl+S saves. Esc closes the form only when no menu or dialog layer is open (#140 rule). - Preview opens `/view/<lang>/<page>?element=<uid>`. The View module resolves the uid and highlights the element, and the preview scrolls it into view. - "Save & close" everywhere. The remaining text glyphs (×, ▾/▸, ⚠, ↺/↻) are now lucide icons. ## Notes and decisions - **No items limit exists in the schema.** The "N of up to M" hint is not shown, and adding a limit is out of scope. - **Enable/Disable in the docheader flips the form's Enabled switch.** It is a normal edit that Save writes, so it never races the open form. - **Structural actions on a dirty form ask first.** Duplicate, Move and Cut go through `leaveForm`, so Save & continue saves before they run. Delete… asks only its own confirmation (the edits go with the element) and then closes unguarded. - **Page Access holds navigation flags through `tab: access`.** It is allowed on page-type fields only; element sidecars still choose only between general and appearance. ## Verification - `make test`: 80 vitest files, 2322 tests, plus `go test ./...`, all green. `tsc --noEmit`, `go vet` and `make build` are clean. - New tests cover: - tab assignment for element and page forms; - validation at rest (field message, row chip, tab dot, clearing); - the guard on Close, Esc, the tree, the rail, browser back and the Properties → Content tab; - Save & continue saving and then proceeding; - ⌘S/Ctrl+S, and Esc with the ⋯ menu open closing only the menu; - Preview's route with `?element=`; - the docheader actions; - the `tab` load rules and `/api/form` errors. - Smoke test against a copy of `testdata/site`: with the hero's required `title` removed, `POST /api/form` returns `errors: {title: "this field is required"}`, and `align` comes back with `tab: appearance`. The UI was not exercised in a real browser. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
A param may declare `tab: general | appearance` (page-type fields also
`access`); described fields carry `tab`, the Enabled field sits on Access
and the backend layout pickers on Appearance. POST /api/form reports the
problems of the loaded values (`errors`, keyed like a save's 422) for
element and properties forms. The scaffold assigns side/cols/lightbox/
variant/size to Appearance and the navigation flags to Access;
AGENTS.md documents `tab` and the drift test enforces it. The preview
bridge scrolls a highlight into view on request.

Refs #172

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
feat(editor): form screen with General · Appearance · Access tabs
All checks were successful
ci / build-and-test (pull_request) Successful in 12m15s
0b4195946e
EditPane becomes FormScreen (Docheader + ElementForm): Close, Save,
Save & close, Preview (View module scrolled to the element), Enable/
Disable, Duplicate and a ⋯ menu; crumbs end in the slot; an "Unsaved
changes" / "Saved just now" status. ElementForm (replacing FormPane) and
PageForm (replacing PageSettings) share FieldGroup/FieldFrame/FormTabs,
show validation at rest (field message, item-row chip, red tab dot) and
open the first item row. Every exit goes through one leaveForm() with
Stay / Discard changes / Save & continue; ⌘/Ctrl+S saves and Esc closes
only when no layer is open. "Save & close" everywhere, lucide icons for
the remaining text glyphs.

Closes #172

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Author
Owner

[autoland] verdict: pass

This was generated by AI while landing a PR.

Verdict: CONCERNS (non-blocking). Validated against #172.

  • Signal: CI ci / build-and-test is green (run 167, 12m15s), so it was not re-run.
  • Conventions: Conventional Commits title, Closes #172, head already contains main (no conflict).
  • CLAUDE.md: scaffold/site/AGENTS.md and the drift test are updated for tab.
  • Diff review: the full diff was read. The server, scaffold, form, guard and navigation code was traced against the issue's acceptance criteria. Nothing blocks.

Concerns (worth a follow-up, none blocks the merge):

  1. ⌘S skips the save-in-flight check. The Save buttons are disabled while saving, but FormScreen.tsx doSave (~258) and PageScreen.tsx saveProps (~218) call submit() without checking saving(). Neither ElementForm.submit (~794) nor PageForm.submit (~212) refuses a second call. Two ⌘S presses during a slow save post the page twice. The mutation lock runs the two saves one after the other, but item rows that have no uid yet get uids stamped twice. guard.ts avoids this same race in saveAndContinue. Fix: return early when saving().
  2. AGENTS.md overstates tab on item elements. scaffold/site/AGENTS.md:91-92 says item elements "take tab the same way". ElementForm renders every item-row field inside the row on General, so the key is validated but changes nothing there. CLAUDE.md requires the prose to be truthful: either say it is ignored inside rows, or implement it.
  3. Empty General tab. An element whose only params are tab: appearance and which has no body and no items opens on a blank General tab (ElementForm.tsx:1294-1316). The scaffold spacer is one, after this PR. Appearance has an "empty" note; General has none.

Nits:

  • RichTextField.tsx:505 still renders a Table ▾ glyph.
  • The return written by a preview click (App.tsx:286) keeps ?element=X, so closing a form opened from the preview re-reveals the old element.
  • The row chip always says "… missing", even for an invalid value.
  • The entry editor's PageForm shows a content path with the hint "Set by the page tree."
  • Untested: ⌘S while a save is in flight, and Discard before a Duplicate or Move.
[autoland] verdict: pass > *This was generated by AI while landing a PR.* **Verdict: CONCERNS (non-blocking).** Validated against #172. - **Signal:** CI `ci / build-and-test` is green (run 167, 12m15s), so it was not re-run. - **Conventions:** Conventional Commits title, `Closes #172`, head already contains `main` (no conflict). - **CLAUDE.md:** `scaffold/site/AGENTS.md` and the drift test are updated for `tab`. - **Diff review:** the full diff was read. The server, scaffold, form, guard and navigation code was traced against the issue's acceptance criteria. Nothing blocks. **Concerns** (worth a follow-up, none blocks the merge): 1. **⌘S skips the save-in-flight check.** The Save buttons are disabled while saving, but `FormScreen.tsx` `doSave` (~258) and `PageScreen.tsx` `saveProps` (~218) call `submit()` without checking `saving()`. Neither `ElementForm.submit` (~794) nor `PageForm.submit` (~212) refuses a second call. Two ⌘S presses during a slow save post the page twice. The mutation lock runs the two saves one after the other, but item rows that have no uid yet get uids stamped twice. `guard.ts` avoids this same race in `saveAndContinue`. Fix: return early when `saving()`. 2. **AGENTS.md overstates `tab` on item elements.** `scaffold/site/AGENTS.md:91-92` says item elements "take `tab` the same way". `ElementForm` renders every item-row field inside the row on General, so the key is validated but changes nothing there. CLAUDE.md requires the prose to be truthful: either say it is ignored inside rows, or implement it. 3. **Empty General tab.** An element whose only params are `tab: appearance` and which has no body and no items opens on a blank General tab (`ElementForm.tsx:1294-1316`). The scaffold `spacer` is one, after this PR. Appearance has an "empty" note; General has none. **Nits:** - `RichTextField.tsx:505` still renders a `Table ▾` glyph. - The `return` written by a preview click (`App.tsx:286`) keeps `?element=X`, so closing a form opened from the preview re-reveals the old element. - The row chip always says "… missing", even for an invalid value. - The entry editor's `PageForm` shows a content path with the hint "Set by the page tree." - Untested: ⌘S while a save is in flight, and Discard before a Duplicate or Move.
fix(editor): one save at a time, a note on an empty General tab
All checks were successful
ci / build-and-test (pull_request) Successful in 11m10s
8219fc39b3
- ElementForm/PageForm `submit()` joins a save already in flight instead
  of posting again. A second ⌘S during a slow save no longer posts the
  page twice or stamps uids on new item rows twice. Once the first save
  lands, the joined call saves again only if an edit made meanwhile left
  the form dirty.
- An element with no General params, no body and no items (the scaffold
  spacer) shows a one-line note on General instead of a blank tab.
- AGENTS.md: `tab` on an item element is checked but has no effect,
  because an item row shows all its fields inside the row on General.
  It also documents the new General note.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Author
Owner

[autoland] verdict: pass

This was generated by AI while landing a PR.

Re-validated at 8219fc3. Verdict: PASS.

  • Signal: CI ci / build-and-test is green on the new head (run 168, 11m10s), so it was not re-run.
  • Conventions: still met (Conventional Commits title, Closes #172, no conflict with main).

All three earlier concerns are resolved:

  1. ⌘S double save: submit() in ElementForm and PageForm now joins a save in flight instead of posting again. It saves once more only if an edit made meanwhile left the form dirty. The reset runs before any joiner's follow-up, so nothing posts in parallel. Covered by tests in ElementForm, PageForm and FormScreen (⌘S).
  2. AGENTS.md: now says tab is checked but has no effect on item elements.
  3. Empty General tab: General now shows a one-line note when the element has no content and no items (tested).

Still open, non-blocking nits:

  • the Table ▾ glyph in RichTextField.tsx
  • ?element= kept in the return link a preview click writes (App.tsx:286)
  • the row chip's "missing" wording
  • the entry editor's PageForm Path hint
[autoland] verdict: pass > *This was generated by AI while landing a PR.* **Re-validated at `8219fc3`. Verdict: PASS.** - **Signal:** CI `ci / build-and-test` is green on the new head (run 168, 11m10s), so it was not re-run. - **Conventions:** still met (Conventional Commits title, `Closes #172`, no conflict with `main`). All three earlier concerns are resolved: 1. **⌘S double save:** `submit()` in `ElementForm` and `PageForm` now joins a save in flight instead of posting again. It saves once more only if an edit made meanwhile left the form dirty. The reset runs before any joiner's follow-up, so nothing posts in parallel. Covered by tests in ElementForm, PageForm and FormScreen (⌘S). 2. **AGENTS.md:** now says `tab` is checked but has no effect on item elements. 3. **Empty General tab:** General now shows a one-line note when the element has no content and no items (tested). **Still open, non-blocking nits:** - the `Table ▾` glyph in `RichTextField.tsx` - `?element=` kept in the return link a preview click writes (`App.tsx:286`) - the row chip's "missing" wording - the entry editor's `PageForm` Path hint
Sign in to join this conversation.
No reviewers
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!175
No description provided.