@gv-tech/ui-native 2.15.2 → 2.17.0
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/README.md +1 -1
- package/dist/index.d.ts +101 -3
- package/dist/ui-native.mjs +1053 -693
- package/package.json +1 -1
- package/src/index.ts +17 -1
- package/src/lib/utils.ts +10 -0
- package/src/popover.tsx +82 -6
- package/src/progress.tsx +22 -8
- package/src/scroll-to-top.tsx +186 -0
- package/src/support-fab.tsx +144 -0
- package/src/table-of-contents.tsx +240 -0
- package/src/tabs.tsx +6 -3
- package/src/toast.tsx +5 -5
- package/src/tooltip.tsx +16 -22
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -112,7 +112,7 @@ export { NavigationMenu } from './navigation-menu';
|
|
|
112
112
|
export { Pagination } from './pagination';
|
|
113
113
|
|
|
114
114
|
// Popover
|
|
115
|
-
export { Popover } from './popover';
|
|
115
|
+
export { Popover, PopoverAnchor, PopoverContent, PopoverTrigger } from './popover';
|
|
116
116
|
|
|
117
117
|
// Progress
|
|
118
118
|
export { Progress } from './progress';
|
|
@@ -144,6 +144,14 @@ export {
|
|
|
144
144
|
SelectValue,
|
|
145
145
|
} from './select';
|
|
146
146
|
|
|
147
|
+
// Scroll To Top
|
|
148
|
+
export { ScrollToTop } from './scroll-to-top';
|
|
149
|
+
export type { ScrollToTopHandle, ScrollToTopProps } from './scroll-to-top';
|
|
150
|
+
|
|
151
|
+
// Support FAB
|
|
152
|
+
export { SupportFab } from './support-fab';
|
|
153
|
+
export type { SupportFabProps } from './support-fab';
|
|
154
|
+
|
|
147
155
|
// Separator
|
|
148
156
|
export { Separator } from './separator';
|
|
149
157
|
|
|
@@ -207,3 +215,11 @@ export { ThemeToggle } from './theme-toggle';
|
|
|
207
215
|
|
|
208
216
|
// Toaster
|
|
209
217
|
export { Toaster } from './toaster';
|
|
218
|
+
|
|
219
|
+
// Table Of Contents
|
|
220
|
+
export {
|
|
221
|
+
TableOfContents,
|
|
222
|
+
TableOfContentsContent,
|
|
223
|
+
TableOfContentsHeading,
|
|
224
|
+
TableOfContentsList,
|
|
225
|
+
} from './table-of-contents';
|
package/src/lib/utils.ts
CHANGED
|
@@ -4,3 +4,13 @@ import { twMerge } from 'tailwind-merge';
|
|
|
4
4
|
export function cn(...inputs: ClassValue[]) {
|
|
5
5
|
return twMerge(clsx(inputs));
|
|
6
6
|
}
|
|
7
|
+
|
|
8
|
+
export function slugify(text: string): string {
|
|
9
|
+
return text
|
|
10
|
+
.toString()
|
|
11
|
+
.toLowerCase()
|
|
12
|
+
.trim()
|
|
13
|
+
.replace(/\s+/g, '-') // Replace spaces with -
|
|
14
|
+
.replace(/[^\w-]+/g, '') // Remove all non-word chars
|
|
15
|
+
.replace(/--+/g, '-'); // Replace multiple - with single -
|
|
16
|
+
}
|
package/src/popover.tsx
CHANGED
|
@@ -1,9 +1,85 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
PopoverAnchorBaseProps,
|
|
3
|
+
PopoverBaseProps,
|
|
4
|
+
PopoverContentBaseProps,
|
|
5
|
+
PopoverTriggerBaseProps,
|
|
6
|
+
} from '@gv-tech/ui-core';
|
|
7
|
+
import * as React from 'react';
|
|
8
|
+
import { Modal, Pressable, View } from 'react-native';
|
|
9
|
+
|
|
10
|
+
import { cn } from './lib/utils';
|
|
11
|
+
|
|
12
|
+
const PopoverContext = React.createContext<{
|
|
13
|
+
open: boolean;
|
|
14
|
+
setOpen: (open: boolean) => void;
|
|
15
|
+
}>({
|
|
16
|
+
open: false,
|
|
17
|
+
setOpen: () => {},
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
const Popover = React.forwardRef<
|
|
21
|
+
React.ElementRef<typeof View>,
|
|
22
|
+
React.ComponentPropsWithoutRef<typeof View> & PopoverBaseProps
|
|
23
|
+
>(({ children, open, onOpenChange, ...props }, ref) => {
|
|
24
|
+
const [internalOpen, setInternalOpen] = React.useState(false);
|
|
25
|
+
const isControlled = open !== undefined;
|
|
26
|
+
const isOpen = isControlled ? open : internalOpen;
|
|
27
|
+
const setOpen = isControlled ? onOpenChange || (() => {}) : setInternalOpen;
|
|
28
|
+
|
|
29
|
+
return (
|
|
30
|
+
<PopoverContext.Provider value={{ open: isOpen, setOpen }}>
|
|
31
|
+
<View ref={ref} {...props}>
|
|
32
|
+
{children}
|
|
33
|
+
</View>
|
|
34
|
+
</PopoverContext.Provider>
|
|
35
|
+
);
|
|
36
|
+
});
|
|
37
|
+
Popover.displayName = 'Popover';
|
|
38
|
+
|
|
39
|
+
const PopoverTrigger = React.forwardRef<
|
|
40
|
+
React.ElementRef<typeof Pressable>,
|
|
41
|
+
React.ComponentPropsWithoutRef<typeof Pressable> & PopoverTriggerBaseProps
|
|
42
|
+
>(({ children, ...props }, ref) => {
|
|
43
|
+
const { setOpen } = React.useContext(PopoverContext);
|
|
2
44
|
|
|
3
|
-
export const Popover = () => {
|
|
4
45
|
return (
|
|
5
|
-
<
|
|
6
|
-
|
|
7
|
-
</
|
|
46
|
+
<Pressable ref={ref} onPress={() => setOpen(true)} {...props}>
|
|
47
|
+
{children}
|
|
48
|
+
</Pressable>
|
|
8
49
|
);
|
|
9
|
-
};
|
|
50
|
+
});
|
|
51
|
+
PopoverTrigger.displayName = 'PopoverTrigger';
|
|
52
|
+
|
|
53
|
+
const PopoverAnchor = React.forwardRef<
|
|
54
|
+
React.ElementRef<typeof View>,
|
|
55
|
+
React.ComponentPropsWithoutRef<typeof View> & PopoverAnchorBaseProps
|
|
56
|
+
>(({ ...props }, ref) => <View ref={ref} {...props} />);
|
|
57
|
+
PopoverAnchor.displayName = 'PopoverAnchor';
|
|
58
|
+
|
|
59
|
+
const PopoverContent = React.forwardRef<
|
|
60
|
+
React.ElementRef<typeof View>,
|
|
61
|
+
React.ComponentPropsWithoutRef<typeof View> & PopoverContentBaseProps
|
|
62
|
+
>(({ className, children, ...props }, ref) => {
|
|
63
|
+
const { open, setOpen } = React.useContext(PopoverContext);
|
|
64
|
+
|
|
65
|
+
return (
|
|
66
|
+
<Modal visible={open} transparent animationType="fade" onRequestClose={() => setOpen(false)}>
|
|
67
|
+
<Pressable className="flex-1" onPress={() => setOpen(false)}>
|
|
68
|
+
<View className="flex-1 items-center justify-center bg-black/50">
|
|
69
|
+
<Pressable onPress={() => {}}>
|
|
70
|
+
<View
|
|
71
|
+
ref={ref}
|
|
72
|
+
className={cn('bg-popover border-border mx-4 w-full max-w-sm rounded-md border p-4 shadow-lg', className)}
|
|
73
|
+
{...props}
|
|
74
|
+
>
|
|
75
|
+
{children}
|
|
76
|
+
</View>
|
|
77
|
+
</Pressable>
|
|
78
|
+
</View>
|
|
79
|
+
</Pressable>
|
|
80
|
+
</Modal>
|
|
81
|
+
);
|
|
82
|
+
});
|
|
83
|
+
PopoverContent.displayName = 'PopoverContent';
|
|
84
|
+
|
|
85
|
+
export { Popover, PopoverAnchor, PopoverContent, PopoverTrigger };
|
package/src/progress.tsx
CHANGED
|
@@ -1,9 +1,23 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { ProgressBaseProps } from '@gv-tech/ui-core';
|
|
2
|
+
import * as React from 'react';
|
|
3
|
+
import { View } from 'react-native';
|
|
2
4
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
5
|
+
import { cn } from './lib/utils';
|
|
6
|
+
|
|
7
|
+
const Progress = React.forwardRef<
|
|
8
|
+
React.ElementRef<typeof View>,
|
|
9
|
+
React.ComponentPropsWithoutRef<typeof View> & ProgressBaseProps
|
|
10
|
+
>(({ className, value, ...props }, ref) => (
|
|
11
|
+
<View
|
|
12
|
+
ref={ref}
|
|
13
|
+
className={cn('bg-muted relative h-2 w-full overflow-hidden rounded-full', className)}
|
|
14
|
+
accessibilityRole="progressbar"
|
|
15
|
+
role="progressbar"
|
|
16
|
+
{...props}
|
|
17
|
+
>
|
|
18
|
+
<View className="bg-primary h-full rounded-full" style={{ width: `${value || 0}%` }} />
|
|
19
|
+
</View>
|
|
20
|
+
));
|
|
21
|
+
Progress.displayName = 'Progress';
|
|
22
|
+
|
|
23
|
+
export { Progress };
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { ArrowUp } from 'lucide-react-native';
|
|
4
|
+
import * as React from 'react';
|
|
5
|
+
import {
|
|
6
|
+
AccessibilityInfo,
|
|
7
|
+
Animated,
|
|
8
|
+
type FlatList,
|
|
9
|
+
type NativeScrollEvent,
|
|
10
|
+
type NativeSyntheticEvent,
|
|
11
|
+
Platform,
|
|
12
|
+
ScrollView,
|
|
13
|
+
View,
|
|
14
|
+
} from 'react-native';
|
|
15
|
+
|
|
16
|
+
import type { ScrollToTopBaseProps } from '@gv-tech/ui-core';
|
|
17
|
+
import { Button } from './button';
|
|
18
|
+
import { cn } from './lib/utils';
|
|
19
|
+
|
|
20
|
+
export interface ScrollToTopProps extends ScrollToTopBaseProps {
|
|
21
|
+
/**
|
|
22
|
+
* For Native: The scroll target is typically a ref to a ScrollView or FlatList.
|
|
23
|
+
* This is required unless you manually call the scroll handler.
|
|
24
|
+
*/
|
|
25
|
+
scrollRef?: React.RefObject<ScrollView | FlatList>;
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Custom duration specifically for Native animations (opacity/transform).
|
|
29
|
+
* @default 300
|
|
30
|
+
*/
|
|
31
|
+
animationDuration?: number;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* GV Tech Animated Scroll To Top (Native)
|
|
36
|
+
*
|
|
37
|
+
* A floating action button that appears when scrolling down and allows the user
|
|
38
|
+
* to quickly return to the top of a ScrollView, FlatList, or SectionList.
|
|
39
|
+
*
|
|
40
|
+
* Reuses the internal GV Tech Button primitive for consistent styling.
|
|
41
|
+
*/
|
|
42
|
+
export interface ScrollToTopHandle {
|
|
43
|
+
handleScroll: (event: NativeSyntheticEvent<NativeScrollEvent>) => void;
|
|
44
|
+
scrollToTop: () => void;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export const ScrollToTop = React.forwardRef<ScrollToTopHandle, ScrollToTopProps>(
|
|
48
|
+
(
|
|
49
|
+
{
|
|
50
|
+
threshold = 240,
|
|
51
|
+
exitDuration = 450,
|
|
52
|
+
behavior, // Native uses 'smooth' or just scrolls
|
|
53
|
+
label = 'Scroll to top',
|
|
54
|
+
className,
|
|
55
|
+
scrollRef,
|
|
56
|
+
animationDuration = 300,
|
|
57
|
+
...props
|
|
58
|
+
},
|
|
59
|
+
ref,
|
|
60
|
+
) => {
|
|
61
|
+
const [isVisible, setIsVisible] = React.useState(false);
|
|
62
|
+
const [isExiting, setIsExiting] = React.useState(false);
|
|
63
|
+
|
|
64
|
+
// Animation states
|
|
65
|
+
const opacity = React.useRef(new Animated.Value(0)).current;
|
|
66
|
+
const translateY = React.useRef(new Animated.Value(20)).current;
|
|
67
|
+
|
|
68
|
+
const animateIn = () => {
|
|
69
|
+
Animated.parallel([
|
|
70
|
+
Animated.timing(opacity, {
|
|
71
|
+
toValue: 1,
|
|
72
|
+
duration: animationDuration,
|
|
73
|
+
useNativeDriver: true,
|
|
74
|
+
}),
|
|
75
|
+
Animated.timing(translateY, {
|
|
76
|
+
toValue: 0,
|
|
77
|
+
duration: animationDuration,
|
|
78
|
+
useNativeDriver: true,
|
|
79
|
+
}),
|
|
80
|
+
]).start();
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
const animateOut = () => {
|
|
84
|
+
Animated.parallel([
|
|
85
|
+
Animated.timing(opacity, {
|
|
86
|
+
toValue: 0,
|
|
87
|
+
duration: animationDuration,
|
|
88
|
+
useNativeDriver: true,
|
|
89
|
+
}),
|
|
90
|
+
Animated.timing(translateY, {
|
|
91
|
+
toValue: 20,
|
|
92
|
+
duration: animationDuration,
|
|
93
|
+
useNativeDriver: true,
|
|
94
|
+
}),
|
|
95
|
+
]).start(() => {
|
|
96
|
+
if (!isExiting) {
|
|
97
|
+
setIsVisible(false);
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
const handleScroll = (event: NativeSyntheticEvent<NativeScrollEvent>) => {
|
|
103
|
+
const offset = event.nativeEvent.contentOffset.y;
|
|
104
|
+
|
|
105
|
+
if (isExiting) {
|
|
106
|
+
if (offset <= threshold) {
|
|
107
|
+
setIsVisible(false);
|
|
108
|
+
setIsExiting(false);
|
|
109
|
+
}
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (offset > threshold && !isVisible) {
|
|
114
|
+
setIsVisible(true);
|
|
115
|
+
animateIn();
|
|
116
|
+
} else if (offset <= threshold && isVisible) {
|
|
117
|
+
animateOut();
|
|
118
|
+
}
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
const scrollToTop = () => {
|
|
122
|
+
setIsExiting(true);
|
|
123
|
+
animateOut();
|
|
124
|
+
|
|
125
|
+
// Delay the actual scroll to let exit animation start
|
|
126
|
+
setTimeout(() => {
|
|
127
|
+
if (scrollRef?.current) {
|
|
128
|
+
// Detect if it's a FlatList or ScrollView and handle types safely
|
|
129
|
+
const current = scrollRef.current;
|
|
130
|
+
if ('scrollToOffset' in current && typeof current.scrollToOffset === 'function') {
|
|
131
|
+
current.scrollToOffset({ offset: 0, animated: behavior !== 'auto' });
|
|
132
|
+
} else if ('scrollTo' in current && typeof current.scrollTo === 'function') {
|
|
133
|
+
current.scrollTo({ y: 0, animated: behavior !== 'auto' });
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Announcements for accessibility
|
|
138
|
+
if (Platform.OS === 'ios' || Platform.OS === 'android') {
|
|
139
|
+
AccessibilityInfo.announceForAccessibility('Scrolled to top');
|
|
140
|
+
}
|
|
141
|
+
}, exitDuration);
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
// Expose handleScroll so parents can pipe their scroll events to this component
|
|
145
|
+
React.useImperativeHandle(ref, () => ({
|
|
146
|
+
handleScroll,
|
|
147
|
+
scrollToTop,
|
|
148
|
+
}));
|
|
149
|
+
|
|
150
|
+
if (!isVisible && !isExiting) {
|
|
151
|
+
return null;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
return (
|
|
155
|
+
<View
|
|
156
|
+
pointerEvents="box-none"
|
|
157
|
+
style={{
|
|
158
|
+
position: 'absolute',
|
|
159
|
+
bottom: 24,
|
|
160
|
+
right: 24,
|
|
161
|
+
zIndex: 50,
|
|
162
|
+
}}
|
|
163
|
+
>
|
|
164
|
+
<Animated.View
|
|
165
|
+
style={{
|
|
166
|
+
opacity,
|
|
167
|
+
transform: [{ translateY }],
|
|
168
|
+
}}
|
|
169
|
+
>
|
|
170
|
+
<Button
|
|
171
|
+
variant="outline"
|
|
172
|
+
size="icon"
|
|
173
|
+
className={cn('bg-background/80 h-12 w-12 rounded-full border-2 shadow-lg', className)}
|
|
174
|
+
onPress={scrollToTop}
|
|
175
|
+
aria-label={label}
|
|
176
|
+
{...props}
|
|
177
|
+
>
|
|
178
|
+
<ArrowUp size={24} className="text-foreground" />
|
|
179
|
+
</Button>
|
|
180
|
+
</Animated.View>
|
|
181
|
+
</View>
|
|
182
|
+
);
|
|
183
|
+
},
|
|
184
|
+
);
|
|
185
|
+
|
|
186
|
+
ScrollToTop.displayName = 'ScrollToTop';
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { Coffee, ExternalLink } from 'lucide-react-native';
|
|
4
|
+
import * as React from 'react';
|
|
5
|
+
import { Linking, View } from 'react-native';
|
|
6
|
+
|
|
7
|
+
import { Button } from './button';
|
|
8
|
+
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from './dialog';
|
|
9
|
+
import { cn } from './lib/utils';
|
|
10
|
+
import { Text } from './text';
|
|
11
|
+
|
|
12
|
+
const normalizeBaseUrl = (url: string) => {
|
|
13
|
+
const trimmed = url.trim();
|
|
14
|
+
return trimmed.endsWith('/') ? trimmed.slice(0, -1) : trimmed;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
const sanitizeCreator = (creatorId: string) => creatorId.trim().replace(/^@+/, '');
|
|
18
|
+
|
|
19
|
+
const buildPageUrl = (supportUrl: string, creatorId: string) => {
|
|
20
|
+
const base = normalizeBaseUrl(supportUrl);
|
|
21
|
+
const creator = encodeURIComponent(sanitizeCreator(creatorId));
|
|
22
|
+
return `${base}/${creator}`;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export interface SupportFabProps extends Omit<React.ComponentPropsWithoutRef<typeof Button>, 'onPress'> {
|
|
26
|
+
supportUrl?: string;
|
|
27
|
+
creatorId: string;
|
|
28
|
+
title?: string;
|
|
29
|
+
description?: string;
|
|
30
|
+
open?: boolean;
|
|
31
|
+
defaultOpen?: boolean;
|
|
32
|
+
onOpenChange?: (open: boolean) => void;
|
|
33
|
+
positionClassName?: string;
|
|
34
|
+
buttonClassName?: string;
|
|
35
|
+
panelClassName?: string;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function SupportFab({
|
|
39
|
+
supportUrl = 'https://www.buymeacoffee.com',
|
|
40
|
+
creatorId,
|
|
41
|
+
title = 'Buy me a coffee',
|
|
42
|
+
description = 'Support the project directly from this panel.',
|
|
43
|
+
open,
|
|
44
|
+
defaultOpen = false,
|
|
45
|
+
onOpenChange,
|
|
46
|
+
positionClassName,
|
|
47
|
+
buttonClassName,
|
|
48
|
+
panelClassName,
|
|
49
|
+
className,
|
|
50
|
+
...buttonProps
|
|
51
|
+
}: SupportFabProps) {
|
|
52
|
+
const isControlled = open !== undefined;
|
|
53
|
+
const [internalOpen, setInternalOpen] = React.useState(defaultOpen);
|
|
54
|
+
const isOpen = isControlled ? open : internalOpen;
|
|
55
|
+
|
|
56
|
+
const setOpen = React.useCallback(
|
|
57
|
+
(nextOpen: boolean) => {
|
|
58
|
+
if (!isControlled) {
|
|
59
|
+
setInternalOpen(nextOpen);
|
|
60
|
+
}
|
|
61
|
+
onOpenChange?.(nextOpen);
|
|
62
|
+
},
|
|
63
|
+
[isControlled, onOpenChange],
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
const pageUrl = React.useMemo(() => buildPageUrl(supportUrl, creatorId), [creatorId, supportUrl]);
|
|
67
|
+
|
|
68
|
+
const handleExternalLink = React.useCallback(async () => {
|
|
69
|
+
try {
|
|
70
|
+
await Linking.openURL(pageUrl);
|
|
71
|
+
} catch (error) {
|
|
72
|
+
console.warn('Failed to open external link:', error);
|
|
73
|
+
}
|
|
74
|
+
}, [pageUrl]);
|
|
75
|
+
|
|
76
|
+
const trigger = (
|
|
77
|
+
<View
|
|
78
|
+
style={{
|
|
79
|
+
position: 'absolute',
|
|
80
|
+
bottom: 24,
|
|
81
|
+
right: 24,
|
|
82
|
+
zIndex: 50,
|
|
83
|
+
}}
|
|
84
|
+
className={positionClassName}
|
|
85
|
+
>
|
|
86
|
+
<Button
|
|
87
|
+
variant="outline"
|
|
88
|
+
size="icon"
|
|
89
|
+
className={cn(
|
|
90
|
+
'h-14 w-14 rounded-full border border-black/15 bg-[#ffdd00] shadow-lg',
|
|
91
|
+
buttonClassName,
|
|
92
|
+
className,
|
|
93
|
+
)}
|
|
94
|
+
onPress={() => setOpen(true)}
|
|
95
|
+
aria-label="Support this project"
|
|
96
|
+
{...buttonProps}
|
|
97
|
+
>
|
|
98
|
+
<Coffee size={24} className="text-black" />
|
|
99
|
+
</Button>
|
|
100
|
+
</View>
|
|
101
|
+
);
|
|
102
|
+
|
|
103
|
+
return (
|
|
104
|
+
<>
|
|
105
|
+
{trigger}
|
|
106
|
+
<Dialog open={isOpen} onOpenChange={setOpen}>
|
|
107
|
+
<DialogContent className={cn('max-w-sm', panelClassName)}>
|
|
108
|
+
<DialogHeader>
|
|
109
|
+
<DialogTitle>{title}</DialogTitle>
|
|
110
|
+
<DialogDescription>{description}</DialogDescription>
|
|
111
|
+
</DialogHeader>
|
|
112
|
+
|
|
113
|
+
<View className="flex flex-col gap-4">
|
|
114
|
+
<View className="overflow-hidden rounded-md border">
|
|
115
|
+
<View className="bg-muted flex h-48 items-center justify-center">
|
|
116
|
+
<Text className="text-muted-foreground text-center text-sm">
|
|
117
|
+
Embedded support form would appear here on web.{'\n'}
|
|
118
|
+
On mobile, this opens the external support page.
|
|
119
|
+
</Text>
|
|
120
|
+
</View>
|
|
121
|
+
</View>
|
|
122
|
+
|
|
123
|
+
<View className="text-muted-foreground flex items-center justify-between gap-2 text-xs">
|
|
124
|
+
<Text className="text-muted-foreground flex-1 text-xs">
|
|
125
|
+
If the embedded checkout is unavailable, open the support page directly.
|
|
126
|
+
</Text>
|
|
127
|
+
<Button
|
|
128
|
+
variant="outline"
|
|
129
|
+
size="sm"
|
|
130
|
+
onPress={handleExternalLink}
|
|
131
|
+
className="flex flex-row items-center gap-1"
|
|
132
|
+
>
|
|
133
|
+
<Text className="text-xs">Open Buy Me a Coffee</Text>
|
|
134
|
+
<ExternalLink size={12} className="text-muted-foreground" />
|
|
135
|
+
</Button>
|
|
136
|
+
</View>
|
|
137
|
+
</View>
|
|
138
|
+
</DialogContent>
|
|
139
|
+
</Dialog>
|
|
140
|
+
</>
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export { buildPageUrl };
|