@umituz/react-native-design-system 4.28.11 → 4.28.13

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.
Files changed (49) hide show
  1. package/package.json +31 -8
  2. package/src/atoms/AtomicAvatar.tsx +69 -40
  3. package/src/atoms/AtomicDatePicker.tsx +6 -6
  4. package/src/atoms/AtomicSpinner.tsx +24 -22
  5. package/src/atoms/AtomicText.tsx +32 -27
  6. package/src/atoms/AtomicTextArea.tsx +17 -15
  7. package/src/atoms/EmptyState.tsx +44 -41
  8. package/src/atoms/button/AtomicButton.tsx +8 -9
  9. package/src/atoms/card/AtomicCard.tsx +26 -8
  10. package/src/atoms/datepicker/components/DatePickerButton.tsx +8 -8
  11. package/src/atoms/datepicker/components/DatePickerModal.tsx +7 -7
  12. package/src/atoms/fab/styles/fabStyles.ts +0 -21
  13. package/src/atoms/icon/index.ts +6 -20
  14. package/src/atoms/picker/components/PickerModal.tsx +24 -4
  15. package/src/atoms/skeleton/AtomicSkeleton.tsx +9 -11
  16. package/src/carousel/Carousel.tsx +43 -20
  17. package/src/carousel/carouselCalculations.ts +12 -9
  18. package/src/carousel/index.ts +0 -1
  19. package/src/device/detection/iPadDetection.ts +5 -14
  20. package/src/device/infrastructure/services/DeviceFeatureService.ts +89 -9
  21. package/src/device/infrastructure/services/DeviceInfoService.ts +33 -0
  22. package/src/device/infrastructure/services/UserFriendlyIdService.ts +8 -6
  23. package/src/device/infrastructure/utils/__tests__/stringUtils.test.ts +56 -20
  24. package/src/device/infrastructure/utils/nativeModuleUtils.ts +16 -2
  25. package/src/device/infrastructure/utils/stringUtils.ts +51 -5
  26. package/src/filesystem/domain/utils/FileUtils.ts +5 -1
  27. package/src/image/domain/utils/ImageUtils.ts +6 -0
  28. package/src/layouts/AppHeader/AppHeader.tsx +13 -3
  29. package/src/layouts/Container/Container.tsx +19 -1
  30. package/src/layouts/FormLayout/FormLayout.tsx +20 -1
  31. package/src/layouts/Grid/Grid.tsx +34 -4
  32. package/src/layouts/ScreenHeader/ScreenHeader.tsx +4 -0
  33. package/src/layouts/ScreenLayout/ScreenLayout.tsx +42 -3
  34. package/src/molecules/SearchBar/SearchBar.tsx +27 -23
  35. package/src/molecules/action-footer/ActionFooter.tsx +32 -31
  36. package/src/molecules/alerts/AlertService.ts +60 -15
  37. package/src/molecules/avatar/Avatar.tsx +3 -3
  38. package/src/molecules/avatar/AvatarGroup.tsx +7 -7
  39. package/src/molecules/bottom-sheet/components/BottomSheet.tsx +3 -3
  40. package/src/molecules/calendar/infrastructure/utils/DateUtilities.ts +12 -1
  41. package/src/molecules/calendar/presentation/components/CalendarDayCell.tsx +48 -32
  42. package/src/molecules/info-grid/InfoGrid.tsx +5 -3
  43. package/src/organisms/FormContainer.tsx +11 -1
  44. package/src/tanstack/domain/utils/ErrorHelpers.ts +2 -2
  45. package/src/tanstack/domain/utils/MetricsCalculator.ts +6 -1
  46. package/src/theme/core/colors/ColorUtils.ts +7 -4
  47. package/src/utils/formatters/stringFormatter.ts +18 -3
  48. package/src/utils/index.ts +6 -4
  49. package/src/utils/math/CalculationUtils.ts +10 -1
@@ -3,7 +3,7 @@
3
3
  * Refactored: Extracted configs, styles, and types
4
4
  */
5
5
 
6
- import React, { useCallback } from 'react';
6
+ import React, { useCallback, useMemo } from 'react';
7
7
  import { StyleProp, ViewStyle, TextStyle, TouchableOpacity } from 'react-native';
