Skip to main content

STEMBlock.ai - Design System Documentation

Version: 1.0 Date: October 25, 2025 For: Development Team


Table of Contents

  1. Overview
  2. Design Tokens
  3. Component API Specifications
  4. Layout System
  5. Theming Architecture
  6. Implementation Guidelines

Overview

This design system provides the technical implementation details for STEMBlock.ai's UI components. It is built on:

  • Next.js 14+ with React 18
  • TypeScript for type safety
  • Tailwind CSS for utility-first styling
  • shadcn/ui as component foundation
  • Radix UI for accessible primitives

Design Tokens

Color Tokens

// tailwind.config.ts
export const colors = {
// Primary Palette
'stem-blue': {
50: '#EFF6FF',
100: '#DBEAFE',
200: '#BFDBFE',
300: '#93C5FD',
400: '#60A5FA',
500: '#2E7CF6', // Primary
600: '#2563EB',
700: '#1D4ED8',
800: '#1E40AF',
900: '#1E3A8A',
},
'stem-purple': {
50: '#FAF5FF',
100: '#F3E8FF',
200: '#E9D5FF',
300: '#D8B4FE',
400: '#C084FC',
500: '#8B5CF6', // Secondary
600: '#9333EA',
700: '#7C3AED',
800: '#6B21A8',
900: '#581C87',
},
'stem-green': {
50: '#ECFDF5',
100: '#D1FAE5',
200: '#A7F3D0',
300: '#6EE7B7',
400: '#34D399',
500: '#10B981', // Success
600: '#059669',
700: '#047857',
800: '#065F46',
900: '#064E3B',
},
'stem-orange': {
50: '#FFF7ED',
100: '#FFEDD5',
200: '#FED7AA',
300: '#FDBA74',
400: '#FB923C',
500: '#F59E0B', // Warning
600: '#EA580C',
700: '#C2410C',
800: '#9A3412',
900: '#7C2D12',
},
'stem-red': {
50: '#FEF2F2',
100: '#FEE2E2',
200: '#FECACA',
300: '#FCA5A5',
400: '#F87171',
500: '#EF4444', // Error
600: '#DC2626',
700: '#B91C1C',
800: '#991B1B',
900: '#7F1D1D',
},
'stem-teal': {
50: '#F0FDFA',
100: '#CCFBF1',
200: '#99F6E4',
300: '#5EEAD4',
400: '#2DD4BF',
500: '#14B8A6', // Accent
600: '#0D9488',
700: '#0F766E',
800: '#115E59',
900: '#134E4A',
},
};

Typography Tokens

// Font families
export const fontFamily = {
sans: ['Inter', 'system-ui', 'sans-serif'],
display: ['Poppins', 'system-ui', 'sans-serif'],
mono: ['JetBrains Mono', 'Consolas', 'monospace'],
};

// Font sizes (rem-based)
export const fontSize = {
'xs': ['0.75rem', { lineHeight: '1rem' }], // 12px
'sm': ['0.875rem', { lineHeight: '1.25rem' }], // 14px
'base': ['1rem', { lineHeight: '1.5rem' }], // 16px
'lg': ['1.125rem', { lineHeight: '1.75rem' }], // 18px
'xl': ['1.25rem', { lineHeight: '1.75rem' }], // 20px
'2xl': ['1.5rem', { lineHeight: '2rem' }], // 24px
'3xl': ['1.875rem', { lineHeight: '2.25rem' }], // 30px
'4xl': ['2.25rem', { lineHeight: '2.5rem' }], // 36px
};

// Font weights
export const fontWeight = {
normal: '400',
medium: '500',
semibold: '600',
bold: '700',
};

Spacing Tokens

export const spacing = {
'xs': '0.25rem', // 4px
'sm': '0.5rem', // 8px
'md': '1rem', // 16px
'lg': '1.5rem', // 24px
'xl': '2rem', // 32px
'2xl': '3rem', // 48px
'3xl': '4rem', // 64px
'4xl': '6rem', // 96px
};

Border Radius Tokens

export const borderRadius = {
'none': '0',
'sm': '0.25rem', // 4px
'md': '0.5rem', // 8px
'lg': '0.75rem', // 12px
'xl': '1rem', // 16px
'full': '9999px', // Pill shape
};

Shadow Tokens

