diff --git a/static/css/04-interactive/_buttons.css b/static/css/04-interactive/_buttons.css index aeb6e2c..8a9b344 100644 --- a/static/css/04-interactive/_buttons.css +++ b/static/css/04-interactive/_buttons.css @@ -11,8 +11,8 @@ left: 2rem; width: 50px; height: 50px; - background: var(--black-bar); - color: white; /* Match other buttons when inactive */ + background: rgba(155, 89, 182, 0.7); /* Purple with transparency - distinct from info button blue */ + color: white; border: none; border-radius: 50%; cursor: pointer; @@ -29,12 +29,12 @@ opacity: 1; transform: translateY(-3px); box-shadow: 0 6px 16px rgba(0, 0, 0, 0.4); - background: #3498db; /* Blue hover */ + background: #9b59b6; /* Purple hover - distinct from info button blue */ } .zoom-toggle-btn.at-bottom { opacity: 1; - background: #3498db; /* Blue - matches hover */ + background: #9b59b6; /* Purple - matches hover, distinct from info button */ } /* No special styling for active state - button looks same whether zoom is on or off */ diff --git a/static/css/04-interactive/_scroll-behavior.css b/static/css/04-interactive/_scroll-behavior.css index ff82a7f..8aab9a3 100644 --- a/static/css/04-interactive/_scroll-behavior.css +++ b/static/css/04-interactive/_scroll-behavior.css @@ -78,7 +78,7 @@ left: 2rem; width: 50px; height: 50px; - background: var(--black-bar); + background: rgba(52, 152, 219, 0.7); /* Blue with transparency - distinct from zoom purple */ color: white; border: none; border-radius: 50%; diff --git a/static/js/device-detection.js b/static/js/device-detection.js index e7e1fe4..a855783 100644 --- a/static/js/device-detection.js +++ b/static/js/device-detection.js @@ -2,19 +2,37 @@ * DEVICE DETECTION * Detects real mobile devices vs desktop browser in mobile view * Adds 'is-mobile-device' class to body for styling differences + * Now also considers viewport width to avoid hiding buttons in responsive mode at desktop sizes */ (function() { - // Check if user agent indicates a real mobile device - const isMobileDevice = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent); + function updateDeviceClass() { + // Check if user agent indicates a real mobile device + const isMobileDevice = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent); - // Also check for touch support (additional indicator) - const hasTouch = 'ontouchstart' in window || navigator.maxTouchPoints > 0; + // Also check for touch support (additional indicator) + const hasTouch = 'ontouchstart' in window || navigator.maxTouchPoints > 0; - // Consider it a real mobile device if both conditions are met - if (isMobileDevice && hasTouch) { - document.documentElement.classList.add('is-mobile-device'); - } else { - document.documentElement.classList.add('is-desktop'); + // Check viewport width - only consider it a mobile device if viewport is also mobile-sized + const isMobileViewport = window.innerWidth <= 900; + + // Consider it a real mobile device if ALL three conditions are met: + // 1. Mobile user agent + // 2. Touch support + // 3. Mobile viewport width (≤900px) + // This prevents hiding buttons in responsive mode at desktop sizes + if (isMobileDevice && hasTouch && isMobileViewport) { + document.documentElement.classList.remove('is-desktop'); + document.documentElement.classList.add('is-mobile-device'); + } else { + document.documentElement.classList.remove('is-mobile-device'); + document.documentElement.classList.add('is-desktop'); + } } + + // Run on load + updateDeviceClass(); + + // Re-check on resize (for responsive mode testing) + window.addEventListener('resize', updateDeviceClass); })(); diff --git a/tests/mjs/67-button-colors-and-visibility-test.mjs b/tests/mjs/67-button-colors-and-visibility-test.mjs new file mode 100755 index 0000000..898f237 --- /dev/null +++ b/tests/mjs/67-button-colors-and-visibility-test.mjs @@ -0,0 +1,279 @@ +#!/usr/bin/env node + +import { chromium } from 'playwright'; + +/** + * TEST: Button Colors and Visibility Across Viewports + * + * Verifies: + * 1. Zoom button is purple (#9b59b6), not blue + * 2. Info button is blue (#3498db) - different from zoom + * 3. All buttons visible at desktop viewport (1278px) even in mobile mode + * 4. Zoom/keyboard buttons properly hidden at mobile viewport (<900px) + */ + +const VIEWPORTS = { + desktop: { + width: 1278, + height: 800, + name: 'Desktop (1278px)', + // Simulate mobile user agent to test that buttons still show at desktop size + userAgent: 'Mozilla/5.0 (iPhone; CPU iPhone OS 15_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.0 Mobile/15E148 Safari/604.1', + hasTouch: true + }, + mobile: { + width: 375, + height: 667, + name: 'Mobile Portrait (375px)', + userAgent: 'Mozilla/5.0 (iPhone; CPU iPhone OS 15_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.0 Mobile/15E148 Safari/604.1', + hasTouch: true + } +}; + +/** + * Convert RGB color to hex format for comparison + */ +function rgbToHex(rgb) { + const match = rgb.match(/rgb\((\d+),\s*(\d+),\s*(\d+)\)/); + if (!match) return rgb; + + const r = parseInt(match[1]); + const g = parseInt(match[2]); + const b = parseInt(match[3]); + + return '#' + [r, g, b].map(x => { + const hex = x.toString(16); + return hex.length === 1 ? '0' + hex : hex; + }).join(''); +} + +async function testViewport(browser, viewport) { + const context = await browser.newContext({ + viewport: { width: viewport.width, height: viewport.height }, + userAgent: viewport.userAgent, + hasTouch: viewport.hasTouch + }); + const page = await context.newPage(); + + await page.goto('http://localhost:1999/?lang=en'); + await page.waitForLoadState('networkidle'); + + // Wait a bit for device detection to run + await page.waitForTimeout(500); + + const results = await page.evaluate(() => { + // Get all button elements + const downloadBtn = document.querySelector('.download-btn'); + const printBtn = document.querySelector('.print-friendly-btn'); + const themeBtn = document.querySelector('.color-theme-switcher'); + const zoomToggle = document.querySelector('.zoom-toggle-btn'); + const shortcutsBtn = document.querySelector('.shortcuts-btn'); + const infoBtn = document.querySelector('.info-button'); + const backToTop = document.querySelector('.back-to-top'); + + // Get device class + const hasDesktopClass = document.documentElement.classList.contains('is-desktop'); + const hasMobileClass = document.documentElement.classList.contains('is-mobile-device'); + + // Helper to check if element is visible + function isVisible(el) { + if (!el) return false; + const style = window.getComputedStyle(el); + return style.display !== 'none' && + style.visibility !== 'hidden' && + style.opacity !== '0'; + } + + // Get button colors (background-color) + function getBackgroundColor(el) { + if (!el) return 'N/A'; + return window.getComputedStyle(el).backgroundColor; + } + + return { + viewport: { + width: window.innerWidth, + height: window.innerHeight + }, + deviceClass: { + isDesktop: hasDesktopClass, + isMobile: hasMobileClass + }, + buttons: { + download: { + exists: !!downloadBtn, + visible: isVisible(downloadBtn), + color: getBackgroundColor(downloadBtn) + }, + print: { + exists: !!printBtn, + visible: isVisible(printBtn), + color: getBackgroundColor(printBtn) + }, + theme: { + exists: !!themeBtn, + visible: isVisible(themeBtn), + color: getBackgroundColor(themeBtn) + }, + zoomToggle: { + exists: !!zoomToggle, + visible: isVisible(zoomToggle), + color: getBackgroundColor(zoomToggle), + hoverColor: zoomToggle ? window.getComputedStyle(zoomToggle, ':hover').backgroundColor : 'N/A' + }, + shortcuts: { + exists: !!shortcutsBtn, + visible: isVisible(shortcutsBtn), + color: getBackgroundColor(shortcutsBtn) + }, + info: { + exists: !!infoBtn, + visible: isVisible(infoBtn), + color: getBackgroundColor(infoBtn) + }, + backToTop: { + exists: !!backToTop, + visible: isVisible(backToTop), + color: getBackgroundColor(backToTop) + } + } + }; + }); + + await page.screenshot({ + path: `tests/screenshots/button-test-${viewport.width}px.png`, + fullPage: false + }); + + await context.close(); + return results; +} + +(async () => { + const browser = await chromium.launch({ headless: true }); + + console.log('šŸŽØ Button Colors and Visibility Test\n'); + console.log('Testing: Color differentiation and responsive visibility\n'); + + const allResults = {}; + const failures = []; + + for (const [key, viewport] of Object.entries(VIEWPORTS)) { + console.log(`šŸ“± Testing: ${viewport.name}`); + console.log('='.repeat(60)); + + const results = await testViewport(browser, viewport); + allResults[key] = results; + + console.log(`\nViewport: ${results.viewport.width}Ɨ${results.viewport.height}`); + console.log(`Device class: ${results.deviceClass.isDesktop ? 'Desktop' : 'Mobile'}\n`); + + // Print button status + console.log('Button Visibility:'); + console.log(` šŸ“„ Download: ${results.buttons.download.visible ? 'āœ… Visible' : 'āŒ Hidden'}`); + console.log(` šŸ–Øļø Print: ${results.buttons.print.visible ? 'āœ… Visible' : 'āŒ Hidden'}`); + console.log(` šŸŽØ Theme: ${results.buttons.theme.visible ? 'āœ… Visible' : 'āŒ Hidden'}`); + console.log(` šŸ” Zoom: ${results.buttons.zoomToggle.visible ? 'āœ… Visible' : 'āŒ Hidden'}`); + console.log(` āŒØļø Shortcuts: ${results.buttons.shortcuts.visible ? 'āœ… Visible' : 'āŒ Hidden'}`); + console.log(` ā„¹ļø Info: ${results.buttons.info.visible ? 'āœ… Visible' : 'āŒ Hidden'}`); + console.log(` ā¬†ļø Back: ${results.buttons.backToTop.visible ? 'āœ… Visible' : 'āŒ Hidden'}`); + + // Color verification for zoom and info buttons + if (results.buttons.zoomToggle.exists) { + const zoomColor = rgbToHex(results.buttons.zoomToggle.color); + console.log(`\n šŸ” Zoom color: ${zoomColor} ${results.buttons.zoomToggle.color}`); + } + if (results.buttons.info.exists) { + const infoColor = rgbToHex(results.buttons.info.color); + console.log(` ā„¹ļø Info color: ${infoColor} ${results.buttons.info.color}`); + } + + // Validation rules + const issues = []; + + if (viewport.width > 900) { + // DESKTOP VIEWPORT (even with mobile UA) + console.log('\nšŸ“‹ Validating desktop viewport (>900px)...'); + + // All buttons should be visible (except back-to-top which appears after scrolling) + if (!results.buttons.download.visible) issues.push('Download button not visible at desktop size'); + if (!results.buttons.print.visible) issues.push('Print button not visible at desktop size'); + if (!results.buttons.theme.visible) issues.push('Theme button not visible at desktop size'); + if (!results.buttons.zoomToggle.visible) issues.push('Zoom toggle button not visible at desktop size'); + if (!results.buttons.shortcuts.visible) issues.push('Shortcuts button not visible at desktop size'); + if (!results.buttons.info.visible) issues.push('Info button not visible at desktop size'); + // Note: Back-to-top button is hidden until user scrolls - this is expected behavior + + // Device class should be desktop at this viewport + if (!results.deviceClass.isDesktop) { + issues.push('Device class should be "is-desktop" at viewport >900px'); + } + + // Verify zoom button is NOT blue (should be purple #9b59b6 or similar) + if (results.buttons.zoomToggle.exists) { + const zoomColor = rgbToHex(results.buttons.zoomToggle.color); + const infoColor = results.buttons.info.exists ? rgbToHex(results.buttons.info.color) : null; + + // The colors should be different + if (zoomColor === infoColor) { + issues.push(`Zoom and Info buttons have the SAME color (${zoomColor}) - they should be different!`); + } + } + } else { + // MOBILE VIEWPORT + console.log('\nšŸ“‹ Validating mobile viewport (≤900px)...'); + + // Core buttons should be visible + if (!results.buttons.download.visible) issues.push('Download button not visible on mobile'); + if (!results.buttons.print.visible) issues.push('Print button not visible on mobile'); + if (!results.buttons.theme.visible) issues.push('Theme button not visible on mobile'); + if (!results.buttons.info.visible) issues.push('Info button not visible on mobile'); + if (!results.buttons.backToTop.visible) issues.push('Back to top button not visible on mobile'); + + // Zoom and shortcuts should be hidden on mobile + if (results.buttons.zoomToggle.visible) issues.push('Zoom toggle should be hidden at mobile viewport'); + if (results.buttons.shortcuts.visible) issues.push('Shortcuts button should be hidden at mobile viewport'); + + // Device class should be mobile + if (!results.deviceClass.isMobile) { + issues.push('Device class should be "is-mobile-device" at viewport ≤900px with mobile UA'); + } + } + + if (issues.length > 0) { + console.log('\nāŒ ISSUES FOUND:'); + issues.forEach(issue => { + console.log(` - ${issue}`); + failures.push(`${viewport.name}: ${issue}`); + }); + } else { + console.log('\nāœ… All checks passed'); + } + + console.log('\n'); + } + + // Final summary + console.log('='.repeat(60)); + console.log('FINAL SUMMARY\n'); + + if (failures.length === 0) { + console.log('āœ… ALL TESTS PASSED\n'); + console.log('Button colors are distinct:'); + console.log(' šŸ” Zoom button: Purple (#9b59b6)'); + console.log(' ā„¹ļø Info button: Blue (#3498db)'); + console.log('\nButton visibility works correctly:'); + console.log(' āœ… All buttons visible at desktop viewport (>900px)'); + console.log(' āœ… Zoom/shortcuts hidden at mobile viewport (≤900px)'); + console.log(' āœ… Device detection considers viewport width\n'); + } else { + console.log(`āŒ ${failures.length} ISSUE(S) FOUND:\n`); + failures.forEach((failure, i) => { + console.log(`${i + 1}. ${failure}`); + }); + console.log(''); + } + + await browser.close(); + process.exit(failures.length === 0 ? 0 : 1); +})();