From 8dda21ce1b10328fec702d8f9e6e4c1ac7587cdc Mon Sep 17 00:00:00 2001 From: amosblomqvist Date: Wed, 2 Sep 2026 02:39:49 +0300 Subject: [PATCH] Tree panel: animated width toggle, no reflow flash; steady chat header MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - The three panel modes were three separate fixed-width asides, so a mode switch jumped the whole transcript column width in one frame: scrollbar flash, overshoot, re-correct. Now ONE aside animates its width exactly like the left sidebar (reusing the sidebar-motion hook, so it inherits the provider's mount-gated transition), with a FIXED-width inner layer clipped by overflow-hidden — content never reflows mid-transition. - The chat header was auto-height: the sidebar toggle (a size-9 icon button) renders only while the sidebar is closed, so opening it collapsed the row and slid the title + phase chip up. The header is now a constant h-10. --- components/chat.tsx | 5 ++- components/tree-panel.tsx | 70 ++++++++++++++++++++++++--------------- next-env.d.ts | 4 +-- 3 files changed, 50 insertions(+), 29 deletions(-) diff --git a/components/chat.tsx b/components/chat.tsx index 0c8d9a5..0dcd3b4 100644 --- a/components/chat.tsx +++ b/components/chat.tsx @@ -210,7 +210,10 @@ export function Chat({ dock stays sticky inside the scroller, untouched. */}
-
+ {/* h-10, not py-2: the toggle (a size-9 icon button) renders only + while the sidebar is CLOSED — an auto-height row would collapse + when it disappears and slide the title + chip up. */} +
{title}
diff --git a/components/tree-panel.tsx b/components/tree-panel.tsx index 4e24dab..7f50d10 100644 --- a/components/tree-panel.tsx +++ b/components/tree-panel.tsx @@ -41,6 +41,14 @@ import { planNodeStatus, type PlanNode, type PlanNodeStatus, type PlanTree } fro /** localStorage: the mode persists as "lh:tree-panel-mode" * ("rail" | "list" | "graph"); the pre-graph key * ("lh:tree-panel-collapsed": "1" rail / "0" list) is the fallback. */ +/** The panel width per mode — the aside AND its fixed inner layer (see + * TreePanel: the inner layer must not squish while the aside animates). */ +const PANEL_WIDTH: Record = { + rail: "w-10", + list: "w-64", + graph: "w-96", +}; + const MODE_KEY = "lh:tree-panel-mode"; const LEGACY_KEY = "lh:tree-panel-collapsed"; @@ -198,19 +206,35 @@ export function TreePanel({ // the moment plan_init's output lands and updates live from there. if (!tree || tree.nodes.length === 0) return null; - if (mode === "rail") { - return ( - go("list")} - /> - ); - } - if (mode === "graph") { - return go("list")} onCollapse={() => go("rail")} />; - } - return go("graph")} onCollapse={() => go("rail")} />; + // ONE animated container, exactly like the left sidebar: the aside + // animates its width (sidebar-motion transition cascades from the + // provider and is mount-gated there) while the inner layer keeps a + // FIXED width and is clipped by overflow-hidden — content never + // reflows mid-transition, so no scrollbar flash or overshoot. The + // mode's content just swaps instantly inside the steady frame. + return ( + + ); } /** The collapsed panel: a thin strip with the map icon + quiet progress. */ @@ -224,9 +248,9 @@ export function TreePanelRail({ onExpand: () => void; }) { return ( - +
); } @@ -264,10 +288,7 @@ export function TreePanelBody({ const totalCount = tree.nodes.length; return ( - +
); } @@ -777,10 +798,7 @@ export function TreeGraphPanel({ const doneCount = tree.doneNodeIds.length; const totalCount = tree.nodes.length; return ( - + ); } diff --git a/next-env.d.ts b/next-env.d.ts index a419cbe..ce4e94a 100644 --- a/next-env.d.ts +++ b/next-env.d.ts @@ -1,7 +1,7 @@ /// /// -import "./.next/dev/types/routes.d.ts"; -import "./.next/dev/types/root-params.d.ts"; +import "./.next/types/routes.d.ts"; +import "./.next/types/root-params.d.ts"; // NOTE: This file should not be edited // see https://nextjs.org/docs/app/api-reference/config/typescript for more information.