@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 +6 -1
- package/dist/components/Tour.d.ts.map +1 -1
- package/dist/components/Tour.js +144 -27
- package/package.json +3 -2
- package/src/components/Tour.tsx +157 -64
- package/tailwind.config.js +8 -0
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,
|
|
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"}
|
package/dist/components/Tour.js
CHANGED
|
@@ -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 -
|
|
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 +
|
|
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 -
|
|
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 +
|
|
47
|
+
left = rect.right + scrollLeft + spacing;
|
|
38
48
|
break;
|
|
39
49
|
default:
|
|
40
|
-
top = rect.bottom + scrollTop +
|
|
50
|
+
top = rect.bottom + scrollTop + spacing;
|
|
41
51
|
left = rect.left + scrollLeft + rect.width / 2;
|
|
52
|
+
finalPosition = "bottom";
|
|
42
53
|
}
|
|
43
|
-
|
|
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 (!
|
|
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
|
-
}, [
|
|
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
|
-
//
|
|
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 (!
|
|
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
|
-
}, [
|
|
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 (!
|
|
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,
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
left:
|
|
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:
|
|
259
|
+
transform: tooltipPosition.position === "left" || tooltipPosition.position === "right"
|
|
145
260
|
? "translate(0, -50%)"
|
|
146
261
|
: "translate(-50%, 0)",
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
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
|
-
} })),
|
|
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
|
-
} })),
|
|
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
|
-
} })),
|
|
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:
|
|
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.
|
|
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",
|
package/src/components/Tour.tsx
CHANGED
|
@@ -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 -
|
|
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 +
|
|
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 -
|
|
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 +
|
|
69
|
+
left = rect.right + scrollLeft + spacing;
|
|
72
70
|
break;
|
|
73
71
|
default:
|
|
74
|
-
top = rect.bottom + scrollTop +
|
|
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 (!
|
|
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
|
-
}, [
|
|
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
|
-
//
|
|
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 (!
|
|
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
|
-
}, [
|
|
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 (!
|
|
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
|
|
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:
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
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:
|
|
227
|
-
|
|
228
|
-
|
|
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
|
-
{
|
|
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
|
-
{
|
|
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
|
-
{
|
|
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
|
-
{
|
|
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
|
-
|