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