MicProtocol / apps /web /src /components /auth /AuthLayout.tsx
Benaah's picture
Upload folder using huggingface_hub
fb12607 verified
Raw History Blame Contribute Delete
13.9 kB
"use client";
import React, { useEffect, useState } from "react";
import Link from "next/link";
import Image from "next/image";
import { ArrowLeft } from "lucide-react";
import { ThemeToggle } from "@/lib/theme-toggle";
interface AuthLayoutProps {
title: string;
subtitle: string;
children: React.ReactNode;
footerText?: string;
footerLinkText?: string;
footerLinkHref?: string;
}
/* Waveform design β€” "Golden Studio": the auth visual column is a dim
control room: amber record light (pulsing LED + mono timecode), a live
waveform as the single hero object, and the tail of the guest pipeline
fading into canvas at the bottom. Collapses below 900px per the design
system (Β§5.4) β€” the form is never broken.
Bar heights are a fixed, hand-tuned array (never Math.random β€” would
cause hydration mismatches between SSR and client). */
const WAVE_HEIGHTS = [
12, 18, 26, 20, 34, 48, 42, 60, 84, 72, 96, 120, 104, 132, 116, 140, 124, 108,
128, 96, 112, 78, 88, 60, 68, 44, 52, 32, 40, 24, 28, 16, 20, 12,
];
const PIPELINE_CARDS = [
{ label: "Prospect", count: 8, color: "var(--status-prospect)", rotate: "-2deg" },
{ label: "Contacted", count: 5, color: "var(--status-contacted)", rotate: "1.5deg" },
{ label: "Scheduled", count: 3, color: "var(--status-scheduled)", rotate: "-1deg" },
];
function formatTimecode(totalSeconds: number): string {
const h = Math.floor(totalSeconds / 3600);
const m = Math.floor((totalSeconds % 3600) / 60);
const s = totalSeconds % 60;
return `${h}:${String(m).padStart(2, "0")}:${String(s).padStart(2, "0")}`;
}
export function AuthLayout({
title,
subtitle,
children,
footerText,
footerLinkText,
footerLinkHref,
}: AuthLayoutProps) {
return (
<div className="auth-layout-container">
<style>{authStyles}</style>
<AuthFormColumn title={title} subtitle={subtitle}>
{children}
{footerText && footerLinkText && footerLinkHref && (
<p className="auth-form-footer">
{footerText}{" "}
<Link href={footerLinkHref} style={{ color: "var(--color-primary)", fontWeight: 600, textDecoration: "none" }}>
{footerLinkText}
</Link>
</p>
)}
</AuthFormColumn>
<AuthVisualColumn />
</div>
);
}
/* ─── Form column (left, 40%) ──────────────────────────────── */
function AuthFormColumn({
title,
subtitle,
children,
}: {
title: string;
subtitle: string;
children: React.ReactNode;
}) {
return (
<div className="auth-form-col">
<div className="auth-form-brand">
<Link href="/" className="auth-brand-link">
<Image src="/logo.png" alt="MicProtocol" width={40} height={40} style={{ borderRadius: "var(--radius-md)", objectFit: "contain" }} />
<span className="auth-brand-name">MicProtocol</span>
</Link>
<ThemeToggle className="auth-theme-toggle" />
</div>
<div className="auth-form-center">
<div style={{ width: "100%", maxWidth: 420 }}>
<Link
href="/"
className="auth-back-link"
>
<ArrowLeft size={14} />
<span>Back to home</span>
</Link>
</div>
<div className="auth-card texture-surface texture-surface--fluted-glass">
<h1 className="auth-title">{title}</h1>
<p className="auth-subtitle">{subtitle}</p>
{children}
</div>
</div>
<div className="auth-form-meta">
<span>&copy; {new Date().getFullYear()} MicProtocol</span>
<div className="auth-form-meta-links">
<Link href="/privacy">Privacy</Link>
<Link href="/terms">Terms</Link>
<Link href="/security">Security</Link>
</div>
</div>
</div>
);
}
/* ─── Visual column (right, 60%) ───────────────────────────── */
function AuthVisualColumn() {
const [seconds, setSeconds] = useState(0);
useEffect(() => {
const id = window.setInterval(() => {
setSeconds((prev) => prev + 1);
}, 1000);
return () => window.clearInterval(id);
}, []);
return (
<div className="auth-visual-col texture-surface texture-surface--speckled-graphite" aria-hidden="true">
<div className="auth-visual-bg" />
{/* Record light */}
<div className="auth-rec-indicator">
<span className="auth-rec-dot" />
<span className="auth-rec-time">REC {formatTimecode(seconds)}</span>
</div>
{/* Live waveform β€” the hero object */}
<div className="auth-waveform">
{WAVE_HEIGHTS.map((height, i) => (
<span
key={i}
className="auth-waveform-bar"
style={{ height, animationDelay: `${(i % 12) * -160}ms` }}
/>
))}
</div>
{/* Pipeline tail */}
<div className="auth-pipeline-stack">
{PIPELINE_CARDS.map(({ label, count, color, rotate }) => (
<div key={label} className="auth-pipeline-card" style={{ transform: `rotate(${rotate})` }}>
<span className="auth-pipeline-dot" style={{ backgroundColor: color }} />
<span className="auth-pipeline-label">{label}</span>
<span className="auth-pipeline-count">{count}</span>
</div>
))}
</div>
</div>
);
}
/* ─── Styles ───────────────────────────────────────────────── */
const authStyles = `
.auth-layout-container {
display: grid;
grid-template-columns: 1fr;
min-height: 100vh;
background: var(--bg-primary);
}
/* ── Form column ── */
.auth-form-col {
display: flex;
flex-direction: column;
min-height: 100vh;
padding: clamp(1.5rem, 4vw, 2.5rem);
}
.auth-form-brand {
width: 100%;
max-width: 420px;
margin: 0 auto;
display: flex;
align-items: center;
justify-content: space-between;
}
.auth-theme-toggle {
padding: var(--space-2);
background: var(--bg-tertiary);
border: 1px solid var(--border-color);
border-radius: var(--radius-md);
color: var(--text-primary);
cursor: pointer;
transition: all var(--transition-fast);
display: inline-flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
}
.auth-theme-toggle:hover {
background: var(--bg-elevated);
border-color: var(--color-primary);
}
.auth-brand-link {
display: inline-flex;
align-items: center;
gap: var(--space-3);
color: inherit;
text-decoration: none;
}
.auth-brand-name {
font-family: var(--font-display);
font-size: var(--text-lg);
font-weight: 700;
letter-spacing: -0.02em;
}
.auth-form-center {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
width: 100%;
}
.auth-form-meta {
width: 100%;
max-width: 420px;
margin: 0 auto;
display: flex;
align-items: center;
justify-content: space-between;
font-size: var(--text-xs);
color: var(--text-tertiary);
}
.auth-form-meta-links {
display: flex;
gap: var(--space-4);
}
.auth-form-meta-links a {
color: var(--text-tertiary);
text-decoration: none;
transition: color var(--transition-fast);
}
.auth-form-meta-links a:hover {
color: var(--text-secondary);
}
.auth-card {
width: 100%;
max-width: 420px;
padding: clamp(1.5rem, 4vw, 2.5rem);
background: var(--bg-elevated);
border: 1px solid var(--border-color);
border-radius: var(--radius-lg);
box-shadow: var(--shadow-md);
animation: auth-card-in 0.45s var(--ease-out-expo);
}
@keyframes auth-card-in {
from {
opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.auth-title {
font-family: var(--font-display);
font-size: var(--text-2xl);
font-weight: 700;
letter-spacing: -0.03em;
line-height: 1.1;
color: var(--text-primary);
margin: 0 0 var(--space-2);
}
.auth-subtitle {
font-size: var(--text-sm);
line-height: 1.55;
color: var(--text-secondary);
margin: 0 0 var(--space-6);
}
.auth-form-footer {
margin: var(--space-6) 0 0;
text-align: center;
font-size: var(--text-xs);
color: var(--text-secondary);
}
/* ── Visual column ── */
.auth-visual-col {
display: none;
position: relative;
overflow: hidden;
background: var(--bg-secondary);
}
/* Dim control-room backdrop: low amber light source bottom-center
(the "record light" spilling onto the desk), cool top read */
.auth-visual-bg {
position: absolute;
inset: 0;
background:
radial-gradient(ellipse 60% 45% at 50% 92%, color-mix(in srgb, var(--color-primary) 10%, transparent), transparent 70%),
radial-gradient(ellipse 80% 60% at 50% 45%, color-mix(in srgb, var(--color-primary) 4%, transparent), transparent 70%),
var(--bg-secondary);
}
.auth-visual-bg::after {
content: "";
position: absolute;
inset: 0;
opacity: 0.4;
background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 256 256' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.6' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)' opacity='0.05'/%3E%3C/svg%3E");
pointer-events: none;
}
.auth-rec-indicator {
position: absolute;
top: clamp(1.5rem, 3vw, 2.5rem);
right: clamp(1.5rem, 3vw, 2.5rem);
display: inline-flex;
align-items: center;
gap: 10px;
padding: 8px 14px;
border-radius: var(--radius-full);
border: 1px solid var(--border-color);
background: color-mix(in srgb, var(--bg-primary) 70%, transparent);
z-index: 2;
}
.auth-rec-dot {
width: 8px;
height: 8px;
border-radius: var(--radius-full);
background: var(--color-primary);
animation: auth-rec-pulse 1.5s ease-in-out infinite;
}
@keyframes auth-rec-pulse {
0%, 100% {
box-shadow: 0 0 4px color-mix(in srgb, var(--color-primary) 60%, transparent);
opacity: 1;
}
50% {
box-shadow: 0 0 12px color-mix(in srgb, var(--color-primary) 90%, transparent);
opacity: 0.75;
}
}
.auth-rec-time {
font-family: var(--font-mono);
font-variant-numeric: tabular-nums;
font-size: var(--text-xs);
font-weight: 500;
color: var(--text-secondary);
letter-spacing: 0.02em;
}
/* Waveform β€” centered hero object */
.auth-waveform {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -55%);
display: flex;
align-items: center;
gap: 4px;
width: min(560px, 72%);
justify-content: center;
z-index: 1;
}
.auth-waveform-bar {
flex: 1;
max-width: 6px;
min-width: 2px;
border-radius: var(--radius-full);
background: linear-gradient(
180deg,
color-mix(in srgb, var(--color-primary) 55%, var(--bg-secondary)) 0%,
var(--color-primary) 100%
);
opacity: 0.85;
animation: auth-wave-motion 1.6s ease-in-out infinite alternate;
}
@keyframes auth-wave-motion {
from {
transform: scaleY(0.85);
opacity: 0.55;
}
to {
transform: scaleY(1.12);
opacity: 0.95;
}
}
/* Pipeline tail β€” three stage cards drifting down-right, fading out */
.auth-pipeline-stack {
position: absolute;
bottom: 0;
left: clamp(1.5rem, 4vw, 3rem);
width: min(280px, 55%);
display: flex;
flex-direction: column;
gap: var(--space-3);
padding-bottom: var(--space-6);
-webkit-mask-image: linear-gradient(180deg, #000 30%, transparent 98%);
mask-image: linear-gradient(180deg, #000 30%, transparent 98%);
z-index: 1;
}
.auth-pipeline-card {
display: flex;
align-items: center;
gap: var(--space-3);
padding: var(--space-3) var(--space-4);
border-radius: var(--radius-md);
background: var(--bg-elevated);
border: 1px solid var(--border-color);
box-shadow: var(--shadow-sm);
}
.auth-pipeline-dot {
width: 8px;
height: 8px;
border-radius: var(--radius-full);
flex-shrink: 0;
}
.auth-pipeline-label {
flex: 1;
font-size: var(--text-xs);
font-weight: 600;
color: var(--text-secondary);
}
.auth-pipeline-count {
font-family: var(--font-display);
font-size: var(--text-sm);
font-weight: 700;
font-variant-numeric: tabular-nums;
color: var(--text-primary);
}
/* ── Split at 900px: form + visual columns ── */
@media (min-width: 900px) {
.auth-layout-container {
grid-template-columns: 2fr 3fr;
}
.auth-visual-col {
display: block;
}
}
/* ── Big screens (>= 1200px): balanced form + visual, no container shrink ── */
@media (min-width: 1200px) {
.auth-layout-container {
grid-template-columns: 1fr 1fr;
}
.auth-form-col {
padding: clamp(2rem, 5vw, 3rem);
}
.auth-card {
max-width: 420px;
margin: 0 auto;
}
.auth-visual-col {
min-width: 0;
}
}
/* ── Reduced motion: calm the room ── */
@media (prefers-reduced-motion: reduce) {
.auth-waveform-bar,
.auth-rec-dot,
.auth-card {
animation: none;
}
.auth-rec-indicator {
background: var(--bg-primary);
}
}
/* ── Small screens: stricter rhythm ── */
@media (max-width: 480px) {
.auth-form-col {
padding: var(--space-4);
}
.auth-form-meta {
justify-content: center;
gap: var(--space-4);
}
}
`;