# Button Component Implementation Plan

## 1. TypeScript Type Definitions

### Discriminated Union Variant Type

```typescript
type ButtonVariant =
  | 'primary'
  | 'outline-gold'
  | 'outline-dark'
  | 'outline-light'
  | 'ghost'
  | 'circular'
  | 'icon-edit'
  | 'icon-delete'
  | 'file-upload';
```

### Props Interface (Discriminated Union via Generics)

```typescript
type TButtonBaseProps = {
  /**
   * Button variant — controls visual style and behavior.
   */
  variant: ButtonVariant;
  /**
   * Button size: 'small' | 'medium' | 'large'.
   * Maps to the design specs: primary/outline-gold=large, outline-dark/outline-light/ghost=medium,
   * circular/icon-edit/icon-delete=small, file-upload=medium.
   */
  size?: 'small' | 'medium' | 'large';
  /**
   * Whether the button is disabled.
   */
  disabled?: boolean;
  /**
   * Click handler.
   */
  onClick?: (e: React.MouseEvent<HTMLButtonElement>) => void;
  /**
   * Optional HTML button type attribute.
   */
  type?: 'button' | 'submit' | 'reset';
  /**
   * Optional className for external styling overrides.
   */
  className?: string;
  /**
   * Optional aria-label for accessibility.
   */
  'aria-label'?: string;
};

type TTextButtonProps = TButtonBaseProps & {
  /**
   * Button text content. Required for text-based variants.
   */
  children: React.ReactNode;
};

type TIconButtonProps = TButtonBaseProps & {
  /**
   * Icon children only — no text. For circular, icon-edit, icon-delete variants.
   */
  children: React.ReactNode;
  variant: 'circular' | 'icon-edit' | 'icon-delete';
};

type TFileUploadProps = TButtonBaseProps & {
  children: React.ReactNode;
  variant: 'file-upload';
  /**
   * File accept attribute (e.g., 'image/*', '.pdf').
   */
  accept?: string;
};

type TButtonProps = TTextButtonProps | TIconButtonProps | TFileUploadProps;
```

**Rationale:**
- Using a union of `TButtonProps` variants enforces at the type level that icon-only variants (`circular`, `icon-edit`, `icon-delete`) are distinct from text variants. This prevents developers from passing text to icon-only buttons.
- `size` is optional — the component maps the variant to the correct size automatically, but allows override for flexibility.
- `TTextButtonProps` is the broadest type; `TIconButtonProps` and `TFileUploadProps` narrow the `variant` field. TypeScript will correctly narrow `children` and `variant` when discriminated.

---

## 2. Component Structure (JSX)

```tsx
const Button: FC<TButtonProps> = ({
  variant,
  size,
  disabled = false,
  onClick,
  type = 'button',
  className = '',
  'aria-label': ariaLabel,
  children,
  accept,
}) => {
  // Auto-size mapping: variant → size
  const resolvedSize = size ?? variantToSize[variant];

  // Build class name: base + variant + size + state
  const classes = [
    'Button',
    `Button--${variant}`,
    `Button--${resolvedSize}`,
    disabled && 'Button--disabled',
    className,
  ]
    .filter(Boolean)
    .join(' ');

  // File-upload variant renders as a label wrapping an input
  if (variant === 'file-upload') {
    return (
      <label className={classes} onClick={onClick}>
        <input
          type="file"
          accept={accept}
          style={{ display: 'none' }}
          onChange={(e) => onClick?.(e as unknown as React.MouseEvent<HTMLButtonElement>)}
        />
        {children}
      </label>
    );
  }

  return (
    <button
      className={classes}
      type={type}
      disabled={disabled}
      onClick={onClick}
      aria-label={ariaLabel}
    >
      {children}
    </button>
  );
};
```

**Key Design Decisions:**
- **`<button>` for all text/icon variants** — semantic, accessible, keyboard-focusable.
- **`<label>` for `file-upload`** — clicking the label triggers the hidden `<input type="file">`. This is the standard pattern for custom file upload buttons.
- **`variantToSize` map** — a constant object mapping each variant to its default size, keeping the component DRY.
- **CSS class composition** — BEM modifier pattern: `.Button--primary`, `.Button--large`, `.Button--disabled`.

