From db959b6f8f43ead18975fab4be8a152a7f304ff4 Mon Sep 17 00:00:00 2001 From: Till JS Date: Wed, 15 Apr 2026 21:34:22 +0200 Subject: [PATCH] feat(workbench): auto-scroll on scene switch, unify rename to inline MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two tightly coupled UX fixes on the homepage: 1. Switching scenes via SceneAppBar used to leave the carousel parked wherever it was before — the new scene's SceneHeader appeared off-screen to the left, and the user saw a seemingly stale row of cards until they manually scrolled. Now an $effect watches activeSceneId, tracks the last seen value, and smooth-scrolls the .fokus-track to left=0 on every real change (ignoring the initial hydration tick so we don't fight the carousel's own centering). 2. Scene rename had two concurrent paths: the SceneHeader contenteditable

(live DOM) and a SceneRenameDialog modal opened from the scene-pill context menu (reads from the store). If a user was mid-edit inline and right-clicked Umbenennen, the dialog opened with the pre-edit value and on save clobbered the inline change. The modal is gone. The context-menu "Umbenennen" entry now switches to the target scene, scrolls the carousel to the header, and focuses the contenteditable after a 120ms tick so the scroll has time to start. Single source of truth, single code path. SceneRenameDialog.svelte + the sceneDialog state machine in +page.svelte are removed. ConfirmDialog (used for delete) is untouched. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../workbench/scenes/SceneRenameDialog.svelte | 208 ------------------ .../apps/web/src/routes/(app)/+page.svelte | 70 +++--- 2 files changed, 40 insertions(+), 238 deletions(-) delete mode 100644 apps/mana/apps/web/src/lib/components/workbench/scenes/SceneRenameDialog.svelte diff --git a/apps/mana/apps/web/src/lib/components/workbench/scenes/SceneRenameDialog.svelte b/apps/mana/apps/web/src/lib/components/workbench/scenes/SceneRenameDialog.svelte deleted file mode 100644 index 9981700a9..000000000 --- a/apps/mana/apps/web/src/lib/components/workbench/scenes/SceneRenameDialog.svelte +++ /dev/null @@ -1,208 +0,0 @@ - - - - - -{#if show} - -{/if} - - diff --git a/apps/mana/apps/web/src/routes/(app)/+page.svelte b/apps/mana/apps/web/src/routes/(app)/+page.svelte index aec8d127b..e4b7c8284 100644 --- a/apps/mana/apps/web/src/routes/(app)/+page.svelte +++ b/apps/mana/apps/web/src/routes/(app)/+page.svelte @@ -2,7 +2,6 @@ import AppPage from '$lib/components/workbench/AppPage.svelte'; import AppPagePicker from '$lib/components/workbench/AppPagePicker.svelte'; import SceneAppBar from '$lib/components/workbench/SceneAppBar.svelte'; - import SceneRenameDialog from '$lib/components/workbench/scenes/SceneRenameDialog.svelte'; import SceneHeader from '$lib/components/workbench/scenes/SceneHeader.svelte'; import ConfirmDialog from '$lib/components/workbench/scenes/ConfirmDialog.svelte'; import { PageCarousel, type CarouselPage } from '$lib/components/page-carousel'; @@ -120,6 +119,32 @@ if (el) el.scrollIntoView({ behavior: 'smooth', inline: 'center', block: 'nearest' }); } + function scrollCarouselToStart() { + // Reset the horizontal scroll position so the SceneHeader (first + // item in the track) is visible — used after switching scenes so + // the user lands on the new scene's intro rather than wherever + // the previous scene was scrolled to. + const track = document.querySelector('.fokus-track'); + track?.scrollTo({ left: 0, behavior: 'smooth' }); + } + + // ── Reset scroll on scene switch ──────────────────────── + // Watches activeSceneId. On the first tick (initial mount) we skip + // the scroll because that's just the hydration pass — otherwise we + // would fight the carousel's own centre-the-first-card layout. + let lastSceneId: string | null = null; + $effect(() => { + const id = activeSceneId; + if (lastSceneId === null) { + lastSceneId = id; + return; + } + if (id !== lastSceneId) { + lastSceneId = id; + scrollCarouselToStart(); + } + }); + // ── Keyboard shortcuts 1-9 / 0 ───────────────────────── // 1-9 scroll to the Nth open app in the active scene. // 0 opens the new-app picker (which scrolls itself into view). @@ -269,30 +294,25 @@ } // ── Scene CRUD dialogs ────────────────────────────────── - type SceneDialogMode = { - kind: 'rename'; - id: string; - name: string; - description: string; - }; - let sceneDialog = $state(null); let sceneToDelete = $state<{ id: string; name: string } | null>(null); function handleRequestRename(id: string) { + // Unified rename path: scroll the carousel to the scene header + // and focus its contenteditable

. Previously opened a modal + // dialog that read from the store while the live header might + // already hold unsaved typing — the two paths could overwrite + // each other. Inline is the single source of truth now. const scene = scenes.find((s) => s.id === id); if (!scene) return; - sceneDialog = { - kind: 'rename', - id, - name: scene.name, - description: scene.description ?? '', - }; - } - async function handleSubmitSceneDialog(name: string, description: string) { - const mode = sceneDialog; - if (!mode) return; - await workbenchScenesStore.renameScene(mode.id, name, description.trim() || null); - sceneDialog = null; + if (id !== activeSceneId) workbenchScenesStore.setActiveScene(id); + scrollCarouselToStart(); + // Next tick: the header is mounted + the scroll has started; + // querying now lands on the active scene's h1 which + // contenteditable plaintext-only focuses cleanly. + setTimeout(() => { + const h1 = document.querySelector('.scene-header .scene-name[contenteditable]'); + h1?.focus(); + }, 120); } function handleDuplicateScene(id: string) { workbenchScenesStore.duplicateScene(id); @@ -358,16 +378,6 @@ onClose={() => ctxMenu.close()} /> - (sceneDialog = null)} - /> -