/** * DT Drawer Component * * Hybrid component: RNP Portal for layering + custom Animated panel * for the sliding behavior. RNP has no sliding panel component. * * Uses the dual-SVG-path technique (outer border fill + inner dark fill) * consistent with DTCard/DTMediaFrame for proper visual clipping at * beveled corners. * * Web CSS reference (Aside.tsx): * - Fixed sidebar sliding from right * - Beveled left edges (top-left + bottom-left for right drawer) * - Colored header bar * - Overlay backdrop * - transition: transform 200ms ease-in-out */ import {useRef, useEffect} from 'react'; import { StyleSheet, View, ViewStyle, StyleProp, Pressable, Animated, BackHandler, ScrollView, useWindowDimensions, } from 'react-native'; import {Portal, Text} from 'react-native-paper'; import Svg, {Path} from 'react-native-svg'; import {useSafeAreaInsets} from 'react-native-safe-area-context'; import {useDTTheme} from '../theme/DTThemeProvider'; import {type DTVariant, getVariantColor} from '../utils/variantColors'; import {buildDrawerBevelPath} from '../utils/bevelPaths'; interface DTDrawerProps { /** * Whether the drawer is visible */ visible: boolean; /** * Called when the drawer is dismissed */ onDismiss: () => void; /** * Heading text in the drawer header bar */ heading: string; /** * Visual variant for the header bar * @default 'emphasis' */ headingVariant?: DTVariant; /** * Which side the drawer slides from * @default 'right' */ position?: 'right' | 'left'; /** * Drawer panel width in pixels * @default 400 */ width?: number; /** * Bevel size in pixels * @default 32 */ bevelSize?: number; /** * Border width in pixels * @default 2 */ borderWidth?: number; /** * Drawer content */ children: React.ReactNode; /** * Additional styles */ style?: StyleProp; } /** * DT-styled Drawer/Aside with beveled edges and slide animation * * @example * setCartOpen(false)} heading="Cart"> * * * * @example * * * */ export function DTDrawer({ visible, onDismiss, heading, headingVariant = 'emphasis', position = 'right', width: drawerWidth = 400, bevelSize = 32, borderWidth = 2, children, style, }: DTDrawerProps) { const theme = useDTTheme(); const useBevels = theme.custom.bevelMd > 0; const slideAnim = useRef(new Animated.Value(0)).current; const overlayAnim = useRef(new Animated.Value(0)).current; const headerColor = getVariantColor(theme, headingVariant); const {width: screenWidth, height: screenHeight} = useWindowDimensions(); const insets = useSafeAreaInsets(); const effectiveWidth = Math.min(drawerWidth, screenWidth); useEffect(() => { if (visible) { Animated.parallel([ Animated.timing(slideAnim, { toValue: 1, duration: 200, useNativeDriver: true, }), Animated.timing(overlayAnim, { toValue: 1, duration: 200, useNativeDriver: true, }), ]).start(); } else { Animated.parallel([ Animated.timing(slideAnim, { toValue: 0, duration: 200, useNativeDriver: true, }), Animated.timing(overlayAnim, { toValue: 0, duration: 200, useNativeDriver: true, }), ]).start(); } }, [visible, slideAnim, overlayAnim]); // Android back button handling useEffect(() => { if (!visible) return; const handler = BackHandler.addEventListener('hardwareBackPress', () => { onDismiss(); return true; }); return () => handler.remove(); }, [visible, onDismiss]); const translateX = slideAnim.interpolate({ inputRange: [0, 1], outputRange: [ position === 'right' ? effectiveWidth : -effectiveWidth, 0, ], }); // Don't render when fully hidden const pointerEvents = visible ? 'auto' : 'none'; return ( {/* Overlay backdrop */} {/* Drawer panel */} {/* Dual-SVG-path technique: outer border + inner dark fill — beveled mode only */} {useBevels && ( )} {/* Header bar */} {heading} ({opacity: pressed ? 0.7 : 1})}> {/* Content */} {children} ); } const styles = StyleSheet.create({ panel: { position: 'absolute', top: 0, bottom: 0, }, panelRight: { right: 0, }, panelLeft: { left: 0, }, header: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', paddingHorizontal: 16, paddingVertical: 16, zIndex: 1, }, headerText: { fontWeight: '700', fontSize: 18, letterSpacing: 0.5, }, closeButton: { fontSize: 20, fontWeight: '700', paddingHorizontal: 8, }, content: { flex: 1, zIndex: 1, }, contentContainer: { padding: 16, }, });