@xonic0506/sonic-ui 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/DESIGN.md CHANGED
@@ -156,6 +156,7 @@ import { Plus } from 'lucide-react';
156
156
  | 위치 | 값 |
157
157
  |---|---|
158
158
  | 페이지 콘텐츠 | 최대 너비 960, 가운데 정렬. 좌우 padding `space/4`(960 안쪽), 위아래 padding `space/10` |
159
+ | 사이드 내비가 있는 화면 | 헤더와 사이드 내비(너비 240)는 화면 전체를 쓰고, 960 규칙은 본문 영역에만 적용 |
159
160
  | 섹션 사이 | `space/6` |
160
161
  | Card 안쪽 padding / Header·Body·Footer 사이 | `space/5` / `space/4` |
161
162
  | 폼 필드 사이 | `space/4` |
@@ -211,7 +212,7 @@ hover, focus는 **컴포넌트 variant로 만들지 않아요.** React prop이
211
212
  | 상태 | Figma | 코드 |
212
213
  |---|---|---|
213
214
  | hover | Core 문서 "Interaction states" 매트릭스에 인스턴스 오버라이드로 그림 | `:hover` (disabled일 때는 적용 안 함) |
214
- | focus | 같은 매트릭스. 2px `border/focus` 링, 간격 2px. invalid Input은 `border/danger` 링 | `:focus-visible` → `outline 2px`, `outline-offset 2px` (Tab만 안쪽 −2px). 포커스 링을 그리는 요소에는 `outline-none`을 같이 쓰지 않아요(Tailwind v4에서 링이 사라져요) |
215
+ | focus | 같은 매트릭스. 2px `border/focus` 링, 간격 2px. invalid Input은 `border/danger` 링 | `:focus-visible` → `outline 2px`, `outline-offset 2px` (Tab, ToggleGroup 항목만 안쪽 −2px). 포커스 링을 그리는 요소에는 `outline-none`을 같이 쓰지 않아요(Tailwind v4에서 링이 사라져요) |
215
216
  | press | 그리지 않음 | 구현 안 함 (v0.1) |
216
217
 
217
218
  상태별 토큰은 §10 컴포넌트 스펙 표가 기준이에요. 화면 디자인에서는 hover, focus를 따로 그리지 않아요. 컴포넌트가 자동으로 처리해요.
@@ -220,7 +221,7 @@ hover, focus는 **컴포넌트 variant로 만들지 않아요.** React prop이
220
221
 
221
222
  | 계층 | 위치 | 기준 | 예 |
222
223
  |---|---|---|---|
223
- | **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, Field, Skeleton, EmptyState, ErrorState |
224
+ | **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 |
224
225
  | **Pattern** | sonic-ui `src/patterns/` | Core 조합이고 도메인을 모르지만, **두 프로젝트 이상에서 같은 형태로 복사됨** | (아직 없음) |
225
226
  | **Domain** | 각 프로젝트 | 도메인 타입, API, 비즈니스 로직 중 하나라도 앎 | `UserProfileCard`, `ApiKeyList` |
226
227
 
@@ -406,7 +407,7 @@ const retryRef = useRef<HTMLButtonElement>(null);
406
407
  | `description` | 설명 문구 (선택) |
407
408
  | `error` | 에러 문구. `invalid`일 때만 보이고, 그때는 **설명 자리를 대신해요** |
408
409
  | `invalid`, `disabled` | boolean. 안의 Input에 자동으로 전달돼요 (Base UI Field) |
409
- | `children` | 입력 컨트롤 (지금은 `Input`) |
410
+ | `children` | 입력 컨트롤 (`Input`, `Textarea`, `Select`, `FileDropzone`) |
410
411
 
411
412
  간격: 라벨 ↔ 입력 ↔ 설명·에러 `space/2`. 라벨 `label` + `text/default`(invalid여도 그대로), 설명 `caption` + `text/muted`, 에러 `caption` + `text/danger`. **Field 안의 Input에는 `invalid`를 따로 주지 않아요.** 입력 컨트롤과 설명 또는 에러는 `aria-describedby`로 연결돼요.
412
413
 
