Skip to content

feat(admin): full-screen entry editor layout - #70

Merged
nasyrov merged 3 commits into
mainfrom
feat/entry-editor-full-screen
Apr 24, 2026
Merged

nasyrov merged 3 commits into
mainfrom
feat/entry-editor-full-screen

Conversation

@nasyrov

@nasyrov nasyrov commented Apr 24, 2026

Copy link
Copy Markdown
Contributor

Summary

Replaces the bordered-form-in-shell entry editor with a full-screen canvas + right rail, modelled on WP Gutenberg / Builder.io / Ghost. The editor is the page — no admin shell, no left nav. Every registered meta box lives in the right rail as a collapsible accordion section alongside built-in panels (Permalink / Status / Excerpt).

Entry URLs are unchanged — /entries/\$slug/new and /entries/\$slug/\$id still resolve to the same browser path; only the outer layout route moves from _authenticated to a new _editor sibling (TanStack Router underscore layouts don't contribute to the URL).

Architecture

  • New _editor route group, sibling to _authenticated. Both call a shared requireAuthenticatedSession gate, but _editor drops the shell (no left nav, no ShellHeader).
  • PostEditorForm owns the layout: sticky header (back · headline · Save · sidebar toggle) + scrollable centered canvas (borderless h1 title + Tiptap) + shadcn Sidebar side="right" collapsible="offcanvas" for the rail.
  • Rail is a single <Accordion type="multiple"> — Permalink, Status, Excerpt, and plugin meta boxes all render as AccordionItems. All open by default (Gutenberg parity).

Feature gating via entryType.supports

PostEditorForm accepts supports: readonly string[]. Canvas + rail pieces render conditionally:

Feature Renders
title Borderless h1 input on the canvas
editor Tiptap below the title
slug Permalink accordion section
excerpt Excerpt accordion section

Default when entryType.supports is omitted: ["title", "editor", "slug"] (WP parity + slug, which is load-bearing for routing). Entry types that omit editor get a muted placeholder in the canvas and drive everything from the rail — mirrors WP Gutenberg's remove_post_type_support("editor") fallback.

Meta-box changes

  • MetaBoxCard splits into two variants over a shared MetaBoxFieldsGrid: MetaBoxCard (card shell — still used by term + user edit forms) and MetaBoxAccordionItem (new — used by the editor rail).
  • EntryMetaBoxOptions.location and EntryMetaBoxManifestEntry.location marked @deprecated. The admin no longer reads it; kept on the wire so older plugins don't fail manifest validation.
  • partitionBoxesByLocation deleted — every box lands in the rail.

Non-goals / follow-ups

  • MetaBoxFieldSpan keeps its responsive object form (base/sm/md/lg), but at the ~20rem rail width only base effectively applies. Can be simplified in a follow-up once we see real usage — other surfaces (settings pages) still benefit from the breakpoints.
  • Post-save "Saved" indicator, keyboard shortcut for save, block inserter — all future work. This PR is the stage; blocks ship on top.

Test plan

  • pnpm typecheck — 15 packages clean
  • pnpm lint — only the pre-existing data-table warning
  • pnpm format
  • pnpm test — 487 core + 86 admin unit tests green
  • pnpm test:e2e — all 50 admin Playwright scenarios green (meta-box test updated: both boxes now assert in the rail)
  • pnpm knip / pnpm publint / pnpm attw

Commits

  1. feat(admin): full-screen entry editor layout — routes moved, canvas + rail shell, rail stacked as Cards
  2. refactor(admin,core): rail Cards → Accordion sections; deprecate box.location

nasyrov added 2 commits April 24, 2026 07:25
Replaces the bordered-form-inside-shell entry editor with a dedicated
full-screen layout: sticky header + canvas + right rail. Modelled on
WP Gutenberg / Builder.io / Ghost — the editor *is* the page, not a
form embedded in one.

**Layout shape**
- New `_editor` route group, sibling to `_authenticated`. Same auth
  gate (hoisted into `requireAuthenticatedSession` in `lib/session.ts`)
  but no admin shell / left nav.
- Canvas: centered max-w-3xl, borderless h1-styled title input, Tiptap
  content editor filling the rest.
- Right rail: shadcn `Sidebar side="right" collapsible="offcanvas"`.
  Holds Permalink / Status / Excerpt cards plus every registered
  plugin meta box.

**Entry URLs unchanged.** `_editor` is an underscore layout route, so
`/entries/$slug/new` and `/entries/$slug/$id` render under the new
layout without the browser URL moving.

**`box.location` no longer honoured.** Every meta box lands in the
right rail; `partitionBoxesByLocation` is gone. The manifest field
stays declared so existing plugins don't break — it's just ignored. A
follow-up commit turns the rail cards into collapsible accordion
sections and formalises the deprecation.

**Feature gating.** `PostEditorForm` now takes an `entryType.supports`
list (`"title" | "editor" | "slug" | "excerpt"`) and renders canvas +
rail pieces conditionally. `DEFAULT_ENTRY_SUPPORTS = ["title",
"editor", "slug"]` matches WP's default (plus `slug`, which is
load-bearing for routing). Entry types that omit `editor` get a
muted placeholder in the canvas and drive everything from the rail —
mirrors WP's Gutenberg fallback for `remove_post_type_support`.

