UNPKG

@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
// 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