Tree panel: animated width toggle, no reflow flash; steady chat header
- 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.
This commit is contained in:
+4
-1
@@ -210,7 +210,10 @@ export function Chat({
|
|||||||
dock stays sticky inside the scroller, untouched. */}
|
dock stays sticky inside the scroller, untouched. */}
|
||||||
<div className="flex h-dvh">
|
<div className="flex h-dvh">
|
||||||
<div className="flex min-w-0 flex-1 flex-col">
|
<div className="flex min-w-0 flex-1 flex-col">
|
||||||
<header className="flex items-center gap-3 px-3 py-2">
|
{/* 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. */}
|
||||||
|
<header className="flex h-10 items-center gap-3 px-3">
|
||||||
<SidebarToggle />
|
<SidebarToggle />
|
||||||
<div className="min-w-0 flex-1 truncate font-medium">{title}</div>
|
<div className="min-w-0 flex-1 truncate font-medium">{title}</div>
|
||||||
<PhaseChip phase={phases.at(-1) ?? "probe"} />
|
<PhaseChip phase={phases.at(-1) ?? "probe"} />
|
||||||
|
|||||||
+37
-19
@@ -41,6 +41,14 @@ import { planNodeStatus, type PlanNode, type PlanNodeStatus, type PlanTree } fro
|
|||||||
/** localStorage: the mode persists as "lh:tree-panel-mode"
|
/** localStorage: the mode persists as "lh:tree-panel-mode"
|
||||||
* ("rail" | "list" | "graph"); the pre-graph key
|
* ("rail" | "list" | "graph"); the pre-graph key
|
||||||
* ("lh:tree-panel-collapsed": "1" rail / "0" list) is the fallback. */
|
* ("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<PanelMode, string> = {
|
||||||
|
rail: "w-10",
|
||||||
|
list: "w-64",
|
||||||
|
graph: "w-96",
|
||||||
|
};
|
||||||
|
|
||||||
const MODE_KEY = "lh:tree-panel-mode";
|
const MODE_KEY = "lh:tree-panel-mode";
|
||||||
const LEGACY_KEY = "lh:tree-panel-collapsed";
|
const LEGACY_KEY = "lh:tree-panel-collapsed";
|
||||||
|
|
||||||
@@ -198,20 +206,36 @@ export function TreePanel({
|
|||||||
// the moment plan_init's output lands and updates live from there.
|
// the moment plan_init's output lands and updates live from there.
|
||||||
if (!tree || tree.nodes.length === 0) return null;
|
if (!tree || tree.nodes.length === 0) return null;
|
||||||
|
|
||||||
if (mode === "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 (
|
return (
|
||||||
|
<aside
|
||||||
|
data-testid="tree-panel"
|
||||||
|
className={cn(
|
||||||
|
"sidebar-motion hidden shrink-0 overflow-hidden border-l border-border/60 bg-card/30 duration-200 xl:flex",
|
||||||
|
PANEL_WIDTH[mode],
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div className={cn("flex h-full flex-col", PANEL_WIDTH[mode])}>
|
||||||
|
{mode === "rail" ? (
|
||||||
<TreePanelRail
|
<TreePanelRail
|
||||||
doneCount={tree.doneNodeIds.length}
|
doneCount={tree.doneNodeIds.length}
|
||||||
totalCount={tree.nodes.length}
|
totalCount={tree.nodes.length}
|
||||||
onExpand={() => go("list")}
|
onExpand={() => go("list")}
|
||||||
/>
|
/>
|
||||||
|
) : mode === "graph" ? (
|
||||||
|
<TreeGraphPanel tree={tree} onShowList={() => go("list")} onCollapse={() => go("rail")} />
|
||||||
|
) : (
|
||||||
|
<TreePanelBody tree={tree} onShowGraph={() => go("graph")} onCollapse={() => go("rail")} />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (mode === "graph") {
|
|
||||||
return <TreeGraphPanel tree={tree} onShowList={() => go("list")} onCollapse={() => go("rail")} />;
|
|
||||||
}
|
|
||||||
return <TreePanelBody tree={tree} onShowGraph={() => go("graph")} onCollapse={() => go("rail")} />;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** The collapsed panel: a thin strip with the map icon + quiet progress. */
|
/** The collapsed panel: a thin strip with the map icon + quiet progress. */
|
||||||
export function TreePanelRail({
|
export function TreePanelRail({
|
||||||
@@ -224,9 +248,9 @@ export function TreePanelRail({
|
|||||||
onExpand: () => void;
|
onExpand: () => void;
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<aside
|
<div
|
||||||
data-testid="tree-panel-rail"
|
data-testid="tree-panel-rail"
|
||||||
className="hidden w-10 shrink-0 flex-col items-center gap-3 border-l border-border/60 py-3 xl:flex"
|
className="flex h-full flex-col items-center gap-3 py-3"
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@@ -243,7 +267,7 @@ export function TreePanelRail({
|
|||||||
>
|
>
|
||||||
{doneCount}/{totalCount}
|
{doneCount}/{totalCount}
|
||||||
</span>
|
</span>
|
||||||
</aside>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -264,10 +288,7 @@ export function TreePanelBody({
|
|||||||
const totalCount = tree.nodes.length;
|
const totalCount = tree.nodes.length;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<aside
|
<div className="flex h-full flex-col">
|
||||||
data-testid="tree-panel"
|
|
||||||
className="hidden w-64 shrink-0 flex-col border-l border-border/60 bg-card/30 xl:flex"
|
|
||||||
>
|
|
||||||
<header className="flex items-center gap-2 px-3 py-2.5">
|
<header className="flex items-center gap-2 px-3 py-2.5">
|
||||||
<ListTreeIcon className="size-3.5 text-muted-foreground" />
|
<ListTreeIcon className="size-3.5 text-muted-foreground" />
|
||||||
<span className="text-[11px] font-medium uppercase tracking-wide text-muted-foreground">
|
<span className="text-[11px] font-medium uppercase tracking-wide text-muted-foreground">
|
||||||
@@ -354,7 +375,7 @@ export function TreePanelBody({
|
|||||||
})}
|
})}
|
||||||
</ol>
|
</ol>
|
||||||
</nav>
|
</nav>
|
||||||
</aside>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -777,10 +798,7 @@ export function TreeGraphPanel({
|
|||||||
const doneCount = tree.doneNodeIds.length;
|
const doneCount = tree.doneNodeIds.length;
|
||||||
const totalCount = tree.nodes.length;
|
const totalCount = tree.nodes.length;
|
||||||
return (
|
return (
|
||||||
<aside
|
<div data-testid="tree-graph-panel" className="flex h-full flex-col">
|
||||||
data-testid="tree-graph-panel"
|
|
||||||
className="hidden w-96 shrink-0 flex-col border-l border-border/60 bg-card/30 xl:flex"
|
|
||||||
>
|
|
||||||
<header className="flex items-center gap-2 px-3 py-2.5">
|
<header className="flex items-center gap-2 px-3 py-2.5">
|
||||||
<NetworkIcon className="size-3.5 text-muted-foreground" />
|
<NetworkIcon className="size-3.5 text-muted-foreground" />
|
||||||
<span className="text-[11px] font-medium uppercase tracking-wide text-muted-foreground">Plan</span>
|
<span className="text-[11px] font-medium uppercase tracking-wide text-muted-foreground">Plan</span>
|
||||||
@@ -810,6 +828,6 @@ export function TreeGraphPanel({
|
|||||||
</button>
|
</button>
|
||||||
</header>
|
</header>
|
||||||
<TreeGraph tree={tree} />
|
<TreeGraph tree={tree} />
|
||||||
</aside>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Vendored
+2
-2
@@ -1,7 +1,7 @@
|
|||||||
/// <reference types="next" />
|
/// <reference types="next" />
|
||||||
/// <reference types="next/image-types/global" />
|
/// <reference types="next/image-types/global" />
|
||||||
import "./.next/dev/types/routes.d.ts";
|
import "./.next/types/routes.d.ts";
|
||||||
import "./.next/dev/types/root-params.d.ts";
|
import "./.next/types/root-params.d.ts";
|
||||||
|
|
||||||
// NOTE: This file should not be edited
|
// NOTE: This file should not be edited
|
||||||
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
|
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
|
||||||
|
|||||||
Reference in New Issue
Block a user