@@ -616,6 +617,150 @@ toast.add({ title: 'API 키를 만들었어요', description: '키는 지금 한
616
617
 
617
618
  라벨 `label` + `text/default`, 오른쪽에 백분율 `mono-caption` + `text/muted`, 막대와 사이 `space/2`. 트랙 높이 4(`space/1`) `bg/subtle` `radius/full`, 채움 `bg/primary`이 `duration/normal`로 늘어나요. 끝이 정해진 작업(업로드, 내보내기)에만 써요. 끝을 알 수 없는 기다림은 Button `loading`이나 Skeleton을 써요.
618
619
 
620
+ ### Link
621
+
622
+ ```tsx
623
+ <Link href="/docs">DESIGN.md 보기</Link>
624
+ <Link href="https://base-ui.com" external>Base UI 문서</Link>
625
+ <Link render={<NextLink href="/docs" />}>문서</Link> // Next.js Link와 함께
626
+ ```
627
+
628
+ | prop | 값 |
629
+ |---|---|
630
+ | `href` | 이동할 주소 |
631
+ | `external` | 새 탭으로 열고(`rel="noopener noreferrer"`) 오른쪽에 `external-link` 아이콘을 붙여요 |
632
+ | `render` | 다른 링크 컴포넌트로 렌더링 (예: Next.js `Link`) |
633
+
634
+ 다른 페이지나 문서로 **이동**할 때 써요. 동작을 실행하면 Button을 써요. 글자 크기는 부모를 따르고, 색은 `text/default` + 밑줄(offset 2px)이에요. 강조는 한 곳에만 쓰는 원칙(§1)에 따라 링크에 코발트를 쓰지 않아요. hover는 `text/muted`, 포커스 링은 공통 규칙(2px, offset 2px)이에요. 외부 링크 아이콘은 16, 글자와 간격 `space/1`이에요.
635
+
636
+ ### Table
637
+
638
+ ```tsx
639
+ <Table>
640
+ <TableHeader>
641
+ <TableRow><TableHead>이름</TableHead><TableHead align="end">요청 수</TableHead></TableRow>
642
+ </TableHeader>
643
+ <TableBody>
644
+ <TableRow><TableCell>production</TableCell><TableCell align="end">12,480</TableCell></TableRow>
645
+ </TableBody>
646
+ </Table>
647
+ ```
648
+
649
+ | prop | 값 |
650
+ |---|---|
651
+ | `TableHead`, `TableCell`의 `align` | `start` · `end` (숫자 열은 `end`) |
652
+
653
+ 여러 항목의 같은 속성을 비교할 때 써요. 행 높이 40(`space/10`), 좌우 padding `space/3`, 행 아래 1px `border/default`. 머리글 `label` + `text/muted`, 본문 `body` + `text/default`, `align="end"`인 본문 칸은 숫자라서 `mono`예요. 줄무늬나 hover 바탕은 쓰지 않아요. 표가 부모보다 넓으면 가로로 스크롤해요.
654
+
655
+ ### ToggleGroup
656
+
657
+ ```tsx
658
+ <ToggleGroup value={view} onValueChange={setView} aria-label="보기 방식"> // 단일: string
659
+ <ToggleGroupItem value="list">목록</ToggleGroupItem>
660
+ <ToggleGroupItem value="card">카드</ToggleGroupItem>
661
+ </ToggleGroup>
662
+ <ToggleGroup multiple value={filters} onValueChange={setFilters}>…</ToggleGroup> // 다중: string[]
663
+ <ToggleGroupItem value="dark" iconOnly aria-label="다크 테마"><Moon /></ToggleGroupItem> // 아이콘만이면 iconOnly + aria-label 필수
664
+ ```
665
+
666
+ | prop | 값 |
667
+ |---|---|
668
+ | `multiple` | 여러 개를 함께 누를 수 있음. 켜면 `value`·`onValueChange`가 배열이에요(타입으로 강제) |
669
+ | `value`, `defaultValue`, `onValueChange` | 단일은 `string`, 다중은 `string[]`. 단일은 항상 하나가 눌려 있어요(눌린 항목을 다시 눌러도 꺼지지 않음) |
670
+ | `disabled` | 그룹 전체 또는 항목 하나 |
671
+ | `ToggleGroupItem`의 `iconOnly` | 아이콘만 있는 24×24 항목. `aria-label`이 필수예요(타입으로 강제) |
672
+
673
+ 같은 대상을 **보는 방식**을 바꿀 때 써요(목록/카드, 라이트/다크). 누르면 바로 적용돼요. 폼에서 값을 고를 때는 RadioGroup이나 Select를 써요. 바탕 `bg/subtle` + 안쪽 `space/1` + `radius/md`(전체 높이 32). 항목 높이 24(`space/6`), 좌우 `space/2`, `label`, `radius/sm`. 눌린 항목 `bg/surface` + 1px `border/default` + `text/default`, 나머지 `text/muted`(hover `text/default`). 아이콘만 있는 항목은 24×24, 아이콘 16이에요. 화살표 키로 항목 사이를 이동해요. 항목끼리 붙어 있어서 포커스 링은 Tab처럼 **안쪽**(offset −2px)에 그려요.
674
+
675
+ ### Accordion
676
+
677
+ ```tsx
678
+ <Accordion defaultValue={['limit']}>
679
+ <AccordionItem value="limit">
680
+ <AccordionTrigger>키는 몇 개까지 만들 수 있나요?</AccordionTrigger>
681
+ <AccordionPanel>프로젝트마다 10개까지 만들 수 있어요.</AccordionPanel>
682
+ </AccordionItem>
683
+ </Accordion>
684
+ ```
685
+
686
+ | prop | 값 |
687
+ |---|---|
688
+ | `multiple` | 여러 항목을 함께 열 수 있음 (기본 `false`) |
689
+ | `value`, `defaultValue`, `onValueChange` | 열린 항목들의 `value` 배열 |
690
+ | `AccordionItem`의 `disabled` | 항목 하나를 비활성화 |
691
+
692
+ 긴 내용을 제목만 먼저 보여 주고 필요할 때 펼칠 때 써요(자주 묻는 질문, 문서 사이드바 그룹). 꼭 읽어야 하는 내용은 접어 두지 않아요. 항목 아래 1px `border/default`. 트리거는 위아래 `space/3` + `body-strong`, 오른쪽 `chevron-down` 16(`text/muted`)이 열리면 180° 돌아요. 패널은 `body` + `text/muted`, 아래 `space/4`이고, 높이가 `duration/normal`로 변해요. 닫힌 내용도 브라우저 페이지 찾기로 찾아 열 수 있어요.
693
+
694
+ ### FileDropzone / FileList
695
+
696
+ ```tsx
697
+ const [items, setItems] = useState<FileItemData[]>(serverFiles.map(toItem)); // 서버 파일도 같은 모양으로
698
+
699
+ <Field label="첨부 파일" description="PDF, 10MB 이하, 5개까지" error="파일을 1개 이상 올려 주세요." invalid={submitted && !items.length}>
700
+ <FileDropzone
701
+ multiple accept=".pdf" maxSize={10_000_000} maxFiles={5 - items.length}
702
+ onFilesChange={(accepted, rejected) => { addItems(accepted); upload(accepted); }} // 업로드는 프로젝트가 해요
703
+ />
704
+ </Field>
705
+ <FileList items={items} onRemove={remove} onRetry={retry} />
706
+ <FileList layout="grid" items={images} onRemove={remove} />
707
+ ```
708
+
709
+ 컴포넌트는 **고르기·검사·표시만** 해요. 실제 업로드(어느 API로, 어떤 인증으로), 진행률 계산, 재시도는 API를 아는 프로젝트가 하고, 그 결과를 `items`로 넘겨요(§7). 두 번째 프로젝트에서 같은 업로드 코드를 복사하게 되면 `useFileUpload` Pattern으로 올려요.
710
+
711
+ **FileDropzone**
712
+
713
+ | prop | 값 |
714
+ |---|---|
715
+ | `onFilesChange` | `(accepted: File[], rejected: FileRejection[]) => void` (필수) |
716
+ | `accept` | `.pdf`, `image/*`, `image/png` 같은 목록 (쉼표로 구분) |
717
+ | `multiple`, `maxFiles` | 여러 개 고르기와 최대 개수. `multiple`이 없으면 한 개만 받아요 |
718
+ | `maxSize` | 파일 하나의 최대 크기(바이트) |
719
+ | `variant` | `file` · `image` (아이콘과 기본 문구) |
720
+ | `title`, `description` | 영역 안 문구. 기본은 "파일을 끌어다 놓거나 눌러서 고르세요". Field 안에서는 조건을 Field `description`에 쓰고 영역 안 `description`은 비워요 |
721
+ | `invalid`, `disabled` | boolean |
722
+
723
+ - **다른 폼 항목처럼 Field로 감싸요.** Field의 라벨이 버튼 이름이 되고, 설명·에러·`invalid`·`disabled`가 자동으로 연결돼요(Base UI `Field.Control`). Field 없이 쓰면 영역 안 제목이 버튼 이름이 돼요.
724
+ - 에러는 두 종류예요. 고르는 순간 조건에 맞지 않은 파일은 **영역 안**에 바로 알려 주고, "1개 이상 올려 주세요"처럼 제출할 때 확인하는 규칙은 Field `error`로 **아래**에 보여 줘요.
725
+ - 영역 전체가 버튼이라 눌러도, Enter·Space를 눌러도 파일 선택 창이 열려요. 파일을 끌어다 놓아도 돼요.
726
+ - 조건(형식 `type`, 크기 `size`, 개수 `count`)에 맞지 않는 파일은 `rejected`로 넘기고, 영역 안에 고치는 방법을 기본 문구로 보여 줘요(예: "계약서.pdf: 10MB보다 큰 파일은 올릴 수 없어요"). 문구는 `rejected[].message`로 바꿀 수 있어요.
727
+ - 점선 1px `border/strong` + `bg/surface` + `radius/lg`, 위아래 `space/6`, 좌우 `space/4`, 요소 사이 `space/2`. 아이콘 24(`upload` · `image`, `text/muted`), 제목 `body-strong`, 조건 `caption` + `text/muted`. hover `bg/subtle`, 끌어다 놓는 중 `border/focus` + `bg/subtle`, 거부되면 `border/danger` + 문구 `text/danger`.
728
+
729
+ **FileList**
730
+
731
+ | prop | 값 |
732
+ |---|---|
733
+ | `items` | `FileItemData[]` (아래) |
734
+ | `layout` | `list` (일반 파일) · `grid` (이미지 썸네일) |
735
+ | `onRemove`, `onRetry` | `(id) => void`. 넘기면 삭제·다시 시도 버튼이 생겨요 |
736
+
737
+ ```ts
738
+ interface FileItemData {
739
+ id: string;
740
+ name: string;
741
+ size?: number; // 바이트, "2.4 MB"로 표시
742
+ type?: string; // MIME, 이미지 여부 판단
743
+ status: 'uploading' | 'done' | 'error';
744
+ progress?: number; // 0~100, uploading일 때 Progress
745
+ error?: string; // error일 때 문구 (고치는 방법 포함)
746
+ url?: string; // done 파일을 열 주소 → 이름이 Link
747
+ previewUrl?: string; // 썸네일 주소
748
+ file?: File; // 방금 고른 로컬 파일
749
+ }
750
+ ```
751
+
752
+ 미리보기는 `previewUrl` 문자열 하나로 받아요.
753
+
754
+ | 출처 | `previewUrl` | 비고 |
755
+ |---|---|---|
756
+ | 서버 이미지 | `https://…/thumb.jpg` | 서버가 썸네일 주소를 따로 주는 게 가장 가벼워요 |
757
+ | 서버가 base64로 줌 | `data:image/png;base64,…` | 그대로 표시돼요. 응답이 커지니 썸네일 크기만 권장 |
758
+ | 방금 고른 파일 | 넣지 않고 `file`만 넘김 | 컴포넌트가 blob URL을 만들고, 목록에서 빠지면 해제해요 |
759
+ | 인증이 필요한 파일 | 프로젝트가 받아 만든 blob URL | `<img>`는 인증 헤더를 붙일 수 없어요 |
760
+
761
+ - `list`: `bg/surface` + 1px `border/default` + `radius/md`, padding·간격 `space/3`, 항목 사이 `space/2`. 아이콘 칸 32(`bg/subtle`, `file` 16), 이름 `body`(완료이고 `url`이 있으면 Link), 크기·진행률 `mono-caption` + `text/muted`. 업로드 중에는 라벨 없는 Progress, 에러면 `border/danger` + `caption` + `text/danger`. 버튼은 ghost `icon-sm`(`refresh-cw` 다시 시도, `x` 삭제).
762
+ - `grid`: 썸네일 96×96(`bg/subtle`, 1px `border/default`, `radius/md`, 안쪽 `space/1`), 항목 사이 `space/3`. 오른쪽 위 secondary `icon-sm` 버튼, 아래 Progress, 아래 이름 `caption` 한 줄 말줄임. 에러면 `border/danger` + `circle-alert`.
763
+
619
764
  ## 11. Figma ↔ 코드 매핑 (Code Connect 대체)
620
765
 
621
766
  Sonic 팀은 Figma Pro 요금제라 Code Connect(Organization 이상)를 쓸 수 없어요. 아래 표로 대신합니다. Figma 컴포넌트를 코드로 옮길 때는 이 표의 컴포넌트를 그대로 써요.
@@ -647,6 +792,11 @@ Sonic 팀은 Figma Pro 요금제라 Code Connect(Organization 이상)를 쓸 수
647
792
  | Toast | `57:3549` | `useToast().add({ tone, title, description })` (`showDescription` → `description`을 넘길지), 앱에는 `<ToastProvider>` | `src/components/toast.tsx` |
648
793
  | Badge | `57:3498` | `<Badge tone variant>` (`children` → 자식 문구) | `src/components/badge.tsx` |
649
794
  | Progress | `58:5093` | `<Progress value max label>` (`value` variant는 채움 미리보기, `value text`는 코드가 자동 표시) | `src/components/progress.tsx` |
650
- | Icon (32개) | Foundations `Source icons` | `lucide-react`의 같은 이름 컴포넌트 (`Icon / chevron-down` → `<ChevronDown />`) | 쓰는 쪽에서 설치 |
795
+ | Link | `65:3733` | `<Link href external>` (`children` → 자식 문구) | `src/components/link.tsx` |
796
+ | Table Cell · Table | `65:3742` · `65:3743` | `Table`, `TableHeader`, `TableBody`, `TableRow`, `TableHead`, `TableCell` (`type=header` → `TableHead`, `align` → 같은 prop) | `src/components/table.tsx` |
797
+ | Toggle Item · Toggle Group | `66:3760` · `66:3761` | `<ToggleGroup multiple>` + `<ToggleGroupItem value disabled>` (`pressed` → 현재 값, `content=icon`이면 `aria-label`) | `src/components/toggle-group.tsx` |
798
+ | Accordion Item · Accordion | `66:3792` · `66:3793` | `Accordion`, `AccordionItem`, `AccordionTrigger`, `AccordionPanel` (`open` → 현재 값, `title`/`content` → 자식) | `src/components/accordion.tsx` |
799
+ | 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` |
800
+ | Icon (35개) | Foundations `Source icons` | `lucide-react`의 같은 이름 컴포넌트 (`Icon / chevron-down` → `<ChevronDown />`) | 쓰는 쪽에서 설치 |
651
801
 
652
802
  Figma 속성 → props 변환 규칙: variant 속성은 같은 이름의 prop으로, 텍스트 속성 `children`은 자식으로, boolean `header`/`footer`는 해당 하위 컴포넌트를 넣을지 말지로 옮겨요. Button의 `iconLeft`/`iconRight`(+ `leftIcon`/`rightIcon` 교체)와 Button Icon의 `icon`은 prop이 아니라 **자식 순서**예요: `<Button><Plus />추가하기<ChevronDown /></Button>`.
package/README.md CHANGED
@@ -1,65 +1,95 @@
1
- # @xonic0506/sonic-ui
1
+ <div align="center">
2
2
 
3
- Figma를 원본으로 하는 개인 디자인 시스템이에요. React 컴포넌트는 [Base UI](https://base-ui.com) 위에 만들었고, CSS는 Tailwind v4로 미리 빌드해서 배포해요.
3
+ <img src="./docs/assets/sonic-mascot.webp" alt="Sonic 마스코트" width="240" />
4
4
 
5
- - 디자인 규칙: [DESIGN.md](./DESIGN.md)
6
- - AI 작업 규칙: [AI-RULES.md](./AI-RULES.md)
7
- - Figma: [Sonic Design System](https://www.figma.com/design/XifnZsjqJWJqFmAmrM6p3u)
5
+ # Sonic UI
8
6
 
9
- ## 설치
7
+ [![npm version](https://img.shields.io/npm/v/@xonic0506/sonic-ui.svg)](https://www.npmjs.com/package/@xonic0506/sonic-ui) ![React](https://img.shields.io/badge/react-18.2%20%7C%2019-61dafb.svg) [![License](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
8
+
9
+ Figma를 원본으로 하는 개인 디자인 시스템이에요.
10
+
11
+ [Figma](https://www.figma.com/design/XifnZsjqJWJqFmAmrM6p3u) | [디자인 규칙 (DESIGN.md)](./DESIGN.md) | [AI 작업 규칙 (AI-RULES.md)](./AI-RULES.md)
12
+
13
+ </div>
14
+
15
+ Sonic UI는 모든 사이드 프로젝트에서 같은 디자인을 쓰기 위해 만들었어요. 디자인 룰을 Figma와 DESIGN.md에 정해 두면, AI가 그 룰로 Figma 디자인을 만들고 이 패키지의 컴포넌트로 코드를 짜요.
16
+
17
+ ```text
18
+ Figma (원본) → DESIGN.md (룰) → @xonic0506/sonic-ui (코드) → 실제 프로젝트
19
+ ```
20
+
21
+ ## 특징
22
+
23
+ - Figma Variables가 그대로 CSS 변수가 돼요. `color/bg/primary`는 `--sn-color-bg-primary`이고, light/dark 테마를 함께 가져요.
24
+ - CSS를 미리 빌드해서 배포해요. 쓰는 쪽 프로젝트는 Tailwind가 있어도 되고 없어도 돼요.
25
+ - 모든 클래스에 `sn:` 접두사를 붙이고 `@layer sonic` 안에 넣어서, 쓰는 쪽 스타일과 부딪히지 않아요.
26
+ - [Base UI](https://base-ui.com) 위에 만들어서 키보드 조작, 포커스 관리, 화면 낭독기 지원이 기본으로 들어 있어요.
27
+ - 모든 파일에 `"use client"`가 붙어 있어서 Next.js 서버 컴포넌트에서도 바로 import할 수 있어요.
28
+ - 쓰는 쪽 AI가 읽을 규칙 문서(DESIGN.md, AI-RULES.md)와 디자인 lint 규칙을 함께 배포해요.
29
+
30
+ ## 컴포넌트
31
+
32
+ | 분류 | 컴포넌트 |
33
+ | --- | --- |
34
+ | 기본 | Button, Input, Textarea, Text, Card, Avatar, Separator |
35
+ | 폼 | Field, Checkbox, CheckboxGroup, RadioGroup, Switch, Select (단일·다중) |
36
+ | 오버레이 | Dialog, AlertDialog, Menu, Popover, Tooltip |
37
+ | 탐색 | Tabs |
38
+ | 피드백·상태 | Toast, Badge, Progress, Skeleton, EmptyState, ErrorState |
39
+
40
+ 컴포넌트별 props와 스펙은 [DESIGN.md §10](./DESIGN.md#10-컴포넌트-스펙-core)에 있어요.
41
+
42
+ ## 시작하기
43
+
44
+ ### 설치
10
45
 
11
46
  ```bash
12
47
  npm i @xonic0506/sonic-ui
13
48
  ```
14
49
 
15
- `react`, `react-dom` 18.2 이상(19 포함)이 필요해요(peerDependencies).
50
+ `react`, `react-dom` 18.2 이상(19 포함)이 필요해요.
16
51
 
17
52
  ### 글꼴
18
53
 
19
- 글꼴은 패키지에 들어 있지 않아요. IBM Plex Sans(400·500·600)와 IBM Plex Mono(400)를 프로젝트에서 불러와요. 없으면 시스템 글꼴로 대체돼요.
54
+ IBM Plex Sans(400, 500, 600)와 IBM Plex Mono(400)를 프로젝트에서 불러와요. 없으면 시스템 글꼴로 대체돼요.
20
55
 
21
56
  ```html
22
57
  <link href="https://fonts.googleapis.com/css2?family=IBM+Plex+Mono&family=IBM+Plex+Sans:wght@400;500;600&display=swap" rel="stylesheet" />
23
58
  ```
24
59
 
25
- sonic-ui는 글꼴 이름 `"IBM Plex Sans"`, `"IBM Plex Mono"`를 찾아요. 이름을 바꾸는 방식(예: `next/font`의 기본 설정)으로 불러오면 적용되지 않으니, 위 링크나 `@fontsource/ibm-plex-sans`처럼 원래 이름을 유지하는 방식을 써요.
60
+ > [!NOTE]
61
+ > 글꼴은 패키지에 들어 있지 않아요. sonic-ui는 글꼴 이름 `"IBM Plex Sans"`, `"IBM Plex Mono"`를 찾기 때문에, 이름을 바꾸는 방식(예: `next/font` 기본 설정)으로 불러오면 적용되지 않아요. 위 링크나 `@fontsource/ibm-plex-sans`처럼 원래 이름을 유지하는 방식을 써요.
26
62
 
27
- ## 설정
63
+ ### CSS 연결
28
64
 
29
- ### Tailwind v4 프로젝트 (권장)
65
+ Tailwind v4 프로젝트(권장)에서는 이렇게 연결해요.
30
66
 
31
67
  ```css
32
68
  /* app/globals.css */
33
69
  @layer theme, base, sonic, components, utilities;
34
70
  @import "tailwindcss";
35
71
  @import "@xonic0506/sonic-ui/styles.css";
36
- @import "@xonic0506/sonic-ui/tailwind.css"; /* 선택: bg-primary, text-muted, rounded-md, text-body … 클래스 */
72
+ @import "@xonic0506/sonic-ui/tailwind.css"; /* 선택: bg-primary, text-muted, rounded-md, text-body 같은 클래스 */
37
73
  ```
38
74
 
39
- **layer 순서가 중요해요.** `sonic`은 Tailwind preflight(`base`)보다 뒤, 쓰는 쪽 `components`/`utilities`보다 앞이에요. 그래서 다음이 보장돼요.
40
-
41
- - preflight가 버튼 배경, 글자 크기를 지우지 않아요.
42
- - 쓰는 쪽 `className="bg-red-500"`은 항상 sonic-ui 기본 스타일을 이겨요.
75
+ > [!IMPORTANT]
76
+ > layer 순서를 지켜요. `sonic`이 Tailwind preflight(`base`)보다 뒤에 있어야 버튼 배경과 글자 크기가 지워지지 않고, 쓰는 쪽 `components`/`utilities`보다 앞에 있어야 `className="bg-red-500"` 같은 덮어쓰기가 항상 이겨요.
43
77
 
44
- ### Tailwind 없는 프로젝트
78
+ Tailwind가 없는 프로젝트에서는 CSS 파일 하나만 불러와요. 토큰은 `var(--sn-color-bg-primary)`처럼 CSS 변수로 써요.
45
79
 
46
80
  ```ts
47
81
  import '@xonic0506/sonic-ui/styles.css';
48
82
  ```
49
83
 
50
- sonic-ui CSS는 전부 `@layer sonic` 안에 있어서, layer에 넣지 않은 쓰는 쪽 CSS가 항상 이겨요. 토큰은 `var(--sn-color-bg-primary)`처럼 CSS 변수로 써요.
84
+ ### 앱 루트와 테마
51
85
 
52
- ### 앱 최상위 요소
86
+ 앱 루트에 `isolation: isolate`를 지정해요. 앱의 z-index가 `body` 아래에 그려지는 Dialog, Toast를 가리지 않게 해 줘요.
53
87
 
54
88
  ```tsx
55
89
  <div style={{ isolation: 'isolate' }}>{/* 앱 */}</div> // Tailwind: className="isolate"
56
90
  ```
57
91
 
58
- 앱 루트에 `isolation: isolate`를 지정해요. 앱의 z-index가 Dialog Portal(`body` 아래)을 넘어서지 않게 해 줘요.
59
-
60
- ### 테마
61
-
62
- `<html data-theme="light">` 또는 `data-theme="dark"`로 바꿔요. 속성이 없으면 light예요. Portal로 `body`에 그려지는 Dialog에도 같은 테마가 적용돼요.
92
+ 테마는 `<html data-theme="light">` 또는 `data-theme="dark"`로 바꿔요. 속성이 없으면 light이고, `body` 아래에 그려지는 팝업에도 같은 테마가 적용돼요.
63
93
 
64
94
  ## 사용
65
95
 
@@ -86,22 +116,26 @@ import { Button, Card, CardBody, CardHeader, CardTitle, Dialog, DialogClose, Dia
86
116
  </Dialog>
87
117
  ```
88
118
 
89
- 컴포넌트별 props는 [DESIGN.md §10](./DESIGN.md#10-컴포넌트-스펙-core)에 있어요. 모든 파일에 `"use client"`가 붙어 있어서 Next.js 서버 컴포넌트에서도 바로 import할 수 있어요. 하위 컴포넌트는 `CardHeader`처럼 이름 있는 export로 써요.
90
-
91
- Toast를 쓰려면 앱 최상단(Next.js는 `app/layout.tsx`)을 `<ToastProvider>`로 한 번 감싸고, 클라이언트 컴포넌트에서 `useToast().add({ title, tone })`로 띄워요.
119
+ 하위 컴포넌트는 `Card.Header`가 아니라 `CardHeader`처럼 이름 있는 export로 써요. 서버 컴포넌트에서도 깨지지 않게 하기 위해서예요.
92
120
 
93
- ## Claude Code에서 쓰기
121
+ Toast는 앱 최상단(Next.js는 `app/layout.tsx`)을 `<ToastProvider>`로 한 번 감싸고, 클라이언트 컴포넌트에서 띄워요.
94
122
 
95
- 프로젝트 CLAUDE.md에 추가해요.
96
-
97
- ```md
98
- @node_modules/@xonic0506/sonic-ui/DESIGN.md
99
- @node_modules/@xonic0506/sonic-ui/AI-RULES.md
123
+ ```tsx
124
+ const toast = useToast();
125
+ toast.add({ title: 'API 키를 만들었어요', tone: 'success' });
100
126
  ```
101
127
 
102
- ## 디자인 lint 규칙 (선택)
128
+ ## AI와 함께 쓰기
129
+
130
+ > [!TIP]
131
+ > Claude Code를 쓰는 프로젝트라면 CLAUDE.md에 아래 두 줄을 추가해요. AI가 디자인 규칙을 읽고 sonic-ui 컴포넌트로 화면을 만들어요.
132
+ >
133
+ > ```md
134
+ > @node_modules/@xonic0506/sonic-ui/DESIGN.md
135
+ > @node_modules/@xonic0506/sonic-ui/AI-RULES.md
136
+ > ```
103
137
 
104
- 임의의 값 클래스, Tailwind 기본 팔레트, hex 색을 막는 ESLint flat config 조각을 제공해요. 켜는 것은 쓰는 쪽 설정에서 해요.
138
+ 임의의 값 클래스, Tailwind 기본 팔레트, hex 색을 막는 ESLint flat config도 제공해요. 켜는 것은 쓰는 쪽 설정에서 해요.
105
139
 
106
140
  ```js
107
141
  // eslint.config.js
@@ -111,15 +145,28 @@ export default [/* … */, sonicDesign];
111
145
 
112
146
  ## 지원 브라우저
113
147
 
114
- Tailwind v4 기준과 같아요: Chrome 111+, Edge 111+, Safari 16.4+, Firefox 128+. CSS `@layer`와 `@property`를 쓰기 때문이에요.
148
+ Chrome 111+, Edge 111+, Safari 16.4+, Firefox 128+. CSS `@layer`와 `@property`를 쓰기 때문에 Tailwind v4와 기준이 같아요.
115
149
 
116
150
  ## 개발
117
151
 
118
152
  ```bash
119
- npm run build # 토큰 생성 → JS(Vite) → 타입 → CSS(@layer sonic)
120
- npm test # Vitest
153
+ npm run build # 토큰 생성 → JS(Vite) → 타입 → CSS(@layer sonic)
154
+ npm test # Vitest
121
155
  npm run typecheck
122
- npm run playground # Figma Core 페이지와 비교하는 화면 (build 먼저)
156
+ npm run check:contrast # 글자와 바탕 대비 4.5:1 검사
157
+ npm run playground # Figma와 비교하는 화면 (build 먼저)
158
+ ```
159
+
160
+ ```text
161
+ .
162
+ ├── src/components/ # Core 컴포넌트
163
+ ├── src/patterns/ # Pattern 승격 기준 (두 번째 프로젝트에서 복사하게 될 때)
164
+ ├── scripts/ # Figma 토큰 추출, CSS 빌드, 대비 검사
165
+ ├── lint/ # 디자인 lint 규칙
166
+ ├── playground/ # Figma와 비교하는 화면
167
+ ├── fixtures/ # 쓰는 쪽 예제 (vite, Next.js)
168
+ └── DESIGN.md # 디자인 규칙 (패키지에 포함)
123
169
  ```
124
170
 
125
- 운영 규칙은 [CLAUDE.md](./CLAUDE.md)에 있어요.
171
+ > [!NOTE]
172
+ > 디자인을 바꿀 때는 Figma → DESIGN.md → 토큰 추출 → 코드 순서를 지켜요. 자세한 운영 규칙은 [CLAUDE.md](./CLAUDE.md)에 있어요.
@@ -0,0 +1,30 @@
1
+ import * as React from 'react';
2
+ export interface AccordionProps {
3
+ /** 여러 항목을 함께 열 수 있음 (기본 false) */
4
+ multiple?: boolean;
5
+ /** 열린 항목들의 value */
6
+ value?: string[];
7
+ defaultValue?: string[];
8
+ onValueChange?: (value: string[]) => void;
9
+ className?: string;
10
+ children?: React.ReactNode;
11
+ }
12
+ /** 닫힌 내용도 브라우저 페이지 찾기로 찾아 열 수 있어요 (hiddenUntilFound) */
13
+ export declare function Accordion({ onValueChange, className, ...props }: AccordionProps): React.JSX.Element;
14
+ export interface AccordionItemProps {
15
+ value?: string;
16
+ disabled?: boolean;
17
+ className?: string;
18
+ children?: React.ReactNode;
19
+ }
20
+ export declare function AccordionItem({ className, ...props }: AccordionItemProps): React.JSX.Element;
21
+ export interface AccordionTriggerProps {
22
+ className?: string;
23
+ children?: React.ReactNode;
24
+ }
25
+ export declare function AccordionTrigger({ className, children }: AccordionTriggerProps): React.JSX.Element;
26
+ export interface AccordionPanelProps {
27
+ className?: string;
28
+ children?: React.ReactNode;
29
+ }
30
+ export declare function AccordionPanel({ className, children }: AccordionPanelProps): React.JSX.Element;
@@ -0,0 +1,44 @@
1
+ "use client";
2
+ import { cn as e } from "../lib/cn.js";
3
+ import "react";
4
+ import { ChevronDown as t } from "lucide-react";
5
+ import { jsx as n, jsxs as r } from "react/jsx-runtime";
6
+ import { Accordion as i } from "@base-ui/react/accordion";
7
+ //#region src/components/accordion.tsx
8
+ function a({ onValueChange: t, className: r, ...a }) {
9
+ return /* @__PURE__ */ n(i.Root, {
10
+ ...a,
11
+ hiddenUntilFound: !0,
12
+ onValueChange: t && ((e) => t(e)),
13
+ className: e("sn:flex sn:flex-col sn:font-sans", r)
14
+ });
15
+ }
16
+ function o({ className: t, ...r }) {
17
+ return /* @__PURE__ */ n(i.Item, {
18
+ ...r,
19
+ className: e("sn:border-b sn:border-default sn:data-disabled:opacity-(--sn-opacity-disabled)", t)
20
+ });
21
+ }
22
+ function s({ className: a, children: o }) {
23
+ return /* @__PURE__ */ n(i.Header, {
24
+ className: "sn:m-0",
25
+ children: /* @__PURE__ */ r(i.Trigger, {
26
+ className: e("sn:group sn:flex sn:w-full sn:items-center sn:gap-2 sn:rounded-sm sn:border-0 sn:bg-transparent sn:px-0 sn:py-3 sn:text-left sn:font-sans sn:text-body-strong sn:text-default sn:cursor-pointer sn:focus-visible:outline-2 sn:focus-visible:outline-offset-2 sn:focus-visible:outline-focus sn:data-disabled:cursor-not-allowed", a),
27
+ children: [/* @__PURE__ */ n("span", {
28
+ className: "sn:min-w-0 sn:flex-1",
29
+ children: o
30
+ }), /* @__PURE__ */ n(t, { className: "sn:size-4 sn:shrink-0 sn:text-muted sn:transition-transform sn:duration-(--sn-duration-normal) sn:ease-standard sn:group-data-panel-open:rotate-180 sn:motion-reduce:transition-none" })]
31
+ })
32
+ });
33
+ }
34
+ function c({ className: t, children: r }) {
35
+ return /* @__PURE__ */ n(i.Panel, {
36
+ className: "sn:h-(--accordion-panel-height) sn:overflow-hidden sn:transition-[height] sn:duration-(--sn-duration-normal) sn:ease-standard sn:data-starting-style:h-0 sn:data-ending-style:h-0 sn:motion-reduce:transition-none",
37
+ children: /* @__PURE__ */ n("div", {
38
+ className: e("sn:pb-4 sn:font-sans sn:text-body sn:text-muted", t),
39
+ children: r
40
+ })
41
+ });
42
+ }
43
+ //#endregion
44
+ export { a as Accordion, o as AccordionItem, c as AccordionPanel, s as AccordionTrigger };
@@ -0,0 +1,57 @@
1
+ import * as React from 'react';
2
+ /** 바이트를 "2.4 MB"처럼 표시 */
3
+ export declare function formatBytes(bytes: number): string;
4
+ export interface FileRejection {
5
+ file: File;
6
+ reason: 'type' | 'size' | 'count';
7
+ /** 기본 문구. 바꾸려면 이 값 대신 직접 보여 주세요 */
8
+ message: string;
9
+ }
10
+ export interface FileDropzoneProps {
11
+ onFilesChange: (accepted: File[], rejected: FileRejection[]) => void;
12
+ /** 예: ".pdf", "image/*", "image/png,image/jpeg" */
13
+ accept?: string;
14
+ multiple?: boolean;
15
+ /** 남은 개수. multiple일 때만 */
16
+ maxFiles?: number;
17
+ /** 파일 하나의 최대 크기(바이트) */
18
+ maxSize?: number;
19
+ variant?: 'file' | 'image';
20
+ /** 영역 안 안내 문구. Field 밖에서 쓰면 버튼 이름이 돼요 */
21
+ title?: React.ReactNode;
22
+ description?: React.ReactNode;
23
+ invalid?: boolean;
24
+ disabled?: boolean;
25
+ name?: string;
26
+ id?: string;
27
+ className?: string;
28
+ }
29
+ /** Input처럼 Field로 감싸면 라벨·설명·에러·invalid·disabled가 연결돼요 */
30
+ export declare function FileDropzone({ onFilesChange, accept, multiple, maxFiles, maxSize, variant, title, description, invalid, disabled, name, id, className }: FileDropzoneProps): React.JSX.Element;
31
+ export interface FileItemData {
32
+ id: string;
33
+ name: string;
34
+ /** 바이트 */
35
+ size?: number;
36
+ /** MIME. 이미지 여부 판단 */
37
+ type?: string;
38
+ status: 'uploading' | 'done' | 'error';
39
+ /** 0~100 */
40
+ progress?: number;
41
+ /** 무엇이 실패했고 어떻게 고치는지 */
42
+ error?: string;
43
+ /** 완료된 파일을 열 주소 */
44
+ url?: string;
45
+ /** 썸네일: 서버 주소, data URL, blob URL 모두 가능 */
46
+ previewUrl?: string;
47
+ /** 방금 고른 로컬 파일. previewUrl이 없으면 blob URL을 만들어 미리보기에 써요 */
48
+ file?: File;
49
+ }
50
+ export interface FileListProps {
51
+ items: FileItemData[];
52
+ layout?: 'list' | 'grid';
53
+ onRemove?: (id: string) => void;
54
+ onRetry?: (id: string) => void;
55
+ className?: string;
56
+ }
57
+ export declare function FileList({ items, layout, onRemove, onRetry, className }: FileListProps): React.JSX.Element;
@@ -0,0 +1,238 @@
1
+ "use client";
2
+ import { cn as e } from "../lib/cn.js";
3
+ import { Button as t } from "./button.js";
4
+ import { Progress as n } from "./progress.js";
5
+ import { Link as r } from "./link.js";
6
+ import * as i from "react";
7
+ import { CircleAlert as a, File as o, Image as s, RefreshCw as c, Upload as l, X as u } from "lucide-react";
8
+ import { Fragment as d, jsx as f, jsxs as p } from "react/jsx-runtime";
9
+ import { Field as m } from "@base-ui/react/field";
10
+ //#region src/components/file-upload.tsx
11
+ function h(e) {
12
+ if (e < 1024) return `${e} B`;
13
+ let t = [
14
+ "KB",
15
+ "MB",
16
+ "GB"
17
+ ], n = e / 1024, r = 0;
18
+ for (; n >= 1024 && r < t.length - 1;) n /= 1024, r++;
19
+ return `${Number(n.toFixed(1))} ${t[r]}`;
20
+ }
21
+ function g(e, t) {
22
+ if (!t) return !0;
23
+ let n = e.name.toLowerCase(), r = e.type.toLowerCase();
24
+ return t.split(",").map((e) => e.trim().toLowerCase()).filter(Boolean).some((e) => e.startsWith(".") ? n.endsWith(e) : e.endsWith("/*") ? r.startsWith(e.slice(0, -1)) : r === e);
25
+ }
26
+ function _(e, { accept: t, maxSize: n, maxFiles: r, multiple: i }) {
27
+ let a = i ? r ?? Infinity : 1, o = [], s = [];
28
+ for (let r of e) g(r, t) ? n !== void 0 && r.size > n ? s.push({
29
+ file: r,
30
+ reason: "size",
31
+ message: `${r.name}: ${h(n)}보다 큰 파일은 올릴 수 없어요.`
32
+ }) : o.length >= a ? s.push({
33
+ file: r,
34
+ reason: "count",
35
+ message: a <= 0 ? "더 올릴 수 없어요. 파일을 삭제한 뒤 다시 골라 주세요." : `${a}개까지 올릴 수 있어요.`
36
+ }) : o.push(r) : s.push({
37
+ file: r,
38
+ reason: "type",
39
+ message: `${r.name}: 올릴 수 없는 형식이에요. ${t} 파일만 올려 주세요.`
40
+ });
41
+ return {
42
+ accepted: o,
43
+ rejected: s
44
+ };
45
+ }
46
+ function v({ onFilesChange: t, accept: n, multiple: r, maxFiles: a, maxSize: o, variant: c = "file", title: u, description: h, invalid: g, disabled: v, name: y, id: b, className: x }) {
47
+ let S = i.useRef(null), [C, w] = i.useState(!1), [T, E] = i.useState(), D = i.useId(), O = (e) => {
48
+ if (!e.length) return;
49
+ let { accepted: i, rejected: s } = _(e, {
50
+ accept: n,
51
+ maxSize: o,
52
+ maxFiles: a,
53
+ multiple: r
54
+ });
55
+ E(s.length ? s[0].message + (s.length > 1 ? ` (외 ${s.length - 1}개)` : "") : void 0), t(i, s);
56
+ }, k = g || !!T, A = c === "image" ? s : l;
57
+ return /* @__PURE__ */ p(d, { children: [/* @__PURE__ */ p(m.Control, {
58
+ render: /* @__PURE__ */ f("button", { type: "button" }),
59
+ id: b,
60
+ disabled: v,
61
+ "aria-describedby": D,
62
+ "data-dragging": C ? "" : void 0,
63
+ "aria-invalid": g || void 0,
64
+ "data-invalid": k ? "" : void 0,
65
+ onClick: () => S.current?.click(),
66
+ onDragEnter: (e) => {
67
+ e.preventDefault(), v || w(!0);
68
+ },
69
+ onDragOver: (e) => e.preventDefault(),
70
+ onDragLeave: (e) => {
71
+ e.currentTarget.contains(e.relatedTarget) || w(!1);
72
+ },
73
+ onDrop: (e) => {
74
+ e.preventDefault(), w(!1), v || O([...e.dataTransfer.files]);
75
+ },
76
+ className: e("sn:group sn:box-border sn:flex sn:w-full sn:flex-col sn:items-center sn:gap-2 sn:rounded-lg sn:border sn:border-dashed sn:border-strong sn:bg-surface sn:px-4 sn:py-6 sn:font-sans sn:text-center sn:cursor-pointer sn:transition-colors sn:duration-(--sn-duration-fast) sn:ease-standard sn:hover:not-disabled:bg-subtle sn:data-dragging:border-focus sn:data-dragging:bg-subtle sn:data-invalid:border-danger sn:aria-invalid:border-danger sn:focus-visible:outline-2 sn:focus-visible:outline-offset-2 sn:focus-visible:outline-focus sn:disabled:cursor-not-allowed sn:disabled:opacity-(--sn-opacity-disabled) sn:motion-reduce:transition-none", x),
77
+ children: [
78
+ /* @__PURE__ */ f(A, { className: "sn:size-6 sn:text-muted sn:group-data-invalid:text-danger sn:group-aria-invalid:text-danger" }),
79
+ /* @__PURE__ */ f("span", {
80
+ className: "sn:text-body-strong sn:text-default",
81
+ children: u ?? (c === "image" ? "이미지를 끌어다 놓거나 눌러서 고르세요" : "파일을 끌어다 놓거나 눌러서 고르세요")
82
+ }),
83
+ /* @__PURE__ */ f("span", {
84
+ id: D,
85
+ "aria-live": "polite",
86
+ className: e("sn:text-caption", T ? "sn:text-danger" : "sn:text-muted"),
87
+ children: T ?? h
88
+ })
89
+ ]
90
+ }), /* @__PURE__ */ f("input", {
91
+ ref: S,
92
+ type: "file",
93
+ hidden: !0,
94
+ name: y,
95
+ accept: n,
96
+ multiple: r,
97
+ disabled: v,
98
+ onChange: (e) => {
99
+ O([...e.target.files ?? []]), e.target.value = "";
100
+ }
101
+ })] });
102
+ }
103
+ function y(e) {
104
+ let [t, n] = i.useState(), r = (e.type ?? e.file?.type ?? "").startsWith("image/");
105
+ return i.useEffect(() => {
106
+ if (e.previewUrl || !e.file || !r) return;
107
+ let t = URL.createObjectURL(e.file);
108
+ return n(t), () => URL.revokeObjectURL(t);
109
+ }, [
110
+ e.previewUrl,
111
+ e.file,
112
+ r
113
+ ]), e.previewUrl ?? t;
114
+ }
115
+ function b({ items: t, layout: n = "list", onRemove: r, onRetry: i, className: a }) {
116
+ let o = n === "grid" ? S : x;
117
+ return /* @__PURE__ */ f("ul", {
118
+ className: e("sn:m-0 sn:list-none sn:p-0 sn:font-sans", n === "grid" ? "sn:flex sn:flex-wrap sn:gap-3" : "sn:flex sn:flex-col sn:gap-2", a),
119
+ children: t.map((e) => /* @__PURE__ */ f(o, {
120
+ item: e,
121
+ onRemove: r,
122
+ onRetry: i
123
+ }, e.id))
124
+ });
125
+ }
126
+ function x({ item: t, onRemove: i, onRetry: a }) {
127
+ let s = t.status === "error";
128
+ return /* @__PURE__ */ p("li", {
129
+ className: e("sn:box-border sn:flex sn:items-center sn:gap-3 sn:rounded-md sn:border sn:bg-surface sn:p-3", s ? "sn:border-danger" : "sn:border-default"),
130
+ children: [
131
+ /* @__PURE__ */ f("span", {
132
+ className: "sn:flex sn:size-8 sn:shrink-0 sn:items-center sn:justify-center sn:rounded-md sn:bg-subtle",
133
+ children: /* @__PURE__ */ f(o, { className: e("sn:size-4", s ? "sn:text-danger" : "sn:text-muted") })
134
+ }),
135
+ /* @__PURE__ */ p("div", {
136
+ className: "sn:flex sn:min-w-0 sn:flex-1 sn:flex-col sn:gap-1",
137
+ children: [
138
+ /* @__PURE__ */ p("div", {
139
+ className: "sn:flex sn:items-center sn:justify-between sn:gap-2",
140
+ children: [t.status === "done" && t.url ? /* @__PURE__ */ f(r, {
141
+ href: t.url,
142
+ target: "_blank",
143
+ rel: "noopener noreferrer",
144
+ className: "sn:min-w-0 sn:truncate sn:text-body",
145
+ children: t.name
146
+ }) : /* @__PURE__ */ f("span", {
147
+ className: "sn:min-w-0 sn:truncate sn:text-body sn:text-default",
148
+ children: t.name
149
+ }), /* @__PURE__ */ f("span", {
150
+ className: "sn:shrink-0 sn:font-mono sn:text-caption sn:text-muted",
151
+ children: t.status === "uploading" ? `${Math.round(t.progress ?? 0)}%` : t.size === void 0 ? null : h(t.size)
152
+ })]
153
+ }),
154
+ t.status === "uploading" && /* @__PURE__ */ f(n, {
155
+ value: t.progress ?? 0,
156
+ "aria-label": `${t.name} 올리는 중`
157
+ }),
158
+ s && t.error && /* @__PURE__ */ f("span", {
159
+ className: "sn:text-caption sn:text-danger",
160
+ children: t.error
161
+ })
162
+ ]
163
+ }),
164
+ /* @__PURE__ */ f(C, {
165
+ item: t,
166
+ onRemove: i,
167
+ onRetry: a,
168
+ variant: "ghost"
169
+ })
170
+ ]
171
+ });
172
+ }
173
+ function S({ item: t, onRemove: r, onRetry: i }) {
174
+ let o = t.status === "error", c = y(t);
175
+ return /* @__PURE__ */ p("li", {
176
+ className: "sn:flex sn:w-24 sn:flex-col sn:gap-1",
177
+ children: [
178
+ /* @__PURE__ */ p("div", {
179
+ className: e("sn:relative sn:box-border sn:flex sn:size-24 sn:flex-col sn:justify-between sn:overflow-hidden sn:rounded-md sn:border sn:bg-subtle sn:p-1", o ? "sn:border-danger" : "sn:border-default"),
180
+ children: [
181
+ c && !o ? /* @__PURE__ */ f("img", {
182
+ src: c,
183
+ alt: t.name,
184
+ className: "sn:absolute sn:inset-0 sn:size-full sn:object-cover"
185
+ }) : /* @__PURE__ */ f("span", {
186
+ className: "sn:absolute sn:inset-0 sn:flex sn:items-center sn:justify-center",
187
+ children: o ? /* @__PURE__ */ f(a, { className: "sn:size-6 sn:text-danger" }) : /* @__PURE__ */ f(s, { className: "sn:size-6 sn:text-muted" })
188
+ }),
189
+ /* @__PURE__ */ f("div", {
190
+ className: "sn:relative sn:flex sn:justify-end",
191
+ children: /* @__PURE__ */ f(C, {
192
+ item: t,
193
+ onRemove: r,
194
+ onRetry: i,
195
+ variant: "secondary"
196
+ })
197
+ }),
198
+ t.status === "uploading" && /* @__PURE__ */ f("div", {
199
+ className: "sn:relative",
200
+ children: /* @__PURE__ */ f(n, {
201
+ value: t.progress ?? 0,
202
+ "aria-label": `${t.name} 올리는 중`
203
+ })
204
+ })
205
+ ]
206
+ }),
207
+ /* @__PURE__ */ f("span", {
208
+ className: e("sn:truncate sn:text-caption", o ? "sn:text-danger" : "sn:text-muted"),
209
+ title: t.name,
210
+ children: t.name
211
+ }),
212
+ o && t.error && /* @__PURE__ */ f("span", {
213
+ className: "sn:sr-only",
214
+ children: t.error
215
+ })
216
+ ]
217
+ });
218
+ }
219
+ function C({ item: e, onRemove: n, onRetry: r, variant: i }) {
220
+ return /* @__PURE__ */ p("div", {
221
+ className: "sn:flex sn:shrink-0",
222
+ children: [e.status === "error" && r && /* @__PURE__ */ f(t, {
223
+ size: "icon-sm",
224
+ variant: i,
225
+ "aria-label": `${e.name} 다시 시도`,
226
+ onClick: () => r(e.id),
227
+ children: /* @__PURE__ */ f(c, {})
228
+ }), n && /* @__PURE__ */ f(t, {
229
+ size: "icon-sm",
230
+ variant: i,
231
+ "aria-label": `${e.name} 삭제`,
232
+ onClick: () => n(e.id),
233
+ children: /* @__PURE__ */ f(u, {})
234
+ })]
235
+ });
236
+ }
237
+ //#endregion
238
+ export { v as FileDropzone, b as FileList, h as formatBytes };
@@ -0,0 +1,9 @@
1
+ import * as React from 'react';
2
+ export interface LinkProps extends Omit<React.AnchorHTMLAttributes<HTMLAnchorElement>, 'className'> {
3
+ /** 새 탭으로 열고 external-link 아이콘을 붙여요 */
4
+ external?: boolean;
5
+ className?: string;
6
+ /** 다른 링크 컴포넌트로 렌더링 (예: render={<NextLink href="/docs" />}) */
7
+ render?: React.ReactElement<Record<string, unknown>>;
8
+ }
9
+ export declare const Link: React.ForwardRefExoticComponent<LinkProps & React.RefAttributes<HTMLAnchorElement>>;
@@ -0,0 +1,30 @@
1
+ "use client";
2
+ import { cn as e } from "../lib/cn.js";
3
+ import * as t from "react";
4
+ import { ExternalLink as n } from "lucide-react";
5
+ import { Fragment as r, jsx as i, jsxs as a } from "react/jsx-runtime";
6
+ //#region src/components/link.tsx
7
+ var o = "sn:rounded-sm sn:text-default sn:underline sn:underline-offset-2 sn:transition-colors sn:duration-(--sn-duration-fast) sn:ease-standard sn:hover:text-muted sn:focus-visible:outline-2 sn:focus-visible:outline-offset-2 sn:focus-visible:outline-focus sn:motion-reduce:transition-none", s = t.forwardRef(function({ external: s, className: c, render: l, children: u, ...d }, f) {
8
+ let p = s ? /* @__PURE__ */ a(r, { children: [
9
+ u,
10
+ /* @__PURE__ */ i(n, { className: "sn:ml-1 sn:inline-block sn:size-4 sn:align-text-bottom" }),
11
+ /* @__PURE__ */ i("span", {
12
+ className: "sn:sr-only",
13
+ children: "(새 탭에서 열림)"
14
+ })
15
+ ] }) : u, m = {
16
+ ref: f,
17
+ className: e(o, c),
18
+ ...s && {
19
+ target: "_blank",
20
+ rel: "noopener noreferrer"
21
+ },
22
+ ...d
23
+ };
24
+ return l ? t.cloneElement(l, m, p) : /* @__PURE__ */ i("a", {
25
+ ...m,
26
+ children: p
27
+ });
28
+ });
29
+ //#endregion
30
+ export { s as Link };
@@ -0,0 +1,24 @@
1
+ import * as React from 'react';
2
+ /** 넓으면 가로로 스크롤해요 */
3
+ export declare const Table: React.ForwardRefExoticComponent<Omit<React.HTMLAttributes<HTMLTableElement>, "className"> & {
4
+ className?: string;
5
+ } & React.RefAttributes<HTMLTableElement>>;
6
+ export declare const TableHeader: React.ForwardRefExoticComponent<Omit<React.HTMLAttributes<HTMLTableSectionElement>, "className"> & {
7
+ className?: string;
8
+ } & React.RefAttributes<HTMLTableSectionElement>>;
9
+ export declare const TableBody: React.ForwardRefExoticComponent<Omit<React.HTMLAttributes<HTMLTableSectionElement>, "className"> & {
10
+ className?: string;
11
+ } & React.RefAttributes<HTMLTableSectionElement>>;
12
+ export declare const TableRow: React.ForwardRefExoticComponent<Omit<React.HTMLAttributes<HTMLTableRowElement>, "className"> & {
13
+ className?: string;
14
+ } & React.RefAttributes<HTMLTableRowElement>>;
15
+ export declare const TableHead: React.ForwardRefExoticComponent<Omit<React.TdHTMLAttributes<HTMLTableCellElement>, "align" | "className"> & {
16
+ /** 숫자 열은 end */
17
+ align?: 'start' | 'end';
18
+ className?: string;
19
+ } & React.RefAttributes<HTMLTableCellElement>>;
20
+ export declare const TableCell: React.ForwardRefExoticComponent<Omit<React.TdHTMLAttributes<HTMLTableCellElement>, "align" | "className"> & {
21
+ /** 숫자 열은 end */
22
+ align?: 'start' | 'end';
23
+ className?: string;
24
+ } & React.RefAttributes<HTMLTableCellElement>>;
@@ -0,0 +1,46 @@
1
+ "use client";
2
+ import { cn as e } from "../lib/cn.js";
3
+ import * as t from "react";
4
+ import { jsx as n } from "react/jsx-runtime";
5
+ //#region src/components/table.tsx
6
+ var r = t.forwardRef(function({ className: t, ...r }, i) {
7
+ return /* @__PURE__ */ n("div", {
8
+ 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
13
+ })
14
+ });
15
+ }), i = t.forwardRef(function(e, t) {
16
+ return /* @__PURE__ */ n("thead", {
17
+ ref: t,
18
+ ...e
19
+ });
20
+ }), a = t.forwardRef(function(e, t) {
21
+ return /* @__PURE__ */ n("tbody", {
22
+ ref: t,
23
+ ...e
24
+ });
25
+ }), o = t.forwardRef(function({ className: e, ...t }, r) {
26
+ return /* @__PURE__ */ n("tr", {
27
+ ref: r,
28
+ className: e,
29
+ ...t
30
+ });
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,
34
+ scope: "col",
35
+ className: e(s, "sn:text-label sn:text-muted", t === "end" ? "sn:text-right" : "sn:text-left", r),
36
+ ...i
37
+ });
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),
42
+ ...i
43
+ });
44
+ });
45
+ //#endregion
46
+ export { r as Table, a as TableBody, l as TableCell, c as TableHead, i as TableHeader, o as TableRow };
@@ -0,0 +1,36 @@
1
+ import * as React from 'react';
2
+ interface ToggleGroupBaseProps {
3
+ disabled?: boolean;
4
+ className?: string;
5
+ 'aria-label'?: string;
6
+ children?: React.ReactNode;
7
+ }
8
+ /** 단일은 항상 하나가 눌려 있어요(다시 눌러도 꺼지지 않음). multiple이면 값이 배열이에요 */
9
+ export type ToggleGroupProps = ToggleGroupBaseProps & ({
10
+ multiple?: false;
11
+ value?: string;
12
+ defaultValue?: string;
13
+ onValueChange?: (value: string) => void;
14
+ } | {
15
+ multiple: true;
16
+ value?: string[];
17
+ defaultValue?: string[];
18
+ onValueChange?: (value: string[]) => void;
19
+ });
20
+ export declare function ToggleGroup(props: ToggleGroupProps): React.JSX.Element;
21
+ interface ToggleGroupItemBaseProps {
22
+ value: string;
23
+ disabled?: boolean;
24
+ className?: string;
25
+ children?: React.ReactNode;
26
+ }
27
+ /** 아이콘만 있는 항목은 읽을 글자가 없어서 aria-label이 필수예요 */
28
+ export type ToggleGroupItemProps = ToggleGroupItemBaseProps & ({
29
+ iconOnly?: false;
30
+ 'aria-label'?: string;
31
+ } | {
32
+ iconOnly: true;
33
+ 'aria-label': string;
34
+ });
35
+ export declare const ToggleGroupItem: React.ForwardRefExoticComponent<ToggleGroupItemProps & React.RefAttributes<HTMLButtonElement>>;
36
+ export {};
@@ -0,0 +1,44 @@
1
+ "use client";
2
+ import { cn as e } from "../lib/cn.js";
3
+ import * as t from "react";
4
+ import { jsx as n } from "react/jsx-runtime";
5
+ import { ToggleGroup as r } from "@base-ui/react/toggle-group";
6
+ import { Toggle as i } from "@base-ui/react/toggle";
7
+ //#region src/components/toggle-group.tsx
8
+ var a = "sn:inline-flex sn:items-center sn:rounded-md sn:bg-subtle sn:p-1";
9
+ function o(i) {
10
+ let { disabled: o, className: s, children: c } = i, [l, u] = t.useState(i.multiple ? void 0 : i.defaultValue), d = {
11
+ disabled: o,
12
+ "aria-label": i["aria-label"],
13
+ className: e(a, s)
14
+ };
15
+ if (i.multiple) return /* @__PURE__ */ n(r, {
16
+ ...d,
17
+ multiple: !0,
18
+ value: i.value,
19
+ defaultValue: i.defaultValue,
20
+ onValueChange: i.onValueChange && ((e) => i.onValueChange(e)),
21
+ children: c
22
+ });
23
+ let f = i.value === void 0 ? l : i.value;
24
+ return /* @__PURE__ */ n(r, {
25
+ ...d,
26
+ value: f === void 0 ? [] : [f],
27
+ onValueChange: (e) => {
28
+ e.length && (u(e[0]), i.onValueChange?.(e[0]));
29
+ },
30
+ children: c
31
+ });
32
+ }
33
+ var s = "sn:box-border sn:inline-flex sn:h-6 sn:items-center sn:justify-center sn:gap-1 sn:rounded-sm sn:border sn:border-transparent sn:bg-transparent sn:px-2 sn:font-sans sn:text-label sn:text-muted sn:cursor-pointer sn:select-none sn:transition-colors sn:duration-(--sn-duration-fast) sn:ease-standard sn:hover:not-data-disabled:text-default sn:data-pressed:border-default sn:data-pressed:bg-surface sn:data-pressed:text-default sn:focus-visible:outline-2 sn:focus-visible:-outline-offset-2 sn:focus-visible:outline-focus sn:data-disabled:cursor-not-allowed sn:data-disabled:opacity-(--sn-opacity-disabled) sn:motion-reduce:transition-none sn:[&_svg]:size-4 sn:[&_svg]:shrink-0", c = t.forwardRef(function({ value: t, disabled: r, iconOnly: a, className: o, children: c, ...l }, u) {
34
+ return /* @__PURE__ */ n(i, {
35
+ ref: u,
36
+ value: t,
37
+ disabled: r,
38
+ "aria-label": l["aria-label"],
39
+ className: e(s, a && "sn:w-6 sn:px-0", o),
40
+ children: c
41
+ });
42
+ });
43
+ //#endregion
44
+ export { o as ToggleGroup, c as ToggleGroupItem };
package/dist/index.d.ts CHANGED
@@ -22,3 +22,8 @@ export { Separator, type SeparatorProps } from './components/separator';
22
22
  export { ToastProvider, useToast, type ToastProviderProps, type ToastOptions, type ToastTone } from './components/toast';
