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

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