프로젝트에서 사용할 공통 컴포넌트를 제작하는 ui 패키지를 만들었다

 

Directory

 

components: Button, Input 과 같은 컴포넌트가 있는 폴더

hooks: custom hook을 관리할 폴더

styles: reset.css palette.css 등 공통 css 를 관리할 폴더

types: helper type이 있는 폴더

Style

https://vanilla-extract.style/를 이용하여 스타일을 설정하였습니다

 

vanilla-extract의 장점으로

  • type safe하게 theme를 다룰 수 있습니다.
  • 프론트앤드 프레임워크에 구애받지 않습니다.
  • Tailwind 처럼 Atomic CSS를 구성할 수도 있습니다.
  • Sttitches 처럼 variant 기반 스타일링을 구성할 수 있습니다.

Bundle

rollup 을 이용해서 번들링을 하였습니다

 

// rollup.config.js
import { vanillaExtractPlugin } from '@vanilla-extract/rollup-plugin';
import rollupResolve from '@rollup/plugin-node-resolve';
import commonjs from '@rollup/plugin-commonjs';
import typescript from 'rollup-plugin-typescript2';
import peerDepsExternal from 'rollup-plugin-peer-deps-external';
import preserveDirective from 'rollup-plugin-preserve-directives';
import dts from 'rollup-plugin-dts';
import pkg from './package.json' assert { type: 'json' };

const external = [...Object.keys(pkg.dependencies || {}), ...Object.keys(pkg.peerDependencies || {})];

/**
 * @type {import('rollup').RollupOptions}
 */
