@korsolutions/guidon 1.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.
@@ -0,0 +1,291 @@
1
+ import { useState, useEffect, useCallback } from 'react';
2
+
3
+ // src/persistence/adapters.ts
4
+ var STORAGE_KEY_PREFIX = "@guidon:";
5
+ var createNoopAdapter = () => ({
6
+ loadProgress: async () => null,
7
+ saveProgress: async () => {
8
+ },
9
+ loadAllProgress: async () => ({}),
10
+ clearProgress: async () => {
11
+ }
12
+ });
13
+ var createMemoryAdapter = () => {
14
+ const store = {};
15
+ return {
16
+ loadProgress: async (guidonId) => store[guidonId] ?? null,
17
+ saveProgress: async (progress) => {
18
+ store[progress.guidonId] = progress;
19
+ },
20
+ loadAllProgress: async () => ({ ...store }),
21
+ clearProgress: async (guidonId) => {
22
+ delete store[guidonId];
23
+ }
24
+ };
25
+ };
26
+ var createLocalStorageAdapter = (keyPrefix = STORAGE_KEY_PREFIX) => ({
27
+ loadProgress: async (guidonId) => {
28
+ if (typeof window === "undefined" || !window.localStorage) {
29
+ return null;
30
+ }
31
+ try {
32
+ const data = localStorage.getItem(`${keyPrefix}${guidonId}`);
33
+ return data ? JSON.parse(data) : null;
34
+ } catch {
35
+ return null;
36
+ }
37
+ },
38
+ saveProgress: async (progress) => {
39
+ if (typeof window === "undefined" || !window.localStorage) {
40
+ return;
41
+ }
42
+ try {
43
+ localStorage.setItem(
44
+ `${keyPrefix}${progress.guidonId}`,
45
+ JSON.stringify(progress)
46
+ );
47
+ } catch {
48
+ }
49
+ },
50
+ loadAllProgress: async () => {
51
+ if (typeof window === "undefined" || !window.localStorage) {
52
+ return {};
53
+ }
54
+ const result = {};
55
+ try {
56
+ for (let i = 0; i < localStorage.length; i++) {
57
+ const key = localStorage.key(i);
58
+ if (key?.startsWith(keyPrefix)) {
59
+ const data = localStorage.getItem(key);
60
+ if (data) {
61
+ const progress = JSON.parse(data);
62
+ result[progress.guidonId] = progress;
63
+ }
64
+ }
65
+ }
66
+ } catch {
67
+ }
68
+ return result;
69
+ },
70
+ clearProgress: async (guidonId) => {
71
+ if (typeof window === "undefined" || !window.localStorage) {
72
+ return;
73
+ }
74
+ try {
75
+ localStorage.removeItem(`${keyPrefix}${guidonId}`);
76
+ } catch {
77
+ }
78
+ }
79
+ });
80
+ var createAsyncStorageAdapter = (asyncStorage, keyPrefix = STORAGE_KEY_PREFIX) => ({
81
+ loadProgress: async (guidonId) => {
82
+ try {
83
+ const data = await asyncStorage.getItem(`${keyPrefix}${guidonId}`);
84
+ return data ? JSON.parse(data) : null;
85
+ } catch {
86
+ return null;
87
+ }
88
+ },
89
+ saveProgress: async (progress) => {
90
+ try {
91
+ await asyncStorage.setItem(
92
+ `${keyPrefix}${progress.guidonId}`,
93
+ JSON.stringify(progress)
94
+ );
95
+ } catch {
96
+ }
97
+ },
98
+ loadAllProgress: async () => {
99
+ const result = {};
100
+ try {
101
+ const allKeys = await asyncStorage.getAllKeys();
102
+ const relevantKeys = allKeys.filter((key) => key.startsWith(keyPrefix));
103
+ const pairs = await asyncStorage.multiGet(relevantKeys);
104
+ for (const [, value] of pairs) {
105
+ if (value) {
106
+ const progress = JSON.parse(value);
107
+ result[progress.guidonId] = progress;
108
+ }
109
+ }
110
+ } catch {
111
+ }
112
+ return result;
113
+ },
114
+ clearProgress: async (guidonId) => {
115
+ try {
116
+ await asyncStorage.removeItem(`${keyPrefix}${guidonId}`);
117
+ } catch {
118
+ }
119
+ }
120
+ });
121
+ var createApiAdapter = (handlers) => {
122
+ const noopAdapter = createNoopAdapter();
123
+ return {
124
+ loadProgress: handlers.loadProgress ?? noopAdapter.loadProgress,
125
+ saveProgress: handlers.saveProgress ?? noopAdapter.saveProgress,
126
+ loadAllProgress: handlers.loadAllProgress ?? noopAdapter.loadAllProgress,
127
+ clearProgress: handlers.clearProgress ?? noopAdapter.clearProgress
128
+ };
129
+ };
130
+ var createCompositeAdapter = (adapters) => ({
131
+ loadProgress: async (guidonId) => {
132
+ for (const adapter of adapters) {
133
+ const progress = await adapter.loadProgress(guidonId);
134
+ if (progress) return progress;
135
+ }
136
+ return null;
137
+ },
138
+ saveProgress: async (progress) => {
139
+ await Promise.all(adapters.map((adapter) => adapter.saveProgress(progress)));
140
+ },
141
+ loadAllProgress: async () => {
142
+ const result = {};
143
+ for (const adapter of adapters) {
144
+ if (adapter.loadAllProgress) {
145
+ const data = await adapter.loadAllProgress();
146
+ Object.assign(result, data);
147
+ }
148
+ }
149
+ return result;
150
+ },
151
+ clearProgress: async (guidonId) => {
152
+ await Promise.all(
153
+ adapters.map((adapter) => adapter.clearProgress?.(guidonId))
154
+ );
155
+ }
156
+ });
157
+ function useGuidonPersistence(adapter, guidonId) {
158
+ const [progress, setProgress] = useState(null);
159
+ const [isLoading, setIsLoading] = useState(true);
160
+ const [error, setError] = useState(null);
161
+ useEffect(() => {
162
+ if (!adapter) {
163
+ setIsLoading(false);
164
+ return;
165
+ }
166
+ let mounted = true;
167
+ const loadProgress = async () => {
168
+ try {
169
+ setIsLoading(true);
170
+ setError(null);
171
+ const data = await adapter.loadProgress(guidonId);
172
+ if (mounted) {
173
+ setProgress(data);
174
+ }
175
+ } catch (err) {
176
+ if (mounted) {
177
+ setError(err instanceof Error ? err.message : "Failed to load progress");
178
+ }
179
+ } finally {
180
+ if (mounted) {
181
+ setIsLoading(false);
182
+ }
183
+ }
184
+ };
185
+ loadProgress();
186
+ return () => {
187
+ mounted = false;
188
+ };
189
+ }, [adapter, guidonId]);
190
+ const saveProgress = useCallback(
191
+ async (newProgress) => {
192
+ if (!adapter) return;
193
+ const fullProgress = {
194
+ ...newProgress,
195
+ guidonId
196
+ };
197
+ try {
198
+ setError(null);
199
+ await adapter.saveProgress(fullProgress);
200
+ setProgress(fullProgress);
201
+ } catch (err) {
202
+ setError(err instanceof Error ? err.message : "Failed to save progress");
203
+ throw err;
204
+ }
205
+ },
206
+ [adapter, guidonId]
207
+ );
208
+ const clearProgress = useCallback(async () => {
209
+ if (!adapter?.clearProgress) return;
210
+ try {
211
+ setError(null);
212
+ await adapter.clearProgress(guidonId);
213
+ setProgress(null);
214
+ } catch (err) {
215
+ setError(err instanceof Error ? err.message : "Failed to clear progress");
216
+ throw err;
217
+ }
218
+ }, [adapter, guidonId]);
219
+ const markCompleted = useCallback(async () => {
220
+ const currentCount = progress?.completionCount ?? 0;
221
+ await saveProgress({
222
+ completed: true,
223
+ completedAt: (/* @__PURE__ */ new Date()).toISOString(),
224
+ completionCount: currentCount + 1
225
+ });
226
+ }, [saveProgress, progress?.completionCount]);
227
+ const markStepViewed = useCallback(
228
+ async (stepIndex) => {
229
+ await saveProgress({
230
+ completed: progress?.completed ?? false,
231
+ lastStepIndex: stepIndex,
232
+ completedAt: progress?.completedAt,
233
+ completionCount: progress?.completionCount
234
+ });
235
+ },
236
+ [saveProgress, progress]
237
+ );
238
+ return {
239
+ progress,
240
+ isLoading,
241
+ error,
242
+ isCompleted: progress?.completed ?? false,
243
+ hasStarted: progress !== null,
244
+ saveProgress,
245
+ clearProgress,
246
+ markCompleted,
247
+ markStepViewed
248
+ };
249
+ }
250
+ function useShouldShowGuidon(adapter, guidonId, options) {
251
+ const { progress, isLoading } = useGuidonPersistence(adapter, guidonId);
252
+ const [shouldShow, setShouldShow] = useState(false);
253
+ const [isChecking, setIsChecking] = useState(true);
254
+ useEffect(() => {
255
+ if (isLoading) return;
256
+ const checkCondition = async () => {
257
+ setIsChecking(true);
258
+ if (options?.forceShow) {
259
+ setShouldShow(true);
260
+ setIsChecking(false);
261
+ return;
262
+ }
263
+ if (progress?.completed) {
264
+ setShouldShow(false);
265
+ setIsChecking(false);
266
+ return;
267
+ }
268
+ if (options?.additionalCondition) {
269
+ try {
270
+ const result = await options.additionalCondition();
271
+ setShouldShow(result);
272
+ } catch {
273
+ setShouldShow(true);
274
+ }
275
+ } else {
276
+ setShouldShow(true);
277
+ }
278
+ setIsChecking(false);
279
+ };
280
+ checkCondition();
281
+ }, [isLoading, progress?.completed, options?.forceShow, options?.additionalCondition]);
282
+ return {
283
+ shouldShow,
284
+ isChecking: isLoading || isChecking,
285
+ isCompleted: progress?.completed ?? false
286
+ };
287
+ }
288
+
289
+ export { createApiAdapter, createAsyncStorageAdapter, createCompositeAdapter, createLocalStorageAdapter, createMemoryAdapter, createNoopAdapter, useGuidonPersistence, useShouldShowGuidon };
290
+ //# sourceMappingURL=index.mjs.map
291
+ //# sourceMappingURL=index.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/persistence/adapters.ts","../../src/persistence/hooks.ts"],"names":[],"mappings":"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","file":"index.mjs","sourcesContent":["import type { GuidonPersistenceAdapter, GuidonProgress } from '../types';\n\nconst STORAGE_KEY_PREFIX = '@guidon:';\n\n/**\n * No-op adapter that doesn't persist anything\n * Useful for testing or when persistence is not needed\n */\nexport const createNoopAdapter = (): GuidonPersistenceAdapter => ({\n loadProgress: async () => null,\n saveProgress: async () => {},\n loadAllProgress: async () => ({}),\n clearProgress: async () => {},\n});\n\n/**\n * Memory adapter that stores progress in memory\n * Data is lost when the app is closed\n */\nexport const createMemoryAdapter = (): GuidonPersistenceAdapter => {\n const store: Record<string, GuidonProgress> = {};\n\n return {\n loadProgress: async (guidonId) => store[guidonId] ?? null,\n saveProgress: async (progress) => {\n store[progress.guidonId] = progress;\n },\n loadAllProgress: async () => ({ ...store }),\n clearProgress: async (guidonId) => {\n delete store[guidonId];\n },\n };\n};\n\n/**\n * localStorage adapter for web\n * Only works in browser environments\n */\nexport const createLocalStorageAdapter = (\n keyPrefix: string = STORAGE_KEY_PREFIX\n): GuidonPersistenceAdapter => ({\n loadProgress: async (guidonId) => {\n if (typeof window === 'undefined' || !window.localStorage) {\n return null;\n }\n try {\n const data = localStorage.getItem(`${keyPrefix}${guidonId}`);\n return data ? JSON.parse(data) : null;\n } catch {\n return null;\n }\n },\n saveProgress: async (progress) => {\n if (typeof window === 'undefined' || !window.localStorage) {\n return;\n }\n try {\n localStorage.setItem(\n `${keyPrefix}${progress.guidonId}`,\n JSON.stringify(progress)\n );\n } catch {\n // Silently fail if storage is full\n }\n },\n loadAllProgress: async () => {\n if (typeof window === 'undefined' || !window.localStorage) {\n return {};\n }\n const result: Record<string, GuidonProgress> = {};\n try {\n for (let i = 0; i < localStorage.length; i++) {\n const key = localStorage.key(i);\n if (key?.startsWith(keyPrefix)) {\n const data = localStorage.getItem(key);\n if (data) {\n const progress = JSON.parse(data) as GuidonProgress;\n result[progress.guidonId] = progress;\n }\n }\n }\n } catch {\n // Silently fail\n }\n return result;\n },\n clearProgress: async (guidonId) => {\n if (typeof window === 'undefined' || !window.localStorage) {\n return;\n }\n try {\n localStorage.removeItem(`${keyPrefix}${guidonId}`);\n } catch {\n // Silently fail\n }\n },\n});\n\n/**\n * AsyncStorage adapter for React Native\n * Requires @react-native-async-storage/async-storage to be installed\n *\n * @example\n * import AsyncStorage from '@react-native-async-storage/async-storage';\n * const adapter = createAsyncStorageAdapter(AsyncStorage);\n */\nexport const createAsyncStorageAdapter = (\n asyncStorage: {\n getItem: (key: string) => Promise<string | null>;\n setItem: (key: string, value: string) => Promise<void>;\n removeItem: (key: string) => Promise<void>;\n getAllKeys: () => Promise<readonly string[]>;\n multiGet: (keys: readonly string[]) => Promise<readonly [string, string | null][]>;\n },\n keyPrefix: string = STORAGE_KEY_PREFIX\n): GuidonPersistenceAdapter => ({\n loadProgress: async (guidonId) => {\n try {\n const data = await asyncStorage.getItem(`${keyPrefix}${guidonId}`);\n return data ? JSON.parse(data) : null;\n } catch {\n return null;\n }\n },\n saveProgress: async (progress) => {\n try {\n await asyncStorage.setItem(\n `${keyPrefix}${progress.guidonId}`,\n JSON.stringify(progress)\n );\n } catch {\n // Silently fail\n }\n },\n loadAllProgress: async () => {\n const result: Record<string, GuidonProgress> = {};\n try {\n const allKeys = await asyncStorage.getAllKeys();\n const relevantKeys = allKeys.filter((key) => key.startsWith(keyPrefix));\n const pairs = await asyncStorage.multiGet(relevantKeys);\n for (const [, value] of pairs) {\n if (value) {\n const progress = JSON.parse(value) as GuidonProgress;\n result[progress.guidonId] = progress;\n }\n }\n } catch {\n // Silently fail\n }\n return result;\n },\n clearProgress: async (guidonId) => {\n try {\n await asyncStorage.removeItem(`${keyPrefix}${guidonId}`);\n } catch {\n // Silently fail\n }\n },\n});\n\n/**\n * Create a custom API adapter for backend persistence\n * This is a factory function that creates an adapter based on your API endpoints\n *\n * @example\n * const adapter = createApiAdapter({\n * loadProgress: async (guidonId) => {\n * const response = await fetch(`/api/guidon/${guidonId}`);\n * return response.json();\n * },\n * saveProgress: async (progress) => {\n * await fetch(`/api/guidon/${progress.guidonId}`, {\n * method: 'POST',\n * body: JSON.stringify(progress),\n * });\n * },\n * });\n */\nexport const createApiAdapter = (\n handlers: Partial<GuidonPersistenceAdapter>\n): GuidonPersistenceAdapter => {\n const noopAdapter = createNoopAdapter();\n return {\n loadProgress: handlers.loadProgress ?? noopAdapter.loadProgress,\n saveProgress: handlers.saveProgress ?? noopAdapter.saveProgress,\n loadAllProgress: handlers.loadAllProgress ?? noopAdapter.loadAllProgress,\n clearProgress: handlers.clearProgress ?? noopAdapter.clearProgress,\n };\n};\n\n/**\n * Combine multiple adapters (e.g., save to both local and API)\n * Loads from the first adapter that returns data\n * Saves to all adapters\n */\nexport const createCompositeAdapter = (\n adapters: GuidonPersistenceAdapter[]\n): GuidonPersistenceAdapter => ({\n loadProgress: async (guidonId) => {\n for (const adapter of adapters) {\n const progress = await adapter.loadProgress(guidonId);\n if (progress) return progress;\n }\n return null;\n },\n saveProgress: async (progress) => {\n await Promise.all(adapters.map((adapter) => adapter.saveProgress(progress)));\n },\n loadAllProgress: async () => {\n const result: Record<string, GuidonProgress> = {};\n for (const adapter of adapters) {\n if (adapter.loadAllProgress) {\n const data = await adapter.loadAllProgress();\n Object.assign(result, data);\n }\n }\n return result;\n },\n clearProgress: async (guidonId) => {\n await Promise.all(\n adapters.map((adapter) => adapter.clearProgress?.(guidonId))\n );\n },\n});\n","import { useCallback, useEffect, useState } from 'react';\nimport type { GuidonPersistenceAdapter, GuidonProgress } from '../types';\n\n/**\n * Hook to manage guidon's walkthrough progress with a persistence adapter\n */\nexport function useGuidonPersistence(\n adapter: GuidonPersistenceAdapter | undefined,\n guidonId: string\n) {\n const [progress, setProgress] = useState<GuidonProgress | null>(null);\n const [isLoading, setIsLoading] = useState(true);\n const [error, setError] = useState<string | null>(null);\n\n // Load progress on mount\n useEffect(() => {\n if (!adapter) {\n setIsLoading(false);\n return;\n }\n\n let mounted = true;\n\n const loadProgress = async () => {\n try {\n setIsLoading(true);\n setError(null);\n const data = await adapter.loadProgress(guidonId);\n if (mounted) {\n setProgress(data);\n }\n } catch (err) {\n if (mounted) {\n setError(err instanceof Error ? err.message : 'Failed to load progress');\n }\n } finally {\n if (mounted) {\n setIsLoading(false);\n }\n }\n };\n\n loadProgress();\n\n return () => {\n mounted = false;\n };\n }, [adapter, guidonId]);\n\n const saveProgress = useCallback(\n async (newProgress: Omit<GuidonProgress, 'guidonId'>) => {\n if (!adapter) return;\n\n const fullProgress: GuidonProgress = {\n ...newProgress,\n guidonId,\n };\n\n try {\n setError(null);\n await adapter.saveProgress(fullProgress);\n setProgress(fullProgress);\n } catch (err) {\n setError(err instanceof Error ? err.message : 'Failed to save progress');\n throw err;\n }\n },\n [adapter, guidonId]\n );\n\n const clearProgress = useCallback(async () => {\n if (!adapter?.clearProgress) return;\n\n try {\n setError(null);\n await adapter.clearProgress(guidonId);\n setProgress(null);\n } catch (err) {\n setError(err instanceof Error ? err.message : 'Failed to clear progress');\n throw err;\n }\n }, [adapter, guidonId]);\n\n const markCompleted = useCallback(async () => {\n const currentCount = progress?.completionCount ?? 0;\n await saveProgress({\n completed: true,\n completedAt: new Date().toISOString(),\n completionCount: currentCount + 1,\n });\n }, [saveProgress, progress?.completionCount]);\n\n const markStepViewed = useCallback(\n async (stepIndex: number) => {\n await saveProgress({\n completed: progress?.completed ?? false,\n lastStepIndex: stepIndex,\n completedAt: progress?.completedAt,\n completionCount: progress?.completionCount,\n });\n },\n [saveProgress, progress]\n );\n\n return {\n progress,\n isLoading,\n error,\n isCompleted: progress?.completed ?? false,\n hasStarted: progress !== null,\n saveProgress,\n clearProgress,\n markCompleted,\n markStepViewed,\n };\n}\n\n/**\n * Hook to check if a guidon should be shown\n */\nexport function useShouldShowGuidon(\n adapter: GuidonPersistenceAdapter | undefined,\n guidonId: string,\n options?: {\n /** Show even if completed (for replay) */\n forceShow?: boolean;\n /** Additional condition to check */\n additionalCondition?: () => boolean | Promise<boolean>;\n }\n) {\n const { progress, isLoading } = useGuidonPersistence(adapter, guidonId);\n const [shouldShow, setShouldShow] = useState(false);\n const [isChecking, setIsChecking] = useState(true);\n\n useEffect(() => {\n if (isLoading) return;\n\n const checkCondition = async () => {\n setIsChecking(true);\n\n // If forceShow is true, always show\n if (options?.forceShow) {\n setShouldShow(true);\n setIsChecking(false);\n return;\n }\n\n // If already completed, don't show\n if (progress?.completed) {\n setShouldShow(false);\n setIsChecking(false);\n return;\n }\n\n // Check additional condition if provided\n if (options?.additionalCondition) {\n try {\n const result = await options.additionalCondition();\n setShouldShow(result);\n } catch {\n setShouldShow(true); // Default to showing on error\n }\n } else {\n // Default: show if not completed\n setShouldShow(true);\n }\n\n setIsChecking(false);\n };\n\n checkCondition();\n }, [isLoading, progress?.completed, options?.forceShow, options?.additionalCondition]);\n\n return {\n shouldShow,\n isChecking: isLoading || isChecking,\n isCompleted: progress?.completed ?? false,\n };\n}\n"]}
package/package.json ADDED
@@ -0,0 +1,76 @@
1
+ {
2
+ "name": "@korsolutions/guidon",
3
+ "version": "1.0.0",
4
+ "description": "A cross-platform walkthrough/onboarding component library for React Native with web support. Features spotlight effects, customizable tooltips, and flexible persistence options. ",
5
+ "repository": "https://github.com/KorSoftwareSolutions/guidon.git",
6
+ "author": "Christian Jimenez <christianjimenezfael@gmail.com>",
7
+ "main": "dist/index.js",
8
+ "module": "dist/index.mjs",
9
+ "types": "dist/index.d.ts",
10
+ "exports": {
11
+ ".": {
12
+ "types": "./dist/index.d.ts",
13
+ "import": "./dist/index.mjs",
14
+ "require": "./dist/index.js"
15
+ },
16
+ "./persistence": {
17
+ "types": "./dist/persistence/index.d.ts",
18
+ "import": "./dist/persistence/index.mjs",
19
+ "require": "./dist/persistence/index.js"
20
+ }
21
+ },
22
+ "files": [
23
+ "dist",
24
+ "src"
25
+ ],
26
+ "scripts": {
27
+ "build": "tsup",
28
+ "dev": "tsup --watch",
29
+ "typecheck": "tsc --noEmit",
30
+ "clean": "rm -rf dist",
31
+ "prepublishOnly": "yarn build"
32
+ },
33
+ "peerDependencies": {
34
+ "react": ">=18.0.0",
35
+ "react-native": ">=0.70.0",
36
+ "react-native-reanimated": ">=3.0.0",
37
+ "react-native-safe-area-context": ">=4.0.0",
38
+ "react-native-svg": ">=13.0.0",
39
+ "zustand": ">=4.0.0"
40
+ },
41
+ "peerDependenciesMeta": {
42
+ "react-native": {
43
+ "optional": true
44
+ },
45
+ "react-native-reanimated": {
46
+ "optional": true
47
+ },
48
+ "react-native-safe-area-context": {
49
+ "optional": true
50
+ },
51
+ "react-native-svg": {
52
+ "optional": true
53
+ }
54
+ },
55
+ "devDependencies": {
56
+ "@types/react": "^19.2.9",
57
+ "@types/react-native": "^0.73.0",
58
+ "react-native-reanimated": "^4.2.1",
59
+ "react-native-safe-area-context": "^5.6.2",
60
+ "react-native-svg": "^15.15.1",
61
+ "tsup": "^8.5.1",
62
+ "typescript": "^5.0.0",
63
+ "zustand": "^5.0.10"
64
+ },
65
+ "keywords": [
66
+ "react-native",
67
+ "walkthrough",
68
+ "onboarding",
69
+ "tutorial",
70
+ "spotlight",
71
+ "tooltip",
72
+ "cross-platform",
73
+ "expo"
74
+ ],
75
+ "license": "MIT"
76
+ }
@@ -0,0 +1,159 @@
1
+ import { useMemo } from 'react';
2
+ import {
3
+ StyleSheet,
4
+ Dimensions,
5
+ TouchableWithoutFeedback,
6
+ Platform,
7
+ } from 'react-native';
8
+ import Animated, {
9
+ useAnimatedStyle,
10
+ withTiming,
11
+ Easing,
12
+ } from 'react-native-reanimated';
13
+ import Svg, { Defs, Mask, Rect, G } from 'react-native-svg';
14
+ import { useGuidonStore } from '../store';
15
+ import type { TargetMeasurements, GuidonTheme, GuidonStore } from '../types';
16
+
17
+ const AnimatedSvg = Animated.createAnimatedComponent(Svg);
18
+
19
+ const DEFAULT_THEME: Required<
20
+ Pick<
21
+ GuidonTheme,
22
+ 'backdropColor' | 'backdropOpacity' | 'spotlightBorderRadius' | 'spotlightPadding'
23
+ >
24
+ > = {
25
+ backdropColor: '#000000',
26
+ backdropOpacity: 0.75,
27
+ spotlightBorderRadius: 8,
28
+ spotlightPadding: 8,
29
+ };
30
+
31
+ interface GuidonOverlayProps {
32
+ theme?: GuidonTheme;
33
+ animationDuration?: number;
34
+ onBackdropPress?: () => void;
35
+ }
36
+
37
+ export function GuidonOverlay({
38
+ theme = {},
39
+ animationDuration = 300,
40
+ onBackdropPress,
41
+ }: GuidonOverlayProps) {
42
+ const isActive = useGuidonStore((state: GuidonStore) => state.isActive);
43
+ const config = useGuidonStore((state: GuidonStore) => state.config);
44
+ const currentStepIndex = useGuidonStore((state: GuidonStore) => state.currentStepIndex);
45
+ const targetMeasurements = useGuidonStore((state: GuidonStore) => state.targetMeasurements);
46
+
47
+ const mergedTheme = { ...DEFAULT_THEME, ...theme };
48
+ const { width: screenWidth, height: screenHeight } = Dimensions.get('window');
49
+
50
+ // Get current step's target measurements
51
+ const currentStep = config?.steps[currentStepIndex];
52
+ const currentTargetId = currentStep?.targetId;
53
+ const measurements: TargetMeasurements | undefined = currentTargetId
54
+ ? targetMeasurements[currentTargetId]
55
+ : undefined;
56
+
57
+ // Calculate spotlight dimensions with padding
58
+ const spotlight = useMemo(() => {
59
+ if (!measurements) {
60
+ return { x: 0, y: 0, width: 0, height: 0 };
61
+ }
62
+ return {
63
+ x: measurements.x - mergedTheme.spotlightPadding,
64
+ y: measurements.y - mergedTheme.spotlightPadding,
65
+ width: measurements.width + mergedTheme.spotlightPadding * 2,
66
+ height: measurements.height + mergedTheme.spotlightPadding * 2,
67
+ };
68
+ }, [measurements, mergedTheme.spotlightPadding]);
69
+
70
+ // Animated styles for fade in/out
71
+ const animatedStyle = useAnimatedStyle(() => {
72
+ return {
73
+ opacity: withTiming(isActive && measurements ? 1 : 0, {
74
+ duration: animationDuration,
75
+ easing: Easing.inOut(Easing.ease),
76
+ }),
77
+ };
78
+ }, [isActive, measurements, animationDuration]);
79
+
80
+ if (!isActive || !measurements) {
81
+ return null;
82
+ }
83
+
84
+ // For web, use a different approach with CSS
85
+ if (Platform.OS === 'web') {
86
+ return (
87
+ <TouchableWithoutFeedback onPress={onBackdropPress}>
88
+ <Animated.View style={[styles.container, animatedStyle]}>
89
+ <div
90
+ style={{
91
+ position: 'absolute',
92
+ inset: 0,
93
+ backgroundColor: mergedTheme.backdropColor,
94
+ opacity: mergedTheme.backdropOpacity,
95
+ clipPath: `polygon(
96
+ 0% 0%,
97
+ 0% 100%,
98
+ ${spotlight.x}px 100%,
99
+ ${spotlight.x}px ${spotlight.y}px,
100
+ ${spotlight.x + spotlight.width}px ${spotlight.y}px,
101
+ ${spotlight.x + spotlight.width}px ${spotlight.y + spotlight.height}px,
102
+ ${spotlight.x}px ${spotlight.y + spotlight.height}px,
103
+ ${spotlight.x}px 100%,
104
+ 100% 100%,
105
+ 100% 0%
106
+ )`,
107
+ }}
108
+ />
109
+ </Animated.View>
110
+ </TouchableWithoutFeedback>
111
+ );
112
+ }
113
+
114
+ // Native implementation using SVG mask
115
+ return (
116
+ <TouchableWithoutFeedback onPress={onBackdropPress}>
117
+ <AnimatedSvg
118
+ style={[styles.container, animatedStyle]}
119
+ width={screenWidth}
120
+ height={screenHeight}
121
+ viewBox={`0 0 ${screenWidth} ${screenHeight}`}
122
+ >
123
+ <Defs>
124
+ <Mask id="spotlight-mask">
125
+ {/* White background (visible) */}
126
+ <Rect x="0" y="0" width="100%" height="100%" fill="white" />
127
+ {/* Black cutout (transparent) */}
128
+ <Rect
129
+ x={spotlight.x}
130
+ y={spotlight.y}
131
+ width={spotlight.width}
132
+ height={spotlight.height}
133
+ rx={mergedTheme.spotlightBorderRadius}
134
+ ry={mergedTheme.spotlightBorderRadius}
135
+ fill="black"
136
+ />
137
+ </Mask>
138
+ </Defs>
139
+ <G mask="url(#spotlight-mask)">
140
+ <Rect
141
+ x="0"
142
+ y="0"
143
+ width="100%"
144
+ height="100%"
145
+ fill={mergedTheme.backdropColor}
146
+ fillOpacity={mergedTheme.backdropOpacity}
147
+ />
148
+ </G>
149
+ </AnimatedSvg>
150
+ </TouchableWithoutFeedback>
151
+ );
152
+ }
153
+
154
+ const styles = StyleSheet.create({
155
+ container: {
156
+ ...StyleSheet.absoluteFillObject,
157
+ zIndex: 999,
158
+ },
159
+ });