---

## 3. SCSS Architecture

### File: `src/components/Button/style.scss`

```scss
// ─── Variables (design tokens) ───────────────────────────────────────────
$color-gold: #EABF2D;
$color-amber: #D4880F;
$color-dark: #1A1A2E;
$color-white: #FFFFFF;
$color-danger: #DE3626;
$color-input-border: #DADCE0;
$color-muted: #9AA0A6;
$color-gray: #6B6B7B;

$font-size-xs: 11px;
$font-size-sm: 12px;
$font-size-md: 14px;
$font-size-lg: 16px;

$font-weight-bold: 700;
$font-weight-normal: 400;

$border-radius-sm: 4px;
$border-radius-md: 6px;
$border-radius-pill: 20px;
$border-radius-circle: 28px;

$border-width: 1.5px;

// ─── Base ────────────────────────────────────────────────────────────────
.Button {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  cursor: pointer;
  border: none;
  font-family: inherit;
  font-weight: $font-weight-bold;
  text-decoration: none;
  transition: all 0.2s ease;
  user-select: none;
  white-space: nowrap;

  // Disabled state
  &--disabled {
    opacity: 0.5;
    cursor: not-allowed;
    pointer-events: none;
  }

  // ─── Size variants ───────────────────────────────────────────────────
  &--small {
    width: 36px;
    height: 36px;
    font-size: $font-size-sm;
    border-radius: $border-radius-sm;
    padding: 0;
  }

  &--medium {
    height: 40px;
    font-size: $font-size-sm;
    padding: 0 16px;
  }

  &--large {
    height: 40px;
    font-size: $font-size-sm;
    padding: 0 24px;
    min-width: 170px;
  }

  // ─── Variant: primary ────────────────────────────────────────────────
  &--primary {
    background-color: $color-gold;
    color: $color-dark;
    border-radius: $border-radius-md;
    font-weight: $font-weight-bold;
  }

  // ─── Variant: outline-gold ───────────────────────────────────────────
  &--outline-gold {
    background-color: $color-white;
    border: $border-width solid $color-gold;
    color: $color-amber;
    border-radius: $border-radius-md;
    font-weight: $font-weight-bold;
  }

  // ─── Variant: outline-dark ───────────────────────────────────────────
  &--outline-dark {
    background-color: $color-white;
    border: $border-width solid $color-dark;
    color: $color-dark;
    border-radius: $border-radius-sm;

    &:hover {
      background-color: $color-amber;
      color: $color-white;
    }
  }

  // ─── Variant: outline-light ──────────────────────────────────────────
  &--outline-light {
    background-color: $color-dark;
    border: $border-width solid $color-white;
    color: $color-white;
    border-radius: $border-radius-sm;
  }

  // ─── Variant: ghost ──────────────────────────────────────────────────
  &--ghost {
    background-color: $color-white;
    border: $border-width solid $color-dark;
    color: $color-dark;
    border-radius: $border-radius-pill;

    &:hover {
      background-color: $color-amber;
      color: $color-white;
    }
  }

  // ─── Variant: circular ───────────────────────────────────────────────
  &--circular {
    width: 56px;
    height: 56px;
    border-radius: $border-radius-circle;
    background-color: $color-white;
    border: $border-width solid $color-dark;
    color: $color-dark;
    font-size: 20px;
    padding: 0;
  }

  // ─── Variant: icon-edit ──────────────────────────────────────────────
  &--icon-edit {
    background-color: $color-white;
    border: $border-width solid $color-gold;
    color: $color-amber;
    font-size: 16px;

    &:hover {
      background-color: $color-gold;
      color: $color-white;
    }
  }

  // ─── Variant: icon-delete ────────────────────────────────────────────
  &--icon-delete {
    background-color: $color-white;
    border: $border-width solid $color-danger;
    color: $color-danger;
    font-size: 14px;

    &:hover {
      background-color: $color-danger;
      color: $color-white;
    }
  }

  // ─── Variant: file-upload ────────────────────────────────────────────
  &--file-upload {
    background-color: $color-white;
    border: $border-width solid $color-gold;
    color: $color-amber;
    border-radius: $border-radius-sm;
    font-weight: $font-weight-normal; // file-upload is NOT bold

    &:hover {
      background-color: $color-gold;
      color: $color-white;
    }
  }
}
```

