135 lines
5.4 KiB
TypeScript
135 lines
5.4 KiB
TypeScript
import type { ReactNode } from "react";
|
|
import { twClassNames } from "~/ui/utils";
|
|
import { Logo } from "~/ui/components/Logo";
|
|
|
|
type StatusTone = "positive" | "negative" | "neutral";
|
|
|
|
const STATUS_BADGE: Record<StatusTone, string> = {
|
|
positive: "bg-surface-feedback-subtle-positive",
|
|
negative: "bg-surface-feedback-subtle-negative",
|
|
neutral: "bg-surface-feedback-subtle-neutral",
|
|
};
|
|
const STATUS_ICON: Record<StatusTone, string> = {
|
|
positive: "text-icon-feedback-positive",
|
|
negative: "text-icon-feedback-negative",
|
|
neutral: "text-icon-neutral-primary",
|
|
};
|
|
|
|
// Gemeinsames Auth-Layout nach Davids Subframe-Auth-Screens (Login/Signup/Reset/2FA/Magic-Link):
|
|
// zentriertes Logo (64) + Card (Titel/Untertitel + Inhalt) + optionaler Footer. min-h-screen statt Davids
|
|
// h-full (Auth-Routen ohne full-height-Parent). Titel-Ausrichtung + Untertitel-Gewicht variieren je Screen
|
|
// in Davids Designs -> als Props (titleAlign/subtitleStrong). twClassNames, damit cardClassName Basis-Tokens
|
|
// (gap/shadow) sauber ueberschreibt.
|
|
|
|
export function AuthShell({
|
|
title,
|
|
subtitle,
|
|
children,
|
|
footer,
|
|
cardClassName = "max-w-[400px]",
|
|
logoIconOnly = false,
|
|
titleAlign = "start",
|
|
subtitleStrong = false,
|
|
dataUiId,
|
|
}: {
|
|
title: ReactNode;
|
|
subtitle?: ReactNode;
|
|
children: ReactNode;
|
|
footer?: ReactNode;
|
|
/** Breite/Overrides der Card; Forms = max-w-[400px] (Default). */
|
|
cardClassName?: string;
|
|
/** Icon-only-Logo (wie Davids 2FA-/Status-Screens) statt vollem Wortmarken-Logo. */
|
|
logoIconOnly?: boolean;
|
|
/** Titel-/Untertitel-Ausrichtung. David: Login=center, Signup/Forgot/Reset/2FA=start. */
|
|
titleAlign?: "center" | "start";
|
|
/** Untertitel-Gewicht. David: Forgot/2FA = medium-150, sonst regular-0. */
|
|
subtitleStrong?: boolean;
|
|
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" showIcon={logoIconOnly || undefined} hideText={logoIconOnly || undefined} />
|
|
<div
|
|
className={twClassNames(
|
|
"flex w-full flex-col items-stretch gap-4 rounded-lg border border-solid border-border-default bg-surface-neutral-secondary px-8 py-8 shadow-sm mobile:px-6",
|
|
cardClassName
|
|
)}
|
|
>
|
|
<div className={twClassNames("flex w-full flex-col gap-1", titleAlign === "center" ? "items-center" : "items-start")}>
|
|
<h1 className="text-heading- font-heading- text-text-neutral-primary">{title}</h1>
|
|
{subtitle ? (
|
|
<p
|
|
className={
|
|
subtitleStrong
|
|
? "text-body-small-medium-150 font-body-small-medium-150 text-text-neutral-tertiary"
|
|
: "text-body-small-regular-0 font-body-small-regular-0 text-text-neutral-tertiary"
|
|
}
|
|
>
|
|
{subtitle}
|
|
</p>
|
|
) : null}
|
|
</div>
|
|
{children}
|
|
</div>
|
|
{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" />
|
|
<div className="flex w-full max-w-[400px] flex-col items-center gap-6 rounded-lg border border-solid border-border-default bg-surface-neutral-secondary px-8 py-8 text-center shadow-sm mobile:px-6">
|
|
<div className={`flex h-12 w-12 flex-none items-center justify-center rounded-round ${STATUS_BADGE[tone]}`}>
|
|
<span className={`text-heading- font-heading- ${STATUS_ICON[tone]}`}>{icon}</span>
|
|
</div>
|
|
<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>
|
|
{children ? <div className="flex w-full flex-col items-center gap-3">{children}</div> : null}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
// Davids "oder"-Trenner zwischen Primär- und Sekundär-Aktion (z.B. Login vs. Magic-Link).
|
|
export function AuthDivider({ label }: { label: ReactNode }) {
|
|
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="text-body-small-regular-0 font-body-small-regular-0 text-text-neutral-tertiary">{label}</span>
|
|
<div className="flex h-px grow shrink-0 basis-0 flex-col items-center bg-border-default" />
|
|
</div>
|
|
);
|
|
}
|