feat(design): the button families on the decided ladder, first half of the sweep (E28 S3b1)
Button is rebuilt to A1/B1/B3: explicit heights (xs 24 / sm 28 / md 32, lg 40 for a hero CTA), r8 (a pill on media), state layers instead of opacity and bg-card hovers, a `pressed` toggle, `overlay`, `media` and `link-muted` variants. The icon-only sizes move to the new IconButton (28 / 32, 48 only for the Play on a picture; a required label). Pressable (a card, row or tile that is the control) and DragHandle join them. The state-* utilities now stand down on a disabled, busy or pressed control, held by theme.contract.test.ts. Swept onto them: the Button icon-size callers and 16 files (Plex playlist, Plex detail, the YT player modal chrome, Welcome, Plex and video cards with AddToPlaylist and DownloadButton, the video editor, Feed, personal views, Scheduler, Users, the nav rail, Plex detail parts, Settings): 268 raw buttons are 153. Fixed on the way: FocusSafeButton overwrote a caller's aria-disabled (the "unavailable, here is why" state; adminInvites caught it), a season toggle and a Plex settings switch had no name/state, and the video card's overlay follows the C5 card (Resume pill, From the start pill, a round Play).
This commit is contained in:
@@ -35,7 +35,8 @@ import { inputCls } from "./ui/form";
|
||||
import { entryHash, entrySearch } from "../lib/entryUrl";
|
||||
import type { VerifyEntry } from "../lib/useVerifyEntry";
|
||||
import { MODE_URL, type Mode } from "./welcomeMode";
|
||||
import { FocusSafeButton } from "./ui/FocusSafeButton";
|
||||
import { IconButton } from "./ui/IconButton";
|
||||
import { Pressable } from "./ui/Pressable";
|
||||
|
||||
const EMAIL_RE = /^[^@\s]+@[^@\s]+\.[^@\s]+$/;
|
||||
const PASSWORD_MIN = 10;
|
||||
@@ -232,8 +233,7 @@ function Preview({
|
||||
);
|
||||
}
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
<Pressable
|
||||
onClick={() => onOpen?.(src, label)}
|
||||
aria-label={label}
|
||||
className={`group relative glass-card rounded-2xl overflow-hidden block cursor-zoom-in ${className ?? ""}`}
|
||||
@@ -248,7 +248,7 @@ function Preview({
|
||||
<span className="absolute inset-0 grid place-items-center bg-black/0 opacity-0 transition group-hover:bg-black/30 group-hover:opacity-100">
|
||||
<Expand className="w-6 h-6 text-white drop-shadow-lg" />
|
||||
</span>
|
||||
</button>
|
||||
</Pressable>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -306,14 +306,15 @@ function Lightbox({ src, label, onClose }: { src: string; label: string; onClose
|
||||
aria-modal="true"
|
||||
aria-label={label}
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
<IconButton
|
||||
variant="media"
|
||||
size="md"
|
||||
onClick={onClose}
|
||||
aria-label={t("common.close")}
|
||||
className="absolute top-4 right-4 p-2 rounded-full glass-card glass-hover text-fg"
|
||||
label={t("common.close")}
|
||||
className="absolute top-4 right-4"
|
||||
>
|
||||
<X className="w-5 h-5" />
|
||||
</button>
|
||||
<X />
|
||||
</IconButton>
|
||||
<figure
|
||||
className="flex max-h-full max-w-full flex-col items-center gap-3"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
@@ -644,15 +645,16 @@ function AuthCard({ verify }: { verify: VerifyEntry }) {
|
||||
) : done ? (
|
||||
<div className="mt-3">
|
||||
<p className="text-sm text-fg/80 leading-relaxed">{done}</p>
|
||||
<button
|
||||
<Button
|
||||
variant="link"
|
||||
className="mt-4 text-sm"
|
||||
onClick={() => {
|
||||
setMode("signin");
|
||||
setDone("");
|
||||
}}
|
||||
className="mt-4 text-sm text-accent hover:underline"
|
||||
>
|
||||
{t("welcome.auth.backToSignin")}
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
@@ -688,25 +690,18 @@ function AuthCard({ verify }: { verify: VerifyEntry }) {
|
||||
{error}
|
||||
</p>
|
||||
)}
|
||||
<FocusSafeButton
|
||||
type="submit"
|
||||
disabled={busy}
|
||||
className="px-4 py-2.5 rounded-xl font-semibold btn-primary disabled:opacity-50 transition"
|
||||
>
|
||||
<Button type="submit" size="lg" disabled={busy}>
|
||||
{busy ? t("welcome.auth.working") : t(SUBMIT_KEY[mode])}
|
||||
</FocusSafeButton>
|
||||
</Button>
|
||||
</form>
|
||||
|
||||
{/* A confirmation mail can be slow, filtered, or — before E9.5 S6 — never sent at all for a
|
||||
repeat sign-up. Offered right under the form rather than below the mode switches: it is
|
||||
the second thing a stuck person needs, after the password they just tried. */}
|
||||
{mode === "signin" && (
|
||||
<button
|
||||
onClick={goResend}
|
||||
className="mt-3 block w-full text-center text-xs text-muted hover:text-fg"
|
||||
>
|
||||
<Button variant="link-muted" fullWidth className="mt-3 text-xs" onClick={goResend}>
|
||||
{t("welcome.auth.resendLink")}
|
||||
</button>
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{/* Mode switches */}
|
||||
@@ -722,15 +717,15 @@ function AuthCard({ verify }: { verify: VerifyEntry }) {
|
||||
>
|
||||
{t("welcome.auth.createLink")}
|
||||
</Button>
|
||||
<button
|
||||
<Button
|
||||
variant="link-muted"
|
||||
onClick={() => {
|
||||
reset();
|
||||
setMode("forgot");
|
||||
}}
|
||||
className="text-muted hover:text-fg"
|
||||
>
|
||||
{t("welcome.auth.forgotLink")}
|
||||
</button>
|
||||
</Button>
|
||||
</>
|
||||
) : (
|
||||
<Button
|
||||
@@ -761,12 +756,15 @@ function AuthCard({ verify }: { verify: VerifyEntry }) {
|
||||
</a>
|
||||
)}
|
||||
{!showDemo ? (
|
||||
<button
|
||||
<Button
|
||||
variant="outline"
|
||||
size="lg"
|
||||
fullWidth
|
||||
className="mt-2"
|
||||
onClick={() => setShowDemo(true)}
|
||||
className="w-full mt-2 px-4 py-2.5 rounded-xl text-sm bg-card border border-border hover:border-accent transition"
|
||||
>
|
||||
{t("welcome.auth.tryDemo")}
|
||||
</button>
|
||||
</Button>
|
||||
) : (
|
||||
<form onSubmit={onDemo} className="mt-2 flex flex-col gap-2">
|
||||
<p className="text-2xs text-muted">{t("welcome.auth.demoHint")}</p>
|
||||
@@ -778,13 +776,9 @@ function AuthCard({ verify }: { verify: VerifyEntry }) {
|
||||
placeholder={t("welcome.auth.demoEmail")}
|
||||
className={inputCls}
|
||||
/>
|
||||
<FocusSafeButton
|
||||
type="submit"
|
||||
disabled={demoBusy}
|
||||
className="shrink-0 px-3 py-2.5 rounded-xl text-sm font-semibold bg-card border border-border hover:border-accent disabled:opacity-50 transition"
|
||||
>
|
||||
<Button type="submit" variant="outline" size="lg" disabled={demoBusy}>
|
||||
{demoBusy ? t("welcome.auth.working") : t("welcome.auth.demoEnter")}
|
||||
</FocusSafeButton>
|
||||
</Button>
|
||||
</div>
|
||||
{demoError && <p className="text-xs text-danger">{demoError}</p>}
|
||||
</form>
|
||||
@@ -858,12 +852,9 @@ function StatePanel({
|
||||
{t("welcome.auth.backToSignin")}
|
||||
</Button>
|
||||
{cta && (
|
||||
<button
|
||||
onClick={cta.onClick}
|
||||
className="shrink-0 px-3 py-2 rounded-xl text-xs font-semibold bg-card border border-border hover:border-accent transition"
|
||||
>
|
||||
<Button variant="outline" size="sm" onClick={cta.onClick}>
|
||||
{cta.label}
|
||||
</button>
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user