mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-12 21:53:00 -06:00
improve(ui): polish sidebar scrolling and control alignment (#104586)
* fix(ui): make sidebar session scrollbar track transparent * feat(ui): fade overflowing sidebar sessions * style(ui): extend sidebar session fade * style(ui): align pinned item checks right * style(ui): align sidebar trailing controls * refactor(ui): trim sidebar polish noise * fix(ui): narrow sidebar resize observer target
This commit is contained in:
committed by
GitHub
parent
554f253cb0
commit
b14db9fc63
@@ -244,6 +244,39 @@ describe("AppSidebar update card wiring", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("AppSidebar session scroll fade", () => {
|
||||
it("shows fades only toward additional session content", async () => {
|
||||
const gateway = createGateway({} as GatewayBrowserClient);
|
||||
const { sidebar } = await mountSidebar(gateway, createSessions("main", ["agent:main:main"]));
|
||||
const scroller = sidebar.querySelector<HTMLElement>(".sidebar-recent-sessions");
|
||||
if (!scroller) {
|
||||
throw new Error("Expected sidebar session scroller");
|
||||
}
|
||||
|
||||
let scrollHeight = 100;
|
||||
Object.defineProperties(scroller, {
|
||||
clientHeight: { configurable: true, value: 100 },
|
||||
scrollHeight: { configurable: true, get: () => scrollHeight },
|
||||
});
|
||||
|
||||
const expectScrollState = async (
|
||||
scrollTop: number,
|
||||
expected: "none" | "top" | "middle" | "bottom",
|
||||
) => {
|
||||
scroller.scrollTop = scrollTop;
|
||||
scroller.dispatchEvent(new Event("scroll"));
|
||||
await sidebar.updateComplete;
|
||||
expect(scroller.classList.contains(`sidebar-recent-sessions--scroll-${expected}`)).toBe(true);
|
||||
};
|
||||
|
||||
await expectScrollState(0, "none");
|
||||
scrollHeight = 300;
|
||||
await expectScrollState(0, "top");
|
||||
await expectScrollState(80, "middle");
|
||||
await expectScrollState(200, "bottom");
|
||||
});
|
||||
});
|
||||
|
||||
describe("AppSidebar lobster outcome wiring", () => {
|
||||
it.each([
|
||||
["panel", "failed", "error"],
|
||||
|
||||
@@ -113,6 +113,7 @@ type SidebarSessionGroupMenuState = {
|
||||
};
|
||||
|
||||
type SidebarSessionSortMode = "created" | "updated";
|
||||
type SidebarSessionsScrollState = "none" | "top" | "middle" | "bottom";
|
||||
type SidebarSessionGroupDropTarget = {
|
||||
group: string;
|
||||
position: "before" | "after";
|
||||
@@ -229,6 +230,7 @@ class AppSidebar extends OpenClawLightDomContentsElement {
|
||||
@state() private sessionsAgentId: string | null = null;
|
||||
@state() private sessionsLoading = false;
|
||||
@state() private nativeSessionSidebarReady = false;
|
||||
@state() private sessionsScrollState: SidebarSessionsScrollState = "none";
|
||||
|
||||
private readonly subscriptions = new SubscriptionsController(this);
|
||||
private customizeMenuTrigger: HTMLElement | null = null;
|
||||
@@ -245,6 +247,8 @@ class AppSidebar extends OpenClawLightDomContentsElement {
|
||||
private gatewaySource: ApplicationContext<RouteId>["gateway"] | null = null;
|
||||
private gatewayClient: GatewayBrowserClient | null = null;
|
||||
private nativeSessionSidebarLoadStarted = false;
|
||||
private sessionsScrollElement: HTMLElement | null = null;
|
||||
private sessionsScrollResizeObserver: ResizeObserver | null = null;
|
||||
private readonly routePreloadTimers = new Map<
|
||||
EventTarget,
|
||||
ReturnType<typeof globalThis.setTimeout>
|
||||
@@ -285,10 +289,14 @@ class AppSidebar extends OpenClawLightDomContentsElement {
|
||||
globalThis.clearTimeout(timer);
|
||||
}
|
||||
this.routePreloadTimers.clear();
|
||||
this.sessionsScrollResizeObserver?.disconnect();
|
||||
this.sessionsScrollResizeObserver = null;
|
||||
this.sessionsScrollElement = null;
|
||||
super.disconnectedCallback();
|
||||
}
|
||||
|
||||
override updated() {
|
||||
this.syncSessionsScrollObserver();
|
||||
const advertised = this.pluginTabs().some(
|
||||
(tab) => tab.id === "sessions" && (tab.pluginId === "codex" || tab.pluginId === "anthropic"),
|
||||
);
|
||||
@@ -301,6 +309,41 @@ class AppSidebar extends OpenClawLightDomContentsElement {
|
||||
});
|
||||
}
|
||||
|
||||
private syncSessionsScrollObserver() {
|
||||
const element = this.querySelector<HTMLElement>(".sidebar-recent-sessions");
|
||||
if (element !== this.sessionsScrollElement) {
|
||||
this.sessionsScrollResizeObserver?.disconnect();
|
||||
this.sessionsScrollElement = element;
|
||||
this.sessionsScrollResizeObserver = null;
|
||||
if (element && typeof ResizeObserver === "function") {
|
||||
this.sessionsScrollResizeObserver = new ResizeObserver(() =>
|
||||
this.updateSessionsScrollState(element),
|
||||
);
|
||||
this.sessionsScrollResizeObserver.observe(element);
|
||||
}
|
||||
}
|
||||
if (element) {
|
||||
this.updateSessionsScrollState(element);
|
||||
}
|
||||
}
|
||||
|
||||
private updateSessionsScrollState(element: HTMLElement) {
|
||||
const maxScrollTop = Math.max(0, element.scrollHeight - element.clientHeight);
|
||||
let nextState: SidebarSessionsScrollState = "none";
|
||||
if (maxScrollTop > 1) {
|
||||
if (element.scrollTop <= 1) {
|
||||
nextState = "top";
|
||||
} else if (element.scrollTop >= maxScrollTop - 1) {
|
||||
nextState = "bottom";
|
||||
} else {
|
||||
nextState = "middle";
|
||||
}
|
||||
}
|
||||
if (nextState !== this.sessionsScrollState) {
|
||||
this.sessionsScrollState = nextState;
|
||||
}
|
||||
}
|
||||
|
||||
// The shell calls this before CSS hides the panel or drawer. Mounted menus
|
||||
// keep their document-level shortcuts alive even when an ancestor is hidden.
|
||||
dismissTransientMenus(): boolean {
|
||||
@@ -1323,13 +1366,13 @@ class AppSidebar extends OpenClawLightDomContentsElement {
|
||||
aria-checked=${String(pinned)}
|
||||
@click=${() => this.togglePinnedRoute(routeId)}
|
||||
>
|
||||
<span class="sidebar-customize-menu__check" aria-hidden="true">
|
||||
${pinned ? icons.check : nothing}
|
||||
</span>
|
||||
<span class="nav-item__icon" aria-hidden="true"
|
||||
>${icons[navigationIconForRoute(routeId)]}</span
|
||||
>
|
||||
<span class="sidebar-customize-menu__text">${titleForRoute(routeId)}</span>
|
||||
<span class="sidebar-customize-menu__check" aria-hidden="true">
|
||||
${pinned ? icons.check : nothing}
|
||||
</span>
|
||||
</button>
|
||||
`;
|
||||
})}
|
||||
@@ -1344,7 +1387,6 @@ class AppSidebar extends OpenClawLightDomContentsElement {
|
||||
this.closeCustomizeMenu({ restoreFocus: true });
|
||||
}}
|
||||
>
|
||||
<span class="sidebar-customize-menu__check" aria-hidden="true"></span>
|
||||
<span class="nav-item__icon" aria-hidden="true">${icons.refresh}</span>
|
||||
<span class="sidebar-customize-menu__text">${t("nav.customizeReset")}</span>
|
||||
</button>
|
||||
@@ -2009,7 +2051,13 @@ class AppSidebar extends OpenClawLightDomContentsElement {
|
||||
const expandedAgentId = this.expandedAgentId();
|
||||
return html`
|
||||
<section class="sidebar-sessions">
|
||||
<div class="sidebar-recent-sessions" aria-label=${titleForRoute("sessions")}>
|
||||
<div
|
||||
class="sidebar-recent-sessions sidebar-recent-sessions--scroll-${this
|
||||
.sessionsScrollState}"
|
||||
aria-label=${titleForRoute("sessions")}
|
||||
@scroll=${(event: Event) =>
|
||||
this.updateSessionsScrollState(event.currentTarget as HTMLElement)}
|
||||
>
|
||||
<div class="sidebar-recent-sessions__head sidebar-recent-sessions__head--root">
|
||||
<span class="sidebar-recent-sessions__label-text">${t("sessionsView.title")}</span>
|
||||
<button
|
||||
@@ -2096,7 +2144,7 @@ class AppSidebar extends OpenClawLightDomContentsElement {
|
||||
aria-expanded=${String(expanded)}
|
||||
>
|
||||
<span class="nav-section__label-text">${t("nav.more")}</span>
|
||||
<span class="nav-section__chevron"> ${icons.chevronDown} </span>
|
||||
<span class="nav-section__chevron">${icons.chevronDown}</span>
|
||||
</button>
|
||||
<div class="nav-section__items">
|
||||
${moreRoutes.map((routeId) => this.renderRoute(routeId))}
|
||||
|
||||
@@ -996,12 +996,48 @@ html.openclaw-native-nav .shell-nav-expand:focus-visible {
|
||||
margin: 0 -8px;
|
||||
min-height: 0;
|
||||
overflow-y: auto;
|
||||
scrollbar-color: color-mix(in srgb, currentColor 8%, transparent) transparent;
|
||||
scrollbar-width: thin;
|
||||
/* Sessions are app chrome, not hyperlinks: the whole region uses the
|
||||
default arrow cursor; the pointer hand is reserved for real links. */
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.sidebar-recent-sessions::-webkit-scrollbar,
|
||||
.sidebar-recent-sessions::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.sidebar-recent-sessions--scroll-top {
|
||||
--sidebar-sessions-mask: linear-gradient(
|
||||
to bottom,
|
||||
black 0,
|
||||
black calc(100% - 23px),
|
||||
transparent 100%
|
||||
);
|
||||
}
|
||||
|
||||
.sidebar-recent-sessions--scroll-middle {
|
||||
--sidebar-sessions-mask: linear-gradient(
|
||||
to bottom,
|
||||
transparent 0,
|
||||
black 23px,
|
||||
black calc(100% - 23px),
|
||||
transparent 100%
|
||||
);
|
||||
}
|
||||
|
||||
.sidebar-recent-sessions--scroll-bottom {
|
||||
--sidebar-sessions-mask: linear-gradient(to bottom, transparent 0, black 23px, black 100%);
|
||||
}
|
||||
|
||||
.sidebar-recent-sessions--scroll-top,
|
||||
.sidebar-recent-sessions--scroll-middle,
|
||||
.sidebar-recent-sessions--scroll-bottom {
|
||||
-webkit-mask-image: var(--sidebar-sessions-mask);
|
||||
mask-image: var(--sidebar-sessions-mask);
|
||||
}
|
||||
|
||||
/* The scroller's children must not flex-shrink: shrink squeezes each section
|
||||
to its min-height while the fixed-height rows inside overflow and paint
|
||||
over the following section. Fixed sizing makes the list scroll instead. */
|
||||
@@ -1093,11 +1129,13 @@ html.openclaw-native-nav .shell-nav-expand:focus-visible {
|
||||
}
|
||||
|
||||
.sidebar-session-group-count {
|
||||
width: 26px;
|
||||
flex: 0 0 auto;
|
||||
color: var(--muted);
|
||||
font-size: 10px;
|
||||
font-variant-numeric: tabular-nums;
|
||||
opacity: 0.7;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.sidebar-session-group-drag-handle {
|
||||
@@ -1402,6 +1440,10 @@ html.openclaw-native-nav .shell-nav-expand:focus-visible {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 26px;
|
||||
height: 26px;
|
||||
min-width: 0;
|
||||
flex: 0 0 26px;
|
||||
opacity: 0.5;
|
||||
transition: transform var(--duration-fast) ease;
|
||||
}
|
||||
@@ -1497,6 +1539,7 @@ html.openclaw-native-nav .shell-nav-expand:focus-visible {
|
||||
}
|
||||
|
||||
.sidebar-customize-menu__text {
|
||||
flex: 1;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
|
||||
Reference in New Issue
Block a user