@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
@@ -4,41 +4,44 @@ import * as t from "react";
4
4
  import { jsx as n, jsxs as r } from "react/jsx-runtime";
5
5
  import { Dialog as i } from "@base-ui/react/dialog";
6
6
  //#region src/components/dialog.tsx
7
- function a({ onOpenChange: e, ...t }) {
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 }) {
8
9
  return /* @__PURE__ */ n(i.Root, {
9
10
  ...t,
10
11
  onOpenChange: e && ((t) => e(t))
11
12
  });
12
13
  }
13
- var o = t.forwardRef(function(e, t) {
14
+ var u = t.forwardRef(function(e, t) {
14
15
  return /* @__PURE__ */ n(i.Trigger, {
15
16
  ref: t,
16
17
  ...e
17
18
  });
18
- }), s = t.forwardRef(function(e, t) {
19
+ }), d = t.forwardRef(function(e, t) {
19
20
  return /* @__PURE__ */ n(i.Close, {
20
21
  ref: t,
21
22
  ...e
22
23
  });
23
- }), c = t.forwardRef(function({ className: t, ...a }, o) {
24
- return /* @__PURE__ */ r(i.Portal, { children: [/* @__PURE__ */ n(i.Backdrop, { className: "sn:fixed sn:inset-0 sn:z-50 sn:bg-overlay sn:transition-opacity sn:duration-(--sn-duration-normal) sn:ease-standard sn:data-starting-style:opacity-0 sn:data-ending-style:opacity-0 sn:motion-reduce:transition-none" }), /* @__PURE__ */ n(i.Popup, {
25
- ref: o,
26
- className: e("sn:fixed sn:top-1/2 sn:left-1/2 sn:z-50 sn:box-border sn:flex sn:w-110 sn:max-w-[calc(100vw-2*var(--sn-space-4))] sn:-translate-x-1/2 sn:-translate-y-1/2 sn:flex-col sn:gap-4 sn:rounded-lg sn:border sn:border-default sn:bg-surface sn:p-6 sn:font-sans sn:text-body sn:text-default sn:outline-none sn:transition-[opacity,scale] sn:duration-(--sn-duration-normal) sn:ease-standard sn:data-starting-style:scale-98 sn:data-starting-style:opacity-0 sn:data-ending-style:scale-98 sn:data-ending-style:opacity-0 sn:motion-reduce:transition-none", t),
27
- ...a
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
28
31
  })] });
29
- }), l = t.forwardRef(function({ className: t, ...r }, a) {
32
+ }), p = t.forwardRef(function({ className: t, ...r }, a) {
30
33
  return /* @__PURE__ */ n(i.Title, {
31
34
  ref: a,
32
- className: e("sn:m-0 sn:text-title sn:text-default", t),
35
+ className: e(s, t),
33
36
  ...r
34
37
  });
35
- }), u = t.forwardRef(function({ className: t, ...r }, a) {
38
+ }), m = t.forwardRef(function({ className: t, ...r }, a) {
36
39
  return /* @__PURE__ */ n(i.Description, {
37
40
  ref: a,
38
- className: e("sn:-mt-3 sn:mb-0 sn:text-caption sn:text-muted", t),
41
+ className: e(c, t),
39
42
  ...r
40
43
  });
41
- }), d = t.forwardRef(function({ className: t, ...r }, i) {
44
+ }), h = t.forwardRef(function({ className: t, ...r }, i) {
42
45
  return /* @__PURE__ */ n("div", {
43
46
  ref: i,
44
47
  className: e("sn:mt-2 sn:flex sn:justify-end sn:gap-2", t),
@@ -46,4 +49,4 @@ var o = t.forwardRef(function(e, t) {
46
49
  });
47
50
  });
48
51
  //#endregion
49
- export { a as Dialog, s as DialogClose, u as DialogDescription, d as DialogFooter, c as DialogPopup, l as DialogTitle, o as DialogTrigger };
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 };
@@ -0,0 +1,12 @@
1
+ import * as React from 'react';
2
+ export interface ErrorStateProps {
3
+ /** 기본값: triangle-alert */
4
+ icon?: React.ReactNode;
5
+ title: React.ReactNode;
6
+ description?: React.ReactNode;
7
+ /** 있으면 "다시 시도" 버튼을 보여 줘요 */
8
+ onRetry?: () => void;
9
+ retryLabel?: string;
10
+ className?: string;
11
+ }
12
+ export declare function ErrorState({ icon, title, description, onRetry, retryLabel, className }: ErrorStateProps): React.JSX.Element;
@@ -0,0 +1,24 @@
1
+ "use client";
2
+ import { Button as e } from "./button.js";
3
+ import { StateLayout as t } from "./state-layout.js";
4
+ import "react";
5
+ import { RefreshCw as n, TriangleAlert as r } from "lucide-react";
6
+ import { jsx as i, jsxs as a } from "react/jsx-runtime";
7
+ //#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, {
10
+ role: "alert",
11
+ icon: o,
12
+ iconClassName: "sn:text-danger",
13
+ title: s,
14
+ description: c,
15
+ className: d,
16
+ children: l && /* @__PURE__ */ a(e, {
17
+ variant: "secondary",
18
+ onClick: l,
19
+ children: [/* @__PURE__ */ i(n, {}), u]
20
+ })
21
+ });
22
+ }
23
+ //#endregion
24
+ export { o as ErrorState };
@@ -0,0 +1,13 @@
1
+ import * as React from 'react';
2
+ export interface FieldProps {
3
+ label: React.ReactNode;
4
+ description?: React.ReactNode;
5
+ /** invalid일 때만 보이고, 그때는 description 자리를 대신해요 */
6
+ error?: React.ReactNode;
7
+ /** 안의 Input에 자동으로 전달돼요. Input에는 invalid를 따로 주지 않아요 */
8
+ invalid?: boolean;
9
+ disabled?: boolean;
10
+ className?: string;
11
+ children: React.ReactNode;
12
+ }
13
+ export declare const Field: React.ForwardRefExoticComponent<FieldProps & React.RefAttributes<HTMLDivElement>>;
@@ -0,0 +1,33 @@
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 { Field as i } from "@base-ui/react/field";
6
+ //#region src/components/field.tsx
7
+ var a = t.forwardRef(function({ label: t, description: a, error: o, invalid: s, disabled: c, className: l, children: u }, d) {
8
+ let f = s && !!o;
9
+ return /* @__PURE__ */ r(i.Root, {
10
+ ref: d,
11
+ invalid: s,
12
+ disabled: c,
13
+ className: e("sn:flex sn:flex-col sn:gap-2 sn:font-sans", l),
14
+ children: [
15
+ /* @__PURE__ */ n(i.Label, {
16
+ className: "sn:text-label sn:text-default",
17
+ children: t
18
+ }),
19
+ u,
20
+ a && !f && /* @__PURE__ */ n(i.Description, {
21
+ className: "sn:m-0 sn:text-caption sn:text-muted",
22
+ children: a
23
+ }),
24
+ f && /* @__PURE__ */ n(i.Error, {
25
+ match: !0,
26
+ className: "sn:m-0 sn:text-caption sn:text-danger",
27
+ children: o
28
+ })
29
+ ]
30
+ });
31
+ });
32
+ //#endregion
33
+ export { a as Field };
@@ -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 };
@@ -1,4 +1,5 @@
1
1
  import * as React from 'react';
