Files
healer-man/src/ui/LoginVirtualKeyboard.tsx
T
2026-09-20 13:40:23 -04:00

81 lines
3.5 KiB
TypeScript

import type { CSSProperties } from "react";
import {
loginKeyboardRows,
type LoginField,
type LoginInputSnapshot,
type LoginKeyDefinition,
} from "../app/loginInputStore";
import type { CompanionCommand } from "../platform/dualScreenSync";
import { BrandMark, FrontSurface } from "./FrontSurface";
function passwordPreview(length: number): string {
if (!length) return "Password";
return `${"•".repeat(Math.min(length, 18))}${length > 18 ? "…" : ""}`;
}
function keyLabel(key: LoginKeyDefinition, activeField: LoginField): string {
if (key.kind === "field") return activeField === "username" ? "Password" : "Username";
return key.label;
}
export function LoginVirtualKeyboard({ snapshot, notice, send }: {
snapshot: LoginInputSnapshot;
notice: string;
send: (command: CompanionCommand) => void;
}) {
const rows = loginKeyboardRows(snapshot.keyboardMode);
const chooseField = (field: LoginField) => send({ name: "selectLoginField", field });
return (
<FrontSurface bottom className="companion-panel companion-login companion-login-keyboard" ariaLabel="Controller login keyboard">
<header><BrandMark compact /><b>CONTROLLER KEYBOARD</b></header>
<main>
<div className="login-keyboard-fields" role="group" aria-label="Login fields">
<button
type="button"
className={snapshot.activeField === "username" ? "is-active" : ""}
aria-pressed={snapshot.activeField === "username"}
onClick={() => chooseField("username")}
>
<small>Username</small><strong>{snapshot.username || "Select to enter"}</strong>
</button>
<button
type="button"
className={snapshot.activeField === "password" ? "is-active" : ""}
aria-pressed={snapshot.activeField === "password"}
onClick={() => chooseField("password")}
>
<small>Password</small><strong>{passwordPreview(snapshot.passwordLength)}</strong>
</button>
</div>
<div className="login-keyboard-grid" role="group" aria-label={`${snapshot.keyboardMode} virtual keyboard`}>
{rows.flat().map((key) => {
const isAction = ["login", "create", "offline"].includes(key.kind);
const unavailableForUsername = snapshot.activeField === "username"
&& key.kind === "character"
&& !/^[A-Za-z0-9_]$/.test(key.value ?? "");
return (
<button
key={key.id}
type="button"
className={`login-keyboard-key login-keyboard-key--${key.kind} ${snapshot.selectedKeyId === key.id ? "is-controller-selected" : ""} ${unavailableForUsername ? "is-unavailable" : ""}`}
style={{ gridColumn: `span ${key.span ?? 1}` } as CSSProperties}
aria-label={key.ariaLabel}
aria-current={snapshot.selectedKeyId === key.id ? "true" : undefined}
disabled={isAction && snapshot.busy !== null}
onClick={() => send({ name: "pressLoginKey", keyId: key.id })}
>
{keyLabel(key, snapshot.activeField)}
</button>
);
})}
</div>
</main>
<footer>
<span><b>D-Pad</b> Move &nbsp; <b>A</b> Type</span>
<strong>{snapshot.busy ? `${snapshot.busy === "create" ? "Creating account" : "Opening gateway"}…` : notice || `${snapshot.activeField === "username" ? "Username" : "Password"} selected`}</strong>
</footer>
</FrontSurface>
);
}