@restnfeel/agentc-starter-kit
Version:
한국어 기업용 CMS 모듈 - Task Master AI와 함께 빠르게 웹사이트를 구현할 수 있는 재사용 가능한 컴포넌트 시스템
314 lines (311 loc) • 13.1 kB
JavaScript
"use client";
import { jsx } from 'react/jsx-runtime';
import { useRef, useEffect, useCallback, useState } from 'react';
// Hook for detecting keyboard visibility and viewport changes
function useKeyboardDetection() {
const [keyboardState, setKeyboardState] = useState({
isVisible: false,
height: 0,
duration: 300,
easing: "ease-out",
});
const [viewportInfo, setViewportInfo] = useState({
width: 0,
height: 0,
visualHeight: 0,
keyboardHeight: 0,
});
useEffect(() => {
if (typeof window === "undefined")
return;
let initialHeight = window.innerHeight;
const updateViewport = () => {
const currentHeight = window.innerHeight;
const currentWidth = window.innerWidth;
// Use Visual Viewport API if available
let visualViewportHeight = currentHeight;
if ("visualViewport" in window && window.visualViewport) {
visualViewportHeight = window.visualViewport.height;
}
const keyboardHeight = Math.max(0, initialHeight - visualViewportHeight);
const keyboardVisible = keyboardHeight > 150; // Threshold for keyboard detection
setKeyboardState((prev) => ({
...prev,
isVisible: keyboardVisible,
height: keyboardHeight,
}));
setViewportInfo({
width: currentWidth,
height: currentHeight,
visualHeight: visualViewportHeight,
keyboardHeight,
});
};
// Handle orientation changes
const handleOrientationChange = () => {
// Wait for orientation change to complete
setTimeout(() => {
initialHeight = window.innerHeight;
updateViewport();
}, 500);
};
// Multiple event listeners for comprehensive coverage
const events = ["resize", "orientationchange"];
events.forEach((event) => {
window.addEventListener(event, updateViewport);
});
// Special handling for orientation change
window.addEventListener("orientationchange", handleOrientationChange);
// Visual Viewport API listeners
if ("visualViewport" in window && window.visualViewport) {
window.visualViewport.addEventListener("resize", updateViewport);
window.visualViewport.addEventListener("scroll", updateViewport);
}
// iOS specific handling
if (/iPhone|iPad|iPod/.test(navigator.userAgent)) {
// iOS specific events
document.addEventListener("focusin", updateViewport);
document.addEventListener("focusout", updateViewport);
}
// Initial update
updateViewport();
return () => {
events.forEach((event) => {
window.removeEventListener(event, updateViewport);
});
window.removeEventListener("orientationchange", handleOrientationChange);
if ("visualViewport" in window && window.visualViewport) {
window.visualViewport.removeEventListener("resize", updateViewport);
window.visualViewport.removeEventListener("scroll", updateViewport);
}
if (/iPhone|iPad|iPod/.test(navigator.userAgent)) {
document.removeEventListener("focusin", updateViewport);
document.removeEventListener("focusout", updateViewport);
}
};
}, []);
return { ...keyboardState, ...viewportInfo };
}
// Hook for managing input focus and keyboard behavior
function useMobileInputFocus() {
const scrollToInput = useCallback((inputElement) => {
if (!inputElement)
return;
// Prevent default zoom behavior on iOS
const originalFontSize = inputElement.style.fontSize;
document.querySelector('meta[name="viewport"]');
// Temporarily set font size to prevent zoom
inputElement.style.fontSize = "16px";
// Scroll input into view
setTimeout(() => {
inputElement.scrollIntoView({
behavior: "smooth",
block: "center",
inline: "nearest",
});
// Focus the input
inputElement.focus();
// Restore original font size
if (originalFontSize) {
inputElement.style.fontSize = originalFontSize;
}
}, 100);
}, []);
const preventZoom = useCallback((inputElement) => {
// Prevent zoom on input focus (iOS)
const currentFontSize = window.getComputedStyle(inputElement).fontSize;
const fontSize = parseFloat(currentFontSize);
if (fontSize < 16) {
inputElement.style.fontSize = "16px";
inputElement.style.transform = `scale(${fontSize / 16})`;
inputElement.style.transformOrigin = "left center";
}
}, []);
const handleInputFocus = useCallback((event) => {
const input = event.target;
preventZoom(input);
scrollToInput(input);
}, [preventZoom, scrollToInput]);
return { scrollToInput, preventZoom, handleInputFocus };
}
// Component for handling keyboard adjustments
function MobileKeyboardHandler({ children, onKeyboardToggle, adjustmentStrategy = "resize", className = "", enableSafeArea = true, minVisibleHeight = 200, }) {
const keyboard = useKeyboardDetection();
const containerRef = useRef(null);
// Notify parent of keyboard changes
useEffect(() => {
onKeyboardToggle === null || onKeyboardToggle === void 0 ? void 0 : onKeyboardToggle(keyboard.isVisible, keyboard.height);
}, [keyboard.isVisible, keyboard.height, onKeyboardToggle]);
// Apply adjustments based on strategy
const getContainerStyle = useCallback(() => {
const baseStyle = {
position: "relative",
transition: `all ${keyboard.duration}ms ${keyboard.easing}`,
width: "100%",
};
if (!keyboard.isVisible) {
return {
...baseStyle,
height: "100vh",
maxHeight: "100vh",
};
}
switch (adjustmentStrategy) {
case "resize":
return {
...baseStyle,
height: `${keyboard.visualHeight}px`,
maxHeight: `${keyboard.visualHeight}px`,
overflow: "hidden",
};
case "scroll":
return {
...baseStyle,
height: "100vh",
paddingBottom: `${keyboard.keyboardHeight}px`,
overflowY: "auto",
};
case "shrink":
const availableHeight = Math.max(keyboard.visualHeight, minVisibleHeight);
return {
...baseStyle,
height: `${availableHeight}px`,
maxHeight: `${availableHeight}px`,
transform: keyboard.isVisible
? `translateY(-${Math.max(0, keyboard.keyboardHeight - (keyboard.height - availableHeight))}px)`
: "translateY(0)",
};
default:
return baseStyle;
}
}, [
keyboard.isVisible,
keyboard.visualHeight,
keyboard.keyboardHeight,
keyboard.height,
keyboard.duration,
keyboard.easing,
adjustmentStrategy,
minVisibleHeight,
]);
// Apply safe area styles for devices with notches
const getSafeAreaStyle = useCallback(() => {
if (!enableSafeArea)
return {};
return {
paddingTop: "env(safe-area-inset-top)",
paddingLeft: "env(safe-area-inset-left)",
paddingRight: "env(safe-area-inset-right)",
paddingBottom: keyboard.isVisible ? "0" : "env(safe-area-inset-bottom)",
};
}, [enableSafeArea, keyboard.isVisible]);
return (jsx("div", { ref: containerRef, className: `mobile-keyboard-handler ${className}`, style: {
...getContainerStyle(),
...getSafeAreaStyle(),
}, "data-keyboard-visible": keyboard.isVisible, "data-keyboard-height": keyboard.height, children: children }));
}
function KeyboardAwareInput({ value, onChange, onSubmit, placeholder, disabled = false, className = "", autoFocus = false, keepVisible = true, }) {
const inputRef = useRef(null);
const { handleInputFocus } = useMobileInputFocus();
const keyboard = useKeyboardDetection();
// Keep input visible when keyboard appears
useEffect(() => {
if (keepVisible && keyboard.isVisible && inputRef.current) {
const input = inputRef.current;
const inputRect = input.getBoundingClientRect();
const viewportHeight = keyboard.visualHeight;
// Check if input is hidden by keyboard
if (inputRect.bottom > viewportHeight) {
input.scrollIntoView({
behavior: "smooth",
block: "end",
});
}
}
}, [keyboard.isVisible, keyboard.visualHeight, keepVisible]);
const handleKeyPress = useCallback((e) => {
if (e.key === "Enter" && onSubmit) {
e.preventDefault();
onSubmit();
}
}, [onSubmit]);
return (jsx("input", { ref: inputRef, type: "text", value: value, onChange: (e) => onChange(e.target.value), onKeyPress: handleKeyPress, onFocus: handleInputFocus, placeholder: placeholder, disabled: disabled, autoFocus: autoFocus, className: className, style: {
fontSize: "16px", // Prevent zoom on iOS
minHeight: "48px",
padding: "12px 16px",
borderRadius: "24px",
border: "2px solid #e1e5e9",
outline: "none",
width: "100%",
backgroundColor: "#f8f9fa",
transition: "border-color 0.2s ease",
touchAction: "manipulation",
WebkitAppearance: "none", // Remove iOS styling
} }));
}
// Hook for back button handling (Android/Browser)
function useBackButtonHandler(onBackPress) {
useEffect(() => {
if (typeof window === "undefined" || !onBackPress)
return;
// Handle browser back button
const handlePopState = (event) => {
event.preventDefault();
onBackPress();
};
// Push a state to handle back button
window.history.pushState({ modal: true }, "", "");
window.addEventListener("popstate", handlePopState);
return () => {
var _a;
window.removeEventListener("popstate", handlePopState);
// Clean up history state if still on the same state
if ((_a = window.history.state) === null || _a === void 0 ? void 0 : _a.modal) {
window.history.back();
}
};
}, [onBackPress]);
}
// Hook for preventing body scroll when modal is open
function useScrollLock(isLocked) {
useEffect(() => {
if (typeof document === "undefined")
return;
const body = document.body;
const scrollBarWidth = window.innerWidth - document.documentElement.clientWidth;
if (isLocked) {
window.getComputedStyle(body);
const originalTop = body.style.top;
const originalPosition = body.style.position;
const originalOverflow = body.style.overflow;
const originalPaddingRight = body.style.paddingRight;
// Lock scroll
body.style.overflow = "hidden";
body.style.paddingRight = `${scrollBarWidth}px`;
// iOS specific fixes
if (/iPhone|iPad|iPod/.test(navigator.userAgent)) {
const scrollTop = window.pageYOffset || document.documentElement.scrollTop;
body.style.position = "fixed";
body.style.top = `-${scrollTop}px`;
body.style.left = "0";
body.style.right = "0";
}
return () => {
// Restore scroll
body.style.overflow = originalOverflow;
body.style.paddingRight = originalPaddingRight;
if (/iPhone|iPad|iPod/.test(navigator.userAgent)) {
body.style.position = originalPosition;
body.style.top = originalTop;
body.style.left = "";
body.style.right = "";
// Restore scroll position
const top = parseInt(originalTop || "0", 10) * -1;
window.scrollTo(0, top);
}
};
}
}, [isLocked]);
}
export { KeyboardAwareInput, MobileKeyboardHandler, useBackButtonHandler, useKeyboardDetection, useMobileInputFocus, useScrollLock };
//# sourceMappingURL=mobile-keyboard-handler.js.map