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

@ -10,12 +10,13 @@
import { useEffect, useState } from "react";
import type { Sprint, TicketPriority, TicketSummary } from "@/domain";
import { Button, Input, Panel, Spinner, cn } from "@/shared";
import { Button, Input, Panel, Spinner } from "@/shared";
import { useTickets } from "./useTickets";
import { useProjectAgents } from "./useProjectAgents";
import { SprintManager } from "./SprintManager";
import { SprintPicker } from "./SprintPicker";
import { TicketFacetsBar } from "./TicketFacetsBar";
import { TicketViewportSelect } from "./TicketViewportSelect";
import {
PriorityBadge,
StatusBadge,
@ -24,12 +25,6 @@ import {
priorityLabel,
} from "./ticketMeta";
const selectClass = cn(
"h-8 rounded-md bg-raised px-2 text-xs text-content",
"border border-border outline-none transition-colors",
"focus:border-primary disabled:cursor-not-allowed disabled:opacity-50",
);
export interface TicketsPanelProps {
projectId: string;
/** Opens the detail overlay for the given `#ref` (F7). */
@ -151,18 +146,15 @@ export function TicketsPanel({ projectId, onOpen }: TicketsPanelProps) {
onChange={(e) => setNewTitle(e.target.value)}
/>
<div className="flex items-center gap-2">
<select
<TicketViewportSelect
aria-label="new ticket priority"
className={selectClass}
value={newPriority}
onChange={(e) => setNewPriority(e.target.value as TicketPriority)}
>
{TICKET_PRIORITIES.map((p) => (
<option key={p} value={p}>
{priorityLabel(p)}
</option>
))}
</select>
options={TICKET_PRIORITIES.map((priority) => ({
value: priority,
label: priorityLabel(priority),
}))}
onChange={(next) => setNewPriority(next as TicketPriority)}
/>
<Button
type="submit"
size="sm"
@ -211,24 +203,23 @@ export function TicketsPanel({ projectId, onOpen }: TicketsPanelProps) {
}}
/>
<div className="flex flex-wrap items-center gap-2">
<select
<TicketViewportSelect
aria-label="filter by assignee"
className={selectClass}
value={vm.query.assignedAgentId ?? ""}
onChange={(e) =>
options={[
{ value: "", label: "All assignees" },
...agents.map((agent) => ({
value: agent.id,
label: agent.name,
})),
]}
onChange={(next) =>
vm.setQuery({
...vm.query,
assignedAgentId: e.target.value || undefined,
assignedAgentId: next || undefined,
})
}
>
<option value="">All assignees</option>
{agents.map((a) => (
<option key={a.id} value={a.id}>
{a.name}
</option>
))}
</select>
/>
</div>
</div>
@ -363,21 +354,20 @@ function SprintSection({
</span>
</div>
<div className="mt-1 pl-6">
<select
<TicketViewportSelect
aria-label={`sprint for ${t.ref}`}
className={selectClass}
value={t.sprintId ?? ""}
onChange={(e) =>
onAssignSprint(t.ref, e.target.value || null)
options={[
{ value: "", label: "— No sprint —" },
...sprints.map((sprint) => ({
value: sprint.id,
label: sprint.name,
})),
]}
onChange={(next) =>
onAssignSprint(t.ref, next || null)
}
>
<option value=""> No sprint </option>
{sprints.map((s) => (
<option key={s.id} value={s.id}>
{s.name}
</option>
))}
</select>
/>
</div>
</li>
))}