@vvedantb/native-ui 0.13.0 → 0.17.1

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.
Files changed (111) hide show
  1. package/README.md +42 -21
  2. package/package.json +36 -3
  3. package/src/BottomSheetContainer.tsx +2 -2
  4. package/src/BulkActionBar.tsx +240 -105
  5. package/src/Button.tsx +11 -7
  6. package/src/ConfirmDialog.tsx +25 -35
  7. package/src/Dialog.tsx +47 -35
  8. package/src/DividedList.tsx +11 -7
  9. package/src/DropdownMenu.tsx +22 -8
  10. package/src/FloatingSurface.tsx +5 -2
  11. package/src/FloatingTabBar.tsx +13 -22
  12. package/src/HeaderIconButton.tsx +4 -3
  13. package/src/Input.tsx +2 -2
  14. package/src/LoadingState.tsx +132 -0
  15. package/src/PageTitle.tsx +9 -2
  16. package/src/Pager.tsx +62 -0
  17. package/src/PasswordInput.tsx +1 -1
  18. package/src/PillTabs.tsx +144 -34
  19. package/src/Progress.tsx +9 -7
  20. package/src/Select.tsx +4 -4
  21. package/src/Sheet.tsx +29 -22
  22. package/src/SheetRow.tsx +27 -9
  23. package/src/Skeleton.tsx +91 -9
  24. package/src/SurfaceRow.tsx +111 -0
  25. package/src/SwipeableRow.tsx +43 -0
  26. package/src/Switch.tsx +6 -4
  27. package/src/Tabs.tsx +68 -13
  28. package/src/Toast.tsx +107 -65
  29. package/src/ai/activity-step-detail.tsx +1 -3
  30. package/src/ai/activity-step-label.ts +981 -0
  31. package/src/ai/activity-step-types.ts +107 -0
  32. package/src/ai/activity-tasks-utils.ts +199 -0
  33. package/src/ai/activity-timeline.ts +45 -0
  34. package/src/ai/attachments.tsx +3 -1
  35. package/src/ai/chat-text.ts +13 -7
  36. package/src/ai/chat-thread.tsx +34 -27
  37. package/src/ai/choice-question.tsx +154 -35
  38. package/src/ai/code-block.tsx +189 -49
  39. package/src/ai/collapsible.tsx +23 -13
  40. package/src/ai/conversation.tsx +394 -57
  41. package/src/ai/icon-button.tsx +41 -6
  42. package/src/ai/index.ts +55 -4
  43. package/src/ai/message-action-sheet.tsx +4 -1
  44. package/src/ai/message.tsx +60 -31
  45. package/src/ai/prompt-input-menu.tsx +197 -0
  46. package/src/ai/prompt-input.tsx +301 -75
  47. package/src/ai/shimmer.tsx +4 -6
  48. package/src/ai/subagent-cards.tsx +84 -14
  49. package/src/ai/tool-approval-card.tsx +139 -8
  50. package/src/ai/trace.tsx +54 -17
  51. package/src/ai/unified-diff.tsx +182 -0
  52. package/src/auth/GoogleSignInButton.tsx +6 -3
  53. package/src/auth/SignInScreen.tsx +11 -2
  54. package/src/brand/ProviderIcon.tsx +136 -0
  55. package/src/brand/index.ts +2 -0
  56. package/src/configureTheme.ts +39 -11
  57. package/src/controlStyles.ts +46 -30
  58. package/src/entries/gesture-handler.ts +4 -0
  59. package/src/entries/keyboard-controller.ts +4 -0
  60. package/src/entries/markdown.ts +1 -1
  61. package/src/entries/primitives.ts +24 -3
  62. package/src/entries/theme.ts +2 -1
  63. package/src/entries/video.ts +4 -0
  64. package/src/entries/webview.ts +6 -0
  65. package/src/examples/AiChatGallery.tsx +70 -0
  66. package/src/floating.ts +25 -4
  67. package/src/haptics.ts +30 -4
  68. package/src/highlight.ts +302 -1
  69. package/src/index.ts +27 -5
  70. package/src/loadingGrid.ts +75 -0
  71. package/src/markdown/Markdown.tsx +234 -32
  72. package/src/markdown/MarkdownTable.tsx +188 -0
  73. package/src/markdown/tableText.ts +58 -0
  74. package/src/marketing/UpgradeToProCard.tsx +4 -3
  75. package/src/media/ImageGallery.tsx +224 -0
  76. package/src/media/MediaCard.tsx +100 -0
  77. package/src/media/imageGrid.ts +51 -0
  78. package/src/motion.ts +33 -0
  79. package/src/pagerSwipe.ts +102 -0
  80. package/src/palette.ts +8 -1
  81. package/src/primitives/button.tsx +4 -1
  82. package/src/primitives/keyboard-controller.tsx +106 -0
  83. package/src/primitives/keyboard-parts.tsx +104 -0
  84. package/src/primitives/overlay.tsx +133 -45
  85. package/src/primitives/pager-gesture.tsx +131 -0
  86. package/src/primitives/sheet-drag.tsx +305 -0
  87. package/src/primitives/sheet-gesture.tsx +181 -0
  88. package/src/primitives/swipe-row-gesture.tsx +290 -0
  89. package/src/primitives/toast-drag-gesture.tsx +92 -0
  90. package/src/primitives/web-click-guard.ts +50 -0
  91. package/src/relativeTime.ts +94 -0
  92. package/src/router/RouterStackPageHeader.tsx +2 -2
  93. package/src/settings/SettingsEmptyState.tsx +20 -10
  94. package/src/settings/SettingsField.tsx +11 -5
  95. package/src/settings/SettingsSection.tsx +8 -5
  96. package/src/settings/SettingsToggleRow.tsx +55 -22
  97. package/src/settings/relaxedSizeStyle.ts +11 -0
  98. package/src/sheetDrag.ts +67 -0
  99. package/src/swipeRow.ts +169 -0
  100. package/src/tabBarRoutes.ts +18 -0
  101. package/src/tabIndicator.ts +71 -0
  102. package/src/tailwind-preset.cjs +13 -6
  103. package/src/tailwind-preset.d.cts +5 -0
  104. package/src/theme.tsx +6 -2
  105. package/src/toastDrag.ts +58 -0
  106. package/src/typography.ts +20 -3
  107. package/src/useScheme.ts +10 -1
  108. package/src/video/InlineVideo.tsx +91 -0
  109. package/src/webview/HtmlView.tsx +91 -0
  110. package/src/webview/HtmlView.web.tsx +21 -0
  111. package/src/webview/htmlView.ts +45 -0
