@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.
- package/LICENSE +21 -0
- package/README.md +274 -0
- package/dist/AlertDialog/index.cjs +185 -0
- package/dist/AlertDialog/index.cjs.map +1 -0
- package/dist/AlertDialog/index.d.cts +49 -0
- package/dist/AlertDialog/index.d.ts +49 -0
- package/dist/AlertDialog/index.js +148 -0
- package/dist/AlertDialog/index.js.map +1 -0
- package/dist/AlertDialog/styles.css +187 -0
- package/dist/Button/index.cjs +136 -0
- package/dist/Button/index.cjs.map +1 -0
- package/dist/Button/index.d.cts +65 -0
- package/dist/Button/index.d.ts +65 -0
- package/dist/Button/index.js +109 -0
- package/dist/Button/index.js.map +1 -0
- package/dist/Button/styles.css +181 -0
- package/dist/Checkbox/index.cjs +83 -0
- package/dist/Checkbox/index.cjs.map +1 -0
- package/dist/Checkbox/index.d.cts +40 -0
- package/dist/Checkbox/index.d.ts +40 -0
- package/dist/Checkbox/index.js +46 -0
- package/dist/Checkbox/index.js.map +1 -0
- package/dist/Checkbox/styles.css +78 -0
- package/dist/Input/index.cjs +103 -0
- package/dist/Input/index.cjs.map +1 -0
- package/dist/Input/index.d.cts +53 -0
- package/dist/Input/index.d.ts +53 -0
- package/dist/Input/index.js +76 -0
- package/dist/Input/index.js.map +1 -0
- package/dist/Input/styles.css +135 -0
- package/dist/Modal/index.cjs +120 -0
- package/dist/Modal/index.cjs.map +1 -0
- package/dist/Modal/index.d.cts +62 -0
- package/dist/Modal/index.d.ts +62 -0
- package/dist/Modal/index.js +83 -0
- package/dist/Modal/index.js.map +1 -0
- package/dist/Modal/styles.css +229 -0
- package/dist/Radio/index.cjs +86 -0
- package/dist/Radio/index.cjs.map +1 -0
- package/dist/Radio/index.d.cts +46 -0
- package/dist/Radio/index.d.ts +46 -0
- package/dist/Radio/index.js +48 -0
- package/dist/Radio/index.js.map +1 -0
- package/dist/Radio/styles.css +76 -0
- package/dist/Select/index.cjs +116 -0
- package/dist/Select/index.cjs.map +1 -0
- package/dist/Select/index.d.cts +61 -0
- package/dist/Select/index.d.ts +61 -0
- package/dist/Select/index.js +79 -0
- package/dist/Select/index.js.map +1 -0
- package/dist/Select/styles.css +157 -0
- package/dist/Switch/index.cjs +71 -0
- package/dist/Switch/index.cjs.map +1 -0
- package/dist/Switch/index.d.cts +34 -0
- package/dist/Switch/index.d.ts +34 -0
- package/dist/Switch/index.js +34 -0
- package/dist/Switch/index.js.map +1 -0
- package/dist/Switch/styles.css +71 -0
- package/dist/Textarea/index.cjs +127 -0
- package/dist/Textarea/index.cjs.map +1 -0
- package/dist/Textarea/index.d.cts +48 -0
- package/dist/Textarea/index.d.ts +48 -0
- package/dist/Textarea/index.js +100 -0
- package/dist/Textarea/index.js.map +1 -0
- package/dist/Textarea/styles.css +95 -0
- package/dist/Toast/index.cjs +184 -0
- package/dist/Toast/index.cjs.map +1 -0
- package/dist/Toast/index.d.cts +67 -0
- package/dist/Toast/index.d.ts +67 -0
- package/dist/Toast/index.js +146 -0
- package/dist/Toast/index.js.map +1 -0
- package/dist/Toast/styles.css +122 -0
- package/dist/Tooltip/index.cjs +105 -0
- package/dist/Tooltip/index.cjs.map +1 -0
- package/dist/Tooltip/index.d.cts +65 -0
- package/dist/Tooltip/index.d.ts +65 -0
- package/dist/Tooltip/index.js +67 -0
- package/dist/Tooltip/index.js.map +1 -0
- package/dist/Tooltip/styles.css +42 -0
- package/dist/index.cjs +900 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +16 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.js +850 -0
- package/dist/index.js.map +1 -0
- package/dist/styles.css +11 -0
- package/dist/tokens.css +232 -0
- package/dist/typography.css +104 -0
- package/llms.txt +369 -0
- 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
|
+
}
|