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

Merged
dominik.polakovics merged 1 commit from afk/284 into main 2026-10-14 19:13:25 +02:00

Closes #284

What

  • History… in the element card's ⋮ menu and the form's More menu, a clock icon, in its own group directly above Delete… in both (the two menus put Move up/down between the clipboard group and Delete, so "above Delete" is the one position that is the same in both). A connected translation's locked card menu keeps it: the history is read-only and each language has its own.
  • HistorySheet (web/src/components/HistorySheet.tsx) on Sheet: the eyebrow "History", the title, a subtitle naming the element type, page, language and uid, and one request on open, with no polling. Rows are disclosure buttons (aria-expanded/aria-controls). The pending row comes first with the blue badge and "working tree, not on main". The first two rows are open. The per-field diff dispatches on kind/widget, never on field: the server's word runs as del/ins; Before/After boxes drawn by the widget's own read-only display (image thumbnail + ref, link, choice…); item rows tagged kept/added/removed/changed with their own field changes; Enabled shown with the switch's on/off hints; an absent side shows "none".
  • Footer: "N entries in the last 1,000 publishes" and Show older only when the answer had next. It sends before, appends rows without duplicates (by commit) and is disabled while a request runs. Once the chain is exhausted it reads "No older entries" and stays disabled.
  • States: a loading placeholder. unavailable shows its reason translated by its code. A 404 closes the sheet and shows the translated message in the screen's element-action-error strip. Other errors show in the sheet body.
  • web/src/api/history.ts: a decoder and fetchElementHistory, status-driven (ok | notFound | error).
  • One owner: translating unavailable was written twice before (publish.ts, stores/pageTree.ts). It is now one unavailableReason in api/client.ts, used by all three callers. The switch's hint is exported (switchHint) instead of being copied.
  • i18n: history.*, element.menu.history and form.menu.history in en.json/de.json ("Verlauf", "Noch nicht veröffentlicht", "Geändert", "Erstellt", "Entfernt", "Ältere anzeigen"). The glossary gains history / history entry (Änderung) / working tree (Arbeitskopie) / before, after.
  • Docs: one sentence in docs/architecture.md's element-history paragraph. The scaffolded AGENTS.md and skills are not affected, because the editor menus are not part of what they document.

Decisions where the brief was silent

  • The pending row has no date, only "working tree, not on main".
  • The title is the card's served summary when it has one, with the element label in the subtitle (the mockup's "Our workshop · Text with media"). Without a summary, the label is the title.
  • A kept item row shows only its tag, because the server sends no fields for kept rows.
  • The footer count excludes the pending row. After a Show older it reads "N entries so far". On the last window it reads "N entries, whole history loaded".
  • Inserted words are also underlined, so colour is never the only signal.

Verification

  • cd web && npx tsc --noEmit is clean. npm test passes: 126 files, 3255 tests, including i18nParity, i18nLeak and the source tests. npm run build succeeds.
  • make build, go vet ./... and go test ./... are green.
  • New web/src/test/HistorySheet.test.tsx (21 tests) runs against one fixture that covers every entry kind and widget, plus next, unavailable, 404 and the keyboard. Menu tests in ElementCrud, FormScreen and ConnectedGuard check the position in both languages, exactly one request and none before, that the form is not opened from the card and stays mounted from the form, a 404 reaching the error strip, and Escape returning focus to the menu trigger.
  • Not done: the sheet has not been rendered in a browser. The 390 px result comes from the mobile-first CSS: grids use minmax(0, …), and @container shell (min-width: 721px) switches to the wide layout.

🤖 Generated with Claude Code

