@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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@latte-macchiat-io/latte-vanilla-components",
3
- "version": "0.0.334",
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",
@@ -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
- gap?: number | Partial<Record<keyof typeof breakpoints, number>>;
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
- const containerWidth = carouselRef.current.getBoundingClientRect().width;
90
- const currentGap = getResponsiveValue(gap, windowWidth);
91
- const totalGap = (visibleItems - 1) * currentGap;
92
- setItemWidth((containerWidth - totalGap) / visibleItems);
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 currentGap = getResponsiveValue(gap, windowWidth);
165
- const translateX = -(currentIndex * (itemWidth + currentGap));
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: `${currentGap}px`,
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
+ };