uo287827-react-native-tour-component 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.
Files changed (32) hide show
  1. package/README.md +175 -0
  2. package/dist/components/tour-highlight.component.d.ts +9 -0
  3. package/dist/components/tour-highlight.component.d.ts.map +1 -0
  4. package/dist/components/tour-highlight.component.js +23 -0
  5. package/dist/components/tour-overlay.component.d.ts +16 -0
  6. package/dist/components/tour-overlay.component.d.ts.map +1 -0
  7. package/dist/components/tour-overlay.component.js +51 -0
  8. package/dist/components/tour-tooltip-position.component.d.ts +7 -0
  9. package/dist/components/tour-tooltip-position.component.d.ts.map +1 -0
  10. package/dist/components/tour-tooltip-position.component.js +16 -0
  11. package/dist/components/tour-tooltip.component.d.ts +19 -0
  12. package/dist/components/tour-tooltip.component.d.ts.map +1 -0
  13. package/dist/components/tour-tooltip.component.js +60 -0
  14. package/dist/components/tour.component.d.ts +22 -0
  15. package/dist/components/tour.component.d.ts.map +1 -0
  16. package/dist/components/tour.component.js +32 -0
  17. package/dist/index.d.ts +5 -0
  18. package/dist/index.d.ts.map +1 -0
  19. package/dist/index.js +4 -0
  20. package/dist/service/tour-theme.context.d.ts +8 -0
  21. package/dist/service/tour-theme.context.d.ts.map +1 -0
  22. package/dist/service/tour-theme.context.js +6 -0
  23. package/dist/service/tour.context.d.ts +8 -0
  24. package/dist/service/tour.context.d.ts.map +1 -0
  25. package/dist/service/tour.context.js +8 -0
  26. package/dist/tour.types.d.ts +58 -0
  27. package/dist/tour.types.d.ts.map +1 -0
  28. package/dist/tour.types.js +14 -0
  29. package/dist/utils/tour-tooltip-position.d.ts +8 -0
  30. package/dist/utils/tour-tooltip-position.d.ts.map +1 -0
  31. package/dist/utils/tour-tooltip-position.js +16 -0
  32. package/package.json +35 -0
