@nurimedia/react-ui 0.1.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 (90) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +274 -0
  3. package/dist/AlertDialog/index.cjs +185 -0
  4. package/dist/AlertDialog/index.cjs.map +1 -0
  5. package/dist/AlertDialog/index.d.cts +49 -0
  6. package/dist/AlertDialog/index.d.ts +49 -0
  7. package/dist/AlertDialog/index.js +148 -0
  8. package/dist/AlertDialog/index.js.map +1 -0
  9. package/dist/AlertDialog/styles.css +187 -0
  10. package/dist/Button/index.cjs +136 -0
  11. package/dist/Button/index.cjs.map +1 -0
  12. package/dist/Button/index.d.cts +65 -0
  13. package/dist/Button/index.d.ts +65 -0
  14. package/dist/Button/index.js +109 -0
  15. package/dist/Button/index.js.map +1 -0
  16. package/dist/Button/styles.css +181 -0
  17. package/dist/Checkbox/index.cjs +83 -0
  18. package/dist/Checkbox/index.cjs.map +1 -0
  19. package/dist/Checkbox/index.d.cts +40 -0
  20. package/dist/Checkbox/index.d.ts +40 -0
  21. package/dist/Checkbox/index.js +46 -0
  22. package/dist/Checkbox/index.js.map +1 -0
  23. package/dist/Checkbox/styles.css +78 -0
  24. package/dist/Input/index.cjs +103 -0
  25. package/dist/Input/index.cjs.map +1 -0
  26. package/dist/Input/index.d.cts +53 -0
  27. package/dist/Input/index.d.ts +53 -0
  28. package/dist/Input/index.js +76 -0
  29. package/dist/Input/index.js.map +1 -0
  30. package/dist/Input/styles.css +135 -0
  31. package/dist/Modal/index.cjs +120 -0
  32. package/dist/Modal/index.cjs.map +1 -0
  33. package/dist/Modal/index.d.cts +62 -0
  34. package/dist/Modal/index.d.ts +62 -0
  35. package/dist/Modal/index.js +83 -0
  36. package/dist/Modal/index.js.map +1 -0
  37. package/dist/Modal/styles.css +229 -0
  38. package/dist/Radio/index.cjs +86 -0
  39. package/dist/Radio/index.cjs.map +1 -0
  40. package/dist/Radio/index.d.cts +46 -0
  41. package/dist/Radio/index.d.ts +46 -0
  42. package/dist/Radio/index.js +48 -0
  43. package/dist/Radio/index.js.map +1 -0
  44. package/dist/Radio/styles.css +76 -0
  45. package/dist/Select/index.cjs +116 -0
  46. package/dist/Select/index.cjs.map +1 -0
  47. package/dist/Select/index.d.cts +61 -0
  48. package/dist/Select/index.d.ts +61 -0
  49. package/dist/Select/index.js +79 -0
  50. package/dist/Select/index.js.map +1 -0
  51. package/dist/Select/styles.css +157 -0
  52. package/dist/Switch/index.cjs +71 -0
  53. package/dist/Switch/index.cjs.map +1 -0
  54. package/dist/Switch/index.d.cts +34 -0
  55. package/dist/Switch/index.d.ts +34 -0
  56. package/dist/Switch/index.js +34 -0
  57. package/dist/Switch/index.js.map +1 -0
  58. package/dist/Switch/styles.css +71 -0
  59. package/dist/Textarea/index.cjs +127 -0
  60. package/dist/Textarea/index.cjs.map +1 -0
  61. package/dist/Textarea/index.d.cts +48 -0
  62. package/dist/Textarea/index.d.ts +48 -0
  63. package/dist/Textarea/index.js +100 -0
  64. package/dist/Textarea/index.js.map +1 -0
  65. package/dist/Textarea/styles.css +95 -0
  66. package/dist/Toast/index.cjs +184 -0
  67. package/dist/Toast/index.cjs.map +1 -0
  68. package/dist/Toast/index.d.cts +67 -0
  69. package/dist/Toast/index.d.ts +67 -0
  70. package/dist/Toast/index.js +146 -0
  71. package/dist/Toast/index.js.map +1 -0
  72. package/dist/Toast/styles.css +122 -0
  73. package/dist/Tooltip/index.cjs +105 -0
  74. package/dist/Tooltip/index.cjs.map +1 -0
  75. package/dist/Tooltip/index.d.cts +65 -0
  76. package/dist/Tooltip/index.d.ts +65 -0
  77. package/dist/Tooltip/index.js +67 -0
  78. package/dist/Tooltip/index.js.map +1 -0
  79. package/dist/Tooltip/styles.css +42 -0
  80. package/dist/index.cjs +900 -0
  81. package/dist/index.cjs.map +1 -0
  82. package/dist/index.d.cts +16 -0
  83. package/dist/index.d.ts +16 -0
  84. package/dist/index.js +850 -0
  85. package/dist/index.js.map +1 -0
  86. package/dist/styles.css +11 -0
  87. package/dist/tokens.css +232 -0
  88. package/dist/typography.css +104 -0
  89. package/llms.txt +369 -0
  90. package/package.json +183 -0
