@xonic0506/sonic-ui 0.5.0 → 0.6.1

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
 
@@ -254,7 +255,7 @@ hover, focus는 **컴포넌트 variant로 만들지 않아요.** React prop이
254
255
 
255
256
  | 계층 | 위치 | 기준 | 예 |
256
257
  |---|---|---|---|
257
- | **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 |
258
259
  | **Pattern** | sonic-ui `src/patterns/` | Core 조합이고 도메인을 모르지만, **두 프로젝트 이상에서 같은 형태로 복사됨** | (아직 없음) |
259
260
  | **Domain** | 각 프로젝트 | 도메인 타입, API, 비즈니스 로직 중 하나라도 앎 | `UserProfileCard`, `ApiKeyList` |
260
261
 
@@ -302,6 +303,7 @@ Figma Core 페이지의 컴포넌트 속성 이름 = React props 이름이에요
302
303
  | `disabled` | boolean | `false` |
303
304
  | `children` | 버튼 문구. 아이콘은 문구 앞뒤에 lucide 컴포넌트로 넣음 | — |
304
305
  | `loading` | boolean. 스피너 + 클릭 막기 + `aria-busy` | `false` |
306
+ | `focusableWhenDisabled` | boolean. 비활성이 돼도 포커스를 유지해요(`aria-disabled`). 누른 뒤 비활성이 되는 버튼에 써요(Pagination의 처음·마지막) | `false` |
305
307
  | `aria-label` | icon 크기일 때 **필수** (타입으로 강제) | — |
306
308
 
307
309
  | | sm | md | lg |
