@latte-macchiat-io/latte-vanilla-components 0.0.334 → 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
@@ -2,6 +2,7 @@
|
|
2
2
|
|
3
3
|
import { clsx } from 'clsx';
|
4
4
|
import { ReactNode, useEffect, useRef, useState } from 'react';
|
5
|
+
|
5
6
|
import {
|
6
7
|
carouselBullet,
|
7
8
|
carouselBulletActive,
|
@@ -16,38 +17,39 @@ import {
|
|
16
17
|
carouselSlide,
|
17
18
|
type CarouselVariants,
|
18
19
|
} from './styles.css';
|
20
|
+
|
19
21
|
import { breakpoints } from '../../styles/mediaqueries';
|
22
|
+
import { getResponsiveValue, parseResponsiveNumber } from '../../utils/getResponsiveValue';
|
20
23
|
import { useWindowSize } from '../../utils/useWindowSize';
|
24
|
+
|
21
25
|
import { Icon } from '../Icon';
|
22
26
|
|
23
27
|
export type CarouselProps = React.HTMLAttributes<HTMLDivElement> &
|
24
28
|
CarouselVariants &
|
25
29
|
CarouselNavVariants &
|
26
30
|
CarouselContentVariants & {
|
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 */
|
27
35
|
data: ReactNode[];
|
28
|
-
|
36
|
+
|
37
|
+
/** Number of items visible per view */
|
29
38
|
itemsPerView?: number;
|
39
|
+
|
40
|
+
/** Show pagination bullets */
|
30
41
|
showBullets?: boolean;
|
42
|
+
|
43
|
+
/** Show prev/next nav buttons */
|
31
44
|
showNavButtons?: boolean;
|
45
|
+
|
46
|
+
/** Autoplay enabled */
|
32
47
|
autoplay?: boolean;
|
48
|
+
|
49
|
+
/** Autoplay interval in ms */
|
33
50
|
autoplayInterval?: number;
|
34
51
|
};
|
35
52
|
|
36
|
-
const getResponsiveValue = (values: number | Partial<Record<keyof typeof breakpoints, number>>, width: number | undefined): number => {
|
37
|
-
if (typeof values === 'number') return values;
|
38
|
-
|
39
|
-
const bpOrder: (keyof typeof breakpoints)[] = ['mobile', 'sm', 'md', 'lg', 'xl', '2xl'];
|
40
|
-
let currentValue: number = values.mobile ?? 0;
|
41
|
-
|
42
|
-
for (const bp of bpOrder) {
|
43
|
-
if (width && width >= breakpoints[bp] && values[bp] !== undefined) {
|
44
|
-
currentValue = values[bp]!;
|
45
|
-
}
|
46
|
-
}
|
47
|
-
|
48
|
-
return currentValue;
|
49
|
-
};
|
50
|
-
|
51
53
|
export const Carousel = ({
|
52
54
|
data,
|
53
55
|
overflow,
|
@@ -63,6 +65,7 @@ export const Carousel = ({
|
|
63
65
|
navPositionHorizontal,
|
64
66
|
}: CarouselProps) => {
|
65
67
|
const { width: windowWidth } = useWindowSize();
|
68
|
+
|
66
69
|
const [currentIndex, setCurrentIndex] = useState(0);
|
67
70
|
const [itemWidth, setItemWidth] = useState(0);
|
68
71
|
const [visibleItems, setVisibleItems] = useState(itemsPerView);
|
@@ -73,6 +76,7 @@ export const Carousel = ({
|
|
73
76
|
const isTablet = windowWidth !== undefined && windowWidth > breakpoints.md;
|
74
77
|
const isDesktop = windowWidth !== undefined && windowWidth > breakpoints.lg;
|
75
78
|
|
79
|
+
/** Update number of visible items responsively */
|
76
80
|
useEffect(() => {
|
77
81
|
if (isDesktop) {
|
78
82
|
setVisibleItems(itemsPerView);
|
@@ -83,14 +87,17 @@ export const Carousel = ({
|
|
83
87
|
}
|
84
88
|
}, [isTablet, isDesktop, itemsPerView]);
|
85
89
|
|
90
|
+
/** Compute width per item based on container width + gap */
|
86
91
|
useEffect(() => {
|
87
92
|
const calculateItemWidth = () => {
|
88
|
-
if (carouselRef.current)
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
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);
|
94
101
|
};
|
95
102
|
|
96
103
|
calculateItemWidth();
|
@@ -98,17 +105,21 @@ export const Carousel = ({
|
|
98
105
|
return () => window.removeEventListener('resize', calculateItemWidth);
|
99
106
|
}, [visibleItems, gap, windowWidth]);
|
100
107
|
|
108
|
+
/** Autoplay logic */
|
101
109
|
useEffect(() => {
|
102
110
|
if (!autoplay) return;
|
111
|
+
|
103
112
|
const interval = setInterval(() => {
|
104
113
|
setCurrentIndex((prev) => {
|
105
114
|
const maxIndex = Math.max(0, data.length - visibleItems);
|
106
115
|
return prev >= maxIndex ? 0 : prev + 1;
|
107
116
|
});
|
108
117
|
}, autoplayInterval);
|
118
|
+
|
109
119
|
return () => clearInterval(interval);
|
110
120
|
}, [autoplay, autoplayInterval, data.length, visibleItems]);
|
111
121
|
|
122
|
+
/** Swipe mobile logic */
|
112
123
|
useEffect(() => {
|
113
124
|
const carousel = carouselRef.current;
|
114
125
|
if (!carousel) return;
|
@@ -122,12 +133,8 @@ export const Carousel = ({
|
|
122
133
|
|
123
134
|
const handleTouchEnd = (e: TouchEvent) => {
|
124
135
|
touchEndX = e.changedTouches[0].screenX;
|
125
|
-
handleSwipe();
|
126
|
-
};
|
127
|
-
|
128
|
-
const handleSwipe = () => {
|
129
|
-
const swipeThreshold = 100;
|
130
136
|
const diff = touchStartX - touchEndX;
|
137
|
+
const swipeThreshold = 100;
|
131
138
|
|
132
139
|
if (Math.abs(diff) > swipeThreshold) {
|
133
140
|
if (diff > 0) {
|
@@ -147,10 +154,7 @@ export const Carousel = ({
|
|
147
154
|
};
|
148
155
|
}, []);
|
149
156
|
|
150
|
-
const handlePrevious = () =>
|
151
|
-
setCurrentIndex((prev) => Math.max(0, prev - 1));
|
152
|
-
};
|
153
|
-
|
157
|
+
const handlePrevious = () => setCurrentIndex((prev) => Math.max(0, prev - 1));
|
154
158
|
const handleNext = () => {
|
155
159
|
const maxIndex = Math.max(0, data.length - visibleItems);
|
156
160
|
setCurrentIndex((prev) => Math.min(maxIndex, prev + 1));
|
@@ -161,8 +165,10 @@ export const Carousel = ({
|
|
161
165
|
setCurrentIndex(Math.min(index, maxIndex));
|
162
166
|
};
|
163
167
|
|
164
|
-
const
|
165
|
-
const
|
168
|
+
const responsiveGap = getResponsiveValue(gap, windowWidth);
|
169
|
+
const numericGap = parseResponsiveNumber(responsiveGap);
|
170
|
+
|
171
|
+
const translateX = -(currentIndex * (itemWidth + numericGap));
|
166
172
|
const maxIndex = Math.max(0, data.length - visibleItems);
|
167
173
|
|
168
174
|
return (
|
@@ -172,7 +178,7 @@ export const Carousel = ({
|
|
172
178
|
ref={slideRef}
|
173
179
|
className={carouselSlide}
|
174
180
|
style={{
|
175
|
-
gap: `${
|
181
|
+
gap: typeof responsiveGap === 'number' ? `${responsiveGap}px` : responsiveGap,
|
176
182
|
transform: `translateX(${translateX}px)`,
|
177
183
|
}}>
|
178
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
|
+
};
|