Files
Parsons/docs/conventions/component-conventions.md
Richie 732c872576 Initial commit: FA 2.0 Design System foundation
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>
2026-03-25 15:08:15 +11:00

242 lines
7.7 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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<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 `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<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():**
```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<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.)