fix(tickets): dropdowns ne sortent plus du viewport en bas d'écran (#3)

Remplace les <select> natifs de la vue Tickets (statut, priorité, tri,
lien, agent assigné/assistant, sprint) par TicketViewportSelect, qui
repositionne intelligemment son overlay selon l'espace disponible dans
la fenêtre au lieu de toujours ouvrir vers le bas.

QA vert : 80/80 (frontend/src/features/tickets), tsc --noEmit propre.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-28 18:29:17 +02:00
parent 454f8ad57d
commit 05402c7544
8 changed files with 416 additions and 157 deletions

View File

@ -17,6 +17,7 @@ import { useTicketDetail } from "./useTicketDetail";
import { useProjectAgents } from "./useProjectAgents";
import { TicketAssistantPanel } from "./TicketAssistantPanel";
import { TicketPicker } from "./TicketPicker";
import { TicketViewportSelect } from "./TicketViewportSelect";
import {
PriorityBadge,
StatusBadge,
@ -30,12 +31,6 @@ import {
statusLabel,
} from "./ticketMeta";
const selectClass = cn(
"h-9 rounded-md bg-raised px-3 text-sm text-content",
"border border-border outline-none transition-colors",
"focus:border-primary disabled:cursor-not-allowed disabled:opacity-50",
);
const textareaClass = cn(
"w-full rounded-md bg-raised p-3 text-sm text-content",
"border border-border outline-none transition-colors",
@ -320,44 +315,38 @@ export function TicketDetail({
<div className="flex flex-wrap items-center gap-3">
<label className="flex items-center gap-2 text-xs text-muted">
Status
<select
<TicketViewportSelect
aria-label="ticket status"
className={selectClass}
value={t.status}
disabled={vm.busy}
onChange={(e) =>
options={TICKET_STATUSES.map((status) => ({
value: status,
label: statusLabel(status),
}))}
onChange={(next) =>
void vm.updateFields({
status: e.target.value as (typeof TICKET_STATUSES)[number],
status: next as (typeof TICKET_STATUSES)[number],
})
}
>
{TICKET_STATUSES.map((s) => (
<option key={s} value={s}>
{statusLabel(s)}
</option>
))}
</select>
/>
</label>
<label className="flex items-center gap-2 text-xs text-muted">
Priority
<select
<TicketViewportSelect
aria-label="ticket priority"
className={selectClass}
value={t.priority}
disabled={vm.busy}
onChange={(e) =>
options={TICKET_PRIORITIES.map((priority) => ({
value: priority,
label: priorityLabel(priority),
}))}
onChange={(next) =>
void vm.updateFields({
priority:
e.target.value as (typeof TICKET_PRIORITIES)[number],
next as (typeof TICKET_PRIORITIES)[number],
})
}
>
{TICKET_PRIORITIES.map((p) => (
<option key={p} value={p}>
{priorityLabel(p)}
</option>
))}
</select>
/>
</label>
</div>
</Section>
@ -426,19 +415,16 @@ export function TicketDetail({
{/* Add a link via the TicketPicker popup (#17): the kind is chosen
here, the target ticket is selected in the popup (no manual #id). */}
<div className="mt-1 flex flex-wrap items-center gap-2">
<select
<TicketViewportSelect
aria-label="link kind"
className={selectClass}
value={linkKind}
disabled={vm.busy}
onChange={(e) => setLinkKind(e.target.value as TicketLinkKind)}
>
{TICKET_LINK_KINDS.map((k) => (
<option key={k} value={k}>
{linkKindLabel(k)}
</option>
))}
</select>
options={TICKET_LINK_KINDS.map((kind) => ({
value: kind,
label: linkKindLabel(kind),
}))}
onChange={(next) => setLinkKind(next as TicketLinkKind)}
/>
<Button
size="sm"
aria-label="add link"
@ -479,24 +465,25 @@ export function TicketDetail({
</ul>
)}
<div className="mt-1 flex items-center gap-2">
<select
<TicketViewportSelect
aria-label="assign agent"
className={selectClass}
value={assignPick}
disabled={vm.busy || assignable.length === 0}
onChange={(e) => setAssignPick(e.target.value)}
>
<option value="">
{assignable.length === 0
? "No more agents"
: "Select an agent"}
</option>
{assignable.map((a) => (
<option key={a.id} value={a.id}>
{a.name}
</option>
))}
</select>
options={[
{
value: "",
label:
assignable.length === 0
? "No more agents"
: "Select an agent…",
},
...assignable.map((agent) => ({
value: agent.id,
label: agent.name,
})),
]}
onChange={setAssignPick}
/>
<Button
size="sm"
disabled={!assignPick || vm.busy}