examforge / src /engines /ui /hooks /useLayoutValidator.ts
Benjahmin's picture
feat(ui): implement design token system and core components
bf954c9
Raw
History Blame Contribute Delete
5.71 kB
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<LayoutAnomaly[]>([]);
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 };
}