fix(ui): reserve the scrollbar gutter on main scroll areas

v7 shows an 8px scrollbar that takes layout space, so a page or list
whose content crossed the overflow threshold (filtering the MCP table,
switching usage tabs) shifted every column sideways by 8px. Add a
scroll-stable utility (scrollbar-gutter: stable) and apply it to page
scroll areas, sheet bodies and the large list/table containers; menus,
code blocks and small dialogs keep the default. Drop the unused
scroll-overlay utility.
This commit is contained in:
Jason committed 2026-10-03 23:29:00 +08:00
1 parent 4aaafb8f41
commit c57d64f4de
17 files changed
+36 -28

No files matched your search

+10 -4
View File
@@ -1207,7 +1207,7 @@ function App() {
<div
ref={providerScrollContainerRef}
id="main-content"
className="min-h-0 flex-1 overflow-y-auto overflow-x-hidden px-6 pb-12 pt-4"
className="min-h-0 flex-1 overflow-y-auto scroll-stable overflow-x-hidden px-6 pb-12 pt-4"
>
<div className="space-y-4">
{activeApp === "claude-desktop" && (
@@ -1284,7 +1284,7 @@ function App() {
id="main-content"
role="tabpanel"
aria-labelledby={`openclaw-config-${openclawConfigTab}`}
className="min-h-0 flex-1 overflow-y-auto"
className="min-h-0 flex-1 overflow-y-auto scroll-stable"
>
{openclawConfigTab === "env" ? (
<EnvPanel />
@@ -1298,7 +1298,10 @@ function App() {
);
case "hermesMemory":
return (
<div id="main-content" className="min-h-0 flex-1 overflow-y-auto">
<div
id="main-content"
className="min-h-0 flex-1 overflow-y-auto scroll-stable"
>
<HermesMemoryPanel onOpenWebUI={openHermesWebUI} />
</div>
);
@@ -1350,7 +1353,10 @@ function App() {
</>
}
/>
<div id="main-content" className="min-h-0 flex-1 overflow-y-auto">
<div
id="main-content"
className="min-h-0 flex-1 overflow-y-auto scroll-stable"
>
<div className="px-6 pb-10 pt-4">
<AuthCenterPanel showIntro={false} />
</div>
+4 -1
View File
@@ -249,7 +249,10 @@ export function AppsPage() {
}
/>
<div id="main-content" className="min-h-0 flex-1 overflow-y-auto">
<div
id="main-content"
className="min-h-0 flex-1 overflow-y-auto scroll-stable"
>
<div className="px-6 pb-10 pt-4">
<div className="flex h-8 items-center px-4 text-caption font-medium text-fg-2">
<span className="flex-1">{t("appsPage.columnApp")}</span>
+1 -1
View File
@@ -163,7 +163,7 @@ export const FullScreenPanel: React.FC<FullScreenPanelProps> = ({
}
/>
<div className="flex-1 overflow-y-auto">
<div className="flex-1 overflow-y-auto scroll-stable">
<div className={cn("w-full space-y-6 px-6 py-6", contentClassName)}>
{children}
</div>
+2 -2
View File
@@ -734,7 +734,7 @@ const UnifiedMcpPanel: React.FC<UnifiedMcpPanelProps> = ({
) : (
<div
data-testid="mcp-matrix"
className="min-h-0 overflow-auto rounded-panel border border-border bg-surface"
className="min-h-0 overflow-auto scroll-stable rounded-panel border border-border bg-surface"
>
<MatrixColumnHighlight>
<div className="min-w-[560px]">
@@ -1051,7 +1051,7 @@ const UnifiedMcpPanel: React.FC<UnifiedMcpPanelProps> = ({
</p>
)}
</div>
<div className="flex min-h-0 flex-col gap-4 overflow-y-auto overscroll-contain">
<div className="flex min-h-0 flex-col gap-4 overflow-y-auto scroll-stable overscroll-contain">
{importReport.error && (
<section className="flex flex-col gap-2">
<h3 className="m-0 text-body font-semibold">
@@ -134,7 +134,7 @@ function PromptDrawer({
</SheetTitle>
<SheetDescription className="sr-only">{description}</SheetDescription>
</div>
<div className="flex min-h-0 flex-1 flex-col gap-5 overflow-y-auto overscroll-contain px-6 pb-6 pt-5">
<div className="flex min-h-0 flex-1 flex-col gap-5 overflow-y-auto scroll-stable overscroll-contain px-6 pb-6 pt-5">
{children}
</div>
<div className="flex h-14 shrink-0 items-center gap-2 border-t border-border px-6">
@@ -907,7 +907,7 @@ export const PiPromptTemplates = forwardRef<
return (
<ul
aria-label={t("pi.prompts.templatesLabel")}
className="m-0 min-h-0 shrink list-none overflow-y-auto rounded-panel border border-border bg-surface p-0"
className="m-0 min-h-0 shrink list-none overflow-y-auto scroll-stable rounded-panel border border-border bg-surface p-0"
>
{filteredTemplates.map((template, index) => {
const summary = getPiPromptTemplateSummary(template.content);
+1 -1
View File
@@ -185,7 +185,7 @@ const PromptFormPanel: React.FC<PromptFormPanelProps> = ({
</SheetDescription>
</div>
<div className="flex min-h-0 flex-1 flex-col gap-5 overflow-y-auto overscroll-contain px-6 pb-6 pt-5">
<div className="flex min-h-0 flex-1 flex-col gap-5 overflow-y-auto scroll-stable overscroll-contain px-6 pb-6 pt-5">
<div className="flex shrink-0 flex-col gap-1.5">
<label
htmlFor={`${baseId}-name`}
+1 -1
View File
@@ -78,7 +78,7 @@ export function PromptLibrary({
return (
<ul
aria-label={listLabel}
className="m-0 min-h-0 shrink list-none overflow-y-auto rounded-panel border border-border bg-surface p-0"
className="m-0 min-h-0 shrink list-none overflow-y-auto scroll-stable rounded-panel border border-border bg-surface p-0"
>
{filtered.map(([id, prompt], index) => {
const summary =
@@ -517,7 +517,7 @@ export function SwitchModePanel({
<div
ref={scrollRef}
id="main-content"
className="min-h-0 flex-1 overflow-y-auto overflow-x-hidden px-6 pb-12 pt-3"
className="min-h-0 flex-1 overflow-y-auto scroll-stable overflow-x-hidden px-6 pb-12 pt-3"
>
{notices.length > 0 && (
<div role="status" className="mb-3 flex flex-col gap-2">
@@ -1299,7 +1299,7 @@ export function SessionManagerPage({
defaultValue: "会话列表",
})}
ref={listScrollRef}
className="mx-6 mt-3 min-h-0 shrink overflow-y-auto overscroll-contain rounded-panel border border-border bg-surface"
className="mx-6 mt-3 min-h-0 shrink overflow-y-auto scroll-stable overscroll-contain rounded-panel border border-border bg-surface"
style={{ scrollPaddingTop: groupMode === "project" ? 44 : 8 }}
>
{renderList()}
@@ -103,7 +103,7 @@ export const SessionOutline = memo(function SessionOutline({
</div>
<ol
ref={listRef}
className="m-0 min-h-0 flex-1 list-none overflow-y-auto overscroll-contain px-2 pb-4"
className="m-0 min-h-0 flex-1 list-none overflow-y-auto scroll-stable overscroll-contain px-2 pb-4"
>
{entries.map((entry) => {
const active = entry.turn.index === activeTurn;
@@ -688,7 +688,7 @@ export function SessionReader({
const bottom = el.scrollTop + el.clientHeight >= el.scrollHeight - 24;
setAtBottom((current) => (current === bottom ? current : bottom));
}}
className="min-h-0 flex-1 overflow-y-auto overscroll-contain px-6"
className="min-h-0 flex-1 overflow-y-auto scroll-stable overscroll-contain px-6"
>
<div
className="relative w-full"
+1 -1
View File
@@ -381,7 +381,7 @@ export function SettingsPage({
<div
ref={scrollRef}
id="main-content"
className="min-h-0 flex-1 overflow-y-auto"
className="min-h-0 flex-1 overflow-y-auto scroll-stable"
>
{isBusy ? (
<div className="flex h-full items-center justify-center">
+1 -1
View File
@@ -644,7 +644,7 @@ export function SkillsPage({
<div className="flex min-h-0 flex-1 flex-col px-6 pb-5">
<div
ref={listScrollRef}
className="min-h-0 overflow-auto rounded-panel border border-border bg-surface"
className="min-h-0 overflow-auto scroll-stable rounded-panel border border-border bg-surface"
>
{loadingList ? (
<div className="flex items-center justify-center gap-2 py-16 text-body text-fg-2">
+1 -1
View File
@@ -1097,7 +1097,7 @@ const UnifiedSkillsPanel: React.FC<UnifiedSkillsPanelProps> = ({
return (
<div
data-testid="skills-matrix"
className="min-h-0 overflow-auto rounded-panel border border-border bg-surface"
className="min-h-0 overflow-auto scroll-stable rounded-panel border border-border bg-surface"
>
<MatrixColumnHighlight>
<div className="min-w-[600px]">
+1 -1
View File
@@ -84,7 +84,7 @@ const SheetBody = ({
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
"min-h-0 flex-1 overflow-y-auto overscroll-contain px-5 py-4",
"min-h-0 flex-1 overflow-y-auto scroll-stable overscroll-contain px-5 py-4",
className,
)}
{...props}
+3 -3
View File
@@ -723,12 +723,12 @@ export function UsageDashboard({
// 空库里只有定价可配:不画全是 0 的概览和趋势,直接给页签
<div
id="main-content"
className="flex min-h-0 flex-1 flex-col gap-3.5 overflow-y-auto px-6 pb-6 pt-1"
className="flex min-h-0 flex-1 flex-col gap-3.5 overflow-y-auto scroll-stable px-6 pb-6 pt-1"
>
{tabsSection}
</div>
) : isEmpty ? (
<div className="flex min-h-0 flex-1 flex-col items-center overflow-y-auto px-6 pb-6">
<div className="flex min-h-0 flex-1 flex-col items-center overflow-y-auto scroll-stable px-6 pb-6">
<div className="mt-16 flex max-w-[440px] flex-col items-center gap-3 text-center">
<span
aria-hidden="true"
@@ -768,7 +768,7 @@ export function UsageDashboard({
) : (
<div
id="main-content"
className="flex min-h-0 flex-1 flex-col gap-3.5 overflow-y-auto px-6 pb-6 pt-1"
className="flex min-h-0 flex-1 flex-col gap-3.5 overflow-y-auto scroll-stable px-6 pb-6 pt-1"
>
<UsageHero
range={range}
+4 -5
View File
@@ -501,11 +501,10 @@ html:not([data-keyboard]) [tabindex]:focus {
display: none;
}
.scroll-overlay {
scrollbar-gutter: stable both-edges;
padding-right: 0.5rem;
margin-right: -0.5rem;
overflow-x: hidden;
/* 滚动条常显且占 8px:主滚动区、大列表先把这条槽留出来,
内容高度跨过能否滚动的临界时整列不会左右横跳 */
.scroll-stable {
scrollbar-gutter: stable;
}
.border-default {