react-native-tuto-showcase 1.0.6 → 2.0.0

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/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # 🔦 **react-native-tuto-showcase**
2
2
 
3
- > Fully customizable **Spotlight / Tutorial / Coachmark overlay** for React Native — perfect for onboarding, feature discovery & guided tours.
3
+ **Fully Customizable Spotlight / Tutorial / Coachmark Overlay for React Native — Perfect for Onboarding, Feature Discovery & Guided Tours**
4
4
 
5
5
  [![npm version](https://img.shields.io/npm/v/react-native-tuto-showcase.svg)](https://www.npmjs.com/package/react-native-tuto-showcase)
6
6
  [![license](https://img.shields.io/npm/l/react-native-tuto-showcase.svg)](https://github.com/ahmedhegazydev/react-native-tuto-showcase/blob/master/LICENSE)
@@ -15,7 +15,7 @@ A lightweight yet powerful **spotlight / walkthrough / coachmark** component for
15
15
  * Explain **complex UI**
16
16
  * Drive **feature adoption & discovery**
17
17
 
18
- Now with **full-screen Lottie placement** + **custom offsets**.
18
+ Now with **full-screen Lottie placement**, **custom offsets**, and **smart tooltips**.
19
19
 
20
20
  ---
21
21
 
@@ -40,6 +40,13 @@ Now with **full-screen Lottie placement** + **custom offsets**.
40
40
  * `bottom-left`, `bottom-center`, `bottom-right`
41
41
  * `center`
42
42
  * Fine-tune with `lottieOffset={{ dx, dy }}`
43
+ * 💬 **Tooltips (Coachmark Bubble) — NEW**
44
+
45
+ * Custom tooltip bubble near the spotlight target
46
+ * Smart placement: `auto | above | below`
47
+ * RTL-aware arrow positioning (`left | center`)
48
+ * Custom arrow inset & spacing
49
+ * Fully customizable background & layout
43
50
  * 📦 **Multiple spots on one screen**
44
51
 
45
52
  * Chain calls on the same ref (circle / rect / gestures)
@@ -52,8 +59,8 @@ Now with **full-screen Lottie placement** + **custom offsets**.
52
59
 
53
60
  * Overlay color
54
61
  * Button container & text style
55
- * Custom title & description JSX
56
- * Works great with your design system / theming
62
+ * Custom title / description JSX
63
+ * Optional tooltip bubble
57
64
  * 📱 **Cross-platform**
58
65
 
59
66
  * iOS & Android
@@ -72,7 +79,7 @@ npm install react-native-tuto-showcase
72
79
  yarn add react-native-tuto-showcase
73
80
  ```
74
81
 
75
- No extra native setup required beyond your standard React Native project.
82
+ No extra native setup required.
76
83
 
77
84
  ---
78
85
 
@@ -92,10 +99,7 @@ export default function Home() {
92
99
  <TutoShowcase
93
100
  ref={tutoRef}
94
101
  title={<Text style={{ color: '#fff', fontSize: 22 }}>ترتيب الأقسام</Text>}
95
- description={
96
- 'يمكنك سحب هذا القسم لأعلى أو لأسفل لتغيير ترتيبه.\n' +
97
- 'سيتم حفظ الترتيب تلقائيًا.'
98
- }
102
+ description="يمكنك سحب هذا القسم لأعلى أو لأسفل لتغيير ترتيبه."
99
103
  buttonText="تمام"
100
104
  />
101
105
 
@@ -124,13 +128,13 @@ export default function Home() {
124
128
 
125
129
  ---
126
130
 
127
- ## 🎞 Lottie Placement (New)
131
+ ## 🎞 Lottie Placement
128
132
 
129
- By default, the Lottie view is positioned **relative to the full screen**, so you can use it as a **hero pointer / hand animation** independent from the spotlight.
133
+ The Lottie animation is positioned **relative to the full screen**, not the spotlight.
130
134
 
131
- ### 🔹 Available placements
135
+ ### Available placements
132
136
 
133
- ```text
137
+ ```
134
138
  top-left
135
139
  top-center
136
140
  top-right
@@ -140,7 +144,7 @@ bottom-right
140
144
  center
141
145
  ```
142
146
 
143
- ### 🔹 Example
147
+ ### Example
144
148
 
145
149
  ```tsx
146
150
  import LottieView from 'lottie-react-native';
@@ -162,54 +166,88 @@ import LottieView from 'lottie-react-native';
162
166
 
163
167
  ---
164
168
 
169
+ ## 💬 Tooltip (New)
170
+
171
+ Render a **custom tooltip bubble** near the highlighted target instead of the default title/description section.
172
+
173
+ ### Example
174
+
175
+ ```tsx
176
+ <TutoShowcase
177
+ ref={tutoRef}
178
+ tooltip={
179
+ <Text style={{ color: '#fff', fontSize: 16, lineHeight: 22 }}>
180
+ Tap here to open your profile settings
181
+ </Text>
182
+ }
183
+ tooltipPlacement="auto"
184
+ tooltipArrowSide="left"
185
+ tooltipArrowInset={24}
186
+ tooltipGap={14}
187
+ tooltipBackgroundColor="#3B3563"
188
+ />
189
+ ```
190
+
191
+ ### Notes
192
+
193
+ * When `tooltip` is provided, the default `title / description` section will **not render**.
194
+ * Tooltip placement is calculated based on the **first spotlight target**.
195
+ * Tooltip arrow automatically flips based on placement (`above / below`).
196
+ * Fully RTL-aware.
197
+
198
+ ---
199
+
165
200
  ## 🧩 `<TutoShowcase />` Props
166
201
 
167
- | Prop | Type | Default | Description |
168
- | ------------------------ | ------------------------------ | ------------------ | ------------------------------------ |
169
- | `title` | `ReactNode` | — | Title JSX (text, icons, etc.) |
170
- | `description` | `string \| ReactNode` | — | Description text or custom JSX |
171
- | `buttonText` | `string` | `"GOT IT"` | CTA button label |
172
- | `buttonTextStyle` | `TextStyle` | — | Style for CTA text |
173
- | `buttonContainerStyle` | `ViewStyle` | — | Style for CTA button wrapper |
174
- | `overlayBackgroundColor` | `string` | `rgba(0,0,0,0.78)` | Dim background overlay color |
175
- | `onGotIt` | `() => void` | — | Callback when CTA is pressed |
176
- | `lottie` | `ReactElement` | — | Lottie animation component |
177
- | `lottiePlacement` | `"top-left" \| ...` | `"top-center"` | Full-screen placement of the Lottie |
178
- | `lottieOffset` | `{ dx?: number; dy?: number }` | `{}` | Extra offset applied after placement |
179
-
180
- > 💡 You can pass any **custom JSX** for `title` / `description` to plug into your design system (e.g. `Text`, `Heading`, `Icon + Text`, etc.)
202
+ | Prop | Type | Default | Description |
203
+ | ------------------------ | ------------------------------ | ------------------ | ------------------- |
204
+ | `title` | `ReactNode` | — | Title JSX |
205
+ | `description` | `string \| ReactNode` | — | Description content |
206
+ | `buttonText` | `string` | `"GOT IT"` | CTA label |
207
+ | `buttonTextStyle` | `TextStyle` | — | CTA text style |
208
+ | `buttonContainerStyle` | `ViewStyle` | — | CTA container style |
209
+ | `overlayBackgroundColor` | `string` | `rgba(0,0,0,0.78)` | Overlay color |
210
+ | `onGotIt` | `() => void` | — | CTA callback |
211
+ | `lottie` | `ReactElement` | — | Lottie animation |
212
+ | `lottiePlacement` | `Placement` | `"top-center"` | Lottie position |
213
+ | `lottieOffset` | `{ dx?: number; dy?: number }` | `{}` | Lottie offset |
214
+ | `tooltip` | `ReactNode` | — | Tooltip content |
215
+ | `tooltipOffset` | `{ dx?: number; dy?: number }` | `{}` | Tooltip offset |
216
+ | `tooltipPlacement` | `"auto" \| "above" \| "below"` | `"auto"` | Tooltip placement |
217
+ | `tooltipArrowSide` | `"left" \| "center"` | `"left"` | Arrow alignment |
218
+ | `tooltipArrowInset` | `number` | `24` | Arrow inset |
219
+ | `tooltipGap` | `number` | `14` | Gap from target |
220
+ | `tooltipArrowGap` | `number` | `6` | Extra arrow gap |
221
+ | `tooltipEdgePadding` | `number` | `12` | Screen edge padding |
222
+ | `tooltipBackgroundColor` | `string` | `#3B3563` | Tooltip background |
181
223
 
182
224
  ---
183
225
 
184
226
  ## 🎛 Ref API
185
227
 
186
228
  ```ts
187
- import { TutoShowcaseHandle } from 'react-native-tuto-showcase';
188
-
189
229
  const tutoRef = useRef<TutoShowcaseHandle>(null);
190
230
  ```
191
231
 
192
- | Method | Description |
193
- | -------------------------------------- | ---------------------------------------------- |
194
- | `on(ref)` | Attach spotlight to a target element `ref` |
195
- | `addCircle(ratio?)` | Add **circular** spotlight |
196
- | `addRoundRect(ratio?, radius?, opts?)` | Add **rounded-rectangle** spotlight |
197
- | `withBorder()` | Add border around spotlight |
198
- | `displaySwipableLeft()` | Show swipe-left gesture hint |
199
- | `displaySwipableRight()` | Show swipe-right gesture hint |
200
- | `displayScrollable()` | Show scroll gesture hint |
201
- | `onClick(cb)` | Capture taps inside spotlight area |
202
- | `show()` | Show tutorial immediately |
203
- | `showOnce(key)` | Show only once per key (saved in AsyncStorage) |
204
- | `resetShowOnce(key)` | Clear stored key; show again next time |
205
- | `isShowOnce(key)` | Check if a specific key has already been shown |
232
+ | Method | Description |
233
+ | -------------------------------------- | -------------------------- |
234
+ | `on(ref)` | Attach spotlight to target |
235
+ | `addCircle(ratio?)` | Circular spotlight |
236
+ | `addRoundRect(ratio?, radius?, opts?)` | Rounded rectangle |
237
+ | `withBorder()` | Border around spotlight |
238
+ | `displaySwipableLeft()` | Swipe-left hint |
239
+ | `displaySwipableRight()` | Swipe-right hint |
240
+ | `displayScrollable()` | Scroll hint |
241
+ | `onClick(cb)` | Capture clicks |
242
+ | `show()` | Show immediately |
243
+ | `showOnce(key)` | Show once only |
244
+ | `resetShowOnce(key)` | Reset key |
245
+ | `isShowOnce(key)` | Check key |
206
246
 
207
247
  ---
208
248
 
209
249
  ## 🗂 Show Once Logic
210
250
 
211
- Use `showOnce(key)` to avoid spamming users with the same tutorial:
212
-
213
251
  ```tsx
214
252
  tutoRef.current
215
253
  ?.on(boxRef)
@@ -217,111 +255,33 @@ tutoRef.current
217
255
  .showOnce('first-time-highlight');
218
256
  ```
219
257
 
220
- To reset:
258
+ Reset:
221
259
 
222
260
  ```tsx
223
261
  tutoRef.current?.resetShowOnce('first-time-highlight');
224
262
  ```
225
263
 
226
- To check:
227
-
228
- ```tsx
229
- const alreadyShown = await tutoRef.current?.isShowOnce('first-time-highlight');
230
- ```
231
-
232
- ---
233
-
234
- ## 🎨 Button & Overlay Customization
235
-
236
- ```tsx
237
- <TutoShowcase
238
- overlayBackgroundColor="rgba(15, 23, 42, 0.85)" // slate-like
239
- buttonContainerStyle={{
240
- backgroundColor: '#FFD700',
241
- borderRadius: 999,
242
- paddingHorizontal: 24,
243
- paddingVertical: 10,
244
- }}
245
- buttonTextStyle={{
246
- color: '#000',
247
- fontWeight: 'bold',
248
- fontSize: 16,
249
- }}
250
- />
251
- ```
252
-
253
- ---
254
-
255
- ## 📐 Spotlight Examples
256
-
257
- ### Circle
258
-
259
- ```tsx
260
- tutoRef.current
261
- ?.on(boxRef)
262
- .addCircle(1.3) // 1.0 = tight, >1 expands radius
263
- .show();
264
- ```
265
-
266
- ### Rounded Rectangle
267
-
268
- ```tsx
269
- tutoRef.current
270
- ?.on(boxRef)
271
- .addRoundRect(1.1, 20, { pad: 30 }) // ratio, corner radius, extra padding
272
- .show();
273
- ```
274
-
275
- ---
276
-
277
- ## 🎥 Demo Videos
278
-
279
- ### 📱 iOS
280
-
281
- [View iOS demo GIF](https://github.com/ahmedhegazydev/react-native-tuto-showcase/blob/master/src/assets/SimulatorScreenRecording-iPhone16Pro-2025-11-24at14.14.46online-video-cutter.com-ezgif.com-video-to-gif-converter.gif)
282
-
283
- ### 🤖 Android
284
-
285
- [View Android demo GIF](https://github.com/ahmedhegazydev/react-native-tuto-showcase/blob/master/src/assets/ScreenRecording2025-12-02at7.52.23PMonline-video-cutter.com-ezgif.com-video-to-gif-converter.gif)
286
-
287
264
  ---
288
265
 
289
266
  ## 🧠 Typical Use Cases
290
267
 
291
- * App **onboarding** & first-time user experience
292
- * Feature discovery (“What’s new” walkthroughs)
293
- * Highlighting **FAB buttons**, menus, tabs, drawers
294
- * Teaching users **drag & drop** / **reorder** interactions
295
- * Coachmarks for:
296
-
297
- * Dashboards
298
- * Settings screens
299
- * Maps & complex UI
300
- * Internal QA / UX testing sessions
301
-
302
- ---
303
-
304
- ## 🛠 Troubleshooting
305
-
306
- | Issue | Hint |
307
- | ------------------------------- | ------------------------------------------------------------ |
308
- | Spotlight not aligned correctly | Ensure the target has a `ref` and is rendered on screen |
309
- | Nothing shows up | Check `tutoRef.current` is not `null` before calling methods |
310
- | `showOnce` not working | Verify AsyncStorage is properly linked & no key collisions |
311
- | Lottie is cut or off-screen | Adjust `lottiePlacement` + `lottieOffset` |
268
+ * App onboarding
269
+ * Feature discovery
270
+ * Coachmarks
271
+ * Dashboards & settings
272
+ * Drag & drop education
273
+ * UX testing
312
274
 
313
275
  ---
314
276
 
315
277
  ## 🗺 Roadmap
316
278
 
317
- * ✅ Multiple-step walkthrough sequences
318
- * ✅ Better gesture presets
319
- * 🔜 RTL-aware layouts
320
- * 🔜 Built-in themes (light / dark)
321
- * 🔜 Auto “Next / Previous between spots
322
- * 🔜 Expo Snack examples
323
-
324
- > PRs & feature requests are very welcome ✨
279
+ * ✅ Tooltips
280
+ * ✅ Lottie full-screen placement
281
+ * 🔜 Multi-step sequences
282
+ * 🔜 Built-in themes
283
+ * 🔜 Next / Previous navigation
284
+ * 🔜 Expo examples
325
285
 
326
286
  ---
327
287
 
@@ -330,17 +290,10 @@ tutoRef.current
330
290
  Built with ❤️ by **Ahmed Hegazy**
331
291
 
332
292
  * 📧 [ahmedmhegazy.eg@gmail.com](mailto:ahmedmhegazy.eg@gmail.com)
333
- * 🐙 GitHub: [@ahmedhegazydev](https://github.com/ahmedhegazydev)
293
+ * 🐙 GitHub: @ahmedhegazydev
334
294
 
335
295
  ---
336
296
 
337
297
  ## 📄 License
338
298
 
339
- MIT © **Ahmed Mohamed Ali Ali Hegazy**
340
-
341
- ---
342
-
343
- ## 🔍 SEO / Keywords
344
-
345
- **Keywords:**
346
- `react native tutorial` · `react native onboarding` · `react native coach marks` · `react native walkthrough` · `react native spotlight` · `react native highlight view` · `react native feature discovery` · `react native guided tour` · `react native tooltip overlay` · `react native help overlay` · `react native lottie overlay` · `react native interactive tutorial` · `react native drag reorder tutorial` · `react native product tour` · `react native showcase` · `react native hint` · `react native gesture hint` · `react native ux onboarding` · `react native step by step tutorial`
299
+ MIT © **Ahmed Mohamed Ali Ali Hegazy**
@@ -1,10 +1,7 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { View, Text, Pressable, StyleSheet } from 'react-native';
3
3
  export default function ContentSection({ title, description, buttonText, buttonTextStyle, buttonContainerStyle, coords, onPress, }) {
4
- return (_jsxs(View, { style: [
5
- styles.contentBase,
6
- coords,
7
- ], children: [title ? _jsx(View, { style: styles.titleContainer, children: title }) : null, description ? (_jsx(View, { style: styles.description, children: description })) : null, _jsx(Pressable, { style: [styles.cta, buttonContainerStyle], onPress: onPress, children: _jsx(Text, { style: [styles.ctaText, buttonTextStyle], children: buttonText || 'GOT IT' }) })] }));
4
+ return (_jsxs(View, { style: [styles.contentBase, coords], children: [title ? _jsx(View, { style: styles.titleContainer, children: title }) : null, description ? _jsx(View, { style: styles.description, children: description }) : null, _jsx(Pressable, { style: [styles.cta, buttonContainerStyle], onPress: onPress, children: _jsx(Text, { style: [styles.ctaText, buttonTextStyle], children: buttonText || 'GOT IT' }) })] }));
8
5
  }
9
6
  const styles = StyleSheet.create({
10
7
  contentBase: {
@@ -1,6 +1,6 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import React from 'react';
3
- import { View, StyleSheet } from 'react-native';
3
+ import { View, StyleSheet, } from 'react-native';
4
4
  const PADDING_H = 24;
5
5
  const PADDING_V = 32;
6
6
  export default function LottieAboveTarget({ shapes, lottie, overlayWidth, overlayHeight, placement = 'top-center', // 👈 الديفولت: فوق في النص بالنسبة للشاشة كلها
@@ -56,7 +56,7 @@ export function buildActionsFactory(setVisible, measureOverlayOrigin, setShapes,
56
56
  const lr = toLocalRect(r, overlayOrigin);
57
57
  setClickZones(p => [
58
58
  ...p,
59
- { x: lr.x, y: lr.y, w: lr.width, h: lr.height, onClick }
59
+ { x: lr.x, y: lr.y, w: lr.width, h: lr.height, onClick },
60
60
  ]);
61
61
  });
62
62
  return expose;
@@ -107,10 +107,7 @@ export function buildActionsFactory(setVisible, measureOverlayOrigin, setShapes,
107
107
  y += opts.dy;
108
108
  const cx = x + w / 2;
109
109
  const cy = y + h / 2;
110
- setShapes(p => [
111
- ...p,
112
- { type: 'roundrect', x, y, w, h, cx, cy, radius }
113
- ]);
110
+ setShapes(p => [...p, { type: 'roundrect', x, y, w, h, cx, cy, radius }]);
114
111
  setClickZones(p => [...p, { x, y, w, h, onClick: settings.onClick }]);
115
112
  });
116
113
  return editor(api);
@@ -122,7 +119,7 @@ export function buildActionsFactory(setVisible, measureOverlayOrigin, setShapes,
122
119
  const lr = toLocalRect(r, overlayOrigin);
123
120
  setHints(p => [
124
121
  ...p,
125
- { kind: 'left', x: lr.x + lr.width * 0.7, y: lr.y + lr.height / 2 }
122
+ { kind: 'left', x: lr.x + lr.width * 0.7, y: lr.y + lr.height / 2 },
126
123
  ]);
127
124
  });
128
125
  return actionEditor(api);
@@ -132,10 +129,7 @@ export function buildActionsFactory(setVisible, measureOverlayOrigin, setShapes,
132
129
  if (!r)
133
130
  return;
134
131
  const lr = toLocalRect(r, overlayOrigin);
135
- setHints(p => [
136
- ...p,
137
- { kind: 'right', x: lr.x, y: lr.y + lr.height / 2 }
138
- ]);
132
+ setHints(p => [...p, { kind: 'right', x: lr.x, y: lr.y + lr.height / 2 }]);
139
133
  });
140
134
  return actionEditor(api);
141
135
  },
@@ -146,7 +140,7 @@ export function buildActionsFactory(setVisible, measureOverlayOrigin, setShapes,
146
140
  const lr = toLocalRect(r, overlayOrigin);
147
141
  setHints(p => [
148
142
  ...p,
149
- { kind: 'scroll', x: lr.x + lr.width / 2, y: lr.y + lr.height * 0.1 }
143
+ { kind: 'scroll', x: lr.x + lr.width / 2, y: lr.y + lr.height * 0.1 },
150
144
  ]);
151
145
  });
152
146
  return actionEditor(api);
@@ -177,7 +171,7 @@ export function buildActionsFactory(setVisible, measureOverlayOrigin, setShapes,
177
171
  animated: a => {
178
172
  settings.animated = a;
179
173
  return actionEditor(api);
180
- }
174
+ },
181
175
  };
182
176
  return api;
183
177
  };
@@ -15,29 +15,29 @@ export function usePulseAnimation(visible) {
15
15
  toValue: SPOT_SCALE_MAX,
16
16
  duration: SPOT_PULSE_MS,
17
17
  easing: Easing.out(Easing.quad),
18
- useNativeDriver: true
18
+ useNativeDriver: true,
19
19
  }),
20
20
  Animated.timing(opacity, {
21
21
  toValue: 0.85,
22
22
  duration: SPOT_PULSE_MS,
23
23
  easing: Easing.out(Easing.quad),
24
- useNativeDriver: true
25
- })
24
+ useNativeDriver: true,
25
+ }),
26
26
  ]),
