improve(ui): align transient menu chrome with design-system geometry (#122109)

* improve(ui): align transient menu chrome with design-system geometry

Use one shared transient-menu recipe across Control UI panels and items.

Match the carapace reference: 6px item radius plus 4px inset yields a 10px panel radius, with restrained motion for fixed menus.

* test(ui): align gateway picker menu expectations

* test(cli): keep benign sqlite diagnostics out of ACP exit stderr assertions

* fix(ui): restore 44px menu touch targets on coarse pointers
This commit is contained in:
Peter Steinberger
2026-08-11 10:08:53 -07:00
committed by GitHub
parent cc9119e22a
commit 8fa897a3f1
14 changed files with 196 additions and 121 deletions
+15 -4
View File
@@ -7,7 +7,7 @@ import {
} from "node:child_process";
import { createServer } from "node:http";
import path from "node:path";
import { promisify } from "node:util";
import { promisify, stripVTControlCharacters } from "node:util";
import { describe, expect, it } from "vitest";
import { type RawData, WebSocketServer } from "ws";
import {
@@ -62,6 +62,17 @@ function createAcpProcessEnv(baseEnv: NodeJS.ProcessEnv): NodeJS.ProcessEnv {
};
}
function withoutSqliteTransactionWarnings(stderr: string): string {
// Slow-hold logger output is load-dependent performance diagnostics, not ACP clean-exit
// signal; see logSlowTransactionHold in sqlite-transaction.ts.
return stderr
.split("\n")
.filter(
(line) => !stripVTControlCharacters(line).trimStart().startsWith("[sqlite/transaction]"),
)
.join("\n");
}
function waitForExit(child: ChildProcessWithoutNullStreams) {
return new Promise<{ code: number | null; signal: NodeJS.Signals | null }>((resolve, reject) => {
child.once("error", reject);
@@ -139,7 +150,7 @@ describe("ACP CLI process exit", () => {
},
);
expect(result.stderr).toBe("");
expect(withoutSqliteTransactionWarnings(result.stderr)).toBe("");
expect(result.stdout).toContain(usage);
} finally {
await state.cleanup();
@@ -173,7 +184,7 @@ describe("ACP CLI process exit", () => {
expect(result.error).toBeUndefined();
expect(result.signal).toBeNull();
expect(result.status).toBe(0);
expect(result.stderr).toBe("");
expect(withoutSqliteTransactionWarnings(result.stderr)).toBe("");
} finally {
await state.cleanup();
}
@@ -272,7 +283,7 @@ describe("ACP CLI process exit", () => {
child.stdin.end();
const exit = await exitPromise;
expect(exit).toEqual({ code: 0, signal: null });
expect(stderr).toBe("");
expect(withoutSqliteTransactionWarnings(stderr)).toBe("");
} finally {
child?.kill("SIGKILL");
for (const socket of wss.clients) {
@@ -47,11 +47,11 @@ export const terminalPanelStyles = css`
width: min(360px, calc(100vw - 24px));
max-height: min(420px, var(--tp-session-menu-max-height));
overflow-y: auto;
border: 1px solid var(--border, #262b34);
border-radius: 8px;
background: var(--bg, #0e1015);
box-shadow: 0 12px 30px rgb(0 0 0 / 35%);
padding: 6px;
padding: var(--menu-padding);
border: 1px solid var(--border-strong);
border-radius: var(--menu-radius);
background: var(--bg-elevated);
box-shadow: var(--shadow-md);
}
.tp-session-menu__header {
display: flex;
@@ -77,7 +77,8 @@ export const terminalPanelStyles = css`
gap: 8px;
width: 100%;
border: 0;
border-radius: 6px;
min-height: var(--menu-item-height);
border-radius: var(--menu-item-radius);
background: transparent;
color: var(--text, #d7dae0);
padding: 7px 8px;
@@ -85,7 +86,7 @@ export const terminalPanelStyles = css`
}
.tp-session:not(:disabled):hover,
.tp-session:not(:disabled):focus-visible {
background: color-mix(in srgb, var(--text, #d7dae0) 10%, transparent);
background: var(--bg-hover);
}
.tp-session:disabled {
opacity: 0.55;
@@ -872,35 +872,54 @@ describeBrowserLayout.concurrent("chat responsive browser layout", () => {
</body></html>`,
);
const styles = await page.evaluate(() => {
const dropdown = document.querySelector<HTMLElement>(".chat-pane__gateway-menu")!;
const menu = dropdown.shadowRoot!.querySelector<HTMLElement>('[part="menu"]')!;
const item = dropdown.querySelector<HTMLElement>(".chat-pane__gateway-menu-item")!;
const menuStyle = getComputedStyle(menu);
const itemStyle = getComputedStyle(item);
return {
menu: {
borderRadius: menuStyle.borderRadius,
padding: menuStyle.padding,
},
item: {
borderRadius: itemStyle.borderRadius,
fontSize: itemStyle.fontSize,
minHeight: itemStyle.minHeight,
padding: itemStyle.padding,
},
};
});
const readGatewayMenuStyles = () =>
page.evaluate(() => {
const dropdown = document.querySelector<HTMLElement>(".chat-pane__gateway-menu")!;
const menu = dropdown.shadowRoot!.querySelector<HTMLElement>('[part="menu"]')!;
const item = dropdown.querySelector<HTMLElement>(".chat-pane__gateway-menu-item")!;
const menuStyle = getComputedStyle(menu);
const itemStyle = getComputedStyle(item);
return {
menu: {
borderRadius: menuStyle.borderRadius,
padding: menuStyle.padding,
},
item: {
borderRadius: itemStyle.borderRadius,
fontSize: itemStyle.fontSize,
minHeight: itemStyle.minHeight,
padding: itemStyle.padding,
},
};
});
const styles = await readGatewayMenuStyles();
expect(styles).toEqual({
menu: { borderRadius: "8px", padding: "6px" },
menu: { borderRadius: "10px", padding: "4px" },
item: {
borderRadius: "8px",
borderRadius: "6px",
fontSize: "13px",
minHeight: "30px",
minHeight: "28px",
padding: "0px 8px",
},
});
const session = await page.context().newCDPSession(page);
try {
await session.send("Emulation.setTouchEmulationEnabled", {
enabled: true,
maxTouchPoints: 1,
});
await session.send("Emulation.setEmulatedMedia", {
media: "screen",
features: [{ name: "pointer", value: "coarse" }],
});
expect(await page.evaluate(() => matchMedia("(pointer: coarse)").matches)).toBe(true);
expect((await readGatewayMenuStyles()).item.minHeight).toBe("44px");
} finally {
await session.detach();
}
} finally {
await closeBrowserPage(page);
}
+25
View File
@@ -166,6 +166,13 @@
--radius-full: 9999px;
--radius: 10px;
/* Menu items stay optically parallel to their panel edge: radius 6 + padding 4
= panel radius 10. Shared knobs keep transient menus from drifting again. */
--menu-radius: var(--radius-md);
--menu-item-radius: var(--radius-sm);
--menu-item-height: 28px;
--menu-padding: 4px;
/* Transitions - Crisp and responsive */
--ease-out: cubic-bezier(0.16, 1, 0.3, 1);
--ease-in-out: cubic-bezier(0.4, 0, 0.2, 1);
@@ -189,6 +196,13 @@
color-scheme: dark;
}
/* Menu actions follow the platform 44px touch-target contract for coarse pointers. */
@media (pointer: coarse) {
:root {
--menu-item-height: 44px;
}
}
/* The claw family deepens only the text-bearing destructive fill. */
:root[data-theme="dark"] {
--destructive: #d32f2f;
@@ -852,6 +866,17 @@ textarea,
}
}
@keyframes menu-in {
from {
opacity: 0;
transform: translateY(6px) scale(0.985);
}
to {
opacity: 1;
transform: translateY(0) scale(1);
}
}
@keyframes scale-in {
from {
opacity: 0;
+5 -5
View File
@@ -114,12 +114,12 @@ openclaw-board-view {
.board-tabs__overflow::part(menu),
.board-widget__menu::part(menu) {
background: var(--panel);
border: 1px solid var(--board-line);
border-radius: 10px;
box-shadow: 0 18px 48px rgb(0 0 0 / 32%);
min-width: 190px;
padding: 6px;
padding: var(--menu-padding);
border: 1px solid var(--border-strong);
border-radius: var(--menu-radius);
background: var(--bg-elevated);
box-shadow: var(--shadow-md);
}
.board-tabs__overflow-item {
+25 -22
View File
@@ -1548,12 +1548,13 @@ openclaw-chat-video-player {
.chat-reply-context-menu {
position: fixed;
z-index: 9999;
background: var(--bg);
border: 1px solid var(--border);
border-radius: 8px;
padding: 4px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
padding: var(--menu-padding);
border: 1px solid var(--border-strong);
border-radius: var(--menu-radius);
background: var(--bg-elevated);
box-shadow: var(--shadow-md);
min-width: 160px;
animation: menu-in 140ms var(--ease-out);
}
.chat-reply-context-menu button {
@@ -1565,13 +1566,14 @@ openclaw-chat-video-player {
border: none;
background: none;
color: var(--text);
border-radius: 6px;
min-height: var(--menu-item-height);
border-radius: var(--menu-item-radius);
text-align: left;
}
.chat-reply-context-menu button:hover,
.chat-reply-context-menu button:focus-visible {
background: color-mix(in srgb, var(--accent) 12%, transparent);
background: var(--bg-hover);
outline: none;
}
@@ -1580,11 +1582,11 @@ openclaw-chat-video-player {
z-index: 9999;
display: flex;
gap: 2px;
background: var(--bg);
border: 1px solid var(--border);
border-radius: 8px;
padding: 3px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
padding: var(--menu-padding);
border: 1px solid var(--border-strong);
border-radius: var(--menu-radius);
background: var(--bg-elevated);
box-shadow: var(--shadow-md);
animation: fade-in 0.12s var(--ease-out);
}
@@ -1596,14 +1598,15 @@ openclaw-chat-video-player {
border: none;
background: none;
color: var(--text);
border-radius: 6px;
min-height: var(--menu-item-height);
border-radius: var(--menu-item-radius);
font-size: 12px;
white-space: nowrap;
}
.chat-selection-popup button:hover,
.chat-selection-popup button:focus-visible {
background: color-mix(in srgb, var(--accent) 12%, transparent);
background: var(--bg-hover);
outline: none;
}
@@ -2704,11 +2707,11 @@ openclaw-chat-video-player {
.agent-chat__attach-menu::part(menu) {
width: 152px;
padding: 5px;
border: 1px solid var(--border);
border-radius: var(--radius-md);
padding: var(--menu-padding);
border: 1px solid var(--border-strong);
border-radius: var(--menu-radius);
background: var(--bg-elevated);
box-shadow: var(--shadow-lg);
box-shadow: var(--shadow-md);
}
.agent-chat__attach-menu-option {
@@ -3789,13 +3792,13 @@ openclaw-chat-video-player {
right: 0;
max-height: 320px;
overflow-y: auto;
background: var(--popover);
background: var(--bg-elevated);
border: 1px solid var(--border-strong);
border-radius: var(--radius-lg);
box-shadow: var(--shadow-lg);
border-radius: var(--menu-radius);
box-shadow: var(--shadow-md);
z-index: 30;
margin-bottom: 4px;
padding: 6px;
padding: var(--menu-padding);
scrollbar-width: thin;
}
+6 -6
View File
@@ -301,17 +301,17 @@ openclaw-chat-pane {
}
.chat-pane__gateway-menu::part(menu) {
padding: 6px;
border: 1px solid color-mix(in srgb, var(--border-strong) 78%, transparent);
border-radius: 8px;
padding: var(--menu-padding);
border: 1px solid var(--border-strong);
border-radius: var(--menu-radius);
background: var(--bg-elevated);
box-shadow: var(--shadow-lg);
box-shadow: var(--shadow-md);
}
.chat-pane__gateway-menu-item {
min-height: 30px;
min-height: var(--menu-item-height);
padding: 0 8px;
border-radius: 8px;
border-radius: var(--menu-item-radius);
color: var(--text);
font: inherit;
font-size: 13px;
+5
View File
@@ -783,6 +783,11 @@
.chat-tool-card__widget-actions::part(menu) {
min-width: 176px;
padding: var(--menu-padding);
border: 1px solid var(--border-strong);
border-radius: var(--menu-radius);
background: var(--bg-elevated);
box-shadow: var(--shadow-md);
}
.chat-tool-card__widget-actions-trigger,
+4 -4
View File
@@ -3522,11 +3522,11 @@ td.data-table-key-col {
width: min(320px, calc(100vw - 24px));
max-height: 320px;
overflow-y: auto;
padding: 6px;
border: 1px solid color-mix(in srgb, var(--border-strong) 78%, transparent);
border-radius: var(--radius-lg);
padding: var(--menu-padding);
border: 1px solid var(--border-strong);
border-radius: var(--menu-radius);
background: var(--bg-elevated);
box-shadow: var(--shadow-lg);
box-shadow: var(--shadow-md);
color: var(--text);
}
+10 -9
View File
@@ -783,13 +783,14 @@
}
.cron-job-menu__item {
min-height: var(--menu-item-height);
border: 0;
background: transparent;
color: var(--text);
font-size: 13px;
text-align: left;
padding: 7px 10px;
border-radius: var(--radius-sm);
border-radius: var(--menu-item-radius);
cursor: var(--cursor-action);
}
@@ -1043,18 +1044,18 @@
.cron-job-menu::part(menu) {
min-width: 170px;
border: 1px solid var(--border);
border-radius: var(--radius-md);
padding: var(--menu-padding);
border: 1px solid var(--border-strong);
border-radius: var(--menu-radius);
background: var(--bg-elevated);
padding: var(--space-1);
box-shadow: var(--shadow-lg);
box-shadow: var(--shadow-md);
}
.cron-filter-dropdown__details::part(menu) {
width: min(300px, calc(100vw - 48px));
padding: var(--space-3);
border: 1px solid var(--border);
border-radius: var(--radius-md);
padding: var(--menu-padding);
border: 1px solid var(--border-strong);
border-radius: var(--menu-radius);
background: var(--bg-elevated);
box-shadow: var(--shadow-lg);
box-shadow: var(--shadow-md);
}
+33 -30
View File
@@ -2470,11 +2470,12 @@ body.update-dialog-open .sidebar-update-card__status {
max-width: 264px;
max-height: 420px;
overflow-y: auto;
padding: 6px;
border: 1px solid color-mix(in srgb, var(--border-strong) 78%, transparent);
border-radius: var(--radius-lg);
padding: var(--menu-padding);
border: 1px solid var(--border-strong);
border-radius: var(--menu-radius);
background: var(--bg-elevated);
box-shadow: var(--shadow-lg);
box-shadow: var(--shadow-md);
animation: menu-in 140ms var(--ease-out);
}
wa-dropdown.sidebar-customize-menu {
@@ -2487,11 +2488,11 @@ wa-dropdown.sidebar-customize-menu::part(menu) {
max-width: 264px;
max-height: 420px;
overflow-y: auto;
padding: 6px;
border: 1px solid color-mix(in srgb, var(--border-strong) 78%, transparent);
border-radius: var(--radius-lg);
padding: var(--menu-padding);
border: 1px solid var(--border-strong);
border-radius: var(--menu-radius);
background: var(--bg-elevated);
box-shadow: var(--shadow-lg);
box-shadow: var(--shadow-md);
}
.sidebar-customize-menu__title {
@@ -2526,10 +2527,10 @@ wa-dropdown.sidebar-customize-menu::part(menu) {
align-items: center;
gap: 8px;
width: 100%;
min-height: 30px;
min-height: var(--menu-item-height);
padding: 0 8px;
border: none;
border-radius: var(--radius-md);
border-radius: var(--menu-item-radius);
background: transparent;
color: var(--text);
font: inherit;
@@ -2541,7 +2542,7 @@ wa-dropdown.sidebar-customize-menu::part(menu) {
}
.sidebar-customize-menu__item:hover {
background: color-mix(in srgb, var(--bg-hover) 84%, transparent);
background: var(--bg-hover);
}
/* Web Awesome's shadow row adds `margin-inline-end: 0.75em !important` to the
@@ -2616,11 +2617,12 @@ wa-dropdown-item.sidebar-customize-menu__item {
z-index: 121;
min-width: 224px;
max-width: 264px;
padding: 6px;
border: 1px solid color-mix(in srgb, var(--border-strong) 78%, transparent);
border-radius: var(--radius-lg);
padding: var(--menu-padding);
border: 1px solid var(--border-strong);
border-radius: var(--menu-radius);
background: var(--bg-elevated);
box-shadow: var(--shadow-lg);
box-shadow: var(--shadow-md);
animation: menu-in 140ms var(--ease-out);
}
wa-dropdown.session-menu {
@@ -2631,11 +2633,11 @@ wa-dropdown.session-menu {
wa-dropdown.session-menu::part(menu) {
min-width: 224px;
max-width: 264px;
padding: 6px;
border: 1px solid color-mix(in srgb, var(--border-strong) 78%, transparent);
border-radius: var(--radius-lg);
padding: var(--menu-padding);
border: 1px solid var(--border-strong);
border-radius: var(--menu-radius);
background: var(--bg-elevated);
box-shadow: var(--shadow-lg);
box-shadow: var(--shadow-md);
}
/* Popover hosts live in the top layer so menus escape in-page stacking
@@ -2658,11 +2660,12 @@ openclaw-session-menu[popover] {
z-index: 121;
min-width: 176px;
max-width: 220px;
padding: 6px;
border: 1px solid color-mix(in srgb, var(--border-strong) 78%, transparent);
border-radius: var(--radius-lg);
padding: var(--menu-padding);
border: 1px solid var(--border-strong);
border-radius: var(--menu-radius);
background: var(--bg-elevated);
box-shadow: var(--shadow-lg);
box-shadow: var(--shadow-md);
animation: menu-in 140ms var(--ease-out);
}
wa-dropdown.sidebar-session-sort-menu {
@@ -2673,11 +2676,11 @@ wa-dropdown.sidebar-session-sort-menu {
wa-dropdown.sidebar-session-sort-menu::part(menu) {
min-width: 176px;
max-width: 220px;
padding: 6px;
border: 1px solid color-mix(in srgb, var(--border-strong) 78%, transparent);
border-radius: var(--radius-lg);
padding: var(--menu-padding);
border: 1px solid var(--border-strong);
border-radius: var(--menu-radius);
background: var(--bg-elevated);
box-shadow: var(--shadow-lg);
box-shadow: var(--shadow-md);
}
.sidebar-session-sort-menu__title {
@@ -2695,10 +2698,10 @@ wa-dropdown.sidebar-session-sort-menu::part(menu) {
align-items: center;
gap: 8px;
width: 100%;
min-height: 30px;
min-height: var(--menu-item-height);
padding: 0 8px;
border: none;
border-radius: var(--radius-md);
border-radius: var(--menu-item-radius);
background: transparent;
color: var(--text);
font: inherit;
@@ -2732,7 +2735,7 @@ wa-dropdown-item.session-menu__item::part(submenu-icon) {
.session-menu__item:focus-visible,
.sidebar-session-sort-menu__item:hover,
.sidebar-session-sort-menu__item:focus-visible {
background: color-mix(in srgb, var(--bg-hover) 84%, transparent);
background: var(--bg-hover);
}
.session-menu__item:disabled {
+3 -3
View File
@@ -204,11 +204,11 @@
width: min(620px, calc(100vw - 32px));
max-height: min(420px, 58dvh);
overflow-y: auto;
padding: 6px;
padding: var(--menu-padding);
border: 1px solid var(--border-strong);
border-radius: var(--radius-lg);
border-radius: var(--menu-radius);
background: var(--bg-elevated);
box-shadow: var(--shadow-lg);
box-shadow: var(--shadow-md);
color: var(--text);
}
+9 -8
View File
@@ -238,7 +238,7 @@ openclaw-new-session-page {
}
/* Menu panel anchored under its trigger; item rows reuse the shared
.session-menu__* styles so every menu in the app reads the same. */
.session-menu__* geometry so its panel and hover pills stay concentric. */
wa-popover.new-session-page__select {
--max-width: min(320px, calc(100vw - 24px));
}
@@ -246,11 +246,11 @@ wa-popover.new-session-page__select {
wa-popover.new-session-page__select::part(body) {
min-width: 224px;
max-width: min(320px, calc(100vw - 24px));
padding: 6px;
border: 1px solid color-mix(in srgb, var(--border-strong) 78%, transparent);
border-radius: var(--radius-lg);
padding: var(--menu-padding);
border: 1px solid var(--border-strong);
border-radius: var(--menu-radius);
background: var(--bg-elevated);
box-shadow: var(--shadow-lg);
box-shadow: var(--shadow-md);
}
.new-session-page__menu-title {
@@ -316,7 +316,7 @@ wa-popover.new-session-page__place-popover::part(body) {
root menu needs its own scroll region or long place lists become unreachable. */
.new-session-page__place-root {
max-height: min(420px, 60vh);
padding: 6px;
padding: var(--menu-padding);
overflow-y: auto;
}
@@ -494,7 +494,8 @@ wa-dropdown.new-session-page__start-menu {
gap: 8px;
padding: 6px 8px;
border: 0;
border-radius: var(--radius-sm);
min-height: var(--menu-item-height);
border-radius: var(--menu-item-radius);
background: transparent;
color: var(--text);
font-size: 13px;
@@ -503,7 +504,7 @@ wa-dropdown.new-session-page__start-menu {
}
.new-session-page__browser-entry:hover {
background: color-mix(in srgb, var(--bg-hover) 84%, transparent);
background: var(--bg-hover);
}
.new-session-page__browser-entry:disabled {
+7 -1
View File
@@ -561,6 +561,11 @@
.usage-export-menu::part(menu) {
min-width: 220px;
max-width: min(320px, calc(100vw - 48px));
padding: var(--menu-padding);
border: 1px solid var(--border-strong);
border-radius: var(--menu-radius);
background: var(--bg-elevated);
box-shadow: var(--shadow-md);
}
.usage-filter-option {
@@ -569,7 +574,8 @@
gap: 8px;
align-items: center;
padding: 6px 8px;
border-radius: var(--radius-md);
min-height: var(--menu-item-height);
border-radius: var(--menu-item-radius);
background: transparent;
font-size: 12px;
transition: background 0.12s ease;