fix(ui): center the onboarding dashboard and fix its card chrome (#121416)

* fix(ui): center onboarding dashboard and quiet its chrome

The custodian onboarding route carried the settings takeover class, so the
shell reserved a hidden 288px sidebar column and the whole page rendered
off-center. Onboarding now opts out of the takeover, drops the duplicate
header identity (the thread already introduces the mascot), and option-card
hover no longer lifts or re-tints cards.

* fix(ui): stop next-step cards overlaying the message meta row

The option-card and wizard-step thread attachments pulled themselves up with
a -12px top margin, covering the preceding message's sender/timestamp line.

* test(ui): align nudge e2e with the minimal onboarding header
This commit is contained in:
Peter Steinberger
2026-08-09 22:31:17 -07:00
committed by GitHub
parent 4b6a9785c9
commit 7d1958bf61
8 changed files with 54 additions and 26 deletions
+3 -1
View File
@@ -125,7 +125,9 @@ export function renderApplicationShell(host: ShellViewHost) {
? pluginTabRefFromSearch(host.routeState.location?.search ?? "")
: null;
const activePluginTabId = activePluginRef ? pluginTabKey(activePluginRef) : "";
const settingsTakeover = isSettingsNavigationRoute(activeRoute);
// Onboarding renders without any navigation chrome, so the settings takeover
// must not reserve its fixed sidebar column (the grid would stay off-center).
const settingsTakeover = isSettingsNavigationRoute(activeRoute) && !host.onboardingMode;
const runtimeConfig = context.runtimeConfig.state;
const settingsSearchBlocks = findSettingsSearchBlocks({
query: host.settingsSearchQuery,
+2 -1
View File
@@ -423,7 +423,8 @@ suite.define(() => {
const response = await page.goto(`${suite.server.baseUrl}custodian?onboarding=1`);
expect(response?.status()).toBe(200);
await page.getByRole("heading", { name: "OpenClaw", exact: true }).waitFor();
// Onboarding chrome keeps only the header actions; no identity heading.
await page.locator(".custodian__header--minimal").waitFor();
await gateway.emitGatewayEvent("health", {
channelLabels: { telegram: "Telegram" },
channels: {
+3 -1
View File
@@ -119,7 +119,9 @@ suite.define(() => {
await page.getByRole("button", { name: "Continue setup" }).click();
await expect.poll(() => new URL(page.url()).pathname).toBe("/custodian");
expect(new URL(page.url()).searchParams.get("onboarding")).toBe("1");
await page.getByRole("heading", { name: "OpenClaw", exact: true }).waitFor();
// Onboarding chrome keeps only the header actions; no identity heading.
await page.locator(".custodian__header--minimal").waitFor();
await page.getByRole("button", { name: "Exit setup" }).waitFor();
await expect
.poll(() => page.locator(".shell").getAttribute("class"))
.toContain("shell--onboarding");
@@ -57,7 +57,8 @@ describe("custodian page", () => {
.querySelector<HTMLImageElement>("img.chat-avatar.assistant")
?.getAttribute("src"),
).toBe("/favicon.svg");
expect(page.querySelector(".custodian__mark openclaw-mascot")).not.toBeNull();
// Onboarding strips the header identity; the thread avatar is the only mascot.
expect(page.querySelector(".custodian__mark openclaw-mascot")).toBeNull();
const card = page.querySelector("openclaw-option-card")!;
await card.updateComplete;
expect(page.querySelector(".option-card__choice--recommended")?.textContent).toContain(
+19 -13
View File
@@ -224,19 +224,25 @@ export class CustodianPage extends OpenClawLightDomElement {
? "custodian--setup-required"
: ""}"
>
<header class="custodian__header custodian__column">
<div class="custodian__identity">
<div class="custodian__mark" aria-hidden="true">
<openclaw-mascot
.mood=${this.store.sending ? "thinking" : "idle"}
.size=${38}
></openclaw-mascot>
</div>
<div>
<h1>${t("custodian.title")}</h1>
<p>${t(this.onboarding ? "custodian.subtitle" : "custodian.subtitleCaretaker")}</p>
</div>
</div>
<header
class="custodian__header custodian__column ${this.onboarding
? "custodian__header--minimal"
: ""}"
>
${this.onboarding
? nothing
: html`<div class="custodian__identity">
<div class="custodian__mark" aria-hidden="true">
<openclaw-mascot
.mood=${this.store.sending ? "thinking" : "idle"}
.size=${38}
></openclaw-mascot>
</div>
<div>
<h1>${t("custodian.title")}</h1>
<p>${t("custodian.subtitleCaretaker")}</p>
</div>
</div>`}
<div class="custodian__header-actions">
${this.historyAvailable
? html`<button
+4 -3
View File
@@ -96,8 +96,9 @@ describe("custodian route", () => {
>("openclaw-custodian-page");
await onboardingPage?.updateComplete;
expect(onboardingPage?.querySelector(".custodian__header .btn")).not.toBeNull();
expect(onboardingPage?.querySelector(".custodian__header p")?.textContent?.trim()).toBe(
"Your system setup guide",
);
// Onboarding renders the minimal header: actions only, no identity block.
expect(onboardingPage?.querySelector(".custodian__header--minimal")).not.toBeNull();
expect(onboardingPage?.querySelector(".custodian__header p")).toBeNull();
expect(onboardingPage?.querySelector(".custodian__identity")).toBeNull();
});
});
+12 -3
View File
@@ -109,6 +109,14 @@ openclaw-custodian-page {
gap: 8px;
}
/* Onboarding: the thread already introduces the mascot, so the header keeps
only its actions as a quiet top-right row. */
.custodian__header--minimal {
justify-content: flex-end;
padding-bottom: 8px;
border-bottom: 0;
}
.custodian__identity {
display: flex;
align-items: center;
@@ -252,14 +260,15 @@ openclaw-custodian-page {
.custodian__option-card {
/* Left offset = avatar column (36px) + group gap (10px) so cards align with
the message text column, not the avatar gutter. */
margin: -12px 16px 14px 46px;
the message text column, not the avatar gutter. No negative top margin:
it overlays the preceding message's sender/timestamp row. */
margin: 4px 16px 14px 46px;
}
.custodian__wizard-step {
display: grid;
gap: 12px;
margin: -12px 16px 14px 46px;
margin: 4px 16px 14px 46px;
padding: 16px;
border: 1px solid var(--border);
border-radius: var(--radius-lg);
+9 -3
View File
@@ -48,14 +48,12 @@
transition:
border-color var(--duration-fast) var(--ease-out),
background var(--duration-fast) var(--ease-out),
box-shadow var(--duration-fast) var(--ease-out),
transform var(--duration-fast) var(--ease-out);
box-shadow var(--duration-fast) var(--ease-out);
}
.option-card__choice:hover:not(:disabled) {
border-color: var(--border-strong);
background: var(--bg-hover);
transform: translateY(-1px);
}
.option-card__choice:focus-visible {
@@ -69,6 +67,14 @@
background: var(--accent-subtle);
}
/* Hover must not swap the accent tint for the neutral one; the mismatched
layers read as the card overlapping itself. */
.option-card__choice--recommended:hover:not(:disabled),
.option-card__choice--selected:hover:not(:disabled) {
border-color: var(--accent);
background: var(--accent-subtle);
}
.option-card__choice-copy {
display: grid;
gap: 5px;