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();
// The status still says what is wrong, where the picture would have been.
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 () => {

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">
{/* A camera that can't log in has no picture to show, so the form takes its place. */}
{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" />
<CredentialsForm
cam={cam}
reason={problem?.kind === "auth" ? problem.message : undefined}
onSaved={() => setEditingLogin(false)}
onCancel={problem?.kind === "auth" ? undefined : () => setEditingLogin(false)}
/>
<div className="min-h-0 flex-1 overflow-y-auto">
<CredentialsForm
cam={cam}
reason={problem?.kind === "auth" ? problem.message : undefined}
onSaved={() => setEditingLogin(false)}
onCancel={problem?.kind === "auth" ? undefined : () => setEditingLogin(false)}
compact
/>
</div>
</div>
) : (
<Snapshot

View File

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

View File

@ -59,6 +59,8 @@ describe("CameraLoginControls", () => {
expect(screen.queryByText("Camera login")).toBeNull();
fireEvent.click(screen.getByRole("button", { name: "Change login" }));
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" }));
expect(screen.queryByText("Camera login")).toBeNull();