import { useEffect, useState } from 'react'; export interface LayoutAnomaly { id: string; type: 'text-overflow' | 'layout-overflow' | 'grid-violation' | 'clipping'; elementName: string; selector: string; message: string; severity: 'warning' | 'critical'; } /** * High-performance hook that scans the DOM periodically (or upon resize / layout adjustments) * to detect any layout anomalies, text overflows, clipped content, or styling grid violations. */ export function useLayoutValidator(active: boolean = true) { const [anomalies, setAnomalies] = useState([]); useEffect(() => { if (!active || typeof window === 'undefined') return; let scanTimer: any; const performAudit = () => { const findings: LayoutAnomaly[] = []; // 1. Check for global window horizontal scrollbar (Layout Overflow) if (document.documentElement.scrollWidth > window.innerWidth) { findings.push({ id: 'viewport-x-overflow', type: 'layout-overflow', elementName: 'Viewport (HTML/Body)', selector: 'html', message: `Viewport horizontal overflow detected: scrollWidth is ${document.documentElement.scrollWidth}px but innerWidth is ${window.innerWidth}px. Can cause platform horizontal scrolling.`, severity: 'critical' }); } // 2. Perform deep DOM checks on text-bearing elements or structural containers const elementsToAudit = document.querySelectorAll('p, span, h1, h2, h3, button, td, div.card, div.container'); elementsToAudit.forEach((el, index) => { const clientWidth = el.clientWidth; const scrollWidth = el.scrollWidth; const clientHeight = el.clientHeight; const scrollHeight = el.scrollHeight; // Trace unique selector or identifier const tagName = el.tagName.toLowerCase(); const idStr = el.id ? `#${el.id}` : ''; const classNames = Array.from(el.classList).slice(0, 2).map(c => `.${c}`).join(''); const resolvedSelector = `${tagName}${idStr}${classNames} [idx:${index}]`; // Case A: Text / Content Overflow (scrollWidth > clientWidth) if (scrollWidth > clientWidth && clientWidth > 0) { const style = window.getComputedStyle(el); const isOverflowHiddenX = style.overflowX === 'hidden' || style.overflow === 'hidden'; const isScrollableX = style.overflowX === 'scroll' || style.overflowX === 'auto'; if (!isScrollableX && isOverflowHiddenX) { findings.push({ id: `text-overflow-${index}`, type: 'text-overflow', elementName: el.id || `${tagName} tag`, selector: resolvedSelector, message: `Potential clipped text detected: content width (${scrollWidth}px) exceeds container width (${clientWidth}px) under hidden overflow bounds.`, severity: 'warning' }); } } // Case B: Vertical Clipping (scrollHeight > clientHeight) if (scrollHeight > clientHeight && clientHeight > 0) { const style = window.getComputedStyle(el); const isOverflowHiddenY = style.overflowY === 'hidden' || style.overflow === 'hidden'; const isScrollableY = style.overflowY === 'scroll' || style.overflowY === 'auto'; if (!isScrollableY && isOverflowHiddenY) { findings.push({ id: `vertical-clip-${index}`, type: 'clipping', elementName: el.id || `${tagName} tag`, selector: resolvedSelector, message: `Vertical text/content clipping: scrollHeight is ${scrollHeight}px but visible bounds are limited to ${clientHeight}px.`, severity: 'warning' }); } } // Case C: Check for 8px Grid violation for structural margins or paddings const style = window.getComputedStyle(el); const paddingLeft = parseFloat(style.paddingLeft) || 0; const paddingRight = parseFloat(style.paddingRight) || 0; const paddingTop = parseFloat(style.paddingTop) || 0; const paddingBottom = parseFloat(style.paddingBottom) || 0; const isPaddingViolation = (val: number) => val > 0 && val % 4 !== 0; // check for alignment scale if (isPaddingViolation(paddingLeft) || isPaddingViolation(paddingTop)) { findings.push({ id: `grid-violation-${index}`, type: 'grid-violation', elementName: el.id || `${tagName} tag`, selector: resolvedSelector, message: `Padding values (${paddingLeft}px, ${paddingRight}px) do not align nicely with the 8px layout token scale.`, severity: 'warning' }); } }); // Filter duplicate or redundant findings setAnomalies(findings.slice(0, 10)); // Limit to top 10 to protect execution frames }; // Run audit trigger loop const triggerAudit = () => { cancelAnimationFrame(scanTimer); scanTimer = requestAnimationFrame(performAudit); }; // Attach resize listeners to trigger on orientation changes or size changes window.addEventListener('resize', triggerAudit); document.addEventListener('DOMSubtreeModified', triggerAudit); // Listen for updates // Perform initial delayed checks const initialDelay = setTimeout(performAudit, 1000); return () => { window.removeEventListener('resize', triggerAudit); document.removeEventListener('DOMSubtreeModified', triggerAudit); clearTimeout(initialDelay); cancelAnimationFrame(scanTimer); }; }, [active]); return { anomalies }; }