🔖

StorybookでIconカタログを作成する

2021/03/21に公開

はじめに

storybookを使ってデザイナーとコンポーネント管理する中で、アイコンのように挙動が同じだが見た目が違う大量のコンポーネントを、各々コンポーネントとして登録し、1ページ1アイコンで表示するのは把握、確認がとても不便でした。
そこで一覧で表現し、挙動はもう一つのページでも確認できるようにします。

カタログ

先に結果の表示をお見せします。実際のstorybookはこちら

カタログ部分

使用方法の説明とともに、全てのアイコンを展開します。

挙動確認部分

アイコンや色、大きさを切り替えてすべてのコンポーネントの挙動を確認できます。

作成手順

ここでは、storybookの環境設定はできているものとし、src/配下に登録したい全てのアイコンがある想定で作成していきます。
@storybook/addon-docs@storybook/addon-controlsを使用します。

1. manifest作成スクリプト作成

create-manifest.js
const fs = require('fs');
const path = require('path');
// indexファイルはアイコンでないので除外する
const exclusionList = ['index.ts'];
// 対象dirを読む
const files = fs.readdirSync(path.resolve(process.cwd(), `./src`));
// (必要であれば)種類ごとにfieldを分けてdataを作成します。
const data = {
  Icons: files
    .filter((file) => !exclusionList.includes(file))
    .map((file) => file.replace('.tsx', '')),
};
// jsonに書き出します。
fs.writeFileSync('./docs/manifest.json', JSON.stringify(data, null, '    '));

2. スクリプトを実行する

package.json
 "build:manifest": "node ./scripts/createManifest.js",
 "storybook": "yarn build:manifest && start-storybook -p 6006",

storyの更新が忘れられないようにgithub actionも作成しておきます

check-manifest-update.yml
- name: Generate story
 run: yarn build:manifest
- name: '`yarn build:manifest` changes committed?'
 run: git diff --exit-code

3. mdxファイルを作成する

アイコンの使用方法や、注意点などがあれば、ここに記載していきます。
<Icons />部分はアイコンを一覧表示するコンポーネントです

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

<Meta title="Icons" />

# Feather Icons

[Feather Svg Icons](https://github.com/feathericons/feather) converted to [Material-UI](https://github.com/mui-org/material-ui) React components. You can install the package by running the following command in your project:

'''sh
# With yarn:
yarn add mui-feather
# With npm:
npm install mui-feather
'''

<Icons />

4. カタログを作成する

作成したmanifest使用して、種類ごとにタイトル区切りをしつつ、アイコンとそのコンポーネント名を同時に展開、表示します。

Icons.tsx
import * as React from 'react';
import * as iconComponents from '../src';
import manifest from './manifest.json';

const Icons = () => {
  return Object.keys(manifest).map((key) => {
    const files = manifest[key];
    if (!Array.isArray(files)) return null;

    return (
      <div key={key}>
        <h3>{key}</h3>
        <div style={{ display: 'flex', flexWrap: 'wrap' }}>
          {files.map((file) => {
            const Icon = iconComponents[`${file}`];

            if (!Icon) {
              console.log(`could not find icon...: ${file}`);
              return null;
            }

            return (
              <div
                key={file}
                style={{
                  width: '7.5rem',
                  display: 'flex',
                  flexDirection: 'column',
                  alignItems: 'center',
                  backgroundColor: '#d8d8d8',
                  color: 'black',
                  borderRadius: '4px',
                  marginRight: '8px',
                  marginBottom: '8px',
                  padding: '16px',
                }}
              >
                <Icon />
                <label htmlFor="id" style={{ marginTop: '8px' }}>
                  {file}
                </label>
              </div>
            );
          })}
        </div>
      </div>
    );
  });
};

export default Icons;

5. 挙動確認可能なstoryを作成する

Icons.stories.tsx
import { SvgIconProps } from '@material-ui/core/SvgIcon';
import * as React from 'react';
import * as iconComponents from '../src';
import manifest from './manifest.json';

// manifestから全コンポーネント名を取り出す
const allIcons = Object.values(manifest)
  .map((value) => value)
  .reduce((a, b) => a.concat(b), []);

// controlできるfieldを設定します。
// @storybook/addon-controlsが必要
export default {
  title: 'Icon/Basic',
  argTypes: {
    component: { control: { type: 'select', options: allIcons } },
    fontSize: { control: { type: 'select', options: ['default', 'small', 'large'] } },
    color: {
      control: {
        type: 'select',
        options: ['inherit', 'primary', 'secondary', 'disabled', 'error'],
      },
    },
  },
};

interface Props extends SvgIconProps {
  component: string;
}

export const Control = (args: Props) => {
  const Component = iconComponents[args.component];
  return <Component fontSize={args.fontSize} color={args.color} />;
};

Control.args = { component: 'User', fontSize: 'default', color: 'inherit' };

おわりに

簡易なスクリプトとmdxファイルを組み合わせて、アイコンカタログを作成してみました。
デザイン/開発者のコミュニケーションがし易くしつつも、コストをかけ過ぎず、うまくstorybook運用したいですね🤔

Discussion