fix(web): restore login page design and add runtime config injection (#435)
All checks were successful
ci/woodpecker/push/orchestrator Pipeline was successful
ci/woodpecker/push/api Pipeline was successful
ci/woodpecker/push/web Pipeline was successful

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:
2026-02-21 23:16:02 +00:00
committed by jason.woltje
parent 23d610ba5b
commit 1b66417be5
12 changed files with 416 additions and 150 deletions

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

View File

@@ -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";