@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,103 @@
|
|
|
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/Input/index.ts
|
|
21
|
+
var Input_exports = {};
|
|
22
|
+
__export(Input_exports, {
|
|
23
|
+
Input: () => Input
|
|
24
|
+
});
|
|
25
|
+
module.exports = __toCommonJS(Input_exports);
|
|
26
|
+
|
|
27
|
+
// src/Input/Input.tsx
|
|
28
|
+
var import_styles = require("./styles.css");
|
|
29
|
+
var import_react = require("react");
|
|
30
|
+
|
|
31
|
+
// src/utils/cx.ts
|
|
32
|
+
function cx(...classes) {
|
|
33
|
+
return classes.filter(Boolean).join(" ");
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// src/Input/Input.tsx
|
|
37
|
+
var import_jsx_runtime = require("react/jsx-runtime");
|
|
38
|
+
var Input = (0, import_react.forwardRef)(function Input2({
|
|
39
|
+
size = "md",
|
|
40
|
+
label,
|
|
41
|
+
hideLabel = false,
|
|
42
|
+
description,
|
|
43
|
+
invalid = false,
|
|
44
|
+
errorMessage,
|
|
45
|
+
iconLeft,
|
|
46
|
+
iconRight,
|
|
47
|
+
block = false,
|
|
48
|
+
disabled = false,
|
|
49
|
+
className,
|
|
50
|
+
id: idProp,
|
|
51
|
+
"aria-describedby": ariaDescribedby,
|
|
52
|
+
...rest
|
|
53
|
+
}, ref) {
|
|
54
|
+
const reactId = (0, import_react.useId)();
|
|
55
|
+
const id = idProp ?? `nuri-input-${reactId}`;
|
|
56
|
+
const messageId = `${id}-message`;
|
|
57
|
+
const message = invalid && errorMessage != null ? errorMessage : description;
|
|
58
|
+
const hasMessage = message != null;
|
|
59
|
+
const describedBy = cx(hasMessage ? messageId : void 0, ariaDescribedby) || void 0;
|
|
60
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: cx("nuri-input", block && "nuri-input--block", className), children: [
|
|
61
|
+
label != null && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("label", { htmlFor: id, className: cx("nuri-input__label", hideLabel && "nuri-sr-only"), children: label }),
|
|
62
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
|
|
63
|
+
"div",
|
|
64
|
+
{
|
|
65
|
+
className: cx(
|
|
66
|
+
"nuri-input__field",
|
|
67
|
+
`nuri-input__field--${size}`,
|
|
68
|
+
invalid && "nuri-input__field--invalid",
|
|
69
|
+
disabled && "nuri-input__field--disabled"
|
|
70
|
+
),
|
|
71
|
+
children: [
|
|
72
|
+
iconLeft != null && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "nuri-input__icon", "aria-hidden": "true", children: iconLeft }),
|
|
73
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
74
|
+
"input",
|
|
75
|
+
{
|
|
76
|
+
ref,
|
|
77
|
+
id,
|
|
78
|
+
className: "nuri-input__control",
|
|
79
|
+
disabled,
|
|
80
|
+
"aria-invalid": invalid || void 0,
|
|
81
|
+
"aria-describedby": describedBy,
|
|
82
|
+
...rest
|
|
83
|
+
}
|
|
84
|
+
),
|
|
85
|
+
iconRight != null && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "nuri-input__icon", "aria-hidden": "true", children: iconRight })
|
|
86
|
+
]
|
|
87
|
+
}
|
|
88
|
+
),
|
|
89
|
+
hasMessage && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
90
|
+
"p",
|
|
91
|
+
{
|
|
92
|
+
id: messageId,
|
|
93
|
+
className: cx("nuri-input__message", invalid && "nuri-input__message--error"),
|
|
94
|
+
children: message
|
|
95
|
+
}
|
|
96
|
+
)
|
|
97
|
+
] });
|
|
98
|
+
});
|
|
99
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
100
|
+
0 && (module.exports = {
|
|
101
|
+
Input
|
|
102
|
+
});
|
|
103
|
+
//# sourceMappingURL=index.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/Input/index.ts","../../src/Input/Input.tsx","../../src/utils/cx.ts"],"sourcesContent":["export { Input } from './Input.js';\nexport type { InputProps } from './Input.js';\n","'use client';\n\nimport './styles.css';\nimport { forwardRef, useId } from 'react';\nimport type { InputHTMLAttributes, ReactNode } from 'react';\nimport { cx } from '../utils/cx.js';\n\ntype Size = 'sm' | 'md' | 'lg';\n\nexport interface InputProps extends Omit<InputHTMLAttributes<HTMLInputElement>, 'size'> {\n /**\n * 사이즈. sm 36 / md 42 / lg 48 px 높이.\n * @default 'md'\n */\n size?: Size;\n /**\n * 라벨 (필드 위). 지정 시 `<label>`로 렌더되어 자동 연결됨.\n * 라벨을 두지 않으면 `aria-label`로 접근 가능한 이름을 주세요\n * (placeholder는 이름을 대체하지 못함 — 입력 시 사라지고 대비가 낮음).\n */\n label?: ReactNode;\n /**\n * 라벨을 시각적으로만 숨김 (스크린리더에는 노출). 디자인상 라벨을 안 보이게 하되\n * 접근성 이름은 유지할 때 사용.\n * @default false\n */\n hideLabel?: boolean;\n /** 평상시 힌트 (회색, 필드 아래). `aria-describedby`로 연결됨. */\n description?: ReactNode;\n /** 에러 상태 — 보더·텍스트·배경이 에러색으로 바뀌고 `aria-invalid`가 설정됨. */\n invalid?: boolean;\n /** `invalid`일 때 표시할 에러 메시지 (빨강). 있으면 description을 대체. */\n errorMessage?: ReactNode;\n /** 좌측 어도먼트 (아이콘 등). */\n iconLeft?: ReactNode;\n /** 우측 어도먼트 (아이콘·clear·버튼 등). */\n iconRight?: ReactNode;\n /** 컨테이너 전체 너비로 확장. @default false */\n block?: boolean;\n}\n\n/**\n * 텍스트 입력 필드. 옵션으로 라벨(위)·헬퍼/에러 메시지(아래)를 포함하는 자체 완결형.\n * 상태(default/focus/invalid/disabled)는 CSS로, 값 채움(filled)은 네이티브로 처리.\n *\n * @example 기본\n * <Input placeholder=\"검색어\" aria-label=\"검색\" />\n *\n * @example 라벨 + 헬퍼 + 에러\n * <Input\n * label=\"비밀번호\"\n * description=\"8자 이상 입력하세요\"\n * invalid\n * errorMessage=\"비밀번호가 일치하지 않습니다\"\n * />\n */\nexport const Input = forwardRef<HTMLInputElement, InputProps>(function Input(\n {\n size = 'md',\n label,\n hideLabel = false,\n description,\n invalid = false,\n errorMessage,\n iconLeft,\n iconRight,\n block = false,\n disabled = false,\n className,\n id: idProp,\n 'aria-describedby': ariaDescribedby,\n ...rest\n },\n ref,\n) {\n const reactId = useId();\n const id = idProp ?? `nuri-input-${reactId}`;\n const messageId = `${id}-message`;\n\n // invalid + errorMessage → 에러 메시지 / 아니면 힌트. 시각 슬롯은 항상 한 줄.\n const message = invalid && errorMessage != null ? errorMessage : description;\n const hasMessage = message != null;\n const describedBy =\n cx(hasMessage ? messageId : undefined, ariaDescribedby as string | undefined) || undefined;\n\n return (\n <div className={cx('nuri-input', block && 'nuri-input--block', className)}>\n {label != null && (\n <label htmlFor={id} className={cx('nuri-input__label', hideLabel && 'nuri-sr-only')}>\n {label}\n </label>\n )}\n\n <div\n className={cx(\n 'nuri-input__field',\n `nuri-input__field--${size}`,\n invalid && 'nuri-input__field--invalid',\n disabled && 'nuri-input__field--disabled',\n )}\n >\n {iconLeft != null && (\n <span className=\"nuri-input__icon\" aria-hidden=\"true\">\n {iconLeft}\n </span>\n )}\n <input\n ref={ref}\n id={id}\n className=\"nuri-input__control\"\n disabled={disabled}\n aria-invalid={invalid || undefined}\n aria-describedby={describedBy}\n {...rest}\n />\n {iconRight != null && (\n <span className=\"nuri-input__icon\" aria-hidden=\"true\">\n {iconRight}\n </span>\n )}\n </div>\n\n {hasMessage && (\n <p\n id={messageId}\n className={cx('nuri-input__message', invalid && 'nuri-input__message--error')}\n >\n {message}\n </p>\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,mBAAkC;;;ACC3B,SAAS,MAAM,SAA2D;AAC/E,SAAO,QAAQ,OAAO,OAAO,EAAE,KAAK,GAAG;AACzC;;;ADkFQ;AAhCD,IAAM,YAAQ,yBAAyC,SAASA,OACrE;AAAA,EACE,OAAO;AAAA,EACP;AAAA,EACA,YAAY;AAAA,EACZ;AAAA,EACA,UAAU;AAAA,EACV;AAAA,EACA;AAAA,EACA;AAAA,EACA,QAAQ;AAAA,EACR,WAAW;AAAA,EACX;AAAA,EACA,IAAI;AAAA,EACJ,oBAAoB;AAAA,EACpB,GAAG;AACL,GACA,KACA;AACA,QAAM,cAAU,oBAAM;AACtB,QAAM,KAAK,UAAU,cAAc,OAAO;AAC1C,QAAM,YAAY,GAAG,EAAE;AAGvB,QAAM,UAAU,WAAW,gBAAgB,OAAO,eAAe;AACjE,QAAM,aAAa,WAAW;AAC9B,QAAM,cACJ,GAAG,aAAa,YAAY,QAAW,eAAqC,KAAK;AAEnF,SACE,6CAAC,SAAI,WAAW,GAAG,cAAc,SAAS,qBAAqB,SAAS,GACrE;AAAA,aAAS,QACR,4CAAC,WAAM,SAAS,IAAI,WAAW,GAAG,qBAAqB,aAAa,cAAc,GAC/E,iBACH;AAAA,IAGF;AAAA,MAAC;AAAA;AAAA,QACC,WAAW;AAAA,UACT;AAAA,UACA,sBAAsB,IAAI;AAAA,UAC1B,WAAW;AAAA,UACX,YAAY;AAAA,QACd;AAAA,QAEC;AAAA,sBAAY,QACX,4CAAC,UAAK,WAAU,oBAAmB,eAAY,QAC5C,oBACH;AAAA,UAEF;AAAA,YAAC;AAAA;AAAA,cACC;AAAA,cACA;AAAA,cACA,WAAU;AAAA,cACV;AAAA,cACA,gBAAc,WAAW;AAAA,cACzB,oBAAkB;AAAA,cACjB,GAAG;AAAA;AAAA,UACN;AAAA,UACC,aAAa,QACZ,4CAAC,UAAK,WAAU,oBAAmB,eAAY,QAC5C,qBACH;AAAA;AAAA;AAAA,IAEJ;AAAA,IAEC,cACC;AAAA,MAAC;AAAA;AAAA,QACC,IAAI;AAAA,QACJ,WAAW,GAAG,uBAAuB,WAAW,4BAA4B;AAAA,QAE3E;AAAA;AAAA,IACH;AAAA,KAEJ;AAEJ,CAAC;","names":["Input"]}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import * as react from 'react';
|
|
2
|
+
import { InputHTMLAttributes, ReactNode } from 'react';
|
|
3
|
+
|
|
4
|
+
type Size = 'sm' | 'md' | 'lg';
|
|
5
|
+
interface InputProps extends Omit<InputHTMLAttributes<HTMLInputElement>, 'size'> {
|
|
6
|
+
/**
|
|
7
|
+
* 사이즈. sm 36 / md 42 / lg 48 px 높이.
|
|
8
|
+
* @default 'md'
|
|
9
|
+
*/
|
|
10
|
+
size?: Size;
|
|
11
|
+
/**
|
|
12
|
+
* 라벨 (필드 위). 지정 시 `<label>`로 렌더되어 자동 연결됨.
|
|
13
|
+
* 라벨을 두지 않으면 `aria-label`로 접근 가능한 이름을 주세요
|
|
14
|
+
* (placeholder는 이름을 대체하지 못함 — 입력 시 사라지고 대비가 낮음).
|
|
15
|
+
*/
|
|
16
|
+
label?: ReactNode;
|
|
17
|
+
/**
|
|
18
|
+
* 라벨을 시각적으로만 숨김 (스크린리더에는 노출). 디자인상 라벨을 안 보이게 하되
|
|
19
|
+
* 접근성 이름은 유지할 때 사용.
|
|
20
|
+
* @default false
|
|
21
|
+
*/
|
|
22
|
+
hideLabel?: boolean;
|
|
23
|
+
/** 평상시 힌트 (회색, 필드 아래). `aria-describedby`로 연결됨. */
|
|
24
|
+
description?: ReactNode;
|
|
25
|
+
/** 에러 상태 — 보더·텍스트·배경이 에러색으로 바뀌고 `aria-invalid`가 설정됨. */
|
|
26
|
+
invalid?: boolean;
|
|
27
|
+
/** `invalid`일 때 표시할 에러 메시지 (빨강). 있으면 description을 대체. */
|
|
28
|
+
errorMessage?: ReactNode;
|
|
29
|
+
/** 좌측 어도먼트 (아이콘 등). */
|
|
30
|
+
iconLeft?: ReactNode;
|
|
31
|
+
/** 우측 어도먼트 (아이콘·clear·버튼 등). */
|
|
32
|
+
iconRight?: ReactNode;
|
|
33
|
+
/** 컨테이너 전체 너비로 확장. @default false */
|
|
34
|
+
block?: boolean;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* 텍스트 입력 필드. 옵션으로 라벨(위)·헬퍼/에러 메시지(아래)를 포함하는 자체 완결형.
|
|
38
|
+
* 상태(default/focus/invalid/disabled)는 CSS로, 값 채움(filled)은 네이티브로 처리.
|
|
39
|
+
*
|
|
40
|
+
* @example 기본
|
|
41
|
+
* <Input placeholder="검색어" aria-label="검색" />
|
|
42
|
+
*
|
|
43
|
+
* @example 라벨 + 헬퍼 + 에러
|
|
44
|
+
* <Input
|
|
45
|
+
* label="비밀번호"
|
|
46
|
+
* description="8자 이상 입력하세요"
|
|
47
|
+
* invalid
|
|
48
|
+
* errorMessage="비밀번호가 일치하지 않습니다"
|
|
49
|
+
* />
|
|
50
|
+
*/
|
|
51
|
+
declare const Input: react.ForwardRefExoticComponent<InputProps & react.RefAttributes<HTMLInputElement>>;
|
|
52
|
+
|
|
53
|
+
export { Input, type InputProps };
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import * as react from 'react';
|
|
2
|
+
import { InputHTMLAttributes, ReactNode } from 'react';
|
|
3
|
+
|
|
4
|
+
type Size = 'sm' | 'md' | 'lg';
|
|
5
|
+
interface InputProps extends Omit<InputHTMLAttributes<HTMLInputElement>, 'size'> {
|
|
6
|
+
/**
|
|
7
|
+
* 사이즈. sm 36 / md 42 / lg 48 px 높이.
|
|
8
|
+
* @default 'md'
|
|
9
|
+
*/
|
|
10
|
+
size?: Size;
|
|
11
|
+
/**
|
|
12
|
+
* 라벨 (필드 위). 지정 시 `<label>`로 렌더되어 자동 연결됨.
|
|
13
|
+
* 라벨을 두지 않으면 `aria-label`로 접근 가능한 이름을 주세요
|
|
14
|
+
* (placeholder는 이름을 대체하지 못함 — 입력 시 사라지고 대비가 낮음).
|
|
15
|
+
*/
|
|
16
|
+
label?: ReactNode;
|
|
17
|
+
/**
|
|
18
|
+
* 라벨을 시각적으로만 숨김 (스크린리더에는 노출). 디자인상 라벨을 안 보이게 하되
|
|
19
|
+
* 접근성 이름은 유지할 때 사용.
|
|
20
|
+
* @default false
|
|
21
|
+
*/
|
|
22
|
+
hideLabel?: boolean;
|
|
23
|
+
/** 평상시 힌트 (회색, 필드 아래). `aria-describedby`로 연결됨. */
|
|
24
|
+
description?: ReactNode;
|
|
25
|
+
/** 에러 상태 — 보더·텍스트·배경이 에러색으로 바뀌고 `aria-invalid`가 설정됨. */
|
|
26
|
+
invalid?: boolean;
|
|
27
|
+
/** `invalid`일 때 표시할 에러 메시지 (빨강). 있으면 description을 대체. */
|
|
28
|
+
errorMessage?: ReactNode;
|
|
29
|
+
/** 좌측 어도먼트 (아이콘 등). */
|
|
30
|
+
iconLeft?: ReactNode;
|
|
31
|
+
/** 우측 어도먼트 (아이콘·clear·버튼 등). */
|
|
32
|
+
iconRight?: ReactNode;
|
|
33
|
+
/** 컨테이너 전체 너비로 확장. @default false */
|
|
34
|
+
block?: boolean;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* 텍스트 입력 필드. 옵션으로 라벨(위)·헬퍼/에러 메시지(아래)를 포함하는 자체 완결형.
|
|
38
|
+
* 상태(default/focus/invalid/disabled)는 CSS로, 값 채움(filled)은 네이티브로 처리.
|
|
39
|
+
*
|
|
40
|
+
* @example 기본
|
|
41
|
+
* <Input placeholder="검색어" aria-label="검색" />
|
|
42
|
+
*
|
|
43
|
+
* @example 라벨 + 헬퍼 + 에러
|
|
44
|
+
* <Input
|
|
45
|
+
* label="비밀번호"
|
|
46
|
+
* description="8자 이상 입력하세요"
|
|
47
|
+
* invalid
|
|
48
|
+
* errorMessage="비밀번호가 일치하지 않습니다"
|
|
49
|
+
* />
|
|
50
|
+
*/
|
|
51
|
+
declare const Input: react.ForwardRefExoticComponent<InputProps & react.RefAttributes<HTMLInputElement>>;
|
|
52
|
+
|
|
53
|
+
export { Input, type InputProps };
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
// src/Input/Input.tsx
|
|
2
|
+
import "./styles.css";
|
|
3
|
+
import { forwardRef, useId } from "react";
|
|
4
|
+
|
|
5
|
+
// src/utils/cx.ts
|
|
6
|
+
function cx(...classes) {
|
|
7
|
+
return classes.filter(Boolean).join(" ");
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
// src/Input/Input.tsx
|
|
11
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
12
|
+
var Input = forwardRef(function Input2({
|
|
13
|
+
size = "md",
|
|
14
|
+
label,
|
|
15
|
+
hideLabel = false,
|
|
16
|
+
description,
|
|
17
|
+
invalid = false,
|
|
18
|
+
errorMessage,
|
|
19
|
+
iconLeft,
|
|
20
|
+
iconRight,
|
|
21
|
+
block = false,
|
|
22
|
+
disabled = false,
|
|
23
|
+
className,
|
|
24
|
+
id: idProp,
|
|
25
|
+
"aria-describedby": ariaDescribedby,
|
|
26
|
+
...rest
|
|
27
|
+
}, ref) {
|
|
28
|
+
const reactId = useId();
|
|
29
|
+
const id = idProp ?? `nuri-input-${reactId}`;
|
|
30
|
+
const messageId = `${id}-message`;
|
|
31
|
+
const message = invalid && errorMessage != null ? errorMessage : description;
|
|
32
|
+
const hasMessage = message != null;
|
|
33
|
+
const describedBy = cx(hasMessage ? messageId : void 0, ariaDescribedby) || void 0;
|
|
34
|
+
return /* @__PURE__ */ jsxs("div", { className: cx("nuri-input", block && "nuri-input--block", className), children: [
|
|
35
|
+
label != null && /* @__PURE__ */ jsx("label", { htmlFor: id, className: cx("nuri-input__label", hideLabel && "nuri-sr-only"), children: label }),
|
|
36
|
+
/* @__PURE__ */ jsxs(
|
|
37
|
+
"div",
|
|
38
|
+
{
|
|
39
|
+
className: cx(
|
|
40
|
+
"nuri-input__field",
|
|
41
|
+
`nuri-input__field--${size}`,
|
|
42
|
+
invalid && "nuri-input__field--invalid",
|
|
43
|
+
disabled && "nuri-input__field--disabled"
|
|
44
|
+
),
|
|
45
|
+
children: [
|
|
46
|
+
iconLeft != null && /* @__PURE__ */ jsx("span", { className: "nuri-input__icon", "aria-hidden": "true", children: iconLeft }),
|
|
47
|
+
/* @__PURE__ */ jsx(
|
|
48
|
+
"input",
|
|
49
|
+
{
|
|
50
|
+
ref,
|
|
51
|
+
id,
|
|
52
|
+
className: "nuri-input__control",
|
|
53
|
+
disabled,
|
|
54
|
+
"aria-invalid": invalid || void 0,
|
|
55
|
+
"aria-describedby": describedBy,
|
|
56
|
+
...rest
|
|
57
|
+
}
|
|
58
|
+
),
|
|
59
|
+
iconRight != null && /* @__PURE__ */ jsx("span", { className: "nuri-input__icon", "aria-hidden": "true", children: iconRight })
|
|
60
|
+
]
|
|
61
|
+
}
|
|
62
|
+
),
|
|
63
|
+
hasMessage && /* @__PURE__ */ jsx(
|
|
64
|
+
"p",
|
|
65
|
+
{
|
|
66
|
+
id: messageId,
|
|
67
|
+
className: cx("nuri-input__message", invalid && "nuri-input__message--error"),
|
|
68
|
+
children: message
|
|
69
|
+
}
|
|
70
|
+
)
|
|
71
|
+
] });
|
|
72
|
+
});
|
|
73
|
+
export {
|
|
74
|
+
Input
|
|
75
|
+
};
|
|
76
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/Input/Input.tsx","../../src/utils/cx.ts"],"sourcesContent":["'use client';\n\nimport './styles.css';\nimport { forwardRef, useId } from 'react';\nimport type { InputHTMLAttributes, ReactNode } from 'react';\nimport { cx } from '../utils/cx.js';\n\ntype Size = 'sm' | 'md' | 'lg';\n\nexport interface InputProps extends Omit<InputHTMLAttributes<HTMLInputElement>, 'size'> {\n /**\n * 사이즈. sm 36 / md 42 / lg 48 px 높이.\n * @default 'md'\n */\n size?: Size;\n /**\n * 라벨 (필드 위). 지정 시 `<label>`로 렌더되어 자동 연결됨.\n * 라벨을 두지 않으면 `aria-label`로 접근 가능한 이름을 주세요\n * (placeholder는 이름을 대체하지 못함 — 입력 시 사라지고 대비가 낮음).\n */\n label?: ReactNode;\n /**\n * 라벨을 시각적으로만 숨김 (스크린리더에는 노출). 디자인상 라벨을 안 보이게 하되\n * 접근성 이름은 유지할 때 사용.\n * @default false\n */\n hideLabel?: boolean;\n /** 평상시 힌트 (회색, 필드 아래). `aria-describedby`로 연결됨. */\n description?: ReactNode;\n /** 에러 상태 — 보더·텍스트·배경이 에러색으로 바뀌고 `aria-invalid`가 설정됨. */\n invalid?: boolean;\n /** `invalid`일 때 표시할 에러 메시지 (빨강). 있으면 description을 대체. */\n errorMessage?: ReactNode;\n /** 좌측 어도먼트 (아이콘 등). */\n iconLeft?: ReactNode;\n /** 우측 어도먼트 (아이콘·clear·버튼 등). */\n iconRight?: ReactNode;\n /** 컨테이너 전체 너비로 확장. @default false */\n block?: boolean;\n}\n\n/**\n * 텍스트 입력 필드. 옵션으로 라벨(위)·헬퍼/에러 메시지(아래)를 포함하는 자체 완결형.\n * 상태(default/focus/invalid/disabled)는 CSS로, 값 채움(filled)은 네이티브로 처리.\n *\n * @example 기본\n * <Input placeholder=\"검색어\" aria-label=\"검색\" />\n *\n * @example 라벨 + 헬퍼 + 에러\n * <Input\n * label=\"비밀번호\"\n * description=\"8자 이상 입력하세요\"\n * invalid\n * errorMessage=\"비밀번호가 일치하지 않습니다\"\n * />\n */\nexport const Input = forwardRef<HTMLInputElement, InputProps>(function Input(\n {\n size = 'md',\n label,\n hideLabel = false,\n description,\n invalid = false,\n errorMessage,\n iconLeft,\n iconRight,\n block = false,\n disabled = false,\n className,\n id: idProp,\n 'aria-describedby': ariaDescribedby,\n ...rest\n },\n ref,\n) {\n const reactId = useId();\n const id = idProp ?? `nuri-input-${reactId}`;\n const messageId = `${id}-message`;\n\n // invalid + errorMessage → 에러 메시지 / 아니면 힌트. 시각 슬롯은 항상 한 줄.\n const message = invalid && errorMessage != null ? errorMessage : description;\n const hasMessage = message != null;\n const describedBy =\n cx(hasMessage ? messageId : undefined, ariaDescribedby as string | undefined) || undefined;\n\n return (\n <div className={cx('nuri-input', block && 'nuri-input--block', className)}>\n {label != null && (\n <label htmlFor={id} className={cx('nuri-input__label', hideLabel && 'nuri-sr-only')}>\n {label}\n </label>\n )}\n\n <div\n className={cx(\n 'nuri-input__field',\n `nuri-input__field--${size}`,\n invalid && 'nuri-input__field--invalid',\n disabled && 'nuri-input__field--disabled',\n )}\n >\n {iconLeft != null && (\n <span className=\"nuri-input__icon\" aria-hidden=\"true\">\n {iconLeft}\n </span>\n )}\n <input\n ref={ref}\n id={id}\n className=\"nuri-input__control\"\n disabled={disabled}\n aria-invalid={invalid || undefined}\n aria-describedby={describedBy}\n {...rest}\n />\n {iconRight != null && (\n <span className=\"nuri-input__icon\" aria-hidden=\"true\">\n {iconRight}\n </span>\n )}\n </div>\n\n {hasMessage && (\n <p\n id={messageId}\n className={cx('nuri-input__message', invalid && 'nuri-input__message--error')}\n >\n {message}\n </p>\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,SAAS,YAAY,aAAa;;;ACC3B,SAAS,MAAM,SAA2D;AAC/E,SAAO,QAAQ,OAAO,OAAO,EAAE,KAAK,GAAG;AACzC;;;ADkFQ,cAKF,YALE;AAhCD,IAAM,QAAQ,WAAyC,SAASA,OACrE;AAAA,EACE,OAAO;AAAA,EACP;AAAA,EACA,YAAY;AAAA,EACZ;AAAA,EACA,UAAU;AAAA,EACV;AAAA,EACA;AAAA,EACA;AAAA,EACA,QAAQ;AAAA,EACR,WAAW;AAAA,EACX;AAAA,EACA,IAAI;AAAA,EACJ,oBAAoB;AAAA,EACpB,GAAG;AACL,GACA,KACA;AACA,QAAM,UAAU,MAAM;AACtB,QAAM,KAAK,UAAU,cAAc,OAAO;AAC1C,QAAM,YAAY,GAAG,EAAE;AAGvB,QAAM,UAAU,WAAW,gBAAgB,OAAO,eAAe;AACjE,QAAM,aAAa,WAAW;AAC9B,QAAM,cACJ,GAAG,aAAa,YAAY,QAAW,eAAqC,KAAK;AAEnF,SACE,qBAAC,SAAI,WAAW,GAAG,cAAc,SAAS,qBAAqB,SAAS,GACrE;AAAA,aAAS,QACR,oBAAC,WAAM,SAAS,IAAI,WAAW,GAAG,qBAAqB,aAAa,cAAc,GAC/E,iBACH;AAAA,IAGF;AAAA,MAAC;AAAA;AAAA,QACC,WAAW;AAAA,UACT;AAAA,UACA,sBAAsB,IAAI;AAAA,UAC1B,WAAW;AAAA,UACX,YAAY;AAAA,QACd;AAAA,QAEC;AAAA,sBAAY,QACX,oBAAC,UAAK,WAAU,oBAAmB,eAAY,QAC5C,oBACH;AAAA,UAEF;AAAA,YAAC;AAAA;AAAA,cACC;AAAA,cACA;AAAA,cACA,WAAU;AAAA,cACV;AAAA,cACA,gBAAc,WAAW;AAAA,cACzB,oBAAkB;AAAA,cACjB,GAAG;AAAA;AAAA,UACN;AAAA,UACC,aAAa,QACZ,oBAAC,UAAK,WAAU,oBAAmB,eAAY,QAC5C,qBACH;AAAA;AAAA;AAAA,IAEJ;AAAA,IAEC,cACC;AAAA,MAAC;AAAA;AAAA,QACC,IAAI;AAAA,QACJ,WAAW,GAAG,uBAAuB,WAAW,4BAA4B;AAAA,QAE3E;AAAA;AAAA,IACH;AAAA,KAEJ;AAEJ,CAAC;","names":["Input"]}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
.nuri-input {
|
|
2
|
+
display: inline-flex;
|
|
3
|
+
flex-direction: column;
|
|
4
|
+
gap: var(--nuri-input-field-gap);
|
|
5
|
+
font-family: inherit; /* 앱 폰트 상속 */
|
|
6
|
+
}
|
|
7
|
+
.nuri-input--block {
|
|
8
|
+
display: flex;
|
|
9
|
+
width: 100%;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/* 라벨 (옵션, 위) */
|
|
13
|
+
.nuri-input__label {
|
|
14
|
+
font-size: var(--nuri-input-label-size);
|
|
15
|
+
font-weight: var(--nuri-input-label-weight);
|
|
16
|
+
line-height: var(--nuri-input-line-height);
|
|
17
|
+
color: var(--nuri-input-label-color);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/* 시각적으로만 숨김 (스크린리더에는 노출) */
|
|
21
|
+
.nuri-sr-only {
|
|
22
|
+
position: absolute;
|
|
23
|
+
width: 1px;
|
|
24
|
+
height: 1px;
|
|
25
|
+
padding: 0;
|
|
26
|
+
margin: -1px;
|
|
27
|
+
overflow: hidden;
|
|
28
|
+
clip: rect(0, 0, 0, 0);
|
|
29
|
+
white-space: nowrap;
|
|
30
|
+
border: 0;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/* 필드 (보더·배경을 가진 래퍼) */
|
|
34
|
+
.nuri-input__field {
|
|
35
|
+
box-sizing: border-box;
|
|
36
|
+
display: flex;
|
|
37
|
+
align-items: center;
|
|
38
|
+
gap: var(--nuri-input-gap);
|
|
39
|
+
padding: 0 var(--nuri-input-padding-x);
|
|
40
|
+
background-color: var(--nuri-input-bg);
|
|
41
|
+
border: 1px solid var(--nuri-input-border);
|
|
42
|
+
border-radius: var(--nuri-input-radius);
|
|
43
|
+
transition:
|
|
44
|
+
border-color 120ms ease,
|
|
45
|
+
background-color 120ms ease;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/* focus — 브랜드색 (use-site 폴백: 토큰 미정의 시 브랜드 프리미티브) */
|
|
49
|
+
.nuri-input__field:focus-within:not(.nuri-input__field--invalid):not(.nuri-input__field--disabled) {
|
|
50
|
+
border-color: var(--nuri-input-border-focus, var(--nuri-color-brand-default));
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/* error */
|
|
54
|
+
.nuri-input__field--invalid {
|
|
55
|
+
background-color: var(--nuri-input-bg-error);
|
|
56
|
+
border-color: var(--nuri-input-border-error);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/* disabled */
|
|
60
|
+
.nuri-input__field--disabled {
|
|
61
|
+
background-color: var(--nuri-input-bg-disabled);
|
|
62
|
+
border-color: var(--nuri-input-border-disabled);
|
|
63
|
+
cursor: not-allowed;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/* 사이즈 (높이) */
|
|
67
|
+
.nuri-input__field--sm {
|
|
68
|
+
height: var(--nuri-input-size-sm-height);
|
|
69
|
+
}
|
|
70
|
+
.nuri-input__field--md {
|
|
71
|
+
height: var(--nuri-input-size-md-height);
|
|
72
|
+
}
|
|
73
|
+
.nuri-input__field--lg {
|
|
74
|
+
height: var(--nuri-input-size-lg-height);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/* 실제 input (테두리·배경 없음, 필드 래퍼가 담당) */
|
|
78
|
+
.nuri-input__control {
|
|
79
|
+
flex: 1 1 auto;
|
|
80
|
+
min-width: 0;
|
|
81
|
+
height: 100%;
|
|
82
|
+
padding: 0;
|
|
83
|
+
border: none;
|
|
84
|
+
background: transparent;
|
|
85
|
+
font-family: inherit;
|
|
86
|
+
font-size: var(--nuri-input-font-size);
|
|
87
|
+
font-weight: var(--nuri-input-font-weight);
|
|
88
|
+
line-height: var(--nuri-input-line-height);
|
|
89
|
+
color: var(--nuri-input-text);
|
|
90
|
+
outline: none;
|
|
91
|
+
appearance: none;
|
|
92
|
+
}
|
|
93
|
+
.nuri-input__control::placeholder {
|
|
94
|
+
color: var(--nuri-input-placeholder);
|
|
95
|
+
opacity: 1; /* Firefox 기본 opacity 제거 */
|
|
96
|
+
}
|
|
97
|
+
.nuri-input__field--invalid .nuri-input__control,
|
|
98
|
+
.nuri-input__field--invalid .nuri-input__control::placeholder {
|
|
99
|
+
color: var(--nuri-input-text-error);
|
|
100
|
+
}
|
|
101
|
+
.nuri-input__control:disabled {
|
|
102
|
+
color: var(--nuri-input-text-disabled);
|
|
103
|
+
cursor: not-allowed;
|
|
104
|
+
-webkit-text-fill-color: var(--nuri-input-text-disabled); /* Safari disabled 색 강제 */
|
|
105
|
+
}
|
|
106
|
+
.nuri-input__field--invalid .nuri-input__control:disabled,
|
|
107
|
+
.nuri-input__field--disabled .nuri-input__control::placeholder {
|
|
108
|
+
color: var(--nuri-input-text-disabled);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/* 어도먼트 (좌/우 아이콘) */
|
|
112
|
+
.nuri-input__icon {
|
|
113
|
+
display: inline-flex;
|
|
114
|
+
align-items: center;
|
|
115
|
+
justify-content: center;
|
|
116
|
+
flex-shrink: 0;
|
|
117
|
+
width: var(--nuri-input-icon-size);
|
|
118
|
+
height: var(--nuri-input-icon-size);
|
|
119
|
+
color: var(--nuri-input-icon-color);
|
|
120
|
+
}
|
|
121
|
+
.nuri-input__icon > svg {
|
|
122
|
+
width: 100%;
|
|
123
|
+
height: 100%;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/* 메시지 (헬퍼/에러, 아래) */
|
|
127
|
+
.nuri-input__message {
|
|
128
|
+
margin: 0;
|
|
129
|
+
font-size: var(--nuri-input-desc-size);
|
|
130
|
+
line-height: var(--nuri-input-line-height);
|
|
131
|
+
color: var(--nuri-input-desc-color);
|
|
132
|
+
}
|
|
133
|
+
.nuri-input__message--error {
|
|
134
|
+
color: var(--nuri-input-text-error);
|
|
135
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
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/Modal/index.ts
|
|
31
|
+
var Modal_exports = {};
|
|
32
|
+
__export(Modal_exports, {
|
|
33
|
+
Modal: () => Modal
|
|
34
|
+
});
|
|
35
|
+
module.exports = __toCommonJS(Modal_exports);
|
|
36
|
+
|
|
37
|
+
// src/Modal/Modal.tsx
|
|
38
|
+
var import_styles = require("./styles.css");
|
|
39
|
+
var Dialog = __toESM(require("@radix-ui/react-dialog"), 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/Modal/Modal.tsx
|
|
48
|
+
var import_jsx_runtime = require("react/jsx-runtime");
|
|
49
|
+
var Root2 = (props) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Dialog.Root, { ...props });
|
|
50
|
+
Root2.displayName = "Modal";
|
|
51
|
+
var Trigger2 = Dialog.Trigger;
|
|
52
|
+
var Content2 = (0, import_react.forwardRef)(
|
|
53
|
+
function ModalContent({ placement = "center", size = "md", overlay = true, container, className, children, ...rest }, ref) {
|
|
54
|
+
const classes = cx("nuri-modal", `nuri-modal--${placement}`, `nuri-modal--${size}`, className);
|
|
55
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Dialog.Portal, { container, children: [
|
|
56
|
+
overlay && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Dialog.Overlay, { className: "nuri-modal__overlay" }),
|
|
57
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(Dialog.Content, { ref, className: classes, ...rest, children })
|
|
58
|
+
] });
|
|
59
|
+
}
|
|
60
|
+
);
|
|
61
|
+
var Header = (0, import_react.forwardRef)(function ModalHeader({ className, ...rest }, ref) {
|
|
62
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { ref, className: cx("nuri-modal__header", className), ...rest });
|
|
63
|
+
});
|
|
64
|
+
var Title2 = (0, import_react.forwardRef)(function ModalTitle({ visuallyHidden = false, className, ...rest }, ref) {
|
|
65
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
66
|
+
Dialog.Title,
|
|
67
|
+
{
|
|
68
|
+
ref,
|
|
69
|
+
className: cx("nuri-modal__title", visuallyHidden && "nuri-sr-only", className),
|
|
70
|
+
...rest
|
|
71
|
+
}
|
|
72
|
+
);
|
|
73
|
+
});
|
|
74
|
+
var Description2 = (0, import_react.forwardRef)(function ModalDescription({ className, ...rest }, ref) {
|
|
75
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Dialog.Description, { ref, className: cx("nuri-modal__description", className), ...rest });
|
|
76
|
+
});
|
|
77
|
+
var Body = (0, import_react.forwardRef)(function ModalBody({ className, ...rest }, ref) {
|
|
78
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { ref, className: cx("nuri-modal__body", className), ...rest });
|
|
79
|
+
});
|
|
80
|
+
var Footer = (0, import_react.forwardRef)(function ModalFooter({ align = "end", className, ...rest }, ref) {
|
|
81
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
82
|
+
"div",
|
|
83
|
+
{
|
|
84
|
+
ref,
|
|
85
|
+
className: cx("nuri-modal__footer", `nuri-modal__footer--${align}`, className),
|
|
86
|
+
...rest
|
|
87
|
+
}
|
|
88
|
+
);
|
|
89
|
+
});
|
|
90
|
+
var Close2 = Dialog.Close;
|
|
91
|
+
var CloseIcon = () => /* @__PURE__ */ (0, import_jsx_runtime.jsx)("svg", { width: "20", height: "20", viewBox: "0 0 20 20", fill: "none", "aria-hidden": "true", children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("path", { d: "M5 5l10 10M15 5L5 15", stroke: "currentColor", strokeWidth: "1.5", strokeLinecap: "round" }) });
|
|
92
|
+
var CloseButton = (0, import_react.forwardRef)(function ModalCloseButton({ className, "aria-label": ariaLabel = "\uB2EB\uAE30", children, ...rest }, ref) {
|
|
93
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Dialog.Close, { asChild: true, children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
94
|
+
"button",
|
|
95
|
+
{
|
|
96
|
+
ref,
|
|
97
|
+
type: "button",
|
|
98
|
+
"aria-label": ariaLabel,
|
|
99
|
+
className: cx("nuri-modal__close", className),
|
|
100
|
+
...rest,
|
|
101
|
+
children: children ?? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(CloseIcon, {})
|
|
102
|
+
}
|
|
103
|
+
) });
|
|
104
|
+
});
|
|
105
|
+
var Modal = Object.assign(Root2, {
|
|
106
|
+
Trigger: Trigger2,
|
|
107
|
+
Content: Content2,
|
|
108
|
+
Header,
|
|
109
|
+
Title: Title2,
|
|
110
|
+
Description: Description2,
|
|
111
|
+
Body,
|
|
112
|
+
Footer,
|
|
113
|
+
Close: Close2,
|
|
114
|
+
CloseButton
|
|
115
|
+
});
|
|
116
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
117
|
+
0 && (module.exports = {
|
|
118
|
+
Modal
|
|
119
|
+
});
|
|
120
|
+
//# sourceMappingURL=index.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/Modal/index.ts","../../src/Modal/Modal.tsx","../../src/utils/cx.ts"],"sourcesContent":["export { Modal } from './Modal.js';\nexport type { ModalContentProps, ModalFooterProps, ModalTitleProps } from './Modal.js';\n","'use client';\n\nimport './styles.css';\nimport * as Dialog from '@radix-ui/react-dialog';\nimport { forwardRef } from 'react';\nimport type { ComponentPropsWithoutRef, ElementRef, ReactNode } from 'react';\nimport { cx } from '../utils/cx.js';\n\ntype Placement = 'center' | 'bottom';\ntype Size = 'sm' | 'md' | 'lg';\n\n/**\n * 모달 루트. open/onOpenChange로 제어하거나, Modal.Trigger와 함께 비제어로도 사용.\n * Radix Dialog 기반 — 포커스 트랩·Esc·스크롤 락·포커스 복귀·aria 연결을 자동 제공.\n *\n * @example 제어형\n * <Modal open={open} onOpenChange={setOpen}>\n * <Modal.Content>...</Modal.Content>\n * </Modal>\n *\n * @example 비제어 (트리거)\n * <Modal>\n * <Modal.Trigger asChild><Button>열기</Button></Modal.Trigger>\n * <Modal.Content>...</Modal.Content>\n * </Modal>\n */\nconst Root = (props: ComponentPropsWithoutRef<typeof Dialog.Root>) => <Dialog.Root {...props} />;\nRoot.displayName = 'Modal';\n\n/** 모달을 여는 트리거. `asChild`로 임의 엘리먼트(Button 등)에 위임. */\nconst Trigger = Dialog.Trigger;\n\nexport interface ModalContentProps extends ComponentPropsWithoutRef<typeof Dialog.Content> {\n /**\n * 표시 위치.\n * - `center`: 화면 중앙 다이얼로그 (기본)\n * - `bottom`: 하단 바텀시트 (모바일에서 아래에서 올라옴)\n * @default 'center'\n */\n placement?: Placement;\n /**\n * 최대 너비. **`placement=\"center\"`에서만 적용** (bottom 바텀시트는 항상 풀 width).\n * @default 'md'\n */\n size?: Size;\n /** 백드롭(오버레이) 표시 여부. @default true */\n overlay?: boolean;\n /** 포털 렌더 대상. 기본은 document.body. 다른 요소에 렌더하려면 지정. */\n container?: HTMLElement | null;\n}\n\n/**\n * 오버레이 + 포털 + 다이얼로그 패널을 한 번에 렌더.\n * 내부에 Modal.Header / Modal.Body / Modal.Footer를 자유롭게 합성.\n */\nconst Content = forwardRef<ElementRef<typeof Dialog.Content>, ModalContentProps>(\n function ModalContent(\n { placement = 'center', size = 'md', overlay = true, container, className, children, ...rest },\n ref,\n ) {\n const classes = cx('nuri-modal', `nuri-modal--${placement}`, `nuri-modal--${size}`, className);\n\n return (\n <Dialog.Portal container={container}>\n {overlay && <Dialog.Overlay className=\"nuri-modal__overlay\" />}\n <Dialog.Content ref={ref} className={classes} {...rest}>\n {children}\n </Dialog.Content>\n </Dialog.Portal>\n );\n },\n);\n\n/** 헤더 영역 (Title/Description/CloseButton을 담는 레이아웃 슬롯). */\nconst Header = forwardRef<HTMLDivElement, ComponentPropsWithoutRef<'div'>>(function ModalHeader(\n { className, ...rest },\n ref,\n) {\n return <div ref={ref} className={cx('nuri-modal__header', className)} {...rest} />;\n});\n\nexport interface ModalTitleProps extends ComponentPropsWithoutRef<typeof Dialog.Title> {\n /**\n * 시각적으로는 숨기되 스크린리더에는 노출. 시각적 제목이 없는 모달도\n * 접근 가능한 이름을 가져야 하므로(Radix Dialog는 Title 필수), 이 옵션으로 처리.\n * @example <Modal.Title visuallyHidden>이미지 미리보기</Modal.Title>\n */\n visuallyHidden?: boolean;\n}\n\n/** 모달 제목. 렌더 시 자동으로 `aria-labelledby`로 연결됨 (Radix Dialog.Title). */\nconst Title = forwardRef<ElementRef<typeof Dialog.Title>, ModalTitleProps>(function ModalTitle(\n { visuallyHidden = false, className, ...rest },\n ref,\n) {\n return (\n <Dialog.Title\n ref={ref}\n className={cx('nuri-modal__title', visuallyHidden && 'nuri-sr-only', className)}\n {...rest}\n />\n );\n});\n\n/** 모달 설명. 렌더 시 자동으로 `aria-describedby`로 연결됨 (Radix Dialog.Description). */\nconst Description = forwardRef<\n ElementRef<typeof Dialog.Description>,\n ComponentPropsWithoutRef<typeof Dialog.Description>\n>(function ModalDescription({ className, ...rest }, ref) {\n return (\n <Dialog.Description ref={ref} className={cx('nuri-modal__description', className)} {...rest} />\n );\n});\n\n/** 본문 영역 (스크롤 가능). */\nconst Body = forwardRef<HTMLDivElement, ComponentPropsWithoutRef<'div'>>(function ModalBody(\n { className, ...rest },\n ref,\n) {\n return <div ref={ref} className={cx('nuri-modal__body', className)} {...rest} />;\n});\n\nexport interface ModalFooterProps extends ComponentPropsWithoutRef<'div'> {\n /**\n * 액션 버튼 정렬.\n * - `end`: 우측 (기본, 데스크탑 관례)\n * - `start`: 좌측\n * - `center`: 가운데\n * - `between`: 양끝 분산 (예: 좌측 위험 액션 + 우측 확인)\n * - `full`: 버튼들이 가로를 균등하게 채움 (모바일 전체너비)\n * @default 'end'\n */\n align?: 'start' | 'center' | 'end' | 'between' | 'full';\n}\n\n/** 푸터 영역 (액션 버튼 배치). 정렬은 `align` prop으로 제어. */\nconst Footer = forwardRef<HTMLDivElement, ModalFooterProps>(function ModalFooter(\n { align = 'end', className, ...rest },\n ref,\n) {\n return (\n <div\n ref={ref}\n className={cx('nuri-modal__footer', `nuri-modal__footer--${align}`, className)}\n {...rest}\n />\n );\n});\n\n/**\n * 모달을 닫는 액션. `asChild`로 임의 엘리먼트에 위임.\n * @example <Modal.Close asChild><Button variant=\"outline\">취소</Button></Modal.Close>\n */\nconst Close = Dialog.Close;\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/** 우상단 X 닫기 버튼 (미리 스타일링됨). 기본 `aria-label=\"닫기\"`. */\nconst CloseButton = forwardRef<\n HTMLButtonElement,\n Omit<ComponentPropsWithoutRef<'button'>, 'children'> & { children?: ReactNode }\n>(function ModalCloseButton(\n { className, 'aria-label': ariaLabel = '닫기', children, ...rest },\n ref,\n) {\n return (\n <Dialog.Close asChild>\n <button\n ref={ref}\n type=\"button\"\n aria-label={ariaLabel}\n className={cx('nuri-modal__close', className)}\n {...rest}\n >\n {children ?? <CloseIcon />}\n </button>\n </Dialog.Close>\n );\n});\n\nexport const Modal = Object.assign(Root, {\n Trigger,\n Content,\n Header,\n Title,\n Description,\n Body,\n Footer,\n Close,\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"],"mappings":"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","names":["Root","Trigger","Content","Title","Description","Close"]}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import * as react from 'react';
|
|
2
|
+
import { ComponentPropsWithoutRef, ReactNode } from 'react';
|
|
3
|
+
import * as Dialog from '@radix-ui/react-dialog';
|
|
4
|
+
|
|
5
|
+
type Placement = 'center' | 'bottom';
|
|
6
|
+
type Size = 'sm' | 'md' | 'lg';
|
|
7
|
+
interface ModalContentProps extends ComponentPropsWithoutRef<typeof Dialog.Content> {
|
|
8
|
+
/**
|
|
9
|
+
* 표시 위치.
|
|
10
|
+
* - `center`: 화면 중앙 다이얼로그 (기본)
|
|
11
|
+
* - `bottom`: 하단 바텀시트 (모바일에서 아래에서 올라옴)
|
|
12
|
+
* @default 'center'
|
|
13
|
+
*/
|
|
14
|
+
placement?: Placement;
|
|
15
|
+
/**
|
|
16
|
+
* 최대 너비. **`placement="center"`에서만 적용** (bottom 바텀시트는 항상 풀 width).
|
|
17
|
+
* @default 'md'
|
|
18
|
+
*/
|
|
19
|
+
size?: Size;
|
|
20
|
+
/** 백드롭(오버레이) 표시 여부. @default true */
|
|
21
|
+
overlay?: boolean;
|
|
22
|
+
/** 포털 렌더 대상. 기본은 document.body. 다른 요소에 렌더하려면 지정. */
|
|
23
|
+
container?: HTMLElement | null;
|
|
24
|
+
}
|
|
25
|
+
interface ModalTitleProps extends ComponentPropsWithoutRef<typeof Dialog.Title> {
|
|
26
|
+
/**
|
|
27
|
+
* 시각적으로는 숨기되 스크린리더에는 노출. 시각적 제목이 없는 모달도
|
|
28
|
+
* 접근 가능한 이름을 가져야 하므로(Radix Dialog는 Title 필수), 이 옵션으로 처리.
|
|
29
|
+
* @example <Modal.Title visuallyHidden>이미지 미리보기</Modal.Title>
|
|
30
|
+
*/
|
|
31
|
+
visuallyHidden?: boolean;
|
|
32
|
+
}
|
|
33
|
+
interface ModalFooterProps extends ComponentPropsWithoutRef<'div'> {
|
|
34
|
+
/**
|
|
35
|
+
* 액션 버튼 정렬.
|
|
36
|
+
* - `end`: 우측 (기본, 데스크탑 관례)
|
|
37
|
+
* - `start`: 좌측
|
|
38
|
+
* - `center`: 가운데
|
|
39
|
+
* - `between`: 양끝 분산 (예: 좌측 위험 액션 + 우측 확인)
|
|
40
|
+
* - `full`: 버튼들이 가로를 균등하게 채움 (모바일 전체너비)
|
|
41
|
+
* @default 'end'
|
|
42
|
+
*/
|
|
43
|
+
align?: 'start' | 'center' | 'end' | 'between' | 'full';
|
|
44
|
+
}
|
|
45
|
+
declare const Modal: {
|
|
46
|
+
(props: ComponentPropsWithoutRef<typeof Dialog.Root>): react.JSX.Element;
|
|
47
|
+
displayName: string;
|
|
48
|
+
} & {
|
|
49
|
+
Trigger: react.ForwardRefExoticComponent<Dialog.DialogTriggerProps & react.RefAttributes<HTMLButtonElement>>;
|
|
50
|
+
Content: react.ForwardRefExoticComponent<ModalContentProps & react.RefAttributes<HTMLDivElement>>;
|
|
51
|
+
Header: react.ForwardRefExoticComponent<Omit<react.DetailedHTMLProps<react.HTMLAttributes<HTMLDivElement>, HTMLDivElement>, "ref"> & react.RefAttributes<HTMLDivElement>>;
|
|
52
|
+
Title: react.ForwardRefExoticComponent<ModalTitleProps & react.RefAttributes<HTMLHeadingElement>>;
|
|
53
|
+
Description: react.ForwardRefExoticComponent<Omit<Dialog.DialogDescriptionProps & react.RefAttributes<HTMLParagraphElement>, "ref"> & react.RefAttributes<HTMLParagraphElement>>;
|
|
54
|
+
Body: react.ForwardRefExoticComponent<Omit<react.DetailedHTMLProps<react.HTMLAttributes<HTMLDivElement>, HTMLDivElement>, "ref"> & react.RefAttributes<HTMLDivElement>>;
|
|
55
|
+
Footer: react.ForwardRefExoticComponent<ModalFooterProps & react.RefAttributes<HTMLDivElement>>;
|
|
56
|
+
Close: react.ForwardRefExoticComponent<Dialog.DialogCloseProps & react.RefAttributes<HTMLButtonElement>>;
|
|
57
|
+
CloseButton: react.ForwardRefExoticComponent<Omit<Omit<react.DetailedHTMLProps<react.ButtonHTMLAttributes<HTMLButtonElement>, HTMLButtonElement>, "ref">, "children"> & {
|
|
58
|
+
children?: ReactNode;
|
|
59
|
+
} & react.RefAttributes<HTMLButtonElement>>;
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
export { Modal, type ModalContentProps, type ModalFooterProps, type ModalTitleProps };
|