const config = [
  {
    input: 'src/index.ts',
    output: [
      {
        file: pkg.types,
        format: 'esm',
      },
    ],
    plugins: [dts()],
    external,
  },
  {
    input: 'src/index.ts',
    output: [
      {
        dir: 'dist',
        format: 'esm',
        exports: 'named',
        banner: arg => (/components\/[^/]+\/index.js/.test(arg.fileName) ? `'use client'` : ''),
        preserveModules: true,
        preserveModulesRoot: 'src',
        assetFileNames(assetInfo) {
          const assetPath = assetInfo.name.replace(/^src\//, 'css/');
          return assetPath;
        },
      },
    ],
    external,
    treeshake: {
      moduleSideEffects: false,
    },
    plugins: [
      vanillaExtractPlugin(),
      rollupResolve(),
      commonjs({ include: /node_modules/ }),
      typescript({ useTsconfigDeclarationDir: true }),
      peerDepsExternal(),
      preserveDirective(),
    ],
  },
];

export default config;

use client banner를 추가하여 next app router에서도 사용할수 있게 설정하였고

vanilla-extract css 관련파일은 css dir하위로 생성하게 설정하였습니다

 

post build 단계에서 다음 스크립트를 실행시켜 index.css파일을 생성한뒤 사용하는 프로젝트에서 css파일을 로드시키는것으로 하였습니다

// scripts/postbuild.js
import path from 'path';
import fs from 'fs';

function getAllFiles(dirPath, filesArray = []) {
  const files = fs.readdirSync(dirPath);

  files.forEach(file => {
    const filePath = path.join(dirPath, file);
    if (fs.statSync(filePath).isDirectory()) {
      getAllFiles(filePath, filesArray);
    } else {
      filesArray.push(filePath);
    }
  });

  return filesArray;
}
const vanillaCssList = getAllFiles('dist/css');

const contents = vanillaCssList.map(filePath => fs.readFileSync(filePath, 'utf8')).join('\n\n');
fs.writeFileSync('dist/css/index.css', contents);
// package.json
{
  "name": "@breadlee/ui",
  "version": "0.0.4",
  "main": "./dist/index.js",
  "module": "./dist/index.js",
  "types": "./dist/index.d.ts",
  "sideEffects": false,
  "type": "module",
  "license": "MIT",
  "description": "",
  "files": [
    "dist/**"
  ],
  "scripts": {
    "prebuild": "[ ! -d dist ] && mkdir -p dist || rm -rf dist/*",
    "build": "rollup --config",
    "postbuild": "node scripts/postbuild.js && pnpm run minify",
    "minify": "cleancss -o ./dist/css/index.css ./dist/css/index.css",
    "typecheck": "tsc -b tsconfig.json"
  },
  "peerDependencies": {
    "react": "^18.2.0",
    "react-dom": "^18.2.0"
  },
  "devDependencies": {
    "@breadlee/eslint-config": "workspace:*",
    "@breadlee/icons": "workspace:*",
    "@breadlee/tsconfig": "workspace:*",
    "@breadlee/utils": "workspace:^",
    "@rollup/plugin-commonjs": "^25.0.7",
    "@rollup/plugin-node-resolve": "^15.2.3",
    "@types/node": "^20.11.7",
    "@types/react": "^18.2.55",
    "@types/react-dom": "^18.2.19",
    "@vanilla-extract/css": "^1.14.1",
    "@vanilla-extract/rollup-plugin": "^1.3.4",
    "clean-css-cli": "^5.6.3",
    "eslint": "^8.56.0",
    "rollup": "^4.9.6",
    "rollup-plugin-dts": "^6.1.0",
    "rollup-plugin-peer-deps-external": "^2.2.4",
    "rollup-plugin-preserve-directives": "^0.4.0",
    "rollup-plugin-typescript2": "^0.36.0",
    "typescript": "^5.3.3"
  },
  "publishConfig": {
    "access": "public"
  }
}

 

Component

다음 Button 컴포넌트와 같이 컴포넌트를 components하위에 개발할 예정입니다.

// src/components/Button/index.tsx

import { ComponentPropsWithoutRef, ElementType, ReactNode, useMemo } from 'react';
import { classnames } from '@breadlee/utils';
import Typography, { TypographyProps } from '../Typography';
import styles from './index.css';

interface ButtonOwnProps {
  children: ReactNode;
  color?: 'primary' | 'secondary' | 'tertiary' | 'error';
  size?: 'xlarge' | 'large' | 'medium' | 'small';
  isFullWidth?: boolean;
  typographyProps?: Omit<TypographyProps, 'children'>;
}

export type ButtonProps<E extends ElementType = 'button'> = ButtonOwnProps &
  Omit<ComponentPropsWithoutRef<E>, keyof ButtonOwnProps> & { component?: E };

const Button = <E extends ElementType = 'button'>({
  children,
  color = 'primary',
  component,
  isFullWidth,
  size = 'medium',
  typographyProps,
  ...otherProps
}: ButtonProps<E>) => {
  const Component = component || 'button';

  if (component && !(otherProps.href || otherProps.to)) {
    throw new Error('anchor tag 또는 button tag 만 올수있습니다');
  }

  const defaultTypographyProps = useMemo<ButtonProps['typographyProps']>(() => {
    switch (size) {
      case 'xlarge':
        return {
          weight: 'regular',
          variant: 'B1',
        };
      case 'large':
        return {
          weight: 'regular',
          variant: 'B2',
        };
      case 'small':
        return {
          weight: 'regular',
          variant: 'D2',
        };
      default:
        return {
          weight: 'regular',
          variant: 'B2',
        };
    }
  }, [size]);

  return (
    <Component
      className={classnames(styles.base, styles.color[color], styles.size[size], {
        [styles.fullWidth]: !!isFullWidth,
      })}
      {...(Component === 'button' && { type: 'button' })}
      {...otherProps}
    >
      {typeof children === 'string' ? (
        <Typography {...defaultTypographyProps} {...typographyProps}>
          {children}
        </Typography>
      ) : (
        children
      )}
    </Component>
  );
};

export default Button;

// src/components/Button/index.css.ts

import { style } from '@vanilla-extract/css';
import { palette } from '../../styles';

const base = style({
  display: 'inline-flex',
  alignItems: 'center',
  textAlign: 'center',
  justifyContent: 'center',
  padding: '0 24px',
  borderRadius: 8,
});

const fullWidth = style({
  width: '100%',
});

const size = {
  xlarge: style({
    height: 52,
  }),
  large: style({
    height: 48,
  }),
  medium: style({
    height: 40,
  }),
  small: style({
    height: 32,
    borderRadius: 4,
    padding: '0 12px',
  }),
};

const color = {
  primary: style({
    backgroundColor: palette.Primary,
    border: `1px solid ${palette.Primary}`,
    color: palette.PrimaryOn,

    ':active': {
      backgroundColor: palette.PrimaryContainer,
      border: `1px solid ${palette.PrimaryContainer}`,
      color: palette.PrimaryContainerOn,
    },
    ':disabled': {
      backgroundColor: palette.Gray500,
      border: `1px solid ${palette.Gray500}`,
    },
  }),
  secondary: style({
    backgroundColor: palette.Secondary,
    border: `1px solid ${palette.Secondary}`,
    color: palette.SecondaryOn,

    ':active': {
      backgroundColor: palette.SecondaryContainer,
      border: `1px solid ${palette.SecondaryContainer}`,
      color: palette.SecondaryContainerOn,
    },
    ':disabled': {
      backgroundColor: palette.Gray500,
      border: `1px solid ${palette.Gray500}`,
    },
  }),
  tertiary: style({
    backgroundColor: palette.Tertiary,
    border: `1px solid ${palette.Tertiary}`,
    color: palette.TertiaryOn,

    ':active': {
      backgroundColor: palette.TertiaryContainer,
      border: `1px solid ${palette.TertiaryContainer}`,
      color: palette.TertiaryContainerOn,
    },
    ':disabled': {
      backgroundColor: palette.Gray500,
      border: `1px solid ${palette.Gray500}`,
    },
  }),
  error: style({
    backgroundColor: palette.Error,
    border: `1px solid ${palette.Error}`,
    color: palette.ErrorOn,

    ':active': {
      backgroundColor: palette.ErrorContainer,
      border: `1px solid ${palette.ErrorContainer}`,
      color: palette.ErrorContainerOn,
    },
    ':disabled': {
      backgroundColor: palette.Gray500,
      border: `1px solid ${palette.Gray500}`,
    },
  }),
};

const buttonStyle = {
  base,
  fullWidth,
  size,
  color,
};
export default buttonStyle;

 

 

자세한 소스는 여기서 확인가능합니다

https://github.com/leeyc924/leeyc-package/tree/main/packages/ui

'SideProject > Design System' 카테고리의 다른 글

7. Storybook  (3) 2024.02.23
6. utils  (2) 2024.02.18
4. icon  (5) 2024.02.08
3. npm publish  (2) 2024.02.02
2. eslint, tsconifg 설정  (3) 2024.02.01

+ Recent posts