/** * DT Button Component * * Interactive bevel button matching the storefront .menu-item-clipped pattern: * - Default: outlined rectangle, no bevel, colored border with thick top * - Pressed/hover: bottom-right bevel appears, fills with mode color, text goes dark * - Selected: bevel persists, fills with 70% opacity mode color */ import {useRef, useEffect} from 'react'; import {StyleSheet, View, ViewStyle, StyleProp, Pressable, Animated} from 'react-native'; import {Text} from 'react-native-paper'; import Svg, {Path, Rect} from 'react-native-svg'; import {useDTTheme} from '../theme/DTThemeProvider'; import {type DTVariant, getVariantColor} from '../utils/variantColors'; import {buildBeveledRectPath} from '../utils/bevelPaths'; import {useComponentLayout} from '../utils/useComponentLayout'; interface DTButtonProps { /** * Button content/label */ children: React.ReactNode; /** * Visual variant of the button * @default 'normal' */ variant?: DTVariant; /** * Button display mode * - 'outlined': Border with transparent background, bevel on press (default) * - 'contained': Filled background with static bevel * @default 'outlined' */ mode?: 'outlined' | 'contained'; /** * Whether the button is in a persistent selected state * (bevel visible, filled with 70% opacity mode color) */ selected?: boolean; /** * Custom color (overrides variant) */ color?: string; /** * Press handler */ onPress?: () => void; /** * Whether button is disabled */ disabled?: boolean; /** * Additional styles for container */ style?: StyleProp; /** * Border width * @default 2 */ borderWidth?: number; /** * Bevel size in pixels (bottom-right corner) * @default 16 */ bevelSize?: number; } export function DTButton({ children, variant = 'normal', mode = 'outlined', selected = false, color, onPress, disabled = false, style, borderWidth = 2, bevelSize = 16, }: DTButtonProps) { const theme = useDTTheme(); const {dimensions, onLayout, hasDimensions} = useComponentLayout(); const pulseAnim = useRef(new Animated.Value(1)).current; const pressed = useRef(false); const accentColor = getVariantColor(theme, variant, color); const selectedColor = accentColor + 'B3'; // ~70% opacity const isContained = mode === 'contained'; const useBevels = theme.custom.bevelMd > 0; const {width, height} = dimensions; // Build SVG paths: rectangle (default) and beveled (active) const rectPath = hasDimensions ? buildBeveledRectPath(width, height, {corners: {}, strokeWidth: borderWidth}) : ''; const bevelPath = useBevels && hasDimensions ? buildBeveledRectPath(width, height, { corners: {bottomRight: bevelSize}, strokeWidth: borderWidth, }) : ''; // Pulse animation for pressed/selected hover useEffect(() => { if (selected) { // No pulse when just selected (matches storefront: animation: none on .selected) pulseAnim.setValue(1); } return () => { pulseAnim.stopAnimation(); }; }, [selected, pulseAnim]); const startPulse = () => { Animated.loop( Animated.sequence([ Animated.timing(pulseAnim, { toValue: 0.5, duration: 1000, useNativeDriver: true, }), Animated.timing(pulseAnim, { toValue: 1, duration: 1000, useNativeDriver: true, }), ]), ).start(); }; const stopPulse = () => { pulseAnim.stopAnimation(); pulseAnim.setValue(1); }; const handlePressIn = () => { pressed.current = true; startPulse(); }; const handlePressOut = () => { pressed.current = false; if (!selected) { stopPulse(); } }; // Determine visual state const showBevel = isContained || selected; const getBgColor = (isPressed: boolean) => { if (isContained) return accentColor; if (selected) return selectedColor; if (isPressed) return accentColor; return 'transparent'; }; const getTextColor = (isPressed: boolean) => { if (isContained || selected || isPressed) return theme.colors.onPrimary; return accentColor; }; return ( {({pressed: isPressedState}) => ( {/* Non-beveled mode (classic brand) */} {!useBevels && ( )} {/* Beveled mode (DT brand) */} {useBevels && hasDimensions && ( {/* Thick top accent — rendered inside SVG for exact alignment */} )} {typeof children === 'string' ? ( {children} ) : ( children )} )} ); } const styles = StyleSheet.create({ container: { position: 'relative', minHeight: 44, }, content: { position: 'relative', zIndex: 1, paddingVertical: 12, paddingHorizontal: 24, alignItems: 'center', justifyContent: 'center', }, label: { fontWeight: '600', letterSpacing: 1, }, });