export const boxShadow = {
'sm': '0 1px 2px 0 rgba(0, 0, 0, 0.05)',
'DEFAULT': '0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px -1px rgba(0, 0, 0, 0.1)',
'md': '0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -2px rgba(0, 0, 0, 0.1)',
'lg': '0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -4px rgba(0, 0, 0, 0.1)',
'xl': '0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 8px 10px -6px rgba(0, 0, 0, 0.1)',
'blue': '0 4px 12px rgba(46, 124, 246, 0.3)',
'purple': '0 4px 12px rgba(139, 92, 246, 0.3)',
};

Component API Specifications

Button Component

// components/ui/button.tsx
import { type VariantProps } from 'class-variance-authority';

interface ButtonProps extends VariantProps<typeof buttonVariants> {
children: React.ReactNode;
onClick?: () => void;
disabled?: boolean;
loading?: boolean;
icon?: React.ReactNode;
iconPosition?: 'left' | 'right';
}

const buttonVariants = cva(
'inline-flex items-center justify-center rounded-lg font-medium transition-all focus-visible:outline-none focus-visible:ring-3 focus-visible:ring-stem-blue-500/20 disabled:pointer-events-none disabled:opacity-50',
{
variants: {
variant: {
primary: 'bg-stem-blue-500 text-white hover:bg-stem-blue-600 shadow-blue active:scale-98',
secondary: 'border-2 border-stem-blue-500 text-stem-blue-500 hover:bg-stem-blue-500 hover:text-white',
success: 'bg-stem-green-500 text-white hover:bg-stem-green-600',
danger: 'bg-stem-red-500 text-white hover:bg-stem-red-600',
ghost: 'hover:bg-slate-100 text-slate-700',
link: 'text-stem-blue-500 underline-offset-4 hover:underline',
},
size: {
sm: 'h-9 px-4 text-sm',
md: 'h-11 px-6 text-base',
lg: 'h-13 px-8 text-lg',
},
fullWidth: {
true: 'w-full',
},
},
defaultVariants: {
variant: 'primary',
size: 'md',
},
}
);

// Usage:
<Button variant="primary" size="md">
Submit Assignment
</Button>

Card Component

// components/ui/card.tsx
interface CardProps {
children: React.ReactNode;
className?: string;
padding?: 'none' | 'sm' | 'md' | 'lg';
hoverable?: boolean;
gradient?: 'hero' | 'achievement' | 'none';
}

const Card = ({
children,
padding = 'md',
hoverable = false,
gradient = 'none',
className
}: CardProps) => {
const baseStyles = 'bg-white rounded-lg border border-slate-200 shadow-sm';
const hoverStyles = hoverable ? 'hover:shadow-md hover:-translate-y-0.5 transition-all' : '';
const paddingStyles = {
none: '',
sm: 'p-4',
md: 'p-6',
lg: 'p-8',
};

return (
<div className={cn(baseStyles, hoverStyles, paddingStyles[padding], className)}>
{gradient !== 'none' && (
<div className={cn(
'h-2 rounded-t-lg',
gradient === 'hero' && 'bg-gradient-to-r from-stem-blue-500 to-stem-purple-500',
gradient === 'achievement' && 'bg-gradient-to-r from-stem-orange-500 to-stem-red-500'
)} />
)}
{children}
</div>
);
};

// Usage:
<Card hoverable gradient="hero" padding="lg">
<h3>Student Progress</h3>
<p>Current score: 85%</p>
</Card>

Input Component

// components/ui/input.tsx
interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {
label?: string;
error?: string;
helperText?: string;
leftIcon?: React.ReactNode;
rightIcon?: React.ReactNode;
}

const Input = React.forwardRef<HTMLInputElement, InputProps>(
({ label, error, helperText, leftIcon, rightIcon, className, ...props }, ref) => {
return (
<div className="w-full">
{label && (
<label className="block text-sm font-medium text-slate-900 mb-2">
{label}
</label>
)}
<div className="relative">
{leftIcon && (
<div className="absolute left-3 top-1/2 -translate-y-1/2 text-slate-400">
{leftIcon}
</div>
)}
<input
ref={ref}
className={cn(
'h-11 w-full rounded-lg border px-4 text-base',
'focus:outline-none focus:ring-3',
leftIcon && 'pl-10',
rightIcon && 'pr-10',
error
? 'border-stem-red-500 focus:border-stem-red-500 focus:ring-stem-red-500/20'
: 'border-slate-300 focus:border-stem-blue-500 focus:ring-stem-blue-500/20',
className
)}
{...props}
/>
{rightIcon && (
<div className="absolute right-3 top-1/2 -translate-y-1/2 text-slate-400">
{rightIcon}
</div>
)}
</div>
{error && (
<p className="mt-1 text-sm text-stem-red-500 flex items-center gap-1">
<AlertCircle className="w-4 h-4" />
{error}
</p>
)}
{helperText && !error && (
<p className="mt-1 text-sm text-slate-500">{helperText}</p>
)}
</div>
);
}
);

