feat(editor): rebuild the Page module screen on shared components #174

Merged
dominik.polakovics merged 12 commits from afk/171 into main 2026-10-09 09:03:27 +02:00

Closes #171

Rebuilds the Page module's content screen on a shared component layer, following the Direction A mockup (docs/mockups/editing-screens/).

What changed

Shared components (web/src/components/)

  • New: Menu (MenuButton, built on layers.ts), Badge, Tabs, ButtonBar, PageBanner, ClipboardBar, ErrorStrip, ElementPicker, LayoutGrid → SlotColumn → InsertRow + ElementCard, PageContent, PageScreen, PendingChanges.
  • Docheader gains title, badge and tab rows, and its stale note is gone.
  • The duplicated badge markup (ModuleScreen, PageEditorChrome, EditPane) and the action error strips now render through Badge and ErrorStrip.
  • The legacy FormPane wrapper is removed.

Content grid (PagePane.tsx is deleted)

  • One code path for every case. A page without a backend layout is a one-slot grid. The page module and the entry editor's Content tab both mount PageContent.
  • Columns stretch to equal height. Each slot header shows a count and an ⓘ listing the slot's allowlist.
  • Nested visible children, such as accordion items, render as an indented sub-list with their own insert rows.
  • Inherited slots render read-only (dashed cards, "From ") with one override row at the top and an "Edit on →" link.
  • Unused content is a collapsible section rendered with the same cards.
  • Insert rows are always visible and have a fixed height. The rows are the only drop targets. During a drag, rows whose slot or placement refuses the element go grey and refuse the drop. This removes the edge zones behind #127 (referenced, not closed here). dropzone.ts itself stays, because TreePane, item collections and pagemanage still use it; the grid no longer does.
  • Cards show a handle (only the handle drags), icon, label, #uid, the Disabled badge, a two-line summary and image thumbnails. Hover reveals Edit, Enable/Disable and ⋮.
  • The ⋮ menu is Edit, Enable/Disable, Duplicate, —, Cut, Copy, —, Move up, Move down, —, Delete…. "Move to column…" no longer exists in web/.
  • Hovering a card still posts the preview highlight.

Insert dialog

  • Left side: All, then the sidecar categories with counts (alphabetical, Other last), plus a search over label and description.
  • Elements the slot or placement refuses stay visible but disabled, with the reason. A note names what the slot allows.

Clipboard

  • Holds one cut or copied element, kept in sessionStorage for the tab.
  • A Paste button sits next to every "+ Element". It is disabled, with the reason, where the allowlist or placement refuses, across languages, and around or inside the cut element itself.
  • What a paste does:
    • Cut on the same page uses the move op.
    • Copy on the same page, and everything across pages, uses the new transfer op.
    • Either way, one paste clears the clipboard.
  • A ClipboardBar names the element and its source page. Shortcuts: ⌘/Ctrl+X and +C on a focused card, ⌘/Ctrl+V on a focused insert row.

Page level

  • Content (with count) and Properties are docheader tabs. Properties lives at ?tab=properties, and ?props=1 is gone.
  • A disabled page shows the banner with Enable page.
  • The page ⋯ menu is Rename…, New subpage…, Enable/Disable page, —, Delete….
  • Undo's tooltip names what it will undo and when, e.g. "Undo: save on About, 2 min ago".
  • The top-bar "N changes" chip opens the pending-changes panel:
    • One row per page or media file. Page rows show counts such as "2 elements changed, 1 added".
    • Rows open the page or the Media module.
    • The footer has Publish all and Discard all….

Server / engine

  • Element sidecars get optional category and description keys. The scaffold, the news bundle and testdata/site declare them, scaffold/site/AGENTS.md documents them, and the drift test pins them.
  • POST /api/element/transfer moves or copies an element to a slot or container on the same or another page:
    • It validates against the destination page.
    • It serializes both pages before writing either, and rolls the source back if the destination write fails.
    • It takes one git snapshot.
    • Undo on either page restores both.
    • Pasting into an inherited slot gives the page its own content there.
  • POST /api/element/enabled toggles one element.
  • GET /api/publish/pending returns the pending listing, with element counts diffed by uid against main. POST /api/publish/discard-all resets every pending change.
  • GET /api/git/state now describes what undo would revert.
  • The content root can be enabled again; only disabling it is refused.

