From 976b8ae2e28ed7b3ed760ba9e9a869e303cf6712 Mon Sep 17 00:00:00 2001 From: juanatsap Date: Mon, 1 Dec 2025 12:31:31 +0000 Subject: [PATCH] fix: Scale floating button icons proportionally on mobile viewports Remove hardcoded width/height HTML attributes from iconify-icon elements that were overriding CSS sizing. The iconify-icon component uses HTML attributes for SVG rendering, ignoring CSS width/height. - Remove width="28" height="28" from 8 button templates - Remove conflicting 768px media query from _buttons.css - Add default desktop icon sizes (24px) in _scroll-behavior.css - Icons now scale via clamp() from 18px (380px) to 24px (900px) --- static/css/04-interactive/_buttons.css | 96 +++++++- .../css/04-interactive/_scroll-behavior.css | 158 ++++++++++--- static/css/05-responsive/_breakpoints.css | 24 +- templates/partials/color-theme-switcher.html | 2 +- templates/partials/widgets/back-to-top.html | 2 +- templates/partials/widgets/cmd-k-button.html | 11 + .../partials/widgets/contact-button.html | 2 +- .../partials/widgets/download-button.html | 2 +- templates/partials/widgets/info-button.html | 2 +- .../widgets/print-friendly-button.html | 2 +- .../partials/widgets/shortcuts-button.html | 2 +- .../mjs/74-button-icon-fluid-sizing.test.mjs | 215 ++++++++++++++++++ tests/mjs/75-debug-button-icons.mjs | 137 +++++++++++ tests/mjs/76-visual-verification.mjs | 56 +++++ 14 files changed, 658 insertions(+), 53 deletions(-) create mode 100644 templates/partials/widgets/cmd-k-button.html create mode 100644 tests/mjs/74-button-icon-fluid-sizing.test.mjs create mode 100644 tests/mjs/75-debug-button-icons.mjs create mode 100644 tests/mjs/76-visual-verification.mjs diff --git a/static/css/04-interactive/_buttons.css b/static/css/04-interactive/_buttons.css index b3f6875..4c4528b 100644 --- a/static/css/04-interactive/_buttons.css +++ b/static/css/04-interactive/_buttons.css @@ -78,7 +78,7 @@ /* Print-Friendly Button (second from top) */ .print-friendly-btn { position: fixed; - bottom: 18rem; /* Below download button (22rem) */ + bottom: 22rem; /* Below download button (26rem) */ left: 2rem; width: 50px; height: 50px; @@ -127,7 +127,7 @@ /* Download Button (TOP POSITION) */ .download-btn { position: fixed; - bottom: 22rem; /* Top button position */ + bottom: 26rem; /* Top button position */ left: 2rem; width: 50px; height: 50px; @@ -172,13 +172,89 @@ background: #cd6060 !important; /* PDF red - matches hover */ } -/* Mobile adjustments */ -@media (max-width: 768px) { - .shortcuts-btn { - bottom: 5.5rem; /* Above back-to-top button (1.5rem + 45px + gap) */ - left: 1.5rem; /* LEFT SIDE on mobile too */ - width: 45px; - height: 45px; - } +/* CMD+K Command Bar Button (TOP position - first button) */ +.cmd-k-btn { + position: fixed; + bottom: 30rem; /* TOP position - above download button */ + left: 2rem; /* Left side */ + width: 50px; + height: 50px; + background: var(--black-bar, #2b2b2b); + color: white; + border: none; + border-radius: 50%; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3); + transition: all 0.3s ease; + z-index: 999; + opacity: 0.6; +} + +.cmd-k-btn:hover { + opacity: 1; + transform: translateY(-3px); + box-shadow: 0 6px 16px rgba(0, 0, 0, 0.4); + background: #00897b; /* Teal - distinct from zoom button purple */ +} + +.cmd-k-btn.at-bottom { + opacity: 1; + background: #00897b; /* Teal - distinct from zoom button purple */ +} + +.cmd-k-btn:active { + transform: translateY(-1px); +} + +/* Mobile adjustments - now handled by fluid sizing in _scroll-behavior.css + The 900px media query there uses clamp() for smooth scaling of all buttons and icons */ + + +/* ============================================================================= + NINJA-KEYS COMMAND BAR STYLING + ============================================================================= */ + +ninja-keys { + --ninja-font-family: 'Quicksand', -apple-system, BlinkMacSystemFont, sans-serif; + --ninja-accent-color: #667eea; + --ninja-z-index: 10000; + --ninja-width: 640px; + --ninja-backdrop-filter: blur(8px); + + /* Light mode colors */ + --ninja-modal-background: rgba(255, 255, 255, 0.95); + --ninja-modal-shadow: 0 16px 70px rgba(0, 0, 0, 0.2); + --ninja-text-color: #1a1a1a; + --ninja-secondary-text-color: #666; + --ninja-actions-background: #f5f5f5; + --ninja-selected-background: #667eea; + --ninja-selected-text-color: white; + --ninja-key-background: #e0e0e0; + --ninja-key-text-color: #333; + --ninja-footer-background: #f9f9f9; + --ninja-placeholder-color: #999; +} + +/* Dark mode colors */ +[data-color-theme="dark"] ninja-keys { + --ninja-modal-background: rgba(40, 40, 40, 0.95); + --ninja-text-color: #e0e0e0; + --ninja-secondary-text-color: #999; + --ninja-actions-background: #2a2a2a; + --ninja-key-background: #444; + --ninja-key-text-color: #e0e0e0; + --ninja-footer-background: #2a2a2a; + --ninja-placeholder-color: #777; +} + +/* Shortcut highlight in shortcuts modal */ +.shortcut-highlight { + background: linear-gradient(135deg, rgba(102, 126, 234, 0.1) 0%, rgba(118, 75, 162, 0.1) 100%); + border-radius: 8px; + padding: 0.5rem; + margin: -0.5rem; } diff --git a/static/css/04-interactive/_scroll-behavior.css b/static/css/04-interactive/_scroll-behavior.css index 03ea11a..74ca6a4 100644 --- a/static/css/04-interactive/_scroll-behavior.css +++ b/static/css/04-interactive/_scroll-behavior.css @@ -109,6 +109,20 @@ box-shadow: 0 3px 10px rgba(0, 0, 0, 0.3); } +/* Default icon sizes for floating buttons (desktop) */ +.cmd-k-btn iconify-icon, +.download-btn iconify-icon, +.print-friendly-btn iconify-icon, +.fixed-btn.contact-btn iconify-icon, +.shortcuts-btn iconify-icon, +.info-button iconify-icon, +.back-to-top iconify-icon, +.color-theme-switcher iconify-icon { + width: 24px; + height: 24px; + font-size: 24px; +} + /* Hide keyboard shortcuts button on real mobile devices (no keyboard) */ .is-mobile-device .shortcuts-btn, .is-mobile-device .zoom-toggle-btn, @@ -125,21 +139,56 @@ } /* Reset fixed positioning for FLEXBOX buttons on mobile (exclude back-to-top) */ + /* Use fluid sizing: at 900px = 50px, at 380px = 36px, scales linearly */ + /* Formula: 2.7vw + 25.7px gives 36px at 380px and 50px at 900px */ + .cmd-k-btn, .download-btn, .print-friendly-btn, + .fixed-btn.contact-btn, .shortcuts-btn, .info-button { position: fixed !important; bottom: 1.5rem !important; left: auto !important; right: auto !important; - width: 50px !important; - height: 50px !important; + /* Fluid button size: 36px at 380px, 50px at 900px */ + width: clamp(36px, calc(2.7vw + 25.7px), 50px) !important; + height: clamp(36px, calc(2.7vw + 25.7px), 50px) !important; /* Removed opacity: 1 !important to allow .footer-hovered to work */ transform: none !important; } + /* Also apply fluid sizing to back-to-top */ + .back-to-top { + width: clamp(36px, calc(2.7vw + 25.7px), 50px) !important; + height: clamp(36px, calc(2.7vw + 25.7px), 50px) !important; + } + + /* Scale icons to fit buttons properly */ + /* Icons ~50% of button size: 18px at 36px button, 24px at 50px button */ + /* Formula: 1.15vw + 13.6px gives 18px at 380px and 24px at 900px */ + .cmd-k-btn iconify-icon, + .download-btn iconify-icon, + .print-friendly-btn iconify-icon, + .fixed-btn.contact-btn iconify-icon, + .shortcuts-btn iconify-icon, + .info-button iconify-icon, + .back-to-top iconify-icon, + .color-theme-switcher iconify-icon { + width: clamp(18px, calc(1.15vw + 13.6px), 24px) !important; + height: clamp(18px, calc(1.15vw + 13.6px), 24px) !important; + font-size: clamp(18px, calc(1.15vw + 13.6px), 24px) !important; + /* Force override HTML width/height attributes */ + min-width: 0 !important; + max-width: clamp(18px, calc(1.15vw + 13.6px), 24px) !important; + } + /* Mobile: Show colors at full opacity (no transparency with blur bar) */ + .cmd-k-btn { + background: rgba(0, 137, 123, 1) !important; /* Teal - full opacity */ + opacity: 1 !important; /* Override base opacity */ + } + .download-btn { background: rgba(205, 96, 96, 1) !important; /* PDF red - full opacity */ opacity: 1 !important; /* Override base opacity */ @@ -154,6 +203,11 @@ color: #27ae60 !important; /* Green icon */ } + .fixed-btn.contact-btn { + background: rgba(52, 152, 219, 1) !important; /* Blue - full opacity */ + opacity: 1 !important; /* Override base opacity */ + } + .shortcuts-btn { background: rgba(243, 156, 18, 1) !important; /* Orange - full opacity */ opacity: 1 !important; /* Override base opacity */ @@ -170,63 +224,82 @@ } /* Flexbox container behavior - buttons arrange themselves */ - /* Buttons will be positioned using JavaScript or individual positioning */ - /* For now, use fixed spacing from center */ + /* Fluid positioning that scales with viewport */ + /* At 900px: 8 * 50px + 7 * 8px = 456px total, start at -228px */ + /* At 380px: 8 * 36px + 7 * 4px = 316px total, start at -158px */ - /* 6 buttons: Download, Print, Shortcuts, Theme, Info, Back-to-top */ - /* Spacing: 10px gap between buttons, centered horizontally */ - /* Total width: 6 * 50px + 5 * 10px = 350px */ - /* Start position: 50% - 175px */ + .cmd-k-btn { + /* First button: -228px at 900px, -158px at 380px */ + left: calc(50% - clamp(158px, calc(158px + (228 - 158) * ((100vw - 380px) / (900 - 380))), 228px)) !important; + } .download-btn { - left: calc(50% - 175px) !important; /* First button */ + /* Second button: -170px at 900px, -118px at 380px */ + left: calc(50% - clamp(118px, calc(118px + (170 - 118) * ((100vw - 380px) / (900 - 380))), 170px)) !important; } .print-friendly-btn { - left: calc(50% - 115px) !important; /* Second button */ + /* Third button: -112px at 900px, -78px at 380px */ + left: calc(50% - clamp(78px, calc(78px + (112 - 78) * ((100vw - 380px) / (900 - 380))), 112px)) !important; + } + + .fixed-btn.contact-btn { + /* Fourth button: -54px at 900px, -38px at 380px */ + left: calc(50% - clamp(38px, calc(38px + (54 - 38) * ((100vw - 380px) / (900 - 380))), 54px)) !important; } .shortcuts-btn { - left: calc(50% - 55px) !important; /* Third button */ + /* Fifth button: +4px at 900px, +2px at 380px */ + left: calc(50% + clamp(2px, calc(2px + (4 - 2) * ((100vw - 380px) / (900 - 380))), 4px)) !important; } - /* Theme switcher button - fourth position (defined in _themes.css) */ - /* left: calc(50% + 5px) !important; */ + /* Theme switcher button - sixth position (defined in _themes.css) */ + /* +62px at 900px, +42px at 380px */ .info-button { - left: calc(50% + 65px) !important; /* Fifth button */ + /* Seventh button: +120px at 900px, +82px at 380px */ + left: calc(50% + clamp(82px, calc(82px + (120 - 82) * ((100vw - 380px) / (900 - 380))), 120px)) !important; } - /* Back-to-top button - now part of the button row (sixth button) */ + /* Back-to-top button - now part of the button row (eighth button) */ .back-to-top { position: fixed !important; bottom: 1.5rem !important; - left: calc(50% + 125px) !important; /* Sixth button (last) */ + /* Eighth button: +178px at 900px, +122px at 380px */ + left: calc(50% + clamp(122px, calc(122px + (178 - 122) * ((100vw - 380px) / (900 - 380))), 178px)) !important; right: auto !important; /* Override previous right positioning */ - width: 50px !important; - height: 50px !important; - /* Removed fixed opacity - will be controlled by .at-bottom class */ + /* Fluid size already set above, no need to repeat */ display: flex !important; /* Ensure it's always displayed */ } - /* REAL MOBILE DEVICES: 5 buttons without shortcuts (no gap) */ - /* Total width: 5 * 50px + 4 * 10px = 290px, start at 50% - 145px */ + /* REAL MOBILE DEVICES: 7 buttons without shortcuts */ + /* At 900px: 7 * 50px + 6 * 8px = 398px, start at -199px */ + /* At 380px: 7 * 36px + 6 * 4px = 276px, start at -138px */ + .is-mobile-device .cmd-k-btn { + left: calc(50% - clamp(138px, calc(138px + (199 - 138) * ((100vw - 380px) / (900 - 380))), 199px)) !important; + } + .is-mobile-device .download-btn { - left: calc(50% - 145px) !important; /* First button */ + left: calc(50% - clamp(98px, calc(98px + (141 - 98) * ((100vw - 380px) / (900 - 380))), 141px)) !important; } .is-mobile-device .print-friendly-btn { - left: calc(50% - 85px) !important; /* Second button */ + left: calc(50% - clamp(58px, calc(58px + (83 - 58) * ((100vw - 380px) / (900 - 380))), 83px)) !important; } - /* Theme switcher on mobile - third position (see _themes.css for override) */ + .is-mobile-device .fixed-btn.contact-btn { + left: calc(50% - clamp(18px, calc(18px + (25 - 18) * ((100vw - 380px) / (900 - 380))), 25px)) !important; + } + + /* Theme switcher on mobile - fifth position (see _themes.css for override) */ + /* +33px at 900px, +22px at 380px */ .is-mobile-device .info-button { - left: calc(50% + 35px) !important; /* Fourth button */ + left: calc(50% + clamp(62px, calc(62px + (91 - 62) * ((100vw - 380px) / (900 - 380))), 91px)) !important; } .is-mobile-device .back-to-top { - left: calc(50% + 95px) !important; /* Fifth button (last) */ + left: calc(50% + clamp(102px, calc(102px + (149 - 102) * ((100vw - 380px) / (900 - 380))), 149px)) !important; } /* Always show back-to-top on mobile (don't wait for scroll) */ @@ -235,6 +308,12 @@ } /* Hover effects - Full color opacity on hover */ + .cmd-k-btn:hover { + background: rgba(0, 137, 123, 1) !important; /* Full teal opacity */ + transform: translateY(-3px) !important; + box-shadow: 0 6px 16px rgba(0, 0, 0, 0.4) !important; + } + .download-btn:hover, .download-btn.pdf-hover-sync { background: rgba(205, 96, 96, 1) !important; /* Full red opacity */ @@ -249,6 +328,12 @@ box-shadow: 0 6px 16px rgba(0, 0, 0, 0.4) !important; } + .fixed-btn.contact-btn:hover { + background: rgba(52, 152, 219, 1) !important; /* Full blue opacity */ + transform: translateY(-3px) !important; + box-shadow: 0 6px 16px rgba(0, 0, 0, 0.4) !important; + } + .shortcuts-btn:hover { background: rgba(243, 156, 18, 1) !important; /* Full orange opacity */ transform: translateY(-3px) !important; @@ -268,6 +353,12 @@ } /* Keep at-bottom state - full opacity colors for each button */ + .cmd-k-btn.at-bottom { + background: rgba(0, 137, 123, 1) !important; /* Full teal opacity */ + opacity: 1 !important; + transform: none !important; + } + .download-btn.at-bottom { background: rgba(205, 96, 96, 1) !important; /* Full red opacity */ opacity: 1 !important; @@ -280,6 +371,12 @@ transform: none !important; } + .fixed-btn.contact-btn.at-bottom { + background: rgba(52, 152, 219, 1) !important; /* Full blue opacity */ + opacity: 1 !important; + transform: none !important; + } + .shortcuts-btn.at-bottom { background: rgba(243, 156, 18, 1) !important; /* Full orange opacity */ opacity: 1 !important; @@ -299,8 +396,10 @@ } /* Make all buttons semi-transparent when footer is hovered (applied via JS) */ + .cmd-k-btn.footer-hovered, .download-btn.footer-hovered, .print-friendly-btn.footer-hovered, + .fixed-btn.contact-btn.footer-hovered, .shortcuts-btn.footer-hovered, .info-button.footer-hovered, .back-to-top.footer-hovered, @@ -310,13 +409,6 @@ } } -/* Very narrow mobile - Stack buttons vertically or adjust spacing if needed */ -@media (max-width: 483px) { - /* For very narrow screens, you may need to adjust button spacing or size */ - /* For now, keep the same horizontal layout but buttons might overflow slightly */ - /* Users can scroll horizontally if needed, or we can reduce button sizes */ -} - /* ======================================== Mobile: Keep action bar visible (prevent hiding on scroll) ======================================== */ diff --git a/static/css/05-responsive/_breakpoints.css b/static/css/05-responsive/_breakpoints.css index f412c23..dbe1641 100644 --- a/static/css/05-responsive/_breakpoints.css +++ b/static/css/05-responsive/_breakpoints.css @@ -1030,18 +1030,36 @@ visibility: visible !important; } - /* Adjust bottom button bar for landscape - smaller buttons */ + /* Adjust bottom button bar for landscape - fluid sizing + Buttons: clamp(32px, calc(2.2vw + 19.6px), 40px) - scales from 32px at 380px to 40px at 915px + Icons: clamp(16px, calc(1.1vw + 9.8px), 20px) - scales from 16px at 380px to 20px at 915px */ + .cmd-k-btn, .download-btn, .print-friendly-btn, + .fixed-btn.contact-btn, .shortcuts-btn, .info-button, .back-to-top, .color-theme-switcher { - width: 40px !important; - height: 40px !important; + width: clamp(32px, calc(2.2vw + 19.6px), 40px) !important; + height: clamp(32px, calc(2.2vw + 19.6px), 40px) !important; bottom: 1rem !important; } + /* Scale icons proportionally for landscape buttons */ + .cmd-k-btn iconify-icon, + .download-btn iconify-icon, + .print-friendly-btn iconify-icon, + .fixed-btn.contact-btn iconify-icon, + .shortcuts-btn iconify-icon, + .info-button iconify-icon, + .back-to-top iconify-icon, + .color-theme-switcher iconify-icon { + width: clamp(16px, calc(1.1vw + 9.8px), 20px) !important; + height: clamp(16px, calc(1.1vw + 9.8px), 20px) !important; + font-size: clamp(16px, calc(1.1vw + 9.8px), 20px) !important; + } + /* Recalculate button positions for smaller 40px buttons */ /* 6 buttons: 6 * 40px + 5 * 10px = 290px total */ .download-btn { diff --git a/templates/partials/color-theme-switcher.html b/templates/partials/color-theme-switcher.html index f9eb7a6..d2e47f8 100644 --- a/templates/partials/color-theme-switcher.html +++ b/templates/partials/color-theme-switcher.html @@ -5,7 +5,7 @@ class="color-theme-switcher no-print has-tooltip" aria-label="Toggle color theme" data-tooltip="Toggle color theme"> - + diff --git a/templates/partials/widgets/back-to-top.html b/templates/partials/widgets/back-to-top.html index 0903074..7bb3521 100644 --- a/templates/partials/widgets/back-to-top.html +++ b/templates/partials/widgets/back-to-top.html @@ -6,6 +6,6 @@ data-tooltip="{{.UI.Widgets.BackToTop.Tooltip}}" style="display: none;" _="on click call scrollToTop(event)"> - + {{end}} diff --git a/templates/partials/widgets/cmd-k-button.html b/templates/partials/widgets/cmd-k-button.html new file mode 100644 index 0000000..cfe9649 --- /dev/null +++ b/templates/partials/widgets/cmd-k-button.html @@ -0,0 +1,11 @@ +{{define "cmd-k-button"}} + + +{{end}} diff --git a/templates/partials/widgets/contact-button.html b/templates/partials/widgets/contact-button.html index 7800178..edc6821 100644 --- a/templates/partials/widgets/contact-button.html +++ b/templates/partials/widgets/contact-button.html @@ -6,6 +6,6 @@ onclick="document.getElementById('contact-modal').showModal()" aria-label="{{.UI.Widgets.Contact.AriaLabel}}" data-tooltip="{{.UI.Widgets.Contact.Tooltip}}"> - + {{end}} diff --git a/templates/partials/widgets/download-button.html b/templates/partials/widgets/download-button.html index 66d5b8b..7b94465 100644 --- a/templates/partials/widgets/download-button.html +++ b/templates/partials/widgets/download-button.html @@ -8,6 +8,6 @@ onclick="openPdfModal()" _="on mouseenter call syncPdfHover(true) on mouseleave call syncPdfHover(false)"> - + {{end}} diff --git a/templates/partials/widgets/info-button.html b/templates/partials/widgets/info-button.html index f60c032..faede72 100644 --- a/templates/partials/widgets/info-button.html +++ b/templates/partials/widgets/info-button.html @@ -4,6 +4,6 @@ aria-label="{{.UI.Widgets.Info.AriaLabel}}" data-tooltip="{{.UI.Widgets.Info.Tooltip}}" onclick="document.getElementById('info-modal').showModal()"> - + {{end}} diff --git a/templates/partials/widgets/print-friendly-button.html b/templates/partials/widgets/print-friendly-button.html index ad2fcd1..9efe667 100644 --- a/templates/partials/widgets/print-friendly-button.html +++ b/templates/partials/widgets/print-friendly-button.html @@ -8,6 +8,6 @@ onclick="window.print()" _="on mouseenter call syncPrintHover(true) on mouseleave call syncPrintHover(false)"> - + {{end}} diff --git a/templates/partials/widgets/shortcuts-button.html b/templates/partials/widgets/shortcuts-button.html index 09af3cb..73b3fd8 100644 --- a/templates/partials/widgets/shortcuts-button.html +++ b/templates/partials/widgets/shortcuts-button.html @@ -6,6 +6,6 @@ onclick="document.getElementById('shortcuts-modal').showModal()" aria-label="{{.UI.Widgets.Shortcuts.AriaLabel}}" data-tooltip="{{.UI.Widgets.Shortcuts.Tooltip}}"> - + {{end}} diff --git a/tests/mjs/74-button-icon-fluid-sizing.test.mjs b/tests/mjs/74-button-icon-fluid-sizing.test.mjs new file mode 100644 index 0000000..86f2c19 --- /dev/null +++ b/tests/mjs/74-button-icon-fluid-sizing.test.mjs @@ -0,0 +1,215 @@ +#!/usr/bin/env node +/** + * Test: Button and Icon Fluid Sizing + * + * Verifies that on mobile view (max-width: 900px): + * - Buttons scale fluidly from 36px (at 380px viewport) to 50px (at 900px viewport) + * - Icons scale proportionally from 18px to 24px + * - Icons properly override HTML width/height attributes + */ + +import { chromium } from 'playwright'; + +const TEST_URL = 'http://localhost:1999'; +// Use height > width to ensure portrait orientation (avoid landscape breakpoint) +const VIEWPORT_HEIGHT = 1000; + +// Expected sizes at different viewport widths based on CSS formulas: +// Buttons: clamp(36px, calc(2.7vw + 25.7px), 50px) +// Icons: clamp(18px, calc(1.15vw + 13.6px), 24px) +function expectedSizes(viewportWidth) { + const btnCalc = 0.027 * viewportWidth + 25.7; + const iconCalc = 0.0115 * viewportWidth + 13.6; + return { + button: Math.min(50, Math.max(36, btnCalc)), + icon: Math.min(24, Math.max(18, iconCalc)) + }; +} + +async function testButtonIconFluidSizing() { + console.log('๐Ÿงช Testing Button and Icon Fluid Sizing'); + console.log('='.repeat(70)); + + const browser = await chromium.launch({ headless: true }); + const context = await browser.newContext({ + viewport: { width: 900, height: VIEWPORT_HEIGHT }, + deviceScaleFactor: 1, + }); + const page = await context.newPage(); + + // Disable cache + await page.route('**/*', (route) => { + route.continue({ + headers: { + ...route.request().headers(), + 'Cache-Control': 'no-cache, no-store, must-revalidate', + }, + }); + }); + + try { + await page.goto(TEST_URL, { waitUntil: 'networkidle' }); + console.log(`โœ… Navigated to ${TEST_URL}`); + await page.waitForTimeout(500); + + const viewportWidths = [900, 700, 500, 380]; + const buttons = [ + { selector: '.cmd-k-btn', name: 'CmdK' }, + { selector: '.download-btn', name: 'Download' }, + { selector: '.print-friendly-btn', name: 'Print' }, + { selector: '.fixed-btn.contact-btn', name: 'Contact' }, + { selector: '.shortcuts-btn', name: 'Shortcuts' }, + { selector: '.color-theme-switcher', name: 'Theme' }, + { selector: '.info-button', name: 'Info' }, + { selector: '.back-to-top', name: 'BackToTop' }, + ]; + + let allTestsPassed = true; + const tolerance = 4; // Allow 4px tolerance for rounding + + for (const width of viewportWidths) { + await page.setViewportSize({ width, height: VIEWPORT_HEIGHT }); + await page.waitForTimeout(300); + + const expected = expectedSizes(width); + console.log(`\n๐Ÿ“ Viewport: ${width}px (expected button: ~${Math.round(expected.button)}px, icon: ~${Math.round(expected.icon)}px)`); + console.log('-'.repeat(70)); + + for (const btn of buttons) { + try { + const buttonElement = await page.$(btn.selector); + if (!buttonElement) { + // Some buttons may be hidden on certain viewports + continue; + } + + const isVisible = await buttonElement.isVisible(); + if (!isVisible) { + continue; + } + + // Get button bounding box + const btnBox = await buttonElement.boundingBox(); + if (!btnBox) continue; + + const btnWidth = Math.round(btnBox.width); + const btnHeight = Math.round(btnBox.height); + + // Get icon inside button + const iconElement = await buttonElement.$('iconify-icon'); + let iconWidth = 0; + let iconHeight = 0; + + if (iconElement) { + const iconBox = await iconElement.boundingBox(); + if (iconBox) { + iconWidth = Math.round(iconBox.width); + iconHeight = Math.round(iconBox.height); + } + } + + // Check if sizes are within tolerance of expected + const btnOk = Math.abs(btnWidth - expected.button) <= tolerance; + const iconOk = iconWidth === 0 || Math.abs(iconWidth - expected.icon) <= tolerance; + + const status = btnOk && iconOk ? 'โœ…' : 'โŒ'; + + if (btnOk && iconOk) { + console.log(`${status} ${btn.name}: button=${btnWidth}x${btnHeight}, icon=${iconWidth}x${iconHeight}`); + } else { + console.log(`${status} ${btn.name}: button=${btnWidth}x${btnHeight} (expected ~${Math.round(expected.button)}), icon=${iconWidth}x${iconHeight} (expected ~${Math.round(expected.icon)})`); + allTestsPassed = false; + } + + } catch (error) { + // Button may not exist + } + } + } + + // Test landscape orientation (width > height) + console.log('\n\n๐Ÿ“ LANDSCAPE ORIENTATION TESTS'); + console.log('='.repeat(70)); + + // Landscape viewports: width > height triggers landscape media query + // Note: At 400x400, width == height so CSS treats it as portrait, not landscape + const landscapeViewports = [ + { width: 800, height: 400 }, // Clear landscape + { width: 600, height: 350 }, // Clear landscape + { width: 500, height: 300 }, // Clear landscape + ]; + + // Landscape formula: clamp(32px, calc(2.2vw + 19.6px), 40px) for buttons + // clamp(16px, calc(1.1vw + 9.8px), 20px) for icons + function expectedLandscapeSizes(viewportWidth) { + const btnCalc = 0.022 * viewportWidth + 19.6; + const iconCalc = 0.011 * viewportWidth + 9.8; + return { + button: Math.min(40, Math.max(32, btnCalc)), + icon: Math.min(20, Math.max(16, iconCalc)) + }; + } + + for (const viewport of landscapeViewports) { + await page.setViewportSize({ width: viewport.width, height: viewport.height }); + await page.waitForTimeout(300); + + const expected = expectedLandscapeSizes(viewport.width); + console.log(`\n๐Ÿ“ Landscape ${viewport.width}x${viewport.height} (expected button: ~${Math.round(expected.button)}px, icon: ~${Math.round(expected.icon)}px)`); + console.log('-'.repeat(70)); + + for (const btn of buttons) { + try { + const buttonElement = await page.$(btn.selector); + if (!buttonElement) continue; + const isVisible = await buttonElement.isVisible(); + if (!isVisible) continue; + + const btnBox = await buttonElement.boundingBox(); + if (!btnBox) continue; + + const btnWidth = Math.round(btnBox.width); + const btnHeight = Math.round(btnBox.height); + + const iconElement = await buttonElement.$('iconify-icon'); + let iconWidth = 0; + if (iconElement) { + const iconBox = await iconElement.boundingBox(); + if (iconBox) iconWidth = Math.round(iconBox.width); + } + + const btnOk = Math.abs(btnWidth - expected.button) <= tolerance; + const iconOk = iconWidth === 0 || Math.abs(iconWidth - expected.icon) <= tolerance; + const status = btnOk && iconOk ? 'โœ…' : 'โŒ'; + + if (btnOk && iconOk) { + console.log(`${status} ${btn.name}: button=${btnWidth}x${btnHeight}, icon=${iconWidth}`); + } else { + console.log(`${status} ${btn.name}: button=${btnWidth}x${btnHeight} (expected ~${Math.round(expected.button)}), icon=${iconWidth} (expected ~${Math.round(expected.icon)})`); + allTestsPassed = false; + } + } catch (error) {} + } + } + + console.log('\n' + '='.repeat(70)); + + if (allTestsPassed) { + console.log('\nโœ… ALL TESTS PASSED - Button and icon fluid sizing working correctly!'); + console.log(' โ€ข Portrait: Buttons 36-50px, Icons 18-24px'); + console.log(' โ€ข Landscape: Buttons 32-40px, Icons 16-20px'); + } else { + console.log('\nโŒ SOME TESTS FAILED - Check output above'); + } + + await browser.close(); + process.exit(allTestsPassed ? 0 : 1); + + } catch (error) { + console.error('\nโŒ Test error:', error); + await browser.close(); + process.exit(1); + } +} + +testButtonIconFluidSizing(); diff --git a/tests/mjs/75-debug-button-icons.mjs b/tests/mjs/75-debug-button-icons.mjs new file mode 100644 index 0000000..78513e2 --- /dev/null +++ b/tests/mjs/75-debug-button-icons.mjs @@ -0,0 +1,137 @@ +#!/usr/bin/env node +/** + * Debug: Check actual computed styles for buttons and icons + */ + +import { chromium } from 'playwright'; + +const TEST_URL = 'http://localhost:1999'; + +async function debugButtonIcons() { + console.log('๐Ÿ” Debugging Button and Icon Actual Computed Styles'); + console.log('='.repeat(70)); + + const browser = await chromium.launch({ headless: true }); + const context = await browser.newContext({ + viewport: { width: 375, height: 812 }, // iPhone X + deviceScaleFactor: 2, + }); + const page = await context.newPage(); + + await page.goto(TEST_URL, { waitUntil: 'networkidle' }); + await page.waitForTimeout(1000); + + console.log('\n๐Ÿ“ฑ Viewport: 375x812 (iPhone X portrait)\n'); + + const buttons = [ + '.cmd-k-btn', + '.download-btn', + '.print-friendly-btn', + '.fixed-btn.contact-btn', + '.shortcuts-btn', + '.color-theme-switcher', + '.info-button', + '.back-to-top', + ]; + + for (const selector of buttons) { + try { + const result = await page.evaluate((sel) => { + const btn = document.querySelector(sel); + if (!btn) return null; + + const btnStyles = window.getComputedStyle(btn); + const icon = btn.querySelector('iconify-icon'); + + let iconInfo = null; + if (icon) { + const iconStyles = window.getComputedStyle(icon); + iconInfo = { + width: iconStyles.width, + height: iconStyles.height, + fontSize: iconStyles.fontSize, + // Check HTML attributes + htmlWidth: icon.getAttribute('width'), + htmlHeight: icon.getAttribute('height'), + // Check inline style + inlineStyle: icon.getAttribute('style'), + }; + } + + return { + selector: sel, + button: { + width: btnStyles.width, + height: btnStyles.height, + }, + icon: iconInfo, + }; + }, selector); + + if (result) { + console.log(`${selector}:`); + console.log(` Button: ${result.button.width} x ${result.button.height}`); + if (result.icon) { + console.log(` Icon computed: ${result.icon.width} x ${result.icon.height} (font-size: ${result.icon.fontSize})`); + console.log(` Icon HTML attrs: width="${result.icon.htmlWidth}" height="${result.icon.htmlHeight}"`); + if (result.icon.inlineStyle) { + console.log(` Icon inline style: ${result.icon.inlineStyle}`); + } + } + console.log(''); + } + } catch (e) { + console.log(`${selector}: Error - ${e.message}\n`); + } + } + + // Also check what CSS rules are being applied + console.log('\n๐Ÿ“‹ Checking CSS rule specificity for iconify-icon...\n'); + + const cssInfo = await page.evaluate(() => { + const icon = document.querySelector('.download-btn iconify-icon'); + if (!icon) return 'No icon found'; + + // Get all stylesheets and find rules for iconify-icon + const sheets = [...document.styleSheets]; + const rules = []; + + for (const sheet of sheets) { + try { + const cssRules = [...sheet.cssRules]; + for (const rule of cssRules) { + if (rule.selectorText && rule.selectorText.includes('iconify-icon')) { + rules.push({ + selector: rule.selectorText, + width: rule.style.width, + height: rule.style.height, + fontSize: rule.style.fontSize, + }); + } + } + } catch (e) { + // CORS restrictions on external stylesheets + } + } + + return rules; + }); + + console.log('CSS rules targeting iconify-icon:'); + if (Array.isArray(cssInfo)) { + for (const rule of cssInfo) { + if (rule.width || rule.height || rule.fontSize) { + console.log(` ${rule.selector}`); + if (rule.width) console.log(` width: ${rule.width}`); + if (rule.height) console.log(` height: ${rule.height}`); + if (rule.fontSize) console.log(` font-size: ${rule.fontSize}`); + } + } + } else { + console.log(cssInfo); + } + + await browser.close(); +} + +debugButtonIcons().catch(console.error); diff --git a/tests/mjs/76-visual-verification.mjs b/tests/mjs/76-visual-verification.mjs new file mode 100644 index 0000000..e8537cd --- /dev/null +++ b/tests/mjs/76-visual-verification.mjs @@ -0,0 +1,56 @@ +import { chromium } from 'playwright'; + +const browser = await chromium.launch(); +const context = await browser.newContext({ + viewport: { width: 375, height: 812 } +}); +const page = await context.newPage(); + +await page.goto('http://localhost:1999'); +await page.waitForTimeout(1500); + +// Scroll to show back-to-top button +await page.evaluate(() => window.scrollTo(0, 500)); +await page.waitForTimeout(500); + +// Take screenshot of bottom-left floating buttons area +await page.screenshot({ + path: 'tests/screenshots/button-icon-scaling-375px.png', + clip: { x: 0, y: 600, width: 200, height: 200 } +}); + +// Take full page for context +await page.screenshot({ + path: 'tests/screenshots/full-page-375px.png', + fullPage: false +}); + +console.log('๐Ÿ“ธ Screenshots saved to tests/screenshots/'); + +// Print actual sizes for verification +const sizes = await page.evaluate(() => { + const results = []; + const selectors = ['.cmd-k-btn', '.download-btn', '.print-friendly-btn', + '.fixed-btn.contact-btn', '.shortcuts-btn', + '.color-theme-switcher', '.info-button', '.back-to-top']; + + for (const sel of selectors) { + const btn = document.querySelector(sel); + if (btn) { + const btnStyle = getComputedStyle(btn); + const icon = btn.querySelector('iconify-icon'); + const iconStyle = icon ? getComputedStyle(icon) : null; + results.push({ + selector: sel, + button: Math.round(Number.parseFloat(btnStyle.width)) + 'x' + Math.round(Number.parseFloat(btnStyle.height)), + icon: iconStyle ? Math.round(Number.parseFloat(iconStyle.width)) + 'x' + Math.round(Number.parseFloat(iconStyle.height)) : 'N/A' + }); + } + } + return results; +}); + +console.log('\n๐Ÿ“ Sizes at 375px viewport:'); +sizes.forEach(s => console.log(' ' + s.selector + ': button=' + s.button + ', icon=' + s.icon)); + +await browser.close();