Skip to content

Commit 1390ead

Browse files
authored
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
1 parent a2cf05c commit 1390ead

4 files changed

Lines changed: 82 additions & 9 deletions

File tree

ui/src/styles/layout.css

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,23 @@
160160
color: var(--muted);
161161
}
162162

163+
.topnav-shell .dashboard-header__breadcrumb-link {
164+
display: inline-flex;
165+
align-items: center;
166+
margin: -2px -4px;
167+
padding: 2px 4px;
168+
border-radius: var(--radius-sm);
169+
text-decoration: none;
170+
white-space: nowrap;
171+
}
172+
173+
.topnav-shell .dashboard-header__breadcrumb-link:hover,
174+
.topnav-shell .dashboard-header__breadcrumb-link:focus-visible {
175+
color: var(--text);
176+
text-decoration: underline;
177+
text-underline-offset: 3px;
178+
}
179+
163180
.topnav-shell .dashboard-header__breadcrumb-current {
164181
color: var(--accent);
165182
font-weight: 650;

ui/src/ui/app-render.ts

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,13 @@ import {
117117
import { buildExternalLinkRel, EXTERNAL_LINK_TARGET } from "./external-link.ts";
118118
import { icons } from "./icons.ts";
119119
import { createLazyView, renderLazyView } from "./lazy-view.ts";
120-
import { normalizeBasePath, TAB_GROUPS, subtitleForTab, titleForTab } from "./navigation.ts";
120+
import {
121+
normalizeBasePath,
122+
TAB_GROUPS,
123+
subtitleForTab,
124+
titleForTab,
125+
type Tab,
126+
} from "./navigation.ts";
121127
import { isPluginEnabledInConfigSnapshot } from "./plugin-activation.ts";
122128
import "./components/dashboard-header.ts";
123129
import {
@@ -1346,7 +1352,13 @@ export function renderApp(state: AppViewState) {
13461352
<span class="nav-collapse-toggle__icon" aria-hidden="true">${icons.menu}</span>
13471353
</button>
13481354
<div class="topnav-shell__content">
1349-
<dashboard-header .tab=${state.tab}></dashboard-header>
1355+
<dashboard-header
1356+
.tab=${state.tab}
1357+
.basePath=${state.basePath}
1358+
@navigate=${(event: CustomEvent<Tab>) => {
1359+
state.setTab(event.detail);
1360+
}}
1361+
></dashboard-header>
13501362
</div>
13511363
<div class="topnav-shell__actions">
13521364
<button

ui/src/ui/components/dashboard-header.ts

Lines changed: 23 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,45 @@
11
import { LitElement, html } from "lit";
22
import { property } from "lit/decorators.js";
3-
import { titleForTab, type Tab } from "../navigation.js";
3+
import { pathForTab, titleForTab, type Tab } from "../navigation.js";
44

55
export class DashboardHeader extends LitElement {
66
override createRenderRoot() {
77
return this;
88
}
99

1010
@property() tab: Tab = "overview";
11+
@property() basePath = "";
12+
13+
private handleOverviewClick(event: MouseEvent) {
14+
if (
15+
event.defaultPrevented ||
16+
event.button !== 0 ||
17+
event.metaKey ||
18+
event.ctrlKey ||
19+
event.shiftKey ||
20+
event.altKey
21+
) {
22+
return;
23+
}
24+
event.preventDefault();
25+
this.dispatchEvent(
26+
new CustomEvent("navigate", { detail: "overview", bubbles: true, composed: true }),
27+
);
28+
}
1129

1230
override render() {
1331
const label = titleForTab(this.tab);
1432

1533
return html`
1634
<div class="dashboard-header">
1735
<div class="dashboard-header__breadcrumb">
18-
<span
36+
<a
1937
class="dashboard-header__breadcrumb-link"
20-
@click=${() =>
21-
this.dispatchEvent(
22-
new CustomEvent("navigate", { detail: "overview", bubbles: true, composed: true }),
23-
)}
38+
href=${pathForTab("overview", this.basePath)}
39+
@click=${this.handleOverviewClick}
2440
>
2541
OpenClaw
26-
</span>
42+
</a>
2743
<span class="dashboard-header__breadcrumb-sep"></span>
2844
<span class="dashboard-header__breadcrumb-current">${label}</span>
2945
</div>

ui/src/ui/navigation.browser.test.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,34 @@ describe("control UI routing", () => {
4848
expect(dreamsLink).not.toBeNull();
4949
});
5050

51+
it("renders the dashboard breadcrumb as an overview link", async () => {
52+
const app = mountApp("/channels");
53+
await app.updateComplete;
54+
55+
const breadcrumb = app.querySelector<HTMLAnchorElement>(
56+
"dashboard-header .dashboard-header__breadcrumb-link",
57+
);
58+
expect(breadcrumb).toBeInstanceOf(HTMLAnchorElement);
59+
expect(breadcrumb?.getAttribute("href")).toBe("/overview");
60+
61+
breadcrumb?.dispatchEvent(new MouseEvent("click", { bubbles: true, cancelable: true }));
62+
await app.updateComplete;
63+
64+
expect(app.tab).toBe("overview");
65+
expect(window.location.pathname).toBe("/overview");
66+
});
67+
68+
it("keeps the dashboard breadcrumb link inside the configured base path", async () => {
69+
const app = mountApp("/ui/channels");
70+
await app.updateComplete;
71+
72+
const breadcrumb = app.querySelector<HTMLAnchorElement>(
73+
"dashboard-header .dashboard-header__breadcrumb-link",
74+
);
75+
expect(breadcrumb).toBeInstanceOf(HTMLAnchorElement);
76+
expect(breadcrumb?.getAttribute("href")).toBe("/ui/overview");
77+
});
78+
5179
it("renders the dreaming view on the /dreaming route", async () => {
5280
const app = mountApp("/dreaming");
5381
app.dreamingStatus = {

0 commit comments

Comments
 (0)