Fix multiple onboarding and navigation issues
**1. Remove duplicate navigation buttons in SetupWizard** - Removed external navigation footer from SetupWizard (lines 370-383) - All setup wizard steps now have only internal navigation buttons - Prevents confusion with double Continue buttons in onboarding **2. Fix quality template API call failure** - Fixed userId validation in QualitySetupStep:17 - Changed from defaulting to empty string to undefined - Added validation check before API call to prevent UUID errors - Disabled submit button when userId not available - Added error message display for missing user Related: frontend/src/components/domain/setup-wizard/steps/QualitySetupStep.tsx:17,51-54,376 **3. Delete regular tours implementation (keep demo tour)** Removed custom tours system while preserving demo tour functionality: - Deleted TourContext.tsx and TourProvider - Deleted Tour UI components folder - Deleted tours/tours.ts definitions - Deleted tour.json translations - Removed TourProvider from App.tsx - Removed TourButton from Sidebar Demo tour (useDemoTour, driver.js) remains intact and functional. Files deleted: - frontend/src/contexts/TourContext.tsx - frontend/src/components/ui/Tour/* (all files) - frontend/src/tours/tours.ts - frontend/src/locales/es/tour.json **4. Issues verified/confirmed:** - Quality type select UI already working (callback setState pattern) - Inventory lots UI confirmed present in InventorySetupStep:683,788,833 - Lots UI visible after adding ingredients in onboarding flow **Build Status:** ✓ All changes verified, build successful in 21.95s
This commit is contained in:
@@ -366,21 +366,6 @@ export const SetupWizard: React.FC = () => {
|
||||
canContinue={canContinue}
|
||||
/>
|
||||
</CardBody>
|
||||
|
||||
{/* Navigation Footer */}
|
||||
<div className="border-t border-[var(--border-primary)] px-6 py-4 bg-[var(--bg-secondary)]/30">
|
||||
<StepNavigation
|
||||
currentStep={currentStep}
|
||||
currentStepIndex={currentStepIndex}
|
||||
totalSteps={SETUP_STEPS.length}
|
||||
canContinue={canContinue}
|
||||
onPrevious={handlePrevious}
|
||||
onNext={handleNext}
|
||||
onSkip={handleSkip}
|
||||
onComplete={handleStepComplete}
|
||||
isLoading={markStepCompleted.isPending}
|
||||
/>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -14,7 +14,7 @@ export const QualitySetupStep: React.FC<SetupStepProps> = ({ onUpdate, onComplet
|
||||
const currentTenant = useCurrentTenant();
|
||||
const user = useAuthUser();
|
||||
const tenantId = currentTenant?.id || user?.tenant_id || '';
|
||||
const userId = user?.id || '';
|
||||
const userId = user?.id; // Keep undefined if not available - backend requires valid UUID
|
||||
|
||||
// Fetch quality templates
|
||||
const { data: templatesData, isLoading } = useQualityTemplates(tenantId);
|
||||
@@ -48,6 +48,12 @@ export const QualitySetupStep: React.FC<SetupStepProps> = ({ onUpdate, onComplet
|
||||
const validateForm = (): boolean => {
|
||||
const newErrors: Record<string, string> = {};
|
||||
|
||||
if (!userId) {
|
||||
newErrors.form = t('common:error_loading_user', 'User not loaded. Please wait or refresh the page.');
|
||||
setErrors(newErrors);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!formData.name.trim()) {
|
||||
newErrors.name = t('setup_wizard:quality.errors.name_required', 'Name is required');
|
||||
}
|
||||
@@ -358,10 +364,16 @@ export const QualitySetupStep: React.FC<SetupStepProps> = ({ onUpdate, onComplet
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{errors.form && (
|
||||
<div className="p-3 bg-[var(--color-error)]/10 border border-[var(--color-error)]/20 rounded-lg text-sm text-[var(--color-error)]">
|
||||
{errors.form}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex gap-2 pt-2">
|
||||
<button
|
||||
type="submit"
|
||||
disabled={createTemplateMutation.isPending}
|
||||
disabled={createTemplateMutation.isPending || !userId}
|
||||
className="px-4 py-2 bg-[var(--color-primary)] text-white rounded-lg hover:bg-[var(--color-primary-dark)] disabled:opacity-50 disabled:cursor-not-allowed transition-colors font-medium"
|
||||
>
|
||||
{createTemplateMutation.isPending ? (
|
||||
|
||||
@@ -12,7 +12,6 @@ import { Button } from '../../ui';
|
||||
import { Badge } from '../../ui';
|
||||
import { Tooltip } from '../../ui';
|
||||
import { Avatar } from '../../ui';
|
||||
import { TourButton } from '../../ui/Tour/TourButton';
|
||||
import {
|
||||
LayoutDashboard,
|
||||
Package,
|
||||
@@ -597,16 +596,6 @@ export const Sidebar = forwardRef<SidebarRef, SidebarProps>(({
|
||||
const isHovered = hoveredItem === item.id;
|
||||
const ItemIcon = item.icon;
|
||||
|
||||
// Add tour data attributes for main navigation sections
|
||||
const getTourAttribute = (itemPath: string) => {
|
||||
if (itemPath === '/app/database') return 'sidebar-database';
|
||||
if (itemPath === '/app/operations') return 'sidebar-operations';
|
||||
if (itemPath === '/app/analytics') return 'sidebar-analytics';
|
||||
return undefined;
|
||||
};
|
||||
|
||||
const tourAttr = getTourAttribute(item.path);
|
||||
|
||||
const itemContent = (
|
||||
<div
|
||||
className={clsx(
|
||||
@@ -726,7 +715,7 @@ export const Sidebar = forwardRef<SidebarRef, SidebarProps>(({
|
||||
);
|
||||
|
||||
return (
|
||||
<li key={item.id} className="relative" data-tour={tourAttr}>
|
||||
<li key={item.id} className="relative">
|
||||
{isCollapsed && !hasChildren && ItemIcon ? (
|
||||
<Tooltip content={item.label} side="right">
|
||||
{button}
|
||||
@@ -812,13 +801,6 @@ export const Sidebar = forwardRef<SidebarRef, SidebarProps>(({
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Tour Button */}
|
||||
{!isCollapsed && (
|
||||
<div className="px-4 pb-2">
|
||||
<TourButton variant="button" />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Navigation */}
|
||||
<nav className={clsx('flex-1 overflow-y-auto overflow-x-hidden', isCollapsed ? 'px-1 py-4' : 'p-4')}>
|
||||
<ul className={clsx(isCollapsed ? 'space-y-1 flex flex-col items-center' : 'space-y-2')}>
|
||||
|
||||
@@ -1,49 +0,0 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import { createPortal } from 'react-dom';
|
||||
import { useTour } from '../../../contexts/TourContext';
|
||||
import TourTooltip from './TourTooltip';
|
||||
import TourSpotlight from './TourSpotlight';
|
||||
|
||||
export const Tour: React.FC = () => {
|
||||
const { state, nextStep, previousStep, skipTour, completeTour } = useTour();
|
||||
|
||||
// Disable body scroll when tour is active
|
||||
useEffect(() => {
|
||||
if (state.isActive) {
|
||||
const originalOverflow = document.body.style.overflow;
|
||||
document.body.style.overflow = 'hidden';
|
||||
|
||||
return () => {
|
||||
document.body.style.overflow = originalOverflow;
|
||||
};
|
||||
}
|
||||
}, [state.isActive]);
|
||||
|
||||
if (!state.isActive || !state.currentTour) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const currentStep = state.currentTour.steps[state.currentStepIndex];
|
||||
if (!currentStep) return null;
|
||||
|
||||
return createPortal(
|
||||
<>
|
||||
{/* Spotlight highlight */}
|
||||
<TourSpotlight target={currentStep.target} padding={8} />
|
||||
|
||||
{/* Tooltip with content and navigation */}
|
||||
<TourTooltip
|
||||
step={currentStep}
|
||||
currentStep={state.currentStepIndex}
|
||||
totalSteps={state.currentTour.steps.length}
|
||||
onNext={nextStep}
|
||||
onPrevious={previousStep}
|
||||
onSkip={skipTour}
|
||||
onComplete={completeTour}
|
||||
/>
|
||||
</>,
|
||||
document.body
|
||||
);
|
||||
};
|
||||
|
||||
export default Tour;
|
||||
@@ -1,164 +0,0 @@
|
||||
import React, { useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { HelpCircle, Play, CheckCircle } from 'lucide-react';
|
||||
import Button from '../Button/Button';
|
||||
import { useTour } from '../../../contexts/TourContext';
|
||||
import { allTours } from '../../../tours/tours';
|
||||
|
||||
export interface TourButtonProps {
|
||||
tourId?: keyof typeof allTours;
|
||||
variant?: 'icon' | 'button' | 'menu';
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export const TourButton: React.FC<TourButtonProps> = ({
|
||||
tourId,
|
||||
variant = 'button',
|
||||
className = '',
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const { startTour, isTourCompleted } = useTour();
|
||||
const [showMenu, setShowMenu] = useState(false);
|
||||
|
||||
const handleStartTour = (id: keyof typeof allTours) => {
|
||||
const tour = allTours[id];
|
||||
if (tour) {
|
||||
startTour(tour);
|
||||
setShowMenu(false);
|
||||
}
|
||||
};
|
||||
|
||||
// Icon variant - just a help icon that opens menu
|
||||
if (variant === 'icon') {
|
||||
return (
|
||||
<div className="relative">
|
||||
<button
|
||||
onClick={() => setShowMenu(!showMenu)}
|
||||
className={`p-2 rounded-lg hover:bg-gray-100 transition-colors ${className}`}
|
||||
aria-label={t('tour:trigger.button', 'Tours Disponibles')}
|
||||
>
|
||||
<HelpCircle className="w-5 h-5 text-gray-600" />
|
||||
</button>
|
||||
|
||||
{showMenu && (
|
||||
<>
|
||||
<div
|
||||
className="fixed inset-0 z-40"
|
||||
onClick={() => setShowMenu(false)}
|
||||
/>
|
||||
<div className="absolute right-0 top-full mt-2 w-72 bg-white rounded-lg shadow-xl border border-gray-200 z-50 animate-scale-in">
|
||||
<div className="p-4 border-b border-gray-200">
|
||||
<h3 className="font-semibold text-gray-900">
|
||||
{t('tour:trigger.button', 'Tours Disponibles')}
|
||||
</h3>
|
||||
<p className="text-sm text-gray-600 mt-1">
|
||||
{t('tour:trigger.tooltip', '¿Necesitas ayuda? Inicia un tour guiado')}
|
||||
</p>
|
||||
</div>
|
||||
<div className="p-2">
|
||||
{Object.entries(allTours).map(([key, tour]) => (
|
||||
<button
|
||||
key={tour.id}
|
||||
onClick={() => handleStartTour(key as keyof typeof allTours)}
|
||||
className="w-full flex items-center justify-between p-3 rounded-lg hover:bg-gray-50 transition-colors text-left"
|
||||
>
|
||||
<div className="flex-1">
|
||||
<div className="font-medium text-gray-900">{tour.name}</div>
|
||||
<div className="text-xs text-gray-500 mt-0.5">
|
||||
{tour.steps.length} {t('common:steps', 'pasos')}
|
||||
</div>
|
||||
</div>
|
||||
{isTourCompleted(tour.id) ? (
|
||||
<CheckCircle className="w-5 h-5 text-green-500 flex-shrink-0" />
|
||||
) : (
|
||||
<Play className="w-5 h-5 text-primary-500 flex-shrink-0" />
|
||||
)}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Single tour button variant
|
||||
if (tourId) {
|
||||
const tour = allTours[tourId];
|
||||
if (!tour) return null;
|
||||
|
||||
const isCompleted = isTourCompleted(tour.id);
|
||||
|
||||
return (
|
||||
<Button
|
||||
onClick={() => handleStartTour(tourId)}
|
||||
variant={isCompleted ? 'outline' : 'primary'}
|
||||
size="sm"
|
||||
leftIcon={isCompleted ? <CheckCircle /> : <Play />}
|
||||
className={className}
|
||||
>
|
||||
{isCompleted
|
||||
? t('tour:restart_tour', 'Reiniciar tour')
|
||||
: t('tour:start_tour', 'Iniciar tour')}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
// Default button that shows menu
|
||||
return (
|
||||
<div className="relative">
|
||||
<Button
|
||||
onClick={() => setShowMenu(!showMenu)}
|
||||
variant="outline"
|
||||
size="sm"
|
||||
leftIcon={<HelpCircle />}
|
||||
className={className}
|
||||
>
|
||||
{t('tour:trigger.button', 'Tours Disponibles')}
|
||||
</Button>
|
||||
|
||||
{showMenu && (
|
||||
<>
|
||||
<div
|
||||
className="fixed inset-0 z-40"
|
||||
onClick={() => setShowMenu(false)}
|
||||
/>
|
||||
<div className="absolute right-0 top-full mt-2 w-72 bg-white rounded-lg shadow-xl border border-gray-200 z-50 animate-scale-in">
|
||||
<div className="p-4 border-b border-gray-200">
|
||||
<h3 className="font-semibold text-gray-900">
|
||||
{t('tour:trigger.button', 'Tours Disponibles')}
|
||||
</h3>
|
||||
<p className="text-sm text-gray-600 mt-1">
|
||||
{t('tour:trigger.tooltip', '¿Necesitas ayuda? Inicia un tour guiado')}
|
||||
</p>
|
||||
</div>
|
||||
<div className="p-2">
|
||||
{Object.entries(allTours).map(([key, tour]) => (
|
||||
<button
|
||||
key={tour.id}
|
||||
onClick={() => handleStartTour(key as keyof typeof allTours)}
|
||||
className="w-full flex items-center justify-between p-3 rounded-lg hover:bg-gray-50 transition-colors text-left"
|
||||
>
|
||||
<div className="flex-1">
|
||||
<div className="font-medium text-gray-900">{tour.name}</div>
|
||||
<div className="text-xs text-gray-500 mt-0.5">
|
||||
{tour.steps.length} {t('common:steps', 'pasos')}
|
||||
</div>
|
||||
</div>
|
||||
{isTourCompleted(tour.id) ? (
|
||||
<CheckCircle className="w-5 h-5 text-green-500 flex-shrink-0" />
|
||||
) : (
|
||||
<Play className="w-5 h-5 text-primary-500 flex-shrink-0" />
|
||||
)}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default TourButton;
|
||||
@@ -1,91 +0,0 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
|
||||
export interface TourSpotlightProps {
|
||||
target: string; // CSS selector
|
||||
padding?: number;
|
||||
}
|
||||
|
||||
export const TourSpotlight: React.FC<TourSpotlightProps> = ({ target, padding = 8 }) => {
|
||||
const [rect, setRect] = useState<DOMRect | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const updateRect = () => {
|
||||
const element = document.querySelector(target);
|
||||
if (element) {
|
||||
const elementRect = element.getBoundingClientRect();
|
||||
setRect(elementRect);
|
||||
|
||||
// Scroll element into view if not fully visible
|
||||
element.scrollIntoView({
|
||||
behavior: 'smooth',
|
||||
block: 'center',
|
||||
inline: 'center',
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
updateRect();
|
||||
|
||||
// Update on resize or scroll
|
||||
window.addEventListener('resize', updateRect);
|
||||
window.addEventListener('scroll', updateRect, true);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('resize', updateRect);
|
||||
window.removeEventListener('scroll', updateRect, true);
|
||||
};
|
||||
}, [target]);
|
||||
|
||||
if (!rect) return null;
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* Overlay with cutout */}
|
||||
<svg
|
||||
className="fixed inset-0 z-[10000] pointer-events-none"
|
||||
style={{ width: '100vw', height: '100vh' }}
|
||||
>
|
||||
<defs>
|
||||
<mask id="tour-spotlight-mask">
|
||||
{/* White background */}
|
||||
<rect x="0" y="0" width="100%" height="100%" fill="white" />
|
||||
{/* Black cutout for target element */}
|
||||
<rect
|
||||
x={rect.left - padding}
|
||||
y={rect.top - padding}
|
||||
width={rect.width + padding * 2}
|
||||
height={rect.height + padding * 2}
|
||||
rx="8"
|
||||
fill="black"
|
||||
/>
|
||||
</mask>
|
||||
</defs>
|
||||
|
||||
{/* Semi-transparent overlay with mask */}
|
||||
<rect
|
||||
x="0"
|
||||
y="0"
|
||||
width="100%"
|
||||
height="100%"
|
||||
fill="rgba(0, 0, 0, 0.7)"
|
||||
mask="url(#tour-spotlight-mask)"
|
||||
className="animate-fade-in"
|
||||
/>
|
||||
</svg>
|
||||
|
||||
{/* Highlighted border around target */}
|
||||
<div
|
||||
className="fixed z-[10000] pointer-events-none rounded-lg animate-pulse"
|
||||
style={{
|
||||
top: `${rect.top - padding}px`,
|
||||
left: `${rect.left - padding}px`,
|
||||
width: `${rect.width + padding * 2}px`,
|
||||
height: `${rect.height + padding * 2}px`,
|
||||
boxShadow: '0 0 0 2px rgba(217, 119, 6, 0.8), 0 0 20px rgba(217, 119, 6, 0.3)',
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default TourSpotlight;
|
||||
@@ -1,206 +0,0 @@
|
||||
import React, { useEffect, useState, useRef } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { X, ArrowLeft, ArrowRight, Check } from 'lucide-react';
|
||||
import Button from '../Button/Button';
|
||||
import { TourStep } from '../../../contexts/TourContext';
|
||||
|
||||
export interface TourTooltipProps {
|
||||
step: TourStep;
|
||||
currentStep: number;
|
||||
totalSteps: number;
|
||||
onNext: () => void;
|
||||
onPrevious: () => void;
|
||||
onSkip: () => void;
|
||||
onComplete: () => void;
|
||||
}
|
||||
|
||||
export const TourTooltip: React.FC<TourTooltipProps> = ({
|
||||
step,
|
||||
currentStep,
|
||||
totalSteps,
|
||||
onNext,
|
||||
onPrevious,
|
||||
onSkip,
|
||||
onComplete,
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const [position, setPosition] = useState({ top: 0, left: 0 });
|
||||
const [placement, setPlacement] = useState(step.placement || 'bottom');
|
||||
const tooltipRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const calculatePosition = () => {
|
||||
const target = document.querySelector(step.target);
|
||||
if (!target || !tooltipRef.current) return;
|
||||
|
||||
const targetRect = target.getBoundingClientRect();
|
||||
const tooltipRect = tooltipRef.current.getBoundingClientRect();
|
||||
const viewportWidth = window.innerWidth;
|
||||
const viewportHeight = window.innerHeight;
|
||||
|
||||
let top = 0;
|
||||
let left = 0;
|
||||
let finalPlacement = step.placement || 'bottom';
|
||||
|
||||
// Calculate initial position based on placement
|
||||
switch (step.placement || 'bottom') {
|
||||
case 'top':
|
||||
top = targetRect.top - tooltipRect.height - 16;
|
||||
left = targetRect.left + (targetRect.width - tooltipRect.width) / 2;
|
||||
break;
|
||||
case 'bottom':
|
||||
top = targetRect.bottom + 16;
|
||||
left = targetRect.left + (targetRect.width - tooltipRect.width) / 2;
|
||||
break;
|
||||
case 'left':
|
||||
top = targetRect.top + (targetRect.height - tooltipRect.height) / 2;
|
||||
left = targetRect.left - tooltipRect.width - 16;
|
||||
break;
|
||||
case 'right':
|
||||
top = targetRect.top + (targetRect.height - tooltipRect.height) / 2;
|
||||
left = targetRect.right + 16;
|
||||
break;
|
||||
}
|
||||
|
||||
// Adjust if tooltip goes off screen
|
||||
if (left < 16) {
|
||||
left = 16;
|
||||
} else if (left + tooltipRect.width > viewportWidth - 16) {
|
||||
left = viewportWidth - tooltipRect.width - 16;
|
||||
}
|
||||
|
||||
if (top < 16) {
|
||||
top = 16;
|
||||
finalPlacement = 'bottom';
|
||||
} else if (top + tooltipRect.height > viewportHeight - 16) {
|
||||
top = viewportHeight - tooltipRect.height - 16;
|
||||
finalPlacement = 'top';
|
||||
}
|
||||
|
||||
setPosition({ top, left });
|
||||
setPlacement(finalPlacement);
|
||||
};
|
||||
|
||||
// Calculate position on mount and when step changes
|
||||
calculatePosition();
|
||||
|
||||
// Recalculate on window resize or scroll
|
||||
window.addEventListener('resize', calculatePosition);
|
||||
window.addEventListener('scroll', calculatePosition, true);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('resize', calculatePosition);
|
||||
window.removeEventListener('scroll', calculatePosition, true);
|
||||
};
|
||||
}, [step]);
|
||||
|
||||
const isFirstStep = currentStep === 0;
|
||||
const isLastStep = currentStep === totalSteps - 1;
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={tooltipRef}
|
||||
className="fixed z-[10001] bg-white rounded-lg shadow-2xl border border-gray-200 max-w-md animate-scale-in"
|
||||
style={{
|
||||
top: `${position.top}px`,
|
||||
left: `${position.left}px`,
|
||||
}}
|
||||
>
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between p-4 border-b border-gray-200">
|
||||
<div className="flex-1">
|
||||
<h3 className="text-lg font-semibold text-gray-900">{step.title}</h3>
|
||||
<p className="text-xs text-gray-500 mt-1">
|
||||
{t('tour:step_progress', 'Paso {{current}} de {{total}}', {
|
||||
current: currentStep + 1,
|
||||
total: totalSteps,
|
||||
})}
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={onSkip}
|
||||
className="ml-4 text-gray-400 hover:text-gray-600 transition-colors"
|
||||
aria-label={t('tour:close', 'Cerrar')}
|
||||
>
|
||||
<X className="w-5 h-5" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<div className="p-4">
|
||||
<p className="text-gray-700 leading-relaxed">{step.content}</p>
|
||||
|
||||
{/* Action button if provided */}
|
||||
{step.action && (
|
||||
<div className="mt-4">
|
||||
<Button
|
||||
onClick={step.action.onClick}
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="w-full"
|
||||
>
|
||||
{step.action.label}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Footer */}
|
||||
<div className="flex items-center justify-between p-4 border-t border-gray-200 bg-gray-50">
|
||||
{/* Progress dots */}
|
||||
<div className="flex items-center gap-1.5">
|
||||
{Array.from({ length: totalSteps }).map((_, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className={`w-2 h-2 rounded-full transition-all ${
|
||||
index === currentStep
|
||||
? 'bg-primary-500 w-4'
|
||||
: index < currentStep
|
||||
? 'bg-primary-300'
|
||||
: 'bg-gray-300'
|
||||
}`}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Navigation buttons */}
|
||||
<div className="flex items-center gap-2">
|
||||
{!isFirstStep && (
|
||||
<Button onClick={onPrevious} variant="ghost" size="sm" leftIcon={<ArrowLeft />}>
|
||||
{t('tour:previous', 'Anterior')}
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{isLastStep ? (
|
||||
<Button onClick={onComplete} variant="primary" size="sm" rightIcon={<Check />}>
|
||||
{t('tour:finish', 'Finalizar')}
|
||||
</Button>
|
||||
) : (
|
||||
<Button onClick={onNext} variant="primary" size="sm" rightIcon={<ArrowRight />}>
|
||||
{t('tour:next', 'Siguiente')}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Arrow pointing to target */}
|
||||
<div
|
||||
className={`absolute w-4 h-4 bg-white border-gray-200 transform rotate-45 ${
|
||||
placement === 'top'
|
||||
? 'bottom-[-8px] border-b border-r'
|
||||
: placement === 'bottom'
|
||||
? 'top-[-8px] border-t border-l'
|
||||
: placement === 'left'
|
||||
? 'right-[-8px] border-t border-r'
|
||||
: 'left-[-8px] border-b border-l'
|
||||
}`}
|
||||
style={{
|
||||
[placement === 'top' || placement === 'bottom' ? 'left' : 'top']: '50%',
|
||||
transform: `translate(-50%, 0) rotate(45deg)`,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default TourTooltip;
|
||||
@@ -1,3 +0,0 @@
|
||||
export { Tour, default } from './Tour';
|
||||
export { TourTooltip } from './TourTooltip';
|
||||
export { TourSpotlight } from './TourSpotlight';
|
||||
Reference in New Issue
Block a user