react-native-quick-preview 1.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 Oliver Lindblad
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,188 @@
1
+ # react-native-quicklook
2
+
3
+ A beautiful, customizable quick preview modal component for React Native.
4
+ Think **Gorhom Bottom Sheet**, but for quick previews.
5
+
6
+ Perfect for:
7
+ - Instagram-style **long-press previews**
8
+ - πŸ›οΈ **E-commerce product quick views**
9
+ - πŸ“° **Article teasers**
10
+ - ✈️ **Travel destination peeks**
11
+ - …any content that needs a **quick peek before full navigation**
12
+
13
+ ---
14
+
15
+ ## ✨ Features
16
+
17
+ - 🎨 Smooth **fade, scale, and swipe-to-close animations**
18
+ - 🎯 **Universal content** – works with any layout (products, posts, etc.)
19
+ - πŸŽ›οΈ **Customizable** – theme, behavior, styling, backdrop
20
+ - β™Ώ **Accessibility ready** – screen reader & keyboard navigation
21
+ - πŸ“± **Cross-platform** – iOS & Android
22
+ - πŸ”§ **TypeScript support**
23
+ - ⚑ **Performance optimized** – native drivers
24
+
25
+ ---
26
+
27
+ ## πŸ“¦ Installation
28
+
29
+ ```bash
30
+ npm install react-native-quicklook
31
+ # or
32
+ yarn add react-native-quicklook
33
+ ```
34
+
35
+ ---
36
+
37
+ ## πŸš€ Quick Start
38
+
39
+ ```tsx
40
+ import React, { useState } from 'react';
41
+ import { View, Text, TouchableOpacity } from 'react-native';
42
+ import { QuickLook } from 'react-native-quicklook';
43
+
44
+ export default function App() {
45
+ const [visible, setVisible] = useState(false);
46
+
47
+ return (
48
+ <View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
49
+ <TouchableOpacity onPress={() => setVisible(true)}>
50
+ <Text>Show Quick Look</Text>
51
+ </TouchableOpacity>
52
+
53
+ <QuickLook visible={visible} onClose={() => setVisible(false)}>
54
+ <View style={{ backgroundColor: '#fff', padding: 20 }}>
55
+ <Text style={{ fontSize: 18, fontWeight: 'bold' }}>Quick Preview Content</Text>
56
+ <Text style={{ marginTop: 10 }}>Any custom content goes here.</Text>
57
+ </View>
58
+ </QuickLook>
59
+ </View>
60
+ );
61
+ }
62
+ ```
63
+
64
+ ---
65
+
66
+ ## πŸ“– Usage Examples
67
+
68
+ ### πŸ›οΈ E-commerce Product Quick View
69
+ ```tsx
70
+ <QuickLook visible={visible} onClose={onClose} onPressCard={onViewDetails}>
71
+ <View>
72
+ <Image source={{ uri: product.image }} style={{ width: '100%', height: 200 }} />
73
+ <Text>{product.name}</Text>
74
+ <Text>{product.price}</Text>
75
+ </View>
76
+ </QuickLook>
77
+ ```
78
+
79
+ ### πŸ“° Article Preview
80
+ ```tsx
81
+ <QuickLook visible={visible} onClose={onClose} theme="dark">
82
+ <View style={{ backgroundColor: '#1a1a1a' }}>
83
+ <Image source={{ uri: article.coverImage }} style={{ width: '100%', height: 200 }} />
84
+ <Text style={{ color: '#fff' }}>{article.title}</Text>
85
+ <Text style={{ color: '#ccc' }}>{article.excerpt}</Text>
86
+ </View>
87
+ </QuickLook>
88
+ ```
89
+
90
+ ### ✈️ Travel Destination Peek
91
+ ```tsx
92
+ <QuickLook visible={visible} onClose={onClose}>
93
+ <Image source={{ uri: destination.image }} style={{ width: '100%', height: 200 }} />
94
+ <Text>{destination.title}</Text>
95
+ <Text>From ${destination.price}</Text>
96
+ </QuickLook>
97
+ ```
98
+
99
+ ---
100
+
101
+ ## πŸŽ›οΈ API Reference
102
+
103
+ ### Props
104
+
105
+ | Prop | Type | Default | Description |
106
+ |------|------|---------|-------------|
107
+ | `visible` | `boolean` | **required** | Controls modal visibility |
108
+ | `onClose` | `() => void` | **required** | Called when modal closes |
109
+ | `onPressCard` | `() => void` | `undefined` | Card press handler |
110
+ | `children` | `React.ReactNode` | `undefined` | Content to render |
111
+ | `theme` | `'light' \| 'dark'` | `'light'` | Overlay theme |
112
+ | `backdropOpacity` | `number` | `0.5 / 0.8` | Overlay opacity |
113
+ | `animationDuration` | `number` | `220` | Animation duration |
114
+ | `closeOnBackdropPress` | `boolean` | `true` | Close on backdrop press |
115
+ | `closeOnBackButton` | `boolean` | `true` | Close on Android back button |
116
+ | `enableSwipeToClose` | `boolean` | `true` | Swipe down to close |
117
+ | `swipeThreshold` | `number` | `80` | Swipe distance threshold |
118
+ | `unmountOnExit` | `boolean` | `true` | Unmount when hidden |
119
+ | `avoidKeyboard` | `boolean` | `false` | Avoid keyboard overlap |
120
+ | `renderBackdrop` | `(opacity) => ReactNode` | `undefined` | Custom backdrop |
121
+ | `onBackdropPress` | `() => void` | `undefined` | Backdrop press handler |
122
+ | `testID` | `string` | `'quicklook'` | Test identifier |
123
+ | `accessibilityLabel` | `string` | `'Quick look'` | A11y label |
124
+ | `stylesOverride` | `object` | `{}` | Override default styles |
125
+
126
+ ---
127
+
128
+ ## 🎨 Customization
129
+
130
+ ### Custom Backdrop
131
+ ```tsx
132
+ <QuickLook
133
+ visible={visible}
134
+ onClose={onClose}
135
+ renderBackdrop={(opacity) => (
136
+ <Animated.View style={[StyleSheet.absoluteFill, { backgroundColor: 'rgba(255,0,0,0.5)', opacity }]} />
137
+ )}
138
+ >
139
+ {/* content */}
140
+ </QuickLook>
141
+ ```
142
+
143
+ ### Custom Styles
144
+ ```tsx
145
+ <QuickLook
146
+ visible={visible}
147
+ onClose={onClose}
148
+ stylesOverride={{
149
+ container: { borderRadius: 24, backgroundColor: '#000' },
150
+ overlay: { backgroundColor: 'rgba(0,0,0,0.9)' }
151
+ }}
152
+ >
153
+ {/* content */}
154
+ </QuickLook>
155
+ ```
156
+
157
+ ---
158
+
159
+ ## πŸ§ͺ Testing
160
+
161
+ ```tsx
162
+ import { render, fireEvent } from '@testing-library/react-native';
163
+
164
+ test('QuickLook closes on backdrop press', () => {
165
+ const onClose = jest.fn();
166
+ const { getByTestId } = render(
167
+ <QuickLook visible={true} onClose={onClose} testID="quicklook">
168
+ <Text>Test content</Text>
169
+ </QuickLook>
170
+ );
171
+
172
+ fireEvent.press(getByTestId('ql-backdrop'));
173
+ expect(onClose).toHaveBeenCalled();
174
+ });
175
+ ```
176
+
177
+ ---
178
+
179
+ ## πŸ“± Platform Support
180
+ - βœ… iOS 12+
181
+ - βœ… Android API 21+
182
+ - βœ… Expo SDK 48+
183
+
184
+ ---
185
+
186
+ ## πŸ“„ License
187
+
188
+ MIT Β© Your Name
@@ -0,0 +1,42 @@
1
+ import React, { ReactNode } from 'react';
2
+ import { Animated } from 'react-native';
3
+
4
+ type ThemeMode = 'light' | 'dark';
5
+ type CloseReason = 'programmatic' | 'backdrop' | 'backButton' | 'swipe';
6
+ interface QuickLookProps {
7
+ visible: boolean;
8
+ onClose: () => void;
9
+ /** Lifecycle hooks (great for haptics/analytics) */
10
+ onOpen?: () => void;
11
+ onClosed?: (reason: CloseReason) => void;
12
+ /** Optional: press whole card (prefer explicit CTAs as well) */
13
+ onPressCard?: () => void;
14
+ theme?: ThemeMode;
15
+ backdropOpacity?: number;
16
+ animationDuration?: number;
17
+ /** Behavior toggles */
18
+ closeOnBackdropPress?: boolean;
19
+ closeOnBackButton?: boolean;
20
+ enableSwipeToClose?: boolean;
21
+ swipeThreshold?: number;
22
+ unmountOnExit?: boolean;
23
+ avoidKeyboard?: boolean;
24
+ /** Headless content */
25
+ children?: ReactNode;
26
+ /** Backdrop customization */
27
+ renderBackdrop?: (opacity: Animated.AnimatedInterpolation<number>) => ReactNode;
28
+ onBackdropPress?: () => void;
29
+ /** Testing & a11y */
30
+ testID?: string;
31
+ accessibilityLabel?: string;
32
+ /** Outer wrapper style overrides */
33
+ stylesOverride?: {
34
+ overlay?: any;
35
+ container?: any;
36
+ centerWrap?: any;
37
+ };
38
+ }
39
+
40
+ declare const QuickLook: React.FC<QuickLookProps>;
41
+
42
+ export { type CloseReason, QuickLook, type QuickLookProps, type ThemeMode };
@@ -0,0 +1,42 @@
1
+ import React, { ReactNode } from 'react';
2
+ import { Animated } from 'react-native';
3
+
4
+ type ThemeMode = 'light' | 'dark';
5
+ type CloseReason = 'programmatic' | 'backdrop' | 'backButton' | 'swipe';
6
+ interface QuickLookProps {
7
+ visible: boolean;
8
+ onClose: () => void;
9
+ /** Lifecycle hooks (great for haptics/analytics) */
10
+ onOpen?: () => void;
11
+ onClosed?: (reason: CloseReason) => void;
12
+ /** Optional: press whole card (prefer explicit CTAs as well) */
13
+ onPressCard?: () => void;
14
+ theme?: ThemeMode;
15
+ backdropOpacity?: number;
16
+ animationDuration?: number;
17
+ /** Behavior toggles */
18
+ closeOnBackdropPress?: boolean;
19
+ closeOnBackButton?: boolean;
20
+ enableSwipeToClose?: boolean;
21
+ swipeThreshold?: number;
22
+ unmountOnExit?: boolean;
23
+ avoidKeyboard?: boolean;
24
+ /** Headless content */
25
+ children?: ReactNode;
26
+ /** Backdrop customization */
27
+ renderBackdrop?: (opacity: Animated.AnimatedInterpolation<number>) => ReactNode;
28
+ onBackdropPress?: () => void;
29
+ /** Testing & a11y */
30
+ testID?: string;
31
+ accessibilityLabel?: string;
32
+ /** Outer wrapper style overrides */
33
+ stylesOverride?: {
34
+ overlay?: any;
35
+ container?: any;
36
+ centerWrap?: any;
37
+ };
38
+ }
39
+
40
+ declare const QuickLook: React.FC<QuickLookProps>;
41
+
42
+ export { type CloseReason, QuickLook, type QuickLookProps, type ThemeMode };
package/dist/index.js ADDED
@@ -0,0 +1,2 @@
1
+ 'use strict';var react=require('react'),reactNative=require('react-native'),jsxRuntime=require('react/jsx-runtime');var K=Object.defineProperty,U=Object.defineProperties;var X=Object.getOwnPropertyDescriptors;var Q=Object.getOwnPropertySymbols;var Z=Object.prototype.hasOwnProperty,B=Object.prototype.propertyIsEnumerable;var x=(r,e,a)=>e in r?K(r,e,{enumerable:true,configurable:true,writable:true,value:a}):r[e]=a,I=(r,e)=>{for(var a in e||(e={}))Z.call(e,a)&&x(r,a,e[a]);if(Q)for(var a of Q(e))B.call(e,a)&&x(r,a,e[a]);return r},S=(r,e)=>U(r,X(e));var {width:ne,height:_}=reactNative.Dimensions.get("window"),oe="dialog",se=(r,e)=>`rgba(0,0,0,${e!=null?e:r==="dark"?.8:.5})`,ue=({visible:r,onClose:e,onOpen:a,onClosed:d,theme:g="light",backdropOpacity:w,animationDuration:f=220,closeOnBackdropPress:j=true,closeOnBackButton:z=true,enableSwipeToClose:m=true,swipeThreshold:R=80,unmountOnExit:M=true,avoidKeyboard:v=false,children:E,renderBackdrop:D,onBackdropPress:k,testID:T="quicklook",accessibilityLabel:Y="Quick look",stylesOverride:b={}})=>{let[u,L]=react.useState(false),[$,A]=react.useState(0),n=react.useRef(new reactNative.Animated.Value(0)).current,o=react.useRef(new reactNative.Animated.Value(.96)).current,s=react.useRef(new reactNative.Animated.Value(0)).current,G=react.useMemo(()=>se(g,w),[g,w]),l=react.useRef("programmatic");react.useEffect(()=>{r&&!u?(L(true),a==null||a(),requestAnimationFrame(()=>{reactNative.Animated.parallel([reactNative.Animated.timing(n,{toValue:1,duration:f,useNativeDriver:true}),reactNative.Animated.spring(o,{toValue:1,useNativeDriver:true,friction:7,tension:80})]).start();})):!r&&u&&reactNative.Animated.parallel([reactNative.Animated.timing(n,{toValue:0,duration:f,useNativeDriver:true}),reactNative.Animated.timing(o,{toValue:.96,duration:f,useNativeDriver:true})]).start(()=>{s.setValue(0),d==null||d(l.current),l.current="programmatic",M&&L(false);});},[r,u,f,n,o,s,M,a,d]),react.useEffect(()=>{if(!v)return;let y=reactNative.Keyboard.addListener("keyboardDidShow",p=>{var N,P;return A((P=(N=p.endCoordinates)==null?void 0:N.height)!=null?P:0)}),i=reactNative.Keyboard.addListener("keyboardDidHide",()=>A(0));return ()=>{y.remove(),i.remove();}},[v]);let J=react.useRef(reactNative.PanResponder.create({onMoveShouldSetPanResponder:(y,i)=>m&&Math.abs(i.dy)>Math.abs(i.dx)&&Math.abs(i.dy)>5,onPanResponderMove:(y,i)=>{if(m&&i.dy>0){s.setValue(i.dy);let p=Math.min(1,i.dy/(R*2));n.setValue(1-p*.25),o.setValue(1-p*.03);}},onPanResponderRelease:(y,i)=>{m&&(i.dy>R?(l.current="swipe",reactNative.Animated.parallel([reactNative.Animated.timing(s,{toValue:_*.35,duration:180,useNativeDriver:true}),reactNative.Animated.timing(n,{toValue:0,duration:180,useNativeDriver:true})]).start(()=>e==null?void 0:e())):reactNative.Animated.parallel([reactNative.Animated.spring(s,{toValue:0,useNativeDriver:true}),reactNative.Animated.spring(n,{toValue:1,useNativeDriver:true}),reactNative.Animated.spring(o,{toValue:1,useNativeDriver:true})]).start());}})).current;return !u&&!r?null:jsxRuntime.jsx(reactNative.Modal,{transparent:true,visible:u||r,animationType:"none",onRequestClose:()=>{z&&(l.current="backButton",e==null||e());},presentationStyle:"overFullScreen",statusBarTranslucent:reactNative.Platform.OS==="android",children:jsxRuntime.jsxs(reactNative.Animated.View,{style:[V.overlay,{backgroundColor:G,opacity:n},b.overlay],accessibilityViewIsModal:true,children:[D?D(n):jsxRuntime.jsx(reactNative.Pressable,{onPress:()=>{k==null||k(),j&&(l.current="backdrop",e==null||e());},style:reactNative.StyleSheet.absoluteFill,android_disableSound:true,accessibilityRole:"button",accessibilityLabel:"Close quick look",testID:"ql-backdrop"}),jsxRuntime.jsx(reactNative.View,{style:[V.centerWrap,{paddingBottom:Math.max(16,v?$:16)},b.centerWrap],pointerEvents:"box-none",children:jsxRuntime.jsx(reactNative.Animated.View,S(I({},m?J.panHandlers:{}),{style:[V.container,{transform:[{scale:o},{translateY:s}]},b.container],testID:T,accessibilityRole:oe,accessibilityViewIsModal:true,accessibilityLabel:Y,children:E}))})]})})},V=reactNative.StyleSheet.create({overlay:{flex:1,justifyContent:"center",alignItems:"center"},centerWrap:{flex:1,width:"100%",justifyContent:"center",alignItems:"center",paddingHorizontal:16},container:{width:Math.min(ne*.92,560),maxHeight:_*.82,borderRadius:16,overflow:"hidden",elevation:8,shadowColor:"#000",shadowOpacity:.2,shadowRadius:12,shadowOffset:{width:0,height:6},backgroundColor:"#fff"}});exports.QuickLook=ue;//# sourceMappingURL=index.js.map
2
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/QuickLook.tsx"],"names":["screenWidth","screenHeight","Dimensions","dialogA11yRole","overlayColor","theme","custom","QuickLook","visible","onClose","onOpen","onClosed","backdropOpacity","animationDuration","closeOnBackdropPress","closeOnBackButton","enableSwipeToClose","swipeThreshold","unmountOnExit","avoidKeyboard","children","renderBackdrop","onBackdropPress","testID","accessibilityLabel","stylesOverride","mounted","setMounted","useState","kb","setKb","fade","useRef","Animated","scale","translateY","baseOverlay","useMemo","closeReasonRef","useEffect","show","Keyboard","e","_a","_b","hide","panResponder","PanResponder","_evt","g","jsx","Modal","Platform","jsxs","styles","Pressable","StyleSheet","View","__spreadProps","__spreadValues"],"mappings":"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","file":"index.js","sourcesContent":["// src/QuickLook.tsx\nimport React, { useEffect, useMemo, useRef, useState } from 'react';\nimport {\n View,\n StyleSheet,\n Modal,\n Dimensions,\n Animated,\n Platform,\n Pressable,\n PanResponder,\n Keyboard,\n} from 'react-native';\nimport type { AccessibilityRole } from 'react-native';\nimport type { CloseReason, QuickLookProps, ThemeMode } from './QuickLookProperties';\n\nconst { width: screenWidth, height: screenHeight } = Dimensions.get('window');\n\n// TS-safe across RN versions where \"dialog\" may not exist in AccessibilityRole\nconst dialogA11yRole = 'dialog' as unknown as AccessibilityRole;\n\nconst overlayColor = (theme: ThemeMode, custom?: number) =>\n `rgba(0,0,0,${custom ?? (theme === 'dark' ? 0.8 : 0.5)})`;\n\nexport const QuickLook: React.FC<QuickLookProps> = ({\n visible,\n onClose,\n onOpen,\n onClosed,\n theme = 'light',\n backdropOpacity,\n animationDuration = 220,\n closeOnBackdropPress = true,\n closeOnBackButton = true,\n enableSwipeToClose = true,\n swipeThreshold = 80,\n unmountOnExit = true,\n avoidKeyboard = false,\n children,\n renderBackdrop,\n onBackdropPress,\n testID = 'quicklook',\n accessibilityLabel = 'Quick look',\n stylesOverride = {},\n}) => {\n const [mounted, setMounted] = useState(false);\n const [kb, setKb] = useState(0); // keyboard height\n\n const fade = useRef(new Animated.Value(0)).current;\n const scale = useRef(new Animated.Value(0.96)).current;\n const translateY = useRef(new Animated.Value(0)).current;\n\n const baseOverlay = useMemo(() => overlayColor(theme, backdropOpacity), [theme, backdropOpacity]);\n\n // Track why we closed so we can report it to onClosed\n const closeReasonRef = useRef<CloseReason>('programmatic');\n\n // Mount/unmount with animation\n useEffect(() => {\n if (visible && !mounted) {\n setMounted(true);\n onOpen?.(); // great place to trigger optional haptics in-app\n\n requestAnimationFrame(() => {\n Animated.parallel([\n Animated.timing(fade, { toValue: 1, duration: animationDuration, useNativeDriver: true }),\n Animated.spring(scale, { toValue: 1, useNativeDriver: true, friction: 7, tension: 80 }),\n ]).start();\n });\n } else if (!visible && mounted) {\n Animated.parallel([\n Animated.timing(fade, { toValue: 0, duration: animationDuration, useNativeDriver: true }),\n Animated.timing(scale, { toValue: 0.96, duration: animationDuration, useNativeDriver: true }),\n ]).start(() => {\n translateY.setValue(0);\n onClosed?.(closeReasonRef.current);\n closeReasonRef.current = 'programmatic';\n if (unmountOnExit) setMounted(false);\n });\n }\n }, [visible, mounted, animationDuration, fade, scale, translateY, unmountOnExit, onOpen, onClosed]);\n\n // Keyboard avoidance (optional)\n useEffect(() => {\n if (!avoidKeyboard) return;\n const show = Keyboard.addListener('keyboardDidShow', (e) => setKb(e.endCoordinates?.height ?? 0));\n const hide = Keyboard.addListener('keyboardDidHide', () => setKb(0));\n return () => {\n show.remove();\n hide.remove();\n };\n }, [avoidKeyboard]);\n\n // Swipe to close\n const panResponder = useRef(\n PanResponder.create({\n onMoveShouldSetPanResponder: (_evt, g) =>\n enableSwipeToClose && Math.abs(g.dy) > Math.abs(g.dx) && Math.abs(g.dy) > 5,\n onPanResponderMove: (_evt, g) => {\n if (!enableSwipeToClose) return;\n if (g.dy > 0) {\n translateY.setValue(g.dy);\n const p = Math.min(1, g.dy / (swipeThreshold * 2));\n fade.setValue(1 - p * 0.25);\n scale.setValue(1 - p * 0.03);\n }\n },\n onPanResponderRelease: (_evt, g) => {\n if (!enableSwipeToClose) return;\n if (g.dy > swipeThreshold) {\n closeReasonRef.current = 'swipe';\n Animated.parallel([\n Animated.timing(translateY, { toValue: screenHeight * 0.35, duration: 180, useNativeDriver: true }),\n Animated.timing(fade, { toValue: 0, duration: 180, useNativeDriver: true }),\n ]).start(() => onClose?.());\n } else {\n Animated.parallel([\n Animated.spring(translateY, { toValue: 0, useNativeDriver: true }),\n Animated.spring(fade, { toValue: 1, useNativeDriver: true }),\n Animated.spring(scale, { toValue: 1, useNativeDriver: true }),\n ]).start();\n }\n },\n })\n ).current;\n\n if (!mounted && !visible) return null;\n\n return (\n <Modal\n transparent\n visible={mounted || visible}\n animationType=\"none\"\n onRequestClose={() => {\n if (closeOnBackButton) {\n closeReasonRef.current = 'backButton';\n onClose?.();\n }\n }}\n presentationStyle=\"overFullScreen\"\n statusBarTranslucent={Platform.OS === 'android'}\n >\n {/* Backdrop */}\n <Animated.View\n style={[\n styles.overlay,\n { backgroundColor: baseOverlay, opacity: fade },\n stylesOverride.overlay,\n ]}\n accessibilityViewIsModal\n >\n {renderBackdrop ? (\n // If you provide a custom backdrop, you control its hit area/closing\n renderBackdrop(fade as unknown as Animated.AnimatedInterpolation<number>)\n ) : (\n <Pressable\n onPress={() => {\n onBackdropPress?.();\n if (closeOnBackdropPress) {\n closeReasonRef.current = 'backdrop';\n onClose?.();\n }\n }}\n style={StyleSheet.absoluteFill}\n android_disableSound\n accessibilityRole=\"button\"\n accessibilityLabel=\"Close quick look\"\n testID=\"ql-backdrop\"\n />\n )}\n\n {/* Centered card */}\n <View\n style={[\n styles.centerWrap,\n { paddingBottom: Math.max(16, avoidKeyboard ? kb : 16) },\n stylesOverride.centerWrap,\n ]}\n pointerEvents=\"box-none\"\n >\n <Animated.View\n {...(enableSwipeToClose ? panResponder.panHandlers : {})}\n style={[\n styles.container,\n { transform: [{ scale }, { translateY }] },\n stylesOverride.container,\n ]}\n testID={testID}\n accessibilityRole={dialogA11yRole}\n accessibilityViewIsModal\n accessibilityLabel={accessibilityLabel}\n >\n {children}\n </Animated.View>\n </View>\n </Animated.View>\n </Modal>\n );\n};\n\nconst styles = StyleSheet.create({\n overlay: { flex: 1, justifyContent: 'center', alignItems: 'center' },\n centerWrap: {\n flex: 1,\n width: '100%',\n justifyContent: 'center',\n alignItems: 'center',\n paddingHorizontal: 16,\n },\n container: {\n width: Math.min(screenWidth * 0.92, 560),\n maxHeight: screenHeight * 0.82,\n borderRadius: 16,\n overflow: 'hidden',\n elevation: 8,\n shadowColor: '#000',\n shadowOpacity: 0.2,\n shadowRadius: 12,\n shadowOffset: { width: 0, height: 6 },\n backgroundColor: '#fff',\n },\n});\n"]}
package/dist/index.mjs ADDED
@@ -0,0 +1,2 @@
1
+ import {useState,useRef,useMemo,useEffect}from'react';import {Dimensions,StyleSheet,Animated,Keyboard,PanResponder,Modal,Pressable,View,Platform}from'react-native';import {jsx,jsxs}from'react/jsx-runtime';var K=Object.defineProperty,U=Object.defineProperties;var X=Object.getOwnPropertyDescriptors;var Q=Object.getOwnPropertySymbols;var Z=Object.prototype.hasOwnProperty,B=Object.prototype.propertyIsEnumerable;var x=(r,e,a)=>e in r?K(r,e,{enumerable:true,configurable:true,writable:true,value:a}):r[e]=a,I=(r,e)=>{for(var a in e||(e={}))Z.call(e,a)&&x(r,a,e[a]);if(Q)for(var a of Q(e))B.call(e,a)&&x(r,a,e[a]);return r},S=(r,e)=>U(r,X(e));var {width:ne,height:_}=Dimensions.get("window"),oe="dialog",se=(r,e)=>`rgba(0,0,0,${e!=null?e:r==="dark"?.8:.5})`,ue=({visible:r,onClose:e,onOpen:a,onClosed:d,theme:g="light",backdropOpacity:w,animationDuration:f=220,closeOnBackdropPress:j=true,closeOnBackButton:z=true,enableSwipeToClose:m=true,swipeThreshold:R=80,unmountOnExit:M=true,avoidKeyboard:v=false,children:E,renderBackdrop:D,onBackdropPress:k,testID:T="quicklook",accessibilityLabel:Y="Quick look",stylesOverride:b={}})=>{let[u,L]=useState(false),[$,A]=useState(0),n=useRef(new Animated.Value(0)).current,o=useRef(new Animated.Value(.96)).current,s=useRef(new Animated.Value(0)).current,G=useMemo(()=>se(g,w),[g,w]),l=useRef("programmatic");useEffect(()=>{r&&!u?(L(true),a==null||a(),requestAnimationFrame(()=>{Animated.parallel([Animated.timing(n,{toValue:1,duration:f,useNativeDriver:true}),Animated.spring(o,{toValue:1,useNativeDriver:true,friction:7,tension:80})]).start();})):!r&&u&&Animated.parallel([Animated.timing(n,{toValue:0,duration:f,useNativeDriver:true}),Animated.timing(o,{toValue:.96,duration:f,useNativeDriver:true})]).start(()=>{s.setValue(0),d==null||d(l.current),l.current="programmatic",M&&L(false);});},[r,u,f,n,o,s,M,a,d]),useEffect(()=>{if(!v)return;let y=Keyboard.addListener("keyboardDidShow",p=>{var N,P;return A((P=(N=p.endCoordinates)==null?void 0:N.height)!=null?P:0)}),i=Keyboard.addListener("keyboardDidHide",()=>A(0));return ()=>{y.remove(),i.remove();}},[v]);let J=useRef(PanResponder.create({onMoveShouldSetPanResponder:(y,i)=>m&&Math.abs(i.dy)>Math.abs(i.dx)&&Math.abs(i.dy)>5,onPanResponderMove:(y,i)=>{if(m&&i.dy>0){s.setValue(i.dy);let p=Math.min(1,i.dy/(R*2));n.setValue(1-p*.25),o.setValue(1-p*.03);}},onPanResponderRelease:(y,i)=>{m&&(i.dy>R?(l.current="swipe",Animated.parallel([Animated.timing(s,{toValue:_*.35,duration:180,useNativeDriver:true}),Animated.timing(n,{toValue:0,duration:180,useNativeDriver:true})]).start(()=>e==null?void 0:e())):Animated.parallel([Animated.spring(s,{toValue:0,useNativeDriver:true}),Animated.spring(n,{toValue:1,useNativeDriver:true}),Animated.spring(o,{toValue:1,useNativeDriver:true})]).start());}})).current;return !u&&!r?null:jsx(Modal,{transparent:true,visible:u||r,animationType:"none",onRequestClose:()=>{z&&(l.current="backButton",e==null||e());},presentationStyle:"overFullScreen",statusBarTranslucent:Platform.OS==="android",children:jsxs(Animated.View,{style:[V.overlay,{backgroundColor:G,opacity:n},b.overlay],accessibilityViewIsModal:true,children:[D?D(n):jsx(Pressable,{onPress:()=>{k==null||k(),j&&(l.current="backdrop",e==null||e());},style:StyleSheet.absoluteFill,android_disableSound:true,accessibilityRole:"button",accessibilityLabel:"Close quick look",testID:"ql-backdrop"}),jsx(View,{style:[V.centerWrap,{paddingBottom:Math.max(16,v?$:16)},b.centerWrap],pointerEvents:"box-none",children:jsx(Animated.View,S(I({},m?J.panHandlers:{}),{style:[V.container,{transform:[{scale:o},{translateY:s}]},b.container],testID:T,accessibilityRole:oe,accessibilityViewIsModal:true,accessibilityLabel:Y,children:E}))})]})})},V=StyleSheet.create({overlay:{flex:1,justifyContent:"center",alignItems:"center"},centerWrap:{flex:1,width:"100%",justifyContent:"center",alignItems:"center",paddingHorizontal:16},container:{width:Math.min(ne*.92,560),maxHeight:_*.82,borderRadius:16,overflow:"hidden",elevation:8,shadowColor:"#000",shadowOpacity:.2,shadowRadius:12,shadowOffset:{width:0,height:6},backgroundColor:"#fff"}});export{ue as QuickLook};//# sourceMappingURL=index.mjs.map
2
+ //# sourceMappingURL=index.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/QuickLook.tsx"],"names":["screenWidth","screenHeight","Dimensions","dialogA11yRole","overlayColor","theme","custom","QuickLook","visible","onClose","onOpen","onClosed","backdropOpacity","animationDuration","closeOnBackdropPress","closeOnBackButton","enableSwipeToClose","swipeThreshold","unmountOnExit","avoidKeyboard","children","renderBackdrop","onBackdropPress","testID","accessibilityLabel","stylesOverride","mounted","setMounted","useState","kb","setKb","fade","useRef","Animated","scale","translateY","baseOverlay","useMemo","closeReasonRef","useEffect","show","Keyboard","e","_a","_b","hide","panResponder","PanResponder","_evt","g","jsx","Modal","Platform","jsxs","styles","Pressable","StyleSheet","View","__spreadProps","__spreadValues"],"mappings":"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","file":"index.mjs","sourcesContent":["// src/QuickLook.tsx\nimport React, { useEffect, useMemo, useRef, useState } from 'react';\nimport {\n View,\n StyleSheet,\n Modal,\n Dimensions,\n Animated,\n Platform,\n Pressable,\n PanResponder,\n Keyboard,\n} from 'react-native';\nimport type { AccessibilityRole } from 'react-native';\nimport type { CloseReason, QuickLookProps, ThemeMode } from './QuickLookProperties';\n\nconst { width: screenWidth, height: screenHeight } = Dimensions.get('window');\n\n// TS-safe across RN versions where \"dialog\" may not exist in AccessibilityRole\nconst dialogA11yRole = 'dialog' as unknown as AccessibilityRole;\n\nconst overlayColor = (theme: ThemeMode, custom?: number) =>\n `rgba(0,0,0,${custom ?? (theme === 'dark' ? 0.8 : 0.5)})`;\n\nexport const QuickLook: React.FC<QuickLookProps> = ({\n visible,\n onClose,\n onOpen,\n onClosed,\n theme = 'light',\n backdropOpacity,\n animationDuration = 220,\n closeOnBackdropPress = true,\n closeOnBackButton = true,\n enableSwipeToClose = true,\n swipeThreshold = 80,\n unmountOnExit = true,\n avoidKeyboard = false,\n children,\n renderBackdrop,\n onBackdropPress,\n testID = 'quicklook',\n accessibilityLabel = 'Quick look',\n stylesOverride = {},\n}) => {\n const [mounted, setMounted] = useState(false);\n const [kb, setKb] = useState(0); // keyboard height\n\n const fade = useRef(new Animated.Value(0)).current;\n const scale = useRef(new Animated.Value(0.96)).current;\n const translateY = useRef(new Animated.Value(0)).current;\n\n const baseOverlay = useMemo(() => overlayColor(theme, backdropOpacity), [theme, backdropOpacity]);\n\n // Track why we closed so we can report it to onClosed\n const closeReasonRef = useRef<CloseReason>('programmatic');\n\n // Mount/unmount with animation\n useEffect(() => {\n if (visible && !mounted) {\n setMounted(true);\n onOpen?.(); // great place to trigger optional haptics in-app\n\n requestAnimationFrame(() => {\n Animated.parallel([\n Animated.timing(fade, { toValue: 1, duration: animationDuration, useNativeDriver: true }),\n Animated.spring(scale, { toValue: 1, useNativeDriver: true, friction: 7, tension: 80 }),\n ]).start();\n });\n } else if (!visible && mounted) {\n Animated.parallel([\n Animated.timing(fade, { toValue: 0, duration: animationDuration, useNativeDriver: true }),\n Animated.timing(scale, { toValue: 0.96, duration: animationDuration, useNativeDriver: true }),\n ]).start(() => {\n translateY.setValue(0);\n onClosed?.(closeReasonRef.current);\n closeReasonRef.current = 'programmatic';\n if (unmountOnExit) setMounted(false);\n });\n }\n }, [visible, mounted, animationDuration, fade, scale, translateY, unmountOnExit, onOpen, onClosed]);\n\n // Keyboard avoidance (optional)\n useEffect(() => {\n if (!avoidKeyboard) return;\n const show = Keyboard.addListener('keyboardDidShow', (e) => setKb(e.endCoordinates?.height ?? 0));\n const hide = Keyboard.addListener('keyboardDidHide', () => setKb(0));\n return () => {\n show.remove();\n hide.remove();\n };\n }, [avoidKeyboard]);\n\n // Swipe to close\n const panResponder = useRef(\n PanResponder.create({\n onMoveShouldSetPanResponder: (_evt, g) =>\n enableSwipeToClose && Math.abs(g.dy) > Math.abs(g.dx) && Math.abs(g.dy) > 5,\n onPanResponderMove: (_evt, g) => {\n if (!enableSwipeToClose) return;\n if (g.dy > 0) {\n translateY.setValue(g.dy);\n const p = Math.min(1, g.dy / (swipeThreshold * 2));\n fade.setValue(1 - p * 0.25);\n scale.setValue(1 - p * 0.03);\n }\n },\n onPanResponderRelease: (_evt, g) => {\n if (!enableSwipeToClose) return;\n if (g.dy > swipeThreshold) {\n closeReasonRef.current = 'swipe';\n Animated.parallel([\n Animated.timing(translateY, { toValue: screenHeight * 0.35, duration: 180, useNativeDriver: true }),\n Animated.timing(fade, { toValue: 0, duration: 180, useNativeDriver: true }),\n ]).start(() => onClose?.());\n } else {\n Animated.parallel([\n Animated.spring(translateY, { toValue: 0, useNativeDriver: true }),\n Animated.spring(fade, { toValue: 1, useNativeDriver: true }),\n Animated.spring(scale, { toValue: 1, useNativeDriver: true }),\n ]).start();\n }\n },\n })\n ).current;\n\n if (!mounted && !visible) return null;\n\n return (\n <Modal\n transparent\n visible={mounted || visible}\n animationType=\"none\"\n onRequestClose={() => {\n if (closeOnBackButton) {\n closeReasonRef.current = 'backButton';\n onClose?.();\n }\n }}\n presentationStyle=\"overFullScreen\"\n statusBarTranslucent={Platform.OS === 'android'}\n >\n {/* Backdrop */}\n <Animated.View\n style={[\n styles.overlay,\n { backgroundColor: baseOverlay, opacity: fade },\n stylesOverride.overlay,\n ]}\n accessibilityViewIsModal\n >\n {renderBackdrop ? (\n // If you provide a custom backdrop, you control its hit area/closing\n renderBackdrop(fade as unknown as Animated.AnimatedInterpolation<number>)\n ) : (\n <Pressable\n onPress={() => {\n onBackdropPress?.();\n if (closeOnBackdropPress) {\n closeReasonRef.current = 'backdrop';\n onClose?.();\n }\n }}\n style={StyleSheet.absoluteFill}\n android_disableSound\n accessibilityRole=\"button\"\n accessibilityLabel=\"Close quick look\"\n testID=\"ql-backdrop\"\n />\n )}\n\n {/* Centered card */}\n <View\n style={[\n styles.centerWrap,\n { paddingBottom: Math.max(16, avoidKeyboard ? kb : 16) },\n stylesOverride.centerWrap,\n ]}\n pointerEvents=\"box-none\"\n >\n <Animated.View\n {...(enableSwipeToClose ? panResponder.panHandlers : {})}\n style={[\n styles.container,\n { transform: [{ scale }, { translateY }] },\n stylesOverride.container,\n ]}\n testID={testID}\n accessibilityRole={dialogA11yRole}\n accessibilityViewIsModal\n accessibilityLabel={accessibilityLabel}\n >\n {children}\n </Animated.View>\n </View>\n </Animated.View>\n </Modal>\n );\n};\n\nconst styles = StyleSheet.create({\n overlay: { flex: 1, justifyContent: 'center', alignItems: 'center' },\n centerWrap: {\n flex: 1,\n width: '100%',\n justifyContent: 'center',\n alignItems: 'center',\n paddingHorizontal: 16,\n },\n container: {\n width: Math.min(screenWidth * 0.92, 560),\n maxHeight: screenHeight * 0.82,\n borderRadius: 16,\n overflow: 'hidden',\n elevation: 8,\n shadowColor: '#000',\n shadowOpacity: 0.2,\n shadowRadius: 12,\n shadowOffset: { width: 0, height: 6 },\n backgroundColor: '#fff',\n },\n});\n"]}
package/package.json ADDED
@@ -0,0 +1,81 @@
1
+ {
2
+ "name": "react-native-quick-preview",
3
+ "version": "1.0.4",
4
+ "description": "A beautiful, customizable quick preview modal component for React Native",
5
+ "author": "Oliver Lindblad",
6
+ "license": "MIT",
7
+
8
+ "main": "dist/index.js",
9
+ "module": "dist/index.mjs",
10
+ "types": "dist/index.d.ts",
11
+ "react-native": "dist/index.js",
12
+
13
+ "exports": {
14
+ ".": {
15
+ "types": "./dist/index.d.ts",
16
+ "react-native": "./dist/index.js",
17
+ "import": "./dist/index.mjs",
18
+ "require": "./dist/index.js",
19
+ "default": "./dist/index.js"
20
+ },
21
+ "./package.json": "./package.json"
22
+ },
23
+
24
+ "sideEffects": false,
25
+ "files": ["dist", "src", "README.md", "LICENSE"],
26
+
27
+ "scripts": {
28
+ "build": "tsup",
29
+ "dev": "tsup --watch",
30
+ "type-check": "tsc --noEmit",
31
+ "lint": "eslint src --ext .ts,.tsx",
32
+ "clean": "rimraf dist",
33
+ "prepublishOnly": "rimraf dist && tsup"
34
+ },
35
+
36
+ "keywords": [
37
+ "react-native",
38
+ "modal",
39
+ "preview",
40
+ "quicklook",
41
+ "expo",
42
+ "typescript",
43
+ "ui",
44
+ "component"
45
+ ],
46
+
47
+ "peerDependencies": {
48
+ "react": ">=18.2.0 <21",
49
+ "react-native": ">=0.64.0 <1"
50
+ },
51
+
52
+ "devDependencies": {
53
+ "@types/react": "^19.0.0",
54
+ "@typescript-eslint/eslint-plugin": "^7.18.0",
55
+ "@typescript-eslint/parser": "^7.18.0",
56
+ "eslint": "^8.57.0",
57
+ "rimraf": "^6.0.0",
58
+ "tsup": "^8.0.0",
59
+ "typescript": "^5.0.0",
60
+
61
+ "react": "19.0.0",
62
+ "react-native": "0.79.5"
63
+ },
64
+
65
+ "overrides": {
66
+ "eslint": "^8.57.0",
67
+ "@typescript-eslint/parser": "^7.18.0",
68
+ "@typescript-eslint/eslint-plugin": "^7.18.0"
69
+ },
70
+
71
+ "repository": {
72
+ "type": "git",
73
+ "url": "https://github.com/Hashtagsmile/react-native-quicklook.git"
74
+ },
75
+ "bugs": {
76
+ "url": "https://github.com/Hashtagsmile/react-native-quicklook/issues"
77
+ },
78
+ "homepage": "https://github.com/Hashtagsmile/react-native-quicklook#readme",
79
+ "publishConfig": { "access": "public" },
80
+ "engines": { "node": ">=16" }
81
+ }
@@ -0,0 +1,222 @@
1
+ // src/QuickLook.tsx
2
+ import React, { useEffect, useMemo, useRef, useState } from 'react';
3
+ import {
4
+ View,
5
+ StyleSheet,
6
+ Modal,
7
+ Dimensions,
8
+ Animated,
9
+ Platform,
10
+ Pressable,
11
+ PanResponder,
12
+ Keyboard,
13
+ } from 'react-native';
14
+ import type { AccessibilityRole } from 'react-native';
15
+ import type { CloseReason, QuickLookProps, ThemeMode } from './QuickLookProperties';
16
+
17
+ const { width: screenWidth, height: screenHeight } = Dimensions.get('window');
18
+
19
+ // TS-safe across RN versions where "dialog" may not exist in AccessibilityRole
20
+ const dialogA11yRole = 'dialog' as unknown as AccessibilityRole;
21
+
22
+ const overlayColor = (theme: ThemeMode, custom?: number) =>
23
+ `rgba(0,0,0,${custom ?? (theme === 'dark' ? 0.8 : 0.5)})`;
24
+
25
+ export const QuickLook: React.FC<QuickLookProps> = ({
26
+ visible,
27
+ onClose,
28
+ onOpen,
29
+ onClosed,
30
+ theme = 'light',
31
+ backdropOpacity,
32
+ animationDuration = 220,
33
+ closeOnBackdropPress = true,
34
+ closeOnBackButton = true,
35
+ enableSwipeToClose = true,
36
+ swipeThreshold = 80,
37
+ unmountOnExit = true,
38
+ avoidKeyboard = false,
39
+ children,
40
+ renderBackdrop,
41
+ onBackdropPress,
42
+ testID = 'quicklook',
43
+ accessibilityLabel = 'Quick look',
44
+ stylesOverride = {},
45
+ }) => {
46
+ const [mounted, setMounted] = useState(false);
47
+ const [kb, setKb] = useState(0); // keyboard height
48
+
49
+ const fade = useRef(new Animated.Value(0)).current;
50
+ const scale = useRef(new Animated.Value(0.96)).current;
51
+ const translateY = useRef(new Animated.Value(0)).current;
52
+
53
+ const baseOverlay = useMemo(() => overlayColor(theme, backdropOpacity), [theme, backdropOpacity]);
54
+
55
+ // Track why we closed so we can report it to onClosed
56
+ const closeReasonRef = useRef<CloseReason>('programmatic');
57
+
58
+ // Mount/unmount with animation
59
+ useEffect(() => {
60
+ if (visible && !mounted) {
61
+ setMounted(true);
62
+ onOpen?.(); // great place to trigger optional haptics in-app
63
+
64
+ requestAnimationFrame(() => {
65
+ Animated.parallel([
66
+ Animated.timing(fade, { toValue: 1, duration: animationDuration, useNativeDriver: true }),
67
+ Animated.spring(scale, { toValue: 1, useNativeDriver: true, friction: 7, tension: 80 }),
68
+ ]).start();
69
+ });
70
+ } else if (!visible && mounted) {
71
+ Animated.parallel([
72
+ Animated.timing(fade, { toValue: 0, duration: animationDuration, useNativeDriver: true }),
73
+ Animated.timing(scale, { toValue: 0.96, duration: animationDuration, useNativeDriver: true }),
74
+ ]).start(() => {
75
+ translateY.setValue(0);
76
+ onClosed?.(closeReasonRef.current);
77
+ closeReasonRef.current = 'programmatic';
78
+ if (unmountOnExit) setMounted(false);
79
+ });
80
+ }
81
+ }, [visible, mounted, animationDuration, fade, scale, translateY, unmountOnExit, onOpen, onClosed]);
82
+
83
+ // Keyboard avoidance (optional)
84
+ useEffect(() => {
85
+ if (!avoidKeyboard) return;
86
+ const show = Keyboard.addListener('keyboardDidShow', (e) => setKb(e.endCoordinates?.height ?? 0));
87
+ const hide = Keyboard.addListener('keyboardDidHide', () => setKb(0));
88
+ return () => {
89
+ show.remove();
90
+ hide.remove();
91
+ };
92
+ }, [avoidKeyboard]);
93
+
94
+ // Swipe to close
95
+ const panResponder = useRef(
96
+ PanResponder.create({
97
+ onMoveShouldSetPanResponder: (_evt, g) =>
98
+ enableSwipeToClose && Math.abs(g.dy) > Math.abs(g.dx) && Math.abs(g.dy) > 5,
99
+ onPanResponderMove: (_evt, g) => {
100
+ if (!enableSwipeToClose) return;
101
+ if (g.dy > 0) {
102
+ translateY.setValue(g.dy);
103
+ const p = Math.min(1, g.dy / (swipeThreshold * 2));
104
+ fade.setValue(1 - p * 0.25);
105
+ scale.setValue(1 - p * 0.03);
106
+ }
107
+ },
108
+ onPanResponderRelease: (_evt, g) => {
109
+ if (!enableSwipeToClose) return;
110
+ if (g.dy > swipeThreshold) {
111
+ closeReasonRef.current = 'swipe';
112
+ Animated.parallel([
113
+ Animated.timing(translateY, { toValue: screenHeight * 0.35, duration: 180, useNativeDriver: true }),
114
+ Animated.timing(fade, { toValue: 0, duration: 180, useNativeDriver: true }),
115
+ ]).start(() => onClose?.());
116
+ } else {
117
+ Animated.parallel([
118
+ Animated.spring(translateY, { toValue: 0, useNativeDriver: true }),
119
+ Animated.spring(fade, { toValue: 1, useNativeDriver: true }),
120
+ Animated.spring(scale, { toValue: 1, useNativeDriver: true }),
121
+ ]).start();
122
+ }
123
+ },
124
+ })
125
+ ).current;
126
+
127
+ if (!mounted && !visible) return null;
128
+
129
+ return (
130
+ <Modal
131
+ transparent
132
+ visible={mounted || visible}
133
+ animationType="none"
134
+ onRequestClose={() => {
135
+ if (closeOnBackButton) {
136
+ closeReasonRef.current = 'backButton';
137
+ onClose?.();
138
+ }
139
+ }}
140
+ presentationStyle="overFullScreen"
141
+ statusBarTranslucent={Platform.OS === 'android'}
142
+ >
143
+ {/* Backdrop */}
144
+ <Animated.View
145
+ style={[
146
+ styles.overlay,
147
+ { backgroundColor: baseOverlay, opacity: fade },
148
+ stylesOverride.overlay,
149
+ ]}
150
+ accessibilityViewIsModal
151
+ >
152
+ {renderBackdrop ? (
153
+ // If you provide a custom backdrop, you control its hit area/closing
154
+ renderBackdrop(fade as unknown as Animated.AnimatedInterpolation<number>)
155
+ ) : (
156
+ <Pressable
157
+ onPress={() => {
158
+ onBackdropPress?.();
159
+ if (closeOnBackdropPress) {
160
+ closeReasonRef.current = 'backdrop';
161
+ onClose?.();
162
+ }
163
+ }}
164
+ style={StyleSheet.absoluteFill}
165
+ android_disableSound
166
+ accessibilityRole="button"
167
+ accessibilityLabel="Close quick look"
168
+ testID="ql-backdrop"
169
+ />
170
+ )}
171
+
172
+ {/* Centered card */}
173
+ <View
174
+ style={[
175
+ styles.centerWrap,
176
+ { paddingBottom: Math.max(16, avoidKeyboard ? kb : 16) },
177
+ stylesOverride.centerWrap,
178
+ ]}
179
+ pointerEvents="box-none"
180
+ >
181
+ <Animated.View
182
+ {...(enableSwipeToClose ? panResponder.panHandlers : {})}
183
+ style={[
184
+ styles.container,
185
+ { transform: [{ scale }, { translateY }] },
186
+ stylesOverride.container,
187
+ ]}
188
+ testID={testID}
189
+ accessibilityRole={dialogA11yRole}
190
+ accessibilityViewIsModal
191
+ accessibilityLabel={accessibilityLabel}
192
+ >
193
+ {children}
194
+ </Animated.View>
195
+ </View>
196
+ </Animated.View>
197
+ </Modal>
198
+ );
199
+ };
200
+
201
+ const styles = StyleSheet.create({
202
+ overlay: { flex: 1, justifyContent: 'center', alignItems: 'center' },
203
+ centerWrap: {
204
+ flex: 1,
205
+ width: '100%',
206
+ justifyContent: 'center',
207
+ alignItems: 'center',
208
+ paddingHorizontal: 16,
209
+ },
210
+ container: {
211
+ width: Math.min(screenWidth * 0.92, 560),
212
+ maxHeight: screenHeight * 0.82,
213
+ borderRadius: 16,
214
+ overflow: 'hidden',
215
+ elevation: 8,
216
+ shadowColor: '#000',
217
+ shadowOpacity: 0.2,
218
+ shadowRadius: 12,
219
+ shadowOffset: { width: 0, height: 6 },
220
+ backgroundColor: '#fff',
221
+ },
222
+ });
@@ -0,0 +1,48 @@
1
+ import type { ReactNode } from 'react';
2
+ import type { Animated } from 'react-native';
3
+
4
+ export type ThemeMode = 'light' | 'dark';
5
+ export type CloseReason = 'programmatic' | 'backdrop' | 'backButton' | 'swipe';
6
+
7
+
8
+ export interface QuickLookProps {
9
+ visible: boolean;
10
+ onClose: () => void;
11
+
12
+ /** Lifecycle hooks (great for haptics/analytics) */
13
+ onOpen?: () => void; // fires when animating in
14
+ onClosed?: (reason: CloseReason) => void; // fires after fully closed
15
+
16
+ /** Optional: press whole card (prefer explicit CTAs as well) */
17
+ onPressCard?: () => void;
18
+
19
+ theme?: ThemeMode;
20
+ backdropOpacity?: number; // default 0.5 light / 0.8 dark
21
+ animationDuration?: number; // default 220
22
+
23
+ /** Behavior toggles */
24
+ closeOnBackdropPress?: boolean; // default true
25
+ closeOnBackButton?: boolean; // default true
26
+ enableSwipeToClose?: boolean; // default true
27
+ swipeThreshold?: number; // default 80
28
+ unmountOnExit?: boolean; // default true
29
+ avoidKeyboard?: boolean; // default false
30
+
31
+ /** Headless content */
32
+ children?: ReactNode;
33
+
34
+ /** Backdrop customization */
35
+ renderBackdrop?: (opacity: Animated.AnimatedInterpolation<number>) => ReactNode;
36
+ onBackdropPress?: () => void;
37
+
38
+ /** Testing & a11y */
39
+ testID?: string;
40
+ accessibilityLabel?: string;
41
+
42
+ /** Outer wrapper style overrides */
43
+ stylesOverride?: {
44
+ overlay?: any;
45
+ container?: any;
46
+ centerWrap?: any;
47
+ };
48
+ }
package/src/index.ts ADDED
@@ -0,0 +1,2 @@
1
+ export { QuickLook } from './QuickLook';
2
+ export type { QuickLookProps, ThemeMode, CloseReason } from './QuickLookProperties';