// Theme Management with Draggable Fun Button class ThemeManager { constructor() { this.isDragging = false; this.dragOffset = { x: 0, y: 0 }; this.position = { x: 0, y: 0 }; this.clickStartTime = 0; this.clickThreshold = 200; // ms this.init(); } init() { // Load saved theme or detect OS preference const savedTheme = localStorage.getItem('theme'); const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches; // Set initial theme if (savedTheme) { this.setTheme(savedTheme); } else if (prefersDark) { this.setTheme('dark'); } else { this.setTheme('light'); } // Listen for OS theme changes window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', (e) => { if (!localStorage.getItem('theme')) { this.setTheme(e.matches ? 'dark' : 'light'); } }); // Set up toggle button with drag functionality this.setupToggleButton(); this.loadPosition(); } setTheme(theme) { // Add smooth transition class temporarily document.body.classList.add('theme-transitioning'); document.documentElement.setAttribute('data-theme', theme); this.updateToggleButton(theme); this.addPulseEffect(); // Remove transition class after animation completes setTimeout(() => { document.body.classList.remove('theme-transitioning'); }, 400); } toggleTheme() { const currentTheme = document.documentElement.getAttribute('data-theme'); const newTheme = currentTheme === 'dark' ? 'light' : 'dark'; this.setTheme(newTheme); localStorage.setItem('theme', newTheme); this.addBounceEffect(); } setupToggleButton() { const toggleButton = document.getElementById('theme-toggle'); if (!toggleButton) return; // Simple click handler as fallback toggleButton.addEventListener('click', (e) => { // Only handle click if we haven't detected dragging if (!this.isDragging) { e.preventDefault(); e.stopPropagation(); this.toggleTheme(); } }); // Mouse events for drag functionality toggleButton.addEventListener('mousedown', this.handleStart.bind(this)); document.addEventListener('mousemove', this.handleMove.bind(this)); document.addEventListener('mouseup', this.handleEnd.bind(this)); // Touch events for mobile toggleButton.addEventListener('touchstart', this.handleStart.bind(this), { passive: false }); document.addEventListener('touchmove', this.handleMove.bind(this), { passive: false }); document.addEventListener('touchend', this.handleEnd.bind(this)); // Prevent context menu on long press toggleButton.addEventListener('contextmenu', (e) => e.preventDefault()); // Double-click for fun bounce effect toggleButton.addEventListener('dblclick', (e) => { e.preventDefault(); this.addBounceEffect(); this.snapToNearestCorner(); }); } handleStart(e) { e.preventDefault(); this.clickStartTime = Date.now(); this.isDragging = false; this.startPosition = { x: 0, y: 0 }; const toggleButton = document.getElementById('theme-toggle'); const rect = toggleButton.getBoundingClientRect(); const clientX = e.clientX || (e.touches && e.touches[0].clientX); const clientY = e.clientY || (e.touches && e.touches[0].clientY); this.dragOffset.x = clientX - rect.left; this.dragOffset.y = clientY - rect.top; this.startPosition.x = clientX; this.startPosition.y = clientY; toggleButton.classList.add('dragging'); } handleMove(e) { const toggleButton = document.getElementById('theme-toggle'); if (!toggleButton.classList.contains('dragging')) return; const clientX = e.clientX || (e.touches && e.touches[0].clientX); const clientY = e.clientY || (e.touches && e.touches[0].clientY); // Calculate distance moved from start position const deltaX = Math.abs(clientX - this.startPosition.x); const deltaY = Math.abs(clientY - this.startPosition.y); const dragThreshold = 5; // pixels // Only start dragging if moved more than threshold if (deltaX > dragThreshold || deltaY > dragThreshold) { this.isDragging = true; e.preventDefault(); this.position.x = clientX - this.dragOffset.x; this.position.y = clientY - this.dragOffset.y; // Keep button within viewport bounds const buttonSize = 60; const margin = 10; this.position.x = Math.max(margin, Math.min(window.innerWidth - buttonSize - margin, this.position.x)); this.position.y = Math.max(margin, Math.min(window.innerHeight - buttonSize - margin, this.position.y)); toggleButton.style.left = this.position.x + 'px'; toggleButton.style.top = this.position.y + 'px'; toggleButton.style.right = 'auto'; } } handleEnd(e) { const toggleButton = document.getElementById('theme-toggle'); toggleButton.classList.remove('dragging'); if (this.isDragging) { // It was a drag, save position and add bounce effect this.savePosition(); this.addBounceEffect(); // Prevent click event from firing after drag setTimeout(() => { this.isDragging = false; }, 50); } else { this.isDragging = false; } } savePosition() { localStorage.setItem('themeButtonPosition', JSON.stringify(this.position)); // Update CSS variables for immediate positioning on reload document.documentElement.style.setProperty('--theme-button-x', this.position.x + 'px'); document.documentElement.style.setProperty('--theme-button-y', this.position.y + 'px'); const toggleButton = document.getElementById('theme-toggle'); if (toggleButton) { toggleButton.classList.add('positioned'); } } loadPosition() { const savedPosition = localStorage.getItem('themeButtonPosition'); if (savedPosition) { this.position = JSON.parse(savedPosition); const toggleButton = document.getElementById('theme-toggle'); if (toggleButton) { // Position is already set by CSS variables, just ensure the class is added toggleButton.classList.add('positioned'); toggleButton.style.left = this.position.x + 'px'; toggleButton.style.top = this.position.y + 'px'; toggleButton.style.right = 'auto'; } } } snapToNearestCorner() { const toggleButton = document.getElementById('theme-toggle'); const buttonSize = 60; const margin = 20; const corners = [ { x: margin, y: margin }, // top-left { x: window.innerWidth - buttonSize - margin, y: margin }, // top-right { x: margin, y: window.innerHeight - buttonSize - margin }, // bottom-left { x: window.innerWidth - buttonSize - margin, y: window.innerHeight - buttonSize - margin } // bottom-right ]; // Find nearest corner let nearestCorner = corners[0]; let minDistance = Infinity; corners.forEach(corner => { const distance = Math.sqrt( Math.pow(corner.x - this.position.x, 2) + Math.pow(corner.y - this.position.y, 2) ); if (distance < minDistance) { minDistance = distance; nearestCorner = corner; } }); // Animate to nearest corner this.position = nearestCorner; toggleButton.style.transition = 'all 0.5s cubic-bezier(0.4, 0, 0.2, 1)'; toggleButton.style.left = nearestCorner.x + 'px'; toggleButton.style.top = nearestCorner.y + 'px'; setTimeout(() => { toggleButton.style.transition = ''; this.savePosition(); }, 500); } addBounceEffect() { const toggleButton = document.getElementById('theme-toggle'); toggleButton.classList.remove('bounce'); // Trigger reflow toggleButton.offsetHeight; toggleButton.classList.add('bounce'); setTimeout(() => toggleButton.classList.remove('bounce'), 600); } addPulseEffect() { const toggleButton = document.getElementById('theme-toggle'); toggleButton.classList.remove('pulse'); // Trigger reflow toggleButton.offsetHeight; toggleButton.classList.add('pulse'); setTimeout(() => toggleButton.classList.remove('pulse'), 1000); } updateToggleButton(theme) { const toggleButton = document.getElementById('theme-toggle'); if (toggleButton) { toggleButton.innerHTML = theme === 'dark' ? '☀️' : '🌙'; toggleButton.setAttribute('aria-label', `Switch to ${theme === 'dark' ? 'light' : 'dark'} mode`); toggleButton.setAttribute('title', `Click to switch to ${theme === 'dark' ? 'light' : 'dark'} mode • Drag to move • Double-click to snap to corner`); } } getCurrentTheme() { return document.documentElement.getAttribute('data-theme'); } } // Initialize theme manager when DOM is loaded document.addEventListener('DOMContentLoaded', () => { window.themeManager = new ThemeManager(); });