feat(frontend): shell web responsive pour viewport téléphone (#69)

Lots 1 et 2 du ticket #69 : rendre le client web (#13) utilisable sur un
téléphone. Le client web n'a jamais eu de docks ni de fenêtres flottantes —
il est déjà une colonne verticale unique — donc le lot 2 se réduit aux
rangées qui débordaient réellement à 360px.

- `100dvh` sur html/body/#root : `height: 100%` se résout sur le *large*
  viewport, donc la barre d'URL rétractable d'un navigateur mobile masquait
  le bas de l'app. Desktop inchangé (dynamic == large viewport).
- `viewport-fit=cover` + padding `env(safe-area-inset-*)` sur le header et
  le workspace : l'app peut peindre sous une encoche sans perdre ses
  contrôles. Le zoom reste libre (accessibilité).
- Padding responsive (`p-4 sm:p-6`) : 48px de gouttière sur 360px, c'était
  13% de la largeur.
- `AgentLiveRow` et la rangée de tâche de fond empilent leurs contrôles sur
  téléphone (nom / badges / actions) et retrouvent leur ligne unique dès
  `sm` — à 360px les boutons Cancel+Retry ne tenaient pas.
- Code d'appairage : `inputMode="numeric"`, pas d'autocapitalisation.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-16 13:27:56 +02:00
parent fe0e53ec59
commit 48b8853214
5 changed files with 75 additions and 29 deletions

View File

@ -75,7 +75,10 @@ export function WebWorkspace() {
);
return (
<div className="flex h-full flex-col gap-4 overflow-y-auto p-6">
<div
data-testid="web-workspace"
className="flex h-full flex-col gap-4 overflow-y-auto p-4 pb-[max(1rem,env(safe-area-inset-bottom))] pl-[max(1rem,env(safe-area-inset-left))] pr-[max(1rem,env(safe-area-inset-right))] sm:p-6"
>
<ReconnectBanner />
<div className="flex items-center justify-between">
<h2 className="text-base font-semibold tracking-tight">Projets</h2>
@ -223,7 +226,11 @@ function AgentLiveRow({
return (
<li className="py-2 first:pt-0 last:pb-0">
<div className="flex items-center justify-between gap-2">
{/* #69 — at 360px the name + two badges + the button no longer fit on one
line, so the name takes its own row on phones and the status cluster
wraps under it. From `sm` up this collapses back to the original
single-line desktop layout. */}
<div className="flex flex-col gap-1.5 sm:flex-row sm:items-center sm:justify-between sm:gap-2">
<span className="min-w-0 truncate text-sm text-content">{agent.name}</span>
<span className="flex shrink-0 items-center gap-1.5 text-xs">
<span className={cn("rounded-full px-2 py-0.5 font-medium", live ? "bg-success/15 text-success" : "bg-raised text-muted")}>
@ -311,29 +318,36 @@ function WebBackgroundTaskRow({
return (
<li className="min-w-0 text-xs text-muted">
<div className="flex min-w-0 items-center gap-2">
<span className={cn("shrink-0 rounded-full px-1.5 py-0.5 font-medium", taskStatusClass(task.status))}>
{TASK_STATUS_LABEL[task.status]}
</span>
<span className="min-w-0 flex-1 truncate text-content">{task.kind}</span>
<Button
size="sm"
variant="ghost"
disabled={!canCancel || actionBusy}
loading={actionBusy}
onClick={() => void runAction((id) => workState.cancelBackgroundTask(id))}
>
Cancel
</Button>
<Button
size="sm"
variant="ghost"
disabled={!canRetry || actionBusy}
loading={actionBusy}
onClick={() => void runAction((id) => workState.retryBackgroundTask(id))}
>
Retry
</Button>
{/* #69 — status + kind + Cancel + Retry overflow a phone row. On narrow
screens the status/kind pair keeps the first line and the two actions
wrap onto a second, right-aligned one; `sm` restores the single row. */}
<div className="flex min-w-0 flex-col gap-1 sm:flex-row sm:items-center sm:gap-2">
<div className="flex min-w-0 items-center gap-2">
<span className={cn("shrink-0 rounded-full px-1.5 py-0.5 font-medium", taskStatusClass(task.status))}>
{TASK_STATUS_LABEL[task.status]}
</span>
<span className="min-w-0 flex-1 truncate text-content">{task.kind}</span>
</div>
<div className="flex shrink-0 items-center justify-end gap-1">
<Button
size="sm"
variant="ghost"
disabled={!canCancel || actionBusy}
loading={actionBusy}
onClick={() => void runAction((id) => workState.cancelBackgroundTask(id))}
>
Cancel
</Button>
<Button
size="sm"
variant="ghost"
disabled={!canRetry || actionBusy}
loading={actionBusy}
onClick={() => void runAction((id) => workState.retryBackgroundTask(id))}
>
Retry
</Button>
</div>
</div>
{message && <p role="alert" className="mt-1 text-danger">{message}</p>}
</li>