@latte-macchiat-io/latte-vanilla-components 0.0.333 → 0.0.335
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/package.json
CHANGED
@@ -19,7 +19,7 @@ import {
|
|
19
19
|
} from './styles.css';
|
20
20
|
|
21
21
|
import { breakpoints } from '../../styles/mediaqueries';
|
22
|
-
|
22
|
+
import { getResponsiveValue, parseResponsiveNumber } from '../../utils/getResponsiveValue';
|
23
23
|
import { useWindowSize } from '../../utils/useWindowSize';
|
24
24
|
|
25
25
|
import { Icon } from '../Icon';
|
@@ -28,13 +28,25 @@ export type CarouselProps = React.HTMLAttributes<HTMLDivElement> &
|
|
28
28
|
CarouselVariants &
|
29
29
|
CarouselNavVariants &
|
30
30
|
CarouselContentVariants & {
|
31
|
-
gap
|
31
|
+
/** Responsive gap between items (e.g. { mobile: '8px', md: '16px' }) */
|
32
|
+
gap?: number | Record<keyof typeof breakpoints, string | number>;
|
33
|
+
|
34
|
+
/** Items to render */
|
32
35
|
data: ReactNode[];
|
36
|
+
|
37
|
+
/** Number of items visible per view */
|
33
38
|
itemsPerView?: number;
|
39
|
+
|
40
|
+
/** Show pagination bullets */
|
34
41
|
showBullets?: boolean;
|
42
|
+
|
43
|
+
/** Show prev/next nav buttons */
|
35
44
|
showNavButtons?: boolean;
|
36
45
|
|
46
|
+
/** Autoplay enabled */
|
37
47
|
autoplay?: boolean;
|
48
|
+
|
49
|
+
/** Autoplay interval in ms */
|
38
50
|
autoplayInterval?: number;
|
39
51
|
};
|
40
52
|
|
@@ -53,6 +65,7 @@ export const Carousel = ({
|
|
53
65
|
navPositionHorizontal,
|
54
66
|
}: CarouselProps) => {
|
55
67
|
const { width: windowWidth } = useWindowSize();
|
68
|
+
|
56
69
|
const [currentIndex, setCurrentIndex] = useState(0);
|
57
70
|
const [itemWidth, setItemWidth] = useState(0);
|
58
71
|
const [visibleItems, setVisibleItems] = useState(itemsPerView);
|
@@ -63,7 +76,7 @@ export const Carousel = ({
|
|
63
76
|
const isTablet = windowWidth !== undefined && windowWidth > breakpoints.md;
|
64
77
|
const isDesktop = windowWidth !== undefined && windowWidth > breakpoints.lg;
|
65
78
|
|
66
|
-
|
79
|
+
/** Update number of visible items responsively */
|
67
80
|
useEffect(() => {
|
68
81
|
if (isDesktop) {
|
69
82
|
setVisibleItems(itemsPerView);
|
@@ -74,22 +87,25 @@ export const Carousel = ({
|
|
74
87
|
}
|
75
88
|
}, [isTablet, isDesktop, itemsPerView]);
|
76
89
|
|
77
|
-
|
90
|
+
/** Compute width per item based on container width + gap */
|
78
91
|
useEffect(() => {
|
79
92
|
const calculateItemWidth = () => {
|
80
|
-
if (carouselRef.current)
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
93
|
+
if (!carouselRef.current) return;
|
94
|
+
|
95
|
+
const containerWidth = carouselRef.current.getBoundingClientRect().width;
|
96
|
+
const responsiveGap = getResponsiveValue(gap, windowWidth);
|
97
|
+
const numericGap = parseResponsiveNumber(responsiveGap);
|
98
|
+
const totalGap = (visibleItems - 1) * numericGap;
|
99
|
+
|
100
|
+
setItemWidth((containerWidth - totalGap) / visibleItems);
|
85
101
|
};
|
86
102
|
|
87
103
|
calculateItemWidth();
|
88
104
|
window.addEventListener('resize', calculateItemWidth);
|
89
105
|
return () => window.removeEventListener('resize', calculateItemWidth);
|
90
|
-
}, [visibleItems, gap]);
|
106
|
+
}, [visibleItems, gap, windowWidth]);
|
91
107
|
|
92
|
-
|
108
|
+
/** Autoplay logic */
|
93
109
|
useEffect(() => {
|
94
110
|
if (!autoplay) return;
|
95
111
|
|
@@ -103,7 +119,7 @@ export const Carousel = ({
|
|
103
119
|
return () => clearInterval(interval);
|
104
120
|
}, [autoplay, autoplayInterval, data.length, visibleItems]);
|
105
121
|
|
106
|
-
|
122
|
+
/** Swipe mobile logic */
|
107
123
|
useEffect(() => {
|
108
124
|
const carousel = carouselRef.current;
|
109
125
|
if (!carousel) return;
|
@@ -117,12 +133,8 @@ export const Carousel = ({
|
|
117
133
|
|
118
134
|
const handleTouchEnd = (e: TouchEvent) => {
|
119
135
|
touchEndX = e.changedTouches[0].screenX;
|
120
|
-
handleSwipe();
|
121
|
-
};
|
122
|
-
|
123
|
-
const handleSwipe = () => {
|
124
|
-
const swipeThreshold = 100;
|
125
136
|
const diff = touchStartX - touchEndX;
|
137
|
+
const swipeThreshold = 100;
|
126
138
|
|
127
139
|
if (Math.abs(diff) > swipeThreshold) {
|
128
140
|
if (diff > 0) {
|
@@ -142,10 +154,7 @@ export const Carousel = ({
|
|
142
154
|
};
|
143
155
|
}, []);
|
144
156
|
|
145
|
-
const handlePrevious = () =>
|
146
|
-
setCurrentIndex((prev) => Math.max(0, prev - 1));
|
147
|
-
};
|
148
|
-
|
157
|
+
const handlePrevious = () => setCurrentIndex((prev) => Math.max(0, prev - 1));
|
149
158
|
const handleNext = () => {
|
150
159
|
const maxIndex = Math.max(0, data.length - visibleItems);
|
151
160
|
setCurrentIndex((prev) => Math.min(maxIndex, prev + 1));
|
@@ -156,7 +165,10 @@ export const Carousel = ({
|
|
156
165
|
setCurrentIndex(Math.min(index, maxIndex));
|
157
166
|
};
|
158
167
|
|
159
|
-
const
|
168
|
+
const responsiveGap = getResponsiveValue(gap, windowWidth);
|
169
|
+
const numericGap = parseResponsiveNumber(responsiveGap);
|
170
|
+
|
171
|
+
const translateX = -(currentIndex * (itemWidth + numericGap));
|
160
172
|
const maxIndex = Math.max(0, data.length - visibleItems);
|
161
173
|
|
162
174
|
return (
|
@@ -166,7 +178,7 @@ export const Carousel = ({
|
|
166
178
|
ref={slideRef}
|
167
179
|
className={carouselSlide}
|
168
180
|
style={{
|
169
|
-
gap: `${
|
181
|
+
gap: typeof responsiveGap === 'number' ? `${responsiveGap}px` : responsiveGap,
|
170
182
|
transform: `translateX(${translateX}px)`,
|
171
183
|
}}>
|
172
184
|
{data.map((item, index) => (
|
@@ -0,0 +1,52 @@
|
|
1
|
+
import { breakpoints } from '../styles/mediaqueries';
|
2
|
+
|
3
|
+
type BreakpointKey = keyof typeof breakpoints;
|
4
|
+
type ResponsiveValue = string | number;
|
5
|
+
type ResponsiveMap = Partial<Record<BreakpointKey, ResponsiveValue>>;
|
6
|
+
|
7
|
+
/**
|
8
|
+
* Get a responsive value based on current viewport width.
|
9
|
+
*
|
10
|
+
* Supports both raw values (number/string) and responsive maps.
|
11
|
+
*
|
12
|
+
* Example:
|
13
|
+
* getResponsiveValue({ mobile: '8px', sm: '12px', md: '16px' }, 768) -> "12px"
|
14
|
+
* getResponsiveValue(24, 1280) -> 24
|
15
|
+
*/
|
16
|
+
export const getResponsiveValue = (values: ResponsiveValue | ResponsiveMap, width: number | undefined): ResponsiveValue => {
|
17
|
+
// ✅ Case 1: single non-responsive value
|
18
|
+
if (typeof values === 'string' || typeof values === 'number') return values;
|
19
|
+
|
20
|
+
const bpOrder: BreakpointKey[] = ['mobile', 'sm', 'md', 'lg', 'xl', '2xl'];
|
21
|
+
|
22
|
+
// ✅ Default fallback: mobile or first defined value
|
23
|
+
let currentValue: ResponsiveValue = values.mobile ?? Object.values(values).find((v): v is ResponsiveValue => v !== undefined) ?? 0;
|
24
|
+
|
25
|
+
// ✅ Iterate breakpoints in order and update if current width passes threshold
|
26
|
+
for (const bp of bpOrder) {
|
27
|
+
const bpMin = breakpoints[bp];
|
28
|
+
const bpValue = values[bp];
|
29
|
+
|
30
|
+
if (width !== undefined && width >= bpMin && bpValue !== undefined) {
|
31
|
+
currentValue = bpValue;
|
32
|
+
}
|
33
|
+
}
|
34
|
+
|
35
|
+
return currentValue;
|
36
|
+
};
|
37
|
+
|
38
|
+
/**
|
39
|
+
* Convert a responsive value (with units) into a numeric value for arithmetic operations.
|
40
|
+
* If it's a string with units (e.g., "16px"), the numeric part is extracted.
|
41
|
+
*
|
42
|
+
* Example:
|
43
|
+
* parseResponsiveNumber("16px") -> 16
|
44
|
+
* parseResponsiveNumber(24) -> 24
|
45
|
+
*/
|
46
|
+
export const parseResponsiveNumber = (value: ResponsiveValue): number => {
|
47
|
+
if (typeof value === 'number') return value;
|
48
|
+
|
49
|
+
// Extract numeric portion from string, e.g. "16px" -> 16
|
50
|
+
const parsed = parseFloat(value);
|
51
|
+
return isNaN(parsed) ? 0 : parsed;
|
52
|
+
};
|