@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,109 @@
1
+ // src/Button/Button.tsx
2
+ import "./styles.css";
3
+ import { Slot } from "@radix-ui/react-slot";
4
+ import { Children, cloneElement, forwardRef, isValidElement } from "react";
5
+
6
+ // src/Button/Spinner.tsx
7
+ import { jsx, jsxs } from "react/jsx-runtime";
8
+ var Spinner = () => /* @__PURE__ */ jsx("span", { className: "nuri-button__icon", "aria-hidden": "true", children: /* @__PURE__ */ jsxs("svg", { className: "nuri-button__spinner", viewBox: "0 0 16 16", width: "16", height: "16", fill: "none", children: [
9
+ /* @__PURE__ */ jsx("circle", { cx: "8", cy: "8", r: "6", stroke: "currentColor", strokeOpacity: "0.25", strokeWidth: "2" }),
10
+ /* @__PURE__ */ jsx("path", { d: "M14 8a6 6 0 0 0-6-6", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round" })
11
+ ] }) });
12
+
13
+ // src/utils/cx.ts
14
+ function cx(...classes) {
15
+ return classes.filter(Boolean).join(" ");
16
+ }
17
+
18
+ // src/Button/Button.tsx
19
+ import { Fragment, jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
20
+ var Button = forwardRef(function Button2({
21
+ variant = "primary",
22
+ size = "md",
23
+ iconLeft,
24
+ iconRight,
25
+ iconOnly = false,
26
+ block = false,
27
+ loading = false,
28
+ disabled = false,
29
+ asChild = false,
30
+ type = "button",
31
+ className,
32
+ children,
33
+ onClick,
34
+ ...rest
35
+ }, ref) {
36
+ const isBlocked = disabled || loading;
37
+ if (process.env.NODE_ENV !== "production" && iconOnly && !rest["aria-label"] && !rest["aria-labelledby"]) {
38
+ console.warn(
39
+ "[nuri/Button] `iconOnly` \uBC84\uD2BC\uC740 `aria-label`(\uB610\uB294 `aria-labelledby`)\uC774 \uD544\uC694\uD569\uB2C8\uB2E4."
40
+ );
41
+ }
42
+ const hasLeftSlot = !iconOnly && (loading || iconLeft != null);
43
+ const hasRightSlot = !iconOnly && !loading && iconRight != null;
44
+ const classes = cx(
45
+ "nuri-button",
46
+ `nuri-button--${variant}`,
47
+ `nuri-button--${size}`,
48
+ block && "nuri-button--block",
49
+ iconOnly && "nuri-button--icon-only",
50
+ isBlocked && "nuri-button--disabled",
51
+ loading && "nuri-button--loading",
52
+ hasLeftSlot && "nuri-button--has-left",
53
+ hasRightSlot && "nuri-button--has-right",
54
+ className
55
+ );
56
+ const handleClick = (e) => {
57
+ if (isBlocked) {
58
+ e.preventDefault();
59
+ e.stopPropagation();
60
+ return;
61
+ }
62
+ onClick?.(e);
63
+ };
64
+ const renderContent = (content) => {
65
+ if (iconOnly) {
66
+ return /* @__PURE__ */ jsx2("span", { className: "nuri-button__icon", "aria-hidden": "true", children: loading ? /* @__PURE__ */ jsx2(Spinner, {}) : content });
67
+ }
68
+ return /* @__PURE__ */ jsxs2(Fragment, { children: [
69
+ loading ? /* @__PURE__ */ jsx2(Spinner, {}) : iconLeft != null && /* @__PURE__ */ jsx2("span", { className: "nuri-button__icon nuri-button__icon--left", "aria-hidden": "true", children: iconLeft }),
70
+ content,
71
+ hasRightSlot && /* @__PURE__ */ jsx2("span", { className: "nuri-button__icon nuri-button__icon--right", "aria-hidden": "true", children: iconRight })
72
+ ] });
73
+ };
74
+ if (asChild) {
75
+ const child = Children.only(children);
76
+ if (!isValidElement(child)) return null;
77
+ const childElement = child;
78
+ return /* @__PURE__ */ jsx2(
79
+ Slot,
80
+ {
81
+ ref,
82
+ className: classes,
83
+ "aria-disabled": isBlocked || void 0,
84
+ "aria-busy": loading || void 0,
85
+ onClick: handleClick,
86
+ ...rest,
87
+ children: cloneElement(childElement, void 0, renderContent(childElement.props.children))
88
+ }
89
+ );
90
+ }
91
+ return /* @__PURE__ */ jsx2(
92
+ "button",
93
+ {
94
+ ref,
95
+ type,
96
+ className: classes,
97
+ disabled,
98
+ "aria-disabled": loading || void 0,
99
+ "aria-busy": loading || void 0,
100
+ onClick: handleClick,
101
+ ...rest,
102
+ children: renderContent(children)
103
+ }
104
+ );
105
+ });
106
+ export {
107
+ Button
108
+ };
109
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/Button/Button.tsx","../../src/Button/Spinner.tsx","../../src/utils/cx.ts"],"sourcesContent":["'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":["jsx","jsxs","Button"]}
@@ -0,0 +1,181 @@
1
+ .nuri-button {
2
+ display: inline-flex;
3
+ align-items: center;
4
+ justify-content: center;
5
+ gap: var(--nuri-button-gap);
6
+ border: 1px solid transparent;
7
+ font-family: inherit; /* 앱이 설정한 폰트를 그대로 상속 (디자인 시스템은 폰트 종류를 강제하지 않음) */
8
+ font-weight: var(--nuri-button-font-weight);
9
+ line-height: var(--nuri-button-line-height);
10
+ cursor: pointer;
11
+ white-space: nowrap;
12
+ user-select: none;
13
+ transition:
14
+ background-color 120ms ease,
15
+ color 120ms ease,
16
+ border-color 120ms ease;
17
+ -webkit-appearance: none;
18
+ appearance: none;
19
+ }
20
+
21
+ .nuri-button:focus-visible {
22
+ outline: var(--nuri-button-focus-ring-width) solid
23
+ var(--nuri-button-focus-ring-color, var(--nuri-color-brand-default));
24
+ outline-offset: var(--nuri-button-focus-ring-offset);
25
+ }
26
+
27
+ .nuri-button:disabled,
28
+ .nuri-button[aria-disabled='true'] {
29
+ cursor: not-allowed;
30
+ pointer-events: none;
31
+ }
32
+
33
+ .nuri-button--block {
34
+ display: flex;
35
+ width: 100%;
36
+ }
37
+
38
+ .nuri-button--loading {
39
+ cursor: progress;
40
+ }
41
+
42
+ .nuri-button--sm {
43
+ height: var(--nuri-button-size-sm-height);
44
+ padding: var(--nuri-button-size-sm-padding-y) var(--nuri-button-size-sm-padding-x);
45
+ font-size: var(--nuri-button-size-sm-font-size);
46
+ border-radius: var(--nuri-button-size-sm-radius);
47
+ }
48
+ .nuri-button--md {
49
+ height: var(--nuri-button-size-md-height);
50
+ padding: var(--nuri-button-size-md-padding-y) var(--nuri-button-size-md-padding-x);
51
+ font-size: var(--nuri-button-size-md-font-size);
52
+ border-radius: var(--nuri-button-size-md-radius);
53
+ }
54
+ .nuri-button--lg {
55
+ height: var(--nuri-button-size-lg-height);
56
+ padding: var(--nuri-button-size-lg-padding-y) var(--nuri-button-size-lg-padding-x);
57
+ font-size: var(--nuri-button-size-lg-font-size);
58
+ border-radius: var(--nuri-button-size-lg-radius);
59
+ }
60
+
61
+ .nuri-button--sm.nuri-button--has-left {
62
+ padding-left: var(--nuri-button-size-sm-padding-x-narrow);
63
+ }
64
+ .nuri-button--sm.nuri-button--has-right {
65
+ padding-right: var(--nuri-button-size-sm-padding-x-narrow);
66
+ }
67
+ .nuri-button--md.nuri-button--has-left {
68
+ padding-left: var(--nuri-button-size-md-padding-x-narrow);
69
+ }
70
+ .nuri-button--md.nuri-button--has-right {
71
+ padding-right: var(--nuri-button-size-md-padding-x-narrow);
72
+ }
73
+ .nuri-button--lg.nuri-button--has-left {
74
+ padding-left: var(--nuri-button-size-lg-padding-x-narrow);
75
+ }
76
+ .nuri-button--lg.nuri-button--has-right {
77
+ padding-right: var(--nuri-button-size-lg-padding-x-narrow);
78
+ }
79
+
80
+ .nuri-button--icon-only {
81
+ padding: 0;
82
+ }
83
+ .nuri-button--icon-only.nuri-button--sm {
84
+ width: var(--nuri-button-size-sm-height);
85
+ }
86
+ .nuri-button--icon-only.nuri-button--md {
87
+ width: var(--nuri-button-size-md-height);
88
+ }
89
+ .nuri-button--icon-only.nuri-button--lg {
90
+ width: var(--nuri-button-size-lg-height);
91
+ }
92
+
93
+ .nuri-button__icon {
94
+ display: inline-flex;
95
+ align-items: center;
96
+ justify-content: center;
97
+ width: var(--nuri-button-icon-size);
98
+ height: var(--nuri-button-icon-size);
99
+ flex-shrink: 0;
100
+ }
101
+ .nuri-button__icon > svg {
102
+ width: 100%;
103
+ height: 100%;
104
+ }
105
+
106
+ .nuri-button__spinner {
107
+ animation: nuri-button-spin 800ms linear infinite;
108
+ }
109
+ @keyframes nuri-button-spin {
110
+ to {
111
+ transform: rotate(360deg);
112
+ }
113
+ }
114
+
115
+ /* 멀미 민감 사용자: 모션 줄이되 상태 신호는 유지 */
116
+ @media (prefers-reduced-motion: reduce) {
117
+ .nuri-button {
118
+ transition: none;
119
+ }
120
+ .nuri-button__spinner {
121
+ animation-duration: 2400ms;
122
+ }
123
+ }
124
+
125
+ .nuri-button--primary {
126
+ background-color: var(--nuri-button-primary-bg, var(--nuri-color-brand-default));
127
+ color: var(--nuri-button-primary-fg);
128
+ }
129
+ .nuri-button--primary:hover:not(:disabled):not([aria-disabled='true']),
130
+ .nuri-button--primary:active:not(:disabled):not([aria-disabled='true']) {
131
+ background-color: var(--nuri-button-primary-bg-hover, var(--nuri-color-brand-pressed));
132
+ }
133
+ .nuri-button--primary:disabled,
134
+ .nuri-button--primary[aria-disabled='true'] {
135
+ background-color: var(--nuri-button-primary-bg-disabled);
136
+ color: var(--nuri-button-primary-fg-disabled);
137
+ }
138
+
139
+ .nuri-button--secondary {
140
+ background-color: var(--nuri-button-secondary-bg, var(--nuri-color-brand-subtle));
141
+ color: var(--nuri-button-secondary-fg, var(--nuri-color-brand-default));
142
+ }
143
+ .nuri-button--secondary:hover:not(:disabled):not([aria-disabled='true']),
144
+ .nuri-button--secondary:active:not(:disabled):not([aria-disabled='true']) {
145
+ background-color: var(--nuri-button-secondary-bg-hover, var(--nuri-color-brand-subtle-pressed));
146
+ }
147
+ .nuri-button--secondary:disabled,
148
+ .nuri-button--secondary[aria-disabled='true'] {
149
+ background-color: var(--nuri-button-secondary-bg-disabled);
150
+ color: var(--nuri-button-secondary-fg-disabled);
151
+ }
152
+
153
+ .nuri-button--outline {
154
+ background-color: var(--nuri-button-outline-bg);
155
+ color: var(--nuri-button-outline-fg);
156
+ border-color: var(--nuri-button-outline-border);
157
+ }
158
+ .nuri-button--outline:hover:not(:disabled):not([aria-disabled='true']),
159
+ .nuri-button--outline:active:not(:disabled):not([aria-disabled='true']) {
160
+ background-color: var(--nuri-button-outline-bg-hover);
161
+ }
162
+ .nuri-button--outline:disabled,
163
+ .nuri-button--outline[aria-disabled='true'] {
164
+ background-color: var(--nuri-button-outline-bg-disabled);
165
+ color: var(--nuri-button-outline-fg-disabled);
166
+ border-color: var(--nuri-button-outline-border-disabled);
167
+ }
168
+
169
+ .nuri-button--ghost {
170
+ background-color: var(--nuri-button-ghost-bg);
171
+ color: var(--nuri-button-ghost-fg);
172
+ }
173
+ .nuri-button--ghost:hover:not(:disabled):not([aria-disabled='true']),
174
+ .nuri-button--ghost:active:not(:disabled):not([aria-disabled='true']) {
175
+ background-color: var(--nuri-button-ghost-bg-hover);
176
+ }
177
+ .nuri-button--ghost:disabled,
178
+ .nuri-button--ghost[aria-disabled='true'] {
179
+ background-color: var(--nuri-button-ghost-bg-disabled);
180
+ color: var(--nuri-button-ghost-fg-disabled);
181
+ }
@@ -0,0 +1,83 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __export = (target, all) => {
9
+ for (var name in all)
10
+ __defProp(target, name, { get: all[name], enumerable: true });
11
+ };
12
+ var __copyProps = (to, from, except, desc) => {
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (let key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(to, key) && key !== except)
16
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
+ }
18
+ return to;
19
+ };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
28
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
+
30
+ // src/Checkbox/index.ts
31
+ var Checkbox_exports = {};
32
+ __export(Checkbox_exports, {
33
+ Checkbox: () => Checkbox
34
+ });
35
+ module.exports = __toCommonJS(Checkbox_exports);
36
+
37
+ // src/Checkbox/Checkbox.tsx
38
+ var import_styles = require("./styles.css");
39
+ var RadixCheckbox = __toESM(require("@radix-ui/react-checkbox"), 1);
40
+ var import_react = require("react");
41
+
42
+ // src/utils/cx.ts
43
+ function cx(...classes) {
44
+ return classes.filter(Boolean).join(" ");
45
+ }
46
+
47
+ // src/Checkbox/Checkbox.tsx
48
+ var import_jsx_runtime = require("react/jsx-runtime");
49
+ var CheckIcon = () => /* @__PURE__ */ (0, import_jsx_runtime.jsx)("svg", { viewBox: "0 0 12 12", fill: "none", "aria-hidden": "true", children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
50
+ "path",
51
+ {
52
+ d: "M2.5 6.2l2.4 2.4 4.6-5",
53
+ stroke: "currentColor",
54
+ strokeWidth: "1.6",
55
+ strokeLinecap: "round",
56
+ strokeLinejoin: "round"
57
+ }
58
+ ) });
59
+ var IndeterminateIcon = () => /* @__PURE__ */ (0, import_jsx_runtime.jsx)("svg", { viewBox: "0 0 12 12", fill: "none", "aria-hidden": "true", children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("path", { d: "M2.8 6h6.4", stroke: "currentColor", strokeWidth: "1.6", strokeLinecap: "round" }) });
60
+ var Checkbox = (0, import_react.forwardRef)(function Checkbox2({ label, size = "md", disabled = false, id: idProp, className, checked, ...rest }, ref) {
61
+ const reactId = (0, import_react.useId)();
62
+ const id = idProp ?? `nuri-checkbox-${reactId}`;
63
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: cx("nuri-checkbox", `nuri-checkbox--${size}`, className), children: [
64
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
65
+ RadixCheckbox.Root,
66
+ {
67
+ ref,
68
+ id,
69
+ className: "nuri-checkbox__control",
70
+ disabled,
71
+ checked,
72
+ ...rest,
73
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(RadixCheckbox.Indicator, { className: "nuri-checkbox__indicator", children: checked === "indeterminate" ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(IndeterminateIcon, {}) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)(CheckIcon, {}) })
74
+ }
75
+ ),
76
+ label != null && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("label", { htmlFor: id, className: "nuri-checkbox__label", children: label })
77
+ ] });
78
+ });
79
+ // Annotate the CommonJS export names for ESM import in node:
80
+ 0 && (module.exports = {
81
+ Checkbox
82
+ });
83
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/Checkbox/index.ts","../../src/Checkbox/Checkbox.tsx","../../src/utils/cx.ts"],"sourcesContent":["export { Checkbox } from './Checkbox.js';\nexport type { CheckboxProps } from './Checkbox.js';\n","'use client';\n\nimport './styles.css';\nimport * as RadixCheckbox from '@radix-ui/react-checkbox';\nimport { forwardRef, useId } from 'react';\nimport type { ReactNode } from 'react';\nimport { cx } from '../utils/cx.js';\n\ntype Size = 'sm' | 'md';\n\nexport interface CheckboxProps {\n /** 라벨 (컨트롤 우측). 지정 시 클릭 영역에 포함되고 자동 연결됨. 없으면 `aria-label` 필수. */\n label?: ReactNode;\n /**\n * 사이즈. md 16px(디자인 Large, 기본) / sm 12px(디자인 Small).\n * @default 'md'\n */\n size?: Size;\n /** 체크 상태 (제어형). `'indeterminate'`는 부분 선택 표시. */\n checked?: boolean | 'indeterminate';\n /** 초기 체크 상태 (비제어형). */\n defaultChecked?: boolean;\n onCheckedChange?: (checked: boolean | 'indeterminate') => void;\n disabled?: boolean;\n /** 폼 전송용 name — 네이티브 checkbox처럼 폼에 참여. */\n name?: string;\n /** 폼 전송 시 값. @default 'on' */\n value?: string;\n required?: boolean;\n id?: string;\n className?: string;\n 'aria-label'?: string;\n}\n\n/** 체크마크. TODO(Figma 실측): 디자이너 아이콘으로 교체 예정 — 잠정 표준형. */\nconst CheckIcon = () => (\n <svg viewBox=\"0 0 12 12\" fill=\"none\" aria-hidden=\"true\">\n <path\n d=\"M2.5 6.2l2.4 2.4 4.6-5\"\n stroke=\"currentColor\"\n strokeWidth=\"1.6\"\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n />\n </svg>\n);\n\n/** 부분 선택(indeterminate) 표시 — 가로 막대. */\nconst IndeterminateIcon = () => (\n <svg viewBox=\"0 0 12 12\" fill=\"none\" aria-hidden=\"true\">\n <path d=\"M2.8 6h6.4\" stroke=\"currentColor\" strokeWidth=\"1.6\" strokeLinecap=\"round\" />\n </svg>\n);\n\n/**\n * 체크박스. Radix Checkbox 기반 — Space 토글·aria-checked·폼 참여 자동.\n * 라벨을 지정하면 라벨 클릭으로도 토글된다.\n *\n * @example\n * <Checkbox label=\"이용약관에 동의합니다\" checked={agreed} onCheckedChange={setAgreed} />\n *\n * @example 부분 선택 (전체선택 헤더 등)\n * <Checkbox label=\"전체 선택\" checked={someChecked ? 'indeterminate' : allChecked} … />\n */\nexport const Checkbox = forwardRef<HTMLButtonElement, CheckboxProps>(function Checkbox(\n { label, size = 'md', disabled = false, id: idProp, className, checked, ...rest },\n ref,\n) {\n const reactId = useId();\n const id = idProp ?? `nuri-checkbox-${reactId}`;\n\n return (\n <div className={cx('nuri-checkbox', `nuri-checkbox--${size}`, className)}>\n <RadixCheckbox.Root\n ref={ref}\n id={id}\n className=\"nuri-checkbox__control\"\n disabled={disabled}\n checked={checked}\n {...rest}\n >\n <RadixCheckbox.Indicator className=\"nuri-checkbox__indicator\">\n {checked === 'indeterminate' ? <IndeterminateIcon /> : <CheckIcon />}\n </RadixCheckbox.Indicator>\n </RadixCheckbox.Root>\n {label != null && (\n <label htmlFor={id} className=\"nuri-checkbox__label\">\n {label}\n </label>\n )}\n </div>\n );\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":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACEA,oBAAO;AACP,oBAA+B;AAC/B,mBAAkC;;;ACA3B,SAAS,MAAM,SAA2D;AAC/E,SAAO,QAAQ,OAAO,OAAO,EAAE,KAAK,GAAG;AACzC;;;AD+BI;AAFJ,IAAM,YAAY,MAChB,4CAAC,SAAI,SAAQ,aAAY,MAAK,QAAO,eAAY,QAC/C;AAAA,EAAC;AAAA;AAAA,IACC,GAAE;AAAA,IACF,QAAO;AAAA,IACP,aAAY;AAAA,IACZ,eAAc;AAAA,IACd,gBAAe;AAAA;AACjB,GACF;AAIF,IAAM,oBAAoB,MACxB,4CAAC,SAAI,SAAQ,aAAY,MAAK,QAAO,eAAY,QAC/C,sDAAC,UAAK,GAAE,cAAa,QAAO,gBAAe,aAAY,OAAM,eAAc,SAAQ,GACrF;AAaK,IAAM,eAAW,yBAA6C,SAASA,UAC5E,EAAE,OAAO,OAAO,MAAM,WAAW,OAAO,IAAI,QAAQ,WAAW,SAAS,GAAG,KAAK,GAChF,KACA;AACA,QAAM,cAAU,oBAAM;AACtB,QAAM,KAAK,UAAU,iBAAiB,OAAO;AAE7C,SACE,6CAAC,SAAI,WAAW,GAAG,iBAAiB,kBAAkB,IAAI,IAAI,SAAS,GACrE;AAAA;AAAA,MAAe;AAAA,MAAd;AAAA,QACC;AAAA,QACA;AAAA,QACA,WAAU;AAAA,QACV;AAAA,QACA;AAAA,QACC,GAAG;AAAA,QAEJ,sDAAe,yBAAd,EAAwB,WAAU,4BAChC,sBAAY,kBAAkB,4CAAC,qBAAkB,IAAK,4CAAC,aAAU,GACpE;AAAA;AAAA,IACF;AAAA,IACC,SAAS,QACR,4CAAC,WAAM,SAAS,IAAI,WAAU,wBAC3B,iBACH;AAAA,KAEJ;AAEJ,CAAC;","names":["Checkbox"]}
@@ -0,0 +1,40 @@
1
+ import * as react from 'react';
2
+ import { ReactNode } from 'react';
3
+
4
+ type Size = 'sm' | 'md';
5
+ interface CheckboxProps {
6
+ /** 라벨 (컨트롤 우측). 지정 시 클릭 영역에 포함되고 자동 연결됨. 없으면 `aria-label` 필수. */
7
+ label?: ReactNode;
8
+ /**
9
+ * 사이즈. md 16px(디자인 Large, 기본) / sm 12px(디자인 Small).
10
+ * @default 'md'
11
+ */
12
+ size?: Size;
13
+ /** 체크 상태 (제어형). `'indeterminate'`는 부분 선택 표시. */
14
+ checked?: boolean | 'indeterminate';
15
+ /** 초기 체크 상태 (비제어형). */
16
+ defaultChecked?: boolean;
17
+ onCheckedChange?: (checked: boolean | 'indeterminate') => void;
18
+ disabled?: boolean;
19
+ /** 폼 전송용 name — 네이티브 checkbox처럼 폼에 참여. */
20
+ name?: string;
21
+ /** 폼 전송 시 값. @default 'on' */
22
+ value?: string;
23
+ required?: boolean;
24
+ id?: string;
25
+ className?: string;
26
+ 'aria-label'?: string;
27
+ }
28
+ /**
29
+ * 체크박스. Radix Checkbox 기반 — Space 토글·aria-checked·폼 참여 자동.
30
+ * 라벨을 지정하면 라벨 클릭으로도 토글된다.
31
+ *
32
+ * @example
33
+ * <Checkbox label="이용약관에 동의합니다" checked={agreed} onCheckedChange={setAgreed} />
34
+ *
35
+ * @example 부분 선택 (전체선택 헤더 등)
36
+ * <Checkbox label="전체 선택" checked={someChecked ? 'indeterminate' : allChecked} … />
37
+ */
38
+ declare const Checkbox: react.ForwardRefExoticComponent<CheckboxProps & react.RefAttributes<HTMLButtonElement>>;
39
+
40
+ export { Checkbox, type CheckboxProps };
@@ -0,0 +1,40 @@
1
+ import * as react from 'react';
2
+ import { ReactNode } from 'react';
3
+
4
+ type Size = 'sm' | 'md';
5
+ interface CheckboxProps {
6
+ /** 라벨 (컨트롤 우측). 지정 시 클릭 영역에 포함되고 자동 연결됨. 없으면 `aria-label` 필수. */
7
+ label?: ReactNode;
8
+ /**
9
+ * 사이즈. md 16px(디자인 Large, 기본) / sm 12px(디자인 Small).
10
+ * @default 'md'
11
+ */
12
+ size?: Size;
13
+ /** 체크 상태 (제어형). `'indeterminate'`는 부분 선택 표시. */
14
+ checked?: boolean | 'indeterminate';
15
+ /** 초기 체크 상태 (비제어형). */
16
+ defaultChecked?: boolean;
17
+ onCheckedChange?: (checked: boolean | 'indeterminate') => void;
18
+ disabled?: boolean;
19
+ /** 폼 전송용 name — 네이티브 checkbox처럼 폼에 참여. */
20
+ name?: string;
21
+ /** 폼 전송 시 값. @default 'on' */
22
+ value?: string;
23
+ required?: boolean;
24
+ id?: string;
25
+ className?: string;
26
+ 'aria-label'?: string;
27
+ }
28
+ /**
29
+ * 체크박스. Radix Checkbox 기반 — Space 토글·aria-checked·폼 참여 자동.
30
+ * 라벨을 지정하면 라벨 클릭으로도 토글된다.
31
+ *
32
+ * @example
33
+ * <Checkbox label="이용약관에 동의합니다" checked={agreed} onCheckedChange={setAgreed} />
34
+ *
35
+ * @example 부분 선택 (전체선택 헤더 등)
36
+ * <Checkbox label="전체 선택" checked={someChecked ? 'indeterminate' : allChecked} … />
37
+ */
38
+ declare const Checkbox: react.ForwardRefExoticComponent<CheckboxProps & react.RefAttributes<HTMLButtonElement>>;
39
+
40
+ export { Checkbox, type CheckboxProps };
@@ -0,0 +1,46 @@
1
+ // src/Checkbox/Checkbox.tsx
2
+ import "./styles.css";
3
+ import * as RadixCheckbox from "@radix-ui/react-checkbox";
4
+ import { forwardRef, useId } from "react";
5
+
6
+ // src/utils/cx.ts
7
+ function cx(...classes) {
8
+ return classes.filter(Boolean).join(" ");
9
+ }
10
+
11
+ // src/Checkbox/Checkbox.tsx
12
+ import { jsx, jsxs } from "react/jsx-runtime";
13
+ var CheckIcon = () => /* @__PURE__ */ jsx("svg", { viewBox: "0 0 12 12", fill: "none", "aria-hidden": "true", children: /* @__PURE__ */ jsx(
14
+ "path",
15
+ {
16
+ d: "M2.5 6.2l2.4 2.4 4.6-5",
17
+ stroke: "currentColor",
18
+ strokeWidth: "1.6",
19
+ strokeLinecap: "round",
20
+ strokeLinejoin: "round"
21
+ }
22
+ ) });
23
+ var IndeterminateIcon = () => /* @__PURE__ */ jsx("svg", { viewBox: "0 0 12 12", fill: "none", "aria-hidden": "true", children: /* @__PURE__ */ jsx("path", { d: "M2.8 6h6.4", stroke: "currentColor", strokeWidth: "1.6", strokeLinecap: "round" }) });
24
+ var Checkbox = forwardRef(function Checkbox2({ label, size = "md", disabled = false, id: idProp, className, checked, ...rest }, ref) {
25
+ const reactId = useId();
26
+ const id = idProp ?? `nuri-checkbox-${reactId}`;
27
+ return /* @__PURE__ */ jsxs("div", { className: cx("nuri-checkbox", `nuri-checkbox--${size}`, className), children: [
28
+ /* @__PURE__ */ jsx(
29
+ RadixCheckbox.Root,
30
+ {
31
+ ref,
32
+ id,
33
+ className: "nuri-checkbox__control",
34
+ disabled,
35
+ checked,
36
+ ...rest,
37
+ children: /* @__PURE__ */ jsx(RadixCheckbox.Indicator, { className: "nuri-checkbox__indicator", children: checked === "indeterminate" ? /* @__PURE__ */ jsx(IndeterminateIcon, {}) : /* @__PURE__ */ jsx(CheckIcon, {}) })
38
+ }
39
+ ),
40
+ label != null && /* @__PURE__ */ jsx("label", { htmlFor: id, className: "nuri-checkbox__label", children: label })
41
+ ] });
42
+ });
43
+ export {
44
+ Checkbox
45
+ };
46
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/Checkbox/Checkbox.tsx","../../src/utils/cx.ts"],"sourcesContent":["'use client';\n\nimport './styles.css';\nimport * as RadixCheckbox from '@radix-ui/react-checkbox';\nimport { forwardRef, useId } from 'react';\nimport type { ReactNode } from 'react';\nimport { cx } from '../utils/cx.js';\n\ntype Size = 'sm' | 'md';\n\nexport interface CheckboxProps {\n /** 라벨 (컨트롤 우측). 지정 시 클릭 영역에 포함되고 자동 연결됨. 없으면 `aria-label` 필수. */\n label?: ReactNode;\n /**\n * 사이즈. md 16px(디자인 Large, 기본) / sm 12px(디자인 Small).\n * @default 'md'\n */\n size?: Size;\n /** 체크 상태 (제어형). `'indeterminate'`는 부분 선택 표시. */\n checked?: boolean | 'indeterminate';\n /** 초기 체크 상태 (비제어형). */\n defaultChecked?: boolean;\n onCheckedChange?: (checked: boolean | 'indeterminate') => void;\n disabled?: boolean;\n /** 폼 전송용 name — 네이티브 checkbox처럼 폼에 참여. */\n name?: string;\n /** 폼 전송 시 값. @default 'on' */\n value?: string;\n required?: boolean;\n id?: string;\n className?: string;\n 'aria-label'?: string;\n}\n\n/** 체크마크. TODO(Figma 실측): 디자이너 아이콘으로 교체 예정 — 잠정 표준형. */\nconst CheckIcon = () => (\n <svg viewBox=\"0 0 12 12\" fill=\"none\" aria-hidden=\"true\">\n <path\n d=\"M2.5 6.2l2.4 2.4 4.6-5\"\n stroke=\"currentColor\"\n strokeWidth=\"1.6\"\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n />\n </svg>\n);\n\n/** 부분 선택(indeterminate) 표시 — 가로 막대. */\nconst IndeterminateIcon = () => (\n <svg viewBox=\"0 0 12 12\" fill=\"none\" aria-hidden=\"true\">\n <path d=\"M2.8 6h6.4\" stroke=\"currentColor\" strokeWidth=\"1.6\" strokeLinecap=\"round\" />\n </svg>\n);\n\n/**\n * 체크박스. Radix Checkbox 기반 — Space 토글·aria-checked·폼 참여 자동.\n * 라벨을 지정하면 라벨 클릭으로도 토글된다.\n *\n * @example\n * <Checkbox label=\"이용약관에 동의합니다\" checked={agreed} onCheckedChange={setAgreed} />\n *\n * @example 부분 선택 (전체선택 헤더 등)\n * <Checkbox label=\"전체 선택\" checked={someChecked ? 'indeterminate' : allChecked} … />\n */\nexport const Checkbox = forwardRef<HTMLButtonElement, CheckboxProps>(function Checkbox(\n { label, size = 'md', disabled = false, id: idProp, className, checked, ...rest },\n ref,\n) {\n const reactId = useId();\n const id = idProp ?? `nuri-checkbox-${reactId}`;\n\n return (\n <div className={cx('nuri-checkbox', `nuri-checkbox--${size}`, className)}>\n <RadixCheckbox.Root\n ref={ref}\n id={id}\n className=\"nuri-checkbox__control\"\n disabled={disabled}\n checked={checked}\n {...rest}\n >\n <RadixCheckbox.Indicator className=\"nuri-checkbox__indicator\">\n {checked === 'indeterminate' ? <IndeterminateIcon /> : <CheckIcon />}\n </RadixCheckbox.Indicator>\n </RadixCheckbox.Root>\n {label != null && (\n <label htmlFor={id} className=\"nuri-checkbox__label\">\n {label}\n </label>\n )}\n </div>\n );\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":";AAEA,OAAO;AACP,YAAY,mBAAmB;AAC/B,SAAS,YAAY,aAAa;;;ACA3B,SAAS,MAAM,SAA2D;AAC/E,SAAO,QAAQ,OAAO,OAAO,EAAE,KAAK,GAAG;AACzC;;;AD+BI,cAmCA,YAnCA;AAFJ,IAAM,YAAY,MAChB,oBAAC,SAAI,SAAQ,aAAY,MAAK,QAAO,eAAY,QAC/C;AAAA,EAAC;AAAA;AAAA,IACC,GAAE;AAAA,IACF,QAAO;AAAA,IACP,aAAY;AAAA,IACZ,eAAc;AAAA,IACd,gBAAe;AAAA;AACjB,GACF;AAIF,IAAM,oBAAoB,MACxB,oBAAC,SAAI,SAAQ,aAAY,MAAK,QAAO,eAAY,QAC/C,8BAAC,UAAK,GAAE,cAAa,QAAO,gBAAe,aAAY,OAAM,eAAc,SAAQ,GACrF;AAaK,IAAM,WAAW,WAA6C,SAASA,UAC5E,EAAE,OAAO,OAAO,MAAM,WAAW,OAAO,IAAI,QAAQ,WAAW,SAAS,GAAG,KAAK,GAChF,KACA;AACA,QAAM,UAAU,MAAM;AACtB,QAAM,KAAK,UAAU,iBAAiB,OAAO;AAE7C,SACE,qBAAC,SAAI,WAAW,GAAG,iBAAiB,kBAAkB,IAAI,IAAI,SAAS,GACrE;AAAA;AAAA,MAAe;AAAA,MAAd;AAAA,QACC;AAAA,QACA;AAAA,QACA,WAAU;AAAA,QACV;AAAA,QACA;AAAA,QACC,GAAG;AAAA,QAEJ,8BAAe,yBAAd,EAAwB,WAAU,4BAChC,sBAAY,kBAAkB,oBAAC,qBAAkB,IAAK,oBAAC,aAAU,GACpE;AAAA;AAAA,IACF;AAAA,IACC,SAAS,QACR,oBAAC,WAAM,SAAS,IAAI,WAAU,wBAC3B,iBACH;AAAA,KAEJ;AAEJ,CAAC;","names":["Checkbox"]}
@@ -0,0 +1,78 @@
1
+ /* 공통 컨트롤 토큰(--nuri-control-*) 사용. 체크마크 모양·radius는 잠정 — TODO(Figma 실측 후 확정) */
2
+ .nuri-checkbox {
3
+ display: inline-flex;
4
+ align-items: center;
5
+ gap: var(--nuri-control-gap);
6
+ font-family: inherit;
7
+ }
8
+
9
+ .nuri-checkbox__control {
10
+ box-sizing: border-box;
11
+ display: inline-flex;
12
+ align-items: center;
13
+ justify-content: center;
14
+ flex-shrink: 0;
15
+ padding: 0;
16
+ background-color: var(--nuri-control-bg);
17
+ border: 1px solid var(--nuri-control-border);
18
+ border-radius: var(--nuri-control-checkbox-radius);
19
+ cursor: pointer;
20
+ transition:
21
+ background-color 120ms ease,
22
+ border-color 120ms ease;
23
+ }
24
+ .nuri-checkbox--md .nuri-checkbox__control {
25
+ width: var(--nuri-control-checkbox-size-md);
26
+ height: var(--nuri-control-checkbox-size-md);
27
+ }
28
+ .nuri-checkbox--sm .nuri-checkbox__control {
29
+ width: var(--nuri-control-checkbox-size-sm);
30
+ height: var(--nuri-control-checkbox-size-sm);
31
+ }
32
+
33
+ .nuri-checkbox__control[data-state='checked'],
34
+ .nuri-checkbox__control[data-state='indeterminate'] {
35
+ background-color: var(--nuri-control-bg-checked, var(--nuri-color-brand-default));
36
+ border-color: var(--nuri-control-bg-checked, var(--nuri-color-brand-default));
37
+ }
38
+
39
+ .nuri-checkbox__indicator {
40
+ display: inline-flex;
41
+ align-items: center;
42
+ justify-content: center;
43
+ width: 100%;
44
+ height: 100%;
45
+ color: var(--nuri-control-fg-checked);
46
+ }
47
+ .nuri-checkbox__indicator > svg {
48
+ width: 80%;
49
+ height: 80%;
50
+ }
51
+
52
+ .nuri-checkbox__control:focus-visible {
53
+ outline: var(--nuri-button-focus-ring-width) solid
54
+ var(--nuri-button-focus-ring-color, var(--nuri-color-brand-default));
55
+ outline-offset: var(--nuri-button-focus-ring-offset);
56
+ }
57
+
58
+ .nuri-checkbox__control:disabled {
59
+ background-color: var(--nuri-control-bg-disabled);
60
+ border-color: var(--nuri-control-border-disabled);
61
+ cursor: not-allowed;
62
+ }
63
+ .nuri-checkbox__control:disabled[data-state='checked'],
64
+ .nuri-checkbox__control:disabled[data-state='indeterminate'] {
65
+ background-color: var(--nuri-control-border-disabled);
66
+ }
67
+
68
+ .nuri-checkbox__label {
69
+ font-size: var(--nuri-control-label-size);
70
+ line-height: var(--nuri-control-line-height);
71
+ color: var(--nuri-control-label-color);
72
+ cursor: pointer;
73
+ user-select: none;
74
+ }
75
+ .nuri-checkbox:has(.nuri-checkbox__control:disabled) .nuri-checkbox__label {
76
+ color: var(--nuri-control-label-disabled);
77
+ cursor: not-allowed;
78
+ }