Skip to content

feat(layouts): Design cover and standalone pages in the layout editor - #100

Merged
patriksimms merged 7 commits into
mainfrom
feat/designed-static-pages
Aug 30, 2026
Merged

feat(layouts): Design cover and standalone pages in the layout editor#100
patriksimms merged 7 commits into
mainfrom
feat/designed-static-pages

Conversation

@patriksimms

@patriksimms patriksimms commented Aug 29, 2026

Copy link
Copy Markdown
Owner

Closes #92

Outcome

Cover and standalone pages are designed in the same visual editor as response layouts, instead of being a title and a body paragraph.

Every layout now has a role:

Role Count Where it goes
Response layout any assigned to submissions during generation
Front cover at most one first tab, first book page
Back cover at most one last tab, last book page
Standalone page any inserted anywhere in the book from Book review

The role is chosen in the existing "New layout" dialog, alongside the background. Covers are pinned by role, not by position, so neither regeneration, layout reordering, layout deletion, nor page drag-and-drop can move them out of place — the server re-pins whatever page order the client sends. Layouts without a response behind them offer only response-independent palette actions (static text, shapes, decorative images), because a bound element there would always render empty.

Standalone book pages now reference a layout, and preview and PDF export both render them through the shared element renderer that already draws response pages. That let the separate text-only drawing path in pdf-renderer.ts go away.

Migration

Books saved before this change carry pageType/title/body/background on their standalone pages. They are converted on load and before regeneration: one layout per page, reproducing the old serif-title-over-sans-body rendering, then the page is rewritten to reference it. A legacy page claims a cover role only if it already sat first (cover) or last (closing), so the conversion never reorders an existing book. It probes cheaply and is idempotent, so a project pays for it once.

bookStatus is deliberately left untouched by the conversion: the rendering is equivalent, and forcing every existing project to stale on first load after deploy would be a worse outcome than leaving it current.

Validation

  • CI verify green (format, lint, typecheck, unit, integration, e2e, build, compose checks)
  • New unit suites cover layout roles, the legacy conversion, cover pinning across regeneration and reordering, dropping a standalone page whose layout was deleted, and standalone-page problem inspection
  • 5 new repository integration tests cover the single-cover rule, reorder pinning, cover duplication, delete repositioning, and the on-load conversion including idempotency

Visual evidence

Captured on the seeded demo project, 1440×900, light theme; "before" is origin/main, "after" is this branch, against the same database so the same book is shown.

Layout tab bar. The front cover is pinned first and the back cover last; a response layout is selected in both, so the palette is unchanged.

Before After
before after

New layout dialog. Choosing what the layout is for now sits above the background choices.

Before After
before after

Standalone page in Book review. The same page before and after the on-load conversion: it keeps its title and body, but now renders through the layout renderer and is edited in the layout editor. Its up/down arrows are disabled because the front cover is pinned.

Before After
before after

Cover layout in the editor (after only). The converted front cover open for editing. There is no equivalent "before" because cover layouts did not exist. The palette offers no response-bound action.

cover layout palette

