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

7.7 KiB
Raw Blame History

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 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:

// 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.)