fix(ui): align settings search with navigation rows (#112172)

* fix(ui): align settings search with navigation

* fix(ui): separate scrolled settings navigation
This commit is contained in:
Vincent Koc
2026-07-21 15:16:06 +08:00
committed by GitHub
parent 7d7c2835a5
commit cec527a58a
3 changed files with 138 additions and 2 deletions
+15 -1
View File
@@ -198,6 +198,15 @@ function renderBlockItem(props: SettingsSidebarProps, block: SettingsSearchBlock
`;
}
function syncSettingsSearchScrollShadow(nav: HTMLElement) {
// The nav's top padding scrolls away with its rows. Keep the fixed search
// region visually separated once content reaches that boundary.
nav
.closest(".settings-sidebar")
?.querySelector(".settings-sidebar__search")
?.classList.toggle("settings-sidebar__search--scrolled", nav.scrollTop > 0);
}
export function renderSettingsSidebar(props: SettingsSidebarProps) {
const gatewayStatus = t("chat.gatewayStatus", {
status: props.connected ? t("common.online") : t("common.offline"),
@@ -255,7 +264,12 @@ export function renderSettingsSidebar(props: SettingsSidebarProps) {
`
: nothing}
</div>
<nav class="settings-sidebar__nav" aria-label=${t("common.settingsSections")}>
<nav
class="settings-sidebar__nav"
aria-label=${t("common.settingsSections")}
@scroll=${(event: Event) =>
syncSettingsSearchScrollShadow(event.currentTarget as HTMLElement)}
>
${navigationGroups.length === 0
? html`<p class="settings-sidebar__empty" role="status">
${t("nav.settingsSearchNoResults")}
@@ -121,6 +121,109 @@ describeControlUiE2e("Control UI sidebar customization mocked Gateway E2E", () =
await server?.close();
});
it.each([
{ mode: "standalone", webChrome: false },
{ mode: "native web chrome", webChrome: true },
])("aligns the settings search with navigation rows in $mode", async ({ mode, webChrome }) => {
const context = await browser.newContext({
locale: "en-US",
serviceWorkers: "block",
viewport: { height: 620, width: 1440 },
});
const page = await context.newPage();
if (webChrome) {
await page.addInitScript(() => {
const nativeWindow = window as Window & {
__OPENCLAW_NATIVE_WEB_CHROME__?: boolean;
__OPENCLAW_NATIVE_HISTORY__?: { canGoBack: boolean; canGoForward: boolean };
};
nativeWindow["__OPENCLAW_NATIVE_WEB_CHROME__"] = true;
nativeWindow["__OPENCLAW_NATIVE_HISTORY__"] = {
canGoBack: false,
canGoForward: false,
};
const stamp = () =>
document.documentElement.classList.add(
"openclaw-native-macos",
"openclaw-native-web-chrome",
);
if (document.documentElement) {
stamp();
} else {
document.addEventListener("DOMContentLoaded", stamp);
}
});
}
await installMockGateway(page);
try {
await page.goto(`${server.baseUrl}settings/general`);
const settingsSidebar = page.locator(".settings-sidebar");
const settingsSearchShell = settingsSidebar.locator(".settings-sidebar__search");
const settingsSearchInput = settingsSidebar.locator(".settings-sidebar__search-input");
const settingsNav = settingsSidebar.locator(".settings-sidebar__nav");
const firstSettingsLink = settingsSidebar.locator(".settings-sidebar__item").first();
await settingsSidebar.waitFor();
await expect
.poll(() =>
page
.locator("html")
.evaluate((element) => element.classList.contains("openclaw-native-web-chrome")),
)
.toBe(webChrome);
await captureSettingsSidebarProof(
settingsSidebar,
`settings-search-alignment-${mode.replaceAll(" ", "-")}.png`,
);
await expect
.poll(async () => {
const [searchBox, firstLinkBox] = await Promise.all([
settingsSearchShell.boundingBox(),
firstSettingsLink.boundingBox(),
]);
if (!searchBox || !firstLinkBox) {
return null;
}
return Math.round(searchBox.x - firstLinkBox.x);
})
.toBe(0);
await expect
.poll(async () => {
const [searchBox, navBox] = await Promise.all([
settingsSearchInput.boundingBox(),
settingsNav.boundingBox(),
]);
if (!searchBox || !navBox) {
return null;
}
return Math.round(navBox.y - (searchBox.y + searchBox.height));
})
.toBe(8);
await settingsNav.evaluate((element) => {
element.scrollTop = Math.min(48, element.scrollHeight - element.clientHeight);
element.dispatchEvent(new Event("scroll"));
});
await expect
.poll(() =>
settingsSearchShell.evaluate((element) =>
element.classList.contains("settings-sidebar__search--scrolled"),
),
)
.toBe(true);
await expect
.poll(() =>
settingsSearchShell.evaluate((element) => getComputedStyle(element, "::after").opacity),
)
.toBe("1");
await captureSettingsSidebarProof(
settingsSidebar,
`settings-search-scrolled-${mode.replaceAll(" ", "-")}.png`,
);
} finally {
await context.close();
}
});
it("pins routes, restores defaults, and persists navigation state across reloads", async () => {
if (captureUiProofEnabled) {
await mkdir(uiProofArtifactDir, { recursive: true });
+20 -1
View File
@@ -326,13 +326,32 @@ html.openclaw-native-web-chrome .shell:not(.shell--mobile-nav) .sidebar-brand .s
.settings-sidebar__search {
position: relative;
z-index: 1;
flex-shrink: 0;
display: flex;
align-items: center;
margin: 8px 21px 0;
margin: 8px 12px 0;
padding-bottom: 8px;
color: var(--muted);
}
.settings-sidebar__search::after {
content: "";
position: absolute;
inset-inline: -12px;
bottom: 0;
height: 1px;
pointer-events: none;
background: color-mix(in srgb, var(--border) 62%, transparent);
box-shadow: 0 6px 12px color-mix(in srgb, var(--text) 14%, transparent);
opacity: 0;
transition: opacity var(--duration-fast) ease;
}
.settings-sidebar__search--scrolled::after {
opacity: 1;
}
.settings-sidebar__search-icon {
position: absolute;
inset-inline-start: 10px;