jy-headless 0.3.14 → 0.3.16

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/README.md CHANGED
@@ -18,6 +18,12 @@ React용 Headless UI 라이브러리
18
18
 
19
19
  ---
20
20
 
21
+ ## 스토리북
22
+
23
+ [Storybook](https://6795bdd4b570ec0f79b87452-rmxhztnaqg.chromatic.com/?path=/docs/components-autocomplete--docs) 에서
24
+ 컴포넌트를 테스트 해 볼 수 있습니다
25
+ ___
26
+
21
27
  ## 제공 요소
22
28
 
23
29
  ### Components
@@ -1,5 +1,108 @@
1
1
  import React from 'react';
2
2
  import type { AutocompleteInputProps, AutocompleteOptionProps, AutocompleteOptionsProps, AutocompleteProps } from './Autocomplete.type';
3
+ /**
4
+ * 범용 Autocomplete(Combobox) 컴포넌트 (Compound API)
5
+ *
6
+ * - Compound 구성: <Autocomplete> + <Autocomplete.Input /> + <Autocomplete.Options /> + <Autocomplete.Option />
7
+ * - a11y: combobox/listbox/option role + aria-controls/activedescendant + aria-live(결과 수 안내)
8
+ * - 키보드: ArrowUp/Down 이동, Enter 선택, Escape 닫기, Tab 닫기
9
+ * - IME(한글/일본어) 조합 입력 중에는 방향키/엔터 선택 로직을 막아 UX를 보호
10
+ * - Options는 portal로 렌더링되며, outside click 시 닫힘
11
+ *
12
+ * ## 모드
13
+ * 1) items 모드 (권장)
14
+ * - <Autocomplete.Options items={...} renderItem={...} />
15
+ * - 내부에서 filterFn으로 filtered를 만들고, Options는 filtered를 가상화로 렌더
16
+ *
17
+ * 2) children 모드 (간단 리스트)
18
+ * - <Autocomplete.Options> 안에 <Autocomplete.Option />을 직접 나열
19
+ * - 소규모 옵션에만 권장 (가상화/activeIndex 기반 aria-activedescendant는 items 모드가 더 적합)
20
+ *
21
+ * @example
22
+ * // 1) items 모드 (기본 / 권장)
23
+ * const items = [
24
+ * { value: 'apple', label: 'Apple' },
25
+ * { value: 'banana', label: 'Banana' },
26
+ * { value: 'grape', label: 'Grape' },
27
+ * ];
28
+ *
29
+ * const [value, setValue] = useState<string | null>(null);
30
+ *
31
+ * <Autocomplete value={value} onChange={setValue}>
32
+ * <Autocomplete.Input placeholder="과일을 검색하세요" />
33
+ * <Autocomplete.Options
34
+ * items={items}
35
+ * renderItem={(item) => <div style={{ padding: 8 }}>{item.label}</div>}
36
+ * />
37
+ * </Autocomplete>
38
+ *
39
+ * @example
40
+ * // 2) 입력값(query)까지 controlled로 관리하고 싶을 때
41
+ * const [value, setValue] = useState<string | null>(null);
42
+ * const [inputValue, setInputValue] = useState('');
43
+ *
44
+ * <Autocomplete
45
+ * value={value}
46
+ * onChange={setValue}
47
+ * inputValue={inputValue}
48
+ * onInputChange={setInputValue}
49
+ * >
50
+ * <Autocomplete.Input placeholder="검색어 제어" />
51
+ * <Autocomplete.Options
52
+ * items={items}
53
+ * renderItem={(item) => <div style={{ padding: 8 }}>{item.label}</div>}
54
+ * />
55
+ * </Autocomplete>
56
+ *
57
+ * @example
58
+ * // 3) filterFn 커스터마이징 (prefix match)
59
+ * const startsWithFilter = (item, query) =>
60
+ * item.label.toLowerCase().startsWith(query.trim().toLowerCase());
61
+ *
62
+ * <Autocomplete value={value} onChange={setValue} filterFn={startsWithFilter}>
63
+ * <Autocomplete.Input placeholder="앞글자 매칭" />
64
+ * <Autocomplete.Options
65
+ * items={items}
66
+ * renderItem={(item) => <div style={{ padding: 8 }}>{item.label}</div>}
67
+ * />
68
+ * </Autocomplete>
69
+ *
70
+ * @example
71
+ * // 4) 가상화 옵션 조정 (많은 데이터)
72
+ * <Autocomplete value={value} onChange={setValue}>
73
+ * <Autocomplete.Input placeholder="대량 데이터" />
74
+ * <Autocomplete.Options
75
+ * items={bigItems}
76
+ * renderItem={(item) => <div style={{ padding: 8 }}>{item.label}</div>}
77
+ * itemHeight={36}
78
+ * maxVisibleItems={8}
79
+ * overscan={3}
80
+ * />
81
+ * </Autocomplete>
82
+ *
83
+ * @example
84
+ * // 5) disabled
85
+ * <Autocomplete value={value} onChange={setValue} disabled>
86
+ * <Autocomplete.Input placeholder="비활성화" />
87
+ * <Autocomplete.Options
88
+ * items={items}
89
+ * renderItem={(item) => <div style={{ padding: 8 }}>{item.label}</div>}
90
+ * />
91
+ * </Autocomplete>
92
+ *
93
+ * @example
94
+ * // 6) children 모드 (소규모 리스트에만 권장)
95
+ * const [value, setValue] = useState<string | null>(null);
96
+ *
97
+ * <Autocomplete value={value} onChange={setValue}>
98
+ * <Autocomplete.Input placeholder="직접 옵션 나열" />
99
+ * <Autocomplete.Options>
100
+ * <Autocomplete.Option value="seoul" label="Seoul" />
101
+ * <Autocomplete.Option value="busan" label="Busan" />
102
+ * <Autocomplete.Option value="jeju" label="Jeju" disabled />
103
+ * </Autocomplete.Options>
104
+ * </Autocomplete>
105
+ */
3
106
  export declare const Autocomplete: (({ value, onChange, inputValue, onInputChange, disabled, filterFn, children, }: AutocompleteProps) => import("react/jsx-runtime").JSX.Element) & {
4
107
  Input: ({ onKeyDown, onFocus, onChange, onCompositionStart, onCompositionEnd, ...props }: AutocompleteInputProps) => import("react/jsx-runtime").JSX.Element;
5
108
  Options: ({ items, renderItem, itemHeight, maxVisibleItems, overscan, children, ...props }: AutocompleteOptionsProps) => React.ReactPortal | null;
@@ -290,6 +290,109 @@ const Option = ({ value, label, disabled, children, ...props }) => {
290
290
  setActiveIndex(-1);
291
291
  }, ...props, children: children ?? label }));