**SCSS Design Decisions:**
- **Variables at top** — all colors, sizes, and radii are tokenized for easy maintenance. If the design system changes a gold shade, it's one variable.
- **BEM modifier pattern** — `.Button--primary`, `.Button--circular`, etc. Each variant is a separate BEM modifier block.
- **Hover states** — `outline-dark`, `ghost`, `icon-edit`, `icon-delete`, and `file-upload` all have hover states defined in the design. These are handled with `&:hover` selectors.
- **`file-upload` is the only non-bold variant** — explicitly set `font-weight: $font-weight-normal`.
- **Size overrides** — `circular` explicitly sets `width: 56px; height: 56px` to override the medium size defaults. `icon-edit` and `icon-delete` use the small size defaults.
- **`padding: 0` for icon variants** — ensures icon-only buttons don't add unwanted horizontal padding.

---

## 4. Storybook Stories Structure

### File: `src/components/Button/Button.stories.tsx`

```tsx
import type { Meta, StoryObj } from '@storybook/react-webpack5';
import { Button } from '@components';

const meta: Meta<typeof Button> = {
  title: 'BL Consultores/Button',
  component: Button,
  parameters: {
    docs: {
      description: {
        component: 'A versatile button component with 9 variants for all BL Consultores UI contexts.',
      },
    },
  },
  argTypes: {
    variant: {
      control: 'select',
      options: [
        'primary',
        'outline-gold',
        'outline-dark',
        'outline-light',
        'ghost',
        'circular',
        'icon-edit',
        'icon-delete',
        'file-upload',
      ],
      description: 'Visual variant of the button',
    },
    size: {
      control: 'select',
      options: ['small', 'medium', 'large', undefined],
      description: 'Button size (auto-detected from variant if not specified)',
    },
    disabled: {
      control: 'boolean',
      description: 'Whether the button is disabled',
    },
    type: {
      control: 'select',
      options: ['button', 'submit', 'reset'],
      description: 'HTML button type attribute',
    },
    children: {
      control: 'text',
      description: 'Button content (text or icon)',
    },
    'aria-label': {
      control: 'text',
      description: 'Accessibility label for icon-only buttons',
    },
    accept: {
      control: 'text',
      description: 'File accept attribute (file-upload variant only)',
    },
  },
};

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

// ─── Text Button Variants ────────────────────────────────────────────────

export const Primary: Story = {
  args: {
    variant: 'primary',
    children: 'Primary Button',
  },
};

export const OutlineGold: Story = {
  args: {
    variant: 'outline-gold',
    children: 'Outline Gold',
  },
};

export const OutlineDark: Story = {
  args: {
    variant: 'outline-dark',
    children: 'Outline Dark',
  },
};

export const OutlineLight: Story = {
  args: {
    variant: 'outline-light',
    children: 'Outline Light',
  },
};

export const Ghost: Story = {
  args: {
    variant: 'ghost',
    children: 'Ghost Button',
  },
};

export const FileUpload: Story = {
  args: {
    variant: 'file-upload',
    children: 'Upload File',
    accept: 'image/*,.pdf',
  },
};

// ─── Icon-Only Variants ──────────────────────────────────────────────────

export const Circular: Story = {
  args: {
    variant: 'circular',
    children: '✕',
    'aria-label': 'Close',
  },
};

export const IconEdit: Story = {
  args: {
    variant: 'icon-edit',
    children: '✎',
    'aria-label': 'Edit',
  },
};

export const IconDelete: Story = {
  args: {
    variant: 'icon-delete',
    children: '✕',
    'aria-label': 'Delete',
  },
};

// ─── States ──────────────────────────────────────────────────────────────

export const PrimaryDisabled: Story = {
  args: {
    variant: 'primary',
    children: 'Primary Button',
    disabled: true,
  },
};

export const OutlineDarkHover: Story = {
  args: {
    variant: 'outline-dark',
    children: 'Hover Me',
  },
  play: async ({ canvasElement }) => {
    const button = canvasElement.querySelector('button');
    if (button) {
      button.dispatchEvent(new MouseEvent('mouseover', { bubbles: true }));
    }
  },
};

// ─── All Variants Overview ───────────────────────────────────────────────

export const AllVariants: Story = {
  render: () => (
    <div style={{ display: 'flex', flexWrap: 'wrap', gap: '16px', padding: '24px' }}>
      <Button variant="primary" children="Primary" />
      <Button variant="outline-gold" children="Outline Gold" />
      <Button variant="outline-dark" children="Outline Dark" />
      <Button variant="outline-light" children="Outline Light" />
      <Button variant="ghost" children="Ghost" />
      <Button variant="circular" children="✕" aria-label="Close" />
      <Button variant="icon-edit" children="✎" aria-label="Edit" />
      <Button variant="icon-delete" children="✕" aria-label="Delete" />
      <Button variant="file-upload" children="Upload" accept="image/*" />
    </div>
  ),
};
```

