diff --git a/web/app/layout.tsx b/web/app/layout.tsx index be69b61..5e273fd 100644 --- a/web/app/layout.tsx +++ b/web/app/layout.tsx @@ -10,6 +10,11 @@ export const metadata: Metadata = { export const viewport: Viewport = { themeColor: "#18181b", + // Lets the page draw under the iPhone notch / Dynamic Island when the + // PWA runs in standalone mode. Components that pin to the edges (Nav, + // Toast) read env(safe-area-inset-*) to keep their content out of the + // hardware cutouts. + viewportFit: "cover", }; export default function RootLayout({ diff --git a/web/components/create-account-dialog.tsx b/web/components/create-account-dialog.tsx index 7915c4b..12925c8 100644 --- a/web/components/create-account-dialog.tsx +++ b/web/components/create-account-dialog.tsx @@ -1,6 +1,6 @@ "use client"; -import { useEffect, useState, useTransition } from "react"; +import { useEffect, useRef, useState, useTransition } from "react"; import { createAccount } from "@/app/actions"; import FormDialogShell, { Field, inputClass } from "./form-dialog-shell"; @@ -18,6 +18,7 @@ export default function CreateAccountDialog({ open, onClose, onSuccess, prefixPa const [password, setPassword] = useState(""); const [status, setStatus] = useState(""); const [link, setLink] = useState(""); + const firstFieldRef = useRef(null); // Reset on open. useEffect(() => { @@ -27,6 +28,15 @@ export default function CreateAccountDialog({ open, onClose, onSuccess, prefixPa setStatus(""); setLink(""); setError(null); + // Autofocus the first field only on devices with a fine pointer + // (desktop). Phones skip this — the soft keyboard popping the + // moment a dialog opens is jarring and reflows the page. + if (typeof window !== "undefined") { + const isPointerDevice = window.matchMedia("(hover: hover) and (pointer: fine)").matches; + if (isPointerDevice) { + requestAnimationFrame(() => firstFieldRef.current?.focus()); + } + } } }, [open]); @@ -65,10 +75,10 @@ export default function CreateAccountDialog({ open, onClose, onSuccess, prefixPa > setUsername(e.target.value)} disabled={pending} - autoFocus autoComplete="off" spellCheck={false} placeholder={prefixPattern ? `${prefixPattern}1234` : "username"} diff --git a/web/components/create-user-dialog.tsx b/web/components/create-user-dialog.tsx index 9cff32d..747983d 100644 --- a/web/components/create-user-dialog.tsx +++ b/web/components/create-user-dialog.tsx @@ -1,6 +1,6 @@ "use client"; -import { useEffect, useState, useTransition } from "react"; +import { useEffect, useRef, useState, useTransition } from "react"; import { createUser } from "@/app/actions"; import FormDialogShell, { Field, inputClass } from "./form-dialog-shell"; @@ -17,6 +17,7 @@ export default function CreateUserDialog({ open, onClose, onSuccess }: Props) { const [fPassword, setFPassword] = useState(""); const [tUsername, setTUsername] = useState(""); const [tPassword, setTPassword] = useState(""); + const firstFieldRef = useRef(null); useEffect(() => { if (open) { @@ -25,6 +26,13 @@ export default function CreateUserDialog({ open, onClose, onSuccess }: Props) { setTUsername(""); setTPassword(""); setError(null); + // Autofocus first field only on pointer devices — see CreateAccountDialog. + if (typeof window !== "undefined") { + const isPointerDevice = window.matchMedia("(hover: hover) and (pointer: fine)").matches; + if (isPointerDevice) { + requestAnimationFrame(() => firstFieldRef.current?.focus()); + } + } } }, [open]); @@ -63,10 +71,10 @@ export default function CreateUserDialog({ open, onClose, onSuccess }: Props) { > setFUsername(e.target.value)} disabled={pending} - autoFocus autoComplete="off" spellCheck={false} className={inputClass} diff --git a/web/components/nav.tsx b/web/components/nav.tsx index ba4d31d..bedfc53 100644 --- a/web/components/nav.tsx +++ b/web/components/nav.tsx @@ -9,7 +9,16 @@ export default function Nav() { const isAccounts = !isUsers; return ( -
+
diff --git a/web/components/toast.tsx b/web/components/toast.tsx index 2d8d3d3..b90204c 100644 --- a/web/components/toast.tsx +++ b/web/components/toast.tsx @@ -39,7 +39,13 @@ export default function Toast({