Assumptions checked

  • Inherit slots: the engine and the theme already let a page own content in an inherit slot. Its own content wins, so no engine change was needed.
  • Visible block children: only the scaffold accordion has them (cards, image and text-media use hidden items). The nested sub-list is built generally.
  • Pending listing: publish.go's changed-file set (ChangedPublishFiles) already gives the list. Element counts needed a new main-version reader (gitrepo.MainFile).
  • Undo snapshot info: the subject and time were not exposed before; now they are. The element label is not recorded, so the tooltip names the operation and the page instead.
  • deckle-hosted: has no dependency on ?props=1 or the PagePane DOM.

Verification

  • make test passes: 79 web test files with 2218 tests, and every Go package.
  • go vet ./... and tsc --noEmit are clean. The only gofmt -l hit is internal/theme/icon_test.go, which is already on main.
  • The old PagePane, ElementCrud and dropzone-zone tests are ported or replaced. New tests cover insert positions, drop verdicts per slot allowlist, clipboard transitions, menu contents, tabs, banner, pending panel and the server ops.
  • Smoke check with harness serve on a git copy of testdata/site:
    • A cross-page POST /api/element/transfer move produced exactly one deckle: transfer A -> B snapshot touching both files.
    • A copy got a fresh uid.
    • ?tab=properties serves.

Notes / follow-ups

  • Category order: the insert dialog orders categories alphabetically, because categories are free labels with no declared order.
  • Pre-existing, not changed here: inserting or moving an element right after a text run that has no trailing newline puts the shortcode on the same line (…text.{{< note >}}). This is content.InsertChild behaviour shared by the existing insert op, and worth its own issue.
  • Root page drafted by hand: the tree and banner show it as disabled, but its Properties form shows Enabled as read-only on. The banner's Enable page works.

🤖 Generated with Claude Code

