mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-26 04:15:48 -06:00
fix(ui): restore floating sidebar attention cluster on collapsed nav (#128419)
* fix(ui): restore floating sidebar attention cluster on collapsed nav * fix(ui): let stylesheets own sidebar-attention display and clear the collapsed header The LightDomContents base stamps inline display:contents, which defeated the floating cluster's fixed flex layout; display is now stylesheet-owned. The collapsed chat pane header clears the cluster's resting width, and a stale-client refresh card drops below the strip. The layout browser test upgrades the real element so the inline-style regression is covered.
This commit is contained in:
committed by
GitHub
parent
3aab15c077
commit
ab94c85dfd
@@ -1,12 +1,67 @@
|
||||
import { afterEach, describe, expect, it } from "vitest";
|
||||
import "../test-helpers/load-styles.ts";
|
||||
import "../styles/hub-tabs.css";
|
||||
import "../styles/sidebar-footer-update.css";
|
||||
import "../styles/sidebar-issues.css";
|
||||
import "./web-awesome-tabs.ts";
|
||||
// Upgrade the real element: the floating layout once regressed because a base
|
||||
// class stamped inline `display: contents`, which only a live upgrade reveals.
|
||||
import "./sidebar-attention.ts";
|
||||
|
||||
afterEach(() => document.body.replaceChildren());
|
||||
afterEach(() => {
|
||||
document.body.replaceChildren();
|
||||
document.documentElement.classList.remove(
|
||||
"openclaw-native-nav",
|
||||
"openclaw-native-macos",
|
||||
"openclaw-native-web-chrome",
|
||||
);
|
||||
});
|
||||
|
||||
describe.runIf("__vitest_browser__" in globalThis)("Inbox panel layout", () => {
|
||||
it("positions collapsed sidebar attention beyond chrome and access controls", () => {
|
||||
const shell = document.createElement("div");
|
||||
shell.className = "shell shell--nav-collapsed";
|
||||
shell.innerHTML = `
|
||||
<div class="shell-chrome-controls">
|
||||
<button class="shell-chrome-controls__button"></button>
|
||||
<button class="shell-chrome-controls__button"></button>
|
||||
<button class="shell-chrome-controls__button"></button>
|
||||
<button class="shell-chrome-controls__button shell-chrome-controls__custodian"></button>
|
||||
</div>
|
||||
<button class="shell-chrome-controls__button scope-upgrade-shell-status"></button>
|
||||
<main class="content">
|
||||
<openclaw-sidebar-attention class="sidebar-attention--floating">
|
||||
<button class="sidebar-issues-button"></button>
|
||||
</openclaw-sidebar-attention>
|
||||
</main>
|
||||
`;
|
||||
document.body.append(shell);
|
||||
|
||||
const attention = shell.querySelector<HTMLElement>("openclaw-sidebar-attention")!;
|
||||
const chrome = shell.querySelector<HTMLElement>(".shell-chrome-controls")!;
|
||||
const access = shell.querySelector<HTMLElement>(".scope-upgrade-shell-status")!;
|
||||
const inbox = attention.querySelector<HTMLElement>(".sidebar-issues-button")!;
|
||||
|
||||
expect(getComputedStyle(attention).position).toBe("fixed");
|
||||
expect(getComputedStyle(attention).display).toBe("flex");
|
||||
expect(attention.getBoundingClientRect().left).toBeGreaterThanOrEqual(
|
||||
Math.max(chrome.getBoundingClientRect().right, access.getBoundingClientRect().right) + 8,
|
||||
);
|
||||
expect(Number.parseFloat(getComputedStyle(inbox).borderTopWidth)).toBeGreaterThan(0);
|
||||
|
||||
document.documentElement.classList.add("openclaw-native-nav");
|
||||
expect(getComputedStyle(attention).left).toBe("52px");
|
||||
expect(attention.getBoundingClientRect().left).toBeGreaterThanOrEqual(
|
||||
access.getBoundingClientRect().right + 8,
|
||||
);
|
||||
|
||||
document.documentElement.classList.add("openclaw-native-macos");
|
||||
expect(getComputedStyle(attention).top).toBe("52px");
|
||||
|
||||
document.documentElement.classList.add("openclaw-native-web-chrome");
|
||||
expect(getComputedStyle(attention).left).toBe("16px");
|
||||
});
|
||||
|
||||
it("keeps hub tabs compact and item rails flush with the scrollport", async () => {
|
||||
const fixture = document.createElement("section");
|
||||
fixture.className = "sidebar-issues-panel";
|
||||
|
||||
@@ -17,6 +17,11 @@ import {
|
||||
import { ISSUE_TABS, issueTabLabel, type IssueTab } from "./sidebar-issues-tabs.ts";
|
||||
import "./menu-surface.ts";
|
||||
|
||||
export type SidebarAttentionPanelPosition = { left: number } & (
|
||||
| { anchor: "top"; top: number }
|
||||
| { anchor: "bottom"; bottom: number }
|
||||
);
|
||||
|
||||
type SidebarAttentionPanelParams = {
|
||||
approvalQueue: readonly ExecApprovalRequest[];
|
||||
context: ApplicationContext;
|
||||
@@ -32,13 +37,17 @@ type SidebarAttentionPanelParams = {
|
||||
onSelectTab: (tab: IssueTab) => void;
|
||||
overflowAbove: boolean;
|
||||
overflowBelow: boolean;
|
||||
panelPosition: { left: number; bottom: number };
|
||||
panelPosition: SidebarAttentionPanelPosition;
|
||||
selectedTab: IssueTab;
|
||||
updateSurface: boolean;
|
||||
watchUpdateProgress?: (listener: (progress: UpdateProgress) => void) => () => void;
|
||||
};
|
||||
|
||||
export function renderSidebarAttentionPanel(params: SidebarAttentionPanelParams): TemplateResult {
|
||||
const { anchor } = params.panelPosition;
|
||||
const panelOffset =
|
||||
params.panelPosition.anchor === "top" ? params.panelPosition.top : params.panelPosition.bottom;
|
||||
const panelStyle = `left:${params.panelPosition.left}px;${anchor}:${panelOffset}px;--sidebar-issues-panel-${anchor}:${panelOffset}px`;
|
||||
const automationItems = params.items.filter(
|
||||
(item) => item.kind === "cronFailed" || item.kind === "cronOverdue",
|
||||
);
|
||||
@@ -106,7 +115,7 @@ export function renderSidebarAttentionPanel(params: SidebarAttentionPanelParams)
|
||||
class="sidebar-issues-panel"
|
||||
role="dialog"
|
||||
aria-labelledby="sidebar-issues-panel-heading"
|
||||
style=${`left:${params.panelPosition.left}px;bottom:${params.panelPosition.bottom}px;--sidebar-issues-panel-bottom:${params.panelPosition.bottom}px`}
|
||||
style=${panelStyle}
|
||||
@keydown=${params.onKeydown}
|
||||
>
|
||||
<div class="sidebar-issues-panel__grabber" aria-hidden="true"></div>
|
||||
|
||||
@@ -420,7 +420,7 @@ describe("sidebar attention refresh ownership", () => {
|
||||
switchedAuth.resolve({ ts: 3, providers: [] });
|
||||
});
|
||||
|
||||
it("clears a stale failure alert when the gateway reports an automation change", async () => {
|
||||
it("opens top-mounted attention downward and clears stale live automation alerts", async () => {
|
||||
const responses = {
|
||||
"cron.list": [cronListResponse([cronJob("failed")]), cronListResponse([])],
|
||||
"models.authStatus": [{ ts: 1, providers: [] }],
|
||||
@@ -478,10 +478,26 @@ describe("sidebar attention refresh ownership", () => {
|
||||
await waitForFast(() =>
|
||||
expect(element.querySelector<HTMLButtonElement>(".sidebar-issues-button")).not.toBeNull(),
|
||||
);
|
||||
element.querySelector<HTMLButtonElement>(".sidebar-issues-button")?.click();
|
||||
const trigger = element.querySelector<HTMLButtonElement>(".sidebar-issues-button")!;
|
||||
vi.spyOn(trigger, "getBoundingClientRect").mockReturnValue({
|
||||
x: 20,
|
||||
y: 10,
|
||||
left: 20,
|
||||
top: 10,
|
||||
right: 52,
|
||||
bottom: 42,
|
||||
width: 32,
|
||||
height: 32,
|
||||
toJSON: () => ({}),
|
||||
});
|
||||
trigger.click();
|
||||
await waitForFast(() =>
|
||||
expect(element.querySelector('[data-attention-kind="cronFailed"]')).not.toBeNull(),
|
||||
);
|
||||
const panel = element.querySelector<HTMLElement>(".sidebar-issues-panel")!;
|
||||
expect(panel.style.top).toBe("50px");
|
||||
expect(panel.style.bottom).toBe("");
|
||||
expect(panel.style.getPropertyValue("--sidebar-issues-panel-top")).toBe("50px");
|
||||
|
||||
eventListener?.({ type: "event", event: "cron", payload: {} });
|
||||
await waitForFast(() =>
|
||||
|
||||
@@ -19,7 +19,7 @@ import { t } from "../i18n/index.ts";
|
||||
import { createInitialCronState, loadCronJobsPage } from "../lib/cron/index.ts";
|
||||
import { canCallGatewayMethod } from "../lib/gateway-methods.ts";
|
||||
import { loadModelAuthStatus } from "../lib/model-auth.ts";
|
||||
import { OpenClawLightDomContentsElement } from "../lit/openclaw-element.ts";
|
||||
import { OpenClawLightDomElement } from "../lit/openclaw-element.ts";
|
||||
import { SubscriptionsController } from "../lit/subscriptions-controller.ts";
|
||||
import "../styles/sidebar-footer-update.css";
|
||||
import { icons } from "./icons.ts";
|
||||
@@ -41,6 +41,7 @@ import {
|
||||
buildSidebarAttentionItems,
|
||||
type SidebarAttentionItem,
|
||||
} from "./sidebar-attention-items.ts";
|
||||
import type { SidebarAttentionPanelPosition } from "./sidebar-attention-panel.runtime.ts";
|
||||
import "./tooltip.ts";
|
||||
import type { IssueTab } from "./sidebar-issues-tabs.ts";
|
||||
|
||||
@@ -56,7 +57,10 @@ const ITEM_PRIORITY: Record<SidebarAttentionItem["kind"], number> = {
|
||||
cronFailed: 1,
|
||||
cronOverdue: 2,
|
||||
};
|
||||
class SidebarAttention extends OpenClawLightDomContentsElement {
|
||||
// Display is stylesheet-owned (layout.css `display: contents` in the footer,
|
||||
// flex when floating): the LightDomContents base's inline display would defeat
|
||||
// the floating override, re-piling the collapsed-nav cluster at the origin.
|
||||
class SidebarAttention extends OpenClawLightDomElement {
|
||||
@consume({ context: applicationContext, subscribe: true })
|
||||
private context?: ApplicationContext;
|
||||
|
||||
@@ -64,7 +68,11 @@ class SidebarAttention extends OpenClawLightDomContentsElement {
|
||||
@state() private modelAuthStatus: ModelAuthStatusResult | null = null;
|
||||
@state() private dismissed: SidebarAttentionDismissals = {};
|
||||
@state() private panelOpen = false;
|
||||
@state() private panelPosition = { left: 8, bottom: 8 };
|
||||
@state() private panelPosition: SidebarAttentionPanelPosition = {
|
||||
left: 8,
|
||||
anchor: "bottom",
|
||||
bottom: 8,
|
||||
};
|
||||
@state() private selectedTab: IssueTab = "all";
|
||||
@state() private overflowAbove = false;
|
||||
@state() private overflowBelow = false;
|
||||
@@ -471,12 +479,13 @@ class SidebarAttention extends OpenClawLightDomContentsElement {
|
||||
const rect = trigger.getBoundingClientRect();
|
||||
const width = Math.min(390, globalThis.innerWidth - 16);
|
||||
const preferredLeft = rect.left + rect.width / 2 - width / 2;
|
||||
const left = Math.max(8, Math.min(preferredLeft, globalThis.innerWidth - width - 8));
|
||||
this.panelTrigger = trigger;
|
||||
this.panelRenderer = panelRenderer;
|
||||
this.panelPosition = {
|
||||
left: Math.max(8, Math.min(preferredLeft, globalThis.innerWidth - width - 8)),
|
||||
bottom: Math.max(8, globalThis.innerHeight - rect.top + 8),
|
||||
};
|
||||
this.panelPosition =
|
||||
rect.top < globalThis.innerHeight / 2
|
||||
? { left, anchor: "top", top: Math.max(8, rect.bottom + 8) }
|
||||
: { left, anchor: "bottom", bottom: Math.max(8, globalThis.innerHeight - rect.top + 8) };
|
||||
this.selectedTab = "all";
|
||||
this.panelOpen = true;
|
||||
document.addEventListener("pointerdown", this.closeOnOutsidePointer, true);
|
||||
|
||||
@@ -860,6 +860,22 @@ html:not(.openclaw-native-macos):not(.openclaw-native-nav):not(.openclaw-native-
|
||||
padding-left: 124px;
|
||||
}
|
||||
|
||||
/* The floating attention cluster widens the overlay strip; clear its resting
|
||||
width (inbox 32 + gap 4 + update 32 + 12 breathing) so the session title is
|
||||
not covered. The update pill's hover expansion may transiently overlap.
|
||||
Native-nav/web-chrome hosts pin the cluster at the far left, inside the
|
||||
larger insets below. */
|
||||
html:not(.openclaw-native-macos):not(.openclaw-native-nav):not(.openclaw-native-web-chrome)
|
||||
.shell--nav-collapsed:not(.shell--mobile-nav):has(.sidebar-attention--floating)
|
||||
.chat-split-view__column:first-child
|
||||
> .chat-split-view__cell:first-child
|
||||
:is(.chat-main__conversation-column, .chat-pane-primary-column)
|
||||
> .chat-pane__header {
|
||||
padding-left: calc(
|
||||
var(--shell-chrome-controls-inset) + var(--shell-chrome-controls-collapsed-width) + 88px
|
||||
);
|
||||
}
|
||||
|
||||
/* Native macOS desktop shells: the pane header is the window's top surface,
|
||||
so it adopts the app titlebar height (52px injected by
|
||||
DashboardWindowController; the 50px fallback mirrors
|
||||
|
||||
@@ -1,3 +1,46 @@
|
||||
/* Floating footer controls must clear the collapsed chrome and scope indicator. */
|
||||
openclaw-sidebar-attention.sidebar-attention--floating {
|
||||
--sidebar-bg: var(--bg-content, var(--bg));
|
||||
|
||||
position: fixed;
|
||||
top: 10px;
|
||||
left: calc(
|
||||
var(--shell-chrome-controls-inset) + var(--shell-chrome-controls-collapsed-width) + 8px
|
||||
);
|
||||
z-index: 45;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--shell-chrome-controls-gap);
|
||||
}
|
||||
|
||||
/* Native navigation hides web controls but leaves its scope indicator in-page. */
|
||||
html.openclaw-native-nav openclaw-sidebar-attention.sidebar-attention--floating {
|
||||
left: calc(var(--shell-chrome-controls-inset) + var(--shell-chrome-controls-access-width) + 6px);
|
||||
}
|
||||
|
||||
html.openclaw-native-web-chrome openclaw-sidebar-attention.sidebar-attention--floating {
|
||||
left: 16px;
|
||||
}
|
||||
|
||||
html.openclaw-native-macos openclaw-sidebar-attention.sidebar-attention--floating {
|
||||
top: 52px;
|
||||
}
|
||||
|
||||
/* A stale-client refresh card shares the collapsed overlay strip's row; drop it
|
||||
below the fixed attention cluster instead of rendering both at the origin. */
|
||||
.sidebar-attention--floating ~ .sidebar-update-card--floating .sidebar-update-card {
|
||||
margin-top: 50px;
|
||||
}
|
||||
|
||||
/* Footer buttons need opaque chrome when they float over arbitrary page content. */
|
||||
:where(openclaw-sidebar-attention.sidebar-attention--floating) .sidebar-issues-button {
|
||||
border: 1px solid color-mix(in srgb, var(--border) 88%, transparent);
|
||||
background: color-mix(in srgb, var(--panel) 92%, transparent);
|
||||
box-shadow: var(--shadow-sm);
|
||||
backdrop-filter: blur(10px);
|
||||
-webkit-backdrop-filter: blur(10px);
|
||||
}
|
||||
|
||||
.sidebar-footer-bar:has(.sidebar-footer-update-slot) {
|
||||
padding-inline-end: 88px;
|
||||
}
|
||||
|
||||
@@ -6,8 +6,8 @@
|
||||
500px,
|
||||
calc(
|
||||
100dvh -
|
||||
max(var(--sidebar-issues-panel-bottom, 8px), calc(8px + env(safe-area-inset-bottom, 0px))) -
|
||||
8px - env(safe-area-inset-top, 0px)
|
||||
max(var(--sidebar-issues-panel-top, 8px), calc(8px + env(safe-area-inset-top, 0px))) -
|
||||
max(var(--sidebar-issues-panel-bottom, 8px), calc(8px + env(safe-area-inset-bottom, 0px)))
|
||||
)
|
||||
);
|
||||
display: flex;
|
||||
|
||||
Reference in New Issue
Block a user