// Usage:
<Input
label="Student Name"
placeholder="Enter your name"
helperText="This will appear on your profile"
error={errors.name}
/>

Progress Component

// components/ui/progress.tsx
interface ProgressProps {
value: number; // 0-100
label?: string;
showPercentage?: boolean;
size?: 'sm' | 'md' | 'lg';
color?: 'blue' | 'green' | 'purple';
}

const Progress = ({
value,
label,
showPercentage = true,
size = 'md',
color = 'blue'
}: ProgressProps) => {
const heights = {
sm: 'h-2',
md: 'h-3',
lg: 'h-4',
};

const colors = {
blue: 'bg-stem-blue-500',
green: 'bg-stem-green-500',
purple: 'bg-stem-purple-500',
};

return (
<div className="w-full">
{(label || showPercentage) && (
<div className="flex justify-between mb-2">
{label && <span className="text-sm font-medium text-slate-700">{label}</span>}
{showPercentage && <span className="text-sm font-semibold text-slate-900">{value}%</span>}
</div>
)}
<div className={cn('w-full bg-slate-200 rounded-full overflow-hidden', heights[size])}>
<div
className={cn('h-full rounded-full transition-all duration-300', colors[color])}
style={{ width: `${Math.min(100, Math.max(0, value))}%` }}
/>
</div>
</div>
);
};

// Usage:
<Progress value={75} label="Course Progress" color="green" />

Badge Component

// components/ui/badge.tsx
interface BadgeProps {
children: React.ReactNode;
variant?: 'default' | 'success' | 'warning' | 'error' | 'info';
size?: 'sm' | 'md' | 'lg';
}

const badgeVariants = cva(
'inline-flex items-center rounded-full font-medium',
{
variants: {
variant: {
default: 'bg-slate-100 text-slate-700',
success: 'bg-stem-green-100 text-stem-green-700',
warning: 'bg-stem-orange-100 text-stem-orange-700',
error: 'bg-stem-red-100 text-stem-red-700',
info: 'bg-stem-blue-100 text-stem-blue-700',
},
size: {
sm: 'px-2 py-0.5 text-xs',
md: 'px-3 py-1 text-sm',
lg: 'px-4 py-1.5 text-base',
},
},
defaultVariants: {
variant: 'default',
size: 'md',
},
}
);

// Usage:
<Badge variant="success">Completed</Badge>
<Badge variant="warning">Pending Review</Badge>

Alert Component

// components/ui/alert.tsx
interface AlertProps {
variant: 'success' | 'warning' | 'error' | 'info';
title?: string;
children: React.ReactNode;
onClose?: () => void;
}

const Alert = ({ variant, title, children, onClose }: AlertProps) => {
const styles = {
success: {
bg: 'bg-stem-green-50',
border: 'border-stem-green-200',
icon: CheckCircle,
iconColor: 'text-stem-green-500',
},
warning: {
bg: 'bg-stem-orange-50',
border: 'border-stem-orange-200',
icon: AlertTriangle,
iconColor: 'text-stem-orange-500',
},
error: {
bg: 'bg-stem-red-50',
border: 'border-stem-red-200',
icon: XCircle,
iconColor: 'text-stem-red-500',
},
info: {
bg: 'bg-stem-blue-50',
border: 'border-stem-blue-200',
icon: InfoIcon,
iconColor: 'text-stem-blue-500',
},
};

const { bg, border, icon: Icon, iconColor } = styles[variant];

return (
<div className={cn('p-4 rounded-lg border', bg, border)}>
<div className="flex gap-3">
<Icon className={cn('w-5 h-5 flex-shrink-0 mt-0.5', iconColor)} />
<div className="flex-1">
{title && <h4 className="font-semibold text-slate-900 mb-1">{title}</h4>}
<div className="text-sm text-slate-700">{children}</div>
</div>
{onClose && (
<button onClick={onClose} className="text-slate-400 hover:text-slate-600">
<X className="w-4 h-4" />
</button>
)}
</div>
</div>
);
};