Closes #171 Rebuilds the Page module's content screen on a shared component layer, following the Direction A mockup (`docs/mockups/editing-screens/`). ## What changed **Shared components** (`web/src/components/`) - New: `Menu` (MenuButton, built on `layers.ts`), `Badge`, `Tabs`, `ButtonBar`, `PageBanner`, `ClipboardBar`, `ErrorStrip`, `ElementPicker`, `LayoutGrid` → `SlotColumn` → `InsertRow` + `ElementCard`, `PageContent`, `PageScreen`, `PendingChanges`. - `Docheader` gains title, badge and tab rows, and its stale note is gone. - The duplicated badge markup (ModuleScreen, PageEditorChrome, EditPane) and the action error strips now render through `Badge` and `ErrorStrip`. - The legacy `FormPane` wrapper is removed. **Content grid** (`PagePane.tsx` is deleted) - One code path for every case. A page without a backend layout is a one-slot grid. The page module and the entry editor's Content tab both mount `PageContent`. - Columns stretch to equal height. Each slot header shows a count and an ⓘ listing the slot's allowlist. - Nested visible children, such as accordion items, render as an indented sub-list with their own insert rows. - Inherited slots render read-only (dashed cards, "From <page>") with one override row at the top and an "Edit on <page> →" link. - Unused content is a collapsible section rendered with the same cards. - Insert rows are always visible and have a fixed height. The rows are the only drop targets. During a drag, rows whose slot or placement refuses the element go grey and refuse the drop. This removes the edge zones behind #127 (referenced, not closed here). `dropzone.ts` itself stays, because TreePane, item collections and pagemanage still use it; the grid no longer does. - Cards show a handle (only the handle drags), icon, label, `#uid`, the Disabled badge, a two-line summary and image thumbnails. Hover reveals Edit, Enable/Disable and ⋮. - The ⋮ menu is Edit, Enable/Disable, Duplicate, —, Cut, Copy, —, Move up, Move down, —, Delete…. "Move to column…" no longer exists in `web/`. - Hovering a card still posts the preview highlight. **Insert dialog** - Left side: All, then the sidecar categories with counts (alphabetical, Other last), plus a search over label and description. - Elements the slot or placement refuses stay visible but disabled, with the reason. A note names what the slot allows. **Clipboard** - Holds one cut or copied element, kept in sessionStorage for the tab. - A Paste button sits next to every "+ Element". It is disabled, with the reason, where the allowlist or placement refuses, across languages, and around or inside the cut element itself. - What a paste does: - Cut on the same page uses the move op. - Copy on the same page, and everything across pages, uses the new transfer op. - Either way, one paste clears the clipboard. - A `ClipboardBar` names the element and its source page. Shortcuts: ⌘/Ctrl+X and +C on a focused card, ⌘/Ctrl+V on a focused insert row. **Page level** - Content (with count) and Properties are docheader tabs. Properties lives at `?tab=properties`, and `?props=1` is gone. - A disabled page shows the banner with Enable page. - The page ⋯ menu is Rename…, New subpage…, Enable/Disable page, —, Delete…. - Undo's tooltip names what it will undo and when, e.g. "Undo: save on About, 2 min ago". - The top-bar "N changes" chip opens the pending-changes panel: - One row per page or media file. Page rows show counts such as "2 elements changed, 1 added". - Rows open the page or the Media module. - The footer has Publish all and Discard all…. **Server / engine** - Element sidecars get optional `category` and `description` keys. The scaffold, the news bundle and `testdata/site` declare them, `scaffold/site/AGENTS.md` documents them, and the drift test pins them. - `POST /api/element/transfer` moves or copies an element to a slot or container on the same or another page: - It validates against the destination page. - It serializes both pages before writing either, and rolls the source back if the destination write fails. - It takes one git snapshot. - Undo on either page restores both. - Pasting into an inherited slot gives the page its own content there. - `POST /api/element/enabled` toggles one element. - `GET /api/publish/pending` returns the pending listing, with element counts diffed by uid against main. `POST /api/publish/discard-all` resets every pending change. - `GET /api/git/state` now describes what undo would revert. - The content root can be enabled again; only disabling it is refused. ## Assumptions checked - **Inherit slots:** the engine and the theme already let a page own content in an `inherit` slot. Its own content wins, so no engine change was needed. - **Visible block children:** only the scaffold `accordion` has them (`cards`, `image` and `text-media` use hidden items). The nested sub-list is built generally. - **Pending listing:** `publish.go`'s changed-file set (`ChangedPublishFiles`) already gives the list. Element counts needed a new main-version reader (`gitrepo.MainFile`). - **Undo snapshot info:** the subject and time were not exposed before; now they are. The element label is not recorded, so the tooltip names the operation and the page instead. - **deckle-hosted:** has no dependency on `?props=1` or the PagePane DOM. ## Verification - `make test` passes: 79 web test files with 2218 tests, and every Go package. - `go vet ./...` and `tsc --noEmit` are clean. The only `gofmt -l` hit is `internal/theme/icon_test.go`, which is already on main. - The old PagePane, ElementCrud and dropzone-zone tests are ported or replaced. New tests cover insert positions, drop verdicts per slot allowlist, clipboard transitions, menu contents, tabs, banner, pending panel and the server ops. - Smoke check with `harness serve` on a git copy of `testdata/site`: - A cross-page `POST /api/element/transfer` move produced exactly one `deckle: transfer A -> B` snapshot touching both files. - A copy got a fresh uid. - `?tab=properties` serves. ## Notes / follow-ups - **Category order:** the insert dialog orders categories alphabetically, because categories are free labels with no declared order. - **Pre-existing, not changed here:** inserting or moving an element right after a text run that has no trailing newline puts the shortcode on the same line (`…text.{{< note >}}`). This is `content.InsertChild` behaviour shared by the existing insert op, and worth its own issue. - **Root page drafted by hand:** the tree and banner show it as disabled, but its Properties form shows Enabled as read-only on. The banner's Enable page works. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
The insert dialog groups elements by a free `category` label and shows a
one-line `description` on each tile. The scaffold and the test site
declare both; AGENTS.md documents them and the drift test pins them.

Refs #171

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- POST /api/element/transfer moves or copies an element to a slot or
  container on the same or another page; a cross-page transfer writes
  both pages and takes one snapshot. Undo of a transfer restores both
  pages, and GET /api/git/state describes what undo would revert.
