# Component conventions

These conventions MUST be followed by any agent creating or modifying components.

## File structure

Every component lives in its own folder:

```
src/components/atoms/Button/
├── Button.tsx            # The component
├── Button.stories.tsx    # Storybook stories
├── Button.test.tsx       # Unit tests (optional, added later)
└── index.ts              # Re-export
```

The `index.ts` always looks like:
```typescript
export { default } from './Button';
export * from './Button';
```

## Component template

```typescript
import React from 'react';
import { ButtonBase, ButtonBaseProps } from '@mui/material';
import { styled, useTheme } from '@mui/material/styles';

// ─── Types ───────────────────────────────────────────────────────────────────

/** Props for the Button component */
export interface ButtonProps extends Omit<ButtonBaseProps, 'color'> {
  /** Visual style variant */
  variant?: 'contained' | 'outlined' | 'text';
  /** Size preset */
  size?: 'small' | 'medium' | 'large';
  /** Colour intent */
  color?: 'primary' | 'secondary' | 'neutral';
  /** Show loading spinner and disable interaction */
  loading?: boolean;
  /** Full width of parent container */
  fullWidth?: boolean;
}

// ─── Styled wrapper (if needed) ──────────────────────────────────────────────

const StyledButton = styled(ButtonBase, {
  shouldForwardProp: (prop) => 
    !['variant', 'size', 'color', 'loading', 'fullWidth'].includes(prop as string),
})<ButtonProps>(({ theme, variant, size, color }) => ({
  // ALL values come from theme — never hardcode
  borderRadius: theme.shape.borderRadius,
  fontFamily: theme.typography.fontFamily,
  fontWeight: theme.typography.fontWeightMedium,
  transition: theme.transitions.create(
    ['background-color', 'box-shadow', 'border-color'],
    { duration: theme.transitions.duration.short }
  ),
  // ... variant-specific styles using theme values
}));

// ─── Component ───────────────────────────────────────────────────────────────

/** Primary action button for the FA design system */
export const Button: React.FC<ButtonProps> = ({
  variant = 'contained',
  size = 'medium',
  color = 'primary',
  loading = false,
  fullWidth = false,
  children,
  disabled,
  ...props
}) => {
  return (
    <StyledButton
      variant={variant}
      size={size}
      color={color}
      loading={loading}
      fullWidth={fullWidth}
      disabled={loading || disabled}
      {...props}
    >
      {loading ? 'Loading…' : children}
    </StyledButton>
  );
};

export default Button;
```

## Rules

### Theming
- **NEVER hardcode** colour values, spacing, font sizes, shadows, or border radii
- **Semantic tokens** (text, surface, border, interactive, feedback, typography, spacing): use MUI theme accessors — `theme.palette.*`, `theme.spacing()`, `theme.typography.*`, `theme.shape.*` — when inside a theme callback. CSS variables (`var(--fa-color-*)`, `var(--fa-spacing-*)`) are also acceptable for semantic tokens when more ergonomic (e.g., static colour maps, non-callback contexts).
- **Component tokens** (badge sizes, card shadows, input dimensions, etc.): use CSS variables (`var(--fa-badge-*)`, `var(--fa-card-*)`) — these are NOT mapped into the MUI theme.
- See decision D031 in `docs/memory/decisions-log.md` for full rationale.
- For one-off theme overrides, use the `sx` prop pattern
- Every component MUST accept and forward the `sx` prop for consumer overrides

### TypeScript
- Every prop must have a JSDoc description
- Use interface (not type) for props — interfaces produce better autodocs
- Export both the component and the props interface
- Use `Omit<>` to remove conflicting MUI base props

### Composition
- Prefer composition over configuration
- Small, focused components that compose well
- Avoid god-components with 20+ props — split into variants or sub-components
- Use React.forwardRef for all interactive elements (buttons, inputs, links)

