6.6 KiB
6.6 KiB
Login Dialog Design Spec
Overview
A composable login component for the Cameleer3 design system. Provides a LoginForm content component and a LoginDialog wrapper that puts it inside a Modal. Supports username/password credentials, configurable social/SSO providers, and built-in client-side validation.
Components
LoginForm
Core form component. Lives in src/design-system/composites/LoginForm/.
interface SocialProvider {
label: string // e.g. "Continue with Google"
icon?: ReactNode // SVG icon, optional
onClick: () => void
}
interface LoginFormProps {
logo?: ReactNode
title?: string // Default: "Sign in"
socialProviders?: SocialProvider[] // Omit or [] to hide social section + divider
onSubmit: (credentials: { email: string; password: string; remember: boolean }) => void
onForgotPassword?: () => void // Omit to hide link
onSignUp?: () => void // Omit to hide "Don't have an account?"
error?: string // Server-side error, rendered as Alert
loading?: boolean // Disables form, spinner on submit button
className?: string
}
LoginDialog
Thin wrapper — passes all LoginFormProps through to LoginForm, adds Modal control.
interface LoginDialogProps extends LoginFormProps {
open: boolean
onClose: () => void
}
Uses Modal with size="sm" (400px).
Layout
Social-first ordering, top to bottom:
- Logo slot — optional
ReactNoderendered centered above title - Title — "Sign in" default, centered
- Server error —
Alert variant="error"shown whenerrorprop is set, between title and social buttons - Social buttons — stacked vertically, each is a
Button variant="secondary"with icon + label. Hidden whensocialProvidersis empty/omitted. - Divider — horizontal rule with "or" text, centered. Hidden when social section is hidden.
- Email field —
FormField+Input, required, placeholder "you@example.com" - Password field —
FormField+Input type="password", required, placeholder "••••••••" - Remember me / Forgot password row —
Checkboxon the left, amber link on the right. Forgot password link hidden whenonForgotPasswordomitted. - Submit button —
Button variant="primary", full width, label "Sign in" - Sign up link — "Don't have an account? Sign up" centered below. Hidden when
onSignUpomitted.
Configuration Variants
The form adapts automatically based on props:
- Full — social providers + credentials + all links
- Credentials only — no
socialProviderspassed, social section and divider hidden - Social only — only
socialProviderspassed, credentials section hidden (makeonSubmitoptional for this case, or accept it will never fire)
Validation
Client-side, triggered on form submit (not on blur):
| Field | Rule | Error message |
|---|---|---|
| Required | "Email is required" | |
Basic format: /^[^\s@]+@[^\s@]+\.[^\s@]+$/ |
"Please enter a valid email address" | |
| Password | Required | "Password is required" |
| Password | Minimum 8 characters | "Password must be at least 8 characters" |
onSubmitonly fires when all validation passes- Field errors displayed inline below each input using
FormFielderror pattern (red border + message) - Field errors clear when the user starts typing in that field
- Server
errorprop clears automatically on next submit attempt
States
Loading
When loading={true}:
- All inputs disabled
- All social buttons disabled
- Submit button shows
Spinnercomponent, text hidden (matches existingButton loadingpattern) - Form cannot be submitted
Error
- Server error:
Alert variant="error"rendered between title and social buttons - Field errors: inline below each input via
FormFielderror styling (red border, error text)
Styling
- CSS Modules:
LoginForm.module.css - All colors via CSS custom properties from
tokens.css - Dark mode works automatically — no extra overrides needed
- Social buttons:
var(--bg-surface)background,var(--border)border, hover usesvar(--bg-hover) - Divider:
var(--border)line,var(--text-muted)"or" text - Forgot password + Sign up links:
var(--amber)color,font-weight: 500 - Form gap: 14px between fields
- Social button gap: 8px between buttons
Accessibility
<form>element witharia-label="Sign in"- Labels tied to inputs via
htmlFor/id - Error messages linked with
aria-describedby - First input auto-focused on mount
LoginDialogtraps focus via Modal- Social buttons are
<button>elements, keyboard-navigable - Alert uses
role="alert"for screen readers - Enter key submits form (standard
<form onSubmit>)
File Structure
src/design-system/composites/LoginForm/
LoginForm.tsx
LoginForm.module.css
LoginForm.test.tsx
LoginDialog.tsx
LoginDialog.test.tsx
Exports added to src/design-system/composites/index.ts.
Primitives Reused
FormField— label + error display wrapperInput— email and password fieldsCheckbox— remember meButton— submit (primary) + social buttons (secondary)Alert— server error displaySpinner— loading state in submit buttonModal— LoginDialog wrapper
Testing
Tests with Vitest + React Testing Library, wrapped in ThemeProvider.
LoginForm tests:
- Renders all elements when all props provided
- Hides social section when
socialProvidersis empty - Hides divider when no social providers
- Hides forgot password link when
onForgotPasswordomitted - Hides sign up link when
onSignUpomitted - Shows server error Alert when
errorprop set - Validates required email
- Validates email format
- Validates required password
- Validates password minimum length
- Clears field errors on typing
- Calls
onSubmitwith credentials when valid - Does not call
onSubmitwhen validation fails - Disables form when
loading={true} - Shows spinner on submit button when loading
- Calls social provider
onClickwhen clicked - Calls
onForgotPasswordwhen link clicked - Calls
onSignUpwhen link clicked
LoginDialog tests:
- Renders Modal with LoginForm when
open={true} - Does not render when
open={false} - Calls
onCloseon backdrop click / Esc - Passes all LoginForm props through