@xonic0506/sonic-ui 0.0.0-stage → 0.2.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 +18 -0
  2. package/DESIGN.md +652 -0
  3. package/LICENSE +21 -0
  4. package/README.md +124 -2
  5. package/dist/components/alert-dialog.d.ts +16 -0
  6. package/dist/components/alert-dialog.js +46 -0
  7. package/dist/components/avatar.d.ts +10 -0
  8. package/dist/components/avatar.js +27 -0
  9. package/dist/components/badge.d.ts +8 -0
  10. package/dist/components/badge.js +77 -0
  11. package/dist/components/button.d.ts +18 -0
  12. package/dist/components/button.js +49 -0
  13. package/dist/components/card.d.ts +24 -0
  14. package/dist/components/card.js +15 -0
  15. package/dist/components/checkbox.d.ts +30 -0
  16. package/dist/components/checkbox.js +48 -0
  17. package/dist/components/dialog.d.ts +39 -0
  18. package/dist/components/dialog.js +52 -0
  19. package/dist/components/empty-state.d.ts +11 -0
  20. package/dist/components/empty-state.js +17 -0
  21. package/dist/components/error-state.d.ts +12 -0
  22. package/dist/components/error-state.js +24 -0
  23. package/dist/components/field.d.ts +13 -0
  24. package/dist/components/field.js +33 -0
  25. package/dist/components/input.d.ts +8 -0
  26. package/dist/components/input.js +16 -0
  27. package/dist/components/menu.d.ts +33 -0
  28. package/dist/components/menu.js +44 -0
  29. package/dist/components/popover.d.ts +26 -0
  30. package/dist/components/popover.js +50 -0
  31. package/dist/components/popup-styles.d.ts +3 -0
  32. package/dist/components/popup-styles.js +5 -0
  33. package/dist/components/progress.d.ts +16 -0
  34. package/dist/components/progress.js +27 -0
  35. package/dist/components/radio-group.d.ts +20 -0
  36. package/dist/components/radio-group.js +40 -0
  37. package/dist/components/select.d.ts +30 -0
  38. package/dist/components/select.js +66 -0
  39. package/dist/components/separator.d.ts +7 -0
  40. package/dist/components/separator.js +14 -0
  41. package/dist/components/skeleton.d.ts +8 -0
  42. package/dist/components/skeleton.js +22 -0
  43. package/dist/components/state-layout.d.ts +11 -0
  44. package/dist/components/state-layout.js +31 -0
  45. package/dist/components/switch.d.ts +12 -0
  46. package/dist/components/switch.js +24 -0
  47. package/dist/components/tabs.d.ts +31 -0
  48. package/dist/components/tabs.js +41 -0
  49. package/dist/components/text.d.ts +10 -0
  50. package/dist/components/text.js +39 -0
  51. package/dist/components/textarea.d.ts +6 -0
  52. package/dist/components/textarea.js +20 -0
  53. package/dist/components/toast.d.ts +24 -0
  54. package/dist/components/toast.js +62 -0
  55. package/dist/components/tooltip.d.ts +11 -0
  56. package/dist/components/tooltip.js +19 -0
  57. package/dist/index.d.ts +24 -0
  58. package/dist/index.js +26 -0
  59. package/dist/lib/cn.d.ts +1 -0
  60. package/dist/lib/cn.js +19 -0
  61. package/dist/styles.css +3 -0
  62. package/dist/tailwind.css +72 -0
  63. package/lint/design-rules.js +25 -0
  64. package/package.json +62 -4
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,125 @@
1
- # Temporary Holding Version
1
+ # @xonic0506/sonic-ui
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
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
+ Toast를 쓰려면 앱 최상단(Next.js는 `app/layout.tsx`)을 `<ToastProvider>`로 한 번 감싸고, 클라이언트 컴포넌트에서 `useToast().add({ title, tone })`로 띄워요.
92
+
93
+ ## Claude Code에서 쓰기
94
+
95
+ 프로젝트 CLAUDE.md에 추가해요.
96
+
97
+ ```md
98
+ @node_modules/@xonic0506/sonic-ui/DESIGN.md
99
+ @node_modules/@xonic0506/sonic-ui/AI-RULES.md
100
+ ```
101
+
102
+ ## 디자인 lint 규칙 (선택)
103
+
104
+ 임의의 값 클래스, Tailwind 기본 팔레트, hex 색을 막는 ESLint flat config 조각을 제공해요. 켜는 것은 쓰는 쪽 설정에서 해요.
105
+
106
+ ```js
107
+ // eslint.config.js
108
+ import sonicDesign from '@xonic0506/sonic-ui/eslint-design-rules';
109
+ export default [/* … */, sonicDesign];
110
+ ```
111
+
112
+ ## 지원 브라우저
113
+
114
+ Tailwind v4 기준과 같아요: Chrome 111+, Edge 111+, Safari 16.4+, Firefox 128+. CSS `@layer`와 `@property`를 쓰기 때문이에요.
115
+
116
+ ## 개발
117
+
118
+ ```bash
119
+ npm run build # 토큰 생성 → JS(Vite) → 타입 → CSS(@layer sonic)
120
+ npm test # Vitest
121
+ npm run typecheck
122
+ npm run playground # Figma Core 페이지와 비교하는 화면 (build 먼저)
123
+ ```
124
+
125
+ 운영 규칙은 [CLAUDE.md](./CLAUDE.md)에 있어요.
@@ -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,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,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 };
@@ -0,0 +1,18 @@
1
+ import * as React from 'react';
2
+ interface ButtonBaseProps extends Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, 'className'> {
3
+ variant?: 'primary' | 'secondary' | 'ghost' | 'danger';
4
+ className?: string;
5
+ /** 다른 요소로 렌더링할 때 (예: render={<a href="/" />}) */
6
+ render?: React.ReactElement;
7
+ /** 진행 중: 스피너 표시, 클릭 막기, 포커스 유지, aria-busy */
8
+ loading?: boolean;
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
+ });
17
+ export declare const Button: React.ForwardRefExoticComponent<ButtonProps & React.RefAttributes<HTMLButtonElement>>;
18
+ export {};
@@ -0,0 +1,49 @@
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 { LoaderCircle as i } from "lucide-react";
7
+ import { Fragment as a, jsx as o, jsxs as s } from "react/jsx-runtime";
8
+ //#region src/components/button.tsx
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", {
10
+ variants: {
11
+ variant: {
12
+ primary: "sn:bg-primary sn:text-on-primary sn:hover:not-data-disabled:bg-primary-hover",
13
+ secondary: "sn:border-default sn:bg-surface sn:text-default sn:hover:not-data-disabled:bg-subtle",
14
+ ghost: "sn:bg-transparent sn:text-default sn:hover:not-data-disabled:bg-subtle",
15
+ danger: "sn:bg-danger sn:text-on-danger"
16
+ },
17
+ size: {
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"
24
+ }
25
+ },
26
+ defaultVariants: {
27
+ variant: "primary",
28
+ size: "md"
29
+ }
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({
41
+ variant: t,
42
+ size: r
43
+ }), l),
44
+ ...m,
45
+ children: d && r?.startsWith("icon-") ? g : d ? /* @__PURE__ */ s(a, { children: [g, p] }) : p
46
+ });
47
+ });
48
+ //#endregion
49
+ export { l 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,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 };
@@ -0,0 +1,39 @@
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";
6
+ export interface DialogProps {
7
+ open?: boolean;
8
+ defaultOpen?: boolean;
9
+ onOpenChange?: (open: boolean) => void;
10
+ children?: React.ReactNode;
11
+ }
12
+ export declare function Dialog({ onOpenChange, ...props }: DialogProps): React.JSX.Element;
13
+ export interface DialogButtonProps extends Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, 'className'> {
14
+ className?: string;
15
+ /** 보통 sonic-ui Button을 넘겨요: render={<Button variant="secondary" />} */
16
+ render?: React.ReactElement;
17
+ }
18
+ export declare const DialogTrigger: React.ForwardRefExoticComponent<DialogButtonProps & React.RefAttributes<HTMLButtonElement>>;
19
+ export declare const DialogClose: React.ForwardRefExoticComponent<DialogButtonProps & React.RefAttributes<HTMLButtonElement>>;
20
+ export interface DialogPopupProps extends Omit<React.HTMLAttributes<HTMLDivElement>, 'className'> {
21
+ className?: string;
22
+ /** 열릴 때 포커스할 요소 (기본: 팝업 안 첫 포커스 가능 요소) */
23
+ initialFocus?: React.RefObject<HTMLElement | null>;
24
+ /** 닫힌 뒤 포커스할 요소 (기본: 트리거나 열기 전 포커스 요소). 코드로 열었을 때 지정해요 */
25
+ finalFocus?: React.RefObject<HTMLElement | null>;
26
+ }
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
+ };
31
+ export declare const DialogTitle: React.ForwardRefExoticComponent<Omit<React.HTMLAttributes<HTMLHeadingElement>, "className"> & {
32
+ className?: string;
33
+ } & React.RefAttributes<HTMLHeadingElement>>;
34
+ export declare const DialogDescription: React.ForwardRefExoticComponent<Omit<React.HTMLAttributes<HTMLParagraphElement>, "className"> & {
35
+ className?: string;
36
+ } & React.RefAttributes<HTMLParagraphElement>>;
37
+ export declare const DialogFooter: React.ForwardRefExoticComponent<Omit<React.HTMLAttributes<HTMLDivElement>, "className"> & {
38
+ className?: string;
39
+ } & React.RefAttributes<HTMLDivElement>>;
@@ -0,0 +1,52 @@
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
+ var a = "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", o = "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", s = "sn:m-0 sn:text-title sn:text-default", c = "sn:-mt-3 sn:mb-0 sn:text-caption sn:text-muted";
8
+ function l({ onOpenChange: e, ...t }) {
9
+ return /* @__PURE__ */ n(i.Root, {
10
+ ...t,
11
+ onOpenChange: e && ((t) => e(t))
12
+ });
13
+ }
14
+ var u = t.forwardRef(function(e, t) {
15
+ return /* @__PURE__ */ n(i.Trigger, {
16
+ ref: t,
17
+ ...e
18
+ });
19
+ }), d = t.forwardRef(function(e, t) {
20
+ return /* @__PURE__ */ n(i.Close, {
21
+ ref: t,
22
+ ...e
23
+ });
24
+ }), f = t.forwardRef(function({ className: t, initialFocus: s, finalFocus: c, ...l }, u) {
25
+ return /* @__PURE__ */ r(i.Portal, { children: [/* @__PURE__ */ n(i.Backdrop, { className: a }), /* @__PURE__ */ n(i.Popup, {
26
+ ref: u,
27
+ initialFocus: s,
28
+ finalFocus: c,
29
+ className: e(o, t),
30
+ ...l
31
+ })] });
32
+ }), p = t.forwardRef(function({ className: t, ...r }, a) {
33
+ return /* @__PURE__ */ n(i.Title, {
34
+ ref: a,
35
+ className: e(s, t),
36
+ ...r
37
+ });
38
+ }), m = t.forwardRef(function({ className: t, ...r }, a) {
39
+ return /* @__PURE__ */ n(i.Description, {
40
+ ref: a,
41
+ className: e(c, t),
42
+ ...r
43
+ });
44
+ }), h = t.forwardRef(function({ className: t, ...r }, i) {
45
+ return /* @__PURE__ */ n("div", {
46
+ ref: i,
47
+ className: e("sn:mt-2 sn:flex sn:justify-end sn:gap-2", t),
48
+ ...r
49
+ });
50
+ });
51
+ //#endregion
52
+ export { l as Dialog, d as DialogClose, m as DialogDescription, h as DialogFooter, f as DialogPopup, p as DialogTitle, u as DialogTrigger, a as backdropClass, c as dialogDescriptionClass, o as dialogPopupClass, s as dialogTitleClass };
@@ -0,0 +1,11 @@
1
+ import * as React from 'react';
2
+ export interface EmptyStateProps {
3
+ /** lucide 아이콘 (24px로 맞춰져요) */
4
+ icon?: React.ReactNode;
5
+ title: React.ReactNode;
6
+ description?: React.ReactNode;
7
+ /** 다음에 할 행동 하나. 보통 <Button> */
8
+ action?: React.ReactNode;
9
+ className?: string;
10
+ }
11
+ export declare function EmptyState({ icon, title, description, action, className }: EmptyStateProps): React.JSX.Element;
@@ -0,0 +1,17 @@
1
+ "use client";
2
+ import { StateLayout as e } from "./state-layout.js";
3
+ import "react";
4
+ import { jsx as t } from "react/jsx-runtime";
5
+ //#region src/components/empty-state.tsx
6
+ function n({ icon: n, title: r, description: i, action: a, className: o }) {
7
+ return /* @__PURE__ */ t(e, {
8
+ icon: n,
9
+ iconClassName: "sn:text-muted",
10
+ title: r,
11
+ description: i,
12
+ className: o,
13
+ children: a
14
+ });
15
+ }
16
+ //#endregion
17
+ export { n as EmptyState };