@ncds/ui-admin 1.1.3 → 1.2.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/dist/cjs/index.js CHANGED
@@ -310,4 +310,15 @@ Object.keys(_components).forEach(function (key) {
310
310
  return _components[key];
311
311
  }
312
312
  });
313
+ });
314
+ var _hooks = require("./src/hooks");
315
+ Object.keys(_hooks).forEach(function (key) {
316
+ if (key === "default" || key === "__esModule") return;
317
+ if (key in exports && exports[key] === _hooks[key]) return;
318
+ Object.defineProperty(exports, key, {
319
+ enumerable: true,
320
+ get: function () {
321
+ return _hooks[key];
322
+ }
323
+ });
313
324
  });
@@ -0,0 +1,222 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.FileInputErrorType = exports.FileInput = void 0;
7
+ var _jsxRuntime = require("react/jsx-runtime");
8
+ var _react = require("react");
9
+ var _classnames = _interopRequireDefault(require("classnames"));
10
+ var _uiAdminIcon = _interopRequireDefault(require("@ncds/ui-admin-icon"));
11
+ var _Tag = require("../tag/Tag");
12
+ var _button = require("../button");
13
+ var _shared = require("../shared");
14
+ function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
15
+ var __assign = void 0 && (void 0).__assign || function () {
16
+ __assign = Object.assign || function (t) {
17
+ for (var s, i = 1, n = arguments.length; i < n; i++) {
18
+ s = arguments[i];
19
+ for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) t[p] = s[p];
20
+ }
21
+ return t;
22
+ };
23
+ return __assign.apply(this, arguments);
24
+ };
25
+ var __rest = void 0 && (void 0).__rest || function (s, e) {
26
+ var t = {};
27
+ for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0) t[p] = s[p];
28
+ if (s != null && typeof Object.getOwnPropertySymbols === "function") for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {
29
+ if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i])) t[p[i]] = s[p[i]];
30
+ }
31
+ return t;
32
+ };
33
+ var __spreadArray = void 0 && (void 0).__spreadArray || function (to, from, pack) {
34
+ if (pack || arguments.length === 2) for (var i = 0, l = from.length, ar; i < l; i++) {
35
+ if (ar || !(i in from)) {
36
+ if (!ar) ar = Array.prototype.slice.call(from, 0, i);
37
+ ar[i] = from[i];
38
+ }
39
+ }
40
+ return to.concat(ar || Array.prototype.slice.call(from));
41
+ };
42
+ var FileInputErrorType;
43
+ (function (FileInputErrorType) {
44
+ FileInputErrorType["ALREADY_UPLOADED"] = "ALREADY_UPLOADED";
45
+ FileInputErrorType["EXCEED_MAX_FILE_SIZE"] = "EXCEED_MAX_FILE_SIZE";
46
+ FileInputErrorType["EXCEED_MAX_FILE_COUNT"] = "EXCEED_MAX_FILE_COUNT";
47
+ })(FileInputErrorType || (exports.FileInputErrorType = FileInputErrorType = {}));
48
+ var FileInput = exports.FileInput = /*#__PURE__*/(0, _react.forwardRef)(function (_a, ref) {
49
+ var _b = _a.size,
50
+ size = _b === void 0 ? 'xs' : _b,
51
+ accept = _a.accept,
52
+ _c = _a.multiple,
53
+ multiple = _c === void 0 ? false : _c,
54
+ maxFileSize = _a.maxFileSize,
55
+ maxFileCount = _a.maxFileCount,
56
+ value = _a.value,
57
+ onChange = _a.onChange,
58
+ onFileSelect = _a.onFileSelect,
59
+ onFail = _a.onFail,
60
+ _d = _a.buttonLabel,
61
+ buttonLabel = _d === void 0 ? '파일 찾기' : _d,
62
+ disabled = _a.disabled,
63
+ label = _a.label,
64
+ hintItems = _a.hintItems,
65
+ validation = _a.validation,
66
+ destructive = _a.destructive,
67
+ isRequired = _a.isRequired,
68
+ showHelpIcon = _a.showHelpIcon,
69
+ hintText = _a.hintText,
70
+ props = __rest(_a, ["size", "accept", "multiple", "maxFileSize", "maxFileCount", "value", "onChange", "onFileSelect", "onFail", "buttonLabel", "disabled", "label", "hintItems", "validation", "destructive", "isRequired", "showHelpIcon", "hintText"]);
71
+ var fileInputRef = (0, _react.useRef)(null);
72
+ var _e = (0, _react.useState)([]),
73
+ internalFiles = _e[0],
74
+ setInternalFiles = _e[1];
75
+ // Determine if component is controlled or uncontrolled
76
+ var isControlled = value !== undefined;
77
+ var files = isControlled ? value : internalFiles;
78
+ // Sync internal state with controlled value
79
+ (0, _react.useEffect)(function () {
80
+ if (isControlled && value) {
81
+ setInternalFiles(value);
82
+ }
83
+ }, [isControlled, value]);
84
+ var updateFiles = function (newFiles) {
85
+ if (isControlled) {
86
+ onChange === null || onChange === void 0 ? void 0 : onChange(newFiles);
87
+ } else {
88
+ setInternalFiles(newFiles);
89
+ onFileSelect === null || onFileSelect === void 0 ? void 0 : onFileSelect(newFiles);
90
+ }
91
+ };
92
+ var handleFileChange = function (event) {
93
+ var selectedFiles = event.target.files;
94
+ if (!selectedFiles || selectedFiles.length === 0) return;
95
+ var _a = validateFiles(Array.from(selectedFiles)),
96
+ validFiles = _a.validFiles,
97
+ invalidFiles = _a.invalidFiles;
98
+ var nextFiles = __spreadArray(__spreadArray([], files, true), validFiles, true);
99
+ updateFiles(nextFiles);
100
+ if (onFail && invalidFiles.length > 0) {
101
+ onFail(invalidFiles);
102
+ }
103
+ };
104
+ var validateFiles = function (fileList) {
105
+ var validFiles = [];
106
+ var invalidFiles = [];
107
+ var _loop_1 = function (file) {
108
+ if (files.some(function (f) {
109
+ return f.name === file.name && f.size === file.size;
110
+ })) {
111
+ invalidFiles.push(__assign(__assign({}, file), {
112
+ errorType: FileInputErrorType.ALREADY_UPLOADED
113
+ }));
114
+ return "continue";
115
+ }
116
+ if (!!maxFileSize && file.size > maxFileSize) {
117
+ invalidFiles.push(__assign(__assign({}, file), {
118
+ errorType: FileInputErrorType.EXCEED_MAX_FILE_SIZE
119
+ }));
120
+ return "continue";
121
+ }
122
+ if (!!maxFileCount && files.length + validFiles.length >= maxFileCount) {
123
+ invalidFiles.push(__assign(__assign({}, file), {
124
+ errorType: FileInputErrorType.EXCEED_MAX_FILE_COUNT
125
+ }));
126
+ return "continue";
127
+ }
128
+ validFiles.push(file);
129
+ };
130
+ for (var _i = 0, fileList_1 = fileList; _i < fileList_1.length; _i++) {
131
+ var file = fileList_1[_i];
132
+ _loop_1(file);
133
+ }
134
+ return {
135
+ validFiles: validFiles,
136
+ invalidFiles: invalidFiles
137
+ };
138
+ };
139
+ var handleBrowseClick = function () {
140
+ if (fileInputRef.current && !disabled) {
141
+ fileInputRef.current.click();
142
+ }
143
+ };
144
+ var handleRemoveFile = function (index) {
145
+ var newFiles = __spreadArray([], files, true);
146
+ newFiles.splice(index, 1);
147
+ updateFiles(newFiles);
148
+ };
149
+ var renderFileTagList = function () {
150
+ if (files.length === 0) return null;
151
+ return (0, _jsxRuntime.jsx)("div", __assign({
152
+ className: "ncua-file-input__file-tags"
153
+ }, {
154
+ children: files.map(function (file, index) {
155
+ return (0, _jsxRuntime.jsx)(_Tag.Tag, {
156
+ text: file.name,
157
+ size: size === 'xs' ? 'sm' : 'md',
158
+ close: true,
159
+ onButtonClick: function () {
160
+ return handleRemoveFile(index);
161
+ }
162
+ }, "".concat(file.name, "-").concat(index));
163
+ })
164
+ }));
165
+ };
166
+ var renderHintList = function () {
167
+ if (!hintItems || hintItems.length === 0) return null;
168
+ return (0, _jsxRuntime.jsx)("ul", __assign({
169
+ className: "ncua-file-input__hint-list"
170
+ }, {
171
+ children: hintItems.map(function (hint, index) {
172
+ return (0, _jsxRuntime.jsx)("li", __assign({
173
+ className: "ncua-file-input__hint-item"
174
+ }, {
175
+ children: hint
176
+ }), index);
177
+ })
178
+ }));
179
+ };
180
+ return (0, _jsxRuntime.jsxs)("div", __assign({
181
+ className: (0, _classnames.default)('ncua-file-input', "ncua-file-input--".concat(size))
182
+ }, {
183
+ children: [(0, _jsxRuntime.jsx)("input", __assign({
184
+ hidden: true,
185
+ ref: fileInputRef,
186
+ type: "file",
187
+ accept: accept,
188
+ multiple: multiple,
189
+ onChange: handleFileChange,
190
+ tabIndex: -1,
191
+ "aria-hidden": "true"
192
+ }, props)), (0, _jsxRuntime.jsxs)("div", __assign({
193
+ className: "ncua-file-input__input-container"
194
+ }, {
195
+ children: [(0, _jsxRuntime.jsxs)("div", __assign({
196
+ className: "ncua-file-input__label"
197
+ }, {
198
+ children: [(0, _jsxRuntime.jsx)(_shared.Label, __assign({
199
+ isRequired: isRequired
200
+ }, {
201
+ children: label
202
+ })), showHelpIcon && (0, _jsxRuntime.jsx)(_uiAdminIcon.default, {
203
+ className: "ncua-input__help-icon",
204
+ name: "help-circle"
205
+ })]
206
+ })), (0, _jsxRuntime.jsx)(_button.Button, {
207
+ size: size,
208
+ onClick: handleBrowseClick,
209
+ disabled: disabled,
210
+ leadingIcon: {
211
+ type: 'icon',
212
+ icon: 'upload-cloud-02'
213
+ },
214
+ label: buttonLabel
215
+ }), hintText && (0, _jsxRuntime.jsx)(_shared.HintText, __assign({
216
+ destructive: destructive
217
+ }, {
218
+ children: hintText
219
+ }))]
220
+ })), renderHintList(), renderFileTagList()]
221
+ }));
222
+ });
@@ -5,10 +5,11 @@ Object.defineProperty(exports, "__esModule", {
5
5
  });
