fix(control-ui): link dashboard breadcrumb

Make the topbar OpenClaw breadcrumb a semantic Overview link, wire the existing navigate event at the app shell, and preserve prefixed Control UI base paths.\n\nValidation:\n- pnpm test ui/src/ui/navigation.browser.test.ts\n- pnpm exec oxfmt --check --threads=1 ui/src/ui/components/dashboard-header.ts ui/src/ui/app-render.ts ui/src/ui/navigation.browser.test.ts\n- git diff --check origin/main...HEAD
This commit is contained in:
Val Alexander
2026-04-29 10:27:10 -05:00
committed by GitHub
parent a2cf05c4fb
commit 1390eadd92
4 changed files with 82 additions and 9 deletions

View File

@@ -160,6 +160,23 @@
color: var(--muted);
}
.topnav-shell .dashboard-header__breadcrumb-link {
display: inline-flex;
align-items: center;
margin: -2px -4px;
padding: 2px 4px;
border-radius: var(--radius-sm);
text-decoration: none;
white-space: nowrap;
}
.topnav-shell .dashboard-header__breadcrumb-link:hover,
.topnav-shell .dashboard-header__breadcrumb-link:focus-visible {
color: var(--text);
text-decoration: underline;
text-underline-offset: 3px;
}
.topnav-shell .dashboard-header__breadcrumb-current {
color: var(--accent);
font-weight: 650;

View File

@@ -117,7 +117,13 @@ import {
import { buildExternalLinkRel, EXTERNAL_LINK_TARGET } from "./external-link.ts";
import { icons } from "./icons.ts";
import { createLazyView, renderLazyView } from "./lazy-view.ts";
import { normalizeBasePath, TAB_GROUPS, subtitleForTab, titleForTab } from "./navigation.ts";
import {
normalizeBasePath,
TAB_GROUPS,
subtitleForTab,
titleForTab,
type Tab,
} from "./navigation.ts";
import { isPluginEnabledInConfigSnapshot } from "./plugin-activation.ts";
import "./components/dashboard-header.ts";
import {
@@ -1346,7 +1352,13 @@ export function renderApp(state: AppViewState) {
<span class="nav-collapse-toggle__icon" aria-hidden="true">${icons.menu}</span>
</button>
<div class="topnav-shell__content">
<dashboard-header .tab=${state.tab}></dashboard-header>
<dashboard-header
.tab=${state.tab}
.basePath=${state.basePath}
@navigate=${(event: CustomEvent<Tab>) => {
state.setTab(event.detail);
}}
></dashboard-header>
</div>
<div class="topnav-shell__actions">
<button

View File

@@ -1,6 +1,6 @@
import { LitElement, html } from "lit";
import { property } from "lit/decorators.js";
import { titleForTab, type Tab } from "../navigation.js";
import { pathForTab, titleForTab, type Tab } from "../navigation.js";
export class DashboardHeader extends LitElement {
override createRenderRoot() {
@@ -8,6 +8,24 @@ export class DashboardHeader extends LitElement {
}
@property() tab: Tab = "overview";
@property() basePath = "";
private handleOverviewClick(event: MouseEvent) {
if (
event.defaultPrevented ||
event.button !== 0 ||
event.metaKey ||
event.ctrlKey ||
event.shiftKey ||
event.altKey
) {
return;
}
event.preventDefault();
this.dispatchEvent(
new CustomEvent("navigate", { detail: "overview", bubbles: true, composed: true }),
);
}
override render() {
const label = titleForTab(this.tab);
@@ -15,15 +33,13 @@ export class DashboardHeader extends LitElement {
return html`
<div class="dashboard-header">
<div class="dashboard-header__breadcrumb">
<span
<a
class="dashboard-header__breadcrumb-link"
@click=${() =>
this.dispatchEvent(
new CustomEvent("navigate", { detail: "overview", bubbles: true, composed: true }),
)}
href=${pathForTab("overview", this.basePath)}
@click=${this.handleOverviewClick}
>
OpenClaw
</span>
</a>
<span class="dashboard-header__breadcrumb-sep"></span>
<span class="dashboard-header__breadcrumb-current">${label}</span>
</div>

View File

@@ -48,6 +48,34 @@ describe("control UI routing", () => {
expect(dreamsLink).not.toBeNull();
});
it("renders the dashboard breadcrumb as an overview link", async () => {
const app = mountApp("/channels");
await app.updateComplete;
const breadcrumb = app.querySelector<HTMLAnchorElement>(
"dashboard-header .dashboard-header__breadcrumb-link",
);
expect(breadcrumb).toBeInstanceOf(HTMLAnchorElement);
expect(breadcrumb?.getAttribute("href")).toBe("/overview");
breadcrumb?.dispatchEvent(new MouseEvent("click", { bubbles: true, cancelable: true }));
await app.updateComplete;
expect(app.tab).toBe("overview");
expect(window.location.pathname).toBe("/overview");
});
it("keeps the dashboard breadcrumb link inside the configured base path", async () => {
const app = mountApp("/ui/channels");
await app.updateComplete;
const breadcrumb = app.querySelector<HTMLAnchorElement>(
"dashboard-header .dashboard-header__breadcrumb-link",
);
expect(breadcrumb).toBeInstanceOf(HTMLAnchorElement);
expect(breadcrumb?.getAttribute("href")).toBe("/ui/overview");
});
it("renders the dreaming view on the /dreaming route", async () => {
const app = mountApp("/dreaming");
app.dreamingStatus = {