Compare commits

...

1 Commits

Author SHA1 Message Date
Danielle Maywood 48fd7b372b refactor(site): extract AgentsLayout from AgentsPageView
Rename AgentsPageView to AgentsLayout to follow the convention used
by HealthLayout, TemplateLayout, and other layout components. Move
the scrollbar gutter lock effect from AgentsPage into the layout
so all viewport-level concerns live in one place.
2026-03-10 22:03:41 +00:00
5 changed files with 57 additions and 57 deletions
@@ -29,7 +29,7 @@ import {
} from "storybook-addon-remix-react-router";
import AgentDetail from "./AgentDetail";
import { RIGHT_PANEL_OPEN_KEY } from "./AgentDetailView";
import type { AgentsOutletContext } from "./AgentsPage";
import type { AgentsOutletContext } from "./AgentsLayout";
// ---------------------------------------------------------------------------
// Layout wrapper provides outlet context for the child route.
+1 -1
View File
@@ -73,7 +73,7 @@ import {
AgentDetailNotFoundView,
AgentDetailView,
} from "./AgentDetailView";
import type { AgentsOutletContext } from "./AgentsPage";
import type { AgentsOutletContext } from "./AgentsLayout";
import {
getModelCatalogStatusMessage,
getModelOptionsFromCatalog,
@@ -7,7 +7,7 @@ import type { Chat } from "api/typesGenerated";
import type { ModelSelectorOption } from "components/ai-elements";
import { fn, spyOn } from "storybook/test";
import { reactRouterParameters } from "storybook-addon-remix-react-router";
import { AgentsPageView } from "./AgentsPageView";
import { AgentsLayout } from "./AgentsLayout";
const defaultModelOptions: ModelSelectorOption[] = [
{
@@ -57,9 +57,9 @@ const agentsRouting = [
...{ path: string; useStoryElement: boolean }[],
];
const meta: Meta<typeof AgentsPageView> = {
title: "pages/AgentsPage/AgentsPageView",
component: AgentsPageView,
const meta: Meta<typeof AgentsLayout> = {
title: "pages/AgentsPage/AgentsLayout",
component: AgentsLayout,
decorators: [withAuthProvider, withDashboardProvider],
parameters: {
layout: "fullscreen",
@@ -112,7 +112,7 @@ const meta: Meta<typeof AgentsPageView> = {
};
export default meta;
type Story = StoryObj<typeof AgentsPageView>;
type Story = StoryObj<typeof AgentsLayout>;
export const EmptyState: Story = {};
@@ -4,7 +4,7 @@ import { Button } from "components/Button/Button";
import { ExternalImage } from "components/ExternalImage/ExternalImage";
import { CoderIcon } from "components/Icons/CoderIcon";
import { PanelLeftIcon } from "lucide-react";
import { type FC, useState } from "react";
import { type FC, useEffect, useState } from "react";
import { NavLink, Outlet } from "react-router";
import { cn } from "utils/cn";
import { pageTitle } from "utils/page";
@@ -29,7 +29,7 @@ export interface AgentsOutletContext {
onToggleSidebarCollapsed: () => void;
}
interface AgentsPageViewProps {
interface AgentsLayoutProps {
agentId: string | undefined;
chatList: TypesGen.Chat[];
catalogModelOptions: readonly ChatModelOption[];
@@ -57,7 +57,7 @@ interface AgentsPageViewProps {
onLoadMore: () => void;
}
export const AgentsPageView: FC<AgentsPageViewProps> = ({
export const AgentsLayout: FC<AgentsLayoutProps> = ({
agentId,
chatList,
catalogModelOptions,
@@ -84,6 +84,49 @@ export const AgentsPageView: FC<AgentsPageViewProps> = ({
hasNextPage,
onLoadMore,
}) => {
// The global CSS sets scrollbar-gutter: stable on <html> to prevent
// layout shift on pages that toggle scrollbars. The agents page
// uses its own internal scroll containers so the reserved gutter
// space is unnecessary and wastes horizontal room.
//
// Removing the gutter requires three things:
//
// 1. overflow:hidden on both <html> and <body> so neither element
// can produce a scrollbar.
// 2. scrollbar-gutter:auto on <html> so the browser stops
// reserving space for a scrollbar that will never appear.
// This is what makes react-remove-scroll-bar measure a gap of
// 0 when a Radix dropdown opens, so it injects no padding or
// margin compensation.
// 3. An injected <style> that overrides the global
// `overflow-y: scroll !important` on body[data-scroll-locked].
// Without this, opening any Radix dropdown would force a
// scrollbar onto <body>, re-introducing the layout shift.
useEffect(() => {
const html = document.documentElement;
const body = document.body;
const prevHtmlOverflow = html.style.overflow;
const prevHtmlScrollbarGutter = html.style.scrollbarGutter;
const prevBodyOverflow = body.style.overflow;
html.style.overflow = "hidden";
html.style.scrollbarGutter = "auto";
body.style.overflow = "hidden";
const style = document.createElement("style");
style.textContent =
"html body[data-scroll-locked] { overflow-y: hidden !important; }";
document.head.appendChild(style);
return () => {
html.style.overflow = prevHtmlOverflow;
html.style.scrollbarGutter = prevHtmlScrollbarGutter;
body.style.overflow = prevBodyOverflow;
style.remove();
};
}, []);
const {
chatErrorReasons,
requestArchiveAgent,
+4 -47
View File
@@ -39,8 +39,8 @@ import {
emptyInputStorageKey,
} from "./AgentCreateForm";
import { maybePlayChime } from "./AgentDetail/useAgentChime";
import type { AgentsOutletContext } from "./AgentsPageView";
import { AgentsPageView } from "./AgentsPageView";
import type { AgentsOutletContext } from "./AgentsLayout";
import { AgentsLayout } from "./AgentsLayout";
import { getModelOptionsFromCatalog } from "./modelOptions";
import { useAgentsPageKeybindings } from "./useAgentsPageKeybindings";
import { useAgentsPWA } from "./useAgentsPWA";
@@ -62,7 +62,7 @@ function isChatListSSEEvent(
);
}
export type { AgentsOutletContext } from "./AgentsPageView";
export type { AgentsOutletContext } from "./AgentsLayout";
const AgentsPage: FC = () => {
useAgentsPWA();
@@ -75,49 +75,6 @@ const AgentsPage: FC = () => {
permissions.editDeploymentConfig ||
user.roles.some((role) => role.name === "owner" || role.name === "admin");
// The global CSS sets scrollbar-gutter: stable on <html> to prevent
// layout shift on pages that toggle scrollbars. The agents page
// uses its own internal scroll containers so the reserved gutter
// space is unnecessary and wastes horizontal room.
//
// Removing the gutter requires three things:
//
// 1. overflow:hidden on both <html> and <body> so neither element
// can produce a scrollbar.
// 2. scrollbar-gutter:auto on <html> so the browser stops
// reserving space for a scrollbar that will never appear.
// This is what makes react-remove-scroll-bar measure a gap of
// 0 when a Radix dropdown opens, so it injects no padding or
// margin compensation.
// 3. An injected <style> that overrides the global
// `overflow-y: scroll !important` on body[data-scroll-locked].
// Without this, opening any Radix dropdown would force a
// scrollbar onto <body>, re-introducing the layout shift.
useEffect(() => {
const html = document.documentElement;
const body = document.body;
const prevHtmlOverflow = html.style.overflow;
const prevHtmlScrollbarGutter = html.style.scrollbarGutter;
const prevBodyOverflow = body.style.overflow;
html.style.overflow = "hidden";
html.style.scrollbarGutter = "auto";
body.style.overflow = "hidden";
const style = document.createElement("style");
style.textContent =
"html body[data-scroll-locked] { overflow-y: hidden !important; }";
document.head.appendChild(style);
return () => {
html.style.overflow = prevHtmlOverflow;
html.style.scrollbarGutter = prevHtmlScrollbarGutter;
body.style.overflow = prevBodyOverflow;
style.remove();
};
}, []);
const chatsQuery = useInfiniteQuery(infiniteChats());
const chatModelsQuery = useQuery(chatModels());
const chatModelConfigsQuery = useQuery(chatModelConfigs());
@@ -458,7 +415,7 @@ const AgentsPage: FC = () => {
});
return (
<AgentsPageView
<AgentsLayout
agentId={agentId}
chatList={chatList}
catalogModelOptions={catalogModelOptions}