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>
103 lines
3.4 KiB
TypeScript
103 lines
3.4 KiB
TypeScript
/**
|
|
* Web pairing screen — ticket #13, lot F2.
|
|
*
|
|
* Shown by {@link WebApp} when the client is not paired. The user types the code
|
|
* the server printed at first launch; on submit we `POST /api/pair {code}` via
|
|
* the {@link WebSession}. On success the server sets the HttpOnly session cookie
|
|
* and we advance to the workspace; a wrong code shows a clear message.
|
|
*
|
|
* Transport-neutral at the component seam: it talks to the injected
|
|
* {@link WebSession}, never to `@tauri-apps/api` (the CI guard `no-direct-invoke`
|
|
* stays green). Pure web-only UI — desktop never mounts it.
|
|
*/
|
|
|
|
import { useState, type FormEvent } from "react";
|
|
|
|
import type { GatewayError } from "@/domain";
|
|
import { Button, Field, Input, Panel } from "@/shared";
|
|
import type { WebSession } from "@/adapters/http";
|
|
|
|
interface PairingScreenProps {
|
|
/** The shared web session performing the `POST /api/pair` handshake. */
|
|
session: WebSession;
|
|
/** Called once pairing succeeds (cookie set) so the app can advance. */
|
|
onPaired: () => void;
|
|
}
|
|
|
|
function describe(e: unknown): string {
|
|
if (e && typeof e === "object" && "message" in e) {
|
|
return String((e as GatewayError).message);
|
|
}
|
|
return String(e);
|
|
}
|
|
|
|
export function PairingScreen({ session, onPaired }: PairingScreenProps) {
|
|
const [code, setCode] = useState("");
|
|
const [error, setError] = useState<string | null>(null);
|
|
const [busy, setBusy] = useState(false);
|
|
|
|
async function submit(e: FormEvent): Promise<void> {
|
|
e.preventDefault();
|
|
const trimmed = code.trim();
|
|
if (!trimmed || busy) return;
|
|
setBusy(true);
|
|
setError(null);
|
|
try {
|
|
await session.pair(trimmed);
|
|
onPaired();
|
|
} catch (err) {
|
|
setError(describe(err));
|
|
} finally {
|
|
setBusy(false);
|
|
}
|
|
}
|
|
|
|
return (
|
|
<div className="flex h-full items-center justify-center overflow-y-auto bg-canvas p-4 sm:p-6">
|
|
<Panel className="w-full max-w-sm">
|
|
<form onSubmit={submit} className="flex flex-col gap-4">
|
|
<div className="flex flex-col gap-1">
|
|
<h1 className="text-lg font-semibold tracking-tight">Appairer cet appareil</h1>
|
|
<p className="text-sm text-muted">
|
|
Saisissez le code affiché par le serveur IdeA pour connecter ce
|
|
navigateur.
|
|
</p>
|
|
</div>
|
|
|
|
<Field label="Code d'appairage">
|
|
{({ id, describedBy }) => (
|
|
<Input
|
|
id={id}
|
|
aria-describedby={describedBy}
|
|
value={code}
|
|
onChange={(e) => setCode(e.target.value)}
|
|
placeholder="p. ex. 4821-93"
|
|
autoFocus
|
|
autoComplete="one-time-code"
|
|
// #69 — phones: summon the numeric-ish keypad for a pairing code
|
|
// and keep the OS from "helpfully" capitalising/correcting it.
|
|
inputMode="numeric"
|
|
autoCapitalize="off"
|
|
autoCorrect="off"
|
|
spellCheck={false}
|
|
disabled={busy}
|
|
invalid={!!error}
|
|
/>
|
|
)}
|
|
</Field>
|
|
|
|
{error && (
|
|
<p role="alert" data-testid="pairing-error" className="text-sm text-danger">
|
|
{error}
|
|
</p>
|
|
)}
|
|
|
|
<Button type="submit" disabled={busy || code.trim().length === 0}>
|
|
{busy ? "Appairage…" : "Appairer"}
|
|
</Button>
|
|
</form>
|
|
</Panel>
|
|
</div>
|
|
);
|
|
}
|