@birhaus/provider
Version:
BIRHAUS Provider - Context provider for real-time UX validation and cognitive load monitoring
424 lines (420 loc) • 16.6 kB
JavaScript
;
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
// src/index.ts
var index_exports = {};
__export(index_exports, {
BIRHAUS_ENV_CONFIG: () => import_primitives2.BIRHAUS_ENV_CONFIG,
BIRHAUS_PERFORMANCE: () => import_primitives2.BIRHAUS_PERFORMANCE,
BIRHAUS_SCORING: () => import_primitives2.BIRHAUS_SCORING,
BirhausProvider: () => BirhausProvider,
DEFAULT_BIRHAUS_CONFIG: () => import_primitives2.DEFAULT_BIRHAUS_CONFIG,
Provider: () => BirhausProvider_default,
useBirhaus: () => useBirhaus
});
module.exports = __toCommonJS(index_exports);
// src/BirhausProvider.tsx
var import_react = require("react");
var import_primitives = require("@birhaus/primitives");
var import_jsx_runtime = require("react/jsx-runtime");
var BirhausContext = (0, import_react.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 = (0, import_react.useMemo)(() => ({
...import_primitives.BIRHAUS_ENV_CONFIG[process.env.NODE_ENV] || import_primitives.BIRHAUS_ENV_CONFIG.development,
...userConfig
}), [userConfig]);
const [currentLanguage, setCurrentLanguage] = (0, import_react.useState)(language);
const [currentTheme] = (0, import_react.useState)(theme);
const [currentScore, setCurrentScore] = (0, import_react.useState)(null);
const [violations, setViolations] = (0, import_react.useState)([]);
const [currentConfig, setCurrentConfig] = (0, import_react.useState)(config);
const validatorRef = (0, import_react.useRef)(null);
const analyticsQueueRef = (0, import_react.useRef)([]);
const t = (0, import_react.useCallback)((key, fallback) => {
return fallback || key;
}, []);
const handleViolation = (0, import_react.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 = (0, import_react.useCallback)((violation) => {
handleViolation({
...violation,
timestamp: /* @__PURE__ */ new Date()
});
}, [handleViolation]);
const updateConfig = (0, import_react.useCallback)((newConfig) => {
setCurrentConfig((prev) => ({ ...prev, ...newConfig }));
}, []);
const recalculateScore = (0, import_react.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]);
(0, import_react.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]);
(0, import_react.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]);
(0, import_react.useEffect)(() => {
recalculateScore();
}, [violations, recalculateScore]);
const contextValue = {
config: currentConfig,
language: currentLanguage,
theme: currentTheme || {},
score: currentScore,
violations,
t,
reportViolation,
updateConfig,
setLanguage: setCurrentLanguage,
recalculateScore
};
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(BirhausContext.Provider, { value: contextValue, children: /* @__PURE__ */ (0, import_jsx_runtime.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 = (0, import_react.useContext)(BirhausContext);
if (!context) {
throw new Error("useBirhaus must be used within BirhausProvider");
}
return context;
}
var BirhausProvider_default = BirhausProvider;
// src/index.ts
var import_primitives2 = require("@birhaus/primitives");
// Annotate the CommonJS export names for ESM import in node:
0 && (module.exports = {
BIRHAUS_ENV_CONFIG,
BIRHAUS_PERFORMANCE,
BIRHAUS_SCORING,
BirhausProvider,
DEFAULT_BIRHAUS_CONFIG,
Provider,
useBirhaus
});