Files
TimeTracker/app/static/onboarding.js
T
Dries Peeters 07186d7b6b feat(web): base-init, PWA, keyboard shortcuts, onboarding, and template updates
- Add base-init.js for shared keyboard/sidebar init; single PWA registration in pwa-enhancements
- Update keyboard-shortcuts, onboarding, enhanced-search, service-worker
- Template updates: base, list pages, dashboard, mileage/gps, timer; fix duplicate IDs and a11y
2026-03-06 15:45:50 +01:00

929 lines
35 KiB
JavaScript

/**
* Onboarding System for TimeTracker
* Interactive product tours and first-time user experience
*/
class OnboardingManager {
constructor() {
this.currentStep = 0;
this.steps = [];
this.overlay = null;
this.tooltip = null;
this.storageKey = 'onboarding_completed';
}
/**
* Initialize onboarding
*/
init(steps) {
if (this.isCompleted()) {
return;
}
// Disable tour on mobile devices (width < 768px)
const isMobile = window.innerWidth <= 768;
if (isMobile) {
// Mark as completed to prevent future attempts
localStorage.setItem(this.storageKey, 'true');
return;
}
this.steps = steps;
this.createOverlay();
this.createTooltip();
// Add resize handler to handle window resizing during tour
this.resizeHandler = () => {
const isMobile = window.innerWidth <= 768;
if (isMobile) {
// If window is resized to mobile size, cancel the tour
this.complete();
}
};
window.addEventListener('resize', this.resizeHandler);
this.showStep(0);
}
/**
* Create overlay element
*/
createOverlay() {
this.overlay = document.createElement('div');
this.overlay.className = 'onboarding-overlay';
this.overlay.innerHTML = `
<style>
.onboarding-overlay {
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.7);
z-index: 9998;
backdrop-filter: blur(2px);
animation: fadeIn 0.3s ease-out;
pointer-events: none;
}
.onboarding-overlay * {
pointer-events: none;
}
.onboarding-highlight {
position: fixed;
border: 4px solid #3b82f6;
border-radius: 8px;
z-index: 10000 !important;
transition: all 0.3s ease-out;
pointer-events: none;
background: transparent;
box-shadow:
0 0 0 0 rgba(59, 130, 246, 0),
0 0 20px rgba(59, 130, 246, 0.6),
0 0 40px rgba(59, 130, 246, 0.4),
inset 0 0 20px rgba(59, 130, 246, 0.2);
}
.onboarding-highlight::before {
content: '';
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.8);
z-index: 9999;
pointer-events: none;
mask: radial-gradient(ellipse at center, transparent 0%, transparent 100%);
-webkit-mask: radial-gradient(ellipse at center, transparent 0%, transparent 100%);
}
.onboarding-overlay {
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.5);
z-index: 9998;
backdrop-filter: blur(2px);
animation: fadeIn 0.3s ease-out;
pointer-events: none;
}
.onboarding-mask {
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.6);
z-index: 9999;
pointer-events: none;
transition: all 0.3s ease-out;
}
.onboarding-tooltip {
position: fixed;
background: white;
border-radius: 12px;
box-shadow: 0 20px 50px rgba(0, 0, 0, 0.3);
padding: 24px;
max-width: 400px;
min-width: 300px;
z-index: 10001 !important;
animation: slideInUp 0.3s ease-out;
display: block;
visibility: visible;
transition: opacity 0.2s ease-out;
pointer-events: auto;
}
/* Mobile responsive styles (for future use if tour is enabled on mobile) */
@media (max-width: 768px) {
.onboarding-tooltip {
max-width: calc(100vw - 32px);
min-width: unset;
width: calc(100vw - 32px);
padding: 20px;
left: 16px !important;
right: 16px !important;
top: auto !important;
bottom: 20px !important;
transform: translateY(0) !important;
}
.onboarding-tooltip-header {
margin-bottom: 10px;
}
.onboarding-tooltip-title {
font-size: 16px;
}
.onboarding-tooltip-body {
font-size: 14px;
margin-bottom: 16px;
}
.onboarding-tooltip-footer {
flex-direction: column;
gap: 12px;
}
.onboarding-tooltip-buttons {
width: 100%;
flex-direction: column;
}
.onboarding-btn {
width: 100%;
padding: 12px 16px;
}
.onboarding-tooltip-progress {
text-align: center;
width: 100%;
}
}
.onboarding-tooltip * {
pointer-events: auto;
}
.dark .onboarding-tooltip {
background: #2d3748;
color: #e2e8f0;
}
.onboarding-tooltip-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 12px;
}
.onboarding-tooltip-title {
font-size: 18px;
font-weight: 600;
color: #1e293b;
}
.dark .onboarding-tooltip-title {
color: #e2e8f0;
}
.onboarding-tooltip-close {
background: none;
border: none;
font-size: 20px;
color: #9ca3af;
cursor: pointer;
padding: 0;
width: 24px;
height: 24px;
display: flex;
align-items: center;
justify-content: center;
}
.onboarding-tooltip-close:hover {
color: #ef4444;
}
.onboarding-tooltip-body {
color: #64748b;
line-height: 1.6;
margin-bottom: 20px;
}
.dark .onboarding-tooltip-body {
color: #94a3b8;
}
.onboarding-tooltip-body kbd {
display: inline-block;
padding: 2px 6px;
font-size: 0.875rem;
font-family: monospace;
background: #f1f5f9;
border: 1px solid #cbd5e1;
border-radius: 4px;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
color: #334155;
}
.dark .onboarding-tooltip-body kbd {
background: #1e293b;
border-color: #334155;
color: #e2e8f0;
}
.onboarding-tooltip-body strong {
color: #1e293b;
font-weight: 600;
}
.dark .onboarding-tooltip-body strong {
color: #e2e8f0;
}
.onboarding-tooltip-footer {
display: flex;
align-items: center;
justify-content: space-between;
}
.onboarding-tooltip-progress {
font-size: 14px;
color: #9ca3af;
}
.onboarding-tooltip-buttons {
display: flex;
gap: 8px;
}
.onboarding-btn {
padding: 8px 16px;
border-radius: 6px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s;
border: none;
font-size: 14px;
}
.onboarding-btn-skip {
background: #f3f4f6;
color: #6b7280;
}
.dark .onboarding-btn-skip {
background: #374151;
color: #9ca3af;
}
.onboarding-btn-skip:hover {
background: #e5e7eb;
}
.dark .onboarding-btn-skip:hover {
background: #4b5563;
}
.onboarding-btn-primary {
background: #3b82f6;
color: white;
}
.onboarding-btn-primary:hover {
background: #2563eb;
}
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
@keyframes slideInUp {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
</style>
`;
document.body.appendChild(this.overlay);
}
/**
* Create tooltip element
*/
createTooltip() {
this.tooltip = document.createElement('div');
this.tooltip.className = 'onboarding-tooltip';
document.body.appendChild(this.tooltip);
}
/**
* Show a specific step
*/
showStep(index) {
if (index < 0 || index >= this.steps.length) {
this.complete();
return;
}
this.currentStep = index;
const step = this.steps[index];
// Find target element with better selector handling
let target = null;
// Try to find the element
const elements = document.querySelectorAll(step.target);
if (elements.length === 0) {
console.warn(`Onboarding target not found: ${step.target}`);
// Try once more after a short delay in case element is still loading
setTimeout(() => {
const retryElements = document.querySelectorAll(step.target);
if (retryElements.length > 0) {
target = retryElements[0];
this.displayStep(target, step, index);
} else {
console.warn(`Onboarding target still not found after retry: ${step.target}, skipping step`);
this.showStep(index + 1);
}
}, 200);
return;
}
// If multiple elements found, prefer visible ones or first one
for (const el of elements) {
const rect = el.getBoundingClientRect();
const style = window.getComputedStyle(el);
// Check if element is visible (not hidden by display:none or visibility:hidden)
if (rect.width > 0 && rect.height > 0 &&
style.display !== 'none' &&
style.visibility !== 'hidden' &&
style.opacity !== '0') {
target = el;
break;
}
}
// If no visible element found, use first one anyway
if (!target && elements.length > 0) {
target = elements[0];
// Try to make it visible if it's in a hidden dropdown
const dropdown = target.closest('[id*="Dropdown"]');
if (dropdown && dropdown.classList.contains('hidden')) {
dropdown.classList.remove('hidden');
}
}
if (!target) {
console.warn(`Onboarding target not accessible: ${step.target}`);
this.showStep(index + 1);
return;
}
this.displayStep(target, step, index);
}
/**
* Display a step for a given target element
*/
displayStep(target, step, index) {
// Ensure element is visible (expand dropdowns, etc.)
const dropdown = target.closest('[id*="Dropdown"]');
if (dropdown && dropdown.classList.contains('hidden')) {
dropdown.classList.remove('hidden');
}
// Scroll target into view first
target.scrollIntoView({ behavior: 'smooth', block: 'center', inline: 'nearest' });
// Wait for scroll animation to complete, then proceed
setTimeout(() => {
// Update tooltip content first (so we can measure it)
this.updateTooltip(step, index);
// Wait for tooltip to render with content, then position both highlight and tooltip
requestAnimationFrame(() => {
requestAnimationFrame(() => {
// Get fresh element position after scroll
const finalRect = target.getBoundingClientRect();
const style = window.getComputedStyle(target);
// Validate that element is actually visible
if (finalRect.width === 0 || finalRect.height === 0) {
console.warn('Target element has zero dimensions, but continuing anyway');
// Don't skip - just proceed with positioning
}
// Highlight target after content is rendered
this.highlightElement(target);
// Position tooltip
this.positionTooltip(target, step);
});
});
}, 400);
}
/**
* Highlight target element
*/
highlightElement(element) {
let highlight = document.querySelector('.onboarding-highlight');
let mask = document.querySelector('.onboarding-mask');
if (!highlight) {
highlight = document.createElement('div');
highlight.className = 'onboarding-highlight';
document.body.appendChild(highlight);
}
if (!mask) {
mask = document.createElement('div');
mask.className = 'onboarding-mask';
mask.style.cssText = `
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.6);
z-index: 9999;
pointer-events: none;
transition: opacity 0.3s ease-out;
`;
document.body.appendChild(mask);
}
// Use getBoundingClientRect which already accounts for scroll
const rect = element.getBoundingClientRect();
const padding = 10;
// Calculate highlight position
const highlightTop = Math.round(rect.top - padding);
const highlightLeft = Math.round(rect.left - padding);
const highlightWidth = Math.round(rect.width + padding * 2);
const highlightHeight = Math.round(rect.height + padding * 2);
// Use fixed positioning to match viewport coordinates
highlight.style.position = 'fixed';
highlight.style.top = `${highlightTop}px`;
highlight.style.left = `${highlightLeft}px`;
highlight.style.width = `${highlightWidth}px`;
highlight.style.height = `${highlightHeight}px`;
highlight.style.display = 'block';
highlight.style.zIndex = '10000';
highlight.style.visibility = 'visible';
// Create a mask that reveals the highlighted area
// Use CSS radial gradient for the cutout
const centerX = rect.left + rect.width / 2;
const centerY = rect.top + rect.height / 2;
// Make cutout larger than the highlight for better visibility
const ellipseWidth = Math.max(highlightWidth * 2, 200);
const ellipseHeight = Math.max(highlightHeight * 2, 200);
// Apply mask to overlay mask element using CSS radial gradient
// In CSS mask-image: white/opaque = visible (shows), black/transparent = hidden (hides)
// We want: hide overlay at center (reveal element), show overlay at edges (cover everything else)
// So: transparent at center (hides overlay), white at edges (shows overlay)
const overlayMask = document.querySelector('.onboarding-mask');
if (overlayMask) {
// Radial gradient: transparent at center (hides overlay, reveals element), white at edges (shows overlay, covers background)
const maskGradient = `radial-gradient(ellipse ${ellipseWidth}px ${ellipseHeight}px at ${centerX}px ${centerY}px, transparent 0%, transparent 50%, rgba(255,255,255,0.2) 65%, rgba(255,255,255,0.6) 80%, white 100%)`;
overlayMask.style.maskImage = maskGradient;
overlayMask.style.webkitMaskImage = maskGradient;
overlayMask.style.maskSize = '100% 100%';
overlayMask.style.maskPosition = '0 0';
overlayMask.style.maskRepeat = 'no-repeat';
}
// Also apply mask to the base overlay
const baseOverlay = document.querySelector('.onboarding-overlay');
if (baseOverlay) {
const overlayGradient = `radial-gradient(ellipse ${ellipseWidth}px ${ellipseHeight}px at ${centerX}px ${centerY}px, transparent 0%, transparent 50%, rgba(255,255,255,0.2) 65%, rgba(255,255,255,0.6) 80%, white 100%)`;
baseOverlay.style.maskImage = overlayGradient;
baseOverlay.style.webkitMaskImage = overlayGradient;
baseOverlay.style.maskSize = '100% 100%';
baseOverlay.style.maskPosition = '0 0';
baseOverlay.style.maskRepeat = 'no-repeat';
}
}
/**
* Position tooltip relative to target
*/
positionTooltip(element, step) {
// Get element position first - getBoundingClientRect already accounts for scroll
const rect = element.getBoundingClientRect();
// Validate element is visible
if (rect.width === 0 || rect.height === 0) {
console.warn('Cannot position tooltip: element has zero dimensions');
return;
}
// Ensure tooltip is positioned off-screen for measurement
this.tooltip.style.position = 'fixed';
this.tooltip.style.top = '-9999px';
this.tooltip.style.left = '-9999px';
this.tooltip.style.visibility = 'hidden';
this.tooltip.style.display = 'block';
this.tooltip.style.opacity = '0';
this.tooltip.style.transform = 'scale(0)';
// Force a reflow to ensure tooltip is rendered
void this.tooltip.offsetWidth;
// Now measure the tooltip
const tooltipRect = this.tooltip.getBoundingClientRect();
const position = step.position || 'bottom';
// If tooltip has no dimensions, use default dimensions
const tooltipWidth = tooltipRect.width > 0 ? tooltipRect.width : 400;
const tooltipHeight = tooltipRect.height > 0 ? tooltipRect.height : 200;
let top, left;
// Calculate position based on preference
// All coordinates are relative to viewport (from getBoundingClientRect)
switch (position) {
case 'top':
top = rect.top - tooltipHeight - 20;
left = rect.left + (rect.width / 2) - (tooltipWidth / 2);
break;
case 'bottom':
top = rect.bottom + 20;
left = rect.left + (rect.width / 2) - (tooltipWidth / 2);
break;
case 'left':
top = rect.top + (rect.height / 2) - (tooltipHeight / 2);
left = rect.left - tooltipWidth - 20;
break;
case 'right':
top = rect.top + (rect.height / 2) - (tooltipHeight / 2);
left = rect.right + 20;
break;
default:
top = rect.bottom + 20;
left = rect.left + (rect.width / 2) - (tooltipWidth / 2);
}
// Keep within viewport
const viewportWidth = window.innerWidth;
const viewportHeight = window.innerHeight;
const padding = 10;
// Adjust horizontal position if needed
if (left < padding) {
left = padding;
} else if (left + tooltipWidth > viewportWidth - padding) {
left = Math.max(padding, viewportWidth - tooltipWidth - padding);
}
// Adjust vertical position if needed
if (top < padding) {
// If tooltip would go above viewport, try bottom instead
if (position === 'top') {
top = rect.bottom + 20;
} else {
top = padding;
}
} else if (top + tooltipHeight > viewportHeight - padding) {
// If tooltip would go below viewport, try top instead
if (position === 'bottom') {
top = Math.max(padding, rect.top - tooltipHeight - 20);
} else {
top = Math.max(padding, viewportHeight - tooltipHeight - padding);
}
}
// Validate final coordinates are reasonable
if (isNaN(top) || isNaN(left) || top < 0 || left < 0) {
console.error('Invalid tooltip position calculated:', { top, left, rect });
// Fallback to center of viewport
top = (viewportHeight - tooltipHeight) / 2;
left = (viewportWidth - tooltipWidth) / 2;
}
// Apply final position using fixed positioning (viewport coordinates)
this.tooltip.style.position = 'fixed';
this.tooltip.style.top = `${Math.round(top)}px`;
this.tooltip.style.left = `${Math.round(left)}px`;
this.tooltip.style.visibility = 'visible';
this.tooltip.style.display = 'block';
this.tooltip.style.opacity = '1';
this.tooltip.style.transform = 'scale(1)';
this.tooltip.style.zIndex = '10001'; // Ensure it's above overlay (z-index 9998) and highlight (10000)
}
/**
* Update tooltip content
*/
updateTooltip(step, index) {
const isLast = index === this.steps.length - 1;
// Store reference to manager for event handlers
const manager = this;
this.tooltip.innerHTML = `
<div class="onboarding-tooltip-header">
<h3 class="onboarding-tooltip-title">${step.title}</h3>
<button class="onboarding-tooltip-close" data-action="skip">
<i class="fas fa-times"></i>
</button>
</div>
<div class="onboarding-tooltip-body">
${step.content}
</div>
<div class="onboarding-tooltip-footer">
<span class="onboarding-tooltip-progress">
${index + 1} / ${this.steps.length}
</span>
<div class="onboarding-tooltip-buttons">
<button class="onboarding-btn onboarding-btn-skip" data-action="skip">
Skip Tour
</button>
${index > 0 ? `
<button class="onboarding-btn onboarding-btn-skip" data-action="previous">
<i class="fas fa-arrow-left mr-1"></i> Back
</button>
` : ''}
<button class="onboarding-btn onboarding-btn-primary" data-action="next">
${isLast ? 'Finish' : 'Next'} <i class="fas fa-arrow-right ml-1"></i>
</button>
</div>
</div>
`;
// Attach event listeners using event delegation
this.tooltip.querySelectorAll('[data-action]').forEach(btn => {
const action = btn.getAttribute('data-action');
btn.addEventListener('click', (e) => {
e.stopPropagation();
if (action === 'skip') {
manager.skip();
} else if (action === 'next') {
manager.next();
} else if (action === 'previous') {
manager.previous();
}
});
});
}
/**
* Go to next step
*/
next() {
this.showStep(this.currentStep + 1);
}
/**
* Go to previous step
*/
previous() {
this.showStep(this.currentStep - 1);
}
/**
* Skip the tour
*/
async skip() {
// Temporarily lower the mask and overlay z-index so the confirmation dialog (z-index 2000) appears above them
const mask = document.querySelector('.onboarding-mask');
const highlight = document.querySelector('.onboarding-highlight');
const originalMaskZ = mask?.style.zIndex;
const originalOverlayZ = this.overlay?.style.zIndex;
const originalHighlightZ = highlight?.style.zIndex;
// Lower mask and overlay z-index so confirmation dialog (z-index 2000) appears above
if (mask) {
mask.style.zIndex = '1500';
}
if (this.overlay) {
this.overlay.style.zIndex = '1500';
}
if (highlight) {
highlight.style.zIndex = '1501';
}
const confirmed = await showConfirm(
'Are you sure you want to skip the tour? You can restart it later from the Help menu.',
{
title: 'Skip Tour',
confirmText: 'Skip',
cancelText: 'Continue Tour',
variant: 'warning'
}
);
// Restore original z-index values
if (mask) {
if (originalMaskZ) {
mask.style.zIndex = originalMaskZ;
} else {
mask.style.zIndex = '';
}
}
if (this.overlay) {
if (originalOverlayZ) {
this.overlay.style.zIndex = originalOverlayZ;
} else {
this.overlay.style.zIndex = '';
}
}
if (highlight) {
if (originalHighlightZ) {
highlight.style.zIndex = originalHighlightZ;
} else {
highlight.style.zIndex = '';
}
}
if (confirmed) {
this.complete();
}
}
/**
* Complete the tour
*/
complete() {
// Remove elements
if (this.overlay) this.overlay.remove();
if (this.tooltip) this.tooltip.remove();
document.querySelector('.onboarding-highlight')?.remove();
document.querySelector('.onboarding-mask')?.remove();
// Remove resize listener if it exists
if (this.resizeHandler) {
window.removeEventListener('resize', this.resizeHandler);
this.resizeHandler = null;
}
// Mark as completed
localStorage.setItem(this.storageKey, 'true');
// Show success message
if (window.toastManager) {
window.toastManager.success('Tour completed! You\'re all set to start tracking time.');
}
// Trigger completion callback if provided
if (this.onComplete) {
this.onComplete();
}
}
/**
* Check if onboarding is completed
*/
isCompleted() {
return localStorage.getItem(this.storageKey) === 'true';
}
/**
* Reset onboarding (for testing)
*/
reset() {
localStorage.removeItem(this.storageKey);
}
}
// Default tour steps for TimeTracker
const defaultTourSteps = [
{
target: '#sidebar',
title: 'Welcome to TimeTracker! 👋',
content: 'Let\'s take a quick tour to help you get started. This is your main navigation where you can access all features. <strong>Tip:</strong> You can collapse the sidebar by clicking the arrow icon to maximize your workspace.',
position: 'right'
},
{
target: 'a[href*="dashboard"]',
title: 'Dashboard',
content: 'Your command center! View today\'s hours, active timers, recent entries, top projects, and activity timeline. <strong>Pro tip:</strong> Customize widgets to see what matters most to you. You can also see your time tracking at a glance without navigating away.',
position: 'right'
},
{
target: 'a[href*="timer"]',
title: 'Time Tracking',
content: 'Start timers or manually log your time. <strong>Key features:</strong><br>• Timers run server-side (even if browser closes!)<br>• Press <kbd>T</kbd> to quickly toggle timer<br>• Use bulk entry for multiple days<br>• Save time entry templates for recurring work<br>• Idle detection auto-pauses after inactivity',
position: 'right'
},
{
target: 'a[href*="projects"]',
title: 'Projects',
content: 'Organize your work with projects. <strong>What you can do:</strong><br>• Link projects to clients for billing<br>• Set hourly rates per project<br>• Track billable vs non-billable hours<br>• Monitor project budgets and costs<br>• Add project descriptions with Markdown<br>• Archive completed projects',
position: 'right'
},
{
target: 'a[href*="clients"]',
title: 'Clients',
content: 'Manage your clients and their information. Store contact details, billing rates, and company information. Clients are automatically linked to projects for streamlined invoicing and reporting.',
position: 'right'
},
{
target: 'a[href*="tasks"]',
title: 'Tasks',
content: 'Break down projects into manageable tasks. <strong>Features:</strong><br>• Track time against specific tasks<br>• Set priorities and due dates<br>• Assign tasks to team members<br>• Monitor progress with status tracking<br>• Use estimates vs actuals for planning<br>• Add comments for collaboration',
position: 'right'
},
{
target: 'a[href*="kanban"]',
title: 'Kanban Board',
content: 'Visual task management with drag-and-drop! Move tasks between columns (To Do, In Progress, Review, Done) to track progress. Perfect for agile workflows and visual project management. <strong>Power feature:</strong> Customize columns to match your workflow.',
position: 'right'
},
{
target: 'a[href*="calendar"]',
title: 'Calendar View',
content: 'Visualize your time entries on a calendar. See your schedule at a glance, spot gaps, and plan your time more effectively. Drag and drop entries to reschedule them quickly.',
position: 'right'
},
{
target: 'a[href*="reports"]',
title: 'Reports & Analytics',
content: 'Gain insights into your time usage. <strong>Available reports:</strong><br>• Time breakdown by project, user, or date<br>• Billable vs non-billable analysis<br>• Export to PDF or CSV<br>• Custom date ranges<br>• Save filters for quick access<br>• Visual charts and graphs',
position: 'right'
},
{
target: 'a[href*="invoices"]',
title: 'Invoicing',
content: 'Generate professional invoices from your tracked time. <strong>Features:</strong><br>• Auto-generate from time entries<br>• Add custom line items and expenses<br>• Tax calculations<br>• PDF export with branding<br>• Track payment status<br>• Send invoices to clients',
position: 'right'
},
{
target: 'a[href*="analytics"]',
title: 'Analytics Dashboard',
content: 'Deep insights into your productivity and time patterns. View trends, project analytics, and performance metrics. Identify your most productive times and optimize your workflow.',
position: 'right'
},
{
target: '#header-search',
title: 'Global Search',
content: 'Quickly find anything! Search across projects, tasks, clients, and time entries. <strong>Keyboard shortcut:</strong> Press <kbd>Ctrl+/</kbd> (or <kbd>Cmd+/</kbd> on Mac) to focus the search bar instantly.',
position: 'bottom'
},
{
target: 'button[onclick*="openCommandPalette"]',
title: 'Command Palette',
content: 'Power user feature! Press <kbd>Ctrl+K</kbd> (or <kbd>Cmd+K</kbd> on Mac) to open the command palette. Navigate to any feature, execute actions, and access shortcuts without using your mouse. <strong>Try it:</strong> Press <kbd>Ctrl+K</kbd> now!',
position: 'bottom'
},
{
target: '#theme-toggle',
title: 'Theme Toggle',
content: 'Switch between light and dark mode. Your preference is saved automatically. <strong>Keyboard shortcut:</strong> Press <kbd>Ctrl+Shift+L</kbd> to toggle themes quickly.',
position: 'bottom'
}
];
// Initialize global onboarding manager
window.onboardingManager = new OnboardingManager();
// Auto-start onboarding for new users
document.addEventListener('DOMContentLoaded', () => {
// Check if user is on dashboard and hasn't completed onboarding
if (window.location.pathname === '/main/dashboard' || window.location.pathname === '/') {
setTimeout(() => {
// Skip on mobile devices (width < 768px)
const isMobile = window.innerWidth <= 768;
if (!isMobile && !window.onboardingManager.isCompleted()) {
window.onboardingManager.init(defaultTourSteps);
} else if (isMobile) {
// Mark as completed on mobile to prevent future attempts
localStorage.setItem('onboarding_completed', 'true');
}
}, 1000);
}
});
// Add restart tour button to help menu
function restartTour() {
window.onboardingManager.reset();
window.onboardingManager.init(defaultTourSteps);
}