fix(web): restore login page design and add runtime config injection (#435)
Co-authored-by: Jason Woltje <jason@diversecanvas.com> Co-committed-by: Jason Woltje <jason@diversecanvas.com>
This commit was merged in pull request #435.
This commit is contained in:
165
packages/ui/src/components/AuthSurface.tsx
Normal file
165
packages/ui/src/components/AuthSurface.tsx
Normal file
@@ -0,0 +1,165 @@
|
||||
import type { HTMLAttributes, ReactElement, ReactNode } from "react";
|
||||
|
||||
function joinClassNames(...classNames: (string | undefined)[]): string {
|
||||
return classNames.filter(Boolean).join(" ");
|
||||
}
|
||||
|
||||
export interface AuthShellProps extends HTMLAttributes<HTMLElement> {
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
export function AuthShell({ children, className, ...props }: AuthShellProps): ReactElement {
|
||||
return (
|
||||
<main
|
||||
className={joinClassNames(
|
||||
"relative isolate flex min-h-screen items-center justify-center overflow-hidden bg-[#f0f4fc] px-4 py-8 text-[#0f141d] sm:px-8 dark:bg-[#080b12] dark:text-[#eef3ff]",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<div aria-hidden="true" className="pointer-events-none absolute inset-0 overflow-hidden">
|
||||
<div
|
||||
className="absolute left-1/2 top-1/2 h-[52rem] w-[52rem] -translate-x-1/2 -translate-y-1/2 rounded-full opacity-20 blur-[2px] animate-spin"
|
||||
style={{
|
||||
background:
|
||||
"conic-gradient(from 0deg, #2f80ff, #8b5cf6, #ec4899, #e5484d, #f59e0b, #14b8a6, #2f80ff)",
|
||||
animationDuration: "30s",
|
||||
}}
|
||||
/>
|
||||
<div
|
||||
className="absolute left-1/2 top-1/2 h-[36rem] w-[36rem] -translate-x-1/2 -translate-y-1/2 rounded-full opacity-20 blur-[1px] animate-spin"
|
||||
style={{
|
||||
background:
|
||||
"conic-gradient(from 120deg, #14b8a6, #06b6d4, #2f80ff, #6366f1, #8b5cf6, #14b8a6)",
|
||||
animationDuration: "20s",
|
||||
animationDirection: "reverse",
|
||||
}}
|
||||
/>
|
||||
<div
|
||||
className="absolute left-1/2 top-1/2 h-[22rem] w-[22rem] -translate-x-1/2 -translate-y-1/2 rounded-full opacity-25 animate-spin"
|
||||
style={{
|
||||
background:
|
||||
"conic-gradient(from 240deg, #f59e0b, #f97316, #e5484d, #ec4899, #8b5cf6, #f59e0b)",
|
||||
animationDuration: "14s",
|
||||
}}
|
||||
/>
|
||||
<div className="absolute inset-0 bg-[radial-gradient(ellipse_at_center,rgba(248,250,255,0.58)_0%,rgba(15,20,29,0.72)_62%,rgba(15,20,29,0.9)_100%)] dark:bg-[radial-gradient(ellipse_at_center,rgba(8,11,18,0.32)_0%,rgba(8,11,18,0.78)_62%,rgba(8,11,18,0.96)_100%)]" />
|
||||
</div>
|
||||
<div className="relative z-10 w-full max-w-[27rem]">{children}</div>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
export interface AuthCardProps extends HTMLAttributes<HTMLDivElement> {
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
export function AuthCard({ children, className, ...props }: AuthCardProps): ReactElement {
|
||||
return (
|
||||
<div
|
||||
className={joinClassNames(
|
||||
"relative w-full rounded-2xl border border-[#b8c4de] bg-[#dde4f2]/90 p-6 shadow-[0_30px_70px_rgba(15,20,29,0.24)] backdrop-blur-sm sm:p-10 dark:border-[#2f3b52] dark:bg-[#1b2331]/92 dark:shadow-[0_32px_80px_rgba(0,0,0,0.52)]",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<div
|
||||
aria-hidden="true"
|
||||
className="pointer-events-none absolute inset-x-0 top-0 h-0.5 rounded-t-2xl bg-[linear-gradient(90deg,#2f80ff,#8b5cf6,#14b8a6,#f59e0b)]"
|
||||
/>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export interface AuthBrandProps {
|
||||
title?: string;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function AuthBrand({ title = "Mosaic Stack", className }: AuthBrandProps): ReactElement {
|
||||
return (
|
||||
<div className={joinClassNames("flex items-center justify-center gap-3", className)}>
|
||||
<div className="relative h-9 w-9 animate-spin" style={{ animationDuration: "20s" }}>
|
||||
<span className="absolute left-0 top-0 h-[0.88rem] w-[0.88rem] rounded-[3px] bg-[#2f80ff]" />
|
||||
<span className="absolute right-0 top-0 h-[0.88rem] w-[0.88rem] rounded-[3px] bg-[#8b5cf6]" />
|
||||
<span className="absolute bottom-0 right-0 h-[0.88rem] w-[0.88rem] rounded-[3px] bg-[#14b8a6]" />
|
||||
<span className="absolute bottom-0 left-0 h-[0.88rem] w-[0.88rem] rounded-[3px] bg-[#f59e0b]" />
|
||||
<span className="absolute left-1/2 top-1/2 h-3 w-3 -translate-x-1/2 -translate-y-1/2 rounded-full bg-[#ec4899]" />
|
||||
</div>
|
||||
<span className="bg-[linear-gradient(135deg,#56a0ff,#8b5cf6,#14b8a6)] bg-clip-text text-xl font-extrabold tracking-tight text-transparent">
|
||||
{title}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export type AuthStatusTone = "neutral" | "info" | "success" | "warning" | "danger";
|
||||
|
||||
export interface AuthStatusPillProps {
|
||||
label: string;
|
||||
tone?: AuthStatusTone;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function AuthStatusPill({
|
||||
label,
|
||||
tone = "neutral",
|
||||
className,
|
||||
}: AuthStatusPillProps): ReactElement {
|
||||
const toneStyles: Record<AuthStatusTone, string> = {
|
||||
neutral:
|
||||
"border-[#b8c4de] bg-[#f8faff] text-[#2f3b52] dark:border-[#2f3b52] dark:bg-[#0f141d]/70 dark:text-[#c5d0e6]",
|
||||
info: "border-sky-400/50 bg-sky-500/15 text-sky-900 dark:text-sky-200",
|
||||
success: "border-emerald-400/55 bg-emerald-500/15 text-emerald-900 dark:text-emerald-200",
|
||||
warning: "border-amber-400/60 bg-amber-500/15 text-amber-900 dark:text-amber-200",
|
||||
danger: "border-rose-400/55 bg-rose-500/15 text-rose-900 dark:text-rose-200",
|
||||
};
|
||||
|
||||
const dotStyles: Record<AuthStatusTone, string> = {
|
||||
neutral: "bg-[#5a6a87] dark:bg-[#8f9db7]",
|
||||
info: "bg-sky-500",
|
||||
success: "bg-emerald-500",
|
||||
warning: "bg-amber-500",
|
||||
danger: "bg-rose-500",
|
||||
};
|
||||
|
||||
return (
|
||||
<span
|
||||
className={joinClassNames(
|
||||
"inline-flex items-center gap-2 rounded-full border px-2.5 py-1 text-[0.67rem] font-semibold uppercase tracking-[0.08em]",
|
||||
toneStyles[tone],
|
||||
className
|
||||
)}
|
||||
>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className={joinClassNames("h-1.5 w-1.5 rounded-full", dotStyles[tone])}
|
||||
/>
|
||||
<span>{label}</span>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
export interface AuthDividerProps {
|
||||
text?: string;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function AuthDivider({
|
||||
text = "or continue with",
|
||||
className,
|
||||
}: AuthDividerProps): ReactElement {
|
||||
return (
|
||||
<div
|
||||
className={joinClassNames(
|
||||
"my-5 flex items-center gap-3 text-[0.67rem] font-semibold uppercase tracking-[0.08em] text-[#5a6a87] dark:text-[#8f9db7]",
|
||||
className
|
||||
)}
|
||||
>
|
||||
<span aria-hidden="true" className="h-px flex-1 bg-[#b8c4de] dark:bg-[#2f3b52]" />
|
||||
<span>{text}</span>
|
||||
<span aria-hidden="true" className="h-px flex-1 bg-[#b8c4de] dark:bg-[#2f3b52]" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -43,3 +43,20 @@ export type {
|
||||
ToastContextValue,
|
||||
ToastProviderProps,
|
||||
} from "./components/Toast.js";
|
||||
|
||||
// Auth Surface
|
||||
export {
|
||||
AuthShell,
|
||||
AuthCard,
|
||||
AuthBrand,
|
||||
AuthStatusPill,
|
||||
AuthDivider,
|
||||
} from "./components/AuthSurface.js";
|
||||
export type {
|
||||
AuthShellProps,
|
||||
AuthCardProps,
|
||||
AuthBrandProps,
|
||||
AuthStatusTone,
|
||||
AuthStatusPillProps,
|
||||
AuthDividerProps,
|
||||
} from "./components/AuthSurface.js";
|
||||
|
||||
Reference in New Issue
Block a user