UNPKG

@productshiv/baapui

Version:

A truly cross-platform multi-design UI framework that works with React, Next.js, React Native, and any React-based framework.

1,464 lines (1,449 loc) 124 kB
import React, { createContext, useState, useCallback, useContext, useMemo, useEffect } from 'react'; import { jsx, jsxs } from 'react/jsx-runtime'; // Platform abstraction layer for cross-platform compatibility // Platform detection using environment-specific globals const PlatformInfo = { isReactNative: (typeof navigator !== 'undefined' && navigator.product === 'ReactNative') || (typeof global !== 'undefined' && // @ts-ignore global.__DEV__ !== undefined && typeof window === 'undefined'), isWeb: typeof window !== 'undefined' && typeof document !== 'undefined', }; // Enhanced style conversion function const convertRNStyleToCSS = (style) => { var _a, _b, _c, _d; if (!style) return {}; const cssStyle = { ...style }; // Handle React Native specific properties if (style.paddingHorizontal !== undefined) { cssStyle.paddingLeft = style.paddingHorizontal; cssStyle.paddingRight = style.paddingHorizontal; delete cssStyle.paddingHorizontal; } if (style.paddingVertical !== undefined) { cssStyle.paddingTop = style.paddingVertical; cssStyle.paddingBottom = style.paddingVertical; delete cssStyle.paddingVertical; } if (style.marginHorizontal !== undefined) { cssStyle.marginLeft = style.marginHorizontal; cssStyle.marginRight = style.marginHorizontal; delete cssStyle.marginHorizontal; } if (style.marginVertical !== undefined) { cssStyle.marginTop = style.marginVertical; cssStyle.marginBottom = style.marginVertical; delete cssStyle.marginVertical; } // Handle text shadow properties if (style.textShadowColor || style.textShadowOffset || style.textShadowRadius) { const color = style.textShadowColor || 'rgba(0,0,0,0.3)'; const offsetX = ((_a = style.textShadowOffset) === null || _a === void 0 ? void 0 : _a.width) || 1; const offsetY = ((_b = style.textShadowOffset) === null || _b === void 0 ? void 0 : _b.height) || 1; const radius = style.textShadowRadius || 1; cssStyle.textShadow = `${offsetX}px ${offsetY}px ${radius}px ${color}`; delete cssStyle.textShadowColor; delete cssStyle.textShadowOffset; delete cssStyle.textShadowRadius; } // Handle shadow properties if (style.shadowColor || style.shadowOffset || style.shadowRadius || style.shadowOpacity) { style.shadowColor || 'rgba(0,0,0,0.3)'; const offsetX = ((_c = style.shadowOffset) === null || _c === void 0 ? void 0 : _c.width) || 0; const offsetY = ((_d = style.shadowOffset) === null || _d === void 0 ? void 0 : _d.height) || 2; const radius = style.shadowRadius || 4; const opacity = style.shadowOpacity || 0.3; cssStyle.boxShadow = `${offsetX}px ${offsetY}px ${radius}px rgba(0,0,0,${opacity})`; delete cssStyle.shadowColor; delete cssStyle.shadowOffset; delete cssStyle.shadowRadius; delete cssStyle.shadowOpacity; } // Handle elevation (Android) if (style.elevation !== undefined) { cssStyle.boxShadow = `0px ${style.elevation}px ${style.elevation * 2}px rgba(0,0,0,0.2)`; delete cssStyle.elevation; } // Ensure color values are strings if (style.backgroundColor && typeof style.backgroundColor === 'object') { cssStyle.backgroundColor = String(style.backgroundColor); } if (style.color && typeof style.color === 'object') { cssStyle.color = String(style.color); } return cssStyle; }; // Style processing helper const processStyle = (style) => { if (Array.isArray(style)) { return style.reduce((acc, s) => ({ ...acc, ...convertRNStyleToCSS(s) }), {}); } return convertRNStyleToCSS(style); }; // Mock Animated for web const MockAnimated = { View: ({ style, children, ...props }) => { const processedStyle = processStyle(style); return React.createElement('div', { style: processedStyle, ...props, }, children); }, Text: ({ style, children, ...props }) => { const processedStyle = processStyle(style); return React.createElement('span', { style: processedStyle, ...props, }, children); }, Value: class MockValue { constructor(value) { this.value = value; } setValue(value) { this.value = value; } addListener() { return 'mock-listener'; } removeListener() { } interpolate(config) { // Simple mock interpolation for web return this.value; } }, timing: (animatedValue, config) => ({ start: (callback) => { if (animatedValue && animatedValue.setValue) { animatedValue.setValue(config.toValue); } callback === null || callback === void 0 ? void 0 : callback(); }, }), spring: (animatedValue, config) => ({ start: (callback) => { if (animatedValue && animatedValue.setValue) { animatedValue.setValue(config.toValue); } callback === null || callback === void 0 ? void 0 : callback(); }, }), loop: (animation) => ({ start: () => { } }), sequence: (animations) => ({ start: (callback) => callback === null || callback === void 0 ? void 0 : callback() }), decay: () => ({ start: (callback) => callback === null || callback === void 0 ? void 0 : callback() }), parallel: (animations) => ({ start: (callback) => callback === null || callback === void 0 ? void 0 : callback() }), stagger: (time, animations) => ({ start: (callback) => callback === null || callback === void 0 ? void 0 : callback(), }), delay: (time) => ({ start: (callback) => callback === null || callback === void 0 ? void 0 : callback() }), }; // Web-only components (no React Native dependencies) const WebComponents = { View: ({ style, children, onPress, ...props }) => { const processedStyle = processStyle(style); const webProps = { style: processedStyle, ...props }; if (onPress) { webProps.onClick = onPress; webProps.style = { ...processedStyle, cursor: 'pointer', userSelect: 'none', }; return React.createElement('button', webProps, children); } return React.createElement('div', webProps, children); }, Text: ({ style, children, numberOfLines, adjustsFontSizeToFit, minimumFontScale, ...props }) => { const processedStyle = processStyle(style); // Filter out React Native-specific props const webProps = { ...props }; delete webProps.numberOfLines; delete webProps.adjustsFontSizeToFit; delete webProps.minimumFontScale; return React.createElement('span', { style: processedStyle, ...webProps }, children); }, TextInput: ({ style, value, onChangeText, placeholder, multiline, numberOfLines, ...props }) => { const processedStyle = processStyle(style); const handleChange = (e) => { onChangeText === null || onChangeText === void 0 ? void 0 : onChangeText(e.target.value); }; if (multiline) { return React.createElement('textarea', { style: { ...processedStyle, resize: 'vertical' }, value, onChange: handleChange, placeholder, rows: numberOfLines || 4, ...props, }); } return React.createElement('input', { style: processedStyle, type: 'text', value, onChange: handleChange, placeholder, ...props, }); }, TouchableOpacity: ({ style, children, onPress, disabled, onPressIn, onPressOut, activeOpacity, ...props }) => { const processedStyle = processStyle(style); // Filter out React Native-specific props const webProps = { ...props }; delete webProps.onPressIn; delete webProps.onPressOut; delete webProps.activeOpacity; return React.createElement('button', { style: { ...processedStyle, cursor: disabled ? 'not-allowed' : 'pointer', opacity: disabled ? 0.6 : 1, border: 'none', background: 'transparent', padding: 0, userSelect: 'none', }, onClick: disabled ? undefined : onPress, disabled, ...webProps, }, children); }, Pressable: ({ style, onPress, children, disabled, onPressIn, onPressOut, ...props }) => { const processedStyle = processStyle(style); // Filter out React Native-specific props const webProps = { ...props }; delete webProps.onPressIn; delete webProps.onPressOut; return React.createElement('button', { style: { ...processedStyle, cursor: disabled ? 'not-allowed' : 'pointer', opacity: disabled ? 0.6 : 1, background: 'transparent', border: 'none', }, onClick: disabled ? undefined : onPress, disabled, ...webProps, }, children); }, ScrollView: ({ style, children, horizontal, ...props }) => { const processedStyle = processStyle(style); return React.createElement('div', { style: { ...processedStyle, overflow: 'auto', display: horizontal ? 'flex' : 'block', flexDirection: horizontal ? 'row' : 'column', }, ...props, }, children); }, SafeAreaView: ({ style, children, ...props }) => { const processedStyle = processStyle(style); return React.createElement('div', { style: { ...processedStyle, paddingTop: 'env(safe-area-inset-top)', paddingBottom: 'env(safe-area-inset-bottom)', paddingLeft: 'env(safe-area-inset-left)', paddingRight: 'env(safe-area-inset-right)', }, ...props, }, children); }, Image: ({ style, source, ...props }) => { const processedStyle = processStyle(style); const src = typeof source === 'object' ? source.uri : source; return React.createElement('img', { style: processedStyle, src, ...props, }); }, ActivityIndicator: ({ color = '#000', size = 'medium', style }) => { const processedStyle = processStyle(style); const sizeValue = size === 'small' ? '16px' : size === 'large' ? '32px' : '24px'; return React.createElement('div', { style: { ...processedStyle, width: sizeValue, height: sizeValue, border: `2px solid ${color}30`, borderTop: `2px solid ${color}`, borderRadius: '50%', animation: 'baap-spin 1s linear infinite', }, }); }, Modal: ({ visible, transparent, animationType, onRequestClose, children, ...props }) => { // For React Native, this will be replaced by the actual RN Modal at runtime // For web, we use our custom modal implementation if (!visible) return null; return React.createElement('div', { style: { position: 'fixed', top: 0, left: 0, right: 0, bottom: 0, backgroundColor: transparent ? 'transparent' : 'rgba(0,0,0,0.5)', display: 'flex', alignItems: 'center', justifyContent: 'center', zIndex: 1000, }, onClick: (e) => { if (e.target === e.currentTarget) { onRequestClose === null || onRequestClose === void 0 ? void 0 : onRequestClose(); } }, ...props, }, children); }, Animated: MockAnimated, }; // Web-only APIs const WebAPIs = { StyleSheet: { create: (styles) => { // For React Native, this will use the actual StyleSheet.create at runtime // For web, we just return the styles as-is return styles; }, flatten: (style) => { if (Array.isArray(style)) { return style.reduce((acc, s) => ({ ...acc, ...s }), {}); } return style || {}; }, }, Platform: { OS: (() => { // For React Native, this will be replaced with actual Platform.OS at runtime // For web, we always return 'web' return 'web'; })(), select: (options) => { const os = WebAPIs.Platform.OS; return options[os] || options.default; }, }, Dimensions: { get: (dimension) => { // For React Native, this will be replaced with actual Dimensions.get at runtime // For web, we use window dimensions if (typeof window !== 'undefined') { return { width: window.innerWidth, height: window.innerHeight, }; } return { width: 375, height: 667 }; }, }, }; // Export platform components and APIs (always web components) const { View, Text, TextInput, TouchableOpacity, Pressable, ScrollView, SafeAreaView, Image, ActivityIndicator, Modal: Modal$1, Animated, } = WebComponents; const { StyleSheet, Platform, Dimensions } = WebAPIs; // Add CSS animation for spinner if (typeof document !== 'undefined') { const style = document.createElement('style'); style.textContent = ` @keyframes baap-spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } } `; document.head.appendChild(style); } const flatLightTheme = { mode: 'light', design: 'flat', colors: { primary: '#007AFF', secondary: '#5856D6', background: '#FFFFFF', surface: '#F2F2F7', text: '#000000', textSecondary: '#8E8E93', border: '#C7C7CC', error: '#FF3B30', success: '#34C759', warning: '#FF9500', info: '#5856D6', lightShadow: '#FFFFFF', darkShadow: '#000000', }, shadows: { small: { shadowColor: '#000000', shadowOffset: { width: 0, height: 1 }, shadowOpacity: 0.18, shadowRadius: 1.0, elevation: 1, }, medium: { shadowColor: '#000000', shadowOffset: { width: 0, height: 2 }, shadowOpacity: 0.25, shadowRadius: 3.84, elevation: 3, }, large: { shadowColor: '#000000', shadowOffset: { width: 0, height: 4 }, shadowOpacity: 0.3, shadowRadius: 4.65, elevation: 5, }, }, spacing: { xs: 4, sm: 8, md: 16, lg: 24, xl: 32, }, typography: { h1: { fontSize: 34, fontWeight: 'bold', letterSpacing: 0.37, }, h2: { fontSize: 28, fontWeight: '600', letterSpacing: 0.35, }, h3: { fontSize: 22, fontWeight: '600', letterSpacing: 0.35, }, body: { fontSize: 17, fontWeight: 'normal', letterSpacing: -0.41, }, button: { fontSize: 17, fontWeight: '600', letterSpacing: -0.41, }, caption: { fontSize: 12, fontWeight: 'normal', letterSpacing: 0, }, }, shape: { borderRadius: { sm: 4, md: 8, lg: 12, full: 9999, }, }, }; const neumorphicLightTheme = { mode: 'light', design: 'neumorphic', colors: { primary: '#4A90E2', secondary: '#5856D6', background: '#E0E5EC', surface: '#E0E5EC', text: '#2D3436', textSecondary: '#636E72', border: '#D1D9E6', error: '#FF3B30', success: '#34C759', warning: '#FF9500', info: '#5856D6', lightShadow: '#FFFFFF', darkShadow: '#A3B1C6', }, shadows: { small: { shadowColor: '#000000', shadowOffset: { width: -3, height: -3 }, shadowOpacity: 0.2, shadowRadius: 3, elevation: 3, }, medium: { shadowColor: '#000000', shadowOffset: { width: -5, height: -5 }, shadowOpacity: 0.2, shadowRadius: 6, elevation: 5, }, large: { shadowColor: '#000000', shadowOffset: { width: -8, height: -8 }, shadowOpacity: 0.2, shadowRadius: 10, elevation: 8, }, }, spacing: { xs: 4, sm: 8, md: 16, lg: 24, xl: 32, }, typography: { h1: { fontSize: 34, fontWeight: 'bold', letterSpacing: 0.37, }, h2: { fontSize: 28, fontWeight: '600', letterSpacing: 0.35, }, h3: { fontSize: 22, fontWeight: '600', letterSpacing: 0.35, }, body: { fontSize: 17, fontWeight: 'normal', letterSpacing: -0.41, }, button: { fontSize: 17, fontWeight: '600', letterSpacing: -0.41, }, caption: { fontSize: 12, fontWeight: 'normal', letterSpacing: 0, }, }, shape: { borderRadius: { sm: 8, md: 12, lg: 16, full: 9999, }, }, }; const ThemeContext = createContext(undefined); const ThemeProvider = ({ children }) => { const [currentTheme, setCurrentTheme] = useState(flatLightTheme); const setMode = useCallback((mode) => { setCurrentTheme(prevTheme => { // Here we'll implement the logic to switch between light/dark variants // of the current design theme return prevTheme; }); }, []); const setDesign = useCallback((design) => { setCurrentTheme(prevTheme => { const mode = prevTheme.mode; switch (design) { case 'neumorphic': return { ...neumorphicLightTheme, mode }; case 'skeuomorphic': case 'material': case 'simplistic': // Temporarily fall back to flat theme for unimplemented designs return { ...flatLightTheme, mode }; default: return { ...flatLightTheme, mode }; } }); }, []); return (jsx(ThemeContext.Provider, { value: { theme: currentTheme, setMode, setDesign }, children: children })); }; const useTheme = () => { const context = useContext(ThemeContext); if (context === undefined) { throw new Error('useTheme must be used within a ThemeProvider'); } return context; }; const NEUMORPHIC_COLORS = { background: '#fff', darkShadow: '#cacaca', lightShadow: '#f6f6f6', text: '#2d2d2d', primary: '#4A90E2', secondary: '#6c757d', success: '#28a745', danger: '#dc3545', warning: '#ffc107', info: '#17a2b8', }; const NEUMORPHIC_CONFIG = { distance: 10, blur: 41, intensity: 1, borderRadius: 22, }; const getNeumorphicStyles = ({ isPressed = false, customBackground, customBorderRadius, } = {}) => { const baseStyles = [ { backgroundColor: customBackground || NEUMORPHIC_COLORS.background, borderWidth: 0, borderRadius: customBorderRadius || NEUMORPHIC_CONFIG.borderRadius, padding: 16, }, ]; if (isPressed) { // Inset shadow effect when pressed baseStyles.push({ boxShadow: `inset ${NEUMORPHIC_CONFIG.distance}px ${NEUMORPHIC_CONFIG.distance}px ${NEUMORPHIC_CONFIG.blur}px ${NEUMORPHIC_COLORS.darkShadow}, inset -${NEUMORPHIC_CONFIG.distance}px -${NEUMORPHIC_CONFIG.distance}px ${NEUMORPHIC_CONFIG.blur}px ${NEUMORPHIC_COLORS.lightShadow}`, }); } else { // Raised effect when not pressed baseStyles.push({ boxShadow: `${NEUMORPHIC_CONFIG.distance}px ${NEUMORPHIC_CONFIG.distance}px ${NEUMORPHIC_CONFIG.blur}px ${NEUMORPHIC_COLORS.darkShadow}, -${NEUMORPHIC_CONFIG.distance}px -${NEUMORPHIC_CONFIG.distance}px ${NEUMORPHIC_CONFIG.blur}px ${NEUMORPHIC_COLORS.lightShadow}`, }); } return baseStyles; }; const useNeumorphicShadow = (options = {}) => { const { theme } = useTheme(); const { size = 'medium', intensity = 1, inset = false, distance: customDistance, blur: customBlur, } = options; return useMemo(() => { if (theme.design !== 'neumorphic') { return theme.shadows[size]; } const distances = { small: 3, medium: 5, large: 8, }; const blurs = { small: 6, medium: 10, large: 16, }; const distance = customDistance || distances[size]; const blur = customBlur || blurs[size]; const spread = distance / 2; if (Platform.OS === 'web') { const lightShadow = theme.colors.lightShadow; const darkShadow = theme.colors.darkShadow; const lightOpacity = 1 * intensity; const darkOpacity = 0.7 * intensity; const insetStr = inset ? 'inset ' : ''; return { backgroundColor: theme.colors.background, borderRadius: theme.shape.borderRadius.md, boxShadow: ` ${insetStr}${-distance}px ${-distance}px ${blur}px ${spread}px rgba(${lightShadow}, ${lightOpacity}), ${insetStr}${distance}px ${distance}px ${blur}px ${spread}px rgba(${darkShadow}, ${darkOpacity}) `, }; } // React Native - We need to simulate the effect with a single shadow // since React Native doesn't support multiple shadows return { backgroundColor: theme.colors.background, borderRadius: theme.shape.borderRadius.md, shadowColor: inset ? theme.colors.darkShadow : theme.colors.lightShadow, shadowOffset: { width: inset ? distance : -distance, height: inset ? distance : -distance, }, shadowOpacity: intensity * (inset ? 0.7 : 1), shadowRadius: blur / 2, elevation: distance, }; }, [theme, size, intensity, inset, customDistance, customBlur]); }; const Button = ({ children, variant = 'primary', size = 'medium', design, disabled, loading, onPress, style, textStyle, backgroundColor, textColor, }) => { const { theme } = useTheme(); const [isPressed, setIsPressed] = useState(false); const activeDesign = design || theme.design; const getVariantStyles = () => { // If design is neumorphic, use neumorphic styles if (activeDesign === 'neumorphic') { return { container: { backgroundColor: backgroundColor || '#fff', borderRadius: 22, ...(isPressed ? { boxShadow: 'inset 10px 10px 41px #cacaca, inset -10px -10px 41px #f6f6f6', } : { boxShadow: '10px 10px 41px #cacaca, -10px -10px 41px #f6f6f6', }), opacity: disabled ? 0.6 : 1, }, text: { color: disabled ? '#9e9e9e' : textColor || '#2196f3', ...theme.typography.button, }, }; } // For other designs, use the standard flat styles with custom colors switch (variant) { case 'secondary': return { container: { ...styles$u.secondaryButton, backgroundColor: backgroundColor || '#f50057', }, text: { ...styles$u.secondaryText, color: textColor || 'white', }, }; case 'outline': return { container: { ...styles$u.outlineButton, borderColor: backgroundColor || '#2196f3', }, text: { ...styles$u.outlineText, color: textColor || backgroundColor || '#2196f3', }, }; case 'text': return { container: styles$u.textButton, text: { ...styles$u.textButtonText, color: textColor || '#2196f3', }, }; default: return { container: { ...styles$u.primaryButton, backgroundColor: backgroundColor || '#2196f3', }, text: { ...styles$u.primaryText, color: textColor || 'white', }, }; } }; const getSizeStyles = () => { const sizes = { small: { paddingVertical: 8, paddingHorizontal: 16, borderRadius: 12, }, medium: { paddingVertical: 12, paddingHorizontal: 24, borderRadius: 16, }, large: { paddingVertical: 16, paddingHorizontal: 32, borderRadius: 20, }, }; return sizes[size]; }; const variantStyles = getVariantStyles(); const sizeStyles = getSizeStyles(); return (jsx(TouchableOpacity, { style: [ styles$u.button, variantStyles.container, sizeStyles, disabled && styles$u.disabledButton, style, ], onPress: onPress, onPressIn: () => setIsPressed(true), onPressOut: () => setIsPressed(false), disabled: disabled || loading, activeOpacity: activeDesign === 'neumorphic' ? 1 : 0.7, children: loading ? (jsx(ActivityIndicator, { color: variant === 'primary' ? 'white' : '#666666' })) : (jsx(Text, { style: [styles$u.text, variantStyles.text, disabled && styles$u.disabledText, textStyle], children: children })) })); }; const styles$u = StyleSheet.create({ button: { alignItems: 'center', justifyContent: 'center', flexDirection: 'row', }, primaryButton: { backgroundColor: '#2196f3', }, primaryText: { color: 'white', fontWeight: '600', }, secondaryButton: { backgroundColor: '#f50057', }, secondaryText: { color: 'white', fontWeight: '600', }, outlineButton: { backgroundColor: 'transparent', borderWidth: 2, borderColor: '#2196f3', }, outlineText: { color: '#2196f3', fontWeight: '600', }, textButton: { backgroundColor: 'transparent', }, textButtonText: { color: '#2196f3', fontWeight: '600', }, disabledButton: { opacity: 0.6, }, disabledText: { color: '#999999', }, text: { fontSize: 16, fontWeight: '600', }, }); const Input = ({ style, label, error, design = 'flat', backgroundColor = NEUMORPHIC_COLORS.background, textColor = NEUMORPHIC_COLORS.text, isFocused = false, ...props }) => { const [focused, setFocused] = React.useState(isFocused); const getInputStyles = () => { const baseStyles = [styles$t.input]; if (error) { baseStyles.push(styles$t.inputError); } if (design === 'neumorphic') { const neumorphicStyles = getNeumorphicStyles({ isPressed: focused, customBackground: backgroundColor, customBorderRadius: 12, }); baseStyles.push(...neumorphicStyles); baseStyles.push({ height: 40, paddingHorizontal: 10, color: textColor, borderWidth: 0, }); } if (style) { baseStyles.push(style); } return baseStyles; }; const getLabelStyles = () => { const labelStyles = [styles$t.label]; if (design === 'neumorphic') { labelStyles.push({ color: textColor, fontSize: 14, fontWeight: '500', marginBottom: 8, textShadowColor: NEUMORPHIC_COLORS.lightShadow, textShadowOffset: { width: 1, height: 1 }, textShadowRadius: 1, }); } return labelStyles; }; return (jsxs(View, { style: styles$t.container, children: [label && jsx(Text, { style: getLabelStyles(), children: label }), jsx(TextInput, { style: getInputStyles(), placeholderTextColor: design === 'neumorphic' ? `${textColor}80` : undefined, onFocus: () => setFocused(true), onBlur: () => setFocused(false), ...props }), error && jsx(Text, { style: styles$t.error, children: error })] })); }; const styles$t = StyleSheet.create({ container: { gap: 4, }, input: { height: 40, borderColor: '#ccc', borderWidth: 1, paddingHorizontal: 10, borderRadius: 5, }, label: { fontSize: 14, fontWeight: '500', }, error: { color: 'red', fontSize: 12, }, inputError: { borderColor: 'red', }, }); const TextArea = ({ style, design = 'flat', backgroundColor = NEUMORPHIC_COLORS.background, textColor = NEUMORPHIC_COLORS.text, ...props }) => { const [isFocused, setIsFocused] = useState(false); const getTextAreaStyles = () => { const baseStyles = [styles$s.textArea]; if (design === 'neumorphic') { const neumorphicStyles = getNeumorphicStyles({ isPressed: isFocused, customBackground: backgroundColor, customBorderRadius: 8, }); baseStyles.push(...neumorphicStyles); baseStyles.push({ backgroundColor, borderWidth: 0, }); } if (style) { baseStyles.push(style); } return baseStyles; }; return (jsx(TextInput, { style: getTextAreaStyles(), multiline: true, onFocus: () => setIsFocused(true), onBlur: () => setIsFocused(false), placeholderTextColor: design === 'neumorphic' ? textColor + '80' : undefined, defaultValue: "", ...props, ...(design === 'neumorphic' && { selectionColor: textColor }) })); }; const styles$s = StyleSheet.create({ textArea: { height: 100, paddingHorizontal: 16, paddingVertical: 12, borderRadius: 8, textAlignVertical: 'top', marginVertical: 8, }, }); const Checkbox = ({ checked = false, onChange, label = 'Checkbox', style, disabled = false, design = 'flat', backgroundColor = NEUMORPHIC_COLORS.background, textColor = NEUMORPHIC_COLORS.text, }) => { const [isChecked, setIsChecked] = useState(checked); const [isPressed, setIsPressed] = useState(false); const toggleCheckbox = () => { if (disabled) return; const newChecked = !isChecked; setIsChecked(newChecked); if (onChange) { onChange(newChecked); } }; const getCheckboxStyles = () => { const baseStyles = [styles$r.checkbox]; if (design === 'neumorphic') { const neumorphicStyles = getNeumorphicStyles({ isPressed: isPressed || isChecked, customBackground: backgroundColor, customBorderRadius: 6, }); baseStyles.push(...neumorphicStyles); baseStyles.push({ width: 24, height: 24, borderWidth: 0, padding: 0, }); } if (disabled) { baseStyles.push(styles$r.disabled); } return baseStyles; }; const getCheckMarkStyles = () => { const baseStyles = [styles$r.checked]; if (design === 'neumorphic') { baseStyles.push({ width: 14, height: 14, backgroundColor: textColor, borderRadius: 3, }); } return baseStyles; }; return (jsxs(View, { style: [styles$r.container, style], children: [jsx(TouchableOpacity, { style: getCheckboxStyles(), onPress: toggleCheckbox, disabled: disabled, onPressIn: () => setIsPressed(true), onPressOut: () => setIsPressed(false), children: isChecked && jsx(View, { style: getCheckMarkStyles() }) }), jsx(Text, { style: [ styles$r.label, disabled && styles$r.disabledText, design === 'neumorphic' && { color: textColor, textShadowColor: NEUMORPHIC_COLORS.lightShadow, textShadowOffset: { width: 1, height: 1 }, textShadowRadius: 1, }, ], children: label })] })); }; const styles$r = StyleSheet.create({ container: { flexDirection: 'row', alignItems: 'center', marginVertical: 5, }, checkbox: { width: 20, height: 20, borderWidth: 1, borderColor: '#000', justifyContent: 'center', alignItems: 'center', marginRight: 10, }, checked: { width: 12, height: 12, backgroundColor: '#000', }, label: { fontSize: 16, }, disabled: { backgroundColor: '#ccc', }, disabledText: { color: '#ccc', }, }); const RadioButton = ({ initialSelected = false, onToggle, label = 'Radio Button', style, disabled = false, design = 'flat', backgroundColor = NEUMORPHIC_COLORS.background, textColor = NEUMORPHIC_COLORS.text, }) => { const [selected, setSelected] = useState(initialSelected); const [isPressed, setIsPressed] = useState(false); const toggleSelection = () => { if (disabled) return; const newSelected = !selected; setSelected(newSelected); if (onToggle) { onToggle(newSelected); } }; const getRadioStyles = () => { const baseStyles = [styles$q.radio]; if (design === 'neumorphic') { const neumorphicStyles = getNeumorphicStyles({ isPressed: isPressed || selected, customBackground: backgroundColor, customBorderRadius: 15, }); baseStyles.push(...neumorphicStyles); baseStyles.push({ width: 28, height: 28, borderWidth: 0, padding: 0, }); } if (disabled) { baseStyles.push(styles$q.disabled); } return baseStyles; }; const getSelectedStyles = () => { const baseStyles = [styles$q.selected]; if (design === 'neumorphic') { baseStyles.push({ width: 14, height: 14, backgroundColor: textColor, borderRadius: 7, }); } return baseStyles; }; return (jsxs(View, { style: [styles$q.container, style], children: [jsx(TouchableOpacity, { style: getRadioStyles(), onPress: toggleSelection, disabled: disabled, onPressIn: () => setIsPressed(true), onPressOut: () => setIsPressed(false), children: selected && jsx(View, { style: getSelectedStyles() }) }), jsx(Text, { style: [ styles$q.label, disabled && styles$q.disabledText, design === 'neumorphic' && { color: textColor, textShadowColor: NEUMORPHIC_COLORS.lightShadow, textShadowOffset: { width: 1, height: 1 }, textShadowRadius: 1, }, ], children: label })] })); }; const styles$q = StyleSheet.create({ container: { flexDirection: 'row', alignItems: 'center', marginVertical: 5, }, radio: { width: 20, height: 20, borderRadius: 10, borderWidth: 1, borderColor: '#000', justifyContent: 'center', alignItems: 'center', marginRight: 10, }, selected: { width: 12, height: 12, borderRadius: 6, backgroundColor: '#000', }, label: { fontSize: 16, }, disabled: { backgroundColor: '#ccc', }, disabledText: { color: '#ccc', }, }); const ToggleSwitch = ({ initialValue = false, onToggle, label = 'Toggle', style, disabled = false, design = 'flat', backgroundColor = NEUMORPHIC_COLORS.background, textColor = NEUMORPHIC_COLORS.text, activeColor = '#4CAF50', }) => { const [value, setValue] = useState(initialValue); const [isPressed, setIsPressed] = useState(false); const translateX = useState(new Animated.Value(value ? 28 : 0))[0]; const toggleSwitch = () => { if (disabled) return; const newValue = !value; setValue(newValue); Animated.spring(translateX, { toValue: newValue ? 28 : 0, useNativeDriver: true, bounciness: 8, }).start(); if (onToggle) { onToggle(newValue); } }; const getToggleContainerStyles = () => { const baseStyles = [styles$p.toggleContainer]; if (design === 'neumorphic') { const neumorphicStyles = getNeumorphicStyles({ isPressed, customBackground: backgroundColor, customBorderRadius: 20, }); baseStyles.push(...neumorphicStyles); baseStyles.push({ width: 56, height: 28, padding: 2, }); } if (disabled) { baseStyles.push(styles$p.disabled); } return baseStyles; }; const getKnobStyles = () => { const baseStyles = [styles$p.knob]; if (design === 'neumorphic') { const knobNeumorphicStyles = getNeumorphicStyles({ isPressed: value, customBackground: value ? activeColor : backgroundColor, customBorderRadius: 12, }); baseStyles.push(...knobNeumorphicStyles); baseStyles.push({ width: 24, height: 24, margin: 0, }); } return baseStyles; }; return (jsxs(View, { style: [styles$p.container, style], children: [jsx(Text, { style: [ styles$p.label, disabled && styles$p.disabledText, design === 'neumorphic' && { color: textColor, textShadowColor: NEUMORPHIC_COLORS.lightShadow, textShadowOffset: { width: 1, height: 1 }, textShadowRadius: 1, }, ], children: label }), jsx(TouchableOpacity, { style: getToggleContainerStyles(), onPress: toggleSwitch, disabled: disabled, onPressIn: () => setIsPressed(true), onPressOut: () => setIsPressed(false), activeOpacity: 1, children: jsx(Animated.View, { style: [ getKnobStyles(), { transform: [{ translateX }], }, ] }) })] })); }; const styles$p = StyleSheet.create({ container: { flexDirection: 'row', alignItems: 'center', marginVertical: 5, }, toggleContainer: { width: 50, height: 24, borderRadius: 12, backgroundColor: '#E0E0E0', justifyContent: 'center', }, knob: { width: 20, height: 20, borderRadius: 10, backgroundColor: '#FFFFFF', shadowColor: '#000', shadowOffset: { width: 0, height: 2, }, shadowOpacity: 0.25, shadowRadius: 3.84, elevation: 5, }, label: { marginRight: 10, fontSize: 16, }, disabled: { opacity: 0.5, }, disabledText: { color: '#ccc', }, }); const CustomSlider = ({ value, onValueChange, minimumValue = 0, maximumValue = 100, minimumTrackTintColor = NEUMORPHIC_COLORS.primary, maximumTrackTintColor = NEUMORPHIC_COLORS.lightShadow, thumbTintColor = NEUMORPHIC_COLORS.primary, style, design = 'flat', backgroundColor = NEUMORPHIC_COLORS.background, width = 200, step, ...props }) => { const getContainerStyles = () => { const baseStyles = [styles$o.container]; if (design === 'neumorphic') { const neumorphicStyles = getNeumorphicStyles({ isPressed: false, customBackground: backgroundColor, customBorderRadius: 8, }); baseStyles.push(...neumorphicStyles); baseStyles.push({ backgroundColor, padding: 12, }); } if (style) { baseStyles.push(style); } return baseStyles; }; // Cross-platform slider using HTML input range const WebSlider = () => (jsx("input", { type: "range", min: minimumValue, max: maximumValue, value: value, step: step, onChange: e => onValueChange(Number(e.target.value)), style: { width, height: 20, background: maximumTrackTintColor, outline: 'none', opacity: 0.7, transition: 'opacity 0.2s', cursor: 'pointer', borderRadius: '10px', appearance: 'none', WebkitAppearance: 'none', }, ...props })); return (jsx(View, { style: getContainerStyles(), children: jsx(WebSlider, {}) })); }; const styles$o = StyleSheet.create({ container: { marginVertical: 10, alignItems: 'center', }, }); /** * A component for displaying text with consistent styling across the app. * Implements Material Design typography scale with responsive sizing. * * @example * ```tsx * // Basic usage * <Typography variant="h1">Hello World</Typography> * * // With neumorphic styling * <Typography * variant="h1" * design="neumorphic" * backgroundColor="#E0E5EC" * > * Neumorphic Heading * </Typography> * * // With custom styling * <Typography * variant="body1" * color="#007AFF" * align="center" * > * Centered blue text * </Typography> * * // Auto-sizing text * <Typography * adjustsFontSizeToFit * numberOfLines={1} * > * This text will shrink to fit * </Typography> * ``` */ const Typography = ({ variant = 'body1', children, style, color = '#000000', align = 'left', numberOfLines, adjustsFontSizeToFit = true, minimumFontScale = 0.7, design = 'flat', backgroundColor = NEUMORPHIC_COLORS.background, ...props }) => { const variantStyles = { h1: { fontSize: 32, fontWeight: '700', lineHeight: 40 }, h2: { fontSize: 28, fontWeight: '700', lineHeight: 36 }, h3: { fontSize: 24, fontWeight: '600', lineHeight: 32 }, h4: { fontSize: 20, fontWeight: '600', lineHeight: 28 }, h5: { fontSize: 18, fontWeight: '500', lineHeight: 26 }, h6: { fontSize: 16, fontWeight: '500', lineHeight: 24 }, subtitle1: { fontSize: 16, fontWeight: '400', lineHeight: 24 }, subtitle2: { fontSize: 14, fontWeight: '400', lineHeight: 22 }, body1: { fontSize: 16, fontWeight: '400', lineHeight: 24 }, body2: { fontSize: 14, fontWeight: '400', lineHeight: 22 }, caption: { fontSize: 12, fontWeight: '400', lineHeight: 20 }, overline: { fontSize: 10, fontWeight: '400', lineHeight: 16, textTransform: 'uppercase' }, }; const getTypographyStyles = () => { const baseStyles = [ variantStyles[variant], { color, textAlign: align }, ]; if (design === 'neumorphic') { const neumorphicStyles = getNeumorphicStyles({ customBackground: backgroundColor, customBorderRadius: 8, }); baseStyles.push(...neumorphicStyles); baseStyles.push({ padding: 8 }); } if (style) { baseStyles.push(style); } return baseStyles; }; return (jsx(Text, { ...props, style: getTypographyStyles(), numberOfLines: numberOfLines, adjustsFontSizeToFit: adjustsFontSizeToFit, minimumFontScale: minimumFontScale, children: children })); }; StyleSheet.create({ base: { fontFamily: Platform.select({ ios: 'System', android: 'Roboto', default: 'System', }), }, h1: { fontSize: 32, fontWeight: '700', lineHeight: 40, letterSpacing: -0.5, marginBottom: 8, }, h2: { fontSize: 28, fontWeight: '700', lineHeight: 36, letterSpacing: -0.5, marginBottom: 8, }, h3: { fontSize: 24, fontWeight: '600', lineHeight: 32, marginBottom: 8, }, h4: { fontSize: 20, fontWeight: '600', lineHeight: 28, marginBottom: 8, }, h5: { fontSize: 18, fontWeight: '600', lineHeight: 26, marginBottom: 8, }, h6: { fontSize: 16, fontWeight: '600', lineHeight: 24, marginBottom: 8, }, subtitle1: { fontSize: 16, fontWeight: '500', lineHeight: 24, letterSpacing: 0.15, marginBottom: 4, }, subtitle2: { fontSize: 14, fontWeight: '500', lineHeight: 22, letterSpacing: 0.1, marginBottom: 4, }, body1: { fontSize: 16, fontWeight: '400', lineHeight: 24, letterSpacing: 0.15, }, body2: { fontSize: 14, fontWeight: '400', lineHeight: 22, letterSpacing: 0.15, }, caption: { fontSize: 12, fontWeight: '400', lineHeight: 18, letterSpacing: 0.4, }, overline: { fontSize: 10, fontWeight: '500', lineHeight: 16, letterSpacing: 1.5, textTransform: 'uppercase', }, }); const Dropdown = ({ options, onSelect, label = 'Select', value, placeholder = 'Choose an option', style, dropdownStyle, optionStyle, labelStyle, textStyle, design = 'flat', backgroundColor = NEUMORPHIC_COLORS.background, textColor = NEUMORPHIC_COLORS.text, }) => { const [isOpen, setIsOpen] = useState(false); const [animation] = useState(new Animated.Value(0)); const [pressedOption, setPressedOption] = useState(null); const toggleDropdown = () => { const toValue = isOpen ? 0 : 1; setIsOpen(!isOpen); Animated.timing(animation, { toValue, duration: 200, useNativeDriver: false, }).start(); }; const optionsHeight = animation.interpolate({ inputRange: [0, 1], outputRange: [0, Math.min(options.length * 48 + 16, 250)], }); const getContainerStyles = () => { const baseStyles = [styles$n.container]; if (style) { baseStyles.push(style); } return baseStyles; }; const getDropdownStyles = () => { const baseStyles = [styles$n.dropdown]; if (design === 'neumorphic') { const neumorphicStyles = getNeumorphicStyles({ isPressed: isOpen, customBackground: backgroundColor, customBorderRadius: 8, }); baseStyles.push(...neumorphicStyles); baseStyles.push({ borderWidth: 0, }); } if (dropdownStyle) { baseStyles.push(dropdownStyle); } return baseStyles; }; const getOptionsContainerStyles = () => { const baseStyles = [styles$n.optionsContainer]; if (design === 'neumorphic') { const neumorphicStyles = getNeumorphicStyles({ isPressed: true, customBackground: backgroundColor, customBorderRadius: 8, }); baseStyles.push({ shadowColor: neumorphicStyles[0].shadowColor, shadowOffset: { width: 2, height: 2 }, shadowO