improve(ui): move shell controls into side rails

This commit is contained in:
vyctorbrzezowski 2026-07-06 13:39:23 -03:00 committed by Shakker
parent 64015e71dd
commit 8295a12a83
55 changed files with 755 additions and 548 deletions

View file

@ -15,7 +15,7 @@ import "../components/terminal/terminal-panel.ts";
import "../components/tooltip.ts";
import "../components/update-banner.ts";
import type { SidebarNavRoute } from "../app-navigation.ts";
import { APP_ROUTE_IDS, isRouteId, pathForRoute, type RouteId } from "../app-routes.ts";
import { APP_ROUTE_IDS, isRouteId, type RouteId } from "../app-routes.ts";
import {
COMMAND_PALETTE_TARGET_EVENT,
type CommandPalette,
@ -27,8 +27,6 @@ import { copyToClipboard } from "../lib/clipboard.ts";
import { isGatewayMethodAdvertised } from "../lib/gateway-methods.ts";
import { isWorkboardEnabledInConfigSnapshot } from "../lib/plugin-activation.ts";
import { searchForSession } from "../lib/sessions/index.ts";
import { resolveAgentIdFromSessionKey } from "../lib/sessions/session-key.ts";
import { normalizeLowercaseStringOrEmpty, normalizeOptionalString } from "../lib/string-coerce.ts";
import { renderDevicePairSetup } from "../pages/nodes/view-pairing.ts";
import { pluginTabKey, pluginTabRefFromSearch } from "../pages/plugin/route.ts";
import { bootstrapApplication, type ApplicationRuntime } from "./bootstrap.ts";
@ -70,18 +68,6 @@ function equalShellRouteState(previous: ShellRouteState, next: ShellRouteState):
);
}
function resolveAgentLabel(sessionKey: string, agentsList: AgentsListResult | null): string {
const agentId = resolveAgentIdFromSessionKey(sessionKey);
const agent = agentsList?.agents.find(
(entry) => normalizeLowercaseStringOrEmpty(entry.id) === agentId,
);
return (
normalizeOptionalString(agent?.identity?.name) ??
normalizeOptionalString(agent?.name) ??
agentId
);
}
function resolveOnboardingMode(): boolean {
const raw = new URLSearchParams(globalThis.location?.search ?? "").get("onboarding");
return raw !== null && /^(?:1|true|yes|on)$/iu.test(raw.trim());
@ -123,10 +109,14 @@ function isTerminalAvailable(
}
return (
hasOperatorAdminAccess(snapshot.hello?.auth ?? null) &&
isGatewayMethodAdvertised(snapshot, "terminal.open") === true
isGatewayMethodAdvertised(snapshot, "terminal.open")
);
}
function isMobileNavLayout(): boolean {
return globalThis.matchMedia?.("(max-width: 1100px)").matches ?? false;
}
class OpenClawApp extends LitElement {
@state() private gatewayConnected = false;
@state() private gatewayReconnecting = false;
@ -376,7 +366,6 @@ class OpenClawShell extends LitElement {
@state() private terminalAvailable = false;
@state() private terminalClient: GatewayBrowserClient | null = null;
@state() private activeSessionKey = "";
@state() private agentLabel = "";
@state() private routeState: ShellRouteState = {};
@state() private overlaySnapshot: ApplicationOverlaySnapshot = {
updateAvailable: null,
@ -396,7 +385,6 @@ class OpenClawShell extends LitElement {
private navDrawerTrigger: HTMLElement | null = null;
private agentsListClient: GatewayBrowserClient | null = null;
private sessionKeyClient: GatewayBrowserClient | null = null;
private stopAgentsSubscription: (() => void) | undefined;
private stopConfigSubscription: (() => void) | undefined;
private stopGatewaySubscription: (() => void) | undefined;
private stopNavigationSubscription: (() => void) | undefined;
@ -413,6 +401,7 @@ class OpenClawShell extends LitElement {
super.connectedCallback();
this.startSubscriptions();
this.addEventListener(COMMAND_PALETTE_TARGET_EVENT, this.handleCommandPaletteTarget);
document.addEventListener("keydown", this.handleDocumentKeydown);
}
override updated() {
@ -425,7 +414,6 @@ class OpenClawShell extends LitElement {
if (
!runtime ||
!context ||
this.stopAgentsSubscription ||
this.stopConfigSubscription ||
this.stopGatewaySubscription ||
this.stopNavigationSubscription ||
@ -443,13 +431,11 @@ class OpenClawShell extends LitElement {
this.updateGatewaySessionKey(context.gateway.snapshot);
this.updateGatewayStatus(context.gateway.snapshot);
this.updateTerminalSurface(context.gateway.snapshot);
this.updateAgentLabel();
this.ensureRuntimeConfig(context.gateway.snapshot);
this.stopGatewaySubscription = context.gateway.subscribe((snapshot) => {
this.updateGatewaySessionKey(snapshot);
this.updateGatewayStatus(snapshot);
this.updateTerminalSurface(snapshot);
this.updateAgentLabel();
this.ensureAgentsList(snapshot);
this.ensureRuntimeConfig(snapshot);
});
@ -457,9 +443,6 @@ class OpenClawShell extends LitElement {
this.updateTerminalSurface(context.gateway.snapshot);
});
this.stopThemeSubscription = context.theme.subscribe(() => this.requestUpdate());
this.stopAgentsSubscription = context.agents.subscribe(() => {
this.updateAgentLabel();
});
this.updateRouteState(selectShellRouteState(runtime.router.getState()));
this.stopRouteSubscription = runtime.router.subscribeSelector(
selectShellRouteState,
@ -480,8 +463,7 @@ class OpenClawShell extends LitElement {
override disconnectedCallback() {
this.removeEventListener(COMMAND_PALETTE_TARGET_EVENT, this.handleCommandPaletteTarget);
this.stopAgentsSubscription?.();
this.stopAgentsSubscription = undefined;
document.removeEventListener("keydown", this.handleDocumentKeydown);
this.stopConfigSubscription?.();
this.stopConfigSubscription = undefined;
this.stopGatewaySubscription?.();
@ -533,13 +515,23 @@ class OpenClawShell extends LitElement {
this.context?.replace("chat", this.chatNavigationOptions());
}
private toggleNavDrawer(trigger: HTMLElement) {
if (this.navDrawerOpen) {
this.closeNavDrawer({ restoreFocus: true });
private toggleNavigationSurface(trigger?: HTMLElement) {
const context = this.context;
if (!context || this.onboarding) {
return;
}
this.navDrawerTrigger = trigger;
this.navDrawerOpen = true;
if (isMobileNavLayout()) {
if (this.navDrawerOpen) {
this.closeNavDrawer({ restoreFocus: Boolean(trigger) });
return;
}
this.navDrawerTrigger = trigger ?? null;
this.navDrawerOpen = true;
return;
}
context.navigation.update({
navCollapsed: !this.navCollapsed,
});
}
private closeNavDrawer(options: { restoreFocus?: boolean } = {}) {
@ -564,6 +556,21 @@ class OpenClawShell extends LitElement {
this.closeNavDrawer({ restoreFocus: true });
};
private readonly handleDocumentKeydown = (event: KeyboardEvent) => {
if (
event.defaultPrevented ||
event.altKey ||
event.shiftKey ||
!event.metaKey ||
event.ctrlKey ||
event.key.toLowerCase() !== "b"
) {
return;
}
event.preventDefault();
this.toggleNavigationSurface();
};
private readonly openPalette = () => {
this.commandPalette?.openPalette();
};
@ -676,21 +683,9 @@ class OpenClawShell extends LitElement {
const sessionKey = new URLSearchParams(routeState.location?.search).get("session")?.trim();
if (sessionKey) {
this.activeSessionKey = sessionKey;
this.updateAgentLabel();
}
}
private updateAgentLabel() {
const context = this.context;
if (!context) {
return;
}
this.agentLabel = resolveAgentLabel(
this.activeSessionKey || context.gateway.snapshot.sessionKey,
context.agents.state.agentsList,
);
}
private readonly updateNavigationPreferences = (
snapshot: ApplicationRuntime["context"]["navigation"]["snapshot"],
) => {
@ -740,23 +735,12 @@ class OpenClawShell extends LitElement {
@click=${() => this.closeNavDrawer({ restoreFocus: true })}
></button>
<openclaw-app-topbar
.routeId=${activeRoute}
.basePath=${context.basePath}
.agentLabel=${this.agentLabel}
.overviewHref=${pathForRoute("overview", context.basePath)}
.searchDisabled=${false}
.navDrawerOpen=${navDrawerOpen}
.navCollapsed=${navCollapsed}
.onboarding=${this.onboarding}
.terminalAvailable=${this.terminalAvailable}
.onToggleTerminal=${() =>
window.dispatchEvent(new CustomEvent("openclaw:terminal-toggle"))}
.onToggleDrawer=${(trigger: HTMLElement) => this.toggleNavDrawer(trigger)}
.onToggleCollapse=${() =>
context.navigation.update({
navCollapsed: !navCollapsed,
})}
.onNavigate=${(routeId: string, options?: ApplicationNavigationOptions) =>
this.navigate(routeId, options)}
.onOpenPalette=${this.openPalette}
.onToggleDrawer=${(trigger: HTMLElement) => this.toggleNavigationSurface(trigger)}
></openclaw-app-topbar>
<div class="shell-nav">
<openclaw-app-sidebar
@ -773,6 +757,7 @@ class OpenClawShell extends LitElement {
.sidebarMoreExpanded=${this.sidebarMoreExpanded}
.themeMode=${context.theme.mode}
.onOpenPalette=${this.openPalette}
.onToggleSidebar=${() => this.toggleNavigationSurface()}
.onToggleMore=${() =>
context.navigation.update({
sidebarMoreExpanded: !context.navigation.snapshot.sidebarMoreExpanded,

View file

@ -70,6 +70,7 @@ type SidebarRecentSession = {
meta: string;
href: string;
active: boolean;
visuallyActive: boolean;
hasActiveRun: boolean;
kind?: string;
pinned: boolean;
@ -113,6 +114,14 @@ const SIDEBAR_SESSION_SORT_OPTIONS = [
labelKey: "chat.sidebar.sortCreated" | "chat.sidebar.sortUpdated";
}>;
function formatSidebarTimestamp(timestampMs: number | null | undefined): string {
const value = formatRelativeTimestamp(timestampMs, { fallback: "" });
if (value === "just now") {
return "now";
}
return value.endsWith(" ago") ? value.slice(0, -" ago".length) : value;
}
function shouldHandleNavigationClick(event: MouseEvent): boolean {
return (
!event.defaultPrevented &&
@ -142,6 +151,7 @@ class AppSidebar extends LitElement {
@property({ attribute: false }) sidebarMoreExpanded = false;
@property({ attribute: false }) themeMode: ThemeMode = "system";
@property({ attribute: false }) onOpenPalette?: () => void;
@property({ attribute: false }) onToggleSidebar?: () => void;
@property({ attribute: false }) onToggleMore?: () => void;
@property({ attribute: false }) onUpdatePinnedRoutes?: (routes: SidebarNavRoute[]) => void;
@property({ attribute: false }) onPairMobile?: () => void;
@ -278,6 +288,37 @@ class AppSidebar extends LitElement {
this.gatewayClient = client;
}
private renderBrand() {
const collapseLabel = this.collapsed ? t("nav.expand") : t("nav.collapse");
const collapseTooltip = `${collapseLabel} (⌘B)`;
return html`
<div class="sidebar-brand">
<div class="sidebar-brand__identity">
<img
class="sidebar-brand__logo"
src=${controlUiPublicAssetPath("apple-touch-icon.png", this.basePath)}
alt=""
aria-hidden="true"
/>
${this.collapsed ? nothing : html`<span class="sidebar-brand__title">OpenClaw</span>`}
</div>
<div class="sidebar-brand__actions">
<openclaw-tooltip .content=${collapseTooltip}>
<button
class="sidebar-brand__icon"
type="button"
@click=${() => this.onToggleSidebar?.()}
aria-label=${collapseLabel}
aria-expanded=${String(!this.collapsed)}
>
${this.collapsed ? icons.panelLeftOpen : icons.panelLeftClose}
</button>
</openclaw-tooltip>
</div>
</div>
`;
}
private getRouteSessionKey(): string {
return this.sessionKey.trim() || this.context?.gateway.snapshot.sessionKey.trim() || "";
}
@ -321,12 +362,14 @@ class AppSidebar extends LitElement {
hello: context?.gateway.snapshot.hello,
compareSessions: this.compareSidebarSessionRows,
});
const highlightCurrentSession = this.activeRouteId === "chat";
const toSidebarSession = (row: SessionsListResult["sessions"][number]) => ({
key: row.key,
label: resolveSessionDisplayName(row.key, row),
meta: row.updatedAt ? formatRelativeTimestamp(row.updatedAt) : "",
meta: formatSidebarTimestamp(row.updatedAt),
href: `${pathForRoute("chat", context?.basePath ?? "")}${searchForSession(row.key)}`,
active: row.key === navigation.activeRowKey,
visuallyActive: highlightCurrentSession && row.key === navigation.currentSessionKey,
hasActiveRun: Boolean(row.hasActiveRun),
kind: row.kind,
pinned: row.pinned === true,
@ -1211,7 +1254,7 @@ class AppSidebar extends LitElement {
const rowClass = [
"sidebar-recent-session",
"session-row-host",
session.active ? "sidebar-recent-session--active" : "",
session.visuallyActive ? "sidebar-recent-session--active" : "",
session.pinned ? "session-row-host--pinned" : "",
session.hasActiveRun ? "session-row-host--running" : "",
this.draggingSessionKey === session.key ? "sidebar-recent-session--dragging" : "",
@ -1454,7 +1497,7 @@ class AppSidebar extends LitElement {
@click=${(event: MouseEvent) => {
const trigger = event.currentTarget as HTMLElement;
const rect = trigger.getBoundingClientRect();
this.openSessionSortMenu(rect.right - 180, rect.bottom + 4, trigger);
this.openSessionSortMenu(rect.right, rect.bottom + 4, trigger);
}}
>
${icons.listFilter}
@ -1621,6 +1664,7 @@ class AppSidebar extends LitElement {
alt="OpenClaw"
/>
<div class="sidebar-shell">
${this.renderBrand()}
<div class="sidebar-shell__body">
${this.renderSearch()}
<nav class="sidebar-nav" @contextmenu=${this.openCustomizeMenuFromContext}>
@ -1667,9 +1711,7 @@ class AppSidebar extends LitElement {
${icons.settings}
</a>
</openclaw-tooltip>
<openclaw-tooltip
.content=${t("chat.docsOpensInNewTab", { label: t("common.docs") })}
>
<openclaw-tooltip .content=${t("chat.docsTooltip")}>
<a
class="sidebar-footer-icon"
href="https://docs.openclaw.ai"

View file

@ -1,87 +1,29 @@
import { LitElement, html, nothing } from "lit";
import { property } from "lit/decorators.js";
import type { NavigationRouteId } from "../app-navigation.ts";
import "./dashboard-header.ts";
import "./tooltip.ts";
import { controlUiPublicAssetPath } from "../app/public-assets.ts";
import { t } from "../i18n/index.ts";
import { icons } from "./icons.ts";
// Mirrors the layout.mobile.css breakpoint where the sidebar becomes a
// slide-over drawer; the one topbar toggle switches behavior there.
const NAV_DRAWER_MEDIA_QUERY = "(max-width: 1100px)";
import "./tooltip.ts";
class AppTopbar extends LitElement {
override createRenderRoot() {
return this;
}
@property({ attribute: false }) routeId?: NavigationRouteId;
@property({ attribute: false }) basePath = "";
@property({ attribute: false }) agentLabel = "";
@property({ attribute: false }) navDrawerOpen = false;
@property({ attribute: false }) navCollapsed = false;
@property({ attribute: false }) onboarding = false;
@property({ attribute: false }) basePath = "";
@property({ attribute: false }) onToggleDrawer?: (trigger: HTMLElement) => void;
@property({ attribute: false }) onToggleCollapse?: () => void;
@property({ attribute: false }) onToggleTerminal?: () => void;
@property({ attribute: false }) onNavigate?: (routeId: NavigationRouteId) => void;
@property({ attribute: false }) overviewHref = "";
@property({ attribute: false }) terminalAvailable = false;
private drawerMedia?: MediaQueryList;
private drawerMediaCleanup?: () => void;
@property({ attribute: false }) onOpenPalette?: () => void;
@property({ attribute: false }) searchDisabled = false;
override connectedCallback() {
super.connectedCallback();
this.style.display = "contents";
if (typeof globalThis.matchMedia === "function") {
const media = globalThis.matchMedia(NAV_DRAWER_MEDIA_QUERY);
this.drawerMedia = media;
// Older WebViews expose only the legacy addListener API (see the same
// pattern in app/bootstrap.ts).
if (typeof media.addEventListener === "function") {
media.addEventListener("change", this.handleDrawerMediaChange);
this.drawerMediaCleanup = () =>
media.removeEventListener("change", this.handleDrawerMediaChange);
} else if (typeof media.addListener === "function") {
media.addListener(this.handleDrawerMediaChange);
this.drawerMediaCleanup = () => media.removeListener(this.handleDrawerMediaChange);
}
}
}
override disconnectedCallback() {
this.drawerMediaCleanup?.();
this.drawerMediaCleanup = undefined;
this.drawerMedia = undefined;
super.disconnectedCallback();
}
private readonly handleDrawerMediaChange = () => {
this.requestUpdate();
};
private get drawerMode(): boolean {
return this.drawerMedia?.matches ?? false;
}
private readonly handleNavigate = (event: CustomEvent<NavigationRouteId>) => {
this.onNavigate?.(event.detail);
};
private readonly handleToggleSidebar = (event: MouseEvent) => {
if (this.drawerMode) {
this.onToggleDrawer?.(event.currentTarget as HTMLElement);
} else {
this.onToggleCollapse?.();
}
};
override render() {
// One toggle, viewport-dependent behavior: it drives the persistent rail
// collapse on desktop and the slide-over drawer at drawer breakpoints.
const sidebarOpen = this.drawerMode ? this.navDrawerOpen : !this.navCollapsed;
const toggleLabel = sidebarOpen ? t("nav.collapse") : t("nav.expand");
const drawerLabel = this.navDrawerOpen ? t("nav.collapse") : t("nav.expand");
return html`
<header
class="topbar"
@ -89,41 +31,40 @@ class AppTopbar extends LitElement {
aria-hidden=${this.onboarding ? "true" : nothing}
>
<div class="topnav-shell">
<openclaw-tooltip .content=${toggleLabel}>
<openclaw-tooltip .content=${drawerLabel}>
<button
type="button"
class="topbar-icon-btn topbar-nav-toggle"
@click=${this.handleToggleSidebar}
aria-label=${toggleLabel}
aria-expanded=${String(sidebarOpen)}
class="sidebar-menu-trigger topbar-nav-toggle"
@click=${(event: MouseEvent) =>
this.onToggleDrawer?.(event.currentTarget as HTMLElement)}
aria-label=${drawerLabel}
aria-expanded=${String(this.navDrawerOpen)}
>
${icons.panelLeft}
<span class="nav-collapse-toggle__icon" aria-hidden="true">${icons.menu}</span>
</button>
</openclaw-tooltip>
<div class="topnav-shell__content">
<dashboard-header
.routeId=${this.routeId}
.basePath=${this.basePath}
.agentLabel=${this.agentLabel}
.overviewHref=${this.overviewHref}
@navigate=${this.handleNavigate}
></dashboard-header>
<div class="topbar-brand" aria-label="OpenClaw">
<img
class="topbar-brand__logo"
src=${controlUiPublicAssetPath("apple-touch-icon.png", this.basePath)}
alt=""
aria-hidden="true"
/>
<span class="topbar-brand__title">OpenClaw</span>
</div>
</div>
<div class="topnav-shell__actions">
${this.terminalAvailable
? html`
<openclaw-tooltip .content=${t("terminal.toggle")}>
<button
class="topbar-icon-btn"
type="button"
@click=${() => this.onToggleTerminal?.()}
aria-label=${t("terminal.toggle")}
>
${icons.terminal}
</button>
</openclaw-tooltip>
`
: nothing}
<openclaw-tooltip .content=${t("chat.commandPaletteTitle")}>
<button
class="topbar-search"
?disabled=${this.searchDisabled || !this.onOpenPalette}
@click=${() => this.onOpenPalette?.()}
aria-label=${t("chat.openCommandPalette")}
>
${icons.search}
</button>
</openclaw-tooltip>
</div>
</div>
</header>

View file

@ -1,76 +0,0 @@
// Control UI component implements the dashboard header element.
import { LitElement, html, nothing } from "lit";
import { property } from "lit/decorators.js";
import { titleForRoute, type NavigationRouteId } from "../app-navigation.ts";
class DashboardHeader extends LitElement {
override createRenderRoot() {
return this;
}
@property() routeId?: NavigationRouteId;
@property() basePath = "";
@property() agentLabel = "";
@property() overviewHref = "";
private readonly 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 = this.routeId ? titleForRoute(this.routeId) : "";
const rawAgentLabel = this.agentLabel.trim();
// Skip the agent crumb when it repeats the brand crumb ("OpenClaw OpenClaw …").
const agentLabel = rawAgentLabel.toLowerCase() === "openclaw" ? "" : rawAgentLabel;
return html`
<div class="dashboard-header">
<div class="dashboard-header__breadcrumb">
${this.overviewHref
? html`
<a
class="dashboard-header__breadcrumb-link"
href=${this.overviewHref}
@click=${this.handleOverviewClick}
>
OpenClaw
</a>
`
: html`<span class="dashboard-header__breadcrumb-link">OpenClaw</span>`}
${agentLabel
? html`
<span class="dashboard-header__breadcrumb-segment">
<span class="dashboard-header__breadcrumb-sep"></span>
<span class="dashboard-header__breadcrumb-context" title=${agentLabel}>
${agentLabel}
</span>
</span>
`
: nothing}
<span class="dashboard-header__breadcrumb-sep"></span>
<span class="dashboard-header__breadcrumb-current">${label}</span>
</div>
<div class="dashboard-header__actions">
<slot></slot>
</div>
</div>
`;
}
}
if (!customElements.get("dashboard-header")) {
customElements.define("dashboard-header", DashboardHeader);
}

View file

@ -20,6 +20,10 @@ const DOCK_BOTTOM_GLYPH = svg`<svg viewBox="0 0 16 16" width="13" height="13" fi
const DOCK_RIGHT_GLYPH = svg`<svg viewBox="0 0 16 16" width="13" height="13" fill="none" stroke="currentColor" stroke-width="1.3"><rect x="2" y="2.5" width="12" height="11" rx="1.5" /><path d="M10 2.5v11" /></svg>`;
type TerminalDock = "bottom" | "right";
type TerminalToggleDetail = {
dock?: TerminalDock;
open?: boolean;
};
type PanelLayout = {
open: boolean;
@ -148,7 +152,7 @@ export class OpenClawTerminalPanel extends LitElement {
private connection: TerminalConnection | null = null;
private tabSeq = 0;
private readonly onGlobalKeyDown = (event: KeyboardEvent) => this.handleGlobalKey(event);
private readonly onToggleRequest = () => this.toggle();
private readonly onToggleRequest = (event: Event) => this.handleToggleRequest(event);
// Re-clamp a dock sized on a larger window so the header/resizer never end
// up off-screen after the viewport shrinks (e.g. rotate, window resize).
private readonly onViewportResize = () => {
@ -275,13 +279,37 @@ export class OpenClawTerminalPanel extends LitElement {
this.closePanel();
} else {
this.open = true;
this.syncLayoutReservation();
this.persistLayout();
void this.restoreSessions();
}
}
private handleToggleRequest(event: Event): void {
const detail =
event instanceof CustomEvent && typeof event.detail === "object" && event.detail !== null
? (event.detail as TerminalToggleDetail)
: null;
const dock = detail?.dock === "right" || detail?.dock === "bottom" ? detail.dock : null;
if (dock) {
this.dock = dock;
}
if (detail?.open === true) {
if (!this.available) {
return;
}
this.open = true;
this.syncLayoutReservation();
this.persistLayout();
void this.restoreSessions();
return;
}
this.toggle();
}
private closePanel(): void {
this.open = false;
this.syncLayoutReservation();
this.persistLayout();
}
@ -599,6 +627,7 @@ export class OpenClawTerminalPanel extends LitElement {
private setDock(dock: TerminalDock): void {
this.dock = dock;
this.syncLayoutReservation();
this.persistLayout();
void this.updateComplete.then(() => {
for (const tab of this.tabs) {

View file

@ -22,13 +22,21 @@ class ThemeModeToggle extends LitElement {
this.style.display = "contents";
}
private readonly handleModeChange = (mode: ThemeMode, event: Event) => {
if (mode === this.mode) {
return;
private readonly nextMode = (): ThemeMode => {
switch (this.mode) {
case "system":
return "light";
case "light":
return "dark";
case "dark":
return "system";
}
};
private readonly handleModeChange = (event: Event) => {
this.dispatchEvent(
new CustomEvent<ThemeModeChangeDetail>("theme-change", {
detail: { mode, element: event.currentTarget as HTMLElement },
detail: { mode: this.nextMode(), element: event.currentTarget as HTMLElement },
bubbles: true,
composed: true,
}),
@ -36,38 +44,26 @@ class ThemeModeToggle extends LitElement {
};
override render() {
const options: Array<{ id: ThemeMode; labelKey: string }> = [
{ id: "system", labelKey: "common.system" },
{ id: "light", labelKey: "common.light" },
{ id: "dark", labelKey: "common.dark" },
];
const labelKey =
this.mode === "system"
? "common.system"
: this.mode === "light"
? "common.light"
: "common.dark";
const label = t(labelKey);
const tooltip = t("common.colorModeOption", { mode: label });
return html`
<div class="theme-mode-toggle" role="group" aria-label=${t("common.colorMode")}>
${options.map((option) => {
const label = t(option.labelKey);
const tooltip = t("common.colorModeOption", { mode: label });
return html`
<openclaw-tooltip .content=${tooltip}>
<button
type="button"
class="theme-mode-toggle__btn ${option.id === this.mode
? "theme-mode-toggle__btn--active"
: ""}"
aria-label=${tooltip}
aria-pressed=${option.id === this.mode}
@click=${(event: Event) => this.handleModeChange(option.id, event)}
>
${option.id === "system"
? icons.monitor
: option.id === "light"
? icons.sun
: icons.moon}
</button>
</openclaw-tooltip>
`;
})}
</div>
<openclaw-tooltip .content=${tooltip}>
<button
type="button"
class="theme-mode-toggle"
aria-label=${tooltip}
@click=${this.handleModeChange}
>
${this.mode === "system" ? icons.monitor : this.mode === "light" ? icons.sun : icons.moon}
</button>
</openclaw-tooltip>
`;
}
}

View file

@ -70,7 +70,7 @@ describeControlUiE2e("Control UI sidebar customization mocked Gateway E2E", () =
const sidebar = page.locator("openclaw-app-sidebar");
const pinnedItems = sidebar.locator(".sidebar-nav > .nav-section__items > .nav-item");
await expect.poll(() => trimmedTextContents(pinnedItems)).toEqual(["Overview"]);
await expect.poll(() => sidebar.locator(".sidebar-brand").count()).toBe(0);
await expect.poll(() => sidebar.locator(".sidebar-brand").count()).toBe(1);
const settingsLink = sidebar.getByRole("link", { name: "Settings" });
await expect.poll(() => settingsLink.isVisible()).toBe(true);
await settingsLink.click();
@ -101,9 +101,9 @@ describeControlUiE2e("Control UI sidebar customization mocked Gateway E2E", () =
)
.not.toContain("Workboard");
const customizeButton = sidebar.getByRole("button", { name: "Customize sidebar" });
const customizeButton = sidebar.getByRole("button", { name: "Edit pinned items" });
await customizeButton.click();
const menu = sidebar.getByRole("menu", { name: "Customize sidebar" });
const menu = sidebar.getByRole("menu", { name: "Edit pinned items" });
await expect
.poll(() => trimmedTextContents(menu.getByRole("menuitemcheckbox")))
.not.toContain("Workboard");
@ -133,7 +133,7 @@ describeControlUiE2e("Control UI sidebar customization mocked Gateway E2E", () =
await captureUiProof(page, "03-persisted-customization.png");
await customizeButton.click();
await menu.getByRole("menuitem", { name: "Reset to defaults" }).click();
await menu.getByRole("menuitem", { name: "Reset pinned items" }).click();
await expect.poll(() => trimmedTextContents(pinnedItems)).toEqual(["Overview"]);
// The sidebar search field is the command palette entry point.
@ -144,10 +144,12 @@ describeControlUiE2e("Control UI sidebar customization mocked Gateway E2E", () =
await page.keyboard.press("Escape");
await expect.poll(() => paletteInput.isVisible()).toBe(false);
// The sidebar toggle lives in the topbar, macOS style.
// The sidebar toggle lives in the sidebar brand row on desktop.
const collapseButton = page.getByRole("button", { name: "Collapse sidebar" });
await expect
.poll(() => collapseButton.evaluate((element) => Boolean(element.closest(".topbar"))))
.poll(() =>
collapseButton.evaluate((element) => Boolean(element.closest(".sidebar-brand"))),
)
.toBe(true);
await collapseButton.click();
await expect

View file

@ -1,11 +1,13 @@
{
"fallbackKeys": [],
"generatedAt": "2026-07-07T08:47:32.656Z",
"fallbackKeys": [
"chat.docsTooltip"
],
"generatedAt": "2026-07-07T14:06:13.827Z",
"locale": "ar",
"model": "gpt-5.5",
"provider": "openai",
"sourceHash": "d012bf7dea64cb5e4858358eb65d806374d86512260f7e16949b761b8bc0627b",
"sourceHash": "6bda04d5bd3fed6bddc012598826e7d0c785534d870fc78820873063c896606c",
"totalKeys": 1669,
"translatedKeys": 1669,
"translatedKeys": 1668,
"workflow": 1
}

View file

@ -1,11 +1,13 @@
{
"fallbackKeys": [],
"generatedAt": "2026-07-07T08:47:24.981Z",
"fallbackKeys": [
"chat.docsTooltip"
],
"generatedAt": "2026-07-07T14:06:13.071Z",
"locale": "de",
"model": "gpt-5.5",
"provider": "openai",
"sourceHash": "d012bf7dea64cb5e4858358eb65d806374d86512260f7e16949b761b8bc0627b",
"sourceHash": "6bda04d5bd3fed6bddc012598826e7d0c785534d870fc78820873063c896606c",
"totalKeys": 1669,
"translatedKeys": 1669,
"translatedKeys": 1668,
"workflow": 1
}

View file

@ -1,11 +1,13 @@
{
"fallbackKeys": [],
"generatedAt": "2026-07-07T08:47:26.317Z",
"fallbackKeys": [
"chat.docsTooltip"
],
"generatedAt": "2026-07-07T14:06:13.195Z",
"locale": "es",
"model": "gpt-5.5",
"provider": "openai",
"sourceHash": "d012bf7dea64cb5e4858358eb65d806374d86512260f7e16949b761b8bc0627b",
"sourceHash": "6bda04d5bd3fed6bddc012598826e7d0c785534d870fc78820873063c896606c",
"totalKeys": 1669,
"translatedKeys": 1669,
"translatedKeys": 1668,
"workflow": 1
}

View file

@ -1,11 +1,13 @@
{
"fallbackKeys": [],
"generatedAt": "2026-07-07T08:47:43.221Z",
"fallbackKeys": [
"chat.docsTooltip"
],
"generatedAt": "2026-07-07T14:06:14.942Z",
"locale": "fa",
"model": "gpt-5.5",
"provider": "openai",
"sourceHash": "d012bf7dea64cb5e4858358eb65d806374d86512260f7e16949b761b8bc0627b",
"sourceHash": "6bda04d5bd3fed6bddc012598826e7d0c785534d870fc78820873063c896606c",
"totalKeys": 1669,
"translatedKeys": 1669,
"translatedKeys": 1668,
"workflow": 1
}

View file

@ -1,11 +1,13 @@
{
"fallbackKeys": [],
"generatedAt": "2026-07-07T08:47:30.068Z",
"fallbackKeys": [
"chat.docsTooltip"
],
"generatedAt": "2026-07-07T14:06:13.564Z",
"locale": "fr",
"model": "gpt-5.5",
"provider": "openai",
"sourceHash": "d012bf7dea64cb5e4858358eb65d806374d86512260f7e16949b761b8bc0627b",
"sourceHash": "6bda04d5bd3fed6bddc012598826e7d0c785534d870fc78820873063c896606c",
"totalKeys": 1669,
"translatedKeys": 1669,
"translatedKeys": 1668,
"workflow": 1
}

View file

@ -1,11 +1,13 @@
{
"fallbackKeys": [],
"generatedAt": "2026-07-07T08:47:31.678Z",
"fallbackKeys": [
"chat.docsTooltip"
],
"generatedAt": "2026-07-07T14:06:13.695Z",
"locale": "hi",
"model": "gpt-5.5",
"provider": "openai",
"sourceHash": "d012bf7dea64cb5e4858358eb65d806374d86512260f7e16949b761b8bc0627b",
"sourceHash": "6bda04d5bd3fed6bddc012598826e7d0c785534d870fc78820873063c896606c",
"totalKeys": 1669,
"translatedKeys": 1669,
"translatedKeys": 1668,
"workflow": 1
}

View file

@ -1,11 +1,13 @@
{
"fallbackKeys": [],
"generatedAt": "2026-07-07T08:47:37.327Z",
"fallbackKeys": [
"chat.docsTooltip"
],
"generatedAt": "2026-07-07T14:06:14.328Z",
"locale": "id",
"model": "gpt-5.5",
"provider": "openai",
"sourceHash": "d012bf7dea64cb5e4858358eb65d806374d86512260f7e16949b761b8bc0627b",
"sourceHash": "6bda04d5bd3fed6bddc012598826e7d0c785534d870fc78820873063c896606c",
"totalKeys": 1669,
"translatedKeys": 1669,
"translatedKeys": 1668,
"workflow": 1
}

View file

@ -1,11 +1,13 @@
{
"fallbackKeys": [],
"generatedAt": "2026-07-07T08:47:33.970Z",
"fallbackKeys": [
"chat.docsTooltip"
],
"generatedAt": "2026-07-07T14:06:13.952Z",
"locale": "it",
"model": "gpt-5.5",
"provider": "openai",
"sourceHash": "d012bf7dea64cb5e4858358eb65d806374d86512260f7e16949b761b8bc0627b",
"sourceHash": "6bda04d5bd3fed6bddc012598826e7d0c785534d870fc78820873063c896606c",
"totalKeys": 1669,
"translatedKeys": 1669,
"translatedKeys": 1668,
"workflow": 1
}

View file

@ -1,11 +1,13 @@
{
"fallbackKeys": [],
"generatedAt": "2026-07-07T08:47:27.648Z",
"fallbackKeys": [
"chat.docsTooltip"
],
"generatedAt": "2026-07-07T14:06:13.321Z",
"locale": "ja-JP",
"model": "gpt-5.5",
"provider": "openai",
"sourceHash": "d012bf7dea64cb5e4858358eb65d806374d86512260f7e16949b761b8bc0627b",
"sourceHash": "6bda04d5bd3fed6bddc012598826e7d0c785534d870fc78820873063c896606c",
"totalKeys": 1669,
"translatedKeys": 1669,
"translatedKeys": 1668,
"workflow": 1
}

View file

@ -1,11 +1,13 @@
{
"fallbackKeys": [],
"generatedAt": "2026-07-07T08:47:29.117Z",
"fallbackKeys": [
"chat.docsTooltip"
],
"generatedAt": "2026-07-07T14:06:13.440Z",
"locale": "ko",
"model": "gpt-5.5",
"provider": "openai",
"sourceHash": "d012bf7dea64cb5e4858358eb65d806374d86512260f7e16949b761b8bc0627b",
"sourceHash": "6bda04d5bd3fed6bddc012598826e7d0c785534d870fc78820873063c896606c",
"totalKeys": 1669,
"translatedKeys": 1669,
"translatedKeys": 1668,
"workflow": 1
}

View file

@ -1,11 +1,13 @@
{
"fallbackKeys": [],
"generatedAt": "2026-07-07T08:47:41.995Z",
"fallbackKeys": [
"chat.docsTooltip"
],
"generatedAt": "2026-07-07T14:06:14.823Z",
"locale": "nl",
"model": "gpt-5.5",
"provider": "openai",
"sourceHash": "d012bf7dea64cb5e4858358eb65d806374d86512260f7e16949b761b8bc0627b",
"sourceHash": "6bda04d5bd3fed6bddc012598826e7d0c785534d870fc78820873063c896606c",
"totalKeys": 1669,
"translatedKeys": 1669,
"translatedKeys": 1668,
"workflow": 1
}

View file

@ -1,11 +1,13 @@
{
"fallbackKeys": [],
"generatedAt": "2026-07-07T08:47:38.593Z",
"fallbackKeys": [
"chat.docsTooltip"
],
"generatedAt": "2026-07-07T14:06:14.446Z",
"locale": "pl",
"model": "gpt-5.5",
"provider": "openai",
"sourceHash": "d012bf7dea64cb5e4858358eb65d806374d86512260f7e16949b761b8bc0627b",
"sourceHash": "6bda04d5bd3fed6bddc012598826e7d0c785534d870fc78820873063c896606c",
"totalKeys": 1669,
"translatedKeys": 1669,
"translatedKeys": 1668,
"workflow": 1
}

View file

@ -1,11 +1,13 @@
{
"fallbackKeys": [],
"generatedAt": "2026-07-07T08:47:23.799Z",
"fallbackKeys": [
"chat.docsTooltip"
],
"generatedAt": "2026-07-07T14:06:12.950Z",
"locale": "pt-BR",
"model": "gpt-5.5",
"provider": "openai",
"sourceHash": "d012bf7dea64cb5e4858358eb65d806374d86512260f7e16949b761b8bc0627b",
"sourceHash": "6bda04d5bd3fed6bddc012598826e7d0c785534d870fc78820873063c896606c",
"totalKeys": 1669,
"translatedKeys": 1669,
"translatedKeys": 1668,
"workflow": 1
}

View file

@ -8,6 +8,27 @@
"path": "ui/src/app/app-host.ts",
"text": "Close navigation"
},
{
"count": 1,
"kind": "html-text",
"name": "text",
"path": "ui/src/components/app-sidebar.ts",
"text": "OpenClaw"
},
{
"count": 1,
"kind": "html-attribute",
"name": "aria-label",
"path": "ui/src/components/app-topbar.ts",
"text": "OpenClaw"
},
{
"count": 1,
"kind": "html-text",
"name": "text",
"path": "ui/src/components/app-topbar.ts",
"text": "OpenClaw"
},
{
"count": 1,
"kind": "html-text",
@ -575,13 +596,6 @@
"path": "ui/src/components/config-form.render.ts",
"text": "Web"
},
{
"count": 2,
"kind": "html-text",
"name": "text",
"path": "ui/src/components/dashboard-header.ts",
"text": "OpenClaw"
},
{
"count": 1,
"kind": "html-text",

View file

@ -1,11 +1,13 @@
{
"fallbackKeys": [],
"generatedAt": "2026-07-07T08:47:46.145Z",
"fallbackKeys": [
"chat.docsTooltip"
],
"generatedAt": "2026-07-07T14:06:15.074Z",
"locale": "ru",
"model": "gpt-5.5",
"provider": "openai",
"sourceHash": "d012bf7dea64cb5e4858358eb65d806374d86512260f7e16949b761b8bc0627b",
"sourceHash": "6bda04d5bd3fed6bddc012598826e7d0c785534d870fc78820873063c896606c",
"totalKeys": 1669,
"translatedKeys": 1669,
"translatedKeys": 1668,
"workflow": 1
}

View file

@ -1,11 +1,13 @@
{
"fallbackKeys": [],
"generatedAt": "2026-07-07T08:47:39.502Z",
"fallbackKeys": [
"chat.docsTooltip"
],
"generatedAt": "2026-07-07T14:06:14.574Z",
"locale": "th",
"model": "gpt-5.5",
"provider": "openai",
"sourceHash": "d012bf7dea64cb5e4858358eb65d806374d86512260f7e16949b761b8bc0627b",
"sourceHash": "6bda04d5bd3fed6bddc012598826e7d0c785534d870fc78820873063c896606c",
"totalKeys": 1669,
"translatedKeys": 1669,
"translatedKeys": 1668,
"workflow": 1
}

View file

@ -1,11 +1,13 @@
{
"fallbackKeys": [],
"generatedAt": "2026-07-07T08:47:34.970Z",
"fallbackKeys": [
"chat.docsTooltip"
],
"generatedAt": "2026-07-07T14:06:14.078Z",
"locale": "tr",
"model": "gpt-5.5",
"provider": "openai",
"sourceHash": "d012bf7dea64cb5e4858358eb65d806374d86512260f7e16949b761b8bc0627b",
"sourceHash": "6bda04d5bd3fed6bddc012598826e7d0c785534d870fc78820873063c896606c",
"totalKeys": 1669,
"translatedKeys": 1669,
"translatedKeys": 1668,
"workflow": 1
}

View file

@ -1,11 +1,13 @@
{
"fallbackKeys": [],
"generatedAt": "2026-07-07T08:47:35.919Z",
"fallbackKeys": [
"chat.docsTooltip"
],
"generatedAt": "2026-07-07T14:06:14.198Z",
"locale": "uk",
"model": "gpt-5.5",
"provider": "openai",
"sourceHash": "d012bf7dea64cb5e4858358eb65d806374d86512260f7e16949b761b8bc0627b",
"sourceHash": "6bda04d5bd3fed6bddc012598826e7d0c785534d870fc78820873063c896606c",
"totalKeys": 1669,
"translatedKeys": 1669,
"translatedKeys": 1668,
"workflow": 1
}

View file

@ -1,11 +1,13 @@
{
"fallbackKeys": [],
"generatedAt": "2026-07-07T08:47:40.747Z",
"fallbackKeys": [
"chat.docsTooltip"
],
"generatedAt": "2026-07-07T14:06:14.699Z",
"locale": "vi",
"model": "gpt-5.5",
"provider": "openai",
"sourceHash": "d012bf7dea64cb5e4858358eb65d806374d86512260f7e16949b761b8bc0627b",
"sourceHash": "6bda04d5bd3fed6bddc012598826e7d0c785534d870fc78820873063c896606c",
"totalKeys": 1669,
"translatedKeys": 1669,
"translatedKeys": 1668,
"workflow": 1
}

View file

@ -1,11 +1,13 @@
{
"fallbackKeys": [],
"generatedAt": "2026-07-07T08:47:21.379Z",
"fallbackKeys": [
"chat.docsTooltip"
],
"generatedAt": "2026-07-07T14:06:12.672Z",
"locale": "zh-CN",
"model": "gpt-5.5",
"provider": "openai",
"sourceHash": "d012bf7dea64cb5e4858358eb65d806374d86512260f7e16949b761b8bc0627b",
"sourceHash": "6bda04d5bd3fed6bddc012598826e7d0c785534d870fc78820873063c896606c",
"totalKeys": 1669,
"translatedKeys": 1669,
"translatedKeys": 1668,
"workflow": 1
}

View file

@ -1,11 +1,13 @@
{
"fallbackKeys": [],
"generatedAt": "2026-07-07T08:47:22.571Z",
"fallbackKeys": [
"chat.docsTooltip"
],
"generatedAt": "2026-07-07T14:06:12.828Z",
"locale": "zh-TW",
"model": "gpt-5.5",
"provider": "openai",
"sourceHash": "d012bf7dea64cb5e4858358eb65d806374d86512260f7e16949b761b8bc0627b",
"sourceHash": "6bda04d5bd3fed6bddc012598826e7d0c785534d870fc78820873063c896606c",
"totalKeys": 1669,
"translatedKeys": 1669,
"translatedKeys": 1668,
"workflow": 1
}

View file

@ -1561,7 +1561,7 @@ export const ar: TranslationMap = {
gatewayStatus: "حالة Gateway: {status}",
commandPaletteTitle: "ابحث أو انتقل إلى… (⌘K)",
openCommandPalette: "فتح لوحة الأوامر",
docsOpensInNewTab: "{label} (يفتح في علامة تبويب جديدة)",
docsTooltip: "Documentation ↗",
updateAvailable: "يتوفر تحديث:",
runningVersion: "الإصدار قيد التشغيل v{version}",
updating: "جارٍ التحديث…",

View file

@ -1591,7 +1591,7 @@ export const de: TranslationMap = {
gatewayStatus: "Gateway-Status: {status}",
commandPaletteTitle: "Suchen oder springen zu… (⌘K)",
openCommandPalette: "Befehlspalette öffnen",
docsOpensInNewTab: "{label} (wird in neuem Tab geöffnet)",
docsTooltip: "Documentation ↗",
updateAvailable: "Update verfügbar:",
runningVersion: "v{version} wird ausgeführt",
updating: "Wird aktualisiert…",

View file

@ -472,8 +472,8 @@ export const en: TranslationMap = {
collapse: "Collapse sidebar",
resize: "Resize sidebar",
more: "More",
customize: "Customize sidebar",
customizeReset: "Reset to defaults",
customize: "Edit pinned items",
customizeReset: "Reset pinned items",
},
terminal: {
title: "Terminal",
@ -1567,7 +1567,7 @@ export const en: TranslationMap = {
gatewayStatus: "Gateway status: {status}",
commandPaletteTitle: "Search or jump to… (⌘K)",
openCommandPalette: "Open command palette",
docsOpensInNewTab: "{label} (opens in new tab)",
docsTooltip: "Documentation ↗",
updateAvailable: "Update available:",
runningVersion: "running v{version}",
updating: "Updating…",

View file

@ -1588,7 +1588,7 @@ export const es: TranslationMap = {
gatewayStatus: "Estado del Gateway: {status}",
commandPaletteTitle: "Buscar o ir a… (⌘K)",
openCommandPalette: "Abrir paleta de comandos",
docsOpensInNewTab: "{label} (se abre en una pestaña nueva)",
docsTooltip: "Documentation ↗",
updateAvailable: "Actualización disponible:",
runningVersion: "ejecutando v{version}",
updating: "Actualizando…",

View file

@ -1579,7 +1579,7 @@ export const fa: TranslationMap = {
gatewayStatus: "وضعیت Gateway: {status}",
commandPaletteTitle: "جست‌وجو یا رفتن به… (⌘K)",
openCommandPalette: "باز کردن پالت فرمان",
docsOpensInNewTab: "{label} (در زبانهٔ جدید باز می‌شود)",
docsTooltip: "Documentation ↗",
updateAvailable: "به‌روزرسانی در دسترس است:",
runningVersion: "در حال اجرای v{version}",
updating: "در حال به‌روزرسانی…",

View file

@ -1598,7 +1598,7 @@ export const fr: TranslationMap = {
gatewayStatus: "État du Gateway : {status}",
commandPaletteTitle: "Rechercher ou accéder à… (⌘K)",
openCommandPalette: "Ouvrir la palette de commandes",
docsOpensInNewTab: "{label} (souvre dans un nouvel onglet)",
docsTooltip: "Documentation ↗",
updateAvailable: "Mise à jour disponible :",
runningVersion: "version v{version} en cours dexécution",
updating: "Mise à jour…",

View file

@ -1563,7 +1563,7 @@ export const hi: TranslationMap = {
gatewayStatus: "Gateway स्थिति: {status}",
commandPaletteTitle: "खोजें या यहां जाएं… (⌘K)",
openCommandPalette: "कमांड पैलेट खोलें",
docsOpensInNewTab: "{label} (नए टैब में खुलता है)",
docsTooltip: "Documentation ↗",
updateAvailable: "अपडेट उपलब्ध है:",
runningVersion: "v{version} चल रहा है",
updating: "अपडेट हो रहा है…",

View file

@ -1580,7 +1580,7 @@ export const id: TranslationMap = {
gatewayStatus: "Status Gateway: {status}",
commandPaletteTitle: "Cari atau lompat ke… (⌘K)",
openCommandPalette: "Buka palet perintah",
docsOpensInNewTab: "{label} (terbuka di tab baru)",
docsTooltip: "Documentation ↗",
updateAvailable: "Pembaruan tersedia:",
runningVersion: "menjalankan v{version}",
updating: "Memperbarui…",

View file

@ -1590,7 +1590,7 @@ export const it: TranslationMap = {
gatewayStatus: "Stato Gateway: {status}",
commandPaletteTitle: "Cerca o passa a… (⌘K)",
openCommandPalette: "Apri tavolozza comandi",
docsOpensInNewTab: "{label} (si apre in una nuova scheda)",
docsTooltip: "Documentation ↗",
updateAvailable: "Aggiornamento disponibile:",
runningVersion: "in esecuzione v{version}",
updating: "Aggiornamento…",

View file

@ -1586,7 +1586,7 @@ export const ja_JP: TranslationMap = {
gatewayStatus: "Gateway ステータス: {status}",
commandPaletteTitle: "検索または移動… (⌘K)",
openCommandPalette: "コマンドパレットを開く",
docsOpensInNewTab: "{label} (新しいタブで開きます)",
docsTooltip: "Documentation ↗",
updateAvailable: "アップデートがあります:",
runningVersion: "実行中 v{version}",
updating: "更新中…",

View file

@ -1568,7 +1568,7 @@ export const ko: TranslationMap = {
gatewayStatus: "Gateway 상태: {status}",
commandPaletteTitle: "검색하거나 이동… (⌘K)",
openCommandPalette: "명령 팔레트 열기",
docsOpensInNewTab: "{label}(새 탭에서 열림)",
docsTooltip: "Documentation ↗",
updateAvailable: "업데이트 사용 가능:",
runningVersion: "실행 중 v{version}",
updating: "업데이트 중…",

View file

@ -1585,7 +1585,7 @@ export const nl: TranslationMap = {
gatewayStatus: "Gateway-status: {status}",
commandPaletteTitle: "Zoeken of springen naar… (⌘K)",
openCommandPalette: "Opdrachtenpalet openen",
docsOpensInNewTab: "{label} (opent in nieuw tabblad)",
docsTooltip: "Documentation ↗",
updateAvailable: "Update beschikbaar:",
runningVersion: "actieve versie v{version}",
updating: "Bijwerken…",

View file

@ -1583,7 +1583,7 @@ export const pl: TranslationMap = {
gatewayStatus: "Status Gateway: {status}",
commandPaletteTitle: "Wyszukaj lub przejdź do… (⌘K)",
openCommandPalette: "Otwórz paletę poleceń",
docsOpensInNewTab: "{label} (otwiera się w nowej karcie)",
docsTooltip: "Documentation ↗",
updateAvailable: "Dostępna aktualizacja:",
runningVersion: "uruchomiona wersja v{version}",
updating: "Aktualizowanie…",

View file

@ -1581,7 +1581,7 @@ export const pt_BR: TranslationMap = {
gatewayStatus: "Status do Gateway: {status}",
commandPaletteTitle: "Pesquisar ou ir para… (⌘K)",
openCommandPalette: "Abrir paleta de comandos",
docsOpensInNewTab: "{label} (abre em nova aba)",
docsTooltip: "Documentation ↗",
updateAvailable: "Atualização disponível:",
runningVersion: "executando v{version}",
updating: "Atualizando…",

View file

@ -1592,7 +1592,7 @@ export const ru: TranslationMap = {
gatewayStatus: "Статус Gateway: {status}",
commandPaletteTitle: "Поиск или переход к… (⌘K)",
openCommandPalette: "Открыть палитру команд",
docsOpensInNewTab: "{label} (откроется в новой вкладке)",
docsTooltip: "Documentation ↗",
updateAvailable: "Доступно обновление:",
runningVersion: "запущена v{version}",
updating: "Обновление…",

View file

@ -1545,7 +1545,7 @@ export const th: TranslationMap = {
gatewayStatus: "สถานะ Gateway: {status}",
commandPaletteTitle: "ค้นหาหรือข้ามไปที่… (⌘K)",
openCommandPalette: "เปิดแถบคำสั่ง",
docsOpensInNewTab: "{label} (เปิดในแท็บใหม่)",
docsTooltip: "Documentation ↗",
updateAvailable: "มีอัปเดตพร้อมใช้งาน:",
runningVersion: "กำลังใช้ v{version}",
updating: "กำลังอัปเดต…",

View file

@ -1585,7 +1585,7 @@ export const tr: TranslationMap = {
gatewayStatus: "Gateway durumu: {status}",
commandPaletteTitle: "Ara veya şuraya git… (⌘K)",
openCommandPalette: "Komut paletini aç",
docsOpensInNewTab: "{label} (yeni sekmede açılır)",
docsTooltip: "Documentation ↗",
updateAvailable: "Güncelleme mevcut:",
runningVersion: "çalışan v{version}",
updating: "Güncelleniyor…",

View file

@ -1581,7 +1581,7 @@ export const uk: TranslationMap = {
gatewayStatus: "Стан Gateway: {status}",
commandPaletteTitle: "Пошук або перехід до… (⌘K)",
openCommandPalette: "Відкрити палітру команд",
docsOpensInNewTab: "{label} (відкривається в новій вкладці)",
docsTooltip: "Documentation ↗",
updateAvailable: "Доступне оновлення:",
runningVersion: "працює v{version}",
updating: "Оновлення…",

View file

@ -1570,7 +1570,7 @@ export const vi: TranslationMap = {
gatewayStatus: "Trạng thái Gateway: {status}",
commandPaletteTitle: "Tìm kiếm hoặc chuyển đến… (⌘K)",
openCommandPalette: "Mở bảng lệnh",
docsOpensInNewTab: "{label} (mở trong tab mới)",
docsTooltip: "Documentation ↗",
updateAvailable: "Có bản cập nhật:",
runningVersion: "đang chạy v{version}",
updating: "Đang cập nhật…",

View file

@ -1540,7 +1540,7 @@ export const zh_CN: TranslationMap = {
gatewayStatus: "Gateway 状态:{status}",
commandPaletteTitle: "搜索或跳转到… (⌘K)",
openCommandPalette: "打开命令面板",
docsOpensInNewTab: "{label}(在新标签页中打开)",
docsTooltip: "Documentation ↗",
updateAvailable: "有可用更新:",
runningVersion: "正在运行 v{version}",
updating: "正在更新…",

View file

@ -1542,7 +1542,7 @@ export const zh_TW: TranslationMap = {
gatewayStatus: "Gateway 狀態:{status}",
commandPaletteTitle: "搜尋或跳至… (⌘K)",
openCommandPalette: "開啟命令面板",
docsOpensInNewTab: "{label}(在新分頁開啟)",
docsTooltip: "Documentation ↗",
updateAvailable: "有可用更新:",
runningVersion: "執行中版本 v{version}",
updating: "正在更新…",

View file

@ -16,6 +16,7 @@ import {
import { icons } from "../../components/icons.ts";
import "../../components/tooltip.ts";
import { t } from "../../i18n/index.ts";
import { isGatewayMethodAdvertised } from "../../lib/gateway-methods.ts";
import { resolveSessionDisplayName } from "../../lib/session-display.ts";
import { resolveSessionKey, scopedAgentParamsForSession } from "../../lib/sessions/index.ts";
import {
@ -569,6 +570,12 @@ class ChatPane extends LitElement {
if (!state) {
return;
}
const previousTerminalAvailable = state.terminalAvailable;
state.terminalAvailable =
config.terminalEnabled &&
state.connected &&
hasOperatorAdminAccess(state.hello?.auth ?? null) &&
isGatewayMethodAdvertised(this.context.gateway.snapshot, "terminal.open");
const rootsChanged =
state.localMediaPreviewRoots.length !== config.localMediaPreviewRoots.length ||
state.localMediaPreviewRoots.some(
@ -576,6 +583,7 @@ class ChatPane extends LitElement {
);
if (
!rootsChanged &&
state.terminalAvailable === previousTerminalAvailable &&
state.embedSandboxMode === config.embedSandboxMode &&
state.allowExternalEmbedUrls === config.allowExternalEmbedUrls &&
state.chatMessageMaxWidth === config.chatMessageMaxWidth
@ -599,6 +607,11 @@ class ChatPane extends LitElement {
state.client = snapshot.client;
state.connected = snapshot.connected;
state.hello = snapshot.hello;
state.terminalAvailable =
this.context.config.current.terminalEnabled &&
snapshot.connected &&
hasOperatorAdminAccess(snapshot.hello?.auth ?? null) &&
isGatewayMethodAdvertised(snapshot, "terminal.open");
state.assistantAgentId = snapshot.assistantAgentId;
const routeSessionKey = this.sessionKey.trim();
const canonicalRouteSessionKey = routeSessionKey

View file

@ -980,6 +980,7 @@ export function createPageState(
client: null,
connected: false,
hello: null,
terminalAvailable: false,
assistantAgentId: context.agentSelection.state.selectedId,
sessionKey: settings.sessionKey,
chatLoading: false,

View file

@ -31,6 +31,7 @@ export type SessionWorkspaceProps = {
onOpenFile: (path: string, origin: "session" | "workspace") => void;
onSearch: (search: string) => void;
onOpenArtifact: (artifactId: string) => void;
onToggleTerminal?: () => void;
};
type SessionWorkspaceState = {
@ -63,6 +64,7 @@ export type SessionWorkspaceHost = {
client: GatewayBrowserClient | null;
connected: boolean;
hello: GatewayHelloOk | null;
terminalAvailable?: boolean;
assistantAgentId?: string | null;
agentsList?: SessionScopeHost["agentsList"];
sessionWorkspaceState?: SessionWorkspaceState;
@ -493,6 +495,15 @@ export function createSessionWorkspaceProps(state: SessionWorkspaceHost): Sessio
}, 160);
},
onOpenArtifact: (artifactId) => openArtifact(state, workspace, artifactId),
onToggleTerminal: state.terminalAvailable
? () => {
window.dispatchEvent(
new CustomEvent("openclaw:terminal-toggle", {
detail: { dock: "right", open: true },
}),
);
}
: undefined,
};
}
@ -535,6 +546,20 @@ export function renderSessionWorkspaceRail(
if (!sessionWorkspace) {
return nothing;
}
const terminalButton = sessionWorkspace.onToggleTerminal
? html`
<openclaw-tooltip .content=${t("terminal.toggle")}>
<button
type="button"
class="chat-workspace-rail__terminal"
aria-label=${t("terminal.toggle")}
@click=${sessionWorkspace.onToggleTerminal}
>
${icons.terminal}
</button>
</openclaw-tooltip>
`
: nothing;
if (sessionWorkspace.collapsed) {
return html`
<aside
@ -557,6 +582,7 @@ export function renderSessionWorkspaceRail(
<span class="chat-workspace-rail__collapsed-icon" aria-hidden="true"
>${icons.fileText}</span
>
${terminalButton}
</aside>
`;
}
@ -676,38 +702,6 @@ export function renderSessionWorkspaceRail(
: t("chat.workspaceFiles.session");
return html`<span class="chat-workspace-rail__file-badge">${label}</span>`;
};
const renderBrowserBreadcrumbs = (): TemplateResult | typeof nothing => {
if (!browser || browser.search) {
return nothing;
}
const parts = browser.path ? browser.path.split("/").filter(Boolean) : [];
let currentPath = "";
return html`
<div class="chat-workspace-rail__breadcrumbs" aria-label=${t("chat.workspaceFiles.path")}>
<button
class="chat-workspace-rail__crumb"
type="button"
@click=${() => sessionWorkspace.onBrowsePath("")}
>
${t("chat.workspaceFiles.root")}
</button>
${parts.map((part) => {
currentPath = currentPath ? `${currentPath}/${part}` : part;
const pathForPart = currentPath;
return html`
<span class="chat-workspace-rail__crumb-separator">/</span>
<button
class="chat-workspace-rail__crumb"
type="button"
@click=${() => sessionWorkspace.onBrowsePath(pathForPart)}
>
${part}
</button>
`;
})}
</div>
`;
};
const renderBrowserRows = (): TemplateResult => {
const entries = browser?.entries ?? [];
const parentPath = browser?.parentPath;
@ -728,7 +722,6 @@ export function renderSessionWorkspaceRail(
/>
</label>
</div>
${renderBrowserBreadcrumbs()}
${browser?.search
? html`<div class="chat-workspace-rail__browser-caption">
${t("chat.workspaceFiles.searchResults")}
@ -874,6 +867,7 @@ export function renderSessionWorkspaceRail(
<strong>${t("chat.workspaceFiles.files")}</strong>
</div>
<div class="chat-workspace-rail__actions">
${terminalButton}
<openclaw-tooltip .content=${t("chat.workspaceFiles.refresh")}>
<button
class="btn btn--ghost btn--sm chat-workspace-rail__refresh"

View file

@ -111,6 +111,7 @@
gap: 6px;
}
.chat-workspace-rail__terminal,
.chat-workspace-rail__refresh,
.chat-workspace-rail__collapse-toggle {
width: 32px;
@ -119,11 +120,41 @@
padding: 0;
}
.chat-workspace-rail__terminal {
display: inline-flex;
align-items: center;
justify-content: center;
border: 1px solid color-mix(in srgb, var(--border) 76%, transparent);
border-radius: var(--radius-sm);
background: transparent;
color: var(--muted);
cursor: pointer;
}
.chat-workspace-rail__terminal:hover,
.chat-workspace-rail__terminal:focus-visible {
color: var(--text);
border-color: color-mix(in srgb, var(--accent) 34%, var(--border));
background: color-mix(in srgb, var(--accent) 8%, transparent);
}
.chat-workspace-rail--collapsed .chat-workspace-rail__terminal {
border-color: transparent;
background: transparent;
}
.chat-workspace-rail--collapsed .chat-workspace-rail__terminal:hover,
.chat-workspace-rail--collapsed .chat-workspace-rail__terminal:focus-visible {
border-color: transparent;
background: transparent;
}
.chat-workspace-rail__collapse-toggle {
flex: 0 0 auto;
box-shadow: none;
}
.chat-workspace-rail__terminal svg,
.chat-workspace-rail__refresh svg,
.chat-workspace-rail__collapse-toggle svg,
.chat-workspace-rail__file-icon svg {
@ -407,40 +438,6 @@
color: var(--muted);
}
.chat-workspace-rail__breadcrumbs {
display: flex;
align-items: center;
gap: 4px;
min-width: 0;
padding: 0 12px 6px;
color: var(--muted);
font-size: var(--control-ui-text-xs);
line-height: 1.2;
overflow: hidden;
}
.chat-workspace-rail__crumb {
min-width: 0;
max-width: 88px;
padding: 0;
border: 0;
background: transparent;
color: inherit;
cursor: pointer;
font: inherit;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.chat-workspace-rail__crumb:hover,
.chat-workspace-rail__crumb:focus-visible {
color: var(--text);
text-decoration: underline;
text-underline-offset: 2px;
}
.chat-workspace-rail__crumb-separator,
.chat-workspace-rail__browser-caption {
color: var(--muted);
}

View file

@ -7,16 +7,14 @@
--shell-gap: 16px;
--shell-nav-width: 258px;
--shell-nav-rail-width: 78px;
--shell-topbar-height: 44px;
--shell-topbar-height: 0px;
--shell-focus-duration: 200ms;
--shell-focus-ease: var(--ease-out);
height: 100vh;
display: grid;
grid-template-columns: var(--shell-nav-width) minmax(0, 1fr);
grid-template-rows: var(--shell-topbar-height) 1fr;
grid-template-areas:
"nav topbar"
"nav content";
grid-template-rows: 1fr;
grid-template-areas: "nav content";
gap: 0;
animation: dashboard-enter 0.3s var(--ease-out);
transition:
@ -35,6 +33,12 @@
overflow: hidden;
}
.shell--no-topbar {
grid-template-rows: 1fr;
grid-template-areas: "nav content";
--shell-topbar-height: 0px;
}
.shell--nav-collapsed {
grid-template-columns: var(--shell-nav-rail-width) minmax(0, 1fr);
/* Keep --shell-nav-width in sync with the real column so fixed-position
@ -48,10 +52,6 @@
--shell-nav-width: 0px;
}
.shell--onboarding .topbar {
display: none;
}
.shell--onboarding .shell-nav {
display: none;
}
@ -69,7 +69,7 @@
position: sticky;
top: 0;
z-index: 40;
display: flex;
display: none;
align-items: center;
padding: 0 12px;
min-height: var(--shell-topbar-height);
@ -79,6 +79,10 @@
-webkit-backdrop-filter: blur(12px) saturate(1.6);
}
.shell--onboarding .topbar {
display: none;
}
.topnav-shell {
display: flex;
align-items: center;
@ -104,77 +108,120 @@
flex: 1;
}
.topnav-shell .dashboard-header {
.topbar-brand {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
justify-content: center;
gap: 7px;
min-width: 0;
}
.topnav-shell .dashboard-header__breadcrumb {
.topbar-brand__logo {
display: block;
width: 24px;
height: 24px;
border-radius: var(--radius-md);
}
.topbar-brand__title {
min-width: 0;
overflow: hidden;
color: var(--text-strong);
font-size: 14px;
font-weight: 650;
letter-spacing: 0;
text-overflow: ellipsis;
white-space: nowrap;
}
.topbar .nav-collapse-toggle {
width: 36px;
height: 36px;
margin-bottom: 0;
}
.topbar .nav-collapse-toggle__icon {
width: 20px;
height: 20px;
}
.topbar .nav-collapse-toggle__icon svg {
width: 20px;
height: 20px;
}
.topbar-status {
display: flex;
align-items: center;
gap: 8px;
min-width: 0;
overflow: hidden;
}
.topbar-status .pill {
padding: 6px 10px;
gap: 6px;
font-size: 12px;
font-weight: 500;
height: 32px;
box-sizing: border-box;
}
.topbar-status .pill .mono {
display: flex;
align-items: center;
line-height: 1;
margin-top: 0px;
}
.topbar-status .statusDot {
width: 6px;
height: 6px;
}
.topbar-status .theme-orb__trigger {
width: 26px;
height: 26px;
font-size: 13px;
}
.topnav-shell .dashboard-header__breadcrumb-link,
.topnav-shell .dashboard-header__breadcrumb-sep {
.topbar-search {
display: none;
align-items: center;
gap: 12px;
min-height: 38px;
padding: 0 14px;
border: 1px solid color-mix(in srgb, var(--border) 88%, transparent);
border-radius: var(--radius-full);
background: color-mix(in srgb, var(--bg-elevated) 84%, transparent);
color: var(--muted);
font-size: 13px;
cursor: pointer;
transition:
border-color var(--duration-fast) ease,
background var(--duration-fast) ease,
color var(--duration-fast) ease;
min-width: 200px;
}
.topnav-shell .dashboard-header__breadcrumb-segment {
display: inline-flex;
align-items: center;
gap: 8px;
min-width: 0;
}
.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 {
.topbar-search:hover {
border-color: color-mix(in srgb, var(--border-strong) 90%, transparent);
background: color-mix(in srgb, var(--bg-hover) 84%, transparent);
color: var(--text);
text-decoration: underline;
text-underline-offset: 3px;
}
.topnav-shell .dashboard-header__breadcrumb-current {
color: var(--accent);
font-weight: 650;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
.topbar-search:focus-visible {
outline: none;
box-shadow: var(--focus-ring);
}
.topnav-shell .dashboard-header__breadcrumb-context {
max-width: 18ch;
overflow: hidden;
color: var(--muted);
font-weight: 550;
text-overflow: ellipsis;
white-space: nowrap;
.topbar-search svg {
width: 16px;
height: 16px;
stroke: currentColor;
fill: none;
stroke-width: 1.7px;
stroke-linecap: round;
stroke-linejoin: round;
}
@media (max-width: 720px) {
.topnav-shell .dashboard-header__breadcrumb-context {
max-width: 10ch;
}
}
/* Quiet ghost buttons: the tiny top bar carries navigation chrome only, so
its controls stay borderless and muted until hovered. */
.topbar-icon-btn {
display: inline-flex;
align-items: center;
@ -261,6 +308,91 @@
box-shadow: none;
}
.sidebar-brand {
display: flex;
align-items: center;
gap: 8px;
min-height: 34px;
padding: 0 8px 12px;
flex-shrink: 0;
}
.sidebar-brand__identity {
display: flex;
align-items: center;
gap: 7px;
min-width: 0;
flex: 1 1 auto;
}
.sidebar-brand__logo {
width: 24px;
height: 24px;
border-radius: var(--radius-md);
flex: 0 0 auto;
}
.sidebar-brand__title {
min-width: 0;
overflow: hidden;
color: var(--text-strong);
font-size: 14px;
font-weight: 650;
letter-spacing: 0;
text-overflow: ellipsis;
white-space: nowrap;
}
.sidebar-brand__actions {
display: flex;
align-items: center;
gap: 4px;
flex: 0 0 auto;
}
.sidebar-brand__icon {
display: inline-flex;
align-items: center;
justify-content: center;
width: 30px;
height: 30px;
padding: 0;
border: none;
border-radius: var(--radius-md);
background: transparent;
color: var(--muted);
cursor: pointer;
transition:
background var(--duration-fast) ease,
color var(--duration-fast) ease;
}
.sidebar-brand__icon:hover:not(:disabled),
.sidebar-brand__icon:focus-visible {
background: color-mix(in srgb, var(--bg-hover) 84%, transparent);
color: var(--text);
}
.sidebar-brand__icon:focus-visible {
outline: none;
box-shadow: var(--focus-ring);
}
.sidebar-brand__icon:disabled {
cursor: not-allowed;
opacity: 0.42;
}
.sidebar-brand__icon svg {
width: 16px;
height: 16px;
stroke: currentColor;
fill: none;
stroke-width: 1.7px;
stroke-linecap: round;
stroke-linejoin: round;
}
.sidebar--collapsed {
width: var(--shell-nav-rail-width);
min-width: var(--shell-nav-rail-width);
@ -390,6 +522,13 @@
min-height: 36px;
margin: 0 auto 10px;
padding: 0;
border-color: transparent;
background: transparent;
}
.sidebar--collapsed .sidebar-search:hover:not(:disabled) {
border-color: transparent;
background: transparent;
}
.sidebar-nav {
@ -1195,6 +1334,23 @@
padding: 12px 8px 10px;
}
.sidebar--collapsed .sidebar-brand {
flex-direction: column;
gap: 8px;
align-items: center;
justify-content: flex-start;
padding: 5px 0 14px;
}
.sidebar--collapsed .sidebar-brand__identity {
flex: 0 0 auto;
justify-content: center;
}
.sidebar--collapsed .sidebar-brand__actions {
flex-direction: column;
}
.sidebar--collapsed .sidebar-nav {
padding: 0;
}
@ -1249,19 +1405,10 @@
display: none;
}
/* Rail active state stays flat: it inherits the accent-subtle background from
.nav-item--active and only adds the accent bar. Gradients/drop shadows made
the rail look like a floating card, especially in light mode. */
/* Rail active state stays flat and uses only the inherited active background. */
.sidebar--collapsed .nav-item--active::before,
.sidebar--collapsed .nav-item.active::before {
content: "";
position: absolute;
left: 8px;
top: 10px;
bottom: 10px;
width: 3px;
border-radius: var(--radius-full);
background: color-mix(in srgb, var(--accent) 86%, transparent);
content: none;
}
/* Single compact icon row: status dot, then utility icons right-aligned.
@ -1370,20 +1517,12 @@
display: none;
}
/* Color mode segmented toggle, sidebar footer edition: quiet enough to sit
between plain icon buttons (no pill chrome until a segment is active). */
.sidebar-mode-switch {
display: inline-flex;
margin-left: 2px;
}
.theme-mode-toggle {
display: inline-flex;
align-items: center;
gap: 2px;
}
.theme-mode-toggle__btn {
display: inline-flex;
align-items: center;
justify-content: center;
@ -1401,25 +1540,19 @@
border-color var(--duration-fast) ease;
}
.theme-mode-toggle__btn:hover {
.theme-mode-toggle:hover {
color: var(--text);
background: color-mix(in srgb, var(--bg-hover) 84%, transparent);
}
.theme-mode-toggle__btn:focus-visible {
.theme-mode-toggle:focus-visible {
outline: none;
box-shadow: var(--focus-ring);
}
.theme-mode-toggle__btn--active {
color: var(--accent);
background: var(--accent-subtle);
border-color: color-mix(in srgb, var(--accent) 25%, transparent);
}
.theme-mode-toggle__btn svg {
width: 14px;
height: 14px;
.theme-mode-toggle svg {
width: 16px;
height: 16px;
stroke: currentColor;
fill: none;
stroke-width: 1.75px;
@ -1427,15 +1560,10 @@
stroke-linejoin: round;
}
/* Rail mode: the footer stacks vertically, so the segments do too. */
.sidebar--collapsed .sidebar-mode-switch {
margin-left: 0;
}
.sidebar--collapsed .theme-mode-toggle {
flex-direction: column;
}
.shell--nav-collapsed .shell-nav {
width: var(--shell-nav-rail-width);
min-width: var(--shell-nav-rail-width);

View file

@ -93,6 +93,7 @@
@media (max-width: 1100px) {
.shell,
.shell--nav-collapsed {
--shell-topbar-height: 58px;
grid-template-columns: minmax(0, 1fr);
grid-template-rows: var(--shell-topbar-height) minmax(0, 1fr);
grid-template-areas:
@ -104,6 +105,13 @@
--shell-nav-width: 0px;
}
.shell--no-topbar,
.shell--nav-collapsed.shell--no-topbar {
--shell-topbar-height: 0px;
grid-template-rows: minmax(0, 1fr);
grid-template-areas: "content";
}
.shell-nav,
.shell--nav-collapsed .shell-nav {
position: fixed;
@ -147,6 +155,50 @@
pointer-events: auto;
}
/* Show the hamburger toggle at the same breakpoint where the drawer takes over;
the desktop rail toggle makes no sense once the sidebar is a drawer. */
.topbar-nav-toggle {
display: inline-flex;
}
.topbar {
display: flex;
}
.topnav-shell {
display: grid;
grid-template-columns: 40px minmax(0, 1fr) 40px;
gap: 10px;
}
.topnav-shell__content {
display: flex;
justify-content: center;
}
.topnav-shell__actions {
min-width: 0;
justify-content: flex-end;
}
.topbar-search {
display: inline-flex;
justify-content: center;
width: 38px;
min-width: 38px;
height: 38px;
min-height: 38px;
padding: 0;
}
.topbar-search__kbd {
display: none;
}
.sidebar-brand__actions {
display: none;
}
.sidebar,
.sidebar--collapsed {
width: 100%;
@ -233,26 +285,75 @@
/* Topbar */
.topbar {
display: flex;
padding: 10px 12px;
min-height: auto;
}
.topnav-shell {
flex-wrap: wrap;
gap: 10px;
}
.topnav-shell__actions {
min-width: 0;
flex: 1 1 auto;
justify-content: flex-end;
gap: 10px;
align-items: center;
}
.topnav-shell__content {
display: flex;
justify-content: center;
}
.topbar-nav-toggle {
display: inline-flex;
}
.topbar-status {
gap: 6px;
width: auto;
flex-wrap: nowrap;
}
.topbar-search {
display: inline-flex;
justify-content: center;
width: 38px;
min-width: 38px;
height: 38px;
min-height: 38px;
padding: 0;
}
.topbar-search__kbd {
display: none;
}
.topbar-theme-mode {
display: none;
}
.sidebar-mode-switch {
display: block;
}
.sidebar-mode-switch .topbar-theme-mode {
display: inline-flex;
width: 100%;
justify-content: center;
}
.topbar-status .pill {
padding: 4px 8px;
font-size: 12px; /* was 11px */
gap: 4px;
}
.topbar-status .pill .mono {
display: none;
}
.topbar-status .pill span:nth-child(2) {
display: none;
}
.shell-nav,