Fit the login form to the picture's box

The form now sits in the same 16:9 box as the snapshot, so a card keeps
its height whichever it shows. A compact variant drops the aside about
which user to create and tightens the spacing, since that space is fixed;
the camera page keeps the full form, where there is room. Anything that
still doesn't fit scrolls inside the box rather than stretching the card.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Michael Mainguy 2026-09-20 07:28:43 -05:00
parent 6a663a5d82
commit f953f38716
4 changed files with 30 additions and 12 deletions

View File

@ -270,6 +270,10 @@ describe("CameraCard", () => {
expect(screen.queryByRole("link", { name: "Pop out Porch" })).toBeNull(); expect(screen.queryByRole("link", { name: "Pop out Porch" })).toBeNull();
// The status still says what is wrong, where the picture would have been. // The status still says what is wrong, where the picture would have been.
expect(container.querySelector("[data-status='auth']")).toBeTruthy(); expect(container.querySelector("[data-status='auth']")).toBeTruthy();
// Same box as the picture, and the aside that wouldn't fit is left out.
expect(container.querySelector(".aspect-video")).toBeTruthy();
expect(container.textContent).not.toContain("Use the ONVIF user you created");
}); });
it("keeps a username the user typed over the saved one", async () => { it("keeps a username the user typed over the saved one", async () => {

View File

@ -178,14 +178,17 @@ export default function CameraCard({
<li className="flex flex-col gap-3 rounded-lg border border-zinc-200 p-4 dark:border-zinc-800"> <li className="flex flex-col gap-3 rounded-lg border border-zinc-200 p-4 dark:border-zinc-800">
{/* A camera that can't log in has no picture to show, so the form takes its place. */} {/* A camera that can't log in has no picture to show, so the form takes its place. */}
{showLogin ? ( {showLogin ? (
<div className="flex flex-col gap-2 rounded bg-zinc-100 p-3 dark:bg-zinc-900"> <div className="flex aspect-video flex-col gap-2 overflow-hidden rounded bg-zinc-100 p-2 dark:bg-zinc-900">
<StatusBadge status={status} className="self-start" /> <StatusBadge status={status} className="self-start" />
<CredentialsForm <div className="min-h-0 flex-1 overflow-y-auto">
cam={cam} <CredentialsForm
reason={problem?.kind === "auth" ? problem.message : undefined} cam={cam}
onSaved={() => setEditingLogin(false)} reason={problem?.kind === "auth" ? problem.message : undefined}
onCancel={problem?.kind === "auth" ? undefined : () => setEditingLogin(false)} onSaved={() => setEditingLogin(false)}
/> onCancel={problem?.kind === "auth" ? undefined : () => setEditingLogin(false)}
compact
/>
</div>
</div> </div>
) : ( ) : (
<Snapshot <Snapshot

View File

@ -31,12 +31,15 @@ export function CredentialsForm({
reason, reason,
onSaved, onSaved,
onCancel, onCancel,
compact = false,
}: { }: {
cam: CameraSummary; cam: CameraSummary;
reason?: string; reason?: string;
/** Called after a save or clear; the camera's queries have already been reset. */ /** Called after a save or clear; the camera's queries have already been reset. */
onSaved: () => void; onSaved: () => void;
onCancel?: () => void; onCancel?: () => void;
/** For the dashboard card, where the form stands in for the picture. */
compact?: boolean;
}) { }) {
const status = useCredentialStatus(cam.id); const status = useCredentialStatus(cam.id);
const save = useSaveCredentials(cam.id); const save = useSaveCredentials(cam.id);
@ -63,16 +66,22 @@ export function CredentialsForm({
const busy = save.isPending || clear.isPending; const busy = save.isPending || clear.isPending;
return ( return (
<form onSubmit={submit} className="flex flex-col gap-2 rounded border border-amber-400/60 p-3 text-sm"> <form
onSubmit={submit}
className={`flex h-full flex-col rounded border border-amber-400/60 text-sm ${compact ? "gap-1.5 p-2" : "gap-2 p-3"}`}
>
<div className="flex items-center gap-1.5 font-medium"> <div className="flex items-center gap-1.5 font-medium">
<KeyRound aria-hidden className="size-4 shrink-0" /> <KeyRound aria-hidden className="size-4 shrink-0" />
Camera login Camera login
</div> </div>
{reason && <div className="text-xs text-zinc-500">{reason}</div>} {reason && <div className="text-xs text-zinc-500">{reason}</div>}
<div className="text-xs text-zinc-500"> {/* On a card this has to fit where the picture would be, so the aside goes. */}
Use the ONVIF user you created on the camera ( {!compact && (
<WebUiLink cam={cam}>camera web page</WebUiLink>). <div className="text-xs text-zinc-500">
</div> Use the ONVIF user you created on the camera (
<WebUiLink cam={cam}>camera web page</WebUiLink>).
</div>
)}
<input <input
value={username} value={username}
onChange={(e) => setTypedUsername(e.target.value)} onChange={(e) => setTypedUsername(e.target.value)}

View File

@ -59,6 +59,8 @@ describe("CameraLoginControls", () => {
expect(screen.queryByText("Camera login")).toBeNull(); expect(screen.queryByText("Camera login")).toBeNull();
fireEvent.click(screen.getByRole("button", { name: "Change login" })); fireEvent.click(screen.getByRole("button", { name: "Change login" }));
expect(await screen.findByText("Camera login")).toBeTruthy(); expect(await screen.findByText("Camera login")).toBeTruthy();
// With room to spare here, the form keeps its aside about which user to use.
expect(screen.getByText(/Use the ONVIF user you created/)).toBeTruthy();
fireEvent.click(screen.getByRole("button", { name: "Cancel" })); fireEvent.click(screen.getByRole("button", { name: "Cancel" }));
expect(screen.queryByText("Camera login")).toBeNull(); expect(screen.queryByText("Camera login")).toBeNull();