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.
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:
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:
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:
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:
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:
Create Component Directory
Create a folder under src/components/ComponentName/
Create Main Component
Build the router component in ComponentName.tsx that delegates to variants
Create Variant Components
Implement each variant: ComponentPrimary.tsx, ComponentSecondary.tsx, etc.
Add Type Definitions
Export comprehensive TypeScript interfaces with JSDoc comments
Create Story File
Document with ComponentName.stories.tsx including interactive tests
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