@umituz/react-native-onboarding 3.6.10 → 3.6.12

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": "@umituz/react-native-onboarding",
3
- "version": "3.6.10",
3
+ "version": "3.6.12",
4
4
  "description": "Advanced onboarding flow for React Native apps with personalization questions, theme-aware colors, animations, and customizable slides. SOLID, DRY, KISS principles applied.",
5
5
  "main": "./src/index.ts",
6
6
  "types": "./src/index.ts",
@@ -86,6 +86,42 @@ export interface OnboardingSlide {
86
86
  */
87
87
  backgroundImage?: any;
88
88
 
89
+ /**
90
+ * Optional multiple background images (URLs or require paths)
91
+ * Displayed in a collage/grid pattern behind content
92
+ * If provided, takes precedence over single backgroundImage
93
+ */
94
+ backgroundImages?: any[];
95
+
96
+ /**
97
+ * Layout pattern for multiple background images
98
+ * 'grid' - Equal sized grid (auto columns)
99
+ * 'dense' - Dense grid with many small images (6 columns)
100
+ * 'masonry' - Pinterest-style masonry layout
101
+ * 'collage' - Random sizes and positions
102
+ * 'scattered' - Small randomly placed images
103
+ * 'tiles' - Fixed size tiles centered
104
+ * 'honeycomb' - Hexagonal pattern
105
+ * Default: 'grid'
106
+ */
107
+ backgroundImagesLayout?: 'grid' | 'dense' | 'masonry' | 'collage' | 'scattered' | 'tiles' | 'honeycomb';
108
+
109
+ /**
110
+ * Number of columns for grid-based layouts
111
+ * Only applies to: grid, dense, masonry, tiles
112
+ */
113
+ backgroundImagesColumns?: number;
114
+
115
+ /**
116
+ * Gap between images in pixels
117
+ */
118
+ backgroundImagesGap?: number;
119
+
120
+ /**
121
+ * Border radius for images
122
+ */
123
+ backgroundImagesBorderRadius?: number;
124
+
89
125
  /**
90
126
  * Optional background video (URL or require path)
91
127
  * Plays in loop behind content
package/src/index.ts CHANGED
@@ -69,8 +69,16 @@ export { OnboardingScreen, type OnboardingScreenProps } from "./presentation/scr
69
69
  export { OnboardingHeader, type OnboardingHeaderProps } from "./presentation/components/OnboardingHeader";
70
70
  export { OnboardingFooter, type OnboardingFooterProps } from "./presentation/components/OnboardingFooter";
71
71
  export { OnboardingProvider, type OnboardingProviderProps, useOnboardingProvider } from "./presentation/providers/OnboardingProvider";
72
+ export { BackgroundImageCollage, type CollageLayout, type BackgroundImageCollageProps } from "./presentation/components/BackgroundImageCollage";
72
73
  export type { OnboardingTheme, OnboardingColors } from "./presentation/types/OnboardingTheme";
73
74
 
75
+ // =============================================================================
76
+ // UTILITIES - Helper functions
77
+ // =============================================================================
78
+
79
+ export { ensureArray, safeIncludes, safeFilter } from "./infrastructure/utils/arrayUtils";
80
+ export type { ImageLayoutItem, LayoutConfig } from "./infrastructure/utils/imageLayoutUtils";
81
+
74
82
  // Export OnboardingSlide component
75
83
  // Note: TypeScript doesn't allow exporting both a type and a value with the same name
76
84
  // The type is exported above as OnboardingSlide
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Array Utilities
3
+ * Safe array operations for onboarding components
4
+ */
5
+
6
+ /**
7
+ * Ensures the value is a valid array, returns empty array if not
8
+ */
9
+ export const ensureArray = <T>(value: T[] | undefined | null): T[] => {
10
+ return Array.isArray(value) ? value : [];
11
+ };
12
+
13
+ /**
14
+ * Safe includes check that handles undefined/null values
15
+ */
16
+ export const safeIncludes = <T>(array: T[] | undefined | null, item: T): boolean => {
17
+ return ensureArray(array).includes(item);
18
+ };
19
+
20
+ /**
21
+ * Safe filter that handles undefined/null values
22
+ */
23
+ export const safeFilter = <T>(
24
+ array: T[] | undefined | null,
25
+ predicate: (item: T) => boolean,
26
+ ): T[] => {
27
+ return ensureArray(array).filter(predicate);
28
+ };
@@ -0,0 +1,280 @@
1
+ /**
2
+ * Image Layout Utilities
3
+ * Layout generators for BackgroundImageCollage component
4
+ */
5
+
6
+ import { Dimensions } from "react-native";
7
+
8
+ const { width: SCREEN_WIDTH, height: SCREEN_HEIGHT } = Dimensions.get("window");
9
+
10
+ export interface ImageLayoutStyle {
11
+ position: "absolute";
12
+ top: number;
13
+ left: number;
14
+ width: number;
15
+ height: number;
16
+ borderRadius?: number;
17
+ }
18
+
19
+ export interface ImageLayoutItem {
20
+ source: unknown;
21
+ style: ImageLayoutStyle;
22
+ }
23
+
24
+ export interface LayoutConfig {
25
+ columns?: number;
26
+ gap?: number;
27
+ borderRadius?: number;
28
+ randomizeSize?: boolean;
29
+ }
30
+
31
+ /**
32
+ * Generate a uniform grid layout
33
+ */
34
+ export const generateGridLayout = (
35
+ images: unknown[],
36
+ config: LayoutConfig = {},
37
+ ): ImageLayoutItem[] => {
38
+ const { columns, gap = 0, borderRadius = 0 } = config;
39
+ const count = images.length;
40
+ const cols = columns ?? Math.ceil(Math.sqrt(count));
41
+ const rows = Math.ceil(count / cols);
42
+ const totalGapX = gap * (cols - 1);
43
+ const totalGapY = gap * (rows - 1);
44
+ const cellWidth = (SCREEN_WIDTH - totalGapX) / cols;
45
+ const cellHeight = (SCREEN_HEIGHT - totalGapY) / rows;
46
+
47
+ return images.map((source, index) => {
48
+ const col = index % cols;
49
+ const row = Math.floor(index / cols);
50
+
51
+ return {
52
+ source,
53
+ style: {
54
+ position: "absolute" as const,
55
+ left: col * (cellWidth + gap),
56
+ top: row * (cellHeight + gap),
57
+ width: cellWidth,
58
+ height: cellHeight,
59
+ borderRadius,
60
+ },
61
+ };
62
+ });
63
+ };
64
+
65
+ /**
66
+ * Generate a dense grid layout with many small images
67
+ */
68
+ export const generateDenseGridLayout = (
69
+ images: unknown[],
70
+ config: LayoutConfig = {},
71
+ ): ImageLayoutItem[] => {
72
+ const { columns = 6, gap = 2, borderRadius = 4 } = config;
73
+ const count = images.length;
74
+ const rows = Math.ceil(count / columns);
75
+ const totalGapX = gap * (columns - 1);
76
+ const totalGapY = gap * (rows - 1);
77
+ const cellWidth = (SCREEN_WIDTH - totalGapX) / columns;
78
+ const cellHeight = (SCREEN_HEIGHT - totalGapY) / rows;
79
+
80
+ return images.map((source, index) => {
81
+ const col = index % columns;
82
+ const row = Math.floor(index / columns);
83
+
84
+ return {
85
+ source,
86
+ style: {
87
+ position: "absolute" as const,
88
+ left: col * (cellWidth + gap),
89
+ top: row * (cellHeight + gap),
90
+ width: cellWidth,
91
+ height: cellHeight,
92
+ borderRadius,
93
+ },
94
+ };
95
+ });
96
+ };
97
+
98
+ /**
99
+ * Generate a masonry-style layout (Pinterest-like)
100
+ */
101
+ export const generateMasonryLayout = (
102
+ images: unknown[],
103
+ config: LayoutConfig = {},
104
+ ): ImageLayoutItem[] => {
105
+ const { columns = 3, gap = 2, borderRadius = 4 } = config;
106
+ const colWidth = (SCREEN_WIDTH - gap * (columns - 1)) / columns;
107
+ const columnHeights = new Array(columns).fill(0);
108
+
109
+ return images.map((source) => {
110
+ const shortestCol = columnHeights.indexOf(Math.min(...columnHeights));
111
+ const aspectRatio = 0.7 + Math.random() * 0.6;
112
+ const height = colWidth * aspectRatio;
113
+
114
+ const layout: ImageLayoutItem = {
115
+ source,
116
+ style: {
117
+ position: "absolute" as const,
118
+ left: shortestCol * (colWidth + gap),
119
+ top: columnHeights[shortestCol],
120
+ width: colWidth,
121
+ height,
122
+ borderRadius,
123
+ },
124
+ };
125
+
126
+ columnHeights[shortestCol] += height + gap;
127
+ return layout;
128
+ });
129
+ };
130
+
131
+ /**
132
+ * Generate a random collage layout with varying sizes
133
+ */
134
+ export const generateCollageLayout = (
135
+ images: unknown[],
136
+ config: LayoutConfig = {},
137
+ ): ImageLayoutItem[] => {
138
+ const { borderRadius = 8 } = config;
139
+ const layouts: ImageLayoutItem[] = [];
140
+ const minSize = Math.min(SCREEN_WIDTH, SCREEN_HEIGHT) * 0.15;
141
+ const maxSize = Math.min(SCREEN_WIDTH, SCREEN_HEIGHT) * 0.35;
142
+
143
+ images.forEach((source) => {
144
+ const size = minSize + Math.random() * (maxSize - minSize);
145
+ const maxX = SCREEN_WIDTH - size;
146
+ const maxY = SCREEN_HEIGHT - size;
147
+
148
+ let attempts = 0;
149
+ let overlaps = true;
150
+ let left = 0;
151
+ let top = 0;
152
+
153
+ while (overlaps && attempts < 50) {
154
+ left = Math.random() * maxX;
155
+ top = Math.random() * maxY;
156
+ overlaps = false;
157
+
158
+ for (const existing of layouts) {
159
+ const overlapThreshold = 20;
160
+ if (
161
+ left < existing.style.left + existing.style.width - overlapThreshold &&
162
+ left + size > existing.style.left + overlapThreshold &&
163
+ top < existing.style.top + existing.style.height - overlapThreshold &&
164
+ top + size > existing.style.top + overlapThreshold
165
+ ) {
166
+ overlaps = true;
167
+ break;
168
+ }
169
+ }
170
+
171
+ attempts++;
172
+ }
173
+
174
+ layouts.push({
175
+ source,
176
+ style: {
177
+ position: "absolute" as const,
178
+ left,
179
+ top,
180
+ width: size,
181
+ height: size,
182
+ borderRadius,
183
+ },
184
+ });
185
+ });
186
+
187
+ return layouts;
188
+ };
189
+
190
+ /**
191
+ * Generate a scattered layout with small images
192
+ */
193
+ export const generateScatteredLayout = (
194
+ images: unknown[],
195
+ config: LayoutConfig = {},
196
+ ): ImageLayoutItem[] => {
197
+ const { borderRadius = 6 } = config;
198
+ const minSize = 60;
199
+ const maxSize = 100;
200
+
201
+ return images.map((source) => {
202
+ const size = minSize + Math.random() * (maxSize - minSize);
203
+ const left = Math.random() * (SCREEN_WIDTH - size);
204
+ const top = Math.random() * (SCREEN_HEIGHT - size);
205
+
206
+ return {
207
+ source,
208
+ style: {
209
+ position: "absolute" as const,
210
+ left,
211
+ top,
212
+ width: size,
213
+ height: size,
214
+ borderRadius,
215
+ },
216
+ };
217
+ });
218
+ };
219
+
220
+ /**
221
+ * Generate a tiles layout with fixed small size
222
+ */
223
+ export const generateTilesLayout = (
224
+ images: unknown[],
225
+ config: LayoutConfig = {},
226
+ ): ImageLayoutItem[] => {
227
+ const { columns = 5, gap = 4, borderRadius = 8 } = config;
228
+ const tileSize = (SCREEN_WIDTH - gap * (columns + 1)) / columns;
229
+ const rows = Math.ceil(images.length / columns);
230
+ const startY = (SCREEN_HEIGHT - rows * (tileSize + gap)) / 2;
231
+
232
+ return images.map((source, index) => {
233
+ const col = index % columns;
234
+ const row = Math.floor(index / columns);
235
+
236
+ return {
237
+ source,
238
+ style: {
239
+ position: "absolute" as const,
240
+ left: gap + col * (tileSize + gap),
241
+ top: startY + row * (tileSize + gap),
242
+ width: tileSize,
243
+ height: tileSize,
244
+ borderRadius,
245
+ },
246
+ };
247
+ });
248
+ };
249
+
250
+ /**
251
+ * Generate a honeycomb/hex layout
252
+ */
253
+ export const generateHoneycombLayout = (
254
+ images: unknown[],
255
+ config: LayoutConfig = {},
256
+ ): ImageLayoutItem[] => {
257
+ const { borderRadius = 50 } = config;
258
+ const size = 80;
259
+ const horizontalSpacing = size * 0.85;
260
+ const verticalSpacing = size * 0.75;
261
+ const columns = Math.floor(SCREEN_WIDTH / horizontalSpacing);
262
+
263
+ return images.map((source, index) => {
264
+ const row = Math.floor(index / columns);
265
+ const col = index % columns;
266
+ const offsetX = row % 2 === 1 ? horizontalSpacing / 2 : 0;
267
+
268
+ return {
269
+ source,
270
+ style: {
271
+ position: "absolute" as const,
272
+ left: col * horizontalSpacing + offsetX,
273
+ top: row * verticalSpacing,
274
+ width: size,
275
+ height: size,
276
+ borderRadius,
277
+ },
278
+ };
279
+ });
280
+ };
@@ -0,0 +1,93 @@
1
+ /**
2
+ * Background Image Collage Component
3
+ * Displays multiple images in various layout patterns
4
+ */
5
+
6
+ import React, { useMemo } from "react";
7
+ import { View, StyleSheet } from "react-native";
8
+ import { Image } from "expo-image";
9
+ import {
10
+ generateGridLayout,
11
+ generateDenseGridLayout,
12
+ generateMasonryLayout,
13
+ generateCollageLayout,
14
+ generateScatteredLayout,
15
+ generateTilesLayout,
16
+ generateHoneycombLayout,
17
+ type ImageLayoutItem,
18
+ type LayoutConfig,
19
+ } from "../../infrastructure/utils/imageLayoutUtils";
20
+ import { ensureArray } from "../../infrastructure/utils/arrayUtils";
21
+
22
+ export type CollageLayout =
23
+ | "grid"
24
+ | "dense"
25
+ | "masonry"
26
+ | "collage"
27
+ | "scattered"
28
+ | "tiles"
29
+ | "honeycomb";
30
+
31
+ export interface BackgroundImageCollageProps {
32
+ images: unknown[];
33
+ layout?: CollageLayout;
34
+ columns?: number;
35
+ gap?: number;
36
+ borderRadius?: number;
37
+ opacity?: number;
38
+ }
39
+
40
+ const getLayoutGenerator = (layout: CollageLayout) => {
41
+ const generators: Record<
42
+ CollageLayout,
43
+ (images: unknown[], config: LayoutConfig) => ImageLayoutItem[]
44
+ > = {
45
+ grid: generateGridLayout,
46
+ dense: generateDenseGridLayout,
47
+ masonry: generateMasonryLayout,
48
+ collage: generateCollageLayout,
49
+ scattered: generateScatteredLayout,
50
+ tiles: generateTilesLayout,
51
+ honeycomb: generateHoneycombLayout,
52
+ };
53
+
54
+ return generators[layout] ?? generateGridLayout;
55
+ };
56
+
57
+ export const BackgroundImageCollage: React.FC<BackgroundImageCollageProps> = ({
58
+ images,
59
+ layout = "grid",
60
+ columns,
61
+ gap,
62
+ borderRadius,
63
+ opacity = 1,
64
+ }) => {
65
+ const safeImages = ensureArray(images);
66
+
67
+ const imageLayouts = useMemo(() => {
68
+ if (safeImages.length === 0) return [];
69
+
70
+ const generator = getLayoutGenerator(layout);
71
+ const config: LayoutConfig = { columns, gap, borderRadius };
72
+
73
+ return generator(safeImages, config);
74
+ }, [safeImages, layout, columns, gap, borderRadius]);
75
+
76
+ if (imageLayouts.length === 0) {
77
+ return null;
78
+ }
79
+
80
+ return (
81
+ <View style={[StyleSheet.absoluteFill, { opacity }]} pointerEvents="none">
82
+ {imageLayouts.map((item, index) => (
83
+ <Image
84
+ key={index}
85
+ source={item.source}
86
+ style={item.style}
87
+ contentFit="cover"
88
+ transition={300}
89
+ />
90
+ ))}
91
+ </View>
92
+ );
93
+ };
@@ -9,6 +9,7 @@ import { LinearGradient } from "expo-linear-gradient";
9
9
  import { Image } from "expo-image";
10
10
  import type { OnboardingSlide } from "../../domain/entities/OnboardingSlide";
11
11
  import { BackgroundVideo } from "./BackgroundVideo";
12
+ import { BackgroundImageCollage } from "./BackgroundImageCollage";
12
13
 
13
14
  interface OnboardingBackgroundProps {
14
15
  currentSlide: OnboardingSlide | undefined;
@@ -35,6 +36,18 @@ export const OnboardingBackground: React.FC<OnboardingBackgroundProps> = ({
35
36
  );
36
37
  }
37
38
 
39
+ if (currentSlide.backgroundImages && currentSlide.backgroundImages.length > 0) {
40
+ return (
41
+ <BackgroundImageCollage
42
+ images={currentSlide.backgroundImages}
43
+ layout={currentSlide.backgroundImagesLayout || "grid"}
44
+ columns={currentSlide.backgroundImagesColumns}
45
+ gap={currentSlide.backgroundImagesGap}
46
+ borderRadius={currentSlide.backgroundImagesBorderRadius}
47
+ />
48
+ );
49
+ }
50
+
38
51
  if (currentSlide.backgroundImage) {
39
52
  return (
40
53
  <Image
@@ -53,7 +53,9 @@ export const OnboardingScreenContent = ({
53
53
  });
54
54
 
55
55
  const hasMedia =
56
- !!currentSlide?.backgroundImage || !!currentSlide?.backgroundVideo;
56
+ !!currentSlide?.backgroundImage ||
57
+ !!currentSlide?.backgroundVideo ||
58
+ (!!currentSlide?.backgroundImages && currentSlide.backgroundImages.length > 0);
57
59
  const overlayOpacity = currentSlide?.overlayOpacity ?? 0.5;
58
60
  const effectivelyUseGradient = useGradient || hasMedia;
59
61
 
@@ -2,6 +2,7 @@ import React from "react";
2
2
  import { View, TouchableOpacity, StyleSheet } from "react-native";
3
3
  import { AtomicIcon, AtomicText } from "@umituz/react-native-design-system";
4
4
  import { useOnboardingProvider } from "../../providers/OnboardingProvider";
5
+ import { ensureArray } from "../../../infrastructure/utils/arrayUtils";
5
6
  import type { OnboardingQuestion, QuestionOption } from "../../../domain/entities/OnboardingQuestion";
6
7
 
7
8
  export interface MultipleChoiceQuestionProps {
@@ -12,17 +13,19 @@ export interface MultipleChoiceQuestionProps {
12
13
 
13
14
  export const MultipleChoiceQuestion = ({
14
15
  question,
15
- value = [],
16
+ value,
16
17
  onChange,
17
18
  }: MultipleChoiceQuestionProps) => {
18
19
  const {
19
20
  theme: { colors },
20
21
  } = useOnboardingProvider();
21
22
 
23
+ const safeValue = ensureArray(value);
24
+
22
25
  const handleToggle = (optionId: string) => {
23
- const newValue = value.includes(optionId)
24
- ? value.filter((id) => id !== optionId)
25
- : [...value, optionId];
26
+ const newValue = safeValue.includes(optionId)
27
+ ? safeValue.filter((id) => id !== optionId)
28
+ : [...safeValue, optionId];
26
29
 
27
30
  if (question.validation?.maxSelections && newValue.length > question.validation.maxSelections) {
28
31
  return;
@@ -30,7 +33,7 @@ export const MultipleChoiceQuestion = ({
30
33
  onChange(newValue);
31
34
  };
32
35
  const renderOption = (option: QuestionOption) => {
33
- const isSelected = value.includes(option.id);
36
+ const isSelected = safeValue.includes(option.id);
34
37
  const isEmoji = option.iconType === 'emoji';
35
38
 
36
39
  return (