@birhaus/test-utils
Version:
BIRHAUS v3.0 Radical Minimalist Testing Framework - Glass morphism validators, generous spacing tests, and v3 component validation utilities
1,041 lines (1,037 loc) • 42.7 kB
JavaScript
// src/v3/GlassMorphismValidator.ts
var glassValidationRules = [
{
name: "backdrop-blur-required",
description: "Glass elements must have backdrop-blur effect",
severity: "error",
validate: (element, style) => {
const backdropFilter = style.getPropertyValue("backdrop-filter");
return backdropFilter.includes("blur(") && !backdropFilter.includes("blur(0px)");
},
message: "Glass morphism elements must include backdrop-blur",
messageEs: "Elementos glass morphism deben incluir backdrop-blur"
},
{
name: "backdrop-saturate-recommended",
description: "Glass elements should have backdrop saturation for richer effect",
severity: "warning",
validate: (element, style) => {
const backdropFilter = style.getPropertyValue("backdrop-filter");
return backdropFilter.includes("saturate(") && !backdropFilter.includes("saturate(100%)");
},
message: "Consider adding backdrop-saturate for richer glass effect",
messageEs: "Considera agregar backdrop-saturate para efecto glass m\xE1s rico"
},
{
name: "semi-transparent-background",
description: "Glass backgrounds should be semi-transparent",
severity: "error",
validate: (element, style) => {
const bg = style.getPropertyValue("background-color");
const opacity = extractOpacity(bg);
return opacity > 0.1 && opacity < 0.9;
},
message: "Glass backgrounds must be semi-transparent (0.1 to 0.9 opacity)",
messageEs: "Fondos glass deben ser semi-transparentes (opacidad 0.1 a 0.9)"
},
{
name: "glass-border-transparency",
description: "Glass borders should be semi-transparent",
severity: "warning",
validate: (element, style) => {
const borderColor = style.getPropertyValue("border-color");
if (!borderColor || borderColor === "transparent") return true;
const opacity = extractOpacity(borderColor);
return opacity < 0.6;
},
message: "Glass borders should be semi-transparent for better effect",
messageEs: "Bordes glass deben ser semi-transparentes para mejor efecto"
},
{
name: "gradient-overlay-present",
description: "Glass elements should have subtle gradient overlay",
severity: "info",
validate: (element, style) => {
const hasOverlay = element.querySelector("::before, ::after") || element.querySelector('[class*="gradient"]');
return !!hasOverlay;
},
message: "Consider adding gradient overlay for enhanced glass effect",
messageEs: "Considera agregar superposici\xF3n de gradiente para efecto glass mejorado"
},
{
name: "performance-blur-limit",
description: "Backdrop blur should not exceed reasonable performance limits",
severity: "warning",
validate: (element, style) => {
const backdropFilter = style.getPropertyValue("backdrop-filter");
const blurMatch = backdropFilter.match(/blur\((\d+)px\)/);
if (!blurMatch) return true;
const blurValue = parseInt(blurMatch[1]);
return blurValue <= 20;
},
message: "Backdrop blur > 20px may impact performance",
messageEs: "Backdrop blur > 20px puede impactar el rendimiento"
},
{
name: "contrast-accessibility",
description: "Glass backgrounds must maintain sufficient contrast",
severity: "error",
validate: (element, style) => {
const bg = style.getPropertyValue("background-color");
style.getPropertyValue("color");
const bgOpacity = extractOpacity(bg);
return bgOpacity > 0.4 || isHighContrast();
},
message: "Glass elements must maintain WCAG AA contrast ratios",
messageEs: "Elementos glass deben mantener ratios de contraste WCAG AA"
}
];
function extractOpacity(color) {
if (!color) return 1;
const rgbaMatch = color.match(/rgba?\(\s*\d+\s*,\s*\d+\s*,\s*\d+\s*,?\s*([\d.]+)?\s*\)/);
if (rgbaMatch && rgbaMatch[1]) {
return parseFloat(rgbaMatch[1]);
}
const hslaMatch = color.match(/hsla?\(\s*\d+\s*,\s*\d+%\s*,\s*\d+%\s*,?\s*([\d.]+)?\s*\)/);
if (hslaMatch && hslaMatch[1]) {
return parseFloat(hslaMatch[1]);
}
if (color.includes("/")) {
const parts = color.split("/");
const alpha = parts[parts.length - 1].trim();
if (alpha.endsWith("%")) {
return parseFloat(alpha) / 100;
}
return parseFloat(alpha);
}
return 1;
}
function isHighContrast(color1, color2) {
return true;
}
function extractGlassProperties(element) {
const style = window.getComputedStyle(element);
const backdropFilter = style.getPropertyValue("backdrop-filter");
const blurMatch = backdropFilter.match(/blur\(([^)]+)\)/);
const backdropBlur = blurMatch ? blurMatch[1] : null;
const saturateMatch = backdropFilter.match(/saturate\(([^)]+)\)/);
const backdropSaturate = saturateMatch ? saturateMatch[1] : null;
const backgroundColor = style.getPropertyValue("background-color");
const borderColor = style.getPropertyValue("border-color");
const zIndex = parseInt(style.getPropertyValue("z-index")) || 0;
const hasGradientOverlay = element.querySelector('[class*="gradient"]') !== null || style.getPropertyValue("background-image").includes("gradient");
return {
backdropBlur,
backdropSaturate,
backgroundColor,
borderColor,
borderOpacity: extractOpacity(borderColor),
backgroundOpacity: extractOpacity(backgroundColor),
zIndex,
hasGradientOverlay
};
}
function validateGlassElement(element, rules = glassValidationRules) {
const style = window.getComputedStyle(element);
const results = [];
for (const rule of rules) {
const isValid = rule.validate(element, style);
if (!isValid) {
results.push({
isValid: false,
rule,
element,
actualValue: extractActualValue(element, rule),
expectedValue: rule.description,
severity: rule.severity
});
}
}
return results;
}
function extractActualValue(element, rule) {
const style = window.getComputedStyle(element);
switch (rule.name) {
case "backdrop-blur-required":
return style.getPropertyValue("backdrop-filter");
case "backdrop-saturate-recommended":
return style.getPropertyValue("backdrop-filter");
case "semi-transparent-background":
return style.getPropertyValue("background-color");
case "glass-border-transparency":
return style.getPropertyValue("border-color");
default:
return "N/A";
}
}
function findGlassElements(container = document.body) {
const selectors = [
'[class*="backdrop-blur"]',
'[class*="glass"]',
'[style*="backdrop-filter"]',
'[data-glass="true"]',
"[data-birhaus-glass]"
];
const elements = /* @__PURE__ */ new Set();
for (const selector of selectors) {
container.querySelectorAll(selector).forEach((el) => elements.add(el));
}
container.querySelectorAll("*").forEach((el) => {
const style = window.getComputedStyle(el);
const backdropFilter = style.getPropertyValue("backdrop-filter");
if (backdropFilter && backdropFilter !== "none") {
elements.add(el);
}
});
return Array.from(elements);
}
function validateGlassMorphism(container = document.body, rules = glassValidationRules) {
const glassElements = findGlassElements(container);
const allResults = [];
for (const element of glassElements) {
const results = validateGlassElement(element, rules);
allResults.push(...results);
}
const errors = allResults.filter((r) => r.severity === "error");
const warnings = allResults.filter((r) => r.severity === "warning");
const info = allResults.filter((r) => r.severity === "info");
const estimatedRenderCost = calculateGlassRenderCost(glassElements);
const recommendations = generatePerformanceRecommendations(glassElements, estimatedRenderCost);
return {
totalElements: glassElements.length,
validElements: glassElements.length - new Set(allResults.map((r) => r.element)).size,
invalidElements: new Set(allResults.map((r) => r.element)).size,
errors,
warnings,
info,
performance: {
glassElementsCount: glassElements.length,
estimatedRenderCost,
recommendations
}
};
}
function calculateGlassRenderCost(elements) {
let cost = 0;
for (const element of elements) {
const style = window.getComputedStyle(element);
const backdropFilter = style.getPropertyValue("backdrop-filter");
cost += 1;
const blurMatch = backdropFilter.match(/blur\((\d+)px\)/);
if (blurMatch) {
const blurValue = parseInt(blurMatch[1]);
cost += blurValue * 0.1;
}
if (backdropFilter.includes("saturate(")) {
cost += 0.5;
}
const rect = element.getBoundingClientRect();
const area = rect.width * rect.height;
cost += area / 1e4;
}
return Math.round(cost * 10) / 10;
}
function generatePerformanceRecommendations(elements, renderCost) {
const recommendations = [];
if (elements.length > 10) {
recommendations.push("Consider reducing number of glass elements (>10 detected)");
}
if (renderCost > 20) {
recommendations.push("High render cost detected - optimize blur values and element sizes");
}
const nestedElements = elements.filter(
(el) => elements.some((other) => other !== el && other.contains(el))
);
if (nestedElements.length > 0) {
recommendations.push("Avoid nesting glass elements - use single glass container");
}
const highBlurElements = elements.filter((el) => {
const style = window.getComputedStyle(el);
const backdropFilter = style.getPropertyValue("backdrop-filter");
const blurMatch = backdropFilter.match(/blur\((\d+)px\)/);
return blurMatch && parseInt(blurMatch[1]) > 15;
});
if (highBlurElements.length > 0) {
recommendations.push("Reduce backdrop-blur values >15px for better performance");
}
return recommendations;
}
// src/v3/V3SpacingValidator.ts
var v3SpacingRules = [
{
name: "generous-padding",
description: "Components should use generous padding (minimum 1.5rem for cards)",
severity: "warning",
validate: (element, measurements) => {
const isContainer = hasContainerClasses(element);
if (!isContainer) return true;
const minPadding = 24;
const avgPadding = (measurements.paddingTop + measurements.paddingRight + measurements.paddingBottom + measurements.paddingLeft) / 4;
return avgPadding >= minPadding;
},
message: "Use generous padding (\u226524px) for better v3.0 visual hierarchy",
messageEs: "Usa padding generoso (\u226524px) para mejor jerarqu\xEDa visual v3.0"
},
{
name: "eight-point-grid",
description: "Spacing should follow 8pt grid system",
severity: "warning",
validate: (element, measurements) => {
const spacingValues = [
measurements.paddingTop,
measurements.paddingRight,
measurements.paddingBottom,
measurements.paddingLeft,
measurements.marginTop,
measurements.marginRight,
measurements.marginBottom,
measurements.marginLeft
].filter((val) => val > 0);
if (spacingValues.length === 0) return true;
const gridCompliant = spacingValues.filter((val) => val % 8 === 0);
return gridCompliant.length / spacingValues.length >= 0.8;
},
message: "Spacing should follow 8pt grid system (multiples of 8px)",
messageEs: "El espaciado debe seguir el sistema de cuadr\xEDcula de 8pt (m\xFAltiplos de 8px)"
},
{
name: "touch-target-minimum",
description: "Interactive elements must meet 44px minimum touch target",
severity: "error",
validate: (element, measurements) => {
if (!isInteractiveElement(element)) return true;
const minTouchTarget = 44;
return measurements.width >= minTouchTarget && measurements.height >= minTouchTarget;
},
message: "Interactive elements must be at least 44px \xD7 44px for touch accessibility",
messageEs: "Elementos interactivos deben ser al menos 44px \xD7 44px para accesibilidad t\xE1ctil"
},
{
name: "button-generous-padding",
description: "Buttons should have generous padding for v3.0 design",
severity: "warning",
validate: (element, measurements) => {
if (!isButtonElement(element)) return true;
const minHorizontalPadding = 24;
const minVerticalPadding = 16;
const horizontalPadding = Math.min(measurements.paddingLeft, measurements.paddingRight);
const verticalPadding = Math.min(measurements.paddingTop, measurements.paddingBottom);
return horizontalPadding >= minHorizontalPadding && verticalPadding >= minVerticalPadding;
},
message: "Buttons should have generous padding (\u226524px horizontal, \u226516px vertical)",
messageEs: "Botones deben tener padding generoso (\u226524px horizontal, \u226516px vertical)"
},
{
name: "input-comfortable-padding",
description: "Form inputs should have comfortable padding for mobile use",
severity: "warning",
validate: (element, measurements) => {
if (!isInputElement(element)) return true;
const minPadding = 16;
const avgPadding = (measurements.paddingTop + measurements.paddingRight + measurements.paddingBottom + measurements.paddingLeft) / 4;
return avgPadding >= minPadding;
},
message: "Form inputs should have comfortable padding (\u226516px) for mobile use",
messageEs: "Campos de formulario deben tener padding c\xF3modo (\u226516px) para uso m\xF3vil"
},
{
name: "card-breathing-space",
description: "Cards should have generous internal spacing for readability",
severity: "info",
validate: (element, measurements) => {
if (!isCardElement(element)) return true;
const minPadding = 32;
const avgPadding = (measurements.paddingTop + measurements.paddingRight + measurements.paddingBottom + measurements.paddingLeft) / 4;
return avgPadding >= minPadding;
},
message: "Cards benefit from generous internal padding (\u226532px) for better readability",
messageEs: "Tarjetas se benefician de padding interno generoso (\u226532px) para mejor legibilidad"
},
{
name: "consistent-gap-spacing",
description: "Flex/grid gaps should use consistent spacing scale",
severity: "info",
validate: (element, measurements) => {
if (measurements.gap === 0) return true;
const standardGaps = [8, 16, 24, 32, 40, 48, 56, 64];
return standardGaps.includes(measurements.gap);
},
message: "Use standard gap values (8px, 16px, 24px, 32px, etc.) for consistency",
messageEs: "Usa valores de gap est\xE1ndar (8px, 16px, 24px, 32px, etc.) para consistencia"
},
{
name: "modal-generous-margins",
description: "Modals and overlays should have generous margins on mobile",
severity: "warning",
validate: (element, measurements) => {
if (!isModalElement(element)) return true;
const minMargin = 16;
return measurements.marginTop >= minMargin && measurements.marginLeft >= minMargin && measurements.marginRight >= minMargin && measurements.marginBottom >= minMargin;
},
message: "Modals should have generous margins (\u226516px) on all sides for mobile",
messageEs: "Modales deben tener m\xE1rgenes generosos (\u226516px) en todos los lados para m\xF3vil"
}
];
function extractSpacingMeasurements(element) {
const style = window.getComputedStyle(element);
const rect = element.getBoundingClientRect();
return {
paddingTop: parseFloat(style.paddingTop) || 0,
paddingRight: parseFloat(style.paddingRight) || 0,
paddingBottom: parseFloat(style.paddingBottom) || 0,
paddingLeft: parseFloat(style.paddingLeft) || 0,
marginTop: parseFloat(style.marginTop) || 0,
marginRight: parseFloat(style.marginRight) || 0,
marginBottom: parseFloat(style.marginBottom) || 0,
marginLeft: parseFloat(style.marginLeft) || 0,
gap: parseFloat(style.gap) || 0,
width: rect.width,
height: rect.height
};
}
function hasContainerClasses(element) {
const className = element.className.toLowerCase();
const containerIndicators = [
"container",
"card",
"panel",
"wrapper",
"box",
"section",
"modal",
"dialog",
"popup",
"dropdown",
"tooltip"
];
return containerIndicators.some(
(indicator) => className.includes(indicator) || element.getAttribute("data-testid")?.includes(indicator)
);
}
function isInteractiveElement(element) {
const tagName = element.tagName.toLowerCase();
const role = element.getAttribute("role");
const interactiveTags = ["button", "a", "input", "select", "textarea"];
const interactiveRoles = ["button", "link", "tab", "menuitem", "checkbox", "radio"];
return interactiveTags.includes(tagName) || role && interactiveRoles.includes(role) || element.hasAttribute("onclick") || element.getAttribute("tabindex") === "0";
}
function isButtonElement(element) {
const tagName = element.tagName.toLowerCase();
const role = element.getAttribute("role");
const type = element.getAttribute("type");
return tagName === "button" || role === "button" || tagName === "input" && ["button", "submit", "reset"].includes(type || "");
}
function isInputElement(element) {
const tagName = element.tagName.toLowerCase();
const role = element.getAttribute("role");
return tagName === "input" || tagName === "textarea" || tagName === "select" || role === "textbox" || element.hasAttribute("contenteditable");
}
function isCardElement(element) {
const className = element.className.toLowerCase();
const role = element.getAttribute("role");
return className.includes("card") || role === "article" || className.includes("panel") || className.includes("tile");
}
function isModalElement(element) {
const className = element.className.toLowerCase();
const role = element.getAttribute("role");
const ariaModal = element.getAttribute("aria-modal");
return className.includes("modal") || className.includes("dialog") || className.includes("popup") || className.includes("overlay") || role === "dialog" || ariaModal === "true";
}
function validateV3Spacing(element, rules = v3SpacingRules) {
const measurements = extractSpacingMeasurements(element);
const results = [];
for (const rule of rules) {
const isValid = rule.validate(element, measurements);
if (!isValid) {
results.push({
isValid: false,
rule,
element,
measurements,
expectedValue: rule.description,
actualValue: generateActualValueDescription(measurements, rule),
severity: rule.severity
});
}
}
return results;
}
function generateActualValueDescription(measurements, rule) {
switch (rule.name) {
case "generous-padding":
const avgPadding = (measurements.paddingTop + measurements.paddingRight + measurements.paddingBottom + measurements.paddingLeft) / 4;
return `${avgPadding.toFixed(1)}px average padding`;
case "eight-point-grid":
const spacingValues = [
measurements.paddingTop,
measurements.paddingRight,
measurements.paddingBottom,
measurements.paddingLeft,
measurements.marginTop,
measurements.marginRight,
measurements.marginBottom,
measurements.marginLeft
].filter((val) => val > 0);
return `Spacing values: ${spacingValues.join(", ")}px`;
case "touch-target-minimum":
return `${measurements.width.toFixed(0)}px \xD7 ${measurements.height.toFixed(0)}px`;
case "consistent-gap-spacing":
return `${measurements.gap}px gap`;
default:
return "N/A";
}
}
function findSpacingElements(container = document.body) {
const selectors = [
"button",
"input",
"textarea",
"select",
'[role="button"]',
'[role="textbox"]',
'[class*="card"]',
'[class*="panel"]',
'[class*="container"]',
'[class*="modal"]',
'[class*="dialog"]',
"[data-testid]",
".birhaus-card",
".birhaus-button",
".birhaus-input",
"[data-birhaus]"
];
const elements = /* @__PURE__ */ new Set();
for (const selector of selectors) {
try {
container.querySelectorAll(selector).forEach((el) => elements.add(el));
} catch (e) {
}
}
return Array.from(elements);
}
function validateV3SpacingSystem(container = document.body, rules = v3SpacingRules) {
const spacingElements = findSpacingElements(container);
const allResults = [];
const allMeasurements = [];
for (const element of spacingElements) {
const results = validateV3Spacing(element, rules);
const measurements = extractSpacingMeasurements(element);
allResults.push(...results);
allMeasurements.push(measurements);
}
const errors = allResults.filter((r) => r.severity === "error");
const warnings = allResults.filter((r) => r.severity === "warning");
const info = allResults.filter((r) => r.severity === "info");
const spacingAnalysis = calculateSpacingAnalysis(spacingElements, allMeasurements);
return {
totalElements: spacingElements.length,
validElements: spacingElements.length - new Set(allResults.map((r) => r.element)).size,
invalidElements: new Set(allResults.map((r) => r.element)).size,
errors,
warnings,
info,
spacingAnalysis
};
}
function calculateSpacingAnalysis(elements, measurements) {
if (measurements.length === 0) {
return {
averagePadding: 0,
averageMargin: 0,
gridCompliance: 0,
touchTargetCompliance: 0,
generousSpacingCompliance: 0
};
}
const totalPadding = measurements.reduce((sum, m) => sum + m.paddingTop + m.paddingRight + m.paddingBottom + m.paddingLeft, 0);
const averagePadding = totalPadding / (measurements.length * 4);
const totalMargin = measurements.reduce((sum, m) => sum + m.marginTop + m.marginRight + m.marginBottom + m.marginLeft, 0);
const averageMargin = totalMargin / (measurements.length * 4);
let gridCompliantCount = 0;
measurements.forEach((m) => {
const spacingValues = [
m.paddingTop,
m.paddingRight,
m.paddingBottom,
m.paddingLeft,
m.marginTop,
m.marginRight,
m.marginBottom,
m.marginLeft
].filter((val) => val > 0);
if (spacingValues.length > 0) {
const compliant = spacingValues.filter((val) => val % 8 === 0);
if (compliant.length / spacingValues.length >= 0.5) {
gridCompliantCount++;
}
}
});
const gridCompliance = gridCompliantCount / measurements.length * 100;
let touchTargetCompliantCount = 0;
elements.forEach((element, index) => {
if (isInteractiveElement(element)) {
const m = measurements[index];
if (m.width >= 44 && m.height >= 44) {
touchTargetCompliantCount++;
}
}
});
const interactiveCount = elements.filter(isInteractiveElement).length;
const touchTargetCompliance = interactiveCount > 0 ? touchTargetCompliantCount / interactiveCount * 100 : 100;
let generousSpacingCount = 0;
measurements.forEach((m) => {
const avgPadding = (m.paddingTop + m.paddingRight + m.paddingBottom + m.paddingLeft) / 4;
if (avgPadding >= 16) {
generousSpacingCount++;
}
});
const generousSpacingCompliance = generousSpacingCount / measurements.length * 100;
return {
averagePadding: Math.round(averagePadding * 10) / 10,
averageMargin: Math.round(averageMargin * 10) / 10,
gridCompliance: Math.round(gridCompliance * 10) / 10,
touchTargetCompliance: Math.round(touchTargetCompliance * 10) / 10,
generousSpacingCompliance: Math.round(generousSpacingCompliance * 10) / 10
};
}
// src/v3/V3AnimationValidator.ts
var v3AnimationRules = [
{
name: "smooth-easing-curves",
description: "Use smooth easing curves instead of linear or harsh ease functions",
severity: "warning",
validate: (element, properties) => {
const timingFunctions = [
...properties.transitionTimingFunction,
...properties.animationTimingFunction
];
if (timingFunctions.length === 0) return true;
const smoothCurves = [
"ease",
"ease-out",
"ease-in-out",
"cubic-bezier(0.4, 0, 0.2, 1)",
// Material Design standard
"cubic-bezier(0.25, 0.46, 0.45, 0.94)",
// ease-smooth
"cubic-bezier(0.16, 1, 0.3, 1)"
// ease-gentle
];
const smoothCount = timingFunctions.filter(
(tf) => smoothCurves.some((curve) => tf.includes(curve)) || tf.includes("cubic-bezier")
).length;
return smoothCount / timingFunctions.length >= 0.8;
},
message: "Use smooth easing curves (ease-out, cubic-bezier) for better v3.0 feel",
messageEs: "Usa curvas de suavizado (ease-out, cubic-bezier) para mejor sensaci\xF3n v3.0"
},
{
name: "appropriate-duration",
description: "Animation durations should be appropriate for the effect",
severity: "warning",
validate: (element, properties) => {
const allDurations = [
...properties.transitionDuration,
...properties.animationDuration
];
if (allDurations.length === 0) return true;
const appropriateDurations = allDurations.filter(
(duration) => duration >= 150 && duration <= 500
);
return appropriateDurations.length / allDurations.length >= 0.8;
},
message: "Use appropriate durations (150-500ms) for smooth UI animations",
messageEs: "Usa duraciones apropiadas (150-500ms) para animaciones UI suaves"
},
{
name: "gpu-acceleration",
description: "Use GPU-accelerated properties for better performance",
severity: "info",
validate: (element, properties) => {
const gpuProperties = ["transform", "opacity", "filter"];
const transitionProps = properties.transitionProperty;
if (transitionProps.length === 0) return true;
const gpuAnimatedProps = transitionProps.filter(
(prop) => gpuProperties.some((gpuProp) => prop.includes(gpuProp))
);
return gpuAnimatedProps.length / transitionProps.length >= 0.6;
},
message: "Prefer GPU-accelerated properties (transform, opacity) for smooth animations",
messageEs: "Prefiere propiedades GPU (transform, opacity) para animaciones suaves"
},
{
name: "will-change-optimization",
description: "Use will-change for complex animations to optimize performance",
severity: "info",
validate: (element, properties) => {
const hasComplexAnimation = properties.transitionProperty.length > 2 || properties.animationName.length > 0 || properties.transform !== "none";
if (!hasComplexAnimation) return true;
return properties.willChange !== "auto" && properties.willChange !== "";
},
message: "Consider using will-change for complex animations to optimize performance",
messageEs: "Considera usar will-change para animaciones complejas para optimizar rendimiento"
},
{
name: "reduced-motion-support",
description: "Respect prefers-reduced-motion for accessibility",
severity: "error",
validate: (element, properties) => {
const prefersReducedMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
if (!prefersReducedMotion) return true;
const hasStrongAnimation = properties.transitionDuration.some((d) => d > 200) || properties.animationDuration.some((d) => d > 200);
return !hasStrongAnimation;
},
message: "Respect prefers-reduced-motion setting for accessibility",
messageEs: "Respeta la configuraci\xF3n prefers-reduced-motion para accesibilidad"
},
{
name: "consistent-timing",
description: "Use consistent animation timing across related elements",
severity: "info",
validate: (element, properties) => {
const standardDurations = [150, 200, 250, 300, 400, 500];
const allDurations = [...properties.transitionDuration, ...properties.animationDuration];
if (allDurations.length === 0) return true;
const standardCount = allDurations.filter(
(duration) => standardDurations.some((std) => Math.abs(duration - std) <= 50)
);
return standardCount.length / allDurations.length >= 0.8;
},
message: "Use standard timing values (150ms, 200ms, 300ms, etc.) for consistency",
messageEs: "Usa valores de tiempo est\xE1ndar (150ms, 200ms, 300ms, etc.) para consistencia"
},
{
name: "avoid-layout-thrashing",
description: "Avoid animating layout properties that cause reflow",
severity: "warning",
validate: (element, properties) => {
const layoutProperties = [
"width",
"height",
"padding",
"margin",
"border",
"top",
"left",
"right",
"bottom",
"position"
];
const animatedLayoutProps = properties.transitionProperty.filter(
(prop) => layoutProperties.some((layoutProp) => prop.includes(layoutProp))
);
return animatedLayoutProps.length === 0;
},
message: "Avoid animating layout properties (width, height, padding) - use transform instead",
messageEs: "Evita animar propiedades de layout (width, height, padding) - usa transform"
},
{
name: "glass-animation-performance",
description: "Glass morphism animations should use optimized properties",
severity: "warning",
validate: (element, properties) => {
const style = window.getComputedStyle(element);
const hasBackdropFilter = style.getPropertyValue("backdrop-filter") !== "none";
if (!hasBackdropFilter) return true;
const animatesBackdropFilter = properties.transitionProperty.includes("backdrop-filter");
return !animatesBackdropFilter;
},
message: "Avoid animating backdrop-filter directly - animate opacity or transform instead",
messageEs: "Evita animar backdrop-filter directamente - anima opacity o transform"
}
];
function extractAnimationProperties(element) {
const style = window.getComputedStyle(element);
const parseMultiValue = (value) => value ? value.split(",").map((v) => v.trim()) : [];
const parseDurations = (value) => parseMultiValue(value).map((v) => {
const num = parseFloat(v);
return v.includes("ms") ? num : num * 1e3;
});
return {
transitionProperty: parseMultiValue(style.getPropertyValue("transition-property")),
transitionDuration: parseDurations(style.getPropertyValue("transition-duration")),
transitionTimingFunction: parseMultiValue(style.getPropertyValue("transition-timing-function")),
transitionDelay: parseDurations(style.getPropertyValue("transition-delay")),
animationName: parseMultiValue(style.getPropertyValue("animation-name")),
animationDuration: parseDurations(style.getPropertyValue("animation-duration")),
animationTimingFunction: parseMultiValue(style.getPropertyValue("animation-timing-function")),
animationFillMode: parseMultiValue(style.getPropertyValue("animation-fill-mode")),
transform: style.getPropertyValue("transform"),
willChange: style.getPropertyValue("will-change"),
backfaceVisibility: style.getPropertyValue("backface-visibility")
};
}
function validateV3Animation(element, rules = v3AnimationRules) {
const properties = extractAnimationProperties(element);
const results = [];
for (const rule of rules) {
const isValid = rule.validate(element, properties);
if (!isValid) {
results.push({
isValid: false,
rule,
element,
properties,
expectedValue: rule.description,
actualValue: generateActualAnimationValue(properties, rule),
severity: rule.severity
});
}
}
return results;
}
function generateActualAnimationValue(properties, rule) {
switch (rule.name) {
case "smooth-easing-curves":
return `Timing functions: ${[
...properties.transitionTimingFunction,
...properties.animationTimingFunction
].join(", ")}`;
case "appropriate-duration":
return `Durations: ${[
...properties.transitionDuration,
...properties.animationDuration
].map((d) => `${d}ms`).join(", ")}`;
case "gpu-acceleration":
return `Properties: ${properties.transitionProperty.join(", ")}`;
case "will-change-optimization":
return `will-change: ${properties.willChange || "auto"}`;
case "avoid-layout-thrashing":
return `Properties: ${properties.transitionProperty.join(", ")}`;
default:
return "N/A";
}
}
function findAnimatedElements(container = document.body) {
const elements = /* @__PURE__ */ new Set();
container.querySelectorAll("*").forEach((el) => {
const style = window.getComputedStyle(el);
const hasTransition = style.getPropertyValue("transition-duration") !== "0s";
const hasAnimation = style.getPropertyValue("animation-name") !== "none";
const hasTransform = style.getPropertyValue("transform") !== "none";
if (hasTransition || hasAnimation || hasTransform) {
elements.add(el);
}
});
const animationSelectors = [
'[class*="transition"]',
'[class*="animate"]',
'[class*="motion"]',
'[style*="transition"]',
'[style*="animation"]',
"[data-animate]",
"[data-transition]"
];
for (const selector of animationSelectors) {
try {
container.querySelectorAll(selector).forEach((el) => elements.add(el));
} catch (e) {
}
}
return Array.from(elements);
}
function validateV3AnimationSystem(container = document.body, rules = v3AnimationRules) {
const animatedElements = findAnimatedElements(container);
const allResults = [];
const allProperties = [];
for (const element of animatedElements) {
const results = validateV3Animation(element, rules);
const properties = extractAnimationProperties(element);
allResults.push(...results);
allProperties.push(properties);
}
const errors = allResults.filter((r) => r.severity === "error");
const warnings = allResults.filter((r) => r.severity === "warning");
const info = allResults.filter((r) => r.severity === "info");
const performanceMetrics = calculateAnimationPerformance(animatedElements, allProperties);
return {
totalElements: animatedElements.length,
validElements: animatedElements.length - new Set(allResults.map((r) => r.element)).size,
invalidElements: new Set(allResults.map((r) => r.element)).size,
errors,
warnings,
info,
performance: performanceMetrics
};
}
function calculateAnimationPerformance(elements, properties) {
if (elements.length === 0) {
return {
gpuAcceleratedElements: 0,
reducedMotionCompliantElements: 0,
smoothEasingElements: 0,
performanceScore: 100,
recommendations: []
};
}
let gpuAcceleratedCount = 0;
const gpuProps = ["transform", "opacity", "filter"];
properties.forEach((props) => {
const hasGpuProps = props.transitionProperty.some(
(prop) => gpuProps.some((gpuProp) => prop.includes(gpuProp))
);
if (hasGpuProps) gpuAcceleratedCount++;
});
const prefersReducedMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
let reducedMotionCompliantCount = elements.length;
if (prefersReducedMotion) {
reducedMotionCompliantCount = properties.filter((props) => {
const maxDuration = Math.max(
...props.transitionDuration,
...props.animationDuration,
0
);
return maxDuration <= 200;
}).length;
}
const smoothCurves = ["ease", "ease-out", "ease-in-out", "cubic-bezier"];
let smoothEasingCount = 0;
properties.forEach((props) => {
const timingFunctions = [
...props.transitionTimingFunction,
...props.animationTimingFunction
];
const hasSmoothEasing = timingFunctions.some(
(tf) => smoothCurves.some((curve) => tf.includes(curve))
);
if (hasSmoothEasing) smoothEasingCount++;
});
const gpuScore = gpuAcceleratedCount / elements.length * 30;
const reducedMotionScore = reducedMotionCompliantCount / elements.length * 30;
const smoothEasingScore = smoothEasingCount / elements.length * 20;
let layoutThrashingPenalty = 0;
const layoutProps = ["width", "height", "padding", "margin", "border", "top", "left"];
properties.forEach((props) => {
const hasLayoutAnimation = props.transitionProperty.some(
(prop) => layoutProps.some((layoutProp) => prop.includes(layoutProp))
);
if (hasLayoutAnimation) layoutThrashingPenalty += 10;
});
const performanceScore = Math.max(
0,
Math.min(100, gpuScore + reducedMotionScore + smoothEasingScore + 20 - layoutThrashingPenalty)
);
const recommendations = [];
if (gpuAcceleratedCount / elements.length < 0.6) {
recommendations.push("Increase use of GPU-accelerated properties (transform, opacity)");
}
if (prefersReducedMotion && reducedMotionCompliantCount / elements.length < 0.9) {
recommendations.push("Better respect for prefers-reduced-motion setting needed");
}
if (smoothEasingCount / elements.length < 0.7) {
recommendations.push("Use more smooth easing curves (ease-out, cubic-bezier)");
}
const avgDuration = properties.reduce((sum, props) => {
const durations = [...props.transitionDuration, ...props.animationDuration];
return sum + durations.reduce((dSum, d) => dSum + d, 0);
}, 0) / Math.max(1, properties.reduce(
(sum, props) => sum + props.transitionDuration.length + props.animationDuration.length,
0
));
if (avgDuration > 500) {
recommendations.push("Consider shorter animation durations for better perceived performance");
}
return {
gpuAcceleratedElements: gpuAcceleratedCount,
reducedMotionCompliantElements: reducedMotionCompliantCount,
smoothEasingElements: smoothEasingCount,
performanceScore: Math.round(performanceScore),
recommendations
};
}
// src/v3/index.ts
function validateV3Comprehensive(container = document.body) {
const glassReport = validateGlassMorphism(container);
const spacingReport = validateV3SpacingSystem(container);
const animationReport = validateV3AnimationSystem(container);
const glassScore = calculateGlassScore(glassReport);
const spacingScore = calculateSpacingScore(spacingReport);
const animationScore = animationReport.performance.performanceScore;
const overallScore = Math.round((glassScore + spacingScore + animationScore) / 3);
const totalIssues = glassReport.errors.length + glassReport.warnings.length + spacingReport.errors.length + spacingReport.warnings.length + animationReport.errors.length + animationReport.warnings.length;
const criticalIssues = glassReport.errors.length + spacingReport.errors.length + animationReport.errors.length;
const recommendations = [
...glassReport.performance.recommendations,
...animationReport.performance.recommendations
];
if (spacingReport.spacingAnalysis.touchTargetCompliance < 90) {
recommendations.push("Improve touch target accessibility (44px minimum)");
}
if (spacingReport.spacingAnalysis.generousSpacingCompliance < 70) {
recommendations.push("Increase use of generous spacing for v3.0 visual hierarchy");
}
if (spacingReport.spacingAnalysis.gridCompliance < 80) {
recommendations.push("Better adhere to 8pt grid system for consistent spacing");
}
return {
glassReport,
spacingReport,
animationReport,
overallScore,
summary: {
totalIssues,
criticalIssues,
recommendations: [...new Set(recommendations)]
// Remove duplicates
}
};
}
function calculateGlassScore(report) {
if (report.totalElements === 0) return 100;
const errorWeight = 10;
const warningWeight = 5;
const infoWeight = 1;
const totalPenalty = report.errors.length * errorWeight + report.warnings.length * warningWeight + report.info.length * infoWeight;
const maxPossiblePenalty = report.totalElements * errorWeight;
const score = Math.max(0, 100 - totalPenalty / maxPossiblePenalty * 100);
return Math.round(score);
}
function calculateSpacingScore(report) {
if (report.totalElements === 0) return 100;
const errorWeight = 15;
const warningWeight = 8;
const infoWeight = 2;
const totalPenalty = report.errors.length * errorWeight + report.warnings.length * warningWeight + report.info.length * infoWeight;
const maxPossiblePenalty = report.totalElements * errorWeight;
let score = Math.max(0, 100 - totalPenalty / maxPossiblePenalty * 100);
const analysis = report.spacingAnalysis;
if (analysis.touchTargetCompliance > 95) score += 5;
if (analysis.generousSpacingCompliance > 80) score += 5;
if (analysis.gridCompliance > 90) score += 5;
return Math.round(Math.min(100, score));
}
function generateV3Summary(report) {
const lines = [];
lines.push("\u{1F3A8} BIRHAUS v3.0 Validation Report");
lines.push("=".repeat(40));
lines.push(`Overall Score: ${report.overallScore}/100`);
lines.push("");
lines.push("\u{1F52E} Glass Morphism Analysis:");
lines.push(` Elements: ${report.glassReport.totalElements}`);
lines.push(` Errors: ${report.glassReport.errors.length}`);
lines.push(` Warnings: ${report.glassReport.warnings.length}`);
lines.push(` Performance Cost: ${report.glassReport.performance.estimatedRenderCost}`);
lines.push("");
lines.push("\u{1F4CF} Spacing Analysis:");
lines.push(` Elements: ${report.spacingReport.totalElements}`);
lines.push(` 8pt Grid Compliance: ${report.spacingReport.spacingAnalysis.gridCompliance}%`);
lines.push(` Touch Target Compliance: ${report.spacingReport.spacingAnalysis.touchTargetCompliance}%`);
lines.push(` Generous Spacing: ${report.spacingReport.spacingAnalysis.generousSpacingCompliance}%`);
lines.push("");
lines.push("\u2728 Animation Analysis:");
lines.push(` Elements: ${report.animationReport.totalElements}`);
lines.push(` GPU Accelerated: ${report.animationReport.performance.gpuAcceleratedElements}`);
lines.push(` Smooth Easing: ${report.animationReport.performance.smoothEasingElements}`);
lines.push(` Performance Score: ${report.animationReport.performance.performanceScore}/100`);
lines.push("");
if (report.summary.recommendations.length > 0) {
lines.push("\u{1F4A1} Recommendations:");
report.summary.recommendations.forEach((rec) => {
lines.push(` \u2022 ${rec}`);
});
}
return lines.join("\n");
}
export { extractAnimationProperties, extractGlassProperties, extractSpacingMeasurements, findAnimatedElements, findGlassElements, findSpacingElements, generateV3Summary, glassValidationRules, v3AnimationRules, v3SpacingRules, validateGlassElement, validateGlassMorphism, validateV3Animation, validateV3AnimationSystem, validateV3Comprehensive, validateV3Spacing, validateV3SpacingSystem };
//# sourceMappingURL=index.mjs.map
//# sourceMappingURL=index.mjs.map