|
| 1 | +import clsx from 'clsx' |
| 2 | +import {InputHTMLAttributes, ReactNode} from 'react' |
| 3 | + |
| 4 | +// Shared building blocks for the /register and /signin pages so the two stay in |
| 5 | +// visual lockstep. Mirrors the home page design language: warm tokens, a soft |
| 6 | +// radial glow for depth, and staggered fade-up entrances. |
| 7 | + |
| 8 | +/** Page wrapper: centers the card and paints the hero-style radial glow behind it. */ |
| 9 | +export function AuthShell({children}: {children: ReactNode}) { |
| 10 | + return ( |
| 11 | + <div className="relative min-h-screen flex items-center justify-center py-12 px-4 sm:px-6 lg:px-8"> |
| 12 | + {/* Soft radial glow behind the card for depth — mirrors the home hero. |
| 13 | + Dark mode uses a brighter, lighter-hued, faster-falloff core so it reads |
| 14 | + as light, not brown haze. */} |
| 15 | + {/*<div*/} |
| 16 | + {/* aria-hidden*/} |
| 17 | + {/* className="pointer-events-none absolute inset-x-0 top-0 -z-10 h-[420px] bg-[radial-gradient(ellipse_60%_55%_at_50%_28%,rgba(193,127,62,0.16),transparent_70%)] dark:h-[360px] dark:bg-[radial-gradient(ellipse_48%_42%_at_50%_22%,rgba(224,162,102,0.22),rgba(193,127,62,0.06)_45%,transparent_64%)]"*/} |
| 18 | + {/*/>*/} |
| 19 | + <div className="max-w-md w-full space-y-8">{children}</div> |
| 20 | + </div> |
| 21 | + ) |
| 22 | +} |
| 23 | + |
| 24 | +/** Favicon + title + optional subtitle. */ |
| 25 | +export function AuthHeader({title, subtitle}: {title: string; subtitle?: string}) { |
| 26 | + return ( |
| 27 | + <div className="animate-fade-up"> |
| 28 | + {/*<div className="flex justify-center mb-6">*/} |
| 29 | + {/* <FavIconBlack className="dark:invert" />*/} |
| 30 | + {/*</div>*/} |
| 31 | + <h2 className="text-center text-3xl font-extrabold tracking-tight text-ink-1000">{title}</h2> |
| 32 | + {subtitle && <p className="mt-3 text-center text-ink-600">{subtitle}</p>} |
| 33 | + </div> |
| 34 | + ) |
| 35 | +} |
| 36 | + |
| 37 | +/** The form element, with the standard entrance animation. */ |
| 38 | +export function AuthForm({ |
| 39 | + onSubmit, |
| 40 | + children, |
| 41 | +}: { |
| 42 | + onSubmit: (event: React.FormEvent<HTMLFormElement>) => void |
| 43 | + children: ReactNode |
| 44 | +}) { |
| 45 | + return ( |
| 46 | + <form |
| 47 | + className="mt-8 space-y-6 animate-fade-up" |
| 48 | + style={{animationDelay: '80ms'}} |
| 49 | + onSubmit={onSubmit} |
| 50 | + > |
| 51 | + {children} |
| 52 | + </form> |
| 53 | + ) |
| 54 | +} |
| 55 | + |
| 56 | +/** Wraps stacked inputs so adjacent borders collapse into one. */ |
| 57 | +export function AuthFieldGroup({children}: {children: ReactNode}) { |
| 58 | + return <div className="-space-y-px">{children}</div> |
| 59 | +} |
| 60 | + |
| 61 | +const authInputBase = |
| 62 | + 'bg-canvas-50 appearance-none relative block w-full px-4 py-3 border-[1.5px] border-canvas-200 text-ink-1000 placeholder-ink-400 transition-colors focus:outline-none focus:border-primary-500 focus:ring-1 focus:ring-primary-500 focus:z-10 sm:text-sm' |
| 63 | + |
| 64 | +/** Email/password input with a screen-reader label. `position` rounds the outer corner. */ |
| 65 | +export function AuthInput({ |
| 66 | + position, |
| 67 | + label, |
| 68 | + below, |
| 69 | + className, |
| 70 | + ...props |
| 71 | +}: { |
| 72 | + position: 'top' | 'bottom' |
| 73 | + label: string |
| 74 | + below?: ReactNode |
| 75 | +} & InputHTMLAttributes<HTMLInputElement>) { |
| 76 | + return ( |
| 77 | + <div> |
| 78 | + <label htmlFor={props.id} className="sr-only"> |
| 79 | + {label} |
| 80 | + </label> |
| 81 | + <input |
| 82 | + className={clsx( |
| 83 | + authInputBase, |
| 84 | + position === 'top' ? 'rounded-t-xl' : 'rounded-b-xl', |
| 85 | + className, |
| 86 | + )} |
| 87 | + {...props} |
| 88 | + /> |
| 89 | + {below} |
| 90 | + </div> |
| 91 | + ) |
| 92 | +} |
| 93 | + |
| 94 | +/** Gradient rule with a centered label. */ |
| 95 | +export function AuthDivider({label}: {label: string}) { |
| 96 | + return ( |
| 97 | + <div className="relative"> |
| 98 | + <div className="absolute inset-0 flex items-center" aria-hidden> |
| 99 | + <div className="w-full h-px bg-gradient-to-r from-transparent via-canvas-300 to-transparent" /> |
| 100 | + </div> |
| 101 | + <div className="relative flex justify-center text-sm"> |
| 102 | + <span className="px-3 body-bg text-ink-500">{label}</span> |
| 103 | + </div> |
| 104 | + </div> |
| 105 | + ) |
| 106 | +} |
| 107 | + |
| 108 | +/** Primary CTA, matching the home hero button (amber, shadow, hover lift). */ |
| 109 | +export function AuthSubmitButton({ |
| 110 | + isLoading, |
| 111 | + children, |
| 112 | +}: { |
| 113 | + isLoading?: boolean |
| 114 | + children: ReactNode |
| 115 | +}) { |
| 116 | + return ( |
| 117 | + <button |
| 118 | + type="submit" |
| 119 | + disabled={isLoading} |
| 120 | + className={clsx( |
| 121 | + 'group relative w-full flex justify-center py-3.5 px-4 text-[15px] font-bold rounded-xl text-white bg-primary-500 shadow-[0_4px_16px_rgba(193,127,62,0.35)] transition-all duration-150 hover:bg-primary-600 hover:-translate-y-0.5 hover:shadow-[0_8px_24px_rgba(193,127,62,0.4)] focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500', |
| 122 | + isLoading && 'opacity-70 cursor-not-allowed hover:translate-y-0', |
| 123 | + )} |
| 124 | + > |
| 125 | + {children} |
| 126 | + </button> |
| 127 | + ) |
| 128 | +} |
| 129 | + |
| 130 | +/** Centered error message. Renders nothing when empty. */ |
| 131 | +export function AuthError({children}: {children: ReactNode}) { |
| 132 | + if (!children) return null |
| 133 | + return <div className="text-red-500 text-sm text-center">{children}</div> |
| 134 | +} |
| 135 | + |
| 136 | +/** Footer line ("Already have an account?" etc.) with link styling + entrance. */ |
| 137 | +export function AuthFooter({children}: {children: ReactNode}) { |
| 138 | + return ( |
| 139 | + <div |
| 140 | + className="text-center text-ink-600 custom-link animate-fade-up" |
| 141 | + style={{animationDelay: '160ms'}} |
| 142 | + > |
| 143 | + <p>{children}</p> |
| 144 | + </div> |
| 145 | + ) |
| 146 | +} |
0 commit comments