**Ships**
- Accordion shadcn primitive added for the meta-box follow-up.
- Test IDs preserved for e2e; `post-editor-{new,edit}-heading` collapse
  into `post-editor-headline` (the header caption). `meta-boxes-bottom`
  is gone — everything lives in `meta-boxes-sidebar` now.
…e box.location

Follow-up to the full-screen editor layout. Turns the flat stack of
Cards in the document rail into a single `<Accordion type="multiple">`
where every section — built-in (Permalink / Status / Excerpt) and
plugin-registered meta boxes — reads as the same kind of collapsible
panel. Matches WP Gutenberg / Builder.io / Notion: one consistent
rail with one interaction pattern per section.

**Changes**

- `components/meta-box/meta-box.tsx` now exports two variants over a
  shared `MetaBoxFieldsGrid` helper:
  - `MetaBoxCard` (card shell — unchanged behaviour, still used by
    term + user edit forms)
  - `MetaBoxAccordionItem` (new — used inside the entry editor rail)
- Entry editor's built-in rail sections (Permalink, Status, Excerpt)
  become `AccordionItem`s alongside plugin meta boxes. All open by
  default (Gutenberg parity); user collapses what they don't want.
- `EntryMetaBoxOptions.location` and `EntryMetaBoxManifestEntry.location`
  marked `@deprecated` with JSDoc. The admin editor no longer reads
  the field; it remains declared so plugins that set it don't fail
  manifest validation.

**Not touched**

- `MetaBoxFieldSpan`'s responsive object form (`base`/`sm`/`md`/`lg`)
  stays declared. In the ~20rem rail the breakpoints never activate,
  so only `base` effectively applies — but the full shape is still
  useful on wider surfaces (settings pages keep the 12-col grid at
  card width). Can be formally simplified in a later pass once real
  usage informs the choice.
Three follow-ups to the previous two commits on this branch, all from
running find-bugs / code-review / code-simplifier on the PR diff:

**Bugs**

1. `SidebarTrigger` inside the editor form defaulted to `type="submit"`
   (shadcn's `Button` doesn't set a default, and its onClick handler
   runs before — not instead of — the native form action). Clicking
   the rail toggle on an edit screen with valid fields would fire a
   spurious `entry.update`. Pass `type="button"` at the call site.

2. Skeleton aside used `w-(--sidebar-width)`, but the
   `--sidebar-width` CSS variable is set by `SidebarProvider`, which
   only mounts once the form itself renders. Hardcode `w-64` (matches
   shadcn's `SIDEBAR_WIDTH = "16rem"`) so the skeleton doesn't reflow
   when the real rail takes over.

**Design**

3. Move `ENTRIES_LIST_DEFAULT_SEARCH` from the list route's
   `-constants.ts` into a neutral `@/lib/entries.ts`. The editor
   routes now reach across layouts via a clean alias import instead
   of `../../../_authenticated/entries/$slug/-constants.js`.

4. Collapse the duplicated `entryType.supports ?? DEFAULT_ENTRY_SUPPORTS`
   at every `PostEditorForm` call-site: the fallback is applied
   internally, and `supports` is now optional on the component.

**Simplification**

5. Extract `RailSection` — the three built-in rail sections (Permalink /
   Status / Excerpt) shared identical `AccordionItem` + `AccordionTrigger`
   + `AccordionContent` wrappers with the same className overrides. Net
   ~18 lines saved and each section's JSX reads as "just the field".

6. Comment the `<div>` wrapper inside `ContentField`'s `FormControl` —
   it looks pointless, but it's load-bearing: shadcn's FormControl
   clones its single direct child via Radix `Slot.Root` to merge
   `id`/`aria-*`, and `TiptapEditor` (a component) doesn't forward DOM
   attrs, so the div is the Slot target.
@nasyrov
nasyrov merged commit 66adea3 into main Apr 24, 2026
14 checks passed
@nasyrov
nasyrov deleted the feat/entry-editor-full-screen branch April 24, 2026 07:03
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.

1 participant