Give split-layout pages independent scroll regions

Chat and Settings escaped the content padding with a hardcoded
calc(100vh - 44px) height and negative margins, while Files and Social let the
whole page scroll, so secondary panes (channel list, settings nav, file tree)
scrolled away. Add a .content.flush modifier the Shell applies to the chat,
settings, files and social routes, plus a reusable .pane-scroll region, and
rework those pages to fill the content area with each pane scrolling on its
own. Removes the magic-number coupling to the topbar height.
This commit is contained in:
ZacharyZhang-NY committed 2026-05-28 19:32:40 -04:00
1 parent 5f70480907
commit 91145fd923
7 files changed
+56 -31

No files matched your search

@@ -196,7 +196,7 @@ export function ChatPage({ snapshot, actions }: PageProps) {
}));
return (
<div className="split-main" style={{ height: "calc(100vh - 44px)", margin: "-28px -32px -40px" }}>
<div className="split-main">
<ChatSidebar
channels={channels}
directs={directs}
@@ -46,7 +46,7 @@ export function FilesPage({ snapshot, actions }: PageProps) {
const totalBytes = visibleFiles.reduce((sum, file) => sum + file.sizeBytes, 0);
return (
<div style={{ display: "grid", gridTemplateColumns: "minmax(160px, 200px) minmax(0, 1fr)", gap: 24 }}>
<div style={{ display: "grid", gridTemplateColumns: "minmax(160px, 200px) minmax(0, 1fr)", gap: 24, height: "100%", minHeight: 0, padding: "28px 32px 0" }}>
<FilesTree
files={files}
pinnedFiles={pinnedFiles}
@@ -56,7 +56,7 @@ export function FilesPage({ snapshot, actions }: PageProps) {
onChange={setSection}
/>
<div style={{ minWidth: 0 }}>
<div style={{ minWidth: 0, display: "flex", flexDirection: "column", minHeight: 0 }}>
<TopbarActions>
<Button tone="ghost"><Icon name="filter" size={13} /></Button>
<Segmented<ViewMode>
@@ -76,6 +76,7 @@ export function FilesPage({ snapshot, actions }: PageProps) {
{visibleFiles.length} {visibleFiles.length === 1 ? "file" : "files"} · {formatBytes(totalBytes)}
</p>
<div className="pane-scroll" style={{ paddingBottom: 40 }}>
{pinnedFiles.length > 0 && section.kind === "all" && (
<>
<div className="section-label" style={{ marginBottom: 10 }}>Pinned</div>
@@ -115,6 +116,7 @@ export function FilesPage({ snapshot, actions }: PageProps) {
onTogglePin={(file) => void actions.run(() => window.agentCompany.setFilePinned({ orgId, fileId: file.id, pinned: !file.pinned }))}
/>
)}
</div>
</div>
</div>
);
@@ -137,7 +139,7 @@ function FilesTree({
}) {
const recent = files.filter((file) => isRecent(file.createdAt));
return (
<aside>
<aside style={{ overflowY: "auto", minHeight: 0, paddingBottom: 40 }}>
<div className="section-label">Library</div>
<TreeItem
icon="folder"
@@ -110,14 +110,16 @@ export function SettingsPage({ snapshot, actions }: PageProps) {
const rollbackSnapshots = useMemo(() => snapshot.rollbackSnapshots.filter((item) => item.orgId === orgId), [snapshot.rollbackSnapshots, orgId]);
return (
<div className="split-main" style={{ height: "calc(100vh - 44px)", margin: "-28px -32px -40px" }}>
<div className="split-main">
<aside className="secondary-pane">
<div style={{ fontSize: 13, fontWeight: 600, padding: "4px 8px 14px" }}>Settings</div>
<NavGroup items={WORKSPACE_GROUP} active={tab} onChange={setTab} />
<NavGroupLabel>Agents</NavGroupLabel>
<NavGroup items={AGENTS_GROUP} active={tab} onChange={setTab} />
<NavGroupLabel>Account</NavGroupLabel>
<NavGroup items={ACCOUNT_GROUP} active={tab} onChange={setTab} />
<div className="pane-scroll">
<NavGroup items={WORKSPACE_GROUP} active={tab} onChange={setTab} />
<NavGroupLabel>Agents</NavGroupLabel>
<NavGroup items={AGENTS_GROUP} active={tab} onChange={setTab} />
<NavGroupLabel>Account</NavGroupLabel>
<NavGroup items={ACCOUNT_GROUP} active={tab} onChange={setTab} />
</div>
</aside>
<div className="main" style={{ flex: 1, minWidth: 0 }}>
<div className="content">
@@ -76,7 +76,7 @@ export function SocialPage({ snapshot, actions }: PageProps) {
void actions.run(() => window.agentCompany.setScheduledPostState({ orgId, scheduledPostId: post.id, state, employeeId: employee.id }));
return (
<div className="page">
<div className="page" style={{ display: "flex", flexDirection: "column", minHeight: 0, height: "100%", padding: "28px 32px 0" }}>
<TopbarActions>
<Button onClick={() => setConnectOpen(true)}><Icon name="plus" size={13} /> Connect account</Button>
<Button tone="primary" onClick={() => setDraftOpen(true)} disabled={accounts.length === 0}>
@@ -89,6 +89,7 @@ export function SocialPage({ snapshot, actions }: PageProps) {
{operatorName ? ` · managed by ${operatorName}` : ""}
</p>
<div className="pane-scroll" style={{ paddingBottom: 40 }}>
{accounts.length > 0 && (
<>
<div className="section-label" style={{ marginBottom: 10 }}>Accounts</div>
@@ -148,6 +149,7 @@ export function SocialPage({ snapshot, actions }: PageProps) {
<SocialCalendar posts={scheduledPosts} accounts={accounts} />
</div>
</div>
<Dialog title="Connect social account" open={connectOpen} submitLabel="Connect" onClose={() => setConnectOpen(false)} onSubmit={connect}>
<Field label="Platform">
@@ -26,33 +26,35 @@ export function ChatSidebar({
<div style={{ fontSize: 13, fontWeight: 600 }}>Messages</div>
<IconButton icon="plus" label="New space" onClick={onCreateSpace} />
</div>
<NavGroup label="Channels">
{channels.length === 0 ? (
<Empty>No channels.</Empty>
) : channels.map((space) => (
<ChannelItem
key={space.id}
space={space}
active={space.id === activeSpaceId}
count={countMessagesIn(space.id, messages)}
onClick={() => onSelect(space.id)}
/>
))}
</NavGroup>
{directs.length > 0 && (
<NavGroup label="Direct">
{directs.map((space) => (
<DirectItem
<div className="pane-scroll">
<NavGroup label="Channels">
{channels.length === 0 ? (
<Empty>No channels.</Empty>
) : channels.map((space) => (
<ChannelItem
key={space.id}
space={space}
agents={agents}
active={space.id === activeSpaceId}
count={countMessagesIn(space.id, messages)}
onClick={() => onSelect(space.id)}
/>
))}
</NavGroup>
)}
{directs.length > 0 && (
<NavGroup label="Direct">
{directs.map((space) => (
<DirectItem
key={space.id}
space={space}
agents={agents}
active={space.id === activeSpaceId}
count={countMessagesIn(space.id, messages)}
onClick={() => onSelect(space.id)}
/>
))}
</NavGroup>
)}
</div>
</aside>
);
}
@@ -22,6 +22,10 @@ interface NavSpec {
icon: IconName;
}
/** Routes whose pages own a full-height, multi-pane layout with their own
* internal scroll regions, so the content area must not scroll as one block. */
const FLUSH_ROUTES = new Set<RouteId>(["chat", "settings", "files", "social"]);
const primaryNav: NavSpec[] = [
{ id: "home", label: "Home", icon: "home" },
{ id: "team", label: "Team", icon: "team" },
@@ -132,7 +136,7 @@ export function Shell({
)}
</div>
</header>
<div className="content">
<div className={FLUSH_ROUTES.has(route) ? "content flush" : "content"}>
<TopbarSlotContext.Provider value={actionsSlot}>{children}</TopbarSlotContext.Provider>
</div>
</main>
@@ -298,6 +298,19 @@ a { color: inherit; }
overflow: auto;
padding: 28px 32px 40px;
}
/* Split-layout routes fill the content area and let their panes scroll
independently instead of scrolling the whole page. */
.content.flush {
display: flex;
flex-direction: column;
overflow: hidden;
padding: 0;
}
.pane-scroll {
flex: 1;
min-height: 0;
overflow-y: auto;
}
.page { max-width: 1100px; }
.page-wide { width: 100%; }
.page-h1 {