**Storybook Design Decisions:**
- **Separate stories per variant** — each variant gets its own named story for easy navigation in the Storybook sidebar.
- **`AllVariants` overview story** — shows all variants together for quick visual comparison (common DS pattern).
- **`OutlineDarkHover` with `play`** — demonstrates the hover state via Storybook's Play function.
- **`aria-label` documented** — icon-only variants require it; the `argTypes` documents this.

---

## 5. Test Structure

### Jest Test: `src/components/__tests__/Button.test.tsx`

```tsx
import { render, screen } from '@testing-library/react';
import { Button } from '@components';

describe('<Button /> Component', () => {
  it('renders text content', () => {
    render(<Button variant="primary" children="Click me" />);
    expect(screen.getByText('Click me')).toBeInTheDocument();
  });

  it('applies the correct variant class', () => {
    render(<Button variant="primary" children="Click me" />);
    const button = screen.getByRole('button');
    expect(button).toHaveClass('Button--primary');
  });

  it('renders as a button element', () => {
    render(<Button variant="primary" children="Click me" />);
    expect(screen.getByRole('button')).toBeInTheDocument();
  });

  it('is disabled when disabled prop is true', () => {
    render(<Button variant="primary" children="Click me" disabled />);
    expect(screen.getByRole('button')).toBeDisabled();
  });

  it('applies disabled class when disabled', () => {
    render(<Button variant="primary" children="Click me" disabled />);
    expect(screen.getByRole('button')).toHaveClass('Button--disabled');
  });

  it('calls onClick when clicked', () => {
    const handleClick = jest.fn();
    render(<Button variant="primary" children="Click me" onClick={handleClick} />);
    screen.getByText('Click me').click();
    expect(handleClick).toHaveBeenCalledTimes(1);
  });

  it('renders file-upload as a label with hidden input', () => {
    render(<Button variant="file-upload" children="Upload" accept="image/*" />);
    expect(screen.getByRole('label')).toBeInTheDocument();
    expect(screen.getByLabelText('')).toHaveAttribute('accept', 'image/*');
  });

  it('renders icon variant with aria-label', () => {
    render(<Button variant="circular" children="✕" aria-label="Close" />);
    expect(screen.getByRole('button', { name: 'Close' })).toBeInTheDocument();
  });

  it('accepts custom className', () => {
    render(<Button variant="primary" children="Click me" className="custom-class" />);
    expect(screen.getByText('Click me')).toHaveClass('custom-class');
  });

  it('renders with correct type attribute', () => {
    render(<Button variant="primary" children="Submit" type="submit" />);
    expect(screen.getByRole('button')).toHaveAttribute('type', 'submit');
  });
});
```

