From e309dc839f7972db6edba8723220285d403af08a Mon Sep 17 00:00:00 2001 From: Michael Aufreiter Date: Fri, 24 Jul 2026 22:28:14 +0200 Subject: [PATCH 01/11] Make PageUrlDialog available from page menu --- ARCHITECTURE.md | 10 +- IMPLEMENTATION_PLAN.md | 3 +- src/routes/components/App.svelte | 3 + src/routes/components/Overlays.svelte | 2 + src/routes/components/PageUrlDialog.svelte | 229 ++++++++++++++++++ src/routes/components/PagesDrawer.svelte | 154 +----------- src/routes/components/Toolbar.svelte | 32 ++- .../page_url_dialog_context.svelte.ts | 40 +++ 8 files changed, 323 insertions(+), 150 deletions(-) create mode 100644 src/routes/components/PageUrlDialog.svelte create mode 100644 src/routes/components/page_url_dialog_context.svelte.ts diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 5b29760b..8a3b8030 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -531,7 +531,15 @@ The home page is excluded from this mapping: #### Custom slugs -Admins can change a page's Page URL from the page browser ellipsis menu. +Admins can change a page's Page URL from the page browser ellipsis menu, and from +the page-actions ellipsis menu in the browsing toolbar for the page they are on. + +Both entry points share one implementation: `PageUrlDialog.svelte` owns the dialog +and the `update_page_slug` call, and is rendered once by `Overlays.svelte`. Callers +open it through the `page_url_dialog` context with the target page's +`document_id` and current `page_href`, plus an optional `on_saved` hook for +caller-specific cleanup. The home page has no editable slug, so the entry is +disabled in both menus. When a user changes a page's slug: diff --git a/IMPLEMENTATION_PLAN.md b/IMPLEMENTATION_PLAN.md index dd2843b3..7f040d66 100644 --- a/IMPLEMENTATION_PLAN.md +++ b/IMPLEMENTATION_PLAN.md @@ -501,7 +501,8 @@ Required UI behavior: - link pickers must not expose drafts to unauthenticated users - toolbar actions that require admin auth must be hidden or disabled when unauthenticated - authenticated admins get an ellipsis page-actions menu in the browsing toolbar with Duplicate page, Edit URL, Delete page, and Logout entries -- keep the initial page-management entries presentational and retain the existing working Logout action; wire Duplicate page, Edit URL, and Delete page in a follow-up task +- Edit URL opens the same dialog as the page browser ellipsis menu, and is disabled on the home page +- Duplicate page and Delete page stay presentational for now; wire them in a follow-up task ### Page browser behavior diff --git a/src/routes/components/App.svelte b/src/routes/components/App.svelte index ae1a9f22..38ecf0ac 100644 --- a/src/routes/components/App.svelte +++ b/src/routes/components/App.svelte @@ -10,6 +10,7 @@ import { create_session } from '../create_session.js'; import { create_page_browser, set_page_browser } from './page_browser_context.svelte.js'; import type { PageBrowser } from './page_browser_context.svelte.js'; + import { create_page_url_dialog, set_page_url_dialog } from './page_url_dialog_context.svelte.js'; import { demo_doc } from '$lib/demo_doc.js'; @@ -104,6 +105,8 @@ set_page_browser(page_browser); + set_page_url_dialog(create_page_url_dialog()); + $effect(() => { document.documentElement.style.scrollBehavior = editable ? 'auto' : 'smooth'; }); diff --git a/src/routes/components/Overlays.svelte b/src/routes/components/Overlays.svelte index 901e88cd..62541a7f 100644 --- a/src/routes/components/Overlays.svelte +++ b/src/routes/components/Overlays.svelte @@ -10,6 +10,7 @@ import LinkPreview from './LinkPreview.svelte'; import AuthDialog from './AuthDialog.svelte'; import Drawer from './Drawer.svelte'; + import PageUrlDialog from './PageUrlDialog.svelte'; const svedit = get_svedit_context(); const app = get_app_context(); @@ -238,6 +239,7 @@ + {/if} diff --git a/src/routes/components/PageUrlDialog.svelte b/src/routes/components/PageUrlDialog.svelte new file mode 100644 index 00000000..a3714852 --- /dev/null +++ b/src/routes/components/PageUrlDialog.svelte @@ -0,0 +1,229 @@ + + + + {#if target} +
{ + event.preventDefault(); + void save(); + }} + > +

Edit URL