23
23
  export { Badge, type BadgeProps } from './components/badge';
24
24
  export { Progress, type ProgressProps } from './components/progress';
25
+ export { Link, type LinkProps } from './components/link';
26
+ export { Table, TableHeader, TableBody, TableRow, TableHead, TableCell } from './components/table';
27
+ export { ToggleGroup, ToggleGroupItem, type ToggleGroupProps, type ToggleGroupItemProps } from './components/toggle-group';
28
+ export { Accordion, AccordionItem, AccordionTrigger, AccordionPanel, type AccordionProps, type AccordionItemProps } from './components/accordion';
29
+ export { FileDropzone, FileList, formatBytes, type FileDropzoneProps, type FileListProps, type FileItemData, type FileRejection } from './components/file-upload';
package/dist/index.js CHANGED
@@ -23,4 +23,9 @@ import { Separator as Z } from "./components/separator.js";
23
23
  import { ToastProvider as Q, useToast as $ } from "./components/toast.js";
24
24
  import { Badge as ee } from "./components/badge.js";
25
25
  import { Progress as te } from "./components/progress.js";
26
- export { O as AlertDialog, k as AlertDialogClose, A as AlertDialogDescription, j as AlertDialogFooter, M as AlertDialogPopup, N as AlertDialogTitle, P as AlertDialogTrigger, g as Avatar, ee as Badge, e as Button, u as Card, d as CardBody, f as CardDescription, p as CardFooter, m as CardHeader, h as CardTitle, r as Checkbox, i as CheckboxGroup, x as Dialog, S as DialogClose, C as DialogDescription, w as DialogFooter, T as DialogPopup, E as DialogTitle, D as DialogTrigger, y as EmptyState, b as ErrorState, _ as Field, t as Input, F as Menu, I as MenuItem, L as MenuPopup, R as MenuSeparator, z as MenuTrigger, B as Popover, V as PopoverClose, H as PopoverDescription, U as PopoverPopup, W as PopoverTitle, G as PopoverTrigger, te as Progress, a as Radio, o as RadioGroup, c as Select, Z as Separator, v as Skeleton, s as Switch, q as Tab, J as Tabs, Y as TabsList, X as TabsPanel, l as Text, n as Textarea, Q as ToastProvider, K as Tooltip, $ as useToast };
26
+ import { Link as ne } from "./components/link.js";
27
+ import { Table as re, TableBody as ie, TableCell as ae, TableHead as oe, TableHeader as se, TableRow as ce } from "./components/table.js";
28
+ import { ToggleGroup as le, ToggleGroupItem as ue } from "./components/toggle-group.js";
29
+ import { Accordion as de, AccordionItem as fe, AccordionPanel as pe, AccordionTrigger as me } from "./components/accordion.js";
30
+ import { FileDropzone as he, FileList as ge, formatBytes as _e } from "./components/file-upload.js";
31
+ export { de as Accordion, fe as AccordionItem, pe as AccordionPanel, me as AccordionTrigger, O as AlertDialog, k as AlertDialogClose, A as AlertDialogDescription, j as AlertDialogFooter, M as AlertDialogPopup, N as AlertDialogTitle, P as AlertDialogTrigger, g as Avatar, ee as Badge, e as Button, u as Card, d as CardBody, f as CardDescription, p as CardFooter, m as CardHeader, h as CardTitle, r as Checkbox, i as CheckboxGroup, x as Dialog, S as DialogClose, C as DialogDescription, w as DialogFooter, T as DialogPopup, E as DialogTitle, D as DialogTrigger, y as EmptyState, b as ErrorState, _ as Field, he as FileDropzone, ge as FileList, t as Input, ne as Link, F as Menu, I as MenuItem, L as MenuPopup, R as MenuSeparator, z as MenuTrigger, B as Popover, V as PopoverClose, H as PopoverDescription, U as PopoverPopup, W as PopoverTitle, G as PopoverTrigger, te as Progress, a as Radio, o as RadioGroup, c as Select, Z as Separator, v as Skeleton, s as Switch, q as Tab, re as Table, ie as TableBody, ae as TableCell, oe as TableHead, se as TableHeader, ce as TableRow, J as Tabs, Y as TabsList, X as TabsPanel, l as Text, n as Textarea, Q as ToastProvider, le as ToggleGroup, ue as ToggleGroupItem, K as Tooltip, _e as formatBytes, $ as useToast };
package/dist/styles.css CHANGED
@@ -1,3 +1,3 @@
1
1
  /*! sonic-ui | 전체가 @layer sonic 안에 있어요. 쓰는 쪽: @layer theme, base, sonic, components, utilities; */