292
292
  };
293
+ /**
294
+ * 범용 Autocomplete(Combobox) 컴포넌트 (Compound API)
295
+ *
296
+ * - Compound 구성: <Autocomplete> + <Autocomplete.Input /> + <Autocomplete.Options /> + <Autocomplete.Option />
297
+ * - a11y: combobox/listbox/option role + aria-controls/activedescendant + aria-live(결과 수 안내)
298
+ * - 키보드: ArrowUp/Down 이동, Enter 선택, Escape 닫기, Tab 닫기
299
+ * - IME(한글/일본어) 조합 입력 중에는 방향키/엔터 선택 로직을 막아 UX를 보호
300
+ * - Options는 portal로 렌더링되며, outside click 시 닫힘
301
+ *
302
+ * ## 모드
303
+ * 1) items 모드 (권장)
304
+ * - <Autocomplete.Options items={...} renderItem={...} />
305
+ * - 내부에서 filterFn으로 filtered를 만들고, Options는 filtered를 가상화로 렌더
306
+ *
307
+ * 2) children 모드 (간단 리스트)
308
+ * - <Autocomplete.Options> 안에 <Autocomplete.Option />을 직접 나열
309
+ * - 소규모 옵션에만 권장 (가상화/activeIndex 기반 aria-activedescendant는 items 모드가 더 적합)
310
+ *
311
+ * @example
312
+ * // 1) items 모드 (기본 / 권장)
313
+ * const items = [
314
+ * { value: 'apple', label: 'Apple' },
315
+ * { value: 'banana', label: 'Banana' },
316
+ * { value: 'grape', label: 'Grape' },
317
+ * ];
318
+ *
319
+ * const [value, setValue] = useState<string | null>(null);
320
+ *
321
+ * <Autocomplete value={value} onChange={setValue}>
322
+ * <Autocomplete.Input placeholder="과일을 검색하세요" />
323
+ * <Autocomplete.Options
324
+ * items={items}
325
+ * renderItem={(item) => <div style={{ padding: 8 }}>{item.label}</div>}
326
+ * />
327
+ * </Autocomplete>
328
+ *
329
+ * @example
330
+ * // 2) 입력값(query)까지 controlled로 관리하고 싶을 때
331
+ * const [value, setValue] = useState<string | null>(null);
332
+ * const [inputValue, setInputValue] = useState('');
333
+ *
334
+ * <Autocomplete
335
+ * value={value}
336
+ * onChange={setValue}
337
+ * inputValue={inputValue}
338
+ * onInputChange={setInputValue}
339
+ * >
340
+ * <Autocomplete.Input placeholder="검색어 제어" />
341
+ * <Autocomplete.Options
342
+ * items={items}
343
+ * renderItem={(item) => <div style={{ padding: 8 }}>{item.label}</div>}
344
+ * />
345
+ * </Autocomplete>
346
+ *
347
+ * @example
348
+ * // 3) filterFn 커스터마이징 (prefix match)
349
+ * const startsWithFilter = (item, query) =>
350
+ * item.label.toLowerCase().startsWith(query.trim().toLowerCase());
351
+ *
352
+ * <Autocomplete value={value} onChange={setValue} filterFn={startsWithFilter}>
353
+ * <Autocomplete.Input placeholder="앞글자 매칭" />
354
+ * <Autocomplete.Options
355
+ * items={items}
356
+ * renderItem={(item) => <div style={{ padding: 8 }}>{item.label}</div>}
357
+ * />
358
+ * </Autocomplete>
359
+ *
360
+ * @example
361
+ * // 4) 가상화 옵션 조정 (많은 데이터)
362
+ * <Autocomplete value={value} onChange={setValue}>
363
+ * <Autocomplete.Input placeholder="대량 데이터" />
364
+ * <Autocomplete.Options
365
+ * items={bigItems}
366
+ * renderItem={(item) => <div style={{ padding: 8 }}>{item.label}</div>}
367
+ * itemHeight={36}
368
+ * maxVisibleItems={8}
369
+ * overscan={3}
370
+ * />
371
+ * </Autocomplete>
372
+ *
373
+ * @example
374
+ * // 5) disabled
375
+ * <Autocomplete value={value} onChange={setValue} disabled>
376
+ * <Autocomplete.Input placeholder="비활성화" />
377
+ * <Autocomplete.Options
378
+ * items={items}
379
+ * renderItem={(item) => <div style={{ padding: 8 }}>{item.label}</div>}
380
+ * />
381
+ * </Autocomplete>
382
+ *
383
+ * @example
384
+ * // 6) children 모드 (소규모 리스트에만 권장)
385
+ * const [value, setValue] = useState<string | null>(null);
386
+ *
387
+ * <Autocomplete value={value} onChange={setValue}>
388
+ * <Autocomplete.Input placeholder="직접 옵션 나열" />
389
+ * <Autocomplete.Options>
390
+ * <Autocomplete.Option value="seoul" label="Seoul" />
391
+ * <Autocomplete.Option value="busan" label="Busan" />
392
+ * <Autocomplete.Option value="jeju" label="Jeju" disabled />
393
+ * </Autocomplete.Options>
394
+ * </Autocomplete>
395
+ */
293
396
  const Autocomplete = Object.assign(AutocompleteContainer, {
294
397
  Input,
295
398
  Options,
@@ -1,2 +1,55 @@
1
1
  import { NumberInputProps } from './NumberInput.type';
2
+ /**
3
+ * 숫자 전용 Input 컴포넌트
4
+ *
5
+ * - 숫자만 입력 가능 (콤마는 내부적으로 제거 후 처리)
6
+ * - max: 최대값 제한 (초과 시 자동으로 max로 보정)
7
+ * - useThousandsSeparator: 천단위 콤마 자동 포맷팅
8
+ * - 실제 onChange로 넘어가는 값은:
9
+ * - useThousandsSeparator=true → 콤마가 포함된 문자열
10
+ * - false → 순수 숫자 문자열
11
+ *
12
+ * @example
13
+ * // 1) 기본 숫자 입력
14
+ * <NumberInput
15
+ * placeholder="숫자 입력"
16
+ * onChange={(e) => console.log(e.target.value)}
17
+ * />
18
+ *
19
+ * @example
20
+ * // 2) 최대값 제한
21
+ * <NumberInput
22
+ * max={100}
23
+ * placeholder="최대 100"
24
+ * onChange={(e) => console.log(e.target.value)}
25
+ * />
26
+ *
27
+ * @example
28
+ * // 3) 천단위 콤마 포맷
29
+ * <NumberInput
30
+ * useThousandsSeparator
31
+ * placeholder="금액 입력"
32
+ * onChange={(e) => console.log(e.target.value)} // 10000 → "10,000"
33
+ * />
34
+ *
35
+ * @example
36
+ * // 4) max + 천단위 콤마 같이 사용
37
+ * <NumberInput
38
+ * max={1000000}
39
+ * useThousandsSeparator
40
+ * placeholder="최대 1,000,000"
41
+ * onChange={(e) => console.log(e.target.value)}
42
+ * />
43
+ *
44
+ * @example
45
+ * // 5) React Hook Form 등과 함께 사용할 때
46
+ * // - 저장 시에는 콤마 제거 후 숫자로 변환하는 게 일반적
47
+ * <NumberInput
48
+ * useThousandsSeparator
49
+ * onChange={(e) => {
50
+ * const numeric = Number(e.target.value.replace(/,/g, ''));
51
+ * console.log(numeric);
52
+ * }}
53
+ * />
54
+ */
2
55
  export declare const NumberInput: ({ max, useThousandsSeparator, onChange, ...props }: NumberInputProps) => import("react/jsx-runtime").JSX.Element;
@@ -1,6 +1,59 @@
1
1
  import { jsx } from 'react/jsx-runtime';
2
2
  import { TextInput } from './TextInput.js';
3
3
 
4
+ /**
5
+ * 숫자 전용 Input 컴포넌트
6
+ *
7
+ * - 숫자만 입력 가능 (콤마는 내부적으로 제거 후 처리)
8
+ * - max: 최대값 제한 (초과 시 자동으로 max로 보정)
9
+ * - useThousandsSeparator: 천단위 콤마 자동 포맷팅
10
+ * - 실제 onChange로 넘어가는 값은:
11
+ * - useThousandsSeparator=true → 콤마가 포함된 문자열
12
+ * - false → 순수 숫자 문자열
13
+ *
14
+ * @example
15
+ * // 1) 기본 숫자 입력
16
+ * <NumberInput
17
+ * placeholder="숫자 입력"
18
+ * onChange={(e) => console.log(e.target.value)}
19
+ * />
20
+ *
21
+ * @example
22
+ * // 2) 최대값 제한
23
+ * <NumberInput
24
+ * max={100}
25
+ * placeholder="최대 100"
26
+ * onChange={(e) => console.log(e.target.value)}
27
+ * />
28
+ *
29
+ * @example
30
+ * // 3) 천단위 콤마 포맷
31
+ * <NumberInput
32
+ * useThousandsSeparator
33
+ * placeholder="금액 입력"
34
+ * onChange={(e) => console.log(e.target.value)} // 10000 → "10,000"
35
+ * />
36
+ *
37
+ * @example
38
+ * // 4) max + 천단위 콤마 같이 사용
39
+ * <NumberInput
40
+ * max={1000000}
41
+ * useThousandsSeparator
42
+ * placeholder="최대 1,000,000"
43
+ * onChange={(e) => console.log(e.target.value)}
44
+ * />
45
+ *
46
+ * @example
47
+ * // 5) React Hook Form 등과 함께 사용할 때
48
+ * // - 저장 시에는 콤마 제거 후 숫자로 변환하는 게 일반적
49
+ * <NumberInput
50
+ * useThousandsSeparator
51
+ * onChange={(e) => {
52
+ * const numeric = Number(e.target.value.replace(/,/g, ''));
53
+ * console.log(numeric);
54
+ * }}
55
+ * />
56
+ */
4
57
  const NumberInput = ({ max, useThousandsSeparator, onChange, ...props }) => {
5
58
  const handleChange = (e) => {
6
59
  const rawValue = e.target.value.replace(/,/g, '');
@@ -1,3 +1,98 @@
1
1
  import React from 'react';
2
2
  import { TextInputProps } from './TextInput.type';
3
+ /**
4
+ * 범용 TextInput 컴포넌트
5
+ *
6
+ * - maxLength: (조합 입력 중이 아닐 때) 최대 길이 제한
7
+ * - pattern: 정규식 문자열(매 입력마다 test 통과해야 반영)
8
+ * - disallowPattern: "허용" 정규식(현재 구현은 test가 false면 return)
9
+ * - validator: 값 검증 (boolean | error message string 반환 가능)
10
+ * - trimWhitespace: blur 시 앞뒤 공백 제거 후 onChange 호출
11
+ * - debounceMs / onDebouncedChange: 디바운스된 값 콜백
12
+ * - throttleMs / onThrottledChange: 스로틀된 값 콜백
13
+ *
14
+ * @example
15
+ * // 1) 기본 사용
16
+ * <TextInput
17
+ * placeholder="이름을 입력하세요"
18
+ * onChange={(e) => console.log(e.target.value)}
19
+ * />
20
+ *
21
+ * @example
22
+ * // 2) maxLength + trimWhitespace (blur 시 공백 제거)
23
+ * <TextInput
24
+ * maxLength={20}
25
+ * trimWhitespace
26
+ * placeholder="최대 20자, blur 시 공백 제거"
27
+ * onChange={(e) => console.log('change:', e.target.value)}
28
+ * />
29
+ *
30
+ * @example
31
+ * // 3) pattern(문자열)로 숫자만 허용
32
+ * // - 입력값이 정규식에 매번 매칭되어야 반영됨
33
+ * <TextInput
34
+ * pattern="^[0-9]*$"
35
+ * inputMode="numeric"
36
+ * placeholder="숫자만 입력"
37
+ * onChange={(e) => console.log(e.target.value)}
38
+ * />
39
+ *
40
+ * @example
41
+ * // 4) disallowPattern(정규식)로 "허용 규칙" 적용 (현재 구현 기준)
42
+ * // - test가 false면 반영되지 않음
43
+ * // - 예: 영문/숫자/언더스코어만 허용
44
+ * <TextInput
45
+ * disallowPattern={/^[a-zA-Z0-9_]*$/}
46
+ * placeholder="영문/숫자/_ 만"
47
+ * onChange={(e) => console.log(e.target.value)}
48
+ * />
49
+ *
50
+ * @example
51
+ * // 5) validator + onValidate
52
+ * // - validator가 false면 onChange가 호출되지 않음
53
+ * // - 문자열 반환 시 그 문자열이 error로 넘어감
54
+ * <TextInput
55
+ * placeholder="이메일"
56
+ * validator={(v) => {
57
+ * if (!v) return true; // 빈 값은 통과(원하면 false로 바꿔도 됨)
58
+ * const ok = /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(v);
59
+ * return ok || '이메일 형식이 아니에요';
60
+ * }}
61
+ * onValidate={(isValid, error) => {
62
+ * console.log('valid:', isValid, 'error:', error);
63
+ * }}
64
+ * onChange={(e) => console.log(e.target.value)}
65
+ * />
66
+ *
67
+ * @example
68
+ * // 6) debounce: 입력은 즉시 반영(onChange), 서버검색/자동완성은 디바운스로 처리
69
+ * <TextInput
70
+ * placeholder="검색어 입력"
71
+ * debounceMs={300}
72
+ * onDebouncedChange={(value) => {
73
+ * // fetch(`/api/search?q=${encodeURIComponent(value)}`)
74
+ * console.log('debounced:', value);
75
+ * }}
76
+ * onChange={(e) => console.log('immediate:', e.target.value)}
77
+ * />
78
+ *
79
+ * @example
80
+ * // 7) throttle: 스크롤/리사이즈처럼 자주 쏘는 이벤트에 유용한 패턴
81
+ * <TextInput
82
+ * placeholder="입력값 로깅(최대 200ms에 1번)"
83
+ * throttleMs={200}
84
+ * onThrottledChange={(value) => console.log('throttled:', value)}
85
+ * onChange={(e) => console.log('immediate:', e.target.value)}
86
+ * />
87
+ *
88
+ * @example
89
+ * // 8) IME(한글/일본어) 조합 이벤트 처리 예시
90
+ * <TextInput
91
+ * placeholder="한글 입력"
92
+ * maxLength={10}
93
+ * onCompositionStart={() => console.log('compose start')}
94
+ * onCompositionEnd={() => console.log('compose end')}
95
+ * onChange={(e) => console.log(e.target.value)}
96
+ * />
97
+ */
3
98
  export declare const TextInput: React.ForwardRefExoticComponent<TextInputProps & React.RefAttributes<HTMLInputElement>>;
@@ -16,6 +16,101 @@ function mergeRefs(...refs) {
16
16
  });
17
17
  };
18
18
  }
19
+ /**
20
+ * 범용 TextInput 컴포넌트
21
+ *
22
+ * - maxLength: (조합 입력 중이 아닐 때) 최대 길이 제한
23
+ * - pattern: 정규식 문자열(매 입력마다 test 통과해야 반영)
24
+ * - disallowPattern: "허용" 정규식(현재 구현은 test가 false면 return)
25
+ * - validator: 값 검증 (boolean | error message string 반환 가능)
26
+ * - trimWhitespace: blur 시 앞뒤 공백 제거 후 onChange 호출
27
+ * - debounceMs / onDebouncedChange: 디바운스된 값 콜백
28
+ * - throttleMs / onThrottledChange: 스로틀된 값 콜백
29
+ *
30
+ * @example
31
+ * // 1) 기본 사용
32
+ * <TextInput
33
+ * placeholder="이름을 입력하세요"
34
+ * onChange={(e) => console.log(e.target.value)}
35
+ * />
36
+ *
37
+ * @example
38
+ * // 2) maxLength + trimWhitespace (blur 시 공백 제거)
39
+ * <TextInput
40
+ * maxLength={20}
41
+ * trimWhitespace
42
+ * placeholder="최대 20자, blur 시 공백 제거"
43
+ * onChange={(e) => console.log('change:', e.target.value)}
44
+ * />
45
+ *
46
+ * @example
47
+ * // 3) pattern(문자열)로 숫자만 허용
48
+ * // - 입력값이 정규식에 매번 매칭되어야 반영됨
49
+ * <TextInput
50
+ * pattern="^[0-9]*$"
51
+ * inputMode="numeric"
52
+ * placeholder="숫자만 입력"
53
+ * onChange={(e) => console.log(e.target.value)}
54
+ * />
55
+ *
56
+ * @example
57
+ * // 4) disallowPattern(정규식)로 "허용 규칙" 적용 (현재 구현 기준)
58
+ * // - test가 false면 반영되지 않음
59
+ * // - 예: 영문/숫자/언더스코어만 허용
60
+ * <TextInput
61
+ * disallowPattern={/^[a-zA-Z0-9_]*$/}
62
+ * placeholder="영문/숫자/_ 만"
63
+ * onChange={(e) => console.log(e.target.value)}
64
+ * />
65
+ *
66
+ * @example
67
+ * // 5) validator + onValidate
68
+ * // - validator가 false면 onChange가 호출되지 않음
69
+ * // - 문자열 반환 시 그 문자열이 error로 넘어감
70
+ * <TextInput
71
+ * placeholder="이메일"
72
+ * validator={(v) => {
73
+ * if (!v) return true; // 빈 값은 통과(원하면 false로 바꿔도 됨)
74
+ * const ok = /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(v);
75
+ * return ok || '이메일 형식이 아니에요';
76
+ * }}
77
+ * onValidate={(isValid, error) => {
78
+ * console.log('valid:', isValid, 'error:', error);
79
+ * }}
80
+ * onChange={(e) => console.log(e.target.value)}
81
+ * />
82
+ *
83
+ * @example
84
+ * // 6) debounce: 입력은 즉시 반영(onChange), 서버검색/자동완성은 디바운스로 처리
85
+ * <TextInput
86
+ * placeholder="검색어 입력"
87
+ * debounceMs={300}
88
+ * onDebouncedChange={(value) => {
89
+ * // fetch(`/api/search?q=${encodeURIComponent(value)}`)
90
+ * console.log('debounced:', value);
91
+ * }}
92
+ * onChange={(e) => console.log('immediate:', e.target.value)}
93
+ * />
94
+ *
95
+ * @example
96
+ * // 7) throttle: 스크롤/리사이즈처럼 자주 쏘는 이벤트에 유용한 패턴
97
+ * <TextInput
98
+ * placeholder="입력값 로깅(최대 200ms에 1번)"
99
+ * throttleMs={200}
100
+ * onThrottledChange={(value) => console.log('throttled:', value)}
101
+ * onChange={(e) => console.log('immediate:', e.target.value)}
102
+ * />
103
+ *
104
+ * @example
105
+ * // 8) IME(한글/일본어) 조합 이벤트 처리 예시
106
+ * <TextInput
107
+ * placeholder="한글 입력"
108
+ * maxLength={10}
109
+ * onCompositionStart={() => console.log('compose start')}
110
+ * onCompositionEnd={() => console.log('compose end')}
111
+ * onChange={(e) => console.log(e.target.value)}
112
+ * />
113
+ */
19
114
  const TextInput = forwardRef(({ maxLength, onChange, pattern, onValidate, validator, onCompositionStart, onCompositionEnd, disallowPattern, trimWhitespace, debounceMs, throttleMs, onDebouncedChange, onThrottledChange, onBlur, ...props }, ref) => {
20
115
  const [isComposing, setIsComposing] = useState(false);
21
116
  const innerRef = useRef(null);
@@ -1,5 +1,108 @@
1
1
  import React from 'react';
2
2
  import type { AutocompleteInputProps, AutocompleteOptionProps, AutocompleteOptionsProps, AutocompleteProps } from './Autocomplete.type';
3
+ /**
4
+ * 범용 Autocomplete(Combobox) 컴포넌트 (Compound API)
5
+ *
6
+ * - Compound 구성: <Autocomplete> + <Autocomplete.Input /> + <Autocomplete.Options /> + <Autocomplete.Option />
7
+ * - a11y: combobox/listbox/option role + aria-controls/activedescendant + aria-live(결과 수 안내)
8
+ * - 키보드: ArrowUp/Down 이동, Enter 선택, Escape 닫기, Tab 닫기
9
+ * - IME(한글/일본어) 조합 입력 중에는 방향키/엔터 선택 로직을 막아 UX를 보호
10
+ * - Options는 portal로 렌더링되며, outside click 시 닫힘
11
+ *
12
+ * ## 모드
13
+ * 1) items 모드 (권장)
14
+ * - <Autocomplete.Options items={...} renderItem={...} />
15
+ * - 내부에서 filterFn으로 filtered를 만들고, Options는 filtered를 가상화로 렌더
16
+ *
17
+ * 2) children 모드 (간단 리스트)
18
+ * - <Autocomplete.Options> 안에 <Autocomplete.Option />을 직접 나열
19
+ * - 소규모 옵션에만 권장 (가상화/activeIndex 기반 aria-activedescendant는 items 모드가 더 적합)
20
+ *
21
+ * @example
22
+ * // 1) items 모드 (기본 / 권장)
23
+ * const items = [
24
+ * { value: 'apple', label: 'Apple' },
25
+ * { value: 'banana', label: 'Banana' },
26
+ * { value: 'grape', label: 'Grape' },
27
+ * ];
28
+ *
29
+ * const [value, setValue] = useState<string | null>(null);
30
+ *
31
+ * <Autocomplete value={value} onChange={setValue}>
32
+ * <Autocomplete.Input placeholder="과일을 검색하세요" />
33
+ * <Autocomplete.Options
34
+ * items={items}
35
+ * renderItem={(item) => <div style={{ padding: 8 }}>{item.label}</div>}
36
+ * />
37
+ * </Autocomplete>
38
+ *
39
+ * @example
40
+ * // 2) 입력값(query)까지 controlled로 관리하고 싶을 때
41
+ * const [value, setValue] = useState<string | null>(null);
42
+ * const [inputValue, setInputValue] = useState('');
43
+ *
44
+ * <Autocomplete
45
+ * value={value}
46
+ * onChange={setValue}
47
+ * inputValue={inputValue}
48
+ * onInputChange={setInputValue}
49
+ * >
50
+ * <Autocomplete.Input placeholder="검색어 제어" />
51
+ * <Autocomplete.Options
52
+ * items={items}
53
+ * renderItem={(item) => <div style={{ padding: 8 }}>{item.label}</div>}
54
+ * />
55
+ * </Autocomplete>
56
+ *
57
+ * @example
58
+ * // 3) filterFn 커스터마이징 (prefix match)
59
+ * const startsWithFilter = (item, query) =>
60
+ * item.label.toLowerCase().startsWith(query.trim().toLowerCase());
61
+ *
62
+ * <Autocomplete value={value} onChange={setValue} filterFn={startsWithFilter}>
63
+ * <Autocomplete.Input placeholder="앞글자 매칭" />
64
+ * <Autocomplete.Options
65
+ * items={items}
66
+ * renderItem={(item) => <div style={{ padding: 8 }}>{item.label}</div>}
67
+ * />
68
+ * </Autocomplete>
69
+ *
70
+ * @example
71
+ * // 4) 가상화 옵션 조정 (많은 데이터)
72
+ * <Autocomplete value={value} onChange={setValue}>
73
+ * <Autocomplete.Input placeholder="대량 데이터" />
74
+ * <Autocomplete.Options
75
+ * items={bigItems}
76
+ * renderItem={(item) => <div style={{ padding: 8 }}>{item.label}</div>}
77
+ * itemHeight={36}
78
+ * maxVisibleItems={8}
79
+ * overscan={3}
80
+ * />
81
+ * </Autocomplete>
82
+ *
83
+ * @example
84
+ * // 5) disabled
85
+ * <Autocomplete value={value} onChange={setValue} disabled>
86
+ * <Autocomplete.Input placeholder="비활성화" />
87
+ * <Autocomplete.Options
88
+ * items={items}
89
+ * renderItem={(item) => <div style={{ padding: 8 }}>{item.label}</div>}
90
+ * />
91
+ * </Autocomplete>
92
+ *
93
+ * @example
94
+ * // 6) children 모드 (소규모 리스트에만 권장)
95
+ * const [value, setValue] = useState<string | null>(null);
96
+ *
97
+ * <Autocomplete value={value} onChange={setValue}>
98
+ * <Autocomplete.Input placeholder="직접 옵션 나열" />
99
+ * <Autocomplete.Options>
100
+ * <Autocomplete.Option value="seoul" label="Seoul" />
101
+ * <Autocomplete.Option value="busan" label="Busan" />
102
+ * <Autocomplete.Option value="jeju" label="Jeju" disabled />
103
+ * </Autocomplete.Options>
104
+ * </Autocomplete>
105
+ */
3
106
  export declare const Autocomplete: (({ value, onChange, inputValue, onInputChange, disabled, filterFn, children, }: AutocompleteProps) => import("react/jsx-runtime").JSX.Element) & {
4
107
  Input: ({ onKeyDown, onFocus, onChange, onCompositionStart, onCompositionEnd, ...props }: AutocompleteInputProps) => import("react/jsx-runtime").JSX.Element;
5
108
  Options: ({ items, renderItem, itemHeight, maxVisibleItems, overscan, children, ...props }: AutocompleteOptionsProps) => React.ReactPortal | null;
@@ -292,6 +292,109 @@ const Option = ({ value, label, disabled, children, ...props }) => {
292
292
  setActiveIndex(-1);
293
293
  }, ...props, children: children ?? label }));
294
294
  };
