# Component conventions These conventions MUST be followed by any agent creating or modifying components. ## File structure Every component lives in its own folder: ``` src/components/atoms/Button/ ├── Button.tsx # The component ├── Button.stories.tsx # Storybook stories ├── Button.test.tsx # Unit tests (optional, added later) └── index.ts # Re-export ``` The `index.ts` always looks like: ```typescript export { default } from './Button'; export * from './Button'; ``` ## Component template ```typescript import React from 'react'; import { ButtonBase, ButtonBaseProps } from '@mui/material'; import { styled, useTheme } from '@mui/material/styles'; // ─── Types ─────────────────────────────────────────────────────────────────── /** Props for the Button component */ export interface ButtonProps extends Omit { /** Visual style variant */ variant?: 'contained' | 'outlined' | 'text'; /** Size preset */ size?: 'small' | 'medium' | 'large'; /** Colour intent */ color?: 'primary' | 'secondary' | 'neutral'; /** Show loading spinner and disable interaction */ loading?: boolean; /** Full width of parent container */ fullWidth?: boolean; } // ─── Styled wrapper (if needed) ────────────────────────────────────────────── const StyledButton = styled(ButtonBase, { shouldForwardProp: (prop) => !['variant', 'size', 'color', 'loading', 'fullWidth'].includes(prop as string), })(({ theme, variant, size, color }) => ({ // ALL values come from theme — never hardcode borderRadius: theme.shape.borderRadius, fontFamily: theme.typography.fontFamily, fontWeight: theme.typography.fontWeightMedium, transition: theme.transitions.create( ['background-color', 'box-shadow', 'border-color'], { duration: theme.transitions.duration.short } ), // ... variant-specific styles using theme values })); // ─── Component ─────────────────────────────────────────────────────────────── /** Primary action button for the FA design system */ export const Button: React.FC = ({ variant = 'contained', size = 'medium', color = 'primary', loading = false, fullWidth = false, children, disabled, ...props }) => { return ( {loading ? 'Loading…' : children} ); }; export default Button; ``` ## Rules ### Theming - **NEVER hardcode** colour values, spacing, font sizes, shadows, or border radii - Use `theme.palette.*`, `theme.spacing()`, `theme.typography.*`, `theme.shape.*` - For one-off theme overrides, use the `sx` prop pattern - Every component MUST accept and forward the `sx` prop for consumer overrides ### TypeScript - Every prop must have a JSDoc description - Use interface (not type) for props — interfaces produce better autodocs - Export both the component and the props interface - Use `Omit<>` to remove conflicting MUI base props ### Composition - Prefer composition over configuration - Small, focused components that compose well - Avoid god-components with 20+ props — split into variants or sub-components - Use React.forwardRef for all interactive elements (buttons, inputs, links) ### Accessibility - All interactive elements must have a minimum 44×44px touch target - Always include `aria-label` or visible label text - Buttons must have `type="button"` unless they're form submit buttons - Focus indicators must be visible — never remove outline without replacement - Disabled elements should use `aria-disabled` alongside visual treatment ### MUI integration patterns **When wrapping MUI components:** ```typescript // Extend MUI's own props type interface ButtonProps extends Omit { // Add your custom props, omitting any MUI props you're overriding } // Forward all unknown props to MUI const Button: React.FC = ({ customProp, ...muiProps }) => { return ; }; ``` **When building from scratch with styled():** ```typescript // Use shouldForwardProp to prevent custom props leaking to DOM const StyledDiv = styled('div', { shouldForwardProp: (prop) => prop !== 'isActive', })<{ isActive?: boolean }>(({ theme, isActive }) => ({ backgroundColor: isActive ? theme.palette.primary.main : theme.palette.background.paper, })); ``` **Theme-aware responsive styles:** ```typescript const StyledCard = styled(Card)(({ theme }) => ({ padding: theme.spacing(3), [theme.breakpoints.up('md')]: { padding: theme.spacing(4), }, })); ``` ## Storybook story template ```typescript import type { Meta, StoryObj } from '@storybook/react'; import { Button } from './Button'; const meta: Meta = { title: 'Atoms/Button', // Tier/ComponentName component: Button, tags: ['autodocs'], // Always include for auto-documentation parameters: { layout: 'centered', // Use 'centered' for atoms, 'fullscreen' for layouts }, argTypes: { variant: { control: 'select', options: ['contained', 'outlined', 'text'], description: 'Visual style variant', table: { defaultValue: { summary: 'contained' } }, }, // ... one entry per prop onClick: { action: 'clicked' }, // Log actions for event props }, }; export default meta; type Story = StoryObj; // ─── Individual stories ────────────────────────────────────────────────────── /** Default button appearance */ export const Default: Story = { args: { children: 'Get started' }, }; /** All visual variants side by side */ export const AllVariants: Story = { render: () => (
), }; /** All sizes side by side */ export const AllSizes: Story = { render: () => (
), }; /** Interactive states */ export const Disabled: Story = { args: { children: 'Disabled', disabled: true }, }; export const Loading: Story = { args: { children: 'Submitting', loading: true }, }; ``` ### Story naming conventions - `title`: Use atomic tier as prefix: `Atoms/Button`, `Molecules/PriceCard`, `Organisms/PricingTable` - Individual stories: PascalCase, descriptive of the state or variant shown - Always include: `Default`, `AllVariants` (if applicable), `AllSizes` (if applicable), `Disabled` - For composed components, include a story showing the component with realistic content ### Story coverage checklist For every component, stories must cover: - [ ] Default state with typical content - [ ] All visual variants side by side - [ ] All sizes side by side (if applicable) - [ ] Disabled state - [ ] Loading state (if applicable) - [ ] Error state (if applicable) - [ ] Long content / content overflow - [ ] Empty/minimal content - [ ] With and without optional elements (icons, badges, etc.)