From a163aeb17e7c489c8b2388599d15be728d0a7024 Mon Sep 17 00:00:00 2001 From: Pouyan Jahangiri Date: Sat, 25 Jul 2026 19:30:30 -0700 Subject: [PATCH] Move the lineage zoom controls into the mobile transport pill On a phone the floating zoom cluster (- / % / + / recenter) ate a lot of the canvas. Pinch already handles zoom in/out there, so the cluster is hidden and only the useful single action - "fit the tree" - moves into the transport pill as one more bare icon. The canvas keeps its full cluster on desktop. The canvas hands its recenter up via an onrecenterready callback (the same register pattern Canvas uses); the parent passes it to the pill only while the lineage canvas is the live view, so the button is absent in the focus view. --- src/lib/components/bench/LineageCanvas.svelte | 29 ++++++++++++------- src/lib/components/shell/Sidebar.svelte | 6 ++-- src/lib/components/shell/SidebarRail.svelte | 13 ++++++++- src/routes/+page.svelte | 12 ++++++-- 4 files changed, 44 insertions(+), 16 deletions(-) diff --git a/src/lib/components/bench/LineageCanvas.svelte b/src/lib/components/bench/LineageCanvas.svelte index 6232fe8..8795998 100644 --- a/src/lib/components/bench/LineageCanvas.svelte +++ b/src/lib/components/bench/LineageCanvas.svelte @@ -22,9 +22,19 @@ interface Props { /** Add a fresh root world - the canvas offers it in its own corner, not only the sidebar. */ onaddworld: () => void; + /** + * Handed the canvas's "fit the tree" action on mount (the same `register` pattern Canvas uses). + * On a phone the canvas hides its own zoom cluster - pinch handles zoom - so the recenter rides + * in the transport pill instead, which calls back through this. The parent only offers it to the + * pill while this canvas is the live view, so a handle to a torn-down canvas is never reached. + */ + onrecenterready?: (recenter: () => void) => void; } - let { onaddworld }: Props = $props(); + let { onaddworld, onrecenterready }: Props = $props(); + + // Hand our recenter up to the parent once mounted (recenterTree is a hoisted declaration below). + onMount(() => onrecenterready?.(recenterTree)); let container: HTMLDivElement; /** Each node's rendered height, measured live - the edges need it to leave from a parent's foot. */ @@ -181,7 +191,7 @@ } /** Frame the whole tree in the viewport - the "recenter" action. */ - function recenter() { + function recenterTree() { const rect = container.getBoundingClientRect(); if (!bench.worlds.length) { canvas.reset(); @@ -295,7 +305,7 @@ - @@ -423,16 +433,13 @@ border-color: var(--accent); } - /* Phone: the floating transport pill (SidebarRail) owns the bottom-centre and already carries an - "add environment" button, so the canvas's own Add pill stands down and the zoom cluster lifts - clear of the transport pill instead of sharing the bottom edge with it. */ + /* Phone: the floating canvas controls stand down. The transport pill (SidebarRail) already carries + an "add environment" button and now a recenter button, and pinch handles zoom in/out - so both the + Add pill and the whole zoom cluster (which the owner found ate too much space) are hidden here. */ @media (max-width: 900px) { - .add { - display: none; - } - + .add, .controls { - bottom: calc(env(safe-area-inset-bottom, 0px) + 76px); + display: none; } } diff --git a/src/lib/components/shell/Sidebar.svelte b/src/lib/components/shell/Sidebar.svelte index 80a8274..f3c9dd9 100644 --- a/src/lib/components/shell/Sidebar.svelte +++ b/src/lib/components/shell/Sidebar.svelte @@ -40,9 +40,11 @@ interface Props { onaddworld: () => void; onplaystory: () => void; + /** Passed through to the rail's phone pill: "fit the tree" when the lineage canvas is showing. */ + onrecenter?: () => void; } - let { onaddworld, onplaystory }: Props = $props(); + let { onaddworld, onplaystory, onrecenter }: Props = $props(); const target = $derived(trainTarget(bench.generationsEvolved, bench.maxGenerations)); const label = $derived(trainLabel(bench.maxGenerations)); @@ -87,7 +89,7 @@ every time the panel opened. --> {#if !shell.open || shell.narrow} - + {/if} {#if shell.open} diff --git a/src/lib/components/shell/SidebarRail.svelte b/src/lib/components/shell/SidebarRail.svelte index d7e3ca9..0620f9d 100644 --- a/src/lib/components/shell/SidebarRail.svelte +++ b/src/lib/components/shell/SidebarRail.svelte @@ -22,9 +22,12 @@ interface Props { onaddworld: () => void; onplaystory: () => void; + /** "Fit the tree", when the lineage canvas is showing. On a phone its own zoom cluster is hidden, + so this rides in the pill; absent (the focus view, or desktop) the button is not offered. */ + onrecenter?: () => void; } - let { onaddworld, onplaystory }: Props = $props(); + let { onaddworld, onplaystory, onrecenter }: Props = $props(); const speed = $derived(SPEEDS.find((option) => option.value === bench.speed) ?? SPEEDS[1]); const train = $derived(trainLabel(bench.maxGenerations)); @@ -104,6 +107,14 @@ > + + + {#if shell.narrow && onrecenter} + + {/if} {/if} diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte index f55a6fc..e681ee9 100644 --- a/src/routes/+page.svelte +++ b/src/routes/+page.svelte @@ -93,6 +93,10 @@ // exists. A focusedId left pointing at a removed world falls straight back to the canvas. const focusing = $derived(bench.focusedId ? bench.find(bench.focusedId) : undefined); + // The lineage canvas binds its "fit the tree" action here so the phone transport pill can offer it. + // It is only live while the canvas is mounted (not the focus view), so the pill hides the button then. + let recenterTree = $state<(() => void) | undefined>(); + /** * Space plays/pauses - but ONLY when it isn't already the focused control's key. * @@ -144,7 +148,11 @@ {#if !app.research} - bench.playStory()} /> + bench.playStory()} + onrecenter={focusing ? undefined : recenterTree} + /> {/if}