Notes for the reviewer

  • Capturing the evidence found a bug, fixed in 273ae14: the new role picker rendered the raw submission value instead of "Response layout", because it did not pass items to the Select the way every other picker in the editor does.
  • One unrelated commit. build: keep editor caches out of the formatter adds .vscode/** to .oxfmtrc.json. .vscode/PythonImportHelper-v2-Completion.json is a VS Code extension cache pushed directly to main in f14a8f7; it fails format:check on a clean checkout and blocks every pull request. Ignoring editor state was the narrow unblock — the file itself is probably better removed from the repository separately.

Deployment

Needs bun run db:migrate: drizzle/20260829162036_gifted_menace.sql adds layouts.role (defaulting to submission, so existing rows keep working) plus two partial unique indexes enforcing one front and one back cover per project. No other out-of-repository configuration is required.

Summary:
- give every layout a role: response, front cover, back cover, or
  standalone page
- pin the front-cover layout first and the back-cover layout last in
  the layout tab bar, in the generated book, and across reordering
- hide response-bound palette actions on layouts that have no
  submission behind them
- back standalone book pages with a layout instead of their own title,
  body, and background, and render them through the shared element
  renderer in preview and PDF export
- convert books saved with text-only standalone pages on load, creating
  one layout per page

Rationale:
- a standalone page could only hold one heading and one paragraph, so
  covers could not carry artwork, shapes, or freely placed text
- reusing the layout editor keeps one authoring surface and one
  renderer instead of a second, weaker text-only path
- covers are pinned by role rather than position so regeneration and
  page reordering cannot move them out of place
- the conversion runs on read because a stored book has to render
  correctly before the organizer regenerates it; it is idempotent and
  probes cheaply so the read path pays for it only once

Tests:
- bun run format:check, lint, typecheck, build
- 263 unit tests across src/domain and src/components passed
- integration and end-to-end suites not run locally: this machine has
  no Docker access, so Postgres and the seeded dev server are
  unavailable; CI runs them

Closes #92

AI-Assisted: true
AI-Agent: claude-code
AI-Model: anthropic/claude-opus-5
Comment thread src/domain/book.ts
Comment thread src/domain/standalone-page-migration.ts
Comment thread src/server/repository.ts
Comment thread src/domain/standalone-page-migration.ts
Comment thread src/domain/standalone-page-migration.ts
Comment thread src/domain/generation.ts
Comment thread src/server/db/schema.ts
Summary:
- ignore .vscode/** in .oxfmtrc.json

Rationale:
- .vscode/PythonImportHelper-v2-Completion.json is a VS Code extension
  cache committed directly to main in f14a8f7; it fails format:check on
  a clean checkout and blocks every pull request
- editor state is not source, so excluding it is the narrow fix; the
  file itself is better removed from the repository separately

Tests:
- bun run format:check

AI-Assisted: true
AI-Agent: claude-code
AI-Model: anthropic/claude-opus-5
Summary:
- run the standalone-page conversion at the start of
  generateProjectBook
- extract the static-text content check in inspectStandalonePage into a
  named local

Rationale:
- regeneration reads the stored book directly rather than through
  getProject, so a book that had never been loaded still held legacy
  pages; generateBook could not resolve their layout and dropped them
  instead of carrying them over

Tests:
- bun run format:check, lint, typecheck
- 191 unit tests in src/domain passed

AI-Assisted: true
AI-Agent: claude-code
AI-Model: anthropic/claude-opus-5
The exporter no longer draws standalone pages itself; they render
through the shared layout element path like every other page.

Tests:
- bun run typecheck, lint, format:check
- 5 PDF renderer tests passed

AI-Assisted: true
AI-Agent: claude-code
AI-Model: anthropic/claude-opus-5
@patriksimms
patriksimms marked this pull request as ready for review August 29, 2026 17:02
Addresses the Macroscope review on #100.

Summary:
- copy layouts.role in duplicateProject, which silently reset every
  cover and standalone layout to a response layout in the copy
- offer only response layouts in the Book review page-layout selector,
  so a manual assignment cannot be made to a layout generation would
  discard
- reject page sets that PATCH /book must never persist: a response page
  on a non-response layout, a standalone page on a response layout, a
  duplicated cover, or a new page on a layout the project does not own
- reproduce the old exporter's standalone geometry in the conversion:
  15 mm side margin, title baseline at 68 % of trim height, body
  baseline 14 mm below on a 16 pt grid, body stopping 12 mm above the
  trim edge
- shrink migrated titles and truncate migrated bodies, as the old
  exporter did

Rationale:
- roles are only meaningful if every write path preserves them; project
  duplication and the book PATCH both bypassed them
- a stored page keeps its layout even when that layout is gone, so
  deleting a layout still leaves a stale book editable
- the migrated body previously inherited overflow "flag", which renders
  every line past the box and raises a blocking overflow problem; an
  already-exported book would have become un-exportable on load

Tests:
- bun run format:check, lint, typecheck, build
- 273 unit tests passed, including new suites for page validation and
  the reproduced legacy geometry across A5, A6, and A4
- 3 new integration tests for duplication, page rejection, and stale
  book reordering; not run locally (no Docker), CI runs them

AI-Assisted: true
AI-Agent: claude-code
AI-Model: anthropic/claude-opus-5
The duplication test asserted presentation order, but getProject
returns layouts in stored position order and duplicateProject copies
positions verbatim; only the UI derives cover-first ordering from the
role. Comparing name and role against the source project tests the
property that matters, which is that duplication preserves roles.

Tests:
- bun run format:check, lint, typecheck

AI-Assisted: true
AI-Agent: claude-code
AI-Model: anthropic/claude-opus-5
Summary:
- pass items to the role Select so it renders "Response layout" rather
  than the raw "submission" value
- add before/after screenshots for issue #92

Rationale:
- base-ui's Select.Value resolves its label from the items prop, which
  every other Select in the editor passes; without it the trigger shows
  the stored enum value
- caught while capturing the visual evidence for the pull request

Tests:
- bun run format:check, lint, typecheck
- 72 component tests passed

AI-Assisted: true
AI-Agent: claude-code
AI-Model: anthropic/claude-opus-5
@patriksimms
patriksimms merged commit 62080b3 into main Aug 30, 2026
3 checks passed
@patriksimms
patriksimms deleted the feat/designed-static-pages branch August 30, 2026 19:13
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat(layouts): Design cover and standalone pages in the layout editor

1 participant