@gv-tech/ui-native 2.15.2 → 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/dist/index.d.ts +35 -0
- package/dist/ui-native.mjs +690 -569
- package/package.json +1 -1
- package/src/index.ts +8 -0
- package/src/lib/utils.ts +10 -0
- package/src/table-of-contents.tsx +237 -0
- package/src/toast.tsx +5 -5
package/package.json
CHANGED
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
|
|
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 };
|