- GET /api/publish/pending lists pages (with element-level counts) and
  media files that are saved but not yet published.
- POST /api/publish/discard-all resets every pending change to main.

Refs #171

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The element card's Enable/Disable sets the state the way a form save
with `enabled` changed does, without touching anything else.

Refs #171

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Menu, Badge, Tabs, ButtonBar, PageBanner, ClipboardBar and ErrorStrip
become single shared components; the docheader gains title, badge and
tab rows. PageActionsMenu, the page badges and the action error strips
render through them, and the legacy FormPane wrapper goes.

ElementPicker is the new insert dialog: All plus the sidecar categories
with counts, a search over label and description, and refused elements
shown disabled with their reason.

Refs #171

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The "N changes" chip opens a panel listing the pages and media files
with saved, unpublished changes; rows open the page or the Media module,
and the footer offers Publish all and Discard all…. Adds the clients for
the pending listing, discard all and element transfer, and the undo
description on the git state.

Refs #171

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
PagePane is replaced by LayoutGrid → SlotColumn → InsertRow +
ElementCard, shared by the page module (PageScreen) and the entry
editor's Content tab (PageContent). A page without a backend layout is a
one-slot grid. Always-visible insert rows open the categorised insert
dialog and are the only drop targets: rows whose slot or placement
refuses the dragged element go grey and refuse the drop, which removes
the edge-zone drops of #127. Cards show a summary, image thumbnails and
the Disabled badge, nest visible children with their own insert rows,
and carry Edit, Enable/Disable and the new element menu; "Move to
column…" is gone. Inherited slots render read-only with one override
row.

Refs #171

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A transfer's partner page is only restored by an undo in the same
language.

Refs #171

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- Cut and Copy put one element on a tab-scoped clipboard (sessionStorage);
  Paste sits beside every + Element, disabled with the reason where the
  slot or placement refuses. Paste moves or copies within the page or
  across pages through the transfer op and clears the clipboard. A
  ClipboardBar names the element and its source page.
- Content and Properties are docheader tabs (?tab=properties); the
  ?props=1 view is gone.
- A disabled page shows a banner with Enable page; the page menu is
  Rename…, New subpage…, Enable/Disable page, Delete….
- Undo's tooltip names what it will undo and when.

Refs #171

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- Operations update the grid in place instead of remounting it, so focus
  stays with a card moved by Move up/down, the preview highlight never
  points at a stale position, and Unused content stays expanded.
- Only the card's grip starts a drag, so buttons in a nested sub-list no
  longer drag the parent.
- The insert dialog returns focus to the row that opened it.
- The no-layout slot counts the cards it shows, not slot wrappers.

Refs #171

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
fix(editor): guard paste and the page menu on the Properties tab
All checks were successful
ci / build-and-test (pull_request) Successful in 10m34s
3aba648920
- Paste is refused across languages; a renamed or missing source page
  keeps the clipboard instead of reporting the element gone.
- Rename… from the Properties tab offers to save unsaved edits first and
  lands back on the tab.
- The content root can be enabled again (only disabling it is refused),
  so the disabled-page banner works there too.

Refs #171

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). Landable as-is; the findings below are follow-up material.

Signal relied on: Forgejo Actions ci / build-and-test green on head 3aba648 (make test, go vet, tsc) — not re-run. Head is up to date with main; no conflict. Title is Conventional Commits; body carries Closes #171. scaffold/site/AGENTS.md documents the new category/description sidecar keys and the drift test pins them.

Reviewed: full Go diff (transfer/enabled/pending/discard-all/undo/sidecar keys), full web diff (LayoutGrid/PageContent/PageScreen/clipboard/picker/pending panel vs the Go handlers), every acceptance criterion of #171, and deckle-hosted (pure reverse proxy, no dependency on removed DOM/routes). styles.css churn is a diff-algorithm artifact (histogram: +1514/−611), all feature work.

