Add page builder admin UI + public page route (step 5 + step 6 client)

- PagesAdmin: list view of pages (title/slug/status/protected/updated) with
  new/edit navigation and a View link to the live page.
- PageBuilder: full-page block canvas — palette (adds any registered block),
  per-block editor cards with show/hide, up/down + native drag reorder, and
  remove; Content / Settings tabs; SEO metadata + layout/nav settings panels;
  publish/unpublish; protect (PATCH) and password-gated unprotect (modal);
  draft preview (mints a token, opens /preview/:id/:token); delete (blocked
  while protected). Surfaces server block-validation details on save.
- CmsPage: public renderer for /:slug (published; staff see drafts) and the
  token-gated /preview/:id/:token, rendering blocks via BlockList and
  reflecting the page title/meta.
- Routing: /:slug catch-all after all named routes + /preview/:id/:token
  outside the maintenance gate; admin /admin/pages, /pages/new, /pages/:id.
- api client: public page/pagePreview + admin pages CRUD/unprotect/preview.
- AdminLayout: "Pages" nav entry (Content group) with icon.
- theme.css: builder canvas + preview-banner + shell-wide styles.

Client builds clean (216 modules).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-09 20:54:36 -05:00
parent 4d87c5f627
commit 1dd7603f54
8 changed files with 758 additions and 7 deletions

View File

@@ -79,6 +79,10 @@ a {
width: min(760px, calc(100% - 32px));
margin: 0 auto;
}
.shell-wide {
width: min(1280px, calc(100% - 32px));
margin: 0 auto;
}
.page {
min-height: 100vh;
display: flex;
@@ -884,3 +888,121 @@ button[disabled] {
font-size: 0.8rem;
line-height: 1;
}
/* ===== CMS page builder — admin canvas ===== */
.pb-toolbar {
display: flex;
align-items: center;
gap: 10px;
position: sticky;
top: 0;
z-index: 5;
padding: 10px 0;
background: var(--bg);
border-bottom: 1px solid var(--line);
}
.pb-error {
border: 1px solid #6e3b38;
background: rgba(110, 59, 56, 0.16);
color: #e6a9a3;
border-radius: 8px;
padding: 10px 14px;
margin-top: 14px;
font-size: 0.86rem;
}
.pb-notice {
border: 1px solid var(--accent);
background: var(--blue);
color: var(--accent-bright);
border-radius: 8px;
padding: 8px 14px;
margin-top: 14px;
font-size: 0.86rem;
}
.pb-tabs {
display: flex;
gap: 4px;
border-bottom: 1px solid var(--line);
margin-bottom: 18px;
}
.pb-tab {
background: transparent;
border: none;
border-bottom: 2px solid transparent;
color: var(--muted);
font-family: var(--sans);
font-size: 0.9rem;
padding: 10px 16px;
cursor: pointer;
}
.pb-tab.is-active {
color: var(--ink);
border-bottom-color: var(--accent);
}
.pb-palette {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 8px;
padding: 12px;
border: 1px dashed var(--line);
border-radius: 10px;
margin-bottom: 16px;
}
.pb-canvas {
display: flex;
flex-direction: column;
gap: 14px;
}
.pb-block-card {
border: 1px solid var(--line);
border-radius: 10px;
background: var(--panel-flat, transparent);
}
.pb-block-card.is-dragging {
opacity: 0.5;
}
.pb-block-card.is-hidden {
opacity: 0.6;
}
.pb-block-head {
display: flex;
align-items: center;
gap: 8px;
padding: 8px 12px;
border-bottom: 1px solid var(--line);
}
.pb-drag {
cursor: grab;
color: var(--muted);
user-select: none;
}
.pb-block-body {
padding: 14px;
}
.pb-settings {
display: flex;
flex-direction: column;
gap: 16px;
max-width: 720px;
}
.pb-danger {
border-color: #6e3b38;
color: #d98b84;
}
.pb-danger:hover:not([disabled]) {
background: rgba(110, 59, 56, 0.18);
border-color: #8a4b47;
}
/* Draft-preview banner on the public renderer. */
.page-preview-banner {
border: 1px solid var(--accent);
background: var(--blue);
color: var(--accent-bright);
border-radius: 8px;
padding: 8px 14px;
margin-bottom: 20px;
font-size: 0.85rem;
text-align: center;
}