Pattern gallery

Auth card shell

The card-as-page-shell for signed-out routes: brand mark, title, subtitle, form, then the way out.

What this solves

Convergent implementation with no shared source. Every signed-out screen in both apps is the identical composition, a centered viewport, a fixed-width card, a stacked brand mark, a title, an optional subtitle, then the screen's own content, and the two apps built it independently under two different names. This composite is that shape as a kit primitive, so the next app never invents it a third way.
Use it when
  • Any signed-out route: sign in, forgot password, reset password, no-access, logout.speedway's five auth routes all wrap in the same AuthCard.tsx shell.
  • A signup screen, or any other entry point before a session exists.walmart-mvp's Login and Signup both wrap in the AuthShell defined inside Login.tsx.

Rendered

Fixture data; check both themes.
Versable

Sign in

Use the email and password for your account

Email
Password

Forgot your password?

patterns/auth-card-shell/example.tsxCopy this into a fresh route and it renders as above.
"use client";
import { Alert, Button, Card, Input, RenderIcon, Tabs } from "@versable-git/ui";
import { useState } from "react";
// Both apps independently built this shell under two different names, no
// shared source: a centered fixed-width Card, a stacked brand mark, an h1
// title, an optional subtitle, then the screen's own content (speedway's
// AuthCard.tsx:8-38, walmart's AuthShell in Login.tsx:98-113). The Tabs knob
// switches one shell through its four real states.
type View = "signin" | "error" | "pending" | "no-access";
const COPY: Record<View, { title: string; subtitle: string }> = {
signin: { title: "Sign in", subtitle: "Use the email and password for your account" },
error: { title: "Sign in", subtitle: "Use the email and password for your account" },
pending: { title: "Sign in", subtitle: "Use the email and password for your account" },
"no-access": {
title: "You're not in an org yet",
subtitle: "Your account isn't a member of any organization. Ask your team owner to add you.",
},
};
export function AuthCardShell() {
const [view, setView] = useState<View>("signin");
const { title, subtitle } = COPY[view];
const pending = view === "pending";
return (
<div className="flex flex-col gap-3">
<Tabs
size="sm"
value={view}
onChange={(v) => setView(v as View)}
items={[
{ value: "signin", label: "Sign in" },
{ value: "error", label: "Error" },
{ value: "pending", label: "Pending" },
{ value: "no-access", label: "No access" },
]}
/>
<div className="bg-base-200 rounded-box flex justify-center p-8">
{/* Card + centered max-w-md + stacked brand mark, the shell both
AuthCard and AuthShell converge on. */}
<Card className="w-full max-w-md" noAnimate>
<div className="mb-2 flex flex-col items-center gap-2">
<span className="bg-primary/10 text-primary grid size-9 place-items-center rounded-lg">
<RenderIcon Icon="Sparkle" size={18} />
</span>
<span className="text-base-content/70 text-xs font-semibold tracking-wide uppercase">Versable</span>
</div>
<h1 className="text-center text-2xl font-semibold">{title}</h1>
<h3 className="text-base-content/65 mt-1 mb-4 text-center text-sm">{subtitle}</h3>
{view === "no-access" ? (
// No color at all marks a dead-end/exit action (canon doc 2 §A2),
// the same shape as speedway's no-access.tsx:17-19.
<div className="flex justify-center">
<Button type="button" Icon="SignOut">
Sign out
</Button>
</div>
) : (
<form className="flex flex-col gap-4" onSubmit={(e) => e.preventDefault()}>
{/* fullWidth belongs to signed-out auth screens only (canon doc 2 §A3). */}
<Input top="Email" type="email" autoComplete="username" readOnly={pending} fullWidth />
<Input top="Password" type="password" autoComplete="current-password" readOnly={pending} fullWidth />
{view === "error" && <Alert tone="error">Incorrect email or password.</Alert>}
<Button type="submit" color="primary" Icon="RightArrow" iconRight loading={pending} fullWidth>
Sign in
</Button>
<p className="text-base-content/65 text-center text-sm">Forgot your password?</p>
</form>
)}
</Card>
</div>
</div>
);
}

Where it ships

  • speedway/app/components/AuthCard.tsx:8-38the shipped shell: Card, stacked logo and wordmark, h1 title, optional subtitle, children. Consumed by login.tsx:67, forgot.tsx:48, reset.$token.tsx:52, no-access.tsx:12, logout.tsx:29
  • walmart-mvp/frontend/src/pages/Login.tsx:98-113the same shell under a different name, AuthShell, built independently with no shared source. Consumed by Login.tsx:40 and Signup.tsx:41

App-specific: Real routes wire the form to a loader and an action (session cookies, safe-next redirects, timing-constant password checks) instead of the fixture's no-op submit, and the brand mark here is a placeholder icon in place of the two apps' real logo assets. The no-access exit deliberately carries no color, per canon doc 2 §A2's rule that a dead-end action stays colorless.

@versable-git/ui · composites proven in the apps