@vvedantb/native-ui 0.17.2 → 0.18.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.
Files changed (220) hide show
  1. package/dist/AlertDialog.jsx +123 -0
  2. package/dist/AmbientBackground.jsx +73 -0
  3. package/dist/ArtifactList.jsx +59 -0
  4. package/dist/Avatar.jsx +285 -0
  5. package/dist/Badge.jsx +154 -0
  6. package/dist/BottomSheetContainer.jsx +208 -0
  7. package/dist/BulkActionBar.jsx +717 -0
  8. package/dist/Button.jsx +344 -0
  9. package/dist/CaptureButton.jsx +104 -0
  10. package/dist/Card.jsx +371 -0
  11. package/dist/Checkbox.jsx +177 -0
  12. package/dist/Chip.jsx +79 -0
  13. package/dist/ColorSwatches.jsx +159 -0
  14. package/dist/ConfirmDialog.jsx +157 -0
  15. package/dist/DateTimeField.jsx +204 -0
  16. package/dist/Dialog.jsx +614 -0
  17. package/dist/DividedList.jsx +120 -0
  18. package/dist/DropdownMenu.jsx +849 -0
  19. package/dist/EmptyState.jsx +434 -0
  20. package/dist/ExpoWebRoot.jsx +134 -0
  21. package/dist/FloatingFAB.jsx +95 -0
  22. package/dist/FloatingSurface.jsx +226 -0
  23. package/dist/FloatingTabBar.jsx +403 -0
  24. package/dist/HeaderIconButton.jsx +53 -0
  25. package/dist/IconButton.jsx +157 -0
  26. package/dist/Input.jsx +96 -0
  27. package/dist/Label.jsx +96 -0
  28. package/dist/LabeledSwitchRow.jsx +170 -0
  29. package/dist/LiquidGlass.jsx +2 -0
  30. package/dist/ListRow.jsx +281 -0
  31. package/dist/LoadingState.jsx +205 -0
  32. package/dist/PageIndicator.jsx +128 -0
  33. package/dist/PageTitle.jsx +205 -0
  34. package/dist/Pager.jsx +105 -0
  35. package/dist/PasswordInput.jsx +120 -0
  36. package/dist/PillTabs.jsx +300 -0
  37. package/dist/PinnedBotTile.jsx +212 -0
  38. package/dist/Progress.jsx +147 -0
  39. package/dist/SafeAreaContainer.jsx +38 -0
  40. package/dist/ScreenContainer.jsx +117 -0
  41. package/dist/SearchInput.jsx +150 -0
  42. package/dist/SectionLabel.jsx +128 -0
  43. package/dist/Select.jsx +223 -0
  44. package/dist/Separator.jsx +95 -0
  45. package/dist/Sheet.jsx +343 -0
  46. package/dist/SheetRow.jsx +198 -0
  47. package/dist/Skeleton.jsx +311 -0
  48. package/dist/Spinner.jsx +195 -0
  49. package/dist/StackPageHeader.jsx +160 -0
  50. package/dist/StatGrid.jsx +74 -0
  51. package/dist/StatusDot.jsx +129 -0
  52. package/dist/SurfaceRow.jsx +143 -0
  53. package/dist/SwipeableRow.jsx +50 -0
  54. package/dist/Switch.jsx +130 -0
  55. package/dist/Tabs.jsx +385 -0
  56. package/dist/Textarea.jsx +103 -0
  57. package/dist/ThemeToggle.jsx +78 -0
  58. package/dist/Toast.jsx +396 -0
  59. package/dist/ai/activity-step-detail.jsx +179 -0
  60. package/dist/ai/activity-step-label.js +816 -0
  61. package/dist/ai/activity-step-types.js +14 -0
  62. package/dist/ai/activity-step.jsx +98 -0
  63. package/dist/ai/activity-tasks-utils.js +172 -0
  64. package/dist/ai/activity-timeline.js +47 -0
  65. package/dist/ai/attachments.jsx +258 -0
  66. package/dist/ai/card-button.jsx +188 -0
  67. package/dist/ai/chain-of-thought.jsx +459 -0
  68. package/dist/ai/chat-jump-rail.jsx +57 -0
  69. package/dist/ai/chat-text.js +44 -0
  70. package/dist/ai/chat-thread.jsx +135 -0
  71. package/dist/ai/choice-question.jsx +925 -0
  72. package/dist/ai/code-block.jsx +574 -0
  73. package/dist/ai/collapsible.jsx +275 -0
  74. package/dist/ai/conversation.jsx +723 -0
  75. package/dist/ai/file-chip.jsx +198 -0
  76. package/dist/ai/icon-button.jsx +179 -0
  77. package/dist/ai/index.js +37 -0
  78. package/dist/ai/message-action-sheet.jsx +408 -0
  79. package/dist/ai/message-quote.jsx +241 -0
  80. package/dist/ai/message.jsx +739 -0
  81. package/dist/ai/prompt-input-menu.jsx +365 -0
  82. package/dist/ai/prompt-input.jsx +626 -0
  83. package/dist/ai/reasoning.jsx +446 -0
  84. package/dist/ai/reply-quote-chip.jsx +278 -0
  85. package/dist/ai/response-text.jsx +130 -0
  86. package/dist/ai/shimmer.jsx +149 -0
  87. package/dist/ai/sources.jsx +260 -0
  88. package/dist/ai/subagent-cards.jsx +645 -0
  89. package/dist/ai/suggestion.jsx +186 -0
  90. package/dist/ai/tool-approval-card.jsx +359 -0
  91. package/dist/ai/trace.jsx +728 -0
  92. package/dist/ai/unified-diff.jsx +280 -0
  93. package/dist/ai/use-streaming-disclosure.js +70 -0
  94. package/dist/auth/AuthProviders.jsx +27 -0
  95. package/dist/auth/AuthScreen.jsx +292 -0
  96. package/dist/auth/GoogleSignInButton.jsx +188 -0
  97. package/dist/auth/SecondFactorScreen.jsx +133 -0
  98. package/dist/auth/SignInScreen.jsx +482 -0
  99. package/dist/auth/authErrors.js +131 -0
  100. package/dist/auth/index.js +11 -0
  101. package/dist/auth/totp.js +378 -0
  102. package/dist/blur.js +36 -0
  103. package/dist/brand/AppIcons.jsx +118 -0
  104. package/dist/brand/AppMark.jsx +128 -0
  105. package/dist/brand/AppWordmark.jsx +138 -0
  106. package/dist/brand/GoogleMark.jsx +71 -0
  107. package/dist/brand/ProviderIcon.jsx +192 -0
  108. package/dist/brand/familyApps.js +33 -0
  109. package/dist/brand/index.js +11 -0
  110. package/dist/brand/svg.js +252 -0
  111. package/dist/brand/vProAppBenefits.js +19 -0
  112. package/dist/cn.js +5 -0
  113. package/dist/colors.js +15 -0
  114. package/dist/configureTheme.js +72 -0
  115. package/dist/controlStyles.js +520 -0
  116. package/dist/definiteSize.js +12 -0
  117. package/dist/dialogLayout.js +18 -0
  118. package/dist/entries/ai.js +4 -0
  119. package/dist/entries/ambient.js +3 -0
  120. package/dist/entries/auth.js +5 -0
  121. package/dist/entries/bottom-sheet.js +2 -0
  122. package/dist/entries/brand.js +2 -0
  123. package/dist/entries/button.js +2 -0
  124. package/dist/entries/date-time.js +3 -0
  125. package/dist/entries/expo-router.js +4 -0
  126. package/dist/entries/expo-web-root.js +3 -0
  127. package/dist/entries/floating.js +7 -0
  128. package/dist/entries/gesture-handler.js +4 -0
  129. package/dist/entries/keyboard-controller.js +4 -0
  130. package/dist/entries/layout.js +16 -0
  131. package/dist/entries/markdown.js +4 -0
  132. package/dist/entries/marketing.js +3 -0
  133. package/dist/entries/page-title.js +7 -0
  134. package/dist/entries/password-input.js +3 -0
  135. package/dist/entries/primitives.js +57 -0
  136. package/dist/entries/search-input.js +3 -0
  137. package/dist/entries/tab-bar.js +4 -0
  138. package/dist/entries/theme.js +7 -0
  139. package/dist/entries/toast.js +5 -0
  140. package/dist/entries/video.js +3 -0
  141. package/dist/entries/webview.js +4 -0
  142. package/dist/examples/AiChatGallery.jsx +509 -0
  143. package/dist/examples/ParityGallery.jsx +1307 -0
  144. package/dist/floating.js +62 -0
  145. package/dist/glassBlur.jsx +145 -0
  146. package/dist/gradient.js +17 -0
  147. package/dist/haptics.js +44 -0
  148. package/dist/highlight.js +382 -0
  149. package/dist/index.js +85 -0
  150. package/dist/initials.js +4 -0
  151. package/dist/loadingGrid.js +81 -0
  152. package/dist/markdown/Markdown.jsx +515 -0
  153. package/dist/markdown/MarkdownTable.jsx +443 -0
  154. package/dist/markdown/tableText.js +68 -0
  155. package/dist/marketing/PricingModal.jsx +608 -0
  156. package/dist/marketing/UpgradeToProCard.jsx +164 -0
  157. package/dist/marketing/pricing.js +34 -0
  158. package/dist/media/ImageGallery.jsx +580 -0
  159. package/dist/media/MediaCard.jsx +162 -0
  160. package/dist/media/imageGrid.js +54 -0
  161. package/dist/motion.js +33 -0
  162. package/dist/pagerSwipe.js +104 -0
  163. package/dist/palette.js +104 -0
  164. package/dist/primitives/button.jsx +127 -0
  165. package/dist/primitives/card.jsx +249 -0
  166. package/dist/primitives/icon.jsx +37 -0
  167. package/dist/primitives/input.jsx +56 -0
  168. package/dist/primitives/keyboard-controller.jsx +207 -0
  169. package/dist/primitives/keyboard-parts.jsx +75 -0
  170. package/dist/primitives/native-only-animated-view.jsx +68 -0
  171. package/dist/primitives/overlay.jsx +391 -0
  172. package/dist/primitives/pager-gesture.jsx +131 -0
  173. package/dist/primitives/select-host.jsx +103 -0
  174. package/dist/primitives/select.jsx +555 -0
  175. package/dist/primitives/sheet-drag.jsx +420 -0
  176. package/dist/primitives/sheet-gesture.jsx +219 -0
  177. package/dist/primitives/swipe-row-gesture.jsx +341 -0
  178. package/dist/primitives/switch.jsx +105 -0
  179. package/dist/primitives/text.jsx +145 -0
  180. package/dist/primitives/toast-drag-gesture.jsx +94 -0
  181. package/dist/primitives/use-controllable-state.js +40 -0
  182. package/dist/primitives/web-click-guard.js +69 -0
  183. package/dist/push.js +166 -0
  184. package/dist/relativeTime.js +70 -0
  185. package/dist/router/RouterStackPageHeader.jsx +53 -0
  186. package/dist/selectA11y.js +32 -0
  187. package/dist/settings/MoreFromProjectV.jsx +168 -0
  188. package/dist/settings/SettingsEmptyState.jsx +102 -0
  189. package/dist/settings/SettingsField.jsx +49 -0
  190. package/dist/settings/SettingsSection.jsx +101 -0
  191. package/dist/settings/SettingsStack.jsx +17 -0
  192. package/dist/settings/SettingsToggleRow.jsx +163 -0
  193. package/dist/settings/projectVMobileApps.js +126 -0
  194. package/dist/settings/relaxedSizeStyle.js +11 -0
  195. package/dist/sheetDrag.js +76 -0
  196. package/dist/svgA11y.js +46 -0
  197. package/dist/swipeRow.js +161 -0
  198. package/dist/tabBarRoutes.js +41 -0
  199. package/dist/tabIndicator.js +139 -0
  200. package/dist/tailwind-preset.cjs +255 -0
  201. package/dist/tailwind-preset.d.cts +55 -0
  202. package/dist/theme.jsx +62 -0
  203. package/dist/toastChrome.js +110 -0
  204. package/dist/toastDrag.js +51 -0
  205. package/dist/toastStore.js +54 -0
  206. package/dist/tokens.js +70 -0
  207. package/dist/typography.js +40 -0
  208. package/dist/useReduceMotion.js +32 -0
  209. package/dist/useReduceTransparency.js +27 -0
  210. package/dist/useScheme.js +15 -0
  211. package/dist/video/InlineVideo.jsx +115 -0
  212. package/dist/webview/HtmlView.jsx +169 -0
  213. package/dist/webview/HtmlView.web.jsx +63 -0
  214. package/dist/webview/htmlView.js +43 -0
  215. package/package.json +70 -33
  216. package/src/DropdownMenu.tsx +28 -16
  217. package/src/FloatingTabBar.tsx +78 -41
  218. package/src/ai/conversation.tsx +12 -8
  219. package/src/glassBlur.tsx +47 -22
  220. package/src/primitives/swipe-row-gesture.tsx +11 -2