// Usage:
<Alert variant="success" title="Assignment Submitted!">
Your work has been submitted successfully and is being evaluated.
</Alert>

Avatar Component

// components/ui/avatar.tsx
interface AvatarProps {
src?: string;
alt: string;
fallback: string; // Initials
size?: 'sm' | 'md' | 'lg' | 'xl';
status?: 'online' | 'offline' | 'away';
}

const Avatar = ({ src, alt, fallback, size = 'md', status }: AvatarProps) => {
const sizes = {
sm: 'w-8 h-8 text-xs',
md: 'w-10 h-10 text-sm',
lg: 'w-12 h-12 text-base',
xl: 'w-16 h-16 text-lg',
};

const statusColors = {
online: 'bg-stem-green-500',
offline: 'bg-slate-400',
away: 'bg-stem-orange-500',
};

return (
<div className="relative inline-block">
<div className={cn(
'rounded-full overflow-hidden bg-stem-blue-100 flex items-center justify-center font-semibold text-stem-blue-700',
sizes[size]
)}>
{src ? (
<img src={src} alt={alt} className="w-full h-full object-cover" />
) : (
fallback
)}
</div>
{status && (
<span className={cn(
'absolute bottom-0 right-0 block w-3 h-3 rounded-full border-2 border-white',
statusColors[status]
)} />
)}
</div>
);
};

// Usage:
<Avatar src="/avatars/student.jpg" alt="John Doe" fallback="JD" size="lg" status="online" />

Layout System

Container Component

// components/layout/container.tsx
interface ContainerProps {
children: React.ReactNode;
maxWidth?: 'sm' | 'md' | 'lg' | 'xl' | 'full';
}

const Container = ({ children, maxWidth = 'xl' }: ContainerProps) => {
const widths = {
sm: 'max-w-screen-sm', // 640px
md: 'max-w-screen-md', // 768px
lg: 'max-w-screen-lg', // 1024px
xl: 'max-w-screen-xl', // 1280px
full: 'max-w-full',
};

return (
<div className={cn('mx-auto px-4 sm:px-6 lg:px-8', widths[maxWidth])}>
{children}
</div>
);
};

Grid Component

// components/layout/grid.tsx
interface GridProps {
children: React.ReactNode;
cols?: {
mobile?: number;
tablet?: number;
desktop?: number;
};
gap?: 'sm' | 'md' | 'lg';
}

const Grid = ({ children, cols = { mobile: 1, tablet: 2, desktop: 3 }, gap = 'md' }: GridProps) => {
const gapSizes = {
sm: 'gap-4',
md: 'gap-6',
lg: 'gap-8',
};

return (
<div className={cn(
'grid',
`grid-cols-${cols.mobile}`,
`md:grid-cols-${cols.tablet}`,
`lg:grid-cols-${cols.desktop}`,
gapSizes[gap]
)}>
{children}
</div>
);
};

// Usage:
<Grid cols={{ mobile: 1, tablet: 2, desktop: 3 }} gap="lg">
<Card>Card 1</Card>
<Card>Card 2</Card>
<Card>Card 3</Card>
</Grid>

Dashboard Layout

// components/layout/dashboard-layout.tsx
interface DashboardLayoutProps {
children: React.ReactNode;
sidebar: React.ReactNode;
header: React.ReactNode;
}

const DashboardLayout = ({ children, sidebar, header }: DashboardLayoutProps) => {
return (
<div className="min-h-screen bg-slate-50">
{/* Header */}
<header className="sticky top-0 z-50 bg-white border-b border-slate-200 shadow-sm">
{header}
</header>

{/* Main Content with Sidebar */}
<div className="flex">
{/* Sidebar */}
<aside className="hidden lg:block w-64 bg-slate-50 border-r border-slate-200 min-h-[calc(100vh-64px)]">
{sidebar}
</aside>

{/* Main Content */}
<main className="flex-1 p-6 lg:p-8">
{children}
</main>
</div>
</div>
);
};

Theming Architecture

Theme Provider

// components/theme-provider.tsx
type Theme = 'light' | 'dark';

interface ThemeContextValue {
theme: Theme;
setTheme: (theme: Theme) => void;
}

const ThemeContext = createContext<ThemeContextValue | undefined>(undefined);

