jy-headless 0.3.15 → 0.3.17

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/dist/Autocomplete/Autocomplete.d.ts +103 -0
  2. package/dist/Autocomplete/Autocomplete.js +104 -0
  3. package/dist/Autocomplete/Autocomplete.js.map +1 -0
  4. package/dist/Input/NumberInput.d.ts +53 -0
  5. package/dist/Input/NumberInput.js +54 -0
  6. package/dist/Input/NumberInput.js.map +1 -0
  7. package/dist/Input/TextInput.d.ts +95 -0
  8. package/dist/Input/TextInput.js +96 -0
  9. package/dist/Input/TextInput.js.map +1 -0
  10. package/dist/Select/Select.js +1 -0
  11. package/dist/Select/Select.js.map +1 -0
  12. package/dist/Tooltip/Tooltip.js +1 -0
  13. package/dist/Tooltip/Tooltip.js.map +1 -0
  14. package/dist/hooks/useDebounce.js +1 -0
  15. package/dist/hooks/useDebounce.js.map +1 -0
  16. package/dist/hooks/usePortal.js +1 -0
  17. package/dist/hooks/usePortal.js.map +1 -0
  18. package/dist/hooks/useThrottle.js +1 -0
  19. package/dist/hooks/useThrottle.js.map +1 -0
  20. package/dist/index.js +1 -0
  21. package/dist/index.js.map +1 -0
  22. package/package.json +4 -3
  23. package/dist/cjs/Autocomplete/Autocomplete.d.ts +0 -7
  24. package/dist/cjs/Autocomplete/Autocomplete.js +0 -301
  25. package/dist/cjs/Autocomplete/Autocomplete.type.d.ts +0 -37
  26. package/dist/cjs/Autocomplete/index.d.ts +0 -2
  27. package/dist/cjs/Input/NumberInput.d.ts +0 -2
  28. package/dist/cjs/Input/NumberInput.js +0 -40
  29. package/dist/cjs/Input/NumberInput.type.d.ts +0 -31
  30. package/dist/cjs/Input/TextInput.d.ts +0 -3
  31. package/dist/cjs/Input/TextInput.js +0 -79
  32. package/dist/cjs/Input/TextInput.type.d.ts +0 -95
  33. package/dist/cjs/Input/index.d.ts +0 -4
  34. package/dist/cjs/Select/Select.d.ts +0 -51
  35. package/dist/cjs/Select/Select.js +0 -179
  36. package/dist/cjs/Select/Select.type.d.ts +0 -52
  37. package/dist/cjs/Select/index.d.ts +0 -2
  38. package/dist/cjs/Tooltip/Tooltip.d.ts +0 -10
  39. package/dist/cjs/Tooltip/Tooltip.js +0 -38
  40. package/dist/cjs/Tooltip/Tooltip.type.d.ts +0 -20
  41. package/dist/cjs/Tooltip/index.d.ts +0 -2
  42. package/dist/cjs/hooks/index.d.ts +0 -3
  43. package/dist/cjs/hooks/useDebounce.d.ts +0 -1
  44. package/dist/cjs/hooks/useDebounce.js +0 -24
  45. package/dist/cjs/hooks/usePortal.d.ts +0 -23
  46. package/dist/cjs/hooks/usePortal.js +0 -80
  47. package/dist/cjs/hooks/useThrottle.d.ts +0 -1
  48. package/dist/cjs/hooks/useThrottle.js +0 -34
  49. package/dist/cjs/index.d.ts +0 -5
  50. package/dist/cjs/index.js +0 -23
