UNPKG

@oxyhq/services

Version:

OxyHQ Expo/React Native SDK — UI components, screens, and native features

434 lines (428 loc) 14.2 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.getHeaderHeight = exports.default = void 0; var _reactNative = require("react-native"); var _react = require("react"); var _reactNativeReanimated = _interopRequireWildcard(require("react-native-reanimated")); var _reactNativeSafeAreaContext = require("react-native-safe-area-context"); var _vectorIcons = require("@expo/vector-icons"); var _OxyIcon = _interopRequireDefault(require("./icon/OxyIcon.js")); var _fonts = require("../styles/fonts.js"); var _useColorScheme = require("../hooks/useColorScheme.js"); var _themeUtils = require("../utils/themeUtils.js"); var _theme = require("../constants/theme.js"); var _jsxRuntime = require("react/jsx-runtime"); function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; } function _interopRequireWildcard(e, t) { if ("function" == typeof WeakMap) var r = new WeakMap(), n = new WeakMap(); return (_interopRequireWildcard = function (e, t) { if (!t && e && e.__esModule) return e; var o, i, f = { __proto__: null, default: e }; if (null === e || "object" != typeof e && "function" != typeof e) return f; if (o = t ? n : r) { if (o.has(e)) return o.get(e); o.set(e, f); } for (const t in e) "default" !== t && {}.hasOwnProperty.call(e, t) && ((i = (o = Object.defineProperty) && Object.getOwnPropertyDescriptor(e, t)) && (i.get || i.set) ? o(f, t, i) : f[t] = e[t]); return f; })(e, t); } // Calculate header height based on platform and variant const getHeaderHeight = (variant = 'default', safeAreaTop = 0) => { const paddingTop = _reactNative.Platform.OS === 'ios' ? Math.max(safeAreaTop, 50) : 16; const paddingBottom = 12; const contentHeight = variant === 'minimal' ? 36 : 40; return paddingTop + contentHeight + paddingBottom; }; exports.getHeaderHeight = getHeaderHeight; const Header = ({ title, subtitle, onBack, onClose, rightAction, rightActions, theme, showBackButton = true, showCloseButton = false, showThemeToggle = false, onThemeToggle, variant = 'default', elevation = 'subtle', subtitleVariant = 'default', titleAlignment = 'left', scrollY }) => { // Use theme colors directly from Colors constant (like Accounts sidebar) // Ensure colorScheme is always 'light' or 'dark' with proper fallback chain const colorScheme = (0, _themeUtils.normalizeColorScheme)((0, _useColorScheme.useColorScheme)(), theme); const colors = _theme.Colors[colorScheme]; const insets = (0, _reactNativeSafeAreaContext.useSafeAreaInsets)(); const headerHeight = getHeaderHeight(variant, insets.top); // Animated style for sticky behavior on native // Only create animated style if scrollY is provided and we're on native platform const animatedHeaderStyle = (0, _reactNativeReanimated.useAnimatedStyle)(() => { if (_reactNative.Platform.OS === 'web' || !scrollY) { return {}; } // Sticky behavior: header scrolls with content initially, then sticks at top // When scrollY = 0, translateY = 0 (header at normal position) // When scrollY > 0, translateY becomes negative to keep header at top // Clamp to prevent header from going above viewport const translateY = (0, _reactNativeReanimated.interpolate)(scrollY.value, [0, headerHeight], [0, -headerHeight], _reactNativeReanimated.Extrapolation.CLAMP); return { transform: [{ translateY }] }; }, [scrollY, headerHeight]); const handleBackPress = () => { if (!onBack) return; // Navigate immediately and synchronously - this prioritizes navigation // over keyboard dismiss. The keyboard will close naturally after screen changes. onBack(); }; const renderBackButton = () => { if (!showBackButton || !onBack) return null; return /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.TouchableOpacity, { style: [styles.backButton, { backgroundColor: colors.card }], onPress: handleBackPress, activeOpacity: 0.7, children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_OxyIcon.default, { name: "chevron-back", size: 18, color: colors.tint }) }); }; const renderCloseButton = () => { if (!showCloseButton || !onClose) return null; return /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.TouchableOpacity, { style: [styles.closeButton, { backgroundColor: colors.card }], onPress: onClose, activeOpacity: 0.7, children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_vectorIcons.Ionicons, { name: "close", size: 18, color: colors.text }) }); }; const renderRightActionButton = (action, idx) => { const isTextAction = action.text; return /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.TouchableOpacity, { style: [styles.rightActionButton, isTextAction ? styles.textActionButton : styles.iconActionButton, { backgroundColor: isTextAction ? colors.tint : colors.card, opacity: action.disabled ? 0.5 : 1 }], onPress: action.onPress, disabled: action.disabled || action.loading, activeOpacity: 0.7, children: action.loading ? /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, { style: styles.loadingContainer, children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, { style: [styles.loadingDot, { backgroundColor: isTextAction ? '#FFFFFF' : colors.tint }] }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, { style: [styles.loadingDot, { backgroundColor: isTextAction ? '#FFFFFF' : colors.tint }] }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, { style: [styles.loadingDot, { backgroundColor: isTextAction ? '#FFFFFF' : colors.tint }] })] }) : isTextAction ? /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, { style: [styles.actionText, { color: '#FFFFFF' }], children: action.text }) : /*#__PURE__*/(0, _jsxRuntime.jsx)(_vectorIcons.Ionicons, { name: action.icon, size: 18, color: colors.tint }) }, action.key || idx); }; const renderRightActions = () => { const actions = []; // Add existing right actions if (rightActions?.length) { actions.push(...rightActions); } else if (rightAction) { actions.push(rightAction); } // Add theme toggle button if enabled if (showThemeToggle && onThemeToggle) { actions.push({ icon: colorScheme === 'dark' ? 'sunny' : 'moon', onPress: onThemeToggle, key: 'theme-toggle' }); } if (actions.length === 0) return null; if (actions.length > 1) { return /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, { style: styles.rightActionsRow, children: actions.map((a, i) => renderRightActionButton(a, i)) }); } return renderRightActionButton(actions[0], 0); }; const renderTitle = () => { const titleStyle = variant === 'large' ? styles.titleLarge : variant === 'minimal' ? styles.titleMinimal : styles.titleDefault; const subtitleStyle = variant === 'large' ? styles.subtitleLarge : variant === 'minimal' ? styles.subtitleMinimal : subtitleVariant === 'small' ? styles.subtitleSmall : subtitleVariant === 'large' ? styles.subtitleLarge : subtitleVariant === 'muted' ? styles.subtitleMuted : styles.subtitleDefault; const getTitleAlignment = () => { switch (titleAlignment) { case 'center': return styles.titleContainerCenter; case 'right': return styles.titleContainerRight; default: return styles.titleContainerLeft; } }; return /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, { style: [styles.titleContainer, getTitleAlignment(), variant === 'minimal' && styles.titleContainerMinimal], children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, { style: [titleStyle, { color: colors.text }], children: title }), subtitle && /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, { style: [subtitleStyle, { color: colors.secondaryText }], children: subtitle })] }); }; const getElevationStyle = () => { const isDark = colorScheme === 'dark'; switch (elevation) { case 'none': return {}; case 'subtle': return _reactNative.Platform.select({ web: { boxShadow: isDark ? '0 1px 3px rgba(0,0,0,0.3)' : '0 1px 3px rgba(0,0,0,0.1)' }, default: { shadowColor: '#000000', shadowOffset: { width: 0, height: 1 }, shadowOpacity: isDark ? 0.3 : 0.1, shadowRadius: 3, elevation: 2 } }); case 'prominent': return _reactNative.Platform.select({ web: { boxShadow: isDark ? '0 4px 12px rgba(0,0,0,0.4)' : '0 4px 12px rgba(0,0,0,0.15)' }, default: { shadowColor: '#000000', shadowOffset: { width: 0, height: 4 }, shadowOpacity: isDark ? 0.4 : 0.15, shadowRadius: 12, elevation: 8 } }); default: return {}; } }; const getBackgroundStyle = () => { if (variant === 'gradient') { return { backgroundColor: colors.background, // Add gradient overlay effect borderBottomWidth: 1, borderBottomColor: colors.border }; } return { backgroundColor: colors.background, borderBottomWidth: elevation === 'none' ? 0 : 1, borderBottomColor: colors.border }; }; const backgroundStyle = getBackgroundStyle(); const elevationStyle = getElevationStyle(); const containerStyle = (0, _react.useMemo)(() => [styles.container, { paddingTop: _reactNative.Platform.OS === 'ios' ? Math.max(insets.top, 50) : 16 }, // When header is inside ScrollView (has scrollY), don't use absolute positioning !scrollY && _reactNative.Platform.OS !== 'web' ? { position: 'absolute', top: 0, left: 0, right: 0 } : {}, backgroundStyle, elevationStyle], [insets.top, backgroundStyle, elevationStyle, scrollY]); const HeaderContainer = _reactNative.Platform.OS === 'web' || !scrollY ? _reactNative.View : _reactNativeReanimated.default.View; // Only apply animated styles when HeaderContainer is an animated component const shouldUseAnimatedStyle = _reactNative.Platform.OS !== 'web' && scrollY !== undefined; const headerStyle = shouldUseAnimatedStyle ? [containerStyle, animatedHeaderStyle] : containerStyle; return /*#__PURE__*/(0, _jsxRuntime.jsx)(HeaderContainer, { style: headerStyle, children: /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, { style: [styles.content, variant === 'minimal' && styles.contentMinimal], children: [renderBackButton(), renderTitle(), renderRightActions(), renderCloseButton()] }) }); }; const styles = _reactNative.StyleSheet.create({ container: { paddingBottom: 12, zIndex: 1000, ..._reactNative.Platform.select({ web: { position: 'sticky', top: 0, left: 0, right: 0 }, default: { // Position will be set dynamically based on scrollY prop } }) }, content: { flexDirection: 'row', alignItems: 'center', paddingHorizontal: 16, position: 'relative', minHeight: 40 }, contentMinimal: { paddingHorizontal: 12, minHeight: 36 }, backButton: { width: 32, height: 32, borderRadius: 16, alignItems: 'center', justifyContent: 'center', marginRight: 10 }, closeButton: { width: 32, height: 32, borderRadius: 16, alignItems: 'center', justifyContent: 'center', marginLeft: 10 }, titleContainer: { flex: 1, alignItems: 'flex-start', justifyContent: 'center' }, titleContainerLeft: { alignItems: 'flex-start' }, titleContainerCenter: { alignItems: 'center' }, titleContainerRight: { alignItems: 'flex-end' }, titleContainerMinimal: { alignItems: 'center', marginHorizontal: 16 }, titleDefault: { fontSize: 18, fontWeight: '700', fontFamily: _fonts.fontFamilies.interBold, letterSpacing: -0.5, lineHeight: 22 }, titleLarge: { fontSize: 28, fontWeight: '800', fontFamily: _fonts.fontFamilies.interExtraBold, letterSpacing: -1, lineHeight: 34, marginBottom: 3 }, titleMinimal: { fontSize: 16, fontWeight: '600', fontFamily: _fonts.fontFamilies.interSemiBold, letterSpacing: -0.3, lineHeight: 20 }, subtitleDefault: { fontSize: 14, fontWeight: '400', lineHeight: 17, marginTop: 1 }, subtitleLarge: { fontSize: 16, fontWeight: '400', lineHeight: 19, marginTop: 3 }, subtitleMinimal: { fontSize: 13, fontWeight: '400', lineHeight: 15, marginTop: 1 }, subtitleSmall: { fontSize: 12, fontWeight: '400', lineHeight: 14, marginTop: 0 }, subtitleMuted: { fontSize: 14, fontWeight: '400', lineHeight: 17, marginTop: 1, opacity: 0.7 }, rightActionButton: { alignItems: 'center', justifyContent: 'center', marginLeft: 10 }, iconActionButton: { width: 32, height: 32, borderRadius: 16 }, textActionButton: { paddingHorizontal: 14, paddingVertical: 6, borderRadius: 18, minWidth: 56 }, actionText: { fontSize: 14, fontWeight: '600', fontFamily: _fonts.fontFamilies.interSemiBold, letterSpacing: -0.2 }, loadingContainer: { flexDirection: 'row', alignItems: 'center', justifyContent: 'center', gap: 2 }, loadingDot: { width: 4, height: 4, borderRadius: 2, opacity: 0.6 }, rightActionsRow: { flexDirection: 'row', alignItems: 'center' } }); var _default = exports.default = Header; //# sourceMappingURL=Header.js.map