export const ThemeProvider = ({ children }: { children: React.ReactNode }) => {
const [theme, setTheme] = useState<Theme>('light');

useEffect(() => {
const root = window.document.documentElement;
root.classList.remove('light', 'dark');
root.classList.add(theme);
}, [theme]);

return (
<ThemeContext.Provider value={{ theme, setTheme }}>
{children}
</ThemeContext.Provider>
);
};

export const useTheme = () => {
const context = useContext(ThemeContext);
if (!context) throw new Error('useTheme must be used within ThemeProvider');
return context;
};

Role-Based Theme

// hooks/useRoleTheme.ts
type UserRole = 'student' | 'parent' | 'coach' | 'admin';

interface RoleTheme {
primaryColor: string;
accentColor: string;
iconSet: string;
}

const roleThemes: Record<UserRole, RoleTheme> = {
student: {
primaryColor: 'stem-blue-500',
accentColor: 'stem-purple-500',
iconSet: 'playful',
},
parent: {
primaryColor: 'stem-blue-600',
accentColor: 'stem-teal-500',
iconSet: 'simple',
},
coach: {
primaryColor: 'stem-blue-700',
accentColor: 'stem-green-500',
iconSet: 'professional',
},
admin: {
primaryColor: 'slate-700',
accentColor: 'stem-blue-500',
iconSet: 'technical',
},
};

export const useRoleTheme = (role: UserRole) => {
return roleThemes[role];
};

Implementation Guidelines

File Structure

src/
├── components/
│ ├── ui/ # Base shadcn components
│ │ ├── button.tsx
│ │ ├── card.tsx
│ │ ├── input.tsx
│ │ └── ...
│ ├── student/ # Student-specific components
│ │ ├── assignment-card.tsx
│ │ ├── progress-tracker.tsx
│ │ └── achievement-badge.tsx
│ ├── parent/ # Parent-specific components
│ │ ├── child-selector.tsx
│ │ ├── progress-summary.tsx
│ │ └── ...
│ ├── coach/ # Coach-specific components
│ │ ├── class-overview.tsx
│ │ ├── student-matrix.tsx
│ │ └── ...
│ └── shared/ # Shared components
│ ├── navigation.tsx
│ ├── footer.tsx
│ └── ...
├── lib/
│ ├── utils.ts # cn() utility and helpers
│ └── constants.ts # Design tokens as constants
├── styles/
│ └── globals.css # Global styles and Tailwind imports
└── app/ # Next.js app directory

CSS Architecture

/* styles/globals.css */
@tailwind base;
@tailwind components;
@tailwind utilities;

@layer base {
:root {
/* Light theme variables */
--background: 0 0% 100%;
--foreground: 222.2 84% 4.9%;
--primary: 217 91% 60%;
--primary-foreground: 210 40% 98%;
/* ... */
}

.dark {
/* Dark theme variables */
--background: 222.2 84% 4.9%;
--foreground: 210 40% 98%;
/* ... */
}
}

@layer components {
/* Custom component classes */
.card-hover {
@apply hover:shadow-md hover:-translate-y-0.5 transition-all duration-200;
}

.focus-ring {
@apply focus-visible:outline-none focus-visible:ring-3 focus-visible:ring-stem-blue-500/20;
}
}

Tailwind Configuration

// tailwind.config.ts
import type { Config } from 'tailwindcss';

const config: Config = {
darkMode: ['class'],
content: [
'./pages/**/*.{ts,tsx}',
'./components/**/*.{ts,tsx}',
'./app/**/*.{ts,tsx}',
'./src/**/*.{ts,tsx}',
],
theme: {
container: {
center: true,
padding: '2rem',
screens: {
'2xl': '1400px',
},
},
extend: {
colors: {
// Import color tokens from design-tokens.ts
'stem-blue': { /* ... */ },
'stem-purple': { /* ... */ },
// etc.
},
fontFamily: {
sans: ['Inter', 'sans-serif'],
display: ['Poppins', 'sans-serif'],
mono: ['JetBrains Mono', 'monospace'],
},
boxShadow: {
'blue': '0 4px 12px rgba(46, 124, 246, 0.3)',
'purple': '0 4px 12px rgba(139, 92, 246, 0.3)',
},
keyframes: {
'slide-in': {
'0%': { transform: 'translateX(100%)' },
'100%': { transform: 'translateX(0)' },
},
'fade-in': {
'0%': { opacity: '0' },
'100%': { opacity: '1' },
},
},
animation: {
'slide-in': 'slide-in 0.3s ease-out',
'fade-in': 'fade-in 0.2s ease-out',
},
},
},
plugins: [
require('tailwindcss-animate'),
require('@tailwindcss/forms'),
require('@tailwindcss/typography'),
],
};

