@oxyhq/services
Version:
434 lines (428 loc) • 14.2 kB
JavaScript
"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