UNPKG

@andreaswissel/uiflow

Version:

Adaptive UI density management library with progressive disclosure, element dependencies, A/B testing, and intelligent behavior-based adaptation

275 lines (228 loc) 7.62 kB
/** * UIFlow React Adapter * React hooks and components for UIFlow integration */ import { useState, useEffect, useRef, useCallback, createContext, useContext } from 'react'; import { UIFlow } from '../../index.js'; // React Context for UIFlow instance const UIFlowContext = createContext(null); /** * UIFlow Provider Component * Wraps your app to provide UIFlow instance to all child components */ export function UIFlowProvider({ children, config = {}, onReady = null }) { const [uiflow, setUIFlow] = useState(null); const [isReady, setIsReady] = useState(false); useEffect(() => { const instance = new UIFlow(); instance.init(config).then(() => { setUIFlow(instance); setIsReady(true); if (onReady) onReady(instance); }).catch(console.error); return () => { if (instance) { instance.destroy(); } }; }, []); return React.createElement(UIFlowContext.Provider, { value: { uiflow, isReady } }, children); } /** * Hook to access UIFlow instance */ export function useUIFlow() { const context = useContext(UIFlowContext); if (!context) { throw new Error('useUIFlow must be used within UIFlowProvider'); } return context; } /** * Hook for element categorization with ref */ export function useUIFlowElement(category, area = 'default', options = {}) { const elementRef = useRef(null); const { uiflow, isReady } = useUIFlow(); const [isVisible, setIsVisible] = useState(true); useEffect(() => { if (!isReady || !uiflow || !elementRef.current) return; // Categorize element uiflow.categorize(elementRef.current, category, area, options); // Listen for visibility changes const handleVisibilityChange = (event) => { if (event.detail.area === area) { const shouldShow = uiflow.shouldShowElement(category, area); setIsVisible(shouldShow); } }; document.addEventListener('uiflow:density-changed', handleVisibilityChange); document.addEventListener('uiflow:adaptation', handleVisibilityChange); document.addEventListener('uiflow:override-applied', handleVisibilityChange); // Initial visibility check setIsVisible(uiflow.shouldShowElement(category, area)); return () => { document.removeEventListener('uiflow:density-changed', handleVisibilityChange); document.removeEventListener('uiflow:adaptation', handleVisibilityChange); document.removeEventListener('uiflow:override-applied', handleVisibilityChange); }; }, [isReady, category, area, options.helpText, options.isNew]); return { elementRef, isVisible, uiflow: isReady ? uiflow : null }; } /** * Hook for area density tracking */ export function useAreaDensity(area = 'default') { const { uiflow, isReady } = useUIFlow(); const [density, setDensity] = useState(0.3); const [hasOverride, setHasOverride] = useState(false); useEffect(() => { if (!isReady || !uiflow) return; const updateDensity = () => { setDensity(uiflow.getDensityLevel(area)); setHasOverride(uiflow.hasOverride(area)); }; // Listen for density changes const handleDensityChange = (event) => { if (event.detail.area === area) { updateDensity(); } }; const handleOverride = (event) => { if (event.detail.area === area) { updateDensity(); } }; document.addEventListener('uiflow:density-changed', handleDensityChange); document.addEventListener('uiflow:adaptation', handleDensityChange); document.addEventListener('uiflow:override-applied', handleOverride); document.addEventListener('uiflow:override-cleared', handleOverride); // Initial value updateDensity(); return () => { document.removeEventListener('uiflow:density-changed', handleDensityChange); document.removeEventListener('uiflow:adaptation', handleDensityChange); document.removeEventListener('uiflow:override-applied', handleOverride); document.removeEventListener('uiflow:override-cleared', handleOverride); }; }, [isReady, area]); const setDensityLevel = useCallback((level) => { if (uiflow) { uiflow.setDensityLevel(level, area); } }, [uiflow, area]); return { density, hasOverride, setDensityLevel }; } /** * Hook for element highlighting */ export function useUIFlowHighlight() { const { uiflow, isReady } = useUIFlow(); const highlight = useCallback((elementId, style = 'default', options = {}) => { if (uiflow) { return uiflow.highlightElement(elementId, style, options); } }, [uiflow]); const removeHighlight = useCallback((elementId) => { if (uiflow) { return uiflow.removeHighlight(elementId); } }, [uiflow]); const flagAsNew = useCallback((elementId, helpText, duration) => { if (uiflow) { return uiflow.flagAsNew(elementId, helpText, duration); } }, [uiflow]); const showTooltip = useCallback((elementId, text, options) => { if (uiflow) { return uiflow.showTooltip(elementId, text, options); } }, [uiflow]); return { highlight, removeHighlight, flagAsNew, showTooltip }; } /** * UIFlow Element Component * Wrapper component that automatically categorizes and manages visibility */ export function UIFlowElement({ category, area = 'default', helpText = null, isNew = false, children, as = 'div', fallback = null, ...props }) { const { elementRef, isVisible } = useUIFlowElement(category, area, { helpText, isNew }); if (!isVisible) { return fallback; } return React.createElement(as, { ref: elementRef, ...props }, children); } /** * Conditional render based on density level */ export function UIFlowConditional({ area = 'default', minDensity = 0, maxDensity = 1, children, fallback = null }) { const { density } = useAreaDensity(area); const shouldRender = density >= minDensity && density <= maxDensity; return shouldRender ? children : fallback; } /** * Area density display component */ export function UIFlowDensityIndicator({ area = 'default', showOverride = true }) { const { density, hasOverride } = useAreaDensity(area); return React.createElement('div', { className: 'uiflow-density-indicator', style: { padding: '4px 8px', backgroundColor: hasOverride ? '#fbbf24' : '#3b82f6', color: 'white', borderRadius: '4px', fontSize: '12px', fontWeight: 'bold' } }, [ React.createElement('span', { key: 'area' }, `${area}: `), React.createElement('span', { key: 'density' }, `${Math.round(density * 100)}%`), showOverride && hasOverride && React.createElement('span', { key: 'override', style: { marginLeft: '4px', opacity: 0.8 } }, '(override)') ]); } /** * Hook for UIFlow events */ export function useUIFlowEvents(eventHandlers = {}) { const { isReady } = useUIFlow(); useEffect(() => { if (!isReady) return; const listeners = []; Object.entries(eventHandlers).forEach(([eventName, handler]) => { const fullEventName = eventName.startsWith('uiflow:') ? eventName : `uiflow:${eventName}`; const listener = (event) => handler(event.detail); document.addEventListener(fullEventName, listener); listeners.push([fullEventName, listener]); }); return () => { listeners.forEach(([eventName, listener]) => { document.removeEventListener(eventName, listener); }); }; }, [isReady, eventHandlers]); } // Export React version for compatibility check export const REACT_ADAPTER_VERSION = '1.0.0';