2
+ export declare const inputClass = "sn:box-border sn:m-0 sn:w-full sn:min-w-0 sn:rounded-md sn:border sn:border-default sn:bg-surface sn:px-3 sn:font-sans sn:text-body sn:text-default sn:placeholder:text-muted sn:transition-colors sn:duration-(--sn-duration-fast) sn:ease-standard sn:hover:not-data-disabled:border-strong sn:focus-visible:outline-2 sn:focus-visible:outline-offset-2 sn:focus-visible:outline-focus sn:aria-invalid:focus-visible:outline-danger sn:data-invalid:focus-visible:outline-danger sn:aria-invalid:border-danger sn:hover:aria-invalid:border-danger sn:data-invalid:border-danger sn:data-disabled:cursor-not-allowed sn:data-disabled:opacity-(--sn-opacity-disabled)";
2
3
  export interface InputProps extends Omit<React.InputHTMLAttributes<HTMLInputElement>, 'className' | 'size'> {
3
4
  /** 에러 상태. aria-invalid로 전달돼요 */
4
5
  invalid?: boolean;
@@ -4,13 +4,13 @@ import * as t from "react";
4
4
  import { jsx as n } from "react/jsx-runtime";
5
5
  import { Input as r } from "@base-ui/react/input";
6
6
  //#region src/components/input.tsx
7
- var i = t.forwardRef(function({ invalid: t, className: i, ...a }, o) {
7
+ var i = "sn:box-border sn:m-0 sn:w-full sn:min-w-0 sn:rounded-md sn:border sn:border-default sn:bg-surface sn:px-3 sn:font-sans sn:text-body sn:text-default sn:placeholder:text-muted sn:transition-colors sn:duration-(--sn-duration-fast) sn:ease-standard sn:hover:not-data-disabled:border-strong sn:focus-visible:outline-2 sn:focus-visible:outline-offset-2 sn:focus-visible:outline-focus sn:aria-invalid:focus-visible:outline-danger sn:data-invalid:focus-visible:outline-danger sn:aria-invalid:border-danger sn:hover:aria-invalid:border-danger sn:data-invalid:border-danger sn:data-disabled:cursor-not-allowed sn:data-disabled:opacity-(--sn-opacity-disabled)", a = t.forwardRef(function({ invalid: t, className: a, ...o }, s) {
8
8
  return /* @__PURE__ */ n(r, {
9
- ref: o,
9
+ ref: s,
10
10
  "aria-invalid": t || void 0,
11
- className: e("sn:box-border sn:m-0 sn:h-8 sn:w-full sn:min-w-0 sn:rounded-md sn:border sn:border-default sn:bg-surface sn:px-3 sn:font-sans sn:text-body sn:text-default sn:placeholder:text-muted sn:transition-colors sn:duration-(--sn-duration-fast) sn:ease-standard sn:hover:not-data-disabled:border-strong sn:focus-visible:outline-2 sn:focus-visible:outline-offset-2 sn:focus-visible:outline-focus sn:aria-invalid:border-danger sn:hover:aria-invalid:border-danger sn:data-invalid:border-danger sn:data-disabled:cursor-not-allowed sn:data-disabled:opacity-(--sn-opacity-disabled)", i),
12
- ...a
11
+ className: e("sn:h-8", i, a),
12
+ ...o
13
13
  });
14
14
  });
15
15
  //#endregion
16
- export { i as Input };
16
+ export { a as Input, i as inputClass };
@@ -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 };
@@ -0,0 +1,33 @@
1
+ import * as React from 'react';
2
+ export interface MenuProps {
3
+ open?: boolean;
4
+ defaultOpen?: boolean;
5
+ onOpenChange?: (open: boolean) => void;
6
+ children?: React.ReactNode;
7
+ }
8
+ export declare function Menu({ onOpenChange, ...props }: MenuProps): React.JSX.Element;
9
+ export interface MenuTriggerProps extends Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, 'className'> {
10
+ className?: string;
11
+ /** 보통 sonic-ui Button: render={<Button size="icon-md" variant="ghost" aria-label="더 보기" />} */
12
+ render?: React.ReactElement;
13
+ }
14
+ export declare const MenuTrigger: React.ForwardRefExoticComponent<MenuTriggerProps & React.RefAttributes<HTMLButtonElement>>;
15
+ export interface MenuPopupProps {
16
+ className?: string;
17
+ children?: React.ReactNode;
18
+ }
19
+ export declare const MenuPopup: React.ForwardRefExoticComponent<MenuPopupProps & React.RefAttributes<HTMLDivElement>>;
20
+ export interface MenuItemProps {
21
+ /** danger: 삭제처럼 위험한 동작. 맨 아래, 구분선 뒤에 둬요 */
22
+ tone?: 'default' | 'danger';
23
+ onClick?: () => void;
24
+ disabled?: boolean;
25
+ /** 고른 뒤 메뉴를 닫을지 (기본 true) */
26
+ closeOnClick?: boolean;
27
+ className?: string;
28
+ children?: React.ReactNode;
29
+ }
30
+ export declare const MenuItem: React.ForwardRefExoticComponent<MenuItemProps & React.RefAttributes<HTMLDivElement>>;
31
+ export declare function MenuSeparator({ className }: {
32
+ className?: string;
33
+ }): React.JSX.Element;
@@ -0,0 +1,44 @@
1
+ "use client";
2
+ import { cn as e } from "../lib/cn.js";
3
+ import { itemClass as t, popupClass as n } from "./popup-styles.js";
4
+ import * as r from "react";
5
+ import { jsx as i } from "react/jsx-runtime";
6
+ import { Menu as a } from "@base-ui/react/menu";
7
+ //#region src/components/menu.tsx
8
+ function o({ onOpenChange: e, ...t }) {
9
+ return /* @__PURE__ */ i(a.Root, {
10
+ ...t,
11
+ onOpenChange: e && ((t) => e(t))
12
+ });
13
+ }
14
+ var s = r.forwardRef(function(e, t) {
15
+ return /* @__PURE__ */ i(a.Trigger, {
16
+ ref: t,
17
+ ...e
18
+ });
19
+ }), c = r.forwardRef(function({ className: t, children: r }, o) {
20
+ return /* @__PURE__ */ i(a.Portal, { children: /* @__PURE__ */ i(a.Positioner, {
21
+ sideOffset: 4,
22
+ align: "start",
23
+ className: "sn:z-50 sn:outline-none",
24
+ children: /* @__PURE__ */ i(a.Popup, {
25
+ ref: o,
26
+ className: e(n, "sn:min-w-48", t),
27
+ children: r
28
+ })
29
+ }) });
30
+ }), l = r.forwardRef(function({ tone: n = "default", onClick: r, disabled: o, closeOnClick: s, className: c, children: l }, u) {
31
+ return /* @__PURE__ */ i(a.Item, {
32
+ ref: u,
33
+ onClick: r && (() => r()),
34
+ disabled: o,
35
+ closeOnClick: s,
36
+ className: e(t, n === "danger" ? "sn:text-danger" : "sn:text-default", c),
37
+ children: l
38
+ });
39
+ });
40
+ function u({ className: t }) {
41
+ return /* @__PURE__ */ i(a.Separator, { className: e("sn:my-1 sn:h-px sn:border-0 sn:bg-(--sn-color-border-default)", t) });
42
+ }
43
+ //#endregion
44
+ export { o as Menu, l as MenuItem, c as MenuPopup, u as MenuSeparator, s as MenuTrigger };
@@ -0,0 +1,26 @@
1
+ import * as React from 'react';
2
+ export interface PopoverProps {
3
+ open?: boolean;
4
+ defaultOpen?: boolean;
5
+ onOpenChange?: (open: boolean) => void;
6
+ children?: React.ReactNode;
7
+ }
8
+ export declare function Popover({ onOpenChange, ...props }: PopoverProps): React.JSX.Element;
9
+ export interface PopoverButtonProps extends Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, 'className'> {
10
+ className?: string;
11
+ render?: React.ReactElement;
12
+ }
13
+ export declare const PopoverTrigger: React.ForwardRefExoticComponent<PopoverButtonProps & React.RefAttributes<HTMLButtonElement>>;
14
+ export declare const PopoverClose: React.ForwardRefExoticComponent<PopoverButtonProps & React.RefAttributes<HTMLButtonElement>>;
15
+ export interface PopoverPopupProps {
16
+ side?: 'top' | 'bottom' | 'left' | 'right';
17
+ className?: string;
18
+ children?: React.ReactNode;
19
+ }
20
+ export declare const PopoverPopup: React.ForwardRefExoticComponent<PopoverPopupProps & React.RefAttributes<HTMLDivElement>>;
21
+ export declare const PopoverTitle: React.ForwardRefExoticComponent<Omit<React.HTMLAttributes<HTMLHeadingElement>, "className"> & {
22
+ className?: string;
23
+ } & React.RefAttributes<HTMLHeadingElement>>;
24
+ export declare const PopoverDescription: React.ForwardRefExoticComponent<Omit<React.HTMLAttributes<HTMLParagraphElement>, "className"> & {
25
+ className?: string;
26
+ } & React.RefAttributes<HTMLParagraphElement>>;