27
27
  Animated.parallel([
28
28
  Animated.timing(scale, {
29
29
  toValue: SPOT_SCALE_MIN,
30
30
  duration: SPOT_PULSE_MS,
31
31
  easing: Easing.in(Easing.quad),
32
- useNativeDriver: true
32
+ useNativeDriver: true,
33
33
  }),
34
34
  Animated.timing(opacity, {
35
35
  toValue: 1,
36
36
  duration: SPOT_PULSE_MS,
37
37
  easing: Easing.in(Easing.quad),
38
- useNativeDriver: true
39
- })
40
- ])
38
+ useNativeDriver: true,
39
+ }),
40
+ ]),
41
41
  ]));
42
42
  loop.start();
43
43
  return () => loop.stop();
package/dist/index.d.ts CHANGED
@@ -22,6 +22,25 @@ type TutoShowcaseProps = {
22
22
  dx?: number;
23
23
  dy?: number;
24
24
  };
25
+ tooltip?: React.ReactNode;
26
+ tooltipOffset?: {
27
+ dx?: number;
28
+ dy?: number;
29
+ };
30
+ tooltipArrowSide?: 'left' | 'center';
31
+ tooltipArrowInset?: number;
32
+ tooltipPlacement?: 'auto' | 'above' | 'below';
33
+ tooltipEdgePadding?: number;
34
+ tooltipGap?: number;
35
+ tooltipArrowGap?: number;
36
+ tooltipBackgroundColor?: string;
37
+ overlayPadding?: {
38
+ top?: number;
39
+ right?: number;
40
+ bottom?: number;
41
+ left?: number;
42
+ };
43
+ allowOverflow?: boolean;
25
44
  };