@@ -0,0 +1,148 @@
1
+ // src/AlertDialog/AlertDialog.tsx
2
+ import "./styles.css";
3
+ import * as Alert from "@radix-ui/react-alert-dialog";
4
+ import { forwardRef } from "react";
5
+
6
+ // src/utils/cx.ts
7
+ function cx(...classes) {
8
+ return classes.filter(Boolean).join(" ");
9
+ }
10
+
11
+ // src/AlertDialog/icons.tsx
12
+ import { jsx, jsxs } from "react/jsx-runtime";
13
+ var SuccessIcon = () => /* @__PURE__ */ jsxs("svg", { width: "24", height: "24", viewBox: "0 0 24 24", fill: "none", "aria-hidden": "true", children: [
14
+ /* @__PURE__ */ jsx("circle", { cx: "12", cy: "12", r: "12", fill: "var(--nuri-alert-tone-success-bg)" }),
15
+ /* @__PURE__ */ jsx(
16
+ "path",
17
+ {
18
+ d: "M15.77 9.58L10.65 14.70L8.90 12.95",
19
+ stroke: "var(--nuri-alert-tone-success-fg)",
20
+ strokeWidth: "2.5",
21
+ strokeLinecap: "round",
22
+ strokeLinejoin: "round"
23
+ }
24
+ )
25
+ ] });
26
+ var WarningIcon = () => /* @__PURE__ */ jsxs("svg", { width: "24", height: "24", viewBox: "0 0 24 24", fill: "none", "aria-hidden": "true", children: [
27
+ /* @__PURE__ */ jsx("circle", { cx: "12", cy: "12", r: "12", fill: "var(--nuri-alert-tone-warning-bg)" }),
28
+ /* @__PURE__ */ jsx(
29
+ "path",
30
+ {
31
+ d: "M12 6.75V12",
32
+ stroke: "var(--nuri-alert-tone-warning-fg)",
33
+ strokeWidth: "2.5",
34
+ strokeLinecap: "round"
35
+ }
36
+ ),
37
+ /* @__PURE__ */ jsx("circle", { cx: "12", cy: "16.31", r: "1.1", fill: "var(--nuri-alert-tone-warning-fg)" })
38
+ ] });
39
+ var InfoIcon = () => /* @__PURE__ */ jsxs("svg", { width: "24", height: "24", viewBox: "0 0 24 24", fill: "none", "aria-hidden": "true", children: [
40
+ /* @__PURE__ */ jsx("circle", { cx: "12", cy: "12", r: "12", fill: "var(--nuri-alert-tone-info-bg)" }),
41
+ /* @__PURE__ */ jsx(
42
+ "path",
43
+ {
44
+ d: "M12.25 7h0.01",
45
+ stroke: "var(--nuri-alert-tone-info-fg)",
46
+ strokeWidth: "3",
47
+ strokeLinecap: "round",
48
+ strokeLinejoin: "round"
49
+ }
50
+ ),
51
+ /* @__PURE__ */ jsx(
52
+ "path",
53
+ {
54
+ d: "M11 12h1.25v5h1.25",
55
+ stroke: "var(--nuri-alert-tone-info-fg)",
56
+ strokeWidth: "2.5",
57
+ strokeLinecap: "round",
58
+ strokeLinejoin: "round"
59
+ }
60
+ )
61
+ ] });
62
+ var defaultToneIcon = {
63
+ info: /* @__PURE__ */ jsx(InfoIcon, {}),
64
+ success: /* @__PURE__ */ jsx(SuccessIcon, {}),
65
+ warning: /* @__PURE__ */ jsx(WarningIcon, {})
66
+ };
67
+
68
+ // src/AlertDialog/AlertDialog.tsx
69
+ import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
70
+ var Root2 = (props) => /* @__PURE__ */ jsx2(Alert.Root, { ...props });
71
+ Root2.displayName = "AlertDialog";
72
+ var Trigger2 = Alert.Trigger;
73
+ var Content2 = forwardRef(
74
+ function AlertDialogContent({ tone = "info", icon, overlay = true, container, className, children, ...rest }, ref) {
75
+ const resolvedIcon = icon === void 0 ? defaultToneIcon[tone] : icon;
76
+ return /* @__PURE__ */ jsxs2(Alert.Portal, { container, children: [
77
+ overlay && /* @__PURE__ */ jsx2(Alert.Overlay, { className: "nuri-alert__overlay" }),
78
+ /* @__PURE__ */ jsxs2(
79
+ Alert.Content,
80
+ {
81
+ ref,
82
+ className: cx("nuri-alert", `nuri-alert--${tone}`, className),
83
+ ...rest,
84
+ children: [
85
+ resolvedIcon != null && /* @__PURE__ */ jsx2("div", { className: "nuri-alert__icon", "aria-hidden": "true", children: resolvedIcon }),
86
+ children
87
+ ]
88
+ }
89
+ )
90
+ ] });
91
+ }
92
+ );
93
+ var Header = forwardRef(
94
+ function AlertDialogHeader({ className, ...rest }, ref) {
95
+ return /* @__PURE__ */ jsx2("div", { ref, className: cx("nuri-alert__header", className), ...rest });
96
+ }
97
+ );
98
+ var Title2 = forwardRef(function AlertDialogTitle({ className, ...rest }, ref) {
99
+ return /* @__PURE__ */ jsx2(Alert.Title, { ref, className: cx("nuri-alert__title", className), ...rest });
100
+ });
101
+ var Description2 = forwardRef(function AlertDialogDescription({ className, ...rest }, ref) {
102
+ return /* @__PURE__ */ jsx2(Alert.Description, { ref, className: cx("nuri-alert__description", className), ...rest });
103
+ });
104
+ var Body = forwardRef(function AlertDialogBody({ className, ...rest }, ref) {
105
+ return /* @__PURE__ */ jsx2("div", { ref, className: cx("nuri-alert__body", className), ...rest });
106
+ });
107
+ var Footer = forwardRef(function AlertDialogFooter({ align = "full", className, ...rest }, ref) {
108
+ return /* @__PURE__ */ jsx2(
109
+ "div",
110
+ {
111
+ ref,
112
+ className: cx("nuri-alert__footer", `nuri-alert__footer--${align}`, className),
113
+ ...rest
114
+ }
115
+ );
116
+ });
117
+ var Action2 = Alert.Action;
118
+ var Cancel2 = Alert.Cancel;
119
+ var CloseIcon = () => /* @__PURE__ */ jsx2("svg", { width: "20", height: "20", viewBox: "0 0 20 20", fill: "none", "aria-hidden": "true", children: /* @__PURE__ */ jsx2("path", { d: "M5 5l10 10M15 5L5 15", stroke: "currentColor", strokeWidth: "1.5", strokeLinecap: "round" }) });
120
+ var CloseButton = forwardRef(function AlertDialogCloseButton({ className, "aria-label": ariaLabel = "\uB2EB\uAE30", children, ...rest }, ref) {
121
+ return /* @__PURE__ */ jsx2(Alert.Cancel, { asChild: true, children: /* @__PURE__ */ jsx2(
122
+ "button",
123
+ {
124
+ ref,
125
+ type: "button",
126
+ "aria-label": ariaLabel,
127
+ className: cx("nuri-alert__close", className),
128
+ ...rest,
129
+ children: children ?? /* @__PURE__ */ jsx2(CloseIcon, {})
130
+ }
131
+ ) });
132
+ });
133
+ var AlertDialog = Object.assign(Root2, {
134
+ Trigger: Trigger2,
135
+ Content: Content2,
136
+ Header,
137
+ Title: Title2,
138
+ Description: Description2,
139
+ Body,
140
+ Footer,
141
+ Action: Action2,
142
+ Cancel: Cancel2,
143
+ CloseButton
144
+ });
145
+ export {
146
+ AlertDialog
147
+ };
148
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/AlertDialog/AlertDialog.tsx","../../src/utils/cx.ts","../../src/AlertDialog/icons.tsx"],"sourcesContent":["'use client';\n\nimport './styles.css';\nimport * as Alert from '@radix-ui/react-alert-dialog';\nimport { forwardRef } from 'react';\nimport type { ComponentPropsWithoutRef, ElementRef, ReactNode } from 'react';\nimport { cx } from '../utils/cx.js';\nimport { defaultToneIcon } from './icons.js';\n\ntype Tone = 'info' | 'success' | 'warning';\n\n/**\n * 확인 강제형 다이얼로그 (alertdialog). Radix AlertDialog 기반.\n *\n * 일반 Modal과의 차이:\n * - `role=\"alertdialog\"` — 스크린리더가 더 강하게 안내\n * - **바깥(백드롭) 클릭으로 닫히지 않음** — 실수로 닫히는 것 방지\n * - 포커스가 기본적으로 Cancel(취소)에 위치 — 실수로 위험 액션 방지\n * - **중앙 정렬 + 상단 아이콘** (디자인 시스템 표준 레이아웃)\n *\n * Esc 키는 닫힘(키보드 탈출구 — 접근성). 막으려면 Content에 `onEscapeKeyDown` 전달.\n *\n * @example\n * <AlertDialog>\n * <AlertDialog.Trigger asChild><Button>삭제</Button></AlertDialog.Trigger>\n * <AlertDialog.Content tone=\"warning\">\n * <AlertDialog.Header>\n * <AlertDialog.Title>정말 삭제할까요?</AlertDialog.Title>\n * <AlertDialog.Description>되돌릴 수 없습니다.</AlertDialog.Description>\n * </AlertDialog.Header>\n * <AlertDialog.Footer>\n * <AlertDialog.Cancel asChild><Button variant=\"outline\">취소</Button></AlertDialog.Cancel>\n * <AlertDialog.Action asChild><Button onClick={onDelete}>삭제</Button></AlertDialog.Action>\n * </AlertDialog.Footer>\n * </AlertDialog.Content>\n * </AlertDialog>\n */\nconst Root = (props: ComponentPropsWithoutRef<typeof Alert.Root>) => <Alert.Root {...props} />;\nRoot.displayName = 'AlertDialog';\n\n/** 다이얼로그를 여는 트리거. `asChild`로 임의 엘리먼트에 위임. */\nconst Trigger = Alert.Trigger;\n\nexport interface AlertDialogContentProps extends ComponentPropsWithoutRef<typeof Alert.Content> {\n /**\n * 상태 톤 — 기본 아이콘과 그 색을 결정.\n * @default 'info'\n */\n tone?: Tone;\n /**\n * 상단 아이콘.\n * - 미지정: `tone`에 맞는 **기본 아이콘** 자동 렌더\n * - `ReactNode`: 커스텀 아이콘으로 교체\n * - `null`: 아이콘 제거\n */\n icon?: ReactNode | null;\n /** 백드롭(오버레이) 표시 여부. @default true */\n overlay?: boolean;\n /** 포털 렌더 대상. 기본은 document.body. 다른 요소에 렌더하려면 지정. */\n container?: HTMLElement | null;\n}\n\n/** 오버레이 + 포털 + 패널을 한 번에 렌더. 중앙 정렬 + 상단 아이콘 레이아웃. */\nconst Content = forwardRef<ElementRef<typeof Alert.Content>, AlertDialogContentProps>(\n function AlertDialogContent(\n { tone = 'info', icon, overlay = true, container, className, children, ...rest },\n ref,\n ) {\n // icon 미지정(undefined) → 기본 tone 아이콘 / null → 제거 / ReactNode → 교체\n const resolvedIcon = icon === undefined ? defaultToneIcon[tone] : icon;\n return (\n <Alert.Portal container={container}>\n {overlay && <Alert.Overlay className=\"nuri-alert__overlay\" />}\n <Alert.Content\n ref={ref}\n className={cx('nuri-alert', `nuri-alert--${tone}`, className)}\n {...rest}\n >\n {resolvedIcon != null && (\n <div className=\"nuri-alert__icon\" aria-hidden=\"true\">\n {resolvedIcon}\n </div>\n )}\n {children}\n </Alert.Content>\n </Alert.Portal>\n );\n },\n);\n\n/** 헤더 슬롯 (Title/Description 묶음, 중앙 정렬). */\nconst Header = forwardRef<HTMLDivElement, ComponentPropsWithoutRef<'div'>>(\n function AlertDialogHeader({ className, ...rest }, ref) {\n return <div ref={ref} className={cx('nuri-alert__header', className)} {...rest} />;\n },\n);\n\n/** 제목. 자동으로 `aria-labelledby`로 연결됨. */\nconst Title = forwardRef<\n ElementRef<typeof Alert.Title>,\n ComponentPropsWithoutRef<typeof Alert.Title>\n>(function AlertDialogTitle({ className, ...rest }, ref) {\n return <Alert.Title ref={ref} className={cx('nuri-alert__title', className)} {...rest} />;\n});\n\n/** 설명. 자동으로 `aria-describedby`로 연결됨. */\nconst Description = forwardRef<\n ElementRef<typeof Alert.Description>,\n ComponentPropsWithoutRef<typeof Alert.Description>\n>(function AlertDialogDescription({ className, ...rest }, ref) {\n return (\n <Alert.Description ref={ref} className={cx('nuri-alert__description', className)} {...rest} />\n );\n});\n\n/** 본문 슬롯 (추가 콘텐츠). */\nconst Body = forwardRef<HTMLDivElement, ComponentPropsWithoutRef<'div'>>(function AlertDialogBody(\n { className, ...rest },\n ref,\n) {\n return <div ref={ref} className={cx('nuri-alert__body', className)} {...rest} />;\n});\n\nexport interface AlertDialogFooterProps extends ComponentPropsWithoutRef<'div'> {\n /**\n * 버튼 정렬. 디자인 표준은 전체너비 버튼이라 기본값 `full`.\n * @default 'full'\n */\n align?: 'start' | 'center' | 'end' | 'between' | 'full';\n}\n\n/** 푸터 슬롯 (Cancel/Action 배치). 기본 전체너비(`full`). */\nconst Footer = forwardRef<HTMLDivElement, AlertDialogFooterProps>(function AlertDialogFooter(\n { align = 'full', className, ...rest },\n ref,\n) {\n return (\n <div\n ref={ref}\n className={cx('nuri-alert__footer', `nuri-alert__footer--${align}`, className)}\n {...rest}\n />\n );\n});\n\n/**\n * 확인(주 액션) 버튼. 클릭 시 닫힘. `asChild`로 Button 위임.\n * @example <AlertDialog.Action asChild><Button onClick={onConfirm}>삭제</Button></AlertDialog.Action>\n */\nconst Action = Alert.Action;\n\n/**\n * 취소 버튼. 클릭 시 닫힘. 포커스가 기본적으로 여기 위치.\n * @example <AlertDialog.Cancel asChild><Button variant=\"outline\">취소</Button></AlertDialog.Cancel>\n */\nconst Cancel = Alert.Cancel;\n\nconst CloseIcon = () => (\n <svg width=\"20\" height=\"20\" viewBox=\"0 0 20 20\" fill=\"none\" aria-hidden=\"true\">\n <path d=\"M5 5l10 10M15 5L5 15\" stroke=\"currentColor\" strokeWidth=\"1.5\" strokeLinecap=\"round\" />\n </svg>\n);\n\n/**\n * 우상단 X 닫기 버튼 (옵션). 내부적으로 Cancel과 동일하게 닫힘. 기본 `aria-label=\"닫기\"`.\n * 확인을 강제하려면 사용하지 않는 편이 좋습니다 (Cancel/Action으로 명시적 선택 유도).\n */\nconst CloseButton = forwardRef<\n HTMLButtonElement,\n Omit<ComponentPropsWithoutRef<'button'>, 'children'> & { children?: ReactNode }\n>(function AlertDialogCloseButton(\n { className, 'aria-label': ariaLabel = '닫기', children, ...rest },\n ref,\n) {\n return (\n <Alert.Cancel asChild>\n <button\n ref={ref}\n type=\"button\"\n aria-label={ariaLabel}\n className={cx('nuri-alert__close', className)}\n {...rest}\n >\n {children ?? <CloseIcon />}\n </button>\n </Alert.Cancel>\n );\n});\n\nexport const AlertDialog = Object.assign(Root, {\n Trigger,\n Content,\n Header,\n Title,\n Description,\n Body,\n Footer,\n Action,\n Cancel,\n CloseButton,\n});\n","/**\n * 조건부 className 합성 유틸. falsy 값(undefined/false/null/'')은 제거.\n * @example cx('btn', isActive && 'btn--active', className)\n */\nexport function cx(...classes: Array<string | false | null | undefined>): string {\n return classes.filter(Boolean).join(' ');\n}\n","/**\n * tone별 기본 아이콘 — 디자이너 시스템(Figma) 에셋을 그대로 inline SVG로 재현.\n * 구조: 틴트 원 배경 + tone 색 심볼. 색은 토큰(`--nuri-alert-tone-*`)으로 분리.\n * AlertDialog.Content의 `icon` 미지정 시 tone에 맞춰 자동 렌더.\n * `icon={<Custom/>}`로 교체, `icon={null}`로 제거 가능.\n */\nimport type { ReactNode } from 'react';\n\nconst SuccessIcon = () => (\n <svg width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" aria-hidden=\"true\">\n <circle cx=\"12\" cy=\"12\" r=\"12\" fill=\"var(--nuri-alert-tone-success-bg)\" />\n <path\n d=\"M15.77 9.58L10.65 14.70L8.90 12.95\"\n stroke=\"var(--nuri-alert-tone-success-fg)\"\n strokeWidth=\"2.5\"\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n />\n </svg>\n);\n\nconst WarningIcon = () => (\n <svg width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" aria-hidden=\"true\">\n <circle cx=\"12\" cy=\"12\" r=\"12\" fill=\"var(--nuri-alert-tone-warning-bg)\" />\n <path\n d=\"M12 6.75V12\"\n stroke=\"var(--nuri-alert-tone-warning-fg)\"\n strokeWidth=\"2.5\"\n strokeLinecap=\"round\"\n />\n <circle cx=\"12\" cy=\"16.31\" r=\"1.1\" fill=\"var(--nuri-alert-tone-warning-fg)\" />\n </svg>\n);\n\nconst InfoIcon = () => (\n <svg width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" aria-hidden=\"true\">\n <circle cx=\"12\" cy=\"12\" r=\"12\" fill=\"var(--nuri-alert-tone-info-bg)\" />\n <path\n d=\"M12.25 7h0.01\"\n stroke=\"var(--nuri-alert-tone-info-fg)\"\n strokeWidth=\"3\"\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n />\n <path\n d=\"M11 12h1.25v5h1.25\"\n stroke=\"var(--nuri-alert-tone-info-fg)\"\n strokeWidth=\"2.5\"\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n />\n </svg>\n);\n\nexport const defaultToneIcon: Record<'info' | 'success' | 'warning', ReactNode> = {\n info: <InfoIcon />,\n success: <SuccessIcon />,\n warning: <WarningIcon />,\n};\n"],"mappings":";AAEA,OAAO;AACP,YAAY,WAAW;AACvB,SAAS,kBAAkB;;;ACApB,SAAS,MAAM,SAA2D;AAC/E,SAAO,QAAQ,OAAO,OAAO,EAAE,KAAK,GAAG;AACzC;;;ACGE,SACE,KADF;AADF,IAAM,cAAc,MAClB,qBAAC,SAAI,OAAM,MAAK,QAAO,MAAK,SAAQ,aAAY,MAAK,QAAO,eAAY,QACtE;AAAA,sBAAC,YAAO,IAAG,MAAK,IAAG,MAAK,GAAE,MAAK,MAAK,qCAAoC;AAAA,EACxE;AAAA,IAAC;AAAA;AAAA,MACC,GAAE;AAAA,MACF,QAAO;AAAA,MACP,aAAY;AAAA,MACZ,eAAc;AAAA,MACd,gBAAe;AAAA;AAAA,EACjB;AAAA,GACF;AAGF,IAAM,cAAc,MAClB,qBAAC,SAAI,OAAM,MAAK,QAAO,MAAK,SAAQ,aAAY,MAAK,QAAO,eAAY,QACtE;AAAA,sBAAC,YAAO,IAAG,MAAK,IAAG,MAAK,GAAE,MAAK,MAAK,qCAAoC;AAAA,EACxE;AAAA,IAAC;AAAA;AAAA,MACC,GAAE;AAAA,MACF,QAAO;AAAA,MACP,aAAY;AAAA,MACZ,eAAc;AAAA;AAAA,EAChB;AAAA,EACA,oBAAC,YAAO,IAAG,MAAK,IAAG,SAAQ,GAAE,OAAM,MAAK,qCAAoC;AAAA,GAC9E;AAGF,IAAM,WAAW,MACf,qBAAC,SAAI,OAAM,MAAK,QAAO,MAAK,SAAQ,aAAY,MAAK,QAAO,eAAY,QACtE;AAAA,sBAAC,YAAO,IAAG,MAAK,IAAG,MAAK,GAAE,MAAK,MAAK,kCAAiC;AAAA,EACrE;AAAA,IAAC;AAAA;AAAA,MACC,GAAE;AAAA,MACF,QAAO;AAAA,MACP,aAAY;AAAA,MACZ,eAAc;AAAA,MACd,gBAAe;AAAA;AAAA,EACjB;AAAA,EACA;AAAA,IAAC;AAAA;AAAA,MACC,GAAE;AAAA,MACF,QAAO;AAAA,MACP,aAAY;AAAA,MACZ,eAAc;AAAA,MACd,gBAAe;AAAA;AAAA,EACjB;AAAA,GACF;AAGK,IAAM,kBAAqE;AAAA,EAChF,MAAM,oBAAC,YAAS;AAAA,EAChB,SAAS,oBAAC,eAAY;AAAA,EACtB,SAAS,oBAAC,eAAY;AACxB;;;AFrBqE,gBAAAA,MAoC7D,QAAAC,aApC6D;AAArE,IAAMC,QAAO,CAAC,UAAuD,gBAAAF,KAAO,YAAN,EAAY,GAAG,OAAO;AAC5FE,MAAK,cAAc;AAGnB,IAAMC,WAAgB;AAsBtB,IAAMC,WAAU;AAAA,EACd,SAAS,mBACP,EAAE,OAAO,QAAQ,MAAM,UAAU,MAAM,WAAW,WAAW,UAAU,GAAG,KAAK,GAC/E,KACA;AAEA,UAAM,eAAe,SAAS,SAAY,gBAAgB,IAAI,IAAI;AAClE,WACE,gBAAAH,MAAO,cAAN,EAAa,WACX;AAAA,iBAAW,gBAAAD,KAAO,eAAN,EAAc,WAAU,uBAAsB;AAAA,MAC3D,gBAAAC;AAAA,QAAO;AAAA,QAAN;AAAA,UACC;AAAA,UACA,WAAW,GAAG,cAAc,eAAe,IAAI,IAAI,SAAS;AAAA,UAC3D,GAAG;AAAA,UAEH;AAAA,4BAAgB,QACf,gBAAAD,KAAC,SAAI,WAAU,oBAAmB,eAAY,QAC3C,wBACH;AAAA,YAED;AAAA;AAAA;AAAA,MACH;AAAA,OACF;AAAA,EAEJ;AACF;AAGA,IAAM,SAAS;AAAA,EACb,SAAS,kBAAkB,EAAE,WAAW,GAAG,KAAK,GAAG,KAAK;AACtD,WAAO,gBAAAA,KAAC,SAAI,KAAU,WAAW,GAAG,sBAAsB,SAAS,GAAI,GAAG,MAAM;AAAA,EAClF;AACF;AAGA,IAAMK,SAAQ,WAGZ,SAAS,iBAAiB,EAAE,WAAW,GAAG,KAAK,GAAG,KAAK;AACvD,SAAO,gBAAAL,KAAO,aAAN,EAAY,KAAU,WAAW,GAAG,qBAAqB,SAAS,GAAI,GAAG,MAAM;AACzF,CAAC;AAGD,IAAMM,eAAc,WAGlB,SAAS,uBAAuB,EAAE,WAAW,GAAG,KAAK,GAAG,KAAK;AAC7D,SACE,gBAAAN,KAAO,mBAAN,EAAkB,KAAU,WAAW,GAAG,2BAA2B,SAAS,GAAI,GAAG,MAAM;AAEhG,CAAC;AAGD,IAAM,OAAO,WAA4D,SAAS,gBAChF,EAAE,WAAW,GAAG,KAAK,GACrB,KACA;AACA,SAAO,gBAAAA,KAAC,SAAI,KAAU,WAAW,GAAG,oBAAoB,SAAS,GAAI,GAAG,MAAM;AAChF,CAAC;AAWD,IAAM,SAAS,WAAmD,SAAS,kBACzE,EAAE,QAAQ,QAAQ,WAAW,GAAG,KAAK,GACrC,KACA;AACA,SACE,gBAAAA;AAAA,IAAC;AAAA;AAAA,MACC;AAAA,MACA,WAAW,GAAG,sBAAsB,uBAAuB,KAAK,IAAI,SAAS;AAAA,MAC5E,GAAG;AAAA;AAAA,EACN;AAEJ,CAAC;AAMD,IAAMO,UAAe;AAMrB,IAAMC,UAAe;AAErB,IAAM,YAAY,MAChB,gBAAAR,KAAC,SAAI,OAAM,MAAK,QAAO,MAAK,SAAQ,aAAY,MAAK,QAAO,eAAY,QACtE,0BAAAA,KAAC,UAAK,GAAE,wBAAuB,QAAO,gBAAe,aAAY,OAAM,eAAc,SAAQ,GAC/F;AAOF,IAAM,cAAc,WAGlB,SAAS,uBACT,EAAE,WAAW,cAAc,YAAY,gBAAM,UAAU,GAAG,KAAK,GAC/D,KACA;AACA,SACE,gBAAAA,KAAO,cAAN,EAAa,SAAO,MACnB,0BAAAA;AAAA,IAAC;AAAA;AAAA,MACC;AAAA,MACA,MAAK;AAAA,MACL,cAAY;AAAA,MACZ,WAAW,GAAG,qBAAqB,SAAS;AAAA,MAC3C,GAAG;AAAA,MAEH,sBAAY,gBAAAA,KAAC,aAAU;AAAA;AAAA,EAC1B,GACF;AAEJ,CAAC;AAEM,IAAM,cAAc,OAAO,OAAOE,OAAM;AAAA,EAC7C,SAAAC;AAAA,EACA,SAAAC;AAAA,EACA;AAAA,EACA,OAAAC;AAAA,EACA,aAAAC;AAAA,EACA;AAAA,EACA;AAAA,EACA,QAAAC;AAAA,EACA,QAAAC;AAAA,EACA;AACF,CAAC;","names":["jsx","jsxs","Root","Trigger","Content","Title","Description","Action","Cancel"]}
@@ -0,0 +1,187 @@
1
+ .nuri-alert__overlay {
2
+ position: fixed;
3
+ inset: 0;
4
+ background-color: var(--nuri-modal-overlay);
5
+ z-index: var(--nuri-modal-z-index);
6
+ animation: nuri-alert-overlay-in 200ms ease;
7
+ }
8
+ .nuri-alert__overlay[data-state='closed'] {
9
+ animation: nuri-alert-overlay-out 150ms ease;
10
+ }
11
+
12
+ .nuri-alert,
13
+ .nuri-alert__header,
14
+ .nuri-alert__body,
15
+ .nuri-alert__footer {
16
+ box-sizing: border-box;
17
+ }
18
+
19
+ .nuri-alert {
20
+ position: fixed;
21
+ top: 50%;
22
+ left: 50%;
23
+ transform: translate(-50%, -50%);
24
+ z-index: calc(var(--nuri-modal-z-index) + 1);
25
+ display: flex;
26
+ flex-direction: column;
27
+ align-items: center; /* 중앙 정렬 (디자인 표준) */
28
+ width: calc(100vw - 32px);
29
+ max-width: var(--nuri-alert-width);
30
+ max-height: calc(100dvh - 48px);
31
+ padding: var(--nuri-modal-padding);
32
+ border-radius: var(--nuri-modal-radius);
33
+ background-color: var(--nuri-modal-surface);
34
+ color: var(--nuri-modal-fg);
35
+ box-shadow: var(--nuri-modal-shadow);
36
+ font-family: inherit;
37
+ text-align: center;
38
+ outline: none;
39
+ animation: nuri-alert-in 200ms ease;
40
+ }
41
+ .nuri-alert[data-state='closed'] {
42
+ animation: nuri-alert-out 150ms ease;
43
+ }
44
+
45
+ /* ─── 상단 아이콘 (틴트 원 + 심볼은 SVG에 토큰색으로 내장) ─── */
46
+ .nuri-alert__icon {
47
+ display: inline-flex;
48
+ align-items: center;
49
+ justify-content: center;
50
+ width: var(--nuri-alert-icon-size);
51
+ height: var(--nuri-alert-icon-size);
52
+ flex-shrink: 0;
53
+ margin-bottom: var(--nuri-alert-icon-title-gap); /* 아이콘 → 제목 (4px) */
54
+ }
55
+ .nuri-alert__icon > svg {
56
+ width: 100%;
57
+ height: 100%;
58
+ }
59
+
60
+ /* ─── 슬롯 (중앙) ─── */
61
+ /* 간격: 아이콘→제목 4px(위 icon margin), 제목→설명 6px(header gap),
62
+ 텍스트→버튼 16px(body/footer margin-top) */
63
+ .nuri-alert__header {
64
+ display: flex;
65
+ flex-direction: column;
66
+ gap: var(--nuri-alert-title-desc-gap);
67
+ width: 100%;
68
+ }
69
+ .nuri-alert__title {
70
+ margin: 0;
71
+ font-size: var(--nuri-alert-title-size);
72
+ font-weight: var(--nuri-alert-title-weight);
73
+ color: var(--nuri-alert-title-color);
74
+ line-height: 1.6;
75
+ }
76
+ .nuri-alert__description {
77
+ margin: 0;
78
+ font-size: var(--nuri-alert-desc-size);
79
+ font-weight: var(--nuri-alert-desc-weight);
80
+ color: var(--nuri-alert-desc-color);
81
+ line-height: 1.6;
82
+ }
83
+ .nuri-alert__body {
84
+ width: 100%;
85
+ margin-top: var(--nuri-alert-section-gap);
86
+ font-size: var(--nuri-modal-body-size);
87
+ line-height: 1.6;
88
+ }
89
+
90
+ /* ─── 푸터 (전체너비 기본) ─── */
91
+ .nuri-alert__footer {
92
+ display: flex;
93
+ gap: var(--nuri-alert-button-gap);
94
+ width: 100%;
95
+ margin-top: var(--nuri-alert-section-gap); /* 텍스트 그룹 → 버튼 (16px) */
96
+ flex-shrink: 0;
97
+ }
98
+ .nuri-alert__footer--start {
99
+ justify-content: flex-start;
100
+ }
101
+ .nuri-alert__footer--center {
102
+ justify-content: center;
103
+ }
104
+ .nuri-alert__footer--end {
105
+ justify-content: flex-end;
106
+ }
107
+ .nuri-alert__footer--between {
108
+ justify-content: space-between;
109
+ }
110
+ .nuri-alert__footer--full > * {
111
+ flex: 1 1 0;
112
+ }
113
+
114
+ /* ─── 우상단 닫기 (옵션) ─── */
115
+ .nuri-alert__close {
116
+ position: absolute;
117
+ top: 18px;
118
+ right: 18px;
119
+ display: inline-flex;
120
+ align-items: center;
121
+ justify-content: center;
122
+ width: 28px;
123
+ height: 28px;
124
+ padding: 0;
125
+ border: none;
126
+ border-radius: 6px;
127
+ background: transparent;
128
+ color: var(--nuri-modal-close-color);
129
+ cursor: pointer;
130
+ transition:
131
+ background-color 120ms ease,
132
+ color 120ms ease;
133
+ }
134
+ .nuri-alert__close:hover {
135
+ background-color: var(--nuri-modal-close-bg-hover);
136
+ color: var(--nuri-modal-close-color-hover);
137
+ }
138
+ .nuri-alert__close:focus-visible {
139
+ outline: var(--nuri-button-focus-ring-width) solid
140
+ var(--nuri-button-focus-ring-color, var(--nuri-color-brand-default));
141
+ outline-offset: 2px;
142
+ }
143
+
144
+ /* ─── 애니메이션 ─── */
145
+ @keyframes nuri-alert-overlay-in {
146
+ from {
147
+ opacity: 0;
148
+ }
149
+ to {
150
+ opacity: 1;
151
+ }
152
+ }
153
+ @keyframes nuri-alert-overlay-out {
154
+ from {
155
+ opacity: 1;
156
+ }
157
+ to {
158
+ opacity: 0;
159
+ }
160
+ }
161
+ @keyframes nuri-alert-in {
162
+ from {
163
+ opacity: 0;
164
+ transform: translate(-50%, -50%) scale(0.96);
165
+ }
166
+ to {
167
+ opacity: 1;
168
+ transform: translate(-50%, -50%) scale(1);
169
+ }
170
+ }
171
+ @keyframes nuri-alert-out {
172
+ from {
173
+ opacity: 1;
174
+ transform: translate(-50%, -50%) scale(1);
175
+ }
176
+ to {
177
+ opacity: 0;
178
+ transform: translate(-50%, -50%) scale(0.96);
179
+ }
180
+ }
181
+
182
+ @media (prefers-reduced-motion: reduce) {
183
+ .nuri-alert__overlay,
184
+ .nuri-alert {
185
+ animation-duration: 1ms;
186
+ }
187
+ }
@@ -0,0 +1,136 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/Button/index.ts
21
+ var Button_exports = {};
22
+ __export(Button_exports, {
23
+ Button: () => Button
24
+ });
25
+ module.exports = __toCommonJS(Button_exports);
26
+
27
+ // src/Button/Button.tsx
28
+ var import_styles = require("./styles.css");
29
+ var import_react_slot = require("@radix-ui/react-slot");
30
+ var import_react = require("react");
31
+
32
+ // src/Button/Spinner.tsx
33
+ var import_jsx_runtime = require("react/jsx-runtime");
34
+ var Spinner = () => /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "nuri-button__icon", "aria-hidden": "true", children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("svg", { className: "nuri-button__spinner", viewBox: "0 0 16 16", width: "16", height: "16", fill: "none", children: [
35
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("circle", { cx: "8", cy: "8", r: "6", stroke: "currentColor", strokeOpacity: "0.25", strokeWidth: "2" }),
36
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("path", { d: "M14 8a6 6 0 0 0-6-6", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round" })
37
+ ] }) });
38
+
39
+ // src/utils/cx.ts
40
+ function cx(...classes) {
41
+ return classes.filter(Boolean).join(" ");
42
+ }
43
+
44
+ // src/Button/Button.tsx
45
+ var import_jsx_runtime2 = require("react/jsx-runtime");
46
+ var Button = (0, import_react.forwardRef)(function Button2({
47
+ variant = "primary",
48
+ size = "md",
49
+ iconLeft,
50
+ iconRight,
51
+ iconOnly = false,
52
+ block = false,
53
+ loading = false,
54
+ disabled = false,
55
+ asChild = false,
56
+ type = "button",
57
+ className,
58
+ children,
59
+ onClick,
60
+ ...rest
61
+ }, ref) {
62
+ const isBlocked = disabled || loading;
63
+ if (process.env.NODE_ENV !== "production" && iconOnly && !rest["aria-label"] && !rest["aria-labelledby"]) {
64
+ console.warn(
65
+ "[nuri/Button] `iconOnly` \uBC84\uD2BC\uC740 `aria-label`(\uB610\uB294 `aria-labelledby`)\uC774 \uD544\uC694\uD569\uB2C8\uB2E4."
66
+ );
67
+ }
68
+ const hasLeftSlot = !iconOnly && (loading || iconLeft != null);
69
+ const hasRightSlot = !iconOnly && !loading && iconRight != null;
70
+ const classes = cx(
71
+ "nuri-button",
72
+ `nuri-button--${variant}`,
73
+ `nuri-button--${size}`,
74
+ block && "nuri-button--block",
75
+ iconOnly && "nuri-button--icon-only",
76
+ isBlocked && "nuri-button--disabled",
77
+ loading && "nuri-button--loading",
78
+ hasLeftSlot && "nuri-button--has-left",
79
+ hasRightSlot && "nuri-button--has-right",
80
+ className
81
+ );
82
+ const handleClick = (e) => {
83
+ if (isBlocked) {
84
+ e.preventDefault();
85
+ e.stopPropagation();
86
+ return;
87
+ }
88
+ onClick?.(e);
89
+ };
90
+ const renderContent = (content) => {
91
+ if (iconOnly) {
92
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { className: "nuri-button__icon", "aria-hidden": "true", children: loading ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(Spinner, {}) : content });
93
+ }
94
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_jsx_runtime2.Fragment, { children: [
95
+ loading ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(Spinner, {}) : iconLeft != null && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { className: "nuri-button__icon nuri-button__icon--left", "aria-hidden": "true", children: iconLeft }),
96
+ content,
97
+ hasRightSlot && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { className: "nuri-button__icon nuri-button__icon--right", "aria-hidden": "true", children: iconRight })
98
+ ] });
99
+ };
100
+ if (asChild) {
101
+ const child = import_react.Children.only(children);
102
+ if (!(0, import_react.isValidElement)(child)) return null;
103
+ const childElement = child;
104
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
105
+ import_react_slot.Slot,
106
+ {
107
+ ref,
108
+ className: classes,
109
+ "aria-disabled": isBlocked || void 0,
110
+ "aria-busy": loading || void 0,
111
+ onClick: handleClick,
112
+ ...rest,
113
+ children: (0, import_react.cloneElement)(childElement, void 0, renderContent(childElement.props.children))
114
+ }
115
+ );
116
+ }
117
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
118
+ "button",
119
+ {
120
+ ref,
121
+ type,
122
+ className: classes,
123
+ disabled,
124
+ "aria-disabled": loading || void 0,
125
+ "aria-busy": loading || void 0,
126
+ onClick: handleClick,
127
+ ...rest,
128
+ children: renderContent(children)
129
+ }
130
+ );
131
+ });
132
+ // Annotate the CommonJS export names for ESM import in node:
133
+ 0 && (module.exports = {
134
+ Button
135
+ });
136
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/Button/index.ts","../../src/Button/Button.tsx","../../src/Button/Spinner.tsx","../../src/utils/cx.ts"],"sourcesContent":["export { Button } from './Button.js';\nexport type { ButtonProps } from './Button.js';\n","'use client';\n\nimport './styles.css';\nimport { Slot } from '@radix-ui/react-slot';\nimport { Children, cloneElement, forwardRef, isValidElement } from 'react';\nimport type { ButtonHTMLAttributes, MouseEvent, ReactElement, ReactNode } from 'react';\nimport { Spinner } from './Spinner.js';\nimport { cx } from '../utils/cx.js';\n\ntype Variant = 'primary' | 'secondary' | 'outline' | 'ghost';\ntype Size = 'sm' | 'md' | 'lg';\n\nexport interface ButtonProps extends Omit<ButtonHTMLAttributes<HTMLButtonElement>, 'type'> {\n /**\n * 시각 유형.\n * - `primary`: 브랜드 색 배경 (기본)\n * - `secondary`: 브랜드 옅은 톤 배경\n * - `outline`: 투명 배경 + 테두리\n * - `ghost`: 투명 배경, hover 시에만 배경 채움\n * @default 'primary'\n */\n variant?: Variant;\n\n /**\n * 사이즈.\n * - `sm`: 36px 높이 — 밀집된 UI, 테이블 액션\n * - `md`: 42px 높이 — 일반 콘텐츠 영역 (기본)\n * - `lg`: 48px 높이 — 주요 CTA, 모바일 터치\n * @default 'md'\n */\n size?: Size;\n\n /**\n * 라벨 좌측에 표시할 아이콘.\n * @example <Button iconLeft={<SearchIcon />}>검색</Button>\n */\n iconLeft?: ReactNode;\n\n /**\n * 라벨 우측에 표시할 아이콘.\n */\n iconRight?: ReactNode;\n\n /**\n * 아이콘만 표시하는 정사각 버튼. **`aria-label` 필수**\n * (icon-only는 시각적으로만 의미가 있으므로 스크린리더용 라벨이 없으면 사용자가 용도를 알 수 없음).\n * children은 단일 아이콘 엘리먼트.\n * @example <Button iconOnly aria-label=\"닫기\"><CloseIcon /></Button>\n */\n iconOnly?: boolean;\n\n /**\n * 컨테이너 전체 너비로 확장.\n */\n block?: boolean;\n\n /**\n * 로딩 중 표시. 스피너를 좌측 아이콘 자리에 표시하고, 클릭을 차단하며 `aria-busy=\"true\"`를 부여.\n */\n loading?: boolean;\n\n /**\n * Radix Slot으로 자식 엘리먼트를 렌더 — 자식이 Button의 className·ref·이벤트 핸들러를 모두 물려받음.\n * 링크를 버튼처럼 보이게 할 때 가장 자주 사용 (next/link, react-router Link 등).\n * @example\n * <Button asChild><Link href=\"/about\">소개</Link></Button>\n */\n asChild?: boolean;\n\n /**\n * 폼 버튼 타입. 의도치 않은 form submit 방지를 위해 기본값 `'button'`.\n * 폼 제출 버튼이면 명시적으로 `type=\"submit\"` 지정.\n * @default 'button'\n */\n type?: 'button' | 'submit' | 'reset';\n\n children?: ReactNode;\n}\n\nexport const Button = forwardRef<HTMLButtonElement, ButtonProps>(function Button(\n {\n variant = 'primary',\n size = 'md',\n iconLeft,\n iconRight,\n iconOnly = false,\n block = false,\n loading = false,\n disabled = false,\n asChild = false,\n type = 'button',\n className,\n children,\n onClick,\n ...rest\n },\n ref,\n) {\n const isBlocked = disabled || loading;\n\n if (\n process.env.NODE_ENV !== 'production' &&\n iconOnly &&\n !rest['aria-label'] &&\n !rest['aria-labelledby']\n ) {\n // iconOnly 버튼은 텍스트가 없어 스크린리더가 용도를 알 수 없음 → 개발 모드 경고\n console.warn(\n '[nuri/Button] `iconOnly` 버튼은 `aria-label`(또는 `aria-labelledby`)이 필요합니다.',\n );\n }\n\n const hasLeftSlot = !iconOnly && (loading || iconLeft != null);\n const hasRightSlot = !iconOnly && !loading && iconRight != null;\n\n const classes = cx(\n 'nuri-button',\n `nuri-button--${variant}`,\n `nuri-button--${size}`,\n block && 'nuri-button--block',\n iconOnly && 'nuri-button--icon-only',\n isBlocked && 'nuri-button--disabled',\n loading && 'nuri-button--loading',\n hasLeftSlot && 'nuri-button--has-left',\n hasRightSlot && 'nuri-button--has-right',\n className,\n );\n\n const handleClick = (e: MouseEvent<HTMLButtonElement>) => {\n if (isBlocked) {\n e.preventDefault();\n e.stopPropagation();\n return;\n }\n onClick?.(e);\n };\n\n const renderContent = (content: ReactNode) => {\n if (iconOnly) {\n return (\n <span className=\"nuri-button__icon\" aria-hidden=\"true\">\n {loading ? <Spinner /> : content}\n </span>\n );\n }\n return (\n <>\n {loading ? (\n <Spinner />\n ) : (\n iconLeft != null && (\n <span className=\"nuri-button__icon nuri-button__icon--left\" aria-hidden=\"true\">\n {iconLeft}\n </span>\n )\n )}\n {content}\n {hasRightSlot && (\n <span className=\"nuri-button__icon nuri-button__icon--right\" aria-hidden=\"true\">\n {iconRight}\n </span>\n )}\n </>\n );\n };\n\n if (asChild) {\n const child = Children.only(children);\n if (!isValidElement(child)) return null;\n const childElement = child as ReactElement<{ children?: ReactNode }>;\n return (\n <Slot\n ref={ref}\n className={classes}\n aria-disabled={isBlocked || undefined}\n aria-busy={loading || undefined}\n onClick={handleClick}\n {...rest}\n >\n {cloneElement(childElement, undefined, renderContent(childElement.props.children))}\n </Slot>\n );\n }\n\n return (\n <button\n ref={ref}\n type={type}\n className={classes}\n // disabled = 진짜 비활성 → native disabled (포커스 불가, 의도된 동작)\n // loading = 일시적 처리중 → aria-disabled로 포커스 유지 (클릭은 handleClick이 가드)\n disabled={disabled}\n aria-disabled={loading || undefined}\n aria-busy={loading || undefined}\n onClick={handleClick}\n {...rest}\n >\n {renderContent(children)}\n </button>\n );\n});\n","export const Spinner = () => (\n <span className=\"nuri-button__icon\" aria-hidden=\"true\">\n <svg className=\"nuri-button__spinner\" viewBox=\"0 0 16 16\" width=\"16\" height=\"16\" fill=\"none\">\n <circle cx=\"8\" cy=\"8\" r=\"6\" stroke=\"currentColor\" strokeOpacity=\"0.25\" strokeWidth=\"2\" />\n <path d=\"M14 8a6 6 0 0 0-6-6\" stroke=\"currentColor\" strokeWidth=\"2\" strokeLinecap=\"round\" />\n </svg>\n </span>\n);\n","/**\n * 조건부 className 합성 유틸. falsy 값(undefined/false/null/'')은 제거.\n * @example cx('btn', isActive && 'btn--active', className)\n */\nexport function cx(...classes: Array<string | false | null | undefined>): string {\n return classes.filter(Boolean).join(' ');\n}\n"],"mappings":"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","names":["import_jsx_runtime","Button"]}
@@ -0,0 +1,65 @@
1
+ import * as react from 'react';
2
+ import { ButtonHTMLAttributes, ReactNode } from 'react';
3
+
4
+ type Variant = 'primary' | 'secondary' | 'outline' | 'ghost';
5
+ type Size = 'sm' | 'md' | 'lg';
6
+ interface ButtonProps extends Omit<ButtonHTMLAttributes<HTMLButtonElement>, 'type'> {
7
+ /**
8
+ * 시각 유형.
9
+ * - `primary`: 브랜드 색 배경 (기본)
10
+ * - `secondary`: 브랜드 옅은 톤 배경
11
+ * - `outline`: 투명 배경 + 테두리
12
+ * - `ghost`: 투명 배경, hover 시에만 배경 채움
13
+ * @default 'primary'
14
+ */
15
+ variant?: Variant;
16
+ /**
17
+ * 사이즈.
18
+ * - `sm`: 36px 높이 — 밀집된 UI, 테이블 액션
19
+ * - `md`: 42px 높이 — 일반 콘텐츠 영역 (기본)
20
+ * - `lg`: 48px 높이 — 주요 CTA, 모바일 터치
21
+ * @default 'md'
22
+ */
23
+ size?: Size;
24
+ /**
25
+ * 라벨 좌측에 표시할 아이콘.
26
+ * @example <Button iconLeft={<SearchIcon />}>검색</Button>
27
+ */
28
+ iconLeft?: ReactNode;
29
+ /**
30
+ * 라벨 우측에 표시할 아이콘.
31
+ */
32
+ iconRight?: ReactNode;
33
+ /**
34
+ * 아이콘만 표시하는 정사각 버튼. **`aria-label` 필수**
35
+ * (icon-only는 시각적으로만 의미가 있으므로 스크린리더용 라벨이 없으면 사용자가 용도를 알 수 없음).
36
+ * children은 단일 아이콘 엘리먼트.
37
+ * @example <Button iconOnly aria-label="닫기"><CloseIcon /></Button>
38
+ */
39
+ iconOnly?: boolean;
40
+ /**
41
+ * 컨테이너 전체 너비로 확장.
42
+ */
43
+ block?: boolean;
44
+ /**
45
+ * 로딩 중 표시. 스피너를 좌측 아이콘 자리에 표시하고, 클릭을 차단하며 `aria-busy="true"`를 부여.
46
+ */
47
+ loading?: boolean;
48
+ /**
49
+ * Radix Slot으로 자식 엘리먼트를 렌더 — 자식이 Button의 className·ref·이벤트 핸들러를 모두 물려받음.
50
+ * 링크를 버튼처럼 보이게 할 때 가장 자주 사용 (next/link, react-router Link 등).
51
+ * @example
52
+ * <Button asChild><Link href="/about">소개</Link></Button>
53
+ */
54
+ asChild?: boolean;
55
+ /**
56
+ * 폼 버튼 타입. 의도치 않은 form submit 방지를 위해 기본값 `'button'`.
57
+ * 폼 제출 버튼이면 명시적으로 `type="submit"` 지정.
58
+ * @default 'button'
59
+ */
60
+ type?: 'button' | 'submit' | 'reset';
61
+ children?: ReactNode;
62
+ }
63
+ declare const Button: react.ForwardRefExoticComponent<ButtonProps & react.RefAttributes<HTMLButtonElement>>;
64
+
65
+ export { Button, type ButtonProps };
@@ -0,0 +1,65 @@
1
+ import * as react from 'react';
2
+ import { ButtonHTMLAttributes, ReactNode } from 'react';
3
+
4
+ type Variant = 'primary' | 'secondary' | 'outline' | 'ghost';
5
+ type Size = 'sm' | 'md' | 'lg';
6
+ interface ButtonProps extends Omit<ButtonHTMLAttributes<HTMLButtonElement>, 'type'> {
7
+ /**
8
+ * 시각 유형.
9
+ * - `primary`: 브랜드 색 배경 (기본)
10
+ * - `secondary`: 브랜드 옅은 톤 배경
11
+ * - `outline`: 투명 배경 + 테두리
12
+ * - `ghost`: 투명 배경, hover 시에만 배경 채움
13
+ * @default 'primary'
14
+ */
15
+ variant?: Variant;
16
+ /**
17
+ * 사이즈.
18
+ * - `sm`: 36px 높이 — 밀집된 UI, 테이블 액션
19
+ * - `md`: 42px 높이 — 일반 콘텐츠 영역 (기본)
20
+ * - `lg`: 48px 높이 — 주요 CTA, 모바일 터치
21
+ * @default 'md'
22
+ */
23
+ size?: Size;
24
+ /**
25
+ * 라벨 좌측에 표시할 아이콘.
26
+ * @example <Button iconLeft={<SearchIcon />}>검색</Button>
27
+ */
28
+ iconLeft?: ReactNode;
29
+ /**
30
+ * 라벨 우측에 표시할 아이콘.
31
+ */
32
+ iconRight?: ReactNode;
33
+ /**
34
+ * 아이콘만 표시하는 정사각 버튼. **`aria-label` 필수**
35
+ * (icon-only는 시각적으로만 의미가 있으므로 스크린리더용 라벨이 없으면 사용자가 용도를 알 수 없음).
36
+ * children은 단일 아이콘 엘리먼트.
37
+ * @example <Button iconOnly aria-label="닫기"><CloseIcon /></Button>
38
+ */
39
+ iconOnly?: boolean;
40
+ /**
41
+ * 컨테이너 전체 너비로 확장.
42
+ */
43
+ block?: boolean;
44
+ /**
45
+ * 로딩 중 표시. 스피너를 좌측 아이콘 자리에 표시하고, 클릭을 차단하며 `aria-busy="true"`를 부여.
46
+ */
47
+ loading?: boolean;
48
+ /**
49
+ * Radix Slot으로 자식 엘리먼트를 렌더 — 자식이 Button의 className·ref·이벤트 핸들러를 모두 물려받음.
50
+ * 링크를 버튼처럼 보이게 할 때 가장 자주 사용 (next/link, react-router Link 등).
51
+ * @example
52
+ * <Button asChild><Link href="/about">소개</Link></Button>
53
+ */
54
+ asChild?: boolean;
55
+ /**
56
+ * 폼 버튼 타입. 의도치 않은 form submit 방지를 위해 기본값 `'button'`.
57
+ * 폼 제출 버튼이면 명시적으로 `type="submit"` 지정.
58
+ * @default 'button'
59
+ */
60
+ type?: 'button' | 'submit' | 'reset';
61
+ children?: ReactNode;
62
+ }
63
+ declare const Button: react.ForwardRefExoticComponent<ButtonProps & react.RefAttributes<HTMLButtonElement>>;
64
+
65
+ export { Button, type ButtonProps };