Files
HiveGrid-Wireframes/03 Quellen und Referenzen/hivegrid-cloud/apps/web/app/components/auth/AuthShell.tsx
T

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>
);
}