feat(ui): uniformise les droplist dynamiques sur le style du picker d'agent

Introduit SmallDropdown comme composant partagé et le fait adopter par
AgentsPanel, ModelServerSelect, OpenCodeModeFields, TemplateEditor et
TicketViewportSelect, pour que toutes les listes déroulantes dynamiques
(agent, modèle, template, viewport) partagent la même apparence que le
sélecteur d'agent de la création de ticket.

Refs #114

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-30 12:55:57 +02:00
parent fda4126a5f
commit 9b1ced50be
14 changed files with 550 additions and 360 deletions

View File

@ -8,7 +8,7 @@ import type {
OpenCodeProviderCatalogEntry,
} from "@/domain";
import { useGateways } from "@/app/di";
import { Button, IconButton, Input, cn } from "@/shared";
import { Button, IconButton, Input, SmallDropdown, cn } from "@/shared";
import { ModelServerSelect } from "@/features/model-servers";
import {
defaultOpenCodeConfig,
@ -300,12 +300,11 @@ function OpenCodeProviderFields({
className="h-8 w-full rounded-md border border-border bg-raised px-3 text-xs text-content outline-none"
/>
)}
<select
<SmallDropdown
aria-label={`${profile.name} provider`}
value={providerId}
disabled={!catalogReady}
onChange={(e) => {
const v = e.target.value;
onChange={(v) => {
if (v === CUSTOM_PROVIDER_VALUE) {
setMode("custom");
setProviderId("");
@ -316,22 +315,27 @@ function OpenCodeProviderFields({
}
setFieldErrors((prev) => ({ ...prev, providerId: undefined }));
}}
className={cn(
"h-9 w-full rounded-md border bg-raised px-3 text-sm text-content outline-none",
"disabled:cursor-not-allowed disabled:opacity-50",
fieldErrors.providerId ? "border-danger" : "border-border",
)}
>
<option value="" disabled>
{catalog.loading ? "Chargement des providers…" : "Choisir un provider…"}
</option>
{filteredProviders.map((p) => (
<option key={p.providerId} value={p.providerId}>
{p.displayName}
</option>
))}
<option value={CUSTOM_PROVIDER_VALUE}>Autre / personnalisé</option>
</select>
className="w-full"
size="md"
invalid={Boolean(fieldErrors.providerId)}
options={[
{
value: "",
label: catalog.loading
? "Chargement des providers…"
: "Choisir un provider…",
disabled: true,
},
...filteredProviders.map((p) => ({
value: p.providerId,
label: p.displayName,
})),
{
value: CUSTOM_PROVIDER_VALUE,
label: "Autre / personnalisé…",
},
]}
/>
{fieldErrors.providerId && (
<small className="text-xs text-danger">{fieldErrors.providerId}</small>
)}
@ -339,29 +343,26 @@ function OpenCodeProviderFields({
<label className="flex flex-col gap-1">
<Caption>Modèle</Caption>
<select
<SmallDropdown
aria-label={`${profile.name} model`}
value={model}
disabled={providerId.length === 0}
onChange={(e) => {
setModel(e.target.value);
onChange={(next) => {
setModel(next);
setFieldErrors((prev) => ({ ...prev, model: undefined }));
}}
className={cn(
"h-9 w-full rounded-md border bg-raised px-3 text-sm text-content outline-none",
"disabled:cursor-not-allowed disabled:opacity-50",
fieldErrors.model ? "border-danger" : "border-border",
)}
>
<option value="" disabled>
{providerId.length === 0 ? "—" : "Choisir un modèle…"}
</option>
{models.map((m) => (
<option key={m} value={m}>
{m}
</option>
))}
</select>
className="w-full"
size="md"
invalid={Boolean(fieldErrors.model)}
options={[
{
value: "",
label: providerId.length === 0 ? "—" : "Choisir un modèle…",
disabled: true,
},
...models.map((m) => ({ value: m, label: m })),
]}
/>
{fieldErrors.model && (
<small className="text-xs text-danger">{fieldErrors.model}</small>
)}