26
45
  declare const TutoShowcase: React.ForwardRefExoticComponent<TutoShowcaseProps & React.RefAttributes<TutoShowcaseHandle>>;
27
46
  export default TutoShowcase;
package/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { forwardRef, useState, useCallback, useImperativeHandle, useEffect, } from 'react';
3
- import { Modal, TouchableWithoutFeedback, View, StyleSheet, Dimensions, } from 'react-native';
2
+ import { forwardRef, useState, useCallback, useImperativeHandle, useEffect, useMemo, } from 'react';
3
+ import { Modal, TouchableWithoutFeedback, View, StyleSheet, Dimensions, I18nManager, } from 'react-native';
4
4
  import Overlay from './components/Overlay';
5
5
  import GestureHints from './components/GestureHints';
6
6
  import ContentSection from './components/ContentSection';
@@ -11,21 +11,25 @@ import { usePulseAnimation } from './hooks/usePulseAnimation';
11
11
  import { buildActionsFactory } from './helpers/buildActions';
12
12
  import { useContentPosition } from './hooks/useContentPosition';
13
13
  const DEFAULT_BG = 'rgba(0,0,0,0.78)';
14
+ const ARROW_H = 12;
15
+ function getBBox(shape) {
16
+ return { x: shape.x, y: shape.y, w: shape.w, h: shape.h };
17
+ }
14
18
  const TutoShowcase = forwardRef(function Tuto({ title, description, buttonText = 'GOT IT', buttonTextStyle, buttonContainerStyle, overlayBackgroundColor, onGotIt, lottie, lottiePlacement, // 👈 خدناه من props
15
- lottieOffset, }, ref) {
19
+ lottieOffset, tooltip, tooltipOffset, tooltipArrowSide = 'left', tooltipArrowInset = 24, tooltipPlacement = 'auto', tooltipEdgePadding = 12, tooltipGap = 14, tooltipArrowGap = 6, tooltipBackgroundColor = '#3B3563', overlayPadding, allowOverflow, }, ref) {
16
20
  // -------- Hooks يجب أن تكون في الأعلى وبنفس الترتيب دائماً --------
17
21
  const [visible, setVisible] = useState(false);
18
22
  const [bgColor, setBgColor] = useState(overlayBackgroundColor || DEFAULT_BG);
19
23
  const [shapes, setShapes] = useState([]);
20
24
  const [hints, setHints] = useState([]);
21
25
  const [clickZones, setClickZones] = useState([]);
22
- const { rootRef, overlayOrigin, overlaySize, onLayout, measureOverlayOrigin, } = useOverlayMeasurements();
26
+ const { rootRef, overlayOrigin, overlaySize, onLayout, measureOverlayOrigin } = useOverlayMeasurements();
23
27
  const { scale: spotScale, opacity: spotOpacity } = usePulseAnimation(visible);
24
28
  const dims = Dimensions.get('window');
25
29
  const width = overlaySize.width || dims.width;
26
30
  const height = overlaySize.height || dims.height;
27
31
  const coords = useContentPosition(visible, shapes, height);
28
- const { shownKeys, loaded, markShown, reset: resetShowOnce, isShown, } = useShowOnce();
32
+ const { shownKeys, loaded, markShown, reset: resetShowOnce, isShown } = useShowOnce();
29
33
  // لو اتغير الـ prop من برّه نحدث الـ state
30
34
  useEffect(() => {
31
35
  if (overlayBackgroundColor) {
@@ -82,12 +86,129 @@ lottieOffset, }, ref) {
82
86
  }
83
87
  }
84
88
  }, [clickZones]);
