Files
design-system/docs/superpowers/specs/2026-03-24-login-dialog-design.md
hsiegeln be23161582 docs: fix onSubmit optionality in login dialog spec
Make onSubmit optional so the social-only variant works cleanly
without requiring a no-op callback.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-24 10:47:50 +01:00

6.7 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  // Omit to hide credentials section
  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:

  1. Logo slot — optional ReactNode rendered centered above title
  2. Title — "Sign in" default, centered
  3. Server errorAlert 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 fieldFormField + Input, required, placeholder "you@example.com"
  7. Password fieldFormField + Input type="password", required, placeholder "••••••••"
  8. Remember me / Forgot password rowCheckbox on the left, amber link on the right. Forgot password link hidden when onForgotPassword omitted.
  9. Submit buttonButton 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:

  • FullsocialProviders + onSubmit both provided. Social buttons, divider, and credentials all shown.
  • Credentials onlyonSubmit provided, no socialProviders. Social section and divider hidden.
  • Social onlysocialProviders provided, onSubmit omitted. Credentials section (email, password, remember me, submit button) and divider hidden.

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