@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.
Files changed (64) hide show
  1. package/AI-RULES.md +1 -1
  2. package/DESIGN.md +505 -16
  3. package/README.md +87 -38
  4. package/dist/components/accordion.d.ts +30 -0
  5. package/dist/components/accordion.js +44 -0
  6. package/dist/components/alert-dialog.d.ts +16 -0
  7. package/dist/components/alert-dialog.js +46 -0
  8. package/dist/components/badge.d.ts +8 -0
  9. package/dist/components/badge.js +77 -0
  10. package/dist/components/button.d.ts +11 -2
  11. package/dist/components/button.js +24 -14
  12. package/dist/components/checkbox.d.ts +30 -0
  13. package/dist/components/checkbox.js +48 -0
  14. package/dist/components/dialog.d.ts +11 -0
  15. package/dist/components/dialog.js +17 -14
  16. package/dist/components/empty-state.d.ts +11 -0
  17. package/dist/components/empty-state.js +17 -0
  18. package/dist/components/error-state.d.ts +12 -0
  19. package/dist/components/error-state.js +24 -0
  20. package/dist/components/field.d.ts +13 -0
  21. package/dist/components/field.js +33 -0
  22. package/dist/components/file-upload.d.ts +57 -0
  23. package/dist/components/file-upload.js +238 -0
  24. package/dist/components/input.d.ts +1 -0
  25. package/dist/components/input.js +5 -5
  26. package/dist/components/link.d.ts +9 -0
  27. package/dist/components/link.js +30 -0
  28. package/dist/components/menu.d.ts +33 -0
  29. package/dist/components/menu.js +44 -0
  30. package/dist/components/popover.d.ts +26 -0
  31. package/dist/components/popover.js +50 -0
  32. package/dist/components/popup-styles.d.ts +3 -0
  33. package/dist/components/popup-styles.js +5 -0
  34. package/dist/components/progress.d.ts +16 -0
  35. package/dist/components/progress.js +27 -0
  36. package/dist/components/radio-group.d.ts +20 -0
  37. package/dist/components/radio-group.js +40 -0
  38. package/dist/components/select.d.ts +30 -0
  39. package/dist/components/select.js +66 -0
  40. package/dist/components/separator.d.ts +7 -0
  41. package/dist/components/separator.js +14 -0
  42. package/dist/components/skeleton.d.ts +8 -0
  43. package/dist/components/skeleton.js +22 -0
  44. package/dist/components/state-layout.d.ts +11 -0
  45. package/dist/components/state-layout.js +31 -0
  46. package/dist/components/switch.d.ts +12 -0
  47. package/dist/components/switch.js +24 -0
  48. package/dist/components/table.d.ts +24 -0
  49. package/dist/components/table.js +46 -0
  50. package/dist/components/tabs.d.ts +31 -0
  51. package/dist/components/tabs.js +41 -0
  52. package/dist/components/textarea.d.ts +6 -0
  53. package/dist/components/textarea.js +20 -0
  54. package/dist/components/toast.d.ts +24 -0
  55. package/dist/components/toast.js +62 -0
  56. package/dist/components/toggle-group.d.ts +36 -0
  57. package/dist/components/toggle-group.js +44 -0
  58. package/dist/components/tooltip.d.ts +11 -0
  59. package/dist/components/tooltip.js +19 -0
  60. package/dist/index.d.ts +23 -0
  61. package/dist/index.js +28 -5
  62. package/dist/styles.css +2 -2
  63. package/dist/tailwind.css +15 -0
  64. package/package.json +9 -2
package/README.md CHANGED
@@ -1,65 +1,95 @@
1
- # @xonic0506/sonic-ui
1
+ <div align="center">
2
2
 
