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)
This commit is contained in:
@@ -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();
|
||||
@@ -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);
|
||||
@@ -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();
|
||||
Reference in New Issue
Block a user