### Cypress Test: `src/components/__tests__/Button.test.cy.tsx`

```tsx
import { Button } from '@components';

describe('Testing Button Component', () => {
  it('renders primary button with text', () => {
    cy.mount(<Button variant="primary" children="Primary Button" />);
    cy.get('button').should('have.class', 'Button--primary');
    cy.get('button').contains('Primary Button');
  });

  it('renders all variants', () => {
    const variants: Array<{ variant: string; label: string }> = [
      { variant: 'primary', label: 'Primary' },
      { variant: 'outline-gold', label: 'Outline Gold' },
      { variant: 'outline-dark', label: 'Outline Dark' },
      { variant: 'outline-light', label: 'Outline Light' },
      { variant: 'ghost', label: 'Ghost' },
      { variant: 'circular', label: 'Circular' },
      { variant: 'icon-edit', label: 'Icon Edit' },
      { variant: 'icon-delete', label: 'Icon Delete' },
      { variant: 'file-upload', label: 'File Upload' },
    ];

    variants.forEach(({ variant, label }) => {
      cy.mount(<Button variant={variant} children={label} />);
      cy.get('.Button').should('have.class', `Button--${variant}`);
    });
  });

  it('handles hover states for outline-dark', () => {
    cy.mount(<Button variant="outline-dark" children="Hover Me" />);
    cy.get('button').trigger('mouseover');
    cy.get('button').should('have.css', 'background-color');
  });

  it('disables button when disabled prop is true', () => {
    cy.mount(<Button variant="primary" children="Disabled" disabled />);
    cy.get('button').should('be.disabled');
  });

  it('renders file-upload as a label', () => {
    cy.mount(<Button variant="file-upload" children="Upload" accept="image/*" />);
    cy.get('label').should('exist');
    cy.get('input[type="file"]').should('exist');
  });
});
```

**Test Design Decisions:**
- **Jest tests** — focus on React rendering behavior: DOM structure, props → class mapping, event handling, disabled state.
- **Cypress tests** — focus on visual/interaction behavior: CSS class presence, hover states, DOM structure for file-upload.
- **`file-upload` as `<label>`** — tested separately since it doesn't render a `<button>` element.
- **All 9 variants covered** — both Jest and Cypress verify each variant renders with its correct CSS class.

---

## 6. Export Update

### File: `src/components/index.tsx` (updated)

```tsx
export * from './Card';
export * from './Button';
```

### File: `src/components/Button/index.tsx` (exports)

```tsx
export { Button };
export type { TButtonProps, ButtonVariant };
```

---

## 7. Implementation Checklist

- [ ] Create `src/components/Button/index.tsx` with type definitions + component
- [ ] Create `src/components/Button/style.scss` with all variant styles
- [ ] Create `src/components/Button/Button.stories.tsx` with all stories
- [ ] Create `src/components/__tests__/Button.test.tsx` (Jest)
- [ ] Create `src/components/__tests__/Button.test.cy.tsx` (Cypress)
- [ ] Update `src/components/index.tsx` to export Button
- [ ] Run `npm test` — all tests pass
- [ ] Run `npm run storybook` — all stories render correctly
- [ ] Run `npm run cy:run` — all Cypress tests pass

---

## 8. Design Decisions Summary

| Decision | Rationale |
|----------|-----------|
| Single component, 9 variants | Reduces maintenance; variants are visual differences, not behavioral ones |
| Discriminated union types | TypeScript enforces which props are valid per variant at compile time |
| BEM CSS with variant modifiers | Matches existing Card pattern; easy to add/remove variants |
| `size` is optional, auto-mapped | Variant → size mapping is a design constant; override available for edge cases |
| `file-upload` renders `<label>` | Standard pattern for custom file inputs; keeps it keyboard-accessible |
| Hover states in SCSS | All hover transitions are defined in the design spec; handled purely in CSS |
| Separate Jest + Cypress tests | Jest for unit/rendering logic; Cypress for visual/DOM verification |
| Storybook stories per variant | Follows Storybook best practices; easy for designers/developers to browse |
