@xonic0506/sonic-ui 0.0.0-stage → 0.1.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/AI-RULES.md +18 -0
- package/DESIGN.md +313 -0
- package/LICENSE +21 -0
- package/README.md +122 -2
- package/dist/components/avatar.d.ts +10 -0
- package/dist/components/avatar.js +27 -0
- package/dist/components/button.d.ts +9 -0
- package/dist/components/button.js +39 -0
- package/dist/components/card.d.ts +24 -0
- package/dist/components/card.js +15 -0
- package/dist/components/dialog.d.ts +28 -0
- package/dist/components/dialog.js +49 -0
- package/dist/components/input.d.ts +7 -0
- package/dist/components/input.js +16 -0
- package/dist/components/text.d.ts +10 -0
- package/dist/components/text.js +39 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +8 -0
- package/dist/lib/cn.d.ts +1 -0
- package/dist/lib/cn.js +19 -0
- package/dist/styles.css +3 -0
- package/dist/tailwind.css +57 -0
- package/lint/design-rules.js +25 -0
- package/package.json +60 -4
package/AI-RULES.md
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# AI-RULES — sonic-ui를 쓰는 프로젝트의 AI 작업 규칙
|
|
2
|
+
|
|
3
|
+
이 파일은 sonic-ui를 설치한 프로젝트에서 Claude가 따를 규칙이에요. 프로젝트의 CLAUDE.md에 아래 두 줄을 넣어 불러와요.
|
|
4
|
+
|
|
5
|
+
```md
|
|
6
|
+
@node_modules/@xonic0506/sonic-ui/DESIGN.md
|
|
7
|
+
@node_modules/@xonic0506/sonic-ui/AI-RULES.md
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
## 규칙
|
|
11
|
+
|
|
12
|
+
1. **DESIGN.md가 기준이에요.** 색, 간격, radius, 글자, 모션은 DESIGN.md의 토큰만 써요. 임의의 값(`p-[7px]`, `#3355ff`, `text-[15px]`)과 Tailwind 기본 팔레트(`bg-blue-500`)는 쓰지 않아요.
|
|
13
|
+
2. **UI는 sonic-ui 컴포넌트로 조립해요.** Button, Input, Text, Card, Avatar, Dialog가 있는 역할은 직접 만들지 않아요. 없는 역할이 필요하면 먼저 사용자에게 알려요(라이브러리에 추가할지 판단이 필요해요).
|
|
14
|
+
3. **frontend-design은 제한해서 써요.** 레이아웃과 문구 판단에만 참고하고, 새 색·새 폰트·새 토큰 제안은 따르지 않아요. 컴포넌트를 구현할 때는 쓰지 않아요.
|
|
15
|
+
4. **데이터 화면은 4가지 상태를 모두 만들어요.** 정상, 로딩, 빈 상태, 에러. Figma에 4가지가 없으면 디자인부터 요청해요(DESIGN.md §8).
|
|
16
|
+
5. **Domain 컴포넌트는 프로젝트에 둬요.** 도메인 타입, API 호출, 비즈니스 규칙을 아는 컴포넌트는 sonic-ui에 넣지 않아요. 같은 형태를 두 번째 프로젝트에서 복사하게 되면 Pattern 승격을 제안해요(DESIGN.md §7).
|
|
17
|
+
6. **작업 흐름:** 페이지 요청 → DESIGN.md 기준 Figma 디자인 → Figma 기준 코딩. Figma 컴포넌트는 DESIGN.md §11 매핑표의 코드 컴포넌트로 옮겨요.
|
|
18
|
+
7. **덮어쓰기는 className으로만 해요.** 쓰는 쪽 Tailwind 클래스가 sonic-ui 기본 스타일을 이겨요(layer 순서 때문). sonic-ui 내부 클래스(`sn:*`)를 쓰는 쪽 코드에서 쓰지 않아요.
|
package/DESIGN.md
ADDED
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
# Sonic Design System — DESIGN.md
|
|
2
|
+
|
|
3
|
+
Sonic(`sonic-ui`)의 디자인 규칙이에요. Figma에서 디자인할 때, 그리고 코드를 짤 때 모두 이 문서를 따릅니다.
|
|
4
|
+
|
|
5
|
+
- 원본: Figma [Sonic Design System](https://www.figma.com/design/XifnZsjqJWJqFmAmrM6p3u). 값이 다르면 Figma가 맞아요.
|
|
6
|
+
- 방향: **Cobalt Instrument**. 계기판처럼 정밀하고 촘촘한 화면이에요. 차가운 회색을 바탕으로 쓰고, 행동을 유도하는 곳에만 코발트를 써요.
|
|
7
|
+
|
|
8
|
+
## 1. 원칙
|
|
9
|
+
|
|
10
|
+
1. **토큰 밖의 값은 없다.** 색, 간격, radius, 글자 크기는 모두 아래 표에서 고릅니다. 임의의 값(`#3355ff`, `13px`, `p-[7px]`)은 쓰지 않아요.
|
|
11
|
+
2. **강조는 한 곳에만.** 한 화면에서 `primary` 버튼은 하나만 둬요. 나머지는 `secondary`나 `ghost`를 씁니다.
|
|
12
|
+
3. **숫자는 고정폭.** 수치, ID, 키 값은 `mono` 계열 스타일로 써서 자리를 맞춰요.
|
|
13
|
+
4. **그림자 대신 선.** 높낮이는 1px 테두리(`color/border/default`)와 배경 단계(canvas → surface → subtle)로 구분해요. 그림자 토큰은 없어요.
|
|
14
|
+
5. **움직임은 사용자 행동에 대한 응답만.** 열기, 닫기, hover처럼 사용자가 일으킨 변화에만 모션을 넣고, `prefers-reduced-motion`을 지켜요.
|
|
15
|
+
|
|
16
|
+
## 2. 이름 규칙
|
|
17
|
+
|
|
18
|
+
Figma Variable 이름이 그대로 CSS 변수가 돼요. `/`는 `-`로 바꾸고 앞에 `--sn-`를 붙입니다.
|
|
19
|
+
|
|
20
|
+
| Figma Variable | CSS 변수 | 쓰는 쪽 Tailwind 클래스 | 라이브러리 내부 |
|
|
21
|
+
|---|---|---|---|
|
|
22
|
+
| `color/bg/primary` | `--sn-color-bg-primary` | `bg-primary` | `sn:bg-primary` |
|
|
23
|
+
| `color/text/muted` | `--sn-color-text-muted` | `text-muted` | `sn:text-muted` |
|
|
24
|
+
| `color/border/default` | `--sn-color-border-default` | `border-default`, `outline-default` | `sn:border-default` |
|
|
25
|
+
| `space/4` | `--sn-space-4` | `p-4`, `gap-4` (1단위 = `space/1` = 4px) | `sn:p-4` |
|
|
26
|
+
| `radius/md` | `--sn-radius-md` | `rounded-md` | `sn:rounded-md` |
|
|
27
|
+
| 텍스트 스타일 `body` | `--sn-text-body` (+ `--line-height`, `--font-weight`, `--letter-spacing`) | `text-body` | `sn:text-body` |
|
|
28
|
+
| 텍스트 스타일 `mono` / `mono-caption` | `--sn-font-mono` | `font-mono text-body` / `font-mono text-caption` | 앞에 `sn:` |
|
|
29
|
+
| `duration/fast`, `easing/standard` | `--sn-duration-fast`, `--sn-easing-standard` | `duration-(--sn-duration-fast) ease-standard` | 앞에 `sn:` |
|
|
30
|
+
|
|
31
|
+
- 쓰는 쪽 클래스는 `@xonic0506/sonic-ui/tailwind.css`를 import해야 생겨요(README 참고). Tailwind가 없는 프로젝트는 `var(--sn-…)`를 직접 써요.
|
|
32
|
+
- 간격은 `space/*` 표에 있는 단계(0·1·2·3·4·5·6·7·8·10·12·16)만 써요. `p-9`처럼 표에 없는 단계는 쓰지 않아요.
|
|
33
|
+
- 컴포넌트 이름은 PascalCase(`Button`), 속성 이름은 React props와 똑같이 씁니다(`variant`, `size`, `disabled`).
|
|
34
|
+
- variant 값은 소문자예요(`primary`, `md`).
|
|
35
|
+
|
|
36
|
+
## 3. 토큰
|
|
37
|
+
|
|
38
|
+
### 3.1 색 (Semantic, Light / Dark)
|
|
39
|
+
|
|
40
|
+
화면에서는 Semantic만 써요. Primitives(`gray/*`, `cobalt/*`, `red/*`)는 Semantic이 참조하는 값일 뿐이라 직접 쓰지 않습니다.
|
|
41
|
+
|
|
42
|
+
| 토큰 | Light | Dark | 쓰는 곳 |
|
|
43
|
+
|---|---|---|---|
|
|
44
|
+
| `color/bg/canvas` | gray/50 `#F4F6F8` | gray/950 `#0E1216` | 페이지 바탕 |
|
|
45
|
+
| `color/bg/surface` | gray/0 `#FFFFFF` | gray/900 `#161B21` | Card, Dialog, Input 바탕 |
|
|
46
|
+
| `color/bg/subtle` | gray/100 `#E6EAEE` | gray/800 `#1E252D` | hover 바탕, 진행 막대 트랙, Avatar 바탕 |
|
|
47
|
+
| `color/bg/primary` | cobalt/600 `#2340D0` | cobalt/400 `#6E86FF` | primary 버튼, 진행 막대 |
|
|
48
|
+
| `color/bg/primary-hover` | cobalt/700 `#1B33AE` | cobalt/300 `#8C9FFF` | primary hover |
|
|
49
|
+
| `color/bg/danger` | red/600 `#C8312B` | red/400 `#F06A5F` | 삭제처럼 되돌릴 수 없는 동작 |
|
|
50
|
+
| `color/bg/overlay` | gray/scrim `#0E1216` 50% | 같음 | Dialog backdrop |
|
|
51
|
+
| `color/text/default` | gray/950 | gray/100 | 본문, 제목 |
|
|
52
|
+
| `color/text/muted` | gray/600 `#5B6672` | gray/400 `#8D99A6` | 보조 설명, caption |
|
|
53
|
+
| `color/text/on-primary` | gray/0 | gray/950 | primary 바탕 위 글자 |
|
|
54
|
+
| `color/text/on-danger` | gray/0 | gray/950 | danger 바탕 위 글자 |
|
|
55
|
+
| `color/text/danger` | red/600 | red/400 | 에러 메시지 |
|
|
56
|
+
| `color/border/default` | gray/200 `#D5DBE1` | gray/700 `#2A323B` | 기본 테두리, 구분선 |
|
|
57
|
+
| `color/border/strong` | gray/400 | gray/600 | hover 테두리 |
|
|
58
|
+
| `color/border/focus` | cobalt/600 | cobalt/400 | 포커스 링 |
|
|
59
|
+
| `color/border/danger` | red/600 | red/400 | 에러 상태 Input |
|
|
60
|
+
|
|
61
|
+
### 3.2 간격 (4px 단위)
|
|
62
|
+
|
|
63
|
+
| 토큰 | 값 | 토큰 | 값 |
|
|
64
|
+
|---|---|---|---|
|
|
65
|
+
| `space/0` | 0 | `space/6` | 24 |
|
|
66
|
+
| `space/1` | 4 | `space/7` | 28 |
|
|
67
|
+
| `space/2` | 8 | `space/8` | 32 |
|
|
68
|
+
| `space/3` | 12 | `space/10` | 40 |
|
|
69
|
+
| `space/4` | 16 | `space/12` | 48 |
|
|
70
|
+
| `space/5` | 20 | `space/16` | 64 |
|
|
71
|
+
|
|
72
|
+
컨트롤 높이와 아바타 크기도 이 표에서 가져와요: sm 28(`space/7`) · md 32(`space/8`) · lg 40(`space/10`).
|
|
73
|
+
|
|
74
|
+
### 3.3 Radius / Stroke
|
|
75
|
+
|
|
76
|
+
| 토큰 | 값 | 쓰는 곳 |
|
|
77
|
+
|---|---|---|
|
|
78
|
+
| `radius/sm` | 2 | 작은 표시(배지, 체크박스) |
|
|
79
|
+
| `radius/md` | 4 | Button, Input |
|
|
80
|
+
| `radius/lg` | 6 | Card, Dialog |
|
|
81
|
+
| `radius/full` | 9999 | Avatar, 진행 막대 |
|
|
82
|
+
| `stroke/default` | 1 | 모든 테두리 |
|
|
83
|
+
| `stroke/focus` | 2 | 포커스 링 (offset 2px) |
|
|
84
|
+
|
|
85
|
+
### 3.4 텍스트 스타일 (IBM Plex Sans / IBM Plex Mono)
|
|
86
|
+
|
|
87
|
+
| 스타일 | 크기/행간 | 굵기 | 자간 | 쓰는 곳 |
|
|
88
|
+
|---|---|---|---|---|
|
|
89
|
+
| `display` | 32/40 | 600 | −2% | 페이지 제목, 핵심 수치 |
|
|
90
|
+
| `title` | 20/28 | 600 | −1% | 섹션, Card, Dialog 제목 |
|
|
91
|
+
| `body` | 14/20 | 400 | 0 | 본문 |
|
|
92
|
+
| `body-strong` | 14/20 | 500 | 0 | 버튼(md/lg), 강조 본문 |
|
|
93
|
+
| `label` | 13/16 | 500 | 0 | 폼 라벨, 버튼(sm) |
|
|
94
|
+
| `caption` | 12/16 | 400 | 0 | 보조 설명, 에러 메시지 |
|
|
95
|
+
| `mono` | 14/20 | 400 | 0 | 수치, 코드 |
|
|
96
|
+
| `mono-caption` | 12/16 | 400 | 0 | 작은 ID, 키 값 |
|
|
97
|
+
|
|
98
|
+
### 3.5 모션
|
|
99
|
+
|
|
100
|
+
| 토큰 | 값 |
|
|
101
|
+
|---|---|
|
|
102
|
+
| `duration/fast` | 120ms (hover, 색 전환) |
|
|
103
|
+
| `duration/normal` | 200ms (Dialog 열기/닫기) |
|
|
104
|
+
| `easing/standard` | `cubic-bezier(0.2, 0, 0, 1)` |
|
|
105
|
+
|
|
106
|
+
## 4. Auto layout 규칙 (Figma)
|
|
107
|
+
|
|
108
|
+
- 자식이 둘 이상인 프레임은 모두 auto layout을 씁니다. 절대 좌표 배치는 쓰지 않아요.
|
|
109
|
+
- padding과 gap에는 반드시 `space/*` 변수를 바인딩해요. 숫자를 직접 입력하지 않습니다.
|
|
110
|
+
- 크기 규칙: 텍스트는 Hug, 입력란처럼 늘어나야 하는 요소는 Fill, 컨트롤 높이는 Fixed(`space/7`·`8`·`10`).
|
|
111
|
+
- 레이어 구조는 코드 DOM 구조와 같게 맞춰요. Card → Header / Body / Footer 순서.
|
|
112
|
+
- 0인 padding, gap도 `space/0`에 바인딩해요. 테두리 두께는 `stroke/default`에 바인딩하고 안쪽 정렬(Inside)로 둡니다.
|
|
113
|
+
- 화면 프레임은 Light 모드로 그리고, 마지막에 프레임을 복제해 Dark 모드(Semantic 컬렉션 mode 전환)로 한 번 확인해요.
|
|
114
|
+
|
|
115
|
+
### 간격 기본값
|
|
116
|
+
|
|
117
|
+
| 위치 | 값 |
|
|
118
|
+
|---|---|
|
|
119
|
+
| 페이지 콘텐츠 | 최대 너비 960, 가운데 정렬. 좌우 padding `space/4`(960 안쪽), 위아래 padding `space/10` |
|
|
120
|
+
| 섹션 사이 | `space/6` |
|
|
121
|
+
| Card 안쪽 padding / Header·Body·Footer 사이 | `space/5` / `space/4` |
|
|
122
|
+
| 폼 필드 사이 | `space/4` |
|
|
123
|
+
| 라벨 ↔ 입력란 ↔ 도움말·에러 문구 | `space/2` |
|
|
124
|
+
| 버튼 묶음 사이 | `space/2` |
|
|
125
|
+
| 목록 항목 안 (아바타 ↔ 텍스트) | `space/3` |
|
|
126
|
+
| 제목 ↔ 바로 아래 설명 | `space/1` |
|
|
127
|
+
|
|
128
|
+
- 버튼 묶음은 오른쪽 정렬이고, 보조 버튼이 왼쪽, primary가 맨 오른쪽이에요. Dialog Footer도 같아요.
|
|
129
|
+
- 에러 상태: 입력란 테두리를 `color/border/danger`로 바꾸고, 입력란 바로 아래에 `caption` + `color/text/danger`로 고치는 방법을 씁니다. 라벨 색은 바꾸지 않아요.
|
|
130
|
+
- 문구는 한국어 해요체예요. 취소 버튼은 "취소"로 충분해요. 무엇을 취소하는지는 바로 옆 primary 버튼 문구가 알려 줘요.
|
|
131
|
+
- Avatar 이니셜은 이름에서 1~2글자를 써요(한글은 이름 첫 글자, 영문은 이름·성 첫 글자).
|
|
132
|
+
|
|
133
|
+
## 5. 해야 할 것 / 하지 말아야 할 것
|
|
134
|
+
|
|
135
|
+
| 해야 할 것 | 하지 말아야 할 것 |
|
|
136
|
+
|---|---|
|
|
137
|
+
| Semantic 토큰과 텍스트 스타일만 쓰기 | hex 값, Primitives, 임의 px 직접 쓰기 |
|
|
138
|
+
| sonic-ui 컴포넌트를 조합하기 | 같은 역할의 버튼, 입력란을 새로 그리기 |
|
|
139
|
+
| 버튼 문구는 동작 그대로 쓰기("API 키 만들기", "저장") | "확인", "Submit"처럼 모호한 문구, 문구 끝에 화살표 붙이기 |
|
|
140
|
+
| 에러 문구는 무엇이 잘못됐고 어떻게 고치는지 쓰기 | "오류가 발생했습니다"만 쓰기 |
|
|
141
|
+
| light/dark 둘 다 확인하기 | 한 모드에서만 보고 끝내기 |
|
|
142
|
+
| 포커스 링(`color/border/focus`, 2px, offset 2px) 유지하기 | `outline: none`으로 포커스 표시 지우기 |
|
|
143
|
+
| 문장형 대소문자(sentence case) 쓰기 | 라벨을 전부 대문자로 쓰기 |
|
|
144
|
+
|
|
145
|
+
## 6. 컴포넌트 구조 (Base UI)
|
|
146
|
+
|
|
147
|
+
sonic-ui는 [Base UI](https://base-ui.com) 위에 만들어요. 겹쳐 뜨는 컴포넌트는 Base UI 구조를 그대로 따릅니다.
|
|
148
|
+
|
|
149
|
+
| 부분 | 역할 |
|
|
150
|
+
|---|---|
|
|
151
|
+
| `Trigger` | 여는 버튼 |
|
|
152
|
+
| `Backdrop` | 뒤 화면을 덮는 막 (`color/bg/overlay`) |
|
|
153
|
+
| `Positioner` | 위치를 잡는 래퍼 (Popover, Menu에서 사용) |
|
|
154
|
+
| `Popup` | 실제 내용 상자 (`color/bg/surface`, `radius/lg`) |
|
|
155
|
+
|
|
156
|
+
Figma에서도 레이어 이름을 이 이름으로 맞춰요. 예: `Dialog / Backdrop`, `Dialog / Popup`.
|
|
157
|
+
|
|
158
|
+
**상태 variant 이름은 Base UI의 data-\* 속성 이름을 씁니다.**
|
|
159
|
+
|
|
160
|
+
| Figma variant | 코드 |
|
|
161
|
+
|---|---|
|
|
162
|
+
| `open=true` | `[data-open]` |
|
|
163
|
+
| `checked=true` | `[data-checked]` |
|
|
164
|
+
| `disabled=true` | `[data-disabled]` / `disabled` prop |
|
|
165
|
+
| `highlighted=true` | `[data-highlighted]` (키보드나 hover로 선택된 항목) |
|
|
166
|
+
| `invalid=true` | `[data-invalid]` (Base UI Field) |
|
|
167
|
+
|
|
168
|
+
hover, focus는 Figma에서 `state=hover`, `state=focus`로 그리고, 코드에서는 CSS의 `:hover`, `:focus-visible`로 처리해요.
|
|
169
|
+
|
|
170
|
+
## 7. 3계층 판단 기준
|
|
171
|
+
|
|
172
|
+
| 계층 | 위치 | 기준 | 예 |
|
|
173
|
+
|---|---|---|---|
|
|
174
|
+
| **Core** | sonic-ui `src/components/` | 더 나눌 수 없고, 도메인을 전혀 모름 | Button, Input, Text, Card, Avatar, Dialog |
|
|
175
|
+
| **Pattern** | sonic-ui `src/patterns/` | Core 조합이고 도메인을 모르지만, **두 프로젝트 이상에서 같은 형태로 복사됨** | (아직 없음) |
|
|
176
|
+
| **Domain** | 각 프로젝트 | 도메인 타입, API, 비즈니스 로직 중 하나라도 앎 | `UserProfileCard`, `ApiKeyList` |
|
|
177
|
+
|
|
178
|
+
판단 순서:
|
|
179
|
+
1. props에 도메인 타입(`User`, `Order`)이 들어가나요? → **Domain**
|
|
180
|
+
2. 데이터를 직접 불러오거나 비즈니스 규칙이 있나요? → **Domain**
|
|
181
|
+
3. 두 번째 프로젝트에서 같은 형태를 복사하게 됐나요? → **Pattern으로 승격**
|
|
182
|
+
4. 아직 한 프로젝트에서만 쓰나요? → 그 프로젝트의 Domain에 둡니다
|
|
183
|
+
|
|
184
|
+
## 8. 데이터 화면의 4가지 상태
|
|
185
|
+
|
|
186
|
+
데이터를 보여 주는 화면은 Figma에서 **4가지 상태를 모두 그려야** 개발로 넘어가요.
|
|
187
|
+
|
|
188
|
+
| 상태 | 그릴 것 |
|
|
189
|
+
|---|---|
|
|
190
|
+
| 정상 | 실제와 비슷한 양의 데이터 |
|
|
191
|
+
| 로딩 | 정상 화면과 같은 배치에 자리만 표시(`color/bg/subtle` 블록) |
|
|
192
|
+
| 빈 상태 | 무엇이 없는지, 다음에 할 행동 하나(버튼) |
|
|
193
|
+
| 에러 | 무엇이 실패했는지, 다시 시도할 방법 |
|
|
194
|
+
|
|
195
|
+
Skeleton, EmptyState, ErrorState 컴포넌트는 첫 실제 프로젝트에서 필요할 때 Core에 추가해요. 그 전에는 Core 조합으로 그립니다.
|
|
196
|
+
|
|
197
|
+
## 9. frontend-design 사용 규칙
|
|
198
|
+
|
|
199
|
+
- **디자인 시스템을 만들 때**(이 저장소): frontend-design으로 방향을 탐색할 수 있어요. 결과물은 `explorations/`에만 두고 라이브러리에 넣지 않습니다.
|
|
200
|
+
- **프로젝트에서 쓸 때**: DESIGN.md, sonic-ui 컴포넌트, `sn` 토큰만 씁니다. frontend-design의 새 색, 새 폰트, 임의의 값 제안은 쓰지 않아요. 레이아웃과 문구 판단에만 참고해요.
|
|
201
|
+
- **컴포넌트를 구현할 때**: Figma를 그대로 따르고 frontend-design은 쓰지 않습니다.
|
|
202
|
+
|
|
203
|
+
## 10. 컴포넌트 스펙 (Core)
|
|
204
|
+
|
|
205
|
+
Figma Core 페이지의 컴포넌트 속성 이름 = React props 이름이에요. 모든 컴포넌트는 `className`을 받고, 넘긴 클래스가 기본 스타일을 덮어써요.
|
|
206
|
+
|
|
207
|
+
하위 컴포넌트는 `Card.Header`가 아니라 `CardHeader`처럼 **이름 있는 export**로만 제공해요. Next.js 서버 컴포넌트에서는 `"use client"` 모듈의 export에 점(`.`)으로 접근할 수 없기 때문이에요.
|
|
208
|
+
|
|
209
|
+
### Button
|
|
210
|
+
|
|
211
|
+
`<Button variant="primary" size="md" disabled>저장</Button>`
|
|
212
|
+
|
|
213
|
+
| prop | 값 | 기본값 |
|
|
214
|
+
|---|---|---|
|
|
215
|
+
| `variant` | `primary` · `secondary` · `ghost` · `danger` | `primary` |
|
|
216
|
+
| `size` | `sm` · `md` · `lg` | `md` |
|
|
217
|
+
| `disabled` | boolean | `false` |
|
|
218
|
+
| `children` | 버튼 문구 | — |
|
|
219
|
+
|
|
220
|
+
| | sm | md | lg |
|
|
221
|
+
|---|---|---|---|
|
|
222
|
+
| 높이 | `space/7` (28) | `space/8` (32) | `space/10` (40) |
|
|
223
|
+
| 좌우 padding | `space/2` | `space/3` | `space/4` |
|
|
224
|
+
| 글자 | `label` | `body-strong` | `body-strong` |
|
|
225
|
+
|
|
226
|
+
| variant | 바탕 | 글자 | 테두리 | hover |
|
|
227
|
+
|---|---|---|---|---|
|
|
228
|
+
| primary | `bg/primary` | `text/on-primary` | 없음 | `bg/primary-hover` |
|
|
229
|
+
| secondary | `bg/surface` | `text/default` | `border/default` | `bg/subtle` |
|
|
230
|
+
| ghost | 없음 | `text/default` | 없음 | `bg/subtle` |
|
|
231
|
+
| danger | `bg/danger` | `text/on-danger` | 없음 | 없음 (v0.1에는 danger hover 토큰이 없음) |
|
|
232
|
+
|
|
233
|
+
공통: radius `radius/md`, 아이콘과 글자 사이 `space/2`, disabled는 `opacity/disabled`(45%), focus는 2px `border/focus` + offset 2px, 색 전환은 `duration/fast`.
|
|
234
|
+
|
|
235
|
+
### Input
|
|
236
|
+
|
|
237
|
+
`<Input placeholder="예: production" invalid disabled />`
|
|
238
|
+
|
|
239
|
+
| prop | 값 |
|
|
240
|
+
|---|---|
|
|
241
|
+
| `invalid` | boolean. Base UI Field의 `[data-invalid]`와 같음 |
|
|
242
|
+
| `disabled` | boolean |
|
|
243
|
+
| 그 외 | 기본 `<input>` 속성 전부 |
|
|
244
|
+
|
|
245
|
+
높이 `space/8`, 좌우 padding `space/3`, radius `radius/md`, 바탕 `bg/surface`, 테두리 `border/default`(hover `border/strong`, invalid `border/danger`), 입력값 `body` + `text/default`, placeholder `text/muted`, 너비는 부모를 채움.
|
|
246
|
+
|
|
247
|
+
### Text
|
|
248
|
+
|
|
249
|
+
`<Text variant="caption" tone="muted" as="p">…</Text>`
|
|
250
|
+
|
|
251
|
+
| prop | 값 | 기본값 |
|
|
252
|
+
|---|---|---|
|
|
253
|
+
| `variant` | 텍스트 스타일 8개 (`display` … `mono-caption`) | `body` |
|
|
254
|
+
| `tone` | `default` · `muted` · `danger` | `default` |
|
|
255
|
+
| `as` | 렌더할 태그 (`p`, `span`, `div`, `h1`~`h6`, `label`, `strong`, `code`) | `span` |
|
|
256
|
+
|
|
257
|
+
### Card
|
|
258
|
+
|
|
259
|
+
```tsx
|
|
260
|
+
<Card>
|
|
261
|
+
<CardHeader><CardTitle>…</CardTitle><CardDescription>…</CardDescription></CardHeader>
|
|
262
|
+
<CardBody>…</CardBody>
|
|
263
|
+
<CardFooter>…</CardFooter>
|
|
264
|
+
</Card>
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
바탕 `bg/surface`, 테두리 `border/default` 1px, radius `radius/lg`, padding `space/5`, 영역 사이 `space/4`. Title은 `title`, Description은 `body` + `text/muted`, Header 안 간격 `space/1`. Footer는 오른쪽 정렬, 버튼 사이 `space/2`.
|
|
268
|
+
|
|
269
|
+
### Avatar
|
|
270
|
+
|
|
271
|
+
`<Avatar size="md" src="…" alt="박형규" fallback="HP" />`
|
|
272
|
+
|
|
273
|
+
| size | 크기 | fallback 글자 |
|
|
274
|
+
|---|---|---|
|
|
275
|
+
| `sm` | `space/6` (24) | `caption` |
|
|
276
|
+
| `md` | `space/8` (32) | `label` |
|
|
277
|
+
| `lg` | `space/10` (40) | `body-strong` |
|
|
278
|
+
|
|
279
|
+
radius `radius/full`, 바탕 `bg/subtle`, fallback 글자 `text/muted`. 이미지를 불러오지 못하면 fallback을 보여 줘요(Base UI Avatar).
|
|
280
|
+
|
|
281
|
+
### Dialog
|
|
282
|
+
|
|
283
|
+
```tsx
|
|
284
|
+
<Dialog>
|
|
285
|
+
<DialogTrigger render={<Button />}>API 키 만들기</DialogTrigger>
|
|
286
|
+
<DialogPopup>
|
|
287
|
+
<DialogTitle>API 키 만들기</DialogTitle>
|
|
288
|
+
<DialogDescription>키는 만든 직후 한 번만 보여 줘요.</DialogDescription>
|
|
289
|
+
…
|
|
290
|
+
<DialogFooter><DialogClose render={<Button variant="secondary" />}>취소</DialogClose>…</DialogFooter>
|
|
291
|
+
</DialogPopup>
|
|
292
|
+
</Dialog>
|
|
293
|
+
```
|
|
294
|
+
|
|
295
|
+
- `DialogPopup`이 Portal과 Backdrop을 함께 그려요. 쓰는 쪽에서 Backdrop을 따로 넣지 않아요.
|
|
296
|
+
- Backdrop `bg/overlay`. Popup 너비 440(화면이 좁으면 좌우 `space/4`를 뺀 너비), padding `space/6`, 영역 사이 `space/4`, 바탕 `bg/surface`, 테두리 1px, radius `radius/lg`.
|
|
297
|
+
- 열기/닫기: `duration/normal` + `easing/standard`. `[data-starting-style]`과 `[data-ending-style]`에서 opacity 0, scale 0.98. Backdrop은 opacity만 바꿔요.
|
|
298
|
+
- `open`, `onOpenChange`로 제어할 수 있어요. Esc와 바깥 클릭으로 닫혀요.
|
|
299
|
+
|
|
300
|
+
## 11. Figma ↔ 코드 매핑 (Code Connect 대체)
|
|
301
|
+
|
|
302
|
+
Sonic 팀은 Figma Pro 요금제라 Code Connect(Organization 이상)를 쓸 수 없어요. 아래 표로 대신합니다. Figma 컴포넌트를 코드로 옮길 때는 이 표의 컴포넌트를 그대로 써요.
|
|
303
|
+
|
|
304
|
+
| Figma 컴포넌트 (Core 페이지) | node id | 코드 | 파일 |
|
|
305
|
+
|---|---|---|---|
|
|
306
|
+
| Button | `5:52` | `<Button variant size disabled>` | `src/components/button.tsx` |
|
|
307
|
+
| Input | `5:61` | `<Input invalid disabled placeholder>` | `src/components/input.tsx` |
|
|
308
|
+
| Text | `5:110` | `<Text variant tone as>` | `src/components/text.tsx` |
|
|
309
|
+
| Avatar | `5:117` | `<Avatar size src alt fallback>` | `src/components/avatar.tsx` |
|
|
310
|
+
| Card | `6:2` | `Card`, `CardHeader`, `CardTitle`, `CardDescription`, `CardBody`, `CardFooter` | `src/components/card.tsx` |
|
|
311
|
+
| Dialog | `6:13` | `Dialog`, `DialogTrigger`, `DialogPopup`, `DialogTitle`, `DialogDescription`, `DialogClose`, `DialogFooter` | `src/components/dialog.tsx` |
|
|
312
|
+
|
|
313
|
+
Figma 속성 → props 변환 규칙: variant 속성은 같은 이름의 prop으로, 텍스트 속성 `children`은 자식으로, boolean `header`/`footer`는 해당 하위 컴포넌트를 넣을지 말지로 옮겨요.
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 xonic0506
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,123 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @xonic0506/sonic-ui
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Figma를 원본으로 하는 개인 디자인 시스템이에요. React 컴포넌트는 [Base UI](https://base-ui.com) 위에 만들었고, CSS는 Tailwind v4로 미리 빌드해서 배포해요.
|
|
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)
|
|
8
|
+
|
|
9
|
+
## 설치
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
npm i @xonic0506/sonic-ui
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
`react`, `react-dom` 18.2 이상(19 포함)이 필요해요(peerDependencies).
|
|
16
|
+
|
|
17
|
+
### 글꼴
|
|
18
|
+
|
|
19
|
+
글꼴은 패키지에 들어 있지 않아요. IBM Plex Sans(400·500·600)와 IBM Plex Mono(400)를 프로젝트에서 불러와요. 없으면 시스템 글꼴로 대체돼요.
|
|
20
|
+
|
|
21
|
+
```html
|
|
22
|
+
<link href="https://fonts.googleapis.com/css2?family=IBM+Plex+Mono&family=IBM+Plex+Sans:wght@400;500;600&display=swap" rel="stylesheet" />
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
sonic-ui는 글꼴 이름 `"IBM Plex Sans"`, `"IBM Plex Mono"`를 찾아요. 이름을 바꾸는 방식(예: `next/font`의 기본 설정)으로 불러오면 적용되지 않으니, 위 링크나 `@fontsource/ibm-plex-sans`처럼 원래 이름을 유지하는 방식을 써요.
|
|
26
|
+
|
|
27
|
+
## 설정
|
|
28
|
+
|
|
29
|
+
### Tailwind v4 프로젝트 (권장)
|
|
30
|
+
|
|
31
|
+
```css
|
|
32
|
+
/* app/globals.css */
|
|
33
|
+
@layer theme, base, sonic, components, utilities;
|
|
34
|
+
@import "tailwindcss";
|
|
35
|
+
@import "@xonic0506/sonic-ui/styles.css";
|
|
36
|
+
@import "@xonic0506/sonic-ui/tailwind.css"; /* 선택: bg-primary, text-muted, rounded-md, text-body … 클래스 */
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
**layer 순서가 중요해요.** `sonic`은 Tailwind preflight(`base`)보다 뒤, 쓰는 쪽 `components`/`utilities`보다 앞이에요. 그래서 다음이 보장돼요.
|
|
40
|
+
|
|
41
|
+
- preflight가 버튼 배경, 글자 크기를 지우지 않아요.
|
|
42
|
+
- 쓰는 쪽 `className="bg-red-500"`은 항상 sonic-ui 기본 스타일을 이겨요.
|
|
43
|
+
|
|
44
|
+
### Tailwind 없는 프로젝트
|
|
45
|
+
|
|
46
|
+
```ts
|
|
47
|
+
import '@xonic0506/sonic-ui/styles.css';
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
sonic-ui CSS는 전부 `@layer sonic` 안에 있어서, layer에 넣지 않은 쓰는 쪽 CSS가 항상 이겨요. 토큰은 `var(--sn-color-bg-primary)`처럼 CSS 변수로 써요.
|
|
51
|
+
|
|
52
|
+
### 앱 최상위 요소
|
|
53
|
+
|
|
54
|
+
```tsx
|
|
55
|
+
<div style={{ isolation: 'isolate' }}>{/* 앱 */}</div> // Tailwind: className="isolate"
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
앱 루트에 `isolation: isolate`를 지정해요. 앱의 z-index가 Dialog Portal(`body` 아래)을 넘어서지 않게 해 줘요.
|
|
59
|
+
|
|
60
|
+
### 테마
|
|
61
|
+
|
|
62
|
+
`<html data-theme="light">` 또는 `data-theme="dark"`로 바꿔요. 속성이 없으면 light예요. Portal로 `body`에 그려지는 Dialog에도 같은 테마가 적용돼요.
|
|
63
|
+
|
|
64
|
+
## 사용
|
|
65
|
+
|
|
66
|
+
```tsx
|
|
67
|
+
import { Button, Card, CardBody, CardHeader, CardTitle, Dialog, DialogClose, DialogFooter, DialogPopup, DialogTitle, DialogTrigger, Input, Text } from '@xonic0506/sonic-ui';
|
|
68
|
+
|
|
69
|
+
<Card>
|
|
70
|
+
<CardHeader><CardTitle>API 키</CardTitle></CardHeader>
|
|
71
|
+
<CardBody>
|
|
72
|
+
<Text variant="mono-caption" tone="muted">sk_live_…a91f</Text>
|
|
73
|
+
</CardBody>
|
|
74
|
+
</Card>
|
|
75
|
+
|
|
76
|
+
<Dialog>
|
|
77
|
+
<DialogTrigger render={<Button />}>API 키 만들기</DialogTrigger>
|
|
78
|
+
<DialogPopup>
|
|
79
|
+
<DialogTitle>API 키 만들기</DialogTitle>
|
|
80
|
+
<Input placeholder="예: production" />
|
|
81
|
+
<DialogFooter>
|
|
82
|
+
<DialogClose render={<Button variant="secondary" />}>취소</DialogClose>
|
|
83
|
+
<Button>만들기</Button>
|
|
84
|
+
</DialogFooter>
|
|
85
|
+
</DialogPopup>
|
|
86
|
+
</Dialog>
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
컴포넌트별 props는 [DESIGN.md §10](./DESIGN.md#10-컴포넌트-스펙-core)에 있어요. 모든 파일에 `"use client"`가 붙어 있어서 Next.js 서버 컴포넌트에서도 바로 import할 수 있어요. 하위 컴포넌트는 `CardHeader`처럼 이름 있는 export로 써요.
|
|
90
|
+
|
|
91
|
+
## Claude Code에서 쓰기
|
|
92
|
+
|
|
93
|
+
프로젝트 CLAUDE.md에 추가해요.
|
|
94
|
+
|
|
95
|
+
```md
|
|
96
|
+
@node_modules/@xonic0506/sonic-ui/DESIGN.md
|
|
97
|
+
@node_modules/@xonic0506/sonic-ui/AI-RULES.md
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
## 디자인 lint 규칙 (선택)
|
|
101
|
+
|
|
102
|
+
임의의 값 클래스, Tailwind 기본 팔레트, hex 색을 막는 ESLint flat config 조각을 제공해요. 켜는 것은 쓰는 쪽 설정에서 해요.
|
|
103
|
+
|
|
104
|
+
```js
|
|
105
|
+
// eslint.config.js
|
|
106
|
+
import sonicDesign from '@xonic0506/sonic-ui/eslint-design-rules';
|
|
107
|
+
export default [/* … */, sonicDesign];
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
## 지원 브라우저
|
|
111
|
+
|
|
112
|
+
Tailwind v4 기준과 같아요: Chrome 111+, Edge 111+, Safari 16.4+, Firefox 128+. CSS `@layer`와 `@property`를 쓰기 때문이에요.
|
|
113
|
+
|
|
114
|
+
## 개발
|
|
115
|
+
|
|
116
|
+
```bash
|
|
117
|
+
npm run build # 토큰 생성 → JS(Vite) → 타입 → CSS(@layer sonic)
|
|
118
|
+
npm test # Vitest
|
|
119
|
+
npm run typecheck
|
|
120
|
+
npm run playground # Figma Core 페이지와 비교하는 화면 (build 먼저)
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
운영 규칙은 [CLAUDE.md](./CLAUDE.md)에 있어요.
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
export interface AvatarProps {
|
|
3
|
+
size?: 'sm' | 'md' | 'lg';
|
|
4
|
+
src?: string;
|
|
5
|
+
alt?: string;
|
|
6
|
+
/** 이미지가 없거나 불러오지 못했을 때 보여 줄 이니셜 */
|
|
7
|
+
fallback?: React.ReactNode;
|
|
8
|
+
className?: string;
|
|
9
|
+
}
|
|
10
|
+
export declare const Avatar: React.ForwardRefExoticComponent<AvatarProps & React.RefAttributes<HTMLSpanElement>>;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { cn as e } from "../lib/cn.js";
|
|
3
|
+
import * as t from "react";
|
|
4
|
+
import { cva as n } from "class-variance-authority";
|
|
5
|
+
import { jsx as r, jsxs as i } from "react/jsx-runtime";
|
|
6
|
+
import { Avatar as a } from "@base-ui/react/avatar";
|
|
7
|
+
//#region src/components/avatar.tsx
|
|
8
|
+
var o = n("sn:box-border sn:inline-flex sn:shrink-0 sn:items-center sn:justify-center sn:overflow-hidden sn:rounded-full sn:bg-subtle sn:align-middle sn:font-sans sn:text-muted sn:select-none", {
|
|
9
|
+
variants: { size: {
|
|
10
|
+
sm: "sn:size-6 sn:text-caption",
|
|
11
|
+
md: "sn:size-8 sn:text-label",
|
|
12
|
+
lg: "sn:size-10 sn:text-body-strong"
|
|
13
|
+
} },
|
|
14
|
+
defaultVariants: { size: "md" }
|
|
15
|
+
}), s = t.forwardRef(function({ size: t, src: n, alt: s = "", fallback: c, className: l }, u) {
|
|
16
|
+
return /* @__PURE__ */ i(a.Root, {
|
|
17
|
+
ref: u,
|
|
18
|
+
className: e(o({ size: t }), l),
|
|
19
|
+
children: [n && /* @__PURE__ */ r(a.Image, {
|
|
20
|
+
src: n,
|
|
21
|
+
alt: s,
|
|
22
|
+
className: "sn:size-full sn:object-cover"
|
|
23
|
+
}), /* @__PURE__ */ r(a.Fallback, { children: c })]
|
|
24
|
+
});
|
|
25
|
+
});
|
|
26
|
+
//#endregion
|
|
27
|
+
export { s as Avatar };
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
export interface ButtonProps extends Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, 'className'> {
|
|
3
|
+
variant?: 'primary' | 'secondary' | 'ghost' | 'danger';
|
|
4
|
+
size?: 'sm' | 'md' | 'lg';
|
|
5
|
+
className?: string;
|
|
6
|
+
/** 다른 요소로 렌더링할 때 (예: render={<a href="/" />}) */
|
|
7
|
+
render?: React.ReactElement;
|
|
8
|
+
}
|
|
9
|
+
export declare const Button: React.ForwardRefExoticComponent<ButtonProps & React.RefAttributes<HTMLButtonElement>>;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { cn as e } from "../lib/cn.js";
|
|
3
|
+
import * as t from "react";
|
|
4
|
+
import { Button as n } from "@base-ui/react/button";
|
|
5
|
+
import { cva as r } from "class-variance-authority";
|
|
6
|
+
import { jsx as i } from "react/jsx-runtime";
|
|
7
|
+
//#region src/components/button.tsx
|
|
8
|
+
var a = r("sn:box-border sn:inline-flex sn:items-center sn:justify-center sn:gap-2 sn:whitespace-nowrap sn:rounded-md sn:border sn:border-transparent sn:font-sans sn:cursor-pointer sn:select-none sn:transition-colors sn:duration-(--sn-duration-fast) sn:ease-standard 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)", {
|
|
9
|
+
variants: {
|
|
10
|
+
variant: {
|
|
11
|
+
primary: "sn:bg-primary sn:text-on-primary sn:hover:not-data-disabled:bg-primary-hover",
|
|
12
|
+
secondary: "sn:border-default sn:bg-surface sn:text-default sn:hover:not-data-disabled:bg-subtle",
|
|
13
|
+
ghost: "sn:bg-transparent sn:text-default sn:hover:not-data-disabled:bg-subtle",
|
|
14
|
+
danger: "sn:bg-danger sn:text-on-danger"
|
|
15
|
+
},
|
|
16
|
+
size: {
|
|
17
|
+
sm: "sn:h-7 sn:px-2 sn:text-label",
|
|
18
|
+
md: "sn:h-8 sn:px-3 sn:text-body-strong",
|
|
19
|
+
lg: "sn:h-10 sn:px-4 sn:text-body-strong"
|
|
20
|
+
}
|
|
21
|
+
},
|
|
22
|
+
defaultVariants: {
|
|
23
|
+
variant: "primary",
|
|
24
|
+
size: "md"
|
|
25
|
+
}
|
|
26
|
+
}), o = t.forwardRef(function({ variant: t, size: r, className: o, render: s, ...c }, l) {
|
|
27
|
+
return /* @__PURE__ */ i(n, {
|
|
28
|
+
ref: l,
|
|
29
|
+
render: s,
|
|
30
|
+
nativeButton: !s || s.type === "button",
|
|
31
|
+
className: e(a({
|
|
32
|
+
variant: t,
|
|
33
|
+
size: r
|
|
34
|
+
}), o),
|
|
35
|
+
...c
|
|
36
|
+
});
|
|
37
|
+
});
|
|
38
|
+
//#endregion
|
|
39
|
+
export { o as Button };
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
type DivProps = Omit<React.HTMLAttributes<HTMLDivElement>, 'className'> & {
|
|
3
|
+
className?: string;
|
|
4
|
+
};
|
|
5
|
+
export type CardProps = DivProps;
|
|
6
|
+
export declare const Card: React.ForwardRefExoticComponent<Omit<React.HTMLAttributes<HTMLDivElement>, "className"> & {
|
|
7
|
+
className?: string;
|
|
8
|
+
} & React.RefAttributes<HTMLDivElement>>;
|
|
9
|
+
export declare const CardHeader: React.ForwardRefExoticComponent<Omit<React.HTMLAttributes<HTMLDivElement>, "className"> & {
|
|
10
|
+
className?: string;
|
|
11
|
+
} & React.RefAttributes<HTMLDivElement>>;
|
|
12
|
+
export declare const CardTitle: React.ForwardRefExoticComponent<Omit<React.HTMLAttributes<HTMLHeadingElement>, "className"> & {
|
|
13
|
+
className?: string;
|
|
14
|
+
} & React.RefAttributes<HTMLHeadingElement>>;
|
|
15
|
+
export declare const CardDescription: React.ForwardRefExoticComponent<Omit<React.HTMLAttributes<HTMLParagraphElement>, "className"> & {
|
|
16
|
+
className?: string;
|
|
17
|
+
} & React.RefAttributes<HTMLParagraphElement>>;
|
|
18
|
+
export declare const CardBody: React.ForwardRefExoticComponent<Omit<React.HTMLAttributes<HTMLDivElement>, "className"> & {
|
|
19
|
+
className?: string;
|
|
20
|
+
} & React.RefAttributes<HTMLDivElement>>;
|
|
21
|
+
export declare const CardFooter: React.ForwardRefExoticComponent<Omit<React.HTMLAttributes<HTMLDivElement>, "className"> & {
|
|
22
|
+
className?: string;
|
|
23
|
+
} & React.RefAttributes<HTMLDivElement>>;
|
|
24
|
+
export {};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { cn as e } from "../lib/cn.js";
|
|
3
|
+
import * as t from "react";
|
|
4
|
+
//#region src/components/card.tsx
|
|
5
|
+
function n(n, r, i) {
|
|
6
|
+
let a = t.forwardRef(({ className: i, ...a }, o) => t.createElement(n, {
|
|
7
|
+
ref: o,
|
|
8
|
+
className: e(r, i),
|
|
9
|
+
...a
|
|
10
|
+
}));
|
|
11
|
+
return a.displayName = i, a;
|
|
12
|
+
}
|
|
13
|
+
var r = n("div", "sn:box-border sn:flex sn:flex-col sn:gap-4 sn:rounded-lg sn:border sn:border-default sn:bg-surface sn:p-5 sn:font-sans sn:text-body sn:text-default", "Card"), i = n("div", "sn:flex sn:flex-col sn:gap-1", "CardHeader"), a = n("h3", "sn:m-0 sn:text-title sn:text-default", "CardTitle"), o = n("p", "sn:m-0 sn:text-body sn:text-muted", "CardDescription"), s = n("div", "sn:flex sn:flex-col sn:gap-2", "CardBody"), c = n("div", "sn:flex sn:justify-end sn:gap-2", "CardFooter");
|
|
14
|
+
//#endregion
|
|
15
|
+
export { r as Card, s as CardBody, o as CardDescription, c as CardFooter, i as CardHeader, a as CardTitle };
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
export interface DialogProps {
|
|
3
|
+
open?: boolean;
|
|
4
|
+
defaultOpen?: boolean;
|
|
5
|
+
onOpenChange?: (open: boolean) => void;
|
|
6
|
+
children?: React.ReactNode;
|
|
7
|
+
}
|
|
8
|
+
export declare function Dialog({ onOpenChange, ...props }: DialogProps): React.JSX.Element;
|
|
9
|
+
export interface DialogButtonProps extends Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, 'className'> {
|
|
10
|
+
className?: string;
|
|
11
|
+
/** 보통 sonic-ui Button을 넘겨요: render={<Button variant="secondary" />} */
|
|
12
|
+
render?: React.ReactElement;
|
|
13
|
+
}
|
|
14
|
+
export declare const DialogTrigger: React.ForwardRefExoticComponent<DialogButtonProps & React.RefAttributes<HTMLButtonElement>>;
|
|
15
|
+
export declare const DialogClose: React.ForwardRefExoticComponent<DialogButtonProps & React.RefAttributes<HTMLButtonElement>>;
|
|
16
|
+
export interface DialogPopupProps extends Omit<React.HTMLAttributes<HTMLDivElement>, 'className'> {
|
|
17
|
+
className?: string;
|
|
18
|
+
}
|
|
19
|
+
export declare const DialogPopup: React.ForwardRefExoticComponent<DialogPopupProps & React.RefAttributes<HTMLDivElement>>;
|
|
20
|
+
export declare const DialogTitle: React.ForwardRefExoticComponent<Omit<React.HTMLAttributes<HTMLHeadingElement>, "className"> & {
|
|
21
|
+
className?: string;
|
|
22
|
+
} & React.RefAttributes<HTMLHeadingElement>>;
|
|
23
|
+
export declare const DialogDescription: React.ForwardRefExoticComponent<Omit<React.HTMLAttributes<HTMLParagraphElement>, "className"> & {
|
|
24
|
+
className?: string;
|
|
25
|
+
} & React.RefAttributes<HTMLParagraphElement>>;
|
|
26
|
+
export declare const DialogFooter: React.ForwardRefExoticComponent<Omit<React.HTMLAttributes<HTMLDivElement>, "className"> & {
|
|
27
|
+
className?: string;
|
|
28
|
+
} & React.RefAttributes<HTMLDivElement>>;
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { cn as e } from "../lib/cn.js";
|
|
3
|
+
import * as t from "react";
|
|
4
|
+
import { jsx as n, jsxs as r } from "react/jsx-runtime";
|
|
5
|
+
import { Dialog as i } from "@base-ui/react/dialog";
|
|
6
|
+
//#region src/components/dialog.tsx
|
|
7
|
+
function a({ onOpenChange: e, ...t }) {
|
|
8
|
+
return /* @__PURE__ */ n(i.Root, {
|
|
9
|
+
...t,
|
|
10
|
+
onOpenChange: e && ((t) => e(t))
|
|
11
|
+
});
|
|
12
|
+
}
|
|
13
|
+
var o = t.forwardRef(function(e, t) {
|
|
14
|
+
return /* @__PURE__ */ n(i.Trigger, {
|
|
15
|
+
ref: t,
|
|
16
|
+
...e
|
|
17
|
+
});
|
|
18
|
+
}), s = t.forwardRef(function(e, t) {
|
|
19
|
+
return /* @__PURE__ */ n(i.Close, {
|
|
20
|
+
ref: t,
|
|
21
|
+
...e
|
|
22
|
+
});
|
|
23
|
+
}), c = t.forwardRef(function({ className: t, ...a }, o) {
|
|
24
|
+
return /* @__PURE__ */ r(i.Portal, { children: [/* @__PURE__ */ n(i.Backdrop, { className: "sn:fixed sn:inset-0 sn:z-50 sn:bg-overlay sn:transition-opacity sn:duration-(--sn-duration-normal) sn:ease-standard sn:data-starting-style:opacity-0 sn:data-ending-style:opacity-0 sn:motion-reduce:transition-none" }), /* @__PURE__ */ n(i.Popup, {
|
|
25
|
+
ref: o,
|
|
26
|
+
className: e("sn:fixed sn:top-1/2 sn:left-1/2 sn:z-50 sn:box-border sn:flex sn:w-110 sn:max-w-[calc(100vw-2*var(--sn-space-4))] sn:-translate-x-1/2 sn:-translate-y-1/2 sn:flex-col sn:gap-4 sn:rounded-lg sn:border sn:border-default sn:bg-surface sn:p-6 sn:font-sans sn:text-body sn:text-default sn:outline-none sn:transition-[opacity,scale] sn:duration-(--sn-duration-normal) sn:ease-standard sn:data-starting-style:scale-98 sn:data-starting-style:opacity-0 sn:data-ending-style:scale-98 sn:data-ending-style:opacity-0 sn:motion-reduce:transition-none", t),
|
|
27
|
+
...a
|
|
28
|
+
})] });
|
|
29
|
+
}), l = t.forwardRef(function({ className: t, ...r }, a) {
|
|
30
|
+
return /* @__PURE__ */ n(i.Title, {
|
|
31
|
+
ref: a,
|
|
32
|
+
className: e("sn:m-0 sn:text-title sn:text-default", t),
|
|
33
|
+
...r
|
|
34
|
+
});
|
|
35
|
+
}), u = t.forwardRef(function({ className: t, ...r }, a) {
|
|
36
|
+
return /* @__PURE__ */ n(i.Description, {
|
|
37
|
+
ref: a,
|
|
38
|
+
className: e("sn:-mt-3 sn:mb-0 sn:text-caption sn:text-muted", t),
|
|
39
|
+
...r
|
|
40
|
+
});
|
|
41
|
+
}), d = t.forwardRef(function({ className: t, ...r }, i) {
|
|
42
|
+
return /* @__PURE__ */ n("div", {
|
|
43
|
+
ref: i,
|
|
44
|
+
className: e("sn:mt-2 sn:flex sn:justify-end sn:gap-2", t),
|
|
45
|
+
...r
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
//#endregion
|
|
49
|
+
export { a as Dialog, s as DialogClose, u as DialogDescription, d as DialogFooter, c as DialogPopup, l as DialogTitle, o as DialogTrigger };
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
export interface InputProps extends Omit<React.InputHTMLAttributes<HTMLInputElement>, 'className' | 'size'> {
|
|
3
|
+
/** 에러 상태. aria-invalid로 전달돼요 */
|
|
4
|
+
invalid?: boolean;
|
|
5
|
+
className?: string;
|
|
6
|
+
}
|
|
7
|
+
export declare const Input: React.ForwardRefExoticComponent<InputProps & React.RefAttributes<HTMLInputElement>>;
|
|
@@ -0,0 +1,16 @@
|
|
|
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 { Input as r } from "@base-ui/react/input";
|
|
6
|
+
//#region src/components/input.tsx
|
|
7
|
+
var i = t.forwardRef(function({ invalid: t, className: i, ...a }, o) {
|
|
8
|
+
return /* @__PURE__ */ n(r, {
|
|
9
|
+
ref: o,
|
|
10
|
+
"aria-invalid": t || void 0,
|
|
11
|
+
className: e("sn:box-border sn:m-0 sn:h-8 sn:w-full sn:min-w-0 sn:rounded-md sn:border sn:border-default sn:bg-surface sn:px-3 sn:font-sans sn:text-body sn:text-default sn:placeholder:text-muted sn:transition-colors sn:duration-(--sn-duration-fast) sn:ease-standard sn:hover:not-data-disabled:border-strong sn:focus-visible:outline-2 sn:focus-visible:outline-offset-2 sn:focus-visible:outline-focus sn:aria-invalid:border-danger sn:hover:aria-invalid:border-danger sn:data-invalid:border-danger sn:data-disabled:cursor-not-allowed sn:data-disabled:opacity-(--sn-opacity-disabled)", i),
|
|
12
|
+
...a
|
|
13
|
+
});
|
|
14
|
+
});
|
|
15
|
+
//#endregion
|
|
16
|
+
export { i as Input };
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
export type TextVariant = 'display' | 'title' | 'body' | 'body-strong' | 'label' | 'caption' | 'mono' | 'mono-caption';
|
|
3
|
+
export interface TextProps extends Omit<React.HTMLAttributes<HTMLElement>, 'className'>, Pick<React.LabelHTMLAttributes<HTMLLabelElement>, 'htmlFor'> {
|
|
4
|
+
variant?: TextVariant;
|
|
5
|
+
tone?: 'default' | 'muted' | 'danger';
|
|
6
|
+
/** 렌더할 태그. 기본값 span */
|
|
7
|
+
as?: 'span' | 'p' | 'div' | 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6' | 'label' | 'strong' | 'code';
|
|
8
|
+
className?: string;
|
|
9
|
+
}
|
|
10
|
+
export declare const Text: React.ForwardRefExoticComponent<TextProps & React.RefAttributes<HTMLElement>>;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { cn as e } from "../lib/cn.js";
|
|
3
|
+
import * as t from "react";
|
|
4
|
+
import { cva as n } from "class-variance-authority";
|
|
5
|
+
//#region src/components/text.tsx
|
|
6
|
+
var r = n("sn:m-0 sn:font-sans", {
|
|
7
|
+
variants: {
|
|
8
|
+
variant: {
|
|
9
|
+
display: "sn:text-display",
|
|
10
|
+
title: "sn:text-title",
|
|
11
|
+
body: "sn:text-body",
|
|
12
|
+
"body-strong": "sn:text-body-strong",
|
|
13
|
+
label: "sn:text-label",
|
|
14
|
+
caption: "sn:text-caption",
|
|
15
|
+
mono: "sn:font-mono sn:text-body sn:tabular-nums",
|
|
16
|
+
"mono-caption": "sn:font-mono sn:text-caption sn:tabular-nums"
|
|
17
|
+
},
|
|
18
|
+
tone: {
|
|
19
|
+
default: "sn:text-default",
|
|
20
|
+
muted: "sn:text-muted",
|
|
21
|
+
danger: "sn:text-danger"
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
defaultVariants: {
|
|
25
|
+
variant: "body",
|
|
26
|
+
tone: "default"
|
|
27
|
+
}
|
|
28
|
+
}), i = t.forwardRef(function({ variant: n, tone: i, as: a = "span", className: o, ...s }, c) {
|
|
29
|
+
return t.createElement(a, {
|
|
30
|
+
ref: c,
|
|
31
|
+
className: e(r({
|
|
32
|
+
variant: n,
|
|
33
|
+
tone: i
|
|
34
|
+
}), o),
|
|
35
|
+
...s
|
|
36
|
+
});
|
|
37
|
+
});
|
|
38
|
+
//#endregion
|
|
39
|
+
export { i as Text };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export { Button, type ButtonProps } from './components/button';
|
|
2
|
+
export { Input, type InputProps } from './components/input';
|
|
3
|
+
export { Text, type TextProps, type TextVariant } from './components/text';
|
|
4
|
+
export { Card, CardHeader, CardTitle, CardDescription, CardBody, CardFooter, type CardProps } from './components/card';
|
|
5
|
+
export { Avatar, type AvatarProps } from './components/avatar';
|
|
6
|
+
export { Dialog, DialogTrigger, DialogPopup, DialogTitle, DialogDescription, DialogClose, DialogFooter, type DialogProps, type DialogButtonProps, type DialogPopupProps, } from './components/dialog';
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { Button as e } from "./components/button.js";
|
|
3
|
+
import { Input as t } from "./components/input.js";
|
|
4
|
+
import { Text as n } from "./components/text.js";
|
|
5
|
+
import { Card as r, CardBody as i, CardDescription as a, CardFooter as o, CardHeader as s, CardTitle as c } from "./components/card.js";
|
|
6
|
+
import { Avatar as l } from "./components/avatar.js";
|
|
7
|
+
import { Dialog as u, DialogClose as d, DialogDescription as f, DialogFooter as p, DialogPopup as m, DialogTitle as h, DialogTrigger as g } from "./components/dialog.js";
|
|
8
|
+
export { l as Avatar, e as Button, r as Card, i as CardBody, a as CardDescription, o as CardFooter, s as CardHeader, c as CardTitle, u as Dialog, d as DialogClose, f as DialogDescription, p as DialogFooter, m as DialogPopup, h as DialogTitle, g as DialogTrigger, t as Input, n as Text };
|
package/dist/lib/cn.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function cn(...inputs: Array<string | false | null | undefined>): string;
|
package/dist/lib/cn.js
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { extendTailwindMerge as e } from "tailwind-merge";
|
|
3
|
+
//#region src/lib/cn.ts
|
|
4
|
+
var t = e({
|
|
5
|
+
prefix: "sn",
|
|
6
|
+
extend: { classGroups: { "font-size": [{ text: [
|
|
7
|
+
"display",
|
|
8
|
+
"title",
|
|
9
|
+
"body",
|
|
10
|
+
"body-strong",
|
|
11
|
+
"label",
|
|
12
|
+
"caption"
|
|
13
|
+
] }] } }
|
|
14
|
+
});
|
|
15
|
+
function n(...e) {
|
|
16
|
+
return t(e.filter(Boolean).join(" "));
|
|
17
|
+
}
|
|
18
|
+
//#endregion
|
|
19
|
+
export { n as cn };
|
package/dist/styles.css
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
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\:fixed{position:fixed}.sn\:inset-0{inset:0}.sn\:top-1\/2{top:50%}.sn\:left-1\/2{left:50%}.sn\:z-50{z-index:50}.sn\:m-0{margin:0}.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\:flex{display:flex}.sn\:inline-flex{display:inline-flex}.sn\:size-6{width:calc(var(--sn-space-1) * 6);height:calc(var(--sn-space-1) * 6)}.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-full{width:100%;height:100%}.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\:w-110{width:calc(var(--sn-space-1) * 110)}.sn\:w-full{width:100%}.sn\:max-w-\[calc\(100vw-2\*var\(--sn-space-4\)\)\]{max-width:calc(100vw - 2 * var(--sn-space-4))}.sn\:min-w-0{min-width:0}.sn\:shrink-0{flex-shrink:0}.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-y-1\/2{--tw-translate-y:calc(calc(1 / 2 * 100%) * -1);translate:var(--tw-translate-x) var(--tw-translate-y)}.sn\:cursor-pointer{cursor:pointer}.sn\:flex-col{flex-direction:column}.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-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\:overflow-hidden{overflow:hidden}.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\:border{border-style:var(--tw-border-style);border-width:1px}.sn\:border-default{border-color:var(--sn-color-border-default)}.sn\:border-transparent{border-color:#0000}.sn\:bg-canvas{background-color:var(--sn-color-bg-canvas)}.sn\:bg-danger{background-color:var(--sn-color-bg-danger)}.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-surface{background-color:var(--sn-color-bg-surface)}.sn\:bg-transparent{background-color:#0000}.sn\:object-cover{object-fit:cover}.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\: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\: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-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\: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\:transition-\[opacity\,scale\]{transition-property:opacity,scale;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\: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\: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\: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: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\: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-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-invalid\:border-danger[data-invalid]{border-color:var(--sn-color-border-danger)}.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\:transition-none{transition-property:none}}}: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-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}[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}}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/* 생성물: scripts/build-tokens.mjs. 손으로 수정 금지 (원본: Figma → scripts/figma-tokens.json) */
|
|
2
|
+
/* Tailwind v4 @theme 매핑. sn 토큰을 Tailwind 클래스 이름으로 연결해요 (bg-primary, text-muted, rounded-md, text-body …).
|
|
3
|
+
inline reference: 클래스가 var(--sn-*)를 직접 참조하고, Tailwind 테마 변수는 따로 출력하지 않음 */
|
|
4
|
+
@theme inline reference {
|
|
5
|
+
--spacing: var(--sn-space-1);
|
|
6
|
+
--radius-sm: var(--sn-radius-sm);
|
|
7
|
+
--radius-md: var(--sn-radius-md);
|
|
8
|
+
--radius-lg: var(--sn-radius-lg);
|
|
9
|
+
--radius-full: var(--sn-radius-full);
|
|
10
|
+
--ease-standard: var(--sn-easing-standard);
|
|
11
|
+
--font-sans: var(--sn-font-sans);
|
|
12
|
+
--font-mono: var(--sn-font-mono);
|
|
13
|
+
--text-display: var(--sn-text-display);
|
|
14
|
+
--text-display--line-height: var(--sn-text-display--line-height);
|
|
15
|
+
--text-display--font-weight: var(--sn-text-display--font-weight);
|
|
16
|
+
--text-display--letter-spacing: var(--sn-text-display--letter-spacing);
|
|
17
|
+
--text-title: var(--sn-text-title);
|
|
18
|
+
--text-title--line-height: var(--sn-text-title--line-height);
|
|
19
|
+
--text-title--font-weight: var(--sn-text-title--font-weight);
|
|
20
|
+
--text-title--letter-spacing: var(--sn-text-title--letter-spacing);
|
|
21
|
+
--text-body: var(--sn-text-body);
|
|
22
|
+
--text-body--line-height: var(--sn-text-body--line-height);
|
|
23
|
+
--text-body--font-weight: var(--sn-text-body--font-weight);
|
|
24
|
+
--text-body--letter-spacing: var(--sn-text-body--letter-spacing);
|
|
25
|
+
--text-body-strong: var(--sn-text-body-strong);
|
|
26
|
+
--text-body-strong--line-height: var(--sn-text-body-strong--line-height);
|
|
27
|
+
--text-body-strong--font-weight: var(--sn-text-body-strong--font-weight);
|
|
28
|
+
--text-body-strong--letter-spacing: var(--sn-text-body-strong--letter-spacing);
|
|
29
|
+
--text-label: var(--sn-text-label);
|
|
30
|
+
--text-label--line-height: var(--sn-text-label--line-height);
|
|
31
|
+
--text-label--font-weight: var(--sn-text-label--font-weight);
|
|
32
|
+
--text-label--letter-spacing: var(--sn-text-label--letter-spacing);
|
|
33
|
+
--text-caption: var(--sn-text-caption);
|
|
34
|
+
--text-caption--line-height: var(--sn-text-caption--line-height);
|
|
35
|
+
--text-caption--font-weight: var(--sn-text-caption--font-weight);
|
|
36
|
+
--text-caption--letter-spacing: var(--sn-text-caption--letter-spacing);
|
|
37
|
+
--background-color-canvas: var(--sn-color-bg-canvas);
|
|
38
|
+
--background-color-surface: var(--sn-color-bg-surface);
|
|
39
|
+
--background-color-subtle: var(--sn-color-bg-subtle);
|
|
40
|
+
--background-color-primary: var(--sn-color-bg-primary);
|
|
41
|
+
--background-color-primary-hover: var(--sn-color-bg-primary-hover);
|
|
42
|
+
--background-color-danger: var(--sn-color-bg-danger);
|
|
43
|
+
--background-color-overlay: var(--sn-color-bg-overlay);
|
|
44
|
+
--text-color-default: var(--sn-color-text-default);
|
|
45
|
+
--text-color-muted: var(--sn-color-text-muted);
|
|
46
|
+
--text-color-on-primary: var(--sn-color-text-on-primary);
|
|
47
|
+
--text-color-on-danger: var(--sn-color-text-on-danger);
|
|
48
|
+
--text-color-danger: var(--sn-color-text-danger);
|
|
49
|
+
--border-color-default: var(--sn-color-border-default);
|
|
50
|
+
--outline-color-default: var(--sn-color-border-default);
|
|
51
|
+
--border-color-strong: var(--sn-color-border-strong);
|
|
52
|
+
--outline-color-strong: var(--sn-color-border-strong);
|
|
53
|
+
--border-color-focus: var(--sn-color-border-focus);
|
|
54
|
+
--outline-color-focus: var(--sn-color-border-focus);
|
|
55
|
+
--border-color-danger: var(--sn-color-border-danger);
|
|
56
|
+
--outline-color-danger: var(--sn-color-border-danger);
|
|
57
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
// DESIGN.md를 지키게 하는 ESLint 규칙 (flat config 조각). 이 저장소에서는 켜지 않고 제공만 함.
|
|
2
|
+
// 쓰는 쪽: import sonicDesign from '@xonic0506/sonic-ui/eslint-design-rules'; export default [..., sonicDesign];
|
|
3
|
+
const className = "JSXAttribute[name.name='className']";
|
|
4
|
+
|
|
5
|
+
export default {
|
|
6
|
+
name: 'sonic-ui/design-rules',
|
|
7
|
+
files: ['**/*.{jsx,tsx}'],
|
|
8
|
+
rules: {
|
|
9
|
+
'no-restricted-syntax': [
|
|
10
|
+
'error',
|
|
11
|
+
{
|
|
12
|
+
selector: `${className} Literal[value=/(^|\\s)[\\w:-]*-\\[[^\\]]+\\]/], ${className} TemplateElement[value.raw=/(^|\\s)[\\w:-]*-\\[[^\\]]+\\]/]`,
|
|
13
|
+
message: 'DESIGN.md: 임의의 값 클래스(p-[7px] 등) 대신 토큰 클래스를 쓰세요.',
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
selector: `${className} Literal[value=/(^|\\s)[\\w:-]*(bg|text|border|outline|ring|fill|stroke)-(red|orange|amber|yellow|lime|green|emerald|teal|cyan|sky|blue|indigo|violet|purple|fuchsia|pink|rose|slate|gray|zinc|neutral|stone)-\\d+/]`,
|
|
17
|
+
message: 'DESIGN.md: Tailwind 기본 팔레트 대신 sn 토큰 색(bg-primary, text-muted …)을 쓰세요.',
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
selector: 'Literal[value=/^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})$/]',
|
|
21
|
+
message: 'DESIGN.md: hex 색을 직접 쓰지 말고 var(--sn-color-…) 토큰을 쓰세요.',
|
|
22
|
+
},
|
|
23
|
+
],
|
|
24
|
+
},
|
|
25
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,62 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xonic0506/sonic-ui",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Figma-first personal design system (Base UI + Tailwind v4, prebuilt CSS)",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"sideEffects": [
|
|
8
|
+
"**/*.css"
|
|
9
|
+
],
|
|
10
|
+
"exports": {
|
|
11
|
+
".": {
|
|
12
|
+
"types": "./dist/index.d.ts",
|
|
13
|
+
"import": "./dist/index.js"
|
|
14
|
+
},
|
|
15
|
+
"./styles.css": "./dist/styles.css",
|
|
16
|
+
"./tailwind.css": "./dist/tailwind.css",
|
|
17
|
+
"./DESIGN.md": "./DESIGN.md",
|
|
18
|
+
"./AI-RULES.md": "./AI-RULES.md",
|
|
19
|
+
"./eslint-design-rules": "./lint/design-rules.js"
|
|
20
|
+
},
|
|
21
|
+
"files": [
|
|
22
|
+
"dist",
|
|
23
|
+
"lint",
|
|
24
|
+
"DESIGN.md",
|
|
25
|
+
"AI-RULES.md"
|
|
26
|
+
],
|
|
27
|
+
"scripts": {
|
|
28
|
+
"tokens": "node scripts/build-tokens.mjs",
|
|
29
|
+
"build:css": "node scripts/build-css.mjs",
|
|
30
|
+
"build:js": "vite build && tsc -p tsconfig.build.json",
|
|
31
|
+
"build": "rm -rf dist && npm run tokens && npm run build:js && npm run build:css",
|
|
32
|
+
"typecheck": "tsc --noEmit",
|
|
33
|
+
"test": "vitest run",
|
|
34
|
+
"playground": "vite --config playground/vite.config.ts",
|
|
35
|
+
"prepublishOnly": "npm run typecheck && npm test && npm run build"
|
|
36
|
+
},
|
|
37
|
+
"peerDependencies": {
|
|
38
|
+
"react": "^18.2.0 || ^19.0.0",
|
|
39
|
+
"react-dom": "^18.2.0 || ^19.0.0"
|
|
40
|
+
},
|
|
41
|
+
"dependencies": {
|
|
42
|
+
"@base-ui/react": "^1.9.0",
|
|
43
|
+
"class-variance-authority": "^0.7.1",
|
|
44
|
+
"tailwind-merge": "^3.7.0"
|
|
45
|
+
},
|
|
46
|
+
"devDependencies": {
|
|
47
|
+
"@tailwindcss/cli": "^4.3.3",
|
|
48
|
+
"@testing-library/jest-dom": "^7.0.1",
|
|
49
|
+
"@testing-library/react": "^16.3.3",
|
|
50
|
+
"@testing-library/user-event": "^14.6.8",
|
|
51
|
+
"@types/react": "^19.3.0",
|
|
52
|
+
"@types/react-dom": "^19.3.0",
|
|
53
|
+
"@vitejs/plugin-react": "^6.1.2",
|
|
54
|
+
"jsdom": "^30.1.3",
|
|
55
|
+
"react": "^19.3.1",
|
|
56
|
+
"react-dom": "^19.3.1",
|
|
57
|
+
"tailwindcss": "^4.3.3",
|
|
58
|
+
"typescript": "^7.0.2",
|
|
59
|
+
"vite": "^8.3.4",
|
|
60
|
+
"vitest": "^5.0.5"
|
|
61
|
+
}
|
|
62
|
+
}
|