Introduce toolbar panel rows

This commit is contained in:
murashit 2026-05-25 22:30:20 +09:00
parent 5819414e28
commit 2f56caf79c
3 changed files with 48 additions and 46 deletions

View file

@ -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;
}

View file

@ -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;

View file

@ -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<HTMLButtonElement>(".codex-panel__status-panel-item")]
.find((button) => button.textContent.includes("Refresh diagnostics"))
[...parent.querySelectorAll<HTMLElement>(".codex-panel__status-panel-item")]
.find((item) => item.textContent.includes("Refresh diagnostics"))
?.click();
expect(refreshDiagnostics).toHaveBeenCalled();
});