Skip to main content

Overview

The library follows a variant router pattern that provides a clean separation between component APIs and their implementations. This architectural pattern enables flexible component variants while maintaining a consistent developer experience.

Variant Router Pattern

The core architectural pattern consists of three layers:

1. Main Component (Router)

The main component acts as a router that delegates to variant implementations based on props. It provides a single, unified API for consumers.
Button.tsx
import React, { ComponentPropsWithoutRef } from "react";

import ButtonPrimary from "./ButtonPrimary";
import ButtonSecondary from "./ButtonSecondary";
import ButtonText from "./ButtonText";
import ButtonImage from "./ButtonImage";
import CallToAction from "./CallToAction";

const Button = ({ design, onSuccess, ...restProps }: ButtonProps) => {
  return design === "secondary" ? (
    <ButtonSecondary onSuccess={onSuccess} {...restProps} />
  ) : design === "text" ? (
    <ButtonText {...restProps} />
  ) : design === "image" ? (
    <ButtonImage {...restProps} />
  ) : design === "call-to-action" ? (
    <CallToAction {...restProps} />
  ) : (
    <ButtonPrimary onSuccess={onSuccess} {...restProps} />
  );
};
The main component uses conditional logic to route to the appropriate variant based on the design prop, with a default fallback to the primary variant.

2. Variant Components (Implementations)

Each variant is a self-contained implementation with its own styled-components and logic:
ButtonPrimary.tsx
import React, { ComponentPropsWithoutRef, useEffect, useState } from "react";
import styled from "styled-components";
import { Player } from "@lottiefiles/react-lottie-player";

import AnimationCheck from "@assets/animations/button-check.json";
import AnimationLoading from "@assets/animations/button-loading.json";
import Text from "@components/Text/Text";
import Color from "@constants/Color";

const ButtonPrimary = styled.button<{
  $size?: "small" | "normal";
  $loading?: boolean;
  $countdown?: boolean;
  $success?: boolean;
}>`
  display: flex;
  flex-direction: row;
  align-items: center;
  justify-content: center;
  height: ${(props) => (props.$size === "small" ? "40px" : "56px")};
  padding: ${(props) =>
    props.$loading
      ? "0px"
      : props.$size === "small"
      ? "0px 12px"
      : "0px 24px"};
  border-radius: 1000px;
  border: none;
  color: white;
  background-color: ${(props) =>
    props.$success ? Color.status.color.success : Color.background.primary};
  opacity: ${(props) => (props.disabled ? 0.48 : 1)};
  cursor: ${(props) =>
    props.disabled || props.$loading || props.$countdown
      ? "default"
      : "pointer"};
`;
Variant implementations use transient props (prefixed with $) for styled-components to prevent props from being passed to the DOM element.

3. Type Definitions

Components export comprehensive TypeScript interfaces that document all available props:
export interface ButtonProps extends ComponentPropsWithoutRef<"button"> {
  /** Visual variant: `primary` for main actions, `secondary` for less emphasis */
  design?: "primary" | "secondary" | "text" | "image" | "call-to-action";
  /** Button size: `normal` (default) or `small` for compact layouts */
  size?: "small" | "normal";
  icon?: React.ReactElement;
  iconPosition?: "left" | "right";
  /** Shows animated loading spinner (Lottie animation) */
  loading?: boolean;
  disabled?: boolean;
  /** Triggers success animation (Lottie checkmark) */
  success?: boolean;
  /** Callback fired when success animation completes */
  onSuccess?: (success: boolean) => void;
}

Text Component Example

The Text component demonstrates the pattern routing to different semantic HTML elements:
Text.tsx
import Header, { HeaderProps } from "./Header";
import Paragraph, { ParagraphProps } from "./Paragraph";
import Button, { ButtonProps } from "./Button";