+
+ {app.origin || 'example.com'}/ + +
+ {#if page_url_error} + + {/if} +
+ + +
+
+ {/if} +
+ + diff --git a/src/routes/components/PagesDrawer.svelte b/src/routes/components/PagesDrawer.svelte index e7e817ad..ba7d64b9 100644 --- a/src/routes/components/PagesDrawer.svelte +++ b/src/routes/components/PagesDrawer.svelte @@ -1,5 +1,4 @@ + + + {#if target} +
{ + event.preventDefault(); + void confirm_delete(); + }} + > +

Delete page

+

{get_confirmation_message()}

+ {#if delete_error} + + {/if} +
+ + +
+
+ {/if} +
+ + diff --git a/src/routes/components/PagesDrawer.svelte b/src/routes/components/PagesDrawer.svelte index ba7d64b9..8a22d83c 100644 --- a/src/routes/components/PagesDrawer.svelte +++ b/src/routes/components/PagesDrawer.svelte @@ -1,5 +1,4 @@ From 12c0fea88c7aa124afccf899e92f0adbe9fecae3 Mon Sep 17 00:00:00 2001 From: Michael Aufreiter Date: Sat, 25 Jul 2026 00:46:38 +0200 Subject: [PATCH 04/11] DuplicateNodesCommand --- src/routes/commands.svelte.ts | 59 +++++++++++++++++++++++++++++++++++ src/routes/create_session.ts | 7 +++-- 2 files changed, 64 insertions(+), 2 deletions(-) diff --git a/src/routes/commands.svelte.ts b/src/routes/commands.svelte.ts index 03514890..5f3943b9 100644 --- a/src/routes/commands.svelte.ts +++ b/src/routes/commands.svelte.ts @@ -344,3 +344,62 @@ export class EditLinkCommand extends Command { } } } + +/** + * Duplicate the selected nodes, following the same steps as copy → collapse the + * cursor at the end of the selection → paste, so the copies land directly after + * the originals. + * + * Text and property selections duplicate their closest parent node instead, so + * the command works from inside a node without selecting it first. + */ +export class DuplicateNodesCommand extends Command { + is_enabled() { + const selection = this.context.session.selection; + if (!selection) return false; + + // A collapsed node cursor sits between nodes — there is nothing to copy. + if (selection.type === 'node') return !is_selection_collapsed(selection); + + // Same condition select_parent() uses to decide whether a parent exists. + return selection.path.length > 3; + } + + execute() { + const { session } = this.context; + + // Anything that is not already a node selection duplicates its closest + // parent node, which select_parent() selects as a single-node range. + if (session.selection && session.selection.type !== 'node') { + session.select_parent(); + } + + const selection = session.selection; + if (selection?.type !== 'node' || is_selection_collapsed(selection)) return; + + // Read the payload before touching the selection — this is the same data + // the clipboard would carry for a node copy. + const payload = session.get_selected_annotated_nodes(); + if (!payload) return; + + const { nodes, main_nodes, marks, annotations } = payload; + const selection_end = Math.max(selection.anchor_offset, selection.focus_offset); + + const tr = session.tr; + // Collapsing is what makes this a duplicate rather than a replace: + // insert_nodes deletes a non-collapsed selection before inserting. + tr.set_selection({ + type: 'node', + path: selection.path, + anchor_offset: selection_end, + focus_offset: selection_end + }); + tr.insert_nodes( + main_nodes.map((node_id) => tr.build(node_id, nodes)), + marks, + annotations, + nodes + ); + session.apply(tr); + } +} diff --git a/src/routes/create_session.ts b/src/routes/create_session.ts index 3084f711..276d6c23 100644 --- a/src/routes/create_session.ts +++ b/src/routes/create_session.ts @@ -27,7 +27,8 @@ import { EditLinkCommand, ReplaceMediaCommand, EditImageCommand, - ToggleAccordionCommand + ToggleAccordionCommand, + DuplicateNodesCommand } from './commands.svelte.js'; // System components @@ -376,7 +377,8 @@ const session_config = { toggle_link: new ToggleLinkCommand(context), edit_link: new EditLinkCommand(context), edit_image: new EditImageCommand(context), - replace_media: new ReplaceMediaCommand(context) + replace_media: new ReplaceMediaCommand(context), + duplicate_nodes: new DuplicateNodesCommand(context) }; // Define keymap binding keys to commands @@ -397,6 +399,7 @@ const session_config = { 'meta+shift+c,ctrl+shift+c': [commands.toggle_code], 'meta+u,ctrl+u': [commands.toggle_highlight], 'meta+shift+s,ctrl+shift+s': [commands.toggle_section], + 'meta+d,ctrl+d': [commands.duplicate_nodes], 'meta+z,ctrl+z': [commands.undo], 'meta+shift+z,ctrl+shift+z': [commands.redo], escape: [commands.select_parent], From 6ae51f5f0a248a10cdd745f554be2575baa98f06 Mon Sep 17 00:00:00 2001 From: Michael Aufreiter Date: Sat, 25 Jul 2026 01:48:03 +0200 Subject: [PATCH 05/11] Focus rings fix --- src/routes/components/Toolbar.svelte | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/routes/components/Toolbar.svelte b/src/routes/components/Toolbar.svelte index 354a2e34..69b1291a 100644 --- a/src/routes/components/Toolbar.svelte +++ b/src/routes/components/Toolbar.svelte @@ -231,8 +231,11 @@ {@render selection_leading_contents()} {/if} +
@@ -242,8 +245,10 @@
+
{#if !editable} From 7ab2266842cdd26fe185001be3395c6f3af0f20c Mon Sep 17 00:00:00 2001 From: Michael Aufreiter Date: Sat, 25 Jul 2026 02:02:54 +0200 Subject: [PATCH 06/11] Fix focus ring for NodeNavigator --- src/routes/components/NodeNavigator.svelte | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/routes/components/NodeNavigator.svelte b/src/routes/components/NodeNavigator.svelte index 21e2d98b..a0d1e67d 100644 --- a/src/routes/components/NodeNavigator.svelte +++ b/src/routes/components/NodeNavigator.svelte @@ -135,7 +135,7 @@ {#if variant_item}
@@ -233,6 +233,14 @@ background-image: none; } + /* The real control is a transparent