8
8
  import { AtomicText } from '../AtomicText';
9
9
  import { AtomicIcon } from '../icon';
@@ -46,7 +46,7 @@ export const AtomicButton: React.FC<AtomicButtonProps> = React.memo(({
46
46
  const variantStyles = getVariantStyles(variant, tokens);
47
47
  const iconColor = variantStyles.text.color;
48
48
 
49
- const containerStyle: StyleProp<ViewStyle> = [
49
+ const containerStyle = useMemo<StyleProp<ViewStyle>>(() => [
50
50
  buttonStyles.button,
51
51
  {
52
52
  paddingVertical: config.paddingVertical,
@@ -58,9 +58,9 @@ export const AtomicButton: React.FC<AtomicButtonProps> = React.memo(({
58
58
  fullWidth ? buttonStyles.fullWidth : undefined,
59
59
  isDisabled ? buttonStyles.disabled : undefined,
60
60
  style,
61
- ];
61
+ ], [config, variantStyles.container, fullWidth, isDisabled, style, tokens.borders.radius.md]);
62
62
 
63
- const buttonTextStyle: StyleProp<TextStyle> = [
63
+ const buttonTextStyle = useMemo<StyleProp<TextStyle>>(() => [
64
64
  {
65
65
  fontSize: config.fontSize,
66
66
  fontWeight: '600',
@@ -68,17 +68,16 @@ export const AtomicButton: React.FC<AtomicButtonProps> = React.memo(({
68
68
  variantStyles.text,
69
69
  isDisabled ? buttonStyles.disabledText : undefined,
70
70
  textStyle,
71
- ];
71
+ ], [config.fontSize, variantStyles.text, isDisabled, textStyle]);
72
+
73
+ const rowReverseStyle = useMemo(() => iconPosition === 'right' ? buttonStyles.rowReverse : undefined, [iconPosition]);
72
74
 
73
75
  const buttonText = title || children;
74
76
  const showIcon = icon;
75
77
 
76
78
  return (
77
79
  <TouchableOpacity
78
- style={[
79
- containerStyle,
80
- iconPosition === 'right' && buttonStyles.rowReverse,
81
- ]}
80
+ style={[containerStyle, rowReverseStyle]}
82
81
  onPress={handlePress}
83
82
  activeOpacity={activeOpacity}
84
83
  disabled={isDisabled}
@@ -5,7 +5,7 @@
5
5
  * Replaces InfoCard, MediaCard, and GlowingCard molecules.
6
6
  */
7
7
 
8
- import React, { useMemo } from 'react';
8
+ import React, { useMemo, useCallback } from 'react';
9
9
  import {
10
10
  View,
11
11
  Pressable,
@@ -58,10 +58,28 @@ const CardContent: React.FC<CardContentProps> = React.memo(({
58
58
  }) => {
59
59
  const tokens = useAppDesignTokens();
60
60
 
61
+ const badgeStyle = useMemo(() => [
62
+ cardStyles.badge,
63
+ { backgroundColor: tokens.colors.primary },
64
+ ], [cardStyles.badge, tokens.colors.primary]);
65
+
66
+ const imageStyle = useMemo(() => [
67
+ cardStyles.image,
68
+ { aspectRatio: imageAspectRatio },
69
+ ], [cardStyles.image, imageAspectRatio]);
70
+
71
+ const contentContainerStyle = useMemo(() => ({
72
+ padding: paddingValue,
73
+ }), [paddingValue]);
74
+
75
+ const leftIconStyle = useMemo(() => ({ marginRight: 8 }), []);
76
+
77
+ const rightIconStyle = useMemo(() => ({ marginLeft: 8 }), []);
78
+
61
79
  return (
62
80
  <>
63
81
  {badge && (
64
- <View style={[cardStyles.badge, { backgroundColor: tokens.colors.primary }]}>
82
+ <View style={badgeStyle}>
65
83
  <AtomicText type="labelSmall" color="onPrimary">
66
84
  {badge}
67
85
  </AtomicText>
@@ -71,7 +89,7 @@ const CardContent: React.FC<CardContentProps> = React.memo(({
71
89
  {image && (
72
90
  <AtomicImage
73
91
  source={typeof image === 'string' ? { uri: image } : image}
74
- style={[cardStyles.image, { aspectRatio: imageAspectRatio }]}
92
+ style={imageStyle}
75
93
  contentFit="cover"
76
94
  />
77
95
  )}
@@ -82,7 +100,7 @@ const CardContent: React.FC<CardContentProps> = React.memo(({
82
100
  </View>
83
101
  )}
84
102
 
85
- <View style={{ padding: paddingValue }}>
103
+ <View style={contentContainerStyle}>
86
104
  {(title || leftIcon || rightIcon) && (
87
105
  <View style={cardStyles.header}>
88
106
  {leftIcon && (
@@ -90,7 +108,7 @@ const CardContent: React.FC<CardContentProps> = React.memo(({
90
108
  name={leftIcon}
91
109
  size="sm"
92
110
  color="primary"
93
- style={{ marginRight: 8 }}
111
+ style={leftIconStyle}
94
112
  />
95
113
  )}
96
114
  <View style={cardStyles.titleContainer}>
@@ -117,7 +135,7 @@ const CardContent: React.FC<CardContentProps> = React.memo(({
117
135
  name={rightIcon}
118
136
  size="sm"
119
137
  color="textSecondary"
120
- style={{ marginLeft: 8 }}
138
+ style={rightIconStyle}
121
139
  />
122
140
  )}
123
141
  </View>
@@ -190,11 +208,11 @@ const AtomicCardComponent: React.FC<AtomicCardProps> = ({
190
208
  style,
191
209
  ], [tokens.borders.radius.lg, variantStyles.container, selected, tokens.colors.primary, style]);
192
210
 
193
- const handlePress = (event: GestureResponderEvent) => {
211
+ const handlePress = useCallback((event: GestureResponderEvent) => {
194
212
  if (!disabled && onPress) {
195
213
  onPress(event);
196
214
  }
197
- };
215
+ }, [disabled, onPress]);
198
216
 
199
217
  const content = (
200
218
  <CardContent
@@ -35,11 +35,11 @@ export const DatePickerButton: React.FC<DatePickerButtonProps> = ({
35
35
  const tokens = useAppDesignTokens();
36
36
  const calendarIcon = useIconName('calendar');
37
37
 
38
- const buttonStyles = useMemo(() => StyleSheet.create({
38
+ const buttonStyles = useMemo(() => ({
39
39
  container: {
40
- flexDirection: 'row',
41
- alignItems: 'center',
42
- justifyContent: 'space-between',
40
+ flexDirection: 'row' as const,
41
+ alignItems: 'center' as const,
42
+ justifyContent: 'space-between' as const,
43
43
  paddingHorizontal: tokens.spacing.md,
44
44
  paddingVertical: tokens.spacing.sm,
45
45
  borderRadius: tokens.borders.radius.md,
@@ -71,18 +71,18 @@ export const DatePickerButton: React.FC<DatePickerButtonProps> = ({
71
71
  fontWeight: '500',
72
72
  },
73
73
  iconContainer: {
74
- flexDirection: 'row',
75
- alignItems: 'center',
74
+ flexDirection: 'row' as const,
75
+ alignItems: 'center' as const,
76
76
  gap: tokens.spacing.xs,
77
77
  },
78
78
  }), [tokens]);
79
79
 
80
- const containerStyle = [
80
+ const containerStyle = useMemo(() => [
81
81
  buttonStyles.container,
82
82
  error ? buttonStyles.containerError :
83
83
  disabled ? buttonStyles.containerDisabled :
84
84
  buttonStyles.containerDefault,
85
- ];
85
+ ], [buttonStyles, error, disabled]);
86
86
 
87
87
  const textStyle = hasValue ? buttonStyles.valueText : buttonStyles.placeholderText;
88
88
 
@@ -64,11 +64,11 @@ export const DatePickerModal: React.FC<DatePickerModalProps> = ({
64
64
  const tokens = useAppDesignTokens();
65
65
  const insets = useSafeAreaInsets();
66
66
 
67
- const modalStyles = useMemo(() => StyleSheet.create({
67
+ const modalStyles = useMemo(() => ({
68
68
  overlay: {
69
69
  flex: 1,
70
70
  backgroundColor: `rgba(0, 0, 0, ${overlayOpacity})`,
71
- justifyContent: 'flex-end',
71
+ justifyContent: 'flex-end' as const,
72
72
  },
73
73
  container: {
74
74
  backgroundColor: tokens.colors.surface,
@@ -77,9 +77,9 @@ export const DatePickerModal: React.FC<DatePickerModalProps> = ({
77
77
  paddingBottom: insets.bottom,
78
78
  },
79
79
  header: {
80
- flexDirection: 'row',
81
- justifyContent: 'space-between',
82
- alignItems: 'center',
80
+ flexDirection: 'row' as const,
81
+ justifyContent: 'space-between' as const,
82
+ alignItems: 'center' as const,
83
83
  paddingHorizontal: tokens.spacing.md,
84
84
  paddingVertical: tokens.spacing.sm,
85
85
  borderBottomWidth: 1,
@@ -87,7 +87,7 @@ export const DatePickerModal: React.FC<DatePickerModalProps> = ({
87
87
  },
88
88
  title: {
89
89
  fontSize: tokens.typography.titleLarge.fontSize,
90
- fontWeight: '600',
90
+ fontWeight: '600' as const,
91
91
  color: tokens.colors.onSurface,
92
92
  },
93
93
  doneButton: {
@@ -98,7 +98,7 @@ export const DatePickerModal: React.FC<DatePickerModalProps> = ({
98
98
  },
99
99
  doneButtonText: {
100
100
  fontSize: tokens.typography.labelMedium.fontSize,
101
- fontWeight: '500',
101
+ fontWeight: '500' as const,
102
102
  color: tokens.colors.onPrimary,
103
103
  },
104
104
  }), [overlayOpacity, tokens, insets.bottom]);
@@ -35,27 +35,6 @@ export function getFabSizes(spacingMultiplier: number): Record<'sm' | 'md' | 'lg
35
35
  };
36
36
  }
37
37
 
38
- /**
39
- * @deprecated Use getFabSizes(spacingMultiplier) instead
40
- */
41
- export const FAB_SIZES: Record<'sm' | 'md' | 'lg', FabSizeConfig> = {
42
- sm: {
43
- width: BASE_FAB_SIZES.sm,
44
- height: BASE_FAB_SIZES.sm,
45
- borderRadius: 12,
46
- },
47
- md: {
48
- width: BASE_FAB_SIZES.md,
49
- height: BASE_FAB_SIZES.md,
50
- borderRadius: 16,
51
- },
52
- lg: {
53
- width: BASE_FAB_SIZES.lg,
54
- height: BASE_FAB_SIZES.lg,
55
- borderRadius: 20,
56
- },
57
- } as const;
58
-
59
38
  /**
60
39
  * Get FAB variant configurations based on design tokens
61
40
  * @param tokens - Design tokens from theme
@@ -1,25 +1,11 @@
1
1
  /**
2
- * @deprecated This icon system is deprecated. Use @umituz/react-native-icons instead.
2
+ * Icon System - AtomicIcon and Related Utilities
3
3
  *
4
- * **Migration Guide:**
5
- * ```tsx
6
- * // Old way (deprecated)
7
- * import { AtomicIcon } from '@umituz/react-native-design-system/atoms';
8
- * <AtomicIcon name="User" size={24} color="text" />
9
- *
10
- * // New way (recommended)
11
- * import { Icon } from '@umituz/react-native-icons';
12
- * <Icon name="User" size={24} color="text" />
13
- * ```
14
- *
15
- * **Benefits of @umituz/react-native-icons:**
16
- * - ✅ Package-agnostic (works with Lucide, Expo, or custom providers)
17
- * - ✅ Lazy loading (only configured provider is loaded)
18
- * - ✅ Automatic name normalization (kebab ↔ PascalCase ↔ camelCase)
19
- * - ✅ Better performance with caching
20
- * - ✅ Zero configuration needed in DesignSystemProvider
21
- *
22
- * This module is kept for backward compatibility only and will be removed in a future version.
4
+ * Provides icon components with:
5
+ * - Configurable icon rendering (Lucide, Expo Vector Icons, custom)
6
+ * - Size presets (xs, sm, md, lg, xl, xxl) and custom sizes
7
+ * - Color tokens integration
8
+ * - Icon name normalization and caching
23
9
  */
24
10
 
25
11
  // Main Component
@@ -2,7 +2,7 @@
2
2
  * PickerModal - Selection modal for AtomicPicker
3
3
  */
4
4
 
5
- import React, { useCallback } from 'react';
5
+ import React, { useCallback, useMemo } from 'react';
6
6
  import { View, Modal, FlatList, TextInput, TouchableOpacity } from 'react-native';
7
7
  import { useSafeAreaInsets } from '../../../safe-area';
8
8
  import { useAppDesignTokens } from '../../../theme';
@@ -60,7 +60,7 @@ export const PickerModal: React.FC<PickerModalProps> = React.memo(({
60
60
  const insets = useSafeAreaInsets();
61
61
  const icons = { checkCircle: useIconName('checkCircle'), search: useIconName('search'), close: useIconName('close'), info: useIconName('info') };
62
62
 
63
- const styles = {
63
+ const styles = useMemo(() => ({
64
64
  overlay: getModalOverlayStyles(),
65
65
  container: getModalContainerStyles(tokens, 0),
66
66
  header: getModalHeaderStyles(tokens),
@@ -69,10 +69,18 @@ export const PickerModal: React.FC<PickerModalProps> = React.memo(({
69
69
  searchInput: getSearchInputStyles(tokens),
70
70
  empty: getEmptyStateStyles(tokens),
71
71
  emptyText: getEmptyStateTextStyles(tokens),
72
- };
72
+ }), [tokens]);
73
73
 
74
74
  const isSelected = useCallback((value: string) => selectedValues?.includes(value) ?? false, [selectedValues]);
75
75
 
76
+ const OPTION_HEIGHT = 56; // Approximate height of each option
77
+
78
+ const getItemLayout = useCallback((_: unknown, index: number) => ({
79
+ length: OPTION_HEIGHT,
80
+ offset: OPTION_HEIGHT * index,
81
+ index,
82
+ }), []);
83
+
76
84
  const renderOption = useCallback(({ item }: { item: PickerOption }) => {
77
85
  const selected = isSelected(item.value);
78
86
  const disabled = item.disabled || false;
@@ -117,7 +125,19 @@ export const PickerModal: React.FC<PickerModalProps> = React.memo(({
117
125
  )}
118
126
 
119
127
  {filteredOptions.length > 0 ? (
120
- <FlatList data={filteredOptions} keyExtractor={(item: PickerOption) => item.value} renderItem={renderOption} showsVerticalScrollIndicator keyboardShouldPersistTaps="handled" testID={`${testID}-list`} />
128
+ <FlatList
129
+ data={filteredOptions}
130
+ keyExtractor={(item: PickerOption) => item.value}
131
+ renderItem={renderOption}
132
+ getItemLayout={getItemLayout}
133
+ windowSize={5}
134
+ initialNumToRender={10}
135
+ maxToRenderPerBatch={5}
136
+ removeClippedSubviews
137
+ showsVerticalScrollIndicator={false}
138
+ keyboardShouldPersistTaps="handled"
139
+ testID={`${testID}-list`}
140
+ />
121
141
  ) : (
122
142
  <View style={styles.empty}>
123
143
  <AtomicIcon name={icons.info} size="xl" color="secondary" />
@@ -44,7 +44,7 @@ export interface AtomicSkeletonProps {
44
44
 
45
45
  /**
46
46
  * Skeleton loader component
47
- *
47
+ *
48
48
  * Provides visual feedback during content loading with customizable patterns
49
49
  */
50
50
  const SkeletonItem: React.FC<{
@@ -52,18 +52,16 @@ const SkeletonItem: React.FC<{
52
52
  baseColor: string;
53
53
  multiplier: number;
54
54
  }> = React.memo(({ config, baseColor, multiplier }) => {
55
- const itemStyles = useMemo(() => StyleSheet.create({
56
- item: {
57
- ...styles.skeleton,
58
- width: (typeof config.width === 'number' ? config.width * multiplier : config.width) as DimensionValue,
59
- height: config.height ? config.height * multiplier : undefined,
60
- borderRadius: config.borderRadius ? config.borderRadius * multiplier : undefined,
61
- marginBottom: config.marginBottom ? config.marginBottom * multiplier : undefined,
62
- backgroundColor: baseColor,
63
- },
55
+ const itemStyle = useMemo<ViewStyle>(() => ({
56
+ ...styles.skeleton,
57
+ width: (typeof config.width === 'number' ? config.width * multiplier : config.width) as DimensionValue,
58
+ height: config.height ? config.height * multiplier : undefined,
59
+ borderRadius: config.borderRadius ? config.borderRadius * multiplier : undefined,
60
+ marginBottom: config.marginBottom ? config.marginBottom * multiplier : undefined,
61
+ backgroundColor: baseColor,
64
62
  }), [config, baseColor, multiplier]);
65
63
 
66
- return <View style={itemStyles.item} />;
64
+ return <View style={itemStyle} />;
67
65
  });
68
66
 
69
67
  export const AtomicSkeleton: React.FC<AtomicSkeletonProps> = ({
@@ -1,12 +1,13 @@
1
- import React from "react";
2
- import { View, StyleSheet, ViewStyle } from "react-native";
1
+ import React, { useCallback, useMemo } from "react";
2
+ import { View, StyleSheet, ViewStyle, FlatList } from "react-native";
3
3
  import { useAppDesignTokens } from "../theme";
4
4
  import { CarouselScrollView } from "./CarouselScrollView";
5
5
  import { CarouselDots } from "./CarouselDots";
6
6
  import { CarouselItem } from "./CarouselItem";
7
7
  import { useCarouselScroll } from "./useCarouselScroll";
8
8
  import { calculateItemWidth } from "./carouselCalculations";
9
- import type { CarouselProps } from "./types";
9
+ import { useScreenWidth } from "../responsive/useScreenDimensions";
10
+ import type { CarouselProps, CarouselItem as CarouselItemType } from "./types";
10
11
 
11
12
  export const Carousel = <T,>({
12
13
  items,
@@ -19,7 +20,8 @@ export const Carousel = <T,>({
19
20
  style,
20
21
  }: CarouselProps<T> & { style?: ViewStyle }) => {
21
22
  const tokens = useAppDesignTokens();
22
- const calculatedItemWidth = itemWidth || calculateItemWidth(spacing);
23
+ const screenWidth = useScreenWidth(); // Reactive width
24
+ const calculatedItemWidth = itemWidth || calculateItemWidth(screenWidth, spacing);
23
25
 
24
26
  const pageWidth = calculatedItemWidth + spacing;
25
27
 
@@ -32,26 +34,47 @@ export const Carousel = <T,>({
32
34
  return null;
33
35
  }
34
36
 
37
+ // Stable key extractor
38
+ const keyExtractor = useCallback((item: CarouselItemType<T>) => item.id, []);
39
+
40
+ // Memoized render item to prevent unnecessary re-renders
41
+ const renderCarouselItem = useCallback(({ item, index }: { item: CarouselItemType<T>; index: number }) => (
42
+ <CarouselItem
43
+ item={item}
44
+ itemWidth={calculatedItemWidth}
45
+ renderContent={(itemData) => renderItem(itemData, index)}
46
+ style={
47
+ index < items.length - 1 ? { marginRight: spacing } : undefined
48
+ }
49
+ />
50
+ ), [calculatedItemWidth, renderItem, items.length, spacing]);
51
+
52
+ // Get item layout for better performance
53
+ const getItemLayout = useCallback((_: unknown, index: number) => ({
54
+ length: calculatedItemWidth + spacing,
55
+ offset: (calculatedItemWidth + spacing) * index,
56
+ index,
57
+ }), [calculatedItemWidth, spacing]);
58
+
59
+ // Use FlatList for virtualization (renders only visible items)
35
60
  return (
36
61
  <View style={[styles.container, style]}>
37
- <CarouselScrollView
62
+ <FlatList
63
+ horizontal
64
+ data={items}
65
+ renderItem={renderCarouselItem}
66
+ keyExtractor={keyExtractor}
67
+ getItemLayout={getItemLayout}
38
68
  onScroll={handleScroll}
39
69
  pagingEnabled={pagingEnabled}
40
- spacing={spacing}
41
- itemWidth={calculatedItemWidth}
42
- >
43
- {items.map((item, index) => (
44
- <CarouselItem
45
- key={item.id}
46
- item={item}
47
- itemWidth={calculatedItemWidth}
48
- renderContent={(itemData) => renderItem(itemData, index)}
49
- style={
50
- index < items.length - 1 ? { marginRight: spacing } : undefined
51
- }
52
- />
53
- ))}
54
- </CarouselScrollView>
70
+ snapToInterval={pageWidth}
71
+ decelerationRate="fast"
72
+ showsHorizontalScrollIndicator={false}
73
+ windowSize={3} // Render 3 screens worth of items
74
+ initialNumToRender={2} // Start with 2 items
75
+ maxToRenderPerBatch={2} // Batch rendering
76
+ removeClippedSubviews
77
+ />
55
78
 
56
79
  {showDots && items.length > 1 && (
57
80
  <CarouselDots
@@ -1,9 +1,16 @@
1
- import { Dimensions } from "react-native";
1
+ /**
2
+ * Carousel Calculations
3
+ *
4
+ * Utility functions for carousel calculations.
5
+ * Note: Pass screenWidth from useScreenWidth() hook to ensure
6
+ * reactivity on orientation changes, iPad Split View, etc.
7
+ */
2
8
 
3
- const { width: SCREEN_WIDTH } = Dimensions.get("window");
4
-
5
- export const calculateItemWidth = (padding: number = 16): number => {
6
- return SCREEN_WIDTH - padding * 2;
9
+ export const calculateItemWidth = (
10
+ screenWidth: number,
11
+ padding: number = 16
12
+ ): number => {
13
+ return screenWidth - padding * 2;
7
14
  };
8
15
 
9
16
  export const calculateIndexFromScroll = (
@@ -12,7 +19,3 @@ export const calculateIndexFromScroll = (
12
19
  ): number => {
13
20
  return Math.round(scrollPosition / itemWidth);
14
21
  };
15
-
16
- export const getScreenWidth = (): number => {
17
- return SCREEN_WIDTH;
18
- };
@@ -10,7 +10,6 @@ export { useCarouselScroll } from "./useCarouselScroll";
10
10
  export {
11
11
  calculateItemWidth,
12
12
  calculateIndexFromScroll,
13
- getScreenWidth,
14
13
  } from "./carouselCalculations";
15
14
 
16
15
  export { CarouselDots } from "./CarouselDots";
@@ -3,23 +3,21 @@
3
3
  *
4
4
  * Uses expo-device for system-level tablet detection,
5
5
  * then uses screen dimensions for iPad-specific sub-categories.
6
+ *
7
+ * ⚠️ NOTE: These functions use Dimensions.get() which doesn't update on
8
+ * device rotation, iPad Split View, or Stage Manager. For reactive detection
9
+ * that updates on dimension changes, consider using useWindowDimensions() hook
10
+ * directly in your components.
6
11
  */
7
12
 
8
13
  import { Dimensions } from 'react-native';
9
14
  import { IPAD_BREAKPOINTS } from './iPadBreakpoints';
10
15
  import { isTablet, isLandscape } from './deviceDetection';
11
16
 
12
- /**
13
- * Detect if the current device is an iPad (or Android tablet)
14
- * Uses expo-device for accurate system-level detection
15
- */
16
17
  export function isIPad(): boolean {
17
18
  return isTablet();
18
19
  }
19
20
 
20
- /**
21
- * Detect if the current device is an iPad mini
22
- */
23
21
  export function isIPadMini(): boolean {
24
22
  if (!isIPad()) return false;
25
23
 
@@ -28,9 +26,6 @@ export function isIPadMini(): boolean {
28
26
  return minWidth < IPAD_BREAKPOINTS.IPAD_AIR;
29
27
  }
30
28
 
31
- /**
32
- * Detect if the current device is an iPad Pro (12.9")
33
- */
34
29
  export function isIPadPro(): boolean {
35
30
  if (!isIPad()) return false;
36
31
 
@@ -39,10 +34,6 @@ export function isIPadPro(): boolean {
39
34
  return minWidth >= IPAD_BREAKPOINTS.IPAD_11_PRO;
40
35
  }
41
36
 
42
- /**
43
- * Check if tablet device is in landscape orientation
44
- * Uses shared isLandscape detection for consistency
45
- */
46
37
  export function isIPadLandscape(): boolean {
47
38
  return isLandscape();
48
39
  }