/** * @sc4rfurryx/proteusjs/adapters/react * React hooks and components for ProteusJS * * @version 2.0.0 * @author sc4rfurry * @license MIT */ import React, { useEffect, useRef, useCallback, RefObject, createElement, useState } from 'react'; // Legacy v1.x imports (still supported) import { transition, TransitionOptions } from '../modules/transitions'; import { scrollAnimate, ScrollAnimateOptions } from '../modules/scroll'; import { attach as attachPopover, PopoverOptions, PopoverController } from '../modules/popover'; import { tether, TetherOptions, TetherController } from '../modules/anchor'; import { defineContainer, ContainerOptions } from '../modules/container'; // v2.0.0 package imports (when available) // Note: These imports will be resolved at runtime to avoid build issues // In production, users should install the individual packages /** * Hook for view transitions (v1.x compatibility) */ export function useTransition() { return useCallback(async ( run: () => Promise | any, opts?: TransitionOptions ) => { return transition(run, opts); }, []); } /** * Hook for Navigation API (v2.0.0) */ export function useNavigation() { const [isNavigating, setIsNavigating] = useState(false); const [currentUrl, setCurrentUrl] = useState(() => typeof window !== 'undefined' ? window.location.href : '' ); const navigate = useCallback(async (url: string, options: any = {}) => { setIsNavigating(true); try { // Dynamic import to avoid build issues const { navigate: navigateImpl } = await import('../../packages/router/src/index.js').catch(() => ({ navigate: (url: string) => { window.history.pushState(null, '', url); setCurrentUrl(url); } })); await navigateImpl(url, options); setCurrentUrl(url); } finally { setIsNavigating(false); } }, []); const back = useCallback(async () => { setIsNavigating(true); try { const { back: backImpl } = await import('../../packages/router/src/index.js').catch(() => ({ back: () => window.history.back() })); backImpl(); } finally { setIsNavigating(false); } }, []); const forward = useCallback(async () => { setIsNavigating(true); try { const { forward: forwardImpl } = await import('../../packages/router/src/index.js').catch(() => ({ forward: () => window.history.forward() })); forwardImpl(); } finally { setIsNavigating(false); } }, []); useEffect(() => { const handlePopState = () => { setCurrentUrl(window.location.href); }; window.addEventListener('popstate', handlePopState); return () => window.removeEventListener('popstate', handlePopState); }, []); return { navigate, back, forward, isNavigating, currentUrl }; } /** * Hook for View Transitions API (v2.0.0) */ export function useViewTransition() { const [isTransitioning, setIsTransitioning] = useState(false); const startTransition = useCallback(async ( updateCallback: () => void | Promise, options: any = {} ) => { setIsTransitioning(true); try { const { viewTransition } = await import('../../packages/transitions/src/index.js').catch(() => ({ viewTransition: async (callback: () => void | Promise) => { await callback(); } })); await viewTransition(updateCallback, options); } finally { setIsTransitioning(false); } }, []); const slideTransition = useCallback(async ( direction: 'left' | 'right' | 'up' | 'down', updateCallback: () => void | Promise, options: any = {} ) => { setIsTransitioning(true); try { const { slideTransition: slideImpl } = await import('../../packages/transitions/src/index.js').catch(() => ({ slideTransition: async (dir: string, callback: () => void | Promise) => { await callback(); } })); await slideImpl(direction, updateCallback, options); } finally { setIsTransitioning(false); } }, []); return { startTransition, slideTransition, isTransitioning, isSupported: typeof document !== 'undefined' && 'startViewTransition' in document }; } /** * Hook for Popover API (v2.0.0) */ export function usePopover() { const [isOpen, setIsOpen] = useState(false); const triggerRef = useRef(null); const contentRef = useRef(null); const controllerRef = useRef(null); const show = useCallback(async () => { if (!triggerRef.current || !contentRef.current) return; try { const { popover } = await import('../../packages/layer/src/index.js').catch(() => ({ popover: () => ({ show: () => {}, hide: () => {} }) })); if (!controllerRef.current) { controllerRef.current = popover(triggerRef.current, contentRef.current, { trigger: 'manual' }); } controllerRef.current.show(); setIsOpen(true); } catch (error) { console.warn('Popover API not available:', error); } }, []); const hide = useCallback(() => { if (controllerRef.current) { controllerRef.current.hide(); setIsOpen(false); } }, []); const toggle = useCallback(() => { if (isOpen) { hide(); } else { show(); } }, [isOpen, show, hide]); return { triggerRef, contentRef, show, hide, toggle, isOpen, isSupported: typeof HTMLElement !== 'undefined' && 'popover' in HTMLElement.prototype }; } /** * Hook for Scheduler API (v2.0.0) */ export function useScheduler() { const [isProcessing, setIsProcessing] = useState(false); const postTask = useCallback(async ( callback: () => any, options: any = {} ) => { setIsProcessing(true); try { const { postTask: postTaskImpl } = await import('../../packages/schedule/src/index.js').catch(() => ({ postTask: async (cb: () => any) => cb() })); return await postTaskImpl(callback, options); } finally { setIsProcessing(false); } }, []); const processInChunks = useCallback(async ( items: any[], processor: (item: any, index: number) => any, options: any = {} ) => { setIsProcessing(true); try { const { processInChunks: processImpl } = await import('../../packages/schedule/src/index.js').catch(() => ({ processInChunks: async (items: any[], proc: any) => items.map(proc) })); return await processImpl(items, processor, options); } finally { setIsProcessing(false); } }, []); const yieldToMain = useCallback(async (options: any = {}) => { const { yieldToMain: yieldImpl } = await import('../../packages/schedule/src/index.js').catch(() => ({ yieldToMain: async () => new Promise(resolve => setTimeout(resolve, 0)) })); return await yieldImpl(options); }, []); return { postTask, processInChunks, yieldToMain, isProcessing, isSupported: typeof window !== 'undefined' && 'scheduler' in window }; } /** * Hook for PWA APIs (v2.0.0) */ export function usePWA() { const [isInstallable, setIsInstallable] = useState(false); const [isInstalled, setIsInstalled] = useState(false); const deferredPromptRef = useRef(null); useEffect(() => { // Check if already installed const checkInstalled = () => { const isStandalone = window.matchMedia('(display-mode: standalone)').matches || (window.navigator as any).standalone === true; setIsInstalled(isStandalone); }; checkInstalled(); // Listen for install prompt const handleBeforeInstallPrompt = (e: any) => { e.preventDefault(); deferredPromptRef.current = e; setIsInstallable(true); }; window.addEventListener('beforeinstallprompt', handleBeforeInstallPrompt); return () => { window.removeEventListener('beforeinstallprompt', handleBeforeInstallPrompt); }; }, []); const install = useCallback(async () => { if (!deferredPromptRef.current) return false; try { deferredPromptRef.current.prompt(); const result = await deferredPromptRef.current.userChoice; if (result.outcome === 'accepted') { setIsInstallable(false); setIsInstalled(true); return true; } return false; } catch (error) { console.warn('Install prompt failed:', error); return false; } }, []); const share = useCallback(async (data: any) => { try { const { Share } = await import('../../packages/pwa/src/index.js').catch(() => ({ Share: { share: async () => {} } })); await Share.share(data); return true; } catch (error) { console.warn('Web Share failed:', error); return false; } }, []); const setBadge = useCallback(async (count?: number) => { try { const { Badging } = await import('../../packages/pwa/src/index.js').catch(() => ({ Badging: { set: async () => {} } })); await Badging.set({ count }); } catch (error) { console.warn('Badging failed:', error); } }, []); const clearBadge = useCallback(async () => { try { const { Badging } = await import('../../packages/pwa/src/index.js').catch(() => ({ Badging: { clear: async () => {} } })); await Badging.clear(); } catch (error) { console.warn('Badging clear failed:', error); } }, []); return { install, share, setBadge, clearBadge, isInstallable, isInstalled, capabilities: { share: 'share' in navigator, badging: 'setAppBadge' in navigator, fileSystem: 'showOpenFilePicker' in window } }; } /** * Hook for scroll-driven animations (legacy v1.x) */ export function useScrollAnimate( ref: RefObject, opts: ScrollAnimateOptions ) { useEffect(() => { if (!ref.current) return; scrollAnimate(ref.current, opts); return () => { // Cleanup would be handled by the scroll module }; }, [ref, opts]); } /** * Hook for popover functionality (legacy v1.x) */ export function useLegacyPopover( triggerRef: RefObject, panelRef: RefObject, opts?: PopoverOptions ): PopoverController | null { const controllerRef = useRef(null); useEffect(() => { if (!triggerRef.current || !panelRef.current) return; controllerRef.current = attachPopover(triggerRef.current, panelRef.current, opts); return () => { if (controllerRef.current) { controllerRef.current.destroy(); controllerRef.current = null; } }; }, [triggerRef, panelRef, opts]); return controllerRef.current; } /** * Hook for anchor positioning */ export function useAnchor( floatingRef: RefObject, anchorRef: RefObject, opts?: Omit ): TetherController | null { const controllerRef = useRef(null); useEffect(() => { if (!floatingRef.current || !anchorRef.current) return; controllerRef.current = tether(floatingRef.current, { anchor: anchorRef.current, ...opts }); return () => { if (controllerRef.current) { controllerRef.current.destroy(); controllerRef.current = null; } }; }, [floatingRef, anchorRef, opts]); return controllerRef.current; } /** * Hook for container queries */ export function useContainer( ref: RefObject, name?: string, opts?: ContainerOptions ) { useEffect(() => { if (!ref.current) return; defineContainer(ref.current, name, opts); }, [ref, name, opts]); } /** * Hook for performance optimizations */ export function usePerformance(ref: RefObject) { useEffect(() => { if (!ref.current) return; // Apply basic performance optimizations const element = ref.current; // Content visibility for off-screen content const observer = new IntersectionObserver( (entries) => { entries.forEach(entry => { if (entry.isIntersecting) { element.style.contentVisibility = 'visible'; } else { element.style.contentVisibility = 'auto'; } }); }, { rootMargin: '50px' } ); observer.observe(element); return () => { observer.disconnect(); }; }, [ref]); } /** * Hook for accessibility features */ export function useA11y(ref: RefObject, options: { announceChanges?: boolean; focusManagement?: boolean; } = {}): void { const { announceChanges = false, focusManagement = true } = options; useEffect(() => { if (!ref.current) return; const element = ref.current; // Basic accessibility enhancements if (focusManagement) { // Ensure focusable elements have visible focus indicators const focusableElements = element.querySelectorAll( 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])' ); focusableElements.forEach((el: Element) => { const htmlEl = el as HTMLElement; if (!htmlEl.style.outline && !getComputedStyle(htmlEl).outline) { htmlEl.style.outline = '2px solid transparent'; htmlEl.style.outlineOffset = '2px'; htmlEl.addEventListener('focus', () => { htmlEl.style.outline = '2px solid #0066cc'; }); htmlEl.addEventListener('blur', () => { htmlEl.style.outline = '2px solid transparent'; }); } }); } if (announceChanges) { // Set up mutation observer for announcing changes const observer = new MutationObserver((mutations) => { mutations.forEach(mutation => { if (mutation.type === 'childList' && mutation.addedNodes.length > 0) { // Announce significant changes to screen readers const announcement = document.createElement('div'); announcement.setAttribute('aria-live', 'polite'); announcement.setAttribute('aria-atomic', 'true'); announcement.style.position = 'absolute'; announcement.style.left = '-10000px'; announcement.textContent = 'Content updated'; document.body.appendChild(announcement); setTimeout(() => { document.body.removeChild(announcement); }, 1000); } }); }); observer.observe(element, { childList: true, subtree: true }); return () => { observer.disconnect(); }; } }, [ref, announceChanges, focusManagement]); } /** * Higher-order component for adding ProteusJS features */ export function withProteus

( Component: React.ComponentType

, features: { container?: { name?: string; options?: ContainerOptions }; performance?: boolean; accessibility?: boolean; } = {} ) { return function ProteusEnhanced(props: P) { const ref = useRef(null); if (features.container) { useContainer(ref, features.container.name, features.container.options); } if (features.performance) { usePerformance(ref); } if (features.accessibility) { useA11y(ref); } return createElement( 'div', { ref }, createElement(Component, props) ); }; } // Export all hooks and utilities export default { useTransition, useScrollAnimate, usePopover, useAnchor, useContainer, usePerformance, useA11y, withProteus };