@xonic0506/sonic-ui 0.4.3 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/DESIGN.md CHANGED
@@ -70,6 +70,7 @@ Figma Variable 이름이 그대로 CSS 변수가 돼요. `/`는 `-`로 바꾸고
70
70
  | `color/text/warning` | amber/700 `#85520A` | amber/300 `#EEC46A` | 주의 문구, 아이콘 |
71
71
  | `color/border/warning` | amber/600 | amber/400 | 주의 강조 테두리 |
72
72
  | `color/bg/danger-subtle` | red/50 `#FCEDEC` | red/950 `#2B1311` | danger Badge(subtle) |
73
+ | `color/bg/selected` | cobalt/50 `#EEF1FF` | cobalt/950 `#18214D` | DataGrid 선택된 행 (hover `bg/subtle`과 구분) |
73
74
 
74
75
  글자와 바탕 조합은 모두 WCAG 4.5:1 이상이에요. 토큰을 바꾸면 `npm run check:contrast`로 다시 확인해요(배포 전 검사에도 들어 있어요).
75
76
 
@@ -97,7 +98,9 @@ Figma Variable 이름이 그대로 CSS 변수가 돼요. `/`는 `-`로 바꾸고
97
98
  | `stroke/default` | 1 | 모든 테두리 |
98
99
  | `stroke/focus` | 2 | 포커스 링 (offset 2px) |
99
100
 
100
- ### 3.4 텍스트 스타일 (IBM Plex Sans / IBM Plex Mono)
101
+ ### 3.4 텍스트 스타일 (IBM Plex Sans KR / IBM Plex Mono)
102
+
103
+ 한글과 영문을 모두 IBM Plex Sans KR로 그려요. IBM Plex Sans에는 한글이 없어서, 쓰면 Figma와 브라우저가 각자 다른 대체 글꼴로 그려 줄바꿈이 달라져요.
101
104
 
102
105
  | 스타일 | 크기/행간 | 굵기 | 자간 | 쓰는 곳 |
103
106
  |---|---|---|---|---|
@@ -153,9 +156,11 @@ import { Plus } from 'lucide-react';
153
156
  | `error-404` · `error-403` · `error-500` | 320 | ErrorPage |
154
157
 
155
158
  - **색 토큰 규칙 밖의 이미지 자산이에요.** 캐릭터의 주황은 화면 강조색이 아니라 그림의 일부라서 §1 "강조는 한 곳에만"과 섞지 않아요. 그래서 위 세 곳 말고는 쓰지 않아요(버튼, 카드 장식, 배너 X).
159
+ - 예외: 디자인 시스템 문서·소개 화면에서 그림 자체를 보여 줄 때는 `illustrations`(아래)로 URL을 받아 써요. 제품 화면에서는 위 세 컴포넌트를 써요.
156
160
  - 배경이 투명해서 Light/Dark에 같은 그림을 써요.
157
161
  - 장식이라 `alt=""`로 숨겨요. 무엇이 없는지, 무엇이 실패했는지는 제목과 설명이 전해요.
158
162
  - 이미지는 패키지에 webp 파일로 들어 있어요(`dist/illustrations`, 표시 크기의 2배). 쓰는 쪽은 설정 없이 Vite와 Next.js가 자산으로 처리해요.
163
+ - URL은 `illustrations['<이름>']`으로 받아요(`import { illustrations } from '@xonic0506/sonic-ui'`, 서버 컴포넌트에서도 쓸 수 있어요). 이름은 위 표와 같아요. 장식이 아니라 그림이 주인공인 문서 화면이면 `alt`에 이름을 써요.
159
164
  - Popover, Select 안처럼 좁은 곳에서는 `icon`을 넘겨 아이콘으로 바꿔요(§10 EmptyState).
160
165
  - 추가할 때: 같은 캐릭터, 투명 배경, 표시 크기 2배로 만들어 Figma `Source illustrations`와 `src/illustrations`에 함께 넣어요.
161
166
 
@@ -250,7 +255,7 @@ hover, focus는 **컴포넌트 variant로 만들지 않아요.** React prop이
250
255
 
