@jk-core/components 1.1.17 → 1.1.19

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 (91) hide show
  1. package/README.md +73 -73
  2. package/dist/index.js +1299 -997
  3. package/dist/index.js.map +1 -1
  4. package/dist/index.umd.cjs +8 -8
  5. package/dist/index.umd.cjs.map +1 -1
  6. package/dist/src/common/Carousel/index.d.ts +15 -0
  7. package/dist/src/common/RollingBanner/index.d.ts +11 -0
  8. package/dist/src/index.d.ts +3 -1
  9. package/dist/src/utils/ts/formatMoney.d.ts +1 -1
  10. package/package.json +99 -99
  11. package/src/Calendar/Calendar.module.scss +213 -213
  12. package/src/Calendar/RangeCalendar.tsx +125 -125
  13. package/src/Calendar/ScrollCalendar.module.scss +214 -214
  14. package/src/Calendar/ScrollCalendar.tsx +124 -124
  15. package/src/Calendar/SingleCalendar.tsx +121 -121
  16. package/src/Calendar/components/DateLabel/DateLabel.module.scss +89 -89
  17. package/src/Calendar/components/DateLabel/index.tsx +91 -91
  18. package/src/Calendar/components/DayTile/DayTile.module.scss +117 -117
  19. package/src/Calendar/components/DayTile/index.tsx +108 -108
  20. package/src/Calendar/components/MonthTile/MonthTile.module.scss +59 -59
  21. package/src/Calendar/components/MonthTile/index.tsx +50 -50
  22. package/src/Calendar/components/ViewSelector/ViewSelector.module.scss +48 -48
  23. package/src/Calendar/components/ViewSelector/index.tsx +49 -49
  24. package/src/Calendar/components/YearTile/YearTile.module.scss +85 -85
  25. package/src/Calendar/components/YearTile/index.tsx +65 -65
  26. package/src/Calendar/hooks/useCalendarNav.ts +83 -83
  27. package/src/Calendar/hooks/useDateSelect.ts +54 -54
  28. package/src/Calendar/index.scss +189 -189
  29. package/src/Calendar/index.tsx +66 -66
  30. package/src/Calendar/type.ts +3 -3
  31. package/src/Calendar/utils/getWeeksInMonth.ts +45 -45
  32. package/src/Calendar/utils/isInRange.ts +8 -8
  33. package/src/Calendar/utils/isSameDay.ts +21 -21
  34. package/src/assets/arrow.svg +11 -11
  35. package/src/assets/close.svg +15 -15
  36. package/src/assets/drop-arrow.svg +3 -3
  37. package/src/common/Accordion/Accordion.module.scss +53 -53
  38. package/src/common/Accordion/arrow-down.svg +3 -3
  39. package/src/common/Accordion/arrow-up.svg +3 -3
  40. package/src/common/Accordion/index.tsx +54 -54
  41. package/src/common/Breadcrumbs/Breadcrumbs.module.scss +46 -46
  42. package/src/common/Breadcrumbs/home.svg +5 -5
  43. package/src/common/Breadcrumbs/index.tsx +82 -82
  44. package/src/common/Button/Button.module.scss +127 -127
  45. package/src/common/Button/index.tsx +60 -60
  46. package/src/common/Card/Card.module.scss +28 -28
  47. package/src/common/Card/index.tsx +19 -19
  48. package/src/common/Carousel/Carousel.module.scss +222 -0
  49. package/src/common/Carousel/index.tsx +411 -0
  50. package/src/common/Divider/Divider.module.scss +101 -101
  51. package/src/common/Divider/index.tsx +24 -24
  52. package/src/common/DropDown/DropDown.module.scss +135 -135
  53. package/src/common/DropDown/List.tsx +156 -156
  54. package/src/common/DropDown/arrow-down.svg +3 -3
  55. package/src/common/DropDown/index.tsx +108 -108
  56. package/src/common/DropDown/search.svg +4 -4
  57. package/src/common/Pagination/Pagination.module.scss +210 -210
  58. package/src/common/Pagination/arrow-left.svg +11 -11
  59. package/src/common/Pagination/arrow-right.svg +11 -11
  60. package/src/common/Pagination/index.tsx +156 -156
  61. package/src/common/RollingBanner/RollingBanner.module.scss +126 -0
  62. package/src/common/RollingBanner/index.tsx +140 -0
  63. package/src/common/SegmentButton/SegmentButton.module.scss +46 -45
  64. package/src/common/SegmentButton/index.tsx +79 -79
  65. package/src/common/Skeleton/Skeleton.module.scss +80 -80
  66. package/src/common/Skeleton/index.tsx +47 -47
  67. package/src/common/SwitchButton/SwitchButton.module.scss +65 -65
  68. package/src/common/SwitchButton/index.tsx +57 -57
  69. package/src/common/Table/Table.module.scss +70 -70
  70. package/src/common/Table/index.tsx +128 -128
  71. package/src/index.scss +1 -1
  72. package/src/index.tsx +24 -22
  73. package/src/styles/color.scss +346 -346
  74. package/src/styles/font-face.scss +18 -18
  75. package/src/styles/font.scss +49 -49
  76. package/src/styles/mediaQuery.scss +22 -22
  77. package/src/styles/scrollbar.scss +71 -71
  78. package/src/svg.d.ts +6 -6
  79. package/src/utils/styles/mediaQuery.scss +22 -22
  80. package/src/utils/ts/allowDecimal.ts +4 -4
  81. package/src/utils/ts/autoHypen.ts +33 -33
  82. package/src/utils/ts/calculateMax.ts +24 -24
  83. package/src/utils/ts/checkIsMobilePlatform.ts +15 -15
  84. package/src/utils/ts/formatFileSize.ts +16 -16
  85. package/src/utils/ts/formatMoney.ts +16 -16
  86. package/src/utils/ts/gradientRatio.ts +61 -61
  87. package/src/utils/ts/kiloToMega.ts +30 -30
  88. package/src/utils/ts/maskingPhone.ts +8 -8
  89. package/src/utils/ts/toQueryString.ts +7 -7
  90. package/src/utils/ts/valueAsNumber.ts +15 -15
  91. package/src/vite-env.d.ts +2 -2
