Auth card shell
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
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:29walmart-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.