251
256
  | 계층 | 위치 | 기준 | 예 |
252
257
  |---|---|---|---|
253
- | **Core** | sonic-ui `src/components/` | 더 나눌 수 없고, 도메인을 전혀 모름 | Button, Input, Textarea, Checkbox, RadioGroup, Switch, Select, Text, Card, Avatar, Dialog, AlertDialog, Menu, Popover, Tooltip, Tabs, Separator, Toast, Badge, Progress, Link, Table, ToggleGroup, Accordion, FileDropzone, FileList, Field, Skeleton, EmptyState, ErrorState, ErrorPage |
258
+ | **Core** | sonic-ui `src/components/` | 더 나눌 수 없고, 도메인을 전혀 모름 | Button, Input, Textarea, Checkbox, RadioGroup, Switch, Select, Text, Card, Avatar, Dialog, AlertDialog, Menu, Popover, Tooltip, Tabs, Separator, Toast, Badge, Progress, Link, Table, DataGrid, Pagination, ToggleGroup, Accordion, FileDropzone, FileList, Field, Skeleton, EmptyState, ErrorState, ErrorPage |
254
259
  | **Pattern** | sonic-ui `src/patterns/` | Core 조합이고 도메인을 모르지만, **두 프로젝트 이상에서 같은 형태로 복사됨** | (아직 없음) |
255
260
  | **Domain** | 각 프로젝트 | 도메인 타입, API, 비즈니스 로직 중 하나라도 앎 | `UserProfileCard`, `ApiKeyList` |
256
261
 
@@ -298,6 +303,7 @@ Figma Core 페이지의 컴포넌트 속성 이름 = React props 이름이에요
298
303
  | `disabled` | boolean | `false` |
299
304
  | `children` | 버튼 문구. 아이콘은 문구 앞뒤에 lucide 컴포넌트로 넣음 | — |
300
305
  | `loading` | boolean. 스피너 + 클릭 막기 + `aria-busy` | `false` |
306
+ | `focusableWhenDisabled` | boolean. 비활성이 돼도 포커스를 유지해요(`aria-disabled`). 누른 뒤 비활성이 되는 버튼에 써요(Pagination의 처음·마지막) | `false` |
301
307
  | `aria-label` | icon 크기일 때 **필수** (타입으로 강제) | — |
302
308
 
303
309
  | | sm | md | lg |
