@pulley-hub/design-system
v1.0.0
Published
Pulley React design system and design tokens
Maintainers
Readme
@pulley-hub/design-system
Pulley 서비스에서 사용하는 React 컴포넌트, 아이콘, 디자인 토큰 패키지입니다.
- React 18 이상
- Tailwind CSS 4 디자인 토큰
- Radix UI 기반 접근성 컴포넌트
- ESM 및 TypeScript 타입 제공
AI 에이전트용 사용 계약
AI는 이 패키지를 사용할 때 아래 규칙을 우선 적용합니다.
- 이미 제공되는 컴포넌트를 HTML과 Tailwind로 다시 만들지 않습니다.
- 모든 컴포넌트와 아이콘은
@pulley-hub/design-system루트에서 named import 합니다. - 앱의 전역 진입점에서 스타일을 정확히 한 번 import 합니다.
- 색상, 간격, 타이포그래피는 raw 값보다
semantic-*디자인 토큰을 사용합니다. - 폼 요소에는 연결된 label 또는
aria-label을 제공합니다. - 아이콘만 있는 버튼에는 반드시
aria-label을 제공합니다. Dialog,AlertDialog,DropdownMenu,Popover,Tabs는 아래의 compound 구조를 유지합니다.Input과SearchInput의onChange는 DOM event가 아니라 변경된 문자열을 받습니다.- 로딩 중인 액션에는 직접 spinner를 조합하기보다
Button loading을 우선 사용합니다. - 앱 루트에는
Sonner를 한 번만 렌더링하고, 알림은useToast로 표시합니다.
AI에게 제공할 최소 프롬프트:
@pulley-hub/design-system을 우선 사용해 화면을 구현해.
README의 AI 에이전트용 사용 계약과 컴포넌트 API를 지켜.
기존 컴포넌트를 Tailwind로 다시 만들지 말고 semantic 디자인 토큰만 사용해.설치
pnpm add @pulley-hub/design-systemReact와 React DOM은 peer dependency입니다.
pnpm add react react-dom스타일 설정
Tailwind CSS 4 앱
앱의 전역 CSS에 다음 순서로 추가합니다.
@import 'tailwindcss';
@import '@pulley-hub/design-system/theme.css';
@import '@pulley-hub/design-system/styles.css';theme.css: 앱 코드에서도bg-semantic-fill-primary같은 Pulley 토큰 유틸리티를 생성합니다.styles.css: 컴포넌트에 필요한 CSS, 폰트, 토큰을 미리 컴파일한 파일입니다.- 전역 CSS는 Next.js의 root
layout.tsx등에서 한 번만 불러옵니다.
Tailwind를 사용하지 않는 React 앱
앱의 최상위 진입점에서 컴파일된 스타일만 import합니다.
import '@pulley-hub/design-system/styles.css'빠른 시작
'use client'
import { Button, Field, FieldLabel, FieldTitle, Input, Sonner, Text } from '@pulley-hub/design-system'
import { useState } from 'react'
export function ProfileForm() {
const [name, setName] = useState('')
return (
<>
<div className='flex flex-col gap-16'>
<Text as='h1' variant='title-2-bold'>
프로필
</Text>
<Field>
<FieldLabel htmlFor='name'>
<FieldTitle>이름</FieldTitle>
</FieldLabel>
<Input id='name' value={name} onChange={setName} />
</Field>
<Button type='submit'>저장</Button>
</div>
<Sonner />
</>
)
}선택 가이드
- 일반 액션:
Button - 링크처럼 약한 액션:
TextButton - 상태 또는 짧은 분류:
Badge - 짧은 텍스트 입력:
Input - 검색 입력:
SearchInput - 비밀번호 입력:
PasswordInput - 긴 텍스트 입력:
Textarea - 고정 목록 단일 선택:
Select - 검색 가능한 목록 또는 서버 페이지네이션 선택:
Combobox - 참/거짓 설정:
ToggleSwitch - 복수 선택:
Checkbox - 단일 선택 그룹:
RadioGroup - 짧은 확인 모달:
AlertDialog - 폼이나 복합 콘텐츠 모달:
Dialog - 일시적인 액션 목록:
DropdownMenu - 보조 콘텐츠:
Popover - 짧은 설명:
TailTooltip - 상호작용 가능한 상세 도움말:
DialogTooltip - 비동기 알림:
useToast+Sonner
컴포넌트 API
Typography
Text
as로 HTML 요소를 바꾸는 polymorphic 타이포그래피 컴포넌트입니다.
as:span,p,h1등variant: 아래 타이포 variantclassName: semantic 색상 등 추가 스타일
Variant:
- Display:
display-1-medium,display-1-bold,display-2-medium,display-2-bold,display-3-medium,display-3-bold - Title:
title-1-medium,title-1-bold,title-2-medium,title-2-bold,title-3-medium,title-3-bold - Heading:
heading-1-medium,heading-1-semibold,heading-2-medium,heading-2-semibold,heading-3-medium,heading-3-semibold - Body:
body-1-regular,body-1-medium,body-2-regular,body-2-medium,body-2-semibold,body-3-regular,body-3-medium,body-3-semibold - Label:
label-1-regular,label-1-semibold,label-2-medium,label-2-semibold - Caption:
caption-1-medium,caption-1-semibold,caption-2-medium,caption-2-semibold
<Text as='h2' variant='heading-1-semibold'>
강의 정보
</Text>Actions
Button
variant:primary|secondary|primary-weak|secondary-weak|secondary-weak-line|danger|danger-weak|text-secondary-line|text-weak-line|text-weaksize:xxlarge|xlarge|large|medium|small|xsmallloading: 로딩 표시 및 클릭 비활성화leftIcon,rightIcon: 텍스트 양옆 아이콘onlyIcon: 정사각형 아이콘 버튼radiusMax: pill 형태asChild: 자식 요소에 버튼 스타일 적용
<Button
variant='primary'
leftIcon={<IconPlusLine aria-hidden />}
loading={isSubmitting}
type='submit'
>
강의 추가
</Button>
<Button variant='secondary-weak' onlyIcon aria-label='닫기'>
<IconClosedLine aria-hidden />
</Button>xsmall은 텍스트 없는 onlyIcon 버튼에만 사용합니다.
TextButton
variant:text-secondary-line|text-weak-line|text-weaksize:xxlarge|xlarge|large|medium|smallloading,leftIcon,rightIcon,asChild지원
Filter
필터 trigger 버튼입니다. Dropdown 또는 Popover trigger와 조합합니다.
label: 표시할 값size:medium|smallhasValue: 선택된 필터가 있는지 여부thumbnail: 선택 값 앞의 썸네일hideIcon: 왼쪽 filter 아이콘 숨김
Form controls
Input
value: controlled valueonChange(text: string): 변경 문자열 콜백size:small|medium|largeinverse: 흰 배경 사용isError: 오류 스타일과aria-invalid적용- 나머지는 기본
input속성 지원
value가 있으면 clear 버튼이 자동으로 표시됩니다.
SearchInput
Input과 같은 API를 사용하며 search 아이콘과 clear 버튼을 포함합니다.
<SearchInput
aria-label='강의 검색'
placeholder='강의명을 입력하세요'
value={query}
onChange={setQuery}
/>PasswordInput
size:small|medium|largeinverse,isError지원- 비밀번호 표시/숨김 버튼을 내장합니다.
Textarea
size:small|medium|largeinverse: 흰 배경 사용- 기본
textarea속성 지원
InputGroup
커스텀 입력 UI가 필요할 때만 사용합니다.
InputGroup: 외곽 컨테이너,size,inverse지원InputGroupInput: 내부 inputInputGroupAddon:align='inline-start' | 'inline-end'
일반 텍스트 입력에는 Input 또는 SearchInput을 우선 사용합니다.
Select
검색이 필요 없는 단일 선택입니다.
const options = [
{ value: 'all', label: '전체' },
{ value: 'active', label: '진행 중' },
] as const
<Select
aria-label='강의 상태'
options={options}
value={status}
onValueChange={setStatus}
placeholder='상태 선택'
/>options:{ value, label, textValue?, disabled? }[]value,onValueChangesize:small|medium|largemenuSize:small|medium|largealign:start|center|endinverse,disabled,required,name,contentClassName
Combobox
검색, 서버 필터링, 무한 스크롤이 필요한 선택입니다.
options:{ value: string, label, textValue?, disabled? }[]selectedOption,value,onValueChange(value, label)searchValue,onSearchValueChangeshouldFilterOptions: 클라이언트 필터링 여부isLoading,loadingMessage,emptyMessagehasNextPage,onLoadMore,loadMoreThresholdsize,menuSize:small|medium|largealign,disabled,required,name,contentClassName
서버 검색에서는 shouldFilterOptions={false}를 사용합니다.
Checkbox
size:large|mediumlabel: 체크박스 옆 콘텐츠- Radix Checkbox의
checked,onCheckedChange,disabled지원
RadioGroup, RadioGroupItem
<RadioGroup value={role} onValueChange={setRole}>
<RadioGroupItem value='student' label='학생' />
<RadioGroupItem value='professor' label='교수자' />
</RadioGroup>RadioGroupItem.size:large|mediumRadioGroupItem.label: 라디오 옆 콘텐츠
ToggleSwitch
checked,onCheckedChangelabellabelPosition:left|rightdisabled
Slider
- Radix Slider의
value,onValueChange,min,max,step,disabled leftIcon,RightIconthumbAriaLabel
Field family
폼 label, 설명, 오류, 카운터를 일관된 구조로 조합합니다.
Field: 최상위 컨테이너,inverse,disabledFieldLabel,FieldTitleFieldLabelRequired,FieldLabelOptionalFieldDescription,FieldErrorFieldMessage:variant='information' | 'success' | 'danger'FieldGroup,FieldFooterFieldCounter:current,max,error
<Field>
<FieldLabel htmlFor='title'>
<FieldTitle>제목</FieldTitle>
<FieldLabelRequired />
</FieldLabel>
<Input id='title' value={title} onChange={setTitle} isError={hasError} />
<FieldFooter>
{hasError && <FieldError>제목을 입력하세요.</FieldError>}
<FieldCounter current={title.length} max={50} error={hasError} />
</FieldFooter>
</Field>Feedback and status
Badge
size:large|medium|smallcolor:primary|secondary|gradient|accent|positive|negative|warningtype:filled|weakshape:square|pill
Spinner
size:xxsmall|xsmall|small|medium|large|xlarge|xxlargetype:light|dark
Progress
value: 0~100, 범위를 벗어나면 자동 보정label,valueLabeltype:primary|secondary|mutedshimmer: 진행 중 shimmer 표시
Sonner, useToast
앱 root에 <Sonner />를 한 번 렌더링합니다.
const toast = useToast()
toast.success('저장했습니다.')
toast.danger('저장하지 못했습니다.')
toast.loading('자료를 생성하고 있습니다.')
toast.info('확인이 필요합니다.', {
action: {
label: '보기',
onClick: openDetail,
},
})메서드: info, success, warning, danger, loading, close.
Overlay
Dialog family
<Dialog open={isOpen} onOpenChange={setIsOpen}>
<DialogTrigger asChild>
<Button>열기</Button>
</DialogTrigger>
<DialogContent size='medium' closeOnOverlayClick={false}>
<DialogHeader>
<DialogTitle>강의 편집</DialogTitle>
</DialogHeader>
<DialogDescription>강의 정보를 수정합니다.</DialogDescription>
<DialogBody>{/* content */}</DialogBody>
<DialogFooter>{/* actions */}</DialogFooter>
</DialogContent>
</Dialog>DialogContent.size:large|medium|smallDialogContent.closeOnOverlayClick: 기본trueDialogHeader.hideCloseButton- 공개 primitive:
DialogTrigger,DialogPortal,DialogOverlay,DialogClose
AlertDialog family
되돌리기 어려운 확인 액션에 사용합니다.
AlertDialog,AlertDialogTrigger,AlertDialogContentAlertDialogHeader,AlertDialogTitle,AlertDialogDescriptionAlertDialogBody,AlertDialogFooterAlertDialogAction,AlertDialogCancelAlertDialogPortal,AlertDialogOverlay
DropdownMenu family
DropdownMenu,DropdownMenuTrigger,DropdownMenuContentDropdownMenuLabel,DropdownMenuItemDropdownMenuCheckboxItem,DropdownMenuSeparator- content/item
size:large|medium|small
Popover family
Popover,PopoverTrigger,PopoverContentPopoverAnchor,PopoverClose
Tooltip family
- 단순 도움말:
TailTooltip,TailTooltipTrigger,TailTooltipContent,TailTooltipProvider - 클릭 가능한 상세 도움말:
DialogTooltip,DialogTooltipTrigger,DialogTooltipContent - low-level primitive:
TooltipRoot,TooltipTrigger,TooltipContent,TooltipArrow,TooltipPortal,TooltipProvider
Navigation and data display
Tabs
<Tabs defaultValue='overview'>
<TabsList variant='line'>
<TabsTrigger value='overview'>개요</TabsTrigger>
<TabsTrigger value='students'>학생</TabsTrigger>
</TabsList>
<TabsContent value='overview'>...</TabsContent>
<TabsContent value='students'>...</TabsContent>
</Tabs>TabsList.variant:line|item- line 크기:
lineSize='medium' | 'large' - item 크기:
itemSize='small' | 'medium'
Breadcrumb family
Breadcrumb,BreadcrumbList,BreadcrumbItemBreadcrumbLink,BreadcrumbPageBreadcrumbSeparator,BreadcrumbEllipsis
Pagination family
Pagination,PaginationList,PaginationItemPaginationLink,PaginationPrevious,PaginationNextPaginationEllipsis
현재 페이지에는 PaginationLink isActive를 지정하고 접근 가능한 label을 제공합니다.
Table family
Table:variant='default' | 'inverse'TableHeader,TableBody,TableFooter,TableRowTableHead:required지원TableCell,TableCaptionTableEmpty: 빈 상태 행
ScrollArea
orientation:vertical|horizontal|both|nonevariant:light|darkScrollBar를 직접 조합할 수도 있습니다.
Avatar, AvatarGroup
Avatar.label: 표시할 이름 또는 이니셜Avatar.type:border|filledAvatar.color:primary|mutedAvatarGroup.data:AvatarProps[], 앞 3개와 나머지 개수를 표시
아이콘
모든 아이콘은 React SVG 컴포넌트이며 className, aria-hidden 등 SVG props를 받습니다.
import { IconPlusLine } from '@pulley-hub/design-system'
;<IconPlusLine className='size-20' aria-hidden />사용 가능한 아이콘:
IconAccountFilled, IconAiFilled, IconAsteriskLine, IconBoldLine, IconBookLine,
IconBottomLine, IconCalendarLine, IconCheckBold, IconCheckLine, IconClosedBold,
IconClosedLine, IconCompleteFilled, IconCompleteLine, IconComposeFilled,
IconCourseFilled, IconCoursesFilled, IconDeleteLine, IconDownLine,
IconDownloadLine, IconEditLine, IconEllipsisFilled, IconEmptydataLine,
IconExternalLine, IconFileLine, IconFileUseFilled, IconFilterdownFilled,
IconFilterLine, IconFilterupFilled, IconFindConceptLine, IconFindLine,
IconFindTestLine, IconFindTypeLine, IconFolderLine, IconGripFilled, IconGroupLine,
IconHeadingLine, IconHideFilled, IconHighlighterLine, IconHistoryLine,
IconImageLine, IconIndeterminateBold, IconIndeterminateLine, IconInfoFilled,
IconInfoLine, IconItalicLine, IconLanguageLine, IconLeftLine, IconListLine,
IconListNumberLine, IconLockFilled, IconMuteLine, IconNegativeFilled,
IconNofileLine, IconPanelRightLine, IconPlannerLine, IconPlusBold, IconPlusLine,
IconQuestionLine, IconRadicalLine, IconReloadLine, IconRightLine,
IconSearchcheckLine, IconSearchLine, IconSeparatorVertical, IconSettingLine,
IconSortdownLine, IconSortupLine, IconStarFilled, IconSubmitLine, IconSwitchLine,
IconTopLine, IconTreeExpand, IconTypeLine, IconUnderlineLine, IconUnmuteLine,
IconUserLine, IconViewFilled, IconWarningFilled.
디자인 토큰
Tailwind CSS 4 앱에서는 theme.css를 import한 뒤 semantic token을 사용할 수 있습니다.
<section className='bg-semantic-background-card text-semantic-text-normal p-24'>...</section>우선 사용해야 하는 토큰 접두어:
- 텍스트:
text-semantic-text-* - 아이콘:
text-semantic-icon-* - 배경/채움:
bg-semantic-fill-*,bg-semantic-background-* - 선:
border-semantic-line-* - 타이포그래피:
text-body-*,text-heading-*,text-label-* - 간격:
gap-primitive-*,p-primitive-* - radius:
rounded-semantic-element-size-*
#ffffff, text-gray-500, p-[13px]처럼 디자인 시스템 밖의 raw 값은 디자인 요구가 명확할 때만 사용합니다.
개발 및 배포
# Storybook
pnpm dev:ds
# 패키지 빌드
pnpm --filter @pulley-hub/design-system build
# 변경사항 기록
pnpm changeset배포는 main 브랜치의 Changesets GitHub Actions가 담당합니다. CI는 npm Trusted Publishing(OIDC)을 쓰므로 NPM_TOKEN은 필요 없습니다. npm 패키지 설정에서 Trusted Publisher로 GitHub Actions 워크플로 release-design-system.yml을 등록해야 합니다.
