Aller au contenu

Livre d'histoire Feuille de chaleur

Storybook - Construire des UI dans l'isolement

Storybook est un atelier de façade pour construire des composants d'interface utilisateur et des pages en isolement. Il vous aide à développer et partager des états difficiles à atteindre et des cas de bord sans avoir besoin d'exécuter toute votre application. Des milliers d'équipes l'utilisent pour le développement de l'interface utilisateur, les tests et la documentation.

Copier toutes les commandes Générer PDF

Sommaire

  • [Installation] (#installation)
  • [Pour commencer] (#getting-started)
  • [Histoires écrites] (#writing-stories)
  • [Formulaires de l'histoire] (#story-formats)
  • [Contrôles et actions] (#controls--actions)
  • [Addons] (#addons)
  • [Configuration] (#configuration)
  • [Theming] (#theming)
  • [Essais] (#testing)
  • [Documentation] (#documentation)
  • [Déploiement] (#deployment)
  • [Caractéristiques avancées] (#advanced-features)
  • [Performance] (#performance)
  • [Intégration] (#integration)
  • [Dépannage] (#troubleshooting)
  • [Meilleures pratiques] (#best-practices)

Installation

Démarrer rapidement

# Initialize Storybook in existing project
npx storybook@latest init

# Create new project with Storybook
npx create-react-app my-app
cd my-app
npx storybook@latest init

# Start Storybook
npm run storybook

Installation manuelle

# Install Storybook CLI
npm install -g @storybook/cli

# Initialize in project
sb init

# Or specify framework
sb init --type react
sb init --type vue3
sb init --type angular
```_

### Cadre spécifique
```bash
# React
npx storybook@latest init --type react

# Vue 3
npx storybook@latest init --type vue3

# Angular
npx storybook@latest init --type angular

# Svelte
npx storybook@latest init --type svelte

# Web Components
npx storybook@latest init --type web-components

# HTML
npx storybook@latest init --type html
```_

### Structure du projet
my-app/ ├── .storybook/ │ ├── main.js # Main configuration │ └── preview.js # Global settings ├── src/ │ ├── components/ │ │ ├── Button/ │ │ │ ├── Button.js │ │ │ ├── Button.css │ │ │ └── Button.stories.js │ │ └── Header/ │ │ ├── Header.js │ │ └── Header.stories.js │ └── stories/ # Example stories ├── package.json └── README.md
## Commencer

### Histoire fondamentale
```javascript
// src/components/Button/Button.stories.js
import { Button } from './Button';

export default {
  title: 'Example/Button',
  component: Button,
  parameters: {
    layout: 'centered',
  },
  tags: ['autodocs'],
  argTypes: {
    backgroundColor: { control: 'color' },
  },
};

export const Primary = {
  args: {
    primary: true,
    label: 'Button',
  },
};

export const Secondary = {
  args: {
    label: 'Button',
  },
};

export const Large = {
  args: {
    size: 'large',
    label: 'Button',
  },
};

export const Small = {
  args: {
    size: 'small',
    label: 'Button',
  },
};

Exemple de composant

// src/components/Button/Button.js
import React from 'react';
import PropTypes from 'prop-types';
import './button.css';

export const Button = ({ primary = false, backgroundColor = null, size = 'medium', label, ...props }) => {
  const mode = primary ? 'storybook-button--primary' : 'storybook-button--secondary';
  return (
    <button
      type="button"
      className={['storybook-button', `storybook-button--${size}`, mode].join(' ')}
      style={{ backgroundColor }}
      {...props}
    >
      {label}
    </button>
  );
};

Button.propTypes = {
  primary: PropTypes.bool,
  backgroundColor: PropTypes.string,
  size: PropTypes.oneOf(['small', 'medium', 'large']),
  label: PropTypes.string.isRequired,
  onClick: PropTypes.func,
};

Button.defaultProps = {
  primary: false,
  onClick: undefined,
};

Histoire de course

# Start development server
npm run storybook

# Build static Storybook
npm run build-storybook

# Serve built Storybook
npx http-server storybook-static

# Start with specific port
npm run storybook -- --port 9001

# Start in quiet mode
npm run storybook -- --quiet

Écrire des histoires

Structure de l'histoire

// Component.stories.js
import { Component } from './Component';

// Default export - story metadata
export default {
  title: 'Path/To/Component',
  component: Component,
  parameters: {
    // Story-level parameters
  },
  argTypes: {
    // Control definitions
  },
  args: {
    // Default args for all stories
  },
};

// Named exports - individual stories
export const Default = {};

export const WithProps = {
  args: {
    prop1: 'value1',
    prop2: 'value2',
  },
};

export const CustomStory = {
  render: (args) => <Component {...args} />,
  args: {
    // Story-specific args
  },
};

Nom de l'histoire

// Organize stories with hierarchy
export default {
  title: 'Design System/Components/Button',
  // Creates: Design System > Components > Button
};

export default {
  title: 'Pages/Dashboard/UserProfile',
  // Creates: Pages > Dashboard > UserProfile
};

export default {
  title: 'Example/Button',
  // Creates: Example > Button
};

Composants multiples

// stories/Forms.stories.js
import { Input } from '../components/Input';
import { Button } from '../components/Button';
import { Form } from '../components/Form';

export default {
  title: 'Forms/LoginForm',
};

export const LoginForm = () => (
  <Form>
    <Input type="email" placeholder="Email" />
    <Input type="password" placeholder="Password" />
    <Button primary>Login</Button>
  </Form>
);

export const SignupForm = () => (
  <Form>
    <Input placeholder="Full Name" />
    <Input type="email" placeholder="Email" />
    <Input type="password" placeholder="Password" />
    <Input type="password" placeholder="Confirm Password" />
    <Button primary>Sign Up</Button>
  </Form>
);

Paramètres de l'histoire

export default {
  title: 'Example/Button',
  component: Button,
  parameters: {
    // Layout
    layout: 'centered', // 'centered' | 'fullscreen' | 'padded'

    // Backgrounds
    backgrounds: {
      default: 'light',
      values: [
        { name: 'light', value: '#ffffff' },
        { name: 'dark', value: '#333333' },
      ],
    },

    // Viewport
    viewport: {
      defaultViewport: 'mobile1',
    },

    // Documentation
    docs: {
      description: {
        component: 'Button component for user interactions',
      },
    },
  },
};

Formats d'histoire

CCA 3.0 (actuel)

// Modern format with object syntax
export default {
  title: 'Example/Button',
  component: Button,
};

export const Primary = {
  args: {
    primary: true,
    label: 'Button',
  },
};

export const Secondary = {
  args: {
    primary: false,
    label: 'Button',
  },
};

CCA 2.0 (Legacy)

// Function-based format
export default {
  title: 'Example/Button',
  component: Button,
};

const Template = (args) => <Button {...args} />;

export const Primary = Template.bind({});
Primary.args = {
  primary: true,
  label: 'Button',
};

export const Secondary = Template.bind({});
Secondary.args = {
  primary: false,
  label: 'Button',
};

Fonctions de rendu personnalisées

export const CustomRender = {
  render: (args) => {
    const [count, setCount] = React.useState(0);

    return (
      <div>
        <p>Count: {count}</p>
        <Button 
          {...args} 
          onClick={() => setCount(count + 1)}
        />
      </div>
    );
  },
  args: {
    label: 'Increment',
  },
};

Fonctions de jeu

import { userEvent, within } from '@storybook/testing-library';

export const InteractiveExample = {
  args: {
    label: 'Click me',
  },
  play: async ({ canvasElement }) => {
    const canvas = within(canvasElement);
    const button = canvas.getByRole('button');

    // Simulate user interaction
    await userEvent.click(button);

    // Add assertions if needed
    // expect(button).toHaveClass('clicked');
  },
};

Contrôles et actions

Configuration ArgTypes

export default {
  title: 'Example/Button',
  component: Button,
  argTypes: {
    // Text input
    label: {
      control: 'text',
      description: 'Button label text',
    },

    // Boolean
    primary: {
      control: 'boolean',
      description: 'Primary button style',
    },

    // Select dropdown
    size: {
      control: 'select',
      options: ['small', 'medium', 'large'],
      description: 'Button size',
    },

    // Radio buttons
    variant: {
      control: 'radio',
      options: ['primary', 'secondary', 'danger'],
    },

    // Color picker
    backgroundColor: {
      control: 'color',
    },

    // Number input
    borderRadius: {
      control: { type: 'number', min: 0, max: 50, step: 1 },
    },

    // Range slider
    opacity: {
      control: { type: 'range', min: 0, max: 1, step: 0.1 },
    },

    // Object editor
    style: {
      control: 'object',
    },

    // Array editor
    items: {
      control: 'object',
    },

    // Date picker
    createdAt: {
      control: 'date',
    },

    // Disable control
    onClick: {
      action: 'clicked',
      control: false,
    },
  },
};

Actions

import { action } from '@storybook/addon-actions';

export default {
  title: 'Example/Button',
  component: Button,
  argTypes: {
    onClick: { action: 'clicked' },
    onMouseEnter: { action: 'mouse-enter' },
    onMouseLeave: { action: 'mouse-leave' },
  },
};

// Or in individual stories
export const WithActions = {
  args: {
    label: 'Button',
    onClick: action('button-click'),
    onMouseEnter: action('mouse-enter'),
  },
};

// Multiple actions
export const MultipleActions = {
  args: {
    label: 'Multi Action Button',
    onClick: (...args) => {
      action('primary-click')(...args);
      action('analytics-track')('button-clicked');
    },
  },
};

Contrôles avancés

export default {
  title: 'Example/Form',
  component: Form,
  argTypes: {
    // Conditional controls
    showAdvanced: {
      control: 'boolean',
    },
    advancedOptions: {
      control: 'object',
      if: { arg: 'showAdvanced', truthy: true },
    },

    // Custom control
    theme: {
      control: {
        type: 'select',
        labels: {
          light: 'Light Theme',
          dark: 'Dark Theme',
          auto: 'Auto (System)',
        },
      },
      options: ['light', 'dark', 'auto'],
    },

    // Multi-select
    features: {
      control: 'multi-select',
      options: ['feature1', 'feature2', 'feature3'],
    },
  },
};

Addons

Addons essentiels

// .storybook/main.js
module.exports = {
  addons: [
    '@storybook/addon-essentials', // Includes most common addons
    '@storybook/addon-interactions',
    '@storybook/addon-a11y',
    '@storybook/addon-design-tokens',
  ],
};

// Individual addons (if not using essentials)
module.exports = {
  addons: [
    '@storybook/addon-controls',
    '@storybook/addon-actions',
    '@storybook/addon-viewport',
    '@storybook/addon-backgrounds',
    '@storybook/addon-docs',
    '@storybook/addon-toolbars',
  ],
};

Ajout d'accessibilité

// .storybook/preview.js
export const parameters = {
  a11y: {
    // Optional selector to inspect
    element: '#storybook-root',
    config: {
      rules: [
        {
          // Disable specific rules
          id: 'color-contrast',
          enabled: false,
        },
      ],
    },
    // Show violations in action panel
    manual: true,
  },
};

// In stories
export const AccessibleButton = {
  args: {
    label: 'Accessible Button',
  },
  parameters: {
    a11y: {
      // Story-specific a11y config
      config: {
        rules: [
          { id: 'button-name', enabled: true },
        ],
      },
    },
  },
};

Ajout d'un port de vue

// .storybook/preview.js
export const parameters = {
  viewport: {
    viewports: {
      mobile: {
        name: 'Mobile',
        styles: {
          width: '375px',
          height: '667px',
        },
      },
      tablet: {
        name: 'Tablet',
        styles: {
          width: '768px',
          height: '1024px',
        },
      },
      desktop: {
        name: 'Desktop',
        styles: {
          width: '1024px',
          height: '768px',
        },
      },
    },
    defaultViewport: 'mobile',
  },
};

Marques de conception Addon

// design-tokens.json
{
  "colors": {
    "primary": "#007bff",
    "secondary": "#6c757d",
    "success": "#28a745",
    "danger": "#dc3545"
  },
  "spacing": {
    "xs": "4px",
    "sm": "8px",
    "md": "16px",
    "lg": "24px",
    "xl": "32px"
  },
  "typography": {
    "fontFamily": {
      "primary": "Inter, sans-serif",
      "mono": "Monaco, monospace"
    },
    "fontSize": {
      "sm": "14px",
      "md": "16px",
      "lg": "18px",
      "xl": "24px"
    }
  }
}

// .storybook/main.js
module.exports = {
  addons: [
    {
      name: '@storybook/addon-design-tokens',
      options: {
        designTokenGlob: '**/design-tokens.json',
      },
    },
  ],
};

Addons personnalisés

// .storybook/addons/theme-switcher/register.js
import { addons, types } from '@storybook/addons';
import { ADDON_ID, TOOL_ID } from './constants';
import { Tool } from './Tool';

addons.register(ADDON_ID, () => {
  addons.add(TOOL_ID, {
    type: types.TOOL,
    title: 'Theme Switcher',
    render: Tool,
  });
});

// .storybook/addons/theme-switcher/Tool.js
import React from 'react';
import { useGlobals } from '@storybook/api';
import { IconButton } from '@storybook/components';

export const Tool = () => {
  const [globals, updateGlobals] = useGlobals();
  const theme = globals.theme || 'light';

  const toggleTheme = () => {
    updateGlobals({
      theme: theme === 'light' ? 'dark' : 'light',
    });
  };

  return (
    <IconButton
      key="theme-switcher"
      title="Toggle theme"
      onClick={toggleTheme}
    >
      {theme === 'light' ? '🌙' : '☀️'}
    </IconButton>
  );
};

Configuration

Configuration principale

// .storybook/main.js
module.exports = {
  // Stories location
  stories: [
    '../src/**/*.stories.@(js|jsx|ts|tsx|mdx)',
    '../stories/**/*.stories.@(js|jsx|ts|tsx|mdx)',
  ],

  // Addons
  addons: [
    '@storybook/addon-essentials',
    '@storybook/addon-interactions',
    '@storybook/addon-a11y',
  ],

  // Framework
  framework: {
    name: '@storybook/react-vite',
    options: {},
  },

  // Features
  features: {
    buildStoriesJson: true,
    storyStoreV7: true,
  },

  // TypeScript
  typescript: {
    check: false,
    reactDocgen: 'react-docgen-typescript',
    reactDocgenTypescriptOptions: {
      shouldExtractLiteralValuesFromEnum: true,
      propFilter: (prop) => (prop.parent ? !/node_modules/.test(prop.parent.fileName) : true),
    },
  },

  // Webpack customization
  webpackFinal: async (config) => {
    // Add custom webpack config
    config.module.rules.push({
      test: /\.scss$/,
      use: ['style-loader', 'css-loader', 'sass-loader'],
    });

    return config;
  },

  // Vite customization
  viteFinal: async (config) => {
    // Customize Vite config
    config.define = {
      ...config.define,
      global: 'globalThis',
    };

    return config;
  },

  // Documentation
  docs: {
    autodocs: 'tag',
  },

  // Static directories
  staticDirs: ['../public'],
};

Aperçu de la configuration

// .storybook/preview.js
import '../src/index.css'; // Global styles

export const parameters = {
  // Actions
  actions: { argTypesRegex: '^on[A-Z].*' },

  // Controls
  controls: {
    matchers: {
      color: /(background|color)$/i,
      date: /Date$/,
    },
    expanded: true,
    sort: 'requiredFirst',
  },

  // Layout
  layout: 'centered',

  // Backgrounds
  backgrounds: {
    default: 'light',
    values: [
      { name: 'light', value: '#ffffff' },
      { name: 'dark', value: '#333333' },
      { name: 'gray', value: '#f8f9fa' },
    ],
  },

  // Viewport
  viewport: {
    viewports: {
      mobile1: {
        name: 'Small mobile',
        styles: { width: '320px', height: '568px' },
      },
      mobile2: {
        name: 'Large mobile',
        styles: { width: '414px', height: '896px' },
      },
      tablet: {
        name: 'Tablet',
        styles: { width: '768px', height: '1024px' },
      },
    },
  },

  // Documentation
  docs: {
    theme: themes.light,
    source: {
      state: 'open',
    },
  },
};

// Global decorators
export const decorators = [
  (Story) => (
    <div style={{ margin: '3em', fontFamily: 'Inter, sans-serif' }}>
      <Story />
    </div>
  ),
];

// Global args
export const args = {
  // Default args for all stories
};

// Global arg types
export const argTypes = {
  // Global arg types
};

Variables d'environnement

// .storybook/main.js
module.exports = {
  env: (config) => ({
    ...config,
    API_URL: 'https://api.example.com',
    FEATURE_FLAG: true,
  }),

  webpackFinal: async (config) => {
    config.plugins.push(
      new webpack.DefinePlugin({
        'process.env.STORYBOOK': JSON.stringify(true),
      })
    );
    return config;
  },
};

// In components
const apiUrl = process.env.STORYBOOK 
  ? 'https://api-mock.example.com' 
  : process.env.API_URL;

Thèmes

Personnalisé Thème

// .storybook/theme.js
import { create } from '@storybook/theming';

export default create({
  base: 'light', // 'light' | 'dark'

  // Brand
  brandTitle: 'My Company Storybook',
  brandUrl: 'https://example.com',
  brandImage: 'https://example.com/logo.svg',
  brandTarget: '_self',

  // Colors
  colorPrimary: '#FF4785',
  colorSecondary: '#029CFD',

  // UI
  appBg: '#F6F9FC',
  appContentBg: '#FFFFFF',
  appBorderColor: '#E6ECF0',
  appBorderRadius: 4,

  // Typography
  fontBase: '"Inter", sans-serif',
  fontCode: 'Monaco, monospace',

  // Text colors
  textColor: '#2E3438',
  textInverseColor: '#FFFFFF',
  textMutedColor: '#798186',

  // Toolbar
  barTextColor: '#798186',
  barSelectedColor: '#029CFD',
  barBg: '#FFFFFF',

  // Form
  inputBg: '#FFFFFF',
  inputBorder: '#E6ECF0',
  inputTextColor: '#2E3438',
  inputBorderRadius: 4,
});

// .storybook/manager.js
import { addons } from '@storybook/addons';
import theme from './theme';

addons.setConfig({
  theme,
});

Thème sombre

// .storybook/dark-theme.js
import { create } from '@storybook/theming';

export default create({
  base: 'dark',

  brandTitle: 'My Company Storybook',
  brandUrl: 'https://example.com',
  brandImage: 'https://example.com/logo-white.svg',

  colorPrimary: '#FF4785',
  colorSecondary: '#029CFD',

  appBg: '#1A1A1A',
  appContentBg: '#2D2D2D',
  appBorderColor: '#404040',

  textColor: '#FFFFFF',
  textInverseColor: '#1A1A1A',
  textMutedColor: '#CCCCCC',

  barTextColor: '#CCCCCC',
  barSelectedColor: '#029CFD',
  barBg: '#2D2D2D',

  inputBg: '#404040',
  inputBorder: '#666666',
  inputTextColor: '#FFFFFF',
});

Changement de thème

// .storybook/preview.js
import { themes } from '@storybook/theming';

export const parameters = {
  docs: {
    theme: themes.light,
  },
  darkMode: {
    // Override the default dark theme
    dark: { ...themes.dark, appBg: 'black' },
    // Override the default light theme
    light: { ...themes.normal, appBg: 'white' },
    // Set the initial theme
    current: 'light',
    // Disable the addon for specific stories
    stylePreview: true,
  },
};

// Global decorator for theme switching
export const decorators = [
  (Story, context) => {
    const theme = context.globals.theme || 'light';

    return (
      <div className={`theme-${theme}`}>
        <Story />
      </div>
    );
  },
];

// Global types for theme switcher
export const globalTypes = {
  theme: {
    name: 'Theme',
    description: 'Global theme for components',
    defaultValue: 'light',
    toolbar: {
      icon: 'circlehollow',
      items: [
        { value: 'light', icon: 'sun', title: 'Light' },
        { value: 'dark', icon: 'moon', title: 'Dark' },
      ],
      showName: true,
    },
  },
};

Essais

Essais visuels

// .storybook/test-runner.js
const { getStoryContext } = require('@storybook/test-runner');

module.exports = {
  async postRender(page, context) {
    const storyContext = await getStoryContext(page, context);

    // Skip visual tests for specific stories
    if (storyContext.parameters?.skipVisualTest) {
      return;
    }

    // Take screenshot
    const image = await page.screenshot();
    expect(image).toMatchImageSnapshot({
      customSnapshotIdentifier: context.id,
    });
  },
};

// In stories
export const VisualTest = {
  args: {
    label: 'Visual Test Button',
  },
  parameters: {
    // Skip this story in visual tests
    skipVisualTest: true,
  },
};

Test d'interaction

// Button.stories.js
import { userEvent, within, expect } from '@storybook/test';

export const InteractionTest = {
  args: {
    label: 'Click me',
  },
  play: async ({ canvasElement, args }) => {
    const canvas = within(canvasElement);

    // Find elements
    const button = canvas.getByRole('button', { name: /click me/i });

    // Simulate interactions
    await userEvent.click(button);

    // Assertions
    await expect(args.onClick).toHaveBeenCalled();

    // Check DOM changes
    await expect(button).toHaveClass('clicked');

    // Wait for async operations
    await canvas.findByText('Success!');
  },
};

export const FormInteraction = {
  play: async ({ canvasElement }) => {
    const canvas = within(canvasElement);

    // Fill form
    const emailInput = canvas.getByLabelText(/email/i);
    const passwordInput = canvas.getByLabelText(/password/i);
    const submitButton = canvas.getByRole('button', { name: /submit/i });

    await userEvent.type(emailInput, 'user@example.com');
    await userEvent.type(passwordInput, 'password123');
    await userEvent.click(submitButton);

    // Check results
    await expect(canvas.getByText(/success/i)).toBeInTheDocument();
  },
};

Essai en unité avec des histoires

// Button.test.js
import { render, screen } from '@testing-library/react';
import { composeStories } from '@storybook/testing-react';
import * as stories from './Button.stories';

const { Primary, Secondary, Large } = composeStories(stories);

describe('Button', () => {
  test('renders primary button', () => {
    render(<Primary />);
    expect(screen.getByRole('button')).toHaveClass('storybook-button--primary');
  });

  test('renders secondary button', () => {
    render(<Secondary />);
    expect(screen.getByRole('button')).not.toHaveClass('storybook-button--primary');
  });

  test('renders large button', () => {
    render(<Large />);
    expect(screen.getByRole('button')).toHaveClass('storybook-button--large');
  });
});

Essais d'accessibilité

// a11y.test.js
import { axe, toHaveNoViolations } from 'jest-axe';
import { render } from '@testing-library/react';
import { composeStories } from '@storybook/testing-react';
import * as stories from './Button.stories';

expect.extend(toHaveNoViolations);

const { Primary, Secondary } = composeStories(stories);

describe('Button Accessibility', () => {
  test('Primary button should not have accessibility violations', async () => {
    const { container } = render(<Primary />);
    const results = await axe(container);
    expect(results).toHaveNoViolations();
  });

  test('Secondary button should not have accessibility violations', async () => {
    const { container } = render(<Secondary />);
    const results = await axe(container);
    expect(results).toHaveNoViolations();
  });
});

Documentation

Documentation automatique

// Component with JSDoc
/**
 * Primary UI component for user interaction
 */
export const Button = ({
  /**
   * Is this the principal call to action on the page?
   */
  primary = false,
  /**
   * What background color to use
   */
  backgroundColor,
  /**
   * How large should the button be?
   */
  size = 'medium',
  /**
   * Button contents
   */
  label,
  /**
   * Optional click handler
   */
  onClick,
  ...props
}) => {
  // Component implementation
};

// Stories with auto-generated docs
export default {
  title: 'Example/Button',
  component: Button,
  tags: ['autodocs'], // Enable auto-documentation
  parameters: {
    docs: {
      description: {
        component: 'Button component for user interactions. Supports multiple sizes and variants.',
      },
    },
  },
};

Documentation personnalisée

// Button.stories.js
export default {
  title: 'Example/Button',
  component: Button,
  parameters: {
    docs: {
      description: {
        component: `
# Button Component

The Button component is a fundamental UI element used throughout the application.

## Usage

\`\`\`jsx
import { Button } from './Button';

<Button primary onClick={handleClick}>
  Click me
</Button>
\`\`\`

## Design Guidelines

- Use primary buttons for main actions
- Use secondary buttons for supporting actions
- Ensure sufficient color contrast for accessibility
        `,
      },
    },
  },
};

export const Primary = {
  args: {
    primary: true,
    label: 'Button',
  },
  parameters: {
    docs: {
      description: {
        story: 'Primary buttons are used for the main call-to-action on a page.',
      },
    },
  },
};

MDX Documentation

<!-- Button.stories.mdx -->
import { Meta, Story, Canvas, ArgsTable, Description } from '@storybook/addon-docs';
import { Button } from './Button';

<Meta title="Example/Button" component={Button} />

# Button

<Description of={Button} />

## Examples

### Primary Button

<Canvas>
  <Story name="Primary" args={{ primary: true, label: 'Button' }}>
    {(args) => <Button {...args} />}
  </Story>
</Canvas>

### Secondary Button

<Canvas>
  <Story name="Secondary" args={{ label: 'Button' }}>
    {(args) => <Button {...args} />}
  </Story>
</Canvas>

## Props

<ArgsTable of={Button} />

## Usage Guidelines

- Use primary buttons sparingly, typically one per page
- Secondary buttons can be used multiple times
- Always provide meaningful labels
- Consider accessibility when choosing colors

## Code Example

```jsx
importer le bouton {} de './Button';

fonction MyComponent() {
retour (
<div>
<Button primaire onClick={() => alerte('Primary clicked!')}>
Action primaire
</Button>
<Button onClick={() => alerte('Secondary clicked!')}>
Action secondaire
</Button>
</div>
);
}
### Pages de documentation
```javascript
// .storybook/main.js
module.exports = {
  stories: [
    '../src/**/*.stories.@(js|jsx|ts|tsx)',
    '../docs/**/*.stories.mdx', // Documentation stories
  ],
};

// docs/Introduction.stories.mdx
import { Meta } from '@storybook/addon-docs';

<Meta title="Introduction" />

# Design System

Welcome to our design system documentation.

## Getting Started

This Storybook contains all the components, patterns, and guidelines for our design system.

### Installation

```bash
npm install @company/design-system

Usage

Importer { Bouton, entrée, carte } de '@company/design-system';

Principles

  1. Consistency - Maintain visual and functional consistency
  2. Accessibility - Ensure all components are accessible
  3. Performance - Optimize for speed and efficiency
  4. Flexibility - Support customization and theming
    ## Déploiement
    
    ### Construction statique
    ```bash
    # Build Storybook for deployment
    npm run build-storybook
    
    # Output directory
    ls storybook-static/
    
    # Serve locally
    npx http-server storybook-static
    

Déploiement net

# netlify.toml
[build]
  command = "npm run build-storybook"
  publish = "storybook-static"

[build.environment]
  NODE_VERSION = "16"

# Deploy
npm install -g netlify-cli
netlify deploy --prod --dir=storybook-static

Déploiement de Vercel

// vercel.json
{
  "buildCommand": "npm run build-storybook",
  "outputDirectory": "storybook-static",
  "framework": null
}

Pages GitHub

# .github/workflows/storybook.yml
name: Build and Deploy Storybook

on:
  push:
    branches: [ main ]

jobs:
  build-and-deploy:
    runs-on: ubuntu-latest

    steps:
    - name: Checkout
      uses: actions/checkout@v3

    - name: Setup Node
      uses: actions/setup-node@v3
      with:
        node-version: '16'
        cache: 'npm'

    - name: Install dependencies
      run: npm ci

    - name: Build Storybook
      run: npm run build-storybook

    - name: Deploy to GitHub Pages
      uses: peaceiris/actions-gh-pages@v3
      with:
        github_token: ${{ secrets.GITHUB_TOKEN }}
        publish_dir: ./storybook-static

Déploiement Docker

# Dockerfile
FROM node:16-alpine as builder

WORKDIR /app
COPY package*.json ./
RUN npm ci

COPY . .
RUN npm run build-storybook

FROM nginx:alpine
COPY --from=builder /app/storybook-static /usr/share/nginx/html
COPY nginx.conf /etc/nginx/nginx.conf

EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]

AWS S3 + CloudFront

# Build and deploy to S3
npm run build-storybook
aws s3 sync storybook-static/ s3://my-storybook-bucket --delete
aws cloudfront create-invalidation --distribution-id ABCD1234 --paths "/*"

Caractéristiques avancées

Configuration personnalisée du paquet Web

// .storybook/main.js
const path = require('path');

module.exports = {
  webpackFinal: async (config) => {
    // Add alias
    config.resolve.alias = {
      ...config.resolve.alias,
      '@': path.resolve(__dirname, '../src'),
      '@components': path.resolve(__dirname, '../src/components'),
    };

    // Add custom loader
    config.module.rules.push({
      test: /\.scss$/,
      use: [
        'style-loader',
        'css-loader',
        {
          loader: 'sass-loader',
          options: {
            additionalData: `@import "@/styles/variables.scss";`,
          },
        },
      ],
    });

    // Add plugin
    config.plugins.push(
      new webpack.DefinePlugin({
        __VERSION__: JSON.stringify(process.env.npm_package_version),
      })
    );

    return config;
  },
};

Configuration personnalisée de Babel

// .storybook/main.js
module.exports = {
  babel: async (options) => ({
    ...options,
    plugins: [
      ...options.plugins,
      ['babel-plugin-styled-components', { displayName: true }],
    ],
  }),
};

Configuration spécifique à l'environnement

// .storybook/main.js
const isDevelopment = process.env.NODE_ENV === 'development';

module.exports = {
  addons: [
    '@storybook/addon-essentials',
    ...(isDevelopment ? ['@storybook/addon-a11y'] : []),
  ],

  features: {
    storyStoreV7: !isDevelopment, // Disable in development for faster builds
  },

  webpackFinal: async (config) => {
    if (isDevelopment) {
      // Development-specific webpack config
      config.optimization.minimize = false;
    }

    return config;
  },
};

UI de gestionnaire personnalisé

// .storybook/manager.js
import { addons } from '@storybook/addons';

addons.setConfig({
  // Show/hide panels
  showPanel: true,
  panelPosition: 'bottom', // 'bottom' | 'right'

  // Sidebar
  showNav: true,
  showToolbar: true,

  // Initial active panel
  selectedPanel: 'storybook/controls/panel',

  // Sidebar tree expansion
  initialActive: 'sidebar',
  sidebar: {
    showRoots: false,
    collapsedRoots: ['other'],
  },

  // Toolbar
  toolbar: {
    title: { hidden: false },
    zoom: { hidden: false },
    eject: { hidden: false },
    copy: { hidden: false },
    fullscreen: { hidden: false },
  },
});

Rendement

Chargement paresseux

// .storybook/main.js
module.exports = {
  features: {
    storyStoreV7: true, // Enable lazy loading
  },

  stories: [
    {
      directory: '../src/components',
      files: '**/*.stories.*',
      titlePrefix: 'Components',
    },
    {
      directory: '../src/pages',
      files: '**/*.stories.*',
      titlePrefix: 'Pages',
    },
  ],
};

Optimisation des ensembles

// .storybook/main.js
module.exports = {
  webpackFinal: async (config) => {
    // Code splitting
    config.optimization.splitChunks = {
      chunks: 'all',
      cacheGroups: {
        vendor: {
          test: /[\\/]node_modules[\\/]/,
          name: 'vendors',
          chunks: 'all',
        },
      },
    };

    // Tree shaking
    config.optimization.usedExports = true;
    config.optimization.sideEffects = false;

    return config;
  },
};

Optimisation de la mémoire

// .storybook/preview.js
export const parameters = {
  // Reduce memory usage
  options: {
    storySort: {
      method: 'alphabetical',
      order: ['Introduction', 'Components', 'Pages'],
      locales: 'en-US',
    },
  },

  // Disable source code addon for better performance
  docs: {
    source: {
      state: 'closed',
    },
  },
};

// Cleanup decorators
export const decorators = [
  (Story, context) => {
    // Cleanup on story change
    React.useEffect(() => {
      return () => {
        // Cleanup logic
      };
    }, [context.id]);

    return <Story />;
  },
];

Intégration

Intégration des outils de conception

// Figma integration
export default {
  title: 'Example/Button',
  component: Button,
  parameters: {
    design: {
      type: 'figma',
      url: 'https://www.figma.com/file/ABC123/Design-System?node-id=123%3A456',
    },
  },
};

// Sketch integration
export const SketchExample = {
  parameters: {
    design: {
      type: 'sketch',
      url: 'https://sketch.cloud/s/abc123',
    },
  },
};

// Adobe XD integration
export const XDExample = {
  parameters: {
    design: {
      type: 'adobe-xd',
      url: 'https://xd.adobe.com/view/abc123',
    },
  },
};

Tester l'intégration

// jest.config.js
module.exports = {
  testEnvironment: 'jsdom',
  setupFilesAfterEnv: ['<rootDir>/src/setupTests.js'],
  testPathIgnorePatterns: ['/node_modules/', '/storybook-static/'],

  // Test stories
  testMatch: [
    '<rootDir>/src/**/__tests__/**/*.(js|jsx|ts|tsx)',
    '<rootDir>/src/**/?(*.)(spec|test).(js|jsx|ts|tsx)',
    '<rootDir>/src/**/*.stories.test.(js|jsx|ts|tsx)',
  ],
};

// Chromatic integration
// .github/workflows/chromatic.yml
name: 'Chromatic'
on: push

jobs:
  chromatic-deployment:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v1
      - run: yarn
      - uses: chromaui/action@v1
        with:
          token: ${{ secrets.GITHUB_TOKEN }}
          projectToken: ${{ secrets.CHROMATIC_PROJECT_TOKEN }}

Intégration CI/CD

# .github/workflows/storybook-tests.yml
name: Storybook Tests
on: [push, pull_request]

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - uses: actions/setup-node@v3
        with:
          node-version: '16'
          cache: 'npm'

      - run: npm ci
      - run: npm run build-storybook --quiet
      - run: npx concurrently -k -s first -n "SB,TEST" -c "magenta,blue" \
          "npx http-server storybook-static --port 6006 --silent" \
          "npx wait-on http://127.0.0.1:6006 && npm run test-storybook"

Dépannage

Questions communes

# Port already in use
npm run storybook -- --port 9001

# Clear cache
rm -rf node_modules/.cache/storybook

# Webpack issues
npm run storybook -- --no-manager-cache

# TypeScript issues
npm run storybook -- --no-dll

# Memory issues
NODE_OPTIONS="--max-old-space-size=4096" npm run storybook

Mode de débogage

# Enable debug logging
DEBUG=storybook:* npm run storybook

# Webpack debug
npm run storybook -- --debug-webpack

# Verbose output
npm run storybook -- --verbose

Problèmes de performance

// .storybook/main.js
module.exports = {
  // Disable source maps in development
  webpackFinal: async (config) => {
    if (process.env.NODE_ENV === 'development') {
      config.devtool = false;
    }
    return config;
  },

  // Reduce bundle size
  features: {
    storyStoreV7: true,
    buildStoriesJson: true,
  },
};

Créer des problèmes

# Clear all caches
rm -rf node_modules/.cache
rm -rf storybook-static
npm run build-storybook

# Check for conflicting dependencies
npm ls

# Update Storybook
npx storybook@latest upgrade

Meilleures pratiques

Histoire Organisation

  • ** Structure hiérarchique**: Utiliser des patrons de noms cohérents
  • Groupement logistique: composantes liées au groupe
  • Désignation claire: Utiliser des noms descriptifs d'histoires
  • Documentation: Inclure une documentation complète

Développement des composantes

  • Isolation: Développer des composants isolés
  • Interface Props: Conception d'interfaces prop claires et cohérentes
  • Valeurs par défaut: Fournir des valeurs par défaut raisonnables
  • Manipulation de l'erreur: Boîtes de bord de poignée gracieusement

Stratégie d'essai

  • Essais visuels: Utiliser des tests de régression visuelle
  • Essais d'interaction: Tester les interactions avec l'utilisateur
  • ** Tests d'accessibilité** : s'assurer que les composants sont accessibles
  • Essais d'unité: Logique du composant d ' essai

Optimisation des performances

  • ** Chargement paresseux** : Utiliser story store v7 pour le chargement paresseux
  • Scission du bassin: Optimiser la configuration du webpack
  • Gestion de la mémoire : Nettoyer correctement les ressources
  • ** Optimisation de la construction** : Minimiser les temps de construction

Collaboration d'équipe

  • Système de conception: Maintenir un système de conception cohérent
  • Documentation: Conserver la documentation à jour
  • Avis de code: Récapitulation des histoires et des composants
  • Automation: Automatiser les essais et le déploiement

Résumé

Storybook est un outil essentiel pour le développement moderne de frontend qui permet:

  • Isolation des composants: Développer et tester les composants de l'assurance-chômage indépendamment
  • ** Documentation visuelle** : Créer une documentation vivante pour les systèmes de conception
  • Développement interactif: Construire des composants avec rétroaction en temps réel
  • Test de l'intégration: Tests visuels, d'interaction et d'accessibilité
  • ** Collaboration avec l'équipe** : Partager les composantes entre les équipes et les parties prenantes
  • Gestion du système de conception : maintenir des modèles d'assurance-chômage cohérents

Les principaux avantages comprennent des cycles de développement plus rapides, une meilleure qualité des composants, une meilleure collaboration d'équipe et une documentation complète. Storybook prend en charge tous les principaux cadres frontend et fournit des options de personnalisation étendues via des addons et la configuration.