6
6
  exports.PasswordInput = void 0;
7
7
  var _jsxRuntime = require("react/jsx-runtime");
8
- var _uiAdminIcon = require("@ncds/ui-admin-icon");
9
- var _classnames = _interopRequireDefault(require("classnames"));
10
8
  var _react = require("react");
11
9
  var _InputBase = require("./InputBase");
10
+ var _uiAdminIcon = require("@ncds/ui-admin-icon");
11
+ var _classnames = _interopRequireDefault(require("classnames"));
12
+ var _hooks = require("../../hooks");
12
13
  function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
13
14
  var __assign = void 0 && (void 0).__assign || function () {
14
15
  __assign = Object.assign || function (t) {
@@ -36,13 +37,25 @@ var PasswordInput = exports.PasswordInput = /*#__PURE__*/(0, _react.forwardRef)(
36
37
  var _b = _a.size,
37
38
  size = _b === void 0 ? 'xs' : _b,
38
39
  props = __rest(_a, ["size"]);
39
- var inputRef = (0, _react.useRef)(null);
40
40
  var _c = (0, _react.useState)(false),
41
41
  isVisible = _c[0],
42
42
  setIsVisible = _c[1];
43
43
  var _d = (0, _react.useState)(false),
44
44
  hasContent = _d[0],
45
45
  setHasContent = _d[1];
46
+ var callbackRef = (0, _hooks.useCallbackRef)(function (node) {
47
+ if (node) {
48
+ setHasContent(!!node.value);
49
+ var handleInput_1 = function () {
50
+ setHasContent(!!node.value);
51
+ };
52
+ node.addEventListener('input', handleInput_1);
53
+ return function () {
54
+ node.removeEventListener('input', handleInput_1);
55
+ };
56
+ }
57
+ });
58
+ var mergedRef = (0, _hooks.useMergeRefs)([ref, callbackRef]);
46
59
  var svgProps = {
47
60
  width: svgSize[size],
48
61
  height: svgSize[size]
@@ -50,29 +63,8 @@ var PasswordInput = exports.PasswordInput = /*#__PURE__*/(0, _react.forwardRef)(
50
63
  var handleVisibilityChange = function () {
51
64
  setIsVisible(!isVisible);
52
65
  };
53
- (0, _react.useEffect)(function () {
54
- if (inputRef.current) {
55
- setHasContent(!!inputRef.current.value);
56
- var handleInput_1 = function () {
57
- var _a;
58
- setHasContent(!!((_a = inputRef.current) === null || _a === void 0 ? void 0 : _a.value));
59
- };
60
- inputRef.current.addEventListener('input', handleInput_1);
61
- return function () {
62
- var _a;
63
- (_a = inputRef.current) === null || _a === void 0 ? void 0 : _a.removeEventListener('input', handleInput_1);
64
- };
65
- }
66
- }, []);
67
66
  return (0, _jsxRuntime.jsx)(_InputBase.InputBase, __assign({
68
- ref: function (node) {
69
- if (typeof ref === 'function') {
70
- ref(node);
71
- } else if (ref) {
72
- ref.current = node;
73
- }
74
- inputRef.current = node;
75
- },
67
+ ref: mergedRef,
76
68
  size: size,
77
69
  type: isVisible ? 'text' : 'password',
78
70
  leadingElement: {
@@ -35,4 +35,15 @@ Object.keys(_Textarea).forEach(function (key) {
35
35
  return _Textarea[key];
36
36
  }
37
37
  });
38
+ });
39
+ var _FileInput = require("./FileInput");
40
+ Object.keys(_FileInput).forEach(function (key) {
41
+ if (key === "default" || key === "__esModule") return;
42
+ if (key in exports && exports[key] === _FileInput[key]) return;
43
+ Object.defineProperty(exports, key, {
44
+ enumerable: true,
45
+ get: function () {
46
+ return _FileInput[key];
47
+ }
48
+ });
38
49
  });
@@ -0,0 +1,26 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ Object.defineProperty(exports, "useCallbackRef", {
7
+ enumerable: true,
8
+ get: function () {
9
+ return _useCallbackRef.useCallbackRef;
10
+ }
11
+ });
12
+ Object.defineProperty(exports, "useMediaQuery", {
13
+ enumerable: true,
14
+ get: function () {
15
+ return _useMediaQuery.useMediaQuery;
16
+ }
17
+ });
18
+ Object.defineProperty(exports, "useMergeRefs", {
19
+ enumerable: true,
20
+ get: function () {
21
+ return _useMergeRefs.useMergeRefs;
22
+ }
23
+ });
24
+ var _useMediaQuery = require("./useMediaQuery");
25
+ var _useMergeRefs = require("./useMergeRefs");
26
+ var _useCallbackRef = require("./useCallbackRef");
@@ -0,0 +1,49 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.useCallbackRef = useCallbackRef;
7
+ var _react = require("react");
8
+ /**
9
+ * A callback ref hook that supports cleanup functions.
10
+ * This is useful when you need to perform setup/cleanup operations when the ref changes.
11
+ *
12
+ * @param callback - Function that receives the node and optionally returns a cleanup function
13
+ * @returns A callback ref
14
+ *
15
+ * @example
16
+ * ```tsx
17
+ * const MyComponent = () => {
18
+ * const ref = useCallbackRef<HTMLInputElement>((node) => {
19
+ * if (node) {
20
+ * const handleInput = () => console.log('input changed');
21
+ * node.addEventListener('input', handleInput);
22
+ *
23
+ * // Return cleanup function
24
+ * return () => {
25
+ * node.removeEventListener('input', handleInput);
26
+ * };
27
+ * }
28
+ * });
29
+ *
30
+ * return <input ref={ref} />;
31
+ * };
32
+ * ```
33
+ */
34
+ function useCallbackRef(callback) {
35
+ var cleanupRef = (0, _react.useRef)();
36
+ return (0, _react.useCallback)(function (node) {
37
+ // Cleanup previous ref
38
+ if (cleanupRef.current) {
39
+ cleanupRef.current();
40
+ cleanupRef.current = undefined;
41
+ }
42
+ // Set up new ref
43
+ if (node) {
44
+ cleanupRef.current = callback(node);
45
+ } else {
46
+ callback(null);
47
+ }
48
+ }, [callback]);
49
+ }
@@ -0,0 +1,36 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.useMergeRefs = useMergeRefs;
7
+ /**
8
+ * Merges multiple refs into a single callback ref.
9
+ * This is useful when you need to forward a ref while also keeping an internal ref.
10
+ *
11
+ * @param refs - Array of refs to merge
12
+ * @returns A callback ref that will update all provided refs
13
+ *
14
+ * @example
15
+ * ```tsx
16
+ * const MyComponent = forwardRef<HTMLInputElement, Props>((props, ref) => {
17
+ * const internalRef = useRef<HTMLInputElement>(null);
18
+ * const mergedRef = useMergeRefs([ref, internalRef]);
19
+ *
20
+ * return <input ref={mergedRef} />;
21
+ * });
22
+ * ```
23
+ */
24
+ function useMergeRefs(refs) {
25
+ return function (node) {
26
+ refs.forEach(function (ref) {
27
+ if (ref) {
28
+ if (typeof ref === 'function') {
29
+ ref(node);
30
+ } else {
31
+ ref.current = node;
32
+ }
33
+ }
34
+ });
35
+ };
36
+ }
package/dist/esm/index.js CHANGED
@@ -25,4 +25,5 @@ export * from './src/components/tab';
25
25
  export * from './src/components/tag';
26
26
  export * from './src/components/toggle';
27
27
  export * from './src/components/tooltip';
28
- export * from './src/components';
28
+ export * from './src/components';
29
+ export * from './src/hooks';
@@ -0,0 +1,215 @@
1
+ var __assign = this && this.__assign || function () {
2
+ __assign = Object.assign || function (t) {
3
+ for (var s, i = 1, n = arguments.length; i < n; i++) {
4
+ s = arguments[i];
5
+ for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) t[p] = s[p];
6
+ }
7
+ return t;
8
+ };
9
+ return __assign.apply(this, arguments);
10
+ };
11
+ var __rest = this && this.__rest || function (s, e) {
12
+ var t = {};
13
+ for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0) t[p] = s[p];
14
+ if (s != null && typeof Object.getOwnPropertySymbols === "function") for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {
15
+ if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i])) t[p[i]] = s[p[i]];
16
+ }
17
+ return t;
18
+ };
19
+ var __spreadArray = this && this.__spreadArray || function (to, from, pack) {
20
+ if (pack || arguments.length === 2) for (var i = 0, l = from.length, ar; i < l; i++) {
21
+ if (ar || !(i in from)) {
22
+ if (!ar) ar = Array.prototype.slice.call(from, 0, i);
23
+ ar[i] = from[i];
24
+ }
25
+ }
26
+ return to.concat(ar || Array.prototype.slice.call(from));
27
+ };
28
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
29
+ import { forwardRef, useState, useRef, useEffect } from 'react';
30
+ import classNames from 'classnames';
31
+ import Icon from '@ncds/ui-admin-icon';
32
+ import { Tag } from '../tag/Tag';
33
+ import { Button } from '../button';
34
+ import { HintText, Label } from '../shared';
35
+ export var FileInputErrorType;
36
+ (function (FileInputErrorType) {
37
+ FileInputErrorType["ALREADY_UPLOADED"] = "ALREADY_UPLOADED";
38
+ FileInputErrorType["EXCEED_MAX_FILE_SIZE"] = "EXCEED_MAX_FILE_SIZE";
39
+ FileInputErrorType["EXCEED_MAX_FILE_COUNT"] = "EXCEED_MAX_FILE_COUNT";
40
+ })(FileInputErrorType || (FileInputErrorType = {}));
41
+ export var FileInput = /*#__PURE__*/forwardRef(function (_a, ref) {
42
+ var _b = _a.size,
43
+ size = _b === void 0 ? 'xs' : _b,
44
+ accept = _a.accept,
45
+ _c = _a.multiple,
46
+ multiple = _c === void 0 ? false : _c,
47
+ maxFileSize = _a.maxFileSize,
48
+ maxFileCount = _a.maxFileCount,
49
+ value = _a.value,
50
+ onChange = _a.onChange,
51
+ onFileSelect = _a.onFileSelect,
52
+ onFail = _a.onFail,
53
+ _d = _a.buttonLabel,
54
+ buttonLabel = _d === void 0 ? '파일 찾기' : _d,
55
+ disabled = _a.disabled,
56
+ label = _a.label,
57
+ hintItems = _a.hintItems,
58
+ validation = _a.validation,
59
+ destructive = _a.destructive,
60
+ isRequired = _a.isRequired,
61
+ showHelpIcon = _a.showHelpIcon,
62
+ hintText = _a.hintText,
63
+ props = __rest(_a, ["size", "accept", "multiple", "maxFileSize", "maxFileCount", "value", "onChange", "onFileSelect", "onFail", "buttonLabel", "disabled", "label", "hintItems", "validation", "destructive", "isRequired", "showHelpIcon", "hintText"]);
64
+ var fileInputRef = useRef(null);
65
+ var _e = useState([]),
66
+ internalFiles = _e[0],
67
+ setInternalFiles = _e[1];
68
+ // Determine if component is controlled or uncontrolled
69
+ var isControlled = value !== undefined;
70
+ var files = isControlled ? value : internalFiles;
71
+ // Sync internal state with controlled value
72
+ useEffect(function () {
73
+ if (isControlled && value) {
74
+ setInternalFiles(value);
75
+ }
76
+ }, [isControlled, value]);
77
+ var updateFiles = function (newFiles) {
78
+ if (isControlled) {
79
+ onChange === null || onChange === void 0 ? void 0 : onChange(newFiles);
80
+ } else {
81
+ setInternalFiles(newFiles);
82
+ onFileSelect === null || onFileSelect === void 0 ? void 0 : onFileSelect(newFiles);
83
+ }
84
+ };
85
+ var handleFileChange = function (event) {
86
+ var selectedFiles = event.target.files;
87
+ if (!selectedFiles || selectedFiles.length === 0) return;
88
+ var _a = validateFiles(Array.from(selectedFiles)),
89
+ validFiles = _a.validFiles,
90
+ invalidFiles = _a.invalidFiles;
91
+ var nextFiles = __spreadArray(__spreadArray([], files, true), validFiles, true);
92
+ updateFiles(nextFiles);
93
+ if (onFail && invalidFiles.length > 0) {
94
+ onFail(invalidFiles);
95
+ }
96
+ };
97
+ var validateFiles = function (fileList) {
98
+ var validFiles = [];
99
+ var invalidFiles = [];
100
+ var _loop_1 = function (file) {
101
+ if (files.some(function (f) {
102
+ return f.name === file.name && f.size === file.size;
103
+ })) {
104
+ invalidFiles.push(__assign(__assign({}, file), {
105
+ errorType: FileInputErrorType.ALREADY_UPLOADED
106
+ }));
107
+ return "continue";
108
+ }
109
+ if (!!maxFileSize && file.size > maxFileSize) {
110
+ invalidFiles.push(__assign(__assign({}, file), {
111
+ errorType: FileInputErrorType.EXCEED_MAX_FILE_SIZE
112
+ }));
113
+ return "continue";
114
+ }
115
+ if (!!maxFileCount && files.length + validFiles.length >= maxFileCount) {
116
+ invalidFiles.push(__assign(__assign({}, file), {
117
+ errorType: FileInputErrorType.EXCEED_MAX_FILE_COUNT
118
+ }));
119
+ return "continue";
120
+ }
121
+ validFiles.push(file);
122
+ };
123
+ for (var _i = 0, fileList_1 = fileList; _i < fileList_1.length; _i++) {
124
+ var file = fileList_1[_i];
125
+ _loop_1(file);
126
+ }
127
+ return {
128
+ validFiles: validFiles,
129
+ invalidFiles: invalidFiles
130
+ };
131
+ };
132
+ var handleBrowseClick = function () {
133
+ if (fileInputRef.current && !disabled) {
134
+ fileInputRef.current.click();
135
+ }
136
+ };
137
+ var handleRemoveFile = function (index) {
138
+ var newFiles = __spreadArray([], files, true);
139
+ newFiles.splice(index, 1);
140
+ updateFiles(newFiles);
141
+ };
142
+ var renderFileTagList = function () {
143
+ if (files.length === 0) return null;
144
+ return _jsx("div", __assign({
145
+ className: "ncua-file-input__file-tags"
146
+ }, {
147
+ children: files.map(function (file, index) {
148
+ return _jsx(Tag, {
149
+ text: file.name,
150
+ size: size === 'xs' ? 'sm' : 'md',
151
+ close: true,
152
+ onButtonClick: function () {
153
+ return handleRemoveFile(index);
154
+ }
155
+ }, "".concat(file.name, "-").concat(index));
156
+ })
157
+ }));
158
+ };
159
+ var renderHintList = function () {
160
+ if (!hintItems || hintItems.length === 0) return null;
161
+ return _jsx("ul", __assign({
162
+ className: "ncua-file-input__hint-list"
163
+ }, {
164
+ children: hintItems.map(function (hint, index) {
165
+ return _jsx("li", __assign({
166
+ className: "ncua-file-input__hint-item"
167
+ }, {
168
+ children: hint
169
+ }), index);
170
+ })
171
+ }));
172
+ };
173
+ return _jsxs("div", __assign({
174
+ className: classNames('ncua-file-input', "ncua-file-input--".concat(size))
175
+ }, {
176
+ children: [_jsx("input", __assign({
177
+ hidden: true,
178
+ ref: fileInputRef,
179
+ type: "file",
180
+ accept: accept,
181
+ multiple: multiple,
182
+ onChange: handleFileChange,
183
+ tabIndex: -1,
184
+ "aria-hidden": "true"
185
+ }, props)), _jsxs("div", __assign({
186
+ className: "ncua-file-input__input-container"
187
+ }, {
188
+ children: [_jsxs("div", __assign({
189
+ className: "ncua-file-input__label"
190
+ }, {
191
+ children: [_jsx(Label, __assign({
192
+ isRequired: isRequired
193
+ }, {
194
+ children: label
195
+ })), showHelpIcon && _jsx(Icon, {
196
+ className: "ncua-input__help-icon",
197
+ name: "help-circle"
198
+ })]
199
+ })), _jsx(Button, {
200
+ size: size,
201
+ onClick: handleBrowseClick,
202
+ disabled: disabled,
203
+ leadingIcon: {
204
+ type: 'icon',
205
+ icon: 'upload-cloud-02'
206
+ },
207
+ label: buttonLabel
208
+ }), hintText && _jsx(HintText, __assign({
209
+ destructive: destructive
210
+ }, {
211
+ children: hintText
212
+ }))]
213
+ })), renderHintList(), renderFileTagList()]
214
+ }));
215
+ });
@@ -17,10 +17,11 @@ var __rest = this && this.__rest || function (s, e) {
17
17
  return t;
18
18
  };
19
19
  import { jsx as _jsx } from "react/jsx-runtime";
20
+ import { forwardRef, useState } from 'react';
21
+ import { InputBase } from './InputBase';
20
22
  import { Eye, EyeOff } from '@ncds/ui-admin-icon';
21
23
  import classNames from 'classnames';
22
- import { forwardRef, useEffect, useRef, useState } from 'react';
23
- import { InputBase } from './InputBase';
24
+ import { useMergeRefs, useCallbackRef } from '../../hooks';
24
25
  var svgSize = {
25
26
  xs: 14,
26
27
  sm: 20
@@ -29,13 +30,25 @@ export var PasswordInput = /*#__PURE__*/forwardRef(function (_a, ref) {
29
30
  var _b = _a.size,
30
31
  size = _b === void 0 ? 'xs' : _b,
31
32
  props = __rest(_a, ["size"]);
32
- var inputRef = useRef(null);
33
33
  var _c = useState(false),
34
34
  isVisible = _c[0],
35
35
  setIsVisible = _c[1];
36
36
  var _d = useState(false),
37
37
  hasContent = _d[0],
38
38
  setHasContent = _d[1];
39
+ var callbackRef = useCallbackRef(function (node) {
40
+ if (node) {
41
+ setHasContent(!!node.value);
42
+ var handleInput_1 = function () {
43
+ setHasContent(!!node.value);
44
+ };
45
+ node.addEventListener('input', handleInput_1);
46
+ return function () {
47
+ node.removeEventListener('input', handleInput_1);
48
+ };
49
+ }
50
+ });
51
+ var mergedRef = useMergeRefs([ref, callbackRef]);
39
52
  var svgProps = {
40
53
  width: svgSize[size],
41
54
  height: svgSize[size]
@@ -43,29 +56,8 @@ export var PasswordInput = /*#__PURE__*/forwardRef(function (_a, ref) {
43
56
  var handleVisibilityChange = function () {
44
57
  setIsVisible(!isVisible);
45
58
  };
46
- useEffect(function () {
47
- if (inputRef.current) {
48
- setHasContent(!!inputRef.current.value);
49
- var handleInput_1 = function () {
50
- var _a;
51
- setHasContent(!!((_a = inputRef.current) === null || _a === void 0 ? void 0 : _a.value));
52
- };
53
- inputRef.current.addEventListener('input', handleInput_1);
54
- return function () {
55
- var _a;
56
- (_a = inputRef.current) === null || _a === void 0 ? void 0 : _a.removeEventListener('input', handleInput_1);
57
- };
58
- }
59
- }, []);
60
59
  return _jsx(InputBase, __assign({
61
- ref: function (node) {
62
- if (typeof ref === 'function') {
63
- ref(node);
64
- } else if (ref) {
65
- ref.current = node;
66
- }
67
- inputRef.current = node;
68
- },
60
+ ref: mergedRef,
69
61
  size: size,
70
62
  type: isVisible ? 'text' : 'password',
71
63
  leadingElement: {
@@ -1,3 +1,4 @@
1
1
  export * from './InputBase';
2
2
  export * from './PasswordInput';
3
- export * from './Textarea';
3
+ export * from './Textarea';
4
+ export * from './FileInput';
@@ -0,0 +1,3 @@
1
+ export { useMediaQuery } from './useMediaQuery';
2
+ export { useMergeRefs } from './useMergeRefs';
3
+ export { useCallbackRef } from './useCallbackRef';
@@ -0,0 +1,43 @@
1
+ import { useCallback, useRef } from 'react';
2
+ /**
3
+ * A callback ref hook that supports cleanup functions.
4
+ * This is useful when you need to perform setup/cleanup operations when the ref changes.
5
+ *
6
+ * @param callback - Function that receives the node and optionally returns a cleanup function
7
+ * @returns A callback ref
8
+ *
9
+ * @example
10
+ * ```tsx
11
+ * const MyComponent = () => {
12
+ * const ref = useCallbackRef<HTMLInputElement>((node) => {
13
+ * if (node) {
14
+ * const handleInput = () => console.log('input changed');
15
+ * node.addEventListener('input', handleInput);
16
+ *
17
+ * // Return cleanup function
18
+ * return () => {
19
+ * node.removeEventListener('input', handleInput);
20
+ * };
21
+ * }
22
+ * });
23
+ *
24
+ * return <input ref={ref} />;
25
+ * };
26
+ * ```
27
+ */
28
+ export function useCallbackRef(callback) {
29
+ var cleanupRef = useRef();
30
+ return useCallback(function (node) {
31
+ // Cleanup previous ref
32
+ if (cleanupRef.current) {
33
+ cleanupRef.current();
34
+ cleanupRef.current = undefined;
35
+ }
36
+ // Set up new ref
37
+ if (node) {
38
+ cleanupRef.current = callback(node);
39
+ } else {
40
+ callback(null);
41
+ }
42
+ }, [callback]);
43
+ }
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Merges multiple refs into a single callback ref.
3
+ * This is useful when you need to forward a ref while also keeping an internal ref.
4
+ *
5
+ * @param refs - Array of refs to merge
6
+ * @returns A callback ref that will update all provided refs
7
+ *
8
+ * @example
9
+ * ```tsx
10
+ * const MyComponent = forwardRef<HTMLInputElement, Props>((props, ref) => {
11
+ * const internalRef = useRef<HTMLInputElement>(null);
12
+ * const mergedRef = useMergeRefs([ref, internalRef]);
13
+ *
14
+ * return <input ref={mergedRef} />;
15
+ * });
16
+ * ```
17
+ */
18
+ export function useMergeRefs(refs) {
19
+ return function (node) {
20
+ refs.forEach(function (ref) {
21
+ if (ref) {
22
+ if (typeof ref === 'function') {
23
+ ref(node);
24
+ } else {
25
+ ref.current = node;
26
+ }
27
+ }
28
+ });
29
+ };
30
+ }
@@ -26,4 +26,5 @@ export * from './src/components/tag';
26
26
  export * from './src/components/toggle';
27
27
  export * from './src/components/tooltip';
28
28
  export * from './src/components';
29
+ export * from './src/hooks';
29
30
  //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1,62 @@
1
+ import { InputBaseProps } from './InputBase';
2
+ export declare enum FileInputErrorType {
3
+ ALREADY_UPLOADED = "ALREADY_UPLOADED",
4
+ EXCEED_MAX_FILE_SIZE = "EXCEED_MAX_FILE_SIZE",
5
+ EXCEED_MAX_FILE_COUNT = "EXCEED_MAX_FILE_COUNT"
6
+ }
7
+ export type InvalidFile = File & {
8
+ errorType: FileInputErrorType;
9
+ };
10
+ export interface FileInputProps extends Omit<InputBaseProps, 'clearText' | 'onClearText' | 'hintText' | 'value' | 'onChange'> {
11
+ /**
12
+ * Accepted file types
13
+ * e.g. '.jpg,.png,.pdf' or 'image/*'
14
+ */
15
+ accept?: string;
16
+ /**
17
+ * Maximum number of files
18
+ */
19
+ maxFileCount?: number;
20
+ /**
21
+ * Maximum file size in bytes
22
+ */
23
+ maxFileSize?: number;
24
+ /**
25
+ * Current files (controlled mode)
26
+ */
27
+ value?: File[];
28
+ /**
29
+ * Callback when files change (controlled mode)
30
+ */
31
+ onChange?: (files: File[]) => void;
32
+ /**
33
+ * Callback when files are selected (uncontrolled mode)
34
+ */
35
+ onFileSelect?: (files: File[]) => void;
36
+ /**
37
+ * Callback when file selection fails
38
+ */
39
+ onFail?: (files: InvalidFile[]) => void;
40
+ /**
41
+ * Label shown on the button
42
+ */
43
+ buttonLabel?: string;
44
+ /**
45
+ * Hint text items to display as a list
46
+ */
47
+ hintItems?: string[];
48
+ /**
49
+ * Whether the input is required
50
+ */
51
+ isRequired?: boolean;
52
+ /**
53
+ * Whether to show the help icon
54
+ */
55
+ showHelpIcon?: boolean;
56
+ /**
57
+ * Hint text to display
58
+ */
59
+ hintText?: string;
60
+ }
61
+ export declare const FileInput: import("react").ForwardRefExoticComponent<FileInputProps & import("react").RefAttributes<HTMLInputElement>>;
62
+ //# sourceMappingURL=FileInput.d.ts.map
@@ -1,4 +1,5 @@
1
1
  export * from './InputBase';
2
2
  export * from './PasswordInput';
3
3
  export * from './Textarea';
4
+ export * from './FileInput';
4
5
  //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1,4 @@
1
+ export { useMediaQuery } from './useMediaQuery';
2
+ export { useMergeRefs } from './useMergeRefs';
3
+ export { useCallbackRef } from './useCallbackRef';
4
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1,28 @@
1
+ /**
2
+ * A callback ref hook that supports cleanup functions.
3
+ * This is useful when you need to perform setup/cleanup operations when the ref changes.
4
+ *
5
+ * @param callback - Function that receives the node and optionally returns a cleanup function
6
+ * @returns A callback ref
7
+ *
8
+ * @example
9
+ * ```tsx
10
+ * const MyComponent = () => {
11
+ * const ref = useCallbackRef<HTMLInputElement>((node) => {
12
+ * if (node) {
13
+ * const handleInput = () => console.log('input changed');
14
+ * node.addEventListener('input', handleInput);
15
+ *
16
+ * // Return cleanup function
17
+ * return () => {
18
+ * node.removeEventListener('input', handleInput);
19
+ * };
20
+ * }
21
+ * });
22
+ *
23
+ * return <input ref={ref} />;
24
+ * };
25
+ * ```
26
+ */
27
+ export declare function useCallbackRef<T>(callback: (node: T | null) => (() => void) | void): React.RefCallback<T>;
28
+ //# sourceMappingURL=useCallbackRef.d.ts.map
@@ -0,0 +1,21 @@
1
+ type Ref<T> = React.RefCallback<T> | React.MutableRefObject<T | null> | null | undefined;
2
+ /**
3
+ * Merges multiple refs into a single callback ref.
4
+ * This is useful when you need to forward a ref while also keeping an internal ref.
5
+ *
6
+ * @param refs - Array of refs to merge
7
+ * @returns A callback ref that will update all provided refs
8
+ *
9
+ * @example
10
+ * ```tsx
11
+ * const MyComponent = forwardRef<HTMLInputElement, Props>((props, ref) => {
12
+ * const internalRef = useRef<HTMLInputElement>(null);
13
+ * const mergedRef = useMergeRefs([ref, internalRef]);
14
+ *
15
+ * return <input ref={mergedRef} />;
16
+ * });
17
+ * ```
18
+ */
19
+ export declare function useMergeRefs<T>(refs: Array<Ref<T>>): React.RefCallback<T>;
20
+ export {};
21
+ //# sourceMappingURL=useMergeRefs.d.ts.map
@@ -1188,6 +1188,60 @@ button {
1188
1188
  color: var(--primary-red-600);
1189
1189
  }
1190
1190
 
1191
+ .ncua-file-input {
1192
+ display: inline-flex;
1193
+ flex-direction: column;
1194
+ align-items: flex-start;
1195
+ gap: 6px;
1196
+ }
1197
+ .ncua-file-input__input-container {
1198
+ display: flex;
1199
+ flex-direction: column;
1200
+ align-items: flex-start;
1201
+ gap: 4px;
1202
+ }
1203
+ .ncua-file-input__label {
1204
+ display: flex;
1205
+ align-items: center;
1206
+ gap: 4px;
1207
+ }
1208
+ .ncua-file-input__file-tags {
1209
+ display: flex;
1210
+ padding: 12px 8px;
1211
+ flex-direction: column;
1212
+ align-items: flex-start;
1213
+ gap: 6px;
1214
+ align-self: stretch;
1215
+ background: var(--gray-50);
1216
+ }
1217
+ .ncua-file-input__hint-list {
1218
+ margin: 0;
1219
+ list-style: disc;
1220
+ color: var(--gray-400);
1221
+ }
1222
+ .ncua-file-input--xs {
1223
+ font-size: var(--font-size-xxs);
1224
+ line-height: var(--line-heights-xxs);
1225
+ }
1226
+ .ncua-file-input--sm {
1227
+ font-size: var(--font-size-xs);
1228
+ line-height: var(--line-heights-xs);
1229
+ }
1230
+ .ncua-file-input--xs .ncua-file-input__input-container {
1231
+ gap: 4px;
1232
+ }
1233
+ .ncua-file-input--sm .ncua-file-input__input-container {
1234
+ gap: 6px;
1235
+ }
1236
+ .ncua-file-input--xs .ncua-file-input__hint-list {
1237
+ font-size: var(--font-size-xxxs);
1238
+ line-height: var(--line-heights-xxxs);
1239
+ }
1240
+ .ncua-file-input--md .ncua-file-input__hint-list {
1241
+ font-size: inherit;
1242
+ line-height: inherit;
1243
+ }
1244
+
1191
1245
  .ncua-input {
1192
1246
  display: inline-flex;
1193
1247
  flex-direction: column;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ncds/ui-admin",
3
- "version": "1.1.3",
3
+ "version": "1.2.0",
4
4
  "description": "nhn-commerce의 어드민 디자인 시스템입니다.",
5
5
  "scripts": {
6
6
  "barrel": "node barrel.js",
@@ -63,7 +63,7 @@
63
63
  "dependencies": {
64
64
  "@ncds/types-common": "^1.0.0",
65
65
  "@ncds/types-layout": "^1.0.0",
66
- "@ncds/ui-admin-icon": "0.0.16",
66
+ "@ncds/ui-admin-icon": "0.0.17",
67
67
  "classnames": "^2.3.2",
68
68
  "lodash": "^4.17.21",
69
69
  "lodash-es": "^4.17.21",