CONCERNS

  1. Undo tooltip/scope mismatch. web/src/components/PageEditorChrome.tsx:122-135 + web/src/undoinfo.ts name the site's last snapshot regardless of the open page, but /api/page/undo restores only the open page (plus a transfer partner when the open page is one of the step's pages). Save on Services, open About → tooltip "Undo: save on Services", click restores About only (a no-op).
    • Same root, worse case — undo after Discard all (internal/server/pending.go:538-548, internal/server/git.go:554-568): tooltip reads "Undo: discard on all" (while the confirm at PendingChanges.tsx:295 says "This cannot be undone"); undo restores only the open page, and that page can then reference a library upload discard-all deleted. Draft-only — the publish dangling-refs check keeps it off main. No test combines discard-all with undo.
  2. Cross-page copy rewrites the source page. internal/server/element_transfer.go:242-262 serializes and writes srcDoc in copy mode too, though copy leaves it unchanged — a hand-edited non-canonical source gets reformatted and shows as modified with zero element changes (an unserializable source 422s the copy). The test fixture is already canonical. Skip the source write when mode == copy.
  3. Cross-page cut silently breaks references. Anchor links to /<pageA>/#c-<uid> (and restamped colliding uids) dangle, and bundle-local media (bare filenames) stop resolving on the destination; delete warns about linking pages, cut/paste doesn't.

Minor

  • scaffold/editor/elements/html.yaml (insertable: false) gained a description, contradicting AGENTS.md:90-92 ("have no use for either key").
  • accordion-item has no category, so the scaffold's top-level insert dialog shows an "Other" category holding only a disabled tile.
  • styles.css:5066-5088 re-declares .app-header--hosted grid areas for the pending chip, silently overriding the originals at :2455/:3111 — worth a look in a hosted instance.
[autoland] verdict: pass > *This was generated by AI while landing a PR.* **Verdict: CONCERNS (non-blocking).** Landable as-is; the findings below are follow-up material. **Signal relied on:** Forgejo Actions `ci / build-and-test` green on head 3aba648 (make test, go vet, tsc) — not re-run. Head is up to date with main; no conflict. Title is Conventional Commits; body carries `Closes #171`. `scaffold/site/AGENTS.md` documents the new `category`/`description` sidecar keys and the drift test pins them. **Reviewed:** full Go diff (transfer/enabled/pending/discard-all/undo/sidecar keys), full web diff (LayoutGrid/PageContent/PageScreen/clipboard/picker/pending panel vs the Go handlers), every acceptance criterion of #171, and deckle-hosted (pure reverse proxy, no dependency on removed DOM/routes). styles.css churn is a diff-algorithm artifact (histogram: +1514/−611), all feature work. **CONCERNS** 1. **Undo tooltip/scope mismatch.** `web/src/components/PageEditorChrome.tsx:122-135` + `web/src/undoinfo.ts` name the site's last snapshot regardless of the open page, but `/api/page/undo` restores only the open page (plus a transfer partner when the open page is one of the step's pages). Save on Services, open About → tooltip "Undo: save on Services", click restores About only (a no-op). - Same root, worse case — **undo after Discard all** (`internal/server/pending.go:538-548`, `internal/server/git.go:554-568`): tooltip reads "Undo: discard on all" (while the confirm at `PendingChanges.tsx:295` says "This cannot be undone"); undo restores only the open page, and that page can then reference a library upload discard-all deleted. Draft-only — the publish dangling-refs check keeps it off main. No test combines discard-all with undo. 2. **Cross-page copy rewrites the source page.** `internal/server/element_transfer.go:242-262` serializes and writes `srcDoc` in copy mode too, though copy leaves it unchanged — a hand-edited non-canonical source gets reformatted and shows as modified with zero element changes (an unserializable source 422s the copy). The test fixture is already canonical. Skip the source write when `mode == copy`. 3. **Cross-page cut silently breaks references.** Anchor links to `/<pageA>/#c-<uid>` (and restamped colliding uids) dangle, and bundle-local media (bare filenames) stop resolving on the destination; delete warns about linking pages, cut/paste doesn't. **Minor** - `scaffold/editor/elements/html.yaml` (`insertable: false`) gained a `description`, contradicting AGENTS.md:90-92 ("have no use for either key"). - `accordion-item` has no `category`, so the scaffold's top-level insert dialog shows an "Other" category holding only a disabled tile. - `styles.css:5066-5088` re-declares `.app-header--hosted` grid areas for the pending chip, silently overriding the originals at `:2455`/`:3111` — worth a look in a hosted instance.
- accordion-item declares category Text, so the scaffold's insert dialog
  never shows an "Other" heading holding only its disabled tile.
