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:
co-authored by
Claude Opus 4.8
parent
df589b70d4
commit
a5ae0d6042
@@ -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
|
||||||
|
title="Your crew is"
|
||||||
|
accent="on deck"
|
||||||
|
subtitle="Sign in to your workspace"
|
||||||
|
>
|
||||||
|
<div className="flex justify-center">
|
||||||
<SignIn routing="hash" />
|
<SignIn routing="hash" />
|
||||||
</main>
|
</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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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 |
Reference in New Issue
Block a user