@@ -1,301 +0,0 @@
1
- 'use strict';
2
-
3
- var jsxRuntime = require('react/jsx-runtime');
4
- var react = require('react');
5
- var usePortal = require('../hooks/usePortal.js');
6
- var TextInput = require('../Input/TextInput.js');
7
-
8
- const AutocompleteContext = react.createContext(null);
9
- const useAutocomplete = () => {
10
- const ctx = react.useContext(AutocompleteContext);
11
- if (!ctx)
12
- throw new Error('Autocomplete components must be used within <Autocomplete>');
13
- return ctx;
14
- };
15
- const defaultFilter = (item, query) => item.label.toLowerCase().includes(query.trim().toLowerCase());
16
- /**
17
- * Root
18
- */
19
- const AutocompleteContainer = ({ value, onChange, inputValue, onInputChange, disabled, filterFn = defaultFilter, children, }) => {
20
- const [open, setOpen] = react.useState(false);
21
- const [activeIndex, setActiveIndex] = react.useState(-1);
22
- const inputRef = react.useRef(null);
23
- // controlled/uncontrolled query
24
- const [internalQuery, setInternalQuery] = react.useState('');
25
- const query = inputValue ?? internalQuery;
26
- const setQuery = (v) => {
27
- onInputChange?.(v);
28
- if (inputValue === undefined)
29
- setInternalQuery(v);
30
- };
31
- const listboxId = react.useId();
32
- // NOTE:
33
- // - virtualization을 제대로 하려면 options data(items)가 Root에 필요하지만,
34
- // compound API 유지를 위해 Options에서 items를 주입받아 Root에서 filtered를 만들기 어렵다.
35
- // 그래서 Root에서는 "filtered"를 Options에서 제공할 수 있게 설계하면 복잡해짐.
36
- // ✅ 해결: Options에 items를 주면 Root가 접근할 수 있도록 "itemsRef"를 둔다.
37
- const itemsRef = react.useRef([]);
38
- const setItems = (items) => {
39
- itemsRef.current = items;
40
- };
41
- const filtered = react.useMemo(() => {
42
- const src = itemsRef.current ?? [];
43
- if (!query.trim())
44
- return src;
45
- return src.filter((it) => filterFn(it, query));
46
- }, [query, filterFn]);
47
- const close = () => {
48
- setOpen(false);
49
- setActiveIndex(-1);
50
- };
51
- const commitByIndex = (index) => {
52
- const item = filtered[index];
53
- if (!item || item.disabled)
54
- return;
55
- onChange(item.value);
56
- setQuery(item.label);
57
- close();
58
- };
59
- // 외부 value 변경 시 input label 동기화
60
- react.useEffect(() => {
61
- if (!value)
62
- return;
63
- const found = itemsRef.current.find((i) => i.value === value);
64
- if (found)
65
- setQuery(found.label);
66
- // eslint-disable-next-line react-hooks/exhaustive-deps
67
- }, [value]);
68
- const statusText = react.useMemo(() => {
69
- if (!open)
70
- return '';
71
- const n = filtered.length;
72
- if (n === 0)
73
- return 'No results.';
74
- if (n === 1)
75
- return '1 result available.';
76
- return `${n} results available.`;
77
- }, [open, filtered.length]);
78
- return (jsxRuntime.jsxs(AutocompleteContext.Provider, { value: {
79
- open,
80
- setOpen,
81
- selectedValue: value,
82
- setSelectedValue: onChange,
83
- query,
84
- setQuery,
85
- disabled,
86
- inputRef,
87
- listboxId,
88
- activeIndex,
89
- setActiveIndex,
90
- filtered,
91
- statusText,
92
- commitByIndex,
93
- close,
94
- }, children: [jsxRuntime.jsx(ItemsBridge, { onItems: setItems }), children] }));
95
- };
96
- /**
97
- * Options에서 items를 주입해주기 위한 브릿지(보이지 않는 컴포넌트)
98
- * - Options가 items prop을 받으면, 내부에서 window.__ 같은 걸 쓰지 않고 Root ref에 주입
99
- */
100
- const ItemsBridgeContext = react.createContext(null);
101
- const ItemsBridge = ({ onItems }) => {
102
- return jsxRuntime.jsx(ItemsBridgeContext.Provider, { value: onItems });
103
- };
104
- const useItemsBridge = () => react.useContext(ItemsBridgeContext);
105
- /**
106
- * Input (Combobox Trigger)
107
- * - 포커스는 input 유지
108
- * - aria-activedescendant로 active option을 알려줌
109
- */
110
- // ...생략
111
- const Input = ({ onKeyDown, onFocus, onChange, onCompositionStart, onCompositionEnd, ...props }) => {
112
- const { open, setOpen, query, setQuery, disabled, listboxId, activeIndex, setActiveIndex, filtered, commitByIndex, close, inputRef, } = useAutocomplete();
113
- // ✅ IME 조합 중에는 방향키/엔터로 옵션 선택하지 않게 막기
114
- const composingRef = react.useRef(false);
115
- const activeId = activeIndex >= 0 ? `${listboxId}-opt-${activeIndex}` : undefined;
116
- const move = (delta) => {
117
- if (!filtered.length)
118
- return;
119
- setOpen(true);
120
- setActiveIndex(activeIndex < 0 ? 0 : (activeIndex + delta + filtered.length) % filtered.length);
121
- };
122
- return (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [jsxRuntime.jsx(TextInput.TextInput, { ref: inputRef, role: 'combobox', "aria-autocomplete": 'list', "aria-expanded": open, "aria-controls": listboxId, "aria-activedescendant": activeId, disabled: disabled, value: query, onFocus: (e) => {
123
- if (!disabled) {
124
- setOpen(true);
125
- setActiveIndex(filtered.length ? 0 : -1);
126
- }
127
- onFocus?.(e);
128
- }, onChange: (e) => {
129
- if (disabled)
130
- return;
131
- setQuery(e.target.value);
132
- setOpen(true);
133
- setActiveIndex(0);
134
- onChange?.(e);
135
- }, onCompositionStart: (e) => {
136
- composingRef.current = true;
137
- onCompositionStart?.(e);
138
- }, onCompositionEnd: (e) => {
139
- composingRef.current = false;
140
- onCompositionEnd?.(e);
141
- }, onKeyDown: (e) => {
142
- if (disabled)
143
- return;
144
- // ✅ 조합중이면 Autocomplete 키처리 하지 않음
145
- if (composingRef.current) {
146
- onKeyDown?.(e);
147
- return;
148
- }
149
- switch (e.key) {
150
- case 'ArrowDown':
151
- e.preventDefault();
152
- move(1);
153
- break;
154
- case 'ArrowUp':
155
- e.preventDefault();
156
- move(-1);
157
- break;
158
- case 'Enter':
159
- if (open && activeIndex >= 0) {
160
- e.preventDefault();
161
- commitByIndex(activeIndex);
162
- }
163
- break;
164
- case 'Escape':
165
- e.preventDefault();
166
- close();
167
- break;
168
- case 'Tab':
169
- close();
170
- break;
171
- }
172
- onKeyDown?.(e);
173
- }, ...props }), jsxRuntime.jsx("span", { "aria-live": 'polite', style: {
174
- position: 'absolute',
175
- width: 1,
176
- height: 1,
177
- overflow: 'hidden',
178
- clip: 'rect(0 0 0 0)',
179
- whiteSpace: 'nowrap',
180
- }, children: open ? `${filtered.length} results.` : '' })] }));
181
- };
182
- /**
183
- * Options
184
- * - portal
185
- * - outside click close
186
- * - virtualization (items + renderItem provided)
187
- */
188
- const Options = ({ items, renderItem, itemHeight = 36, maxVisibleItems = 8, overscan = 3, children, ...props }) => {
189
- const bridge = useItemsBridge();
190
- const { open, setOpen, close, inputRef, listboxId, filtered, activeIndex, setActiveIndex, commitByIndex, } = useAutocomplete();
191
- // items 주입(가상화 모드)
192
- react.useEffect(() => {
193
- if (items && bridge)
194
- bridge(items);
195
- }, [items, bridge]);
196
- const popoverRef = react.useRef(null);
197
- const triggerWidth = inputRef.current?.getBoundingClientRect().width;
198
- // outside click
199
- react.useEffect(() => {
200
- if (!open)
201
- return;
202
- const onDown = (e) => {
203
- const t = e.target;
204
- if (inputRef.current?.contains(t) || popoverRef.current?.contains(t))
205
- return;
206
- close();
207
- };
208
- document.addEventListener('mousedown', onDown);
209
- return () => document.removeEventListener('mousedown', onDown);
210
- }, [open, close, inputRef]);
211
- // scroll container ref for virtualization
212
- const scrollRef = react.useRef(null);
213
- // active option이 항상 보이게 스크롤 보정
214
- react.useEffect(() => {
215
- if (!open)
216
- return;
217
- if (!scrollRef.current)
218
- return;
219
- if (activeIndex < 0)
220
- return;
221
- const top = activeIndex * itemHeight;
222
- const bottom = top + itemHeight;
223
- const viewTop = scrollRef.current.scrollTop;
224
- const viewBottom = viewTop + scrollRef.current.clientHeight;
225
- if (top < viewTop)
226
- scrollRef.current.scrollTop = top;
227
- else if (bottom > viewBottom)
228
- scrollRef.current.scrollTop = bottom - scrollRef.current.clientHeight;
229
- }, [open, activeIndex, itemHeight]);
230
- // virtualization range
231
- const total = filtered.length;
232
- const viewportCount = Math.min(maxVisibleItems, Math.max(1, total));
233
- const viewportHeight = viewportCount * itemHeight;
234
- const [scrollTop, setScrollTop] = react.useState(0);
235
- const startIndex = Math.max(0, Math.floor(scrollTop / itemHeight) - overscan);
236
- const endIndex = Math.min(total, Math.ceil((scrollTop + viewportHeight) / itemHeight) + overscan);
237
- const visible = filtered.slice(startIndex, endIndex);
238
- const { portal } = usePortal({
239
- visible: open,
240
- targetRef: inputRef,
241
- popoverRef,
242
- direction: 'bottom',
243
- gap: 4,
244
- content: (jsxRuntime.jsx("div", { ref: popoverRef, style: { width: triggerWidth }, ...props, children: items && renderItem ? (jsxRuntime.jsx("div", { id: listboxId, role: 'listbox', ref: scrollRef, style: { maxHeight: viewportHeight, overflow: 'auto', position: 'relative' }, onScroll: (e) => setScrollTop(e.target.scrollTop), children: jsxRuntime.jsx("div", { style: { height: total * itemHeight, position: 'relative' }, children: visible.map((item, i) => {
245
- const index = startIndex + i; // filtered index
246
- const isActive = index === activeIndex;
247
- const isSelected = item.value === undefined; // selection 표시를 더 강하게 원하면 ctx.selectedValue 비교해서 쓰면 됨
248
- return (jsxRuntime.jsx("div", { id: `${listboxId}-opt-${index}`, role: 'option', "aria-selected": isSelected, "aria-disabled": item.disabled, "data-active": isActive, tabIndex: -1, onMouseEnter: () => setActiveIndex(index), onMouseDown: (e) => {
249
- // 클릭 시 input blur 방지(중요)
250
- e.preventDefault();
251
- }, onClick: () => commitByIndex(index), style: {
252
- position: 'absolute',
253
- top: index * itemHeight,
254
- left: 0,
255
- right: 0,
256
- height: itemHeight,
257
- display: 'flex',
258
- alignItems: 'center',
259
- }, children: renderItem(item) }, item.value));
260
- }) }) })) : (
261
- /* ✅ children 모드(비가상화) */
262
- jsxRuntime.jsx("div", { id: listboxId, role: 'listbox', children: children })) })),
263
- });
264
- return open ? portal : null;
265
- };
266
- /**
267
- * Option (children 모드 전용 / small list)
268
- * - a11y option role/ids만 최소 보장
269
- * - 가상화는 여기엔 적용하지 않음
270
- */
271
- const Option = ({ value, label, disabled, children, ...props }) => {
272
- const { listboxId, filtered, query, open, setOpen, setQuery, setSelectedValue, activeIndex, setActiveIndex, } = useAutocomplete();
273
- // children 모드에서 필터링은 “간단 버전”
274
- const visible = react.useMemo(() => {
275
- if (!query.trim())
276
- return true;
277
- return label.toLowerCase().includes(query.trim().toLowerCase());
278
- }, [label, query]);
279
- react.useMemo(() => {
280
- // filtered는 items 모드에서만 의미있음.
281
- // children 모드는 간단히 -1 처리(aria-activedescendant는 items 모드가 권장)
282
- return -1;
283
- }, []);
284
- if (!visible)
285
- return null;
286
- return (jsxRuntime.jsx("div", { role: "option", "aria-disabled": disabled, "aria-selected": false, tabIndex: -1, onMouseDown: (e) => e.preventDefault(), onClick: () => {
287
- if (disabled)
288
- return;
289
- setSelectedValue(value);
290
- setQuery(label);
291
- setOpen(false);
292
- setActiveIndex(-1);
293
- }, ...props, children: children ?? label }));
294
- };
295
- const Autocomplete = Object.assign(AutocompleteContainer, {
296
- Input,
297
- Options,
298
- Option,
299
- });
300
-
301
- exports.Autocomplete = Autocomplete;
@@ -1,37 +0,0 @@
1
- import { HTMLAttributes, ReactNode } from 'react';
2
- import { TextInputProps } from '../Input/TextInput.type';
3
- export type AutocompleteItem = {
4
- value: string;
5
- label: string;
6
- disabled?: boolean;
7
- };
8
- export interface AutocompleteProps {
9
- value: string | null;
10
- onChange: (v: string | null) => void;
11
- inputValue?: string;
12
- onInputChange?: (v: string) => void;
13
- disabled?: boolean;
14
- /** 옵션 필터 커스터마이즈 */
15
- filterFn?: (item: AutocompleteItem, query: string) => boolean;
16
- children: ReactNode;
17
- }
18
- export interface AutocompleteInputProps extends TextInputProps {
19
- /** label 연결용 (없으면 aria-label 필수) */
20
- 'aria-label'?: string;
21
- }
22
- export interface AutocompleteOptionsProps extends HTMLAttributes<HTMLDivElement> {
23
- /** ✅ 가상화 모드: 데이터 기반 */
24
- items?: AutocompleteItem[];
25
- /** 가상화 모드에서 항목 렌더 */
26
- renderItem?: (item: AutocompleteItem) => ReactNode;
27
- /** virtualization 옵션 */
28
- itemHeight?: number;
29
- maxVisibleItems?: number;
30
- overscan?: number;
31
- }
32
- export interface AutocompleteOptionProps extends HTMLAttributes<HTMLDivElement> {
33
- /** children 모드(비가상화) */
34
- value: string;
35
- label: string;
36
- disabled?: boolean;
37
- }
@@ -1,2 +0,0 @@
1
- export * from './Autocomplete';
2
- export * from './Autocomplete.type';
@@ -1,2 +0,0 @@
1
- import { NumberInputProps } from './NumberInput.type';
2
- export declare const NumberInput: ({ max, useThousandsSeparator, onChange, ...props }: NumberInputProps) => import("react/jsx-runtime").JSX.Element;
@@ -1,40 +0,0 @@
1
- 'use strict';
2
-
3
- var jsxRuntime = require('react/jsx-runtime');
4
- var TextInput = require('./TextInput.js');
5
-
6
- const NumberInput = ({ max, useThousandsSeparator, onChange, ...props }) => {
7
- const handleChange = (e) => {
8
- const rawValue = e.target.value.replace(/,/g, '');
9
- if (rawValue && isNaN(Number(rawValue)))
10
- return;
11
- let finalValue = rawValue;
12
- if (max && Number(rawValue) > max) {
13
- finalValue = String(max);
14
- }
15
- if (useThousandsSeparator && finalValue) {
16
- const formattedValue = Number(finalValue).toLocaleString();
17
- const syntheticEvent = {
18
- ...e,
19
- target: {
20
- ...e.target,
21
- value: formattedValue,
22
- },
23
- };
24
- onChange?.(syntheticEvent);
25
- }
26
- else {
27
- const syntheticEvent = {
28
- ...e,
29
- target: {
30
- ...e.target,
31
- value: finalValue,
32
- },
33
- };
34
- onChange?.(syntheticEvent);
35
- }
36
- };
37
- return jsxRuntime.jsx(TextInput.TextInput, { ...props, onChange: handleChange });
38
- };
39
-
40
- exports.NumberInput = NumberInput;
@@ -1,31 +0,0 @@
1
- import { TextInputProps } from './TextInput.type';
2
- /**
3
- * 숫자 전용 입력 컴포넌트 props
4
- *
5
- * TextInput을 기반으로 하며,
6
- * 숫자 입력에 특화된 기능을 제공합니다.
7
- */
8
- export interface NumberInputProps extends TextInputProps {
9
- /**
10
- * 최대 허용 값
11
- *
12
- * 초과 시:
13
- * - 입력 제한
14
- * - 또는 validation 에러 처리 가능
15
- *
16
- * @example
17
- * max={100}
18
- */
19
- max?: number;
20
- /**
21
- * 천 단위 구분자 사용 여부
22
- *
23
- * true일 경우:
24
- * 10000 → 10,000
25
- *
26
- * 내부 값은 parse 단계에서 콤마 제거 권장
27
- *
28
- * @default false
29
- */
30
- useThousandsSeparator?: boolean;
31
- }
@@ -1,3 +0,0 @@
1
- import React from 'react';
2
- import { TextInputProps } from './TextInput.type';
3
- export declare const TextInput: React.ForwardRefExoticComponent<TextInputProps & React.RefAttributes<HTMLInputElement>>;
@@ -1,79 +0,0 @@
1
- 'use strict';
2
-
3
- var jsxRuntime = require('react/jsx-runtime');
4
- var react = require('react');
5
- require('react-dom');
6
- var useDebounce = require('../hooks/useDebounce.js');
7
- var useThrottle = require('../hooks/useThrottle.js');
8
-
9
- function mergeRefs(...refs) {
10
- return (value) => {
11
- refs.forEach((ref) => {
12
- if (!ref)
13
- return;
14
- if (typeof ref === 'function')
15
- ref(value);
16
- else
17
- ref.current = value;
18
- });
19
- };
20
- }
21
- const TextInput = react.forwardRef(({ maxLength, onChange, pattern, onValidate, validator, onCompositionStart, onCompositionEnd, disallowPattern, trimWhitespace, debounceMs, throttleMs, onDebouncedChange, onThrottledChange, onBlur, ...props }, ref) => {
22
- const [isComposing, setIsComposing] = react.useState(false);
23
- const innerRef = react.useRef(null);
24
- const combinedRef = react.useMemo(() => mergeRefs(innerRef, ref), [ref]);
25
- const debouncedChange = useDebounce.useDebounce((value) => onDebouncedChange?.(value), debounceMs || 0);
26
- const throttledChange = useThrottle.useThrottle((value) => onThrottledChange?.(value), throttleMs || 0);
27
- const handleCompositionStart = (e) => {
28
- setIsComposing(true);
29
- onCompositionStart?.(e);
30
- };
31
- const handleCompositionEnd = (e) => {
32
- setIsComposing(false);
33
- onCompositionEnd?.(e);
34
- };
35
- const handleChange = (e) => {
36
- if (maxLength && !isComposing && e.target.value.length > maxLength)
37
- return;
38
- if (pattern) {
39
- const regex = new RegExp(pattern);
40
- if (!regex.test(e.target.value))
41
- return;
42
- }
43
- if (disallowPattern && !disallowPattern.test(e.target.value))
44
- return;
45
- if (debounceMs && onDebouncedChange)
46
- debouncedChange(e.target.value);
47
- if (throttleMs && onThrottledChange)
48
- throttledChange(e.target.value);
49
- if (validator) {
50
- const result = validator(e.target.value);
51
- const isValid = typeof result === 'boolean' ? result : true;
52
- const error = typeof result === 'string' ? result : undefined;
53
- onValidate?.(isValid, error);
54
- if (!isValid)
55
- return;
56
- }
57
- onChange?.(e);
58
- };
59
- const handleBlur = (e) => {
60
- if (trimWhitespace && innerRef.current) {
61
- const trimmedValue = e.target.value.trim();
62
- if (trimmedValue !== e.target.value) {
63
- innerRef.current.value = trimmedValue;
64
- const syntheticEvent = {
65
- ...e,
66
- target: innerRef.current,
67
- currentTarget: innerRef.current,
68
- type: 'change',
69
- };
70
- onChange?.(syntheticEvent);
71
- }
72
- }
73
- onBlur?.(e);
74
- };
75
- return (jsxRuntime.jsx("input", { ref: combinedRef, ...props, onBlur: handleBlur, onChange: handleChange, onCompositionStart: handleCompositionStart, onCompositionEnd: handleCompositionEnd }));
76
- });
77
- TextInput.displayName = 'TextInput';
78
-
79
- exports.TextInput = TextInput;
@@ -1,95 +0,0 @@
1
- import { ChangeEvent, CompositionEvent, InputHTMLAttributes } from 'react';
2
- /**
3
- * 고급 기능을 제공하는 TextInput 컴포넌트 props
4
- *
5
- * 기능:
6
- * - validation
7
- * - formatting/parsing
8
- * - debounce/throttle
9
- * - IME(한글 입력) 대응
10
- * - 공백 제어
11
- */
12
- export interface TextInputProps extends InputHTMLAttributes<HTMLInputElement> {
13
- /**
14
- * 입력값 검증 함수
15
- *
16
- * return:
17
- * - true → 유효
18
- * - false → 유효하지 않음
19
- * - string → 에러 메시지
20
- *
21
- * @example
22
- * validator={(v) => v.length > 3 || "최소 3자 이상"}
23
- */
24
- validator?: (value: string) => boolean | string;
25
- /**
26
- * 검증 결과 콜백
27
- */
28
- onValidate?: (isValid: boolean, error?: string) => void;
29
- /**
30
- * 표시용 값 포맷팅
31
- *
32
- * ex) 숫자 천단위 콤마
33
- */
34
- format?: (value: string) => string;
35
- /**
36
- * 실제 값으로 변환
37
- *
38
- * ex) 콤마 제거
39
- */
40
- parse?: (value: string) => string;
41
- /**
42
- * 입력을 막을 패턴
43
- *
44
- * @example 숫자만 허용
45
- * disallowPattern={/[^0-9]/g}
46
- */
47
- disallowPattern?: RegExp;
48
- /**
49
- * 공백 제거 방식
50
- *
51
- * - true → 전체 trim
52
- * - 'leading' → 앞 공백만
53
- * - 'trailing' → 뒤 공백만
54
- * - 'both' → 양쪽
55
- */
56
- trimWhitespace?: boolean | 'leading' | 'trailing' | 'both';
57
- /**
58
- * IME 입력 시작 이벤트
59
- *
60
- * 한글/중국어 입력 시 중요
61
- */
62
- onCompositionStart?: (e: CompositionEvent<HTMLInputElement>) => void;
63
- /**
64
- * IME 입력 종료 이벤트
65
- */
66
- onCompositionEnd?: (e: CompositionEvent<HTMLInputElement>) => void;
67
- /**
68
- * IME 입력 중 이벤트
69
- */
70
- onCompositionUpdate?: (e: CompositionEvent<HTMLInputElement>) => void;
71
- /**
72
- * debounce 지연(ms)
73
- *
74
- * @example 300
75
- */
76
- debounceMs?: number;
77
- /**
78
- * throttle 지연(ms)
79
- */
80
- throttleMs?: number;
81
- /**
82
- * debounce 후 값 변경 콜백
83
- */
84
- onDebouncedChange?: (value: string) => void;
85
- /**
86
- * throttle 후 값 변경 콜백
87
- */
88
- onThrottledChange?: (value: string) => void;
89
- /**
90
- * 기본 onChange override
91
- *
92
- * ⚠️ 일반 HTML onChange와 동일
93
- */
94
- onChange?: (value: ChangeEvent<HTMLInputElement>) => void;
95
- }
@@ -1,4 +0,0 @@
1
- export * from './NumberInput';
2
- export * from './TextInput';
3
- export * from './TextInput.type';
4
- export * from './NumberInput.type';
@@ -1,51 +0,0 @@
1
- import { Dispatch, SetStateAction } from 'react';
2
- import { SelectOptionProps, SelectOptionsProps, SelectProps, SelectTriggerProps } from './Select.type';
3
- /**
4
- * Select 내부 상태 공유용 Context
5
- */
6
- type SelectContextValue = {
7
- /** 드롭다운 열림 여부 */
8
- open: boolean;
9
- /** 드롭다운 열기/닫기 */
10
- setOpen: (v: boolean) => void;
11
- /** 현재 선택된 값들 */
12
- value: string[];
13
- /** 값 토글 */
14
- toggleValue: (v: string) => void;
15
- /** 다중 선택 여부 */
16
- multiple: boolean;
17
- /** Trigger DOM ref */
18
- triggerRef: React.RefObject<HTMLDivElement | null>;
19
- /** Option DOM refs 목록 */
20
- optionRefs: React.MutableRefObject<HTMLDivElement[]>;
21
- /** 현재 포커스된 옵션 index */
22
- focusedIndex: number | null;
23
- /** 포커스 index 설정 */
24
- setFocusedIndex: Dispatch<SetStateAction<number>>;
25
- };
26
- /**
27
- * Select Context 접근 훅
28
- *
29
- * @throws Select 외부에서 사용할 경우 에러
30
- */
31
- export declare const useSelectContext: () => SelectContextValue;
32
- /**
33
- * Compound Select 컴포넌트
34
- *
35
- * 사용 예시:
36
- *
37
- * @example
38
- * <Select value={value} onChange={setValue}>
39
- * <Select.Trigger>열기</Select.Trigger>
40
- * <Select.Options>
41
- * <Select.Option value="a">A</Select.Option>
42
- * <Select.Option value="b">B</Select.Option>
43
- * </Select.Options>
44
- * </Select>
45
- */
46
- export declare const Select: (({ value, onChange, multiple, children }: SelectProps) => import("react/jsx-runtime").JSX.Element) & {
47
- Trigger: (props: SelectTriggerProps) => import("react/jsx-runtime").JSX.Element;
48
- Options: ({ children, ...props }: SelectOptionsProps) => import("react").ReactPortal | null;
49
- Option: ({ value, disabled, children, ...props }: SelectOptionProps) => import("react/jsx-runtime").JSX.Element;
50
- };
51
- export {};