UNPKG

circuit-bricks

Version:

A modular, Lego-style SVG circuit component system for React (ALPHA - Not for production use)

377 lines (375 loc) 11 kB
"use client"; "use strict"; const require_chunk = require('./chunk-DkEB4ore.cjs'); const require_ssrUtils = require('./ssrUtils-BTgsP4pb.cjs'); const react = require_chunk.__toESM(require("react")); //#region src/utils/touchUtils.ts /** * Default touch configuration */ const defaultTouchConfig = { tapThreshold: 10, longPressDelay: 500, doubleTapDelay: 300, swipeThreshold: 50, swipeVelocityThreshold: .5, pinchThreshold: .1, preventDefaultOnTouch: true }; /** * Calculate distance between two points */ const getDistance = (p1, p2) => { const dx = p2.x - p1.x; const dy = p2.y - p1.y; return Math.sqrt(dx * dx + dy * dy); }; /** * Calculate distance between two touches */ const getTouchDistance = (touch1, touch2) => { return getDistance({ x: touch1.clientX, y: touch1.clientY }, { x: touch2.clientX, y: touch2.clientY }); }; /** * Get touch point from touch event */ const getTouchPoint = (touch) => ({ x: touch.clientX, y: touch.clientY }); /** * Hook for handling touch gestures */ const useTouchGestures = (onGesture, config = {}) => { const touchConfig = { ...defaultTouchConfig, ...config }; const stateRef = (0, react.useRef)({ isActive: false, startTime: 0, startPoint: { x: 0, y: 0 }, currentPoint: { x: 0, y: 0 }, lastTapTime: 0, touches: [], initialScale: 1 }); const longPressTimerRef = (0, react.useRef)(void 0); const clearLongPressTimer = (0, react.useCallback)(() => { if (longPressTimerRef.current) { clearTimeout(longPressTimerRef.current); longPressTimerRef.current = void 0; } }, []); const handleTouchStart = (0, react.useCallback)((event) => { if (touchConfig.preventDefaultOnTouch) event.preventDefault(); const touch = event.touches[0]; const currentTime = Date.now(); const touchPoint = getTouchPoint(touch); stateRef.current = { isActive: true, startTime: currentTime, startPoint: touchPoint, currentPoint: touchPoint, lastTapTime: stateRef.current.lastTapTime, touches: Array.from(event.touches), initialScale: 1 }; longPressTimerRef.current = setTimeout(() => { if (stateRef.current.isActive) { const distance = getDistance(stateRef.current.startPoint, stateRef.current.currentPoint); if (distance < touchConfig.tapThreshold) onGesture({ type: "long-press", startPoint: stateRef.current.startPoint, currentPoint: stateRef.current.currentPoint, deltaX: 0, deltaY: 0, distance: 0, duration: currentTime - stateRef.current.startTime }); } }, touchConfig.longPressDelay); if (event.touches.length === 2) stateRef.current.initialDistance = getTouchDistance(event.touches[0], event.touches[1]); }, [onGesture, touchConfig]); const handleTouchMove = (0, react.useCallback)((event) => { if (!stateRef.current.isActive) return; if (touchConfig.preventDefaultOnTouch) event.preventDefault(); const touch = event.touches[0]; const currentPoint = getTouchPoint(touch); const currentTime = Date.now(); stateRef.current.currentPoint = currentPoint; stateRef.current.touches = Array.from(event.touches); const deltaX = currentPoint.x - stateRef.current.startPoint.x; const deltaY = currentPoint.y - stateRef.current.startPoint.y; const distance = Math.sqrt(deltaX * deltaX + deltaY * deltaY); const duration = currentTime - stateRef.current.startTime; if (distance > touchConfig.tapThreshold) clearLongPressTimer(); if (event.touches.length === 2 && stateRef.current.initialDistance) { const currentDistance = getTouchDistance(event.touches[0], event.touches[1]); const scale = currentDistance / stateRef.current.initialDistance; if (Math.abs(scale - stateRef.current.initialScale) > touchConfig.pinchThreshold) { onGesture({ type: "pinch", startPoint: stateRef.current.startPoint, currentPoint, deltaX, deltaY, distance, scale, duration }); stateRef.current.initialScale = scale; } } else if (event.touches.length === 1 && distance > touchConfig.tapThreshold) { const velocity = { x: deltaX / Math.max(duration, 1), y: deltaY / Math.max(duration, 1) }; onGesture({ type: "pan", startPoint: stateRef.current.startPoint, currentPoint, deltaX, deltaY, distance, velocity, duration }); } }, [ onGesture, touchConfig, clearLongPressTimer ]); const handleTouchEnd = (0, react.useCallback)((event) => { if (!stateRef.current.isActive) return; if (touchConfig.preventDefaultOnTouch) event.preventDefault(); clearLongPressTimer(); const currentTime = Date.now(); const deltaX = stateRef.current.currentPoint.x - stateRef.current.startPoint.x; const deltaY = stateRef.current.currentPoint.y - stateRef.current.startPoint.y; const distance = Math.sqrt(deltaX * deltaX + deltaY * deltaY); const duration = currentTime - stateRef.current.startTime; if (distance < touchConfig.tapThreshold && duration < touchConfig.longPressDelay) { const timeSinceLastTap = currentTime - stateRef.current.lastTapTime; if (timeSinceLastTap < touchConfig.doubleTapDelay) { onGesture({ type: "double-tap", startPoint: stateRef.current.startPoint, currentPoint: stateRef.current.currentPoint, deltaX: 0, deltaY: 0, distance: 0, duration }); stateRef.current.lastTapTime = 0; } else { onGesture({ type: "tap", startPoint: stateRef.current.startPoint, currentPoint: stateRef.current.currentPoint, deltaX: 0, deltaY: 0, distance: 0, duration }); stateRef.current.lastTapTime = currentTime; } } else if (distance > touchConfig.swipeThreshold) { const velocity = { x: deltaX / Math.max(duration, 1), y: deltaY / Math.max(duration, 1) }; const velocityMagnitude = Math.sqrt(velocity.x * velocity.x + velocity.y * velocity.y); if (velocityMagnitude > touchConfig.swipeVelocityThreshold) onGesture({ type: "swipe", startPoint: stateRef.current.startPoint, currentPoint: stateRef.current.currentPoint, deltaX, deltaY, distance, velocity, duration }); } stateRef.current.isActive = false; }, [ onGesture, touchConfig, clearLongPressTimer ]); const touchHandlers = { onTouchStart: handleTouchStart, onTouchMove: handleTouchMove, onTouchEnd: handleTouchEnd, onTouchCancel: handleTouchEnd }; return touchHandlers; }; /** * Hook for detecting device orientation */ const useDeviceOrientation = () => { const [orientation, setOrientation] = (0, react.useState)("portrait"); (0, react.useEffect)(() => { if (!require_ssrUtils.isBrowser()) return; const updateOrientation = () => { setOrientation(window.innerHeight > window.innerWidth ? "portrait" : "landscape"); }; updateOrientation(); window.addEventListener("resize", updateOrientation); window.addEventListener("orientationchange", updateOrientation); return () => { window.removeEventListener("resize", updateOrientation); window.removeEventListener("orientationchange", updateOrientation); }; }, []); return orientation; }; /** * Hook for safe area insets (for devices with notches) */ const useSafeAreaInsets = () => { const [insets, setInsets] = (0, react.useState)({ top: 0, right: 0, bottom: 0, left: 0 }); (0, react.useEffect)(() => { if (!require_ssrUtils.isBrowser()) return; const updateInsets = () => { const computedStyle = getComputedStyle(document.documentElement); setInsets({ top: parseInt(computedStyle.getPropertyValue("env(safe-area-inset-top)") || "0"), right: parseInt(computedStyle.getPropertyValue("env(safe-area-inset-right)") || "0"), bottom: parseInt(computedStyle.getPropertyValue("env(safe-area-inset-bottom)") || "0"), left: parseInt(computedStyle.getPropertyValue("env(safe-area-inset-left)") || "0") }); }; updateInsets(); window.addEventListener("resize", updateInsets); return () => { window.removeEventListener("resize", updateInsets); }; }, []); return insets; }; /** * Prevent zoom on double tap (iOS Safari) */ const usePreventZoom = (enabled = true) => { (0, react.useEffect)(() => { if (!require_ssrUtils.isBrowser() || !enabled) return; const preventDefault = (e) => { if (e.touches.length > 1) e.preventDefault(); }; const preventZoom = (e) => { e.preventDefault(); }; document.addEventListener("touchstart", preventDefault, { passive: false }); document.addEventListener("gesturestart", preventZoom, { passive: false }); return () => { document.removeEventListener("touchstart", preventDefault); document.removeEventListener("gesturestart", preventZoom); }; }, [enabled]); }; /** * SSR-safe user agent detection (internal) */ const useUserAgentInternal = () => { const [userAgent, setUserAgent] = (0, react.useState)(""); (0, react.useEffect)(() => { if (require_ssrUtils.isBrowser()) setUserAgent(navigator.userAgent); }, []); return userAgent; }; /** * SSR-safe media query hook (internal) */ const useMediaQueryInternal = (query) => { const [matches, setMatches] = (0, react.useState)(false); (0, react.useEffect)(() => { if (!require_ssrUtils.isBrowser()) return; const mediaQuery = window.matchMedia(query); setMatches(mediaQuery.matches); const handler = (event) => { setMatches(event.matches); }; mediaQuery.addEventListener("change", handler); return () => { mediaQuery.removeEventListener("change", handler); }; }, [query]); return matches; }; /** * Detect mobile devices */ const useIsMobileTouch = () => { const userAgent = useUserAgentInternal(); const isSmallScreen = useMediaQueryInternal("(max-width: 768px)"); const isMobileUA = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(userAgent); return isMobileUA || isSmallScreen; }; /** * Detect touch support */ const useHasTouchSupportTouch = () => { const [hasTouch, setHasTouch] = (0, react.useState)(false); (0, react.useEffect)(() => { if (require_ssrUtils.isBrowser()) setHasTouch("ontouchstart" in window || navigator.maxTouchPoints > 0 || navigator.msMaxTouchPoints > 0); }, []); return hasTouch; }; //#endregion Object.defineProperty(exports, 'useDeviceOrientation', { enumerable: true, get: function () { return useDeviceOrientation; } }); Object.defineProperty(exports, 'useHasTouchSupportTouch', { enumerable: true, get: function () { return useHasTouchSupportTouch; } }); Object.defineProperty(exports, 'useIsMobileTouch', { enumerable: true, get: function () { return useIsMobileTouch; } }); Object.defineProperty(exports, 'usePreventZoom', { enumerable: true, get: function () { return usePreventZoom; } }); Object.defineProperty(exports, 'useSafeAreaInsets', { enumerable: true, get: function () { return useSafeAreaInsets; } }); Object.defineProperty(exports, 'useTouchGestures', { enumerable: true, get: function () { return useTouchGestures; } }); //# sourceMappingURL=touchUtils-_BBqMLob.cjs.map