Login: clone WorkClaw's two-panel /sign-in shell (Clawmates-branded)

Restyled /login into WorkClaw's two-panel auth design: a white form card on
the left, a periwinkle "meet the crew" panel on the right (claw roster strip,
"Your AI crew, all hands on deck", the claws-at-desk art), light-themed via
the marketing palette. The right panel is hidden on mobile.

- Kept /login as the canonical route (workspace redirect, /api/auth/login,
  and the whole E2E sign-in flow depend on it) and kept our email/password
  mechanism — only the chrome changed. Field labels (Email/Password) and the
  "Sign in" button are preserved so p0–p6 keep passing.
- Added /sign-in → /login redirect so WorkClaw's URL resolves.
- New AuthShell wraps both the local form and the Clerk SignIn branch.
- LoginForm restyled to the light card aesthetic (rounded inputs, full-width
  ink button); uses AA-safe coral-strong for the focus ring / error.
- Regenerated the login visual baseline.

typecheck/lint/build green · p0-shell (full sign-in flow) green · p6-a11y
green · login + marketing @visual green · deployed and verified live
(/login 200, /sign-in 307 → /login).

Co-Authored-By: Claude Opus 4.8 <[email protected]>
This commit is contained in:
Omar Sobh
2026-06-13 14:13:20 -05:00
co-authored by Claude Opus 4.8
parent df589b70d4
commit a5ae0d6042
5 changed files with 125 additions and 20 deletions
+16 -13
View File
@@ -1,3 +1,4 @@
import { AuthShell } from "@/components/auth/AuthShell";
import { LoginForm } from "@/components/auth/LoginForm"; import { LoginForm } from "@/components/auth/LoginForm";
import { authMode } from "@/lib/auth/mode"; import { authMode } from "@/lib/auth/mode";
@@ -5,22 +6,24 @@ export default async function LoginPage() {
if (authMode() === "clerk") { if (authMode() === "clerk") {
const { SignIn } = await import("@clerk/nextjs"); const { SignIn } = await import("@clerk/nextjs");
return ( return (
<main className="flex min-h-dvh items-center justify-center"> <AuthShell
<SignIn routing="hash" /> title="Your crew is"
</main> accent="on deck"
subtitle="Sign in to your workspace"
>
<div className="flex justify-center">
<SignIn routing="hash" />
</div>
</AuthShell>
); );
} }
return ( return (
<main className="flex min-h-dvh flex-col items-center justify-center gap-6 motion-safe:animate-[fade-up_var(--duration-normal)_var(--ease-app)]"> <AuthShell
<div className="text-center"> title="Your crew is"
<h1 className="font-mono text-xl font-semibold tracking-tight"> accent="on deck"
clawmates subtitle="Sign in to your workspace"
</h1> >
<p className="pt-1 text-sm text-muted-foreground">
Sign in to your workspace
</p>
</div>
<LoginForm /> <LoginForm />
</main> </AuthShell>
); );
} }
+8
View File
@@ -0,0 +1,8 @@
import { redirect } from "next/navigation";
// WorkClaw signs in at /sign-in; our canonical route is /login (the workspace
// redirect, the backend /api/auth/login, and the E2E flow all use it). Keep
// the familiar URL working by redirecting here.
export default function SignInPage() {
redirect("/login");
}
@@ -0,0 +1,93 @@
import Link from "next/link";
import { PawPrint } from "lucide-react";
import type { ReactNode } from "react";
/** Two-panel auth shell mirroring WorkClaw's /sign-in: a white form card on
* the left, a periwinkle "meet the crew" panel on the right (hidden on
* mobile). Light-themed via the `marketing` palette. The actual auth UI
* (our email/password form, or Clerk) is passed as children. */
export function AuthShell({
title,
accent,
subtitle,
children,
}: {
title: string;
accent: string;
subtitle: string;
children: ReactNode;
}) {
return (
<div className="marketing flex min-h-dvh items-center justify-center bg-[#e9eef5] p-4 text-marketing-ink">
<div className="grid w-full max-w-[1040px] overflow-hidden rounded-[32px] bg-white shadow-[0_32px_80px_-32px_rgba(22,29,39,0.35)] lg:grid-cols-2">
{/* Left — form card */}
<div className="flex flex-col items-center gap-8 px-8 py-12 sm:px-14">
<Link href="/" aria-label="clawmates" className="flex items-center gap-2">
<span className="flex size-9 items-center justify-center rounded-xl bg-marketing-ink">
<PawPrint aria-hidden size={20} className="text-marketing-coral" />
</span>
<span className="text-lg font-bold tracking-tight text-marketing-ink">
clawmates
</span>
</Link>
<div className="text-center">
<h1
className="font-extrabold tracking-[-0.04em]"
style={{ fontSize: "clamp(34px, 4vw, 44px)", lineHeight: 0.95 }}
>
{title}
<br />
<span className="text-marketing-coral-strong">{accent}</span>
</h1>
<p className="pt-3 text-base font-medium text-marketing-slate">
{subtitle}
</p>
</div>
<div className="w-full max-w-sm">{children}</div>
<p className="max-w-sm text-center text-xs text-marketing-slate">
By continuing, you agree to our{" "}
<a href="#" className="underline hover:text-marketing-ink">
Terms of Service
</a>{" "}
and{" "}
<a href="#" className="underline hover:text-marketing-ink">
Privacy Policy
</a>
.
</p>
</div>
{/* Right — crew panel (decorative; hidden on mobile) */}
<div className="relative hidden flex-col items-center justify-between overflow-hidden bg-[#ebf4fd] py-12 lg:flex">
<div
className="absolute inset-0 opacity-[0.5]"
style={{
backgroundImage: "url('/images/marketing/dot-pattern.svg')",
backgroundSize: "cover",
backgroundPosition: "center",
}}
/>
<div className="relative w-[140%] max-w-none opacity-90">
{/* eslint-disable-next-line @next/next/no-img-element */}
<img
src="/images/marketing/claw-carousel-strip.png"
alt=""
className="block h-auto w-full"
/>
</div>
<p className="relative px-8 text-center text-xl font-semibold text-marketing-ink">
Your AI crew, all hands on deck
</p>
<div className="relative w-[88%]">
{/* eslint-disable-next-line @next/next/no-img-element */}
<img
src="/images/marketing/claws-at-desk.png"
alt="The clawmate crew"
className="block h-auto w-full"
/>
</div>
</div>
</div>
</div>
);
}
+8 -7
View File
@@ -33,39 +33,40 @@ export function LoginForm() {
return ( return (
<form <form
onSubmit={handleSubmit} onSubmit={handleSubmit}
className={`flex w-full max-w-sm flex-col gap-3 ${ className={`flex w-full flex-col gap-4 ${
failed ? "motion-safe:animate-[shake_0.4s_var(--ease-app)]" : "" failed ? "motion-safe:animate-[shake_0.4s_var(--ease-app)]" : ""
}`} }`}
> >
<label className="flex flex-col gap-1 text-xs text-muted-foreground"> <label className="flex flex-col gap-1.5 text-sm font-medium text-marketing-slate-dark">
Email Email
<input <input
name="email" name="email"
type="email" type="email"
required required
autoComplete="email" autoComplete="email"
className="rounded-(--radius) border border-input bg-subtle px-3 py-2 text-sm text-foreground outline-none focus:border-coral" placeholder="e.g. [email protected]"
className="rounded-xl border border-[#e2e8f0] bg-white px-4 py-3 text-base text-marketing-ink outline-none transition-colors placeholder:text-marketing-slate/60 focus:border-marketing-coral-strong"
/> />
</label> </label>
<label className="flex flex-col gap-1 text-xs text-muted-foreground"> <label className="flex flex-col gap-1.5 text-sm font-medium text-marketing-slate-dark">
Password Password
<input <input
name="password" name="password"
type="password" type="password"
required required
autoComplete="current-password" autoComplete="current-password"
className="rounded-(--radius) border border-input bg-subtle px-3 py-2 text-sm text-foreground outline-none focus:border-coral" className="rounded-xl border border-[#e2e8f0] bg-white px-4 py-3 text-base text-marketing-ink outline-none transition-colors focus:border-marketing-coral-strong"
/> />
</label> </label>
{failed && ( {failed && (
<p role="alert" className="text-xs text-coral"> <p role="alert" className="text-sm text-marketing-coral-strong">
Invalid email or password. Invalid email or password.
</p> </p>
)} )}
<button <button
type="submit" type="submit"
disabled={pending} disabled={pending}
className="mt-1 rounded-(--radius-button) bg-coral px-4 py-2 text-sm font-medium text-background shadow-(--shadow-cta) hover:bg-coral-light disabled:opacity-50" className="mt-1 flex h-12 items-center justify-center rounded-full bg-marketing-ink px-4 text-base font-semibold text-white transition-colors hover:bg-marketing-slate-dark disabled:opacity-50"
> >
{pending ? "Signing in…" : "Sign in"} {pending ? "Signing in…" : "Sign in"}
</button> </button>
Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 264 KiB