@xonic0506/sonic-ui 0.2.0 → 0.4.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.
@@ -0,0 +1,45 @@
1
+ "use client";
2
+ import { cn as e } from "../lib/cn.js";
3
+ import { illustrations as t } from "./illustration.js";
4
+ import "react";
5
+ import { jsx as n, jsxs as r } from "react/jsx-runtime";
6
+ //#region src/components/error-page.tsx
7
+ var i = {
8
+ 404: ["페이지를 찾을 수 없어요", "주소가 바뀌었거나 삭제된 페이지예요. 주소를 확인해 주세요."],
9
+ 403: ["이 페이지를 볼 권한이 없어요", "관리자에게 권한을 요청해 주세요."],
10
+ 500: ["서버에 문제가 생겼어요", "잠시 뒤 다시 시도해 주세요."]
11
+ };
12
+ function a({ status: a, title: o = i[a][0], description: s = i[a][1], action: c, className: l }) {
13
+ return /* @__PURE__ */ r("div", {
14
+ className: e("sn:box-border sn:flex sn:w-full sn:flex-col sn:items-center sn:gap-6 sn:px-6 sn:py-16 sn:text-center sn:font-sans", l),
15
+ children: [
16
+ /* @__PURE__ */ n("img", {
17
+ src: t[a],
18
+ alt: "",
19
+ width: 320,
20
+ height: 213,
21
+ className: "sn:block sn:h-auto sn:max-w-full"
22
+ }),
23
+ /* @__PURE__ */ r("div", {
24
+ className: "sn:flex sn:flex-col sn:items-center sn:gap-2",
25
+ children: [
26
+ /* @__PURE__ */ r("p", {
27
+ className: "sn:m-0 sn:font-mono sn:text-caption sn:tabular-nums sn:text-muted",
28
+ children: ["오류 코드 ", a]
29
+ }),
30
+ /* @__PURE__ */ n("h1", {
31
+ className: "sn:m-0 sn:text-title sn:text-default",
32
+ children: o
33
+ }),
34
+ s && /* @__PURE__ */ n("p", {
35
+ className: "sn:m-0 sn:text-body sn:text-muted",
36
+ children: s
37
+ })
38
+ ]
39
+ }),
40
+ c
41
+ ]
42
+ });
43
+ }
44
+ //#endregion
45
+ export { a as ErrorPage };
@@ -1,6 +1,6 @@
1
1
  import * as React from 'react';
