/** * Tooltip Definitions * Defines all tooltip content, positioning, and behavior for the onboarding system */ (function(window) { 'use strict'; /** * Validate a tooltip definition * @param {Object} tooltip - The tooltip definition to validate * @returns {Object} { valid: boolean, errors: string[] } */ function validateTooltipDefinition(tooltip) { const errors = []; // Required fields if (!tooltip.id || typeof tooltip.id !== 'string') { errors.push('Tooltip must have a valid string id'); } if (!tooltip.element) { errors.push('Tooltip must have an element selector or HTMLElement'); } if (!tooltip.popover || typeof tooltip.popover !== 'object') { errors.push('Tooltip must have a popover object'); } else { // Validate popover fields if (!tooltip.popover.title || typeof tooltip.popover.title !== 'string') { errors.push('Tooltip popover must have a valid string title'); } if (!tooltip.popover.description || typeof tooltip.popover.description !== 'string') { errors.push('Tooltip popover must have a valid string description'); } // Validate position if provided if (tooltip.popover.position) { const validPositions = ['top', 'bottom', 'left', 'right', 'center']; if (!validPositions.includes(tooltip.popover.position)) { errors.push(`Invalid position: ${tooltip.popover.position}. Must be one of: ${validPositions.join(', ')}`); } } // Validate align if provided if (tooltip.popover.align) { const validAligns = ['start', 'center', 'end']; if (!validAligns.includes(tooltip.popover.align)) { errors.push(`Invalid align: ${tooltip.popover.align}. Must be one of: ${validAligns.join(', ')}`); } } // Validate showButtons if provided if (tooltip.popover.showButtons && !Array.isArray(tooltip.popover.showButtons)) { errors.push('showButtons must be an array'); } // Validate callbacks if provided const callbacks = ['onNext', 'onPrevious', 'onClose', 'onSetupNow', 'onLater']; callbacks.forEach(callback => { if (tooltip.popover[callback] && typeof tooltip.popover[callback] !== 'function') { errors.push(`${callback} must be a function`); } }); } // Validate condition if provided if (tooltip.condition && typeof tooltip.condition !== 'function') { errors.push('condition must be a function'); } // Validate priority if provided if (tooltip.priority !== undefined && typeof tooltip.priority !== 'number') { errors.push('priority must be a number'); } return { valid: errors.length === 0, errors }; } /** * Validate an array of tooltip definitions * @param {Array} tooltips - Array of tooltip definitions * @returns {Object} { valid: boolean, errors: Object[] } */ function validateTooltipDefinitions(tooltips) { if (!Array.isArray(tooltips)) { return { valid: false, errors: [{ tooltip: null, errors: ['tooltips must be an array'] }] }; } const allErrors = []; const ids = new Set(); tooltips.forEach((tooltip, index) => { const validation = validateTooltipDefinition(tooltip); if (!validation.valid) { allErrors.push({ tooltip: tooltip.id || `index ${index}`, errors: validation.errors }); } // Check for duplicate IDs if (tooltip.id) { if (ids.has(tooltip.id)) { allErrors.push({ tooltip: tooltip.id, errors: [`Duplicate tooltip ID: ${tooltip.id}`] }); } ids.add(tooltip.id); } }); return { valid: allErrors.length === 0, errors: allErrors }; } /** * Error handler for tooltip system */ class TooltipError extends Error { constructor(message, tooltipId = null) { super(message); this.name = 'TooltipError'; this.tooltipId = tooltipId; } } /** * Handle tooltip definition errors * @param {Object} validation - Validation result * @throws {TooltipError} If validation fails */ function handleValidationErrors(validation) { if (!validation.valid) { const errorMessages = validation.errors.map(e => `${e.tooltip}: ${e.errors.join(', ')}` ).join('\n'); console.error('[TooltipDefinitions] Validation errors:', errorMessages); throw new TooltipError(`Tooltip validation failed:\n${errorMessages}`); } } // Export to global scope window.TooltipValidation = { validateTooltipDefinition, validateTooltipDefinitions, handleValidationErrors, TooltipError }; console.log('[TooltipDefinitions] Validation module loaded'); })(window); /** * Tooltip Definitions Array * Defines all tooltips for the onboarding tour */ const TOOLTIP_DEFINITIONS = [ // 1. Welcome { id: 'welcome', element: '#brand', popover: { title: 'Welcome to DashCaddy!', description: `
Your unified control panel for Docker, Caddy, and DNS — all in one place.
This tour will walk you through every section so you know exactly where everything is.
You can click your logo anytime to customize it.
`, position: 'bottom', align: 'start', showButtons: ['next'], showProgress: true }, priority: 1 }, // 2. Infrastructure row { id: 'infrastructure', element: '.top', popover: { title: 'Infrastructure Overview', description: `This top row shows the backbone of your setup:
These core services are always pinned here, separate from your app grid.
`, position: 'bottom', showButtons: ['previous', 'next'], showProgress: true }, priority: 2 }, // 3. Service cards { id: 'service-cards', element: '#cards', popover: { title: 'Your App Grid', description: `Every app you deploy appears here as a card. Each one shows:
Hover over a card to see action buttons: Logs, Restart, Update, Settings, and Open.
`, position: 'top', showButtons: ['previous', 'next'], showProgress: true }, priority: 3 }, // 4. App Selector { id: 'app-selector', element: '#add-service-btn', popover: { title: 'App Selector — Deploy in One Click', description: `Browse 50+ self-hosted apps organized by category:
Each app deploys with Docker, Caddy reverse proxy, and DNS — fully configured automatically.
★ Premium: Recipes let you deploy entire stacks (e.g., full media server) in one click with pre-wired configs.
`, position: 'top', showButtons: ['previous', 'next'], showProgress: true }, priority: 4, condition: () => document.getElementById('add-service-btn') !== null }, // 5. Smart Arr Connect { id: 'smart-arr', element: '#arr-setup-btn', popover: { title: 'Smart Arr Connect', description: `Automatically wire up your entire media stack:
No manual API key copying — DashCaddy handles it all.
`, position: 'top', showButtons: ['previous', 'next'], showProgress: true }, priority: 5, condition: () => document.getElementById('arr-setup-btn') !== null }, // 6. Add App Manually (toolbar) { id: 'add-manual', element: '#add-service', popover: { title: 'Add App Manually', description: `Already have a service running? Add it to your dashboard manually.
You can add:
Switch between 7 built-in themes:
Dark, Light, Blue, Nord, Dracula, Solarized Dark, and Solarized Light.
Your preference is saved automatically. You can also build custom themes with the Theme Builder (in Admin → settings).
`, position: 'bottom', showButtons: ['previous', 'next'], showProgress: true }, priority: 7 }, // 8. Toolbar: Status section { id: 'toolbar-status', element: '.tools-section[data-section="status"]', popover: { title: 'Status Tools', description: `Click Status to expand these tools:
These sections remember whether you left them open or closed.
`, position: 'bottom', showButtons: ['previous', 'next'], showProgress: true }, priority: 8 }, // 9. Toolbar: Tools section { id: 'toolbar-tools', element: '.tools-section[data-section="tools"]', popover: { title: 'Operational Tools', description: `Click Tools for day-to-day operations:
Click Admin for setup and maintenance:
Shows current conditions for your location. Click the gear icon to configure your city or switch temperature units.
Uses Open-Meteo — no API key required.
`, position: 'bottom', showButtons: ['previous', 'next'], showProgress: true }, priority: 11, condition: () => document.getElementById('weather-widget') !== null }, // 12. Reload Caddy { id: 'reload-caddy', element: '#reload-caddy-top', popover: { title: 'Reload Caddy', description: `After changing Caddy configuration (adding reverse proxy rules, SSL settings, etc.), click here to apply the changes live.
This is a graceful reload — existing connections are not dropped.
`, position: 'bottom', align: 'end', showButtons: ['previous', 'next'], showProgress: true }, priority: 12, condition: () => document.getElementById('reload-caddy-top') !== null }, // 13. Finish { id: 'tour-complete', element: '#brand', popover: { title: 'You\'re All Set!', description: `That covers the essentials. A few tips to get the most out of DashCaddy:
? anytime to see them all★ Unlock Premium
Premium adds powerful features for serious homelabbers:
Activate in Admin → License
You can restart this tour anytime from Admin → Help Tour.
`, position: 'bottom', align: 'start', showButtons: ['previous', 'close'], showProgress: true }, priority: 13 } ]; /** * Get tooltip definitions * @returns {Array} Array of tooltip definitions */ function getTooltipDefinitions() { return TOOLTIP_DEFINITIONS; } /** * Get a specific tooltip by ID * @param {string} id - Tooltip ID * @returns {Object|null} Tooltip definition or null if not found */ function getTooltipById(id) { return TOOLTIP_DEFINITIONS.find(t => t.id === id) || null; } /** * Get tooltips filtered by condition * @returns {Array} Array of tooltips that pass their condition check */ function getActiveTooltips() { return TOOLTIP_DEFINITIONS.filter(tooltip => { if (tooltip.condition && typeof tooltip.condition === 'function') { try { return tooltip.condition(); } catch (error) { console.error(`[TooltipDefinitions] Error evaluating condition for ${tooltip.id}:`, error); return false; } } return true; }); } /** * Get tooltips sorted by priority * @returns {Array} Array of tooltips sorted by priority (ascending) */ function getSortedTooltips() { const tooltips = getActiveTooltips(); return tooltips.sort((a, b) => { const priorityA = a.priority || 999; const priorityB = b.priority || 999; return priorityA - priorityB; }); } /** * Get tooltips marked as new features * @returns {Array} Array of tooltips marked with isNewFeature flag */ function getNewFeatureTooltips() { const tooltips = getActiveTooltips(); return tooltips.filter(tooltip => tooltip.isNewFeature === true) .sort((a, b) => { const priorityA = a.priority || 999; const priorityB = b.priority || 999; return priorityA - priorityB; }); } // Export to global scope window.TooltipDefinitions = { TOOLTIP_DEFINITIONS, getTooltipDefinitions, getTooltipById, getActiveTooltips, getSortedTooltips, getNewFeatureTooltips }; console.log('[TooltipDefinitions] Definitions loaded:', TOOLTIP_DEFINITIONS.length, 'tooltips');