Token pipeline (Style Dictionary v4, DTCG format): - Primitive tokens: colour palettes (brand, sage, neutral, feedback), typography (3 font families, 21-variant type scale), spacing (4px grid), border radius, shadows, opacity - Semantic tokens: text, surface, border, interactive, feedback colours; typography roles; layout spacing - Component tokens: Button (4 sizes), Input (2 sizes) - Generated outputs: CSS custom properties, JS ES6 module, flat JSON Atoms (3 components): - Button: contained/soft/outlined/text × primary/secondary, 4 sizes, loading state, underline for text variant - Typography: 21 variants across display/heading/body/label/caption/overline, maxLines truncation - Input: external label, helper text, error/success validation, start/end icons, required indicator, 2 sizes, multiline support Infrastructure: - MUI v5 theme with full token mapping - Storybook 8 with autodocs - Claude Code agents and skills for token/component workflows - Design system documentation and cross-session memory Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
7.7 KiB
7.7 KiB
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:
export { default } from './Button';
export * from './Button';
Component template
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<ButtonBaseProps, 'color'> {
/** 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),
})<ButtonProps>(({ 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<ButtonProps> = ({
variant = 'contained',
size = 'medium',
color = 'primary',
loading = false,
fullWidth = false,
children,
disabled,
...props
}) => {
return (
<StyledButton
variant={variant}
size={size}
color={color}
loading={loading}
fullWidth={fullWidth}
disabled={loading || disabled}
{...props}
>
{loading ? 'Loading…' : children}
</StyledButton>
);
};
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
sxprop pattern - Every component MUST accept and forward the
sxprop 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-labelor 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-disabledalongside visual treatment
MUI integration patterns
When wrapping MUI components:
// Extend MUI's own props type
interface ButtonProps extends Omit<MuiButtonProps, 'color'> {
// Add your custom props, omitting any MUI props you're overriding
}
// Forward all unknown props to MUI
const Button: React.FC<ButtonProps> = ({ customProp, ...muiProps }) => {
return <MuiButton {...muiProps} />;
};
When building from scratch with styled():
// 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:
const StyledCard = styled(Card)(({ theme }) => ({
padding: theme.spacing(3),
[theme.breakpoints.up('md')]: {
padding: theme.spacing(4),
},
}));
Storybook story template
import type { Meta, StoryObj } from '@storybook/react';
import { Button } from './Button';
const meta: Meta<typeof Button> = {
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<typeof Button>;
// ─── Individual stories ──────────────────────────────────────────────────────
/** Default button appearance */
export const Default: Story = {
args: { children: 'Get started' },
};
/** All visual variants side by side */
export const AllVariants: Story = {
render: () => (
<div style={{ display: 'flex', gap: 16, alignItems: 'center' }}>
<Button variant="contained">Contained</Button>
<Button variant="outlined">Outlined</Button>
<Button variant="text">Text</Button>
</div>
),
};
/** All sizes side by side */
export const AllSizes: Story = {
render: () => (
<div style={{ display: 'flex', gap: 16, alignItems: 'center' }}>
<Button size="small">Small</Button>
<Button size="medium">Medium</Button>
<Button size="large">Large</Button>
</div>
),
};
/** 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.)