Unify admin editors into a tabbed EditorShell (site, structure, article, banner)

- One mounted shell with four kept-alive tabs; pens open/switch tabs,
  closing only hides the shell so unsaved page-editor state survives
  until a real reload; admin panels never reload the page, regions are
  refreshed in place via the shared swapdoc.js helper
- SiteEditor split: site settings vs. the new StructureEditor tree tab;
  BannerEditor split out of the old site editor
- Article editor format bar (bold/italic/code/link/table/image) with a
  table size picker and Ctrl/Cmd-B/I/S bindings; fenced code block on an
  empty line; icon-only save button disabled (saturate(0)) when clean
- Media uploads use icon buttons; favicon upload by clicking the preview
  tile; placeholders reserved for actual defaults in effect
- Banner/site pens and auth buttons grouped in an .editor-pens container;
  page pen no longer shown to anonymous visitors
- While editing, window scroll is locked, the panel exactly fills the
  available window height, and only #main scrolls; editor scroll drives
  the article
- Banner design inherit label names the design and its true source;
  backend banner_design_source excludes the node's own setting and the
  doc payload carries banner_design_inherited
This commit is contained in:
2026-08-19 00:14:05 +00:00
parent 43b841ebb6
commit 4dac16a9ac
13 changed files with 1673 additions and 1019 deletions
+4 -5
View File
@@ -1,6 +1,6 @@
<script setup>
// Recursive site-structure tree with drag-and-drop ordering (vue-draggable).
// Nodes come from the server (GET /_api/pages via SiteEditor.vue) as
// Nodes come from the server (GET /_api/pages via StructureEditor.vue) as
// {slug, path, title, order, published, has_content, children}.
// Every node is real: a label whose title and slug are always editable
// inline — the title saves while typing (and focusing it opens the page),
@@ -19,8 +19,8 @@
// The front page is the root row with an empty slug: renaming it away
// leaves no front page, and giving another top-level row the empty slug
// makes it the front page. Delete is a two-step inline button (no dialog).
// Actions are injected from SiteEditor.vue to avoid per-level event
// forwarding.
// Actions are injected from the parent editor (StructureEditor.vue) to
// avoid per-level event forwarding.
import { inject } from 'vue'
import draggable from 'vuedraggable'
import { slugify } from './slugify'
@@ -104,7 +104,7 @@ function onEnd() {
v-model="element.title"
v-focus
class="edit title-edit"
placeholder="Title"
title="Page title"
@keyup.enter="handlers.commitPending()"
@keyup.esc="handlers.discardPending()"
/>
@@ -126,7 +126,6 @@ function onEnd() {
<input
class="edit title-edit"
:value="element.title"
placeholder="Title"
title="Label in the navigation — saves while typing; click opens the page"
@input="handlers.titleInput(element, $event)"
@focus="handlers.open(element.path)"