export default config;

Utility Functions

// lib/utils.ts
import { type ClassValue, clsx } from 'clsx';
import { twMerge } from 'tailwind-merge';

/**
* Merge Tailwind classes with proper precedence
*/
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}

/**
* Get age-appropriate styles
*/
export function getAgeStyles(age: number) {
if (age <= 10) {
return {
fontSize: 'text-lg',
buttonSize: 'lg',
iconSize: 'w-6 h-6',
};
} else if (age <= 14) {
return {
fontSize: 'text-base',
buttonSize: 'md',
iconSize: 'w-5 h-5',
};
} else {
return {
fontSize: 'text-base',
buttonSize: 'md',
iconSize: 'w-4 h-4',
};
}
}

/**
* Get role-specific color
*/
export function getRoleColor(role: UserRole): string {
const colors = {
student: 'stem-blue-500',
parent: 'stem-teal-500',
coach: 'stem-green-500',
admin: 'slate-700',
};
return colors[role];
}

Accessibility Implementation

Focus Management

// hooks/useFocusTrap.ts
export const useFocusTrap = (ref: RefObject<HTMLElement>) => {
useEffect(() => {
const element = ref.current;
if (!element) return;

const focusableElements = element.querySelectorAll(
'a[href], button, textarea, input, select, [tabindex]:not([tabindex="-1"])'
);

const firstElement = focusableElements[0] as HTMLElement;
const lastElement = focusableElements[focusableElements.length - 1] as HTMLElement;

const handleTabKey = (e: KeyboardEvent) => {
if (e.key !== 'Tab') return;

if (e.shiftKey && document.activeElement === firstElement) {
e.preventDefault();
lastElement.focus();
} else if (!e.shiftKey && document.activeElement === lastElement) {
e.preventDefault();
firstElement.focus();
}
};

element.addEventListener('keydown', handleTabKey);
return () => element.removeEventListener('keydown', handleTabKey);
}, [ref]);
};

ARIA Labels

// Example: Accessible Button with Icon
const IconButton = ({ icon: Icon, label, ...props }: IconButtonProps) => {
return (
<button
aria-label={label}
className="p-2 rounded-lg hover:bg-slate-100 focus-ring"
{...props}
>
<Icon className="w-5 h-5" aria-hidden="true" />
<span className="sr-only">{label}</span>
</button>
);
};

Performance Optimization

Code Splitting

// Lazy load heavy components
const CoachAnalytics = dynamic(() => import('@/components/coach/analytics'), {
loading: () => <LoadingSpinner />,
ssr: false,
});

Memoization

// Memoize expensive components
const StudentCard = memo(({ student }: { student: Student }) => {
return (
<Card>
<h3>{student.name}</h3>
<p>{student.score}</p>
</Card>
);
});

Testing Guidelines

Component Testing

// Example: Button component test
import { render, screen } from '@testing-library/react';
import { Button } from '@/components/ui/button';

describe('Button', () => {
it('renders with correct text', () => {
render(<Button>Click me</Button>);
expect(screen.getByText('Click me')).toBeInTheDocument();
});

it('applies variant styles correctly', () => {
render(<Button variant="success">Success</Button>);
const button = screen.getByText('Success');
expect(button).toHaveClass('bg-stem-green-500');
});
});

Documentation & Storybook

Storybook Stories

// Button.stories.tsx
import type { Meta, StoryObj } from '@storybook/react';
import { Button } from './button';

const meta: Meta<typeof Button> = {
title: 'UI/Button',
component: Button,
tags: ['autodocs'],
argTypes: {
variant: {
control: 'select',
options: ['primary', 'secondary', 'success', 'danger'],
},
},
};

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

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

export const WithIcon: Story = {
args: {
children: 'Submit Assignment',
variant: 'primary',
icon: <Upload className="w-4 h-4" />,
},
};

Version Control

Component Versioning:

  • Document breaking changes
  • Maintain backward compatibility where possible
  • Use semantic versioning for major updates

Changelog:

  • Track all design token changes
  • Document new components
  • Note deprecated patterns

Document Status: Ready for Implementation Next Steps:

  1. Set up Tailwind configuration
  2. Install and configure shadcn/ui
  3. Create base component library
  4. Build role-specific components
  5. Set up Storybook for documentation