Closes #284 ## What - **`History…`** in the element card's ⋮ menu and the form's More menu, a clock icon, in its own group directly above Delete… in both (the two menus put Move up/down between the clipboard group and Delete, so "above Delete" is the one position that is the same in both). A connected translation's locked card menu keeps it: the history is read-only and each language has its own. - **`HistorySheet`** (`web/src/components/HistorySheet.tsx`) on `Sheet`: the eyebrow "History", the title, a subtitle naming the element type, page, language and uid, and one request on open, with no polling. Rows are disclosure buttons (`aria-expanded`/`aria-controls`). The pending row comes first with the blue badge and "working tree, not on main". The first two rows are open. The per-field diff dispatches on `kind`/`widget`, never on `field`: the server's word runs as `del`/`ins`; Before/After boxes drawn by the widget's own read-only display (image thumbnail + ref, link, choice…); item rows tagged kept/added/removed/changed with their own field changes; Enabled shown with the switch's on/off hints; an absent side shows "none". - **Footer**: "N entries in the last 1,000 publishes" and `Show older` only when the answer had `next`. It sends `before`, appends rows without duplicates (by commit) and is disabled while a request runs. Once the chain is exhausted it reads "No older entries" and stays disabled. - **States**: a loading placeholder. `unavailable` shows its reason translated by its code. A 404 closes the sheet and shows the translated message in the screen's `element-action-error` strip. Other errors show in the sheet body. - **`web/src/api/history.ts`**: a decoder and `fetchElementHistory`, status-driven (`ok | notFound | error`). - **One owner**: translating `unavailable` was written twice before (`publish.ts`, `stores/pageTree.ts`). It is now one `unavailableReason` in `api/client.ts`, used by all three callers. The switch's hint is exported (`switchHint`) instead of being copied. - **i18n**: `history.*`, `element.menu.history` and `form.menu.history` in `en.json`/`de.json` ("Verlauf", "Noch nicht veröffentlicht", "Geändert", "Erstellt", "Entfernt", "Ältere anzeigen"). The glossary gains history / history entry (Änderung) / working tree (Arbeitskopie) / before, after. - **Docs**: one sentence in `docs/architecture.md`'s element-history paragraph. The scaffolded AGENTS.md and skills are not affected, because the editor menus are not part of what they document. ## Decisions where the brief was silent - The pending row has no date, only "working tree, not on main". - The title is the card's served summary when it has one, with the element label in the subtitle (the mockup's "Our workshop · Text with media"). Without a summary, the label is the title. - A kept item row shows only its tag, because the server sends no fields for kept rows. - The footer count excludes the pending row. After a Show older it reads "N entries so far". On the last window it reads "N entries, whole history loaded". - Inserted words are also underlined, so colour is never the only signal. ## Verification - `cd web && npx tsc --noEmit` is clean. `npm test` passes: 126 files, 3255 tests, including i18nParity, i18nLeak and the source tests. `npm run build` succeeds. - `make build`, `go vet ./...` and `go test ./...` are green. - New `web/src/test/HistorySheet.test.tsx` (21 tests) runs against one fixture that covers every entry kind and widget, plus `next`, `unavailable`, 404 and the keyboard. Menu tests in `ElementCrud`, `FormScreen` and `ConnectedGuard` check the position in both languages, exactly one request and none before, that the form is not opened from the card and stays mounted from the form, a 404 reaching the error strip, and Escape returning focus to the menu trigger. - Not done: the sheet has not been rendered in a browser. The 390 px result comes from the mobile-first CSS: grids use `minmax(0, …)`, and `@container shell (min-width: 721px)` switches to the wide layout. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
feat(editor): the element History sheet and its two menu entries
All checks were successful
ci / build-and-test (pull_request) Successful in 3m19s
ci / release (pull_request) Has been skipped
eea2c6a632
History… opens a read-only sheet of the element's publish history from
GET /api/element/history: a fold per entry (the pending row first and
blue, the first two open), badges, field summaries by the server's
labels, date, time and author, and per-field diffs dispatched on kind and
widget — the server's word runs inline, Before/After boxes drawn by each
widget's read-only display, media thumbnails, item rows by status,
Enabled by the switch's own hints, "none" for an absent side. Show older
pages with `next`; `unavailable` and a 404 show the translated coded
message.

The entry sits directly above Delete… in both the element card's menu
(kept in a connected translation's locked menu) and the form's More menu.
The `unavailable` translation, written twice before, is one helper in
api/client.ts; the switch's hint is exported for the Enabled diff.

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

[autoland] verdict: pass

PASS — validated by the lander.

Signal: labctl pr checks 290 aggregate success. ci / build-and-test ran npm ci, npm run build, npm test (vitest, i18n parity/leak/source tests included), go vet, go test and go build on this exact head, so it covers every file in the diff. Nothing was re-run locally.

Conventions: the Conventional Commits title is feat(editor). Closes #284 is present. The branch is based on main's tip (5079051), so there is no conflict.

Scope: in line with #284's brief. Both menu entries sit in their own group directly above Delete… (ElementCard.tsx elementMenuEntries, FormScreen.tsx menuEntries), and the locked menu keeps History…. HistorySheet.tsx renders the server's kinds, widgets, word runs and row statuses, and the decoder (api/history.ts) matches docs/api.md "GET /api/element/history". Show older dedupes rows by commit and ends disabled. A 404 goes to the element-action strip, and unavailable is translated by its code. The edits to Sheet.tsx (eyebrow/detail) and the unavailableReason consolidation in publish.ts and pageTree.ts follow CLAUDE.md's one-owner rule and are not drive-bys. The CLAUDE.md doc rules are met: one sentence in docs/architecture.md, no new route, and nothing in AGENTS.md or the skills enumerates the element menu.

Nit, not blocking: valueSide in HistorySheet.tsx gives every Before/After display the same id="history-value". A switch-widget param would therefore render duplicate input ids. It has no functional effect, because the switch is named by aria-label.

[autoland] verdict: pass PASS — validated by the lander. Signal: `labctl pr checks 290` aggregate **success**. ci / build-and-test ran npm ci, npm run build, npm test (vitest, i18n parity/leak/source tests included), go vet, go test and go build on this exact head, so it covers every file in the diff. Nothing was re-run locally. Conventions: the Conventional Commits title is feat(editor). `Closes #284` is present. The branch is based on main's tip (5079051), so there is no conflict. Scope: in line with #284's brief. Both menu entries sit in their own group directly above Delete… (ElementCard.tsx elementMenuEntries, FormScreen.tsx menuEntries), and the locked menu keeps History…. HistorySheet.tsx renders the server's kinds, widgets, word runs and row statuses, and the decoder (api/history.ts) matches docs/api.md "GET /api/element/history". Show older dedupes rows by commit and ends disabled. A 404 goes to the element-action strip, and `unavailable` is translated by its code. The edits to Sheet.tsx (eyebrow/detail) and the `unavailableReason` consolidation in publish.ts and pageTree.ts follow CLAUDE.md's one-owner rule and are not drive-bys. The CLAUDE.md doc rules are met: one sentence in docs/architecture.md, no new route, and nothing in AGENTS.md or the skills enumerates the element menu. Nit, not blocking: valueSide in HistorySheet.tsx gives every Before/After display the same `id="history-value"`. A switch-widget param would therefore render duplicate input ids. It has no functional effect, because the switch is named by aria-label.
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!290
No description provided.