STEMBlock.ai - Design System Documentation
Version: 1.0 Date: October 25, 2025 For: Development Team
Table of Contents
- Overview
- Design Tokens
- Component API Specifications
- Layout System
- Theming Architecture
- 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:
- Set up Tailwind configuration
- Install and configure shadcn/ui
- Create base component library
- Build role-specific components
- Set up Storybook for documentation