2
2
  export interface ErrorStateProps {
3
- /** 기본값: triangle-alert */
3
+ /** 없으면 캐릭터 일러스트(120). 좁은 곳에서는 <TriangleAlert /> 같은 아이콘을 넘겨요 */
4
4
  icon?: React.ReactNode;
5
5
  title: React.ReactNode;
6
6
  description?: React.ReactNode;
@@ -1,22 +1,24 @@
1
1
  "use client";
2
2
  import { Button as e } from "./button.js";
3
- import { StateLayout as t } from "./state-layout.js";
3
+ import { illustrations as t } from "./illustration.js";
4
+ import { StateLayout as n } from "./state-layout.js";
4
5
  import "react";
5
- import { RefreshCw as n, TriangleAlert as r } from "lucide-react";
6
+ import { RefreshCw as r } from "lucide-react";
6
7
  import { jsx as i, jsxs as a } from "react/jsx-runtime";
7
8
  //#region src/components/error-state.tsx
8
- function o({ icon: o = /* @__PURE__ */ i(r, {}), title: s, description: c, onRetry: l, retryLabel: u = "다시 시도", className: d }) {
9
- return /* @__PURE__ */ i(t, {
9
+ function o({ icon: o, title: s, description: c, onRetry: l, retryLabel: u = "다시 시도", className: d }) {
10
+ return /* @__PURE__ */ i(n, {
10
11
  role: "alert",
11
12
  icon: o,
12
13
  iconClassName: "sn:text-danger",
14
+ illustration: t.error,
13
15
  title: s,
14
16
  description: c,
15
17
  className: d,
16
18
  children: l && /* @__PURE__ */ a(e, {
17
19
  variant: "secondary",
18
20
  onClick: l,
19
- children: [/* @__PURE__ */ i(n, {}), u]
21
+ children: [/* @__PURE__ */ i(r, {}), u]
20
22
  })
21
23
  });
22
24
  }
@@ -0,0 +1,57 @@
1
+ import * as React from 'react';
2
+ /** 바이트를 "2.4 MB"처럼 표시 */
3
+ export declare function formatBytes(bytes: number): string;
4
+ export interface FileRejection {
5
+ file: File;
6
+ reason: 'type' | 'size' | 'count';
7
+ /** 기본 문구. 바꾸려면 이 값 대신 직접 보여 주세요 */
8
+ message: string;
9
+ }
10
+ export interface FileDropzoneProps {
11
+ onFilesChange: (accepted: File[], rejected: FileRejection[]) => void;
12
+ /** 예: ".pdf", "image/*", "image/png,image/jpeg" */
13
+ accept?: string;
14
+ multiple?: boolean;
15
+ /** 남은 개수. multiple일 때만 */
16
+ maxFiles?: number;
17
+ /** 파일 하나의 최대 크기(바이트) */
18
+ maxSize?: number;
19
+ variant?: 'file' | 'image';
20
+ /** 영역 안 안내 문구. Field 밖에서 쓰면 버튼 이름이 돼요 */
21
+ title?: React.ReactNode;
22
+ description?: React.ReactNode;
23
+ invalid?: boolean;
24
+ disabled?: boolean;
25
+ name?: string;
26
+ id?: string;
27
+ className?: string;
28
+ }
29
+ /** Input처럼 Field로 감싸면 라벨·설명·에러·invalid·disabled가 연결돼요 */
30
+ export declare function FileDropzone({ onFilesChange, accept, multiple, maxFiles, maxSize, variant, title, description, invalid, disabled, name, id, className }: FileDropzoneProps): React.JSX.Element;
31
+ export interface FileItemData {
32
+ id: string;
33
+ name: string;
34
+ /** 바이트 */
35
+ size?: number;
36
+ /** MIME. 이미지 여부 판단 */
37
+ type?: string;
38
+ status: 'uploading' | 'done' | 'error';
39
+ /** 0~100 */
40
+ progress?: number;
41
+ /** 무엇이 실패했고 어떻게 고치는지 */
42
+ error?: string;
43
+ /** 완료된 파일을 열 주소 */
44
+ url?: string;
45
+ /** 썸네일: 서버 주소, data URL, blob URL 모두 가능 */
46
+ previewUrl?: string;
47
+ /** 방금 고른 로컬 파일. previewUrl이 없으면 blob URL을 만들어 미리보기에 써요 */
48
+ file?: File;
49
+ }
50
+ export interface FileListProps {
51
+ items: FileItemData[];
52
+ layout?: 'list' | 'grid';
53
+ onRemove?: (id: string) => void;
54
+ onRetry?: (id: string) => void;
55
+ className?: string;
56
+ }
57
+ export declare function FileList({ items, layout, onRemove, onRetry, className }: FileListProps): React.JSX.Element;
@@ -0,0 +1,238 @@
1
+ "use client";
2
+ import { cn as e } from "../lib/cn.js";
3
+ import { Button as t } from "./button.js";
4
+ import { Progress as n } from "./progress.js";
5
+ import { Link as r } from "./link.js";
6
+ import * as i from "react";
7
+ import { CircleAlert as a, File as o, Image as s, RefreshCw as c, Upload as l, X as u } from "lucide-react";
8
+ import { Fragment as d, jsx as f, jsxs as p } from "react/jsx-runtime";
9
+ import { Field as m } from "@base-ui/react/field";
10
+ //#region src/components/file-upload.tsx
11
+ function h(e) {
12
+ if (e < 1024) return `${e} B`;
13
+ let t = [
14
+ "KB",
15
+ "MB",
16
+ "GB"
17
+ ], n = e / 1024, r = 0;
18
+ for (; n >= 1024 && r < t.length - 1;) n /= 1024, r++;
19
+ return `${Number(n.toFixed(1))} ${t[r]}`;
20
+ }
21
+ function g(e, t) {
22
+ if (!t) return !0;
23
+ let n = e.name.toLowerCase(), r = e.type.toLowerCase();
24
+ return t.split(",").map((e) => e.trim().toLowerCase()).filter(Boolean).some((e) => e.startsWith(".") ? n.endsWith(e) : e.endsWith("/*") ? r.startsWith(e.slice(0, -1)) : r === e);
25
+ }
26
+ function _(e, { accept: t, maxSize: n, maxFiles: r, multiple: i }) {
27
+ let a = i ? r ?? Infinity : 1, o = [], s = [];
28
+ for (let r of e) g(r, t) ? n !== void 0 && r.size > n ? s.push({
29
+ file: r,
30
+ reason: "size",
31
+ message: `${r.name}: ${h(n)}보다 큰 파일은 올릴 수 없어요.`
32
+ }) : o.length >= a ? s.push({
33
+ file: r,
34
+ reason: "count",
35
+ message: a <= 0 ? "더 올릴 수 없어요. 파일을 삭제한 뒤 다시 골라 주세요." : `${a}개까지 올릴 수 있어요.`
36
+ }) : o.push(r) : s.push({
37
+ file: r,
38
+ reason: "type",
39
+ message: `${r.name}: 올릴 수 없는 형식이에요. ${t} 파일만 올려 주세요.`
40
+ });
41
+ return {
42
+ accepted: o,
43
+ rejected: s
44
+ };
45
+ }
46
+ function v({ onFilesChange: t, accept: n, multiple: r, maxFiles: a, maxSize: o, variant: c = "file", title: u, description: h, invalid: g, disabled: v, name: y, id: b, className: x }) {
47
+ let S = i.useRef(null), [C, w] = i.useState(!1), [T, E] = i.useState(), D = i.useId(), O = (e) => {
48
+ if (!e.length) return;
49
+ let { accepted: i, rejected: s } = _(e, {
50
+ accept: n,
51
+ maxSize: o,
52
+ maxFiles: a,
53
+ multiple: r
54
+ });
55
+ E(s.length ? s[0].message + (s.length > 1 ? ` (외 ${s.length - 1}개)` : "") : void 0), t(i, s);
56
+ }, k = g || !!T, A = c === "image" ? s : l;
57
+ return /* @__PURE__ */ p(d, { children: [/* @__PURE__ */ p(m.Control, {
58
+ render: /* @__PURE__ */ f("button", { type: "button" }),
59
+ id: b,
60
+ disabled: v,
61
+ "aria-describedby": D,
62
+ "data-dragging": C ? "" : void 0,
63
+ "aria-invalid": g || void 0,
64
+ "data-invalid": k ? "" : void 0,
65
+ onClick: () => S.current?.click(),
66
+ onDragEnter: (e) => {
67
+ e.preventDefault(), v || w(!0);
68
+ },
69
+ onDragOver: (e) => e.preventDefault(),
70
+ onDragLeave: (e) => {
71
+ e.currentTarget.contains(e.relatedTarget) || w(!1);
72
+ },
73
+ onDrop: (e) => {
74
+ e.preventDefault(), w(!1), v || O([...e.dataTransfer.files]);
75
+ },
76
+ className: e("sn:group sn:box-border sn:flex sn:w-full sn:flex-col sn:items-center sn:gap-2 sn:rounded-lg sn:border sn:border-dashed sn:border-strong sn:bg-surface sn:px-4 sn:py-6 sn:font-sans sn:text-center sn:cursor-pointer sn:transition-colors sn:duration-(--sn-duration-fast) sn:ease-standard sn:hover:not-disabled:bg-subtle sn:data-dragging:border-focus sn:data-dragging:bg-subtle sn:data-invalid:border-danger sn:aria-invalid:border-danger sn:focus-visible:outline-2 sn:focus-visible:outline-offset-2 sn:focus-visible:outline-focus sn:disabled:cursor-not-allowed sn:disabled:opacity-(--sn-opacity-disabled) sn:motion-reduce:transition-none", x),
77
+ children: [
78
+ /* @__PURE__ */ f(A, { className: "sn:size-6 sn:text-muted sn:group-data-invalid:text-danger sn:group-aria-invalid:text-danger" }),
79
+ /* @__PURE__ */ f("span", {
80
+ className: "sn:text-body-strong sn:text-default",
81
+ children: u ?? (c === "image" ? "이미지를 끌어다 놓거나 눌러서 고르세요" : "파일을 끌어다 놓거나 눌러서 고르세요")
82
+ }),
83
+ /* @__PURE__ */ f("span", {
84
+ id: D,
85
+ "aria-live": "polite",
86
+ className: e("sn:text-caption", T ? "sn:text-danger" : "sn:text-muted"),
87
+ children: T ?? h
88
+ })
89
+ ]
90
+ }), /* @__PURE__ */ f("input", {
91
+ ref: S,
92
+ type: "file",
93
+ hidden: !0,
94
+ name: y,
95
+ accept: n,
96
+ multiple: r,
97
+ disabled: v,
98
+ onChange: (e) => {
99
+ O([...e.target.files ?? []]), e.target.value = "";
100
+ }
101
+ })] });
102
+ }
103
+ function y(e) {
104
+ let [t, n] = i.useState(), r = (e.type ?? e.file?.type ?? "").startsWith("image/");
105
+ return i.useEffect(() => {
106
+ if (e.previewUrl || !e.file || !r) return;
107
+ let t = URL.createObjectURL(e.file);
108
+ return n(t), () => URL.revokeObjectURL(t);
109
+ }, [
110
+ e.previewUrl,
111
+ e.file,
112
+ r
113
+ ]), e.previewUrl ?? t;
114
+ }
115
+ function b({ items: t, layout: n = "list", onRemove: r, onRetry: i, className: a }) {
116
+ let o = n === "grid" ? S : x;
117
+ return /* @__PURE__ */ f("ul", {
118
+ className: e("sn:m-0 sn:list-none sn:p-0 sn:font-sans", n === "grid" ? "sn:flex sn:flex-wrap sn:gap-3" : "sn:flex sn:flex-col sn:gap-2", a),
119
+ children: t.map((e) => /* @__PURE__ */ f(o, {
120
+ item: e,
121
+ onRemove: r,
122
+ onRetry: i
123
+ }, e.id))
124
+ });
125
+ }
126
+ function x({ item: t, onRemove: i, onRetry: a }) {
127
+ let s = t.status === "error";
128
+ return /* @__PURE__ */ p("li", {
129
+ className: e("sn:box-border sn:flex sn:items-center sn:gap-3 sn:rounded-md sn:border sn:bg-surface sn:p-3", s ? "sn:border-danger" : "sn:border-default"),
130
+ children: [
131
+ /* @__PURE__ */ f("span", {
132
+ className: "sn:flex sn:size-8 sn:shrink-0 sn:items-center sn:justify-center sn:rounded-md sn:bg-subtle",
133
+ children: /* @__PURE__ */ f(o, { className: e("sn:size-4", s ? "sn:text-danger" : "sn:text-muted") })
134
+ }),
135
+ /* @__PURE__ */ p("div", {
136
+ className: "sn:flex sn:min-w-0 sn:flex-1 sn:flex-col sn:gap-1",
137
+ children: [
138
+ /* @__PURE__ */ p("div", {
139
+ className: "sn:flex sn:items-center sn:justify-between sn:gap-2",
140
+ children: [t.status === "done" && t.url ? /* @__PURE__ */ f(r, {
141
+ href: t.url,
142
+ target: "_blank",
143
+ rel: "noopener noreferrer",
144
+ className: "sn:min-w-0 sn:truncate sn:text-body",
145
+ children: t.name
146
+ }) : /* @__PURE__ */ f("span", {
147
+ className: "sn:min-w-0 sn:truncate sn:text-body sn:text-default",
148
+ children: t.name
149
+ }), /* @__PURE__ */ f("span", {
150
+ className: "sn:shrink-0 sn:font-mono sn:text-caption sn:text-muted",
151
+ children: t.status === "uploading" ? `${Math.round(t.progress ?? 0)}%` : t.size === void 0 ? null : h(t.size)
152
+ })]
153
+ }),
154
+ t.status === "uploading" && /* @__PURE__ */ f(n, {
155
+ value: t.progress ?? 0,
156
+ "aria-label": `${t.name} 올리는 중`
157
+ }),
158
+ s && t.error && /* @__PURE__ */ f("span", {
159
+ className: "sn:text-caption sn:text-danger",
160
+ children: t.error
161
+ })
162
+ ]
163
+ }),
164
+ /* @__PURE__ */ f(C, {
165
+ item: t,
166
+ onRemove: i,
167
+ onRetry: a,
168
+ variant: "ghost"
169
+ })
170
+ ]
171
+ });
172
+ }
173
+ function S({ item: t, onRemove: r, onRetry: i }) {
174
+ let o = t.status === "error", c = y(t);
175
+ return /* @__PURE__ */ p("li", {
176
+ className: "sn:flex sn:w-24 sn:flex-col sn:gap-1",
177
+ children: [
178
+ /* @__PURE__ */ p("div", {
179
+ className: e("sn:relative sn:box-border sn:flex sn:size-24 sn:flex-col sn:justify-between sn:overflow-hidden sn:rounded-md sn:border sn:bg-subtle sn:p-1", o ? "sn:border-danger" : "sn:border-default"),
180
+ children: [
181
+ c && !o ? /* @__PURE__ */ f("img", {
182
+ src: c,
183
+ alt: t.name,
184
+ className: "sn:absolute sn:inset-0 sn:size-full sn:object-cover"
185
+ }) : /* @__PURE__ */ f("span", {
186
+ className: "sn:absolute sn:inset-0 sn:flex sn:items-center sn:justify-center",
187
+ children: o ? /* @__PURE__ */ f(a, { className: "sn:size-6 sn:text-danger" }) : /* @__PURE__ */ f(s, { className: "sn:size-6 sn:text-muted" })
188
+ }),
189
+ /* @__PURE__ */ f("div", {
190
+ className: "sn:relative sn:flex sn:justify-end",
191
+ children: /* @__PURE__ */ f(C, {
192
+ item: t,
193
+ onRemove: r,
194
+ onRetry: i,
195
+ variant: "secondary"
196
+ })
197
+ }),
198
+ t.status === "uploading" && /* @__PURE__ */ f("div", {
199
+ className: "sn:relative",
200
+ children: /* @__PURE__ */ f(n, {
201
+ value: t.progress ?? 0,
202
+ "aria-label": `${t.name} 올리는 중`
203
+ })
204
+ })
205
+ ]
206
+ }),
207
+ /* @__PURE__ */ f("span", {
208
+ className: e("sn:truncate sn:text-caption", o ? "sn:text-danger" : "sn:text-muted"),
209
+ title: t.name,
210
+ children: t.name
211
+ }),
212
+ o && t.error && /* @__PURE__ */ f("span", {
213
+ className: "sn:sr-only",
214
+ children: t.error
215
+ })
216
+ ]
217
+ });
218
+ }
219
+ function C({ item: e, onRemove: n, onRetry: r, variant: i }) {
220
+ return /* @__PURE__ */ p("div", {
221
+ className: "sn:flex sn:shrink-0",
222
+ children: [e.status === "error" && r && /* @__PURE__ */ f(t, {
223
+ size: "icon-sm",
224
+ variant: i,
225
+ "aria-label": `${e.name} 다시 시도`,
226
+ onClick: () => r(e.id),
227
+ children: /* @__PURE__ */ f(c, {})
228
+ }), n && /* @__PURE__ */ f(t, {
229
+ size: "icon-sm",
230
+ variant: i,
231
+ "aria-label": `${e.name} 삭제`,
232
+ onClick: () => n(e.id),
233
+ children: /* @__PURE__ */ f(u, {})
234
+ })]
235
+ });
236
+ }
237
+ //#endregion
238
+ export { v as FileDropzone, b as FileList, h as formatBytes };
@@ -0,0 +1,7 @@
1
+ export declare const illustrations: {
2
+ empty: string;
3
+ error: string;
4
+ 404: string;
5
+ 403: string;
6
+ 500: string;
7
+ };
@@ -0,0 +1,11 @@
1
+ "use client";
2
+ //#region src/components/illustration.ts
3
+ var e = {
4
+ empty: new URL("../illustrations/empty.webp", import.meta.url).href,
5
+ error: new URL("../illustrations/error.webp", import.meta.url).href,
6
+ 404: new URL("../illustrations/error-404.webp", import.meta.url).href,
7
+ 403: new URL("../illustrations/error-403.webp", import.meta.url).href,
8
+ 500: new URL("../illustrations/error-500.webp", import.meta.url).href
9
+ };
10
+ //#endregion
11
+ export { e as illustrations };
@@ -0,0 +1,9 @@
1
+ import * as React from 'react';
2
+ export interface LinkProps extends Omit<React.AnchorHTMLAttributes<HTMLAnchorElement>, 'className'> {
3
+ /** 새 탭으로 열고 external-link 아이콘을 붙여요 */
4
+ external?: boolean;
5
+ className?: string;
6
+ /** 다른 링크 컴포넌트로 렌더링 (예: render={<NextLink href="/docs" />}) */
7
+ render?: React.ReactElement<Record<string, unknown>>;
8
+ }
9
+ export declare const Link: React.ForwardRefExoticComponent<LinkProps & React.RefAttributes<HTMLAnchorElement>>;
@@ -0,0 +1,30 @@
1
+ "use client";
2
+ import { cn as e } from "../lib/cn.js";
3
+ import * as t from "react";
4
+ import { ExternalLink as n } from "lucide-react";
5
+ import { Fragment as r, jsx as i, jsxs as a } from "react/jsx-runtime";
6
+ //#region src/components/link.tsx
7
+ var o = "sn:rounded-sm sn:text-default sn:underline sn:underline-offset-2 sn:transition-colors sn:duration-(--sn-duration-fast) sn:ease-standard sn:hover:text-muted sn:focus-visible:outline-2 sn:focus-visible:outline-offset-2 sn:focus-visible:outline-focus sn:motion-reduce:transition-none", s = t.forwardRef(function({ external: s, className: c, render: l, children: u, ...d }, f) {
8
+ let p = s ? /* @__PURE__ */ a(r, { children: [
9
+ u,
10
+ /* @__PURE__ */ i(n, { className: "sn:ml-1 sn:inline-block sn:size-4 sn:align-text-bottom" }),
11
+ /* @__PURE__ */ i("span", {
12
+ className: "sn:sr-only",
13
+ children: "(새 탭에서 열림)"
14
+ })
15
+ ] }) : u, m = {
16
+ ref: f,
17
+ className: e(o, c),
18
+ ...s && {
19
+ target: "_blank",
20
+ rel: "noopener noreferrer"
21
+ },
22
+ ...d
23
+ };
24
+ return l ? t.cloneElement(l, m, p) : /* @__PURE__ */ i("a", {
25
+ ...m,
26
+ children: p
27
+ });
28
+ });
29
+ //#endregion
30
+ export { s as Link };
@@ -1,11 +1,14 @@
1
1
  import * as React from 'react';
2
2
  export interface StateLayoutProps {
3
+ /** 넘기면 아이콘 원(48), 없으면 일러스트(120) */
3
4
  icon?: React.ReactNode;
4
5
  iconClassName: string;
6
+ /** 일러스트 주소 */
7
+ illustration: string;
5
8
  title: React.ReactNode;
6
9
  description?: React.ReactNode;
7
10
  className?: string;
8
11
  role?: 'alert';
9
12
  children?: React.ReactNode;
10
13
  }
11
- export declare function StateLayout({ icon, iconClassName, title, description, className, role, children }: StateLayoutProps): React.JSX.Element;
14
+ export declare function StateLayout({ icon, iconClassName, illustration, title, description, className, role, children }: StateLayoutProps): React.JSX.Element;
@@ -3,27 +3,33 @@ import { cn as e } from "../lib/cn.js";
3
3
  import "react";
4
4
  import { jsx as t, jsxs as n } from "react/jsx-runtime";
5
5
  //#region src/components/state-layout.tsx
6
- function r({ icon: r, iconClassName: i, title: a, description: o, className: s, role: c, children: l }) {
6
+ function r({ icon: r, iconClassName: i, illustration: a, title: o, description: s, className: c, role: l, children: u }) {
7
7
  return /* @__PURE__ */ n("div", {
8
- role: c,
9
- className: e("sn:box-border sn:flex sn:flex-col sn:items-center sn:gap-4 sn:px-6 sn:py-12 sn:text-center sn:font-sans", s),
8
+ role: l,
9
+ className: e("sn:box-border sn:flex sn:flex-col sn:items-center sn:gap-4 sn:px-6 sn:py-12 sn:text-center sn:font-sans", c),
10
10
  children: [
11
- r && /* @__PURE__ */ t("span", {
11
+ r ? /* @__PURE__ */ t("span", {
12
12
  "aria-hidden": !0,
13
13
  className: e("sn:inline-flex sn:size-12 sn:items-center sn:justify-center sn:rounded-full sn:bg-subtle sn:[&_svg]:size-6", i),
14
14
  children: r
15
+ }) : /* @__PURE__ */ t("img", {
16
+ src: a,
17
+ alt: "",
18
+ width: 120,
19
+ height: 120,
20
+ className: "sn:block"
15
21
  }),
16
22
  /* @__PURE__ */ n("div", {
17
23
  className: "sn:flex sn:flex-col sn:gap-1",
18
24
  children: [/* @__PURE__ */ t("p", {
19
25
  className: "sn:m-0 sn:text-body-strong sn:text-default",
20
- children: a
21
- }), o && /* @__PURE__ */ t("p", {
22
- className: "sn:m-0 sn:text-body sn:text-muted",
23
26
  children: o
27
+ }), s && /* @__PURE__ */ t("p", {
28
+ className: "sn:m-0 sn:text-body sn:text-muted",
29
+ children: s
24
30
  })]
25
31
  }),
26
- l
32
+ u
27
33
  ]
28
34
  });
29
35
  }
@@ -0,0 +1,24 @@
1
+ import * as React from 'react';
2
+ /** 넓으면 가로로 스크롤해요 */
3
+ export declare const Table: React.ForwardRefExoticComponent<Omit<React.HTMLAttributes<HTMLTableElement>, "className"> & {
4
+ className?: string;
5
+ } & React.RefAttributes<HTMLTableElement>>;
6
+ export declare const TableHeader: React.ForwardRefExoticComponent<Omit<React.HTMLAttributes<HTMLTableSectionElement>, "className"> & {
7
+ className?: string;
8
+ } & React.RefAttributes<HTMLTableSectionElement>>;
9
+ export declare const TableBody: React.ForwardRefExoticComponent<Omit<React.HTMLAttributes<HTMLTableSectionElement>, "className"> & {
10
+ className?: string;
11
+ } & React.RefAttributes<HTMLTableSectionElement>>;
12
+ export declare const TableRow: React.ForwardRefExoticComponent<Omit<React.HTMLAttributes<HTMLTableRowElement>, "className"> & {
13
+ className?: string;
14
+ } & React.RefAttributes<HTMLTableRowElement>>;
15
+ export declare const TableHead: React.ForwardRefExoticComponent<Omit<React.TdHTMLAttributes<HTMLTableCellElement>, "align" | "className"> & {
16
+ /** 숫자 열은 end */
17
+ align?: 'start' | 'end';
18
+ className?: string;
19
+ } & React.RefAttributes<HTMLTableCellElement>>;
20
+ export declare const TableCell: React.ForwardRefExoticComponent<Omit<React.TdHTMLAttributes<HTMLTableCellElement>, "align" | "className"> & {
21
+ /** 숫자 열은 end */
22
+ align?: 'start' | 'end';
23
+ className?: string;
24
+ } & React.RefAttributes<HTMLTableCellElement>>;
@@ -0,0 +1,46 @@
1
+ "use client";
2
+ import { cn as e } from "../lib/cn.js";
3
+ import * as t from "react";
4
+ import { jsx as n } from "react/jsx-runtime";
5
+ //#region src/components/table.tsx
6
+ var r = t.forwardRef(function({ className: t, ...r }, i) {
7
+ return /* @__PURE__ */ n("div", {
8
+ className: "sn:w-full sn:overflow-x-auto",
9
+ children: /* @__PURE__ */ n("table", {
10
+ ref: i,
11
+ className: e("sn:w-full sn:border-separate sn:border-spacing-0 sn:font-sans sn:text-body sn:text-default", t),
12
+ ...r
13
+ })
14
+ });
15
+ }), i = t.forwardRef(function(e, t) {
16
+ return /* @__PURE__ */ n("thead", {
17
+ ref: t,
18
+ ...e
19
+ });
20
+ }), a = t.forwardRef(function(e, t) {
21
+ return /* @__PURE__ */ n("tbody", {
22
+ ref: t,
23
+ ...e
24
+ });
25
+ }), o = t.forwardRef(function({ className: e, ...t }, r) {
26
+ return /* @__PURE__ */ n("tr", {
27
+ ref: r,
28
+ className: e,
29
+ ...t
30
+ });
31
+ }), s = "sn:box-border sn:h-10 sn:border-b sn:border-default sn:px-3 sn:py-0 sn:align-middle", c = t.forwardRef(function({ align: t = "start", className: r, ...i }, a) {
32
+ return /* @__PURE__ */ n("th", {
33
+ ref: a,
34
+ scope: "col",
35
+ className: e(s, "sn:text-label sn:text-muted", t === "end" ? "sn:text-right" : "sn:text-left", r),
36
+ ...i
37
+ });
38
+ }), l = t.forwardRef(function({ align: t = "start", className: r, ...i }, a) {
39
+ return /* @__PURE__ */ n("td", {
40
+ ref: a,
41
+ className: e(s, t === "end" ? "sn:text-right sn:font-mono" : "sn:text-left", r),
42
+ ...i
43
+ });
44
+ });
45
+ //#endregion
46
+ export { r as Table, a as TableBody, l as TableCell, c as TableHead, i as TableHeader, o as TableRow };
@@ -0,0 +1,36 @@
1
+ import * as React from 'react';
2
+ interface ToggleGroupBaseProps {
3
+ disabled?: boolean;
4
+ className?: string;
5
+ 'aria-label'?: string;
6
+ children?: React.ReactNode;
7
+ }
8
+ /** 단일은 항상 하나가 눌려 있어요(다시 눌러도 꺼지지 않음). multiple이면 값이 배열이에요 */
9
+ export type ToggleGroupProps = ToggleGroupBaseProps & ({
10
+ multiple?: false;
11
+ value?: string;
12
+ defaultValue?: string;
13
+ onValueChange?: (value: string) => void;
14
+ } | {
15
+ multiple: true;
16
+ value?: string[];
17
+ defaultValue?: string[];
18
+ onValueChange?: (value: string[]) => void;
19
+ });
20
+ export declare function ToggleGroup(props: ToggleGroupProps): React.JSX.Element;
21
+ interface ToggleGroupItemBaseProps {
22
+ value: string;
23
+ disabled?: boolean;
24
+ className?: string;
25
+ children?: React.ReactNode;
26
+ }
27
+ /** 아이콘만 있는 항목은 읽을 글자가 없어서 aria-label이 필수예요 */
28
+ export type ToggleGroupItemProps = ToggleGroupItemBaseProps & ({
29
+ iconOnly?: false;
30
+ 'aria-label'?: string;
31
+ } | {
32
+ iconOnly: true;
33
+ 'aria-label': string;
34
+ });
35
+ export declare const ToggleGroupItem: React.ForwardRefExoticComponent<ToggleGroupItemProps & React.RefAttributes<HTMLButtonElement>>;
36
+ export {};
@@ -0,0 +1,44 @@
1
+ "use client";
2
+ import { cn as e } from "../lib/cn.js";
3
+ import * as t from "react";
4
+ import { jsx as n } from "react/jsx-runtime";
5
+ import { ToggleGroup as r } from "@base-ui/react/toggle-group";
6
+ import { Toggle as i } from "@base-ui/react/toggle";
7
+ //#region src/components/toggle-group.tsx
8
+ var a = "sn:inline-flex sn:items-center sn:rounded-md sn:bg-subtle sn:p-1";
9
+ function o(i) {
10
+ let { disabled: o, className: s, children: c } = i, [l, u] = t.useState(i.multiple ? void 0 : i.defaultValue), d = {
11
+ disabled: o,
12
+ "aria-label": i["aria-label"],
13
+ className: e(a, s)
14
+ };
15
+ if (i.multiple) return /* @__PURE__ */ n(r, {
16
+ ...d,
17
+ multiple: !0,
18
+ value: i.value,
19
+ defaultValue: i.defaultValue,
20
+ onValueChange: i.onValueChange && ((e) => i.onValueChange(e)),
21
+ children: c
22
+ });
23
+ let f = i.value === void 0 ? l : i.value;
24
+ return /* @__PURE__ */ n(r, {
25
+ ...d,
26
+ value: f === void 0 ? [] : [f],
27
+ onValueChange: (e) => {
28
+ e.length && (u(e[0]), i.onValueChange?.(e[0]));
29
+ },
30
+ children: c
31
+ });
32
+ }
33
+ var s = "sn:box-border sn:inline-flex sn:h-6 sn:items-center sn:justify-center sn:gap-1 sn:rounded-sm sn:border sn:border-transparent sn:bg-transparent sn:px-2 sn:font-sans sn:text-label sn:text-muted sn:cursor-pointer sn:select-none sn:transition-colors sn:duration-(--sn-duration-fast) sn:ease-standard sn:hover:not-data-disabled:text-default sn:data-pressed:border-default sn:data-pressed:bg-surface sn:data-pressed:text-default sn:focus-visible:outline-2 sn:focus-visible:-outline-offset-2 sn:focus-visible:outline-focus sn:data-disabled:cursor-not-allowed sn:data-disabled:opacity-(--sn-opacity-disabled) sn:motion-reduce:transition-none sn:[&_svg]:size-4 sn:[&_svg]:shrink-0", c = t.forwardRef(function({ value: t, disabled: r, iconOnly: a, className: o, children: c, ...l }, u) {
34
+ return /* @__PURE__ */ n(i, {
35
+ ref: u,
36
+ value: t,
37
+ disabled: r,
38
+ "aria-label": l["aria-label"],
39
+ className: e(s, a && "sn:w-6 sn:px-0", o),
40
+ children: c
41
+ });
42
+ });
43
+ //#endregion
44
+ export { o as ToggleGroup, c as ToggleGroupItem };
Binary file