@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.
- package/README.md +42 -21
- package/package.json +36 -3
- package/src/BottomSheetContainer.tsx +2 -2
- package/src/BulkActionBar.tsx +240 -105
- package/src/Button.tsx +11 -7
- package/src/ConfirmDialog.tsx +25 -35
- package/src/Dialog.tsx +47 -35
- package/src/DividedList.tsx +11 -7
- package/src/DropdownMenu.tsx +22 -8
- package/src/FloatingSurface.tsx +5 -2
- package/src/FloatingTabBar.tsx +13 -22
- package/src/HeaderIconButton.tsx +4 -3
- package/src/Input.tsx +2 -2
- package/src/LoadingState.tsx +132 -0
- package/src/PageTitle.tsx +9 -2
- package/src/Pager.tsx +62 -0
- package/src/PasswordInput.tsx +1 -1
- package/src/PillTabs.tsx +144 -34
- package/src/Progress.tsx +9 -7
- package/src/Select.tsx +4 -4
- package/src/Sheet.tsx +29 -22
- package/src/SheetRow.tsx +27 -9
- package/src/Skeleton.tsx +91 -9
- package/src/SurfaceRow.tsx +111 -0
- package/src/SwipeableRow.tsx +43 -0
- package/src/Switch.tsx +6 -4
- package/src/Tabs.tsx +68 -13
- package/src/Toast.tsx +107 -65
- package/src/ai/activity-step-detail.tsx +1 -3
- package/src/ai/activity-step-label.ts +981 -0
- package/src/ai/activity-step-types.ts +107 -0
- package/src/ai/activity-tasks-utils.ts +199 -0
- package/src/ai/activity-timeline.ts +45 -0
- package/src/ai/attachments.tsx +3 -1
- package/src/ai/chat-text.ts +13 -7
- package/src/ai/chat-thread.tsx +34 -27
- package/src/ai/choice-question.tsx +154 -35
- package/src/ai/code-block.tsx +189 -49
- package/src/ai/collapsible.tsx +23 -13
- package/src/ai/conversation.tsx +394 -57
- package/src/ai/icon-button.tsx +41 -6
- package/src/ai/index.ts +55 -4
- package/src/ai/message-action-sheet.tsx +4 -1
- package/src/ai/message.tsx +60 -31
- package/src/ai/prompt-input-menu.tsx +197 -0
- package/src/ai/prompt-input.tsx +301 -75
- package/src/ai/shimmer.tsx +4 -6
- package/src/ai/subagent-cards.tsx +84 -14
- package/src/ai/tool-approval-card.tsx +139 -8
- package/src/ai/trace.tsx +54 -17
- package/src/ai/unified-diff.tsx +182 -0
- package/src/auth/GoogleSignInButton.tsx +6 -3
- package/src/auth/SignInScreen.tsx +11 -2
- package/src/brand/ProviderIcon.tsx +136 -0
- package/src/brand/index.ts +2 -0
- package/src/configureTheme.ts +39 -11
- package/src/controlStyles.ts +46 -30
- package/src/entries/gesture-handler.ts +4 -0
- package/src/entries/keyboard-controller.ts +4 -0
- package/src/entries/markdown.ts +1 -1
- package/src/entries/primitives.ts +24 -3
- package/src/entries/theme.ts +2 -1
- package/src/entries/video.ts +4 -0
- package/src/entries/webview.ts +6 -0
- package/src/examples/AiChatGallery.tsx +70 -0
- package/src/floating.ts +25 -4
- package/src/haptics.ts +30 -4
- package/src/highlight.ts +302 -1
- package/src/index.ts +27 -5
- package/src/loadingGrid.ts +75 -0
- package/src/markdown/Markdown.tsx +234 -32
- package/src/markdown/MarkdownTable.tsx +188 -0
- package/src/markdown/tableText.ts +58 -0
- package/src/marketing/UpgradeToProCard.tsx +4 -3
- package/src/media/ImageGallery.tsx +224 -0
- package/src/media/MediaCard.tsx +100 -0
- package/src/media/imageGrid.ts +51 -0
- package/src/motion.ts +33 -0
- package/src/pagerSwipe.ts +102 -0
- package/src/palette.ts +8 -1
- package/src/primitives/button.tsx +4 -1
- package/src/primitives/keyboard-controller.tsx +106 -0
- package/src/primitives/keyboard-parts.tsx +104 -0
- package/src/primitives/overlay.tsx +133 -45
- package/src/primitives/pager-gesture.tsx +131 -0
- package/src/primitives/sheet-drag.tsx +305 -0
- package/src/primitives/sheet-gesture.tsx +181 -0
- package/src/primitives/swipe-row-gesture.tsx +290 -0
- package/src/primitives/toast-drag-gesture.tsx +92 -0
- package/src/primitives/web-click-guard.ts +50 -0
- package/src/relativeTime.ts +94 -0
- package/src/router/RouterStackPageHeader.tsx +2 -2
- package/src/settings/SettingsEmptyState.tsx +20 -10
- package/src/settings/SettingsField.tsx +11 -5
- package/src/settings/SettingsSection.tsx +8 -5
- package/src/settings/SettingsToggleRow.tsx +55 -22
- package/src/settings/relaxedSizeStyle.ts +11 -0
- package/src/sheetDrag.ts +67 -0
- package/src/swipeRow.ts +169 -0
- package/src/tabBarRoutes.ts +18 -0
- package/src/tabIndicator.ts +71 -0
- package/src/tailwind-preset.cjs +13 -6
- package/src/tailwind-preset.d.cts +5 -0
- package/src/theme.tsx +6 -2
- package/src/toastDrag.ts +58 -0
- package/src/typography.ts +20 -3
- package/src/useScheme.ts +10 -1
- package/src/video/InlineVideo.tsx +91 -0
- package/src/webview/HtmlView.tsx +91 -0
- package/src/webview/HtmlView.web.tsx +21 -0
- 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
|
+
}
|