@@ -1,12 +1,12 @@
1
- <!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
2
-
3
- <!-- Uploaded to: SVG Repo, www.svgrepo.com, Transformed by: SVG Repo Mixer Tools -->
4
- <svg width="30px" height="30px" viewBox="0 0 24 24" fill="#000000" xmlns="http://www.w3.org/2000/svg">
5
-
6
- <g id="SVGRepo_bgCarrier"/>
7
-
8
- <g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"/>
9
-
10
- <g id="SVGRepo_iconCarrier"> <path d="M7.82054 20.7313C8.21107 21.1218 8.84423 21.1218 9.23476 20.7313L15.8792 14.0868C17.0505 12.9155 17.0508 11.0167 15.88 9.84497L9.3097 3.26958C8.91918 2.87905 8.28601 2.87905 7.89549 3.26958C7.50497 3.6601 7.50497 4.29327 7.89549 4.68379L14.4675 11.2558C14.8581 11.6464 14.8581 12.2795 14.4675 12.67L7.82054 19.317C7.43002 19.7076 7.43002 20.3407 7.82054 20.7313Z"/> </g>
11
-
1
+ <!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
2
+
3
+ <!-- Uploaded to: SVG Repo, www.svgrepo.com, Transformed by: SVG Repo Mixer Tools -->
4
+ <svg width="30px" height="30px" viewBox="0 0 24 24" fill="#000000" xmlns="http://www.w3.org/2000/svg">
5
+
6
+ <g id="SVGRepo_bgCarrier"/>
7
+
8
+ <g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"/>
9
+
10
+ <g id="SVGRepo_iconCarrier"> <path d="M7.82054 20.7313C8.21107 21.1218 8.84423 21.1218 9.23476 20.7313L15.8792 14.0868C17.0505 12.9155 17.0508 11.0167 15.88 9.84497L9.3097 3.26958C8.91918 2.87905 8.28601 2.87905 7.89549 3.26958C7.50497 3.6601 7.50497 4.29327 7.89549 4.68379L14.4675 11.2558C14.8581 11.6464 14.8581 12.2795 14.4675 12.67L7.82054 19.317C7.43002 19.7076 7.43002 20.3407 7.82054 20.7313Z"/> </g>
11
+
12
12
  </svg>