- html.yaml (insertable: false) drops its description, as AGENTS.md says
  such elements have no use for one. A theme test pins both rules.
- The hosted header's grid declares the pending-changes column once, in
  its original rules, instead of re-declaring them further down.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
fix(editor): page-scoped Undo, source-free copy, warn before a cross-page cut
All checks were successful
ci / build-and-test (pull_request) Successful in 10m36s
bc735c9051
- Undo names and offers only a step on the open page. /api/git/state now
  always reports the step's language (undoLang); when the step touched
  other pages, or only media, Undo is disabled with "Nothing to undo on
  this page — the last change was …".
- Discard all is not undoable: /api/git/state reports nothing to undo
  after it and /api/page/undo refuses it with a 409, matching the
  confirmation's "This cannot be undone".
- A cross-page copy writes only the destination (one "save" snapshot),
  so a hand-formatted source is no longer reformatted or marked modified.
- New GET /api/element/move-refs lists the pages linking to the cut
  element's anchors and the bundle-local images the destination cannot
  resolve; pasting a cut on another page asks first when either is
  non-empty (warn-and-proceed, like Delete).

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

Addressed the review concerns in 6a40e75 and bc735c9:

  1. Undo tooltip/scope. Undo is now page-aware. /api/git/state always reports the step's language (undoLang). When the last step didn't touch the open page (another page, another language, or a media op), Undo is disabled with "Nothing to undo on this page — the last change was save on Services, 2 min ago". Discard all is no longer undoable: git state reports nothing to undo, and /api/page/undo refuses it with a 409, so the confirmation's "This cannot be undone" is true. New test: TestDiscardAllIsNotUndoable.
  2. Cross-page copy. Only the destination is written now, with one deckle: save <dst> snapshot. A hand-formatted source keeps its bytes (TestElementTransferCopyLeavesNonCanonicalSourceAlone). The PR body's "Undo on either page restores both" now applies only to cross-page moves.
  3. Cross-page cut. New read-only GET /api/element/move-refs?page=&lang=&uid=&toPage= lists the pages linking to any #c-<uid> in the cut subtree, plus the bare-filename images that the source bundle holds and the destination's doesn't. Pasting a cut on another page shows a warn-and-proceed "Move element" dialog when either list is non-empty (Cancel / Move anyway), like Delete. Copies and same-page cuts are never asked about.

Minor:

  • html.yaml no longer has a description.
  • accordion-item is in category Text, so there is no lone "Other" heading.
  • A theme test pins both rules.
  • The hosted header grid declares the pending column once, in the original rules at the top and in the 900px query. The later re-declaration is gone.
  • README's single-step-undo limitation now describes the page scope and Discard all.

Verification: go test ./..., go vet ./..., tsc --noEmit and vitest (79 files, 2227 tests) all pass.

Addressed the review concerns in 6a40e75 and bc735c9: 1. **Undo tooltip/scope.** Undo is now page-aware. `/api/git/state` always reports the step's language (`undoLang`). When the last step didn't touch the open page (another page, another language, or a media op), Undo is disabled with "Nothing to undo on this page — the last change was save on Services, 2 min ago". **Discard all** is no longer undoable: git state reports nothing to undo, and `/api/page/undo` refuses it with a 409, so the confirmation's "This cannot be undone" is true. New test: `TestDiscardAllIsNotUndoable`. 2. **Cross-page copy.** Only the destination is written now, with one `deckle: save <dst>` snapshot. A hand-formatted source keeps its bytes (`TestElementTransferCopyLeavesNonCanonicalSourceAlone`). The PR body's "Undo on either page restores both" now applies only to cross-page moves. 3. **Cross-page cut.** New read-only `GET /api/element/move-refs?page=&lang=&uid=&toPage=` lists the pages linking to any `#c-<uid>` in the cut subtree, plus the bare-filename images that the source bundle holds and the destination's doesn't. Pasting a cut on another page shows a warn-and-proceed "Move element" dialog when either list is non-empty (Cancel / Move anyway), like Delete. Copies and same-page cuts are never asked about. Minor: - `html.yaml` no longer has a description. - `accordion-item` is in category Text, so there is no lone "Other" heading. - A theme test pins both rules. - The hosted header grid declares the `pending` column once, in the original rules at the top and in the 900px query. The later re-declaration is gone. - README's single-step-undo limitation now describes the page scope and Discard all. Verification: `go test ./...`, `go vet ./...`, `tsc --noEmit` and vitest (79 files, 2227 tests) all pass.
Author
Owner

