reptidex Component Library
Overview
The reptidex component library implements our Forest & Stone design system using Radix UI primitives with Tailwind CSS styling. Built as@reptidex/ui, it provides production-ready components for professional reptile breeding workflows across all four frontend applications.
Architecture: Radix UI headless components + Tailwind CSS + Forest & Stone design tokens. Shared across
web-public, web-breeder, web-admin, and web-embed applications.Package Architecture
@reptidex/ui Structure
Copy
packages/ui/
├── src/
│ ├── components/ # All UI components
│ │ ├── ui/ # Base UI primitives
│ │ │ ├── button/
│ │ │ ├── input/
│ │ │ ├── dialog/
│ │ │ ├── dropdown-menu/
│ │ │ └── ...
│ │ ├── forms/ # Form components
│ │ │ ├── animal-form/
│ │ │ ├── breeding-form/
│ │ │ └── ...
│ │ ├── specialized/ # Domain-specific components
│ │ │ ├── pedigree/
│ │ │ ├── marketplace/
│ │ │ └── ...
│ │ └── layout/ # Layout components
│ │ ├── header/
│ │ ├── sidebar/
│ │ └── ...
│ ├── lib/ # Utilities and helpers
│ │ ├── utils.ts # Class merging, validation
│ │ ├── cn.ts # clsx + tailwind-merge
│ │ └── ...
│ ├── hooks/ # Shared React hooks
│ ├── icons/ # Icon components
│ └── styles/ # Global styles and tokens
│ ├── globals.css
│ └── components.css
├── tailwind.config.js # Tailwind configuration
├── tsconfig.json # TypeScript configuration
└── package.json # Package dependencies
Dependencies
Copy
{
"dependencies": {
"@radix-ui/react-accordion": "^1.1.2",
"@radix-ui/react-alert-dialog": "^1.0.5",
"@radix-ui/react-avatar": "^1.0.4",
"@radix-ui/react-checkbox": "^1.0.4",
"@radix-ui/react-dialog": "^1.0.5",
"@radix-ui/react-dropdown-menu": "^2.0.6",
"@radix-ui/react-form": "^0.0.3",
"@radix-ui/react-label": "^2.0.2",
"@radix-ui/react-popover": "^1.0.7",
"@radix-ui/react-progress": "^1.0.3",
"@radix-ui/react-select": "^2.0.0",
"@radix-ui/react-separator": "^1.0.3",
"@radix-ui/react-slot": "^1.0.2",
"@radix-ui/react-switch": "^1.0.3",
"@radix-ui/react-tabs": "^1.0.4",
"@radix-ui/react-toast": "^1.1.5",
"@radix-ui/react-tooltip": "^1.0.7",
"class-variance-authority": "^0.7.0",
"clsx": "^2.0.0",
"lucide-react": "^0.294.0",
"tailwind-merge": "^2.0.0",
"tailwindcss-animate": "^1.0.7"
}
}
Design System Foundation
Tailwind Configuration
tailwind.config.js
tailwind.config.js
Copy
/** @type {import('tailwindcss').Config} */
module.exports = {
darkMode: ["class"],
content: [
'./pages/**/*.{ts,tsx}',
'./components/**/*.{ts,tsx}',
'./app/**/*.{ts,tsx}',
'./src/**/*.{ts,tsx}',
],
theme: {
extend: {
colors: {
// Forest & Stone Color System
'deep-pine': '#3B4031',
'off-white': '#F6F3EE',
'moss-green': {
DEFAULT: '#8A9A5B',
50: '#F4F6E8',
100: '#E8EDD1',
200: '#D2DBA3',
300: '#BBC975',
400: '#A5B747',
500: '#8A9A5B',
600: '#6E7B49',
700: '#535C37',
800: '#373D25',
900: '#1C1E12',
},
'warm-sand': {
DEFAULT: '#D9CBA3',
50: '#F7F4ED',
100: '#EFE9DB',
200: '#DFD3B7',
300: '#CFBD93',
400: '#BFA76F',
500: '#D9CBA3',
600: '#ADA282',
700: '#817961',
800: '#555040',
900: '#2A281F',
},
'soft-clay': {
DEFAULT: '#C68642',
50: '#F5EFDF',
100: '#EBDFBF',
200: '#D7BF7F',
300: '#C39F3F',
400: '#C68642',
500: '#C68642',
600: '#9E6B35',
700: '#775028',
800: '#4F351A',
900: '#281A0D',
},
// Semantic colors
border: 'hsl(var(--border))',
input: 'hsl(var(--input))',
ring: 'hsl(var(--ring))',
background: 'hsl(var(--background))',
foreground: 'hsl(var(--foreground))',
primary: {
DEFAULT: '#8A9A5B',
foreground: '#FFFFFF',
},
secondary: {
DEFAULT: '#D9CBA3',
foreground: '#3B4031',
},
destructive: {
DEFAULT: 'hsl(var(--destructive))',
foreground: 'hsl(var(--destructive-foreground))',
},
muted: {
DEFAULT: 'hsl(var(--muted))',
foreground: 'hsl(var(--muted-foreground))',
},
accent: {
DEFAULT: '#C68642',
foreground: '#FFFFFF',
},
popover: {
DEFAULT: 'hsl(var(--popover))',
foreground: 'hsl(var(--popover-foreground))',
},
card: {
DEFAULT: 'hsl(var(--card))',
foreground: 'hsl(var(--card-foreground))',
},
},
borderRadius: {
lg: '16px',
md: '12px',
sm: '8px',
},
fontFamily: {
heading: ['Merriweather', 'serif'],
body: ['Roboto', 'sans-serif'],
accent: ['Playfair Display', 'serif'],
},
keyframes: {
'accordion-down': {
from: { height: 0 },
to: { height: 'var(--radix-accordion-content-height)' },
},
'accordion-up': {
from: { height: 'var(--radix-accordion-content-height)' },
to: { height: 0 },
},
},
animation: {
'accordion-down': 'accordion-down 0.2s ease-out',
'accordion-up': 'accordion-up 0.2s ease-out',
},
},
},
plugins: [require('tailwindcss-animate')],
}
CSS Variables
globals.css
globals.css
Copy
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer base {
:root {
--background: 246 243 238; /* off-white */
--foreground: 59 64 49; /* deep-pine */
--card: 255 255 255;
--card-foreground: 59 64 49;
--popover: 255 255 255;
--popover-foreground: 59 64 49;
--primary: 138 154 91; /* moss-green */
--primary-foreground: 255 255 255;
--secondary: 217 203 163; /* warm-sand */
--secondary-foreground: 59 64 49;
--muted: 217 203 163;
--muted-foreground: 59 64 49;
--accent: 198 134 66; /* soft-clay */
--accent-foreground: 255 255 255;
--destructive: 220 38 38;
--destructive-foreground: 255 255 255;
--border: 217 203 163;
--input: 217 203 163;
--ring: 138 154 91;
--radius: 16px;
}
}
@layer base {
* {
@apply border-border;
}
body {
@apply bg-background text-foreground font-body;
}
h1, h2, h3, h4, h5, h6 {
@apply font-heading;
}
}
Core UI Components
Button Component
Button (Radix UI + CVA)
Button (Radix UI + CVA)
Copy
// components/ui/button/button.tsx
import * as React from "react"
import { Slot } from "@radix-ui/react-slot"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const buttonVariants = cva(
"inline-flex items-center justify-center whitespace-nowrap rounded-2xl text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
{
variants: {
variant: {
default: "bg-moss-green text-white hover:bg-moss-green-600 shadow-sm hover:shadow-md",
destructive: "bg-destructive text-destructive-foreground hover:bg-destructive/90",
outline: "border border-warm-sand bg-background hover:bg-warm-sand/10 hover:text-accent-foreground",
secondary: "bg-warm-sand text-deep-pine hover:bg-warm-sand-400 shadow-sm hover:shadow-md",
accent: "bg-soft-clay text-white hover:bg-soft-clay-600 shadow-sm hover:shadow-md",
ghost: "hover:bg-warm-sand/20 hover:text-accent-foreground",
link: "text-moss-green underline-offset-4 hover:underline",
},
size: {
default: "h-11 px-6 py-3 min-w-[120px]",
sm: "h-9 px-4 py-2 min-w-[80px]",
lg: "h-13 px-8 py-4 text-base min-w-[140px]",
icon: "h-11 w-11",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
}
)
export interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {
asChild?: boolean
}
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant, size, asChild = false, ...props }, ref) => {
const Comp = asChild ? Slot : "button"
return (
<Comp
className={cn(buttonVariants({ variant, size, className }))}
ref={ref}
{...props}
/>
)
}
)
Button.displayName = "Button"
export { Button, buttonVariants }
Copy
// Primary action
<Button>Add Animal</Button>
// Secondary action
<Button variant="secondary">Cancel</Button>
// Call-to-action
<Button variant="accent" size="lg">Upgrade Plan</Button>
// Icon button
<Button variant="ghost" size="icon">
<Edit className="h-4 w-4" />
</Button>
// Link-style button
<Button variant="link">View Details</Button>
Form Components
Input with Label (Radix UI Form)
Input with Label (Radix UI Form)
Copy
// components/ui/input/input.tsx
import * as React from "react"
import { cn } from "@/lib/utils"
export interface InputProps
extends React.InputHTMLAttributes<HTMLInputElement> {}
const Input = React.forwardRef<HTMLInputElement, InputProps>(
({ className, type, ...props }, ref) => {
return (
<input
type={type}
className={cn(
"flex h-11 w-full rounded-2xl border border-warm-sand bg-background px-4 py-3 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-moss-green focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
className
)}
ref={ref}
{...props}
/>
)
}
)
Input.displayName = "Input"
// components/ui/label/label.tsx
import * as React from "react"
import * as LabelPrimitive from "@radix-ui/react-label"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const labelVariants = cva(
"text-sm font-medium text-deep-pine leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
)
const Label = React.forwardRef<
React.ElementRef<typeof LabelPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root> &
VariantProps<typeof labelVariants>
>(({ className, ...props }, ref) => (
<LabelPrimitive.Root
ref={ref}
className={cn(labelVariants(), className)}
{...props}
/>
))
Label.displayName = LabelPrimitive.Root.displayName
// Form field wrapper
export function FormField({
label,
children,
error,
hint,
required = false
}: {
label: string
children: React.ReactNode
error?: string
hint?: string
required?: boolean
}) {
return (
<div className="space-y-2">
<Label className="block">
{label}
{required && <span className="text-destructive ml-1">*</span>}
</Label>
{children}
{hint && (
<p className="text-xs text-muted-foreground">{hint}</p>
)}
{error && (
<p className="text-xs text-destructive flex items-center">
<AlertCircle className="w-3 h-3 mr-1" />
{error}
</p>
)}
</div>
)
}
export { Input, Label }
Copy
<FormField
label="Animal Name"
required
hint="Choose a unique name for identification"
error={errors.name}
>
<Input
placeholder="Enter animal name"
value={name}
onChange={(e) => setName(e.target.value)}
/>
</FormField>
Select Dropdown (Radix UI)
Select Dropdown (Radix UI)
Copy
// components/ui/select/select.tsx
import * as React from "react"
import * as SelectPrimitive from "@radix-ui/react-select"
import { Check, ChevronDown, ChevronUp } from "lucide-react"
import { cn } from "@/lib/utils"
const Select = SelectPrimitive.Root
const SelectGroup = SelectPrimitive.Group
const SelectValue = SelectPrimitive.Value
const SelectTrigger = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger>
>(({ className, children, ...props }, ref) => (
<SelectPrimitive.Trigger
ref={ref}
className={cn(
"flex h-11 w-full items-center justify-between rounded-2xl border border-warm-sand bg-background px-4 py-3 text-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-moss-green focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1",
className
)}
{...props}
>
{children}
<SelectPrimitive.Icon asChild>
<ChevronDown className="h-4 w-4 opacity-50" />
</SelectPrimitive.Icon>
</SelectPrimitive.Trigger>
))
SelectTrigger.displayName = SelectPrimitive.Trigger.displayName
const SelectScrollUpButton = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.ScrollUpButton>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollUpButton>
>(({ className, ...props }, ref) => (
<SelectPrimitive.ScrollUpButton
ref={ref}
className={cn(
"flex cursor-default items-center justify-center py-1",
className
)}
{...props}
>
<ChevronUp className="h-4 w-4" />
</SelectPrimitive.ScrollUpButton>
))
SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName
const SelectScrollDownButton = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.ScrollDownButton>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollDownButton>
>(({ className, ...props }, ref) => (
<SelectPrimitive.ScrollDownButton
ref={ref}
className={cn(
"flex cursor-default items-center justify-center py-1",
className
)}
{...props}
>
<ChevronDown className="h-4 w-4" />
</SelectPrimitive.ScrollDownButton>
))
SelectScrollDownButton.displayName =
SelectPrimitive.ScrollDownButton.displayName
const SelectContent = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Content>
>(({ className, children, position = "popper", ...props }, ref) => (
<SelectPrimitive.Portal>
<SelectPrimitive.Content
ref={ref}
className={cn(
"relative z-50 max-h-96 min-w-[8rem] overflow-hidden rounded-2xl border border-warm-sand bg-white text-deep-pine shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
position === "popper" &&
"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
className
)}
position={position}
{...props}
>
<SelectScrollUpButton />
<SelectPrimitive.Viewport
className={cn(
"p-1",
position === "popper" &&
"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]"
)}
>
{children}
</SelectPrimitive.Viewport>
<SelectScrollDownButton />
</SelectPrimitive.Content>
</SelectPrimitive.Portal>
))
SelectContent.displayName = SelectPrimitive.Content.displayName
const SelectLabel = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Label>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Label>
>(({ className, ...props }, ref) => (
<SelectPrimitive.Label
ref={ref}
className={cn("py-1.5 pl-8 pr-2 text-sm font-semibold", className)}
{...props}
/>
))
SelectLabel.displayName = SelectPrimitive.Label.displayName
const SelectItem = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Item>
>(({ className, children, ...props }, ref) => (
<SelectPrimitive.Item
ref={ref}
className={cn(
"relative flex w-full cursor-default select-none items-center rounded-xl py-2 pl-8 pr-2 text-sm outline-none focus:bg-warm-sand/20 focus:text-deep-pine data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
className
)}
{...props}
>
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<SelectPrimitive.ItemIndicator>
<Check className="h-4 w-4" />
</SelectPrimitive.ItemIndicator>
</span>
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
</SelectPrimitive.Item>
))
SelectItem.displayName = SelectPrimitive.Item.displayName
const SelectSeparator = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Separator>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Separator>
>(({ className, ...props }, ref) => (
<SelectPrimitive.Separator
ref={ref}
className={cn("-mx-1 my-1 h-px bg-warm-sand", className)}
{...props}
/>
))
SelectSeparator.displayName = SelectPrimitive.Separator.displayName
export {
Select,
SelectGroup,
SelectValue,
SelectTrigger,
SelectContent,
SelectLabel,
SelectItem,
SelectSeparator,
SelectScrollUpButton,
SelectScrollDownButton,
}
Copy
<FormField label="Species" required>
<Select value={species} onValueChange={setSpecies}>
<SelectTrigger>
<SelectValue placeholder="Select species..." />
</SelectTrigger>
<SelectContent>
<SelectItem value="ball-python">Ball Python (Python regius)</SelectItem>
<SelectItem value="corn-snake">Corn Snake (Pantherophis guttatus)</SelectItem>
<SelectItem value="leopard-gecko">Leopard Gecko (Eublepharis macularius)</SelectItem>
</SelectContent>
</Select>
</FormField>
Dialog Components
Modal Dialog (Radix UI)
Modal Dialog (Radix UI)
Copy
// components/ui/dialog/dialog.tsx
import * as React from "react"
import * as DialogPrimitive from "@radix-ui/react-dialog"
import { X } from "lucide-react"
import { cn } from "@/lib/utils"
const Dialog = DialogPrimitive.Root
const DialogTrigger = DialogPrimitive.Trigger
const DialogPortal = DialogPrimitive.Portal
const DialogClose = DialogPrimitive.Close
const DialogOverlay = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Overlay>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Overlay
ref={ref}
className={cn(
"fixed inset-0 z-50 bg-deep-pine/50 backdrop-blur-sm data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
className
)}
{...props}
/>
))
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName
const DialogContent = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>
>(({ className, children, ...props }, ref) => (
<DialogPortal>
<DialogOverlay />
<DialogPrimitive.Content
ref={ref}
className={cn(
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border border-warm-sand bg-white p-6 shadow-2xl duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] rounded-3xl",
className
)}
{...props}
>
{children}
<DialogPrimitive.Close className="absolute right-4 top-4 rounded-xl opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-moss-green focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground">
<X className="h-4 w-4" />
<span className="sr-only">Close</span>
</DialogPrimitive.Close>
</DialogPrimitive.Content>
</DialogPortal>
))
DialogContent.displayName = DialogPrimitive.Content.displayName
const DialogHeader = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
"flex flex-col space-y-1.5 text-center sm:text-left",
className
)}
{...props}
/>
)
DialogHeader.displayName = "DialogHeader"
const DialogFooter = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
className
)}
{...props}
/>
)
DialogFooter.displayName = "DialogFooter"
const DialogTitle = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Title>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Title
ref={ref}
className={cn(
"text-lg font-semibold leading-none tracking-tight text-deep-pine font-heading",
className
)}
{...props}
/>
))
DialogTitle.displayName = DialogPrimitive.Title.displayName
const DialogDescription = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Description>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Description
ref={ref}
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
))
DialogDescription.displayName = DialogPrimitive.Description.displayName
export {
Dialog,
DialogPortal,
DialogOverlay,
DialogClose,
DialogTrigger,
DialogContent,
DialogHeader,
DialogFooter,
DialogTitle,
DialogDescription,
}
Copy
<Dialog>
<DialogTrigger asChild>
<Button variant="destructive">Delete Animal</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>Confirm Deletion</DialogTitle>
<DialogDescription>
Are you sure you want to delete this animal record? This action cannot be undone.
</DialogDescription>
</DialogHeader>
<DialogFooter>
<DialogClose asChild>
<Button variant="secondary">Cancel</Button>
</DialogClose>
<Button variant="destructive" onClick={handleDelete}>
Delete
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
Specialized Components
Animal Components
Animal Card
Animal Card
Copy
// components/specialized/animal/animal-card.tsx
import * as React from "react"
import { Card, CardContent, CardFooter, CardHeader } from "@/components/ui/card"
import { Badge } from "@/components/ui/badge"
import { Button } from "@/components/ui/button"
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"
import { Edit, Eye } from "lucide-react"
import { cn } from "@/lib/utils"
interface AnimalCardProps {
animal: {
id: string
name: string
species: string
sex: 'male' | 'female' | 'unknown'
status: 'available' | 'sold' | 'reserved' | 'breeding'
hatchDate: string
weight?: number
photos: string[]
genetics?: string
}
onEdit?: () => void
onView?: () => void
className?: string
}
export function AnimalCard({
animal,
onEdit,
onView,
className
}: AnimalCardProps) {
const statusColors = {
available: 'bg-moss-green text-white',
sold: 'bg-gray-500 text-white',
reserved: 'bg-yellow-500 text-white',
breeding: 'bg-soft-clay text-white'
}
return (
<Card className={cn("hover:shadow-xl transition-shadow duration-300", className)}>
<CardHeader className="space-y-4">
<div className="flex items-start justify-between">
<div>
<h4 className="text-lg font-bold text-deep-pine font-heading">
{animal.name}
</h4>
<p className="text-sm text-muted-foreground">
{animal.species} • {animal.sex} • {animal.hatchDate}
</p>
</div>
<Badge className={statusColors[animal.status]}>
{animal.status}
</Badge>
</div>
{animal.photos[0] && (
<Avatar className="w-full h-48 rounded-2xl">
<AvatarImage
src={animal.photos[0]}
alt={animal.name}
className="object-cover"
/>
<AvatarFallback className="rounded-2xl text-2xl">
{animal.name.slice(0, 2).toUpperCase()}
</AvatarFallback>
</Avatar>
)}
</CardHeader>
<CardContent>
<div className="grid grid-cols-2 gap-4 text-sm">
<div>
<span className="text-muted-foreground">Hatch Date:</span>
<span className="text-deep-pine ml-2">{animal.hatchDate}</span>
</div>
{animal.weight && (
<div>
<span className="text-muted-foreground">Weight:</span>
<span className="text-deep-pine ml-2">{animal.weight}g</span>
</div>
)}
{animal.genetics && (
<div className="col-span-2">
<span className="text-muted-foreground">Genetics:</span>
<span className="text-deep-pine ml-2">{animal.genetics}</span>
</div>
)}
</div>
</CardContent>
<CardFooter className="gap-3">
{onView && (
<Button variant="default" size="sm" onClick={onView} className="flex-1">
<Eye className="w-4 h-4 mr-2" />
View Details
</Button>
)}
{onEdit && (
<Button variant="secondary" size="sm" onClick={onEdit} className="flex-1">
<Edit className="w-4 h-4 mr-2" />
Edit
</Button>
)}
</CardFooter>
</Card>
)
}
Pedigree Components
Pedigree Tree
Pedigree Tree
Copy
// components/specialized/pedigree/pedigree-tree.tsx
import * as React from "react"
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
import { Badge } from "@/components/ui/badge"
import { Button } from "@/components/ui/button"
import { Download, Eye } from "lucide-react"
import { cn } from "@/lib/utils"
interface PedigreeAnimal {
id: string
name: string
species: string
genetics?: string
verification: 'verified' | 'community' | 'self-reported' | 'purple-ribbon'
}
interface PedigreeTreeProps {
centerAnimal: PedigreeAnimal
sire?: PedigreeAnimal
dam?: PedigreeAnimal
generations?: number
onViewFull?: () => void
onGenerateCertificate?: () => void
className?: string
}
export function PedigreeTree({
centerAnimal,
sire,
dam,
generations = 3,
onViewFull,
onGenerateCertificate,
className
}: PedigreeTreeProps) {
const verificationColors = {
'verified': 'bg-moss-green text-white',
'community': 'bg-yellow-500 text-white',
'self-reported': 'bg-gray-400 text-white',
'purple-ribbon': 'bg-purple-600 text-white'
}
const AnimalNode = ({ animal, className }: { animal: PedigreeAnimal, className?: string }) => (
<div className={cn(
"border rounded-2xl p-4 text-center min-w-[180px]",
className
)}>
<h5 className="font-medium text-deep-pine font-heading text-sm mb-1">
{animal.name}
</h5>
<p className="text-xs text-muted-foreground mb-2">
{animal.genetics || 'Unknown genetics'}
</p>
<Badge
size="sm"
className={verificationColors[animal.verification]}
>
{animal.verification.replace('-', ' ')}
</Badge>
</div>
)
return (
<Card className={cn("relative", className)}>
<CardHeader>
<CardTitle className="flex items-center justify-between">
<span>{generations}-Generation Pedigree</span>
<div className="flex gap-2">
{onViewFull && (
<Button variant="ghost" size="sm" onClick={onViewFull}>
<Eye className="w-4 h-4 mr-2" />
View Full
</Button>
)}
{onGenerateCertificate && (
<Button variant="default" size="sm" onClick={onGenerateCertificate}>
<Download className="w-4 h-4 mr-2" />
Certificate
</Button>
)}
</div>
</CardTitle>
</CardHeader>
<CardContent>
<div className="pedigree-layout space-y-8">
{/* Center Animal */}
<div className="flex justify-center">
<AnimalNode
animal={centerAnimal}
className="bg-moss-green/10 border-moss-green border-2"
/>
</div>
{/* Parents Generation */}
{(sire || dam) && (
<div className="grid grid-cols-2 gap-8">
{sire ? (
<div className="flex justify-center">
<AnimalNode
animal={sire}
className="bg-blue-50 border-blue-200"
/>
</div>
) : (
<div className="flex justify-center">
<div className="border-dashed border-2 border-gray-300 rounded-2xl p-4 text-center min-w-[180px]">
<p className="text-sm text-muted-foreground">Unknown Sire</p>
</div>
</div>
)}
{dam ? (
<div className="flex justify-center">
<AnimalNode
animal={dam}
className="bg-pink-50 border-pink-200"
/>
</div>
) : (
<div className="flex justify-center">
<div className="border-dashed border-2 border-gray-300 rounded-2xl p-4 text-center min-w-[180px]">
<p className="text-sm text-muted-foreground">Unknown Dam</p>
</div>
</div>
)}
</div>
)}
{/* Connection Lines (simplified) */}
<svg className="absolute inset-0 pointer-events-none" style={{zIndex: -1}}>
<line x1="50%" y1="120px" x2="25%" y2="200px" stroke="#8A9A5B" strokeWidth="2" />
<line x1="50%" y1="120px" x2="75%" y2="200px" stroke="#8A9A5B" strokeWidth="2" />
</svg>
</div>
</CardContent>
</Card>
)
}
Toast Notifications
Toast (Radix UI)
Toast (Radix UI)
Copy
// components/ui/toast/toast.tsx
import * as React from "react"
import * as ToastPrimitives from "@radix-ui/react-toast"
import { cva, type VariantProps } from "class-variance-authority"
import { X } from "lucide-react"
import { cn } from "@/lib/utils"
const ToastProvider = ToastPrimitives.Provider
const ToastViewport = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Viewport>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Viewport>
>(({ className, ...props }, ref) => (
<ToastPrimitives.Viewport
ref={ref}
className={cn(
"fixed top-0 right-0 z-[100] flex max-h-screen w-full flex-col-reverse p-4 sm:top-auto sm:right-0 sm:flex-col md:max-w-[420px]",
className
)}
{...props}
/>
))
ToastViewport.displayName = ToastPrimitives.Viewport.displayName
const toastVariants = cva(
"group pointer-events-auto relative flex w-full items-center justify-between space-x-4 overflow-hidden rounded-r-2xl border border-l-4 p-4 pr-8 shadow-lg transition-all data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)] data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)] data-[swipe=move]:transition-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[swipe=end]:animate-out data-[state=closed]:fade-out-80 data-[state=closed]:slide-out-to-right-full data-[state=open]:slide-in-from-top-full data-[state=open]:sm:slide-in-from-bottom-full",
{
variants: {
variant: {
default: "border-l-moss-green bg-white text-deep-pine",
destructive: "border-l-red-500 bg-white text-deep-pine",
success: "border-l-green-500 bg-white text-deep-pine",
warning: "border-l-yellow-500 bg-white text-deep-pine",
},
},
defaultVariants: {
variant: "default",
},
}
)
const Toast = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Root>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Root> &
VariantProps<typeof toastVariants>
>(({ className, variant, ...props }, ref) => {
return (
<ToastPrimitives.Root
ref={ref}
className={cn(toastVariants({ variant }), className)}
{...props}
/>
)
})
Toast.displayName = ToastPrimitives.Root.displayName
const ToastAction = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Action>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Action>
>(({ className, ...props }, ref) => (
<ToastPrimitives.Action
ref={ref}
className={cn(
"inline-flex h-8 shrink-0 items-center justify-center rounded-md border bg-transparent px-3 text-sm font-medium ring-offset-background transition-colors hover:bg-secondary focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 group-[.destructive]:border-muted/40 group-[.destructive]:hover:border-destructive/30 group-[.destructive]:hover:bg-destructive group-[.destructive]:hover:text-destructive-foreground group-[.destructive]:focus:ring-destructive",
className
)}
{...props}
/>
))
ToastAction.displayName = ToastPrimitives.Action.displayName
const ToastClose = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Close>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Close>
>(({ className, ...props }, ref) => (
<ToastPrimitives.Close
ref={ref}
className={cn(
"absolute right-2 top-2 rounded-md p-1 text-foreground/50 opacity-0 transition-opacity hover:text-foreground focus:opacity-100 focus:outline-none focus:ring-2 group-hover:opacity-100 group-[.destructive]:text-red-300 group-[.destructive]:hover:text-red-50 group-[.destructive]:focus:ring-red-400 group-[.destructive]:focus:ring-offset-red-600",
className
)}
toast-close=""
{...props}
>
<X className="h-4 w-4" />
</ToastPrimitives.Close>
))
ToastClose.displayName = ToastPrimitives.Close.displayName
const ToastTitle = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Title>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Title>
>(({ className, ...props }, ref) => (
<ToastPrimitives.Title
ref={ref}
className={cn("text-sm font-semibold", className)}
{...props}
/>
))
ToastTitle.displayName = ToastPrimitives.Title.displayName
const ToastDescription = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Description>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Description>
>(({ className, ...props }, ref) => (
<ToastPrimitives.Description
ref={ref}
className={cn("text-sm opacity-90", className)}
{...props}
/>
))
ToastDescription.displayName = ToastPrimitives.Description.displayName
type ToastProps = React.ComponentPropsWithoutRef<typeof Toast>
type ToastActionElement = React.ReactElement<typeof ToastAction>
export {
type ToastProps,
type ToastActionElement,
ToastProvider,
ToastViewport,
Toast,
ToastTitle,
ToastDescription,
ToastClose,
ToastAction,
}
Copy
// hooks/use-toast.ts
import * as React from "react"
import { ToastActionElement, type ToastProps } from "@/components/ui/toast"
const TOAST_LIMIT = 1
const TOAST_REMOVE_DELAY = 1000000
type ToasterToast = ToastProps & {
id: string
title?: React.ReactNode
description?: React.ReactNode
action?: ToastActionElement
}
const actionTypes = {
ADD_TOAST: "ADD_TOAST",
UPDATE_TOAST: "UPDATE_TOAST",
DISMISS_TOAST: "DISMISS_TOAST",
REMOVE_TOAST: "REMOVE_TOAST",
} as const
let count = 0
function genId() {
count = (count + 1) % Number.MAX_SAFE_INTEGER
return count.toString()
}
type ActionType = typeof actionTypes
type Action =
| {
type: ActionType["ADD_TOAST"]
toast: ToasterToast
}
| {
type: ActionType["UPDATE_TOAST"]
toast: Partial<ToasterToast>
}
| {
type: ActionType["DISMISS_TOAST"]
toastId?: ToasterToast["id"]
}
| {
type: ActionType["REMOVE_TOAST"]
toastId?: ToasterToast["id"]
}
interface State {
toasts: ToasterToast[]
}
const toastTimeouts = new Map<string, ReturnType<typeof setTimeout>>()
const addToRemoveQueue = (toastId: string) => {
if (toastTimeouts.has(toastId)) {
return
}
const timeout = setTimeout(() => {
toastTimeouts.delete(toastId)
dispatch({
type: "REMOVE_TOAST",
toastId: toastId,
})
}, TOAST_REMOVE_DELAY)
toastTimeouts.set(toastId, timeout)
}
export const reducer = (state: State, action: Action): State => {
switch (action.type) {
case "ADD_TOAST":
return {
...state,
toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT),
}
case "UPDATE_TOAST":
return {
...state,
toasts: state.toasts.map((t) =>
t.id === action.toast.id ? { ...t, ...action.toast } : t
),
}
case "DISMISS_TOAST": {
const { toastId } = action
if (toastId) {
addToRemoveQueue(toastId)
} else {
state.toasts.forEach((toast) => {
addToRemoveQueue(toast.id)
})
}
return {
...state,
toasts: state.toasts.map((t) =>
t.id === toastId || toastId === undefined
? {
...t,
open: false,
}
: t
),
}
}
case "REMOVE_TOAST":
if (action.toastId === undefined) {
return {
...state,
toasts: [],
}
}
return {
...state,
toasts: state.toasts.filter((t) => t.id !== action.toastId),
}
}
}
const listeners: Array<(state: State) => void> = []
let memoryState: State = { toasts: [] }
function dispatch(action: Action) {
memoryState = reducer(memoryState, action)
listeners.forEach((listener) => {
listener(memoryState)
})
}
type Toast = Omit<ToasterToast, "id">
function toast({ ...props }: Toast) {
const id = genId()
const update = (props: ToasterToast) =>
dispatch({
type: "UPDATE_TOAST",
toast: { ...props, id },
})
const dismiss = () => dispatch({ type: "DISMISS_TOAST", toastId: id })
dispatch({
type: "ADD_TOAST",
toast: {
...props,
id,
open: true,
onOpenChange: (open) => {
if (!open) dismiss()
},
},
})
return {
id: id,
dismiss,
update,
}
}
function useToast() {
const [state, setState] = React.useState<State>(memoryState)
React.useEffect(() => {
listeners.push(setState)
return () => {
const index = listeners.indexOf(setState)
if (index > -1) {
listeners.splice(index, 1)
}
}
}, [state])
return {
...state,
toast,
dismiss: (toastId?: string) => dispatch({ type: "DISMISS_TOAST", toastId }),
}
}
export { useToast, toast }
Copy
import { useToast } from "@/hooks/use-toast"
function MyComponent() {
const { toast } = useToast()
return (
<Button
onClick={() => {
toast({
variant: "success",
title: "Animal Added Successfully",
description: "Luna has been added to your registry",
})
}}
>
Add Animal
</Button>
)
}
Component Testing
Testing Setup
Component Test Example
Component Test Example
Copy
// components/ui/button/button.test.tsx
import { render, screen, fireEvent } from '@testing-library/react'
import { describe, it, expect, vi } from 'vitest'
import { Button } from './button'
describe('Button', () => {
it('renders with correct text', () => {
render(<Button>Add Animal</Button>)
expect(screen.getByRole('button', { name: 'Add Animal' })).toBeInTheDocument()
})
it('calls onClick handler when clicked', () => {
const handleClick = vi.fn()
render(<Button onClick={handleClick}>Click me</Button>)
fireEvent.click(screen.getByRole('button'))
expect(handleClick).toHaveBeenCalledTimes(1)
})
it('applies variant styles correctly', () => {
render(<Button variant="accent">Upgrade</Button>)
const button = screen.getByRole('button')
expect(button).toHaveClass('bg-soft-clay')
})
it('applies size styles correctly', () => {
render(<Button size="lg">Large Button</Button>)
const button = screen.getByRole('button')
expect(button).toHaveClass('h-13')
})
it('forwards ref correctly', () => {
const ref = React.createRef<HTMLButtonElement>()
render(<Button ref={ref}>Button</Button>)
expect(ref.current).toBeInstanceOf(HTMLButtonElement)
})
it('works as a child component with asChild', () => {
render(
<Button asChild>
<a href="/test">Link Button</a>
</Button>
)
const link = screen.getByRole('link')
expect(link).toHaveClass('bg-moss-green')
})
})
Storybook Stories
Storybook Stories
Copy
// components/ui/button/button.stories.tsx
import type { Meta, StoryObj } from '@storybook/react'
import { Button } from './button'
import { Download, Edit, Heart, Plus } from 'lucide-react'
const meta: Meta<typeof Button> = {
title: 'UI/Button',
component: Button,
parameters: {
layout: 'centered',
docs: {
description: {
component: 'A versatile button component built on Radix UI Slot with Forest & Stone design system styling.'
}
}
},
argTypes: {
variant: {
control: 'select',
options: ['default', 'destructive', 'outline', 'secondary', 'accent', 'ghost', 'link']
},
size: {
control: 'select',
options: ['default', 'sm', 'lg', 'icon']
},
asChild: {
control: 'boolean',
description: 'Render as a different element using Radix UI Slot'
}
}
}
export default meta
type Story = StoryObj<typeof Button>
export const Default: Story = {
args: {
children: 'Add Animal',
variant: 'default'
}
}
export const Secondary: Story = {
args: {
children: 'Cancel',
variant: 'secondary'
}
}
export const Accent: Story = {
args: {
children: 'Upgrade Plan',
variant: 'accent'
}
}
export const WithIcon: Story = {
args: {
children: (
<>
<Plus className="w-4 h-4 mr-2" />
Add Animal
</>
),
variant: 'default'
}
}
export const IconOnly: Story = {
args: {
children: <Edit className="w-4 h-4" />,
variant: 'ghost',
size: 'icon'
}
}
export const AllVariants: Story = {
render: () => (
<div className="flex flex-wrap gap-4">
<Button variant="default">Default</Button>
<Button variant="secondary">Secondary</Button>
<Button variant="accent">Accent</Button>
<Button variant="destructive">Destructive</Button>
<Button variant="outline">Outline</Button>
<Button variant="ghost">Ghost</Button>
<Button variant="link">Link</Button>
</div>
)
}
export const AllSizes: Story = {
render: () => (
<div className="flex items-center gap-4">
<Button size="sm">Small</Button>
<Button size="default">Default</Button>
<Button size="lg">Large</Button>
<Button size="icon">
<Heart className="w-4 h-4" />
</Button>
</div>
)
}
export const AsLink: Story = {
args: {
asChild: true,
children: <a href="#" onClick={(e) => e.preventDefault()}>I'm actually a link!</a>
}
}
Integration with @reptidex/core
State Management
Zustand Store Integration
Zustand Store Integration
Copy
// Integration with @reptidex/core stores
import { useAnimalStore } from '@reptidex/core/stores/animal-store'
import { useAuthStore } from '@reptidex/core/stores/auth-store'
import { AnimalCard } from '@reptidex/ui/components/specialized/animal'
function AnimalRegistry() {
const { animals, loading, addAnimal, updateAnimal } = useAnimalStore()
const { user } = useAuthStore()
if (loading) {
return <AnimalRegistrySkeleton />
}
return (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{animals.map((animal) => (
<AnimalCard
key={animal.id}
animal={animal}
onEdit={() => updateAnimal(animal.id)}
onView={() => /* navigate to animal detail */}
/>
))}
</div>
)
}
API Integration
React Query Integration
React Query Integration
Copy
// Integration with @reptidex/core API clients
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
import { animalApi } from '@reptidex/core/api/animal-api'
import { useToast } from '@reptidex/ui/hooks/use-toast'
import { Button } from '@reptidex/ui/components/ui/button'
function useAnimalActions() {
const queryClient = useQueryClient()
const { toast } = useToast()
const createAnimalMutation = useMutation({
mutationFn: animalApi.create,
onSuccess: () => {
queryClient.invalidateQueries(['animals'])
toast({
variant: "success",
title: "Animal Added Successfully",
description: "The animal has been added to your registry",
})
},
onError: (error) => {
toast({
variant: "destructive",
title: "Error",
description: error.message,
})
}
})
return {
createAnimal: createAnimalMutation.mutate,
isCreating: createAnimalMutation.isPending
}
}
function AddAnimalButton() {
const { createAnimal, isCreating } = useAnimalActions()
return (
<Button
onClick={() => createAnimal(animalData)}
disabled={isCreating}
>
{isCreating ? 'Adding...' : 'Add Animal'}
</Button>
)
}
Package Exports
Main Export File
index.ts
index.ts
Copy
// packages/ui/src/index.ts
// Base UI Components
export * from './components/ui/button'
export * from './components/ui/input'
export * from './components/ui/label'
export * from './components/ui/select'
export * from './components/ui/dialog'
export * from './components/ui/card'
export * from './components/ui/badge'
export * from './components/ui/avatar'
export * from './components/ui/separator'
export * from './components/ui/tabs'
export * from './components/ui/accordion'
export * from './components/ui/toast'
export * from './components/ui/progress'
export * from './components/ui/switch'
export * from './components/ui/checkbox'
// Form Components
export * from './components/forms/form-field'
export * from './components/forms/animal-form'
export * from './components/forms/breeding-form'
// Layout Components
export * from './components/layout/header'
export * from './components/layout/sidebar'
export * from './components/layout/breadcrumb'
// Specialized Components
export * from './components/specialized/animal/animal-card'
export * from './components/specialized/animal/animal-table'
export * from './components/specialized/pedigree/pedigree-tree'
export * from './components/specialized/pedigree/genetic-prediction'
export * from './components/specialized/marketplace/listing-card'
// Hooks
export * from './hooks/use-toast'
// Utilities
export * from './lib/utils'
export * from './lib/cn'
// Types
export type * from './types'
Development Workflow
Local Development
Package Development
Package Development
Copy
# In the @reptidex/ui package
npm run dev # Start Storybook development server
npm run build # Build package for production
npm run test # Run component tests
npm run test:watch # Run tests in watch mode
npm run lint # Lint TypeScript and styles
npm run type-check # TypeScript type checking
# In consuming applications
npm run dev # Start Vite dev server with HMR
npm run build # Build application
npm run preview # Preview built application
CI/CD Integration
GitHub Actions
GitHub Actions
Copy
# .github/workflows/ui-package.yml
name: UI Package CI
on:
push:
paths:
- 'packages/ui/**'
pull_request:
paths:
- 'packages/ui/**'
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '18'
cache: 'npm'
- name: Install dependencies
run: npm ci
working-directory: packages/ui
- name: Run type checking
run: npm run type-check
working-directory: packages/ui
- name: Run tests
run: npm run test:ci
working-directory: packages/ui
- name: Run linting
run: npm run lint
working-directory: packages/ui
- name: Build package
run: npm run build
working-directory: packages/ui
- name: Upload coverage
uses: codecov/codecov-action@v3
with:
file: packages/ui/coverage/lcov.info
Production Ready: This component library is architected for scale with Radix UI accessibility, Tailwind CSS performance, comprehensive testing, and seamless integration with the reptidex platform ecosystem.

