@wireservers-ui/react-natives 1.0.0-rc.1 → 1.0.0-rc.3

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": "@wireservers-ui/react-natives",
3
- "version": "1.0.0-rc.1",
3
+ "version": "1.0.0-rc.3",
4
4
  "description": "70+ production-ready React Native components — TypeScript-first, themeable, accessible. Created by WireServers-UI.",
5
5
  "main": "src/index.ts",
6
6
  "types": "src/index.ts",
@@ -48,6 +48,12 @@
48
48
  "react-native-svg": ">=13"
49
49
  },
50
50
  "peerDependenciesMeta": {
51
+ "nativewind": {
52
+ "optional": true
53
+ },
54
+ "tailwind-variants": {
55
+ "optional": true
56
+ },
51
57
  "react-native-reanimated": {
52
58
  "optional": true
53
59
  },
@@ -1,7 +1,7 @@
1
1
  import { tv } from 'tailwind-variants';
2
2
 
3
3
  export const buttonStyle = tv({
4
- base: 'group/button rounded bg-primary-500 self-start flex-row items-center justify-center data-[focus-visible=true]:web:outline-none data-[focus-visible=true]:web:ring-2 data-[disabled=true]:opacity-40 gap-2',
4
+ base: 'group/button overflow-visible rounded bg-primary-500 self-start flex-row items-center justify-center data-[focus-visible=true]:web:outline-none data-[focus-visible=true]:web:ring-2 data-[disabled=true]:opacity-40 gap-2',
5
5
  variants: {
6
6
  action: {
7
7
  primary:
@@ -7,7 +7,7 @@ import { carouselStyle, carouselContentStyle, carouselItemStyle, carouselPreviou
7
7
  export const [CarouselProvider, useCarouselContext] = createComponentContext<CarouselContextValue>('Carousel');
8
8
 
9
9
  export const Carousel = React.forwardRef<React.ElementRef<typeof View>, CarouselProps>(
10
- ({ className, defaultIndex = 0, loop = false, onIndexChange, children, ...props }, ref) => {
10
+ ({ className, defaultIndex = 0, loop = false, onIndexChange, itemWidth: itemWidthProp = 0, gap: gapProp = 0, autoPlay = false, autoPlayInterval = 3000, children, ...props }, ref) => {
11
11
  const [activeIndex, _setActiveIndex] = useState(defaultIndex);
12
12
  const [itemCount, setItemCount] = useState(0);
13
13
  const [width, setWidth] = useState(0);
@@ -17,7 +17,12 @@ export const Carousel = React.forwardRef<React.ElementRef<typeof View>, Carousel
17
17
  const animX = useRef(new Animated.Value(0)).current;
18
18
  const animXValueRef = useRef(0);
19
19
 
20
- // Track animX value changes
20
+ // Keep prop refs stable for callbacks
21
+ const itemWidthRef = useRef(itemWidthProp);
22
+ itemWidthRef.current = itemWidthProp;
23
+ const gapRef = useRef(gapProp);
24
+ gapRef.current = gapProp;
25
+
21
26
  useEffect(() => {
22
27
  const id = animX.addListener(({ value }) => { animXValueRef.current = value; });
23
28
  return () => animX.removeListener(id);
@@ -29,9 +34,27 @@ export const Carousel = React.forwardRef<React.ElementRef<typeof View>, Carousel
29
34
  onIndexChange?.(index);
30
35
  }, [onIndexChange]);
31
36
 
37
+ /** step = distance between one item start and the next */
38
+ const getStep = useCallback(() => {
39
+ const iw = itemWidthRef.current;
40
+ return iw > 0 ? iw + gapRef.current : widthRef.current;
41
+ }, []);
42
+
43
+ /** How many items to clone on each side for seamless loop */
44
+ const getCloneCount = useCallback(() => {
45
+ const iw = itemWidthRef.current;
46
+ if (!loop) return 0;
47
+ if (iw > 0) {
48
+ const step = iw + gapRef.current;
49
+ return step > 0 ? Math.ceil(widthRef.current / step) + 1 : 1;
50
+ }
51
+ return 1;
52
+ }, [loop]);
53
+
32
54
  const goTo = useCallback((index: number, animate = true) => {
33
- const w = widthRef.current;
34
- const targetX = loop ? -(index + 1) * w : -index * w;
55
+ const step = getStep();
56
+ const clones = getCloneCount();
57
+ const targetX = loop ? -(index + clones) * step : -index * step;
35
58
  setActiveIndex(index);
36
59
  if (animate) {
37
60
  Animated.timing(animX, { toValue: targetX, duration: 300, useNativeDriver: true }).start();
@@ -39,18 +62,20 @@ export const Carousel = React.forwardRef<React.ElementRef<typeof View>, Carousel
39
62
  animX.setValue(targetX);
40
63
  animXValueRef.current = targetX;
41
64
  }
42
- }, [loop, animX, setActiveIndex]);
65
+ }, [loop, animX, setActiveIndex, getStep, getCloneCount]);
43
66
 
44
67
  const next = useCallback(() => {
45
68
  const count = itemCountRef.current;
46
- const w = widthRef.current;
69
+ const step = getStep();
70
+ const clones = getCloneCount();
47
71
  if (loop) {
48
72
  const nextIdx = activeIndexRef.current + 1;
49
73
  if (nextIdx >= count) {
50
- // Animate into clone-of-first, then jump to real first
51
- Animated.timing(animX, { toValue: -(count + 1) * w, duration: 300, useNativeDriver: true }).start(() => {
52
- animX.setValue(-w);
53
- animXValueRef.current = -w;
74
+ // Animate into first clone zone, then jump to real first
75
+ Animated.timing(animX, { toValue: -(count + clones) * step, duration: 300, useNativeDriver: true }).start(() => {
76
+ const jumpX = -clones * step;
77
+ animX.setValue(jumpX);
78
+ animXValueRef.current = jumpX;
54
79
  setActiveIndex(0);
55
80
  });
56
81
  } else {
@@ -59,18 +84,20 @@ export const Carousel = React.forwardRef<React.ElementRef<typeof View>, Carousel
59
84
  } else {
60
85
  goTo(Math.min(activeIndexRef.current + 1, count - 1));
61
86
  }
62
- }, [loop, animX, goTo, setActiveIndex]);
87
+ }, [loop, animX, goTo, setActiveIndex, getStep, getCloneCount]);
63
88
 
64
89
  const previous = useCallback(() => {
65
90
  const count = itemCountRef.current;
66
- const w = widthRef.current;
91
+ const step = getStep();
92
+ const clones = getCloneCount();
67
93
  if (loop) {
68
94
  const prevIdx = activeIndexRef.current - 1;
69
95
  if (prevIdx < 0) {
70
- // Animate into clone-of-last, then jump to real last
71
- Animated.timing(animX, { toValue: 0, duration: 300, useNativeDriver: true }).start(() => {
72
- animX.setValue(-count * w);
73
- animXValueRef.current = -count * w;
96
+ // Animate into last clone zone, then jump to real last
97
+ Animated.timing(animX, { toValue: -(clones - 1) * step, duration: 300, useNativeDriver: true }).start(() => {
98
+ const jumpX = -(count - 1 + clones) * step;
99
+ animX.setValue(jumpX);
100
+ animXValueRef.current = jumpX;
74
101
  setActiveIndex(count - 1);
75
102
  });
76
103
  } else {
@@ -79,10 +106,17 @@ export const Carousel = React.forwardRef<React.ElementRef<typeof View>, Carousel
79
106
  } else {
80
107
  goTo(Math.max(activeIndexRef.current - 1, 0));
81
108
  }
82
- }, [loop, animX, goTo, setActiveIndex]);
109
+ }, [loop, animX, goTo, setActiveIndex, getStep, getCloneCount]);
110
+
111
+ // Auto-play
112
+ useEffect(() => {
113
+ if (!autoPlay || itemCountRef.current === 0) return;
114
+ const id = setInterval(() => { next(); }, autoPlayInterval);
115
+ return () => clearInterval(id);
116
+ }, [autoPlay, autoPlayInterval, next]);
83
117
 
84
118
  return (
85
- <CarouselProvider value={{ activeIndex, setActiveIndex, activeIndexRef, itemCount, setItemCount, itemCountRef, loop, width, setWidth, widthRef, animX, animXValueRef, goTo, next, previous }}>
119
+ <CarouselProvider value={{ activeIndex, setActiveIndex, activeIndexRef, itemCount, setItemCount, itemCountRef, loop, width, setWidth, widthRef, animX, animXValueRef, goTo, next, previous, itemWidth: itemWidthProp, gap: gapProp }}>
86
120
  <View ref={ref} className={carouselStyle({ class: className })} {...props}>{children}</View>
87
121
  </CarouselProvider>
88
122
  );
@@ -91,16 +125,19 @@ export const Carousel = React.forwardRef<React.ElementRef<typeof View>, Carousel
91
125
  Carousel.displayName = 'Carousel';
92
126
 
93
127
  export const CarouselContent = React.forwardRef<React.ElementRef<typeof View>, CarouselContentProps>(({ className, children, style, ...props }, ref) => {
94
- const { setWidth, setItemCount, itemCountRef, loop, setActiveIndex, activeIndexRef, animX, animXValueRef, widthRef } = useCarouselContext();
128
+ const { setWidth, setItemCount, itemCountRef, loop, setActiveIndex, activeIndexRef, animX, animXValueRef, widthRef, itemWidth: itemWidthProp, gap: gapProp } = useCarouselContext();
95
129
  const dragStartValue = useRef(0);
96
130
  const dragStartIndex = useRef(0);
97
131
  const internalRef = useRef<View>(null);
98
132
 
99
- // Keep mutable refs for values used in gesture callbacks
100
133
  const loopRef = useRef(loop);
101
134
  loopRef.current = loop;
102
135
  const setActiveIndexRef = useRef(setActiveIndex);
103
136
  setActiveIndexRef.current = setActiveIndex;
137
+ const itemWidthRef = useRef(itemWidthProp);
138
+ itemWidthRef.current = itemWidthProp;
139
+ const gapRef = useRef(gapProp);
140
+ gapRef.current = gapProp;
104
141
 
105
142
  const childArray = React.Children.toArray(children);
106
143
  const count = childArray.length;
@@ -109,27 +146,58 @@ export const CarouselContent = React.forwardRef<React.ElementRef<typeof View>, C
109
146
  itemCountRef.current = count;
110
147
  }, [count]);
111
148
 
112
- // In loop mode: [cloneLast, ...real, cloneFirst]
113
- const displayedChildren = loop && count > 0
114
- ? [
115
- React.cloneElement(childArray[count - 1] as React.ReactElement, { key: '__clone-last__' }),
116
- ...childArray,
117
- React.cloneElement(childArray[0] as React.ReactElement, { key: '__clone-first__' }),
118
- ]
119
- : children;
149
+ const getStep = () => {
150
+ const iw = itemWidthRef.current;
151
+ return iw > 0 ? iw + gapRef.current : widthRef.current;
152
+ };
153
+
154
+ const getCloneCount = () => {
155
+ const iw = itemWidthRef.current;
156
+ if (!loopRef.current) return 0;
157
+ if (iw > 0) {
158
+ const step = iw + gapRef.current;
159
+ return step > 0 ? Math.ceil(widthRef.current / step) + 1 : 1;
160
+ }
161
+ return 1;
162
+ };
163
+
164
+ // Build displayed children with clones for loop mode
165
+ const cloneCount = getCloneCount();
166
+ let displayedChildren: React.ReactNode;
167
+ if (loop && count > 0 && cloneCount > 0) {
168
+ const leadClones: React.ReactElement[] = [];
169
+ const trailClones: React.ReactElement[] = [];
170
+ for (let i = 0; i < cloneCount; i++) {
171
+ // Lead clones: last `cloneCount` items
172
+ const leadIdx = ((count - cloneCount + i) % count + count) % count;
173
+ leadClones.push(
174
+ React.cloneElement(childArray[leadIdx] as React.ReactElement, { key: `__clone-lead-${i}__` }),
175
+ );
176
+ // Trail clones: first `cloneCount` items
177
+ const trailIdx = i % count;
178
+ trailClones.push(
179
+ React.cloneElement(childArray[trailIdx] as React.ReactElement, { key: `__clone-trail-${i}__` }),
180
+ );
181
+ }
182
+ displayedChildren = [...leadClones, ...childArray, ...trailClones];
183
+ } else {
184
+ displayedChildren = children;
185
+ }
120
186
 
121
187
  const handleLayout = (e: LayoutChangeEvent) => {
122
188
  const w = e.nativeEvent.layout.width;
123
189
  widthRef.current = w;
124
190
  setWidth(w);
125
191
  if (loop && w > 0) {
126
- // Start at real first (padded index 1)
127
- animX.setValue(-w);
128
- animXValueRef.current = -w;
192
+ const step = getStep();
193
+ const clones = getCloneCount();
194
+ const initialX = -clones * step;
195
+ animX.setValue(initialX);
196
+ animXValueRef.current = initialX;
129
197
  }
130
198
  };
131
199
 
132
- // --- Shared gesture helpers (access everything via refs for stable closures) ---
200
+ // --- Shared gesture helpers ---
133
201
  const gestureStart = () => {
134
202
  animX.stopAnimation();
135
203
  dragStartValue.current = animXValueRef.current;
@@ -137,60 +205,67 @@ export const CarouselContent = React.forwardRef<React.ElementRef<typeof View>, C
137
205
  };
138
206
 
139
207
  const gestureMove = (dx: number) => {
140
- const w = widthRef.current;
141
- const clamped = Math.max(-w, Math.min(w, dx));
208
+ const step = getStep();
209
+ const clamped = Math.max(-step, Math.min(step, dx));
142
210
  animX.setValue(dragStartValue.current + clamped);
143
211
  };
144
212
 
145
213
  const gestureEnd = (dx: number, vx: number) => {
146
- const w = widthRef.current;
147
- if (w === 0) return;
214
+ const step = getStep();
215
+ if (step === 0) return;
148
216
  const cnt = itemCountRef.current;
149
217
  const start = dragStartIndex.current;
150
218
  const isLoop = loopRef.current;
151
219
  const setIdx = setActiveIndexRef.current;
220
+ const clones = getCloneCount();
152
221
 
153
222
  let delta = 0;
154
- if (vx < -0.3 || dx < -w * 0.3) delta = 1;
155
- else if (vx > 0.3 || dx > w * 0.3) delta = -1;
223
+ if (vx < -0.3 || dx < -step * 0.3) delta = 1;
224
+ else if (vx > 0.3 || dx > step * 0.3) delta = -1;
156
225
 
157
226
  const targetLogical = start + delta;
158
227
 
159
228
  if (isLoop) {
160
- const paddedStart = start + 1;
229
+ const paddedStart = start + clones;
161
230
  const targetPage = paddedStart + delta;
162
231
 
163
- if (targetPage <= 0) {
164
- Animated.timing(animX, { toValue: 0, duration: 200, useNativeDriver: true }).start(() => {
165
- animX.setValue(-cnt * w);
166
- animXValueRef.current = -cnt * w;
167
- setIdx(cnt - 1);
232
+ if (targetPage < clones) {
233
+ // Went before first real item animate to clone, then jump
234
+ Animated.timing(animX, { toValue: -targetPage * step, duration: 200, useNativeDriver: true }).start(() => {
235
+ const jumpIdx = cnt - 1;
236
+ const jumpX = -(jumpIdx + clones) * step;
237
+ animX.setValue(jumpX);
238
+ animXValueRef.current = jumpX;
239
+ setIdx(jumpIdx);
168
240
  });
169
- } else if (targetPage >= cnt + 1) {
170
- Animated.timing(animX, { toValue: -(cnt + 1) * w, duration: 200, useNativeDriver: true }).start(() => {
171
- animX.setValue(-w);
172
- animXValueRef.current = -w;
241
+ } else if (targetPage >= cnt + clones) {
242
+ // Went past last real item animate to clone, then jump
243
+ Animated.timing(animX, { toValue: -targetPage * step, duration: 200, useNativeDriver: true }).start(() => {
244
+ const jumpX = -clones * step;
245
+ animX.setValue(jumpX);
246
+ animXValueRef.current = jumpX;
173
247
  setIdx(0);
174
248
  });
175
249
  } else {
176
- setIdx(Math.max(0, Math.min(cnt - 1, targetLogical)));
177
- Animated.timing(animX, { toValue: -targetPage * w, duration: 200, useNativeDriver: true }).start();
250
+ const idx = Math.max(0, Math.min(cnt - 1, targetLogical));
251
+ setIdx(idx);
252
+ Animated.timing(animX, { toValue: -targetPage * step, duration: 200, useNativeDriver: true }).start();
178
253
  }
179
254
  } else {
180
255
  const clampedIdx = Math.max(0, Math.min(cnt - 1, targetLogical));
181
256
  setIdx(clampedIdx);
182
- Animated.timing(animX, { toValue: -clampedIdx * w, duration: 200, useNativeDriver: true }).start();
257
+ Animated.timing(animX, { toValue: -clampedIdx * step, duration: 200, useNativeDriver: true }).start();
183
258
  }
184
259
  };
185
260
 
186
261
  const gestureCancel = () => {
187
- const w = widthRef.current;
188
- if (w === 0) return;
262
+ const step = getStep();
263
+ if (step === 0) return;
189
264
  const start = dragStartIndex.current;
190
- const isLoop = loopRef.current;
191
- const targetPage = isLoop ? start + 1 : start;
265
+ const clones = getCloneCount();
266
+ const targetPage = loopRef.current ? start + clones : start;
192
267
  setActiveIndexRef.current(start);
193
- Animated.timing(animX, { toValue: -targetPage * w, duration: 200, useNativeDriver: true }).start();
268
+ Animated.timing(animX, { toValue: -targetPage * step, duration: 200, useNativeDriver: true }).start();
194
269
  };
195
270
 
196
271
  // --- Native: PanResponder ---
@@ -207,7 +282,7 @@ export const CarouselContent = React.forwardRef<React.ElementRef<typeof View>, C
207
282
  : null
208
283
  ).current;
209
284
 
210
- // --- Web: Pointer events (PanResponder is unreliable on web) ---
285
+ // --- Web: Pointer events ---
211
286
  useEffect(() => {
212
287
  if (Platform.OS !== 'web') return;
213
288
  const el = internalRef.current as unknown as HTMLElement;
@@ -296,7 +371,7 @@ export const CarouselContent = React.forwardRef<React.ElementRef<typeof View>, C
296
371
  {...(panResponder?.panHandlers ?? {})}
297
372
  {...props}
298
373
  >
299
- <Animated.View style={{ flexDirection: 'row', transform: [{ translateX: animX }] }}>
374
+ <Animated.View style={{ flexDirection: 'row', gap: gapProp, transform: [{ translateX: animX }] }}>
300
375
  {displayedChildren}
301
376
  </Animated.View>
302
377
  </View>
@@ -305,12 +380,13 @@ export const CarouselContent = React.forwardRef<React.ElementRef<typeof View>, C
305
380
  CarouselContent.displayName = 'CarouselContent';
306
381
 
307
382
  export const CarouselItem = React.forwardRef<React.ElementRef<typeof View>, CarouselItemProps>(({ className, style, onPress, children, ...props }, ref) => {
308
- const { width } = useCarouselContext();
383
+ const { width, itemWidth } = useCarouselContext();
384
+ const effectiveWidth = itemWidth > 0 ? itemWidth : width;
309
385
  return (
310
386
  <View
311
387
  ref={ref}
312
388
  className={carouselItemStyle({ class: className })}
313
- style={[width > 0 ? { width } : undefined, style]}
389
+ style={[effectiveWidth > 0 ? { width: effectiveWidth } : undefined, style]}
314
390
  {...props}
315
391
  >
316
392
  {onPress ? (
@@ -16,6 +16,8 @@ export interface CarouselContextValue {
16
16
  goTo: (index: number, animate?: boolean) => void;
17
17
  next: () => void;
18
18
  previous: () => void;
19
+ itemWidth: number;
20
+ gap: number;
19
21
  }
20
22
 
21
23
  export interface CarouselProps extends React.ComponentPropsWithoutRef<typeof View> {
@@ -23,6 +25,14 @@ export interface CarouselProps extends React.ComponentPropsWithoutRef<typeof Vie
23
25
  defaultIndex?: number;
24
26
  loop?: boolean;
25
27
  onIndexChange?: (index: number) => void;
28
+ /** Fixed width for each item. When set, multiple items are visible. */
29
+ itemWidth?: number;
30
+ /** Gap between items in pixels. */
31
+ gap?: number;
32
+ /** Automatically advance slides. */
33
+ autoPlay?: boolean;
34
+ /** Interval in ms between auto-advances (default 3000). */
35
+ autoPlayInterval?: number;
26
36
  }
27
37
  export interface CarouselContentProps extends React.ComponentPropsWithoutRef<typeof View> { className?: string; }
28
38
  export interface CarouselItemProps extends React.ComponentPropsWithoutRef<typeof View> { className?: string; onPress?: () => void; }
@@ -1,7 +1,7 @@
1
1
  import { tv } from 'tailwind-variants';
2
2
 
3
3
  export const dividerStyle = tv({
4
- base: 'bg-outline-200',
4
+ base: 'bg-outline-200 shrink-0',
5
5
  variants: {
6
6
  orientation: {
7
7
  horizontal: 'h-px w-full',
@@ -172,9 +172,9 @@ module.exports = {
172
172
  },
173
173
  },
174
174
  fontFamily: {
175
- heading: undefined,
176
- body: undefined,
177
- mono: undefined,
175
+ heading: ['System', 'ui-sans-serif', 'system-ui', 'sans-serif'],
176
+ body: ['System', 'ui-sans-serif', 'system-ui', 'sans-serif'],
177
+ mono: ['ui-monospace', 'SFMono-Regular', 'monospace'],
178
178
  jakarta: ['var(--font-plus-jakarta-sans)'],
179
179
  roboto: ['var(--font-roboto)'],
180
180
  code: ['var(--font-source-code-pro)'],