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 +99 -146
- package/dist/components/ContentSection.js +1 -4
- package/dist/components/LottieAboveTarget.js +1 -1
- package/dist/helpers/buildActions.js +6 -12
- package/dist/hooks/usePulseAnimation.js +7 -7
- package/dist/index.d.ts +19 -0
- package/dist/index.js +130 -9
- package/package.json +25 -3
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# 🔦 **react-native-tuto-showcase**
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
**Fully Customizable Spotlight / Tutorial / Coachmark Overlay for React Native — Perfect for Onboarding, Feature Discovery & Guided Tours**
|
|
4
4
|
|
|
5
5
|
[](https://www.npmjs.com/package/react-native-tuto-showcase)
|
|
6
6
|
[](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**
|
|
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
|
|
56
|
-
*
|
|
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
|
|
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
|
|
131
|
+
## 🎞 Lottie Placement
|
|
128
132
|
|
|
129
|
-
|
|
133
|
+
The Lottie animation is positioned **relative to the full screen**, not the spotlight.
|
|
130
134
|
|
|
131
|
-
###
|
|
135
|
+
### Available placements
|
|
132
136
|
|
|
133
|
-
```
|
|
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
|
-
###
|
|
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
|
|
170
|
-
| `description` | `string \| ReactNode` | — | Description
|
|
171
|
-
| `buttonText` | `string` | `"GOT IT"` | CTA
|
|
172
|
-
| `buttonTextStyle` | `TextStyle` | — |
|
|
173
|
-
| `buttonContainerStyle` | `ViewStyle` | — |
|
|
174
|
-
| `overlayBackgroundColor` | `string` | `rgba(0,0,0,0.78)` |
|
|
175
|
-
| `onGotIt` | `() => void` | — |
|
|
176
|
-
| `lottie` | `ReactElement` | — | Lottie animation
|
|
177
|
-
| `lottiePlacement` | `
|
|
178
|
-
| `lottieOffset` | `{ dx?: number; dy?: number }` | `{}` |
|
|
179
|
-
|
|
180
|
-
|
|
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
|
|
195
|
-
| `addCircle(ratio?)` |
|
|
196
|
-
| `addRoundRect(ratio?, radius?, opts?)` |
|
|
197
|
-
| `withBorder()` |
|
|
198
|
-
| `displaySwipableLeft()` |
|
|
199
|
-
| `displaySwipableRight()` |
|
|
200
|
-
| `displayScrollable()` |
|
|
201
|
-
| `onClick(cb)` | Capture
|
|
202
|
-
| `show()` | Show
|
|
203
|
-
| `showOnce(key)` | Show
|
|
204
|
-
| `resetShowOnce(key)` |
|
|
205
|
-
| `isShowOnce(key)` | Check
|
|
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
|
-
|
|
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
|
|
292
|
-
* Feature discovery
|
|
293
|
-
*
|
|
294
|
-
*
|
|
295
|
-
*
|
|
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
|
-
* ✅
|
|
318
|
-
* ✅
|
|
319
|
-
* 🔜
|
|
320
|
-
* 🔜 Built-in themes
|
|
321
|
-
* 🔜
|
|
322
|
-
* 🔜 Expo
|
|
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:
|
|
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
|
|
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
|
|
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 }),
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
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": "
|
|
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
|
}
|