432 lines
18 KiB
JavaScript
432 lines
18 KiB
JavaScript
// ============================================================
|
||
// auth-screens.jsx — Sign-in / Sign-up / Onboarding for the
|
||
// Lattice brand, in three aesthetic directions that match the
|
||
// three nav styles from the prior file:
|
||
//
|
||
// A · Light minimal ← Sidebar / Notion school
|
||
// B · Dark split-hero ← Topbar / Vercel school
|
||
// C · Glass aurora ← Floating-pill marketing school
|
||
//
|
||
// Each style ships all three screens. Shared <LatticeMark>,
|
||
// <SocialBtn>, <Field> components keep the family resemblance.
|
||
// ============================================================
|
||
|
||
// ── Shared atoms ────────────────────────────────────────────
|
||
// Branded "G" / "MS" social logos drawn inline as little glyphs
|
||
// so there are no missing-asset placeholders. They're recognizable
|
||
// without using actual brand marks.
|
||
const GoogleGlyph = () => (
|
||
<svg width="16" height="16" viewBox="0 0 24 24" aria-hidden="true">
|
||
<path fill="#4285F4" d="M21.6 12.2c0-.7-.1-1.4-.2-2H12v3.8h5.4c-.2 1.2-.9 2.2-2 2.9v2.4h3.2c1.9-1.7 3-4.3 3-7.1z"/>
|
||
<path fill="#34A853" d="M12 22c2.7 0 5-.9 6.6-2.4l-3.2-2.4c-.9.6-2 1-3.4 1-2.6 0-4.8-1.7-5.6-4.1H3.1v2.5C4.8 19.8 8.2 22 12 22z"/>
|
||
<path fill="#FBBC05" d="M6.4 14.1c-.2-.6-.3-1.3-.3-2s.1-1.4.3-2V7.6H3.1C2.4 9 2 10.4 2 12s.4 3 1.1 4.4l3.3-2.3z"/>
|
||
<path fill="#EA4335" d="M12 6c1.5 0 2.8.5 3.8 1.5l2.9-2.9C16.9 3.1 14.7 2 12 2 8.2 2 4.8 4.2 3.1 7.6l3.3 2.5C7.2 7.7 9.4 6 12 6z"/>
|
||
</svg>
|
||
);
|
||
const MicrosoftGlyph = () => (
|
||
<svg width="14" height="14" viewBox="0 0 24 24" aria-hidden="true">
|
||
<rect x="1" y="1" width="10" height="10" fill="#F25022"/>
|
||
<rect x="13" y="1" width="10" height="10" fill="#7FBA00"/>
|
||
<rect x="1" y="13" width="10" height="10" fill="#00A4EF"/>
|
||
<rect x="13" y="13" width="10" height="10" fill="#FFB900"/>
|
||
</svg>
|
||
);
|
||
const AppleGlyph = () => (
|
||
<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
|
||
<path d="M16.4 12.7c0-2.6 2.1-3.8 2.2-3.9-1.2-1.7-3-2-3.6-2-1.5-.2-3 .9-3.8.9-.8 0-2-.9-3.3-.9-1.7 0-3.3 1-4.2 2.6-1.8 3.1-.5 7.7 1.3 10.2.9 1.2 1.9 2.6 3.2 2.5 1.3-.1 1.8-.8 3.4-.8 1.6 0 2 .8 3.4.8 1.4 0 2.3-1.2 3.1-2.5.7-1 1.1-2 1.4-3-2.6-1-3.1-3.7-3.1-3.9zM13.5 5c.7-.9 1.2-2.1 1.1-3.4-1 .1-2.3.7-3 1.6-.7.8-1.3 2-1.1 3.2 1.2.1 2.3-.6 3-1.4z"/>
|
||
</svg>
|
||
);
|
||
|
||
const sansAuth = "'Inter', -apple-system, BlinkMacSystemFont, system-ui, sans-serif";
|
||
|
||
// Tiny stroke icon helper (re-defining locally so this file is standalone)
|
||
const Icn = ({ d, size = 16, sw = 1.6 }) => (
|
||
<svg width={size} height={size} viewBox="0 0 24 24" fill="none"
|
||
stroke="currentColor" strokeWidth={sw}
|
||
strokeLinecap="round" strokeLinejoin="round">{d}</svg>
|
||
);
|
||
const Pa = {
|
||
eye: <><path d="M2 12s3.5-7 10-7 10 7 10 7-3.5 7-10 7S2 12 2 12z"/><circle cx="12" cy="12" r="3"/></>,
|
||
check: <path d="M5 12l5 5L20 7"/>,
|
||
arrow: <path d="M5 12h14M13 5l7 7-7 7"/>,
|
||
chevR: <path d="m9 6 6 6-6 6"/>,
|
||
chevL: <path d="m15 6-6 6 6 6"/>,
|
||
star: <path d="m12 3 2.6 6.2 6.7.5-5.1 4.4 1.6 6.6L12 17.3 6.2 20.7l1.6-6.6L2.7 9.7l6.7-.5z"/>,
|
||
spark: <path d="M12 3v4M12 17v4M3 12h4M17 12h4M6 6l3 3M15 15l3 3M6 18l3-3M15 9l3-3"/>,
|
||
bolt: <path d="m13 2-9 13h7l-1 7 9-13h-7z"/>,
|
||
shield: <path d="M12 2 4 5v7c0 5 3.5 9 8 10 4.5-1 8-5 8-10V5z"/>,
|
||
briefcase: <><rect x="3" y="7" width="18" height="13" rx="2"/><path d="M8 7V5a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2M3 13h18"/></>,
|
||
};
|
||
|
||
// Brand mark (gradient triangle), shared
|
||
const Mark = ({ size = 20, mono }) => (
|
||
<svg width={size} height={size} viewBox="0 0 24 24" fill="none">
|
||
{mono ? (
|
||
<path d="M3 20 L12 4 L21 20 Z" fill="currentColor"/>
|
||
) : (
|
||
<>
|
||
<defs>
|
||
<linearGradient id={`mk${size}`} x1="0" y1="0" x2="1" y2="1">
|
||
<stop offset="0%" stopColor="#6e6cff"/>
|
||
<stop offset="100%" stopColor="#b15bff"/>
|
||
</linearGradient>
|
||
</defs>
|
||
<path d="M3 20 L12 4 L21 20 Z" fill={`url(#mk${size})`}/>
|
||
</>
|
||
)}
|
||
</svg>
|
||
);
|
||
|
||
// ============================================================
|
||
// STYLE A — LIGHT MINIMAL (Notion / Linear school)
|
||
// Centered card on warm neutral, no flourish, lots of air.
|
||
// ============================================================
|
||
const a = {
|
||
bg: "#f5f5f2", surface: "#ffffff",
|
||
border: "#e8e8e3", borderStrong: "#d8d8d2",
|
||
text: "#111", subtext: "#5a5a5e", muted: "#8a8a90",
|
||
accent: "#5e5cff", accentText: "#fff",
|
||
};
|
||
const fontA = sansAuth;
|
||
|
||
const AFieldLabel = ({ children, optional }) => (
|
||
<div style={{
|
||
display: "flex", justifyContent: "space-between", alignItems: "baseline",
|
||
fontSize: 12, fontWeight: 500, color: a.text, marginBottom: 6,
|
||
}}>
|
||
<span>{children}</span>
|
||
{optional && <span style={{ color: a.muted, fontWeight: 400 }}>optional</span>}
|
||
</div>
|
||
);
|
||
|
||
const AField = ({ label, value, placeholder, hint, type = "text", icon, optional }) => (
|
||
<div style={{ marginBottom: 14 }}>
|
||
{label && <AFieldLabel optional={optional}>{label}</AFieldLabel>}
|
||
<div style={{
|
||
display: "flex", alignItems: "center", gap: 8,
|
||
padding: "10px 12px", borderRadius: 7,
|
||
background: "#fff", border: `1px solid ${a.border}`,
|
||
fontSize: 13, color: value ? a.text : a.muted,
|
||
boxShadow: "0 1px 0 #00000004",
|
||
}}>
|
||
{icon && <span style={{ color: a.muted, display: "flex" }}>{icon}</span>}
|
||
<span style={{ flex: 1 }}>{value || placeholder}</span>
|
||
{type === "password" && <span style={{ color: a.muted, display: "flex" }}>
|
||
<Icn d={Pa.eye} size={14} /></span>}
|
||
</div>
|
||
{hint && <div style={{ fontSize: 11, color: a.muted, marginTop: 5 }}>{hint}</div>}
|
||
</div>
|
||
);
|
||
|
||
const ASocial = ({ children, glyph }) => (
|
||
<button style={{
|
||
flex: 1, display: "flex", alignItems: "center", justifyContent: "center", gap: 8,
|
||
padding: "10px 12px", borderRadius: 7, background: "#fff",
|
||
border: `1px solid ${a.border}`, color: a.text, fontSize: 13,
|
||
fontFamily: fontA, fontWeight: 500, cursor: "pointer",
|
||
}}>
|
||
{glyph}
|
||
<span>{children}</span>
|
||
</button>
|
||
);
|
||
|
||
const APrimary = ({ children, full = true }) => (
|
||
<button style={{
|
||
width: full ? "100%" : "auto",
|
||
padding: "11px 18px", borderRadius: 7,
|
||
background: "#111", color: "#fff", border: "none",
|
||
fontSize: 13, fontWeight: 500, fontFamily: fontA, cursor: "pointer",
|
||
display: "flex", alignItems: "center", justifyContent: "center", gap: 6,
|
||
}}>{children}</button>
|
||
);
|
||
|
||
const ACardShell = ({ children, foot }) => (
|
||
<div style={{
|
||
width: "100%", height: "100%", background: a.bg,
|
||
color: a.text, fontFamily: fontA,
|
||
display: "grid", gridTemplateRows: "auto 1fr auto",
|
||
}}>
|
||
{/* Top bar: brand on left, support on right */}
|
||
<header style={{
|
||
display: "flex", justifyContent: "space-between", alignItems: "center",
|
||
padding: "20px 28px",
|
||
}}>
|
||
<div style={{ display: "flex", alignItems: "center", gap: 8, fontWeight: 600, fontSize: 14 }}>
|
||
<Mark size={20} />
|
||
Lattice
|
||
</div>
|
||
<div style={{ fontSize: 12, color: a.subtext, display: "flex", gap: 18 }}>
|
||
<span>Status</span>
|
||
<span>Docs</span>
|
||
<span>Sign in ↗</span>
|
||
</div>
|
||
</header>
|
||
|
||
{/* Centered card */}
|
||
<main style={{ display: "flex", alignItems: "center", justifyContent: "center", padding: 24 }}>
|
||
<div style={{
|
||
width: 420, padding: "32px 36px", borderRadius: 12,
|
||
background: a.surface, border: `1px solid ${a.border}`,
|
||
boxShadow: "0 1px 2px #0000000a, 0 8px 32px -12px #0000000f",
|
||
}}>
|
||
{children}
|
||
</div>
|
||
</main>
|
||
|
||
{/* Footer band */}
|
||
<footer style={{
|
||
display: "flex", justifyContent: "space-between", alignItems: "center",
|
||
padding: "16px 28px", fontSize: 11, color: a.muted,
|
||
}}>
|
||
<span>© 2026 Lattice Studio · Made in Copenhagen</span>
|
||
<div style={{ display: "flex", gap: 16 }}>
|
||
<span>Privacy</span><span>Terms</span><span>Security</span>
|
||
</div>
|
||
</footer>
|
||
</div>
|
||
);
|
||
|
||
const ASignIn = () => (
|
||
<ACardShell>
|
||
<h1 style={{ fontSize: 22, fontWeight: 600, margin: 0, letterSpacing: "-0.01em" }}>
|
||
Welcome back
|
||
</h1>
|
||
<p style={{ fontSize: 13, color: a.subtext, margin: "6px 0 22px" }}>
|
||
Sign in to your Lattice workspace.
|
||
</p>
|
||
|
||
<div style={{ display: "flex", gap: 8, marginBottom: 18 }}>
|
||
<ASocial glyph={<GoogleGlyph/>}>Google</ASocial>
|
||
<ASocial glyph={<MicrosoftGlyph/>}>Microsoft</ASocial>
|
||
<ASocial glyph={<span style={{ color: a.text, display: "flex" }}><AppleGlyph/></span>}>Apple</ASocial>
|
||
</div>
|
||
|
||
<div style={{
|
||
display: "flex", alignItems: "center", gap: 10,
|
||
fontSize: 11, color: a.muted, margin: "0 0 18px",
|
||
}}>
|
||
<div style={{ flex: 1, height: 1, background: a.border }}></div>
|
||
<span style={{ textTransform: "uppercase", letterSpacing: "0.08em" }}>or with email</span>
|
||
<div style={{ flex: 1, height: 1, background: a.border }}></div>
|
||
</div>
|
||
|
||
<AField label="Email" value="mira@lattice.co" />
|
||
<div style={{ marginBottom: 14 }}>
|
||
<div style={{
|
||
display: "flex", justifyContent: "space-between", alignItems: "baseline",
|
||
fontSize: 12, fontWeight: 500, color: a.text, marginBottom: 6,
|
||
}}>
|
||
<span>Password</span>
|
||
<span style={{ color: a.accent, cursor: "pointer", fontWeight: 400 }}>Forgot?</span>
|
||
</div>
|
||
<div style={{
|
||
display: "flex", alignItems: "center", gap: 8,
|
||
padding: "10px 12px", borderRadius: 7, background: "#fff",
|
||
border: `1px solid ${a.border}`, fontSize: 13, color: a.text,
|
||
letterSpacing: "0.2em",
|
||
}}>
|
||
<span style={{ flex: 1 }}>••••••••••</span>
|
||
<span style={{ color: a.muted, display: "flex" }}><Icn d={Pa.eye} size={14}/></span>
|
||
</div>
|
||
</div>
|
||
|
||
<div style={{ display: "flex", alignItems: "center", gap: 8, marginBottom: 18 }}>
|
||
<div style={{
|
||
width: 14, height: 14, borderRadius: 3, background: "#111",
|
||
display: "flex", alignItems: "center", justifyContent: "center",
|
||
color: "#fff",
|
||
}}><Icn d={Pa.check} size={10} sw={2.4}/></div>
|
||
<span style={{ fontSize: 12, color: a.subtext }}>Keep me signed in for 30 days</span>
|
||
</div>
|
||
|
||
<APrimary>Sign in →</APrimary>
|
||
|
||
<div style={{ fontSize: 12, color: a.subtext, marginTop: 18, textAlign: "center" }}>
|
||
New here? <span style={{ color: a.text, fontWeight: 500, cursor: "pointer" }}>
|
||
Create an account
|
||
</span>
|
||
</div>
|
||
</ACardShell>
|
||
);
|
||
|
||
const ASignUp = () => (
|
||
<ACardShell>
|
||
<h1 style={{ fontSize: 22, fontWeight: 600, margin: 0, letterSpacing: "-0.01em" }}>
|
||
Create your workspace
|
||
</h1>
|
||
<p style={{ fontSize: 13, color: a.subtext, margin: "6px 0 22px" }}>
|
||
Free for up to 10 people. No card needed.
|
||
</p>
|
||
|
||
<div style={{ display: "flex", gap: 8, marginBottom: 18 }}>
|
||
<ASocial glyph={<GoogleGlyph/>}>Continue with Google</ASocial>
|
||
<ASocial glyph={<MicrosoftGlyph/>}>Microsoft</ASocial>
|
||
</div>
|
||
|
||
<div style={{
|
||
display: "flex", alignItems: "center", gap: 10,
|
||
fontSize: 11, color: a.muted, margin: "0 0 18px",
|
||
}}>
|
||
<div style={{ flex: 1, height: 1, background: a.border }}></div>
|
||
<span style={{ textTransform: "uppercase", letterSpacing: "0.08em" }}>or with email</span>
|
||
<div style={{ flex: 1, height: 1, background: a.border }}></div>
|
||
</div>
|
||
|
||
<AField label="Full name" placeholder="Mira Reyes" />
|
||
<AField label="Work email" value="mira@lattice.co"
|
||
hint="We'll send a 6-digit code to confirm." />
|
||
<AField label="Password" value="••••••••••" type="password"
|
||
hint="At least 10 characters, including a number." />
|
||
|
||
<div style={{ display: "flex", alignItems: "flex-start", gap: 8, margin: "4px 0 18px" }}>
|
||
<div style={{
|
||
width: 14, height: 14, borderRadius: 3, marginTop: 2,
|
||
background: "#fff", border: `1px solid ${a.borderStrong}`,
|
||
}}></div>
|
||
<span style={{ fontSize: 12, color: a.subtext, lineHeight: 1.5 }}>
|
||
I agree to Lattice's <span style={{ color: a.text, fontWeight: 500 }}>Terms</span> and{" "}
|
||
<span style={{ color: a.text, fontWeight: 500 }}>Privacy Policy</span>.
|
||
</span>
|
||
</div>
|
||
|
||
<APrimary>Create workspace →</APrimary>
|
||
|
||
<div style={{ fontSize: 12, color: a.subtext, marginTop: 18, textAlign: "center" }}>
|
||
Already have one? <span style={{ color: a.text, fontWeight: 500, cursor: "pointer" }}>
|
||
Sign in
|
||
</span>
|
||
</div>
|
||
</ACardShell>
|
||
);
|
||
|
||
const AOnboarding = () => {
|
||
const Step = ({ n, label, state }) => (
|
||
<div style={{ display: "flex", alignItems: "center", gap: 8, flex: 1, minWidth: 0 }}>
|
||
<div style={{
|
||
width: 22, height: 22, borderRadius: "50%",
|
||
background: state === "done" ? "#22c55e" : state === "active" ? "#111" : "transparent",
|
||
color: state === "todo" ? a.muted : "#fff",
|
||
border: state === "todo" ? `1px solid ${a.borderStrong}` : "none",
|
||
display: "flex", alignItems: "center", justifyContent: "center",
|
||
fontSize: 11, fontWeight: 600, flexShrink: 0,
|
||
}}>{state === "done" ? <Icn d={Pa.check} size={12} sw={2.4} /> : n}</div>
|
||
<div style={{ fontSize: 12, color: state === "todo" ? a.muted : a.text,
|
||
whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" }}>{label}</div>
|
||
</div>
|
||
);
|
||
|
||
const Tile = ({ title, sub, selected, icon }) => (
|
||
<div style={{
|
||
padding: 14, borderRadius: 8, cursor: "pointer", textAlign: "left",
|
||
border: selected ? `1.5px solid ${a.accent}` : `1px solid ${a.border}`,
|
||
background: selected ? "#f6f5ff" : "#fff",
|
||
boxShadow: selected ? `0 0 0 3px ${a.accent}1a` : "0 1px 0 #00000004",
|
||
}}>
|
||
<div style={{
|
||
width: 28, height: 28, borderRadius: 7, marginBottom: 10,
|
||
background: selected ? a.accent : "#f1f0eb",
|
||
color: selected ? "#fff" : a.subtext,
|
||
display: "flex", alignItems: "center", justifyContent: "center",
|
||
}}>{icon}</div>
|
||
<div style={{ fontSize: 13, fontWeight: 500, marginBottom: 2 }}>{title}</div>
|
||
<div style={{ fontSize: 11, color: a.muted, lineHeight: 1.4 }}>{sub}</div>
|
||
</div>
|
||
);
|
||
|
||
return (
|
||
<div style={{
|
||
width: "100%", height: "100%", background: a.bg, color: a.text,
|
||
fontFamily: fontA, display: "grid", gridTemplateRows: "auto 1fr auto",
|
||
}}>
|
||
<header style={{
|
||
display: "flex", justifyContent: "space-between", alignItems: "center",
|
||
padding: "20px 28px",
|
||
}}>
|
||
<div style={{ display: "flex", alignItems: "center", gap: 8, fontWeight: 600, fontSize: 14 }}>
|
||
<Mark size={20} /> Lattice
|
||
</div>
|
||
<div style={{ fontSize: 12, color: a.subtext }}>Step 2 of 4 · ⌘. to skip</div>
|
||
</header>
|
||
|
||
<main style={{
|
||
padding: "12px 28px 28px",
|
||
display: "flex", flexDirection: "column", alignItems: "center",
|
||
}}>
|
||
<div style={{
|
||
width: 640, padding: "30px 36px 36px", borderRadius: 14,
|
||
background: a.surface, border: `1px solid ${a.border}`,
|
||
boxShadow: "0 1px 2px #0000000a, 0 8px 32px -12px #0000000f",
|
||
}}>
|
||
{/* Stepper */}
|
||
<div style={{ display: "flex", alignItems: "center", gap: 4, marginBottom: 22 }}>
|
||
<Step n="1" label="Account" state="done" />
|
||
<div style={{ flex: 1, height: 1, background: a.border, margin: "0 6px" }}></div>
|
||
<Step n="2" label="Workspace" state="active" />
|
||
<div style={{ flex: 1, height: 1, background: a.border, margin: "0 6px" }}></div>
|
||
<Step n="3" label="Invite team" state="todo" />
|
||
<div style={{ flex: 1, height: 1, background: a.border, margin: "0 6px" }}></div>
|
||
<Step n="4" label="Import" state="todo" />
|
||
</div>
|
||
|
||
<h1 style={{ fontSize: 24, fontWeight: 600, margin: 0, letterSpacing: "-0.02em" }}>
|
||
Tell us about your work
|
||
</h1>
|
||
<p style={{ fontSize: 13, color: a.subtext, margin: "6px 0 22px" }}>
|
||
We'll tailor your workspace based on this. You can change it later.
|
||
</p>
|
||
|
||
<AField label="Workspace name" value="Lattice Studio"
|
||
hint="This is how your team will see it." />
|
||
|
||
<div style={{ fontSize: 12, fontWeight: 500, margin: "16px 0 8px" }}>
|
||
What do you do?
|
||
</div>
|
||
<div style={{ display: "grid", gridTemplateColumns: "repeat(3, 1fr)", gap: 10 }}>
|
||
<Tile title="Sales & Revenue" sub="Pipeline, contacts, deals" selected icon={<Icn d={Pa.bolt} size={15}/>} />
|
||
<Tile title="Operations" sub="Vendors, ops, suppliers" icon={<Icn d={Pa.shield} size={15}/>} />
|
||
<Tile title="Product" sub="Customers, feedback, research" icon={<Icn d={Pa.spark} size={15}/>} />
|
||
<Tile title="Recruiting" sub="Candidates, pipeline" icon={<Icn d={Pa.briefcase} size={15}/>} />
|
||
<Tile title="Just exploring" sub="I'll figure it out" icon={<Icn d={Pa.star} size={15}/>} />
|
||
</div>
|
||
|
||
<div style={{ fontSize: 12, fontWeight: 500, margin: "20px 0 8px" }}>How big is your team?</div>
|
||
<div style={{ display: "flex", gap: 6 }}>
|
||
{["Just me", "2–10", "11–50", "51–200", "200+"].map((s, i) => (
|
||
<div key={s} style={{
|
||
flex: 1, padding: "9px 8px", textAlign: "center", borderRadius: 7,
|
||
fontSize: 12, fontWeight: 500, cursor: "pointer",
|
||
border: i === 1 ? `1.5px solid ${a.accent}` : `1px solid ${a.border}`,
|
||
background: i === 1 ? "#f6f5ff" : "#fff",
|
||
color: i === 1 ? a.accent : a.subtext,
|
||
}}>{s}</div>
|
||
))}
|
||
</div>
|
||
|
||
<div style={{
|
||
display: "flex", justifyContent: "space-between", marginTop: 26, alignItems: "center",
|
||
}}>
|
||
<button style={{
|
||
background: "transparent", border: "none", color: a.subtext,
|
||
fontSize: 13, fontFamily: fontA, cursor: "pointer", padding: 0,
|
||
}}>← Back</button>
|
||
<APrimary full={false}>Continue <Icn d={Pa.arrow} size={13}/></APrimary>
|
||
</div>
|
||
</div>
|
||
</main>
|
||
|
||
<footer style={{
|
||
display: "flex", justifyContent: "space-between", alignItems: "center",
|
||
padding: "16px 28px", fontSize: 11, color: a.muted,
|
||
}}>
|
||
<span>Press <code style={{
|
||
background: "#fff", padding: "1px 5px", borderRadius: 3,
|
||
border: `1px solid ${a.border}`, fontFamily: "monospace",
|
||
}}>⌘ + Enter</code> to continue</span>
|
||
<span>Need help? <span style={{ color: a.text, fontWeight: 500 }}>support@lattice.co</span></span>
|
||
</footer>
|
||
</div>
|
||
);
|
||
};
|
||
|
||
Object.assign(window, { ASignIn, ASignUp, AOnboarding });
|