diff --git a/AGENTS.md b/AGENTS.md
index bea3bb7..c8fb8c6 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -100,8 +100,17 @@ not for the public pages. See `docs/design-principles.md` for the design.
the banner pen — everything saves immediately as you edit (brand/title
debounced, slug on commit since it renames the path), tree rows navigate
in place without transitions when focused, and the front page is a
- root-only row whose empty slug is editable like any other (empty child
- lists become drop zones while dragging). The two pens swap the docked
+ root-only row whose empty slug is editable like any other. Every
+ non-empty list (and the root) ends with a non-draggable ➕ footer row
+ (vuedraggable `#footer` slot): clicking it starts a new pending page at
+ that level, and while dragging it is the list's "end of list" drop
+ target. Dropping ON the lower part of a row moves the page under that
+ row (the child list's container invisibly overlaps its own row's bottom
+ via negative margin — Sortable inserts it as the first child natively),
+ while a row's exposed top edge inserts a sibling before it. Row
+ indentation is structural (each nested list margin-indents itself), so a
+ dragged row previews its whole subtree at the target list's depth. The
+ two pens swap the docked
panel for the other editor; clicking the open editor's own pen closes it. Normally dynamic-imported onto the content page by
pagerite.js when a 🖊️ edit link is clicked (the link carries
`data-editor-src`/`data-editor-css`/`data-editor-mode`); the `/_/admin`
diff --git a/docs/design-principles.md b/docs/design-principles.md
index 1fe147d..e21c202 100644
--- a/docs/design-principles.md
+++ b/docs/design-principles.md
@@ -167,14 +167,18 @@ evolves.
insert `` at the cursor.
- The **structure panel** (vue-draggable tree of the whole site, in site
mode) covers page management: reorder any menu level, drag across
- sections (empty child lists appear as drop zones while dragging), add,
- delete (two clicks: the button arms, then deletes — no dialogs).
- Every node is a real label — content-less category rows offer a
- ➕ to give them a landing page.
+ sections, add, delete (two clicks: the button arms, then deletes — no
+ dialogs). Every node is a real label — content-less category rows offer
+ a ➕ to give them a landing page.
Deleting a category removes only its landing page (the label and its
- subpages stay). The ➕ in the panel header starts a new page as a
- local-only tree row that can be dragged into place before its title and
- slug are filled in; it is persisted only on commit. Rows are always
+ subpages stay). Every non-empty list ends with a ➕ row that starts a
+ new page as a local-only tree row at that level; the row can be dragged
+ into place before its title and slug are filled in and is persisted only
+ on commit. While dragging, these ➕ rows double as "end of this list"
+ drop targets; dropping ON the lower part of a row makes the page that
+ row's first child (even a leaf's, creating a sublist), while a row's
+ exposed top edge inserts a sibling before it. A dragged row's
+ indentation previews the target list's depth. Rows are always
editable: titles save while typing, slug edits commit on blur/Enter
since they rename the path (moving the whole subtree). The front page
is the root row with an empty slug — renaming it away leaves no front
diff --git a/frontend/src/SiteEditor.vue b/frontend/src/SiteEditor.vue
index 81e1e69..d69cf75 100644
--- a/frontend/src/SiteEditor.vue
+++ b/frontend/src/SiteEditor.vue
@@ -157,12 +157,13 @@ function followMove(oldPath, newPath) {
}
// --- New page flow -------------------------------------------------------
-// The ➕ in the pages header adds a *pending* row to the tree: a local-only
-// item that can be dragged into place before anything is filled in. It is
-// persisted only on commit (✓/Enter), at wherever it currently sits.
+// The ➕ row at the end of any list adds a *pending* row there: a
+// local-only item that can be dragged into place before anything is
+// filled in. It is persisted only on commit (✓/Enter), at wherever it
+// currently sits.
const pending = ref(null)
-function newPage() {
+function newPage(list) {
if (pending.value) return // one at a time
pending.value = {
slug: '',
@@ -174,7 +175,7 @@ function newPage() {
children: [],
pending: true,
}
- tree.value.push(pending.value)
+ list.push(pending.value)
}
// Where does the pending row currently sit? -> {parentPath, list, index}.
@@ -431,6 +432,7 @@ provide('structureHandlers', {
addContent,
commitPending,
discardPending,
+ newPage,
})
// --- Banner editing ------------------------------------------------------
@@ -644,12 +646,6 @@ onUnmounted(() => {
-
@@ -774,21 +770,4 @@ onUnmounted(() => {
overflow-y: auto;
min-height: 0;
}
-
-/* Icon buttons (➕) keep the emoji's own color, no button chrome. */
-.structure .add {
- align-self: flex-start; /* don't stretch to the block's full width */
- margin-top: 0.3rem;
- margin-left: 1.2em; /* align with the row titles, past the drag handle */
- padding: 0.1rem 0.3rem;
- background: none;
- border: none;
- font-size: 1rem;
- cursor: pointer;
- opacity: 0.7;
-}
-
-.structure .add:hover {
- opacity: 1;
-}
diff --git a/frontend/src/StructureTree.vue b/frontend/src/StructureTree.vue
index de24fd3..936957e 100644
--- a/frontend/src/StructureTree.vue
+++ b/frontend/src/StructureTree.vue
@@ -7,9 +7,15 @@
// the slug commits on blur/Enter since it renames the path, moving the
// whole subtree. Nodes without content are category labels that redirect
// to their first child; the ➕ on their row gives them a landing page.
-// The ➕ in the panel header adds a *pending* row: a local-only item that
-// can be dragged into place before its title/slug are filled in, and is
-// persisted to the server only on commit (✓/Enter, Esc discards).
+// Every non-empty list (and the root list) ends with a ➕ footer row:
+// clicking it adds a *pending* row (a local-only item persisted to the
+// server only on commit, ✓/Enter, Esc discards) at the end of that list,
+// and while dragging it doubles as the list's "end of list" drop target.
+// Leaf pages have no ➕ row of their own, but every row's child list is a
+// drop target: its (invisible) container box reaches up over the bottom of
+// its row, so dropping ON a row makes it a child (first position), while
+// dropping on the row's top edge inserts a sibling before it. See the
+// `.node > .treelist` style.
// 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).
@@ -59,8 +65,8 @@ function onMove(evt) {
return true
}
-// While dragging, reveal empty child lists as drop zones (style.css) so a
-// page can be moved under a childless page.
+// While dragging, the child-list overlap strips become hit-testable (see
+// styles). Purely functional — nothing is shown or resized.
function onStart() {
document.body.classList.add('tree-dragging')
}
@@ -85,12 +91,12 @@ function onEnd() {
>
-
+
⠿
@@ -161,26 +167,77 @@ function onEnd() {
/>
+
+
+
+
+
+
diff --git a/frontend/src/assets/style.css b/frontend/src/assets/style.css
index ce03454..55e85d7 100644
--- a/frontend/src/assets/style.css
+++ b/frontend/src/assets/style.css
@@ -191,14 +191,6 @@ body.editing #sidebar {
}
}
-/* Site structure tree: while dragging, empty child lists appear as drop
- zones so a page can be moved under a childless page. */
-body.tree-dragging .treelist:empty {
- min-height: 1.2rem;
- outline: 1px dashed var(--line);
- border-radius: 4px;
-}
-
/* The banner's own pen: opens the site editor (banner + structure).
Qualified with `button` to beat the later .edit-link rule's left offset
(both classes apply to the same element). */
@@ -211,15 +203,10 @@ button.banner-edit-link {
font: inherit;
border: none;
cursor: pointer;
- opacity: 0.55;
background: none;
padding: 0;
}
-.banner-edit-link:hover {
- opacity: 1;
-}
-
#sidebar {
grid-column: 1;
/* Pinned to the page's left edge (not the article's) and kept in view