295
+ /**
296
+ * 범용 Autocomplete(Combobox) 컴포넌트 (Compound API)
297
+ *
298
+ * - Compound 구성: <Autocomplete> + <Autocomplete.Input /> + <Autocomplete.Options /> + <Autocomplete.Option />
299
+ * - a11y: combobox/listbox/option role + aria-controls/activedescendant + aria-live(결과 수 안내)
300
+ * - 키보드: ArrowUp/Down 이동, Enter 선택, Escape 닫기, Tab 닫기
301
+ * - IME(한글/일본어) 조합 입력 중에는 방향키/엔터 선택 로직을 막아 UX를 보호
302
+ * - Options는 portal로 렌더링되며, outside click 시 닫힘
303
+ *
304
+ * ## 모드
305
+ * 1) items 모드 (권장)
306
+ * - <Autocomplete.Options items={...} renderItem={...} />
307
+ * - 내부에서 filterFn으로 filtered를 만들고, Options는 filtered를 가상화로 렌더
308
+ *
309
+ * 2) children 모드 (간단 리스트)
310
+ * - <Autocomplete.Options> 안에 <Autocomplete.Option />을 직접 나열
311
+ * - 소규모 옵션에만 권장 (가상화/activeIndex 기반 aria-activedescendant는 items 모드가 더 적합)
312
+ *
313
+ * @example
314
+ * // 1) items 모드 (기본 / 권장)
315
+ * const items = [
316
+ * { value: 'apple', label: 'Apple' },
317
+ * { value: 'banana', label: 'Banana' },
318
+ * { value: 'grape', label: 'Grape' },
319
+ * ];
320
+ *
321
+ * const [value, setValue] = useState<string | null>(null);
322
+ *
323
+ * <Autocomplete value={value} onChange={setValue}>
324
+ * <Autocomplete.Input placeholder="과일을 검색하세요" />
325
+ * <Autocomplete.Options
326
+ * items={items}
327
+ * renderItem={(item) => <div style={{ padding: 8 }}>{item.label}</div>}
328
+ * />
329
+ * </Autocomplete>
330
+ *
331
+ * @example
332
+ * // 2) 입력값(query)까지 controlled로 관리하고 싶을 때
333
+ * const [value, setValue] = useState<string | null>(null);
334
+ * const [inputValue, setInputValue] = useState('');
335
+ *
336
+ * <Autocomplete
337
+ * value={value}
338
+ * onChange={setValue}
339
+ * inputValue={inputValue}
340
+ * onInputChange={setInputValue}
341
+ * >
342
+ * <Autocomplete.Input placeholder="검색어 제어" />
343
+ * <Autocomplete.Options
344
+ * items={items}
345
+ * renderItem={(item) => <div style={{ padding: 8 }}>{item.label}</div>}
346
+ * />
347
+ * </Autocomplete>
348
+ *
349
+ * @example
350
+ * // 3) filterFn 커스터마이징 (prefix match)
351
+ * const startsWithFilter = (item, query) =>
352
+ * item.label.toLowerCase().startsWith(query.trim().toLowerCase());
353
+ *
354
+ * <Autocomplete value={value} onChange={setValue} filterFn={startsWithFilter}>
355
+ * <Autocomplete.Input placeholder="앞글자 매칭" />
356
+ * <Autocomplete.Options
357
+ * items={items}
358
+ * renderItem={(item) => <div style={{ padding: 8 }}>{item.label}</div>}
359
+ * />
360
+ * </Autocomplete>
361
+ *
362
+ * @example
363
+ * // 4) 가상화 옵션 조정 (많은 데이터)
364
+ * <Autocomplete value={value} onChange={setValue}>
365
+ * <Autocomplete.Input placeholder="대량 데이터" />
366
+ * <Autocomplete.Options
367
+ * items={bigItems}
368
+ * renderItem={(item) => <div style={{ padding: 8 }}>{item.label}</div>}
369
+ * itemHeight={36}
370
+ * maxVisibleItems={8}
371
+ * overscan={3}
372
+ * />
373
+ * </Autocomplete>
374
+ *
375
+ * @example
376
+ * // 5) disabled
377
+ * <Autocomplete value={value} onChange={setValue} disabled>
378
+ * <Autocomplete.Input placeholder="비활성화" />
379
+ * <Autocomplete.Options
380
+ * items={items}
381
+ * renderItem={(item) => <div style={{ padding: 8 }}>{item.label}</div>}
382
+ * />
383
+ * </Autocomplete>
384
+ *
385
+ * @example
386
+ * // 6) children 모드 (소규모 리스트에만 권장)
387
+ * const [value, setValue] = useState<string | null>(null);
388
+ *
389
+ * <Autocomplete value={value} onChange={setValue}>
390
+ * <Autocomplete.Input placeholder="직접 옵션 나열" />
391
+ * <Autocomplete.Options>
392
+ * <Autocomplete.Option value="seoul" label="Seoul" />
393
+ * <Autocomplete.Option value="busan" label="Busan" />
394
+ * <Autocomplete.Option value="jeju" label="Jeju" disabled />
395
+ * </Autocomplete.Options>
396
+ * </Autocomplete>
397
+ */
295
398
  const Autocomplete = Object.assign(AutocompleteContainer, {
296
399
  Input,
297
400
  Options,
@@ -1,2 +1,55 @@
1
1
  import { NumberInputProps } from './NumberInput.type';
2
+ /**
3
+ * 숫자 전용 Input 컴포넌트
4
+ *
5
+ * - 숫자만 입력 가능 (콤마는 내부적으로 제거 후 처리)
6
+ * - max: 최대값 제한 (초과 시 자동으로 max로 보정)
7
+ * - useThousandsSeparator: 천단위 콤마 자동 포맷팅
8
+ * - 실제 onChange로 넘어가는 값은:
9
+ * - useThousandsSeparator=true → 콤마가 포함된 문자열
10
+ * - false → 순수 숫자 문자열
11
+ *
12
+ * @example
13
+ * // 1) 기본 숫자 입력
14
+ * <NumberInput
15
+ * placeholder="숫자 입력"
16
+ * onChange={(e) => console.log(e.target.value)}
17
+ * />
18
+ *
19
+ * @example
20
+ * // 2) 최대값 제한
21
+ * <NumberInput
22
+ * max={100}
23
+ * placeholder="최대 100"
24
+ * onChange={(e) => console.log(e.target.value)}
25
+ * />
26
+ *
27
+ * @example
28
+ * // 3) 천단위 콤마 포맷
29
+ * <NumberInput
30
+ * useThousandsSeparator
31
+ * placeholder="금액 입력"
32
+ * onChange={(e) => console.log(e.target.value)} // 10000 → "10,000"
33
+ * />
34
+ *
35
+ * @example
36
+ * // 4) max + 천단위 콤마 같이 사용
37
+ * <NumberInput
38
+ * max={1000000}
39
+ * useThousandsSeparator
40
+ * placeholder="최대 1,000,000"
41
+ * onChange={(e) => console.log(e.target.value)}
42
+ * />
43
+ *
44
+ * @example
45
+ * // 5) React Hook Form 등과 함께 사용할 때
46
+ * // - 저장 시에는 콤마 제거 후 숫자로 변환하는 게 일반적
47
+ * <NumberInput
48
+ * useThousandsSeparator
49
+ * onChange={(e) => {
50
+ * const numeric = Number(e.target.value.replace(/,/g, ''));
51
+ * console.log(numeric);
52
+ * }}
53
+ * />
54
+ */
2
55
  export declare const NumberInput: ({ max, useThousandsSeparator, onChange, ...props }: NumberInputProps) => import("react/jsx-runtime").JSX.Element;
@@ -3,6 +3,59 @@
3
3
  var jsxRuntime = require('react/jsx-runtime');
4
4
  var TextInput = require('./TextInput.js');
5
5
 
6
+ /**
7
+ * 숫자 전용 Input 컴포넌트
8
+ *
9
+ * - 숫자만 입력 가능 (콤마는 내부적으로 제거 후 처리)
10
+ * - max: 최대값 제한 (초과 시 자동으로 max로 보정)
11
+ * - useThousandsSeparator: 천단위 콤마 자동 포맷팅
12
+ * - 실제 onChange로 넘어가는 값은:
13
+ * - useThousandsSeparator=true → 콤마가 포함된 문자열
14
+ * - false → 순수 숫자 문자열
15
+ *
16
+ * @example
17
+ * // 1) 기본 숫자 입력
18
+ * <NumberInput
19
+ * placeholder="숫자 입력"
20
+ * onChange={(e) => console.log(e.target.value)}
21
+ * />
22
+ *
23
+ * @example
24
+ * // 2) 최대값 제한
25
+ * <NumberInput
26
+ * max={100}
27
+ * placeholder="최대 100"
28
+ * onChange={(e) => console.log(e.target.value)}
29
+ * />
30
+ *
31
+ * @example
32
+ * // 3) 천단위 콤마 포맷
33
+ * <NumberInput
34
+ * useThousandsSeparator
35
+ * placeholder="금액 입력"
36
+ * onChange={(e) => console.log(e.target.value)} // 10000 → "10,000"
37
+ * />
38
+ *
39
+ * @example
40
+ * // 4) max + 천단위 콤마 같이 사용
41
+ * <NumberInput
42
+ * max={1000000}
43
+ * useThousandsSeparator
44
+ * placeholder="최대 1,000,000"
45
+ * onChange={(e) => console.log(e.target.value)}
46
+ * />
47
+ *
48
+ * @example
49
+ * // 5) React Hook Form 등과 함께 사용할 때
50
+ * // - 저장 시에는 콤마 제거 후 숫자로 변환하는 게 일반적
51
+ * <NumberInput
52
+ * useThousandsSeparator
53
+ * onChange={(e) => {
54
+ * const numeric = Number(e.target.value.replace(/,/g, ''));
55
+ * console.log(numeric);
56
+ * }}
57
+ * />
58
+ */
6
59
  const NumberInput = ({ max, useThousandsSeparator, onChange, ...props }) => {
7
60
  const handleChange = (e) => {
8
61
  const rawValue = e.target.value.replace(/,/g, '');
@@ -1,3 +1,98 @@
1
1
  import React from 'react';
2
2
  import { TextInputProps } from './TextInput.type';
3
+ /**
4
+ * 범용 TextInput 컴포넌트
5
+ *
6
+ * - maxLength: (조합 입력 중이 아닐 때) 최대 길이 제한
7
+ * - pattern: 정규식 문자열(매 입력마다 test 통과해야 반영)
8
+ * - disallowPattern: "허용" 정규식(현재 구현은 test가 false면 return)
9
+ * - validator: 값 검증 (boolean | error message string 반환 가능)
10
+ * - trimWhitespace: blur 시 앞뒤 공백 제거 후 onChange 호출
11
+ * - debounceMs / onDebouncedChange: 디바운스된 값 콜백
12
+ * - throttleMs / onThrottledChange: 스로틀된 값 콜백
13
+ *
14
+ * @example
15
+ * // 1) 기본 사용
16
+ * <TextInput
17
+ * placeholder="이름을 입력하세요"
18
+ * onChange={(e) => console.log(e.target.value)}
19
+ * />
20
+ *
21
+ * @example
22
+ * // 2) maxLength + trimWhitespace (blur 시 공백 제거)
23
+ * <TextInput
24
+ * maxLength={20}
25
+ * trimWhitespace
26
+ * placeholder="최대 20자, blur 시 공백 제거"
27
+ * onChange={(e) => console.log('change:', e.target.value)}
28
+ * />
29
+ *
30
+ * @example
31
+ * // 3) pattern(문자열)로 숫자만 허용
32
+ * // - 입력값이 정규식에 매번 매칭되어야 반영됨
33
+ * <TextInput
34
+ * pattern="^[0-9]*$"
35
+ * inputMode="numeric"
36
+ * placeholder="숫자만 입력"
37
+ * onChange={(e) => console.log(e.target.value)}
38
+ * />
39
+ *
40
+ * @example
41
+ * // 4) disallowPattern(정규식)로 "허용 규칙" 적용 (현재 구현 기준)
42
+ * // - test가 false면 반영되지 않음
43
+ * // - 예: 영문/숫자/언더스코어만 허용
44
+ * <TextInput
45
+ * disallowPattern={/^[a-zA-Z0-9_]*$/}
46
+ * placeholder="영문/숫자/_ 만"
47
+ * onChange={(e) => console.log(e.target.value)}
48
+ * />
49
+ *
50
+ * @example
51
+ * // 5) validator + onValidate
52
+ * // - validator가 false면 onChange가 호출되지 않음
53
+ * // - 문자열 반환 시 그 문자열이 error로 넘어감
54
+ * <TextInput
55
+ * placeholder="이메일"
56
+ * validator={(v) => {
57
+ * if (!v) return true; // 빈 값은 통과(원하면 false로 바꿔도 됨)
58
+ * const ok = /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(v);
59
+ * return ok || '이메일 형식이 아니에요';
60
+ * }}
61
+ * onValidate={(isValid, error) => {
62
+ * console.log('valid:', isValid, 'error:', error);
63
+ * }}
64
+ * onChange={(e) => console.log(e.target.value)}
65
+ * />
66
+ *
67
+ * @example
68
+ * // 6) debounce: 입력은 즉시 반영(onChange), 서버검색/자동완성은 디바운스로 처리
69
+ * <TextInput
70
+ * placeholder="검색어 입력"
71
+ * debounceMs={300}
72
+ * onDebouncedChange={(value) => {
73
+ * // fetch(`/api/search?q=${encodeURIComponent(value)}`)
74
+ * console.log('debounced:', value);
75
+ * }}
76
+ * onChange={(e) => console.log('immediate:', e.target.value)}
77
+ * />
78
+ *
79
+ * @example
80
+ * // 7) throttle: 스크롤/리사이즈처럼 자주 쏘는 이벤트에 유용한 패턴
81
+ * <TextInput
82
+ * placeholder="입력값 로깅(최대 200ms에 1번)"
83
+ * throttleMs={200}
84
+ * onThrottledChange={(value) => console.log('throttled:', value)}
85
+ * onChange={(e) => console.log('immediate:', e.target.value)}
86
+ * />
87
+ *
88
+ * @example
89
+ * // 8) IME(한글/일본어) 조합 이벤트 처리 예시
90
+ * <TextInput
91
+ * placeholder="한글 입력"
92
+ * maxLength={10}
93
+ * onCompositionStart={() => console.log('compose start')}
94
+ * onCompositionEnd={() => console.log('compose end')}
95
+ * onChange={(e) => console.log(e.target.value)}
96
+ * />
97
+ */
3
98
  export declare const TextInput: React.ForwardRefExoticComponent<TextInputProps & React.RefAttributes<HTMLInputElement>>;
@@ -18,6 +18,101 @@ function mergeRefs(...refs) {
18
18
  });
19
19
  };
20
20
  }
21
+ /**
22
+ * 범용 TextInput 컴포넌트
23
+ *
24
+ * - maxLength: (조합 입력 중이 아닐 때) 최대 길이 제한
25
+ * - pattern: 정규식 문자열(매 입력마다 test 통과해야 반영)
26
+ * - disallowPattern: "허용" 정규식(현재 구현은 test가 false면 return)
27
+ * - validator: 값 검증 (boolean | error message string 반환 가능)
28
+ * - trimWhitespace: blur 시 앞뒤 공백 제거 후 onChange 호출
29
+ * - debounceMs / onDebouncedChange: 디바운스된 값 콜백
30
+ * - throttleMs / onThrottledChange: 스로틀된 값 콜백
31
+ *
32
+ * @example
33
+ * // 1) 기본 사용
34
+ * <TextInput
35
+ * placeholder="이름을 입력하세요"
36
+ * onChange={(e) => console.log(e.target.value)}
37
+ * />
38
+ *
39
+ * @example
40
+ * // 2) maxLength + trimWhitespace (blur 시 공백 제거)
41
+ * <TextInput
42
+ * maxLength={20}
43
+ * trimWhitespace
44
+ * placeholder="최대 20자, blur 시 공백 제거"
45
+ * onChange={(e) => console.log('change:', e.target.value)}
46
+ * />
47
+ *
48
+ * @example
49
+ * // 3) pattern(문자열)로 숫자만 허용
50
+ * // - 입력값이 정규식에 매번 매칭되어야 반영됨
51
+ * <TextInput
52
+ * pattern="^[0-9]*$"
53
+ * inputMode="numeric"
54
+ * placeholder="숫자만 입력"
55
+ * onChange={(e) => console.log(e.target.value)}
56
+ * />
57
+ *
58
+ * @example
59
+ * // 4) disallowPattern(정규식)로 "허용 규칙" 적용 (현재 구현 기준)
60
+ * // - test가 false면 반영되지 않음
61
+ * // - 예: 영문/숫자/언더스코어만 허용
62
+ * <TextInput
63
+ * disallowPattern={/^[a-zA-Z0-9_]*$/}
64
+ * placeholder="영문/숫자/_ 만"
65
+ * onChange={(e) => console.log(e.target.value)}
66
+ * />
67
+ *
68
+ * @example
69
+ * // 5) validator + onValidate
70
+ * // - validator가 false면 onChange가 호출되지 않음
71
+ * // - 문자열 반환 시 그 문자열이 error로 넘어감
72
+ * <TextInput
73
+ * placeholder="이메일"
74
+ * validator={(v) => {
75
+ * if (!v) return true; // 빈 값은 통과(원하면 false로 바꿔도 됨)
76
+ * const ok = /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(v);
77
+ * return ok || '이메일 형식이 아니에요';
78
+ * }}
79
+ * onValidate={(isValid, error) => {
80
+ * console.log('valid:', isValid, 'error:', error);
81
+ * }}
82
+ * onChange={(e) => console.log(e.target.value)}
83
+ * />
84
+ *
85
+ * @example
86
+ * // 6) debounce: 입력은 즉시 반영(onChange), 서버검색/자동완성은 디바운스로 처리
87
+ * <TextInput
88
+ * placeholder="검색어 입력"
89
+ * debounceMs={300}
90
+ * onDebouncedChange={(value) => {
91
+ * // fetch(`/api/search?q=${encodeURIComponent(value)}`)
92
+ * console.log('debounced:', value);
93
+ * }}
94
+ * onChange={(e) => console.log('immediate:', e.target.value)}
95
+ * />
96
+ *
97
+ * @example
98
+ * // 7) throttle: 스크롤/리사이즈처럼 자주 쏘는 이벤트에 유용한 패턴
99
+ * <TextInput
100
+ * placeholder="입력값 로깅(최대 200ms에 1번)"
101
+ * throttleMs={200}
102
+ * onThrottledChange={(value) => console.log('throttled:', value)}
103
+ * onChange={(e) => console.log('immediate:', e.target.value)}
104
+ * />
105
+ *
106
+ * @example
107
+ * // 8) IME(한글/일본어) 조합 이벤트 처리 예시
108
+ * <TextInput
109
+ * placeholder="한글 입력"
110
+ * maxLength={10}
111
+ * onCompositionStart={() => console.log('compose start')}
112
+ * onCompositionEnd={() => console.log('compose end')}
113
+ * onChange={(e) => console.log(e.target.value)}
114
+ * />
115
+ */
21
116
  const TextInput = react.forwardRef(({ maxLength, onChange, pattern, onValidate, validator, onCompositionStart, onCompositionEnd, disallowPattern, trimWhitespace, debounceMs, throttleMs, onDebouncedChange, onThrottledChange, onBlur, ...props }, ref) => {
22
117
  const [isComposing, setIsComposing] = react.useState(false);
23
118
  const innerRef = react.useRef(null);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jy-headless",
3
- "version": "0.3.14",
3
+ "version": "0.3.16",
4
4
  "description": "A lightweight and customizable headless UI library for React components",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.js",