@@ -1,156 +1,156 @@
1
-
2
- import { useMemo, useState } from 'react';
3
- import LeftArrowIcon from './arrow-left.svg';
4
- import RightArrowIcon from './arrow-right.svg';
5
- import styles from './Pagination.module.scss';
6
-
7
- interface PaginationProps {
8
- /**
9
- * 총 페이지 수
10
- */
11
- totalPage: number;
12
- /**
13
- * 현재 페이지
14
- */
15
- currentPage: number;
16
-
17
- /**
18
- * 페이지 크기 (기본값: false)
19
- * true로 설정하면 보여지는 페이지의 개수가 5개에서 7개로 늘어납니다.
20
- */
21
- large?: boolean;
22
- /**
23
- * 페이지 클릭 핸들러
24
- */
25
- onPageClick: (page: number) => void;
26
- }
27
-
28
- export default function Pagination({ totalPage, currentPage, onPageClick, large = false }: PaginationProps) {
29
- const [animation, setAnimation] = useState<'left' | 'right' | 'doubleRight' | 'doubleLeft' | 'tripleRight' | 'tripleLeft' | ''>('');
30
- const [selectedPage, setSelectedPage] = useState(currentPage || 1);
31
- const weight = useMemo(() => (large ? 2 : 0), [large]);
32
-
33
- const pageArray = useMemo(() => {
34
- if (!totalPage) return Array.from({ length: 9 + weight }).map((_, i) => (i === 2 ? 1 : null));
35
-
36
- return Array.from({ length: 9 + weight }).map((_, i) => {
37
- const pageIndex = selectedPage + i - 2 - (weight / 2) ;
38
-
39
- if (pageIndex < 1 || pageIndex > totalPage) {
40
- return null;
41
- }
42
- return pageIndex;
43
- });
44
- }, [selectedPage, totalPage, weight]);
45
-
46
- const handlePageClick = (page: number) => {
47
- if (page > totalPage || page < 1) return;
48
- onPageClick(page);
49
- if (page === null) return;
50
- const diff = page - selectedPage;
51
-
52
- switch (diff) {
53
- case 1:
54
- setAnimation('right');
55
- break;
56
- case -1:
57
- setAnimation('left');
58
- break;
59
- case 2:
60
- setAnimation('doubleRight');
61
- break;
62
- case -2:
63
- setAnimation('doubleLeft');
64
- break;
65
- case 3:
66
- setAnimation('tripleRight');
67
- break;
68
- case -3:
69
- setAnimation('tripleLeft');
70
- break;
71
- default:
72
- setAnimation('');
73
- }
74
-
75
- setSelectedPage(page);
76
-
77
- setTimeout(() => {
78
- setAnimation('');
79
- }, 200);
80
- };
81
-
82
- return (
83
- <div className={styles.pagination}>
84
- <button
85
- className={`${styles.arrow} ${selectedPage <= 1 ? styles['arrow--disabled'] : '' }`}
86
- type="button"
87
- onClick={() => handlePageClick(selectedPage - 1)}
88
- >
89
- <LeftArrowIcon />
90
- </button>
91
- <div
92
- className={styles.slider}
93
- style={{ width: `${(8 + weight) * 40}px` }}
94
- >
95
- <div
96
- className={styles.track}
97
- />
98
- <div className={styles.portal}>
99
- {selectedPage > (3 + weight / 2) && (
100
- < >
101
- <button
102
- className={styles.page}
103
- type="button"
104
- onClick={() => handlePageClick(1)}
105
- >1
106
- </button>
107
- <span className={styles.portal__ellipsis}>•••</span>
108
- </>
109
- )}
110
- </div>
111
- <div
112
- className={styles.pages}
113
- style={{ width: `${(5 + weight) * 40}px` }}
114
- >
115
- <div className={`${styles.pageWrapper} ${animation === 'left' ? styles.left : ''} ${styles[animation]}`}>
116
- {pageArray.map((page, index) => (
117
- <button
118
- className={`${styles.page} ${selectedPage === page ? styles['page--selected'] : ''}`}
119
- key={index}
120
- type="button"
121
- disabled={page === null}
122
- onClick={() => {
123
- if (page !== null) {
124
- handlePageClick(page);
125
- }
126
- }}
127
- >
128
- <span>{page}</span>
129
- </button>
130
- ))}
131
- </div>
132
- </div>
133
- <div className={styles.portal}>
134
- {selectedPage < totalPage - (2 + weight / 2) && (
135
- <>
136
- <span className={styles.portal__ellipsis}>•••</span>
137
- <button
138
- className={styles.page}
139
- type="button"
140
- onClick={() => handlePageClick(totalPage)}
141
- >{totalPage}
142
- </button>
143
- </>
144
- )}
145
- </div>
146
- </div>
147
- <button
148
- className={`${styles.arrow} ${selectedPage >= totalPage ? styles['arrow--disabled'] : ''}`}
149
- type="button"
150
- onClick={() => handlePageClick(selectedPage + 1)}
151
- >
152
- <RightArrowIcon />
153
- </button>
154
- </div>
155
- );
156
- }
1
+
2
+ import { useMemo, useState } from 'react';
3
+ import LeftArrowIcon from './arrow-left.svg';
4
+ import RightArrowIcon from './arrow-right.svg';
5
+ import styles from './Pagination.module.scss';
6
+
7
+ interface PaginationProps {
8
+ /**
9
+ * 총 페이지 수
10
+ */
11
+ totalPage: number;
12
+ /**
13
+ * 현재 페이지
14
+ */
15
+ currentPage: number;
16
+
17
+ /**
18
+ * 페이지 크기 (기본값: false)
19
+ * true로 설정하면 보여지는 페이지의 개수가 5개에서 7개로 늘어납니다.
20
+ */
21
+ large?: boolean;
22
+ /**
23
+ * 페이지 클릭 핸들러
24
+ */
25
+ onPageClick: (page: number) => void;
26
+ }
27
+
28
+ export default function Pagination({ totalPage, currentPage, onPageClick, large = false }: PaginationProps) {
29
+ const [animation, setAnimation] = useState<'left' | 'right' | 'doubleRight' | 'doubleLeft' | 'tripleRight' | 'tripleLeft' | ''>('');
30
+ const [selectedPage, setSelectedPage] = useState(currentPage || 1);
31
+ const weight = useMemo(() => (large ? 2 : 0), [large]);
32
+
33
+ const pageArray = useMemo(() => {
34
+ if (!totalPage) return Array.from({ length: 9 + weight }).map((_, i) => (i === 2 ? 1 : null));
35
+
36
+ return Array.from({ length: 9 + weight }).map((_, i) => {
37
+ const pageIndex = selectedPage + i - 2 - (weight / 2) ;
38
+
39
+ if (pageIndex < 1 || pageIndex > totalPage) {
40
+ return null;
41
+ }
42
+ return pageIndex;
43
+ });
44
+ }, [selectedPage, totalPage, weight]);
45
+
46
+ const handlePageClick = (page: number) => {
47
+ if (page > totalPage || page < 1) return;
48
+ onPageClick(page);
49
+ if (page === null) return;
50
+ const diff = page - selectedPage;
51
+
52
+ switch (diff) {
53
+ case 1:
54
+ setAnimation('right');
55
+ break;
56
+ case -1:
57
+ setAnimation('left');
58
+ break;
59
+ case 2:
60
+ setAnimation('doubleRight');
61
+ break;
62
+ case -2:
63
+ setAnimation('doubleLeft');
64
+ break;
65
+ case 3:
66
+ setAnimation('tripleRight');
67
+ break;
68
+ case -3:
69
+ setAnimation('tripleLeft');
70
+ break;
71
+ default:
72
+ setAnimation('');
73
+ }
74
+
75
+ setSelectedPage(page);
76
+
77
+ setTimeout(() => {
78
+ setAnimation('');
79
+ }, 200);
80
+ };
81
+
82
+ return (
83
+ <div className={styles.pagination}>
84
+ <button
85
+ className={`${styles.arrow} ${selectedPage <= 1 ? styles['arrow--disabled'] : '' }`}
86
+ type="button"
87
+ onClick={() => handlePageClick(selectedPage - 1)}
88
+ >
89
+ <LeftArrowIcon />
90
+ </button>
91
+ <div
92
+ className={styles.slider}
93
+ style={{ width: `${(8 + weight) * 40}px` }}
94
+ >
95
+ <div
96
+ className={styles.track}
97
+ />
98
+ <div className={styles.portal}>
99
+ {selectedPage > (3 + weight / 2) && (
100
+ < >
101
+ <button
102
+ className={styles.page}
103
+ type="button"
104
+ onClick={() => handlePageClick(1)}
105
+ >1
106
+ </button>
107
+ <span className={styles.portal__ellipsis}>•••</span>
108
+ </>
109
+ )}
110
+ </div>
111
+ <div
112
+ className={styles.pages}
113
+ style={{ width: `${(5 + weight) * 40}px` }}
114
+ >
115
+ <div className={`${styles.pageWrapper} ${animation === 'left' ? styles.left : ''} ${styles[animation]}`}>
116
+ {pageArray.map((page, index) => (
117
+ <button
118
+ className={`${styles.page} ${selectedPage === page ? styles['page--selected'] : ''}`}
119
+ key={index}
120
+ type="button"
121
+ disabled={page === null}
122
+ onClick={() => {
123
+ if (page !== null) {
124
+ handlePageClick(page);
125
+ }
126
+ }}
127
+ >
128
+ <span>{page}</span>
129
+ </button>
130
+ ))}
131
+ </div>
132
+ </div>
133
+ <div className={styles.portal}>
134
+ {selectedPage < totalPage - (2 + weight / 2) && (
135
+ <>
136
+ <span className={styles.portal__ellipsis}>•••</span>
137
+ <button
138
+ className={styles.page}
139
+ type="button"
140
+ onClick={() => handlePageClick(totalPage)}
141
+ >{totalPage}
142
+ </button>
143
+ </>
144
+ )}
145
+ </div>
146
+ </div>
147
+ <button
148
+ className={`${styles.arrow} ${selectedPage >= totalPage ? styles['arrow--disabled'] : ''}`}
149
+ type="button"
150
+ onClick={() => handlePageClick(selectedPage + 1)}
151
+ >
152
+ <RightArrowIcon />
153
+ </button>
154
+ </div>
155
+ );
156
+ }
@@ -0,0 +1,126 @@
1
+ @keyframes scroll {
2
+ 0% {
3
+ transform: translateX(0%);
4
+ }
5
+
6
+ 100% {
7
+ transform: translateX(-100%);
8
+ }
9
+ }
10
+
11
+ @keyframes scroll-column {
12
+ 0% {
13
+ transform: translateY(0%);
14
+ }
15
+
16
+ 100% {
17
+ transform: translateY(-100%);
18
+ }
19
+ }
20
+
21
+ .marquee-container {
22
+ overflow-x: hidden;
23
+ display: flex;
24
+ flex-direction: row;
25
+ position: relative;
26
+ width: 100%;
27
+
28
+ &--column {
29
+ overflow: hidden;
30
+ flex-direction: column;
31
+ height: 100%;
32
+ width: auto;
33
+ }
34
+ }
35
+
36
+ .overlay {
37
+ position: absolute;
38
+ width: 100%;
39
+ height: 100%;
40
+
41
+ @mixin gradient {
42
+ background: linear-gradient(to right, var(--gradient-color), rgb(255, 255, 255, 0));
43
+ }
44
+
45
+ @mixin gradientColumn {
46
+ background: linear-gradient(to bottom, var(--gradient-color), rgb(255, 255, 255, 0));
47
+ }
48
+
49
+ &::before,
50
+ &::after {
51
+ @include gradient;
52
+ content: "";
53
+ height: 100%;
54
+ position: absolute;
55
+ width: var(--gradient-width);
56
+ z-index: 2;
57
+ pointer-events: none;
58
+ touch-action: none;
59
+ }
60
+
61
+ &::after {
62
+ right: 0;
63
+ top: 0;
64
+ transform: rotateZ(180deg);
65
+ }
66
+
67
+ &::before {
68
+ left: 0;
69
+ top: 0;
70
+ }
71
+
72
+ // Column direction용 gradient
73
+ &--column {
74
+ &::before,
75
+ &::after {
76
+ @include gradientColumn;
77
+ width: 100%;
78
+ height: var(--gradient-width);
79
+ }
80
+
81
+ &::before {
82
+ left: 0;
83
+ top: 0;
84
+ transform: none;
85
+ }
86
+
87
+ &::after {
88
+ inset: auto auto 0 0;
89
+ transform: rotateZ(180deg);
90
+ }
91
+ }
92
+ }
93
+
94
+ .marquee {
95
+ flex: 0 0 auto;
96
+ width: auto;
97
+ z-index: 1;
98
+ display: flex;
99
+ align-items: center;
100
+ animation: scroll var(--duration, 10s) linear infinite;
101
+ animation-direction: var(--direction, normal);
102
+ flex-direction: var(--flex-direction, row);
103
+
104
+ // Column direction 전용 클래스
105
+ &--column {
106
+ flex-direction: column;
107
+ animation-name: scroll-column;
108
+ animation-duration: var(--duration, 10s);
109
+ animation-timing-function: linear;
110
+ animation-iteration-count: infinite;
111
+ animation-direction: var(--direction, normal);
112
+ }
113
+ }
114
+
115
+ .initial-child-container {
116
+ flex: 0 0 auto;
117
+ width: auto;
118
+ display: flex;
119
+ flex-flow: var(--flex-direction, row) nowrap;
120
+ flex-shrink: 0;
121
+ align-items: center;
122
+ }
123
+
124
+ .child {
125
+ transform: var(--transform);
126
+ }
@@ -0,0 +1,140 @@
1
+ import { Children, Fragment, useCallback, useEffect, useMemo, useRef, useState } from 'react';
2
+ import styles from './RollingBanner.module.scss';
3
+
4
+ interface RollingBannerProps {
5
+ speed?: number; // 속도 (픽셀/초)
6
+ reverse?: boolean; // 역방향 재생 여부
7
+ children?: React.ReactNode | React.ReactNode[];
8
+ gradientColor?: string;
9
+ gradient?: boolean;
10
+ gradientWidth?: number | string;
11
+ direction?: 'row' | 'column';
12
+ }
13
+
14
+ export default function RollingBanner({
15
+ speed = 50, gradient = true, gradientColor = 'white', gradientWidth = 200,
16
+ reverse = false, children, direction = 'row' }: RollingBannerProps) {
17
+ const containerRef = useRef<HTMLDivElement>(null);
18
+ const marqueeRef = useRef<HTMLDivElement>(null);
19
+ const [isMounted, setIsMounted] = useState(false);
20
+ const [marqueeWidth, setMarqueeWidth] = useState(0);
21
+ const [repeat, setRepeat] = useState(1);
22
+
23
+ const calculateWidth = useCallback(() => {
24
+ if (marqueeRef.current && containerRef.current) {
25
+ const containerRect = containerRef.current.getBoundingClientRect();
26
+ const marqueeRect = marqueeRef.current.getBoundingClientRect();
27
+
28
+ // direction에 따라 width 또는 height 계산
29
+ const containerSize = direction === 'row' ? containerRect.width : containerRect.height;
30
+ const marqueeSize = direction === 'row' ? marqueeRect.width : marqueeRect.height;
31
+
32
+ if (containerSize && marqueeSize) {
33
+ setRepeat(
34
+ marqueeSize < containerSize
35
+ ? Math.ceil(containerSize / marqueeSize)
36
+ : 1,
37
+ );
38
+ } else {
39
+ setRepeat(1);
40
+ }
41
+
42
+ setMarqueeWidth(marqueeSize);
43
+ }
44
+ }, [containerRef, direction]);
45
+
46
+ const duration = useMemo(() => {
47
+ const calculatedDuration = (marqueeWidth * repeat) / speed;
48
+ // duration이 0이거나 유효하지 않으면 기본값 사용
49
+ return calculatedDuration > 0 && Number.isFinite(calculatedDuration) ? calculatedDuration : 10;
50
+ }, [marqueeWidth, repeat, speed]);
51
+
52
+ useEffect(() => {
53
+ setIsMounted(true);
54
+ }, []);
55
+
56
+ useEffect(() => {
57
+ if (!isMounted) return;
58
+
59
+ calculateWidth();
60
+ if (marqueeRef.current && containerRef.current) {
61
+ const resizeObserver = new ResizeObserver(() => calculateWidth());
62
+ resizeObserver.observe(containerRef.current);
63
+ resizeObserver.observe(marqueeRef.current);
64
+ return () => {
65
+ if (!resizeObserver) return;
66
+ resizeObserver.disconnect();
67
+ };
68
+ }
69
+ }, [calculateWidth, containerRef, isMounted]);
70
+
71
+ useEffect(() => {
72
+ calculateWidth();
73
+ }, [calculateWidth, children]);
74
+
75
+ const marqueeStyle = useMemo(
76
+ () => {
77
+ const style = {
78
+ ['--direction' as string]: reverse ? 'reverse' : 'normal',
79
+ ['--duration' as string]: `${duration}s`,
80
+ ['--flex-direction' as string]: direction,
81
+ ['--animation-name' as string]: direction === 'column' ? 'scroll-column' : 'scroll',
82
+ };
83
+
84
+ return style;
85
+ },
86
+ [reverse, duration, direction],
87
+ );
88
+
89
+ const repeatChildren = useCallback((repeat: number) => {
90
+ return [
91
+ ...Array(Number.isFinite(repeat) && repeat >= 0 ? repeat : 0),
92
+ ].map((_, index) => (
93
+ <Fragment key={index}>
94
+ {Children.map(children, (child) => (
95
+ <div className={styles.child}>
96
+ {child}
97
+ </div>
98
+ ))}
99
+ </Fragment>
100
+ ));
101
+ }, [children]);
102
+
103
+ const gradientStyle = useMemo(
104
+ () => ({
105
+ ['--gradient-color' as string]: gradientColor,
106
+ ['--gradient-width' as string]:
107
+ typeof gradientWidth === 'number'
108
+ ? `${gradientWidth}px`
109
+ : gradientWidth,
110
+ }),
111
+ [gradientColor, gradientWidth]);
112
+
113
+ return (
114
+ <div
115
+ className={`${styles['marquee-container']} ${direction === 'column' ? styles['marquee-container--column'] : ''}`}
116
+ ref={containerRef}
117
+ >
118
+ {gradient && <div style={gradientStyle} className={`${styles.overlay} ${direction === 'column' ? styles['overlay--column'] : ''}`} />}
119
+ <div
120
+ className={`${styles.marquee} ${direction === 'column' ? styles['marquee--column'] : ''}`}
121
+ style={marqueeStyle}
122
+ >
123
+ <div className={styles['initial-child-container']} ref={marqueeRef} style={marqueeStyle}>
124
+ {Children.map(children, (child) => {
125
+ return (
126
+ <div className={styles.child}>
127
+ {child}
128
+ </div>
129
+ );
130
+ })}
131
+ </div>
132
+ {repeatChildren(repeat - 1)}
133
+ </div>
134
+ <div className={`${styles.marquee} ${direction === 'column' ? styles['marquee--column'] : ''}`} style={marqueeStyle}>
135
+ {repeatChildren(repeat)}
136
+ </div>
137
+
138
+ </div>
139
+ );
140
+ }