@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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@latte-macchiat-io/latte-vanilla-components",
3
- "version": "0.0.333",
3
+ "version": "0.0.335",
4
4
  "description": "Beautiful components for amazing projects, with a touch of Vanilla 🥤",
5
5
  "type": "module",
6
6
  "main": "./src/index.ts",
@@ -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?: number;
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
- // Adapter le nombre d’items visibles selon le viewport
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
- // Calcul largeur d’un item
90
+ /** Compute width per item based on container width + gap */
78
91
  useEffect(() => {
79
92
  const calculateItemWidth = () => {
80
- if (carouselRef.current) {
81
- const containerWidth = carouselRef.current.getBoundingClientRect().width;
82
- const totalGap = (visibleItems - 1) * gap;
83
- setItemWidth((containerWidth - totalGap) / visibleItems);
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
- // Autoplay
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
- // Swipe mobile
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 translateX = -(currentIndex * (itemWidth + gap));
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: `${gap}px`,
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
+ };