@xonic0506/sonic-ui 0.1.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/AI-RULES.md +1 -1
- package/DESIGN.md +505 -16
- package/README.md +87 -38
- package/dist/components/accordion.d.ts +30 -0
- package/dist/components/accordion.js +44 -0
- package/dist/components/alert-dialog.d.ts +16 -0
- package/dist/components/alert-dialog.js +46 -0
- package/dist/components/badge.d.ts +8 -0
- package/dist/components/badge.js +77 -0
- package/dist/components/button.d.ts +11 -2
- package/dist/components/button.js +24 -14
- package/dist/components/checkbox.d.ts +30 -0
- package/dist/components/checkbox.js +48 -0
- package/dist/components/dialog.d.ts +11 -0
- package/dist/components/dialog.js +17 -14
- package/dist/components/empty-state.d.ts +11 -0
- package/dist/components/empty-state.js +17 -0
- package/dist/components/error-state.d.ts +12 -0
- package/dist/components/error-state.js +24 -0
- package/dist/components/field.d.ts +13 -0
- package/dist/components/field.js +33 -0
- package/dist/components/file-upload.d.ts +57 -0
- package/dist/components/file-upload.js +238 -0
- package/dist/components/input.d.ts +1 -0
- package/dist/components/input.js +5 -5
- package/dist/components/link.d.ts +9 -0
- package/dist/components/link.js +30 -0
- package/dist/components/menu.d.ts +33 -0
- package/dist/components/menu.js +44 -0
- package/dist/components/popover.d.ts +26 -0
- package/dist/components/popover.js +50 -0
- package/dist/components/popup-styles.d.ts +3 -0
- package/dist/components/popup-styles.js +5 -0
- package/dist/components/progress.d.ts +16 -0
- package/dist/components/progress.js +27 -0
- package/dist/components/radio-group.d.ts +20 -0
- package/dist/components/radio-group.js +40 -0
- package/dist/components/select.d.ts +30 -0
- package/dist/components/select.js +66 -0
- package/dist/components/separator.d.ts +7 -0
- package/dist/components/separator.js +14 -0
- package/dist/components/skeleton.d.ts +8 -0
- package/dist/components/skeleton.js +22 -0
- package/dist/components/state-layout.d.ts +11 -0
- package/dist/components/state-layout.js +31 -0
- package/dist/components/switch.d.ts +12 -0
- package/dist/components/switch.js +24 -0
- package/dist/components/table.d.ts +24 -0
- package/dist/components/table.js +46 -0
- package/dist/components/tabs.d.ts +31 -0
- package/dist/components/tabs.js +41 -0
- package/dist/components/textarea.d.ts +6 -0
- package/dist/components/textarea.js +20 -0
- package/dist/components/toast.d.ts +24 -0
- package/dist/components/toast.js +62 -0
- package/dist/components/toggle-group.d.ts +36 -0
- package/dist/components/toggle-group.js +44 -0
- package/dist/components/tooltip.d.ts +11 -0
- package/dist/components/tooltip.js +19 -0
- package/dist/index.d.ts +23 -0
- package/dist/index.js +28 -5
- package/dist/styles.css +2 -2
- package/dist/tailwind.css +15 -0
- package/package.json +9 -2
package/README.md
CHANGED
|
@@ -1,65 +1,95 @@
|
|
|
1
|
-
|
|
1
|
+
<div align="center">
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
<img src="./docs/assets/sonic-mascot.webp" alt="Sonic 마스코트" width="240" />
|
|
4
4
|
|
|
5
|
-
|
|
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
|
+
[](https://www.npmjs.com/package/@xonic0506/sonic-ui)  [](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 포함)이
|
|
50
|
+
`react`, `react-dom` 18.2 이상(19 포함)이 필요해요.
|
|
16
51
|
|
|
17
52
|
### 글꼴
|
|
18
53
|
|
|
19
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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,20 +116,26 @@ import { Button, Card, CardBody, CardHeader, CardTitle, Dialog, DialogClose, Dia
|
|
|
86
116
|
</Dialog>
|
|
87
117
|
```
|
|
88
118
|
|
|
89
|
-
|
|
119
|
+
하위 컴포넌트는 `Card.Header`가 아니라 `CardHeader`처럼 이름 있는 export로 써요. 서버 컴포넌트에서도 깨지지 않게 하기 위해서예요.
|
|
90
120
|
|
|
91
|
-
|
|
121
|
+
Toast는 앱 최상단(Next.js는 `app/layout.tsx`)을 `<ToastProvider>`로 한 번 감싸고, 클라이언트 컴포넌트에서 띄워요.
|
|
92
122
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
@node_modules/@xonic0506/sonic-ui/DESIGN.md
|
|
97
|
-
@node_modules/@xonic0506/sonic-ui/AI-RULES.md
|
|
123
|
+
```tsx
|
|
124
|
+
const toast = useToast();
|
|
125
|
+
toast.add({ title: 'API 키를 만들었어요', tone: 'success' });
|
|
98
126
|
```
|
|
99
127
|
|
|
100
|
-
##
|
|
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
|
+
> ```
|
|
101
137
|
|
|
102
|
-
임의의 값 클래스, Tailwind 기본 팔레트, hex 색을 막는 ESLint flat config
|
|
138
|
+
임의의 값 클래스, Tailwind 기본 팔레트, hex 색을 막는 ESLint flat config도 제공해요. 켜는 것은 쓰는 쪽 설정에서 해요.
|
|
103
139
|
|
|
104
140
|
```js
|
|
105
141
|
// eslint.config.js
|
|
@@ -109,15 +145,28 @@ export default [/* … */, sonicDesign];
|
|
|
109
145
|
|
|
110
146
|
## 지원 브라우저
|
|
111
147
|
|
|
112
|
-
|
|
148
|
+
Chrome 111+, Edge 111+, Safari 16.4+, Firefox 128+. CSS `@layer`와 `@property`를 쓰기 때문에 Tailwind v4와 기준이 같아요.
|
|
113
149
|
|
|
114
150
|
## 개발
|
|
115
151
|
|
|
116
152
|
```bash
|
|
117
|
-
npm run build
|
|
118
|
-
npm test
|
|
153
|
+
npm run build # 토큰 생성 → JS(Vite) → 타입 → CSS(@layer sonic)
|
|
154
|
+
npm test # Vitest
|
|
119
155
|
npm run typecheck
|
|
120
|
-
npm run
|
|
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 # 디자인 규칙 (패키지에 포함)
|
|
121
169
|
```
|
|
122
170
|
|
|
123
|
-
|
|
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,16 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { type DialogButtonProps, type DialogPopupProps, type DialogProps } from './dialog';
|
|
3
|
+
/** 되돌릴 수 없는 동작(삭제 등) 확인. Dialog와 같은 모양이고 바깥을 눌러도 닫히지 않아요 */
|
|
4
|
+
export declare function AlertDialog({ onOpenChange, ...props }: DialogProps): React.JSX.Element;
|
|
5
|
+
export declare const AlertDialogTrigger: React.ForwardRefExoticComponent<DialogButtonProps & React.RefAttributes<HTMLButtonElement>>;
|
|
6
|
+
export declare const AlertDialogClose: React.ForwardRefExoticComponent<DialogButtonProps & React.RefAttributes<HTMLButtonElement>>;
|
|
7
|
+
export declare const AlertDialogPopup: React.ForwardRefExoticComponent<DialogPopupProps & React.RefAttributes<HTMLDivElement>>;
|
|
8
|
+
export declare const AlertDialogTitle: React.ForwardRefExoticComponent<Omit<React.HTMLAttributes<HTMLHeadingElement>, "className"> & {
|
|
9
|
+
className?: string;
|
|
10
|
+
} & React.RefAttributes<HTMLHeadingElement>>;
|
|
11
|
+
export declare const AlertDialogDescription: React.ForwardRefExoticComponent<Omit<React.HTMLAttributes<HTMLParagraphElement>, "className"> & {
|
|
12
|
+
className?: string;
|
|
13
|
+
} & React.RefAttributes<HTMLParagraphElement>>;
|
|
14
|
+
export declare const AlertDialogFooter: React.ForwardRefExoticComponent<Omit<React.HTMLAttributes<HTMLDivElement>, "className"> & {
|
|
15
|
+
className?: string;
|
|
16
|
+
} & React.RefAttributes<HTMLDivElement>>;
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { cn as e } from "../lib/cn.js";
|
|
3
|
+
import { DialogFooter as t, backdropClass as n, dialogDescriptionClass as r, dialogPopupClass as i, dialogTitleClass as a } from "./dialog.js";
|
|
4
|
+
import * as o from "react";
|
|
5
|
+
import { jsx as s, jsxs as c } from "react/jsx-runtime";
|
|
6
|
+
import { AlertDialog as l } from "@base-ui/react/alert-dialog";
|
|
7
|
+
//#region src/components/alert-dialog.tsx
|
|
8
|
+
function u({ onOpenChange: e, ...t }) {
|
|
9
|
+
return /* @__PURE__ */ s(l.Root, {
|
|
10
|
+
...t,
|
|
11
|
+
onOpenChange: e && ((t) => e(t))
|
|
12
|
+
});
|
|
13
|
+
}
|
|
14
|
+
var d = o.forwardRef(function(e, t) {
|
|
15
|
+
return /* @__PURE__ */ s(l.Trigger, {
|
|
16
|
+
ref: t,
|
|
17
|
+
...e
|
|
18
|
+
});
|
|
19
|
+
}), f = o.forwardRef(function(e, t) {
|
|
20
|
+
return /* @__PURE__ */ s(l.Close, {
|
|
21
|
+
ref: t,
|
|
22
|
+
...e
|
|
23
|
+
});
|
|
24
|
+
}), p = o.forwardRef(function({ className: t, initialFocus: r, finalFocus: a, ...o }, u) {
|
|
25
|
+
return /* @__PURE__ */ c(l.Portal, { children: [/* @__PURE__ */ s(l.Backdrop, { className: n }), /* @__PURE__ */ s(l.Popup, {
|
|
26
|
+
ref: u,
|
|
27
|
+
initialFocus: r,
|
|
28
|
+
finalFocus: a,
|
|
29
|
+
className: e(i, t),
|
|
30
|
+
...o
|
|
31
|
+
})] });
|
|
32
|
+
}), m = o.forwardRef(function({ className: t, ...n }, r) {
|
|
33
|
+
return /* @__PURE__ */ s(l.Title, {
|
|
34
|
+
ref: r,
|
|
35
|
+
className: e(a, t),
|
|
36
|
+
...n
|
|
37
|
+
});
|
|
38
|
+
}), h = o.forwardRef(function({ className: t, ...n }, i) {
|
|
39
|
+
return /* @__PURE__ */ s(l.Description, {
|
|
40
|
+
ref: i,
|
|
41
|
+
className: e(r, t),
|
|
42
|
+
...n
|
|
43
|
+
});
|
|
44
|
+
}), g = t;
|
|
45
|
+
//#endregion
|
|
46
|
+
export { u as AlertDialog, f as AlertDialogClose, h as AlertDialogDescription, g as AlertDialogFooter, p as AlertDialogPopup, m as AlertDialogTitle, d as AlertDialogTrigger };
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
export interface BadgeProps extends Omit<React.HTMLAttributes<HTMLSpanElement>, 'className'> {
|
|
3
|
+
tone?: 'neutral' | 'success' | 'warning' | 'danger';
|
|
4
|
+
/** 목록·표에서는 subtle, 꼭 눈에 띄어야 하는 한 곳에만 solid */
|
|
5
|
+
variant?: 'subtle' | 'solid';
|
|
6
|
+
className?: string;
|
|
7
|
+
}
|
|
8
|
+
export declare const Badge: React.ForwardRefExoticComponent<BadgeProps & React.RefAttributes<HTMLSpanElement>>;
|
|
@@ -0,0 +1,77 @@
|
|
|
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 } from "react/jsx-runtime";
|
|
6
|
+
//#region src/components/badge.tsx
|
|
7
|
+
var i = n("sn:box-border sn:inline-flex sn:h-5 sn:shrink-0 sn:items-center sn:gap-1 sn:whitespace-nowrap sn:rounded-sm sn:px-2 sn:font-sans sn:text-label sn:[&_svg]:size-3 sn:[&_svg]:shrink-0", {
|
|
8
|
+
variants: {
|
|
9
|
+
tone: {
|
|
10
|
+
neutral: "",
|
|
11
|
+
success: "",
|
|
12
|
+
warning: "",
|
|
13
|
+
danger: ""
|
|
14
|
+
},
|
|
15
|
+
variant: {
|
|
16
|
+
subtle: "",
|
|
17
|
+
solid: ""
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
compoundVariants: [
|
|
21
|
+
{
|
|
22
|
+
tone: "neutral",
|
|
23
|
+
variant: "subtle",
|
|
24
|
+
className: "sn:bg-subtle sn:text-default"
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
tone: "neutral",
|
|
28
|
+
variant: "solid",
|
|
29
|
+
className: "sn:bg-inverse sn:text-inverse"
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
tone: "success",
|
|
33
|
+
variant: "subtle",
|
|
34
|
+
className: "sn:bg-success-subtle sn:text-success"
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
tone: "success",
|
|
38
|
+
variant: "solid",
|
|
39
|
+
className: "sn:bg-success sn:text-on-success"
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
tone: "warning",
|
|
43
|
+
variant: "subtle",
|
|
44
|
+
className: "sn:bg-warning-subtle sn:text-warning"
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
tone: "warning",
|
|
48
|
+
variant: "solid",
|
|
49
|
+
className: "sn:bg-warning sn:text-on-warning"
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
tone: "danger",
|
|
53
|
+
variant: "subtle",
|
|
54
|
+
className: "sn:bg-danger-subtle sn:text-danger"
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
tone: "danger",
|
|
58
|
+
variant: "solid",
|
|
59
|
+
className: "sn:bg-danger sn:text-on-danger"
|
|
60
|
+
}
|
|
61
|
+
],
|
|
62
|
+
defaultVariants: {
|
|
63
|
+
tone: "neutral",
|
|
64
|
+
variant: "subtle"
|
|
65
|
+
}
|
|
66
|
+
}), a = t.forwardRef(function({ tone: t, variant: n, className: a, ...o }, s) {
|
|
67
|
+
return /* @__PURE__ */ r("span", {
|
|
68
|
+
ref: s,
|
|
69
|
+
className: e(i({
|
|
70
|
+
tone: t,
|
|
71
|
+
variant: n
|
|
72
|
+
}), a),
|
|
73
|
+
...o
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
//#endregion
|
|
77
|
+
export { a as Badge };
|
|
@@ -1,9 +1,18 @@
|
|
|
1
1
|
import * as React from 'react';
|
|
2
|
-
|
|
2
|
+
interface ButtonBaseProps extends Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, 'className'> {
|
|
3
3
|
variant?: 'primary' | 'secondary' | 'ghost' | 'danger';
|
|
4
|
-
size?: 'sm' | 'md' | 'lg';
|
|
5
4
|
className?: string;
|
|
6
5
|
/** 다른 요소로 렌더링할 때 (예: render={<a href="/" />}) */
|
|
7
6
|
render?: React.ReactElement;
|
|
7
|
+
/** 진행 중: 스피너 표시, 클릭 막기, 포커스 유지, aria-busy */
|
|
8
|
+
loading?: boolean;
|
|
8
9
|
}
|
|
10
|
+
/** 아이콘만 있는 버튼은 읽을 글자가 없어서 aria-label이 필수예요 */
|
|
11
|
+
export type ButtonProps = ButtonBaseProps & ({
|
|
12
|
+
size?: 'sm' | 'md' | 'lg';
|
|
13
|
+
} | {
|
|
14
|
+
size: 'icon-sm' | 'icon-md' | 'icon-lg';
|
|
15
|
+
'aria-label': string;
|
|
16
|
+
});
|
|
9
17
|
export declare const Button: React.ForwardRefExoticComponent<ButtonProps & React.RefAttributes<HTMLButtonElement>>;
|
|
18
|
+
export {};
|
|
@@ -3,9 +3,10 @@ import { cn as e } from "../lib/cn.js";
|
|
|
3
3
|
import * as t from "react";
|
|
4
4
|
import { Button as n } from "@base-ui/react/button";
|
|
5
5
|
import { cva as r } from "class-variance-authority";
|
|
6
|
-
import {
|
|
6
|
+
import { LoaderCircle as i } from "lucide-react";
|
|
7
|
+
import { Fragment as a, jsx as o, jsxs as s } from "react/jsx-runtime";
|
|
7
8
|
//#region src/components/button.tsx
|
|
8
|
-
var
|
|
9
|
+
var c = 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:not-data-loading:cursor-not-allowed sn:data-disabled:not-data-loading:opacity-(--sn-opacity-disabled) sn:data-loading:cursor-progress sn:[&_svg]:pointer-events-none sn:[&_svg]:shrink-0", {
|
|
9
10
|
variants: {
|
|
10
11
|
variant: {
|
|
11
12
|
primary: "sn:bg-primary sn:text-on-primary sn:hover:not-data-disabled:bg-primary-hover",
|
|
@@ -14,26 +15,35 @@ var a = r("sn:box-border sn:inline-flex sn:items-center sn:justify-center sn:gap
|
|
|
14
15
|
danger: "sn:bg-danger sn:text-on-danger"
|
|
15
16
|
},
|
|
16
17
|
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"
|
|
18
|
+
sm: "sn:h-7 sn:px-2 sn:text-label sn:[&_svg]:size-4",
|
|
19
|
+
md: "sn:h-8 sn:px-3 sn:text-body-strong sn:[&_svg]:size-4",
|
|
20
|
+
lg: "sn:h-10 sn:px-4 sn:text-body-strong sn:[&_svg]:size-5",
|
|
21
|
+
"icon-sm": "sn:size-7 sn:p-0 sn:[&_svg]:size-4",
|
|
22
|
+
"icon-md": "sn:size-8 sn:p-0 sn:[&_svg]:size-4",
|
|
23
|
+
"icon-lg": "sn:size-10 sn:p-0 sn:[&_svg]:size-5"
|
|
20
24
|
}
|
|
21
25
|
},
|
|
22
26
|
defaultVariants: {
|
|
23
27
|
variant: "primary",
|
|
24
28
|
size: "md"
|
|
25
29
|
}
|
|
26
|
-
}),
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
30
|
+
}), l = t.forwardRef(function({ variant: t, size: r, className: l, render: u, loading: d, disabled: f, children: p, ...m }, h) {
|
|
31
|
+
let g = /* @__PURE__ */ o(i, { className: "sn:animate-[sn-spin_var(--sn-duration-loop)_linear_infinite] sn:motion-reduce:animate-none" });
|
|
32
|
+
return /* @__PURE__ */ o(n, {
|
|
33
|
+
ref: h,
|
|
34
|
+
render: u,
|
|
35
|
+
nativeButton: !u || u.type === "button",
|
|
36
|
+
disabled: f || d,
|
|
37
|
+
focusableWhenDisabled: d,
|
|
38
|
+
"aria-busy": d || void 0,
|
|
39
|
+
"data-loading": d ? "" : void 0,
|
|
40
|
+
className: e(c({
|
|
32
41
|
variant: t,
|
|
33
42
|
size: r
|
|
34
|
-
}),
|
|
35
|
-
...
|
|
43
|
+
}), l),
|
|
44
|
+
...m,
|
|
45
|
+
children: d && r?.startsWith("icon-") ? g : d ? /* @__PURE__ */ s(a, { children: [g, p] }) : p
|
|
36
46
|
});
|
|
37
47
|
});
|
|
38
48
|
//#endregion
|
|
39
|
-
export {
|
|
49
|
+
export { l as Button };
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
declare const labelClass = "sn:inline-flex sn:items-center sn:gap-2 sn:font-sans sn:text-body sn:text-default sn:cursor-pointer";
|
|
3
|
+
declare const disabledClass = "sn:cursor-not-allowed sn:opacity-(--sn-opacity-disabled)";
|
|
4
|
+
declare const focusClass = "sn:focus-visible:outline-2 sn:focus-visible:outline-offset-2 sn:focus-visible:outline-focus";
|
|
5
|
+
export interface CheckboxProps {
|
|
6
|
+
checked?: boolean;
|
|
7
|
+
defaultChecked?: boolean;
|
|
8
|
+
onCheckedChange?: (checked: boolean) => void;
|
|
9
|
+
/** 일부만 선택된 상태 (그룹의 상위 항목) */
|
|
10
|
+
indeterminate?: boolean;
|
|
11
|
+
disabled?: boolean;
|
|
12
|
+
/** CheckboxGroup 안에서 쓰는 값 */
|
|
13
|
+
value?: string;
|
|
14
|
+
name?: string;
|
|
15
|
+
className?: string;
|
|
16
|
+
children?: React.ReactNode;
|
|
17
|
+
}
|
|
18
|
+
export declare const Checkbox: React.ForwardRefExoticComponent<CheckboxProps & React.RefAttributes<HTMLElement>>;
|
|
19
|
+
export interface CheckboxGroupProps {
|
|
20
|
+
/** 그룹 이름 (fieldset legend) */
|
|
21
|
+
label: React.ReactNode;
|
|
22
|
+
value?: string[];
|
|
23
|
+
defaultValue?: string[];
|
|
24
|
+
onValueChange?: (value: string[]) => void;
|
|
25
|
+
disabled?: boolean;
|
|
26
|
+
className?: string;
|
|
27
|
+
children: React.ReactNode;
|
|
28
|
+
}
|
|
29
|
+
export declare function CheckboxGroup({ label, value, defaultValue, onValueChange, disabled, className, children }: CheckboxGroupProps): React.JSX.Element;
|
|
30
|
+
export { labelClass as choiceLabelClass, disabledClass as choiceDisabledClass, focusClass as choiceFocusClass };
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { cn as e } from "../lib/cn.js";
|
|
3
|
+
import { boxClass as t } from "./popup-styles.js";
|
|
4
|
+
import * as n from "react";
|
|
5
|
+
import { Check as r, Minus as i } from "lucide-react";
|
|
6
|
+
import { jsx as a, jsxs as o } from "react/jsx-runtime";
|
|
7
|
+
import { Checkbox as s } from "@base-ui/react/checkbox";
|
|
8
|
+
import { CheckboxGroup as c } from "@base-ui/react/checkbox-group";
|
|
9
|
+
import { Fieldset as l } from "@base-ui/react/fieldset";
|
|
10
|
+
//#region src/components/checkbox.tsx
|
|
11
|
+
var u = "sn:inline-flex sn:items-center sn:gap-2 sn:font-sans sn:text-body sn:text-default sn:cursor-pointer", d = "sn:cursor-not-allowed sn:opacity-(--sn-opacity-disabled)", f = "sn:focus-visible:outline-2 sn:focus-visible:outline-offset-2 sn:focus-visible:outline-focus", p = n.forwardRef(function({ checked: n, defaultChecked: c, onCheckedChange: l, indeterminate: d, disabled: p, value: m, name: h, className: g, children: _ }, v) {
|
|
12
|
+
return /* @__PURE__ */ o("label", {
|
|
13
|
+
className: e(u, p && "sn:cursor-not-allowed sn:opacity-(--sn-opacity-disabled)", g),
|
|
14
|
+
children: [/* @__PURE__ */ a(s.Root, {
|
|
15
|
+
ref: v,
|
|
16
|
+
checked: n,
|
|
17
|
+
defaultChecked: c,
|
|
18
|
+
onCheckedChange: l && ((e) => l(e)),
|
|
19
|
+
indeterminate: d,
|
|
20
|
+
disabled: p,
|
|
21
|
+
value: m,
|
|
22
|
+
name: h,
|
|
23
|
+
className: e(t, f, "sn:data-checked:border-transparent sn:data-checked:bg-primary sn:data-indeterminate:border-transparent sn:data-indeterminate:bg-primary"),
|
|
24
|
+
children: /* @__PURE__ */ a(s.Indicator, {
|
|
25
|
+
className: "sn:flex",
|
|
26
|
+
children: a(d ? i : r, {})
|
|
27
|
+
})
|
|
28
|
+
}), _]
|
|
29
|
+
});
|
|
30
|
+
});
|
|
31
|
+
function m({ label: t, value: n, defaultValue: r, onValueChange: i, disabled: s, className: u, children: d }) {
|
|
32
|
+
return /* @__PURE__ */ o(l.Root, {
|
|
33
|
+
className: e("sn:m-0 sn:flex sn:min-w-0 sn:flex-col sn:gap-3 sn:border-0 sn:p-0 sn:font-sans", u),
|
|
34
|
+
children: [/* @__PURE__ */ a(l.Legend, {
|
|
35
|
+
className: "sn:p-0 sn:text-label sn:text-default",
|
|
36
|
+
children: t
|
|
37
|
+
}), /* @__PURE__ */ a(c, {
|
|
38
|
+
value: n,
|
|
39
|
+
defaultValue: r,
|
|
40
|
+
onValueChange: i && ((e) => i(e)),
|
|
41
|
+
disabled: s,
|
|
42
|
+
className: "sn:flex sn:flex-col sn:gap-2",
|
|
43
|
+
children: d
|
|
44
|
+
})]
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
//#endregion
|
|
48
|
+
export { p as Checkbox, m as CheckboxGroup, d as choiceDisabledClass, f as choiceFocusClass, u as choiceLabelClass };
|
|
@@ -1,4 +1,8 @@
|
|
|
1
1
|
import * as React from 'react';
|
|
2
|
+
export declare const backdropClass = "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";
|
|
3
|
+
export declare const dialogPopupClass = "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";
|
|
4
|
+
export declare const dialogTitleClass = "sn:m-0 sn:text-title sn:text-default";
|
|
5
|
+
export declare const dialogDescriptionClass = "sn:-mt-3 sn:mb-0 sn:text-caption sn:text-muted";
|
|
2
6
|
export interface DialogProps {
|
|
3
7
|
open?: boolean;
|
|
4
8
|
defaultOpen?: boolean;
|
|
@@ -15,8 +19,15 @@ export declare const DialogTrigger: React.ForwardRefExoticComponent<DialogButton
|
|
|
15
19
|
export declare const DialogClose: React.ForwardRefExoticComponent<DialogButtonProps & React.RefAttributes<HTMLButtonElement>>;
|
|
16
20
|
export interface DialogPopupProps extends Omit<React.HTMLAttributes<HTMLDivElement>, 'className'> {
|
|
17
21
|
className?: string;
|
|
22
|
+
/** 열릴 때 포커스할 요소 (기본: 팝업 안 첫 포커스 가능 요소) */
|
|
23
|
+
initialFocus?: React.RefObject<HTMLElement | null>;
|
|
24
|
+
/** 닫힌 뒤 포커스할 요소 (기본: 트리거나 열기 전 포커스 요소). 코드로 열었을 때 지정해요 */
|
|
25
|
+
finalFocus?: React.RefObject<HTMLElement | null>;
|
|
18
26
|
}
|
|
19
27
|
export declare const DialogPopup: React.ForwardRefExoticComponent<DialogPopupProps & React.RefAttributes<HTMLDivElement>>;
|
|
28
|
+
export type TextProps<E extends HTMLElement> = Omit<React.HTMLAttributes<E>, 'className'> & {
|
|
29
|
+
className?: string;
|
|
30
|
+
};
|
|
20
31
|
export declare const DialogTitle: React.ForwardRefExoticComponent<Omit<React.HTMLAttributes<HTMLHeadingElement>, "className"> & {
|
|
21
32
|
className?: string;
|
|
22
33
|
} & React.RefAttributes<HTMLHeadingElement>>;
|