@elogroup-sereduc/portal-aluno-tour 1.0.2 → 1.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -8,6 +8,12 @@ Componente de tour guiado customizado usando HeroUI para o Portal do Aluno.
8
8
  npm install @elogroup-sereduc/portal-aluno-tour
9
9
  ```
10
10
 
11
+ ## Configuração do Tailwind
12
+
13
+ Este pacote usa `@elogroup-sereduc/portal-aluno-tailwind-config` com o prefixo `portal-tour:`.
14
+
15
+ Certifique-se de que o Tailwind está configurado para processar as classes deste pacote. O pacote inclui um `tailwind.config.js` que estende a configuração compartilhada.
16
+
11
17
  ## Uso
12
18
 
13
19
  ```tsx
@@ -77,4 +83,3 @@ function MyComponent() {
77
83
  - `showBullets?: boolean` - Mostrar bullets de navegação
78
84
  - `exitOnOverlayClick?: boolean` - Permitir fechar clicando no overlay
79
85
  - `exitOnEsc?: boolean` - Permitir fechar com ESC
80
-
@@ -1 +1 @@
1
- {"version":3,"file":"Tour.d.ts","sourceRoot":"","sources":["../../src/components/Tour.tsx"],"names":[],"mappings":"AAEA,OAAO,EAAE,SAAS,EAAY,MAAM,UAAU,CAAC;AAG/C;;GAEG;AACH,wBAAgB,IAAI,CAAC,EACnB,OAAO,EACP,KAAK,EACL,WAAe,EACf,OAAY,EACZ,MAAM,EACN,UAAU,GACX,EAAE,SAAS,kDA4UX"}
1
+ {"version":3,"file":"Tour.d.ts","sourceRoot":"","sources":["../../src/components/Tour.tsx"],"names":[],"mappings":"AAEA,OAAO,EAAE,SAAS,EAAY,MAAM,UAAU,CAAC;AAG/C;;GAEG;AACH,wBAAgB,IAAI,CAAC,EACnB,OAAO,EACP,KAAK,EACL,WAAe,EACf,OAAY,EACZ,MAAM,EACN,UAAU,GACX,EAAE,SAAS,kDAqcX"}
@@ -7,44 +7,119 @@ import { HiXMark, HiChevronLeft, HiChevronRight } from "react-icons/hi2";
7
7
  */
8
8
  export function Tour({ enabled, steps, initialStep = 0, options = {}, onExit, onComplete, }) {
9
9
  const [currentStep, setCurrentStep] = useState(initialStep);
10
+ const [isVisible, setIsVisible] = useState(false);
10
11
  const [highlightedElement, setHighlightedElement] = useState(null);
11
12
  const [tooltipPosition, setTooltipPosition] = useState(null);
12
13
  const overlayRef = useRef(null);
13
14
  const tooltipRef = useRef(null);
15
+ const isConfiguredRef = useRef(false);
14
16
  const { nextLabel = "Próximo", prevLabel = "Anterior", skipLabel = "Pular", doneLabel = "Concluir", showProgress = true, showBullets = true, exitOnOverlayClick = false, exitOnEsc = true, } = options;
15
17
  // Calcula a posição da tooltip baseado no elemento destacado
16
18
  const calculateTooltipPosition = useCallback((element, position = "bottom") => {
17
19
  const rect = element.getBoundingClientRect();
18
20
  const scrollTop = window.pageYOffset || document.documentElement.scrollTop;
19
21
  const scrollLeft = window.pageXOffset || document.documentElement.scrollLeft;
22
+ const viewportWidth = window.innerWidth;
23
+ const viewportHeight = window.innerHeight;
24
+ // Tamanho estimado da tooltip (ajuste conforme necessário)
25
+ const tooltipWidth = 384; // max-w-sm = 384px
26
+ const tooltipHeight = 200; // altura estimada
27
+ const spacing = 16; // espaçamento entre elemento e tooltip
20
28
  let top = 0;
21
29
  let left = 0;
30
+ let finalPosition = position;
31
+ // Calcula posição base
22
32
  switch (position) {
23
33
  case "top":
24
- top = rect.top + scrollTop - 10;
34
+ top = rect.top + scrollTop - spacing;
25
35
  left = rect.left + scrollLeft + rect.width / 2;
26
36
  break;
27
37
  case "bottom":
28
- top = rect.bottom + scrollTop + 10;
38
+ top = rect.bottom + scrollTop + spacing;
29
39
  left = rect.left + scrollLeft + rect.width / 2;
30
40
  break;
31
41
  case "left":
32
42
  top = rect.top + scrollTop + rect.height / 2;
33
- left = rect.left + scrollLeft - 10;
43
+ left = rect.left + scrollLeft - spacing;
34
44
  break;
35
45
  case "right":
36
46
  top = rect.top + scrollTop + rect.height / 2;
37
- left = rect.right + scrollLeft + 10;
47
+ left = rect.right + scrollLeft + spacing;
38
48
  break;
39
49
  default:
40
- top = rect.bottom + scrollTop + 10;
50
+ top = rect.bottom + scrollTop + spacing;
41
51
  left = rect.left + scrollLeft + rect.width / 2;
52
+ finalPosition = "bottom";
42
53
  }
43
- return { top, left };
54
+ // Ajusta posição se a tooltip sair da viewport
55
+ if (finalPosition === "bottom" || finalPosition === "top") {
56
+ // Ajusta horizontalmente
57
+ if (left - tooltipWidth / 2 < scrollLeft) {
58
+ left = scrollLeft + tooltipWidth / 2 + 16;
59
+ }
60
+ else if (left + tooltipWidth / 2 > scrollLeft + viewportWidth) {
61
+ left = scrollLeft + viewportWidth - tooltipWidth / 2 - 16;
62
+ }
63
+ // Se não couber embaixo, tenta em cima
64
+ if (finalPosition === "bottom" && top + tooltipHeight > scrollTop + viewportHeight) {
65
+ if (rect.top - tooltipHeight > scrollTop) {
66
+ top = rect.top + scrollTop - tooltipHeight - spacing;
67
+ finalPosition = "top";
68
+ }
69
+ }
70
+ // Se não couber em cima, tenta embaixo
71
+ else if (finalPosition === "top" && top - tooltipHeight < scrollTop) {
72
+ if (rect.bottom + tooltipHeight < scrollTop + viewportHeight) {
73
+ top = rect.bottom + scrollTop + spacing;
74
+ finalPosition = "bottom";
75
+ }
76
+ }
77
+ }
78
+ else if (finalPosition === "left" || finalPosition === "right") {
79
+ // Ajusta verticalmente
80
+ if (top - tooltipHeight / 2 < scrollTop) {
81
+ top = scrollTop + tooltipHeight / 2 + 16;
82
+ }
83
+ else if (top + tooltipHeight / 2 > scrollTop + viewportHeight) {
84
+ top = scrollTop + viewportHeight - tooltipHeight / 2 - 16;
85
+ }
86
+ // Se não couber à esquerda, tenta à direita
87
+ if (finalPosition === "left" && left - tooltipWidth < scrollLeft) {
88
+ if (rect.right + tooltipWidth < scrollLeft + viewportWidth) {
89
+ left = rect.right + scrollLeft + spacing;
90
+ finalPosition = "right";
91
+ }
92
+ }
93
+ // Se não couber à direita, tenta à esquerda
94
+ else if (finalPosition === "right" && left + tooltipWidth > scrollLeft + viewportWidth) {
95
+ if (rect.left - tooltipWidth > scrollLeft) {
96
+ left = rect.left + scrollLeft - spacing;
97
+ finalPosition = "left";
98
+ }
99
+ }
100
+ }
101
+ return { top, left, position: finalPosition };
102
+ }, []);
103
+ // Configura o tour quando steps ou options mudam
104
+ const configureTour = useCallback(() => {
105
+ isConfiguredRef.current = true;
44
106
  }, []);
107
+ // Renderiza os steps (mostra/esconde o tour)
108
+ const renderSteps = useCallback(() => {
109
+ if (enabled && steps.length > 0 && !isVisible) {
110
+ setIsVisible(true);
111
+ setCurrentStep(initialStep);
112
+ }
113
+ else if (!enabled && isVisible) {
114
+ setIsVisible(false);
115
+ setCurrentStep(initialStep);
116
+ setHighlightedElement(null);
117
+ setTooltipPosition(null);
118
+ }
119
+ }, [enabled, steps.length, isVisible, initialStep]);
45
120
  // Destaca o elemento atual
46
121
  useEffect(() => {
47
- if (!enabled || currentStep < 0 || currentStep >= steps.length) {
122
+ if (!isVisible || currentStep < 0 || currentStep >= steps.length) {
48
123
  setHighlightedElement(null);
49
124
  setTooltipPosition(null);
50
125
  return;
@@ -60,28 +135,43 @@ export function Tour({ enabled, steps, initialStep = 0, options = {}, onExit, on
60
135
  element.scrollIntoView({ behavior: "smooth", block: "center" });
61
136
  // Calcula posição da tooltip após um pequeno delay para garantir que o scroll terminou
62
137
  setTimeout(() => {
63
- const position = calculateTooltipPosition(element, step.position);
138
+ const position = calculateTooltipPosition(element, step.position || "bottom");
64
139
  setTooltipPosition(position);
65
140
  }, 300);
66
- }, [enabled, currentStep, steps, calculateTooltipPosition]);
141
+ }, [isVisible, currentStep, steps, calculateTooltipPosition]);
67
142
  // Adiciona overlay e highlight ao elemento
68
143
  useEffect(() => {
69
144
  if (!highlightedElement) {
70
145
  // Remove highlights anteriores
71
146
  document.querySelectorAll(".tour-highlight").forEach((el) => {
72
147
  el.classList.remove("tour-highlight");
148
+ el.style.zIndex = "";
73
149
  });
74
150
  return;
75
151
  }
76
- // Adiciona classe de highlight
152
+ // Adiciona classe de highlight e z-index alto
77
153
  highlightedElement.classList.add("tour-highlight");
154
+ const originalZIndex = highlightedElement.style.zIndex;
155
+ highlightedElement.style.zIndex = "10000";
78
156
  return () => {
79
157
  highlightedElement.classList.remove("tour-highlight");
158
+ highlightedElement.style.zIndex = originalZIndex;
80
159
  };
81
160
  }, [highlightedElement]);
82
- // Handler de teclado (ESC)
161
+ // Configura o tour quando necessário
162
+ useEffect(() => {
163
+ if (!isConfiguredRef.current || steps.length > 0) {
164
+ configureTour();
165
+ renderSteps();
166
+ }
167
+ }, [steps, configureTour, renderSteps]);
168
+ // Atualiza quando enabled muda
169
+ useEffect(() => {
170
+ renderSteps();
171
+ }, [enabled, renderSteps]);
172
+ // Handler de teclado (ESC e setas)
83
173
  useEffect(() => {
84
- if (!enabled || !exitOnEsc)
174
+ if (!isVisible || !exitOnEsc)
85
175
  return;
86
176
  const handleKeyDown = (e) => {
87
177
  if (e.key === "Escape") {
@@ -96,7 +186,19 @@ export function Tour({ enabled, steps, initialStep = 0, options = {}, onExit, on
96
186
  };
97
187
  window.addEventListener("keydown", handleKeyDown);
98
188
  return () => window.removeEventListener("keydown", handleKeyDown);
99
- }, [enabled, exitOnEsc, currentStep, steps.length]);
189
+ }, [isVisible, exitOnEsc, currentStep, steps.length]);
190
+ // Controla overflow do body
191
+ useEffect(() => {
192
+ if (isVisible) {
193
+ document.body.style.overflow = "hidden";
194
+ }
195
+ else {
196
+ document.body.style.overflow = "";
197
+ }
198
+ return () => {
199
+ document.body.style.overflow = "";
200
+ };
201
+ }, [isVisible]);
100
202
  const handleNext = useCallback(() => {
101
203
  if (currentStep < steps.length - 1) {
102
204
  setCurrentStep(currentStep + 1);
@@ -118,6 +220,7 @@ export function Tour({ enabled, steps, initialStep = 0, options = {}, onExit, on
118
220
  handleExit();
119
221
  }, [onComplete]);
120
222
  const handleExit = useCallback(() => {
223
+ setIsVisible(false);
121
224
  setCurrentStep(initialStep);
122
225
  setHighlightedElement(null);
123
226
  setTooltipPosition(null);
@@ -128,51 +231,65 @@ export function Tour({ enabled, steps, initialStep = 0, options = {}, onExit, on
128
231
  handleExit();
129
232
  }
130
233
  }, [exitOnOverlayClick, handleExit]);
131
- if (!enabled) {
234
+ if (!isVisible) {
132
235
  return null;
133
236
  }
134
237
  const currentStepData = steps[currentStep];
135
238
  const isFirstStep = currentStep === 0;
136
239
  const isLastStep = currentStep === steps.length - 1;
137
240
  const progress = ((currentStep + 1) / steps.length) * 100;
138
- return (_jsxs(_Fragment, { children: [_jsx("div", { ref: overlayRef, className: "fixed inset-0 bg-black/60 z-[9998]", onClick: handleOverlayClick, style: { pointerEvents: exitOnOverlayClick ? "auto" : "none" } }), tooltipPosition && currentStepData && highlightedElement && (_jsxs("div", { ref: tooltipRef, className: "fixed z-[9999] max-w-sm", style: {
139
- top: currentStepData.position === "bottom" ? `${tooltipPosition.top}px` : undefined,
140
- bottom: currentStepData.position === "top" ? `${window.innerHeight - tooltipPosition.top}px` : undefined,
141
- left: currentStepData.position === "left" || currentStepData.position === "right"
241
+ return (_jsxs(_Fragment, { children: [_jsx("div", { ref: overlayRef, onClick: handleOverlayClick, style: {
242
+ position: "fixed",
243
+ top: 0,
244
+ left: 0,
245
+ right: 0,
246
+ bottom: 0,
247
+ backgroundColor: "rgba(0, 0, 0, 0.6)",
248
+ zIndex: 9998,
249
+ pointerEvents: exitOnOverlayClick ? "auto" : "none",
250
+ } }), tooltipPosition && currentStepData && highlightedElement && (_jsxs("div", { ref: tooltipRef, className: "portal-tour:max-w-sm", style: {
251
+ position: "fixed",
252
+ top: tooltipPosition.position === "bottom" || tooltipPosition.position === "left" || tooltipPosition.position === "right"
253
+ ? `${tooltipPosition.top}px`
254
+ : undefined,
255
+ bottom: tooltipPosition.position === "top" ? `${window.innerHeight - tooltipPosition.top}px` : undefined,
256
+ left: tooltipPosition.position === "left" || tooltipPosition.position === "right"
142
257
  ? `${tooltipPosition.left}px`
143
258
  : `${tooltipPosition.left}px`,
144
- transform: currentStepData.position === "left" || currentStepData.position === "right"
259
+ transform: tooltipPosition.position === "left" || tooltipPosition.position === "right"
145
260
  ? "translate(0, -50%)"
146
261
  : "translate(-50%, 0)",
147
- }, children: [_jsxs("div", { className: "bg-white rounded-lg shadow-xl p-6 relative", children: [_jsx("button", { onClick: handleExit, className: "absolute top-2 right-2 p-1 rounded-full hover:bg-gray-100 transition-colors", "aria-label": "Fechar tour", children: _jsx(HiXMark, { className: "w-5 h-5 text-gray-500" }) }), currentStepData.title && (_jsx("h3", { className: "text-lg font-semibold text-gray-900 mb-2 pr-6", children: currentStepData.title })), _jsx("p", { className: "text-gray-700 mb-4", children: currentStepData.intro }), showProgress && (_jsxs("div", { className: "mb-4", children: [_jsx("div", { className: "w-full bg-gray-200 rounded-full h-2", children: _jsx("div", { className: "bg-brand-primary h-2 rounded-full transition-all duration-300", style: { width: `${progress}%` } }) }), _jsxs("p", { className: "text-xs text-gray-500 mt-1 text-center", children: [currentStep + 1, " de ", steps.length] })] })), showBullets && (_jsx("div", { className: "flex justify-center gap-1 mb-4", children: steps.map((_, index) => (_jsx("button", { onClick: () => setCurrentStep(index), className: `w-2 h-2 rounded-full transition-all ${index === currentStep
148
- ? "bg-brand-primary w-6"
149
- : "bg-gray-300 hover:bg-gray-400"}`, "aria-label": `Ir para passo ${index + 1}` }, index))) })), _jsxs("div", { className: "flex justify-between items-center gap-2", children: [_jsx("div", { className: "flex gap-2", children: !isFirstStep && (_jsx(Button, { variant: "bordered", onPress: handlePrev, startContent: _jsx(HiChevronLeft, { className: "w-4 h-4" }), children: prevLabel })) }), _jsxs("div", { className: "flex gap-2", children: [_jsx(Button, { variant: "light", onPress: handleSkip, children: skipLabel }), _jsx(Button, { color: "primary", onPress: isLastStep ? handleComplete : handleNext, endContent: !isLastStep ? _jsx(HiChevronRight, { className: "w-4 h-4" }) : undefined, children: isLastStep ? doneLabel : nextLabel })] })] })] }), currentStepData.position === "bottom" && (_jsx("div", { className: "absolute w-0 h-0 border-8 border-transparent", style: {
262
+ zIndex: 10001,
263
+ }, children: [_jsxs("div", { className: "portal-tour:bg-white portal-tour:rounded-lg portal-tour:shadow-xl portal-tour:p-6 portal-tour:relative", children: [_jsx("button", { onClick: handleExit, className: "portal-tour:absolute portal-tour:top-2 portal-tour:right-2 portal-tour:p-1 portal-tour:rounded-full portal-tour:hover:bg-gray-100 portal-tour:transition-colors portal-tour:z-10", "aria-label": "Fechar tour", children: _jsx(HiXMark, { className: "portal-tour:w-5 portal-tour:h-5 portal-tour:text-gray-500" }) }), currentStepData.title && (_jsx("h3", { className: "portal-tour:text-lg portal-tour:font-semibold portal-tour:text-gray-900 portal-tour:mb-2 portal-tour:pr-6", children: currentStepData.title })), _jsx("p", { className: "portal-tour:text-gray-700 portal-tour:mb-4", children: currentStepData.intro }), showProgress && (_jsxs("div", { className: "portal-tour:mb-4", children: [_jsx("div", { className: "portal-tour:w-full portal-tour:bg-gray-200 portal-tour:rounded-full portal-tour:h-2", children: _jsx("div", { className: "portal-tour:bg-brand-primary portal-tour:h-2 portal-tour:rounded-full portal-tour:transition-all portal-tour:duration-300", style: { width: `${progress}%` } }) }), _jsxs("p", { className: "portal-tour:text-xs portal-tour:text-gray-500 portal-tour:mt-1 portal-tour:text-center", children: [currentStep + 1, " de ", steps.length] })] })), showBullets && (_jsx("div", { className: "portal-tour:flex portal-tour:justify-center portal-tour:gap-1 portal-tour:mb-4", children: steps.map((_, index) => (_jsx("button", { onClick: () => setCurrentStep(index), className: `portal-tour:w-2 portal-tour:h-2 portal-tour:rounded-full portal-tour:transition-all ${index === currentStep
264
+ ? "portal-tour:bg-brand-primary portal-tour:w-6"
265
+ : "portal-tour:bg-gray-300 portal-tour:hover:bg-gray-400"}`, "aria-label": `Ir para passo ${index + 1}` }, index))) })), _jsxs("div", { className: "portal-tour:flex portal-tour:justify-between portal-tour:items-center portal-tour:gap-2", children: [_jsx("div", { className: "portal-tour:flex portal-tour:gap-2", children: !isFirstStep && (_jsx(Button, { variant: "bordered", onPress: handlePrev, startContent: _jsx(HiChevronLeft, { className: "portal-tour:w-4 portal-tour:h-4" }), children: prevLabel })) }), _jsxs("div", { className: "portal-tour:flex portal-tour:gap-2", children: [_jsx(Button, { variant: "light", onPress: handleSkip, children: skipLabel }), _jsx(Button, { color: "primary", onPress: isLastStep ? handleComplete : handleNext, endContent: !isLastStep ? _jsx(HiChevronRight, { className: "portal-tour:w-4 portal-tour:h-4" }) : undefined, children: isLastStep ? doneLabel : nextLabel })] })] })] }), tooltipPosition.position === "bottom" && (_jsx("div", { className: "portal-tour:absolute portal-tour:w-0 portal-tour:h-0 portal-tour:border-8 portal-tour:border-transparent", style: {
150
266
  top: "-16px",
151
267
  left: "50%",
152
268
  transform: "translateX(-50%)",
153
269
  borderColor: "transparent transparent white transparent",
154
- } })), currentStepData.position === "top" && (_jsx("div", { className: "absolute w-0 h-0 border-8 border-transparent", style: {
270
+ } })), tooltipPosition.position === "top" && (_jsx("div", { className: "portal-tour:absolute portal-tour:w-0 portal-tour:h-0 portal-tour:border-8 portal-tour:border-transparent", style: {
155
271
  bottom: "-16px",
156
272
  left: "50%",
157
273
  transform: "translateX(-50%)",
158
274
  borderColor: "white transparent transparent transparent",
159
- } })), currentStepData.position === "right" && (_jsx("div", { className: "absolute w-0 h-0 border-8 border-transparent", style: {
275
+ } })), tooltipPosition.position === "right" && (_jsx("div", { className: "portal-tour:absolute portal-tour:w-0 portal-tour:h-0 portal-tour:border-8 portal-tour:border-transparent", style: {
160
276
  left: "-16px",
161
277
  top: "50%",
162
278
  transform: "translateY(-50%)",
163
279
  borderColor: "transparent white transparent transparent",
164
- } })), currentStepData.position === "left" && (_jsx("div", { className: "absolute w-0 h-0 border-8 border-transparent", style: {
280
+ } })), tooltipPosition.position === "left" && (_jsx("div", { className: "portal-tour:absolute portal-tour:w-0 portal-tour:h-0 portal-tour:border-8 portal-tour:border-transparent", style: {
165
281
  right: "-16px",
166
282
  top: "50%",
167
283
  transform: "translateY(-50%)",
168
284
  borderColor: "transparent transparent transparent white",
169
285
  } }))] })), _jsx("style", { children: `
170
286
  .tour-highlight {
171
- position: relative;
172
- z-index: 9999 !important;
287
+ position: relative !important;
288
+ z-index: 10000 !important;
173
289
  outline: 3px solid var(--brand-primary, #0056b0) !important;
174
290
  outline-offset: 2px;
175
291
  border-radius: 4px;
292
+ box-shadow: 0 0 0 9999px rgba(0, 0, 0, 0.6) !important;
176
293
  }
177
294
  ` })] }));
178
295
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@elogroup-sereduc/portal-aluno-tour",
3
- "version": "1.0.2",
3
+ "version": "1.0.4",
4
4
  "description": "Componente de tour guiado customizado usando HeroUI para o Portal do Aluno",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -13,7 +13,8 @@
13
13
  "react-dom": "^18.0.0",
14
14
  "@heroui/button": "^2.0.0",
15
15
  "@heroui/system": "^2.0.0",
16
- "react-icons": "^5.0.0"
16
+ "react-icons": "^5.0.0",
17
+ "@elogroup-sereduc/portal-aluno-tailwind-config": "^1.0.0"
17
18
  },
18
19
  "devDependencies": {
19
20
  "@types/react": "^18.0.0",
@@ -15,10 +15,12 @@ export function Tour({
15
15
  onComplete,
16
16
  }: TourProps) {
17
17
  const [currentStep, setCurrentStep] = useState(initialStep);
18
+ const [isVisible, setIsVisible] = useState(false);
18
19
  const [highlightedElement, setHighlightedElement] = useState<HTMLElement | null>(null);
19
- const [tooltipPosition, setTooltipPosition] = useState<{ top: number; left: number } | null>(null);
20
+ const [tooltipPosition, setTooltipPosition] = useState<{ top: number; left: number; position: string } | null>(null);
20
21
  const overlayRef = useRef<HTMLDivElement>(null);
21
22
  const tooltipRef = useRef<HTMLDivElement>(null);
23
+ const isConfiguredRef = useRef(false);
22
24
 
23
25
  const {
24
26
  nextLabel = "Próximo",
@@ -31,56 +33,118 @@ export function Tour({
31
33
  exitOnEsc = true,
32
34
  } = options;
33
35
 
34
- // Garante que o overlay seja renderizado quando enabled for true
35
- useEffect(() => {
36
- if (enabled) {
37
- // Força o body a não ter scroll quando o tour está ativo
38
- document.body.style.overflow = "hidden";
39
- } else {
40
- document.body.style.overflow = "";
41
- }
42
- return () => {
43
- document.body.style.overflow = "";
44
- };
45
- }, [enabled]);
46
-
47
36
  // Calcula a posição da tooltip baseado no elemento destacado
48
37
  const calculateTooltipPosition = useCallback((element: HTMLElement, position: string = "bottom") => {
49
38
  const rect = element.getBoundingClientRect();
50
39
  const scrollTop = window.pageYOffset || document.documentElement.scrollTop;
51
40
  const scrollLeft = window.pageXOffset || document.documentElement.scrollLeft;
41
+ const viewportWidth = window.innerWidth;
42
+ const viewportHeight = window.innerHeight;
43
+
44
+ // Tamanho estimado da tooltip (ajuste conforme necessário)
45
+ const tooltipWidth = 384; // max-w-sm = 384px
46
+ const tooltipHeight = 200; // altura estimada
47
+ const spacing = 16; // espaçamento entre elemento e tooltip
52
48
 
53
49
  let top = 0;
54
50
  let left = 0;
51
+ let finalPosition = position;
55
52
 
53
+ // Calcula posição base
56
54
  switch (position) {
57
55
  case "top":
58
- top = rect.top + scrollTop - 10;
56
+ top = rect.top + scrollTop - spacing;
59
57
  left = rect.left + scrollLeft + rect.width / 2;
60
58
  break;
61
59
  case "bottom":
62
- top = rect.bottom + scrollTop + 10;
60
+ top = rect.bottom + scrollTop + spacing;
63
61
  left = rect.left + scrollLeft + rect.width / 2;
64
62
  break;
65
63
  case "left":
66
64
  top = rect.top + scrollTop + rect.height / 2;
67
- left = rect.left + scrollLeft - 10;
65
+ left = rect.left + scrollLeft - spacing;
68
66
  break;
69
67
  case "right":
70
68
  top = rect.top + scrollTop + rect.height / 2;
71
- left = rect.right + scrollLeft + 10;
69
+ left = rect.right + scrollLeft + spacing;
72
70
  break;
73
71
  default:
74
- top = rect.bottom + scrollTop + 10;
72
+ top = rect.bottom + scrollTop + spacing;
75
73
  left = rect.left + scrollLeft + rect.width / 2;
74
+ finalPosition = "bottom";
75
+ }
76
+
77
+ // Ajusta posição se a tooltip sair da viewport
78
+ if (finalPosition === "bottom" || finalPosition === "top") {
79
+ // Ajusta horizontalmente
80
+ if (left - tooltipWidth / 2 < scrollLeft) {
81
+ left = scrollLeft + tooltipWidth / 2 + 16;
82
+ } else if (left + tooltipWidth / 2 > scrollLeft + viewportWidth) {
83
+ left = scrollLeft + viewportWidth - tooltipWidth / 2 - 16;
84
+ }
85
+
86
+ // Se não couber embaixo, tenta em cima
87
+ if (finalPosition === "bottom" && top + tooltipHeight > scrollTop + viewportHeight) {
88
+ if (rect.top - tooltipHeight > scrollTop) {
89
+ top = rect.top + scrollTop - tooltipHeight - spacing;
90
+ finalPosition = "top";
91
+ }
92
+ }
93
+ // Se não couber em cima, tenta embaixo
94
+ else if (finalPosition === "top" && top - tooltipHeight < scrollTop) {
95
+ if (rect.bottom + tooltipHeight < scrollTop + viewportHeight) {
96
+ top = rect.bottom + scrollTop + spacing;
97
+ finalPosition = "bottom";
98
+ }
99
+ }
100
+ } else if (finalPosition === "left" || finalPosition === "right") {
101
+ // Ajusta verticalmente
102
+ if (top - tooltipHeight / 2 < scrollTop) {
103
+ top = scrollTop + tooltipHeight / 2 + 16;
104
+ } else if (top + tooltipHeight / 2 > scrollTop + viewportHeight) {
105
+ top = scrollTop + viewportHeight - tooltipHeight / 2 - 16;
106
+ }
107
+
108
+ // Se não couber à esquerda, tenta à direita
109
+ if (finalPosition === "left" && left - tooltipWidth < scrollLeft) {
110
+ if (rect.right + tooltipWidth < scrollLeft + viewportWidth) {
111
+ left = rect.right + scrollLeft + spacing;
112
+ finalPosition = "right";
113
+ }
114
+ }
115
+ // Se não couber à direita, tenta à esquerda
116
+ else if (finalPosition === "right" && left + tooltipWidth > scrollLeft + viewportWidth) {
117
+ if (rect.left - tooltipWidth > scrollLeft) {
118
+ left = rect.left + scrollLeft - spacing;
119
+ finalPosition = "left";
120
+ }
121
+ }
76
122
  }
77
123
 
78
- return { top, left };
124
+ return { top, left, position: finalPosition };
79
125
  }, []);
80
126
 
127
+ // Configura o tour quando steps ou options mudam
128
+ const configureTour = useCallback(() => {
129
+ isConfiguredRef.current = true;
130
+ }, []);
131
+
132
+ // Renderiza os steps (mostra/esconde o tour)
133
+ const renderSteps = useCallback(() => {
134
+ if (enabled && steps.length > 0 && !isVisible) {
135
+ setIsVisible(true);
136
+ setCurrentStep(initialStep);
137
+ } else if (!enabled && isVisible) {
138
+ setIsVisible(false);
139
+ setCurrentStep(initialStep);
140
+ setHighlightedElement(null);
141
+ setTooltipPosition(null);
142
+ }
143
+ }, [enabled, steps.length, isVisible, initialStep]);
144
+
81
145
  // Destaca o elemento atual
82
146
  useEffect(() => {
83
- if (!enabled || currentStep < 0 || currentStep >= steps.length) {
147
+ if (!isVisible || currentStep < 0 || currentStep >= steps.length) {
84
148
  setHighlightedElement(null);
85
149
  setTooltipPosition(null);
86
150
  return;
@@ -101,10 +165,10 @@ export function Tour({
101
165
 
102
166
  // Calcula posição da tooltip após um pequeno delay para garantir que o scroll terminou
103
167
  setTimeout(() => {
104
- const position = calculateTooltipPosition(element, step.position);
168
+ const position = calculateTooltipPosition(element, step.position || "bottom");
105
169
  setTooltipPosition(position);
106
170
  }, 300);
107
- }, [enabled, currentStep, steps, calculateTooltipPosition]);
171
+ }, [isVisible, currentStep, steps, calculateTooltipPosition]);
108
172
 
109
173
  // Adiciona overlay e highlight ao elemento
110
174
  useEffect(() => {
@@ -112,6 +176,7 @@ export function Tour({
112
176
  // Remove highlights anteriores
113
177
  document.querySelectorAll(".tour-highlight").forEach((el) => {
114
178
  el.classList.remove("tour-highlight");
179
+ (el as HTMLElement).style.zIndex = "";
115
180
  });
116
181
  return;
117
182
  }
@@ -127,9 +192,22 @@ export function Tour({
127
192
  };
128
193
  }, [highlightedElement]);
129
194
 
130
- // Handler de teclado (ESC)
195
+ // Configura o tour quando necessário
196
+ useEffect(() => {
197
+ if (!isConfiguredRef.current || steps.length > 0) {
198
+ configureTour();
199
+ renderSteps();
200
+ }
201
+ }, [steps, configureTour, renderSteps]);
202
+
203
+ // Atualiza quando enabled muda
204
+ useEffect(() => {
205
+ renderSteps();
206
+ }, [enabled, renderSteps]);
207
+
208
+ // Handler de teclado (ESC e setas)
131
209
  useEffect(() => {
132
- if (!enabled || !exitOnEsc) return;
210
+ if (!isVisible || !exitOnEsc) return;
133
211
 
134
212
  const handleKeyDown = (e: KeyboardEvent) => {
135
213
  if (e.key === "Escape") {
@@ -143,7 +221,19 @@ export function Tour({
143
221
 
144
222
  window.addEventListener("keydown", handleKeyDown);
145
223
  return () => window.removeEventListener("keydown", handleKeyDown);
146
- }, [enabled, exitOnEsc, currentStep, steps.length]);
224
+ }, [isVisible, exitOnEsc, currentStep, steps.length]);
225
+
226
+ // Controla overflow do body
227
+ useEffect(() => {
228
+ if (isVisible) {
229
+ document.body.style.overflow = "hidden";
230
+ } else {
231
+ document.body.style.overflow = "";
232
+ }
233
+ return () => {
234
+ document.body.style.overflow = "";
235
+ };
236
+ }, [isVisible]);
147
237
 
148
238
  const handleNext = useCallback(() => {
149
239
  if (currentStep < steps.length - 1) {
@@ -169,6 +259,7 @@ export function Tour({
169
259
  }, [onComplete]);
170
260
 
171
261
  const handleExit = useCallback(() => {
262
+ setIsVisible(false);
172
263
  setCurrentStep(initialStep);
173
264
  setHighlightedElement(null);
174
265
  setTooltipPosition(null);
@@ -184,7 +275,7 @@ export function Tour({
184
275
  [exitOnOverlayClick, handleExit]
185
276
  );
186
277
 
187
- if (!enabled) {
278
+ if (!isVisible) {
188
279
  return null;
189
280
  }
190
281
 
@@ -195,11 +286,11 @@ export function Tour({
195
286
 
196
287
  return (
197
288
  <>
198
- {/* Overlay escuro - sempre visível quando tour está ativo */}
289
+ {/* Overlay escuro */}
199
290
  <div
200
291
  ref={overlayRef}
201
292
  onClick={handleOverlayClick}
202
- style={{
293
+ style={{
203
294
  position: "fixed",
204
295
  top: 0,
205
296
  left: 0,
@@ -215,50 +306,53 @@ export function Tour({
215
306
  {tooltipPosition && currentStepData && highlightedElement && (
216
307
  <div
217
308
  ref={tooltipRef}
218
- className="max-w-sm"
309
+ className="portal-tour:max-w-sm"
219
310
  style={{
220
311
  position: "fixed",
221
- top: currentStepData.position === "bottom" ? `${tooltipPosition.top}px` : undefined,
222
- bottom: currentStepData.position === "top" ? `${window.innerHeight - tooltipPosition.top}px` : undefined,
223
- left: currentStepData.position === "left" || currentStepData.position === "right"
224
- ? `${tooltipPosition.left}px`
312
+ top: tooltipPosition.position === "bottom" || tooltipPosition.position === "left" || tooltipPosition.position === "right"
313
+ ? `${tooltipPosition.top}px`
314
+ : undefined,
315
+ bottom: tooltipPosition.position === "top" ? `${window.innerHeight - tooltipPosition.top}px` : undefined,
316
+ left: tooltipPosition.position === "left" || tooltipPosition.position === "right"
317
+ ? `${tooltipPosition.left}px`
225
318
  : `${tooltipPosition.left}px`,
226
- transform: currentStepData.position === "left" || currentStepData.position === "right"
227
- ? "translate(0, -50%)"
228
- : "translate(-50%, 0)",
319
+ transform:
320
+ tooltipPosition.position === "left" || tooltipPosition.position === "right"
321
+ ? "translate(0, -50%)"
322
+ : "translate(-50%, 0)",
229
323
  zIndex: 10001,
230
324
  }}
231
325
  >
232
- <div className="bg-white rounded-lg shadow-xl p-6 relative">
326
+ <div className="portal-tour:bg-white portal-tour:rounded-lg portal-tour:shadow-xl portal-tour:p-6 portal-tour:relative">
233
327
  {/* Botão fechar */}
234
328
  <button
235
329
  onClick={handleExit}
236
- className="absolute top-2 right-2 p-1 rounded-full hover:bg-gray-100 transition-colors"
330
+ className="portal-tour:absolute portal-tour:top-2 portal-tour:right-2 portal-tour:p-1 portal-tour:rounded-full portal-tour:hover:bg-gray-100 portal-tour:transition-colors portal-tour:z-10"
237
331
  aria-label="Fechar tour"
238
332
  >
239
- <HiXMark className="w-5 h-5 text-gray-500" />
333
+ <HiXMark className="portal-tour:w-5 portal-tour:h-5 portal-tour:text-gray-500" />
240
334
  </button>
241
335
 
242
336
  {/* Título (se fornecido) */}
243
337
  {currentStepData.title && (
244
- <h3 className="text-lg font-semibold text-gray-900 mb-2 pr-6">
338
+ <h3 className="portal-tour:text-lg portal-tour:font-semibold portal-tour:text-gray-900 portal-tour:mb-2 portal-tour:pr-6">
245
339
  {currentStepData.title}
246
340
  </h3>
247
341
  )}
248
342
 
249
343
  {/* Conteúdo */}
250
- <p className="text-gray-700 mb-4">{currentStepData.intro}</p>
344
+ <p className="portal-tour:text-gray-700 portal-tour:mb-4">{currentStepData.intro}</p>
251
345
 
252
346
  {/* Progresso */}
253
347
  {showProgress && (
254
- <div className="mb-4">
255
- <div className="w-full bg-gray-200 rounded-full h-2">
348
+ <div className="portal-tour:mb-4">
349
+ <div className="portal-tour:w-full portal-tour:bg-gray-200 portal-tour:rounded-full portal-tour:h-2">
256
350
  <div
257
- className="bg-brand-primary h-2 rounded-full transition-all duration-300"
351
+ className="portal-tour:bg-brand-primary portal-tour:h-2 portal-tour:rounded-full portal-tour:transition-all portal-tour:duration-300"
258
352
  style={{ width: `${progress}%` }}
259
353
  />
260
354
  </div>
261
- <p className="text-xs text-gray-500 mt-1 text-center">
355
+ <p className="portal-tour:text-xs portal-tour:text-gray-500 portal-tour:mt-1 portal-tour:text-center">
262
356
  {currentStep + 1} de {steps.length}
263
357
  </p>
264
358
  </div>
@@ -266,15 +360,15 @@ export function Tour({
266
360
 
267
361
  {/* Bullets */}
268
362
  {showBullets && (
269
- <div className="flex justify-center gap-1 mb-4">
363
+ <div className="portal-tour:flex portal-tour:justify-center portal-tour:gap-1 portal-tour:mb-4">
270
364
  {steps.map((_, index) => (
271
365
  <button
272
366
  key={index}
273
367
  onClick={() => setCurrentStep(index)}
274
- className={`w-2 h-2 rounded-full transition-all ${
368
+ className={`portal-tour:w-2 portal-tour:h-2 portal-tour:rounded-full portal-tour:transition-all ${
275
369
  index === currentStep
276
- ? "bg-brand-primary w-6"
277
- : "bg-gray-300 hover:bg-gray-400"
370
+ ? "portal-tour:bg-brand-primary portal-tour:w-6"
371
+ : "portal-tour:bg-gray-300 portal-tour:hover:bg-gray-400"
278
372
  }`}
279
373
  aria-label={`Ir para passo ${index + 1}`}
280
374
  />
@@ -283,27 +377,27 @@ export function Tour({
283
377
  )}
284
378
 
285
379
  {/* Botões de navegação */}
286
- <div className="flex justify-between items-center gap-2">
287
- <div className="flex gap-2">
380
+ <div className="portal-tour:flex portal-tour:justify-between portal-tour:items-center portal-tour:gap-2">
381
+ <div className="portal-tour:flex portal-tour:gap-2">
288
382
  {!isFirstStep && (
289
383
  <Button
290
384
  variant="bordered"
291
385
  onPress={handlePrev}
292
- startContent={<HiChevronLeft className="w-4 h-4" />}
386
+ startContent={<HiChevronLeft className="portal-tour:w-4 portal-tour:h-4" />}
293
387
  >
294
388
  {prevLabel}
295
389
  </Button>
296
390
  )}
297
391
  </div>
298
392
 
299
- <div className="flex gap-2">
393
+ <div className="portal-tour:flex portal-tour:gap-2">
300
394
  <Button variant="light" onPress={handleSkip}>
301
395
  {skipLabel}
302
396
  </Button>
303
397
  <Button
304
398
  color="primary"
305
399
  onPress={isLastStep ? handleComplete : handleNext}
306
- endContent={!isLastStep ? <HiChevronRight className="w-4 h-4" /> : undefined}
400
+ endContent={!isLastStep ? <HiChevronRight className="portal-tour:w-4 portal-tour:h-4" /> : undefined}
307
401
  >
308
402
  {isLastStep ? doneLabel : nextLabel}
309
403
  </Button>
@@ -312,9 +406,9 @@ export function Tour({
312
406
  </div>
313
407
 
314
408
  {/* Seta indicadora */}
315
- {currentStepData.position === "bottom" && (
409
+ {tooltipPosition.position === "bottom" && (
316
410
  <div
317
- className="absolute w-0 h-0 border-8 border-transparent"
411
+ className="portal-tour:absolute portal-tour:w-0 portal-tour:h-0 portal-tour:border-8 portal-tour:border-transparent"
318
412
  style={{
319
413
  top: "-16px",
320
414
  left: "50%",
@@ -323,9 +417,9 @@ export function Tour({
323
417
  }}
324
418
  />
325
419
  )}
326
- {currentStepData.position === "top" && (
420
+ {tooltipPosition.position === "top" && (
327
421
  <div
328
- className="absolute w-0 h-0 border-8 border-transparent"
422
+ className="portal-tour:absolute portal-tour:w-0 portal-tour:h-0 portal-tour:border-8 portal-tour:border-transparent"
329
423
  style={{
330
424
  bottom: "-16px",
331
425
  left: "50%",
@@ -334,9 +428,9 @@ export function Tour({
334
428
  }}
335
429
  />
336
430
  )}
337
- {currentStepData.position === "right" && (
431
+ {tooltipPosition.position === "right" && (
338
432
  <div
339
- className="absolute w-0 h-0 border-8 border-transparent"
433
+ className="portal-tour:absolute portal-tour:w-0 portal-tour:h-0 portal-tour:border-8 portal-tour:border-transparent"
340
434
  style={{
341
435
  left: "-16px",
342
436
  top: "50%",
@@ -345,9 +439,9 @@ export function Tour({
345
439
  }}
346
440
  />
347
441
  )}
348
- {currentStepData.position === "left" && (
442
+ {tooltipPosition.position === "left" && (
349
443
  <div
350
- className="absolute w-0 h-0 border-8 border-transparent"
444
+ className="portal-tour:absolute portal-tour:w-0 portal-tour:h-0 portal-tour:border-8 portal-tour:border-transparent"
351
445
  style={{
352
446
  right: "-16px",
353
447
  top: "50%",
@@ -373,4 +467,3 @@ export function Tour({
373
467
  </>
374
468
  );
375
469
  }
376
-
@@ -0,0 +1,8 @@
1
+ /** @type {import('tailwindcss').Config} */
2
+ const shared = require("@elogroup-sereduc/portal-aluno-tailwind-config");
3
+
4
+ module.exports = {
5
+ ...shared,
6
+ prefix: "portal-tour",
7
+ };
8
+