@@ -0,0 +1,91 @@
1
+ import { useState } from "react";
2
+ import { Image, Pressable, StyleSheet, View } from "react-native";
3
+ import { useVideoPlayer, VideoView } from "expo-video";
4
+ import { IconPlayerPlayFilled } from "@tabler/icons-react-native";
5
+ import { MEDIA } from "../media/imageGrid";
6
+ import { withAlpha } from "../palette";
7
+ import { tokens } from "../tokens";
8
+ import { useScheme } from "../useScheme";
9
+
10
+ /** The box before playback: 16:9 unless the caller knows the video's shape. */
11
+ export const INLINE_VIDEO_ASPECT = 16 / 9;
12
+
13
+ export type InlineVideoProps = {
14
+ /** An http(s) or local video URL. */
15
+ url: string;
16
+ /** Width over height of the box, fixed so nothing moves when the video loads. Default 16:9. */
17
+ aspectRatio?: number;
18
+ /** An image to show before playback. Without it the box is black with the play button. */
19
+ posterUrl?: string;
20
+ /** Names the play button. Default "Play video". */
21
+ accessibilityLabel?: string;
22
+ testID?: string;
23
+ };
24
+
25
+ /**
26
+ * An inline video on `expo-video`, as the web `VideoPreview`: no autoplay and
27
+ * sound on. Until the play button is pressed it is a still box (the poster,
28
+ * or black), so a list of replies holds no players; pressing it mounts the
29
+ * player, starts playback and shows the platform controls (play and pause,
30
+ * scrub, full screen). The box keeps one size throughout, letterboxing the
31
+ * video (`contain`). Nothing plays or moves on its own, so it needs no Reduce
32
+ * Motion branch.
33
+ *
34
+ * `@vvedantb/native-ui/video` is the only entry that imports `expo-video` (an
35
+ * optional peer).
36
+ */
37
+ export function InlineVideo({ url, aspectRatio = INLINE_VIDEO_ASPECT, posterUrl, accessibilityLabel = "Play video", testID }: InlineVideoProps) {
38
+ const scheme = useScheme();
39
+ const [started, setStarted] = useState(false);
40
+ return (
41
+ <View
42
+ testID={testID}
43
+ style={{
44
+ width: "100%",
45
+ aspectRatio,
46
+ borderRadius: MEDIA.radius,
47
+ borderWidth: 1,
48
+ borderColor: MEDIA.outline[scheme],
49
+ overflow: "hidden",
50
+ backgroundColor: tokens.color.black,
51
+ }}
52
+ >
53
+ {started ? (
54
+ <Player url={url} />
55
+ ) : (
56
+ <Pressable
57
+ accessibilityRole="button"
58
+ accessibilityLabel={accessibilityLabel}
59
+ onPress={() => setStarted(true)}
60
+ className="flex-1 items-center justify-center active:opacity-80"
61
+ >
62
+ {posterUrl ? <Image source={{ uri: posterUrl }} resizeMode="cover" style={StyleSheet.absoluteFill} /> : null}
63
+ <View
64
+ className="h-14 w-14 items-center justify-center rounded-full"
65
+ style={{ backgroundColor: withAlpha(tokens.color.black, 0.5), borderWidth: 1, borderColor: withAlpha(tokens.color.white, 0.2) }}
66
+ >
67
+ <IconPlayerPlayFilled size={24} color={tokens.color.white} />
68
+ </View>
69
+ </Pressable>
70
+ )}
71
+ </View>
72
+ );
73
+ }
74
+
75
+ /** Mounted on the first press: one player per pressed video, released when the row unmounts. */
76
+ function Player({ url }: { url: string }) {
77
+ const player = useVideoPlayer(url, (created) => {
78
+ created.loop = false;
79
+ created.play();
80
+ });
81
+ return (
82
+ <VideoView
83
+ player={player}
84
+ nativeControls
85
+ contentFit="contain"
86
+ fullscreenOptions={{ enable: true }}
87
+ allowsPictureInPicture={false}
88
+ style={StyleSheet.absoluteFill}
89
+ />
90
+ );
91
+ }
@@ -0,0 +1,91 @@
1
+ import { useEffect, useRef, useState } from "react";
2
+ import { View } from "react-native";
3
+ import { WebView } from "react-native-webview";
4
+ import { HTML_VIEW_HEIGHT_SCRIPT, htmlViewNavigation, htmlViewPostScript, parseHtmlViewHeight } from "./htmlView";
5
+
6
+ export type HtmlViewProps = {
7
+ /** A whole HTML document, shown as written. */
8
+ html: string;
9
+ /** Names the view for TalkBack and VoiceOver. */
10
+ title: string;
11
+ /** The box height. Pass the page's own (`onContentHeight`) once known; the page scrolls inside past it. */
12
+ height: number;
13
+ /** Lets the page run scripts. Default `true`, as an `allow-scripts` iframe. */
14
+ javaScriptEnabled?: boolean;
15
+ /** A top-level http(s) link the page opened: show it outside, such as in the system browser. */
16
+ onOpenLink: (url: string) => void;
17
+ /** The page's content height in points, each time it changes. */
18
+ onContentHeight?: (height: number) => void;
19
+ /**
20
+ * A JSON message for the page, delivered as a `message` event once it has
21
+ * loaded and again whenever it changes (a theme switch, say), without a reload.
22
+ */
23
+ hostMessage?: string;
24
+ testID?: string;
25
+ };
26
+
27
+ /**
28
+ * An HTML page in a sandboxed `react-native-webview`, as the web's sandboxed
29
+ * `srcdoc` iframe: transparent on the screen behind it, scripts on, and no
30
+ * way out of its own document. A top-level link goes to `onOpenLink`; a
31
+ * popup (`target="_blank"`, `window.open`) does too; any other navigation is
32
+ * dropped. Incognito, with no DOM storage, cookies or file access, so the page
33
+ * keeps nothing between views and reaches nothing of the app's. It reports its
34
+ * height (`onContentHeight`) for a box that fits it.
35
+ *
36
+ * `@vvedantb/native-ui/webview` is the only entry that imports
37
+ * `react-native-webview` (an optional peer). On web, `HtmlView.web.tsx` draws a
38
+ * sandboxed iframe instead.
39
+ */
40
+ export function HtmlView({ html, title, height, javaScriptEnabled = true, onOpenLink, onContentHeight, hostMessage, testID }: HtmlViewProps) {
41
+ const ref = useRef<WebView>(null);
42
+ const [loaded, setLoaded] = useState(false);
43
+
44
+ // The page is an external system: push each host message into it once it can listen.
45
+ useEffect(() => {
46
+ if (loaded && hostMessage) ref.current?.injectJavaScript(htmlViewPostScript(hostMessage));
47
+ }, [loaded, hostMessage]);
48
+
49
+ const openIfWeb = (url: string) => {
50
+ if (htmlViewNavigation(url, true) === "open") onOpenLink(url);
51
+ };
52
+
53
+ return (
54
+ <View testID={testID} style={{ height }}>
55
+ <WebView
56
+ ref={ref}
57
+ source={{ html }}
58
+ accessibilityLabel={title}
59
+ // Every URL reaches `onShouldStartLoadWithRequest`: outside the
60
+ // whitelist the WebView would hand it to `Linking` itself (an
61
+ // `intent:` link would open another app).
62
+ originWhitelist={["*"]}
63
+ javaScriptEnabled={javaScriptEnabled}
64
+ injectedJavaScriptBeforeContentLoaded={javaScriptEnabled ? HTML_VIEW_HEIGHT_SCRIPT : undefined}
65
+ onMessage={(event) => {
66
+ const reported = parseHtmlViewHeight(event.nativeEvent.data);
67
+ if (reported !== null) onContentHeight?.(reported);
68
+ }}
69
+ onShouldStartLoadWithRequest={(request) => {
70
+ const action = htmlViewNavigation(request.url, request.isTopFrame);
71
+ if (action === "open") onOpenLink(request.url);
72
+ return action === "load";
73
+ }}
74
+ setSupportMultipleWindows
75
+ onOpenWindow={(event) => openIfWeb(event.nativeEvent.targetUrl)}
76
+ onLoadEnd={() => setLoaded(true)}
77
+ incognito
78
+ domStorageEnabled={false}
79
+ thirdPartyCookiesEnabled={false}
80
+ sharedCookiesEnabled={false}
81
+ allowFileAccess={false}
82
+ allowFileAccessFromFileURLs={false}
83
+ allowUniversalAccessFromFileURLs={false}
84
+ mixedContentMode="never"
85
+ mediaPlaybackRequiresUserAction
86
+ nestedScrollEnabled
87
+ style={{ backgroundColor: "transparent" }}
88
+ />
89
+ </View>
90
+ );
91
+ }
@@ -0,0 +1,21 @@
1
+ import { View } from "react-native";
2
+ import type { HtmlViewProps } from "./HtmlView";
3
+
4
+ /**
5
+ * `HtmlView` on web (`react-native-webview` has no web build): the page in a
6
+ * sandboxed `srcdoc` iframe at `height`, scrolling inside. Scripts, forms and
7
+ * popups only, with no `allow-same-origin`. It does not report a height or
8
+ * deliver `hostMessage`; the native view does.
9
+ */
10
+ export function HtmlView({ html, title, height, javaScriptEnabled = true, testID }: HtmlViewProps) {
11
+ return (
12
+ <View testID={testID} style={{ height }}>
13
+ <iframe
14
+ title={title}
15
+ srcDoc={html}
16
+ sandbox={javaScriptEnabled ? "allow-scripts allow-forms allow-popups" : ""}
17
+ style={{ border: 0, width: "100%", height: "100%", background: "transparent" }}
18
+ />
19
+ </View>
20
+ );
21
+ }
@@ -0,0 +1,45 @@
1
+ /**
2
+ * The pure half of `HtmlView`: which navigations the page may make, and the
3
+ * script that reports the page's height to the app.
4
+ */
5
+
6
+ /** What `HtmlView` does with a navigation: let it load, hand it to `onOpenLink`, or drop it. */
7
+ export type HtmlViewNavigation = "load" | "open" | "block";
8
+
9
+ /**
10
+ * The page never leaves its own document. Its first load (`about:`) and its
11
+ * frames load; a top-level http(s) link goes to `onOpenLink` (the system
12
+ * browser), as the web's sandboxed frame opens links in a new tab; any other
13
+ * scheme (`javascript:`, `intent:`, `file:`, `data:`) is dropped.
14
+ */
15
+ export function htmlViewNavigation(url: string, isTopFrame: boolean): HtmlViewNavigation {
16
+ if (!isTopFrame) return "load";
17
+ if (url.startsWith("about:")) return "load";
18
+ return /^https?:\/\//i.test(url) ? "open" : "block";
19
+ }
20
+
21
+ const HEIGHT_PREFIX = "native-ui:height:";
22
+
23
+ /**
24
+ * Runs in the page before its own scripts. It posts the document's height
25
+ * (`native-ui:height:<px>`) whenever it changes: the full scroll height when
26
+ * the content is taller than the view, else the content's own height, so the
27
+ * view can both grow and shrink to fit.
28
+ */
29
+ export const HTML_VIEW_HEIGHT_SCRIPT = `(function(){var h,o;function z(){var r=document.documentElement,b=window.ReactNativeWebView;if(!r||!b)return;var v=Math.ceil(r.scrollHeight>r.clientHeight?r.scrollHeight:r.getBoundingClientRect().height);if(v===h)return;h=v;b.postMessage(${JSON.stringify(HEIGHT_PREFIX)}+v)}if(window.ResizeObserver){o=new ResizeObserver(z);o.observe(document.documentElement)}document.addEventListener("DOMContentLoaded",function(){if(o&&document.body)o.observe(document.body);z()});window.addEventListener("load",z)})();true;`;
30
+
31
+ /** The height in a message from `HTML_VIEW_HEIGHT_SCRIPT`; `null` for any other message. */
32
+ export function parseHtmlViewHeight(data: string): number | null {
33
+ if (!data.startsWith(HEIGHT_PREFIX)) return null;
34
+ const height = Number(data.slice(HEIGHT_PREFIX.length));
35
+ return Number.isFinite(height) && height > 0 ? height : null;
36
+ }
37
+
38
+ /**
39
+ * Script that delivers `message` (a JSON string) to the page as a `message`
40
+ * event from its own window, as a host frame's `postMessage` would. In a
41
+ * WebView the page is the top window, so `event.source === window.parent`.
42
+ */
43
+ export function htmlViewPostScript(message: string): string {
44
+ return `window.postMessage(JSON.parse(${JSON.stringify(message)}),"*");true;`;
45
+ }