@@ -0,0 +1,40 @@
1
+ import { c as _c } from "react/compiler-runtime";
2
+ import { useCallback, useState } from "react";
3
+
4
+ /** `value` when the parent controls it, else local state seeded by `defaultValue`. `onChange` fires either way. */
5
+ export function useControllableState(t0) {
6
+ const $ = _c(6);
7
+ const {
8
+ value,
9
+ defaultValue,
10
+ onChange
11
+ } = t0;
12
+ const [local, setLocal] = useState(defaultValue);
13
+ const controlled = value !== undefined;
14
+ const current = controlled ? value : local;
15
+ let t1;
16
+ if ($[0] !== controlled || $[1] !== onChange) {
17
+ t1 = next => {
18
+ if (!controlled) {
19
+ setLocal(next);
20
+ }
21
+ onChange?.(next);
22
+ };
23
+ $[0] = controlled;
24
+ $[1] = onChange;
25
+ $[2] = t1;
26
+ } else {
27
+ t1 = $[2];
28
+ }
29
+ const setValue = t1;
30
+ let t2;
31
+ if ($[3] !== current || $[4] !== setValue) {
32
+ t2 = [current, setValue];
33
+ $[3] = current;
34
+ $[4] = setValue;
35
+ $[5] = t2;
36
+ } else {
37
+ t2 = $[5];
38
+ }
39
+ return t2;
40
+ }
@@ -0,0 +1,69 @@
1
+ import { c as _c } from "react/compiler-runtime";
2
+ import { useRef } from "react";
3
+ import { Platform } from "react-native";
4
+
5
+ /** The part of a DOM element the web click guard uses: on the web a `View` is a DOM element. */
6
+
7
+ function isDomNode(node) {
8
+ return "addEventListener" in node && typeof node.addEventListener === "function";
9
+ }
10
+
11
+ /**
12
+ * On the web a pressable presses on the DOM click, which neither the touch
13
+ * responder nor Gesture Handler cancels (it captures the pointer on the
14
+ * element pressed). So, in the capture phase on the dragged content, this
15
+ * drops the click that ends a drag (`swallowNext`, at the drag's start).
16
+ * `swallowOnDown` decides afresh at each touch whether its click is dropped
17
+ * (a tap made while a swiped row is open). On Android and iOS it does
18
+ * nothing: there the drag cancels the press.
19
+ */
20
+ export function useWebClickGuard(t0) {
21
+ const $ = _c(3);
22
+ const swallowOnDown = t0 === undefined ? _temp : t0;
23
+ const swallow = useRef(false);
24
+ let t1;
25
+ if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
26
+ t1 = () => {
27
+ swallow.current = true;
28
+ };
29
+ $[0] = t1;
30
+ } else {
31
+ t1 = $[0];
32
+ }
33
+ let t2;
34
+ if ($[1] !== swallowOnDown) {
35
+ t2 = {
36
+ swallowNext: t1,
37
+ ref: node => {
38
+ if (Platform.OS !== "web" || !node || !isDomNode(node)) {
39
+ return;
40
+ }
41
+ const onClick = event => {
42
+ if (!swallow.current) {
43
+ return;
44
+ }
45
+ swallow.current = false;
46
+ event.stopPropagation();
47
+ event.preventDefault();
48
+ };
49
+ const onPointerDown = () => {
50
+ swallow.current = swallowOnDown();
51
+ };
52
+ node.addEventListener("click", onClick, true);
53
+ node.addEventListener("pointerdown", onPointerDown, true);
54
+ return () => {
55
+ node.removeEventListener("click", onClick, true);
56
+ node.removeEventListener("pointerdown", onPointerDown, true);
57
+ };
58
+ }
59
+ };
60
+ $[1] = swallowOnDown;
61
+ $[2] = t2;
62
+ } else {
63
+ t2 = $[2];
64
+ }
65
+ return t2;
66
+ }
67
+ function _temp() {
68
+ return false;
69
+ }
package/dist/push.js ADDED
@@ -0,0 +1,166 @@
1
+ import { useEffect, useRef } from "react";
2
+ import { Platform } from "react-native";
3
+
4
+ // `expo-notifications`, `expo-constants` and `expo-device` are optional peers.
5
+ // Metro bundles a `require` placed directly in a `try` block as an optional
6
+ // dependency, so a missing peer does not break the bundle.
7
+
8
+ let notifications;
9
+ let constants;
10
+ let device;
11
+
12
+ /** `expo-notifications`, or `null` when the optional peer is not installed. */
13
+ export function loadNotifications() {
14
+ if (notifications === undefined) {
15
+ try {
16
+ notifications = require("expo-notifications");
17
+ } catch {
18
+ notifications = null;
19
+ }
20
+ }
21
+ return notifications;
22
+ }
23
+
24
+ /** `expo-constants` (its default export is the `Constants` object), or `null` when not installed. */
25
+ export function loadConstants() {
26
+ if (constants === undefined) {
27
+ try {
28
+ constants = require("expo-constants");
29
+ } catch {
30
+ constants = null;
31
+ }
32
+ }
33
+ return constants;
34
+ }
35
+
36
+ /** `expo-device`, or `null` when the optional peer is not installed. */
37
+ export function loadDevice() {
38
+ if (device === undefined) {
39
+ try {
40
+ device = require("expo-device");
41
+ } catch {
42
+ device = null;
43
+ }
44
+ }
45
+ return device;
46
+ }
47
+
48
+ /** A tapped push: the notification's request id and its `data` object. */
49
+
50
+ /**
51
+ * The Expo push token for this device, or `null` when push is not possible:
52
+ * a peer is missing, a simulator or emulator, no permission, no EAS project id
53
+ * (`expo.extra.eas.projectId`), or no FCM credentials in this Android build.
54
+ */
55
+ export async function getExpoPushToken({
56
+ ask,
57
+ channelName = "default"
58
+ }) {
59
+ const Notifications = loadNotifications();
60
+ const constantsModule = loadConstants();
61
+ const deviceModule = loadDevice();
62
+ if (!Notifications || !constantsModule || !deviceModule) return null;
63
+ if (!deviceModule.isDevice) return null;
64
+ const projectId = constantsModule.default.expoConfig?.extra?.eas?.projectId;
65
+ if (typeof projectId !== "string" || projectId === "") return null;
66
+ if (Platform.OS === "android") {
67
+ await Notifications.setNotificationChannelAsync(channelName, {
68
+ name: channelName,
69
+ importance: Notifications.AndroidImportance.HIGH
70
+ });
71
+ }
72
+ const current = await Notifications.getPermissionsAsync();
73
+ const granted = current.granted || ask && (await Notifications.requestPermissionsAsync()).granted;
74
+ if (!granted) return null;
75
+ try {
76
+ return (await Notifications.getExpoPushTokenAsync({
77
+ projectId
78
+ })).data;
79
+ } catch {
80
+ // No google-services.json in this build: FCM cannot issue a token.
81
+ return null;
82
+ }
83
+ }
84
+ let foregroundHandlerSet = false;
85
+
86
+ /** Shows pushes as banners while the app is open. Runs once; the hook calls it. */
87
+ export function showPushesInForeground() {
88
+ const Notifications = loadNotifications();
89
+ if (!Notifications || foregroundHandlerSet) return;
90
+ foregroundHandlerSet = true;
91
+ Notifications.setNotificationHandler({
92
+ handleNotification: async () => ({
93
+ shouldShowBanner: true,
94
+ shouldShowList: true,
95
+ shouldPlaySound: true,
96
+ shouldSetBadge: false
97
+ })
98
+ });
99
+ }
100
+ function toTap(response) {
101
+ if (!response) return null;
102
+ const {
103
+ identifier,
104
+ content
105
+ } = response.notification.request;
106
+ const data = content.data;
107
+ return {
108
+ identifier,
109
+ data: typeof data === "object" && data !== null ? data : {},
110
+ response
111
+ };
112
+ }
113
+
114
+ /**
115
+ * Mount once under your auth gate. Registers this device's token through
116
+ * `register` and reports taps on pushes through `onNotificationTap`. Does
117
+ * nothing when a peer is missing. Navigation is yours: route from
118
+ * `tap.data` in the callback.
119
+ */
120
+ export function useExpoPushRegistration(options) {
121
+ const {
122
+ channelName,
123
+ ask = true,
124
+ showInForeground = true
125
+ } = options;
126
+ const latest = useRef(options);
127
+ latest.current = options;
128
+ useEffect(() => {
129
+ if (showInForeground) showPushesInForeground();
130
+ let cancelled = false;
131
+ void getExpoPushToken({
132
+ ask,
133
+ channelName
134
+ }).then(token => {
135
+ if (token && !cancelled) {
136
+ void latest.current.register({
137
+ token,
138
+ platform: Platform.OS === "ios" ? "ios" : "android"
139
+ });
140
+ }
141
+ });
142
+ return () => {
143
+ cancelled = true;
144
+ };
145
+ }, [ask, channelName, showInForeground]);
146
+ useEffect(() => {
147
+ const Notifications = loadNotifications();
148
+ if (!Notifications) return;
149
+ const seen = new Set();
150
+ const deliver = response => {
151
+ const tap = toTap(response);
152
+ if (!tap || seen.has(tap.identifier)) return;
153
+ seen.add(tap.identifier);
154
+ latest.current.onNotificationTap?.(tap);
155
+ };
156
+ let cancelled_0 = false;
157
+ void Notifications.getLastNotificationResponseAsync().then(response_0 => {
158
+ if (!cancelled_0) deliver(response_0);
159
+ });
160
+ const subscription = Notifications.addNotificationResponseReceivedListener(deliver);
161
+ return () => {
162
+ cancelled_0 = true;
163
+ subscription.remove();
164
+ };
165
+ }, []);
166
+ }
@@ -0,0 +1,70 @@
1
+ // The twin of `@vvedantb/ui`'s `formatCompactRelativeTime`
2
+ // (`ui/app/src/time/compactRelativeTime.ts`), which `RelativeDateTime` shows.
3
+ // The web file pulls no DOM, but the web package does, so this is a copy;
4
+ // tests/relativeTime.test.ts checks both give the same strings.
5
+
6
+ const MS_PER_MINUTE = 60_000;
7
+ const MS_PER_DAY = 86_400_000;
8
+
9
+ /** Rounds toward zero; `-0` becomes `0`. */
10
+ function truncate(n) {
11
+ return n < 0 ? Math.ceil(n) || 0 : Math.floor(n);
12
+ }
13
+
14
+ /** Local UTC offset in minutes, rounded to 15 minutes. */
15
+ function utcOffset(ms) {
16
+ return -Math.round(new Date(ms).getTimezoneOffset() / 15) * 15;
17
+ }
18
+
19
+ /** Adds calendar months in local time and clamps the day to the target month. */
20
+ function addMonths(ms, months) {
21
+ const date = new Date(ms);
22
+ const day = date.getDate();
23
+ date.setDate(1);
24
+ date.setMonth(date.getMonth() + months);
25
+ const daysInMonth = new Date(date.getFullYear(), date.getMonth() + 1, 0).getDate();
26
+ date.setDate(Math.min(day, daysInMonth));
27
+ return date.getTime();
28
+ }
29
+
30
+ /** Fractional calendar months from `a` to `b`, negated (the moment.js formula). */
31
+ function monthDiff(a, b) {
32
+ if (new Date(a).getDate() < new Date(b).getDate()) return -monthDiff(b, a);
33
+ const from = new Date(a);
34
+ const to = new Date(b);
35
+ const whole = (to.getFullYear() - from.getFullYear()) * 12 + (to.getMonth() - from.getMonth());
36
+ const anchor = addMonths(a, whole);
37
+ const before = b - anchor < 0;
38
+ const anchor2 = addMonths(a, whole + (before ? -1 : 1));
39
+ return +(-(whole + (b - anchor) / (before ? anchor - anchor2 : anchor2 - anchor)) || 0);
40
+ }
41
+
42
+ /**
43
+ * Time since `date` as one compact unit: `"now"` or `"42s"`, then `"5m"`,
44
+ * `"3h"`, `"2d"`, `"4mo"`, `"1y"`. Strings are parsed with `new Date(string)`.
45
+ * An invalid date gives `"NaNy"`, or `"0mo"` with `calendar`.
46
+ */
47
+ export function formatCompactRelativeTime(date, options = {}) {
48
+ const now = options.now ?? Date.now();
49
+ const then = new Date(date).getTime();
50
+ const diff = now - then;
51
+ const seconds = truncate(diff / 1000);
52
+ if (seconds < 60) return options.justNow ?? `${seconds}s`;
53
+ const minutes = truncate(diff / MS_PER_MINUTE);
54
+ if (minutes < 60) return `${minutes}m`;
55
+ const hours = truncate(diff / (60 * MS_PER_MINUTE));
56
+ if (hours < 24) return `${hours}h`;
57
+ if (!options.calendar) {
58
+ const days = Math.floor(hours / 24);
59
+ if (days < 30) return `${days}d`;
60
+ const months = Math.floor(days / 30);
61
+ if (months < 12) return `${months}mo`;
62
+ return `${Math.floor(months / 12)}y`;
63
+ }
64
+ const zoneDelta = (utcOffset(then) - utcOffset(now)) * MS_PER_MINUTE;
65
+ const days = truncate((diff - zoneDelta) / MS_PER_DAY);
66
+ if (days < 30) return `${days}d`;
67
+ const months = monthDiff(now, then);
68
+ if (truncate(months) < 12) return `${truncate(months)}mo`;
69
+ return `${truncate(months / 12)}y`;
70
+ }
@@ -0,0 +1,53 @@
1
+ import { c as _c } from "react/compiler-runtime";
2
+ import { useRouter } from "expo-router";
3
+ import { StackPageHeader } from "../StackPageHeader";
4
+ /** Goes back when the stack has a screen below, else replaces the route with `fallbackHref`. */
5
+ export function goBackOrReplace(router, fallbackHref = "/") {
6
+ if (router.canGoBack()) router.back();else router.replace(fallbackHref);
7
+ }
8
+
9
+ /**
10
+ * `StackPageHeader` with a router-aware back button: `router.canGoBack() ?
11
+ * router.back() : router.replace(fallbackHref)`. Same props as
12
+ * `StackPageHeader` without `onBack`, and `fallbackHref` (default `"/"`).
13
+ * On `@vvedantb/native-ui/expo-router` only, so the other entries never import
14
+ * `expo-router`.
15
+ */
16
+ export function RouterStackPageHeader(t0) {
17
+ const $ = _c(9);
18
+ let props;
19
+ let t1;
20
+ if ($[0] !== t0) {
21
+ ({
22
+ fallbackHref: t1,
23
+ ...props
24
+ } = t0);
25
+ $[0] = t0;
26
+ $[1] = props;
27
+ $[2] = t1;
28
+ } else {
29
+ props = $[1];
30
+ t1 = $[2];
31
+ }
32
+ const fallbackHref = t1 === undefined ? "/" : t1;
33
+ const router = useRouter();
34
+ let t2;
35
+ if ($[3] !== fallbackHref || $[4] !== router) {
36
+ t2 = () => goBackOrReplace(router, fallbackHref);
37
+ $[3] = fallbackHref;
38
+ $[4] = router;
39
+ $[5] = t2;
40
+ } else {
41
+ t2 = $[5];
42
+ }
43
+ let t3;
44
+ if ($[6] !== props || $[7] !== t2) {
45
+ t3 = <StackPageHeader {...props} onBack={t2} />;
46
+ $[6] = props;
47
+ $[7] = t2;
48
+ $[8] = t3;
49
+ } else {
50
+ t3 = $[8];
51
+ }
52
+ return t3;
53
+ }
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Accessibility and test props for a `Select` option. On web
3
+ * `@rn-primitives/select` passes the props of its `Item` through a `Slot` to
4
+ * the Radix `Select.Item`, a DOM `<div>`. React Native Web never sees them, so
5
+ * React logs `accessibilityLabel` and `testID` there as unknown DOM props. Web
6
+ * gets `aria-label` and `data-testid`; iOS and Android keep the native props.
7
+ * No React Native import, so the tests can load this file.
8
+ */
9
+
10
+ /**
11
+ * Spread on the `SelectItem`. Pass `Platform.OS` as `os`. Without `testID` the
12
+ * result has no test id key, not one set to `undefined`.
13
+ */
14
+ export function selectItemA11yProps({
15
+ label,
16
+ testID
17
+ }, os) {
18
+ if (os === "web") {
19
+ return testID === undefined ? {
20
+ "aria-label": label
21
+ } : {
22
+ "aria-label": label,
23
+ "data-testid": testID
24
+ };
25
+ }
26
+ return testID === undefined ? {
27
+ accessibilityLabel: label
28
+ } : {
29
+ accessibilityLabel: label,
30
+ testID
31
+ };
32
+ }
@@ -0,0 +1,168 @@
1
+ import { c as _c } from "react/compiler-runtime";
2
+ import { Linking, Platform, Text, View } from "react-native";
3
+ import { APP_ICONS } from "../brand/AppIcons";
4
+ import { AppMark } from "../brand/AppMark";
5
+ import { cn } from "../cn";
6
+ import { textStyle, typeStyle } from "../controlStyles";
7
+ import { ListRow } from "../ListRow";
8
+ import { tokens } from "../tokens";
9
+ import { useScheme } from "../useScheme";
10
+ import { SettingsSection } from "./SettingsSection";
11
+ import { openProjectVApp, otherProjectVMobileApps } from "./projectVMobileApps";
12
+
13
+ /**
14
+ * The mark for each phone app. V Store has no mark of its own (it was left out
15
+ * of the brand marks in 0.3.9), so it shows the Project V mark.
16
+ */
17
+ const MARK = {
18
+ vibot: "vibot",
19
+ verve: "verve",
20
+ velth: "velth",
21
+ vmem: "vmem",
22
+ vstore: "projectv"
23
+ };
24
+ /** A small muted "from Project V" line with the Project V mark, for the end of a settings screen. */
25
+ export function FromProjectV(t0) {
26
+ const $ = _c(19);
27
+ const {
28
+ className,
29
+ testID
30
+ } = t0;
31
+ const scheme = useScheme();
32
+ const muted = tokens.color.muted[scheme];
33
+ const ProjectVMark = APP_ICONS.projectv;
34
+ let t1;
35
+ if ($[0] !== className) {
36
+ t1 = cn("flex-row items-center justify-center gap-1.5 py-2", className);
37
+ $[0] = className;
38
+ $[1] = t1;
39
+ } else {
40
+ t1 = $[1];
41
+ }
42
+ let t2;
43
+ if ($[2] !== scheme) {
44
+ t2 = textStyle("muted", scheme);
45
+ $[2] = scheme;
46
+ $[3] = t2;
47
+ } else {
48
+ t2 = $[3];
49
+ }
50
+ let t3;
51
+ if ($[4] !== t2) {
52
+ t3 = <Text className="text-sm font-instrument-sans" style={t2}>from</Text>;
53
+ $[4] = t2;
54
+ $[5] = t3;
55
+ } else {
56
+ t3 = $[5];
57
+ }
58
+ let t4;
59
+ if ($[6] !== muted) {
60
+ t4 = <ProjectVMark size={14} color={muted} />;
61
+ $[6] = muted;
62
+ $[7] = t4;
63
+ } else {
64
+ t4 = $[7];
65
+ }
66
+ let t5;
67
+ if ($[8] !== scheme) {
68
+ t5 = textStyle("muted", scheme);
69
+ $[8] = scheme;
70
+ $[9] = t5;
71
+ } else {
72
+ t5 = $[9];
73
+ }
74
+ let t6;
75
+ if ($[10] === Symbol.for("react.memo_cache_sentinel")) {
76
+ t6 = typeStyle("semibold");
77
+ $[10] = t6;
78
+ } else {
79
+ t6 = $[10];
80
+ }
81
+ let t7;
82
+ if ($[11] !== t5) {
83
+ t7 = <Text className="text-sm font-instrument-sans-semibold" style={[t5, t6]}>Project V</Text>;
84
+ $[11] = t5;
85
+ $[12] = t7;
86
+ } else {
87
+ t7 = $[12];
88
+ }
89
+ let t8;
90
+ if ($[13] !== t1 || $[14] !== t3 || $[15] !== t4 || $[16] !== t7 || $[17] !== testID) {
91
+ t8 = <View testID={testID} accessible={true} accessibilityLabel="from Project V" className={t1}>{t3}{t4}{t7}</View>;
92
+ $[13] = t1;
93
+ $[14] = t3;
94
+ $[15] = t4;
95
+ $[16] = t7;
96
+ $[17] = testID;
97
+ $[18] = t8;
98
+ } else {
99
+ t8 = $[18];
100
+ }
101
+ return t8;
102
+ }
103
+ /**
104
+ * Settings section listing the other Project V phone apps, each with its mark,
105
+ * name and one line. A row opens the app if it is installed, else V Store's
106
+ * home, else its website (`openProjectVApp`).
107
+ */
108
+ export function MoreFromProjectV(t0) {
109
+ const $ = _c(13);
110
+ const {
111
+ current,
112
+ variant: t1,
113
+ title: t2,
114
+ onOpen,
115
+ className
116
+ } = t0;
117
+ const variant = t1 === undefined ? "section" : t1;
118
+ const title = t2 === undefined ? "More from Project V" : t2;
119
+ if (variant === "footer") {
120
+ let t3;
121
+ if ($[0] !== className) {
122
+ t3 = <FromProjectV className={className} />;
123
+ $[0] = className;
124
+ $[1] = t3;
125
+ } else {
126
+ t3 = $[1];
127
+ }
128
+ return t3;
129
+ }
130
+ let t3;
131
+ if ($[2] !== current || $[3] !== onOpen) {
132
+ let t4;
133
+ if ($[5] !== onOpen) {
134
+ t4 = app => <ListRow key={app.id} testID={`more-from-project-v-${app.id}`} title={app.name} meta={app.description} leading={<AppMark app={MARK[app.id]} size={28} />} accessibilityLabel={`${app.name}, ${app.description}`} onPress={() => {
135
+ openProjectVApp(app.id, Linking, Platform.OS).then(target => onOpen?.(app.id, target));
136
+ }} />;
137
+ $[5] = onOpen;
138
+ $[6] = t4;
139
+ } else {
140
+ t4 = $[6];
141
+ }
142
+ t3 = otherProjectVMobileApps(current).map(t4);
143
+ $[2] = current;
144
+ $[3] = onOpen;
145
+ $[4] = t3;
146
+ } else {
147
+ t3 = $[4];
148
+ }
149
+ let t4;
150
+ if ($[7] !== t3) {
151
+ t4 = <View className="flex-col gap-1 p-1">{t3}</View>;
152
+ $[7] = t3;
153
+ $[8] = t4;
154
+ } else {
155
+ t4 = $[8];
156
+ }
157
+ let t5;
158
+ if ($[9] !== className || $[10] !== t4 || $[11] !== title) {
159
+ t5 = <SettingsSection title={title} bodyVariant="list" className={className}>{t4}</SettingsSection>;
160
+ $[9] = className;
161
+ $[10] = t4;
162
+ $[11] = title;
163
+ $[12] = t5;
164
+ } else {
165
+ t5 = $[12];
166
+ }
167
+ return t5;
168
+ }
@@ -0,0 +1,102 @@
1
+ import { c as _c } from "react/compiler-runtime";
2
+ import { Text, View } from "react-native";
3
+ import { sizeStyle, textStyle, typeStyle } from "../controlStyles";
4
+ import { useLargerText } from "../theme";
5
+ import { tokens } from "../tokens";
6
+ import { useScheme } from "../useScheme";
7
+ import { relaxedSizeStyle } from "./relaxedSizeStyle";
8
+ /**
9
+ * The empty note for the inside of a `SettingsSection` (web
10
+ * `SettingsEmptyState`): an optional muted icon (28, half opacity), a centred
11
+ * `text-sm` medium title (14 / 20 / 500), a muted `text-xs leading-relaxed`
12
+ * description (12 / 19.5) and an optional action, in `px-4 py-16`. Use
13
+ * `EmptyState` for a whole screen.
14
+ */
15
+ export function SettingsEmptyState(t0) {
16
+ const $ = _c(24);
17
+ const {
18
+ title,
19
+ description,
20
+ icon: Icon,
21
+ action,
22
+ testID
23
+ } = t0;
24
+ const scheme = useScheme();
25
+ const largerText = useLargerText();
26
+ let t1;
27
+ if ($[0] !== Icon || $[1] !== largerText || $[2] !== scheme) {
28
+ t1 = Icon ? <View className="mb-5 opacity-50"><Icon size={largerText ? 32 : 28} color={tokens.color.muted[scheme]} /></View> : null;
29
+ $[0] = Icon;
30
+ $[1] = largerText;
31
+ $[2] = scheme;
32
+ $[3] = t1;
33
+ } else {
34
+ t1 = $[3];
35
+ }
36
+ let t2;
37
+ if ($[4] !== scheme) {
38
+ t2 = textStyle("foreground", scheme);
39
+ $[4] = scheme;
40
+ $[5] = t2;
41
+ } else {
42
+ t2 = $[5];
43
+ }
44
+ let t3;
45
+ let t4;
46
+ if ($[6] === Symbol.for("react.memo_cache_sentinel")) {
47
+ t3 = sizeStyle("text-sm");
48
+ t4 = typeStyle("medium");
49
+ $[6] = t3;
50
+ $[7] = t4;
51
+ } else {
52
+ t3 = $[6];
53
+ t4 = $[7];
54
+ }
55
+ let t5;
56
+ if ($[8] !== t2) {
57
+ t5 = [t2, t3, t4];
58
+ $[8] = t2;
59
+ $[9] = t5;
60
+ } else {
61
+ t5 = $[9];
62
+ }
63
+ let t6;
64
+ if ($[10] !== t5 || $[11] !== title) {
65
+ t6 = <Text className="text-center font-instrument-sans-medium text-sm" style={t5}>{title}</Text>;
66
+ $[10] = t5;
67
+ $[11] = title;
68
+ $[12] = t6;
69
+ } else {
70
+ t6 = $[12];
71
+ }
72
+ let t7;
73
+ if ($[13] !== description || $[14] !== scheme) {
74
+ t7 = description ? <Text className="mt-2 max-w-sm text-center font-instrument-sans text-xs leading-relaxed" style={[textStyle("muted", scheme), relaxedSizeStyle("text-xs")]}>{description}</Text> : null;
75
+ $[13] = description;
76
+ $[14] = scheme;
77
+ $[15] = t7;
78
+ } else {
79
+ t7 = $[15];
80
+ }
81
+ let t8;
82
+ if ($[16] !== action) {
83
+ t8 = action ? <View className="mt-6">{action}</View> : null;
84
+ $[16] = action;
85
+ $[17] = t8;
86
+ } else {
87
+ t8 = $[17];
88
+ }
89
+ let t9;
90
+ if ($[18] !== t1 || $[19] !== t6 || $[20] !== t7 || $[21] !== t8 || $[22] !== testID) {
91
+ t9 = <View testID={testID} className="items-center justify-center px-4 py-16">{t1}{t6}{t7}{t8}</View>;
92
+ $[18] = t1;
93
+ $[19] = t6;
94
+ $[20] = t7;
95
+ $[21] = t8;
96
+ $[22] = testID;
97
+ $[23] = t9;
98
+ } else {
99
+ t9 = $[23];
100
+ }
101
+ return t9;
102
+ }