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>
79 lines
2.9 KiB
TypeScript
79 lines
2.9 KiB
TypeScript
/**
|
|
* Web client root — ticket #13, lot F2. Mounted (instead of the desktop `App`)
|
|
* only when the HTTP transport is selected (`VITE_TRANSPORT="http"`).
|
|
*
|
|
* Routes on the {@link WebSession} paired flag: not paired ⇒ {@link PairingScreen};
|
|
* paired ⇒ the read-only {@link WebWorkspace}. It subscribes to the session's
|
|
* `onUnauthorized` signal so a `401` from any `/api/invoke` (expired/missing
|
|
* cookie) drops back to pairing automatically. "Se déconnecter" (F6) revokes the
|
|
* server session (`POST /api/logout`), tears down the live WS singleton, and
|
|
* returns to pairing.
|
|
*/
|
|
|
|
import { useEffect, useState } from "react";
|
|
|
|
import { Button } from "@/shared";
|
|
import { disconnectWebLive, getWebSession, type WebSession } from "@/adapters/http";
|
|
import { PairingScreen } from "./PairingScreen";
|
|
import { WebWorkspace } from "./WebWorkspace";
|
|
|
|
interface WebAppProps {
|
|
/** Injectable session (tests); defaults to the shared singleton. */
|
|
session?: WebSession;
|
|
}
|
|
|
|
export function WebApp({ session }: WebAppProps = {}) {
|
|
const webSession = session ?? getWebSession();
|
|
const [paired, setPaired] = useState(() => webSession.isPaired());
|
|
const [signingOut, setSigningOut] = useState(false);
|
|
|
|
useEffect(() => {
|
|
// A 401 anywhere clears the flag and fires this: return to pairing. The live
|
|
// WS is torn down by the composition-root 401 handler (F6).
|
|
return webSession.onUnauthorized(() => setPaired(false));
|
|
}, [webSession]);
|
|
|
|
async function signOut(): Promise<void> {
|
|
if (signingOut) return;
|
|
setSigningOut(true);
|
|
try {
|
|
// Revoke the server session (best-effort), then drop the live socket so it
|
|
// stops reconnecting, and return to pairing regardless of the outcome.
|
|
await webSession.logout();
|
|
} finally {
|
|
disconnectWebLive();
|
|
setSigningOut(false);
|
|
setPaired(false);
|
|
}
|
|
}
|
|
|
|
return (
|
|
<div className="flex h-full flex-col bg-canvas text-content">
|
|
<header
|
|
data-testid="web-header"
|
|
className="flex shrink-0 items-center justify-between border-b border-border px-4 py-3 pt-[max(0.75rem,env(safe-area-inset-top))] pl-[max(1rem,env(safe-area-inset-left))] pr-[max(1rem,env(safe-area-inset-right))] sm:px-6"
|
|
>
|
|
<div className="flex items-baseline gap-2">
|
|
<h1 className="text-base font-semibold tracking-tight sm:text-lg">IdeA</h1>
|
|
<span className="rounded-md bg-raised px-1.5 py-0.5 text-[0.65rem] font-medium uppercase text-muted">
|
|
web
|
|
</span>
|
|
</div>
|
|
{paired && (
|
|
<Button variant="ghost" size="sm" loading={signingOut} onClick={() => void signOut()}>
|
|
Se déconnecter
|
|
</Button>
|
|
)}
|
|
</header>
|
|
|
|
<div className="flex flex-1 flex-col overflow-hidden">
|
|
{paired ? (
|
|
<WebWorkspace />
|
|
) : (
|
|
<PairingScreen session={webSession} onPaired={() => setPaired(true)} />
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|