admin onboarding (#4863)

# Description of Changes

<!--
Please provide a summary of the changes, including:

- What was changed
- Why the change was made
- Any challenges encountered

Closes #(issue_number)
-->

---

## Checklist

### General

- [ ] I have read the [Contribution
Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md)
- [ ] I have read the [Stirling-PDF Developer
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md)
(if applicable)
- [ ] I have read the [How to add new languages to
Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md)
(if applicable)
- [ ] I have performed a self-review of my own code
- [ ] My changes generate no new warnings

### Documentation

- [ ] I have updated relevant docs on [Stirling-PDF's doc
repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/)
(if functionality has heavily changed)
- [ ] I have read the section [Add New Translation
Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags)
(for new translation tags only)

### Translations (if applicable)

- [ ] I ran
[`scripts/counter_translation.py`](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/docs/counter_translation.md)

### UI Changes (if applicable)

- [ ] Screenshots or videos demonstrating the UI changes are attached
(e.g., as comments or direct attachments in the PR)

### Testing (if applicable)

- [ ] I have tested my changes locally. Refer to the [Testing
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing)
for more details.

---------

Co-authored-by: EthanHealy01 <80844253+EthanHealy01@users.noreply.github.com>
This commit is contained in:
Anthony Stirling 2025-11-10 13:47:43 +00:00 committed by GitHub
parent ebf4bab80b
commit 3cf89b6ede
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 431 additions and 19 deletions

View File

@ -3539,7 +3539,13 @@
"account": "Account",
"config": "Config",
"adminSettings": "Admin Settings",
"allTools": "All Tools"
"allTools": "All Tools",
"helpMenu": {
"toolsTour": "Tools Tour",
"toolsTourDesc": "Learn what the tools can do",
"adminTour": "Admin Tour",
"adminTourDesc": "Explore admin settings & features"
}
},
"admin": {
"error": "Error",
@ -4674,6 +4680,17 @@
"startTour": "Start Tour",
"startTourDescription": "Take a guided tour of Stirling PDF's key features"
},
"adminOnboarding": {
"welcome": "Welcome to the <strong>Admin Tour</strong>! Let's explore the powerful enterprise features and settings available to system administrators.",
"configButton": "Click the <strong>Config</strong> button to access all system settings and administrative controls.",
"settingsOverview": "This is the <strong>Settings Panel</strong>. Admin settings are organised by category for easy navigation.",
"teamsAndUsers": "Manage <strong>Teams</strong> and individual users here. You can invite new users via email, shareable links, or create custom accounts for them yourself.",
"systemCustomization": "We have extensive ways to customise the UI: <strong>System Settings</strong> let you change the app name and languages, <strong>Features</strong> allows server certificate management, and <strong>Endpoints</strong> lets you enable or disable specific tools for your users.",
"databaseSection": "For advanced production environments, we have settings to allow <strong>external database hookups</strong> so you can integrate with your existing infrastructure.",
"connectionsSection": "The <strong>Connections</strong> section supports various login methods including custom SSO and SAML providers like Google and GitHub, plus email integrations for notifications and communications.",
"adminTools": "Finally, we have advanced administration tools like <strong>Auditing</strong> to track system activity and <strong>Usage Analytics</strong> to monitor how your users interact with the platform.",
"wrapUp": "That's the admin tour! You've seen the enterprise features that make Stirling PDF a powerful, customisable solution for organisations. Access this tour anytime from the <strong>Help</strong> menu."
},
"workspace": {
"title": "Workspace",
"people": {

View File

@ -14,6 +14,7 @@ import { ViewerProvider } from "@app/contexts/ViewerContext";
import { SignatureProvider } from "@app/contexts/SignatureContext";
import { OnboardingProvider } from "@app/contexts/OnboardingContext";
import { TourOrchestrationProvider } from "@app/contexts/TourOrchestrationContext";
import { AdminTourOrchestrationProvider } from "@app/contexts/AdminTourOrchestrationContext";
import ErrorBoundary from "@app/components/shared/ErrorBoundary";
import { useScarfTracking } from "@app/hooks/useScarfTracking";
import { useAppInitialization } from "@app/hooks/useAppInitialization";
@ -59,7 +60,9 @@ export function AppProviders({ children, appConfigRetryOptions }: AppProvidersPr
<SignatureProvider>
<RightRailProvider>
<TourOrchestrationProvider>
{children}
<AdminTourOrchestrationProvider>
{children}
</AdminTourOrchestrationProvider>
</TourOrchestrationProvider>
</RightRailProvider>
</SignatureProvider>

View File

@ -6,3 +6,28 @@
ry: 8px;
filter: drop-shadow(0 0 10px var(--mantine-primary-color-filled));
}
/* Add glowing border to navigation items during admin tour */
.modal-nav-item.tour-nav-glow {
position: relative;
box-shadow:
0 0 0 2px var(--mantine-primary-color-filled),
0 0 15px var(--mantine-primary-color-filled),
inset 0 0 15px rgba(59, 130, 246, 0.1);
border-radius: 8px;
}
@keyframes pulse-glow {
0%, 100% {
box-shadow:
0 0 0 3px var(--mantine-primary-color-filled),
0 0 20px var(--mantine-primary-color-filled),
inset 0 0 20px rgba(59, 130, 246, 0.1);
}
50% {
box-shadow:
0 0 0 3px var(--mantine-primary-color-filled),
0 0 30px var(--mantine-primary-color-filled),
inset 0 0 30px rgba(59, 130, 246, 0.2);
}
}

View File

@ -5,6 +5,7 @@ import { useTranslation } from 'react-i18next';
import { CloseButton, ActionIcon } from '@mantine/core';
import { useFilesModalContext } from '@app/contexts/FilesModalContext';
import { useTourOrchestration } from '@app/contexts/TourOrchestrationContext';
import { useAdminTourOrchestration } from '@app/contexts/AdminTourOrchestrationContext';
import ArrowForwardIcon from '@mui/icons-material/ArrowForward';
import CheckIcon from '@mui/icons-material/Check';
import TourWelcomeModal from '@app/components/onboarding/TourWelcomeModal';
@ -32,6 +33,18 @@ enum TourStep {
WRAP_UP,
}
enum AdminTourStep {
WELCOME,
CONFIG_BUTTON,
SETTINGS_OVERVIEW,
TEAMS_AND_USERS,
SYSTEM_CUSTOMIZATION,
DATABASE_SECTION,
CONNECTIONS_SECTION,
ADMIN_TOOLS,
WRAP_UP,
}
function TourContent() {
const { isOpen } = useOnboarding();
const { setIsOpen, setCurrentStep } = useTour();
@ -54,8 +67,36 @@ function TourContent() {
export default function OnboardingTour() {
const { t } = useTranslation();
const { completeTour, showWelcomeModal, setShowWelcomeModal, startTour } = useOnboarding();
const { completeTour, showWelcomeModal, setShowWelcomeModal, startTour, tourType, isOpen } = useOnboarding();
const { openFilesModal, closeFilesModal } = useFilesModalContext();
// Helper to add glow to multiple elements
const addGlowToElements = (selectors: string[]) => {
selectors.forEach(selector => {
const element = document.querySelector(selector);
if (element) {
if (selector === '[data-tour="settings-content-area"]') {
element.classList.add('tour-content-glow');
} else {
element.classList.add('tour-nav-glow');
}
}
});
};
// Helper to remove all glows
const removeAllGlows = () => {
document.querySelectorAll('.tour-content-glow').forEach(el => el.classList.remove('tour-content-glow'));
document.querySelectorAll('.tour-nav-glow').forEach(el => el.classList.remove('tour-nav-glow'));
};
// Cleanup glows when tour closes
React.useEffect(() => {
if (!isOpen) {
removeAllGlows();
}
return () => removeAllGlows();
}, [isOpen]);
const {
saveWorkbenchState,
restoreWorkbenchState,
@ -70,6 +111,13 @@ export default function OnboardingTour() {
modifyCropSettings,
executeTool,
} = useTourOrchestration();
const {
saveAdminState,
restoreAdminState,
openConfigModal,
navigateToSection,
scrollNavToSection,
} = useAdminTourOrchestration();
// Define steps as object keyed by enum - TypeScript ensures all keys are present
const stepsConfig: Record<TourStep, StepType> = {
@ -202,8 +250,125 @@ export default function OnboardingTour() {
},
};
// Convert to array using enum's numeric ordering
const steps = Object.values(stepsConfig);
// Define admin tour steps
const adminStepsConfig: Record<AdminTourStep, StepType> = {
[AdminTourStep.WELCOME]: {
selector: '[data-tour="config-button"]',
content: t('adminOnboarding.welcome', "Welcome to the <strong>Admin Tour</strong>! Let's explore the powerful enterprise features and settings available to system administrators."),
position: 'right',
padding: 10,
action: () => {
saveAdminState();
},
},
[AdminTourStep.CONFIG_BUTTON]: {
selector: '[data-tour="config-button"]',
content: t('adminOnboarding.configButton', "Click the <strong>Config</strong> button to access all system settings and administrative controls."),
position: 'right',
padding: 10,
actionAfter: () => {
openConfigModal();
},
},
[AdminTourStep.SETTINGS_OVERVIEW]: {
selector: '.modal-nav',
content: t('adminOnboarding.settingsOverview', "This is the <strong>Settings Panel</strong>. Admin settings are organised by category for easy navigation."),
position: 'right',
padding: 0,
action: () => {
removeAllGlows();
},
},
[AdminTourStep.TEAMS_AND_USERS]: {
selector: '[data-tour="admin-people-nav"]',
highlightedSelectors: ['[data-tour="admin-people-nav"]', '[data-tour="admin-teams-nav"]', '[data-tour="settings-content-area"]'],
content: t('adminOnboarding.teamsAndUsers', "Manage <strong>Teams</strong> and individual users here. You can invite new users via email, shareable links, or create custom accounts for them yourself."),
position: 'right',
padding: 10,
action: () => {
removeAllGlows();
navigateToSection('people');
setTimeout(() => {
addGlowToElements(['[data-tour="admin-people-nav"]', '[data-tour="admin-teams-nav"]', '[data-tour="settings-content-area"]']);
}, 100);
},
},
[AdminTourStep.SYSTEM_CUSTOMIZATION]: {
selector: '[data-tour="admin-adminGeneral-nav"]',
highlightedSelectors: ['[data-tour="admin-adminGeneral-nav"]', '[data-tour="admin-adminFeatures-nav"]', '[data-tour="admin-adminEndpoints-nav"]', '[data-tour="settings-content-area"]'],
content: t('adminOnboarding.systemCustomization', "We have extensive ways to customise the UI: <strong>System Settings</strong> let you change the app name and languages, <strong>Features</strong> allows server certificate management, and <strong>Endpoints</strong> lets you enable or disable specific tools for your users."),
position: 'right',
padding: 10,
action: () => {
removeAllGlows();
navigateToSection('adminGeneral');
setTimeout(() => {
addGlowToElements(['[data-tour="admin-adminGeneral-nav"]', '[data-tour="admin-adminFeatures-nav"]', '[data-tour="admin-adminEndpoints-nav"]', '[data-tour="settings-content-area"]']);
}, 100);
},
},
[AdminTourStep.DATABASE_SECTION]: {
selector: '[data-tour="admin-adminDatabase-nav"]',
highlightedSelectors: ['[data-tour="admin-adminDatabase-nav"]', '[data-tour="settings-content-area"]'],
content: t('adminOnboarding.databaseSection', "For advanced production environments, we have settings to allow <strong>external database hookups</strong> so you can integrate with your existing infrastructure."),
position: 'right',
padding: 10,
action: () => {
removeAllGlows();
navigateToSection('adminDatabase');
setTimeout(() => {
addGlowToElements(['[data-tour="admin-adminDatabase-nav"]', '[data-tour="settings-content-area"]']);
}, 100);
},
},
[AdminTourStep.CONNECTIONS_SECTION]: {
selector: '[data-tour="admin-adminConnections-nav"]',
highlightedSelectors: ['[data-tour="admin-adminConnections-nav"]', '[data-tour="settings-content-area"]'],
content: t('adminOnboarding.connectionsSection', "The <strong>Connections</strong> section supports various login methods including custom SSO and SAML providers like Google and GitHub, plus email integrations for notifications and communications."),
position: 'right',
padding: 10,
action: () => {
removeAllGlows();
navigateToSection('adminConnections');
setTimeout(() => {
addGlowToElements(['[data-tour="admin-adminConnections-nav"]', '[data-tour="settings-content-area"]']);
}, 100);
},
actionAfter: async () => {
// Scroll for the NEXT step before it shows
await scrollNavToSection('adminAudit');
},
},
[AdminTourStep.ADMIN_TOOLS]: {
selector: '[data-tour="admin-adminAudit-nav"]',
highlightedSelectors: ['[data-tour="admin-adminAudit-nav"]', '[data-tour="admin-adminUsage-nav"]', '[data-tour="settings-content-area"]'],
content: t('adminOnboarding.adminTools', "Finally, we have advanced administration tools like <strong>Auditing</strong> to track system activity and <strong>Usage Analytics</strong> to monitor how your users interact with the platform."),
position: 'right',
padding: 10,
action: () => {
// Just navigate, scroll already happened in previous step
removeAllGlows();
navigateToSection('adminAudit');
setTimeout(() => {
addGlowToElements(['[data-tour="admin-adminAudit-nav"]', '[data-tour="admin-adminUsage-nav"]', '[data-tour="settings-content-area"]']);
}, 100);
},
},
[AdminTourStep.WRAP_UP]: {
selector: '[data-tour="help-button"]',
content: t('adminOnboarding.wrapUp', "That's the admin tour! You've seen the enterprise features that make Stirling PDF a powerful, customisable solution for organisations. Access this tour anytime from the <strong>Help</strong> menu."),
position: 'right',
padding: 10,
action: () => {
removeAllGlows();
},
},
};
// Select steps based on tour type
const steps = tourType === 'admin'
? Object.values(adminStepsConfig)
: Object.values(stepsConfig);
const advanceTour = ({ setCurrentStep, currentStep, steps, setIsOpen }: {
setCurrentStep: (value: number | ((prev: number) => number)) => void;
@ -213,7 +378,11 @@ export default function OnboardingTour() {
}) => {
if (steps && currentStep === steps.length - 1) {
setIsOpen(false);
restoreWorkbenchState();
if (tourType === 'admin') {
restoreAdminState();
} else {
restoreWorkbenchState();
}
completeTour();
} else if (steps) {
setCurrentStep((s) => (s === steps.length - 1 ? 0 : s + 1));
@ -222,7 +391,11 @@ export default function OnboardingTour() {
const handleCloseTour = ({ setIsOpen }: { setIsOpen: (value: boolean) => void }) => {
setIsOpen(false);
restoreWorkbenchState();
if (tourType === 'admin') {
restoreAdminState();
} else {
restoreWorkbenchState();
}
completeTour();
};
@ -243,7 +416,9 @@ export default function OnboardingTour() {
}}
/>
<TourProvider
key={tourType}
steps={steps}
maskClassName={tourType === 'admin' ? 'admin-tour-mask' : undefined}
onClickClose={handleCloseTour}
onClickMask={advanceTour}
onClickHighlighted={(e, clickProps) => {

View File

@ -154,6 +154,7 @@ const AppConfigModal: React.FC<AppConfigModalProps> = ({ opened, onClose }) => {
opacity: isDisabled ? 0.5 : 1,
cursor: isDisabled ? 'not-allowed' : 'pointer',
}}
data-tour={`admin-${item.key}-nav`}
>
<LocalIcon icon={item.icon} width={iconSize} height={iconSize} style={{ color }} />
{!isMobile && (
@ -185,7 +186,7 @@ const AppConfigModal: React.FC<AppConfigModalProps> = ({ opened, onClose }) => {
</div>
{/* Right content */}
<div className="modal-content">
<div className="modal-content" data-tour="settings-content-area">
<div className="modal-content-scroll">
{/* Sticky header with section title and small close button */}
<div

View File

@ -1,5 +1,5 @@
import React, { useState, useRef, forwardRef, useEffect } from "react";
import { ActionIcon, Stack, Divider } from "@mantine/core";
import { ActionIcon, Stack, Divider, Menu } from "@mantine/core";
import { useTranslation } from 'react-i18next';
import { useNavigate, useLocation } from 'react-router-dom';
import LocalIcon from '@app/components/shared/LocalIcon';
@ -21,6 +21,7 @@ import {
getNavButtonStyle,
getActiveNavButton,
} from '@app/components/shared/quickAccessBar/QuickAccessBar';
import { Z_INDEX_OVER_FULLSCREEN_SURFACE } from '@app/styles/zIndex';
const QuickAccessBar = forwardRef<HTMLDivElement>((_, ref) => {
const { t } = useTranslation();
@ -179,7 +180,7 @@ const QuickAccessBar = forwardRef<HTMLDivElement>((_, ref) => {
size: 'lg',
type: 'action',
onClick: () => {
startTour();
// This will be overridden by the wrapper logic
},
},
{
@ -258,11 +259,70 @@ const QuickAccessBar = forwardRef<HTMLDivElement>((_, ref) => {
{/* Bottom section */}
<Stack gap="lg" align="center">
{bottomButtons.map((config, index) => (
<React.Fragment key={config.id}>
{renderNavButton(config, index)}
</React.Fragment>
))}
{bottomButtons.map((buttonConfig, index) => {
// Handle help button with menu or direct action
if (buttonConfig.id === 'help') {
const isAdmin = config?.isAdmin === true;
// If not admin, just show button that starts tools tour directly
if (!isAdmin) {
return (
<div
key={buttonConfig.id}
data-tour="help-button"
onClick={() => startTour('tools')}
>
{renderNavButton(buttonConfig, index)}
</div>
);
}
// If admin, show menu with both options
return (
<div key={buttonConfig.id} data-tour="help-button">
<Menu position="right" offset={10} zIndex={Z_INDEX_OVER_FULLSCREEN_SURFACE}>
<Menu.Target>
<div>{renderNavButton(buttonConfig, index)}</div>
</Menu.Target>
<Menu.Dropdown>
<Menu.Item
leftSection={<LocalIcon icon="view-carousel-rounded" width="1.25rem" height="1.25rem" />}
onClick={() => startTour('tools')}
>
<div>
<div style={{ fontWeight: 500 }}>
{t("quickAccess.helpMenu.toolsTour", "Tools Tour")}
</div>
<div style={{ fontSize: '0.875rem', opacity: 0.7 }}>
{t("quickAccess.helpMenu.toolsTourDesc", "Learn what the tools can do")}
</div>
</div>
</Menu.Item>
<Menu.Item
leftSection={<LocalIcon icon="admin-panel-settings-rounded" width="1.25rem" height="1.25rem" />}
onClick={() => startTour('admin')}
>
<div>
<div style={{ fontWeight: 500 }}>
{t("quickAccess.helpMenu.adminTour", "Admin Tour")}
</div>
<div style={{ fontSize: '0.875rem', opacity: 0.7 }}>
{t("quickAccess.helpMenu.adminTourDesc", "Explore admin settings & features")}
</div>
</div>
</Menu.Item>
</Menu.Dropdown>
</Menu>
</div>
);
}
return (
<React.Fragment key={buttonConfig.id}>
{renderNavButton(buttonConfig, index)}
</React.Fragment>
);
})}
</Stack>
</div>
</div>

View File

@ -0,0 +1,124 @@
import React, { createContext, useContext, useCallback, useRef } from 'react';
import { useNavigate, useLocation } from 'react-router-dom';
interface AdminTourOrchestrationContextType {
// State management
saveAdminState: () => void;
restoreAdminState: () => void;
// Modal & navigation
openConfigModal: () => void;
closeConfigModal: () => void;
navigateToSection: (section: string) => void;
scrollNavToSection: (section: string) => void;
// Section-specific actions
scrollToSetting: (settingId: string) => void;
}
const AdminTourOrchestrationContext = createContext<AdminTourOrchestrationContextType | undefined>(undefined);
export const AdminTourOrchestrationProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
const navigate = useNavigate();
const location = useLocation();
// Store the user's location before tour starts
const savedLocationRef = useRef<string>('');
const saveAdminState = useCallback(() => {
savedLocationRef.current = location.pathname;
console.log('Saving admin state, location:', location.pathname);
}, [location.pathname]);
const restoreAdminState = useCallback(() => {
console.log('Restoring admin state, saved location:', savedLocationRef.current);
// Navigate back to saved location or home
const targetPath = savedLocationRef.current || '/';
navigate(targetPath, { replace: true });
savedLocationRef.current = '';
}, [navigate]);
const openConfigModal = useCallback(() => {
// Navigate to settings overview to open the modal
navigate('/settings/overview');
}, [navigate]);
const closeConfigModal = useCallback(() => {
// Navigate back to home to close the modal
navigate('/', { replace: true });
}, [navigate]);
const navigateToSection = useCallback((section: string) => {
navigate(`/settings/${section}`);
}, [navigate]);
const scrollNavToSection = useCallback((section: string): Promise<void> => {
return new Promise((resolve) => {
const navElement = document.querySelector(`[data-tour="admin-${section}-nav"]`) as HTMLElement;
const scrollContainer = document.querySelector('.modal-nav-scroll') as HTMLElement;
if (navElement && scrollContainer) {
// Get the position of the nav element relative to the scroll container
const navTop = navElement.offsetTop;
const containerHeight = scrollContainer.clientHeight;
const navHeight = navElement.offsetHeight;
// Calculate scroll position to center the element
const scrollTo = navTop - (containerHeight / 2) + (navHeight / 2);
// Instant scroll to avoid timing issues
scrollContainer.scrollTo({
top: Math.max(0, scrollTo),
behavior: 'auto'
});
// Use multiple animation frames to ensure browser has fully updated
requestAnimationFrame(() => {
requestAnimationFrame(() => {
requestAnimationFrame(() => {
resolve();
});
});
});
} else {
resolve();
}
});
}, []);
const scrollToSetting = useCallback((settingId: string) => {
// Wait for the DOM to update, then scroll to the setting
setTimeout(() => {
const element = document.querySelector(`[data-tour="${settingId}"]`);
if (element) {
element.scrollIntoView({ behavior: 'smooth', block: 'center' });
}
}, 300);
}, []);
const value: AdminTourOrchestrationContextType = {
saveAdminState,
restoreAdminState,
openConfigModal,
closeConfigModal,
navigateToSection,
scrollNavToSection,
scrollToSetting,
};
return (
<AdminTourOrchestrationContext.Provider value={value}>
{children}
</AdminTourOrchestrationContext.Provider>
);
};
export const useAdminTourOrchestration = (): AdminTourOrchestrationContextType => {
const context = useContext(AdminTourOrchestrationContext);
if (!context) {
throw new Error('useAdminTourOrchestration must be used within AdminTourOrchestrationProvider');
}
return context;
};

View File

@ -2,14 +2,17 @@ import React, { createContext, useContext, useState, useEffect, useCallback } fr
import { usePreferences } from '@app/contexts/PreferencesContext';
import { useShouldShowWelcomeModal } from '@app/hooks/useShouldShowWelcomeModal';
export type TourType = 'tools' | 'admin';
interface OnboardingContextValue {
isOpen: boolean;
currentStep: number;
tourType: TourType;
setCurrentStep: (step: number) => void;
startTour: () => void;
startTour: (type?: TourType) => void;
closeTour: () => void;
completeTour: () => void;
resetTour: () => void;
resetTour: (type?: TourType) => void;
showWelcomeModal: boolean;
setShowWelcomeModal: (show: boolean) => void;
}
@ -20,6 +23,7 @@ export const OnboardingProvider: React.FC<{ children: React.ReactNode }> = ({ ch
const { updatePreference } = usePreferences();
const [isOpen, setIsOpen] = useState(false);
const [currentStep, setCurrentStep] = useState(0);
const [tourType, setTourType] = useState<TourType>('tools');
const [showWelcomeModal, setShowWelcomeModal] = useState(false);
const shouldShow = useShouldShowWelcomeModal();
@ -30,7 +34,8 @@ export const OnboardingProvider: React.FC<{ children: React.ReactNode }> = ({ ch
}
}, [shouldShow]);
const startTour = useCallback(() => {
const startTour = useCallback((type: TourType = 'tools') => {
setTourType(type);
setCurrentStep(0);
setIsOpen(true);
}, []);
@ -44,8 +49,9 @@ export const OnboardingProvider: React.FC<{ children: React.ReactNode }> = ({ ch
updatePreference('hasCompletedOnboarding', true);
}, [updatePreference]);
const resetTour = useCallback(() => {
const resetTour = useCallback((type: TourType = 'tools') => {
updatePreference('hasCompletedOnboarding', false);
setTourType(type);
setCurrentStep(0);
setIsOpen(true);
}, [updatePreference]);
@ -55,6 +61,7 @@ export const OnboardingProvider: React.FC<{ children: React.ReactNode }> = ({ ch
value={{
isOpen,
currentStep,
tourType,
setCurrentStep,
startTour,
closeTour,