89
+ const [tooltipSize, setTooltipSize] = useState({ w: 0, h: 0 });
90
+ const targetBox = useMemo(() => {
91
+ if (!shapes?.length)
92
+ return null;
93
+ return getBBox(shapes[0]);
94
+ }, [shapes]);
95
+ const tooltipPos = useMemo(() => {
96
+ if (!targetBox)
97
+ return null;
98
+ const dx = tooltipOffset?.dx ?? 0;
99
+ const dy = tooltipOffset?.dy ?? 0;
100
+ const padding = tooltipEdgePadding;
101
+ // 1) left centered on target + clamp
102
+ let left = targetBox.x + targetBox.w / 2 - tooltipSize.w / 2 + dx;
103
+ left = Math.max(padding, Math.min(left, width - tooltipSize.w - padding));
104
+ // 2) compute above/below
105
+ const gap = tooltipGap;
106
+ // arrowGap only for BELOW (because arrow is on top when below)
107
+ const arrowGapAbove = 0;
108
+ const arrowGapBelow = tooltipArrowGap;
109
+ const topAbove = targetBox.y - tooltipSize.h - ARROW_H - gap - arrowGapAbove + dy;
110
+ const topBelow = targetBox.y + targetBox.h + ARROW_H + gap + arrowGapBelow + dy;
111
+ const fitsAbove = topAbove >= padding;
112
+ const fitsBelow = topBelow + tooltipSize.h <= height - padding;
113
+ // 3) choose placement
114
+ let placement = tooltipPlacement === 'above'
115
+ ? 'above'
116
+ : tooltipPlacement === 'below'
117
+ ? 'below'
118
+ : fitsAbove
119
+ ? 'above'
120
+ : 'below';
121
+ if (placement === 'below' && !fitsBelow && fitsAbove)
122
+ placement = 'above';
123
+ if (placement === 'above' && !fitsAbove && fitsBelow)
124
+ placement = 'below';
125
+ // 4) final top clamp
126
+ let top = placement === 'above' ? topAbove : topBelow;
127
+ top = Math.max(padding, Math.min(top, height - tooltipSize.h - padding));
128
+ // 5) Arrow position inside tooltip
129
+ const inset = tooltipArrowInset;
130
+ const isRTL = I18nManager.isRTL;
131
+ let arrowLeft;
132
+ if (tooltipArrowSide === 'center') {
133
+ arrowLeft = Math.max(20, Math.min(tooltipSize.w - 40, tooltipSize.w / 2 - 14));
134
+ }
135
+ else {
136
+ // start edge: LTR->left, RTL->right
137
+ arrowLeft = !isRTL ? Math.max(20, tooltipSize.w - inset - 28) : inset;
138
+ }
139
+ return { left, top, arrowLeft, placement };
140
+ }, [
141
+ targetBox,
142
+ tooltipSize,
143
+ width,
144
+ height,
145
+ tooltipOffset,
146
+ tooltipArrowSide,
147
+ tooltipArrowInset,
148
+ tooltipPlacement,
149
+ tooltipEdgePadding,
150
+ tooltipGap,
151
+ tooltipArrowGap,
152
+ ]);
153
+ const shouldRenderContentSection = !tooltip && (title || description);
154
+ const pad = overlayPadding || {};
155
+ const padRight = pad.right ?? 0;
156
+ const padLeft = pad.left ?? 0;
85
157
  // ✅ الشرط هنا بعد كل الـ hooks