3
- Figma를 원본으로 하는 개인 디자인 시스템이에요. React 컴포넌트는 [Base UI](https://base-ui.com) 위에 만들었고, CSS는 Tailwind v4로 미리 빌드해서 배포해요.
3
+ <img src="./docs/assets/sonic-mascot.webp" alt="Sonic 마스코트" width="240" />
4
4
 
5
- - 디자인 규칙: [DESIGN.md](./DESIGN.md)
6
- - AI 작업 규칙: [AI-RULES.md](./AI-RULES.md)
7
- - Figma: [Sonic Design System](https://www.figma.com/design/XifnZsjqJWJqFmAmrM6p3u)
5
+ # Sonic UI
8
6
 
9
- ## 설치
7
+ [![npm version](https://img.shields.io/npm/v/@xonic0506/sonic-ui.svg)](https://www.npmjs.com/package/@xonic0506/sonic-ui) ![React](https://img.shields.io/badge/react-18.2%20%7C%2019-61dafb.svg) [![License](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
8
+
9
+ Figma를 원본으로 하는 개인 디자인 시스템이에요.
10
+
11
+ [Figma](https://www.figma.com/design/XifnZsjqJWJqFmAmrM6p3u) | [디자인 규칙 (DESIGN.md)](./DESIGN.md) | [AI 작업 규칙 (AI-RULES.md)](./AI-RULES.md)
12
+
13
+ </div>
14
+
15
+ Sonic UI는 모든 사이드 프로젝트에서 같은 디자인을 쓰기 위해 만들었어요. 디자인 룰을 Figma와 DESIGN.md에 정해 두면, AI가 그 룰로 Figma 디자인을 만들고 이 패키지의 컴포넌트로 코드를 짜요.
16
+
17
+ ```text
18
+ Figma (원본) → DESIGN.md (룰) → @xonic0506/sonic-ui (코드) → 실제 프로젝트
19
+ ```
20
+
21
+ ## 특징
22
+
23
+ - Figma Variables가 그대로 CSS 변수가 돼요. `color/bg/primary`는 `--sn-color-bg-primary`이고, light/dark 테마를 함께 가져요.
24
+ - CSS를 미리 빌드해서 배포해요. 쓰는 쪽 프로젝트는 Tailwind가 있어도 되고 없어도 돼요.
25
+ - 모든 클래스에 `sn:` 접두사를 붙이고 `@layer sonic` 안에 넣어서, 쓰는 쪽 스타일과 부딪히지 않아요.
26
+ - [Base UI](https://base-ui.com) 위에 만들어서 키보드 조작, 포커스 관리, 화면 낭독기 지원이 기본으로 들어 있어요.
27
+ - 모든 파일에 `"use client"`가 붙어 있어서 Next.js 서버 컴포넌트에서도 바로 import할 수 있어요.
28
+ - 쓰는 쪽 AI가 읽을 규칙 문서(DESIGN.md, AI-RULES.md)와 디자인 lint 규칙을 함께 배포해요.
29
+
30
+ ## 컴포넌트
31
+
32
+ | 분류 | 컴포넌트 |
33
+ | --- | --- |
34
+ | 기본 | Button, Input, Textarea, Text, Card, Avatar, Separator |
35
+ | 폼 | Field, Checkbox, CheckboxGroup, RadioGroup, Switch, Select (단일·다중) |
36
+ | 오버레이 | Dialog, AlertDialog, Menu, Popover, Tooltip |
37
+ | 탐색 | Tabs |
38
+ | 피드백·상태 | Toast, Badge, Progress, Skeleton, EmptyState, ErrorState |
39
+
40
+ 컴포넌트별 props와 스펙은 [DESIGN.md §10](./DESIGN.md#10-컴포넌트-스펙-core)에 있어요.
41
+
42
+ ## 시작하기
43
+
44
+ ### 설치
10
45
 
11
46
  ```bash
12
47
  npm i @xonic0506/sonic-ui
13
48
  ```
14
49
 
15
- `react`, `react-dom` 18.2 이상(19 포함)이 필요해요(peerDependencies).
50
+ `react`, `react-dom` 18.2 이상(19 포함)이 필요해요.
16
51
 
17
52
  ### 글꼴
18
53
 
19
- 글꼴은 패키지에 들어 있지 않아요. IBM Plex Sans(400·500·600)와 IBM Plex Mono(400)를 프로젝트에서 불러와요. 없으면 시스템 글꼴로 대체돼요.
54
+ IBM Plex Sans(400, 500, 600)와 IBM Plex Mono(400)를 프로젝트에서 불러와요. 없으면 시스템 글꼴로 대체돼요.
20
55
 
21
56
  ```html
22
57
  <link href="https://fonts.googleapis.com/css2?family=IBM+Plex+Mono&family=IBM+Plex+Sans:wght@400;500;600&display=swap" rel="stylesheet" />
23
58
  ```
24
59
 
25
- sonic-ui는 글꼴 이름 `"IBM Plex Sans"`, `"IBM Plex Mono"`를 찾아요. 이름을 바꾸는 방식(예: `next/font`의 기본 설정)으로 불러오면 적용되지 않으니, 위 링크나 `@fontsource/ibm-plex-sans`처럼 원래 이름을 유지하는 방식을 써요.
60
+ > [!NOTE]
61
+ > 글꼴은 패키지에 들어 있지 않아요. sonic-ui는 글꼴 이름 `"IBM Plex Sans"`, `"IBM Plex Mono"`를 찾기 때문에, 이름을 바꾸는 방식(예: `next/font` 기본 설정)으로 불러오면 적용되지 않아요. 위 링크나 `@fontsource/ibm-plex-sans`처럼 원래 이름을 유지하는 방식을 써요.
26
62
 
27
- ## 설정
63
+ ### CSS 연결
28
64
 
29
- ### Tailwind v4 프로젝트 (권장)
65
+ Tailwind v4 프로젝트(권장)에서는 이렇게 연결해요.
30
66
 
31
67
  ```css
32
68
  /* app/globals.css */
33
69
  @layer theme, base, sonic, components, utilities;
34
70
  @import "tailwindcss";
35
71
  @import "@xonic0506/sonic-ui/styles.css";
36
- @import "@xonic0506/sonic-ui/tailwind.css"; /* 선택: bg-primary, text-muted, rounded-md, text-body … 클래스 */
72
+ @import "@xonic0506/sonic-ui/tailwind.css"; /* 선택: bg-primary, text-muted, rounded-md, text-body 같은 클래스 */
37
73
  ```
38
74
 
39
- **layer 순서가 중요해요.** `sonic`은 Tailwind preflight(`base`)보다 뒤, 쓰는 쪽 `components`/`utilities`보다 앞이에요. 그래서 다음이 보장돼요.
40
-
41
- - preflight가 버튼 배경, 글자 크기를 지우지 않아요.
42
- - 쓰는 쪽 `className="bg-red-500"`은 항상 sonic-ui 기본 스타일을 이겨요.
75
+ > [!IMPORTANT]
76
+ > layer 순서를 지켜요. `sonic`이 Tailwind preflight(`base`)보다 뒤에 있어야 버튼 배경과 글자 크기가 지워지지 않고, 쓰는 쪽 `components`/`utilities`보다 앞에 있어야 `className="bg-red-500"` 같은 덮어쓰기가 항상 이겨요.
43
77
 
44
- ### Tailwind 없는 프로젝트
78
+ Tailwind가 없는 프로젝트에서는 CSS 파일 하나만 불러와요. 토큰은 `var(--sn-color-bg-primary)`처럼 CSS 변수로 써요.
45
79
 
46
80
  ```ts
47
81
  import '@xonic0506/sonic-ui/styles.css';
48
82
  ```
49
83
 
50
- sonic-ui CSS는 전부 `@layer sonic` 안에 있어서, layer에 넣지 않은 쓰는 쪽 CSS가 항상 이겨요. 토큰은 `var(--sn-color-bg-primary)`처럼 CSS 변수로 써요.
84
+ ### 앱 루트와 테마
51
85
 
52
- ### 앱 최상위 요소
86
+ 앱 루트에 `isolation: isolate`를 지정해요. 앱의 z-index가 `body` 아래에 그려지는 Dialog, Toast를 가리지 않게 해 줘요.
53
87
 
54
88
  ```tsx
55
89
  <div style={{ isolation: 'isolate' }}>{/* 앱 */}</div> // Tailwind: className="isolate"
56
90
  ```
57
91
 
58
- 앱 루트에 `isolation: isolate`를 지정해요. 앱의 z-index가 Dialog Portal(`body` 아래)을 넘어서지 않게 해 줘요.
59
-
60
- ### 테마
61
-
62
- `<html data-theme="light">` 또는 `data-theme="dark"`로 바꿔요. 속성이 없으면 light예요. Portal로 `body`에 그려지는 Dialog에도 같은 테마가 적용돼요.
92
+ 테마는 `<html data-theme="light">` 또는 `data-theme="dark"`로 바꿔요. 속성이 없으면 light이고, `body` 아래에 그려지는 팝업에도 같은 테마가 적용돼요.
63
93
 
64
94
  ## 사용
65
95
 
@@ -86,20 +116,26 @@ import { Button, Card, CardBody, CardHeader, CardTitle, Dialog, DialogClose, Dia
86
116
  </Dialog>
87
117
  ```
88
118
 
89
- 컴포넌트별 props는 [DESIGN.md §10](./DESIGN.md#10-컴포넌트-스펙-core)에 있어요. 모든 파일에 `"use client"`가 붙어 있어서 Next.js 서버 컴포넌트에서도 바로 import할 수 있어요. 하위 컴포넌트는 `CardHeader`처럼 이름 있는 export로 써요.
119
+ 하위 컴포넌트는 `Card.Header`가 아니라 `CardHeader`처럼 이름 있는 export로 써요. 서버 컴포넌트에서도 깨지지 않게 하기 위해서예요.
90
120
 
91
- ## Claude Code에서 쓰기
121
+ Toast는 앱 최상단(Next.js는 `app/layout.tsx`)을 `<ToastProvider>`로 한 번 감싸고, 클라이언트 컴포넌트에서 띄워요.
92
122
 
93
- 프로젝트 CLAUDE.md에 추가해요.
94
-
95
- ```md
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
- ## 디자인 lint 규칙 (선택)
128
+ ## AI와 함께 쓰기
129
+
130
+ > [!TIP]
131
+ > Claude Code를 쓰는 프로젝트라면 CLAUDE.md에 아래 두 줄을 추가해요. AI가 디자인 규칙을 읽고 sonic-ui 컴포넌트로 화면을 만들어요.
132
+ >
133
+ > ```md
134
+ > @node_modules/@xonic0506/sonic-ui/DESIGN.md
135
+ > @node_modules/@xonic0506/sonic-ui/AI-RULES.md
136
+ > ```
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
- Tailwind v4 기준과 같아요: Chrome 111+, Edge 111+, Safari 16.4+, Firefox 128+. CSS `@layer`와 `@property`를 쓰기 때문이에요.
148
+ Chrome 111+, Edge 111+, Safari 16.4+, Firefox 128+. CSS `@layer`와 `@property`를 쓰기 때문에 Tailwind v4와 기준이 같아요.
113
149
 
114
150
  ## 개발
115
151
 
116
152
  ```bash
117
- npm run build # 토큰 생성 → JS(Vite) → 타입 → CSS(@layer sonic)
118
- npm test # Vitest
153
+ npm run build # 토큰 생성 → JS(Vite) → 타입 → CSS(@layer sonic)
154
+ npm test # Vitest
119
155
  npm run typecheck
120
- npm run playground # Figma Core 페이지와 비교하는 화면 (build 먼저)
156
+ npm run check:contrast # 글자와 바탕 대비 4.5:1 검사
157
+ npm run playground # Figma와 비교하는 화면 (build 먼저)
158
+ ```
159
+
160
+ ```text
161
+ .
162
+ ├── src/components/ # Core 컴포넌트
163
+ ├── src/patterns/ # Pattern 승격 기준 (두 번째 프로젝트에서 복사하게 될 때)
164
+ ├── scripts/ # Figma 토큰 추출, CSS 빌드, 대비 검사
165
+ ├── lint/ # 디자인 lint 규칙
166
+ ├── playground/ # Figma와 비교하는 화면
167
+ ├── fixtures/ # 쓰는 쪽 예제 (vite, Next.js)
168
+ └── DESIGN.md # 디자인 규칙 (패키지에 포함)
121
169
  ```
122
170
 
123
- 운영 규칙은 [CLAUDE.md](./CLAUDE.md)에 있어요.
171
+ > [!NOTE]
172
+ > 디자인을 바꿀 때는 Figma → DESIGN.md → 토큰 추출 → 코드 순서를 지켜요. 자세한 운영 규칙은 [CLAUDE.md](./CLAUDE.md)에 있어요.
@@ -0,0 +1,30 @@
1
+ import * as React from 'react';
2
+ export interface AccordionProps {
3
+ /** 여러 항목을 함께 열 수 있음 (기본 false) */
4
+ multiple?: boolean;
5
+ /** 열린 항목들의 value */
6
+ value?: string[];
7
+ defaultValue?: string[];
8
+ onValueChange?: (value: string[]) => void;
9
+ className?: string;
10
+ children?: React.ReactNode;
11
+ }
12
+ /** 닫힌 내용도 브라우저 페이지 찾기로 찾아 열 수 있어요 (hiddenUntilFound) */
13
+ export declare function Accordion({ onValueChange, className, ...props }: AccordionProps): React.JSX.Element;
14
+ export interface AccordionItemProps {
15
+ value?: string;
16
+ disabled?: boolean;
17
+ className?: string;
18
+ children?: React.ReactNode;
19
+ }
20
+ export declare function AccordionItem({ className, ...props }: AccordionItemProps): React.JSX.Element;
21
+ export interface AccordionTriggerProps {
22
+ className?: string;
23
+ children?: React.ReactNode;
24
+ }
25
+ export declare function AccordionTrigger({ className, children }: AccordionTriggerProps): React.JSX.Element;
26
+ export interface AccordionPanelProps {
27
+ className?: string;
28
+ children?: React.ReactNode;
29
+ }
30
+ export declare function AccordionPanel({ className, children }: AccordionPanelProps): React.JSX.Element;
@@ -0,0 +1,44 @@
1
+ "use client";
2
+ import { cn as e } from "../lib/cn.js";
3
+ import "react";
4
+ import { ChevronDown as t } from "lucide-react";
5
+ import { jsx as n, jsxs as r } from "react/jsx-runtime";
6
+ import { Accordion as i } from "@base-ui/react/accordion";
7
+ //#region src/components/accordion.tsx
8
+ function a({ onValueChange: t, className: r, ...a }) {
9
+ return /* @__PURE__ */ n(i.Root, {
10
+ ...a,
11
+ hiddenUntilFound: !0,
12
+ onValueChange: t && ((e) => t(e)),
13
+ className: e("sn:flex sn:flex-col sn:font-sans", r)
14
+ });
15
+ }
16
+ function o({ className: t, ...r }) {
17
+ return /* @__PURE__ */ n(i.Item, {
18
+ ...r,
19
+ className: e("sn:border-b sn:border-default sn:data-disabled:opacity-(--sn-opacity-disabled)", t)
20
+ });
21
+ }
22
+ function s({ className: a, children: o }) {
23
+ return /* @__PURE__ */ n(i.Header, {
24
+ className: "sn:m-0",
25
+ children: /* @__PURE__ */ r(i.Trigger, {
26
+ className: e("sn:group sn:flex sn:w-full sn:items-center sn:gap-2 sn:rounded-sm sn:border-0 sn:bg-transparent sn:px-0 sn:py-3 sn:text-left sn:font-sans sn:text-body-strong sn:text-default sn:cursor-pointer sn:focus-visible:outline-2 sn:focus-visible:outline-offset-2 sn:focus-visible:outline-focus sn:data-disabled:cursor-not-allowed", a),
27
+ children: [/* @__PURE__ */ n("span", {
28
+ className: "sn:min-w-0 sn:flex-1",
29
+ children: o
30
+ }), /* @__PURE__ */ n(t, { className: "sn:size-4 sn:shrink-0 sn:text-muted sn:transition-transform sn:duration-(--sn-duration-normal) sn:ease-standard sn:group-data-panel-open:rotate-180 sn:motion-reduce:transition-none" })]
31
+ })
32
+ });
33
+ }
34
+ function c({ className: t, children: r }) {
35
+ return /* @__PURE__ */ n(i.Panel, {
36
+ className: "sn:h-(--accordion-panel-height) sn:overflow-hidden sn:transition-[height] sn:duration-(--sn-duration-normal) sn:ease-standard sn:data-starting-style:h-0 sn:data-ending-style:h-0 sn:motion-reduce:transition-none",
37
+ children: /* @__PURE__ */ n("div", {
38
+ className: e("sn:pb-4 sn:font-sans sn:text-body sn:text-muted", t),
39
+ children: r
40
+ })
41
+ });
42
+ }
43
+ //#endregion
44
+ export { a as Accordion, o as AccordionItem, c as AccordionPanel, s as AccordionTrigger };
@@ -0,0 +1,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
- export interface ButtonProps extends Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, 'className'> {
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 { jsx as i } from "react/jsx-runtime";
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 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
+ 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
- }), 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({
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
- }), o),
35
- ...c
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 { o as Button };
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>>;