const Text = (props: TextProps) => {
  return props.type === "d1" ||
    props.type === "h1" ||
    props.type === "h2" ||
    props.type === "h3" ||
    props.type === "h4" ||
    props.type === "h5" ||
    props.type === "h6" ? (
    <Header role="header" {...props}>
      {props.children}
    </Header>
  ) : props.type === "p" ||
    props.type === "p2" ||
    props.type === "c1" ||
    props.type === "c2" ||
    props.type === "o1" ||
    props.type === "o2" ? (
    <Paragraph role="paragraph" {...props}>
      {props.children}
    </Paragraph>
  ) : props.type === "b1" || props.type === "b2" || props.type === "b3" ? (
    <Button role="button" {...props}>
      {props.children}
    </Button>
  ) : null;
};

export type TextProps = HeaderProps | ParagraphProps | ButtonProps;

Directory Structure

Components are organized following a consistent pattern:
src/
├── components/
│   ├── Button/
│   │   ├── Button.tsx              # Main router component
│   │   ├── ButtonPrimary.tsx       # Primary variant
│   │   ├── ButtonSecondary.tsx     # Secondary variant
│   │   ├── ButtonText.tsx          # Text variant
│   │   ├── ButtonImage.tsx         # Image variant
│   │   └── Button.stories.tsx      # Storybook documentation
│   ├── Text/
│   │   ├── Text.tsx                # Main router
│   │   ├── Header.tsx              # Header variant
│   │   ├── Paragraph.tsx           # Paragraph variant
│   │   ├── Button.tsx              # Button text variant
│   │   └── Text.stories.tsx
│   └── Input/                      # Complex components use subdirectories
│       ├── Basic/
│       ├── Phone/
│       ├── Location/
│       └── ...
├── constants/                      # Shared constants
│   ├── Color.tsx
│   ├── ColorV2.tsx
│   └── Country.tsx
└── assets/                         # Static assets
    └── animations/

Export Pattern

The library uses a centralized export strategy for clean imports:

1. Component-Level Exports

Each component exports its main component and types:
export default Button;
export interface ButtonProps { /* ... */ }

2. Components Index

All components are exported through src/components/index.ts:
src/components/index.ts
export { default as Button } from "./Button/Button";
export { default as Text } from "./Text/Text";
export { default as Input } from "./Input/Basic/Input";
export { default as Modal } from "./Modal/Modal";
// ... all other components

3. Root Index

The root src/index.ts re-exports everything:
src/index.ts
export * from "./components";
export * from "./constants";

4. Consumer Usage

Consumers import from the package name:
import { Button, Text, Input, Color, ColorV2 } from "@adoptaunabuelo/react-components";
This pattern ensures all imports are clean and consistent, with no need for deep path imports.

Benefits of This Architecture

Separation of Concerns

Each variant is isolated with its own logic and styles, making it easier to maintain and test.

Consistent API

Consumers interact with a single component interface, regardless of which variant they’re using.

Easy Extension

Adding new variants doesn’t break existing code—just add a new variant file and update the router.

Type Safety

TypeScript ensures all variants implement compatible interfaces and props are correctly typed.

Creating New Components

When adding a new component, follow this pattern:
1

Create Component Directory

Create a folder under src/components/ComponentName/
2

Create Main Component

Build the router component in ComponentName.tsx that delegates to variants
3

Create Variant Components

Implement each variant: ComponentPrimary.tsx, ComponentSecondary.tsx, etc.
4

Add Type Definitions

Export comprehensive TypeScript interfaces with JSDoc comments
5

Create Story File

Document with ComponentName.stories.tsx including interactive tests
6

Export Component

Add to src/components/index.ts:
export { default as ComponentName } from "./ComponentName/ComponentName";
All variant component files should use relative imports for same-directory files, but use path aliases for cross-directory imports. See Path Aliases for details.

Styling Guide

Learn about styled-components, theming, and color constants

TypeScript Usage

Understand type definitions and path aliases

Build docs developers (and LLMs) love