feat(editor): the element History sheet and its two menu entries #284

Closed
opened 2026-10-14 13:01:53 +02:00 by dominik.polakovics · 0 comments

Summary

Give the editor the read-only element history: a History… entry in the element card's three-dot menu and in the element form's More menu, opening a sheet that lists the publishes that changed the element with a per-field diff, exactly as the mockup in docs/mockups/element-history/ shows (issue #282; artifact https://claude.ai/artifact/22NW7Pjqc1QUSUEJCxv1qT, private to the owner). The data comes from GET /api/element/history (#283); the client renders what the server computed and derives nothing.

Current behavior

  • The element card (web/src/components/ElementCard.tsx) has a menu with Move up, Move down, Copy, Cut, Duplicate, Paste inside and Delete (element.menu.* catalog keys); the element form's docheader has a More menu with the same actions (form.menu.*). Neither has a history entry.
  • The editor's sheet component (web/src/components/Sheet.tsx) is the bottom sheet the pickers and dialogs open in; the pending-changes panel behind the "N changes" chip is the closest existing list of changes.
  • Undo's tooltip (web/src/undoinfo.ts) is the only place the editor shows anything about history.

Desired behavior

  • Entry points. History… in both menus, below the clipboard group and above Delete, with a clock icon, as the mockup's frames 1 and 2 show. The card menu entry opens the sheet for that element without opening the form; the form's entry opens it over the form. A guard on unsaved form edits is not needed: the sheet is read-only and the form stays mounted beneath it.
  • The sheet (mockup frame 3): header with the eyebrow "History", the element's label as title, and a subtitle naming the element type, the page title, the language and the uid; a close control.
  • Rows. One per entry from the API, newest first, each a fold: a chevron, a badge (Not yet published · Changed · Created · Removed, the mockup's colours from the editor's state tokens), the changed fields named in words (field labels joined, an items collection as " 1 row added"), the date and time in the interface language, and the author name under it. The pending entry is the first row with the blue badge; it reads "working tree, not on main" where the others show the author. The first two rows open by default; the rest fold.
  • The diff. Per field, a label column and the change: inline word diff for text-like widgets, rendered from the server's equal/deleted/inserted runs (deleted in the red tokens, inserted in the green); before and after boxes for scalars, choices and links; thumbnails plus the media ref for imagePicker; item rows as a list marked kept, added, removed or changed, a changed row listing its own field changes; enabled as Hidden/Visible using the switch's on/off labels. created shows before as "none". On a phone the row stacks and the pair columns become one, as the mockup's narrow layout does.
  • Footer. "N entries in the last 1,000 publishes" and a Show older button when the answer carried next; it fetches the next window with before and appends the rows; when the chain is exhausted the button reads "No older entries" and is disabled. A request in flight disables the button.
  • States. Loading shows the sheet with a placeholder; an unavailable answer shows the reason in the sheet body (the coded message translated as everywhere); a 404 for the uid closes with the coded message in the editor's usual error surface. Opening the sheet performs the one request; nothing is fetched before the user asks for the history (the server call is the whole cost; the editor never polls it).
  • i18n. Every string is a catalog key in en.json and de.json following web/src/i18n/glossary.md's register (German: "Verlauf" for History, "Noch nicht veröffentlicht", "Geändert", "Erstellt", "Entfernt", "Ältere anzeigen"); the parity and leak tests pass.
  • Accessibility. The sheet is a dialog with the title as its label; rows are disclosure buttons reachable by keyboard with the visible focus state the editor uses; the diff colours are never the only signal (the tags Before/After, kept/added/removed stay).

Decisions

  • Both menus, same position — editors reach the history from the page module without opening the form, and from the form while editing; one position in both keeps the menus parallel.
  • The client renders only — labels, widgets, the word diff and the row kinds come from the server (CLAUDE.md: the client never re-derives a rule).
  • Fold rows, first two open — the page is complete at rest and the newest changes are what an editor looks for.
  • No restore control — v1 is read-only; a later increment adds restore as a forward save.
  • Show older, not infinite scroll — the window is a cost bound chosen by the user, and a button makes the cost visible.

Key interfaces

  • GET /api/element/history as docs/api.md documents it after #283 lands: the response's identity block, pending, entries, next, unavailable
  • web/src/api/ — a client for the route in the shape the other API modules take
  • ElementCard.tsx and the form docheader's More menu (ElementForm.tsx, Docheader.tsx) — the new entry; Sheet.tsx — the container
  • A new HistorySheet component and its vitest suite under web/src/test/, using the fixture pattern the other sheet tests use
  • web/src/i18n/en.json, de.json — the keys; glossary.md if a new term is introduced
  • Starting hints: web/src/components/Sheet.tsx, ElementCard.tsx, ElementForm.tsx, the pending panel component behind the changes chip, web/src/test/Sheet.test.tsx, i18nParity.test.ts

Acceptance criteria

  • History… appears in the element card menu and in the form's More menu, below the clipboard group and above Delete, in both languages
  • Choosing it opens the sheet and performs exactly one history request for the element's uid and the current language; nothing is requested before
  • Rows render badge, field summary, date, time and author as specified; the pending row is first and blue; the first two rows are open
  • Each widget kind renders its diff as specified: inline word diff, before/after pair, media thumbnails, item rows, enabled as Hidden/Visible, created with "none" before
  • Show older appears only with next, appends the next window without duplicates, and ends disabled as "No older entries"
  • unavailable and 404 are shown with the translated coded message; no raw code reaches the user
  • Phone width (390 px): no horizontal scroll, the row and the pairs stack
  • Keyboard: rows toggle with Enter/Space, the sheet closes with Escape and returns focus to the control that opened it
  • All vitest suites pass, including the i18n parity, leak and source tests; go test ./... still passes (the embedded web/dist builds)

How to verify

  • cd web && npm test; make build and ./harness serve --site testdata/site on a checkout whose testdata/site has a scripted main with a few publishes, then open the history from both menus and compare with the mockup (sh docs/mockups/editing-screens/wrap.sh docs/mockups/element-history/element-history.html > /tmp/h.html)
  • A vitest suite that mounts the sheet with a fixture answer covering every entry kind and widget, the pending row, next, unavailable and 404

Assumptions to check

  • The exact response shape of GET /api/element/history as landed by #283 (field names, the runs of the word diff, how enabled's labels travel); read docs/api.md, do not work from this brief's prose
  • That the form's More menu and the card's menu share no component today; if they do, one entry serves both

Out of scope

  • Restoring a version, or any write from the sheet
  • Page-level history, or a history entry in the page menu
  • Showing moves between pages
  • Changing the server's response or computing any diff client-side

Blocked by

## Summary Give the editor the read-only element history: a `History…` entry in the element card's three-dot menu and in the element form's More menu, opening a sheet that lists the publishes that changed the element with a per-field diff, exactly as the mockup in `docs/mockups/element-history/` shows (issue #282; artifact https://claude.ai/artifact/22NW7Pjqc1QUSUEJCxv1qT, private to the owner). The data comes from `GET /api/element/history` (#283); the client renders what the server computed and derives nothing. ## Current behavior - The element card (`web/src/components/ElementCard.tsx`) has a menu with Move up, Move down, Copy, Cut, Duplicate, Paste inside and Delete (`element.menu.*` catalog keys); the element form's docheader has a More menu with the same actions (`form.menu.*`). Neither has a history entry. - The editor's sheet component (`web/src/components/Sheet.tsx`) is the bottom sheet the pickers and dialogs open in; the pending-changes panel behind the "N changes" chip is the closest existing list of changes. - Undo's tooltip (`web/src/undoinfo.ts`) is the only place the editor shows anything about history. ## Desired behavior - **Entry points.** `History…` in both menus, below the clipboard group and above Delete, with a clock icon, as the mockup's frames 1 and 2 show. The card menu entry opens the sheet for that element without opening the form; the form's entry opens it over the form. A guard on unsaved form edits is not needed: the sheet is read-only and the form stays mounted beneath it. - **The sheet** (mockup frame 3): header with the eyebrow "History", the element's label as title, and a subtitle naming the element type, the page title, the language and the uid; a close control. - **Rows.** One per entry from the API, newest first, each a fold: a chevron, a badge (Not yet published · Changed · Created · Removed, the mockup's colours from the editor's state tokens), the changed fields named in words (field labels joined, an items collection as "<label> 1 row added"), the date and time in the interface language, and the author name under it. The `pending` entry is the first row with the blue badge; it reads "working tree, not on main" where the others show the author. The first two rows open by default; the rest fold. - **The diff.** Per field, a label column and the change: inline word diff for text-like widgets, rendered from the server's equal/deleted/inserted runs (deleted in the red tokens, inserted in the green); before and after boxes for scalars, choices and links; thumbnails plus the media ref for `imagePicker`; item rows as a list marked kept, added, removed or changed, a changed row listing its own field changes; `enabled` as Hidden/Visible using the switch's on/off labels. `created` shows before as "none". On a phone the row stacks and the pair columns become one, as the mockup's narrow layout does. - **Footer.** "N entries in the last 1,000 publishes" and a `Show older` button when the answer carried `next`; it fetches the next window with `before` and appends the rows; when the chain is exhausted the button reads "No older entries" and is disabled. A request in flight disables the button. - **States.** Loading shows the sheet with a placeholder; an `unavailable` answer shows the reason in the sheet body (the coded message translated as everywhere); a `404` for the uid closes with the coded message in the editor's usual error surface. Opening the sheet performs the one request; nothing is fetched before the user asks for the history (the server call is the whole cost; the editor never polls it). - **i18n.** Every string is a catalog key in `en.json` and `de.json` following `web/src/i18n/glossary.md`'s register (German: "Verlauf" for History, "Noch nicht veröffentlicht", "Geändert", "Erstellt", "Entfernt", "Ältere anzeigen"); the parity and leak tests pass. - **Accessibility.** The sheet is a dialog with the title as its label; rows are disclosure buttons reachable by keyboard with the visible focus state the editor uses; the diff colours are never the only signal (the tags Before/After, kept/added/removed stay). ## Decisions - **Both menus, same position** — editors reach the history from the page module without opening the form, and from the form while editing; one position in both keeps the menus parallel. - **The client renders only** — labels, widgets, the word diff and the row kinds come from the server (CLAUDE.md: the client never re-derives a rule). - **Fold rows, first two open** — the page is complete at rest and the newest changes are what an editor looks for. - **No restore control** — v1 is read-only; a later increment adds restore as a forward save. - **Show older, not infinite scroll** — the window is a cost bound chosen by the user, and a button makes the cost visible. ## Key interfaces - `GET /api/element/history` as `docs/api.md` documents it after #283 lands: the response's identity block, `pending`, `entries`, `next`, `unavailable` - `web/src/api/` — a client for the route in the shape the other API modules take - `ElementCard.tsx` and the form docheader's More menu (`ElementForm.tsx`, `Docheader.tsx`) — the new entry; `Sheet.tsx` — the container - A new `HistorySheet` component and its vitest suite under `web/src/test/`, using the fixture pattern the other sheet tests use - `web/src/i18n/en.json`, `de.json` — the keys; `glossary.md` if a new term is introduced - Starting hints: `web/src/components/Sheet.tsx`, `ElementCard.tsx`, `ElementForm.tsx`, the pending panel component behind the changes chip, `web/src/test/Sheet.test.tsx`, `i18nParity.test.ts` ## Acceptance criteria - [ ] `History…` appears in the element card menu and in the form's More menu, below the clipboard group and above Delete, in both languages - [ ] Choosing it opens the sheet and performs exactly one history request for the element's uid and the current language; nothing is requested before - [ ] Rows render badge, field summary, date, time and author as specified; the pending row is first and blue; the first two rows are open - [ ] Each widget kind renders its diff as specified: inline word diff, before/after pair, media thumbnails, item rows, enabled as Hidden/Visible, created with "none" before - [ ] `Show older` appears only with `next`, appends the next window without duplicates, and ends disabled as "No older entries" - [ ] `unavailable` and `404` are shown with the translated coded message; no raw code reaches the user - [ ] Phone width (390 px): no horizontal scroll, the row and the pairs stack - [ ] Keyboard: rows toggle with Enter/Space, the sheet closes with Escape and returns focus to the control that opened it - [ ] All vitest suites pass, including the i18n parity, leak and source tests; `go test ./...` still passes (the embedded `web/dist` builds) ## How to verify - `cd web && npm test`; `make build` and `./harness serve --site testdata/site` on a checkout whose `testdata/site` has a scripted `main` with a few publishes, then open the history from both menus and compare with the mockup (`sh docs/mockups/editing-screens/wrap.sh docs/mockups/element-history/element-history.html > /tmp/h.html`) - A vitest suite that mounts the sheet with a fixture answer covering every entry kind and widget, the pending row, `next`, `unavailable` and `404` ## Assumptions to check - The exact response shape of `GET /api/element/history` as landed by #283 (field names, the runs of the word diff, how `enabled`'s labels travel); read `docs/api.md`, do not work from this brief's prose - That the form's More menu and the card's menu share no component today; if they do, one entry serves both ## Out of scope - Restoring a version, or any write from the sheet - Page-level history, or a history entry in the page menu - Showing moves between pages - Changing the server's response or computing any diff client-side ## Blocked by - #283
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#284
No description provided.