2
- @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-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 theme;@layer utilities{.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-0{margin-bottom:0}.sn\:box-border{box-sizing:border-box}.sn\:block{display:block}.sn\:flex{display:flex}.sn\:hidden{display:none}.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-full{width:100%;height:100%}.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-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-full{height:100%}.sn\:h-px{height:1px}.sn\:max-h-\(--available-height\){max-height:var(--available-height)}.sn\:min-h-20{min-height:calc(var(--sn-space-1) * 20)}.sn\:w-\(--active-tab-width\){width:var(--active-tab-width)}.sn\:w-7{width:calc(var(--sn-space-1) * 7)}.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-px{width:1px}.sn\:max-w-\[calc\(100vw-2\*var\(--sn-space-4\)\)\]{max-width:calc(100vw - 2 * var(--sn-space-4))}.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\:flex-1{flex:1}.sn\:shrink-0{flex-shrink:0}.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\: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-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-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-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-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-1{padding-block:var(--sn-space-1)}.sn\:py-2{padding-block:calc(var(--sn-space-1) * 2)}.sn\:py-12{padding-block:calc(var(--sn-space-1) * 12)}.sn\:pt-1{padding-top:var(--sn-space-1)}.sn\:text-center{text-align:center}.sn\:text-left{text-align:left}.sn\:align-middle{vertical-align:middle}.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\:opacity-\(--sn-opacity-disabled\){opacity:var(--sn-opacity-disabled)}.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-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\: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\: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-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-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}}.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", 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-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 theme;@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-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-full{height:100%}.sn\:h-px{height:1px}.sn\:max-h-\(--available-height\){max-height:var(--available-height)}.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-px{width:1px}.sn\:max-w-\[calc\(100vw-2\*var\(--sn-space-4\)\)\]{max-width:calc(100vw - 2 * var(--sn-space-4))}.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\: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\: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\:pt-1{padding-top:var(--sn-space-1)}.sn\:pb-4{padding-bottom:calc(var(--sn-space-1) * 4)}.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}}.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", 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)}}}
package/package.json CHANGED
@@ -1,8 +1,13 @@
1
1
  {
2
2
  "name": "@xonic0506/sonic-ui",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "Figma-first personal design system (Base UI + Tailwind v4, prebuilt CSS)",
5
5
  "license": "MIT",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git+https://github.com/sonic0506/sonic-design-system.git"
9
+ },
10
+ "homepage": "https://github.com/sonic0506/sonic-design-system#readme",
6
11
  "type": "module",
7
12
  "sideEffects": [
8
13
  "**/*.css"