### Accessibility
- All interactive elements must have a minimum 44×44px touch target
- Always include `aria-label` or visible label text
- Buttons must have `type="button"` unless they're form submit buttons
- Focus indicators must be visible — never remove outline without replacement
- Disabled elements should use `aria-disabled` alongside visual treatment

### MUI integration patterns

**When wrapping MUI components:**
```typescript
// Extend MUI's own props type
interface ButtonProps extends Omit<MuiButtonProps, 'color'> {
  // Add your custom props, omitting any MUI props you're overriding
}

// Forward all unknown props to MUI
const Button: React.FC<ButtonProps> = ({ customProp, ...muiProps }) => {
  return <MuiButton {...muiProps} />;
};
```

**When building from scratch with styled():**
```typescript
// Use shouldForwardProp to prevent custom props leaking to DOM
const StyledDiv = styled('div', {
  shouldForwardProp: (prop) => prop !== 'isActive',
})<{ isActive?: boolean }>(({ theme, isActive }) => ({
  backgroundColor: isActive 
    ? theme.palette.primary.main 
    : theme.palette.background.paper,
}));
```

**Theme-aware responsive styles:**
```typescript
const StyledCard = styled(Card)(({ theme }) => ({
  padding: theme.spacing(3),
  [theme.breakpoints.up('md')]: {
    padding: theme.spacing(4),
  },
}));
```

## Storybook story template

```typescript
import type { Meta, StoryObj } from '@storybook/react';
import { Button } from './Button';

const meta: Meta<typeof Button> = {
  title: 'Atoms/Button',       // Tier/ComponentName
  component: Button,
  tags: ['autodocs'],           // Always include for auto-documentation
  parameters: {
    layout: 'centered',         // Use 'centered' for atoms, 'fullscreen' for layouts
  },
  argTypes: {
    variant: {
      control: 'select',
      options: ['contained', 'outlined', 'text'],
      description: 'Visual style variant',
      table: { defaultValue: { summary: 'contained' } },
    },
    // ... one entry per prop
    onClick: { action: 'clicked' },  // Log actions for event props
  },
};

export default meta;
type Story = StoryObj<typeof Button>;

// ─── Individual stories ──────────────────────────────────────────────────────

/** Default button appearance */
export const Default: Story = {
  args: { children: 'Get started' },
};

/** All visual variants side by side */
export const AllVariants: Story = {
  render: () => (
    <div style={{ display: 'flex', gap: 16, alignItems: 'center' }}>
      <Button variant="contained">Contained</Button>
      <Button variant="outlined">Outlined</Button>
      <Button variant="text">Text</Button>
    </div>
  ),
};

/** All sizes side by side */
export const AllSizes: Story = {
  render: () => (
    <div style={{ display: 'flex', gap: 16, alignItems: 'center' }}>
      <Button size="small">Small</Button>
      <Button size="medium">Medium</Button>
      <Button size="large">Large</Button>
    </div>
  ),
};

/** Interactive states */
export const Disabled: Story = {
  args: { children: 'Disabled', disabled: true },
};

export const Loading: Story = {
  args: { children: 'Submitting', loading: true },
};
```

### Story naming conventions
- `title`: Use atomic tier as prefix: `Atoms/Button`, `Molecules/PriceCard`, `Organisms/PricingTable`
- Individual stories: PascalCase, descriptive of the state or variant shown
- Always include: `Default`, `AllVariants` (if applicable), `AllSizes` (if applicable), `Disabled`
- For composed components, include a story showing the component with realistic content

### Story coverage checklist
For every component, stories must cover:
- [ ] Default state with typical content
- [ ] All visual variants side by side
- [ ] All sizes side by side (if applicable)
- [ ] Disabled state
- [ ] Loading state (if applicable)
- [ ] Error state (if applicable)
- [ ] Long content / content overflow
- [ ] Empty/minimal content
- [ ] With and without optional elements (icons, badges, etc.)
