FeaturesDesign System 컴포넌트Atoms (Button · Banner · …)

Atoms — Button · Banner · StatusPill · …

packages/ui/src/components/ (flat — atoms/ 하위 디렉토리 없음). SPEC #012 v2.

Inventory

@linkmusic/ui 가 export 하는 컴포넌트 전체 (packages/ui/src/index.ts 단일 소스):

컴포넌트용도API / variants
Buttonprimary actionvariant: primary · secondary · ghost · danger · size: sm/md/lg
Banner정보 / 경고 / 위험 / 성공variant(=tone 별칭): info · warn · danger · success (default info)
StatusPillenum status 시각tone: success · warn · danger · info · muted · playing (default muted)
OrgAvatar본사/매장 아바타initial-based fallback (이름 첫 글자)
Card카드 컨테이너Card · CardHeader · CardTitle · CardContent
DialogRadix Dialog wrapper개별 export — DialogTrigger·DialogPortal·DialogOverlay·DialogContent·DialogHeader·DialogBody·DialogFooter·DialogTitle·DialogDescription·DialogClose (namespace Dialog.X 없음)
DropdownMenuRadix DropdownMenu wrapperDropdownMenuTrigger·DropdownMenuPortal·DropdownMenuContent·DropdownMenuItem·DropdownMenuLabel·DropdownMenuSeparator
Tabs / Tab필터 button grouprole="group" + aria-pressed (ARIA Tabs pattern 아님 — button group 구현)
ListToolbar목록 공용 필터바 (검색 + 필터 N개 + 적용/초기화) — 운영자·매장·본사·감사 4목록 공용controlledsearch/onSearch · filters[](ListToolbarFilterkind:"select"(기본) | "date". date 면 <input type=date> + min/max, 감사 기간 from/to 용) · dirty(미적용 변경 힌트) · onApply(form submit) · onReset · rightSlot. 시안 ops-list-toolbar(배치6)
ListPagination목록 공용 페이지네이션 footertotal · page(1-base) · totalPages · onPrev/onNext(경계 disabled) · summary(0건 override)
ListNoResults조건 0건 빈 상태(필터 초기화 CTA)onReset · icon/title/description/resetLabel. 진짜 빈 목록(OpsEmpty)과 구분
ToastRadix Toast wrapperToastProvider·ToastViewport·Toast·ToastTitle·ToastDescription·ToastAction·ToastClose
Switchon/off 토글SwitchProps
Fieldlabel + input + errorrequired indicator · helperText
Input / Label폼 primitiveInputProps · LabelProps
FormSection폼 영역 groupingtitle · description · children
AuthShell인증 화면 셸Shells 참고
AuthCard인증 폼 카드login · change-password 본문 카드

OpsStepper · PageHeader@linkmusic/ui 가 아니라 apps/admin 의 셸 컴포넌트다 (apps/admin/src/components/shell/ops-stepper.tsx · page-header.tsx). 디자인 시스템 인벤토리가 아니므로 여기서 다루지 않는다.

Button

import { Button } from "@linkmusic/ui";
<Button variant="primary" size="md" onClick={...}>등록</Button>

CVA variants:

  • variant: primary · secondary · ghost · danger
  • size: sm (--tap-min 28px) · md (default 32) · lg (40)
  • disabled state
<Banner variant="warn">임시 비밀번호 사용 중</Banner>
<Banner tone="danger" title="장애">송출이 중단되었습니다</Banner>
<Banner variant="info">안내 메시지</Banner>
<Banner variant="success">저장되었습니다</Banner>

variant(info · warn · danger · success, default info) + tone(variant 별칭, 같은 값, 둘 다 오면 tone 우선). 옵셔널 title · action. 임퍼소네이션 빨간 배너는 별도 컴포넌트로, Banner variant 에는 없다.

StatusPill

<StatusPill tone="success">ACTIVE</StatusPill>
<StatusPill tone="warn">UNPAID</StatusPill>
<StatusPill tone="danger">SUSPENDED</StatusPill>

