@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
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import {
|
|
3
|
+
LayoutChangeEvent,
|
|
4
|
+
NativeScrollEvent,
|
|
5
|
+
NativeSyntheticEvent,
|
|
6
|
+
Platform,
|
|
7
|
+
Text as RNText,
|
|
8
|
+
ScrollView,
|
|
9
|
+
TouchableOpacity,
|
|
10
|
+
View,
|
|
11
|
+
} from 'react-native';
|
|
12
|
+
|
|
13
|
+
import {
|
|
14
|
+
HeadingItem as BaseHeadingItem,
|
|
15
|
+
TableOfContentsContentBaseProps,
|
|
16
|
+
TableOfContentsRootBaseProps,
|
|
17
|
+
} from '@gv-tech/ui-core';
|
|
18
|
+
import { cn, slugify } from './lib/utils';
|
|
19
|
+
import { Text } from './text';
|
|
20
|
+
|
|
21
|
+
interface HeadingItem extends BaseHeadingItem {
|
|
22
|
+
pageY: number;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
interface TOCContextValue {
|
|
26
|
+
headings: HeadingItem[];
|
|
27
|
+
activeId: string | null;
|
|
28
|
+
activeHeadingText: string | null;
|
|
29
|
+
registerHeading: (id: string, text: string, level: number, pageY: number) => void;
|
|
30
|
+
unregisterHeading: (id: string) => void;
|
|
31
|
+
scrollToHeading: (id: string) => void;
|
|
32
|
+
onScroll: (event: NativeSyntheticEvent<NativeScrollEvent>) => void;
|
|
33
|
+
scrollViewRef: React.RefObject<ScrollView | null>;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const TOCContext = React.createContext<TOCContextValue | undefined>(undefined);
|
|
37
|
+
|
|
38
|
+
export function useTOC() {
|
|
39
|
+
const context = React.useContext(TOCContext);
|
|
40
|
+
if (!context) {
|
|
41
|
+
throw new Error('useTOC must be used within a TableOfContents provider');
|
|
42
|
+
}
|
|
43
|
+
return context;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Native Table of Contents Provider
|
|
48
|
+
*/
|
|
49
|
+
export function TableOfContents({ children, activeId: activeIdOverride }: TableOfContentsRootBaseProps) {
|
|
50
|
+
const [headings, setHeadings] = React.useState<HeadingItem[]>([]);
|
|
51
|
+
const [activeId, setActiveId] = React.useState<string | null>(null);
|
|
52
|
+
const scrollViewRef = React.useRef<ScrollView>(null);
|
|
53
|
+
|
|
54
|
+
const activeHeadingText = React.useMemo(() => {
|
|
55
|
+
const active = activeIdOverride || activeId;
|
|
56
|
+
return headings.find((h) => h.id === active)?.text || null;
|
|
57
|
+
}, [headings, activeId, activeIdOverride]);
|
|
58
|
+
|
|
59
|
+
const registerHeading = React.useCallback((id: string, text: string, level: number, pageY: number) => {
|
|
60
|
+
setHeadings((prev) => {
|
|
61
|
+
const exists = prev.find((h) => h.id === id);
|
|
62
|
+
if (exists && Math.abs(exists.pageY - pageY) < 1) {
|
|
63
|
+
return prev;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const newHeadings = exists
|
|
67
|
+
? prev.map((h) => (h.id === id ? { id, text, level, pageY } : h))
|
|
68
|
+
: [...prev, { id, text, level, pageY }];
|
|
69
|
+
|
|
70
|
+
return newHeadings.sort((a, b) => a.pageY - b.pageY);
|
|
71
|
+
});
|
|
72
|
+
}, []);
|
|
73
|
+
|
|
74
|
+
const unregisterHeading = React.useCallback((id: string) => {
|
|
75
|
+
setHeadings((prev) => prev.filter((h) => h.id !== id));
|
|
76
|
+
}, []);
|
|
77
|
+
|
|
78
|
+
const scrollToHeading = React.useCallback(
|
|
79
|
+
(id: string) => {
|
|
80
|
+
const heading = headings.find((h) => h.id === id);
|
|
81
|
+
if (heading && scrollViewRef.current) {
|
|
82
|
+
scrollViewRef.current.scrollTo({ y: heading.pageY - 20, animated: true });
|
|
83
|
+
}
|
|
84
|
+
},
|
|
85
|
+
[headings],
|
|
86
|
+
);
|
|
87
|
+
|
|
88
|
+
const onScroll = React.useCallback(
|
|
89
|
+
(event: NativeSyntheticEvent<NativeScrollEvent>) => {
|
|
90
|
+
const scrollY = event.nativeEvent.contentOffset.y;
|
|
91
|
+
|
|
92
|
+
// Find the current active heading
|
|
93
|
+
let currentId = null;
|
|
94
|
+
for (let i = headings.length - 1; i >= 0; i--) {
|
|
95
|
+
if (scrollY >= headings[i].pageY - 100) {
|
|
96
|
+
// Offset for header/buffer
|
|
97
|
+
currentId = headings[i].id;
|
|
98
|
+
break;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (currentId !== activeId) {
|
|
103
|
+
setActiveId(currentId);
|
|
104
|
+
}
|
|
105
|
+
},
|
|
106
|
+
[headings, activeId],
|
|
107
|
+
);
|
|
108
|
+
|
|
109
|
+
const value = React.useMemo(
|
|
110
|
+
() => ({
|
|
111
|
+
headings,
|
|
112
|
+
activeId: activeIdOverride || activeId,
|
|
113
|
+
activeHeadingText,
|
|
114
|
+
registerHeading,
|
|
115
|
+
unregisterHeading,
|
|
116
|
+
scrollToHeading,
|
|
117
|
+
onScroll,
|
|
118
|
+
scrollViewRef,
|
|
119
|
+
}),
|
|
120
|
+
[
|
|
121
|
+
headings,
|
|
122
|
+
activeId,
|
|
123
|
+
activeIdOverride,
|
|
124
|
+
activeHeadingText,
|
|
125
|
+
registerHeading,
|
|
126
|
+
unregisterHeading,
|
|
127
|
+
scrollToHeading,
|
|
128
|
+
onScroll,
|
|
129
|
+
],
|
|
130
|
+
);
|
|
131
|
+
|
|
132
|
+
return (
|
|
133
|
+
<TOCContext.Provider value={value}>
|
|
134
|
+
<View className="flex-1">{children}</View>
|
|
135
|
+
</TOCContext.Provider>
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Heading component that registers itself with the TOC provider
|
|
141
|
+
*/
|
|
142
|
+
export function TableOfContentsHeader({
|
|
143
|
+
children,
|
|
144
|
+
level = 2,
|
|
145
|
+
id: manualId,
|
|
146
|
+
className,
|
|
147
|
+
}: {
|
|
148
|
+
children: string;
|
|
149
|
+
level?: number;
|
|
150
|
+
id?: string;
|
|
151
|
+
className?: string;
|
|
152
|
+
}) {
|
|
153
|
+
const { registerHeading, unregisterHeading } = useTOC();
|
|
154
|
+
const id = manualId || slugify(children);
|
|
155
|
+
|
|
156
|
+
const onLayout = React.useCallback(
|
|
157
|
+
(event: LayoutChangeEvent) => {
|
|
158
|
+
const { y } = event.nativeEvent.layout;
|
|
159
|
+
registerHeading(id, children, level, y);
|
|
160
|
+
},
|
|
161
|
+
[id, children, level, registerHeading],
|
|
162
|
+
);
|
|
163
|
+
|
|
164
|
+
React.useEffect(() => {
|
|
165
|
+
return () => unregisterHeading(id);
|
|
166
|
+
}, [id, unregisterHeading]);
|
|
167
|
+
|
|
168
|
+
return (
|
|
169
|
+
<View onLayout={onLayout}>
|
|
170
|
+
<Text variant={level === 1 ? 'h1' : level === 2 ? 'h2' : level === 3 ? 'h3' : 'h4'} className={className}>
|
|
171
|
+
{children}
|
|
172
|
+
</Text>
|
|
173
|
+
</View>
|
|
174
|
+
);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Renders the TOC list of links
|
|
179
|
+
*/
|
|
180
|
+
export function TableOfContentsList({ className }: { className?: string }) {
|
|
181
|
+
const { headings, activeId, scrollToHeading } = useTOC();
|
|
182
|
+
|
|
183
|
+
if (headings.length === 0) {
|
|
184
|
+
return null;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const minLevel = Math.min(...headings.map((h) => h.level));
|
|
188
|
+
|
|
189
|
+
return (
|
|
190
|
+
<View className={cn('space-y-2 p-4', className)}>
|
|
191
|
+
<Text variant="overline" className="mb-2 font-bold">
|
|
192
|
+
On this page
|
|
193
|
+
</Text>
|
|
194
|
+
{headings.map((heading) => {
|
|
195
|
+
const isActive = activeId === heading.id;
|
|
196
|
+
const paddingLeft = (heading.level - minLevel) * 16;
|
|
197
|
+
|
|
198
|
+
return (
|
|
199
|
+
<TouchableOpacity
|
|
200
|
+
key={heading.id}
|
|
201
|
+
onPress={() => scrollToHeading(heading.id)}
|
|
202
|
+
style={{ paddingLeft }}
|
|
203
|
+
className="py-1"
|
|
204
|
+
>
|
|
205
|
+
<RNText className={cn('text-sm', isActive ? 'text-primary font-bold' : 'text-muted-foreground')}>
|
|
206
|
+
{heading.text}
|
|
207
|
+
</RNText>
|
|
208
|
+
</TouchableOpacity>
|
|
209
|
+
);
|
|
210
|
+
})}
|
|
211
|
+
</View>
|
|
212
|
+
);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Wrapper for content that handles scrolling
|
|
217
|
+
*/
|
|
218
|
+
export function TableOfContentsContent({ children, className }: TableOfContentsContentBaseProps) {
|
|
219
|
+
const { scrollViewRef, onScroll } = useTOC();
|
|
220
|
+
|
|
221
|
+
return (
|
|
222
|
+
<ScrollView
|
|
223
|
+
ref={scrollViewRef}
|
|
224
|
+
onScroll={onScroll}
|
|
225
|
+
className={cn('flex-1', className)}
|
|
226
|
+
{...(Platform.OS !== 'web' && {
|
|
227
|
+
scrollEventThrottle: 16,
|
|
228
|
+
contentContainerStyle: { padding: 16 },
|
|
229
|
+
})}
|
|
230
|
+
>
|
|
231
|
+
{children}
|
|
232
|
+
</ScrollView>
|
|
233
|
+
);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
export { TableOfContentsHeader as TableOfContentsHeading };
|
|
237
|
+
|
|
238
|
+
TableOfContents.List = TableOfContentsList;
|
|
239
|
+
TableOfContents.Content = TableOfContentsContent;
|
|
240
|
+
TableOfContents.Heading = TableOfContentsHeader;
|
package/src/tabs.tsx
CHANGED
|
@@ -5,10 +5,13 @@ import * as React from 'react';
|
|
|
5
5
|
import { cn } from './lib/utils';
|
|
6
6
|
import { TextClassContext } from './text';
|
|
7
7
|
|
|
8
|
-
const Tabs = TabsPrimitive.Root
|
|
8
|
+
const Tabs = React.forwardRef<React.ElementRef<typeof TabsPrimitive.Root>, TabsProps>((props, ref) => (
|
|
9
|
+
<TabsPrimitive.Root ref={ref} {...props} />
|
|
10
|
+
));
|
|
11
|
+
Tabs.displayName = 'Tabs';
|
|
9
12
|
|
|
10
|
-
export
|
|
11
|
-
|
|
13
|
+
export type TabsProps = Omit<React.ComponentPropsWithoutRef<typeof TabsPrimitive.Root>, 'onValueChange' | 'value'> &
|
|
14
|
+
Omit<TabsBaseProps, 'value'> & { value: string; onValueChange: (value: string) => void };
|
|
12
15
|
export interface TabsListProps extends React.ComponentPropsWithoutRef<typeof TabsPrimitive.List>, TabsListBaseProps {}
|
|
13
16
|
export interface TabsTriggerProps
|
|
14
17
|
extends
|
package/src/toast.tsx
CHANGED
|
@@ -30,7 +30,7 @@ const Toast = React.forwardRef<
|
|
|
30
30
|
</ToastPrimitive.Root>
|
|
31
31
|
);
|
|
32
32
|
});
|
|
33
|
-
Toast.displayName = ToastPrimitive.Root
|
|
33
|
+
Toast.displayName = ToastPrimitive.Root?.displayName || 'Toast';
|
|
34
34
|
|
|
35
35
|
const ToastTitle = React.forwardRef<
|
|
36
36
|
React.ElementRef<typeof ToastPrimitive.Title>,
|
|
@@ -38,7 +38,7 @@ const ToastTitle = React.forwardRef<
|
|
|
38
38
|
>(({ className, ...props }, ref) => (
|
|
39
39
|
<ToastPrimitive.Title ref={ref} className={cn('text-foreground text-sm font-semibold', className)} {...props} />
|
|
40
40
|
));
|
|
41
|
-
ToastTitle.displayName = ToastPrimitive.Title
|
|
41
|
+
ToastTitle.displayName = ToastPrimitive.Title?.displayName || 'ToastTitle';
|
|
42
42
|
|
|
43
43
|
const ToastDescription = React.forwardRef<
|
|
44
44
|
React.ElementRef<typeof ToastPrimitive.Description>,
|
|
@@ -50,7 +50,7 @@ const ToastDescription = React.forwardRef<
|
|
|
50
50
|
{...props}
|
|
51
51
|
/>
|
|
52
52
|
));
|
|
53
|
-
ToastDescription.displayName = ToastPrimitive.Description
|
|
53
|
+
ToastDescription.displayName = ToastPrimitive.Description?.displayName || 'ToastDescription';
|
|
54
54
|
|
|
55
55
|
const ToastClose = React.forwardRef<
|
|
56
56
|
React.ElementRef<typeof ToastPrimitive.Close>,
|
|
@@ -67,7 +67,7 @@ const ToastClose = React.forwardRef<
|
|
|
67
67
|
<X size={16} className="text-muted-foreground" />
|
|
68
68
|
</ToastPrimitive.Close>
|
|
69
69
|
));
|
|
70
|
-
ToastClose.displayName = ToastPrimitive.Close
|
|
70
|
+
ToastClose.displayName = ToastPrimitive.Close?.displayName || 'ToastClose';
|
|
71
71
|
|
|
72
72
|
const ToastAction = React.forwardRef<
|
|
73
73
|
React.ElementRef<typeof ToastPrimitive.Action>,
|
|
@@ -82,7 +82,7 @@ const ToastAction = React.forwardRef<
|
|
|
82
82
|
{...props}
|
|
83
83
|
/>
|
|
84
84
|
));
|
|
85
|
-
ToastAction.displayName = ToastPrimitive.Action
|
|
85
|
+
ToastAction.displayName = ToastPrimitive.Action?.displayName || 'ToastAction';
|
|
86
86
|
|
|
87
87
|
export type ToastProps = React.ComponentPropsWithoutRef<typeof Toast>;
|
|
88
88
|
export { Toast, ToastAction, ToastClose, ToastDescription, ToastProvider, ToastTitle, ToastViewport };
|
package/src/tooltip.tsx
CHANGED
|
@@ -19,28 +19,22 @@ const TooltipContent: React.ForwardRefExoticComponent<
|
|
|
19
19
|
React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Content> & {
|
|
20
20
|
portalHost?: string;
|
|
21
21
|
}
|
|
22
|
-
>(
|
|
23
|
-
|
|
24
|
-
{
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
<Text className="text-popover-foreground native:text-base text-sm">{props.children}</Text>
|
|
39
|
-
</Animated.View>
|
|
40
|
-
</TooltipPrimitive.Overlay>
|
|
41
|
-
</TooltipPrimitive.Portal>
|
|
42
|
-
),
|
|
43
|
-
);
|
|
22
|
+
>(({ className, portalHost, ...props }, _ref) => (
|
|
23
|
+
<TooltipPrimitive.Portal hostName={portalHost}>
|
|
24
|
+
<TooltipPrimitive.Overlay style={Platform.OS !== 'web' ? StyleSheet.absoluteFill : undefined}>
|
|
25
|
+
<Animated.View
|
|
26
|
+
entering={FadeIn}
|
|
27
|
+
exiting={FadeOut}
|
|
28
|
+
className={cn(
|
|
29
|
+
'border-border bg-popover web:animate-in web:fade-in-0 web:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 overflow-hidden rounded-md border px-3 py-1.5 shadow-md',
|
|
30
|
+
className,
|
|
31
|
+
)}
|
|
32
|
+
>
|
|
33
|
+
<Text className="text-popover-foreground native:text-base text-sm">{props.children}</Text>
|
|
34
|
+
</Animated.View>
|
|
35
|
+
</TooltipPrimitive.Overlay>
|
|
36
|
+
</TooltipPrimitive.Portal>
|
|
37
|
+
));
|
|
44
38
|
TooltipContent.displayName = TooltipPrimitive.Content?.displayName || 'TooltipContent';
|
|
45
39
|
|
|
46
40
|
const TooltipProvider = ({ children }: { children: React.ReactNode }) => <>{children}</>;
|