From 1c00421bd208dc300fe303e3b2f0a367977fe96d Mon Sep 17 00:00:00 2001 From: juanatsap Date: Wed, 12 Nov 2025 15:09:27 +0000 Subject: [PATCH] feat: add draggable zoom control with close button and menu toggle - Add close button (X) to zoom control widget - Make zoom control draggable anywhere on screen - Persist dragged position in localStorage (cv-zoom-position) - Add "Zoom" button to action bar to show control when hidden - Persist visibility state in localStorage (cv-zoom-visible) - Cursor changes to "move" to indicate draggability - Interactive elements (slider, buttons) don't trigger drag - Position stays within viewport bounds - All features work on desktop only (zoom hidden on mobile) --- static/css/main.css | 30 ++++++++- static/js/main.js | 146 ++++++++++++++++++++++++++++++++++++++++++- templates/index.html | 19 +++++- 3 files changed, 192 insertions(+), 3 deletions(-) diff --git a/static/css/main.css b/static/css/main.css index 22813df..e590987 100644 --- a/static/css/main.css +++ b/static/css/main.css @@ -3474,7 +3474,7 @@ html { .zoom-control { position: fixed; - bottom: 100px; /* Increased from 70px to clear footer */ + bottom: 100px; /* Default position, can be dragged */ left: 50%; transform: translateX(-50%); z-index: 900; @@ -3490,6 +3490,34 @@ html { transition: all 0.3s ease; opacity: 0.7; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif; + cursor: move; /* Indicate draggability */ + user-select: none; /* Prevent text selection while dragging */ +} + +/* Close button for zoom control */ +.zoom-close-btn { + position: absolute; + top: -8px; + right: -8px; + width: 24px; + height: 24px; + background: rgba(220, 53, 69, 0.9); + border: 2px solid rgba(255, 255, 255, 0.3); + border-radius: 50%; + color: white; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + padding: 0; + transition: all 0.2s ease; + z-index: 1; +} + +.zoom-close-btn:hover { + background: rgba(220, 53, 69, 1); + transform: scale(1.1); + box-shadow: 0 2px 8px rgba(220, 53, 69, 0.4); } .zoom-control:hover { diff --git a/static/js/main.js b/static/js/main.js index 597f1db..56a96c1 100644 --- a/static/js/main.js +++ b/static/js/main.js @@ -405,6 +405,144 @@ applyZoom(newZoom, true); } + /** + * Make zoom control draggable and persist position + */ + function initZoomDragging() { + const zoomControl = document.getElementById('zoom-control'); + if (!zoomControl || isMobileView()) return; + + let isDragging = false; + let currentX, currentY, initialX, initialY; + + // Restore saved position from localStorage + const savedPosition = localStorage.getItem('cv-zoom-position'); + if (savedPosition) { + const { bottom, left } = JSON.parse(savedPosition); + zoomControl.style.bottom = bottom; + zoomControl.style.left = left; + zoomControl.style.transform = 'none'; // Remove centering transform when positioned + } + + // Start drag on mousedown (but not on slider, close button, or reset button) + zoomControl.addEventListener('mousedown', function(e) { + // Ignore if clicking on interactive elements + if (e.target.closest('.zoom-slider, .zoom-close-btn, .zoom-reset-btn')) { + return; + } + + isDragging = true; + zoomControl.style.transition = 'none'; // Disable transitions during drag + + // Get current position + const rect = zoomControl.getBoundingClientRect(); + initialX = e.clientX - rect.left; + initialY = e.clientY - rect.top; + + e.preventDefault(); + }); + + // Drag on mousemove + document.addEventListener('mousemove', function(e) { + if (!isDragging) return; + + e.preventDefault(); + + currentX = e.clientX - initialX; + currentY = e.clientY - initialY; + + // Keep within viewport bounds + const maxX = window.innerWidth - zoomControl.offsetWidth; + const maxY = window.innerHeight - zoomControl.offsetHeight; + + currentX = Math.max(0, Math.min(currentX, maxX)); + currentY = Math.max(0, Math.min(currentY, maxY)); + + // Update position + zoomControl.style.left = currentX + 'px'; + zoomControl.style.bottom = (window.innerHeight - currentY - zoomControl.offsetHeight) + 'px'; + zoomControl.style.transform = 'none'; // Remove centering transform + }); + + // End drag on mouseup + document.addEventListener('mouseup', function() { + if (isDragging) { + isDragging = false; + zoomControl.style.transition = 'all 0.3s ease'; // Re-enable transitions + + // Save position to localStorage + const position = { + bottom: zoomControl.style.bottom, + left: zoomControl.style.left + }; + localStorage.setItem('cv-zoom-position', JSON.stringify(position)); + } + }); + } + + /** + * Hide zoom control and show menu button + */ + function hideZoomControl() { + const zoomControl = document.getElementById('zoom-control'); + const showButton = document.getElementById('show-zoom-btn'); + + if (zoomControl) { + zoomControl.style.display = 'none'; + localStorage.setItem('cv-zoom-visible', 'false'); + } + + if (showButton) { + showButton.style.display = 'inline-flex'; + } + } + + /** + * Show zoom control and hide menu button (global function for onclick) + */ + window.showZoomControl = function() { + const zoomControl = document.getElementById('zoom-control'); + const showButton = document.getElementById('show-zoom-btn'); + + if (zoomControl) { + zoomControl.style.display = 'flex'; + localStorage.setItem('cv-zoom-visible', 'true'); + } + + if (showButton) { + showButton.style.display = 'none'; + } + }; + + /** + * Initialize zoom visibility state from localStorage + */ + function initZoomVisibility() { + if (isMobileView()) return; // Always hidden on mobile + + const zoomControl = document.getElementById('zoom-control'); + const showButton = document.getElementById('show-zoom-btn'); + const isVisible = localStorage.getItem('cv-zoom-visible'); + + // Default to visible if not set + if (isVisible === 'false') { + if (zoomControl) zoomControl.style.display = 'none'; + if (showButton) showButton.style.display = 'inline-flex'; + } else { + if (zoomControl) zoomControl.style.display = 'flex'; + if (showButton) showButton.style.display = 'none'; + } + + // Setup close button + const closeBtn = document.getElementById('zoom-close'); + if (closeBtn) { + closeBtn.addEventListener('click', function(e) { + e.stopPropagation(); // Prevent drag from starting + hideZoomControl(); + }); + } + } + // ============================================================================= // PRINT & PDF // ============================================================================= @@ -518,8 +656,14 @@ window.toggleTheme(); } - // Initialize zoom control + // Initialize zoom control (zoom level, event listeners) initZoomControl(); + + // Initialize zoom visibility state (show/hide based on localStorage) + initZoomVisibility(); + + // Initialize zoom dragging (make draggable, restore position) + initZoomDragging(); } // ============================================================================= diff --git a/templates/index.html b/templates/index.html index 12bf383..6c530c2 100644 --- a/templates/index.html +++ b/templates/index.html @@ -174,6 +174,15 @@
+
- +
+ +