HqStatus / StoreStatus enum 표시.

OrgAvatar

<OrgAvatar name="본사 X" size="sm" />
// 첫 글자 "본" + 색상 hash (name 기반)

Dialog

Radix UI Dialog wrapper. namespace API (Dialog.Content) 가 아니라 개별 export 를 import 한다 (Dialog.Content 로 쓰면 빌드 깨짐):

import {
  Dialog,
  DialogContent,
  DialogHeader,
  DialogTitle,
  DialogDescription,
  DialogFooter,
} from "@linkmusic/ui";
 
<Dialog open={open} onOpenChange={setOpen}>
  <DialogContent>
    <DialogHeader>
      <DialogTitle>본사 모드로 전환</DialogTitle>
      <DialogDescription>모든 작업은 감사 로그에 기록됩니다.</DialogDescription>
    </DialogHeader>
    <DialogFooter>
      <Button variant="ghost" onClick={() => setOpen(false)}>취소</Button>
      <Button variant="primary" onClick={confirm}>전환</Button>
    </DialogFooter>
  </DialogContent>
</Dialog>

DialogContent 가 내부에서 DialogPortal + DialogOverlay 를 렌더하므로 호출처는 DialogContent 만 배치하면 된다.

Tabs

Radix Tabs primitive 이 아니라 button group 구현 (role="group" + aria-pressed). 라우팅·content panel 전환 없이 클라이언트 측 필터링에만 쓰여 ARIA Tabs pattern (role="tablist"/role="tab"/화살표 키 내비) 은 도입하지 않았다.

import { Tabs, Tab } from "@linkmusic/ui";
 
<Tabs aria-label="본사 상태 필터">
  <Tab active={filter === "all"} count={42} onClick={() => setFilter("all")}>전체</Tab>
  <Tab active={filter === "active"} count={30} onClick={() => setFilter("active")}>활성</Tab>
</Tabs>

Field

<Field label="이메일" required error={errors.email?.message}>
  <input type="email" {...register("email")} />
</Field>

Constraints

  • 모두 토큰만 사용 (raw color 금지)
  • Radix primitive (Dialog · DropdownMenu · Toast) 위에 우리 토큰으로 스킨. Tabs 는 Radix 미사용 (button group)
  • 화살표 함수 + 명명 export
  • props 명시적 type

Storybook 카탈로그

@linkmusic/ui 는 Storybook 9 (react-vite) 카탈로그를 갖는다. 토큰 cascade(Pretendard + Tailwind v4 + src/styles/all.css)가 preview 에 주입되고 light/dark 테마 토글(toolbar)로 토큰 변경 영향을 즉시 확인한다.

pnpm --filter @linkmusic/ui storybook        # dev (localhost:6006)
pnpm --filter @linkmusic/ui build-storybook  # 정적 빌드
  • config: packages/ui/.storybook/{main.ts,preview.tsx,storybook.css}
  • 대표 stories(컴포넌트 인접 *.stories.tsx): Button · StatusPill · Banner · Card · Field · Input · Dialog · Tabs · OrgAvatar · ListToolbar. 나머지 atom 은 후속 확장.
  • stories 는 프로덕션 pnpm -r build 에 섞이지 않는다(Storybook deps = devDependencies).

Roadmap

  • Table primitive (PRD Page 2·5·10 공통)
  • 미리듣기 Player (Surface 11)
  • Storybook stories 잔여 확장 (Switch·DropdownMenu·Toast·FormSection·AuthShell/AuthCard)

Toast · DropdownMenu 는 이미 @linkmusic/ui 에 존재하며 admin 에서 사용 중이다 (roadmap 항목 아님). 잔여 정비 항목이 있다면 API/스타일 정돈 수준이다.

References

  • SPEC #006 §2-3 (v1 atoms)
  • SPEC #012 (v2 재정의)
  • linkmusic-frontend-space/packages/ui/src/components/ (flat)
  • linkmusic-frontend-space/packages/ui/src/index.ts (export 단일 소스)