86
158
  if (!visible)
87
159
  return null;
88
- return (_jsx(Modal, { visible: true, transparent: true, animationType: "fade", children: _jsx(TouchableWithoutFeedback, { onPress: onOverlayPress, children: _jsxs(View, { ref: rootRef, onLayout: onLayout, style: [StyleSheet.absoluteFill, { direction: 'ltr' }], children: [_jsx(Overlay, { width: width, height: height, shapes: shapes, bgColor: bgColor, spotScale: spotScale, spotOpacity: spotOpacity }), lottie && (_jsx(LottieAboveTarget, { shapes: shapes, lottie: lottie, overlayWidth: width, overlayHeight: height, placement: lottiePlacement, offset: lottieOffset })), (title || description) && (_jsx(ContentSection, { title: title, description: description, buttonText: buttonText, buttonTextStyle: buttonTextStyle, buttonContainerStyle: buttonContainerStyle, coords: coords, onPress: () => {
89
- onGotIt?.();
90
- dismiss();
91
- } })), _jsx(GestureHints, { hints: hints })] }) }) }));
160
+ return (_jsx(Modal, { visible: true, transparent: true, animationType: "fade", children: _jsx(TouchableWithoutFeedback, { onPress: onOverlayPress, children: _jsxs(View, { ref: rootRef, onLayout: onLayout, style: [StyleSheet.absoluteFill, { direction: 'ltr' }], children: [_jsx(Overlay, { width: width, height: height, shapes: shapes, bgColor: bgColor, spotScale: spotScale, spotOpacity: spotOpacity }), _jsxs(View, { pointerEvents: "box-none", style: [
161
+ StyleSheet.absoluteFill,
162
+ {
163
+ paddingLeft: padLeft,
164
+ paddingRight: padRight,
165
+ overflow: allowOverflow ? 'visible' : 'hidden',
166
+ },
167
+ ], children: [lottie && (_jsx(LottieAboveTarget, { shapes: shapes, lottie: lottie, overlayWidth: width, overlayHeight: height, placement: lottiePlacement, offset: lottieOffset })), !!tooltip && !!tooltipPos && (_jsxs(View, { onLayout: e => {
168
+ const w = e.nativeEvent.layout.width;
169
+ const h = e.nativeEvent.layout.height;
170
+ if (w !== tooltipSize.w || h !== tooltipSize.h)
171
+ setTooltipSize({ w, h });
172
+ }, style: {
173
+ position: 'absolute',
174
+ left: tooltipPos.left,
175
+ top: tooltipPos.top,
176
+ }, children: [_jsx(View, { style: {
177
+ backgroundColor: tooltipBackgroundColor,
178
+ borderRadius: 28,
179
+ paddingVertical: 22,
180
+ paddingHorizontal: 26,
181
+ minWidth: 280,
182
+ maxWidth: width - 24,
183
+ }, children: tooltip }), _jsx(View, { style: tooltipPos.placement === 'above'
184
+ ? {
185
+ position: 'absolute',
186
+ bottom: -ARROW_H + 2,
187
+ left: tooltipPos.arrowLeft,
188
+ width: 0,
189
+ height: 0,
190
+ borderLeftWidth: 14,
191
+ borderRightWidth: 14,
192
+ borderTopWidth: ARROW_H,
193
+ borderLeftColor: 'transparent',
194
+ borderRightColor: 'transparent',
195
+ borderTopColor: tooltipBackgroundColor,
196
+ }
197
+ : {
198
+ position: 'absolute',
199
+ top: -ARROW_H + 2,
200
+ left: tooltipPos.arrowLeft,
201
+ width: 0,
202
+ height: 0,
203
+ borderLeftWidth: 14,
204
+ borderRightWidth: 14,
205
+ borderBottomWidth: ARROW_H,
206
+ borderLeftColor: 'transparent',
207
+ borderRightColor: 'transparent',
208
+ borderBottomColor: tooltipBackgroundColor,
209
+ } })] })), shouldRenderContentSection && (_jsx(ContentSection, { title: title, description: description, buttonText: buttonText, buttonTextStyle: buttonTextStyle, buttonContainerStyle: buttonContainerStyle, coords: coords, onPress: () => {
210
+ onGotIt?.();
211
+ dismiss();
212
+ } })), _jsx(GestureHints, { hints: hints })] })] }) }) }));
92
213
  });