@@ -712,8 +718,110 @@ toast.add({ title: 'API 키를 만들었어요', description: '키는 지금 한
712
718
  | prop | 값 |
713
719
  |---|---|
714
720
  | `TableHead`, `TableCell`의 `align` | `start` · `end` (숫자 열은 `end`) |
721
+ | `TableHead`의 `sort`, `onSort` | `asc` · `desc` · `none`. `onSort`를 넘기면 머리글이 버튼이 되고 `aria-sort`가 붙어요 |
722
+ | `TableRow`의 `selected`, `interactive`, `dragging` | 선택 `bg/selected`, hover `bg/subtle`(interactive일 때만), 이동 중 모양 |
723
+ | `TableHead`의 `handle` | 컬럼 이동 손잡이. hover·포커스 때만 보이고 정렬 반대쪽 끝에 놓여요 (DataGrid가 씀) |
724
+ | `TableHead`, `TableCell`의 `pinned` | `left` · `right`. 가로 스크롤 중에도 붙어 있고 경계에 1px `border/default`. 여러 열을 고정하면 `style.left`·`style.right`로 위치를 넘겨요 |
725
+
726
+ 여러 항목의 같은 속성을 비교할 때 써요. 행 높이 40(`space/10`), 좌우 padding `space/3`, 행 아래 1px `border/default`. 머리글 `label` + `text/muted`, 본문 `body` + `text/default`, `align="end"`인 본문 칸은 숫자라서 `mono`예요. 줄무늬는 쓰지 않고, hover 바탕은 `interactive` 행에만 써요. 표가 부모보다 넓으면 가로로 스크롤해요.
727
+
728
+ 위 상태 props는 모양만 바꿔요. 정렬·선택·이동 상태를 직접 관리하기 번거로우면 DataGrid를 써요. 정렬된 머리글은 `text/default` + `arrow-up`/`arrow-down` 16(간격 `space/1`)이고, 칸끼리 붙어 있어서 포커스 링은 안쪽(offset −2px)에 그려요.
729
+
730
+ ### DataGrid
731
+
732
+ ```tsx
733
+ <DataGrid
734
+ aria-label="API 키"
735
+ rows={keys}
736
+ getRowId={row => row.id}
737
+ columns={[
738
+ { field: 'name', header: '이름', type: 'text', pinned: 'left', width: 160, sortable: true },
739
+ { field: 'requests', header: '요청 수', type: 'number', sortable: true },
740
+ { field: 'status', header: '상태', type: 'badge', tones: { active: 'success', revoked: 'danger' }, labels: { active: '활성', revoked: '폐기됨' } },
741
+ { field: 'createdAt', header: '만든 날', type: 'date' },
742
+ { field: 'enabled', header: '사용', type: 'switch', onChange: (row, on) => toggle(row.id, on) },
743
+ { id: 'actions', type: 'actions', pinned: 'right', actions: [
744
+ { label: '이름 바꾸기', icon: Pencil, onClick: rename },
745
+ { label: '삭제', icon: Trash2, tone: 'danger', onClick: confirmDelete },
746
+ ] },
747
+ ]}
748
+ selection={selected} onSelectionChange={setSelected}
749
+ onRowOrderChange={ids => saveOrder(ids)}
750
+ loading={isLoading}
751
+ emptyState={<EmptyState title="API 키가 없어요" action={<Button>API 키 만들기</Button>} />}
752
+ />
753
+ ```
754
+
755
+ 정렬, 행 선택, 좌/우 고정, 컬럼·행 이동이 필요한 목록에 써요. 보여 주기만 하는 표는 Table을 써요. 모양은 Table이 그리고, 상태는 DataGrid가 관리해요(TanStack Table, dnd-kit. 쓰는 쪽은 설치하지 않아요).
756
+
757
+ | prop | 값 |
758
+ |---|---|
759
+ | `rows`, `getRowId` | 행 데이터와 행 id. `rows`에는 **원본 값**을 넣어요(숫자는 number, 날짜는 ISO 문자열) |
760
+ | `columns` | 열 정의 배열. `type`에 따라 필요한 속성이 타입으로 강제돼요(아래 표) |
761
+ | `aria-label` | 표 이름 (필수) |
762
+ | `sorting`, `defaultSorting`, `onSortingChange` | `{ id, desc }` 또는 `null`. 한 번에 한 열만 정렬해요 |
763
+ | `selection`, `onSelectionChange` | 선택된 행 id 배열. `onSelectionChange`를 넘기면 맨 앞에 체크박스 열이 생겨요 |
764
+ | `columnOrder`, `onColumnOrderChange` | 열 id 순서. `onColumnOrderChange`를 넘기면 머리글을 끌어서 옮길 수 있어요 |
765
+ | `onRowOrderChange` | `(ids) => void`. 넘기면 핸들 열이 생겨요. 정렬 중에는 핸들을 숨겨요 |
766
+ | `loading` | Skeleton 행 5개 + `aria-busy` |
767
+ | `emptyState`, `errorState` | 행이 없을 때, 불러오기 실패했을 때 본문 자리에 보여 줄 EmptyState·ErrorState |
768
+ | `pagination` | 넘기면 표 아래에 Pagination이 붙어요. `{ page, defaultPage, onPageChange, pageSize, defaultPageSize, onPageSizeChange, pageSizeOptions, showFirstLast, rowCount }` (모두 선택, 페이지는 1부터) |
769
+
770
+ | `type` | 보여 주는 것 | 추가 속성 |
771
+ |---|---|---|
772
+ | `text` | `body` | — |
773
+ | `number` | `mono`, 오른쪽 정렬, 천 단위 쉼표 | — |
774
+ | `date` · `datetime` | `mono`, `YYYY-MM-DD` · `YYYY-MM-DD HH:mm:ss` (브라우저 현지 시간) | — |
775
+ | `badge` | Badge subtle | `tones`(값 → tone, 필수), `labels`(값 → 문구). 없는 값은 neutral |
776
+ | `switch` | Switch, 누르면 바로 적용 | `onChange(row, checked)` (필수) |
777
+ | `actions` | 1개면 ghost `icon-sm` 버튼, 2개 이상이면 `ellipsis` Menu | `actions: { label, icon?, tone?, onClick(row) }[]` (필수) |
778
+
779
+ 공통 열 속성: `header`, `width`(px), `pinned`(`left` · `right`), `sortable`. 고정 열에는 `width`를 꼭 줘요(다음 고정 열 위치 계산에 써요). `select`·`handle` 열은 너비 40이고, 있으면 항상 왼쪽에 고정돼요.
780
+
781
+ - 행 hover `bg/subtle`, 선택 `bg/selected`. 고정 열 경계는 1px `border/default`이고 그림자는 쓰지 않아요(§1 원칙 4).
782
+ - 컬럼 이동 중인 머리글은 `bg/subtle`, 행 이동 중인 행은 `bg/surface` + 1px `border/default` + `radius/md`. 놓을 위치는 2px `border/focus` 선이에요.
783
+ - 핸들은 `grip-vertical` 16(`text/muted`, hover `text/default`)이고 바깥 2px 포커스 링을 그려요. 키보드로도 옮겨요: 핸들에 포커스 → Space로 들기 → 화살표 → Space로 놓기(Esc 취소). 화면 낭독기 안내는 한국어 해요체예요.
784
+ - 위험한 actions는 바로 실행하지 말고 `onClick`에서 AlertDialog를 열어요(§10 Menu).
785
+ - 행이 많으면(100개 초과) 보이는 행만 그려요. 이때는 `className`으로 높이를 정해 주세요(예: `h-96`). 그 안에서 세로로 스크롤돼요.
786
+ - Next.js: `columns`에 함수가 들어 있어서 서버 컴포넌트에서 넘길 수 없어요. DataGrid는 클라이언트 컴포넌트 안에서 정의해요.
787
+
788
+ **페이지로 나누기**
789
+
790
+ ```tsx
791
+ <DataGrid rows={allKeys} pagination={{ defaultPageSize: 20 }} … /> // 클라이언트: rows 전체를 DataGrid가 나눠요
792
+ <DataGrid rows={pageRows} pagination={{ page, onPageChange: setPage, pageSize, onPageSizeChange: setPageSize, rowCount: total }}
793
+ sorting={sort} onSortingChange={setSort} … /> // 서버: 현재 페이지 rows + 전체 개수
794
+ ```
795
+
796
+ - `rowCount`가 없으면 클라이언트 모드예요. 정렬하거나 페이지당 행 수를 바꾸면 1페이지로 돌아가요.
797
+ - `rowCount`를 넘기면 서버 모드예요. 받은 `rows`를 그대로 보여 주고, 정렬도 서버가 해요(`onSortingChange`로 받아 다시 불러와요). 한 페이지만 정렬하면 결과가 틀리기 때문이에요.
798
+ - 전체 선택 체크박스는 **현재 페이지** 행만 골라요. 다른 페이지에서 고른 행은 그대로 남아요.
799
+ - 행 이동은 현재 페이지 안에서만 돼요. 페이지네이션을 켜면 가상 스크롤은 쓰지 않아요.
800
+ - 행이 없거나 불러오기 실패면 Pagination을 숨겨요.
801
+
802
+ ### Pagination
803
+
804
+ ```tsx
805
+ <Pagination page={page} pageCount={20} onPageChange={setPage} />
806
+ <Pagination page={page} pageCount={20} onPageChange={setPage} pageSize={pageSize} onPageSizeChange={setPageSize} showFirstLast={false} />
807
+ ```
808
+
809
+ | prop | 값 | 기본값 |
810
+ |---|---|---|
811
+ | `page`, `onPageChange` | 현재 페이지(1부터)와 바뀔 때 | (필수) |
812
+ | `pageCount` | 전체 페이지 수 | (필수) |
813
+ | `pageSize`, `onPageSizeChange` | 넘기면 왼쪽에 "페이지당 행" Select가 생겨요 | — |
814
+ | `pageSizeOptions` | 페이지당 행 수 목록 | `[10, 20, 50]` |
815
+ | `showFirstLast` | 처음·마지막 버튼 | `true` |
816
+ | `aria-label` | 페이지 이동 영역 이름 | "페이지" |
817
+
818
+ 목록을 페이지로 나눌 때 써요. Table이나 카드 목록과 함께 쓰고, DataGrid는 `pagination` prop으로 붙여요.
715
819
 
716
- 여러 항목의 같은 속성을 비교할 때 써요. 행 높이 40(`space/10`), 좌우 padding `space/3`, 행 아래 1px `border/default`. 머리글 `label` + `text/muted`, 본문 `body` + `text/default`, `align="end"`인 본문 칸은 숫자라서 `mono`예요. 줄무늬나 hover 바탕은 쓰지 않아요. 표가 부모보다 넓으면 가로로 스크롤해요.
820
+ - 왼쪽에 페이지당 행 수(라벨 `label` + `text/muted`, Select는 글자 너비에 맞춰요, 라벨과 `space/2`), 오른쪽에 `« ‹ 1 … 4 5 6 … 20 › »`. 둘 사이 `space/4`, 버튼 사이 `space/1`.
821
+ - 번호 칸 28×28(`space/7`), `radius/md`, `mono`. 현재 페이지는 `bg/subtle` + `text/default` + `aria-current="page"`이고 강조색은 쓰지 않아요(§1 원칙 2). 다른 번호는 `text/muted`, hover `bg/subtle` + `text/default`. 포커스 링은 바깥 2px(offset 2px).
822
+ - 번호는 처음·마지막과 현재 양옆 1개만 보이고, 나머지는 `…`로 줄여요. 항상 최대 7칸이라 너비가 흔들리지 않아요.
823
+ - 처음·이전·다음·마지막은 ghost `icon-sm`(`chevrons-left`, `chevron-left`, `chevron-right`, `chevrons-right`)이고, 첫 페이지에서는 처음·이전이, 마지막 페이지에서는 다음·마지막이 비활성이에요.
824
+ - 좁은 화면(640 미만)에서는 번호 대신 `4 / 20`(`mono-caption` + `text/muted`)만 보이고, 행 수 라벨도 숨겨요(Select 이름은 `aria-label`로 남아요). Figma `viewport=mobile`.
717
825
 
718
826
  ### ToggleGroup
719
827
 
@@ -858,11 +966,13 @@ Sonic 팀은 Figma Pro 요금제라 Code Connect(Organization 이상)를 쓸 수
858
966
  | Badge | `57:3498` | `<Badge tone variant>` (`children` → 자식 문구) | `src/components/badge.tsx` |
859
967
  | Progress | `58:5093` | `<Progress value max label>` (`value` variant는 채움 미리보기, `value text`는 코드가 자동 표시) | `src/components/progress.tsx` |
860
968
  | Link | `65:3733` | `<Link href external>` (`children` → 자식 문구) | `src/components/link.tsx` |
861
- | Table Cell · Table | `65:3742` · `65:3743` | `Table`, `TableHeader`, `TableBody`, `TableRow`, `TableHead`, `TableCell` (`type=header` → `TableHead`, `align` → 같은 prop) | `src/components/table.tsx` |
969
+ | Table Cell · Table | `65:3742` · `65:3743` | `Table`, `TableHeader`, `TableBody`, `TableRow`, `TableHead`, `TableCell` (`type=header` → `TableHead`, `align`·`sort`·`pinned` → 같은 prop, `state=hover·selected` → `TableRow`의 `interactive`·`selected`) | `src/components/table.tsx` |
970
+ | Pagination Item · Pagination | `138:5684` · `138:5747` | `<Pagination page pageCount onPageChange pageSize onPageSizeChange showFirstLast>` (`state=current` → 현재 `page`, `viewport` → prop 아님, 좁은 화면은 코드가 자동 처리) | `src/components/pagination.tsx` |
971
+ | Data Grid Cell | `130:6234` | `<DataGrid columns>`의 `columns[].type` (`type` 값이 같음. `select`·`handle`은 `onSelectionChange`·`onRowOrderChange`로 생기는 열) | `src/components/data-grid.tsx` |
862
972
  | Toggle Item · Toggle Group | `66:3760` · `66:3761` | `<ToggleGroup multiple>` + `<ToggleGroupItem value disabled>` (`pressed` → 현재 값, `content=icon`이면 `aria-label`) | `src/components/toggle-group.tsx` |
863
973
  | Accordion Item · Accordion | `66:3792` · `66:3793` | `Accordion`, `AccordionItem`, `AccordionTrigger`, `AccordionPanel` (`open` → 현재 값, `title`/`content` → 자식) | `src/components/accordion.tsx` |
864
974
  | File Dropzone · File Item | `71:4191` · `72:4251` | `<FileDropzone variant accept multiple maxSize maxFiles onFilesChange>` (`state` → 끌어다 놓기·거부·`disabled`), `<FileList layout items onRemove onRetry>` (`status` → `items[].status`) | `src/components/file-upload.tsx` |
865
- | Illustration (5개) | Foundations `Source illustrations` | 코드에서 직접 쓰지 않음. EmptyState·ErrorState·ErrorPage 안에 들어 있어요(§3.7) | `src/illustrations/*.webp` |
866
- | Icon (37개) | Foundations `Source icons` | `lucide-react`의 같은 이름 컴포넌트 (`Icon / chevron-down` → `<ChevronDown />`) | 쓰는 쪽에서 설치 |
975
+ | Illustration (5개) | Foundations `Source illustrations` | 제품 화면은 EmptyState·ErrorState·ErrorPage 안에서 써요. 문서 화면은 `illustrations['error-404']`로 URL(§3.7) | `src/illustrations/*.webp` |
976
+ | Icon (42개) | Foundations `Source icons` | `lucide-react`의 같은 이름 컴포넌트 (`Icon / chevron-down` → `<ChevronDown />`) | 쓰는 쪽에서 설치 |
867
977
 
868
978
  Figma 속성 → props 변환 규칙: variant 속성은 같은 이름의 prop으로, 텍스트 속성 `children`은 자식으로, boolean `header`/`footer`는 해당 하위 컴포넌트를 넣을지 말지로 옮겨요. Button의 `iconLeft`/`iconRight`(+ `leftIcon`/`rightIcon` 교체)와 Button Icon의 `icon`은 prop이 아니라 **자식 순서**예요: `<Button><Plus />추가하기<ChevronDown /></Button>`.
package/README.md CHANGED
@@ -51,14 +51,14 @@ npm i @xonic0506/sonic-ui
51
51
 
52
52
  ### 글꼴
53
53
 
54
- IBM Plex Sans(400, 500, 600)와 IBM Plex Mono(400)를 프로젝트에서 불러와요. 없으면 시스템 글꼴로 대체돼요.
54
+ IBM Plex Sans KR(400, 500, 600)과 IBM Plex Mono(400)를 프로젝트에서 불러와요. 없으면 시스템 글꼴로 대체돼요. 한글과 영문을 모두 IBM Plex Sans KR로 그려요. Figma 텍스트 스타일과 같은 글꼴이라 줄바꿈 위치가 같아요.
55
55
 
56
56
  ```html
57
- <link href="https://fonts.googleapis.com/css2?family=IBM+Plex+Mono&family=IBM+Plex+Sans:wght@400;500;600&display=swap" rel="stylesheet" />
57
+ <link href="https://fonts.googleapis.com/css2?family=IBM+Plex+Mono&family=IBM+Plex+Sans+KR:wght@400;500;600&display=swap" rel="stylesheet" />
58
58
  ```
59
59
 
60
60
  > [!NOTE]
61
- > 글꼴은 패키지에 들어 있지 않아요. sonic-ui는 글꼴 이름 `"IBM Plex Sans"`, `"IBM Plex Mono"`를 찾기 때문에, 이름을 바꾸는 방식(예: `next/font` 기본 설정)으로 불러오면 적용되지 않아요. 위 링크나 `@fontsource/ibm-plex-sans`처럼 원래 이름을 유지하는 방식을 써요.
61
+ > 글꼴은 패키지에 들어 있지 않아요. sonic-ui는 글꼴 이름 `"IBM Plex Sans KR"`, `"IBM Plex Mono"`를 찾기 때문에, 이름을 바꾸는 방식(예: `next/font` 기본 설정)으로 불러오면 적용되지 않아요. 위 링크나 `@fontsource/ibm-plex-sans-kr`처럼 원래 이름을 유지하는 방식을 써요. 한글 글꼴은 크지만 unicode-range로 나뉘어 있어서, 실제로는 화면에 쓴 글자가 든 조각만 받아요.
62
62
 
63
63
  ### CSS 연결
64
64
 
@@ -6,6 +6,8 @@ interface ButtonBaseProps extends Omit<React.ButtonHTMLAttributes<HTMLButtonElem
6
6
  render?: React.ReactElement;
7
7
  /** 진행 중: 스피너 표시, 클릭 막기, 포커스 유지, aria-busy */
8
8
  loading?: boolean;
9
+ /** 비활성이 돼도 포커스를 잃지 않아요. 누른 뒤 비활성이 되는 버튼에 써요 (예: 마지막 페이지) */
10
+ focusableWhenDisabled?: boolean;
9
11
  }
10
12
  /** 아이콘만 있는 버튼은 읽을 글자가 없어서 aria-label이 필수예요 */
11
13
  export type ButtonProps = ButtonBaseProps & ({
@@ -6,22 +6,22 @@ import { Button as r } from "@base-ui/react/button";
6
6
  import { LoaderCircle as i } from "lucide-react";
7
7
  import { Fragment as a, jsx as o, jsxs as s } from "react/jsx-runtime";
8
8
  //#region src/components/button.tsx
9
- var c = n.forwardRef(function({ variant: n, size: c, className: l, render: u, loading: d, disabled: f, children: p, ...m }, h) {
10
- let g = /* @__PURE__ */ o(i, { className: "sn:animate-[sn-spin_var(--sn-duration-loop)_linear_infinite] sn:motion-reduce:animate-none" });
9
+ var c = n.forwardRef(function({ variant: n, size: c, className: l, render: u, loading: d, disabled: f, focusableWhenDisabled: p, children: m, ...h }, g) {
10
+ let _ = /* @__PURE__ */ o(i, { className: "sn:animate-[sn-spin_var(--sn-duration-loop)_linear_infinite] sn:motion-reduce:animate-none" });
11
11
  return /* @__PURE__ */ o(r, {
12
- ref: h,
12
+ ref: g,
13
13
  render: u,
14
14
  nativeButton: !u || u.type === "button",
15
15
  disabled: f || d,
16
- focusableWhenDisabled: d,
16
+ focusableWhenDisabled: d || p,
17
17
  "aria-busy": d || void 0,
18
18
  "data-loading": d ? "" : void 0,
19
19
  className: e(t({
20
20
  variant: n,
21
21
  size: c
22
22
  }), l),
23
- ...m,
24
- children: d && c?.startsWith("icon-") ? g : d ? /* @__PURE__ */ s(a, { children: [g, p] }) : p
23
+ ...h,
24
+ children: d && c?.startsWith("icon-") ? _ : d ? /* @__PURE__ */ s(a, { children: [_, m] }) : m
25
25
  });
26
26
  });
27
27
  //#endregion
@@ -0,0 +1,88 @@
1
+ import * as React from 'react';
2
+ import { type RowData } from '@tanstack/react-table';
3
+ import { type BadgeProps } from './badge';
4
+ type Field<Row> = Extract<keyof Row, string>;
5
+ type BadgeTone = NonNullable<BadgeProps['tone']>;
6
+ interface ColumnBase {
7
+ header?: string;
8
+ /** px. 고정 열에는 꼭 줘요 (다음 고정 열 위치 계산) */
9
+ width?: number;
10
+ pinned?: 'left' | 'right';
11
+ }
12
+ interface FieldColumn<Row> extends ColumnBase {
13
+ field: Field<Row>;
14
+ /** 기본은 field */
15
+ id?: string;
16
+ sortable?: boolean;
17
+ }
18
+ export interface DataGridAction<Row> {
19
+ label: string;
20
+ icon?: React.ComponentType<{
21
+ className?: string;
22
+ }>;
23
+ /** danger는 맨 아래 구분선 뒤에 놓여요. 실행 전에 AlertDialog로 확인해요 */
24
+ tone?: 'default' | 'danger';
25
+ onClick: (row: Row) => void;
26
+ }
27
+ /** Figma Data Grid Cell의 type과 같은 이름이에요 */
28
+ export type DataGridColumn<Row> = (FieldColumn<Row> & {
29
+ type: 'text' | 'number' | 'date' | 'datetime';
30
+ }) | (FieldColumn<Row> & {
31
+ type: 'badge';
32
+ tones: Record<string, BadgeTone>;
33
+ labels?: Record<string, string>;
34
+ }) | (FieldColumn<Row> & {
35
+ type: 'switch';
36
+ onChange: (row: Row, checked: boolean) => void;
37
+ }) | (ColumnBase & {
38
+ type: 'actions';
39
+ id: string;
40
+ actions: DataGridAction<Row>[];
41
+ });
42
+ export interface DataGridSort {
43
+ id: string;
44
+ desc: boolean;
45
+ }
46
+ export interface DataGridPagination {
47
+ /** 현재 페이지 (1부터) */
48
+ page?: number;
49
+ defaultPage?: number;
50
+ onPageChange?: (page: number) => void;
51
+ pageSize?: number;
52
+ /** 기본 20 */
53
+ defaultPageSize?: number;
54
+ onPageSizeChange?: (pageSize: number) => void;
55
+ pageSizeOptions?: number[];
56
+ showFirstLast?: boolean;
57
+ /** 서버에서 나눌 때 전체 행 수 */
58
+ rowCount?: number;
59
+ }
60
+ export interface DataGridProps<Row> {
61
+ /** 원본 값을 넣어요 (숫자는 number, 날짜는 ISO 문자열) */
62
+ rows: Row[];
63
+ getRowId: (row: Row) => string;
64
+ columns: DataGridColumn<Row>[];
65
+ 'aria-label': string;
66
+ sorting?: DataGridSort | null;
67
+ defaultSorting?: DataGridSort | null;
68
+ onSortingChange?: (sorting: DataGridSort | null) => void;
69
+ /** 선택된 행 id. onSelectionChange를 넘기면 체크박스 열이 생겨요 */
70
+ selection?: string[];
71
+ onSelectionChange?: (ids: string[]) => void;
72
+ /** 열 id 순서. onColumnOrderChange를 넘기면 머리글을 끌어서 옮길 수 있어요 */
73
+ columnOrder?: string[];
74
+ onColumnOrderChange?: (ids: string[]) => void;
75
+ /** 넘기면 핸들 열이 생겨요. 정렬 중에는 핸들을 숨겨요 */
76
+ onRowOrderChange?: (ids: string[]) => void;
77
+ loading?: boolean;
78
+ emptyState?: React.ReactNode;
79
+ errorState?: React.ReactNode;
80
+ /** 넘기면 표 아래에 Pagination이 붙어요. rowCount를 넘기면 서버 모드(받은 rows를 그대로, 정렬도 서버) */
81
+ pagination?: DataGridPagination;
82
+ /** 행이 많으면 보이는 행만 그려요. 이때 높이를 정해 주세요 (예: h-96) */
83
+ className?: string;
84
+ }
85
+ /** Figma Data Grid Cell의 type 목록 (select·handle은 props로 생기는 열) */
86
+ export declare const dataGridCellTypes: readonly ['text', 'number', 'date', 'datetime', 'badge', 'switch', 'actions', 'select', 'handle'];
87
+ export declare function DataGrid<Row extends RowData>({ rows, getRowId, columns, 'aria-label': ariaLabel, sorting, defaultSorting, onSortingChange, selection, onSelectionChange, columnOrder, onColumnOrderChange, onRowOrderChange, loading, emptyState, errorState, pagination, className, }: DataGridProps<Row>): React.JSX.Element;
88
+ export {};