@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.
@@ -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 interface TabsProps
11
- extends Omit<React.ComponentPropsWithoutRef<typeof TabsPrimitive.Root>, 'onValueChange' | 'value'>, TabsBaseProps {}
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.displayName;
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.displayName;
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.displayName;
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.displayName;
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.displayName;
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
- { className, portalHost, ...props },
25
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
26
- _ref,
27
- ) => (
28
- <TooltipPrimitive.Portal hostName={portalHost}>
29
- <TooltipPrimitive.Overlay style={Platform.OS !== 'web' ? StyleSheet.absoluteFill : undefined}>
30
- <Animated.View
31
- entering={FadeIn}
32
- exiting={FadeOut}
33
- className={cn(
34
- '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',
35
- className,
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}</>;