93
214
  export default TutoShowcase;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-native-tuto-showcase",
3
- "version": "1.0.6",
3
+ "version": "2.0.0",
4
4
  "description": "Customizable tutorial / spotlight overlay for React Native (onboarding, feature tours, coachmarks).",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -13,7 +13,9 @@
13
13
  "url": "https://github.com/ahmedhegazydev/react-native-tuto-showcase.git"
14
14
  },
15
15
  "scripts": {
16
- "build": "tsc"
16
+ "build": "tsc",
17
+ "format": "prettier --write \"src/**/*.{js,jsx,ts,tsx,json,md}\"",
18
+ "format:check": "prettier --check \"src/**/*.{js,jsx,ts,tsx,json,md}\""
17
19
  },
18
20
  "keywords": [
19
21
  "react-native",
@@ -22,7 +24,26 @@
22
24
  "spotlight",
23
25
  "showcase",
24
26
  "coachmark",
25
- "overlay"
27
+ "overlay",
28
+ "react native tutorial",
29
+ "react native onboarding",
30
+ "react native coach marks",
31
+ "react native walkthrough",
32
+ "react native spotlight",
33
+ "react native highlight view",
34
+ "react native feature discovery",
35
+ "react native guided tour",
36
+ "react native tooltip overlay",
37
+ "react native help overlay",
38
+ "react native lottie overlay",
39
+ "react native interactive tutorial",
40
+ "react native drag reorder tutorial",
41
+ "react native product tour",
42
+ "react native showcase",
43
+ "react native hint",
44
+ "react native gesture hint",
45
+ "react native ux onboarding",
46
+ "react native step by step tutorial"
26
47
  ],
27
48
  "author": "Ahmed Mohamed Ali Ali Hegazy",
28
49
  "license": "MIT",
@@ -37,6 +58,7 @@
37
58
  "@react-native-async-storage/async-storage": "^1.23.0",
38
59
  "@types/react": "^19.2.7",
39
60
  "@types/react-native": "^0.72.8",
61
+ "prettier": "^3.7.4",
40
62
  "react-native-svg": "^13.14.0",
41
63
  "typescript": "^5.9.3"
42
64
  }