@@ -716,8 +718,110 @@ toast.add({ title: 'API 키를 만들었어요', description: '키는 지금 한
716
718
  | prop | 값 |
717
719
  |---|---|
718
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`로 위치를 넘겨요 |
719
725
 
720
- 여러 항목의 같은 속성을 비교할 때 써요. 행 높이 40(`space/10`), 좌우 padding `space/3`, 행 아래 1px `border/default`. 머리글 `label` + `text/muted`, 본문 `body` + `text/default`, `align="end"`인 본문 칸은 숫자라서 `mono`예요. 줄무늬나 hover 바탕은 쓰지 않아요. 표가 부모보다 넓으면 가로로 스크롤해요.
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으로 붙여요.
819
+
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`.
721
825
 
722
826
  ### ToggleGroup
723
827
 
@@ -862,11 +966,13 @@ Sonic 팀은 Figma Pro 요금제라 Code Connect(Organization 이상)를 쓸 수
862
966
  | Badge | `57:3498` | `<Badge tone variant>` (`children` → 자식 문구) | `src/components/badge.tsx` |
863
967
  | Progress | `58:5093` | `<Progress value max label>` (`value` variant는 채움 미리보기, `value text`는 코드가 자동 표시) | `src/components/progress.tsx` |
864
968
  | Link | `65:3733` | `<Link href external>` (`children` → 자식 문구) | `src/components/link.tsx` |
865
- | 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` |
866
972
  | Toggle Item · Toggle Group | `66:3760` · `66:3761` | `<ToggleGroup multiple>` + `<ToggleGroupItem value disabled>` (`pressed` → 현재 값, `content=icon`이면 `aria-label`) | `src/components/toggle-group.tsx` |
867
973
  | Accordion Item · Accordion | `66:3792` · `66:3793` | `Accordion`, `AccordionItem`, `AccordionTrigger`, `AccordionPanel` (`open` → 현재 값, `title`/`content` → 자식) | `src/components/accordion.tsx` |
868
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` |
869
975
  | Illustration (5개) | Foundations `Source illustrations` | 제품 화면은 EmptyState·ErrorState·ErrorPage 안에서 써요. 문서 화면은 `illustrations['error-404']`로 URL(§3.7) | `src/illustrations/*.webp` |
870
- | Icon (37개) | Foundations `Source icons` | `lucide-react`의 같은 이름 컴포넌트 (`Icon / chevron-down` → `<ChevronDown />`) | 쓰는 쪽에서 설치 |
976
+ | Icon (42개) | Foundations `Source icons` | `lucide-react`의 같은 이름 컴포넌트 (`Icon / chevron-down` → `<ChevronDown />`) | 쓰는 쪽에서 설치 |
871
977
 
872
978
  Figma 속성 → props 변환 규칙: variant 속성은 같은 이름의 prop으로, 텍스트 속성 `children`은 자식으로, boolean `header`/`footer`는 해당 하위 컴포넌트를 넣을지 말지로 옮겨요. Button의 `iconLeft`/`iconRight`(+ `leftIcon`/`rightIcon` 교체)와 Button Icon의 `icon`은 prop이 아니라 **자식 순서**예요: `<Button><Plus />추가하기<ChevronDown /></Button>`.
@@ -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 {};
@@ -0,0 +1,415 @@
1
+ "use client";
2
+ import { cn as e } from "../lib/cn.js";
3
+ import { Button as t } from "./button.js";
4
+ import { Checkbox as n } from "./checkbox.js";
5
+ import { Switch as r } from "./switch.js";
6
+ import { Skeleton as i } from "./skeleton.js";
7
+ import { Menu as a, MenuItem as o, MenuPopup as s, MenuSeparator as c, MenuTrigger as l } from "./menu.js";
8
+ import { Badge as u } from "./badge.js";
9
+ import { TableBody as d, TableCell as f, TableHead as p, TableHeader as ee, TableRow as m, tableClass as te } from "./table.js";
10
+ import { formatDate as h, formatDateTime as g, formatNumber as _ } from "../lib/format.js";
11
+ import { Pagination as ne } from "./pagination.js";
12
+ import * as v from "react";
13
+ import { Ellipsis as y, GripVertical as b } from "lucide-react";
14
+ import { Fragment as re, jsx as x, jsxs as S } from "react/jsx-runtime";
15
+ import { columnOrderingFeature as C, columnPinningFeature as w, columnSizingFeature as T, createPaginatedRowModel as ie, createSortedRowModel as E, rowPaginationFeature as D, rowSortingFeature as O, tableFeatures as k, useTable as ae } from "@tanstack/react-table";
16
+ import { useVirtualizer as oe } from "@tanstack/react-virtual";
17
+ import { DndContext as se, KeyboardSensor as ce, PointerSensor as le, closestCenter as ue, useSensor as de, useSensors as fe } from "@dnd-kit/core";
18
+ import { SortableContext as pe, arrayMove as me, horizontalListSortingStrategy as he, sortableKeyboardCoordinates as ge, useSortable as A, verticalListSortingStrategy as _e } from "@dnd-kit/sortable";
19
+ //#region src/components/data-grid.tsx
20
+ var j = "__select", M = "__handle", ve = 100, ye = k({
21
+ rowSortingFeature: O,
22
+ sortedRowModel: E(),
23
+ rowPaginationFeature: D,
24
+ paginatedRowModel: ie(),
25
+ columnOrderingFeature: C,
26
+ columnSizingFeature: T,
27
+ columnPinningFeature: w
28
+ }), be = (e, t, n) => {
29
+ let r = e.getValue(n), i = t.getValue(n);
30
+ return r == null || i == null ? r == null ? i == null ? 0 : -1 : 1 : typeof r == "number" && typeof i == "number" ? r - i : typeof r == "boolean" ? Number(r) - Number(i) : String(r).localeCompare(String(i), "ko", { numeric: !0 });
31
+ }, N = (e) => "field" in e ? e.id ?? e.field : e.id, xe = {
32
+ before: "sn:shadow-[inset_0_2px_0_var(--sn-color-border-focus)]",
33
+ after: "sn:shadow-[inset_0_-2px_0_var(--sn-color-border-focus)]"
34
+ }, P = {
35
+ before: "sn:shadow-[inset_2px_0_0_var(--sn-color-border-focus)]",
36
+ after: "sn:shadow-[inset_-2px_0_0_var(--sn-color-border-focus)]"
37
+ }, F = "sn:flex sn:size-4 sn:cursor-grab sn:items-center sn:justify-center sn:rounded-md sn:border-0 sn:bg-transparent sn:p-0 sn:text-muted sn:hover:text-default sn:focus-visible:outline-2 sn:focus-visible:outline-offset-2 sn:focus-visible:outline-focus sn:[&_svg]:size-4";
38
+ function I(e, t) {
39
+ let n = (e) => t().indexOf(String(e)) + 1;
40
+ return {
41
+ onDragStart: ({ active: t }) => `${e}을 들었어요. 지금 ${n(t.id)}번째예요.`,
42
+ onDragOver: ({ over: e }) => e ? `${n(e.id)}번째 위치예요.` : "놓을 수 없는 위치예요.",
43
+ onDragEnd: ({ over: e }) => e ? `${n(e.id)}번째에 놓았어요.` : "옮기지 않았어요.",
44
+ onDragCancel: () => "옮기기를 취소했어요."
45
+ };
46
+ }
47
+ var Se = { draggable: "Space로 들고, 화살표로 옮긴 다음 Space로 놓아요. Esc로 취소해요." };
48
+ function Ce(e, t) {
49
+ let [n, r] = v.useState(null);
50
+ return {
51
+ drop: n,
52
+ onDragOver: ({ active: t, over: n }) => {
53
+ if (!n || n.id === t.id) return r(null);
54
+ r({
55
+ id: String(n.id),
56
+ side: e.indexOf(String(n.id)) > e.indexOf(String(t.id)) ? "after" : "before"
57
+ });
58
+ },
59
+ onDragEnd: ({ active: n, over: i }) => {
60
+ r(null), i && i.id !== n.id && t(me(e, e.indexOf(String(n.id)), e.indexOf(String(i.id))));
61
+ },
62
+ onDragCancel: () => r(null)
63
+ };
64
+ }
65
+ function L(e, t, n) {
66
+ let [r, i] = v.useState(t);
67
+ return [e === void 0 ? r : e, (t) => {
68
+ e === void 0 && i(t), n?.(t);
69
+ }];
70
+ }
71
+ function we(e, t) {
72
+ let n = e.getIsPinned(), r = t || n ? e.getSize() : void 0;
73
+ return {
74
+ width: r,
75
+ minWidth: r,
76
+ left: n === "start" ? e.getStart("start") : void 0,
77
+ right: n === "end" ? e.getAfter("end") : void 0
78
+ };
79
+ }
80
+ function Te({ actions: e, row: n }) {
81
+ if (e.length === 1) {
82
+ let [r] = e, i = r.icon;
83
+ return i ? /* @__PURE__ */ x(t, {
84
+ size: "icon-sm",
85
+ variant: "ghost",
86
+ "aria-label": r.label,
87
+ onClick: () => r.onClick(n),
88
+ children: /* @__PURE__ */ x(i, {})
89
+ }) : /* @__PURE__ */ x(t, {
90
+ size: "sm",
91
+ variant: "ghost",
92
+ onClick: () => r.onClick(n),
93
+ children: r.label
94
+ });
95
+ }
96
+ let r = e.filter((e) => e.tone !== "danger"), i = e.filter((e) => e.tone === "danger"), u = (e) => {
97
+ let t = e.icon;
98
+ return /* @__PURE__ */ S(o, {
99
+ tone: e.tone,
100
+ onClick: () => e.onClick(n),
101
+ children: [t && /* @__PURE__ */ x(t, {}), e.label]
102
+ }, e.label);
103
+ };
104
+ return /* @__PURE__ */ S(a, { children: [/* @__PURE__ */ x(l, {
105
+ render: /* @__PURE__ */ x(t, {
106
+ size: "icon-sm",
107
+ variant: "ghost",
108
+ "aria-label": "더 보기"
109
+ }),
110
+ children: /* @__PURE__ */ x(y, {})
111
+ }), /* @__PURE__ */ S(s, { children: [
112
+ r.map(u),
113
+ r.length > 0 && i.length > 0 && /* @__PURE__ */ x(c, {}),
114
+ i.map(u)
115
+ ] })] });
116
+ }
117
+ function Ee(e, t, n) {
118
+ switch (e.type) {
119
+ case "text": return n == null ? "" : String(n);
120
+ case "number": return _(n);
121
+ case "date": return h(n);
122
+ case "datetime": return g(n);
123
+ case "badge": {
124
+ let t = n == null ? "" : String(n);
125
+ return /* @__PURE__ */ x(u, {
126
+ tone: e.tones[t] ?? "neutral",
127
+ children: e.labels?.[t] ?? t
128
+ });
129
+ }
130
+ case "switch": return /* @__PURE__ */ x(r, {
131
+ checked: !!n,
132
+ onCheckedChange: (n) => e.onChange(t, n),
133
+ children: /* @__PURE__ */ x("span", {
134
+ className: "sn:sr-only",
135
+ children: e.header
136
+ })
137
+ });
138
+ case "actions": return /* @__PURE__ */ x(Te, {
139
+ actions: e.actions,
140
+ row: t
141
+ });
142
+ }
143
+ }
144
+ function De({ id: t, label: n, drop: r, className: i, ...a }) {
145
+ let { setNodeRef: o, setActivatorNodeRef: s, attributes: c, listeners: l, isDragging: u } = A({
146
+ id: t,
147
+ attributes: { roleDescription: "옮길 수 있는 열" }
148
+ });
149
+ return /* @__PURE__ */ x(p, {
150
+ ref: o,
151
+ className: e(i, u && "sn:bg-subtle", r && P[r]),
152
+ handle: /* @__PURE__ */ x("button", {
153
+ type: "button",
154
+ ref: s,
155
+ ...c,
156
+ ...l,
157
+ "aria-label": `${n} 열 옮기기`,
158
+ className: F,
159
+ children: /* @__PURE__ */ x(b, {})
160
+ }),
161
+ ...a
162
+ });
163
+ }
164
+ function Oe({ id: e, draggable: t, renderCells: n, ...r }) {
165
+ let { setNodeRef: i, setActivatorNodeRef: a, attributes: o, listeners: s, isDragging: c } = A({
166
+ id: e,
167
+ disabled: !t,
168
+ attributes: { roleDescription: "옮길 수 있는 행" }
169
+ }), l = t ? /* @__PURE__ */ x("button", {
170
+ type: "button",
171
+ ref: a,
172
+ ...o,
173
+ ...s,
174
+ "aria-label": "행 옮기기",
175
+ className: F,
176
+ children: /* @__PURE__ */ x(b, {})
177
+ }) : null;
178
+ return /* @__PURE__ */ x(m, {
179
+ ref: i,
180
+ interactive: !0,
181
+ dragging: c,
182
+ ...r,
183
+ children: n(l)
184
+ });
185
+ }
186
+ function ke({ rows: t, getRowId: r, columns: a, "aria-label": o, sorting: s, defaultSorting: c = null, onSortingChange: l, selection: u, onSelectionChange: h, columnOrder: g, onColumnOrderChange: _, onRowOrderChange: y, loading: b, emptyState: C, errorState: w, pagination: T, className: ie }) {
187
+ let E = u !== void 0 || h !== void 0, D = y !== void 0, O = _ !== void 0, [k, me] = L(s, c, l), [A, P] = L(u, [], h), [F, Te] = L(g, a.map(N), _), [ke, R] = L(T?.page, T?.defaultPage ?? 1, T?.onPageChange), [z, Ae] = L(T?.pageSize, T?.defaultPageSize ?? 20, T?.onPageSizeChange), B = T?.rowCount !== void 0, je = T ? Math.max(1, Math.ceil((B ? T.rowCount : t.length) / z)) : 1, V = Math.min(ke, je), Me = v.useId(), H = v.useMemo(() => new Map(a.map((e) => [N(e), e])), [a]), Ne = v.useMemo(() => {
188
+ let e = [];
189
+ E && e.push({
190
+ id: j,
191
+ size: 40,
192
+ enableSorting: !1
193
+ }), D && e.push({
194
+ id: M,
195
+ size: 40,
196
+ enableSorting: !1
197
+ });
198
+ for (let t of a) {
199
+ let n = t.width ?? (t.type === "actions" ? 56 : void 0);
200
+ e.push("field" in t ? {
201
+ id: N(t),
202
+ accessorFn: (e) => e[t.field],
203
+ enableSorting: !!t.sortable,
204
+ sortFn: be,
205
+ ...n && { size: n }
206
+ } : {
207
+ id: t.id,
208
+ enableSorting: !1,
209
+ ...n && { size: n }
210
+ });
211
+ }
212
+ return e;
213
+ }, [
214
+ a,
215
+ E,
216
+ D
217
+ ]), Pe = v.useMemo(() => ({
218
+ start: [
219
+ ...E ? [j] : [],
220
+ ...D ? [M] : [],
221
+ ...a.filter((e) => e.pinned === "left").map(N)
222
+ ],
223
+ end: a.filter((e) => e.pinned === "right").map(N)
224
+ }), [
225
+ a,
226
+ E,
227
+ D
228
+ ]), U = ae({
229
+ features: ye,
230
+ columns: Ne,
231
+ data: t,
232
+ getRowId: (e) => r(e),
233
+ state: {
234
+ sorting: k ? [k] : [],
235
+ columnOrder: F,
236
+ columnPinning: Pe,
237
+ pagination: {
238
+ pageIndex: V - 1,
239
+ pageSize: T ? z : t.length
240
+ }
241
+ },
242
+ manualPagination: !T || B,
243
+ manualSorting: B
244
+ }), W = [
245
+ ...U.getStartFlatHeaders(),
246
+ ...U.getCenterFlatHeaders(),
247
+ ...U.getEndFlatHeaders()
248
+ ], G = U.getRowModel().rows, Fe = t.map(r), K = G.map((e) => e.id), q = U.getCenterFlatHeaders().map((e) => e.column.id), Ie = D && !k, Le = v.useRef(null), J = !T && G.length > ve, Re = oe({
249
+ count: J ? G.length : 0,
250
+ getScrollElement: () => Le.current,
251
+ estimateSize: () => 40,
252
+ overscan: 8
253
+ }), Y = J ? Re.getVirtualItems() : null, ze = Y ? Y.map((e) => G[e.index]) : G, Be = Y?.[0]?.start ?? 0, X = Y ? Re.getTotalSize() - (Y.at(-1)?.end ?? 0) : 0, Ve = typeof document > "u" ? void 0 : document.body, He = fe(de(le, { activationConstraint: { distance: 4 } }), de(ce, { coordinateGetter: ge })), Z = Ce(Fe, (e) => y?.(e)), Q = Ce(q, (e) => Te([...F.filter((t) => !e.includes(t)), ...e])), Ue = (e) => {
254
+ me(k?.id === e ? k.desc ? null : {
255
+ id: e,
256
+ desc: !0
257
+ } : {
258
+ id: e,
259
+ desc: !1
260
+ }), T && V !== 1 && R(1);
261
+ }, We = K.length > 0 && K.every((e) => A.includes(e)), Ge = !We && K.some((e) => A.includes(e)), Ke = (e) => P([...A.filter((e) => !K.includes(e)), ...e ? K : []]), qe = (t) => {
262
+ let { column: r } = t, i = r.id, a = r.getIsPinned(), o = a === "start" ? "left" : a === "end" ? "right" : void 0, s = {
263
+ pinned: o,
264
+ style: we(r, i === j || i === M || H.get(i)?.width !== void 0),
265
+ className: e("sn:sticky sn:top-0 sn:z-20 sn:bg-surface", o && "sn:z-30")
266
+ };
267
+ if (i === j) return /* @__PURE__ */ x(p, {
268
+ ...s,
269
+ children: /* @__PURE__ */ x(n, {
270
+ checked: We,
271
+ indeterminate: Ge,
272
+ onCheckedChange: Ke,
273
+ children: /* @__PURE__ */ x("span", {
274
+ className: "sn:sr-only",
275
+ children: "모든 행 선택"
276
+ })
277
+ })
278
+ }, i);
279
+ if (i === M) return /* @__PURE__ */ x(p, {
280
+ ...s,
281
+ children: /* @__PURE__ */ x("span", {
282
+ className: "sn:sr-only",
283
+ children: "순서"
284
+ })
285
+ }, i);
286
+ let c = H.get(i), l = c.header ?? "", u = "sortable" in c && c.sortable ? {
287
+ sort: k?.id === i ? k.desc ? "desc" : "asc" : "none",
288
+ onSort: () => Ue(i)
289
+ } : {}, d = c.type === "actions" && !c.header ? /* @__PURE__ */ x("span", {
290
+ className: "sn:sr-only",
291
+ children: "동작"
292
+ }) : l, f = c.type === "number" || c.type === "actions" ? "end" : "start";
293
+ return O && !o ? /* @__PURE__ */ x(De, {
294
+ id: i,
295
+ label: l || "동작",
296
+ align: f,
297
+ drop: Q.drop?.id === i ? Q.drop.side : void 0,
298
+ ...s,
299
+ ...u,
300
+ children: d
301
+ }, i) : /* @__PURE__ */ x(p, {
302
+ align: f,
303
+ ...s,
304
+ ...u,
305
+ children: d
306
+ }, i);
307
+ }, Je = (e) => /* @__PURE__ */ x(m, { children: /* @__PURE__ */ x(f, {
308
+ colSpan: W.length,
309
+ className: "sn:h-auto sn:p-0",
310
+ children: e
311
+ }) }), $;
312
+ return $ = w ? Je(w) : b ? Array.from({ length: 5 }, (e, t) => /* @__PURE__ */ x(m, { children: W.map((e) => /* @__PURE__ */ x(f, { children: e.column.id === j || e.column.id === M ? null : /* @__PURE__ */ x(i, {
313
+ shape: "text",
314
+ className: "sn:w-2/3"
315
+ }) }, e.id)) }, t)) : G.length === 0 ? C ? Je(C) : null : /* @__PURE__ */ S(re, { children: [
316
+ Be > 0 && /* @__PURE__ */ x("tr", {
317
+ "aria-hidden": !0,
318
+ style: { height: Be }
319
+ }),
320
+ ze.map((t) => {
321
+ let r = A.includes(t.id), i = Z.drop?.id === t.id ? xe[Z.drop.side] : void 0, a = [
322
+ ...t.getStartVisibleCells(),
323
+ ...t.getCenterVisibleCells(),
324
+ ...t.getEndVisibleCells()
325
+ ];
326
+ return /* @__PURE__ */ x(Oe, {
327
+ id: t.id,
328
+ draggable: Ie,
329
+ selected: r,
330
+ "aria-selected": E ? r : void 0,
331
+ "aria-rowindex": J ? t.index + 2 : void 0,
332
+ renderCells: (o) => a.map((a) => {
333
+ let { column: s } = a, c = s.id, l = s.getIsPinned(), u = l === "start" ? "left" : l === "end" ? "right" : void 0, d = H.get(c), p;
334
+ return p = c === j ? /* @__PURE__ */ x(n, {
335
+ checked: r,
336
+ onCheckedChange: (e) => P(e ? [...A, t.id] : A.filter((e) => e !== t.id)),
337
+ children: /* @__PURE__ */ x("span", {
338
+ className: "sn:sr-only",
339
+ children: "행 선택"
340
+ })
341
+ }) : c === M ? o : Ee(d, t.original, a.getValue()), /* @__PURE__ */ x(f, {
342
+ pinned: u,
343
+ style: we(s, c === j || c === M || d?.width !== void 0),
344
+ align: d?.type === "number" ? "end" : "start",
345
+ className: e((d?.type === "date" || d?.type === "datetime") && "sn:font-mono", d?.type === "actions" && "sn:text-right", i),
346
+ children: p
347
+ }, a.id);
348
+ })
349
+ }, t.id);
350
+ }),
351
+ X > 0 && /* @__PURE__ */ x("tr", {
352
+ "aria-hidden": !0,
353
+ style: { height: X }
354
+ })
355
+ ] }), /* @__PURE__ */ S("div", {
356
+ className: e("sn:flex sn:w-full sn:flex-col sn:gap-4", ie),
357
+ children: [/* @__PURE__ */ x("div", {
358
+ ref: Le,
359
+ className: "sn:relative sn:min-h-0 sn:w-full sn:flex-1 sn:overflow-auto",
360
+ children: /* @__PURE__ */ S("table", {
361
+ "aria-label": o,
362
+ "aria-busy": b || void 0,
363
+ "aria-rowcount": J ? G.length + 1 : void 0,
364
+ className: e(te, "sn:bg-surface"),
365
+ children: [/* @__PURE__ */ x(se, {
366
+ id: `DndDescribedBy-${Me}-columns`,
367
+ sensors: He,
368
+ collisionDetection: ue,
369
+ accessibility: {
370
+ container: Ve,
371
+ announcements: I("열", () => q),
372
+ screenReaderInstructions: Se
373
+ },
374
+ onDragOver: Q.onDragOver,
375
+ onDragEnd: Q.onDragEnd,
376
+ onDragCancel: Q.onDragCancel,
377
+ children: /* @__PURE__ */ x(ee, { children: /* @__PURE__ */ x(pe, {
378
+ items: O ? q : [],
379
+ strategy: he,
380
+ children: /* @__PURE__ */ x(m, { children: W.map(qe) })
381
+ }) })
382
+ }), /* @__PURE__ */ x(se, {
383
+ id: `DndDescribedBy-${Me}-rows`,
384
+ sensors: He,
385
+ collisionDetection: ue,
386
+ accessibility: {
387
+ container: Ve,
388
+ announcements: I("행", () => K),
389
+ screenReaderInstructions: Se
390
+ },
391
+ onDragOver: Z.onDragOver,
392
+ onDragEnd: Z.onDragEnd,
393
+ onDragCancel: Z.onDragCancel,
394
+ children: /* @__PURE__ */ x(pe, {
395
+ items: Ie ? K : [],
396
+ strategy: _e,
397
+ children: /* @__PURE__ */ x(d, { children: $ })
398
+ })
399
+ })]
400
+ })
401
+ }), T && !w && (b || G.length > 0) && /* @__PURE__ */ x(ne, {
402
+ page: V,
403
+ pageCount: je,
404
+ onPageChange: R,
405
+ pageSize: z,
406
+ onPageSizeChange: (e) => {
407
+ Ae(e), V !== 1 && R(1);
408
+ },
409
+ pageSizeOptions: T.pageSizeOptions,
410
+ showFirstLast: T.showFirstLast
411
+ })]
412
+ });
413
+ }
414
+ //#endregion
415
+ export { ke as DataGrid };
@@ -0,0 +1,18 @@
1
+ import * as React from 'react';
2
+ export interface PaginationProps {
3
+ /** 현재 페이지 (1부터) */
4
+ page: number;
5
+ pageCount: number;
6
+ onPageChange: (page: number) => void;
7
+ /** onPageSizeChange를 넘기면 왼쪽에 페이지당 행 수 Select가 생겨요 */
8
+ pageSize?: number;
9
+ onPageSizeChange?: (pageSize: number) => void;
10
+ pageSizeOptions?: number[];
11
+ /** 처음·마지막 버튼 */
12
+ showFirstLast?: boolean;
13
+ 'aria-label'?: string;
14
+ className?: string;
15
+ }
16
+ /** 처음·마지막 + 현재 양옆 1개, 나머지는 줄임표. 항상 최대 7칸이라 너비가 흔들리지 않아요 */
17
+ export declare function getPageItems(page: number, pageCount: number): Array<number | 'ellipsis'>;
18
+ export declare function Pagination({ page, pageCount, onPageChange, pageSize, onPageSizeChange, pageSizeOptions, showFirstLast, 'aria-label': ariaLabel, className, }: PaginationProps): React.JSX.Element;
@@ -0,0 +1,101 @@
1
+ "use client";
2
+ import { cn as e } from "../lib/cn.js";
3
+ import { Button as t } from "./button.js";
4
+ import { Select as n } from "./select.js";
5
+ import "react";
6
+ import { ChevronLeft as r, ChevronRight as i, ChevronsLeft as a, ChevronsRight as o } from "lucide-react";
7
+ import { jsx as s, jsxs as c } from "react/jsx-runtime";
8
+ //#region src/components/pagination.tsx
9
+ function l(e, t) {
10
+ return t <= 7 ? Array.from({ length: t }, (e, t) => t + 1) : e <= 4 ? [
11
+ 1,
12
+ 2,
13
+ 3,
14
+ 4,
15
+ 5,
16
+ "ellipsis",
17
+ t
18
+ ] : e >= t - 3 ? [
19
+ 1,
20
+ "ellipsis",
21
+ ...Array.from({ length: 5 }, (e, n) => t - 4 + n)
22
+ ] : [
23
+ 1,
24
+ "ellipsis",
25
+ e - 1,
26
+ e,
27
+ e + 1,
28
+ "ellipsis",
29
+ t
30
+ ];
31
+ }
32
+ var u = "sn:box-border sn:flex sn:h-7 sn:min-w-7 sn:items-center sn:justify-center sn:rounded-md sn:border-0 sn:px-1 sn:font-mono sn:text-body";
33
+ function d({ page: d, pageCount: f, onPageChange: p, pageSize: m, onPageSizeChange: h, pageSizeOptions: g = [
34
+ 10,
35
+ 20,
36
+ 50
37
+ ], showFirstLast: _ = !0, "aria-label": v = "페이지", className: y }) {
38
+ let b = Math.max(1, f), x = Math.min(Math.max(1, d), b), S = (e) => e !== x && p(e), C = (e, n, r, i) => /* @__PURE__ */ s(t, {
39
+ size: "icon-sm",
40
+ variant: "ghost",
41
+ "aria-label": e,
42
+ disabled: r,
43
+ focusableWhenDisabled: !0,
44
+ onClick: () => S(n),
45
+ children: /* @__PURE__ */ s(i, {})
46
+ });
47
+ return /* @__PURE__ */ c("div", {
48
+ className: e("sn:flex sn:items-center sn:justify-between sn:gap-4 sn:font-sans", y),
49
+ children: [h && /* @__PURE__ */ c("div", {
50
+ className: "sn:flex sn:items-center sn:gap-2",
51
+ children: [/* @__PURE__ */ s("span", {
52
+ "aria-hidden": !0,
53
+ className: "sn:hidden sn:text-label sn:text-muted sn:sm:inline",
54
+ children: "페이지당 행"
55
+ }), /* @__PURE__ */ s(n, {
56
+ "aria-label": "페이지당 행",
57
+ className: "sn:w-auto",
58
+ options: g.map((e) => ({
59
+ value: String(e),
60
+ label: String(e)
61
+ })),
62
+ value: m === void 0 ? null : String(m),
63
+ onValueChange: (e) => e && h(Number(e))
64
+ })]
65
+ }), /* @__PURE__ */ c("nav", {
66
+ "aria-label": v,
67
+ className: "sn:ml-auto sn:flex sn:items-center sn:gap-1",
68
+ children: [
69
+ _ && C("처음 페이지", 1, x <= 1, a),
70
+ C("이전 페이지", x - 1, x <= 1, r),
71
+ /* @__PURE__ */ s("ul", {
72
+ className: "sn:m-0 sn:hidden sn:list-none sn:items-center sn:gap-1 sn:p-0 sn:sm:flex",
73
+ children: l(x, b).map((t, n) => t === "ellipsis" ? /* @__PURE__ */ s("li", {
74
+ "aria-hidden": !0,
75
+ className: e(u, "sn:text-muted"),
76
+ children: "…"
77
+ }, `e${n}`) : /* @__PURE__ */ s("li", { children: /* @__PURE__ */ s("button", {
78
+ type: "button",
79
+ "aria-label": `${t}페이지`,
80
+ "aria-current": t === x ? "page" : void 0,
81
+ onClick: () => S(t),
82
+ className: e(u, "sn:cursor-pointer sn:transition-colors sn:duration-(--sn-duration-fast) sn:ease-standard sn:motion-reduce:transition-none", "sn:focus-visible:outline-2 sn:focus-visible:outline-offset-2 sn:focus-visible:outline-focus", t === x ? "sn:bg-subtle sn:text-default" : "sn:bg-transparent sn:text-muted sn:hover:bg-subtle sn:hover:text-default"),
83
+ children: t
84
+ }) }, t))
85
+ }),
86
+ /* @__PURE__ */ c("span", {
87
+ className: "sn:px-1 sn:font-mono sn:text-caption sn:text-muted sn:sm:hidden",
88
+ children: [
89
+ x,
90
+ " / ",
91
+ b
92
+ ]
93
+ }),
94
+ C("다음 페이지", x + 1, x >= b, i),
95
+ _ && C("마지막 페이지", b, x >= b, o)
96
+ ]
97
+ })]
98
+ });
99
+ }
100
+ //#endregion
101
+ export { d as Pagination, l as getPageItems };
@@ -1,4 +1,6 @@
1
1
  import * as React from 'react';
2
+ type Pinned = 'left' | 'right';
3
+ export declare const tableClass = "sn:w-full sn:border-separate sn:border-spacing-0 sn:font-sans sn:text-body sn:text-default";
2
4
  /** 넓으면 가로로 스크롤해요 */
3
5
  export declare const Table: React.ForwardRefExoticComponent<Omit<React.HTMLAttributes<HTMLTableElement>, "className"> & {
4
6
  className?: string;
@@ -11,14 +13,29 @@ export declare const TableBody: React.ForwardRefExoticComponent<Omit<React.HTMLA
11
13
  } & React.RefAttributes<HTMLTableSectionElement>>;
12
14
  export declare const TableRow: React.ForwardRefExoticComponent<Omit<React.HTMLAttributes<HTMLTableRowElement>, "className"> & {
13
15
  className?: string;
16
+ } & {
17
+ /** 선택된 행 (bg/selected) */
18
+ selected?: boolean;
19
+ /** hover 바탕을 보여 줘요 (DataGrid처럼 행을 고르거나 옮길 때) */
20
+ interactive?: boolean;
21
+ /** 옮기는 중인 행 */
22
+ dragging?: boolean;
14
23
  } & React.RefAttributes<HTMLTableRowElement>>;
15
- export declare const TableHead: React.ForwardRefExoticComponent<Omit<React.TdHTMLAttributes<HTMLTableCellElement>, "align" | "className"> & {
16
- /** 숫자 열은 end */
24
+ export declare const TableHead: React.ForwardRefExoticComponent<Omit<React.ThHTMLAttributes<HTMLTableCellElement>, "align" | "className"> & {
17
25
  align?: 'start' | 'end';
26
+ pinned?: Pinned;
27
+ /** 정렬 표시. onSort를 넘기면 머리글이 버튼이 돼요 */
28
+ sort?: 'asc' | 'desc' | 'none';
29
+ onSort?: () => void;
30
+ /** 컬럼 이동 손잡이. hover·포커스 때만 보이고, 정렬 반대쪽 끝에 놓여요 */
31
+ handle?: React.ReactNode;
18
32
  className?: string;
19
33
  } & React.RefAttributes<HTMLTableCellElement>>;
20
34
  export declare const TableCell: React.ForwardRefExoticComponent<Omit<React.TdHTMLAttributes<HTMLTableCellElement>, "align" | "className"> & {
21
35
  /** 숫자 열은 end */
22
36
  align?: 'start' | 'end';
37
+ /** 가로 스크롤 중에도 붙어 있어요. 여러 열을 고정하면 style.left·right로 위치를 넘겨요 */
38
+ pinned?: Pinned;
23
39
  className?: string;
24
40
  } & React.RefAttributes<HTMLTableCellElement>>;
41
+ export {};
@@ -1,46 +1,70 @@
1
1
  "use client";
2
2
  import { cn as e } from "../lib/cn.js";
3
3
  import * as t from "react";
4
- import { jsx as n } from "react/jsx-runtime";
4
+ import { ArrowDown as n, ArrowUp as r } from "lucide-react";
5
+ import { Fragment as i, jsx as a, jsxs as o } from "react/jsx-runtime";
5
6
  //#region src/components/table.tsx
6
- var r = t.forwardRef(function({ className: t, ...r }, i) {
7
- return /* @__PURE__ */ n("div", {
7
+ var s = "sn:w-full sn:border-separate sn:border-spacing-0 sn:font-sans sn:text-body sn:text-default", c = t.forwardRef(function({ className: t, ...n }, r) {
8
+ return /* @__PURE__ */ a("div", {
8
9
  className: "sn:w-full sn:overflow-x-auto",
9
- children: /* @__PURE__ */ n("table", {
10
- ref: i,
11
- className: e("sn:w-full sn:border-separate sn:border-spacing-0 sn:font-sans sn:text-body sn:text-default", t),
12
- ...r
10
+ children: /* @__PURE__ */ a("table", {
11
+ ref: r,
12
+ className: e(s, t),
13
+ ...n
13
14
  })
14
15
  });
15
- }), i = t.forwardRef(function(e, t) {
16
- return /* @__PURE__ */ n("thead", {
16
+ }), l = t.forwardRef(function(e, t) {
17
+ return /* @__PURE__ */ a("thead", {
17
18
  ref: t,
18
19
  ...e
19
20
  });
20
- }), a = t.forwardRef(function(e, t) {
21
- return /* @__PURE__ */ n("tbody", {
21
+ }), u = t.forwardRef(function(e, t) {
22
+ return /* @__PURE__ */ a("tbody", {
22
23
  ref: t,
23
24
  ...e
24
25
  });
25
- }), o = t.forwardRef(function({ className: e, ...t }, r) {
26
- return /* @__PURE__ */ n("tr", {
27
- ref: r,
28
- className: e,
29
- ...t
26
+ }), d = t.forwardRef(function({ selected: t, interactive: n, dragging: r, className: i, ...o }, s) {
27
+ return /* @__PURE__ */ a("tr", {
28
+ ref: s,
29
+ "data-selected": t ? "" : void 0,
30
+ "data-interactive": n ? "" : void 0,
31
+ "data-dragging": r ? "" : void 0,
32
+ className: e("sn:group/row sn:data-interactive:hover:bg-subtle sn:data-selected:bg-selected", "sn:data-dragging:bg-surface sn:data-dragging:outline sn:data-dragging:outline-1 sn:data-dragging:-outline-offset-1 sn:data-dragging:outline-default", i),
33
+ ...o
30
34
  });
31
- }), s = "sn:box-border sn:h-10 sn:border-b sn:border-default sn:px-3 sn:py-0 sn:align-middle", c = t.forwardRef(function({ align: t = "start", className: r, ...i }, a) {
32
- return /* @__PURE__ */ n("th", {
33
- ref: a,
35
+ }), f = "sn:box-border sn:h-10 sn:border-b sn:border-default sn:px-3 sn:py-0 sn:align-middle", p = {
36
+ left: "sn:sticky sn:left-0 sn:z-10 sn:border-r sn:border-r-default sn:bg-surface",
37
+ right: "sn:sticky sn:right-0 sn:z-10 sn:border-l sn:border-l-default sn:bg-surface"
38
+ }, m = "sn:group-data-interactive/row:group-hover/row:bg-subtle sn:group-data-selected/row:bg-selected", h = t.forwardRef(function({ align: t = "start", sort: s, onSort: c, handle: l, pinned: u, className: d, children: m, ...h }, g) {
39
+ let _ = s === "asc" || s === "desc", v = _ ? /* @__PURE__ */ o(i, { children: [m, /* @__PURE__ */ a(s === "asc" ? r : n, {
40
+ className: "sn:size-4 sn:shrink-0",
41
+ "aria-hidden": !0
42
+ })] }) : m, y = e("sn:flex sn:h-10 sn:w-full sn:items-center sn:gap-1 sn:px-3", t === "end" && "sn:justify-end");
43
+ return /* @__PURE__ */ o("th", {
44
+ ref: g,
34
45
  scope: "col",
35
- className: e(s, "sn:text-label sn:text-muted", t === "end" ? "sn:text-right" : "sn:text-left", r),
36
- ...i
46
+ "aria-sort": s === "asc" ? "ascending" : s === "desc" ? "descending" : s === "none" ? "none" : void 0,
47
+ className: e(f, !!l && "sn:group/head sn:relative", "sn:text-label", _ ? "sn:text-default" : "sn:text-muted", t === "end" ? "sn:text-right" : "sn:text-left", u && p[u], (c || _) && "sn:px-0", d),
48
+ ...h,
49
+ children: [c ? /* @__PURE__ */ a("button", {
50
+ type: "button",
51
+ onClick: c,
52
+ className: e(y, "sn:cursor-pointer sn:border-0 sn:bg-transparent sn:[font:inherit] sn:text-inherit sn:hover:text-default", "sn:focus-visible:outline-2 sn:focus-visible:-outline-offset-2 sn:focus-visible:outline-focus"),
53
+ children: v
54
+ }) : _ ? /* @__PURE__ */ a("span", {
55
+ className: y,
56
+ children: v
57
+ }) : m, l ? /* @__PURE__ */ a("span", {
58
+ className: e("sn:absolute sn:inset-y-0 sn:flex sn:items-center sn:opacity-0 sn:group-hover/head:opacity-100 sn:focus-within:opacity-100", t === "end" ? "sn:left-1" : "sn:right-1"),
59
+ children: l
60
+ }) : null]
37
61
  });
38
- }), l = t.forwardRef(function({ align: t = "start", className: r, ...i }, a) {
39
- return /* @__PURE__ */ n("td", {
40
- ref: a,
41
- className: e(s, t === "end" ? "sn:text-right sn:font-mono" : "sn:text-left", r),
62
+ }), g = t.forwardRef(function({ align: t = "start", pinned: n, className: r, ...i }, o) {
63
+ return /* @__PURE__ */ a("td", {
64
+ ref: o,
65
+ className: e(f, t === "end" ? "sn:text-right sn:font-mono" : "sn:text-left", n && [p[n], m].join(" "), r),
42
66
  ...i
43
67
  });
44
68
  });
45
69
  //#endregion
46
- export { r as Table, a as TableBody, l as TableCell, c as TableHead, i as TableHeader, o as TableRow };
70
+ export { c as Table, u as TableBody, g as TableCell, h as TableHead, l as TableHeader, d as TableRow, s as tableClass };
package/dist/index.d.ts CHANGED
@@ -27,6 +27,9 @@ export { Badge, type BadgeProps } from './components/badge';
27
27
  export { Progress, type ProgressProps } from './components/progress';
28
28
  export { Link, type LinkProps } from './components/link';
29
29
  export { Table, TableHeader, TableBody, TableRow, TableHead, TableCell } from './components/table';
30
+ export { DataGrid, type DataGridProps, type DataGridColumn, type DataGridAction, type DataGridSort, type DataGridPagination } from './components/data-grid';
31
+ export { Pagination, type PaginationProps } from './components/pagination';
32
+ export { formatNumber, formatDate, formatDateTime } from './lib/format';
30
33
  export { ToggleGroup, ToggleGroupItem, type ToggleGroupProps, type ToggleGroupItemProps } from './components/toggle-group';
31
34
  export { Accordion, AccordionItem, AccordionTrigger, AccordionPanel, type AccordionProps, type AccordionItemProps } from './components/accordion';
32
35
  export { FileDropzone, FileList, formatBytes, type FileDropzoneProps, type FileListProps, type FileItemData, type FileRejection } from './components/file-upload';
package/dist/index.js CHANGED
@@ -27,7 +27,10 @@ import { Badge as re } from "./components/badge.js";
27
27
  import { Progress as ie } from "./components/progress.js";
28
28
  import { Link as ae } from "./components/link.js";
29
29
  import { Table as oe, TableBody as se, TableCell as ce, TableHead as le, TableHeader as ue, TableRow as de } from "./components/table.js";
30
- import { ToggleGroup as fe, ToggleGroupItem as pe } from "./components/toggle-group.js";
31
- import { Accordion as me, AccordionItem as he, AccordionPanel as ge, AccordionTrigger as _e } from "./components/accordion.js";
32
- import { FileDropzone as ve, FileList as ye, formatBytes as be } from "./components/file-upload.js";
33
- export { me as Accordion, he as AccordionItem, ge as AccordionPanel, _e as AccordionTrigger, j as AlertDialog, M as AlertDialogClose, N as AlertDialogDescription, P as AlertDialogFooter, F as AlertDialogPopup, I as AlertDialogTitle, L as AlertDialogTrigger, _ as Avatar, re as Badge, t as Button, d as Card, f as CardBody, p as CardDescription, m as CardFooter, h as CardHeader, g as CardTitle, i as Checkbox, a as CheckboxGroup, w as Dialog, T as DialogClose, E as DialogDescription, D as DialogFooter, O as DialogPopup, k as DialogTitle, A as DialogTrigger, x as EmptyState, C as ErrorPage, S as ErrorState, v as Field, ve as FileDropzone, ye as FileList, n as Input, ae as Link, R as Menu, z as MenuItem, B as MenuPopup, V as MenuSeparator, H as MenuTrigger, U as Popover, W as PopoverClose, G as PopoverDescription, K as PopoverPopup, q as PopoverTitle, J as PopoverTrigger, ie as Progress, o as Radio, s as RadioGroup, l as Select, ee as Separator, y as Skeleton, c as Switch, X as Tab, oe as Table, se as TableBody, ce as TableCell, le as TableHead, ue as TableHeader, de as TableRow, Z as Tabs, Q as TabsList, $ as TabsPanel, u as Text, r as Textarea, te as ToastProvider, fe as ToggleGroup, pe as ToggleGroupItem, Y as Tooltip, e as buttonVariants, be as formatBytes, b as illustrations, ne as useToast };
30
+ import { formatDate as fe, formatDateTime as pe, formatNumber as me } from "./lib/format.js";
31
+ import { Pagination as he } from "./components/pagination.js";
32
+ import { DataGrid as ge } from "./components/data-grid.js";
33
+ import { ToggleGroup as _e, ToggleGroupItem as ve } from "./components/toggle-group.js";
34
+ import { Accordion as ye, AccordionItem as be, AccordionPanel as xe, AccordionTrigger as Se } from "./components/accordion.js";
35
+ import { FileDropzone as Ce, FileList as we, formatBytes as Te } from "./components/file-upload.js";
36
+ export { ye as Accordion, be as AccordionItem, xe as AccordionPanel, Se as AccordionTrigger, j as AlertDialog, M as AlertDialogClose, N as AlertDialogDescription, P as AlertDialogFooter, F as AlertDialogPopup, I as AlertDialogTitle, L as AlertDialogTrigger, _ as Avatar, re as Badge, t as Button, d as Card, f as CardBody, p as CardDescription, m as CardFooter, h as CardHeader, g as CardTitle, i as Checkbox, a as CheckboxGroup, ge as DataGrid, w as Dialog, T as DialogClose, E as DialogDescription, D as DialogFooter, O as DialogPopup, k as DialogTitle, A as DialogTrigger, x as EmptyState, C as ErrorPage, S as ErrorState, v as Field, Ce as FileDropzone, we as FileList, n as Input, ae as Link, R as Menu, z as MenuItem, B as MenuPopup, V as MenuSeparator, H as MenuTrigger, he as Pagination, U as Popover, W as PopoverClose, G as PopoverDescription, K as PopoverPopup, q as PopoverTitle, J as PopoverTrigger, ie as Progress, o as Radio, s as RadioGroup, l as Select, ee as Separator, y as Skeleton, c as Switch, X as Tab, oe as Table, se as TableBody, ce as TableCell, le as TableHead, ue as TableHeader, de as TableRow, Z as Tabs, Q as TabsList, $ as TabsPanel, u as Text, r as Textarea, te as ToastProvider, _e as ToggleGroup, ve as ToggleGroupItem, Y as Tooltip, e as buttonVariants, Te as formatBytes, fe as formatDate, pe as formatDateTime, me as formatNumber, b as illustrations, ne as useToast };
@@ -0,0 +1,8 @@
1
+ type DateInput = string | number | Date | null | undefined;
2
+ /** 12480 → "12,480" */
3
+ export declare function formatNumber(value: number | null | undefined): string;
4
+ /** YYYY-MM-DD (브라우저 현지 시간) */
5
+ export declare function formatDate(value: DateInput): string;
6
+ /** YYYY-MM-DD HH:mm:ss (브라우저 현지 시간) */
7
+ export declare function formatDateTime(value: DateInput): string;
8
+ export {};
@@ -0,0 +1,20 @@
1
+ //#region src/lib/format.ts
2
+ var e = new Intl.NumberFormat("ko-KR"), t = (e) => String(e).padStart(2, "0"), n = (e) => {
3
+ if (e == null || e === "") return null;
4
+ let t = e instanceof Date ? e : new Date(e);
5
+ return Number.isNaN(t.getTime()) ? null : t;
6
+ };
7
+ function r(t) {
8
+ return t == null || Number.isNaN(t) ? "" : e.format(t);
9
+ }
10
+ function i(e) {
11
+ if (typeof e == "string" && /^\d{4}-\d{2}-\d{2}$/.test(e)) return e;
12
+ let r = n(e);
13
+ return r ? `${r.getFullYear()}-${t(r.getMonth() + 1)}-${t(r.getDate())}` : "";
14
+ }
15
+ function a(e) {
16
+ let r = n(e);
17
+ return r ? `${i(r)} ${t(r.getHours())}:${t(r.getMinutes())}:${t(r.getSeconds())}` : "";
18
+ }
19
+ //#endregion
20
+ export { i as formatDate, a as formatDateTime, r as formatNumber };
package/dist/styles.css CHANGED
@@ -1,3 +1,3 @@
1
1
  /*! sonic-ui | 전체가 @layer sonic 안에 있어요. 쓰는 쪽: @layer theme, base, sonic, components, utilities; */
2
- @property --tw-border-spacing-x{syntax:"<length>";inherits:false;initial-value:0}@property --tw-border-spacing-y{syntax:"<length>";inherits:false;initial-value:0}@property --tw-translate-x{syntax:"*";inherits:false;initial-value:0}@property --tw-translate-y{syntax:"*";inherits:false;initial-value:0}@property --tw-translate-z{syntax:"*";inherits:false;initial-value:0}@property --tw-border-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-ordinal{syntax:"*";inherits:false}@property --tw-slashed-zero{syntax:"*";inherits:false}@property --tw-numeric-figure{syntax:"*";inherits:false}@property --tw-numeric-spacing{syntax:"*";inherits:false}@property --tw-numeric-fraction{syntax:"*";inherits:false}@property --tw-duration{syntax:"*";inherits:false}@property --tw-ease{syntax:"*";inherits:false}@property --tw-outline-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-scale-x{syntax:"*";inherits:false;initial-value:1}@property --tw-scale-y{syntax:"*";inherits:false;initial-value:1}@property --tw-scale-z{syntax:"*";inherits:false;initial-value:1}@layer sonic{/*! tailwindcss v4.3.3 | MIT License | https://tailwindcss.com */
3
- @layer properties{@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-border-spacing-x:0;--tw-border-spacing-y:0;--tw-translate-x:0;--tw-translate-y:0;--tw-translate-z:0;--tw-border-style:solid;--tw-ordinal:initial;--tw-slashed-zero:initial;--tw-numeric-figure:initial;--tw-numeric-spacing:initial;--tw-numeric-fraction:initial;--tw-duration:initial;--tw-ease:initial;--tw-outline-style:solid;--tw-scale-x:1;--tw-scale-y:1;--tw-scale-z:1}}}@layer utilities{.sn\:sr-only{clip-path:inset(50%);white-space:nowrap;border-width:0;width:1px;height:1px;margin:-1px;padding:0;position:absolute;overflow:hidden}.sn\:absolute{position:absolute}.sn\:fixed{position:fixed}.sn\:relative{position:relative}.sn\:inset-0{inset:0}.sn\:top-1\/2{top:50%}.sn\:right-4{right:calc(var(--sn-space-1) * 4)}.sn\:bottom-4{bottom:calc(var(--sn-space-1) * 4)}.sn\:bottom-\[-1px\]{bottom:-1px}.sn\:left-0{left:0}.sn\:left-1\/2{left:50%}.sn\:z-50{z-index:50}.sn\:m-0{margin:0}.sn\:my-1{margin-block:var(--sn-space-1)}.sn\:-mt-2{margin-top:calc(var(--sn-space-1) * -2)}.sn\:-mt-3{margin-top:calc(var(--sn-space-1) * -3)}.sn\:mt-2{margin-top:calc(var(--sn-space-1) * 2)}.sn\:-mb-px{margin-bottom:-1px}.sn\:mb-0{margin-bottom:0}.sn\:ml-1{margin-left:var(--sn-space-1)}.sn\:box-border{box-sizing:border-box}.sn\:block{display:block}.sn\:flex{display:flex}.sn\:hidden{display:none}.sn\:inline-block{display:inline-block}.sn\:inline-flex{display:inline-flex}.sn\:size-2{width:calc(var(--sn-space-1) * 2);height:calc(var(--sn-space-1) * 2)}.sn\:size-3{width:calc(var(--sn-space-1) * 3);height:calc(var(--sn-space-1) * 3)}.sn\:size-4{width:calc(var(--sn-space-1) * 4);height:calc(var(--sn-space-1) * 4)}.sn\:size-6{width:calc(var(--sn-space-1) * 6);height:calc(var(--sn-space-1) * 6)}.sn\:size-7{width:calc(var(--sn-space-1) * 7);height:calc(var(--sn-space-1) * 7)}.sn\:size-8{width:calc(var(--sn-space-1) * 8);height:calc(var(--sn-space-1) * 8)}.sn\:size-10{width:calc(var(--sn-space-1) * 10);height:calc(var(--sn-space-1) * 10)}.sn\:size-12{width:calc(var(--sn-space-1) * 12);height:calc(var(--sn-space-1) * 12)}.sn\:size-24{width:calc(var(--sn-space-1) * 24);height:calc(var(--sn-space-1) * 24)}.sn\:size-full{width:100%;height:100%}.sn\:h-\(--accordion-panel-height\){height:var(--accordion-panel-height)}.sn\:h-0\.5{height:calc(var(--sn-space-1) * .5)}.sn\:h-1{height:var(--sn-space-1)}.sn\:h-3{height:calc(var(--sn-space-1) * 3)}.sn\:h-4{height:calc(var(--sn-space-1) * 4)}.sn\:h-5{height:calc(var(--sn-space-1) * 5)}.sn\:h-6{height:calc(var(--sn-space-1) * 6)}.sn\:h-7{height:calc(var(--sn-space-1) * 7)}.sn\:h-8{height:calc(var(--sn-space-1) * 8)}.sn\:h-10{height:calc(var(--sn-space-1) * 10)}.sn\:h-16{height:calc(var(--sn-space-1) * 16)}.sn\:h-auto{height:auto}.sn\:h-full{height:100%}.sn\:h-px{height:1px}.sn\:max-h-\(--available-height\){max-height:var(--available-height)}.sn\:max-h-\[calc\(100dvh-2\*var\(--sn-space-4\)\)\]{max-height:calc(100dvh - 2 * var(--sn-space-4))}.sn\:min-h-20{min-height:calc(var(--sn-space-1) * 20)}.sn\:w-\(--active-tab-width\){width:var(--active-tab-width)}.sn\:w-6{width:calc(var(--sn-space-1) * 6)}.sn\:w-7{width:calc(var(--sn-space-1) * 7)}.sn\:w-24{width:calc(var(--sn-space-1) * 24)}.sn\:w-80{width:calc(var(--sn-space-1) * 80)}.sn\:w-90{width:calc(var(--sn-space-1) * 90)}.sn\:w-110{width:calc(var(--sn-space-1) * 110)}.sn\:w-full{width:100%}.sn\:w-max{width:max-content}.sn\:w-px{width:1px}.sn\:max-w-\[calc\(100vw-2\*var\(--sn-space-4\)\)\]{max-width:calc(100vw - 2 * var(--sn-space-4))}.sn\:max-w-full{max-width:100%}.sn\:min-w-\(--anchor-width\){min-width:var(--anchor-width)}.sn\:min-w-0{min-width:0}.sn\:min-w-48{min-width:calc(var(--sn-space-1) * 48)}.sn\:min-w-full{min-width:100%}.sn\:flex-1{flex:1}.sn\:shrink-0{flex-shrink:0}.sn\:border-separate{border-collapse:separate}.sn\:border-spacing-0{--tw-border-spacing-x:0px;--tw-border-spacing-y:0px;border-spacing:var(--tw-border-spacing-x) var(--tw-border-spacing-y)}.sn\:origin-\(--transform-origin\){transform-origin:var(--transform-origin)}.sn\:-translate-x-1\/2{--tw-translate-x:calc(calc(1 / 2 * 100%) * -1);translate:var(--tw-translate-x) var(--tw-translate-y)}.sn\:translate-x-\(--active-tab-left\){--tw-translate-x:var(--active-tab-left);translate:var(--tw-translate-x) var(--tw-translate-y)}.sn\:-translate-y-1\/2{--tw-translate-y:calc(calc(1 / 2 * 100%) * -1);translate:var(--tw-translate-x) var(--tw-translate-y)}.sn\:animate-\[sn-pulse_var\(--sn-duration-loop\)_ease-in-out_infinite\]{animation:sn-pulse var(--sn-duration-loop) ease-in-out infinite}.sn\:animate-\[sn-spin_var\(--sn-duration-loop\)_linear_infinite\]{animation:sn-spin var(--sn-duration-loop) linear infinite}.sn\:cursor-default{cursor:default}.sn\:cursor-not-allowed{cursor:not-allowed}.sn\:cursor-pointer{cursor:pointer}.sn\:resize-y{resize:vertical}.sn\:\[scrollbar-width\:none\]{scrollbar-width:none}.sn\:list-none{list-style-type:none}.sn\:flex-col{flex-direction:column}.sn\:flex-col-reverse{flex-direction:column-reverse}.sn\:flex-wrap{flex-wrap:wrap}.sn\:items-center{align-items:center}.sn\:items-start{align-items:flex-start}.sn\:justify-between{justify-content:space-between}.sn\:justify-center{justify-content:center}.sn\:justify-end{justify-content:flex-end}.sn\:gap-1{gap:var(--sn-space-1)}.sn\:gap-2{gap:calc(var(--sn-space-1) * 2)}.sn\:gap-3{gap:calc(var(--sn-space-1) * 3)}.sn\:gap-4{gap:calc(var(--sn-space-1) * 4)}.sn\:gap-6{gap:calc(var(--sn-space-1) * 6)}.sn\:gap-10{gap:calc(var(--sn-space-1) * 10)}.sn\:self-stretch{align-self:stretch}.sn\:truncate{text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.sn\:overflow-hidden{overflow:hidden}.sn\:overflow-x-auto{overflow-x:auto}.sn\:overflow-y-auto{overflow-y:auto}.sn\:rounded-full{border-radius:var(--sn-radius-full)}.sn\:rounded-lg{border-radius:var(--sn-radius-lg)}.sn\:rounded-md{border-radius:var(--sn-radius-md)}.sn\:rounded-sm{border-radius:var(--sn-radius-sm)}.sn\:border{border-style:var(--tw-border-style);border-width:1px}.sn\:border-0{border-style:var(--tw-border-style);border-width:0}.sn\:border-b{border-bottom-style:var(--tw-border-style);border-bottom-width:1px}.sn\:border-dashed{--tw-border-style:dashed;border-style:dashed}.sn\:border-danger{border-color:var(--sn-color-border-danger)}.sn\:border-default{border-color:var(--sn-color-border-default)}.sn\:border-strong{border-color:var(--sn-color-border-strong)}.sn\:border-transparent{border-color:#0000}.sn\:bg-\(--sn-color-border-default\){background-color:var(--sn-color-border-default)}.sn\:bg-\(--sn-color-text-on-primary\){background-color:var(--sn-color-text-on-primary)}.sn\:bg-canvas{background-color:var(--sn-color-bg-canvas)}.sn\:bg-danger{background-color:var(--sn-color-bg-danger)}.sn\:bg-danger-subtle{background-color:var(--sn-color-bg-danger-subtle)}.sn\:bg-inverse{background-color:var(--sn-color-bg-inverse)}.sn\:bg-overlay{background-color:var(--sn-color-bg-overlay)}.sn\:bg-primary{background-color:var(--sn-color-bg-primary)}.sn\:bg-subtle{background-color:var(--sn-color-bg-subtle)}.sn\:bg-success{background-color:var(--sn-color-bg-success)}.sn\:bg-success-subtle{background-color:var(--sn-color-bg-success-subtle)}.sn\:bg-surface{background-color:var(--sn-color-bg-surface)}.sn\:bg-transparent{background-color:#0000}.sn\:bg-warning{background-color:var(--sn-color-bg-warning)}.sn\:bg-warning-subtle{background-color:var(--sn-color-bg-warning-subtle)}.sn\:object-cover{object-fit:cover}.sn\:p-0{padding:0}.sn\:p-1{padding:var(--sn-space-1)}.sn\:p-3{padding:calc(var(--sn-space-1) * 3)}.sn\:p-4{padding:calc(var(--sn-space-1) * 4)}.sn\:p-5{padding:calc(var(--sn-space-1) * 5)}.sn\:p-6{padding:calc(var(--sn-space-1) * 6)}.sn\:p-px{padding:1px}.sn\:px-0{padding-inline:0}.sn\:px-2{padding-inline:calc(var(--sn-space-1) * 2)}.sn\:px-3{padding-inline:calc(var(--sn-space-1) * 3)}.sn\:px-4{padding-inline:calc(var(--sn-space-1) * 4)}.sn\:px-6{padding-inline:calc(var(--sn-space-1) * 6)}.sn\:py-0{padding-block:0}.sn\:py-1{padding-block:var(--sn-space-1)}.sn\:py-2{padding-block:calc(var(--sn-space-1) * 2)}.sn\:py-3{padding-block:calc(var(--sn-space-1) * 3)}.sn\:py-6{padding-block:calc(var(--sn-space-1) * 6)}.sn\:py-12{padding-block:calc(var(--sn-space-1) * 12)}.sn\:py-16{padding-block:calc(var(--sn-space-1) * 16)}.sn\:pt-1{padding-top:var(--sn-space-1)}.sn\:pb-4{padding-bottom:calc(var(--sn-space-1) * 4)}.sn\:pb-px{padding-bottom:1px}.sn\:text-center{text-align:center}.sn\:text-left{text-align:left}.sn\:text-right{text-align:right}.sn\:align-middle{vertical-align:middle}.sn\:align-text-bottom{vertical-align:text-bottom}.sn\:font-mono{font-family:var(--sn-font-mono)}.sn\:font-sans{font-family:var(--sn-font-sans)}.sn\:text-body{font-size:var(--sn-text-body);line-height:var(--tw-leading,var(--sn-text-body--line-height));letter-spacing:var(--tw-tracking,var(--sn-text-body--letter-spacing));font-weight:var(--tw-font-weight,var(--sn-text-body--font-weight))}.sn\:text-body-strong{font-size:var(--sn-text-body-strong);line-height:var(--tw-leading,var(--sn-text-body-strong--line-height));letter-spacing:var(--tw-tracking,var(--sn-text-body-strong--letter-spacing));font-weight:var(--tw-font-weight,var(--sn-text-body-strong--font-weight))}.sn\:text-caption{font-size:var(--sn-text-caption);line-height:var(--tw-leading,var(--sn-text-caption--line-height));letter-spacing:var(--tw-tracking,var(--sn-text-caption--letter-spacing));font-weight:var(--tw-font-weight,var(--sn-text-caption--font-weight))}.sn\:text-display{font-size:var(--sn-text-display);line-height:var(--tw-leading,var(--sn-text-display--line-height));letter-spacing:var(--tw-tracking,var(--sn-text-display--letter-spacing));font-weight:var(--tw-font-weight,var(--sn-text-display--font-weight))}.sn\:text-label{font-size:var(--sn-text-label);line-height:var(--tw-leading,var(--sn-text-label--line-height));letter-spacing:var(--tw-tracking,var(--sn-text-label--letter-spacing));font-weight:var(--tw-font-weight,var(--sn-text-label--font-weight))}.sn\:text-title{font-size:var(--sn-text-title);line-height:var(--tw-leading,var(--sn-text-title--line-height));letter-spacing:var(--tw-tracking,var(--sn-text-title--letter-spacing));font-weight:var(--tw-font-weight,var(--sn-text-title--font-weight))}.sn\:whitespace-nowrap{white-space:nowrap}.sn\:text-danger{color:var(--sn-color-text-danger)}.sn\:text-default{color:var(--sn-color-text-default)}.sn\:text-inverse{color:var(--sn-color-text-inverse)}.sn\:text-muted{color:var(--sn-color-text-muted)}.sn\:text-on-danger{color:var(--sn-color-text-on-danger)}.sn\:text-on-primary{color:var(--sn-color-text-on-primary)}.sn\:text-on-success{color:var(--sn-color-text-on-success)}.sn\:text-on-warning{color:var(--sn-color-text-on-warning)}.sn\:text-success{color:var(--sn-color-text-success)}.sn\:text-warning{color:var(--sn-color-text-warning)}.sn\:tabular-nums{--tw-numeric-spacing:tabular-nums;font-variant-numeric:var(--tw-ordinal,) var(--tw-slashed-zero,) var(--tw-numeric-figure,) var(--tw-numeric-spacing,) var(--tw-numeric-fraction,)}.sn\:underline{text-decoration-line:underline}.sn\:underline-offset-2{text-underline-offset:2px}.sn\:opacity-\(--sn-opacity-disabled\){opacity:var(--sn-opacity-disabled)}.sn\:transition-\[height\]{transition-property:height;transition-timing-function:var(--tw-ease,ease);transition-duration:var(--tw-duration,0s)}.sn\:transition-\[opacity\,scale\]{transition-property:opacity,scale;transition-timing-function:var(--tw-ease,ease);transition-duration:var(--tw-duration,0s)}.sn\:transition-\[opacity\,translate\]{transition-property:opacity,translate;transition-timing-function:var(--tw-ease,ease);transition-duration:var(--tw-duration,0s)}.sn\:transition-\[translate\,width\]{transition-property:translate,width;transition-timing-function:var(--tw-ease,ease);transition-duration:var(--tw-duration,0s)}.sn\:transition-\[width\]{transition-property:width;transition-timing-function:var(--tw-ease,ease);transition-duration:var(--tw-duration,0s)}.sn\:transition-colors{transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,ease);transition-duration:var(--tw-duration,0s)}.sn\:transition-opacity{transition-property:opacity;transition-timing-function:var(--tw-ease,ease);transition-duration:var(--tw-duration,0s)}.sn\:transition-transform{transition-property:transform,translate,scale,rotate;transition-timing-function:var(--tw-ease,ease);transition-duration:var(--tw-duration,0s)}.sn\:duration-\(--sn-duration-fast\){--tw-duration:var(--sn-duration-fast);transition-duration:var(--sn-duration-fast)}.sn\:duration-\(--sn-duration-normal\){--tw-duration:var(--sn-duration-normal);transition-duration:var(--sn-duration-normal)}.sn\:ease-standard{--tw-ease:var(--sn-easing-standard);transition-timing-function:var(--sn-easing-standard)}.sn\:outline-none{--tw-outline-style:none;outline-style:none}.sn\:select-none{-webkit-user-select:none;user-select:none}.sn\:group-aria-invalid\:text-danger:is(:where(.sn\:group)[aria-invalid=true] *),.sn\:group-data-invalid\:text-danger:is(:where(.sn\:group)[data-invalid] *){color:var(--sn-color-text-danger)}.sn\:group-data-panel-open\:rotate-180:is(:where(.sn\:group)[data-panel-open] *),.sn\:group-data-popup-open\:rotate-180:is(:where(.sn\:group)[data-popup-open] *){rotate:180deg}.sn\:group-data-selected\:border-transparent:is(:where(.sn\:group)[data-selected] *){border-color:#0000}.sn\:group-data-selected\:bg-primary:is(:where(.sn\:group)[data-selected] *){background-color:var(--sn-color-bg-primary)}.sn\:placeholder\:text-muted::placeholder{color:var(--sn-color-text-muted)}@media (hover:hover){.sn\:hover\:text-muted:hover{color:var(--sn-color-text-muted)}.sn\:hover\:not-disabled\:bg-subtle:hover:not(:disabled){background-color:var(--sn-color-bg-subtle)}.sn\:hover\:not-data-disabled\:border-strong:hover:not([data-disabled]){border-color:var(--sn-color-border-strong)}.sn\:hover\:not-data-disabled\:bg-primary-hover:hover:not([data-disabled]){background-color:var(--sn-color-bg-primary-hover)}.sn\:hover\:not-data-disabled\:bg-subtle:hover:not([data-disabled]){background-color:var(--sn-color-bg-subtle)}.sn\:hover\:not-data-disabled\:text-default:hover:not([data-disabled]){color:var(--sn-color-text-default)}}.sn\:focus-visible\:outline-2:focus-visible{outline-style:var(--tw-outline-style);outline-width:2px}.sn\:focus-visible\:-outline-offset-2:focus-visible{outline-offset:calc(2px * -1)}.sn\:focus-visible\:outline-offset-2:focus-visible{outline-offset:2px}.sn\:focus-visible\:outline-focus:focus-visible{outline-color:var(--sn-color-border-focus)}.sn\:disabled\:cursor-not-allowed:disabled{cursor:not-allowed}.sn\:disabled\:opacity-\(--sn-opacity-disabled\):disabled{opacity:var(--sn-opacity-disabled)}.sn\:aria-invalid\:border-danger[aria-invalid=true]{border-color:var(--sn-color-border-danger)}@media (hover:hover){.sn\:hover\:aria-invalid\:border-danger:hover[aria-invalid=true]{border-color:var(--sn-color-border-danger)}}.sn\:aria-invalid\:focus-visible\:outline-danger[aria-invalid=true]:focus-visible{outline-color:var(--sn-color-border-danger)}.sn\:data-active\:text-default[data-active]{color:var(--sn-color-text-default)}.sn\:data-checked\:translate-x-3[data-checked]{--tw-translate-x:calc(var(--sn-space-1) * 3);translate:var(--tw-translate-x) var(--tw-translate-y)}.sn\:data-checked\:border-transparent[data-checked]{border-color:#0000}.sn\:data-checked\:bg-\(--sn-color-text-on-primary\)[data-checked]{background-color:var(--sn-color-text-on-primary)}.sn\:data-checked\:bg-primary[data-checked]{background-color:var(--sn-color-bg-primary)}.sn\:data-disabled\:cursor-not-allowed[data-disabled]{cursor:not-allowed}.sn\:data-disabled\:opacity-\(--sn-opacity-disabled\)[data-disabled]{opacity:var(--sn-opacity-disabled)}.sn\:data-disabled\:not-data-loading\:cursor-not-allowed[data-disabled]:not([data-loading]){cursor:not-allowed}.sn\:data-disabled\:not-data-loading\:opacity-\(--sn-opacity-disabled\)[data-disabled]:not([data-loading]){opacity:var(--sn-opacity-disabled)}.sn\:data-dragging\:border-focus[data-dragging]{border-color:var(--sn-color-border-focus)}.sn\:data-dragging\:bg-subtle[data-dragging]{background-color:var(--sn-color-bg-subtle)}.sn\:data-ending-style\:h-0[data-ending-style]{height:0}.sn\:data-ending-style\:translate-y-2[data-ending-style]{--tw-translate-y:calc(var(--sn-space-1) * 2);translate:var(--tw-translate-x) var(--tw-translate-y)}.sn\:data-ending-style\:scale-98[data-ending-style]{--tw-scale-x:98%;--tw-scale-y:98%;--tw-scale-z:98%;scale:var(--tw-scale-x) var(--tw-scale-y)}.sn\:data-ending-style\:opacity-0[data-ending-style]{opacity:0}.sn\:data-highlighted\:bg-subtle[data-highlighted]{background-color:var(--sn-color-bg-subtle)}.sn\:data-indeterminate\:border-transparent[data-indeterminate]{border-color:#0000}.sn\:data-indeterminate\:bg-primary[data-indeterminate]{background-color:var(--sn-color-bg-primary)}.sn\:data-invalid\:border-danger[data-invalid]{border-color:var(--sn-color-border-danger)}.sn\:data-invalid\:focus-visible\:outline-danger[data-invalid]:focus-visible{outline-color:var(--sn-color-border-danger)}.sn\:data-limited\:hidden[data-limited]{display:none}.sn\:data-loading\:cursor-progress[data-loading]{cursor:progress}.sn\:data-placeholder\:text-muted[data-placeholder]{color:var(--sn-color-text-muted)}.sn\:data-popup-open\:border-focus[data-popup-open]{border-color:var(--sn-color-border-focus)}.sn\:data-pressed\:border-default[data-pressed]{border-color:var(--sn-color-border-default)}.sn\:data-pressed\:bg-surface[data-pressed]{background-color:var(--sn-color-bg-surface)}.sn\:data-pressed\:text-default[data-pressed]{color:var(--sn-color-text-default)}.sn\:data-starting-style\:h-0[data-starting-style]{height:0}.sn\:data-starting-style\:translate-y-2[data-starting-style]{--tw-translate-y:calc(var(--sn-space-1) * 2);translate:var(--tw-translate-x) var(--tw-translate-y)}.sn\:data-starting-style\:scale-98[data-starting-style]{--tw-scale-x:98%;--tw-scale-y:98%;--tw-scale-z:98%;scale:var(--tw-scale-x) var(--tw-scale-y)}.sn\:data-starting-style\:opacity-0[data-starting-style]{opacity:0}@media (prefers-reduced-motion:reduce){.sn\:motion-reduce\:animate-none{animation:none}.sn\:motion-reduce\:transition-none{transition-property:none}}@media not all and (min-width:40rem){.sn\:max-sm\:flex-col-reverse{flex-direction:column-reverse}.sn\:max-sm\:text-\[16px\]{font-size:16px}:is(.sn\:max-sm\:\*\:h-10>*){height:calc(var(--sn-space-1) * 10)}:is(.sn\:max-sm\:\*\:w-full>*){width:100%}}@media (pointer:coarse){.sn\:pointer-coarse\:hidden{display:none}.sn\:pointer-coarse\:inline{display:inline}}.sn\:\[\&_svg\]\:pointer-events-none svg{pointer-events:none}.sn\:\[\&_svg\]\:size-3 svg{width:calc(var(--sn-space-1) * 3);height:calc(var(--sn-space-1) * 3)}.sn\:\[\&_svg\]\:size-4 svg{width:calc(var(--sn-space-1) * 4);height:calc(var(--sn-space-1) * 4)}.sn\:\[\&_svg\]\:size-5 svg{width:calc(var(--sn-space-1) * 5);height:calc(var(--sn-space-1) * 5)}.sn\:\[\&_svg\]\:size-6 svg{width:calc(var(--sn-space-1) * 6);height:calc(var(--sn-space-1) * 6)}.sn\:\[\&_svg\]\:shrink-0 svg{flex-shrink:0}}:root{--sn-space-0:0px;--sn-space-1:4px;--sn-space-2:8px;--sn-space-3:12px;--sn-space-4:16px;--sn-space-5:20px;--sn-space-6:24px;--sn-space-7:28px;--sn-space-8:32px;--sn-space-10:40px;--sn-space-12:48px;--sn-space-16:64px;--sn-radius-sm:2px;--sn-radius-md:4px;--sn-radius-lg:6px;--sn-radius-full:9999px;--sn-stroke-default:1px;--sn-stroke-focus:2px;--sn-duration-fast:.12s;--sn-duration-normal:.2s;--sn-easing-standard:cubic-bezier(.2, 0, 0, 1);--sn-duration-loop:1s;--sn-opacity-disabled:.45;--sn-font-sans:"IBM Plex Sans KR", ui-sans-serif, system-ui, sans-serif;--sn-font-mono:"IBM Plex Mono", ui-monospace, SFMono-Regular, Menlo, monospace;--sn-text-display:32px;--sn-text-display--line-height:40px;--sn-text-display--font-weight:600;--sn-text-display--letter-spacing:-.02em;--sn-text-title:20px;--sn-text-title--line-height:28px;--sn-text-title--font-weight:600;--sn-text-title--letter-spacing:-.01em;--sn-text-body:14px;--sn-text-body--line-height:20px;--sn-text-body--font-weight:400;--sn-text-body--letter-spacing:0em;--sn-text-body-strong:14px;--sn-text-body-strong--line-height:20px;--sn-text-body-strong--font-weight:500;--sn-text-body-strong--letter-spacing:0em;--sn-text-label:13px;--sn-text-label--line-height:16px;--sn-text-label--font-weight:500;--sn-text-label--letter-spacing:0em;--sn-text-caption:12px;--sn-text-caption--line-height:16px;--sn-text-caption--font-weight:400;--sn-text-caption--letter-spacing:0em;--sn-text-mono:14px;--sn-text-mono--line-height:20px;--sn-text-mono--font-weight:400;--sn-text-mono--letter-spacing:0em;--sn-text-mono-caption:12px;--sn-text-mono-caption--line-height:16px;--sn-text-mono-caption--font-weight:400;--sn-text-mono-caption--letter-spacing:0em}:root,[data-theme=light]{color-scheme:light;--sn-color-bg-canvas:#f4f6f8;--sn-color-bg-surface:#fff;--sn-color-bg-subtle:#e6eaee;--sn-color-bg-primary:#2340d0;--sn-color-bg-primary-hover:#1b33ae;--sn-color-bg-danger:#c8312b;--sn-color-bg-overlay:#0e121680;--sn-color-text-default:#0e1216;--sn-color-text-muted:#5b6672;--sn-color-text-on-primary:#fff;--sn-color-text-on-danger:#fff;--sn-color-text-danger:#c8312b;--sn-color-border-default:#d5dbe1;--sn-color-border-strong:#8d99a6;--sn-color-border-focus:#2340d0;--sn-color-border-danger:#c8312b;--sn-color-bg-inverse:#0e1216;--sn-color-text-inverse:#fff;--sn-color-bg-success:#16804f;--sn-color-text-on-success:#fff;--sn-color-bg-success-subtle:#eaf7f0;--sn-color-text-success:#0f6640;--sn-color-bg-warning:#e3a43a;--sn-color-text-on-warning:#0e1216;--sn-color-bg-warning-subtle:#fcf5e6;--sn-color-text-warning:#85520a;--sn-color-bg-danger-subtle:#fcedec;--sn-color-border-success:#16804f;--sn-color-border-warning:#a86c12}[data-theme=dark]{color-scheme:dark;--sn-color-bg-canvas:#0e1216;--sn-color-bg-surface:#161b21;--sn-color-bg-subtle:#1e252d;--sn-color-bg-primary:#6e86ff;--sn-color-bg-primary-hover:#8c9fff;--sn-color-bg-danger:#f06a5f;--sn-color-bg-overlay:#0e121680;--sn-color-text-default:#e6eaee;--sn-color-text-muted:#8d99a6;--sn-color-text-on-primary:#0e1216;--sn-color-text-on-danger:#0e1216;--sn-color-text-danger:#f06a5f;--sn-color-border-default:#2a323b;--sn-color-border-strong:#5b6672;--sn-color-border-focus:#6e86ff;--sn-color-border-danger:#f06a5f;--sn-color-bg-inverse:#e6eaee;--sn-color-text-inverse:#0e1216;--sn-color-bg-success:#3db882;--sn-color-text-on-success:#0e1216;--sn-color-bg-success-subtle:#0b2218;--sn-color-text-success:#6bd1a0;--sn-color-bg-warning:#e3a43a;--sn-color-text-on-warning:#0e1216;--sn-color-bg-warning-subtle:#24190a;--sn-color-text-warning:#eec46a;--sn-color-bg-danger-subtle:#2b1311;--sn-color-border-success:#3db882;--sn-color-border-warning:#e3a43a}@keyframes sn-pulse{50%{opacity:.5}}@keyframes sn-spin{to{transform:rotate(360deg)}}}
2
+ @property --tw-border-spacing-x{syntax:"<length>";inherits:false;initial-value:0}@property --tw-border-spacing-y{syntax:"<length>";inherits:false;initial-value:0}@property --tw-translate-x{syntax:"*";inherits:false;initial-value:0}@property --tw-translate-y{syntax:"*";inherits:false;initial-value:0}@property --tw-translate-z{syntax:"*";inherits:false;initial-value:0}@property --tw-border-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-ordinal{syntax:"*";inherits:false}@property --tw-slashed-zero{syntax:"*";inherits:false}@property --tw-numeric-figure{syntax:"*";inherits:false}@property --tw-numeric-spacing{syntax:"*";inherits:false}@property --tw-numeric-fraction{syntax:"*";inherits:false}@property --tw-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-shadow-color{syntax:"*";inherits:false}@property --tw-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-inset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-shadow-color{syntax:"*";inherits:false}@property --tw-inset-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-ring-color{syntax:"*";inherits:false}@property --tw-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-ring-color{syntax:"*";inherits:false}@property --tw-inset-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-ring-inset{syntax:"*";inherits:false}@property --tw-ring-offset-width{syntax:"<length>";inherits:false;initial-value:0}@property --tw-ring-offset-color{syntax:"*";inherits:false;initial-value:#fff}@property --tw-ring-offset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-duration{syntax:"*";inherits:false}@property --tw-ease{syntax:"*";inherits:false}@property --tw-outline-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-scale-x{syntax:"*";inherits:false;initial-value:1}@property --tw-scale-y{syntax:"*";inherits:false;initial-value:1}@property --tw-scale-z{syntax:"*";inherits:false;initial-value:1}@layer sonic{/*! tailwindcss v4.3.3 | MIT License | https://tailwindcss.com */
3
+ @layer properties{@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-border-spacing-x:0;--tw-border-spacing-y:0;--tw-translate-x:0;--tw-translate-y:0;--tw-translate-z:0;--tw-border-style:solid;--tw-ordinal:initial;--tw-slashed-zero:initial;--tw-numeric-figure:initial;--tw-numeric-spacing:initial;--tw-numeric-fraction:initial;--tw-shadow:0 0 #0000;--tw-shadow-color:initial;--tw-shadow-alpha:100%;--tw-inset-shadow:0 0 #0000;--tw-inset-shadow-color:initial;--tw-inset-shadow-alpha:100%;--tw-ring-color:initial;--tw-ring-shadow:0 0 #0000;--tw-inset-ring-color:initial;--tw-inset-ring-shadow:0 0 #0000;--tw-ring-inset:initial;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-offset-shadow:0 0 #0000;--tw-duration:initial;--tw-ease:initial;--tw-outline-style:solid;--tw-scale-x:1;--tw-scale-y:1;--tw-scale-z:1}}}@layer utilities{.sn\:sr-only{clip-path:inset(50%);white-space:nowrap;border-width:0;width:1px;height:1px;margin:-1px;padding:0;position:absolute;overflow:hidden}.sn\:absolute{position:absolute}.sn\:fixed{position:fixed}.sn\:relative{position:relative}.sn\:sticky{position:sticky}.sn\:inset-0{inset:0}.sn\:inset-y-0{inset-block:0}.sn\:top-0{top:0}.sn\:top-1\/2{top:50%}.sn\:right-0{right:0}.sn\:right-1{right:var(--sn-space-1)}.sn\:right-4{right:calc(var(--sn-space-1) * 4)}.sn\:bottom-4{bottom:calc(var(--sn-space-1) * 4)}.sn\:bottom-\[-1px\]{bottom:-1px}.sn\:left-0{left:0}.sn\:left-1{left:var(--sn-space-1)}.sn\:left-1\/2{left:50%}.sn\:z-10{z-index:10}.sn\:z-20{z-index:20}.sn\:z-30{z-index:30}.sn\:z-50{z-index:50}.sn\:m-0{margin:0}.sn\:my-1{margin-block:var(--sn-space-1)}.sn\:-mt-2{margin-top:calc(var(--sn-space-1) * -2)}.sn\:-mt-3{margin-top:calc(var(--sn-space-1) * -3)}.sn\:mt-2{margin-top:calc(var(--sn-space-1) * 2)}.sn\:-mb-px{margin-bottom:-1px}.sn\:mb-0{margin-bottom:0}.sn\:ml-1{margin-left:var(--sn-space-1)}.sn\:ml-auto{margin-left:auto}.sn\:box-border{box-sizing:border-box}.sn\:block{display:block}.sn\:flex{display:flex}.sn\:hidden{display:none}.sn\:inline-block{display:inline-block}.sn\:inline-flex{display:inline-flex}.sn\:size-2{width:calc(var(--sn-space-1) * 2);height:calc(var(--sn-space-1) * 2)}.sn\:size-3{width:calc(var(--sn-space-1) * 3);height:calc(var(--sn-space-1) * 3)}.sn\:size-4{width:calc(var(--sn-space-1) * 4);height:calc(var(--sn-space-1) * 4)}.sn\:size-6{width:calc(var(--sn-space-1) * 6);height:calc(var(--sn-space-1) * 6)}.sn\:size-7{width:calc(var(--sn-space-1) * 7);height:calc(var(--sn-space-1) * 7)}.sn\:size-8{width:calc(var(--sn-space-1) * 8);height:calc(var(--sn-space-1) * 8)}.sn\:size-10{width:calc(var(--sn-space-1) * 10);height:calc(var(--sn-space-1) * 10)}.sn\:size-12{width:calc(var(--sn-space-1) * 12);height:calc(var(--sn-space-1) * 12)}.sn\:size-24{width:calc(var(--sn-space-1) * 24);height:calc(var(--sn-space-1) * 24)}.sn\:size-full{width:100%;height:100%}.sn\:h-\(--accordion-panel-height\){height:var(--accordion-panel-height)}.sn\:h-0\.5{height:calc(var(--sn-space-1) * .5)}.sn\:h-1{height:var(--sn-space-1)}.sn\:h-3{height:calc(var(--sn-space-1) * 3)}.sn\:h-4{height:calc(var(--sn-space-1) * 4)}.sn\:h-5{height:calc(var(--sn-space-1) * 5)}.sn\:h-6{height:calc(var(--sn-space-1) * 6)}.sn\:h-7{height:calc(var(--sn-space-1) * 7)}.sn\:h-8{height:calc(var(--sn-space-1) * 8)}.sn\:h-10{height:calc(var(--sn-space-1) * 10)}.sn\:h-16{height:calc(var(--sn-space-1) * 16)}.sn\:h-96{height:calc(var(--sn-space-1) * 96)}.sn\:h-auto{height:auto}.sn\:h-full{height:100%}.sn\:h-px{height:1px}.sn\:max-h-\(--available-height\){max-height:var(--available-height)}.sn\:max-h-\[calc\(100dvh-2\*var\(--sn-space-4\)\)\]{max-height:calc(100dvh - 2 * var(--sn-space-4))}.sn\:min-h-0{min-height:0}.sn\:min-h-20{min-height:calc(var(--sn-space-1) * 20)}.sn\:w-\(--active-tab-width\){width:var(--active-tab-width)}.sn\:w-2\/3{width:66.6667%}.sn\:w-6{width:calc(var(--sn-space-1) * 6)}.sn\:w-7{width:calc(var(--sn-space-1) * 7)}.sn\:w-24{width:calc(var(--sn-space-1) * 24)}.sn\:w-80{width:calc(var(--sn-space-1) * 80)}.sn\:w-90{width:calc(var(--sn-space-1) * 90)}.sn\:w-110{width:calc(var(--sn-space-1) * 110)}.sn\:w-auto{width:auto}.sn\:w-full{width:100%}.sn\:w-max{width:max-content}.sn\:w-px{width:1px}.sn\:max-w-\[calc\(100vw-2\*var\(--sn-space-4\)\)\]{max-width:calc(100vw - 2 * var(--sn-space-4))}.sn\:max-w-full{max-width:100%}.sn\:min-w-\(--anchor-width\){min-width:var(--anchor-width)}.sn\:min-w-0{min-width:0}.sn\:min-w-7{min-width:calc(var(--sn-space-1) * 7)}.sn\:min-w-48{min-width:calc(var(--sn-space-1) * 48)}.sn\:min-w-full{min-width:100%}.sn\:flex-1{flex:1}.sn\:shrink-0{flex-shrink:0}.sn\:border-separate{border-collapse:separate}.sn\:border-spacing-0{--tw-border-spacing-x:0px;--tw-border-spacing-y:0px;border-spacing:var(--tw-border-spacing-x) var(--tw-border-spacing-y)}.sn\:origin-\(--transform-origin\){transform-origin:var(--transform-origin)}.sn\:-translate-x-1\/2{--tw-translate-x:calc(calc(1 / 2 * 100%) * -1);translate:var(--tw-translate-x) var(--tw-translate-y)}.sn\:translate-x-\(--active-tab-left\){--tw-translate-x:var(--active-tab-left);translate:var(--tw-translate-x) var(--tw-translate-y)}.sn\:-translate-y-1\/2{--tw-translate-y:calc(calc(1 / 2 * 100%) * -1);translate:var(--tw-translate-x) var(--tw-translate-y)}.sn\:animate-\[sn-pulse_var\(--sn-duration-loop\)_ease-in-out_infinite\]{animation:sn-pulse var(--sn-duration-loop) ease-in-out infinite}.sn\:animate-\[sn-spin_var\(--sn-duration-loop\)_linear_infinite\]{animation:sn-spin var(--sn-duration-loop) linear infinite}.sn\:cursor-default{cursor:default}.sn\:cursor-grab{cursor:grab}.sn\:cursor-not-allowed{cursor:not-allowed}.sn\:cursor-pointer{cursor:pointer}.sn\:resize-y{resize:vertical}.sn\:\[scrollbar-width\:none\]{scrollbar-width:none}.sn\:list-none{list-style-type:none}.sn\:flex-col{flex-direction:column}.sn\:flex-col-reverse{flex-direction:column-reverse}.sn\:flex-wrap{flex-wrap:wrap}.sn\:items-center{align-items:center}.sn\:items-start{align-items:flex-start}.sn\:justify-between{justify-content:space-between}.sn\:justify-center{justify-content:center}.sn\:justify-end{justify-content:flex-end}.sn\:gap-1{gap:var(--sn-space-1)}.sn\:gap-2{gap:calc(var(--sn-space-1) * 2)}.sn\:gap-3{gap:calc(var(--sn-space-1) * 3)}.sn\:gap-4{gap:calc(var(--sn-space-1) * 4)}.sn\:gap-6{gap:calc(var(--sn-space-1) * 6)}.sn\:gap-10{gap:calc(var(--sn-space-1) * 10)}.sn\:self-stretch{align-self:stretch}.sn\:truncate{text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.sn\:overflow-auto{overflow:auto}.sn\:overflow-hidden{overflow:hidden}.sn\:overflow-x-auto{overflow-x:auto}.sn\:overflow-y-auto{overflow-y:auto}.sn\:rounded-full{border-radius:var(--sn-radius-full)}.sn\:rounded-lg{border-radius:var(--sn-radius-lg)}.sn\:rounded-md{border-radius:var(--sn-radius-md)}.sn\:rounded-sm{border-radius:var(--sn-radius-sm)}.sn\:border{border-style:var(--tw-border-style);border-width:1px}.sn\:border-0{border-style:var(--tw-border-style);border-width:0}.sn\:border-r{border-right-style:var(--tw-border-style);border-right-width:1px}.sn\:border-b{border-bottom-style:var(--tw-border-style);border-bottom-width:1px}.sn\:border-l{border-left-style:var(--tw-border-style);border-left-width:1px}.sn\:border-dashed{--tw-border-style:dashed;border-style:dashed}.sn\:border-danger{border-color:var(--sn-color-border-danger)}.sn\:border-default{border-color:var(--sn-color-border-default)}.sn\:border-strong{border-color:var(--sn-color-border-strong)}.sn\:border-transparent{border-color:#0000}.sn\:border-r-default{border-right-color:var(--sn-color-border-default)}.sn\:border-l-default{border-left-color:var(--sn-color-border-default)}.sn\:bg-\(--sn-color-border-default\){background-color:var(--sn-color-border-default)}.sn\:bg-\(--sn-color-text-on-primary\){background-color:var(--sn-color-text-on-primary)}.sn\:bg-canvas{background-color:var(--sn-color-bg-canvas)}.sn\:bg-danger{background-color:var(--sn-color-bg-danger)}.sn\:bg-danger-subtle{background-color:var(--sn-color-bg-danger-subtle)}.sn\:bg-inverse{background-color:var(--sn-color-bg-inverse)}.sn\:bg-overlay{background-color:var(--sn-color-bg-overlay)}.sn\:bg-primary{background-color:var(--sn-color-bg-primary)}.sn\:bg-subtle{background-color:var(--sn-color-bg-subtle)}.sn\:bg-success{background-color:var(--sn-color-bg-success)}.sn\:bg-success-subtle{background-color:var(--sn-color-bg-success-subtle)}.sn\:bg-surface{background-color:var(--sn-color-bg-surface)}.sn\:bg-transparent{background-color:#0000}.sn\:bg-warning{background-color:var(--sn-color-bg-warning)}.sn\:bg-warning-subtle{background-color:var(--sn-color-bg-warning-subtle)}.sn\:object-cover{object-fit:cover}.sn\:p-0{padding:0}.sn\:p-1{padding:var(--sn-space-1)}.sn\:p-3{padding:calc(var(--sn-space-1) * 3)}.sn\:p-4{padding:calc(var(--sn-space-1) * 4)}.sn\:p-5{padding:calc(var(--sn-space-1) * 5)}.sn\:p-6{padding:calc(var(--sn-space-1) * 6)}.sn\:p-px{padding:1px}.sn\:px-0{padding-inline:0}.sn\:px-1{padding-inline:var(--sn-space-1)}.sn\:px-2{padding-inline:calc(var(--sn-space-1) * 2)}.sn\:px-3{padding-inline:calc(var(--sn-space-1) * 3)}.sn\:px-4{padding-inline:calc(var(--sn-space-1) * 4)}.sn\:px-6{padding-inline:calc(var(--sn-space-1) * 6)}.sn\:py-0{padding-block:0}.sn\:py-1{padding-block:var(--sn-space-1)}.sn\:py-2{padding-block:calc(var(--sn-space-1) * 2)}.sn\:py-3{padding-block:calc(var(--sn-space-1) * 3)}.sn\:py-6{padding-block:calc(var(--sn-space-1) * 6)}.sn\:py-12{padding-block:calc(var(--sn-space-1) * 12)}.sn\:py-16{padding-block:calc(var(--sn-space-1) * 16)}.sn\:pt-1{padding-top:var(--sn-space-1)}.sn\:pb-4{padding-bottom:calc(var(--sn-space-1) * 4)}.sn\:pb-px{padding-bottom:1px}.sn\:text-center{text-align:center}.sn\:text-left{text-align:left}.sn\:text-right{text-align:right}.sn\:align-middle{vertical-align:middle}.sn\:align-text-bottom{vertical-align:text-bottom}.sn\:font-mono{font-family:var(--sn-font-mono)}.sn\:font-sans{font-family:var(--sn-font-sans)}.sn\:text-body{font-size:var(--sn-text-body);line-height:var(--tw-leading,var(--sn-text-body--line-height));letter-spacing:var(--tw-tracking,var(--sn-text-body--letter-spacing));font-weight:var(--tw-font-weight,var(--sn-text-body--font-weight))}.sn\:text-body-strong{font-size:var(--sn-text-body-strong);line-height:var(--tw-leading,var(--sn-text-body-strong--line-height));letter-spacing:var(--tw-tracking,var(--sn-text-body-strong--letter-spacing));font-weight:var(--tw-font-weight,var(--sn-text-body-strong--font-weight))}.sn\:text-caption{font-size:var(--sn-text-caption);line-height:var(--tw-leading,var(--sn-text-caption--line-height));letter-spacing:var(--tw-tracking,var(--sn-text-caption--letter-spacing));font-weight:var(--tw-font-weight,var(--sn-text-caption--font-weight))}.sn\:text-display{font-size:var(--sn-text-display);line-height:var(--tw-leading,var(--sn-text-display--line-height));letter-spacing:var(--tw-tracking,var(--sn-text-display--letter-spacing));font-weight:var(--tw-font-weight,var(--sn-text-display--font-weight))}.sn\:text-label{font-size:var(--sn-text-label);line-height:var(--tw-leading,var(--sn-text-label--line-height));letter-spacing:var(--tw-tracking,var(--sn-text-label--letter-spacing));font-weight:var(--tw-font-weight,var(--sn-text-label--font-weight))}.sn\:text-title{font-size:var(--sn-text-title);line-height:var(--tw-leading,var(--sn-text-title--line-height));letter-spacing:var(--tw-tracking,var(--sn-text-title--letter-spacing));font-weight:var(--tw-font-weight,var(--sn-text-title--font-weight))}.sn\:whitespace-nowrap{white-space:nowrap}.sn\:text-danger{color:var(--sn-color-text-danger)}.sn\:text-default{color:var(--sn-color-text-default)}.sn\:text-inherit{color:inherit}.sn\:text-inverse{color:var(--sn-color-text-inverse)}.sn\:text-muted{color:var(--sn-color-text-muted)}.sn\:text-on-danger{color:var(--sn-color-text-on-danger)}.sn\:text-on-primary{color:var(--sn-color-text-on-primary)}.sn\:text-on-success{color:var(--sn-color-text-on-success)}.sn\:text-on-warning{color:var(--sn-color-text-on-warning)}.sn\:text-success{color:var(--sn-color-text-success)}.sn\:text-warning{color:var(--sn-color-text-warning)}.sn\:tabular-nums{--tw-numeric-spacing:tabular-nums;font-variant-numeric:var(--tw-ordinal,) var(--tw-slashed-zero,) var(--tw-numeric-figure,) var(--tw-numeric-spacing,) var(--tw-numeric-fraction,)}.sn\:underline{text-decoration-line:underline}.sn\:underline-offset-2{text-underline-offset:2px}.sn\:opacity-\(--sn-opacity-disabled\){opacity:var(--sn-opacity-disabled)}.sn\:opacity-0{opacity:0}.sn\:shadow-\[inset_-2px_0_0_var\(--sn-color-border-focus\)\]{--tw-shadow:inset -2px 0 0 var(--tw-shadow-color,var(--sn-color-border-focus));box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.sn\:shadow-\[inset_0_-2px_0_var\(--sn-color-border-focus\)\]{--tw-shadow:inset 0 -2px 0 var(--tw-shadow-color,var(--sn-color-border-focus));box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.sn\:shadow-\[inset_0_2px_0_var\(--sn-color-border-focus\)\]{--tw-shadow:inset 0 2px 0 var(--tw-shadow-color,var(--sn-color-border-focus));box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.sn\:shadow-\[inset_2px_0_0_var\(--sn-color-border-focus\)\]{--tw-shadow:inset 2px 0 0 var(--tw-shadow-color,var(--sn-color-border-focus));box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.sn\:transition-\[height\]{transition-property:height;transition-timing-function:var(--tw-ease,ease);transition-duration:var(--tw-duration,0s)}.sn\:transition-\[opacity\,scale\]{transition-property:opacity,scale;transition-timing-function:var(--tw-ease,ease);transition-duration:var(--tw-duration,0s)}.sn\:transition-\[opacity\,translate\]{transition-property:opacity,translate;transition-timing-function:var(--tw-ease,ease);transition-duration:var(--tw-duration,0s)}.sn\:transition-\[translate\,width\]{transition-property:translate,width;transition-timing-function:var(--tw-ease,ease);transition-duration:var(--tw-duration,0s)}.sn\:transition-\[width\]{transition-property:width;transition-timing-function:var(--tw-ease,ease);transition-duration:var(--tw-duration,0s)}.sn\:transition-colors{transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,ease);transition-duration:var(--tw-duration,0s)}.sn\:transition-opacity{transition-property:opacity;transition-timing-function:var(--tw-ease,ease);transition-duration:var(--tw-duration,0s)}.sn\:transition-transform{transition-property:transform,translate,scale,rotate;transition-timing-function:var(--tw-ease,ease);transition-duration:var(--tw-duration,0s)}.sn\:duration-\(--sn-duration-fast\){--tw-duration:var(--sn-duration-fast);transition-duration:var(--sn-duration-fast)}.sn\:duration-\(--sn-duration-normal\){--tw-duration:var(--sn-duration-normal);transition-duration:var(--sn-duration-normal)}.sn\:ease-standard{--tw-ease:var(--sn-easing-standard);transition-timing-function:var(--sn-easing-standard)}.sn\:outline-none{--tw-outline-style:none;outline-style:none}.sn\:select-none{-webkit-user-select:none;user-select:none}.sn\:\[font\:inherit\]{font:inherit}@media (hover:hover){.sn\:group-hover\/head\:opacity-100:is(:where(.sn\:group\/head):hover *){opacity:1}}.sn\:group-aria-invalid\:text-danger:is(:where(.sn\:group)[aria-invalid=true] *){color:var(--sn-color-text-danger)}@media (hover:hover){.sn\:group-data-interactive\/row\:group-hover\/row\:bg-subtle:is(:where(.sn\:group\/row)[data-interactive] *):is(:where(.sn\:group\/row):hover *){background-color:var(--sn-color-bg-subtle)}}.sn\:group-data-invalid\:text-danger:is(:where(.sn\:group)[data-invalid] *){color:var(--sn-color-text-danger)}.sn\:group-data-panel-open\:rotate-180:is(:where(.sn\:group)[data-panel-open] *),.sn\:group-data-popup-open\:rotate-180:is(:where(.sn\:group)[data-popup-open] *){rotate:180deg}.sn\:group-data-selected\:border-transparent:is(:where(.sn\:group)[data-selected] *){border-color:#0000}.sn\:group-data-selected\:bg-primary:is(:where(.sn\:group)[data-selected] *){background-color:var(--sn-color-bg-primary)}.sn\:group-data-selected\/row\:bg-selected:is(:where(.sn\:group\/row)[data-selected] *){background-color:var(--sn-color-bg-selected)}.sn\:placeholder\:text-muted::placeholder{color:var(--sn-color-text-muted)}.sn\:focus-within\:opacity-100:focus-within{opacity:1}@media (hover:hover){.sn\:hover\:bg-subtle:hover{background-color:var(--sn-color-bg-subtle)}.sn\:hover\:text-default:hover{color:var(--sn-color-text-default)}.sn\:hover\:text-muted:hover{color:var(--sn-color-text-muted)}.sn\:hover\:not-disabled\:bg-subtle:hover:not(:disabled){background-color:var(--sn-color-bg-subtle)}.sn\:hover\:not-data-disabled\:border-strong:hover:not([data-disabled]){border-color:var(--sn-color-border-strong)}.sn\:hover\:not-data-disabled\:bg-primary-hover:hover:not([data-disabled]){background-color:var(--sn-color-bg-primary-hover)}.sn\:hover\:not-data-disabled\:bg-subtle:hover:not([data-disabled]){background-color:var(--sn-color-bg-subtle)}.sn\:hover\:not-data-disabled\:text-default:hover:not([data-disabled]){color:var(--sn-color-text-default)}}.sn\:focus-visible\:outline-2:focus-visible{outline-style:var(--tw-outline-style);outline-width:2px}.sn\:focus-visible\:-outline-offset-2:focus-visible{outline-offset:calc(2px * -1)}.sn\:focus-visible\:outline-offset-2:focus-visible{outline-offset:2px}.sn\:focus-visible\:outline-focus:focus-visible{outline-color:var(--sn-color-border-focus)}.sn\:disabled\:cursor-not-allowed:disabled{cursor:not-allowed}.sn\:disabled\:opacity-\(--sn-opacity-disabled\):disabled{opacity:var(--sn-opacity-disabled)}.sn\:aria-invalid\:border-danger[aria-invalid=true]{border-color:var(--sn-color-border-danger)}@media (hover:hover){.sn\:hover\:aria-invalid\:border-danger:hover[aria-invalid=true]{border-color:var(--sn-color-border-danger)}}.sn\:aria-invalid\:focus-visible\:outline-danger[aria-invalid=true]:focus-visible{outline-color:var(--sn-color-border-danger)}.sn\:data-active\:text-default[data-active]{color:var(--sn-color-text-default)}.sn\:data-checked\:translate-x-3[data-checked]{--tw-translate-x:calc(var(--sn-space-1) * 3);translate:var(--tw-translate-x) var(--tw-translate-y)}.sn\:data-checked\:border-transparent[data-checked]{border-color:#0000}.sn\:data-checked\:bg-\(--sn-color-text-on-primary\)[data-checked]{background-color:var(--sn-color-text-on-primary)}.sn\:data-checked\:bg-primary[data-checked]{background-color:var(--sn-color-bg-primary)}.sn\:data-disabled\:cursor-not-allowed[data-disabled]{cursor:not-allowed}.sn\:data-disabled\:opacity-\(--sn-opacity-disabled\)[data-disabled]{opacity:var(--sn-opacity-disabled)}.sn\:data-disabled\:not-data-loading\:cursor-not-allowed[data-disabled]:not([data-loading]){cursor:not-allowed}.sn\:data-disabled\:not-data-loading\:opacity-\(--sn-opacity-disabled\)[data-disabled]:not([data-loading]){opacity:var(--sn-opacity-disabled)}.sn\:data-dragging\:border-focus[data-dragging]{border-color:var(--sn-color-border-focus)}.sn\:data-dragging\:bg-subtle[data-dragging]{background-color:var(--sn-color-bg-subtle)}.sn\:data-dragging\:bg-surface[data-dragging]{background-color:var(--sn-color-bg-surface)}.sn\:data-dragging\:outline[data-dragging],.sn\:data-dragging\:outline-1[data-dragging]{outline-style:var(--tw-outline-style);outline-width:1px}.sn\:data-dragging\:-outline-offset-1[data-dragging]{outline-offset:calc(1px * -1)}.sn\:data-dragging\:outline-default[data-dragging]{outline-color:var(--sn-color-border-default)}.sn\:data-ending-style\:h-0[data-ending-style]{height:0}.sn\:data-ending-style\:translate-y-2[data-ending-style]{--tw-translate-y:calc(var(--sn-space-1) * 2);translate:var(--tw-translate-x) var(--tw-translate-y)}.sn\:data-ending-style\:scale-98[data-ending-style]{--tw-scale-x:98%;--tw-scale-y:98%;--tw-scale-z:98%;scale:var(--tw-scale-x) var(--tw-scale-y)}.sn\:data-ending-style\:opacity-0[data-ending-style]{opacity:0}.sn\:data-highlighted\:bg-subtle[data-highlighted]{background-color:var(--sn-color-bg-subtle)}.sn\:data-indeterminate\:border-transparent[data-indeterminate]{border-color:#0000}.sn\:data-indeterminate\:bg-primary[data-indeterminate]{background-color:var(--sn-color-bg-primary)}@media (hover:hover){.sn\:data-interactive\:hover\:bg-subtle[data-interactive]:hover{background-color:var(--sn-color-bg-subtle)}}.sn\:data-invalid\:border-danger[data-invalid]{border-color:var(--sn-color-border-danger)}.sn\:data-invalid\:focus-visible\:outline-danger[data-invalid]:focus-visible{outline-color:var(--sn-color-border-danger)}.sn\:data-limited\:hidden[data-limited]{display:none}.sn\:data-loading\:cursor-progress[data-loading]{cursor:progress}.sn\:data-placeholder\:text-muted[data-placeholder]{color:var(--sn-color-text-muted)}.sn\:data-popup-open\:border-focus[data-popup-open]{border-color:var(--sn-color-border-focus)}.sn\:data-pressed\:border-default[data-pressed]{border-color:var(--sn-color-border-default)}.sn\:data-pressed\:bg-surface[data-pressed]{background-color:var(--sn-color-bg-surface)}.sn\:data-pressed\:text-default[data-pressed]{color:var(--sn-color-text-default)}.sn\:data-selected\:bg-selected[data-selected]{background-color:var(--sn-color-bg-selected)}.sn\:data-starting-style\:h-0[data-starting-style]{height:0}.sn\:data-starting-style\:translate-y-2[data-starting-style]{--tw-translate-y:calc(var(--sn-space-1) * 2);translate:var(--tw-translate-x) var(--tw-translate-y)}.sn\:data-starting-style\:scale-98[data-starting-style]{--tw-scale-x:98%;--tw-scale-y:98%;--tw-scale-z:98%;scale:var(--tw-scale-x) var(--tw-scale-y)}.sn\:data-starting-style\:opacity-0[data-starting-style]{opacity:0}@media (prefers-reduced-motion:reduce){.sn\:motion-reduce\:animate-none{animation:none}.sn\:motion-reduce\:transition-none{transition-property:none}}@media not all and (min-width:40rem){.sn\:max-sm\:flex-col-reverse{flex-direction:column-reverse}.sn\:max-sm\:text-\[16px\]{font-size:16px}:is(.sn\:max-sm\:\*\:h-10>*){height:calc(var(--sn-space-1) * 10)}:is(.sn\:max-sm\:\*\:w-full>*){width:100%}}@media (min-width:40rem){.sn\:sm\:flex{display:flex}.sn\:sm\:hidden{display:none}.sn\:sm\:inline{display:inline}}@media (pointer:coarse){.sn\:pointer-coarse\:hidden{display:none}.sn\:pointer-coarse\:inline{display:inline}}.sn\:\[\&_svg\]\:pointer-events-none svg{pointer-events:none}.sn\:\[\&_svg\]\:size-3 svg{width:calc(var(--sn-space-1) * 3);height:calc(var(--sn-space-1) * 3)}.sn\:\[\&_svg\]\:size-4 svg{width:calc(var(--sn-space-1) * 4);height:calc(var(--sn-space-1) * 4)}.sn\:\[\&_svg\]\:size-5 svg{width:calc(var(--sn-space-1) * 5);height:calc(var(--sn-space-1) * 5)}.sn\:\[\&_svg\]\:size-6 svg{width:calc(var(--sn-space-1) * 6);height:calc(var(--sn-space-1) * 6)}.sn\:\[\&_svg\]\:shrink-0 svg{flex-shrink:0}}:root{--sn-space-0:0px;--sn-space-1:4px;--sn-space-2:8px;--sn-space-3:12px;--sn-space-4:16px;--sn-space-5:20px;--sn-space-6:24px;--sn-space-7:28px;--sn-space-8:32px;--sn-space-10:40px;--sn-space-12:48px;--sn-space-16:64px;--sn-radius-sm:2px;--sn-radius-md:4px;--sn-radius-lg:6px;--sn-radius-full:9999px;--sn-stroke-default:1px;--sn-stroke-focus:2px;--sn-duration-fast:.12s;--sn-duration-normal:.2s;--sn-easing-standard:cubic-bezier(.2, 0, 0, 1);--sn-duration-loop:1s;--sn-opacity-disabled:.45;--sn-font-sans:"IBM Plex Sans KR", ui-sans-serif, system-ui, sans-serif;--sn-font-mono:"IBM Plex Mono", ui-monospace, SFMono-Regular, Menlo, monospace;--sn-text-display:32px;--sn-text-display--line-height:40px;--sn-text-display--font-weight:600;--sn-text-display--letter-spacing:-.02em;--sn-text-title:20px;--sn-text-title--line-height:28px;--sn-text-title--font-weight:600;--sn-text-title--letter-spacing:-.01em;--sn-text-body:14px;--sn-text-body--line-height:20px;--sn-text-body--font-weight:400;--sn-text-body--letter-spacing:0em;--sn-text-body-strong:14px;--sn-text-body-strong--line-height:20px;--sn-text-body-strong--font-weight:500;--sn-text-body-strong--letter-spacing:0em;--sn-text-label:13px;--sn-text-label--line-height:16px;--sn-text-label--font-weight:500;--sn-text-label--letter-spacing:0em;--sn-text-caption:12px;--sn-text-caption--line-height:16px;--sn-text-caption--font-weight:400;--sn-text-caption--letter-spacing:0em;--sn-text-mono:14px;--sn-text-mono--line-height:20px;--sn-text-mono--font-weight:400;--sn-text-mono--letter-spacing:0em;--sn-text-mono-caption:12px;--sn-text-mono-caption--line-height:16px;--sn-text-mono-caption--font-weight:400;--sn-text-mono-caption--letter-spacing:0em}:root,[data-theme=light]{color-scheme:light;--sn-color-bg-canvas:#f4f6f8;--sn-color-bg-surface:#fff;--sn-color-bg-subtle:#e6eaee;--sn-color-bg-primary:#2340d0;--sn-color-bg-primary-hover:#1b33ae;--sn-color-bg-danger:#c8312b;--sn-color-bg-overlay:#0e121680;--sn-color-text-default:#0e1216;--sn-color-text-muted:#5b6672;--sn-color-text-on-primary:#fff;--sn-color-text-on-danger:#fff;--sn-color-text-danger:#c8312b;--sn-color-border-default:#d5dbe1;--sn-color-border-strong:#8d99a6;--sn-color-border-focus:#2340d0;--sn-color-border-danger:#c8312b;--sn-color-bg-inverse:#0e1216;--sn-color-text-inverse:#fff;--sn-color-bg-success:#16804f;--sn-color-text-on-success:#fff;--sn-color-bg-success-subtle:#eaf7f0;--sn-color-text-success:#0f6640;--sn-color-bg-warning:#e3a43a;--sn-color-text-on-warning:#0e1216;--sn-color-bg-warning-subtle:#fcf5e6;--sn-color-text-warning:#85520a;--sn-color-bg-danger-subtle:#fcedec;--sn-color-border-success:#16804f;--sn-color-border-warning:#a86c12;--sn-color-bg-selected:#eef1ff}[data-theme=dark]{color-scheme:dark;--sn-color-bg-canvas:#0e1216;--sn-color-bg-surface:#161b21;--sn-color-bg-subtle:#1e252d;--sn-color-bg-primary:#6e86ff;--sn-color-bg-primary-hover:#8c9fff;--sn-color-bg-danger:#f06a5f;--sn-color-bg-overlay:#0e121680;--sn-color-text-default:#e6eaee;--sn-color-text-muted:#8d99a6;--sn-color-text-on-primary:#0e1216;--sn-color-text-on-danger:#0e1216;--sn-color-text-danger:#f06a5f;--sn-color-border-default:#2a323b;--sn-color-border-strong:#5b6672;--sn-color-border-focus:#6e86ff;--sn-color-border-danger:#f06a5f;--sn-color-bg-inverse:#e6eaee;--sn-color-text-inverse:#0e1216;--sn-color-bg-success:#3db882;--sn-color-text-on-success:#0e1216;--sn-color-bg-success-subtle:#0b2218;--sn-color-text-success:#6bd1a0;--sn-color-bg-warning:#e3a43a;--sn-color-text-on-warning:#0e1216;--sn-color-bg-warning-subtle:#24190a;--sn-color-text-warning:#eec46a;--sn-color-bg-danger-subtle:#2b1311;--sn-color-border-success:#3db882;--sn-color-border-warning:#e3a43a;--sn-color-bg-selected:#18214d}@keyframes sn-pulse{50%{opacity:.5}}@keyframes sn-spin{to{transform:rotate(360deg)}}}
package/dist/tailwind.css CHANGED
@@ -69,4 +69,5 @@
69
69
  --outline-color-success: var(--sn-color-border-success);
70
70
  --border-color-warning: var(--sn-color-border-warning);
71
71
  --outline-color-warning: var(--sn-color-border-warning);
72
+ --background-color-selected: var(--sn-color-bg-selected);
72
73
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xonic0506/sonic-ui",
3
- "version": "0.5.0",
3
+ "version": "0.6.1",
4
4
  "description": "Figma-first personal design system (Base UI + Tailwind v4, prebuilt CSS)",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -46,6 +46,11 @@
46
46
  },
47
47
  "dependencies": {
48
48
  "@base-ui/react": "^1.9.0",
49
+ "@dnd-kit/core": "^6.3.1",
50
+ "@dnd-kit/sortable": "^10.0.0",
51
+ "@dnd-kit/utilities": "^3.2.2",
52
+ "@tanstack/react-table": "^9.3.1",
53
+ "@tanstack/react-virtual": "^3.14.22",
49
54
  "class-variance-authority": "^0.7.1",
50
55
  "lucide-react": "^1.59.0",
51
56
  "tailwind-merge": "^3.7.0"