[autoland] verdict: pass

This was generated by AI while landing a PR.

Re-verified on head bc735c9 — verdict: PASS.

Signal relied on: Forgejo Actions ci / build-and-test green on bc735c9 (run 165) — not re-run. Head is up to date with main; no conflict.

Prior CONCERNS, all resolved (reviewed the 3aba648..bc735c9 delta):

  1. Undo scope — undoReach (web/src/undoinfo.ts) disables Undo on a page the last step didn't touch and names that step; /api/git/state always reports undoLang with undoPages. Discard all is no longer undoable: git state reports nothing to undo and /api/page/undo 409s (isDiscardAllStep, internal/server/git.go), matching the confirmation's "cannot be undone". Test: TestDiscardAllIsNotUndoable.
  2. Cross-page copy writes only the destination, one save snapshot (internal/server/element_transfer.go). Test: TestElementTransferCopyLeavesNonCanonicalSourceAlone.
  3. Cross-page cut — new read-only GET /api/element/move-refs (same linkrefs.References scan as the delete warning, plus bundle-local images the destination lacks) feeds a warn-and-proceed "Move element" dialog; copies and same-page cuts are never asked. Tests: TestElementMoveRefs + two Clipboard tests.
  • Minor: html.yaml description dropped; accordion-item in Text (no lone "Other"), both pinned by TestElementSidecarsCategories; hosted header grid declared once in the original rules (column/area counts match at both widths).

Note (below the CONCERNS bar): in branch mode a deckle: save before sync / save before publish tip (only written when the worktree was dirty) parses as op save on page "before sync", so Undo is disabled everywhere with "the last change was save on before sync" — rare, and the old behaviour there wasn't meaningful either.

[autoland] verdict: pass > *This was generated by AI while landing a PR.* **Re-verified on head bc735c9 — verdict: PASS.** **Signal relied on:** Forgejo Actions `ci / build-and-test` green on bc735c9 (run 165) — not re-run. Head is up to date with main; no conflict. **Prior CONCERNS, all resolved** (reviewed the 3aba648..bc735c9 delta): 1. Undo scope — `undoReach` (`web/src/undoinfo.ts`) disables Undo on a page the last step didn't touch and names that step; `/api/git/state` always reports `undoLang` with `undoPages`. Discard all is no longer undoable: git state reports nothing to undo and `/api/page/undo` 409s (`isDiscardAllStep`, `internal/server/git.go`), matching the confirmation's "cannot be undone". Test: `TestDiscardAllIsNotUndoable`. 2. Cross-page copy writes only the destination, one `save` snapshot (`internal/server/element_transfer.go`). Test: `TestElementTransferCopyLeavesNonCanonicalSourceAlone`. 3. Cross-page cut — new read-only `GET /api/element/move-refs` (same `linkrefs.References` scan as the delete warning, plus bundle-local images the destination lacks) feeds a warn-and-proceed "Move element" dialog; copies and same-page cuts are never asked. Tests: `TestElementMoveRefs` + two Clipboard tests. - Minor: `html.yaml` description dropped; `accordion-item` in Text (no lone "Other"), both pinned by `TestElementSidecarsCategories`; hosted header grid declared once in the original rules (column/area counts match at both widths). **Note (below the CONCERNS bar):** in branch mode a `deckle: save before sync` / `save before publish` tip (only written when the worktree was dirty) parses as op `save` on page "before sync", so Undo is disabled everywhere with "the last change was save on before sync" — rare, and the old behaviour there wasn't meaningful either.
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!174
No description provided.