@uniai-fe/uds-primitives 0.3.3 → 0.3.5

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.
@@ -0,0 +1,372 @@
1
+ import type * as React from "react";
2
+
3
+ /**
4
+ * Carousel Types; 방향 타입
5
+ * @typedef {"prev" | "next"} CarouselDirection
6
+ */
7
+ export type CarouselDirection = "prev" | "next";
8
+
9
+ /**
10
+ * Carousel Types; 버튼 배치 타입
11
+ * @typedef {"outside" | "inside"} CarouselButtonPlacement
12
+ */
13
+ export type CarouselButtonPlacement = "outside" | "inside";
14
+
15
+ /**
16
+ * Carousel Types; Hook 옵션
17
+ * @property {boolean} [optional] true면 Provider 미존재 시 null 반환
18
+ */
19
+ export interface CarouselHookOptions {
20
+ /**
21
+ * true면 Provider 미존재 시 null 반환
22
+ */
23
+ optional?: boolean;
24
+ }
25
+
26
+ /**
27
+ * Carousel Types; Hook optional 반환 강제 옵션
28
+ * @property {true} optional Provider 미존재 시 null 반환을 허용하는 옵션
29
+ */
30
+ export interface CarouselHookOptionalOptions {
31
+ /**
32
+ * Provider 미존재 시 null 반환을 허용하는 옵션
33
+ */
34
+ optional: true;
35
+ }
36
+
37
+ /**
38
+ * Carousel Types; Provider props
39
+ * @property {React.ReactNode} children Carousel 하위 콘텐츠
40
+ * @property {number} [buttonMoveCount=1] Prev/Next 기본 viewport page 이동 개수
41
+ * @property {number} [visibleCount] viewport 내 표시 item 개수 고정값
42
+ * @property {(index: number) => void} [onIndexChange] 현재 index 변경 콜백
43
+ */
44
+ export interface CarouselProviderProps {
45
+ /**
46
+ * Carousel 하위 콘텐츠
47
+ */
48
+ children: React.ReactNode;
49
+ /**
50
+ * Prev/Next 기본 viewport page 이동 개수
51
+ */
52
+ buttonMoveCount?: number;
53
+ /**
54
+ * viewport 내 표시 item 개수 고정값
55
+ */
56
+ visibleCount?: number;
57
+ /**
58
+ * 현재 index 변경 콜백
59
+ */
60
+ onIndexChange?: (index: number) => void;
61
+ }
62
+
63
+ /**
64
+ * Carousel Types; Provider Controller 파라미터
65
+ * @property {number} [buttonMoveCount] Prev/Next 기본 viewport page 이동 개수
66
+ * @property {number} [visibleCount] viewport 내 표시 item 개수 고정값
67
+ * @property {(index: number) => void} [onIndexChange] 현재 index 변경 콜백
68
+ */
69
+ export interface CarouselProviderControllerParams {
70
+ /**
71
+ * Prev/Next 기본 viewport page 이동 개수
72
+ */
73
+ buttonMoveCount?: number;
74
+ /**
75
+ * viewport 내 표시 item 개수 고정값
76
+ */
77
+ visibleCount?: number;
78
+ /**
79
+ * 현재 index 변경 콜백
80
+ */
81
+ onIndexChange?: (index: number) => void;
82
+ }
83
+
84
+ /**
85
+ * Carousel Types; Context 값
86
+ * @property {React.RefObject<HTMLDivElement | null>} viewportRef viewport ref
87
+ * @property {React.RefObject<HTMLUListElement | null>} trackRef track ref
88
+ * @property {() => void} onPrev 이전 이동 핸들러
89
+ * @property {() => void} onNext 다음 이동 핸들러
90
+ * @property {(count: number) => void} onPrevBy count 단위 이전 이동
91
+ * @property {(count: number) => void} onNextBy count 단위 다음 이동
92
+ * @property {(count: number) => void} moveBy count 단위 이동
93
+ * @property {(index: number) => void} moveTo 지정 index 이동
94
+ * @property {(count: number) => void} registerItemCount 아이템 개수 등록
95
+ * @property {number} itemCount 총 item 개수
96
+ * @property {number} visibleCount viewport 내 가시 item 개수
97
+ * @property {number} currentIndex 현재 index
98
+ * @property {number} focusIndex 포커스 index
99
+ * @property {number} maxIndex 이동 가능한 마지막 index
100
+ * @property {boolean} isReachStart 시작 지점 도달 여부
101
+ * @property {boolean} isReachEnd 마지막 지점 도달 여부
102
+ */
103
+ export interface CarouselContextValue {
104
+ /**
105
+ * viewport ref
106
+ */
107
+ viewportRef: React.RefObject<HTMLDivElement | null>;
108
+ /**
109
+ * track ref
110
+ */
111
+ trackRef: React.RefObject<HTMLUListElement | null>;
112
+ /**
113
+ * 이전 이동 핸들러
114
+ */
115
+ onPrev: () => void;
116
+ /**
117
+ * 다음 이동 핸들러
118
+ */
119
+ onNext: () => void;
120
+ /**
121
+ * count 단위 이전 이동
122
+ */
123
+ onPrevBy: (count: number) => void;
124
+ /**
125
+ * count 단위 다음 이동
126
+ */
127
+ onNextBy: (count: number) => void;
128
+ /**
129
+ * count 단위 이동
130
+ */
131
+ moveBy: (count: number) => void;
132
+ /**
133
+ * 지정 index 이동
134
+ */
135
+ moveTo: (index: number) => void;
136
+ /**
137
+ * 아이템 개수 등록
138
+ */
139
+ registerItemCount: (count: number) => void;
140
+ /**
141
+ * 총 item 개수
142
+ */
143
+ itemCount: number;
144
+ /**
145
+ * viewport 내 가시 item 개수
146
+ */
147
+ visibleCount: number;
148
+ /**
149
+ * 현재 index
150
+ */
151
+ currentIndex: number;
152
+ /**
153
+ * 포커스 index
154
+ */
155
+ focusIndex: number;
156
+ /**
157
+ * 이동 가능한 마지막 index
158
+ */
159
+ maxIndex: number;
160
+ /**
161
+ * 시작 지점 도달 여부
162
+ */
163
+ isReachStart: boolean;
164
+ /**
165
+ * 마지막 지점 도달 여부
166
+ */
167
+ isReachEnd: boolean;
168
+ }
169
+
170
+ /**
171
+ * Carousel Types; Container props
172
+ * @property {React.ReactNode} children Carousel 콘텐츠
173
+ * @property {boolean} [activeButton=true] Prev/Next 자동 렌더링 여부
174
+ * @property {string} [className] 컨테이너 className
175
+ * @property {CarouselButtonPlacement} [buttonPlacement="outside"] 버튼 배치 방식
176
+ * @property {number} [buttonMoveCount=1] 버튼 클릭 시 이동 viewport page 개수
177
+ * @property {number} [visibleCount] viewport 내 표시 item 개수 고정값
178
+ * @property {(index: number) => void} [onIndexChange] 현재 index 변경 콜백
179
+ */
180
+ export interface CarouselContainerProps {
181
+ /**
182
+ * Carousel 콘텐츠
183
+ */
184
+ children: React.ReactNode;
185
+ /**
186
+ * Prev/Next 버튼 자동 렌더링 여부
187
+ */
188
+ activeButton?: boolean;
189
+ /**
190
+ * 컨테이너 className
191
+ */
192
+ className?: string;
193
+ /**
194
+ * 버튼 배치 방식
195
+ */
196
+ buttonPlacement?: CarouselButtonPlacement;
197
+ /**
198
+ * 버튼 클릭 시 이동 viewport page 개수
199
+ */
200
+ buttonMoveCount?: number;
201
+ /**
202
+ * viewport 내 표시 item 개수 고정값
203
+ */
204
+ visibleCount?: number;
205
+ /**
206
+ * 현재 index 변경 콜백
207
+ */
208
+ onIndexChange?: (index: number) => void;
209
+ }
210
+
211
+ /**
212
+ * Carousel Types; Control props
213
+ * @property {React.ReactNode} children 버튼 사이에 배치할 콘텐츠
214
+ * @property {string} [className] 컨트롤 래퍼 className
215
+ * @property {CarouselButtonPlacement} [buttonPlacement="outside"] 버튼 배치 방식
216
+ * @property {number} [buttonMoveCount=1] 버튼 클릭 시 이동 viewport page 개수
217
+ */
218
+ export interface CarouselControlProps {
219
+ /**
220
+ * 버튼 사이에 배치할 콘텐츠
221
+ */
222
+ children: React.ReactNode;
223
+ /**
224
+ * 컨트롤 래퍼 className
225
+ */
226
+ className?: string;
227
+ /**
228
+ * 버튼 배치 방식
229
+ */
230
+ buttonPlacement?: CarouselButtonPlacement;
231
+ /**
232
+ * 버튼 클릭 시 이동 viewport page 개수
233
+ */
234
+ buttonMoveCount?: number;
235
+ }
236
+
237
+ /**
238
+ * Carousel Types; Track props
239
+ * @property {React.ReactNode} children track item 목록
240
+ * @property {string} [className] viewport className
241
+ * @property {string} [trackClassName] track className
242
+ * @property {string} [itemClassName] 각 item에 공통으로 부여할 className
243
+ */
244
+ export interface CarouselTrackProps extends Omit<
245
+ React.ComponentPropsWithoutRef<"div">,
246
+ "children"
247
+ > {
248
+ /**
249
+ * track item 목록
250
+ */
251
+ children: React.ReactNode;
252
+ /**
253
+ * viewport className
254
+ */
255
+ className?: string;
256
+ /**
257
+ * track className
258
+ */
259
+ trackClassName?: string;
260
+ /**
261
+ * 각 item에 공통으로 부여할 className
262
+ */
263
+ itemClassName?: string;
264
+ }
265
+
266
+ /**
267
+ * Carousel Types; Button 공통 시각 props
268
+ * @property {React.ReactNode} [children] 버튼 콘텐츠 완전 교체 슬롯
269
+ * @property {React.ReactNode} [label] 기본 아이콘 조합에서 사용할 레이블
270
+ * @property {string} [className] 버튼 className
271
+ * @property {boolean} [disabled] 비활성 상태
272
+ */
273
+ export interface CarouselButtonVisualProps extends Omit<
274
+ React.ComponentPropsWithoutRef<"button">,
275
+ "children" | "onClick"
276
+ > {
277
+ /**
278
+ * 버튼 콘텐츠 완전 교체 슬롯
279
+ */
280
+ children?: React.ReactNode;
281
+ /**
282
+ * 기본 아이콘 조합에서 사용할 레이블
283
+ */
284
+ label?: React.ReactNode;
285
+ /**
286
+ * 버튼 className
287
+ */
288
+ className?: string;
289
+ /**
290
+ * 비활성 상태
291
+ */
292
+ disabled?: boolean;
293
+ }
294
+
295
+ /**
296
+ * Carousel Types; 이동 버튼 아이콘 래퍼 props
297
+ * @property {string} [className] 아이콘 래퍼 className
298
+ * @property {React.ReactNode} children 아이콘 래퍼 내부 콘텐츠
299
+ */
300
+ export interface CarouselButtonIconFigureProps {
301
+ /**
302
+ * 아이콘 래퍼 className
303
+ */
304
+ className?: string;
305
+ /**
306
+ * 아이콘 래퍼 내부 콘텐츠
307
+ */
308
+ children: React.ReactNode;
309
+ }
310
+
311
+ /**
312
+ * Carousel Types; Base button props
313
+ * @extends CarouselButtonVisualProps
314
+ * @property {React.ReactNode} [children] 버튼 콘텐츠 완전 교체 슬롯
315
+ * @property {React.ReactNode} [label] 기본 아이콘 조합에서 사용할 레이블
316
+ * @property {string} [className] 버튼 className
317
+ * @property {boolean} [disabled] 비활성 상태
318
+ * @property {CarouselDirection} direction 버튼 방향
319
+ * @property {() => void} onMove 버튼 클릭 이동 핸들러
320
+ */
321
+ export interface CarouselBaseButtonProps extends CarouselButtonVisualProps {
322
+ /**
323
+ * 버튼 방향
324
+ */
325
+ direction: CarouselDirection;
326
+ /**
327
+ * 버튼 클릭 이동 핸들러
328
+ */
329
+ onMove: () => void;
330
+ }
331
+
332
+ /**
333
+ * Carousel Types; Prev button props
334
+ * @extends CarouselButtonVisualProps
335
+ * @property {React.ReactNode} [children] 버튼 콘텐츠 완전 교체 슬롯
336
+ * @property {React.ReactNode} [label] 기본 아이콘 조합에서 사용할 레이블
337
+ * @property {string} [className] 버튼 className
338
+ * @property {boolean} [disabled] 비활성 상태
339
+ * @property {() => void} [onPrev] 이전 이동 핸들러
340
+ * @property {number} [moveCount=1] 버튼 클릭 시 이동 viewport page 개수
341
+ */
342
+ export interface CarouselPrevButtonProps extends CarouselButtonVisualProps {
343
+ /**
344
+ * 이전 이동 핸들러
345
+ */
346
+ onPrev?: () => void;
347
+ /**
348
+ * 버튼 클릭 시 이동 viewport page 개수
349
+ */
350
+ moveCount?: number;
351
+ }
352
+
353
+ /**
354
+ * Carousel Types; Next button props
355
+ * @extends CarouselButtonVisualProps
356
+ * @property {React.ReactNode} [children] 버튼 콘텐츠 완전 교체 슬롯
357
+ * @property {React.ReactNode} [label] 기본 아이콘 조합에서 사용할 레이블
358
+ * @property {string} [className] 버튼 className
359
+ * @property {boolean} [disabled] 비활성 상태
360
+ * @property {() => void} [onNext] 다음 이동 핸들러
361
+ * @property {number} [moveCount=1] 버튼 클릭 시 이동 viewport page 개수
362
+ */
363
+ export interface CarouselNextButtonProps extends CarouselButtonVisualProps {
364
+ /**
365
+ * 다음 이동 핸들러
366
+ */
367
+ onNext?: () => void;
368
+ /**
369
+ * 버튼 클릭 시 이동 viewport page 개수
370
+ */
371
+ moveCount?: number;
372
+ }
@@ -0,0 +1 @@
1
+ export { normalizePositiveInteger, resolveIndexFromScrollLeft } from "./math";
@@ -0,0 +1,62 @@
1
+ /**
2
+ * Carousel Utils; 양수 정수 정규화 유틸
3
+ * @param {number | undefined} value 입력값
4
+ * @param {number} fallback 유효하지 않을 때 사용할 기본값
5
+ * @returns {number} 1 이상의 정수
6
+ * @example
7
+ * normalizePositiveInteger(undefined, 1); // 1
8
+ * normalizePositiveInteger(2.9, 1); // 2
9
+ */
10
+ export const normalizePositiveInteger = (
11
+ value: number | undefined,
12
+ fallback: number,
13
+ ): number => {
14
+ if (typeof value !== "number" || Number.isNaN(value)) {
15
+ return fallback;
16
+ }
17
+
18
+ return Math.max(1, Math.floor(value));
19
+ };
20
+
21
+ /**
22
+ * Carousel Utils; scrollLeft 기준 현재 index 계산 유틸
23
+ * @param {object} params 계산 파라미터
24
+ * @param {HTMLCollection} params.panels track children 목록
25
+ * @param {number} params.scrollLeft viewport scrollLeft
26
+ * @param {number} params.maxIndex 최대 index
27
+ * @returns {number} 계산된 현재 index
28
+ * @example
29
+ * const index = resolveIndexFromScrollLeft({
30
+ * panels: track.children,
31
+ * scrollLeft: viewport.scrollLeft,
32
+ * maxIndex: 5,
33
+ * });
34
+ */
35
+ export const resolveIndexFromScrollLeft = ({
36
+ panels,
37
+ scrollLeft,
38
+ maxIndex,
39
+ }: {
40
+ panels: HTMLCollection;
41
+ scrollLeft: number;
42
+ maxIndex: number;
43
+ }): number => {
44
+ if (!panels.length) {
45
+ return 0;
46
+ }
47
+
48
+ let resolvedIndex = 0;
49
+ const threshold = 1;
50
+
51
+ // viewport 좌측 edge를 기준으로 마지막으로 지나온 panel index를 선택한다.
52
+ for (let index = 0; index < panels.length; index += 1) {
53
+ const panel = panels[index] as HTMLElement;
54
+ if (panel.offsetLeft <= scrollLeft + threshold) {
55
+ resolvedIndex = index;
56
+ continue;
57
+ }
58
+ break;
59
+ }
60
+
61
+ return Math.min(resolvedIndex, maxIndex);
62
+ };
package/src/index.scss CHANGED
@@ -2,6 +2,7 @@
2
2
  @use "./components/badge";
3
3
  @use "./components/button";
4
4
  @use "./components/calendar";
5
+ @use "./components/carousel";
5
6
  @use "./components/checkbox";
6
7
  @use "./components/chip";
7
8
  @use "./components/divider";
package/src/index.tsx CHANGED
@@ -7,6 +7,7 @@ export * from "./components/alternate";
7
7
  export * from "./components/badge";
8
8
  export * from "./components/button";
9
9
  export * from "./components/calendar";
10
+ export * from "./components/carousel";
10
11
  export * from "./components/checkbox";
11
12
  export * from "./components/chip";
12
13
  export * from "./components/divider";