diff --git a/src/features/chat/ui/toolbar.ts b/src/features/chat/ui/toolbar.ts index bd178599..f5776f48 100644 --- a/src/features/chat/ui/toolbar.ts +++ b/src/features/chat/ui/toolbar.ts @@ -257,12 +257,12 @@ function renderToolbarPanel(toolbar: HTMLElement, model: ToolbarViewModel, actio function renderStatusPanel(parent: HTMLElement, model: ToolbarViewModel, actions: ToolbarActions): void { const statusItems = parent.createDiv({ cls: "codex-panel__status-panel-items", attr: { role: "menu" } }); - createToolbarPanelItem(statusItems, model.connectLabel, { onClick: actions.connect, className: "codex-panel__status-panel-item" }); - createToolbarPanelItem(statusItems, "Refresh diagnostics", { + createToolbarPanelRow(statusItems, model.connectLabel, { onClick: actions.connect, className: "codex-panel__status-panel-item" }); + createToolbarPanelRow(statusItems, "Refresh diagnostics", { onClick: actions.refreshDiagnostics, className: "codex-panel__status-panel-item", }); - createToolbarPanelItem(statusItems, "Refresh thread list", { + createToolbarPanelRow(statusItems, "Refresh thread list", { onClick: actions.refreshThreads, className: "codex-panel__status-panel-item", }); @@ -310,25 +310,21 @@ function renderConnectionDiagnostics(parent: HTMLElement, sections: ToolbarDiagn } function renderRuntimePicker(parent: HTMLElement, model: ToolbarViewModel): void { - const picker = parent.createDiv({ cls: "codex-panel__runtime-picker", attr: { role: "listbox", "aria-label": "Runtime controls" } }); + const picker = parent.createDiv({ cls: "codex-panel__runtime-picker", attr: { role: "listbox" } }); picker.createDiv({ cls: "codex-panel__runtime-picker-label", text: "Reasoning effort" }); for (const choice of model.effortChoices) { - createToolbarPanelItem(picker, choice.label, { ...choice, className: "codex-panel__runtime-choice", highlightSelected: false }); + createToolbarPanelRow(picker, choice.label, { ...choice, className: "codex-panel__runtime-choice" }); } picker.createDiv({ cls: "codex-panel__runtime-picker-label", text: "Model" }); for (const choice of model.modelChoices) { - createToolbarPanelItem(picker, choice.label, { ...choice, className: "codex-panel__runtime-choice", highlightSelected: false }); + createToolbarPanelRow(picker, choice.label, { ...choice, className: "codex-panel__runtime-choice" }); } } function renderThreadList(parent: HTMLElement, threads: ToolbarThreadRow[], actions: ToolbarActions): void { const threadsEl = parent.createDiv({ cls: "codex-panel__threads" }); if (threads.length === 0) { - const empty = threadsEl.createDiv({ - cls: "menu-item codex-panel__toolbar-panel-item codex-panel__thread codex-panel__thread--empty is-disabled", - }); - empty.createSpan({ cls: "menu-item-icon codex-panel__toolbar-panel-check" }); - empty.createSpan({ cls: "menu-item-title codex-panel__toolbar-panel-label", text: "No threads" }); + createToolbarPanelRow(threadsEl, "No threads", { disabled: true, className: "codex-panel__thread codex-panel__thread--empty" }); return; } @@ -341,7 +337,7 @@ function renderThreadList(parent: HTMLElement, threads: ToolbarThreadRow[], acti continue; } - createToolbarPanelItem(row, thread.title, { + createToolbarPanelRow(row, thread.title, { selected: thread.selected, disabled: thread.disabled, title: `${thread.title}\n${thread.threadId}`, @@ -374,7 +370,7 @@ function renderThreadList(parent: HTMLElement, threads: ToolbarThreadRow[], acti function renderThreadRenameRow(parent: HTMLElement, thread: ToolbarThreadRow, actions: ToolbarActions): void { const form = parent.createDiv({ cls: "menu-item codex-panel__toolbar-panel-item codex-panel__thread codex-panel__thread-rename" }); form.createSpan({ - cls: "menu-item-icon codex-panel__toolbar-panel-check codex-panel__thread-rename-spacer", + cls: "codex-panel__toolbar-panel-check codex-panel__thread-rename-spacer", attr: { "aria-hidden": "true" }, }); const field = form.createDiv({ cls: "codex-panel__thread-rename-field" }); @@ -431,7 +427,7 @@ function renderThreadRenameRow(parent: HTMLElement, thread: ToolbarThreadRow, ac }; } -function createToolbarPanelItem( +function createToolbarPanelRow( parent: HTMLElement, label: string, options: { @@ -440,37 +436,34 @@ function createToolbarPanelItem( title?: string; meta?: string; className?: string; - highlightSelected?: boolean; onClick?: () => void; } = {}, ): HTMLElement { const selected = Boolean(options.selected); const disabled = Boolean(options.disabled); - const highlightSelected = options.highlightSelected ?? true; - const item = parent.createEl("button", { - cls: [ - "menu-item", - "tappable", - "codex-panel__toolbar-panel-item", - options.className ?? "", - selected && highlightSelected ? "selected is-selected" : "", - disabled ? "is-disabled" : "", - ] + const item = parent.createDiv({ + cls: ["codex-panel__toolbar-panel-item", options.className ?? "", selected ? "is-checked" : "", disabled ? "is-disabled" : ""] .filter(Boolean) .join(" "), attr: { - type: "button", + role: "button", + tabindex: disabled ? "-1" : "0", title: options.title ?? label, + "aria-disabled": disabled ? "true" : "false", "aria-selected": selected ? "true" : "false", }, }); - item.disabled = disabled; item.onclick = () => { if (!disabled) options.onClick?.(); }; - const check = item.createSpan({ cls: "menu-item-icon codex-panel__toolbar-panel-check" }); + item.onkeydown = (event) => { + if (disabled || (event.key !== "Enter" && event.key !== " ")) return; + event.preventDefault(); + options.onClick?.(); + }; + const check = item.createSpan({ cls: "codex-panel__toolbar-panel-check" }); if (selected) setIcon(check, "check"); - item.createSpan({ cls: "menu-item-title codex-panel__toolbar-panel-label", text: label }); + item.createSpan({ cls: "codex-panel__toolbar-panel-label", text: label }); if (options.meta) item.createSpan({ cls: "codex-panel__toolbar-panel-meta", text: options.meta }); return item; } diff --git a/styles.css b/styles.css index 48ccc450..dc0e11f8 100644 --- a/styles.css +++ b/styles.css @@ -490,13 +490,13 @@ gap: var(--codex-panel-toolbar-panel-row-gap); } -.codex-panel .menu-item.codex-panel__toolbar-panel-item { +.codex-panel__toolbar-panel-item { display: flex; align-items: center; gap: var(--size-4-2); width: 100%; min-width: 0; - height: auto; + min-height: var(--codex-panel-size-nav-item); padding: var(--size-4-1) var(--size-4-2); border: 0; border-radius: var(--radius-s); @@ -509,12 +509,18 @@ line-height: var(--codex-panel-toolbar-line-height); text-align: left; cursor: pointer; + transform: none; user-select: none; } .codex-panel__toolbar-panel-check { flex: 0 0 auto; + display: inline-flex; + align-items: center; + justify-content: center; min-width: var(--codex-panel-size-icon-s); + width: var(--codex-panel-size-icon-s); + height: var(--codex-panel-size-icon-s); color: var(--text-accent); } @@ -539,23 +545,28 @@ font-size: var(--codex-panel-toolbar-meta-size); } -.codex-panel .menu-item.codex-panel__toolbar-panel-item:hover:not(:where(:disabled, .is-disabled)), -.codex-panel .menu-item.codex-panel__toolbar-panel-item.selected, -.codex-panel .menu-item.codex-panel__toolbar-panel-item.is-selected, -.codex-panel .menu-item.codex-panel__toolbar-panel-item.is-active { +.codex-panel__toolbar-panel-item:hover:not(:where(:disabled, .is-disabled)) { color: var(--text-normal); -} - -.codex-panel .menu-item.codex-panel__toolbar-panel-item:hover:not(:where(:disabled, .is-disabled)) { background: var(--background-modifier-hover); + box-shadow: none; + transform: none; } -.codex-panel .menu-item.codex-panel__toolbar-panel-item.is-disabled { +.codex-panel__toolbar-panel-item:focus, +.codex-panel__toolbar-panel-item:focus-visible, +.codex-panel__toolbar-panel-item:active { + background: transparent; + box-shadow: none; + outline: none; + transform: none; +} + +.codex-panel__toolbar-panel-item.is-disabled { cursor: default; opacity: 0.45; } -.codex-panel .menu-item.codex-panel__toolbar-panel-item:disabled { +.codex-panel__toolbar-panel-item:disabled { cursor: default; opacity: 0.45; } @@ -1123,16 +1134,14 @@ grid-template-columns: minmax(0, 1fr) auto auto auto; } -.codex-panel .menu-item.codex-panel__thread:hover:not(:where(:disabled, .is-disabled)), -.codex-panel .menu-item.codex-panel__thread.selected, -.codex-panel .menu-item.codex-panel__thread.is-selected { +.codex-panel__thread:hover:not(:where(:disabled, .is-disabled)), +.codex-panel__thread.is-checked { background: transparent; } .codex-panel .menu-item.codex-panel__thread-rename { position: relative; box-sizing: border-box; - height: auto; min-height: var(--codex-panel-size-nav-item); padding-block: var(--size-4-1); cursor: text; diff --git a/tests/features/chat/ui/view-renderers.test.ts b/tests/features/chat/ui/view-renderers.test.ts index e6c0a016..453e60e3 100644 --- a/tests/features/chat/ui/view-renderers.test.ts +++ b/tests/features/chat/ui/view-renderers.test.ts @@ -1783,8 +1783,8 @@ describe("toolbar renderer decisions", () => { expect(parent.textContent).toContain("Refresh diagnostics"); expect(parent.textContent).toContain("codex-cli/1.2.3"); expect(parent.querySelector(".codex-panel__connection-diagnostics-row--error")?.textContent).toContain("model/list failed"); - [...parent.querySelectorAll(".codex-panel__status-panel-item")] - .find((button) => button.textContent.includes("Refresh diagnostics")) + [...parent.querySelectorAll(".codex-panel__status-panel-item")] + .find((item) => item.textContent.includes("Refresh diagnostics")) ?.click(); expect(refreshDiagnostics).toHaveBeenCalled(); });