polish(ui): standardize composer select rows on shared menu tokens

This commit is contained in:
vyctorbrzezowski
2026-08-21 23:21:41 -03:00
parent e3762a1693
commit 8d2d9d15d1
3 changed files with 168 additions and 81 deletions
+57
View File
@@ -0,0 +1,57 @@
# Composer selects — padronização e polish
Lane: composer selects (worktree `composer-bench`). Bench files intocados.
## Inventário (arquivo → tipo decidido)
| Select | Arquivo | Tipo | Estado |
| --- | --- | --- | --- |
| Model picker | `ui/src/pages/chat/components/chat-model-picker.ts` + `chat-model-picker-options.ts` | single-select (check à direita + wash) | tocado (CSS) |
| Reasoning/effort | `ui/src/pages/chat/components/chat-effort-picker.ts` | single-select via slider; stop único usa check | tocado (CSS via família compartilhada) |
| Fast mode | idem (dentro do effort menu) | toggle (`role="switch"`) — correto | inalterado |
| Permission/guardian | `ui/src/pages/chat/components/chat-permission-picker.ts` | single-select (`menuitemradio` + check) | tocado (CSS) |
| Plus (+) menu | `ui/src/pages/chat/components/chat-composer-plus-menu.ts` | comandos + toggles | tocado (TS + CSS) |
| — Web search | idem | **corrigido**: era `wa-dropdown-item type="checkbox"` (check à esquerda), virou toggle `wa-switch` igual a skills/connectors — é on/off binário, não escolha entre opções | tocado |
| — Skills/Connectors/Tools | idem | toggles (`wa-switch`) — correto | inalterado |
| Attach menu (camera/photo/file) | `ui/src/pages/chat/components/chat-attachments.ts` | comandos (sem estado) | tocado (CSS) |
| Microphone device picker | `ui/src/pages/chat/components/chat-composer-controls.ts` | single-select (`menuitemradio` + check) | tocado (CSS) |
| Hold-to-dictate | idem | toggle (`role="switch"`) — correto | inalterado |
| Context-usage ring popover | `chat-composer-context.ts` | info-only, sem seleção | inalterado |
| Slash menu / Skill menu | `chat-composer-slash-menu.ts` / `chat-composer-skill-menu.ts` | autocomplete listbox (highlight ativo, não checked) | inalterado — família distinta por semântica |
| /new composer | `ui/src/pages/new-session/composer.ts` | reusa os pickers acima | herda tudo |
## Tokens escolhidos (`ui/src/styles/chat/layout.css`, bloco `--chat-composer-*`)
- `--chat-composer-menu-row-height: 40px` — antes: 40/44/36 conforme o menu.
- `--chat-composer-menu-row-padding: 6px 9px` — antes: `6px 10px`, `6px 9px`, `3px 7px`.
- `--chat-composer-menu-row-gap: 8px` — antes: 10/8/7.
- `--chat-composer-menu-row-radius: calc(10px * var(--openclaw-corner-radius-scale))` — antes: `--radius-sm`, `--radius-md`, `calc(10px*…)`.
- `--chat-composer-menu-icon: 16px` — ícones de row; muted por padrão (já era); permission usava 18px.
- `--chat-composer-menu-selected: color-mix(in srgb, var(--text) 8%, transparent)` — antes: 7%/8%/9% com receitas diferentes.
## Padrão de checked/selected
Único tratamento: **check accent à direita + fundo sutil** (`--chat-composer-menu-selected`) para single-selects; **switch** para binários. Check = caixa 16px com glifo 14px (`.chat-controls__inline-select-check` alinhado ao `.chat-talk-input-picker__check`).
## O que mudou
- `layout.css`: rows de model/permission/mic/attach/capability nos tokens acima; selected wash unificado; check 16/14; ícone da permission 18→16; permission rows 44→40; attach/capability rows 36→40 e `3px 7px`→token; removida regra morta `:has([slot="icon"]) { padding-inline-start: 7px }` (igual ao padding base); gap das model rows 7→8 alinhando com o provider heading (comentário do stem atualizado); `inline-select-section-label` virou variante inline do popover-title (type igual, padding 0 — o locked-model row é quem espaça); mic trigger `:focus-visible` de `outline 2px` para `box-shadow: var(--focus-ring)` (mesmo ring dos outros triggers); root do plus menu 184→208px para "Web search" + switch caberem em uma linha.
- `chat-composer-plus-menu.ts`: `renderCapabilityToggleRow` ganhou `icon?`; Web search deixou de ser checkbox e virou toggle row com globe + `wa-switch`.
## Hover vs focus / teclado
- Menus abertos por mouse: nenhum ring (regras `data-chat-pointer-opened-picker` / `data-chat-pointer-restored-focus` pré-existentes cobrem; hover = `--bg-hover`).
- Teclado: setas navegam (verificado no plus menu — ring visível só via `:focus-visible`), Enter seleciona, Esc fecha e devolve foco ao textarea (verificado: `document.activeElement` = TEXTAREA após Esc).
## Verificação
- Visual em `http://127.0.0.1:5230/chat?bench=default` via Playwright, dark e light: model, effort, permission, attach e capability menus (screenshots em `/tmp/herdr/shot-*.png`, todos inspecionados).
- `stylelint` no `layout.css`: mesmos 3 erros pré-existentes do HEAD (hex `#fff` ×2, duplicate selector) — nenhum novo.
- `tsgo:ui`: nenhum erro nos arquivos tocados (erros pré-existentes do checkpoint da outra lane em `chat-pane-session-controls.ts` etc. permanecem).
- `chat-view.test.ts`: 241 pass / 16 fail — idêntico com e sem a mudança (falhas pré-existentes no checkpoint).
## Follow-up: voz de label unificada (attach/capability vs permission)
- Causa da diferença apontada em review: `wa-dropdown-item::part(icon)` tem 24px intrínsecos (SVG 16px centrado → gap visual maior) e labels 12px/400 `--text` vs 13px/600 `--text-strong` do permission.
- Fix (`layout.css`): `::part(icon)` forçado a 16px nos attach/capability items; `::part(label)` = 13px/600 `--text-strong`; notes voltam a 11px/400 muted; mic device labels e model option titles alinhados ao mesmo 13px/600.
- Verificado visualmente (dark) em :5230 — attach/capability e permission agora com a mesma tipografia e mesmo trilho de ícone.
@@ -102,6 +102,7 @@ function renderCapabilityToggleRow(options: {
disabled: boolean;
title: string | null | undefined;
note?: TemplateResult | typeof nothing;
icon?: TemplateResult;
}) {
return html`
<wa-dropdown-item
@@ -110,6 +111,7 @@ function renderCapabilityToggleRow(options: {
?disabled=${options.disabled}
title=${options.title ?? ""}
>
${options.icon ? html`<span slot="icon" aria-hidden="true">${options.icon}</span>` : nothing}
<span class="agent-chat__capability-menu-label">
<span>${options.label}</span>
${options.note ?? nothing}
@@ -179,17 +181,14 @@ function renderRootView(props: ChatComposerPlusMenuProps) {
>
</span>
</wa-dropdown-item>
<wa-dropdown-item
class="agent-chat__capability-menu-item"
type="checkbox"
value="toggle-web-search"
.checked=${webSearchEnabled}
?disabled=${props.mutationBlockedReason !== null}
title=${props.mutationBlockedReason ?? ""}
>
<span slot="icon" aria-hidden="true">${icons.globe}</span>
<span>${t("chat.composer.menu.webSearch")}</span>
</wa-dropdown-item>
${renderCapabilityToggleRow({
value: "toggle-web-search",
label: t("chat.composer.menu.webSearch"),
checked: webSearchEnabled,
disabled: props.mutationBlockedReason !== null,
title: props.mutationBlockedReason,
icon: icons.globe,
})}
${menuDivider()}
<wa-dropdown-item class="agent-chat__capability-menu-item" value="manage-plugins">
<span slot="icon" aria-hidden="true">${icons.puzzle}</span>
+101 -70
View File
@@ -2941,6 +2941,17 @@ button.chat-pr__diff {
--chat-composer-chip-padding-inline: 8px;
--chat-composer-chip-text: 13px;
--chat-composer-chip-icon: 16px;
/* Menu-row family: every option row inside a composer popover (model, effort,
permission, plus menu, attach, microphone) shares one box, icon size, and
selected wash so the pickers read as one system rather than per-feature
designs. Selected rows also carry a trailing accent check where the row is
a single-select choice; binary rows carry a switch instead. */
--chat-composer-menu-row-height: 40px;
--chat-composer-menu-row-padding: 6px 9px;
--chat-composer-menu-row-gap: 8px;
--chat-composer-menu-row-radius: calc(10px * var(--openclaw-corner-radius-scale));
--chat-composer-menu-icon: 16px;
--chat-composer-menu-selected: color-mix(in srgb, var(--text) 8%, transparent);
/* Breathing room between the draft and the surface it sits in. Small on
purpose — it exists so the text never touches the frame, not to pad the
composer out — and owned here so the editor's inset is one decision for
@@ -3871,12 +3882,11 @@ button.chat-pr__diff {
box-sizing: border-box;
display: flex;
align-items: center;
gap: 8px;
gap: var(--chat-composer-menu-row-gap);
width: 100%;
min-height: 36px;
padding: 3px 7px;
padding-inline: 7px;
border-radius: calc(10px * var(--openclaw-corner-radius-scale));
min-height: var(--chat-composer-menu-row-height);
padding: var(--chat-composer-menu-row-padding);
border-radius: var(--chat-composer-menu-row-radius);
color: var(--text);
font-size: 12px;
}
@@ -3885,20 +3895,15 @@ button.chat-pr__diff {
color: var(--muted);
}
.agent-chat__attach-menu-option:has([slot="icon"]),
.agent-chat__capability-menu-item:has([slot="icon"]) {
padding-inline-start: 7px;
}
.agent-chat__attach-menu-option::part(base) {
box-sizing: border-box;
width: 100%;
}
.agent-chat__attach-menu-option svg {
width: 16px;
height: 16px;
flex: 0 0 16px;
width: var(--chat-composer-menu-icon);
height: var(--chat-composer-menu-icon);
flex: 0 0 var(--chat-composer-menu-icon);
fill: none;
stroke: currentColor;
stroke-width: 1.5px;
@@ -3924,9 +3929,10 @@ button.chat-pr__diff {
}
.agent-chat__capability-menu[data-view="root"]::part(menu) {
width: 184px;
min-width: 184px;
max-width: min(184px, calc(100vw - 24px));
/* Wide enough that "Web search" plus its trailing switch stays one line. */
width: 208px;
min-width: 208px;
max-width: min(208px, calc(100vw - 24px));
overflow-y: visible;
scrollbar-gutter: auto;
}
@@ -3945,12 +3951,11 @@ button.chat-pr__diff {
box-sizing: border-box;
display: flex;
align-items: center;
gap: 8px;
gap: var(--chat-composer-menu-row-gap);
width: 100%;
min-height: 36px;
padding: 3px 7px;
padding-inline: 7px;
border-radius: calc(10px * var(--openclaw-corner-radius-scale));
min-height: var(--chat-composer-menu-row-height);
padding: var(--chat-composer-menu-row-padding);
border-radius: var(--chat-composer-menu-row-radius);
}
.agent-chat__capability-menu-item,
@@ -3963,17 +3968,17 @@ button.chat-pr__diff {
.agent-chat__attach-menu-option::part(base) {
box-sizing: border-box;
width: 100%;
min-height: 36px;
padding: 3px 7px;
border-radius: calc(10px * var(--openclaw-corner-radius-scale));
min-height: var(--chat-composer-menu-row-height);
padding: var(--chat-composer-menu-row-padding);
border-radius: var(--chat-composer-menu-row-radius);
}
.agent-chat__capability-menu-item [slot="icon"],
.agent-chat__capability-menu-item svg,
.agent-chat__capability-menu-state svg {
width: 16px;
height: 16px;
flex: 0 0 16px;
width: var(--chat-composer-menu-icon);
height: var(--chat-composer-menu-icon);
flex: 0 0 var(--chat-composer-menu-icon);
}
.agent-chat__capability-menu-item [slot="icon"] {
@@ -4023,6 +4028,22 @@ button.chat-pr__diff {
color: var(--muted);
}
/* Row voice shared with the permission picker: 13/600 strong titles, muted 11px
notes, and a 16px icon part so the icon→label distance is the row gap alone
(wa-dropdown-item's default icon part is 24px wide, which read as a wider,
misaligned gap next to the custom-markup pickers). */
.agent-chat__attach-menu-option::part(icon),
.agent-chat__capability-menu-item::part(icon) {
width: var(--chat-composer-menu-icon);
}
.agent-chat__attach-menu-option::part(label),
.agent-chat__capability-menu-item::part(label) {
color: var(--text-strong);
font-size: 13px;
font-weight: 600;
}
.agent-chat__capability-menu-label {
display: flex;
flex: 1 1 auto;
@@ -4036,6 +4057,12 @@ button.chat-pr__diff {
color: var(--muted);
}
.agent-chat__capability-menu-note {
font-size: 11px;
font-weight: 400;
line-height: 1.25;
}
.agent-chat__capability-menu-state {
padding: 10px 12px;
line-height: 1.4;
@@ -4754,8 +4781,8 @@ button.chat-pr__diff {
}
.chat-talk-input-picker__trigger:focus-visible {
outline: 2px solid var(--ring);
outline-offset: 2px;
outline: none;
box-shadow: var(--focus-ring);
}
.chat-talk-input-picker__trigger svg {
@@ -4803,27 +4830,27 @@ button.chat-pr__diff {
}
.chat-talk-input-picker__item::part(base) {
min-height: 40px;
padding: 6px 9px;
border-radius: var(--radius-md);
min-height: var(--chat-composer-menu-row-height);
padding: var(--chat-composer-menu-row-padding);
border-radius: var(--chat-composer-menu-row-radius);
}
.chat-talk-input-picker__item[aria-checked="true"]::part(base) {
background: color-mix(in srgb, var(--text) 7%, transparent);
background: var(--chat-composer-menu-selected);
}
.chat-talk-input-picker__option-icon {
display: inline-flex;
width: 16px;
height: 16px;
width: var(--chat-composer-menu-icon);
height: var(--chat-composer-menu-icon);
align-items: center;
justify-content: center;
color: var(--muted);
}
.chat-talk-input-picker__option-icon svg {
width: 16px;
height: 16px;
width: var(--chat-composer-menu-icon);
height: var(--chat-composer-menu-icon);
fill: none;
stroke: currentColor;
stroke-width: 1.7px;
@@ -4832,6 +4859,9 @@ button.chat-pr__diff {
.chat-talk-input-picker__label {
min-width: 0;
overflow: hidden;
color: var(--text-strong);
font-size: 13px;
font-weight: 600;
text-overflow: ellipsis;
white-space: nowrap;
}
@@ -4840,8 +4870,8 @@ button.chat-pr__diff {
display: inline-flex;
align-items: center;
justify-content: center;
width: 16px;
height: 16px;
width: var(--chat-composer-menu-icon);
height: var(--chat-composer-menu-icon);
color: var(--accent);
}
@@ -6392,8 +6422,8 @@ button.chat-pr__diff {
}
.chat-controls__inline-select-check svg {
width: 100%;
height: 100%;
width: 14px;
height: 14px;
stroke: currentColor;
fill: none;
}
@@ -6402,12 +6432,12 @@ button.chat-pr__diff {
display: flex;
align-items: center;
justify-content: space-between;
gap: 10px;
gap: var(--chat-composer-menu-row-gap);
width: 100%;
min-height: 40px;
padding: 6px 10px;
min-height: var(--chat-composer-menu-row-height);
padding: var(--chat-composer-menu-row-padding);
border: 1px solid transparent;
border-radius: var(--radius-sm);
border-radius: var(--chat-composer-menu-row-radius);
background: transparent;
color: var(--text);
font: inherit;
@@ -6424,11 +6454,7 @@ button.chat-pr__diff {
.chat-controls__inline-select-option--selected {
color: var(--text-strong);
background: color-mix(
in srgb,
var(--chat-composer-popover-background, var(--bg-elevated)) 92%,
var(--text-strong) 8%
);
background: var(--chat-composer-menu-selected);
}
/* Model and effort are independent surfaces so either choice stays one focused
@@ -6462,11 +6488,11 @@ button.chat-pr__diff {
box-sizing: border-box;
display: flex;
align-items: center;
gap: 8px;
gap: var(--chat-composer-menu-row-gap);
width: 100%;
min-height: 44px;
padding: 6px 9px;
border-radius: calc(10px * var(--openclaw-corner-radius-scale));
min-height: var(--chat-composer-menu-row-height);
padding: var(--chat-composer-menu-row-padding);
border-radius: var(--chat-composer-menu-row-radius);
font-size: 12px;
}
@@ -6511,13 +6537,13 @@ button.chat-pr__diff {
.chat-controls__permission-option::part(base) {
align-items: center;
min-height: 44px;
padding: 6px 9px;
border-radius: var(--radius-md);
min-height: var(--chat-composer-menu-row-height);
padding: var(--chat-composer-menu-row-padding);
border-radius: var(--chat-composer-menu-row-radius);
}
.chat-controls__permission-option--selected::part(base) {
background: color-mix(in srgb, var(--text) 9%, transparent);
background: var(--chat-composer-menu-selected);
}
.chat-controls__permission-option-copy {
@@ -6528,8 +6554,8 @@ button.chat-pr__diff {
.chat-controls__permission-option-icon {
display: inline-flex;
width: 18px;
height: 18px;
width: var(--chat-composer-menu-icon);
height: var(--chat-composer-menu-icon);
color: var(--muted);
}
@@ -6639,12 +6665,13 @@ button.chat-pr__diff {
text-transform: uppercase;
}
/* Left padding is 9px, not the rows' 8px: option buttons add a 1px transparent
border, so 9 + 18 + 8 puts this label on the same stem as the model names. */
/* Left padding is 10px, not the rows' 9px: option buttons add a 1px transparent
border, so 10 + 16 + row gap puts this label on the same stem as the model
names (9 + 1 + 16 + gap). */
.chat-controls__provider-heading {
display: flex;
align-items: center;
gap: 8px;
gap: var(--chat-composer-menu-row-gap);
padding: 8px 10px 3px;
color: var(--muted);
font-size: 11px;
@@ -6697,12 +6724,15 @@ button.chat-pr__diff {
gap: 2px;
}
/* Inline variant of the popover title: it sits inside the locked-model row,
which owns the spacing, so the label carries type only. */
.chat-controls__inline-select-section-label {
padding: 3px 6px 2px;
padding: 0;
color: var(--muted);
font-size: 11px;
font-weight: 700;
letter-spacing: 0.08em;
line-height: 1.2;
text-transform: uppercase;
}
@@ -6722,9 +6752,6 @@ button.chat-pr__diff {
.chat-controls__model-option {
order: var(--chat-model-rank, 0);
justify-content: flex-start;
gap: 7px;
min-height: 40px;
padding: 6px 9px;
}
/* display: flex above beats the UA [hidden] style; without this, filtered-out
@@ -6809,7 +6836,7 @@ button.chat-pr__diff {
align-items: center;
gap: 0;
color: var(--text);
font-size: 12px;
font-size: 13px;
font-weight: 600;
}
@@ -7279,10 +7306,14 @@ button.chat-pr__diff {
opacity: 0.55;
}
/* Selected-state check shares the mic picker's box: a 16px slot centering a
14px accent glyph, so checks land on the same rail as 16px row icons. */
.chat-controls__inline-select-check {
display: inline-flex;
width: 14px;
height: 14px;
align-items: center;
justify-content: center;
width: var(--chat-composer-menu-icon);
height: var(--chat-composer-menu-icon);
color: var(--accent);
flex: 0 0 auto;
}