@elogroup-sereduc/portal-aluno-tour 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.
package/README.md ADDED
@@ -0,0 +1,80 @@
1
+ # Portal Aluno Tour
2
+
3
+ Componente de tour guiado customizado usando HeroUI para o Portal do Aluno.
4
+
5
+ ## Instalação
6
+
7
+ ```bash
8
+ npm install @elogroup-sereduc/portal-aluno-tour
9
+ ```
10
+
11
+ ## Uso
12
+
13
+ ```tsx
14
+ import { Tour } from "@elogroup-sereduc/portal-aluno-tour";
15
+ import { useState } from "react";
16
+
17
+ function MyComponent() {
18
+ const [tourEnabled, setTourEnabled] = useState(false);
19
+
20
+ const steps = [
21
+ {
22
+ element: ".my-element",
23
+ intro: "Este é o primeiro passo do tour",
24
+ position: "bottom",
25
+ },
26
+ {
27
+ element: ".another-element",
28
+ intro: "Este é o segundo passo",
29
+ position: "right",
30
+ },
31
+ ];
32
+
33
+ return (
34
+ <>
35
+ <button onClick={() => setTourEnabled(true)}>Iniciar Tour</button>
36
+ <Tour
37
+ enabled={tourEnabled}
38
+ steps={steps}
39
+ onExit={() => setTourEnabled(false)}
40
+ options={{
41
+ nextLabel: "Próximo",
42
+ prevLabel: "Anterior",
43
+ skipLabel: "Pular",
44
+ doneLabel: "Concluir",
45
+ }}
46
+ />
47
+ </>
48
+ );
49
+ }
50
+ ```
51
+
52
+ ## Props
53
+
54
+ ### TourProps
55
+
56
+ - `enabled: boolean` - Se o tour está ativo
57
+ - `steps: TourStep[]` - Array de passos do tour
58
+ - `initialStep?: number` - Passo inicial (padrão: 0)
59
+ - `options?: TourOptions` - Opções de configuração
60
+ - `onExit?: () => void` - Callback quando o tour é encerrado
61
+ - `onComplete?: () => void` - Callback quando o tour é concluído
62
+
63
+ ### TourStep
64
+
65
+ - `element: string` - Seletor CSS do elemento
66
+ - `intro: string` - Texto explicativo
67
+ - `position?: "top" | "bottom" | "left" | "right"` - Posição da tooltip
68
+ - `title?: string` - Título do passo (opcional)
69
+
70
+ ### TourOptions
71
+
72
+ - `nextLabel?: string` - Label do botão "Próximo"
73
+ - `prevLabel?: string` - Label do botão "Anterior"
74
+ - `skipLabel?: string` - Label do botão "Pular"
75
+ - `doneLabel?: string` - Label do botão "Concluir"
76
+ - `showProgress?: boolean` - Mostrar barra de progresso
77
+ - `showBullets?: boolean` - Mostrar bullets de navegação
78
+ - `exitOnOverlayClick?: boolean` - Permitir fechar clicando no overlay
79
+ - `exitOnEsc?: boolean` - Permitir fechar com ESC
80
+
package/package.json ADDED
@@ -0,0 +1,35 @@
1
+ {
2
+ "name": "@elogroup-sereduc/portal-aluno-tour",
3
+ "version": "1.0.0",
4
+ "description": "Componente de tour guiado customizado usando HeroUI para o Portal do Aluno",
5
+ "main": "dist/index.js",
6
+ "types": "dist/index.d.ts",
7
+ "scripts": {
8
+ "build": "tsc",
9
+ "dev": "tsc --watch"
10
+ },
11
+ "peerDependencies": {
12
+ "react": "^18.0.0",
13
+ "react-dom": "^18.0.0",
14
+ "@heroui/button": "^2.0.0",
15
+ "@heroui/system": "^2.0.0",
16
+ "react-icons": "^5.0.0"
17
+ },
18
+ "devDependencies": {
19
+ "@types/react": "^18.0.0",
20
+ "@types/react-dom": "^18.0.0",
21
+ "typescript": "^5.0.0"
22
+ },
23
+ "keywords": [
24
+ "tour",
25
+ "onboarding",
26
+ "heroui",
27
+ "react"
28
+ ],
29
+ "author": "Leandro Passos - Elogroup",
30
+ "license": "MIT",
31
+ "publishConfig": {
32
+ "access": "public"
33
+ }
34
+ }
35
+
@@ -0,0 +1,349 @@
1
+ import { useEffect, useState, useCallback, useRef } from "react";
2
+ import { Button } from "@heroui/button";
3
+ import { TourProps, TourStep } from "../types";
4
+ import { HiX, HiChevronLeft, HiChevronRight } from "react-icons/hi2";
5
+
6
+ /**
7
+ * Componente de tour guiado customizado usando HeroUI
8
+ */
9
+ export function Tour({
10
+ enabled,
11
+ steps,
12
+ initialStep = 0,
13
+ options = {},
14
+ onExit,
15
+ onComplete,
16
+ }: TourProps) {
17
+ const [currentStep, setCurrentStep] = useState(initialStep);
18
+ const [highlightedElement, setHighlightedElement] = useState<HTMLElement | null>(null);
19
+ const [tooltipPosition, setTooltipPosition] = useState<{ top: number; left: number } | null>(null);
20
+ const overlayRef = useRef<HTMLDivElement>(null);
21
+ const tooltipRef = useRef<HTMLDivElement>(null);
22
+
23
+ const {
24
+ nextLabel = "Próximo",
25
+ prevLabel = "Anterior",
26
+ skipLabel = "Pular",
27
+ doneLabel = "Concluir",
28
+ showProgress = true,
29
+ showBullets = true,
30
+ exitOnOverlayClick = false,
31
+ exitOnEsc = true,
32
+ } = options;
33
+
34
+ // Calcula a posição da tooltip baseado no elemento destacado
35
+ const calculateTooltipPosition = useCallback((element: HTMLElement, position: string = "bottom") => {
36
+ const rect = element.getBoundingClientRect();
37
+ const scrollTop = window.pageYOffset || document.documentElement.scrollTop;
38
+ const scrollLeft = window.pageXOffset || document.documentElement.scrollLeft;
39
+
40
+ let top = 0;
41
+ let left = 0;
42
+
43
+ switch (position) {
44
+ case "top":
45
+ top = rect.top + scrollTop - 10;
46
+ left = rect.left + scrollLeft + rect.width / 2;
47
+ break;
48
+ case "bottom":
49
+ top = rect.bottom + scrollTop + 10;
50
+ left = rect.left + scrollLeft + rect.width / 2;
51
+ break;
52
+ case "left":
53
+ top = rect.top + scrollTop + rect.height / 2;
54
+ left = rect.left + scrollLeft - 10;
55
+ break;
56
+ case "right":
57
+ top = rect.top + scrollTop + rect.height / 2;
58
+ left = rect.right + scrollLeft + 10;
59
+ break;
60
+ default:
61
+ top = rect.bottom + scrollTop + 10;
62
+ left = rect.left + scrollLeft + rect.width / 2;
63
+ }
64
+
65
+ return { top, left };
66
+ }, []);
67
+
68
+ // Destaca o elemento atual
69
+ useEffect(() => {
70
+ if (!enabled || currentStep < 0 || currentStep >= steps.length) {
71
+ setHighlightedElement(null);
72
+ setTooltipPosition(null);
73
+ return;
74
+ }
75
+
76
+ const step = steps[currentStep];
77
+ const element = document.querySelector(step.element) as HTMLElement;
78
+
79
+ if (!element) {
80
+ console.warn(`Elemento não encontrado: ${step.element}`);
81
+ return;
82
+ }
83
+
84
+ setHighlightedElement(element);
85
+
86
+ // Scroll para o elemento
87
+ element.scrollIntoView({ behavior: "smooth", block: "center" });
88
+
89
+ // Calcula posição da tooltip após um pequeno delay para garantir que o scroll terminou
90
+ setTimeout(() => {
91
+ const position = calculateTooltipPosition(element, step.position);
92
+ setTooltipPosition(position);
93
+ }, 300);
94
+ }, [enabled, currentStep, steps, calculateTooltipPosition]);
95
+
96
+ // Adiciona overlay e highlight ao elemento
97
+ useEffect(() => {
98
+ if (!highlightedElement) {
99
+ // Remove highlights anteriores
100
+ document.querySelectorAll(".tour-highlight").forEach((el) => {
101
+ el.classList.remove("tour-highlight");
102
+ });
103
+ return;
104
+ }
105
+
106
+ // Adiciona classe de highlight
107
+ highlightedElement.classList.add("tour-highlight");
108
+
109
+ return () => {
110
+ highlightedElement.classList.remove("tour-highlight");
111
+ };
112
+ }, [highlightedElement]);
113
+
114
+ // Handler de teclado (ESC)
115
+ useEffect(() => {
116
+ if (!enabled || !exitOnEsc) return;
117
+
118
+ const handleKeyDown = (e: KeyboardEvent) => {
119
+ if (e.key === "Escape") {
120
+ handleExit();
121
+ } else if (e.key === "ArrowRight") {
122
+ handleNext();
123
+ } else if (e.key === "ArrowLeft") {
124
+ handlePrev();
125
+ }
126
+ };
127
+
128
+ window.addEventListener("keydown", handleKeyDown);
129
+ return () => window.removeEventListener("keydown", handleKeyDown);
130
+ }, [enabled, exitOnEsc, currentStep, steps.length]);
131
+
132
+ const handleNext = useCallback(() => {
133
+ if (currentStep < steps.length - 1) {
134
+ setCurrentStep(currentStep + 1);
135
+ } else {
136
+ handleComplete();
137
+ }
138
+ }, [currentStep, steps.length]);
139
+
140
+ const handlePrev = useCallback(() => {
141
+ if (currentStep > 0) {
142
+ setCurrentStep(currentStep - 1);
143
+ }
144
+ }, [currentStep]);
145
+
146
+ const handleSkip = useCallback(() => {
147
+ handleExit();
148
+ }, []);
149
+
150
+ const handleComplete = useCallback(() => {
151
+ onComplete?.();
152
+ handleExit();
153
+ }, [onComplete]);
154
+
155
+ const handleExit = useCallback(() => {
156
+ setCurrentStep(initialStep);
157
+ setHighlightedElement(null);
158
+ setTooltipPosition(null);
159
+ onExit?.();
160
+ }, [initialStep, onExit]);
161
+
162
+ const handleOverlayClick = useCallback(
163
+ (e: React.MouseEvent<HTMLDivElement>) => {
164
+ if (exitOnOverlayClick && e.target === overlayRef.current) {
165
+ handleExit();
166
+ }
167
+ },
168
+ [exitOnOverlayClick, handleExit]
169
+ );
170
+
171
+ if (!enabled) {
172
+ return null;
173
+ }
174
+
175
+ const currentStepData = steps[currentStep];
176
+ const isFirstStep = currentStep === 0;
177
+ const isLastStep = currentStep === steps.length - 1;
178
+ const progress = ((currentStep + 1) / steps.length) * 100;
179
+
180
+ return (
181
+ <>
182
+ {/* Overlay escuro */}
183
+ <div
184
+ ref={overlayRef}
185
+ className="fixed inset-0 bg-black/60 z-[9998]"
186
+ onClick={handleOverlayClick}
187
+ style={{ pointerEvents: exitOnOverlayClick ? "auto" : "none" }}
188
+ />
189
+
190
+ {/* Tooltip */}
191
+ {tooltipPosition && currentStepData && highlightedElement && (
192
+ <div
193
+ ref={tooltipRef}
194
+ className="fixed z-[9999] max-w-sm"
195
+ style={{
196
+ top: currentStepData.position === "bottom" ? `${tooltipPosition.top}px` : undefined,
197
+ bottom: currentStepData.position === "top" ? `${window.innerHeight - tooltipPosition.top}px` : undefined,
198
+ left: currentStepData.position === "left" || currentStepData.position === "right"
199
+ ? `${tooltipPosition.left}px`
200
+ : `${tooltipPosition.left}px`,
201
+ transform: currentStepData.position === "left" || currentStepData.position === "right"
202
+ ? "translate(0, -50%)"
203
+ : "translate(-50%, 0)",
204
+ }}
205
+ >
206
+ <div className="bg-white rounded-lg shadow-xl p-6 relative">
207
+ {/* Botão fechar */}
208
+ <button
209
+ onClick={handleExit}
210
+ className="absolute top-2 right-2 p-1 rounded-full hover:bg-gray-100 transition-colors"
211
+ aria-label="Fechar tour"
212
+ >
213
+ <HiX className="w-5 h-5 text-gray-500" />
214
+ </button>
215
+
216
+ {/* Título (se fornecido) */}
217
+ {currentStepData.title && (
218
+ <h3 className="text-lg font-semibold text-gray-900 mb-2 pr-6">
219
+ {currentStepData.title}
220
+ </h3>
221
+ )}
222
+
223
+ {/* Conteúdo */}
224
+ <p className="text-gray-700 mb-4">{currentStepData.intro}</p>
225
+
226
+ {/* Progresso */}
227
+ {showProgress && (
228
+ <div className="mb-4">
229
+ <div className="w-full bg-gray-200 rounded-full h-2">
230
+ <div
231
+ className="bg-brand-primary h-2 rounded-full transition-all duration-300"
232
+ style={{ width: `${progress}%` }}
233
+ />
234
+ </div>
235
+ <p className="text-xs text-gray-500 mt-1 text-center">
236
+ {currentStep + 1} de {steps.length}
237
+ </p>
238
+ </div>
239
+ )}
240
+
241
+ {/* Bullets */}
242
+ {showBullets && (
243
+ <div className="flex justify-center gap-1 mb-4">
244
+ {steps.map((_, index) => (
245
+ <button
246
+ key={index}
247
+ onClick={() => setCurrentStep(index)}
248
+ className={`w-2 h-2 rounded-full transition-all ${
249
+ index === currentStep
250
+ ? "bg-brand-primary w-6"
251
+ : "bg-gray-300 hover:bg-gray-400"
252
+ }`}
253
+ aria-label={`Ir para passo ${index + 1}`}
254
+ />
255
+ ))}
256
+ </div>
257
+ )}
258
+
259
+ {/* Botões de navegação */}
260
+ <div className="flex justify-between items-center gap-2">
261
+ <div className="flex gap-2">
262
+ {!isFirstStep && (
263
+ <Button
264
+ variant="bordered"
265
+ onPress={handlePrev}
266
+ startContent={<HiChevronLeft className="w-4 h-4" />}
267
+ >
268
+ {prevLabel}
269
+ </Button>
270
+ )}
271
+ </div>
272
+
273
+ <div className="flex gap-2">
274
+ <Button variant="light" onPress={handleSkip}>
275
+ {skipLabel}
276
+ </Button>
277
+ <Button
278
+ color="primary"
279
+ onPress={isLastStep ? handleComplete : handleNext}
280
+ endContent={!isLastStep ? <HiChevronRight className="w-4 h-4" /> : undefined}
281
+ >
282
+ {isLastStep ? doneLabel : nextLabel}
283
+ </Button>
284
+ </div>
285
+ </div>
286
+ </div>
287
+
288
+ {/* Seta indicadora */}
289
+ {currentStepData.position === "bottom" && (
290
+ <div
291
+ className="absolute w-0 h-0 border-8 border-transparent"
292
+ style={{
293
+ top: "-16px",
294
+ left: "50%",
295
+ transform: "translateX(-50%)",
296
+ borderColor: "transparent transparent white transparent",
297
+ }}
298
+ />
299
+ )}
300
+ {currentStepData.position === "top" && (
301
+ <div
302
+ className="absolute w-0 h-0 border-8 border-transparent"
303
+ style={{
304
+ bottom: "-16px",
305
+ left: "50%",
306
+ transform: "translateX(-50%)",
307
+ borderColor: "white transparent transparent transparent",
308
+ }}
309
+ />
310
+ )}
311
+ {currentStepData.position === "right" && (
312
+ <div
313
+ className="absolute w-0 h-0 border-8 border-transparent"
314
+ style={{
315
+ left: "-16px",
316
+ top: "50%",
317
+ transform: "translateY(-50%)",
318
+ borderColor: "transparent white transparent transparent",
319
+ }}
320
+ />
321
+ )}
322
+ {currentStepData.position === "left" && (
323
+ <div
324
+ className="absolute w-0 h-0 border-8 border-transparent"
325
+ style={{
326
+ right: "-16px",
327
+ top: "50%",
328
+ transform: "translateY(-50%)",
329
+ borderColor: "transparent transparent transparent white",
330
+ }}
331
+ />
332
+ )}
333
+ </div>
334
+ )}
335
+
336
+ {/* Estilos inline para highlight */}
337
+ <style>{`
338
+ .tour-highlight {
339
+ position: relative;
340
+ z-index: 9999 !important;
341
+ outline: 3px solid var(--brand-primary, #0056b0) !important;
342
+ outline-offset: 2px;
343
+ border-radius: 4px;
344
+ }
345
+ `}</style>
346
+ </>
347
+ );
348
+ }
349
+
package/src/index.ts ADDED
@@ -0,0 +1,3 @@
1
+ export { Tour } from "./components/Tour";
2
+ export type { TourProps, TourStep, TourOptions } from "./types";
3
+
@@ -0,0 +1,110 @@
1
+ /**
2
+ * Tipos para o componente de tour
3
+ */
4
+
5
+ export interface TourStep {
6
+ /**
7
+ * Seletor CSS do elemento a ser destacado
8
+ */
9
+ element: string;
10
+
11
+ /**
12
+ * Texto explicativo do passo
13
+ */
14
+ intro: string;
15
+
16
+ /**
17
+ * Posição da tooltip em relação ao elemento
18
+ * @default "bottom"
19
+ */
20
+ position?: "top" | "bottom" | "left" | "right";
21
+
22
+ /**
23
+ * Título do passo (opcional)
24
+ */
25
+ title?: string;
26
+ }
27
+
28
+ export interface TourOptions {
29
+ /**
30
+ * Label do botão "Próximo"
31
+ * @default "Próximo"
32
+ */
33
+ nextLabel?: string;
34
+
35
+ /**
36
+ * Label do botão "Anterior"
37
+ * @default "Anterior"
38
+ */
39
+ prevLabel?: string;
40
+
41
+ /**
42
+ * Label do botão "Pular"
43
+ * @default "Pular"
44
+ */
45
+ skipLabel?: string;
46
+
47
+ /**
48
+ * Label do botão "Concluir"
49
+ * @default "Concluir"
50
+ */
51
+ doneLabel?: string;
52
+
53
+ /**
54
+ * Mostrar barra de progresso
55
+ * @default true
56
+ */
57
+ showProgress?: boolean;
58
+
59
+ /**
60
+ * Mostrar bullets de navegação
61
+ * @default true
62
+ */
63
+ showBullets?: boolean;
64
+
65
+ /**
66
+ * Permitir fechar clicando no overlay
67
+ * @default false
68
+ */
69
+ exitOnOverlayClick?: boolean;
70
+
71
+ /**
72
+ * Permitir fechar com ESC
73
+ * @default true
74
+ */
75
+ exitOnEsc?: boolean;
76
+ }
77
+
78
+ export interface TourProps {
79
+ /**
80
+ * Se o tour está habilitado/ativo
81
+ */
82
+ enabled: boolean;
83
+
84
+ /**
85
+ * Array de passos do tour
86
+ */
87
+ steps: TourStep[];
88
+
89
+ /**
90
+ * Passo inicial (índice)
91
+ * @default 0
92
+ */
93
+ initialStep?: number;
94
+
95
+ /**
96
+ * Opções de configuração do tour
97
+ */
98
+ options?: TourOptions;
99
+
100
+ /**
101
+ * Callback quando o tour é encerrado
102
+ */
103
+ onExit?: () => void;
104
+
105
+ /**
106
+ * Callback quando o tour é concluído
107
+ */
108
+ onComplete?: () => void;
109
+ }
110
+
package/tsconfig.json ADDED
@@ -0,0 +1,23 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2020",
4
+ "module": "ESNext",
5
+ "lib": ["ES2020", "DOM", "DOM.Iterable"],
6
+ "jsx": "react-jsx",
7
+ "declaration": true,
8
+ "declarationMap": true,
9
+ "outDir": "./dist",
10
+ "rootDir": "./src",
11
+ "strict": true,
12
+ "esModuleInterop": true,
13
+ "skipLibCheck": true,
14
+ "forceConsistentCasingInFileNames": true,
15
+ "moduleResolution": "bundler",
16
+ "resolveJsonModule": true,
17
+ "isolatedModules": true,
18
+ "noEmit": false
19
+ },
20
+ "include": ["src/**/*"],
21
+ "exclude": ["node_modules", "dist"]
22
+ }
23
+