package/README.md ADDED
@@ -0,0 +1,175 @@
1
+ # feature-tour
2
+
3
+ Componente de tour/onboarding para React Native.
4
+
5
+ ## Instalación
6
+
7
+ Requiere `react-native-svg`:
8
+
9
+ ```bash
10
+ npx expo install react-native-svg
11
+ ```
12
+
13
+ ## Uso básico
14
+
15
+ ```tsx
16
+ import { useRef } from 'react';
17
+ import { View, Text, Button } from 'react-native';
18
+ import { Tour, TourProvider, TourHandle, TourStepConfig } from './feature-tour';
19
+
20
+ export default function MyScreen() {
21
+ // tourRef expone start() y stop() para controlar el tour de forma imperativa
22
+ const tourRef = useRef<TourHandle>(null);
23
+
24
+ // Cada ref se asigna al elemento de UI que el tour resaltará
25
+ const headerRef = useRef<View>(null);
26
+ const buttonRef = useRef<View>(null);
27
+
28
+ // Define la lista ordenada de pasos; cada paso apunta a un ref y contiene los datos del tooltip
29
+ const steps: TourStepConfig[] = [
30
+ {
31
+ id: 'header',
32
+ ref: headerRef, // El elemento a resaltar
33
+ title: 'Cabecera',
34
+ content: 'Aquí está el título principal.',
35
+ tooltipPosition: 'bottom', // El tooltip aparece debajo del elemento resaltado
36
+ },
37
+ {
38
+ id: 'button',
39
+ ref: buttonRef,
40
+ title: 'Acción',
41
+ content: 'Pulsa aquí para continuar.',
42
+ tooltipPosition: 'top', // El tooltip aparece encima del elemento resaltado
43
+ tooltipSize: 'lg', // Usa la variante de tooltip más ancha (320px en lugar de 260px)
44
+ },
45
+ ];
46
+
47
+ return (
48
+ // TourProvider provee el tema y el contexto compartido a todos los componentes del tour
49
+ <TourProvider>
50
+ {/* <Tour> renderiza el overlay y el tooltip; es invisible hasta que se llama a start() */}
51
+ <Tour ref={tourRef} steps={steps} />
52
+
53
+ {/* Asigna los refs a los elementos que quieres resaltar en cada paso */}
54
+ <Text ref={headerRef}>Mi app</Text>
55
+
56
+ {/* Llamar a tourRef.current.start() inicia el tour en el paso 0 */}
57
+ <Button title="Iniciar tour" onPress={() => tourRef.current?.start()} />
58
+ </TourProvider>
59
+ );
60
+ }
61
+ ```
62
+
63
+ ## Props de `<Tour>`
64
+
65
+ | Prop | Tipo | Default | Descripción |
66
+ |---|---|---|---|
67
+ | `steps` | `TourStepConfig[]` | — | Pasos del tour |
68
+ | `skipLabel` | `string` | `'Saltar'` | Texto del botón "Saltar" |
69
+ | `nextLabel` | `string` | `'Siguiente'` | Texto del botón "Siguiente" |
70
+ | `finishLabel` | `string` | `'Finalizar'` | Texto del botón del último paso |
71
+ | `showSkipButton` | `boolean` | `true` | Muestra u oculta el botón de omitir |
72
+
73
+ Ejemplo con etiquetas en inglés:
74
+
75
+ ```tsx
76
+ // Sobreescribe las etiquetas predeterminadas
77
+ <Tour ref={tourRef} steps={steps} skipLabel="Skip" nextLabel="Next" finishLabel="Done" />
78
+ ```
79
+
80
+ ### Contenido personalizado en un paso
81
+
82
+ `content` acepta tanto un `string` (texto simple) como cualquier `ReactNode`:
83
+
84
+ ```tsx
85
+ // --- Opción 1: string simple ---
86
+ // El tooltip renderiza el texto directamente dentro de un componente <Text>.
87
+ { id: 'step1', ref, title: 'Título', content: 'Descripción sencilla.' }
88
+
89
+ // --- Opción 2: ReactNode personalizado ---
90
+ // Cuando content es un ReactNode, se envuelve en un <View> simple;
91
+ // tienes control total sobre el layout y los estilos.
92
+ {
93
+ id: 'stats',
94
+ ref: statsRef,
95
+ title: 'Estadísticas',
96
+ content: (
97
+ <View style={{ gap: 4 }}>
98
+ {/* Fila de texto descriptivo */}
99
+ <Text style={{ fontSize: 13, color: '#475569' }}>Aquí ves un resumen de tu actividad.</Text>
100
+
101
+ {/* Fila de badges — cada badge es una píldora pequeña con fondo de color */}
102
+ <View style={{ flexDirection: 'row', gap: 8, marginTop: 4 }}>
103
+ <Text style={{ fontSize: 12, backgroundColor: '#e0f2fe', paddingHorizontal: 8,
104
+ paddingVertical: 2, borderRadius: 10 }}>12 proyectos</Text>
105
+ <Text style={{ fontSize: 12, backgroundColor: '#dcfce7', paddingHorizontal: 8,
106
+ paddingVertical: 2, borderRadius: 10 }}>48 tareas</Text>
107
+ </View>
108
+ </View>
109
+ ),
110
+ tooltipPosition: 'bottom', // Coloca el tooltip debajo del elemento de estadísticas
111
+ }
112
+ ```
113
+
114
+ ## Props de `TourStepConfig`
115
+
116
+ | Prop | Tipo | Default | Descripción |
117
+ |---|---|---|---|
118
+ | `id` | `string` | — | Identificador único del paso |
119
+ | `ref` | `RefObject<View>` | — | Referencia al elemento a destacar |
120
+ | `title` | `string` | — | Título del tooltip |
121
+ | `content` | `string \| ReactNode` | — | Texto simple o componente React personalizado |
122
+ | `tooltipPosition` | `'top' \| 'bottom' \| 'auto'` | `'auto'` | Posición del tooltip |
123
+ | `tooltipSize` | `'sm' \| 'lg'` | `'sm'` | Tamaño del tooltip |
124
+ | `highlightPadding` | `number` | `8` | Espacio extra alrededor del elemento |
125
+
126
+ ## Tema
127
+
128
+ Personaliza el aspecto visual pasando un objeto `theme` parcial a `<TourProvider>`:
129
+
130
+ ```tsx
131
+ import { Tour, TourProvider, TourTheme } from './feature-tour';
132
+
133
+ // Define el tema fuera del componente para evitar recrearlo en cada render
134
+ // Partial<TourTheme> permite sobreescribir solo los tokens deseados
135
+ const miTema: Partial<TourTheme> = {
136
+ primaryColor: '#7c3aed', // Botón "Siguiente" en morado
137
+ tooltipBackgroundColor: '#1e1e2e', // Fondo oscuro para el tooltip
138
+ tooltipTitleColor: '#ffffff',
139
+ tooltipDescriptionColor: '#a0a0b0',
140
+ overlayColor: 'rgba(0,0,0,0.75)',
141
+ };
142
+
143
+ // Pasa el tema al provider; solo los tokens definidos sobreescriben el DEFAULT_TOUR_THEME
144
+ export default function App() {
145
+ return (
146
+ <TourProvider theme={miTema}>
147
+ {/* resto de la app */}
148
+ </TourProvider>
149
+ );
150
+ }
151
+ ```
152
+
153
+ También puedes pasar los tokens directamente como objeto literal:
154
+
155
+ ```tsx
156
+ // Proporciona solo los tokens que quieras sobreescribir;
157
+ // el resto tomará los valores de DEFAULT_TOUR_THEME.
158
+ // Aquí cambiamos el color de acción principal y suavizamos las esquinas del highlight.
159
+ <TourProvider theme={{ primaryColor: '#2563eb', highlightBorderRadius: 12 }}>
160
+ ```
161
+
162
+ | Token | Default | Descripción |
163
+ |---|---|---|
164
+ | `primaryColor` | `'#2563eb'` | Color de fondo del botón "Siguiente" |
165
+ | `primaryTextColor` | `'#ffffff'` | Color del texto del botón "Siguiente" |
166
+ | `skipButtonTextColor` | `'#888888'` | Color del texto del botón "Saltar" |
167
+ | `skipButtonBackgroundColor` | `'transparent'` | Color de fondo del botón "Saltar" |
168
+ | `tooltipBackgroundColor` | `'#ffffff'` | Fondo del tooltip |
169
+ | `tooltipTitleColor` | `'#111111'` | Color del título |
170
+ | `tooltipDescriptionColor` | `'#444444'` | Color de la descripción |
171
+ | `tooltipProgressColor` | `'#888888'` | Color del contador de pasos |
172
+ | `tooltipBorderRadius` | `10` | Radio del tooltip |
173
+ | `overlayColor` | `'rgba(0,0,0,0.6)'` | Color del fondo oscuro |
174
+ | `highlightBorderColor` | `'#ffffff'` | Color del borde del elemento destacado |
175
+ | `highlightBorderRadius` | `10` | Radio del recuadro de highlight |
@@ -0,0 +1,9 @@
1
+ import React from 'react';
2
+ import { ElementMeasure } from '../tour.types';
3
+ interface TourHighlightProps {
4
+ measure: ElementMeasure;
5
+ padding: number;
6
+ }
7
+ export declare const TourHighlight: React.FC<TourHighlightProps>;
8
+ export {};
9
+ //# sourceMappingURL=tour-highlight.component.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tour-highlight.component.d.ts","sourceRoot":"","sources":["../../components/tour-highlight.component.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAC;AAE1B,OAAO,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AAG/C,UAAU,kBAAkB;IACxB,OAAO,EAAE,cAAc,CAAC;IACxB,OAAO,EAAE,MAAM,CAAC;CACnB;AAGD,eAAO,MAAM,aAAa,EAAE,KAAK,CAAC,EAAE,CAAC,kBAAkB,CAgBtD,CAAC"}
@@ -0,0 +1,23 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { StyleSheet, View } from 'react-native';
3
+ import { useTourTheme } from '../service/tour-theme.context';
4
+ // Componente que dibuja un borde resaltado alrededor del elemento destacado.
5
+ export const TourHighlight = ({ measure, padding }) => {
6
+ const theme = useTourTheme();
7
+ const { x, y, width, height } = measure;
8
+ return (_jsx(View, { style: [styles.highlight, {
9
+ top: y - padding,
10
+ left: x - padding,
11
+ width: width + padding * 2,
12
+ height: height + padding * 2,
13
+ borderColor: theme.highlightBorderColor,
14
+ borderRadius: theme.highlightBorderRadius,
15
+ }] }));
16
+ };
17
+ const styles = StyleSheet.create({
18
+ highlight: {
19
+ position: 'absolute',
20
+ borderWidth: 2,
21
+ backgroundColor: 'transparent',
22
+ },
23
+ });
@@ -0,0 +1,16 @@
1
+ import React from 'react';
2
+ import { TourStepConfig } from '../tour.types';
3
+ interface TourOverlayProps {
4
+ step: TourStepConfig;
5
+ stepIndex: number;
6
+ totalSteps: number;
7
+ onNext: () => void;
8
+ onSkip: () => void;
9
+ skipLabel: string;
10
+ nextLabel: string;
11
+ finishLabel: string;
12
+ showSkipButton: boolean;
13
+ }
14
+ export declare const TourOverlay: React.FC<TourOverlayProps>;
15
+ export {};
16
+ //# sourceMappingURL=tour-overlay.component.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tour-overlay.component.d.ts","sourceRoot":"","sources":["../../components/tour-overlay.component.tsx"],"names":[],"mappings":"AAAA,OAAO,KAA8B,MAAM,OAAO,CAAC;AAGnD,OAAO,EAAkB,cAAc,EAAE,MAAM,eAAe,CAAC;AAK/D,UAAU,gBAAgB;IACtB,IAAI,EAAE,cAAc,CAAC;IACrB,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,EAAE,MAAM,IAAI,CAAC;IACnB,MAAM,EAAE,MAAM,IAAI,CAAC;IACnB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,WAAW,EAAE,MAAM,CAAC;IACpB,cAAc,EAAE,OAAO,CAAC;CAC3B;AAwBD,eAAO,MAAM,WAAW,EAAE,KAAK,CAAC,EAAE,CAAC,gBAAgB,CAkElD,CAAC"}
@@ -0,0 +1,51 @@
1
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useEffect, useState } from 'react';
3
+ import { Dimensions, Modal, StyleSheet, View } from 'react-native';
4
+ import { Path, Svg } from 'react-native-svg';
5
+ import { useTourTheme } from '../service/tour-theme.context';
6
+ import { TourHighlight } from './tour-highlight.component';
7
+ import { TourTooltip } from './tour-tooltip.component';
8
+ /** Este SVG sirve para oscurecer toda la pantalla excepto el área del elemento destacado, creando un efecto de "agujero". */
9
+ function buildCutoutPath(sw, sh, x, y, w, h, r) {
10
+ return [
11
+ // rectángulo exterior (pantalla completa)
12
+ `M 0 0 H ${sw} V ${sh} H 0 Z`,
13
+ // rectángulo redondeado interior (el "agujero")
14
+ `M ${x + r} ${y}`,
15
+ `H ${x + w - r}`,
16
+ `A ${r} ${r} 0 0 1 ${x + w} ${y + r}`,
17
+ `V ${y + h - r}`,
18
+ `A ${r} ${r} 0 0 1 ${x + w - r} ${y + h}`,
19
+ `H ${x + r}`,
20
+ `A ${r} ${r} 0 0 1 ${x} ${y + h - r}`,
21
+ `V ${y + r}`,
22
+ `A ${r} ${r} 0 0 1 ${x + r} ${y} Z`,
23
+ ].join(' ');
24
+ }
25
+ export const TourOverlay = ({ step, stepIndex, totalSteps, onNext, onSkip, skipLabel, nextLabel, finishLabel, showSkipButton }) => {
26
+ var _a;
27
+ const theme = useTourTheme();
28
+ const [measure, setMeasure] = useState(null);
29
+ const { width: screenWidth, height: screenHeight } = Dimensions.get('window');
30
+ useEffect(() => {
31
+ var _a;
32
+ if (!((_a = step.ref) === null || _a === void 0 ? void 0 : _a.current))
33
+ return;
34
+ const timer = setTimeout(() => {
35
+ step.ref.current.measureInWindow((x, y, width, height) => {
36
+ // measureInWindow devuelve coordenadas relativas a la ventana visible
37
+ setMeasure({ x, y, width, height });
38
+ });
39
+ }, 100);
40
+ return () => clearTimeout(timer);
41
+ }, [step.id]);
42
+ const pad = (_a = step.highlightPadding) !== null && _a !== void 0 ? _a : 8;
43
+ const hx = measure ? measure.x - pad : 0;
44
+ const hy = measure ? measure.y - pad : 0;
45
+ const hw = measure ? measure.width + pad * 2 : 0;
46
+ const hh = measure ? measure.height + pad * 2 : 0;
47
+ return (
48
+ // Modal que cubre toda la pantalla con un fondo semitransparente, dejando un "agujero" alrededor del elemento destacado.
49
+ _jsx(Modal, { transparent: true, visible: true, animationType: "fade", children: _jsxs(View, { style: StyleSheet.absoluteFill, pointerEvents: "box-none", children: [_jsx(Svg, { style: StyleSheet.absoluteFill, width: screenWidth, height: screenHeight, pointerEvents: "none", children: _jsx(Path, { d: buildCutoutPath(screenWidth, screenHeight, hx, hy, hw, hh, theme.highlightBorderRadius), fill: theme.overlayColor, fillRule: "evenodd" }) }), measure && (_jsxs(_Fragment, { children: [_jsx(TourHighlight, { measure: measure, padding: pad }), _jsx(TourTooltip, { step: step, measure: measure, screenWidth: screenWidth, screenHeight: screenHeight, stepIndex: stepIndex, totalSteps: totalSteps, onNext: onNext, onSkip: onSkip, skipLabel: skipLabel, nextLabel: nextLabel, finishLabel: finishLabel, showSkipButton: showSkipButton })] }))] }) }));
50
+ };
51
+ const styles = StyleSheet.create({});
@@ -0,0 +1,7 @@
1
+ import { ElementMeasure, TooltipPosition } from '../tour.types';
2
+ export interface TooltipCoords {
3
+ top: number;
4
+ left: number;
5
+ }
6
+ export declare const calculateTooltipPosition: (measure: ElementMeasure, screenWidth: number, screenHeight: number, preferred?: TooltipPosition) => TooltipCoords;
7
+ //# sourceMappingURL=tour-tooltip-position.component.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tour-tooltip-position.component.d.ts","sourceRoot":"","sources":["../../components/tour-tooltip-position.component.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,eAAe,EAAE,MAAM,eAAe,CAAC;AAKhE,MAAM,WAAW,aAAa;IAC1B,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,MAAM,CAAC;CAChB;AAGD,eAAO,MAAM,wBAAwB,GACjC,SAAS,cAAc,EACvB,aAAa,MAAM,EACnB,cAAc,MAAM,EACpB,YAAW,eAAwB,KACpC,aAgBF,CAAC"}
@@ -0,0 +1,16 @@
1
+ const TOOLTIP_HEIGHT = 120;
2
+ const TOOLTIP_MARGIN = 12;
3
+ // Calcula la posición del tooltip basado en la medida del elemento destacado, el tamaño de la pantalla y la preferencia de posición.
4
+ export const calculateTooltipPosition = (measure, screenWidth, screenHeight, preferred = 'auto') => {
5
+ const spaceBelow = screenHeight - (measure.y + measure.height);
6
+ const spaceAbove = measure.y;
7
+ const position = preferred !== 'auto' ? preferred
8
+ : spaceBelow >= TOOLTIP_HEIGHT + TOOLTIP_MARGIN ? 'bottom'
9
+ : spaceAbove >= TOOLTIP_HEIGHT + TOOLTIP_MARGIN ? 'top'
10
+ : 'bottom'; // valor por defecto
11
+ const left = Math.max(16, Math.min(measure.x, screenWidth - 240));
12
+ if (position === 'bottom') {
13
+ return { top: measure.y + measure.height + TOOLTIP_MARGIN, left };
14
+ }
15
+ return { top: measure.y - TOOLTIP_HEIGHT - TOOLTIP_MARGIN, left };
16
+ };
@@ -0,0 +1,19 @@
1
+ import React from 'react';
2
+ import { ElementMeasure, TourStepConfig } from '../tour.types';
3
+ interface TourTooltipProps {
4
+ step: TourStepConfig;
5
+ measure: ElementMeasure;
6
+ screenWidth: number;
7
+ screenHeight: number;
8
+ stepIndex: number;
9
+ totalSteps: number;
10
+ onNext: () => void;
11
+ onSkip: () => void;
12
+ skipLabel: string;
13
+ nextLabel: string;
14
+ finishLabel: string;
15
+ showSkipButton: boolean;
16
+ }
17
+ export declare const TourTooltip: React.FC<TourTooltipProps>;
18
+ export {};
19
+ //# sourceMappingURL=tour-tooltip.component.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tour-tooltip.component.d.ts","sourceRoot":"","sources":["../../components/tour-tooltip.component.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAmB,MAAM,OAAO,CAAC;AAExC,OAAO,EAAE,cAAc,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AAI/D,UAAU,gBAAgB;IACtB,IAAI,EAAE,cAAc,CAAC;IACrB,OAAO,EAAE,cAAc,CAAC;IACxB,WAAW,EAAE,MAAM,CAAC;IACpB,YAAY,EAAE,MAAM,CAAC;IACrB,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,EAAE,MAAM,IAAI,CAAC;IACnB,MAAM,EAAE,MAAM,IAAI,CAAC;IACnB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,WAAW,EAAE,MAAM,CAAC;IACpB,cAAc,EAAE,OAAO,CAAC;CAC3B;AAGD,eAAO,MAAM,WAAW,EAAE,KAAK,CAAC,EAAE,CAAC,gBAAgB,CA4DlD,CAAC"}
@@ -0,0 +1,60 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useState } from 'react';
3
+ import { StyleSheet, Text, TouchableOpacity, View } from 'react-native';
4
+ import { useTourTheme } from '../service/tour-theme.context';
5
+ import { calculateTooltipPosition } from '../utils/tour-tooltip-position';
6
+ // Componente que muestra el tooltip con la información de la etapa actual, posicionado cerca del elemento destacado.
7
+ export const TourTooltip = ({ step, measure, screenWidth, screenHeight, stepIndex, totalSteps, onNext, onSkip, skipLabel, nextLabel, finishLabel, showSkipButton, }) => {
8
+ const theme = useTourTheme();
9
+ const [tooltipHeight, setTooltipHeight] = useState(0);
10
+ const isLast = stepIndex === totalSteps - 1;
11
+ const tooltipWidth = step.tooltipSize === 'lg' ? 320 : 260;
12
+ const { top, left } = calculateTooltipPosition(measure, screenWidth, screenHeight, step.tooltipPosition, tooltipHeight);
13
+ const handleLayout = (e) => {
14
+ const h = e.nativeEvent.layout.height;
15
+ if (h !== tooltipHeight)
16
+ setTooltipHeight(h);
17
+ };
18
+ return (_jsxs(View, { style: [styles.container, {
19
+ top, left,
20
+ width: tooltipWidth,
21
+ opacity: tooltipHeight === 0 ? 0 : 1,
22
+ backgroundColor: theme.tooltipBackgroundColor,
23
+ borderRadius: theme.tooltipBorderRadius,
24
+ }], onLayout: handleLayout, children: [_jsxs(View, { style: styles.header, children: [_jsx(Text, { style: [styles.title, { color: theme.tooltipTitleColor }], children: step.title }), _jsxs(Text, { style: [styles.progress, { color: theme.tooltipProgressColor }], children: [stepIndex + 1, " / ", totalSteps] })] }), step.content != null && (typeof step.content === 'string'
25
+ ? _jsx(Text, { style: [styles.description, { color: theme.tooltipDescriptionColor }], children: step.content })
26
+ : _jsx(View, { style: styles.customContent, children: step.content })), _jsxs(View, { style: [styles.actions, !showSkipButton && styles.actionsEnd], children: [showSkipButton && (_jsx(TouchableOpacity, { style: [styles.skipBtn, { backgroundColor: theme.skipButtonBackgroundColor, borderRadius: theme.tooltipBorderRadius - 2 }], onPress: onSkip, children: _jsx(Text, { style: [styles.skipBtnText, { color: theme.skipButtonTextColor }], children: skipLabel }) })), _jsx(TouchableOpacity, { style: [styles.nextBtn, { backgroundColor: theme.primaryColor, borderRadius: theme.tooltipBorderRadius - 2 }], onPress: onNext, children: _jsx(Text, { style: [styles.nextBtnText, { color: theme.primaryTextColor }], children: isLast ? finishLabel : nextLabel }) })] })] }));
27
+ };
28
+ // Estilos básicos del tooltip, algunos de ellos se pueden personalizar o sobreescribir con un TourTheme.
29
+ const styles = StyleSheet.create({
30
+ container: {
31
+ position: 'absolute',
32
+ padding: 16,
33
+ shadowColor: '#000',
34
+ shadowOpacity: 0.2,
35
+ shadowRadius: 8,
36
+ elevation: 6,
37
+ },
38
+ header: {
39
+ flexDirection: 'row',
40
+ justifyContent: 'space-between',
41
+ alignItems: 'center',
42
+ marginBottom: 6,
43
+ },
44
+ title: { fontSize: 15, fontWeight: '700' },
45
+ progress: { fontSize: 12 },
46
+ description: { fontSize: 13, lineHeight: 20, marginBottom: 14 },
47
+ customContent: { marginBottom: 14 },
48
+ actions: {
49
+ flexDirection: 'row',
50
+ justifyContent: 'space-between',
51
+ alignItems: 'center',
52
+ },
53
+ actionsEnd: {
54
+ justifyContent: 'flex-end',
55
+ },
56
+ skipBtn: { paddingVertical: 7, paddingHorizontal: 12 },
57
+ skipBtnText: { fontSize: 13 },
58
+ nextBtn: { paddingVertical: 7, paddingHorizontal: 16 },
59
+ nextBtnText: { fontSize: 13, fontWeight: '600' },
60
+ });
@@ -0,0 +1,22 @@
1
+ import React from 'react';
2
+ import { TourHandle, TourStepConfig } from '../tour.types';
3
+ interface TourProps {
4
+ steps: TourStepConfig[];
5
+ /** Texto del botón "Saltar" (por defecto 'Saltar') */
6
+ skipLabel?: string;
7
+ /** Texto del botón "Siguiente" (por defecto 'Siguiente') */
8
+ nextLabel?: string;
9
+ /** Texto del botón del último paso (por defecto 'Finalizar') */
10
+ finishLabel?: string;
11
+ /** Muestra u oculta el botón de omitir. Por defecto `true`. */
12
+ showSkipButton?: boolean;
13
+ }
14
+ /**
15
+ * <Tour ref={tourRef} steps={steps} />
16
+ *
17
+ * Coloca este componente dentro de TourProvider. Se puede iniciar el tour con:
18
+ * tourRef.current?.start()
19
+ */
20
+ export declare const Tour: React.ForwardRefExoticComponent<TourProps & React.RefAttributes<TourHandle>>;
21
+ export {};
22
+ //# sourceMappingURL=tour.component.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tour.component.d.ts","sourceRoot":"","sources":["../../components/tour.component.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAoD,MAAM,OAAO,CAAC;AACzE,OAAO,EAAE,UAAU,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AAG3D,UAAU,SAAS;IACf,KAAK,EAAE,cAAc,EAAE,CAAC;IACxB,sDAAsD;IACtD,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,4DAA4D;IAC5D,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,gEAAgE;IAChE,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,+DAA+D;IAC/D,cAAc,CAAC,EAAE,OAAO,CAAC;CAC5B;AAED;;;;;GAKG;AACH,eAAO,MAAM,IAAI,8EAqCf,CAAC"}
@@ -0,0 +1,32 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { forwardRef, useImperativeHandle, useState } from 'react';
3
+ import { TourOverlay } from './tour-overlay.component';
4
+ /**
5
+ * <Tour ref={tourRef} steps={steps} />
6
+ *
7
+ * Coloca este componente dentro de TourProvider. Se puede iniciar el tour con:
8
+ * tourRef.current?.start()
9
+ */
10
+ export const Tour = forwardRef(({ steps, skipLabel = 'Saltar', nextLabel = 'Siguiente', finishLabel = 'Finalizar', showSkipButton = true }, ref) => {
11
+ const [running, setRunning] = useState(false);
12
+ const [currentIndex, setCurrentIndex] = useState(0);
13
+ useImperativeHandle(ref, () => ({
14
+ start: () => {
15
+ setCurrentIndex(0);
16
+ setRunning(true);
17
+ },
18
+ stop: () => setRunning(false),
19
+ }));
20
+ const handleNext = () => {
21
+ if (currentIndex >= steps.length - 1) {
22
+ setRunning(false);
23
+ }
24
+ else {
25
+ setCurrentIndex(prev => prev + 1);
26
+ }
27
+ };
28
+ const handleSkip = () => setRunning(false);
29
+ if (!running || steps.length === 0)
30
+ return null;
31
+ return (_jsx(TourOverlay, { step: steps[currentIndex], stepIndex: currentIndex, totalSteps: steps.length, onNext: handleNext, onSkip: handleSkip, skipLabel: skipLabel, nextLabel: nextLabel, finishLabel: finishLabel, showSkipButton: showSkipButton }));
32
+ });
@@ -0,0 +1,5 @@
1
+ export { TourProvider } from './service/tour.context';
2
+ export { useTourTheme } from './service/tour-theme.context';
3
+ export { Tour } from './components/tour.component';
4
+ export type { TourStepConfig, TourHandle, TooltipPosition, TourTheme, TourStepSize, TourStepContent } from './tour.types';
5
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,YAAY,EAAE,MAAM,wBAAwB,CAAC;AACtD,OAAO,EAAE,YAAY,EAAE,MAAM,8BAA8B,CAAC;AAC5D,OAAO,EAAE,IAAI,EAAE,MAAM,6BAA6B,CAAC;AACnD,YAAY,EAAE,cAAc,EAAE,UAAU,EAAE,eAAe,EAAE,SAAS,EAAE,YAAY,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,4 @@
1
+ // Única puerta de entrada al módulo
2
+ export { TourProvider } from './service/tour.context';
3
+ export { useTourTheme } from './service/tour-theme.context';
4
+ export { Tour } from './components/tour.component';
@@ -0,0 +1,8 @@
1
+ import React from 'react';
2
+ import { TourTheme } from '../tour.types';
3
+ export declare const TourThemeProvider: React.FC<{
4
+ theme: TourTheme;
5
+ children: React.ReactNode;
6
+ }>;
7
+ export declare const useTourTheme: () => TourTheme;
8
+ //# sourceMappingURL=tour-theme.context.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tour-theme.context.d.ts","sourceRoot":"","sources":["../../service/tour-theme.context.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAoC,MAAM,OAAO,CAAC;AACzD,OAAO,EAAsB,SAAS,EAAE,MAAM,eAAe,CAAC;AAI9D,eAAO,MAAM,iBAAiB,EAAE,KAAK,CAAC,EAAE,CAAC;IACrC,KAAK,EAAE,SAAS,CAAC;IACjB,QAAQ,EAAE,KAAK,CAAC,SAAS,CAAC;CAC7B,CAIA,CAAC;AAEF,eAAO,MAAM,YAAY,QAAO,SAAyC,CAAC"}
@@ -0,0 +1,6 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { createContext, useContext } from 'react';
3
+ import { DEFAULT_TOUR_THEME } from '../tour.types';
4
+ const TourThemeContext = createContext(DEFAULT_TOUR_THEME);
5
+ export const TourThemeProvider = ({ theme, children }) => (_jsx(TourThemeContext.Provider, { value: theme, children: children }));
6
+ export const useTourTheme = () => useContext(TourThemeContext);
@@ -0,0 +1,8 @@
1
+ import React from 'react';
2
+ import { TourTheme } from '../tour.types';
3
+ /** Proveedor raíz — colócalo una vez en la raíz de la app para configurar el tema del tour. */
4
+ export declare const TourProvider: React.FC<{
5
+ children: React.ReactNode;
6
+ theme?: Partial<TourTheme>;
7
+ }>;
8
+ //# sourceMappingURL=tour.context.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tour.context.d.ts","sourceRoot":"","sources":["../../service/tour.context.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAC;AAC1B,OAAO,EAAsB,SAAS,EAAE,MAAM,eAAe,CAAC;AAG9D,+FAA+F;AAC/F,eAAO,MAAM,YAAY,EAAE,KAAK,CAAC,EAAE,CAAC;IAAE,QAAQ,EAAE,KAAK,CAAC,SAAS,CAAC;IAAC,KAAK,CAAC,EAAE,OAAO,CAAC,SAAS,CAAC,CAAA;CAAE,CAO5F,CAAC"}
@@ -0,0 +1,8 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { DEFAULT_TOUR_THEME } from '../tour.types';
3
+ import { TourThemeProvider } from './tour-theme.context';
4
+ /** Proveedor raíz — colócalo una vez en la raíz de la app para configurar el tema del tour. */
5
+ export const TourProvider = ({ children, theme }) => {
6
+ const resolvedTheme = Object.assign(Object.assign({}, DEFAULT_TOUR_THEME), theme);
7
+ return (_jsx(TourThemeProvider, { theme: resolvedTheme, children: children }));
8
+ };
@@ -0,0 +1,58 @@
1
+ import { RefObject } from "react";
2
+ import { View } from "react-native";
3
+ import type { ReactNode } from "react";
4
+ export type TourStepContent = string | ReactNode;
5
+ export type TourState = 'IDLE' | 'RUNNING' | 'FINISHED';
6
+ export type TooltipPosition = 'top' | 'bottom' | 'auto';
7
+ /** Controla el tamaño del tooltip. 'sm' = compacto (predeterminado), 'lg' = ancho. */
8
+ export type TourStepSize = 'sm' | 'lg';
9
+ export interface TourTheme {
10
+ /** Color principal: botón "Siguiente", badge de progreso */
11
+ primaryColor: string;
12
+ /** Color del texto sobre el botón primario */
13
+ primaryTextColor: string;
14
+ /** Color de fondo del tooltip */
15
+ tooltipBackgroundColor: string;
16
+ /** Color del título del tooltip */
17
+ tooltipTitleColor: string;
18
+ /** Color del texto de descripción */
19
+ tooltipDescriptionColor: string;
20
+ /** Color del texto de progreso ("1 / 6") */
21
+ tooltipProgressColor: string;
22
+ /** Color del botón "Saltar" */
23
+ skipButtonTextColor: string;
24
+ /** Color de fondo del botón "Saltar" (por defecto transparente) */
25
+ skipButtonBackgroundColor: string;
26
+ /** Border radius del tooltip */
27
+ tooltipBorderRadius: number;
28
+ /** Color del overlay semitransparente */
29
+ overlayColor: string;
30
+ /** Color del borde del highlight */
31
+ highlightBorderColor: string;
32
+ /** Border radius del highlight */
33
+ highlightBorderRadius: number;
34
+ }
35
+ export declare const DEFAULT_TOUR_THEME: TourTheme;
36
+ export interface TourStepConfig {
37
+ id: string;
38
+ ref: RefObject<View | null>;
39
+ title: string;
40
+ /** Contenido del tooltip. Puede ser un string (texto simple) o un ReactNode (contenido personalizado). */
41
+ content?: TourStepContent;
42
+ tooltipPosition?: TooltipPosition;
43
+ highlightPadding?: number;
44
+ /** Controla el tamaño del tooltip. */
45
+ tooltipSize?: TourStepSize;
46
+ }
47
+ /** Handle imperativo expuesto por <Tour> mediante forwardRef */
48
+ export interface TourHandle {
49
+ start: () => void;
50
+ stop: () => void;
51
+ }
52
+ export interface ElementMeasure {
53
+ x: number;
54
+ y: number;
55
+ width: number;
56
+ height: number;
57
+ }
58
+ //# sourceMappingURL=tour.types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tour.types.d.ts","sourceRoot":"","sources":["../tour.types.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AAClC,OAAO,EAAE,IAAI,EAAE,MAAM,cAAc,CAAC;AACpC,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AAEvC,MAAM,MAAM,eAAe,GAAG,MAAM,GAAG,SAAS,CAAC;AAEjD,MAAM,MAAM,SAAS,GAAG,MAAM,GAAG,SAAS,GAAG,UAAU,CAAC;AAExD,MAAM,MAAM,eAAe,GAAG,KAAK,GAAG,QAAQ,GAAG,MAAM,CAAC;AAExD,sFAAsF;AACtF,MAAM,MAAM,YAAY,GAAG,IAAI,GAAG,IAAI,CAAC;AAEvC,MAAM,WAAW,SAAS;IACtB,4DAA4D;IAC5D,YAAY,EAAE,MAAM,CAAC;IACrB,8CAA8C;IAC9C,gBAAgB,EAAE,MAAM,CAAC;IACzB,iCAAiC;IACjC,sBAAsB,EAAE,MAAM,CAAC;IAC/B,mCAAmC;IACnC,iBAAiB,EAAE,MAAM,CAAC;IAC1B,qCAAqC;IACrC,uBAAuB,EAAE,MAAM,CAAC;IAChC,4CAA4C;IAC5C,oBAAoB,EAAE,MAAM,CAAC;IAC7B,+BAA+B;IAC/B,mBAAmB,EAAE,MAAM,CAAC;IAC5B,mEAAmE;IACnE,yBAAyB,EAAE,MAAM,CAAC;IAClC,gCAAgC;IAChC,mBAAmB,EAAE,MAAM,CAAC;IAC5B,yCAAyC;IACzC,YAAY,EAAE,MAAM,CAAC;IACrB,oCAAoC;IACpC,oBAAoB,EAAE,MAAM,CAAC;IAC7B,kCAAkC;IAClC,qBAAqB,EAAE,MAAM,CAAC;CACjC;AAED,eAAO,MAAM,kBAAkB,EAAE,SAahC,CAAC;AAEF,MAAM,WAAW,cAAc;IAC3B,EAAE,EAAE,MAAM,CAAC;IACX,GAAG,EAAE,SAAS,CAAC,IAAI,GAAG,IAAI,CAAC,CAAC;IAC5B,KAAK,EAAE,MAAM,CAAC;IACd,0GAA0G;IAC1G,OAAO,CAAC,EAAE,eAAe,CAAC;IAC1B,eAAe,CAAC,EAAE,eAAe,CAAC;IAClC,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,sCAAsC;IACtC,WAAW,CAAC,EAAE,YAAY,CAAC;CAC9B;AAED,gEAAgE;AAChE,MAAM,WAAW,UAAU;IACvB,KAAK,EAAE,MAAM,IAAI,CAAC;IAClB,IAAI,EAAE,MAAM,IAAI,CAAC;CACpB;AAED,MAAM,WAAW,cAAc;IAC3B,CAAC,EAAE,MAAM,CAAC;IACV,CAAC,EAAE,MAAM,CAAC;IACV,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;CAClB"}
@@ -0,0 +1,14 @@
1
+ export const DEFAULT_TOUR_THEME = {
2
+ primaryColor: '#2563eb',
3
+ primaryTextColor: '#ffffff',
4
+ tooltipBackgroundColor: '#ffffff',
5
+ tooltipTitleColor: '#111111',
6
+ tooltipDescriptionColor: '#444444',
7
+ tooltipProgressColor: '#888888',
8
+ skipButtonTextColor: '#888888',
9
+ skipButtonBackgroundColor: 'transparent',
10
+ tooltipBorderRadius: 10,
11
+ overlayColor: 'rgba(0,0,0,0.6)',
12
+ highlightBorderColor: '#ffffff',
13
+ highlightBorderRadius: 10,
14
+ };
@@ -0,0 +1,8 @@
1
+ import { ElementMeasure, TooltipPosition } from '../tour.types';
2
+ export interface TooltipCoords {
3
+ top: number;
4
+ left: number;
5
+ }
6
+ /** Calcula la posición del tooltip basado en las medidas del elemento destacado, el tamaño de la pantalla y la posición preferida. */
7
+ export declare const calculateTooltipPosition: (measure: ElementMeasure, screenWidth: number, screenHeight: number, preferred?: TooltipPosition, tooltipHeight?: number) => TooltipCoords;
8
+ //# sourceMappingURL=tour-tooltip-position.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tour-tooltip-position.d.ts","sourceRoot":"","sources":["../../utils/tour-tooltip-position.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,eAAe,EAAE,MAAM,eAAe,CAAC;AAKhE,MAAM,WAAW,aAAa;IAC1B,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,MAAM,CAAC;CAChB;AAED,sIAAsI;AACtI,eAAO,MAAM,wBAAwB,GACjC,SAAS,cAAc,EACvB,aAAa,MAAM,EACnB,cAAc,MAAM,EACpB,YAAW,eAAwB,EACnC,gBAAe,MAAU,KAC1B,aAgBF,CAAC"}
@@ -0,0 +1,16 @@
1
+ const TOOLTIP_WIDTH = 260;
2
+ const TOOLTIP_MARGIN = 10;
3
+ /** Calcula la posición del tooltip basado en las medidas del elemento destacado, el tamaño de la pantalla y la posición preferida. */
4
+ export const calculateTooltipPosition = (measure, screenWidth, screenHeight, preferred = 'auto', tooltipHeight = 0) => {
5
+ const spaceBelow = screenHeight - (measure.y + measure.height);
6
+ const spaceAbove = measure.y;
7
+ const position = preferred !== 'auto' ? preferred
8
+ : spaceBelow >= tooltipHeight + TOOLTIP_MARGIN ? 'bottom'
9
+ : spaceAbove >= tooltipHeight + TOOLTIP_MARGIN ? 'top'
10
+ : 'bottom';
11
+ const clampedLeft = Math.max(16, Math.min(measure.x, screenWidth - TOOLTIP_WIDTH - 16));
12
+ if (position === 'bottom') {
13
+ return { top: measure.y + measure.height + TOOLTIP_MARGIN, left: clampedLeft };
14
+ }
15
+ return { top: measure.y - tooltipHeight - TOOLTIP_MARGIN, left: clampedLeft };
16
+ };
package/package.json ADDED
@@ -0,0 +1,35 @@
1
+ {
2
+ "name": "uo287827-react-native-tour-component",
3
+ "version": "1.0.0",
4
+ "description": "Componente de tour/onboarding interactivo para React Native",
5
+ "main": "dist/index.js",
6
+ "types": "dist/index.d.ts",
7
+ "keywords": [
8
+ "react-native",
9
+ "tour",
10
+ "onboarding",
11
+ "tutorial",
12
+ "guide",
13
+ "walkthrough"
14
+ ],
15
+ "author": "Raúl Mera Soto <UO287827@uniovi.es>",
16
+ "license": "MIT",
17
+ "peerDependencies": {
18
+ "react": ">=18.0.0",
19
+ "react-native": ">=0.70.0",
20
+ "react-native-svg": ">=13.0.0"
21
+ },
22
+ "scripts": {
23
+ "build": "tsc -p tsconfig.json",
24
+ "prepare": "npm run build"
25
+ },
26
+ "files": [
27
+ "dist",
28
+ "README.md"
29
+ ],
30
+ "devDependencies": {
31
+ "typescript": "^5.3.0",
32
+ "@types/react": "^19.2.14",
33
+ "@types/react-native": "^0.73.0"
34
+ }
35
+ }