149 lines
6.3 KiB
TypeScript
149 lines
6.3 KiB
TypeScript
import type { ComponentProps, ReactNode } from "react";
|
|
import { twClassNames } from "~/ui/utils";
|
|
import { Card } from "~/ui/components/Card";
|
|
import { IconWithBackground } from "~/ui/components/IconWithBackground";
|
|
import { Logo } from "~/ui/components/Logo";
|
|
|
|
type StatusTone = "positive" | "negative" | "neutral";
|
|
type CardSize = "small" | "medium" | "large" | "extra-large";
|
|
type SubtitleVariant = "regular-0" | "regular-150" | "medium-150";
|
|
|
|
const SUBTITLE_CLS: Record<SubtitleVariant, string> = {
|
|
"regular-0": "text-body-small-regular-0 font-body-small-regular-0 text-text-neutral-secondary",
|
|
"regular-150": "text-body-small-regular-150 font-body-small-regular-150 text-text-neutral-secondary",
|
|
"medium-150": "text-body-small-medium-150 font-body-small-medium-150 text-text-neutral-secondary",
|
|
};
|
|
|
|
// Gemeinsames Auth-Layout nach Davids WF-P2-Subframe-Auth-Screens (Login/Signup/Reset/2FA/Magic-Link):
|
|
// optionales Logo (64) + Davids ECHTE Card-Komponente (~/ui) + optionaler Footer. min-h-screen statt Davids
|
|
// h-full (Auth-Routen ohne full-height-Parent). Card-Groesse/Logo-Sichtbarkeit/Header-Abstand/Untertitel-Stil
|
|
// variieren je Screen in Davids Designs -> als Props (cardSize/showLogo/headerGap/subtitleVariant/titleAlign).
|
|
// Card statt Inline-Nachbau beseitigt Token-Drift (rounded-md/px-6/items-start kommen jetzt aus der Card).
|
|
// `items-stretch`-Override haelt Form-Kinder auf voller Breite (Davids Card-Root ist items-start).
|
|
|
|
export function AuthShell({
|
|
title,
|
|
subtitle,
|
|
children,
|
|
footer,
|
|
cardSize = "large",
|
|
cardClassName = "max-w-[448px]",
|
|
showLogo = true,
|
|
logoIconOnly = false,
|
|
logoSize = "64",
|
|
titleAlign = "center",
|
|
headerGap = "gap-1.5",
|
|
subtitleVariant = "regular-150",
|
|
dataUiId,
|
|
}: {
|
|
title: ReactNode;
|
|
subtitle?: ReactNode;
|
|
children: ReactNode;
|
|
footer?: ReactNode;
|
|
/** Davids Card-Groesse. S01 Login/V01/V02 = large, S04 2FA = extra-large. */
|
|
cardSize?: CardSize;
|
|
/** Breite/Overrides der Card; Forms = max-w-[400px] (Default). */
|
|
cardClassName?: string;
|
|
/** Logo ueber der Card zeigen. Davids S01 Login/V01/V02 = ohne Logo. */
|
|
showLogo?: boolean;
|
|
/** Icon-only-Logo (wie Davids Auth-Screens, Logo size=64 hideText) statt vollem Wortmarken-Logo. */
|
|
logoIconOnly?: boolean;
|
|
/** Visuelle Logo-Groesse. Auth-Defaults bleiben bei 64; breite Mehrschritt-Flows duerfen kompakter sein. */
|
|
logoSize?: ComponentProps<typeof Logo>["size"];
|
|
/** Titel-/Untertitel-Ausrichtung. David: Login=center, Signup/Forgot/Reset/2FA=start. */
|
|
titleAlign?: "center" | "start";
|
|
/** Abstand Titel<->Untertitel. David: S01 Login = gap-1.5, sonst gap-1. */
|
|
headerGap?: string;
|
|
/** Untertitel-Stil. David: S01 Login = regular-150, 2FA/Forgot = medium-150, sonst regular-0. */
|
|
subtitleVariant?: SubtitleVariant;
|
|
dataUiId?: string;
|
|
}) {
|
|
return (
|
|
<div
|
|
className="flex min-h-screen w-full flex-col items-center justify-center gap-8 bg-surface-neutral-primary px-4 py-12 mobile:gap-6 mobile:py-6"
|
|
data-ui-id={dataUiId}
|
|
>
|
|
{showLogo ? <Logo size={logoSize} showIcon={logoIconOnly || undefined} hideText={logoIconOnly || undefined} /> : null}
|
|
<Card size={cardSize} variant="default" className={twClassNames("items-stretch", cardClassName)}>
|
|
<div className={twClassNames("flex w-full flex-col", headerGap, titleAlign === "center" ? "items-center" : "items-start")}>
|
|
<h1 className="text-heading- font-heading- text-text-neutral-primary">{title}</h1>
|
|
{subtitle ? (
|
|
<p className={twClassNames(SUBTITLE_CLS[subtitleVariant], titleAlign === "center" ? "text-center" : "")}>{subtitle}</p>
|
|
) : null}
|
|
</div>
|
|
{children}
|
|
</Card>
|
|
{footer ? <div className="flex w-full flex-wrap items-center justify-center gap-2">{footer}</div> : null}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
// Status-Screen nach Davids Auth-Status-Design (Magic-Link Sent/Invalid, Forgot Sent, Reset Success ...):
|
|
// volles Logo + Card mit farbigem Icon-Badge + Titel + Nachricht + optionalem Hinweis + Aktionen.
|
|
export function AuthStatus({
|
|
icon,
|
|
tone = "neutral",
|
|
title,
|
|
message,
|
|
hint,
|
|
children,
|
|
dataUiId,
|
|
}: {
|
|
icon: ReactNode;
|
|
tone?: StatusTone;
|
|
title: ReactNode;
|
|
message: ReactNode;
|
|
hint?: ReactNode;
|
|
/** Aktions-Buttons (vertikal gestapelt, zentriert). */
|
|
children?: ReactNode;
|
|
dataUiId?: string;
|
|
}) {
|
|
return (
|
|
<div
|
|
className="flex min-h-screen w-full flex-col items-center justify-center gap-8 bg-surface-neutral-primary px-4 py-12 mobile:gap-6 mobile:py-6"
|
|
data-ui-id={dataUiId}
|
|
>
|
|
<Logo size="64" />
|
|
<Card size="large" variant="default" className="max-w-[448px] items-center text-center">
|
|
<div className="flex w-full flex-col items-center gap-4">
|
|
<IconWithBackground
|
|
variant={tone === "positive" ? "success" : tone === "negative" ? "destructive" : "neutral"}
|
|
size="extra-large"
|
|
round={true}
|
|
icon={icon}
|
|
/>
|
|
<div className="flex w-full flex-col items-center gap-2">
|
|
<h1 className="text-center text-heading- font-heading- text-text-neutral-primary">{title}</h1>
|
|
<p className="text-center text-body-small-medium-150 font-body-small-medium-150 text-text-neutral-tertiary">{message}</p>
|
|
{hint ? (
|
|
<p className="text-center text-body-extra-small-regular-0 font-body-extra-small-regular-0 text-text-neutral-tertiary">{hint}</p>
|
|
) : null}
|
|
</div>
|
|
</div>
|
|
{children ? <div className="flex w-full flex-col items-center gap-3 py-1">{children}</div> : null}
|
|
</Card>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
// Davids "oder"-Trenner zwischen Primär- und Sekundär-Aktion (z.B. Login vs. Magic-Link).
|
|
export function AuthDivider({ label, labelSize = "small" }: { label: ReactNode; labelSize?: "small" | "extra-small" }) {
|
|
return (
|
|
<div className="flex w-full items-center justify-center gap-3">
|
|
<div className="flex h-px grow shrink-0 basis-0 flex-col items-center bg-border-default" />
|
|
<span
|
|
className={twClassNames(
|
|
"text-body-small-regular-0 font-body-small-regular-0 text-text-neutral-secondary",
|
|
{
|
|
"text-body-extra-small-regular-150 font-body-extra-small-regular-150":
|
|
labelSize === "extra-small",
|
|
}
|
|
)}
|
|
>
|
|
{label}
|
|
</span>
|
|
<div className="flex h-px grow shrink-0 basis-0 flex-col items-center bg-border-default" />
|
|
</div>
|
|
);
|
|
}
|