docs: add login dialog design spec
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
173
docs/superpowers/specs/2026-03-24-login-dialog-design.md
Normal file
173
docs/superpowers/specs/2026-03-24-login-dialog-design.md
Normal file
@@ -0,0 +1,173 @@
|
||||
# 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/`.
|
||||
|
||||
```tsx
|
||||
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.
|
||||
|
||||
```tsx
|
||||
interface LoginDialogProps extends LoginFormProps {
|
||||
open: boolean
|
||||
onClose: () => void
|
||||
}
|
||||
```
|
||||
|
||||
Uses `Modal` with `size="sm"` (400px).
|
||||
|
||||
## Layout
|
||||
|
||||
Social-first ordering, top to bottom:
|
||||
|
||||
1. **Logo slot** — optional `ReactNode` rendered centered above title
|
||||
2. **Title** — "Sign in" default, centered
|
||||
3. **Server error** — `Alert variant="error"` shown when `error` prop is set, between title and social buttons
|
||||
4. **Social buttons** — stacked vertically, each is a `Button variant="secondary"` with icon + label. Hidden when `socialProviders` is empty/omitted.
|
||||
5. **Divider** — horizontal rule with "or" text, centered. Hidden when social section is hidden.
|
||||
6. **Email field** — `FormField` + `Input`, required, placeholder "you@example.com"
|
||||
7. **Password field** — `FormField` + `Input type="password"`, required, placeholder "••••••••"
|
||||
8. **Remember me / Forgot password row** — `Checkbox` on the left, amber link on the right. Forgot password link hidden when `onForgotPassword` omitted.
|
||||
9. **Submit button** — `Button variant="primary"`, full width, label "Sign in"
|
||||
10. **Sign up link** — "Don't have an account? Sign up" centered below. Hidden when `onSignUp` omitted.
|
||||
|
||||
### Configuration Variants
|
||||
|
||||
The form adapts automatically based on props:
|
||||
|
||||
- **Full** — social providers + credentials + all links
|
||||
- **Credentials only** — no `socialProviders` passed, social section and divider hidden
|
||||
- **Social only** — only `socialProviders` passed, credentials section hidden (make `onSubmit` optional for this case, or accept it will never fire)
|
||||
|
||||
## Validation
|
||||
|
||||
Client-side, triggered on form submit (not on blur):
|
||||
|
||||
| Field | Rule | Error message |
|
||||
|----------|---------------------------------------------------|----------------------------------------|
|
||||
| Email | Required | "Email is required" |
|
||||
| Email | 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" |
|
||||
|
||||
- `onSubmit` only fires when all validation passes
|
||||
- Field errors displayed inline below each input using `FormField` error pattern (red border + message)
|
||||
- Field errors clear when the user starts typing in that field
|
||||
- Server `error` prop clears automatically on next submit attempt
|
||||
|
||||
## States
|
||||
|
||||
### Loading
|
||||
|
||||
When `loading={true}`:
|
||||
- All inputs disabled
|
||||
- All social buttons disabled
|
||||
- Submit button shows `Spinner` component, text hidden (matches existing `Button loading` pattern)
|
||||
- Form cannot be submitted
|
||||
|
||||
### Error
|
||||
|
||||
- Server error: `Alert variant="error"` rendered between title and social buttons
|
||||
- Field errors: inline below each input via `FormField` error 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 uses `var(--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 with `aria-label="Sign in"`
|
||||
- Labels tied to inputs via `htmlFor`/`id`
|
||||
- Error messages linked with `aria-describedby`
|
||||
- First input auto-focused on mount
|
||||
- `LoginDialog` traps 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 wrapper
|
||||
- `Input` — email and password fields
|
||||
- `Checkbox` — remember me
|
||||
- `Button` — submit (primary) + social buttons (secondary)
|
||||
- `Alert` — server error display
|
||||
- `Spinner` — loading state in submit button
|
||||
- `Modal` — 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 `socialProviders` is empty
|
||||
- Hides divider when no social providers
|
||||
- Hides forgot password link when `onForgotPassword` omitted
|
||||
- Hides sign up link when `onSignUp` omitted
|
||||
- Shows server error Alert when `error` prop set
|
||||
- Validates required email
|
||||
- Validates email format
|
||||
- Validates required password
|
||||
- Validates password minimum length
|
||||
- Clears field errors on typing
|
||||
- Calls `onSubmit` with credentials when valid
|
||||
- Does not call `onSubmit` when validation fails
|
||||
- Disables form when `loading={true}`
|
||||
- Shows spinner on submit button when loading
|
||||
- Calls social provider `onClick` when clicked
|
||||
- Calls `onForgotPassword` when link clicked
|
||||
- Calls `onSignUp` when link clicked
|
||||
|
||||
### LoginDialog tests:
|
||||
- Renders Modal with LoginForm when `open={true}`
|
||||
- Does not render when `open={false}`
|
||||
- Calls `onClose` on backdrop click / Esc
|
||||
- Passes all LoginForm props through
|
||||
Reference in New Issue
Block a user