UNPKG

@birhaus/provider

Version:

BIRHAUS Provider - Context provider for real-time UX validation and cognitive load monitoring

406 lines (404 loc) 15 kB
// src/BirhausProvider.tsx import { createContext, useContext, useState, useEffect, useCallback, useMemo, useRef } from "react"; import { BIRHAUS_ENV_CONFIG } from "@birhaus/primitives"; import { jsx } from "react/jsx-runtime"; var BirhausContext = createContext(null); var BirhausRuntimeValidator = class { constructor(config, onViolation) { this.violations = []; this.config = config; this.onViolation = onViolation; this.observer = new MutationObserver(this.handleMutations.bind(this)); } start() { this.observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ["class", "role", "aria-label"] }); this.validateElement(document.body); } stop() { this.observer.disconnect(); } handleMutations(mutations) { mutations.forEach((mutation) => { if (mutation.type === "childList") { mutation.addedNodes.forEach((node) => { if (node.nodeType === Node.ELEMENT_NODE) { this.validateElement(node); } }); } }); } // eslint-disable-next-line complexity -- Element validation requires multiple BIRHAUS principle checks validateElement(element) { if (!this.config.cognitiveLoadTracking) return; if (element.matches("nav") || element.querySelector("nav")) { this.validateNavigation(element); } if (element.matches("form") || element.querySelector("form")) { this.validateForms(element); } if (element.matches("select") || element.querySelector("select")) { this.validateSelects(element); } if (this.config.undoOverConfirm) { this.validateConfirmationDialogs(element); } if (this.config.spanishFirst) { this.validateSpanishFirst(element); } if (this.config.accessibilityValidation) { this.validateAccessibility(element); } } validateNavigation(element) { const navElements = element.querySelectorAll("nav"); navElements.forEach((nav) => { const navItems = nav.querySelectorAll("a, button"); if (navItems.length > 7) { this.reportViolation({ type: "cognitive", severity: "warning", element: nav, message: `Navigation has ${navItems.length} items (max recommended: 7)`, messageEs: `Navegaci\xF3n con ${navItems.length} elementos (m\xE1ximo recomendado: 7)`, messageEn: `Navigation has ${navItems.length} items (max recommended: 7)`, recommendation: "Consider grouping related navigation items into submenus", birhausPrinciple: 1 // Form serves flow }); } }); } validateForms(element) { const forms = element.querySelectorAll("form"); forms.forEach((form) => { const visibleFields = form.querySelectorAll('input:not([type="hidden"]), select, textarea'); if (visibleFields.length > 7) { this.reportViolation({ type: "cognitive", severity: "warning", element: form, message: `Form has ${visibleFields.length} visible fields (max recommended: 7)`, messageEs: `Formulario con ${visibleFields.length} campos visibles (m\xE1ximo recomendado: 7)`, messageEn: `Form has ${visibleFields.length} visible fields (max recommended: 7)`, recommendation: "Use progressive disclosure or split into multiple steps", birhausPrinciple: 4 // Progressive disclosure }); } }); } validateSelects(element) { const selects = element.querySelectorAll("select"); selects.forEach((select) => { const options = select.querySelectorAll("option"); if (options.length > 7) { this.reportViolation({ type: "cognitive", severity: "warning", element: select, message: `Select has ${options.length} options (max recommended: 7)`, messageEs: `Select con ${options.length} opciones (m\xE1ximo recomendado: 7)`, messageEn: `Select has ${options.length} options (max recommended: 7)`, recommendation: "Add search functionality or group options by category", birhausPrinciple: 4 // Progressive disclosure }); } }); } validateConfirmationDialogs(element) { const confirmButtons = element.querySelectorAll('[onclick*="confirm"], [onclick*="alert"]'); confirmButtons.forEach((button) => { this.reportViolation({ type: "confirmation", severity: "error", element: button, message: "Confirmation dialog detected - use undo pattern instead", messageEs: "Di\xE1logo de confirmaci\xF3n detectado - usa patr\xF3n de deshacer", messageEn: "Confirmation dialog detected - use undo pattern instead", recommendation: "Replace with undo-over-confirm pattern using BirhausButton", birhausPrinciple: 5 // Undo over confirm }); }); const modals = element.querySelectorAll('[role="dialog"], [role="alertdialog"]'); modals.forEach((modal) => { const confirmButtons2 = modal.querySelectorAll('button[data-confirm], button:contains("confirmar")'); if (confirmButtons2.length > 0) { this.reportViolation({ type: "confirmation", severity: "error", element: modal, message: "Modal confirmation dialog found", messageEs: "Di\xE1logo modal de confirmaci\xF3n encontrado", messageEn: "Modal confirmation dialog found", recommendation: "Use inline undo functionality instead of confirmation modals", birhausPrinciple: 5 // Undo over confirm }); } }); } validateSpanishFirst(element) { const buttons = element.querySelectorAll("button"); buttons.forEach((button) => { const text = button.textContent?.toLowerCase() || ""; const englishWords = ["save", "cancel", "delete", "edit", "view", "submit", "close"]; if (englishWords.some((word) => text.includes(word))) { this.reportViolation({ type: "spanish", severity: "warning", element: button, message: `Button with English text: "${button.textContent}"`, messageEs: `Bot\xF3n con texto en ingl\xE9s: "${button.textContent}"`, messageEn: `Button with English text: "${button.textContent}"`, recommendation: "Use Spanish-first labels with BirhausButton component", birhausPrinciple: 7 // Bilingual by design }); } }); const inputs = element.querySelectorAll("input[placeholder], textarea[placeholder]"); inputs.forEach((input) => { const placeholder = input.getAttribute("placeholder")?.toLowerCase() || ""; const englishWords = ["enter", "type", "search", "select", "choose"]; if (englishWords.some((word) => placeholder.includes(word))) { this.reportViolation({ type: "spanish", severity: "warning", element: input, message: `Input with English placeholder: "${placeholder}"`, messageEs: `Input con placeholder en ingl\xE9s: "${placeholder}"`, messageEn: `Input with English placeholder: "${placeholder}"`, recommendation: "Use Spanish-first placeholders with BirhausInput component", birhausPrinciple: 7 // Bilingual by design }); } }); } validateAccessibility(element) { const images = element.querySelectorAll("img:not([alt])"); images.forEach((img) => { this.reportViolation({ type: "accessibility", severity: "error", element: img, message: "Image missing alt attribute", messageEs: "Imagen sin atributo alt", messageEn: "Image missing alt attribute", recommendation: "Add descriptive alt text for screen readers", birhausPrinciple: 6 // Accessibility = dignity }); }); const inputs = element.querySelectorAll("input:not([aria-label]):not([aria-labelledby])"); inputs.forEach((input) => { const id = input.getAttribute("id"); if (!id || !element.querySelector(`label[for="${id}"]`)) { this.reportViolation({ type: "accessibility", severity: "error", element: input, message: "Input field missing label", messageEs: "Campo de entrada sin etiqueta", messageEn: "Input field missing label", recommendation: "Associate input with a label element or add aria-label", birhausPrinciple: 6 // Accessibility = dignity }); } }); } reportViolation(violation) { const fullViolation = { ...violation, timestamp: /* @__PURE__ */ new Date() }; this.violations.push(fullViolation); this.onViolation(fullViolation); } getViolations() { return [...this.violations]; } clearViolations() { this.violations = []; } }; function BirhausProvider({ language = "es", config: userConfig, theme, analyticsEndpoint, scoreThresholds = { warning: 70, critical: 50 }, devMode = process.env.NODE_ENV === "development", children, onViolation, onScoreChange }) { const config = useMemo(() => ({ ...BIRHAUS_ENV_CONFIG[process.env.NODE_ENV] || BIRHAUS_ENV_CONFIG.development, ...userConfig }), [userConfig]); const [currentLanguage, setCurrentLanguage] = useState(language); const [currentTheme] = useState(theme); const [currentScore, setCurrentScore] = useState(null); const [violations, setViolations] = useState([]); const [currentConfig, setCurrentConfig] = useState(config); const validatorRef = useRef(null); const analyticsQueueRef = useRef([]); const t = useCallback((key, fallback) => { return fallback || key; }, []); const handleViolation = useCallback((violation) => { setViolations((prev) => [...prev, violation]); if (devMode && config.detailedLogging) { console.group(`\u{1F9E0} BIRHAUS ${violation.severity.toUpperCase()}: ${violation.type}`); console.log("Message:", currentLanguage === "es" ? violation.messageEs : violation.messageEn); console.log("Recommendation:", violation.recommendation); console.log("BIRHAUS Principle:", violation.birhausPrinciple); console.log("Element:", violation.element); console.groupEnd(); } if (devMode && config.strictMode && violation.severity === "error") { throw new Error(`BIRHAUS Violation: ${violation.messageEn}`); } if (config.analyticsEnabled && analyticsEndpoint) { analyticsQueueRef.current.push({ type: "violation", data: violation, timestamp: (/* @__PURE__ */ new Date()).toISOString() }); } onViolation?.(violation); }, [devMode, config.strictMode, config.detailedLogging, config.analyticsEnabled, currentLanguage, analyticsEndpoint, onViolation]); const reportViolation = useCallback((violation) => { handleViolation({ ...violation, timestamp: /* @__PURE__ */ new Date() }); }, [handleViolation]); const updateConfig = useCallback((newConfig) => { setCurrentConfig((prev) => ({ ...prev, ...newConfig })); }, []); const recalculateScore = useCallback(async () => { const cognitiveViolations = violations.filter((v) => v.type === "cognitive").length; const accessibilityViolations = violations.filter((v) => v.type === "accessibility").length; const spanishViolations = violations.filter((v) => v.type === "spanish").length; const performanceViolations = violations.filter((v) => v.type === "performance").length; const score = { total: Math.max(0, 100 - cognitiveViolations * 5 - accessibilityViolations * 3 - spanishViolations * 2 - performanceViolations * 5), performance: Math.max(0, 15 - performanceViolations * 5), accessibility: Math.max(0, 30 - accessibilityViolations * 3), cognitiveLoad: Math.max(0, 40 - cognitiveViolations * 5), spanishCoverage: Math.max(0, 15 - spanishViolations * 2), violations: violations.map((v) => currentLanguage === "es" ? v.messageEs : v.messageEn), recommendations: violations.map((v) => v.recommendation), timestamp: /* @__PURE__ */ new Date() }; setCurrentScore(score); onScoreChange?.(score); }, [violations, currentLanguage, onScoreChange]); useEffect(() => { if (typeof window === "undefined") return; if (config.cognitiveLoadTracking && !validatorRef.current) { validatorRef.current = new BirhausRuntimeValidator(config, handleViolation); validatorRef.current.start(); } return () => { if (validatorRef.current) { validatorRef.current.stop(); validatorRef.current = null; } }; }, [config, handleViolation]); useEffect(() => { if (!config.analyticsEnabled || !analyticsEndpoint) return; const sendAnalytics = async () => { if (analyticsQueueRef.current.length === 0) return; try { await fetch(analyticsEndpoint, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ events: analyticsQueueRef.current, sessionId: crypto.randomUUID(), userAgent: navigator.userAgent, timestamp: (/* @__PURE__ */ new Date()).toISOString() }) }); analyticsQueueRef.current = []; } catch (error) { console.warn("Failed to send BIRHAUS analytics:", error); } }; const interval = setInterval(sendAnalytics, 3e4); return () => clearInterval(interval); }, [config.analyticsEnabled, analyticsEndpoint]); useEffect(() => { recalculateScore(); }, [violations, recalculateScore]); const contextValue = { config: currentConfig, language: currentLanguage, theme: currentTheme || {}, score: currentScore, violations, t, reportViolation, updateConfig, setLanguage: setCurrentLanguage, recalculateScore }; return /* @__PURE__ */ jsx(BirhausContext.Provider, { value: contextValue, children: /* @__PURE__ */ jsx( "div", { className: "birhaus-provider", "data-birhaus-language": currentLanguage, "data-birhaus-score": currentScore?.total, "data-birhaus-violations": violations.length, style: currentTheme?.cssVariables, children } ) }); } function useBirhaus() { const context = useContext(BirhausContext); if (!context) { throw new Error("useBirhaus must be used within BirhausProvider"); } return context; } var BirhausProvider_default = BirhausProvider; // src/index.ts import { BIRHAUS_PERFORMANCE, BIRHAUS_SCORING, DEFAULT_BIRHAUS_CONFIG, BIRHAUS_ENV_CONFIG as BIRHAUS_ENV_CONFIG2 } from "@birhaus/primitives"; export { BIRHAUS_ENV_CONFIG2 as BIRHAUS_ENV_CONFIG, BIRHAUS_PERFORMANCE, BIRHAUS_SCORING, BirhausProvider, DEFAULT_BIRHAUS_CONFIG, BirhausProvider_default as Provider, useBirhaus };